@polycode-projects/seonix 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/README.md CHANGED
@@ -46,13 +46,13 @@ network calls, **$0**.
46
46
 
47
47
  ```bash
48
48
  # 1. index your repo (deterministic, offline) — writes a machine-local .seonix/
49
- seonix cli index_repository '{"repo_path":"/abs/path/to/repo"}'
49
+ cd /path/to/repo && seonix cli index_repository
50
50
 
51
51
  # 2. self-locate the touched modules from the task text (no human-chosen target)
52
- seonix cli seonix_search '{"repo_path":"/abs/path/to/repo","query":"<your task>"}'
52
+ seonix cli seonix_search '{"query":"<your task>"}'
53
53
 
54
54
  # 3. render the bounded edit digest for those modules
55
- seonix cli digest '{"repo_path":"/abs/path/to/repo","modules":["<top hits>"]}' > digest.txt
55
+ seonix cli digest '{"modules":["<top hits>"]}' > digest.txt
56
56
 
57
57
  # 4. inject digest.txt once at session start, then run your coding agent
58
58
  claude --prompt "Use the seonix digest below to localise and make the change. $(cat digest.txt)"
@@ -60,6 +60,13 @@ claude --prompt "Use the seonix digest below to localise and make the change. $(
60
60
 
61
61
  Add `.seonix/` to your `.gitignore`; it is machine-local and rebuilt from source.
62
62
 
63
+ The JSON argument is optional on every `cli` command: an omitted `repo_path`
64
+ resolves to the nearest ancestor of the current directory (current directory
65
+ first) containing a `.seonix/`, so query commands work from any subdirectory of
66
+ an indexed repo. `index_repository` falls back to the current directory when
67
+ nothing is indexed yet; query commands print a one-line error instead. An
68
+ explicit `repo_path` always wins.
69
+
63
70
  ## Indexing multiple repositories
64
71
 
65
72
  `index_repository` takes one of three argument forms:
@@ -117,14 +124,17 @@ should start it with cwd = the indexed worktree.
117
124
  ### `cli index_repository` — build the graph
118
125
 
119
126
  ```bash
127
+ seonix cli index_repository # no arg: index the repo you are in
120
128
  seonix cli index_repository '{"repo_path":"/abs/repo"}'
121
129
  seonix cli index_repository '{"repo_paths":["/abs/repo1","/abs/repo2"],"out_root":"/abs/dir"}'
122
130
  seonix cli index_repository '{"multi_root":"/abs/estate"}'
123
131
  ```
124
132
 
125
133
  `repo_path` / `repo_paths`+`out_root` / `multi_root` are mutually exclusive — pass
126
- exactly one (or let `seonix.toml`'s `repositories` supply the set when none is
127
- given explicitly). Shared JSON options:
134
+ at most one. When none is given, `seonix.toml`'s `repositories` supplies the set
135
+ if present; otherwise the target defaults to the nearest ancestor of the current
136
+ directory carrying a `.seonix/` (re-index), else the current directory itself
137
+ (first index). Shared JSON options:
128
138
 
129
139
  - `ignores` (bool, default `true`) — honour the repo's `.seonixignore`. Pass
130
140
  `"ignores":false` to index everything regardless (the bench harness always
@@ -235,7 +245,9 @@ seonix cli seonix_context '{"symbol":"buildContextBundle","repo_path":"/abs/repo
235
245
  Any sub-command name that isn't one of the above routes straight to the MCP tool
236
246
  dispatcher, so any tool in the server's surface — cold or hot — is invokable from
237
247
  Bash with no MCP connection. `repo_path` in the JSON arg selects the target
238
- graph; every other key is passed through as the tool's own arguments.
248
+ graph; omitted, it defaults to the nearest ancestor of the current directory
249
+ containing a `.seonix/` (a clean one-line error when there is none). Every other
250
+ key is passed through as the tool's own arguments.
239
251
 
240
252
  ### `hook-augment` — PreToolUse Grep/Glob augmenter
241
253
 
@@ -316,7 +328,11 @@ seonix supplies the graph through tmct's provider seam.
316
328
  Two opt-in/opt-out environment knobs, orthogonal to everything above:
317
329
  `SEONIX_STORE=sqlite` turns on the resident `node:sqlite` store as a derived,
318
330
  rebuildable companion to `graph.json` (rebuild it explicitly with `cli
319
- store_rebuild`; unset, nothing sqlite loads). `SEONIX_GRAPH_FORMAT` controls the
331
+ store_rebuild`; unset, nothing sqlite loads). The variable must be **exported**
332
+ (`export SEONIX_STORE=sqlite`) — a plain shell assignment is invisible to the
333
+ `seonix` child process, and the JSON path runs instead. The index summary's
334
+ `store:` line states which mode actually ran (`store: sqlite (graph.db +
335
+ graph.json)` only when `graph.db` was built). `SEONIX_GRAPH_FORMAT` controls the
320
336
  graph's on-disk wire format — the interned v2 form is the shipped default;
321
337
  set `SEONIX_GRAPH_FORMAT=1` to opt back out to the legacy v1 form.
322
338
 
@@ -325,7 +341,7 @@ set `SEONIX_GRAPH_FORMAT=1` to opt back out to the legacy v1 form.
325
341
  An optional `seonix.toml` at a repo (or estate) root steers indexing and
326
342
  scoring. Absent file = shipped defaults, byte-for-byte today's behaviour.
327
343
  `seonix init` (or `seonix init --dotnet` for an estate) seeds a commented
328
- template — see `packages/seonix/templates/seonix.toml` and
344
+ template — see `templates/seonix.toml` and
329
345
  `templates/seonix-dotnet.toml` for the full annotated reference.
330
346
 
331
347
  - **`repositories`** — the repo set for an estate config: an inline array of
package/bin/cli.mjs CHANGED
@@ -6,6 +6,12 @@
6
6
  // seonix cli index_repository '{"repo_path":"<abs>"}' → deterministic index
7
7
  // (honours <repo>/.seonixignore — gitignore-subset patterns; pass
8
8
  // `"ignores":false` in the JSON arg to index everything regardless)
9
+ // The JSON arg is OPTIONAL for every `cli` command: an omitted repo_path defaults
10
+ // to the nearest ancestor of cwd (cwd first) containing a .seonix/ directory —
11
+ // so a bare `seonix cli index_repository` indexes the repo you are in, and query
12
+ // commands run from any subdirectory of an indexed repo. index_repository falls
13
+ // back to cwd when nothing is indexed yet (it creates the index); query commands
14
+ // error cleanly instead. An explicit repo_path always wins, unchanged.
9
15
  // seonix cli index_repository '{"repo_paths":["<abs1>","<abs2>",…],"out_root":"<abs-dir>"}' → index
10
16
  // n repos into ONE merged graph at <out_root>/.seonix/ (module ids prefixed with each repo's
11
17
  // directory basename; out_root defaults to the paths' deepest common ancestor directory).
@@ -39,7 +45,7 @@
39
45
  // graph artifact to <repo_path>/.seonix/graph.json; the server (started with
40
46
  // cwd = that repo) loads it by default. No flags, no config files.
41
47
 
42
- import { join } from "node:path";
48
+ import { dirname, join } from "node:path";
43
49
  import { existsSync } from "node:fs";
44
50
  import { readFile, writeFile } from "node:fs/promises";
45
51
  import { fileURLToPath } from "node:url";
@@ -52,7 +58,7 @@ import * as source from "../src/source.mjs";
52
58
  import { parseEntities, renderSearch, rankModulesByProximity, searchModulesRanked, selectRankedModules, DEFAULT_SCORE_GAP } from "../src/codegraph.mjs";
53
59
  import { loadTomlConfig, normalizeConfig, mergeEffective, CONFIG_FILE } from "../src/toml-config.mjs";
54
60
  import { compileGlobs } from "../src/walk.mjs";
55
- import { createTelemetry } from "../src/telemetry.mjs";
61
+ import { createTelemetry, truncateStr, looksLikeMiss } from "../src/telemetry.mjs";
56
62
 
57
63
  /** Build a config pointed at a specific repo's artifact (for `cli` sub-commands that
58
64
  * take a repo_path), or fall back to the cwd-derived default. */
@@ -67,6 +73,39 @@ function parsePayload(payload) {
67
73
  catch { return null; }
68
74
  }
69
75
 
76
+ /** Nearest ancestor of `startDir` (itself first) containing a .seonix/ directory, or null. */
77
+ function findIndexRoot(startDir = process.cwd()) {
78
+ let dir = startDir;
79
+ for (;;) {
80
+ if (existsSync(join(dir, ".seonix"))) return dir;
81
+ const parent = dirname(dir);
82
+ if (parent === dir) return null;
83
+ dir = parent;
84
+ }
85
+ }
86
+
87
+ /** Default an omitted repo_path for a `cli` command. Precedence:
88
+ * 1. explicit repo_path — always wins, args returned untouched (byte-identical to today);
89
+ * 2. SEONIX_GRAPH_FILE — also untouched (configFor's loadConfig fallback consumes it);
90
+ * 3. the nearest ancestor of cwd (cwd first) carrying a .seonix/ → written into args.repo_path;
91
+ * 4. nothing found: commands that READ an index (`mustExist`) get a one-line error + exit 2;
92
+ * index_repository falls back to cwd (it CREATES the index there).
93
+ * Mutating args means downstream configFor/tomlFor/telemetry all see the resolved repo. */
94
+ function defaultRepoPath(args, { mustExist = true } = {}) {
95
+ if (args.repo_path !== undefined) return args;
96
+ // The env override only steers commands that READ a graph (query commands); index_repository
97
+ // writes to <repo>/.seonix and never consumes SEONIX_GRAPH_FILE, so it still defaults.
98
+ if (mustExist && process.env.SEONIX_GRAPH_FILE && process.env.SEONIX_GRAPH_FILE.trim()) return args;
99
+ const root = findIndexRoot();
100
+ if (root) { args.repo_path = root; return args; }
101
+ if (mustExist) {
102
+ process.stderr.write("seonix: no .seonix index found here or above — run `seonix cli index_repository` first or pass repo_path\n");
103
+ process.exit(2);
104
+ }
105
+ args.repo_path = process.cwd();
106
+ return args;
107
+ }
108
+
70
109
  /** Load a repo's seonix.toml as a normalized config, or null. Returns null IMMEDIATELY when
71
110
  * `args.config === false` (the BENCH-IMMUNITY flag — an arm's index/locate/digest must NEVER read a
72
111
  * subject repo's seonix.toml) OR when no seonix.toml exists. So on the bench path, and with no
@@ -163,6 +202,7 @@ async function runDigest(args) {
163
202
  // stdout is the digest injected into the no-MCP arm, so telemetry must never touch it. The
164
203
  // seonix.toml [telemetry] toggle rides in via `toml`; SEONIX_TELEMETRY still wins both ways.
165
204
  const tel = createTelemetry({ env: process.env, config: configFor(repoPath), toml, surface: "cli" });
205
+ const t0 = Date.now();
166
206
  const tune = resolveDigestTune(args, toml);
167
207
  let modules = Array.isArray(args.modules) ? args.modules.slice(0, DIGEST_MODULE_CAP) : [];
168
208
  let autoSelected = null; // for the header, when `query` drove selection
@@ -236,10 +276,14 @@ async function runDigest(args) {
236
276
  process.stdout.write(out);
237
277
  // One telemetry line for the digest surface: the selected modules + the effective tier/topup the
238
278
  // header already carries, plus the injected body size. `tier` here MUST equal the header's tier=.
279
+ // (wh dogfood backlog #6): duration + a miss flag — a query-mode digest that selected zero
280
+ // modules is an unambiguous miss (autoSelected is only set in query mode).
239
281
  tel?.record({
240
282
  surface: "digest",
241
- query: args.query ? { raw: args.query } : undefined,
283
+ query: args.query ? { raw: truncateStr(args.query) } : undefined,
242
284
  response: { count: emitted, node_ids: modules, tier: effTier, topup },
285
+ perf: { ms_total: Date.now() - t0 },
286
+ quality: { miss: autoSelected ? autoSelected.length === 0 : emitted === 0 },
243
287
  cost: { returned_chars: out.length, returned_tokens_est: Math.ceil(out.length / 4) },
244
288
  });
245
289
  }
@@ -320,15 +364,21 @@ async function main() {
320
364
  }
321
365
  // seonix.toml: config dir = the explicit repo/estate path, else out_root, else cwd (where the
322
366
  // file sits). Skipped entirely on the bench path (config:false → tomlFor null → byte-identical).
323
- const toml = await tomlFor(args.repo_path || args.multi_root || args.out_root || process.cwd(), args);
367
+ let toml = await tomlFor(args.repo_path || args.multi_root || args.out_root || process.cwd(), args);
324
368
  warnUnwired(toml);
325
369
  // repositories: seonix.toml MAY supply the repo set when NO explicit path arg is given
326
370
  // (explicit repo_path/repo_paths/multi_root always win).
327
371
  const tomlRepos = (given.length === 0 && toml && Array.isArray(toml.repositories) && toml.repositories.length)
328
372
  ? toml.repositories : null;
329
373
  if (given.length === 0 && !tomlRepos) {
330
- process.stderr.write("seonix: index_repository requires repo_path, repo_paths or multi_root\n");
331
- process.exit(2);
374
+ // No path arg and no seonix.toml repo set: default to the nearest ancestor of cwd
375
+ // carrying a .seonix/ (re-index it), else cwd itself (first index — it creates .seonix/).
376
+ defaultRepoPath(args, { mustExist: false });
377
+ // The resolved root may carry its own seonix.toml (cwd can be a subdirectory) — re-read there.
378
+ if (args.repo_path && args.repo_path !== process.cwd()) {
379
+ toml = await tomlFor(args.repo_path, args);
380
+ warnUnwired(toml);
381
+ }
332
382
  }
333
383
  // A5 `history_depth`: unified cap over BOTH git-history passes. 0 = skip history entirely;
334
384
  // N>0 = `git log -n N`; absent = today's defaults. Precedence: explicit arg > seonix.toml
@@ -449,7 +499,7 @@ async function main() {
449
499
  process.stderr.write("seonix: digest expects a JSON arg, e.g. '{\"repo_path\":\"/abs\",\"modules\":[…]}'\n");
450
500
  process.exit(2);
451
501
  }
452
- await runDigest(args);
502
+ await runDigest(defaultRepoPath(args));
453
503
  return;
454
504
  }
455
505
 
@@ -463,6 +513,24 @@ async function main() {
463
513
  process.stderr.write("seonix: seonix_locate expects a JSON arg, e.g. '{\"query\":\"…\",\"repo_path\":\"/abs\"}'\n");
464
514
  process.exit(2);
465
515
  }
516
+ // Tool honesty (wh dogfood T7): seonix_locate used to print NOTHING and exit 0 for
517
+ // any input without a query ({}, {"symbol":…}, {"bogus":1}) — an empty ranking is
518
+ // indistinguishable from success in a tool loop. Bad/missing args are now a one-line
519
+ // error naming the valid keys + a nonzero exit, like the generic dispatchTool route.
520
+ const LOCATE_KEYS = [
521
+ "query", "raw_query", "repo_path", "config", "limit", "spiral", "spiral_depth",
522
+ "demote_nonprod", "call_adjacency", "impl_of_interface", "beam_search", "beam_width", "literal_mention",
523
+ ];
524
+ const unknownKeys = Object.keys(args).filter((k) => !LOCATE_KEYS.includes(k));
525
+ if (unknownKeys.length) {
526
+ process.stderr.write(`seonix: unknown argument(s) for seonix_locate: ${unknownKeys.join(", ")}. Valid: ${LOCATE_KEYS.join(", ")}.\n`);
527
+ process.exit(2);
528
+ }
529
+ if (!String(args.query || "").trim()) {
530
+ process.stderr.write("seonix: seonix_locate requires a non-empty \"query\" (free text to rank modules against). For a single symbol use seonix_search or seonix_describe.\n");
531
+ process.exit(2);
532
+ }
533
+ defaultRepoPath(args);
466
534
  const config = configFor(args.repo_path);
467
535
  // seonix.toml [tune] defaults the locate levers (arg > seonix.toml > default). Skipped on the
468
536
  // bench path (config:false → tomlFor null) and when no seonix.toml exists → byte-identical.
@@ -471,6 +539,7 @@ async function main() {
471
539
  // Opt-in telemetry (default OFF → null → no-op, no file). stdout is the ranked list the rig
472
540
  // parses, so telemetry is LOG FILE ONLY. seonix.toml [telemetry] rides in via `toml`.
473
541
  const tel = createTelemetry({ env: process.env, config, toml, surface: "cli" });
542
+ const t0 = Date.now();
474
543
  const tune = resolveLocateTune(args, toml);
475
544
  try {
476
545
  const graph = parseEntities(await source.fetchEntities(config));
@@ -510,15 +579,18 @@ async function main() {
510
579
  : {}),
511
580
  });
512
581
  process.stdout.write(ranked.map((r) => `${r.path}\t${r.score}`).join("\n") + "\n");
513
- // Telemetry for the locate surface: the ranked count + the top few paths/scores.
582
+ // Telemetry for the locate surface: the ranked count + the top few paths/scores, plus
583
+ // (wh dogfood backlog #6) duration + a miss flag (an empty ranking is an unambiguous miss).
514
584
  tel?.record({
515
585
  surface: "locate",
516
- query: { raw: String(args.query || "") },
586
+ query: { raw: truncateStr(String(args.query || "")) },
517
587
  response: {
518
588
  count: ranked.length,
519
589
  node_ids: ranked.slice(0, 3).map((r) => r.path),
520
590
  scores: ranked.slice(0, 3).map((r) => r.score),
521
591
  },
592
+ perf: { ms_total: Date.now() - t0 },
593
+ quality: { miss: ranked.length === 0 },
522
594
  });
523
595
  } catch (e) {
524
596
  process.stderr.write(`seonix: ${e?.message || e}\n`);
@@ -539,6 +611,7 @@ async function main() {
539
611
  process.stderr.write("seonix: browser_link expects a JSON arg, e.g. '{\"query\":\"classes that change with render\"}'\n");
540
612
  process.exit(2);
541
613
  }
614
+ defaultRepoPath(args);
542
615
  const config = configFor(args.repo_path);
543
616
  const { buildTemporalGraph, gitCommitOrder } = await import("../src/browser.mjs");
544
617
  const { nlToQuery, validateLink, encodeViewState, VIEW_DEFAULTS } = await import("../src/temporal.mjs");
@@ -578,6 +651,7 @@ async function main() {
578
651
  process.stderr.write("seonix: store_rebuild expects a JSON arg, e.g. '{\"repo_path\":\"/abs\"}'\n");
579
652
  process.exit(2);
580
653
  }
654
+ defaultRepoPath(args);
581
655
  const { graphFile } = configFor(args.repo_path);
582
656
  const { readFile, stat } = await import("node:fs/promises");
583
657
  const { writeStore, storeFileFor, verifyStore } = await import("../src/store.mjs");
@@ -612,17 +686,23 @@ async function main() {
612
686
  process.stderr.write(`seonix: ${sub} expects a JSON arg, e.g. '{"symbol":"<name>"}'\n`);
613
687
  process.exit(2);
614
688
  }
689
+ defaultRepoPath(args);
615
690
  const config = configFor(args.repo_path);
616
691
  // seonix.toml [telemetry] toggle for the generic tool surface (byte-identical with no toml:
617
692
  // tomlFor short-circuits to null on the bench path and when no seonix.toml exists).
618
693
  const toml = await tomlFor(args.repo_path, args);
619
694
  const tel = createTelemetry({ env: process.env, config, toml, surface: "cli" });
695
+ const t0 = Date.now();
620
696
  try {
621
697
  const text = await dispatchTool(sub, args, { config });
622
698
  process.stdout.write(text + "\n");
623
- // Telemetry for the generic tool surface: which tool + how many chars it returned.
699
+ // Telemetry for the generic tool surface: which tool + how many chars it returned, plus
700
+ // (wh dogfood backlog #6) the args text/duration/hit-miss the log previously omitted.
624
701
  tel?.record({
625
702
  tool: sub,
703
+ query: { raw: truncateStr(JSON.stringify(args)) },
704
+ perf: { ms_total: Date.now() - t0 },
705
+ quality: { miss: looksLikeMiss(text) },
626
706
  cost: { returned_chars: text.length, returned_tokens_est: Math.ceil(text.length / 4) },
627
707
  });
628
708
  } catch (e) {
@@ -632,7 +712,8 @@ async function main() {
632
712
  return;
633
713
  }
634
714
 
635
- process.stderr.write("seonix: `cli` needs a sub-command (index_repository | digest | <toolName>)\n");
715
+ process.stderr.write("seonix: `cli` needs a sub-command (index_repository | digest | <toolName>). " +
716
+ "The JSON arg is optional — an omitted repo_path defaults to the nearest ancestor of cwd with a .seonix/ index.\n");
636
717
  process.exit(2);
637
718
  }
638
719
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/seonix",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SEONIX — a deterministic, offline, $0 code-graph tool that makes a cheap coding agent edit like an expensive one. Indexes a repo with Python ast + git (zero model calls) and renders a bounded edit-digest.",
@@ -9,8 +9,7 @@
9
9
  "homepage": "https://seonix.polycode.co.uk",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://gitlab.com/polycode-projects/seonix.git",
13
- "directory": "packages/seonix"
12
+ "url": "git+https://gitlab.com/polycode-projects/seonix.git"
14
13
  },
15
14
  "bugs": {
16
15
  "url": "https://gitlab.com/polycode-projects/seonix/-/issues"
@@ -42,9 +41,51 @@
42
41
  "publishConfig": {
43
42
  "access": "public"
44
43
  },
44
+ "workspaces": [
45
+ "infra"
46
+ ],
47
+ "scripts": {
48
+ "setup": "node scripts/setup.mjs",
49
+ "bench": "node bench/run.mjs",
50
+ "bench:quick": "node bench/run.mjs --instances 1 --runs 1",
51
+ "join:telemetry": "node scripts/join-telemetry.mjs",
52
+ "bench:lite": "node bench/run.mjs --suite lite",
53
+ "bench:seonix": "node bench/run.mjs --suite django-lh --arms seonix,seonix-min --runs 5",
54
+ "bench:baseline": "node scripts/use-baseline.mjs",
55
+ "proof": "node scripts/proof-demo.mjs",
56
+ "smoke": "node bench/smoke.mjs --suite django-lh",
57
+ "report": "node bench/report.mjs",
58
+ "all": "npm run setup && npm run bench && npm run report",
59
+ "clean": "node scripts/clean.mjs",
60
+ "verify:counts": "node scripts/verify-counts.mjs",
61
+ "verify:sanity": "node scripts/verify-sanity.mjs",
62
+ "verify:index": "node scripts/verify-index.mjs",
63
+ "verify:query": "node scripts/verify-query.mjs",
64
+ "verify:review": "node scripts/verify-review.mjs",
65
+ "verify:loop": "node scripts/verify-loop.mjs",
66
+ "refs": "node scripts/fetch-references.mjs",
67
+ "refs:embeddings": "node scripts/fetch-embeddings.mjs",
68
+ "audit": "npm audit --audit-level=high",
69
+ "audit:fix": "npm audit fix",
70
+ "test:root": "node --test \"test/**/*.test.mjs\"",
71
+ "test": "npm run test:root && npm run chrono:test",
72
+ "chrono:build": "node chronograph/build.mjs",
73
+ "chrono:serve": "node chronograph/serve.mjs",
74
+ "chrono:test": "node --test \"chronograph/test/**/*.test.mjs\"",
75
+ "test:all": "npm run test",
76
+ "seonix": "node bin/cli.mjs",
77
+ "seonix:viz": "node bin/cli.mjs viz --",
78
+ "e2e:install": "npx playwright install chromium --with-deps",
79
+ "e2e": "playwright test",
80
+ "e2e:site": "playwright test site.behaviour.test.js",
81
+ "e2e:prod": "SEONIX_E2E_BASE_URL=https://seonix.polycode.co.uk playwright test site.behaviour.test.js",
82
+ "site:prompts": "node scripts/site-prompts.mjs",
83
+ "build:ask-bundle": "node scripts/build-ask-bundle.mjs",
84
+ "site:gen": "npm run build:ask-bundle && node bin/cli.mjs viz --focus buildContextBundle --depth 2 --repo-url https://gitlab.com/polycode-projects/seonix --site-nav --out site/seonix-graph.html --data-out site/seonix-graph-data.json --browser-out site/code-browser.html --browser-data-out site/code-browser-data.json --timeline-out site/timeline.html && npm run site:prompts"
85
+ },
45
86
  "dependencies": {
46
87
  "@modelcontextprotocol/sdk": "^1.29.0",
47
- "@polycode-projects/the-mechanical-code-talker": "^0.8.0",
88
+ "@polycode-projects/the-mechanical-code-talker": "^0.8.2",
48
89
  "cytoscape": "^3.30.0",
49
90
  "smol-toml": "^1.7.0",
50
91
  "tree-sitter": "^0.21.1",
@@ -52,8 +93,8 @@
52
93
  "tree-sitter-java": "^0.21.0",
53
94
  "typescript": "~5.6.2"
54
95
  },
55
- "scripts": {
56
- "test": "node --test \"test/**/*.test.mjs\"",
57
- "viz": "node bin/cli.mjs viz"
96
+ "devDependencies": {
97
+ "@playwright/test": "^1.56.0",
98
+ "esbuild": "^0.28.1"
58
99
  }
59
100
  }