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.
- package/agent-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- 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
|
+
}
|