@jefuriiij/synthra 0.1.25 → 0.2.1
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 +481 -426
- package/LICENSE +21 -21
- package/README.md +222 -222
- package/dist/cli/index.js +756 -287
- 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 +476 -68
- package/dist/server/index.js.map +1 -1
- package/package.json +9 -2
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/server/http.ts
|
|
2
2
|
import { serve } from "@hono/node-server";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
|
-
import { writeFile as
|
|
4
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
5
5
|
|
|
6
6
|
// src/activity/activity-log.ts
|
|
7
7
|
import { appendFile, mkdir } from "fs/promises";
|
|
@@ -488,7 +488,20 @@ function extractKeywords(content, _ext) {
|
|
|
488
488
|
}
|
|
489
489
|
|
|
490
490
|
// src/scanner/extract.ts
|
|
491
|
-
var RESOLVE_EXTS = [
|
|
491
|
+
var RESOLVE_EXTS = [
|
|
492
|
+
".ts",
|
|
493
|
+
".tsx",
|
|
494
|
+
".js",
|
|
495
|
+
".jsx",
|
|
496
|
+
".mjs",
|
|
497
|
+
".cjs",
|
|
498
|
+
".py",
|
|
499
|
+
".svelte",
|
|
500
|
+
".vue",
|
|
501
|
+
".dart",
|
|
502
|
+
".html",
|
|
503
|
+
".hubl"
|
|
504
|
+
];
|
|
492
505
|
var INDEX_FILES = ["index.ts", "index.tsx", "index.js", "index.jsx", "__init__.py"];
|
|
493
506
|
function fileId(relPath) {
|
|
494
507
|
return `file:${relPath}`;
|
|
@@ -1597,6 +1610,8 @@ function resolvePaths(projectRoot) {
|
|
|
1597
1610
|
tokenLog: join5(graphDir, "token_log.jsonl"),
|
|
1598
1611
|
gateLog: join5(graphDir, "gate_log.jsonl"),
|
|
1599
1612
|
toolLog: join5(graphDir, "tool_log.jsonl"),
|
|
1613
|
+
accessLog: join5(graphDir, "access_log.jsonl"),
|
|
1614
|
+
learnStore: join5(graphDir, "learn_store.json"),
|
|
1600
1615
|
mcpPort: join5(graphDir, "mcp_port"),
|
|
1601
1616
|
mcpServerLog: join5(graphDir, "mcp_server.log"),
|
|
1602
1617
|
mcpServerErrLog: join5(graphDir, "mcp_server.err.log"),
|
|
@@ -1618,7 +1633,7 @@ import { basename as basename2 } from "path";
|
|
|
1618
1633
|
// src/hooks/claude-md.ts
|
|
1619
1634
|
import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
1620
1635
|
import { basename, dirname as dirname4 } from "path";
|
|
1621
|
-
var POLICY_VERSION =
|
|
1636
|
+
var POLICY_VERSION = 6;
|
|
1622
1637
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
1623
1638
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
1624
1639
|
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;
|
|
@@ -1709,6 +1724,17 @@ function policyBlock() {
|
|
|
1709
1724
|
"- Don't call `graph_continue` more than once per turn.",
|
|
1710
1725
|
"- Don't read whole files when a symbol-level read would suffice.",
|
|
1711
1726
|
"",
|
|
1727
|
+
"### Resuming a session",
|
|
1728
|
+
"",
|
|
1729
|
+
'At session start the primer may begin with a **"Since you were last here"**',
|
|
1730
|
+
"digest \u2014 recent commits, files touched, open next-steps, and recent",
|
|
1731
|
+
"decisions carried over from the previous session. **Trust it.** It is the",
|
|
1732
|
+
"cheapest possible orientation: do NOT re-run `graph_continue` or Grep just",
|
|
1733
|
+
'to rediscover "what were we doing / what changed" \u2014 that work is already',
|
|
1734
|
+
'done. For the concrete next steps, `context_recall({kind:"next"})` returns',
|
|
1735
|
+
"them verbatim. Only reach for fresh retrieval when the task moves beyond",
|
|
1736
|
+
"what the digest covers.",
|
|
1737
|
+
"",
|
|
1712
1738
|
"### Session-end resume note",
|
|
1713
1739
|
"",
|
|
1714
1740
|
`When the user signals they're done (e.g. "bye", "wrap up", "done"),`,
|
|
@@ -1887,7 +1913,9 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
1887
1913
|
if (boot.gitignoreUpdated) log.info(" updated .gitignore");
|
|
1888
1914
|
if (boot.claudeMdCreated) {
|
|
1889
1915
|
log.info(" created CLAUDE.md \u2014 onboarding skeleton for the agent");
|
|
1890
|
-
log.info(
|
|
1916
|
+
log.info(
|
|
1917
|
+
" \u21B3 fill in Build / Conventions / Decisions (or run /init in Claude to auto-draft)"
|
|
1918
|
+
);
|
|
1891
1919
|
} else if (boot.claudeMdUpdated) {
|
|
1892
1920
|
log.info(" updated CLAUDE.md");
|
|
1893
1921
|
}
|
|
@@ -1932,11 +1960,192 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
1932
1960
|
};
|
|
1933
1961
|
}
|
|
1934
1962
|
|
|
1963
|
+
// src/learn/store.ts
|
|
1964
|
+
import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
1965
|
+
import { dirname as dirname5 } from "path";
|
|
1966
|
+
|
|
1967
|
+
// src/learn/usage.ts
|
|
1968
|
+
var LEARN_SCHEMA_VERSION = 1;
|
|
1969
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1970
|
+
function halfLifeMs() {
|
|
1971
|
+
const env = Number(process.env.SYN_LEARN_HALFLIFE_DAYS);
|
|
1972
|
+
const days = Number.isFinite(env) && env > 0 ? env : 7;
|
|
1973
|
+
return days * DAY_MS;
|
|
1974
|
+
}
|
|
1975
|
+
function weightFor(source) {
|
|
1976
|
+
switch (source) {
|
|
1977
|
+
case "register_edit":
|
|
1978
|
+
return 2;
|
|
1979
|
+
case "read":
|
|
1980
|
+
return 1;
|
|
1981
|
+
default:
|
|
1982
|
+
return 0;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
function emptyStore() {
|
|
1986
|
+
return {
|
|
1987
|
+
schema_version: LEARN_SCHEMA_VERSION,
|
|
1988
|
+
asOf: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1989
|
+
files: {}
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
function decayFactor(fromTs, toMs, hl) {
|
|
1993
|
+
const fromMs = Date.parse(fromTs);
|
|
1994
|
+
if (!Number.isFinite(fromMs)) return 1;
|
|
1995
|
+
const dt = toMs - fromMs;
|
|
1996
|
+
if (dt <= 0) return 1;
|
|
1997
|
+
return Math.exp(-(Math.LN2 / hl) * dt);
|
|
1998
|
+
}
|
|
1999
|
+
function foldEvent(store, ev) {
|
|
2000
|
+
const w = weightFor(ev.source);
|
|
2001
|
+
if (w <= 0 || !ev.path) return store;
|
|
2002
|
+
const tMs = Date.parse(ev.ts);
|
|
2003
|
+
if (!Number.isFinite(tMs)) return store;
|
|
2004
|
+
const hl = halfLifeMs();
|
|
2005
|
+
const prev = store.files[ev.path];
|
|
2006
|
+
if (prev) {
|
|
2007
|
+
const decayed = prev.decayed * decayFactor(prev.lastTs, tMs, hl) + w;
|
|
2008
|
+
store.files[ev.path] = { count: prev.count + 1, decayed, lastTs: ev.ts };
|
|
2009
|
+
} else {
|
|
2010
|
+
store.files[ev.path] = { count: 1, decayed: w, lastTs: ev.ts };
|
|
2011
|
+
}
|
|
2012
|
+
return store;
|
|
2013
|
+
}
|
|
2014
|
+
function effectiveScores(store, nowMs) {
|
|
2015
|
+
const hl = halfLifeMs();
|
|
2016
|
+
const out = /* @__PURE__ */ new Map();
|
|
2017
|
+
for (const [path, stat3] of Object.entries(store.files)) {
|
|
2018
|
+
const eff = stat3.decayed * decayFactor(stat3.lastTs, nowMs, hl);
|
|
2019
|
+
if (eff > 0.01) out.set(path, eff);
|
|
2020
|
+
}
|
|
2021
|
+
return out;
|
|
2022
|
+
}
|
|
2023
|
+
function recomputeFromLog(events) {
|
|
2024
|
+
const store = emptyStore();
|
|
2025
|
+
for (const ev of events) foldEvent(store, ev);
|
|
2026
|
+
return store;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// src/learn/store.ts
|
|
2030
|
+
async function readLearnStore(path) {
|
|
2031
|
+
try {
|
|
2032
|
+
const raw = await readFile8(path, "utf8");
|
|
2033
|
+
const parsed = JSON.parse(raw);
|
|
2034
|
+
if (parsed.schema_version !== LEARN_SCHEMA_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
|
|
2035
|
+
return emptyStore();
|
|
2036
|
+
}
|
|
2037
|
+
return {
|
|
2038
|
+
schema_version: LEARN_SCHEMA_VERSION,
|
|
2039
|
+
asOf: typeof parsed.asOf === "string" ? parsed.asOf : emptyStore().asOf,
|
|
2040
|
+
files: parsed.files
|
|
2041
|
+
};
|
|
2042
|
+
} catch {
|
|
2043
|
+
return emptyStore();
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
async function writeLearnStore(path, store) {
|
|
2047
|
+
try {
|
|
2048
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
2049
|
+
await writeFile4(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
2050
|
+
} catch {
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
async function readAccessLog(path) {
|
|
2054
|
+
try {
|
|
2055
|
+
const raw = await readFile8(path, "utf8");
|
|
2056
|
+
const out = [];
|
|
2057
|
+
for (const line of raw.split("\n")) {
|
|
2058
|
+
const t = line.trim();
|
|
2059
|
+
if (!t) continue;
|
|
2060
|
+
try {
|
|
2061
|
+
const ev = JSON.parse(t);
|
|
2062
|
+
if (ev && typeof ev.ts === "string" && typeof ev.path === "string" && typeof ev.source === "string") {
|
|
2063
|
+
out.push(ev);
|
|
2064
|
+
}
|
|
2065
|
+
} catch {
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
return out;
|
|
2069
|
+
} catch {
|
|
2070
|
+
return [];
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
async function appendAccess(path, ev) {
|
|
2074
|
+
try {
|
|
2075
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
2076
|
+
await appendFile2(path, JSON.stringify(ev) + "\n", "utf8");
|
|
2077
|
+
} catch {
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/learn/runtime.ts
|
|
2082
|
+
var PERSIST_DEBOUNCE_MS = 2e3;
|
|
2083
|
+
var LearnRuntime = class _LearnRuntime {
|
|
2084
|
+
constructor(accessLogPath, storePath, store) {
|
|
2085
|
+
this.accessLogPath = accessLogPath;
|
|
2086
|
+
this.storePath = storePath;
|
|
2087
|
+
this.store = store;
|
|
2088
|
+
}
|
|
2089
|
+
accessLogPath;
|
|
2090
|
+
storePath;
|
|
2091
|
+
store;
|
|
2092
|
+
dirty = false;
|
|
2093
|
+
timer = null;
|
|
2094
|
+
/** Load the aggregate from disk; if it's empty but a raw log exists, replay it
|
|
2095
|
+
* (the log is the source of truth). Always succeeds — falls back to empty. */
|
|
2096
|
+
static async load(accessLogPath, storePath) {
|
|
2097
|
+
let store = await readLearnStore(storePath);
|
|
2098
|
+
if (Object.keys(store.files).length === 0) {
|
|
2099
|
+
const events = await readAccessLog(accessLogPath);
|
|
2100
|
+
if (events.length > 0) store = recomputeFromLog(events);
|
|
2101
|
+
}
|
|
2102
|
+
return new _LearnRuntime(accessLogPath, storePath, store);
|
|
2103
|
+
}
|
|
2104
|
+
/** Record an access: append to the durable log + fold into the in-memory
|
|
2105
|
+
* aggregate. Best-effort — never throws into a tool call. */
|
|
2106
|
+
async record(ev) {
|
|
2107
|
+
await appendAccess(this.accessLogPath, ev);
|
|
2108
|
+
foldEvent(this.store, ev);
|
|
2109
|
+
this.schedulePersist();
|
|
2110
|
+
}
|
|
2111
|
+
/** Decayed path→weight map for the ranker, as of now. */
|
|
2112
|
+
effectiveScores(nowMs = Date.now()) {
|
|
2113
|
+
return effectiveScores(this.store, nowMs);
|
|
2114
|
+
}
|
|
2115
|
+
schedulePersist() {
|
|
2116
|
+
this.dirty = true;
|
|
2117
|
+
if (this.timer) return;
|
|
2118
|
+
this.timer = setTimeout(() => {
|
|
2119
|
+
this.timer = null;
|
|
2120
|
+
void this.flush();
|
|
2121
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
2122
|
+
this.timer.unref?.();
|
|
2123
|
+
}
|
|
2124
|
+
/** Persist the aggregate if it changed since the last write. Called on the
|
|
2125
|
+
* debounce and on server shutdown. */
|
|
2126
|
+
async flush() {
|
|
2127
|
+
if (this.timer) {
|
|
2128
|
+
clearTimeout(this.timer);
|
|
2129
|
+
this.timer = null;
|
|
2130
|
+
}
|
|
2131
|
+
if (!this.dirty) return;
|
|
2132
|
+
this.dirty = false;
|
|
2133
|
+
this.store.asOf = (/* @__PURE__ */ new Date()).toISOString();
|
|
2134
|
+
await writeLearnStore(this.storePath, this.store);
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
|
|
1935
2138
|
// src/server/mcp.ts
|
|
1936
|
-
import { appendFile as
|
|
1937
|
-
import { dirname as
|
|
2139
|
+
import { appendFile as appendFile3, mkdir as mkdir7 } from "fs/promises";
|
|
2140
|
+
import { dirname as dirname8 } from "path";
|
|
1938
2141
|
|
|
1939
2142
|
// src/graph/rank.ts
|
|
2143
|
+
var KW_BASE_WEIGHT = 2;
|
|
2144
|
+
var USAGE_BOOST_CAP_DEFAULT = 4;
|
|
2145
|
+
function usageBoostCap() {
|
|
2146
|
+
const env = Number(process.env.SYN_LEARN_BOOST_CAP);
|
|
2147
|
+
return Number.isFinite(env) && env >= 0 ? env : USAGE_BOOST_CAP_DEFAULT;
|
|
2148
|
+
}
|
|
1940
2149
|
var STOPWORDS2 = /* @__PURE__ */ new Set([
|
|
1941
2150
|
"a",
|
|
1942
2151
|
"an",
|
|
@@ -2021,14 +2230,41 @@ function scoreFiles(inputs) {
|
|
|
2021
2230
|
const importsFrom = indexImportEdges(inputs.graph);
|
|
2022
2231
|
const seeds = new Set(inputs.sessionKnownPaths ?? []);
|
|
2023
2232
|
for (const p of inputs.recentlyEditedPaths ?? []) seeds.add(p);
|
|
2233
|
+
const corpusSize = inputs.candidates.length;
|
|
2234
|
+
const queryDf = /* @__PURE__ */ new Map();
|
|
2235
|
+
for (const f of inputs.candidates) {
|
|
2236
|
+
for (const kw of f.keywords) {
|
|
2237
|
+
if (qTokens.has(kw)) queryDf.set(kw, (queryDf.get(kw) ?? 0) + 1);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const idf = (token) => {
|
|
2241
|
+
const n = queryDf.get(token) ?? 0;
|
|
2242
|
+
if (n <= 0) return 0;
|
|
2243
|
+
return Math.log(1 + (corpusSize - n + 0.5) / (n + 0.5));
|
|
2244
|
+
};
|
|
2245
|
+
let idfSum = 0;
|
|
2246
|
+
let idfCount = 0;
|
|
2247
|
+
for (const t of qTokens) {
|
|
2248
|
+
const v = idf(t);
|
|
2249
|
+
if (v > 0) {
|
|
2250
|
+
idfSum += v;
|
|
2251
|
+
idfCount += 1;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
const refIdf = idfCount > 0 ? idfSum / idfCount : 1;
|
|
2024
2255
|
const scored = [];
|
|
2025
2256
|
for (const file of inputs.candidates) {
|
|
2026
2257
|
const reasons = [];
|
|
2027
2258
|
let score2 = 0;
|
|
2028
2259
|
let kwHits = 0;
|
|
2029
|
-
|
|
2260
|
+
let kwScore = 0;
|
|
2261
|
+
for (const kw of file.keywords) {
|
|
2262
|
+
if (!qTokens.has(kw)) continue;
|
|
2263
|
+
kwHits += 1;
|
|
2264
|
+
kwScore += KW_BASE_WEIGHT * (idf(kw) / refIdf);
|
|
2265
|
+
}
|
|
2030
2266
|
if (kwHits) {
|
|
2031
|
-
score2 +=
|
|
2267
|
+
score2 += kwScore;
|
|
2032
2268
|
reasons.push(`kw=${kwHits}`);
|
|
2033
2269
|
}
|
|
2034
2270
|
const symbols = symbolsByFile.get(file.path) ?? [];
|
|
@@ -2083,6 +2319,21 @@ function scoreFiles(inputs) {
|
|
|
2083
2319
|
}
|
|
2084
2320
|
}
|
|
2085
2321
|
}
|
|
2322
|
+
const usage = inputs.usageScores;
|
|
2323
|
+
if (usage && usage.size > 0) {
|
|
2324
|
+
let maxU = 0;
|
|
2325
|
+
for (const v of usage.values()) if (v > maxU) maxU = v;
|
|
2326
|
+
if (maxU > 0) {
|
|
2327
|
+
const cap = usageBoostCap();
|
|
2328
|
+
for (const s of scored) {
|
|
2329
|
+
if (s.score <= 0) continue;
|
|
2330
|
+
const u = usage.get(s.file.path) ?? 0;
|
|
2331
|
+
if (u <= 0) continue;
|
|
2332
|
+
s.score += cap * (u / maxU);
|
|
2333
|
+
s.reasons.push(`used\xD7${Math.round(u)}`);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2086
2337
|
scored.sort((a, b) => b.score - a.score);
|
|
2087
2338
|
return scored;
|
|
2088
2339
|
}
|
|
@@ -2091,9 +2342,7 @@ function scoreFiles(inputs) {
|
|
|
2091
2342
|
async function retrieve(graph, query, options = {}) {
|
|
2092
2343
|
const topK = options.topK ?? 12;
|
|
2093
2344
|
const qTokens = tokenizeQuery(query);
|
|
2094
|
-
const allFiles = graph.nodes.filter(
|
|
2095
|
-
(n) => n.kind === "file"
|
|
2096
|
-
);
|
|
2345
|
+
const allFiles = graph.nodes.filter((n) => n.kind === "file");
|
|
2097
2346
|
if (allFiles.length === 0 || qTokens.length === 0) {
|
|
2098
2347
|
return {
|
|
2099
2348
|
files: [],
|
|
@@ -2107,7 +2356,8 @@ async function retrieve(graph, query, options = {}) {
|
|
|
2107
2356
|
query,
|
|
2108
2357
|
graph,
|
|
2109
2358
|
recentlyEditedPaths: options.recentlyEditedPaths,
|
|
2110
|
-
sessionKnownPaths: options.sessionKnownPaths
|
|
2359
|
+
sessionKnownPaths: options.sessionKnownPaths,
|
|
2360
|
+
usageScores: options.usageScores
|
|
2111
2361
|
};
|
|
2112
2362
|
const scored = scoreFiles(rankInputs);
|
|
2113
2363
|
const positive = scored.filter((s) => s.score > 0);
|
|
@@ -2140,14 +2390,14 @@ async function retrieve(graph, query, options = {}) {
|
|
|
2140
2390
|
|
|
2141
2391
|
// src/memory/branches.ts
|
|
2142
2392
|
import { execFile as execFile2 } from "child_process";
|
|
2143
|
-
import { readFile as
|
|
2393
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2144
2394
|
import { join as join6 } from "path";
|
|
2145
2395
|
import { promisify as promisify2 } from "util";
|
|
2146
2396
|
var execFileAsync2 = promisify2(execFile2);
|
|
2147
2397
|
async function currentBranch(projectRoot) {
|
|
2148
2398
|
try {
|
|
2149
2399
|
const headPath = join6(projectRoot, ".git", "HEAD");
|
|
2150
|
-
const head = await
|
|
2400
|
+
const head = await readFile9(headPath, "utf8");
|
|
2151
2401
|
const trimmed = head.trim();
|
|
2152
2402
|
const match = trimmed.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
2153
2403
|
if (match?.[1]) return match[1];
|
|
@@ -2197,8 +2447,8 @@ function resolveBranchPaths(contextDir, branch, isDefault) {
|
|
|
2197
2447
|
}
|
|
2198
2448
|
|
|
2199
2449
|
// src/memory/context-md.ts
|
|
2200
|
-
import { mkdir as
|
|
2201
|
-
import { dirname as
|
|
2450
|
+
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
2451
|
+
import { dirname as dirname6 } from "path";
|
|
2202
2452
|
var MAX_BULLETS = 3;
|
|
2203
2453
|
function deriveContextMd(entries, branch) {
|
|
2204
2454
|
const tasks = entries.filter((e) => e.type === "task").reverse();
|
|
@@ -2241,17 +2491,17 @@ function formatContextMd(ctx) {
|
|
|
2241
2491
|
return lines.join("\n");
|
|
2242
2492
|
}
|
|
2243
2493
|
async function writeContextMd(path, ctx) {
|
|
2244
|
-
await
|
|
2245
|
-
await
|
|
2494
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2495
|
+
await writeFile5(path, formatContextMd(ctx), "utf8");
|
|
2246
2496
|
}
|
|
2247
2497
|
|
|
2248
2498
|
// src/memory/context-store.ts
|
|
2249
|
-
import { mkdir as
|
|
2250
|
-
import { dirname as
|
|
2499
|
+
import { mkdir as mkdir6, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
2500
|
+
import { dirname as dirname7 } from "path";
|
|
2251
2501
|
var SCHEMA_VERSION2 = 1;
|
|
2252
2502
|
async function readEntries(path) {
|
|
2253
2503
|
try {
|
|
2254
|
-
const raw = await
|
|
2504
|
+
const raw = await readFile11(path, "utf8");
|
|
2255
2505
|
const parsed = JSON.parse(raw);
|
|
2256
2506
|
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
2257
2507
|
} catch {
|
|
@@ -2259,9 +2509,9 @@ async function readEntries(path) {
|
|
|
2259
2509
|
}
|
|
2260
2510
|
}
|
|
2261
2511
|
async function writeEntries(path, entries) {
|
|
2262
|
-
await
|
|
2512
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
2263
2513
|
const store = { schema_version: SCHEMA_VERSION2, entries };
|
|
2264
|
-
await
|
|
2514
|
+
await writeFile6(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
2265
2515
|
}
|
|
2266
2516
|
async function appendEntry(path, entry) {
|
|
2267
2517
|
const entries = await readEntries(path);
|
|
@@ -2535,7 +2785,10 @@ var TOOLS = [
|
|
|
2535
2785
|
inputSchema: {
|
|
2536
2786
|
type: "object",
|
|
2537
2787
|
properties: {
|
|
2538
|
-
query: {
|
|
2788
|
+
query: {
|
|
2789
|
+
type: "string",
|
|
2790
|
+
description: "Natural-language description of what you're looking for."
|
|
2791
|
+
}
|
|
2539
2792
|
},
|
|
2540
2793
|
required: ["query"]
|
|
2541
2794
|
}
|
|
@@ -2691,9 +2944,7 @@ function blastRadius(args, ctx) {
|
|
|
2691
2944
|
const maxDepth = typeof args?.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 3;
|
|
2692
2945
|
if (!targetRaw) return errorContent("blast_radius: 'target' (string) is required");
|
|
2693
2946
|
const filePath = targetRaw.split("::", 1)[0]?.trim() ?? targetRaw;
|
|
2694
|
-
const root = ctx.graph.nodes.find(
|
|
2695
|
-
(n) => n.kind === "file" && n.path === filePath
|
|
2696
|
-
);
|
|
2947
|
+
const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
|
|
2697
2948
|
if (!root) return errorContent(`blast_radius: file not in graph: ${filePath}`);
|
|
2698
2949
|
const incoming = /* @__PURE__ */ new Map();
|
|
2699
2950
|
for (const e of ctx.graph.edges) {
|
|
@@ -2740,8 +2991,8 @@ var LIKELY_ENTRY_PATTERNS = [
|
|
|
2740
2991
|
/(?:^|\/)index\.[a-z0-9_]+$/i,
|
|
2741
2992
|
/(?:^|\/)app\.[a-z0-9_]+$/i,
|
|
2742
2993
|
/(?:^|\/)entry\.[a-z0-9_]+$/i,
|
|
2743
|
-
/(?:^|\/)cli[
|
|
2744
|
-
/(?:^|\/)bin[
|
|
2994
|
+
/(?:^|\/)cli[/.]/i,
|
|
2995
|
+
/(?:^|\/)bin[/.]/i,
|
|
2745
2996
|
/(?:^|\/)server\.[a-z0-9_]+$/i,
|
|
2746
2997
|
/\.test\.[a-z0-9_]+$/i,
|
|
2747
2998
|
/\.spec\.[a-z0-9_]+$/i,
|
|
@@ -2787,9 +3038,11 @@ async function graphContinue(args, ctx) {
|
|
|
2787
3038
|
if (!query) return errorContent("graph_continue: 'query' (string) is required");
|
|
2788
3039
|
const retrieval = await retrieve(ctx.graph, query, {
|
|
2789
3040
|
recentlyEditedPaths: ctx.activity.recentFilePaths(15 * 60 * 1e3),
|
|
2790
|
-
sessionKnownPaths: getRegisteredEdits()
|
|
3041
|
+
sessionKnownPaths: getRegisteredEdits(),
|
|
3042
|
+
usageScores: ctx.learn?.effectiveScores()
|
|
2791
3043
|
});
|
|
2792
3044
|
const packed = await pack(retrieval.files, { query, graph: ctx.graph });
|
|
3045
|
+
await logAccess(ctx, { ts: nowIso(), path: "", source: "continue", query });
|
|
2793
3046
|
const header = `Confidence: ${retrieval.confidence}
|
|
2794
3047
|
Files: ${retrieval.files.map((f) => f.path).join(", ") || "(none)"}
|
|
2795
3048
|
Reason: ${retrieval.reason}
|
|
@@ -2807,7 +3060,7 @@ function resolveFileTarget(graph, filePath) {
|
|
|
2807
3060
|
if (matches.length > 1) return { ambiguous: matches.map((n) => n.path) };
|
|
2808
3061
|
return { none: true };
|
|
2809
3062
|
}
|
|
2810
|
-
function graphRead(args, ctx) {
|
|
3063
|
+
async function graphRead(args, ctx) {
|
|
2811
3064
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
2812
3065
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
2813
3066
|
const [rawFile, symbolName] = target.includes("::") ? target.split("::", 2) : [target, void 0];
|
|
@@ -2824,6 +3077,7 @@ function graphRead(args, ctx) {
|
|
|
2824
3077
|
return errorContent(`graph_read: file not found in graph: ${filePath}`);
|
|
2825
3078
|
}
|
|
2826
3079
|
const fileNode = resolved.node;
|
|
3080
|
+
await logAccess(ctx, { ts: nowIso(), path: fileNode.path, source: "read" });
|
|
2827
3081
|
if (!symbolName) {
|
|
2828
3082
|
return textContent(`# ${fileNode.path}
|
|
2829
3083
|
|
|
@@ -2845,10 +3099,21 @@ ${body}`
|
|
|
2845
3099
|
);
|
|
2846
3100
|
}
|
|
2847
3101
|
var editedFiles = /* @__PURE__ */ new Set();
|
|
2848
|
-
function graphRegisterEdit(args,
|
|
3102
|
+
async function graphRegisterEdit(args, ctx) {
|
|
2849
3103
|
const files = Array.isArray(args?.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
2850
|
-
for (const f of files)
|
|
2851
|
-
|
|
3104
|
+
for (const f of files) {
|
|
3105
|
+
const file = f;
|
|
3106
|
+
editedFiles.add(file);
|
|
3107
|
+
const resolved = resolveFileTarget(ctx.graph, file);
|
|
3108
|
+
await logAccess(ctx, {
|
|
3109
|
+
ts: nowIso(),
|
|
3110
|
+
path: "node" in resolved ? resolved.node.path : file,
|
|
3111
|
+
source: "register_edit"
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
return textContent(
|
|
3115
|
+
`Registered ${files.length} edited file(s). Total tracked this session: ${editedFiles.size}.`
|
|
3116
|
+
);
|
|
2852
3117
|
}
|
|
2853
3118
|
function getRegisteredEdits() {
|
|
2854
3119
|
return Array.from(editedFiles);
|
|
@@ -2884,9 +3149,7 @@ function recentActivity(args, ctx) {
|
|
|
2884
3149
|
let events = ctx.activity.getEvents(sinceMs);
|
|
2885
3150
|
if (limit) events = events.slice(-limit);
|
|
2886
3151
|
if (events.length === 0) {
|
|
2887
|
-
return textContent(
|
|
2888
|
-
`No human-activity events since ${new Date(sinceMs).toISOString()}.`
|
|
2889
|
-
);
|
|
3152
|
+
return textContent(`No human-activity events since ${new Date(sinceMs).toISOString()}.`);
|
|
2890
3153
|
}
|
|
2891
3154
|
const lines = [`# Recent human activity (${events.length} events)`, ""];
|
|
2892
3155
|
for (const e of events) {
|
|
@@ -2918,8 +3181,8 @@ async function contextRecall(args, ctx) {
|
|
|
2918
3181
|
}
|
|
2919
3182
|
async function logToolCall(ctx, tool) {
|
|
2920
3183
|
try {
|
|
2921
|
-
await
|
|
2922
|
-
await
|
|
3184
|
+
await mkdir7(dirname8(ctx.paths.toolLog), { recursive: true });
|
|
3185
|
+
await appendFile3(
|
|
2923
3186
|
ctx.paths.toolLog,
|
|
2924
3187
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
|
|
2925
3188
|
"utf8"
|
|
@@ -2927,6 +3190,16 @@ async function logToolCall(ctx, tool) {
|
|
|
2927
3190
|
} catch {
|
|
2928
3191
|
}
|
|
2929
3192
|
}
|
|
3193
|
+
async function logAccess(ctx, ev) {
|
|
3194
|
+
try {
|
|
3195
|
+
if (ctx.learn) await ctx.learn.record(ev);
|
|
3196
|
+
else await appendAccess(ctx.paths.accessLog, ev);
|
|
3197
|
+
} catch {
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
function nowIso() {
|
|
3201
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3202
|
+
}
|
|
2930
3203
|
async function handleMcpRequest(body, ctx) {
|
|
2931
3204
|
if (!body || typeof body !== "object") {
|
|
2932
3205
|
return err(null, ERR.invalidRequest, "Request body must be a JSON-RPC 2.0 object.");
|
|
@@ -2996,9 +3269,87 @@ async function handleActivity(sinceMs, ctx) {
|
|
|
2996
3269
|
};
|
|
2997
3270
|
}
|
|
2998
3271
|
|
|
3272
|
+
// src/memory/git-snapshot.ts
|
|
3273
|
+
import { execFile as execFile3 } from "child_process";
|
|
3274
|
+
import { promisify as promisify3 } from "util";
|
|
3275
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
3276
|
+
var MAX_COMMITS = 5;
|
|
3277
|
+
var FIELD = "";
|
|
3278
|
+
async function getCommitsSince(projectRoot, sinceIso) {
|
|
3279
|
+
const args = [
|
|
3280
|
+
"log",
|
|
3281
|
+
`--max-count=${MAX_COMMITS}`,
|
|
3282
|
+
"--no-merges",
|
|
3283
|
+
`--pretty=format:%h${FIELD}%s${FIELD}%aI`
|
|
3284
|
+
];
|
|
3285
|
+
if (Number.isFinite(Date.parse(sinceIso))) args.push(`--since=${sinceIso}`);
|
|
3286
|
+
try {
|
|
3287
|
+
const { stdout } = await execFileAsync3("git", args, { cwd: projectRoot });
|
|
3288
|
+
const out = [];
|
|
3289
|
+
for (const line of stdout.split("\n")) {
|
|
3290
|
+
const t = line.trim();
|
|
3291
|
+
if (!t) continue;
|
|
3292
|
+
const [hash, message, date] = t.split(FIELD);
|
|
3293
|
+
if (hash && message) out.push({ hash, message, date: date ?? "" });
|
|
3294
|
+
}
|
|
3295
|
+
return out;
|
|
3296
|
+
} catch {
|
|
3297
|
+
return [];
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
// src/memory/session.ts
|
|
3302
|
+
import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
|
|
3303
|
+
import { dirname as dirname9 } from "path";
|
|
3304
|
+
var SESSION_SCHEMA_VERSION = 1;
|
|
3305
|
+
async function readSession(path) {
|
|
3306
|
+
try {
|
|
3307
|
+
const raw = await readFile12(path, "utf8");
|
|
3308
|
+
const parsed = JSON.parse(raw);
|
|
3309
|
+
if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
|
|
3310
|
+
return parsed;
|
|
3311
|
+
} catch {
|
|
3312
|
+
return null;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
async function writeSession(path, state) {
|
|
3316
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
3317
|
+
await writeFile7(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
3318
|
+
}
|
|
3319
|
+
|
|
2999
3320
|
// src/server/routes/context-update.ts
|
|
3321
|
+
var TOUCHED_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
3322
|
+
async function captureSnapshot(ctx, branchOverride) {
|
|
3323
|
+
const active = await resolveActiveBranch(ctx.paths, branchOverride);
|
|
3324
|
+
const [tasks, decisions, next] = await Promise.all([
|
|
3325
|
+
recallEntries(ctx.paths, { kind: "task", branch: active.branch, limit: 1 }),
|
|
3326
|
+
recallEntries(ctx.paths, { kind: "decision", branch: active.branch, limit: 3 }),
|
|
3327
|
+
recallEntries(ctx.paths, { kind: "next", branch: active.branch, limit: 3 })
|
|
3328
|
+
]);
|
|
3329
|
+
const touched = new Set(getRegisteredEdits());
|
|
3330
|
+
for (const p of ctx.activity.recentFilePaths(TOUCHED_WINDOW_MS)) touched.add(p);
|
|
3331
|
+
const prev = await readSession(ctx.paths.sessionState);
|
|
3332
|
+
const recentCommits = await getCommitsSince(ctx.paths.projectRoot, prev?.endedAt ?? "");
|
|
3333
|
+
const snapshot = {
|
|
3334
|
+
schema_version: SESSION_SCHEMA_VERSION,
|
|
3335
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3336
|
+
branch: active.branch,
|
|
3337
|
+
filesTouched: Array.from(touched),
|
|
3338
|
+
recentCommits,
|
|
3339
|
+
summary: {
|
|
3340
|
+
tasks: tasks.entries.map((e) => e.content),
|
|
3341
|
+
decisions: decisions.entries.map((e) => e.content),
|
|
3342
|
+
next: next.entries.map((e) => e.content)
|
|
3343
|
+
}
|
|
3344
|
+
};
|
|
3345
|
+
await writeSession(ctx.paths.sessionState, snapshot);
|
|
3346
|
+
}
|
|
3000
3347
|
async function handleContextUpdate(req, ctx) {
|
|
3001
3348
|
const r = await refreshContextMd(ctx.paths, req?.branch);
|
|
3349
|
+
try {
|
|
3350
|
+
await captureSnapshot(ctx, req?.branch);
|
|
3351
|
+
} catch {
|
|
3352
|
+
}
|
|
3002
3353
|
return {
|
|
3003
3354
|
updated: true,
|
|
3004
3355
|
branch: r.branch,
|
|
@@ -3008,8 +3359,8 @@ async function handleContextUpdate(req, ctx) {
|
|
|
3008
3359
|
}
|
|
3009
3360
|
|
|
3010
3361
|
// src/server/routes/gate.ts
|
|
3011
|
-
import { appendFile as
|
|
3012
|
-
import { dirname as
|
|
3362
|
+
import { appendFile as appendFile4, mkdir as mkdir9 } from "fs/promises";
|
|
3363
|
+
import { dirname as dirname10 } from "path";
|
|
3013
3364
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
3014
3365
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
3015
3366
|
function extractQuery(toolName, input) {
|
|
@@ -3065,7 +3416,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
3065
3416
|
}
|
|
3066
3417
|
async function logDecision(ctx, toolName, query, decision, reason) {
|
|
3067
3418
|
try {
|
|
3068
|
-
await
|
|
3419
|
+
await mkdir9(dirname10(ctx.paths.gateLog), { recursive: true });
|
|
3069
3420
|
const entry = {
|
|
3070
3421
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3071
3422
|
tool: toolName,
|
|
@@ -3073,7 +3424,7 @@ async function logDecision(ctx, toolName, query, decision, reason) {
|
|
|
3073
3424
|
query,
|
|
3074
3425
|
reason
|
|
3075
3426
|
};
|
|
3076
|
-
await
|
|
3427
|
+
await appendFile4(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
|
|
3077
3428
|
} catch {
|
|
3078
3429
|
}
|
|
3079
3430
|
}
|
|
@@ -3137,16 +3488,16 @@ async function handleGate(req, ctx) {
|
|
|
3137
3488
|
}
|
|
3138
3489
|
|
|
3139
3490
|
// src/server/routes/log.ts
|
|
3140
|
-
import { appendFile as
|
|
3141
|
-
import { dirname as
|
|
3491
|
+
import { appendFile as appendFile5, mkdir as mkdir10 } from "fs/promises";
|
|
3492
|
+
import { dirname as dirname11 } from "path";
|
|
3142
3493
|
async function handleLog(entry, ctx) {
|
|
3143
3494
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
3144
3495
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
3145
3496
|
}
|
|
3146
3497
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3147
3498
|
const record = { ...entry, written_at };
|
|
3148
|
-
await
|
|
3149
|
-
await
|
|
3499
|
+
await mkdir10(dirname11(ctx.paths.tokenLog), { recursive: true });
|
|
3500
|
+
await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
3150
3501
|
return { ok: true, written_at };
|
|
3151
3502
|
}
|
|
3152
3503
|
|
|
@@ -3156,13 +3507,15 @@ async function handlePack(req, ctx) {
|
|
|
3156
3507
|
throw new Error("pack: 'query' (string) is required");
|
|
3157
3508
|
}
|
|
3158
3509
|
const recentlyEditedPaths = ctx.activity.recentFilePaths(15 * 60 * 1e3);
|
|
3159
|
-
const
|
|
3510
|
+
const usageScores = ctx.learn?.effectiveScores();
|
|
3511
|
+
const retrieval = await retrieve(ctx.graph, req.query, { recentlyEditedPaths, usageScores });
|
|
3160
3512
|
const allFiles = ctx.graph.nodes.filter((n) => n.kind === "file");
|
|
3161
3513
|
const scored = scoreFiles({
|
|
3162
3514
|
candidates: allFiles,
|
|
3163
3515
|
query: req.query,
|
|
3164
3516
|
graph: ctx.graph,
|
|
3165
|
-
recentlyEditedPaths
|
|
3517
|
+
recentlyEditedPaths,
|
|
3518
|
+
usageScores
|
|
3166
3519
|
});
|
|
3167
3520
|
const reasons = /* @__PURE__ */ new Map();
|
|
3168
3521
|
for (const s of scored) {
|
|
@@ -3184,14 +3537,74 @@ async function handlePack(req, ctx) {
|
|
|
3184
3537
|
}
|
|
3185
3538
|
|
|
3186
3539
|
// src/server/routes/prime.ts
|
|
3187
|
-
|
|
3540
|
+
var RESUME_PRIMER_MAX_CHARS = 2720;
|
|
3541
|
+
var MAX_FILES = 15;
|
|
3542
|
+
var MAX_COMMITS2 = 5;
|
|
3543
|
+
var MAX_BULLETS2 = 3;
|
|
3544
|
+
function legacyPrimer(ctx) {
|
|
3188
3545
|
const g = ctx.graph;
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
(
|
|
3194
|
-
|
|
3546
|
+
return `Synthra context loaded for ${g.root}.
|
|
3547
|
+
${g.file_count} files indexed, ${g.symbol_count} symbols. Prefer the graph_* MCP tools over Grep/Glob for navigation.`;
|
|
3548
|
+
}
|
|
3549
|
+
function hasContent(snap) {
|
|
3550
|
+
return Boolean(
|
|
3551
|
+
snap.recentCommits.length || snap.filesTouched.length || snap.summary.tasks.length || snap.summary.next.length || snap.summary.decisions.length
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3554
|
+
function buildResumeDigest(snap, branchNow) {
|
|
3555
|
+
const plural = (n) => n === 1 ? "" : "s";
|
|
3556
|
+
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)`;
|
|
3557
|
+
const essential = [head];
|
|
3558
|
+
if (snap.branch !== branchNow) {
|
|
3559
|
+
essential.push("");
|
|
3560
|
+
essential.push(
|
|
3561
|
+
`_(snapshot was for branch '${snap.branch}'; you're now on '${branchNow}' \u2014 may be stale)_`
|
|
3562
|
+
);
|
|
3563
|
+
}
|
|
3564
|
+
if (snap.summary.tasks[0]) {
|
|
3565
|
+
essential.push("", "### In progress", `- ${snap.summary.tasks[0]}`);
|
|
3566
|
+
}
|
|
3567
|
+
if (snap.summary.next.length) {
|
|
3568
|
+
essential.push("", "### Open next steps");
|
|
3569
|
+
for (const n of snap.summary.next.slice(0, MAX_BULLETS2)) essential.push(`- ${n}`);
|
|
3570
|
+
}
|
|
3571
|
+
if (snap.summary.decisions.length) {
|
|
3572
|
+
essential.push("", "### Recent decisions");
|
|
3573
|
+
for (const d of snap.summary.decisions.slice(0, MAX_BULLETS2)) essential.push(`- ${d}`);
|
|
3574
|
+
}
|
|
3575
|
+
const extra = [];
|
|
3576
|
+
if (snap.recentCommits.length) {
|
|
3577
|
+
extra.push("", "### Recent commits");
|
|
3578
|
+
for (const c of snap.recentCommits.slice(0, MAX_COMMITS2)) {
|
|
3579
|
+
const date = c.date ? ` (${c.date.slice(0, 10)})` : "";
|
|
3580
|
+
extra.push(`- \`${c.hash}\` ${c.message}${date}`);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
if (snap.filesTouched.length) {
|
|
3584
|
+
const shown = snap.filesTouched.slice(0, MAX_FILES);
|
|
3585
|
+
const more = snap.filesTouched.length - shown.length;
|
|
3586
|
+
extra.push("", "### Files touched", shown.join(", ") + (more > 0 ? `, +${more} more` : ""));
|
|
3587
|
+
}
|
|
3588
|
+
let out = essential.join("\n");
|
|
3589
|
+
for (const line of extra) {
|
|
3590
|
+
if ((out + "\n" + line).length > RESUME_PRIMER_MAX_CHARS) break;
|
|
3591
|
+
out += "\n" + line;
|
|
3592
|
+
}
|
|
3593
|
+
return (out.length > RESUME_PRIMER_MAX_CHARS ? out.slice(0, RESUME_PRIMER_MAX_CHARS) : out).trimEnd();
|
|
3594
|
+
}
|
|
3595
|
+
async function handlePrime(ctx, port) {
|
|
3596
|
+
const legacy = legacyPrimer(ctx);
|
|
3597
|
+
const snap = await readSession(ctx.paths.sessionState);
|
|
3598
|
+
if (!snap || !hasContent(snap)) {
|
|
3599
|
+
return { primer: legacy, port };
|
|
3600
|
+
}
|
|
3601
|
+
const branchNow = await currentBranch(ctx.paths.projectRoot);
|
|
3602
|
+
const digest = buildResumeDigest(snap, branchNow);
|
|
3603
|
+
return { primer: `${digest}
|
|
3604
|
+
|
|
3605
|
+
---
|
|
3606
|
+
|
|
3607
|
+
${legacy}`, port };
|
|
3195
3608
|
}
|
|
3196
3609
|
|
|
3197
3610
|
// src/server/http.ts
|
|
@@ -3202,9 +3615,7 @@ async function loadContext(paths) {
|
|
|
3202
3615
|
readSymbolIndex(paths.symbolIndex)
|
|
3203
3616
|
]);
|
|
3204
3617
|
if (graph.schema_version !== SCHEMA_VERSION) {
|
|
3205
|
-
log.info(
|
|
3206
|
-
`graph schema v${graph.schema_version} \u2260 current v${SCHEMA_VERSION} \u2014 rescanning\u2026`
|
|
3207
|
-
);
|
|
3618
|
+
log.info(`graph schema v${graph.schema_version} \u2260 current v${SCHEMA_VERSION} \u2014 rescanning\u2026`);
|
|
3208
3619
|
await scanProject(paths.projectRoot, { silent: true });
|
|
3209
3620
|
[graph, symbolIndex] = await Promise.all([
|
|
3210
3621
|
readGraph(paths.infoGraph),
|
|
@@ -3212,7 +3623,8 @@ async function loadContext(paths) {
|
|
|
3212
3623
|
]);
|
|
3213
3624
|
}
|
|
3214
3625
|
const activity = new ActivityStore(paths.activityLog);
|
|
3215
|
-
|
|
3626
|
+
const learn = await LearnRuntime.load(paths.accessLog, paths.learnStore);
|
|
3627
|
+
return { paths, graph, symbolIndex, activity, learn };
|
|
3216
3628
|
} catch (err2) {
|
|
3217
3629
|
throw new Error(
|
|
3218
3630
|
`failed to load graph from ${paths.infoGraph}: ${err2.message}. Run \`syn scan\` first.`
|
|
@@ -3249,9 +3661,7 @@ function buildApp(ctx, port) {
|
|
|
3249
3661
|
app.get("/activity", async (c) => {
|
|
3250
3662
|
const sinceParam = c.req.query("since");
|
|
3251
3663
|
const sinceMs = sinceParam ? Number(sinceParam) : void 0;
|
|
3252
|
-
return c.json(
|
|
3253
|
-
await handleActivity(Number.isFinite(sinceMs) ? sinceMs : void 0, ctx)
|
|
3254
|
-
);
|
|
3664
|
+
return c.json(await handleActivity(Number.isFinite(sinceMs) ? sinceMs : void 0, ctx));
|
|
3255
3665
|
});
|
|
3256
3666
|
app.post("/context-update", async (c) => {
|
|
3257
3667
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -3272,11 +3682,8 @@ async function startServer(paths, options = {}) {
|
|
|
3272
3682
|
const port = options.port ?? await findFreePort();
|
|
3273
3683
|
const app = buildApp(ctx, port);
|
|
3274
3684
|
const nodeServer = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" });
|
|
3275
|
-
await
|
|
3276
|
-
const fileWatcher = createFileWatcher(
|
|
3277
|
-
paths.projectRoot,
|
|
3278
|
-
(e) => ctx.activity.add(e)
|
|
3279
|
-
);
|
|
3685
|
+
await writeFile8(paths.mcpPort, String(port), "utf8");
|
|
3686
|
+
const fileWatcher = createFileWatcher(paths.projectRoot, (e) => ctx.activity.add(e));
|
|
3280
3687
|
const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
|
|
3281
3688
|
await ctx.activity.add(e);
|
|
3282
3689
|
if (e.kind === "branch-switch") {
|
|
@@ -3313,6 +3720,7 @@ async function startServer(paths, options = {}) {
|
|
|
3313
3720
|
async stop() {
|
|
3314
3721
|
await fileWatcher.stop().catch(() => void 0);
|
|
3315
3722
|
await gitWatcher.stop().catch(() => void 0);
|
|
3723
|
+
await ctx.learn?.flush().catch(() => void 0);
|
|
3316
3724
|
await new Promise((resolve2, reject) => {
|
|
3317
3725
|
nodeServer.close((err2) => err2 ? reject(err2) : resolve2());
|
|
3318
3726
|
});
|