dsh-desktop-kit 2.2.1 → 2.7.3
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/CHANGELOG.md +86 -0
- package/README.md +114 -27
- package/client.js +3270 -32
- package/docs/AGENT-HARNESS.md +144 -0
- package/docs/ARCHITECTURE.md +38 -7
- package/docs/COMPATIBILITY.md +4 -2
- package/docs/DEVELOPMENT.md +56 -0
- package/docs/INSTALL.md +20 -11
- package/docs/LAPTOP-INSTALL.md +96 -0
- package/docs/ROADMAP-3.0.md +353 -0
- package/docs/ROADMAP.md +1 -1
- package/docs/SUBAGENTS.md +25 -0
- package/docs/TEST-0.2.2.md +5 -5
- package/docs/TEST-2.3.0.md +49 -0
- package/docs/TEST-2.7.2.md +46 -0
- package/docs/TEST-2.7.3.md +20 -0
- package/docs/TOKENS.md +2 -2
- package/docs/TROUBLESHOOTING.md +27 -0
- package/docs/UI-QA.md +28 -0
- package/docs/images/peak-toast-peak.png +0 -0
- package/docs/images/peak-toast-soon.png +0 -0
- package/docs/preview/peak-toast.html +155 -0
- package/index.js +18 -1
- package/package.json +26 -6
- package/plugins/dsh-agent-extensions/README.md +107 -0
- package/plugins/dsh-agent-extensions/client.js +157 -0
- package/plugins/dsh-agent-extensions/index.js +174 -0
- package/plugins/dsh-agent-extensions/lib/agents.js +76 -0
- package/plugins/dsh-agent-extensions/lib/api.js +53 -0
- package/plugins/dsh-agent-extensions/lib/checkpoints.js +103 -0
- package/plugins/dsh-agent-extensions/lib/commands.js +255 -0
- package/plugins/dsh-agent-extensions/lib/hooks.js +182 -0
- package/plugins/dsh-agent-extensions/lib/policy.js +84 -0
- package/plugins/dsh-agent-extensions/lib/project.js +144 -0
- package/plugins/dsh-agent-extensions/lib/scaffold.js +151 -0
- package/plugins/dsh-agent-extensions/package.json +20 -0
- package/plugins/dsh-agent-extensions/screenshots/agents-1440.png +0 -0
- package/plugins/dsh-agent-extensions/screenshots/agents-390.png +0 -0
- package/plugins/dsh-browser-qa/README.md +221 -0
- package/plugins/dsh-browser-qa/cordis.patch.yml +4 -0
- package/plugins/dsh-browser-qa/index.js +165 -0
- package/plugins/dsh-browser-qa/lib/cdp.js +224 -0
- package/plugins/dsh-browser-qa/lib/chrome.js +466 -0
- package/plugins/dsh-browser-qa/lib/pages.js +552 -0
- package/plugins/dsh-browser-qa/lib/qa.js +827 -0
- package/plugins/dsh-browser-qa/lib/ui-gate.js +256 -0
- package/plugins/dsh-browser-qa/package.json +31 -0
- package/plugins/dsh-checkpoints/README.md +22 -0
- package/plugins/dsh-checkpoints/cordis.patch.yml +3 -0
- package/plugins/dsh-checkpoints/index.js +80 -0
- package/plugins/dsh-checkpoints/lib/checkpoints.js +111 -0
- package/plugins/dsh-checkpoints/package.json +25 -0
- package/plugins/dsh-github-review/README.md +131 -0
- package/plugins/dsh-github-review/cordis.patch.yml +3 -0
- package/plugins/dsh-github-review/index.js +536 -0
- package/plugins/dsh-github-review/lib/gh.js +494 -0
- package/plugins/dsh-github-review/lib/normalize.js +662 -0
- package/plugins/dsh-github-review/package.json +28 -0
- package/plugins/dsh-guardrails/README.md +264 -0
- package/plugins/dsh-guardrails/cordis.patch.yml +5 -0
- package/plugins/dsh-guardrails/index.js +389 -0
- package/plugins/dsh-guardrails/lib/hooks.js +262 -0
- package/plugins/dsh-guardrails/lib/modes.js +77 -0
- package/plugins/dsh-guardrails/lib/policy.js +592 -0
- package/plugins/dsh-guardrails/lib/scan.js +451 -0
- package/plugins/dsh-guardrails/lib/secrets.js +274 -0
- package/plugins/dsh-guardrails/package.json +26 -0
- package/plugins/dsh-mcp-manager/README.md +78 -0
- package/plugins/dsh-mcp-manager/client.js +373 -0
- package/plugins/dsh-mcp-manager/index.js +159 -0
- package/plugins/dsh-mcp-manager/lib/api.js +250 -0
- package/plugins/dsh-mcp-manager/lib/client.js +219 -0
- package/plugins/dsh-mcp-manager/lib/config.js +217 -0
- package/plugins/dsh-mcp-manager/lib/manager.js +146 -0
- package/plugins/dsh-mcp-manager/package.json +18 -0
- package/plugins/dsh-mcp-manager/screenshots/modal-390.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-1440.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-390.png +0 -0
- package/plugins/dsh-memory-bridge/README.md +161 -0
- package/plugins/dsh-memory-bridge/client.js +2264 -0
- package/plugins/dsh-memory-bridge/cordis.patch.yml +5 -0
- package/plugins/dsh-memory-bridge/docs/images/conflicts.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/hero.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-dark.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-light.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-mobile.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/setup.png +0 -0
- package/plugins/dsh-memory-bridge/icon.svg +13 -0
- package/plugins/dsh-memory-bridge/index.js +378 -0
- package/plugins/dsh-memory-bridge/lib/bridge.js +636 -0
- package/plugins/dsh-memory-bridge/lib/github.js +473 -0
- package/plugins/dsh-memory-bridge/lib/merge.js +205 -0
- package/plugins/dsh-memory-bridge/lib/portable.js +314 -0
- package/plugins/dsh-memory-bridge/lib/state.js +565 -0
- package/plugins/dsh-memory-bridge/lib/sync.js +984 -0
- package/plugins/dsh-memory-bridge/locale/en.json +6 -0
- package/plugins/dsh-memory-bridge/locale/ru.json +6 -0
- package/plugins/dsh-memory-bridge/package.json +67 -0
- package/plugins/dsh-peak-guard/README.md +16 -3
- package/plugins/dsh-peak-guard/client.js +223 -8
- package/plugins/dsh-peak-guard/package.json +1 -1
- package/plugins/dsh-plan-mode/README.md +24 -0
- package/plugins/dsh-plan-mode/cordis.patch.yml +3 -0
- package/plugins/dsh-plan-mode/index.js +103 -0
- package/plugins/dsh-plan-mode/lib/plan.js +89 -0
- package/plugins/dsh-plan-mode/package.json +18 -0
- package/plugins/dsh-profile-transfer/client.js +277 -17
- package/plugins/dsh-profile-transfer/index.js +10 -0
- package/plugins/dsh-profile-transfer/package.json +3 -1
- package/plugins/dsh-profile-transfer/transfer.js +101 -2
- package/plugins/dsh-project-memory/index.js +92 -2
- package/plugins/dsh-project-memory/package.json +1 -1
- package/plugins/dsh-repo-map/README.md +26 -0
- package/plugins/dsh-repo-map/cordis.patch.yml +3 -0
- package/plugins/dsh-repo-map/index.js +99 -0
- package/plugins/dsh-repo-map/lib/repo-map.js +170 -0
- package/plugins/dsh-repo-map/package.json +25 -0
- package/plugins/dsh-skill-autopilot/index.js +135 -11
- package/skills/delivery-gate/SKILL.md +31 -0
- package/skills/git-and-ci/SKILL.md +5 -1
- package/skills/manifest.json +31 -6
- package/skills/requirements-and-planning/SKILL.md +3 -0
- package/skills/research-first/SKILL.md +35 -0
- package/skills/research-first/references/query-patterns.md +20 -0
- package/skills/subagent-playbook/SKILL.md +36 -0
- package/skills/subagent-playbook/references/prompts.md +39 -0
- package/skills/ui-craft/SKILL.md +43 -0
- package/skills/visual-qa/SKILL.md +29 -0
- package/skills/visual-qa/references/commands.md +32 -0
- package/skills-baseline.json +219 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-defined project hooks.
|
|
3
|
+
*
|
|
4
|
+
* `.dsh/hooks.json` is opt-in: commands run only when `.dsh/hooks.trusted`
|
|
5
|
+
* exists. Events mirror the useful subset of Claude Code hooks:
|
|
6
|
+
* PreToolUse, PostToolUse, Stop, FileChanged, SessionStart, SessionEnd.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { findDshDir, loadHooks } from "./project.js";
|
|
11
|
+
|
|
12
|
+
const MAX_OUTPUT = 12_000;
|
|
13
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
14
|
+
const MAX_TIMEOUT = 120_000;
|
|
15
|
+
|
|
16
|
+
function sessionCwd(agent) {
|
|
17
|
+
const cwd = agent?.session?.header?.cwd;
|
|
18
|
+
return typeof cwd === "string" && cwd.length > 0 ? cwd : "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hookProject(cwd, fallbackFile) {
|
|
22
|
+
const start = cwd || (typeof fallbackFile === "string" ? path.dirname(fallbackFile) : process.cwd());
|
|
23
|
+
return findDshDir(start);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function matcherMatches(matcher, value) {
|
|
27
|
+
if (typeof matcher !== "string" || matcher.trim().length === 0) return true;
|
|
28
|
+
try {
|
|
29
|
+
return new RegExp(matcher, "iu").test(String(value ?? ""));
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function runCommand(hook, payload, cwd, sessionId) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const timeout = Math.max(1_000, Math.min(MAX_TIMEOUT, Number(hook.timeoutMs) || DEFAULT_TIMEOUT));
|
|
38
|
+
let stdout = "";
|
|
39
|
+
let stderr = "";
|
|
40
|
+
let settled = false;
|
|
41
|
+
let child;
|
|
42
|
+
const finish = (result) => {
|
|
43
|
+
if (settled) return;
|
|
44
|
+
settled = true;
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
resolve(result);
|
|
47
|
+
};
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
try { child?.kill(); } catch { /* best effort */ }
|
|
50
|
+
finish({ code: 124, stdout, stderr: `${stderr}\nhook timed out after ${timeout}ms`.trim(), timedOut: true });
|
|
51
|
+
}, timeout);
|
|
52
|
+
try {
|
|
53
|
+
child = spawn(String(hook.command), {
|
|
54
|
+
cwd,
|
|
55
|
+
shell: true,
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
DSH_HOOK_EVENT: String(hook.event ?? ""),
|
|
60
|
+
DSH_PROJECT_DIR: cwd,
|
|
61
|
+
DSH_SESSION_ID: sessionId,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
finish({ code: 1, stdout: "", stderr: String(error) });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
child.stdout?.on("data", (chunk) => {
|
|
69
|
+
if (stdout.length < MAX_OUTPUT) stdout += chunk.toString("utf8");
|
|
70
|
+
});
|
|
71
|
+
child.stderr?.on("data", (chunk) => {
|
|
72
|
+
if (stderr.length < MAX_OUTPUT) stderr += chunk.toString("utf8");
|
|
73
|
+
});
|
|
74
|
+
child.on("error", (error) => finish({ code: 1, stdout, stderr: String(error) }));
|
|
75
|
+
child.on("close", (code) => finish({ code: Number(code) || 0, stdout, stderr }));
|
|
76
|
+
try {
|
|
77
|
+
child.stdin?.end(JSON.stringify(payload));
|
|
78
|
+
} catch {
|
|
79
|
+
/* no stdin */
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function registerProjectHooks(ctx) {
|
|
85
|
+
const disposers = [];
|
|
86
|
+
const logger = ctx.logger;
|
|
87
|
+
|
|
88
|
+
async function runEvent(event, agent, payload, matchValue) {
|
|
89
|
+
const project = hookProject(sessionCwd(agent), payload?.file ?? payload?.path);
|
|
90
|
+
const loaded = loadHooks(project.root, project.dsh);
|
|
91
|
+
if (!loaded.trusted) return { blocked: false, results: [], untrusted: true };
|
|
92
|
+
const hooks = Array.isArray(loaded.hooks?.[event]) ? loaded.hooks[event] : [];
|
|
93
|
+
if (hooks.length === 0) return { blocked: false, results: [] };
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const hook of hooks) {
|
|
96
|
+
if (!hook || typeof hook.command !== "string" || hook.command.trim().length === 0) continue;
|
|
97
|
+
if (!matcherMatches(hook.matcher, matchValue)) continue;
|
|
98
|
+
const result = await runCommand(hook, { event, ...payload }, project.root, String(agent?.id ?? ""));
|
|
99
|
+
results.push({ ...result, command: hook.command });
|
|
100
|
+
const code = result.code;
|
|
101
|
+
if (code !== 0) {
|
|
102
|
+
logger?.warn?.(`[agent-extensions] ${event} hook exited ${code}: ${(result.stderr || result.stdout).slice(0, 300)}`);
|
|
103
|
+
}
|
|
104
|
+
if (code === 2) return { blocked: true, results, reason: (result.stderr || result.stdout || `${event} hook blocked`).trim() };
|
|
105
|
+
}
|
|
106
|
+
return { blocked: false, results };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const on = (event, handler) => {
|
|
110
|
+
try {
|
|
111
|
+
const dispose = ctx.on(event, handler);
|
|
112
|
+
if (typeof dispose === "function") disposers.push(dispose);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
logger?.warn?.(`[agent-extensions] ${event} hook skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
on("tools/pre-execute", async (exec, next) => {
|
|
119
|
+
const result = await runEvent("PreToolUse", exec?.agent, {
|
|
120
|
+
tool: exec?.name,
|
|
121
|
+
arguments: exec?.arguments ?? {},
|
|
122
|
+
}, exec?.name);
|
|
123
|
+
if (result.blocked) return { kind: "deny", reason: `project hook: ${result.reason}` };
|
|
124
|
+
return typeof next === "function" ? next() : { kind: "allow" };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
on("tools/result", async (exec, result) => {
|
|
128
|
+
await runEvent("PostToolUse", exec?.agent, {
|
|
129
|
+
tool: exec?.name,
|
|
130
|
+
arguments: exec?.arguments ?? {},
|
|
131
|
+
ok: result?.isError !== true,
|
|
132
|
+
}, exec?.name);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
on("fs/write-intent", async (target, actor, next) => {
|
|
136
|
+
const file = target?.displayPath ?? target?.path ?? "";
|
|
137
|
+
const result = await runEvent("FileChanged", actor, { file, operation: "write" }, file);
|
|
138
|
+
if (result.blocked) throw new Error(`project hook: ${result.reason}`);
|
|
139
|
+
return typeof next === "function" ? next() : undefined;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
on("fs/edit-intent", async (target, actor, next) => {
|
|
143
|
+
const file = target?.displayPath ?? target?.path ?? "";
|
|
144
|
+
const result = await runEvent("FileChanged", actor, { file, operation: "edit" }, file);
|
|
145
|
+
if (result.blocked) throw new Error(`project hook: ${result.reason}`);
|
|
146
|
+
return typeof next === "function" ? next() : undefined;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
on("agent/turn-stopping", async (payload) => {
|
|
150
|
+
const agent = payload?.agent;
|
|
151
|
+
const result = await runEvent("Stop", agent, { turn: payload?.turn ?? 0 }, "");
|
|
152
|
+
if (result.blocked) {
|
|
153
|
+
try {
|
|
154
|
+
agent?.steer?.({
|
|
155
|
+
id: `hook-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
156
|
+
role: "user",
|
|
157
|
+
content: [{ type: "text", text: `<project-hook-stop>\n${result.reason}\n</project-hook-stop>` }],
|
|
158
|
+
source: { kind: "user" },
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger?.warn?.(`[agent-extensions] stop steer failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
on("agent/created", async (payload) => {
|
|
167
|
+
await runEvent("SessionStart", payload?.agent, { session: String(payload?.agent?.id ?? "") }, "");
|
|
168
|
+
});
|
|
169
|
+
on("agent/disposed", async (payload) => {
|
|
170
|
+
await runEvent("SessionEnd", payload?.agent, { session: String(payload?.agent?.id ?? "") }, "");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
dispose() {
|
|
175
|
+
for (const dispose of disposers) {
|
|
176
|
+
try { dispose(); } catch { /* best effort */ }
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export { runCommand, matcherMatches };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforcement for project subagents.
|
|
3
|
+
*
|
|
4
|
+
* DSH's subagents service starts a child before a parent can attach a policy,
|
|
5
|
+
* so `project_agent` reserves the child id, records the policy, and only then
|
|
6
|
+
* starts the child. The global pre-execute hook denies calls made by that child
|
|
7
|
+
* when its tool allowlist or permission level forbids them.
|
|
8
|
+
*/
|
|
9
|
+
const MUTATING_TOOLS = new Set(["write", "edit", "apply_patch", "patch", "delete", "move", "mkdir", "touch"]);
|
|
10
|
+
const SHELL_TOOLS = new Set(["bash", "pwsh", "powershell", "shell", "sh", "zsh", "cmd", "terminal"]);
|
|
11
|
+
const MUTATING_SHELL = /(?:^|[;&|]\s*|\s)(?:rm|rmdir|mv|cp|mkdir|touch|truncate|tee|dd|chmod|chown|Set-Content|Add-Content|Out-File|New-Item|Remove-Item|Move-Item|Copy-Item|git\s+(?:commit|push|reset|checkout|clean|merge|rebase)|npm\s+(?:install|uninstall|update)|pnpm\s+(?:add|remove|install)|composer\s+(?:install|update)|php\s+artisan\s+(?:migrate|db:)|dropdb|createdb|psql\b[^\n]*(?:insert|update|delete|drop|truncate|alter))/iu;
|
|
12
|
+
|
|
13
|
+
const policies = new Map();
|
|
14
|
+
|
|
15
|
+
export function parseAllowedTools(value) {
|
|
16
|
+
if (typeof value !== "string") return new Set();
|
|
17
|
+
return new Set(value.split(/[,\s]+/u).map((name) => name.trim()).filter(Boolean));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function setChildPolicy(childId, definition) {
|
|
21
|
+
if (!childId) return;
|
|
22
|
+
policies.set(String(childId), {
|
|
23
|
+
name: definition?.name ?? "project-agent",
|
|
24
|
+
allowed: parseAllowedTools(definition?.tools),
|
|
25
|
+
permission: definition?.permission === "read-only" || definition?.permission === "workspace-write"
|
|
26
|
+
? definition.permission
|
|
27
|
+
: "full",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function clearChildPolicy(childId) {
|
|
32
|
+
if (childId) policies.delete(String(childId));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function checkChildTool(exec) {
|
|
36
|
+
const id = String(exec?.agent?.id ?? "");
|
|
37
|
+
const policy = policies.get(id);
|
|
38
|
+
if (!policy) return null;
|
|
39
|
+
const tool = String(exec?.name ?? "");
|
|
40
|
+
if (policy.permission === "read-only") {
|
|
41
|
+
if (MUTATING_TOOLS.has(tool)) {
|
|
42
|
+
return { kind: "deny", reason: `subagent ${policy.name}: permission read-only запрещает ${tool}` };
|
|
43
|
+
}
|
|
44
|
+
if (SHELL_TOOLS.has(tool)) {
|
|
45
|
+
const command = exec?.arguments && typeof exec.arguments === "object" ? exec.arguments.command : "";
|
|
46
|
+
if (typeof command === "string" && MUTATING_SHELL.test(command)) {
|
|
47
|
+
return { kind: "deny", reason: `subagent ${policy.name}: permission read-only запрещает изменяющую shell-команду` };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (policy.allowed.size > 0 && !policy.allowed.has(tool)) {
|
|
52
|
+
return { kind: "deny", reason: `subagent ${policy.name}: tool ${tool} не входит в allowlist` };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function registerChildPolicyHook(ctx) {
|
|
58
|
+
const disposers = [];
|
|
59
|
+
const on = (event, handler) => {
|
|
60
|
+
try {
|
|
61
|
+
const dispose = ctx.on(event, handler);
|
|
62
|
+
if (typeof dispose === "function") disposers.push(dispose);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
ctx.logger?.warn?.(`[agent-extensions] policy hook ${event} skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
on("tools/pre-execute", (exec, next) => {
|
|
68
|
+
const hit = checkChildTool(exec);
|
|
69
|
+
if (hit) return { kind: "deny", reason: hit.reason };
|
|
70
|
+
return typeof next === "function" ? next() : { kind: "allow" };
|
|
71
|
+
});
|
|
72
|
+
on("agent/disposed", (payload) => clearChildPolicy(payload?.agent?.id));
|
|
73
|
+
return {
|
|
74
|
+
dispose() {
|
|
75
|
+
for (const dispose of disposers) {
|
|
76
|
+
try { dispose(); } catch { /* best effort */ }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function childPolicyCount() {
|
|
83
|
+
return policies.size;
|
|
84
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project discovery for agent extensions.
|
|
3
|
+
*
|
|
4
|
+
* Everything is optional and project-local: `.dsh/hooks.json`,
|
|
5
|
+
* `.dsh/agents/*.md`, `.dsh/commands/*.md`, `.dsh/verify.json`. A project
|
|
6
|
+
* without `.dsh` simply gets no extra hooks/commands/agents.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
export const BUILTIN_COMMANDS = new Set(["init", "doctor", "verify", "hooks", "checkpoint", "rewind", "project"]);
|
|
12
|
+
|
|
13
|
+
export function findDshDir(start) {
|
|
14
|
+
let current = path.resolve(start || process.cwd());
|
|
15
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
16
|
+
const candidate = path.join(current, ".dsh");
|
|
17
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return { root: current, dsh: candidate };
|
|
18
|
+
const parent = path.dirname(current);
|
|
19
|
+
if (parent === current) break;
|
|
20
|
+
current = parent;
|
|
21
|
+
}
|
|
22
|
+
return { root: path.resolve(start || process.cwd()), dsh: null };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function projectFor(cwd) {
|
|
26
|
+
if (typeof cwd !== "string" || cwd.trim().length === 0) return { root: process.cwd(), dsh: null };
|
|
27
|
+
return findDshDir(cwd);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseFrontmatter(text) {
|
|
31
|
+
const source = String(text ?? "").replace(/^\uFEFF/u, "");
|
|
32
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u.exec(source);
|
|
33
|
+
if (!match) return { meta: {}, body: source.trim() };
|
|
34
|
+
const meta = {};
|
|
35
|
+
for (const line of match[1].split(/\r?\n/u)) {
|
|
36
|
+
const pair = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/u.exec(line.trim());
|
|
37
|
+
if (!pair) continue;
|
|
38
|
+
let value = pair[2].trim();
|
|
39
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
40
|
+
value = value.slice(1, -1);
|
|
41
|
+
}
|
|
42
|
+
if (value === "true") value = true;
|
|
43
|
+
else if (value === "false") value = false;
|
|
44
|
+
meta[pair[1]] = value;
|
|
45
|
+
}
|
|
46
|
+
return { meta, body: source.slice(match[0].length).trim() };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function listMarkdown(directory) {
|
|
50
|
+
try {
|
|
51
|
+
return fs
|
|
52
|
+
.readdirSync(directory, { withFileTypes: true })
|
|
53
|
+
.filter((entry) => entry.isFile() && /\.md$/iu.test(entry.name))
|
|
54
|
+
.map((entry) => entry.name)
|
|
55
|
+
.sort((left, right) => left.localeCompare(right));
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function loadAgents(projectDir, dshDir) {
|
|
62
|
+
const directory = dshDir ? path.join(dshDir, "agents") : null;
|
|
63
|
+
if (!directory || !fs.existsSync(directory)) return [];
|
|
64
|
+
const agents = [];
|
|
65
|
+
for (const name of listMarkdown(directory)) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = parseFrontmatter(fs.readFileSync(path.join(directory, name), "utf8"));
|
|
68
|
+
const id = String(parsed.meta.name || name.replace(/\.md$/iu, "")).trim();
|
|
69
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,48}$/iu.test(id)) continue;
|
|
70
|
+
if (!parsed.body) continue;
|
|
71
|
+
agents.push({
|
|
72
|
+
name: id,
|
|
73
|
+
description: typeof parsed.meta.description === "string" ? parsed.meta.description : "",
|
|
74
|
+
model: typeof parsed.meta.model === "string" ? parsed.meta.model : "",
|
|
75
|
+
tools: typeof parsed.meta.tools === "string" ? parsed.meta.tools : "",
|
|
76
|
+
permission: typeof parsed.meta.permission === "string" ? parsed.meta.permission : "full",
|
|
77
|
+
body: parsed.body,
|
|
78
|
+
file: path.join(directory, name),
|
|
79
|
+
});
|
|
80
|
+
} catch {
|
|
81
|
+
/* unreadable agent */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return agents;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function loadCommands(projectDir, dshDir) {
|
|
88
|
+
const directory = dshDir ? path.join(dshDir, "commands") : null;
|
|
89
|
+
if (!directory || !fs.existsSync(directory)) return [];
|
|
90
|
+
const commands = [];
|
|
91
|
+
for (const name of listMarkdown(directory)) {
|
|
92
|
+
const id = name.replace(/\.md$/iu, "").trim().toLowerCase();
|
|
93
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,40}$/u.test(id) || BUILTIN_COMMANDS.has(id)) continue;
|
|
94
|
+
try {
|
|
95
|
+
const parsed = parseFrontmatter(fs.readFileSync(path.join(directory, name), "utf8"));
|
|
96
|
+
if (!parsed.body) continue;
|
|
97
|
+
commands.push({
|
|
98
|
+
name: id,
|
|
99
|
+
description: typeof parsed.meta.description === "string" ? parsed.meta.description : `Проектная команда /${id}`,
|
|
100
|
+
body: parsed.body,
|
|
101
|
+
file: path.join(directory, name),
|
|
102
|
+
});
|
|
103
|
+
} catch {
|
|
104
|
+
/* unreadable command */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return commands;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function loadHooks(projectDir, dshDir) {
|
|
111
|
+
const file = dshDir ? path.join(dshDir, "hooks.json") : null;
|
|
112
|
+
if (!file || !fs.existsSync(file)) return { hooks: {}, trusted: false, file };
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
116
|
+
} catch {
|
|
117
|
+
return { hooks: {}, trusted: false, file, error: "hooks.json: invalid JSON" };
|
|
118
|
+
}
|
|
119
|
+
const hooks = parsed && typeof parsed.hooks === "object" && parsed.hooks !== null ? parsed.hooks : {};
|
|
120
|
+
const trusted = Boolean(dshDir && fs.existsSync(path.join(dshDir, "hooks.trusted")));
|
|
121
|
+
return { hooks, trusted, file };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function loadVerify(projectDir, dshDir) {
|
|
125
|
+
const file = dshDir ? path.join(dshDir, "verify.json") : null;
|
|
126
|
+
if (file && fs.existsSync(file)) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
129
|
+
if (Array.isArray(parsed?.commands)) {
|
|
130
|
+
return { commands: parsed.commands.filter((item) => typeof item === "string" && item.trim()).slice(0, 10), source: file };
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
/* fall through */
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { commands: [], source: null };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function projectSummary(projectDir) {
|
|
140
|
+
const agents = loadAgents(projectDir, path.join(projectDir, ".dsh"));
|
|
141
|
+
const commands = loadCommands(projectDir, path.join(projectDir, ".dsh"));
|
|
142
|
+
const hooks = loadHooks(projectDir, path.join(projectDir, ".dsh"));
|
|
143
|
+
return { agents, commands, hooks };
|
|
144
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-config scaffolding for dsh-desktop-kit.
|
|
3
|
+
*
|
|
4
|
+
* Global: `~/.dsh/mcp.json` (empty, valid).
|
|
5
|
+
* Project: `<project>/.dsh/` with agents/, commands/, hooks.json, verify.json,
|
|
6
|
+
* mcp.json, PROJECT.md and rules/project.md.
|
|
7
|
+
*
|
|
8
|
+
* Trust markers are deliberately NOT created: hooks and project MCP servers
|
|
9
|
+
* must remain an explicit user decision.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
const SCAFFOLD_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
function writeIfMissing(file, content) {
|
|
18
|
+
if (fs.existsSync(file)) return false;
|
|
19
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
20
|
+
fs.writeFileSync(file, content, "utf8");
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ensureGlobalScaffold(home = process.env.DSH_HOME || path.join(os.homedir(), ".dsh")) {
|
|
25
|
+
const created = [];
|
|
26
|
+
const mcpFile = path.join(home, "mcp.json");
|
|
27
|
+
if (writeIfMissing(mcpFile, `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`)) created.push(mcpFile);
|
|
28
|
+
const readme = path.join(home, "mcp.README.md");
|
|
29
|
+
if (
|
|
30
|
+
writeIfMissing(
|
|
31
|
+
readme,
|
|
32
|
+
[
|
|
33
|
+
"# MCP servers",
|
|
34
|
+
"",
|
|
35
|
+
"Add trusted servers to `mcp.json`:",
|
|
36
|
+
"",
|
|
37
|
+
"```json",
|
|
38
|
+
'{ "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }, "trusted": true } } }',
|
|
39
|
+
"```",
|
|
40
|
+
"",
|
|
41
|
+
"A server is started only with `trusted: true`. Project-level servers additionally require `.dsh/mcp.trusted` (`/mcp trust`).",
|
|
42
|
+
"",
|
|
43
|
+
].join("\n"),
|
|
44
|
+
)
|
|
45
|
+
) {
|
|
46
|
+
created.push(readme);
|
|
47
|
+
}
|
|
48
|
+
return { home, created };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isKitRepo(cwd) {
|
|
52
|
+
try {
|
|
53
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
54
|
+
return pkg?.name === "dsh-desktop-kit";
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function shouldScaffold(cwd) {
|
|
61
|
+
if (typeof cwd !== "string" || cwd.length === 0) return false;
|
|
62
|
+
let resolved;
|
|
63
|
+
try { resolved = fs.realpathSync(cwd); } catch { return false; }
|
|
64
|
+
if (!fs.statSync(resolved).isDirectory()) return false;
|
|
65
|
+
const home = path.resolve(os.homedir());
|
|
66
|
+
const root = path.parse(resolved).root;
|
|
67
|
+
if (resolved === home || resolved === root) return false;
|
|
68
|
+
if (resolved.startsWith(path.join(home, ".dsh"))) return false;
|
|
69
|
+
if (isKitRepo(resolved)) return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function ensureProjectScaffold(cwd) {
|
|
74
|
+
if (!shouldScaffold(cwd)) return { skipped: true, created: [] };
|
|
75
|
+
const dsh = path.join(cwd, ".dsh");
|
|
76
|
+
const created = [];
|
|
77
|
+
fs.mkdirSync(dsh, { recursive: true });
|
|
78
|
+
for (const directory of ["agents", "commands", "rules"]) {
|
|
79
|
+
const target = path.join(dsh, directory);
|
|
80
|
+
if (!fs.existsSync(target)) {
|
|
81
|
+
fs.mkdirSync(target, { recursive: true });
|
|
82
|
+
created.push(target);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (writeIfMissing(path.join(dsh, "agents", ".gitkeep"), "")) created.push(path.join(dsh, "agents", ".gitkeep"));
|
|
86
|
+
if (writeIfMissing(path.join(dsh, "commands", ".gitkeep"), "")) created.push(path.join(dsh, "commands", ".gitkeep"));
|
|
87
|
+
if (writeIfMissing(path.join(dsh, "hooks.json"), `${JSON.stringify({ hooks: {} }, null, 2)}\n`)) created.push(path.join(dsh, "hooks.json"));
|
|
88
|
+
if (writeIfMissing(path.join(dsh, "verify.json"), `${JSON.stringify({ commands: [] }, null, 2)}\n`)) created.push(path.join(dsh, "verify.json"));
|
|
89
|
+
if (writeIfMissing(path.join(dsh, "mcp.json"), `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`)) created.push(path.join(dsh, "mcp.json"));
|
|
90
|
+
if (
|
|
91
|
+
writeIfMissing(
|
|
92
|
+
path.join(dsh, ".gitignore"),
|
|
93
|
+
["hooks.trusted", "mcp.trusted", "checkpoints/", ".kit-scaffold.json", ""].join("\n"),
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
created.push(path.join(dsh, ".gitignore"));
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
writeIfMissing(
|
|
100
|
+
path.join(dsh, "README.md"),
|
|
101
|
+
[
|
|
102
|
+
"# .dsh",
|
|
103
|
+
"",
|
|
104
|
+
"Project-local DSH extensions, created automatically by dsh-desktop-kit.",
|
|
105
|
+
"",
|
|
106
|
+
"- `agents/*.md` — custom subagents (`project_agent`).",
|
|
107
|
+
"- `commands/*.md` — project commands (`/project <name>`).",
|
|
108
|
+
"- `hooks.json` — lifecycle hooks; run `/hooks trust` after review.",
|
|
109
|
+
"- `verify.json` — commands for `/verify`.",
|
|
110
|
+
"- `mcp.json` — project MCP servers; run `/mcp trust` after review.",
|
|
111
|
+
"- `PROJECT.md` — project context; `/init --force` regenerates it.",
|
|
112
|
+
"",
|
|
113
|
+
].join("\n"),
|
|
114
|
+
)
|
|
115
|
+
) {
|
|
116
|
+
created.push(path.join(dsh, "README.md"));
|
|
117
|
+
}
|
|
118
|
+
const projectFile = path.join(dsh, "PROJECT.md");
|
|
119
|
+
if (!fs.existsSync(projectFile)) {
|
|
120
|
+
let pkg = {};
|
|
121
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")); } catch { /* optional */ }
|
|
122
|
+
fs.writeFileSync(
|
|
123
|
+
projectFile,
|
|
124
|
+
[
|
|
125
|
+
"# Project",
|
|
126
|
+
"",
|
|
127
|
+
`Root: ${cwd}`,
|
|
128
|
+
pkg.name ? `Package: ${pkg.name}${pkg.version ? ` ${pkg.version}` : ""}` : "",
|
|
129
|
+
pkg.scripts?.test ? `Tests: ${pkg.scripts.test}` : "",
|
|
130
|
+
"",
|
|
131
|
+
"Created automatically by dsh-desktop-kit. Edit freely; `/init --force` regenerates.",
|
|
132
|
+
"",
|
|
133
|
+
]
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.join("\n"),
|
|
136
|
+
"utf8",
|
|
137
|
+
);
|
|
138
|
+
created.push(projectFile);
|
|
139
|
+
}
|
|
140
|
+
const ruleFile = path.join(dsh, "rules", "project.md");
|
|
141
|
+
if (!fs.existsSync(ruleFile)) {
|
|
142
|
+
fs.writeFileSync(ruleFile, "---\nalways: true\n---\n# Правила проекта\n\n- Источник истины: `.dsh/PROJECT.md`.\n- Перед сдачей — тесты и проверка UI.\n", "utf8");
|
|
143
|
+
created.push(ruleFile);
|
|
144
|
+
}
|
|
145
|
+
const marker = path.join(dsh, ".kit-scaffold.json");
|
|
146
|
+
if (!fs.existsSync(marker)) {
|
|
147
|
+
fs.writeFileSync(marker, `${JSON.stringify({ version: SCAFFOLD_VERSION, createdAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
148
|
+
created.push(marker);
|
|
149
|
+
}
|
|
150
|
+
return { skipped: false, created, dsh };
|
|
151
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-agent-extensions",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"./client": {
|
|
12
|
+
"default": "./client.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": {
|
|
15
|
+
"default": "./package.json"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"description": "Project-local hooks, custom agents, commands and safe checkpoints for DeepSeek Harness — Claude Code-like extensibility.",
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|
|
Binary file
|
|
Binary file
|