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 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
+ [![npm](https://img.shields.io/npm/v/pi-supernova.svg)](https://www.npmjs.com/package/pi-supernova)
4
+ [![license](https://img.shields.io/npm/l/pi-supernova.svg)](https://github.com/AdityaVG13/pi-stack/blob/main/packages/pi-supernova/LICENSE)
5
+ [![node](https://img.shields.io/node/v/pi-supernova.svg)](https://nodejs.org)
6
+ [![pi-package](https://img.shields.io/badge/pi--package-extension-7aa2f7)](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
+ }