@jefuriiij/synthra 0.1.24 → 0.2.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/CHANGELOG.md +465 -403
- package/dist/cli/index.js +563 -114
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +14 -3
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +446 -66
- package/dist/server/index.js.map +1 -1
- package/package.json +73 -66
package/dist/cli/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var init_package = __esm({
|
|
|
18
18
|
"package.json"() {
|
|
19
19
|
package_default = {
|
|
20
20
|
name: "@jefuriiij/synthra",
|
|
21
|
-
version: "0.
|
|
21
|
+
version: "0.2.0",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -33,7 +33,12 @@ var init_package = __esm({
|
|
|
33
33
|
dev: "tsup --watch",
|
|
34
34
|
test: "vitest run",
|
|
35
35
|
"test:watch": "vitest",
|
|
36
|
-
|
|
36
|
+
"test:coverage": "vitest run --coverage",
|
|
37
|
+
typecheck: "tsc --noEmit",
|
|
38
|
+
lint: "biome lint .",
|
|
39
|
+
format: "biome format --write .",
|
|
40
|
+
check: "biome check .",
|
|
41
|
+
"check:fix": "biome check --write ."
|
|
37
42
|
},
|
|
38
43
|
files: [
|
|
39
44
|
"dist",
|
|
@@ -75,8 +80,10 @@ var init_package = __esm({
|
|
|
75
80
|
"web-tree-sitter": "^0.25.10"
|
|
76
81
|
},
|
|
77
82
|
devDependencies: {
|
|
83
|
+
"@biomejs/biome": "^2.4.16",
|
|
78
84
|
"@types/cross-spawn": "^6.0.6",
|
|
79
85
|
"@types/node": "^25.9.1",
|
|
86
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
80
87
|
tsup: "^8.5.1",
|
|
81
88
|
typescript: "^6.0.3",
|
|
82
89
|
vitest: "^4.1.7"
|
|
@@ -158,6 +165,8 @@ function resolvePaths(projectRoot) {
|
|
|
158
165
|
tokenLog: join(graphDir, "token_log.jsonl"),
|
|
159
166
|
gateLog: join(graphDir, "gate_log.jsonl"),
|
|
160
167
|
toolLog: join(graphDir, "tool_log.jsonl"),
|
|
168
|
+
accessLog: join(graphDir, "access_log.jsonl"),
|
|
169
|
+
learnStore: join(graphDir, "learn_store.json"),
|
|
161
170
|
mcpPort: join(graphDir, "mcp_port"),
|
|
162
171
|
mcpServerLog: join(graphDir, "mcp_server.log"),
|
|
163
172
|
mcpServerErrLog: join(graphDir, "mcp_server.err.log"),
|
|
@@ -374,7 +383,9 @@ async function computeDashboardData(activePaths, recentN = 500) {
|
|
|
374
383
|
const loaded = await Promise.all(
|
|
375
384
|
allEntries.map((e) => loadProjectFiles(e.path, e.name, e.last_seen))
|
|
376
385
|
);
|
|
377
|
-
const projects = loaded.map(summarize).sort(
|
|
386
|
+
const projects = loaded.map(summarize).sort(
|
|
387
|
+
(a, b) => b.total_input_tokens + b.total_output_tokens - (a.total_input_tokens + a.total_output_tokens)
|
|
388
|
+
);
|
|
378
389
|
const activeFiles = loaded.find((p) => p.path === activePath) ?? {
|
|
379
390
|
path: activePath,
|
|
380
391
|
name: activeName,
|
|
@@ -1440,12 +1451,19 @@ if [ ! -f "$PORT_FILE" ]; then exit 0; fi
|
|
|
1440
1451
|
PORT=$(cat "$PORT_FILE" 2>/dev/null | tr -d '[:space:]')
|
|
1441
1452
|
if [ -z "$PORT" ]; then exit 0; fi
|
|
1442
1453
|
|
|
1454
|
+
# Parse the primer with jq, not sed. The primer now carries a multi-line "Since you
|
|
1455
|
+
# were last here" resume digest with quotes and newlines, so the old greedy sed capture
|
|
1456
|
+
# (.*") both over-ran into the trailing "port" field and broke on inner quotes. jq -r
|
|
1457
|
+
# also decodes JSON escapes, so we print with %s (not %b). No jq \u2192 no primer (matches
|
|
1458
|
+
# prime.sh / stop.sh \u2014 completes the jq migration across all bash hooks).
|
|
1459
|
+
if ! command -v jq >/dev/null 2>&1; then exit 0; fi
|
|
1460
|
+
|
|
1443
1461
|
PRIMER=$(curl -sS --max-time 3 "http://127.0.0.1:$PORT/prime" 2>/dev/null \\
|
|
1444
|
-
|
|
|
1462
|
+
| jq -r '.primer // empty' 2>/dev/null \\
|
|
1445
1463
|
| head -c 8000)
|
|
1446
1464
|
|
|
1447
1465
|
if [ -n "$PRIMER" ]; then
|
|
1448
|
-
printf '%
|
|
1466
|
+
printf '%s\\n' "$PRIMER"
|
|
1449
1467
|
fi
|
|
1450
1468
|
exit 0
|
|
1451
1469
|
`;
|
|
@@ -1458,6 +1476,8 @@ var pre_tool_use_default2 = `#!/usr/bin/env bash\r
|
|
|
1458
1476
|
# PreToolUse hook \u2014 bash. POSTs the tool call to /gate; if server returns\r
|
|
1459
1477
|
# "block", emits the deny-decision JSON to stdout for Claude Code to enforce.\r
|
|
1460
1478
|
# Always exits 0; server failures leave Claude untouched.\r
|
|
1479
|
+
# Requires \`jq\` to read the gate response; falls back to silent no-op (no\r
|
|
1480
|
+
# enforcement) if absent \u2014 same policy as the Stop/Prime hooks.\r
|
|
1461
1481
|
\r
|
|
1462
1482
|
set +e\r
|
|
1463
1483
|
\r
|
|
@@ -1472,14 +1492,20 @@ if [ -z "$INPUT" ]; then exit 0; fi\r
|
|
|
1472
1492
|
RESP=$(curl -sS --max-time 3 -X POST -H "Content-Type: application/json" \\\r
|
|
1473
1493
|
--data "$INPUT" "http://127.0.0.1:$PORT/gate" 2>/dev/null)\r
|
|
1474
1494
|
\r
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1495
|
+
# Parse the gate response with jq, not a greedy sed capture. The block \`reason\`\r
|
|
1496
|
+
# legitimately contains double quotes (it quotes the query, e.g. "login"), so the\r
|
|
1497
|
+
# old sed capture (\\(.*\\)") both over-ran into the trailing JSON fields and, once\r
|
|
1498
|
+
# embedded raw in the heredoc, produced invalid hook output. jq reads each field\r
|
|
1499
|
+
# and re-emits the deny object with correct escaping. (matches stop.sh / prime.sh,\r
|
|
1500
|
+
# jq fix #1.) No jq \u2192 no enforcement; bail silently like the other hooks.\r
|
|
1501
|
+
if ! command -v jq >/dev/null 2>&1; then exit 0; fi\r
|
|
1502
|
+
\r
|
|
1503
|
+
DECISION=$(printf '%s' "$RESP" | jq -r '.decision // empty' 2>/dev/null)\r
|
|
1504
|
+
if [ "$DECISION" = "block" ]; then\r
|
|
1505
|
+
REASON=$(printf '%s' "$RESP" | jq -r '.reason // empty' 2>/dev/null)\r
|
|
1506
|
+
jq -nc --arg r "$REASON" \\\r
|
|
1507
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'\r
|
|
1508
|
+
fi\r
|
|
1483
1509
|
exit 0\r
|
|
1484
1510
|
`;
|
|
1485
1511
|
|
|
@@ -1688,7 +1714,13 @@ exit 0\r
|
|
|
1688
1714
|
// src/hooks/installer.ts
|
|
1689
1715
|
var SCRIPTS = [
|
|
1690
1716
|
{ event: "SessionStart", baseName: "synthra-prime", ps1: prime_default, sh: prime_default2 },
|
|
1691
|
-
{
|
|
1717
|
+
{
|
|
1718
|
+
event: "PreToolUse",
|
|
1719
|
+
matcher: "Grep|Glob",
|
|
1720
|
+
baseName: "synthra-pre-tool-use",
|
|
1721
|
+
ps1: pre_tool_use_default,
|
|
1722
|
+
sh: pre_tool_use_default2
|
|
1723
|
+
},
|
|
1692
1724
|
{ event: "PreCompact", baseName: "synthra-pre-compact", ps1: pre_compact_default, sh: pre_compact_default2 },
|
|
1693
1725
|
{ event: "Stop", baseName: "synthra-stop", ps1: stop_default, sh: stop_default2 }
|
|
1694
1726
|
];
|
|
@@ -1765,7 +1797,7 @@ async function installHooks(paths) {
|
|
|
1765
1797
|
// src/server/http.ts
|
|
1766
1798
|
import { serve as serve2 } from "@hono/node-server";
|
|
1767
1799
|
import { Hono as Hono2 } from "hono";
|
|
1768
|
-
import { writeFile as
|
|
1800
|
+
import { writeFile as writeFile10 } from "fs/promises";
|
|
1769
1801
|
|
|
1770
1802
|
// src/activity/activity-log.ts
|
|
1771
1803
|
import { appendFile, mkdir as mkdir3 } from "fs/promises";
|
|
@@ -2226,7 +2258,20 @@ function extractKeywords(content, _ext) {
|
|
|
2226
2258
|
}
|
|
2227
2259
|
|
|
2228
2260
|
// src/scanner/extract.ts
|
|
2229
|
-
var RESOLVE_EXTS = [
|
|
2261
|
+
var RESOLVE_EXTS = [
|
|
2262
|
+
".ts",
|
|
2263
|
+
".tsx",
|
|
2264
|
+
".js",
|
|
2265
|
+
".jsx",
|
|
2266
|
+
".mjs",
|
|
2267
|
+
".cjs",
|
|
2268
|
+
".py",
|
|
2269
|
+
".svelte",
|
|
2270
|
+
".vue",
|
|
2271
|
+
".dart",
|
|
2272
|
+
".html",
|
|
2273
|
+
".hubl"
|
|
2274
|
+
];
|
|
2230
2275
|
var INDEX_FILES = ["index.ts", "index.tsx", "index.js", "index.jsx", "__init__.py"];
|
|
2231
2276
|
function fileId(relPath) {
|
|
2232
2277
|
return `file:${relPath}`;
|
|
@@ -3325,7 +3370,7 @@ import { basename as basename4 } from "path";
|
|
|
3325
3370
|
// src/hooks/claude-md.ts
|
|
3326
3371
|
import { readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
3327
3372
|
import { basename as basename3, dirname as dirname6 } from "path";
|
|
3328
|
-
var POLICY_VERSION =
|
|
3373
|
+
var POLICY_VERSION = 6;
|
|
3329
3374
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
3330
3375
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
3331
3376
|
var ANY_BLOCK_RE = /<!--\s*synthra-policy\s+v\d+\s+BEGIN\s*-->[\s\S]*?<!--\s*synthra-policy\s+v\d+\s+END\s*-->\s*/g;
|
|
@@ -3416,6 +3461,17 @@ function policyBlock() {
|
|
|
3416
3461
|
"- Don't call `graph_continue` more than once per turn.",
|
|
3417
3462
|
"- Don't read whole files when a symbol-level read would suffice.",
|
|
3418
3463
|
"",
|
|
3464
|
+
"### Resuming a session",
|
|
3465
|
+
"",
|
|
3466
|
+
'At session start the primer may begin with a **"Since you were last here"**',
|
|
3467
|
+
"digest \u2014 recent commits, files touched, open next-steps, and recent",
|
|
3468
|
+
"decisions carried over from the previous session. **Trust it.** It is the",
|
|
3469
|
+
"cheapest possible orientation: do NOT re-run `graph_continue` or Grep just",
|
|
3470
|
+
'to rediscover "what were we doing / what changed" \u2014 that work is already',
|
|
3471
|
+
'done. For the concrete next steps, `context_recall({kind:"next"})` returns',
|
|
3472
|
+
"them verbatim. Only reach for fresh retrieval when the task moves beyond",
|
|
3473
|
+
"what the digest covers.",
|
|
3474
|
+
"",
|
|
3419
3475
|
"### Session-end resume note",
|
|
3420
3476
|
"",
|
|
3421
3477
|
`When the user signals they're done (e.g. "bye", "wrap up", "done"),`,
|
|
@@ -3594,7 +3650,9 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
3594
3650
|
if (boot.gitignoreUpdated) log.info(" updated .gitignore");
|
|
3595
3651
|
if (boot.claudeMdCreated) {
|
|
3596
3652
|
log.info(" created CLAUDE.md \u2014 onboarding skeleton for the agent");
|
|
3597
|
-
log.info(
|
|
3653
|
+
log.info(
|
|
3654
|
+
" \u21B3 fill in Build / Conventions / Decisions (or run /init in Claude to auto-draft)"
|
|
3655
|
+
);
|
|
3598
3656
|
} else if (boot.claudeMdUpdated) {
|
|
3599
3657
|
log.info(" updated CLAUDE.md");
|
|
3600
3658
|
}
|
|
@@ -3642,11 +3700,191 @@ async function scanCommand(rawPath) {
|
|
|
3642
3700
|
return scanProject(rawPath);
|
|
3643
3701
|
}
|
|
3644
3702
|
|
|
3703
|
+
// src/learn/store.ts
|
|
3704
|
+
import { appendFile as appendFile2, mkdir as mkdir6, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
3705
|
+
import { dirname as dirname7 } from "path";
|
|
3706
|
+
|
|
3707
|
+
// src/learn/usage.ts
|
|
3708
|
+
var LEARN_SCHEMA_VERSION = 1;
|
|
3709
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3710
|
+
function halfLifeMs() {
|
|
3711
|
+
const env = Number(process.env.SYN_LEARN_HALFLIFE_DAYS);
|
|
3712
|
+
const days = Number.isFinite(env) && env > 0 ? env : 7;
|
|
3713
|
+
return days * DAY_MS;
|
|
3714
|
+
}
|
|
3715
|
+
function weightFor(source) {
|
|
3716
|
+
switch (source) {
|
|
3717
|
+
case "register_edit":
|
|
3718
|
+
return 2;
|
|
3719
|
+
case "read":
|
|
3720
|
+
return 1;
|
|
3721
|
+
default:
|
|
3722
|
+
return 0;
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
function emptyStore() {
|
|
3726
|
+
return {
|
|
3727
|
+
schema_version: LEARN_SCHEMA_VERSION,
|
|
3728
|
+
asOf: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3729
|
+
files: {}
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
function decayFactor(fromTs, toMs, hl) {
|
|
3733
|
+
const fromMs = Date.parse(fromTs);
|
|
3734
|
+
if (!Number.isFinite(fromMs)) return 1;
|
|
3735
|
+
const dt = toMs - fromMs;
|
|
3736
|
+
if (dt <= 0) return 1;
|
|
3737
|
+
return Math.exp(-(Math.LN2 / hl) * dt);
|
|
3738
|
+
}
|
|
3739
|
+
function foldEvent(store, ev) {
|
|
3740
|
+
const w = weightFor(ev.source);
|
|
3741
|
+
if (w <= 0 || !ev.path) return store;
|
|
3742
|
+
const tMs = Date.parse(ev.ts);
|
|
3743
|
+
if (!Number.isFinite(tMs)) return store;
|
|
3744
|
+
const hl = halfLifeMs();
|
|
3745
|
+
const prev = store.files[ev.path];
|
|
3746
|
+
if (prev) {
|
|
3747
|
+
const decayed = prev.decayed * decayFactor(prev.lastTs, tMs, hl) + w;
|
|
3748
|
+
store.files[ev.path] = { count: prev.count + 1, decayed, lastTs: ev.ts };
|
|
3749
|
+
} else {
|
|
3750
|
+
store.files[ev.path] = { count: 1, decayed: w, lastTs: ev.ts };
|
|
3751
|
+
}
|
|
3752
|
+
return store;
|
|
3753
|
+
}
|
|
3754
|
+
function effectiveScores(store, nowMs) {
|
|
3755
|
+
const hl = halfLifeMs();
|
|
3756
|
+
const out = /* @__PURE__ */ new Map();
|
|
3757
|
+
for (const [path, stat6] of Object.entries(store.files)) {
|
|
3758
|
+
const eff = stat6.decayed * decayFactor(stat6.lastTs, nowMs, hl);
|
|
3759
|
+
if (eff > 0.01) out.set(path, eff);
|
|
3760
|
+
}
|
|
3761
|
+
return out;
|
|
3762
|
+
}
|
|
3763
|
+
function recomputeFromLog(events) {
|
|
3764
|
+
const store = emptyStore();
|
|
3765
|
+
for (const ev of events) foldEvent(store, ev);
|
|
3766
|
+
return store;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
// src/learn/store.ts
|
|
3770
|
+
async function readLearnStore(path) {
|
|
3771
|
+
try {
|
|
3772
|
+
const raw = await readFile11(path, "utf8");
|
|
3773
|
+
const parsed = JSON.parse(raw);
|
|
3774
|
+
if (parsed.schema_version !== LEARN_SCHEMA_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
|
|
3775
|
+
return emptyStore();
|
|
3776
|
+
}
|
|
3777
|
+
return {
|
|
3778
|
+
schema_version: LEARN_SCHEMA_VERSION,
|
|
3779
|
+
asOf: typeof parsed.asOf === "string" ? parsed.asOf : emptyStore().asOf,
|
|
3780
|
+
files: parsed.files
|
|
3781
|
+
};
|
|
3782
|
+
} catch {
|
|
3783
|
+
return emptyStore();
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
async function writeLearnStore(path, store) {
|
|
3787
|
+
try {
|
|
3788
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
3789
|
+
await writeFile6(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
3790
|
+
} catch {
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
async function readAccessLog(path) {
|
|
3794
|
+
try {
|
|
3795
|
+
const raw = await readFile11(path, "utf8");
|
|
3796
|
+
const out = [];
|
|
3797
|
+
for (const line of raw.split("\n")) {
|
|
3798
|
+
const t = line.trim();
|
|
3799
|
+
if (!t) continue;
|
|
3800
|
+
try {
|
|
3801
|
+
const ev = JSON.parse(t);
|
|
3802
|
+
if (ev && typeof ev.ts === "string" && typeof ev.path === "string" && typeof ev.source === "string") {
|
|
3803
|
+
out.push(ev);
|
|
3804
|
+
}
|
|
3805
|
+
} catch {
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
return out;
|
|
3809
|
+
} catch {
|
|
3810
|
+
return [];
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
async function appendAccess(path, ev) {
|
|
3814
|
+
try {
|
|
3815
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
3816
|
+
await appendFile2(path, JSON.stringify(ev) + "\n", "utf8");
|
|
3817
|
+
} catch {
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
// src/learn/runtime.ts
|
|
3822
|
+
var PERSIST_DEBOUNCE_MS = 2e3;
|
|
3823
|
+
var LearnRuntime = class _LearnRuntime {
|
|
3824
|
+
constructor(accessLogPath, storePath, store) {
|
|
3825
|
+
this.accessLogPath = accessLogPath;
|
|
3826
|
+
this.storePath = storePath;
|
|
3827
|
+
this.store = store;
|
|
3828
|
+
}
|
|
3829
|
+
accessLogPath;
|
|
3830
|
+
storePath;
|
|
3831
|
+
store;
|
|
3832
|
+
dirty = false;
|
|
3833
|
+
timer = null;
|
|
3834
|
+
/** Load the aggregate from disk; if it's empty but a raw log exists, replay it
|
|
3835
|
+
* (the log is the source of truth). Always succeeds — falls back to empty. */
|
|
3836
|
+
static async load(accessLogPath, storePath) {
|
|
3837
|
+
let store = await readLearnStore(storePath);
|
|
3838
|
+
if (Object.keys(store.files).length === 0) {
|
|
3839
|
+
const events = await readAccessLog(accessLogPath);
|
|
3840
|
+
if (events.length > 0) store = recomputeFromLog(events);
|
|
3841
|
+
}
|
|
3842
|
+
return new _LearnRuntime(accessLogPath, storePath, store);
|
|
3843
|
+
}
|
|
3844
|
+
/** Record an access: append to the durable log + fold into the in-memory
|
|
3845
|
+
* aggregate. Best-effort — never throws into a tool call. */
|
|
3846
|
+
async record(ev) {
|
|
3847
|
+
await appendAccess(this.accessLogPath, ev);
|
|
3848
|
+
foldEvent(this.store, ev);
|
|
3849
|
+
this.schedulePersist();
|
|
3850
|
+
}
|
|
3851
|
+
/** Decayed path→weight map for the ranker, as of now. */
|
|
3852
|
+
effectiveScores(nowMs = Date.now()) {
|
|
3853
|
+
return effectiveScores(this.store, nowMs);
|
|
3854
|
+
}
|
|
3855
|
+
schedulePersist() {
|
|
3856
|
+
this.dirty = true;
|
|
3857
|
+
if (this.timer) return;
|
|
3858
|
+
this.timer = setTimeout(() => {
|
|
3859
|
+
this.timer = null;
|
|
3860
|
+
void this.flush();
|
|
3861
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
3862
|
+
this.timer.unref?.();
|
|
3863
|
+
}
|
|
3864
|
+
/** Persist the aggregate if it changed since the last write. Called on the
|
|
3865
|
+
* debounce and on server shutdown. */
|
|
3866
|
+
async flush() {
|
|
3867
|
+
if (this.timer) {
|
|
3868
|
+
clearTimeout(this.timer);
|
|
3869
|
+
this.timer = null;
|
|
3870
|
+
}
|
|
3871
|
+
if (!this.dirty) return;
|
|
3872
|
+
this.dirty = false;
|
|
3873
|
+
this.store.asOf = (/* @__PURE__ */ new Date()).toISOString();
|
|
3874
|
+
await writeLearnStore(this.storePath, this.store);
|
|
3875
|
+
}
|
|
3876
|
+
};
|
|
3877
|
+
|
|
3645
3878
|
// src/server/mcp.ts
|
|
3646
|
-
import { appendFile as
|
|
3647
|
-
import { dirname as
|
|
3879
|
+
import { appendFile as appendFile3, mkdir as mkdir9 } from "fs/promises";
|
|
3880
|
+
import { dirname as dirname10 } from "path";
|
|
3648
3881
|
|
|
3649
3882
|
// src/graph/rank.ts
|
|
3883
|
+
var USAGE_BOOST_CAP_DEFAULT = 4;
|
|
3884
|
+
function usageBoostCap() {
|
|
3885
|
+
const env = Number(process.env.SYN_LEARN_BOOST_CAP);
|
|
3886
|
+
return Number.isFinite(env) && env >= 0 ? env : USAGE_BOOST_CAP_DEFAULT;
|
|
3887
|
+
}
|
|
3650
3888
|
var STOPWORDS2 = /* @__PURE__ */ new Set([
|
|
3651
3889
|
"a",
|
|
3652
3890
|
"an",
|
|
@@ -3793,6 +4031,21 @@ function scoreFiles(inputs) {
|
|
|
3793
4031
|
}
|
|
3794
4032
|
}
|
|
3795
4033
|
}
|
|
4034
|
+
const usage = inputs.usageScores;
|
|
4035
|
+
if (usage && usage.size > 0) {
|
|
4036
|
+
let maxU = 0;
|
|
4037
|
+
for (const v of usage.values()) if (v > maxU) maxU = v;
|
|
4038
|
+
if (maxU > 0) {
|
|
4039
|
+
const cap = usageBoostCap();
|
|
4040
|
+
for (const s of scored) {
|
|
4041
|
+
if (s.score <= 0) continue;
|
|
4042
|
+
const u = usage.get(s.file.path) ?? 0;
|
|
4043
|
+
if (u <= 0) continue;
|
|
4044
|
+
s.score += cap * (u / maxU);
|
|
4045
|
+
s.reasons.push(`used\xD7${Math.round(u)}`);
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
3796
4049
|
scored.sort((a, b) => b.score - a.score);
|
|
3797
4050
|
return scored;
|
|
3798
4051
|
}
|
|
@@ -3801,9 +4054,7 @@ function scoreFiles(inputs) {
|
|
|
3801
4054
|
async function retrieve(graph, query, options = {}) {
|
|
3802
4055
|
const topK = options.topK ?? 12;
|
|
3803
4056
|
const qTokens = tokenizeQuery(query);
|
|
3804
|
-
const allFiles = graph.nodes.filter(
|
|
3805
|
-
(n) => n.kind === "file"
|
|
3806
|
-
);
|
|
4057
|
+
const allFiles = graph.nodes.filter((n) => n.kind === "file");
|
|
3807
4058
|
if (allFiles.length === 0 || qTokens.length === 0) {
|
|
3808
4059
|
return {
|
|
3809
4060
|
files: [],
|
|
@@ -3817,7 +4068,8 @@ async function retrieve(graph, query, options = {}) {
|
|
|
3817
4068
|
query,
|
|
3818
4069
|
graph,
|
|
3819
4070
|
recentlyEditedPaths: options.recentlyEditedPaths,
|
|
3820
|
-
sessionKnownPaths: options.sessionKnownPaths
|
|
4071
|
+
sessionKnownPaths: options.sessionKnownPaths,
|
|
4072
|
+
usageScores: options.usageScores
|
|
3821
4073
|
};
|
|
3822
4074
|
const scored = scoreFiles(rankInputs);
|
|
3823
4075
|
const positive = scored.filter((s) => s.score > 0);
|
|
@@ -3850,14 +4102,14 @@ async function retrieve(graph, query, options = {}) {
|
|
|
3850
4102
|
|
|
3851
4103
|
// src/memory/branches.ts
|
|
3852
4104
|
import { execFile as execFile2 } from "child_process";
|
|
3853
|
-
import { readFile as
|
|
4105
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3854
4106
|
import { join as join8 } from "path";
|
|
3855
4107
|
import { promisify as promisify2 } from "util";
|
|
3856
4108
|
var execFileAsync2 = promisify2(execFile2);
|
|
3857
4109
|
async function currentBranch(projectRoot) {
|
|
3858
4110
|
try {
|
|
3859
4111
|
const headPath = join8(projectRoot, ".git", "HEAD");
|
|
3860
|
-
const head = await
|
|
4112
|
+
const head = await readFile12(headPath, "utf8");
|
|
3861
4113
|
const trimmed = head.trim();
|
|
3862
4114
|
const match = trimmed.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
3863
4115
|
if (match?.[1]) return match[1];
|
|
@@ -3907,8 +4159,8 @@ function resolveBranchPaths(contextDir, branch, isDefault) {
|
|
|
3907
4159
|
}
|
|
3908
4160
|
|
|
3909
4161
|
// src/memory/context-md.ts
|
|
3910
|
-
import { mkdir as
|
|
3911
|
-
import { dirname as
|
|
4162
|
+
import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile7 } from "fs/promises";
|
|
4163
|
+
import { dirname as dirname8 } from "path";
|
|
3912
4164
|
var MAX_BULLETS = 3;
|
|
3913
4165
|
function deriveContextMd(entries, branch) {
|
|
3914
4166
|
const tasks = entries.filter((e) => e.type === "task").reverse();
|
|
@@ -3951,17 +4203,17 @@ function formatContextMd(ctx) {
|
|
|
3951
4203
|
return lines.join("\n");
|
|
3952
4204
|
}
|
|
3953
4205
|
async function writeContextMd(path, ctx) {
|
|
3954
|
-
await
|
|
3955
|
-
await
|
|
4206
|
+
await mkdir7(dirname8(path), { recursive: true });
|
|
4207
|
+
await writeFile7(path, formatContextMd(ctx), "utf8");
|
|
3956
4208
|
}
|
|
3957
4209
|
|
|
3958
4210
|
// src/memory/context-store.ts
|
|
3959
|
-
import { mkdir as
|
|
3960
|
-
import { dirname as
|
|
4211
|
+
import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
|
|
4212
|
+
import { dirname as dirname9 } from "path";
|
|
3961
4213
|
var SCHEMA_VERSION3 = 1;
|
|
3962
4214
|
async function readEntries(path) {
|
|
3963
4215
|
try {
|
|
3964
|
-
const raw = await
|
|
4216
|
+
const raw = await readFile14(path, "utf8");
|
|
3965
4217
|
const parsed = JSON.parse(raw);
|
|
3966
4218
|
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
3967
4219
|
} catch {
|
|
@@ -3969,9 +4221,9 @@ async function readEntries(path) {
|
|
|
3969
4221
|
}
|
|
3970
4222
|
}
|
|
3971
4223
|
async function writeEntries(path, entries) {
|
|
3972
|
-
await
|
|
4224
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
3973
4225
|
const store = { schema_version: SCHEMA_VERSION3, entries };
|
|
3974
|
-
await
|
|
4226
|
+
await writeFile8(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
3975
4227
|
}
|
|
3976
4228
|
async function appendEntry(path, entry) {
|
|
3977
4229
|
const entries = await readEntries(path);
|
|
@@ -4245,7 +4497,10 @@ var TOOLS = [
|
|
|
4245
4497
|
inputSchema: {
|
|
4246
4498
|
type: "object",
|
|
4247
4499
|
properties: {
|
|
4248
|
-
query: {
|
|
4500
|
+
query: {
|
|
4501
|
+
type: "string",
|
|
4502
|
+
description: "Natural-language description of what you're looking for."
|
|
4503
|
+
}
|
|
4249
4504
|
},
|
|
4250
4505
|
required: ["query"]
|
|
4251
4506
|
}
|
|
@@ -4401,9 +4656,7 @@ function blastRadius(args, ctx) {
|
|
|
4401
4656
|
const maxDepth = typeof args?.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 3;
|
|
4402
4657
|
if (!targetRaw) return errorContent("blast_radius: 'target' (string) is required");
|
|
4403
4658
|
const filePath = targetRaw.split("::", 1)[0]?.trim() ?? targetRaw;
|
|
4404
|
-
const root = ctx.graph.nodes.find(
|
|
4405
|
-
(n) => n.kind === "file" && n.path === filePath
|
|
4406
|
-
);
|
|
4659
|
+
const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
|
|
4407
4660
|
if (!root) return errorContent(`blast_radius: file not in graph: ${filePath}`);
|
|
4408
4661
|
const incoming = /* @__PURE__ */ new Map();
|
|
4409
4662
|
for (const e of ctx.graph.edges) {
|
|
@@ -4450,8 +4703,8 @@ var LIKELY_ENTRY_PATTERNS = [
|
|
|
4450
4703
|
/(?:^|\/)index\.[a-z0-9_]+$/i,
|
|
4451
4704
|
/(?:^|\/)app\.[a-z0-9_]+$/i,
|
|
4452
4705
|
/(?:^|\/)entry\.[a-z0-9_]+$/i,
|
|
4453
|
-
/(?:^|\/)cli[
|
|
4454
|
-
/(?:^|\/)bin[
|
|
4706
|
+
/(?:^|\/)cli[/.]/i,
|
|
4707
|
+
/(?:^|\/)bin[/.]/i,
|
|
4455
4708
|
/(?:^|\/)server\.[a-z0-9_]+$/i,
|
|
4456
4709
|
/\.test\.[a-z0-9_]+$/i,
|
|
4457
4710
|
/\.spec\.[a-z0-9_]+$/i,
|
|
@@ -4497,9 +4750,11 @@ async function graphContinue(args, ctx) {
|
|
|
4497
4750
|
if (!query) return errorContent("graph_continue: 'query' (string) is required");
|
|
4498
4751
|
const retrieval = await retrieve(ctx.graph, query, {
|
|
4499
4752
|
recentlyEditedPaths: ctx.activity.recentFilePaths(15 * 60 * 1e3),
|
|
4500
|
-
sessionKnownPaths: getRegisteredEdits()
|
|
4753
|
+
sessionKnownPaths: getRegisteredEdits(),
|
|
4754
|
+
usageScores: ctx.learn?.effectiveScores()
|
|
4501
4755
|
});
|
|
4502
4756
|
const packed = await pack(retrieval.files, { query, graph: ctx.graph });
|
|
4757
|
+
await logAccess(ctx, { ts: nowIso(), path: "", source: "continue", query });
|
|
4503
4758
|
const header = `Confidence: ${retrieval.confidence}
|
|
4504
4759
|
Files: ${retrieval.files.map((f) => f.path).join(", ") || "(none)"}
|
|
4505
4760
|
Reason: ${retrieval.reason}
|
|
@@ -4517,7 +4772,7 @@ function resolveFileTarget(graph, filePath) {
|
|
|
4517
4772
|
if (matches.length > 1) return { ambiguous: matches.map((n) => n.path) };
|
|
4518
4773
|
return { none: true };
|
|
4519
4774
|
}
|
|
4520
|
-
function graphRead(args, ctx) {
|
|
4775
|
+
async function graphRead(args, ctx) {
|
|
4521
4776
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
4522
4777
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
4523
4778
|
const [rawFile, symbolName] = target.includes("::") ? target.split("::", 2) : [target, void 0];
|
|
@@ -4534,6 +4789,7 @@ function graphRead(args, ctx) {
|
|
|
4534
4789
|
return errorContent(`graph_read: file not found in graph: ${filePath}`);
|
|
4535
4790
|
}
|
|
4536
4791
|
const fileNode = resolved.node;
|
|
4792
|
+
await logAccess(ctx, { ts: nowIso(), path: fileNode.path, source: "read" });
|
|
4537
4793
|
if (!symbolName) {
|
|
4538
4794
|
return textContent(`# ${fileNode.path}
|
|
4539
4795
|
|
|
@@ -4555,10 +4811,21 @@ ${body}`
|
|
|
4555
4811
|
);
|
|
4556
4812
|
}
|
|
4557
4813
|
var editedFiles = /* @__PURE__ */ new Set();
|
|
4558
|
-
function graphRegisterEdit(args,
|
|
4814
|
+
async function graphRegisterEdit(args, ctx) {
|
|
4559
4815
|
const files = Array.isArray(args?.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
4560
|
-
for (const f of files)
|
|
4561
|
-
|
|
4816
|
+
for (const f of files) {
|
|
4817
|
+
const file = f;
|
|
4818
|
+
editedFiles.add(file);
|
|
4819
|
+
const resolved = resolveFileTarget(ctx.graph, file);
|
|
4820
|
+
await logAccess(ctx, {
|
|
4821
|
+
ts: nowIso(),
|
|
4822
|
+
path: "node" in resolved ? resolved.node.path : file,
|
|
4823
|
+
source: "register_edit"
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
return textContent(
|
|
4827
|
+
`Registered ${files.length} edited file(s). Total tracked this session: ${editedFiles.size}.`
|
|
4828
|
+
);
|
|
4562
4829
|
}
|
|
4563
4830
|
function getRegisteredEdits() {
|
|
4564
4831
|
return Array.from(editedFiles);
|
|
@@ -4594,9 +4861,7 @@ function recentActivity(args, ctx) {
|
|
|
4594
4861
|
let events = ctx.activity.getEvents(sinceMs);
|
|
4595
4862
|
if (limit) events = events.slice(-limit);
|
|
4596
4863
|
if (events.length === 0) {
|
|
4597
|
-
return textContent(
|
|
4598
|
-
`No human-activity events since ${new Date(sinceMs).toISOString()}.`
|
|
4599
|
-
);
|
|
4864
|
+
return textContent(`No human-activity events since ${new Date(sinceMs).toISOString()}.`);
|
|
4600
4865
|
}
|
|
4601
4866
|
const lines = [`# Recent human activity (${events.length} events)`, ""];
|
|
4602
4867
|
for (const e of events) {
|
|
@@ -4628,8 +4893,8 @@ async function contextRecall(args, ctx) {
|
|
|
4628
4893
|
}
|
|
4629
4894
|
async function logToolCall(ctx, tool) {
|
|
4630
4895
|
try {
|
|
4631
|
-
await
|
|
4632
|
-
await
|
|
4896
|
+
await mkdir9(dirname10(ctx.paths.toolLog), { recursive: true });
|
|
4897
|
+
await appendFile3(
|
|
4633
4898
|
ctx.paths.toolLog,
|
|
4634
4899
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
|
|
4635
4900
|
"utf8"
|
|
@@ -4637,6 +4902,16 @@ async function logToolCall(ctx, tool) {
|
|
|
4637
4902
|
} catch {
|
|
4638
4903
|
}
|
|
4639
4904
|
}
|
|
4905
|
+
async function logAccess(ctx, ev) {
|
|
4906
|
+
try {
|
|
4907
|
+
if (ctx.learn) await ctx.learn.record(ev);
|
|
4908
|
+
else await appendAccess(ctx.paths.accessLog, ev);
|
|
4909
|
+
} catch {
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
function nowIso() {
|
|
4913
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
4914
|
+
}
|
|
4640
4915
|
async function handleMcpRequest(body, ctx) {
|
|
4641
4916
|
if (!body || typeof body !== "object") {
|
|
4642
4917
|
return err(null, ERR.invalidRequest, "Request body must be a JSON-RPC 2.0 object.");
|
|
@@ -4687,9 +4962,87 @@ async function handleActivity(sinceMs, ctx) {
|
|
|
4687
4962
|
};
|
|
4688
4963
|
}
|
|
4689
4964
|
|
|
4965
|
+
// src/memory/git-snapshot.ts
|
|
4966
|
+
import { execFile as execFile3 } from "child_process";
|
|
4967
|
+
import { promisify as promisify3 } from "util";
|
|
4968
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
4969
|
+
var MAX_COMMITS = 5;
|
|
4970
|
+
var FIELD = "";
|
|
4971
|
+
async function getCommitsSince(projectRoot, sinceIso) {
|
|
4972
|
+
const args = [
|
|
4973
|
+
"log",
|
|
4974
|
+
`--max-count=${MAX_COMMITS}`,
|
|
4975
|
+
"--no-merges",
|
|
4976
|
+
`--pretty=format:%h${FIELD}%s${FIELD}%aI`
|
|
4977
|
+
];
|
|
4978
|
+
if (Number.isFinite(Date.parse(sinceIso))) args.push(`--since=${sinceIso}`);
|
|
4979
|
+
try {
|
|
4980
|
+
const { stdout } = await execFileAsync3("git", args, { cwd: projectRoot });
|
|
4981
|
+
const out = [];
|
|
4982
|
+
for (const line of stdout.split("\n")) {
|
|
4983
|
+
const t = line.trim();
|
|
4984
|
+
if (!t) continue;
|
|
4985
|
+
const [hash, message, date] = t.split(FIELD);
|
|
4986
|
+
if (hash && message) out.push({ hash, message, date: date ?? "" });
|
|
4987
|
+
}
|
|
4988
|
+
return out;
|
|
4989
|
+
} catch {
|
|
4990
|
+
return [];
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
|
|
4994
|
+
// src/memory/session.ts
|
|
4995
|
+
import { mkdir as mkdir10, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
|
|
4996
|
+
import { dirname as dirname11 } from "path";
|
|
4997
|
+
var SESSION_SCHEMA_VERSION = 1;
|
|
4998
|
+
async function readSession(path) {
|
|
4999
|
+
try {
|
|
5000
|
+
const raw = await readFile15(path, "utf8");
|
|
5001
|
+
const parsed = JSON.parse(raw);
|
|
5002
|
+
if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
|
|
5003
|
+
return parsed;
|
|
5004
|
+
} catch {
|
|
5005
|
+
return null;
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
async function writeSession(path, state) {
|
|
5009
|
+
await mkdir10(dirname11(path), { recursive: true });
|
|
5010
|
+
await writeFile9(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
5011
|
+
}
|
|
5012
|
+
|
|
4690
5013
|
// src/server/routes/context-update.ts
|
|
5014
|
+
var TOUCHED_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
5015
|
+
async function captureSnapshot(ctx, branchOverride) {
|
|
5016
|
+
const active = await resolveActiveBranch(ctx.paths, branchOverride);
|
|
5017
|
+
const [tasks, decisions, next] = await Promise.all([
|
|
5018
|
+
recallEntries(ctx.paths, { kind: "task", branch: active.branch, limit: 1 }),
|
|
5019
|
+
recallEntries(ctx.paths, { kind: "decision", branch: active.branch, limit: 3 }),
|
|
5020
|
+
recallEntries(ctx.paths, { kind: "next", branch: active.branch, limit: 3 })
|
|
5021
|
+
]);
|
|
5022
|
+
const touched = new Set(getRegisteredEdits());
|
|
5023
|
+
for (const p of ctx.activity.recentFilePaths(TOUCHED_WINDOW_MS)) touched.add(p);
|
|
5024
|
+
const prev = await readSession(ctx.paths.sessionState);
|
|
5025
|
+
const recentCommits = await getCommitsSince(ctx.paths.projectRoot, prev?.endedAt ?? "");
|
|
5026
|
+
const snapshot = {
|
|
5027
|
+
schema_version: SESSION_SCHEMA_VERSION,
|
|
5028
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5029
|
+
branch: active.branch,
|
|
5030
|
+
filesTouched: Array.from(touched),
|
|
5031
|
+
recentCommits,
|
|
5032
|
+
summary: {
|
|
5033
|
+
tasks: tasks.entries.map((e) => e.content),
|
|
5034
|
+
decisions: decisions.entries.map((e) => e.content),
|
|
5035
|
+
next: next.entries.map((e) => e.content)
|
|
5036
|
+
}
|
|
5037
|
+
};
|
|
5038
|
+
await writeSession(ctx.paths.sessionState, snapshot);
|
|
5039
|
+
}
|
|
4691
5040
|
async function handleContextUpdate(req, ctx) {
|
|
4692
5041
|
const r = await refreshContextMd(ctx.paths, req?.branch);
|
|
5042
|
+
try {
|
|
5043
|
+
await captureSnapshot(ctx, req?.branch);
|
|
5044
|
+
} catch {
|
|
5045
|
+
}
|
|
4693
5046
|
return {
|
|
4694
5047
|
updated: true,
|
|
4695
5048
|
branch: r.branch,
|
|
@@ -4699,8 +5052,8 @@ async function handleContextUpdate(req, ctx) {
|
|
|
4699
5052
|
}
|
|
4700
5053
|
|
|
4701
5054
|
// src/server/routes/gate.ts
|
|
4702
|
-
import { appendFile as
|
|
4703
|
-
import { dirname as
|
|
5055
|
+
import { appendFile as appendFile4, mkdir as mkdir11 } from "fs/promises";
|
|
5056
|
+
import { dirname as dirname12 } from "path";
|
|
4704
5057
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
4705
5058
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
4706
5059
|
function extractQuery(toolName, input) {
|
|
@@ -4756,7 +5109,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
4756
5109
|
}
|
|
4757
5110
|
async function logDecision(ctx, toolName, query, decision, reason) {
|
|
4758
5111
|
try {
|
|
4759
|
-
await
|
|
5112
|
+
await mkdir11(dirname12(ctx.paths.gateLog), { recursive: true });
|
|
4760
5113
|
const entry = {
|
|
4761
5114
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4762
5115
|
tool: toolName,
|
|
@@ -4764,7 +5117,7 @@ async function logDecision(ctx, toolName, query, decision, reason) {
|
|
|
4764
5117
|
query,
|
|
4765
5118
|
reason
|
|
4766
5119
|
};
|
|
4767
|
-
await
|
|
5120
|
+
await appendFile4(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
|
|
4768
5121
|
} catch {
|
|
4769
5122
|
}
|
|
4770
5123
|
}
|
|
@@ -4828,16 +5181,16 @@ async function handleGate(req, ctx) {
|
|
|
4828
5181
|
}
|
|
4829
5182
|
|
|
4830
5183
|
// src/server/routes/log.ts
|
|
4831
|
-
import { appendFile as
|
|
4832
|
-
import { dirname as
|
|
5184
|
+
import { appendFile as appendFile5, mkdir as mkdir12 } from "fs/promises";
|
|
5185
|
+
import { dirname as dirname13 } from "path";
|
|
4833
5186
|
async function handleLog(entry, ctx) {
|
|
4834
5187
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
4835
5188
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
4836
5189
|
}
|
|
4837
5190
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4838
5191
|
const record = { ...entry, written_at };
|
|
4839
|
-
await
|
|
4840
|
-
await
|
|
5192
|
+
await mkdir12(dirname13(ctx.paths.tokenLog), { recursive: true });
|
|
5193
|
+
await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
4841
5194
|
return { ok: true, written_at };
|
|
4842
5195
|
}
|
|
4843
5196
|
|
|
@@ -4847,13 +5200,15 @@ async function handlePack(req, ctx) {
|
|
|
4847
5200
|
throw new Error("pack: 'query' (string) is required");
|
|
4848
5201
|
}
|
|
4849
5202
|
const recentlyEditedPaths = ctx.activity.recentFilePaths(15 * 60 * 1e3);
|
|
4850
|
-
const
|
|
5203
|
+
const usageScores = ctx.learn?.effectiveScores();
|
|
5204
|
+
const retrieval = await retrieve(ctx.graph, req.query, { recentlyEditedPaths, usageScores });
|
|
4851
5205
|
const allFiles = ctx.graph.nodes.filter((n) => n.kind === "file");
|
|
4852
5206
|
const scored = scoreFiles({
|
|
4853
5207
|
candidates: allFiles,
|
|
4854
5208
|
query: req.query,
|
|
4855
5209
|
graph: ctx.graph,
|
|
4856
|
-
recentlyEditedPaths
|
|
5210
|
+
recentlyEditedPaths,
|
|
5211
|
+
usageScores
|
|
4857
5212
|
});
|
|
4858
5213
|
const reasons = /* @__PURE__ */ new Map();
|
|
4859
5214
|
for (const s of scored) {
|
|
@@ -4875,14 +5230,74 @@ async function handlePack(req, ctx) {
|
|
|
4875
5230
|
}
|
|
4876
5231
|
|
|
4877
5232
|
// src/server/routes/prime.ts
|
|
4878
|
-
|
|
5233
|
+
var RESUME_PRIMER_MAX_CHARS = 2720;
|
|
5234
|
+
var MAX_FILES = 15;
|
|
5235
|
+
var MAX_COMMITS2 = 5;
|
|
5236
|
+
var MAX_BULLETS2 = 3;
|
|
5237
|
+
function legacyPrimer(ctx) {
|
|
4879
5238
|
const g = ctx.graph;
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
(
|
|
4885
|
-
|
|
5239
|
+
return `Synthra context loaded for ${g.root}.
|
|
5240
|
+
${g.file_count} files indexed, ${g.symbol_count} symbols. Prefer the graph_* MCP tools over Grep/Glob for navigation.`;
|
|
5241
|
+
}
|
|
5242
|
+
function hasContent(snap) {
|
|
5243
|
+
return Boolean(
|
|
5244
|
+
snap.recentCommits.length || snap.filesTouched.length || snap.summary.tasks.length || snap.summary.next.length || snap.summary.decisions.length
|
|
5245
|
+
);
|
|
5246
|
+
}
|
|
5247
|
+
function buildResumeDigest(snap, branchNow) {
|
|
5248
|
+
const plural = (n) => n === 1 ? "" : "s";
|
|
5249
|
+
const head = `## Since you were last here \u2014 ${snap.branch} (${snap.recentCommits.length} commit${plural(snap.recentCommits.length)}, ${snap.filesTouched.length} file${plural(snap.filesTouched.length)} touched)`;
|
|
5250
|
+
const essential = [head];
|
|
5251
|
+
if (snap.branch !== branchNow) {
|
|
5252
|
+
essential.push("");
|
|
5253
|
+
essential.push(
|
|
5254
|
+
`_(snapshot was for branch '${snap.branch}'; you're now on '${branchNow}' \u2014 may be stale)_`
|
|
5255
|
+
);
|
|
5256
|
+
}
|
|
5257
|
+
if (snap.summary.tasks[0]) {
|
|
5258
|
+
essential.push("", "### In progress", `- ${snap.summary.tasks[0]}`);
|
|
5259
|
+
}
|
|
5260
|
+
if (snap.summary.next.length) {
|
|
5261
|
+
essential.push("", "### Open next steps");
|
|
5262
|
+
for (const n of snap.summary.next.slice(0, MAX_BULLETS2)) essential.push(`- ${n}`);
|
|
5263
|
+
}
|
|
5264
|
+
if (snap.summary.decisions.length) {
|
|
5265
|
+
essential.push("", "### Recent decisions");
|
|
5266
|
+
for (const d of snap.summary.decisions.slice(0, MAX_BULLETS2)) essential.push(`- ${d}`);
|
|
5267
|
+
}
|
|
5268
|
+
const extra = [];
|
|
5269
|
+
if (snap.recentCommits.length) {
|
|
5270
|
+
extra.push("", "### Recent commits");
|
|
5271
|
+
for (const c of snap.recentCommits.slice(0, MAX_COMMITS2)) {
|
|
5272
|
+
const date = c.date ? ` (${c.date.slice(0, 10)})` : "";
|
|
5273
|
+
extra.push(`- \`${c.hash}\` ${c.message}${date}`);
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
if (snap.filesTouched.length) {
|
|
5277
|
+
const shown = snap.filesTouched.slice(0, MAX_FILES);
|
|
5278
|
+
const more = snap.filesTouched.length - shown.length;
|
|
5279
|
+
extra.push("", "### Files touched", shown.join(", ") + (more > 0 ? `, +${more} more` : ""));
|
|
5280
|
+
}
|
|
5281
|
+
let out = essential.join("\n");
|
|
5282
|
+
for (const line of extra) {
|
|
5283
|
+
if ((out + "\n" + line).length > RESUME_PRIMER_MAX_CHARS) break;
|
|
5284
|
+
out += "\n" + line;
|
|
5285
|
+
}
|
|
5286
|
+
return (out.length > RESUME_PRIMER_MAX_CHARS ? out.slice(0, RESUME_PRIMER_MAX_CHARS) : out).trimEnd();
|
|
5287
|
+
}
|
|
5288
|
+
async function handlePrime(ctx, port) {
|
|
5289
|
+
const legacy = legacyPrimer(ctx);
|
|
5290
|
+
const snap = await readSession(ctx.paths.sessionState);
|
|
5291
|
+
if (!snap || !hasContent(snap)) {
|
|
5292
|
+
return { primer: legacy, port };
|
|
5293
|
+
}
|
|
5294
|
+
const branchNow = await currentBranch(ctx.paths.projectRoot);
|
|
5295
|
+
const digest = buildResumeDigest(snap, branchNow);
|
|
5296
|
+
return { primer: `${digest}
|
|
5297
|
+
|
|
5298
|
+
---
|
|
5299
|
+
|
|
5300
|
+
${legacy}`, port };
|
|
4886
5301
|
}
|
|
4887
5302
|
|
|
4888
5303
|
// src/server/http.ts
|
|
@@ -4893,9 +5308,7 @@ async function loadContext(paths) {
|
|
|
4893
5308
|
readSymbolIndex(paths.symbolIndex)
|
|
4894
5309
|
]);
|
|
4895
5310
|
if (graph.schema_version !== SCHEMA_VERSION2) {
|
|
4896
|
-
log.info(
|
|
4897
|
-
`graph schema v${graph.schema_version} \u2260 current v${SCHEMA_VERSION2} \u2014 rescanning\u2026`
|
|
4898
|
-
);
|
|
5311
|
+
log.info(`graph schema v${graph.schema_version} \u2260 current v${SCHEMA_VERSION2} \u2014 rescanning\u2026`);
|
|
4899
5312
|
await scanProject(paths.projectRoot, { silent: true });
|
|
4900
5313
|
[graph, symbolIndex] = await Promise.all([
|
|
4901
5314
|
readGraph(paths.infoGraph),
|
|
@@ -4903,7 +5316,8 @@ async function loadContext(paths) {
|
|
|
4903
5316
|
]);
|
|
4904
5317
|
}
|
|
4905
5318
|
const activity = new ActivityStore(paths.activityLog);
|
|
4906
|
-
|
|
5319
|
+
const learn = await LearnRuntime.load(paths.accessLog, paths.learnStore);
|
|
5320
|
+
return { paths, graph, symbolIndex, activity, learn };
|
|
4907
5321
|
} catch (err2) {
|
|
4908
5322
|
throw new Error(
|
|
4909
5323
|
`failed to load graph from ${paths.infoGraph}: ${err2.message}. Run \`syn scan\` first.`
|
|
@@ -4940,9 +5354,7 @@ function buildApp(ctx, port) {
|
|
|
4940
5354
|
app.get("/activity", async (c) => {
|
|
4941
5355
|
const sinceParam = c.req.query("since");
|
|
4942
5356
|
const sinceMs = sinceParam ? Number(sinceParam) : void 0;
|
|
4943
|
-
return c.json(
|
|
4944
|
-
await handleActivity(Number.isFinite(sinceMs) ? sinceMs : void 0, ctx)
|
|
4945
|
-
);
|
|
5357
|
+
return c.json(await handleActivity(Number.isFinite(sinceMs) ? sinceMs : void 0, ctx));
|
|
4946
5358
|
});
|
|
4947
5359
|
app.post("/context-update", async (c) => {
|
|
4948
5360
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -4963,11 +5375,8 @@ async function startServer(paths, options = {}) {
|
|
|
4963
5375
|
const port = options.port ?? await findFreePort();
|
|
4964
5376
|
const app = buildApp(ctx, port);
|
|
4965
5377
|
const nodeServer = serve2({ fetch: app.fetch, port, hostname: "127.0.0.1" });
|
|
4966
|
-
await
|
|
4967
|
-
const fileWatcher = createFileWatcher(
|
|
4968
|
-
paths.projectRoot,
|
|
4969
|
-
(e) => ctx.activity.add(e)
|
|
4970
|
-
);
|
|
5378
|
+
await writeFile10(paths.mcpPort, String(port), "utf8");
|
|
5379
|
+
const fileWatcher = createFileWatcher(paths.projectRoot, (e) => ctx.activity.add(e));
|
|
4971
5380
|
const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
|
|
4972
5381
|
await ctx.activity.add(e);
|
|
4973
5382
|
if (e.kind === "branch-switch") {
|
|
@@ -5004,6 +5413,7 @@ async function startServer(paths, options = {}) {
|
|
|
5004
5413
|
async stop() {
|
|
5005
5414
|
await fileWatcher.stop().catch(() => void 0);
|
|
5006
5415
|
await gitWatcher.stop().catch(() => void 0);
|
|
5416
|
+
await ctx.learn?.flush().catch(() => void 0);
|
|
5007
5417
|
await new Promise((resolve6, reject) => {
|
|
5008
5418
|
nodeServer.close((err2) => err2 ? reject(err2) : resolve6());
|
|
5009
5419
|
});
|
|
@@ -5109,7 +5519,7 @@ async function dashboardCommand(rawPath) {
|
|
|
5109
5519
|
}
|
|
5110
5520
|
|
|
5111
5521
|
// src/cli/doctor-command.ts
|
|
5112
|
-
import { readFile as
|
|
5522
|
+
import { readFile as readFile16, stat as stat4 } from "fs/promises";
|
|
5113
5523
|
import { join as join10, resolve as resolve3 } from "path";
|
|
5114
5524
|
import spawn from "cross-spawn";
|
|
5115
5525
|
var ICON = { ok: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
@@ -5140,7 +5550,11 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5140
5550
|
const checks = [];
|
|
5141
5551
|
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
5142
5552
|
checks.push(
|
|
5143
|
-
nodeMajor >= 18 ? { status: "ok", label: "Node", detail: `v${process.versions.node}` } : {
|
|
5553
|
+
nodeMajor >= 18 ? { status: "ok", label: "Node", detail: `v${process.versions.node}` } : {
|
|
5554
|
+
status: "fail",
|
|
5555
|
+
label: "Node",
|
|
5556
|
+
detail: `v${process.versions.node} \u2014 Synthra needs Node >= 18`
|
|
5557
|
+
}
|
|
5144
5558
|
);
|
|
5145
5559
|
const hasJq = await binWorks("jq", ["--version"]);
|
|
5146
5560
|
if (process.platform === "win32") {
|
|
@@ -5167,14 +5581,19 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5167
5581
|
}
|
|
5168
5582
|
);
|
|
5169
5583
|
if (!await exists2(paths.infoGraph)) {
|
|
5170
|
-
checks.push({
|
|
5584
|
+
checks.push({
|
|
5585
|
+
status: "warn",
|
|
5586
|
+
label: "Graph",
|
|
5587
|
+
detail: "no info_graph.json \u2014 run `syn .` (or `syn scan`) here."
|
|
5588
|
+
});
|
|
5171
5589
|
} else {
|
|
5172
5590
|
try {
|
|
5173
|
-
const graph = JSON.parse(await
|
|
5591
|
+
const graph = JSON.parse(await readFile16(paths.infoGraph, "utf8"));
|
|
5174
5592
|
const parts = [`${graph.symbol_count} symbols`, `${graph.file_count} files`];
|
|
5175
5593
|
let status = "ok";
|
|
5176
5594
|
const ageMs = Date.now() - Date.parse(graph.generated_at);
|
|
5177
|
-
if (Number.isFinite(ageMs))
|
|
5595
|
+
if (Number.isFinite(ageMs))
|
|
5596
|
+
parts.push(`scanned ${Math.max(0, Math.round(ageMs / 6e4))}m ago`);
|
|
5178
5597
|
if (graph.schema_version !== SCHEMA_VERSION2) {
|
|
5179
5598
|
status = "warn";
|
|
5180
5599
|
parts.push(`schema v${graph.schema_version} \u2260 v${SCHEMA_VERSION2} (auto-rescans on serve)`);
|
|
@@ -5185,22 +5604,38 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5185
5604
|
}
|
|
5186
5605
|
checks.push({ status, label: "Graph", detail: parts.join(" \xB7 ") });
|
|
5187
5606
|
} catch {
|
|
5188
|
-
checks.push({
|
|
5607
|
+
checks.push({
|
|
5608
|
+
status: "warn",
|
|
5609
|
+
label: "Graph",
|
|
5610
|
+
detail: "info_graph.json unreadable \u2014 re-run `syn scan`."
|
|
5611
|
+
});
|
|
5189
5612
|
}
|
|
5190
5613
|
}
|
|
5191
5614
|
checks.push(
|
|
5192
|
-
await exists2(join10(projectRoot, ".mcp.json")) ? {
|
|
5615
|
+
await exists2(join10(projectRoot, ".mcp.json")) ? {
|
|
5616
|
+
status: "ok",
|
|
5617
|
+
label: "MCP registration",
|
|
5618
|
+
detail: ".mcp.json present (IDE can see graph_* tools)"
|
|
5619
|
+
} : {
|
|
5193
5620
|
status: "warn",
|
|
5194
5621
|
label: "MCP registration",
|
|
5195
5622
|
detail: "no .mcp.json \u2014 the IDE extension won't see Synthra's tools; run `syn .`."
|
|
5196
5623
|
}
|
|
5197
5624
|
);
|
|
5198
5625
|
if (!await exists2(paths.claudeMd)) {
|
|
5199
|
-
checks.push({
|
|
5626
|
+
checks.push({
|
|
5627
|
+
status: "warn",
|
|
5628
|
+
label: "CLAUDE.md policy",
|
|
5629
|
+
detail: "no CLAUDE.md \u2014 run `syn .` to scaffold + inject the policy block."
|
|
5630
|
+
});
|
|
5200
5631
|
} else {
|
|
5201
|
-
const md = await
|
|
5632
|
+
const md = await readFile16(paths.claudeMd, "utf8");
|
|
5202
5633
|
if (md.includes(`synthra-policy v${POLICY_VERSION} BEGIN`)) {
|
|
5203
|
-
checks.push({
|
|
5634
|
+
checks.push({
|
|
5635
|
+
status: "ok",
|
|
5636
|
+
label: "CLAUDE.md policy",
|
|
5637
|
+
detail: `policy block v${POLICY_VERSION}`
|
|
5638
|
+
});
|
|
5204
5639
|
} else {
|
|
5205
5640
|
const m = md.match(/synthra-policy v(\d+) BEGIN/);
|
|
5206
5641
|
checks.push({
|
|
@@ -5211,11 +5646,19 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5211
5646
|
}
|
|
5212
5647
|
}
|
|
5213
5648
|
if (!await exists2(paths.claudeSettings)) {
|
|
5214
|
-
checks.push({
|
|
5649
|
+
checks.push({
|
|
5650
|
+
status: "warn",
|
|
5651
|
+
label: "Hooks",
|
|
5652
|
+
detail: "no .claude/settings.local.json \u2014 run `syn .` to install hooks."
|
|
5653
|
+
});
|
|
5215
5654
|
} else {
|
|
5216
|
-
const s = await
|
|
5655
|
+
const s = await readFile16(paths.claudeSettings, "utf8");
|
|
5217
5656
|
checks.push(
|
|
5218
|
-
s.includes("synthra-hook=true") ? { status: "ok", label: "Hooks", detail: "registered in .claude/settings.local.json" } : {
|
|
5657
|
+
s.includes("synthra-hook=true") ? { status: "ok", label: "Hooks", detail: "registered in .claude/settings.local.json" } : {
|
|
5658
|
+
status: "warn",
|
|
5659
|
+
label: "Hooks",
|
|
5660
|
+
detail: "settings.local.json present but no Synthra hooks \u2014 run `syn .`."
|
|
5661
|
+
}
|
|
5219
5662
|
);
|
|
5220
5663
|
}
|
|
5221
5664
|
return checks;
|
|
@@ -5232,12 +5675,14 @@ async function doctorCommand(rawPath) {
|
|
|
5232
5675
|
const warn = checks.filter((c) => c.status === "warn").length;
|
|
5233
5676
|
const fail = checks.filter((c) => c.status === "fail").length;
|
|
5234
5677
|
log.info("");
|
|
5235
|
-
log.info(
|
|
5678
|
+
log.info(
|
|
5679
|
+
fail === 0 && warn === 0 ? " All checks passed." : ` ${fail} failed \xB7 ${warn} warning(s).`
|
|
5680
|
+
);
|
|
5236
5681
|
log.info("");
|
|
5237
5682
|
}
|
|
5238
5683
|
|
|
5239
5684
|
// src/cli/self-update.ts
|
|
5240
|
-
import { mkdir as
|
|
5685
|
+
import { mkdir as mkdir13, readFile as readFile17, writeFile as writeFile11 } from "fs/promises";
|
|
5241
5686
|
import { homedir as homedir3 } from "os";
|
|
5242
5687
|
import { join as join11 } from "path";
|
|
5243
5688
|
import { createInterface } from "readline/promises";
|
|
@@ -5295,7 +5740,7 @@ async function checkForUpdate() {
|
|
|
5295
5740
|
}
|
|
5296
5741
|
async function readLastSeen() {
|
|
5297
5742
|
try {
|
|
5298
|
-
const raw = await
|
|
5743
|
+
const raw = await readFile17(LAST_SEEN_PATH, "utf8");
|
|
5299
5744
|
const parsed = JSON.parse(raw);
|
|
5300
5745
|
return parsed.version ?? null;
|
|
5301
5746
|
} catch {
|
|
@@ -5304,9 +5749,9 @@ async function readLastSeen() {
|
|
|
5304
5749
|
}
|
|
5305
5750
|
async function writeLastSeen(version) {
|
|
5306
5751
|
try {
|
|
5307
|
-
await
|
|
5752
|
+
await mkdir13(SYNTHRA_DIR, { recursive: true });
|
|
5308
5753
|
const data = { version, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5309
|
-
await
|
|
5754
|
+
await writeFile11(LAST_SEEN_PATH, JSON.stringify(data, null, 2), "utf8");
|
|
5310
5755
|
} catch {
|
|
5311
5756
|
}
|
|
5312
5757
|
}
|
|
@@ -5338,7 +5783,7 @@ async function readInstalledChangelog() {
|
|
|
5338
5783
|
const root = await npmGlobalRoot();
|
|
5339
5784
|
if (!root) return null;
|
|
5340
5785
|
try {
|
|
5341
|
-
return await
|
|
5786
|
+
return await readFile17(join11(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
|
|
5342
5787
|
} catch {
|
|
5343
5788
|
return null;
|
|
5344
5789
|
}
|
|
@@ -5470,7 +5915,9 @@ function runClaude(bin, args, cwd, stdio = "pipe") {
|
|
|
5470
5915
|
}
|
|
5471
5916
|
async function registerMcp(bin, mcpPort, cwd) {
|
|
5472
5917
|
const url = `http://127.0.0.1:${mcpPort}/mcp`;
|
|
5473
|
-
await runClaude(bin, ["mcp", "remove", MCP_NAME, "--scope", "project"], cwd).catch(
|
|
5918
|
+
await runClaude(bin, ["mcp", "remove", MCP_NAME, "--scope", "project"], cwd).catch(
|
|
5919
|
+
() => void 0
|
|
5920
|
+
);
|
|
5474
5921
|
const reg = await runClaude(
|
|
5475
5922
|
bin,
|
|
5476
5923
|
["mcp", "add", MCP_NAME, "--transport", "http", "--scope", "project", url],
|
|
@@ -5501,7 +5948,9 @@ async function spawnClaude(bin, opts) {
|
|
|
5501
5948
|
var VERSION2 = package_default.version;
|
|
5502
5949
|
function printReadyBanner(info) {
|
|
5503
5950
|
log.info("");
|
|
5504
|
-
log.info(
|
|
5951
|
+
log.info(
|
|
5952
|
+
` \u2705 scanned ${info.scan.parsed} files \xB7 ${info.scan.symbolCount} symbols \xB7 ${info.scan.edgeCount} edges`
|
|
5953
|
+
);
|
|
5505
5954
|
if (info.mcpRegistered) {
|
|
5506
5955
|
log.info(` \u{1F9E0} MCP ${info.mcpUrl} \u2192 registered as 'synthra'`);
|
|
5507
5956
|
} else {
|
|
@@ -5514,7 +5963,9 @@ function printReadyBanner(info) {
|
|
|
5514
5963
|
}
|
|
5515
5964
|
log.info(` \u{1FA9D} Hooks installed in .claude/settings.local.json`);
|
|
5516
5965
|
log.info("");
|
|
5517
|
-
log.info(
|
|
5966
|
+
log.info(
|
|
5967
|
+
` \u{1F916} Ready \u2014 open the Claude Code IDE extension (or run \`claude\` in another terminal).`
|
|
5968
|
+
);
|
|
5518
5969
|
log.info(` Synthra's tools and gate will be active for that session.`);
|
|
5519
5970
|
log.info("");
|
|
5520
5971
|
log.info(` Press Ctrl+C here when you're done.`);
|
|
@@ -5571,24 +6022,22 @@ async function defaultFlow(rawPath, opts) {
|
|
|
5571
6022
|
} finally {
|
|
5572
6023
|
await unregisterMcp(cfg.claudeBin, projectRoot).catch(() => void 0);
|
|
5573
6024
|
if (dashboardHandle) {
|
|
5574
|
-
await dashboardHandle.stop().catch(
|
|
5575
|
-
(err2) => log.warn(`dashboard stop error: ${err2.message}`)
|
|
5576
|
-
);
|
|
6025
|
+
await dashboardHandle.stop().catch((err2) => log.warn(`dashboard stop error: ${err2.message}`));
|
|
5577
6026
|
}
|
|
5578
|
-
await mcpHandle.stop().catch(
|
|
5579
|
-
|
|
5580
|
-
);
|
|
5581
|
-
await cleanup(paths).catch(
|
|
5582
|
-
(err2) => log.warn(`cleanup error: ${err2.message}`)
|
|
5583
|
-
);
|
|
6027
|
+
await mcpHandle.stop().catch((err2) => log.warn(`MCP server stop error: ${err2.message}`));
|
|
6028
|
+
await cleanup(paths).catch((err2) => log.warn(`cleanup error: ${err2.message}`));
|
|
5584
6029
|
}
|
|
5585
6030
|
}
|
|
5586
6031
|
function buildProgram() {
|
|
5587
6032
|
const prog = sade("syn");
|
|
5588
6033
|
prog.version(VERSION2).describe("Local context engine for AI coding assistants.");
|
|
5589
|
-
prog.command(
|
|
5590
|
-
|
|
5591
|
-
|
|
6034
|
+
prog.command(
|
|
6035
|
+
". [path]",
|
|
6036
|
+
"Scan + MCP + dashboard + hooks. Default flow \u2014 use with the Claude Code IDE extension.",
|
|
6037
|
+
{
|
|
6038
|
+
default: true
|
|
6039
|
+
}
|
|
6040
|
+
).option("--resume <id>", "Resume an existing Claude session (only with --launch-cli)").option("--launch-cli", "Also spawn `claude` CLI in this terminal (legacy M3 behavior)", false).action(async (path, opts) => {
|
|
5592
6041
|
await defaultFlow(path ?? ".", opts);
|
|
5593
6042
|
});
|
|
5594
6043
|
prog.command("scan [path]", "Scan only \u2014 walk + parse + write graph.").action(async (path) => {
|