@cryer/star-cli 0.3.6 → 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 +96 -40
- package/README.zh-CN.md +94 -41
- package/dist/chunk-4X25DNTF.js +1441 -0
- package/dist/chunk-4X25DNTF.js.map +1 -0
- package/dist/{chunk-I6FDXAWX.js → chunk-OOOW7FC4.js} +1558 -200
- package/dist/chunk-OOOW7FC4.js.map +1 -0
- package/dist/{loop-XY3TE5NN.js → loop-DVVPIOS7.js} +2 -3
- package/dist/main.js +141 -6677
- 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-I6FDXAWX.js.map +0 -1
- package/dist/stream-EFQPULQQ.js +0 -186
- package/dist/stream-EFQPULQQ.js.map +0 -1
- /package/dist/{loop-XY3TE5NN.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";
|
|
@@ -42,6 +33,17 @@ var ModelConfigSchema = z.object({
|
|
|
42
33
|
// request carries no effort field and the server-side default applies.
|
|
43
34
|
// Anthropic maps effort to a thinking budget instead — ignored there.
|
|
44
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(),
|
|
45
47
|
// Cost estimation needs BOTH prices (USD per 1M tokens); with only one set
|
|
46
48
|
// the model is treated as unpriced.
|
|
47
49
|
promptPrice: z.number().nonnegative().optional(),
|
|
@@ -91,10 +93,12 @@ var ConfigSchema = z.object({
|
|
|
91
93
|
compactThresholdTokens: z.number().int().positive().optional(),
|
|
92
94
|
// Seconds without any stream part before a stalled response is ended
|
|
93
95
|
// gracefully (some relays never send the terminal chunks). Applies once
|
|
94
|
-
//
|
|
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).
|
|
95
98
|
streamIdleTimeoutSec: z.number().positive().default(20),
|
|
96
|
-
// Seconds to wait for the very first
|
|
97
|
-
// 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.
|
|
98
102
|
streamFirstChunkTimeoutSec: z.number().positive().default(300),
|
|
99
103
|
// Extra attempts per model request when a stream fails transiently
|
|
100
104
|
// (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
|
|
@@ -150,11 +154,13 @@ import { generateText } from "ai";
|
|
|
150
154
|
var CHARS_PER_TOKEN = 4;
|
|
151
155
|
var MESSAGE_OVERHEAD = 4;
|
|
152
156
|
var IMAGE_PART_TOKENS = 1024;
|
|
153
|
-
|
|
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
|
+
}
|
|
154
160
|
function textWeight(text) {
|
|
155
161
|
let cjk = 0;
|
|
156
|
-
for (
|
|
157
|
-
if (
|
|
162
|
+
for (let i = 0; i < text.length; i++) {
|
|
163
|
+
if (isCjkCode(text.charCodeAt(i))) cjk++;
|
|
158
164
|
}
|
|
159
165
|
return text.length + cjk * (CHARS_PER_TOKEN - 1);
|
|
160
166
|
}
|
|
@@ -183,8 +189,13 @@ function imagePartCount(message) {
|
|
|
183
189
|
if (typeof message.content === "string" || message.role === "tool") return 0;
|
|
184
190
|
return message.content.filter((part) => part.type === "image" || part.type === "file").length;
|
|
185
191
|
}
|
|
192
|
+
var tokenCache = /* @__PURE__ */ new WeakMap();
|
|
186
193
|
function estimateMessageTokens(message) {
|
|
187
|
-
|
|
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;
|
|
188
199
|
}
|
|
189
200
|
function estimateTokens(messages) {
|
|
190
201
|
return messages.reduce((total, message) => total + estimateMessageTokens(message), 0);
|
|
@@ -222,7 +233,7 @@ function serializeMessage(message) {
|
|
|
222
233
|
});
|
|
223
234
|
return `${message.role}: ${parts.join("\n")}`;
|
|
224
235
|
}
|
|
225
|
-
async function summarizeMessages(messages, model, signal) {
|
|
236
|
+
async function summarizeMessages(messages, model, signal, temperature) {
|
|
226
237
|
const transcript = messages.map(serializeMessage).join("\n");
|
|
227
238
|
const { text } = await generateText({
|
|
228
239
|
model,
|
|
@@ -230,6 +241,7 @@ async function summarizeMessages(messages, model, signal) {
|
|
|
230
241
|
prompt: `Summarize this conversation so far:
|
|
231
242
|
|
|
232
243
|
${transcript}`,
|
|
244
|
+
temperature,
|
|
233
245
|
// A hung relay must not stall the whole turn on the summary: cap it at
|
|
234
246
|
// 60s and let the caller's abort (Esc) cut it short as well; the caller
|
|
235
247
|
// falls back to the truncation placeholder on any failure.
|
|
@@ -246,26 +258,29 @@ function compactMessages(messages, maxTokens, opts = {}) {
|
|
|
246
258
|
const hasSystem = first?.role === "system";
|
|
247
259
|
const head = hasSystem && first ? [first] : [];
|
|
248
260
|
const rest = hasSystem ? messages.slice(1) : messages.slice();
|
|
261
|
+
const messageTokens = rest.map((message) => estimateMessageTokens(message));
|
|
249
262
|
const turns = [];
|
|
250
|
-
for (
|
|
263
|
+
for (let i = 0; i < rest.length; i++) {
|
|
251
264
|
const current = turns[turns.length - 1];
|
|
252
|
-
if (
|
|
253
|
-
turns.push([
|
|
265
|
+
if (rest[i]?.role === "user" || !current) {
|
|
266
|
+
turns.push({ length: 1, tokens: messageTokens[i] ?? 0 });
|
|
254
267
|
} else {
|
|
255
|
-
current.
|
|
268
|
+
current.length += 1;
|
|
269
|
+
current.tokens += messageTokens[i] ?? 0;
|
|
256
270
|
}
|
|
257
271
|
}
|
|
272
|
+
const headTokens = estimateTokens(head);
|
|
273
|
+
const restTokens = estimateTokens(rest);
|
|
258
274
|
let droppedCount = 0;
|
|
259
|
-
let
|
|
260
|
-
|
|
261
|
-
const turn = turns[turnIndex];
|
|
275
|
+
let droppedTokens = 0;
|
|
276
|
+
for (const turn of turns) {
|
|
262
277
|
if (rest.length - droppedCount - turn.length < MIN_KEPT_MESSAGES) {
|
|
263
278
|
break;
|
|
264
279
|
}
|
|
265
280
|
droppedCount += turn.length;
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
281
|
+
droppedTokens += turn.tokens;
|
|
282
|
+
const candidateTokens = headTokens + estimateMessageTokens(placeholderMessage(droppedCount)) + (restTokens - droppedTokens);
|
|
283
|
+
if (candidateTokens <= limit) {
|
|
269
284
|
break;
|
|
270
285
|
}
|
|
271
286
|
}
|
|
@@ -390,6 +405,12 @@ ${unstaged}`);
|
|
|
390
405
|
return { status, diff: text, truncated };
|
|
391
406
|
}
|
|
392
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
|
+
|
|
393
414
|
// src/core/image.ts
|
|
394
415
|
var MAX_IMAGE_DIMENSION = 2e3;
|
|
395
416
|
var OVERSIZED_IMAGE_FALLBACK_BYTES = 5 * 1024 * 1024;
|
|
@@ -689,9 +710,365 @@ async function runHooks(event, hooks2, ctx) {
|
|
|
689
710
|
return result;
|
|
690
711
|
}
|
|
691
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
|
+
|
|
692
1069
|
// src/permissions/gate.ts
|
|
693
1070
|
import fs from "fs";
|
|
694
|
-
import
|
|
1071
|
+
import path3 from "path";
|
|
695
1072
|
|
|
696
1073
|
// src/core/sensitive.ts
|
|
697
1074
|
var SENSITIVE_BASENAMES = /* @__PURE__ */ new Set([
|
|
@@ -734,6 +1111,239 @@ function isSensitivePath(p) {
|
|
|
734
1111
|
return false;
|
|
735
1112
|
}
|
|
736
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
|
+
|
|
737
1347
|
// src/permissions/allow.ts
|
|
738
1348
|
function parseAllowRule(rule) {
|
|
739
1349
|
const match = /^([\w-]+)(?:\((.*)\))?$/.exec(rule.trim());
|
|
@@ -759,7 +1369,15 @@ function matchAllowRule(rule, req) {
|
|
|
759
1369
|
}
|
|
760
1370
|
function splitCommandChain(command) {
|
|
761
1371
|
if (command.includes("$(") || command.includes("`")) return null;
|
|
762
|
-
|
|
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);
|
|
763
1381
|
}
|
|
764
1382
|
function withCommand(req, command) {
|
|
765
1383
|
return {
|
|
@@ -796,27 +1414,206 @@ function buildAllowRule(req) {
|
|
|
796
1414
|
}
|
|
797
1415
|
|
|
798
1416
|
// src/permissions/gate.ts
|
|
799
|
-
var FILE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "read_file", "grep", "glob"]);
|
|
800
|
-
var
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
/
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
+
}
|
|
818
1614
|
function isDangerousCommand(command) {
|
|
819
|
-
|
|
1615
|
+
if (FORK_BOMB.test(command)) return true;
|
|
1616
|
+
return commandLineIsDangerous(command, 0);
|
|
820
1617
|
}
|
|
821
1618
|
function getStringArg(args, key) {
|
|
822
1619
|
if (typeof args === "object" && args !== null) {
|
|
@@ -828,7 +1625,7 @@ function getStringArg(args, key) {
|
|
|
828
1625
|
return void 0;
|
|
829
1626
|
}
|
|
830
1627
|
function normalizeAbsolute(p, cwd) {
|
|
831
|
-
const abs =
|
|
1628
|
+
const abs = path3.isAbsolute(p) ? path3.normalize(p) : path3.resolve(cwd, p);
|
|
832
1629
|
return abs.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
833
1630
|
}
|
|
834
1631
|
function isOutsideCwd(p, cwd) {
|
|
@@ -837,20 +1634,20 @@ function isOutsideCwd(p, cwd) {
|
|
|
837
1634
|
return abs !== base && !abs.startsWith(`${base}/`);
|
|
838
1635
|
}
|
|
839
1636
|
function resolveRealPath(p, cwd) {
|
|
840
|
-
let current =
|
|
1637
|
+
let current = path3.isAbsolute(p) ? path3.normalize(p) : path3.resolve(cwd, p);
|
|
841
1638
|
const missing = [];
|
|
842
1639
|
for (; ; ) {
|
|
843
1640
|
try {
|
|
844
|
-
return
|
|
1641
|
+
return path3.join(fs.realpathSync(current), ...missing.reverse());
|
|
845
1642
|
} catch {
|
|
846
|
-
const parent =
|
|
1643
|
+
const parent = path3.dirname(current);
|
|
847
1644
|
if (parent === current) return null;
|
|
848
|
-
missing.push(
|
|
1645
|
+
missing.push(path3.basename(current));
|
|
849
1646
|
current = parent;
|
|
850
1647
|
}
|
|
851
1648
|
}
|
|
852
1649
|
}
|
|
853
|
-
function
|
|
1650
|
+
function isOutsideResolved(p, cwd) {
|
|
854
1651
|
const resolved = resolveRealPath(p, cwd);
|
|
855
1652
|
if (resolved === null) return isOutsideCwd(p, cwd);
|
|
856
1653
|
let realCwd;
|
|
@@ -875,8 +1672,7 @@ function checkPermission(mode, req, ctx, allowRules = [], denyRules = []) {
|
|
|
875
1672
|
if (isWriteTool && isSensitivePath(filePath)) {
|
|
876
1673
|
return "deny";
|
|
877
1674
|
}
|
|
878
|
-
|
|
879
|
-
if (outside) {
|
|
1675
|
+
if (isOutsideResolved(filePath, ctx.cwd)) {
|
|
880
1676
|
if (mode === "ask" && req.level === "read") {
|
|
881
1677
|
return "ask";
|
|
882
1678
|
}
|
|
@@ -950,17 +1746,40 @@ function scheduleSessionTitle(store, userText, model) {
|
|
|
950
1746
|
// src/snapshot/git-tree.ts
|
|
951
1747
|
import { execFile } from "child_process";
|
|
952
1748
|
import { existsSync } from "fs";
|
|
953
|
-
import { mkdir } from "fs/promises";
|
|
954
|
-
import
|
|
955
|
-
import
|
|
1749
|
+
import { mkdir, readdir, rm, writeFile } from "fs/promises";
|
|
1750
|
+
import os2 from "os";
|
|
1751
|
+
import path4 from "path";
|
|
956
1752
|
var GIT_TIMEOUT_MS2 = 1e4;
|
|
1753
|
+
var GC_TIMEOUT_MS = 6e4;
|
|
957
1754
|
var GIT_MAX_BUFFER2 = 8 * 1024 * 1024;
|
|
958
|
-
|
|
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) {
|
|
959
1778
|
return new Promise((resolve) => {
|
|
960
1779
|
execFile(
|
|
961
1780
|
"git",
|
|
962
1781
|
args,
|
|
963
|
-
{ timeout:
|
|
1782
|
+
{ timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER2, encoding: "utf8" },
|
|
964
1783
|
(error, stdout) => {
|
|
965
1784
|
resolve(error ? null : stdout.trim());
|
|
966
1785
|
}
|
|
@@ -968,6 +1787,9 @@ function runGit2(args) {
|
|
|
968
1787
|
});
|
|
969
1788
|
}
|
|
970
1789
|
var gitAvailable = null;
|
|
1790
|
+
var trackFailureUntil = /* @__PURE__ */ new Map();
|
|
1791
|
+
var trackCounts = /* @__PURE__ */ new Map();
|
|
1792
|
+
var scrubbedRepos = /* @__PURE__ */ new Set();
|
|
971
1793
|
async function isGitAvailable() {
|
|
972
1794
|
if (gitAvailable === null) {
|
|
973
1795
|
gitAvailable = await runGit2(["--version"]) !== null;
|
|
@@ -975,41 +1797,119 @@ async function isGitAvailable() {
|
|
|
975
1797
|
return gitAvailable;
|
|
976
1798
|
}
|
|
977
1799
|
function treeRepoDir(cwd) {
|
|
978
|
-
const key = Buffer.from(
|
|
979
|
-
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
|
+
);
|
|
980
1826
|
}
|
|
981
1827
|
function isGuardedDir(cwd) {
|
|
982
|
-
const resolved =
|
|
983
|
-
const home =
|
|
1828
|
+
const resolved = path4.resolve(cwd);
|
|
1829
|
+
const home = path4.resolve(os2.homedir());
|
|
984
1830
|
const same = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
985
|
-
return same(resolved, home) || same(resolved,
|
|
1831
|
+
return same(resolved, home) || same(resolved, path4.parse(resolved).root);
|
|
986
1832
|
}
|
|
987
1833
|
async function ensureTreeRepo(cwd) {
|
|
988
1834
|
const dir = treeRepoDir(cwd);
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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;
|
|
993
1852
|
}
|
|
994
1853
|
function repoArgs(dir, cwd) {
|
|
995
1854
|
return [`--git-dir=${dir}`, `--work-tree=${cwd}`];
|
|
996
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
|
+
}
|
|
997
1873
|
async function trackTree(cwd) {
|
|
998
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) {
|
|
999
1887
|
if (!await isGitAvailable()) return null;
|
|
1000
1888
|
const dir = await ensureTreeRepo(cwd);
|
|
1001
1889
|
if (!dir) return null;
|
|
1002
1890
|
const base = repoArgs(dir, cwd);
|
|
1891
|
+
if (!scrubbedRepos.has(dir)) {
|
|
1892
|
+
if (!await untrackExcluded(dir, cwd)) return null;
|
|
1893
|
+
scrubbedRepos.add(dir);
|
|
1894
|
+
}
|
|
1003
1895
|
if (await runGit2([...base, "add", "-A"]) === null) return null;
|
|
1004
|
-
|
|
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;
|
|
1005
1904
|
}
|
|
1006
1905
|
async function restoreTree(cwd, hash) {
|
|
1007
1906
|
if (isGuardedDir(cwd)) return false;
|
|
1008
1907
|
if (!await isGitAvailable()) return false;
|
|
1009
|
-
const dir =
|
|
1010
|
-
if (!
|
|
1908
|
+
const dir = await ensureTreeRepo(cwd);
|
|
1909
|
+
if (!dir) return false;
|
|
1011
1910
|
const base = repoArgs(dir, cwd);
|
|
1012
1911
|
if (await runGit2([...base, "read-tree", hash]) === null) return false;
|
|
1912
|
+
if (!await untrackExcluded(dir, cwd)) return false;
|
|
1013
1913
|
if (await runGit2([...base, "checkout-index", "-f", "-a"]) === null) return false;
|
|
1014
1914
|
if (await runGit2([...base, "clean", "-fd"]) === null) return false;
|
|
1015
1915
|
return true;
|
|
@@ -1017,21 +1917,23 @@ async function restoreTree(cwd, hash) {
|
|
|
1017
1917
|
async function diffTreeNames(cwd, hash) {
|
|
1018
1918
|
if (!await isGitAvailable()) return null;
|
|
1019
1919
|
const dir = treeRepoDir(cwd);
|
|
1020
|
-
if (!existsSync(
|
|
1920
|
+
if (!existsSync(path4.join(dir, "HEAD"))) return null;
|
|
1021
1921
|
const base = repoArgs(dir, cwd);
|
|
1022
1922
|
const changed = await runGit2([...base, "diff", "--name-only", hash]);
|
|
1023
1923
|
const untracked = await runGit2([...base, "ls-files", "--others", "--exclude-standard"]);
|
|
1024
1924
|
if (changed === null || untracked === null) return null;
|
|
1025
1925
|
const names = /* @__PURE__ */ new Set();
|
|
1026
1926
|
for (const line of [...changed.split("\n"), ...untracked.split("\n")]) {
|
|
1027
|
-
if (line !== "") names.add(line);
|
|
1927
|
+
if (line !== "" && !isSensitivePath(line)) names.add(line);
|
|
1028
1928
|
}
|
|
1029
1929
|
return [...names];
|
|
1030
1930
|
}
|
|
1031
1931
|
|
|
1032
1932
|
// src/tools/fs/snapshots.ts
|
|
1033
|
-
import {
|
|
1034
|
-
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";
|
|
1035
1937
|
var MAX_SNAPSHOTS = 50;
|
|
1036
1938
|
var MAX_SNAPSHOT_CONTENT_BYTES = 5 * 1024 * 1024;
|
|
1037
1939
|
var stack = [];
|
|
@@ -1039,6 +1941,30 @@ var activeTurn = 0;
|
|
|
1039
1941
|
var activeTurnMessageIndex = -1;
|
|
1040
1942
|
var checkpointSeq = 0;
|
|
1041
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
|
+
}
|
|
1042
1968
|
function setSnapshotHooks(next) {
|
|
1043
1969
|
hooks = next;
|
|
1044
1970
|
}
|
|
@@ -1097,8 +2023,10 @@ async function pushSnapshot(snapshot) {
|
|
|
1097
2023
|
console.error("[star] failed to persist snapshot checkpoint:", err);
|
|
1098
2024
|
}
|
|
1099
2025
|
}
|
|
2026
|
+
await spillContent(snapshot);
|
|
1100
2027
|
if (stack.length > MAX_SNAPSHOTS) {
|
|
1101
2028
|
const evicted = stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
2029
|
+
for (const old of evicted) await discardSpill(old);
|
|
1102
2030
|
await removeFromStore(evicted.map((s) => s.id));
|
|
1103
2031
|
}
|
|
1104
2032
|
}
|
|
@@ -1109,9 +2037,11 @@ function listSnapshots() {
|
|
|
1109
2037
|
return [...stack];
|
|
1110
2038
|
}
|
|
1111
2039
|
function clearSnapshots() {
|
|
2040
|
+
for (const snapshot of stack) void discardSpill(snapshot);
|
|
1112
2041
|
stack.length = 0;
|
|
1113
2042
|
}
|
|
1114
2043
|
function hydrateSnapshots(snapshots) {
|
|
2044
|
+
for (const snapshot of stack) void discardSpill(snapshot);
|
|
1115
2045
|
stack.length = 0;
|
|
1116
2046
|
stack.push(...snapshots.slice(-MAX_SNAPSHOTS));
|
|
1117
2047
|
for (const snapshot of stack) {
|
|
@@ -1136,11 +2066,11 @@ async function revert(snapshot) {
|
|
|
1136
2066
|
if (content === null) {
|
|
1137
2067
|
return `Skipped ${snapshot.path}: the snapshot content is no longer available.`;
|
|
1138
2068
|
}
|
|
1139
|
-
await mkdir2(
|
|
1140
|
-
await
|
|
2069
|
+
await mkdir2(path5.dirname(snapshot.path), { recursive: true });
|
|
2070
|
+
await writeFile2(snapshot.path, content, "utf8");
|
|
1141
2071
|
return `Restored ${snapshot.path} to its state before ${snapshot.toolName}.`;
|
|
1142
2072
|
}
|
|
1143
|
-
await
|
|
2073
|
+
await rm2(snapshot.path, { force: true });
|
|
1144
2074
|
return `Deleted ${snapshot.path} (it did not exist before ${snapshot.toolName}).`;
|
|
1145
2075
|
}
|
|
1146
2076
|
function listTurnSnapshots(turn, includeSubagent = false) {
|
|
@@ -1167,6 +2097,7 @@ async function undoTurnSnapshots(turn, includeSubagent = false) {
|
|
|
1167
2097
|
const idx = stack.indexOf(snapshot);
|
|
1168
2098
|
if (idx >= 0) stack.splice(idx, 1);
|
|
1169
2099
|
revertedIds.push(snapshot.id);
|
|
2100
|
+
await discardSpill(snapshot);
|
|
1170
2101
|
} catch (err) {
|
|
1171
2102
|
messages.push(
|
|
1172
2103
|
`Failed to revert ${snapshot.path}: ${err.message} (checkpoint kept)`
|
|
@@ -1178,13 +2109,16 @@ async function undoTurnSnapshots(turn, includeSubagent = false) {
|
|
|
1178
2109
|
}
|
|
1179
2110
|
async function dropTurnSnapshots(turn, includeSubagent = false) {
|
|
1180
2111
|
const dropped = [];
|
|
2112
|
+
const droppedSnapshots = [];
|
|
1181
2113
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
1182
2114
|
const snapshot = stack[i];
|
|
1183
2115
|
if (snapshot && snapshot.turn === turn && (includeSubagent || ownerOf(snapshot) === "root")) {
|
|
1184
2116
|
dropped.push(snapshot.id);
|
|
2117
|
+
droppedSnapshots.push(snapshot);
|
|
1185
2118
|
stack.splice(i, 1);
|
|
1186
2119
|
}
|
|
1187
2120
|
}
|
|
2121
|
+
for (const snapshot of droppedSnapshots) await discardSpill(snapshot);
|
|
1188
2122
|
await removeFromStore(dropped);
|
|
1189
2123
|
return dropped.length;
|
|
1190
2124
|
}
|
|
@@ -1206,6 +2140,7 @@ async function rewindToSnapshot(id, includeSubagent = false) {
|
|
|
1206
2140
|
const idx = stack.indexOf(snapshot);
|
|
1207
2141
|
if (idx >= 0) stack.splice(idx, 1);
|
|
1208
2142
|
revertedIds.push(snapshot.id);
|
|
2143
|
+
await discardSpill(snapshot);
|
|
1209
2144
|
if (snapshot.messageIndex >= 0) {
|
|
1210
2145
|
messageIndex = messageIndex === null ? snapshot.messageIndex : Math.min(messageIndex, snapshot.messageIndex);
|
|
1211
2146
|
}
|
|
@@ -1222,7 +2157,7 @@ async function rewindToSnapshot(id, includeSubagent = false) {
|
|
|
1222
2157
|
// src/tools/bash.ts
|
|
1223
2158
|
import { spawn as spawn3 } from "child_process";
|
|
1224
2159
|
import { existsSync as existsSync2 } from "fs";
|
|
1225
|
-
import
|
|
2160
|
+
import path6 from "path";
|
|
1226
2161
|
import { z as z2 } from "zod";
|
|
1227
2162
|
|
|
1228
2163
|
// src/tasks/manager.ts
|
|
@@ -1241,6 +2176,9 @@ var TaskManager = class extends EventEmitter {
|
|
|
1241
2176
|
const child = spawn2(spec.shell, spec.wrap(opts.command), {
|
|
1242
2177
|
cwd: opts.cwd,
|
|
1243
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"],
|
|
1244
2182
|
// POSIX: the child leads its own process group so killTree can SIGKILL
|
|
1245
2183
|
// the whole tree with a negative pid (Windows uses taskkill /t).
|
|
1246
2184
|
detached: process.platform !== "win32"
|
|
@@ -1356,11 +2294,11 @@ var HALF_OUTPUT = MAX_OUTPUT / 2;
|
|
|
1356
2294
|
var DEFAULT_TIMEOUT = 120;
|
|
1357
2295
|
var MAX_TIMEOUT = 600;
|
|
1358
2296
|
function findOnPath(exe, exclude) {
|
|
1359
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
2297
|
+
for (const dir of (process.env.PATH ?? "").split(path6.delimiter)) {
|
|
1360
2298
|
if (!dir || exclude?.(dir)) {
|
|
1361
2299
|
continue;
|
|
1362
2300
|
}
|
|
1363
|
-
const full =
|
|
2301
|
+
const full = path6.join(dir, exe);
|
|
1364
2302
|
if (existsSync2(full)) {
|
|
1365
2303
|
return full;
|
|
1366
2304
|
}
|
|
@@ -1378,16 +2316,22 @@ function resolveShellUncached() {
|
|
|
1378
2316
|
}
|
|
1379
2317
|
const gitExe = findOnPath("git.exe");
|
|
1380
2318
|
if (gitExe) {
|
|
1381
|
-
const sibling =
|
|
2319
|
+
const sibling = path6.join(path6.dirname(path6.dirname(gitExe)), "bin", "bash.exe");
|
|
1382
2320
|
if (existsSync2(sibling)) {
|
|
1383
2321
|
return { shell: sibling, wrap: (c) => ["-c", c], label: "bash" };
|
|
1384
2322
|
}
|
|
1385
2323
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
2324
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2325
|
+
for (const root of [
|
|
2326
|
+
process.env.ProgramFiles,
|
|
2327
|
+
process.env["ProgramFiles(x86)"],
|
|
2328
|
+
process.env.ProgramW6432
|
|
1390
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);
|
|
1391
2335
|
if (existsSync2(candidate)) {
|
|
1392
2336
|
return { shell: candidate, wrap: (c) => ["-c", c], label: "bash" };
|
|
1393
2337
|
}
|
|
@@ -1489,7 +2433,10 @@ description: ${args.description ?? "(none)"}`
|
|
|
1489
2433
|
const child = spawn3(spec.shell, spec.wrap(args.command), {
|
|
1490
2434
|
cwd: ctx.cwd,
|
|
1491
2435
|
windowsHide: true,
|
|
1492
|
-
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"]
|
|
1493
2440
|
});
|
|
1494
2441
|
const output = new BoundedOutput();
|
|
1495
2442
|
let settled = false;
|
|
@@ -1538,9 +2485,10 @@ Command timed out after ${timeoutSeconds}s`,
|
|
|
1538
2485
|
};
|
|
1539
2486
|
|
|
1540
2487
|
// src/tools/fs/edit.ts
|
|
1541
|
-
import { readFile as readFile2, stat as stat2, writeFile as
|
|
1542
|
-
import
|
|
2488
|
+
import { readFile as readFile2, stat as stat2, writeFile as writeFile3 } from "fs/promises";
|
|
2489
|
+
import path7 from "path";
|
|
1543
2490
|
import { z as z3 } from "zod";
|
|
2491
|
+
var BINARY_PROBE_BYTES = 8192;
|
|
1544
2492
|
var schema2 = z3.object({
|
|
1545
2493
|
path: z3.string().describe("File path, absolute or relative to the working directory"),
|
|
1546
2494
|
old_string: z3.string().min(1).describe("Exact text to replace"),
|
|
@@ -1549,16 +2497,27 @@ var schema2 = z3.object({
|
|
|
1549
2497
|
});
|
|
1550
2498
|
var editFileTool = {
|
|
1551
2499
|
name: "edit_file",
|
|
1552
|
-
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.",
|
|
1553
2501
|
permission: "write",
|
|
1554
2502
|
parameters: schema2,
|
|
1555
2503
|
async execute(args, ctx) {
|
|
1556
|
-
const filePath =
|
|
2504
|
+
const filePath = path7.resolve(ctx.cwd, args.path);
|
|
1557
2505
|
let content;
|
|
1558
2506
|
let readMtimeMs;
|
|
1559
2507
|
try {
|
|
1560
|
-
|
|
1561
|
-
|
|
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");
|
|
1562
2521
|
} catch (err) {
|
|
1563
2522
|
return { content: `Failed to read ${args.path}: ${err.message}`, isError: true };
|
|
1564
2523
|
}
|
|
@@ -1586,7 +2545,7 @@ var editFileTool = {
|
|
|
1586
2545
|
isError: true
|
|
1587
2546
|
};
|
|
1588
2547
|
}
|
|
1589
|
-
await
|
|
2548
|
+
await writeFile3(filePath, updated, "utf8");
|
|
1590
2549
|
} catch (err) {
|
|
1591
2550
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
1592
2551
|
}
|
|
@@ -1608,23 +2567,23 @@ var editFileTool = {
|
|
|
1608
2567
|
};
|
|
1609
2568
|
|
|
1610
2569
|
// src/tools/fs/glob.ts
|
|
1611
|
-
import
|
|
2570
|
+
import path9 from "path";
|
|
1612
2571
|
import { z as z4 } from "zod";
|
|
1613
2572
|
|
|
1614
2573
|
// src/tools/fs/util.ts
|
|
1615
2574
|
import { readFileSync, statSync } from "fs";
|
|
1616
|
-
import { readdir, stat as stat3 } from "fs/promises";
|
|
1617
|
-
import
|
|
2575
|
+
import { readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
2576
|
+
import path8 from "path";
|
|
1618
2577
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
1619
2578
|
async function walkFiles(root, skipDirs = SKIP_DIRS, options = {}) {
|
|
1620
2579
|
const out = [];
|
|
1621
2580
|
async function walk(dir, relBase) {
|
|
1622
|
-
const entries = await
|
|
2581
|
+
const entries = await readdir2(dir, { withFileTypes: true }).catch(() => null);
|
|
1623
2582
|
if (!entries) {
|
|
1624
2583
|
return;
|
|
1625
2584
|
}
|
|
1626
2585
|
for (const entry of entries) {
|
|
1627
|
-
const abs =
|
|
2586
|
+
const abs = path8.join(dir, entry.name);
|
|
1628
2587
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
1629
2588
|
if (entry.isDirectory()) {
|
|
1630
2589
|
if (skipDirs.has(entry.name) || options.ignore?.(abs, true)) {
|
|
@@ -1783,7 +2742,7 @@ function parseIgnorePatterns(body) {
|
|
|
1783
2742
|
return out;
|
|
1784
2743
|
}
|
|
1785
2744
|
function loadIgnorePatterns(dir) {
|
|
1786
|
-
const file =
|
|
2745
|
+
const file = path8.join(dir, IGNORE_FILE);
|
|
1787
2746
|
let mtimeMs;
|
|
1788
2747
|
try {
|
|
1789
2748
|
mtimeMs = statSync(file).mtimeMs;
|
|
@@ -1808,9 +2767,9 @@ function createIgnorePredicate(cwd) {
|
|
|
1808
2767
|
if (patterns.length === 0) {
|
|
1809
2768
|
return void 0;
|
|
1810
2769
|
}
|
|
1811
|
-
const root =
|
|
2770
|
+
const root = path8.resolve(cwd);
|
|
1812
2771
|
return (absPath, isDir) => {
|
|
1813
|
-
const rel =
|
|
2772
|
+
const rel = path8.relative(root, absPath).split(path8.sep).join("/");
|
|
1814
2773
|
if (!rel || rel.startsWith("..")) {
|
|
1815
2774
|
return false;
|
|
1816
2775
|
}
|
|
@@ -1846,15 +2805,24 @@ var schema3 = z4.object({
|
|
|
1846
2805
|
),
|
|
1847
2806
|
path: z4.string().optional().describe("Directory to search, defaults to the working directory")
|
|
1848
2807
|
});
|
|
2808
|
+
function hasParentSegment(pattern) {
|
|
2809
|
+
return pattern.replace(/\\/g, "/").split("/").some((segment) => segment === "..");
|
|
2810
|
+
}
|
|
1849
2811
|
var globTool = {
|
|
1850
2812
|
name: "glob",
|
|
1851
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.",
|
|
1852
2814
|
permission: "read",
|
|
1853
2815
|
parameters: schema3,
|
|
1854
2816
|
async execute(args, ctx) {
|
|
1855
|
-
|
|
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 ?? ".");
|
|
1856
2824
|
const prefixSegments = literalDirPrefix(args.pattern);
|
|
1857
|
-
const walkRoot = prefixSegments.length > 0 ?
|
|
2825
|
+
const walkRoot = prefixSegments.length > 0 ? path9.join(root, ...prefixSegments) : root;
|
|
1858
2826
|
const prefix = prefixSegments.length > 0 ? `${prefixSegments.join("/")}/` : "";
|
|
1859
2827
|
const files = await walkFiles(walkRoot, void 0, { ignore: createIgnorePredicate(ctx.cwd) });
|
|
1860
2828
|
const matched = files.filter((f) => matchesGlob(args.pattern, `${prefix}${f.rel}`)).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_RESULTS);
|
|
@@ -1866,8 +2834,8 @@ var globTool = {
|
|
|
1866
2834
|
};
|
|
1867
2835
|
|
|
1868
2836
|
// src/tools/fs/grep.ts
|
|
1869
|
-
import { readFile as readFile3, stat as stat4 } from "fs/promises";
|
|
1870
|
-
import
|
|
2837
|
+
import { readFile as readFile3, realpath, stat as stat4 } from "fs/promises";
|
|
2838
|
+
import path10 from "path";
|
|
1871
2839
|
import { z as z5 } from "zod";
|
|
1872
2840
|
var MAX_MATCHES = 250;
|
|
1873
2841
|
var MAX_LINE_CHARS = 500;
|
|
@@ -1900,18 +2868,19 @@ var grepTool = {
|
|
|
1900
2868
|
} catch (err) {
|
|
1901
2869
|
return { content: `Invalid regular expression: ${err.message}`, isError: true };
|
|
1902
2870
|
}
|
|
1903
|
-
const root =
|
|
2871
|
+
const root = path10.resolve(ctx.cwd, args.path ?? ".");
|
|
1904
2872
|
let files;
|
|
1905
2873
|
try {
|
|
1906
2874
|
const st = await stat4(root);
|
|
1907
2875
|
if (st.isFile()) {
|
|
1908
|
-
|
|
2876
|
+
const resolved = await realpath(root).catch(() => null);
|
|
2877
|
+
if (isSensitivePath(root) || resolved !== null && isSensitivePath(resolved)) {
|
|
1909
2878
|
return {
|
|
1910
2879
|
content: `Refused to grep sensitive file: ${args.path ?? "."}`,
|
|
1911
2880
|
isError: true
|
|
1912
2881
|
};
|
|
1913
2882
|
}
|
|
1914
|
-
files = [{ abs: root, rel:
|
|
2883
|
+
files = [{ abs: root, rel: path10.basename(root), mtimeMs: st.mtimeMs }];
|
|
1915
2884
|
} else {
|
|
1916
2885
|
const walked = await walkFiles(root, void 0, {
|
|
1917
2886
|
ignore: createIgnorePredicate(ctx.cwd),
|
|
@@ -1977,13 +2946,13 @@ var grepTool = {
|
|
|
1977
2946
|
};
|
|
1978
2947
|
|
|
1979
2948
|
// src/tools/fs/read.ts
|
|
1980
|
-
import { open, stat as stat5 } from "fs/promises";
|
|
1981
|
-
import
|
|
2949
|
+
import { open, realpath as realpath2, stat as stat5 } from "fs/promises";
|
|
2950
|
+
import path11 from "path";
|
|
1982
2951
|
import { z as z6 } from "zod";
|
|
1983
2952
|
var DEFAULT_LIMIT = 2e3;
|
|
1984
2953
|
var MAX_CHARS = 100 * 1024;
|
|
1985
2954
|
var MAX_READ_BYTES = 1024 * 1024;
|
|
1986
|
-
var
|
|
2955
|
+
var BINARY_PROBE_BYTES2 = 8192;
|
|
1987
2956
|
function decodeUtf16be(buf) {
|
|
1988
2957
|
const len = buf.length - buf.length % 2;
|
|
1989
2958
|
const swapped = Buffer.allocUnsafe(len);
|
|
@@ -2000,7 +2969,7 @@ function decodeTextBuffer(buf) {
|
|
|
2000
2969
|
if (buf.length >= 2 && buf[0] === 254 && buf[1] === 255) {
|
|
2001
2970
|
return { text: decodeUtf16be(buf.subarray(2)) };
|
|
2002
2971
|
}
|
|
2003
|
-
if (buf.subarray(0,
|
|
2972
|
+
if (buf.subarray(0, BINARY_PROBE_BYTES2).includes(0)) {
|
|
2004
2973
|
return { binary: true };
|
|
2005
2974
|
}
|
|
2006
2975
|
return { text: buf.toString("utf8") };
|
|
@@ -2016,8 +2985,9 @@ var readFileTool = {
|
|
|
2016
2985
|
permission: "read",
|
|
2017
2986
|
parameters: schema5,
|
|
2018
2987
|
async execute(args, ctx) {
|
|
2019
|
-
const filePath =
|
|
2020
|
-
|
|
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)) {
|
|
2021
2991
|
return { content: `Refused to read sensitive file: ${args.path}`, isError: true };
|
|
2022
2992
|
}
|
|
2023
2993
|
const st = await stat5(filePath).catch(() => null);
|
|
@@ -2078,25 +3048,224 @@ var readFileTool = {
|
|
|
2078
3048
|
}
|
|
2079
3049
|
};
|
|
2080
3050
|
|
|
2081
|
-
// src/tools/fs/
|
|
2082
|
-
import {
|
|
2083
|
-
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";
|
|
2084
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
|
+
};
|
|
2085
3184
|
var schema6 = z7.object({
|
|
2086
|
-
path: z7.string().describe(
|
|
2087
|
-
|
|
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")
|
|
2088
3257
|
});
|
|
2089
3258
|
var writeFileTool = {
|
|
2090
3259
|
name: "write_file",
|
|
2091
3260
|
description: "Write content to a file, creating parent directories as needed. Overwrites existing files.",
|
|
2092
3261
|
permission: "write",
|
|
2093
|
-
parameters:
|
|
3262
|
+
parameters: schema7,
|
|
2094
3263
|
async execute(args, ctx) {
|
|
2095
|
-
const filePath =
|
|
3264
|
+
const filePath = path14.resolve(ctx.cwd, args.path);
|
|
2096
3265
|
const snapshot = await captureSnapshot(filePath, "write_file", ctx.snapshotContext);
|
|
2097
3266
|
try {
|
|
2098
|
-
await mkdir3(
|
|
2099
|
-
await
|
|
3267
|
+
await mkdir3(path14.dirname(filePath), { recursive: true });
|
|
3268
|
+
await writeFile5(filePath, args.content, "utf8");
|
|
2100
3269
|
} catch (err) {
|
|
2101
3270
|
return { content: `Failed to write ${args.path}: ${err.message}`, isError: true };
|
|
2102
3271
|
}
|
|
@@ -2106,7 +3275,7 @@ var writeFileTool = {
|
|
|
2106
3275
|
};
|
|
2107
3276
|
|
|
2108
3277
|
// src/tools/tasks.ts
|
|
2109
|
-
import { z as
|
|
3278
|
+
import { z as z9 } from "zod";
|
|
2110
3279
|
|
|
2111
3280
|
// src/agent/agent-tasks.ts
|
|
2112
3281
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
@@ -2277,8 +3446,8 @@ function formatTaskFinished(task) {
|
|
|
2277
3446
|
}
|
|
2278
3447
|
|
|
2279
3448
|
// src/tools/tasks.ts
|
|
2280
|
-
var idSchema =
|
|
2281
|
-
id:
|
|
3449
|
+
var idSchema = z9.object({
|
|
3450
|
+
id: z9.string().describe("Task id, e.g. task-1 or agent-1")
|
|
2282
3451
|
});
|
|
2283
3452
|
function truncateOutput(s) {
|
|
2284
3453
|
if (s.length <= MAX_OUTPUT) return s;
|
|
@@ -2295,7 +3464,7 @@ var taskListTool = {
|
|
|
2295
3464
|
name: "task_list",
|
|
2296
3465
|
description: "List background tasks (shell commands and subagents) with status and runtime.",
|
|
2297
3466
|
permission: "read",
|
|
2298
|
-
parameters:
|
|
3467
|
+
parameters: z9.object({}),
|
|
2299
3468
|
execute() {
|
|
2300
3469
|
const shell = formatTaskList(defaultTaskManager.list());
|
|
2301
3470
|
const agents = defaultAgentTasks.list();
|
|
@@ -2354,20 +3523,20 @@ var taskKillTool = {
|
|
|
2354
3523
|
};
|
|
2355
3524
|
|
|
2356
3525
|
// src/tools/todo.ts
|
|
2357
|
-
import { mkdir as mkdir4, readFile as
|
|
2358
|
-
import
|
|
2359
|
-
import { z as
|
|
2360
|
-
var todoItemSchema =
|
|
2361
|
-
id:
|
|
2362
|
-
title:
|
|
2363
|
-
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"])
|
|
2364
3533
|
});
|
|
2365
|
-
var writeSchema =
|
|
2366
|
-
todos:
|
|
3534
|
+
var writeSchema = z10.object({
|
|
3535
|
+
todos: z10.array(todoItemSchema).describe("The full todo list, replacing the current one")
|
|
2367
3536
|
});
|
|
2368
|
-
var readSchema =
|
|
3537
|
+
var readSchema = z10.object({});
|
|
2369
3538
|
function fileFor(cwd) {
|
|
2370
|
-
return
|
|
3539
|
+
return path15.join(cwd, ".star", "todos.json");
|
|
2371
3540
|
}
|
|
2372
3541
|
var persistGuard = () => true;
|
|
2373
3542
|
function setTodoPersistGuard(guard) {
|
|
@@ -2387,7 +3556,7 @@ var TodoStore = class {
|
|
|
2387
3556
|
async load(cwd) {
|
|
2388
3557
|
let raw;
|
|
2389
3558
|
try {
|
|
2390
|
-
raw = await
|
|
3559
|
+
raw = await readFile6(fileFor(cwd), "utf8");
|
|
2391
3560
|
} catch {
|
|
2392
3561
|
return;
|
|
2393
3562
|
}
|
|
@@ -2405,10 +3574,10 @@ var TodoStore = class {
|
|
|
2405
3574
|
if (!persistGuard()) {
|
|
2406
3575
|
return;
|
|
2407
3576
|
}
|
|
2408
|
-
await mkdir4(
|
|
3577
|
+
await mkdir4(path15.join(cwd, ".star"), { recursive: true });
|
|
2409
3578
|
const file = fileFor(cwd);
|
|
2410
3579
|
const tmp = `${file}.tmp`;
|
|
2411
|
-
await
|
|
3580
|
+
await writeFile6(tmp, `${JSON.stringify(this.list(), null, 2)}
|
|
2412
3581
|
`);
|
|
2413
3582
|
await rename(tmp, file);
|
|
2414
3583
|
}
|
|
@@ -2481,13 +3650,13 @@ function createTodoTools(store = defaultStore) {
|
|
|
2481
3650
|
}
|
|
2482
3651
|
|
|
2483
3652
|
// src/tools/web/fetch.ts
|
|
2484
|
-
import { z as
|
|
3653
|
+
import { z as z11 } from "zod";
|
|
2485
3654
|
var DEFAULT_MAX_CHARS = 2e4;
|
|
2486
3655
|
var MAX_BYTES = 2 * 1024 * 1024;
|
|
2487
3656
|
var TIMEOUT_MS = 3e4;
|
|
2488
|
-
var
|
|
2489
|
-
url:
|
|
2490
|
-
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)")
|
|
2491
3660
|
});
|
|
2492
3661
|
function safeCodePoint(n, raw) {
|
|
2493
3662
|
if (!Number.isInteger(n) || n < 0 || n > 1114111) {
|
|
@@ -2558,7 +3727,7 @@ var webFetchTool = {
|
|
|
2558
3727
|
name: "web_fetch",
|
|
2559
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.",
|
|
2560
3729
|
permission: "read",
|
|
2561
|
-
parameters:
|
|
3730
|
+
parameters: schema8,
|
|
2562
3731
|
async execute(args, ctx) {
|
|
2563
3732
|
let url;
|
|
2564
3733
|
try {
|
|
@@ -2621,15 +3790,15 @@ var webFetchTool = {
|
|
|
2621
3790
|
};
|
|
2622
3791
|
|
|
2623
3792
|
// src/tools/web/search.ts
|
|
2624
|
-
import { z as
|
|
3793
|
+
import { z as z12 } from "zod";
|
|
2625
3794
|
var TIMEOUT_MS2 = 15e3;
|
|
2626
3795
|
var DEFAULT_MAX_RESULTS = 5;
|
|
2627
3796
|
var MAX_RESULTS_LIMIT = 10;
|
|
2628
3797
|
var SEARCH_URL = "https://html.duckduckgo.com/html/?q=";
|
|
2629
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";
|
|
2630
|
-
var
|
|
2631
|
-
query:
|
|
2632
|
-
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(
|
|
2633
3802
|
`Maximum number of results to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS_LIMIT})`
|
|
2634
3803
|
)
|
|
2635
3804
|
});
|
|
@@ -2680,7 +3849,7 @@ var webSearchTool = {
|
|
|
2680
3849
|
name: "web_search",
|
|
2681
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.",
|
|
2682
3851
|
permission: "read",
|
|
2683
|
-
parameters:
|
|
3852
|
+
parameters: schema9,
|
|
2684
3853
|
async execute(args, ctx) {
|
|
2685
3854
|
const maxResults = Math.min(args.maxResults ?? DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT);
|
|
2686
3855
|
const url = `${SEARCH_URL}${encodeURIComponent(args.query)}`;
|
|
@@ -2743,6 +3912,7 @@ var ToolRegistry = class {
|
|
|
2743
3912
|
constructor(todoStore) {
|
|
2744
3913
|
for (const tool of [
|
|
2745
3914
|
readFileTool,
|
|
3915
|
+
readImageTool,
|
|
2746
3916
|
writeFileTool,
|
|
2747
3917
|
editFileTool,
|
|
2748
3918
|
globTool,
|
|
@@ -2774,12 +3944,12 @@ var ToolRegistry = class {
|
|
|
2774
3944
|
|
|
2775
3945
|
// src/agent/project-memory.ts
|
|
2776
3946
|
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
2777
|
-
import
|
|
3947
|
+
import path16 from "path";
|
|
2778
3948
|
var PROJECT_MEMORY_FILE = "AGENTS.md";
|
|
2779
3949
|
var PROJECT_MEMORY_MAX_CHARS = 32 * 1024;
|
|
2780
3950
|
var cache = /* @__PURE__ */ new Map();
|
|
2781
3951
|
function readProjectMemory(cwd) {
|
|
2782
|
-
const file =
|
|
3952
|
+
const file = path16.join(cwd, PROJECT_MEMORY_FILE);
|
|
2783
3953
|
let mtimeMs;
|
|
2784
3954
|
try {
|
|
2785
3955
|
mtimeMs = statSync2(file).mtimeMs;
|
|
@@ -2809,8 +3979,8 @@ ${body}`;
|
|
|
2809
3979
|
|
|
2810
3980
|
// src/agent/skills.ts
|
|
2811
3981
|
import { readFileSync as readFileSync3, readdirSync, statSync as statSync3 } from "fs";
|
|
2812
|
-
import
|
|
2813
|
-
import { z as
|
|
3982
|
+
import path17 from "path";
|
|
3983
|
+
import { z as z13 } from "zod";
|
|
2814
3984
|
var SKILL_FILE = "SKILL.md";
|
|
2815
3985
|
var SKILL_MAX_CHARS = 32 * 1024;
|
|
2816
3986
|
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
@@ -2840,7 +4010,7 @@ function parseSkillFile(filePath, dirName, scope) {
|
|
|
2840
4010
|
const name = parsed?.meta.name || dirName;
|
|
2841
4011
|
const description = parsed?.meta.description;
|
|
2842
4012
|
if (parsed && NAME_PATTERN.test(name) && description) {
|
|
2843
|
-
skill = { name, description, filePath, dir:
|
|
4013
|
+
skill = { name, description, filePath, dir: path17.dirname(filePath), scope };
|
|
2844
4014
|
}
|
|
2845
4015
|
} catch {
|
|
2846
4016
|
skill = null;
|
|
@@ -2857,14 +4027,14 @@ function scanDir(base, scope, into) {
|
|
|
2857
4027
|
}
|
|
2858
4028
|
for (const entry of entries.sort()) {
|
|
2859
4029
|
if (!NAME_PATTERN.test(entry)) continue;
|
|
2860
|
-
const parsed = parseSkillFile(
|
|
4030
|
+
const parsed = parseSkillFile(path17.join(base, entry, SKILL_FILE), entry, scope);
|
|
2861
4031
|
if (parsed) into.set(parsed.name, parsed);
|
|
2862
4032
|
}
|
|
2863
4033
|
}
|
|
2864
4034
|
function discoverSkills(cwd, home = starHome()) {
|
|
2865
4035
|
const byName = /* @__PURE__ */ new Map();
|
|
2866
|
-
scanDir(
|
|
2867
|
-
scanDir(
|
|
4036
|
+
scanDir(path17.join(home, "skills"), "user", byName);
|
|
4037
|
+
scanDir(path17.join(cwd, ".star", "skills"), "project", byName);
|
|
2868
4038
|
return [...byName.values()];
|
|
2869
4039
|
}
|
|
2870
4040
|
function formatSkillsBlock(skills) {
|
|
@@ -2895,8 +4065,8 @@ function createSkillTool(deps) {
|
|
|
2895
4065
|
name: "skill",
|
|
2896
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.",
|
|
2897
4067
|
permission: "read",
|
|
2898
|
-
parameters:
|
|
2899
|
-
name:
|
|
4068
|
+
parameters: z13.object({
|
|
4069
|
+
name: z13.string().describe("Name of the skill to load, as listed in the system prompt.")
|
|
2900
4070
|
}),
|
|
2901
4071
|
async execute(args) {
|
|
2902
4072
|
const skills = discoverSkills(deps.cwd, deps.home);
|
|
@@ -2926,7 +4096,7 @@ function createSkillTool(deps) {
|
|
|
2926
4096
|
}
|
|
2927
4097
|
|
|
2928
4098
|
// src/agent/subagent.ts
|
|
2929
|
-
import { z as
|
|
4099
|
+
import { z as z14 } from "zod";
|
|
2930
4100
|
|
|
2931
4101
|
// src/tools/index.ts
|
|
2932
4102
|
function createDefaultRegistry(todoStore) {
|
|
@@ -2937,8 +4107,37 @@ function createDefaultRegistry(todoStore) {
|
|
|
2937
4107
|
var MAX_SUBAGENT_DEPTH = 1;
|
|
2938
4108
|
var MAX_REPORT_CHARS = 2e4;
|
|
2939
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
|
+
}
|
|
2940
4139
|
async function runSubagent(deps, args, cwd, signal) {
|
|
2941
|
-
const { AgentLoop: AgentLoop2 } = await import("./loop-
|
|
4140
|
+
const { AgentLoop: AgentLoop2 } = await import("./loop-DVVPIOS7.js");
|
|
2942
4141
|
const child = new AgentLoop2({
|
|
2943
4142
|
model: deps.model,
|
|
2944
4143
|
registry: createDefaultRegistry(),
|
|
@@ -2948,12 +4147,16 @@ async function runSubagent(deps, args, cwd, signal) {
|
|
|
2948
4147
|
|
|
2949
4148
|
${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
|
|
2950
4149
|
providerMetadata: deps.providerMetadata,
|
|
4150
|
+
temperature: deps.temperature,
|
|
4151
|
+
streamIdleTimeoutSec: deps.streamIdleTimeoutSec,
|
|
4152
|
+
streamFirstChunkTimeoutSec: deps.streamFirstChunkTimeoutSec,
|
|
2951
4153
|
subagentDepth: deps.depth + 1
|
|
2952
4154
|
});
|
|
2953
4155
|
const confirmHandler = deps.getConfirmHandler?.();
|
|
2954
4156
|
if (confirmHandler) child.confirmHandler = confirmHandler;
|
|
2955
4157
|
let toolCalls = 0;
|
|
2956
4158
|
let error = null;
|
|
4159
|
+
const usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
2957
4160
|
try {
|
|
2958
4161
|
for await (const event of child.stream(args.prompt, signal)) {
|
|
2959
4162
|
if (event.type === "tool-result") {
|
|
@@ -2961,10 +4164,18 @@ ${SUBAGENT_PROMPT}` : SUBAGENT_PROMPT,
|
|
|
2961
4164
|
} else if (event.type === "error") {
|
|
2962
4165
|
error = event.error.message;
|
|
2963
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);
|
|
2964
4173
|
}
|
|
2965
4174
|
}
|
|
2966
4175
|
} catch (e) {
|
|
2967
4176
|
error = e instanceof Error ? e.message : String(e);
|
|
4177
|
+
} finally {
|
|
4178
|
+
if (usage.totalTokens > 0) deps.onUsage?.(usage, deps.model);
|
|
2968
4179
|
}
|
|
2969
4180
|
if (signal.aborted) {
|
|
2970
4181
|
throw new Error("Tool execution aborted.");
|
|
@@ -2997,10 +4208,10 @@ function createSubagentTool(deps) {
|
|
|
2997
4208
|
name: "subagent",
|
|
2998
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).",
|
|
2999
4210
|
permission: "exec",
|
|
3000
|
-
parameters:
|
|
3001
|
-
prompt:
|
|
3002
|
-
description:
|
|
3003
|
-
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(
|
|
3004
4215
|
"Run concurrently in the background and return a task id immediately (default false). Use for independent subtasks that can run in parallel."
|
|
3005
4216
|
)
|
|
3006
4217
|
}),
|
|
@@ -3026,14 +4237,14 @@ function createSubagentTool(deps) {
|
|
|
3026
4237
|
}
|
|
3027
4238
|
|
|
3028
4239
|
// src/agent/user-memory.ts
|
|
3029
|
-
import { appendFileSync, mkdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
3030
|
-
import
|
|
3031
|
-
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";
|
|
3032
4243
|
var USER_MEMORY_MAX_CHARS = 32 * 1024;
|
|
3033
4244
|
var REMEMBER_TEXT_MAX_CHARS = 500;
|
|
3034
4245
|
var cache3 = /* @__PURE__ */ new Map();
|
|
3035
4246
|
function readUserMemory(home = starHome()) {
|
|
3036
|
-
const file =
|
|
4247
|
+
const file = path18.join(home, "MEMORY.md");
|
|
3037
4248
|
let mtimeMs;
|
|
3038
4249
|
try {
|
|
3039
4250
|
mtimeMs = statSync4(file).mtimeMs;
|
|
@@ -3065,7 +4276,7 @@ function readUserMemory(home = starHome()) {
|
|
|
3065
4276
|
}
|
|
3066
4277
|
function appendUserMemory(text, home = starHome()) {
|
|
3067
4278
|
mkdirSync(home, { recursive: true });
|
|
3068
|
-
|
|
4279
|
+
appendFileSync2(path18.join(home, "MEMORY.md"), `- ${text}
|
|
3069
4280
|
`, "utf8");
|
|
3070
4281
|
}
|
|
3071
4282
|
function createRememberTool(deps = {}) {
|
|
@@ -3073,8 +4284,8 @@ function createRememberTool(deps = {}) {
|
|
|
3073
4284
|
name: "remember",
|
|
3074
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.`,
|
|
3075
4286
|
permission: "write",
|
|
3076
|
-
parameters:
|
|
3077
|
-
text:
|
|
4287
|
+
parameters: z15.object({
|
|
4288
|
+
text: z15.string().max(REMEMBER_TEXT_MAX_CHARS).describe("The note to save, as a single concise sentence.")
|
|
3078
4289
|
}),
|
|
3079
4290
|
async execute(args) {
|
|
3080
4291
|
appendUserMemory(args.text, deps.home);
|
|
@@ -3085,9 +4296,10 @@ function createRememberTool(deps = {}) {
|
|
|
3085
4296
|
|
|
3086
4297
|
// src/agent/loop.ts
|
|
3087
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.";
|
|
3088
|
-
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;
|
|
3089
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.";
|
|
3090
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.";
|
|
3091
4303
|
function isFilteredEmptyFinish(finishReason) {
|
|
3092
4304
|
return finishReason === "content-filter";
|
|
3093
4305
|
}
|
|
@@ -3098,7 +4310,7 @@ function finalNudgeWarning(autoContinues, maxAutoContinues) {
|
|
|
3098
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." : "";
|
|
3099
4311
|
}
|
|
3100
4312
|
var COMPLETION_CHECK_TIMEOUT_MS = 3e4;
|
|
3101
|
-
async function checkTaskComplete(model, request, finalReply, signal) {
|
|
4313
|
+
async function checkTaskComplete(model, request, finalReply, signal, temperature) {
|
|
3102
4314
|
const controller = new AbortController();
|
|
3103
4315
|
const timer = setTimeout(() => controller.abort(), COMPLETION_CHECK_TIMEOUT_MS);
|
|
3104
4316
|
const onAbort = () => controller.abort();
|
|
@@ -3107,6 +4319,7 @@ async function checkTaskComplete(model, request, finalReply, signal) {
|
|
|
3107
4319
|
const { text } = await generateText3({
|
|
3108
4320
|
model,
|
|
3109
4321
|
maxTokens: 8,
|
|
4322
|
+
temperature,
|
|
3110
4323
|
abortSignal: controller.signal,
|
|
3111
4324
|
prompt: [
|
|
3112
4325
|
"An AI coding agent was given this task by the user:",
|
|
@@ -3184,9 +4397,19 @@ var AgentLoop = class {
|
|
|
3184
4397
|
// consumes it as the redo label right after retractLastTurn sets it.
|
|
3185
4398
|
lastUndoneLabel = "";
|
|
3186
4399
|
// Doom-loop guard: signature of the most recent tool call and how many
|
|
3187
|
-
// 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.
|
|
3188
4402
|
lastToolSignature = null;
|
|
3189
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;
|
|
3190
4413
|
// Turn context of the in-flight stream() turn, stamped onto every tool
|
|
3191
4414
|
// execution so file snapshots attribute root changes to a turn and mark
|
|
3192
4415
|
// subagent changes as excluded from turn-level retraction.
|
|
@@ -3226,8 +4449,12 @@ var AgentLoop = class {
|
|
|
3226
4449
|
cwd: opts.cwd,
|
|
3227
4450
|
system: opts.system,
|
|
3228
4451
|
providerMetadata: opts.providerMetadata,
|
|
4452
|
+
temperature: opts.temperature,
|
|
4453
|
+
streamIdleTimeoutSec: opts.streamIdleTimeoutSec,
|
|
4454
|
+
streamFirstChunkTimeoutSec: opts.streamFirstChunkTimeoutSec,
|
|
3229
4455
|
depth,
|
|
3230
|
-
getConfirmHandler: () => this.confirmHandler
|
|
4456
|
+
getConfirmHandler: () => this.confirmHandler,
|
|
4457
|
+
onUsage: (usage, childModel) => this.addSubagentUsage(usage, childModel)
|
|
3231
4458
|
})
|
|
3232
4459
|
);
|
|
3233
4460
|
}
|
|
@@ -3377,7 +4604,10 @@ var AgentLoop = class {
|
|
|
3377
4604
|
}
|
|
3378
4605
|
// Truncates the history back to the given message index (the user message
|
|
3379
4606
|
// at that index and everything after it is dropped) and persists the
|
|
3380
|
-
// 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.
|
|
3381
4611
|
async retractFromIndex(index) {
|
|
3382
4612
|
const cut = this.retractionCut(index);
|
|
3383
4613
|
const removed = this.messages.length - cut;
|
|
@@ -3385,6 +4615,7 @@ var AgentLoop = class {
|
|
|
3385
4615
|
this.messages = this.messages.slice(0, cut);
|
|
3386
4616
|
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
3387
4617
|
this.turnMarkers = this.turnMarkers.filter((m) => m.userIndex < cut);
|
|
4618
|
+
this.redoStack = [];
|
|
3388
4619
|
return removed;
|
|
3389
4620
|
}
|
|
3390
4621
|
async appendContextMessage(text, role = "user") {
|
|
@@ -3492,9 +4723,12 @@ var AgentLoop = class {
|
|
|
3492
4723
|
this.maybeScheduleTitle();
|
|
3493
4724
|
const { config, registry, cwd } = this.opts;
|
|
3494
4725
|
const aiTools = this.buildAiTools();
|
|
4726
|
+
this.lastToolSignature = null;
|
|
4727
|
+
this.repeatedToolCalls = 0;
|
|
3495
4728
|
let autoContinues = 0;
|
|
3496
4729
|
let lastWasNudge = false;
|
|
3497
4730
|
let usedToolsThisTurn = false;
|
|
4731
|
+
let streamCutContinues = 0;
|
|
3498
4732
|
let openTodos = [];
|
|
3499
4733
|
const maxSteps = config.maxSteps;
|
|
3500
4734
|
let step = 0;
|
|
@@ -3508,6 +4742,7 @@ var AgentLoop = class {
|
|
|
3508
4742
|
`Max steps (${maxSteps}) reached with no tool execution since the last checkpoint, stopping.`
|
|
3509
4743
|
)
|
|
3510
4744
|
};
|
|
4745
|
+
await this.runStopHooks("max-steps");
|
|
3511
4746
|
return;
|
|
3512
4747
|
}
|
|
3513
4748
|
toolExecutedSinceCheckpoint = false;
|
|
@@ -3536,6 +4771,7 @@ ${finished.result}`
|
|
|
3536
4771
|
);
|
|
3537
4772
|
if (compacted.compacted) {
|
|
3538
4773
|
this.messages = await this.applyCompactionSummary(compacted, signal);
|
|
4774
|
+
await this.opts.sessionStore?.replaceMessages([...this.messages]);
|
|
3539
4775
|
this.turnMarkers = [];
|
|
3540
4776
|
}
|
|
3541
4777
|
const maxRetries = Math.max(0, config.streamMaxRetries);
|
|
@@ -3546,7 +4782,9 @@ ${finished.result}`
|
|
|
3546
4782
|
let lastFinishReason;
|
|
3547
4783
|
let lastUsage;
|
|
3548
4784
|
let sawReasoning = false;
|
|
4785
|
+
let wasTruncated = false;
|
|
3549
4786
|
let oversizedImagesStripped = false;
|
|
4787
|
+
const hasOutput = () => text.trim().length > 0 || toolCalls.length > 0;
|
|
3550
4788
|
for (let attempt = 0; ; attempt++) {
|
|
3551
4789
|
text = "";
|
|
3552
4790
|
toolCalls.length = 0;
|
|
@@ -3554,6 +4792,7 @@ ${finished.result}`
|
|
|
3554
4792
|
lastFinishReason = void 0;
|
|
3555
4793
|
lastUsage = void 0;
|
|
3556
4794
|
sawReasoning = false;
|
|
4795
|
+
wasTruncated = false;
|
|
3557
4796
|
const timeoutScale = Math.min(attempt + 1, 3);
|
|
3558
4797
|
try {
|
|
3559
4798
|
for await (const event of this.streamOnce(aiTools, signal, timeoutScale)) {
|
|
@@ -3568,8 +4807,9 @@ ${finished.result}`
|
|
|
3568
4807
|
yield event;
|
|
3569
4808
|
} else if (event.type === "finish") {
|
|
3570
4809
|
lastFinishReason = event.finishReason;
|
|
4810
|
+
if (event.truncated) wasTruncated = true;
|
|
3571
4811
|
lastUsage = event.usage;
|
|
3572
|
-
yield event;
|
|
4812
|
+
yield this.foldSubagentUsage(event);
|
|
3573
4813
|
} else if (event.type === "error") {
|
|
3574
4814
|
failure = event.error;
|
|
3575
4815
|
} else {
|
|
@@ -3583,9 +4823,10 @@ ${finished.result}`
|
|
|
3583
4823
|
}
|
|
3584
4824
|
if (signal.aborted) {
|
|
3585
4825
|
yield* this.persistInterrupted(text, toolCalls);
|
|
4826
|
+
await this.runStopHooks("aborted");
|
|
3586
4827
|
return;
|
|
3587
4828
|
}
|
|
3588
|
-
if (!failure && (
|
|
4829
|
+
if (!failure && hasOutput()) break;
|
|
3589
4830
|
if (failure && !oversizedImagesStripped && isOversizedImageError(failure)) {
|
|
3590
4831
|
oversizedImagesStripped = true;
|
|
3591
4832
|
const removed = await this.stripOversizedImages();
|
|
@@ -3599,7 +4840,7 @@ ${finished.result}`
|
|
|
3599
4840
|
}
|
|
3600
4841
|
if (failure && !isRetryableStreamError(failure)) break;
|
|
3601
4842
|
if (attempt >= maxRetries) break;
|
|
3602
|
-
if (!failure &&
|
|
4843
|
+
if (!failure && !hasOutput() && attempt >= 1 && (isFilteredEmptyFinish(lastFinishReason) || sawReasoning || lastUsage !== void 0 && lastUsage.completionTokens > 0))
|
|
3603
4844
|
break;
|
|
3604
4845
|
const delayMs = computeRetryDelayMs(attempt, failure ? baseDelay : baseDelay * 2, failure);
|
|
3605
4846
|
yield {
|
|
@@ -3611,14 +4852,16 @@ ${finished.result}`
|
|
|
3611
4852
|
};
|
|
3612
4853
|
if (!await sleep(delayMs, signal)) {
|
|
3613
4854
|
yield* this.persistInterrupted(text, toolCalls);
|
|
4855
|
+
await this.runStopHooks("aborted");
|
|
3614
4856
|
return;
|
|
3615
4857
|
}
|
|
3616
4858
|
}
|
|
3617
4859
|
if (failure) {
|
|
3618
4860
|
yield { type: "error", error: failure };
|
|
4861
|
+
await this.runStopHooks("error");
|
|
3619
4862
|
return;
|
|
3620
4863
|
}
|
|
3621
|
-
if (
|
|
4864
|
+
if (!hasOutput()) {
|
|
3622
4865
|
if (isSteerableEmptyFinish(lastFinishReason) && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
|
|
3623
4866
|
autoContinues++;
|
|
3624
4867
|
lastWasNudge = true;
|
|
@@ -3638,6 +4881,7 @@ ${finished.result}`
|
|
|
3638
4881
|
type: "error",
|
|
3639
4882
|
error: new Error("The model returned an empty response after retries; ending the turn.")
|
|
3640
4883
|
};
|
|
4884
|
+
await this.runStopHooks("empty");
|
|
3641
4885
|
return;
|
|
3642
4886
|
}
|
|
3643
4887
|
const assistantMessage = {
|
|
@@ -3655,6 +4899,27 @@ ${finished.result}`
|
|
|
3655
4899
|
this.messages.push(assistantMessage);
|
|
3656
4900
|
await this.persist(assistantMessage);
|
|
3657
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
|
+
}
|
|
3658
4923
|
let reason = null;
|
|
3659
4924
|
let nudgeText = AUTO_CONTINUE_NUDGE;
|
|
3660
4925
|
if (openTodos.length > 0) {
|
|
@@ -3662,13 +4927,27 @@ ${finished.result}`
|
|
|
3662
4927
|
nudgeText = `[auto-continue] You ended your turn with unfinished todo item(s): ${openTodos.map((t) => `"${t}"`).join(
|
|
3663
4928
|
", "
|
|
3664
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
|
+
}
|
|
3665
4947
|
} else if (lastWasNudge) {
|
|
3666
4948
|
reason = "reply still had no tool calls";
|
|
3667
4949
|
} else if (PENDING_WORK_PATTERN.test(text)) {
|
|
3668
4950
|
reason = "reply announced unfinished work";
|
|
3669
|
-
} else if (usedToolsThisTurn && !signal.aborted) {
|
|
3670
|
-
const complete = await checkTaskComplete(this.opts.model, inputText, text, signal);
|
|
3671
|
-
if (complete === false) reason = "completion check reports the task unfinished";
|
|
3672
4951
|
}
|
|
3673
4952
|
if (reason !== null && config.permissionMode !== "plan" && autoContinues < config.maxAutoContinues) {
|
|
3674
4953
|
autoContinues++;
|
|
@@ -3691,7 +4970,7 @@ ${finished.result}`
|
|
|
3691
4970
|
message: `${reason}; auto-continue limit reached, handing the turn back \u2014 reply "continue" to keep going.`
|
|
3692
4971
|
};
|
|
3693
4972
|
}
|
|
3694
|
-
await this.
|
|
4973
|
+
await this.runStopHooks("completed");
|
|
3695
4974
|
return;
|
|
3696
4975
|
}
|
|
3697
4976
|
lastWasNudge = false;
|
|
@@ -3722,6 +5001,24 @@ ${finished.result}`
|
|
|
3722
5001
|
this.messages.push(toolMessage);
|
|
3723
5002
|
await this.persist(toolMessage);
|
|
3724
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
|
+
}
|
|
3725
5022
|
if (call.name === "todo_write" && !result.isError) {
|
|
3726
5023
|
openTodos = pendingTodoTitles(call.args);
|
|
3727
5024
|
}
|
|
@@ -3754,7 +5051,10 @@ ${finished.result}`
|
|
|
3754
5051
|
yield { type: "tool-result", id: call.id, name: call.name, content, isError: true };
|
|
3755
5052
|
}
|
|
3756
5053
|
}
|
|
3757
|
-
if (signal.aborted)
|
|
5054
|
+
if (signal.aborted) {
|
|
5055
|
+
await this.runStopHooks("aborted");
|
|
5056
|
+
return;
|
|
5057
|
+
}
|
|
3758
5058
|
step++;
|
|
3759
5059
|
}
|
|
3760
5060
|
}
|
|
@@ -3787,7 +5087,7 @@ ${finished.result}`
|
|
|
3787
5087
|
const content = parts.join("\n\n");
|
|
3788
5088
|
const head = this.messages[0];
|
|
3789
5089
|
if (head?.role === "system" && typeof head.content === "string") {
|
|
3790
|
-
if (head.content !== content)
|
|
5090
|
+
if (head.content !== content) this.messages[0] = { role: "system", content };
|
|
3791
5091
|
} else if (head?.role !== "system") {
|
|
3792
5092
|
this.messages.unshift({ role: "system", content });
|
|
3793
5093
|
}
|
|
@@ -3800,7 +5100,7 @@ ${finished.result}`
|
|
|
3800
5100
|
const headCount = compacted.messages[0]?.role === "system" ? 1 : 0;
|
|
3801
5101
|
const dropped = this.messages.slice(headCount, headCount + compacted.droppedCount);
|
|
3802
5102
|
try {
|
|
3803
|
-
const summary = await summarizeMessages(dropped, model, signal);
|
|
5103
|
+
const summary = await summarizeMessages(dropped, model, signal, this.opts.temperature);
|
|
3804
5104
|
const messages = compacted.messages.slice();
|
|
3805
5105
|
messages[headCount] = {
|
|
3806
5106
|
role: "user",
|
|
@@ -3813,15 +5113,15 @@ ${summary}`
|
|
|
3813
5113
|
}
|
|
3814
5114
|
}
|
|
3815
5115
|
async *streamOnce(aiTools, signal, timeoutScale = 1) {
|
|
3816
|
-
const { streamChat } = await import("./stream-EFQPULQQ.js");
|
|
3817
5116
|
for await (const event of streamChat({
|
|
3818
5117
|
model: this.opts.model,
|
|
3819
5118
|
messages: this.messages,
|
|
3820
5119
|
tools: aiTools,
|
|
3821
5120
|
abortSignal: signal,
|
|
3822
5121
|
providerMetadata: this.opts.providerMetadata,
|
|
3823
|
-
idleTimeoutMs: this.opts.config.streamIdleTimeoutSec * 1e3 * timeoutScale,
|
|
3824
|
-
firstPartTimeoutMs: this.opts.config.streamFirstChunkTimeoutSec * 1e3 * timeoutScale
|
|
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
|
|
3825
5125
|
})) {
|
|
3826
5126
|
if (event.type === "finish" && event.truncated) {
|
|
3827
5127
|
yield {
|
|
@@ -3844,6 +5144,14 @@ ${summary}`
|
|
|
3844
5144
|
}
|
|
3845
5145
|
return tools;
|
|
3846
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
|
+
}
|
|
3847
5155
|
async runEventHooks(event, toolName, toolInput) {
|
|
3848
5156
|
const empty = { blocked: false, warnings: [] };
|
|
3849
5157
|
if (this.opts.config.hooks.length === 0) return empty;
|
|
@@ -3860,6 +5168,47 @@ ${summary}`
|
|
|
3860
5168
|
return empty;
|
|
3861
5169
|
}
|
|
3862
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
|
+
}
|
|
3863
5212
|
// opencode's doom_loop guard: returns a refusal once the identical tool
|
|
3864
5213
|
// call (name + stable-serialized args) has repeated config.doomLoopThreshold
|
|
3865
5214
|
// times in a row; 0 disables. Any different signature resets the streak.
|
|
@@ -3956,17 +5305,25 @@ export {
|
|
|
3956
5305
|
compactMessages,
|
|
3957
5306
|
WORKING_DIFF_MAX_LINES,
|
|
3958
5307
|
isGitRepo,
|
|
3959
|
-
|
|
5308
|
+
getGitSummaryCached,
|
|
3960
5309
|
collectCommitContext,
|
|
3961
5310
|
buildCommitPrompt,
|
|
3962
5311
|
collectWorkingDiff,
|
|
3963
5312
|
MAX_IMAGE_DIMENSION,
|
|
3964
|
-
probeImageDimensions,
|
|
3965
5313
|
reconcileToolCalls,
|
|
5314
|
+
wrapFetchWithActivity,
|
|
5315
|
+
starHome,
|
|
5316
|
+
globalConfigPath,
|
|
5317
|
+
sessionsDir,
|
|
5318
|
+
userMemoryPath,
|
|
5319
|
+
envFilePath,
|
|
5320
|
+
projectConfigPath,
|
|
3966
5321
|
isSensitivePath,
|
|
3967
5322
|
isAllowedByRules,
|
|
3968
5323
|
buildAllowRule,
|
|
3969
5324
|
isDangerousCommand,
|
|
5325
|
+
removeTreeRepo,
|
|
5326
|
+
pruneTreeReposExcept,
|
|
3970
5327
|
diffTreeNames,
|
|
3971
5328
|
listSnapshots,
|
|
3972
5329
|
clearSnapshots,
|
|
@@ -3979,6 +5336,7 @@ export {
|
|
|
3979
5336
|
bashTool,
|
|
3980
5337
|
SKIP_DIRS,
|
|
3981
5338
|
createIgnorePredicate,
|
|
5339
|
+
downsampleImageIfNeeded,
|
|
3982
5340
|
formatTaskList,
|
|
3983
5341
|
formatTaskStarted,
|
|
3984
5342
|
formatTaskFinished,
|
|
@@ -3993,4 +5351,4 @@ export {
|
|
|
3993
5351
|
PLAN_MODE_PROMPT,
|
|
3994
5352
|
AgentLoop
|
|
3995
5353
|
};
|
|
3996
|
-
//# sourceMappingURL=chunk-
|
|
5354
|
+
//# sourceMappingURL=chunk-OOOW7FC4.js.map
|