@dotdotgod/claude-code 0.3.1 → 0.3.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/README.md +3 -1
- package/hooks/runtime.mjs +143 -2
- package/mcp/cli.mjs +5382 -0
- package/mcp/server.mjs +23512 -3
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ Then use the bundled `/dd:*` commands in your repository:
|
|
|
34
34
|
/dd:impact
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
After an update, restart Claude Code and confirm the active plugin points to the new installed cache entry. Source changes do not rewrite an existing `~/.claude/plugins/cache/.../<hash>/`; reinstall the plugin through the supported flow if Claude continues using an older cache hash.
|
|
38
|
+
|
|
37
39
|
For local development from a source checkout, load the plugin directory directly instead of installing:
|
|
38
40
|
|
|
39
41
|
```bash
|
|
@@ -45,7 +47,7 @@ claude --plugin-dir /path/to/dotdotgod/packages/claude-code
|
|
|
45
47
|
| Command or skill | Use it for | Result |
|
|
46
48
|
| --- | --- | --- |
|
|
47
49
|
| `/dd:init` | Start a repository with dotdotgod conventions. | Creates or normalizes shared agent files, docs indexes, local-memory areas, and the complete default project config. |
|
|
48
|
-
| `/dd:load` | Load project memory without changing maintained project files. | Renders the shared Markdown tree and uses `dotdotgod query` when focus text is provided;
|
|
50
|
+
| `/dd:load` | Load project memory without changing maintained project files. | Renders the shared Markdown tree and uses `dotdotgod query` when focus text is provided; unavailable optional semantic routing falls back to the map with bounded evidence. |
|
|
49
51
|
| `/dd:plan` | Plan before implementation. | Writes or updates durable task intent in `docs/plan/<task-slug>/README.md`. |
|
|
50
52
|
| `/dd:impact` | Review changed files before verification or handoff. | Uses `dotdotgod graph impact` to identify likely related docs, tests, commands, and source files. |
|
|
51
53
|
| `document-clarify` | Improve docs wording without changing behavior contracts. | Clarifies README/spec/test/arch/plan/archive docs using memory-area roles. |
|
package/hooks/runtime.mjs
CHANGED
|
@@ -1,3 +1,144 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
// Generated by scripts/build-adapter-runtime.mjs; do not edit.
|
|
3
|
+
|
|
4
|
+
// packages/context/src/hooks.mjs
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
8
|
+
function inputText() {
|
|
9
|
+
return readFileSync(0, "utf8");
|
|
10
|
+
}
|
|
11
|
+
function parseInput() {
|
|
12
|
+
const text = inputText().trim();
|
|
13
|
+
return text ? JSON.parse(text) : {};
|
|
14
|
+
}
|
|
15
|
+
function sessionId(input) {
|
|
16
|
+
return String(input.session_id || input.sessionId || "default").replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
17
|
+
}
|
|
18
|
+
function runtimeStatePath(root, id) {
|
|
19
|
+
return join(root, ".dotdotgod", "context", "runtime", `${id}.json`);
|
|
20
|
+
}
|
|
21
|
+
function declaredProjectRoot(input) {
|
|
22
|
+
const value = input.project_dir || process.env.CLAUDE_PROJECT_DIR;
|
|
23
|
+
return typeof value === "string" && value.trim() ? resolve(value) : null;
|
|
24
|
+
}
|
|
25
|
+
function findExistingStateRoot(cwd, id) {
|
|
26
|
+
let current = resolve(cwd);
|
|
27
|
+
const filesystemRoot = parse(current).root;
|
|
28
|
+
while (true) {
|
|
29
|
+
if (existsSync(runtimeStatePath(current, id))) return current;
|
|
30
|
+
if (current === filesystemRoot) return null;
|
|
31
|
+
current = dirname(current);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function resolveHookContext(input, event) {
|
|
35
|
+
const id = sessionId(input);
|
|
36
|
+
const declared = declaredProjectRoot(input);
|
|
37
|
+
if (declared) return { root: declared, path: runtimeStatePath(declared, id) };
|
|
38
|
+
if (typeof input.cwd !== "string" || !input.cwd.trim()) return null;
|
|
39
|
+
const existing = findExistingStateRoot(input.cwd, id);
|
|
40
|
+
if (existing) return { root: existing, path: runtimeStatePath(existing, id) };
|
|
41
|
+
if (event !== "sessionstart") return null;
|
|
42
|
+
const root = resolve(input.cwd);
|
|
43
|
+
return { root, path: runtimeStatePath(root, id) };
|
|
44
|
+
}
|
|
45
|
+
function readState(path) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
48
|
+
} catch {
|
|
49
|
+
return { loadRequired: false, pending: {} };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function writeState(path, state) {
|
|
53
|
+
if (!path) return;
|
|
54
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
55
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
56
|
+
writeFileSync(temp, `${JSON.stringify(state, null, 2)}
|
|
57
|
+
`);
|
|
58
|
+
renameSync(temp, path);
|
|
59
|
+
}
|
|
60
|
+
function fileHash(path) {
|
|
61
|
+
try {
|
|
62
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
63
|
+
} catch {
|
|
64
|
+
return "missing";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function toolName(input) {
|
|
68
|
+
return String(input.tool_name || input.toolName || "");
|
|
69
|
+
}
|
|
70
|
+
function toolInput(input) {
|
|
71
|
+
return input.tool_input || input.toolInput || {};
|
|
72
|
+
}
|
|
73
|
+
function mcpTool(name, suffix) {
|
|
74
|
+
return name === suffix || name.endsWith(`__${suffix}`) || name.endsWith(`_${suffix}`);
|
|
75
|
+
}
|
|
76
|
+
function deny(reason) {
|
|
77
|
+
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } };
|
|
78
|
+
}
|
|
79
|
+
function commandFrom(input) {
|
|
80
|
+
const value = toolInput(input);
|
|
81
|
+
return String(value.command || value.cmd || "");
|
|
82
|
+
}
|
|
83
|
+
function isSubstantive(name) {
|
|
84
|
+
return /^(Bash|Shell|local_shell|shell|shell_command|exec_command|Edit|Write|apply_patch)$/i.test(name);
|
|
85
|
+
}
|
|
86
|
+
function isImpactGate(name, input) {
|
|
87
|
+
if (!/^(Bash|Shell|local_shell|shell|shell_command|exec_command)$/i.test(name)) return false;
|
|
88
|
+
return /(^|\s)(test|build|lint|verify|commit|push|publish|deploy)(\s|:|$)|\b(git\s+(commit|push)|npm\s+(test|publish)|pnpm\s+[^\n]*(test|build|lint|verify|publish))\b/i.test(commandFrom(input));
|
|
89
|
+
}
|
|
90
|
+
function changedPath(input, root) {
|
|
91
|
+
const value = toolInput(input);
|
|
92
|
+
const path = value.file_path || value.path;
|
|
93
|
+
return root && typeof path === "string" ? resolve(root, path) : null;
|
|
94
|
+
}
|
|
95
|
+
function runHook(event) {
|
|
96
|
+
const input = parseInput();
|
|
97
|
+
const name = toolName(input);
|
|
98
|
+
const context = resolveHookContext(input, event);
|
|
99
|
+
const state = readState(context?.path);
|
|
100
|
+
state.pending ||= {};
|
|
101
|
+
if (event === "sessionstart") {
|
|
102
|
+
if (!context) return {};
|
|
103
|
+
state.loadRequired = true;
|
|
104
|
+
writeState(context.path, state);
|
|
105
|
+
return { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: "Dotdotgod project memory has not been loaded for this session. Before substantive shell or write work, call dotdotgod_project_load once with a concise task-specific focus." } };
|
|
106
|
+
}
|
|
107
|
+
if (event === "pretooluse") {
|
|
108
|
+
if (mcpTool(name, "dotdotgod_project_load") || mcpTool(name, "dotdotgod_project_impact")) return {};
|
|
109
|
+
if (state.loadRequired && isSubstantive(name)) return deny("Project memory is required first. Call dotdotgod_project_load with the current project root and a concise task-specific focus, then retry this tool.");
|
|
110
|
+
const pendingPaths = Object.keys(state.pending);
|
|
111
|
+
if (pendingPaths.length && isImpactGate(name, input)) return deny(`Graph impact is pending. Call dotdotgod_project_impact with paths=${JSON.stringify(pendingPaths)} and then retry this tool.`);
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
if (event === "posttooluse") {
|
|
115
|
+
if (!context) return {};
|
|
116
|
+
if (mcpTool(name, "dotdotgod_project_load")) state.loadRequired = false;
|
|
117
|
+
else if (mcpTool(name, "dotdotgod_project_impact")) {
|
|
118
|
+
const checked = toolInput(input).paths;
|
|
119
|
+
if (Array.isArray(checked)) for (const path of checked) {
|
|
120
|
+
const absolute = resolve(context.root, path);
|
|
121
|
+
if (state.pending[absolute] === fileHash(absolute)) delete state.pending[absolute];
|
|
122
|
+
}
|
|
123
|
+
} else if (/^(Edit|Write|apply_patch)$/i.test(name)) {
|
|
124
|
+
const path = changedPath(input, context.root);
|
|
125
|
+
if (path) state.pending[path] = fileHash(path);
|
|
126
|
+
}
|
|
127
|
+
writeState(context.path, state);
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
function hookMain(event) {
|
|
133
|
+
try {
|
|
134
|
+
process.stdout.write(`${JSON.stringify(runHook(event))}
|
|
135
|
+
`);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
process.stderr.write(`dotdotgod hook error: ${error instanceof Error ? error.message : String(error)}
|
|
138
|
+
`);
|
|
139
|
+
process.stdout.write("{}\n");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// packages/claude-code/hooks/runtime-entry.mjs
|
|
144
|
+
hookMain(process.argv[2] || "");
|