@michelj/context-guard 0.4.4 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Coordinator.md +88 -0
- package/Executor.md +53 -0
- package/README.md +72 -102
- package/README.zh-CN.md +72 -102
- package/SKILL.md +26 -33
- package/THIRD_PARTY_NOTICES.md +47 -0
- package/Tester.md +53 -0
- package/bin/build-runtime.mjs +96 -0
- package/bin/context-guard-skill.js +287 -69
- package/bin/postinstall.js +1 -1
- package/hooks.json +80 -4
- package/licenses/JSONParse-MIT.txt +24 -0
- package/licenses/Marked-MIT.txt +44 -0
- package/licenses/Portless-Apache-2.0.txt +201 -0
- package/package.json +31 -5
- package/prototype/LICENSES/Marked-MIT.txt +44 -0
- package/prototype/LICENSES/Ready-redistribution.txt +14 -0
- package/prototype/attachments.mjs +75 -0
- package/prototype/coordinator-markdown.mjs +283 -0
- package/prototype/coordinator-working-blot.mjs +124 -0
- package/prototype/vendor/marked.mjs +2189 -0
- package/prototype/workbench-app.js +5197 -0
- package/prototype/workbench-data.js +33 -0
- package/prototype/workbench-sync.mjs +898 -0
- package/prototype/workbench.css +1050 -0
- package/prototype/workbench.html +139 -4861
- package/prototype/working-blot-atlas.png +0 -0
- package/references/agent-handoff.md +40 -0
- package/references/claude-runtime.md +120 -0
- package/references/cloud-sync-interface.md +66 -0
- package/references/design-current.md +14 -0
- package/references/map-mount.md +41 -0
- package/references/map-read.md +50 -0
- package/references/memory-definition.md +120 -0
- package/references/memory-filesystem-v2/Bug.en.md +162 -0
- package/references/memory-filesystem-v2/Bug.md +162 -0
- package/references/memory-filesystem-v2/Bug_Coordinater.md +8 -0
- package/references/memory-filesystem-v2/Bug_Executor.md +8 -0
- package/references/memory-filesystem-v2/Bug_Tester.md +7 -0
- package/references/memory-filesystem-v2/Idea.en.md +36 -0
- package/references/memory-filesystem-v2/Idea.md +36 -0
- package/references/memory-filesystem-v2/Node_Module_Index.en.md +88 -0
- package/references/memory-filesystem-v2/Node_Module_Index.md +88 -0
- package/references/memory-filesystem-v2/README.md +60 -0
- package/references/memory-filesystem-v2/Todo.en.md +137 -0
- package/references/memory-filesystem-v2/Todo.md +137 -0
- package/references/memory-filesystem-v2/Todo_Coordinater.md +7 -0
- package/references/memory-filesystem-v2/Todo_Executor.md +7 -0
- package/references/memory-filesystem-v2/Todo_Tester.md +7 -0
- package/references/named-workbench.md +124 -0
- package/references/plan-review.md +12 -0
- package/references/server-memory.md +276 -0
- package/references/test-check.md +7 -0
- package/references/user-reply.md +38 -0
- package/references/workbench-interface.md +531 -0
- package/roles.md +13 -0
- package/scripts/context_guard.py +1163 -321
- package/scripts/context_guard_hook.py +1864 -63
- package/scripts/map_owns.py +68 -138
- package/scripts/shared/LICENSES/JSONParse-MIT.txt +24 -0
- package/scripts/shared/filesystem-v2.mjs +430 -0
- package/scripts/shared/io.mjs +117 -0
- package/scripts/shared/map-model.mjs +506 -0
- package/scripts/shared/memory-schema.mjs +13 -0
- package/scripts/shared/protocol-blobs.mjs +112 -0
- package/scripts/shared/protocol-map.mjs +146 -0
- package/scripts/shared/protocol-snapshots.mjs +84 -0
- package/scripts/shared/protocol-store.mjs +624 -0
- package/scripts/shared/protocol-workflow.mjs +226 -0
- package/scripts/shared/protocol.mjs +125 -0
- package/scripts/shared/vendor/jsonparse.cjs +413 -0
- package/scripts/workbench/access.mjs +496 -0
- package/scripts/workbench/attachments.mjs +92 -0
- package/scripts/workbench/browser-login.mjs +78 -0
- package/scripts/workbench/claude-runtime.mjs +372 -0
- package/scripts/workbench/cli.mjs +980 -0
- package/scripts/workbench/device-heartbeat.mjs +72 -0
- package/scripts/workbench/hook-status.mjs +38 -0
- package/scripts/workbench/inbox.mjs +155 -0
- package/scripts/workbench/journal.mjs +56 -0
- package/scripts/workbench/memory-merge.mjs +65 -0
- package/scripts/workbench/memory.mjs +252 -0
- package/scripts/workbench/named-proxy.mjs +108 -0
- package/scripts/workbench/named.mjs +152 -0
- package/scripts/workbench/portless-routes.mjs +51 -0
- package/scripts/workbench/project.mjs +327 -0
- package/scripts/workbench/projections.mjs +68 -0
- package/scripts/workbench/protocol-client.mjs +165 -0
- package/scripts/workbench/protocol-delivery.mjs +133 -0
- package/scripts/workbench/protocol-device.mjs +316 -0
- package/scripts/workbench/protocol-events.mjs +53 -0
- package/scripts/workbench/protocol-repository.mjs +58 -0
- package/scripts/workbench/reconcile.mjs +244 -0
- package/scripts/workbench/registry.mjs +111 -0
- package/scripts/workbench/runtime.mjs +54 -0
- package/scripts/workbench/server.mjs +1171 -0
- package/scripts/workbench/store.mjs +243 -0
- package/scripts/workbench/sync-coordinator.mjs +518 -0
- package/scripts/workbench/sync.mjs +86 -0
- package/references/bug-record-template.md +0 -37
- package/references/context-template.md +0 -19
|
@@ -12,8 +12,15 @@ const sourceHooksPath = path.join(packageRoot, "hooks.json");
|
|
|
12
12
|
const pythonScript = path.join(sourceSkillDir, "scripts", "context_guard.py");
|
|
13
13
|
const skillInstallEntries = [
|
|
14
14
|
"SKILL.md",
|
|
15
|
+
"roles.md",
|
|
16
|
+
"Coordinator.md",
|
|
17
|
+
"Executor.md",
|
|
18
|
+
"Tester.md",
|
|
15
19
|
"README.md",
|
|
16
20
|
"README.zh-CN.md",
|
|
21
|
+
"THIRD_PARTY_NOTICES.md",
|
|
22
|
+
"licenses",
|
|
23
|
+
"bin",
|
|
17
24
|
"agents",
|
|
18
25
|
"prototype",
|
|
19
26
|
"references",
|
|
@@ -26,7 +33,10 @@ function usage() {
|
|
|
26
33
|
Usage:
|
|
27
34
|
context-guard install [--platform auto|all|codex|cursor|claude] [--no-hooks]
|
|
28
35
|
[--target <dir>] [--hooks-target <file>] [--config-target <file>]
|
|
36
|
+
context-guard doctor [--platform auto|all|codex|cursor|claude] [--root <project>]
|
|
37
|
+
[--target <dir>] [--hooks-target <file>] [--config-target <file>] [--json]
|
|
29
38
|
context-guard path
|
|
39
|
+
context-guard sync ensure|status|pull|prepare|checkpoint|finish [args...]
|
|
30
40
|
context-guard <context_guard.py command> [args...]
|
|
31
41
|
|
|
32
42
|
Examples:
|
|
@@ -51,12 +61,11 @@ function expandHome(inputPath) {
|
|
|
51
61
|
const PLATFORM_SPECS = {
|
|
52
62
|
codex: { env: "CODEX_HOME", folder: ".codex", hooksFile: "hooks.json", configFile: "config.toml" },
|
|
53
63
|
cursor: { env: "CURSOR_HOME", folder: ".cursor", hooksFile: "hooks.json" },
|
|
54
|
-
claude: { env: "CLAUDE_HOME", folder: ".claude", hooksFile: "settings.json" }
|
|
64
|
+
claude: { env: "CLAUDE_CONFIG_DIR", legacyEnv: "CLAUDE_HOME", folder: ".claude", hooksFile: "settings.json" }
|
|
55
65
|
};
|
|
56
66
|
|
|
57
67
|
function platformHome(platform) {
|
|
58
|
-
|
|
59
|
-
return path.resolve(expandHome(process.env[spec.env] || path.join(os.homedir(), spec.folder)));
|
|
68
|
+
return platformHomeBySpec(PLATFORM_SPECS[platform]);
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
function platformTargets(platform) {
|
|
@@ -70,17 +79,32 @@ function platformTargets(platform) {
|
|
|
70
79
|
};
|
|
71
80
|
}
|
|
72
81
|
|
|
82
|
+
let detectedPython;
|
|
83
|
+
function pythonCommand() {
|
|
84
|
+
if (detectedPython !== undefined) return detectedPython;
|
|
85
|
+
const candidates = process.platform === "win32" ? ["python", "python3"] : ["python3", "python"];
|
|
86
|
+
detectedPython = null;
|
|
87
|
+
for (const command of candidates) {
|
|
88
|
+
const probe = spawnSync(command, ["--version"], { encoding: "utf8", windowsHide: true, timeout: 5000 });
|
|
89
|
+
if (!probe.error && probe.status === 0 && /^Python 3\./m.test(`${probe.stdout || ""}\n${probe.stderr || ""}`)) {
|
|
90
|
+
detectedPython = command;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return detectedPython;
|
|
95
|
+
}
|
|
96
|
+
|
|
73
97
|
function selectedPlatforms(requested) {
|
|
74
98
|
if (requested === "all") return Object.keys(PLATFORM_SPECS);
|
|
75
99
|
if (requested !== "auto") return [requested];
|
|
76
100
|
const detected = Object.entries(PLATFORM_SPECS)
|
|
77
|
-
.filter(([, spec]) => Boolean(process.env[spec.env]) || fs.existsSync(platformHomeBySpec(spec)))
|
|
101
|
+
.filter(([, spec]) => Boolean(process.env[spec.env] || process.env[spec.legacyEnv]) || fs.existsSync(platformHomeBySpec(spec)))
|
|
78
102
|
.map(([name]) => name);
|
|
79
103
|
return detected.length ? detected : ["codex"];
|
|
80
104
|
}
|
|
81
105
|
|
|
82
106
|
function platformHomeBySpec(spec) {
|
|
83
|
-
return path.resolve(expandHome(process.env[spec.env] || path.join(os.homedir(), spec.folder)));
|
|
107
|
+
return path.resolve(expandHome(process.env[spec.env] || process.env[spec.legacyEnv] || path.join(os.homedir(), spec.folder)));
|
|
84
108
|
}
|
|
85
109
|
|
|
86
110
|
function parseInstallArgs(args) {
|
|
@@ -147,7 +171,7 @@ function parseInstallArgs(args) {
|
|
|
147
171
|
return options;
|
|
148
172
|
}
|
|
149
173
|
|
|
150
|
-
function
|
|
174
|
+
function migratedHooksFeatureConfig(configTarget) {
|
|
151
175
|
const original = fs.existsSync(configTarget) ? fs.readFileSync(configTarget, "utf8") : "";
|
|
152
176
|
const lines = original ? original.replace(/\r\n/g, "\n").split("\n") : [];
|
|
153
177
|
const sectionStart = lines.findIndex((line) => /^\s*\[features\]\s*(?:#.*)?$/.test(line));
|
|
@@ -186,46 +210,48 @@ function migrateHooksFeatureConfig(configTarget, dryRun) {
|
|
|
186
210
|
}
|
|
187
211
|
|
|
188
212
|
const next = `${nextLines.join("\n").replace(/\n*$/, "")}\n`;
|
|
189
|
-
|
|
190
|
-
if (dryRun) {
|
|
191
|
-
console.log(`[context-guard-skill] would enable hooks in ${configTarget}`);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
fs.mkdirSync(path.dirname(configTarget), { recursive: true });
|
|
195
|
-
if (fs.existsSync(configTarget)) {
|
|
196
|
-
const backupPath = `${configTarget}.bak-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
197
|
-
fs.copyFileSync(configTarget, backupPath);
|
|
198
|
-
console.log(`[context-guard-skill] backed up config: ${backupPath}`);
|
|
199
|
-
}
|
|
200
|
-
fs.writeFileSync(configTarget, next);
|
|
201
|
-
console.log(`[context-guard-skill] enabled hooks: ${configTarget}`);
|
|
213
|
+
return next === original.replace(/\r\n/g, "\n") ? null : next;
|
|
202
214
|
}
|
|
203
215
|
|
|
204
|
-
function copySkill(target
|
|
216
|
+
function copySkill(target) {
|
|
205
217
|
if (!fs.existsSync(path.join(sourceSkillDir, "SKILL.md"))) {
|
|
206
|
-
|
|
218
|
+
throw new Error(`source skill folder is missing: ${sourceSkillDir}`);
|
|
207
219
|
}
|
|
208
|
-
if (dryRun) {
|
|
209
|
-
console.log(`[context-guard-skill] would install skill to ${target}`);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
213
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
214
220
|
fs.mkdirSync(target, { recursive: true });
|
|
215
221
|
for (const entry of skillInstallEntries) {
|
|
216
222
|
const from = path.join(sourceSkillDir, entry);
|
|
217
223
|
if (!fs.existsSync(from)) continue;
|
|
218
224
|
const to = path.join(target, entry);
|
|
219
|
-
|
|
225
|
+
const options = entry === "scripts"
|
|
226
|
+
? {
|
|
227
|
+
recursive: true,
|
|
228
|
+
filter(source) {
|
|
229
|
+
// Node 18 may pass a namespaced Windows path to this callback.
|
|
230
|
+
const relative = path.relative(path.toNamespacedPath(from), path.toNamespacedPath(source));
|
|
231
|
+
if (relative === "") return true;
|
|
232
|
+
const parts = relative.split(path.sep);
|
|
233
|
+
return parts[0] !== "cloud"
|
|
234
|
+
&& relative !== "branch_guard.py"
|
|
235
|
+
&& !parts.includes("__pycache__")
|
|
236
|
+
&& !/\.py[co]$/i.test(parts.at(-1));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
: entry === "bin"
|
|
240
|
+
? { recursive: true, filter(source) {
|
|
241
|
+
const relative = path.relative(path.toNamespacedPath(from), path.toNamespacedPath(source));
|
|
242
|
+
return relative === "" || relative === "context-guard-skill.js";
|
|
243
|
+
} }
|
|
244
|
+
: { recursive: true };
|
|
245
|
+
fs.cpSync(from, to, options);
|
|
220
246
|
}
|
|
221
|
-
console.log(`[context-guard-skill] installed skill: ${target}`);
|
|
222
247
|
}
|
|
223
248
|
|
|
224
249
|
function hookCommand(skillTarget, event, platform) {
|
|
225
250
|
const hookScript = path.join(skillTarget, "scripts", "context_guard_hook.py");
|
|
226
251
|
const encodedHookScript = JSON.stringify(hookScript);
|
|
227
|
-
const
|
|
228
|
-
|
|
252
|
+
const python = pythonCommand();
|
|
253
|
+
if (!python) throw new Error("Python 3 is required before lifecycle hooks can be installed");
|
|
254
|
+
return `${python} ${encodedHookScript} ${event} --platform ${platform}`;
|
|
229
255
|
}
|
|
230
256
|
|
|
231
257
|
function rewriteGroupedHookCommands(hooksConfig, skillTarget, platform) {
|
|
@@ -257,7 +283,7 @@ function cursorHooks(skillTarget) {
|
|
|
257
283
|
hooks[cursorEvent] = [{
|
|
258
284
|
type: "command",
|
|
259
285
|
command: hookCommand(skillTarget, normalizedEvent, "cursor"),
|
|
260
|
-
timeout: 10
|
|
286
|
+
timeout: normalizedEvent === "session-start" ? 20 : 10
|
|
261
287
|
}];
|
|
262
288
|
}
|
|
263
289
|
return { version: 1, hooks };
|
|
@@ -268,9 +294,10 @@ function mergeHooks(existing, incoming) {
|
|
|
268
294
|
merged.hooks = merged.hooks && typeof merged.hooks === "object" ? merged.hooks : {};
|
|
269
295
|
for (const [event, groups] of Object.entries(incoming.hooks || {})) {
|
|
270
296
|
const current = Array.isArray(merged.hooks[event]) ? merged.hooks[event] : [];
|
|
271
|
-
const withoutOldContextGuard = current.
|
|
272
|
-
const hooks =
|
|
273
|
-
|
|
297
|
+
const withoutOldContextGuard = current.flatMap((group) => {
|
|
298
|
+
const hooks = group.hooks.filter((hook) => !String(hook.command || "").includes("context_guard_hook.py"));
|
|
299
|
+
if (hooks.length === group.hooks.length) return [group];
|
|
300
|
+
return hooks.length ? [{ ...group, hooks }] : [];
|
|
274
301
|
});
|
|
275
302
|
merged.hooks[event] = withoutOldContextGuard.concat(groups);
|
|
276
303
|
}
|
|
@@ -290,40 +317,132 @@ function mergeCursorHooks(existing, incoming) {
|
|
|
290
317
|
return merged;
|
|
291
318
|
}
|
|
292
319
|
|
|
293
|
-
function
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
320
|
+
function readObject(target, platform) {
|
|
321
|
+
if (!fs.existsSync(target)) return {};
|
|
322
|
+
const object = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
323
|
+
let value;
|
|
324
|
+
try {
|
|
325
|
+
value = JSON.parse(fs.readFileSync(target, "utf8").replace(/^\uFEFF/, ""));
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw new Error(`Cannot read JSON config ${target}: ${error.message}`);
|
|
297
328
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
329
|
+
if (!object(value) || (value.hooks !== undefined && !object(value.hooks))) {
|
|
330
|
+
throw new Error(`Invalid config object or hooks in ${target}; original file was not changed.`);
|
|
331
|
+
}
|
|
332
|
+
for (const [event, entries] of Object.entries(value.hooks || {})) {
|
|
333
|
+
if (!Array.isArray(entries) || entries.some(entry => !object(entry) ||
|
|
334
|
+
(platform !== "cursor" && (!Array.isArray(entry.hooks) || entry.hooks.some(hook => !object(hook)))))) {
|
|
335
|
+
throw new Error(`Invalid hook event ${event} in ${target}; original file was not changed.`);
|
|
336
|
+
}
|
|
303
337
|
}
|
|
304
|
-
|
|
305
|
-
console.log(`[context-guard-skill] installed ${label}: ${target}`);
|
|
338
|
+
return value;
|
|
306
339
|
}
|
|
307
340
|
|
|
308
|
-
|
|
309
|
-
if (!fs.existsSync(target)) return {};
|
|
310
|
-
const value = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
311
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
312
|
-
}
|
|
341
|
+
const CLAUDE_EVENTS = ["SessionStart", "SubagentStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "PostToolUse", "PostToolUseFailure", "PreCompact", "PostCompact", "SubagentStop", "Stop", "StopFailure", "SessionEnd"];
|
|
313
342
|
|
|
314
|
-
function
|
|
343
|
+
function plannedHooks(platform, skillTarget, hooksTarget) {
|
|
315
344
|
if (!fs.existsSync(sourceHooksPath)) {
|
|
316
|
-
|
|
345
|
+
throw new Error(`source hooks file is missing: ${sourceHooksPath}`);
|
|
317
346
|
}
|
|
318
|
-
const existing = readObject(hooksTarget);
|
|
347
|
+
const existing = readObject(hooksTarget, platform);
|
|
319
348
|
if (platform === "cursor") {
|
|
320
|
-
|
|
321
|
-
return;
|
|
349
|
+
return mergeCursorHooks(existing, cursorHooks(skillTarget));
|
|
322
350
|
}
|
|
323
351
|
const rawIncoming = JSON.parse(fs.readFileSync(sourceHooksPath, "utf8"));
|
|
352
|
+
if (platform === "claude") {
|
|
353
|
+
for (const [name, source, from, to] of [["PostToolUseFailure", "PostToolUse", "post-tool-use", "post-tool-use-failure"], ["SessionEnd", "Stop", "stop", "session-end"], ["StopFailure", "Interrupt", "interrupt", "stop-failure"]]) {
|
|
354
|
+
rawIncoming.hooks[name] = JSON.parse(JSON.stringify(rawIncoming.hooks[source]));
|
|
355
|
+
for (const group of rawIncoming.hooks[name]) for (const hook of group.hooks) hook.command = hook.command.replace(`context_guard_hook.py ${from} `, `context_guard_hook.py ${to} `);
|
|
356
|
+
}
|
|
357
|
+
const supported = new Set(CLAUDE_EVENTS);
|
|
358
|
+
rawIncoming.hooks = Object.fromEntries(Object.entries(rawIncoming.hooks || {}).filter(([event]) => supported.has(event)));
|
|
359
|
+
}
|
|
324
360
|
const incoming = rewriteGroupedHookCommands(rawIncoming, skillTarget, platform);
|
|
325
|
-
|
|
326
|
-
|
|
361
|
+
return mergeHooks(existing, incoming);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function containsPath(parent, child) {
|
|
365
|
+
const relative = path.relative(parent, child);
|
|
366
|
+
return relative === "" || (!relative.startsWith(".." + path.sep) && relative !== ".." && !path.isAbsolute(relative));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function applyInstallPlan(plan, dryRun) {
|
|
370
|
+
for (const entry of plan) {
|
|
371
|
+
if (containsPath(entry.target, os.homedir())) {
|
|
372
|
+
throw new Error(`Install destination must not replace the user home or its parent: ${entry.target}`);
|
|
373
|
+
}
|
|
374
|
+
if (containsPath(entry.target, sourceSkillDir) || containsPath(sourceSkillDir, entry.target)) {
|
|
375
|
+
throw new Error(`Install destination overlaps its source package: ${entry.target}`);
|
|
376
|
+
}
|
|
377
|
+
const stat = fs.lstatSync(entry.target, { throwIfNoEntry: false });
|
|
378
|
+
if (stat && (stat.isSymbolicLink() || (entry.kind === "skill" ? !stat.isDirectory() : !stat.isFile()))) {
|
|
379
|
+
throw new Error(`Invalid install destination: ${entry.target}`);
|
|
380
|
+
}
|
|
381
|
+
entry.mode = stat ? stat.mode & 0o777 : 0o600;
|
|
382
|
+
for (const other of plan) if (entry !== other && containsPath(entry.target, other.target)) {
|
|
383
|
+
throw new Error(`Overlapping install destinations: ${entry.target} and ${other.target}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (dryRun) {
|
|
387
|
+
for (const entry of plan) console.log(`[context-guard-skill] would install ${entry.kind}: ${entry.target}`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const staged = [];
|
|
392
|
+
try {
|
|
393
|
+
// Prepare every replacement before touching existing files. Temporary
|
|
394
|
+
// siblings allow renames on the same filesystem, including on Windows.
|
|
395
|
+
for (const entry of plan) {
|
|
396
|
+
fs.mkdirSync(path.dirname(entry.target), { recursive: true });
|
|
397
|
+
entry.temp = fs.mkdtempSync(path.join(path.dirname(entry.target), ".context-guard-install-"));
|
|
398
|
+
entry.next = path.join(entry.temp, "next");
|
|
399
|
+
entry.previous = path.join(entry.temp, "previous");
|
|
400
|
+
staged.push(entry);
|
|
401
|
+
if (entry.kind === "skill") {
|
|
402
|
+
copySkill(entry.next);
|
|
403
|
+
// Existing Claude receivers may still reference this absolute prompt path.
|
|
404
|
+
if (fs.existsSync(path.join(entry.target, "Developer.md"))) {
|
|
405
|
+
fs.copyFileSync(path.join(entry.next, "Executor.md"), path.join(entry.next, "Developer.md"));
|
|
406
|
+
}
|
|
407
|
+
} else fs.writeFileSync(entry.next, entry.content, { mode: entry.mode });
|
|
408
|
+
}
|
|
409
|
+
for (const entry of staged) {
|
|
410
|
+
if (fs.existsSync(entry.target)) {
|
|
411
|
+
fs.renameSync(entry.target, entry.previous);
|
|
412
|
+
entry.oldMoved = true;
|
|
413
|
+
}
|
|
414
|
+
fs.renameSync(entry.next, entry.target);
|
|
415
|
+
entry.newMoved = true;
|
|
416
|
+
}
|
|
417
|
+
} catch (error) {
|
|
418
|
+
for (const entry of staged.slice().reverse()) {
|
|
419
|
+
try {
|
|
420
|
+
if (entry.newMoved) fs.rmSync(entry.target, { recursive: entry.kind === "skill", force: true });
|
|
421
|
+
if (entry.oldMoved) fs.renameSync(entry.previous, entry.target);
|
|
422
|
+
} catch (rollbackError) {
|
|
423
|
+
entry.preserve = true;
|
|
424
|
+
console.error(`[context-guard-skill] Recovery needed; kept ${entry.temp}: ${rollbackError.message}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
throw error;
|
|
428
|
+
} finally {
|
|
429
|
+
for (const entry of staged) {
|
|
430
|
+
if (entry.preserve) continue;
|
|
431
|
+
try {
|
|
432
|
+
// Keep original client configuration as a uniquely named backup only
|
|
433
|
+
// after a successful replacement. Failed installs restore it instead.
|
|
434
|
+
if (entry.kind !== "skill" && fs.existsSync(entry.previous)) {
|
|
435
|
+
const backup = `${entry.target}.bak-${path.basename(entry.temp)}`;
|
|
436
|
+
fs.renameSync(entry.previous, backup);
|
|
437
|
+
console.log(`[context-guard-skill] backed up config: ${backup}`);
|
|
438
|
+
}
|
|
439
|
+
fs.rmSync(entry.temp, { recursive: true, force: true });
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.warn(`[context-guard-skill] Kept recovery files at ${entry.temp}: ${error.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const entry of plan) console.log(`[context-guard-skill] installed ${entry.kind}: ${entry.target}`);
|
|
327
446
|
}
|
|
328
447
|
|
|
329
448
|
function install(args) {
|
|
@@ -332,32 +451,126 @@ function install(args) {
|
|
|
332
451
|
if ((options.target || options.hooksTarget || options.configTarget) && options.platform === "auto") {
|
|
333
452
|
platforms = ["codex"];
|
|
334
453
|
}
|
|
454
|
+
const plan = [];
|
|
335
455
|
for (const platform of platforms) {
|
|
336
456
|
const defaults = platformTargets(platform);
|
|
337
457
|
const skillTarget = options.target || defaults.target;
|
|
338
458
|
const hooksTarget = options.hooksTarget || defaults.hooksTarget;
|
|
339
459
|
const configTarget = options.configTarget || defaults.configTarget;
|
|
340
|
-
|
|
460
|
+
plan.push({ target: skillTarget, kind: "skill" });
|
|
341
461
|
if (options.withHooks) {
|
|
462
|
+
const hooks = plannedHooks(platform, skillTarget, hooksTarget);
|
|
342
463
|
if (platform === "codex" && configTarget) {
|
|
343
|
-
|
|
464
|
+
const content = migratedHooksFeatureConfig(configTarget);
|
|
465
|
+
if (content !== null) plan.push({ target: configTarget, kind: "config", content });
|
|
344
466
|
}
|
|
345
|
-
|
|
467
|
+
plan.push({ target: hooksTarget, kind: "hooks", content: `${JSON.stringify(hooks, null, 2)}\n` });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
applyInstallPlan(plan, options.dryRun);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function parseDoctorArgs(args) {
|
|
474
|
+
const options = { platform: "auto", root: process.cwd(), target: null, hooksTarget: null, configTarget: null, json: false };
|
|
475
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
476
|
+
const arg = args[i];
|
|
477
|
+
if (["--platform", "--root", "--target", "--hooks-target", "--config-target"].includes(arg)) {
|
|
478
|
+
const value = args[++i];
|
|
479
|
+
if (!value) fail(`${arg} requires a value`);
|
|
480
|
+
if (arg === "--platform") {
|
|
481
|
+
const platform = String(value).toLowerCase();
|
|
482
|
+
if (!["auto", "all", ...Object.keys(PLATFORM_SPECS)].includes(platform)) fail("--platform must be auto, all, codex, cursor, or claude");
|
|
483
|
+
options.platform = platform;
|
|
484
|
+
} else options[{ "--root": "root", "--target": "target", "--hooks-target": "hooksTarget", "--config-target": "configTarget" }[arg]] = path.resolve(expandHome(value));
|
|
485
|
+
} else if (arg === "--json") options.json = true;
|
|
486
|
+
else fail(`unknown doctor option: ${arg}`);
|
|
487
|
+
}
|
|
488
|
+
if ((options.target || options.hooksTarget || options.configTarget) && options.platform === "all") fail("custom doctor targets can only be used with one platform");
|
|
489
|
+
return options;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function hookCommands(config, platform, event) {
|
|
493
|
+
const entries = config?.hooks?.[event];
|
|
494
|
+
if (!Array.isArray(entries)) return [];
|
|
495
|
+
if (platform === "cursor") return entries.map(item => item?.command).filter(Boolean);
|
|
496
|
+
return entries.flatMap(group => (group?.hooks || []).map(item => item?.command)).filter(Boolean);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function doctor(args) {
|
|
500
|
+
const options = parseDoctorArgs(args);
|
|
501
|
+
const results = [];
|
|
502
|
+
const check = (name, ok, detail, required = true) => results.push({ name, ok: Boolean(ok), detail, required });
|
|
503
|
+
const python = pythonCommand();
|
|
504
|
+
check("python", python, python ? `${python} is Python 3` : "no working Python 3 interpreter");
|
|
505
|
+
let platforms = selectedPlatforms(options.platform);
|
|
506
|
+
if ((options.target || options.hooksTarget || options.configTarget) && options.platform === "auto") platforms = ["codex"];
|
|
507
|
+
const eventNames = {
|
|
508
|
+
codex: ["SessionStart", "SubagentStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "PostToolUse", "PreCompact", "PostCompact", "SubagentStop", "Stop", "Interrupt"],
|
|
509
|
+
claude: CLAUDE_EVENTS,
|
|
510
|
+
cursor: ["sessionStart", "subagentStart", "beforeSubmitPrompt", "subagentStop", "stop"]
|
|
511
|
+
};
|
|
512
|
+
for (const platform of platforms) {
|
|
513
|
+
const defaults = platformTargets(platform);
|
|
514
|
+
const target = options.target || defaults.target;
|
|
515
|
+
const hooksTarget = options.hooksTarget || defaults.hooksTarget;
|
|
516
|
+
const configTarget = options.configTarget || defaults.configTarget;
|
|
517
|
+
const installedLauncher = path.join(target, "bin", "context-guard-skill.js");
|
|
518
|
+
check(`${platform}.skill`, fs.existsSync(path.join(target, "SKILL.md")) && fs.existsSync(path.join(target, "scripts", "context_guard_hook.py")) && fs.existsSync(installedLauncher), target);
|
|
519
|
+
check(`${platform}.cli`, fs.existsSync(installedLauncher), installedLauncher);
|
|
520
|
+
let config = null;
|
|
521
|
+
try { config = readObject(hooksTarget, platform); } catch (error) { check(`${platform}.hooks`, false, error.message); }
|
|
522
|
+
if (config) {
|
|
523
|
+
const missing = eventNames[platform].filter(event => !hookCommands(config, platform, event).some(command => command.includes("context_guard_hook.py") && command.includes(`--platform ${platform}`)));
|
|
524
|
+
check(`${platform}.hooks`, missing.length === 0, missing.length ? `missing ${missing.join(", ")}` : hooksTarget);
|
|
346
525
|
}
|
|
526
|
+
if (platform === "codex" && configTarget) {
|
|
527
|
+
const text = fs.existsSync(configTarget) ? fs.readFileSync(configTarget, "utf8") : "";
|
|
528
|
+
check("codex.hooks-feature", /^\s*hooks\s*=\s*true\s*$/m.test(text), configTarget);
|
|
529
|
+
const probe = spawnSync(process.execPath, [path.join(sourceSkillDir, 'scripts/workbench/hook-status.mjs'), options.root, target], { encoding: 'utf8', windowsHide: true, timeout: 7000 });
|
|
530
|
+
let native = {}; try { native = JSON.parse(probe.stdout); } catch {}
|
|
531
|
+
check('codex.hooks-trust', native.trusted, native.unavailable ? 'native trust status unavailable; not ready' : JSON.stringify(native));
|
|
532
|
+
const crypto = require('crypto');
|
|
533
|
+
let expected = '', installed = '';
|
|
534
|
+
try { expected = crypto.createHash('sha256').update(fs.readFileSync(path.join(sourceSkillDir, 'scripts/context_guard_hook.py'))).digest('hex'); installed = crypto.createHash('sha256').update(fs.readFileSync(path.join(target, 'scripts/context_guard_hook.py'))).digest('hex'); } catch {}
|
|
535
|
+
check('codex.hooks-version', expected && expected === installed, 'installed Hook must match this release');
|
|
536
|
+
let events = [];
|
|
537
|
+
try { events = fs.readFileSync(path.join(options.root, '.codex/context/sessions.jsonl'), 'utf8').split('\n').filter(Boolean).map(JSON.parse); } catch {}
|
|
538
|
+
const current = events.filter(event => event.hook_sha256 === installed && (!process.env.CODEX_THREAD_ID || event.session_id === process.env.CODEX_THREAD_ID));
|
|
539
|
+
check('codex.hooks-executed', current.length > 0, 'execution evidence for the current installed script');
|
|
540
|
+
check('codex.context-emitted', current.some(event => event.context_emitted), 'context output emitted; delivery to a model is not observable by doctor');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const ctx = path.join(options.root, ".codex", "context");
|
|
544
|
+
let map = null;
|
|
545
|
+
try { map = JSON.parse(fs.readFileSync(path.join(ctx, "map.json"), "utf8")); } catch {}
|
|
546
|
+
check("project.map", map?.root?.id || map?.root === null, path.join(ctx, "map.json"));
|
|
547
|
+
check("project.sessions", fs.existsSync(path.join(ctx, "sessions.jsonl")), path.join(ctx, "sessions.jsonl"));
|
|
548
|
+
const diagnosticLauncher = platforms.length === 1 ? path.join((options.target || platformTargets(platforms[0]).target), "bin", "context-guard-skill.js") : null;
|
|
549
|
+
let diagnostic = null;
|
|
550
|
+
if (diagnosticLauncher && fs.existsSync(diagnosticLauncher)) {
|
|
551
|
+
const probe = spawnSync(process.execPath, [diagnosticLauncher, "workbench", "--diagnose", "--root", options.root, ...(process.env.CODEX_THREAD_ID ? ["--session", process.env.CODEX_THREAD_ID] : [])], { encoding: "utf8", windowsHide: true, timeout: 7000 });
|
|
552
|
+
try { diagnostic = JSON.parse(probe.stdout); } catch {}
|
|
347
553
|
}
|
|
554
|
+
const runtimeStatus = diagnostic?.runtime?.status || "unknown";
|
|
555
|
+
check("project.workbench", ["ready", "stopped"].includes(runtimeStatus), diagnostic ? JSON.stringify({ status: runtimeStatus, named: diagnostic.runtime.named, services: diagnostic.runtime.services }) : "diagnosis unavailable", false);
|
|
556
|
+
const ok = results.every(item => !item.required || item.ok);
|
|
557
|
+
if (options.json) console.log(JSON.stringify({ ok, results }, null, 2));
|
|
558
|
+
else {
|
|
559
|
+
for (const item of results) console.log(`[${item.ok ? "ok" : item.required ? "fail" : "warn"}] ${item.name}: ${item.detail}`);
|
|
560
|
+
console.log(`[context-guard-skill] doctor: ${ok ? "ready" : "not ready"}`);
|
|
561
|
+
}
|
|
562
|
+
if (!ok) process.exitCode = 1;
|
|
348
563
|
}
|
|
349
564
|
|
|
350
565
|
function runPython(args) {
|
|
351
566
|
if (!fs.existsSync(pythonScript)) {
|
|
352
567
|
fail(`context_guard.py is missing: ${pythonScript}`);
|
|
353
568
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
fail("Python 3 is required; neither `python3` nor `python` was found.");
|
|
569
|
+
const command = pythonCommand();
|
|
570
|
+
if (!command) fail("Python 3 is required; no working Python 3 interpreter was found (`python`, `python3`).");
|
|
571
|
+
const result = spawnSync(command, [pythonScript, ...args], { stdio: "inherit", windowsHide: true });
|
|
572
|
+
if (result.error) fail(result.error.message);
|
|
573
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
361
574
|
}
|
|
362
575
|
|
|
363
576
|
const [command, ...rest] = process.argv.slice(2);
|
|
@@ -365,9 +578,14 @@ const [command, ...rest] = process.argv.slice(2);
|
|
|
365
578
|
if (!command || command === "-h" || command === "--help" || command === "help") {
|
|
366
579
|
usage();
|
|
367
580
|
} else if (command === "install") {
|
|
368
|
-
install(rest);
|
|
581
|
+
try { install(rest); } catch (error) { fail(error.message); }
|
|
369
582
|
} else if (command === "path") {
|
|
370
583
|
console.log(sourceSkillDir);
|
|
584
|
+
} else if (command === "doctor") {
|
|
585
|
+
doctor(rest);
|
|
586
|
+
} else if (["map", "workbench", "memory", "preferences", "sync"].includes(command)) {
|
|
587
|
+
const result = spawnSync(process.execPath, [path.join(sourceSkillDir, "scripts", "workbench", "cli.mjs"), command, ...rest], { stdio: "inherit", windowsHide: true });
|
|
588
|
+
process.exit(result.status ?? 1);
|
|
371
589
|
} else {
|
|
372
590
|
runPython([command, ...rest]);
|
|
373
591
|
}
|
package/bin/postinstall.js
CHANGED
|
@@ -18,7 +18,7 @@ if (!isGlobalInstall && !autoInstall) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const cli = path.join(__dirname, "context-guard-skill.js");
|
|
21
|
-
const result = spawnSync(process.execPath, [cli, "install"], { stdio: "inherit" });
|
|
21
|
+
const result = spawnSync(process.execPath, [cli, "install"], { stdio: "inherit", windowsHide: true });
|
|
22
22
|
|
|
23
23
|
if (result.error) {
|
|
24
24
|
console.warn(`[context-guard-skill] auto install skipped: ${result.error.message}`);
|
package/hooks.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
9
|
"command": "python3 ./scripts/context_guard_hook.py session-start --platform codex",
|
|
10
|
-
"timeout":
|
|
11
|
-
"statusMessage": "
|
|
10
|
+
"timeout": 20,
|
|
11
|
+
"statusMessage": "Checking Session context"
|
|
12
12
|
}
|
|
13
13
|
]
|
|
14
14
|
}
|
|
@@ -37,6 +37,70 @@
|
|
|
37
37
|
]
|
|
38
38
|
}
|
|
39
39
|
],
|
|
40
|
+
"PreToolUse": [
|
|
41
|
+
{
|
|
42
|
+
"matcher": "Bash|exec_command|shell|run_shell_command|apply_patch|Edit|Write|mcp__.*(write|edit|delete|move).*",
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "python3 ./scripts/context_guard_hook.py pre-tool-use --platform codex",
|
|
47
|
+
"timeout": 20,
|
|
48
|
+
"statusMessage": "Checking tool scope"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"PermissionRequest": [
|
|
54
|
+
{
|
|
55
|
+
"hooks": [
|
|
56
|
+
{
|
|
57
|
+
"type": "command",
|
|
58
|
+
"command": "python3 ./scripts/context_guard_hook.py permission-request --platform codex",
|
|
59
|
+
"timeout": 10,
|
|
60
|
+
"statusMessage": "Checking Context Guard node scope"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"PostToolUse": [
|
|
66
|
+
{
|
|
67
|
+
"matcher": "Bash|exec_command|shell|run_shell_command|apply_patch|Edit|Write|mcp__.*(write|edit|delete|move).*",
|
|
68
|
+
"hooks": [
|
|
69
|
+
{
|
|
70
|
+
"type": "command",
|
|
71
|
+
"command": "python3 ./scripts/context_guard_hook.py post-tool-use --platform codex",
|
|
72
|
+
"timeout": 20,
|
|
73
|
+
"statusMessage": "Recording tool result"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"PreCompact": [
|
|
79
|
+
{
|
|
80
|
+
"matcher": "manual|auto",
|
|
81
|
+
"hooks": [
|
|
82
|
+
{
|
|
83
|
+
"type": "command",
|
|
84
|
+
"command": "python3 ./scripts/context_guard_hook.py pre-compact --platform codex",
|
|
85
|
+
"timeout": 10,
|
|
86
|
+
"statusMessage": "Saving Context Guard plan state"
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"PostCompact": [
|
|
92
|
+
{
|
|
93
|
+
"matcher": "manual|auto",
|
|
94
|
+
"hooks": [
|
|
95
|
+
{
|
|
96
|
+
"type": "command",
|
|
97
|
+
"command": "python3 ./scripts/context_guard_hook.py post-compact --platform codex",
|
|
98
|
+
"timeout": 10,
|
|
99
|
+
"statusMessage": "Restoring Context Guard map state"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
],
|
|
40
104
|
"SubagentStop": [
|
|
41
105
|
{
|
|
42
106
|
"hooks": [
|
|
@@ -44,7 +108,7 @@
|
|
|
44
108
|
"type": "command",
|
|
45
109
|
"command": "python3 ./scripts/context_guard_hook.py subagent-stop --platform codex",
|
|
46
110
|
"timeout": 10,
|
|
47
|
-
"statusMessage": "
|
|
111
|
+
"statusMessage": "Recording subagent stop"
|
|
48
112
|
}
|
|
49
113
|
]
|
|
50
114
|
}
|
|
@@ -56,7 +120,19 @@
|
|
|
56
120
|
"type": "command",
|
|
57
121
|
"command": "python3 ./scripts/context_guard_hook.py stop --platform codex",
|
|
58
122
|
"timeout": 10,
|
|
59
|
-
"statusMessage": "
|
|
123
|
+
"statusMessage": "Checking Map archive and plan completion"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"Interrupt": [
|
|
129
|
+
{
|
|
130
|
+
"hooks": [
|
|
131
|
+
{
|
|
132
|
+
"type": "command",
|
|
133
|
+
"command": "python3 ./scripts/context_guard_hook.py interrupt --platform codex",
|
|
134
|
+
"timeout": 3,
|
|
135
|
+
"statusMessage": "Saving interrupted Context Guard state"
|
|
60
136
|
}
|
|
61
137
|
]
|
|
62
138
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2012 Tim Caswell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge,
|
|
6
|
+
to any person obtaining a copy of this software and
|
|
7
|
+
associated documentation files (the "Software"), to
|
|
8
|
+
deal in the Software without restriction, including
|
|
9
|
+
without limitation the rights to use, copy, modify,
|
|
10
|
+
merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom
|
|
12
|
+
the Software is furnished to do so,
|
|
13
|
+
subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice
|
|
16
|
+
shall be included in all copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
20
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
21
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
|
22
|
+
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
23
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
24
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|