@norman-else/dsh-claude 0.1.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.
@@ -0,0 +1,302 @@
1
+ import { l as redactText } from "./events-qlmU1KrH.mjs";
2
+ //#region src/command-bridge.ts
3
+ const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u;
4
+ const MAX_DESCRIPTION_CHARS = 300;
5
+ const MAX_HINT_CHARS = 120;
6
+ /** DSH registry names reject `:`, but plugin-qualified Claude skills carry it
7
+ * (e.g. `awesome-skills:ci-deploy`). Derive a registry-safe public name while
8
+ * forwarding keeps the exact Claude-side name. */
9
+ function registryName(claudeName) {
10
+ if (COMMAND_NAME.test(claudeName)) return claudeName;
11
+ const short = claudeName.includes(":") ? claudeName.slice(claudeName.lastIndexOf(":") + 1) : void 0;
12
+ if (short !== void 0 && COMMAND_NAME.test(short)) return short;
13
+ const sanitized = claudeName.replaceAll(":", "-").toLowerCase();
14
+ return COMMAND_NAME.test(sanitized) ? sanitized : void 0;
15
+ }
16
+ /**
17
+ * Names registered as CLIENT-side commandUi contributions (not host
18
+ * commands). The DSH web command palette fails its whole command group when
19
+ * a host command name equals a contribution name
20
+ * ("contribution /model collides with a host command"), so these can never
21
+ * be registered by this bridge. Keep in sync with the client bundles:
22
+ * dsh-client-ui-model-selection registers "model".
23
+ */
24
+ const CLIENT_CONTRIBUTION_NAMES = /* @__PURE__ */ new Set(["model"]);
25
+ /**
26
+ * Cordis service name the preset route provides (behind an entry-local
27
+ * isolate realm, so each session gets its own instance) and the host reads
28
+ * back with dsh-agent-presets' official `serviceForAgent(ctx, agent, name)`
29
+ * — the supported cross-scope read for callers that already hold the agent.
30
+ */
31
+ const CLAUDE_COMMANDS_SERVICE = "claudeCommands";
32
+ function bounded(value, maxChars) {
33
+ return redactText(value, maxChars);
34
+ }
35
+ function desiredCommands(catalog, reservedNames, forward) {
36
+ const desired = /* @__PURE__ */ new Map();
37
+ const assigned = /* @__PURE__ */ new Set();
38
+ for (const command of catalog) {
39
+ const names = [command.name, ...command.aliases ?? []];
40
+ for (const claudeName of names) {
41
+ const base = registryName(claudeName);
42
+ if (base === void 0) continue;
43
+ let publicName = base;
44
+ let prefixed = false;
45
+ if (reservedNames.has(publicName) || assigned.has(publicName)) {
46
+ publicName = `claude-${base}`;
47
+ prefixed = true;
48
+ }
49
+ if (!COMMAND_NAME.test(publicName) || reservedNames.has(publicName) || assigned.has(publicName)) continue;
50
+ const description = bounded(command.description || `Claude Code /${command.name}`, MAX_DESCRIPTION_CHARS);
51
+ const hint = bounded(command.argumentHint ?? "", MAX_HINT_CHARS);
52
+ const signature = JSON.stringify({
53
+ publicName,
54
+ claudeName,
55
+ description,
56
+ hint
57
+ });
58
+ desired.set(publicName, {
59
+ publicName,
60
+ claudeName,
61
+ prefixed,
62
+ signature,
63
+ definition: {
64
+ name: publicName,
65
+ description,
66
+ ...hint.length === 0 ? {} : { input: { hint } },
67
+ recordInput: false,
68
+ handler: ({ rawInput }) => {
69
+ forward(`/${claudeName}${rawInput}`);
70
+ return { kind: "success" };
71
+ }
72
+ }
73
+ });
74
+ assigned.add(publicName);
75
+ }
76
+ }
77
+ return desired;
78
+ }
79
+ var ClaudeCommandBridge = class {
80
+ #target;
81
+ #live = /* @__PURE__ */ new Map();
82
+ constructor(target) {
83
+ this.#target = target;
84
+ }
85
+ refresh(catalog) {
86
+ const owned = new Set(this.#live.keys());
87
+ const reserved = new Set(this.#target.list().map((command) => command.name).filter((name) => !owned.has(name)));
88
+ for (const name of CLIENT_CONTRIBUTION_NAMES) reserved.add(name);
89
+ const desired = desiredCommands(catalog, reserved, (line) => this.#target.forward(line));
90
+ for (const [name, live] of [...this.#live]) {
91
+ if (desired.get(name)?.signature === live.signature) continue;
92
+ live.dispose();
93
+ this.#live.delete(name);
94
+ }
95
+ for (const [name, next] of desired) {
96
+ if (this.#live.has(name)) continue;
97
+ this.#live.set(name, {
98
+ signature: next.signature,
99
+ dispose: this.#target.register(next.definition)
100
+ });
101
+ }
102
+ return [...desired.values()].map(({ publicName, claudeName, prefixed }) => ({
103
+ publicName,
104
+ claudeName,
105
+ prefixed
106
+ }));
107
+ }
108
+ dispose() {
109
+ for (const command of this.#live.values()) command.dispose();
110
+ this.#live.clear();
111
+ }
112
+ };
113
+ //#endregion
114
+ //#region src/presenters.ts
115
+ function record(value) {
116
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
117
+ }
118
+ function str(value) {
119
+ return typeof value === "string" && value.length > 0 ? value : void 0;
120
+ }
121
+ function resultText(result) {
122
+ return result.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
123
+ }
124
+ /** Bash: a terminal card headed by the command, described above by Claude's own summary. */
125
+ function bashCallView(args) {
126
+ const command = str(record(args)?.command);
127
+ if (command === void 0) return void 0;
128
+ const description = str(record(args)?.description);
129
+ return {
130
+ card: "terminal",
131
+ title: command,
132
+ ...description === void 0 ? {} : { description }
133
+ };
134
+ }
135
+ function terminalResultView(_args, result) {
136
+ const output = resultText(result);
137
+ return output.length === 0 ? void 0 : {
138
+ card: "terminal",
139
+ output
140
+ };
141
+ }
142
+ /** Read: a generic read card that follow-alongs the file window. */
143
+ function readCallView(args) {
144
+ const arguments_ = record(args);
145
+ const path = str(arguments_?.file_path);
146
+ if (path === void 0) return void 0;
147
+ const offset = typeof arguments_?.offset === "number" ? arguments_.offset : void 0;
148
+ return {
149
+ card: "generic",
150
+ kind: "read",
151
+ title: `Read ${path}`,
152
+ locations: [{
153
+ path,
154
+ ...offset === void 0 ? {} : { line: offset }
155
+ }]
156
+ };
157
+ }
158
+ function fileDiffs(args) {
159
+ const arguments_ = record(args);
160
+ const path = str(arguments_?.file_path);
161
+ if (path === void 0) return void 0;
162
+ if (Array.isArray(arguments_?.edits)) {
163
+ const diffs = [];
164
+ for (const edit of arguments_.edits) {
165
+ const item = record(edit);
166
+ const oldText = str(item?.old_string);
167
+ const newText = str(item?.new_string);
168
+ if (newText === void 0) continue;
169
+ diffs.push({
170
+ path,
171
+ oldText: oldText ?? null,
172
+ newText
173
+ });
174
+ }
175
+ return diffs.length > 0 ? diffs : void 0;
176
+ }
177
+ const newText = str(arguments_?.new_string) ?? arguments_?.content;
178
+ if (typeof newText !== "string" || newText.length === 0) return void 0;
179
+ return [{
180
+ path,
181
+ oldText: str(arguments_?.old_string) ?? null,
182
+ newText
183
+ }];
184
+ }
185
+ /** Edit / MultiEdit / Write: inline diff cards derived from the call arguments. */
186
+ function diffCallView(args) {
187
+ const arguments_ = record(args);
188
+ const path = str(arguments_?.file_path);
189
+ const diffs = fileDiffs(args);
190
+ if (path === void 0 || diffs === void 0) return void 0;
191
+ return {
192
+ card: "diff",
193
+ title: `${typeof arguments_?.new_string === "string" || Array.isArray(arguments_?.edits) ? "Edit" : "Write"} ${path}`,
194
+ diffs,
195
+ locations: [{ path }]
196
+ };
197
+ }
198
+ /** Grep / Glob / WebSearch: search-category cards titled by the query. */
199
+ function searchCallView(args) {
200
+ const arguments_ = record(args);
201
+ const query = str(arguments_?.pattern) ?? str(arguments_?.query);
202
+ if (query === void 0) return void 0;
203
+ const scope = str(arguments_?.path);
204
+ return {
205
+ card: "generic",
206
+ kind: "search",
207
+ title: scope === void 0 ? query : `${query} · ${scope}`,
208
+ rawInput: args
209
+ };
210
+ }
211
+ /** WebFetch: a fetch-category card titled by the URL. */
212
+ function fetchCallView(args) {
213
+ const url = str(record(args)?.url);
214
+ if (url === void 0) return void 0;
215
+ return {
216
+ card: "generic",
217
+ kind: "fetch",
218
+ title: url,
219
+ rawInput: args
220
+ };
221
+ }
222
+ /** Task: a subagent card titled by Claude's task description. */
223
+ function taskCallView(args) {
224
+ const arguments_ = record(args);
225
+ return {
226
+ card: "generic",
227
+ kind: "other",
228
+ title: str(arguments_?.description) ?? str(arguments_?.subagent_type) ?? "Subagent",
229
+ rawInput: args
230
+ };
231
+ }
232
+ function genericTitle(title) {
233
+ return {
234
+ card: "generic",
235
+ kind: "other",
236
+ title
237
+ };
238
+ }
239
+ function presenterDefinition(name, description, presentCall, presentResult) {
240
+ return {
241
+ name,
242
+ description,
243
+ parameters: {
244
+ type: "object",
245
+ properties: {}
246
+ },
247
+ output: {
248
+ schema: {
249
+ type: "object",
250
+ properties: {}
251
+ },
252
+ render: () => []
253
+ },
254
+ execute: async () => {
255
+ throw new Error(`dsh-claude: Claude Code owns execution of ${name}`);
256
+ },
257
+ ...presentCall === void 0 ? {} : { presentCall },
258
+ ...presentResult === void 0 ? {} : { presentResult }
259
+ };
260
+ }
261
+ const PRESENTATION_NOTE = "Presentation mirror of the Claude Code tool; execution is owned by Claude Code.";
262
+ /** Generic call view for dynamically observed tools (MCP tools, new built-ins). */
263
+ function genericCallView(toolName) {
264
+ return (args) => {
265
+ return {
266
+ card: "generic",
267
+ kind: "other",
268
+ title: str(record(args)?.description) ?? toolName,
269
+ rawInput: args
270
+ };
271
+ };
272
+ }
273
+ /** One presenter-only mirror for a tool name discovered at runtime. */
274
+ function dynamicPresenterDefinition(name) {
275
+ return presenterDefinition(name, PRESENTATION_NOTE, genericCallView(name));
276
+ }
277
+ /** The presentation-only registry contributed to the Claude Code preset scope. */
278
+ function claudePresenterDefinitions() {
279
+ return [
280
+ presenterDefinition("Bash", PRESENTATION_NOTE, bashCallView, terminalResultView),
281
+ presenterDefinition("Read", PRESENTATION_NOTE, readCallView),
282
+ presenterDefinition("Edit", PRESENTATION_NOTE, diffCallView),
283
+ presenterDefinition("MultiEdit", PRESENTATION_NOTE, diffCallView),
284
+ presenterDefinition("Write", PRESENTATION_NOTE, diffCallView),
285
+ presenterDefinition("NotebookEdit", PRESENTATION_NOTE, (args) => {
286
+ const path = str(record(args)?.notebook_path);
287
+ return path === void 0 ? void 0 : genericTitle(`NotebookEdit ${path}`);
288
+ }),
289
+ presenterDefinition("Grep", PRESENTATION_NOTE, searchCallView),
290
+ presenterDefinition("Glob", PRESENTATION_NOTE, searchCallView),
291
+ presenterDefinition("WebSearch", PRESENTATION_NOTE, searchCallView),
292
+ presenterDefinition("WebFetch", PRESENTATION_NOTE, fetchCallView),
293
+ presenterDefinition("Task", PRESENTATION_NOTE, taskCallView),
294
+ presenterDefinition("TodoWrite", PRESENTATION_NOTE, () => genericTitle("Update todos"))
295
+ ];
296
+ }
297
+ /** Names covered by the static preset-scope registry; dynamic mirrors skip them. */
298
+ const CLAUDE_PRESENTER_NAMES = new Set(claudePresenterDefinitions().map((definition) => definition.name));
299
+ //#endregion
300
+ export { ClaudeCommandBridge as a, CLAUDE_COMMANDS_SERVICE as i, claudePresenterDefinitions as n, dynamicPresenterDefinition as r, CLAUDE_PRESENTER_NAMES as t };
301
+
302
+ //# sourceMappingURL=presenters-DbfG-9KQ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presenters-DbfG-9KQ.mjs","names":["#target","#live"],"sources":["../src/command-bridge.ts","../src/presenters.ts"],"sourcesContent":["import type { SlashCommand } from '@anthropic-ai/claude-agent-sdk'\nimport type { CommandDefinition, CommandDescriptor } from '@deepseek-ai/dsh-commands'\nimport { redactText } from './events.ts'\n\nconst COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u\nconst MAX_DESCRIPTION_CHARS = 300\nconst MAX_HINT_CHARS = 120\n\n/** DSH registry names reject `:`, but plugin-qualified Claude skills carry it\n * (e.g. `awesome-skills:ci-deploy`). Derive a registry-safe public name while\n * forwarding keeps the exact Claude-side name. */\nfunction registryName(claudeName: string): string | undefined {\n if (COMMAND_NAME.test(claudeName)) return claudeName\n const short = claudeName.includes(':') ? claudeName.slice(claudeName.lastIndexOf(':') + 1) : undefined\n if (short !== undefined && COMMAND_NAME.test(short)) return short\n const sanitized = claudeName.replaceAll(':', '-').toLowerCase()\n return COMMAND_NAME.test(sanitized) ? sanitized : undefined\n}\n\n/**\n * Names registered as CLIENT-side commandUi contributions (not host\n * commands). The DSH web command palette fails its whole command group when\n * a host command name equals a contribution name\n * (\"contribution /model collides with a host command\"), so these can never\n * be registered by this bridge. Keep in sync with the client bundles:\n * dsh-client-ui-model-selection registers \"model\".\n */\nconst CLIENT_CONTRIBUTION_NAMES: ReadonlySet<string> = new Set(['model'])\n\nexport interface ClaudeCommandTarget {\n list(): readonly Pick<CommandDescriptor, 'name'>[]\n register(definition: CommandDefinition): () => void\n forward(line: string): void\n}\n\n/**\n * Agent-scope command service contract. The commands service is unreachable\n * from the host plugin's view of `agent.ctx` (\"without inject\"), but the\n * preset route plugin runs INSIDE the agent's preset composition with\n * `commands` injected. Registrations made through the provided service land\n * in the agent's scope layer, which the command registry inherits into\n * `list(agent)` for exactly that agent — invisible to every other session.\n */\nexport interface ClaudeAgentCommandService {\n list(agent: unknown): readonly Pick<CommandDescriptor, 'name'>[]\n register(definition: CommandDefinition): () => void\n}\n\n/**\n * Cordis service name the preset route provides (behind an entry-local\n * isolate realm, so each session gets its own instance) and the host reads\n * back with dsh-agent-presets' official `serviceForAgent(ctx, agent, name)`\n * — the supported cross-scope read for callers that already hold the agent.\n */\nexport const CLAUDE_COMMANDS_SERVICE = 'claudeCommands'\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n claudeCommands?: ClaudeAgentCommandService\n }\n}\n\nexport interface ClaudeCommandView {\n publicName: string\n claudeName: string\n prefixed: boolean\n}\n\ninterface DesiredCommand extends ClaudeCommandView {\n signature: string\n definition: CommandDefinition\n}\n\ninterface LiveCommand {\n signature: string\n dispose: () => void\n}\n\nfunction bounded(value: string, maxChars: number): string {\n return redactText(value, maxChars)\n}\n\nfunction desiredCommands(\n catalog: readonly SlashCommand[],\n reservedNames: ReadonlySet<string>,\n forward: (line: string) => void,\n): Map<string, DesiredCommand> {\n const desired = new Map<string, DesiredCommand>()\n const assigned = new Set<string>()\n for (const command of catalog) {\n const names = [command.name, ...(command.aliases ?? [])]\n for (const claudeName of names) {\n const base = registryName(claudeName)\n if (base === undefined) continue\n let publicName = base\n let prefixed = false\n if (reservedNames.has(publicName) || assigned.has(publicName)) {\n publicName = `claude-${base}`\n prefixed = true\n }\n if (!COMMAND_NAME.test(publicName) || reservedNames.has(publicName) || assigned.has(publicName)) continue\n const description = bounded(command.description || `Claude Code /${command.name}`, MAX_DESCRIPTION_CHARS)\n const hint = bounded(command.argumentHint ?? '', MAX_HINT_CHARS)\n const signature = JSON.stringify({ publicName, claudeName, description, hint })\n desired.set(publicName, {\n publicName,\n claudeName,\n prefixed,\n signature,\n definition: {\n name: publicName,\n description,\n ...(hint.length === 0 ? {} : { input: { hint } }),\n recordInput: false,\n handler: ({ rawInput }) => {\n forward(`/${claudeName}${rawInput}`)\n return { kind: 'success' }\n },\n },\n })\n assigned.add(publicName)\n }\n }\n return desired\n}\n\nexport class ClaudeCommandBridge {\n readonly #target: ClaudeCommandTarget\n readonly #live = new Map<string, LiveCommand>()\n\n constructor(target: ClaudeCommandTarget) {\n this.#target = target\n }\n\n refresh(catalog: readonly SlashCommand[]): readonly ClaudeCommandView[] {\n const owned = new Set(this.#live.keys())\n const reserved = new Set(\n this.#target.list()\n .map(command => command.name)\n .filter(name => !owned.has(name)),\n )\n // Client-side commandUi contributions (e.g. /model from\n // dsh-client-ui-model-selection) are invisible to the host registry, but\n // the web palette throws away its ENTIRE command group when a host\n // command collides with a contribution. Reserve those names so Claude's\n // same-named commands take the claude- prefix instead.\n for (const name of CLIENT_CONTRIBUTION_NAMES) reserved.add(name)\n const desired = desiredCommands(catalog, reserved, line => this.#target.forward(line))\n\n for (const [name, live] of [...this.#live]) {\n const next = desired.get(name)\n if (next?.signature === live.signature) continue\n live.dispose()\n this.#live.delete(name)\n }\n for (const [name, next] of desired) {\n if (this.#live.has(name)) continue\n this.#live.set(name, {\n signature: next.signature,\n dispose: this.#target.register(next.definition),\n })\n }\n return [...desired.values()].map(({ publicName, claudeName, prefixed }) => ({\n publicName,\n claudeName,\n prefixed,\n }))\n }\n\n dispose(): void {\n for (const command of this.#live.values()) command.dispose()\n this.#live.clear()\n }\n}\n","/**\n * Presentation-only tool definitions for the Claude Code preset. Claude Code\n * owns tool execution; these definitions exist solely so the host's tool\n * presentation pipeline (`viewFor` → `presentCall`/`presentResult`) can\n * compute native render intents for the mirrored `tool/call`/`tool/result`\n * events, giving Claude tool rows the same cards DSH-executed tools get.\n */\nimport type { ToolDefinition, ToolResult } from '@deepseek-ai/dsh-tools'\nimport type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'\n\nfunction record(value: unknown): Record<string, unknown> | undefined {\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nfunction resultText(result: ToolResult): string {\n return result.content\n .filter((block): block is Extract<typeof block, { type: 'text' }> => block.type === 'text')\n .map(block => block.text)\n .join('\\n')\n}\n\n/** Bash: a terminal card headed by the command, described above by Claude's own summary. */\nexport function bashCallView(args: unknown): ToolCallView | undefined {\n const command = str(record(args)?.command)\n if (command === undefined) return undefined\n const description = str(record(args)?.description)\n return { card: 'terminal', title: command, ...(description === undefined ? {} : { description }) }\n}\n\nexport function terminalResultView(_args: unknown, result: ToolResult): ToolResultView | undefined {\n const output = resultText(result)\n return output.length === 0 ? undefined : { card: 'terminal', output }\n}\n\n/** Read: a generic read card that follow-alongs the file window. */\nexport function readCallView(args: unknown): ToolCallView | undefined {\n const arguments_ = record(args)\n const path = str(arguments_?.file_path)\n if (path === undefined) return undefined\n const offset = typeof arguments_?.offset === 'number' ? arguments_.offset : undefined\n return {\n card: 'generic',\n kind: 'read',\n title: `Read ${path}`,\n locations: [{ path, ...(offset === undefined ? {} : { line: offset }) }],\n }\n}\n\nfunction fileDiffs(args: unknown): { path: string; oldText: string | null; newText: string }[] | undefined {\n const arguments_ = record(args)\n const path = str(arguments_?.file_path)\n if (path === undefined) return undefined\n if (Array.isArray(arguments_?.edits)) {\n const diffs: { path: string; oldText: string | null; newText: string }[] = []\n for (const edit of arguments_.edits) {\n const item = record(edit)\n const oldText = str(item?.old_string)\n const newText = str(item?.new_string)\n if (newText === undefined) continue\n diffs.push({ path, oldText: oldText ?? null, newText })\n }\n return diffs.length > 0 ? diffs : undefined\n }\n const newText = str(arguments_?.new_string) ?? arguments_?.content\n if (typeof newText !== 'string' || newText.length === 0) return undefined\n const oldText = str(arguments_?.old_string)\n return [{ path, oldText: oldText ?? null, newText }]\n}\n\n/** Edit / MultiEdit / Write: inline diff cards derived from the call arguments. */\nexport function diffCallView(args: unknown): ToolCallView | undefined {\n const arguments_ = record(args)\n const path = str(arguments_?.file_path)\n const diffs = fileDiffs(args)\n if (path === undefined || diffs === undefined) return undefined\n const verb = typeof arguments_?.new_string === 'string' || Array.isArray(arguments_?.edits) ? 'Edit' : 'Write'\n return { card: 'diff', title: `${verb} ${path}`, diffs, locations: [{ path }] }\n}\n\n/** Grep / Glob / WebSearch: search-category cards titled by the query. */\nexport function searchCallView(args: unknown): ToolCallView | undefined {\n const arguments_ = record(args)\n const query = str(arguments_?.pattern) ?? str(arguments_?.query)\n if (query === undefined) return undefined\n const scope = str(arguments_?.path)\n return {\n card: 'generic',\n kind: 'search',\n title: scope === undefined ? query : `${query} · ${scope}`,\n rawInput: args,\n }\n}\n\n/** WebFetch: a fetch-category card titled by the URL. */\nexport function fetchCallView(args: unknown): ToolCallView | undefined {\n const url = str(record(args)?.url)\n if (url === undefined) return undefined\n return { card: 'generic', kind: 'fetch', title: url, rawInput: args }\n}\n\n/** Task: a subagent card titled by Claude's task description. */\nexport function taskCallView(args: unknown): ToolCallView | undefined {\n const arguments_ = record(args)\n const title = str(arguments_?.description) ?? str(arguments_?.subagent_type) ?? 'Subagent'\n return { card: 'generic', kind: 'other', title, rawInput: args }\n}\n\nfunction genericTitle(title: string): ToolCallView {\n return { card: 'generic', kind: 'other', title }\n}\n\nfunction presenterDefinition(\n name: string,\n description: string,\n presentCall?: (args: unknown) => ToolCallView | undefined,\n presentResult?: (args: unknown, result: ToolResult) => ToolResultView | undefined,\n): ToolDefinition {\n return {\n name,\n description,\n parameters: { type: 'object', properties: {} },\n output: {\n schema: { type: 'object', properties: {} } as ToolDefinition['output']['schema'],\n render: () => [],\n },\n execute: async () => {\n throw new Error(`dsh-claude: Claude Code owns execution of ${name}`)\n },\n ...(presentCall === undefined ? {} : { presentCall }),\n ...(presentResult === undefined ? {} : { presentResult }),\n }\n}\n\nconst PRESENTATION_NOTE = 'Presentation mirror of the Claude Code tool; execution is owned by Claude Code.'\n\n/** Generic call view for dynamically observed tools (MCP tools, new built-ins). */\nexport function genericCallView(toolName: string): (args: unknown) => ToolCallView | undefined {\n return (args: unknown) => {\n const description = str(record(args)?.description)\n return { card: 'generic', kind: 'other', title: description ?? toolName, rawInput: args }\n }\n}\n\n/** One presenter-only mirror for a tool name discovered at runtime. */\nexport function dynamicPresenterDefinition(name: string): ToolDefinition {\n return presenterDefinition(name, PRESENTATION_NOTE, genericCallView(name))\n}\n\n/** The presentation-only registry contributed to the Claude Code preset scope. */\nexport function claudePresenterDefinitions(): ToolDefinition[] {\n return [\n presenterDefinition('Bash', PRESENTATION_NOTE, bashCallView, terminalResultView),\n presenterDefinition('Read', PRESENTATION_NOTE, readCallView),\n presenterDefinition('Edit', PRESENTATION_NOTE, diffCallView),\n presenterDefinition('MultiEdit', PRESENTATION_NOTE, diffCallView),\n presenterDefinition('Write', PRESENTATION_NOTE, diffCallView),\n presenterDefinition('NotebookEdit', PRESENTATION_NOTE, args => {\n const path = str(record(args)?.notebook_path)\n return path === undefined ? undefined : genericTitle(`NotebookEdit ${path}`)\n }),\n presenterDefinition('Grep', PRESENTATION_NOTE, searchCallView),\n presenterDefinition('Glob', PRESENTATION_NOTE, searchCallView),\n presenterDefinition('WebSearch', PRESENTATION_NOTE, searchCallView),\n presenterDefinition('WebFetch', PRESENTATION_NOTE, fetchCallView),\n presenterDefinition('Task', PRESENTATION_NOTE, taskCallView),\n presenterDefinition('TodoWrite', PRESENTATION_NOTE, () => genericTitle('Update todos')),\n ]\n}\n\n/** Names covered by the static preset-scope registry; dynamic mirrors skip them. */\nexport const CLAUDE_PRESENTER_NAMES: ReadonlySet<string> = new Set(claudePresenterDefinitions().map(definition => definition.name))\n"],"mappings":"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"}
@@ -0,0 +1,291 @@
1
+ import { _ as LEGACY_CLAUDE_CODE_PRESET_ID, f as CLAUDE_CODE_PRESET_ID, l as redactText } from "./events-qlmU1KrH.mjs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { link, lstat, mkdir, readFile, readdir, rm, rmdir, writeFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
6
+ import { homedir } from "node:os";
7
+ import { fileURLToPath } from "node:url";
8
+ //#region src/executable.ts
9
+ const VERSION_PATTERN = /(?:Claude Code\s+)?v?(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/i;
10
+ const MAX_PROBE_STDOUT = 65536;
11
+ const MAX_PROBE_STDERR = 8192;
12
+ var ClaudeExecutableNotFoundError = class extends Error {
13
+ searched;
14
+ constructor(searched, options) {
15
+ super(`Claude Code executable not found. Searched: ${searched.join(", ")}`, options);
16
+ this.name = "ClaudeExecutableNotFoundError";
17
+ this.searched = [...searched];
18
+ }
19
+ };
20
+ function fallbackCandidates() {
21
+ if (process.platform !== "darwin") return [];
22
+ return [
23
+ join(homedir(), ".local", "bin", "claude"),
24
+ "/opt/homebrew/bin/claude",
25
+ "/usr/local/bin/claude"
26
+ ];
27
+ }
28
+ function abortError(error) {
29
+ return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
30
+ }
31
+ async function resolveClaudeExecutable(runtime, configuredPath, signal) {
32
+ const searched = [];
33
+ const candidates = configuredPath === void 0 ? ["claude", ...fallbackCandidates()] : [configuredPath];
34
+ if (configuredPath !== void 0 && !configuredPath.startsWith("/")) throw new Error(`Claude Code executable path must be absolute: ${configuredPath}`);
35
+ let lastError;
36
+ for (const candidate of candidates) {
37
+ if (searched.includes(candidate)) continue;
38
+ searched.push(candidate);
39
+ try {
40
+ return {
41
+ path: await runtime.resolveExecutable(candidate, void 0, signal),
42
+ searched
43
+ };
44
+ } catch (error) {
45
+ if (abortError(error) || signal?.aborted === true) throw error;
46
+ lastError = error;
47
+ }
48
+ }
49
+ throw new ClaudeExecutableNotFoundError(searched, lastError === void 0 ? void 0 : { cause: lastError });
50
+ }
51
+ async function collect(handle) {
52
+ const outcome = await handle.done;
53
+ const stdout = handle.collected.stdout?.readFrom(0).text ?? "";
54
+ const stderr = handle.collected.stderr?.readFrom(0).text ?? "";
55
+ return {
56
+ ...outcome,
57
+ stdout,
58
+ stderr
59
+ };
60
+ }
61
+ async function runProbe(runtime, executable, args, cwd, signal) {
62
+ return collect(runtime.spawn({
63
+ argv: [executable, ...args],
64
+ cwd,
65
+ stdio: {
66
+ stdin: "ignore",
67
+ stdout: { maxBytes: MAX_PROBE_STDOUT },
68
+ stderr: { maxBytes: MAX_PROBE_STDERR }
69
+ },
70
+ graceMs: 2e3,
71
+ ...signal === void 0 ? {} : { signal },
72
+ env: {}
73
+ }));
74
+ }
75
+ function parseClaudeVersion(output) {
76
+ return VERSION_PATTERN.exec(output)?.[1];
77
+ }
78
+ async function probeClaudeVersion(runtime, executable, cwd, signal) {
79
+ const result = await runProbe(runtime, executable, ["--version"], cwd, signal);
80
+ const version = parseClaudeVersion(`${result.stdout}\n${result.stderr}`);
81
+ if (result.exitCode !== 0 || version === void 0) throw new Error(`Claude Code version probe failed (${result.exitCode ?? result.signal ?? "unknown exit"})`);
82
+ return version;
83
+ }
84
+ async function probeClaudeAuthentication(runtime, executable, cwd, signal) {
85
+ const result = await runProbe(runtime, executable, [
86
+ "auth",
87
+ "status",
88
+ "--json"
89
+ ], cwd, signal);
90
+ if (result.exitCode !== 0) return {
91
+ status: "unknown",
92
+ message: "Claude authentication status command failed"
93
+ };
94
+ try {
95
+ const value = JSON.parse(result.stdout);
96
+ const report = { status: value.loggedIn === true ? "signed-in" : value.loggedIn === false ? "signed-out" : "unknown" };
97
+ if (typeof value.authMethod === "string") report.method = redactText(value.authMethod, 100);
98
+ if (typeof value.apiProvider === "string") report.provider = redactText(value.apiProvider, 100);
99
+ if (typeof value.subscriptionType === "string") report.subscription = redactText(value.subscriptionType, 100);
100
+ return report;
101
+ } catch {
102
+ return {
103
+ status: "unknown",
104
+ message: "Claude authentication status was not valid JSON"
105
+ };
106
+ }
107
+ }
108
+ async function runClaudeDoctor(runtime, options) {
109
+ let resolution;
110
+ try {
111
+ resolution = await resolveClaudeExecutable(runtime, options.configuredPath, options.signal);
112
+ } catch (error) {
113
+ if (error instanceof ClaudeExecutableNotFoundError) return {
114
+ executable: {
115
+ status: "missing",
116
+ searched: error.searched
117
+ },
118
+ version: { status: "not-run" },
119
+ authentication: { status: "not-run" },
120
+ handshake: "not-run"
121
+ };
122
+ throw error;
123
+ }
124
+ const report = {
125
+ executable: {
126
+ status: "found",
127
+ path: resolution.path,
128
+ searched: resolution.searched
129
+ },
130
+ version: { status: "not-run" },
131
+ authentication: { status: "not-run" },
132
+ handshake: "not-run"
133
+ };
134
+ try {
135
+ report.version = {
136
+ status: "ok",
137
+ value: await probeClaudeVersion(runtime, resolution.path, options.cwd, options.signal)
138
+ };
139
+ } catch (error) {
140
+ report.version = {
141
+ status: "error",
142
+ message: error instanceof Error ? error.message : "Version probe failed"
143
+ };
144
+ }
145
+ try {
146
+ report.authentication = await probeClaudeAuthentication(runtime, resolution.path, options.cwd, options.signal);
147
+ } catch (error) {
148
+ report.authentication = {
149
+ status: "unknown",
150
+ message: error instanceof Error ? error.message : "Authentication probe failed"
151
+ };
152
+ }
153
+ return report;
154
+ }
155
+ //#endregion
156
+ //#region src/preset-installer.ts
157
+ const MANAGED_PRESET_FILES = ["agent.cordis.yml", "preset.yml"];
158
+ /** Package specifier kept in the shipped template. DSH Desktop's resolver hook
159
+ * only rewrites bare specifiers issued by the root include; preset subtrees
160
+ * resolve through Node's internal loader with an unrelated base and cannot
161
+ * find linked packages. The installer therefore substitutes the absolute
162
+ * built entry path, which the preset tree imports directly as a file URL. */
163
+ const PRESET_ROUTE_PACKAGE_SPECIFIER = "@norman-else/dsh-claude/preset-route";
164
+ var ManagedPresetConflictError = class extends Error {
165
+ path;
166
+ constructor(path) {
167
+ super(`dsh-claude: refusing to overwrite user-modified preset file ${path}`);
168
+ this.name = "ManagedPresetConflictError";
169
+ this.path = path;
170
+ }
171
+ };
172
+ function defaultManagedPresetPaths(dshHome) {
173
+ const packageRoot = fileURLToPath(new URL("../", import.meta.url));
174
+ return {
175
+ sourceDir: join(packageRoot, "preset"),
176
+ targetDir: dshHome === void 0 ? dshHomePath(".agent-presets", CLAUDE_CODE_PRESET_ID) : join(dshHome, ".agent-presets", CLAUDE_CODE_PRESET_ID),
177
+ legacyTargetDir: dshHome === void 0 ? dshHomePath(".agent-presets", LEGACY_CLAUDE_CODE_PRESET_ID) : join(dshHome, ".agent-presets", LEGACY_CLAUDE_CODE_PRESET_ID)
178
+ };
179
+ }
180
+ async function managedContents(paths) {
181
+ const routeEntry = join(paths.sourceDir, "..", "lib", "preset-route.mjs");
182
+ return await Promise.all(MANAGED_PRESET_FILES.map(async (file) => {
183
+ const source = await readFile(join(paths.sourceDir, file), "utf8");
184
+ const nameRow = `name: ${PRESET_ROUTE_PACKAGE_SPECIFIER}`;
185
+ if (file !== "agent.cordis.yml" || !source.includes(nameRow)) return {
186
+ file,
187
+ content: source,
188
+ legacy: [],
189
+ isLegacy: () => false
190
+ };
191
+ return {
192
+ file,
193
+ content: source.replace(nameRow, `name: ${routeEntry}`),
194
+ legacy: [source],
195
+ isLegacy: (current) => current.includes("id: claude-code-route") && (current.includes(nameRow) || current.includes("lib/preset-route.mjs"))
196
+ };
197
+ }));
198
+ }
199
+ async function readIfPresent(path) {
200
+ try {
201
+ return await readFile(path, "utf8");
202
+ } catch (error) {
203
+ if (error.code === "ENOENT") return void 0;
204
+ throw error;
205
+ }
206
+ }
207
+ async function atomicWrite(path, content) {
208
+ await mkdir(dirname(path), { recursive: true });
209
+ const temporary = `${path}.${randomUUID()}.tmp`;
210
+ try {
211
+ await writeFile(temporary, content, {
212
+ encoding: "utf8",
213
+ mode: 384,
214
+ flag: "wx"
215
+ });
216
+ try {
217
+ await link(temporary, path);
218
+ return true;
219
+ } catch (error) {
220
+ if (error.code !== "EEXIST") throw error;
221
+ if (await readIfPresent(path) === content) return false;
222
+ throw new ManagedPresetConflictError(path);
223
+ }
224
+ } finally {
225
+ await rm(temporary, { force: true });
226
+ }
227
+ }
228
+ async function ensureManagedPreset(paths = defaultManagedPresetPaths()) {
229
+ await assertSafeTargetDirectory(paths.targetDir);
230
+ const expected = await managedContents(paths);
231
+ let changed = false;
232
+ for (const { file, content, legacy, isLegacy } of expected) {
233
+ const target = join(paths.targetDir, file);
234
+ const current = await readIfPresent(target);
235
+ if (current === content) continue;
236
+ if (current !== void 0) {
237
+ if (!legacy.includes(current) && !isLegacy(current)) throw new ManagedPresetConflictError(target);
238
+ await rm(target);
239
+ }
240
+ changed = await atomicWrite(target, content) || changed;
241
+ }
242
+ if (paths.legacyTargetDir !== void 0) changed = await removeLegacyManagedPreset(paths.legacyTargetDir) || changed;
243
+ return changed ? "installed" : "unchanged";
244
+ }
245
+ async function removeLegacyManagedPreset(targetDir) {
246
+ await assertSafeTargetDirectory(targetDir);
247
+ const agent = await readIfPresent(join(targetDir, "agent.cordis.yml"));
248
+ const preset = await readIfPresent(join(targetDir, "preset.yml"));
249
+ if (agent === void 0 && preset === void 0) return false;
250
+ const managedAgent = agent !== void 0 && agent.includes("id: claude-code-route") && (agent.includes("dsh-claude-code/preset-route") || agent.includes("lib/preset-route.mjs"));
251
+ const managedPreset = preset !== void 0 && preset.includes("Managed by dsh-claude-code") && preset.includes("name: Claude Code");
252
+ if (!managedAgent || !managedPreset) return false;
253
+ await rm(join(targetDir, "agent.cordis.yml"));
254
+ await rm(join(targetDir, "preset.yml"));
255
+ if ((await readdir(targetDir)).length === 0) await rmdir(targetDir);
256
+ return true;
257
+ }
258
+ /** Reject a target directory that is a symlink (or occupies the path as a file)
259
+ * so the managed preset never writes through an attacker-controlled link. */
260
+ async function assertSafeTargetDirectory(targetDir) {
261
+ try {
262
+ const stat = await lstat(targetDir);
263
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new ManagedPresetConflictError(targetDir);
264
+ } catch (error) {
265
+ if (error.code === "ENOENT") return;
266
+ throw error;
267
+ }
268
+ }
269
+ async function removeManagedPreset(paths = defaultManagedPresetPaths()) {
270
+ await assertSafeTargetDirectory(paths.targetDir);
271
+ const expected = await managedContents(paths);
272
+ let removed = false;
273
+ for (const { file, content, legacy, isLegacy } of expected) {
274
+ const target = join(paths.targetDir, file);
275
+ const current = await readIfPresent(target);
276
+ if (current === void 0) continue;
277
+ if (current !== content && !legacy.includes(current) && !isLegacy(current)) throw new ManagedPresetConflictError(target);
278
+ await rm(target);
279
+ removed = true;
280
+ }
281
+ try {
282
+ if ((await readdir(paths.targetDir)).length === 0) await rmdir(paths.targetDir);
283
+ } catch (error) {
284
+ if (error.code !== "ENOENT") throw error;
285
+ }
286
+ return removed ? "removed" : "absent";
287
+ }
288
+ //#endregion
289
+ export { runClaudeDoctor as a, resolveClaudeExecutable as i, removeManagedPreset as n, parseClaudeVersion as r, ensureManagedPreset as t };
290
+
291
+ //# sourceMappingURL=preset-installer-DUVN1J6P.mjs.map