@strayl/agent 0.1.2 → 0.1.4

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.
Files changed (58) hide show
  1. package/dist/agent.js +6 -7
  2. package/package.json +5 -1
  3. package/skills/api-creation/SKILL.md +631 -0
  4. package/skills/authentication/SKILL.md +294 -0
  5. package/skills/frontend-design/SKILL.md +108 -0
  6. package/skills/landing-creation/SKILL.md +125 -0
  7. package/skills/reference/SKILL.md +149 -0
  8. package/skills/web-application-creation/SKILL.md +231 -0
  9. package/src/agent.ts +0 -465
  10. package/src/checkpoints/manager.ts +0 -112
  11. package/src/context/manager.ts +0 -185
  12. package/src/context/summarizer.ts +0 -104
  13. package/src/context/trim.ts +0 -55
  14. package/src/emitter.ts +0 -14
  15. package/src/hitl/manager.ts +0 -77
  16. package/src/hitl/transport.ts +0 -13
  17. package/src/index.ts +0 -116
  18. package/src/llm/client.ts +0 -276
  19. package/src/llm/gemini-native.ts +0 -307
  20. package/src/llm/models.ts +0 -64
  21. package/src/middleware/compose.ts +0 -24
  22. package/src/middleware/credential-scrubbing.ts +0 -31
  23. package/src/middleware/forbidden-packages.ts +0 -107
  24. package/src/middleware/plan-mode.ts +0 -143
  25. package/src/middleware/prompt-caching.ts +0 -21
  26. package/src/middleware/tool-compression.ts +0 -25
  27. package/src/middleware/tool-filter.ts +0 -13
  28. package/src/prompts/implementation-mode.md +0 -16
  29. package/src/prompts/plan-mode.md +0 -51
  30. package/src/prompts/system.ts +0 -173
  31. package/src/skills/loader.ts +0 -53
  32. package/src/stdin-listener.ts +0 -61
  33. package/src/subagents/definitions.ts +0 -72
  34. package/src/subagents/manager.ts +0 -161
  35. package/src/todos/manager.ts +0 -61
  36. package/src/tools/builtin/delete.ts +0 -29
  37. package/src/tools/builtin/edit.ts +0 -74
  38. package/src/tools/builtin/exec.ts +0 -216
  39. package/src/tools/builtin/glob.ts +0 -104
  40. package/src/tools/builtin/grep.ts +0 -115
  41. package/src/tools/builtin/ls.ts +0 -54
  42. package/src/tools/builtin/move.ts +0 -31
  43. package/src/tools/builtin/read.ts +0 -69
  44. package/src/tools/builtin/write.ts +0 -42
  45. package/src/tools/executor.ts +0 -51
  46. package/src/tools/external/database.ts +0 -285
  47. package/src/tools/external/enter-plan-mode.ts +0 -34
  48. package/src/tools/external/generate-image.ts +0 -110
  49. package/src/tools/external/hitl-tools.ts +0 -118
  50. package/src/tools/external/preview.ts +0 -28
  51. package/src/tools/external/proxy-fetch.ts +0 -51
  52. package/src/tools/external/task.ts +0 -38
  53. package/src/tools/external/wait.ts +0 -20
  54. package/src/tools/external/web-fetch.ts +0 -57
  55. package/src/tools/external/web-search.ts +0 -61
  56. package/src/tools/registry.ts +0 -36
  57. package/src/tools/zod-to-json-schema.ts +0 -86
  58. package/src/types.ts +0 -151
@@ -1,61 +0,0 @@
1
- import { createInterface } from "node:readline";
2
-
3
- export type StdinCommand =
4
- | { type: "inject"; text: string; images?: Array<{ base64: string; contentType: string }> }
5
- | { type: "cancel" }
6
- | { type: "hitl-response"; id: string; decision: string; data?: unknown }
7
- | { type: "rollback"; checkpoint_id?: string; iteration?: number }
8
- | { type: "confirm-plan" };
9
-
10
- /**
11
- * Listens to stdin for JSON-per-line commands.
12
- * Non-blocking — drains queued commands when polled.
13
- */
14
- export class StdinListener {
15
- private queue: StdinCommand[] = [];
16
- private cancelled = false;
17
- private rl: ReturnType<typeof createInterface> | null = null;
18
-
19
- start(): void {
20
- // Always listen — we receive commands both via pipe and PTY
21
- this.rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
22
-
23
- this.rl.on("line", (line) => {
24
- const trimmed = line.trim();
25
- if (!trimmed) return;
26
-
27
- try {
28
- const cmd = JSON.parse(trimmed);
29
- if (cmd.type === "cancel") {
30
- this.cancelled = true;
31
- }
32
- this.queue.push(cmd as StdinCommand);
33
- } catch {
34
- // Not valid JSON — ignore
35
- }
36
- });
37
-
38
- this.rl.on("close", () => {
39
- // stdin closed — don't treat as cancel.
40
- // Only explicit {"type":"cancel"} cancels the agent.
41
- // If parent dies, the agent will finish its current iteration
42
- // and exit naturally (no more stdin commands).
43
- });
44
- }
45
-
46
- /** Drain all queued commands since last call */
47
- drain(): StdinCommand[] {
48
- const cmds = this.queue;
49
- this.queue = [];
50
- return cmds;
51
- }
52
-
53
- isCancelled(): boolean {
54
- return this.cancelled;
55
- }
56
-
57
- stop(): void {
58
- this.rl?.close();
59
- this.rl = null;
60
- }
61
- }
@@ -1,72 +0,0 @@
1
- export interface SubAgentDef {
2
- name: string;
3
- description: string;
4
- systemPrompt: string;
5
- allowedTools: string[];
6
- }
7
-
8
- export const CODE_EXPLORER: SubAgentDef = {
9
- name: "code-explorer",
10
- description: "Fast agent for exploring the project codebase. Read-only operations only.",
11
- systemPrompt: `You are a code exploration assistant. Your job is to quickly find and analyze code in the project.
12
-
13
- You have access to: ls, read_file, grep, glob (read-only tools only).
14
-
15
- When exploring:
16
- 1. Start with ls to understand project structure
17
- 2. Use glob to find files matching patterns
18
- 3. Use grep to search for specific code patterns
19
- 4. Use read_file to examine file contents
20
-
21
- Output format:
22
- - **Files Found**: List relevant files discovered
23
- - **Key Code**: Important code snippets (with file paths and line numbers)
24
- - **Summary**: Brief analysis answering the original question`,
25
- allowedTools: ["ls", "read_file", "grep", "glob"],
26
- };
27
-
28
- export const WEB_RESEARCHER: SubAgentDef = {
29
- name: "web-researcher",
30
- description: "Research technical questions on the web. Fetches docs and API references.",
31
- systemPrompt: `You are a web research assistant. Your job is to find accurate technical information.
32
-
33
- You have access to: web_search, web_fetch
34
-
35
- Workflow:
36
- 1. Use web_search to find relevant pages
37
- 2. Use web_fetch to read the most promising results
38
- 3. Prefer official documentation over blog posts
39
- 4. Verify edge runtime compatibility when relevant (no Node.js APIs, no TCP, no native bindings)
40
-
41
- Output format:
42
- - **Answer**: Direct answer to the question
43
- - **Sources**: URLs consulted
44
- - **Code Example**: If applicable, working code snippet
45
- - **Edge Compatibility**: Any runtime restrictions noted`,
46
- allowedTools: ["web_search", "web_fetch"],
47
- };
48
-
49
- export const GENERAL_PURPOSE: SubAgentDef = {
50
- name: "general-purpose",
51
- description: "Multi-step tasks combining file exploration and web research.",
52
- systemPrompt: `You assist with multi-step tasks that may require both code exploration and web research.
53
-
54
- You have access to: ls, read_file, grep, glob, web_search, web_fetch
55
-
56
- Rules:
57
- - Always use relative paths
58
- - Don't modify files unless explicitly asked
59
- - Be thorough but concise
60
-
61
- Output format:
62
- - **What was found**: Key discoveries
63
- - **Relevant code**: File paths and snippets
64
- - **Recommendation**: Suggested approach or answer`,
65
- allowedTools: ["ls", "read_file", "grep", "glob", "web_search", "web_fetch"],
66
- };
67
-
68
- export const SUBAGENT_DEFS: Record<string, SubAgentDef> = {
69
- "code-explorer": CODE_EXPLORER,
70
- "web-researcher": WEB_RESEARCHER,
71
- "general-purpose": GENERAL_PURPOSE,
72
- };
@@ -1,161 +0,0 @@
1
- import type { Message } from "../types.js";
2
- import type { Emitter } from "../emitter.js";
3
- import { LLMClient } from "../llm/client.js";
4
- import { SUBAGENT_MODEL } from "../llm/models.js";
5
- import { ToolRegistry } from "../tools/registry.js";
6
- import { executeTool } from "../tools/executor.js";
7
- import { SUBAGENT_DEFS } from "./definitions.js";
8
-
9
- // Import builtin tools for sub-agent use
10
- import { readFileTool } from "../tools/builtin/read.js";
11
- import { lsTool } from "../tools/builtin/ls.js";
12
- import { grepTool } from "../tools/builtin/grep.js";
13
- import { globTool } from "../tools/builtin/glob.js";
14
- import { webSearchTool } from "../tools/external/web-search.js";
15
- import { webFetchTool } from "../tools/external/web-fetch.js";
16
-
17
- export interface SubAgentSpawnConfig {
18
- agentName: string;
19
- label: string;
20
- prompt: string;
21
- workDir: string;
22
- env: Record<string, string>;
23
- }
24
-
25
- const AVAILABLE_TOOLS: Record<string, import("../types.js").ToolDefinition> = {
26
- ls: lsTool,
27
- read_file: readFileTool,
28
- grep: grepTool,
29
- glob: globTool,
30
- web_search: webSearchTool,
31
- web_fetch: webFetchTool,
32
- };
33
-
34
- export class SubAgentManager {
35
- private running = new Map<string, Promise<string>>();
36
- private emitter: Emitter;
37
-
38
- constructor(emitter: Emitter) {
39
- this.emitter = emitter;
40
- }
41
-
42
- spawn(config: SubAgentSpawnConfig): string {
43
- const id = `sa_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
44
-
45
- this.emitter.emit({
46
- type: "subagent-start",
47
- id,
48
- name: config.agentName,
49
- label: config.label,
50
- });
51
-
52
- const promise = this.runSubAgentLoop(id, config);
53
- this.running.set(id, promise);
54
- return id;
55
- }
56
-
57
- async waitFor(id: string): Promise<string> {
58
- const result = await this.running.get(id);
59
- this.running.delete(id);
60
- return result ?? "";
61
- }
62
-
63
- private async runSubAgentLoop(id: string, config: SubAgentSpawnConfig): Promise<string> {
64
- const def = SUBAGENT_DEFS[config.agentName];
65
- if (!def) return JSON.stringify({ error: `Unknown sub-agent: ${config.agentName}` });
66
-
67
- const client = new LLMClient({
68
- modelTier: SUBAGENT_MODEL,
69
- env: config.env,
70
- });
71
-
72
- // Build registry with only allowed tools
73
- const registry = new ToolRegistry();
74
- for (const toolName of def.allowedTools) {
75
- const tool = AVAILABLE_TOOLS[toolName];
76
- if (tool) registry.register(tool);
77
- }
78
-
79
- const messages: Message[] = [
80
- { role: "system", content: def.systemPrompt },
81
- { role: "user", content: config.prompt },
82
- ];
83
-
84
- const tools = registry.toOpenAITools();
85
- const maxIterations = 30;
86
- let result = "";
87
-
88
- for (let i = 0; i < maxIterations; i++) {
89
- let text = "";
90
- const toolCalls: import("../types.js").ToolCall[] = [];
91
-
92
- for await (const chunk of client.stream(messages, tools)) {
93
- switch (chunk.type) {
94
- case "text":
95
- text += chunk.text;
96
- this.emitter.emit({ type: "subagent-delta", id, text: chunk.text });
97
- break;
98
- case "tool_call_complete":
99
- toolCalls.push({
100
- id: chunk.id,
101
- type: "function",
102
- function: { name: chunk.name, arguments: chunk.arguments },
103
- });
104
- break;
105
- }
106
- }
107
-
108
- // Add assistant message
109
- const assistantMsg: Message = { role: "assistant", content: text || null };
110
- if (toolCalls.length > 0) assistantMsg.tool_calls = toolCalls;
111
- messages.push(assistantMsg);
112
-
113
- if (toolCalls.length === 0) {
114
- result = text;
115
- break;
116
- }
117
-
118
- // Execute tool calls
119
- for (const tc of toolCalls) {
120
- let parsedArgs: unknown;
121
- try { parsedArgs = JSON.parse(tc.function.arguments); } catch { parsedArgs = {}; }
122
-
123
- // Emit with _subagent tag so app can associate with this subagent
124
- (this.emitter as { emit(e: Record<string, unknown>): void }).emit({
125
- type: "tool-call-start",
126
- id: tc.id,
127
- name: tc.function.name,
128
- args: parsedArgs,
129
- _subagent: id,
130
- });
131
-
132
- const ctx = {
133
- emitter: this.emitter,
134
- workDir: config.workDir,
135
- env: config.env,
136
- sessionId: id,
137
- toolCallId: tc.id,
138
- };
139
- const output = await executeTool(registry, tc, ctx, []);
140
-
141
- (this.emitter as { emit(e: Record<string, unknown>): void }).emit({
142
- type: "tool-result",
143
- id: tc.id,
144
- name: tc.function.name,
145
- output,
146
- _subagent: id,
147
- });
148
-
149
- messages.push({
150
- role: "tool",
151
- content: output,
152
- tool_call_id: tc.id,
153
- name: tc.function.name,
154
- });
155
- }
156
- }
157
-
158
- this.emitter.emit({ type: "subagent-result", id, output: result });
159
- return result;
160
- }
161
- }
@@ -1,61 +0,0 @@
1
- import { z } from "zod";
2
- import type { ToolDefinition, TodoItem } from "../types.js";
3
- import type { Emitter } from "../emitter.js";
4
-
5
- export class TodoManager {
6
- private todos: TodoItem[] = [];
7
- private emitter: Emitter;
8
-
9
- constructor(emitter: Emitter) {
10
- this.emitter = emitter;
11
- }
12
-
13
- write(todos: TodoItem[]): void {
14
- this.todos = todos;
15
- this.emitter.emit({ type: "todos-update", todos: this.todos });
16
- }
17
-
18
- read(): TodoItem[] {
19
- return this.todos;
20
- }
21
-
22
- restore(todos: TodoItem[]): void {
23
- this.todos = structuredClone(todos);
24
- this.emitter.emit({ type: "todos-update", todos: this.todos });
25
- }
26
-
27
- createTools(): ToolDefinition[] {
28
- return [
29
- {
30
- name: "write_todos",
31
- description:
32
- "Create or update the todo list. Pass the complete list each time. " +
33
- "Set status to 'in_progress' when starting a step, 'completed' when done. " +
34
- "Pass an empty array to clear all todos when the task is complete.",
35
- parameters: z.object({
36
- todos: z.array(
37
- z.object({
38
- content: z.string().describe("Description of the todo item"),
39
- status: z
40
- .enum(["pending", "in_progress", "completed"])
41
- .describe("Current status of the todo"),
42
- }),
43
- ),
44
- }),
45
- execute: async (rawArgs: unknown): Promise<string> => {
46
- const args = rawArgs as { todos: TodoItem[] };
47
- this.write(args.todos);
48
- return JSON.stringify({ success: true, count: args.todos.length });
49
- },
50
- },
51
- {
52
- name: "read_todos",
53
- description: "Read the current todo list to check progress.",
54
- parameters: z.object({}),
55
- execute: async (): Promise<string> => {
56
- return JSON.stringify({ todos: this.read() });
57
- },
58
- },
59
- ];
60
- }
61
- }
@@ -1,29 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { z } from "zod";
4
- import type { ToolDefinition, ToolContext } from "../../types.js";
5
-
6
- export const deleteFileTool: ToolDefinition = {
7
- name: "delete_file",
8
- description: "Delete a file or directory. Directories are deleted recursively.",
9
- parameters: z.object({
10
- path: z.string().describe("Relative path to the file or directory to delete"),
11
- }),
12
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
13
- const args = rawArgs as { path: string };
14
- const filePath = path.resolve(ctx.workDir, args.path);
15
-
16
- // Safety: prevent deleting outside workDir
17
- if (!filePath.startsWith(ctx.workDir)) {
18
- return JSON.stringify({ error: "Cannot delete files outside the project directory" });
19
- }
20
-
21
- try {
22
- await fs.rm(filePath, { recursive: true, force: true });
23
- return JSON.stringify({ deleted: args.path });
24
- } catch (e) {
25
- const msg = e instanceof Error ? e.message : String(e);
26
- return JSON.stringify({ error: msg });
27
- }
28
- },
29
- };
@@ -1,74 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { z } from "zod";
4
- import type { ToolDefinition, ToolContext } from "../../types.js";
5
-
6
- export const editFileTool: ToolDefinition = {
7
- name: "edit_file",
8
- description:
9
- "Edit a file by replacing an exact string match with new content. " +
10
- "The old_string must match exactly (including whitespace and indentation). " +
11
- "If multiple occurrences exist, provide more surrounding context to make the match unique.",
12
- parameters: z.object({
13
- path: z.string().describe("Relative path to the file"),
14
- old_string: z.string().describe("Exact string to find and replace"),
15
- new_string: z.string().describe("String to replace old_string with"),
16
- replace_all: z.boolean().optional().describe("Replace all occurrences instead of just the first"),
17
- }),
18
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
19
- const args = rawArgs as {
20
- path: string;
21
- old_string: string;
22
- new_string: string;
23
- replace_all?: boolean;
24
- };
25
- const filePath = path.resolve(ctx.workDir, args.path);
26
-
27
- try {
28
- const content = await fs.readFile(filePath, "utf-8");
29
- const occurrences = content.split(args.old_string).length - 1;
30
-
31
- if (occurrences === 0) {
32
- return JSON.stringify({
33
- error: "String not found in file. Verify the exact content including whitespace and indentation.",
34
- });
35
- }
36
-
37
- if (occurrences > 1 && !args.replace_all) {
38
- return JSON.stringify({
39
- error: `Found ${occurrences} occurrences. Provide more surrounding context to make the match unique, or set replace_all: true.`,
40
- });
41
- }
42
-
43
- let newContent: string;
44
- if (args.replace_all) {
45
- newContent = content.split(args.old_string).join(args.new_string);
46
- } else {
47
- const idx = content.indexOf(args.old_string);
48
- newContent = content.slice(0, idx) + args.new_string + content.slice(idx + args.old_string.length);
49
- }
50
-
51
- await fs.writeFile(filePath, newContent, "utf-8");
52
-
53
- const lines = newContent.split("\n").length;
54
- const bytes = Buffer.byteLength(newContent, "utf-8");
55
-
56
- ctx.emitter.emit({
57
- type: "file-complete",
58
- id: ctx.toolCallId,
59
- path: args.path,
60
- lines,
61
- bytes,
62
- });
63
-
64
- return JSON.stringify({
65
- path: args.path,
66
- occurrences: args.replace_all ? occurrences : 1,
67
- });
68
- } catch (e) {
69
- const msg = e instanceof Error ? e.message : String(e);
70
- if (msg.includes("ENOENT")) return JSON.stringify({ error: `File not found: ${args.path}` });
71
- return JSON.stringify({ error: msg });
72
- }
73
- },
74
- };
@@ -1,216 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import fs from "node:fs/promises";
3
- import { openSync, closeSync } from "node:fs";
4
- import path from "node:path";
5
- import { z } from "zod";
6
- import type { ToolDefinition, ToolContext } from "../../types.js";
7
-
8
- // Track background processes: pid → log file path
9
- const bgProcesses = new Map<number, { logFile: string; command: string }>();
10
-
11
- export const execTool: ToolDefinition = {
12
- name: "exec",
13
- description:
14
- "Execute a shell command in the project sandbox. Use for running build commands, installing packages, running scripts, etc. " +
15
- "Returns stdout, stderr, and exit code. Use background: true for long-running processes like dev servers. " +
16
- "Background processes write logs to a file — use get_logs to read them.",
17
- parameters: z.object({
18
- command: z.string().describe("Shell command to execute"),
19
- cwd: z
20
- .string()
21
- .optional()
22
- .describe("Subdirectory relative to project root to run command in (e.g. 'web-application')"),
23
- background: z
24
- .boolean()
25
- .optional()
26
- .describe("If true, run in background and return immediately with PID"),
27
- timeout: z
28
- .number()
29
- .optional()
30
- .describe("Timeout in seconds (default: 120, max: 600)"),
31
- }),
32
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
33
- const args = rawArgs as {
34
- command: string;
35
- cwd?: string;
36
- background?: boolean;
37
- timeout?: number;
38
- };
39
-
40
- let workDir = ctx.workDir;
41
- if (args.cwd && args.cwd !== "." && args.cwd !== "./") {
42
- workDir = path.resolve(ctx.workDir, args.cwd);
43
- }
44
-
45
- const timeoutMs = Math.min((args.timeout ?? 120) * 1000, 600_000);
46
-
47
- if (args.background) {
48
- return runBackground(args.command, workDir, ctx);
49
- }
50
-
51
- return runForeground(args.command, workDir, timeoutMs, ctx);
52
- },
53
- };
54
-
55
- export const getLogsTool: ToolDefinition = {
56
- name: "get_logs",
57
- description:
58
- "Read logs from a background process. Use after starting a command with background: true. " +
59
- "Pass the PID returned by exec, or omit to list all background processes. " +
60
- "Use tail: true to get only the last lines (useful for checking dev server status).",
61
- parameters: z.object({
62
- pid: z.number().optional().describe("PID of the background process"),
63
- tail: z.number().optional().describe("Number of lines from the end (default: all)"),
64
- list: z.boolean().optional().describe("If true, list all tracked background processes"),
65
- }),
66
- execute: async (rawArgs: unknown): Promise<string> => {
67
- const args = rawArgs as { pid?: number; tail?: number; list?: boolean };
68
-
69
- if (args.list || args.pid === undefined) {
70
- const processes: Array<{ pid: number; command: string; running: boolean }> = [];
71
- for (const [pid, info] of bgProcesses) {
72
- processes.push({
73
- pid,
74
- command: info.command,
75
- running: isProcessRunning(pid),
76
- });
77
- }
78
- return JSON.stringify({ processes });
79
- }
80
-
81
- const info = bgProcesses.get(args.pid);
82
- if (!info) {
83
- return JSON.stringify({ error: `No tracked background process with PID ${args.pid}` });
84
- }
85
-
86
- try {
87
- const content = await fs.readFile(info.logFile, "utf-8");
88
- const running = isProcessRunning(args.pid);
89
-
90
- if (args.tail) {
91
- const lines = content.split("\n");
92
- const tailed = lines.slice(-args.tail).join("\n");
93
- return JSON.stringify({ pid: args.pid, running, logs: tailed, total_lines: lines.length });
94
- }
95
-
96
- // Truncate if too large
97
- const maxLen = 50_000;
98
- if (content.length > maxLen) {
99
- const truncated = content.slice(-maxLen);
100
- return JSON.stringify({
101
- pid: args.pid,
102
- running,
103
- logs: truncated,
104
- truncated: true,
105
- note: `Showing last ${maxLen} chars. Use tail parameter for specific line count.`,
106
- });
107
- }
108
-
109
- return JSON.stringify({ pid: args.pid, running, logs: content });
110
- } catch {
111
- return JSON.stringify({ pid: args.pid, running: isProcessRunning(args.pid), logs: "" });
112
- }
113
- },
114
- };
115
-
116
- async function runForeground(
117
- command: string,
118
- cwd: string,
119
- timeoutMs: number,
120
- ctx: ToolContext,
121
- ): Promise<string> {
122
- return new Promise((resolve) => {
123
- const proc = spawn("bash", ["-c", command], {
124
- cwd,
125
- env: { ...process.env, ...ctx.env },
126
- stdio: ["ignore", "pipe", "pipe"],
127
- });
128
-
129
- let stdout = "";
130
- let stderr = "";
131
-
132
- proc.stdout.on("data", (data: Buffer) => {
133
- const text = data.toString();
134
- stdout += text;
135
- ctx.emitter.emit({ type: "exec-log", id: ctx.toolCallId, data: text });
136
- });
137
-
138
- proc.stderr.on("data", (data: Buffer) => {
139
- const text = data.toString();
140
- stderr += text;
141
- ctx.emitter.emit({ type: "exec-log", id: ctx.toolCallId, data: text });
142
- });
143
-
144
- const timer = setTimeout(() => {
145
- proc.kill("SIGTERM");
146
- setTimeout(() => proc.kill("SIGKILL"), 5000);
147
- }, timeoutMs);
148
-
149
- proc.on("close", (code) => {
150
- clearTimeout(timer);
151
-
152
- const maxLen = 50_000;
153
- if (stdout.length > maxLen) {
154
- stdout = stdout.slice(0, maxLen / 2) + "\n[... truncated ...]\n" + stdout.slice(-maxLen / 2);
155
- }
156
- if (stderr.length > maxLen) {
157
- stderr = stderr.slice(0, maxLen / 2) + "\n[... truncated ...]\n" + stderr.slice(-maxLen / 2);
158
- }
159
-
160
- resolve(
161
- JSON.stringify({
162
- stdout: stdout.trim(),
163
- stderr: stderr.trim(),
164
- exitCode: code ?? 1,
165
- }),
166
- );
167
- });
168
-
169
- proc.on("error", (err) => {
170
- clearTimeout(timer);
171
- resolve(JSON.stringify({ error: err.message, exitCode: 1 }));
172
- });
173
- });
174
- }
175
-
176
- async function runBackground(
177
- command: string,
178
- cwd: string,
179
- ctx: ToolContext,
180
- ): Promise<string> {
181
- // Create log file for this background process
182
- const logDir = path.join(ctx.workDir, ".strayl", "logs");
183
- await fs.mkdir(logDir, { recursive: true });
184
- const logFile = path.join(logDir, `bg_${Date.now()}.log`);
185
-
186
- // Open log file for writing
187
- const fd = openSync(logFile, "w");
188
-
189
- const proc = spawn("bash", ["-c", command], {
190
- cwd,
191
- env: { ...process.env, ...ctx.env },
192
- stdio: ["ignore", fd, fd], // stdout+stderr → log file
193
- detached: true,
194
- });
195
-
196
- const pid = proc.pid!;
197
- bgProcesses.set(pid, { logFile, command });
198
- proc.unref();
199
- closeSync(fd);
200
-
201
- return JSON.stringify({
202
- pid,
203
- logFile: path.relative(ctx.workDir, logFile),
204
- message: `Command started in background (PID ${pid}). Use get_logs with this PID to check output.`,
205
- background: true,
206
- });
207
- }
208
-
209
- function isProcessRunning(pid: number): boolean {
210
- try {
211
- process.kill(pid, 0); // Signal 0 = check existence
212
- return true;
213
- } catch {
214
- return false;
215
- }
216
- }