@danielblomma/cortex-mcp 0.4.2 → 0.6.4
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/README.md +64 -16
- package/bin/cortex.mjs +32 -60
- package/package.json +17 -3
- package/scaffold/.context/ontology.cypher +47 -0
- package/scaffold/.githooks/post-commit +14 -0
- package/scaffold/.githooks/post-rewrite +23 -0
- package/scaffold/mcp/package-lock.json +19 -23
- package/scaffold/mcp/package.json +3 -1
- package/scaffold/mcp/src/contextEntities.ts +311 -0
- package/scaffold/mcp/src/defaults.ts +6 -0
- package/scaffold/mcp/src/embed.ts +163 -37
- package/scaffold/mcp/src/frontmatter.ts +39 -0
- package/scaffold/mcp/src/graph.ts +330 -109
- package/scaffold/mcp/src/graphMetrics.ts +12 -0
- package/scaffold/mcp/src/impactPresentation.ts +202 -0
- package/scaffold/mcp/src/impactRanking.ts +237 -0
- package/scaffold/mcp/src/impactResponse.ts +47 -0
- package/scaffold/mcp/src/impactResults.ts +173 -0
- package/scaffold/mcp/src/impactSeed.ts +33 -0
- package/scaffold/mcp/src/impactTraversal.ts +83 -0
- package/scaffold/mcp/src/jsonl.ts +34 -0
- package/scaffold/mcp/src/loadGraph.ts +345 -86
- package/scaffold/mcp/src/paths.ts +24 -2
- package/scaffold/mcp/src/presets.ts +137 -0
- package/scaffold/mcp/src/relatedResponse.ts +30 -0
- package/scaffold/mcp/src/relatedTraversal.ts +101 -0
- package/scaffold/mcp/src/rules.ts +27 -0
- package/scaffold/mcp/src/search.ts +191 -355
- package/scaffold/mcp/src/searchCore.ts +274 -0
- package/scaffold/mcp/src/searchResults.ts +133 -0
- package/scaffold/mcp/src/server.ts +95 -3
- package/scaffold/mcp/src/types.ts +99 -3
- package/scaffold/scripts/context.sh +12 -46
- package/scaffold/scripts/dashboard.mjs +797 -0
- package/scaffold/scripts/dashboard.sh +13 -0
- package/scaffold/scripts/ingest.mjs +2219 -59
- package/scaffold/scripts/install-git-hooks.sh +3 -1
- package/scaffold/scripts/memory-compile.mjs +232 -0
- package/scaffold/scripts/memory-compile.sh +20 -0
- package/scaffold/scripts/memory-lint.mjs +375 -0
- package/scaffold/scripts/memory-lint.sh +20 -0
- package/scaffold/scripts/parsers/config.mjs +178 -0
- package/scaffold/scripts/parsers/cpp.mjs +316 -0
- package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +374 -0
- package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +13 -0
- package/scaffold/scripts/parsers/javascript/ast.mjs +61 -0
- package/scaffold/scripts/parsers/javascript/calls.mjs +53 -0
- package/scaffold/scripts/parsers/javascript/chunks.mjs +388 -0
- package/scaffold/scripts/parsers/javascript/imports.mjs +162 -0
- package/scaffold/scripts/parsers/javascript/patterns.mjs +82 -0
- package/scaffold/scripts/parsers/javascript/scope-analysis.mjs +3 -0
- package/scaffold/scripts/parsers/javascript/scope-builder.mjs +305 -0
- package/scaffold/scripts/parsers/javascript/scope-resolver.mjs +82 -0
- package/scaffold/scripts/parsers/javascript.mjs +27 -350
- package/scaffold/scripts/parsers/resources.mjs +166 -0
- package/scaffold/scripts/parsers/sql.mjs +137 -0
- package/scaffold/scripts/parsers/vbnet.mjs +143 -0
- package/scaffold/scripts/status.sh +15 -8
- package/scaffold/scripts/capture-note.sh +0 -55
- package/scaffold/scripts/plan-state-engine.cjs +0 -310
- package/scaffold/scripts/plan-state.sh +0 -71
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Conditional VB.NET parser bridge for Cortex.
|
|
4
|
+
*
|
|
5
|
+
* Uses a Roslyn sidecar via `dotnet run` when a .NET runtime is available.
|
|
6
|
+
* If no runtime exists, callers should skip structured chunk extraction and
|
|
7
|
+
* fall back to plain file-level indexing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const DEFAULT_DOTNET_COMMAND = "dotnet";
|
|
17
|
+
const DEFAULT_PROJECT_PATH = path.join(__dirname, "dotnet", "VbNetParser", "VbNetParser.csproj");
|
|
18
|
+
|
|
19
|
+
let runtimeCache = null;
|
|
20
|
+
|
|
21
|
+
function getDotnetCommand() {
|
|
22
|
+
const override = process.env.CORTEX_DOTNET_CMD;
|
|
23
|
+
return override && override.trim().length > 0 ? override.trim() : DEFAULT_DOTNET_COMMAND;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getProjectPath() {
|
|
27
|
+
const override = process.env.CORTEX_VBNET_PARSER_PROJECT;
|
|
28
|
+
return override && override.trim().length > 0 ? override.trim() : DEFAULT_PROJECT_PATH;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resetVbNetParserRuntimeCache() {
|
|
32
|
+
runtimeCache = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getVbNetParserRuntime() {
|
|
36
|
+
if (runtimeCache) {
|
|
37
|
+
return runtimeCache;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const command = getDotnetCommand();
|
|
41
|
+
const versionProbe = spawnSync(command, ["--version"], {
|
|
42
|
+
encoding: "utf8",
|
|
43
|
+
timeout: 5000
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (versionProbe.error || versionProbe.status !== 0) {
|
|
47
|
+
runtimeCache = {
|
|
48
|
+
available: false,
|
|
49
|
+
command,
|
|
50
|
+
projectPath: getProjectPath(),
|
|
51
|
+
reason:
|
|
52
|
+
versionProbe.error?.message ||
|
|
53
|
+
versionProbe.stderr?.trim() ||
|
|
54
|
+
"dotnet runtime not available"
|
|
55
|
+
};
|
|
56
|
+
return runtimeCache;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
runtimeCache = {
|
|
60
|
+
available: true,
|
|
61
|
+
command,
|
|
62
|
+
projectPath: getProjectPath(),
|
|
63
|
+
version: versionProbe.stdout.trim()
|
|
64
|
+
};
|
|
65
|
+
return runtimeCache;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isVbNetParserAvailable() {
|
|
69
|
+
return getVbNetParserRuntime().available;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseCode(code, filePath, language = "vbnet") {
|
|
73
|
+
const runtime = getVbNetParserRuntime();
|
|
74
|
+
if (!runtime.available) {
|
|
75
|
+
return { chunks: [], errors: [] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const args = [
|
|
79
|
+
"run",
|
|
80
|
+
"--project",
|
|
81
|
+
runtime.projectPath,
|
|
82
|
+
"--configuration",
|
|
83
|
+
"Release",
|
|
84
|
+
"--",
|
|
85
|
+
"--stdin",
|
|
86
|
+
"--file",
|
|
87
|
+
filePath,
|
|
88
|
+
"--language",
|
|
89
|
+
language
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const result = spawnSync(runtime.command, args, {
|
|
93
|
+
input: code,
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
timeout: 30000,
|
|
96
|
+
maxBuffer: 10 * 1024 * 1024
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (result.error || result.status !== 0) {
|
|
100
|
+
return {
|
|
101
|
+
chunks: [],
|
|
102
|
+
errors: [
|
|
103
|
+
{
|
|
104
|
+
message:
|
|
105
|
+
result.error?.message ||
|
|
106
|
+
result.stderr?.trim() ||
|
|
107
|
+
`VB.NET parser failed with exit code ${result.status ?? "unknown"}`
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(result.stdout);
|
|
115
|
+
return {
|
|
116
|
+
chunks: Array.isArray(parsed.chunks) ? parsed.chunks : [],
|
|
117
|
+
errors: Array.isArray(parsed.errors) ? parsed.errors : []
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return {
|
|
121
|
+
chunks: [],
|
|
122
|
+
errors: [
|
|
123
|
+
{
|
|
124
|
+
message: `VB.NET parser returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
132
|
+
const fs = await import("node:fs");
|
|
133
|
+
const filePath = process.argv[2];
|
|
134
|
+
|
|
135
|
+
if (!filePath) {
|
|
136
|
+
console.error("Usage: vbnet.mjs <file.vb>");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const code = fs.readFileSync(filePath, "utf8");
|
|
141
|
+
const result = parseCode(code, filePath, "vbnet");
|
|
142
|
+
console.log(JSON.stringify(result, null, 2));
|
|
143
|
+
}
|
|
@@ -49,12 +49,26 @@ const fs = require("node:fs");
|
|
|
49
49
|
const embedManifestPath = process.argv[1];
|
|
50
50
|
const data = JSON.parse(fs.readFileSync(embedManifestPath, "utf8"));
|
|
51
51
|
const c = data.counts || {};
|
|
52
|
+
const entities = Number(c.entities ?? 0);
|
|
53
|
+
const output = Number(c.output ?? 0);
|
|
54
|
+
const embedded = Number(c.embedded ?? 0);
|
|
55
|
+
const failed = Number(c.failed ?? 0);
|
|
52
56
|
console.log(`[status] embeddings generated_at=${data.generated_at}`);
|
|
53
57
|
console.log(`[status] embeddings model=${data.model} dim=${data.dimensions ?? 0}`);
|
|
54
|
-
console.log(`[status] embeddings entities=${
|
|
58
|
+
console.log(`[status] embeddings entities=${entities} output=${output} embedded=${embedded} reused=${c.reused ?? 0} failed=${failed}`);
|
|
59
|
+
if (embedded > 0 && output > 0 && failed === 0) {
|
|
60
|
+
console.log("[status] semantic_search=embedding+lexical (ready)");
|
|
61
|
+
} else if (embedded > 0 && output > 0) {
|
|
62
|
+
console.log(`[status] semantic_search=embedding+lexical-partial (failed=${failed})`);
|
|
63
|
+
} else if (entities > 0) {
|
|
64
|
+
console.log("[status] semantic_search=lexical-only (run: ./scripts/context.sh embed)");
|
|
65
|
+
} else {
|
|
66
|
+
console.log("[status] semantic_search=lexical-only (no indexed entities)");
|
|
67
|
+
}
|
|
55
68
|
' "$EMBED_MANIFEST"
|
|
56
69
|
else
|
|
57
70
|
echo "[status] embeddings manifest missing (run: ./scripts/context.sh embed)"
|
|
71
|
+
echo "[status] semantic_search=lexical-only (embeddings manifest missing)"
|
|
58
72
|
fi
|
|
59
73
|
|
|
60
74
|
node -e '
|
|
@@ -273,10 +287,3 @@ try {
|
|
|
273
287
|
console.log(`[status] cortex_update_check=unavailable (${message})`);
|
|
274
288
|
}
|
|
275
289
|
' "$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
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
-
NOTES_DIR="$REPO_ROOT/.context/notes"
|
|
6
|
-
DECISIONS_DIR="$REPO_ROOT/.context/decisions"
|
|
7
|
-
|
|
8
|
-
TITLE="${1:-}"
|
|
9
|
-
if [[ -z "$TITLE" ]]; then
|
|
10
|
-
echo "Usage: ./scripts/context.sh note <title> [text]"
|
|
11
|
-
exit 1
|
|
12
|
-
fi
|
|
13
|
-
shift || true
|
|
14
|
-
|
|
15
|
-
BODY="${*:-}"
|
|
16
|
-
if [[ -z "$BODY" && ! -t 0 ]]; then
|
|
17
|
-
BODY="$(cat)"
|
|
18
|
-
fi
|
|
19
|
-
|
|
20
|
-
mkdir -p "$NOTES_DIR" "$DECISIONS_DIR"
|
|
21
|
-
|
|
22
|
-
DATE_UTC="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
23
|
-
DATE_PREFIX="$(date -u +"%Y-%m-%d")"
|
|
24
|
-
SLUG="$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')"
|
|
25
|
-
if [[ -z "$SLUG" ]]; then
|
|
26
|
-
SLUG="note"
|
|
27
|
-
fi
|
|
28
|
-
FILE_PATH="$NOTES_DIR/${DATE_PREFIX}-${SLUG}.md"
|
|
29
|
-
|
|
30
|
-
if [[ -f "$FILE_PATH" ]]; then
|
|
31
|
-
FILE_PATH="$NOTES_DIR/${DATE_PREFIX}-${SLUG}-$(date -u +"%H%M%S").md"
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
echo "---"
|
|
36
|
-
echo "title: \"$TITLE\""
|
|
37
|
-
echo "created_at: \"$DATE_UTC\""
|
|
38
|
-
echo "kind: \"team-note\""
|
|
39
|
-
echo "source_of_truth: false"
|
|
40
|
-
echo "trust_level: 60"
|
|
41
|
-
echo "status: active"
|
|
42
|
-
echo "---"
|
|
43
|
-
echo
|
|
44
|
-
if [[ -n "$BODY" ]]; then
|
|
45
|
-
echo "$BODY"
|
|
46
|
-
else
|
|
47
|
-
echo "_Skriv ner bakgrund, beslut, edge-cases och länkar här._"
|
|
48
|
-
fi
|
|
49
|
-
echo
|
|
50
|
-
echo "## Follow-up"
|
|
51
|
-
echo "- [ ] Convert to ADR in .context/decisions/ if this becomes a long-term rule."
|
|
52
|
-
} > "$FILE_PATH"
|
|
53
|
-
|
|
54
|
-
echo "[note] wrote $FILE_PATH"
|
|
55
|
-
echo "[note] run: ./scripts/context.sh update"
|
|
@@ -1,310 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
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
|