@cydm/pie 1.0.5 → 1.0.6

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.
@@ -0,0 +1,3657 @@
1
+ import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
+ import {
3
+ Agent,
4
+ FileSystemGateway,
5
+ detectPlatform,
6
+ getFileSystem,
7
+ getPlatformConfig,
8
+ streamSimple
9
+ } from "./chunk-MWFBYJOI.js";
10
+ import {
11
+ Type
12
+ } from "./chunk-RID3574D.js";
13
+ import {
14
+ __require
15
+ } from "./chunk-TG2EQLX2.js";
16
+
17
+ // ../../packages/shared-headless-capabilities/src/capability.ts
18
+ function defineSharedCapability(definition) {
19
+ return definition;
20
+ }
21
+ function collectSharedCapabilityPromptAdditions(definitions, activeToolNames) {
22
+ const activeTools = activeToolNames ? new Set(activeToolNames) : null;
23
+ const additions = /* @__PURE__ */ new Set();
24
+ for (const definition of definitions) {
25
+ if (!definition.systemPromptAdditions?.length) continue;
26
+ if (activeTools && definition.tools?.length) {
27
+ const hasActiveTool = definition.tools.some((tool) => {
28
+ return typeof tool?.name === "string" && activeTools.has(tool.name);
29
+ });
30
+ if (!hasActiveTool) continue;
31
+ }
32
+ for (const addition of definition.systemPromptAdditions) {
33
+ const trimmed = addition.trim();
34
+ if (trimmed) additions.add(trimmed);
35
+ }
36
+ }
37
+ return [...additions];
38
+ }
39
+
40
+ // ../../packages/shared-headless-capabilities/src/plan.ts
41
+ var PLAN_MODE_TOOL_NAMES = ["read", "bash", "grep", "find", "ls", "ask_user_multi", "manage_todo_list"];
42
+ var SAFE_PREFIXES = [
43
+ "cat ",
44
+ "ls ",
45
+ "find ",
46
+ "grep ",
47
+ "head ",
48
+ "tail ",
49
+ "echo ",
50
+ "pwd",
51
+ "cd ",
52
+ "which ",
53
+ "git status",
54
+ "git log",
55
+ "git diff",
56
+ "git show",
57
+ "git branch -a",
58
+ "git remote -v",
59
+ "git config --list",
60
+ "npm list",
61
+ "npm view"
62
+ ];
63
+ var DANGEROUS_PATTERNS = [
64
+ /\brm\s+-[rf]*/i,
65
+ />[\s]*\/dev\//,
66
+ /\|.*\brm\b/,
67
+ /git\s+push/i,
68
+ /git\s+reset/i,
69
+ /git\s+checkout\s+-/,
70
+ /git\s+revert/i,
71
+ /npm\s+publish/i,
72
+ /npm\s+uninstall/i,
73
+ /npm\s+rm/i
74
+ ];
75
+ function extractPlanTodoItems(text) {
76
+ const items = [];
77
+ const planMatch = text.match(/Plan:\s*\n?([\s\S]*?)(?=\n\n|\n[A-Z]|$)/i);
78
+ if (!planMatch) return items;
79
+ const lines = planMatch[1].split("\n");
80
+ for (const line of lines) {
81
+ const trimmed = line.trim();
82
+ const match = trimmed.match(/^(?:\d+[.):\s]+|[\-*]\s+)(.+)$/);
83
+ if (!match) continue;
84
+ items.push({
85
+ step: items.length + 1,
86
+ text: match[1].trim(),
87
+ completed: false
88
+ });
89
+ }
90
+ return items;
91
+ }
92
+ function isPlanModeSafeCommand(command) {
93
+ const trimmed = command.trim();
94
+ for (const pattern of DANGEROUS_PATTERNS) {
95
+ if (pattern.test(trimmed)) {
96
+ return false;
97
+ }
98
+ }
99
+ for (const prefix of SAFE_PREFIXES) {
100
+ if (trimmed.startsWith(prefix)) {
101
+ return true;
102
+ }
103
+ }
104
+ if (/^[a-z]+$/.test(trimmed)) {
105
+ return ["pwd", "ls", "clear", "whoami"].includes(trimmed);
106
+ }
107
+ return false;
108
+ }
109
+ function extractDoneSteps(text) {
110
+ const steps = [];
111
+ const matches = text.matchAll(/\[DONE:\s*(\d+)\]/gi);
112
+ for (const match of matches) {
113
+ const step = Number.parseInt(match[1], 10);
114
+ if (Number.isFinite(step)) {
115
+ steps.push(step);
116
+ }
117
+ }
118
+ return steps;
119
+ }
120
+ function markCompletedPlanSteps(text, todoItems) {
121
+ let markedCount = 0;
122
+ for (const stepNum of extractDoneSteps(text)) {
123
+ const item = todoItems.find((todo) => todo.step === stepNum);
124
+ if (item && !item.completed) {
125
+ item.completed = true;
126
+ markedCount++;
127
+ }
128
+ }
129
+ return markedCount;
130
+ }
131
+ function createPlanCapability() {
132
+ return defineSharedCapability({
133
+ id: "plan",
134
+ description: "Shared headless plan/execution capability with plan extraction, read-only filtering, and execution progress tracking helpers.",
135
+ commands: [{
136
+ id: "tools.plan",
137
+ name: "plan",
138
+ path: ["tools", "plan"],
139
+ description: "Toggle plan mode and coordinate plan execution."
140
+ }],
141
+ extensions: [{ name: "plan-mode", description: "Provides plan-mode event handling and execution state tracking." }],
142
+ requiredInteractions: ["notify", "select_one", "multiline_input"]
143
+ });
144
+ }
145
+
146
+ // ../../packages/shared-headless-capabilities/src/read-skill.ts
147
+ var ReadSkillParamsSchema = Type.Object({
148
+ name: Type.String({ description: "Exact skill name from the available_skills list." })
149
+ });
150
+ function createReadSkillCapability(deps) {
151
+ async function resolveSkill(name) {
152
+ if (deps.resolveSkill) {
153
+ return deps.resolveSkill(name);
154
+ }
155
+ const skill = deps.skillLoader?.getSkill?.(name) ?? deps.skillLoader?.loadSkill?.(name);
156
+ if (!skill) {
157
+ return null;
158
+ }
159
+ return {
160
+ name: skill.name,
161
+ source: skill.filePath || "skill-registry",
162
+ content: skill.prompt
163
+ };
164
+ }
165
+ const tool = {
166
+ name: "read_skill",
167
+ label: "read_skill",
168
+ description: "Load the full contents of a builtin or project skill by exact name.",
169
+ parameters: ReadSkillParamsSchema,
170
+ async execute(toolCallIdOrArgs, maybeArgs) {
171
+ const args = typeof toolCallIdOrArgs === "string" ? maybeArgs : toolCallIdOrArgs;
172
+ const skillName = String(args?.name || "").trim();
173
+ const skill = await resolveSkill(skillName);
174
+ if (!skill?.content) {
175
+ return {
176
+ content: [{ type: "text", text: `Skill not found: ${skillName || "(empty)"}` }],
177
+ details: { found: false, name: skillName || void 0 },
178
+ isError: true
179
+ };
180
+ }
181
+ return {
182
+ content: [{
183
+ type: "text",
184
+ text: `# ${skill.name}
185
+ source: ${skill.source}
186
+
187
+ ${skill.content}`
188
+ }],
189
+ details: { found: true, name: skill.name, source: skill.source }
190
+ };
191
+ }
192
+ };
193
+ return {
194
+ ...defineSharedCapability({
195
+ id: "read-skill",
196
+ description: "Shared capability for loading full skill contents through a host-provided skill resolver.",
197
+ tools: [tool]
198
+ }),
199
+ tool
200
+ };
201
+ }
202
+
203
+ // ../../packages/shared-headless-capabilities/src/subagent.ts
204
+ function escapeXml(str) {
205
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
206
+ }
207
+ function formatSkillsForPrompt(skills) {
208
+ if (skills.length === 0) return "";
209
+ const lines = [
210
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
211
+ "Use the read tool to load a skill's file when the task matches its description.",
212
+ "",
213
+ "<available_skills>"
214
+ ];
215
+ for (const skill of skills) {
216
+ lines.push(" <skill>");
217
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
218
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
219
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
220
+ lines.push(" </skill>");
221
+ }
222
+ lines.push("</available_skills>");
223
+ return lines.join("\n");
224
+ }
225
+ function collectLastAssistantText(messages) {
226
+ for (let i = messages.length - 1; i >= 0; i--) {
227
+ const message = messages[i];
228
+ if (message?.role !== "assistant") {
229
+ continue;
230
+ }
231
+ if (Array.isArray(message.content)) {
232
+ const textContent = message.content.filter((content) => content?.type === "text" && typeof content.text === "string").map((content) => content.text).join("\n");
233
+ if (textContent.trim()) {
234
+ return textContent;
235
+ }
236
+ }
237
+ if (typeof message.content === "string" && message.content.trim()) {
238
+ return message.content;
239
+ }
240
+ }
241
+ return "";
242
+ }
243
+ function createAgentInstance(deps, systemPrompt, tools) {
244
+ if (deps.createAgent) {
245
+ return deps.createAgent({
246
+ systemPrompt,
247
+ model: deps.model,
248
+ tools,
249
+ apiKey: deps.apiKey
250
+ });
251
+ }
252
+ return new Agent({
253
+ initialState: {
254
+ systemPrompt,
255
+ model: deps.model,
256
+ tools,
257
+ thinkingLevel: "off"
258
+ },
259
+ apiKey: deps.apiKey
260
+ });
261
+ }
262
+ async function runSubagentWithProgress(deps, task, reportProgress) {
263
+ const startTime = Date.now();
264
+ task.status = "running";
265
+ deps.debugLog?.("run", "runSubagentWithProgress START", { taskName: task.name, yoloMode: deps.yoloMode });
266
+ try {
267
+ reportProgress({ message: `[${task.name}] \u{1F680} Starting...`, increment: 5 });
268
+ const tools = deps.createRuntimeTools();
269
+ const parentToolDefs = deps.getParentTools();
270
+ deps.log(`[Subagent ${task.name}] Sandbox: ${deps.yoloMode ? "disabled" : "enabled"}`);
271
+ deps.log(`[Subagent ${task.name}] Inheriting ${parentToolDefs.length} tools from parent`);
272
+ deps.debugLog?.("run", "getAllTools called", { toolCount: parentToolDefs.length });
273
+ const systemPrompt = `You are a specialized subagent focused on completing one specific task.
274
+
275
+ Guidelines:
276
+ - Work independently using available tools
277
+ - Focus only on the assigned task
278
+ - Return a structured summary when done
279
+ - Be concise - you have limited turns${formatSkillsForPrompt(deps.skills ?? [])}`;
280
+ deps.debugLog?.("agent", "Creating Agent", { toolCount: tools.length, hasApiKey: !!deps.apiKey });
281
+ const subagent = createAgentInstance(deps, systemPrompt, tools);
282
+ const unsubscribe = subagent.subscribe((event) => {
283
+ if (event.type === "tool_execution_start") {
284
+ reportProgress({ message: `[${task.name}] \u{1F527} Using ${event.toolName}...` });
285
+ }
286
+ });
287
+ let prompt = task.prompt;
288
+ if (!prompt.includes("return") && !prompt.includes("summary")) {
289
+ prompt += "\n\nPlease provide a concise summary of your findings.";
290
+ }
291
+ reportProgress({ message: `[${task.name}] \u{1F4AD} Thinking...`, increment: 10 });
292
+ reportProgress({ message: `[${task.name}] Sending prompt to AI...`, increment: 15 });
293
+ deps.debugLog?.("agent", "Calling prompt", { promptLength: prompt.length });
294
+ await subagent.prompt(prompt);
295
+ deps.debugLog?.("agent", "prompt completed");
296
+ reportProgress({ message: `[${task.name}] Collecting response...`, increment: 20 });
297
+ const messages = subagent.state.messages;
298
+ deps.log(`[Subagent ${task.name}] Total messages: ${messages.length}`);
299
+ const response = collectLastAssistantText(messages) || "[No response]";
300
+ unsubscribe();
301
+ const durationMs = Date.now() - startTime;
302
+ const toolCalls = messages.filter(
303
+ (message) => message.role === "assistant" && Array.isArray(message.content) && message.content.some((content) => content.type === "toolCall")
304
+ ).length;
305
+ task.status = "completed";
306
+ task.result = response;
307
+ task.details = {
308
+ turns: Math.floor(messages.length / 2),
309
+ toolCalls,
310
+ durationMs
311
+ };
312
+ task.completedAt = Date.now();
313
+ reportProgress({ message: `[${task.name}] \u2705 Completed (${durationMs}ms, ${toolCalls} tools)`, increment: 100 });
314
+ } catch (error) {
315
+ const errorMessage = error instanceof Error ? error.message : String(error);
316
+ const stackTrace = error instanceof Error ? error.stack : void 0;
317
+ deps.debugLog?.("run", "EXCEPTION caught", {
318
+ error: errorMessage,
319
+ stack: stackTrace,
320
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
321
+ taskName: task.name
322
+ });
323
+ task.status = "failed";
324
+ task.error = stackTrace ? `${errorMessage}
325
+
326
+ Stack:
327
+ ${stackTrace}` : errorMessage;
328
+ task.completedAt = Date.now();
329
+ reportProgress({ message: `[${task.name}] \u274C Failed: ${errorMessage.slice(0, 100)}`, increment: 100 });
330
+ }
331
+ }
332
+ function buildParallelResult(tasks, totalDuration) {
333
+ let resultText = `\u{1F4CA} Parallel Subagent Results (${tasks.length} tasks, ${totalDuration}ms)
334
+ `;
335
+ resultText += `${"=".repeat(60)}
336
+
337
+ `;
338
+ for (const task of tasks) {
339
+ const statusEmoji = task.status === "completed" ? "\u2705" : "\u274C";
340
+ resultText += `${statusEmoji} **${task.name}**`;
341
+ if (task.details) {
342
+ resultText += ` (${task.details.durationMs}ms, ${task.details.toolCalls} tools)`;
343
+ }
344
+ resultText += "\n";
345
+ if (task.status === "completed" && task.result) {
346
+ resultText += `${task.result}
347
+ `;
348
+ } else if (task.status === "failed" && task.error) {
349
+ resultText += `Error: ${task.error}
350
+ `;
351
+ }
352
+ resultText += "\n";
353
+ }
354
+ const completed = tasks.filter((task) => task.status === "completed").length;
355
+ const failed = tasks.filter((task) => task.status === "failed").length;
356
+ resultText += `${"=".repeat(60)}
357
+ `;
358
+ resultText += `Summary: ${completed} completed, ${failed} failed, ${totalDuration}ms total
359
+ `;
360
+ return resultText;
361
+ }
362
+ function createSubagentCapability(deps) {
363
+ const spawnParallelTool = {
364
+ name: "spawn_subagents_parallel",
365
+ label: "Spawn Subagents (Parallel + Transparent)",
366
+ description: `Spawns multiple child AI agents that run IN PARALLEL with full transparency.
367
+
368
+ KEY FEATURES:
369
+ 1. All subagents run simultaneously using Promise.all
370
+ 2. Each subagent's progress is visible in the tool output (Ctrl+O to expand)
371
+ 3. Subagents can run host-provided runtime tools
372
+ 4. No context pollution - results are merged into a single response`,
373
+ parameters: Type.Object({
374
+ tasks: Type.Array(
375
+ Type.Object({
376
+ name: Type.String({ description: "Short name for this subagent (shown in progress updates)" }),
377
+ prompt: Type.String({ description: "The task/prompt for this subagent. Be specific." })
378
+ }),
379
+ { description: "Array of tasks to run in parallel", minItems: 1, maxItems: 10 }
380
+ ),
381
+ max_turns: Type.Optional(Type.Number({ description: "Max turns per subagent (default: 15)", default: 15 }))
382
+ }),
383
+ async execute(args, toolContext) {
384
+ if (!deps.apiKey) {
385
+ return {
386
+ content: [{ type: "text", text: "Error: No API key available for subagent execution." }],
387
+ isError: true
388
+ };
389
+ }
390
+ const taskDefs = args.tasks ?? [];
391
+ const tasks = taskDefs.map((def, index) => ({
392
+ id: `subagent-${Date.now()}-${index}`,
393
+ name: def.name || `Task-${index + 1}`,
394
+ status: "pending",
395
+ prompt: def.prompt,
396
+ createdAt: Date.now()
397
+ }));
398
+ toolContext.log?.(`Spawning ${tasks.length} parallel subagents: ${tasks.map((task) => task.name).join(", ")}`);
399
+ toolContext.reportProgress?.({ message: `\u{1F680} Launching ${tasks.length} parallel subagents...`, increment: 5 });
400
+ const startTime = Date.now();
401
+ await Promise.all(tasks.map((task) => runSubagentWithProgress(deps, task, toolContext.reportProgress ?? (() => void 0))));
402
+ const totalDuration = Date.now() - startTime;
403
+ const completed = tasks.filter((task) => task.status === "completed").length;
404
+ const failed = tasks.filter((task) => task.status === "failed").length;
405
+ deps.notify?.(`\u2705 ${completed}/${tasks.length} subagents completed (${totalDuration}ms)`, failed > 0 ? "warning" : "success");
406
+ return {
407
+ content: [{ type: "text", text: buildParallelResult(tasks, totalDuration) }],
408
+ details: {
409
+ tasks: tasks.map((task) => ({
410
+ name: task.name,
411
+ status: task.status,
412
+ details: task.details
413
+ })),
414
+ totalDuration,
415
+ completed,
416
+ failed
417
+ }
418
+ };
419
+ }
420
+ };
421
+ return {
422
+ ...defineSharedCapability({
423
+ id: "subagent",
424
+ description: "Shared headless subagent capability for spawning child agents in parallel using host-provided runtime tools.",
425
+ tools: [spawnParallelTool],
426
+ extensions: [{ name: "subagent", description: "Provides subagent task orchestration and result aggregation." }]
427
+ }),
428
+ tools: [spawnParallelTool]
429
+ };
430
+ }
431
+
432
+ // ../../packages/shared-headless-capabilities/src/todo.ts
433
+ var TodoItemSchema = Type.Object({
434
+ id: Type.Number({ description: "Unique identifier for the todo. Use sequential numbers starting from 1." }),
435
+ title: Type.String({ description: "Concise action-oriented todo label (3-7 words)." }),
436
+ description: Type.String({ description: "Detailed context or implementation notes." }),
437
+ status: Type.String({
438
+ description: "Progress state. Preferred values: not-started, in-progress, completed. Also accepts aliases like pending, todo, doing, done."
439
+ })
440
+ });
441
+ var ManageTodoListParamsSchema = Type.Object({
442
+ operation: Type.Union([Type.Literal("write"), Type.Literal("read")], {
443
+ description: "write: Replace the entire todo list. read: Return the current todo list."
444
+ }),
445
+ todoList: Type.Optional(
446
+ Type.Array(TodoItemSchema, {
447
+ description: "Required for write. Must include the full todo list with sequential ids starting from 1."
448
+ })
449
+ )
450
+ });
451
+ function cloneTodos(todos) {
452
+ return todos.map((todo) => ({ ...todo }));
453
+ }
454
+ function normalizeStatus(status) {
455
+ const value = String(status || "").trim().toLowerCase();
456
+ if (!value) return null;
457
+ if (value === "not-started" || value === "not_started" || value === "pending" || value === "todo" || value === "not started") {
458
+ return "not-started";
459
+ }
460
+ if (value === "in-progress" || value === "in_progress" || value === "in progress" || value === "doing" || value === "active" || value === "working") {
461
+ return "in-progress";
462
+ }
463
+ if (value === "completed" || value === "complete" || value === "done" || value === "finished") {
464
+ return "completed";
465
+ }
466
+ return null;
467
+ }
468
+ function normalizeTodos(todos) {
469
+ return todos.map((todo) => ({
470
+ ...todo,
471
+ status: normalizeStatus(todo.status) ?? todo.status
472
+ }));
473
+ }
474
+ function validateTodos(todos) {
475
+ const errors = [];
476
+ const validStatuses = /* @__PURE__ */ new Set(["not-started", "in-progress", "completed"]);
477
+ for (let index = 0; index < todos.length; index++) {
478
+ const item = todos[index];
479
+ const prefix = `Item ${index + 1}`;
480
+ if (typeof item?.id !== "number" || !Number.isInteger(item.id) || item.id < 1) {
481
+ errors.push(`${prefix}: 'id' must be a positive integer`);
482
+ }
483
+ if (item?.id !== index + 1) {
484
+ errors.push(`${prefix}: ids must be sequential starting from 1`);
485
+ }
486
+ if (typeof item?.title !== "string" || item.title.trim().length === 0) {
487
+ errors.push(`${prefix}: 'title' is required`);
488
+ }
489
+ if (typeof item?.description !== "string") {
490
+ errors.push(`${prefix}: 'description' must be a string`);
491
+ }
492
+ if (!validStatuses.has(item?.status)) {
493
+ errors.push(`${prefix}: 'status' must be one of: not-started, in-progress, completed`);
494
+ }
495
+ }
496
+ return errors;
497
+ }
498
+ function getTodoValidation(todos) {
499
+ const errors = validateTodos(todos);
500
+ return {
501
+ valid: errors.length === 0,
502
+ errors
503
+ };
504
+ }
505
+ function getTodoStats(todos) {
506
+ const total = todos.length;
507
+ const completed = todos.filter((todo) => todo.status === "completed").length;
508
+ const inProgress = todos.filter((todo) => todo.status === "in-progress").length;
509
+ return {
510
+ total,
511
+ completed,
512
+ inProgress,
513
+ notStarted: total - completed - inProgress
514
+ };
515
+ }
516
+ function buildTodoSuccessMessage(todos) {
517
+ const { completed, total } = getTodoStats(todos);
518
+ if (total > 0 && completed === total) {
519
+ return `Todos updated successfully. ${completed}/${total} completed. All todos are complete, so the list was cleared.`;
520
+ }
521
+ return `Todos updated successfully. ${completed}/${total} completed. Continue updating the list until all items are completed; it will clear automatically when done.`;
522
+ }
523
+ function executeManageTodoList(args, currentTodos) {
524
+ const safeCurrentTodos = cloneTodos(currentTodos);
525
+ if (args.operation === "read") {
526
+ return {
527
+ nextTodos: safeCurrentTodos,
528
+ result: {
529
+ content: [
530
+ {
531
+ type: "text",
532
+ text: safeCurrentTodos.length ? JSON.stringify(safeCurrentTodos, null, 2) : "No todos. Use write operation to create a todo list."
533
+ }
534
+ ],
535
+ details: { operation: "read", todos: safeCurrentTodos }
536
+ }
537
+ };
538
+ }
539
+ if (!Array.isArray(args.todoList)) {
540
+ return {
541
+ nextTodos: safeCurrentTodos,
542
+ result: {
543
+ content: [{ type: "text", text: "Error: todoList is required for write operation." }],
544
+ details: { operation: "write", todos: safeCurrentTodos, error: "todoList required" },
545
+ isError: true
546
+ }
547
+ };
548
+ }
549
+ const nextTodos = normalizeTodos(cloneTodos(args.todoList));
550
+ const errors = validateTodos(nextTodos);
551
+ if (errors.length > 0) {
552
+ return {
553
+ nextTodos: safeCurrentTodos,
554
+ result: {
555
+ content: [{ type: "text", text: `Validation failed:
556
+ ${errors.map((error) => ` - ${error}`).join("\n")}` }],
557
+ details: { operation: "write", todos: safeCurrentTodos, error: errors.join("; ") },
558
+ isError: true
559
+ }
560
+ };
561
+ }
562
+ const stats = getTodoStats(nextTodos);
563
+ const autoCleared = stats.total > 0 && stats.completed === stats.total;
564
+ const storedTodos = autoCleared ? [] : nextTodos;
565
+ return {
566
+ nextTodos: storedTodos,
567
+ result: {
568
+ content: [{ type: "text", text: buildTodoSuccessMessage(nextTodos) }],
569
+ details: {
570
+ operation: "write",
571
+ todos: cloneTodos(storedTodos),
572
+ autoCleared,
573
+ completedTodos: autoCleared ? cloneTodos(nextTodos) : void 0
574
+ }
575
+ }
576
+ };
577
+ }
578
+ function restoreTodoState(state) {
579
+ if (!Array.isArray(state)) {
580
+ return [];
581
+ }
582
+ const normalized = normalizeTodos(cloneTodos(state));
583
+ return validateTodos(normalized).length === 0 ? normalized : [];
584
+ }
585
+ function restoreTodosFromMessages(messages) {
586
+ let todos = [];
587
+ for (const message of messages || []) {
588
+ const toolResultMessage = message;
589
+ if (toolResultMessage.role !== "toolResult" || toolResultMessage.toolName !== "manage_todo_list") {
590
+ continue;
591
+ }
592
+ const details = toolResultMessage.details;
593
+ if (details?.todos) {
594
+ todos = restoreTodoState(details.todos);
595
+ }
596
+ }
597
+ return todos;
598
+ }
599
+ function createTodoStore(initialState) {
600
+ let todos = restoreTodoState(initialState);
601
+ return {
602
+ read() {
603
+ return cloneTodos(todos);
604
+ },
605
+ write(nextTodos) {
606
+ todos = restoreTodoState(nextTodos);
607
+ },
608
+ clear() {
609
+ todos = [];
610
+ },
611
+ restore(state) {
612
+ todos = restoreTodoState(state);
613
+ },
614
+ restoreFromMessages(messages) {
615
+ todos = restoreTodosFromMessages(messages);
616
+ },
617
+ getStats() {
618
+ return getTodoStats(todos);
619
+ },
620
+ validate(nextTodos) {
621
+ if (!Array.isArray(nextTodos)) {
622
+ return { valid: false, errors: ["todoList must be an array"] };
623
+ }
624
+ return getTodoValidation(normalizeTodos(cloneTodos(nextTodos)));
625
+ }
626
+ };
627
+ }
628
+ function createManageTodoCapability() {
629
+ const store = createTodoStore();
630
+ const tool = {
631
+ name: "manage_todo_list",
632
+ label: "manage_todo_list",
633
+ description: "Read or replace the shared todo checklist for the current session. Use this to actually create and update todo tracking when the user asks for a todo demonstration or progress checklist, not just to describe how todo tracking works.",
634
+ parameters: ManageTodoListParamsSchema,
635
+ async execute(args) {
636
+ const { nextTodos, result } = executeManageTodoList(args, store.read());
637
+ store.write(nextTodos);
638
+ return result;
639
+ }
640
+ };
641
+ return {
642
+ ...defineSharedCapability({
643
+ id: "todo",
644
+ description: "Shared headless todo/checklist capability.",
645
+ tools: [tool],
646
+ extensions: [{ name: "todo-state", description: "Provides capability-local todo state storage." }],
647
+ systemPromptAdditions: [
648
+ "When the user asks you to demonstrate or use todo tracking, call `manage_todo_list` instead of only describing how it works.",
649
+ "When you start using manage_todo_list for a task, keep the checklist updated whenever step status changes.",
650
+ "Maintain exactly one in-progress item while work is active, continue updating the checklist until every item is completed, and do not abandon a started checklist.",
651
+ "A todo demonstration is not complete until `manage_todo_list` has been updated through all steps and the list auto-clears.",
652
+ "manage_todo_list clears itself automatically when all items are completed, so drive the checklist to completion instead of leaving completed items behind."
653
+ ]
654
+ }),
655
+ tool,
656
+ createStore: createTodoStore
657
+ };
658
+ }
659
+
660
+ // ../../packages/agent-framework/src/session/compaction.ts
661
+ var COMPACT_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
662
+
663
+ Include:
664
+ - Current progress and key decisions made
665
+ - Important context, constraints, or user preferences
666
+ - What remains to be done (clear next steps)
667
+ - Any critical data, examples, or references needed to continue
668
+
669
+ Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
670
+ var SUMMARY_PREFIX = `Another language model started to solve this problem and produced a summary of its thinking process. You also have access to the state of the tools that were used by that language model. Use this to build on the work that has already been done and avoid duplicating work. Here is the summary produced by the other language model, use the information in this summary to assist with your own analysis:`;
671
+ var DEFAULT_COMPACTION_CONFIG = {
672
+ maxMessages: 100,
673
+ maxTokens: 8e3,
674
+ keepRecentUserMessages: 3,
675
+ maxRetainedUserTokens: 2e4,
676
+ autoCompact: true,
677
+ autoCompactTokenLimit: void 0,
678
+ compactPrompt: COMPACT_PROMPT,
679
+ summaryPrefix: SUMMARY_PREFIX
680
+ };
681
+ function estimateTokens(message) {
682
+ const text = getMessageText(message);
683
+ return Math.ceil(text.length / 4);
684
+ }
685
+ function calculateTotalTokens(messages) {
686
+ return messages.reduce((sum, m) => sum + estimateTokens(m), 0);
687
+ }
688
+ function isCompactSummaryMessage(message, summaryPrefix = SUMMARY_PREFIX) {
689
+ return message.startsWith(`${summaryPrefix}
690
+ `);
691
+ }
692
+ function formatCompactSummaryText(summaryText, summaryPrefix = SUMMARY_PREFIX) {
693
+ const compactSummary = summaryText.trim() || "(no summary available)";
694
+ return `${summaryPrefix}
695
+ ${compactSummary}`;
696
+ }
697
+ function collectRealUserMessages(messages, summaryPrefix = SUMMARY_PREFIX) {
698
+ return messages.filter((message) => message.role === "user").map((message) => getUserMessageText(message)).filter((text) => Boolean(text)).filter((text) => !isCompactSummaryMessage(text, summaryPrefix)).filter((text) => !isSyntheticUserEnvelope(text));
699
+ }
700
+ function findFirstKeptUserEntryId(entries, options = {}) {
701
+ const fullConfig = { ...DEFAULT_COMPACTION_CONFIG, ...options };
702
+ const keepCount = Math.max(0, fullConfig.keepRecentUserMessages);
703
+ if (keepCount <= 0 || entries.length === 0) {
704
+ return null;
705
+ }
706
+ const userEntries = entries.filter((entry) => entry.type === "message" && !!entry.message && entry.message.role === "user").map((entry) => ({ entryId: entry.id, text: getUserMessageText(entry.message) })).filter((entry) => Boolean(entry.text)).filter((entry) => !isCompactSummaryMessage(entry.text, fullConfig.summaryPrefix ?? SUMMARY_PREFIX)).filter((entry) => !isSyntheticUserEnvelope(entry.text));
707
+ if (userEntries.length === 0) {
708
+ return null;
709
+ }
710
+ const recentEntries = userEntries.slice(-keepCount);
711
+ const retainedEntries = applyUserEntryTokenBudget(recentEntries, fullConfig.maxRetainedUserTokens);
712
+ return retainedEntries[0]?.entryId ?? null;
713
+ }
714
+ async function createCompactionSummary(messages, options = {}) {
715
+ const { model, apiKey, systemPrompt, compactPrompt, signal } = options;
716
+ if (!model) {
717
+ return createSimpleSummary(messages);
718
+ }
719
+ try {
720
+ const response = await streamSimple(
721
+ model,
722
+ {
723
+ systemPrompt,
724
+ messages: [
725
+ ...messages.filter(isCompactionCompatibleMessage),
726
+ {
727
+ role: "user",
728
+ content: [{ type: "text", text: compactPrompt ?? COMPACT_PROMPT }],
729
+ timestamp: Date.now()
730
+ }
731
+ ]
732
+ },
733
+ {
734
+ apiKey,
735
+ signal
736
+ }
737
+ ).result();
738
+ return getAssistantText(response) || createSimpleSummary(messages);
739
+ } catch (error) {
740
+ console.warn("[Compaction] Failed to create Codex-style summary:", error);
741
+ return createSimpleSummary(messages);
742
+ }
743
+ }
744
+ function getMessageText(message) {
745
+ if (message.role === "user") {
746
+ return getUserMessageText(message);
747
+ }
748
+ if (message.role === "assistant") {
749
+ return message.content.filter((c) => c.type === "text").map((c) => c.text).join(" ");
750
+ }
751
+ if (message.role === "toolResult") {
752
+ return message.content.filter((c) => c.type === "text").map((c) => c.text).join(" ");
753
+ }
754
+ return "";
755
+ }
756
+ function getUserMessageText(message) {
757
+ if (typeof message.content === "string") {
758
+ return message.content;
759
+ }
760
+ return message.content.filter((c) => c.type === "text").map((c) => c.text).join(" ");
761
+ }
762
+ function getAssistantText(message) {
763
+ return message.content.filter((c) => c.type === "text").map((c) => c.text).join("").trim();
764
+ }
765
+ function createSimpleSummary(messages) {
766
+ const userMessages = collectRealUserMessages(messages);
767
+ const lastUserMessage = userMessages.at(-1);
768
+ const assistantCount = messages.filter((m) => m.role === "assistant").length;
769
+ const toolCount = messages.filter((m) => m.role === "toolResult").length;
770
+ return [
771
+ `- Conversation turns: ${messages.length}`,
772
+ `- Assistant responses: ${assistantCount}`,
773
+ `- Tool results: ${toolCount}`,
774
+ lastUserMessage ? `- Latest user request: ${lastUserMessage}` : ""
775
+ ].filter(Boolean).join("\n");
776
+ }
777
+ function applyUserEntryTokenBudget(entries, maxTokens) {
778
+ if (maxTokens <= 0 || entries.length === 0) {
779
+ return [];
780
+ }
781
+ const selected = [];
782
+ let remaining = maxTokens;
783
+ for (let i = entries.length - 1; i >= 0; i--) {
784
+ const entry = entries[i];
785
+ const messageTokens = approximateTextTokens(entry.text);
786
+ if (messageTokens <= remaining) {
787
+ selected.unshift(entry);
788
+ remaining -= messageTokens;
789
+ continue;
790
+ }
791
+ if (selected.length === 0) {
792
+ selected.unshift({
793
+ entryId: entry.entryId,
794
+ text: truncateTextToTokenBudget(entry.text, remaining)
795
+ });
796
+ }
797
+ break;
798
+ }
799
+ return selected;
800
+ }
801
+ function truncateTextToTokenBudget(text, maxTokens) {
802
+ if (maxTokens <= 0) {
803
+ return "[truncated]";
804
+ }
805
+ const maxChars = Math.max(16, maxTokens * 4);
806
+ if (text.length <= maxChars) {
807
+ return text;
808
+ }
809
+ return `${text.slice(0, Math.max(0, maxChars - 15)).trimEnd()}... [truncated]`;
810
+ }
811
+ function approximateTextTokens(text) {
812
+ return Math.ceil(text.length / 4);
813
+ }
814
+ function isSyntheticUserEnvelope(text) {
815
+ const trimmed = text.trimStart();
816
+ return trimmed.startsWith("<environment_context>") || trimmed.startsWith("<turn_aborted>") || trimmed.startsWith("# AGENTS.md instructions");
817
+ }
818
+ function isCompactionCompatibleMessage(message) {
819
+ if (message.role === "user") return true;
820
+ if (message.role === "assistant") return true;
821
+ if (message.role === "toolResult") return true;
822
+ return false;
823
+ }
824
+
825
+ // ../../packages/agent-framework/src/session/types.ts
826
+ function generateEntryId() {
827
+ const timestamp = Date.now().toString(36);
828
+ const random = Math.random().toString(36).substring(2, 8);
829
+ return `entry-${timestamp}-${random}`;
830
+ }
831
+ function buildTree(entries, activeEntryId) {
832
+ const byId = /* @__PURE__ */ new Map();
833
+ const childrenMap = /* @__PURE__ */ new Map();
834
+ for (const entry of entries) {
835
+ byId.set(entry.id, entry);
836
+ if (entry.parentId) {
837
+ const siblings = childrenMap.get(entry.parentId) ?? [];
838
+ siblings.push(entry.id);
839
+ childrenMap.set(entry.parentId, siblings);
840
+ }
841
+ }
842
+ const rootIds = entries.filter((e) => e.parentId === null).map((e) => e.id);
843
+ const activePath = /* @__PURE__ */ new Set();
844
+ if (activeEntryId) {
845
+ let current = activeEntryId;
846
+ while (current) {
847
+ activePath.add(current);
848
+ const entry = byId.get(current);
849
+ current = entry?.parentId ?? null;
850
+ }
851
+ }
852
+ function buildNode(entryId, depth) {
853
+ const entry = byId.get(entryId);
854
+ const childIds = childrenMap.get(entryId) ?? [];
855
+ return {
856
+ id: entryId,
857
+ parentId: entry.parentId,
858
+ entry,
859
+ children: childIds.map((id) => buildNode(id, depth + 1)),
860
+ depth,
861
+ isActive: activePath.has(entryId)
862
+ };
863
+ }
864
+ return rootIds.map((id) => buildNode(id, 0));
865
+ }
866
+ function flattenTreeToMessages(entries, activeEntryId) {
867
+ const byId = new Map(entries.map((e) => [e.id, e]));
868
+ const messages = [];
869
+ const path = [];
870
+ let current = activeEntryId;
871
+ while (current) {
872
+ path.unshift(current);
873
+ const entry = byId.get(current);
874
+ current = entry?.parentId ?? null;
875
+ }
876
+ const pathEntries = path.map((entryId) => byId.get(entryId)).filter((entry) => Boolean(entry));
877
+ const latestCompactionIndex = findLatestCompactionIndex(pathEntries);
878
+ if (latestCompactionIndex === -1) {
879
+ for (const entry of pathEntries) {
880
+ if (entry.type === "message" && entry.message) {
881
+ messages.push(entry.message);
882
+ }
883
+ }
884
+ return messages;
885
+ }
886
+ const compactionEntry = pathEntries[latestCompactionIndex];
887
+ const compactionData = compactionEntry.compactionData;
888
+ if (!compactionData) {
889
+ for (const entry of pathEntries) {
890
+ if (entry.type === "message" && entry.message) {
891
+ messages.push(entry.message);
892
+ }
893
+ }
894
+ return messages;
895
+ }
896
+ messages.push(createCompactionSummaryMessage(compactionData.summary, compactionEntry.timestamp));
897
+ let foundFirstKept = false;
898
+ for (let i = 0; i < latestCompactionIndex; i++) {
899
+ const entry = pathEntries[i];
900
+ if (entry.id === compactionData.firstKeptEntryId) {
901
+ foundFirstKept = true;
902
+ }
903
+ if (foundFirstKept && entry.type === "message" && entry.message) {
904
+ messages.push(entry.message);
905
+ }
906
+ }
907
+ for (let i = latestCompactionIndex + 1; i < pathEntries.length; i++) {
908
+ const entry = pathEntries[i];
909
+ if (entry.type === "message" && entry.message) {
910
+ messages.push(entry.message);
911
+ }
912
+ }
913
+ return messages;
914
+ }
915
+ function findLatestCompactionIndex(entries) {
916
+ for (let i = entries.length - 1; i >= 0; i--) {
917
+ if (entries[i].type === "compaction" && entries[i].compactionData) {
918
+ return i;
919
+ }
920
+ }
921
+ return -1;
922
+ }
923
+ function createCompactionSummaryMessage(summary, timestamp) {
924
+ return {
925
+ role: "user",
926
+ content: [{ type: "text", text: formatCompactSummaryText(summary) }],
927
+ timestamp
928
+ };
929
+ }
930
+
931
+ // ../../packages/agent-framework/src/session/store.ts
932
+ var SESSION_VERSION = 2;
933
+ function isSessionEntry(value) {
934
+ return !!value && typeof value === "object" && "id" in value && "type" in value;
935
+ }
936
+ function normalizeEntriesInput(entriesOrMessages) {
937
+ if (entriesOrMessages.every(isSessionEntry)) {
938
+ return entriesOrMessages;
939
+ }
940
+ const entries = [];
941
+ let parentId = null;
942
+ for (const message of entriesOrMessages) {
943
+ const entry = {
944
+ id: generateEntryId(),
945
+ parentId,
946
+ type: "message",
947
+ timestamp: Date.now(),
948
+ message
949
+ };
950
+ entries.push(entry);
951
+ parentId = entry.id;
952
+ }
953
+ return entries;
954
+ }
955
+ var LEGACY_VERSION = 1;
956
+ function getDefaultSessionsDir() {
957
+ return getPlatformConfig().sessionsPath;
958
+ }
959
+ function migrateV1ToV2(data) {
960
+ console.log(`[SessionStore] Migrating session ${data.metadata.id} from v1 to v2`);
961
+ const messages = data.messages ?? [];
962
+ const entries = [];
963
+ let parentId = null;
964
+ for (const message of messages) {
965
+ const entry = {
966
+ id: generateEntryId(),
967
+ parentId,
968
+ type: "message",
969
+ timestamp: Date.now(),
970
+ message
971
+ };
972
+ entries.push(entry);
973
+ parentId = entry.id;
974
+ }
975
+ const activeEntryId = entries.length > 0 ? entries[entries.length - 1].id : null;
976
+ const rootEntryId = entries.length > 0 ? entries[0].id : null;
977
+ return {
978
+ version: SESSION_VERSION,
979
+ metadata: {
980
+ ...data.metadata,
981
+ entryCount: entries.length,
982
+ messageCount: messages.length,
983
+ activeEntryId,
984
+ rootEntryId
985
+ },
986
+ entries,
987
+ messages
988
+ // Keep for backward compatibility
989
+ };
990
+ }
991
+ function writeFileAtomic(gateway, filePath, content) {
992
+ const tempPath = filePath + ".tmp";
993
+ gateway.writeFile(tempPath, content, "utf-8");
994
+ try {
995
+ const req = globalThis.require;
996
+ if (typeof req === "function") {
997
+ req("fs").renameSync(tempPath, filePath);
998
+ } else {
999
+ gateway.writeFile(filePath, content, "utf-8");
1000
+ try {
1001
+ gateway.deleteFile(tempPath);
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ } catch {
1006
+ gateway.writeFile(filePath, content, "utf-8");
1007
+ try {
1008
+ gateway.deleteFile(tempPath);
1009
+ } catch {
1010
+ }
1011
+ }
1012
+ }
1013
+ var FileSessionStore = class {
1014
+ sessionsDir;
1015
+ gateway;
1016
+ constructor(sessionsDir) {
1017
+ this.sessionsDir = sessionsDir || getDefaultSessionsDir();
1018
+ this.gateway = new FileSystemGateway({
1019
+ allowedRoots: [this.sessionsDir, getPlatformConfig().dataPath]
1020
+ });
1021
+ this.ensureDirectory();
1022
+ }
1023
+ /**
1024
+ * Ensure the sessions directory exists
1025
+ */
1026
+ ensureDirectory() {
1027
+ try {
1028
+ if (!this.gateway.exists(this.sessionsDir)) {
1029
+ this.gateway.mkdir(this.sessionsDir, { recursive: true });
1030
+ }
1031
+ } catch (e) {
1032
+ console.warn(`[FileSessionStore] Could not create sessions directory: ${e}`);
1033
+ }
1034
+ }
1035
+ /**
1036
+ * Get the full path for a session file
1037
+ */
1038
+ getSessionPath(sessionId) {
1039
+ const safeId = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
1040
+ return this.gateway.join(this.sessionsDir, `${safeId}.json`);
1041
+ }
1042
+ /**
1043
+ * Save a session to disk (v2 format)
1044
+ */
1045
+ async save(sessionId, entries, metadata) {
1046
+ const normalizedEntries = normalizeEntriesInput(entries);
1047
+ const now = Date.now();
1048
+ const existing = await this.load(sessionId);
1049
+ const messageCount = normalizedEntries.filter((e) => e.type === "message" && e.message).length;
1050
+ const rootEntry = normalizedEntries.find((e) => e.parentId === null);
1051
+ const activeEntryId = metadata?.activeEntryId ?? existing?.metadata.activeEntryId ?? normalizedEntries[normalizedEntries.length - 1]?.id ?? null;
1052
+ const sessionData = {
1053
+ version: SESSION_VERSION,
1054
+ metadata: {
1055
+ id: sessionId,
1056
+ name: metadata?.name ?? existing?.metadata.name ?? `Session ${sessionId.slice(0, 8)}`,
1057
+ createdAt: existing?.metadata.createdAt ?? now,
1058
+ updatedAt: now,
1059
+ entryCount: normalizedEntries.length,
1060
+ messageCount,
1061
+ activeEntryId: activeEntryId ?? rootEntry?.id ?? null,
1062
+ rootEntryId: rootEntry?.id ?? null,
1063
+ tags: metadata?.tags ?? existing?.metadata.tags,
1064
+ ...metadata
1065
+ },
1066
+ entries: normalizedEntries,
1067
+ messages: flattenTreeToMessages(normalizedEntries, activeEntryId ?? null)
1068
+ };
1069
+ const json = JSON.stringify(sessionData, null, 2);
1070
+ const filePath = this.getSessionPath(sessionId);
1071
+ writeFileAtomic(this.gateway, filePath, json);
1072
+ }
1073
+ /**
1074
+ * Legacy save method for backward compatibility
1075
+ * @deprecated Use save() with entries instead
1076
+ */
1077
+ async saveLegacy(sessionId, messages, metadata) {
1078
+ console.warn("[FileSessionStore] Using deprecated saveLegacy() - consider migrating to v2");
1079
+ const entries = [];
1080
+ let parentId = null;
1081
+ for (const message of messages) {
1082
+ const entry = {
1083
+ id: generateEntryId(),
1084
+ parentId,
1085
+ type: "message",
1086
+ timestamp: Date.now(),
1087
+ message
1088
+ };
1089
+ entries.push(entry);
1090
+ parentId = entry.id;
1091
+ }
1092
+ await this.save(sessionId, entries, metadata);
1093
+ }
1094
+ /**
1095
+ * Load a session from disk
1096
+ */
1097
+ async load(sessionId) {
1098
+ const filePath = this.getSessionPath(sessionId);
1099
+ if (!this.gateway.exists(filePath)) {
1100
+ return null;
1101
+ }
1102
+ try {
1103
+ const json = this.gateway.readFile(filePath, "utf-8");
1104
+ if (!json || json.trim().length === 0) {
1105
+ console.warn(`[SessionStore] Empty session file: ${sessionId}, removing`);
1106
+ this.gateway.deleteFile(filePath);
1107
+ return null;
1108
+ }
1109
+ const data = JSON.parse(json);
1110
+ if (data.version === LEGACY_VERSION || !data.version) {
1111
+ const migrated = migrateV1ToV2(data);
1112
+ const migratedJson = JSON.stringify(migrated, null, 2);
1113
+ writeFileAtomic(this.gateway, filePath, migratedJson);
1114
+ return migrated;
1115
+ }
1116
+ if (data.version !== SESSION_VERSION) {
1117
+ console.warn(
1118
+ `[SessionStore] Session version mismatch: ${data.version} vs ${SESSION_VERSION}`
1119
+ );
1120
+ }
1121
+ if (!data.entries) {
1122
+ data.entries = [];
1123
+ }
1124
+ return data;
1125
+ } catch (e) {
1126
+ console.error(`[SessionStore] Failed to load session: ${sessionId}`, e);
1127
+ return null;
1128
+ }
1129
+ }
1130
+ /**
1131
+ * Delete a session
1132
+ */
1133
+ async delete(sessionId) {
1134
+ const filePath = this.getSessionPath(sessionId);
1135
+ if (!this.gateway.exists(filePath)) {
1136
+ return false;
1137
+ }
1138
+ try {
1139
+ this.gateway.deleteFile(filePath);
1140
+ return true;
1141
+ } catch (e) {
1142
+ console.error(`[SessionStore] Failed to delete session: ${sessionId}`, e);
1143
+ return false;
1144
+ }
1145
+ }
1146
+ /**
1147
+ * Check if a session exists
1148
+ */
1149
+ async exists(sessionId) {
1150
+ const filePath = this.getSessionPath(sessionId);
1151
+ return this.gateway.exists(filePath);
1152
+ }
1153
+ /**
1154
+ * List all session IDs
1155
+ */
1156
+ async list() {
1157
+ if (!this.gateway.exists(this.sessionsDir)) {
1158
+ return [];
1159
+ }
1160
+ const files = this.gateway.readdir(this.sessionsDir);
1161
+ return files.filter((f) => f.endsWith(".json")).filter((f) => !f.endsWith("-files.json")).map((f) => f.replace(/\.json$/i, ""));
1162
+ }
1163
+ /**
1164
+ * List all session metadata
1165
+ */
1166
+ async listMetadata() {
1167
+ const sessionIds = await this.list();
1168
+ const metadata = [];
1169
+ for (const id of sessionIds) {
1170
+ const meta = await this.getMetadata(id);
1171
+ if (meta) {
1172
+ metadata.push(meta);
1173
+ }
1174
+ }
1175
+ return metadata.sort((a, b) => b.updatedAt - a.updatedAt);
1176
+ }
1177
+ /**
1178
+ * Get session metadata without loading full entries
1179
+ */
1180
+ async getMetadata(sessionId) {
1181
+ const data = await this.load(sessionId);
1182
+ return data?.metadata ?? null;
1183
+ }
1184
+ };
1185
+
1186
+ // ../../packages/agent-framework/src/session/manager.ts
1187
+ function generateSessionId() {
1188
+ const timestamp = Date.now().toString(36);
1189
+ const random = Math.random().toString(36).substring(2, 8);
1190
+ return `${timestamp}-${random}`;
1191
+ }
1192
+ var SessionManager = class {
1193
+ store;
1194
+ activeSession = null;
1195
+ options;
1196
+ autoSaveTimer = null;
1197
+ gateway;
1198
+ constructor(options = {}) {
1199
+ this.options = {
1200
+ sessionsDir: options.sessionsDir ?? getDefaultSessionsDir(),
1201
+ autoSaveInterval: options.autoSaveInterval ?? 0,
1202
+ maxSessions: options.maxSessions ?? 100,
1203
+ version: options.version ?? 2
1204
+ };
1205
+ this.gateway = new FileSystemGateway({
1206
+ allowedRoots: [this.options.sessionsDir, getPlatformConfig().dataPath]
1207
+ });
1208
+ if (!this.gateway.exists(this.options.sessionsDir)) {
1209
+ this.gateway.mkdir(this.options.sessionsDir, { recursive: true });
1210
+ }
1211
+ this.store = new FileSessionStore(this.options.sessionsDir);
1212
+ if (this.options.autoSaveInterval > 0) {
1213
+ this.startAutoSave();
1214
+ }
1215
+ }
1216
+ /**
1217
+ * Get the session store
1218
+ */
1219
+ getStore() {
1220
+ return this.store;
1221
+ }
1222
+ /**
1223
+ * Start auto-save timer
1224
+ */
1225
+ startAutoSave() {
1226
+ if (this.autoSaveTimer) {
1227
+ clearInterval(this.autoSaveTimer);
1228
+ }
1229
+ this.autoSaveTimer = setInterval(() => {
1230
+ if (this.activeSession?.isDirty) {
1231
+ this.save().catch((e) => {
1232
+ });
1233
+ }
1234
+ }, this.options.autoSaveInterval);
1235
+ }
1236
+ /**
1237
+ * Stop auto-save timer
1238
+ */
1239
+ stopAutoSave() {
1240
+ if (this.autoSaveTimer) {
1241
+ clearInterval(this.autoSaveTimer);
1242
+ this.autoSaveTimer = null;
1243
+ }
1244
+ }
1245
+ /**
1246
+ * Create a new session with tree structure
1247
+ */
1248
+ async createSession(name, tags, sessionId) {
1249
+ const id = sessionId || generateSessionId();
1250
+ this.activeSession = {
1251
+ id,
1252
+ metadata: {
1253
+ id,
1254
+ name: name ?? `Session ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
1255
+ createdAt: Date.now(),
1256
+ updatedAt: Date.now(),
1257
+ entryCount: 0,
1258
+ messageCount: 0,
1259
+ activeEntryId: null,
1260
+ rootEntryId: null,
1261
+ tags
1262
+ },
1263
+ isDirty: true,
1264
+ entries: [],
1265
+ activeEntryId: null
1266
+ };
1267
+ await this.save();
1268
+ return id;
1269
+ }
1270
+ /**
1271
+ * Load an existing session
1272
+ */
1273
+ async loadSession(sessionId) {
1274
+ const data = await this.store.load(sessionId);
1275
+ if (!data) {
1276
+ console.warn(`[SessionManager] Session not found: ${sessionId}`);
1277
+ return false;
1278
+ }
1279
+ if (!data.entries) {
1280
+ data.entries = [];
1281
+ }
1282
+ this.activeSession = {
1283
+ id: sessionId,
1284
+ metadata: data.metadata,
1285
+ isDirty: false,
1286
+ lastSavedAt: data.metadata.updatedAt,
1287
+ entries: data.entries,
1288
+ activeEntryId: data.metadata.activeEntryId ?? null
1289
+ };
1290
+ return true;
1291
+ }
1292
+ /**
1293
+ * Save the current session
1294
+ */
1295
+ async save() {
1296
+ if (!this.activeSession) {
1297
+ throw new Error("No active session to save");
1298
+ }
1299
+ this.activeSession.metadata.entryCount = this.activeSession.entries.length;
1300
+ this.activeSession.metadata.messageCount = this.activeSession.entries.filter(
1301
+ (e) => e.type === "message" && e.message
1302
+ ).length;
1303
+ if (this.activeSession.metadata.messageCount === 0) {
1304
+ if (await this.store.exists(this.activeSession.id)) {
1305
+ await this.store.delete(this.activeSession.id);
1306
+ }
1307
+ this.activeSession.isDirty = false;
1308
+ return;
1309
+ }
1310
+ this.activeSession.metadata.updatedAt = Date.now();
1311
+ this.activeSession.metadata.activeEntryId = this.activeSession.activeEntryId;
1312
+ this.activeSession.metadata.rootEntryId = this.activeSession.entries.find(
1313
+ (e) => e.parentId === null
1314
+ )?.id ?? null;
1315
+ await this.store.save(
1316
+ this.activeSession.id,
1317
+ this.activeSession.entries,
1318
+ this.activeSession.metadata
1319
+ );
1320
+ this.activeSession.isDirty = false;
1321
+ this.activeSession.lastSavedAt = Date.now();
1322
+ }
1323
+ /**
1324
+ * Add a new entry to the session
1325
+ */
1326
+ addEntry(entryType, message) {
1327
+ if (!this.activeSession) {
1328
+ throw new Error("No active session");
1329
+ }
1330
+ const entry = {
1331
+ id: generateEntryId(),
1332
+ parentId: this.activeSession.activeEntryId,
1333
+ type: entryType,
1334
+ timestamp: Date.now(),
1335
+ message
1336
+ };
1337
+ this.activeSession.entries.push(entry);
1338
+ this.activeSession.activeEntryId = entry.id;
1339
+ this.activeSession.isDirty = true;
1340
+ return entry;
1341
+ }
1342
+ /**
1343
+ * Add a message entry to the session
1344
+ */
1345
+ addMessage(message) {
1346
+ return this.addEntry("message", message);
1347
+ }
1348
+ /**
1349
+ * Append a compaction checkpoint to the current active path.
1350
+ * Full history remains in entries; active messages are reconstructed from this checkpoint onward.
1351
+ */
1352
+ appendCompaction(summary, firstKeptEntryId, tokensBefore) {
1353
+ if (!this.activeSession) {
1354
+ throw new Error("No active session");
1355
+ }
1356
+ const entry = {
1357
+ id: generateEntryId(),
1358
+ parentId: this.activeSession.activeEntryId,
1359
+ type: "compaction",
1360
+ timestamp: Date.now(),
1361
+ compactionData: {
1362
+ summary,
1363
+ firstKeptEntryId,
1364
+ tokensBefore
1365
+ }
1366
+ };
1367
+ this.activeSession.entries.push(entry);
1368
+ this.activeSession.activeEntryId = entry.id;
1369
+ this.activeSession.isDirty = true;
1370
+ return entry;
1371
+ }
1372
+ /**
1373
+ * Update messages (legacy compatibility - rebuilds tree)
1374
+ * @deprecated Use addMessage() instead
1375
+ */
1376
+ updateMessages(messages) {
1377
+ if (!this.activeSession) {
1378
+ throw new Error("No active session");
1379
+ }
1380
+ const newEntries = [];
1381
+ let parentId = null;
1382
+ for (const message of messages) {
1383
+ const entry = {
1384
+ id: generateEntryId(),
1385
+ parentId,
1386
+ type: "message",
1387
+ timestamp: Date.now(),
1388
+ message
1389
+ };
1390
+ newEntries.push(entry);
1391
+ parentId = entry.id;
1392
+ }
1393
+ this.activeSession.entries = newEntries;
1394
+ this.activeSession.activeEntryId = newEntries.length > 0 ? newEntries[newEntries.length - 1].id : null;
1395
+ this.activeSession.isDirty = true;
1396
+ }
1397
+ /**
1398
+ * Get current messages (linear path from root to active leaf)
1399
+ */
1400
+ getMessages() {
1401
+ if (!this.activeSession) {
1402
+ return [];
1403
+ }
1404
+ return flattenTreeToMessages(this.activeSession.entries, this.activeSession.activeEntryId);
1405
+ }
1406
+ /**
1407
+ * Get current entries
1408
+ */
1409
+ getEntries() {
1410
+ return this.activeSession?.entries ?? [];
1411
+ }
1412
+ /**
1413
+ * Get active session info
1414
+ */
1415
+ getActiveSession() {
1416
+ return this.activeSession;
1417
+ }
1418
+ /**
1419
+ * Get active session ID
1420
+ */
1421
+ getActiveSessionId() {
1422
+ return this.activeSession?.id ?? null;
1423
+ }
1424
+ /**
1425
+ * Get current active entry ID
1426
+ */
1427
+ getActiveEntryId() {
1428
+ return this.activeSession?.activeEntryId ?? null;
1429
+ }
1430
+ /**
1431
+ * Set active entry (for navigation)
1432
+ */
1433
+ setActiveEntry(entryId) {
1434
+ if (!this.activeSession) {
1435
+ return false;
1436
+ }
1437
+ if (entryId === null) {
1438
+ this.activeSession.activeEntryId = null;
1439
+ this.activeSession.isDirty = true;
1440
+ return true;
1441
+ }
1442
+ const entry = this.activeSession.entries.find((e) => e.id === entryId);
1443
+ if (!entry) {
1444
+ console.warn(`[SessionManager] Entry not found: ${entryId}`);
1445
+ return false;
1446
+ }
1447
+ this.activeSession.activeEntryId = entryId;
1448
+ this.activeSession.isDirty = true;
1449
+ return true;
1450
+ }
1451
+ /**
1452
+ * Get entry by ID
1453
+ */
1454
+ getEntry(entryId) {
1455
+ if (!this.activeSession) {
1456
+ return null;
1457
+ }
1458
+ return this.activeSession.entries.find((e) => e.id === entryId) ?? null;
1459
+ }
1460
+ /**
1461
+ * Get children of an entry
1462
+ */
1463
+ getChildren(parentId) {
1464
+ if (!this.activeSession) {
1465
+ return [];
1466
+ }
1467
+ return this.activeSession.entries.filter((e) => e.parentId === parentId);
1468
+ }
1469
+ /**
1470
+ * Get tree structure
1471
+ */
1472
+ getTree() {
1473
+ if (!this.activeSession) {
1474
+ return [];
1475
+ }
1476
+ return buildTree(this.activeSession.entries, this.activeSession.activeEntryId);
1477
+ }
1478
+ /**
1479
+ * Branch from a specific entry (in-session branching)
1480
+ * Moves the active entry pointer to the specified entry.
1481
+ * Next addMessage() will create a child of that entry, forming a new branch.
1482
+ * Existing entries are not modified or deleted.
1483
+ */
1484
+ branch(entryId) {
1485
+ if (!this.activeSession) {
1486
+ return false;
1487
+ }
1488
+ if (entryId === null) {
1489
+ this.activeSession.activeEntryId = null;
1490
+ this.activeSession.isDirty = true;
1491
+ return true;
1492
+ }
1493
+ const entry = this.activeSession.entries.find((e) => e.id === entryId);
1494
+ if (!entry) {
1495
+ console.warn(`[SessionManager] Branch target not found: ${entryId}`);
1496
+ return false;
1497
+ }
1498
+ this.activeSession.activeEntryId = entryId;
1499
+ this.activeSession.isDirty = true;
1500
+ return true;
1501
+ }
1502
+ /**
1503
+ * Get all branch paths (all leaf nodes) in the session
1504
+ * Returns array of entry IDs representing branch tips
1505
+ */
1506
+ getBranchTips() {
1507
+ if (!this.activeSession) {
1508
+ return [];
1509
+ }
1510
+ const hasChildren = /* @__PURE__ */ new Set();
1511
+ for (const entry of this.activeSession.entries) {
1512
+ if (entry.parentId) {
1513
+ hasChildren.add(entry.parentId);
1514
+ }
1515
+ }
1516
+ const leaves = this.activeSession.entries.filter((e) => !hasChildren.has(e.id)).map((e) => e.id);
1517
+ return leaves;
1518
+ }
1519
+ /**
1520
+ * Get the path from root to a specific entry
1521
+ */
1522
+ getPathToEntry(entryId) {
1523
+ if (!this.activeSession) {
1524
+ return [];
1525
+ }
1526
+ const path = [];
1527
+ const byId = new Map(this.activeSession.entries.map((e) => [e.id, e]));
1528
+ let current = entryId;
1529
+ while (current) {
1530
+ const entry = byId.get(current);
1531
+ if (!entry) break;
1532
+ path.unshift(entry);
1533
+ current = entry.parentId;
1534
+ }
1535
+ return path;
1536
+ }
1537
+ /**
1538
+ * Get the timestamp of the latest compaction entry on the active path.
1539
+ */
1540
+ getLatestCompactionTimestamp() {
1541
+ if (!this.activeSession?.activeEntryId) {
1542
+ return null;
1543
+ }
1544
+ const path = this.getPathToEntry(this.activeSession.activeEntryId);
1545
+ for (let i = path.length - 1; i >= 0; i--) {
1546
+ const entry = path[i];
1547
+ if (entry.type === "compaction" && entry.compactionData) {
1548
+ return entry.timestamp;
1549
+ }
1550
+ }
1551
+ return null;
1552
+ }
1553
+ /**
1554
+ * Rename the active session
1555
+ */
1556
+ async rename(name) {
1557
+ if (!this.activeSession) {
1558
+ throw new Error("No active session");
1559
+ }
1560
+ this.activeSession.metadata.name = name;
1561
+ this.activeSession.isDirty = true;
1562
+ await this.save();
1563
+ }
1564
+ /**
1565
+ * Close the current session
1566
+ */
1567
+ async close() {
1568
+ if (this.activeSession?.isDirty) {
1569
+ await this.save();
1570
+ }
1571
+ this.activeSession = null;
1572
+ }
1573
+ /**
1574
+ * Delete a session (active or inactive)
1575
+ */
1576
+ async deleteSession(sessionId) {
1577
+ if (this.activeSession?.id === sessionId) {
1578
+ await this.close();
1579
+ }
1580
+ return await this.store.delete(sessionId);
1581
+ }
1582
+ /**
1583
+ * List all available sessions
1584
+ */
1585
+ async listSessions() {
1586
+ return await this.store.listMetadata();
1587
+ }
1588
+ /**
1589
+ * Check if a session exists
1590
+ */
1591
+ async hasSession(sessionId) {
1592
+ return await this.store.exists(sessionId);
1593
+ }
1594
+ /**
1595
+ * Fork the current session at a specific entry point
1596
+ */
1597
+ async forkAtEntry(entryId, newName) {
1598
+ if (!this.activeSession) {
1599
+ throw new Error("No active session to fork");
1600
+ }
1601
+ const sourceEntry = this.activeSession.entries.find((e) => e.id === entryId);
1602
+ if (!sourceEntry) {
1603
+ throw new Error(`Entry not found: ${entryId}`);
1604
+ }
1605
+ const newSessionId = generateSessionId();
1606
+ const now = Date.now();
1607
+ const subtreeEntries = [];
1608
+ const forkEntry = {
1609
+ id: generateEntryId(),
1610
+ parentId: null,
1611
+ type: "fork",
1612
+ timestamp: now,
1613
+ forkData: {
1614
+ sourceEntryId: entryId,
1615
+ sessionId: this.activeSession.id,
1616
+ name: this.activeSession.metadata.name
1617
+ }
1618
+ };
1619
+ subtreeEntries.push(forkEntry);
1620
+ let currentId = entryId;
1621
+ let newParentId = forkEntry.id;
1622
+ while (currentId) {
1623
+ const entry = this.activeSession.entries.find((e) => e.id === currentId);
1624
+ if (!entry) break;
1625
+ const newEntry = {
1626
+ ...entry,
1627
+ id: generateEntryId(),
1628
+ parentId: newParentId,
1629
+ timestamp: now
1630
+ };
1631
+ subtreeEntries.push(newEntry);
1632
+ newParentId = newEntry.id;
1633
+ const children = this.activeSession.entries.filter((e) => e.parentId === currentId);
1634
+ if (children.length === 0) break;
1635
+ const activePathChild = children.find((c) => {
1636
+ let checkId = this.activeSession?.activeEntryId ?? null;
1637
+ while (checkId) {
1638
+ if (checkId === c.id) return true;
1639
+ const checkEntry = this.activeSession?.entries.find((e) => e.id === checkId);
1640
+ checkId = checkEntry?.parentId ?? null;
1641
+ }
1642
+ return false;
1643
+ });
1644
+ currentId = activePathChild?.id ?? children[0]?.id ?? null;
1645
+ }
1646
+ await this.store.save(
1647
+ newSessionId,
1648
+ subtreeEntries,
1649
+ {
1650
+ name: newName ?? `${this.activeSession.metadata.name} (Branch)`,
1651
+ createdAt: now,
1652
+ updatedAt: now,
1653
+ tags: this.activeSession.metadata.tags
1654
+ }
1655
+ );
1656
+ return newSessionId;
1657
+ }
1658
+ /**
1659
+ * Fork the current session (create a copy with current state)
1660
+ */
1661
+ async fork(newName) {
1662
+ if (!this.activeSession) {
1663
+ throw new Error("No active session to fork");
1664
+ }
1665
+ const newSessionId = generateSessionId();
1666
+ const now = Date.now();
1667
+ const idMap = /* @__PURE__ */ new Map();
1668
+ const newEntries = [];
1669
+ for (const entry of this.activeSession.entries) {
1670
+ idMap.set(entry.id, generateEntryId());
1671
+ }
1672
+ for (const entry of this.activeSession.entries) {
1673
+ const newEntry = {
1674
+ ...entry,
1675
+ id: idMap.get(entry.id),
1676
+ parentId: entry.parentId ? idMap.get(entry.parentId) : null,
1677
+ timestamp: now
1678
+ };
1679
+ newEntries.push(newEntry);
1680
+ }
1681
+ const newActiveEntryId = this.activeSession.activeEntryId ? idMap.get(this.activeSession.activeEntryId) ?? null : null;
1682
+ await this.store.save(
1683
+ newSessionId,
1684
+ newEntries,
1685
+ {
1686
+ name: newName ?? `${this.activeSession.metadata.name} (Copy)`,
1687
+ createdAt: now,
1688
+ updatedAt: now,
1689
+ activeEntryId: newActiveEntryId,
1690
+ tags: this.activeSession.metadata.tags
1691
+ }
1692
+ );
1693
+ return newSessionId;
1694
+ }
1695
+ /**
1696
+ * Clean up old sessions if exceeding maxSessions
1697
+ */
1698
+ async cleanup() {
1699
+ if (this.options.maxSessions <= 0) {
1700
+ return 0;
1701
+ }
1702
+ const sessions = await this.store.listMetadata();
1703
+ if (sessions.length <= this.options.maxSessions) {
1704
+ return 0;
1705
+ }
1706
+ const sorted = sessions.sort((a, b) => a.updatedAt - b.updatedAt);
1707
+ const toDelete = sorted.slice(0, sessions.length - this.options.maxSessions);
1708
+ let deleted = 0;
1709
+ for (const session of toDelete) {
1710
+ if (session.id !== this.activeSession?.id) {
1711
+ await this.store.delete(session.id);
1712
+ deleted++;
1713
+ }
1714
+ }
1715
+ if (deleted > 0) {
1716
+ }
1717
+ return deleted;
1718
+ }
1719
+ /**
1720
+ * Dispose the manager
1721
+ */
1722
+ dispose() {
1723
+ this.stopAutoSave();
1724
+ }
1725
+ };
1726
+ function createSessionManager(options) {
1727
+ return new SessionManager(options);
1728
+ }
1729
+
1730
+ // ../../packages/agent-framework/src/extensions/runtime-registry.ts
1731
+ var typeBoxKind = Symbol.for("TypeBox.Kind");
1732
+
1733
+ // ../../packages/agent-framework/src/interaction.ts
1734
+ var InteractionUnavailableError = class extends Error {
1735
+ request;
1736
+ constructor(message, request) {
1737
+ super(message);
1738
+ this.name = "InteractionUnavailableError";
1739
+ this.request = request;
1740
+ }
1741
+ };
1742
+ var currentInteractionHandler = null;
1743
+ function registerInteractionHandler(handler) {
1744
+ currentInteractionHandler = handler;
1745
+ }
1746
+ function hasInteractionHandler() {
1747
+ return currentInteractionHandler !== null;
1748
+ }
1749
+ async function requestInteraction(request) {
1750
+ if (!currentInteractionHandler) {
1751
+ throw new InteractionUnavailableError(
1752
+ `No interaction handler registered for request type: ${request.type}`,
1753
+ request
1754
+ );
1755
+ }
1756
+ return await currentInteractionHandler(request);
1757
+ }
1758
+
1759
+ // ../../packages/shared-headless-capabilities/src/ask-user.ts
1760
+ var OTHER_OPTION = "\u270F\uFE0F Other (type your own)";
1761
+ var BACK_OPTION = "\u2190 Back";
1762
+ var REVIEW_OPTION = "\u2630 Review all answers";
1763
+ var CONFIRM_OPTION = "\u2713 Confirm and continue";
1764
+ var RESTART_OPTION = "\u21BA Restart all";
1765
+ var DEFAULT_ASK_TIMEOUT_MS = 6e4;
1766
+ var AskUserParamsSchema = Type.Object({
1767
+ question: Type.String({
1768
+ description: "REQUIRED: The question text to ask the user"
1769
+ }),
1770
+ options: Type.Optional(Type.Array(
1771
+ Type.String({ description: "An option label" })
1772
+ )),
1773
+ allowOther: Type.Optional(Type.Boolean({
1774
+ description: "Whether to show 'Other' option (default: true when options provided)",
1775
+ default: true
1776
+ })),
1777
+ allowBack: Type.Optional(Type.Boolean({
1778
+ description: "Whether to allow going back (adds '\u2190 Back' option)",
1779
+ default: false
1780
+ })),
1781
+ placeholder: Type.Optional(Type.String({
1782
+ description: "Placeholder text for input field"
1783
+ }))
1784
+ });
1785
+ var AskUserMultiParamsSchema = Type.Object({
1786
+ prompt: Type.Optional(Type.String({
1787
+ description: "Legacy-compatible shortcut for a single question. Equivalent to `question`."
1788
+ })),
1789
+ question: Type.Optional(Type.String({
1790
+ description: "Preferred shortcut for a single question. Use this for the common case."
1791
+ })),
1792
+ options: Type.Optional(Type.Array(
1793
+ Type.String({ description: "Shortcut options for the single-question form." })
1794
+ )),
1795
+ placeholder: Type.Optional(Type.String({
1796
+ description: "Shortcut placeholder for the single-question form."
1797
+ })),
1798
+ questions: Type.Optional(Type.Array(
1799
+ Type.Union([
1800
+ Type.String({
1801
+ description: "Lightweight question syntax. Use `Question text|Option A|Option B` for single choice, or just `Question text` for free text."
1802
+ }),
1803
+ Type.Object({
1804
+ id: Type.Optional(Type.String({ description: "Question identifier; optional and auto-generated if omitted." })),
1805
+ question: Type.String({ description: "Question text" }),
1806
+ options: Type.Optional(Type.Array(Type.String())),
1807
+ allowOther: Type.Optional(Type.Boolean()),
1808
+ placeholder: Type.Optional(Type.String())
1809
+ })
1810
+ ]),
1811
+ { description: "Questions to ask sequentially. Can be strings or lightweight objects." }
1812
+ )),
1813
+ allowReview: Type.Optional(Type.Boolean({
1814
+ description: "Allow user to review and modify answers at the end",
1815
+ default: true
1816
+ }))
1817
+ });
1818
+ function buildUnsupportedResult(error) {
1819
+ const message = error instanceof InteractionUnavailableError ? error.message : error instanceof Error ? error.message : String(error);
1820
+ return {
1821
+ content: [{ type: "text", text: `Interaction unavailable: ${message}` }],
1822
+ details: { unsupported: true, error: message },
1823
+ isError: true
1824
+ };
1825
+ }
1826
+ function validateQuestions(questions) {
1827
+ if (!Array.isArray(questions) || questions.length === 0) {
1828
+ return "'questions' must be a non-empty array";
1829
+ }
1830
+ for (let index = 0; index < questions.length; index++) {
1831
+ const question = questions[index];
1832
+ if (!question || typeof question !== "object") {
1833
+ return `questions[${index}] must be an object`;
1834
+ }
1835
+ const candidate = question;
1836
+ if (typeof candidate.id !== "string" || candidate.id.trim().length === 0) {
1837
+ return `questions[${index}].id must be a non-empty string`;
1838
+ }
1839
+ if (typeof candidate.question !== "string" || candidate.question.trim().length === 0) {
1840
+ return `questions[${index}].question must be a non-empty string`;
1841
+ }
1842
+ if (candidate.options !== void 0) {
1843
+ if (!Array.isArray(candidate.options) || candidate.options.length === 0) {
1844
+ return `questions[${index}].options must be a non-empty array when provided`;
1845
+ }
1846
+ for (let optionIndex = 0; optionIndex < candidate.options.length; optionIndex++) {
1847
+ const option = candidate.options[optionIndex];
1848
+ if (typeof option !== "string" || option.trim().length === 0) {
1849
+ return `questions[${index}].options[${optionIndex}] must be a non-empty string`;
1850
+ }
1851
+ }
1852
+ }
1853
+ }
1854
+ return null;
1855
+ }
1856
+ function normalizeStringQuestion(value, index) {
1857
+ const parts = value.split("|").map((part) => part.trim()).filter((part) => part.length > 0);
1858
+ if (parts.length === 0) return null;
1859
+ const [question, ...options] = parts;
1860
+ return {
1861
+ id: `q${index + 1}`,
1862
+ question,
1863
+ options: options.length > 0 ? options : void 0
1864
+ };
1865
+ }
1866
+ function normalizeQuestionObject(value, index) {
1867
+ const options = Array.isArray(value.options) ? value.options.filter((option) => typeof option === "string").map((option) => option.trim()).filter((option) => option.length > 0) : void 0;
1868
+ return {
1869
+ id: typeof value.id === "string" && value.id.trim().length > 0 ? value.id.trim() : `q${index + 1}`,
1870
+ question: typeof value.question === "string" ? value.question.trim() : "",
1871
+ options: options && options.length > 0 ? options : void 0,
1872
+ allowOther: typeof value.allowOther === "boolean" ? value.allowOther : void 0,
1873
+ placeholder: typeof value.placeholder === "string" ? value.placeholder : void 0
1874
+ };
1875
+ }
1876
+ function normalizeAskUserArgs(args) {
1877
+ if (typeof args === "string") {
1878
+ try {
1879
+ args = JSON.parse(args);
1880
+ } catch {
1881
+ const preview = args.length > 200 ? `${args.slice(0, 200)}...` : args;
1882
+ return { error: `Tool arguments must be an object or valid JSON object string (received string: ${preview})` };
1883
+ }
1884
+ }
1885
+ const allowReview = args?.allowReview ?? true;
1886
+ const singleQuestionText = typeof args?.question === "string" && args.question.trim().length > 0 ? args.question.trim() : typeof args?.prompt === "string" && args.prompt.trim().length > 0 ? args.prompt.trim() : void 0;
1887
+ const normalizedSingleQuestion = singleQuestionText ? {
1888
+ id: "q1",
1889
+ question: singleQuestionText,
1890
+ options: Array.isArray(args.options) ? args.options.filter((option) => typeof option === "string").map((option) => option.trim()).filter((option) => option.length > 0) : void 0,
1891
+ placeholder: typeof args?.placeholder === "string" ? args.placeholder : void 0
1892
+ } : void 0;
1893
+ if (normalizedSingleQuestion && Array.isArray(args?.questions) && args.questions.length === 0) {
1894
+ return { questions: [normalizedSingleQuestion], allowReview };
1895
+ }
1896
+ if (!Array.isArray(args?.questions)) {
1897
+ if (normalizedSingleQuestion) {
1898
+ return { questions: [normalizedSingleQuestion], allowReview };
1899
+ }
1900
+ return { error: "Provide `question` or `prompt` for a single question, or provide a non-empty `questions` array" };
1901
+ }
1902
+ const normalizedQuestions = args.questions.map((question, index) => {
1903
+ if (typeof question === "string") {
1904
+ return normalizeStringQuestion(question, index);
1905
+ }
1906
+ if (question && typeof question === "object") {
1907
+ return normalizeQuestionObject(question, index);
1908
+ }
1909
+ return null;
1910
+ }).filter((question) => {
1911
+ if (question === null) return false;
1912
+ return typeof question.question === "string" && question.question.trim().length > 0;
1913
+ });
1914
+ if (normalizedQuestions.length > 0) {
1915
+ return { questions: normalizedQuestions, allowReview };
1916
+ }
1917
+ if (normalizedSingleQuestion) {
1918
+ return { questions: [normalizedSingleQuestion], allowReview };
1919
+ }
1920
+ return { error: "Provide `question` or `prompt` for a single question, or provide a non-empty `questions` array" };
1921
+ }
1922
+ function buildSkippedResult(question, timedOut = false) {
1923
+ return {
1924
+ content: [{ type: "text", text: "(skipped)" }],
1925
+ details: {
1926
+ answer: "(skipped)",
1927
+ skipped: true,
1928
+ timedOut,
1929
+ question,
1930
+ cancelled: false
1931
+ }
1932
+ };
1933
+ }
1934
+ async function askSingleQuestion(args) {
1935
+ const { question, options, allowOther = true, allowBack = false, allowReview = false, placeholder } = args;
1936
+ if (!question || typeof question !== "string") {
1937
+ return {
1938
+ content: [{ type: "text", text: "Error: 'question' parameter is required" }],
1939
+ isError: true
1940
+ };
1941
+ }
1942
+ if (!options || options.length === 0) {
1943
+ const response = await requestInteraction({
1944
+ type: "multiline_input",
1945
+ id: `ask-user:${Date.now()}`,
1946
+ prompt: question,
1947
+ timeoutMs: DEFAULT_ASK_TIMEOUT_MS
1948
+ });
1949
+ if (response.type === "multiline_input" && (response.skipped || response.timedOut)) {
1950
+ return buildSkippedResult(question, response.timedOut === true);
1951
+ }
1952
+ const answer = response.type === "multiline_input" ? response.value : void 0;
1953
+ if (answer === void 0) {
1954
+ return { content: [{ type: "text", text: "User cancelled" }], details: { cancelled: true } };
1955
+ }
1956
+ return {
1957
+ content: [{ type: "text", text: answer }],
1958
+ details: { answer, cancelled: false }
1959
+ };
1960
+ }
1961
+ const displayOptions = [...options];
1962
+ if (allowOther !== false) displayOptions.push(OTHER_OPTION);
1963
+ if (allowBack) displayOptions.push(BACK_OPTION);
1964
+ if (allowReview) displayOptions.push(REVIEW_OPTION);
1965
+ const selection = await requestInteraction({
1966
+ type: "select_one",
1967
+ id: `ask-user:select:${Date.now()}`,
1968
+ prompt: question,
1969
+ options: displayOptions,
1970
+ timeoutMs: DEFAULT_ASK_TIMEOUT_MS
1971
+ });
1972
+ if (selection.type === "select_one" && (selection.skipped || selection.timedOut)) {
1973
+ return buildSkippedResult(question, selection.timedOut === true);
1974
+ }
1975
+ if (selection.type !== "select_one" || selection.selection === void 0) {
1976
+ return { content: [{ type: "text", text: "User cancelled" }], details: { cancelled: true } };
1977
+ }
1978
+ if (selection.selection === BACK_OPTION) {
1979
+ return { content: [{ type: "text", text: "BACK" }], details: { goBack: true, cancelled: false } };
1980
+ }
1981
+ if (selection.selection === REVIEW_OPTION) {
1982
+ return { content: [{ type: "text", text: "REVIEW" }], details: { goReview: true, cancelled: false } };
1983
+ }
1984
+ if (selection.selection === OTHER_OPTION) {
1985
+ const response = await requestInteraction({
1986
+ type: "text_input",
1987
+ id: `ask-user:${Date.now()}`,
1988
+ prompt: `${question} (Other)`,
1989
+ placeholder,
1990
+ timeoutMs: DEFAULT_ASK_TIMEOUT_MS
1991
+ });
1992
+ if (response.type === "text_input" && (response.skipped || response.timedOut)) {
1993
+ return buildSkippedResult(question, response.timedOut === true);
1994
+ }
1995
+ const customAnswer = response.type === "text_input" ? response.value : void 0;
1996
+ if (customAnswer === void 0) {
1997
+ return { content: [{ type: "text", text: "User cancelled" }], details: { cancelled: true } };
1998
+ }
1999
+ return {
2000
+ content: [{ type: "text", text: `Other: ${customAnswer}` }],
2001
+ details: { answer: customAnswer, isOther: true, cancelled: false }
2002
+ };
2003
+ }
2004
+ const selectedIndex = displayOptions.indexOf(selection.selection);
2005
+ return {
2006
+ content: [{ type: "text", text: `${String.fromCharCode(65 + selectedIndex)}. ${selection.selection}` }],
2007
+ details: {
2008
+ answer: selection.selection,
2009
+ selectedIndex,
2010
+ isOther: false,
2011
+ cancelled: false
2012
+ }
2013
+ };
2014
+ }
2015
+ function createAskUserCapability() {
2016
+ const tools = [
2017
+ {
2018
+ name: "ask_user_multi",
2019
+ label: "Ask User",
2020
+ description: `Ask one or more questions with review and back navigation. Use this for all user questioning flows, including demonstrations that should actually ask the user instead of only describing the questionnaire. When you decide to use this tool, invoke it immediately. Do not print explanatory preambles, numbered plans, or example JSON in chat before calling it. For a generic demo such as '\u6F14\u793A\u4E00\u4E0B ask \u5DE5\u5177', '\u6F14\u793A\u4E00\u4E0B\u63D0\u95EE\u5DE5\u5177', or 'ask me', ask exactly one simple single-choice question with 3-5 explicit options so the user sees a selector instead of a free-text editor. Do not switch to an open-ended or multi-question flow unless the user explicitly asks for it. Prefer the shortest valid shape. For a single question, use {"question":"Choose a color","options":["Red","Blue","Green"]}. You may also use {"prompt":"Choose a color","options":[...]} for compatibility. For multiple questions, use \`questions\`. Question ids are optional and will be auto-generated.`,
2021
+ parameters: AskUserMultiParamsSchema,
2022
+ async execute(toolCallIdOrArgs, maybeArgs) {
2023
+ try {
2024
+ const args = typeof toolCallIdOrArgs === "string" && maybeArgs !== void 0 ? maybeArgs : toolCallIdOrArgs;
2025
+ const normalized = normalizeAskUserArgs(args);
2026
+ if ("error" in normalized) {
2027
+ return {
2028
+ content: [
2029
+ {
2030
+ type: "text",
2031
+ text: `Error: ${normalized.error}. Use one of these shapes: {"question":"Choose a color","options":["Red","Blue","Green"]} or {"prompt":"Choose a color","options":["Red","Blue","Green"]} or {"questions":[{"question":"Choose a color","options":["Red","Blue","Green"]},{"question":"Describe why"}],"allowReview":true}`
2032
+ }
2033
+ ],
2034
+ isError: true
2035
+ };
2036
+ }
2037
+ const { questions, allowReview } = normalized;
2038
+ const validationError = validateQuestions(questions);
2039
+ if (validationError) {
2040
+ return {
2041
+ content: [
2042
+ {
2043
+ type: "text",
2044
+ text: `Error: ${validationError}. Use one of these shapes: {"question":"Choose a color","options":["Red","Blue","Green"]} or {"prompt":"Choose a color","options":["Red","Blue","Green"]} or {"questions":["Choose a color|Red|Blue|Green","Describe why"],"allowReview":true}`
2045
+ }
2046
+ ],
2047
+ isError: true
2048
+ };
2049
+ }
2050
+ const answers = {};
2051
+ let currentIndex = 0;
2052
+ while (currentIndex < questions.length) {
2053
+ const q = questions[currentIndex];
2054
+ const result = await askSingleQuestion({
2055
+ question: `${currentIndex + 1}/${questions.length}: ${q.question}`,
2056
+ options: q.options,
2057
+ allowOther: q.allowOther,
2058
+ allowBack: currentIndex > 0,
2059
+ allowReview: allowReview && Object.keys(answers).length > 0,
2060
+ placeholder: q.placeholder
2061
+ });
2062
+ if (result.details?.cancelled) {
2063
+ return { content: [{ type: "text", text: "User cancelled" }], details: { cancelled: true } };
2064
+ }
2065
+ if (result.details?.goBack) {
2066
+ currentIndex = Math.max(0, currentIndex - 1);
2067
+ delete answers[questions[currentIndex].id];
2068
+ continue;
2069
+ }
2070
+ if (result.details?.goReview) {
2071
+ break;
2072
+ }
2073
+ answers[q.id] = {
2074
+ question: q.question,
2075
+ answer: result.details?.answer || result.content[0].text,
2076
+ ...result.details
2077
+ };
2078
+ currentIndex++;
2079
+ }
2080
+ if (allowReview && Object.keys(answers).length > 0) {
2081
+ while (true) {
2082
+ const reviewOptions = [
2083
+ CONFIRM_OPTION,
2084
+ ...questions.map((q2, i) => {
2085
+ const answer = answers[q2.id]?.answer || "(not answered)";
2086
+ const displayAnswer = answer.length > 20 ? `${answer.substring(0, 20)}...` : answer;
2087
+ return `\u270F\uFE0F Q${i + 1}: ${displayAnswer}`;
2088
+ }),
2089
+ RESTART_OPTION
2090
+ ];
2091
+ const review = await requestInteraction({
2092
+ type: "select_one",
2093
+ id: `ask-user:review:${Date.now()}`,
2094
+ prompt: "\u2630 Review your answers (select to edit):",
2095
+ options: reviewOptions,
2096
+ timeoutMs: DEFAULT_ASK_TIMEOUT_MS
2097
+ });
2098
+ if (review.type === "select_one" && (review.skipped || review.timedOut)) {
2099
+ break;
2100
+ }
2101
+ if (review.type !== "select_one" || review.selection === void 0) {
2102
+ return { content: [{ type: "text", text: "User cancelled during review" }], details: { cancelled: true } };
2103
+ }
2104
+ const reviewIndex = reviewOptions.indexOf(review.selection);
2105
+ if (review.selection === CONFIRM_OPTION) {
2106
+ break;
2107
+ }
2108
+ if (review.selection === RESTART_OPTION) {
2109
+ Object.keys(answers).forEach((key) => delete answers[key]);
2110
+ currentIndex = 0;
2111
+ break;
2112
+ }
2113
+ const questionIndex = reviewIndex - 1;
2114
+ if (questionIndex < 0 || questionIndex >= questions.length) {
2115
+ continue;
2116
+ }
2117
+ const q = questions[questionIndex];
2118
+ const result = await askSingleQuestion({
2119
+ question: `\u270F\uFE0F Edit Q${questionIndex + 1}/${questions.length}: ${q.question}`,
2120
+ options: q.options,
2121
+ allowOther: q.allowOther,
2122
+ allowBack: questionIndex > 0,
2123
+ allowReview: true,
2124
+ placeholder: q.placeholder
2125
+ });
2126
+ if (result.details?.cancelled) {
2127
+ return { content: [{ type: "text", text: "User cancelled" }], details: { cancelled: true } };
2128
+ }
2129
+ if (result.details?.goBack && questionIndex > 0) {
2130
+ delete answers[q.id];
2131
+ currentIndex = questionIndex - 1;
2132
+ break;
2133
+ }
2134
+ if (result.details?.goReview) {
2135
+ continue;
2136
+ }
2137
+ answers[q.id] = {
2138
+ question: q.question,
2139
+ answer: result.details?.answer || result.content[0].text,
2140
+ ...result.details
2141
+ };
2142
+ }
2143
+ }
2144
+ const summary = Object.entries(answers).map(([id, data]) => `${id}: ${data.answer}`).join("\n");
2145
+ return {
2146
+ content: [{ type: "text", text: summary }],
2147
+ details: { answers, cancelled: false }
2148
+ };
2149
+ } catch (error) {
2150
+ return buildUnsupportedResult(error);
2151
+ }
2152
+ }
2153
+ }
2154
+ ];
2155
+ return {
2156
+ ...defineSharedCapability({
2157
+ id: "ask-user",
2158
+ description: "Ask the user one or more questions through the shared interaction protocol.",
2159
+ tools,
2160
+ requiredInteractions: ["select_one", "confirm", "text_input", "multiline_input"],
2161
+ systemPromptAdditions: [
2162
+ "When the user asks you to ask questions, collect choices, or gather structured input, call `ask_user_multi` instead of describing the questions in plain text.",
2163
+ "When demonstrating `ask_user_multi`, call it immediately instead of first printing explanations, plans, or example JSON.",
2164
+ "When the user asks for a generic ask-tool demo, such as '\u6F14\u793A\u4E00\u4E0B ask \u5DE5\u5177', '\u6F14\u793A\u4E00\u4E0B\u63D0\u95EE\u5DE5\u5177', or 'ask me', ask exactly one simple single-choice question with explicit options so the interaction opens as a selector.",
2165
+ "For a single question, prefer the shortest valid argument shape such as `question` plus optional `options`."
2166
+ ]
2167
+ }),
2168
+ tools
2169
+ };
2170
+ }
2171
+
2172
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/path-utils.ts
2173
+ function isInAllowlist(path, allowlistedDirs) {
2174
+ const normalized = normalizeForComparison(path);
2175
+ for (const dir of allowlistedDirs) {
2176
+ const normalizedDir = normalizeForComparison(dir);
2177
+ const dirWithSlash = withTrailingSep(normalizedDir);
2178
+ if (normalized === normalizedDir || normalized.startsWith(dirWithSlash)) {
2179
+ return true;
2180
+ }
2181
+ }
2182
+ return false;
2183
+ }
2184
+ function normalizeForComparison(path) {
2185
+ const normalized = path.replace(/\\/g, "/").replace(/\/+/g, "/");
2186
+ if (normalized === "/") return normalized;
2187
+ return normalized.replace(/\/+$/, "");
2188
+ }
2189
+ function withTrailingSep(path) {
2190
+ return path.endsWith("/") ? path : `${path}/`;
2191
+ }
2192
+ function isWithinRoot(path, root) {
2193
+ const normalizedPath = normalizeForComparison(path);
2194
+ const normalizedRoot = normalizeForComparison(root);
2195
+ return normalizedPath === normalizedRoot || normalizedPath.startsWith(withTrailingSep(normalizedRoot));
2196
+ }
2197
+ function normalizePrefix(prefix) {
2198
+ const normalized = normalizeForComparison(prefix);
2199
+ return normalized.startsWith("/") ? normalized.slice(1) : normalized;
2200
+ }
2201
+ function getAvailableRoots(options) {
2202
+ if (!options?.roots || options.roots.length === 0) {
2203
+ return [];
2204
+ }
2205
+ const fs = getFileSystem();
2206
+ const seen = /* @__PURE__ */ new Set();
2207
+ const results = [];
2208
+ for (const root of options.roots) {
2209
+ if (!root || typeof root.name !== "string" || typeof root.path !== "string") {
2210
+ continue;
2211
+ }
2212
+ const name = root.name.trim();
2213
+ const path = root.path.trim();
2214
+ if (!name || !path || seen.has(name)) {
2215
+ continue;
2216
+ }
2217
+ seen.add(name);
2218
+ results.push({
2219
+ name,
2220
+ path: fs.normalize(path),
2221
+ description: root.description?.trim() || void 0,
2222
+ defaultPrefixes: Array.isArray(root.defaultPrefixes) ? root.defaultPrefixes.map((prefix) => String(prefix || "").trim()).filter((prefix) => prefix.length > 0) : void 0
2223
+ });
2224
+ }
2225
+ return results;
2226
+ }
2227
+ function getDefaultRootName(sandboxRoot, options) {
2228
+ const roots = getAvailableRoots(options);
2229
+ if (roots.length === 0) {
2230
+ return void 0;
2231
+ }
2232
+ const configured = (options?.defaultRoot || "").trim();
2233
+ if (configured && roots.some((root) => root.name === configured)) {
2234
+ return configured;
2235
+ }
2236
+ const normalizedSandboxRoot = normalizeForComparison(sandboxRoot);
2237
+ const sandboxMatch = roots.find(
2238
+ (root) => normalizeForComparison(root.path) === normalizedSandboxRoot
2239
+ );
2240
+ if (sandboxMatch) {
2241
+ return sandboxMatch.name;
2242
+ }
2243
+ return roots[0]?.name;
2244
+ }
2245
+ function getRootByName(name, sandboxRoot, options) {
2246
+ const roots = getAvailableRoots(options);
2247
+ if (roots.length === 0) {
2248
+ return void 0;
2249
+ }
2250
+ const resolvedName = (name || "").trim() || getDefaultRootName(sandboxRoot, options);
2251
+ if (!resolvedName) {
2252
+ return void 0;
2253
+ }
2254
+ return roots.find((root) => root.name === resolvedName);
2255
+ }
2256
+ function findPrefixMatchedRoot(filePath, options) {
2257
+ const normalizedPath = normalizeForComparison(filePath);
2258
+ for (const root of getAvailableRoots(options)) {
2259
+ for (const prefix of root.defaultPrefixes ?? []) {
2260
+ const normalizedPrefix = normalizePrefix(prefix);
2261
+ if (normalizedPath === normalizedPrefix || normalizedPath.startsWith(withTrailingSep(normalizedPrefix))) {
2262
+ return root;
2263
+ }
2264
+ }
2265
+ }
2266
+ return void 0;
2267
+ }
2268
+ function getResolutionRoot(filePath, sandboxRoot, options, rootName) {
2269
+ if (rootName) {
2270
+ return getRootByName(rootName, sandboxRoot, options);
2271
+ }
2272
+ return findPrefixMatchedRoot(filePath, options) ?? getRootByName(void 0, sandboxRoot, options);
2273
+ }
2274
+ function resolveInSandbox(filePath, sandboxRoot, options, rootName) {
2275
+ const fs = getFileSystem();
2276
+ const allowlistedDirs = options?.allowlistedDirs ?? [];
2277
+ if (!filePath) {
2278
+ throw new Error("Path cannot be empty");
2279
+ }
2280
+ const cleaned = filePath.startsWith("@") ? filePath.slice(1) : filePath;
2281
+ if (fs.isAbsolute(cleaned)) {
2282
+ const normalized = fs.normalize(cleaned);
2283
+ const selectedRoot2 = rootName ? getRootByName(rootName, sandboxRoot, options) : void 0;
2284
+ if (rootName && !selectedRoot2) {
2285
+ const available = getAvailableRoots(options).map((root) => root.name).join(", ");
2286
+ throw new Error(
2287
+ available ? `Unknown root '${rootName}'. Available roots: ${available}` : `Unknown root '${rootName}'. No named roots are configured.`
2288
+ );
2289
+ }
2290
+ if (selectedRoot2) {
2291
+ if (!isWithinRoot(normalized, selectedRoot2.path)) {
2292
+ throw new Error(`Absolute path outside root '${selectedRoot2.name}': ${filePath}`);
2293
+ }
2294
+ return normalized;
2295
+ }
2296
+ for (const root of getAvailableRoots(options)) {
2297
+ if (isWithinRoot(normalized, root.path)) {
2298
+ return normalized;
2299
+ }
2300
+ }
2301
+ if (isInAllowlist(normalized, allowlistedDirs)) {
2302
+ return normalized;
2303
+ }
2304
+ if (!isWithinRoot(normalized, sandboxRoot)) {
2305
+ throw new Error(`Absolute path outside sandbox: ${filePath}`);
2306
+ }
2307
+ return normalized;
2308
+ }
2309
+ const selectedRoot = getResolutionRoot(cleaned, sandboxRoot, options, rootName);
2310
+ if (rootName && !selectedRoot) {
2311
+ const available = getAvailableRoots(options).map((root) => root.name).join(", ");
2312
+ throw new Error(
2313
+ available ? `Unknown root '${rootName}'. Available roots: ${available}` : `Unknown root '${rootName}'. No named roots are configured.`
2314
+ );
2315
+ }
2316
+ const resolvedBase = selectedRoot?.path ?? sandboxRoot;
2317
+ const resolved = fs.resolve(resolvedBase, cleaned);
2318
+ if (selectedRoot) {
2319
+ if (!isWithinRoot(resolved, selectedRoot.path)) {
2320
+ throw new Error(`Path escapes root '${selectedRoot.name}': ${filePath}`);
2321
+ }
2322
+ return resolved;
2323
+ }
2324
+ if (!isWithinRoot(resolved, sandboxRoot) && !isInAllowlist(resolved, allowlistedDirs)) {
2325
+ throw new Error(`Path escapes sandbox: ${filePath}`);
2326
+ }
2327
+ return resolved;
2328
+ }
2329
+ var resolveToCwd = resolveInSandbox;
2330
+ var resolveReadPath = resolveInSandbox;
2331
+
2332
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/schema-utils.ts
2333
+ function formatPrefixes(prefixes) {
2334
+ if (!prefixes || prefixes.length === 0) {
2335
+ return "";
2336
+ }
2337
+ return prefixes.map((prefix) => {
2338
+ const trimmed = prefix.trim();
2339
+ return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
2340
+ }).join(", ");
2341
+ }
2342
+ function buildRootParameterSchema(sandboxRoot, options) {
2343
+ const roots = getAvailableRoots(options);
2344
+ if (roots.length === 0) {
2345
+ return void 0;
2346
+ }
2347
+ const defaultRoot = getDefaultRootName(sandboxRoot, options);
2348
+ const description = [
2349
+ `Optional root name to resolve the path against. Available roots: ${roots.map((root) => root.name).join(", ")}.`,
2350
+ defaultRoot ? `When omitted, the default root is '${defaultRoot}' unless a prefix rule matches first.` : ""
2351
+ ].filter(Boolean).join(" ");
2352
+ if (roots.length === 1) {
2353
+ return Type.Optional(Type.Literal(roots[0].name, { description }));
2354
+ }
2355
+ return Type.Optional(
2356
+ Type.Union(
2357
+ roots.map((root) => Type.Literal(root.name)),
2358
+ { description }
2359
+ )
2360
+ );
2361
+ }
2362
+ function buildPathResolutionDescription(sandboxRoot, options) {
2363
+ const roots = getAvailableRoots(options);
2364
+ if (roots.length === 0) {
2365
+ return "Relative paths resolve against the workspace root.";
2366
+ }
2367
+ const rootSummaries = roots.map((root) => {
2368
+ const pieces = [`'${root.name}'`];
2369
+ if (root.description) {
2370
+ pieces.push(root.description);
2371
+ }
2372
+ const prefixes = formatPrefixes(root.defaultPrefixes);
2373
+ if (prefixes) {
2374
+ pieces.push(`prefixes: ${prefixes}`);
2375
+ }
2376
+ return pieces.join(" - ");
2377
+ });
2378
+ const defaultRoot = getDefaultRootName(sandboxRoot, options);
2379
+ const hasPrefixRules = roots.some((root) => (root.defaultPrefixes?.length ?? 0) > 0);
2380
+ const defaultSentence = defaultRoot ? hasPrefixRules ? `When root is omitted, prefix-matched roots win first; otherwise '${defaultRoot}' is used.` : `When root is omitted, '${defaultRoot}' is used.` : "";
2381
+ return `Available roots: ${rootSummaries.join("; ")}. ${defaultSentence}`.trim();
2382
+ }
2383
+
2384
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/truncate.ts
2385
+ var DEFAULT_MAX_LINES = 2e3;
2386
+ var DEFAULT_MAX_BYTES = 50 * 1024;
2387
+ var GREP_MAX_LINE_LENGTH = 500;
2388
+ function formatSize(bytes) {
2389
+ if (bytes < 1024) {
2390
+ return `${bytes}B`;
2391
+ } else if (bytes < 1024 * 1024) {
2392
+ return `${(bytes / 1024).toFixed(1)}KB`;
2393
+ } else {
2394
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
2395
+ }
2396
+ }
2397
+ function truncateHead(content, options = {}) {
2398
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
2399
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
2400
+ const totalBytes = Buffer.byteLength(content, "utf-8");
2401
+ const lines = content.split("\n");
2402
+ const totalLines = lines.length;
2403
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
2404
+ return {
2405
+ content,
2406
+ truncated: false,
2407
+ truncatedBy: null,
2408
+ totalLines,
2409
+ totalBytes,
2410
+ outputLines: totalLines,
2411
+ outputBytes: totalBytes,
2412
+ lastLinePartial: false,
2413
+ firstLineExceedsLimit: false,
2414
+ maxLines,
2415
+ maxBytes
2416
+ };
2417
+ }
2418
+ const firstLineBytes = Buffer.byteLength(lines[0], "utf-8");
2419
+ if (firstLineBytes > maxBytes) {
2420
+ return {
2421
+ content: "",
2422
+ truncated: true,
2423
+ truncatedBy: "bytes",
2424
+ totalLines,
2425
+ totalBytes,
2426
+ outputLines: 0,
2427
+ outputBytes: 0,
2428
+ lastLinePartial: false,
2429
+ firstLineExceedsLimit: true,
2430
+ maxLines,
2431
+ maxBytes
2432
+ };
2433
+ }
2434
+ const outputLinesArr = [];
2435
+ let outputBytesCount = 0;
2436
+ let truncatedBy = "lines";
2437
+ for (let i = 0; i < lines.length && i < maxLines; i++) {
2438
+ const line = lines[i];
2439
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0);
2440
+ if (outputBytesCount + lineBytes > maxBytes) {
2441
+ truncatedBy = "bytes";
2442
+ break;
2443
+ }
2444
+ outputLinesArr.push(line);
2445
+ outputBytesCount += lineBytes;
2446
+ }
2447
+ if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
2448
+ truncatedBy = "lines";
2449
+ }
2450
+ const outputContent = outputLinesArr.join("\n");
2451
+ const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
2452
+ return {
2453
+ content: outputContent,
2454
+ truncated: true,
2455
+ truncatedBy,
2456
+ totalLines,
2457
+ totalBytes,
2458
+ outputLines: outputLinesArr.length,
2459
+ outputBytes: finalOutputBytes,
2460
+ lastLinePartial: false,
2461
+ firstLineExceedsLimit: false,
2462
+ maxLines,
2463
+ maxBytes
2464
+ };
2465
+ }
2466
+ function truncateTail(content, options = {}) {
2467
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
2468
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
2469
+ const totalBytes = Buffer.byteLength(content, "utf-8");
2470
+ const lines = content.split("\n");
2471
+ const totalLines = lines.length;
2472
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
2473
+ return {
2474
+ content,
2475
+ truncated: false,
2476
+ truncatedBy: null,
2477
+ totalLines,
2478
+ totalBytes,
2479
+ outputLines: totalLines,
2480
+ outputBytes: totalBytes,
2481
+ lastLinePartial: false,
2482
+ firstLineExceedsLimit: false,
2483
+ maxLines,
2484
+ maxBytes
2485
+ };
2486
+ }
2487
+ const outputLinesArr = [];
2488
+ let outputBytesCount = 0;
2489
+ let truncatedBy = "lines";
2490
+ let lastLinePartial = false;
2491
+ for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
2492
+ const line = lines[i];
2493
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
2494
+ if (outputBytesCount + lineBytes > maxBytes) {
2495
+ truncatedBy = "bytes";
2496
+ if (outputLinesArr.length === 0) {
2497
+ const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
2498
+ outputLinesArr.unshift(truncatedLine);
2499
+ outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
2500
+ lastLinePartial = true;
2501
+ }
2502
+ break;
2503
+ }
2504
+ outputLinesArr.unshift(line);
2505
+ outputBytesCount += lineBytes;
2506
+ }
2507
+ if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
2508
+ truncatedBy = "lines";
2509
+ }
2510
+ const outputContent = outputLinesArr.join("\n");
2511
+ const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
2512
+ return {
2513
+ content: outputContent,
2514
+ truncated: true,
2515
+ truncatedBy,
2516
+ totalLines,
2517
+ totalBytes,
2518
+ outputLines: outputLinesArr.length,
2519
+ outputBytes: finalOutputBytes,
2520
+ lastLinePartial,
2521
+ firstLineExceedsLimit: false,
2522
+ maxLines,
2523
+ maxBytes
2524
+ };
2525
+ }
2526
+ function truncateStringToBytesFromEnd(str, maxBytes) {
2527
+ const buf = Buffer.from(str, "utf-8");
2528
+ if (buf.length <= maxBytes) {
2529
+ return str;
2530
+ }
2531
+ let start = buf.length - maxBytes;
2532
+ while (start < buf.length && (buf[start] & 192) === 128) {
2533
+ start++;
2534
+ }
2535
+ return buf.slice(start).toString("utf-8");
2536
+ }
2537
+ function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
2538
+ if (line.length <= maxChars) {
2539
+ return { text: line, wasTruncated: false };
2540
+ }
2541
+ return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
2542
+ }
2543
+
2544
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/read.ts
2545
+ function createReadTool(cwd, options) {
2546
+ const fs = getFileSystem();
2547
+ const rootSchema = buildRootParameterSchema(cwd, options);
2548
+ const readSchema = Type.Object({
2549
+ path: Type.String({ description: "Path to the file to read. Example: 'Scripts/PlayerController.cs' or 'Assets/Gen/config.json'." }),
2550
+ ...rootSchema ? { root: rootSchema } : {},
2551
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from, 1-indexed. Example: 201." })),
2552
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read. Example: 120." }))
2553
+ });
2554
+ const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
2555
+ return {
2556
+ name: "read",
2557
+ label: "read",
2558
+ description: `Read a file's contents. Use this before editing, and use offset/limit for large files. Example: read_file path='Scripts/PlayerController.cs' or read_file path='Data/config.json'. ${pathResolutionDescription} For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). When you need the full file, continue with a larger offset until complete.`,
2559
+ parameters: readSchema,
2560
+ execute: async (_toolCallId, params, signal) => {
2561
+ const { path, root, offset, limit } = params;
2562
+ const absolutePath = resolveReadPath(path, cwd, options, root);
2563
+ return new Promise(
2564
+ (resolve, reject) => {
2565
+ if (signal?.aborted) {
2566
+ reject(new Error("Operation aborted"));
2567
+ return;
2568
+ }
2569
+ let aborted = false;
2570
+ const onAbort = () => {
2571
+ aborted = true;
2572
+ reject(new Error("Operation aborted"));
2573
+ };
2574
+ if (signal) {
2575
+ signal.addEventListener("abort", onAbort, { once: true });
2576
+ }
2577
+ (async () => {
2578
+ try {
2579
+ await fs.access(absolutePath);
2580
+ if (aborted) return;
2581
+ const content = String(await fs.readFile(absolutePath, "utf-8"));
2582
+ const allLines = content.split("\n");
2583
+ const totalFileLines = allLines.length;
2584
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
2585
+ const startLineDisplay = startLine + 1;
2586
+ if (startLine >= allLines.length) {
2587
+ throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
2588
+ }
2589
+ let selectedContent;
2590
+ let userLimitedLines;
2591
+ if (limit !== void 0) {
2592
+ const endLine = Math.min(startLine + limit, allLines.length);
2593
+ selectedContent = allLines.slice(startLine, endLine).join("\n");
2594
+ userLimitedLines = endLine - startLine;
2595
+ } else {
2596
+ selectedContent = allLines.slice(startLine).join("\n");
2597
+ }
2598
+ const truncation = truncateHead(selectedContent);
2599
+ let outputText;
2600
+ let details;
2601
+ if (truncation.firstLineExceedsLimit) {
2602
+ const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], "utf-8"));
2603
+ outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit.]`;
2604
+ details = { truncation };
2605
+ } else if (truncation.truncated) {
2606
+ const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
2607
+ const nextOffset = endLineDisplay + 1;
2608
+ outputText = truncation.content;
2609
+ if (truncation.truncatedBy === "lines") {
2610
+ outputText += `
2611
+
2612
+ [Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
2613
+ } else {
2614
+ outputText += `
2615
+
2616
+ [Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;
2617
+ }
2618
+ details = { truncation };
2619
+ } else if (userLimitedLines !== void 0 && startLine + userLimitedLines < allLines.length) {
2620
+ const remaining = allLines.length - (startLine + userLimitedLines);
2621
+ const nextOffset = startLine + userLimitedLines + 1;
2622
+ outputText = truncation.content;
2623
+ outputText += `
2624
+
2625
+ [${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
2626
+ } else {
2627
+ outputText = truncation.content;
2628
+ }
2629
+ const textContent = [{ type: "text", text: outputText }];
2630
+ if (aborted) return;
2631
+ if (signal) {
2632
+ signal.removeEventListener("abort", onAbort);
2633
+ }
2634
+ resolve({ content: textContent, details });
2635
+ } catch (error) {
2636
+ if (signal) {
2637
+ signal.removeEventListener("abort", onAbort);
2638
+ }
2639
+ if (!aborted) {
2640
+ reject(error);
2641
+ }
2642
+ }
2643
+ })();
2644
+ }
2645
+ );
2646
+ }
2647
+ };
2648
+ }
2649
+
2650
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/write.ts
2651
+ function createWriteTool(cwd, options) {
2652
+ const fs = getFileSystem();
2653
+ const rootSchema = buildRootParameterSchema(cwd, options);
2654
+ const writeSchema = Type.Object({
2655
+ path: Type.String({ description: "Path to the file to write." }),
2656
+ ...rootSchema ? { root: rootSchema } : {},
2657
+ content: Type.String({ description: "Content to write to the file" })
2658
+ });
2659
+ const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
2660
+ return {
2661
+ name: "write",
2662
+ label: "write",
2663
+ description: `Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories. ${pathResolutionDescription}`,
2664
+ parameters: writeSchema,
2665
+ execute: async (_toolCallId, params, signal) => {
2666
+ const { path: filePath, root, content } = params;
2667
+ const absolutePath = resolveToCwd(filePath, cwd, options, root);
2668
+ const dir = fs.dirname(absolutePath);
2669
+ return new Promise(
2670
+ (resolve, reject) => {
2671
+ if (signal?.aborted) {
2672
+ reject(new Error("Operation aborted"));
2673
+ return;
2674
+ }
2675
+ let aborted = false;
2676
+ const onAbort = () => {
2677
+ aborted = true;
2678
+ reject(new Error("Operation aborted"));
2679
+ };
2680
+ if (signal) {
2681
+ signal.addEventListener("abort", onAbort, { once: true });
2682
+ }
2683
+ (async () => {
2684
+ try {
2685
+ await fs.mkdir(dir, { recursive: true });
2686
+ if (aborted) return;
2687
+ await fs.writeFile(absolutePath, content, "utf-8");
2688
+ if (aborted) return;
2689
+ if (signal) {
2690
+ signal.removeEventListener("abort", onAbort);
2691
+ }
2692
+ resolve({
2693
+ content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${filePath}` }],
2694
+ details: void 0
2695
+ });
2696
+ } catch (error) {
2697
+ if (signal) {
2698
+ signal.removeEventListener("abort", onAbort);
2699
+ }
2700
+ if (!aborted) {
2701
+ reject(error);
2702
+ }
2703
+ }
2704
+ })();
2705
+ }
2706
+ );
2707
+ }
2708
+ };
2709
+ }
2710
+
2711
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/edit-diff.ts
2712
+ function detectLineEnding(content) {
2713
+ const crlfIdx = content.indexOf("\r\n");
2714
+ const lfIdx = content.indexOf("\n");
2715
+ if (lfIdx === -1) return "\n";
2716
+ if (crlfIdx === -1) return "\n";
2717
+ return crlfIdx < lfIdx ? "\r\n" : "\n";
2718
+ }
2719
+ function normalizeToLF(text) {
2720
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2721
+ }
2722
+ function restoreLineEndings(text, ending) {
2723
+ return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
2724
+ }
2725
+ function normalizeForFuzzyMatch(text) {
2726
+ return text.split("\n").map((line) => line.trimEnd()).join("\n").replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-").replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
2727
+ }
2728
+ function fuzzyFindText(content, oldText) {
2729
+ const exactIndex = content.indexOf(oldText);
2730
+ if (exactIndex !== -1) {
2731
+ return {
2732
+ found: true,
2733
+ index: exactIndex,
2734
+ matchLength: oldText.length,
2735
+ usedFuzzyMatch: false,
2736
+ contentForReplacement: content
2737
+ };
2738
+ }
2739
+ const fuzzyContent = normalizeForFuzzyMatch(content);
2740
+ const fuzzyOldText = normalizeForFuzzyMatch(oldText);
2741
+ const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);
2742
+ if (fuzzyIndex === -1) {
2743
+ return {
2744
+ found: false,
2745
+ index: -1,
2746
+ matchLength: 0,
2747
+ usedFuzzyMatch: false,
2748
+ contentForReplacement: content
2749
+ };
2750
+ }
2751
+ return {
2752
+ found: true,
2753
+ index: fuzzyIndex,
2754
+ matchLength: fuzzyOldText.length,
2755
+ usedFuzzyMatch: true,
2756
+ contentForReplacement: fuzzyContent
2757
+ };
2758
+ }
2759
+ function stripBom(content) {
2760
+ return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
2761
+ }
2762
+ function generateDiffString(oldContent, newContent, _contextLines = 4) {
2763
+ const oldLines = oldContent.split("\n");
2764
+ const newLines = newContent.split("\n");
2765
+ let firstChangedLine;
2766
+ for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {
2767
+ if (oldLines[i] !== newLines[i]) {
2768
+ firstChangedLine = i + 1;
2769
+ break;
2770
+ }
2771
+ }
2772
+ const diff = `--- old
2773
+ +++ new
2774
+ @@ -1,${oldLines.length} +1,${newLines.length} @@
2775
+ [Diff: ${oldLines.length} lines -> ${newLines.length} lines]`;
2776
+ return { diff, firstChangedLine };
2777
+ }
2778
+
2779
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/edit.ts
2780
+ function createEditTool(cwd, options) {
2781
+ const fs = getFileSystem();
2782
+ const rootSchema = buildRootParameterSchema(cwd, options);
2783
+ const editSchema = Type.Object({
2784
+ path: Type.String({ description: "Path to the file to edit." }),
2785
+ ...rootSchema ? { root: rootSchema } : {},
2786
+ oldText: Type.String({ description: "Exact text to find and replace (must match exactly)" }),
2787
+ newText: Type.String({ description: "New text to replace the old text with" })
2788
+ });
2789
+ const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
2790
+ return {
2791
+ name: "edit",
2792
+ label: "edit",
2793
+ description: `Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits. ${pathResolutionDescription}`,
2794
+ parameters: editSchema,
2795
+ execute: async (_toolCallId, params, signal) => {
2796
+ const { path, root, oldText, newText } = params;
2797
+ const absolutePath = resolveToCwd(path, cwd, options, root);
2798
+ return new Promise((resolve, reject) => {
2799
+ if (signal?.aborted) {
2800
+ reject(new Error("Operation aborted"));
2801
+ return;
2802
+ }
2803
+ let aborted = false;
2804
+ const onAbort = () => {
2805
+ aborted = true;
2806
+ reject(new Error("Operation aborted"));
2807
+ };
2808
+ if (signal) {
2809
+ signal.addEventListener("abort", onAbort, { once: true });
2810
+ }
2811
+ (async () => {
2812
+ try {
2813
+ try {
2814
+ await fs.access(absolutePath);
2815
+ } catch {
2816
+ if (signal) signal.removeEventListener("abort", onAbort);
2817
+ reject(new Error(`File not found: ${path}`));
2818
+ return;
2819
+ }
2820
+ if (aborted) return;
2821
+ const rawContent = await fs.readFile(absolutePath, "utf-8");
2822
+ if (aborted) return;
2823
+ const { bom, text: content } = stripBom(rawContent);
2824
+ const originalEnding = detectLineEnding(content);
2825
+ const normalizedContent = normalizeToLF(content);
2826
+ const normalizedOldText = normalizeToLF(oldText);
2827
+ const normalizedNewText = normalizeToLF(newText);
2828
+ const matchResult = fuzzyFindText(normalizedContent, normalizedOldText);
2829
+ if (!matchResult.found) {
2830
+ if (signal) signal.removeEventListener("abort", onAbort);
2831
+ reject(
2832
+ new Error(
2833
+ `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`
2834
+ )
2835
+ );
2836
+ return;
2837
+ }
2838
+ const fuzzyContent = normalizedContent.replace(/\s+/g, " ");
2839
+ const fuzzyOldText = normalizedOldText.replace(/\s+/g, " ");
2840
+ const occurrences = fuzzyContent.split(fuzzyOldText).length - 1;
2841
+ if (occurrences > 1) {
2842
+ if (signal) signal.removeEventListener("abort", onAbort);
2843
+ reject(
2844
+ new Error(
2845
+ `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`
2846
+ )
2847
+ );
2848
+ return;
2849
+ }
2850
+ if (aborted) return;
2851
+ const baseContent = matchResult.contentForReplacement;
2852
+ const newContent = baseContent.substring(0, matchResult.index) + normalizedNewText + baseContent.substring(matchResult.index + matchResult.matchLength);
2853
+ if (baseContent === newContent) {
2854
+ if (signal) signal.removeEventListener("abort", onAbort);
2855
+ reject(
2856
+ new Error(
2857
+ `No changes made to ${path}. The replacement produced identical content.`
2858
+ )
2859
+ );
2860
+ return;
2861
+ }
2862
+ const finalContent = bom + restoreLineEndings(newContent, originalEnding);
2863
+ await fs.writeFile(absolutePath, finalContent, "utf-8");
2864
+ if (aborted) return;
2865
+ if (signal) {
2866
+ signal.removeEventListener("abort", onAbort);
2867
+ }
2868
+ const diffResult = generateDiffString(content, finalContent);
2869
+ resolve({
2870
+ content: [
2871
+ {
2872
+ type: "text",
2873
+ text: `Successfully replaced text in ${path}.`
2874
+ },
2875
+ { type: "text", text: diffResult.diff }
2876
+ ],
2877
+ details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine }
2878
+ });
2879
+ } catch (error) {
2880
+ if (signal) {
2881
+ signal.removeEventListener("abort", onAbort);
2882
+ }
2883
+ if (!aborted) {
2884
+ reject(error);
2885
+ }
2886
+ }
2887
+ })();
2888
+ });
2889
+ }
2890
+ };
2891
+ }
2892
+
2893
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/ls.ts
2894
+ var DEFAULT_LIMIT = 500;
2895
+ function createLsTool(cwd, options) {
2896
+ const fs = getFileSystem();
2897
+ const rootSchema = buildRootParameterSchema(cwd, options);
2898
+ const lsSchema = Type.Object({
2899
+ path: Type.Optional(Type.String({ description: "Directory to list (default: workspace root). Example: 'Scripts' or 'Data'." })),
2900
+ ...rootSchema ? { root: rootSchema } : {},
2901
+ limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)." }))
2902
+ });
2903
+ const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
2904
+ return {
2905
+ name: "ls",
2906
+ label: "ls",
2907
+ description: `List directory contents. Use this to inspect folders before reading or writing files. Example: list_directory path='Scripts' or list_directory path='Data'. ${pathResolutionDescription} Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
2908
+ parameters: lsSchema,
2909
+ execute: async (_toolCallId, params, signal) => {
2910
+ return new Promise((resolve, reject) => {
2911
+ if (signal?.aborted) {
2912
+ reject(new Error("Operation aborted"));
2913
+ return;
2914
+ }
2915
+ const onAbort = () => reject(new Error("Operation aborted"));
2916
+ signal?.addEventListener("abort", onAbort, { once: true });
2917
+ (async () => {
2918
+ try {
2919
+ const { path: dirPath, root, limit } = params;
2920
+ const targetPath = resolveToCwd(dirPath || ".", cwd, options, root);
2921
+ const effectiveLimit = limit ?? DEFAULT_LIMIT;
2922
+ if (!await fs.exists(targetPath)) {
2923
+ reject(new Error(`Path not found: ${targetPath}`));
2924
+ return;
2925
+ }
2926
+ let entries;
2927
+ try {
2928
+ entries = await fs.readdir(targetPath);
2929
+ } catch (e) {
2930
+ const message = String(e?.message || e || "Unknown error");
2931
+ if (/scandir/i.test(message) || /not a directory/i.test(message)) {
2932
+ reject(new Error(`Not a directory: ${targetPath}`));
2933
+ return;
2934
+ }
2935
+ reject(new Error(`Cannot read directory: ${message}`));
2936
+ return;
2937
+ }
2938
+ entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
2939
+ const results = [];
2940
+ let entryLimitReached = false;
2941
+ for (const entry of entries) {
2942
+ if (results.length >= effectiveLimit) {
2943
+ entryLimitReached = true;
2944
+ break;
2945
+ }
2946
+ const fullPath = fs.join(targetPath, entry);
2947
+ let suffix = "";
2948
+ try {
2949
+ const entryStat = await fs.stat(fullPath);
2950
+ if (entryStat.isDirectory) {
2951
+ suffix = "/";
2952
+ }
2953
+ } catch {
2954
+ continue;
2955
+ }
2956
+ results.push(entry + suffix);
2957
+ }
2958
+ signal?.removeEventListener("abort", onAbort);
2959
+ if (results.length === 0) {
2960
+ resolve({ content: [{ type: "text", text: "(empty directory)" }], details: void 0 });
2961
+ return;
2962
+ }
2963
+ const rawOutput = results.join("\n");
2964
+ const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
2965
+ let output = truncation.content;
2966
+ const details = {};
2967
+ const notices = [];
2968
+ if (entryLimitReached) {
2969
+ notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
2970
+ details.entryLimitReached = effectiveLimit;
2971
+ }
2972
+ if (truncation.truncated) {
2973
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
2974
+ details.truncation = truncation;
2975
+ }
2976
+ if (notices.length > 0) {
2977
+ output += `
2978
+
2979
+ [${notices.join(". ")}]`;
2980
+ }
2981
+ resolve({
2982
+ content: [{ type: "text", text: output }],
2983
+ details: Object.keys(details).length > 0 ? details : void 0
2984
+ });
2985
+ } catch (e) {
2986
+ signal?.removeEventListener("abort", onAbort);
2987
+ reject(e);
2988
+ }
2989
+ })();
2990
+ });
2991
+ }
2992
+ };
2993
+ }
2994
+
2995
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/grep.ts
2996
+ var DEFAULT_LIMIT2 = 100;
2997
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "__pycache__", ".svn", ".hg"]);
2998
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2999
+ ".png",
3000
+ ".jpg",
3001
+ ".jpeg",
3002
+ ".gif",
3003
+ ".bmp",
3004
+ ".ico",
3005
+ ".webp",
3006
+ ".mp3",
3007
+ ".mp4",
3008
+ ".wav",
3009
+ ".avi",
3010
+ ".mov",
3011
+ ".zip",
3012
+ ".tar",
3013
+ ".gz",
3014
+ ".bz2",
3015
+ ".7z",
3016
+ ".rar",
3017
+ ".exe",
3018
+ ".dll",
3019
+ ".so",
3020
+ ".dylib",
3021
+ ".bin",
3022
+ ".pdf",
3023
+ ".doc",
3024
+ ".docx",
3025
+ ".xls",
3026
+ ".xlsx",
3027
+ ".woff",
3028
+ ".woff2",
3029
+ ".ttf",
3030
+ ".eot"
3031
+ ]);
3032
+ function getNativeFileBridge() {
3033
+ try {
3034
+ return typeof CS !== "undefined" && CS?.Pie?.PieFileBridge ? CS.Pie.PieFileBridge : null;
3035
+ } catch {
3036
+ return null;
3037
+ }
3038
+ }
3039
+ function getFileCompletionStore() {
3040
+ const g = globalThis;
3041
+ if (!g.__pieFileCompletionStore) {
3042
+ g.__pieFileCompletionStore = {
3043
+ completions: /* @__PURE__ */ new Map(),
3044
+ waiters: /* @__PURE__ */ new Map()
3045
+ };
3046
+ }
3047
+ if (!g._pieFileComplete) {
3048
+ g._pieFileComplete = (requestId, resultJson, error) => {
3049
+ const store = getFileCompletionStore();
3050
+ const payload = { resultJson, error };
3051
+ store.completions.set(requestId, payload);
3052
+ const waiter = store.waiters.get(requestId);
3053
+ if (waiter) {
3054
+ store.waiters.delete(requestId);
3055
+ waiter(payload);
3056
+ }
3057
+ };
3058
+ }
3059
+ return g.__pieFileCompletionStore;
3060
+ }
3061
+ function waitForFileCompletion(requestId) {
3062
+ const store = getFileCompletionStore();
3063
+ const existing = store.completions.get(requestId);
3064
+ if (existing) {
3065
+ return Promise.resolve(existing);
3066
+ }
3067
+ return new Promise((resolve) => {
3068
+ store.waiters.set(requestId, resolve);
3069
+ });
3070
+ }
3071
+ async function runNativeGrep(searchPath, pattern, globPattern, ignoreCase, literal, contextLines, limit, signal) {
3072
+ const bridge = getNativeFileBridge();
3073
+ if (!bridge) {
3074
+ throw new Error("CS.Pie.PieFileBridge not available");
3075
+ }
3076
+ const requestId = bridge.StartGrep(
3077
+ searchPath,
3078
+ pattern,
3079
+ globPattern ?? "",
3080
+ !!ignoreCase,
3081
+ !!literal,
3082
+ contextLines,
3083
+ limit
3084
+ );
3085
+ const onAbort = () => bridge.CancelRequest(requestId);
3086
+ signal?.addEventListener("abort", onAbort, { once: true });
3087
+ try {
3088
+ if (signal?.aborted) {
3089
+ bridge.CancelRequest(requestId);
3090
+ throw new Error("Operation aborted");
3091
+ }
3092
+ const completion = await waitForFileCompletion(requestId);
3093
+ if (completion.error) {
3094
+ throw new Error(completion.error);
3095
+ }
3096
+ if (!completion.resultJson) {
3097
+ return {};
3098
+ }
3099
+ return JSON.parse(completion.resultJson);
3100
+ } finally {
3101
+ const store = getFileCompletionStore();
3102
+ store.completions.delete(requestId);
3103
+ store.waiters.delete(requestId);
3104
+ signal?.removeEventListener("abort", onAbort);
3105
+ bridge.ReleaseRequest(requestId);
3106
+ }
3107
+ }
3108
+ async function collectFiles(dirPath, globPattern, fs) {
3109
+ const files = [];
3110
+ const queue = [dirPath];
3111
+ while (queue.length > 0) {
3112
+ const dir = queue.pop();
3113
+ let entries;
3114
+ try {
3115
+ entries = await fs.readdir(dir);
3116
+ } catch {
3117
+ continue;
3118
+ }
3119
+ for (const entry of entries) {
3120
+ const fullPath = fs.join(dir, entry);
3121
+ let stat;
3122
+ try {
3123
+ stat = await fs.stat(fullPath);
3124
+ } catch {
3125
+ continue;
3126
+ }
3127
+ if (stat.isDirectory) {
3128
+ if (!SKIP_DIRS.has(entry)) {
3129
+ queue.push(fullPath);
3130
+ }
3131
+ } else if (stat.isFile) {
3132
+ const ext = fs.extname(entry).toLowerCase();
3133
+ if (BINARY_EXTENSIONS.has(ext)) continue;
3134
+ if (globPattern) {
3135
+ if (globPattern.startsWith("*.")) {
3136
+ const globExt = globPattern.slice(1);
3137
+ if (ext !== globExt) continue;
3138
+ } else if (globPattern.startsWith("**/*.")) {
3139
+ const globExt = globPattern.slice(3);
3140
+ if (ext !== globExt) continue;
3141
+ }
3142
+ }
3143
+ files.push(fullPath);
3144
+ }
3145
+ }
3146
+ }
3147
+ return files;
3148
+ }
3149
+ function createGrepTool(cwd, options) {
3150
+ const fs = getFileSystem();
3151
+ const platform = detectPlatform();
3152
+ const rootSchema = buildRootParameterSchema(cwd, options);
3153
+ const grepSchema = Type.Object({
3154
+ pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
3155
+ path: Type.Optional(Type.String({ description: "Directory or file to search (default: workspace root)" })),
3156
+ ...rootSchema ? { root: rootSchema } : {},
3157
+ glob: Type.Optional(Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.js'" })),
3158
+ ignoreCase: Type.Optional(Type.Boolean({ description: "Case-insensitive search (default: false)" })),
3159
+ literal: Type.Optional(
3160
+ Type.Boolean({ description: "Treat pattern as literal string instead of regex (default: false)" })
3161
+ ),
3162
+ context: Type.Optional(
3163
+ Type.Number({ description: "Number of lines to show before and after each match (default: 0)" })
3164
+ ),
3165
+ limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" }))
3166
+ });
3167
+ const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
3168
+ return {
3169
+ name: "grep",
3170
+ label: "grep",
3171
+ description: `Search file contents for a pattern. ${pathResolutionDescription} Returns matching lines with file paths and line numbers. Output is truncated to ${DEFAULT_LIMIT2} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
3172
+ parameters: grepSchema,
3173
+ execute: async (_toolCallId, params, signal) => {
3174
+ const {
3175
+ pattern,
3176
+ path: searchDir,
3177
+ root,
3178
+ glob: globPattern,
3179
+ ignoreCase,
3180
+ literal,
3181
+ context: contextLines,
3182
+ limit
3183
+ } = params;
3184
+ return new Promise((resolve, reject) => {
3185
+ if (signal?.aborted) {
3186
+ reject(new Error("Operation aborted"));
3187
+ return;
3188
+ }
3189
+ const onAbort = () => reject(new Error("Operation aborted"));
3190
+ signal?.addEventListener("abort", onAbort, { once: true });
3191
+ (async () => {
3192
+ try {
3193
+ const searchPath = resolveToCwd(searchDir || ".", cwd, options, root);
3194
+ const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT2);
3195
+ const ctx = contextLines && contextLines > 0 ? contextLines : 0;
3196
+ let isDir;
3197
+ try {
3198
+ const stat = await fs.stat(searchPath);
3199
+ isDir = stat.isDirectory;
3200
+ } catch {
3201
+ reject(new Error(`Path not found: ${searchPath}`));
3202
+ return;
3203
+ }
3204
+ let regex;
3205
+ try {
3206
+ const flags = ignoreCase ? "gi" : "g";
3207
+ if (literal) {
3208
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3209
+ regex = new RegExp(escaped, flags);
3210
+ } else {
3211
+ regex = new RegExp(pattern, flags);
3212
+ }
3213
+ } catch (e) {
3214
+ reject(new Error(`Invalid regex pattern: ${e.message}`));
3215
+ return;
3216
+ }
3217
+ let outputLines = [];
3218
+ let matchCount = 0;
3219
+ let matchLimitReached = false;
3220
+ let linesTruncated = false;
3221
+ if (platform === "puerts" && getNativeFileBridge()) {
3222
+ const nativeResult = await runNativeGrep(
3223
+ searchPath,
3224
+ pattern,
3225
+ globPattern,
3226
+ !!ignoreCase,
3227
+ !!literal,
3228
+ ctx,
3229
+ effectiveLimit,
3230
+ signal
3231
+ );
3232
+ outputLines = nativeResult.Lines ?? [];
3233
+ matchCount = nativeResult.MatchCount ?? 0;
3234
+ matchLimitReached = !!nativeResult.MatchLimitReached;
3235
+ linesTruncated = !!nativeResult.LinesTruncated;
3236
+ globalThis.pieBridge?.log?.(
3237
+ "info",
3238
+ `[grep] path=${searchPath} pattern=${pattern} matches=${matchCount} files=${nativeResult.FilesScanned ?? 0}`
3239
+ );
3240
+ } else {
3241
+ let filePaths;
3242
+ if (isDir) {
3243
+ filePaths = await collectFiles(searchPath, globPattern, fs);
3244
+ filePaths.sort();
3245
+ } else {
3246
+ filePaths = [searchPath];
3247
+ }
3248
+ for (const filePath of filePaths) {
3249
+ if (matchCount >= effectiveLimit) {
3250
+ matchLimitReached = true;
3251
+ break;
3252
+ }
3253
+ if (signal?.aborted) {
3254
+ reject(new Error("Operation aborted"));
3255
+ return;
3256
+ }
3257
+ let content;
3258
+ try {
3259
+ content = await fs.readFile(filePath, "utf-8");
3260
+ } catch {
3261
+ continue;
3262
+ }
3263
+ const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
3264
+ const relativePath = isDir ? fs.relative(searchPath, filePath) : fs.basename(filePath);
3265
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
3266
+ if (matchCount >= effectiveLimit) {
3267
+ matchLimitReached = true;
3268
+ break;
3269
+ }
3270
+ regex.lastIndex = 0;
3271
+ if (regex.test(lines[lineIdx])) {
3272
+ matchCount++;
3273
+ const start = ctx > 0 ? Math.max(0, lineIdx - ctx) : lineIdx;
3274
+ const end = ctx > 0 ? Math.min(lines.length - 1, lineIdx + ctx) : lineIdx;
3275
+ for (let c = start; c <= end; c++) {
3276
+ const lineText = lines[c];
3277
+ const { text: truncatedText, wasTruncated } = truncateLine(lineText);
3278
+ if (wasTruncated) linesTruncated = true;
3279
+ const currentLineNum = c + 1;
3280
+ if (c === lineIdx) {
3281
+ outputLines.push(`${relativePath}:${currentLineNum}: ${truncatedText}`);
3282
+ } else {
3283
+ outputLines.push(`${relativePath}-${currentLineNum}- ${truncatedText}`);
3284
+ }
3285
+ }
3286
+ }
3287
+ }
3288
+ }
3289
+ }
3290
+ signal?.removeEventListener("abort", onAbort);
3291
+ if (matchCount === 0) {
3292
+ resolve({
3293
+ content: [{ type: "text", text: "No matches found" }],
3294
+ details: { noMatches: true }
3295
+ });
3296
+ return;
3297
+ }
3298
+ const rawOutput = outputLines.join("\n");
3299
+ const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
3300
+ let output = truncation.content;
3301
+ const details = {};
3302
+ const notices = [];
3303
+ if (matchLimitReached) {
3304
+ notices.push(
3305
+ `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`
3306
+ );
3307
+ details.matchLimitReached = effectiveLimit;
3308
+ }
3309
+ if (truncation.truncated) {
3310
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
3311
+ details.truncation = truncation;
3312
+ }
3313
+ if (linesTruncated) {
3314
+ notices.push(
3315
+ `Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`
3316
+ );
3317
+ details.linesTruncated = true;
3318
+ }
3319
+ if (notices.length > 0) {
3320
+ output += `
3321
+
3322
+ [${notices.join(". ")}]`;
3323
+ }
3324
+ resolve({
3325
+ content: [{ type: "text", text: output }],
3326
+ details: Object.keys(details).length > 0 ? details : void 0
3327
+ });
3328
+ } catch (err) {
3329
+ signal?.removeEventListener("abort", onAbort);
3330
+ reject(err);
3331
+ }
3332
+ })();
3333
+ });
3334
+ }
3335
+ };
3336
+ }
3337
+
3338
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/find.ts
3339
+ var DEFAULT_LIMIT3 = 1e3;
3340
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__", ".svn", ".hg"]);
3341
+ function globToRegex(pattern) {
3342
+ let regexPattern = pattern.replace(/\*\*/g, "__GLOBSTAR__").replace(/\*/g, "__STAR__").replace(/\?/g, "__QMARK__").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/__GLOBSTAR__/g, ".*").replace(/__STAR__/g, "[^/]*").replace(/__QMARK__/g, ".");
3343
+ regexPattern = regexPattern.replace(/\//g, "[/\\\\]");
3344
+ return new RegExp("^" + regexPattern + "$", "i");
3345
+ }
3346
+ function matchesGlob(filePath, pattern) {
3347
+ const regex = globToRegex(pattern);
3348
+ return regex.test(filePath);
3349
+ }
3350
+ function getNativeFileBridge2() {
3351
+ try {
3352
+ return typeof CS !== "undefined" && CS?.Pie?.PieFileBridge ? CS.Pie.PieFileBridge : null;
3353
+ } catch {
3354
+ return null;
3355
+ }
3356
+ }
3357
+ function getFileCompletionStore2() {
3358
+ const g = globalThis;
3359
+ if (!g.__pieFileCompletionStore) {
3360
+ g.__pieFileCompletionStore = {
3361
+ completions: /* @__PURE__ */ new Map(),
3362
+ waiters: /* @__PURE__ */ new Map()
3363
+ };
3364
+ }
3365
+ if (!g._pieFileComplete) {
3366
+ g._pieFileComplete = (requestId, resultJson, error) => {
3367
+ const store = getFileCompletionStore2();
3368
+ const payload = { resultJson, error };
3369
+ store.completions.set(requestId, payload);
3370
+ const waiter = store.waiters.get(requestId);
3371
+ if (waiter) {
3372
+ store.waiters.delete(requestId);
3373
+ waiter(payload);
3374
+ }
3375
+ };
3376
+ }
3377
+ return g.__pieFileCompletionStore;
3378
+ }
3379
+ function waitForFileCompletion2(requestId) {
3380
+ const store = getFileCompletionStore2();
3381
+ const existing = store.completions.get(requestId);
3382
+ if (existing) {
3383
+ return Promise.resolve(existing);
3384
+ }
3385
+ return new Promise((resolve) => {
3386
+ store.waiters.set(requestId, resolve);
3387
+ });
3388
+ }
3389
+ async function runNativeFind(searchPath, pattern, limit, signal) {
3390
+ const bridge = getNativeFileBridge2();
3391
+ if (!bridge) {
3392
+ throw new Error("CS.Pie.PieFileBridge not available");
3393
+ }
3394
+ const requestId = bridge.StartFind(searchPath, pattern, limit);
3395
+ const onAbort = () => bridge.CancelRequest(requestId);
3396
+ signal?.addEventListener("abort", onAbort, { once: true });
3397
+ try {
3398
+ if (signal?.aborted) {
3399
+ bridge.CancelRequest(requestId);
3400
+ throw new Error("Operation aborted");
3401
+ }
3402
+ const completion = await waitForFileCompletion2(requestId);
3403
+ if (completion.error) {
3404
+ throw new Error(completion.error);
3405
+ }
3406
+ if (!completion.resultJson) {
3407
+ return {};
3408
+ }
3409
+ return JSON.parse(completion.resultJson);
3410
+ } finally {
3411
+ const store = getFileCompletionStore2();
3412
+ store.completions.delete(requestId);
3413
+ store.waiters.delete(requestId);
3414
+ signal?.removeEventListener("abort", onAbort);
3415
+ bridge.ReleaseRequest(requestId);
3416
+ }
3417
+ }
3418
+ async function globFiles(rootPath, pattern, fs, limit, signal) {
3419
+ const results = [];
3420
+ const queue = [{ dir: rootPath, relPrefix: "" }];
3421
+ while (queue.length > 0 && results.length < limit) {
3422
+ if (signal?.aborted) {
3423
+ throw new Error("Operation aborted");
3424
+ }
3425
+ const { dir, relPrefix } = queue.shift();
3426
+ let entries;
3427
+ try {
3428
+ entries = await fs.readdir(dir);
3429
+ } catch {
3430
+ continue;
3431
+ }
3432
+ for (const entry of entries) {
3433
+ if (results.length >= limit) break;
3434
+ const fullPath = fs.join(dir, entry);
3435
+ const relPath = relPrefix ? relPrefix + "/" + entry : entry;
3436
+ let stat;
3437
+ try {
3438
+ stat = await fs.stat(fullPath);
3439
+ } catch {
3440
+ continue;
3441
+ }
3442
+ if (stat.isDirectory) {
3443
+ if (SKIP_DIRS2.has(entry)) continue;
3444
+ queue.push({ dir: fullPath, relPrefix: relPath });
3445
+ } else if (stat.isFile) {
3446
+ if (matchesGlob(relPath, pattern) || matchesGlob(entry, pattern)) {
3447
+ results.push(relPath);
3448
+ }
3449
+ }
3450
+ }
3451
+ }
3452
+ return results;
3453
+ }
3454
+ function createFindTool(cwd, options) {
3455
+ const fs = getFileSystem();
3456
+ const platform = detectPlatform();
3457
+ const rootSchema = buildRootParameterSchema(cwd, options);
3458
+ const findSchema = Type.Object({
3459
+ pattern: Type.String({
3460
+ description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'"
3461
+ }),
3462
+ path: Type.Optional(Type.String({ description: "Directory to search in (default: workspace root)" })),
3463
+ ...rootSchema ? { root: rootSchema } : {},
3464
+ limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" }))
3465
+ });
3466
+ const pathResolutionDescription = buildPathResolutionDescription(cwd, options);
3467
+ return {
3468
+ name: "find",
3469
+ label: "find",
3470
+ description: `Search for files by glob pattern. ${pathResolutionDescription} Returns matching file paths relative to the search directory. Output is truncated to ${DEFAULT_LIMIT3} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
3471
+ parameters: findSchema,
3472
+ execute: async (_toolCallId, params, signal) => {
3473
+ const { pattern, path: searchDir, root, limit } = params;
3474
+ return new Promise((resolve, reject) => {
3475
+ if (signal?.aborted) {
3476
+ reject(new Error("Operation aborted"));
3477
+ return;
3478
+ }
3479
+ const onAbort = () => reject(new Error("Operation aborted"));
3480
+ signal?.addEventListener("abort", onAbort, { once: true });
3481
+ (async () => {
3482
+ try {
3483
+ const searchPath = resolveToCwd(searchDir || ".", cwd, options, root);
3484
+ const effectiveLimit = limit ?? DEFAULT_LIMIT3;
3485
+ if (!await fs.exists(searchPath)) {
3486
+ reject(new Error(`Path not found: ${searchPath}`));
3487
+ return;
3488
+ }
3489
+ let results;
3490
+ if (platform === "puerts" && getNativeFileBridge2()) {
3491
+ const nativeResult = await runNativeFind(searchPath, pattern, effectiveLimit, signal);
3492
+ results = nativeResult.Results ?? [];
3493
+ globalThis.pieBridge?.log?.(
3494
+ "info",
3495
+ `[find] path=${searchPath} pattern=${pattern} matches=${results.length} dirs=${nativeResult.ScannedDirectories ?? 0} files=${nativeResult.ScannedFiles ?? 0}`
3496
+ );
3497
+ } else if (platform === "node") {
3498
+ try {
3499
+ const _require = globalThis.require || __require;
3500
+ const { globSync } = _require("glob");
3501
+ results = globSync(pattern, {
3502
+ cwd: searchPath,
3503
+ ignore: ["**/node_modules/**", "**/.git/**"],
3504
+ nodir: false,
3505
+ absolute: false,
3506
+ dot: true
3507
+ }).slice(0, effectiveLimit);
3508
+ } catch {
3509
+ results = await globFiles(searchPath, pattern, fs, effectiveLimit, signal);
3510
+ }
3511
+ } else {
3512
+ results = await globFiles(searchPath, pattern, fs, effectiveLimit, signal);
3513
+ }
3514
+ signal?.removeEventListener("abort", onAbort);
3515
+ if (results.length === 0) {
3516
+ resolve({
3517
+ content: [{ type: "text", text: "No files found matching pattern" }],
3518
+ details: { noMatches: true }
3519
+ });
3520
+ return;
3521
+ }
3522
+ const resultLimitReached = results.length >= effectiveLimit;
3523
+ const rawOutput = results.join("\n");
3524
+ const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
3525
+ let resultOutput = truncation.content;
3526
+ const details = {};
3527
+ const notices = [];
3528
+ if (resultLimitReached) {
3529
+ notices.push(`${effectiveLimit} results limit reached`);
3530
+ details.resultLimitReached = effectiveLimit;
3531
+ }
3532
+ if (truncation.truncated) {
3533
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
3534
+ details.truncation = truncation;
3535
+ }
3536
+ if (notices.length > 0) {
3537
+ resultOutput += "\n\n[" + notices.join(". ") + "]";
3538
+ }
3539
+ resolve({
3540
+ content: [{ type: "text", text: resultOutput }],
3541
+ details: Object.keys(details).length > 0 ? details : void 0
3542
+ });
3543
+ } catch (e) {
3544
+ signal?.removeEventListener("abort", onAbort);
3545
+ reject(e);
3546
+ }
3547
+ })();
3548
+ });
3549
+ }
3550
+ };
3551
+ }
3552
+
3553
+ // ../../packages/shared-headless-capabilities/src/builtin/fs/index.ts
3554
+ function createSharedFileSystemTools(sandboxRoot, options) {
3555
+ return [
3556
+ createReadTool(sandboxRoot, options),
3557
+ createWriteTool(sandboxRoot, options),
3558
+ createEditTool(sandboxRoot, options),
3559
+ createLsTool(sandboxRoot, options),
3560
+ createGrepTool(sandboxRoot, options),
3561
+ createFindTool(sandboxRoot, options)
3562
+ ];
3563
+ }
3564
+ function createSharedReadOnlyTools(sandboxRoot, options) {
3565
+ return [
3566
+ createReadTool(sandboxRoot, options),
3567
+ createLsTool(sandboxRoot, options),
3568
+ createGrepTool(sandboxRoot, options),
3569
+ createFindTool(sandboxRoot, options)
3570
+ ];
3571
+ }
3572
+ var createFileSystemTools = createSharedFileSystemTools;
3573
+ var createReadOnlyTools = createSharedReadOnlyTools;
3574
+
3575
+ // ../../packages/shared-headless-capabilities/src/index.ts
3576
+ function getSharedCapabilityPromptAdditionsForToolNames(toolNames) {
3577
+ const definitions = [
3578
+ createManageTodoCapability(),
3579
+ createAskUserCapability()
3580
+ ];
3581
+ return collectSharedCapabilityPromptAdditions(
3582
+ definitions,
3583
+ toolNames
3584
+ );
3585
+ }
3586
+
3587
+ export {
3588
+ defineSharedCapability,
3589
+ collectSharedCapabilityPromptAdditions,
3590
+ PLAN_MODE_TOOL_NAMES,
3591
+ extractPlanTodoItems,
3592
+ isPlanModeSafeCommand,
3593
+ extractDoneSteps,
3594
+ markCompletedPlanSteps,
3595
+ createPlanCapability,
3596
+ ReadSkillParamsSchema,
3597
+ createReadSkillCapability,
3598
+ formatSkillsForPrompt,
3599
+ collectLastAssistantText,
3600
+ createSubagentCapability,
3601
+ TodoItemSchema,
3602
+ ManageTodoListParamsSchema,
3603
+ cloneTodos,
3604
+ normalizeStatus,
3605
+ normalizeTodos,
3606
+ validateTodos,
3607
+ getTodoValidation,
3608
+ getTodoStats,
3609
+ buildTodoSuccessMessage,
3610
+ executeManageTodoList,
3611
+ restoreTodoState,
3612
+ restoreTodosFromMessages,
3613
+ createTodoStore,
3614
+ createManageTodoCapability,
3615
+ calculateTotalTokens,
3616
+ findFirstKeptUserEntryId,
3617
+ createCompactionSummary,
3618
+ createSessionManager,
3619
+ registerInteractionHandler,
3620
+ hasInteractionHandler,
3621
+ requestInteraction,
3622
+ AskUserParamsSchema,
3623
+ AskUserMultiParamsSchema,
3624
+ createAskUserCapability,
3625
+ getAvailableRoots,
3626
+ getDefaultRootName,
3627
+ resolveInSandbox,
3628
+ resolveToCwd,
3629
+ resolveReadPath,
3630
+ buildRootParameterSchema,
3631
+ buildPathResolutionDescription,
3632
+ DEFAULT_MAX_LINES,
3633
+ DEFAULT_MAX_BYTES,
3634
+ GREP_MAX_LINE_LENGTH,
3635
+ formatSize,
3636
+ truncateHead,
3637
+ truncateTail,
3638
+ truncateLine,
3639
+ createReadTool,
3640
+ createWriteTool,
3641
+ detectLineEnding,
3642
+ normalizeToLF,
3643
+ restoreLineEndings,
3644
+ normalizeForFuzzyMatch,
3645
+ fuzzyFindText,
3646
+ stripBom,
3647
+ generateDiffString,
3648
+ createEditTool,
3649
+ createLsTool,
3650
+ createGrepTool,
3651
+ createFindTool,
3652
+ createSharedFileSystemTools,
3653
+ createSharedReadOnlyTools,
3654
+ createFileSystemTools,
3655
+ createReadOnlyTools,
3656
+ getSharedCapabilityPromptAdditionsForToolNames
3657
+ };