pi-supernova 0.5.0 → 0.7.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.
Files changed (50) hide show
  1. package/README.md +97 -11
  2. package/docs/CHANGELOG.md +150 -0
  3. package/docs/TOKEN_COSTS.md +71 -29
  4. package/index.js +126 -82
  5. package/package.json +2 -2
  6. package/src/adapters/bash.js +73 -0
  7. package/src/adapters/edit.js +249 -0
  8. package/src/adapters/errors.js +31 -0
  9. package/src/adapters/index.js +31 -0
  10. package/src/adapters/list.js +102 -0
  11. package/src/adapters/read.js +805 -0
  12. package/src/adapters/refs.js +41 -0
  13. package/src/adapters/write.js +96 -0
  14. package/src/bridge/catalog.js +30 -220
  15. package/src/bridge/host-bridge.js +142 -1032
  16. package/src/bridge/invoke.js +35 -0
  17. package/src/bridge/native-tools.js +1 -188
  18. package/src/context/evidence.js +142 -70
  19. package/src/context/fuzzy.js +61 -22
  20. package/src/context/ledger.js +43 -24
  21. package/src/context/outline.js +26 -12
  22. package/src/context/repo-index.js +242 -71
  23. package/src/context/search.js +189 -56
  24. package/src/context/snap.js +306 -150
  25. package/src/context/spans.js +2 -1
  26. package/src/context/surface.js +29 -14
  27. package/src/contract/bash.js +31 -0
  28. package/src/contract/edit.js +95 -0
  29. package/src/contract/read.js +220 -0
  30. package/src/fs/check.js +19 -7
  31. package/src/fs/diff.js +18 -7
  32. package/src/fs/json-read.js +66 -35
  33. package/src/fs/patch.js +97 -51
  34. package/src/fs/source-window.js +82 -0
  35. package/src/fs/text-ops.js +512 -0
  36. package/src/fs/vfs.js +289 -162
  37. package/src/fs/workspace.js +122 -105
  38. package/src/output/bottleneck.js +211 -107
  39. package/src/output/format.js +112 -63
  40. package/src/runtime/guest-deny-imports.js +34 -0
  41. package/src/runtime/guest-worker.js +306 -213
  42. package/src/runtime/parallel.js +99 -63
  43. package/src/runtime/program-batch.js +189 -69
  44. package/src/runtime/program-file.js +6 -3
  45. package/src/runtime/reference.js +13 -12
  46. package/src/runtime/runtime.js +327 -176
  47. package/src/shared/decode.js +61 -27
  48. package/src/ui/omp-frame.js +70 -46
  49. package/src/ui/render-measure.js +51 -29
  50. package/src/ui/render.js +242 -146
package/index.js CHANGED
@@ -25,6 +25,7 @@ try {
25
25
  Array: (items, opts) => ({ type: "array", items, ...opts }),
26
26
  Integer: (opts) => ({ type: "integer", ...opts }),
27
27
  Optional: (s) => ({ ...s }),
28
+ Boolean: (opts) => ({ type: "boolean", ...opts }),
28
29
  };
29
30
  }
30
31
 
@@ -67,7 +68,10 @@ export function progressEmitter(onUpdate) {
67
68
  const wait = PROGRESS_FRAME_MS - (performance.now() - lastSent);
68
69
 
69
70
  if (wait <= 0) send();
70
- else timer = setTimeout(send, wait);
71
+ else {
72
+ timer = setTimeout(send, wait);
73
+ timer.unref?.();
74
+ }
71
75
  };
72
76
 
73
77
  emit.flush = () => {
@@ -124,8 +128,10 @@ error: ${outcome.error}${logsBlock(outcome)}`;
124
128
  function successText(outcome, call) {
125
129
  const truncated = outcome.returnTruncated ? " [return truncated]" : "";
126
130
  const hint = outcome.undefinedReturn ? " (no return statement; add `return` to get a value)" : "";
131
+ const m = outcome.mutations;
132
+ const showMutations = m && (m.committed || m.rolledBack || m.external || m.pendingCommits || m.recoveryFailed) ? mutationText(outcome) : "";
127
133
 
128
- return `ok #${call} ${outcome.wallMs}ms${truncated}${outcome.mutations?.committed || outcome.mutations?.rolledBack || outcome.mutations?.external ? mutationText(outcome) : ""}${splitTurnHint(outcome)}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
134
+ return `ok #${call} ${outcome.wallMs}ms${truncated}${showMutations}${splitTurnHint(outcome)}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
129
135
  }
130
136
 
131
137
  function fitOutput(outcome, call, limit, format) {
@@ -197,12 +203,119 @@ export function registerCodeMode(pi) {
197
203
  speculateCommit: () => runBridge.barrier(() => runBridge.commitSpeculation()),
198
204
  speculateRollback: () => runBridge.barrier(() => runBridge.rollbackSpeculation()),
199
205
  names: () => ["read", "edit", "write", "bash"],
206
+ describeMemory: () => runBridge.describeMemory?.() ?? null,
200
207
  batchRead: runBridge.supportsBatchRead(),
201
208
  nativeArgv: runBridge.supportsNativeArgv?.() === true,
202
209
  cancel,
203
210
  };
204
211
  }
205
212
 
213
+ function rejectLoneParallel(params) {
214
+ if (params?.parallel !== undefined) throw new Error("parallel applies to the programs array; no commands ran");
215
+ }
216
+
217
+ function bindRunSignal(signal) {
218
+ const runController = new AbortController();
219
+ const abortRun = () => runController.abort(signal?.reason);
220
+
221
+ if (signal?.aborted) abortRun();
222
+ else signal?.addEventListener("abort", abortRun, { once: true });
223
+
224
+ return { runController, abortRun };
225
+ }
226
+
227
+ function openRunBridge(ctx, runCwd, budget, runController) {
228
+ const runBridge = bridge.fork({ getCwd: () => runCwd, budget });
229
+ runBridge.bindCallContext(ctx, runController.signal);
230
+ runBridge.resetCallBudget();
231
+
232
+ return runBridge;
233
+ }
234
+
235
+ async function runAndCommit(params, runCwd, runBridge, abortRun, runController, budget) {
236
+ refreshCatalog(runBridge);
237
+ runBridge.beginSpeculation();
238
+ const outcome = await runGuestProgram({
239
+ code: params?.code,
240
+ file: params?.file,
241
+ cwd: runCwd,
242
+ data: params?.data,
243
+ nova: makeNovaApi(runBridge, abortRun),
244
+ config: { ...config, maxLogLines: Math.max(0,config.maxLogLines-(budget?.logLines ?? 0)), timeoutMs: params?.timeoutMs === undefined ? config.timeoutMs : Number(params.timeoutMs) },
245
+ signal: runController.signal,
246
+ onTimeout: abortRun,
247
+ });
248
+ runBridge.close();
249
+
250
+ if (outcome.ok) {
251
+ if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished edit checkpoint; await it before returning");
252
+ await runBridge.commitSpeculation();
253
+ }
254
+ else while (runBridge.getOverlayDepth()) runBridge.rollbackSpeculation();
255
+
256
+ return outcome;
257
+ }
258
+
259
+ function scheduleWarm(runController) {
260
+ cancelWarmTimer();
261
+
262
+ if (!stopped && !runController.signal.aborted) {
263
+ // Deliver the result before paying for another Worker constructor.
264
+ warmTimer = setImmediate(() => {
265
+ warmTimer = undefined;
266
+
267
+ if (!stopped && !runController.signal.aborted) warmGuestWorker(config).catch(() => {});
268
+ });
269
+ warmTimer.unref?.();
270
+ }
271
+ }
272
+
273
+ function finishRun(runBridge, emitProgress, signal, abortRun, runController) {
274
+ runBridge.setCallListener(null);
275
+ emitProgress.flush();
276
+ signal?.removeEventListener("abort", abortRun);
277
+ // Prepare one pristine worker during the model's next decision. Never
278
+ // recycle a worker that has executed arbitrary guest JavaScript.
279
+ scheduleWarm(runController);
280
+ }
281
+
282
+ function attachReceipts(outcome, trace) {
283
+ if (outcome.ok && outcome.result === undefined) {
284
+ const receipts = mutationReceipts(trace);
285
+
286
+ if (receipts) {
287
+ outcome.resultText = receipts;
288
+ outcome.undefinedReturn = false;
289
+ }
290
+ }
291
+ }
292
+
293
+ function throwIfFailed(outcome, visible, response) {
294
+ if (outcome.ok) return response;
295
+ const error = new Error(visible);
296
+ Object.defineProperty(error,"supernovaResult",{value:response});
297
+ throw error;
298
+ }
299
+
300
+ function packExecuteResult(outcome, call, runBridge, budget, runOpts, peakSeen) {
301
+ if (budget) budget.logLines += outcome.logs?.length ?? 0;
302
+ outcome.overlappedTurn = !runOpts?.parallel && peakSeen > 1 ? peakSeen : 0;
303
+ outcome.mutations = runBridge.getMutations();
304
+ const trace = runBridge.getTrace();
305
+ attachReceipts(outcome, trace);
306
+ const bounded = fitOutput(outcome, call, config.maxReturnChars, outcome.ok ? successText : errorText);
307
+ const visible = runBridge.ledger.dedupe(bounded, call);
308
+ const response = result(visible, {
309
+ ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
310
+ returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
311
+ logs: outcome.logs, result: outcome.result, trace, mutations: outcome.mutations,
312
+ });
313
+
314
+ if (outcome.images?.length) response.content.push(...outcome.images);
315
+
316
+ return throwIfFailed(outcome, visible, response);
317
+ }
318
+
206
319
  pi.registerTool({
207
320
  name: "supernova",
208
321
  label: "Supernova",
@@ -218,6 +331,7 @@ export function registerCodeMode(pi) {
218
331
  file: Type.Optional(Type.String({ minLength: 1 })),
219
332
  data: Type.Optional(Type.Unknown()),
220
333
  }, {additionalProperties:false}), {minItems:1,maxItems:32})),
334
+ parallel: Type.Optional(Type.Boolean()),
221
335
  }),
222
336
  // One self-owned result frame is shared by Pi and OMP; renderCall stays empty
223
337
  // so separate call/result slots cannot duplicate the lifecycle card.
@@ -225,50 +339,26 @@ export function registerCodeMode(pi) {
225
339
  mergeCallAndResult: true,
226
340
  renderCall: renderSupernovaCall,
227
341
  renderResult: renderSupernovaResult,
228
- execute: async function execute(_id, params, signal, onUpdate, ctx, budget) {
342
+ execute: async function execute(_id, params, signal, onUpdate, ctx, budget, runOpts) {
229
343
  if (params?.programs !== undefined) return runProgramBatch(_id,params,signal,onUpdate,ctx,config,execute);
344
+ rejectLoneParallel(params);
230
345
  cancelWarmTimer();
231
- const runCwd = ctx?.cwd || cwd;
232
- const runController = new AbortController();
233
- const abortRun = () => runController.abort(signal?.reason);
234
-
235
- if (signal?.aborted) abortRun();
236
- else signal?.addEventListener("abort", abortRun, { once: true });
237
- const runBridge = bridge.fork({ getCwd: () => runCwd, budget });
238
- runBridge.bindCallContext(ctx, runController.signal);
239
- runBridge.resetCallBudget();
240
-
346
+ const runCwd = isString(ctx?.cwd) && ctx.cwd ? ctx.cwd : cwd;
347
+ const { runController, abortRun } = bindRunSignal(signal);
348
+ const runBridge = openRunBridge(ctx, runCwd, budget, runController);
241
349
  const call = ++programSeq;
242
350
  runBridge.ledger.beginProgram(call);
243
351
  const emitProgress = progressEmitter(onUpdate);
244
352
  runBridge.setCallListener((_record, trace) => emitProgress(trace));
245
353
  emitProgress([]);
246
354
  const started = performance.now();
247
- let outcome;
248
355
  inFlight += 1;
249
356
  overlapPeak = Math.max(overlapPeak, inFlight);
250
357
  let peakSeen = overlapPeak;
358
+ let outcome;
251
359
 
252
360
  try {
253
- refreshCatalog(runBridge);
254
- runBridge.beginSpeculation();
255
- outcome = await runGuestProgram({
256
- code: params?.code,
257
- file: params?.file,
258
- cwd: runCwd,
259
- data: params?.data,
260
- nova: makeNovaApi(runBridge, abortRun),
261
- config: { ...config, maxLogLines: Math.max(0,config.maxLogLines-(budget?.logLines ?? 0)), timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs },
262
- signal: runController.signal,
263
- onTimeout: abortRun,
264
- });
265
- runBridge.close();
266
-
267
- if (outcome.ok) {
268
- if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished edit checkpoint; await it before returning");
269
- await runBridge.commitSpeculation();
270
- }
271
- else while (runBridge.getOverlayDepth()) runBridge.rollbackSpeculation();
361
+ outcome = await runAndCommit(params, runCwd, runBridge, abortRun, runController, budget);
272
362
  } catch (error) {
273
363
  abortRun();
274
364
  runBridge.close();
@@ -279,56 +369,10 @@ export function registerCodeMode(pi) {
279
369
  peakSeen = Math.max(peakSeen, overlapPeak);
280
370
  inFlight -= 1;
281
371
  if (inFlight === 0) overlapPeak = 0;
282
- runBridge.setCallListener(null);
283
- emitProgress.flush();
284
- signal?.removeEventListener("abort", abortRun);
285
- // Prepare one pristine worker during the model's next decision. Never
286
- // recycle a worker that has executed arbitrary guest JavaScript.
287
- cancelWarmTimer();
288
-
289
- if (!stopped && !runController.signal.aborted) {
290
- // Deliver the result before paying for another Worker constructor.
291
- warmTimer = setImmediate(() => {
292
- warmTimer = undefined;
293
-
294
- if (!stopped && !runController.signal.aborted) warmGuestWorker(config).catch(() => {});
295
- });
296
- warmTimer.unref?.();
297
- }
298
- }
299
-
300
- if (budget) budget.logLines += outcome.logs?.length ?? 0;
301
- outcome.overlappedTurn = peakSeen > 1 ? peakSeen : 0;
302
- outcome.mutations = runBridge.getMutations();
303
- const trace = runBridge.getTrace();
304
-
305
- if (outcome.ok && outcome.result === undefined) {
306
- const receipts = mutationReceipts(trace);
307
-
308
- if (receipts) {
309
- outcome.resultText = receipts;
310
- outcome.undefinedReturn = false;
311
- }
312
- }
313
- const format = outcome.ok ? successText : errorText;
314
- const bounded = fitOutput(outcome, call, config.maxReturnChars, format);
315
- const visible = runBridge.ledger.dedupe(bounded, call);
316
-
317
- const response = result(visible, {
318
- ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
319
- returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
320
- logs: outcome.logs, result: outcome.result, trace, mutations: outcome.mutations,
321
- });
322
-
323
- if (outcome.images?.length) response.content.push(...outcome.images);
324
-
325
- if (!outcome.ok) {
326
- const error = new Error(visible);
327
- Object.defineProperty(error,"supernovaResult",{value:response});
328
- throw error;
372
+ finishRun(runBridge, emitProgress, signal, abortRun, runController);
329
373
  }
330
374
 
331
- return response;
375
+ return packExecuteResult(outcome, call, runBridge, budget, runOpts, peakSeen);
332
376
  },
333
377
  });
334
378
 
@@ -347,7 +391,7 @@ export function registerCodeMode(pi) {
347
391
  pi.on("session_start", (_event, ctx) => {
348
392
  stopped = false;
349
393
 
350
- if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
394
+ cwd = ctx && isString(ctx.cwd) && ctx.cwd ? ctx.cwd : process.cwd();
351
395
  // A new session is a new model context: nothing has been seen yet.
352
396
  bridge.bindCallContext(ctx);
353
397
  bridge.ledger.reset();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-supernova",
3
- "version": "0.5.0",
4
- "description": "One CodeMode invocation for Pi and OMP, with four guest commands, automatic read batching and source context.",
3
+ "version": "0.7.0",
4
+ "description": "CodeMode for Pi and OMP: read, edit, write and bash, with transactional files, source views and shared-input program batches.",
5
5
  "type": "module",
6
6
  "author": "AdityaVG13",
7
7
  "license": "MIT",
@@ -0,0 +1,73 @@
1
+ import { isString } from "../shared/decode.js";
2
+ import { unwrapIfFullyQuoted } from "../fs/text-ops.js";
3
+ import { sourceForReferences } from "../fs/source-window.js";
4
+ import { resolveWorkspacePath, runCommand, clearPathCache } from "../fs/workspace.js";
5
+
6
+ export function createBash(ctx) {
7
+ const { getCwd, vfs, config, index, ledger, hooks } = ctx;
8
+ function combineBashText(stdout, stderr) {
9
+ return stdout && stderr ? stdout + (stdout.endsWith("\n") ? "" : "\n") + stderr : stdout || stderr;
10
+ }
11
+
12
+ function isLiteralArgv(params) {
13
+ return Array.isArray(params?.args) && process.platform !== "win32" && params.args.length === Object.keys(params.args).length && params.args.every(isString);
14
+ }
15
+
16
+ function bashCommand(params, literal) {
17
+ if (params?.command !== undefined && !isString(params.command)) throw new Error("bash command must be a string");
18
+ if (literal && (!isString(params.command) || params.args.some(arg => !isString(arg)))) throw new Error("bash argv requires a command string and an array of string args");
19
+ const command = literal ? String(params.command) : unwrapIfFullyQuoted(String(params?.command ?? "").trim());
20
+
21
+ if (!command.trim()) throw new Error("bash requires command");
22
+
23
+ return command;
24
+ }
25
+
26
+ function parseBash(params) {
27
+ const literal = isLiteralArgv(params);
28
+ const command = bashCommand(params, literal);
29
+
30
+ return { literal, command, argv: literal ? [command, ...params.args] : ["bash", "-c", command] };
31
+ }
32
+
33
+ async function bash(params, signal) {
34
+ const cwd = getCwd();
35
+ const { literal, command, argv } = parseBash(params);
36
+ const targetCwd = params?.cwd ? await resolveWorkspacePath(cwd, params.cwd, "bash cwd", true) : cwd;
37
+
38
+ const transactionBarrier = await vfs.prepareExternalMutation("bash");
39
+ let res;
40
+
41
+ try {
42
+ res = await runCommand(argv, {
43
+ cwd: targetCwd,
44
+ env: hooks.commandEnv(),
45
+ commandLabel: literal ? command : undefined,
46
+ timeoutMs: params?.timeoutMs,
47
+ signal,
48
+ maxOutputChars: config.maxCallResultChars,
49
+ });
50
+ } catch (error) {
51
+ if (!signal?.aborted) error.message += await sourceForReferences(cwd, targetCwd, error.message, signal, ledger);
52
+ throw error;
53
+ } finally {
54
+ vfs.invalidateObserved();
55
+ index.invalidate();
56
+ clearPathCache();
57
+ hooks.workspaceChanged();
58
+ }
59
+
60
+ const { stdout, stderr } = res;
61
+ let text = combineBashText(stdout, stderr);
62
+
63
+ if (res.exitCode !== 0) text += await sourceForReferences(cwd, targetCwd, text, signal, ledger);
64
+
65
+ return {
66
+ content: [{ type: "text", text }],
67
+ details: { exitCode: res.exitCode, signal: res.signal, outputTruncated: res.outputTruncated, transactionBarrier },
68
+ isError: res.exitCode !== 0,
69
+ };
70
+ }
71
+
72
+ return { bash };
73
+ }
@@ -0,0 +1,249 @@
1
+ import * as path from "node:path";
2
+ import { isString, isNumber } from "../shared/decode.js";
3
+ import { buildEditDiff, buildMultiEditDiff, buildPatchDiff, MAX_DIFF_MATCHES } from "../fs/diff.js";
4
+ import { declaredName, WorkspaceIndex } from "../context/repo-index.js";
5
+ import { quickCheck } from "../fs/check.js";
6
+ import { applyPatchToText } from "../fs/patch.js";
7
+ import { resolveWorkspacePath, relativeSlash } from "../fs/workspace.js";
8
+ import { referencesForNames } from "../context/search.js";
9
+ import {
10
+ textResult, sourceLines, lineTextRange, applyReplacements, applyViewReplace,
11
+ shiftDiffLines, contentLineInfo, boundedEditDiff, QUICK_CHECK_MAX_CHARS,
12
+ } from "../fs/text-ops.js";
13
+
14
+ export function createEdit(ctx) {
15
+ const { getCwd, vfs, index, ledger } = ctx;
16
+ function lineAt(updated, newLines, n) {
17
+ if (newLines) return newLines[n - 1] ?? "";
18
+ const { start, end } = lineTextRange(updated, n);
19
+
20
+ return updated.slice(start, end).replace(/\r?\n$/, "");
21
+ }
22
+
23
+ function spanEditRange(span, lineCount) {
24
+ if (!span || !Number.isInteger(span.start) || !Number.isInteger(span.end) || span.start < 1 || span.end < span.start) return null;
25
+ const end = Math.min(lineCount, span.end);
26
+
27
+ return span.start <= end ? [{ start: span.start, end }] : [];
28
+ }
29
+
30
+ function mergeEditRange(ranges, line, lineCount) {
31
+ const start = Math.max(1, line - 2), end = Math.min(lineCount, line + 2);
32
+
33
+ if (ranges.length && start <= ranges.at(-1).end + 1) ranges.at(-1).end = Math.max(ranges.at(-1).end, end);
34
+ else ranges.push({ start, end });
35
+ }
36
+
37
+ function collectEditRanges(span, diff, lineCount) {
38
+ const explicit = spanEditRange(span, lineCount);
39
+
40
+ if (explicit) return explicit;
41
+ const ranges = [];
42
+ const positions = diff.lines.filter(row => row.type !== "context")
43
+ .map(row => Math.min(lineCount, row.newLineNum ?? row.lineNum)).sort((a, b) => a - b);
44
+
45
+ for (const line of positions) mergeEditRange(ranges, line, lineCount);
46
+
47
+ return ranges;
48
+ }
49
+
50
+ function formatEditBlocks(rel, updated, newLines, ranges) {
51
+ const perRange = Math.max(1, Math.floor(40 / Math.max(1, ranges.length)));
52
+ const blocks = [];
53
+
54
+ for (const { start, end } of ranges) {
55
+ const last = Math.min(end, start + perRange - 1);
56
+ const lines = Array.from({ length: Math.max(0, last - start + 1) }, (_, i) => lineAt(updated, newLines, start + i));
57
+ ledger.recordOrigin(rel, start, lines);
58
+ blocks.push("edited " + rel + ":" + start + "-" + last + "\n" + lines.map((line, i) => String(start + i).padStart(5) + " " + line).join("\n"));
59
+
60
+ if (last < end) blocks.push("[continue with read({path:" + JSON.stringify(rel) + ",offset:" + (last + 1) + ",limit:" + (end - last) + "})]");
61
+ }
62
+
63
+ return blocks.join("\n");
64
+ }
65
+
66
+ async function editSummary(cwd, target, original, updated, diff, signal, span) {
67
+ const rel = relativeSlash(cwd, target);
68
+ const newLines = updated.length <= 512 * 1024 ? updated.split("\n") : null;
69
+ const lineCount = newLines ? newLines.length : contentLineInfo(updated).count;
70
+ let out = formatEditBlocks(rel, updated, newLines, collectEditRanges(span, diff, lineCount));
71
+
72
+ if (diff?.omittedMatches) out += `\n…${diff.omittedMatches} more matches (receipt shows the first ${MAX_DIFF_MATCHES})`;
73
+ const check = updated.length <= QUICK_CHECK_MAX_CHARS ? quickCheck(updated, path.extname(target)) : null;
74
+
75
+ if (check && !check.ok) out += `\ncheck: ${check.message}`;
76
+ const refs = await changedDeclarationRefs(cwd, target, original, updated, diff, signal);
77
+
78
+ if (refs) out += `\n${refs}`;
79
+
80
+ return out;
81
+ }
82
+
83
+ function lineText(text, lines, number) {
84
+ if (lines) return lines[number - 1] ?? "";
85
+ const { start, end } = lineTextRange(text, number);
86
+
87
+ return text.slice(start, end).replace(/\r?\n$/, "");
88
+ }
89
+
90
+ function nameAtDiffLine(l, original, updated, oldLines, newLines, canMapOwners, target, spans) {
91
+ const number = l.type === "remove" ? l.lineNum : l.newLineNum ?? l.lineNum;
92
+ const source = l.type === "remove" ? original : updated;
93
+ const cached = l.type === "remove" ? oldLines : newLines;
94
+ const name = declaredName(lineText(source, cached, number));
95
+
96
+ if (name) return name;
97
+ if (!canMapOwners) return;
98
+ if (!spans.has(l.type)) spans.set(l.type, WorkspaceIndex.spansOf(WorkspaceIndex.fromText(target, source)));
99
+
100
+ return spans.get(l.type).find(span => span.start <= number && number <= span.end)?.name;
101
+ }
102
+
103
+ function collectChangedNames(target, original, updated, diff) {
104
+ const canMapOwners = original.length <= 512 * 1024 && updated.length <= 512 * 1024;
105
+ const oldLines = canMapOwners ? original.split("\n") : null;
106
+ const newLines = canMapOwners ? updated.split("\n") : null;
107
+ const names = new Set();
108
+ const spans = new Map();
109
+
110
+ for (const l of diff.lines) {
111
+ if (l.type === "context") continue;
112
+ const name = nameAtDiffLine(l, original, updated, oldLines, newLines, canMapOwners, target, spans);
113
+
114
+ if (name) names.add(name);
115
+ if (names.size >= 3) break;
116
+ }
117
+
118
+ return names;
119
+ }
120
+
121
+ function formatNameRefs(references, incomplete) {
122
+ const parts = [];
123
+
124
+ for (const [name, refs] of references) {
125
+ if (refs.length) parts.push(name + " also referenced in " + refs.slice(0, 6).join(", ") + (refs.length > 6 ? " (more matches)" : ""));
126
+ }
127
+
128
+ if (incomplete) parts.push("references incomplete: search budget reached");
129
+
130
+ return parts.join("\n");
131
+ }
132
+
133
+ async function changedDeclarationRefs(cwd, target, original, updated, diff, signal) {
134
+ const names = collectChangedNames(target, original, updated, diff);
135
+
136
+ if (names.size === 0) return "";
137
+
138
+ try {
139
+ const { references, incomplete } = await referencesForNames({ root: cwd, names: [...names].slice(0, 3),
140
+ excludePath: target, overlayText: file => vfs.getOverlay(file), pendingPaths: vfs.getOverlayPaths(), signal });
141
+
142
+ return formatNameRefs(references, incomplete);
143
+ } catch (error) {
144
+ signal?.throwIfAborted();
145
+
146
+ return "references unavailable: " + error.message;
147
+ }
148
+ }
149
+
150
+ async function commitEdit(cwd, target, original, updated, diff, signal, span) {
151
+ const { speculative } = await vfs.write(target, updated);
152
+ index.touch(relativeSlash(cwd, target));
153
+ const summary = await editSummary(cwd, target, original, updated, diff, signal, span);
154
+
155
+ return textResult(summary, { path: target, speculative, diff });
156
+ }
157
+
158
+ async function applyViewEdit(cwd, target, content, params, signal) {
159
+ const viewText = String(params.viewText);
160
+ const nextText = String(params.newText);
161
+ const windowNext = isString(params.oldText)
162
+ ? applyReplacements(target, viewText, [{ oldText: String(params.oldText), newText: nextText }]).updated
163
+ : nextText;
164
+ const { updated } = applyViewReplace(target, content, params.viewStart, params.viewEnd, viewText, windowNext);
165
+ const diffFrom = isString(params.oldText) ? String(params.oldText) : viewText;
166
+ const diff = shiftDiffLines(buildEditDiff(target, viewText, diffFrom, isString(params.oldText) ? nextText : windowNext), params.viewStart - 1);
167
+ const spanEnd = params.viewStart + Math.max(sourceLines(windowNext).length, 1) - 1;
168
+
169
+ return commitEdit(cwd, target, content, updated, diff, signal, { start: params.viewStart, end: spanEnd });
170
+ }
171
+
172
+ function diffForMatches(target, content, updated, matches) {
173
+ if (content.length > 512 * 1024 || updated.length > 512 * 1024) return boundedEditDiff(target, content, matches);
174
+ if (matches.length === 1) return buildEditDiff(target, content, matches[0].oldText, matches[0].newText);
175
+
176
+ return buildMultiEditDiff(target, content, matches);
177
+ }
178
+
179
+ async function edit(params, signal) {
180
+ const cwd = getCwd();
181
+ const target = await resolveWorkspacePath(cwd, params?.path, "edit", false);
182
+
183
+ if (signal?.aborted) throw new Error("aborted");
184
+ const content = await vfs.read(target, { maxBytes: 64 * 1024 * 1024 });
185
+
186
+ if (isNumber(params?.viewStart) && isNumber(params?.viewEnd) && isString(params?.viewText) && isString(params?.newText)) {
187
+ return applyViewEdit(cwd, target, content, params, signal);
188
+ }
189
+
190
+ const requestedEdits = Array.isArray(params?.edits) ? params.edits : [{ oldText: params?.oldText, newText: params?.newText }];
191
+ const { updated, matches } = applyReplacements(target, content, requestedEdits);
192
+
193
+ return commitEdit(cwd, target, content, updated, diffForMatches(target, content, updated, matches), signal);
194
+ }
195
+
196
+ function patchInputPath(params) {
197
+ let inputPath = params?.path;
198
+
199
+ if (!inputPath && isString(params?.patch)) {
200
+ for (const match of params.patch.matchAll(/^(?:---|\+\+\+)\s+([^\t\n]+)/gm)) {
201
+ const candidate = match[1].trim().replace(/^[ab]\//, "");
202
+
203
+ if (candidate !== "/dev/null") return candidate;
204
+ }
205
+ }
206
+
207
+ return inputPath;
208
+ }
209
+
210
+ async function readPatchOriginal(target) {
211
+ try {
212
+ return await vfs.read(target, { maxBytes: 64 * 1024 * 1024, preserveRead: true });
213
+ } catch (error) {
214
+ if (error?.code !== "ENOENT") throw error;
215
+
216
+ return "";
217
+ }
218
+ }
219
+
220
+ async function apply_patch(params, signal) {
221
+ const cwd = getCwd();
222
+ const target = await resolveWorkspacePath(cwd, patchInputPath(params), "apply_patch", false);
223
+
224
+ if (!isString(params?.patch) || !params.patch.trim()) {
225
+ throw new Error("apply_patch requires patch");
226
+ }
227
+
228
+ if (signal?.aborted) throw new Error("aborted");
229
+ const original = await readPatchOriginal(target);
230
+ if (original.length > 2 * 1024 * 1024) throw new Error("apply_patch input exceeds 2 MiB; use edit() for targeted replacements");
231
+ const { resultText, hunkCount, relocations } = applyPatchToText(original, params.patch);
232
+ const { speculative } = await vfs.write(target, resultText);
233
+ const diff = buildPatchDiff(target, params.patch, relocations);
234
+ index.touch(relativeSlash(cwd, target));
235
+ let summary = await editSummary(cwd, target, original, resultText, diff, signal);
236
+
237
+ if (relocations.length) summary += "\nrelocated " + relocations.map(entry => "#" + entry.hunk + " " + (entry.offset > 0 ? "+" : "") + entry.offset + " lines").join(", ");
238
+
239
+ return textResult(summary, {
240
+ path: target,
241
+ hunks: hunkCount,
242
+ speculative,
243
+ diff,
244
+ relocated: relocations,
245
+ });
246
+ }
247
+
248
+ return { edit, apply_patch, editSummary };
249
+ }
@@ -0,0 +1,31 @@
1
+ export const IMAGE_MAX_BYTES = 20 * 1024 * 1024;
2
+ export const LARGE_FILE_BYTES = 512 * 1024;
3
+
4
+ /** Raw path-only reads above these must use json/about/offset/complete. */
5
+ export const RAW_JSON_CHARS = 4096;
6
+
7
+ export const RAW_SOURCE_CHARS = 8192;
8
+
9
+ export const RAW_SOURCE_LINES = 160;
10
+
11
+ /** Routing responses above this fall back to the bound error instead of dumping. */
12
+ export const ROUTING_MAX_CHARS = 4096;
13
+ export const ABOUT_TOKEN_MAX = 16;
14
+ export const IMAGE_MIME = {
15
+ ".png": "image/png",
16
+ ".jpg": "image/jpeg",
17
+ ".jpeg": "image/jpeg",
18
+ ".gif": "image/gif",
19
+ ".webp": "image/webp",
20
+ ".bmp": "image/bmp",
21
+ };
22
+
23
+ export function imageTooLarge(rel, size) {
24
+ return new Error("image " + rel + " is " + size + " bytes (" + (size / 1024 / 1024).toFixed(1) + " MiB); the image read limit is " + IMAGE_MAX_BYTES + " bytes (20 MiB); resize or select fewer/smaller images");
25
+ }
26
+
27
+ export function missingFile(targetPath) {
28
+ const error = new Error("no such file: " + targetPath + " (locate it with read using a directory path or source question)");
29
+ error.code = "ENOENT";
30
+ return error;
31
+ }
@@ -0,0 +1,31 @@
1
+ import { createNativeScheduler } from "../runtime/parallel.js";
2
+ import { createRead } from "./read.js";
3
+ import { createWrite } from "./write.js";
4
+ import { createEdit } from "./edit.js";
5
+ import { createBash } from "./bash.js";
6
+ import { createList } from "./list.js";
7
+
8
+ export function createNativeAdapters(getCwd, vfs, config, index, ledger, hooks) {
9
+ const ctx = { getCwd, vfs, config, index, ledger, hooks, reads: createNativeScheduler() };
10
+ const read = createRead(ctx);
11
+ ctx.readDirectory = read.readDirectory;
12
+ const write = createWrite(ctx);
13
+ const edit = createEdit(ctx);
14
+ const bash = createBash(ctx);
15
+ const list = createList(ctx);
16
+ hooks.summarizeEdit = edit.editSummary;
17
+ return {
18
+ read: read.read,
19
+ write: write.write,
20
+ edit: edit.edit,
21
+ apply_patch: edit.apply_patch,
22
+ snap: read.snap,
23
+ evidence: read.evidence,
24
+ surface: read.surface,
25
+ bash: bash.bash,
26
+ grep: list.grep,
27
+ glob: list.glob,
28
+ find: list.find,
29
+ ls: list.ls,
30
+ };
31
+ }