pi-supernova 0.0.8 → 0.0.15
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 +85 -2
- package/README.md +36 -12
- package/bottleneck.js +1 -1
- package/catalog.js +20 -15
- package/check.js +166 -0
- package/config.default.json +1 -0
- package/config.js +1 -1
- package/decode.js +0 -10
- package/evidence.js +429 -0
- package/fuzzy.js +182 -0
- package/guest-worker.js +14 -11
- package/host-bridge.js +184 -74
- package/index.js +65 -42
- package/ledger.js +178 -0
- package/omp-frame.js +7 -22
- package/outline.js +80 -0
- package/package.json +8 -1
- package/render-measure.js +2 -30
- package/render.js +46 -150
- package/repo-index.js +273 -0
- package/runtime.js +3 -20
- package/search.js +141 -0
- package/snap.js +86 -108
- package/surface.js +24 -12
- package/vfs.js +7 -6
- package/workspace.js +33 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.0.15] - 2026-09-04
|
|
6
|
+
|
|
7
|
+
### Removed
|
|
8
|
+
|
|
9
|
+
- Dead code (−638 lines net): the static `extractOperationsFromCode` source-regex preview (the live trace superseded it), `SafeText`/`fitOutputLines`/`wrapPlainToWidth` (the call slot is always empty), `shutdownGuestWorkers`, `decode.js` helpers nobody imported, `vfs.clear`, the `tools` alias for `nova`, unused bridge methods (`hasExecutor`, `clearVfsCache`, `isMutating`), `test/verify-width-crash.mjs`, and unused status-header knobs (`spinnerFrame`, `iconOverride`).
|
|
10
|
+
- `edit` no longer accepts a unified diff (`patch`/`oldText` starting with `@@`); `apply_patch` / `patch()` is the one way to apply a diff.
|
|
11
|
+
- `nova.surface`/`nova.snap` host-side unwrapping and their dedicated RPC methods: both route through `nova.call` and the guest unwraps once.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- One `relativeSlash`, one `isTestPath`, one declaration-span computation (`WorkspaceIndex.spansOf`, cached per file) instead of three copies each; search adapters (fuzzy find, grep, glob listing) live in `search.js`; internal-only names are no longer exported.
|
|
16
|
+
- Render computes the card body once per width (was twice); fuzzy find stats only matched paths (was every file); results shorter than a collapsible run skip ledger hashing.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `nova.speculate` end-to-end (an end-to-end test now covers rollback and commit through the worker RPC).
|
|
21
|
+
|
|
22
|
+
## [0.0.14] - 2026-09-04
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Seen-ledger.** The model's context window is treated as memory: a result never re-sends a run of lines (≥6, mostly substantive) that an earlier result in the session already contained. The run collapses to `⋯ N lines same as #12 · path:a–b ⋯`, citing the earlier program and, when the lines came from a file, the exact range, so one `read(path, a, n)` recovers them. Changed lines are never collapsed, so a re-read after an edit is exactly the delta. Lines the current program read with an explicit `offset`/`limit` are pinned and always shown. Programs are numbered (`ok #12 3ms`) to anchor the citations; the window is `seenWindow` programs (default 40) and resets on `session_start`. Not compression: every collapsed line already exists verbatim in the model's context. `/supernova` reports the session's returned vs. not-re-sent tokens.
|
|
27
|
+
- **Edits close the loop.** `edit` returns the post-edit lines with numbers (±2 context, ≤40 lines) so no verification re-read is needed; a quick structural check (`check: unclosed '{' opened at line 1`, JSON parsed exactly; brackets/strings/templates/regex-aware, 0 false positives over 6,100 source files); and `X also referenced in a.js:12, b.js:40` for every declaration the edit changed, so callers are not forgotten.
|
|
28
|
+
- **Failures carry their source.** A failing `bash` appends `--- source` with ±2 lines around each `path:line` it printed (≤4 sites), so a stack trace or test failure does not cost a read turn.
|
|
29
|
+
- **Outlines carry relations.** Expanded spans in `read(path, {about})` end with `// used by: a.js:12, b.js:40`.
|
|
30
|
+
|
|
31
|
+
Measured on a read→edit→verify→edit→verify→outline→outline loop: 20,967 tokens naive vs **3,098** (−85%); the verification re-read costs 49 tokens instead of 1,078.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- README documents the research (Zero-Mem, Agent Zero Memory, Harness-of-Harness, SPACE) and the fff port precisely: which formulas and constants are fff's, what was not ported, and that the code was reimplemented in JavaScript after reading fff's Rust sources.
|
|
36
|
+
|
|
37
|
+
## [0.0.13] - 2026-09-04
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- A program that made no host calls rendered an empty frame (`╭─ nova: complete ─╮ / ╰──╯`); it is now a single status line. Expanded view still frames the returned value.
|
|
42
|
+
|
|
43
|
+
## [0.0.12] - 2026-09-04
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- `read(path, {about})`: one call returns the whole file as an outline: every declaration (including nested object/class methods) with its signature and line range, and only the bodies relevant to `about` expanded, with line numbers, under a character budget (default 8000, 6 spans, weak-match cutoff at 40% of the best span). A folded body reads ` 75 function applyReplacements(target, content, edits) … 26 lines`, so the follow-up `read(path, 75, 26)` is known without another search. Across 5 files/questions: **65% fewer tokens** than reading the file (23,968 → 8,477), correct spans expanded.
|
|
48
|
+
- fff (dmtrKovalenko/fff) ported to plain JS in `fuzzy.js`, no binary and no spawn:
|
|
49
|
+
- `glob`/`find` with free text (no glob characters) is a typo-tolerant, frecency-ranked path search: up to 2 skipped characters, smart-case, +40%/+20% exact/any filename bonus, frecency boost `base·f/100` with fff's AI-mode decay (3-day half-life, 7-day window, 30s…4h modification steps), +15% for git-modified files, directory-distance penalty from the last touched file.
|
|
50
|
+
- `grep` is smart-case, groups rows under one path header, lists files that *declare* the name first with declaration lines marked `*` (definition-first hinting), accepts `limit`, and falls back to a fuzzy line match when the literal has no hits (`CausualVfs` → `class CausalVfs`).
|
|
51
|
+
- The workspace index watches the tree with `fs.watch` (recursive) and refreshes on change instead of every 10s; TTL remains the fallback when watching is unavailable.
|
|
52
|
+
- Structural surface detects indented methods (`async bash(params, signal) {`, `name: (a) => {`), so adapters and class members are their own spans for `evidence`, `snap`, and outlines.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- Tool guidance: `evidence(question)` across the repo or `read(path, {about})` for one file; plain `read(path)` only for lines you will edit.
|
|
57
|
+
|
|
58
|
+
## [0.0.11] - 2026-09-04
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- `evidence(query, {k?, path?, maxChars?})`: zero-token evidence selection over the codebase after Zero-Mem (arXiv:2607.29377), implemented 1:1 with the paper's non-generative pipeline: declared spans are the context units and identifiers the entities (eq.3), entity–span weights `w(d,e)=c(e,d)/Σc` (eq.4), file→span→line hierarchy (eq.5, eq.11), a deterministic query profile and relational/local route (eq.6–7), lexical entity alignment and one IDF-damped co-occurrence propagation step (eq.8–9), personalized PageRank `π=(1−γ)r+γPᵀπ` over spans (eq.10, γ=0.85, 10 iterations, factored through the entity layer so it is O(nnz)), per-view min-max normalisation and ρ-weighted fusion (eq.12–13, ρ=0.7), closure with definition bridges and in-file neighbours (eq.14), and calibration that filters by boundary/answer type/lexical support and ranks by type compatibility (eq.15). Returns top-K (default 5, per the paper's Top-5 ≈ Top-10 finding) verbatim source spans with path and line provenance under a 6000-char budget. Across 8 understanding questions on this repo the correct span ranks first and the result costs **68% fewer tokens** than reading the files the question spans (43,916 → 14,147). Warm latency 1–4ms.
|
|
63
|
+
- Structural surface now records column-0 `const/let/var` bindings, so module-level tables are their own spans (also sharpens `snap`).
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- Tool guidance: "To understand code, call `evidence(question)` and read only the returned spans; read whole files only to edit them" (Agent Zero Memory's L0→L1→L2 read discipline; Harness-of-Harness progressive disclosure).
|
|
68
|
+
|
|
69
|
+
## [0.0.10] - 2026-09-04
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
- In-process workspace index (`repo-index.js`): one gitignore-aware `rg --files` per 10s window, then file text, lowercase lines, declared names, and structural surfaces are cached per path and validated by mtime. `snap`, `grep`, `glob`, and `find` are served from it without spawning (trees over 4000 files fall back to `rg`). Warm latencies: snap 14ms → 0.36ms, grep 4.8ms → 0.15ms, glob 4.7ms → 0.06ms; a warm `nova.call` is ~20µs and the program floor is ~50µs.
|
|
74
|
+
- `bash` and any on-disk write or commit invalidate the file list, so a file created by a shell command is visible to the next `glob` in the same program.
|
|
75
|
+
- Workspace-path realpath checks are cached per program (two syscalls per call before).
|
|
76
|
+
- Live card updates are coalesced to one host re-render per 40ms frame; a tight loop of calls no longer pays a TUI render per call.
|
|
77
|
+
- Fewer result tokens: `snap` returns a workspace-relative path and a 7-line context window (`►36 text`); `grep` rows are relative; `nova.search` hits drop `callable:true`; `nova.describe` omits `required:false` and the redundant `signature` line.
|
|
78
|
+
|
|
79
|
+
## [0.0.9] - 2026-09-04
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- `snap` returns the defining file and line. `const x = fn(...)` call sites matched the definition regex and earned definition credit, so the busiest caller outranked the definer; definition credit now requires the declared name to contain a query token, mention credit is capped per file, and the anchor is the surface item with the most token matches (`resolveWorkspacePath`, not `getResolvedCwd`).
|
|
84
|
+
- Success cards draw a visible frame. `borderMuted` is background-level in OMP themes, so only error cards had a border; success uses `dim`.
|
|
85
|
+
- Failed calls show their error on the row instead of `done`; rows with no target show nothing.
|
|
86
|
+
- `read([...paths])` rows read `2 files: a.js, b.js` instead of a comma-joined path list.
|
|
87
|
+
|
|
5
88
|
## [0.0.8] - 2026-09-04
|
|
6
89
|
|
|
7
90
|
### Changed
|
|
8
91
|
|
|
9
92
|
- Result card redesigned as an aligned ledger: one row per call (status · tool · duration · `exit N` · `+a/-r` · target), no tree stems or spacer rows, durations humanized (`6.2s`), multi-line commands shown as their first line plus `…+N lines`, and paths fitted to width with the basename kept. Trace records now carry per-call `ms` and non-zero `exitCode`.
|
|
10
|
-
- Guest programs run in a warm worker thread. A hard timeout or abort now terminates synchronous loops (`while (true) {}`), `process.exit()` only ends the program, and `maxHeapMb` (V8 `resourceLimits` plus a process-RSS watchdog for Bun) stops memory blow-ups
|
|
93
|
+
- Guest programs run in a warm worker thread. A hard timeout or abort now terminates synchronous loops (`while (true) {}`), `process.exit()` only ends the program, and `maxHeapMb` (V8 `resourceLimits` plus a process-RSS watchdog for Bun) stops memory blow-ups. None of these can take the host down anymore. Warm-worker overhead is ~0.1ms per program and ~20µs per `nova.call`.
|
|
11
94
|
- Result text is a compact JS literal (`ok 12ms` header, unquoted keys, no separator whitespace, one item per line only past 120 columns): ~43% fewer tokens than the previous pretty JSON. Logs appear under `--- logs` only when present.
|
|
12
95
|
- `maxReturnChars` default lowered from 200000 to 32000.
|
|
13
96
|
- Return values that JSON cannot express are rendered instead of collapsing to `[object Object]`: circular references, `Map`, `Set`, `BigInt`, `Error`, functions, typed arrays.
|
|
@@ -66,7 +149,7 @@
|
|
|
66
149
|
|
|
67
150
|
### Fixed
|
|
68
151
|
|
|
69
|
-
- OMP no longer hangs on "Loading plugins…"
|
|
152
|
+
- OMP no longer hangs on "Loading plugins…": removed top-level import of `@oh-my-pi/pi-coding-agent/tui` from the extension (portable framed chrome only).
|
|
70
153
|
|
|
71
154
|
## [0.0.3] - 2026-09-03
|
|
72
155
|
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Load **before** other tool-owning packages so `registerTool` capture works. Rest
|
|
|
25
25
|
| Parallel reads | Ad hoc | `callMany` Auto / `parallel()` |
|
|
26
26
|
| Hosts | Separate packages | Same tarball for Pi **and** OMP |
|
|
27
27
|
|
|
28
|
-
Guest code runs as an `AsyncFunction` in a worker thread (same trust class as host `bash`)
|
|
28
|
+
Guest code runs as an `AsyncFunction` in a worker thread (same trust class as host `bash`), not an OS/VM sandbox. Tool calls go through `nova.call` RPC to the host thread; a hard timeout, abort, `process.exit`, or a memory blow-up terminates the worker without touching the harness.
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
@@ -46,11 +46,17 @@ async () => {
|
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Globals: `nova
|
|
49
|
+
Globals: `nova`, `parallel`, `pipeline`, `console`, plus shorthand `read` (path or path array), `write`, `edit`, `patch`, `evidence`, `surface`, `snap`, `bash`, and `exec`.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Read discipline that keeps context small: `evidence(question)` across the repo, or `read(path, {about: question})` for one file (full structure, only relevant bodies expanded, ~65% fewer tokens than the file) → `read(path, offset, limit)` only for the lines you will edit.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Results never repeat what the model already saw: a run of lines from an earlier result collapses to `⋯ 23 lines same as #12 · host-bridge.js:40–62 ⋯`; changed lines always show; `read(path, a, n)` shows a cited range again. `edit` returns the post-edit lines, a structural check, and who else references a changed declaration; a failing `bash` attaches the source behind the `path:line` it printed. A read→edit→verify loop costs ~15% of the naive token count.
|
|
54
|
+
|
|
55
|
+
File search is an in-process port of [fff](https://github.com/dmtrKovalenko/fff): `glob("hostbrdge")` finds `host-bridge.js` (typo-tolerant, frecency- and git-status-ranked), `grep` is smart-case with declaration lines first and a fuzzy fallback. None of it spawns a process.
|
|
56
|
+
|
|
57
|
+
The returned value is rendered as a compact JS literal (unquoted keys, one item per line only when a container exceeds 120 columns) and capped at `maxReturnChars`. Strings are returned raw. This costs ~43% fewer tokens than pretty JSON. Return small shaped values, not raw file dumps.
|
|
58
|
+
|
|
59
|
+
Unified Pi/OMP card: one aligned row per call (status · tool · duration · target) with bounded mutation diffs.
|
|
54
60
|
|
|
55
61
|
```text
|
|
56
62
|
╭─── nova: 4 calls · 6.9s ─────────────────────────────────────╮
|
|
@@ -74,9 +80,11 @@ Multi-line commands show their first line plus a hidden-line count. Press Enter
|
|
|
74
80
|
| `nova.search(query, limit?)` | Thin catalog hits |
|
|
75
81
|
| `nova.describe(name)` | Parameter summary on demand |
|
|
76
82
|
| `nova.call(name, args)` | Host tool or native adapter |
|
|
77
|
-
| `nova.callMany([{name,args}])` | Auto parallel wave
|
|
83
|
+
| `nova.callMany([{name,args}])` | Auto parallel wave; iterable array with `.mode` / `.results` |
|
|
84
|
+
| `nova.evidence(query, {k?, path?, maxChars?})` | Top-K source spans (path, lines, verbatim text) that answer a question. Zero-token evidence selection after Zero-Mem; ~68% fewer tokens than reading the files |
|
|
85
|
+
| `read(path, {about})` | Whole-file outline with only the relevant bodies expanded; folded bodies show `line … N lines` |
|
|
78
86
|
| `nova.surface(path)` | Structural outline for a source file |
|
|
79
|
-
| `nova.snap(query, searchRoot?)` |
|
|
87
|
+
| `nova.snap(query, searchRoot?)` | Defining file (workspace-relative), line, signature, confidence, and context for a concept; served from the in-process index in well under 1ms |
|
|
80
88
|
| `nova.has(name)` | Whether a catalog or native tool is callable (sync) |
|
|
81
89
|
| `parallel(thunks)` / `pipeline(items, …stages)` | Raw `Promise.all` helpers |
|
|
82
90
|
| `nova.speculate(fn)` | Counterfactual branch (rollback / commit) |
|
|
@@ -98,16 +106,17 @@ Optional `~/.pi/agent/supernova.json` or `~/.omp/agent/supernova.json`
|
|
|
98
106
|
"maxCallResultChars": 65536,
|
|
99
107
|
"maxReturnChars": 32000,
|
|
100
108
|
"maxHeapMb": 512,
|
|
109
|
+
"seenWindow": 40,
|
|
101
110
|
"maxSearchResults": 12,
|
|
102
111
|
"spillDir": null
|
|
103
112
|
}
|
|
104
113
|
```
|
|
105
114
|
|
|
106
|
-
`maxHeapMb` caps the guest worker heap (V8 `resourceLimits` on Node) and arms a process-RSS watchdog that terminates a runaway program on both Node and Bun.
|
|
115
|
+
`seenWindow` is how many programs back the seen-ledger remembers (set 0 to disable collapsing). `maxHeapMb` caps the guest worker heap (V8 `resourceLimits` on Node) and arms a process-RSS watchdog that terminates a runaway program on both Node and Bun.
|
|
107
116
|
|
|
108
|
-
Defaults also set `excludeTools` (includes `supernova` and DCE helpers). An empty `"excludeTools": []` **replaces** those defaults
|
|
117
|
+
Defaults also set `excludeTools` (includes `supernova` and DCE helpers). An empty `"excludeTools": []` **replaces** those defaults, so omit the key unless you mean that.
|
|
109
118
|
|
|
110
|
-
Slash
|
|
119
|
+
Slash command `/supernova`: catalog size, captured executors, and session token stats.
|
|
111
120
|
|
|
112
121
|
---
|
|
113
122
|
|
|
@@ -134,17 +143,32 @@ Pair with DCE last if you use it: `omp install npm:pi-deferred-context-engine`.
|
|
|
134
143
|
|---------|-----|
|
|
135
144
|
| `unknown tool "…"` | Follow the `Did you mean` hint, or `nova.search("")`; for host tools install supernova **first**; restart; `/supernova` |
|
|
136
145
|
| `Rendered line exceeds terminal width` | ≥0.0.1 and restart so `render.js` reloads |
|
|
137
|
-
| `callMany` / not iterable | ≥0.0.1
|
|
146
|
+
| `callMany` / not iterable | ≥0.0.1; the return is an array with `.mode` / `.results` |
|
|
138
147
|
| Extension missing on OMP | `omp install npm:pi-supernova` (needs `"omp".extensions`) |
|
|
139
148
|
|
|
140
149
|
---
|
|
141
150
|
|
|
142
151
|
## Limitations
|
|
143
152
|
|
|
144
|
-
- Guest JS is **unsandboxed**. Adapter path jails are not a boundary against `import("node:fs")`. The worker only contains hangs, exits, and memory
|
|
153
|
+
- Guest JS is **unsandboxed**. Adapter path jails are not a boundary against `import("node:fs")`. The worker only contains hangs, exits, and memory, not intent.
|
|
145
154
|
- Guest error messages carry `(line:col)` on Node; Bun's engine does not expose guest-relative positions.
|
|
146
155
|
- `bash` / mutating tools flush speculative writes (transaction barrier); error rollback cannot undo that.
|
|
147
|
-
-
|
|
156
|
+
- The workspace index refreshes its file list every 10s or on any supernova mutation; a file created by an external process can take up to 10s to appear in `glob`/`snap` (`read` is never stale).
|
|
157
|
+
- Pre-1.0 package: APIs and TUI may still evolve between minor releases.
|
|
158
|
+
|
|
159
|
+
## Research and prior art
|
|
160
|
+
|
|
161
|
+
Supernova's retrieval and result shaping implement published methods. Where a paper's mechanism needs a model call it stays out of the tool; only the deterministic parts are implemented, and each is cited at the code that implements it.
|
|
162
|
+
|
|
163
|
+
| Work | What we use it for | Where |
|
|
164
|
+
|------|--------------------|-------|
|
|
165
|
+
| **Zero-Mem: Zero-Token Memory Operations for LLM Agents**, Xiao, Zhu, Zhang, Chen, Hong, Zhuang, Zhang, Chen, Ouyang, Ren, Huang (arXiv:2607.29377) | `evidence(query)`: entity–context graph with co-occurrence weights (eq. 3–4), turn/window/episode hierarchy as line/span/file (eq. 5, 11), query profile and relational/local routing (eq. 6–7), lexical entity alignment and one propagation step (eq. 8–9), personalized PageRank over spans (eq. 10), per-view normalisation and ρ-weighted fusion (eq. 12–13), closure with bridges and neighbours (eq. 14), deterministic calibration (eq. 15). Top-K = 5 follows the paper's Top-5 ≈ Top-10 finding. | `evidence.js` |
|
|
166
|
+
| **Agent Zero Memory: Provenance-Aware Long-Term Memory for LLM Agents**, Zhu, Wu (arXiv:2608.29606) | Every returned unit carries provenance (path, line range, verbatim text); the L0→L1→L2 read discipline (`surface` → `evidence`/`read(path, {about})` → `read(path, offset, limit)`); the citation-lock idea that a model should only cite what it actually opened. | `evidence.js`, `outline.js`, tool guidance |
|
|
167
|
+
| **Harness-of-Harness: Multi-Day Autonomous Software Development with Continual Improvement**, Yan, Su, et al. (arXiv:2609.01481) | Progressive disclosure (index first, detail on demand) and carrying evidence forward instead of reconstructing it from code. | outline / result shaping |
|
|
168
|
+
| **Act More, Decide Less: Skill-Guided Adaptive Action Chunking for Long-Horizon LLM Agents**, Yang, Jin, Zhao, et al. (arXiv:2609.02042) | Framing: one supernova program is an action chunk (one model decision, many primitive actions, stop at the first failing one). | runtime design |
|
|
169
|
+
| **fff**, Dmitriy Kovalenko, MIT, <https://github.com/dmtrKovalenko/fff> | File search. We reimplemented fff's ranking in plain JavaScript after reading its Rust sources (`crates/fff-core/src/score.rs`, `dbs/frecency.rs`, `path_utils.rs`); the formulas and constants are fff's, the code is ours, and nothing runs out of process. Ported: typo-tolerant fuzzy path matching with boundary/consecutive/case bonuses and smart-case; exact-filename +40% and filename +20% bonuses; frecency boost `base·f/100` with fff's AI-mode decay (3-day half-life, 7-day window) and modification-recency steps (30s/5m/15m/1h/4h); git-modified +15%; directory-distance penalty from the current file (−1 per hop, floor −20); definition-first result hinting; fuzzy fallback on zero literal matches; weak-match cutoff; watcher-driven index refresh. Not ported: fff's SIMD/frizbee matcher (ours is an fzf-style greedy match with backward tightening), LMDB persistence (frecency is per session), and the MCP/Neovim surfaces. | `fuzzy.js`, `repo-index.js`, `host-bridge.js` |
|
|
170
|
+
|
|
171
|
+
fff is © Dmitriy Kovalenko and contributors, released under the MIT License; this package is also MIT. If you install fff's own Pi extension (`@ff-labs/pi-fff`) alongside supernova, its `ffgrep`/`fffind` tools are captured and callable through `nova.call` like any other host tool.
|
|
148
172
|
|
|
149
173
|
## License
|
|
150
174
|
|
package/bottleneck.js
CHANGED
|
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { isString, isObject } from "./decode.js";
|
|
5
5
|
import { truncateChars, formatValue } from "./format.js";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
function serializeBounded(value, maxChars, label = "value") {
|
|
8
8
|
let serialized;
|
|
9
9
|
try {
|
|
10
10
|
serialized = isString(value) ? value : JSON.stringify(value);
|
package/catalog.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import { isString, isObject } from "./decode.js";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const NATIVE_TOOL_DEFINITIONS = [
|
|
5
5
|
{
|
|
6
6
|
name: "read",
|
|
7
7
|
description: "Read UTF-8 workspace files by path, or resolve a concept query to source. Supports path arrays, offset, and limit.",
|
|
@@ -10,6 +10,7 @@ export const NATIVE_TOOL_DEFINITIONS = [
|
|
|
10
10
|
target: { anyOf: [{ type: "string" }, { type: "array" }], description: "File path/query or array of paths" },
|
|
11
11
|
offset: { type: "number", description: "One-based starting line" },
|
|
12
12
|
limit: { type: "number", description: "Maximum lines to return" },
|
|
13
|
+
about: { type: "string", description: "Question or symbol: returns the whole file as an outline with only the relevant bodies expanded" },
|
|
13
14
|
} },
|
|
14
15
|
},
|
|
15
16
|
{
|
|
@@ -17,9 +18,9 @@ export const NATIVE_TOOL_DEFINITIONS = [
|
|
|
17
18
|
parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] },
|
|
18
19
|
},
|
|
19
20
|
{
|
|
20
|
-
name: "edit", description: "Apply unique text replacements
|
|
21
|
+
name: "edit", description: "Apply unique text replacements to a workspace file; returns the post-edit lines, a structural check, and references to changed declarations.",
|
|
21
22
|
parameters: { type: "object", properties: {
|
|
22
|
-
path: { type: "string" }, oldText: { type: "string" }, newText: { type: "string" }, edits: { type: "array"
|
|
23
|
+
path: { type: "string" }, oldText: { type: "string" }, newText: { type: "string" }, edits: { type: "array", description: "[{oldText, newText}] for several replacements in one call" },
|
|
23
24
|
}, required: ["path"] },
|
|
24
25
|
},
|
|
25
26
|
{
|
|
@@ -33,6 +34,15 @@ export const NATIVE_TOOL_DEFINITIONS = [
|
|
|
33
34
|
path: { type: "string", description: "Optional workspace search root; explicitly targeting a hidden directory includes its hidden files, but Git metadata is always excluded" },
|
|
34
35
|
}, required: ["query"] },
|
|
35
36
|
},
|
|
37
|
+
{
|
|
38
|
+
name: "evidence", description: "Top-K source spans (with path and line provenance) that answer a concept question; read these instead of whole files.",
|
|
39
|
+
parameters: { type: "object", properties: {
|
|
40
|
+
query: { type: "string", description: "Concept, symbol, or question" },
|
|
41
|
+
path: { type: "string", description: "Optional search root" },
|
|
42
|
+
k: { type: "number", description: "Main spans to return (default 5)" },
|
|
43
|
+
maxChars: { type: "number", description: "Total text budget (default 6000)" },
|
|
44
|
+
}, required: ["query"] },
|
|
45
|
+
},
|
|
36
46
|
{
|
|
37
47
|
name: "surface", description: "Extract a structural outline from a workspace source file.",
|
|
38
48
|
parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
|
|
@@ -42,13 +52,13 @@ export const NATIVE_TOOL_DEFINITIONS = [
|
|
|
42
52
|
parameters: { type: "object", properties: { command: { type: "string" }, cwd: { type: "string" }, timeoutMs: { type: "number" } }, required: ["command"] },
|
|
43
53
|
},
|
|
44
54
|
{
|
|
45
|
-
name: "grep", description: "Search
|
|
55
|
+
name: "grep", description: "Search file contents. Smart-case regex; definition lines first (marked *); fuzzy fallback when nothing matches literally.",
|
|
46
56
|
parameters: { type: "object", properties: {
|
|
47
|
-
pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" }, caseSensitive: { type: "boolean" },
|
|
57
|
+
pattern: { type: "string" }, path: { type: "string" }, glob: { type: "string" }, caseSensitive: { type: "boolean" }, limit: { type: "number" },
|
|
48
58
|
}, required: ["pattern"] },
|
|
49
59
|
},
|
|
50
60
|
{
|
|
51
|
-
name: "glob", description: "
|
|
61
|
+
name: "glob", description: "Find files: a glob pattern, or free text for typo-tolerant, frecency-ranked path search.",
|
|
52
62
|
parameters: { type: "object", properties: { pattern: { type: "string" } }, required: ["pattern"] },
|
|
53
63
|
},
|
|
54
64
|
{
|
|
@@ -142,12 +152,7 @@ export function searchCatalog(catalog, query, limit = 12) {
|
|
|
142
152
|
for (const row of catalog) {
|
|
143
153
|
const score = scoreRow(row, tokens);
|
|
144
154
|
if (score <= 0 && tokens.length > 0) continue;
|
|
145
|
-
scored.push({
|
|
146
|
-
name: row.name,
|
|
147
|
-
description: row.description.slice(0, 160),
|
|
148
|
-
score,
|
|
149
|
-
callable: true,
|
|
150
|
-
});
|
|
155
|
+
scored.push({ name: row.name, description: row.description.slice(0, 160), score });
|
|
151
156
|
}
|
|
152
157
|
scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
153
158
|
return scored.slice(0, Math.max(1, limit)).map(({ score: _s, ...hit }) => hit);
|
|
@@ -155,9 +160,10 @@ export function searchCatalog(catalog, query, limit = 12) {
|
|
|
155
160
|
|
|
156
161
|
function fieldSummary(key, schema, required) {
|
|
157
162
|
const s = schema && isObject(schema) ? schema : {};
|
|
163
|
+
// Only signal-bearing keys: `required:false` and empty descriptions cost tokens and say nothing.
|
|
158
164
|
return {
|
|
159
165
|
type: s.type || (Array.isArray(s.anyOf) ? "union" : "unknown"),
|
|
160
|
-
required: required.has(key),
|
|
166
|
+
required: required.has(key) || undefined,
|
|
161
167
|
description: isString(s.description) ? s.description.slice(0, 120) : undefined,
|
|
162
168
|
};
|
|
163
169
|
}
|
|
@@ -195,7 +201,7 @@ function editDistance(a, b) {
|
|
|
195
201
|
}
|
|
196
202
|
|
|
197
203
|
/** Closest tool names for a mistyped name: substring hits first, then a length-scaled edit distance. */
|
|
198
|
-
|
|
204
|
+
function suggestNames(name, candidates, limit = 3) {
|
|
199
205
|
const needle = String(name || "").toLowerCase();
|
|
200
206
|
if (!needle) return [];
|
|
201
207
|
const maxDistance = Math.max(1, Math.floor(needle.length / 3));
|
|
@@ -233,7 +239,6 @@ export function describeTool(catalog, name) {
|
|
|
233
239
|
description: row.description,
|
|
234
240
|
parameters: schemaSummary(row.parameters),
|
|
235
241
|
sourcePath: row.sourcePath,
|
|
236
|
-
signature: `await nova.call(${JSON.stringify(row.name)}, args)`,
|
|
237
242
|
};
|
|
238
243
|
}
|
|
239
244
|
return row._described;
|
package/check.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Quick structural check after an edit, not a parser. Catches the edit failures models make
|
|
2
|
+
// most: an unbalanced brace/bracket/paren or an unterminated string, with the line it happened
|
|
3
|
+
// on, so a broken edit is known now instead of after a test run. JSON is checked exactly.
|
|
4
|
+
|
|
5
|
+
const OPEN = { "{": "}", "[": "]", "(": ")" };
|
|
6
|
+
const CLOSE = new Set(["}", "]", ")"]);
|
|
7
|
+
const REGEX_PRECEDERS = new Set(["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "+", "-", "*", "%", "<", ">", "~", "^", "return", "typeof", "case", "do", "else", "in", "of"]);
|
|
8
|
+
|
|
9
|
+
function skipString(text, i, quote) {
|
|
10
|
+
for (let j = i + 1; j < text.length; j++) {
|
|
11
|
+
if (text[j] === "\\") {
|
|
12
|
+
j++;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (text[j] === quote) return j + 1;
|
|
16
|
+
if (quote !== "`" && text[j] === "\n") return -1;
|
|
17
|
+
}
|
|
18
|
+
return -1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function skipTemplate(text, i, stack) {
|
|
22
|
+
for (let j = i + 1; j < text.length; j++) {
|
|
23
|
+
if (text[j] === "\\") {
|
|
24
|
+
j++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (text[j] === "`") return j + 1;
|
|
28
|
+
if (text[j] === "$" && text[j + 1] === "{") {
|
|
29
|
+
const end = balancedEnd(text, j + 1, stack);
|
|
30
|
+
if (end < 0) return -1;
|
|
31
|
+
j = end - 1; // loop increment lands on the char after "}"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return -1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Index just past the "}" matching the "{" at i, scanning nested code; −1 when unbalanced. */
|
|
38
|
+
function balancedEnd(text, i, stack) {
|
|
39
|
+
const depth = stack.length;
|
|
40
|
+
const r = scan(text, i, stack, depth);
|
|
41
|
+
return r.error ? -1 : r.end;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function skipComment(text, i) {
|
|
45
|
+
if (text[i + 1] === "/") {
|
|
46
|
+
const nl = text.indexOf("\n", i);
|
|
47
|
+
return nl < 0 ? text.length : nl;
|
|
48
|
+
}
|
|
49
|
+
const end = text.indexOf("*/", i + 2);
|
|
50
|
+
return end < 0 ? text.length : end + 2;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function skipRegex(text, i) {
|
|
54
|
+
let inClass = false;
|
|
55
|
+
for (let j = i + 1; j < text.length; j++) {
|
|
56
|
+
const c = text[j];
|
|
57
|
+
if (c === "\\") {
|
|
58
|
+
j++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (c === "\n") return -1;
|
|
62
|
+
if (c === "[") inClass = true;
|
|
63
|
+
else if (c === "]") inClass = false;
|
|
64
|
+
else if (c === "/" && !inClass) return j + 1;
|
|
65
|
+
}
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function lineOf(text, i) {
|
|
70
|
+
let n = 1;
|
|
71
|
+
for (let j = 0; j < i && j < text.length; j++) if (text[j] === "\n") n++;
|
|
72
|
+
return n;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const IDENT_START = /[A-Za-z_$]/;
|
|
76
|
+
const IDENT_PART = /[\w$]/;
|
|
77
|
+
|
|
78
|
+
function readIdentifier(text, i) {
|
|
79
|
+
let j = i + 1;
|
|
80
|
+
while (j < text.length && IDENT_PART.test(text[j])) j++;
|
|
81
|
+
return j;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function consumeQuoted(text, i, stack) {
|
|
85
|
+
const c = text[i];
|
|
86
|
+
if (c === "`") {
|
|
87
|
+
const end = skipTemplate(text, i, stack);
|
|
88
|
+
return end < 0 ? { error: "unterminated template literal", at: i } : { end, prev: "value" };
|
|
89
|
+
}
|
|
90
|
+
const end = skipString(text, i, c);
|
|
91
|
+
return end < 0 ? { error: "unterminated string", at: i } : { end, prev: "value" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Try to consume a comment, string, template, or regex at i. Returns { end, prev } | { error, at } | null. */
|
|
95
|
+
function consumeLiteral(text, i, stack, prev) {
|
|
96
|
+
const c = text[i];
|
|
97
|
+
if (c === '"' || c === "'" || c === "`") return consumeQuoted(text, i, stack);
|
|
98
|
+
if (c !== "/") return null;
|
|
99
|
+
if (text[i + 1] === "/" || text[i + 1] === "*") return { end: skipComment(text, i), prev };
|
|
100
|
+
if (prev !== "" && !REGEX_PRECEDERS.has(prev)) return null;
|
|
101
|
+
const end = skipRegex(text, i);
|
|
102
|
+
return end > 0 ? { end, prev: "value" } : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Push/pop a bracket; returns an error, a stop, or null to continue. */
|
|
106
|
+
function bracket(c, i, stack, stopDepth) {
|
|
107
|
+
if (OPEN[c]) {
|
|
108
|
+
stack.push({ c, at: i });
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (!CLOSE.has(c)) return null;
|
|
112
|
+
const top = stack.pop();
|
|
113
|
+
if (!top || OPEN[top.c] !== c) return { error: "unexpected '" + c + "'", at: i };
|
|
114
|
+
if (stopDepth !== undefined && stack.length <= stopDepth) return { end: i + 1 };
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Skips comments, strings, templates and regex literals; `prev` is the last code token, which decides regex-vs-division. */
|
|
119
|
+
function scan(text, start, stack, stopDepth) {
|
|
120
|
+
let i = start;
|
|
121
|
+
let prev = "";
|
|
122
|
+
while (i < text.length) {
|
|
123
|
+
const c = text[i];
|
|
124
|
+
const literal = consumeLiteral(text, i, stack, prev);
|
|
125
|
+
if (literal) {
|
|
126
|
+
if (literal.error) return literal;
|
|
127
|
+
i = literal.end;
|
|
128
|
+
prev = literal.prev;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (IDENT_START.test(c)) {
|
|
132
|
+
const j = readIdentifier(text, i);
|
|
133
|
+
prev = text.slice(i, j);
|
|
134
|
+
i = j;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const outcome = bracket(c, i, stack, stopDepth);
|
|
138
|
+
if (outcome) return outcome;
|
|
139
|
+
if (!/\s/.test(c)) prev = c;
|
|
140
|
+
i++;
|
|
141
|
+
}
|
|
142
|
+
return { end: i };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const CODE_EXT = new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".mts", ".cts", ".java", ".kt", ".c", ".cc", ".cpp", ".h", ".hpp", ".cs", ".go", ".rs", ".swift", ".css", ".scss"]);
|
|
146
|
+
|
|
147
|
+
/** { ok: true } | { ok: false, message }; message names the problem and line. */
|
|
148
|
+
export function quickCheck(text, ext) {
|
|
149
|
+
if (ext === ".json") {
|
|
150
|
+
try {
|
|
151
|
+
JSON.parse(text);
|
|
152
|
+
return { ok: true, kind: "json" };
|
|
153
|
+
} catch (err) {
|
|
154
|
+
return { ok: false, kind: "json", message: String(err.message).replace(/^JSON\.parse: /, "") };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!CODE_EXT.has(ext)) return null;
|
|
158
|
+
const stack = [];
|
|
159
|
+
const r = scan(text, 0, stack);
|
|
160
|
+
if (r.error) return { ok: false, kind: "balance", message: r.error + " at line " + lineOf(text, r.at) };
|
|
161
|
+
if (stack.length) {
|
|
162
|
+
const top = stack[stack.length - 1];
|
|
163
|
+
return { ok: false, kind: "balance", message: "unclosed '" + top.c + "' opened at line " + lineOf(text, top.at) };
|
|
164
|
+
}
|
|
165
|
+
return { ok: true, kind: "balance" };
|
|
166
|
+
}
|
package/config.default.json
CHANGED
package/config.js
CHANGED
|
@@ -18,6 +18,7 @@ const POSITIVE_INTEGER_KEYS = new Set([
|
|
|
18
18
|
"maxLogLineChars",
|
|
19
19
|
"maxSearchResults",
|
|
20
20
|
"maxHeapMb",
|
|
21
|
+
"seenWindow",
|
|
21
22
|
]);
|
|
22
23
|
|
|
23
24
|
const VALIDATORS = {
|
|
@@ -81,4 +82,3 @@ export function loadConfig() {
|
|
|
81
82
|
return mergeConfig(packageDefaults(), user ?? null);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
export { userConfigPath, KNOWN_KEYS };
|
package/decode.js
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
|
|
2
1
|
const toStr = Object.prototype.toString;
|
|
3
2
|
|
|
4
3
|
export const isString = (v) => toStr.call(v) === "[object String]";
|
|
5
4
|
export const isObject = (v) => toStr.call(v) === "[object Object]";
|
|
6
5
|
export const isFunction = (v) => toStr.call(v) === "[object Function]" || v instanceof Function;
|
|
7
6
|
export const isNumber = (v) => toStr.call(v) === "[object Number]" && Number.isFinite(v);
|
|
8
|
-
export const isRecord = (v) => v !== null && isObject(v);
|
|
9
|
-
|
|
10
|
-
export function parseString(v, fallback = "") {
|
|
11
|
-
return isString(v) ? v : fallback;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function parseNonEmptyString(v) {
|
|
15
|
-
return isString(v) && v.trim().length > 0 ? v.trim() : null;
|
|
16
|
-
}
|