agent-shim 8.28.0 → 8.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -5028,10 +5028,113 @@ function createRcCredentialStore(fs12, dir) {
5028
5028
  // src/frontdoor/rcApi.ts
5029
5029
  import { createORPCClient, ORPCError } from "@orpc/client";
5030
5030
  import { RPCLink } from "@orpc/client/fetch";
5031
+ import { OpenAPIGenerator } from "@orpc/openapi";
5032
+ import { OpenAPIHandler } from "@orpc/openapi/node";
5031
5033
  import { eventIterator, os as os2, withEventMeta } from "@orpc/server";
5032
5034
  import { BodyLimitPlugin, RPCHandler } from "@orpc/server/node";
5033
5035
  import { Agent as Agent3, fetch as undiciFetch } from "undici";
5034
5036
 
5037
+ // package.json
5038
+ var package_default = {
5039
+ name: "agent-shim",
5040
+ version: "8.30.0",
5041
+ description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5042
+ license: "MIT",
5043
+ author: "Joseph Mearman <joseph@mearman.co.uk>",
5044
+ repository: {
5045
+ type: "git",
5046
+ url: "git+https://github.com/ExaDev/agent-shim.git"
5047
+ },
5048
+ homepage: "https://github.com/ExaDev/agent-shim#readme",
5049
+ bugs: "https://github.com/ExaDev/agent-shim/issues",
5050
+ keywords: [
5051
+ "claude-code",
5052
+ "claude",
5053
+ "cli"
5054
+ ],
5055
+ packageManager: "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
5056
+ type: "module",
5057
+ engines: {
5058
+ node: "^22.18 || >=24"
5059
+ },
5060
+ bin: {
5061
+ "agent-shim": "dist/cli.cjs",
5062
+ "claude-use": "dist/cli.cjs"
5063
+ },
5064
+ files: [
5065
+ "dist/cli.cjs",
5066
+ "dist/index.mjs",
5067
+ "dist/index.cjs",
5068
+ "dist/types"
5069
+ ],
5070
+ scripts: {
5071
+ prepare: "husky && node scripts/build.mts --bundle-only",
5072
+ typecheck: "turbo run _typecheck",
5073
+ _typecheck: "tsc --noEmit",
5074
+ lint: "turbo run _lint",
5075
+ _lint: "eslint . --cache --cache-location .eslintcache --max-warnings 0",
5076
+ test: "turbo run _test",
5077
+ _test: "vitest run",
5078
+ "test:coverage": "turbo run _test:coverage",
5079
+ "_test:coverage": "vitest run --coverage",
5080
+ knip: "turbo run _knip",
5081
+ _knip: "knip --cache",
5082
+ build: "turbo run _build",
5083
+ _build: "node scripts/build.mts",
5084
+ "build:bundle": "node scripts/build.mts --bundle-only",
5085
+ prepublishOnly: "pnpm build:bundle",
5086
+ schema: "node scripts/gen-schema.mts"
5087
+ },
5088
+ dependencies: {
5089
+ "@clack/prompts": "^1.8.0",
5090
+ "@orpc/client": "1.15.4",
5091
+ "@orpc/openapi": "1.15.4",
5092
+ "@orpc/server": "1.15.4",
5093
+ commander: "^15.0.0",
5094
+ cosmiconfig: "^10.0.1",
5095
+ picomatch: "^4.0.7",
5096
+ undici: "7.29.1",
5097
+ zod: "^4.6.1"
5098
+ },
5099
+ devDependencies: {
5100
+ "@commitlint/cli": "^21.2.2",
5101
+ "@commitlint/config-conventional": "^21.2.2",
5102
+ "@exadev/eslint-config": "2.12.1",
5103
+ "@semantic-release/changelog": "^7.0.0",
5104
+ "@semantic-release/git": "^11.0.1",
5105
+ "@semantic-release/npm": "^13.1.5",
5106
+ "@types/node": "^26.5.1",
5107
+ "@types/picomatch": "^4.0.3",
5108
+ "@vitest/coverage-v8": "^4.1.11",
5109
+ "conventional-changelog-conventionalcommits": "^10.4.0",
5110
+ esbuild: "^0.28.2",
5111
+ eslint: "^10.10.0",
5112
+ globals: "^17.12.0",
5113
+ husky: "^9.1.7",
5114
+ jiti: "^2.7.0",
5115
+ knip: "^6.35.1",
5116
+ "lint-staged": "^17.5.0",
5117
+ rollup: "4.64.0",
5118
+ "rollup-plugin-dts": "6.5.1",
5119
+ "semantic-release": "^25.0.9",
5120
+ turbo: "^2.11.5",
5121
+ typescript: "^6.0.3",
5122
+ "typescript-eslint": "^8.70.0",
5123
+ vitest: "^4.1.11"
5124
+ },
5125
+ main: "dist/index.cjs",
5126
+ module: "dist/index.mjs",
5127
+ types: "dist/types/index.d.ts",
5128
+ exports: {
5129
+ ".": {
5130
+ types: "./dist/types/index.d.ts",
5131
+ import: "./dist/index.mjs",
5132
+ require: "./dist/index.cjs"
5133
+ },
5134
+ "./package.json": "./package.json"
5135
+ }
5136
+ };
5137
+
5035
5138
  // src/frontdoor/rcSchemas.ts
5036
5139
  import { z as z4 } from "zod";
5037
5140
  var RcPermissionModeSchema = z4.enum(RC_PERMISSION_MODES);
@@ -5193,18 +5296,23 @@ var RcAnswerOutputSchema = z4.strictObject({
5193
5296
 
5194
5297
  // src/frontdoor/rcApi.ts
5195
5298
  var RC_ORPC_PATH_PREFIX = "/__agent-shim/orpc";
5299
+ var RC_OPENAPI_DOC_PATH = "/openapi.json";
5196
5300
  var RC_SUBSCRIBER_BUFFER_EVENTS = 100;
5301
+ function doorApiBearer(headers) {
5302
+ const presented = headers.authorization;
5303
+ const bearerPrefix = "bearer ".length;
5304
+ return typeof presented === "string" && presented.slice(0, bearerPrefix).toLowerCase() === "bearer " ? presented.slice(bearerPrefix) : void 0;
5305
+ }
5197
5306
  function doorApiAuth(expectedToken) {
5198
5307
  return os2.$context().use(({ context, next }) => {
5199
- const presented = context.headers.authorization;
5200
- const bearerPrefix = "bearer ".length;
5201
- const token = typeof presented === "string" && presented.slice(0, bearerPrefix).toLowerCase() === "bearer " ? presented.slice(bearerPrefix) : void 0;
5308
+ const token = doorApiBearer(context.headers);
5202
5309
  if (token === void 0 || !isLiveCapability(token, [expectedToken])) {
5203
5310
  throw new ORPCError("UNAUTHORIZED", { message: "the front door's typed API demands this generation's control token as a Bearer credential" });
5204
5311
  }
5205
5312
  return next();
5206
5313
  });
5207
5314
  }
5315
+ var RC_API_TAG = "remote-control";
5208
5316
  function createRcApiRouter(deps) {
5209
5317
  const authed = doorApiAuth(deps.expectedToken);
5210
5318
  const delivered = (sessionId, result) => {
@@ -5224,41 +5332,41 @@ function createRcApiRouter(deps) {
5224
5332
  };
5225
5333
  return {
5226
5334
  rc: {
5227
- list: authed.output(RcListOutputSchema).handler(() => ({ sessions: deps.list() })),
5228
- status: authed.input(RcSessionQuerySchema).output(RcStatusOutputSchema).handler(({ input }) => {
5335
+ list: authed.route({ method: "GET", path: "/rest/rc/sessions", summary: "List the observed Remote Control sessions", tags: [RC_API_TAG] }).output(RcListOutputSchema).handler(() => ({ sessions: deps.list() })),
5336
+ status: authed.route({ method: "GET", path: "/rest/rc/status", summary: "Read session status, one session's when named", tags: [RC_API_TAG] }).input(RcSessionQuerySchema).output(RcStatusOutputSchema).handler(({ input }) => {
5229
5337
  const statuses = deps.statusOf(input.session);
5230
5338
  if (input.session !== void 0 && !statuses.some((entry) => entry.id === input.session)) {
5231
5339
  throw new ORPCError("NOT_FOUND", { message: rcSessionNotObservedMessage(input.session) });
5232
5340
  }
5233
5341
  return { statuses };
5234
5342
  }),
5235
- pending: authed.input(RcSessionQuerySchema).output(RcPendingOutputSchema).handler(({ input }) => {
5343
+ pending: authed.route({ method: "GET", path: "/rest/rc/pending", summary: "Read the control requests awaiting an answer", tags: [RC_API_TAG] }).input(RcSessionQuerySchema).output(RcPendingOutputSchema).handler(({ input }) => {
5236
5344
  const pending = deps.pendingOf(input.session);
5237
5345
  if (input.session !== void 0 && deps.statusOf(input.session).length === 0) {
5238
5346
  throw new ORPCError("NOT_FOUND", { message: rcSessionNotObservedMessage(input.session) });
5239
5347
  }
5240
5348
  return { pending };
5241
5349
  }),
5242
- send: authed.input(RcSendInputSchema).output(RcWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, sequenceNums: delivered(input.session, await deps.inject(input.session, input.text)) })),
5243
- answer: authed.input(RcAnswerInputSchema).output(RcAnswerOutputSchema).handler(async ({ input }) => {
5350
+ send: authed.route({ method: "POST", path: "/rest/rc/send", summary: "Send one message into a session", tags: [RC_API_TAG] }).input(RcSendInputSchema).output(RcWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, sequenceNums: delivered(input.session, await deps.inject(input.session, input.text)) })),
5351
+ answer: authed.route({ method: "POST", path: "/rest/rc/answer", summary: "Answer one pending control request", tags: [RC_API_TAG] }).input(RcAnswerInputSchema).output(RcAnswerOutputSchema).handler(async ({ input }) => {
5244
5352
  const decision = { approve: input.approve, message: input.approve || input.text === void 0 ? void 0 : input.text };
5245
5353
  return { session: input.session, request: input.request, sequenceNums: delivered(input.session, await deps.answer(input.session, input.request, decision)) };
5246
5354
  }),
5247
- interrupt: authed.input(RcInterruptInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.interrupt(input.session)) })),
5248
- setModel: authed.input(RcSetModelInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.setModel(input.session, input.model)) })),
5249
- setPermissionMode: authed.input(RcSetPermissionModeInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.setPermissionMode(input.session, input.mode)) })),
5250
- endSession: authed.input(RcEndSessionInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.endSession(input.session, input.reason)) })),
5251
- getUsage: authed.input(RcGetUsageInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.getUsage(input.session, input.skipBehaviors)) })),
5252
- getContextUsage: authed.input(RcGetContextUsageInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.getContextUsage(input.session, input.detail)) })),
5253
- readFile: authed.input(RcReadFileInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.readFile(input.session, input.path, { ...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes }, ...input.encoding === void 0 ? {} : { encoding: input.encoding } })) })),
5254
- fileSuggestions: authed.input(RcFileSuggestionsInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.fileSuggestions(input.session, input.query)) })),
5255
- keepAlive: authed.input(RcKeepAliveInputSchema).output(RcWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, sequenceNums: delivered(input.session, await deps.keepAlive(input.session)) })),
5256
- mcpStatus: authed.input(RcMcpStatusInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.mcpStatus(input.session)) })),
5257
- mcpReconnect: authed.input(RcMcpReconnectInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.mcpReconnect(input.session, input.serverName)) })),
5258
- mcpAuthenticate: authed.input(RcMcpAuthenticateInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.mcpAuthenticate(input.session, input.serverName, input.redirectUri)) })),
5259
- mcpOAuthCallbackUrl: authed.input(RcMcpOAuthCallbackUrlInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.mcpOAuthCallbackUrl(input.session, input.serverName, input.callbackUrl)) })),
5260
- teleport: authed.input(RcTeleportInputSchema).output(RcWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, sequenceNums: delivered(input.session, await deps.teleport(input.session, input.marker)) })),
5261
- subscribe: authed.input(RcSessionQuerySchema).output(eventIterator(RcStreamEventSchema)).handler(async function* ({ input, signal }) {
5355
+ interrupt: authed.route({ method: "POST", path: "/rest/rc/interrupt", summary: "Interrupt a session's running turn", tags: [RC_API_TAG] }).input(RcInterruptInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.interrupt(input.session)) })),
5356
+ setModel: authed.route({ method: "POST", path: "/rest/rc/set-model", summary: "Switch a session's model", tags: [RC_API_TAG] }).input(RcSetModelInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.setModel(input.session, input.model)) })),
5357
+ setPermissionMode: authed.route({ method: "POST", path: "/rest/rc/set-permission-mode", summary: "Switch a session's permission mode", tags: [RC_API_TAG] }).input(RcSetPermissionModeInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.setPermissionMode(input.session, input.mode)) })),
5358
+ endSession: authed.route({ method: "POST", path: "/rest/rc/end-session", summary: "End a session", tags: [RC_API_TAG] }).input(RcEndSessionInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.endSession(input.session, input.reason)) })),
5359
+ getUsage: authed.route({ method: "POST", path: "/rest/rc/get-usage", summary: "Ask a session for its usage snapshot", tags: [RC_API_TAG] }).input(RcGetUsageInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.getUsage(input.session, input.skipBehaviors)) })),
5360
+ getContextUsage: authed.route({ method: "POST", path: "/rest/rc/get-context-usage", summary: "Ask a session for its context usage", tags: [RC_API_TAG] }).input(RcGetContextUsageInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.getContextUsage(input.session, input.detail)) })),
5361
+ readFile: authed.route({ method: "POST", path: "/rest/rc/read-file", summary: "Ask a session to read one file", tags: [RC_API_TAG] }).input(RcReadFileInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.readFile(input.session, input.path, { ...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes }, ...input.encoding === void 0 ? {} : { encoding: input.encoding } })) })),
5362
+ fileSuggestions: authed.route({ method: "POST", path: "/rest/rc/file-suggestions", summary: "Ask a session for file-path suggestions", tags: [RC_API_TAG] }).input(RcFileSuggestionsInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.fileSuggestions(input.session, input.query)) })),
5363
+ keepAlive: authed.route({ method: "POST", path: "/rest/rc/keep-alive", summary: "Keep a session's bridge alive", tags: [RC_API_TAG] }).input(RcKeepAliveInputSchema).output(RcWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, sequenceNums: delivered(input.session, await deps.keepAlive(input.session)) })),
5364
+ mcpStatus: authed.route({ method: "POST", path: "/rest/rc/mcp-status", summary: "Ask a session for its MCP servers' status", tags: [RC_API_TAG] }).input(RcMcpStatusInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.mcpStatus(input.session)) })),
5365
+ mcpReconnect: authed.route({ method: "POST", path: "/rest/rc/mcp-reconnect", summary: "Ask a session to reconnect one MCP server", tags: [RC_API_TAG] }).input(RcMcpReconnectInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.mcpReconnect(input.session, input.serverName)) })),
5366
+ mcpAuthenticate: authed.route({ method: "POST", path: "/rest/rc/mcp-authenticate", summary: "Start OAuth for one MCP server on a session", tags: [RC_API_TAG] }).input(RcMcpAuthenticateInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.mcpAuthenticate(input.session, input.serverName, input.redirectUri)) })),
5367
+ mcpOAuthCallbackUrl: authed.route({ method: "POST", path: "/rest/rc/mcp-oauth-callback-url", summary: "Hand a completed MCP OAuth callback to a session", tags: [RC_API_TAG] }).input(RcMcpOAuthCallbackUrlInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, ...deliveredControl(input.session, await deps.mcpOAuthCallbackUrl(input.session, input.serverName, input.callbackUrl)) })),
5368
+ teleport: authed.route({ method: "POST", path: "/rest/rc/teleport", summary: "Teleport a session to a conversation marker", tags: [RC_API_TAG] }).input(RcTeleportInputSchema).output(RcWriteOutputSchema).handler(async ({ input }) => ({ session: input.session, sequenceNums: delivered(input.session, await deps.teleport(input.session, input.marker)) })),
5369
+ subscribe: authed.route({ method: "GET", path: "/rest/rc/events", summary: "Stream a session's Remote Control events (SSE)", tags: [RC_API_TAG] }).input(RcSessionQuerySchema).output(eventIterator(RcStreamEventSchema)).handler(async function* ({ input, signal }) {
5262
5370
  const queue = [];
5263
5371
  let wake;
5264
5372
  const detach = deps.fanout.subscribe(input.session, (event) => {
@@ -5295,15 +5403,44 @@ function createRcApiRouter(deps) {
5295
5403
  }
5296
5404
  };
5297
5405
  }
5298
- function doorApiNodeHandlerOf(router) {
5299
- const handler = new RPCHandler(router, { plugins: [new BodyLimitPlugin({ maxBodySize: CONTROL_BODY_CAP_BYTES })] });
5406
+ function doorApiNodeHandlerOf(router, expectedToken) {
5407
+ const plugins = [new BodyLimitPlugin({ maxBodySize: CONTROL_BODY_CAP_BYTES })];
5408
+ const rpcHandler = new RPCHandler(router, { plugins });
5409
+ const restHandler = new OpenAPIHandler(router, { plugins });
5410
+ let document;
5411
+ const documentOf = async () => document ??= new OpenAPIGenerator().generate(router, { info: { title: "agent-shim front door API", version: package_default.version } });
5300
5412
  return {
5301
5413
  pathPrefix: RC_ORPC_PATH_PREFIX,
5302
- handle: async (request4, response) => await handler.handle(request4, response, { context: { headers: request4.headers }, prefix: RC_ORPC_PATH_PREFIX })
5414
+ handle: async (request4, response) => {
5415
+ if (request4.method === "GET" && request4.url === `${RC_ORPC_PATH_PREFIX}${RC_OPENAPI_DOC_PATH}`) {
5416
+ const token = doorApiBearer(request4.headers);
5417
+ if (token === void 0 || !isLiveCapability(token, [expectedToken])) {
5418
+ const refusal = new ORPCError("UNAUTHORIZED", { message: "the front door's OpenAPI document demands this generation's control token as a Bearer credential" });
5419
+ response.writeHead(HTTP_STATUS.unauthorized, { "content-type": "application/json" });
5420
+ response.end(JSON.stringify(refusal.toJSON()));
5421
+ return { matched: true };
5422
+ }
5423
+ try {
5424
+ const doc = await documentOf();
5425
+ response.writeHead(HTTP_STATUS.ok, { "content-type": "application/json" });
5426
+ response.end(JSON.stringify(doc));
5427
+ } catch (error) {
5428
+ const refusal = new ORPCError("INTERNAL_SERVER_ERROR", { message: `the OpenAPI document could not be generated from the door's own router: ${error instanceof Error ? error.message : String(error)}` });
5429
+ response.writeHead(HTTP_STATUS.internalServerError, { "content-type": "application/json" });
5430
+ response.end(JSON.stringify(refusal.toJSON()));
5431
+ }
5432
+ return { matched: true };
5433
+ }
5434
+ const rest = await restHandler.handle(request4, response, { context: { headers: request4.headers }, prefix: RC_ORPC_PATH_PREFIX });
5435
+ if (rest.matched) {
5436
+ return rest;
5437
+ }
5438
+ return await rpcHandler.handle(request4, response, { context: { headers: request4.headers }, prefix: RC_ORPC_PATH_PREFIX });
5439
+ }
5303
5440
  };
5304
5441
  }
5305
5442
  function createRcApiNodeHandler(deps) {
5306
- return doorApiNodeHandlerOf(createRcApiRouter(deps));
5443
+ return doorApiNodeHandlerOf(createRcApiRouter(deps), deps.expectedToken);
5307
5444
  }
5308
5445
  function frontDoorApiLink(port, ca, token) {
5309
5446
  const dispatcher = new Agent3({ connect: { ca } });
@@ -9738,6 +9875,10 @@ var CheckRunInputSchema = z19.strictObject({
9738
9875
  path: z19.string().min(1),
9739
9876
  identity: z19.string().min(1).optional()
9740
9877
  }).refine((input) => path27.isAbsolute(input.path), { message: "path must be absolute: the door cannot resolve a relative path against the caller's working directory" });
9878
+ var PoolPickInputSchema = z19.strictObject({
9879
+ pool: z19.string().min(1),
9880
+ path: z19.string().min(1)
9881
+ }).refine((input) => path27.isAbsolute(input.path), { message: "path must be absolute: the door cannot resolve a relative path against the caller's working directory" });
9741
9882
  var DoctorFindingSchema = z19.strictObject({
9742
9883
  section: z19.enum(DOCTOR_SECTIONS),
9743
9884
  subject: z19.string().optional(),
@@ -9775,11 +9916,12 @@ var LaunchLifecycleEventSchema = z20.strictObject({
9775
9916
  });
9776
9917
 
9777
9918
  // src/frontdoor/eventsApi.ts
9919
+ var EVENTS_API_TAG = "events";
9778
9920
  function createEventsApiRouter(deps) {
9779
9921
  const authed = doorApiAuth(deps.expectedToken);
9780
9922
  return {
9781
9923
  events: {
9782
- subscribe: authed.input(DoorEventSourceQuerySchema).output(eventIterator2(DoorEventSchema)).handler(async function* ({ input, signal }) {
9924
+ subscribe: authed.route({ method: "GET", path: "/rest/events", summary: "Stream the door's every event source (SSE)", tags: [EVENTS_API_TAG] }).input(DoorEventSourceQuerySchema).output(eventIterator2(DoorEventSchema)).handler(async function* ({ input, signal }) {
9783
9925
  if (input.sources !== void 0) {
9784
9926
  const live = deps.events.sources();
9785
9927
  const unknown = input.sources.filter((source) => !live.includes(source));
@@ -9812,12 +9954,17 @@ function createEventsApiRouter(deps) {
9812
9954
  }
9813
9955
 
9814
9956
  // src/frontdoor/controlApi.ts
9957
+ var USAGE_API_TAG = "usage";
9958
+ var FRONTDOOR_API_TAG = "frontdoor";
9959
+ var CHECK_API_TAG = "check";
9960
+ var DOCTOR_API_TAG = "doctor";
9961
+ var POOL_API_TAG = "pool";
9815
9962
  function createControlApiRouter(deps) {
9816
9963
  const authed = doorApiAuth(deps.expectedToken);
9817
9964
  return {
9818
9965
  usage: {
9819
- list: authed.output(UsageListOutputSchema).handler(() => ({ snapshots: deps.usageSnapshots() })),
9820
- effectiveWindow: authed.input(UsageWindowsInputSchema).output(UsageWindowsOutputSchema).handler(({ input }) => {
9966
+ list: authed.route({ method: "GET", path: "/rest/usage/snapshots", summary: "List every identity's usage snapshot", tags: [USAGE_API_TAG] }).output(UsageListOutputSchema).handler(() => ({ snapshots: deps.usageSnapshots() })),
9967
+ effectiveWindow: authed.route({ method: "GET", path: "/rest/usage/windows", summary: "Read one identity's effective quota windows", tags: [USAGE_API_TAG] }).input(UsageWindowsInputSchema).output(UsageWindowsOutputSchema).handler(({ input }) => {
9821
9968
  const snapshot = deps.usageSnapshotOf(input.identity);
9822
9969
  if (snapshot === void 0) {
9823
9970
  throw new ORPCError3("NOT_FOUND", { message: `identity ${input.identity} has no usage snapshot yet, so no quota window has been observed for it` });
@@ -9834,7 +9981,7 @@ function createControlApiRouter(deps) {
9834
9981
  ...unified?.sevenDay === void 0 ? {} : { sevenDay: effectiveWindow(unified.sevenDay, now) }
9835
9982
  };
9836
9983
  }),
9837
- live: authed.input(RcSessionQuerySchema).output(UsageLiveOutputSchema).handler(({ input }) => {
9984
+ live: authed.route({ method: "GET", path: "/rest/usage/live", summary: "Read the live rate-limit observations the event backbone files", tags: [USAGE_API_TAG] }).input(RcSessionQuerySchema).output(UsageLiveOutputSchema).handler(({ input }) => {
9838
9985
  const sessions = deps.liveRateLimits(input.session);
9839
9986
  if (input.session !== void 0 && sessions.length === 0) {
9840
9987
  if (deps.list().some((session) => session.id === input.session)) {
@@ -9846,15 +9993,27 @@ function createControlApiRouter(deps) {
9846
9993
  })
9847
9994
  },
9848
9995
  frontdoor: {
9849
- status: authed.output(FrontDoorStatusOutputSchema).handler(() => deps.frontDoorStatus()),
9850
- sessions: authed.output(FrontDoorSessionsOutputSchema).handler(() => ({ sessions: deps.frontDoorStatus().sessions }))
9996
+ status: authed.route({ method: "GET", path: "/rest/frontdoor/status", summary: "Read the front door's status", tags: [FRONTDOOR_API_TAG] }).output(FrontDoorStatusOutputSchema).handler(() => deps.frontDoorStatus()),
9997
+ sessions: authed.route({ method: "GET", path: "/rest/frontdoor/sessions", summary: "Read the front door's session registry", tags: [FRONTDOOR_API_TAG] }).output(FrontDoorSessionsOutputSchema).handler(() => ({ sessions: deps.frontDoorStatus().sessions }))
9851
9998
  },
9852
9999
  check: {
9853
10000
  // The report's own JSON shape is the contract (the same one `check --json` prints and `checkReportSchema.ts` defines), so the procedure returns exactly that conversion.
9854
- run: authed.input(CheckRunInputSchema).output(CheckReportJsonSchema).handler(({ input }) => checkReportToJson(deps.checkReport(input.path, input.identity)))
10001
+ run: authed.route({ method: "GET", path: "/rest/check", summary: "Run the check report for one directory", tags: [CHECK_API_TAG] }).input(CheckRunInputSchema).output(CheckReportJsonSchema).handler(({ input }) => checkReportToJson(deps.checkReport(input.path, input.identity)))
9855
10002
  },
9856
10003
  doctor: {
9857
- run: authed.output(DoctorRunOutputSchema).handler(() => deps.doctorReport())
10004
+ run: authed.route({ method: "GET", path: "/rest/doctor", summary: "Run the doctor report", tags: [DOCTOR_API_TAG] }).output(DoctorRunOutputSchema).handler(() => deps.doctorReport())
10005
+ },
10006
+ pool: {
10007
+ pick: authed.route({ method: "GET", path: "/rest/pool/pick", summary: "Rank one pool exactly as a launch from a directory would right now", tags: [POOL_API_TAG] }).input(PoolPickInputSchema).output(PoolPickReportSchema).handler(({ input }) => {
10008
+ const report = deps.poolPick(input.pool, input.path);
10009
+ if (report === void 0) {
10010
+ const live = deps.poolNames();
10011
+ throw new ORPCError3("NOT_FOUND", {
10012
+ message: live.length === 0 ? `no pools are configured, so none is named ${input.pool}` : `no pool is named ${input.pool}; the live pools are ${live.map((name) => `"${name}"`).join(", ")}`
10013
+ });
10014
+ }
10015
+ return report;
10016
+ })
9858
10017
  }
9859
10018
  };
9860
10019
  }
@@ -9862,7 +10021,7 @@ function createDoorApiRouter(deps) {
9862
10021
  return { ...createRcApiRouter(deps), ...createControlApiRouter(deps), ...createEventsApiRouter(deps) };
9863
10022
  }
9864
10023
  function createDoorApiNodeHandler(deps) {
9865
- return doorApiNodeHandlerOf(createDoorApiRouter(deps));
10024
+ return doorApiNodeHandlerOf(createDoorApiRouter(deps), deps.expectedToken);
9866
10025
  }
9867
10026
  function frontDoorApiClient(port, ca, token) {
9868
10027
  return createORPCClient2(frontDoorApiLink(port, ca, token));
@@ -11948,106 +12107,6 @@ function removeDirectoryRule(paths, rulePath) {
11948
12107
  import { randomUUID as randomUUID4 } from "node:crypto";
11949
12108
  import path30 from "node:path";
11950
12109
 
11951
- // package.json
11952
- var package_default = {
11953
- name: "agent-shim",
11954
- version: "8.28.0",
11955
- description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
11956
- license: "MIT",
11957
- author: "Joseph Mearman <joseph@mearman.co.uk>",
11958
- repository: {
11959
- type: "git",
11960
- url: "git+https://github.com/ExaDev/agent-shim.git"
11961
- },
11962
- homepage: "https://github.com/ExaDev/agent-shim#readme",
11963
- bugs: "https://github.com/ExaDev/agent-shim/issues",
11964
- keywords: [
11965
- "claude-code",
11966
- "claude",
11967
- "cli"
11968
- ],
11969
- packageManager: "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c",
11970
- type: "module",
11971
- engines: {
11972
- node: "^22.18 || >=24"
11973
- },
11974
- bin: {
11975
- "agent-shim": "dist/cli.cjs",
11976
- "claude-use": "dist/cli.cjs"
11977
- },
11978
- files: [
11979
- "dist/cli.cjs",
11980
- "dist/index.mjs",
11981
- "dist/index.cjs",
11982
- "dist/types"
11983
- ],
11984
- scripts: {
11985
- prepare: "husky && node scripts/build.mts --bundle-only",
11986
- typecheck: "turbo run _typecheck",
11987
- _typecheck: "tsc --noEmit",
11988
- lint: "turbo run _lint",
11989
- _lint: "eslint . --cache --cache-location .eslintcache --max-warnings 0",
11990
- test: "turbo run _test",
11991
- _test: "vitest run",
11992
- "test:coverage": "turbo run _test:coverage",
11993
- "_test:coverage": "vitest run --coverage",
11994
- knip: "turbo run _knip",
11995
- _knip: "knip --cache",
11996
- build: "turbo run _build",
11997
- _build: "node scripts/build.mts",
11998
- "build:bundle": "node scripts/build.mts --bundle-only",
11999
- prepublishOnly: "pnpm build:bundle",
12000
- schema: "node scripts/gen-schema.mts"
12001
- },
12002
- dependencies: {
12003
- "@clack/prompts": "^1.8.0",
12004
- "@orpc/client": "1.15.4",
12005
- "@orpc/server": "1.15.4",
12006
- commander: "^15.0.0",
12007
- cosmiconfig: "^10.0.1",
12008
- picomatch: "^4.0.7",
12009
- undici: "7.29.1",
12010
- zod: "^4.6.1"
12011
- },
12012
- devDependencies: {
12013
- "@commitlint/cli": "^21.2.2",
12014
- "@commitlint/config-conventional": "^21.2.2",
12015
- "@exadev/eslint-config": "2.12.1",
12016
- "@semantic-release/changelog": "^7.0.0",
12017
- "@semantic-release/git": "^11.0.1",
12018
- "@semantic-release/npm": "^13.1.5",
12019
- "@types/node": "^26.5.1",
12020
- "@types/picomatch": "^4.0.3",
12021
- "@vitest/coverage-v8": "^4.1.11",
12022
- "conventional-changelog-conventionalcommits": "^10.4.0",
12023
- esbuild: "^0.28.2",
12024
- eslint: "^10.10.0",
12025
- globals: "^17.12.0",
12026
- husky: "^9.1.7",
12027
- jiti: "^2.7.0",
12028
- knip: "^6.35.1",
12029
- "lint-staged": "^17.5.0",
12030
- rollup: "4.64.0",
12031
- "rollup-plugin-dts": "6.5.1",
12032
- "semantic-release": "^25.0.9",
12033
- turbo: "^2.11.5",
12034
- typescript: "^6.0.3",
12035
- "typescript-eslint": "^8.70.0",
12036
- vitest: "^4.1.11"
12037
- },
12038
- main: "dist/index.cjs",
12039
- module: "dist/index.mjs",
12040
- types: "dist/types/index.d.ts",
12041
- exports: {
12042
- ".": {
12043
- types: "./dist/types/index.d.ts",
12044
- import: "./dist/index.mjs",
12045
- require: "./dist/index.cjs"
12046
- },
12047
- "./package.json": "./package.json"
12048
- }
12049
- };
12050
-
12051
12110
  // src/launcher/argv.ts
12052
12111
  var ConflictingIdentityError = class extends UsageError {
12053
12112
  constructor(positional, flag) {
@@ -2503,6 +2503,8 @@ interface PrePipelineApi {
2503
2503
  * Mounted on the provider listener beside the bespoke namespace, authenticated by the same per-generation owner-only control token as a Bearer credential: a fresh random value per door start, written owner-only under the front door's state directory, and checked in constant time like every other capability the door accepts. `rc.subscribe` streams the client attachment's fan-out: every event the door's held stream produced, SSE-framed by oRPC's event iterator, each carrying its sequence number as the SSE event id so a consumer's own reconnect can resume exactly as the door's does.
2504
2504
  *
2505
2505
  * This module also owns the mount's router-agnostic plumbing, which the control-plane routers (`controlApi.ts`) serve on the same prefix under the same token: the control-token middleware (`doorApiAuth`), the node-handler builder (`doorApiNodeHandlerOf`) and the TLS-pinned client link (`frontDoorApiLink`).
2506
+ *
2507
+ * Every procedure is route-annotated (method, path, summary, tags) under the mount's one REST namespace (`/rest/...`), so the mount serves the same operations twice over: as the oRPC RPC protocol the typed clients and the web client speak, and as plain REST for consumers with no TypeScript client (curl, scripts, other languages), with the OpenAPI document of the whole surface at `RC_OPENAPI_DOC_PATH` under the same token. The two share one router, one token middleware and one Zod schema source, so nothing can drift between them; an annotated (method, path) pair is served REST-shaped and everything else falls through to the RPC handler, and no RPC procedure path begins with `/rest`, so neither address space can ever capture the other.
2506
2508
  */
2507
2509
  /** The one path prefix the typed API is mounted under, answered before the routed pipeline like the bespoke control routes are. */
2508
2510
  declare const RC_ORPC_PATH_PREFIX = "/__agent-shim/orpc";
@@ -2751,9 +2753,9 @@ declare function createRcApiRouter(deps: RcApiDeps): {
2751
2753
  /** The typed API's router, as the CLI-side client's own type is derived from it. */
2752
2754
  type RcApiRouter = ReturnType<typeof createRcApiRouter>;
2753
2755
  /**
2754
- * Builds the node handler that serves one router of the door's typed API under the mount's one prefix: the pre-pipeline surface the provider listener hands every request under `RC_ORPC_PATH_PREFIX`, answering with `matched: false` for a path under the prefix that names no procedure (which the listener itself answers as a 404). Request bodies are bounded by the same protocol cap the bespoke routes apply. Every router the mount serves (Remote Control and the control plane) goes through this one builder, so the prefix, the context and the cap are stated once.
2756
+ * Builds the node handler that serves one router of the door's typed API under the mount's one prefix, twice over: the OpenAPI handler answers every route-annotated (method, path) pair as plain REST (every annotation under `/rest`, which no RPC procedure path begins with, so the RPC protocol's own POSTs always fall through untouched), and everything else falls to the RPC handler, which answers the oRPC protocol the typed clients and the web client speak. The OpenAPI document of the annotated routes is served at `RC_OPENAPI_DOC_PATH`, behind the same per-generation control token the procedures demand, so a consumer with no TypeScript client reads the whole surface from the mount itself. The pre-pipeline surface the provider listener hands every request under `RC_ORPC_PATH_PREFIX`, answering with `matched: false` for a path under the prefix that names no route and no procedure (which the listener itself answers as a 404). Request bodies are bounded by the same protocol cap the bespoke routes apply, on both handlers. Every router the mount serves (Remote Control and the control plane) goes through this one builder, so the prefix, the context and the cap are stated once.
2755
2757
  */
2756
- declare function doorApiNodeHandlerOf(router: AnyRouter): PrePipelineApi;
2758
+ declare function doorApiNodeHandlerOf(router: AnyRouter, expectedToken: string): PrePipelineApi;
2757
2759
  /**
2758
2760
  * Builds the typed API's node handler for the Remote Control router alone: the pre-pipeline surface the provider listener hands every request under `RC_ORPC_PATH_PREFIX`.
2759
2761
  */
@@ -3913,6 +3915,7 @@ declare function formatFrontDoorStatus(status: FrontDoorStatus, caCertPath: stri
3913
3915
  * - `usage.*` reads the per-identity usage snapshots the door's own middleware writes, reads a provider's quota windows the way every in-process consumer does (`effectiveWindow`: a window past its reset is empty and carries no status), and reads the live rate-limit observations the event backbone files off the Remote Control client stream (`live`: per-session latest state, exactly as fresh as the last turn a session completed, never a timer's derivation of it).
3914
3916
  * - `frontdoor.*` returns what `agent-shim frontdoor status` returns: the supervisor state, its liveness, the session registry's launches and the headroom hop.
3915
3917
  * - `check.run` and `doctor.run` return the reports `agent-shim check` and `agent-shim doctor` print, as data, `check.run` parameterised by an absolute directory path.
3918
+ * - `pool.pick` returns what `agent-shim pool pick` prints, the ranked pick for one pool exactly as a launch from the named absolute directory would make it right now, so a programmatic consumer can ask which identity to use without reading the door host's files.
3916
3919
  *
3917
3920
  * Read-only by design: identity, configuration-profile, provider, pool and directory-rule management stay CLI-side, and adding writes over this mount is a decision of its own rather than a gap here. `createDoorApiNodeHandler` mounts these routers and the door-wide `events.subscribe` router beside the Remote Control router on the one prefix the provider listener already serves, so a consumer dials one address with one token for the whole door.
3918
3921
  */
@@ -3938,6 +3941,10 @@ interface ControlApiDeps {
3938
3941
  readonly checkReport: (path: string, identity?: string) => CheckReport;
3939
3942
  /** The doctor report, as `collectDoctorReport` collects it. */
3940
3943
  readonly doctorReport: () => DoctorReport;
3944
+ /** The ranked pick for one pool as a launch from one absolute directory would make it right now, exactly `pool pick`'s own product; undefined when the pool table names no such pool. */
3945
+ readonly poolPick: (pool: string, directory: string) => PoolPickReport | undefined;
3946
+ /** The pool table's live names, so an unknown pool is refused naming the real ones rather than answered as an empty ranking. */
3947
+ readonly poolNames: () => readonly string[];
3941
3948
  }
3942
3949
  /** Builds the control-plane routers: one procedure per read, every one behind the control-token middleware. */
3943
3950
  declare function createControlApiRouter(deps: ControlApiDeps): {
@@ -4461,6 +4468,44 @@ declare function createControlApiRouter(deps: ControlApiDeps): {
4461
4468
  }, zod_v4_core.$strict>>>;
4462
4469
  }, zod_v4_core.$strict>, Record<never, never>, Record<never, never>>;
4463
4470
  };
4471
+ pool: {
4472
+ pick: _orpc_server.DecoratedProcedure<_orpc_server.MergedInitialContext<DoorApiContext & Record<never, never>, DoorApiContext, DoorApiContext>, any, zod.ZodObject<{
4473
+ pool: zod.ZodString;
4474
+ path: zod.ZodString;
4475
+ }, zod_v4_core.$strict>, zod.ZodObject<{
4476
+ pool: zod.ZodString;
4477
+ directory: zod.ZodString;
4478
+ pick: zod.ZodOptional<zod.ZodString>;
4479
+ candidates: zod.ZodArray<zod.ZodObject<{
4480
+ identity: zod.ZodString;
4481
+ class: zod.ZodEnum<{
4482
+ unknown: "unknown";
4483
+ scored: "scored";
4484
+ "pay-per-use": "pay-per-use";
4485
+ ineligible: "ineligible";
4486
+ }>;
4487
+ score: zod.ZodOptional<zod.ZodNumber>;
4488
+ feasible: zod.ZodBoolean;
4489
+ blockedUntil: zod.ZodOptional<zod.ZodISODateTime>;
4490
+ plan: zod.ZodUnion<readonly [zod.ZodObject<{
4491
+ kind: zod.ZodLiteral<"subscription">;
4492
+ capacity: zod.ZodNumber;
4493
+ recognised: zod.ZodBoolean;
4494
+ tier: zod.ZodOptional<zod.ZodString>;
4495
+ }, zod_v4_core.$strict>, zod.ZodObject<{
4496
+ kind: zod.ZodLiteral<"pay-per-use">;
4497
+ tier: zod.ZodOptional<zod.ZodString>;
4498
+ }, zod_v4_core.$strict>]>;
4499
+ reasons: zod.ZodArray<zod.ZodString>;
4500
+ }, zod_v4_core.$strict>>;
4501
+ missing: zod.ZodArray<zod.ZodString>;
4502
+ earliestReturn: zod.ZodOptional<zod.ZodObject<{
4503
+ identity: zod.ZodString;
4504
+ at: zod.ZodISODateTime;
4505
+ }, zod_v4_core.$strict>>;
4506
+ stickyProblem: zod.ZodOptional<zod.ZodString>;
4507
+ }, zod_v4_core.$strict>, Record<never, never>, Record<never, never>>;
4508
+ };
4464
4509
  };
4465
4510
  /** The control-plane routers, as the merged mount's and the client's own types are derived from them. */
4466
4511
  type ControlApiRouter = ReturnType<typeof createControlApiRouter>;
@@ -4996,6 +5041,44 @@ declare function createDoorApiRouter(deps: DoorApiDeps): {
4996
5041
  }, zod_v4_core.$strict>>>;
4997
5042
  }, zod_v4_core.$strict>, Record<never, never>, Record<never, never>>;
4998
5043
  };
5044
+ pool: {
5045
+ pick: _orpc_server.DecoratedProcedure<_orpc_server.MergedInitialContext<DoorApiContext & Record<never, never>, DoorApiContext, DoorApiContext>, any, zod.ZodObject<{
5046
+ pool: zod.ZodString;
5047
+ path: zod.ZodString;
5048
+ }, zod_v4_core.$strict>, zod.ZodObject<{
5049
+ pool: zod.ZodString;
5050
+ directory: zod.ZodString;
5051
+ pick: zod.ZodOptional<zod.ZodString>;
5052
+ candidates: zod.ZodArray<zod.ZodObject<{
5053
+ identity: zod.ZodString;
5054
+ class: zod.ZodEnum<{
5055
+ unknown: "unknown";
5056
+ scored: "scored";
5057
+ "pay-per-use": "pay-per-use";
5058
+ ineligible: "ineligible";
5059
+ }>;
5060
+ score: zod.ZodOptional<zod.ZodNumber>;
5061
+ feasible: zod.ZodBoolean;
5062
+ blockedUntil: zod.ZodOptional<zod.ZodISODateTime>;
5063
+ plan: zod.ZodUnion<readonly [zod.ZodObject<{
5064
+ kind: zod.ZodLiteral<"subscription">;
5065
+ capacity: zod.ZodNumber;
5066
+ recognised: zod.ZodBoolean;
5067
+ tier: zod.ZodOptional<zod.ZodString>;
5068
+ }, zod_v4_core.$strict>, zod.ZodObject<{
5069
+ kind: zod.ZodLiteral<"pay-per-use">;
5070
+ tier: zod.ZodOptional<zod.ZodString>;
5071
+ }, zod_v4_core.$strict>]>;
5072
+ reasons: zod.ZodArray<zod.ZodString>;
5073
+ }, zod_v4_core.$strict>>;
5074
+ missing: zod.ZodArray<zod.ZodString>;
5075
+ earliestReturn: zod.ZodOptional<zod.ZodObject<{
5076
+ identity: zod.ZodString;
5077
+ at: zod.ZodISODateTime;
5078
+ }, zod_v4_core.$strict>>;
5079
+ stickyProblem: zod.ZodOptional<zod.ZodString>;
5080
+ }, zod_v4_core.$strict>, Record<never, never>, Record<never, never>>;
5081
+ };
4999
5082
  rc: {
5000
5083
  list: _orpc_server.DecoratedProcedure<_orpc_server.MergedInitialContext<DoorApiContext & Record<never, never>, DoorApiContext, DoorApiContext>, any, Schema<unknown, unknown>, zod.ZodObject<{
5001
5084
  sessions: zod.ZodReadonly<zod.ZodArray<zod.ZodObject<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-shim",
3
- "version": "8.28.0",
3
+ "version": "8.30.0",
4
4
  "description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5
5
  "license": "MIT",
6
6
  "author": "Joseph Mearman <joseph@mearman.co.uk>",
@@ -51,6 +51,7 @@
51
51
  "dependencies": {
52
52
  "@clack/prompts": "^1.8.0",
53
53
  "@orpc/client": "1.15.4",
54
+ "@orpc/openapi": "1.15.4",
54
55
  "@orpc/server": "1.15.4",
55
56
  "commander": "^15.0.0",
56
57
  "cosmiconfig": "^10.0.1",