@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/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,191 @@ 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 USAGE_BOOST_CAP_DEFAULT = 4;
|
|
2144
|
+
function usageBoostCap() {
|
|
2145
|
+
const env = Number(process.env.SYN_LEARN_BOOST_CAP);
|
|
2146
|
+
return Number.isFinite(env) && env >= 0 ? env : USAGE_BOOST_CAP_DEFAULT;
|
|
2147
|
+
}
|
|
1940
2148
|
var STOPWORDS2 = /* @__PURE__ */ new Set([
|
|
1941
2149
|
"a",
|
|
1942
2150
|
"an",
|
|
@@ -2083,6 +2291,21 @@ function scoreFiles(inputs) {
|
|
|
2083
2291
|
}
|
|
2084
2292
|
}
|
|
2085
2293
|
}
|
|
2294
|
+
const usage = inputs.usageScores;
|
|
2295
|
+
if (usage && usage.size > 0) {
|
|
2296
|
+
let maxU = 0;
|
|
2297
|
+
for (const v of usage.values()) if (v > maxU) maxU = v;
|
|
2298
|
+
if (maxU > 0) {
|
|
2299
|
+
const cap = usageBoostCap();
|
|
2300
|
+
for (const s of scored) {
|
|
2301
|
+
if (s.score <= 0) continue;
|
|
2302
|
+
const u = usage.get(s.file.path) ?? 0;
|
|
2303
|
+
if (u <= 0) continue;
|
|
2304
|
+
s.score += cap * (u / maxU);
|
|
2305
|
+
s.reasons.push(`used\xD7${Math.round(u)}`);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2086
2309
|
scored.sort((a, b) => b.score - a.score);
|
|
2087
2310
|
return scored;
|
|
2088
2311
|
}
|
|
@@ -2091,9 +2314,7 @@ function scoreFiles(inputs) {
|
|
|
2091
2314
|
async function retrieve(graph, query, options = {}) {
|
|
2092
2315
|
const topK = options.topK ?? 12;
|
|
2093
2316
|
const qTokens = tokenizeQuery(query);
|
|
2094
|
-
const allFiles = graph.nodes.filter(
|
|
2095
|
-
(n) => n.kind === "file"
|
|
2096
|
-
);
|
|
2317
|
+
const allFiles = graph.nodes.filter((n) => n.kind === "file");
|
|
2097
2318
|
if (allFiles.length === 0 || qTokens.length === 0) {
|
|
2098
2319
|
return {
|
|
2099
2320
|
files: [],
|
|
@@ -2107,7 +2328,8 @@ async function retrieve(graph, query, options = {}) {
|
|
|
2107
2328
|
query,
|
|
2108
2329
|
graph,
|
|
2109
2330
|
recentlyEditedPaths: options.recentlyEditedPaths,
|
|
2110
|
-
sessionKnownPaths: options.sessionKnownPaths
|
|
2331
|
+
sessionKnownPaths: options.sessionKnownPaths,
|
|
2332
|
+
usageScores: options.usageScores
|
|
2111
2333
|
};
|
|
2112
2334
|
const scored = scoreFiles(rankInputs);
|
|
2113
2335
|
const positive = scored.filter((s) => s.score > 0);
|
|
@@ -2140,14 +2362,14 @@ async function retrieve(graph, query, options = {}) {
|
|
|
2140
2362
|
|
|
2141
2363
|
// src/memory/branches.ts
|
|
2142
2364
|
import { execFile as execFile2 } from "child_process";
|
|
2143
|
-
import { readFile as
|
|
2365
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2144
2366
|
import { join as join6 } from "path";
|
|
2145
2367
|
import { promisify as promisify2 } from "util";
|
|
2146
2368
|
var execFileAsync2 = promisify2(execFile2);
|
|
2147
2369
|
async function currentBranch(projectRoot) {
|
|
2148
2370
|
try {
|
|
2149
2371
|
const headPath = join6(projectRoot, ".git", "HEAD");
|
|
2150
|
-
const head = await
|
|
2372
|
+
const head = await readFile9(headPath, "utf8");
|
|
2151
2373
|
const trimmed = head.trim();
|
|
2152
2374
|
const match = trimmed.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
2153
2375
|
if (match?.[1]) return match[1];
|
|
@@ -2197,8 +2419,8 @@ function resolveBranchPaths(contextDir, branch, isDefault) {
|
|
|
2197
2419
|
}
|
|
2198
2420
|
|
|
2199
2421
|
// src/memory/context-md.ts
|
|
2200
|
-
import { mkdir as
|
|
2201
|
-
import { dirname as
|
|
2422
|
+
import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
2423
|
+
import { dirname as dirname6 } from "path";
|
|
2202
2424
|
var MAX_BULLETS = 3;
|
|
2203
2425
|
function deriveContextMd(entries, branch) {
|
|
2204
2426
|
const tasks = entries.filter((e) => e.type === "task").reverse();
|
|
@@ -2241,17 +2463,17 @@ function formatContextMd(ctx) {
|
|
|
2241
2463
|
return lines.join("\n");
|
|
2242
2464
|
}
|
|
2243
2465
|
async function writeContextMd(path, ctx) {
|
|
2244
|
-
await
|
|
2245
|
-
await
|
|
2466
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
2467
|
+
await writeFile5(path, formatContextMd(ctx), "utf8");
|
|
2246
2468
|
}
|
|
2247
2469
|
|
|
2248
2470
|
// src/memory/context-store.ts
|
|
2249
|
-
import { mkdir as
|
|
2250
|
-
import { dirname as
|
|
2471
|
+
import { mkdir as mkdir6, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
2472
|
+
import { dirname as dirname7 } from "path";
|
|
2251
2473
|
var SCHEMA_VERSION2 = 1;
|
|
2252
2474
|
async function readEntries(path) {
|
|
2253
2475
|
try {
|
|
2254
|
-
const raw = await
|
|
2476
|
+
const raw = await readFile11(path, "utf8");
|
|
2255
2477
|
const parsed = JSON.parse(raw);
|
|
2256
2478
|
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
2257
2479
|
} catch {
|
|
@@ -2259,9 +2481,9 @@ async function readEntries(path) {
|
|
|
2259
2481
|
}
|
|
2260
2482
|
}
|
|
2261
2483
|
async function writeEntries(path, entries) {
|
|
2262
|
-
await
|
|
2484
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
2263
2485
|
const store = { schema_version: SCHEMA_VERSION2, entries };
|
|
2264
|
-
await
|
|
2486
|
+
await writeFile6(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
2265
2487
|
}
|
|
2266
2488
|
async function appendEntry(path, entry) {
|
|
2267
2489
|
const entries = await readEntries(path);
|
|
@@ -2535,7 +2757,10 @@ var TOOLS = [
|
|
|
2535
2757
|
inputSchema: {
|
|
2536
2758
|
type: "object",
|
|
2537
2759
|
properties: {
|
|
2538
|
-
query: {
|
|
2760
|
+
query: {
|
|
2761
|
+
type: "string",
|
|
2762
|
+
description: "Natural-language description of what you're looking for."
|
|
2763
|
+
}
|
|
2539
2764
|
},
|
|
2540
2765
|
required: ["query"]
|
|
2541
2766
|
}
|
|
@@ -2691,9 +2916,7 @@ function blastRadius(args, ctx) {
|
|
|
2691
2916
|
const maxDepth = typeof args?.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 3;
|
|
2692
2917
|
if (!targetRaw) return errorContent("blast_radius: 'target' (string) is required");
|
|
2693
2918
|
const filePath = targetRaw.split("::", 1)[0]?.trim() ?? targetRaw;
|
|
2694
|
-
const root = ctx.graph.nodes.find(
|
|
2695
|
-
(n) => n.kind === "file" && n.path === filePath
|
|
2696
|
-
);
|
|
2919
|
+
const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
|
|
2697
2920
|
if (!root) return errorContent(`blast_radius: file not in graph: ${filePath}`);
|
|
2698
2921
|
const incoming = /* @__PURE__ */ new Map();
|
|
2699
2922
|
for (const e of ctx.graph.edges) {
|
|
@@ -2740,8 +2963,8 @@ var LIKELY_ENTRY_PATTERNS = [
|
|
|
2740
2963
|
/(?:^|\/)index\.[a-z0-9_]+$/i,
|
|
2741
2964
|
/(?:^|\/)app\.[a-z0-9_]+$/i,
|
|
2742
2965
|
/(?:^|\/)entry\.[a-z0-9_]+$/i,
|
|
2743
|
-
/(?:^|\/)cli[
|
|
2744
|
-
/(?:^|\/)bin[
|
|
2966
|
+
/(?:^|\/)cli[/.]/i,
|
|
2967
|
+
/(?:^|\/)bin[/.]/i,
|
|
2745
2968
|
/(?:^|\/)server\.[a-z0-9_]+$/i,
|
|
2746
2969
|
/\.test\.[a-z0-9_]+$/i,
|
|
2747
2970
|
/\.spec\.[a-z0-9_]+$/i,
|
|
@@ -2787,9 +3010,11 @@ async function graphContinue(args, ctx) {
|
|
|
2787
3010
|
if (!query) return errorContent("graph_continue: 'query' (string) is required");
|
|
2788
3011
|
const retrieval = await retrieve(ctx.graph, query, {
|
|
2789
3012
|
recentlyEditedPaths: ctx.activity.recentFilePaths(15 * 60 * 1e3),
|
|
2790
|
-
sessionKnownPaths: getRegisteredEdits()
|
|
3013
|
+
sessionKnownPaths: getRegisteredEdits(),
|
|
3014
|
+
usageScores: ctx.learn?.effectiveScores()
|
|
2791
3015
|
});
|
|
2792
3016
|
const packed = await pack(retrieval.files, { query, graph: ctx.graph });
|
|
3017
|
+
await logAccess(ctx, { ts: nowIso(), path: "", source: "continue", query });
|
|
2793
3018
|
const header = `Confidence: ${retrieval.confidence}
|
|
2794
3019
|
Files: ${retrieval.files.map((f) => f.path).join(", ") || "(none)"}
|
|
2795
3020
|
Reason: ${retrieval.reason}
|
|
@@ -2807,7 +3032,7 @@ function resolveFileTarget(graph, filePath) {
|
|
|
2807
3032
|
if (matches.length > 1) return { ambiguous: matches.map((n) => n.path) };
|
|
2808
3033
|
return { none: true };
|
|
2809
3034
|
}
|
|
2810
|
-
function graphRead(args, ctx) {
|
|
3035
|
+
async function graphRead(args, ctx) {
|
|
2811
3036
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
2812
3037
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
2813
3038
|
const [rawFile, symbolName] = target.includes("::") ? target.split("::", 2) : [target, void 0];
|
|
@@ -2824,6 +3049,7 @@ function graphRead(args, ctx) {
|
|
|
2824
3049
|
return errorContent(`graph_read: file not found in graph: ${filePath}`);
|
|
2825
3050
|
}
|
|
2826
3051
|
const fileNode = resolved.node;
|
|
3052
|
+
await logAccess(ctx, { ts: nowIso(), path: fileNode.path, source: "read" });
|
|
2827
3053
|
if (!symbolName) {
|
|
2828
3054
|
return textContent(`# ${fileNode.path}
|
|
2829
3055
|
|
|
@@ -2845,10 +3071,21 @@ ${body}`
|
|
|
2845
3071
|
);
|
|
2846
3072
|
}
|
|
2847
3073
|
var editedFiles = /* @__PURE__ */ new Set();
|
|
2848
|
-
function graphRegisterEdit(args,
|
|
3074
|
+
async function graphRegisterEdit(args, ctx) {
|
|
2849
3075
|
const files = Array.isArray(args?.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
2850
|
-
for (const f of files)
|
|
2851
|
-
|
|
3076
|
+
for (const f of files) {
|
|
3077
|
+
const file = f;
|
|
3078
|
+
editedFiles.add(file);
|
|
3079
|
+
const resolved = resolveFileTarget(ctx.graph, file);
|
|
3080
|
+
await logAccess(ctx, {
|
|
3081
|
+
ts: nowIso(),
|
|
3082
|
+
path: "node" in resolved ? resolved.node.path : file,
|
|
3083
|
+
source: "register_edit"
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
return textContent(
|
|
3087
|
+
`Registered ${files.length} edited file(s). Total tracked this session: ${editedFiles.size}.`
|
|
3088
|
+
);
|
|
2852
3089
|
}
|
|
2853
3090
|
function getRegisteredEdits() {
|
|
2854
3091
|
return Array.from(editedFiles);
|
|
@@ -2884,9 +3121,7 @@ function recentActivity(args, ctx) {
|
|
|
2884
3121
|
let events = ctx.activity.getEvents(sinceMs);
|
|
2885
3122
|
if (limit) events = events.slice(-limit);
|
|
2886
3123
|
if (events.length === 0) {
|
|
2887
|
-
return textContent(
|
|
2888
|
-
`No human-activity events since ${new Date(sinceMs).toISOString()}.`
|
|
2889
|
-
);
|
|
3124
|
+
return textContent(`No human-activity events since ${new Date(sinceMs).toISOString()}.`);
|
|
2890
3125
|
}
|
|
2891
3126
|
const lines = [`# Recent human activity (${events.length} events)`, ""];
|
|
2892
3127
|
for (const e of events) {
|
|
@@ -2918,8 +3153,8 @@ async function contextRecall(args, ctx) {
|
|
|
2918
3153
|
}
|
|
2919
3154
|
async function logToolCall(ctx, tool) {
|
|
2920
3155
|
try {
|
|
2921
|
-
await
|
|
2922
|
-
await
|
|
3156
|
+
await mkdir7(dirname8(ctx.paths.toolLog), { recursive: true });
|
|
3157
|
+
await appendFile3(
|
|
2923
3158
|
ctx.paths.toolLog,
|
|
2924
3159
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
|
|
2925
3160
|
"utf8"
|
|
@@ -2927,6 +3162,16 @@ async function logToolCall(ctx, tool) {
|
|
|
2927
3162
|
} catch {
|
|
2928
3163
|
}
|
|
2929
3164
|
}
|
|
3165
|
+
async function logAccess(ctx, ev) {
|
|
3166
|
+
try {
|
|
3167
|
+
if (ctx.learn) await ctx.learn.record(ev);
|
|
3168
|
+
else await appendAccess(ctx.paths.accessLog, ev);
|
|
3169
|
+
} catch {
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
function nowIso() {
|
|
3173
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3174
|
+
}
|
|
2930
3175
|
async function handleMcpRequest(body, ctx) {
|
|
2931
3176
|
if (!body || typeof body !== "object") {
|
|
2932
3177
|
return err(null, ERR.invalidRequest, "Request body must be a JSON-RPC 2.0 object.");
|
|
@@ -2996,9 +3241,87 @@ async function handleActivity(sinceMs, ctx) {
|
|
|
2996
3241
|
};
|
|
2997
3242
|
}
|
|
2998
3243
|
|
|
3244
|
+
// src/memory/git-snapshot.ts
|
|
3245
|
+
import { execFile as execFile3 } from "child_process";
|
|
3246
|
+
import { promisify as promisify3 } from "util";
|
|
3247
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
3248
|
+
var MAX_COMMITS = 5;
|
|
3249
|
+
var FIELD = "";
|
|
3250
|
+
async function getCommitsSince(projectRoot, sinceIso) {
|
|
3251
|
+
const args = [
|
|
3252
|
+
"log",
|
|
3253
|
+
`--max-count=${MAX_COMMITS}`,
|
|
3254
|
+
"--no-merges",
|
|
3255
|
+
`--pretty=format:%h${FIELD}%s${FIELD}%aI`
|
|
3256
|
+
];
|
|
3257
|
+
if (Number.isFinite(Date.parse(sinceIso))) args.push(`--since=${sinceIso}`);
|
|
3258
|
+
try {
|
|
3259
|
+
const { stdout } = await execFileAsync3("git", args, { cwd: projectRoot });
|
|
3260
|
+
const out = [];
|
|
3261
|
+
for (const line of stdout.split("\n")) {
|
|
3262
|
+
const t = line.trim();
|
|
3263
|
+
if (!t) continue;
|
|
3264
|
+
const [hash, message, date] = t.split(FIELD);
|
|
3265
|
+
if (hash && message) out.push({ hash, message, date: date ?? "" });
|
|
3266
|
+
}
|
|
3267
|
+
return out;
|
|
3268
|
+
} catch {
|
|
3269
|
+
return [];
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
// src/memory/session.ts
|
|
3274
|
+
import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
|
|
3275
|
+
import { dirname as dirname9 } from "path";
|
|
3276
|
+
var SESSION_SCHEMA_VERSION = 1;
|
|
3277
|
+
async function readSession(path) {
|
|
3278
|
+
try {
|
|
3279
|
+
const raw = await readFile12(path, "utf8");
|
|
3280
|
+
const parsed = JSON.parse(raw);
|
|
3281
|
+
if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
|
|
3282
|
+
return parsed;
|
|
3283
|
+
} catch {
|
|
3284
|
+
return null;
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
async function writeSession(path, state) {
|
|
3288
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
3289
|
+
await writeFile7(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
3290
|
+
}
|
|
3291
|
+
|
|
2999
3292
|
// src/server/routes/context-update.ts
|
|
3293
|
+
var TOUCHED_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
3294
|
+
async function captureSnapshot(ctx, branchOverride) {
|
|
3295
|
+
const active = await resolveActiveBranch(ctx.paths, branchOverride);
|
|
3296
|
+
const [tasks, decisions, next] = await Promise.all([
|
|
3297
|
+
recallEntries(ctx.paths, { kind: "task", branch: active.branch, limit: 1 }),
|
|
3298
|
+
recallEntries(ctx.paths, { kind: "decision", branch: active.branch, limit: 3 }),
|
|
3299
|
+
recallEntries(ctx.paths, { kind: "next", branch: active.branch, limit: 3 })
|
|
3300
|
+
]);
|
|
3301
|
+
const touched = new Set(getRegisteredEdits());
|
|
3302
|
+
for (const p of ctx.activity.recentFilePaths(TOUCHED_WINDOW_MS)) touched.add(p);
|
|
3303
|
+
const prev = await readSession(ctx.paths.sessionState);
|
|
3304
|
+
const recentCommits = await getCommitsSince(ctx.paths.projectRoot, prev?.endedAt ?? "");
|
|
3305
|
+
const snapshot = {
|
|
3306
|
+
schema_version: SESSION_SCHEMA_VERSION,
|
|
3307
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3308
|
+
branch: active.branch,
|
|
3309
|
+
filesTouched: Array.from(touched),
|
|
3310
|
+
recentCommits,
|
|
3311
|
+
summary: {
|
|
3312
|
+
tasks: tasks.entries.map((e) => e.content),
|
|
3313
|
+
decisions: decisions.entries.map((e) => e.content),
|
|
3314
|
+
next: next.entries.map((e) => e.content)
|
|
3315
|
+
}
|
|
3316
|
+
};
|
|
3317
|
+
await writeSession(ctx.paths.sessionState, snapshot);
|
|
3318
|
+
}
|
|
3000
3319
|
async function handleContextUpdate(req, ctx) {
|
|
3001
3320
|
const r = await refreshContextMd(ctx.paths, req?.branch);
|
|
3321
|
+
try {
|
|
3322
|
+
await captureSnapshot(ctx, req?.branch);
|
|
3323
|
+
} catch {
|
|
3324
|
+
}
|
|
3002
3325
|
return {
|
|
3003
3326
|
updated: true,
|
|
3004
3327
|
branch: r.branch,
|
|
@@ -3008,8 +3331,8 @@ async function handleContextUpdate(req, ctx) {
|
|
|
3008
3331
|
}
|
|
3009
3332
|
|
|
3010
3333
|
// src/server/routes/gate.ts
|
|
3011
|
-
import { appendFile as
|
|
3012
|
-
import { dirname as
|
|
3334
|
+
import { appendFile as appendFile4, mkdir as mkdir9 } from "fs/promises";
|
|
3335
|
+
import { dirname as dirname10 } from "path";
|
|
3013
3336
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
3014
3337
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
3015
3338
|
function extractQuery(toolName, input) {
|
|
@@ -3065,7 +3388,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
3065
3388
|
}
|
|
3066
3389
|
async function logDecision(ctx, toolName, query, decision, reason) {
|
|
3067
3390
|
try {
|
|
3068
|
-
await
|
|
3391
|
+
await mkdir9(dirname10(ctx.paths.gateLog), { recursive: true });
|
|
3069
3392
|
const entry = {
|
|
3070
3393
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3071
3394
|
tool: toolName,
|
|
@@ -3073,7 +3396,7 @@ async function logDecision(ctx, toolName, query, decision, reason) {
|
|
|
3073
3396
|
query,
|
|
3074
3397
|
reason
|
|
3075
3398
|
};
|
|
3076
|
-
await
|
|
3399
|
+
await appendFile4(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
|
|
3077
3400
|
} catch {
|
|
3078
3401
|
}
|
|
3079
3402
|
}
|
|
@@ -3137,16 +3460,16 @@ async function handleGate(req, ctx) {
|
|
|
3137
3460
|
}
|
|
3138
3461
|
|
|
3139
3462
|
// src/server/routes/log.ts
|
|
3140
|
-
import { appendFile as
|
|
3141
|
-
import { dirname as
|
|
3463
|
+
import { appendFile as appendFile5, mkdir as mkdir10 } from "fs/promises";
|
|
3464
|
+
import { dirname as dirname11 } from "path";
|
|
3142
3465
|
async function handleLog(entry, ctx) {
|
|
3143
3466
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
3144
3467
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
3145
3468
|
}
|
|
3146
3469
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3147
3470
|
const record = { ...entry, written_at };
|
|
3148
|
-
await
|
|
3149
|
-
await
|
|
3471
|
+
await mkdir10(dirname11(ctx.paths.tokenLog), { recursive: true });
|
|
3472
|
+
await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
3150
3473
|
return { ok: true, written_at };
|
|
3151
3474
|
}
|
|
3152
3475
|
|
|
@@ -3156,13 +3479,15 @@ async function handlePack(req, ctx) {
|
|
|
3156
3479
|
throw new Error("pack: 'query' (string) is required");
|
|
3157
3480
|
}
|
|
3158
3481
|
const recentlyEditedPaths = ctx.activity.recentFilePaths(15 * 60 * 1e3);
|
|
3159
|
-
const
|
|
3482
|
+
const usageScores = ctx.learn?.effectiveScores();
|
|
3483
|
+
const retrieval = await retrieve(ctx.graph, req.query, { recentlyEditedPaths, usageScores });
|
|
3160
3484
|
const allFiles = ctx.graph.nodes.filter((n) => n.kind === "file");
|
|
3161
3485
|
const scored = scoreFiles({
|
|
3162
3486
|
candidates: allFiles,
|
|
3163
3487
|
query: req.query,
|
|
3164
3488
|
graph: ctx.graph,
|
|
3165
|
-
recentlyEditedPaths
|
|
3489
|
+
recentlyEditedPaths,
|
|
3490
|
+
usageScores
|
|
3166
3491
|
});
|
|
3167
3492
|
const reasons = /* @__PURE__ */ new Map();
|
|
3168
3493
|
for (const s of scored) {
|
|
@@ -3184,14 +3509,74 @@ async function handlePack(req, ctx) {
|
|
|
3184
3509
|
}
|
|
3185
3510
|
|
|
3186
3511
|
// src/server/routes/prime.ts
|
|
3187
|
-
|
|
3512
|
+
var RESUME_PRIMER_MAX_CHARS = 2720;
|
|
3513
|
+
var MAX_FILES = 15;
|
|
3514
|
+
var MAX_COMMITS2 = 5;
|
|
3515
|
+
var MAX_BULLETS2 = 3;
|
|
3516
|
+
function legacyPrimer(ctx) {
|
|
3188
3517
|
const g = ctx.graph;
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
(
|
|
3194
|
-
|
|
3518
|
+
return `Synthra context loaded for ${g.root}.
|
|
3519
|
+
${g.file_count} files indexed, ${g.symbol_count} symbols. Prefer the graph_* MCP tools over Grep/Glob for navigation.`;
|
|
3520
|
+
}
|
|
3521
|
+
function hasContent(snap) {
|
|
3522
|
+
return Boolean(
|
|
3523
|
+
snap.recentCommits.length || snap.filesTouched.length || snap.summary.tasks.length || snap.summary.next.length || snap.summary.decisions.length
|
|
3524
|
+
);
|
|
3525
|
+
}
|
|
3526
|
+
function buildResumeDigest(snap, branchNow) {
|
|
3527
|
+
const plural = (n) => n === 1 ? "" : "s";
|
|
3528
|
+
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)`;
|
|
3529
|
+
const essential = [head];
|
|
3530
|
+
if (snap.branch !== branchNow) {
|
|
3531
|
+
essential.push("");
|
|
3532
|
+
essential.push(
|
|
3533
|
+
`_(snapshot was for branch '${snap.branch}'; you're now on '${branchNow}' \u2014 may be stale)_`
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
if (snap.summary.tasks[0]) {
|
|
3537
|
+
essential.push("", "### In progress", `- ${snap.summary.tasks[0]}`);
|
|
3538
|
+
}
|
|
3539
|
+
if (snap.summary.next.length) {
|
|
3540
|
+
essential.push("", "### Open next steps");
|
|
3541
|
+
for (const n of snap.summary.next.slice(0, MAX_BULLETS2)) essential.push(`- ${n}`);
|
|
3542
|
+
}
|
|
3543
|
+
if (snap.summary.decisions.length) {
|
|
3544
|
+
essential.push("", "### Recent decisions");
|
|
3545
|
+
for (const d of snap.summary.decisions.slice(0, MAX_BULLETS2)) essential.push(`- ${d}`);
|
|
3546
|
+
}
|
|
3547
|
+
const extra = [];
|
|
3548
|
+
if (snap.recentCommits.length) {
|
|
3549
|
+
extra.push("", "### Recent commits");
|
|
3550
|
+
for (const c of snap.recentCommits.slice(0, MAX_COMMITS2)) {
|
|
3551
|
+
const date = c.date ? ` (${c.date.slice(0, 10)})` : "";
|
|
3552
|
+
extra.push(`- \`${c.hash}\` ${c.message}${date}`);
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
if (snap.filesTouched.length) {
|
|
3556
|
+
const shown = snap.filesTouched.slice(0, MAX_FILES);
|
|
3557
|
+
const more = snap.filesTouched.length - shown.length;
|
|
3558
|
+
extra.push("", "### Files touched", shown.join(", ") + (more > 0 ? `, +${more} more` : ""));
|
|
3559
|
+
}
|
|
3560
|
+
let out = essential.join("\n");
|
|
3561
|
+
for (const line of extra) {
|
|
3562
|
+
if ((out + "\n" + line).length > RESUME_PRIMER_MAX_CHARS) break;
|
|
3563
|
+
out += "\n" + line;
|
|
3564
|
+
}
|
|
3565
|
+
return (out.length > RESUME_PRIMER_MAX_CHARS ? out.slice(0, RESUME_PRIMER_MAX_CHARS) : out).trimEnd();
|
|
3566
|
+
}
|
|
3567
|
+
async function handlePrime(ctx, port) {
|
|
3568
|
+
const legacy = legacyPrimer(ctx);
|
|
3569
|
+
const snap = await readSession(ctx.paths.sessionState);
|
|
3570
|
+
if (!snap || !hasContent(snap)) {
|
|
3571
|
+
return { primer: legacy, port };
|
|
3572
|
+
}
|
|
3573
|
+
const branchNow = await currentBranch(ctx.paths.projectRoot);
|
|
3574
|
+
const digest = buildResumeDigest(snap, branchNow);
|
|
3575
|
+
return { primer: `${digest}
|
|
3576
|
+
|
|
3577
|
+
---
|
|
3578
|
+
|
|
3579
|
+
${legacy}`, port };
|
|
3195
3580
|
}
|
|
3196
3581
|
|
|
3197
3582
|
// src/server/http.ts
|
|
@@ -3202,9 +3587,7 @@ async function loadContext(paths) {
|
|
|
3202
3587
|
readSymbolIndex(paths.symbolIndex)
|
|
3203
3588
|
]);
|
|
3204
3589
|
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
|
-
);
|
|
3590
|
+
log.info(`graph schema v${graph.schema_version} \u2260 current v${SCHEMA_VERSION} \u2014 rescanning\u2026`);
|
|
3208
3591
|
await scanProject(paths.projectRoot, { silent: true });
|
|
3209
3592
|
[graph, symbolIndex] = await Promise.all([
|
|
3210
3593
|
readGraph(paths.infoGraph),
|
|
@@ -3212,7 +3595,8 @@ async function loadContext(paths) {
|
|
|
3212
3595
|
]);
|
|
3213
3596
|
}
|
|
3214
3597
|
const activity = new ActivityStore(paths.activityLog);
|
|
3215
|
-
|
|
3598
|
+
const learn = await LearnRuntime.load(paths.accessLog, paths.learnStore);
|
|
3599
|
+
return { paths, graph, symbolIndex, activity, learn };
|
|
3216
3600
|
} catch (err2) {
|
|
3217
3601
|
throw new Error(
|
|
3218
3602
|
`failed to load graph from ${paths.infoGraph}: ${err2.message}. Run \`syn scan\` first.`
|
|
@@ -3249,9 +3633,7 @@ function buildApp(ctx, port) {
|
|
|
3249
3633
|
app.get("/activity", async (c) => {
|
|
3250
3634
|
const sinceParam = c.req.query("since");
|
|
3251
3635
|
const sinceMs = sinceParam ? Number(sinceParam) : void 0;
|
|
3252
|
-
return c.json(
|
|
3253
|
-
await handleActivity(Number.isFinite(sinceMs) ? sinceMs : void 0, ctx)
|
|
3254
|
-
);
|
|
3636
|
+
return c.json(await handleActivity(Number.isFinite(sinceMs) ? sinceMs : void 0, ctx));
|
|
3255
3637
|
});
|
|
3256
3638
|
app.post("/context-update", async (c) => {
|
|
3257
3639
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -3272,11 +3654,8 @@ async function startServer(paths, options = {}) {
|
|
|
3272
3654
|
const port = options.port ?? await findFreePort();
|
|
3273
3655
|
const app = buildApp(ctx, port);
|
|
3274
3656
|
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
|
-
);
|
|
3657
|
+
await writeFile8(paths.mcpPort, String(port), "utf8");
|
|
3658
|
+
const fileWatcher = createFileWatcher(paths.projectRoot, (e) => ctx.activity.add(e));
|
|
3280
3659
|
const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
|
|
3281
3660
|
await ctx.activity.add(e);
|
|
3282
3661
|
if (e.kind === "branch-switch") {
|
|
@@ -3313,6 +3692,7 @@ async function startServer(paths, options = {}) {
|
|
|
3313
3692
|
async stop() {
|
|
3314
3693
|
await fileWatcher.stop().catch(() => void 0);
|
|
3315
3694
|
await gitWatcher.stop().catch(() => void 0);
|
|
3695
|
+
await ctx.learn?.flush().catch(() => void 0);
|
|
3316
3696
|
await new Promise((resolve2, reject) => {
|
|
3317
3697
|
nodeServer.close((err2) => err2 ? reject(err2) : resolve2());
|
|
3318
3698
|
});
|