@danielblomma/cortex-mcp 0.4.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.
Files changed (41) hide show
  1. package/README.md +203 -0
  2. package/bin/cortex.mjs +621 -0
  3. package/docs/MCP_MARKETPLACE.md +160 -0
  4. package/package.json +42 -0
  5. package/scaffold/.context/config.yaml +21 -0
  6. package/scaffold/.context/ontology.cypher +63 -0
  7. package/scaffold/.context/rules.yaml +25 -0
  8. package/scaffold/.githooks/_cortex-update-runner.sh +58 -0
  9. package/scaffold/.githooks/post-checkout +22 -0
  10. package/scaffold/.githooks/post-merge +14 -0
  11. package/scaffold/docs/architecture.md +22 -0
  12. package/scaffold/mcp/package-lock.json +2623 -0
  13. package/scaffold/mcp/package.json +29 -0
  14. package/scaffold/mcp/src/embed.ts +416 -0
  15. package/scaffold/mcp/src/embeddings.ts +192 -0
  16. package/scaffold/mcp/src/graph.ts +666 -0
  17. package/scaffold/mcp/src/loadGraph.ts +597 -0
  18. package/scaffold/mcp/src/paths.ts +33 -0
  19. package/scaffold/mcp/src/search.ts +412 -0
  20. package/scaffold/mcp/src/server.ts +98 -0
  21. package/scaffold/mcp/src/types.ts +109 -0
  22. package/scaffold/mcp/tests/server.test.mjs +60 -0
  23. package/scaffold/mcp/tsconfig.json +13 -0
  24. package/scaffold/scripts/bootstrap.sh +57 -0
  25. package/scaffold/scripts/capture-note.sh +55 -0
  26. package/scaffold/scripts/context.sh +109 -0
  27. package/scaffold/scripts/embed.sh +15 -0
  28. package/scaffold/scripts/ingest.mjs +1118 -0
  29. package/scaffold/scripts/ingest.sh +20 -0
  30. package/scaffold/scripts/install-git-hooks.sh +21 -0
  31. package/scaffold/scripts/load-kuzu.sh +6 -0
  32. package/scaffold/scripts/load-ryu.sh +18 -0
  33. package/scaffold/scripts/parsers/javascript.mjs +390 -0
  34. package/scaffold/scripts/parsers/package-lock.json +51 -0
  35. package/scaffold/scripts/parsers/package.json +17 -0
  36. package/scaffold/scripts/plan-state-engine.cjs +310 -0
  37. package/scaffold/scripts/plan-state.sh +71 -0
  38. package/scaffold/scripts/refresh.sh +9 -0
  39. package/scaffold/scripts/status.sh +282 -0
  40. package/scaffold/scripts/update-context.sh +18 -0
  41. package/scaffold/scripts/watch.sh +374 -0
@@ -0,0 +1,310 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const statePath = process.argv[2];
5
+ const action = process.argv[3] || "show";
6
+ const arg = process.argv[4] || "";
7
+
8
+ const STEP_DEFS = [
9
+ {
10
+ id: "initialize",
11
+ title: "Initialize repository context",
12
+ command: "cortex init --bootstrap"
13
+ },
14
+ {
15
+ id: "connect",
16
+ title: "Connect MCP clients (Codex/Claude)",
17
+ command: "cortex connect"
18
+ },
19
+ {
20
+ id: "update",
21
+ title: "Refresh context while coding",
22
+ command: "cortex update"
23
+ },
24
+ {
25
+ id: "note",
26
+ title: "Capture decisions and TODOs",
27
+ command: "cortex note \"title\" \"details\""
28
+ }
29
+ ];
30
+
31
+ const COMMAND_STEP_MAP = {
32
+ init: ["initialize"],
33
+ bootstrap: ["initialize", "update"],
34
+ connect: ["connect"],
35
+ update: ["update"],
36
+ refresh: ["update"],
37
+ ingest: ["update"],
38
+ embed: ["update"],
39
+ "graph-load": ["update"],
40
+ note: ["note"]
41
+ };
42
+
43
+ function nowIso() {
44
+ return new Date().toISOString();
45
+ }
46
+
47
+ function createDefaultState(now) {
48
+ return {
49
+ version: 1,
50
+ created_at: now,
51
+ updated_at: now,
52
+ last_command: null,
53
+ current_step: STEP_DEFS[0].title,
54
+ next_command: STEP_DEFS[0].command,
55
+ steps: STEP_DEFS.map((step) => ({
56
+ id: step.id,
57
+ title: step.title,
58
+ command: step.command,
59
+ status: "pending",
60
+ updated_at: null
61
+ })),
62
+ history: [],
63
+ next_todo_id: 1,
64
+ todos: []
65
+ };
66
+ }
67
+
68
+ function loadState() {
69
+ if (!fs.existsSync(statePath)) {
70
+ return null;
71
+ }
72
+
73
+ const parsed = JSON.parse(fs.readFileSync(statePath, "utf8"));
74
+ if (!Array.isArray(parsed.steps)) {
75
+ throw new Error("Invalid plan state: steps is missing");
76
+ }
77
+
78
+ if (!Array.isArray(parsed.history)) {
79
+ parsed.history = [];
80
+ }
81
+
82
+ if (!Array.isArray(parsed.todos)) {
83
+ parsed.todos = [];
84
+ }
85
+
86
+ if (!Number.isInteger(parsed.next_todo_id) || parsed.next_todo_id < 1) {
87
+ parsed.next_todo_id = parsed.todos.reduce((max, todo) => Math.max(max, Number(todo.id) || 0), 0) + 1;
88
+ }
89
+
90
+ for (const def of STEP_DEFS) {
91
+ if (!parsed.steps.find((step) => step.id === def.id)) {
92
+ parsed.steps.push({
93
+ id: def.id,
94
+ title: def.title,
95
+ command: def.command,
96
+ status: "pending",
97
+ updated_at: null
98
+ });
99
+ }
100
+ }
101
+
102
+ return parsed;
103
+ }
104
+
105
+ function saveState(state) {
106
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
107
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
108
+ }
109
+
110
+ function updateDerivedFields(state) {
111
+ const nextStep = state.steps.find((step) => step.status !== "completed");
112
+ if (nextStep) {
113
+ state.current_step = nextStep.title;
114
+ state.next_command = nextStep.command;
115
+ return;
116
+ }
117
+
118
+ state.current_step = "Keep context fresh and capture decisions/TODOs";
119
+ state.next_command = "cortex update";
120
+ }
121
+
122
+ function appendHistory(state, command, at) {
123
+ state.last_command = {
124
+ name: command,
125
+ at
126
+ };
127
+ state.history.unshift({ command, at });
128
+ state.history = state.history.slice(0, 100);
129
+ }
130
+
131
+ function markStepCompleted(state, stepId, at) {
132
+ const step = state.steps.find((item) => item.id === stepId);
133
+ if (!step) {
134
+ return;
135
+ }
136
+ step.status = "completed";
137
+ step.updated_at = at;
138
+ }
139
+
140
+ function ensureState() {
141
+ const existing = loadState();
142
+ if (existing) {
143
+ return existing;
144
+ }
145
+ const now = nowIso();
146
+ const created = createDefaultState(now);
147
+ saveState(created);
148
+ return created;
149
+ }
150
+
151
+ function parseTodoId(raw) {
152
+ const cleaned = String(raw || "").trim().replace(/^#/, "");
153
+ const parsed = Number(cleaned);
154
+ if (!Number.isInteger(parsed) || parsed < 1) {
155
+ return null;
156
+ }
157
+ return parsed;
158
+ }
159
+
160
+ function printSummary(state) {
161
+ const openTodos = state.todos.filter((todo) => todo.status === "open");
162
+ const doneTodos = state.todos.filter((todo) => todo.status === "done");
163
+ const stepState = state.steps.map((step) => `${step.id}=${step.status}`).join(" ");
164
+
165
+ console.log(`[plan] current_step=${state.current_step}`);
166
+ console.log(`[plan] next_command=${state.next_command}`);
167
+ console.log(`[plan] steps ${stepState}`);
168
+
169
+ if (state.last_command && state.last_command.name) {
170
+ console.log(`[plan] last_command=${state.last_command.name} at=${state.last_command.at}`);
171
+ }
172
+
173
+ console.log(`[todo] open=${openTodos.length} done=${doneTodos.length}`);
174
+ if (openTodos.length > 0) {
175
+ console.log(`[todo] next=#${openTodos[0].id} ${openTodos[0].text}`);
176
+ }
177
+ }
178
+
179
+ try {
180
+ if (action === "ensure") {
181
+ ensureState();
182
+ process.exit(0);
183
+ }
184
+
185
+ if (action === "reset") {
186
+ const now = nowIso();
187
+ const reset = createDefaultState(now);
188
+ saveState(reset);
189
+ console.log(`[plan] reset ${statePath}`);
190
+ process.exit(0);
191
+ }
192
+
193
+ const state = ensureState();
194
+ const now = nowIso();
195
+
196
+ if (action === "event") {
197
+ const commandName = arg.trim();
198
+ if (!commandName) {
199
+ throw new Error("Usage: plan-state.sh event <command>");
200
+ }
201
+
202
+ const stepsToMark = COMMAND_STEP_MAP[commandName] || [];
203
+ for (const stepId of stepsToMark) {
204
+ markStepCompleted(state, stepId, now);
205
+ }
206
+
207
+ appendHistory(state, commandName, now);
208
+ state.updated_at = now;
209
+ updateDerivedFields(state);
210
+ saveState(state);
211
+ process.exit(0);
212
+ }
213
+
214
+ if (action === "show") {
215
+ updateDerivedFields(state);
216
+ printSummary(state);
217
+ process.exit(0);
218
+ }
219
+
220
+ if (action === "todo-add") {
221
+ const text = arg.trim();
222
+ if (!text) {
223
+ throw new Error('Usage: plan-state.sh todo add "<text>"');
224
+ }
225
+
226
+ const todo = {
227
+ id: state.next_todo_id,
228
+ text,
229
+ status: "open",
230
+ created_at: now,
231
+ updated_at: now,
232
+ completed_at: null
233
+ };
234
+
235
+ state.todos.push(todo);
236
+ state.next_todo_id += 1;
237
+ markStepCompleted(state, "note", now);
238
+ appendHistory(state, "todo:add", now);
239
+ state.updated_at = now;
240
+ updateDerivedFields(state);
241
+ saveState(state);
242
+ console.log(`[todo] added #${todo.id} ${todo.text}`);
243
+ process.exit(0);
244
+ }
245
+
246
+ if (action === "todo-list") {
247
+ const openTodos = state.todos.filter((todo) => todo.status === "open");
248
+ const doneTodos = state.todos.filter((todo) => todo.status === "done");
249
+
250
+ if (openTodos.length === 0 && doneTodos.length === 0) {
251
+ console.log("[todo] no todos");
252
+ process.exit(0);
253
+ }
254
+
255
+ for (const todo of openTodos) {
256
+ console.log(`[todo] [open] #${todo.id} ${todo.text}`);
257
+ }
258
+ for (const todo of doneTodos) {
259
+ console.log(`[todo] [done] #${todo.id} ${todo.text}`);
260
+ }
261
+ process.exit(0);
262
+ }
263
+
264
+ if (action === "todo-done" || action === "todo-reopen" || action === "todo-remove") {
265
+ const todoId = parseTodoId(arg);
266
+ if (!todoId) {
267
+ throw new Error("Usage: plan-state.sh todo <done|reopen|remove> <id>");
268
+ }
269
+
270
+ const index = state.todos.findIndex((todo) => Number(todo.id) === todoId);
271
+ if (index === -1) {
272
+ throw new Error(`TODO #${todoId} not found`);
273
+ }
274
+
275
+ const todo = state.todos[index];
276
+ if (action === "todo-remove") {
277
+ state.todos.splice(index, 1);
278
+ appendHistory(state, "todo:remove", now);
279
+ state.updated_at = now;
280
+ updateDerivedFields(state);
281
+ saveState(state);
282
+ console.log(`[todo] removed #${todoId}`);
283
+ process.exit(0);
284
+ }
285
+
286
+ if (action === "todo-done") {
287
+ todo.status = "done";
288
+ todo.completed_at = now;
289
+ appendHistory(state, "todo:done", now);
290
+ console.log(`[todo] done #${todoId} ${todo.text}`);
291
+ } else {
292
+ todo.status = "open";
293
+ todo.completed_at = null;
294
+ appendHistory(state, "todo:reopen", now);
295
+ console.log(`[todo] reopened #${todoId} ${todo.text}`);
296
+ }
297
+
298
+ todo.updated_at = now;
299
+ state.updated_at = now;
300
+ updateDerivedFields(state);
301
+ saveState(state);
302
+ process.exit(0);
303
+ }
304
+
305
+ throw new Error(`Unknown plan-state action: ${action}`);
306
+ } catch (error) {
307
+ const message = error instanceof Error ? error.message : String(error);
308
+ process.stderr.write(`${message}\n`);
309
+ process.exit(1);
310
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ PLAN_DIR="$REPO_ROOT/.context/plan"
7
+ STATE_FILE="$PLAN_DIR/state.json"
8
+
9
+ SUBCOMMAND="${1:-show}"
10
+ if [[ $# -gt 0 ]]; then
11
+ shift
12
+ fi
13
+
14
+ run_state_tool() {
15
+ node "$SCRIPT_DIR/plan-state-engine.cjs" "$STATE_FILE" "$@"
16
+ }
17
+
18
+ mkdir -p "$PLAN_DIR"
19
+
20
+ case "$SUBCOMMAND" in
21
+ show)
22
+ run_state_tool show
23
+ ;;
24
+ event)
25
+ EVENT_NAME="${1:-}"
26
+ if [[ -z "$EVENT_NAME" ]]; then
27
+ echo "Usage: plan-state.sh event <command-name>"
28
+ exit 1
29
+ fi
30
+ run_state_tool event "$EVENT_NAME"
31
+ ;;
32
+ reset)
33
+ run_state_tool reset
34
+ ;;
35
+ todo)
36
+ TODO_ACTION="${1:-list}"
37
+ if [[ $# -gt 0 ]]; then
38
+ shift
39
+ fi
40
+ case "$TODO_ACTION" in
41
+ add)
42
+ TODO_TEXT="${*:-}"
43
+ run_state_tool todo-add "$TODO_TEXT"
44
+ ;;
45
+ list)
46
+ run_state_tool todo-list
47
+ ;;
48
+ done)
49
+ TODO_ID="${1:-}"
50
+ run_state_tool todo-done "$TODO_ID"
51
+ ;;
52
+ reopen)
53
+ TODO_ID="${1:-}"
54
+ run_state_tool todo-reopen "$TODO_ID"
55
+ ;;
56
+ remove)
57
+ TODO_ID="${1:-}"
58
+ run_state_tool todo-remove "$TODO_ID"
59
+ ;;
60
+ *)
61
+ # Keep ergonomic behavior: unknown first token is treated as todo text.
62
+ run_state_tool todo-add "$TODO_ACTION ${*:-}"
63
+ ;;
64
+ esac
65
+ ;;
66
+ *)
67
+ echo "Unknown subcommand: $SUBCOMMAND"
68
+ echo "Usage: plan-state.sh [show|event <name>|reset|todo <action> [args]]"
69
+ exit 1
70
+ ;;
71
+ esac
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+
6
+ echo "[refresh] running ingestion"
7
+ "$REPO_ROOT/scripts/ingest.sh" "$@"
8
+
9
+ echo "[refresh] done"
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ MANIFEST="$REPO_ROOT/.context/cache/manifest.json"
6
+ GRAPH_MANIFEST="$REPO_ROOT/.context/cache/graph-manifest.json"
7
+ EMBED_MANIFEST="$REPO_ROOT/.context/embeddings/manifest.json"
8
+
9
+ if [[ ! -f "$MANIFEST" ]]; then
10
+ echo "[status] No ingest manifest found."
11
+ echo "[status] Run: ./scripts/context.sh ingest"
12
+ exit 0
13
+ fi
14
+
15
+ node -e '
16
+ const fs = require("node:fs");
17
+ const manifestPath = process.argv[1];
18
+ const data = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
19
+ console.log(`[status] generated_at=${data.generated_at}`);
20
+ console.log(`[status] mode=${data.mode}`);
21
+ console.log(`[status] source_paths=${(data.source_paths || []).join(", ")}`);
22
+ const c = data.counts || {};
23
+ console.log(`[status] files=${c.files ?? 0} adrs=${c.adrs ?? 0} rules=${c.rules ?? 0}`);
24
+ console.log(`[status] rels constrains=${c.relations_constrains ?? 0} implements=${c.relations_implements ?? 0} supersedes=${c.relations_supersedes ?? 0}`);
25
+ const s = data.skipped || {};
26
+ console.log(`[status] skipped unsupported=${s.unsupported ?? 0} too_large=${s.too_large ?? s.tooLarge ?? 0} binary=${s.binary ?? 0}`);
27
+ if (typeof data.incremental_mode === "boolean") {
28
+ console.log(`[status] incremental_mode=${data.incremental_mode} changed_candidates=${data.changed_candidates ?? 0} deleted_paths=${data.deleted_paths ?? 0}`);
29
+ }
30
+ ' "$MANIFEST"
31
+
32
+ if [[ -f "$GRAPH_MANIFEST" ]]; then
33
+ node -e '
34
+ const fs = require("node:fs");
35
+ const graphManifestPath = process.argv[1];
36
+ const data = JSON.parse(fs.readFileSync(graphManifestPath, "utf8"));
37
+ const c = data.counts || {};
38
+ console.log(`[status] graph generated_at=${data.generated_at}`);
39
+ console.log(`[status] graph files=${c.files ?? 0} rules=${c.rules ?? 0} adrs=${c.adrs ?? 0}`);
40
+ console.log(`[status] graph rels constrains=${c.constrains ?? 0} implements=${c.implements ?? 0} supersedes=${c.supersedes ?? 0}`);
41
+ ' "$GRAPH_MANIFEST"
42
+ else
43
+ echo "[status] graph manifest missing (run: ./scripts/context.sh graph-load)"
44
+ fi
45
+
46
+ if [[ -f "$EMBED_MANIFEST" ]]; then
47
+ node -e '
48
+ const fs = require("node:fs");
49
+ const embedManifestPath = process.argv[1];
50
+ const data = JSON.parse(fs.readFileSync(embedManifestPath, "utf8"));
51
+ const c = data.counts || {};
52
+ console.log(`[status] embeddings generated_at=${data.generated_at}`);
53
+ console.log(`[status] embeddings model=${data.model} dim=${data.dimensions ?? 0}`);
54
+ console.log(`[status] embeddings entities=${c.entities ?? 0} output=${c.output ?? 0} embedded=${c.embedded ?? 0} reused=${c.reused ?? 0} failed=${c.failed ?? 0}`);
55
+ ' "$EMBED_MANIFEST"
56
+ else
57
+ echo "[status] embeddings manifest missing (run: ./scripts/context.sh embed)"
58
+ fi
59
+
60
+ node -e '
61
+ const fs = require("node:fs");
62
+ const path = require("node:path");
63
+ const { execSync } = require("node:child_process");
64
+
65
+ const repoRoot = process.argv[1];
66
+ const manifestPath = process.argv[2];
67
+
68
+ function toPosixPath(value) {
69
+ return value.split(path.sep).join("/");
70
+ }
71
+
72
+ function normalizeSource(sourcePath) {
73
+ const source = toPosixPath(sourcePath).replace(/\/+$/, "");
74
+ if (source === ".") {
75
+ return "";
76
+ }
77
+ return source;
78
+ }
79
+
80
+ function hasSourcePrefix(relPath, sourcePaths) {
81
+ return sourcePaths.some((sourcePath) => {
82
+ const source = normalizeSource(sourcePath);
83
+ if (source === "") {
84
+ return true;
85
+ }
86
+ return relPath === source || relPath.startsWith(`${source}/`);
87
+ });
88
+ }
89
+
90
+ function parseChangedPaths() {
91
+ const output = execSync("git status --porcelain", {
92
+ cwd: repoRoot,
93
+ stdio: ["ignore", "pipe", "ignore"],
94
+ encoding: "utf8"
95
+ });
96
+
97
+ const changed = new Set();
98
+ const deleted = new Set();
99
+
100
+ for (const rawLine of output.split(/\r?\n/)) {
101
+ if (!rawLine) continue;
102
+ const status = rawLine.slice(0, 2);
103
+ const payload = rawLine.slice(3).trim();
104
+ if (!payload) continue;
105
+
106
+ if (payload.includes(" -> ")) {
107
+ const [fromPath, toPath] = payload.split(" -> ");
108
+ deleted.add(path.resolve(repoRoot, fromPath));
109
+ changed.add(path.resolve(repoRoot, toPath));
110
+ continue;
111
+ }
112
+
113
+ const absolutePath = path.resolve(repoRoot, payload);
114
+ if (status.includes("D")) {
115
+ deleted.add(absolutePath);
116
+ } else {
117
+ changed.add(absolutePath);
118
+ }
119
+ }
120
+
121
+ return { changed, deleted };
122
+ }
123
+
124
+ function createBar(ratio, size = 20) {
125
+ const clamped = Math.max(0, Math.min(1, ratio));
126
+ const filled = Math.round(clamped * size);
127
+ return `[${"#".repeat(filled)}${"-".repeat(size - filled)}]`;
128
+ }
129
+
130
+ try {
131
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
132
+ const sourcePaths = Array.isArray(manifest.source_paths) ? manifest.source_paths : [];
133
+ const indexedFiles = Number(manifest?.counts?.files ?? 0);
134
+
135
+ const { changed, deleted } = parseChangedPaths();
136
+ let relevantChanged = 0;
137
+ let relevantDeleted = 0;
138
+
139
+ for (const absolutePath of changed) {
140
+ const relPath = toPosixPath(path.relative(repoRoot, absolutePath));
141
+ if (!relPath || relPath.startsWith("..")) continue;
142
+ if (relPath.startsWith(".context/")) continue;
143
+ if (hasSourcePrefix(relPath, sourcePaths)) {
144
+ relevantChanged += 1;
145
+ }
146
+ }
147
+
148
+ for (const absolutePath of deleted) {
149
+ const relPath = toPosixPath(path.relative(repoRoot, absolutePath));
150
+ if (!relPath || relPath.startsWith("..")) continue;
151
+ if (relPath.startsWith(".context/")) continue;
152
+ if (hasSourcePrefix(relPath, sourcePaths)) {
153
+ relevantDeleted += 1;
154
+ }
155
+ }
156
+
157
+ const pending = relevantChanged + relevantDeleted;
158
+ const baseline = Math.max(indexedFiles, pending, 1);
159
+ const freshness = Math.max(0, (baseline - pending) / baseline);
160
+ const freshnessPercent = Math.round(freshness * 100);
161
+ const bar = createBar(freshness);
162
+
163
+ console.log(`[status] freshness ${bar} ${freshnessPercent}%`);
164
+ if (pending === 0) {
165
+ console.log("[status] update_needed=no");
166
+ console.log("[status] context is up to date with current source changes");
167
+ } else {
168
+ console.log(`[status] update_needed=yes changed=${relevantChanged} deleted=${relevantDeleted}`);
169
+ console.log("[status] run: cortex update");
170
+ }
171
+ } catch (error) {
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ console.log(`[status] freshness unavailable (${message})`);
174
+ }
175
+ ' "$REPO_ROOT" "$MANIFEST"
176
+
177
+ node -e '
178
+ const path = require("node:path");
179
+ const { execSync } = require("node:child_process");
180
+
181
+ const repoRoot = process.argv[1];
182
+ const cacheDir = process.argv[2];
183
+ const localVersionEnv = process.argv[3] || "";
184
+
185
+ function parseVersion(value) {
186
+ const match = String(value || "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)$/);
187
+ if (!match) return null;
188
+ return match.slice(1).map((part) => Number(part));
189
+ }
190
+
191
+ function compareVersions(a, b) {
192
+ for (let i = 0; i < 3; i += 1) {
193
+ if (a[i] > b[i]) return 1;
194
+ if (a[i] < b[i]) return -1;
195
+ }
196
+ return 0;
197
+ }
198
+
199
+ function getLocalVersion() {
200
+ const envVersion = String(localVersionEnv || "").trim();
201
+ if (parseVersion(envVersion)) {
202
+ return envVersion;
203
+ }
204
+
205
+ try {
206
+ const output = execSync("cortex --version", {
207
+ cwd: repoRoot,
208
+ stdio: ["ignore", "pipe", "ignore"],
209
+ encoding: "utf8",
210
+ timeout: 1500
211
+ }).trim();
212
+ if (parseVersion(output)) {
213
+ return output;
214
+ }
215
+ } catch {
216
+ // Ignore and report unavailable below.
217
+ }
218
+
219
+ return "";
220
+ }
221
+
222
+ try {
223
+ const localVersion = getLocalVersion();
224
+ if (!localVersion) {
225
+ console.log("[status] cortex_update_check=unavailable (local version not detected)");
226
+ process.exit(0);
227
+ }
228
+
229
+ console.log(`[status] cortex_cli_version=${localVersion}`);
230
+
231
+ const summarizeError = (error) => {
232
+ const raw = error instanceof Error ? error.message : String(error);
233
+ return raw.split(/\r?\n/)[0].trim();
234
+ };
235
+
236
+ const npmCache = path.join(cacheDir, "npm-cache");
237
+ let latestRaw = "";
238
+ try {
239
+ latestRaw = execSync("npm view github:DanielBlomma/cortex version --json", {
240
+ cwd: repoRoot,
241
+ stdio: ["ignore", "pipe", "pipe"],
242
+ encoding: "utf8",
243
+ timeout: 2500,
244
+ env: { ...process.env, NPM_CONFIG_CACHE: npmCache }
245
+ }).trim();
246
+ } catch (error) {
247
+ console.log(`[status] cortex_update_check=unavailable (${summarizeError(error)})`);
248
+ process.exit(0);
249
+ }
250
+
251
+ const parsedLatest = JSON.parse(latestRaw);
252
+ const latestVersion = Array.isArray(parsedLatest) ? parsedLatest[parsedLatest.length - 1] : parsedLatest;
253
+
254
+ const localParsed = parseVersion(localVersion);
255
+ const latestParsed = parseVersion(latestVersion);
256
+ if (!localParsed || !latestParsed) {
257
+ console.log(`[status] cortex_latest_version=${latestVersion}`);
258
+ console.log("[status] cortex_update_check=unavailable (unsupported version format)");
259
+ process.exit(0);
260
+ }
261
+
262
+ const hasUpdate = compareVersions(latestParsed, localParsed) > 0;
263
+ console.log(`[status] cortex_latest_version=${latestVersion}`);
264
+
265
+ if (hasUpdate) {
266
+ console.log("[status] cortex_update_available=yes");
267
+ console.log("[status] run: npm i -g github:DanielBlomma/cortex");
268
+ } else {
269
+ console.log("[status] cortex_update_available=no");
270
+ }
271
+ } catch (error) {
272
+ const message = error instanceof Error ? error.message : String(error);
273
+ console.log(`[status] cortex_update_check=unavailable (${message})`);
274
+ }
275
+ ' "$REPO_ROOT" "$REPO_ROOT/.context/cache" "${CORTEX_CLI_VERSION:-}"
276
+
277
+ PLAN_SCRIPT="$REPO_ROOT/scripts/plan-state.sh"
278
+ if [[ -x "$PLAN_SCRIPT" ]]; then
279
+ if ! "$PLAN_SCRIPT" show; then
280
+ echo "[plan] warning: failed to read plan state"
281
+ fi
282
+ fi
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+
6
+ echo "[update] ingesting changed files"
7
+ "$REPO_ROOT/scripts/ingest.sh" --changed
8
+
9
+ echo "[update] embedding changed entities"
10
+ if ! "$REPO_ROOT/scripts/embed.sh" --changed; then
11
+ echo "[update] warning: embedding generation failed; continuing with lexical search fallback"
12
+ fi
13
+
14
+ echo "[update] rebuilding graph"
15
+ "$REPO_ROOT/scripts/load-ryu.sh"
16
+
17
+ echo "[update] status"
18
+ "$REPO_ROOT/scripts/status.sh"