@jefuriiij/synthra 0.15.0 → 0.16.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 +33 -0
- package/README.md +226 -226
- package/dist/cli/index.js +258 -27
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +3 -2
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +443 -33
- 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;
|
|
@@ -2989,6 +2999,378 @@ async function pack(files, opts) {
|
|
|
2989
2999
|
};
|
|
2990
3000
|
}
|
|
2991
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
|
+
|
|
2992
3374
|
// src/server/mcp.ts
|
|
2993
3375
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
2994
3376
|
var SERVER_INFO = { name: "synthra", version: "0.0.1" };
|
|
@@ -3177,6 +3559,17 @@ var TOOLS = [
|
|
|
3177
3559
|
},
|
|
3178
3560
|
required: ["from", "to"]
|
|
3179
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
|
+
}
|
|
3180
3573
|
}
|
|
3181
3574
|
];
|
|
3182
3575
|
async function callTool(name, args, ctx) {
|
|
@@ -3205,6 +3598,8 @@ async function callTool(name, args, ctx) {
|
|
|
3205
3598
|
return duplicateSymbols(args, ctx);
|
|
3206
3599
|
case "call_path":
|
|
3207
3600
|
return callPath(args, ctx);
|
|
3601
|
+
case "route_task":
|
|
3602
|
+
return routeTask(args, ctx);
|
|
3208
3603
|
default:
|
|
3209
3604
|
return errorContent(`Unknown tool: ${name}`);
|
|
3210
3605
|
}
|
|
@@ -3361,6 +3756,17 @@ function testsCoveringLine(graph, filePaths) {
|
|
|
3361
3756
|
const omitted = tests.length - shown.length;
|
|
3362
3757
|
return `Tests covering the impact: ${shown.join(" \xB7 ")}${omitted > 0 ? ` \u2026+${omitted} more` : ""}`;
|
|
3363
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
|
+
}
|
|
3364
3770
|
function resolveSymbolArg(ctx, arg) {
|
|
3365
3771
|
const a = arg.trim();
|
|
3366
3772
|
if (a.includes("::")) {
|
|
@@ -3698,10 +4104,10 @@ function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars
|
|
|
3698
4104
|
let cUsed = used + sep3 + head.length;
|
|
3699
4105
|
for (const c of callers.slice(0, DEPS_MAX_CALLERS)) {
|
|
3700
4106
|
const part = `${c.name} \u2192 ${c.file}`;
|
|
3701
|
-
const
|
|
3702
|
-
if (cUsed +
|
|
4107
|
+
const join8 = shown.length > 0 ? 3 : 0;
|
|
4108
|
+
if (cUsed + join8 + part.length > maxChars) break;
|
|
3703
4109
|
shown.push(part);
|
|
3704
|
-
cUsed +=
|
|
4110
|
+
cUsed += join8 + part.length;
|
|
3705
4111
|
}
|
|
3706
4112
|
if (lines.length > 0) lines.push("");
|
|
3707
4113
|
if (shown.length > 0) {
|
|
@@ -3919,8 +4325,8 @@ async function contextRecall(args, ctx) {
|
|
|
3919
4325
|
}
|
|
3920
4326
|
async function logToolCall(ctx, tool) {
|
|
3921
4327
|
try {
|
|
3922
|
-
await
|
|
3923
|
-
await
|
|
4328
|
+
await mkdir9(dirname11(ctx.paths.toolLog), { recursive: true });
|
|
4329
|
+
await appendFile4(
|
|
3924
4330
|
ctx.paths.toolLog,
|
|
3925
4331
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
|
|
3926
4332
|
"utf8"
|
|
@@ -4132,12 +4538,12 @@ async function getChangedLineRanges(projectRoot, sinceRef) {
|
|
|
4132
4538
|
}
|
|
4133
4539
|
|
|
4134
4540
|
// src/memory/session.ts
|
|
4135
|
-
import { mkdir as
|
|
4136
|
-
import { dirname as
|
|
4541
|
+
import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
|
|
4542
|
+
import { dirname as dirname12 } from "path";
|
|
4137
4543
|
var SESSION_SCHEMA_VERSION = 2;
|
|
4138
4544
|
async function readSession(path) {
|
|
4139
4545
|
try {
|
|
4140
|
-
const raw = await
|
|
4546
|
+
const raw = await readFile14(path, "utf8");
|
|
4141
4547
|
const parsed = JSON.parse(raw);
|
|
4142
4548
|
if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
|
|
4143
4549
|
return parsed;
|
|
@@ -4146,7 +4552,7 @@ async function readSession(path) {
|
|
|
4146
4552
|
}
|
|
4147
4553
|
}
|
|
4148
4554
|
async function writeSession(path, state) {
|
|
4149
|
-
await
|
|
4555
|
+
await mkdir10(dirname12(path), { recursive: true });
|
|
4150
4556
|
await writeFile8(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
4151
4557
|
}
|
|
4152
4558
|
|
|
@@ -4194,12 +4600,12 @@ async function handleContextUpdate(req, ctx) {
|
|
|
4194
4600
|
}
|
|
4195
4601
|
|
|
4196
4602
|
// src/server/routes/gate.ts
|
|
4197
|
-
import { appendFile as
|
|
4198
|
-
import { dirname as
|
|
4603
|
+
import { appendFile as appendFile6, mkdir as mkdir12 } from "fs/promises";
|
|
4604
|
+
import { dirname as dirname14 } from "path";
|
|
4199
4605
|
|
|
4200
4606
|
// src/server/routes/bash-observe.ts
|
|
4201
|
-
import { appendFile as
|
|
4202
|
-
import { dirname as
|
|
4607
|
+
import { appendFile as appendFile5, mkdir as mkdir11 } from "fs/promises";
|
|
4608
|
+
import { dirname as dirname13 } from "path";
|
|
4203
4609
|
|
|
4204
4610
|
// src/server/routes/query-heuristics.ts
|
|
4205
4611
|
function looksLikeNonSymbolQuery(pattern) {
|
|
@@ -4358,7 +4764,7 @@ var CMD_MAX = 300;
|
|
|
4358
4764
|
var trunc = (s, max) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
4359
4765
|
async function logObservation(ctx, exp, confidence, avoidable, command) {
|
|
4360
4766
|
try {
|
|
4361
|
-
await
|
|
4767
|
+
await mkdir11(dirname13(ctx.paths.bashLog), { recursive: true });
|
|
4362
4768
|
const entry = {
|
|
4363
4769
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4364
4770
|
kind: exp.kind,
|
|
@@ -4368,7 +4774,7 @@ async function logObservation(ctx, exp, confidence, avoidable, command) {
|
|
|
4368
4774
|
avoidable,
|
|
4369
4775
|
command: trunc(command, CMD_MAX)
|
|
4370
4776
|
};
|
|
4371
|
-
await
|
|
4777
|
+
await appendFile5(ctx.paths.bashLog, JSON.stringify(entry) + "\n", "utf8");
|
|
4372
4778
|
} catch {
|
|
4373
4779
|
}
|
|
4374
4780
|
}
|
|
@@ -4438,7 +4844,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
4438
4844
|
var LOG_REASON_MAX_CHARS = 240;
|
|
4439
4845
|
async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
|
|
4440
4846
|
try {
|
|
4441
|
-
await
|
|
4847
|
+
await mkdir12(dirname14(ctx.paths.gateLog), { recursive: true });
|
|
4442
4848
|
const entry = {
|
|
4443
4849
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4444
4850
|
tool: toolName,
|
|
@@ -4447,7 +4853,7 @@ async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
|
|
|
4447
4853
|
reason: reason && reason.length > LOG_REASON_MAX_CHARS ? `${reason.slice(0, LOG_REASON_MAX_CHARS)}\u2026` : reason,
|
|
4448
4854
|
...hintChars === void 0 ? {} : { hint_chars: hintChars }
|
|
4449
4855
|
};
|
|
4450
|
-
await
|
|
4856
|
+
await appendFile6(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
|
|
4451
4857
|
} catch {
|
|
4452
4858
|
}
|
|
4453
4859
|
}
|
|
@@ -4570,16 +4976,16 @@ async function handleGate(req, ctx) {
|
|
|
4570
4976
|
}
|
|
4571
4977
|
|
|
4572
4978
|
// src/server/routes/log.ts
|
|
4573
|
-
import { appendFile as
|
|
4574
|
-
import { dirname as
|
|
4979
|
+
import { appendFile as appendFile7, mkdir as mkdir13 } from "fs/promises";
|
|
4980
|
+
import { dirname as dirname15 } from "path";
|
|
4575
4981
|
async function handleLog(entry, ctx) {
|
|
4576
4982
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
4577
4983
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
4578
4984
|
}
|
|
4579
4985
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4580
4986
|
const record = { ...entry, written_at };
|
|
4581
|
-
await
|
|
4582
|
-
await
|
|
4987
|
+
await mkdir13(dirname15(ctx.paths.tokenLog), { recursive: true });
|
|
4988
|
+
await appendFile7(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
4583
4989
|
return { ok: true, written_at };
|
|
4584
4990
|
}
|
|
4585
4991
|
|
|
@@ -4773,6 +5179,10 @@ function buildApp(ctx, port) {
|
|
|
4773
5179
|
const body = await c.req.json().catch(() => ({}));
|
|
4774
5180
|
return c.json(await handleGate(body, ctx));
|
|
4775
5181
|
});
|
|
5182
|
+
app.post("/route", async (c) => {
|
|
5183
|
+
const body = await c.req.json().catch(() => ({}));
|
|
5184
|
+
return c.json(await handleRoute(body, ctx));
|
|
5185
|
+
});
|
|
4776
5186
|
app.get("/activity", async (c) => {
|
|
4777
5187
|
const sinceParam = c.req.query("since");
|
|
4778
5188
|
const sinceMs = sinceParam ? Number(sinceParam) : void 0;
|