@theokit/sdk-tools 0.24.1 → 0.26.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/index.cjs +28 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -4
- package/dist/index.d.ts +39 -4
- package/dist/index.js +28 -15
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1099,24 +1099,59 @@ declare function todoItemsToPlanNodes(items: readonly TodoItem[]): PlanNode[];
|
|
|
1099
1099
|
/**
|
|
1100
1100
|
* `truncateOutput` — utility for truncating large tool output.
|
|
1101
1101
|
*
|
|
1102
|
-
* When output exceeds `maxBytes`, writes the full content to a temp file
|
|
1103
|
-
*
|
|
1102
|
+
* When output exceeds `maxBytes`, writes the full content to a temp file and returns a truncated
|
|
1103
|
+
* version with a reference to the full output.
|
|
1104
|
+
*
|
|
1105
|
+
* ## M77 — why this grew instead of a new truncator being written
|
|
1106
|
+
*
|
|
1107
|
+
* The M77 discovery measured **six** output ceilings across `sdk-tools`, with five different values
|
|
1108
|
+
* and no coordination between them (`web-fetch.ts` 1 MB, `shell-exec.ts` 5 MB, `git-diff.ts` 5 MB,
|
|
1109
|
+
* `run-vitest.ts` 10 MB, `git-status.ts` configurable, and this helper's 30 000 B). It also found
|
|
1110
|
+
* that this helper — exported from the barrel — had **zero production consumers**: the SDK had
|
|
1111
|
+
* already paid for a shared truncator that nobody used while every tool rolled its own.
|
|
1112
|
+
*
|
|
1113
|
+
* Adding a seventh ceiling in the runtime would have been the defect, not the fix. So `toolResultBudget`
|
|
1114
|
+
* extends this one instead (parsimony-ladder rung 4), gaining the two things it lacked:
|
|
1115
|
+
*
|
|
1116
|
+
* - **`head-tail` mode** — the previous cut was head-only, which for command output discards
|
|
1117
|
+
* exactly the part that matters: the error, the summary, the final prompt.
|
|
1118
|
+
* - **`originalBytes`** — a machine-readable marker. The only prior signal was an English sentence
|
|
1119
|
+
* injected into the text (`"[Output truncated. Full output: …]"`), so a consumer wanting to know
|
|
1120
|
+
* how much was lost had to parse prose.
|
|
1121
|
+
*
|
|
1122
|
+
* Consolidating the MECHANISM is deliberately not the same as unifying the VALUES: 1 MB for
|
|
1123
|
+
* `web-fetch` and 10 MB for `run-vitest` may differ for good reasons. That second step is out of
|
|
1124
|
+
* scope here (plan ADR D2).
|
|
1104
1125
|
*
|
|
1105
1126
|
* Return shape:
|
|
1106
|
-
* - `{ content
|
|
1107
|
-
* - `{ content
|
|
1127
|
+
* - `{ content, truncated: false, originalBytes }`
|
|
1128
|
+
* - `{ content, truncated: true, originalBytes, overflowPath }`
|
|
1108
1129
|
*/
|
|
1130
|
+
/** How the middle is dropped when output exceeds the budget. */
|
|
1131
|
+
type TruncationMode = "head" | "head-tail";
|
|
1109
1132
|
interface TruncationOptions {
|
|
1110
1133
|
/** Maximum output size in bytes before truncation. Default: 30_000. */
|
|
1111
1134
|
maxBytes?: number;
|
|
1112
1135
|
/** Directory for overflow files. Default: ".theocode/tool-output". */
|
|
1113
1136
|
outputDir?: string;
|
|
1137
|
+
/**
|
|
1138
|
+
* `"head"` (default) keeps the first `maxBytes` — the historical behaviour, preserved because this
|
|
1139
|
+
* helper is public API. `"head-tail"` splits the budget between the start and the END, which is
|
|
1140
|
+
* where command output usually carries its conclusion.
|
|
1141
|
+
*/
|
|
1142
|
+
mode?: TruncationMode;
|
|
1114
1143
|
}
|
|
1115
1144
|
interface TruncationResult {
|
|
1116
1145
|
/** The (possibly truncated) content. */
|
|
1117
1146
|
content: string;
|
|
1118
1147
|
/** Whether the output was truncated. */
|
|
1119
1148
|
truncated: boolean;
|
|
1149
|
+
/**
|
|
1150
|
+
* Byte length of the ORIGINAL output, present on both paths. Always present by design: a field
|
|
1151
|
+
* that only appears on failure forces every consumer through an `undefined` check, which is the
|
|
1152
|
+
* door magic values come in through.
|
|
1153
|
+
*/
|
|
1154
|
+
originalBytes: number;
|
|
1120
1155
|
/** Path to the full output file, present only when truncated. */
|
|
1121
1156
|
overflowPath?: string;
|
|
1122
1157
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1099,24 +1099,59 @@ declare function todoItemsToPlanNodes(items: readonly TodoItem[]): PlanNode[];
|
|
|
1099
1099
|
/**
|
|
1100
1100
|
* `truncateOutput` — utility for truncating large tool output.
|
|
1101
1101
|
*
|
|
1102
|
-
* When output exceeds `maxBytes`, writes the full content to a temp file
|
|
1103
|
-
*
|
|
1102
|
+
* When output exceeds `maxBytes`, writes the full content to a temp file and returns a truncated
|
|
1103
|
+
* version with a reference to the full output.
|
|
1104
|
+
*
|
|
1105
|
+
* ## M77 — why this grew instead of a new truncator being written
|
|
1106
|
+
*
|
|
1107
|
+
* The M77 discovery measured **six** output ceilings across `sdk-tools`, with five different values
|
|
1108
|
+
* and no coordination between them (`web-fetch.ts` 1 MB, `shell-exec.ts` 5 MB, `git-diff.ts` 5 MB,
|
|
1109
|
+
* `run-vitest.ts` 10 MB, `git-status.ts` configurable, and this helper's 30 000 B). It also found
|
|
1110
|
+
* that this helper — exported from the barrel — had **zero production consumers**: the SDK had
|
|
1111
|
+
* already paid for a shared truncator that nobody used while every tool rolled its own.
|
|
1112
|
+
*
|
|
1113
|
+
* Adding a seventh ceiling in the runtime would have been the defect, not the fix. So `toolResultBudget`
|
|
1114
|
+
* extends this one instead (parsimony-ladder rung 4), gaining the two things it lacked:
|
|
1115
|
+
*
|
|
1116
|
+
* - **`head-tail` mode** — the previous cut was head-only, which for command output discards
|
|
1117
|
+
* exactly the part that matters: the error, the summary, the final prompt.
|
|
1118
|
+
* - **`originalBytes`** — a machine-readable marker. The only prior signal was an English sentence
|
|
1119
|
+
* injected into the text (`"[Output truncated. Full output: …]"`), so a consumer wanting to know
|
|
1120
|
+
* how much was lost had to parse prose.
|
|
1121
|
+
*
|
|
1122
|
+
* Consolidating the MECHANISM is deliberately not the same as unifying the VALUES: 1 MB for
|
|
1123
|
+
* `web-fetch` and 10 MB for `run-vitest` may differ for good reasons. That second step is out of
|
|
1124
|
+
* scope here (plan ADR D2).
|
|
1104
1125
|
*
|
|
1105
1126
|
* Return shape:
|
|
1106
|
-
* - `{ content
|
|
1107
|
-
* - `{ content
|
|
1127
|
+
* - `{ content, truncated: false, originalBytes }`
|
|
1128
|
+
* - `{ content, truncated: true, originalBytes, overflowPath }`
|
|
1108
1129
|
*/
|
|
1130
|
+
/** How the middle is dropped when output exceeds the budget. */
|
|
1131
|
+
type TruncationMode = "head" | "head-tail";
|
|
1109
1132
|
interface TruncationOptions {
|
|
1110
1133
|
/** Maximum output size in bytes before truncation. Default: 30_000. */
|
|
1111
1134
|
maxBytes?: number;
|
|
1112
1135
|
/** Directory for overflow files. Default: ".theocode/tool-output". */
|
|
1113
1136
|
outputDir?: string;
|
|
1137
|
+
/**
|
|
1138
|
+
* `"head"` (default) keeps the first `maxBytes` — the historical behaviour, preserved because this
|
|
1139
|
+
* helper is public API. `"head-tail"` splits the budget between the start and the END, which is
|
|
1140
|
+
* where command output usually carries its conclusion.
|
|
1141
|
+
*/
|
|
1142
|
+
mode?: TruncationMode;
|
|
1114
1143
|
}
|
|
1115
1144
|
interface TruncationResult {
|
|
1116
1145
|
/** The (possibly truncated) content. */
|
|
1117
1146
|
content: string;
|
|
1118
1147
|
/** Whether the output was truncated. */
|
|
1119
1148
|
truncated: boolean;
|
|
1149
|
+
/**
|
|
1150
|
+
* Byte length of the ORIGINAL output, present on both paths. Always present by design: a field
|
|
1151
|
+
* that only appears on failure forces every consumer through an `undefined` check, which is the
|
|
1152
|
+
* door magic values come in through.
|
|
1153
|
+
*/
|
|
1154
|
+
originalBytes: number;
|
|
1120
1155
|
/** Path to the full output file, present only when truncated. */
|
|
1121
1156
|
overflowPath?: string;
|
|
1122
1157
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { spawn } from 'child_process';
|
|
|
11
11
|
import { resolveInteractive, InteractiveUnavailableError, NoSuchSessionError } from '@theokit/sdk/interactive';
|
|
12
12
|
import { lookup } from 'dns/promises';
|
|
13
13
|
import { isIP } from 'net';
|
|
14
|
+
import { randomUUID } from 'crypto';
|
|
14
15
|
|
|
15
16
|
// src/apply-patch.ts
|
|
16
17
|
|
|
@@ -1798,19 +1799,11 @@ function createQuestionTool(opts) {
|
|
|
1798
1799
|
}
|
|
1799
1800
|
var MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
1800
1801
|
var BINARY_PROBE_BYTES = 8 * 1024;
|
|
1801
|
-
var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([".env", ".git", "node_modules", ".theo"]);
|
|
1802
|
-
function isForbiddenAtAnyDepth(path) {
|
|
1803
|
-
const segs = path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
1804
|
-
return segs.some((s) => {
|
|
1805
|
-
if (s === ".env.example") return false;
|
|
1806
|
-
return SENSITIVE_SEGMENTS.has(s) || /^\.env\./.test(s);
|
|
1807
|
-
});
|
|
1808
|
-
}
|
|
1809
1802
|
function forbiddenReadError(path, allowAbsolute) {
|
|
1810
1803
|
if (isForbiddenPath(path)) {
|
|
1811
1804
|
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
1812
1805
|
}
|
|
1813
|
-
if (allowAbsolute && isAbsolute(path) &&
|
|
1806
|
+
if (allowAbsolute && isAbsolute(path) && ehProibidoEmQualquerProfundidade(path)) {
|
|
1814
1807
|
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
1815
1808
|
}
|
|
1816
1809
|
return null;
|
|
@@ -2551,24 +2544,44 @@ function createTodolistTool() {
|
|
|
2551
2544
|
getItems: (threadId) => ops(threadId).getItems()
|
|
2552
2545
|
};
|
|
2553
2546
|
}
|
|
2547
|
+
function decodeWholeCodePoints(buf) {
|
|
2548
|
+
return new TextDecoder("utf-8").decode(buf, { stream: true });
|
|
2549
|
+
}
|
|
2554
2550
|
function truncateOutput(output, opts) {
|
|
2555
2551
|
const maxBytes = opts?.maxBytes ?? 3e4;
|
|
2556
2552
|
const outputDir = opts?.outputDir ?? ".theocode/tool-output";
|
|
2557
|
-
const
|
|
2558
|
-
|
|
2559
|
-
|
|
2553
|
+
const mode = opts?.mode ?? "head";
|
|
2554
|
+
const originalBytes = Buffer.byteLength(output, "utf-8");
|
|
2555
|
+
if (originalBytes <= maxBytes) {
|
|
2556
|
+
return { content: output, truncated: false, originalBytes };
|
|
2560
2557
|
}
|
|
2561
2558
|
mkdirSync(outputDir, { recursive: true });
|
|
2562
|
-
const filename = `overflow-${Date.now()}.txt`;
|
|
2559
|
+
const filename = `overflow-${Date.now()}-${randomUUID().slice(0, 8)}.txt`;
|
|
2563
2560
|
const overflowPath = join(outputDir, filename);
|
|
2564
2561
|
writeFileSync(overflowPath, output, "utf-8");
|
|
2565
|
-
const
|
|
2562
|
+
const buf = Buffer.from(output, "utf-8");
|
|
2566
2563
|
const trailer = `
|
|
2567
2564
|
|
|
2568
2565
|
[Output truncated. Full output: ${overflowPath}]`;
|
|
2566
|
+
if (mode === "head-tail") {
|
|
2567
|
+
const half = Math.floor(maxBytes / 2);
|
|
2568
|
+
const head = decodeWholeCodePoints(buf.subarray(0, half));
|
|
2569
|
+
const tail = decodeWholeCodePoints(buf.subarray(buf.length - half));
|
|
2570
|
+
return {
|
|
2571
|
+
content: `${head}
|
|
2572
|
+
|
|
2573
|
+
[\u2026 ${String(originalBytes - maxBytes)} bytes omitted \u2026]
|
|
2574
|
+
|
|
2575
|
+
${tail}${trailer}`,
|
|
2576
|
+
truncated: true,
|
|
2577
|
+
originalBytes,
|
|
2578
|
+
overflowPath
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2569
2581
|
return {
|
|
2570
|
-
content:
|
|
2582
|
+
content: decodeWholeCodePoints(buf.subarray(0, maxBytes)) + trailer,
|
|
2571
2583
|
truncated: true,
|
|
2584
|
+
originalBytes,
|
|
2572
2585
|
overflowPath
|
|
2573
2586
|
};
|
|
2574
2587
|
}
|