context-mode 1.0.136 → 1.0.138
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +9 -24
- package/build/adapters/codex/index.js +24 -3
- package/build/adapters/jetbrains-copilot/hooks.d.ts +11 -3
- package/build/adapters/jetbrains-copilot/hooks.js +11 -7
- package/build/adapters/opencode/index.d.ts +1 -0
- package/build/adapters/opencode/index.js +25 -0
- package/build/adapters/opencode/plugin.d.ts +22 -0
- package/build/adapters/opencode/plugin.js +52 -0
- package/build/adapters/pi/extension.js +20 -4
- package/build/adapters/pi/mcp-bridge.d.ts +2 -1
- package/build/adapters/pi/mcp-bridge.js +49 -3
- package/build/adapters/vscode-copilot/hooks.d.ts +27 -3
- package/build/adapters/vscode-copilot/hooks.js +27 -12
- package/build/cli.js +199 -32
- package/build/lifecycle.d.ts +2 -51
- package/build/lifecycle.js +3 -67
- package/build/openclaw-plugin.d.ts +130 -0
- package/build/openclaw-plugin.js +626 -0
- package/build/opencode-plugin.d.ts +122 -0
- package/build/opencode-plugin.js +372 -0
- package/build/pi-extension.d.ts +14 -0
- package/build/pi-extension.js +451 -0
- package/build/server.d.ts +19 -0
- package/build/server.js +145 -59
- package/build/session/db.d.ts +6 -0
- package/build/session/db.js +17 -3
- package/build/util/db-lock.d.ts +65 -0
- package/build/util/db-lock.js +166 -0
- package/build/util/sibling-mcp.d.ts +0 -40
- package/build/util/sibling-mcp.js +11 -116
- package/cli.bundle.mjs +181 -166
- package/configs/kilo/kilo.json +0 -11
- package/configs/opencode/opencode.json +0 -11
- package/hooks/normalize-hooks.mjs +101 -19
- package/hooks/session-db.bundle.mjs +3 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/heal-installed-plugins.mjs +115 -1
- package/scripts/postinstall.mjs +16 -18
- package/server.bundle.mjs +112 -110
- package/start.mjs +11 -14
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { buildNodeCommand } from "../types.js";
|
|
2
1
|
/**
|
|
3
2
|
* adapters/vscode-copilot/hooks — VS Code Copilot hook definitions and matchers.
|
|
4
3
|
*
|
|
@@ -63,21 +62,37 @@ export function isContextModeHook(entry, hookType) {
|
|
|
63
62
|
}
|
|
64
63
|
/**
|
|
65
64
|
* Build the hook command string for a given hook type.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
*
|
|
66
|
+
* Always emits the CLI dispatcher form
|
|
67
|
+
* (`context-mode hook vscode-copilot <event>`) — the `pluginRoot` argument
|
|
68
|
+
* is accepted for API compatibility but intentionally ignored.
|
|
69
|
+
*
|
|
70
|
+
* Why the dispatcher form is mandatory here (Issue #613 — Tier C contract):
|
|
71
|
+
* `.github/hooks/context-mode.json` is a **workspace-committed** file
|
|
72
|
+
* (upstream: refs/platforms/vscode-copilot/assets/prompts/skills/
|
|
73
|
+
* agent-customization/references/hooks.md line 7 — "Workspace
|
|
74
|
+
* (team-shared)"). It lands in every teammate's `git status`. Embedding
|
|
75
|
+
* `process.execPath` or any absolute pluginRoot path here:
|
|
76
|
+
* - Leaks PII (username, `C:/Users/<user>/...` paths).
|
|
77
|
+
* - Breaks cross-machine portability (fnm/nvm/volta/brew shims are
|
|
78
|
+
* per-shell-session ephemeral; the path goes stale immediately on
|
|
79
|
+
* Windows + fnm).
|
|
80
|
+
*
|
|
81
|
+
* Commit `f5c9d02` (2026-03-06) added an absolute-path branch when a
|
|
82
|
+
* pluginRoot was passed. It solved a real PATH-availability bug on
|
|
83
|
+
* Brew/nvm setups by going too far — the CLI then always passes
|
|
84
|
+
* pluginRoot, so the portable form became unreachable in production
|
|
85
|
+
* and every `/ctx-upgrade` baked a non-portable command into the
|
|
86
|
+
* committed config. This reverts to the pre-`f5c9d02` shape.
|
|
87
|
+
*
|
|
88
|
+
* For users without a global install, the recovery path is the same as
|
|
89
|
+
* every other CLI-dispatcher adapter (cursor, codex):
|
|
90
|
+
* `npm install -g context-mode`
|
|
68
91
|
*/
|
|
69
|
-
export function buildHookCommand(hookType,
|
|
92
|
+
export function buildHookCommand(hookType, _pluginRoot) {
|
|
70
93
|
const scriptName = HOOK_SCRIPTS[hookType];
|
|
71
94
|
if (!scriptName) {
|
|
72
95
|
throw new Error(`No script defined for hook type: ${hookType}`);
|
|
73
96
|
}
|
|
74
|
-
if (pluginRoot) {
|
|
75
|
-
// v1.0.107 fix — was `${pluginRoot}/hooks/${scriptName}` which resolved to
|
|
76
|
-
// the Claude-Code generic hook (`hooks/pretooluse.mjs`) instead of the
|
|
77
|
-
// VSCode-specific wrapper at `hooks/vscode-copilot/pretooluse.mjs`. JetBrains
|
|
78
|
-
// adapter already had the correct subdir (jetbrains-copilot/hooks.ts:98)
|
|
79
|
-
// so this brings VSCode to parity.
|
|
80
|
-
return buildNodeCommand(`${pluginRoot}/hooks/vscode-copilot/${scriptName}`);
|
|
81
|
-
}
|
|
82
97
|
return `context-mode hook vscode-copilot ${hookType.toLowerCase()}`;
|
|
83
98
|
}
|
package/build/cli.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import * as p from "@clack/prompts";
|
|
15
15
|
import color from "picocolors";
|
|
16
16
|
import { execFileSync, execSync, execFile as nodeExecFile } from "node:child_process";
|
|
17
|
-
import { readFileSync,
|
|
17
|
+
import { readFileSync, cpSync, accessSync, existsSync, readdirSync, rmSync, closeSync, openSync, chmodSync, constants } from "node:fs";
|
|
18
18
|
import { request as httpsRequest } from "node:https";
|
|
19
19
|
import { resolve, dirname, join } from "node:path";
|
|
20
20
|
import { tmpdir, devNull, homedir } from "node:os";
|
|
@@ -27,7 +27,7 @@ import { discoverSiblingMcpPids, killSiblingMcpServers } from "./util/sibling-mc
|
|
|
27
27
|
// v1.0.119 — Issue #523 Layer 5 heal: post-bump assertion on .claude-plugin/plugin.json
|
|
28
28
|
// mcpServers args. Single source of truth shared with start.mjs HEAL block + postinstall.
|
|
29
29
|
// @ts-expect-error — JS module, no TS declarations
|
|
30
|
-
import { healPluginJsonMcpServers,
|
|
30
|
+
import { healPluginJsonMcpServers, sweepStaleMcpJson } from "../scripts/heal-installed-plugins.mjs";
|
|
31
31
|
// @ts-expect-error — JS module, no TS declarations
|
|
32
32
|
import { detectWindowsVsYear } from "../scripts/heal-better-sqlite3.mjs";
|
|
33
33
|
// Private 16-LOC copy of browserOpenArgv. Canonical version lives in src/server.ts;
|
|
@@ -454,6 +454,166 @@ async function doctor() {
|
|
|
454
454
|
p.log.warn(color.yellow("Plugin enabled: WARN") +
|
|
455
455
|
` — ${pluginCheck.message}`);
|
|
456
456
|
}
|
|
457
|
+
// ── Issue #613 — proactive Tier C absolute-path detection ───────────
|
|
458
|
+
// PR #620 fixed `buildHookCommand` for vscode-copilot + jetbrains-copilot
|
|
459
|
+
// so future writes are CLI-dispatcher-shape. But users who ran
|
|
460
|
+
// /ctx-upgrade on v1.0.136 or earlier are still carrying poisoned
|
|
461
|
+
// committable files in their workspace:
|
|
462
|
+
// - `.github/hooks/context-mode.json` (vscode-copilot, team-shared)
|
|
463
|
+
// - `.jetbrains/copilot/hooks.json` (jetbrains-copilot, team-shared)
|
|
464
|
+
// - `.cursor/hooks.json` (cursor, team-shared)
|
|
465
|
+
// Per ISSUE-613-VERDICT §6.1 these are Tier C — workspace-committed
|
|
466
|
+
// cross-machine config. Doctor scans them for absolute paths and
|
|
467
|
+
// fnm_multishells shims; if found, FAIL with `ctx_upgrade` remediation.
|
|
468
|
+
// Per ISSUE-604-VERDICT §11 ("silent-green doctor while hooks are dead
|
|
469
|
+
// is itself a P0 trust bug") — surface poison BEFORE the user hits a
|
|
470
|
+
// runtime failure.
|
|
471
|
+
p.log.step("Checking team-shared hook configs in your workspace...");
|
|
472
|
+
{
|
|
473
|
+
const projectDir = process.cwd();
|
|
474
|
+
const tierCFiles = [
|
|
475
|
+
".github/hooks/context-mode.json",
|
|
476
|
+
".cursor/hooks.json",
|
|
477
|
+
".jetbrains/copilot/hooks.json",
|
|
478
|
+
];
|
|
479
|
+
let tierCFails = 0;
|
|
480
|
+
let tierCChecked = 0;
|
|
481
|
+
// Detect absolute-path patterns that should never appear in a
|
|
482
|
+
// workspace-committed config. Per Mert's standing Windows-safety rule:
|
|
483
|
+
// handle both `/` and `\\` separators.
|
|
484
|
+
function isAbsoluteOrShimPath(s) {
|
|
485
|
+
// unix absolute
|
|
486
|
+
if (s.startsWith("/"))
|
|
487
|
+
return true;
|
|
488
|
+
// Windows drive-letter absolute (e.g. C:/, C:\)
|
|
489
|
+
if (/^[A-Za-z]:[/\\]/.test(s))
|
|
490
|
+
return true;
|
|
491
|
+
// Windows UNC or escaped-backslash absolute fragments
|
|
492
|
+
if (s.includes("\\\\"))
|
|
493
|
+
return true;
|
|
494
|
+
// fnm shim hint — issue #613 reporter's exact stderr shape
|
|
495
|
+
if (s.includes("fnm_multishells"))
|
|
496
|
+
return true;
|
|
497
|
+
// process.execPath literal baked into JSON
|
|
498
|
+
if (s.includes("process.execPath"))
|
|
499
|
+
return true;
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
function recurseStrings(node, hit) {
|
|
503
|
+
if (typeof node === "string") {
|
|
504
|
+
hit(node);
|
|
505
|
+
}
|
|
506
|
+
else if (Array.isArray(node)) {
|
|
507
|
+
for (const item of node)
|
|
508
|
+
recurseStrings(item, hit);
|
|
509
|
+
}
|
|
510
|
+
else if (node && typeof node === "object") {
|
|
511
|
+
for (const v of Object.values(node))
|
|
512
|
+
recurseStrings(v, hit);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const rel of tierCFiles) {
|
|
516
|
+
const abs = resolve(projectDir, rel);
|
|
517
|
+
if (!existsSync(abs))
|
|
518
|
+
continue; // missing config → SKIP, no false fail
|
|
519
|
+
tierCChecked++;
|
|
520
|
+
try {
|
|
521
|
+
const parsed = JSON.parse(readFileSync(abs, "utf-8"));
|
|
522
|
+
const offenders = [];
|
|
523
|
+
recurseStrings(parsed, (s) => {
|
|
524
|
+
if (isAbsoluteOrShimPath(s))
|
|
525
|
+
offenders.push(s);
|
|
526
|
+
});
|
|
527
|
+
if (offenders.length > 0) {
|
|
528
|
+
criticalFails++;
|
|
529
|
+
tierCFails++;
|
|
530
|
+
// Truncate to one example to keep output readable; show count.
|
|
531
|
+
const example = offenders[0].length > 100
|
|
532
|
+
? offenders[0].slice(0, 97) + "..."
|
|
533
|
+
: offenders[0];
|
|
534
|
+
p.log.error(color.red(`Hook config: FAIL`) +
|
|
535
|
+
` — ${rel} has your machine's local paths baked in` +
|
|
536
|
+
color.dim("\n This file is committed to git, so teammates and CI will get your path and the hooks will break for them." +
|
|
537
|
+
`\n Found ${offenders.length} hard-coded path(s), e.g.: ${example}` +
|
|
538
|
+
"\n Fix: run /context-mode:ctx-upgrade — it rewrites the file to a portable form that works on every machine." +
|
|
539
|
+
"\n Details: https://github.com/mksglu/context-mode/issues/613"));
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
p.log.success(color.green("Hook config: PASS") +
|
|
543
|
+
color.dim(` — ${rel} is portable (no hard-coded paths)`));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
// Malformed JSON should not crash doctor; warn and move on.
|
|
548
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
549
|
+
p.log.warn(color.yellow(`Hook config: WARN`) +
|
|
550
|
+
` — ${rel} is not valid JSON` +
|
|
551
|
+
color.dim("\n Doctor cannot scan it for portability issues until the file parses." +
|
|
552
|
+
"\n Fix: open the file and check it in a JSON validator, or delete it and run /context-mode:ctx-upgrade to regenerate." +
|
|
553
|
+
`\n Parser said: ${msg.slice(0, 160)}`));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (tierCChecked === 0) {
|
|
557
|
+
p.log.info(color.dim("Hook config: SKIP — no team-shared hook configs found in this workspace"));
|
|
558
|
+
}
|
|
559
|
+
else if (tierCFails === 0) {
|
|
560
|
+
// already individual PASS messages above; no need for a summary
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// ── Issue #609 — proactive stale `.mcp.json` detection ──────────────
|
|
564
|
+
// PR #620 deleted the per-version cache `.mcp.json` write from cli.ts
|
|
565
|
+
// and shipped `sweepStaleMcpJson` to clean up any pre-existing copies.
|
|
566
|
+
// But users on the field may still have stale `.mcp.json` files left
|
|
567
|
+
// by /ctx-upgrade flows that ran before PR #620 (or by Claude Code's
|
|
568
|
+
// native auto-update copying a poisoned file forward). Surface those
|
|
569
|
+
// as WARN (recoverable — next ctx_upgrade sweeps them) so the user
|
|
570
|
+
// knows what to do instead of being told everything is green while
|
|
571
|
+
// the file lingers on disk.
|
|
572
|
+
// Per ISSUE-604-VERDICT §11 same trust contract as Tier C check above.
|
|
573
|
+
p.log.step("Checking for leftover .mcp.json files from older versions...");
|
|
574
|
+
{
|
|
575
|
+
const cacheRoot = join(homedir(), ".claude", "plugins", "cache", "context-mode", "context-mode");
|
|
576
|
+
if (!existsSync(cacheRoot)) {
|
|
577
|
+
p.log.info(color.dim("Leftover .mcp.json check: SKIP — no plugin cache exists yet (Claude Code has not installed context-mode here)"));
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
let staleCount = 0;
|
|
581
|
+
const staleVersions = [];
|
|
582
|
+
try {
|
|
583
|
+
const versionDirs = readdirSync(cacheRoot);
|
|
584
|
+
for (const v of versionDirs) {
|
|
585
|
+
const candidate = join(cacheRoot, v, ".mcp.json");
|
|
586
|
+
if (existsSync(candidate)) {
|
|
587
|
+
staleCount++;
|
|
588
|
+
if (staleVersions.length < 5)
|
|
589
|
+
staleVersions.push(v);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch (err) {
|
|
594
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
595
|
+
p.log.warn(color.yellow("Leftover .mcp.json check: WARN") +
|
|
596
|
+
` — could not read the plugin cache directory` +
|
|
597
|
+
color.dim(`\n Path: ${cacheRoot}` +
|
|
598
|
+
`\n Reason: ${msg.slice(0, 160)}` +
|
|
599
|
+
"\n Fix: check that the directory is readable, then re-run doctor. If the issue persists, run /context-mode:ctx-upgrade."));
|
|
600
|
+
staleCount = 0;
|
|
601
|
+
}
|
|
602
|
+
if (staleCount === 0) {
|
|
603
|
+
p.log.success(color.green("Leftover .mcp.json check: PASS") +
|
|
604
|
+
color.dim(" — no old .mcp.json files in the plugin cache"));
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
// WARN, not FAIL — per architect spec this is recoverable.
|
|
608
|
+
p.log.warn(color.yellow("Leftover .mcp.json check: WARN") +
|
|
609
|
+
` — found ${staleCount} old .mcp.json file(s) left over from previous context-mode versions` +
|
|
610
|
+
color.dim("\n These are harmless but should be cleaned up so they cannot confuse Claude Code after an auto-update." +
|
|
611
|
+
`\n Versions affected: ${staleVersions.join(", ")}${staleCount > staleVersions.length ? ", ..." : ""}` +
|
|
612
|
+
"\n Fix: run /context-mode:ctx-upgrade — it sweeps these files automatically on the next run." +
|
|
613
|
+
"\n Details: https://github.com/mksglu/context-mode/issues/609"));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
457
617
|
// FTS5 / SQLite
|
|
458
618
|
p.log.step("Checking FTS5 / SQLite...");
|
|
459
619
|
try {
|
|
@@ -798,20 +958,24 @@ async function upgrade(opts) {
|
|
|
798
958
|
}
|
|
799
959
|
catch { /* some files may not exist in source */ }
|
|
800
960
|
}
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
// ${CLAUDE_PLUGIN_ROOT} placeholder
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
961
|
+
// Issue #609 — DO NOT write `.mcp.json` into the plugin cache dir.
|
|
962
|
+
//
|
|
963
|
+
// Historical context: #411 fixed an absolute-path bake by writing the
|
|
964
|
+
// ${CLAUDE_PLUGIN_ROOT} placeholder form here. #531 (commit 9261377)
|
|
965
|
+
// removed `.mcp.json` from `package.json files[]` so the npm tarball
|
|
966
|
+
// stopped shipping it. But the cli-side write persisted, so every
|
|
967
|
+
// /ctx-upgrade re-baked one. When Claude Code's native plugin manager
|
|
968
|
+
// auto-update later carries a previous version's `.mcp.json` forward
|
|
969
|
+
// into a fresh version dir, the stale start.mjs absolute path goes
|
|
970
|
+
// with it → MODULE_NOT_FOUND on every MCP boot.
|
|
971
|
+
//
|
|
972
|
+
// Architectural fix: Claude Code reads `.claude-plugin/plugin.json`
|
|
973
|
+
// .mcpServers as the canonical source (upstream:
|
|
974
|
+
// refs/platforms/claude-code/src/utils/plugins/mcpPluginIntegration.ts:131-212).
|
|
975
|
+
// `.mcp.json` is a redundant per-version artifact whose only role
|
|
976
|
+
// historically was to be a write-time poison vector. Don't write it.
|
|
977
|
+
// The post-bump cache-sweep below removes any pre-existing copies so
|
|
978
|
+
// the previous-version-carry vector cannot replay.
|
|
815
979
|
// Normalize hooks.json + plugin.json against the REAL pluginRoot now that
|
|
816
980
|
// files have been copied. Two reasons:
|
|
817
981
|
// 1. If a prior buggy postinstall (or any future regression) baked the
|
|
@@ -908,30 +1072,33 @@ async function upgrade(opts) {
|
|
|
908
1072
|
const message = err instanceof Error ? err.message : String(err);
|
|
909
1073
|
throw new Error(`plugin.json drift check failed: ${message}`);
|
|
910
1074
|
}
|
|
911
|
-
//
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
1075
|
+
// Issue #609 — Layer 6 replacement: sweep stale `.mcp.json` files from
|
|
1076
|
+
// every per-version cache dir. Supersedes the previous healMcpJsonArgs
|
|
1077
|
+
// drift-check block (v1.0.122) — that block existed because cli.ts
|
|
1078
|
+
// itself wrote `.mcp.json`. With the write gone (above), the only
|
|
1079
|
+
// remaining `.mcp.json` files are stale carry-forwards from earlier
|
|
1080
|
+
// versions. Sweep them so Claude Code's auto-update can't replay them
|
|
1081
|
+
// into a fresh version dir.
|
|
1082
|
+
//
|
|
1083
|
+
// Belt-and-braces: a second sweep call MUST report removed:[] or we
|
|
1084
|
+
// throw — same architectural-lock pattern as the plugin.json drift
|
|
1085
|
+
// check above. Single source of truth shared with start.mjs HEAL
|
|
1086
|
+
// block + postinstall.
|
|
920
1087
|
try {
|
|
921
1088
|
const pluginCacheRoot = resolve(resolveClaudeConfigDir(), "plugins", "cache");
|
|
922
1089
|
const pluginKey = "context-mode@context-mode";
|
|
923
|
-
const
|
|
924
|
-
if (
|
|
925
|
-
|
|
1090
|
+
const firstSweep = sweepStaleMcpJson({ pluginCacheRoot, pluginKey });
|
|
1091
|
+
if (firstSweep && firstSweep.removed && firstSweep.removed.length > 0) {
|
|
1092
|
+
p.log.info(color.dim(` Swept ${firstSweep.removed.length} stale .mcp.json file(s) from cache`));
|
|
926
1093
|
}
|
|
927
|
-
const
|
|
928
|
-
if (
|
|
929
|
-
throw new Error(`.mcp.json drift:
|
|
1094
|
+
const secondSweep = sweepStaleMcpJson({ pluginCacheRoot, pluginKey });
|
|
1095
|
+
if (secondSweep && Array.isArray(secondSweep.removed) && secondSweep.removed.length > 0) {
|
|
1096
|
+
throw new Error(`.mcp.json sweep drift: ${secondSweep.removed.length} file(s) still present after first pass`);
|
|
930
1097
|
}
|
|
931
1098
|
}
|
|
932
1099
|
catch (err) {
|
|
933
1100
|
const message = err instanceof Error ? err.message : String(err);
|
|
934
|
-
throw new Error(`.mcp.json
|
|
1101
|
+
throw new Error(`.mcp.json sweep check failed: ${message}`);
|
|
935
1102
|
}
|
|
936
1103
|
// v1.0.X — Layer 7 heal: update user-level ~/.claude.json MCP server
|
|
937
1104
|
// registrations that point to old context-mode version dirs.
|
package/build/lifecycle.d.ts
CHANGED
|
@@ -20,54 +20,7 @@ export interface LifecycleGuardOptions {
|
|
|
20
20
|
onShutdown: () => void;
|
|
21
21
|
/** Injectable parent-alive check (for testing). Default: ppid-based check. */
|
|
22
22
|
isParentAlive?: () => boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Idle shutdown threshold in ms (#565). When the server has handled no
|
|
25
|
-
* MCP activity for this long, `onShutdown` fires. `0` disables.
|
|
26
|
-
* Default: env `CONTEXT_MODE_IDLE_TIMEOUT_MS`, else 0 (disabled).
|
|
27
|
-
* Skipped on TTY stdin (interactive dev / OpenCode ts-plugin standalone).
|
|
28
|
-
*
|
|
29
|
-
* Pair with the returned `recordActivity()` callback — call it on every
|
|
30
|
-
* MCP request the server handles so genuinely busy servers never trip.
|
|
31
|
-
*/
|
|
32
|
-
idleTimeoutMs?: number;
|
|
33
|
-
/** Test injection — defaults to `Date.now`. */
|
|
34
|
-
now?: () => number;
|
|
35
23
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Hybrid return type: callable like the original `() => void` cleanup (kept
|
|
38
|
-
* for backwards compatibility with #103/#236/#311/#388/#534 test suites),
|
|
39
|
-
* and additionally exposes `recordActivity` for the idle-timeout path (#565)
|
|
40
|
-
* and `stop` as an explicit alias.
|
|
41
|
-
*/
|
|
42
|
-
export interface LifecycleGuardHandle {
|
|
43
|
-
/** Stop the guard. Calling the handle directly is equivalent. */
|
|
44
|
-
(): void;
|
|
45
|
-
/** Bumps the "last activity" timestamp so the idle timer doesn't fire. */
|
|
46
|
-
recordActivity: () => void;
|
|
47
|
-
/** Stop the guard. Alias for invoking the handle. */
|
|
48
|
-
stop: () => void;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Resolve the idle-shutdown threshold (#565).
|
|
52
|
-
*
|
|
53
|
-
* Idle shutdown is OFF by default (#592) because most hosts (Claude
|
|
54
|
-
* Code, Codex, editor MCP clients) keep registered tool handles after a
|
|
55
|
-
* clean MCP child exit and do NOT transparently respawn on the next call.
|
|
56
|
-
* The global 15 min default introduced in #568 solved OpenCode's child
|
|
57
|
-
* accumulation, but stranded ctx_* tools in Claude Code/Codex-style
|
|
58
|
-
* hosts once the MCP server exited cleanly while the editor stayed alive.
|
|
59
|
-
*
|
|
60
|
-
* Hosts that are known to benefit from idle shutdown MUST opt in via
|
|
61
|
-
* CONTEXT_MODE_IDLE_TIMEOUT_MS in their MCP config. Today that is
|
|
62
|
-
* OpenCode/KiloCode (their configs set 900000 = 15 min). Users and test
|
|
63
|
-
* harnesses can also opt in explicitly with any positive integer.
|
|
64
|
-
*
|
|
65
|
-
* Missing or malformed env = 0 (disabled, safe default). Set env to
|
|
66
|
-
* `0` to disable explicitly.
|
|
67
|
-
*
|
|
68
|
-
* Exported for unit-testing.
|
|
69
|
-
*/
|
|
70
|
-
export declare function idleTimeoutForEnv(env?: NodeJS.ProcessEnv): number;
|
|
71
24
|
/** Injectable dependencies for {@link makeDefaultIsParentAlive}. */
|
|
72
25
|
export interface IsParentAliveDeps {
|
|
73
26
|
/** Read the current ppid. Default: `() => process.ppid`. */
|
|
@@ -107,9 +60,7 @@ export declare function makeDefaultIsParentAlive(deps?: IsParentAliveDeps): () =
|
|
|
107
60
|
*/
|
|
108
61
|
export declare function lifecycleGuardIntervalForEnv(env?: NodeJS.ProcessEnv): number;
|
|
109
62
|
/**
|
|
110
|
-
* Start the lifecycle guard. Returns a
|
|
111
|
-
* on every MCP request to keep idle timer from firing) and `stop`.
|
|
112
|
-
*
|
|
63
|
+
* Start the lifecycle guard. Returns a cleanup function.
|
|
113
64
|
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
|
114
65
|
*/
|
|
115
|
-
export declare function startLifecycleGuard(opts: LifecycleGuardOptions):
|
|
66
|
+
export declare function startLifecycleGuard(opts: LifecycleGuardOptions): () => void;
|
package/build/lifecycle.js
CHANGED
|
@@ -14,35 +14,6 @@
|
|
|
14
14
|
* Cross-platform: macOS, Linux, Windows.
|
|
15
15
|
*/
|
|
16
16
|
import { execFileSync } from "node:child_process";
|
|
17
|
-
/**
|
|
18
|
-
* Resolve the idle-shutdown threshold (#565).
|
|
19
|
-
*
|
|
20
|
-
* Idle shutdown is OFF by default (#592) because most hosts (Claude
|
|
21
|
-
* Code, Codex, editor MCP clients) keep registered tool handles after a
|
|
22
|
-
* clean MCP child exit and do NOT transparently respawn on the next call.
|
|
23
|
-
* The global 15 min default introduced in #568 solved OpenCode's child
|
|
24
|
-
* accumulation, but stranded ctx_* tools in Claude Code/Codex-style
|
|
25
|
-
* hosts once the MCP server exited cleanly while the editor stayed alive.
|
|
26
|
-
*
|
|
27
|
-
* Hosts that are known to benefit from idle shutdown MUST opt in via
|
|
28
|
-
* CONTEXT_MODE_IDLE_TIMEOUT_MS in their MCP config. Today that is
|
|
29
|
-
* OpenCode/KiloCode (their configs set 900000 = 15 min). Users and test
|
|
30
|
-
* harnesses can also opt in explicitly with any positive integer.
|
|
31
|
-
*
|
|
32
|
-
* Missing or malformed env = 0 (disabled, safe default). Set env to
|
|
33
|
-
* `0` to disable explicitly.
|
|
34
|
-
*
|
|
35
|
-
* Exported for unit-testing.
|
|
36
|
-
*/
|
|
37
|
-
export function idleTimeoutForEnv(env = process.env) {
|
|
38
|
-
const raw = env.CONTEXT_MODE_IDLE_TIMEOUT_MS;
|
|
39
|
-
if (raw === undefined)
|
|
40
|
-
return 0;
|
|
41
|
-
const n = Number.parseInt(raw, 10);
|
|
42
|
-
if (!Number.isFinite(n) || n < 0)
|
|
43
|
-
return 0;
|
|
44
|
-
return n;
|
|
45
|
-
}
|
|
46
17
|
/** Read grandparent PID via `ps -o ppid= -p $PPID`. Returns NaN on failure or Windows. */
|
|
47
18
|
function readGrandparentPpidImpl() {
|
|
48
19
|
if (process.platform === "win32")
|
|
@@ -124,52 +95,25 @@ export function lifecycleGuardIntervalForEnv(env = process.env) {
|
|
|
124
95
|
return 1000;
|
|
125
96
|
}
|
|
126
97
|
/**
|
|
127
|
-
* Start the lifecycle guard. Returns a
|
|
128
|
-
* on every MCP request to keep idle timer from firing) and `stop`.
|
|
129
|
-
*
|
|
98
|
+
* Start the lifecycle guard. Returns a cleanup function.
|
|
130
99
|
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
|
131
100
|
*/
|
|
132
101
|
export function startLifecycleGuard(opts) {
|
|
133
102
|
const interval = opts.checkIntervalMs ?? lifecycleGuardIntervalForEnv();
|
|
134
103
|
const check = opts.isParentAlive ?? defaultIsParentAlive;
|
|
135
|
-
const idleTimeoutMs = opts.idleTimeoutMs ?? idleTimeoutForEnv();
|
|
136
|
-
const now = opts.now ?? Date.now;
|
|
137
104
|
let stopped = false;
|
|
138
|
-
let lastActivity = now();
|
|
139
105
|
const shutdown = () => {
|
|
140
106
|
if (stopped)
|
|
141
107
|
return;
|
|
142
108
|
stopped = true;
|
|
143
109
|
opts.onShutdown();
|
|
144
110
|
};
|
|
145
|
-
|
|
146
|
-
lastActivity = now();
|
|
147
|
-
};
|
|
148
|
-
// P0: Periodic parent liveness check.
|
|
111
|
+
// P0: Periodic parent liveness check
|
|
149
112
|
const timer = setInterval(() => {
|
|
150
113
|
if (!check())
|
|
151
114
|
shutdown();
|
|
152
115
|
}, interval);
|
|
153
116
|
timer.unref();
|
|
154
|
-
// P0+: Idle shutdown (#565). Runs on its OWN tick — distinct from the
|
|
155
|
-
// 30 s parent-liveness poll — so a 15 min idle timeout actually reacts
|
|
156
|
-
// close to 15 min instead of "next 30 s tick after 15 min". Pick the
|
|
157
|
-
// tick as min(idleTimeoutMs / 6, 30 s) so a short timeout (e.g. 3 s in
|
|
158
|
-
// e2e tests, 60 s in dev) reacts within ~16 % of its window while a
|
|
159
|
-
// production 15 min timeout still polls every 30 s (cheap).
|
|
160
|
-
//
|
|
161
|
-
// Skipped on TTY because interactive dev sessions are expected to
|
|
162
|
-
// sit idle between commands, and also when idleTimeoutMs is 0 (env
|
|
163
|
-
// opt-out via CONTEXT_MODE_IDLE_TIMEOUT_MS=0).
|
|
164
|
-
let idleTimer = null;
|
|
165
|
-
if (idleTimeoutMs > 0 && !process.stdin.isTTY) {
|
|
166
|
-
const idleTick = Math.max(50, Math.min(Math.floor(idleTimeoutMs / 6), 30_000));
|
|
167
|
-
idleTimer = setInterval(() => {
|
|
168
|
-
if (now() - lastActivity > idleTimeoutMs)
|
|
169
|
-
shutdown();
|
|
170
|
-
}, idleTick);
|
|
171
|
-
idleTimer.unref();
|
|
172
|
-
}
|
|
173
117
|
// P0: OS signals — terminal close, kill, ctrl+c
|
|
174
118
|
const signals = ["SIGTERM", "SIGINT"];
|
|
175
119
|
if (process.platform !== "win32")
|
|
@@ -198,19 +142,11 @@ export function startLifecycleGuard(opts) {
|
|
|
198
142
|
if (!process.stdin.isTTY) {
|
|
199
143
|
process.stdin.on("end", onStdinEnd);
|
|
200
144
|
}
|
|
201
|
-
|
|
145
|
+
return () => {
|
|
202
146
|
stopped = true;
|
|
203
147
|
clearInterval(timer);
|
|
204
|
-
if (idleTimer)
|
|
205
|
-
clearInterval(idleTimer);
|
|
206
148
|
for (const sig of signals)
|
|
207
149
|
process.removeListener(sig, shutdown);
|
|
208
150
|
process.stdin.removeListener("end", onStdinEnd);
|
|
209
151
|
};
|
|
210
|
-
// Hybrid: callable for legacy `const cleanup = startLifecycleGuard(...)`
|
|
211
|
-
// sites, with `.recordActivity` / `.stop` properties for the new contract.
|
|
212
|
-
const handle = cleanup;
|
|
213
|
-
handle.recordActivity = recordActivity;
|
|
214
|
-
handle.stop = cleanup;
|
|
215
|
-
return handle;
|
|
216
152
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw TypeScript plugin entry point for context-mode.
|
|
3
|
+
*
|
|
4
|
+
* Exports an object with { id, name, configSchema, register(api) } for
|
|
5
|
+
* declarative metadata and config validation before code execution.
|
|
6
|
+
*
|
|
7
|
+
* register(api) registers:
|
|
8
|
+
* - before_tool_call hook — Routing enforcement (deny/modify/passthrough)
|
|
9
|
+
* - after_tool_call hook — Session event capture
|
|
10
|
+
* - command:new hook — Session initialization and cleanup
|
|
11
|
+
* - session_start hook — Re-key DB session to OpenClaw's session ID
|
|
12
|
+
* - before_compaction hook — Flush events to resume snapshot
|
|
13
|
+
* - after_compaction hook — Increment compact count
|
|
14
|
+
* - before_prompt_build (p=10) — Resume snapshot injection into system context
|
|
15
|
+
* - before_prompt_build (p=5) — Routing instruction injection into system context
|
|
16
|
+
* - context-mode engine — Context engine with compaction management
|
|
17
|
+
* - /ctx-stats command — Auto-reply command for session statistics
|
|
18
|
+
* - /ctx-doctor command — Auto-reply command for diagnostics
|
|
19
|
+
* - /ctx-upgrade command — Auto-reply command for upgrade
|
|
20
|
+
*
|
|
21
|
+
* Loaded by OpenClaw via: openclaw.extensions entry in package.json
|
|
22
|
+
*
|
|
23
|
+
* OpenClaw plugin paradigm:
|
|
24
|
+
* - Plugins export { id, name, configSchema, register(api) } for metadata
|
|
25
|
+
* - api.registerHook() for event-driven hooks
|
|
26
|
+
* - api.on() for typed lifecycle hooks
|
|
27
|
+
* - api.registerContextEngine() for compaction ownership
|
|
28
|
+
* - api.registerCommand() for auto-reply slash commands
|
|
29
|
+
* - Plugins run in-process with the Gateway (trusted code)
|
|
30
|
+
*/
|
|
31
|
+
import type { OpenClawToolDef } from "./openclaw/mcp-tools.js";
|
|
32
|
+
/** Context for auto-reply command handlers. */
|
|
33
|
+
interface CommandContext {
|
|
34
|
+
senderId?: string;
|
|
35
|
+
channel?: string;
|
|
36
|
+
isAuthorizedSender?: boolean;
|
|
37
|
+
args?: string;
|
|
38
|
+
commandBody?: string;
|
|
39
|
+
config?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
/** OpenClaw plugin API provided to the register function. */
|
|
42
|
+
interface OpenClawPluginApi {
|
|
43
|
+
registerHook(event: string, handler: (...args: unknown[]) => unknown, meta: {
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
}): void;
|
|
47
|
+
/**
|
|
48
|
+
* Register a typed lifecycle hook.
|
|
49
|
+
* Supported names: "session_start", "before_compaction", "after_compaction",
|
|
50
|
+
* "before_prompt_build"
|
|
51
|
+
*/
|
|
52
|
+
on(event: string, handler: (...args: unknown[]) => unknown, opts?: {
|
|
53
|
+
priority?: number;
|
|
54
|
+
}): void;
|
|
55
|
+
registerContextEngine(id: string, factory: () => ContextEngineInstance): void;
|
|
56
|
+
registerCommand?(cmd: {
|
|
57
|
+
name: string;
|
|
58
|
+
description: string;
|
|
59
|
+
acceptsArgs?: boolean;
|
|
60
|
+
requireAuth?: boolean;
|
|
61
|
+
handler: (ctx: CommandContext) => {
|
|
62
|
+
text: string;
|
|
63
|
+
} | Promise<{
|
|
64
|
+
text: string;
|
|
65
|
+
}>;
|
|
66
|
+
}): void;
|
|
67
|
+
registerCli?(factory: (ctx: {
|
|
68
|
+
program: unknown;
|
|
69
|
+
}) => void, meta: {
|
|
70
|
+
commands: string[];
|
|
71
|
+
}): void;
|
|
72
|
+
/**
|
|
73
|
+
* Register an agent tool (OpenClaw native registerTool) — see
|
|
74
|
+
* refs/platforms/openclaw/docs/plugins/building-plugins.md:116. Optional in
|
|
75
|
+
* the type so we degrade silently on legacy hosts that pre-date this API.
|
|
76
|
+
*/
|
|
77
|
+
registerTool?(tool: OpenClawToolDef, opts?: {
|
|
78
|
+
optional?: boolean;
|
|
79
|
+
}): void;
|
|
80
|
+
logger?: {
|
|
81
|
+
info: (...args: unknown[]) => void;
|
|
82
|
+
error: (...args: unknown[]) => void;
|
|
83
|
+
debug?: (...args: unknown[]) => void;
|
|
84
|
+
warn?: (...args: unknown[]) => void;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Context engine instance returned by the factory. */
|
|
88
|
+
interface ContextEngineInstance {
|
|
89
|
+
info: {
|
|
90
|
+
id: string;
|
|
91
|
+
name: string;
|
|
92
|
+
ownsCompaction: boolean;
|
|
93
|
+
};
|
|
94
|
+
ingest(data: unknown): Promise<{
|
|
95
|
+
ingested: boolean;
|
|
96
|
+
}>;
|
|
97
|
+
assemble(ctx: {
|
|
98
|
+
messages: unknown[];
|
|
99
|
+
}): Promise<{
|
|
100
|
+
messages: unknown[];
|
|
101
|
+
estimatedTokens: number;
|
|
102
|
+
}>;
|
|
103
|
+
compact(): Promise<{
|
|
104
|
+
ok: boolean;
|
|
105
|
+
compacted: boolean;
|
|
106
|
+
}>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* OpenClaw plugin definition. The object form provides declarative metadata
|
|
110
|
+
* (id, name, configSchema) that OpenClaw can read without executing code.
|
|
111
|
+
* register() is called once per agent session with a fresh api object.
|
|
112
|
+
* Each call creates isolated closures (db, sessionId, hooks) — no shared state.
|
|
113
|
+
*/
|
|
114
|
+
declare const _default: {
|
|
115
|
+
id: string;
|
|
116
|
+
name: string;
|
|
117
|
+
configSchema: {
|
|
118
|
+
type: "object";
|
|
119
|
+
properties: {
|
|
120
|
+
enabled: {
|
|
121
|
+
type: "boolean";
|
|
122
|
+
default: boolean;
|
|
123
|
+
description: string;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
additionalProperties: boolean;
|
|
127
|
+
};
|
|
128
|
+
register(api: OpenClawPluginApi): void;
|
|
129
|
+
};
|
|
130
|
+
export default _default;
|