claude-plugin-dsh 0.0.0-stage → 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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -2
  3. package/dist/bin/cli.d.ts +3 -0
  4. package/dist/bin/cli.d.ts.map +1 -0
  5. package/dist/bin/cli.js +80 -0
  6. package/dist/bin/cli.js.map +1 -0
  7. package/dist/bin/inspect.d.ts +2 -0
  8. package/dist/bin/inspect.d.ts.map +1 -0
  9. package/dist/bin/inspect.js +45 -0
  10. package/dist/bin/inspect.js.map +1 -0
  11. package/dist/bin/runtime.d.ts +2 -0
  12. package/dist/bin/runtime.d.ts.map +1 -0
  13. package/dist/bin/runtime.js +4 -0
  14. package/dist/bin/runtime.js.map +1 -0
  15. package/dist/bin/scaffold.d.ts +2 -0
  16. package/dist/bin/scaffold.d.ts.map +1 -0
  17. package/dist/bin/scaffold.js +44 -0
  18. package/dist/bin/scaffold.js.map +1 -0
  19. package/dist/bin/sync.d.ts +2 -0
  20. package/dist/bin/sync.d.ts.map +1 -0
  21. package/dist/bin/sync.js +24 -0
  22. package/dist/bin/sync.js.map +1 -0
  23. package/dist/claude-compat.js +825 -0
  24. package/dist/src/claude-compat.d.ts +8 -0
  25. package/dist/src/claude-compat.d.ts.map +1 -0
  26. package/dist/src/claude-compat.js +7 -0
  27. package/dist/src/claude-compat.js.map +1 -0
  28. package/dist/src/dsh-types.d.ts +252 -0
  29. package/dist/src/dsh-types.d.ts.map +1 -0
  30. package/dist/src/dsh-types.js +6 -0
  31. package/dist/src/dsh-types.js.map +1 -0
  32. package/dist/src/hooks.d.ts +38 -0
  33. package/dist/src/hooks.d.ts.map +1 -0
  34. package/dist/src/hooks.js +136 -0
  35. package/dist/src/hooks.js.map +1 -0
  36. package/dist/src/plugin-files.d.ts +92 -0
  37. package/dist/src/plugin-files.d.ts.map +1 -0
  38. package/dist/src/plugin-files.js +198 -0
  39. package/dist/src/plugin-files.js.map +1 -0
  40. package/dist/src/plugin.d.ts +35 -0
  41. package/dist/src/plugin.d.ts.map +1 -0
  42. package/dist/src/plugin.js +422 -0
  43. package/dist/src/plugin.js.map +1 -0
  44. package/dist/src/transcript.d.ts +25 -0
  45. package/dist/src/transcript.d.ts.map +1 -0
  46. package/dist/src/transcript.js +198 -0
  47. package/dist/src/transcript.js.map +1 -0
  48. package/package.json +54 -4
@@ -0,0 +1,825 @@
1
+ // src/plugin.ts
2
+ import { randomUUID as randomUUID2 } from "node:crypto";
3
+ import { existsSync as existsSync2 } from "node:fs";
4
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ // src/hooks.ts
8
+ import { spawn } from "node:child_process";
9
+
10
+ // src/plugin-files.ts
11
+ import { readdirSync, readFileSync } from "node:fs";
12
+ import { basename, dirname, join, resolve } from "node:path";
13
+ var DEFAULT_HOOK_TIMEOUT_S = 60;
14
+ function parseFrontMatter(text) {
15
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
16
+ if (!match) return { meta: {}, body: text };
17
+ const meta = {};
18
+ for (const line of (match[1] ?? "").split(/\r?\n/)) {
19
+ const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
20
+ if (!kv || kv[1] === void 0 || kv[2] === void 0) continue;
21
+ const raw = kv[2].trim();
22
+ let value = raw;
23
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
24
+ value = value.slice(1, -1);
25
+ if (raw.startsWith('"')) value = value.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
26
+ }
27
+ meta[kv[1]] = value;
28
+ }
29
+ return { meta, body: text.slice(match[0].length) };
30
+ }
31
+ var CLAUDE_ONLY_OPEN = /^\s*(?:#+\s*)?<!--\s*harness:claude-only\s*-->\s*$/;
32
+ var CLAUDE_ONLY_CLOSE = /^\s*(?:#+\s*)?<!--\s*\/harness\s*-->\s*$/;
33
+ function stripClaudeOnlyBlocks(text) {
34
+ const out = [];
35
+ let inside = false;
36
+ for (const line of text.split(/(?<=\n)/)) {
37
+ const bare = line.replace(/\r?\n$/, "");
38
+ if (!inside && CLAUDE_ONLY_OPEN.test(bare)) inside = true;
39
+ else if (inside && CLAUDE_ONLY_CLOSE.test(bare)) inside = false;
40
+ else if (!inside) out.push(line);
41
+ }
42
+ return { text: out.join(""), unterminated: inside };
43
+ }
44
+ var stripClaudeOnly = (text) => stripClaudeOnlyBlocks(text).text;
45
+ var escapeClosingTag = (text, tag) => String(text).replaceAll(`</${tag}`, `<\\/${tag}`);
46
+ function expandPluginRoot(text, root) {
47
+ return text.replaceAll("${CLAUDE_PLUGIN_ROOT}", root).replaceAll("$CLAUDE_PLUGIN_ROOT", root);
48
+ }
49
+ var kebab = (value) => String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
50
+ function readText(path) {
51
+ try {
52
+ return readFileSync(path, "utf8");
53
+ } catch {
54
+ return void 0;
55
+ }
56
+ }
57
+ function listDir(path) {
58
+ try {
59
+ return readdirSync(path, { withFileTypes: true });
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
64
+ function readManifest(root) {
65
+ const raw = readText(join(root, ".claude-plugin", "plugin.json"));
66
+ const manifest = raw ? JSON.parse(raw) : {};
67
+ return { ...manifest, name: manifest.name || basename(root) };
68
+ }
69
+ function discoverSkills(root, options = {}) {
70
+ const dir = join(root, "skills");
71
+ const skip = new Set(options.exclude ?? []);
72
+ const out = [];
73
+ for (const entry of listDir(dir)) {
74
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
75
+ if (skip.has(entry.name)) continue;
76
+ const path = join(dir, entry.name, "SKILL.md");
77
+ const text = readText(path);
78
+ if (text === void 0) continue;
79
+ const { meta } = parseFrontMatter(text);
80
+ const bare = kebab(meta.name || entry.name);
81
+ if (!bare || !meta.description) continue;
82
+ const name = options.prefix ? `${options.prefix}-${bare}` : bare;
83
+ out.push({ name, description: meta.description, path, base: dirname(path), meta });
84
+ }
85
+ return out.sort((a, b) => a.name.localeCompare(b.name));
86
+ }
87
+ function discoverCommands(root, options = {}) {
88
+ const dir = join(root, "commands");
89
+ const skip = new Set(options.exclude ?? []);
90
+ const out = [];
91
+ for (const entry of listDir(dir)) {
92
+ if (!entry.name.endsWith(".md")) continue;
93
+ const stem = entry.name.slice(0, -3);
94
+ if (skip.has(stem)) continue;
95
+ const path = join(dir, entry.name);
96
+ const text = readText(path);
97
+ if (text === void 0) continue;
98
+ const { meta, body } = parseFrontMatter(text);
99
+ out.push({ stem, name: kebab(meta.name || stem), description: meta.description || stem, hint: meta["argument-hint"] || "", body, path });
100
+ }
101
+ return out.sort((a, b) => a.name.localeCompare(b.name));
102
+ }
103
+ function discoverAgents(root, options = {}) {
104
+ const dirs = [join(root, "agents"), ...(options.extraDirs ?? []).map((d) => resolve(root, d))];
105
+ const skip = new Set(options.exclude ?? []);
106
+ const out = [];
107
+ const seen = /* @__PURE__ */ new Set();
108
+ for (const dir of dirs) {
109
+ for (const entry of listDir(dir)) {
110
+ if (!entry.name.endsWith(".md")) continue;
111
+ const path = join(dir, entry.name);
112
+ const text = readText(path);
113
+ if (text === void 0) continue;
114
+ const { meta, body } = parseFrontMatter(text);
115
+ const name = kebab(meta.name || entry.name.slice(0, -3));
116
+ if (skip.has(name) || seen.has(name) || !meta.description) continue;
117
+ seen.add(name);
118
+ out.push({ name, description: meta.description, tools: meta.tools || "", model: meta.model || "", body, path });
119
+ }
120
+ }
121
+ return out.sort((a, b) => a.name.localeCompare(b.name));
122
+ }
123
+ function readHooks(root) {
124
+ const raw = readText(join(root, "hooks", "hooks.json"));
125
+ if (!raw) return [];
126
+ const hooks = JSON.parse(raw).hooks ?? {};
127
+ const out = [];
128
+ for (const [event, groups] of Object.entries(hooks)) {
129
+ for (const group of groups ?? []) {
130
+ for (const hook of group.hooks ?? []) {
131
+ if (hook.type !== "command" || !hook.command) continue;
132
+ const spec = { event, matcher: group.matcher ?? "", command: hook.command, timeout: hook.timeout ?? DEFAULT_HOOK_TIMEOUT_S };
133
+ if (hook.args !== void 0) {
134
+ if (!Array.isArray(hook.args) || !hook.args.every((arg) => typeof arg === "string")) throw new Error(`hooks.json: ${event} hook "${hook.command}" has args that are not a list of strings`);
135
+ spec.args = hook.args;
136
+ }
137
+ if (hook.onFailure === "block" || hook.onFailure === "continue") spec.onFailure = hook.onFailure;
138
+ out.push(spec);
139
+ }
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+ function matcherMatches(matcher, toolName) {
145
+ if (!matcher || matcher === "*") return true;
146
+ try {
147
+ return new RegExp(`^(?:${matcher})$`).test(toolName);
148
+ } catch {
149
+ return matcher === toolName;
150
+ }
151
+ }
152
+ function renderCommand(body, rawInput, root) {
153
+ const args = rawInput.trim();
154
+ const positional = args ? args.split(/\s+/) : [];
155
+ let text = expandPluginRoot(body, root);
156
+ const usesArgs = /\$ARGUMENTS|\$[1-9]/.test(text);
157
+ text = text.replace(/\$(ARGUMENTS|[1-9])/g, (_, key) => key === "ARGUMENTS" ? args : positional[Number(key) - 1] ?? "");
158
+ if (!usesArgs && args) text += `
159
+
160
+ Arguments: ${args}`;
161
+ return text;
162
+ }
163
+
164
+ // src/hooks.ts
165
+ var PLACEHOLDERS = ["CLAUDE_PLUGIN_ROOT", "CLAUDE_PROJECT_DIR"];
166
+ function expandPlaceholders(text, env) {
167
+ let out = text;
168
+ for (const name of PLACEHOLDERS) {
169
+ const value = env[name];
170
+ if (value !== void 0) out = out.replaceAll("${" + name + "}", value).replaceAll("$" + name, value);
171
+ }
172
+ return out;
173
+ }
174
+ var HOOK_STDOUT_LIMIT = 256 * 1024;
175
+ var HOOK_DRAIN_MS = 50;
176
+ var PLAIN_CONTEXT_EVENTS = /* @__PURE__ */ new Set(["SessionStart", "UserPromptSubmit"]);
177
+ function runHook(hook, input, env, signal) {
178
+ return new Promise((resolvePromise) => {
179
+ let stdout = "";
180
+ let stderr = "";
181
+ let truncated = false;
182
+ let settled = false;
183
+ let drain;
184
+ const options = { cwd: input.cwd || process.cwd(), env, stdio: ["pipe", "pipe", "pipe"], detached: true };
185
+ const child = hook.args ? spawn(expandPlaceholders(hook.command, env), hook.args.map((arg) => expandPlaceholders(arg, env)), options) : spawn("/bin/sh", ["-c", hook.command], options);
186
+ const killGroup = () => {
187
+ try {
188
+ if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
189
+ } catch {
190
+ child.kill("SIGKILL");
191
+ }
192
+ };
193
+ const timer = setTimeout(
194
+ () => {
195
+ killGroup();
196
+ finish({ code: null, stdout, stderr: stderr + `
197
+ hook timed out after ${hook.timeout}s`, timedOut: true, truncated });
198
+ },
199
+ (hook.timeout || DEFAULT_HOOK_TIMEOUT_S) * 1e3
200
+ );
201
+ const onAbort = () => {
202
+ killGroup();
203
+ finish({ code: null, stdout: "", stderr: "hook cancelled", timedOut: false, truncated: false, aborted: true });
204
+ };
205
+ signal?.addEventListener("abort", onAbort, { once: true });
206
+ function finish(result) {
207
+ if (settled) return;
208
+ settled = true;
209
+ clearTimeout(timer);
210
+ clearTimeout(drain);
211
+ signal?.removeEventListener("abort", onAbort);
212
+ child.stdout.destroy();
213
+ child.stderr.destroy();
214
+ resolvePromise(result);
215
+ }
216
+ child.stdout.on("data", (chunk) => {
217
+ if (stdout.length + chunk.length > HOOK_STDOUT_LIMIT) truncated = true;
218
+ else stdout += chunk;
219
+ });
220
+ child.stderr.on("data", (chunk) => {
221
+ if (stderr.length < HOOK_STDOUT_LIMIT) stderr += chunk;
222
+ });
223
+ child.on("error", (error) => finish({ code: null, stdout, stderr: String(error), timedOut: false, truncated }));
224
+ child.on("exit", (code) => {
225
+ drain = setTimeout(() => finish({ code, stdout, stderr, timedOut: false, truncated }), HOOK_DRAIN_MS);
226
+ });
227
+ child.on("close", (code) => finish({ code, stdout, stderr, timedOut: false, truncated }));
228
+ child.stdin.on("error", () => {
229
+ });
230
+ child.stdin.end(JSON.stringify(input));
231
+ });
232
+ }
233
+ function interpretHookResult(result, event) {
234
+ const out = { context: [], decision: void 0, reason: void 0, updatedInput: void 0, notices: [] };
235
+ if (result.code === 2) {
236
+ out.decision = "block";
237
+ out.reason = result.stderr.trim() || "blocked by hook";
238
+ return out;
239
+ }
240
+ if (result.code !== 0) return out;
241
+ if (result.truncated) {
242
+ out.notices.push(`hook output over ${HOOK_STDOUT_LIMIT} bytes was dropped`);
243
+ return out;
244
+ }
245
+ const text = result.stdout.trim();
246
+ if (!text) return out;
247
+ let json;
248
+ try {
249
+ json = JSON.parse(text);
250
+ } catch {
251
+ if (event === void 0 || PLAIN_CONTEXT_EVENTS.has(event)) out.context.push(text);
252
+ return out;
253
+ }
254
+ const specific = json.hookSpecificOutput ?? {};
255
+ if (specific.additionalContext) out.context.push(String(specific.additionalContext));
256
+ if (specific.updatedInput && typeof specific.updatedInput === "object" && !Array.isArray(specific.updatedInput)) out.updatedInput = specific.updatedInput;
257
+ if (json.systemMessage) out.notices.push(String(json.systemMessage));
258
+ if (specific.permissionDecision) {
259
+ out.decision = specific.permissionDecision;
260
+ out.reason = specific.permissionDecisionReason;
261
+ } else if (json.decision) {
262
+ out.decision = json.decision === "approve" ? "allow" : json.decision;
263
+ out.reason = json.reason;
264
+ }
265
+ if (json.continue === false) {
266
+ out.decision = "block";
267
+ out.reason = json.stopReason || out.reason;
268
+ }
269
+ return out;
270
+ }
271
+
272
+ // src/transcript.ts
273
+ import { randomUUID } from "node:crypto";
274
+ import { appendFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
275
+ import { homedir } from "node:os";
276
+ import { dirname as dirname2, join as join2 } from "node:path";
277
+ var TOOL_NAME_MAP = {
278
+ bash: "Bash",
279
+ read: "Read",
280
+ read_image: "Read",
281
+ write: "Write",
282
+ edit: "Edit",
283
+ str_replace_editor: "Edit",
284
+ glob: "Glob",
285
+ grep: "Grep",
286
+ web_fetch: "WebFetch",
287
+ web_search: "WebSearch",
288
+ skill: "Skill",
289
+ subagent: "Agent",
290
+ subagent_fork: "Agent",
291
+ spawn_teammate: "Agent",
292
+ todo_write: "TodoWrite",
293
+ ask_user_question: "AskUserQuestion"
294
+ };
295
+ var claudeToolName = (dshName) => TOOL_NAME_MAP[dshName] ?? dshName;
296
+ function textOf(message) {
297
+ return (message?.content ?? []).filter((b) => b?.type === "text" && typeof b.text === "string" && b.text !== "").map((b) => b.text).join("\n");
298
+ }
299
+ function parseArguments(raw) {
300
+ if (raw === void 0 || raw === null) return {};
301
+ if (typeof raw !== "string") return raw;
302
+ try {
303
+ return JSON.parse(raw);
304
+ } catch {
305
+ return { raw };
306
+ }
307
+ }
308
+ function toClaudeToolInput(dshName, args) {
309
+ const input = { ...args ?? {} };
310
+ if (dshName === "glob" || dshName === "grep") {
311
+ if (input.include && !input.glob) input.glob = input.include;
312
+ }
313
+ if (dshName === "subagent" || dshName === "subagent_fork" || dshName === "spawn_teammate") {
314
+ if (input.description === void 0 && input.name) input.description = input.name;
315
+ if (input.subagent_type === void 0) input.subagent_type = input.name ?? "general-purpose";
316
+ }
317
+ if (dshName === "skill" && input.name && input.skill === void 0) input.skill = input.name;
318
+ return input;
319
+ }
320
+ function toClaudeToolResponse(dshName, args, result) {
321
+ const input = args ?? {};
322
+ const output = textOf(result);
323
+ const isError = result?.isError === true;
324
+ switch (dshName) {
325
+ case "bash":
326
+ return { stdout: isError ? "" : output, stderr: isError ? output : "", interrupted: false, isImage: false };
327
+ case "read":
328
+ case "read_image":
329
+ return { type: "text", file: { filePath: String(input.file_path ?? input.path ?? ""), content: output } };
330
+ case "write":
331
+ return { type: "update", filePath: String(input.file_path ?? ""), content: String(input.content ?? ""), originalFile: null };
332
+ case "edit":
333
+ case "str_replace_editor":
334
+ return {
335
+ filePath: String(input.file_path ?? input.path ?? ""),
336
+ oldString: String(input.old_string ?? input.old_str ?? ""),
337
+ newString: String(input.new_string ?? input.new_str ?? "")
338
+ };
339
+ case "glob": {
340
+ const filenames = output.split("\n").map((line) => line.trim()).filter(Boolean);
341
+ return { filenames, durationMs: 0, numFiles: filenames.length, truncated: false };
342
+ }
343
+ case "grep": {
344
+ const lines = output.split("\n").filter(Boolean);
345
+ return { content: output, numFiles: lines.length };
346
+ }
347
+ case "web_fetch":
348
+ return { url: String(input.url ?? ""), result: output };
349
+ case "web_search":
350
+ return { query: String(input.query ?? (Array.isArray(input.queries) ? input.queries.join(" ") : "")), results: output };
351
+ default:
352
+ return { content: output, isError };
353
+ }
354
+ }
355
+ var isUserMessageEvent = (e) => e.type === "user/message";
356
+ function toClaudeTranscript(events, sessionId, cwd) {
357
+ const lines = [];
358
+ const stamp = (event) => new Date(event.time ?? Date.now()).toISOString();
359
+ for (const event of events) {
360
+ if (!event || typeof event !== "object") continue;
361
+ if (isUserMessageEvent(event)) {
362
+ const message = event.data;
363
+ if (message?.source?.kind !== "user") continue;
364
+ const text = textOf(message);
365
+ if (!text) continue;
366
+ lines.push({ type: "user", uuid: randomUUID(), sessionId, cwd, timestamp: stamp(event), message: { role: "user", content: text } });
367
+ } else if (event.type === "assistant/message") {
368
+ const message = event.data?.message;
369
+ if (!message) continue;
370
+ const content = [];
371
+ for (const block of message.content ?? []) {
372
+ if (block?.type === "text" && "text" in block && typeof block.text === "string" && block.text) content.push({ type: "text", text: block.text });
373
+ else if (block?.type === "tool-call") {
374
+ const call = block;
375
+ content.push({ type: "tool_use", id: call.id, name: claudeToolName(call.name), input: toClaudeToolInput(call.name, parseArguments(call.arguments)) });
376
+ }
377
+ }
378
+ if (content.length) lines.push({ type: "assistant", uuid: randomUUID(), sessionId, cwd, timestamp: stamp(event), message: { role: "assistant", content } });
379
+ } else if (event.type === "tool/result") {
380
+ const message = event.data?.message;
381
+ if (!message?.toolCallId) continue;
382
+ lines.push({
383
+ type: "user",
384
+ uuid: randomUUID(),
385
+ sessionId,
386
+ cwd,
387
+ timestamp: stamp(event),
388
+ message: { role: "user", content: [{ type: "tool_result", tool_use_id: message.toolCallId, content: textOf(message), is_error: message.isError === true }] }
389
+ });
390
+ }
391
+ }
392
+ return lines.map((line) => JSON.stringify(line)).join("\n") + (lines.length ? "\n" : "");
393
+ }
394
+ function sessionEvents(session) {
395
+ try {
396
+ return session.snapshotEvents?.() ?? session.events ?? session.ownEvents?.() ?? [];
397
+ } catch {
398
+ return [];
399
+ }
400
+ }
401
+ function lastAssistantText(session) {
402
+ const events = sessionEvents(session);
403
+ for (let i = events.length - 1; i >= 0; i--) {
404
+ const event = events[i];
405
+ if (event?.type === "assistant/message") return textOf(event.data?.message);
406
+ }
407
+ return "";
408
+ }
409
+ function stateHome() {
410
+ const base = process.env.DSH_HOME || join2(homedir(), ".dsh");
411
+ return join2(base, "state", "claude-compat");
412
+ }
413
+ function transcriptPath(agent) {
414
+ return join2(stateHome(), "transcripts", `${String(agent.session.header.id).replace(/[^A-Za-z0-9_-]/g, "")}.jsonl`);
415
+ }
416
+ var exported = /* @__PURE__ */ new Map();
417
+ function syncTranscript(agent, cwd) {
418
+ const path = transcriptPath(agent);
419
+ const events = sessionEvents(agent.session);
420
+ let done = exported.get(path);
421
+ if (done === void 0 || done > events.length || !existsSync(path)) {
422
+ mkdirSync(dirname2(path), { recursive: true, mode: 448 });
423
+ writeFileSync(path, "", { mode: 384 });
424
+ done = 0;
425
+ }
426
+ if (events.length > done) appendFileSync(path, toClaudeTranscript(events.slice(done), agent.session.header.id, cwd), { mode: 384 });
427
+ exported.set(path, events.length);
428
+ return path;
429
+ }
430
+ function dropTranscript(agent) {
431
+ const path = transcriptPath(agent);
432
+ exported.delete(path);
433
+ rmSync(path, { force: true });
434
+ }
435
+
436
+ // src/plugin.ts
437
+ var COMPAT_VERSION = "0.1.0";
438
+ var SKILL_RANK = 450;
439
+ function createPlugin(spec) {
440
+ const root = resolve2(spec.root);
441
+ const manifest = readManifest(root);
442
+ const pluginName = kebab(manifest.name);
443
+ const id = `claude-compat-${pluginName}`;
444
+ const commandPrefix = spec.commands?.prefix ?? pluginName;
445
+ const hasAgents = () => !(spec.disableByDefault ?? []).includes("agents") && discoverAgents(root, spec.agents).length > 0;
446
+ const harnessNote = () => [
447
+ `> Loaded from the ${pluginName} Claude Code plugin into DeepSeek Harness. Plugin root: \`${root}\`.`,
448
+ `> A Claude command \`/${pluginName}:<cmd>\` is the DSH command \`/${commandPrefix ? `${commandPrefix}-` : ""}<cmd>\`; when you cannot run it, do what its section here says.`,
449
+ hasAgents() ? `> A Claude agent type \`${pluginName}:<agent>\` is the skill \`${pluginName}-agent-<agent>\`: load it and pass its role text to \`subagent\` or \`spawn_teammate\`.` : ""
450
+ ].filter(Boolean).join("\n");
451
+ function apply(host, config = {}) {
452
+ if (!("inject" in host) || typeof host.inject !== "function" || !("on" in host) || typeof host.on !== "function") {
453
+ throw new TypeError("claude-plugins-dsh: expected a Cordis context with inject() and on()");
454
+ }
455
+ const ctx = host;
456
+ const disabled = new Set(config.disable ?? spec.disableByDefault ?? []);
457
+ const enabled = (part) => !disabled.has(part);
458
+ const warn = (msg) => (ctx.logger ?? console).warn(`[${id}] ${msg}`);
459
+ if (enabled("skills") || enabled("agents")) ctx.inject(["skills"], (sub) => registerSkills(sub, enabled, warn));
460
+ if (enabled("commands")) ctx.inject(["commands"], (sub) => registerCommands(sub, warn));
461
+ if (enabled("instructions")) ctx.inject(["systemPrompt"], (sub) => registerInstructions(sub, warn));
462
+ if (enabled("hooks")) registerHooks(ctx, warn);
463
+ }
464
+ function registerSkills(ctx, enabled, warn) {
465
+ if (!ctx.skills?.registerProvider) return warn("skills service missing, skills not registered");
466
+ const provider = {
467
+ name: id,
468
+ async list() {
469
+ const candidates = [];
470
+ if (enabled("skills")) {
471
+ for (const skill of discoverSkills(root, spec.skills)) {
472
+ candidates.push({
473
+ name: skill.name,
474
+ description: skill.description,
475
+ ...skill.meta.whenToUse ? { whenToUse: skill.meta.whenToUse } : {},
476
+ invocation: { modelInvocable: skill.meta["disable-model-invocation"] !== "true", userInvocable: skill.meta["user-invocable"] !== "false" },
477
+ provider: id,
478
+ source: "custom",
479
+ path: skill.path,
480
+ resourceBase: { kind: "directory", path: skill.base },
481
+ rank: SKILL_RANK,
482
+ locator: { kind: "skill", path: skill.path, base: skill.base }
483
+ });
484
+ }
485
+ }
486
+ if (enabled("agents")) {
487
+ for (const agent of discoverAgents(root, spec.agents)) {
488
+ candidates.push({
489
+ name: `${pluginName}-agent-${agent.name}`,
490
+ description: `Role brief for a delegated worker (${pluginName} agent "${agent.name}"). ${agent.description}`,
491
+ invocation: { modelInvocable: true, userInvocable: true },
492
+ provider: id,
493
+ source: "custom",
494
+ path: agent.path,
495
+ resourceBase: { kind: "directory", path: dirname3(agent.path) },
496
+ rank: SKILL_RANK,
497
+ locator: { kind: "agent", path: agent.path }
498
+ });
499
+ }
500
+ }
501
+ return candidates;
502
+ },
503
+ async get(candidate) {
504
+ const locator = candidate.locator;
505
+ const text = readText(locator.path);
506
+ if (text === void 0) return void 0;
507
+ const { body } = parseFrontMatter(text);
508
+ let content = expandPluginRoot(body, root);
509
+ if (locator.kind === "skill") {
510
+ content = `${harnessNote()}
511
+ > Relative paths in this skill resolve from \`${locator.base}\`.
512
+
513
+ ${content}`;
514
+ } else {
515
+ const agent = discoverAgents(root, spec.agents).find((a) => a.path === locator.path);
516
+ const tools = agent?.tools ? `
517
+ Original tool allowlist: ${agent.tools}. Respect it: do not use tools outside that list.` : "";
518
+ content = [
519
+ `# Delegated role: ${agent?.name ?? candidate.name}`,
520
+ "",
521
+ `This is a Claude Code agent definition from the ${pluginName} plugin. DSH has no agent types, so use it as a brief:`,
522
+ "- To run this role, call `subagent` (or `spawn_teammate` for a durable teammate) with a prompt made of the role text below, then the concrete task, exact files, and the check that proves it done.",
523
+ "- Pick the provider/model yourself; the original pin does not apply in DSH." + tools,
524
+ "",
525
+ "## Role text",
526
+ "",
527
+ content
528
+ ].join("\n");
529
+ }
530
+ const definition = {
531
+ name: candidate.name,
532
+ description: candidate.description,
533
+ invocation: candidate.invocation,
534
+ provider: id,
535
+ source: "custom",
536
+ path: locator.path,
537
+ content
538
+ };
539
+ if (candidate.resourceBase) definition.resourceBase = candidate.resourceBase;
540
+ return definition;
541
+ }
542
+ };
543
+ ctx.skills.registerProvider(() => provider);
544
+ }
545
+ function registerCommands(ctx, warn) {
546
+ const commands = ctx.commands;
547
+ if (!commands?.register) return;
548
+ for (const command of discoverCommands(root, spec.commands)) {
549
+ const name = commandPrefix ? `${commandPrefix}-${command.name}` : command.name;
550
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
551
+ warn(`command name ${name} is not valid, skipped`);
552
+ continue;
553
+ }
554
+ try {
555
+ commands.register({
556
+ name,
557
+ description: command.description,
558
+ ...command.hint ? { input: { hint: command.hint } } : {},
559
+ handler(invocation) {
560
+ const text = readText(command.path);
561
+ const body = text === void 0 ? command.body : parseFrontMatter(text).body;
562
+ invocation.agent.followup({
563
+ id: randomUUID2(),
564
+ role: "user",
565
+ content: [{ type: "text", text: `<command-name>/${pluginName}:${command.stem}</command-name>
566
+
567
+ ${renderCommand(body, invocation.rawInput, root)}` }],
568
+ source: { kind: "user" }
569
+ });
570
+ return { kind: "success", text: `/${name} queued` };
571
+ }
572
+ });
573
+ } catch (error) {
574
+ warn(`command /${name} not registered: ${error.message}`);
575
+ }
576
+ }
577
+ }
578
+ function registerInstructions(ctx, warn) {
579
+ const files = spec.instructions ?? [];
580
+ if (!files.length) return;
581
+ if (!ctx.systemPrompt?.section) return warn("systemPrompt service missing, instructions not registered");
582
+ ctx.systemPrompt.section({
583
+ name: `${id}-instructions`,
584
+ order: 450,
585
+ interpolate: false,
586
+ text: () => files.map((file) => {
587
+ const raw = readText(resolve2(root, file));
588
+ if (raw === void 0) return "";
589
+ const stripped = stripClaudeOnlyBlocks(expandPluginRoot(raw, root));
590
+ if (stripped.unterminated) warn(`${file}: <!--harness:claude-only--> has no closing <!--/harness-->, the rest of the file was dropped`);
591
+ return stripped.text.trim();
592
+ }).filter(Boolean).join("\n\n")
593
+ });
594
+ }
595
+ function registerHooks(ctx, warn) {
596
+ const only = spec.hooks?.events ? new Set(spec.hooks.events) : void 0;
597
+ const hooks = readHooks(root).filter((hook) => !only || only.has(hook.event));
598
+ if (!hooks.length) return;
599
+ const byEvent = (event) => hooks.filter((hook) => hook.event === event);
600
+ const stopBlocks = /* @__PURE__ */ new WeakMap();
601
+ const tag = `${pluginName}-hook`;
602
+ let workingDirectory;
603
+ ctx.inject(["workingDirectory"], (sub) => {
604
+ workingDirectory = sub.workingDirectory;
605
+ sub.effect(() => () => {
606
+ workingDirectory = void 0;
607
+ });
608
+ });
609
+ const cwdOf = (agent) => {
610
+ try {
611
+ return workingDirectory?.get(agent.session) || agent.session.header.cwd || process.cwd();
612
+ } catch {
613
+ return agent.session.header.cwd || process.cwd();
614
+ }
615
+ };
616
+ const envFor = (cwd) => {
617
+ const env = { ...process.env, CLAUDE_PLUGIN_ROOT: root, CLAUDE_PROJECT_DIR: cwd, CLAUDE_HARNESS: "dsh", DSH_CLAUDE_COMPAT: COMPAT_VERSION };
618
+ for (const [key, value] of Object.entries(spec.env ?? {})) env[key] = expandPluginRoot(String(value), root);
619
+ return env;
620
+ };
621
+ const inputFor = (agent, event, cwd, extra = {}) => {
622
+ let transcript = transcriptPath(agent);
623
+ try {
624
+ transcript = syncTranscript(agent, cwd);
625
+ } catch (error) {
626
+ warn(`transcript export failed: ${error.message}`);
627
+ }
628
+ return { session_id: String(agent.session.header.id), transcript_path: transcript, cwd, hook_event_name: event, permission_mode: "default", ...extra };
629
+ };
630
+ async function fire(event, agent, extra, options = {}) {
631
+ const list = byEvent(event).filter(options.filter ?? (() => true));
632
+ if (!list.length) return [];
633
+ const cwd = cwdOf(agent);
634
+ const input = inputFor(agent, event, cwd, extra);
635
+ const env = envFor(cwd);
636
+ const settled = await Promise.all(
637
+ list.map(
638
+ (hook) => runHook(hook, input, env, options.signal).catch((error) => ({ code: null, stdout: "", stderr: String(error?.message ?? error), timedOut: false }))
639
+ )
640
+ );
641
+ return settled.map((result, index) => {
642
+ const failed = !result.aborted && (result.timedOut || result.code !== 0 && result.code !== 2);
643
+ if (failed) warn(`${event} hook exited ${result.code}: ${result.stderr.trim().slice(0, 300)}`);
644
+ const interpreted = interpretHookResult(result, event);
645
+ if (failed && list[index].onFailure === "block") {
646
+ interpreted.decision = "block";
647
+ interpreted.reason = `${pluginName} hook failed and its onFailure is "block": ${result.stderr.trim().slice(0, 300) || `exit ${result.code}`}`;
648
+ }
649
+ for (const notice of interpreted.notices) warn(`${event}: ${notice}`);
650
+ return interpreted;
651
+ });
652
+ }
653
+ const hookMessage = (event, text) => ({
654
+ id: randomUUID2(),
655
+ role: "user",
656
+ content: [{ type: "text", text: `<${tag} event="${event}">
657
+ ${escapeClosingTag(text, tag)}
658
+ </${tag}>` }],
659
+ source: { kind: `plugin:${id}` }
660
+ });
661
+ const contextOf = (results) => results.flatMap((r) => r.context).filter(Boolean).join("\n\n");
662
+ function injectContext(agent, event, results) {
663
+ const text = contextOf(results);
664
+ if (text) agent.inject(hookMessage(event, text));
665
+ }
666
+ const isSubagent = (agent) => agent.session.header.origin === "subagent";
667
+ if (byEvent("SessionStart").length) {
668
+ ctx.on("agent/created", async ({ agent, source, signal }) => {
669
+ if (isSubagent(agent)) return;
670
+ const results = await fire("SessionStart", agent, { source: source ?? "startup" }, { signal });
671
+ injectContext(agent, "SessionStart", results);
672
+ });
673
+ }
674
+ if (byEvent("UserPromptSubmit").length) {
675
+ ctx.on("agent/pre-step", async (payload, next) => {
676
+ const decision = await next();
677
+ if (decision.kind !== "enter" || payload.signal?.aborted) return decision;
678
+ const prompt = decision.messages.filter((m) => m?.source?.kind === "user").map(textOf).join("\n").trim();
679
+ if (!prompt) return decision;
680
+ const results = await fire("UserPromptSubmit", payload.agent, { prompt }, { signal: payload.signal });
681
+ const blocked = results.find((r) => r.decision === "block");
682
+ if (blocked) {
683
+ const notice = `The ${pluginName} plugin blocked the user's last prompt: ${blocked.reason ?? "no reason given"}. Tell the user it was blocked and why. Do not act on it.`;
684
+ return { ...decision, messages: [hookMessage("UserPromptSubmit", notice)] };
685
+ }
686
+ const text = contextOf(results);
687
+ if (!text) return decision;
688
+ return { ...decision, messages: [...decision.messages, hookMessage("UserPromptSubmit", text)] };
689
+ });
690
+ }
691
+ if (byEvent("PreToolUse").length) {
692
+ ctx.on("tools/pre-execute", async (exec, next) => {
693
+ const agent = exec.agent;
694
+ if (!agent) return next();
695
+ const toolName = claudeToolName(exec.name);
696
+ const matches = (hook) => matcherMatches(hook.matcher, toolName);
697
+ if (!byEvent("PreToolUse").some(matches)) return next();
698
+ const results = await fire(
699
+ "PreToolUse",
700
+ agent,
701
+ { tool_name: toolName, tool_input: toClaudeToolInput(exec.name, parseArguments(exec.arguments)), tool_use_id: String(exec.callId) },
702
+ { filter: matches, signal: exec.signal }
703
+ );
704
+ injectContext(agent, "PreToolUse", results);
705
+ const deny = results.find((r) => r.decision === "deny" || r.decision === "block");
706
+ if (deny) return { kind: "deny", reason: deny.reason || `denied by ${pluginName} hook` };
707
+ const rewrote = results.find((r) => r.updatedInput !== void 0);
708
+ if (rewrote) {
709
+ return {
710
+ kind: "deny",
711
+ reason: `${pluginName} hook returned updatedInput, which DSH cannot apply; refusing to run the unmodified tool input`
712
+ };
713
+ }
714
+ const ask = results.find((r) => r.decision === "ask");
715
+ if (ask) {
716
+ const reason = ask.reason || `${pluginName} hook asks for confirmation`;
717
+ return { kind: "ask", reason, displayReason: { en: reason } };
718
+ }
719
+ const decision = await next();
720
+ const allow = results.find((r) => r.decision === "allow");
721
+ if (allow && decision.kind === "ask") return { kind: "allow" };
722
+ return decision;
723
+ });
724
+ }
725
+ if (byEvent("PostToolUse").length) {
726
+ ctx.on("tools/post-execute", async (exec, result, next) => {
727
+ const decision = await next();
728
+ const agent = exec.agent;
729
+ if (!agent) return decision;
730
+ const toolName = claudeToolName(exec.name);
731
+ const matches = (hook) => matcherMatches(hook.matcher, toolName);
732
+ if (!byEvent("PostToolUse").some(matches)) return decision;
733
+ const args = parseArguments(exec.arguments);
734
+ const results = await fire(
735
+ "PostToolUse",
736
+ agent,
737
+ {
738
+ tool_name: toolName,
739
+ tool_input: toClaudeToolInput(exec.name, args),
740
+ tool_response: toClaudeToolResponse(exec.name, args, result),
741
+ tool_use_id: String(exec.callId)
742
+ },
743
+ { filter: matches, signal: exec.signal }
744
+ );
745
+ const text = contextOf(results);
746
+ const contexts = text ? [hookMessage("PostToolUse", text)] : [];
747
+ const blocked = results.find((r) => r.decision === "block" || r.decision === "deny");
748
+ if (blocked) {
749
+ return {
750
+ kind: "block",
751
+ feedback: [{ type: "text", text: blocked.reason || `blocked by ${pluginName} hook` }],
752
+ ...contexts.length ? { additionalContexts: contexts } : {}
753
+ };
754
+ }
755
+ if (!contexts.length) return decision;
756
+ if (decision.kind === "accept" || decision.kind === "block") {
757
+ return { ...decision, additionalContexts: [...decision.additionalContexts ?? [], ...contexts] };
758
+ }
759
+ return decision;
760
+ });
761
+ }
762
+ if (byEvent("Stop").length || byEvent("SubagentStop").length) {
763
+ ctx.on("agent/turn-stopping", async ({ agent, signal }) => {
764
+ if (signal?.aborted) return;
765
+ const event = isSubagent(agent) ? "SubagentStop" : "Stop";
766
+ if (!byEvent(event).length) return;
767
+ const active = stopBlocks.get(agent) === true;
768
+ const results = await fire(event, agent, { stop_hook_active: active, last_assistant_message: lastAssistantText(agent.session) }, { signal });
769
+ const block = results.find((r) => r.decision === "block");
770
+ if (block && !active) {
771
+ stopBlocks.set(agent, true);
772
+ agent.steer(hookMessage(event, block.reason ?? "A plugin hook asked for one more step."));
773
+ } else {
774
+ stopBlocks.set(agent, false);
775
+ }
776
+ });
777
+ }
778
+ ctx.on("agent/disposed", ({ agent }) => {
779
+ const done = byEvent("SessionEnd").length ? fire("SessionEnd", agent, { reason: "other" }) : Promise.resolve([]);
780
+ done.catch((error) => {
781
+ warn(`SessionEnd: ${error.message}`);
782
+ return [];
783
+ }).finally(() => {
784
+ try {
785
+ dropTranscript(agent);
786
+ } catch {
787
+ }
788
+ });
789
+ });
790
+ }
791
+ return { name: id, apply };
792
+ }
793
+ function rootFrom(importMetaUrl) {
794
+ const candidate = resolve2(dirname3(fileURLToPath(importMetaUrl)), "..");
795
+ if (!existsSync2(join3(candidate, ".claude-plugin"))) throw new Error(`claude-plugins-dsh: no .claude-plugin under ${candidate}`);
796
+ return candidate;
797
+ }
798
+ export {
799
+ COMPAT_VERSION,
800
+ DEFAULT_HOOK_TIMEOUT_S,
801
+ SKILL_RANK,
802
+ TOOL_NAME_MAP,
803
+ claudeToolName,
804
+ createPlugin,
805
+ discoverAgents,
806
+ discoverCommands,
807
+ discoverSkills,
808
+ escapeClosingTag,
809
+ expandPluginRoot,
810
+ interpretHookResult,
811
+ kebab,
812
+ matcherMatches,
813
+ parseFrontMatter,
814
+ readHooks,
815
+ readManifest,
816
+ readText,
817
+ renderCommand,
818
+ rootFrom,
819
+ runHook,
820
+ stripClaudeOnly,
821
+ stripClaudeOnlyBlocks,
822
+ toClaudeToolInput,
823
+ toClaudeToolResponse,
824
+ toClaudeTranscript
825
+ };