@cryer/star-cli 0.3.5 → 0.3.7
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/README.md +97 -38
- package/README.zh-CN.md +95 -39
- package/dist/chunk-4X25DNTF.js +1441 -0
- package/dist/chunk-4X25DNTF.js.map +1 -0
- package/dist/{chunk-OIDB2E5J.js → chunk-OOOW7FC4.js} +1574 -200
- package/dist/chunk-OOOW7FC4.js.map +1 -0
- package/dist/{loop-H5RYTO2O.js → loop-DVVPIOS7.js} +2 -3
- package/dist/main.js +126 -5975
- package/dist/main.js.map +1 -1
- package/dist/repl-RT6HBM5B.js +5464 -0
- package/dist/repl-RT6HBM5B.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-B6P4VUBE.js +0 -122
- package/dist/chunk-B6P4VUBE.js.map +0 -1
- package/dist/chunk-OIDB2E5J.js.map +0 -1
- package/dist/stream-H4BYBVAZ.js +0 -184
- package/dist/stream-H4BYBVAZ.js.map +0 -1
- /package/dist/{loop-H5RYTO2O.js.map → loop-DVVPIOS7.js.map} +0 -0
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
computeRetryDelayMs,
|
|
4
|
-
gitTreesDir,
|
|
5
|
-
isRetryableStreamError,
|
|
6
|
-
responseBodyOf,
|
|
7
|
-
starHome,
|
|
8
|
-
summarizeStreamError,
|
|
9
|
-
userMemoryPath
|
|
10
|
-
} from "./chunk-B6P4VUBE.js";
|
|
11
2
|
|
|
12
3
|
// src/agent/loop.ts
|
|
13
4
|
import { tool as aiTool, generateText as generateText3 } from "ai";
|
|
@@ -34,6 +25,25 @@ var ModelConfigSchema = z.object({
|
|
|
34
25
|
// Per-model context window; overrides the top-level contextMaxTokens for
|
|
35
26
|
// compaction and the status bar when set.
|
|
36
27
|
contextMaxTokens: z.number().int().positive().optional(),
|
|
28
|
+
// Reasoning intensity for thinking models, forwarded verbatim as
|
|
29
|
+
// OpenAI-style reasoning_effort on every request (llm/provider.ts
|
|
30
|
+
// reasoningEffortMetadata) — level naming varies by model/provider
|
|
31
|
+
// (low|medium|high are common; some add minimal/max/xhigh/none), so any
|
|
32
|
+
// non-empty string is accepted and the server validates it. Unset = the
|
|
33
|
+
// request carries no effort field and the server-side default applies.
|
|
34
|
+
// Anthropic maps effort to a thinking budget instead — ignored there.
|
|
35
|
+
reasoningEffort: z.string().min(1).optional(),
|
|
36
|
+
// Sampling temperature forwarded on every request. Unset = the AI SDK's
|
|
37
|
+
// own default applies (ai@4 sends 0 — it does NOT omit the field). Some
|
|
38
|
+
// endpoints reject anything but one explicit value (e.g. kimi-for-coding:
|
|
39
|
+
// "invalid temperature: only 1 is allowed") — set it here. No upper
|
|
40
|
+
// bound: ranges vary by provider and the server validates.
|
|
41
|
+
temperature: z.number().min(0).optional(),
|
|
42
|
+
// Per-model overrides of the top-level stream watchdog timeouts (unset =
|
|
43
|
+
// the global values apply). Raise streamIdleTimeoutSec for relays that
|
|
44
|
+
// buffer long generations and stall mid-stream between flushes.
|
|
45
|
+
streamIdleTimeoutSec: z.number().positive().optional(),
|
|
46
|
+
streamFirstChunkTimeoutSec: z.number().positive().optional(),
|
|
37
47
|
// Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
|
|
38
48
|
// the model is treated as unpriced.
|
|
39
49
|
promptPrice: z.number().nonnegative().optional(),
|
|
@@ -83,10 +93,12 @@ var ConfigSchema = z.object({
|
|
|
83
93
|
compactThresholdTokens: z.number().int().positive().optional(),
|
|
84
94
|
// Seconds without any stream part before a stalled response is ended
|
|
85
95
|
// gracefully (some relays never send the terminal chunks). Applies once
|
|
86
|
-
//
|
|
96
|
+
// content has started streaming; the wait for the first content part gets
|
|
97
|
+
// a longer allowance (control/metadata parts don't count as content).
|
|
87
98
|
streamIdleTimeoutSec: z.number().positive().default(20),
|
|
88
|
-
// Seconds to wait for the very first
|
|
89
|
-
// thinking-model
|
|
99
|
+
// Seconds to wait for the very first content part before giving up; slow
|
|
100
|
+
// thinking-model endpoints can stay silent for minutes after accepting
|
|
101
|
+
// the request.
|
|
90
102
|
streamFirstChunkTimeoutSec: z.number().positive().default(300),
|
|
91
103
|
// Extra attempts per model request when a stream fails transiently
|
|
92
104
|
// (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
|
|
@@ -142,11 +154,13 @@ import { generateText } from "ai";
|
|
|
142
154
|
var CHARS_PER_TOKEN = 4;
|
|
143
155
|
var MESSAGE_OVERHEAD = 4;
|
|
144
156
|
var IMAGE_PART_TOKENS = 1024;
|
|
145
|
-
|
|
157
|
+
function isCjkCode(code) {
|
|
158
|
+
return code >= 12288 && code <= 12543 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65519;
|
|
159
|
+
}
|
|
146
160
|
function textWeight(text) {
|
|
147
161
|
let cjk = 0;
|
|
148
|
-
for (
|
|
149
|
-
if (
|
|
162
|
+
for (let i = 0; i < text.length; i++) {
|
|
163
|
+
if (isCjkCode(text.charCodeAt(i))) cjk++;
|
|
150
164
|
}
|
|
151
165
|
return text.length + cjk * (CHARS_PER_TOKEN - 1);
|
|
152
166
|
}
|
|
@@ -175,8 +189,13 @@ function imagePartCount(message) {
|
|
|
175
189
|
if (typeof message.content === "string" || message.role === "tool") return 0;
|
|
176
190
|
return message.content.filter((part) => part.type === "image" || part.type === "file").length;
|
|
177
191
|
}
|
|
192
|
+
var tokenCache = /* @__PURE__ */ new WeakMap();
|
|
178
193
|
function estimateMessageTokens(message) {
|
|
179
|
-
|
|
194
|
+
const cached = tokenCache.get(message);
|
|
195
|
+
if (cached !== void 0) return cached;
|
|
196
|
+
const tokens = Math.ceil(contentCharLength(message) / CHARS_PER_TOKEN) + MESSAGE_OVERHEAD + imagePartCount(message) * IMAGE_PART_TOKENS;
|
|
197
|
+
tokenCache.set(message, tokens);
|
|
198
|
+
return tokens;
|
|
180
199
|
}
|
|
181
200
|
function estimateTokens(messages) {
|
|
182
201
|
return messages.reduce((total, message) => total + estimateMessageTokens(message), 0);
|
|
@@ -214,7 +233,7 @@ function serializeMessage(message) {
|
|
|
214
233
|
});
|
|
215
234
|
return `${message.role}: ${parts.join("\n")}`;
|
|
216
235
|
}
|
|
217
|
-
async function summarizeMessages(messages, model, signal) {
|
|
236
|
+
async function summarizeMessages(messages, model, signal, temperature) {
|
|
218
237
|
const transcript = messages.map(serializeMessage).join("\n");
|
|
219
238
|
const { text } = await generateText({
|
|
220
239
|
model,
|
|
@@ -222,6 +241,7 @@ async function summarizeMessages(messages, model, signal) {
|
|
|
222
241
|
prompt: `Summarize this conversation so far:
|
|
223
242
|
|
|
224
243
|
${transcript}`,
|
|
244
|
+
temperature,
|
|
225
245
|
// A hung relay must not stall the whole turn on the summary: cap it at
|
|
226
246
|
// 60s and let the caller's abort (Esc) cut it short as well; the caller
|
|
227
247
|
// falls back to the truncation placeholder on any failure.
|
|
@@ -238,26 +258,29 @@ function compactMessages(messages, maxTokens, opts = {}) {
|
|
|
238
258
|
const hasSystem = first?.role === "system";
|
|
239
259
|
const head = hasSystem && first ? [first] : [];
|
|
240
260
|
const rest = hasSystem ? messages.slice(1) : messages.slice();
|
|
261
|
+
const messageTokens = rest.map((message) => estimateMessageTokens(message));
|
|
241
262
|
const turns = [];
|
|
242
|
-
for (
|
|
263
|
+
for (let i = 0; i < rest.length; i++) {
|
|
243
264
|
const current = turns[turns.length - 1];
|
|
244
|
-
if (
|
|
245
|
-
turns.push([
|
|
265
|
+
if (rest[i]?.role === "user" || !current) {
|
|
266
|
+
turns.push({ length: 1, tokens: messageTokens[i] ?? 0 });
|
|
246
267
|
} else {
|
|
247
|
-
current.
|
|
268
|
+
current.length += 1;
|
|
269
|
+
current.tokens += messageTokens[i] ?? 0;
|
|
248
270
|
}
|
|
249
271
|
}
|
|
272
|
+
const headTokens = estimateTokens(head);
|
|
273
|
+
const restTokens = estimateTokens(rest);
|
|
250
274
|
let droppedCount = 0;
|
|
251
|
-
let
|
|
252
|
-
|
|
253
|
-
const turn = turns[turnIndex];
|
|
275
|
+
let droppedTokens = 0;
|
|
276
|
+
for (const turn of turns) {
|
|
254
277
|
if (rest.length - droppedCount - turn.length < MIN_KEPT_MESSAGES) {
|
|
255
278
|
break;
|
|
256
279
|
}
|
|
257
280
|
droppedCount += turn.length;
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
281
|
+
droppedTokens += turn.tokens;
|
|
282
|
+
const candidateTokens = headTokens + estimateMessageTokens(placeholderMessage(droppedCount)) + (restTokens - droppedTokens);
|
|
283
|
+
if (candidateTokens <= limit) {
|
|
261
284
|
break;
|
|
262
285
|
}
|
|
263
286
|
}
|
|
@@ -382,6 +405,12 @@ ${unstaged}`);
|
|
|
382
405
|
return { status, diff: text, truncated };
|
|
383
406
|
}
|
|
384
407
|
|
|
408
|
+
// src/core/http-error.ts
|
|
409
|
+
function responseBodyOf(error) {
|
|
410
|
+
const body = error.responseBody;
|
|
411
|
+
return typeof body === "string" && body.length > 0 ? body : void 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
385
414
|
// src/core/image.ts
|
|
386
415
|
var MAX_IMAGE_DIMENSION = 2e3;
|
|
387
416
|
var OVERSIZED_IMAGE_FALLBACK_BYTES = 5 * 1024 * 1024;
|
|
@@ -681,9 +710,365 @@ async function runHooks(event, hooks2, ctx) {
|
|
|
681
710
|
return result;
|
|
682
711
|
}
|
|
683
712
|
|
|
713
|
+
// src/llm/retry.ts
|
|
714
|
+
var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
715
|
+
"AI_NoSuchToolError",
|
|
716
|
+
"AI_InvalidToolArgumentsError",
|
|
717
|
+
"AI_InvalidPromptError",
|
|
718
|
+
"AI_NoSuchModelError",
|
|
719
|
+
"AI_LoadAPIKeyError",
|
|
720
|
+
"AI_TypeValidationError",
|
|
721
|
+
"AI_JSONParseError"
|
|
722
|
+
]);
|
|
723
|
+
var RETRYABLE_MESSAGE_PATTERNS = [
|
|
724
|
+
/\b(408|425|429|500|502|503|504|520|521|522|523|524|529)\b/,
|
|
725
|
+
/rate.?limit|too many requests|rate increased/i,
|
|
726
|
+
/overloaded|service.?unavailable|internal (server )?error|server error|provider returned error|bad gateway|gateway timeout/i,
|
|
727
|
+
/terminated|fetch failed|failed to fetch|network.?error|upstream connect|connection (?:error|refused|lost|reset)|socket (?:hang up|connection)|reset before headers|getaddrinfo|enotfound|eai_again|econnrefused|econnreset|etimedout|epipe|ehostunreach|enetunreach/i,
|
|
728
|
+
/\b(?:request|response|connection|network|stream|read|headers?|body) (?:timeout|timed out|time out)\b/i,
|
|
729
|
+
/resource.?exhausted|try (?:your request )?again|at capacity/i
|
|
730
|
+
];
|
|
731
|
+
function statusCodeOf(error) {
|
|
732
|
+
const status = error.statusCode;
|
|
733
|
+
return typeof status === "number" ? status : void 0;
|
|
734
|
+
}
|
|
735
|
+
function matchesRetryablePattern(text) {
|
|
736
|
+
return typeof text === "string" && RETRYABLE_MESSAGE_PATTERNS.some((p) => p.test(text));
|
|
737
|
+
}
|
|
738
|
+
function isRetryableStreamError(error) {
|
|
739
|
+
if (NON_RETRYABLE_ERROR_NAMES.has(error.name)) return false;
|
|
740
|
+
if (error.isRetryable === true) return true;
|
|
741
|
+
const status = statusCodeOf(error);
|
|
742
|
+
if (status !== void 0) {
|
|
743
|
+
if (status === 408 || status === 429 || status >= 500 && status !== 501) return true;
|
|
744
|
+
return matchesRetryablePattern(error.message) || matchesRetryablePattern(responseBodyOf(error));
|
|
745
|
+
}
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
var RETRY_JITTER_FACTOR = 0.25;
|
|
749
|
+
var MAX_RETRY_DELAY_MS = 6e4;
|
|
750
|
+
function retryAfterDelayMs(error) {
|
|
751
|
+
const headers = error.responseHeaders;
|
|
752
|
+
if (!headers || typeof headers !== "object") return void 0;
|
|
753
|
+
const normalized = {};
|
|
754
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
755
|
+
if (typeof value === "string") normalized[key.toLowerCase()] = value;
|
|
756
|
+
}
|
|
757
|
+
const afterMs = Number.parseFloat(normalized["retry-after-ms"] ?? "");
|
|
758
|
+
if (!Number.isNaN(afterMs)) return Math.max(0, afterMs);
|
|
759
|
+
const after = normalized["retry-after"];
|
|
760
|
+
if (after) {
|
|
761
|
+
const seconds = Number.parseFloat(after);
|
|
762
|
+
if (!Number.isNaN(seconds)) return Math.max(0, Math.ceil(seconds * 1e3));
|
|
763
|
+
const dateMs = Date.parse(after) - Date.now();
|
|
764
|
+
if (!Number.isNaN(dateMs) && dateMs > 0) return Math.ceil(dateMs);
|
|
765
|
+
}
|
|
766
|
+
return void 0;
|
|
767
|
+
}
|
|
768
|
+
function computeRetryDelayMs(attempt, baseMs, error, random = Math.random()) {
|
|
769
|
+
const hinted = error ? retryAfterDelayMs(error) : void 0;
|
|
770
|
+
if (hinted !== void 0) return Math.min(hinted, MAX_RETRY_DELAY_MS);
|
|
771
|
+
const base = baseMs * 2 ** attempt;
|
|
772
|
+
return Math.min(Math.ceil(base + base * RETRY_JITTER_FACTOR * random), MAX_RETRY_DELAY_MS);
|
|
773
|
+
}
|
|
774
|
+
function summarizeStreamError(error) {
|
|
775
|
+
const parts = [error.message || error.name];
|
|
776
|
+
const status = statusCodeOf(error);
|
|
777
|
+
if (status !== void 0 && !error.message.includes(String(status))) {
|
|
778
|
+
parts.push(`(HTTP ${status})`);
|
|
779
|
+
}
|
|
780
|
+
const body = responseBodyOf(error);
|
|
781
|
+
if (body && !error.message.includes(body.slice(0, 40))) {
|
|
782
|
+
const excerpt = body.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
783
|
+
if (excerpt) parts.push(`\u2014 ${excerpt}`);
|
|
784
|
+
}
|
|
785
|
+
return parts.join(" ");
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/llm/stream.ts
|
|
789
|
+
import { streamText } from "ai";
|
|
790
|
+
|
|
791
|
+
// src/llm/activity.ts
|
|
792
|
+
var trackers = /* @__PURE__ */ new WeakMap();
|
|
793
|
+
function streamActivity(signal) {
|
|
794
|
+
return trackers.get(signal);
|
|
795
|
+
}
|
|
796
|
+
function wrapFetchWithActivity(baseFetch = fetch) {
|
|
797
|
+
return async (input, init) => {
|
|
798
|
+
const response = await baseFetch(input, init);
|
|
799
|
+
const signal = init?.signal ?? (input instanceof Request ? input.signal : null);
|
|
800
|
+
if (!signal || !response.body) return response;
|
|
801
|
+
const tracker = { lastChunkAt: Date.now() };
|
|
802
|
+
trackers.set(signal, tracker);
|
|
803
|
+
const reader = response.body.getReader();
|
|
804
|
+
const body = new ReadableStream({
|
|
805
|
+
async pull(controller) {
|
|
806
|
+
try {
|
|
807
|
+
const { done, value } = await reader.read();
|
|
808
|
+
if (done) {
|
|
809
|
+
controller.close();
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
tracker.lastChunkAt = Date.now();
|
|
813
|
+
controller.enqueue(value);
|
|
814
|
+
} catch (error) {
|
|
815
|
+
controller.error(error);
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
cancel(reason) {
|
|
819
|
+
return reader.cancel(reason);
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
return new Response(body, {
|
|
823
|
+
status: response.status,
|
|
824
|
+
statusText: response.statusText,
|
|
825
|
+
headers: response.headers
|
|
826
|
+
});
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/llm/debug.ts
|
|
831
|
+
import { appendFileSync } from "fs";
|
|
832
|
+
import path2 from "path";
|
|
833
|
+
|
|
834
|
+
// src/config/paths.ts
|
|
835
|
+
import os from "os";
|
|
836
|
+
import path from "path";
|
|
837
|
+
function starHome() {
|
|
838
|
+
return process.env.STAR_HOME ?? path.join(os.homedir(), ".star-cli");
|
|
839
|
+
}
|
|
840
|
+
function globalConfigPath() {
|
|
841
|
+
return path.join(starHome(), "config.toml");
|
|
842
|
+
}
|
|
843
|
+
function sessionsDir() {
|
|
844
|
+
return path.join(starHome(), "sessions");
|
|
845
|
+
}
|
|
846
|
+
function userMemoryPath() {
|
|
847
|
+
return path.join(starHome(), "MEMORY.md");
|
|
848
|
+
}
|
|
849
|
+
function gitTreesDir() {
|
|
850
|
+
return path.join(starHome(), "git-trees");
|
|
851
|
+
}
|
|
852
|
+
function envFilePath() {
|
|
853
|
+
return path.join(starHome(), ".env");
|
|
854
|
+
}
|
|
855
|
+
function projectConfigPath(cwd) {
|
|
856
|
+
return path.join(cwd, ".star", "config.toml");
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/llm/debug.ts
|
|
860
|
+
function debugStreamLog(event, data) {
|
|
861
|
+
if (!process.env.STAR_DEBUG) return;
|
|
862
|
+
try {
|
|
863
|
+
const line = `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), event, ...data })}
|
|
864
|
+
`;
|
|
865
|
+
appendFileSync(path2.join(starHome(), "debug.log"), line);
|
|
866
|
+
} catch {
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/llm/stream.ts
|
|
871
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 2e4;
|
|
872
|
+
var DEFAULT_FIRST_PART_TIMEOUT_MS = 12e4;
|
|
873
|
+
var GENERATION_PART_TYPES = /* @__PURE__ */ new Set(["text-delta", "reasoning", "tool-call", "tool-call-delta"]);
|
|
874
|
+
var MAX_KEEPALIVE_SILENCE_MS = 10 * 6e4;
|
|
875
|
+
var CACHE_READ_KEYS = ["cacheReadInputTokens", "cache_read_input_tokens"];
|
|
876
|
+
var CACHED_PROMPT_KEYS = ["cachedPromptTokens", "cached_prompt_tokens", "cachedTokens"];
|
|
877
|
+
function extractCacheUsage(providerMetadata) {
|
|
878
|
+
if (!providerMetadata || typeof providerMetadata !== "object") return {};
|
|
879
|
+
for (const value of Object.values(providerMetadata)) {
|
|
880
|
+
if (!value || typeof value !== "object") continue;
|
|
881
|
+
const record = value;
|
|
882
|
+
for (const key of CACHE_READ_KEYS) {
|
|
883
|
+
const n = record[key];
|
|
884
|
+
if (typeof n === "number" && Number.isFinite(n)) return { cacheReadInputTokens: n };
|
|
885
|
+
}
|
|
886
|
+
for (const key of CACHED_PROMPT_KEYS) {
|
|
887
|
+
const n = record[key];
|
|
888
|
+
if (typeof n === "number" && Number.isFinite(n)) return { cachedPromptTokens: n };
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return {};
|
|
892
|
+
}
|
|
893
|
+
async function* streamChat(opts) {
|
|
894
|
+
const controller = new AbortController();
|
|
895
|
+
const onAbort = () => controller.abort();
|
|
896
|
+
opts.abortSignal?.addEventListener("abort", onAbort);
|
|
897
|
+
if (opts.abortSignal?.aborted) controller.abort();
|
|
898
|
+
const { openai: openaiMetadata, ...otherMetadata } = opts.providerMetadata ?? {};
|
|
899
|
+
const result = streamText({
|
|
900
|
+
model: opts.model,
|
|
901
|
+
messages: opts.messages,
|
|
902
|
+
tools: opts.tools,
|
|
903
|
+
abortSignal: controller.signal,
|
|
904
|
+
maxTokens: opts.maxTokens,
|
|
905
|
+
temperature: opts.temperature,
|
|
906
|
+
// Disable the SDK's own retries (ai@4 defaults to 2 internal attempts with
|
|
907
|
+
// fixed backoff that ignores Retry-After and the abort signal, and
|
|
908
|
+
// exhaustion throws an AI_RetryError stripped of status/headers/body) —
|
|
909
|
+
// the retry policy in agent/loop.ts + llm/retry.ts owns the full budget
|
|
910
|
+
// and needs the raw error to classify.
|
|
911
|
+
maxRetries: 0,
|
|
912
|
+
// Pass tool-call argument deltas through (instead of buffering until the
|
|
913
|
+
// complete call) so large write_file payloads report receive progress.
|
|
914
|
+
toolCallStreaming: true,
|
|
915
|
+
experimental_providerMetadata: {
|
|
916
|
+
// Only the openai provider (our responses-protocol path) reads these;
|
|
917
|
+
// other providers ignore unknown metadata. The SDK's responses provider
|
|
918
|
+
// sends every tool with strict:true by default (strictSchemas), and
|
|
919
|
+
// strict mode requires every schema property to be required — our
|
|
920
|
+
// classic function-calling schemas use optional properties, which
|
|
921
|
+
// relays then reject. store:false matches how codex and opencode talk
|
|
922
|
+
// to relays (no server-side response storage).
|
|
923
|
+
...otherMetadata,
|
|
924
|
+
openai: { strictSchemas: false, store: false, ...openaiMetadata }
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
const idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
928
|
+
const firstPartTimeoutMs = opts.firstPartTimeoutMs ?? DEFAULT_FIRST_PART_TIMEOUT_MS;
|
|
929
|
+
const iterator = result.fullStream[Symbol.asyncIterator]();
|
|
930
|
+
let seenContent = false;
|
|
931
|
+
let deltas = 0;
|
|
932
|
+
let hasContent = false;
|
|
933
|
+
const argsProgress = /* @__PURE__ */ new Map();
|
|
934
|
+
let lastPartAt = Date.now();
|
|
935
|
+
debugStreamLog("request", {
|
|
936
|
+
messages: opts.messages.length,
|
|
937
|
+
idleTimeoutMs,
|
|
938
|
+
firstPartTimeoutMs
|
|
939
|
+
});
|
|
940
|
+
try {
|
|
941
|
+
for (; ; ) {
|
|
942
|
+
let timer;
|
|
943
|
+
const windowMs = seenContent ? idleTimeoutMs : firstPartTimeoutMs;
|
|
944
|
+
const idle = new Promise((resolve) => {
|
|
945
|
+
timer = setTimeout(() => resolve(null), windowMs);
|
|
946
|
+
});
|
|
947
|
+
let next;
|
|
948
|
+
try {
|
|
949
|
+
next = await Promise.race([iterator.next(), idle]);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
debugStreamLog("stream-throw", {
|
|
952
|
+
seenContent,
|
|
953
|
+
deltas,
|
|
954
|
+
error: error instanceof Error ? summarizeStreamError(error) : String(error)
|
|
955
|
+
});
|
|
956
|
+
throw error;
|
|
957
|
+
} finally {
|
|
958
|
+
if (timer) clearTimeout(timer);
|
|
959
|
+
}
|
|
960
|
+
if (next === null) {
|
|
961
|
+
const activity = streamActivity(controller.signal);
|
|
962
|
+
const silentForMs = Date.now() - lastPartAt;
|
|
963
|
+
const lastChunkAgoMs = activity ? Date.now() - activity.lastChunkAt : void 0;
|
|
964
|
+
if (lastChunkAgoMs !== void 0 && lastChunkAgoMs < windowMs && silentForMs < MAX_KEEPALIVE_SILENCE_MS) {
|
|
965
|
+
debugStreamLog("keepalive", { seenContent, deltas, lastChunkAgoMs, silentForMs });
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
debugStreamLog("idle-timeout", {
|
|
969
|
+
seenContent,
|
|
970
|
+
deltas,
|
|
971
|
+
waitedMs: windowMs,
|
|
972
|
+
lastChunkAgoMs
|
|
973
|
+
});
|
|
974
|
+
yield {
|
|
975
|
+
type: "finish",
|
|
976
|
+
finishReason: "idle-timeout",
|
|
977
|
+
usage: void 0,
|
|
978
|
+
// Content already streamed means the reply may be cut off
|
|
979
|
+
// mid-thought; the loop surfaces a notice for that.
|
|
980
|
+
...hasContent ? { truncated: true } : {}
|
|
981
|
+
};
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (next.done) {
|
|
985
|
+
debugStreamLog("end", { seenContent, deltas });
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const part = next.value;
|
|
989
|
+
lastPartAt = Date.now();
|
|
990
|
+
if (GENERATION_PART_TYPES.has(part.type)) seenContent = true;
|
|
991
|
+
switch (part.type) {
|
|
992
|
+
case "text-delta":
|
|
993
|
+
deltas++;
|
|
994
|
+
hasContent = true;
|
|
995
|
+
yield { type: "text-delta", text: part.textDelta };
|
|
996
|
+
break;
|
|
997
|
+
case "reasoning":
|
|
998
|
+
deltas++;
|
|
999
|
+
yield { type: "reasoning", text: part.textDelta };
|
|
1000
|
+
break;
|
|
1001
|
+
case "tool-call":
|
|
1002
|
+
hasContent = true;
|
|
1003
|
+
yield {
|
|
1004
|
+
type: "tool-call",
|
|
1005
|
+
id: part.toolCallId,
|
|
1006
|
+
name: part.toolName,
|
|
1007
|
+
args: part.args
|
|
1008
|
+
};
|
|
1009
|
+
break;
|
|
1010
|
+
case "tool-call-streaming-start": {
|
|
1011
|
+
argsProgress.set(part.toolCallId, {
|
|
1012
|
+
name: part.toolName,
|
|
1013
|
+
chars: 0,
|
|
1014
|
+
nextMark: 4096
|
|
1015
|
+
});
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
case "tool-call-delta": {
|
|
1019
|
+
const progress = argsProgress.get(part.toolCallId) ?? {
|
|
1020
|
+
name: part.toolName,
|
|
1021
|
+
chars: 0,
|
|
1022
|
+
nextMark: 4096
|
|
1023
|
+
};
|
|
1024
|
+
progress.chars += part.argsTextDelta.length;
|
|
1025
|
+
argsProgress.set(part.toolCallId, progress);
|
|
1026
|
+
if (progress.chars >= progress.nextMark) {
|
|
1027
|
+
progress.nextMark *= 2;
|
|
1028
|
+
yield { type: "tool-call-progress", name: progress.name, bytes: progress.chars };
|
|
1029
|
+
}
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
case "finish": {
|
|
1033
|
+
const finite = (n) => Number.isFinite(n) ? n : 0;
|
|
1034
|
+
const cache4 = extractCacheUsage(
|
|
1035
|
+
part.providerMetadata ?? part.experimental_providerMetadata
|
|
1036
|
+
);
|
|
1037
|
+
debugStreamLog("finish", {
|
|
1038
|
+
finishReason: part.finishReason,
|
|
1039
|
+
usage: part.usage,
|
|
1040
|
+
deltas
|
|
1041
|
+
});
|
|
1042
|
+
yield {
|
|
1043
|
+
type: "finish",
|
|
1044
|
+
finishReason: part.finishReason,
|
|
1045
|
+
usage: part.usage ? {
|
|
1046
|
+
promptTokens: finite(part.usage.promptTokens),
|
|
1047
|
+
completionTokens: finite(part.usage.completionTokens),
|
|
1048
|
+
totalTokens: finite(part.usage.totalTokens),
|
|
1049
|
+
...cache4
|
|
1050
|
+
} : void 0
|
|
1051
|
+
};
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
case "error": {
|
|
1055
|
+
if (opts.abortSignal?.aborted) return;
|
|
1056
|
+
const error = part.error instanceof Error ? part.error : new Error(String(part.error));
|
|
1057
|
+
debugStreamLog("error-part", { seenContent, deltas, error: summarizeStreamError(error) });
|
|
1058
|
+
yield { type: "error", error };
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
} finally {
|
|
1064
|
+
opts.abortSignal?.removeEventListener("abort", onAbort);
|
|
1065
|
+
controller.abort();
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
684
1069
|
// src/permissions/gate.ts
|
|
685
1070
|
import fs from "fs";
|
|
686
|
-
import
|
|
1071
|
+
import path3 from "path";
|
|
687
1072
|
|
|
688
1073
|
// src/core/sensitive.ts
|
|
689
1074
|
var SENSITIVE_BASENAMES = /* @__PURE__ */ new Set([
|
|
@@ -726,6 +1111,239 @@ function isSensitivePath(p) {
|
|
|
726
1111
|
return false;
|
|
727
1112
|
}
|
|
728
1113
|
|
|
1114
|
+
// src/permissions/shell.ts
|
|
1115
|
+
var REDIRECT_OPS = /* @__PURE__ */ new Set([">", ">>", "<", ">&", "<&"]);
|
|
1116
|
+
var FILE_REDIRECT_OPS = /* @__PURE__ */ new Set([">", ">>", "<"]);
|
|
1117
|
+
var SEGMENT_SEPARATORS = /* @__PURE__ */ new Set([
|
|
1118
|
+
"&&",
|
|
1119
|
+
"||",
|
|
1120
|
+
"&",
|
|
1121
|
+
"|",
|
|
1122
|
+
";",
|
|
1123
|
+
"\n",
|
|
1124
|
+
"(",
|
|
1125
|
+
")"
|
|
1126
|
+
]);
|
|
1127
|
+
function lexShell(command) {
|
|
1128
|
+
const tokens = [];
|
|
1129
|
+
let word = "";
|
|
1130
|
+
let doubleQuoted = false;
|
|
1131
|
+
let i = 0;
|
|
1132
|
+
const flush = () => {
|
|
1133
|
+
if (word.length > 0) {
|
|
1134
|
+
tokens.push({ kind: "word", text: word });
|
|
1135
|
+
word = "";
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
const findCloseParen = (from) => {
|
|
1139
|
+
let depth = 0;
|
|
1140
|
+
let j = from;
|
|
1141
|
+
while (j < command.length) {
|
|
1142
|
+
const c = command[j];
|
|
1143
|
+
if (c === "\\") {
|
|
1144
|
+
j += 2;
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (c === "'" || c === '"') {
|
|
1148
|
+
const close = command.indexOf(c, j + 1);
|
|
1149
|
+
j = close === -1 ? command.length : close + 1;
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
if (c === "(") depth += 1;
|
|
1153
|
+
if (c === ")") {
|
|
1154
|
+
depth -= 1;
|
|
1155
|
+
if (depth === 0) return j + 1;
|
|
1156
|
+
}
|
|
1157
|
+
j += 1;
|
|
1158
|
+
}
|
|
1159
|
+
return -1;
|
|
1160
|
+
};
|
|
1161
|
+
const pushSubstitution = (openParen, process2) => {
|
|
1162
|
+
const close = findCloseParen(openParen);
|
|
1163
|
+
const innerEnd = close === -1 ? command.length : close - 1;
|
|
1164
|
+
flush();
|
|
1165
|
+
tokens.push({ kind: "subst", inner: command.slice(openParen + 1, innerEnd), process: process2 });
|
|
1166
|
+
return close === -1 ? command.length : close;
|
|
1167
|
+
};
|
|
1168
|
+
const lexDollarOrBacktick = (pos) => {
|
|
1169
|
+
const c = command[pos];
|
|
1170
|
+
if (c === "`") {
|
|
1171
|
+
const close = command.indexOf("`", pos + 1);
|
|
1172
|
+
const end = close === -1 ? command.length : close;
|
|
1173
|
+
flush();
|
|
1174
|
+
tokens.push({ kind: "subst", inner: command.slice(pos + 1, end), process: false });
|
|
1175
|
+
return close === -1 ? command.length : close + 1;
|
|
1176
|
+
}
|
|
1177
|
+
const next = command[pos + 1];
|
|
1178
|
+
if (next === "(") {
|
|
1179
|
+
return pushSubstitution(pos + 1, false);
|
|
1180
|
+
}
|
|
1181
|
+
if (next === "{") {
|
|
1182
|
+
const close = command.indexOf("}", pos + 2);
|
|
1183
|
+
if (close !== -1) {
|
|
1184
|
+
word += `$${command.slice(pos + 2, close)}`;
|
|
1185
|
+
return close + 1;
|
|
1186
|
+
}
|
|
1187
|
+
word += "$";
|
|
1188
|
+
return pos + 1;
|
|
1189
|
+
}
|
|
1190
|
+
if (next !== void 0 && /[A-Za-z_]/.test(next)) {
|
|
1191
|
+
let end = pos + 1;
|
|
1192
|
+
while (end < command.length && /[A-Za-z0-9_]/.test(command[end] ?? "")) {
|
|
1193
|
+
end += 1;
|
|
1194
|
+
}
|
|
1195
|
+
word += command.slice(pos, end);
|
|
1196
|
+
return end;
|
|
1197
|
+
}
|
|
1198
|
+
word += "$";
|
|
1199
|
+
return pos + 1;
|
|
1200
|
+
};
|
|
1201
|
+
while (i < command.length) {
|
|
1202
|
+
const c = command[i];
|
|
1203
|
+
if (doubleQuoted) {
|
|
1204
|
+
if (c === '"') {
|
|
1205
|
+
doubleQuoted = false;
|
|
1206
|
+
i += 1;
|
|
1207
|
+
} else if (c === "\\") {
|
|
1208
|
+
if (i + 1 < command.length) {
|
|
1209
|
+
word += command[i + 1];
|
|
1210
|
+
i += 2;
|
|
1211
|
+
} else {
|
|
1212
|
+
word += "\\";
|
|
1213
|
+
i += 1;
|
|
1214
|
+
}
|
|
1215
|
+
} else if (c === "$" || c === "`") {
|
|
1216
|
+
i = lexDollarOrBacktick(i);
|
|
1217
|
+
} else {
|
|
1218
|
+
word += c;
|
|
1219
|
+
i += 1;
|
|
1220
|
+
}
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (c === "\\") {
|
|
1224
|
+
if (i + 1 < command.length) {
|
|
1225
|
+
word += command[i + 1];
|
|
1226
|
+
i += 2;
|
|
1227
|
+
} else {
|
|
1228
|
+
word += "\\";
|
|
1229
|
+
i += 1;
|
|
1230
|
+
}
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (c === "'") {
|
|
1234
|
+
const close = command.indexOf("'", i + 1);
|
|
1235
|
+
const end = close === -1 ? command.length : close;
|
|
1236
|
+
word += command.slice(i + 1, end);
|
|
1237
|
+
i = close === -1 ? command.length : close + 1;
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
if (c === '"') {
|
|
1241
|
+
doubleQuoted = true;
|
|
1242
|
+
i += 1;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (c === "$" || c === "`") {
|
|
1246
|
+
i = lexDollarOrBacktick(i);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (c === " " || c === " " || c === "\r") {
|
|
1250
|
+
flush();
|
|
1251
|
+
i += 1;
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
if (c === "\n") {
|
|
1255
|
+
flush();
|
|
1256
|
+
tokens.push({ kind: "op", op: "\n" });
|
|
1257
|
+
i += 1;
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (c === "&" || c === "|") {
|
|
1261
|
+
flush();
|
|
1262
|
+
if (command[i + 1] === c) {
|
|
1263
|
+
tokens.push({ kind: "op", op: c === "&" ? "&&" : "||" });
|
|
1264
|
+
i += 2;
|
|
1265
|
+
} else {
|
|
1266
|
+
tokens.push({ kind: "op", op: c });
|
|
1267
|
+
i += 1;
|
|
1268
|
+
}
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (c === ";" || c === "(" || c === ")") {
|
|
1272
|
+
flush();
|
|
1273
|
+
tokens.push({ kind: "op", op: c });
|
|
1274
|
+
i += 1;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (c === ">" || c === "<") {
|
|
1278
|
+
flush();
|
|
1279
|
+
const next = command[i + 1];
|
|
1280
|
+
if (next === "(") {
|
|
1281
|
+
i = pushSubstitution(i + 1, true);
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (c === ">" && next === ">") {
|
|
1285
|
+
tokens.push({ kind: "op", op: ">>" });
|
|
1286
|
+
i += 2;
|
|
1287
|
+
} else if (next === "&") {
|
|
1288
|
+
tokens.push({ kind: "op", op: c === ">" ? ">&" : "<&" });
|
|
1289
|
+
i += 2;
|
|
1290
|
+
} else {
|
|
1291
|
+
tokens.push({ kind: "op", op: c });
|
|
1292
|
+
i += 1;
|
|
1293
|
+
}
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
word += c;
|
|
1297
|
+
i += 1;
|
|
1298
|
+
}
|
|
1299
|
+
flush();
|
|
1300
|
+
return tokens;
|
|
1301
|
+
}
|
|
1302
|
+
function splitShellSegments(tokens) {
|
|
1303
|
+
const segments = [];
|
|
1304
|
+
let current = [];
|
|
1305
|
+
for (const token of tokens) {
|
|
1306
|
+
if (token.kind === "op" && SEGMENT_SEPARATORS.has(token.op)) {
|
|
1307
|
+
if (current.length > 0) {
|
|
1308
|
+
segments.push(current);
|
|
1309
|
+
current = [];
|
|
1310
|
+
}
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
current.push(token);
|
|
1314
|
+
}
|
|
1315
|
+
if (current.length > 0) {
|
|
1316
|
+
segments.push(current);
|
|
1317
|
+
}
|
|
1318
|
+
return segments;
|
|
1319
|
+
}
|
|
1320
|
+
function segmentWords(segment) {
|
|
1321
|
+
const words = [];
|
|
1322
|
+
const redirectTargets = [];
|
|
1323
|
+
for (let k = 0; k < segment.length; k += 1) {
|
|
1324
|
+
const token = segment[k];
|
|
1325
|
+
if (token.kind === "op" && REDIRECT_OPS.has(token.op)) {
|
|
1326
|
+
const target = segment[k + 1];
|
|
1327
|
+
if (target?.kind === "word") {
|
|
1328
|
+
redirectTargets.push(target.text);
|
|
1329
|
+
k += 1;
|
|
1330
|
+
}
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (token.kind === "word") {
|
|
1334
|
+
words.push(token.text);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return { words, redirectTargets };
|
|
1338
|
+
}
|
|
1339
|
+
function renderSegment(segment) {
|
|
1340
|
+
return segment.map((token) => {
|
|
1341
|
+
if (token.kind === "word") return token.text;
|
|
1342
|
+
if (token.kind === "op") return token.op;
|
|
1343
|
+
return token.process ? ">(\u2026)" : "$(\u2026)";
|
|
1344
|
+
}).join(" ");
|
|
1345
|
+
}
|
|
1346
|
+
|
|
729
1347
|
// src/permissions/allow.ts
|
|
730
1348
|
function parseAllowRule(rule) {
|
|
731
1349
|
const match = /^([\w-]+)(?:\((.*)\))?$/.exec(rule.trim());
|
|
@@ -751,7 +1369,15 @@ function matchAllowRule(rule, req) {
|
|
|
751
1369
|
}
|
|
752
1370
|
function splitCommandChain(command) {
|
|
753
1371
|
if (command.includes("$(") || command.includes("`")) return null;
|
|
754
|
-
|
|
1372
|
+
const tokens = lexShell(command);
|
|
1373
|
+
if (tokens.some((token) => token.kind === "subst")) return null;
|
|
1374
|
+
const segments = splitShellSegments(tokens);
|
|
1375
|
+
for (const segment of segments) {
|
|
1376
|
+
if (segment.some((token) => token.kind === "op" && FILE_REDIRECT_OPS.has(token.op))) {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return segments.map(renderSegment).filter((segment) => segment.length > 0);
|
|
755
1381
|
}
|
|
756
1382
|
function withCommand(req, command) {
|
|
757
1383
|
return {
|
|
@@ -788,27 +1414,206 @@ function buildAllowRule(req) {
|
|
|
788
1414
|
}
|
|
789
1415
|
|
|
790
1416
|
// src/permissions/gate.ts
|
|
791
|
-
var FILE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "read_file", "grep", "glob"]);
|
|
792
|
-
var
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
/
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1417
|
+
var FILE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "read_file", "read_image", "grep", "glob"]);
|
|
1418
|
+
var FORK_BOMB = /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/;
|
|
1419
|
+
var MAX_ANALYSIS_DEPTH = 8;
|
|
1420
|
+
function baseName(word) {
|
|
1421
|
+
const base = word.replace(/\\/g, "/").split("/").pop() ?? word;
|
|
1422
|
+
return base.toLowerCase().replace(/\.exe$/, "");
|
|
1423
|
+
}
|
|
1424
|
+
function isDangerousDeleteTarget(target) {
|
|
1425
|
+
if (/^\/+$/.test(target)) return true;
|
|
1426
|
+
const norm = target.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1427
|
+
if (norm === "/*" || norm === "*") return true;
|
|
1428
|
+
if (norm === "~" || norm.startsWith("~/")) return true;
|
|
1429
|
+
if (norm === "." || norm === "./*") return true;
|
|
1430
|
+
if (/^[a-zA-Z]:$/.test(norm)) return true;
|
|
1431
|
+
if (["$HOME", "$USERPROFILE", "$HOMEPATH"].includes(norm)) return true;
|
|
1432
|
+
if (/^%(USERPROFILE|HOMEDRIVE|HOMEPATH)%$/i.test(norm)) return true;
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
function rmIsDangerous(args) {
|
|
1436
|
+
let recursive = false;
|
|
1437
|
+
let optionsDone = false;
|
|
1438
|
+
const targets = [];
|
|
1439
|
+
for (const arg of args) {
|
|
1440
|
+
if (!optionsDone && arg === "--") {
|
|
1441
|
+
optionsDone = true;
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (!optionsDone && arg.startsWith("-")) {
|
|
1445
|
+
if (arg === "--recursive") {
|
|
1446
|
+
recursive = true;
|
|
1447
|
+
} else if (!arg.startsWith("--") && /[rR]/.test(arg)) {
|
|
1448
|
+
recursive = true;
|
|
1449
|
+
}
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
targets.push(arg);
|
|
1453
|
+
}
|
|
1454
|
+
return recursive && targets.some(isDangerousDeleteTarget);
|
|
1455
|
+
}
|
|
1456
|
+
function findIsDangerous(args) {
|
|
1457
|
+
if (!args.includes("-delete")) return false;
|
|
1458
|
+
const paths = [];
|
|
1459
|
+
for (const arg of args) {
|
|
1460
|
+
if (arg.startsWith("-") || arg === "(" || arg === ")" || arg === "!" || arg === ",") {
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
paths.push(arg);
|
|
1464
|
+
}
|
|
1465
|
+
return paths.length === 0 || paths.some(isDangerousDeleteTarget);
|
|
1466
|
+
}
|
|
1467
|
+
var XARGS_VALUE_OPTS = /* @__PURE__ */ new Set([
|
|
1468
|
+
"-I",
|
|
1469
|
+
"-n",
|
|
1470
|
+
"-P",
|
|
1471
|
+
"-s",
|
|
1472
|
+
"-L",
|
|
1473
|
+
"-d",
|
|
1474
|
+
"-E",
|
|
1475
|
+
"-a",
|
|
1476
|
+
"--replace",
|
|
1477
|
+
"--max-args",
|
|
1478
|
+
"--max-procs",
|
|
1479
|
+
"--max-lines",
|
|
1480
|
+
"--delimiter",
|
|
1481
|
+
"--eof",
|
|
1482
|
+
"--arg-file",
|
|
1483
|
+
"--interactive",
|
|
1484
|
+
"--exit"
|
|
1485
|
+
]);
|
|
1486
|
+
function xargsIsDangerous(args) {
|
|
1487
|
+
for (let k = 0; k < args.length; k += 1) {
|
|
1488
|
+
const arg = args[k];
|
|
1489
|
+
if (XARGS_VALUE_OPTS.has(arg)) {
|
|
1490
|
+
k += 1;
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
if (arg.startsWith("-")) {
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
return baseName(arg) === "rm";
|
|
1497
|
+
}
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
function chmodIsDangerous(args) {
|
|
1501
|
+
let recursive = false;
|
|
1502
|
+
let optionsDone = false;
|
|
1503
|
+
const operands = [];
|
|
1504
|
+
for (const arg of args) {
|
|
1505
|
+
if (!optionsDone && arg === "--") {
|
|
1506
|
+
optionsDone = true;
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
if (!optionsDone && arg.startsWith("-")) {
|
|
1510
|
+
if (arg === "--recursive" || !arg.startsWith("--") && arg.includes("R")) {
|
|
1511
|
+
recursive = true;
|
|
1512
|
+
}
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
operands.push(arg);
|
|
1516
|
+
}
|
|
1517
|
+
return recursive && operands.slice(1).some(isDangerousDeleteTarget);
|
|
1518
|
+
}
|
|
1519
|
+
function commandInvocation(words) {
|
|
1520
|
+
let sudo = false;
|
|
1521
|
+
let wrapped = false;
|
|
1522
|
+
let k = 0;
|
|
1523
|
+
for (; ; ) {
|
|
1524
|
+
const word = words[k];
|
|
1525
|
+
if (word === void 0) return null;
|
|
1526
|
+
const lower = word.toLowerCase();
|
|
1527
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) {
|
|
1528
|
+
k += 1;
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
if (lower === "sudo" || lower === "doas" || lower === "env") {
|
|
1532
|
+
sudo = sudo || lower !== "env";
|
|
1533
|
+
wrapped = true;
|
|
1534
|
+
k += 1;
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (wrapped && word.startsWith("-")) {
|
|
1538
|
+
k += 1;
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
return { name: baseName(word), rest: words.slice(k + 1), sudo };
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
function commandLineIsDangerous(command, depth) {
|
|
1545
|
+
if (depth > MAX_ANALYSIS_DEPTH) return true;
|
|
1546
|
+
const tokens = lexShell(command);
|
|
1547
|
+
for (const token of tokens) {
|
|
1548
|
+
if (token.kind === "subst" && commandLineIsDangerous(token.inner, depth + 1)) {
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return splitShellSegments(tokens).some((segment) => segmentIsDangerous(segment, depth));
|
|
1553
|
+
}
|
|
1554
|
+
function segmentIsDangerous(segment, depth) {
|
|
1555
|
+
const { words, redirectTargets } = segmentWords(segment);
|
|
1556
|
+
if (redirectTargets.some((target) => /^\/dev\/sd[a-z]/i.test(target))) {
|
|
1557
|
+
return true;
|
|
1558
|
+
}
|
|
1559
|
+
const invocation = commandInvocation(words);
|
|
1560
|
+
if (!invocation) return false;
|
|
1561
|
+
const { name, rest, sudo } = invocation;
|
|
1562
|
+
if (sudo && name === "rm") return true;
|
|
1563
|
+
switch (name) {
|
|
1564
|
+
case "shutdown":
|
|
1565
|
+
case "reboot":
|
|
1566
|
+
case "poweroff":
|
|
1567
|
+
case "halt":
|
|
1568
|
+
case "diskpart":
|
|
1569
|
+
case "wipefs":
|
|
1570
|
+
case "shred":
|
|
1571
|
+
return true;
|
|
1572
|
+
case "rm":
|
|
1573
|
+
return rmIsDangerous(rest);
|
|
1574
|
+
case "rmdir":
|
|
1575
|
+
case "rd":
|
|
1576
|
+
case "del":
|
|
1577
|
+
case "erase":
|
|
1578
|
+
return rest.some((arg) => /^\/s$/i.test(arg));
|
|
1579
|
+
case "format":
|
|
1580
|
+
return rest.some((arg) => /^[a-z]:$/i.test(arg));
|
|
1581
|
+
case "dd":
|
|
1582
|
+
return rest.some((arg) => arg.startsWith("if="));
|
|
1583
|
+
case "find":
|
|
1584
|
+
return findIsDangerous(rest);
|
|
1585
|
+
case "xargs":
|
|
1586
|
+
return xargsIsDangerous(rest);
|
|
1587
|
+
case "chmod":
|
|
1588
|
+
return chmodIsDangerous(rest);
|
|
1589
|
+
case "systemctl":
|
|
1590
|
+
return rest.some((arg) => ["poweroff", "halt", "reboot"].includes(arg.toLowerCase()));
|
|
1591
|
+
case "init":
|
|
1592
|
+
return rest.some((arg) => arg === "0" || arg === "6");
|
|
1593
|
+
case "sh":
|
|
1594
|
+
case "bash":
|
|
1595
|
+
case "zsh":
|
|
1596
|
+
case "dash":
|
|
1597
|
+
case "ash": {
|
|
1598
|
+
const idx = rest.indexOf("-c");
|
|
1599
|
+
const script = idx >= 0 ? rest[idx + 1] : void 0;
|
|
1600
|
+
return script !== void 0 && commandLineIsDangerous(script, depth + 1);
|
|
1601
|
+
}
|
|
1602
|
+
case "eval":
|
|
1603
|
+
return rest.length > 0 && commandLineIsDangerous(rest.join(" "), depth + 1);
|
|
1604
|
+
case "cmd": {
|
|
1605
|
+
const idx = rest.findIndex((arg) => /^\/c$/i.test(arg));
|
|
1606
|
+
if (idx < 0) return false;
|
|
1607
|
+
const script = rest.slice(idx + 1).join(" ");
|
|
1608
|
+
return script.length > 0 && commandLineIsDangerous(script, depth + 1);
|
|
1609
|
+
}
|
|
1610
|
+
default:
|
|
1611
|
+
return name.startsWith("mkfs") && (name.length === 4 || /[.-]/.test(name.charAt(4)));
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
810
1614
|
function isDangerousCommand(command) {
|
|
811
|
-
|
|
1615
|
+
if (FORK_BOMB.test(command)) return true;
|
|
1616
|
+
return commandLineIsDangerous(command, 0);
|
|
812
1617
|
}
|
|
813
1618
|
function getStringArg(args, key) {
|
|
814
1619
|
if (typeof args === "object" && args !== null) {
|
|
@@ -820,7 +1625,7 @@ function getStringArg(args, key) {
|
|
|
820
1625
|
return void 0;
|
|
821
1626
|
}
|
|
822
1627
|
function normalizeAbsolute(p, cwd) {
|
|
823
|
-
const abs =
|
|
1628
|
+
const abs = path3.isAbsolute(p) ? path3.normalize(p) : path3.resolve(cwd, p);
|
|
824
1629
|
return abs.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
825
1630
|
}
|
|
826
1631
|
function isOutsideCwd(p, cwd) {
|
|
@@ -829,20 +1634,20 @@ function isOutsideCwd(p, cwd) {
|
|
|
829
1634
|
return abs !== base && !abs.startsWith(`${base}/`);
|
|
830
1635
|
}
|
|
831
1636
|
function resolveRealPath(p, cwd) {
|
|
832
|
-
let current =
|
|
1637
|
+
let current = path3.isAbsolute(p) ? path3.normalize(p) : path3.resolve(cwd, p);
|
|
833
1638
|
const missing = [];
|
|
834
1639
|
for (; ; ) {
|
|
835
1640
|
try {
|
|
836
|
-
return
|
|
1641
|
+
return path3.join(fs.realpathSync(current), ...missing.reverse());
|
|
837
1642
|
} catch {
|
|
838
|
-
const parent =
|
|
1643
|
+
const parent = path3.dirname(current);
|
|
839
1644
|
if (parent === current) return null;
|
|
840
|
-
missing.push(
|
|
1645
|
+
missing.push(path3.basename(current));
|
|
841
1646
|
current = parent;
|
|
842
1647
|
}
|
|
843
1648
|
}
|
|
844
1649
|
}
|
|
845
|
-
function
|
|
1650
|
+
function isOutsideResolved(p, cwd) {
|
|
846
1651
|
const resolved = resolveRealPath(p, cwd);
|
|
847
1652
|
if (resolved === null) return isOutsideCwd(p, cwd);
|
|
848
1653
|
let realCwd;
|
|
@@ -867,8 +1672,7 @@ function checkPermission(mode, req, ctx, allowRules = [], denyRules = []) {
|
|
|
867
1672
|
if (isWriteTool && isSensitivePath(filePath)) {
|
|
868
1673
|
return "deny";
|
|
869
1674
|
}
|
|
870
|
-
|
|
871
|
-
if (outside) {
|
|
1675
|
+
if (isOutsideResolved(filePath, ctx.cwd)) {
|
|
872
1676
|
if (mode === "ask" && req.level === "read") {
|
|
873
1677
|
return "ask";
|
|
874
1678
|
}
|
|
@@ -942,17 +1746,40 @@ function scheduleSessionTitle(store, userText, model) {
|
|
|
942
1746
|
// src/snapshot/git-tree.ts
|
|
943
1747
|
import { execFile } from "child_process";
|
|
944
1748
|
import { existsSync } from "fs";
|
|
945
|
-
import { mkdir } from "fs/promises";
|
|
946
|
-
import
|
|
947
|
-
import
|
|
1749
|
+
import { mkdir, readdir, rm, writeFile } from "fs/promises";
|
|
1750
|
+
import os2 from "os";
|
|
1751
|
+
import path4 from "path";
|
|
948
1752
|
var GIT_TIMEOUT_MS2 = 1e4;
|
|
1753
|
+
var GC_TIMEOUT_MS = 6e4;
|
|
949
1754
|
var GIT_MAX_BUFFER2 = 8 * 1024 * 1024;
|
|
950
|
-
|
|
1755
|
+
var GC_INTERVAL = 50;
|
|
1756
|
+
var TRACK_FAILURE_TTL_MS = 5 * 60 * 1e3;
|
|
1757
|
+
var SENSITIVE_EXCLUDE_PATTERNS = [
|
|
1758
|
+
".env",
|
|
1759
|
+
".env.*",
|
|
1760
|
+
"!.env.example",
|
|
1761
|
+
"!.env.sample",
|
|
1762
|
+
"!.env.template",
|
|
1763
|
+
"id_rsa",
|
|
1764
|
+
"id_dsa",
|
|
1765
|
+
"id_ecdsa",
|
|
1766
|
+
"id_ed25519",
|
|
1767
|
+
".npmrc",
|
|
1768
|
+
".netrc",
|
|
1769
|
+
".pgpass",
|
|
1770
|
+
"*.pem",
|
|
1771
|
+
"*.key",
|
|
1772
|
+
"*.p12",
|
|
1773
|
+
"*.pfx",
|
|
1774
|
+
"**/.aws/credentials",
|
|
1775
|
+
"**/.kube/config"
|
|
1776
|
+
];
|
|
1777
|
+
function runGit2(args, timeoutMs = GIT_TIMEOUT_MS2) {
|
|
951
1778
|
return new Promise((resolve) => {
|
|
952
1779
|
execFile(
|
|
953
1780
|
"git",
|
|
954
1781
|
args,
|
|
955
|
-
{ timeout:
|
|
1782
|
+
{ timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER2, encoding: "utf8" },
|
|
956
1783
|
(error, stdout) => {
|
|
957
1784
|
resolve(error ? null : stdout.trim());
|
|
958
1785
|
}
|
|
@@ -960,6 +1787,9 @@ function runGit2(args) {
|
|
|
960
1787
|
});
|
|
961
1788
|
}
|
|
962
1789
|
var gitAvailable = null;
|
|
1790
|
+
var trackFailureUntil = /* @__PURE__ */ new Map();
|
|
1791
|
+
var trackCounts = /* @__PURE__ */ new Map();
|
|
1792
|
+
var scrubbedRepos = /* @__PURE__ */ new Set();
|
|
963
1793
|
async function isGitAvailable() {
|
|
964
1794
|
if (gitAvailable === null) {
|
|
965
1795
|
gitAvailable = await runGit2(["--version"]) !== null;
|
|
@@ -967,41 +1797,119 @@ async function isGitAvailable() {
|
|
|
967
1797
|
return gitAvailable;
|
|
968
1798
|
}
|
|
969
1799
|
function treeRepoDir(cwd) {
|
|
970
|
-
const key = Buffer.from(
|
|
971
|
-
return
|
|
1800
|
+
const key = Buffer.from(path4.resolve(cwd), "utf8").toString("base64url");
|
|
1801
|
+
return path4.join(gitTreesDir(), key);
|
|
1802
|
+
}
|
|
1803
|
+
async function removeTreeRepo(cwd) {
|
|
1804
|
+
const dir = treeRepoDir(cwd);
|
|
1805
|
+
trackFailureUntil.delete(path4.resolve(cwd));
|
|
1806
|
+
trackCounts.delete(dir);
|
|
1807
|
+
scrubbedRepos.delete(dir);
|
|
1808
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
async function pruneTreeReposExcept(remainingCwds) {
|
|
1812
|
+
let entries;
|
|
1813
|
+
try {
|
|
1814
|
+
entries = await readdir(gitTreesDir());
|
|
1815
|
+
} catch {
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1819
|
+
for (const cwd of remainingCwds) keep.add(path4.basename(treeRepoDir(cwd)));
|
|
1820
|
+
await Promise.all(
|
|
1821
|
+
entries.filter((entry) => !keep.has(entry)).map(
|
|
1822
|
+
(entry) => rm(path4.join(gitTreesDir(), entry), { recursive: true, force: true }).catch(() => {
|
|
1823
|
+
})
|
|
1824
|
+
)
|
|
1825
|
+
);
|
|
972
1826
|
}
|
|
973
1827
|
function isGuardedDir(cwd) {
|
|
974
|
-
const resolved =
|
|
975
|
-
const home =
|
|
1828
|
+
const resolved = path4.resolve(cwd);
|
|
1829
|
+
const home = path4.resolve(os2.homedir());
|
|
976
1830
|
const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
977
|
-
return same(resolved, home) || same(resolved,
|
|
1831
|
+
return same(resolved, home) || same(resolved, path4.parse(resolved).root);
|
|
978
1832
|
}
|
|
979
1833
|
async function ensureTreeRepo(cwd) {
|
|
980
1834
|
const dir = treeRepoDir(cwd);
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1835
|
+
try {
|
|
1836
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
1837
|
+
if (!existsSync(path4.join(dir, "HEAD"))) {
|
|
1838
|
+
const out = await runGit2(["init", "--bare", dir]);
|
|
1839
|
+
if (out === null) return null;
|
|
1840
|
+
}
|
|
1841
|
+
await mkdir(path4.join(dir, "info"), { recursive: true });
|
|
1842
|
+
await writeFile(
|
|
1843
|
+
path4.join(dir, "info", "exclude"),
|
|
1844
|
+
`${SENSITIVE_EXCLUDE_PATTERNS.join("\n")}
|
|
1845
|
+
`,
|
|
1846
|
+
"utf8"
|
|
1847
|
+
);
|
|
1848
|
+
} catch {
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
return dir;
|
|
985
1852
|
}
|
|
986
1853
|
function repoArgs(dir, cwd) {
|
|
987
1854
|
return [`--git-dir=${dir}`, `--work-tree=${cwd}`];
|
|
988
1855
|
}
|
|
1856
|
+
async function untrackExcluded(dir, cwd) {
|
|
1857
|
+
const base = repoArgs(dir, cwd);
|
|
1858
|
+
const listed = await runGit2([...base, "ls-files", "-z", "-c", "-i", "--exclude-standard"]);
|
|
1859
|
+
if (listed === null) return false;
|
|
1860
|
+
const files = listed.split("\0").filter(Boolean);
|
|
1861
|
+
if (files.length === 0) return true;
|
|
1862
|
+
const removed = await runGit2([
|
|
1863
|
+
...base,
|
|
1864
|
+
"rm",
|
|
1865
|
+
"--cached",
|
|
1866
|
+
"--ignore-unmatch",
|
|
1867
|
+
"-q",
|
|
1868
|
+
"--",
|
|
1869
|
+
...files.map((file) => `:(literal)${file}`)
|
|
1870
|
+
]);
|
|
1871
|
+
return removed !== null;
|
|
1872
|
+
}
|
|
989
1873
|
async function trackTree(cwd) {
|
|
990
1874
|
if (isGuardedDir(cwd)) return null;
|
|
1875
|
+
const key = path4.resolve(cwd);
|
|
1876
|
+
const failedUntil = trackFailureUntil.get(key);
|
|
1877
|
+
if (failedUntil !== void 0 && Date.now() < failedUntil) return null;
|
|
1878
|
+
const tree = await trackTreeUncached(cwd);
|
|
1879
|
+
if (tree === null) {
|
|
1880
|
+
trackFailureUntil.set(key, Date.now() + TRACK_FAILURE_TTL_MS);
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
trackFailureUntil.delete(key);
|
|
1884
|
+
return tree;
|
|
1885
|
+
}
|
|
1886
|
+
async function trackTreeUncached(cwd) {
|
|
991
1887
|
if (!await isGitAvailable()) return null;
|
|
992
1888
|
const dir = await ensureTreeRepo(cwd);
|
|
993
1889
|
if (!dir) return null;
|
|
994
1890
|
const base = repoArgs(dir, cwd);
|
|
1891
|
+
if (!scrubbedRepos.has(dir)) {
|
|
1892
|
+
if (!await untrackExcluded(dir, cwd)) return null;
|
|
1893
|
+
scrubbedRepos.add(dir);
|
|
1894
|
+
}
|
|
995
1895
|
if (await runGit2([...base, "add", "-A"]) === null) return null;
|
|
996
|
-
|
|
1896
|
+
const tree = await runGit2([...base, "write-tree"]);
|
|
1897
|
+
if (tree === null) return null;
|
|
1898
|
+
const count = (trackCounts.get(dir) ?? 0) + 1;
|
|
1899
|
+
trackCounts.set(dir, count);
|
|
1900
|
+
if (count % GC_INTERVAL === 0) {
|
|
1901
|
+
await runGit2([`--git-dir=${dir}`, "gc", "--auto"], GC_TIMEOUT_MS);
|
|
1902
|
+
}
|
|
1903
|
+
return tree;
|
|
997
1904
|
}
|
|
998
1905
|
async function restoreTree(cwd, hash) {
|
|
999
1906
|
if (isGuardedDir(cwd)) return false;
|
|
1000
1907
|
if (!await isGitAvailable()) return false;
|
|
1001
|
-
const dir =
|
|
1002
|
-
if (!
|
|
1908
|
+
const dir = await ensureTreeRepo(cwd);
|
|
1909
|
+
if (!dir) return false;
|
|
1003
1910
|
const base = repoArgs(dir, cwd);
|
|
1004
1911
|
if (await runGit2([...base, "read-tree", hash]) === null) return false;
|
|
1912
|
+
if (!await untrackExcluded(dir, cwd)) return false;
|
|
1005
1913
|
if (await runGit2([...base, "checkout-index", "-f", "-a"]) === null) return false;
|
|
1006
1914
|
if (await runGit2([...base, "clean", "-fd"]) === null) return false;
|
|
1007
1915
|
return true;
|
|
@@ -1009,21 +1917,23 @@ async function restoreTree(cwd, hash) {
|
|
|
1009
1917
|
async function diffTreeNames(cwd, hash) {
|
|
1010
1918
|
if (!await isGitAvailable()) return null;
|
|
1011
1919
|
const dir = treeRepoDir(cwd);
|
|
1012
|
-
if (!existsSync(
|
|
1920
|
+
if (!existsSync(path4.join(dir, "HEAD"))) return null;
|
|
1013
1921
|
const base = repoArgs(dir, cwd);
|
|
1014
1922
|
const changed = await runGit2([...base, "diff", "--name-only", hash]);
|
|
1015
1923
|
const untracked = await runGit2([...base, "ls-files", "--others", "--exclude-standard"]);
|
|
1016
1924
|
if (changed === null || untracked === null) return null;
|
|
1017
1925
|
const names = /* @__PURE__ */ new Set();
|
|
1018
1926
|
for (const line of [...changed.split("\n"), ...untracked.split("\n")]) {
|
|
1019
|
-
if (line !== "") names.add(line);
|
|
1927
|
+
if (line !== "" && !isSensitivePath(line)) names.add(line);
|
|
1020
1928
|
}
|
|
1021
1929
|
return [...names];
|
|
1022
1930
|
}
|
|
1023
1931
|
|
|
1024
1932
|
// src/tools/fs/snapshots.ts
|
|
1025
|
-
import {
|
|
1026
|
-
import
|
|
1933
|
+
import { randomUUID } from "crypto";
|
|
1934
|
+
import { mkdir as mkdir2, readFile, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
1935
|
+
import os3 from "os";
|
|
1936
|
+
import path5 from "path";
|
|
1027
1937
|
var MAX_SNAPSHOTS = 50;
|
|
1028
1938
|
var MAX_SNAPSHOT_CONTENT_BYTES = 5 * 1024 * 1024;
|
|
1029
1939
|
var stack = [];
|
|
@@ -1031,6 +1941,30 @@ var activeTurn = 0;
|
|
|
1031
1941
|
var activeTurnMessageIndex = -1;
|
|
1032
1942
|
var checkpointSeq = 0;
|
|
1033
1943
|
var hooks = null;
|
|
1944
|
+
var spillDir = path5.join(
|
|
1945
|
+
os3.tmpdir(),
|
|
1946
|
+
`star-cli-snapshots-${process.pid}-${randomUUID().slice(0, 8)}`
|
|
1947
|
+
);
|
|
1948
|
+
function isSpillPath(p) {
|
|
1949
|
+
return p.startsWith(spillDir + path5.sep);
|
|
1950
|
+
}
|
|
1951
|
+
async function spillContent(snapshot) {
|
|
1952
|
+
if (snapshot.content === null || snapshot.contentFile) return;
|
|
1953
|
+
try {
|
|
1954
|
+
const file = path5.join(spillDir, `${snapshot.id}.snap`);
|
|
1955
|
+
await mkdir2(spillDir, { recursive: true, mode: 448 });
|
|
1956
|
+
await writeFile2(file, snapshot.content, { encoding: "utf8", mode: 384 });
|
|
1957
|
+
snapshot.contentFile = file;
|
|
1958
|
+
snapshot.content = null;
|
|
1959
|
+
} catch {
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
async function discardSpill(snapshot) {
|
|
1963
|
+
if (snapshot.contentFile && isSpillPath(snapshot.contentFile)) {
|
|
1964
|
+
await rm2(snapshot.contentFile, { force: true }).catch(() => {
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1034
1968
|
function setSnapshotHooks(next) {
|
|
1035
1969
|
hooks = next;
|
|
1036
1970
|
}
|
|
@@ -1089,8 +2023,10 @@ async function pushSnapshot(snapshot) {
|
|
|
1089
2023
|
console.error("[star] failed to persist snapshot checkpoint:", err);
|
|
1090
2024
|
}
|
|
1091
2025
|
}
|
|
2026
|
+
await spillContent(snapshot);
|
|
1092
2027
|
if (stack.length > MAX_SNAPSHOTS) {
|
|
1093
2028
|
const evicted = stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
2029
|
+
for (const old of evicted) await discardSpill(old);
|
|
1094
2030
|
await removeFromStore(evicted.map((s) => s.id));
|
|
1095
2031
|
}
|
|
1096
2032
|
}
|
|
@@ -1101,9 +2037,11 @@ function listSnapshots() {
|
|
|
1101
2037
|
return [...stack];
|
|
1102
2038
|
}
|
|
1103
2039
|
function clearSnapshots() {
|
|
2040
|
+
for (const snapshot of stack) void discardSpill(snapshot);
|
|
1104
2041
|
stack.length = 0;
|
|
1105
2042
|
}
|
|
1106
2043
|
function hydrateSnapshots(snapshots) {
|
|
2044
|
+
for (const snapshot of stack) void discardSpill(snapshot);
|
|
1107
2045
|
stack.length = 0;
|
|
1108
2046
|
stack.push(...snapshots.slice(-MAX_SNAPSHOTS));
|
|
1109
2047
|
for (const snapshot of stack) {
|
|
@@ -1128,11 +2066,11 @@ async function revert(snapshot) {
|
|
|
1128
2066
|
if (content === null) {
|
|
1129
2067
|
return `Skipped ${snapshot.path}: the snapshot content is no longer available.`;
|
|
1130
2068
|
}
|
|
1131
|
-
await mkdir2(
|
|
1132
|
-
await
|
|
2069
|
+
await mkdir2(path5.dirname(snapshot.path), { recursive: true });
|
|
2070
|
+
await writeFile2(snapshot.path, content, "utf8");
|
|
1133
2071
|
return `Restored ${snapshot.path} to its state before ${snapshot.toolName}.`;
|
|
1134
2072
|
}
|
|
1135
|
-
await
|
|
2073
|
+
await rm2(snapshot.path, { force: true });
|
|
1136
2074
|
return `Deleted ${snapshot.path} (it did not exist before ${snapshot.toolName}).`;
|
|
1137
2075
|
}
|
|
1138
2076
|
function listTurnSnapshots(turn, includeSubagent = false) {
|
|
@@ -1159,6 +2097,7 @@ async function undoTurnSnapshots(turn, includeSubagent = false) {
|
|
|
1159
2097
|
const idx = stack.indexOf(snapshot);
|
|
1160
2098
|
if (idx >= 0) stack.splice(idx, 1);
|
|
1161
2099
|
revertedIds.push(snapshot.id);
|
|
2100
|
+
await discardSpill(snapshot);
|
|
1162
2101
|
} catch (err) {
|
|
1163
2102
|
messages.push(
|
|
1164
2103
|
`Failed to revert ${snapshot.path}: ${err.message} (checkpoint kept)`
|
|
@@ -1170,13 +2109,16 @@ async function undoTurnSnapshots(turn, includeSubagent = false) {
|
|
|
1170
2109
|
}
|
|
1171
2110
|
async function dropTurnSnapshots(turn, includeSubagent = false) {
|
|
1172
2111
|
const dropped = [];
|
|
2112
|
+
const droppedSnapshots = [];
|
|
1173
2113
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
1174
2114
|
const snapshot = stack[i];
|
|
1175
2115
|
if (snapshot && snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
1176
2116
|
dropped.push(snapshot.id);
|
|
2117
|
+
droppedSnapshots.push(snapshot);
|
|
1177
2118
|
stack.splice(i, 1);
|
|
1178
2119
|
}
|
|
1179
2120
|
}
|
|
2121
|
+
for (const snapshot of droppedSnapshots) await discardSpill(snapshot);
|
|
1180
2122
|
await removeFromStore(dropped);
|
|
1181
2123
|
return dropped.length;
|
|
1182
2124
|
}
|
|
@@ -1198,6 +2140,7 @@ async function rewindToSnapshot(id, includeSubagent = false) {
|
|
|
1198
2140
|
const idx = stack.indexOf(snapshot);
|
|
1199
2141
|
if (idx >= 0) stack.splice(idx, 1);
|
|
1200
2142
|
revertedIds.push(snapshot.id);
|
|
2143
|
+
await discardSpill(snapshot);
|
|
1201
2144
|
if (snapshot.messageIndex >= 0) {
|
|
1202
2145
|
messageIndex = messageIndex === null ? snapshot.messageIndex : Math.min(messageIndex, snapshot.messageIndex);
|
|
1203
2146
|
}
|
|
@@ -1214,7 +2157,7 @@ async function rewindToSnapshot(id, includeSubagent = false) {
|
|
|
1214
2157
|
// src/tools/bash.ts
|
|
1215
2158
|
import { spawn as spawn3 } from "child_process";
|
|
1216
2159
|
import { existsSync as existsSync2 } from "fs";
|
|
1217
|
-
import
|
|
2160
|
+
import path6 from "path";
|
|
1218
2161
|
import { z as z2 } from "zod";
|
|
1219
2162
|
|
|
1220
2163
|
// src/tasks/manager.ts
|
|
@@ -1233,6 +2176,9 @@ var TaskManager = class extends EventEmitter {
|
|
|
1233
2176
|
const child = spawn2(spec.shell, spec.wrap(opts.command), {
|
|
1234
2177
|
cwd: opts.cwd,
|
|
1235
2178
|
windowsHide: true,
|
|
2179
|
+
// No stdin: background commands that read it would hang until the
|
|
2180
|
+
// (much longer) task timeout; give them EOF immediately.
|
|
2181
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1236
2182
|
// POSIX: the child leads its own process group so killTree can SIGKILL
|
|
1237
2183
|
// the whole tree with a negative pid (Windows uses taskkill /t).
|
|
1238
2184
|
detached: process.platform !== "win32"
|
|
@@ -1348,11 +2294,11 @@ var HALF_OUTPUT = MAX_OUTPUT / 2;
|
|
|
1348
2294
|
var DEFAULT_TIMEOUT = 120;
|
|
1349
2295
|
var MAX_TIMEOUT = 600;
|
|
1350
2296
|
function findOnPath(exe, exclude) {
|
|
1351
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
2297
|
+
for (const dir of (process.env.PATH ?? "").split(path6.delimiter)) {
|
|
1352
2298
|
if (!dir || exclude?.(dir)) {
|
|
1353
2299
|
continue;
|
|
1354
2300
|
}
|
|
1355
|
-
const full =
|
|
2301
|
+
const full = path6.join(dir, exe);
|
|
1356
2302
|
if (existsSync2(full)) {
|
|
1357
2303
|
return full;
|
|
1358
2304
|
}
|
|
@@ -1370,16 +2316,22 @@ function resolveShellUncached() {
|
|
|
1370
2316
|
}
|
|
1371
2317
|
const gitExe = findOnPath("git.exe");
|
|
1372
2318
|
if (gitExe) {
|
|
1373
|
-
const sibling =
|
|
2319
|
+
const sibling = path6.join(path6.dirname(path6.dirname(gitExe)), "bin", "bash.exe");
|
|
1374
2320
|
if (existsSync2(sibling)) {
|
|
1375
2321
|
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
1376
2322
|
}
|
|
1377
2323
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
2324
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2325
|
+
for (const root of [
|
|
2326
|
+
process.env.ProgramFiles,
|
|
2327
|
+
process.env["ProgramFiles(x86)"],
|
|
2328
|
+
process.env.ProgramW6432
|
|
1382
2329
|
]) {
|
|
2330
|
+
if (!root) continue;
|
|
2331
|
+
const candidate = path6.join(root, "Git", "bin", "bash.exe");
|
|
2332
|
+
const key = candidate.toLowerCase();
|
|
2333
|
+
if (seen.has(key)) continue;
|
|
2334
|
+
seen.add(key);
|
|
1383
2335
|
if (existsSync2(candidate)) {
|
|
1384
2336
|
return { shell: candidate, wrap: (c) => ["-c", c], label: "bash" };
|
|
1385
2337
|
}
|
|
@@ -1481,7 +2433,10 @@ description: ${args.description ?? "(none)"}`
|
|
|
1481
2433
|
const child = spawn3(spec.shell, spec.wrap(args.command), {
|
|
1482
2434
|
cwd: ctx.cwd,
|
|
1483
2435
|
windowsHide: true,
|
|
1484
|
-
detached: process.platform !== "win32"
|
|
2436
|
+
detached: process.platform !== "win32",
|
|
2437
|
+
// No stdin: a command that reads it (cat, read, input()) must get
|
|
2438
|
+
// EOF immediately instead of hanging until the timeout.
|
|
2439
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1485
2440
|
});
|
|
1486
2441
|
const output = new BoundedOutput();
|
|
1487
2442
|
let settled = false;
|
|
@@ -1530,9 +2485,10 @@ Command timed out after ${timeoutSeconds}s`,
|
|
|
1530
2485
|
};
|
|
1531
2486
|
|
|
1532
2487
|
// src/tools/fs/edit.ts
|
|
1533
|
-
import { readFile as readFile2, stat as stat2, writeFile as
|
|
1534
|
-
import
|
|
2488
|
+
import { readFile as readFile2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
2489
|
+
import path7 from "path";
|
|
1535
2490
|
import { z as z3 } from "zod";
|
|
2491
|
+
var BINARY_PROBE_BYTES = 8192;
|
|
1536
2492
|
var schema2 = z3.object({
|
|
1537
2493
|
path: z3.string().describe("File path, absolute or relative to the working directory"),
|
|
1538
2494
|
old_string: z3.string().min(1).describe("Exact text to replace"),
|
|
@@ -1541,16 +2497,27 @@ var schema2 = z3.object({
|
|
|
1541
2497
|
});
|
|
1542
2498
|
var editFileTool = {
|
|
1543
2499
|
name: "edit_file",
|
|
1544
|
-
description: "Replace an exact string in a file. Fails if old_string is not found or matches multiple locations unless replace_all is set.",
|
|
2500
|
+
description: "Replace an exact string in a file. Fails if old_string is not found or matches multiple locations unless replace_all is set. Refuses binary files and files over 5MB.",
|
|
1545
2501
|
permission: "write",
|
|
1546
2502
|
parameters: schema2,
|
|
1547
2503
|
async execute(args, ctx) {
|
|
1548
|
-
const filePath =
|
|
2504
|
+
const filePath = path7.resolve(ctx.cwd, args.path);
|
|
1549
2505
|
let content;
|
|
1550
2506
|
let readMtimeMs;
|
|
1551
2507
|
try {
|
|
1552
|
-
|
|
1553
|
-
|
|
2508
|
+
const st = await stat2(filePath);
|
|
2509
|
+
if (st.size > MAX_SNAPSHOT_CONTENT_BYTES) {
|
|
2510
|
+
return {
|
|
2511
|
+
content: `Refused to edit ${args.path}: file is ${st.size} bytes, over the 5MB limit (edits past it could not be restored by /undo)`,
|
|
2512
|
+
isError: true
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
readMtimeMs = st.mtimeMs;
|
|
2516
|
+
const buf = await readFile2(filePath);
|
|
2517
|
+
if (buf.subarray(0, BINARY_PROBE_BYTES).includes(0)) {
|
|
2518
|
+
return { content: `Refused to edit binary file: ${args.path}`, isError: true };
|
|
2519
|
+
}
|
|
2520
|
+
content = buf.toString("utf8");
|
|
1554
2521
|
} catch (err) {
|
|
1555
2522
|
return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
|
|
1556
2523
|
}
|
|
@@ -1578,7 +2545,7 @@ var editFileTool = {
|
|
|
1578
2545
|
isError: true
|
|
1579
2546
|
};
|
|
1580
2547
|
}
|
|
1581
|
-
await
|
|
2548
|
+
await writeFile3(filePath, updated, "utf8");
|
|
1582
2549
|
} catch (err) {
|
|
1583
2550
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1584
2551
|
}
|
|
@@ -1600,23 +2567,23 @@ var editFileTool = {
|
|
|
1600
2567
|
};
|
|
1601
2568
|
|
|
1602
2569
|
// src/tools/fs/glob.ts
|
|
1603
|
-
import
|
|
2570
|
+
import path9 from "path";
|
|
1604
2571
|
import { z as z4 } from "zod";
|
|
1605
2572
|
|
|
1606
2573
|
// src/tools/fs/util.ts
|
|
1607
2574
|
import { readFileSync, statSync } from "fs";
|
|
1608
|
-
import { readdir, stat as stat3 } from "fs/promises";
|
|
1609
|
-
import
|
|
2575
|
+
import { readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
2576
|
+
import path8 from "path";
|
|
1610
2577
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1611
2578
|
async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
1612
2579
|
const out = [];
|
|
1613
2580
|
async function walk(dir, relBase) {
|
|
1614
|
-
const entries = await
|
|
2581
|
+
const entries = await readdir2(dir, { withFileTypes: true }).catch(() => null);
|
|
1615
2582
|
if (!entries) {
|
|
1616
2583
|
return;
|
|
1617
2584
|
}
|
|
1618
2585
|
for (const entry of entries) {
|
|
1619
|
-
const abs =
|
|
2586
|
+
const abs = path8.join(dir, entry.name);
|
|
1620
2587
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1621
2588
|
if (entry.isDirectory()) {
|
|
1622
2589
|
if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
|
|
@@ -1775,7 +2742,7 @@ function parseIgnorePatterns(body) {
|
|
|
1775
2742
|
return out;
|
|
1776
2743
|
}
|
|
1777
2744
|
function loadIgnorePatterns(dir) {
|
|
1778
|
-
const file =
|
|
2745
|
+
const file = path8.join(dir, IGNORE_FILE);
|
|
1779
2746
|
let mtimeMs;
|
|
1780
2747
|
try {
|
|
1781
2748
|
mtimeMs = statSync(file).mtimeMs;
|
|
@@ -1800,9 +2767,9 @@ function createIgnorePredicate(cwd) {
|
|
|
1800
2767
|
if (patterns.length === 0) {
|
|
1801
2768
|
return void 0;
|
|
1802
2769
|
}
|
|
1803
|
-
const root =
|
|
2770
|
+
const root = path8.resolve(cwd);
|
|
1804
2771
|
return (absPath, isDir) => {
|
|
1805
|
-
const rel =
|
|
2772
|
+
const rel = path8.relative(root, absPath).split(path8.sep).join("/");
|
|
1806
2773
|
if (!rel || rel.startsWith("..")) {
|
|
1807
2774
|
return false;
|
|
1808
2775
|
}
|
|
@@ -1838,15 +2805,24 @@ var schema3 = z4.object({
|
|
|
1838
2805
|
),
|
|
1839
2806
|
path: z4.string().optional().describe("Directory to search, defaults to the working directory")
|
|
1840
2807
|
});
|
|
2808
|
+
function hasParentSegment(pattern) {
|
|
2809
|
+
return pattern.replace(/\\/g, "/").split("/").some((segment) => segment === "..");
|
|
2810
|
+
}
|
|
1841
2811
|
var globTool = {
|
|
1842
2812
|
name: "glob",
|
|
1843
2813
|
description: "Find files by glob pattern. Skips node_modules, .git and dist. Returns paths sorted by modification time, newest first, up to 100 results.",
|
|
1844
2814
|
permission: "read",
|
|
1845
2815
|
parameters: schema3,
|
|
1846
2816
|
async execute(args, ctx) {
|
|
1847
|
-
|
|
2817
|
+
if (hasParentSegment(args.pattern)) {
|
|
2818
|
+
return {
|
|
2819
|
+
content: `Refused glob pattern with '..' segment: ${args.pattern}`,
|
|
2820
|
+
isError: true
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
const root = path9.resolve(ctx.cwd, args.path ?? ".");
|
|
1848
2824
|
const prefixSegments = literalDirPrefix(args.pattern);
|
|
1849
|
-
const walkRoot = prefixSegments.length > 0 ?
|
|
2825
|
+
const walkRoot = prefixSegments.length > 0 ? path9.join(root, ...prefixSegments) : root;
|
|
1850
2826
|
const prefix = prefixSegments.length > 0 ? `${prefixSegments.join("/")}/` : "";
|
|
1851
2827
|
const files = await walkFiles(walkRoot, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
|
|
1852
2828
|
const matched = files.filter((f) => matchesGlob(args.pattern, `${prefix}${f.rel}`)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
|
|
@@ -1858,8 +2834,8 @@ var globTool = {
|
|
|
1858
2834
|
};
|
|
1859
2835
|
|
|
1860
2836
|
// src/tools/fs/grep.ts
|
|
1861
|
-
import { readFile as readFile3, stat as stat4 } from "fs/promises";
|
|
1862
|
-
import
|
|
2837
|
+
import { readFile as readFile3, realpath, stat as stat4 } from "fs/promises";
|
|
2838
|
+
import path10 from "path";
|
|
1863
2839
|
import { z as z5 } from "zod";
|
|
1864
2840
|
var MAX_MATCHES = 250;
|
|
1865
2841
|
var MAX_LINE_CHARS = 500;
|
|
@@ -1892,18 +2868,19 @@ var grepTool = {
|
|
|
1892
2868
|
} catch (err) {
|
|
1893
2869
|
return { content: `Invalid regular expression: ${err.message}`, isError: true };
|
|
1894
2870
|
}
|
|
1895
|
-
const root =
|
|
2871
|
+
const root = path10.resolve(ctx.cwd, args.path ?? ".");
|
|
1896
2872
|
let files;
|
|
1897
2873
|
try {
|
|
1898
2874
|
const st = await stat4(root);
|
|
1899
2875
|
if (st.isFile()) {
|
|
1900
|
-
|
|
2876
|
+
const resolved = await realpath(root).catch(() => null);
|
|
2877
|
+
if (isSensitivePath(root) || resolved !== null && isSensitivePath(resolved)) {
|
|
1901
2878
|
return {
|
|
1902
2879
|
content: `Refused to grep sensitive file: ${args.path ?? "."}`,
|
|
1903
2880
|
isError: true
|
|
1904
2881
|
};
|
|
1905
2882
|
}
|
|
1906
|
-
files = [{ abs: root, rel:
|
|
2883
|
+
files = [{ abs: root, rel: path10.basename(root), mtimeMs: st.mtimeMs }];
|
|
1907
2884
|
} else {
|
|
1908
2885
|
const walked = await walkFiles(root, void 0, {
|
|
1909
2886
|
ignore: createIgnorePredicate(ctx.cwd),
|
|
@@ -1969,13 +2946,13 @@ var grepTool = {
|
|
|
1969
2946
|
};
|
|
1970
2947
|
|
|
1971
2948
|
// src/tools/fs/read.ts
|
|
1972
|
-
import { open, stat as stat5 } from "fs/promises";
|
|
1973
|
-
import
|
|
2949
|
+
import { open, realpath as realpath2, stat as stat5 } from "fs/promises";
|
|
2950
|
+
import path11 from "path";
|
|
1974
2951
|
import { z as z6 } from "zod";
|
|
1975
2952
|
var DEFAULT_LIMIT = 2e3;
|
|
1976
2953
|
var MAX_CHARS = 100 * 1024;
|
|
1977
2954
|
var MAX_READ_BYTES = 1024 * 1024;
|
|
1978
|
-
var
|
|
2955
|
+
var BINARY_PROBE_BYTES2 = 8192;
|
|
1979
2956
|
function decodeUtf16be(buf) {
|
|
1980
2957
|
const len = buf.length - buf.length % 2;
|
|
1981
2958
|
const swapped = Buffer.allocUnsafe(len);
|
|
@@ -1992,7 +2969,7 @@ function decodeTextBuffer(buf) {
|
|
|
1992
2969
|
if (buf.length >= 2 && buf[0] === 254 && buf[1] === 255) {
|
|
1993
2970
|
return { text: decodeUtf16be(buf.subarray(2)) };
|
|
1994
2971
|
}
|
|
1995
|
-
if (buf.subarray(0,
|
|
2972
|
+
if (buf.subarray(0, BINARY_PROBE_BYTES2).includes(0)) {
|
|
1996
2973
|
return { binary: true };
|
|
1997
2974
|
}
|
|
1998
2975
|
return { text: buf.toString("utf8") };
|
|
@@ -2008,8 +2985,9 @@ var readFileTool = {
|
|
|
2008
2985
|
permission: "read",
|
|
2009
2986
|
parameters: schema5,
|
|
2010
2987
|
async execute(args, ctx) {
|
|
2011
|
-
const filePath =
|
|
2012
|
-
|
|
2988
|
+
const filePath = path11.resolve(ctx.cwd, args.path);
|
|
2989
|
+
const resolvedPath = await realpath2(filePath).catch(() => null);
|
|
2990
|
+
if (isSensitivePath(filePath) || resolvedPath !== null && isSensitivePath(resolvedPath)) {
|
|
2013
2991
|
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
2014
2992
|
}
|
|
2015
2993
|
const st = await stat5(filePath).catch(() => null);
|
|
@@ -2070,25 +3048,224 @@ var readFileTool = {
|
|
|
2070
3048
|
}
|
|
2071
3049
|
};
|
|
2072
3050
|
|
|
2073
|
-
// src/tools/fs/
|
|
2074
|
-
import {
|
|
2075
|
-
import
|
|
3051
|
+
// src/tools/fs/read-image.ts
|
|
3052
|
+
import { readFile as readFile5, realpath as realpath3, stat as stat6 } from "fs/promises";
|
|
3053
|
+
import path13 from "path";
|
|
2076
3054
|
import { z as z7 } from "zod";
|
|
3055
|
+
|
|
3056
|
+
// src/core/image-resize.ts
|
|
3057
|
+
import { execFile as execFile2 } from "child_process";
|
|
3058
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3059
|
+
import { readFile as readFile4, unlink, writeFile as writeFile4 } from "fs/promises";
|
|
3060
|
+
import os4 from "os";
|
|
3061
|
+
import path12 from "path";
|
|
3062
|
+
import { promisify } from "util";
|
|
3063
|
+
var RESIZE_TIMEOUT_MS = 1e4;
|
|
3064
|
+
var execFileAsync = promisify(execFile2);
|
|
3065
|
+
function windowsResizeScript(inputFile, outputFile, maxDimension) {
|
|
3066
|
+
const input = inputFile.replace(/'/g, "''");
|
|
3067
|
+
const output = outputFile.replace(/'/g, "''");
|
|
3068
|
+
return [
|
|
3069
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
3070
|
+
`$img = [System.Drawing.Image]::FromFile('${input}')`,
|
|
3071
|
+
`$scale = [Math]::Min(1.0, ${maxDimension} / [Math]::Max($img.Width, $img.Height))`,
|
|
3072
|
+
"$w = [int][Math]::Max(1, [Math]::Round($img.Width * $scale))",
|
|
3073
|
+
"$h = [int][Math]::Max(1, [Math]::Round($img.Height * $scale))",
|
|
3074
|
+
"$bmp = New-Object System.Drawing.Bitmap($w, $h)",
|
|
3075
|
+
"$g = [System.Drawing.Graphics]::FromImage($bmp)",
|
|
3076
|
+
"$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic",
|
|
3077
|
+
"$g.DrawImage($img, 0, 0, $w, $h)",
|
|
3078
|
+
`$bmp.Save('${output}', [System.Drawing.Imaging.ImageFormat]::Png)`,
|
|
3079
|
+
"$g.Dispose(); $bmp.Dispose(); $img.Dispose()"
|
|
3080
|
+
].join("; ");
|
|
3081
|
+
}
|
|
3082
|
+
function resizeImagePlan(platform, inputFile, outputFile, maxDimension, tools = {}) {
|
|
3083
|
+
if (platform === "win32") {
|
|
3084
|
+
return {
|
|
3085
|
+
command: "powershell.exe",
|
|
3086
|
+
args: [
|
|
3087
|
+
"-NoProfile",
|
|
3088
|
+
"-NonInteractive",
|
|
3089
|
+
"-Command",
|
|
3090
|
+
windowsResizeScript(inputFile, outputFile, maxDimension)
|
|
3091
|
+
]
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
if (platform === "darwin") {
|
|
3095
|
+
return {
|
|
3096
|
+
command: "sips",
|
|
3097
|
+
args: ["-Z", String(maxDimension), inputFile, "--out", outputFile]
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
if (platform === "linux") {
|
|
3101
|
+
if (tools.hasConvert) {
|
|
3102
|
+
return {
|
|
3103
|
+
command: "convert",
|
|
3104
|
+
// -auto-orient keeps EXIF-rotated photos upright after the resize
|
|
3105
|
+
// strips their orientation metadata.
|
|
3106
|
+
args: [
|
|
3107
|
+
inputFile,
|
|
3108
|
+
"-auto-orient",
|
|
3109
|
+
"-resize",
|
|
3110
|
+
`${maxDimension}x${maxDimension}>`,
|
|
3111
|
+
outputFile
|
|
3112
|
+
]
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
if (tools.hasFfmpeg) {
|
|
3116
|
+
return {
|
|
3117
|
+
command: "ffmpeg",
|
|
3118
|
+
args: [
|
|
3119
|
+
"-y",
|
|
3120
|
+
"-i",
|
|
3121
|
+
inputFile,
|
|
3122
|
+
"-vf",
|
|
3123
|
+
`scale='if(gt(iw,ih),min(iw,${maxDimension}),-1)':'if(gt(iw,ih),-1,min(ih,${maxDimension}))'`,
|
|
3124
|
+
outputFile
|
|
3125
|
+
]
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
return null;
|
|
3129
|
+
}
|
|
3130
|
+
return null;
|
|
3131
|
+
}
|
|
3132
|
+
async function commandExists(command) {
|
|
3133
|
+
const checker = process.platform === "win32" ? "where" : "which";
|
|
3134
|
+
try {
|
|
3135
|
+
await execFileAsync(checker, [command], { timeout: RESIZE_TIMEOUT_MS });
|
|
3136
|
+
return true;
|
|
3137
|
+
} catch {
|
|
3138
|
+
return false;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
async function downsampleImageIfNeeded(image) {
|
|
3142
|
+
const buf = Buffer.from(image.data, "base64");
|
|
3143
|
+
const dims = probeImageDimensions(buf);
|
|
3144
|
+
if (!dims || Math.max(dims.width, dims.height) <= MAX_IMAGE_DIMENSION) {
|
|
3145
|
+
return { image, oversized: false, resized: false };
|
|
3146
|
+
}
|
|
3147
|
+
const inputFile = path12.join(os4.tmpdir(), `star-image-${randomUUID2()}.src`);
|
|
3148
|
+
const outputFile = path12.join(os4.tmpdir(), `star-image-${randomUUID2()}.png`);
|
|
3149
|
+
try {
|
|
3150
|
+
const [hasConvert, hasFfmpeg] = process.platform === "linux" ? await Promise.all([commandExists("convert"), commandExists("ffmpeg")]) : [false, false];
|
|
3151
|
+
const plan = resizeImagePlan(process.platform, inputFile, outputFile, MAX_IMAGE_DIMENSION, {
|
|
3152
|
+
hasConvert,
|
|
3153
|
+
hasFfmpeg
|
|
3154
|
+
});
|
|
3155
|
+
if (!plan) return { image, oversized: true, resized: false };
|
|
3156
|
+
await writeFile4(inputFile, buf);
|
|
3157
|
+
await execFileAsync(plan.command, plan.args, { timeout: RESIZE_TIMEOUT_MS });
|
|
3158
|
+
const out = await readFile4(outputFile).catch(() => null);
|
|
3159
|
+
if (!out || out.length === 0) return { image, oversized: true, resized: false };
|
|
3160
|
+
return {
|
|
3161
|
+
image: { path: image.path, mimeType: "image/png", data: out.toString("base64") },
|
|
3162
|
+
oversized: true,
|
|
3163
|
+
resized: true
|
|
3164
|
+
};
|
|
3165
|
+
} catch {
|
|
3166
|
+
return { image, oversized: true, resized: false };
|
|
3167
|
+
} finally {
|
|
3168
|
+
await unlink(inputFile).catch(() => {
|
|
3169
|
+
});
|
|
3170
|
+
await unlink(outputFile).catch(() => {
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
// src/tools/fs/read-image.ts
|
|
3176
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
3177
|
+
var IMAGE_MIME_TYPES = {
|
|
3178
|
+
".png": "image/png",
|
|
3179
|
+
".jpg": "image/jpeg",
|
|
3180
|
+
".jpeg": "image/jpeg",
|
|
3181
|
+
".gif": "image/gif",
|
|
3182
|
+
".webp": "image/webp"
|
|
3183
|
+
};
|
|
2077
3184
|
var schema6 = z7.object({
|
|
2078
|
-
path: z7.string().describe(
|
|
2079
|
-
|
|
3185
|
+
path: z7.string().describe(
|
|
3186
|
+
"Image file path (png/jpg/jpeg/gif/webp), absolute or relative to the working directory"
|
|
3187
|
+
)
|
|
3188
|
+
});
|
|
3189
|
+
var readImageTool = {
|
|
3190
|
+
name: "read_image",
|
|
3191
|
+
description: "Read an image file (png, jpg, gif, webp) and show it to the model. Use this whenever the user references a screenshot, diagram, or picture file you need to see. Images larger than 2000px are downsampled automatically.",
|
|
3192
|
+
permission: "read",
|
|
3193
|
+
parameters: schema6,
|
|
3194
|
+
async execute(args, ctx) {
|
|
3195
|
+
const filePath = path13.resolve(ctx.cwd, args.path);
|
|
3196
|
+
const resolvedPath = await realpath3(filePath).catch(() => null);
|
|
3197
|
+
if (isSensitivePath(filePath) || resolvedPath !== null && isSensitivePath(resolvedPath)) {
|
|
3198
|
+
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
3199
|
+
}
|
|
3200
|
+
const mimeType = IMAGE_MIME_TYPES[path13.extname(filePath).toLowerCase()];
|
|
3201
|
+
if (!mimeType) {
|
|
3202
|
+
return {
|
|
3203
|
+
content: `Not a supported image (png/jpg/jpeg/gif/webp): ${args.path}`,
|
|
3204
|
+
isError: true
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
const st = await stat6(filePath).catch(() => null);
|
|
3208
|
+
if (!st || !st.isFile()) {
|
|
3209
|
+
return { content: `File not found: ${args.path}`, isError: true };
|
|
3210
|
+
}
|
|
3211
|
+
if (st.size > MAX_IMAGE_BYTES) {
|
|
3212
|
+
return {
|
|
3213
|
+
content: `Image too large (${Math.round(st.size / 1024)}KB > ${MAX_IMAGE_BYTES / 1024}KB): ${args.path}`,
|
|
3214
|
+
isError: true
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
const buf = await readFile5(filePath).catch(() => null);
|
|
3218
|
+
if (!buf || buf.length === 0) {
|
|
3219
|
+
return { content: `Failed to read ${args.path}`, isError: true };
|
|
3220
|
+
}
|
|
3221
|
+
if (!probeImageDimensions(buf)) {
|
|
3222
|
+
return {
|
|
3223
|
+
content: `File is not a valid image (unrecognized header): ${args.path}`,
|
|
3224
|
+
isError: true
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
const downsampled = await downsampleImageIfNeeded({
|
|
3228
|
+
path: args.path,
|
|
3229
|
+
mimeType,
|
|
3230
|
+
data: buf.toString("base64")
|
|
3231
|
+
});
|
|
3232
|
+
if (downsampled.oversized && !downsampled.resized) {
|
|
3233
|
+
return {
|
|
3234
|
+
content: `Image exceeds 2000px and could not be downsampled on this platform: ${args.path}`,
|
|
3235
|
+
isError: true
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
const image = downsampled.image;
|
|
3239
|
+
const dims = probeImageDimensions(Buffer.from(image.data, "base64"));
|
|
3240
|
+
const sizeKB = Math.round(image.data.length / 1024);
|
|
3241
|
+
const desc = dims ? `${dims.width}x${dims.height}` : "unknown dimensions";
|
|
3242
|
+
const resizedNote = downsampled.resized ? " (downsampled to fit 2000px)" : "";
|
|
3243
|
+
return {
|
|
3244
|
+
content: `Image loaded: ${args.path} (${desc}, ~${sizeKB}KB)${resizedNote}. The image is attached below \u2014 look at it now.`,
|
|
3245
|
+
images: [image]
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
3249
|
+
|
|
3250
|
+
// src/tools/fs/write.ts
|
|
3251
|
+
import { mkdir as mkdir3, writeFile as writeFile5 } from "fs/promises";
|
|
3252
|
+
import path14 from "path";
|
|
3253
|
+
import { z as z8 } from "zod";
|
|
3254
|
+
var schema7 = z8.object({
|
|
3255
|
+
path: z8.string().describe("File path, absolute or relative to the working directory"),
|
|
3256
|
+
content: z8.string().describe("Full content to write to the file")
|
|
2080
3257
|
});
|
|
2081
3258
|
var writeFileTool = {
|
|
2082
3259
|
name: "write_file",
|
|
2083
3260
|
description: "Write content to a file, creating parent directories as needed. Overwrites existing files.",
|
|
2084
3261
|
permission: "write",
|
|
2085
|
-
parameters:
|
|
3262
|
+
parameters: schema7,
|
|
2086
3263
|
async execute(args, ctx) {
|
|
2087
|
-
const filePath =
|
|
3264
|
+
const filePath = path14.resolve(ctx.cwd, args.path);
|
|
2088
3265
|
const snapshot = await captureSnapshot(filePath, "write_file", ctx.snapshotContext);
|
|
2089
3266
|
try {
|
|
2090
|
-
await mkdir3(
|
|
2091
|
-
await
|
|
3267
|
+
await mkdir3(path14.dirname(filePath), { recursive: true });
|
|
3268
|
+
await writeFile5(filePath, args.content, "utf8");
|
|
2092
3269
|
} catch (err) {
|
|
2093
3270
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
2094
3271
|
}
|
|
@@ -2098,7 +3275,7 @@ var writeFileTool = {
|
|
|
2098
3275
|
};
|
|
2099
3276
|
|
|
2100
3277
|
// src/tools/tasks.ts
|
|
2101
|
-
import { z as
|
|
3278
|
+
import { z as z9 } from "zod";
|
|
2102
3279
|
|
|
2103
3280
|
// src/agent/agent-tasks.ts
|
|
2104
3281
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
@@ -2269,8 +3446,8 @@ function formatTaskFinished(task) {
|
|
|
2269
3446
|
}
|
|
2270
3447
|
|
|
2271
3448
|
// src/tools/tasks.ts
|
|
2272
|
-
var idSchema =
|
|
2273
|
-
id:
|
|
3449
|
+
var idSchema = z9.object({
|
|
3450
|
+
id: z9.string().describe("Task id, e.g. task-1 or agent-1")
|
|
2274
3451
|
});
|
|
2275
3452
|
function truncateOutput(s) {
|
|
2276
3453
|
if (s.length <= MAX_OUTPUT) return s;
|
|
@@ -2287,7 +3464,7 @@ var taskListTool = {
|
|
|
2287
3464
|
name: "task_list",
|
|
2288
3465
|
description: "List background tasks (shell commands and subagents) with status and runtime.",
|
|
2289
3466
|
permission: "read",
|
|
2290
|
-
parameters:
|
|
3467
|
+
parameters: z9.object({}),
|
|
2291
3468
|
execute() {
|
|
2292
3469
|
const shell = formatTaskList(defaultTaskManager.list());
|
|
2293
3470
|
const agents = defaultAgentTasks.list();
|
|
@@ -2346,20 +3523,20 @@ var taskKillTool = {
|
|
|
2346
3523
|
};
|
|
2347
3524
|
|
|
2348
3525
|
// src/tools/todo.ts
|
|
2349
|
-
import { mkdir as mkdir4, readFile as
|
|
2350
|
-
import
|
|
2351
|
-
import { z as
|
|
2352
|
-
var todoItemSchema =
|
|
2353
|
-
id:
|
|
2354
|
-
title:
|
|
2355
|
-
status:
|
|
3526
|
+
import { mkdir as mkdir4, readFile as readFile6, rename, writeFile as writeFile6 } from "fs/promises";
|
|
3527
|
+
import path15 from "path";
|
|
3528
|
+
import { z as z10 } from "zod";
|
|
3529
|
+
var todoItemSchema = z10.object({
|
|
3530
|
+
id: z10.number().int(),
|
|
3531
|
+
title: z10.string(),
|
|
3532
|
+
status: z10.enum(["pending", "in_progress", "done"])
|
|
2356
3533
|
});
|
|
2357
|
-
var writeSchema =
|
|
2358
|
-
todos:
|
|
3534
|
+
var writeSchema = z10.object({
|
|
3535
|
+
todos: z10.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
2359
3536
|
});
|
|
2360
|
-
var readSchema =
|
|
3537
|
+
var readSchema = z10.object({});
|
|
2361
3538
|
function fileFor(cwd) {
|
|
2362
|
-
return
|
|
3539
|
+
return path15.join(cwd, ".star", "todos.json");
|
|
2363
3540
|
}
|
|
2364
3541
|
var persistGuard = () => true;
|
|
2365
3542
|
function setTodoPersistGuard(guard) {
|
|
@@ -2379,7 +3556,7 @@ var TodoStore = class {
|
|
|
2379
3556
|
async load(cwd) {
|
|
2380
3557
|
let raw;
|
|
2381
3558
|
try {
|
|
2382
|
-
raw = await
|
|
3559
|
+
raw = await readFile6(fileFor(cwd), "utf8");
|
|
2383
3560
|
} catch {
|
|
2384
3561
|
return;
|
|
2385
3562
|
}
|
|
@@ -2397,10 +3574,10 @@ var TodoStore = class {
|
|
|
2397
3574
|
if (!persistGuard()) {
|
|
2398
3575
|
return;
|
|
2399
3576
|
}
|
|
2400
|
-
await mkdir4(
|
|
3577
|
+
await mkdir4(path15.join(cwd, ".star"), { recursive: true });
|
|
2401
3578
|
const file = fileFor(cwd);
|
|
2402
3579
|
const tmp = `${file}.tmp`;
|
|
2403
|
-
await
|
|
3580
|
+
await writeFile6(tmp, `${JSON.stringify(this.list(), null, 2)}
|
|
2404
3581
|
`);
|
|
2405
3582
|
await rename(tmp, file);
|
|
2406
3583
|
}
|
|
@@ -2473,13 +3650,13 @@ function createTodoTools(store = defaultStore) {
|
|
|
2473
3650
|
}
|
|
2474
3651
|
|
|
2475
3652
|
// src/tools/web/fetch.ts
|
|
2476
|
-
import { z as
|
|
3653
|
+
import { z as z11 } from "zod";
|
|
2477
3654
|
var DEFAULT_MAX_CHARS = 2e4;
|
|
2478
3655
|
var MAX_BYTES = 2 * 1024 * 1024;
|
|
2479
3656
|
var TIMEOUT_MS = 3e4;
|
|
2480
|
-
var
|
|
2481
|
-
url:
|
|
2482
|
-
maxChars:
|
|
3657
|
+
var schema8 = z11.object({
|
|
3658
|
+
url: z11.string().describe("The http(s) URL to fetch"),
|
|
3659
|
+
maxChars: z11.number().int().positive().optional().describe("Maximum characters of text to return (default 20000)")
|
|
2483
3660
|
});
|
|
2484
3661
|
function safeCodePoint(n, raw) {
|
|
2485
3662
|
if (!Number.isInteger(n) || n < 0 || n > 1114111) {
|
|
@@ -2550,7 +3727,7 @@ var webFetchTool = {
|
|
|
2550
3727
|
name: "web_fetch",
|
|
2551
3728
|
description: "Fetch a URL over http(s) and return its contents as readable text. HTML pages are converted to plain text (tags, scripts and styles removed). Output is truncated to maxChars (default 20000). Can reach internal/intranet addresses, so do not use it for untrusted URLs.",
|
|
2552
3729
|
permission: "read",
|
|
2553
|
-
parameters:
|
|
3730
|
+
parameters: schema8,
|
|
2554
3731
|
async execute(args, ctx) {
|
|
2555
3732
|
let url;
|
|
2556
3733
|
try {
|
|
@@ -2613,15 +3790,15 @@ var webFetchTool = {
|
|
|
2613
3790
|
};
|
|
2614
3791
|
|
|
2615
3792
|
// src/tools/web/search.ts
|
|
2616
|
-
import { z as
|
|
3793
|
+
import { z as z12 } from "zod";
|
|
2617
3794
|
var TIMEOUT_MS2 = 15e3;
|
|
2618
3795
|
var DEFAULT_MAX_RESULTS = 5;
|
|
2619
3796
|
var MAX_RESULTS_LIMIT = 10;
|
|
2620
3797
|
var SEARCH_URL = "https://html.duckduckgo.com/html/?q=";
|
|
2621
3798
|
var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
|
2622
|
-
var
|
|
2623
|
-
query:
|
|
2624
|
-
maxResults:
|
|
3799
|
+
var schema9 = z12.object({
|
|
3800
|
+
query: z12.string().min(1).describe("The search query"),
|
|
3801
|
+
maxResults: z12.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().describe(
|
|
2625
3802
|
`Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
|
|
2626
3803
|
)
|
|
2627
3804
|
});
|
|
@@ -2672,7 +3849,7 @@ var webSearchTool = {
|
|
|
2672
3849
|
name: "web_search",
|
|
2673
3850
|
description: "Search the web using DuckDuckGo and return a numbered list of results with title, URL and snippet. No API key required. Use web_fetch to read a result's full page.",
|
|
2674
3851
|
permission: "read",
|
|
2675
|
-
parameters:
|
|
3852
|
+
parameters: schema9,
|
|
2676
3853
|
async execute(args, ctx) {
|
|
2677
3854
|
const maxResults = Math.min(args.maxResults ?? DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT);
|
|
2678
3855
|
const url = `${SEARCH_URL}${encodeURIComponent(args.query)}`;
|
|
@@ -2735,6 +3912,7 @@ var ToolRegistry = class {
|
|
|
2735
3912
|
constructor(todoStore) {
|
|
2736
3913
|
for (const tool of [
|
|
2737
3914
|
readFileTool,
|
|
3915
|
+
readImageTool,
|
|
2738
3916
|
writeFileTool,
|
|
2739
3917
|
editFileTool,
|
|
2740
3918
|
globTool,
|
|
@@ -2766,12 +3944,12 @@ var ToolRegistry = class {
|
|
|
2766
3944
|
|
|
2767
3945
|
// src/agent/project-memory.ts
|
|
2768
3946
|
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
2769
|
-
import
|
|
3947
|
+
import path16 from "path";
|
|
2770
3948
|
var PROJECT_MEMORY_FILE = "AGENTS.md";
|
|
2771
3949
|
var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
|
|
2772
3950
|
var cache = /* @__PURE__ */ new Map();
|
|
2773
3951
|
function readProjectMemory(cwd) {
|
|
2774
|
-
const file =
|
|
3952
|
+
const file = path16.join(cwd, PROJECT_MEMORY_FILE);
|
|
2775
3953
|
let mtimeMs;
|
|
2776
3954
|
try {
|
|
2777
3955
|
mtimeMs = statSync2(file).mtimeMs;
|
|
@@ -2801,8 +3979,8 @@ ${body}`;
|
|
|
2801
3979
|
|
|
2802
3980
|
// src/agent/skills.ts
|
|
2803
3981
|
import { readFileSync as readFileSync3, readdirSync, statSync as statSync3 } from "fs";
|
|
2804
|
-
import
|
|
2805
|
-
import { z as
|
|
3982
|
+
import path17 from "path";
|
|
3983
|
+
import { z as z13 } from "zod";
|
|
2806
3984
|
var SKILL_FILE = "SKILL.md";
|
|
2807
3985
|
var SKILL_MAX_CHARS = 32 * 1024;
|
|
2808
3986
|
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
@@ -2832,7 +4010,7 @@ function parseSkillFile(filePath, dirName, scope) {
|
|
|
2832
4010
|
const name = parsed?.meta.name || dirName;
|
|
2833
4011
|
const description = parsed?.meta.description;
|
|
2834
4012
|
if (parsed && NAME_PATTERN.test(name) && description) {
|
|
2835
|
-
skill = { name, description, filePath, dir:
|
|
4013
|
+
skill = { name, description, filePath, dir: path17.dirname(filePath), scope };
|
|
2836
4014
|
}
|
|
2837
4015
|
} catch {
|
|
2838
4016
|
skill = null;
|
|
@@ -2849,14 +4027,14 @@ function scanDir(base, scope, into) {
|
|
|
2849
4027
|
}
|
|
2850
4028
|
for (const entry of entries.sort()) {
|
|
2851
4029
|
if (!NAME_PATTERN.test(entry)) continue;
|
|
2852
|
-
const parsed = parseSkillFile(
|
|
4030
|
+
const parsed = parseSkillFile(path17.join(base, entry, SKILL_FILE), entry, scope);
|
|
2853
4031
|
if (parsed) into.set(parsed.name, parsed);
|
|
2854
4032
|
}
|
|
2855
4033
|
}
|
|
2856
4034
|
function discoverSkills(cwd, home = starHome()) {
|
|
2857
4035
|
const byName = /* @__PURE__ */ new Map();
|
|
2858
|
-
scanDir(
|
|
2859
|
-
scanDir(
|
|
4036
|
+
scanDir(path17.join(home, "skills"), "user", byName);
|
|
4037
|
+
scanDir(path17.join(cwd, ".star", "skills"), "project", byName);
|
|
2860
4038
|
return [...byName.values()];
|
|
2861
4039
|
}
|
|
2862
4040
|
function formatSkillsBlock(skills) {
|
|
@@ -2887,8 +4065,8 @@ function createSkillTool(deps) {
|
|
|
2887
4065
|
name: "skill",
|
|
2888
4066
|
description: "Load the full instructions of a skill by name. Available skills are listed in the system prompt when present; call this before starting work that matches one of them.",
|
|
2889
4067
|
permission: "read",
|
|
2890
|
-
parameters:
|
|
2891
|
-
name:
|
|
4068
|
+
parameters: z13.object({
|
|
4069
|
+
name: z13.string().describe("Name of the skill to load, as listed in the system prompt.")
|
|
2892
4070
|
}),
|
|
2893
4071
|
async execute(args) {
|
|
2894
4072
|
const skills = discoverSkills(deps.cwd, deps.home);
|
|
@@ -2918,7 +4096,7 @@ function createSkillTool(deps) {
|
|
|
2918
4096
|
}
|
|
2919
4097
|
|
|
2920
4098
|
// src/agent/subagent.ts
|
|
2921
|
-
import { z as
|
|
4099
|
+
import { z as z14 } from "zod";
|
|
2922
4100
|
|
|
2923
4101
|
// src/tools/index.ts
|
|
2924
4102
|
function createDefaultRegistry(todoStore) {
|
|
@@ -2929,8 +4107,37 @@ function createDefaultRegistry(todoStore) {
|
|
|
2929
4107
|
var MAX_SUBAGENT_DEPTH = 1;
|
|
2930
4108
|
var MAX_REPORT_CHARS = 2e4;
|
|
2931
4109
|
var SUBAGENT_PROMPT = "You are a subagent spawned to handle a focused subtask on behalf of the main agent. Work autonomously with the tools available to you, then finish with a concise report of what you found or did. Your final text is returned to the main agent as the tool result \u2014 make it self-contained.";
|
|
4110
|
+
function convertUsagePricing(usage, from, to, config) {
|
|
4111
|
+
if (from === to) return usage;
|
|
4112
|
+
const fromCfg = config.models.find((m) => m.model === from.modelId);
|
|
4113
|
+
const toCfg = config.models.find((m) => m.model === to.modelId);
|
|
4114
|
+
if (fromCfg?.promptPrice === void 0 || fromCfg.completionPrice === void 0 || toCfg?.promptPrice === void 0 || toCfg.completionPrice === void 0 || toCfg.promptPrice <= 0 || toCfg.completionPrice <= 0) {
|
|
4115
|
+
return usage;
|
|
4116
|
+
}
|
|
4117
|
+
const cached = Math.min(Math.max(usage.cachedPromptTokens ?? 0, 0), usage.promptTokens);
|
|
4118
|
+
const promptUsd = (usage.promptTokens - cached) * fromCfg.promptPrice;
|
|
4119
|
+
const cachedUsd = cached * (fromCfg.cacheReadPrice ?? fromCfg.promptPrice);
|
|
4120
|
+
const cacheReadUsd = (usage.cacheReadInputTokens ?? 0) * (fromCfg.cacheReadPrice ?? 0);
|
|
4121
|
+
const completionUsd = usage.completionTokens * fromCfg.completionPrice;
|
|
4122
|
+
const scaledCached = cachedUsd / (toCfg.cacheReadPrice ?? toCfg.promptPrice);
|
|
4123
|
+
const promptTokens = Math.round(promptUsd / toCfg.promptPrice + scaledCached);
|
|
4124
|
+
const cachedPromptTokens = Math.round(scaledCached);
|
|
4125
|
+
const toCacheReadPrice = toCfg.cacheReadPrice !== void 0 && toCfg.cacheReadPrice > 0 ? toCfg.cacheReadPrice : void 0;
|
|
4126
|
+
const completionTokens = Math.round(
|
|
4127
|
+
completionUsd / toCfg.completionPrice + (toCacheReadPrice === void 0 ? cacheReadUsd / toCfg.completionPrice : 0)
|
|
4128
|
+
);
|
|
4129
|
+
const cacheReadInputTokens = toCacheReadPrice === void 0 ? 0 : Math.round(cacheReadUsd / toCacheReadPrice);
|
|
4130
|
+
const result = {
|
|
4131
|
+
promptTokens,
|
|
4132
|
+
completionTokens,
|
|
4133
|
+
totalTokens: promptTokens + completionTokens
|
|
4134
|
+
};
|
|
4135
|
+
if (cachedPromptTokens > 0) result.cachedPromptTokens = cachedPromptTokens;
|
|
4136
|
+
if (cacheReadInputTokens > 0) result.cacheReadInputTokens = cacheReadInputTokens;
|
|
4137
|
+
return result;
|
|
4138
|
+
}
|
|
2932
4139
|
async function runSubagent(deps, args, cwd, signal) {
|
|
2933
|
-
const { AgentLoop: AgentLoop2 } = await import("./loop-
|
|
4140
|
+
const { AgentLoop: AgentLoop2 } = await import("./loop-DVVPIOS7.js");
|
|
2934
4141
|
const child = new AgentLoop2({
|
|
2935
4142
|
model: deps.model,
|
|
2936
4143
|
registry: createDefaultRegistry(),
|
|
@@ -2939,12 +4146,17 @@ async function runSubagent(deps, args, cwd, signal) {
|
|
|
2939
4146
|
system: deps.system ? `${deps.system}
|
|
2940
4147
|
|
|
2941
4148
|
${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
|
|
4149
|
+
providerMetadata: deps.providerMetadata,
|
|
4150
|
+
temperature: deps.temperature,
|
|
4151
|
+
streamIdleTimeoutSec: deps.streamIdleTimeoutSec,
|
|
4152
|
+
streamFirstChunkTimeoutSec: deps.streamFirstChunkTimeoutSec,
|
|
2942
4153
|
subagentDepth: deps.depth + 1
|
|
2943
4154
|
});
|
|
2944
4155
|
const confirmHandler = deps.getConfirmHandler?.();
|
|
2945
4156
|
if (confirmHandler) child.confirmHandler = confirmHandler;
|
|
2946
4157
|
let toolCalls = 0;
|
|
2947
4158
|
let error = null;
|
|
4159
|
+
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
2948
4160
|
try {
|
|
2949
4161
|
for await (const event of child.stream(args.prompt, signal)) {
|
|
2950
4162
|
if (event.type === "tool-result") {
|
|
@@ -2952,10 +4164,18 @@ ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
|
|
|
2952
4164
|
} else if (event.type === "error") {
|
|
2953
4165
|
error = event.error.message;
|
|
2954
4166
|
break;
|
|
4167
|
+
} else if (event.type === "finish" && event.usage) {
|
|
4168
|
+
usage.promptTokens += event.usage.promptTokens;
|
|
4169
|
+
usage.completionTokens += event.usage.completionTokens;
|
|
4170
|
+
usage.totalTokens += event.usage.totalTokens;
|
|
4171
|
+
usage.cachedPromptTokens = (usage.cachedPromptTokens ?? 0) + (event.usage.cachedPromptTokens ?? 0);
|
|
4172
|
+
usage.cacheReadInputTokens = (usage.cacheReadInputTokens ?? 0) + (event.usage.cacheReadInputTokens ?? 0);
|
|
2955
4173
|
}
|
|
2956
4174
|
}
|
|
2957
4175
|
} catch (e) {
|
|
2958
4176
|
error = e instanceof Error ? e.message : String(e);
|
|
4177
|
+
} finally {
|
|
4178
|
+
if (usage.totalTokens > 0) deps.onUsage?.(usage, deps.model);
|
|
2959
4179
|
}
|
|
2960
4180
|
if (signal.aborted) {
|
|
2961
4181
|
throw new Error("Tool execution aborted.");
|
|
@@ -2988,10 +4208,10 @@ function createSubagentTool(deps) {
|
|
|
2988
4208
|
name: "subagent",
|
|
2989
4209
|
description: "Spawn a subagent with its own agent loop to handle a focused subtask (research, exploration, or an isolated change). The subagent has the same tools but cannot spawn further subagents. Returns the subagent's final report. Prefer this for self-contained work that would otherwise clutter the main conversation. Set run_in_background to start it without blocking: independent subtasks then run concurrently and their reports are delivered to you automatically when they finish (also inspectable via task_list/task_output/task_kill).",
|
|
2990
4210
|
permission: "exec",
|
|
2991
|
-
parameters:
|
|
2992
|
-
prompt:
|
|
2993
|
-
description:
|
|
2994
|
-
run_in_background:
|
|
4211
|
+
parameters: z14.object({
|
|
4212
|
+
prompt: z14.string().describe("Complete, self-contained instructions for the subagent."),
|
|
4213
|
+
description: z14.string().optional().describe("Short label for the subtask, shown in the result header."),
|
|
4214
|
+
run_in_background: z14.boolean().optional().describe(
|
|
2995
4215
|
"Run concurrently in the background and return a task id immediately (default false). Use for independent subtasks that can run in parallel."
|
|
2996
4216
|
)
|
|
2997
4217
|
}),
|
|
@@ -3017,14 +4237,14 @@ function createSubagentTool(deps) {
|
|
|
3017
4237
|
}
|
|
3018
4238
|
|
|
3019
4239
|
// src/agent/user-memory.ts
|
|
3020
|
-
import { appendFileSync, mkdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
3021
|
-
import
|
|
3022
|
-
import { z as
|
|
4240
|
+
import { appendFileSync as appendFileSync2, mkdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
4241
|
+
import path18 from "path";
|
|
4242
|
+
import { z as z15 } from "zod";
|
|
3023
4243
|
var USER_MEMORY_MAX_CHARS = 32 * 1024;
|
|
3024
4244
|
var REMEMBER_TEXT_MAX_CHARS = 500;
|
|
3025
4245
|
var cache3 = /* @__PURE__ */ new Map();
|
|
3026
4246
|
function readUserMemory(home = starHome()) {
|
|
3027
|
-
const file =
|
|
4247
|
+
const file = path18.join(home, "MEMORY.md");
|
|
3028
4248
|
let mtimeMs;
|
|
3029
4249
|
try {
|
|
3030
4250
|
mtimeMs = statSync4(file).mtimeMs;
|
|
@@ -3056,7 +4276,7 @@ function readUserMemory(home = starHome()) {
|
|
|
3056
4276
|
}
|
|
3057
4277
|
function appendUserMemory(text, home = starHome()) {
|
|
3058
4278
|
mkdirSync(home, { recursive: true });
|
|
3059
|
-
|
|
4279
|
+
appendFileSync2(path18.join(home, "MEMORY.md"), `- ${text}
|
|
3060
4280
|
`, "utf8");
|
|
3061
4281
|
}
|
|
3062
4282
|
function createRememberTool(deps = {}) {
|
|
@@ -3064,8 +4284,8 @@ function createRememberTool(deps = {}) {
|
|
|
3064
4284
|
name: "remember",
|
|
3065
4285
|
description: `Save a note to the user's long-term memory (${userMemoryPath()}), injected into every future session. ONLY use when the user explicitly asks to remember or save something. Never use autonomously.`,
|
|
3066
4286
|
permission: "write",
|
|
3067
|
-
parameters:
|
|
3068
|
-
text:
|
|
4287
|
+
parameters: z15.object({
|
|
4288
|
+
text: z15.string().max(REMEMBER_TEXT_MAX_CHARS).describe("The note to save, as a single concise sentence.")
|
|
3069
4289
|
}),
|
|
3070
4290
|
async execute(args) {
|
|
3071
4291
|
appendUserMemory(args.text, deps.home);
|
|
@@ -3076,9 +4296,10 @@ function createRememberTool(deps = {}) {
|
|
|
3076
4296
|
|
|
3077
4297
|
// src/agent/loop.ts
|
|
3078
4298
|
var PLAN_MODE_PROMPT = "\n\nYou are currently in PLAN MODE. Research the task using only the read-only tools available to you, then present a concrete, step-by-step implementation plan as your final answer. You must not modify files, run shell commands, or otherwise change the system \u2014 write/exec tools are unavailable in this mode. Do not ask the user to run commands for you; note manual steps in the plan instead. The user will review your plan and approve it before any execution begins.";
|
|
3079
|
-
var PENDING_WORK_PATTERN = /(?:我将|我会|我现在|接下来|下一步|下面(?:我|将)|稍后|随后|准备开始|现在开始|马上|即将|这就|待会|开始(?:执行|进行|处理|转换|动手|生成|写入|运行|创建)|(?:完成后|然后|接着)会|让我(?:们)?(?:来)
|
|
4299
|
+
var PENDING_WORK_PATTERN = /(?:我将|我会|我现在|接下来|下一步|下面(?:我|将)|稍后|随后|准备开始|现在开始|马上|即将|这就|待会|开始(?:执行|进行|处理|转换|动手|生成|写入|运行|创建)|(?:完成后|然后|接着)会|让我(?:们)?(?:来)?(?!知道)|I will|I'll|I am going to|I'm going to|I shall|\blet me(?!\s+know\b)|next,? I|I will now)/i;
|
|
3080
4300
|
var AUTO_CONTINUE_NUDGE = "[auto-continue] You ended your turn with words instead of actions. Do not describe or restate the plan \u2014 continue the task NOW by calling tools. Reply with text only if the task is already fully complete.";
|
|
3081
4301
|
var EMPTY_REPLY_NUDGE = "[auto-continue] Your previous reply was empty \u2014 no text and no tool calls. If the task is fully complete, reply with one short confirmation; otherwise continue NOW by calling tools.";
|
|
4302
|
+
var TRUNCATED_CONTINUE_NUDGE = "[auto-continue] Your previous reply was cut off mid-stream by a timeout \u2014 it is incomplete. Resume EXACTLY where you stopped: do not repeat or restate what you already wrote; continue with the next tool call or the remainder of the text.";
|
|
3082
4303
|
function isFilteredEmptyFinish(finishReason) {
|
|
3083
4304
|
return finishReason === "content-filter";
|
|
3084
4305
|
}
|
|
@@ -3089,7 +4310,7 @@ function finalNudgeWarning(autoContinues, maxAutoContinues) {
|
|
|
3089
4310
|
return autoContinues >= maxAutoContinues ? " This is the final automatic continuation \u2014 if your next reply again has no tool calls, nudging stops and the turn is handed back to the user. Finish the work now or summarize what remains." : "";
|
|
3090
4311
|
}
|
|
3091
4312
|
var COMPLETION_CHECK_TIMEOUT_MS = 3e4;
|
|
3092
|
-
async function checkTaskComplete(model, request, finalReply, signal) {
|
|
4313
|
+
async function checkTaskComplete(model, request, finalReply, signal, temperature) {
|
|
3093
4314
|
const controller = new AbortController();
|
|
3094
4315
|
const timer = setTimeout(() => controller.abort(), COMPLETION_CHECK_TIMEOUT_MS);
|
|
3095
4316
|
const onAbort = () => controller.abort();
|
|
@@ -3098,6 +4319,7 @@ async function checkTaskComplete(model, request, finalReply, signal) {
|
|
|
3098
4319
|
const { text } = await generateText3({
|
|
3099
4320
|
model,
|
|
3100
4321
|
maxTokens: 8,
|
|
4322
|
+
temperature,
|
|
3101
4323
|
abortSignal: controller.signal,
|
|
3102
4324
|
prompt: [
|
|
3103
4325
|
"An AI coding agent was given this task by the user:",
|
|
@@ -3175,9 +4397,19 @@ var AgentLoop = class {
|
|
|
3175
4397
|
// consumes it as the redo label right after retractLastTurn sets it.
|
|
3176
4398
|
lastUndoneLabel = "";
|
|
3177
4399
|
// Doom-loop guard: signature of the most recent tool call and how many
|
|
3178
|
-
// times in a row it has repeated. Any different call resets the count
|
|
4400
|
+
// times in a row it has repeated. Any different call resets the count, and
|
|
4401
|
+
// each new user turn restarts the streak.
|
|
3179
4402
|
lastToolSignature = null;
|
|
3180
4403
|
repeatedToolCalls = 0;
|
|
4404
|
+
// Token usage subagents consumed, waiting to fold into this loop's next
|
|
4405
|
+
// finish event with usage: sync children report when their tool call
|
|
4406
|
+
// settles, background ones whenever they finish (drained or not).
|
|
4407
|
+
// Consumers meter only root-loop finish events, so folding here bills
|
|
4408
|
+
// subagent spend with zero consumer changes — /cost, /usage and
|
|
4409
|
+
// sessionBudgetUsd all cover it. A background child still running when the
|
|
4410
|
+
// session's last finish has passed never reports (best effort, noted in
|
|
4411
|
+
// agent-tasks cleanup paths).
|
|
4412
|
+
subagentUsage = null;
|
|
3181
4413
|
// Turn context of the in-flight stream() turn, stamped onto every tool
|
|
3182
4414
|
// execution so file snapshots attribute root changes to a turn and mark
|
|
3183
4415
|
// subagent changes as excluded from turn-level retraction.
|
|
@@ -3216,8 +4448,13 @@ var AgentLoop = class {
|
|
|
3216
4448
|
config: opts.config,
|
|
3217
4449
|
cwd: opts.cwd,
|
|
3218
4450
|
system: opts.system,
|
|
4451
|
+
providerMetadata: opts.providerMetadata,
|
|
4452
|
+
temperature: opts.temperature,
|
|
4453
|
+
streamIdleTimeoutSec: opts.streamIdleTimeoutSec,
|
|
4454
|
+
streamFirstChunkTimeoutSec: opts.streamFirstChunkTimeoutSec,
|
|
3219
4455
|
depth,
|
|
3220
|
-
getConfirmHandler: () => this.confirmHandler
|
|
4456
|
+
getConfirmHandler: () => this.confirmHandler,
|
|
4457
|
+
onUsage: (usage, childModel) => this.addSubagentUsage(usage, childModel)
|
|
3221
4458
|
})
|
|
3222
4459
|
);
|
|
3223
4460
|
}
|
|
@@ -3225,6 +4462,11 @@ var AgentLoop = class {
|
|
|
3225
4462
|
getMessages() {
|
|
3226
4463
|
return this.messages;
|
|
3227
4464
|
}
|
|
4465
|
+
// Rebinds the per-call provider metadata (e.g. after the reasoning effort
|
|
4466
|
+
// for the current model changed via /model) without recreating the loop.
|
|
4467
|
+
setProviderMetadata(metadata) {
|
|
4468
|
+
this.opts.providerMetadata = metadata;
|
|
4469
|
+
}
|
|
3228
4470
|
// Swaps the persistence target (/new starts a fresh session mid-REPL).
|
|
3229
4471
|
// Title generation is re-armed so the new session gets one after its first
|
|
3230
4472
|
// turn, and snapshot checkpoints now flow to the new store. The redo stack
|
|
@@ -3362,7 +4604,10 @@ var AgentLoop = class {
|
|
|
3362
4604
|
}
|
|
3363
4605
|
// Truncates the history back to the given message index (the user message
|
|
3364
4606
|
// at that index and everything after it is dropped) and persists the
|
|
3365
|
-
// trimmed history, so /rewind stays consistent across resume.
|
|
4607
|
+
// trimmed history, so /rewind stays consistent across resume. The redo
|
|
4608
|
+
// stack dies with the rewound context: its entries describe pre-/undo
|
|
4609
|
+
// working-tree states that no longer line up with this conversation, so
|
|
4610
|
+
// restoring one would clobber the rewound tree.
|
|
3366
4611
|
async retractFromIndex(index) {
|
|
3367
4612
|
const cut = this.retractionCut(index);
|
|
3368
4613
|
const removed = this.messages.length - cut;
|
|
@@ -3370,6 +4615,7 @@ var AgentLoop = class {
|
|
|
3370
4615
|
this.messages = this.messages.slice(0, cut);
|
|
3371
4616
|
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
3372
4617
|
this.turnMarkers = this.turnMarkers.filter((m) => m.userIndex < cut);
|
|
4618
|
+
this.redoStack = [];
|
|
3373
4619
|
return removed;
|
|
3374
4620
|
}
|
|
3375
4621
|
async appendContextMessage(text, role = "user") {
|
|
@@ -3477,9 +4723,12 @@ var AgentLoop = class {
|
|
|
3477
4723
|
this.maybeScheduleTitle();
|
|
3478
4724
|
const { config, registry, cwd } = this.opts;
|
|
3479
4725
|
const aiTools = this.buildAiTools();
|
|
4726
|
+
this.lastToolSignature = null;
|
|
4727
|
+
this.repeatedToolCalls = 0;
|
|
3480
4728
|
let autoContinues = 0;
|
|
3481
4729
|
let lastWasNudge = false;
|
|
3482
4730
|
let usedToolsThisTurn = false;
|
|
4731
|
+
let streamCutContinues = 0;
|
|
3483
4732
|
let openTodos = [];
|
|
3484
4733
|
const maxSteps = config.maxSteps;
|
|
3485
4734
|
let step = 0;
|
|
@@ -3493,6 +4742,7 @@ var AgentLoop = class {
|
|
|
3493
4742
|
`Max steps (${maxSteps}) reached with no tool execution since the last checkpoint, stopping.`
|
|
3494
4743
|
)
|
|
3495
4744
|
};
|
|
4745
|
+
await this.runStopHooks("max-steps");
|
|
3496
4746
|
return;
|
|
3497
4747
|
}
|
|
3498
4748
|
toolExecutedSinceCheckpoint = false;
|
|
@@ -3521,6 +4771,7 @@ ${finished.result}`
|
|
|
3521
4771
|
);
|
|
3522
4772
|
if (compacted.compacted) {
|
|
3523
4773
|
this.messages = await this.applyCompactionSummary(compacted, signal);
|
|
4774
|
+
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
3524
4775
|
this.turnMarkers = [];
|
|
3525
4776
|
}
|
|
3526
4777
|
const maxRetries = Math.max(0, config.streamMaxRetries);
|
|
@@ -3531,7 +4782,9 @@ ${finished.result}`
|
|
|
3531
4782
|
let lastFinishReason;
|
|
3532
4783
|
let lastUsage;
|
|
3533
4784
|
let sawReasoning = false;
|
|
4785
|
+
let wasTruncated = false;
|
|
3534
4786
|
let oversizedImagesStripped = false;
|
|
4787
|
+
const hasOutput = () => text.trim().length > 0 || toolCalls.length > 0;
|
|
3535
4788
|
for (let attempt = 0; ; attempt++) {
|
|
3536
4789
|
text = "";
|
|
3537
4790
|
toolCalls.length = 0;
|
|
@@ -3539,6 +4792,7 @@ ${finished.result}`
|
|
|
3539
4792
|
lastFinishReason = void 0;
|
|
3540
4793
|
lastUsage = void 0;
|
|
3541
4794
|
sawReasoning = false;
|
|
4795
|
+
wasTruncated = false;
|
|
3542
4796
|
const timeoutScale = Math.min(attempt + 1, 3);
|
|
3543
4797
|
try {
|
|
3544
4798
|
for await (const event of this.streamOnce(aiTools, signal, timeoutScale)) {
|
|
@@ -3553,8 +4807,9 @@ ${finished.result}`
|
|
|
3553
4807
|
yield event;
|
|
3554
4808
|
} else if (event.type === "finish") {
|
|
3555
4809
|
lastFinishReason = event.finishReason;
|
|
4810
|
+
if (event.truncated) wasTruncated = true;
|
|
3556
4811
|
lastUsage = event.usage;
|
|
3557
|
-
yield event;
|
|
4812
|
+
yield this.foldSubagentUsage(event);
|
|
3558
4813
|
} else if (event.type === "error") {
|
|
3559
4814
|
failure = event.error;
|
|
3560
4815
|
} else {
|
|
@@ -3568,9 +4823,10 @@ ${finished.result}`
|
|
|
3568
4823
|
}
|
|
3569
4824
|
if (signal.aborted) {
|
|
3570
4825
|
yield* this.persistInterrupted(text, toolCalls);
|
|
4826
|
+
await this.runStopHooks("aborted");
|
|
3571
4827
|
return;
|
|
3572
4828
|
}
|
|
3573
|
-
if (!failure && (
|
|
4829
|
+
if (!failure && hasOutput()) break;
|
|
3574
4830
|
if (failure && !oversizedImagesStripped && isOversizedImageError(failure)) {
|
|
3575
4831
|
oversizedImagesStripped = true;
|
|
3576
4832
|
const removed = await this.stripOversizedImages();
|
|
@@ -3584,7 +4840,7 @@ ${finished.result}`
|
|
|
3584
4840
|
}
|
|
3585
4841
|
if (failure && !isRetryableStreamError(failure)) break;
|
|
3586
4842
|
if (attempt >= maxRetries) break;
|
|
3587
|
-
if (!failure &&
|
|
4843
|
+
if (!failure && !hasOutput() && attempt >= 1 && (isFilteredEmptyFinish(lastFinishReason) || sawReasoning || lastUsage !== void 0 && lastUsage.completionTokens > 0))
|
|
3588
4844
|
break;
|
|
3589
4845
|
const delayMs = computeRetryDelayMs(attempt, failure ? baseDelay : baseDelay * 2, failure);
|
|
3590
4846
|
yield {
|
|
@@ -3596,14 +4852,16 @@ ${finished.result}`
|
|
|
3596
4852
|
};
|
|
3597
4853
|
if (!await sleep(delayMs, signal)) {
|
|
3598
4854
|
yield* this.persistInterrupted(text, toolCalls);
|
|
4855
|
+
await this.runStopHooks("aborted");
|
|
3599
4856
|
return;
|
|
3600
4857
|
}
|
|
3601
4858
|
}
|
|
3602
4859
|
if (failure) {
|
|
3603
4860
|
yield { type: "error", error: failure };
|
|
4861
|
+
await this.runStopHooks("error");
|
|
3604
4862
|
return;
|
|
3605
4863
|
}
|
|
3606
|
-
if (
|
|
4864
|
+
if (!hasOutput()) {
|
|
3607
4865
|
if (isSteerableEmptyFinish(lastFinishReason) && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
|
|
3608
4866
|
autoContinues++;
|
|
3609
4867
|
lastWasNudge = true;
|
|
@@ -3623,6 +4881,7 @@ ${finished.result}`
|
|
|
3623
4881
|
type: "error",
|
|
3624
4882
|
error: new Error("The model returned an empty response after retries; ending the turn.")
|
|
3625
4883
|
};
|
|
4884
|
+
await this.runStopHooks("empty");
|
|
3626
4885
|
return;
|
|
3627
4886
|
}
|
|
3628
4887
|
const assistantMessage = {
|
|
@@ -3640,6 +4899,27 @@ ${finished.result}`
|
|
|
3640
4899
|
this.messages.push(assistantMessage);
|
|
3641
4900
|
await this.persist(assistantMessage);
|
|
3642
4901
|
if (toolCalls.length === 0) {
|
|
4902
|
+
if (wasTruncated) {
|
|
4903
|
+
if (config.permissionMode !== "plan" && streamCutContinues < config.streamMaxRetries) {
|
|
4904
|
+
streamCutContinues++;
|
|
4905
|
+
yield {
|
|
4906
|
+
type: "notice",
|
|
4907
|
+
message: `reply cut short by a stream timeout; asking the model to resume where it stopped (${streamCutContinues}/${config.streamMaxRetries}).`
|
|
4908
|
+
};
|
|
4909
|
+
const nudge = { role: "user", content: TRUNCATED_CONTINUE_NUDGE };
|
|
4910
|
+
this.messages.push(nudge);
|
|
4911
|
+
await this.persist(nudge);
|
|
4912
|
+
continue;
|
|
4913
|
+
}
|
|
4914
|
+
if (config.permissionMode !== "plan") {
|
|
4915
|
+
yield {
|
|
4916
|
+
type: "notice",
|
|
4917
|
+
message: 'reply was cut short by a stream timeout and resume attempts are exhausted, handing the turn back \u2014 reply "continue" to keep going.'
|
|
4918
|
+
};
|
|
4919
|
+
}
|
|
4920
|
+
await this.runStopHooks("completed");
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
3643
4923
|
let reason = null;
|
|
3644
4924
|
let nudgeText = AUTO_CONTINUE_NUDGE;
|
|
3645
4925
|
if (openTodos.length > 0) {
|
|
@@ -3647,13 +4927,27 @@ ${finished.result}`
|
|
|
3647
4927
|
nudgeText = `[auto-continue] You ended your turn with unfinished todo item(s): ${openTodos.map((t) => `"${t}"`).join(
|
|
3648
4928
|
", "
|
|
3649
4929
|
)}. Complete them now with tool calls, or update the list with todo_write if they are no longer needed.`;
|
|
4930
|
+
} else if (usedToolsThisTurn && !signal.aborted) {
|
|
4931
|
+
const complete = await checkTaskComplete(
|
|
4932
|
+
this.opts.model,
|
|
4933
|
+
inputText,
|
|
4934
|
+
text,
|
|
4935
|
+
signal,
|
|
4936
|
+
this.opts.temperature
|
|
4937
|
+
);
|
|
4938
|
+
if (complete === false) {
|
|
4939
|
+
reason = "completion check reports the task unfinished";
|
|
4940
|
+
} else if (complete === null) {
|
|
4941
|
+
if (lastWasNudge) {
|
|
4942
|
+
reason = "reply still had no tool calls";
|
|
4943
|
+
} else if (PENDING_WORK_PATTERN.test(text)) {
|
|
4944
|
+
reason = "reply announced unfinished work";
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
3650
4947
|
} else if (lastWasNudge) {
|
|
3651
4948
|
reason = "reply still had no tool calls";
|
|
3652
4949
|
} else if (PENDING_WORK_PATTERN.test(text)) {
|
|
3653
4950
|
reason = "reply announced unfinished work";
|
|
3654
|
-
} else if (usedToolsThisTurn && !signal.aborted) {
|
|
3655
|
-
const complete = await checkTaskComplete(this.opts.model, inputText, text, signal);
|
|
3656
|
-
if (complete === false) reason = "completion check reports the task unfinished";
|
|
3657
4951
|
}
|
|
3658
4952
|
if (reason !== null && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
|
|
3659
4953
|
autoContinues++;
|
|
@@ -3676,7 +4970,7 @@ ${finished.result}`
|
|
|
3676
4970
|
message: `${reason}; auto-continue limit reached, handing the turn back \u2014 reply "continue" to keep going.`
|
|
3677
4971
|
};
|
|
3678
4972
|
}
|
|
3679
|
-
await this.
|
|
4973
|
+
await this.runStopHooks("completed");
|
|
3680
4974
|
return;
|
|
3681
4975
|
}
|
|
3682
4976
|
lastWasNudge = false;
|
|
@@ -3707,6 +5001,24 @@ ${finished.result}`
|
|
|
3707
5001
|
this.messages.push(toolMessage);
|
|
3708
5002
|
await this.persist(toolMessage);
|
|
3709
5003
|
answered.add(call.id);
|
|
5004
|
+
if (result.images && result.images.length > 0) {
|
|
5005
|
+
const imageMessage = {
|
|
5006
|
+
role: "user",
|
|
5007
|
+
content: [
|
|
5008
|
+
...result.images.map((img) => ({
|
|
5009
|
+
type: "image",
|
|
5010
|
+
image: img.data,
|
|
5011
|
+
mimeType: img.mimeType
|
|
5012
|
+
})),
|
|
5013
|
+
...result.images.map((img) => ({
|
|
5014
|
+
type: "text",
|
|
5015
|
+
text: `[image from ${call.name}: ${img.path}]`
|
|
5016
|
+
}))
|
|
5017
|
+
]
|
|
5018
|
+
};
|
|
5019
|
+
this.messages.push(imageMessage);
|
|
5020
|
+
await this.persist(imageMessage);
|
|
5021
|
+
}
|
|
3710
5022
|
if (call.name === "todo_write" && !result.isError) {
|
|
3711
5023
|
openTodos = pendingTodoTitles(call.args);
|
|
3712
5024
|
}
|
|
@@ -3739,7 +5051,10 @@ ${finished.result}`
|
|
|
3739
5051
|
yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
|
|
3740
5052
|
}
|
|
3741
5053
|
}
|
|
3742
|
-
if (signal.aborted)
|
|
5054
|
+
if (signal.aborted) {
|
|
5055
|
+
await this.runStopHooks("aborted");
|
|
5056
|
+
return;
|
|
5057
|
+
}
|
|
3743
5058
|
step++;
|
|
3744
5059
|
}
|
|
3745
5060
|
}
|
|
@@ -3772,7 +5087,7 @@ ${finished.result}`
|
|
|
3772
5087
|
const content = parts.join("\n\n");
|
|
3773
5088
|
const head = this.messages[0];
|
|
3774
5089
|
if (head?.role === "system" && typeof head.content === "string") {
|
|
3775
|
-
if (head.content !== content)
|
|
5090
|
+
if (head.content !== content) this.messages[0] = { role: "system", content };
|
|
3776
5091
|
} else if (head?.role !== "system") {
|
|
3777
5092
|
this.messages.unshift({ role: "system", content });
|
|
3778
5093
|
}
|
|
@@ -3785,7 +5100,7 @@ ${finished.result}`
|
|
|
3785
5100
|
const headCount = compacted.messages[0]?.role === "system" ? 1 : 0;
|
|
3786
5101
|
const dropped = this.messages.slice(headCount, headCount + compacted.droppedCount);
|
|
3787
5102
|
try {
|
|
3788
|
-
const summary = await summarizeMessages(dropped, model, signal);
|
|
5103
|
+
const summary = await summarizeMessages(dropped, model, signal, this.opts.temperature);
|
|
3789
5104
|
const messages = compacted.messages.slice();
|
|
3790
5105
|
messages[headCount] = {
|
|
3791
5106
|
role: "user",
|
|
@@ -3798,14 +5113,15 @@ ${summary}`
|
|
|
3798
5113
|
}
|
|
3799
5114
|
}
|
|
3800
5115
|
async *streamOnce(aiTools, signal, timeoutScale = 1) {
|
|
3801
|
-
const { streamChat } = await import("./stream-H4BYBVAZ.js");
|
|
3802
5116
|
for await (const event of streamChat({
|
|
3803
5117
|
model: this.opts.model,
|
|
3804
5118
|
messages: this.messages,
|
|
3805
5119
|
tools: aiTools,
|
|
3806
5120
|
abortSignal: signal,
|
|
3807
|
-
|
|
3808
|
-
|
|
5121
|
+
providerMetadata: this.opts.providerMetadata,
|
|
5122
|
+
idleTimeoutMs: (this.opts.streamIdleTimeoutSec ?? this.opts.config.streamIdleTimeoutSec) * 1e3 * timeoutScale,
|
|
5123
|
+
firstPartTimeoutMs: (this.opts.streamFirstChunkTimeoutSec ?? this.opts.config.streamFirstChunkTimeoutSec) * 1e3 * timeoutScale,
|
|
5124
|
+
temperature: this.opts.temperature
|
|
3809
5125
|
})) {
|
|
3810
5126
|
if (event.type === "finish" && event.truncated) {
|
|
3811
5127
|
yield {
|
|
@@ -3828,6 +5144,14 @@ ${summary}`
|
|
|
3828
5144
|
}
|
|
3829
5145
|
return tools;
|
|
3830
5146
|
}
|
|
5147
|
+
// Fires the Stop hooks for a turn's end, on every termination path (normal
|
|
5148
|
+
// completion, Esc abort, exhausted stream retries, terminal empty replies,
|
|
5149
|
+
// the maxSteps no-progress stop). The reason rides in STAR_TOOL_INPUT as
|
|
5150
|
+
// {"reason": ...}: Stop hooks ignore tool matchers and the variable was
|
|
5151
|
+
// never set for Stop before, so existing hooks are unaffected.
|
|
5152
|
+
async runStopHooks(reason) {
|
|
5153
|
+
await this.runEventHooks("Stop", void 0, { reason });
|
|
5154
|
+
}
|
|
3831
5155
|
async runEventHooks(event, toolName, toolInput) {
|
|
3832
5156
|
const empty = { blocked: false, warnings: [] };
|
|
3833
5157
|
if (this.opts.config.hooks.length === 0) return empty;
|
|
@@ -3844,6 +5168,47 @@ ${summary}`
|
|
|
3844
5168
|
return empty;
|
|
3845
5169
|
}
|
|
3846
5170
|
}
|
|
5171
|
+
// Accumulates a settled subagent's usage. When the child provably ran on a
|
|
5172
|
+
// different, fully priced model, the counts are first converted into
|
|
5173
|
+
// this-model-priced equivalents (convertUsagePricing): session cost is
|
|
5174
|
+
// token totals × the active model's price, so the conversion keeps the $
|
|
5175
|
+
// estimate accurate where raw sums would misprice.
|
|
5176
|
+
addSubagentUsage(usage, childModel) {
|
|
5177
|
+
const converted = convertUsagePricing(usage, childModel, this.opts.model, this.opts.config);
|
|
5178
|
+
const pending = this.subagentUsage ?? {
|
|
5179
|
+
promptTokens: 0,
|
|
5180
|
+
completionTokens: 0,
|
|
5181
|
+
totalTokens: 0
|
|
5182
|
+
};
|
|
5183
|
+
pending.promptTokens += converted.promptTokens;
|
|
5184
|
+
pending.completionTokens += converted.completionTokens;
|
|
5185
|
+
pending.totalTokens += converted.totalTokens;
|
|
5186
|
+
if (converted.cachedPromptTokens) {
|
|
5187
|
+
pending.cachedPromptTokens = (pending.cachedPromptTokens ?? 0) + converted.cachedPromptTokens;
|
|
5188
|
+
}
|
|
5189
|
+
if (converted.cacheReadInputTokens) {
|
|
5190
|
+
pending.cacheReadInputTokens = (pending.cacheReadInputTokens ?? 0) + converted.cacheReadInputTokens;
|
|
5191
|
+
}
|
|
5192
|
+
this.subagentUsage = pending;
|
|
5193
|
+
}
|
|
5194
|
+
// Folds pending subagent usage into an outgoing finish event (and clears
|
|
5195
|
+
// it). A finish without usage reports nothing to consumers, so the pending
|
|
5196
|
+
// amount is kept for the next finish that carries usage instead.
|
|
5197
|
+
foldSubagentUsage(event) {
|
|
5198
|
+
const pending = this.subagentUsage;
|
|
5199
|
+
if (!pending || !event.usage) return event;
|
|
5200
|
+
this.subagentUsage = null;
|
|
5201
|
+
const usage = {
|
|
5202
|
+
promptTokens: event.usage.promptTokens + pending.promptTokens,
|
|
5203
|
+
completionTokens: event.usage.completionTokens + pending.completionTokens,
|
|
5204
|
+
totalTokens: event.usage.totalTokens + pending.totalTokens
|
|
5205
|
+
};
|
|
5206
|
+
const cachedPromptTokens = (event.usage.cachedPromptTokens ?? 0) + (pending.cachedPromptTokens ?? 0);
|
|
5207
|
+
const cacheReadInputTokens = (event.usage.cacheReadInputTokens ?? 0) + (pending.cacheReadInputTokens ?? 0);
|
|
5208
|
+
if (cachedPromptTokens > 0) usage.cachedPromptTokens = cachedPromptTokens;
|
|
5209
|
+
if (cacheReadInputTokens > 0) usage.cacheReadInputTokens = cacheReadInputTokens;
|
|
5210
|
+
return { ...event, usage };
|
|
5211
|
+
}
|
|
3847
5212
|
// opencode's doom_loop guard: returns a refusal once the identical tool
|
|
3848
5213
|
// call (name + stable-serialized args) has repeated config.doomLoopThreshold
|
|
3849
5214
|
// times in a row; 0 disables. Any different signature resets the streak.
|
|
@@ -3940,17 +5305,25 @@ export {
|
|
|
3940
5305
|
compactMessages,
|
|
3941
5306
|
WORKING_DIFF_MAX_LINES,
|
|
3942
5307
|
isGitRepo,
|
|
3943
|
-
|
|
5308
|
+
getGitSummaryCached,
|
|
3944
5309
|
collectCommitContext,
|
|
3945
5310
|
buildCommitPrompt,
|
|
3946
5311
|
collectWorkingDiff,
|
|
3947
5312
|
MAX_IMAGE_DIMENSION,
|
|
3948
|
-
probeImageDimensions,
|
|
3949
5313
|
reconcileToolCalls,
|
|
5314
|
+
wrapFetchWithActivity,
|
|
5315
|
+
starHome,
|
|
5316
|
+
globalConfigPath,
|
|
5317
|
+
sessionsDir,
|
|
5318
|
+
userMemoryPath,
|
|
5319
|
+
envFilePath,
|
|
5320
|
+
projectConfigPath,
|
|
3950
5321
|
isSensitivePath,
|
|
3951
5322
|
isAllowedByRules,
|
|
3952
5323
|
buildAllowRule,
|
|
3953
5324
|
isDangerousCommand,
|
|
5325
|
+
removeTreeRepo,
|
|
5326
|
+
pruneTreeReposExcept,
|
|
3954
5327
|
diffTreeNames,
|
|
3955
5328
|
listSnapshots,
|
|
3956
5329
|
clearSnapshots,
|
|
@@ -3963,6 +5336,7 @@ export {
|
|
|
3963
5336
|
bashTool,
|
|
3964
5337
|
SKIP_DIRS,
|
|
3965
5338
|
createIgnorePredicate,
|
|
5339
|
+
downsampleImageIfNeeded,
|
|
3966
5340
|
formatTaskList,
|
|
3967
5341
|
formatTaskStarted,
|
|
3968
5342
|
formatTaskFinished,
|
|
@@ -3977,4 +5351,4 @@ export {
|
|
|
3977
5351
|
PLAN_MODE_PROMPT,
|
|
3978
5352
|
AgentLoop
|
|
3979
5353
|
};
|
|
3980
|
-
//# sourceMappingURL=chunk-
|
|
5354
|
+
//# sourceMappingURL=chunk-OOOW7FC4.js.map
|