context-mode 1.0.0 → 1.0.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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.0"
9
+ "version": "1.0.2"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
15
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
- "version": "1.0.0",
16
+ "version": "1.0.2",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -39,6 +39,8 @@ export declare const REQUIRED_HOOKS: HookType[];
39
39
  export declare const OPTIONAL_HOOKS: HookType[];
40
40
  /**
41
41
  * Check if a hook entry points to a context-mode hook script.
42
+ * Matches both legacy format (node .../pretooluse.mjs) and
43
+ * CLI dispatcher format (context-mode hook claude-code pretooluse).
42
44
  */
43
45
  export declare function isContextModeHook(entry: {
44
46
  hooks?: Array<{
@@ -73,10 +73,13 @@ export const OPTIONAL_HOOKS = [
73
73
  ];
74
74
  /**
75
75
  * Check if a hook entry points to a context-mode hook script.
76
+ * Matches both legacy format (node .../pretooluse.mjs) and
77
+ * CLI dispatcher format (context-mode hook claude-code pretooluse).
76
78
  */
77
79
  export function isContextModeHook(entry, hookType) {
78
80
  const scriptName = HOOK_SCRIPTS[hookType];
79
- return (entry.hooks?.some((h) => h.command?.includes(scriptName)) ?? false);
81
+ const cliCommand = buildHookCommand(hookType);
82
+ return (entry.hooks?.some((h) => h.command?.includes(scriptName) || h.command?.includes(cliCommand)) ?? false);
80
83
  }
81
84
  /**
82
85
  * Build the hook command string for a given hook type.
@@ -30,6 +30,8 @@ export declare const REQUIRED_HOOKS: HookType[];
30
30
  export declare const OPTIONAL_HOOKS: HookType[];
31
31
  /**
32
32
  * Check if a hook entry points to a context-mode hook script.
33
+ * Matches both legacy format (node .../beforetool.mjs) and
34
+ * CLI dispatcher format (context-mode hook gemini-cli beforetool).
33
35
  */
34
36
  export declare function isContextModeHook(entry: {
35
37
  hooks?: Array<{
@@ -49,10 +49,13 @@ export const OPTIONAL_HOOKS = [
49
49
  ];
50
50
  /**
51
51
  * Check if a hook entry points to a context-mode hook script.
52
+ * Matches both legacy format (node .../beforetool.mjs) and
53
+ * CLI dispatcher format (context-mode hook gemini-cli beforetool).
52
54
  */
53
55
  export function isContextModeHook(entry, hookType) {
54
56
  const scriptName = HOOK_SCRIPTS[hookType];
55
- return (entry.hooks?.some((h) => h.command?.includes(scriptName)) ?? false);
57
+ const cliCommand = buildHookCommand(hookType);
58
+ return (entry.hooks?.some((h) => h.command?.includes(scriptName) || h.command?.includes(cliCommand)) ?? false);
56
59
  }
57
60
  /**
58
61
  * Build the hook command string for a given hook type.
@@ -35,6 +35,8 @@ export declare const REQUIRED_HOOKS: HookType[];
35
35
  export declare const OPTIONAL_HOOKS: HookType[];
36
36
  /**
37
37
  * Check if a hook entry points to a context-mode hook script.
38
+ * Matches both legacy format (node .../pretooluse.mjs) and
39
+ * CLI dispatcher format (context-mode hook vscode-copilot pretooluse).
38
40
  */
39
41
  export declare function isContextModeHook(entry: {
40
42
  hooks?: Array<{
@@ -55,12 +55,15 @@ export const OPTIONAL_HOOKS = [
55
55
  ];
56
56
  /**
57
57
  * Check if a hook entry points to a context-mode hook script.
58
+ * Matches both legacy format (node .../pretooluse.mjs) and
59
+ * CLI dispatcher format (context-mode hook vscode-copilot pretooluse).
58
60
  */
59
61
  export function isContextModeHook(entry, hookType) {
60
62
  const scriptName = HOOK_SCRIPTS[hookType];
61
63
  if (!scriptName)
62
64
  return false;
63
- return (entry.hooks?.some((h) => h.command?.includes(scriptName)) ?? false);
65
+ const cliCommand = buildHookCommand(hookType);
66
+ return (entry.hooks?.some((h) => h.command?.includes(scriptName) || h.command?.includes(cliCommand)) ?? false);
64
67
  }
65
68
  /**
66
69
  * Build the hook command string for a given hook type.
package/build/server.js CHANGED
@@ -12,7 +12,7 @@ import { PolyglotExecutor } from "./executor.js";
12
12
  import { ContentStore, cleanupStaleDBs } from "./store.js";
13
13
  import { readBashPolicies, evaluateCommandDenyOnly, extractShellCommands, readToolDenyPatterns, evaluateFilePath, } from "./security.js";
14
14
  import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime, } from "./runtime.js";
15
- const VERSION = "1.0.0";
15
+ const VERSION = "1.0.2";
16
16
  // Prevent silent server death from unhandled async errors
17
17
  process.on("unhandledRejection", (err) => {
18
18
  process.stderr.write(`[context-mode] unhandledRejection: ${err}\n`);
@@ -29,7 +29,11 @@ export interface BuildSnapshotOpts {
29
29
  export declare function renderActiveFiles(fileEvents: StoredEvent[]): string;
30
30
  /**
31
31
  * Render <task_state> from task events.
32
- * Shows the most recent task state (last event's data).
32
+ * Reconstructs the full task list from create/update events,
33
+ * filters out completed tasks, and renders only pending/in-progress work.
34
+ *
35
+ * TaskCreate events have `{ subject }`, TaskUpdate events have `{ taskId, status }`.
36
+ * Match by chronological order: creates[0] → lowest taskId from updates.
33
37
  */
34
38
  export declare function renderTaskState(taskEvents: StoredEvent[]): string;
35
39
  /**
@@ -61,15 +61,50 @@ export function renderActiveFiles(fileEvents) {
61
61
  }
62
62
  /**
63
63
  * Render <task_state> from task events.
64
- * Shows the most recent task state (last event's data).
64
+ * Reconstructs the full task list from create/update events,
65
+ * filters out completed tasks, and renders only pending/in-progress work.
66
+ *
67
+ * TaskCreate events have `{ subject }`, TaskUpdate events have `{ taskId, status }`.
68
+ * Match by chronological order: creates[0] → lowest taskId from updates.
65
69
  */
66
70
  export function renderTaskState(taskEvents) {
67
71
  if (taskEvents.length === 0)
68
72
  return "";
69
- // Use the last task event as the most current state
70
- const lastTask = taskEvents[taskEvents.length - 1];
71
- const data = truncateString(escapeXML(lastTask.data), 200);
72
- return ` <task_state>\n ${data}\n </task_state>`;
73
+ const creates = [];
74
+ const updates = {};
75
+ for (const ev of taskEvents) {
76
+ try {
77
+ const parsed = JSON.parse(ev.data);
78
+ if (typeof parsed.subject === "string") {
79
+ creates.push(parsed.subject);
80
+ }
81
+ else if (typeof parsed.taskId === "string" && typeof parsed.status === "string") {
82
+ updates[parsed.taskId] = parsed.status;
83
+ }
84
+ }
85
+ catch { /* not JSON */ }
86
+ }
87
+ if (creates.length === 0)
88
+ return "";
89
+ // Match creates to updates positionally (creates[0] → lowest taskId)
90
+ const sortedIds = Object.keys(updates).sort((a, b) => Number(a) - Number(b));
91
+ const pending = [];
92
+ for (let i = 0; i < creates.length; i++) {
93
+ const matchedId = sortedIds[i];
94
+ const status = matchedId ? (updates[matchedId] ?? "pending") : "pending";
95
+ if (status !== "completed") {
96
+ pending.push(creates[i]);
97
+ }
98
+ }
99
+ // All tasks completed — nothing to render
100
+ if (pending.length === 0)
101
+ return "";
102
+ const lines = [" <task_state>"];
103
+ for (const task of pending) {
104
+ lines.push(` - ${escapeXML(truncateString(task, 100))}`);
105
+ }
106
+ lines.push(" </task_state>");
107
+ return lines.join("\n");
73
108
  }
74
109
  /**
75
110
  * Render <rules> from rule events.
@@ -62,10 +62,44 @@ export function writeSessionEventsFile(events, eventsPath) {
62
62
  }
63
63
 
64
64
  if (grouped.task?.length > 0) {
65
- lines.push("## Tasks In Progress");
66
- lines.push("");
67
- for (const ev of grouped.task) lines.push(`- ${ev.data}`);
68
- lines.push("");
65
+ const creates = [];
66
+ const updates = {};
67
+ for (const ev of grouped.task) {
68
+ try {
69
+ const parsed = JSON.parse(ev.data);
70
+ if (parsed.subject) {
71
+ creates.push(parsed.subject);
72
+ } else if (parsed.taskId && parsed.status) {
73
+ updates[parsed.taskId] = parsed.status;
74
+ }
75
+ } catch { /* not JSON — dump as-is */
76
+ creates.push(ev.data);
77
+ }
78
+ }
79
+ const sortedIds = Object.keys(updates).sort((a, b) => Number(a) - Number(b));
80
+ const pending = [];
81
+ const completed = [];
82
+ for (let i = 0; i < creates.length; i++) {
83
+ const matchedId = sortedIds[i];
84
+ const status = matchedId ? (updates[matchedId] || "pending") : "pending";
85
+ if (status === "completed") {
86
+ completed.push(creates[i]);
87
+ } else {
88
+ pending.push(creates[i]);
89
+ }
90
+ }
91
+ if (pending.length > 0) {
92
+ lines.push("## Tasks In Progress");
93
+ lines.push("");
94
+ for (const task of pending) lines.push(`- ${task}`);
95
+ lines.push("");
96
+ }
97
+ if (completed.length > 0) {
98
+ lines.push("## Tasks Completed");
99
+ lines.push("");
100
+ for (const task of completed) lines.push(`- ${task}`);
101
+ lines.push("");
102
+ }
69
103
  }
70
104
 
71
105
  if (grouped.decision?.length > 0) {
@@ -179,10 +213,11 @@ export function buildSessionDirective(source, eventMeta) {
179
213
  block += `\n`;
180
214
  }
181
215
 
182
- // 2. Tasks — parsed into readable format with status
216
+ // 2. Tasks — parsed into readable format, only pending/in-progress shown
183
217
  // TaskCreate events have {subject} but no taskId.
184
218
  // TaskUpdate events have {taskId, status} but no subject.
185
219
  // Match by chronological order: creates[0] → lowest taskId from updates.
220
+ // Completed tasks are excluded — the model should not re-work them.
186
221
  if (grouped.task?.length > 0) {
187
222
  const creates = [];
188
223
  const updates = {};
@@ -200,14 +235,21 @@ export function buildSessionDirective(source, eventMeta) {
200
235
 
201
236
  if (creates.length > 0) {
202
237
  const sortedIds = Object.keys(updates).sort((a, b) => Number(a) - Number(b));
203
- block += `\n## Tasks`;
238
+ const pending = [];
204
239
  for (let i = 0; i < creates.length; i++) {
205
240
  const matchedId = sortedIds[i];
206
241
  const status = matchedId ? (updates[matchedId] || "pending") : "pending";
207
- const mark = status === "completed" ? "x" : " ";
208
- block += `\n- [${mark}] ${creates[i]}`;
242
+ if (status !== "completed") {
243
+ pending.push(creates[i]);
244
+ }
245
+ }
246
+ if (pending.length > 0) {
247
+ block += `\n## Pending Tasks`;
248
+ for (const task of pending) {
249
+ block += `\n- ${task}`;
250
+ }
251
+ block += `\n`;
209
252
  }
210
- block += `\n`;
211
253
  }
212
254
  }
213
255
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "description": "MCP plugin that saves 98% of your context window. Works with Claude Code, Gemini CLI, VS Code Copilot, OpenCode, and Codex CLI. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
6
6
  "author": "Mert Koseoğlu",