Skip to content

[TypeScript client] feat: RPC & in-memory transports#8

Draft
0xpolarzero wants to merge 26 commits into
fix/skillgen-syncskills-cli-projectionfrom
typed-client-runtime-foundation
Draft

[TypeScript client] feat: RPC & in-memory transports#8
0xpolarzero wants to merge 26 commits into
fix/skillgen-syncskills-cli-projectionfrom
typed-client-runtime-foundation

Conversation

@0xpolarzero
Copy link
Copy Markdown
Owner

@0xpolarzero 0xpolarzero commented May 25, 2026

This PR adds the runtime and transport foundation that lets TypeScript code call an incur CLI through the same structured command protocol, whether the CLI is served over HTTP or used in process.

API

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

const cli = Cli.create('app').command('project status', {
  args: z.object({ id: z.string() }),
  options: z.object({ verbose: z.boolean().default(false) }),
  run: ({ args, options }) => ({
    id: args.id,
    status: 'ready',
    verbose: options.verbose,
  }),
})

const transport = MemoryTransport.create(cli)()

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

if (!('stream' in result) && result.ok) {
  console.log(result.data)
}

The same request shape works against a served CLI:

import { HttpTransport } from 'incur/client'

const transport = HttpTransport.create({
  baseUrl: 'https://example.com',
  headers: { authorization: 'Bearer token' },
})()

const result = await transport.request({
  command: 'project status',
  args: { id: 'proj_123' },
  options: { verbose: true },
  outputFormat: 'json',
  selection: ['status'],
  outputTokenCount: true,
  outputTokenLimit: 500,
})

HTTP clients can also call the RPC endpoint directly:

const response = await fetch('https://example.com/_incur/rpc', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    command: 'project status',
    args: { id: 'proj_123' },
    options: { verbose: true },
    outputFormat: 'json',
  }),
})

const result = await response.json()

Streaming commands return NDJSON over HTTP and an async record iterator in transports:

const result = await transport.request({ command: 'logs tail' })

if ('stream' in result) {
  for await (const record of result.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)
  }
}

Resource discovery uses the same transport contract for help, schemas, OpenAPI, skills, LLM manifests, and MCP tools:

await transport.discover({ resource: 'help', command: 'project status' })
await transport.discover({ resource: 'schema', command: 'project status' })
await transport.discover({ resource: 'llms' })
await transport.discover({ resource: 'llmsFull', format: 'json' })
await transport.discover({ resource: 'openapi', format: 'yaml' })
await transport.discover({ resource: 'skillsIndex' })
await transport.discover({ resource: 'skill', name: 'project-status' })
await transport.discover({ resource: 'mcpTools' })

Memory transports also expose local-only setup helpers:

await transport.local.skills.list()
await transport.local.skills.add({ depth: 1 })
await transport.local.mcp.add({ command: 'app' })

Changes

  • Adds the incur/client export with ClientError, Rpc, Resources, Local, Transport, HttpTransport, and MemoryTransport.
  • Adds a shared in-process RPC handler that executes canonical command IDs with structured args and options.
  • Adds a memory transport for in-process CLIs without going through cli.fetch().
  • Adds an HTTP transport that posts structured requests to /_incur/rpc.
  • Adds a shared in-process resources handler for help, schemas, OpenAPI, LLM manifests, generated skills, and MCP tools.
  • Adds streaming support that preserves chunk, terminal done, terminal error, metadata, retryability, and cancellation.
  • Adds ClientError so transport and resource-discovery failures carry code, status, data, fieldErrors, and metadata when available.

Notes

  • This is the runtime/transport layer, not the final high-level typed client API. Callers use transport.request({ command, args, options }); a later PR builds the public generated/client-facing API on top.
  • Structured RPC accepts canonical command IDs only. Aliases are rejected as unknown commands, command groups return COMMAND_GROUP, and raw fetch gateways return FETCH_GATEWAY.
  • RPC handler execution preserves command resolution, Zod validation, middleware, env validation, vars, formatted output, output selections, token metadata, CTAs, and streaming terminal records.
  • MemoryTransport.create(cli, { env }) uses explicit env and does not load config defaults.
  • HttpTransport sends OpenAPI resource requests to /openapi.json or /openapi.yaml; other discovery resources use /_incur/*.

Notable side effects

  • Typegen now includes callable root commands. Previously Typegen.fromCli() only walked the subcommand map, so a root CLI created with Cli.create('name', { run }) could be missing from generated command declarations.
  • Typegen now uses the shared runtime context's structured command collection, so generated command declarations follow the same root-command, alias, fetch-gateway, and group traversal rules as RPC/discovery.
  • Typegen now emits exact optional properties (?: T | undefined) and escapes generated command/property keys when they are not valid bare TypeScript identifiers.
  • Typegen now emits declared command output schemas and stream: true markers for async generator commands.
  • Cli.command('api', { fetch, openapi }) now generates OpenAPI-backed command groups synchronously, so mounted OpenAPI operations are available before serving.
  • OpenAPI-mounted operations are now reflected in the CLI command map type via Openapi.Commands<name, spec>.
  • Openapi.generateCommands() now includes JSON response schemas as command output schemas when available.
  • OpenAPI fallback command names changed from underscore-style names to space-delimited command names, for example get users posts.
  • Command.execute() now supports parseMode: 'structured' for RPC requests where args and options are already separated.
  • Several CLI internals and guards are exported so the runtime context can reuse the CLI's command-entry shapes instead of duplicating them.
  • Route coverage moved into the broader CLI and transport tests, with added assertions for RPC status mapping, resource-discovery error envelopes, duration metadata, NDJSON parsing, truncated streams, and cancellation.

@0xpolarzero 0xpolarzero changed the title add typed client runtime foundation feat: TypeScript client runtime May 25, 2026
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from 0204b74 to d0b8c5c 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 changed the base branch from parse-object-validation to graphite-base/8 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 changed the base branch from graphite-base/8 to stream-terminal-command-results May 26, 2026 11:53
@0xpolarzero 0xpolarzero force-pushed the stream-terminal-command-results branch from 225bea7 to 53fdfcf Compare May 26, 2026 17:41
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from 8e074ab to 51a3a8d Compare May 26, 2026 18:06
@0xpolarzero 0xpolarzero changed the base branch from stream-terminal-command-results to graphite-base/8 May 26, 2026 18:11
@0xpolarzero 0xpolarzero changed the base branch from graphite-base/8 to stream-terminal-command-results May 26, 2026 20:00
@0xpolarzero 0xpolarzero changed the base branch from stream-terminal-command-results to graphite-base/8 May 27, 2026 10:54
@0xpolarzero 0xpolarzero changed the base branch from graphite-base/8 to stream-terminal-command-results May 27, 2026 11:02
@0xpolarzero 0xpolarzero changed the base branch from stream-terminal-command-results to main May 27, 2026 12:11
@0xpolarzero 0xpolarzero changed the base branch from main to stream-terminal-command-results May 27, 2026 12:11
@0xpolarzero 0xpolarzero changed the title feat: TypeScript client runtime [TypeScript client] feat: RPC & in-memory transports May 27, 2026
@0xpolarzero 0xpolarzero marked this pull request as ready for review May 27, 2026 13:03
@0xpolarzero 0xpolarzero marked this pull request as draft May 27, 2026 15:10
@0xpolarzero 0xpolarzero changed the base branch from stream-terminal-command-results to graphite-base/8 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 changed the base branch from graphite-base/8 to fix/skillgen-syncskills-cli-projection May 27, 2026 16:49
@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 fix/skillgen-syncskills-cli-projection branch from c677c76 to 4a8351c Compare May 27, 2026 17:21
@0xpolarzero 0xpolarzero force-pushed the fix/skillgen-syncskills-cli-projection branch from 4a8351c to f73324e Compare May 27, 2026 17:23
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from e1db698 to 2f25ed9 Compare May 27, 2026 17:26
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch from 2f25ed9 to 0be074c Compare May 27, 2026 18:50
@0xpolarzero 0xpolarzero force-pushed the fix/skillgen-syncskills-cli-projection branch from 8bed02c to 375c0eb Compare May 27, 2026 18:50
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