Skip to content

[TypeScript client] feat: RPC & in-memory clients#9

Draft
0xpolarzero wants to merge 20 commits into
typed-client-runtime-foundationfrom
typed-client-public-surface
Draft

[TypeScript client] feat: RPC & in-memory clients#9
0xpolarzero wants to merge 20 commits into
typed-client-runtime-foundationfrom
typed-client-public-surface

Conversation

@0xpolarzero
Copy link
Copy Markdown
Owner

@0xpolarzero 0xpolarzero commented May 25, 2026

This PR adds the public typed client surface on top of the runtime and transport foundation from PR #8.

API

Full API example is available in this gist.

HTTP clients call a served CLI through a HttpTransport, while exposing a command-aware client API:

import { HttpClient } from 'incur/client'
import type { Commands } from './generated/incur-client.js'

const client = HttpClient.create<Commands>({
  baseUrl: 'https://ops.example.com',
  headers: { authorization: 'Bearer token' },
  outputFormat: 'toon',
})

const result = await client.run('project status', {
  args: { id: 'proj_123' },
  options: { verbose: true },
})

console.log(result.data)
console.log(result.output?.text)

The command name, args, options, and data type all come from Commands.

When the CLI is available in the same process, memory clients infer command names, args, and options directly from the concrete CLI:

import { Cli, z } from 'incur'
import { MemoryClient } from 'incur/client'

const cli = Cli.create('ops').command('project status', {
  args: z.object({ id: z.string() }),
  output: z.object({ id: z.string(), status: z.string() }),
  run: ({ args }) => ({ id: args.id, status: 'ready' }),
})

const client = MemoryClient.create(cli)

const result = await client.run('project status', {
  args: { id: 'proj_123' },
})

Generated Commands declarations still provide the richest client surface, including declared output data and streaming metadata.

Callers can also create a client directly from a transport factory:

import { Client, HttpTransport } from 'incur/client'

const client = Client.create<Commands>({
  transport: HttpTransport.create({ baseUrl: 'https://ops.example.com' }),
  selection: ['status'],
})

Streaming

Streaming commands return an async iterable for chunks, plus a final promise for the terminal result:

const logs = await client.run('logs tail', {
  args: { service: 'api' },
})

for await (const line of logs) {
  console.log(line)
}

const final = await logs.final
console.log(final.meta.duration)

Callers that need raw protocol records can use records():

for await (const record of logs.records()) {
  if (record.type === 'chunk') console.log(record.data)
  if (record.type === 'done') console.log(record.data, record.meta)
  if (record.type === 'error') console.error(record.error)
}

Resources

The public client exposes resource discovery as methods rather than raw transport.discover(...) requests:

const help = await client.help('project status')
const schema = await client.schema('project status')
const openapi = await client.openapi()
const tools = await client.mcp.tools()

const llmsMarkdown = await client.llms({
  command: 'project',
  format: 'md',
})

Resource scopes are typed from the command map, so command groups such as 'project' and leaf commands such as 'project status' are accepted while unknown scopes are rejected.

Local methods

Memory clients also expose local-only setup methods. These are intentionally not available on HTTP clients.

await client.skills.list()
await client.skills.add({ depth: 1, global: true })
await client.mcp.add({ agents: ['codex'] })

The skills and mcp namespaces merge resource methods and memory-local methods on memory clients:

await client.skills.index()
await client.skills.add()
await client.mcp.tools()
await client.mcp.add()

Type generation

Typegen now emits an exported Commands type and augments both incur and incur/client with the same command map:

export type Commands = {
  'project status': {
    args: { id: string }
    options: { verbose: boolean }
    output: { id: string; status: string }
  }
}

declare module 'incur/client' {
  interface Register {
    commands: Commands
  }
}

That lets users either pass Commands explicitly:

const client = HttpClient.create<Commands>({ baseUrl })

or rely on module augmentation when the generated declarations are loaded:

const client = HttpClient.create({ baseUrl })
await client.run('project status', { args: { id: 'proj_123' } })

Changes

  • Adds the public incur/client namespace surface for Client, HttpClient, MemoryClient, Run, Resources, Local, Rpc, Transport, HttpTransport, and MemoryTransport.
  • Adds Client.create(...), HttpClient.create(...), and MemoryClient.create(...).
  • Adds typed client.run(...) with command-name inference, required input enforcement, exact top-level/inner input key checks, output selection behavior, pagination helpers, CTA follow-up runs, and streaming return types.
  • Adds resource methods for llms, llmsFull, schema, help, openapi, generated skills, and MCP tools.
  • Adds memory-only local methods for generated skill syncing/listing and MCP registration.
  • Moves public run/resource/local/client types into their owning namespace modules instead of a shared type bag.
  • Organizes action implementations under client/actions/*Actions.ts.
  • Updates ClientError and transport error handling to use the canonical Rpc.Error type.
  • Updates typegen to export a reusable Commands type and augment incur/client.
  • Adds incur/client to TypeScript path resolution.

Tests

  • Adds dedicated runtime coverage for Client.create, HttpClient.create, MemoryClient.create, run actions, resource actions, local actions, and transport/client composition.
  • Adds public type tests for Client, HttpClient, MemoryClient, Run, Resources, Local, and Transport.
  • The client test suite covers command input inference, required args/options, extra-key rejection, selected-output fallback to unknown, streaming commands, resource scope narrowing, local-only method visibility, and transport capability types.

Notes

  • This PR intentionally keeps the transport layer from PR [TypeScript client] feat: RPC & in-memory transports #8 intact and builds the public client layer on top.
  • HttpClient.create(...) exposes resource methods but not memory-local methods.
  • MemoryClient.create(cli) exposes the same resource methods and also local skills.add/list and mcp.add.
  • MemoryClient.create(cli) can infer command names, args, and options from a concrete CLI; callers can still pass an explicit command map when they need declared output or stream metadata.
  • Streaming commands reject token pagination controls because stream chunks are consumed incrementally.
  • Resource methods use command scopes derived from the command map, so group scopes such as 'project' narrow LLM manifests to that subtree.

@0xpolarzero 0xpolarzero changed the title feat: add typed client public surface feat: Typescript public client May 25, 2026
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from 0204b74 to d0b8c5c Compare May 25, 2026 17:42
@0xpolarzero 0xpolarzero force-pushed the typed-client-public-surface branch from a910a36 to 0a2be78 Compare May 25, 2026 17:42
Copy link
Copy Markdown
Owner Author

0xpolarzero commented May 25, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from d0b8c5c to d5267b8 Compare May 26, 2026 07:49
@0xpolarzero 0xpolarzero force-pushed the typed-client-public-surface branch 4 times, most recently from 3085f50 to 52cec2a Compare May 26, 2026 11:53
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from 1756ff4 to 8e074ab Compare May 26, 2026 11:53
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from 8e074ab to 51a3a8d Compare May 26, 2026 18:06
@0xpolarzero 0xpolarzero force-pushed the typed-client-public-surface branch from 52cec2a to a0b2d92 Compare May 26, 2026 18:07
@0xpolarzero 0xpolarzero changed the base branch from typed-client-runtime-foundation to graphite-base/9 May 27, 2026 11:02
@0xpolarzero 0xpolarzero changed the title feat: Typescript public client [TypeScript client] feat: RPC & in-memory clients May 27, 2026
@0xpolarzero 0xpolarzero force-pushed the typed-client-public-surface branch from a0b2d92 to d3705f9 Compare May 27, 2026 13:03
@0xpolarzero 0xpolarzero changed the base branch from graphite-base/9 to typed-client-runtime-foundation May 27, 2026 13:03
@0xpolarzero 0xpolarzero force-pushed the typed-client-public-surface branch 4 times, most recently from 2518d21 to ef926cd Compare May 27, 2026 16:48
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from 97a9ab0 to e2182b7 Compare May 27, 2026 16:48
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from e2182b7 to c0ddef3 Compare May 27, 2026 17:21
@0xpolarzero 0xpolarzero force-pushed the typed-client-public-surface branch from ef926cd to 0dc678e Compare May 27, 2026 17:21
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from c0ddef3 to e1db698 Compare May 27, 2026 17:23
@0xpolarzero 0xpolarzero force-pushed the typed-client-public-surface branch from 0dc678e to a0219ef Compare May 27, 2026 17:23
@0xpolarzero 0xpolarzero force-pushed the typed-client-public-surface branch 5 times, most recently from a09a13f to 118f835 Compare May 27, 2026 23:26
@0xpolarzero 0xpolarzero force-pushed the typed-client-public-surface branch from ba9090d to b441dc7 Compare May 27, 2026 23:38
@0xpolarzero 0xpolarzero marked this pull request as ready for review May 27, 2026 23:40
@0xpolarzero 0xpolarzero marked this pull request as draft May 27, 2026 23:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant