context-mode 0.9.21 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/hooks/hooks.json +46 -4
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -4
- package/README.md +377 -191
- package/build/adapters/claude-code/config.d.ts +8 -0
- package/build/adapters/claude-code/config.js +8 -0
- package/build/adapters/claude-code/hooks.d.ts +53 -0
- package/build/adapters/claude-code/hooks.js +88 -0
- package/build/adapters/claude-code/index.d.ts +50 -0
- package/build/adapters/claude-code/index.js +523 -0
- package/build/adapters/codex/config.d.ts +8 -0
- package/build/adapters/codex/config.js +8 -0
- package/build/adapters/codex/hooks.d.ts +21 -0
- package/build/adapters/codex/hooks.js +27 -0
- package/build/adapters/codex/index.d.ts +44 -0
- package/build/adapters/codex/index.js +223 -0
- package/build/adapters/detect.d.ts +26 -0
- package/build/adapters/detect.js +131 -0
- package/build/adapters/gemini-cli/config.d.ts +8 -0
- package/build/adapters/gemini-cli/config.js +8 -0
- package/build/adapters/gemini-cli/hooks.d.ts +44 -0
- package/build/adapters/gemini-cli/hooks.js +64 -0
- package/build/adapters/gemini-cli/index.d.ts +57 -0
- package/build/adapters/gemini-cli/index.js +468 -0
- package/build/adapters/opencode/config.d.ts +8 -0
- package/build/adapters/opencode/config.js +8 -0
- package/build/adapters/opencode/hooks.d.ts +38 -0
- package/build/adapters/opencode/hooks.js +50 -0
- package/build/adapters/opencode/index.d.ts +52 -0
- package/build/adapters/opencode/index.js +386 -0
- package/build/adapters/types.d.ts +218 -0
- package/build/adapters/types.js +13 -0
- package/build/adapters/vscode-copilot/config.d.ts +8 -0
- package/build/adapters/vscode-copilot/config.js +8 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +49 -0
- package/build/adapters/vscode-copilot/hooks.js +76 -0
- package/build/adapters/vscode-copilot/index.d.ts +58 -0
- package/build/adapters/vscode-copilot/index.js +512 -0
- package/build/cli.d.ts +9 -6
- package/build/cli.js +133 -423
- package/build/db-base.d.ts +84 -0
- package/build/db-base.js +128 -0
- package/build/executor.d.ts +6 -7
- package/build/executor.js +111 -51
- package/build/opencode-plugin.d.ts +37 -0
- package/build/opencode-plugin.js +118 -0
- package/build/runtime.js +1 -1
- package/build/server.js +436 -117
- package/build/session/db.d.ts +110 -0
- package/build/session/db.js +285 -0
- package/build/session/extract.d.ts +51 -0
- package/build/session/extract.js +407 -0
- package/build/session/snapshot.d.ts +70 -0
- package/build/session/snapshot.js +309 -0
- package/build/store.d.ts +4 -22
- package/build/store.js +67 -55
- package/build/truncate.d.ts +59 -0
- package/build/truncate.js +157 -0
- package/build/types.d.ts +101 -0
- package/build/types.js +20 -0
- package/configs/claude-code/CLAUDE.md +62 -0
- package/configs/codex/AGENTS.md +58 -0
- package/configs/codex/config.toml +5 -0
- package/configs/gemini-cli/GEMINI.md +58 -0
- package/configs/gemini-cli/mcp.json +7 -0
- package/configs/gemini-cli/settings.json +49 -0
- package/configs/opencode/AGENTS.md +58 -0
- package/configs/opencode/opencode.json +10 -0
- package/configs/vscode-copilot/copilot-instructions.md +58 -0
- package/configs/vscode-copilot/hooks.json +16 -0
- package/configs/vscode-copilot/mcp.json +8 -0
- package/hooks/core/formatters.mjs +86 -0
- package/hooks/core/routing.mjs +262 -0
- package/hooks/core/stdin.mjs +19 -0
- package/hooks/formatters/claude-code.mjs +57 -0
- package/hooks/formatters/gemini-cli.mjs +55 -0
- package/hooks/formatters/vscode-copilot.mjs +55 -0
- package/hooks/gemini-cli/aftertool.mjs +58 -0
- package/hooks/gemini-cli/beforetool.mjs +25 -0
- package/hooks/gemini-cli/precompress.mjs +51 -0
- package/hooks/gemini-cli/sessionstart.mjs +117 -0
- package/hooks/hooks.json +46 -4
- package/hooks/posttooluse.mjs +53 -0
- package/hooks/precompact.mjs +55 -0
- package/hooks/pretooluse.mjs +23 -266
- package/hooks/routing-block.mjs +19 -6
- package/hooks/session-directive.mjs +353 -0
- package/hooks/session-helpers.mjs +112 -0
- package/hooks/sessionstart.mjs +123 -16
- package/hooks/userpromptsubmit.mjs +58 -0
- package/hooks/vscode-copilot/posttooluse.mjs +58 -0
- package/hooks/vscode-copilot/precompact.mjs +51 -0
- package/hooks/vscode-copilot/pretooluse.mjs +25 -0
- package/hooks/vscode-copilot/sessionstart.mjs +115 -0
- package/package.json +20 -17
- package/skills/context-mode/SKILL.md +49 -49
- package/skills/{doctor → ctx-doctor}/SKILL.md +3 -3
- package/skills/{stats → ctx-stats}/SKILL.md +3 -3
- package/skills/{upgrade → ctx-upgrade}/SKILL.md +3 -3
- package/start.mjs +47 -0
- package/hooks/pretooluse.sh +0 -147
- package/server.bundle.mjs +0 -341
package/build/cli.js
CHANGED
|
@@ -3,30 +3,70 @@
|
|
|
3
3
|
* context-mode CLI
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* context-mode
|
|
7
|
-
* context-mode
|
|
8
|
-
* context-mode
|
|
9
|
-
* context-mode
|
|
10
|
-
*
|
|
6
|
+
* context-mode → Start MCP server (stdio)
|
|
7
|
+
* context-mode doctor → Diagnose runtime issues, hooks, FTS5, version
|
|
8
|
+
* context-mode upgrade → Fix hooks, permissions, and settings
|
|
9
|
+
* context-mode hook <platform> <event> → Dispatch a hook script (used by platform hook configs)
|
|
10
|
+
*
|
|
11
|
+
* Platform auto-detection: CLI detects which platform is running
|
|
12
|
+
* (Claude Code, Gemini CLI, OpenCode, etc.) and uses the appropriate adapter.
|
|
11
13
|
*/
|
|
12
14
|
import * as p from "@clack/prompts";
|
|
13
15
|
import color from "picocolors";
|
|
14
16
|
import { execSync } from "node:child_process";
|
|
15
|
-
import { readFileSync,
|
|
16
|
-
import { resolve, dirname } from "node:path";
|
|
17
|
+
import { readFileSync, cpSync, accessSync, readdirSync, rmSync, constants } from "node:fs";
|
|
18
|
+
import { resolve, dirname, join } from "node:path";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
17
20
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { homedir } from "node:os";
|
|
19
21
|
import { detectRuntimes, getRuntimeSummary, hasBunRuntime, getAvailableLanguages, } from "./runtime.js";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// ── Adapter imports ──────────────────────────────────────
|
|
23
|
+
import { detectPlatform, getAdapter } from "./adapters/detect.js";
|
|
24
|
+
/* -------------------------------------------------------
|
|
25
|
+
* Hook dispatcher — `context-mode hook <platform> <event>`
|
|
26
|
+
* ------------------------------------------------------- */
|
|
27
|
+
const HOOK_MAP = {
|
|
28
|
+
"claude-code": {
|
|
29
|
+
pretooluse: "hooks/pretooluse.mjs",
|
|
30
|
+
posttooluse: "hooks/posttooluse.mjs",
|
|
31
|
+
precompact: "hooks/precompact.mjs",
|
|
32
|
+
sessionstart: "hooks/sessionstart.mjs",
|
|
33
|
+
userpromptsubmit: "hooks/userpromptsubmit.mjs",
|
|
34
|
+
},
|
|
35
|
+
"gemini-cli": {
|
|
36
|
+
beforetool: "hooks/gemini-cli/beforetool.mjs",
|
|
37
|
+
aftertool: "hooks/gemini-cli/aftertool.mjs",
|
|
38
|
+
precompress: "hooks/gemini-cli/precompress.mjs",
|
|
39
|
+
sessionstart: "hooks/gemini-cli/sessionstart.mjs",
|
|
40
|
+
},
|
|
41
|
+
"vscode-copilot": {
|
|
42
|
+
pretooluse: "hooks/vscode-copilot/pretooluse.mjs",
|
|
43
|
+
posttooluse: "hooks/vscode-copilot/posttooluse.mjs",
|
|
44
|
+
precompact: "hooks/vscode-copilot/precompact.mjs",
|
|
45
|
+
sessionstart: "hooks/vscode-copilot/sessionstart.mjs",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
async function hookDispatch(platform, event) {
|
|
49
|
+
const scriptPath = HOOK_MAP[platform]?.[event];
|
|
50
|
+
if (!scriptPath) {
|
|
51
|
+
console.error(`Unknown hook: ${platform}/${event}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const pluginRoot = getPluginRoot();
|
|
55
|
+
await import(join(pluginRoot, scriptPath));
|
|
23
56
|
}
|
|
24
|
-
|
|
57
|
+
/* -------------------------------------------------------
|
|
58
|
+
* Entry point
|
|
59
|
+
* ------------------------------------------------------- */
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
if (args[0] === "doctor") {
|
|
25
62
|
doctor().then((code) => process.exit(code));
|
|
26
63
|
}
|
|
27
64
|
else if (args[0] === "upgrade") {
|
|
28
65
|
upgrade();
|
|
29
66
|
}
|
|
67
|
+
else if (args[0] === "hook") {
|
|
68
|
+
hookDispatch(args[1], args[2]);
|
|
69
|
+
}
|
|
30
70
|
else {
|
|
31
71
|
// Default: start MCP server
|
|
32
72
|
import("./server.js");
|
|
@@ -34,26 +74,15 @@ else {
|
|
|
34
74
|
/* -------------------------------------------------------
|
|
35
75
|
* Shared helpers
|
|
36
76
|
* ------------------------------------------------------- */
|
|
77
|
+
/** Normalize Windows backslash paths to forward slashes for Bash (MSYS2) compatibility. */
|
|
78
|
+
export function toUnixPath(p) {
|
|
79
|
+
return p.replace(/\\/g, "/");
|
|
80
|
+
}
|
|
37
81
|
function getPluginRoot() {
|
|
38
82
|
const __filename = fileURLToPath(import.meta.url);
|
|
39
83
|
const __dirname = dirname(__filename);
|
|
40
84
|
return resolve(__dirname, "..");
|
|
41
85
|
}
|
|
42
|
-
function getSettingsPath() {
|
|
43
|
-
return resolve(homedir(), ".claude", "settings.json");
|
|
44
|
-
}
|
|
45
|
-
function readSettings() {
|
|
46
|
-
try {
|
|
47
|
-
const raw = readFileSync(getSettingsPath(), "utf-8");
|
|
48
|
-
return JSON.parse(raw);
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function getHookScriptPath() {
|
|
55
|
-
return resolve(getPluginRoot(), "hooks", "pretooluse.mjs");
|
|
56
|
-
}
|
|
57
86
|
function getLocalVersion() {
|
|
58
87
|
try {
|
|
59
88
|
const pkg = JSON.parse(readFileSync(resolve(getPluginRoot(), "package.json"), "utf-8"));
|
|
@@ -75,71 +104,18 @@ async function fetchLatestVersion() {
|
|
|
75
104
|
return "unknown";
|
|
76
105
|
}
|
|
77
106
|
}
|
|
78
|
-
function getMarketplaceVersion() {
|
|
79
|
-
// Primary: read from installed_plugins.json (source of truth for Claude Code)
|
|
80
|
-
try {
|
|
81
|
-
const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
82
|
-
const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8"));
|
|
83
|
-
const plugins = ipRaw.plugins ?? {};
|
|
84
|
-
for (const [key, entries] of Object.entries(plugins)) {
|
|
85
|
-
if (!key.toLowerCase().includes("context-mode"))
|
|
86
|
-
continue;
|
|
87
|
-
const arr = entries;
|
|
88
|
-
if (arr.length > 0 && typeof arr[0].version === "string") {
|
|
89
|
-
return arr[0].version;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
catch { /* fallback below */ }
|
|
94
|
-
// Fallback: read from own package.json
|
|
95
|
-
const localVer = getLocalVersion();
|
|
96
|
-
if (localVer !== "unknown")
|
|
97
|
-
return localVer;
|
|
98
|
-
// Last resort: scan common plugin cache locations
|
|
99
|
-
const bases = [
|
|
100
|
-
resolve(homedir(), ".claude"),
|
|
101
|
-
resolve(homedir(), ".config", "claude"),
|
|
102
|
-
];
|
|
103
|
-
for (const base of bases) {
|
|
104
|
-
const cacheDir = resolve(base, "plugins", "cache", "claude-context-mode", "context-mode");
|
|
105
|
-
try {
|
|
106
|
-
const entries = readdirSync(cacheDir);
|
|
107
|
-
const versions = entries
|
|
108
|
-
.filter((e) => /^\d+\.\d+\.\d+/.test(e))
|
|
109
|
-
.sort((a, b) => {
|
|
110
|
-
const pa = a.split(".").map(Number);
|
|
111
|
-
const pb = b.split(".").map(Number);
|
|
112
|
-
for (let i = 0; i < 3; i++) {
|
|
113
|
-
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
114
|
-
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
115
|
-
}
|
|
116
|
-
return 0;
|
|
117
|
-
});
|
|
118
|
-
if (versions.length > 0)
|
|
119
|
-
return versions[versions.length - 1];
|
|
120
|
-
}
|
|
121
|
-
catch { /* continue */ }
|
|
122
|
-
}
|
|
123
|
-
return "not installed";
|
|
124
|
-
}
|
|
125
|
-
function semverGt(a, b) {
|
|
126
|
-
const pa = a.split(".").map(Number);
|
|
127
|
-
const pb = b.split(".").map(Number);
|
|
128
|
-
for (let i = 0; i < 3; i++) {
|
|
129
|
-
if ((pa[i] ?? 0) > (pb[i] ?? 0))
|
|
130
|
-
return true;
|
|
131
|
-
if ((pa[i] ?? 0) < (pb[i] ?? 0))
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
107
|
/* -------------------------------------------------------
|
|
137
|
-
* Doctor
|
|
108
|
+
* Doctor — adapter-aware diagnostics
|
|
138
109
|
* ------------------------------------------------------- */
|
|
139
110
|
async function doctor() {
|
|
140
111
|
if (process.stdout.isTTY)
|
|
141
112
|
console.clear();
|
|
113
|
+
// Detect platform
|
|
114
|
+
const detection = detectPlatform();
|
|
115
|
+
const adapter = await getAdapter(detection.platform);
|
|
142
116
|
p.intro(color.bgMagenta(color.white(" context-mode doctor ")));
|
|
117
|
+
p.log.info(`Platform: ${color.cyan(adapter.name)}` +
|
|
118
|
+
color.dim(` (${detection.confidence} confidence — ${detection.reason})`));
|
|
143
119
|
let criticalFails = 0;
|
|
144
120
|
const s = p.spinner();
|
|
145
121
|
s.start("Running diagnostics");
|
|
@@ -152,7 +128,7 @@ async function doctor() {
|
|
|
152
128
|
catch {
|
|
153
129
|
s.stop("Diagnostics partial");
|
|
154
130
|
p.log.warn(color.yellow("Could not detect runtimes") + color.dim(" — module may be missing, restart session after upgrade"));
|
|
155
|
-
p.outro(color.yellow("Doctor could not fully run — try again after restarting
|
|
131
|
+
p.outro(color.yellow("Doctor could not fully run — try again after restarting"));
|
|
156
132
|
return 1;
|
|
157
133
|
}
|
|
158
134
|
s.stop("Diagnostics complete");
|
|
@@ -208,55 +184,23 @@ async function doctor() {
|
|
|
208
184
|
p.log.error(color.red("Server test: FAIL") + ` — ${message}`);
|
|
209
185
|
}
|
|
210
186
|
}
|
|
211
|
-
// Hooks
|
|
212
|
-
p.log.step(
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (preToolUse && preToolUse.length > 0) {
|
|
219
|
-
const hasCorrectHook = preToolUse.some((entry) => entry.hooks?.some((h) => h.command?.includes("pretooluse.mjs")));
|
|
220
|
-
if (hasCorrectHook) {
|
|
221
|
-
p.log.success(color.green("Hooks installed: PASS") + " — PreToolUse hook configured");
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
p.log.error(color.red("Hooks installed: FAIL") +
|
|
225
|
-
" — PreToolUse exists but does not point to pretooluse.mjs" +
|
|
226
|
-
color.dim("\n Run: npx context-mode upgrade"));
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
p.log.error(color.red("Hooks installed: FAIL") +
|
|
231
|
-
" — No PreToolUse hooks found" +
|
|
232
|
-
color.dim("\n Run: npx context-mode upgrade"));
|
|
233
|
-
}
|
|
234
|
-
// Check SessionStart hook
|
|
235
|
-
const sessionStart = hooks?.SessionStart;
|
|
236
|
-
if (sessionStart && sessionStart.length > 0) {
|
|
237
|
-
const hasSessionHook = sessionStart.some((entry) => entry.hooks?.some((h) => h.command?.includes("sessionstart.mjs")));
|
|
238
|
-
if (hasSessionHook) {
|
|
239
|
-
p.log.success(color.green("SessionStart hook: PASS") + " — SessionStart hook configured");
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
p.log.error(color.red("SessionStart hook: FAIL") +
|
|
243
|
-
" — SessionStart exists but does not point to sessionstart.mjs" +
|
|
244
|
-
color.dim("\n Run: npx context-mode upgrade"));
|
|
245
|
-
}
|
|
187
|
+
// Hooks — adapter-aware validation
|
|
188
|
+
p.log.step(`Checking ${adapter.name} hooks configuration...`);
|
|
189
|
+
const pluginRoot = getPluginRoot();
|
|
190
|
+
const hookResults = adapter.validateHooks(pluginRoot);
|
|
191
|
+
for (const result of hookResults) {
|
|
192
|
+
if (result.status === "pass") {
|
|
193
|
+
p.log.success(color.green(`${result.check}: PASS`) + ` — ${result.message}`);
|
|
246
194
|
}
|
|
247
195
|
else {
|
|
248
|
-
p.log.error(color.red(
|
|
249
|
-
|
|
250
|
-
color.dim(
|
|
196
|
+
p.log.error(color.red(`${result.check}: FAIL`) +
|
|
197
|
+
` — ${result.message}` +
|
|
198
|
+
(result.fix ? color.dim(`\n Run: ${result.fix}`) : ""));
|
|
251
199
|
}
|
|
252
200
|
}
|
|
253
|
-
else {
|
|
254
|
-
p.log.error(color.red("Hooks installed: FAIL") +
|
|
255
|
-
" — Could not read ~/.claude/settings.json" +
|
|
256
|
-
color.dim("\n Run: npx context-mode upgrade"));
|
|
257
|
-
}
|
|
258
201
|
// Hook script exists
|
|
259
202
|
p.log.step("Checking hook script...");
|
|
203
|
+
const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.mjs");
|
|
260
204
|
try {
|
|
261
205
|
accessSync(hookScriptPath, constants.R_OK);
|
|
262
206
|
p.log.success(color.green("Hook script exists: PASS") + color.dim(` — ${hookScriptPath}`));
|
|
@@ -265,30 +209,15 @@ async function doctor() {
|
|
|
265
209
|
p.log.error(color.red("Hook script exists: FAIL") +
|
|
266
210
|
color.dim(` — not found at ${hookScriptPath}`));
|
|
267
211
|
}
|
|
268
|
-
// Plugin
|
|
269
|
-
p.log.step(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const pluginKey = Object.keys(enabledPlugins).find((k) => k.startsWith("context-mode"));
|
|
274
|
-
if (pluginKey && enabledPlugins[pluginKey]) {
|
|
275
|
-
p.log.success(color.green("Plugin enabled: PASS") + color.dim(` — ${pluginKey}`));
|
|
276
|
-
}
|
|
277
|
-
else {
|
|
278
|
-
p.log.warn(color.yellow("Plugin enabled: WARN") +
|
|
279
|
-
" — context-mode not in enabledPlugins" +
|
|
280
|
-
color.dim(" (might be using standalone MCP mode)"));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
p.log.warn(color.yellow("Plugin enabled: WARN") +
|
|
285
|
-
" — no enabledPlugins section found" +
|
|
286
|
-
color.dim(" (might be using standalone MCP mode)"));
|
|
287
|
-
}
|
|
212
|
+
// Plugin registration — adapter-aware
|
|
213
|
+
p.log.step(`Checking ${adapter.name} plugin registration...`);
|
|
214
|
+
const pluginCheck = adapter.checkPluginRegistration();
|
|
215
|
+
if (pluginCheck.status === "pass") {
|
|
216
|
+
p.log.success(color.green("Plugin enabled: PASS") + color.dim(` — ${pluginCheck.message}`));
|
|
288
217
|
}
|
|
289
218
|
else {
|
|
290
219
|
p.log.warn(color.yellow("Plugin enabled: WARN") +
|
|
291
|
-
|
|
220
|
+
` — ${pluginCheck.message}`);
|
|
292
221
|
}
|
|
293
222
|
// FTS5 / better-sqlite3
|
|
294
223
|
p.log.step("Checking FTS5 / better-sqlite3...");
|
|
@@ -319,12 +248,11 @@ async function doctor() {
|
|
|
319
248
|
color.dim("\n Try: npm rebuild better-sqlite3"));
|
|
320
249
|
}
|
|
321
250
|
}
|
|
322
|
-
// Version check
|
|
251
|
+
// Version check — adapter-aware
|
|
323
252
|
p.log.step("Checking versions...");
|
|
324
253
|
const localVersion = getLocalVersion();
|
|
325
254
|
const latestVersion = await fetchLatestVersion();
|
|
326
|
-
const
|
|
327
|
-
// npm / MCP version
|
|
255
|
+
const installedVersion = adapter.getInstalledVersion();
|
|
328
256
|
if (latestVersion === "unknown") {
|
|
329
257
|
p.log.warn(color.yellow("npm (MCP): WARN") +
|
|
330
258
|
` — local v${localVersion}, could not reach npm registry`);
|
|
@@ -336,24 +264,23 @@ async function doctor() {
|
|
|
336
264
|
else {
|
|
337
265
|
p.log.warn(color.yellow("npm (MCP): WARN") +
|
|
338
266
|
` — local v${localVersion}, latest v${latestVersion}` +
|
|
339
|
-
color.dim("\n Run: /context-mode:upgrade"));
|
|
267
|
+
color.dim("\n Run: /context-mode:ctx-upgrade"));
|
|
340
268
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
p.log.info(color.dim("Marketplace: not installed") +
|
|
269
|
+
if (installedVersion === "not installed") {
|
|
270
|
+
p.log.info(color.dim(`${adapter.name}: not installed`) +
|
|
344
271
|
" — using standalone MCP mode");
|
|
345
272
|
}
|
|
346
|
-
else if (latestVersion !== "unknown" &&
|
|
347
|
-
p.log.success(color.green(
|
|
348
|
-
` — v${
|
|
273
|
+
else if (latestVersion !== "unknown" && installedVersion === latestVersion) {
|
|
274
|
+
p.log.success(color.green(`${adapter.name}: PASS`) +
|
|
275
|
+
` — v${installedVersion}`);
|
|
349
276
|
}
|
|
350
277
|
else if (latestVersion !== "unknown") {
|
|
351
|
-
p.log.warn(color.yellow(
|
|
352
|
-
` — v${
|
|
353
|
-
color.dim("\n Run: /context-mode:upgrade"));
|
|
278
|
+
p.log.warn(color.yellow(`${adapter.name}: WARN`) +
|
|
279
|
+
` — v${installedVersion}, latest v${latestVersion}` +
|
|
280
|
+
color.dim("\n Run: /context-mode:ctx-upgrade"));
|
|
354
281
|
}
|
|
355
282
|
else {
|
|
356
|
-
p.log.info(
|
|
283
|
+
p.log.info(`${adapter.name}: v${installedVersion}` +
|
|
357
284
|
color.dim(" — could not verify against npm registry"));
|
|
358
285
|
}
|
|
359
286
|
// Summary
|
|
@@ -367,26 +294,29 @@ async function doctor() {
|
|
|
367
294
|
return 0;
|
|
368
295
|
}
|
|
369
296
|
/* -------------------------------------------------------
|
|
370
|
-
* Upgrade
|
|
297
|
+
* Upgrade — adapter-aware hook configuration
|
|
371
298
|
* ------------------------------------------------------- */
|
|
372
299
|
async function upgrade() {
|
|
373
300
|
if (process.stdout.isTTY)
|
|
374
301
|
console.clear();
|
|
302
|
+
// Detect platform
|
|
303
|
+
const detection = detectPlatform();
|
|
304
|
+
const adapter = await getAdapter(detection.platform);
|
|
375
305
|
p.intro(color.bgCyan(color.black(" context-mode upgrade ")));
|
|
306
|
+
p.log.info(`Platform: ${color.cyan(adapter.name)}` +
|
|
307
|
+
color.dim(` (${detection.confidence} confidence)`));
|
|
376
308
|
let pluginRoot = getPluginRoot();
|
|
377
|
-
const settingsPath = getSettingsPath();
|
|
378
309
|
const changes = [];
|
|
379
310
|
const s = p.spinner();
|
|
380
|
-
// Step 1: Pull latest from GitHub
|
|
311
|
+
// Step 1: Pull latest from GitHub
|
|
381
312
|
p.log.step("Pulling latest from GitHub...");
|
|
382
313
|
const localVersion = getLocalVersion();
|
|
383
|
-
const tmpDir =
|
|
384
|
-
s.start("Cloning mksglu/
|
|
314
|
+
const tmpDir = join(tmpdir(), `context-mode-upgrade-${Date.now()}`);
|
|
315
|
+
s.start("Cloning mksglu/context-mode");
|
|
385
316
|
try {
|
|
386
|
-
execSync(`git clone --depth 1 https://github.com/mksglu/
|
|
317
|
+
execSync(`git clone --depth 1 https://github.com/mksglu/context-mode.git "${tmpDir}"`, { stdio: "pipe", timeout: 30000 });
|
|
387
318
|
s.stop("Downloaded");
|
|
388
319
|
const srcDir = tmpDir;
|
|
389
|
-
// Read new version
|
|
390
320
|
const newPkg = JSON.parse(readFileSync(resolve(srcDir, "package.json"), "utf-8"));
|
|
391
321
|
const newVersion = newPkg.version ?? "unknown";
|
|
392
322
|
if (newVersion === localVersion) {
|
|
@@ -408,9 +338,8 @@ async function upgrade() {
|
|
|
408
338
|
timeout: 30000,
|
|
409
339
|
});
|
|
410
340
|
s.stop("Built successfully");
|
|
411
|
-
// Step 3: Update in-place
|
|
341
|
+
// Step 3: Update in-place
|
|
412
342
|
s.start("Updating files in-place");
|
|
413
|
-
// Clean stale version dirs from previous upgrade attempts
|
|
414
343
|
const cacheParentMatch = pluginRoot.match(/^(.*[\\/]plugins[\\/]cache[\\/][^\\/]+[\\/][^\\/]+[\\/])/);
|
|
415
344
|
if (cacheParentMatch) {
|
|
416
345
|
const cacheParent = cacheParentMatch[1];
|
|
@@ -429,7 +358,6 @@ async function upgrade() {
|
|
|
429
358
|
}
|
|
430
359
|
catch { /* parent may not exist */ }
|
|
431
360
|
}
|
|
432
|
-
// Copy new files over old ones — same path, no registry update needed
|
|
433
361
|
const items = [
|
|
434
362
|
"build", "src", "hooks", "skills", ".claude-plugin",
|
|
435
363
|
"start.mjs", "server.bundle.mjs", "package.json", ".mcp.json",
|
|
@@ -442,24 +370,10 @@ async function upgrade() {
|
|
|
442
370
|
catch { /* some files may not exist in source */ }
|
|
443
371
|
}
|
|
444
372
|
s.stop(color.green(`Updated in-place to v${newVersion}`));
|
|
445
|
-
// Fix registry
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
for (const [key, entries] of Object.entries(ipRaw.plugins || {})) {
|
|
450
|
-
if (!key.toLowerCase().includes("context-mode"))
|
|
451
|
-
continue;
|
|
452
|
-
for (const entry of entries) {
|
|
453
|
-
entry.installPath = pluginRoot;
|
|
454
|
-
entry.version = newVersion;
|
|
455
|
-
entry.lastUpdated = new Date().toISOString();
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
writeFileSync(ipPath, JSON.stringify(ipRaw, null, 2) + "\n", "utf-8");
|
|
459
|
-
p.log.info(color.dim(" Registry synced to " + pluginRoot));
|
|
460
|
-
}
|
|
461
|
-
catch { /* best effort */ }
|
|
462
|
-
// Install production deps (rebuild native modules if needed)
|
|
373
|
+
// Fix registry — adapter-aware
|
|
374
|
+
adapter.updatePluginRegistry(pluginRoot, newVersion);
|
|
375
|
+
p.log.info(color.dim(" Registry synced to " + pluginRoot));
|
|
376
|
+
// Install production deps
|
|
463
377
|
s.start("Installing production dependencies");
|
|
464
378
|
execSync("npm install --production --no-audit --no-fund", {
|
|
465
379
|
cwd: pluginRoot,
|
|
@@ -467,10 +381,10 @@ async function upgrade() {
|
|
|
467
381
|
timeout: 60000,
|
|
468
382
|
});
|
|
469
383
|
s.stop("Dependencies ready");
|
|
470
|
-
// Update global npm
|
|
384
|
+
// Update global npm
|
|
471
385
|
s.start("Updating npm global package");
|
|
472
386
|
try {
|
|
473
|
-
execSync(`npm install -g "${pluginRoot}" --no-audit --no-fund
|
|
387
|
+
execSync(`npm install -g "${pluginRoot}" --no-audit --no-fund`, {
|
|
474
388
|
stdio: "pipe",
|
|
475
389
|
timeout: 30000,
|
|
476
390
|
});
|
|
@@ -494,120 +408,40 @@ async function upgrade() {
|
|
|
494
408
|
s.stop(color.red("Update failed"));
|
|
495
409
|
p.log.error(color.red("GitHub pull failed") + ` — ${message}`);
|
|
496
410
|
p.log.info(color.dim("Continuing with hooks/settings fix..."));
|
|
497
|
-
// Cleanup on failure
|
|
498
411
|
try {
|
|
499
412
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
500
413
|
}
|
|
501
414
|
catch { /* ignore */ }
|
|
502
415
|
}
|
|
503
|
-
// Step 3: Backup settings
|
|
504
|
-
p.log.step(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const backupPath = settingsPath + ".bak";
|
|
508
|
-
copyFileSync(settingsPath, backupPath);
|
|
416
|
+
// Step 3: Backup settings — adapter-aware
|
|
417
|
+
p.log.step(`Backing up ${adapter.name} settings...`);
|
|
418
|
+
const backupPath = adapter.backupSettings();
|
|
419
|
+
if (backupPath) {
|
|
509
420
|
p.log.success(color.green("Backup created") + color.dim(" -> " + backupPath));
|
|
510
|
-
changes.push("Backed up settings
|
|
511
|
-
}
|
|
512
|
-
catch {
|
|
513
|
-
p.log.warn(color.yellow("No existing settings.json to backup") +
|
|
514
|
-
" — a new one will be created");
|
|
515
|
-
}
|
|
516
|
-
// Step 4: Fix hooks
|
|
517
|
-
p.log.step("Configuring PreToolUse hooks...");
|
|
518
|
-
const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.mjs");
|
|
519
|
-
const settings = readSettings() ?? {};
|
|
520
|
-
const desiredHookEntry = {
|
|
521
|
-
matcher: "Bash|Read|Grep|WebFetch|Task|mcp__plugin_context-mode_context-mode__execute|mcp__plugin_context-mode_context-mode__execute_file|mcp__plugin_context-mode_context-mode__batch_execute",
|
|
522
|
-
hooks: [
|
|
523
|
-
{
|
|
524
|
-
type: "command",
|
|
525
|
-
command: "node " + hookScriptPath,
|
|
526
|
-
},
|
|
527
|
-
],
|
|
528
|
-
};
|
|
529
|
-
const hooks = (settings.hooks ?? {});
|
|
530
|
-
const existingPreToolUse = hooks.PreToolUse;
|
|
531
|
-
if (existingPreToolUse && Array.isArray(existingPreToolUse)) {
|
|
532
|
-
const existingIdx = existingPreToolUse.findIndex((entry) => {
|
|
533
|
-
const entryHooks = entry.hooks;
|
|
534
|
-
return entryHooks?.some((h) => h.command?.includes("pretooluse.mjs"));
|
|
535
|
-
});
|
|
536
|
-
if (existingIdx >= 0) {
|
|
537
|
-
existingPreToolUse[existingIdx] = desiredHookEntry;
|
|
538
|
-
p.log.info(color.dim("Updated existing PreToolUse hook entry"));
|
|
539
|
-
changes.push("Updated existing PreToolUse hook entry");
|
|
540
|
-
}
|
|
541
|
-
else {
|
|
542
|
-
existingPreToolUse.push(desiredHookEntry);
|
|
543
|
-
p.log.info(color.dim("Added PreToolUse hook entry"));
|
|
544
|
-
changes.push("Added PreToolUse hook entry to existing hooks");
|
|
545
|
-
}
|
|
546
|
-
hooks.PreToolUse = existingPreToolUse;
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
hooks.PreToolUse = [desiredHookEntry];
|
|
550
|
-
p.log.info(color.dim("Created PreToolUse hooks section"));
|
|
551
|
-
changes.push("Created PreToolUse hooks section");
|
|
552
|
-
}
|
|
553
|
-
// --- SessionStart hook ---
|
|
554
|
-
p.log.step("Configuring SessionStart hook...");
|
|
555
|
-
const sessionHookScriptPath = resolve(pluginRoot, "hooks", "sessionstart.mjs");
|
|
556
|
-
const desiredSessionHookEntry = {
|
|
557
|
-
matcher: "",
|
|
558
|
-
hooks: [
|
|
559
|
-
{
|
|
560
|
-
type: "command",
|
|
561
|
-
command: "node " + sessionHookScriptPath,
|
|
562
|
-
},
|
|
563
|
-
],
|
|
564
|
-
};
|
|
565
|
-
const existingSessionStart = hooks.SessionStart;
|
|
566
|
-
if (existingSessionStart && Array.isArray(existingSessionStart)) {
|
|
567
|
-
const existingSessionIdx = existingSessionStart.findIndex((entry) => {
|
|
568
|
-
const entryHooks = entry.hooks;
|
|
569
|
-
return entryHooks?.some((h) => h.command?.includes("sessionstart.mjs"));
|
|
570
|
-
});
|
|
571
|
-
if (existingSessionIdx >= 0) {
|
|
572
|
-
existingSessionStart[existingSessionIdx] = desiredSessionHookEntry;
|
|
573
|
-
p.log.info(color.dim("Updated existing SessionStart hook entry"));
|
|
574
|
-
changes.push("Updated existing SessionStart hook entry");
|
|
575
|
-
}
|
|
576
|
-
else {
|
|
577
|
-
existingSessionStart.push(desiredSessionHookEntry);
|
|
578
|
-
p.log.info(color.dim("Added SessionStart hook entry"));
|
|
579
|
-
changes.push("Added SessionStart hook entry to existing hooks");
|
|
580
|
-
}
|
|
581
|
-
hooks.SessionStart = existingSessionStart;
|
|
421
|
+
changes.push("Backed up settings");
|
|
582
422
|
}
|
|
583
423
|
else {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
changes.push("Created SessionStart hooks section");
|
|
587
|
-
}
|
|
588
|
-
settings.hooks = hooks;
|
|
589
|
-
// Write updated settings
|
|
590
|
-
try {
|
|
591
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
592
|
-
p.log.success(color.green("Hooks configured") + color.dim(" -> " + settingsPath));
|
|
424
|
+
p.log.warn(color.yellow("No existing settings to backup") +
|
|
425
|
+
" — a new one will be created");
|
|
593
426
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
427
|
+
// Step 4: Configure hooks — adapter-aware
|
|
428
|
+
p.log.step(`Configuring ${adapter.name} hooks...`);
|
|
429
|
+
const hookChanges = adapter.configureAllHooks(pluginRoot);
|
|
430
|
+
for (const change of hookChanges) {
|
|
431
|
+
p.log.info(color.dim(` ${change}`));
|
|
432
|
+
changes.push(change);
|
|
599
433
|
}
|
|
600
|
-
|
|
434
|
+
p.log.success(color.green("Hooks configured") + color.dim(` — ${adapter.name}`));
|
|
435
|
+
// Step 5: Set hook script permissions — adapter-aware
|
|
601
436
|
p.log.step("Setting hook script permissions...");
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
changes.push("Set pretooluse.mjs as executable");
|
|
437
|
+
const permSet = adapter.setHookPermissions(pluginRoot);
|
|
438
|
+
if (permSet.length > 0) {
|
|
439
|
+
p.log.success(color.green("Permissions set") + color.dim(` — ${permSet.length} hook script(s)`));
|
|
440
|
+
changes.push(`Set ${permSet.length} hook scripts as executable`);
|
|
607
441
|
}
|
|
608
|
-
|
|
609
|
-
p.log.error(color.red("
|
|
610
|
-
color.dim(" — expected
|
|
442
|
+
else {
|
|
443
|
+
p.log.error(color.red("No hook scripts found") +
|
|
444
|
+
color.dim(" — expected in " + resolve(pluginRoot, "hooks")));
|
|
611
445
|
}
|
|
612
446
|
// Step 6: Report
|
|
613
447
|
if (changes.length > 0) {
|
|
@@ -616,7 +450,7 @@ async function upgrade() {
|
|
|
616
450
|
else {
|
|
617
451
|
p.log.info(color.dim("No changes were needed."));
|
|
618
452
|
}
|
|
619
|
-
// Step 7: Run doctor
|
|
453
|
+
// Step 7: Run doctor
|
|
620
454
|
p.log.step("Running doctor to verify...");
|
|
621
455
|
console.log();
|
|
622
456
|
try {
|
|
@@ -628,130 +462,6 @@ async function upgrade() {
|
|
|
628
462
|
}
|
|
629
463
|
catch {
|
|
630
464
|
p.log.warn(color.yellow("Doctor had warnings") +
|
|
631
|
-
color.dim(
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
/* -------------------------------------------------------
|
|
635
|
-
* Setup
|
|
636
|
-
* ------------------------------------------------------- */
|
|
637
|
-
async function setup() {
|
|
638
|
-
if (process.stdout.isTTY)
|
|
639
|
-
console.clear();
|
|
640
|
-
p.intro(color.bgCyan(color.black(" context-mode setup ")));
|
|
641
|
-
const s = p.spinner();
|
|
642
|
-
// Step 1: Detect runtimes
|
|
643
|
-
s.start("Detecting installed runtimes");
|
|
644
|
-
const runtimes = detectRuntimes();
|
|
645
|
-
const available = getAvailableLanguages(runtimes);
|
|
646
|
-
s.stop("Detected " + available.length + " languages");
|
|
647
|
-
// Show what's available
|
|
648
|
-
p.note(getRuntimeSummary(runtimes), "Detected Runtimes");
|
|
649
|
-
// Step 2: Check Bun
|
|
650
|
-
if (!hasBunRuntime()) {
|
|
651
|
-
p.log.warn(color.yellow("Bun is not installed.") +
|
|
652
|
-
" JS/TS will run with Node.js (3-5x slower).");
|
|
653
|
-
const installBun = await p.confirm({
|
|
654
|
-
message: "Would you like to install Bun for faster execution?",
|
|
655
|
-
initialValue: true,
|
|
656
|
-
});
|
|
657
|
-
if (p.isCancel(installBun)) {
|
|
658
|
-
p.cancel("Setup cancelled.");
|
|
659
|
-
process.exit(0);
|
|
660
|
-
}
|
|
661
|
-
if (installBun) {
|
|
662
|
-
s.start("Installing Bun");
|
|
663
|
-
try {
|
|
664
|
-
execSync("curl -fsSL https://bun.sh/install | bash", {
|
|
665
|
-
stdio: "pipe",
|
|
666
|
-
timeout: 60000,
|
|
667
|
-
});
|
|
668
|
-
s.stop(color.green("Bun installed successfully!"));
|
|
669
|
-
// Re-detect runtimes
|
|
670
|
-
const newRuntimes = detectRuntimes();
|
|
671
|
-
if (hasBunRuntime()) {
|
|
672
|
-
p.log.success("JavaScript and TypeScript will now use Bun " +
|
|
673
|
-
color.dim("(3-5x faster)"));
|
|
674
|
-
}
|
|
675
|
-
p.note(getRuntimeSummary(newRuntimes), "Updated Runtimes");
|
|
676
|
-
}
|
|
677
|
-
catch (err) {
|
|
678
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
679
|
-
s.stop(color.red("Failed to install Bun"));
|
|
680
|
-
p.log.error("Installation failed: " +
|
|
681
|
-
message +
|
|
682
|
-
"\nYou can install manually: curl -fsSL https://bun.sh/install | bash");
|
|
683
|
-
p.log.info(color.dim("Continuing with Node.js — everything will still work."));
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
else {
|
|
687
|
-
p.log.info(color.dim("No problem! Using Node.js. You can install Bun later: curl -fsSL https://bun.sh/install | bash"));
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
else {
|
|
691
|
-
p.log.success(color.green("Bun detected!") +
|
|
692
|
-
" JS/TS will run at maximum speed.");
|
|
693
|
-
}
|
|
694
|
-
// Step 3: Check optional runtimes
|
|
695
|
-
const missing = [];
|
|
696
|
-
if (!runtimes.python)
|
|
697
|
-
missing.push("Python (python3)");
|
|
698
|
-
if (!runtimes.ruby)
|
|
699
|
-
missing.push("Ruby (ruby)");
|
|
700
|
-
if (!runtimes.go)
|
|
701
|
-
missing.push("Go (go)");
|
|
702
|
-
if (!runtimes.php)
|
|
703
|
-
missing.push("PHP (php)");
|
|
704
|
-
if (!runtimes.r)
|
|
705
|
-
missing.push("R (Rscript)");
|
|
706
|
-
if (missing.length > 0) {
|
|
707
|
-
p.log.info(color.dim("Optional runtimes not found: " + missing.join(", ")));
|
|
708
|
-
p.log.info(color.dim("Install them to enable additional language support in context-mode."));
|
|
709
|
-
}
|
|
710
|
-
// Step 4: Installation instructions
|
|
711
|
-
const installMethod = await p.select({
|
|
712
|
-
message: "How would you like to configure context-mode?",
|
|
713
|
-
options: [
|
|
714
|
-
{
|
|
715
|
-
value: "claude-code",
|
|
716
|
-
label: "Claude Code (recommended)",
|
|
717
|
-
hint: "claude mcp add",
|
|
718
|
-
},
|
|
719
|
-
{
|
|
720
|
-
value: "manual",
|
|
721
|
-
label: "Show manual configuration",
|
|
722
|
-
hint: ".mcp.json",
|
|
723
|
-
},
|
|
724
|
-
{ value: "skip", label: "Skip — I'll configure later" },
|
|
725
|
-
],
|
|
726
|
-
});
|
|
727
|
-
if (p.isCancel(installMethod)) {
|
|
728
|
-
p.cancel("Setup cancelled.");
|
|
729
|
-
process.exit(0);
|
|
730
|
-
}
|
|
731
|
-
const serverPath = new URL("./server.js", import.meta.url).pathname;
|
|
732
|
-
if (installMethod === "claude-code") {
|
|
733
|
-
s.start("Adding to Claude Code");
|
|
734
|
-
try {
|
|
735
|
-
execSync(`claude mcp add context-mode -- node ${serverPath}`, { stdio: "pipe", timeout: 10000 });
|
|
736
|
-
s.stop(color.green("Added to Claude Code!"));
|
|
737
|
-
}
|
|
738
|
-
catch {
|
|
739
|
-
s.stop(color.yellow("Could not add automatically"));
|
|
740
|
-
p.log.info("Run manually:\n" +
|
|
741
|
-
color.cyan(` claude mcp add context-mode -- node ${serverPath}`));
|
|
742
|
-
}
|
|
465
|
+
color.dim(` — restart your ${adapter.name} session to pick up the new version`));
|
|
743
466
|
}
|
|
744
|
-
else if (installMethod === "manual") {
|
|
745
|
-
p.note(JSON.stringify({
|
|
746
|
-
mcpServers: {
|
|
747
|
-
"context-mode": {
|
|
748
|
-
command: "node",
|
|
749
|
-
args: [serverPath],
|
|
750
|
-
},
|
|
751
|
-
},
|
|
752
|
-
}, null, 2), "Add to your .mcp.json or Claude Code settings");
|
|
753
|
-
}
|
|
754
|
-
p.outro(color.green("Setup complete!") +
|
|
755
|
-
" " +
|
|
756
|
-
color.dim(available.length + " languages ready."));
|
|
757
467
|
}
|