Skip to content

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

Open
0xpolarzero wants to merge 38 commits into
wevm:mainfrom
0xpolarzero:typed-client-runtime-foundation
Open

[TypeScript client] feat: RPC & in-memory transports#153
0xpolarzero wants to merge 38 commits into
wevm:mainfrom
0xpolarzero:typed-client-runtime-foundation

Conversation

@0xpolarzero

@0xpolarzero 0xpolarzero commented May 27, 2026

Copy link
Copy Markdown
Contributor

Warning

This PR is stacked on top of #154 (fix: reuse CLI skill projection). Because wevm/incur does not have that base branch and this PR targets main, the diff includes the commits from that prerequisite branch as well.

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.

Check the PR on my fork for reviewing from a stacked diff.

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 marked this pull request as draft May 27, 2026 15:10
@0xpolarzero 0xpolarzero force-pushed the typed-client-runtime-foundation branch 4 times, most recently 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 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:42
@0xpolarzero 0xpolarzero marked this pull request as ready for review May 28, 2026 13:30
# Conflicts:
#	src/Cli.test.ts
#	src/Cli.ts
#	src/Skillgen.test.ts
#	src/Skillgen.ts
#	src/internal/command.ts
@pkg-pr-new

pkg-pr-new Bot commented Jun 29, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/incur@153

commit: c0b39b1

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.50549% with 35 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@4de8518). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/Cli.ts 91.93% 9 Missing and 1 partial ⚠️
src/internal/handlers/rpc.ts 92.06% 9 Missing and 1 partial ⚠️
src/client/transports/MemoryTransport.ts 68.96% 9 Missing ⚠️
src/client/transports/HttpTransport.ts 96.94% 4 Missing ⚠️
src/internal/runtime-context.ts 97.10% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #153   +/-   ##
=======================================
  Coverage        ?   94.62%           
=======================================
  Files           ?       34           
  Lines           ?     4169           
  Branches        ?     1672           
=======================================
  Hits            ?     3945           
  Misses          ?      189           
  Partials        ?       35           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@0xpolarzero

Copy link
Copy Markdown
Contributor Author

@jxom these are ready for review again, I fixed the conflicts if that's what was blocking.

Also added #177 to fix some flakiness below in the stack, as this PR was failing CI.

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