@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,104 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import path from "node:path";
3
- import { z } from "zod";
4
- import type { ToolDefinition, ToolContext } from "../../types.js";
5
-
6
- const MAX_RESULTS = 100;
7
-
8
- export const globTool: ToolDefinition = {
9
- name: "glob",
10
- description:
11
- "Find files matching a glob pattern. Uses ripgrep (rg --files) if available, falls back to find. " +
12
- "Returns matching file paths sorted by name.",
13
- parameters: z.object({
14
- pattern: z.string().describe("Glob pattern to match (e.g. '**/*.ts', 'src/**/*.tsx')"),
15
- path: z.string().optional().describe("Relative path to search in (default: project root)"),
16
- }),
17
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
18
- const args = rawArgs as { pattern: string; path?: string };
19
- const searchPath = path.resolve(ctx.workDir, args.path ?? ".");
20
-
21
- return new Promise((resolve) => {
22
- // Try ripgrep first
23
- const proc = spawn("rg", ["--files", "--glob", args.pattern, searchPath], {
24
- cwd: ctx.workDir,
25
- stdio: ["ignore", "pipe", "pipe"],
26
- });
27
-
28
- let output = "";
29
- let fallback = false;
30
-
31
- proc.stdout.on("data", (data: Buffer) => {
32
- output += data.toString();
33
- });
34
-
35
- proc.on("error", () => {
36
- fallback = true;
37
- });
38
-
39
- proc.on("close", (code) => {
40
- if (fallback || (code !== 0 && output.trim() === "")) {
41
- // Fallback to find
42
- runFind(args.pattern, searchPath, ctx.workDir).then(resolve);
43
- return;
44
- }
45
-
46
- const files = output
47
- .trim()
48
- .split("\n")
49
- .filter(Boolean)
50
- .map(f => path.relative(ctx.workDir, f))
51
- .sort()
52
- .slice(0, MAX_RESULTS);
53
-
54
- resolve(
55
- JSON.stringify({
56
- files,
57
- count: files.length,
58
- ...(files.length >= MAX_RESULTS ? { truncated: true } : {}),
59
- }),
60
- );
61
- });
62
- });
63
- },
64
- };
65
-
66
- function runFind(pattern: string, searchPath: string, workDir: string): Promise<string> {
67
- return new Promise((resolve) => {
68
- // Convert glob to find -name pattern (simplified)
69
- const namePattern = pattern.includes("/")
70
- ? pattern.split("/").pop()!
71
- : pattern;
72
-
73
- const proc = spawn("find", [searchPath, "-name", namePattern, "-type", "f"], {
74
- stdio: ["ignore", "pipe", "pipe"],
75
- });
76
-
77
- let output = "";
78
- proc.stdout.on("data", (data: Buffer) => {
79
- output += data.toString();
80
- });
81
-
82
- proc.on("close", () => {
83
- const files = output
84
- .trim()
85
- .split("\n")
86
- .filter(Boolean)
87
- .map(f => path.relative(workDir, f))
88
- .sort()
89
- .slice(0, MAX_RESULTS);
90
-
91
- resolve(
92
- JSON.stringify({
93
- files,
94
- count: files.length,
95
- ...(files.length >= MAX_RESULTS ? { truncated: true } : {}),
96
- }),
97
- );
98
- });
99
-
100
- proc.on("error", () => {
101
- resolve(JSON.stringify({ error: "Neither rg nor find available", files: [] }));
102
- });
103
- });
104
- }
@@ -1,115 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import path from "node:path";
3
- import { z } from "zod";
4
- import type { ToolDefinition, ToolContext } from "../../types.js";
5
-
6
- const MAX_MATCHES = 50;
7
-
8
- export const grepTool: ToolDefinition = {
9
- name: "grep",
10
- description:
11
- "Search for a pattern in files. Uses ripgrep (rg) if available, falls back to grep. " +
12
- "Returns matching lines with file paths, line numbers, and surrounding context.",
13
- parameters: z.object({
14
- pattern: z.string().describe("Regex pattern to search for"),
15
- path: z.string().optional().describe("Relative path to search in (default: project root)"),
16
- glob: z.string().optional().describe("File pattern to filter (e.g. '*.ts', '*.{js,jsx}')"),
17
- ignoreCase: z.boolean().optional().describe("Case insensitive search"),
18
- }),
19
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
20
- const args = rawArgs as {
21
- pattern: string;
22
- path?: string;
23
- glob?: string;
24
- ignoreCase?: boolean;
25
- };
26
-
27
- const searchPath = path.resolve(ctx.workDir, args.path ?? ".");
28
-
29
- // Try ripgrep first, fall back to grep
30
- const rgAvailable = await commandExists("rg");
31
-
32
- const cmdArgs: string[] = rgAvailable
33
- ? buildRgArgs(args, searchPath)
34
- : buildGrepArgs(args, searchPath);
35
-
36
- const cmd = rgAvailable ? "rg" : "grep";
37
-
38
- return new Promise((resolve) => {
39
- const proc = spawn(cmd, cmdArgs, {
40
- cwd: ctx.workDir,
41
- stdio: ["ignore", "pipe", "pipe"],
42
- });
43
-
44
- let output = "";
45
- let lineCount = 0;
46
-
47
- proc.stdout.on("data", (data: Buffer) => {
48
- const text = data.toString();
49
- const lines = text.split("\n");
50
- for (const line of lines) {
51
- if (lineCount >= MAX_MATCHES * 3) break; // ~3 lines per match with context
52
- output += line + "\n";
53
- lineCount++;
54
- }
55
- });
56
-
57
- proc.stderr.on("data", () => {
58
- // ignore stderr
59
- });
60
-
61
- proc.on("close", (code) => {
62
- if (code === 1 && output.trim() === "") {
63
- resolve(JSON.stringify({ matches: 0, output: "No matches found." }));
64
- return;
65
- }
66
-
67
- const trimmed = output.trim();
68
- const matchCount = trimmed.split("\n").filter(l => l.length > 0).length;
69
- const truncated = matchCount >= MAX_MATCHES * 3;
70
-
71
- resolve(
72
- JSON.stringify({
73
- matches: Math.min(matchCount, MAX_MATCHES),
74
- output: trimmed,
75
- ...(truncated ? { truncated: true, note: `Output limited to ~${MAX_MATCHES} matches` } : {}),
76
- }),
77
- );
78
- });
79
-
80
- proc.on("error", () => {
81
- resolve(JSON.stringify({ error: `${cmd} not available` }));
82
- });
83
- });
84
- },
85
- };
86
-
87
- function buildRgArgs(
88
- args: { pattern: string; glob?: string; ignoreCase?: boolean },
89
- searchPath: string,
90
- ): string[] {
91
- const rgArgs = ["-n", "-C", "2", "--max-count", String(MAX_MATCHES)];
92
- if (args.ignoreCase) rgArgs.push("-i");
93
- if (args.glob) rgArgs.push("--glob", args.glob);
94
- rgArgs.push(args.pattern, searchPath);
95
- return rgArgs;
96
- }
97
-
98
- function buildGrepArgs(
99
- args: { pattern: string; glob?: string; ignoreCase?: boolean },
100
- searchPath: string,
101
- ): string[] {
102
- const grepArgs = ["-rn"];
103
- if (args.ignoreCase) grepArgs.push("-i");
104
- if (args.glob) grepArgs.push("--include", args.glob);
105
- grepArgs.push(args.pattern, searchPath);
106
- return grepArgs;
107
- }
108
-
109
- function commandExists(cmd: string): Promise<boolean> {
110
- return new Promise((resolve) => {
111
- const proc = spawn("which", [cmd], { stdio: "ignore" });
112
- proc.on("close", (code) => resolve(code === 0));
113
- proc.on("error", () => resolve(false));
114
- });
115
- }
@@ -1,54 +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 lsTool: ToolDefinition = {
7
- name: "ls",
8
- description:
9
- "List files and directories at a given path. Returns names, types, sizes, and modification times. " +
10
- "Directories are listed first, then files, both sorted alphabetically.",
11
- parameters: z.object({
12
- path: z.string().optional().describe("Relative path to list (default: project root)"),
13
- }),
14
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
15
- const args = rawArgs as { path?: string };
16
- const dirPath = path.resolve(ctx.workDir, args.path ?? ".");
17
-
18
- try {
19
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
20
-
21
- const results: Array<{ name: string; type: string; size?: number; modified?: string }> = [];
22
-
23
- for (const entry of entries) {
24
- const entryPath = path.join(dirPath, entry.name);
25
- const item: { name: string; type: string; size?: number; modified?: string } = {
26
- name: entry.name,
27
- type: entry.isDirectory() ? "dir" : "file",
28
- };
29
-
30
- try {
31
- const stat = await fs.stat(entryPath);
32
- if (!entry.isDirectory()) item.size = stat.size;
33
- item.modified = stat.mtime.toISOString();
34
- } catch {
35
- // stat may fail for broken symlinks
36
- }
37
-
38
- results.push(item);
39
- }
40
-
41
- // Sort: dirs first, then alphabetical
42
- results.sort((a, b) => {
43
- if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
44
- return a.name.localeCompare(b.name);
45
- });
46
-
47
- return JSON.stringify(results);
48
- } catch (e) {
49
- const msg = e instanceof Error ? e.message : String(e);
50
- if (msg.includes("ENOENT")) return JSON.stringify({ error: `Directory not found: ${args.path ?? "."}` });
51
- return JSON.stringify({ error: msg });
52
- }
53
- },
54
- };
@@ -1,31 +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 moveFileTool: ToolDefinition = {
7
- name: "move_file",
8
- description: "Move or rename a file or directory.",
9
- parameters: z.object({
10
- source: z.string().describe("Relative path of the source file/directory"),
11
- destination: z.string().describe("Relative path of the destination"),
12
- }),
13
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
14
- const args = rawArgs as { source: string; destination: string };
15
- const srcPath = path.resolve(ctx.workDir, args.source);
16
- const destPath = path.resolve(ctx.workDir, args.destination);
17
-
18
- if (!srcPath.startsWith(ctx.workDir) || !destPath.startsWith(ctx.workDir)) {
19
- return JSON.stringify({ error: "Cannot move files outside the project directory" });
20
- }
21
-
22
- try {
23
- await fs.mkdir(path.dirname(destPath), { recursive: true });
24
- await fs.rename(srcPath, destPath);
25
- return JSON.stringify({ moved: { from: args.source, to: args.destination } });
26
- } catch (e) {
27
- const msg = e instanceof Error ? e.message : String(e);
28
- return JSON.stringify({ error: msg });
29
- }
30
- },
31
- };
@@ -1,69 +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
- const MAX_FILE_SIZE = 500 * 1024; // 500KB
7
-
8
- export const readFileTool: ToolDefinition = {
9
- name: "read_file",
10
- description:
11
- "Read a file from the project. Returns content with line numbers. " +
12
- "Use offset and limit to read specific sections of large files.",
13
- parameters: z.object({
14
- path: z.string().describe("Relative path to the file"),
15
- offset: z.number().optional().describe("Line number to start reading from (1-based)"),
16
- limit: z.number().optional().describe("Number of lines to read"),
17
- }),
18
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
19
- const args = rawArgs as { path: string; offset?: number; limit?: number };
20
- const filePath = path.resolve(ctx.workDir, args.path);
21
-
22
- try {
23
- const stat = await fs.stat(filePath);
24
-
25
- // Binary file detection
26
- if (stat.size > MAX_FILE_SIZE) {
27
- const sample = Buffer.alloc(512);
28
- const fh = await fs.open(filePath, "r");
29
- await fh.read(sample, 0, 512, 0);
30
- await fh.close();
31
-
32
- if (isBinary(sample)) {
33
- return JSON.stringify({ error: `Binary file, ${stat.size} bytes` });
34
- }
35
- }
36
-
37
- const raw = await fs.readFile(filePath, "utf-8");
38
- const lines = raw.split("\n");
39
-
40
- const offset = Math.max(1, args.offset ?? 1);
41
- const limit = args.limit ?? lines.length;
42
- const selected = lines.slice(offset - 1, offset - 1 + limit);
43
-
44
- const numbered = selected
45
- .map((line, i) => {
46
- const lineNum = String(offset + i).padStart(4, " ");
47
- return `${lineNum}| ${line}`;
48
- })
49
- .join("\n");
50
-
51
- return numbered || "(empty file)";
52
- } catch (e) {
53
- const msg = e instanceof Error ? e.message : String(e);
54
- if (msg.includes("ENOENT")) return JSON.stringify({ error: `File not found: ${args.path}` });
55
- return JSON.stringify({ error: msg });
56
- }
57
- },
58
- };
59
-
60
- function isBinary(buffer: Buffer): boolean {
61
- for (let i = 0; i < buffer.length; i++) {
62
- const byte = buffer[i];
63
- // Null byte or control characters (except common ones)
64
- if (byte === 0) return true;
65
- if (byte < 8) return true;
66
- if (byte === 14 || byte === 15) return true;
67
- }
68
- return false;
69
- }
@@ -1,42 +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 writeFileTool: ToolDefinition = {
7
- name: "write_file",
8
- description:
9
- "Write content to a file. Creates parent directories if needed. " +
10
- "Overwrites existing files. Use for creating new files or complete rewrites.",
11
- parameters: z.object({
12
- path: z.string().describe("Relative path to the file"),
13
- content: z.string().describe("Full content to write to the file"),
14
- }),
15
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
16
- const args = rawArgs as { path: string; content: string };
17
- const filePath = path.resolve(ctx.workDir, args.path);
18
-
19
- try {
20
- // Create parent directories
21
- await fs.mkdir(path.dirname(filePath), { recursive: true });
22
-
23
- await fs.writeFile(filePath, args.content, "utf-8");
24
-
25
- const lines = args.content.split("\n").length;
26
- const bytes = Buffer.byteLength(args.content, "utf-8");
27
-
28
- ctx.emitter.emit({
29
- type: "file-complete",
30
- id: ctx.toolCallId,
31
- path: args.path,
32
- lines,
33
- bytes,
34
- });
35
-
36
- return JSON.stringify({ path: args.path, linesWritten: lines, bytesWritten: bytes });
37
- } catch (e) {
38
- const msg = e instanceof Error ? e.message : String(e);
39
- return JSON.stringify({ error: msg });
40
- }
41
- },
42
- };
@@ -1,51 +0,0 @@
1
- import type { ToolCall, ToolContext, Middleware } from "../types.js";
2
- import type { ToolRegistry } from "./registry.js";
3
-
4
- export async function executeTool(
5
- registry: ToolRegistry,
6
- toolCall: ToolCall,
7
- ctx: ToolContext,
8
- middleware: Middleware[],
9
- ): Promise<string> {
10
- const def = registry.get(toolCall.function.name);
11
- if (!def) {
12
- return JSON.stringify({ error: `Unknown tool: ${toolCall.function.name}` });
13
- }
14
-
15
- let args: unknown;
16
- try {
17
- args = JSON.parse(toolCall.function.arguments);
18
- } catch {
19
- return JSON.stringify({ error: `Invalid JSON arguments for tool ${toolCall.function.name}` });
20
- }
21
-
22
- const parsed = def.parameters.safeParse(args);
23
- if (!parsed.success) {
24
- return JSON.stringify({
25
- error: `Validation error: ${parsed.error.message}. Fix your arguments and retry.`,
26
- });
27
- }
28
-
29
- // Pass raw args (not parsed.data) to execute — HITL merges extra fields
30
- // like "answer", "feedback", "value" that aren't in the Zod schema.
31
- // Zod validation ensures the LLM-provided fields are correct,
32
- // but the execute function needs access to HITL-injected fields too.
33
- const execute = () => def.execute(args, ctx);
34
-
35
- // Apply middleware wrapToolCall chain (innermost first)
36
- let fn = execute;
37
- for (const mw of [...middleware].reverse()) {
38
- if (mw.wrapToolCall) {
39
- const prevFn = fn;
40
- const wrappedMw = mw;
41
- fn = () => wrappedMw.wrapToolCall!(toolCall, prevFn);
42
- }
43
- }
44
-
45
- try {
46
- return await fn();
47
- } catch (e) {
48
- const msg = e instanceof Error ? e.message : String(e);
49
- return JSON.stringify({ error: `Tool execution error: ${msg}. Fix and retry.` });
50
- }
51
- }