@byok-sdk/client 0.1.1 → 0.2.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/README.md CHANGED
@@ -7,8 +7,9 @@ and exposes authenticated local diagnostics/control commands.
7
7
  The package installs `byok-agent` and `byok-approval-mcp` binaries. Provider
8
8
  credentials are not read by the dispatch plane; `@byok-sdk/keys` is separate.
9
9
 
10
- Pi, Claude Code and Codex are user-installed runtimes. Install and authenticate
11
- the runtime CLI you choose; the SDK never packages its executable or
10
+ Pi is a required exact npm dependency and runs as an external Node subprocess;
11
+ authenticate it with your own provider credentials. Claude Code and Codex
12
+ remain user-installed runtimes. The SDK never reads or packages provider
12
13
  credentials. Hosts that only need runtime detection/composition can import the
13
14
  transport-free adapter surface:
14
15
 
@@ -16,5 +17,4 @@ transport-free adapter surface:
16
17
  import { PiAdapter, ClaudeAdapter, CodexAdapter } from '@byok-sdk/client/adapters';
17
18
  ```
18
19
 
19
- MIT licensed. Node.js 20 or newer; SQLite-backed journaling requires a runtime
20
- with `node:sqlite` support.
20
+ MIT licensed. Node.js 22.19.0 or newer.
@@ -6,7 +6,7 @@ export interface ResolvedBin {
6
6
  * Resolve the `claude` (Claude Code) CLI executable.
7
7
  *
8
8
  * Unlike pi (`../pi/resolve-bin.ts`), this package does NOT bundle a
9
- * matched `claude` build as an optionalDependency. Claude Code is the end
9
+ * matched `claude` build as a dependency. Claude Code is the end
10
10
  * user's own globally-installed, individually-authenticated CLI (`claude
11
11
  * auth login`, tied to their Anthropic/claude.ai account) — there is
12
12
  * nothing useful to vendor: a bundled copy could never carry the user's own
@@ -22,6 +22,6 @@ export interface ResolvedBin {
22
22
  * `fake-claude.mjs` fixture ahead of a real claude install, exactly as pi's
23
23
  * own override does), otherwise this falls back to the literal command name
24
24
  * `claude`, resolved via the child process's own PATH lookup — there is no
25
- * optionalDependency tier in between.
25
+ * package-resolution tier in between.
26
26
  */
27
27
  export declare function resolveClaudeBin(): ResolvedBin;
@@ -66,9 +66,10 @@ export declare class CodexAdapter implements RuntimeAdapter {
66
66
  * Two independently-verified channel gotchas apply here, the "pi lesson"
67
67
  * yet again:
68
68
  * - `codex login status`'s human-readable "Logged in using ChatGPT"
69
- * message prints on STDERR, not stdout (the opposite-channel
70
- * counterpart of pi's own `--version`-goes-to-stderr surprise) both
71
- * streams are checked here for exactly that reason.
69
+ * message prints on STDERR, not stdout both streams are checked
70
+ * here for exactly that reason. pi's `--version` is the same class of
71
+ * hazard from the other direction: its channel has moved between pi
72
+ * releases (see ../pi/pi-adapter.ts), so neither stream is assumed.
72
73
  * - The NOT-logged-in message/exit-code shape was deliberately never
73
74
  * empirically tested: this machine has a real, live ChatGPT login, and
74
75
  * running `codex logout` to observe the negative case would have
@@ -1,9 +1,9 @@
1
1
  import { execFile, spawn, spawnSync } from 'child_process';
2
2
  import { promisify } from 'util';
3
- import { promises, realpathSync } from 'fs';
4
- import os from 'os';
5
- import path2 from 'path';
3
+ import { promises, existsSync, readFileSync, realpathSync } from 'fs';
4
+ import path3 from 'path';
6
5
  import { fileURLToPath } from 'url';
6
+ import os from 'os';
7
7
  import 'readline';
8
8
 
9
9
  // src/adapters/pi/pi-adapter.ts
@@ -24,20 +24,50 @@ var SteerUnsupportedError = class extends Error {
24
24
  this.runtimeId = runtimeId;
25
25
  }
26
26
  };
27
-
28
- // src/adapters/pi/resolve-bin.ts
29
27
  var PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
28
+ function readPackageJson(dir) {
29
+ const candidate = path3.join(dir, "package.json");
30
+ if (!existsSync(candidate)) return void 0;
31
+ try {
32
+ return JSON.parse(readFileSync(candidate, "utf8"));
33
+ } catch {
34
+ return void 0;
35
+ }
36
+ }
30
37
  function resolvePiBin() {
31
38
  const override = process.env.BYOK_PI_BIN;
32
39
  if (override) {
33
- return { command: override, source: "path" };
40
+ return { command: override, source: "env" };
34
41
  }
35
- return { command: "pi", source: "path" };
42
+ try {
43
+ const mainEntryUrl = import.meta.resolve(PI_PACKAGE_NAME);
44
+ let dir = path3.dirname(fileURLToPath(mainEntryUrl));
45
+ for (let depth = 0; depth < 6; depth++) {
46
+ const pkg = readPackageJson(dir);
47
+ if (pkg?.name === PI_PACKAGE_NAME) {
48
+ const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
49
+ if (binRel) {
50
+ return { command: path3.join(dir, binRel), source: "package" };
51
+ }
52
+ break;
53
+ }
54
+ const parent = path3.dirname(dir);
55
+ if (parent === dir) break;
56
+ dir = parent;
57
+ }
58
+ } catch (cause) {
59
+ throw new Error(
60
+ `Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.19+ pi sidecar`,
61
+ { cause }
62
+ );
63
+ }
64
+ throw new Error(
65
+ `Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.19+ pi sidecar`
66
+ );
36
67
  }
37
68
 
38
69
  // src/adapters/pi/permission-mapping.ts
39
70
  var READONLY_TOOLS = ["read", "grep", "find", "ls"];
40
- var DEFAULT_ACTIVE_TOOLS = ["read", "bash", "edit", "write"];
41
71
  function mapPermissionPolicyToPiArgs(policy) {
42
72
  if (policy.network === false) {
43
73
  return {
@@ -56,22 +86,18 @@ function mapPermissionPolicyToPiArgs(policy) {
56
86
  const denyTools = policy.denyTools ?? [];
57
87
  if (policy.mode === "readonly") {
58
88
  const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS.includes(tool)) : [...READONLY_TOOLS];
59
- const effective = subtractDenied(base, denyTools);
60
- return { ok: true, args: effective.length === 0 ? ["--no-tools"] : ["--tools", effective.join(",")] };
61
- }
62
- if (denyTools.length > 0) {
63
- const base = policy.allowTools && policy.allowTools.length > 0 ? policy.allowTools : [...DEFAULT_ACTIVE_TOOLS];
64
- const effective = subtractDenied(base, denyTools);
65
- return { ok: true, args: effective.length === 0 ? ["--no-tools"] : ["--tools", effective.join(",")] };
89
+ if (base.length === 0) return { ok: true, args: ["--no-tools"] };
90
+ return {
91
+ ok: true,
92
+ args: ["--tools", base.join(","), ...denyTools.length > 0 ? ["--exclude-tools", denyTools.join(",")] : []]
93
+ };
66
94
  }
95
+ const args = [];
67
96
  if (policy.allowTools && policy.allowTools.length > 0) {
68
- return { ok: true, args: ["--tools", policy.allowTools.join(",")] };
97
+ args.push("--tools", policy.allowTools.join(","));
69
98
  }
70
- return { ok: true, args: [] };
71
- }
72
- function subtractDenied(tools, denyTools) {
73
- const denied = new Set(denyTools);
74
- return tools.filter((tool) => !denied.has(tool));
99
+ if (denyTools.length > 0) args.push("--exclude-tools", denyTools.join(","));
100
+ return { ok: true, args };
75
101
  }
76
102
 
77
103
  // src/adapters/pi/events.ts
@@ -96,7 +122,7 @@ function mapPiMessageToAgentEvent(msg) {
96
122
  output: { result: msg.result, isError: msg.isError === true }
97
123
  };
98
124
  }
99
- case "agent_end":
125
+ case "agent_settled":
100
126
  return { type: "turn_end" };
101
127
  /**
102
128
  * `artifact` is NOT a real pi RPC message — pi's own `write` tool only
@@ -132,16 +158,22 @@ function mapPiMessageToAgentEvent(msg) {
132
158
  // `recordUnmappedFrame`) can tell "known, expected, silently ignored"
133
159
  // apart from "genuinely never seen before" (falls to `default` below).
134
160
  case "agent_start":
161
+ case "agent_end":
162
+ // one low-level run; `agent_settled` is BYOK completion
135
163
  case "turn_start":
136
164
  case "turn_end":
137
- // pi's own per-LLM-turn boundary, not ours — see `agent_end` above
165
+ // pi's own per-LLM-turn boundary, not ours
138
166
  case "message_start":
139
167
  case "message_end":
168
+ case "bash_execution_update":
140
169
  case "tool_execution_update":
141
170
  case "queue_update":
142
171
  case "compaction_start":
143
172
  case "compaction_end":
144
173
  case "auto_retry_start":
174
+ case "summarization_retry_scheduled":
175
+ case "summarization_retry_attempt_start":
176
+ case "summarization_retry_finished":
145
177
  case "session_info_changed":
146
178
  case "thinking_level_changed":
147
179
  return void 0;
@@ -151,15 +183,20 @@ function mapPiMessageToAgentEvent(msg) {
151
183
  }
152
184
  var ROUTINE_PI_EVENT_TYPES = /* @__PURE__ */ new Set([
153
185
  "agent_start",
186
+ "agent_end",
154
187
  "turn_start",
155
188
  "turn_end",
156
189
  "message_start",
157
190
  "message_end",
191
+ "bash_execution_update",
158
192
  "tool_execution_update",
159
193
  "queue_update",
160
194
  "compaction_start",
161
195
  "compaction_end",
162
196
  "auto_retry_start",
197
+ "summarization_retry_scheduled",
198
+ "summarization_retry_attempt_start",
199
+ "summarization_retry_finished",
163
200
  "session_info_changed",
164
201
  "thinking_level_changed"
165
202
  ]);
@@ -293,10 +330,7 @@ var PiRpcClient = class {
293
330
  * traffic. Logs once per distinct type (not per occurrence, so a
294
331
  * repeating unmapped type can't spam stdout); the running tally is also
295
332
  * folded into this client's exit-time error message (`buildExitError`) so
296
- * a post-mortem on a failed/hung task has it without needing separate log
297
- * scraping. This is the exact mechanism that would have turned this
298
- * task's root-cause hang (`agent_end` arriving with no mapping) into a
299
- * one-line, immediate warning instead of a silent stall.
333
+ * a post-mortem on a failed/hung task has it without separate log scraping.
300
334
  */
301
335
  recordUnmappedFrame(type) {
302
336
  const next = (this.unmappedFrameCounts.get(type) ?? 0) + 1;
@@ -437,8 +471,8 @@ var PiAdapter = class {
437
471
  options;
438
472
  id = "pi";
439
473
  async detect() {
440
- const bin = this.resolveBin();
441
474
  try {
475
+ const bin = this.resolveBin();
442
476
  const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
443
477
  const version = stdout.trim() || stderr.trim();
444
478
  const authPresent = KNOWN_PROVIDER_ENV_VARS.some((name) => process.env[name] !== void 0);
@@ -584,7 +618,7 @@ function resolveApprovalMcpBin() {
584
618
  if (override) {
585
619
  return { command: override, args: [], source: "env" };
586
620
  }
587
- const distBin = path2.join(path2.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
621
+ const distBin = path3.join(path3.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
588
622
  return { command: process.execPath, args: [distBin], source: "dist" };
589
623
  }
590
624
 
@@ -615,7 +649,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
615
649
  }
616
650
  if (policy.mode === "readonly") {
617
651
  const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS2.includes(tool)) : [...READONLY_TOOLS2];
618
- const effective = subtractDenied2(base, denyTools);
652
+ const effective = subtractDenied(base, denyTools);
619
653
  return { ok: true, args: ["--permission-mode", "default", "--tools", effective.join(",")] };
620
654
  }
621
655
  if (denyTools.length > 0) {
@@ -632,7 +666,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
632
666
  }
633
667
  return { ok: true, args };
634
668
  }
635
- function subtractDenied2(tools, denyTools) {
669
+ function subtractDenied(tools, denyTools) {
636
670
  const denied = new Set(denyTools);
637
671
  return tools.filter((tool) => !denied.has(tool));
638
672
  }
@@ -664,7 +698,7 @@ var EXTENSION_CONTENT_TYPES = {
664
698
  ".yml": "application/yaml"
665
699
  };
666
700
  function guessContentType(filePath) {
667
- const ext = path2.extname(filePath).toLowerCase();
701
+ const ext = path3.extname(filePath).toLowerCase();
668
702
  return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
669
703
  }
670
704
  function mapAssistant(msg, correlation) {
@@ -736,11 +770,11 @@ function tryBuildArtifactEvent(msg, workspaceDir) {
736
770
  const filePath = toolUseResult && typeof toolUseResult.filePath === "string" ? toolUseResult.filePath : void 0;
737
771
  if (!filePath) return void 0;
738
772
  const realWorkspaceDir = tryRealpath(workspaceDir) ?? workspaceDir;
739
- const fileDir = path2.dirname(filePath);
773
+ const fileDir = path3.dirname(filePath);
740
774
  const realFileDir = tryRealpath(fileDir) ?? fileDir;
741
- const realFilePath = path2.join(realFileDir, path2.basename(filePath));
742
- const relative = path2.relative(realWorkspaceDir, realFilePath);
743
- if (relative === "" || relative.startsWith("..") || path2.isAbsolute(relative)) {
775
+ const realFilePath = path3.join(realFileDir, path3.basename(filePath));
776
+ const relative = path3.relative(realWorkspaceDir, realFilePath);
777
+ if (relative === "" || relative.startsWith("..") || path3.isAbsolute(relative)) {
744
778
  return void 0;
745
779
  }
746
780
  return { type: "artifact", name: relative, contentType: guessContentType(filePath) };
@@ -1039,10 +1073,10 @@ var ClaudeAdapter = class {
1039
1073
  );
1040
1074
  }
1041
1075
  const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
1042
- approvalMcpConfigDir = await promises.mkdtemp(path2.join(os.tmpdir(), "byok-approval-mcp-"));
1076
+ approvalMcpConfigDir = await promises.mkdtemp(path3.join(os.tmpdir(), "byok-approval-mcp-"));
1043
1077
  await promises.chmod(approvalMcpConfigDir, 448).catch(() => {
1044
1078
  });
1045
- const mcpConfigPath = path2.join(approvalMcpConfigDir, "mcp-config.json");
1079
+ const mcpConfigPath = path3.join(approvalMcpConfigDir, "mcp-config.json");
1046
1080
  const mcpConfig = {
1047
1081
  mcpServers: {
1048
1082
  [APPROVAL_MCP_SERVER_NAME]: {
@@ -1389,8 +1423,8 @@ function extractArtifactEvents(changes, workspaceDir) {
1389
1423
  const absolutePath = typeof change.path === "string" ? change.path : void 0;
1390
1424
  const kind = typeof change.kind === "string" ? change.kind : void 0;
1391
1425
  if (!absolutePath || kind === "delete") continue;
1392
- const relative = path2.relative(workspaceDir, absolutePath);
1393
- if (relative.length === 0 || relative.startsWith("..") || path2.isAbsolute(relative)) continue;
1426
+ const relative = path3.relative(workspaceDir, absolutePath);
1427
+ if (relative.length === 0 || relative.startsWith("..") || path3.isAbsolute(relative)) continue;
1394
1428
  events.push({ type: "artifact", name: relative, contentType: guessContentType2(relative) });
1395
1429
  }
1396
1430
  return events;
@@ -1411,7 +1445,7 @@ var CONTENT_TYPE_BY_EXTENSION = {
1411
1445
  ".csv": "text/csv"
1412
1446
  };
1413
1447
  function guessContentType2(relativePath) {
1414
- return CONTENT_TYPE_BY_EXTENSION[path2.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
1448
+ return CONTENT_TYPE_BY_EXTENSION[path3.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
1415
1449
  }
1416
1450
  function extractErrorMessage(rawError) {
1417
1451
  if (typeof rawError === "string") return rawError;
@@ -1572,9 +1606,10 @@ var CodexAdapter = class {
1572
1606
  * Two independently-verified channel gotchas apply here, the "pi lesson"
1573
1607
  * yet again:
1574
1608
  * - `codex login status`'s human-readable "Logged in using ChatGPT"
1575
- * message prints on STDERR, not stdout (the opposite-channel
1576
- * counterpart of pi's own `--version`-goes-to-stderr surprise) both
1577
- * streams are checked here for exactly that reason.
1609
+ * message prints on STDERR, not stdout both streams are checked
1610
+ * here for exactly that reason. pi's `--version` is the same class of
1611
+ * hazard from the other direction: its channel has moved between pi
1612
+ * releases (see ../pi/pi-adapter.ts), so neither stream is assumed.
1578
1613
  * - The NOT-logged-in message/exit-code shape was deliberately never
1579
1614
  * empirically tested: this machine has a real, live ChatGPT login, and
1580
1615
  * running `codex logout` to observe the negative case would have