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.
- package/dist/agent.js +1247 -184
- package/dist/ai.js +72 -4
- package/dist/capabilities.js +69 -2
- package/dist/cli.js +83 -13
- package/dist/connectors-saas.js +66 -0
- package/dist/index.js +83 -13
- package/dist/interop.js +66 -0
- package/dist/mcp.js +270 -363
- package/dist/react-ink.js +15 -11
- package/dist/shell-app.js +83 -13
- package/dist/smithy.js +69 -2
- package/dist/swarm.js +69 -2
- package/dist/types/capabilities/backends/node-backends.d.ts +3 -1
- package/dist/types/capabilities/files/read-state-gate.d.ts +69 -0
- package/dist/types/capabilities/files/read-state-gate.test.d.ts +14 -0
- package/dist/types/capabilities/kernel/context.d.ts +4 -0
- package/dist/types/capabilities/kernel/index.d.ts +2 -2
- package/dist/types/capabilities/kernel/spec.d.ts +55 -0
- package/dist/types/facade/bot/actions/bash.d.ts +15 -0
- package/dist/types/facade/bot/actions/bash.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/checkpoint.d.ts +49 -0
- package/dist/types/facade/bot/actions/checkpoint.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/edit-utils.d.ts +86 -0
- package/dist/types/facade/bot/actions/edit.d.ts +18 -0
- package/dist/types/facade/bot/actions/edit.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/find.d.ts +2 -0
- package/dist/types/facade/bot/actions/find.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/grep.d.ts +10 -0
- package/dist/types/facade/bot/actions/grep.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/index.d.ts +16 -0
- package/dist/types/facade/bot/actions/read-state.d.ts +83 -0
- package/dist/types/facade/bot/actions/read-state.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/read.d.ts +7 -0
- package/dist/types/facade/bot/actions/read.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/sandbox-backend.d.ts +99 -0
- package/dist/types/facade/bot/actions/sandbox-backend.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/websearch.d.ts +5 -2
- package/dist/types/facade/bot/actions/websearch.test.d.ts +1 -0
- package/dist/types/facade/bot/actions/write.d.ts +15 -0
- package/dist/types/facade/bot/agent-loop.d.ts +10 -0
- package/dist/types/facade/bot/agent-loop.test.d.ts +1 -0
- package/dist/types/facade/bot/agent.d.ts +9 -1
- package/dist/types/facade/bot/permission-gate.test.d.ts +1 -0
- package/dist/types/facade/bot/types.d.ts +60 -0
- package/dist/types/facade/mcp-core/client.d.ts +71 -15
- package/dist/types/facade/mcp-core/client.test.d.ts +18 -0
- package/dist/types/facade/mcp-core/types.d.ts +10 -0
- package/dist/types/facade/ml/adapters/anthropic-retry.test.d.ts +1 -0
- package/dist/types/facade/ml/adapters/anthropic.d.ts +17 -0
- package/dist/types/facade/ml/adapters/simple-options.d.ts +13 -0
- package/dist/types/facade/ml/adapters/simple-options.test.d.ts +1 -0
- package/dist/types/react-ink/components/StatusLine.d.ts +10 -1
- package/dist/types/react-ink/components/ToolEventBlock.d.ts +2 -1
- package/dist/types/react-ink/components/ToolEventBlock.test.d.ts +1 -0
- package/package.json +1 -1
package/dist/react-ink.js
CHANGED
|
@@ -2061,6 +2061,9 @@ function statusMarker(status) {
|
|
|
2061
2061
|
return ">";
|
|
2062
2062
|
}
|
|
2063
2063
|
}
|
|
2064
|
+
function statusColorKey(status) {
|
|
2065
|
+
return status === "error" ? "error" : "text";
|
|
2066
|
+
}
|
|
2064
2067
|
function plainToolText(text) {
|
|
2065
2068
|
return stripAnsi5(text);
|
|
2066
2069
|
}
|
|
@@ -2102,7 +2105,7 @@ function ToolEventBlock({
|
|
|
2102
2105
|
showTitle = true,
|
|
2103
2106
|
status,
|
|
2104
2107
|
summary,
|
|
2105
|
-
theme
|
|
2108
|
+
theme,
|
|
2106
2109
|
title
|
|
2107
2110
|
}) {
|
|
2108
2111
|
const normalizedSummary = summary?.trim();
|
|
@@ -2118,19 +2121,19 @@ function ToolEventBlock({
|
|
|
2118
2121
|
const { visibleLines, hiddenLineCount } = clampContentLines(combinedLines, maxContentLines);
|
|
2119
2122
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom, marginLeft: indent, children: [
|
|
2120
2123
|
showTitle ? /* @__PURE__ */ jsxs(Box, { children: [
|
|
2121
|
-
/* @__PURE__ */ jsx(Text, {
|
|
2122
|
-
/* @__PURE__ */ jsx(Text, {
|
|
2123
|
-
showSummaryInline && normalizedSummary ? /* @__PURE__ */ jsx(Text, {
|
|
2124
|
+
/* @__PURE__ */ jsx(Text, { children: theme.color(statusColorKey(status), plainToolText(`${statusMarker(status)} `)) }),
|
|
2125
|
+
/* @__PURE__ */ jsx(Text, { children: theme.color(statusColorKey(status), plainToolText(title)) }),
|
|
2126
|
+
showSummaryInline && normalizedSummary ? /* @__PURE__ */ jsx(Text, { children: theme.muted(plainToolText(` (${normalizedSummary})`)) }) : null
|
|
2124
2127
|
] }) : null,
|
|
2125
2128
|
visibleLines.map((line, index) => /* @__PURE__ */ jsx(
|
|
2126
2129
|
Box,
|
|
2127
2130
|
{
|
|
2128
2131
|
marginLeft: line.kind === "response" ? showTitle ? 2 : 0 : showTitle ? 4 : 2,
|
|
2129
|
-
children: preformatted ? /* @__PURE__ */ jsx(Text, { children: line.text }) : /* @__PURE__ */ jsx(Text, {
|
|
2132
|
+
children: preformatted ? /* @__PURE__ */ jsx(Text, { children: line.text }) : /* @__PURE__ */ jsx(Text, { children: theme.color("text", plainToolText(line.text)) })
|
|
2130
2133
|
},
|
|
2131
2134
|
`${line.kind}:${index}:${stripAnsi5(line.text)}`
|
|
2132
2135
|
)),
|
|
2133
|
-
hiddenLineCount > 0 ? /* @__PURE__ */ jsx(Box, { marginLeft: showTitle ? 4 : 2, children: /* @__PURE__ */ jsx(Text, {
|
|
2136
|
+
hiddenLineCount > 0 ? /* @__PURE__ */ jsx(Box, { marginLeft: showTitle ? 4 : 2, children: /* @__PURE__ */ jsx(Text, { children: theme.muted(plainToolText(`... ${hiddenLineCount} more line(s)`)) }) }) : null
|
|
2134
2137
|
] });
|
|
2135
2138
|
}
|
|
2136
2139
|
|
|
@@ -2473,20 +2476,20 @@ function MessageList({
|
|
|
2473
2476
|
}
|
|
2474
2477
|
|
|
2475
2478
|
// src/react-ink/components/StatusLine.tsx
|
|
2476
|
-
function StatusLine({ snapshot, status, theme }) {
|
|
2479
|
+
function StatusLine({ snapshot, status, theme, showBusyText = true }) {
|
|
2477
2480
|
let text = status?.text;
|
|
2478
2481
|
let tone = status?.kind ?? "info";
|
|
2479
2482
|
if (!text) {
|
|
2480
|
-
if (snapshot.isCompacting) {
|
|
2483
|
+
if (showBusyText && snapshot.isCompacting) {
|
|
2481
2484
|
text = "Compacting conversation context...";
|
|
2482
2485
|
tone = "busy";
|
|
2483
|
-
} else if (snapshot.isBashRunning) {
|
|
2486
|
+
} else if (showBusyText && snapshot.isBashRunning) {
|
|
2484
2487
|
text = "Running bash command...";
|
|
2485
2488
|
tone = "busy";
|
|
2486
|
-
} else if (snapshot.pendingToolCallCount > 0 || snapshot.pendingMessageCount > 0) {
|
|
2489
|
+
} else if (showBusyText && (snapshot.pendingToolCallCount > 0 || snapshot.pendingMessageCount > 0)) {
|
|
2487
2490
|
text = "Agent working...";
|
|
2488
2491
|
tone = "busy";
|
|
2489
|
-
} else if (snapshot.isStreaming) {
|
|
2492
|
+
} else if (showBusyText && snapshot.isStreaming) {
|
|
2490
2493
|
text = "Agent working...";
|
|
2491
2494
|
tone = "busy";
|
|
2492
2495
|
} else if (snapshot.error) {
|
|
@@ -3815,6 +3818,7 @@ export {
|
|
|
3815
3818
|
previewText,
|
|
3816
3819
|
safeStringify,
|
|
3817
3820
|
splitAssistantMessage,
|
|
3821
|
+
statusColorKey,
|
|
3818
3822
|
stringWidth,
|
|
3819
3823
|
wordDiffLine,
|
|
3820
3824
|
wrapSelectionIndex
|
package/dist/shell-app.js
CHANGED
|
@@ -4635,6 +4635,7 @@ function runErrorThrowable(kind, message) {
|
|
|
4635
4635
|
}
|
|
4636
4636
|
|
|
4637
4637
|
// src/capabilities/kernel/spec.ts
|
|
4638
|
+
var READ_STATE_HANDLE_KEY = "readState";
|
|
4638
4639
|
function coerceInput(raw) {
|
|
4639
4640
|
if (typeof raw === "string") {
|
|
4640
4641
|
const trimmed = raw.trim();
|
|
@@ -5088,7 +5089,7 @@ var standardBudget = {
|
|
|
5088
5089
|
[${omitted} bytes elided to stay within the output ceiling]
|
|
5089
5090
|
`
|
|
5090
5091
|
};
|
|
5091
|
-
function makeNodeContext(cwd, signal, budget) {
|
|
5092
|
+
function makeNodeContext(cwd, signal, budget, framework) {
|
|
5092
5093
|
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
5093
5094
|
throw new Error("makeNodeContext requires a non-empty working directory.");
|
|
5094
5095
|
}
|
|
@@ -5097,10 +5098,60 @@ function makeNodeContext(cwd, signal, budget) {
|
|
|
5097
5098
|
fs: nodeFs,
|
|
5098
5099
|
shell: nodeShell,
|
|
5099
5100
|
signal: signal ?? neverAborts(),
|
|
5100
|
-
budget: budget ?? standardBudget
|
|
5101
|
+
budget: budget ?? standardBudget,
|
|
5102
|
+
...framework ? { framework } : {}
|
|
5101
5103
|
};
|
|
5102
5104
|
}
|
|
5103
5105
|
|
|
5106
|
+
// src/capabilities/files/read-state-gate.ts
|
|
5107
|
+
var READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
|
|
5108
|
+
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.";
|
|
5109
|
+
function getReadStateHandle(ctx) {
|
|
5110
|
+
const bag = ctx.framework;
|
|
5111
|
+
if (!bag || typeof bag !== "object") return void 0;
|
|
5112
|
+
const candidate = bag[READ_STATE_HANDLE_KEY];
|
|
5113
|
+
if (!candidate || typeof candidate !== "object") return void 0;
|
|
5114
|
+
const handle = candidate;
|
|
5115
|
+
if (typeof handle.get !== "function" || typeof handle.set !== "function" || typeof handle.has !== "function") {
|
|
5116
|
+
return void 0;
|
|
5117
|
+
}
|
|
5118
|
+
return handle;
|
|
5119
|
+
}
|
|
5120
|
+
async function recordReadState(ctx, absPath, handle) {
|
|
5121
|
+
if (!handle) return;
|
|
5122
|
+
try {
|
|
5123
|
+
const info = await ctx.fs.stat(absPath);
|
|
5124
|
+
const record = {
|
|
5125
|
+
mtimeMs: Math.floor(info.modifiedMs),
|
|
5126
|
+
size: info.size,
|
|
5127
|
+
readAt: Date.now()
|
|
5128
|
+
};
|
|
5129
|
+
handle.set(absPath, record);
|
|
5130
|
+
} catch {
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
async function enforceReadGate(ctx, absPath, handle) {
|
|
5134
|
+
if (!handle) return { ok: true };
|
|
5135
|
+
if (!handle.has(absPath)) {
|
|
5136
|
+
return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
|
|
5137
|
+
}
|
|
5138
|
+
const recorded = handle.get(absPath);
|
|
5139
|
+
if (!recorded) {
|
|
5140
|
+
return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
|
|
5141
|
+
}
|
|
5142
|
+
let info;
|
|
5143
|
+
try {
|
|
5144
|
+
info = await ctx.fs.stat(absPath);
|
|
5145
|
+
} catch {
|
|
5146
|
+
return { ok: true };
|
|
5147
|
+
}
|
|
5148
|
+
const currentMtime = Math.floor(info.modifiedMs);
|
|
5149
|
+
if (currentMtime > recorded.mtimeMs || info.size !== recorded.size) {
|
|
5150
|
+
return { ok: false, message: MODIFIED_SINCE_READ_MESSAGE };
|
|
5151
|
+
}
|
|
5152
|
+
return { ok: true };
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5104
5155
|
// src/capabilities/files/read.ts
|
|
5105
5156
|
var GUTTER_WIDTH = 6;
|
|
5106
5157
|
var DESCRIPTION = [
|
|
@@ -5203,6 +5254,7 @@ var readTool = defineTool({
|
|
|
5203
5254
|
const detail = err instanceof Error ? err.message : String(err);
|
|
5204
5255
|
return failure(`Could not read ${path2}: ${detail}`);
|
|
5205
5256
|
}
|
|
5257
|
+
await recordReadState(ctx, path2, getReadStateHandle(ctx));
|
|
5206
5258
|
const allLines = toLines(text);
|
|
5207
5259
|
const totalLines = allLines.length;
|
|
5208
5260
|
if (totalLines === 0) {
|
|
@@ -5310,11 +5362,19 @@ var writeTool = defineTool({
|
|
|
5310
5362
|
async run(input, ctx) {
|
|
5311
5363
|
const path2 = readPath(input?.path);
|
|
5312
5364
|
const content = readContent(input?.content);
|
|
5365
|
+
const handle = getReadStateHandle(ctx);
|
|
5366
|
+
if (handle && await ctx.fs.exists(path2)) {
|
|
5367
|
+
const gate = await enforceReadGate(ctx, path2, handle);
|
|
5368
|
+
if (!gate.ok) {
|
|
5369
|
+
return asText(gate.message, true);
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5313
5372
|
const folder = parentDir(path2);
|
|
5314
5373
|
if (folder.length > 0) {
|
|
5315
5374
|
await ctx.fs.mkdir(folder, { recursive: true });
|
|
5316
5375
|
}
|
|
5317
5376
|
await ctx.fs.writeFile(path2, content, "utf8");
|
|
5377
|
+
await recordReadState(ctx, path2, handle);
|
|
5318
5378
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
5319
5379
|
const unit = bytes === 1 ? "byte" : "bytes";
|
|
5320
5380
|
return asText(`Saved ${bytes} ${unit} to ${path2}.`);
|
|
@@ -5604,6 +5664,11 @@ async function runEdit(input, ctx) {
|
|
|
5604
5664
|
if (!info.isFile) {
|
|
5605
5665
|
return failure2(`${path2} is not a regular file, so it cannot be edited.`);
|
|
5606
5666
|
}
|
|
5667
|
+
const handle = getReadStateHandle(ctx);
|
|
5668
|
+
const gate = await enforceReadGate(ctx, path2, handle);
|
|
5669
|
+
if (!gate.ok) {
|
|
5670
|
+
return failure2(gate.message);
|
|
5671
|
+
}
|
|
5607
5672
|
const before = await ctx.fs.readFile(path2, "utf8");
|
|
5608
5673
|
const literalHits = countLiteral(before, oldText);
|
|
5609
5674
|
if (literalHits > 0) {
|
|
@@ -5617,6 +5682,7 @@ async function runEdit(input, ctx) {
|
|
|
5617
5682
|
return failure2(`The replacement left ${path2} unchanged.`);
|
|
5618
5683
|
}
|
|
5619
5684
|
await ctx.fs.writeFile(path2, after2, "utf8");
|
|
5685
|
+
await recordReadState(ctx, path2, handle);
|
|
5620
5686
|
return success(path2, before, after2, replaceAll ? literalHits : 1);
|
|
5621
5687
|
}
|
|
5622
5688
|
const spans = findFuzzySpans(before, oldText);
|
|
@@ -5640,6 +5706,7 @@ async function runEdit(input, ctx) {
|
|
|
5640
5706
|
return failure2(`The fuzzy replacement left ${path2} unchanged.`);
|
|
5641
5707
|
}
|
|
5642
5708
|
await ctx.fs.writeFile(path2, after, "utf8");
|
|
5709
|
+
await recordReadState(ctx, path2, handle);
|
|
5643
5710
|
return success(path2, before, after, targets.length);
|
|
5644
5711
|
}
|
|
5645
5712
|
var editTool = defineTool({
|
|
@@ -10381,6 +10448,9 @@ function statusMarker(status) {
|
|
|
10381
10448
|
return ">";
|
|
10382
10449
|
}
|
|
10383
10450
|
}
|
|
10451
|
+
function statusColorKey(status) {
|
|
10452
|
+
return status === "error" ? "error" : "text";
|
|
10453
|
+
}
|
|
10384
10454
|
function plainToolText(text) {
|
|
10385
10455
|
return stripAnsi5(text);
|
|
10386
10456
|
}
|
|
@@ -10422,7 +10492,7 @@ function ToolEventBlock({
|
|
|
10422
10492
|
showTitle = true,
|
|
10423
10493
|
status,
|
|
10424
10494
|
summary,
|
|
10425
|
-
theme
|
|
10495
|
+
theme,
|
|
10426
10496
|
title
|
|
10427
10497
|
}) {
|
|
10428
10498
|
const normalizedSummary = summary?.trim();
|
|
@@ -10438,19 +10508,19 @@ function ToolEventBlock({
|
|
|
10438
10508
|
const { visibleLines, hiddenLineCount } = clampContentLines(combinedLines, maxContentLines);
|
|
10439
10509
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom, marginLeft: indent, children: [
|
|
10440
10510
|
showTitle ? /* @__PURE__ */ jsxs(Box, { children: [
|
|
10441
|
-
/* @__PURE__ */ jsx(Text, {
|
|
10442
|
-
/* @__PURE__ */ jsx(Text, {
|
|
10443
|
-
showSummaryInline && normalizedSummary ? /* @__PURE__ */ jsx(Text, {
|
|
10511
|
+
/* @__PURE__ */ jsx(Text, { children: theme.color(statusColorKey(status), plainToolText(`${statusMarker(status)} `)) }),
|
|
10512
|
+
/* @__PURE__ */ jsx(Text, { children: theme.color(statusColorKey(status), plainToolText(title)) }),
|
|
10513
|
+
showSummaryInline && normalizedSummary ? /* @__PURE__ */ jsx(Text, { children: theme.muted(plainToolText(` (${normalizedSummary})`)) }) : null
|
|
10444
10514
|
] }) : null,
|
|
10445
10515
|
visibleLines.map((line, index) => /* @__PURE__ */ jsx(
|
|
10446
10516
|
Box,
|
|
10447
10517
|
{
|
|
10448
10518
|
marginLeft: line.kind === "response" ? showTitle ? 2 : 0 : showTitle ? 4 : 2,
|
|
10449
|
-
children: preformatted ? /* @__PURE__ */ jsx(Text, { children: line.text }) : /* @__PURE__ */ jsx(Text, {
|
|
10519
|
+
children: preformatted ? /* @__PURE__ */ jsx(Text, { children: line.text }) : /* @__PURE__ */ jsx(Text, { children: theme.color("text", plainToolText(line.text)) })
|
|
10450
10520
|
},
|
|
10451
10521
|
`${line.kind}:${index}:${stripAnsi5(line.text)}`
|
|
10452
10522
|
)),
|
|
10453
|
-
hiddenLineCount > 0 ? /* @__PURE__ */ jsx(Box, { marginLeft: showTitle ? 4 : 2, children: /* @__PURE__ */ jsx(Text, {
|
|
10523
|
+
hiddenLineCount > 0 ? /* @__PURE__ */ jsx(Box, { marginLeft: showTitle ? 4 : 2, children: /* @__PURE__ */ jsx(Text, { children: theme.muted(plainToolText(`... ${hiddenLineCount} more line(s)`)) }) }) : null
|
|
10454
10524
|
] });
|
|
10455
10525
|
}
|
|
10456
10526
|
|
|
@@ -10793,20 +10863,20 @@ function MessageList({
|
|
|
10793
10863
|
}
|
|
10794
10864
|
|
|
10795
10865
|
// src/react-ink/components/StatusLine.tsx
|
|
10796
|
-
function StatusLine({ snapshot, status, theme }) {
|
|
10866
|
+
function StatusLine({ snapshot, status, theme, showBusyText = true }) {
|
|
10797
10867
|
let text = status?.text;
|
|
10798
10868
|
let tone = status?.kind ?? "info";
|
|
10799
10869
|
if (!text) {
|
|
10800
|
-
if (snapshot.isCompacting) {
|
|
10870
|
+
if (showBusyText && snapshot.isCompacting) {
|
|
10801
10871
|
text = "Compacting conversation context...";
|
|
10802
10872
|
tone = "busy";
|
|
10803
|
-
} else if (snapshot.isBashRunning) {
|
|
10873
|
+
} else if (showBusyText && snapshot.isBashRunning) {
|
|
10804
10874
|
text = "Running bash command...";
|
|
10805
10875
|
tone = "busy";
|
|
10806
|
-
} else if (snapshot.pendingToolCallCount > 0 || snapshot.pendingMessageCount > 0) {
|
|
10876
|
+
} else if (showBusyText && (snapshot.pendingToolCallCount > 0 || snapshot.pendingMessageCount > 0)) {
|
|
10807
10877
|
text = "Agent working...";
|
|
10808
10878
|
tone = "busy";
|
|
10809
|
-
} else if (snapshot.isStreaming) {
|
|
10879
|
+
} else if (showBusyText && snapshot.isStreaming) {
|
|
10810
10880
|
text = "Agent working...";
|
|
10811
10881
|
tone = "busy";
|
|
10812
10882
|
} else if (snapshot.error) {
|
package/dist/smithy.js
CHANGED
|
@@ -276,6 +276,7 @@ function getProfile(name) {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
// src/capabilities/kernel/spec.ts
|
|
279
|
+
var READ_STATE_HANDLE_KEY = "readState";
|
|
279
280
|
function coerceInput(raw) {
|
|
280
281
|
if (typeof raw === "string") {
|
|
281
282
|
const trimmed = raw.trim();
|
|
@@ -729,7 +730,7 @@ var standardBudget = {
|
|
|
729
730
|
[${omitted} bytes elided to stay within the output ceiling]
|
|
730
731
|
`
|
|
731
732
|
};
|
|
732
|
-
function makeNodeContext(cwd, signal, budget) {
|
|
733
|
+
function makeNodeContext(cwd, signal, budget, framework) {
|
|
733
734
|
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
734
735
|
throw new Error("makeNodeContext requires a non-empty working directory.");
|
|
735
736
|
}
|
|
@@ -738,10 +739,60 @@ function makeNodeContext(cwd, signal, budget) {
|
|
|
738
739
|
fs: nodeFs,
|
|
739
740
|
shell: nodeShell,
|
|
740
741
|
signal: signal ?? neverAborts(),
|
|
741
|
-
budget: budget ?? standardBudget
|
|
742
|
+
budget: budget ?? standardBudget,
|
|
743
|
+
...framework ? { framework } : {}
|
|
742
744
|
};
|
|
743
745
|
}
|
|
744
746
|
|
|
747
|
+
// src/capabilities/files/read-state-gate.ts
|
|
748
|
+
var READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
|
|
749
|
+
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.";
|
|
750
|
+
function getReadStateHandle(ctx) {
|
|
751
|
+
const bag = ctx.framework;
|
|
752
|
+
if (!bag || typeof bag !== "object") return void 0;
|
|
753
|
+
const candidate = bag[READ_STATE_HANDLE_KEY];
|
|
754
|
+
if (!candidate || typeof candidate !== "object") return void 0;
|
|
755
|
+
const handle = candidate;
|
|
756
|
+
if (typeof handle.get !== "function" || typeof handle.set !== "function" || typeof handle.has !== "function") {
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
return handle;
|
|
760
|
+
}
|
|
761
|
+
async function recordReadState(ctx, absPath, handle) {
|
|
762
|
+
if (!handle) return;
|
|
763
|
+
try {
|
|
764
|
+
const info = await ctx.fs.stat(absPath);
|
|
765
|
+
const record = {
|
|
766
|
+
mtimeMs: Math.floor(info.modifiedMs),
|
|
767
|
+
size: info.size,
|
|
768
|
+
readAt: Date.now()
|
|
769
|
+
};
|
|
770
|
+
handle.set(absPath, record);
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function enforceReadGate(ctx, absPath, handle) {
|
|
775
|
+
if (!handle) return { ok: true };
|
|
776
|
+
if (!handle.has(absPath)) {
|
|
777
|
+
return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
|
|
778
|
+
}
|
|
779
|
+
const recorded = handle.get(absPath);
|
|
780
|
+
if (!recorded) {
|
|
781
|
+
return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
|
|
782
|
+
}
|
|
783
|
+
let info;
|
|
784
|
+
try {
|
|
785
|
+
info = await ctx.fs.stat(absPath);
|
|
786
|
+
} catch {
|
|
787
|
+
return { ok: true };
|
|
788
|
+
}
|
|
789
|
+
const currentMtime = Math.floor(info.modifiedMs);
|
|
790
|
+
if (currentMtime > recorded.mtimeMs || info.size !== recorded.size) {
|
|
791
|
+
return { ok: false, message: MODIFIED_SINCE_READ_MESSAGE };
|
|
792
|
+
}
|
|
793
|
+
return { ok: true };
|
|
794
|
+
}
|
|
795
|
+
|
|
745
796
|
// src/capabilities/files/read.ts
|
|
746
797
|
var GUTTER_WIDTH = 6;
|
|
747
798
|
var DESCRIPTION = [
|
|
@@ -844,6 +895,7 @@ var readTool = defineTool({
|
|
|
844
895
|
const detail = err instanceof Error ? err.message : String(err);
|
|
845
896
|
return failure(`Could not read ${path}: ${detail}`);
|
|
846
897
|
}
|
|
898
|
+
await recordReadState(ctx, path, getReadStateHandle(ctx));
|
|
847
899
|
const allLines = toLines(text);
|
|
848
900
|
const totalLines = allLines.length;
|
|
849
901
|
if (totalLines === 0) {
|
|
@@ -951,11 +1003,19 @@ var writeTool = defineTool({
|
|
|
951
1003
|
async run(input, ctx) {
|
|
952
1004
|
const path = readPath(input?.path);
|
|
953
1005
|
const content = readContent(input?.content);
|
|
1006
|
+
const handle = getReadStateHandle(ctx);
|
|
1007
|
+
if (handle && await ctx.fs.exists(path)) {
|
|
1008
|
+
const gate = await enforceReadGate(ctx, path, handle);
|
|
1009
|
+
if (!gate.ok) {
|
|
1010
|
+
return asText(gate.message, true);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
954
1013
|
const folder = parentDir(path);
|
|
955
1014
|
if (folder.length > 0) {
|
|
956
1015
|
await ctx.fs.mkdir(folder, { recursive: true });
|
|
957
1016
|
}
|
|
958
1017
|
await ctx.fs.writeFile(path, content, "utf8");
|
|
1018
|
+
await recordReadState(ctx, path, handle);
|
|
959
1019
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
960
1020
|
const unit = bytes === 1 ? "byte" : "bytes";
|
|
961
1021
|
return asText(`Saved ${bytes} ${unit} to ${path}.`);
|
|
@@ -1245,6 +1305,11 @@ async function runEdit(input, ctx) {
|
|
|
1245
1305
|
if (!info.isFile) {
|
|
1246
1306
|
return failure2(`${path} is not a regular file, so it cannot be edited.`);
|
|
1247
1307
|
}
|
|
1308
|
+
const handle = getReadStateHandle(ctx);
|
|
1309
|
+
const gate = await enforceReadGate(ctx, path, handle);
|
|
1310
|
+
if (!gate.ok) {
|
|
1311
|
+
return failure2(gate.message);
|
|
1312
|
+
}
|
|
1248
1313
|
const before = await ctx.fs.readFile(path, "utf8");
|
|
1249
1314
|
const literalHits = countLiteral(before, oldText);
|
|
1250
1315
|
if (literalHits > 0) {
|
|
@@ -1258,6 +1323,7 @@ async function runEdit(input, ctx) {
|
|
|
1258
1323
|
return failure2(`The replacement left ${path} unchanged.`);
|
|
1259
1324
|
}
|
|
1260
1325
|
await ctx.fs.writeFile(path, after2, "utf8");
|
|
1326
|
+
await recordReadState(ctx, path, handle);
|
|
1261
1327
|
return success(path, before, after2, replaceAll ? literalHits : 1);
|
|
1262
1328
|
}
|
|
1263
1329
|
const spans = findFuzzySpans(before, oldText);
|
|
@@ -1281,6 +1347,7 @@ async function runEdit(input, ctx) {
|
|
|
1281
1347
|
return failure2(`The fuzzy replacement left ${path} unchanged.`);
|
|
1282
1348
|
}
|
|
1283
1349
|
await ctx.fs.writeFile(path, after, "utf8");
|
|
1350
|
+
await recordReadState(ctx, path, handle);
|
|
1284
1351
|
return success(path, before, after, targets.length);
|
|
1285
1352
|
}
|
|
1286
1353
|
var editTool = defineTool({
|
package/dist/swarm.js
CHANGED
|
@@ -4383,6 +4383,7 @@ function runErrorThrowable(kind, message) {
|
|
|
4383
4383
|
}
|
|
4384
4384
|
|
|
4385
4385
|
// src/capabilities/kernel/spec.ts
|
|
4386
|
+
var READ_STATE_HANDLE_KEY = "readState";
|
|
4386
4387
|
function coerceInput(raw) {
|
|
4387
4388
|
if (typeof raw === "string") {
|
|
4388
4389
|
const trimmed = raw.trim();
|
|
@@ -4836,7 +4837,7 @@ var standardBudget = {
|
|
|
4836
4837
|
[${omitted} bytes elided to stay within the output ceiling]
|
|
4837
4838
|
`
|
|
4838
4839
|
};
|
|
4839
|
-
function makeNodeContext(cwd, signal, budget) {
|
|
4840
|
+
function makeNodeContext(cwd, signal, budget, framework) {
|
|
4840
4841
|
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
4841
4842
|
throw new Error("makeNodeContext requires a non-empty working directory.");
|
|
4842
4843
|
}
|
|
@@ -4845,10 +4846,60 @@ function makeNodeContext(cwd, signal, budget) {
|
|
|
4845
4846
|
fs: nodeFs,
|
|
4846
4847
|
shell: nodeShell,
|
|
4847
4848
|
signal: signal ?? neverAborts(),
|
|
4848
|
-
budget: budget ?? standardBudget
|
|
4849
|
+
budget: budget ?? standardBudget,
|
|
4850
|
+
...framework ? { framework } : {}
|
|
4849
4851
|
};
|
|
4850
4852
|
}
|
|
4851
4853
|
|
|
4854
|
+
// src/capabilities/files/read-state-gate.ts
|
|
4855
|
+
var READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
|
|
4856
|
+
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.";
|
|
4857
|
+
function getReadStateHandle(ctx) {
|
|
4858
|
+
const bag = ctx.framework;
|
|
4859
|
+
if (!bag || typeof bag !== "object") return void 0;
|
|
4860
|
+
const candidate = bag[READ_STATE_HANDLE_KEY];
|
|
4861
|
+
if (!candidate || typeof candidate !== "object") return void 0;
|
|
4862
|
+
const handle = candidate;
|
|
4863
|
+
if (typeof handle.get !== "function" || typeof handle.set !== "function" || typeof handle.has !== "function") {
|
|
4864
|
+
return void 0;
|
|
4865
|
+
}
|
|
4866
|
+
return handle;
|
|
4867
|
+
}
|
|
4868
|
+
async function recordReadState(ctx, absPath, handle) {
|
|
4869
|
+
if (!handle) return;
|
|
4870
|
+
try {
|
|
4871
|
+
const info = await ctx.fs.stat(absPath);
|
|
4872
|
+
const record = {
|
|
4873
|
+
mtimeMs: Math.floor(info.modifiedMs),
|
|
4874
|
+
size: info.size,
|
|
4875
|
+
readAt: Date.now()
|
|
4876
|
+
};
|
|
4877
|
+
handle.set(absPath, record);
|
|
4878
|
+
} catch {
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
async function enforceReadGate(ctx, absPath, handle) {
|
|
4882
|
+
if (!handle) return { ok: true };
|
|
4883
|
+
if (!handle.has(absPath)) {
|
|
4884
|
+
return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
|
|
4885
|
+
}
|
|
4886
|
+
const recorded = handle.get(absPath);
|
|
4887
|
+
if (!recorded) {
|
|
4888
|
+
return { ok: false, message: READ_BEFORE_EDIT_MESSAGE };
|
|
4889
|
+
}
|
|
4890
|
+
let info;
|
|
4891
|
+
try {
|
|
4892
|
+
info = await ctx.fs.stat(absPath);
|
|
4893
|
+
} catch {
|
|
4894
|
+
return { ok: true };
|
|
4895
|
+
}
|
|
4896
|
+
const currentMtime = Math.floor(info.modifiedMs);
|
|
4897
|
+
if (currentMtime > recorded.mtimeMs || info.size !== recorded.size) {
|
|
4898
|
+
return { ok: false, message: MODIFIED_SINCE_READ_MESSAGE };
|
|
4899
|
+
}
|
|
4900
|
+
return { ok: true };
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4852
4903
|
// src/capabilities/files/read.ts
|
|
4853
4904
|
var GUTTER_WIDTH = 6;
|
|
4854
4905
|
var DESCRIPTION = [
|
|
@@ -4951,6 +5002,7 @@ var readTool = defineTool({
|
|
|
4951
5002
|
const detail = err instanceof Error ? err.message : String(err);
|
|
4952
5003
|
return failure(`Could not read ${path}: ${detail}`);
|
|
4953
5004
|
}
|
|
5005
|
+
await recordReadState(ctx, path, getReadStateHandle(ctx));
|
|
4954
5006
|
const allLines = toLines(text);
|
|
4955
5007
|
const totalLines = allLines.length;
|
|
4956
5008
|
if (totalLines === 0) {
|
|
@@ -5058,11 +5110,19 @@ var writeTool = defineTool({
|
|
|
5058
5110
|
async run(input, ctx) {
|
|
5059
5111
|
const path = readPath(input?.path);
|
|
5060
5112
|
const content = readContent(input?.content);
|
|
5113
|
+
const handle = getReadStateHandle(ctx);
|
|
5114
|
+
if (handle && await ctx.fs.exists(path)) {
|
|
5115
|
+
const gate = await enforceReadGate(ctx, path, handle);
|
|
5116
|
+
if (!gate.ok) {
|
|
5117
|
+
return asText(gate.message, true);
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5061
5120
|
const folder = parentDir(path);
|
|
5062
5121
|
if (folder.length > 0) {
|
|
5063
5122
|
await ctx.fs.mkdir(folder, { recursive: true });
|
|
5064
5123
|
}
|
|
5065
5124
|
await ctx.fs.writeFile(path, content, "utf8");
|
|
5125
|
+
await recordReadState(ctx, path, handle);
|
|
5066
5126
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
5067
5127
|
const unit = bytes === 1 ? "byte" : "bytes";
|
|
5068
5128
|
return asText(`Saved ${bytes} ${unit} to ${path}.`);
|
|
@@ -5352,6 +5412,11 @@ async function runEdit(input, ctx) {
|
|
|
5352
5412
|
if (!info.isFile) {
|
|
5353
5413
|
return failure2(`${path} is not a regular file, so it cannot be edited.`);
|
|
5354
5414
|
}
|
|
5415
|
+
const handle = getReadStateHandle(ctx);
|
|
5416
|
+
const gate = await enforceReadGate(ctx, path, handle);
|
|
5417
|
+
if (!gate.ok) {
|
|
5418
|
+
return failure2(gate.message);
|
|
5419
|
+
}
|
|
5355
5420
|
const before = await ctx.fs.readFile(path, "utf8");
|
|
5356
5421
|
const literalHits = countLiteral(before, oldText);
|
|
5357
5422
|
if (literalHits > 0) {
|
|
@@ -5365,6 +5430,7 @@ async function runEdit(input, ctx) {
|
|
|
5365
5430
|
return failure2(`The replacement left ${path} unchanged.`);
|
|
5366
5431
|
}
|
|
5367
5432
|
await ctx.fs.writeFile(path, after2, "utf8");
|
|
5433
|
+
await recordReadState(ctx, path, handle);
|
|
5368
5434
|
return success(path, before, after2, replaceAll ? literalHits : 1);
|
|
5369
5435
|
}
|
|
5370
5436
|
const spans = findFuzzySpans(before, oldText);
|
|
@@ -5388,6 +5454,7 @@ async function runEdit(input, ctx) {
|
|
|
5388
5454
|
return failure2(`The fuzzy replacement left ${path} unchanged.`);
|
|
5389
5455
|
}
|
|
5390
5456
|
await ctx.fs.writeFile(path, after, "utf8");
|
|
5457
|
+
await recordReadState(ctx, path, handle);
|
|
5391
5458
|
return success(path, before, after, targets.length);
|
|
5392
5459
|
}
|
|
5393
5460
|
var editTool = defineTool({
|
|
@@ -25,4 +25,6 @@ export declare const nodeShell: Shell;
|
|
|
25
25
|
* required, while the cancellation `signal` and output `budget` fall back to a
|
|
26
26
|
* never-aborting signal and the kernel's standard window when omitted.
|
|
27
27
|
*/
|
|
28
|
-
export declare function makeNodeContext(cwd: string, signal?: AbortSignal, budget?: OutputBudget
|
|
28
|
+
export declare function makeNodeContext(cwd: string, signal?: AbortSignal, budget?: OutputBudget, framework?: {
|
|
29
|
+
readonly [key: string]: unknown;
|
|
30
|
+
}): ToolContext;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read-before-edit gate (shared by `read`, `edit`, and `write`).
|
|
3
|
+
*
|
|
4
|
+
* This is the framework half of the read-edit-gate feature. When — and ONLY
|
|
5
|
+
* when — a host has stashed a {@link ReadStateHandle} on
|
|
6
|
+
* `ctx.framework[READ_STATE_HANDLE_KEY]`, the file tools enforce a small
|
|
7
|
+
* discipline borrowed from interactive coding agents:
|
|
8
|
+
*
|
|
9
|
+
* 1. A file may not be edited or overwritten until it has been *read* in this
|
|
10
|
+
* session (so the model is never blindly clobbering content it has not
|
|
11
|
+
* seen).
|
|
12
|
+
* 2. A file may not be mutated if it has drifted on disk since that read — its
|
|
13
|
+
* modification time or byte size advanced past what was recorded — because
|
|
14
|
+
* that usually means a human or a linter changed it underneath us.
|
|
15
|
+
*
|
|
16
|
+
* After every successful read (and every successful mutation) the recorded state
|
|
17
|
+
* is refreshed from the fresh on-disk stat, so the next gate check compares
|
|
18
|
+
* against the latest known-good snapshot.
|
|
19
|
+
*
|
|
20
|
+
* The whole mechanism is *additive*: with no handle present, every helper here
|
|
21
|
+
* is a no-op and the tools behave exactly as they did before. Nothing imports
|
|
22
|
+
* from the product — the handle is duck-typed and the key is a shared string
|
|
23
|
+
* literal, so the framework consumer and the product injector agree on shape
|
|
24
|
+
* without a cross-package type import.
|
|
25
|
+
*/
|
|
26
|
+
import type { ReadStateHandle, ToolContext } from "../kernel";
|
|
27
|
+
/** The two byte-stable refusal messages the gate emits. */
|
|
28
|
+
export declare const READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
|
|
29
|
+
export declare const 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.";
|
|
30
|
+
/**
|
|
31
|
+
* Pull the read-state handle off the context, if one was injected.
|
|
32
|
+
*
|
|
33
|
+
* Returns `undefined` whenever the framework bag, the key, or a well-formed
|
|
34
|
+
* handle is absent — the gate then degrades to a no-op. The handle is validated
|
|
35
|
+
* structurally (it must expose `get` / `set` / `has`) so a malformed injection
|
|
36
|
+
* can never throw mid-mutation; it simply disables the gate.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getReadStateHandle(ctx: ToolContext): ReadStateHandle | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Record (or refresh) the read state for `absPath` from a fresh stat.
|
|
41
|
+
*
|
|
42
|
+
* No-op when no handle is present. Stats are taken through the kernel `fs` seam
|
|
43
|
+
* — its `modifiedMs` field is floored to a whole millisecond so a sub-ms clock
|
|
44
|
+
* skew between read and a later compare can never spuriously trip the staleness
|
|
45
|
+
* check. A failed stat is swallowed: state-keeping is best-effort and must never
|
|
46
|
+
* mask the real operation's outcome.
|
|
47
|
+
*/
|
|
48
|
+
export declare function recordReadState(ctx: ToolContext, absPath: string, handle: ReadStateHandle | undefined): Promise<void>;
|
|
49
|
+
/** Outcome of a gate check: either cleared, or refused with a message. */
|
|
50
|
+
export type GateOutcome = {
|
|
51
|
+
ok: true;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
message: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Enforce the read-before-edit + staleness gate ahead of a mutation.
|
|
58
|
+
*
|
|
59
|
+
* With no handle present this always clears (the gate is opt-in). With a handle:
|
|
60
|
+
* - refuse when `absPath` has no recorded read this session;
|
|
61
|
+
* - refuse when the on-disk modification time or byte size has advanced past
|
|
62
|
+
* the recorded read.
|
|
63
|
+
*
|
|
64
|
+
* The on-disk stat is read through the kernel `fs` seam and floored the same way
|
|
65
|
+
* the recorded mtime is, so the comparison is apples-to-apples. A stat failure
|
|
66
|
+
* (e.g. the file vanished) clears the gate so the underlying tool can surface
|
|
67
|
+
* its own, more specific error.
|
|
68
|
+
*/
|
|
69
|
+
export declare function enforceReadGate(ctx: ToolContext, absPath: string, handle: ReadStateHandle | undefined): Promise<GateOutcome>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the read-before-edit gate (framework half of read-edit-gate).
|
|
3
|
+
*
|
|
4
|
+
* The gate is opt-in: it activates only when a host injects a {@link ReadStateHandle}
|
|
5
|
+
* on `ctx.framework[READ_STATE_HANDLE_KEY]`. These cases exercise both worlds —
|
|
6
|
+
* the gate ACTIVE (handle present) and the gate ABSENT (no handle, today's
|
|
7
|
+
* behavior) — across the three file tools (`read`, `edit`, `write`).
|
|
8
|
+
*
|
|
9
|
+
* Everything runs against a real on-disk sandbox via the genuine
|
|
10
|
+
* {@link makeNodeContext}, so the tools take their real filesystem path. The
|
|
11
|
+
* injected store is a tiny in-memory Map, mirroring what the product host will
|
|
12
|
+
* supply through the shared string-literal handle key.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -28,6 +28,10 @@ export interface ContextOverrides {
|
|
|
28
28
|
readonly shell?: Shell;
|
|
29
29
|
readonly signal?: AbortSignal;
|
|
30
30
|
readonly budget?: OutputBudget;
|
|
31
|
+
/** Optional open bag of host-injected framework handles (e.g. the read-state store). */
|
|
32
|
+
readonly framework?: {
|
|
33
|
+
readonly [key: string]: unknown;
|
|
34
|
+
};
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Build a {@link ToolContext}, defaulting every field the caller omits.
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* re-exported here; consumers import those straight from "../../runtime/contract"
|
|
14
14
|
* and "../../llmgateway/contract" so there is a single source of truth.
|
|
15
15
|
*/
|
|
16
|
-
export { defineTool } from "./spec";
|
|
17
|
-
export type { ToolSpec, ToolResult, ToolContentBlock, TextContentBlock, JsonContentBlock, ToolContext, DefinedTool, } from "./spec";
|
|
16
|
+
export { defineTool, READ_STATE_HANDLE_KEY } from "./spec";
|
|
17
|
+
export type { ToolSpec, ToolResult, ToolContentBlock, TextContentBlock, JsonContentBlock, ToolContext, DefinedTool, ReadStateHandle, ReadStateRecord, } from "./spec";
|
|
18
18
|
export type { Fs, FsEntryInfo, DirChild, RemoveOptions, Shell, ShellLaunchOptions, ShellExecOptions, ShellExecResult, ShellHandle, } from "./backends";
|
|
19
19
|
export { clamp } from "./output";
|
|
20
20
|
export type { OutputBudget, ClipEnd } from "./output";
|