indusagi 0.12.34 → 0.13.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 (55) hide show
  1. package/dist/agent.js +1247 -184
  2. package/dist/ai.js +72 -4
  3. package/dist/capabilities.js +69 -2
  4. package/dist/cli.js +83 -13
  5. package/dist/connectors-saas.js +66 -0
  6. package/dist/index.js +83 -13
  7. package/dist/interop.js +66 -0
  8. package/dist/mcp.js +270 -363
  9. package/dist/react-ink.js +15 -11
  10. package/dist/shell-app.js +83 -13
  11. package/dist/smithy.js +69 -2
  12. package/dist/swarm.js +69 -2
  13. package/dist/types/capabilities/backends/node-backends.d.ts +3 -1
  14. package/dist/types/capabilities/files/read-state-gate.d.ts +69 -0
  15. package/dist/types/capabilities/files/read-state-gate.test.d.ts +14 -0
  16. package/dist/types/capabilities/kernel/context.d.ts +4 -0
  17. package/dist/types/capabilities/kernel/index.d.ts +2 -2
  18. package/dist/types/capabilities/kernel/spec.d.ts +55 -0
  19. package/dist/types/facade/bot/actions/bash.d.ts +15 -0
  20. package/dist/types/facade/bot/actions/bash.test.d.ts +1 -0
  21. package/dist/types/facade/bot/actions/checkpoint.d.ts +49 -0
  22. package/dist/types/facade/bot/actions/checkpoint.test.d.ts +1 -0
  23. package/dist/types/facade/bot/actions/edit-utils.d.ts +86 -0
  24. package/dist/types/facade/bot/actions/edit.d.ts +18 -0
  25. package/dist/types/facade/bot/actions/edit.test.d.ts +1 -0
  26. package/dist/types/facade/bot/actions/find.d.ts +2 -0
  27. package/dist/types/facade/bot/actions/find.test.d.ts +1 -0
  28. package/dist/types/facade/bot/actions/grep.d.ts +10 -0
  29. package/dist/types/facade/bot/actions/grep.test.d.ts +1 -0
  30. package/dist/types/facade/bot/actions/index.d.ts +16 -0
  31. package/dist/types/facade/bot/actions/read-state.d.ts +83 -0
  32. package/dist/types/facade/bot/actions/read-state.test.d.ts +1 -0
  33. package/dist/types/facade/bot/actions/read.d.ts +7 -0
  34. package/dist/types/facade/bot/actions/read.test.d.ts +1 -0
  35. package/dist/types/facade/bot/actions/sandbox-backend.d.ts +99 -0
  36. package/dist/types/facade/bot/actions/sandbox-backend.test.d.ts +1 -0
  37. package/dist/types/facade/bot/actions/websearch.d.ts +5 -2
  38. package/dist/types/facade/bot/actions/websearch.test.d.ts +1 -0
  39. package/dist/types/facade/bot/actions/write.d.ts +15 -0
  40. package/dist/types/facade/bot/agent-loop.d.ts +10 -0
  41. package/dist/types/facade/bot/agent-loop.test.d.ts +1 -0
  42. package/dist/types/facade/bot/agent.d.ts +9 -1
  43. package/dist/types/facade/bot/permission-gate.test.d.ts +1 -0
  44. package/dist/types/facade/bot/types.d.ts +60 -0
  45. package/dist/types/facade/mcp-core/client.d.ts +71 -15
  46. package/dist/types/facade/mcp-core/client.test.d.ts +18 -0
  47. package/dist/types/facade/mcp-core/types.d.ts +10 -0
  48. package/dist/types/facade/ml/adapters/anthropic-retry.test.d.ts +1 -0
  49. package/dist/types/facade/ml/adapters/anthropic.d.ts +17 -0
  50. package/dist/types/facade/ml/adapters/simple-options.d.ts +13 -0
  51. package/dist/types/facade/ml/adapters/simple-options.test.d.ts +1 -0
  52. package/dist/types/react-ink/components/StatusLine.d.ts +10 -1
  53. package/dist/types/react-ink/components/ToolEventBlock.d.ts +2 -1
  54. package/dist/types/react-ink/components/ToolEventBlock.test.d.ts +1 -0
  55. package/package.json +1 -1
package/dist/ai.js CHANGED
@@ -13199,24 +13199,58 @@ function normalizeProviderError(error) {
13199
13199
  }
13200
13200
  return new SimpleOptionsProviderError("Unknown provider error", "unknown", error);
13201
13201
  }
13202
+ function abortReason(signal) {
13203
+ const reason = signal.reason;
13204
+ if (reason instanceof SimpleOptionsProviderError) return reason;
13205
+ if (reason instanceof Error) return new SimpleOptionsProviderError(reason.message, "unknown", reason);
13206
+ return new SimpleOptionsProviderError("Request was aborted", "unknown", reason);
13207
+ }
13208
+ function abortableSleep(ms, signal) {
13209
+ return new Promise((resolve, reject) => {
13210
+ if (signal?.aborted) {
13211
+ reject(abortReason(signal));
13212
+ return;
13213
+ }
13214
+ let onAbort;
13215
+ const timer = setTimeout(() => {
13216
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
13217
+ resolve();
13218
+ }, ms);
13219
+ if (signal) {
13220
+ onAbort = () => {
13221
+ clearTimeout(timer);
13222
+ signal.removeEventListener("abort", onAbort);
13223
+ reject(abortReason(signal));
13224
+ };
13225
+ signal.addEventListener("abort", onAbort, { once: true });
13226
+ }
13227
+ });
13228
+ }
13202
13229
  async function executeWithRetry(operation, policy) {
13203
13230
  let attempt = 0;
13204
13231
  let lastError;
13205
13232
  while (attempt < policy.maxAttempts) {
13233
+ if (policy.signal?.aborted) {
13234
+ throw abortReason(policy.signal);
13235
+ }
13206
13236
  attempt++;
13207
13237
  try {
13208
13238
  return await operation();
13209
13239
  } catch (error) {
13210
13240
  lastError = error;
13211
13241
  const normalized = normalizeProviderError(error);
13242
+ if (policy.signal?.aborted) {
13243
+ throw normalized;
13244
+ }
13212
13245
  const defaultRetryable = normalized.code === "rate_limit" || normalized.code === "timeout" || normalized.code === "network";
13213
13246
  const retryable = policy.shouldRetry ? policy.shouldRetry(error, attempt) : defaultRetryable;
13214
13247
  if (!retryable || attempt >= policy.maxAttempts) {
13215
13248
  throw normalized;
13216
13249
  }
13217
13250
  const maxDelay = policy.maxDelayMs ?? Number.MAX_SAFE_INTEGER;
13218
- const backoff = Math.min(policy.baseDelayMs * 2 ** (attempt - 1), maxDelay);
13219
- await new Promise((resolve) => setTimeout(resolve, backoff));
13251
+ const retryAfterMs = policy.getRetryAfterMs?.(error) ?? null;
13252
+ const backoff = retryAfterMs != null ? Math.min(retryAfterMs, maxDelay) : Math.min(policy.baseDelayMs * 2 ** (attempt - 1), maxDelay);
13253
+ await abortableSleep(backoff, policy.signal);
13220
13254
  }
13221
13255
  }
13222
13256
  throw normalizeProviderError(lastError);
@@ -13692,6 +13726,30 @@ var AnthropicEventReducer = class {
13692
13726
  calculateCost(this.model, this.output.usage);
13693
13727
  }
13694
13728
  };
13729
+ function parseAnthropicRetryAfterMs(error) {
13730
+ if (error instanceof Anthropic.APIError) {
13731
+ const header = error.headers?.get?.("retry-after");
13732
+ const seconds = header ? parseInt(header, 10) : Number.NaN;
13733
+ if (!Number.isNaN(seconds) && seconds >= 0) return seconds * 1e3;
13734
+ }
13735
+ return null;
13736
+ }
13737
+ function shouldRetryAnthropic(error, _attempt) {
13738
+ if (error instanceof Anthropic.APIError) {
13739
+ const status = error.status;
13740
+ if (status === 408 || status === 409 || status === 429 || status === 529) return true;
13741
+ if (typeof status === "number" && status >= 500) return true;
13742
+ if (typeof status === "number" && status >= 400 && status < 500) return false;
13743
+ const body = `${error.message} ${JSON.stringify(error.error ?? "")}`.toLowerCase();
13744
+ if (body.includes('"type":"overloaded_error"') || body.includes("overloaded")) return true;
13745
+ return false;
13746
+ }
13747
+ if (error instanceof Error) {
13748
+ const msg = error.message.toLowerCase();
13749
+ return msg.includes("rate limit") || msg.includes("rate_limit") || msg.includes("timeout") || msg.includes("timed out") || msg.includes("network") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("overloaded");
13750
+ }
13751
+ return false;
13752
+ }
13695
13753
  var streamAnthropic = (model, context, options) => {
13696
13754
  const stream2 = new AssistantMessageEventStream();
13697
13755
  const output = createAssistantMessageOutput(model);
@@ -13719,8 +13777,16 @@ var streamAnthropic = (model, context, options) => {
13719
13777
  });
13720
13778
  },
13721
13779
  {
13722
- maxAttempts: options?.signal ? 1 : 3,
13723
- baseDelayMs: 250
13780
+ // Transient retries are NO LONGER gated on signal presence:
13781
+ // the live path always carries a signal, so the old ternary
13782
+ // collapsed to a single attempt = zero retries on 429/529/5xx.
13783
+ // Abort still short-circuits immediately (see executeWithRetry).
13784
+ maxAttempts: 3,
13785
+ baseDelayMs: 500,
13786
+ maxDelayMs: 32e3,
13787
+ signal: options?.signal,
13788
+ shouldRetry: shouldRetryAnthropic,
13789
+ getRetryAfterMs: parseAnthropicRetryAfterMs
13724
13790
  }
13725
13791
  );
13726
13792
  if (options?.signal?.aborted) {
@@ -19700,6 +19766,7 @@ export {
19700
19766
  normalizeToolCallId,
19701
19767
  normalizeToolName,
19702
19768
  openaiCodexOAuthProvider,
19769
+ parseAnthropicRetryAfterMs,
19703
19770
  parseStreamingJson,
19704
19771
  parseStreamingJsonWithDiagnostics,
19705
19772
  processResponsesStream,
@@ -19719,6 +19786,7 @@ export {
19719
19786
  retainThoughtSignature,
19720
19787
  rotateEnvApiKey,
19721
19788
  sanitizeContentString,
19789
+ shouldRetryAnthropic,
19722
19790
  stream,
19723
19791
  streamAnthropic,
19724
19792
  streamAzureOpenAIResponses,
@@ -1,4 +1,5 @@
1
1
  // src/capabilities/kernel/spec.ts
2
+ var READ_STATE_HANDLE_KEY = "readState";
2
3
  function coerceInput(raw) {
3
4
  if (typeof raw === "string") {
4
5
  const trimmed = raw.trim();
@@ -452,7 +453,7 @@ var standardBudget = {
452
453
  [${omitted} bytes elided to stay within the output ceiling]
453
454
  `
454
455
  };
455
- function makeNodeContext(cwd, signal, budget) {
456
+ function makeNodeContext(cwd, signal, budget, framework) {
456
457
  if (typeof cwd !== "string" || cwd.length === 0) {
457
458
  throw new Error("makeNodeContext requires a non-empty working directory.");
458
459
  }
@@ -461,10 +462,60 @@ function makeNodeContext(cwd, signal, budget) {
461
462
  fs: nodeFs,
462
463
  shell: nodeShell,
463
464
  signal: signal ?? neverAborts(),
464
- budget: budget ?? standardBudget
465
+ budget: budget ?? standardBudget,
466
+ ...framework ? { framework } : {}
465
467
  };
466
468
  }
467
469
 
470
+ // src/capabilities/files/read-state-gate.ts
471
+ var READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
472
+ var MODIFIED_SINCE_READ_MESSAGE = "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.";
473
+ function getReadStateHandle(ctx) {
474
+ const bag = ctx.framework;
475
+ if (!bag || typeof bag !== "object") return void 0;
476
+ const candidate = bag[READ_STATE_HANDLE_KEY];
477
+ if (!candidate || typeof candidate !== "object") return void 0;
478
+ const handle = candidate;
479
+ if (typeof handle.get !== "function" || typeof handle.set !== "function" || typeof handle.has !== "function") {
480
+ return void 0;
481
+ }
482
+ return handle;
483
+ }
484
+ async function recordReadState(ctx, absPath, handle) {
485
+ if (!handle) return;
486
+ try {
487
+ const info = await ctx.fs.stat(absPath);
488
+ const record = {
489
+ mtimeMs: Math.floor(info.modifiedMs),
490
+ size: info.size,
491
+ readAt: Date.now()
492
+ };
493
+ handle.set(absPath, record);
494
+ } catch {
495
+ }
496
+ }
497
+ async function enforceReadGate(ctx, absPath, handle) {
498
+ if (!handle) return { ok: true };
499
+ if (!handle.has(absPath)) {
500
+ return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
501
+ }
502
+ const recorded = handle.get(absPath);
503
+ if (!recorded) {
504
+ return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
505
+ }
506
+ let info;
507
+ try {
508
+ info = await ctx.fs.stat(absPath);
509
+ } catch {
510
+ return { ok: true };
511
+ }
512
+ const currentMtime = Math.floor(info.modifiedMs);
513
+ if (currentMtime > recorded.mtimeMs || info.size !== recorded.size) {
514
+ return { ok: false, message: MODIFIED_SINCE_READ_MESSAGE };
515
+ }
516
+ return { ok: true };
517
+ }
518
+
468
519
  // src/capabilities/files/read.ts
469
520
  var GUTTER_WIDTH = 6;
470
521
  var DESCRIPTION = [
@@ -567,6 +618,7 @@ var readTool = defineTool({
567
618
  const detail = err instanceof Error ? err.message : String(err);
568
619
  return failure(`Could not read ${path}: ${detail}`);
569
620
  }
621
+ await recordReadState(ctx, path, getReadStateHandle(ctx));
570
622
  const allLines = toLines(text);
571
623
  const totalLines = allLines.length;
572
624
  if (totalLines === 0) {
@@ -674,11 +726,19 @@ var writeTool = defineTool({
674
726
  async run(input, ctx) {
675
727
  const path = readPath(input?.path);
676
728
  const content = readContent(input?.content);
729
+ const handle = getReadStateHandle(ctx);
730
+ if (handle && await ctx.fs.exists(path)) {
731
+ const gate = await enforceReadGate(ctx, path, handle);
732
+ if (!gate.ok) {
733
+ return asText(gate.message, true);
734
+ }
735
+ }
677
736
  const folder = parentDir(path);
678
737
  if (folder.length > 0) {
679
738
  await ctx.fs.mkdir(folder, { recursive: true });
680
739
  }
681
740
  await ctx.fs.writeFile(path, content, "utf8");
741
+ await recordReadState(ctx, path, handle);
682
742
  const bytes = Buffer.byteLength(content, "utf8");
683
743
  const unit = bytes === 1 ? "byte" : "bytes";
684
744
  return asText(`Saved ${bytes} ${unit} to ${path}.`);
@@ -968,6 +1028,11 @@ async function runEdit(input, ctx) {
968
1028
  if (!info.isFile) {
969
1029
  return failure2(`${path} is not a regular file, so it cannot be edited.`);
970
1030
  }
1031
+ const handle = getReadStateHandle(ctx);
1032
+ const gate = await enforceReadGate(ctx, path, handle);
1033
+ if (!gate.ok) {
1034
+ return failure2(gate.message);
1035
+ }
971
1036
  const before = await ctx.fs.readFile(path, "utf8");
972
1037
  const literalHits = countLiteral(before, oldText);
973
1038
  if (literalHits > 0) {
@@ -981,6 +1046,7 @@ async function runEdit(input, ctx) {
981
1046
  return failure2(`The replacement left ${path} unchanged.`);
982
1047
  }
983
1048
  await ctx.fs.writeFile(path, after2, "utf8");
1049
+ await recordReadState(ctx, path, handle);
984
1050
  return success(path, before, after2, replaceAll ? literalHits : 1);
985
1051
  }
986
1052
  const spans = findFuzzySpans(before, oldText);
@@ -1004,6 +1070,7 @@ async function runEdit(input, ctx) {
1004
1070
  return failure2(`The fuzzy replacement left ${path} unchanged.`);
1005
1071
  }
1006
1072
  await ctx.fs.writeFile(path, after, "utf8");
1073
+ await recordReadState(ctx, path, handle);
1007
1074
  return success(path, before, after, targets.length);
1008
1075
  }
1009
1076
  var editTool = defineTool({
package/dist/cli.js CHANGED
@@ -4637,6 +4637,7 @@ function runErrorThrowable(kind, message) {
4637
4637
  }
4638
4638
 
4639
4639
  // src/capabilities/kernel/spec.ts
4640
+ var READ_STATE_HANDLE_KEY = "readState";
4640
4641
  function coerceInput(raw) {
4641
4642
  if (typeof raw === "string") {
4642
4643
  const trimmed = raw.trim();
@@ -5090,7 +5091,7 @@ var standardBudget = {
5090
5091
  [${omitted} bytes elided to stay within the output ceiling]
5091
5092
  `
5092
5093
  };
5093
- function makeNodeContext(cwd, signal, budget) {
5094
+ function makeNodeContext(cwd, signal, budget, framework) {
5094
5095
  if (typeof cwd !== "string" || cwd.length === 0) {
5095
5096
  throw new Error("makeNodeContext requires a non-empty working directory.");
5096
5097
  }
@@ -5099,10 +5100,60 @@ function makeNodeContext(cwd, signal, budget) {
5099
5100
  fs: nodeFs,
5100
5101
  shell: nodeShell,
5101
5102
  signal: signal ?? neverAborts(),
5102
- budget: budget ?? standardBudget
5103
+ budget: budget ?? standardBudget,
5104
+ ...framework ? { framework } : {}
5103
5105
  };
5104
5106
  }
5105
5107
 
5108
+ // src/capabilities/files/read-state-gate.ts
5109
+ var READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
5110
+ var MODIFIED_SINCE_READ_MESSAGE = "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.";
5111
+ function getReadStateHandle(ctx) {
5112
+ const bag = ctx.framework;
5113
+ if (!bag || typeof bag !== "object") return void 0;
5114
+ const candidate = bag[READ_STATE_HANDLE_KEY];
5115
+ if (!candidate || typeof candidate !== "object") return void 0;
5116
+ const handle = candidate;
5117
+ if (typeof handle.get !== "function" || typeof handle.set !== "function" || typeof handle.has !== "function") {
5118
+ return void 0;
5119
+ }
5120
+ return handle;
5121
+ }
5122
+ async function recordReadState(ctx, absPath, handle) {
5123
+ if (!handle) return;
5124
+ try {
5125
+ const info = await ctx.fs.stat(absPath);
5126
+ const record = {
5127
+ mtimeMs: Math.floor(info.modifiedMs),
5128
+ size: info.size,
5129
+ readAt: Date.now()
5130
+ };
5131
+ handle.set(absPath, record);
5132
+ } catch {
5133
+ }
5134
+ }
5135
+ async function enforceReadGate(ctx, absPath, handle) {
5136
+ if (!handle) return { ok: true };
5137
+ if (!handle.has(absPath)) {
5138
+ return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
5139
+ }
5140
+ const recorded = handle.get(absPath);
5141
+ if (!recorded) {
5142
+ return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
5143
+ }
5144
+ let info;
5145
+ try {
5146
+ info = await ctx.fs.stat(absPath);
5147
+ } catch {
5148
+ return { ok: true };
5149
+ }
5150
+ const currentMtime = Math.floor(info.modifiedMs);
5151
+ if (currentMtime > recorded.mtimeMs || info.size !== recorded.size) {
5152
+ return { ok: false, message: MODIFIED_SINCE_READ_MESSAGE };
5153
+ }
5154
+ return { ok: true };
5155
+ }
5156
+
5106
5157
  // src/capabilities/files/read.ts
5107
5158
  var GUTTER_WIDTH = 6;
5108
5159
  var DESCRIPTION = [
@@ -5205,6 +5256,7 @@ var readTool = defineTool({
5205
5256
  const detail = err instanceof Error ? err.message : String(err);
5206
5257
  return failure(`Could not read ${path2}: ${detail}`);
5207
5258
  }
5259
+ await recordReadState(ctx, path2, getReadStateHandle(ctx));
5208
5260
  const allLines = toLines(text);
5209
5261
  const totalLines = allLines.length;
5210
5262
  if (totalLines === 0) {
@@ -5312,11 +5364,19 @@ var writeTool = defineTool({
5312
5364
  async run(input, ctx) {
5313
5365
  const path2 = readPath(input?.path);
5314
5366
  const content = readContent(input?.content);
5367
+ const handle = getReadStateHandle(ctx);
5368
+ if (handle && await ctx.fs.exists(path2)) {
5369
+ const gate = await enforceReadGate(ctx, path2, handle);
5370
+ if (!gate.ok) {
5371
+ return asText(gate.message, true);
5372
+ }
5373
+ }
5315
5374
  const folder = parentDir(path2);
5316
5375
  if (folder.length > 0) {
5317
5376
  await ctx.fs.mkdir(folder, { recursive: true });
5318
5377
  }
5319
5378
  await ctx.fs.writeFile(path2, content, "utf8");
5379
+ await recordReadState(ctx, path2, handle);
5320
5380
  const bytes = Buffer.byteLength(content, "utf8");
5321
5381
  const unit = bytes === 1 ? "byte" : "bytes";
5322
5382
  return asText(`Saved ${bytes} ${unit} to ${path2}.`);
@@ -5606,6 +5666,11 @@ async function runEdit(input, ctx) {
5606
5666
  if (!info.isFile) {
5607
5667
  return failure2(`${path2} is not a regular file, so it cannot be edited.`);
5608
5668
  }
5669
+ const handle = getReadStateHandle(ctx);
5670
+ const gate = await enforceReadGate(ctx, path2, handle);
5671
+ if (!gate.ok) {
5672
+ return failure2(gate.message);
5673
+ }
5609
5674
  const before = await ctx.fs.readFile(path2, "utf8");
5610
5675
  const literalHits = countLiteral(before, oldText);
5611
5676
  if (literalHits > 0) {
@@ -5619,6 +5684,7 @@ async function runEdit(input, ctx) {
5619
5684
  return failure2(`The replacement left ${path2} unchanged.`);
5620
5685
  }
5621
5686
  await ctx.fs.writeFile(path2, after2, "utf8");
5687
+ await recordReadState(ctx, path2, handle);
5622
5688
  return success(path2, before, after2, replaceAll ? literalHits : 1);
5623
5689
  }
5624
5690
  const spans = findFuzzySpans(before, oldText);
@@ -5642,6 +5708,7 @@ async function runEdit(input, ctx) {
5642
5708
  return failure2(`The fuzzy replacement left ${path2} unchanged.`);
5643
5709
  }
5644
5710
  await ctx.fs.writeFile(path2, after, "utf8");
5711
+ await recordReadState(ctx, path2, handle);
5645
5712
  return success(path2, before, after, targets.length);
5646
5713
  }
5647
5714
  var editTool = defineTool({
@@ -10373,6 +10440,9 @@ function statusMarker(status) {
10373
10440
  return ">";
10374
10441
  }
10375
10442
  }
10443
+ function statusColorKey(status) {
10444
+ return status === "error" ? "error" : "text";
10445
+ }
10376
10446
  function plainToolText(text) {
10377
10447
  return stripAnsi5(text);
10378
10448
  }
@@ -10414,7 +10484,7 @@ function ToolEventBlock({
10414
10484
  showTitle = true,
10415
10485
  status,
10416
10486
  summary,
10417
- theme: _theme,
10487
+ theme,
10418
10488
  title
10419
10489
  }) {
10420
10490
  const normalizedSummary = summary?.trim();
@@ -10430,19 +10500,19 @@ function ToolEventBlock({
10430
10500
  const { visibleLines, hiddenLineCount } = clampContentLines(combinedLines, maxContentLines);
10431
10501
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom, marginLeft: indent, children: [
10432
10502
  showTitle ? /* @__PURE__ */ jsxs(Box, { children: [
10433
- /* @__PURE__ */ jsx(Text, { color: "white", children: plainToolText(`${statusMarker(status)} `) }),
10434
- /* @__PURE__ */ jsx(Text, { color: "white", children: plainToolText(title) }),
10435
- showSummaryInline && normalizedSummary ? /* @__PURE__ */ jsx(Text, { color: "white", children: plainToolText(` (${normalizedSummary})`) }) : null
10503
+ /* @__PURE__ */ jsx(Text, { children: theme.color(statusColorKey(status), plainToolText(`${statusMarker(status)} `)) }),
10504
+ /* @__PURE__ */ jsx(Text, { children: theme.color(statusColorKey(status), plainToolText(title)) }),
10505
+ showSummaryInline && normalizedSummary ? /* @__PURE__ */ jsx(Text, { children: theme.muted(plainToolText(` (${normalizedSummary})`)) }) : null
10436
10506
  ] }) : null,
10437
10507
  visibleLines.map((line, index) => /* @__PURE__ */ jsx(
10438
10508
  Box,
10439
10509
  {
10440
10510
  marginLeft: line.kind === "response" ? showTitle ? 2 : 0 : showTitle ? 4 : 2,
10441
- children: preformatted ? /* @__PURE__ */ jsx(Text, { children: line.text }) : /* @__PURE__ */ jsx(Text, { color: "white", children: plainToolText(line.text) })
10511
+ children: preformatted ? /* @__PURE__ */ jsx(Text, { children: line.text }) : /* @__PURE__ */ jsx(Text, { children: theme.color("text", plainToolText(line.text)) })
10442
10512
  },
10443
10513
  `${line.kind}:${index}:${stripAnsi5(line.text)}`
10444
10514
  )),
10445
- hiddenLineCount > 0 ? /* @__PURE__ */ jsx(Box, { marginLeft: showTitle ? 4 : 2, children: /* @__PURE__ */ jsx(Text, { color: "white", children: plainToolText(`... ${hiddenLineCount} more line(s)`) }) }) : null
10515
+ hiddenLineCount > 0 ? /* @__PURE__ */ jsx(Box, { marginLeft: showTitle ? 4 : 2, children: /* @__PURE__ */ jsx(Text, { children: theme.muted(plainToolText(`... ${hiddenLineCount} more line(s)`)) }) }) : null
10446
10516
  ] });
10447
10517
  }
10448
10518
 
@@ -10785,20 +10855,20 @@ function MessageList({
10785
10855
  }
10786
10856
 
10787
10857
  // src/react-ink/components/StatusLine.tsx
10788
- function StatusLine({ snapshot, status, theme }) {
10858
+ function StatusLine({ snapshot, status, theme, showBusyText = true }) {
10789
10859
  let text = status?.text;
10790
10860
  let tone = status?.kind ?? "info";
10791
10861
  if (!text) {
10792
- if (snapshot.isCompacting) {
10862
+ if (showBusyText && snapshot.isCompacting) {
10793
10863
  text = "Compacting conversation context...";
10794
10864
  tone = "busy";
10795
- } else if (snapshot.isBashRunning) {
10865
+ } else if (showBusyText && snapshot.isBashRunning) {
10796
10866
  text = "Running bash command...";
10797
10867
  tone = "busy";
10798
- } else if (snapshot.pendingToolCallCount > 0 || snapshot.pendingMessageCount > 0) {
10868
+ } else if (showBusyText && (snapshot.pendingToolCallCount > 0 || snapshot.pendingMessageCount > 0)) {
10799
10869
  text = "Agent working...";
10800
10870
  tone = "busy";
10801
- } else if (snapshot.isStreaming) {
10871
+ } else if (showBusyText && snapshot.isStreaming) {
10802
10872
  text = "Agent working...";
10803
10873
  tone = "busy";
10804
10874
  } else if (snapshot.error) {
@@ -1,4 +1,5 @@
1
1
  // src/capabilities/kernel/spec.ts
2
+ var READ_STATE_HANDLE_KEY = "readState";
2
3
  function coerceInput(raw) {
3
4
  if (typeof raw === "string") {
4
5
  const trimmed = raw.trim();
@@ -450,6 +451,55 @@ var standardBudget = {
450
451
  `
451
452
  };
452
453
 
454
+ // src/capabilities/files/read-state-gate.ts
455
+ var READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
456
+ var MODIFIED_SINCE_READ_MESSAGE = "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.";
457
+ function getReadStateHandle(ctx) {
458
+ const bag = ctx.framework;
459
+ if (!bag || typeof bag !== "object") return void 0;
460
+ const candidate = bag[READ_STATE_HANDLE_KEY];
461
+ if (!candidate || typeof candidate !== "object") return void 0;
462
+ const handle = candidate;
463
+ if (typeof handle.get !== "function" || typeof handle.set !== "function" || typeof handle.has !== "function") {
464
+ return void 0;
465
+ }
466
+ return handle;
467
+ }
468
+ async function recordReadState(ctx, absPath, handle) {
469
+ if (!handle) return;
470
+ try {
471
+ const info = await ctx.fs.stat(absPath);
472
+ const record = {
473
+ mtimeMs: Math.floor(info.modifiedMs),
474
+ size: info.size,
475
+ readAt: Date.now()
476
+ };
477
+ handle.set(absPath, record);
478
+ } catch {
479
+ }
480
+ }
481
+ async function enforceReadGate(ctx, absPath, handle) {
482
+ if (!handle) return { ok: true };
483
+ if (!handle.has(absPath)) {
484
+ return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
485
+ }
486
+ const recorded = handle.get(absPath);
487
+ if (!recorded) {
488
+ return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
489
+ }
490
+ let info;
491
+ try {
492
+ info = await ctx.fs.stat(absPath);
493
+ } catch {
494
+ return { ok: true };
495
+ }
496
+ const currentMtime = Math.floor(info.modifiedMs);
497
+ if (currentMtime > recorded.mtimeMs || info.size !== recorded.size) {
498
+ return { ok: false, message: MODIFIED_SINCE_READ_MESSAGE };
499
+ }
500
+ return { ok: true };
501
+ }
502
+
453
503
  // src/capabilities/files/read.ts
454
504
  var GUTTER_WIDTH = 6;
455
505
  var DESCRIPTION = [
@@ -552,6 +602,7 @@ var readTool = defineTool({
552
602
  const detail = err instanceof Error ? err.message : String(err);
553
603
  return failure(`Could not read ${path}: ${detail}`);
554
604
  }
605
+ await recordReadState(ctx, path, getReadStateHandle(ctx));
555
606
  const allLines = toLines(text);
556
607
  const totalLines = allLines.length;
557
608
  if (totalLines === 0) {
@@ -659,11 +710,19 @@ var writeTool = defineTool({
659
710
  async run(input, ctx) {
660
711
  const path = readPath(input?.path);
661
712
  const content = readContent(input?.content);
713
+ const handle = getReadStateHandle(ctx);
714
+ if (handle && await ctx.fs.exists(path)) {
715
+ const gate = await enforceReadGate(ctx, path, handle);
716
+ if (!gate.ok) {
717
+ return asText(gate.message, true);
718
+ }
719
+ }
662
720
  const folder = parentDir(path);
663
721
  if (folder.length > 0) {
664
722
  await ctx.fs.mkdir(folder, { recursive: true });
665
723
  }
666
724
  await ctx.fs.writeFile(path, content, "utf8");
725
+ await recordReadState(ctx, path, handle);
667
726
  const bytes = Buffer.byteLength(content, "utf8");
668
727
  const unit = bytes === 1 ? "byte" : "bytes";
669
728
  return asText(`Saved ${bytes} ${unit} to ${path}.`);
@@ -953,6 +1012,11 @@ async function runEdit(input, ctx) {
953
1012
  if (!info.isFile) {
954
1013
  return failure2(`${path} is not a regular file, so it cannot be edited.`);
955
1014
  }
1015
+ const handle = getReadStateHandle(ctx);
1016
+ const gate = await enforceReadGate(ctx, path, handle);
1017
+ if (!gate.ok) {
1018
+ return failure2(gate.message);
1019
+ }
956
1020
  const before = await ctx.fs.readFile(path, "utf8");
957
1021
  const literalHits = countLiteral(before, oldText);
958
1022
  if (literalHits > 0) {
@@ -966,6 +1030,7 @@ async function runEdit(input, ctx) {
966
1030
  return failure2(`The replacement left ${path} unchanged.`);
967
1031
  }
968
1032
  await ctx.fs.writeFile(path, after2, "utf8");
1033
+ await recordReadState(ctx, path, handle);
969
1034
  return success(path, before, after2, replaceAll ? literalHits : 1);
970
1035
  }
971
1036
  const spans = findFuzzySpans(before, oldText);
@@ -989,6 +1054,7 @@ async function runEdit(input, ctx) {
989
1054
  return failure2(`The fuzzy replacement left ${path} unchanged.`);
990
1055
  }
991
1056
  await ctx.fs.writeFile(path, after, "utf8");
1057
+ await recordReadState(ctx, path, handle);
992
1058
  return success(path, before, after, targets.length);
993
1059
  }
994
1060
  var editTool = defineTool({