@jefuriiij/synthra 0.8.1 → 0.10.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 +46 -0
- package/dist/cli/index.js +350 -75
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +31 -7
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +314 -65
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1785,6 +1785,7 @@ function resolvePaths(projectRoot) {
|
|
|
1785
1785
|
activityLog: join5(graphDir, "activity.jsonl"),
|
|
1786
1786
|
tokenLog: join5(graphDir, "token_log.jsonl"),
|
|
1787
1787
|
gateLog: join5(graphDir, "gate_log.jsonl"),
|
|
1788
|
+
bashLog: join5(graphDir, "bash_log.jsonl"),
|
|
1788
1789
|
toolLog: join5(graphDir, "tool_log.jsonl"),
|
|
1789
1790
|
accessLog: join5(graphDir, "access_log.jsonl"),
|
|
1790
1791
|
learnStore: join5(graphDir, "learn_store.json"),
|
|
@@ -1987,9 +1988,13 @@ async function patchClaudeMd(path, projectName) {
|
|
|
1987
1988
|
return { created: true, updated: false, skipped: false };
|
|
1988
1989
|
}
|
|
1989
1990
|
const stripped = existing.replace(ANY_BLOCK_RE, "");
|
|
1990
|
-
const
|
|
1991
|
-
const desired =
|
|
1992
|
-
|
|
1991
|
+
const base = stripped.replace(/\s+$/, "");
|
|
1992
|
+
const desired = base.length ? `${base}
|
|
1993
|
+
|
|
1994
|
+
${block}
|
|
1995
|
+
` : `${block}
|
|
1996
|
+
`;
|
|
1997
|
+
if (desired === existing) {
|
|
1993
1998
|
return { created: false, updated: false, skipped: true };
|
|
1994
1999
|
}
|
|
1995
2000
|
await writeFile3(path, desired, "utf8");
|
|
@@ -2088,8 +2093,8 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
2088
2093
|
const start = Date.now();
|
|
2089
2094
|
const verbose = !opts.silent;
|
|
2090
2095
|
if (verbose) log.info(`scanning ${projectRoot}`);
|
|
2091
|
-
const boot = await bootstrap(paths);
|
|
2092
|
-
if (verbose) {
|
|
2096
|
+
const boot = opts.skipBootstrap ? null : await bootstrap(paths);
|
|
2097
|
+
if (verbose && boot) {
|
|
2093
2098
|
if (boot.graphCreated) log.info(" created .synthra-graph/");
|
|
2094
2099
|
if (boot.contextCreated) log.info(" created .synthra/");
|
|
2095
2100
|
if (boot.gitignoreUpdated) log.info(" updated .gitignore");
|
|
@@ -2314,6 +2319,40 @@ var LearnRuntime = class _LearnRuntime {
|
|
|
2314
2319
|
}
|
|
2315
2320
|
};
|
|
2316
2321
|
|
|
2322
|
+
// src/shared/config.ts
|
|
2323
|
+
function num(name, fallback) {
|
|
2324
|
+
const v = process.env[name];
|
|
2325
|
+
if (!v) return fallback;
|
|
2326
|
+
const n = Number(v);
|
|
2327
|
+
return Number.isFinite(n) ? n : fallback;
|
|
2328
|
+
}
|
|
2329
|
+
function str(name, fallback) {
|
|
2330
|
+
return process.env[name] ?? fallback;
|
|
2331
|
+
}
|
|
2332
|
+
function loadConfig() {
|
|
2333
|
+
return {
|
|
2334
|
+
hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
|
|
2335
|
+
gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
|
|
2336
|
+
readDepsMaxChars: num("SYN_READ_DEPS_CHARS", 900),
|
|
2337
|
+
turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
|
|
2338
|
+
fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
|
|
2339
|
+
retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
|
|
2340
|
+
// Auto-reindex: re-run the incremental scan + swap the in-memory graph this
|
|
2341
|
+
// many ms after the last source-file change, so graph reads never go stale
|
|
2342
|
+
// mid-session. Set SYN_NO_AUTOREINDEX to disable entirely.
|
|
2343
|
+
reindexDebounceMs: num("SYN_REINDEX_DEBOUNCE_MS", 1e3),
|
|
2344
|
+
autoReindex: !process.env.SYN_NO_AUTOREINDEX,
|
|
2345
|
+
// Observe-only: log codebase-exploration Bash commands (grep/cat/find …) so
|
|
2346
|
+
// the terminal bypass of the Moat can be measured. Never blocks. Opt out
|
|
2347
|
+
// with SYN_NO_BASH_OBSERVE.
|
|
2348
|
+
bashObserve: !process.env.SYN_NO_BASH_OBSERVE,
|
|
2349
|
+
mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
|
|
2350
|
+
dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
|
|
2351
|
+
logLevel: str("SYN_LOG_LEVEL", "info"),
|
|
2352
|
+
claudeBin: str("SYN_CLAUDE_BIN", "claude")
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2317
2356
|
// src/server/mcp.ts
|
|
2318
2357
|
import { appendFile as appendFile3, mkdir as mkdir8 } from "fs/promises";
|
|
2319
2358
|
import { dirname as dirname9 } from "path";
|
|
@@ -2935,31 +2974,6 @@ async function pack(files, opts) {
|
|
|
2935
2974
|
};
|
|
2936
2975
|
}
|
|
2937
2976
|
|
|
2938
|
-
// src/shared/config.ts
|
|
2939
|
-
function num(name, fallback) {
|
|
2940
|
-
const v = process.env[name];
|
|
2941
|
-
if (!v) return fallback;
|
|
2942
|
-
const n = Number(v);
|
|
2943
|
-
return Number.isFinite(n) ? n : fallback;
|
|
2944
|
-
}
|
|
2945
|
-
function str(name, fallback) {
|
|
2946
|
-
return process.env[name] ?? fallback;
|
|
2947
|
-
}
|
|
2948
|
-
function loadConfig() {
|
|
2949
|
-
return {
|
|
2950
|
-
hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
|
|
2951
|
-
gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
|
|
2952
|
-
readDepsMaxChars: num("SYN_READ_DEPS_CHARS", 900),
|
|
2953
|
-
turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
|
|
2954
|
-
fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
|
|
2955
|
-
retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
|
|
2956
|
-
mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
|
|
2957
|
-
dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
|
|
2958
|
-
logLevel: str("SYN_LOG_LEVEL", "info"),
|
|
2959
|
-
claudeBin: str("SYN_CLAUDE_BIN", "claude")
|
|
2960
|
-
};
|
|
2961
|
-
}
|
|
2962
|
-
|
|
2963
2977
|
// src/server/mcp.ts
|
|
2964
2978
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
2965
2979
|
var SERVER_INFO = { name: "synthra", version: "0.0.1" };
|
|
@@ -3552,6 +3566,61 @@ function isFree(port) {
|
|
|
3552
3566
|
});
|
|
3553
3567
|
}
|
|
3554
3568
|
|
|
3569
|
+
// src/server/reindex.ts
|
|
3570
|
+
async function rescanAndSwap(ctx, paths, label) {
|
|
3571
|
+
try {
|
|
3572
|
+
await scanProject(paths.projectRoot, { silent: true, skipBootstrap: true });
|
|
3573
|
+
const [graph, symbolIndex] = await Promise.all([
|
|
3574
|
+
readGraph(paths.infoGraph),
|
|
3575
|
+
readSymbolIndex(paths.symbolIndex)
|
|
3576
|
+
]);
|
|
3577
|
+
ctx.graph = graph;
|
|
3578
|
+
ctx.symbolIndex = symbolIndex;
|
|
3579
|
+
log.info(`reindexed (${label}) \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges.`);
|
|
3580
|
+
} catch (err2) {
|
|
3581
|
+
log.warn(`reindex failed (${label}): ${err2.message}`);
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
function createReindexer(ctx, paths, opts = {}) {
|
|
3585
|
+
const debounceMs = opts.debounceMs ?? 1e3;
|
|
3586
|
+
const rescan = opts.rescan ?? rescanAndSwap;
|
|
3587
|
+
let timer = null;
|
|
3588
|
+
let running = false;
|
|
3589
|
+
let pending = false;
|
|
3590
|
+
async function run() {
|
|
3591
|
+
if (running) {
|
|
3592
|
+
pending = true;
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
running = true;
|
|
3596
|
+
try {
|
|
3597
|
+
await rescan(ctx, paths, "edit");
|
|
3598
|
+
} finally {
|
|
3599
|
+
running = false;
|
|
3600
|
+
if (pending) {
|
|
3601
|
+
pending = false;
|
|
3602
|
+
void run();
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
return {
|
|
3607
|
+
schedule() {
|
|
3608
|
+
if (timer) clearTimeout(timer);
|
|
3609
|
+
timer = setTimeout(() => {
|
|
3610
|
+
timer = null;
|
|
3611
|
+
void run();
|
|
3612
|
+
}, debounceMs);
|
|
3613
|
+
timer.unref?.();
|
|
3614
|
+
},
|
|
3615
|
+
stop() {
|
|
3616
|
+
if (timer) {
|
|
3617
|
+
clearTimeout(timer);
|
|
3618
|
+
timer = null;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3555
3624
|
// src/server/routes/activity.ts
|
|
3556
3625
|
async function handleActivity(sinceMs, ctx) {
|
|
3557
3626
|
const events = ctx.activity.getEvents(sinceMs);
|
|
@@ -3652,22 +3721,14 @@ async function handleContextUpdate(req, ctx) {
|
|
|
3652
3721
|
}
|
|
3653
3722
|
|
|
3654
3723
|
// src/server/routes/gate.ts
|
|
3724
|
+
import { appendFile as appendFile5, mkdir as mkdir11 } from "fs/promises";
|
|
3725
|
+
import { dirname as dirname12 } from "path";
|
|
3726
|
+
|
|
3727
|
+
// src/server/routes/bash-observe.ts
|
|
3655
3728
|
import { appendFile as appendFile4, mkdir as mkdir10 } from "fs/promises";
|
|
3656
3729
|
import { dirname as dirname11 } from "path";
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
function extractQuery(toolName, input) {
|
|
3660
|
-
if (toolName === "Grep") {
|
|
3661
|
-
const pattern = typeof input.pattern === "string" ? input.pattern : "";
|
|
3662
|
-
const query = typeof input.query === "string" ? input.query : "";
|
|
3663
|
-
return (pattern || query).trim() || null;
|
|
3664
|
-
}
|
|
3665
|
-
if (toolName === "Glob") {
|
|
3666
|
-
const pattern = typeof input.pattern === "string" ? input.pattern : "";
|
|
3667
|
-
return pattern.replace(/[*?/\\.]+/g, " ").trim() || null;
|
|
3668
|
-
}
|
|
3669
|
-
return null;
|
|
3670
|
-
}
|
|
3730
|
+
|
|
3731
|
+
// src/server/routes/query-heuristics.ts
|
|
3671
3732
|
function looksLikeNonSymbolQuery(pattern) {
|
|
3672
3733
|
if (/<\/?[a-zA-Z]/.test(pattern)) return true;
|
|
3673
3734
|
if (/[a-zA-Z][\w-]*-[\w-]*\s*=/.test(pattern)) return true;
|
|
@@ -3683,6 +3744,195 @@ function looksLikeNonSymbolQuery(pattern) {
|
|
|
3683
3744
|
if (branches.length > 0 && branches.every(isKebab)) return true;
|
|
3684
3745
|
return false;
|
|
3685
3746
|
}
|
|
3747
|
+
|
|
3748
|
+
// src/server/routes/bash-observe.ts
|
|
3749
|
+
var SEARCH_TOOLS = /* @__PURE__ */ new Set(["grep", "egrep", "fgrep", "rg", "ripgrep", "ag", "ack"]);
|
|
3750
|
+
var READ_TOOLS = /* @__PURE__ */ new Set(["cat", "head", "tail", "less", "more", "bat", "tac"]);
|
|
3751
|
+
var LIST_TOOLS = /* @__PURE__ */ new Set(["find", "tree"]);
|
|
3752
|
+
var SOURCE_EXT = /\.(ts|tsx|js|jsx|cts|mts|cjs|mjs|py|pyi|svelte|vue|go|rs|java|kt|kts|php|rb|c|h|cpp|cc|cxx|hpp|cs|dart|json|md|css|html|hubl|sh|yml|yaml|toml)$/i;
|
|
3753
|
+
function tokenizeCommand(cmd) {
|
|
3754
|
+
const tokens = [];
|
|
3755
|
+
let cur = "";
|
|
3756
|
+
let quote = null;
|
|
3757
|
+
let hasContent2 = false;
|
|
3758
|
+
const flush = () => {
|
|
3759
|
+
if (hasContent2) tokens.push(cur);
|
|
3760
|
+
cur = "";
|
|
3761
|
+
hasContent2 = false;
|
|
3762
|
+
};
|
|
3763
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
3764
|
+
const ch = cmd[i];
|
|
3765
|
+
if (quote) {
|
|
3766
|
+
if (ch === quote) quote = null;
|
|
3767
|
+
else cur += ch;
|
|
3768
|
+
hasContent2 = true;
|
|
3769
|
+
continue;
|
|
3770
|
+
}
|
|
3771
|
+
if (ch === '"' || ch === "'") {
|
|
3772
|
+
quote = ch;
|
|
3773
|
+
hasContent2 = true;
|
|
3774
|
+
continue;
|
|
3775
|
+
}
|
|
3776
|
+
if (ch === "|" || ch === "&" || ch === ";") {
|
|
3777
|
+
flush();
|
|
3778
|
+
let op = ch;
|
|
3779
|
+
if ((ch === "|" || ch === "&") && cmd[i + 1] === ch) {
|
|
3780
|
+
op += ch;
|
|
3781
|
+
i++;
|
|
3782
|
+
}
|
|
3783
|
+
tokens.push(op);
|
|
3784
|
+
continue;
|
|
3785
|
+
}
|
|
3786
|
+
if (/\s/.test(ch)) {
|
|
3787
|
+
flush();
|
|
3788
|
+
continue;
|
|
3789
|
+
}
|
|
3790
|
+
cur += ch;
|
|
3791
|
+
hasContent2 = true;
|
|
3792
|
+
}
|
|
3793
|
+
flush();
|
|
3794
|
+
return tokens;
|
|
3795
|
+
}
|
|
3796
|
+
function isOperator(t) {
|
|
3797
|
+
return t === "|" || t === "||" || t === "&&" || t === ";";
|
|
3798
|
+
}
|
|
3799
|
+
function splitSegments(tokens) {
|
|
3800
|
+
const segs = [];
|
|
3801
|
+
let cur = [];
|
|
3802
|
+
for (const t of tokens) {
|
|
3803
|
+
if (isOperator(t)) {
|
|
3804
|
+
if (cur.length) segs.push(cur);
|
|
3805
|
+
cur = [];
|
|
3806
|
+
} else {
|
|
3807
|
+
cur.push(t);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
if (cur.length) segs.push(cur);
|
|
3811
|
+
return segs;
|
|
3812
|
+
}
|
|
3813
|
+
function commandTokens(seg) {
|
|
3814
|
+
let i = 0;
|
|
3815
|
+
while (i < seg.length) {
|
|
3816
|
+
const t = seg[i];
|
|
3817
|
+
if (t === "env") {
|
|
3818
|
+
i++;
|
|
3819
|
+
continue;
|
|
3820
|
+
}
|
|
3821
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) {
|
|
3822
|
+
i++;
|
|
3823
|
+
continue;
|
|
3824
|
+
}
|
|
3825
|
+
break;
|
|
3826
|
+
}
|
|
3827
|
+
return seg.slice(i);
|
|
3828
|
+
}
|
|
3829
|
+
function baseCmd(tok) {
|
|
3830
|
+
return (tok.split(/[\\/]/).pop() ?? tok).toLowerCase();
|
|
3831
|
+
}
|
|
3832
|
+
function classifySegment(seg) {
|
|
3833
|
+
const toks = commandTokens(seg);
|
|
3834
|
+
if (toks.length === 0) return null;
|
|
3835
|
+
const cmd = baseCmd(toks[0]);
|
|
3836
|
+
const rest = toks.slice(1);
|
|
3837
|
+
const flags = rest.filter((a) => a.startsWith("-"));
|
|
3838
|
+
const nonFlags = rest.filter((a) => !a.startsWith("-"));
|
|
3839
|
+
if (SEARCH_TOOLS.has(cmd)) {
|
|
3840
|
+
const pattern = nonFlags[0];
|
|
3841
|
+
if (!pattern) return null;
|
|
3842
|
+
const recursive = flags.some(
|
|
3843
|
+
(f) => f === "-r" || f === "-R" || f === "--recursive" || /^-[a-zA-Z]*[rR]$/.test(f)
|
|
3844
|
+
);
|
|
3845
|
+
const rgLike = cmd === "rg" || cmd === "ripgrep" || cmd === "ag" || cmd === "ack";
|
|
3846
|
+
const hasPathArg = nonFlags.length > 1;
|
|
3847
|
+
if (!rgLike && !recursive && !hasPathArg) return null;
|
|
3848
|
+
return { kind: "search", tool: cmd, query: pattern };
|
|
3849
|
+
}
|
|
3850
|
+
if (READ_TOOLS.has(cmd)) {
|
|
3851
|
+
const file = nonFlags.find((a) => SOURCE_EXT.test(a));
|
|
3852
|
+
if (!file) return null;
|
|
3853
|
+
return { kind: "read", tool: cmd, query: file };
|
|
3854
|
+
}
|
|
3855
|
+
if (LIST_TOOLS.has(cmd)) {
|
|
3856
|
+
const nameIdx = rest.findIndex((a) => a === "-name" || a === "-iname" || a === "-path");
|
|
3857
|
+
const target = nameIdx >= 0 ? rest[nameIdx + 1] ?? null : nonFlags[0] ?? ".";
|
|
3858
|
+
return { kind: "list", tool: cmd, query: target };
|
|
3859
|
+
}
|
|
3860
|
+
return null;
|
|
3861
|
+
}
|
|
3862
|
+
function classifyBashCommand(command) {
|
|
3863
|
+
if (!command || typeof command !== "string") return null;
|
|
3864
|
+
const tokens = tokenizeCommand(command);
|
|
3865
|
+
if (tokens.length === 0) return null;
|
|
3866
|
+
if (tokens.some((t) => !isOperator(t) && t.includes(">"))) return null;
|
|
3867
|
+
const found = splitSegments(tokens).map(classifySegment).filter((x) => x !== null);
|
|
3868
|
+
if (found.length === 0) return null;
|
|
3869
|
+
const prio = { search: 0, read: 1, list: 2 };
|
|
3870
|
+
found.sort((a, b) => prio[a.kind] - prio[b.kind]);
|
|
3871
|
+
return found[0];
|
|
3872
|
+
}
|
|
3873
|
+
function graphHasFile(ctx, target) {
|
|
3874
|
+
const base = target.split(/[\\/]/).pop() ?? target;
|
|
3875
|
+
for (const n of ctx.graph.nodes) {
|
|
3876
|
+
if (n.kind !== "file") continue;
|
|
3877
|
+
if (n.path === target || n.path.endsWith(`/${target}`) || n.path.split("/").pop() === base) {
|
|
3878
|
+
return true;
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
return false;
|
|
3882
|
+
}
|
|
3883
|
+
var Q_MAX = 200;
|
|
3884
|
+
var CMD_MAX = 300;
|
|
3885
|
+
var trunc = (s, max) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
3886
|
+
async function logObservation(ctx, exp, confidence, avoidable, command) {
|
|
3887
|
+
try {
|
|
3888
|
+
await mkdir10(dirname11(ctx.paths.bashLog), { recursive: true });
|
|
3889
|
+
const entry = {
|
|
3890
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3891
|
+
kind: exp.kind,
|
|
3892
|
+
tool: exp.tool,
|
|
3893
|
+
query: exp.query ? trunc(exp.query, Q_MAX) : null,
|
|
3894
|
+
confidence,
|
|
3895
|
+
avoidable,
|
|
3896
|
+
command: trunc(command, CMD_MAX)
|
|
3897
|
+
};
|
|
3898
|
+
await appendFile4(ctx.paths.bashLog, JSON.stringify(entry) + "\n", "utf8");
|
|
3899
|
+
} catch {
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
async function observeBash(input, ctx) {
|
|
3903
|
+
if (!loadConfig().bashObserve) return;
|
|
3904
|
+
const command = typeof input.command === "string" ? input.command : "";
|
|
3905
|
+
const exp = classifyBashCommand(command);
|
|
3906
|
+
if (!exp) return;
|
|
3907
|
+
let confidence = null;
|
|
3908
|
+
let avoidable = false;
|
|
3909
|
+
if (exp.kind === "search" && exp.query) {
|
|
3910
|
+
if (!looksLikeNonSymbolQuery(exp.query)) {
|
|
3911
|
+
const r = await retrieve(ctx.graph, exp.query);
|
|
3912
|
+
confidence = r.confidence;
|
|
3913
|
+
avoidable = r.confidence !== "low" && r.symbolMatched;
|
|
3914
|
+
}
|
|
3915
|
+
} else if (exp.kind === "read" && exp.query) {
|
|
3916
|
+
avoidable = graphHasFile(ctx, exp.query);
|
|
3917
|
+
}
|
|
3918
|
+
await logObservation(ctx, exp, confidence, avoidable, command);
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
// src/server/routes/gate.ts
|
|
3922
|
+
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
3923
|
+
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
3924
|
+
function extractQuery(toolName, input) {
|
|
3925
|
+
if (toolName === "Grep") {
|
|
3926
|
+
const pattern = typeof input.pattern === "string" ? input.pattern : "";
|
|
3927
|
+
const query = typeof input.query === "string" ? input.query : "";
|
|
3928
|
+
return (pattern || query).trim() || null;
|
|
3929
|
+
}
|
|
3930
|
+
if (toolName === "Glob") {
|
|
3931
|
+
const pattern = typeof input.pattern === "string" ? input.pattern : "";
|
|
3932
|
+
return pattern.replace(/[*?/\\.]+/g, " ").trim() || null;
|
|
3933
|
+
}
|
|
3934
|
+
return null;
|
|
3935
|
+
}
|
|
3686
3936
|
function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
3687
3937
|
if (recentPaths.length === 0) return [];
|
|
3688
3938
|
const recent = new Set(recentPaths);
|
|
@@ -3715,7 +3965,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
3715
3965
|
var LOG_REASON_MAX_CHARS = 240;
|
|
3716
3966
|
async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
|
|
3717
3967
|
try {
|
|
3718
|
-
await
|
|
3968
|
+
await mkdir11(dirname12(ctx.paths.gateLog), { recursive: true });
|
|
3719
3969
|
const entry = {
|
|
3720
3970
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3721
3971
|
tool: toolName,
|
|
@@ -3724,7 +3974,7 @@ async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
|
|
|
3724
3974
|
reason: reason && reason.length > LOG_REASON_MAX_CHARS ? `${reason.slice(0, LOG_REASON_MAX_CHARS)}\u2026` : reason,
|
|
3725
3975
|
...hintChars === void 0 ? {} : { hint_chars: hintChars }
|
|
3726
3976
|
};
|
|
3727
|
-
await
|
|
3977
|
+
await appendFile5(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
|
|
3728
3978
|
} catch {
|
|
3729
3979
|
}
|
|
3730
3980
|
}
|
|
@@ -3789,6 +4039,11 @@ async function handleGate(req, ctx) {
|
|
|
3789
4039
|
if (!req?.tool_name || typeof req.tool_name !== "string") {
|
|
3790
4040
|
return { decision: "allow", reason: "no tool_name" };
|
|
3791
4041
|
}
|
|
4042
|
+
if (req.tool_name === "Bash") {
|
|
4043
|
+
const input2 = req.tool_input && typeof req.tool_input === "object" ? req.tool_input : {};
|
|
4044
|
+
await observeBash(input2, ctx);
|
|
4045
|
+
return { decision: "allow" };
|
|
4046
|
+
}
|
|
3792
4047
|
if (!BLOCKABLE_TOOLS.has(req.tool_name)) {
|
|
3793
4048
|
return { decision: "allow" };
|
|
3794
4049
|
}
|
|
@@ -3842,16 +4097,16 @@ async function handleGate(req, ctx) {
|
|
|
3842
4097
|
}
|
|
3843
4098
|
|
|
3844
4099
|
// src/server/routes/log.ts
|
|
3845
|
-
import { appendFile as
|
|
3846
|
-
import { dirname as
|
|
4100
|
+
import { appendFile as appendFile6, mkdir as mkdir12 } from "fs/promises";
|
|
4101
|
+
import { dirname as dirname13 } from "path";
|
|
3847
4102
|
async function handleLog(entry, ctx) {
|
|
3848
4103
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
3849
4104
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
3850
4105
|
}
|
|
3851
4106
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3852
4107
|
const record = { ...entry, written_at };
|
|
3853
|
-
await
|
|
3854
|
-
await
|
|
4108
|
+
await mkdir12(dirname13(ctx.paths.tokenLog), { recursive: true });
|
|
4109
|
+
await appendFile6(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
3855
4110
|
return { ok: true, written_at };
|
|
3856
4111
|
}
|
|
3857
4112
|
|
|
@@ -4037,24 +4292,17 @@ async function startServer(paths, options = {}) {
|
|
|
4037
4292
|
const app = buildApp(ctx, port);
|
|
4038
4293
|
const nodeServer = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" });
|
|
4039
4294
|
await writeFile9(paths.mcpPort, String(port), "utf8");
|
|
4040
|
-
const
|
|
4295
|
+
const cfg = loadConfig();
|
|
4296
|
+
const reindexer = cfg.autoReindex ? createReindexer(ctx, paths, { debounceMs: cfg.reindexDebounceMs }) : null;
|
|
4297
|
+
const fileWatcher = createFileWatcher(paths.projectRoot, (e) => {
|
|
4298
|
+
void ctx.activity.add(e);
|
|
4299
|
+
reindexer?.schedule();
|
|
4300
|
+
});
|
|
4041
4301
|
const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
|
|
4042
4302
|
await ctx.activity.add(e);
|
|
4043
4303
|
if (e.kind === "branch-switch") {
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
log.info(`branch switched to '${to}' \u2014 rebuilding graph\u2026`);
|
|
4047
|
-
await scanProject(paths.projectRoot, { silent: true });
|
|
4048
|
-
const [g, idx] = await Promise.all([
|
|
4049
|
-
readGraph(paths.infoGraph),
|
|
4050
|
-
readSymbolIndex(paths.symbolIndex)
|
|
4051
|
-
]);
|
|
4052
|
-
ctx.graph = g;
|
|
4053
|
-
ctx.symbolIndex = idx;
|
|
4054
|
-
log.info(`graph rebuilt for '${to}' (${g.symbol_count} symbols).`);
|
|
4055
|
-
} catch (err2) {
|
|
4056
|
-
log.warn(`branch rescan failed: ${err2.message}`);
|
|
4057
|
-
}
|
|
4304
|
+
const to = e.details?.to ?? "unknown";
|
|
4305
|
+
await rescanAndSwap(ctx, paths, `branch ${to}`);
|
|
4058
4306
|
}
|
|
4059
4307
|
});
|
|
4060
4308
|
try {
|
|
@@ -4072,6 +4320,7 @@ async function startServer(paths, options = {}) {
|
|
|
4072
4320
|
port,
|
|
4073
4321
|
url,
|
|
4074
4322
|
async stop() {
|
|
4323
|
+
reindexer?.stop();
|
|
4075
4324
|
await fileWatcher.stop().catch(() => void 0);
|
|
4076
4325
|
await gitWatcher.stop().catch(() => void 0);
|
|
4077
4326
|
await ctx.learn?.flush().catch(() => void 0);
|