super-dsh 0.1.0 → 0.1.2

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.
Files changed (178) hide show
  1. package/agent-agy/bridge/agy_bridge.py +250 -0
  2. package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
  3. package/agent-agy/cordis.patch.yml +49 -0
  4. package/agent-agy/dist/adapter.d.ts +15 -0
  5. package/agent-agy/dist/adapter.js +42 -0
  6. package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
  7. package/agent-agy/dist/agent-preset-agy.js +169 -0
  8. package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
  9. package/agent-agy/dist/agent-preset-projection.js +33 -0
  10. package/agent-agy/dist/agent.d.ts +137 -0
  11. package/agent-agy/dist/agent.js +493 -0
  12. package/agent-agy/dist/agy-cli-client.d.ts +137 -0
  13. package/agent-agy/dist/agy-cli-client.js +301 -0
  14. package/agent-agy/dist/agy-client.d.ts +160 -0
  15. package/agent-agy/dist/agy-client.js +252 -0
  16. package/agent-agy/dist/agy-sessions.d.ts +24 -0
  17. package/agent-agy/dist/agy-sessions.js +116 -0
  18. package/agent-agy/dist/agy-store.d.ts +39 -0
  19. package/agent-agy/dist/agy-store.js +83 -0
  20. package/agent-agy/dist/inbox.d.ts +26 -0
  21. package/agent-agy/dist/inbox.js +53 -0
  22. package/agent-agy/dist/index.d.ts +80 -0
  23. package/agent-agy/dist/index.js +505 -0
  24. package/agent-agy/dist/models.d.ts +41 -0
  25. package/agent-agy/dist/models.js +74 -0
  26. package/agent-agy/dist/world-plugin.d.ts +9 -0
  27. package/agent-agy/dist/world-plugin.js +97 -0
  28. package/agent-agy/package.json +64 -0
  29. package/agent-claude/cordis.patch.yml +42 -0
  30. package/agent-claude/dist/adapter.js +62 -0
  31. package/agent-claude/dist/agent-preset-claude.js +169 -0
  32. package/agent-claude/dist/agent-preset-projection.js +30 -0
  33. package/agent-claude/dist/agent.js +1271 -0
  34. package/agent-claude/dist/claude-client.js +357 -0
  35. package/agent-claude/dist/claude-events.js +223 -0
  36. package/agent-claude/dist/claude-home.js +29 -0
  37. package/agent-claude/dist/commands.js +182 -0
  38. package/agent-claude/dist/content.js +134 -0
  39. package/agent-claude/dist/inbox.js +53 -0
  40. package/agent-claude/dist/index.js +555 -0
  41. package/agent-claude/dist/input-queue.js +64 -0
  42. package/agent-claude/dist/interaction.js +271 -0
  43. package/agent-claude/dist/models.js +158 -0
  44. package/agent-claude/dist/permission.js +120 -0
  45. package/agent-claude/dist/session-id.js +20 -0
  46. package/agent-claude/dist/session-map.js +165 -0
  47. package/agent-claude/dist/world-plugin.js +14 -0
  48. package/agent-claude/package.json +74 -0
  49. package/agent-codex/README.md +74 -0
  50. package/agent-codex/cordis.patch.yml +77 -0
  51. package/agent-codex/dist/adapter.js +55 -0
  52. package/agent-codex/dist/agent-preset-codex.js +171 -0
  53. package/agent-codex/dist/agent-preset-projection.js +37 -0
  54. package/agent-codex/dist/agent.js +1056 -0
  55. package/agent-codex/dist/app-home.js +65 -0
  56. package/agent-codex/dist/codex-client.js +469 -0
  57. package/agent-codex/dist/codex-events.js +297 -0
  58. package/agent-codex/dist/codex-store.js +190 -0
  59. package/agent-codex/dist/inbox.js +53 -0
  60. package/agent-codex/dist/index.js +605 -0
  61. package/agent-codex/dist/models.js +152 -0
  62. package/agent-codex/dist/permission.js +71 -0
  63. package/agent-codex/dist/session-map.js +170 -0
  64. package/agent-codex/dist/spike.js +46 -0
  65. package/agent-codex/dist/world-plugin.js +96 -0
  66. package/agent-codex/package.json +78 -0
  67. package/agent-hermes/README.md +144 -0
  68. package/agent-hermes/cordis.patch.yml +83 -0
  69. package/agent-hermes/dist/adapter.js +83 -0
  70. package/agent-hermes/dist/agent-preset-hermes.js +171 -0
  71. package/agent-hermes/dist/agent-preset-projection.js +34 -0
  72. package/agent-hermes/dist/agent.js +1068 -0
  73. package/agent-hermes/dist/hermes-client.js +953 -0
  74. package/agent-hermes/dist/hermes-events.js +289 -0
  75. package/agent-hermes/dist/hermes-store.js +158 -0
  76. package/agent-hermes/dist/inbox.js +53 -0
  77. package/agent-hermes/dist/index.js +541 -0
  78. package/agent-hermes/dist/models.js +324 -0
  79. package/agent-hermes/dist/permission.js +60 -0
  80. package/agent-hermes/dist/world-plugin.js +101 -0
  81. package/agent-hermes/package.json +76 -0
  82. package/agent-hub/cordis.patch.yml +12 -0
  83. package/agent-hub/dist/agent-roster.d.ts +34 -0
  84. package/agent-hub/dist/agent-roster.js +27 -0
  85. package/agent-hub/dist/carrier.d.ts +43 -0
  86. package/agent-hub/dist/carrier.js +241 -0
  87. package/agent-hub/dist/client-shim.d.ts +28 -0
  88. package/agent-hub/dist/client-shim.js +251 -0
  89. package/agent-hub/dist/envelope.d.ts +54 -0
  90. package/agent-hub/dist/envelope.js +92 -0
  91. package/agent-hub/dist/gateway.d.ts +28 -0
  92. package/agent-hub/dist/gateway.js +18 -0
  93. package/agent-hub/dist/index-pass.d.ts +22 -0
  94. package/agent-hub/dist/index-pass.js +47 -0
  95. package/agent-hub/dist/index.d.ts +29 -0
  96. package/agent-hub/dist/index.js +29 -0
  97. package/agent-hub/dist/labels.d.ts +45 -0
  98. package/agent-hub/dist/labels.js +83 -0
  99. package/agent-hub/dist/ownership.d.ts +37 -0
  100. package/agent-hub/dist/ownership.js +54 -0
  101. package/agent-hub/dist/roster.d.ts +28 -0
  102. package/agent-hub/dist/roster.js +38 -0
  103. package/agent-hub/dist/rpc.d.ts +38 -0
  104. package/agent-hub/dist/rpc.js +52 -0
  105. package/agent-hub/dist/spawn-world.d.ts +46 -0
  106. package/agent-hub/dist/spawn-world.js +72 -0
  107. package/agent-hub/dist/targets.d.ts +33 -0
  108. package/agent-hub/dist/targets.js +30 -0
  109. package/agent-hub/dist/world-entry.d.ts +31 -0
  110. package/agent-hub/dist/world-entry.js +51 -0
  111. package/agent-hub/dist/world-host.d.ts +69 -0
  112. package/agent-hub/dist/world-host.js +44 -0
  113. package/agent-hub/dist/world-join.d.ts +16 -0
  114. package/agent-hub/dist/world-join.js +122 -0
  115. package/agent-hub/dist/world-mount.d.ts +52 -0
  116. package/agent-hub/dist/world-mount.js +52 -0
  117. package/agent-hub/dist/world-mux.d.ts +72 -0
  118. package/agent-hub/dist/world-mux.js +224 -0
  119. package/agent-hub/dist/world-provision.d.ts +20 -0
  120. package/agent-hub/dist/world-provision.js +122 -0
  121. package/agent-hub/dist/world-web-server.d.ts +162 -0
  122. package/agent-hub/dist/world-web-server.js +248 -0
  123. package/agent-hub/lib/client/index.js +242 -0
  124. package/agent-hub/package.json +65 -0
  125. package/agent-omp/README.md +77 -0
  126. package/agent-omp/cordis.patch.yml +92 -0
  127. package/agent-omp/dist/adapter.js +36 -0
  128. package/agent-omp/dist/agent-preset-omp.js +171 -0
  129. package/agent-omp/dist/agent-preset-projection.js +36 -0
  130. package/agent-omp/dist/agent.js +1145 -0
  131. package/agent-omp/dist/app-home.js +86 -0
  132. package/agent-omp/dist/discovery.js +116 -0
  133. package/agent-omp/dist/inbox.js +53 -0
  134. package/agent-omp/dist/index.js +574 -0
  135. package/agent-omp/dist/knobs.js +50 -0
  136. package/agent-omp/dist/lazy-rpc.js +161 -0
  137. package/agent-omp/dist/mobile/gesture.js +148 -0
  138. package/agent-omp/dist/mobile/zoom-guard.js +294 -0
  139. package/agent-omp/dist/mobile-boot.js +38 -0
  140. package/agent-omp/dist/models.js +299 -0
  141. package/agent-omp/dist/omp-cli.js +147 -0
  142. package/agent-omp/dist/omp-disk-discovery.js +178 -0
  143. package/agent-omp/dist/omp-store.js +518 -0
  144. package/agent-omp/dist/pairing.js +45 -0
  145. package/agent-omp/dist/permission.js +96 -0
  146. package/agent-omp/dist/protocol.js +13 -0
  147. package/agent-omp/dist/rpc-types.js +1 -0
  148. package/agent-omp/dist/sdk-client.js +378 -0
  149. package/agent-omp/dist/sidecar-client.js +195 -0
  150. package/agent-omp/dist/store/adapter.js +47 -0
  151. package/agent-omp/dist/store/db.js +210 -0
  152. package/agent-omp/dist/store/index.js +59 -0
  153. package/agent-omp/dist/store/reconcile.js +51 -0
  154. package/agent-omp/dist/store/schema.js +40 -0
  155. package/agent-omp/dist/world-plugin.js +96 -0
  156. package/agent-omp/package.json +97 -0
  157. package/agent-omp/sidecar/main.ts +481 -0
  158. package/agent-pi/README.md +67 -0
  159. package/agent-pi/cordis.patch.yml +80 -0
  160. package/agent-pi/dist/adapter.js +53 -0
  161. package/agent-pi/dist/agent-preset-pi.js +169 -0
  162. package/agent-pi/dist/agent-preset-projection.js +29 -0
  163. package/agent-pi/dist/agent.js +949 -0
  164. package/agent-pi/dist/inbox.js +53 -0
  165. package/agent-pi/dist/index.js +595 -0
  166. package/agent-pi/dist/knobs.js +24 -0
  167. package/agent-pi/dist/models.js +200 -0
  168. package/agent-pi/dist/permission.js +66 -0
  169. package/agent-pi/dist/pi-client.js +224 -0
  170. package/agent-pi/dist/pi-events.js +155 -0
  171. package/agent-pi/dist/pi-home.js +55 -0
  172. package/agent-pi/dist/safe-json.js +25 -0
  173. package/agent-pi/dist/session-map.js +119 -0
  174. package/agent-pi/dist/world-plugin.js +97 -0
  175. package/agent-pi/package.json +77 -0
  176. package/cordis.patch.yml +22 -19
  177. package/package.json +88 -25
  178. package/client.js +0 -4
@@ -0,0 +1,357 @@
1
+ /**
2
+ * ClaudeSdkClient — the lazy, host-binary-driven Claude Agent SDK bridge client.
3
+ *
4
+ * The Claude Agent SDK is a *bridge*: `query()` spawns a real Claude Code CLI
5
+ * subprocess and speaks the stream-json control protocol to it. This client owns
6
+ * that bridge's lifecycle for the adapter. It hands `query()` a streaming-input
7
+ * iterable (an {@link InputQueue}, which is what makes the `Query` control
8
+ * methods exist at all), drives the bridge lazily per session, and fans every
9
+ * message out through `projectClaudeEvent` (claude-events.ts).
10
+ *
11
+ * ## The two hard deviations from the codex precedent
12
+ *
13
+ * 1. **No shared singleton.** `agent-codex` multiplexes all clients through one
14
+ * module-level refcounted SDK instance. That is impossible here: the Claude
15
+ * SDK's `Query` owns its own CLI subprocess AND its own streaming-input
16
+ * iterator, so one `Query` cannot serve multiple sessions. Each
17
+ * `ClaudeSdkClient` instance therefore owns its own `Query`. The module-level
18
+ * `claudeClientRefCount()` still tracks how many instances are live (it goes
19
+ * 1 -> 0 across construction and `close()`), but it never shares a query
20
+ * between instances.
21
+ *
22
+ * 2. **`Options.env` REPLACES the subprocess environment.** Verified verbatim
23
+ * from `sdk.d.ts`: "When set, this value REPLACES the subprocess environment
24
+ * entirely — it is not merged with `process.env`. Spread `process.env`
25
+ * yourself if the subprocess still needs inherited variables like `PATH`,
26
+ * `HOME`, or `ANTHROPIC_API_KEY`." So the options built here are always
27
+ * `env: { ...process.env }`. A bare partial env would strip PATH/HOME/the
28
+ * API key and the CLI would fail with an opaque startup error far from the
29
+ * cause. NO `CLAUDE_CONFIG_DIR` is injected: the CLI resolves its own
30
+ * native `~/.claude` (2026-09-17 home ruling; tests redirect via the
31
+ * ambient env).
32
+ *
33
+ * ## Design decisions the brief left open (each pinned by a test)
34
+ *
35
+ * - **Zero-IO construction**: the injectable factory is never called until the
36
+ * first `prompt()` runs `ensureStarted()`.
37
+ * - **Pending control calls** (`setModel` / `setPermissionMode` / `interrupt`)
38
+ * made before the query exists are *buffered and replayed in order* inside
39
+ * `ensureStarted()` — never silently dropped. They do NOT spawn the CLI on
40
+ * their own (a control call is not a turn).
41
+ * - **Reader loop**: consumes the query's async iterator, projects each message,
42
+ * and delivers the wire events to listeners. On normal iterator end it emits
43
+ * `{ type: "agent_end" }`; on a throw it emits
44
+ * `{ type: "agent_end", isError: true, error }` and records the failure. The
45
+ * loop catches its own errors, so a stream that ends never leaves a dangling
46
+ * or unhandled promise.
47
+ * - **`close()`** is idempotent and safe before `ensureStarted()`; it closes the
48
+ * input queue (if any), closes the query (if any), and decrements the refcount
49
+ * exactly once.
50
+ * - **Post-close pushes**: the client guards with a clear
51
+ * `"agent-claude: client is closed"` error *before* reaching the input queue,
52
+ * rather than surfacing the queue's lower-level "input queue is closed".
53
+ * - **`followUp`** is surface parity with `prompt` — in streaming-input mode
54
+ * every push is queued in order and there is no separate follow-up channel.
55
+ * `steer` differs by pushing with `priority: "now"`.
56
+ */
57
+ import { query as officialQuery } from "@anthropic-ai/claude-agent-sdk";
58
+ import { InputQueue } from "./input-queue.js";
59
+ import { projectClaudeEvent } from "./claude-events.js";
60
+ /** Host Claude Code binary driven through the SDK's bridge (spec ruling R5). */
61
+ export const DEFAULT_CLAUDE_EXECUTABLE = "/home/u1/.local/bin/claude";
62
+ const defaultFactory = (o) => officialQuery({
63
+ prompt: o.prompt,
64
+ options: o.options,
65
+ });
66
+ const defaultExecutableResolver = () => process.env.CLAUDE_EXECUTABLE ?? DEFAULT_CLAUDE_EXECUTABLE;
67
+ let factory = defaultFactory;
68
+ let executableResolver = defaultExecutableResolver;
69
+ let refcount = 0;
70
+ /** Replace the SDK entry point (tests inject a fake query). */
71
+ export function setClaudeFactory(next) {
72
+ factory = next;
73
+ }
74
+ /** Replace the pathToClaudeCodeExecutable resolution. */
75
+ export function setClaudeExecutableResolver(next) {
76
+ executableResolver = next;
77
+ }
78
+ /** Test/observability hook: current live-instance refcount (0 = none alive). */
79
+ export function claudeClientRefCount() {
80
+ return refcount;
81
+ }
82
+ /** Restore module-level seams and refcount (test isolation only). */
83
+ export function resetClaudeClientState() {
84
+ refcount = 0;
85
+ factory = defaultFactory;
86
+ executableResolver = defaultExecutableResolver;
87
+ }
88
+ /**
89
+ * Boot model probe (RC-3): spawn ONE ephemeral SDK query — a streaming-input
90
+ * queue with NO user prompt — ask its `supportedModels()`, and close (the
91
+ * hermes probe-client-then-close precedent). Runs through the module factory,
92
+ * so tests inject a fake query. `persistSession: false` keeps the probe from
93
+ * leaving a session in the CLI's native home. Failure (including the timeout
94
+ * race, which guards against a wedged startup pinning a subprocess forever)
95
+ * is the caller's to trace; the boot catalog stays as-is.
96
+ */
97
+ export async function probeClaudeModels(config = {}) {
98
+ const queue = new InputQueue();
99
+ const query = factory({
100
+ prompt: queue,
101
+ options: {
102
+ cwd: config.cwd ?? process.cwd(),
103
+ env: { ...process.env },
104
+ pathToClaudeCodeExecutable: executableResolver(),
105
+ persistSession: false,
106
+ },
107
+ });
108
+ const timeoutMs = config.timeoutMs ?? 20_000;
109
+ try {
110
+ const ask = typeof query.supportedModels === "function"
111
+ ? query.supportedModels()
112
+ : Promise.resolve([]);
113
+ const models = await Promise.race([
114
+ ask,
115
+ new Promise((_, reject) => {
116
+ const timer = setTimeout(() => reject(new Error(`boot model probe timed out after ${timeoutMs}ms`)), timeoutMs);
117
+ timer.unref?.();
118
+ }),
119
+ ]);
120
+ return Array.isArray(models) ? models : [];
121
+ }
122
+ finally {
123
+ queue.close();
124
+ query.close();
125
+ }
126
+ }
127
+ export class ClaudeSdkClient {
128
+ cwdPath;
129
+ claudeSessionId;
130
+ resumeSessionId;
131
+ canUseTool;
132
+ allowDangerouslySkipPermissions;
133
+ /** True while a user turn is in flight (set on push, cleared on `turn_end`/`agent_end`). */
134
+ turnActive = false;
135
+ queue;
136
+ query;
137
+ pendingControls = [];
138
+ listeners = new Set();
139
+ threadIdValue = null;
140
+ closed = false;
141
+ terminated = false;
142
+ failure = null;
143
+ constructor(config) {
144
+ if (config.claudeSessionId !== undefined && config.resumeSessionId !== undefined) {
145
+ throw new Error("agent-claude: claudeSessionId and resumeSessionId are mutually exclusive");
146
+ }
147
+ this.cwdPath = config.cwd ?? process.cwd();
148
+ this.claudeSessionId = config.claudeSessionId;
149
+ this.resumeSessionId = config.resumeSessionId;
150
+ this.canUseTool = config.canUseTool;
151
+ this.allowDangerouslySkipPermissions = config.allowDangerouslySkipPermissions;
152
+ refcount += 1;
153
+ }
154
+ /**
155
+ * Whether the query has been constructed yet (lazy spawn). This reports
156
+ * "the query was constructed", not "the session is alive": `close()` does
157
+ * not clear it.
158
+ */
159
+ get spawned() {
160
+ return this.query !== undefined;
161
+ }
162
+ /** The cwd the CLI will be spawned in (constructor fact, read by the agent). */
163
+ get cwd() {
164
+ return this.cwdPath;
165
+ }
166
+ /** The SDK session id, filled from the session-init message when it arrives. */
167
+ get threadId() {
168
+ return this.threadIdValue;
169
+ }
170
+ /** The client's own running/streaming attestation (idle-TTL revalidation gate). */
171
+ async getState() {
172
+ return { isStreaming: this.turnActive };
173
+ }
174
+ /** Advertise the CLI's supported models; empty before the query exists. */
175
+ async supportedModels() {
176
+ if (this.query === undefined)
177
+ return [];
178
+ if (this.query.supportedModels === undefined)
179
+ return [];
180
+ return this.query.supportedModels();
181
+ }
182
+ /**
183
+ * Advertise the CLI's slash commands (name + description + argument hint);
184
+ * empty before the query exists, and empty on a CLI/fake without the control.
185
+ * The SDK returns its latest pushed snapshot; nothing here re-fetches the
186
+ * CLI mid-session, so a command the runtime learns about after `session_init`
187
+ * (unless the `inject` re-run reports it) never appears. That is a known
188
+ * limitation, not a freshness guarantee.
189
+ */
190
+ async supportedCommands() {
191
+ if (this.query === undefined)
192
+ return [];
193
+ if (this.query.supportedCommands === undefined)
194
+ return [];
195
+ return this.query.supportedCommands();
196
+ }
197
+ /** Register a wire event listener; returns the unsubscriber. */
198
+ on(listener) {
199
+ this.listeners.add(listener);
200
+ return () => {
201
+ this.listeners.delete(listener);
202
+ };
203
+ }
204
+ /**
205
+ * Lazily construct the query (zero-IO until this runs). Everything up to the
206
+ * `this.query = query` assignment is synchronous, so concurrent first prompts
207
+ * can never build two queries: the first caller assigns `this.query` before
208
+ * any `await` yields control. This single-query invariant therefore depends
209
+ * on the `ClaudeFactory` contract staying synchronous (it must return the
210
+ * `Query` without awaiting). The reader loop is started fire-and-forget before
211
+ * any pending control call is replayed, so a throwing control can never leave
212
+ * the CLI undrained.
213
+ */
214
+ async ensureStarted() {
215
+ if (this.query !== undefined)
216
+ return;
217
+ const queue = new InputQueue();
218
+ const query = factory({ prompt: queue, options: this.buildOptions() });
219
+ this.queue = queue;
220
+ this.query = query;
221
+ void this.runReader(query);
222
+ const controls = this.pendingControls;
223
+ this.pendingControls = [];
224
+ for (const control of controls) {
225
+ await control();
226
+ }
227
+ }
228
+ /**
229
+ * Queue a user turn (plain priority). Accepts plain text OR transcoded
230
+ * Anthropic blocks; the block array is handed through unchanged.
231
+ */
232
+ async prompt(content) {
233
+ this.ensureUsable();
234
+ await this.ensureStarted();
235
+ this.push(content, undefined);
236
+ }
237
+ /** Surface parity with prompt: a plain queued user turn. */
238
+ async followUp(content) {
239
+ this.ensureUsable();
240
+ await this.ensureStarted();
241
+ this.push(content, undefined);
242
+ }
243
+ /** Steer the in-flight turn: a user message with priority "now". */
244
+ async steer(content) {
245
+ this.ensureUsable();
246
+ await this.ensureStarted();
247
+ this.push(content, "now");
248
+ }
249
+ /** Select the active model (buffered until the query exists, then replayed). */
250
+ async setModel(model) {
251
+ this.ensureUsable();
252
+ if (this.query === undefined) {
253
+ this.pendingControls.push(() => this.query?.setModel(model));
254
+ return;
255
+ }
256
+ await this.query.setModel(model);
257
+ }
258
+ /** Change the permission mode (buffered until the query exists, then replayed). */
259
+ async setPermissionMode(mode) {
260
+ this.ensureUsable();
261
+ if (this.query === undefined) {
262
+ this.pendingControls.push(() => this.query?.setPermissionMode(mode));
263
+ return;
264
+ }
265
+ await this.query.setPermissionMode(mode);
266
+ }
267
+ /** Interrupt the current turn (buffered until the query exists, then replayed). */
268
+ async interrupt() {
269
+ this.ensureUsable();
270
+ if (this.query === undefined) {
271
+ this.pendingControls.push(() => this.query?.interrupt());
272
+ return undefined;
273
+ }
274
+ return this.query.interrupt();
275
+ }
276
+ /** Close the queue, close the query, and release the refcount (idempotent). */
277
+ close() {
278
+ if (this.closed)
279
+ return;
280
+ this.closed = true;
281
+ this.queue?.close();
282
+ this.query?.close();
283
+ refcount = Math.max(0, refcount - 1);
284
+ }
285
+ // --- internals ------------------------------------------------------------
286
+ buildOptions() {
287
+ return {
288
+ cwd: this.cwdPath,
289
+ env: { ...process.env },
290
+ pathToClaudeCodeExecutable: executableResolver(),
291
+ persistSession: true,
292
+ ...(this.claudeSessionId !== undefined ? { sessionId: this.claudeSessionId } : {}),
293
+ ...(this.resumeSessionId !== undefined ? { resume: this.resumeSessionId } : {}),
294
+ ...(this.canUseTool !== undefined ? { canUseTool: this.canUseTool } : {}),
295
+ ...(this.allowDangerouslySkipPermissions !== undefined ? { allowDangerouslySkipPermissions: this.allowDangerouslySkipPermissions } : {}),
296
+ };
297
+ }
298
+ /**
299
+ * Push one turn through the queue. A plain string keeps the historical
300
+ * single-text-block normalization; a block array is handed through AS-IS so
301
+ * the transcoder's one-block-per-input-block shape reaches the SDK unchanged.
302
+ */
303
+ push(content, priority) {
304
+ this.turnActive = true;
305
+ this.queue.pushContent(content, {
306
+ parent_tool_use_id: null,
307
+ ...(this.claudeSessionId !== undefined ? { session_id: this.claudeSessionId } : {}),
308
+ ...(priority !== undefined ? { priority } : {}),
309
+ });
310
+ }
311
+ ensureUsable() {
312
+ if (this.closed)
313
+ throw new Error("agent-claude: client is closed");
314
+ if (this.failure !== null)
315
+ throw this.failure;
316
+ if (this.terminated)
317
+ throw new Error("agent-claude: session has ended");
318
+ }
319
+ async runReader(query) {
320
+ try {
321
+ for await (const message of query) {
322
+ const wire = projectClaudeEvent(message);
323
+ if (wire === null)
324
+ continue;
325
+ const events = Array.isArray(wire) ? wire : [wire];
326
+ for (const event of events) {
327
+ if (event.type === "session_init" && typeof event.sessionId === "string") {
328
+ this.threadIdValue = event.sessionId;
329
+ }
330
+ if (event.type === "turn_end")
331
+ this.turnActive = false;
332
+ this.emit(event);
333
+ }
334
+ }
335
+ this.terminated = true;
336
+ this.turnActive = false;
337
+ this.emit({ type: "agent_end" });
338
+ }
339
+ catch (error) {
340
+ this.terminated = true;
341
+ this.failure = error instanceof Error ? error : new Error(String(error));
342
+ process.stderr.write(`agent-claude: query stream error: ${String(error)}\n`);
343
+ this.turnActive = false;
344
+ this.emit({ type: "agent_end", isError: true, error: this.failure.message });
345
+ }
346
+ }
347
+ emit(event) {
348
+ for (const listener of [...this.listeners]) {
349
+ try {
350
+ listener(event);
351
+ }
352
+ catch (error) {
353
+ process.stderr.write(`agent-claude: listener error: ${String(error)}\n`);
354
+ }
355
+ }
356
+ }
357
+ }
@@ -0,0 +1,223 @@
1
+ let toolNames = new Map();
2
+ export function resetProjectionState() {
3
+ toolNames = new Map();
4
+ }
5
+ /**
6
+ * SDK tool name → native dsh tool name (2026-09-18). Keys are the Claude
7
+ * Agent SDK's built-in tool names; values are the dsh vocabulary the WebUI's
8
+ * typed presenters recognize. Unknown names pass through untouched.
9
+ */
10
+ const TOOL_NAME_ALIASES = {
11
+ Bash: "bash",
12
+ Read: "read",
13
+ Write: "write",
14
+ Edit: "edit",
15
+ Grep: "grep",
16
+ Glob: "glob",
17
+ MultiEdit: "edit",
18
+ NotebookEdit: "edit",
19
+ };
20
+ /** Normalize one SDK tool name onto the dsh vocabulary (verbatim when unknown). */
21
+ export function normalizeToolName(name) {
22
+ return TOOL_NAME_ALIASES[name] ?? name;
23
+ }
24
+ function asRecord(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value)
26
+ ? value
27
+ : null;
28
+ }
29
+ function textOf(content) {
30
+ const result = { text: "", reasoning: "", tool: [] };
31
+ if (!Array.isArray(content))
32
+ return result;
33
+ for (const raw of content) {
34
+ const block = asRecord(raw);
35
+ if (block === null)
36
+ continue;
37
+ if (block.type === "text" && typeof block.text === "string")
38
+ result.text += block.text;
39
+ // Reasoning reaches the wire in two shapes: the Beta API `reasoning`
40
+ // block (`{type, text}`) and the CLI's `thinking` block (`{type,
41
+ // thinking}`). Both fold into the same reasoning text.
42
+ if (block.type === "reasoning" && typeof block.text === "string")
43
+ result.reasoning += block.text;
44
+ if (block.type === "thinking" && typeof block.thinking === "string")
45
+ result.reasoning += block.thinking;
46
+ if (block.type === "tool_use")
47
+ result.tool.push(block);
48
+ }
49
+ return result;
50
+ }
51
+ /**
52
+ * Claude `TodoWrite` input → the DSH `todo/write` whole-list snapshot payload
53
+ * (`TodoItem { content, status }`; upstream packages/todo/tool-todo). The
54
+ * whole list is replaced on every write (latest-wins). Defensive: rows with
55
+ * no usable content text are dropped, and an unknown status degrades to
56
+ * `pending`. Returns undefined when the input carries no todos array at all
57
+ * (not a todo write), so a malformed call never clears the list.
58
+ */
59
+ function todoSnapshot(input) {
60
+ if (input === null || typeof input !== "object" || Array.isArray(input))
61
+ return undefined;
62
+ const items = input.todos;
63
+ if (!Array.isArray(items))
64
+ return undefined;
65
+ const todos = [];
66
+ for (const entry of items) {
67
+ const record = asRecord(entry);
68
+ if (record === null)
69
+ continue;
70
+ const content = typeof record.content === "string" ? record.content : "";
71
+ if (content === "")
72
+ continue;
73
+ const status = record.status === "completed" || record.status === "in_progress" ? record.status : "pending";
74
+ todos.push({ content, status });
75
+ }
76
+ return todos;
77
+ }
78
+ export function projectClaudeEvent(message) {
79
+ const msg = asRecord(message);
80
+ if (msg === null || typeof msg.type !== "string")
81
+ return null;
82
+ if (msg.type === "system") {
83
+ switch (msg.subtype) {
84
+ case "init":
85
+ return {
86
+ type: "session_init",
87
+ sessionId: msg.session_id,
88
+ cwd: msg.cwd,
89
+ model: msg.model,
90
+ permissionMode: msg.permissionMode,
91
+ tools: msg.tools,
92
+ slashCommands: msg.slash_commands,
93
+ skills: msg.skills,
94
+ plugins: msg.plugins,
95
+ mcpServers: msg.mcp_servers,
96
+ cliVersion: msg.claude_code_version,
97
+ apiKeySource: msg.apiKeySource,
98
+ };
99
+ case "compact_boundary":
100
+ return { type: "compaction", metadata: msg.compact_metadata };
101
+ case "permission_denied":
102
+ return {
103
+ type: "permission_denied",
104
+ toolName: msg.tool_name,
105
+ callId: msg.tool_use_id,
106
+ reasonType: msg.decision_reason_type,
107
+ };
108
+ case "model_refusal_fallback":
109
+ return {
110
+ type: "refusal_fallback",
111
+ direction: msg.direction,
112
+ scope: msg.scope ?? "session",
113
+ originalModel: msg.original_model,
114
+ fallbackModel: msg.fallback_model,
115
+ category: msg.api_refusal_category ?? null,
116
+ explanation: msg.api_refusal_explanation ?? null,
117
+ retractedMessageUuids: msg.retracted_message_uuids ?? [],
118
+ refusedUserMessageUuid: msg.refused_user_message_uuid ?? null,
119
+ content: msg.content,
120
+ };
121
+ case "model_refusal_no_fallback":
122
+ return {
123
+ type: "refusal_no_fallback",
124
+ originalModel: msg.original_model,
125
+ category: msg.api_refusal_category ?? null,
126
+ explanation: msg.api_refusal_explanation ?? null,
127
+ refusedUserMessageUuid: msg.refused_user_message_uuid ?? null,
128
+ content: msg.content,
129
+ };
130
+ case "local_command_output":
131
+ return { type: "local_command_output", content: msg.content };
132
+ default:
133
+ return null;
134
+ }
135
+ }
136
+ if (msg.type === "assistant") {
137
+ const body = asRecord(msg.message);
138
+ const parsed = textOf(body?.content);
139
+ const out = [];
140
+ // Per-message accounting rides every event projected from THIS message
141
+ // (RC-6): the agent closes the step's attempt with the usage of the
142
+ // message that opened it, so a tool_start flush still lands a sample.
143
+ const usage = asRecord(body?.usage);
144
+ const model = typeof body?.model === "string" && body.model !== "" ? body.model : undefined;
145
+ const provenance = {
146
+ ...(usage === null ? {} : { usage }),
147
+ ...(model === undefined ? {} : { model }),
148
+ };
149
+ if (parsed.reasoning !== "")
150
+ out.push({ type: "assistant_reasoning", text: parsed.reasoning, parentToolUseId: msg.parent_tool_use_id ?? null, ...provenance });
151
+ if (parsed.text !== "")
152
+ out.push({ type: "assistant_text", text: parsed.text, parentToolUseId: msg.parent_tool_use_id ?? null, ...provenance });
153
+ for (const block of parsed.tool) {
154
+ const id = String(block.id ?? "");
155
+ const name = normalizeToolName(String(block.name ?? "tool"));
156
+ if (id !== "")
157
+ toolNames.set(id, name);
158
+ out.push({
159
+ type: "tool_start",
160
+ callId: id,
161
+ name,
162
+ arguments: block.input ?? {},
163
+ parentToolUseId: msg.parent_tool_use_id ?? null,
164
+ ...provenance,
165
+ });
166
+ // `TodoWrite` ALSO projects the DSH `todo/write` whole-list snapshot
167
+ // (log-only, latest-wins) after the tool_start — the tool pair stays.
168
+ if (String(block.name ?? "") === "TodoWrite") {
169
+ const todos = todoSnapshot(block.input ?? null);
170
+ if (todos !== undefined)
171
+ out.push({ type: "todo_write", todos });
172
+ }
173
+ }
174
+ if (out.length === 0)
175
+ out.push({ type: "assistant_message", raw: body ?? null });
176
+ return out;
177
+ }
178
+ if (msg.type === "user") {
179
+ const body = asRecord(msg.message);
180
+ const content = Array.isArray(body?.content) ? body.content : [];
181
+ const out = [];
182
+ for (const raw of content) {
183
+ const block = asRecord(raw);
184
+ if (block?.type !== "tool_result")
185
+ continue;
186
+ const callId = String(block.tool_use_id ?? "");
187
+ const text = typeof block.content === "string"
188
+ ? block.content
189
+ : Array.isArray(block.content)
190
+ ? block.content.map((part) => String(asRecord(part)?.text ?? "")).join("")
191
+ : "";
192
+ out.push({
193
+ type: "tool_end",
194
+ callId,
195
+ name: toolNames.get(callId) ?? "tool",
196
+ content: text,
197
+ isError: block.is_error === true,
198
+ parentToolUseId: msg.parent_tool_use_id ?? null,
199
+ });
200
+ }
201
+ // One result is the common shape and stays a bare event; only a parallel
202
+ // tool_result batch needs the array form (mirrors codex-events' toolEnd).
203
+ return out.length === 0 ? null : out.length === 1 ? out[0] : out;
204
+ }
205
+ if (msg.type === "result") {
206
+ return {
207
+ type: "turn_end",
208
+ subtype: msg.subtype,
209
+ isError: msg.is_error === true,
210
+ numTurns: msg.num_turns,
211
+ durationMs: msg.duration_ms,
212
+ durationApiMs: msg.duration_api_ms,
213
+ costUsd: msg.total_cost_usd,
214
+ usage: msg.usage,
215
+ modelUsage: msg.modelUsage,
216
+ permissionDenials: msg.permission_denials,
217
+ result: msg.result,
218
+ errors: msg.errors,
219
+ stopReason: msg.stop_reason ?? null,
220
+ };
221
+ }
222
+ return null;
223
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Claude Code app-home resolution (native home, user ruling 2026-09-17).
3
+ *
4
+ * The installed Claude CLI keeps its own native home (`~/.claude`; an ambient
5
+ * `CLAUDE_CONFIG_DIR` overrides in tests). The adapter never redirects it at
6
+ * spawn and never copies config into it — this supersedes the S7 nested-home
7
+ * layout (`<dshHome>/agents/claude`) and its one-way config valve, matching
8
+ * the agent-pi precedent.
9
+ */
10
+ import { homedir } from "node:os";
11
+ import { join, resolve } from "node:path";
12
+ const PROD_HOMES = [join(homedir(), ".dsh"), join(homedir(), ".superd")];
13
+ /** Refuse any path that resolves into a production DSH home (repo red line). */
14
+ export function assertNotProdHome(path, label) {
15
+ const p = resolve(path);
16
+ if (PROD_HOMES.includes(p) || PROD_HOMES.some((prod) => p.startsWith(`${prod}/`))) {
17
+ throw new Error(`agent-claude: refusing prod home "${p}" for ${label} — set DSH_HOME to the test home`);
18
+ }
19
+ }
20
+ /**
21
+ * The Claude app home: the NATIVE `~/.claude`, prod-home guarded. An ambient
22
+ * `CLAUDE_CONFIG_DIR` wins (tests pin an isolated home that way); the adapter
23
+ * itself never sets the variable when spawning the CLI.
24
+ */
25
+ export function resolveClaudeHome() {
26
+ const claudeHome = resolve(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"));
27
+ assertNotProdHome(claudeHome, "claudeHome");
28
+ return claudeHome;
29
+ }