pi-supernova 0.0.1
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/bottleneck.js +132 -0
- package/catalog.js +113 -0
- package/config.default.json +14 -0
- package/config.js +75 -0
- package/decode.js +16 -0
- package/diff.js +107 -0
- package/host-bridge.js +822 -0
- package/index.js +227 -0
- package/package.json +89 -0
- package/parallel.js +48 -0
- package/render.js +584 -0
- package/runtime.js +243 -0
- package/snap.js +225 -0
- package/surface.js +127 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.0.1] - 2026-09-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Initial `pi-supernova` CodeMode for Pi and OMP: progressive `nova.search` / `nova.describe` / `nova.call`, result bottleneck, and Amdahl Auto `callMany` / `parallel`.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- TUI no longer crashes with `Rendered line exceeds terminal width (92 > 91)` on long ENOENT/diff lines.
|
|
14
|
+
- Long paths wrap on `/` (filename kept) instead of end-truncating as `packages/pi-supern…`.
|
|
15
|
+
- Call/result cards use a muted violet / grey-blue self-framed chrome (not the stock green tool panel or raw JSON args dump).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AdityaVG13
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# pi-supernova
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/pi-supernova)
|
|
4
|
+
[](https://github.com/AdityaVG13/pi-stack/blob/main/packages/pi-supernova/LICENSE)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md)
|
|
7
|
+
|
|
8
|
+
**CodeMode for [Pi](https://pi.dev) and [OMP](https://omp.sh).** One tool, one guest program: progressive discovery, a hard result bottleneck, and Amdahl-aware parallel waves.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pi install npm:pi-supernova
|
|
12
|
+
omp install npm:pi-supernova
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Load **before** other tool-owning packages so `registerTool` capture works. Restart the host after install.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Why
|
|
20
|
+
|
|
21
|
+
| | Stock multi-tool prompt | **pi-supernova** |
|
|
22
|
+
|--|-------------------------|------------------|
|
|
23
|
+
| Schema tax | Every tool in context | Thin catalog + on-demand `describe` |
|
|
24
|
+
| Multi-step | Model stitches turns | One in-process program |
|
|
25
|
+
| Parallel reads | Ad hoc | `callMany` Auto / `parallel()` |
|
|
26
|
+
| Hosts | Separate packages | Same tarball for Pi **and** OMP |
|
|
27
|
+
|
|
28
|
+
Guest code is an in-process `AsyncFunction` (same trust class as host `bash`) — not an OS/VM sandbox. Tool calls still go through `nova.call`.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quick example
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
async () => {
|
|
36
|
+
const hits = await nova.search("read file contents");
|
|
37
|
+
await nova.describe("read");
|
|
38
|
+
|
|
39
|
+
const a = await nova.call("read", { path: "src/index.ts" });
|
|
40
|
+
const wave = await nova.callMany([
|
|
41
|
+
{ name: "read", args: { path: "a.ts" } },
|
|
42
|
+
{ name: "read", args: { path: "b.ts" } },
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
return { preview: a.value.slice(0, 200), mode: wave.mode, n: wave.length };
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Globals: `nova` / `tools`, `parallel`, `pipeline`, `console`, plus shorthand `read` / `write` / `edit` / `bash` / …
|
|
50
|
+
|
|
51
|
+
Terminal card (muted violet / grey-blue — not a raw JSON args dump):
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
nova · 84ms
|
|
55
|
+
▤ read packages/pi-supernova/host-bridge.js
|
|
56
|
+
✎ edit packages/pi-supernova/diff.js
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
| API | Role |
|
|
64
|
+
|-----|------|
|
|
65
|
+
| `nova.search(query, limit?)` | Thin catalog hits |
|
|
66
|
+
| `nova.describe(name)` | Parameter summary on demand |
|
|
67
|
+
| `nova.call(name, args)` | Host tool or native adapter |
|
|
68
|
+
| `nova.callMany([{name,args}])` | Auto parallel wave — iterable array with `.mode` / `.results` |
|
|
69
|
+
| `parallel(thunks)` / `pipeline(items, …stages)` | Raw `Promise.all` helpers |
|
|
70
|
+
| `nova.speculate(fn)` | Counterfactual branch (rollback / commit) |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
Optional `~/.pi/agent/supernova.json` or `~/.omp/agent/supernova.json`
|
|
77
|
+
(or `PI_SUPERNOVA_CONFIG` / `PI_CONFIG_DIR` / `OMP_CONFIG_DIR`):
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"timeoutMs": 60000,
|
|
82
|
+
"maxCodeChars": 48000,
|
|
83
|
+
"maxBridgeCalls": 256,
|
|
84
|
+
"maxCallResultChars": 65536,
|
|
85
|
+
"maxReturnChars": 200000,
|
|
86
|
+
"maxSearchResults": 12,
|
|
87
|
+
"spillDir": null
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Defaults also set `excludeTools` (includes `supernova` and DCE helpers). An empty `"excludeTools": []` **replaces** those defaults — omit the key unless you mean that.
|
|
92
|
+
|
|
93
|
+
Slash: `/supernova` — catalog size + captured executors.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Install variants
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Path / dogfood
|
|
101
|
+
pi install /path/to/pi-stack/packages/pi-supernova
|
|
102
|
+
omp install /path/to/pi-stack/packages/pi-supernova
|
|
103
|
+
|
|
104
|
+
# Monorepo clone
|
|
105
|
+
git clone https://github.com/AdityaVG13/pi-stack.git
|
|
106
|
+
pi install ./pi-stack/packages/pi-supernova
|
|
107
|
+
omp install ./pi-stack/packages/pi-supernova
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Pair with DCE last if you use it: `omp install npm:pi-deferred-context-engine`.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Troubleshooting
|
|
115
|
+
|
|
116
|
+
| Symptom | Fix |
|
|
117
|
+
|---------|-----|
|
|
118
|
+
| `no executor for tool "…"` | Install supernova **first**; restart; `/supernova` |
|
|
119
|
+
| `Rendered line exceeds terminal width` | ≥0.0.1 and restart so `render.js` reloads |
|
|
120
|
+
| `callMany` / not iterable | ≥0.0.1 — return is an array with `.mode` / `.results` |
|
|
121
|
+
| Extension missing on OMP | `omp install npm:pi-supernova` (needs `"omp".extensions`) |
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Limitations
|
|
126
|
+
|
|
127
|
+
- Guest JS is **unsandboxed**. Adapter path jails are not a boundary against `import("node:fs")`.
|
|
128
|
+
- `bash` / mutating tools flush speculative writes (transaction barrier); error rollback cannot undo that.
|
|
129
|
+
- First public cut (`0.0.1`) — APIs and TUI will iterate.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT · [AdityaVG13/pi-stack](https://github.com/AdityaVG13/pi-stack/tree/main/packages/pi-supernova)
|
package/bottleneck.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { isString, isObject } from "./decode.js";
|
|
6
|
+
|
|
7
|
+
export function truncateChars(text, maxChars, label = "value") {
|
|
8
|
+
const normalized = isString(text) ? text : String(text ?? "");
|
|
9
|
+
const numericLimit = Number(maxChars);
|
|
10
|
+
const limit = Number.isFinite(numericLimit) ? Math.max(0, Math.floor(numericLimit)) : numericLimit === Infinity ? normalized.length : 0;
|
|
11
|
+
if (normalized.length <= limit) return { text: normalized, truncated: false };
|
|
12
|
+
if (limit <= 100) {
|
|
13
|
+
return {
|
|
14
|
+
text: normalized.slice(0, limit),
|
|
15
|
+
truncated: true,
|
|
16
|
+
originalChars: normalized.length,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const head = Math.floor(limit * 0.7);
|
|
20
|
+
let tail = Math.max(0, limit - head);
|
|
21
|
+
let marker = "";
|
|
22
|
+
let previousTail = -1;
|
|
23
|
+
while (tail !== previousTail) {
|
|
24
|
+
previousTail = tail;
|
|
25
|
+
const omitted = normalized.length - head - tail;
|
|
26
|
+
marker = `\n…[${label} truncated ${omitted} chars]…\n`;
|
|
27
|
+
tail = Math.max(0, limit - head - marker.length);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
text: normalized.slice(0, head) + marker + (tail > 0 ? normalized.slice(-tail) : ""),
|
|
31
|
+
truncated: true,
|
|
32
|
+
originalChars: normalized.length,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function serializeBounded(value, maxChars, label = "value") {
|
|
37
|
+
let serialized;
|
|
38
|
+
try {
|
|
39
|
+
serialized = isString(value) ? value : JSON.stringify(value, null, 2);
|
|
40
|
+
} catch {
|
|
41
|
+
serialized = String(value);
|
|
42
|
+
}
|
|
43
|
+
if (serialized === undefined) serialized = "null";
|
|
44
|
+
return truncateChars(serialized, maxChars, label);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractContentText(content) {
|
|
48
|
+
if (!Array.isArray(content)) return "";
|
|
49
|
+
return content
|
|
50
|
+
.filter((part) => part && isObject(part) && part.type === "text" && isString(part.text))
|
|
51
|
+
.map((part) => part.text)
|
|
52
|
+
.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractRawString(raw, maxChars) {
|
|
56
|
+
if (raw == null) return "";
|
|
57
|
+
if (isString(raw)) return raw;
|
|
58
|
+
if (!isObject(raw)) return String(raw);
|
|
59
|
+
if (Array.isArray(raw.content)) return extractContentText(raw.content);
|
|
60
|
+
if (isString(raw.text)) return raw.text;
|
|
61
|
+
return serializeBounded(raw, maxChars, "host-result").text;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function summarizeDetails(details, maxChars) {
|
|
65
|
+
return serializeBounded(details, maxChars, "details").text;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function maybeSpill(cappedText, fullText, config) {
|
|
69
|
+
if (fullText.length <= (config.maxCallResultChars ?? 65536)) return null;
|
|
70
|
+
if (!isString(config.spillDir) || config.spillDir.length === 0) return null;
|
|
71
|
+
try {
|
|
72
|
+
const dir = config.spillDir;
|
|
73
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
74
|
+
const filePath = path.join(dir, `${Date.now()}-${randomUUID().slice(0, 8)}.txt`);
|
|
75
|
+
fs.writeFileSync(filePath, fullText, { encoding: "utf8", mode: 0o600 });
|
|
76
|
+
return { pointer: filePath };
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function packageHostResult(raw, config) {
|
|
83
|
+
const maxChars = config.maxCallResultChars ?? 65536;
|
|
84
|
+
const isError = isObject(raw) && raw.isError === true;
|
|
85
|
+
const details = isObject(raw) ? raw.details : undefined;
|
|
86
|
+
const upstreamTruncated = isObject(details) && details.outputTruncated === true;
|
|
87
|
+
const text = extractRawString(raw, maxChars);
|
|
88
|
+
const summarizedDetails = details === undefined ? undefined : summarizeDetails(details, 2000);
|
|
89
|
+
|
|
90
|
+
const capped = truncateChars(text, maxChars, "host-result");
|
|
91
|
+
if (!capped.truncated) {
|
|
92
|
+
return {
|
|
93
|
+
ok: !isError,
|
|
94
|
+
value: capped.text,
|
|
95
|
+
truncated: upstreamTruncated,
|
|
96
|
+
details: summarizedDetails,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const spill = maybeSpill(capped.text, text, config);
|
|
101
|
+
const value = spill?.pointer
|
|
102
|
+
? `${capped.text}\n\n[full output spilled to ${spill.pointer}]`
|
|
103
|
+
: capped.text;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
ok: !isError,
|
|
107
|
+
value,
|
|
108
|
+
truncated: true,
|
|
109
|
+
originalChars: capped.originalChars,
|
|
110
|
+
spill: spill?.pointer,
|
|
111
|
+
details: summarizedDetails,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function packageFinalReturn(value, logs, config) {
|
|
116
|
+
const maxReturn = config.maxReturnChars ?? 200000;
|
|
117
|
+
const serialized = serializeBounded(value, maxReturn, "return");
|
|
118
|
+
const logLines = Array.isArray(logs) ? logs : [];
|
|
119
|
+
const maxLogLines = config.maxLogLines ?? 100;
|
|
120
|
+
const maxLogLineChars = config.maxLogLineChars ?? 4096;
|
|
121
|
+
const clippedLogs = logLines.slice(0, maxLogLines).map((line) => {
|
|
122
|
+
const s = isString(line) ? line : String(line);
|
|
123
|
+
return s.length > maxLogLineChars ? s.slice(0, maxLogLineChars) + "…" : s;
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
returnValue: serialized.truncated ? serialized.text : value,
|
|
127
|
+
returnText: serialized.text,
|
|
128
|
+
returnTruncated: serialized.truncated,
|
|
129
|
+
logs: clippedLogs,
|
|
130
|
+
logTruncated: logLines.length > maxLogLines,
|
|
131
|
+
};
|
|
132
|
+
}
|
package/catalog.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
|
|
2
|
+
import { isString, isObject } from "./decode.js";
|
|
3
|
+
|
|
4
|
+
function normalizeTool(tool) {
|
|
5
|
+
if (!tool || !isObject(tool)) return null;
|
|
6
|
+
const name = isString(tool.name) ? tool.name : "";
|
|
7
|
+
if (!name) return null;
|
|
8
|
+
const description = isString(tool.description) ? tool.description : "";
|
|
9
|
+
return {
|
|
10
|
+
name,
|
|
11
|
+
nameLower: name.toLowerCase(),
|
|
12
|
+
description,
|
|
13
|
+
descLower: description.toLowerCase(),
|
|
14
|
+
parameters: tool.parameters,
|
|
15
|
+
sourcePath:
|
|
16
|
+
tool.sourceInfo && isString(tool.sourceInfo.path)
|
|
17
|
+
? tool.sourceInfo.path
|
|
18
|
+
: isString(tool.extensionPath)
|
|
19
|
+
? tool.extensionPath
|
|
20
|
+
: isString(tool.sourcePath)
|
|
21
|
+
? tool.sourcePath
|
|
22
|
+
: undefined,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildCatalog(tools, excludeNames = []) {
|
|
27
|
+
const exclude = new Set(excludeNames);
|
|
28
|
+
const rows = [];
|
|
29
|
+
for (const tool of tools || []) {
|
|
30
|
+
const row = normalizeTool(tool);
|
|
31
|
+
if (!row || exclude.has(row.name)) continue;
|
|
32
|
+
rows.push(row);
|
|
33
|
+
}
|
|
34
|
+
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
35
|
+
return rows;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function tokenize(query) {
|
|
39
|
+
return String(query || "")
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.split(/[^a-z0-9_]+/g)
|
|
42
|
+
.filter((t) => t.length > 1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scoreRow(row, tokens) {
|
|
46
|
+
if (tokens.length === 0) return 1;
|
|
47
|
+
const name = row.nameLower || row.name.toLowerCase();
|
|
48
|
+
const desc = row.descLower || row.description.toLowerCase();
|
|
49
|
+
let score = 0;
|
|
50
|
+
for (const token of tokens) {
|
|
51
|
+
if (name === token) score += 10;
|
|
52
|
+
else if (name.includes(token)) score += 5;
|
|
53
|
+
else if (desc.includes(token)) score += 2;
|
|
54
|
+
}
|
|
55
|
+
return score;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function searchCatalog(catalog, query, limit = 12) {
|
|
59
|
+
const tokens = tokenize(query);
|
|
60
|
+
const scored = [];
|
|
61
|
+
for (const row of catalog) {
|
|
62
|
+
const score = scoreRow(row, tokens);
|
|
63
|
+
if (score <= 0 && tokens.length > 0) continue;
|
|
64
|
+
scored.push({
|
|
65
|
+
name: row.name,
|
|
66
|
+
description: row.description.slice(0, 160),
|
|
67
|
+
score,
|
|
68
|
+
callable: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
72
|
+
return scored.slice(0, Math.max(1, limit)).map(({ score: _s, ...hit }) => hit);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function schemaSummary(parameters) {
|
|
76
|
+
if (!parameters || !isObject(parameters)) return { type: "unknown" };
|
|
77
|
+
const props = parameters.properties;
|
|
78
|
+
if (!props || !isObject(props)) {
|
|
79
|
+
return {
|
|
80
|
+
type: parameters.type || "object",
|
|
81
|
+
note: "schema present (no enumerable properties)",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const required = new Set(Array.isArray(parameters.required) ? parameters.required : []);
|
|
85
|
+
const fields = {};
|
|
86
|
+
for (const [key, schema] of Object.entries(props)) {
|
|
87
|
+
const s = schema && isObject(schema) ? schema : {};
|
|
88
|
+
fields[key] = {
|
|
89
|
+
type: s.type || (Array.isArray(s.anyOf) ? "union" : "unknown"),
|
|
90
|
+
required: required.has(key),
|
|
91
|
+
description: isString(s.description) ? s.description.slice(0, 120) : undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { type: "object", fields };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function describeTool(catalog, name) {
|
|
98
|
+
const row = catalog.find((t) => t.name === name);
|
|
99
|
+
if (!row) {
|
|
100
|
+
return { ok: false, error: `unknown tool: ${name}` };
|
|
101
|
+
}
|
|
102
|
+
if (!row._described) {
|
|
103
|
+
row._described = {
|
|
104
|
+
ok: true,
|
|
105
|
+
name: row.name,
|
|
106
|
+
description: row.description,
|
|
107
|
+
parameters: schemaSummary(row.parameters),
|
|
108
|
+
sourcePath: row.sourcePath,
|
|
109
|
+
signature: `await nova.call(${JSON.stringify(row.name)}, args)`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return row._described;
|
|
113
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"timeoutMs": 60000,
|
|
3
|
+
"maxCodeChars": 48000,
|
|
4
|
+
"maxBridgeCalls": 256,
|
|
5
|
+
"maxCallResultChars": 65536,
|
|
6
|
+
"maxReturnChars": 200000,
|
|
7
|
+
"maxLogLines": 100,
|
|
8
|
+
"maxLogLineChars": 4096,
|
|
9
|
+
"maxSearchResults": 12,
|
|
10
|
+
"spillDir": null,
|
|
11
|
+
"excludeTools": ["supernova", "search_tools", "promote_tools", "demote_tools", "list_capabilities"],
|
|
12
|
+
"mutatingTools": ["write", "edit", "bash", "apply_patch", "replace_in_file"],
|
|
13
|
+
"mutatingPrefixes": ["mcp_"]
|
|
14
|
+
}
|
package/config.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { isString, isObject } from "./decode.js";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const DEFAULTS = require("./config.default.json");
|
|
10
|
+
|
|
11
|
+
const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
|
|
12
|
+
const NONNEGATIVE_INTEGER_KEYS = new Set(["maxLogLines"]);
|
|
13
|
+
const POSITIVE_INTEGER_KEYS = new Set([
|
|
14
|
+
"timeoutMs",
|
|
15
|
+
"maxCodeChars",
|
|
16
|
+
"maxBridgeCalls",
|
|
17
|
+
"maxCallResultChars",
|
|
18
|
+
"maxReturnChars",
|
|
19
|
+
"maxLogLineChars",
|
|
20
|
+
"maxSearchResults",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export function packageDefaults() {
|
|
24
|
+
return structuredClone(DEFAULTS);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function userConfigPath() {
|
|
28
|
+
if (process.env.PI_SUPERNOVA_CONFIG) return process.env.PI_SUPERNOVA_CONFIG;
|
|
29
|
+
for (const key of ["PI_CONFIG_DIR", "OMP_CONFIG_DIR"]) {
|
|
30
|
+
const root = process.env[key];
|
|
31
|
+
if (!root || !isString(root)) continue;
|
|
32
|
+
return path.join(path.resolve(root), "agent", "supernova.json");
|
|
33
|
+
}
|
|
34
|
+
const install = import.meta.dirname ?? path.dirname(new URL(import.meta.url).pathname);
|
|
35
|
+
const norm = String(install).replace(/\\/g, "/").toLowerCase();
|
|
36
|
+
const home = os.homedir();
|
|
37
|
+
if (norm.includes("/.omp/")) return path.join(home, ".omp", "agent", "supernova.json");
|
|
38
|
+
return path.join(home, ".pi", "agent", "supernova.json");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readJson(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
44
|
+
return JSON.parse(text);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err && (err.code === "ENOENT" || err.code === "ENOTDIR")) return null;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function mergeConfig(base, overlay) {
|
|
52
|
+
if (!overlay || !isObject(overlay) || Array.isArray(overlay)) return base;
|
|
53
|
+
const out = { ...base };
|
|
54
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
55
|
+
if (!KNOWN_KEYS.has(key)) continue;
|
|
56
|
+
if (POSITIVE_INTEGER_KEYS.has(key)) {
|
|
57
|
+
if (Number.isInteger(value) && value > 0) out[key] = value;
|
|
58
|
+
} else if (NONNEGATIVE_INTEGER_KEYS.has(key)) {
|
|
59
|
+
if (Number.isInteger(value) && value >= 0) out[key] = value;
|
|
60
|
+
} else if (Array.isArray(base[key])) {
|
|
61
|
+
if (Array.isArray(value) && value.every(isString)) out[key] = value.slice();
|
|
62
|
+
} else if (key === "spillDir") {
|
|
63
|
+
if (value === null || (isString(value) && value.length > 0)) out[key] = value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function loadConfig() {
|
|
70
|
+
const userPath = userConfigPath();
|
|
71
|
+
const user = readJson(userPath);
|
|
72
|
+
return mergeConfig(packageDefaults(), user ?? null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { userConfigPath, KNOWN_KEYS };
|
package/decode.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
const toStr = Object.prototype.toString;
|
|
3
|
+
|
|
4
|
+
export const isString = (v) => toStr.call(v) === "[object String]";
|
|
5
|
+
export const isObject = (v) => toStr.call(v) === "[object Object]";
|
|
6
|
+
export const isFunction = (v) => toStr.call(v) === "[object Function]" || v instanceof Function;
|
|
7
|
+
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
|
+
}
|
package/diff.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
|
|
2
|
+
import { isString } from "./decode.js";
|
|
3
|
+
|
|
4
|
+
export function buildEditDiff(filePath, originalText, oldText, newText) {
|
|
5
|
+
const fileLines = isString(originalText) ? originalText.split("\n") : [];
|
|
6
|
+
const idx = isString(originalText) ? originalText.indexOf(oldText) : -1;
|
|
7
|
+
|
|
8
|
+
const startLine = idx >= 0 ? originalText.slice(0, idx).split("\n").length : 1;
|
|
9
|
+
const oldLines = oldText.split("\n");
|
|
10
|
+
const newLines = newText.split("\n");
|
|
11
|
+
|
|
12
|
+
const lines = [];
|
|
13
|
+
if (startLine > 1 && fileLines.length >= startLine - 1) {
|
|
14
|
+
lines.push({ type: "context", lineNum: startLine - 1, text: fileLines[startLine - 2] });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < oldLines.length; i++) {
|
|
18
|
+
lines.push({ type: "remove", lineNum: startLine + i, text: oldLines[i] });
|
|
19
|
+
}
|
|
20
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
21
|
+
lines.push({ type: "add", lineNum: startLine + i, text: newLines[i] });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const afterLine = startLine + oldLines.length;
|
|
25
|
+
if (fileLines.length >= afterLine) {
|
|
26
|
+
lines.push({ type: "context", lineNum: afterLine, text: fileLines[afterLine - 1] });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
path: filePath,
|
|
31
|
+
op: "edit",
|
|
32
|
+
added: newLines.length,
|
|
33
|
+
removed: oldLines.length,
|
|
34
|
+
lines,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildPatchDiff(filePath, patchText) {
|
|
39
|
+
const patchLines = isString(patchText) ? patchText.replace(/\r\n/g, "\n").split("\n") : [];
|
|
40
|
+
const lines = [];
|
|
41
|
+
let added = 0;
|
|
42
|
+
let removed = 0;
|
|
43
|
+
let currentLineNum = 1;
|
|
44
|
+
let inHunk = false;
|
|
45
|
+
|
|
46
|
+
for (const pLine of patchLines) {
|
|
47
|
+
const headerMatch = /^@@\s+-(\d+)/.exec(pLine);
|
|
48
|
+
if (headerMatch) {
|
|
49
|
+
currentLineNum = parseInt(headerMatch[1], 10);
|
|
50
|
+
inHunk = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!inHunk || pLine.startsWith("\\")) continue;
|
|
54
|
+
if (pLine.startsWith("-")) {
|
|
55
|
+
removed += 1;
|
|
56
|
+
lines.push({ type: "remove", lineNum: currentLineNum, text: pLine.slice(1) });
|
|
57
|
+
currentLineNum += 1;
|
|
58
|
+
} else if (pLine.startsWith("+")) {
|
|
59
|
+
added += 1;
|
|
60
|
+
lines.push({ type: "add", lineNum: currentLineNum, text: pLine.slice(1) });
|
|
61
|
+
} else if (pLine.startsWith(" ")) {
|
|
62
|
+
lines.push({ type: "context", lineNum: currentLineNum, text: pLine.slice(1) });
|
|
63
|
+
currentLineNum += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
path: filePath,
|
|
69
|
+
op: "apply_patch",
|
|
70
|
+
added,
|
|
71
|
+
removed,
|
|
72
|
+
lines,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildWriteDiff(filePath, previousText, newText) {
|
|
77
|
+
const newLines = isString(newText) ? newText.split("\n") : [];
|
|
78
|
+
const oldLines = isString(previousText) ? previousText.split("\n") : [];
|
|
79
|
+
|
|
80
|
+
if (oldLines.length === 0 || (oldLines.length === 1 && oldLines[0] === "")) {
|
|
81
|
+
const sampleCount = Math.min(newLines.length, 6);
|
|
82
|
+
const lines = [];
|
|
83
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
84
|
+
lines.push({ type: "add", lineNum: i + 1, text: newLines[i] });
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
path: filePath,
|
|
88
|
+
op: "write",
|
|
89
|
+
added: newLines.length,
|
|
90
|
+
removed: 0,
|
|
91
|
+
lines,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sampleCount = Math.min(newLines.length, 6);
|
|
96
|
+
const lines = [];
|
|
97
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
98
|
+
lines.push({ type: "add", lineNum: i + 1, text: newLines[i] });
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
path: filePath,
|
|
102
|
+
op: "write",
|
|
103
|
+
added: newLines.length,
|
|
104
|
+
removed: oldLines.length,
|
|
105
|
+
lines,
|
|
106
|
+
};
|
|
107
|
+
}
|