@jefuriiij/synthra 0.14.1 → 0.16.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 +43 -0
- package/dist/cli/index.js +339 -34
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +2 -1
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +525 -41
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1560,16 +1560,16 @@ async function readParseCache(path) {
|
|
|
1560
1560
|
return emptyParseCache();
|
|
1561
1561
|
}
|
|
1562
1562
|
}
|
|
1563
|
-
async function writeParseCache(path,
|
|
1563
|
+
async function writeParseCache(path, cache2) {
|
|
1564
1564
|
try {
|
|
1565
1565
|
await mkdir2(dirname3(path), { recursive: true });
|
|
1566
|
-
await writeFile(path, `${JSON.stringify(
|
|
1566
|
+
await writeFile(path, `${JSON.stringify(cache2)}
|
|
1567
1567
|
`, "utf8");
|
|
1568
1568
|
} catch {
|
|
1569
1569
|
}
|
|
1570
1570
|
}
|
|
1571
1571
|
async function incrementalParse(parsable, prev, opts = {}) {
|
|
1572
|
-
const
|
|
1572
|
+
const cache2 = emptyParseCache();
|
|
1573
1573
|
const parsed = [];
|
|
1574
1574
|
let reused = 0;
|
|
1575
1575
|
let reparsed = 0;
|
|
@@ -1591,20 +1591,20 @@ async function incrementalParse(parsable, prev, opts = {}) {
|
|
|
1591
1591
|
imports: cached.imports,
|
|
1592
1592
|
calls: cached.calls
|
|
1593
1593
|
});
|
|
1594
|
-
|
|
1594
|
+
cache2.files[f.relPath] = cached;
|
|
1595
1595
|
reused += 1;
|
|
1596
1596
|
continue;
|
|
1597
1597
|
}
|
|
1598
1598
|
try {
|
|
1599
1599
|
const p = await parseSource(f, source);
|
|
1600
1600
|
parsed.push(p);
|
|
1601
|
-
|
|
1601
|
+
cache2.files[f.relPath] = { hash, symbols: p.symbols, imports: p.imports, calls: p.calls };
|
|
1602
1602
|
reparsed += 1;
|
|
1603
1603
|
} catch {
|
|
1604
1604
|
parseErrors += 1;
|
|
1605
1605
|
}
|
|
1606
1606
|
}
|
|
1607
|
-
return { parsed, cache, reused, reparsed, parseErrors };
|
|
1607
|
+
return { parsed, cache: cache2, reused, reparsed, parseErrors };
|
|
1608
1608
|
}
|
|
1609
1609
|
|
|
1610
1610
|
// src/scanner/walker.ts
|
|
@@ -1796,6 +1796,7 @@ function resolvePaths(projectRoot) {
|
|
|
1796
1796
|
tokenLog: join5(graphDir, "token_log.jsonl"),
|
|
1797
1797
|
gateLog: join5(graphDir, "gate_log.jsonl"),
|
|
1798
1798
|
bashLog: join5(graphDir, "bash_log.jsonl"),
|
|
1799
|
+
routeLog: join5(graphDir, "route_log.jsonl"),
|
|
1799
1800
|
toolLog: join5(graphDir, "tool_log.jsonl"),
|
|
1800
1801
|
accessLog: join5(graphDir, "access_log.jsonl"),
|
|
1801
1802
|
learnStore: join5(graphDir, "learn_store.json"),
|
|
@@ -1821,7 +1822,7 @@ import { basename as basename2 } from "path";
|
|
|
1821
1822
|
// src/hooks/claude-md.ts
|
|
1822
1823
|
import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
1823
1824
|
import { basename, dirname as dirname5 } from "path";
|
|
1824
|
-
var POLICY_VERSION =
|
|
1825
|
+
var POLICY_VERSION = 9;
|
|
1825
1826
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
1826
1827
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
1827
1828
|
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;
|
|
@@ -1840,7 +1841,7 @@ function policyBlock() {
|
|
|
1840
1841
|
"> `mcp__synthra__graph_register_edit`. **Short names will NOT resolve**",
|
|
1841
1842
|
"> in ToolSearch or invocation \u2014 always use the full namespaced form.",
|
|
1842
1843
|
"> If the tools are deferred, load their schemas with ToolSearch:",
|
|
1843
|
-
"> `select:mcp__synthra__graph_continue,mcp__synthra__graph_read,mcp__synthra__graph_register_edit,mcp__synthra__find_symbol`.",
|
|
1844
|
+
"> `select:mcp__synthra__graph_continue,mcp__synthra__graph_read,mcp__synthra__graph_register_edit,mcp__synthra__find_symbol,mcp__synthra__route_task`.",
|
|
1844
1845
|
"> Below, short names (`graph_continue` etc.) appear in prose for",
|
|
1845
1846
|
"> readability only.",
|
|
1846
1847
|
"",
|
|
@@ -1858,6 +1859,11 @@ function policyBlock() {
|
|
|
1858
1859
|
" util, or function, call this to check whether one already exists. If it",
|
|
1859
1860
|
" returns matches, reuse or extend them instead of re-implementing; only",
|
|
1860
1861
|
' "no match \u2014 safe to create" means it is genuinely new.',
|
|
1862
|
+
"- **`route_task(task)`** \u2014 **delegate-first**: before starting a multi-step",
|
|
1863
|
+
" implementation task, ask which installed subagent/skill fits it. Plan on",
|
|
1864
|
+
" the primary model, then hand execution to the recommended subagent on a",
|
|
1865
|
+
" cheaper model (sonnet \u2248 5\xD7 cheaper than opus). Synthra may also inject a",
|
|
1866
|
+
" `[Synthra route]` hint on your prompt \u2014 treat it the same way.",
|
|
1861
1867
|
"",
|
|
1862
1868
|
"### When to call `graph_continue` \u2014 and when to skip",
|
|
1863
1869
|
"",
|
|
@@ -2126,7 +2132,7 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
2126
2132
|
if (verbose) log.info(` walked ${walked.length} files`);
|
|
2127
2133
|
const parsable = walked.filter((f) => PARSABLE_EXTS.has(f.ext));
|
|
2128
2134
|
const prevCache = await readParseCache(paths.parseCache);
|
|
2129
|
-
const { parsed, cache, reused, reparsed, parseErrors } = await incrementalParse(
|
|
2135
|
+
const { parsed, cache: cache2, reused, reparsed, parseErrors } = await incrementalParse(
|
|
2130
2136
|
parsable,
|
|
2131
2137
|
prevCache,
|
|
2132
2138
|
{ full: opts.full }
|
|
@@ -2140,7 +2146,7 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
2140
2146
|
const symbolIndex = buildSymbolIndex(graph);
|
|
2141
2147
|
await writeGraph(paths.infoGraph, graph);
|
|
2142
2148
|
await writeSymbolIndex(paths.symbolIndex, symbolIndex);
|
|
2143
|
-
await writeParseCache(paths.parseCache,
|
|
2149
|
+
await writeParseCache(paths.parseCache, cache2);
|
|
2144
2150
|
if (verbose) {
|
|
2145
2151
|
log.info(
|
|
2146
2152
|
` wrote ${paths.infoGraph} \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges`
|
|
@@ -2360,6 +2366,10 @@ function loadConfig() {
|
|
|
2360
2366
|
// the terminal bypass of the Moat can be measured. Never blocks. Opt out
|
|
2361
2367
|
// with SYN_NO_BASH_OBSERVE.
|
|
2362
2368
|
bashObserve: !process.env.SYN_NO_BASH_OBSERVE,
|
|
2369
|
+
// The Dispatcher: per-prompt routing hints (best-fit agent/skill/model).
|
|
2370
|
+
// Silent unless the top agent clears routeMinScore. SYN_NO_ROUTE disables.
|
|
2371
|
+
route: !process.env.SYN_NO_ROUTE,
|
|
2372
|
+
routeMinScore: num("SYN_ROUTE_MIN_SCORE", 3),
|
|
2363
2373
|
mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
|
|
2364
2374
|
dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
|
|
2365
2375
|
logLevel: str("SYN_LOG_LEVEL", "info"),
|
|
@@ -2368,8 +2378,8 @@ function loadConfig() {
|
|
|
2368
2378
|
}
|
|
2369
2379
|
|
|
2370
2380
|
// src/server/mcp.ts
|
|
2371
|
-
import { appendFile as
|
|
2372
|
-
import { dirname as
|
|
2381
|
+
import { appendFile as appendFile4, mkdir as mkdir9 } from "fs/promises";
|
|
2382
|
+
import { dirname as dirname11 } from "path";
|
|
2373
2383
|
|
|
2374
2384
|
// src/graph/rank.ts
|
|
2375
2385
|
var KW_BASE_WEIGHT = 2;
|
|
@@ -2769,7 +2779,8 @@ async function rememberEntry(paths, input) {
|
|
|
2769
2779
|
content: input.text,
|
|
2770
2780
|
tags: input.tags ?? [],
|
|
2771
2781
|
files: input.files ?? [],
|
|
2772
|
-
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2782
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2783
|
+
...input.anchors && input.anchors.length > 0 ? { anchors: input.anchors } : {}
|
|
2773
2784
|
};
|
|
2774
2785
|
await appendEntry(active.paths.contextStore, entry);
|
|
2775
2786
|
const entries = await readEntries(active.paths.contextStore);
|
|
@@ -2988,6 +2999,378 @@ async function pack(files, opts) {
|
|
|
2988
2999
|
};
|
|
2989
3000
|
}
|
|
2990
3001
|
|
|
3002
|
+
// src/dashboard/arsenal.ts
|
|
3003
|
+
import { readFile as readFile13, readdir as readdir2 } from "fs/promises";
|
|
3004
|
+
import { homedir } from "os";
|
|
3005
|
+
import { basename as basename3, dirname as dirname9, join as join7 } from "path";
|
|
3006
|
+
var DESC_MAX = 300;
|
|
3007
|
+
var TOOLS_MAX = 200;
|
|
3008
|
+
async function readText(path) {
|
|
3009
|
+
try {
|
|
3010
|
+
return await readFile13(path, "utf8");
|
|
3011
|
+
} catch {
|
|
3012
|
+
return null;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
async function readJson2(path) {
|
|
3016
|
+
const text = await readText(path);
|
|
3017
|
+
if (text === null) return null;
|
|
3018
|
+
try {
|
|
3019
|
+
return JSON.parse(text);
|
|
3020
|
+
} catch {
|
|
3021
|
+
return null;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
async function listNames(dir) {
|
|
3025
|
+
try {
|
|
3026
|
+
return await readdir2(dir);
|
|
3027
|
+
} catch {
|
|
3028
|
+
return [];
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
function clip(s, max) {
|
|
3032
|
+
const t = s.trim();
|
|
3033
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
3034
|
+
}
|
|
3035
|
+
function parseFrontmatter(md) {
|
|
3036
|
+
const m = md.match(/^?\s*---\r?\n([\s\S]*?)\r?\n---/);
|
|
3037
|
+
if (!m) return {};
|
|
3038
|
+
const lines = (m[1] ?? "").split(/\r?\n/);
|
|
3039
|
+
const out = {};
|
|
3040
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3041
|
+
const line = lines[i] ?? "";
|
|
3042
|
+
const kv = line.match(/^([A-Za-z][\w-]*):\s?(.*)$/);
|
|
3043
|
+
if (!kv) continue;
|
|
3044
|
+
const key = kv[1] ?? "";
|
|
3045
|
+
if (!key) continue;
|
|
3046
|
+
let val = kv[2] ?? "";
|
|
3047
|
+
if (val.startsWith('"') && !/[^\\]"\s*$/.test(val.slice(1)) || val === '"') {
|
|
3048
|
+
const buf = [val];
|
|
3049
|
+
while (i + 1 < lines.length && !/"\s*$/.test(buf[buf.length - 1] ?? "")) {
|
|
3050
|
+
i += 1;
|
|
3051
|
+
buf.push(lines[i] ?? "");
|
|
3052
|
+
}
|
|
3053
|
+
val = buf.join(" ");
|
|
3054
|
+
}
|
|
3055
|
+
val = val.trim().replace(/^["']|["']$/g, "").trim();
|
|
3056
|
+
out[key] = val;
|
|
3057
|
+
}
|
|
3058
|
+
return out;
|
|
3059
|
+
}
|
|
3060
|
+
function skillItem(fm, fallbackName, scope, source) {
|
|
3061
|
+
const meta = {};
|
|
3062
|
+
if (fm["argument-hint"]) meta.argument_hint = fm["argument-hint"];
|
|
3063
|
+
if (fm["user-invocable"]) meta.user_invocable = fm["user-invocable"];
|
|
3064
|
+
return {
|
|
3065
|
+
name: fm.name || fallbackName,
|
|
3066
|
+
description: clip(fm.description || "", DESC_MAX),
|
|
3067
|
+
scope,
|
|
3068
|
+
...source ? { source } : {},
|
|
3069
|
+
...Object.keys(meta).length ? { meta } : {}
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
function agentItem(fm, fallbackName, scope, source) {
|
|
3073
|
+
const meta = {};
|
|
3074
|
+
if (fm.tools) meta.tools = clip(fm.tools, TOOLS_MAX);
|
|
3075
|
+
if (fm.model) meta.model = fm.model;
|
|
3076
|
+
return {
|
|
3077
|
+
name: fm.name || fallbackName,
|
|
3078
|
+
description: clip(fm.description || "", DESC_MAX),
|
|
3079
|
+
scope,
|
|
3080
|
+
...source ? { source } : {},
|
|
3081
|
+
...Object.keys(meta).length ? { meta } : {}
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
async function scanSkillsDir(dir, scope, source, out) {
|
|
3085
|
+
for (const name of await listNames(dir)) {
|
|
3086
|
+
const md = await readText(join7(dir, name, "SKILL.md"));
|
|
3087
|
+
if (md === null) continue;
|
|
3088
|
+
out.push(skillItem(parseFrontmatter(md), name, scope, source));
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
async function scanAgentsDir(dir, scope, source, out) {
|
|
3092
|
+
for (const file of await listNames(dir)) {
|
|
3093
|
+
if (!file.endsWith(".md")) continue;
|
|
3094
|
+
const md = await readText(join7(dir, file));
|
|
3095
|
+
if (md === null) continue;
|
|
3096
|
+
out.push(agentItem(parseFrontmatter(md), basename3(file, ".md"), scope, source));
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
function mcpItemsFrom(json, scope, source) {
|
|
3100
|
+
if (!json || typeof json !== "object") return [];
|
|
3101
|
+
const record = json;
|
|
3102
|
+
const servers = record.mcpServers && typeof record.mcpServers === "object" ? record.mcpServers : record;
|
|
3103
|
+
const items = [];
|
|
3104
|
+
for (const [name, raw] of Object.entries(servers)) {
|
|
3105
|
+
if (!raw || typeof raw !== "object") continue;
|
|
3106
|
+
const cfg = raw;
|
|
3107
|
+
const type = typeof cfg.type === "string" ? cfg.type : cfg.command ? "stdio" : "http";
|
|
3108
|
+
const url = typeof cfg.url === "string" ? cfg.url.split("?")[0] : typeof cfg.command === "string" ? cfg.command : "";
|
|
3109
|
+
const meta = { type };
|
|
3110
|
+
if (url) meta.url = url;
|
|
3111
|
+
items.push({ name, description: "", scope, ...source ? { source } : {}, meta });
|
|
3112
|
+
}
|
|
3113
|
+
return items;
|
|
3114
|
+
}
|
|
3115
|
+
var SCOPE_ORDER = { project: 0, personal: 1, plugin: 2 };
|
|
3116
|
+
function sortItems(items) {
|
|
3117
|
+
return items.sort((a, b) => {
|
|
3118
|
+
if (a.scope !== b.scope) return SCOPE_ORDER[a.scope] - SCOPE_ORDER[b.scope];
|
|
3119
|
+
const sa = a.source ?? "";
|
|
3120
|
+
const sb = b.source ?? "";
|
|
3121
|
+
if (sa !== sb) return sa < sb ? -1 : 1;
|
|
3122
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
var cache = null;
|
|
3126
|
+
var CACHE_TTL_MS = 15e3;
|
|
3127
|
+
async function computeArsenal(projectRoot, homeDir = homedir()) {
|
|
3128
|
+
const key = `${projectRoot}\0${homeDir}`;
|
|
3129
|
+
const now = Date.now();
|
|
3130
|
+
if (cache && cache.key === key && now - cache.at < CACHE_TTL_MS) return cache.data;
|
|
3131
|
+
const homeClaude = join7(homeDir, ".claude");
|
|
3132
|
+
const projClaude = join7(projectRoot, ".claude");
|
|
3133
|
+
const skills = [];
|
|
3134
|
+
const agents = [];
|
|
3135
|
+
const mcp = [];
|
|
3136
|
+
await scanSkillsDir(join7(projClaude, "skills"), "project", void 0, skills);
|
|
3137
|
+
await scanSkillsDir(join7(homeClaude, "skills"), "personal", void 0, skills);
|
|
3138
|
+
await scanAgentsDir(join7(projClaude, "agents"), "project", void 0, agents);
|
|
3139
|
+
await scanAgentsDir(join7(homeClaude, "agents"), "personal", void 0, agents);
|
|
3140
|
+
mcp.push(...mcpItemsFrom(await readJson2(join7(projectRoot, ".mcp.json")), "project", void 0));
|
|
3141
|
+
mcp.push(
|
|
3142
|
+
...mcpItemsFrom(
|
|
3143
|
+
(await readJson2(join7(homeDir, ".claude.json")))?.mcpServers,
|
|
3144
|
+
"personal",
|
|
3145
|
+
void 0
|
|
3146
|
+
)
|
|
3147
|
+
);
|
|
3148
|
+
const installedRaw = await readJson2(
|
|
3149
|
+
join7(homeClaude, "plugins", "installed_plugins.json")
|
|
3150
|
+
);
|
|
3151
|
+
const pluginsMap = installedRaw?.plugins ?? installedRaw ?? {};
|
|
3152
|
+
const settings = await readJson2(
|
|
3153
|
+
join7(homeClaude, "settings.json")
|
|
3154
|
+
);
|
|
3155
|
+
const enabledMap = settings?.enabledPlugins ?? {};
|
|
3156
|
+
let pluginCount = 0;
|
|
3157
|
+
for (const [pluginKey, entries] of Object.entries(pluginsMap)) {
|
|
3158
|
+
const entry = Array.isArray(entries) ? entries[0] : void 0;
|
|
3159
|
+
if (!entry?.installPath) continue;
|
|
3160
|
+
pluginCount += 1;
|
|
3161
|
+
const pluginName = pluginKey.split("@")[0];
|
|
3162
|
+
const enabled = enabledMap[pluginKey] !== false;
|
|
3163
|
+
const root = entry.installPath;
|
|
3164
|
+
const manifest = await readJson2(
|
|
3165
|
+
join7(root, ".claude-plugin", "plugin.json")
|
|
3166
|
+
);
|
|
3167
|
+
const agentFiles = /* @__PURE__ */ new Set();
|
|
3168
|
+
for (const f of await listNames(join7(root, "agents"))) {
|
|
3169
|
+
if (f.endsWith(".md")) agentFiles.add(join7(root, "agents", f));
|
|
3170
|
+
}
|
|
3171
|
+
for (const rel of manifest?.agents ?? []) agentFiles.add(join7(root, rel));
|
|
3172
|
+
const pAgents = [];
|
|
3173
|
+
for (const file of agentFiles) {
|
|
3174
|
+
const md = await readText(file);
|
|
3175
|
+
if (md !== null)
|
|
3176
|
+
pAgents.push(agentItem(parseFrontmatter(md), basename3(file, ".md"), "plugin", pluginName));
|
|
3177
|
+
}
|
|
3178
|
+
const skillMds = /* @__PURE__ */ new Set();
|
|
3179
|
+
for (const name of await listNames(join7(root, "skills"))) {
|
|
3180
|
+
skillMds.add(join7(root, "skills", name, "SKILL.md"));
|
|
3181
|
+
}
|
|
3182
|
+
for (const rel of manifest?.skills ?? []) {
|
|
3183
|
+
skillMds.add(rel.endsWith(".md") ? join7(root, rel) : join7(root, rel, "SKILL.md"));
|
|
3184
|
+
}
|
|
3185
|
+
const pSkills = [];
|
|
3186
|
+
for (const md of skillMds) {
|
|
3187
|
+
const text = await readText(md);
|
|
3188
|
+
if (text !== null)
|
|
3189
|
+
pSkills.push(
|
|
3190
|
+
skillItem(parseFrontmatter(text), basename3(dirname9(md)), "plugin", pluginName)
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
const pMcp = mcpItemsFrom(await readJson2(join7(root, ".mcp.json")), "plugin", pluginName);
|
|
3194
|
+
for (const it of [...pSkills, ...pAgents, ...pMcp]) it.enabled = enabled;
|
|
3195
|
+
skills.push(...pSkills);
|
|
3196
|
+
agents.push(...pAgents);
|
|
3197
|
+
mcp.push(...pMcp);
|
|
3198
|
+
}
|
|
3199
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3200
|
+
const dedupedSkills = skills.filter((s) => {
|
|
3201
|
+
const k = `${s.scope}:${s.source ?? ""}:${s.name}`;
|
|
3202
|
+
if (seen.has(k)) return false;
|
|
3203
|
+
seen.add(k);
|
|
3204
|
+
return true;
|
|
3205
|
+
});
|
|
3206
|
+
const data = {
|
|
3207
|
+
skills: sortItems(dedupedSkills),
|
|
3208
|
+
agents: sortItems(agents),
|
|
3209
|
+
mcp: sortItems(mcp),
|
|
3210
|
+
counts: {
|
|
3211
|
+
skills: dedupedSkills.length,
|
|
3212
|
+
agents: agents.length,
|
|
3213
|
+
mcp: mcp.length,
|
|
3214
|
+
plugins: pluginCount
|
|
3215
|
+
},
|
|
3216
|
+
scanned_at: new Date(now).toISOString()
|
|
3217
|
+
};
|
|
3218
|
+
cache = { key, at: now, data };
|
|
3219
|
+
return data;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
// src/server/routes/route-match.ts
|
|
3223
|
+
var ROUTE_MIN_PROMPT_TOKENS = 3;
|
|
3224
|
+
var ROUTE_MAX_AGENTS = 3;
|
|
3225
|
+
var ROUTE_MAX_SKILLS = 2;
|
|
3226
|
+
var FINGERPRINT_BOOST = 2;
|
|
3227
|
+
var NAME_HIT_WEIGHT = 3;
|
|
3228
|
+
var EXT_KEYWORDS = {
|
|
3229
|
+
".svelte": ["svelte"],
|
|
3230
|
+
".vue": ["vue"],
|
|
3231
|
+
".tsx": ["react", "typescript"],
|
|
3232
|
+
".jsx": ["react"],
|
|
3233
|
+
".ts": ["typescript"],
|
|
3234
|
+
".py": ["python"],
|
|
3235
|
+
".cs": ["csharp", "dotnet"],
|
|
3236
|
+
".dart": ["flutter", "dart"],
|
|
3237
|
+
".rs": ["rust"],
|
|
3238
|
+
".go": ["golang", "go"],
|
|
3239
|
+
".java": ["java"],
|
|
3240
|
+
".kt": ["kotlin"],
|
|
3241
|
+
".php": ["php", "laravel"],
|
|
3242
|
+
".rb": ["ruby", "rails"],
|
|
3243
|
+
".hubl": ["hubspot", "hubl"],
|
|
3244
|
+
".html": ["html", "css"]
|
|
3245
|
+
};
|
|
3246
|
+
function fingerprintKeywords(extCounts) {
|
|
3247
|
+
const total = [...extCounts.values()].reduce((a, b) => a + b, 0);
|
|
3248
|
+
const out = /* @__PURE__ */ new Set();
|
|
3249
|
+
if (total === 0) return out;
|
|
3250
|
+
for (const [ext, count] of extCounts) {
|
|
3251
|
+
if (count / total < 0.2) continue;
|
|
3252
|
+
for (const kw of EXT_KEYWORDS[ext] ?? []) out.add(kw);
|
|
3253
|
+
}
|
|
3254
|
+
return out;
|
|
3255
|
+
}
|
|
3256
|
+
function scoreItem(item, qTokens, fingerprint) {
|
|
3257
|
+
const nameTokens = new Set(tokenizeQuery(item.name));
|
|
3258
|
+
const descTokens = new Set(tokenizeQuery(item.description));
|
|
3259
|
+
let score2 = 0;
|
|
3260
|
+
const hits = [];
|
|
3261
|
+
for (const t of qTokens) {
|
|
3262
|
+
if (nameTokens.has(t)) {
|
|
3263
|
+
score2 += NAME_HIT_WEIGHT;
|
|
3264
|
+
hits.push(t);
|
|
3265
|
+
} else if (descTokens.has(t)) {
|
|
3266
|
+
score2 += 1;
|
|
3267
|
+
hits.push(t);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
for (const kw of fingerprint) {
|
|
3271
|
+
if (nameTokens.has(kw) || descTokens.has(kw)) {
|
|
3272
|
+
score2 += FINGERPRINT_BOOST;
|
|
3273
|
+
break;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return { item, score: score2, hits };
|
|
3277
|
+
}
|
|
3278
|
+
function scoreArsenal(prompt, arsenal, extCounts, minScore) {
|
|
3279
|
+
const qTokens = new Set(tokenizeQuery(prompt));
|
|
3280
|
+
if (qTokens.size < ROUTE_MIN_PROMPT_TOKENS) {
|
|
3281
|
+
return { confident: false, agents: [], skills: [] };
|
|
3282
|
+
}
|
|
3283
|
+
const fingerprint = fingerprintKeywords(extCounts);
|
|
3284
|
+
const rank = (items) => items.filter((i) => i.enabled !== false).map((i) => scoreItem(i, qTokens, fingerprint)).filter((s) => s.score > 0).sort((a, b) => b.score - a.score || a.item.name.localeCompare(b.item.name));
|
|
3285
|
+
const agents = rank(arsenal.agents).slice(0, ROUTE_MAX_AGENTS).map((s) => ({
|
|
3286
|
+
name: s.item.name,
|
|
3287
|
+
score: s.score,
|
|
3288
|
+
reason: s.hits.length ? `matches: ${s.hits.slice(0, 4).join(", ")}` : "language fit",
|
|
3289
|
+
model: s.item.meta?.model?.trim() || "sonnet"
|
|
3290
|
+
}));
|
|
3291
|
+
const skills = rank(arsenal.skills).slice(0, ROUTE_MAX_SKILLS).map((s) => ({
|
|
3292
|
+
name: s.item.name,
|
|
3293
|
+
score: s.score,
|
|
3294
|
+
reason: s.hits.length ? `matches: ${s.hits.slice(0, 4).join(", ")}` : "language fit"
|
|
3295
|
+
}));
|
|
3296
|
+
const confident = agents.length > 0 && (agents[0]?.score ?? 0) >= minScore;
|
|
3297
|
+
return { confident, agents, skills };
|
|
3298
|
+
}
|
|
3299
|
+
function renderHint(match) {
|
|
3300
|
+
if (!match.confident || match.agents.length === 0) return "";
|
|
3301
|
+
const a = match.agents[0];
|
|
3302
|
+
const skill = match.skills[0] ? ` + skill '${match.skills[0].name}'` : "";
|
|
3303
|
+
return `[Synthra route] This task fits agent '${a.name}' (model: ${a.model})${skill}. Plan here first, then delegate execution to it - execution on cheaper models cuts cost ~5x.`;
|
|
3304
|
+
}
|
|
3305
|
+
function renderRouteReport(task, match) {
|
|
3306
|
+
const lines = [`# route_task: "${task}"`, ""];
|
|
3307
|
+
if (match.agents.length === 0 && match.skills.length === 0) {
|
|
3308
|
+
lines.push(
|
|
3309
|
+
"No strong match in the installed Arsenal \u2014 proceed yourself (browse the dashboard's Arsenal tab to see what's available)."
|
|
3310
|
+
);
|
|
3311
|
+
return lines.join("\n");
|
|
3312
|
+
}
|
|
3313
|
+
if (match.agents.length > 0) {
|
|
3314
|
+
lines.push(match.confident ? "Recommended agents:" : "Possible agents (low confidence):");
|
|
3315
|
+
for (const a of match.agents) {
|
|
3316
|
+
lines.push(`- \`${a.name}\` (model: ${a.model}) \u2014 score ${a.score}, ${a.reason}`);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
if (match.skills.length > 0) {
|
|
3320
|
+
lines.push("");
|
|
3321
|
+
lines.push("Relevant skills:");
|
|
3322
|
+
for (const s of match.skills) lines.push(`- \`${s.name}\` \u2014 score ${s.score}, ${s.reason}`);
|
|
3323
|
+
}
|
|
3324
|
+
lines.push("");
|
|
3325
|
+
lines.push(
|
|
3326
|
+
"_Model policy: plan on the primary model; delegate execution to a subagent on a cheaper model (sonnet \u2248 5\xD7 cheaper than opus)._"
|
|
3327
|
+
);
|
|
3328
|
+
return lines.join("\n");
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
// src/server/routes/route.ts
|
|
3332
|
+
import { appendFile as appendFile3, mkdir as mkdir8 } from "fs/promises";
|
|
3333
|
+
import { dirname as dirname10 } from "path";
|
|
3334
|
+
var defaultDeps = { arsenal: (root) => computeArsenal(root) };
|
|
3335
|
+
function graphExtCounts(ctx) {
|
|
3336
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3337
|
+
for (const n of ctx.graph.nodes) {
|
|
3338
|
+
if (n.kind !== "file") continue;
|
|
3339
|
+
const ext = n.ext;
|
|
3340
|
+
counts.set(ext, (counts.get(ext) ?? 0) + 1);
|
|
3341
|
+
}
|
|
3342
|
+
return counts;
|
|
3343
|
+
}
|
|
3344
|
+
var PROMPT_LOG_MAX = 200;
|
|
3345
|
+
async function logRoute(ctx, prompt, hint) {
|
|
3346
|
+
try {
|
|
3347
|
+
await mkdir8(dirname10(ctx.paths.routeLog), { recursive: true });
|
|
3348
|
+
const entry = {
|
|
3349
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3350
|
+
prompt: prompt.length > PROMPT_LOG_MAX ? `${prompt.slice(0, PROMPT_LOG_MAX)}\u2026` : prompt,
|
|
3351
|
+
routed: hint.length > 0,
|
|
3352
|
+
hint_chars: hint.length
|
|
3353
|
+
};
|
|
3354
|
+
await appendFile3(ctx.paths.routeLog, JSON.stringify(entry) + "\n", "utf8");
|
|
3355
|
+
} catch {
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
async function handleRoute(req, ctx, deps = defaultDeps) {
|
|
3359
|
+
const cfg = loadConfig();
|
|
3360
|
+
if (!cfg.route) return { hint: "" };
|
|
3361
|
+
const prompt = typeof req?.prompt === "string" ? req.prompt.trim() : "";
|
|
3362
|
+
if (!prompt) return { hint: "" };
|
|
3363
|
+
try {
|
|
3364
|
+
const arsenal = await deps.arsenal(ctx.paths.projectRoot);
|
|
3365
|
+
const match = scoreArsenal(prompt, arsenal, graphExtCounts(ctx), cfg.routeMinScore);
|
|
3366
|
+
const hint = renderHint(match);
|
|
3367
|
+
await logRoute(ctx, prompt, hint);
|
|
3368
|
+
return { hint };
|
|
3369
|
+
} catch {
|
|
3370
|
+
return { hint: "" };
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
2991
3374
|
// src/server/mcp.ts
|
|
2992
3375
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
2993
3376
|
var SERVER_INFO = { name: "synthra", version: "0.0.1" };
|
|
@@ -3071,7 +3454,7 @@ var TOOLS = [
|
|
|
3071
3454
|
files: {
|
|
3072
3455
|
type: "array",
|
|
3073
3456
|
items: { type: "string" },
|
|
3074
|
-
description: "Optional project-relative file paths this entry relates to."
|
|
3457
|
+
description: "Optional project-relative file paths this entry relates to. Linked files also anchor the entry: recall flags it 'possibly stale' if they change, and graph_read of those files surfaces it automatically."
|
|
3075
3458
|
}
|
|
3076
3459
|
},
|
|
3077
3460
|
required: ["text", "kind"]
|
|
@@ -3176,6 +3559,17 @@ var TOOLS = [
|
|
|
3176
3559
|
},
|
|
3177
3560
|
required: ["from", "to"]
|
|
3178
3561
|
}
|
|
3562
|
+
},
|
|
3563
|
+
{
|
|
3564
|
+
name: "route_task",
|
|
3565
|
+
description: "Ask Synthra which installed subagent/skill best fits a task, and which model to run it on. Scores the task against every installed agent and skill (plus the project's language fingerprint). Use BEFORE starting a multi-step implementation task: plan on the primary model, then delegate execution to the recommended agent on a cheaper model (sonnet \u2248 5\xD7 cheaper than opus).",
|
|
3566
|
+
inputSchema: {
|
|
3567
|
+
type: "object",
|
|
3568
|
+
properties: {
|
|
3569
|
+
task: { type: "string", description: "The task to route, in a sentence." }
|
|
3570
|
+
},
|
|
3571
|
+
required: ["task"]
|
|
3572
|
+
}
|
|
3179
3573
|
}
|
|
3180
3574
|
];
|
|
3181
3575
|
async function callTool(name, args, ctx) {
|
|
@@ -3204,6 +3598,8 @@ async function callTool(name, args, ctx) {
|
|
|
3204
3598
|
return duplicateSymbols(args, ctx);
|
|
3205
3599
|
case "call_path":
|
|
3206
3600
|
return callPath(args, ctx);
|
|
3601
|
+
case "route_task":
|
|
3602
|
+
return routeTask(args, ctx);
|
|
3207
3603
|
default:
|
|
3208
3604
|
return errorContent(`Unknown tool: ${name}`);
|
|
3209
3605
|
}
|
|
@@ -3360,6 +3756,17 @@ function testsCoveringLine(graph, filePaths) {
|
|
|
3360
3756
|
const omitted = tests.length - shown.length;
|
|
3361
3757
|
return `Tests covering the impact: ${shown.join(" \xB7 ")}${omitted > 0 ? ` \u2026+${omitted} more` : ""}`;
|
|
3362
3758
|
}
|
|
3759
|
+
async function routeTask(args, ctx) {
|
|
3760
|
+
const task = typeof args?.task === "string" ? args.task.trim() : "";
|
|
3761
|
+
if (!task) return errorContent("route_task: 'task' (string) is required");
|
|
3762
|
+
try {
|
|
3763
|
+
const arsenal = await computeArsenal(ctx.paths.projectRoot);
|
|
3764
|
+
const match = scoreArsenal(task, arsenal, graphExtCounts(ctx), loadConfig().routeMinScore);
|
|
3765
|
+
return textContent(renderRouteReport(task, match));
|
|
3766
|
+
} catch (err2) {
|
|
3767
|
+
return errorContent(`route_task: arsenal scan failed \u2014 ${err2.message}`);
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3363
3770
|
function resolveSymbolArg(ctx, arg) {
|
|
3364
3771
|
const a = arg.trim();
|
|
3365
3772
|
if (a.includes("::")) {
|
|
@@ -3613,9 +4020,28 @@ async function graphContinue(args, ctx) {
|
|
|
3613
4020
|
Files: ${retrieval.files.map((f) => f.path).join(", ") || "(none)"}
|
|
3614
4021
|
Reason: ${retrieval.reason}
|
|
3615
4022
|
`;
|
|
3616
|
-
|
|
4023
|
+
const remembered = matchRememberedFacts(query, retrieval.files, await safeRecallAll(ctx), ctx);
|
|
4024
|
+
return textContent(`${header}${remembered}
|
|
3617
4025
|
${packed.text}`);
|
|
3618
4026
|
}
|
|
4027
|
+
function matchRememberedFacts(query, retrievedFiles, entries, ctx) {
|
|
4028
|
+
if (entries.length === 0) return "";
|
|
4029
|
+
const qTokens = new Set(tokenizeQuery(query));
|
|
4030
|
+
const retrievedPaths = new Set(retrievedFiles.map((f) => f.path));
|
|
4031
|
+
const scored = entries.map((e) => {
|
|
4032
|
+
let score2 = 0;
|
|
4033
|
+
for (const t of tokenizeQuery(`${e.content} ${e.tags.join(" ")}`)) {
|
|
4034
|
+
if (qTokens.has(t)) score2 += 1;
|
|
4035
|
+
}
|
|
4036
|
+
if (e.files.some((f) => retrievedPaths.has(f)) || e.anchors?.some((a) => retrievedPaths.has(a.path))) {
|
|
4037
|
+
score2 += 2;
|
|
4038
|
+
}
|
|
4039
|
+
return { e, score: score2 };
|
|
4040
|
+
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score).slice(0, PACK_FACTS_MAX);
|
|
4041
|
+
if (scored.length === 0) return "";
|
|
4042
|
+
return `${scored.map((x) => `Remembered: ${factLine(x.e, ctx.graph).slice(2)}`).join("\n")}
|
|
4043
|
+
`;
|
|
4044
|
+
}
|
|
3619
4045
|
function resolveFileTarget(graph, filePath) {
|
|
3620
4046
|
const files = graph.nodes.filter((n) => n.kind === "file");
|
|
3621
4047
|
const exact = files.find((n) => n.path === filePath);
|
|
@@ -3678,10 +4104,10 @@ function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars
|
|
|
3678
4104
|
let cUsed = used + sep3 + head.length;
|
|
3679
4105
|
for (const c of callers.slice(0, DEPS_MAX_CALLERS)) {
|
|
3680
4106
|
const part = `${c.name} \u2192 ${c.file}`;
|
|
3681
|
-
const
|
|
3682
|
-
if (cUsed +
|
|
4107
|
+
const join8 = shown.length > 0 ? 3 : 0;
|
|
4108
|
+
if (cUsed + join8 + part.length > maxChars) break;
|
|
3683
4109
|
shown.push(part);
|
|
3684
|
-
cUsed +=
|
|
4110
|
+
cUsed += join8 + part.length;
|
|
3685
4111
|
}
|
|
3686
4112
|
if (lines.length > 0) lines.push("");
|
|
3687
4113
|
if (shown.length > 0) {
|
|
@@ -3708,6 +4134,43 @@ function buildTestsFooter(symbol, graph) {
|
|
|
3708
4134
|
if (isLikelyEntry(symbol.file)) return "";
|
|
3709
4135
|
return "Tests: none linked to this file.";
|
|
3710
4136
|
}
|
|
4137
|
+
var FACTS_MAX = 3;
|
|
4138
|
+
var FACTS_CONTENT_MAX = 160;
|
|
4139
|
+
var PACK_FACTS_MAX = 2;
|
|
4140
|
+
function staleAnchorPaths(entry, graph) {
|
|
4141
|
+
if (!entry.anchors || entry.anchors.length === 0) return [];
|
|
4142
|
+
const hashByPath = /* @__PURE__ */ new Map();
|
|
4143
|
+
for (const n of graph.nodes) if (n.kind === "file") hashByPath.set(n.path, n.file_hash);
|
|
4144
|
+
return entry.anchors.filter((a) => hashByPath.get(a.path) !== a.hash).map((a) => a.path);
|
|
4145
|
+
}
|
|
4146
|
+
function factLine(entry, graph) {
|
|
4147
|
+
const content = entry.content.length > FACTS_CONTENT_MAX ? `${entry.content.slice(0, FACTS_CONTENT_MAX - 1)}\u2026` : entry.content;
|
|
4148
|
+
const date = entry.date ? ` (${entry.date.slice(0, 10)})` : "";
|
|
4149
|
+
const stale = staleAnchorPaths(entry, graph);
|
|
4150
|
+
const staleNote = stale.length ? ` \u26A0 possibly stale \u2014 ${stale[0]} changed since stored` : "";
|
|
4151
|
+
return `- [${entry.type}] ${content}${date}${staleNote}`;
|
|
4152
|
+
}
|
|
4153
|
+
function entryLinksFile(entry, filePath) {
|
|
4154
|
+
if (entry.anchors?.some((a) => a.path === filePath)) return true;
|
|
4155
|
+
return entry.files.some((f) => f === filePath || filePath.endsWith(`/${f}`));
|
|
4156
|
+
}
|
|
4157
|
+
function buildFactsFooter(filePath, entries, graph) {
|
|
4158
|
+
const linked = entries.filter((e) => entryLinksFile(e, filePath));
|
|
4159
|
+
if (linked.length === 0) return "";
|
|
4160
|
+
const newestFirst = linked.slice().reverse();
|
|
4161
|
+
const shown = newestFirst.slice(0, FACTS_MAX);
|
|
4162
|
+
const omitted = newestFirst.length - shown.length;
|
|
4163
|
+
const lines = ["\u{1F4CC} Remembered for this file:", ...shown.map((e) => factLine(e, graph))];
|
|
4164
|
+
if (omitted > 0) lines.push(`\u2026+${omitted} more \u2014 mcp__synthra__context_recall()`);
|
|
4165
|
+
return lines.join("\n");
|
|
4166
|
+
}
|
|
4167
|
+
async function safeRecallAll(ctx) {
|
|
4168
|
+
try {
|
|
4169
|
+
return (await recallEntries(ctx.paths, {})).entries;
|
|
4170
|
+
} catch {
|
|
4171
|
+
return [];
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
3711
4174
|
async function graphRead(args, ctx) {
|
|
3712
4175
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
3713
4176
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
@@ -3726,10 +4189,15 @@ async function graphRead(args, ctx) {
|
|
|
3726
4189
|
}
|
|
3727
4190
|
const fileNode = resolved.node;
|
|
3728
4191
|
await logAccess(ctx, { ts: nowIso(), path: fileNode.path, source: "read" });
|
|
4192
|
+
const facts = buildFactsFooter(fileNode.path, await safeRecallAll(ctx), ctx.graph);
|
|
4193
|
+
const factsBlock = facts ? `
|
|
4194
|
+
|
|
4195
|
+
---
|
|
4196
|
+
${facts}` : "";
|
|
3729
4197
|
if (!symbolName) {
|
|
3730
4198
|
return textContent(`# ${fileNode.path}
|
|
3731
4199
|
|
|
3732
|
-
${fileNode.content}`);
|
|
4200
|
+
${fileNode.content}${factsBlock}`);
|
|
3733
4201
|
}
|
|
3734
4202
|
const cleanSym = symbolName.trim();
|
|
3735
4203
|
const symbol = ctx.graph.nodes.find(
|
|
@@ -3759,7 +4227,7 @@ ${tests}` : "";
|
|
|
3759
4227
|
return textContent(
|
|
3760
4228
|
`# ${fileNode.path}::${symbol.name} (L${symbol.start_line}-${symbol.end_line})
|
|
3761
4229
|
|
|
3762
|
-
${body}${depsBlock}${testsBlock}${editHint}`
|
|
4230
|
+
${body}${depsBlock}${testsBlock}${factsBlock}${editHint}`
|
|
3763
4231
|
);
|
|
3764
4232
|
}
|
|
3765
4233
|
var editedFiles = /* @__PURE__ */ new Set();
|
|
@@ -3794,16 +4262,26 @@ async function contextRemember(args, ctx) {
|
|
|
3794
4262
|
}
|
|
3795
4263
|
const tags = Array.isArray(args?.tags) ? args.tags.filter((t) => typeof t === "string") : [];
|
|
3796
4264
|
const files = Array.isArray(args?.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
4265
|
+
const anchors = [];
|
|
4266
|
+
for (const f of files) {
|
|
4267
|
+
const resolved = resolveFileTarget(ctx.graph, f);
|
|
4268
|
+
if ("node" in resolved) {
|
|
4269
|
+
anchors.push({ path: resolved.node.path, hash: resolved.node.file_hash });
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
3797
4272
|
const result = await rememberEntry(ctx.paths, {
|
|
3798
4273
|
text,
|
|
3799
4274
|
kind: kindRaw,
|
|
3800
4275
|
tags,
|
|
3801
|
-
files
|
|
4276
|
+
files,
|
|
4277
|
+
anchors
|
|
3802
4278
|
});
|
|
4279
|
+
const anchorNote = anchors.length ? `
|
|
4280
|
+
Anchored to ${anchors.length} file(s) \u2014 recall will flag this entry if they change.` : "";
|
|
3803
4281
|
return textContent(
|
|
3804
4282
|
`Remembered ${result.entry.type} on branch '${result.branch}'.
|
|
3805
4283
|
Stored: ${result.storePath}
|
|
3806
|
-
CONTEXT.md refreshed: ${result.contextMdPath}`
|
|
4284
|
+
CONTEXT.md refreshed: ${result.contextMdPath}${anchorNote}`
|
|
3807
4285
|
);
|
|
3808
4286
|
}
|
|
3809
4287
|
var DEFAULT_RECENT_WINDOW_MS = 60 * 60 * 1e3;
|
|
@@ -3838,15 +4316,17 @@ async function contextRecall(args, ctx) {
|
|
|
3838
4316
|
const lines = [`# Context entries \u2014 branch: ${result.branch}`, ""];
|
|
3839
4317
|
for (const e of result.entries) {
|
|
3840
4318
|
const tags = e.tags.length ? ` [${e.tags.join(", ")}]` : "";
|
|
3841
|
-
|
|
4319
|
+
const stale = staleAnchorPaths(e, ctx.graph);
|
|
4320
|
+
const staleNote = stale.length ? ` \u26A0 possibly stale \u2014 ${stale.join(", ")} changed since stored` : "";
|
|
4321
|
+
lines.push(`- **${e.type}**${tags} (${e.date}): ${e.content}${staleNote}`);
|
|
3842
4322
|
if (e.files.length) lines.push(` files: ${e.files.join(", ")}`);
|
|
3843
4323
|
}
|
|
3844
4324
|
return textContent(lines.join("\n"));
|
|
3845
4325
|
}
|
|
3846
4326
|
async function logToolCall(ctx, tool) {
|
|
3847
4327
|
try {
|
|
3848
|
-
await
|
|
3849
|
-
await
|
|
4328
|
+
await mkdir9(dirname11(ctx.paths.toolLog), { recursive: true });
|
|
4329
|
+
await appendFile4(
|
|
3850
4330
|
ctx.paths.toolLog,
|
|
3851
4331
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
|
|
3852
4332
|
"utf8"
|
|
@@ -4058,12 +4538,12 @@ async function getChangedLineRanges(projectRoot, sinceRef) {
|
|
|
4058
4538
|
}
|
|
4059
4539
|
|
|
4060
4540
|
// src/memory/session.ts
|
|
4061
|
-
import { mkdir as
|
|
4062
|
-
import { dirname as
|
|
4541
|
+
import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
|
|
4542
|
+
import { dirname as dirname12 } from "path";
|
|
4063
4543
|
var SESSION_SCHEMA_VERSION = 2;
|
|
4064
4544
|
async function readSession(path) {
|
|
4065
4545
|
try {
|
|
4066
|
-
const raw = await
|
|
4546
|
+
const raw = await readFile14(path, "utf8");
|
|
4067
4547
|
const parsed = JSON.parse(raw);
|
|
4068
4548
|
if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
|
|
4069
4549
|
return parsed;
|
|
@@ -4072,7 +4552,7 @@ async function readSession(path) {
|
|
|
4072
4552
|
}
|
|
4073
4553
|
}
|
|
4074
4554
|
async function writeSession(path, state) {
|
|
4075
|
-
await
|
|
4555
|
+
await mkdir10(dirname12(path), { recursive: true });
|
|
4076
4556
|
await writeFile8(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
4077
4557
|
}
|
|
4078
4558
|
|
|
@@ -4120,12 +4600,12 @@ async function handleContextUpdate(req, ctx) {
|
|
|
4120
4600
|
}
|
|
4121
4601
|
|
|
4122
4602
|
// src/server/routes/gate.ts
|
|
4123
|
-
import { appendFile as
|
|
4124
|
-
import { dirname as
|
|
4603
|
+
import { appendFile as appendFile6, mkdir as mkdir12 } from "fs/promises";
|
|
4604
|
+
import { dirname as dirname14 } from "path";
|
|
4125
4605
|
|
|
4126
4606
|
// src/server/routes/bash-observe.ts
|
|
4127
|
-
import { appendFile as
|
|
4128
|
-
import { dirname as
|
|
4607
|
+
import { appendFile as appendFile5, mkdir as mkdir11 } from "fs/promises";
|
|
4608
|
+
import { dirname as dirname13 } from "path";
|
|
4129
4609
|
|
|
4130
4610
|
// src/server/routes/query-heuristics.ts
|
|
4131
4611
|
function looksLikeNonSymbolQuery(pattern) {
|
|
@@ -4284,7 +4764,7 @@ var CMD_MAX = 300;
|
|
|
4284
4764
|
var trunc = (s, max) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
4285
4765
|
async function logObservation(ctx, exp, confidence, avoidable, command) {
|
|
4286
4766
|
try {
|
|
4287
|
-
await
|
|
4767
|
+
await mkdir11(dirname13(ctx.paths.bashLog), { recursive: true });
|
|
4288
4768
|
const entry = {
|
|
4289
4769
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4290
4770
|
kind: exp.kind,
|
|
@@ -4294,7 +4774,7 @@ async function logObservation(ctx, exp, confidence, avoidable, command) {
|
|
|
4294
4774
|
avoidable,
|
|
4295
4775
|
command: trunc(command, CMD_MAX)
|
|
4296
4776
|
};
|
|
4297
|
-
await
|
|
4777
|
+
await appendFile5(ctx.paths.bashLog, JSON.stringify(entry) + "\n", "utf8");
|
|
4298
4778
|
} catch {
|
|
4299
4779
|
}
|
|
4300
4780
|
}
|
|
@@ -4364,7 +4844,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
4364
4844
|
var LOG_REASON_MAX_CHARS = 240;
|
|
4365
4845
|
async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
|
|
4366
4846
|
try {
|
|
4367
|
-
await
|
|
4847
|
+
await mkdir12(dirname14(ctx.paths.gateLog), { recursive: true });
|
|
4368
4848
|
const entry = {
|
|
4369
4849
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4370
4850
|
tool: toolName,
|
|
@@ -4373,7 +4853,7 @@ async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
|
|
|
4373
4853
|
reason: reason && reason.length > LOG_REASON_MAX_CHARS ? `${reason.slice(0, LOG_REASON_MAX_CHARS)}\u2026` : reason,
|
|
4374
4854
|
...hintChars === void 0 ? {} : { hint_chars: hintChars }
|
|
4375
4855
|
};
|
|
4376
|
-
await
|
|
4856
|
+
await appendFile6(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
|
|
4377
4857
|
} catch {
|
|
4378
4858
|
}
|
|
4379
4859
|
}
|
|
@@ -4496,16 +4976,16 @@ async function handleGate(req, ctx) {
|
|
|
4496
4976
|
}
|
|
4497
4977
|
|
|
4498
4978
|
// src/server/routes/log.ts
|
|
4499
|
-
import { appendFile as
|
|
4500
|
-
import { dirname as
|
|
4979
|
+
import { appendFile as appendFile7, mkdir as mkdir13 } from "fs/promises";
|
|
4980
|
+
import { dirname as dirname15 } from "path";
|
|
4501
4981
|
async function handleLog(entry, ctx) {
|
|
4502
4982
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
4503
4983
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
4504
4984
|
}
|
|
4505
4985
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4506
4986
|
const record = { ...entry, written_at };
|
|
4507
|
-
await
|
|
4508
|
-
await
|
|
4987
|
+
await mkdir13(dirname15(ctx.paths.tokenLog), { recursive: true });
|
|
4988
|
+
await appendFile7(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
4509
4989
|
return { ok: true, written_at };
|
|
4510
4990
|
}
|
|
4511
4991
|
|
|
@@ -4699,6 +5179,10 @@ function buildApp(ctx, port) {
|
|
|
4699
5179
|
const body = await c.req.json().catch(() => ({}));
|
|
4700
5180
|
return c.json(await handleGate(body, ctx));
|
|
4701
5181
|
});
|
|
5182
|
+
app.post("/route", async (c) => {
|
|
5183
|
+
const body = await c.req.json().catch(() => ({}));
|
|
5184
|
+
return c.json(await handleRoute(body, ctx));
|
|
5185
|
+
});
|
|
4702
5186
|
app.get("/activity", async (c) => {
|
|
4703
5187
|
const sinceParam = c.req.query("since");
|
|
4704
5188
|
const sinceMs = sinceParam ? Number(sinceParam) : void 0;
|