@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.
@@ -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
- var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
3722
- var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
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 mkdir10(dirname11(ctx.paths.gateLog), { recursive: true });
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 appendFile4(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
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 appendFile5, mkdir as mkdir11 } from "fs/promises";
3910
- import { dirname as dirname12 } from "path";
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 mkdir11(dirname12(ctx.paths.tokenLog), { recursive: true });
3918
- await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
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