@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,374 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ WATCH_DIR="$REPO_ROOT/.context/watch"
6
+ PID_FILE="$WATCH_DIR/watch.pid"
7
+ STAMP_FILE="$WATCH_DIR/last-stamp.sha1"
8
+ HEARTBEAT_FILE="$WATCH_DIR/heartbeat.txt"
9
+ LOG_FILE="$WATCH_DIR/watch.log"
10
+ MODE_FILE="$WATCH_DIR/mode.txt"
11
+
12
+ ACTION="${1:-status}"
13
+ if [[ $# -gt 0 ]]; then
14
+ shift
15
+ fi
16
+
17
+ INTERVAL_SEC="${CORTEX_WATCH_INTERVAL:-45}"
18
+ DEBOUNCE_SEC="${CORTEX_WATCH_DEBOUNCE:-8}"
19
+ WATCH_MODE="${CORTEX_WATCH_MODE:-auto}"
20
+ EVENT_BACKEND=""
21
+
22
+ print_usage() {
23
+ cat <<'USAGE'
24
+ Usage: ./scripts/watch.sh [start|stop|status|run|once] [--interval <sec>] [--debounce <sec>] [--mode <auto|event|poll>]
25
+
26
+ Commands:
27
+ start Start background watch loop
28
+ stop Stop running watch loop
29
+ status Show running status
30
+ run Run loop in foreground
31
+ once Run one immediate cortex update
32
+ USAGE
33
+ }
34
+
35
+ ensure_number() {
36
+ local raw="$1"
37
+ local name="$2"
38
+ if ! [[ "$raw" =~ ^[0-9]+$ ]] || [[ "$raw" -lt 1 ]]; then
39
+ echo "[watch] $name must be a positive integer (got '$raw')"
40
+ exit 1
41
+ fi
42
+ }
43
+
44
+ ensure_mode() {
45
+ case "$1" in
46
+ auto|event|poll)
47
+ ;;
48
+ *)
49
+ echo "[watch] mode must be one of: auto|event|poll (got '$1')"
50
+ exit 1
51
+ ;;
52
+ esac
53
+ }
54
+
55
+ is_running() {
56
+ if [[ ! -f "$PID_FILE" ]]; then
57
+ return 1
58
+ fi
59
+
60
+ local pid
61
+ pid="$(cat "$PID_FILE" 2>/dev/null || true)"
62
+ if [[ -z "$pid" ]]; then
63
+ return 1
64
+ fi
65
+
66
+ if kill -0 "$pid" >/dev/null 2>&1; then
67
+ return 0
68
+ fi
69
+
70
+ return 1
71
+ }
72
+
73
+ detect_event_backend() {
74
+ if command -v inotifywait >/dev/null 2>&1; then
75
+ echo "inotifywait"
76
+ return 0
77
+ fi
78
+
79
+ if command -v fswatch >/dev/null 2>&1; then
80
+ echo "fswatch"
81
+ return 0
82
+ fi
83
+
84
+ return 1
85
+ }
86
+
87
+ resolve_mode() {
88
+ case "$WATCH_MODE" in
89
+ auto)
90
+ if EVENT_BACKEND="$(detect_event_backend)"; then
91
+ WATCH_MODE="event"
92
+ else
93
+ WATCH_MODE="poll"
94
+ EVENT_BACKEND=""
95
+ fi
96
+ ;;
97
+ event)
98
+ EVENT_BACKEND="$(detect_event_backend || true)"
99
+ if [[ -z "$EVENT_BACKEND" ]]; then
100
+ echo "[watch] mode=event requested but no event backend found"
101
+ echo "[watch] install inotifywait (Linux) or fswatch (macOS), or use --mode poll"
102
+ exit 1
103
+ fi
104
+ ;;
105
+ poll)
106
+ EVENT_BACKEND=""
107
+ ;;
108
+ esac
109
+ }
110
+
111
+ status_digest() {
112
+ if command -v git >/dev/null 2>&1 && git -C "$REPO_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
113
+ local head_commit
114
+ local head_ref
115
+ head_commit="$(git -C "$REPO_ROOT" rev-parse --verify HEAD 2>/dev/null || echo "NO_HEAD")"
116
+ head_ref="$(git -C "$REPO_ROOT" symbolic-ref --short -q HEAD 2>/dev/null || echo "DETACHED")"
117
+
118
+ {
119
+ printf 'HEAD:%s\n' "$head_commit"
120
+ printf 'REF:%s\n' "$head_ref"
121
+ git -C "$REPO_ROOT" status --porcelain=1 --untracked-files=all \
122
+ | awk '
123
+ {
124
+ path = substr($0, 4);
125
+ if (path ~ /^\.context\//) next;
126
+ if (path ~ /^mcp\/node_modules\//) next;
127
+ if (path ~ /^mcp\/dist\//) next;
128
+ if (path ~ /^mcp\/\.npm-cache\//) next;
129
+ if (path ~ /^scripts\/parsers\/node_modules\//) next;
130
+ if (path ~ /^scripts\/parsers\/\.npm-cache\//) next;
131
+ print $0;
132
+ }
133
+ '
134
+ } \
135
+ | shasum -a 1 \
136
+ | awk '{print $1}'
137
+ return
138
+ fi
139
+
140
+ # Fallback for non-git directories.
141
+ find "$REPO_ROOT" -type f \
142
+ ! -path "$REPO_ROOT/.context/*" \
143
+ ! -path "$REPO_ROOT/mcp/node_modules/*" \
144
+ ! -path "$REPO_ROOT/mcp/dist/*" \
145
+ ! -path "$REPO_ROOT/mcp/.npm-cache/*" \
146
+ ! -path "$REPO_ROOT/scripts/parsers/node_modules/*" \
147
+ ! -path "$REPO_ROOT/scripts/parsers/.npm-cache/*" \
148
+ -print \
149
+ | LC_ALL=C sort \
150
+ | shasum -a 1 \
151
+ | awk '{print $1}'
152
+ }
153
+
154
+ run_update() {
155
+ local start_ts
156
+ start_ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
157
+ echo "[watch] update start at $start_ts"
158
+
159
+ if bash "$REPO_ROOT/scripts/context.sh" update; then
160
+ echo "[watch] update success"
161
+ return 0
162
+ fi
163
+
164
+ echo "[watch] update failed"
165
+ return 1
166
+ }
167
+
168
+ maybe_run_update() {
169
+ local previous_digest="$1"
170
+
171
+ sleep "$DEBOUNCE_SEC"
172
+ local settled_digest
173
+ settled_digest="$(status_digest)"
174
+
175
+ if [[ "$settled_digest" == "$previous_digest" ]]; then
176
+ echo "$previous_digest"
177
+ return 0
178
+ fi
179
+
180
+ if run_update; then
181
+ :
182
+ else
183
+ # Move forward anyway to avoid tight retry loops on persistent failures.
184
+ :
185
+ fi
186
+
187
+ echo "$settled_digest" > "$STAMP_FILE"
188
+ echo "$settled_digest"
189
+ }
190
+
191
+ wait_for_change_event() {
192
+ case "$EVENT_BACKEND" in
193
+ inotifywait)
194
+ inotifywait -q -r \
195
+ -e modify,create,delete,move \
196
+ --exclude '(^|/)\\.git(/|$)|(^|/)\\.context(/|$)|(^|/)mcp/(node_modules|dist|\\.npm-cache)(/|$)|(^|/)scripts/parsers/(node_modules|\\.npm-cache)(/|$)' \
197
+ "$REPO_ROOT" >/dev/null 2>&1 || true
198
+ ;;
199
+ fswatch)
200
+ fswatch -1 -r \
201
+ --exclude '(^|/)\\.git(/|$)' \
202
+ --exclude '(^|/)\\.context(/|$)' \
203
+ --exclude '(^|/)mcp/node_modules(/|$)' \
204
+ --exclude '(^|/)mcp/dist(/|$)' \
205
+ --exclude '(^|/)mcp/\\.npm-cache(/|$)' \
206
+ --exclude '(^|/)scripts/parsers/node_modules(/|$)' \
207
+ --exclude '(^|/)scripts/parsers/\\.npm-cache(/|$)' \
208
+ "$REPO_ROOT" >/dev/null 2>&1 || true
209
+ ;;
210
+ *)
211
+ echo "[watch] internal error: missing event backend"
212
+ exit 1
213
+ ;;
214
+ esac
215
+ }
216
+
217
+ run_poll_loop() {
218
+ local previous_digest="$1"
219
+
220
+ while true; do
221
+ date -u +"%Y-%m-%dT%H:%M:%SZ" > "$HEARTBEAT_FILE"
222
+ local current_digest
223
+ current_digest="$(status_digest)"
224
+
225
+ if [[ "$current_digest" != "$previous_digest" ]]; then
226
+ echo "[watch] change detected, waiting ${DEBOUNCE_SEC}s"
227
+ previous_digest="$(maybe_run_update "$previous_digest")"
228
+ fi
229
+
230
+ sleep "$INTERVAL_SEC"
231
+ done
232
+ }
233
+
234
+ run_event_loop() {
235
+ local previous_digest="$1"
236
+
237
+ while true; do
238
+ date -u +"%Y-%m-%dT%H:%M:%SZ" > "$HEARTBEAT_FILE"
239
+ wait_for_change_event
240
+ date -u +"%Y-%m-%dT%H:%M:%SZ" > "$HEARTBEAT_FILE"
241
+ echo "[watch] change event detected, waiting ${DEBOUNCE_SEC}s"
242
+ previous_digest="$(maybe_run_update "$previous_digest")"
243
+ done
244
+ }
245
+
246
+ run_loop() {
247
+ mkdir -p "$WATCH_DIR"
248
+
249
+ local previous_digest
250
+ if [[ -f "$STAMP_FILE" ]]; then
251
+ previous_digest="$(cat "$STAMP_FILE" 2>/dev/null || true)"
252
+ else
253
+ previous_digest="$(status_digest)"
254
+ echo "$previous_digest" > "$STAMP_FILE"
255
+ fi
256
+
257
+ resolve_mode
258
+ local backend_label
259
+ backend_label="${EVENT_BACKEND:-none}"
260
+ printf 'mode=%s\nbackend=%s\ninterval=%s\ndebounce=%s\n' "$WATCH_MODE" "$backend_label" "$INTERVAL_SEC" "$DEBOUNCE_SEC" > "$MODE_FILE"
261
+ echo "[watch] running (mode=${WATCH_MODE} backend=${backend_label} interval=${INTERVAL_SEC}s debounce=${DEBOUNCE_SEC}s)"
262
+
263
+ if [[ "$WATCH_MODE" == "event" ]]; then
264
+ run_event_loop "$previous_digest"
265
+ return
266
+ fi
267
+
268
+ run_poll_loop "$previous_digest"
269
+ }
270
+
271
+ while [[ $# -gt 0 ]]; do
272
+ case "$1" in
273
+ --interval)
274
+ INTERVAL_SEC="${2:-}"
275
+ shift 2
276
+ ;;
277
+ --debounce)
278
+ DEBOUNCE_SEC="${2:-}"
279
+ shift 2
280
+ ;;
281
+ --mode)
282
+ WATCH_MODE="${2:-}"
283
+ shift 2
284
+ ;;
285
+ -h|--help)
286
+ print_usage
287
+ exit 0
288
+ ;;
289
+ *)
290
+ echo "Unknown option: $1"
291
+ print_usage
292
+ exit 1
293
+ ;;
294
+ esac
295
+ done
296
+
297
+ ensure_number "$INTERVAL_SEC" "interval"
298
+ ensure_number "$DEBOUNCE_SEC" "debounce"
299
+ ensure_mode "$WATCH_MODE"
300
+ mkdir -p "$WATCH_DIR"
301
+
302
+ case "$ACTION" in
303
+ start)
304
+ if is_running; then
305
+ echo "[watch] already running (pid $(cat "$PID_FILE"))"
306
+ exit 0
307
+ fi
308
+
309
+ nohup bash "$0" run --interval "$INTERVAL_SEC" --debounce "$DEBOUNCE_SEC" --mode "$WATCH_MODE" >> "$LOG_FILE" 2>&1 &
310
+ WATCH_PID=$!
311
+ echo "$WATCH_PID" > "$PID_FILE"
312
+ sleep 1
313
+
314
+ if kill -0 "$WATCH_PID" >/dev/null 2>&1; then
315
+ echo "[watch] started (pid $WATCH_PID)"
316
+ echo "[watch] log: $LOG_FILE"
317
+ exit 0
318
+ fi
319
+
320
+ echo "[watch] failed to start"
321
+ exit 1
322
+ ;;
323
+
324
+ stop)
325
+ if ! is_running; then
326
+ rm -f "$PID_FILE"
327
+ echo "[watch] not running"
328
+ exit 0
329
+ fi
330
+
331
+ WATCH_PID="$(cat "$PID_FILE")"
332
+ kill "$WATCH_PID" >/dev/null 2>&1 || true
333
+ sleep 1
334
+ if kill -0 "$WATCH_PID" >/dev/null 2>&1; then
335
+ kill -9 "$WATCH_PID" >/dev/null 2>&1 || true
336
+ fi
337
+ rm -f "$PID_FILE"
338
+ echo "[watch] stopped"
339
+ ;;
340
+
341
+ status)
342
+ if is_running; then
343
+ echo "[watch] running (pid $(cat "$PID_FILE"))"
344
+ echo "[watch] log: $LOG_FILE"
345
+ if [[ -f "$MODE_FILE" ]]; then
346
+ local_mode="$(tr '\n' ' ' < "$MODE_FILE" | sed 's/[[:space:]]\+$//')"
347
+ echo "[watch] $local_mode"
348
+ fi
349
+ if [[ -f "$HEARTBEAT_FILE" ]]; then
350
+ echo "[watch] heartbeat: $(cat "$HEARTBEAT_FILE")"
351
+ fi
352
+ else
353
+ echo "[watch] stopped"
354
+ fi
355
+ ;;
356
+
357
+ run)
358
+ run_loop
359
+ ;;
360
+
361
+ once)
362
+ run_update
363
+ ;;
364
+
365
+ help|-h|--help)
366
+ print_usage
367
+ ;;
368
+
369
+ *)
370
+ echo "Unknown command: $ACTION"
371
+ print_usage
372
+ exit 1
373
+ ;;
374
+ esac