@jefuriiij/synthra 0.9.0 → 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 +18 -0
- package/dist/cli/index.js +247 -30
- 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 +211 -20
- 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"),
|
|
@@ -2341,6 +2342,10 @@ function loadConfig() {
|
|
|
2341
2342
|
// mid-session. Set SYN_NO_AUTOREINDEX to disable entirely.
|
|
2342
2343
|
reindexDebounceMs: num("SYN_REINDEX_DEBOUNCE_MS", 1e3),
|
|
2343
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,
|
|
2344
2349
|
mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
|
|
2345
2350
|
dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
|
|
2346
2351
|
logLevel: str("SYN_LOG_LEVEL", "info"),
|
|
@@ -3716,22 +3721,14 @@ async function handleContextUpdate(req, ctx) {
|
|
|
3716
3721
|
}
|
|
3717
3722
|
|
|
3718
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
|
|
3719
3728
|
import { appendFile as appendFile4, mkdir as mkdir10 } from "fs/promises";
|
|
3720
3729
|
import { dirname as dirname11 } from "path";
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
function extractQuery(toolName, input) {
|
|
3724
|
-
if (toolName === "Grep") {
|
|
3725
|
-
const pattern = typeof input.pattern === "string" ? input.pattern : "";
|
|
3726
|
-
const query = typeof input.query === "string" ? input.query : "";
|
|
3727
|
-
return (pattern || query).trim() || null;
|
|
3728
|
-
}
|
|
3729
|
-
if (toolName === "Glob") {
|
|
3730
|
-
const pattern = typeof input.pattern === "string" ? input.pattern : "";
|
|
3731
|
-
return pattern.replace(/[*?/\\.]+/g, " ").trim() || null;
|
|
3732
|
-
}
|
|
3733
|
-
return null;
|
|
3734
|
-
}
|
|
3730
|
+
|
|
3731
|
+
// src/server/routes/query-heuristics.ts
|
|
3735
3732
|
function looksLikeNonSymbolQuery(pattern) {
|
|
3736
3733
|
if (/<\/?[a-zA-Z]/.test(pattern)) return true;
|
|
3737
3734
|
if (/[a-zA-Z][\w-]*-[\w-]*\s*=/.test(pattern)) return true;
|
|
@@ -3747,6 +3744,195 @@ function looksLikeNonSymbolQuery(pattern) {
|
|
|
3747
3744
|
if (branches.length > 0 && branches.every(isKebab)) return true;
|
|
3748
3745
|
return false;
|
|
3749
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
|
+
}
|
|
3750
3936
|
function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
3751
3937
|
if (recentPaths.length === 0) return [];
|
|
3752
3938
|
const recent = new Set(recentPaths);
|
|
@@ -3779,7 +3965,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
3779
3965
|
var LOG_REASON_MAX_CHARS = 240;
|
|
3780
3966
|
async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
|
|
3781
3967
|
try {
|
|
3782
|
-
await
|
|
3968
|
+
await mkdir11(dirname12(ctx.paths.gateLog), { recursive: true });
|
|
3783
3969
|
const entry = {
|
|
3784
3970
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3785
3971
|
tool: toolName,
|
|
@@ -3788,7 +3974,7 @@ async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
|
|
|
3788
3974
|
reason: reason && reason.length > LOG_REASON_MAX_CHARS ? `${reason.slice(0, LOG_REASON_MAX_CHARS)}\u2026` : reason,
|
|
3789
3975
|
...hintChars === void 0 ? {} : { hint_chars: hintChars }
|
|
3790
3976
|
};
|
|
3791
|
-
await
|
|
3977
|
+
await appendFile5(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
|
|
3792
3978
|
} catch {
|
|
3793
3979
|
}
|
|
3794
3980
|
}
|
|
@@ -3853,6 +4039,11 @@ async function handleGate(req, ctx) {
|
|
|
3853
4039
|
if (!req?.tool_name || typeof req.tool_name !== "string") {
|
|
3854
4040
|
return { decision: "allow", reason: "no tool_name" };
|
|
3855
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
|
+
}
|
|
3856
4047
|
if (!BLOCKABLE_TOOLS.has(req.tool_name)) {
|
|
3857
4048
|
return { decision: "allow" };
|
|
3858
4049
|
}
|
|
@@ -3906,16 +4097,16 @@ async function handleGate(req, ctx) {
|
|
|
3906
4097
|
}
|
|
3907
4098
|
|
|
3908
4099
|
// src/server/routes/log.ts
|
|
3909
|
-
import { appendFile as
|
|
3910
|
-
import { dirname as
|
|
4100
|
+
import { appendFile as appendFile6, mkdir as mkdir12 } from "fs/promises";
|
|
4101
|
+
import { dirname as dirname13 } from "path";
|
|
3911
4102
|
async function handleLog(entry, ctx) {
|
|
3912
4103
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
3913
4104
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
3914
4105
|
}
|
|
3915
4106
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3916
4107
|
const record = { ...entry, written_at };
|
|
3917
|
-
await
|
|
3918
|
-
await
|
|
4108
|
+
await mkdir12(dirname13(ctx.paths.tokenLog), { recursive: true });
|
|
4109
|
+
await appendFile6(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
3919
4110
|
return { ok: true, written_at };
|
|
3920
4111
|
}
|
|
3921
4112
|
|