@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,285 +0,0 @@
1
- import { z } from "zod";
2
- import type { ToolDefinition, ToolContext } from "../../types.js";
3
-
4
- /**
5
- * All database tools proxy through api.strayl.dev/v1/tools/database/*
6
- * The proxy adds auth, resolves project context, and forwards to db.strayl.dev.
7
- *
8
- * In direct mode (STRAYL_LLM_DIRECT=1), calls db.strayl.dev directly.
9
- */
10
-
11
- async function dbFetch(
12
- path: string,
13
- init: RequestInit,
14
- ctx: ToolContext,
15
- ): Promise<Response> {
16
- const env = ctx.env;
17
-
18
- if (env.STRAYL_LLM_DIRECT === "1") {
19
- // Direct mode — call db.strayl.dev with auth token
20
- const dbApiUrl = env.DB_API_URL ?? "https://db.strayl.dev";
21
- const authToken = env.STRAYL_AUTH_TOKEN ?? "";
22
- const username = env.STRAYL_USERNAME ?? "";
23
- const projectSlug = env.STRAYL_PROJECT_SLUG ?? "";
24
-
25
- const url = `${dbApiUrl}/databases/${username}/${projectSlug}${path}`;
26
- const headers = new Headers(init.headers);
27
- headers.set("Authorization", `Bearer ${authToken}`);
28
- headers.set("Content-Type", "application/json");
29
- return fetch(url, { ...init, headers });
30
- }
31
-
32
- // Proxy mode — route through api.strayl.dev
33
- const apiUrl = env.STRAYL_API_URL ?? "https://api.strayl.dev";
34
- const token = env.STRAYL_SESSION_TOKEN ?? "";
35
- const username = env.STRAYL_USERNAME ?? "";
36
- const projectSlug = env.STRAYL_PROJECT_SLUG ?? "";
37
-
38
- const url = `${apiUrl}/v1/tools/database${path}`;
39
- const headers = new Headers(init.headers);
40
- headers.set("Authorization", `Bearer ${token}`);
41
- headers.set("Content-Type", "application/json");
42
- headers.set("X-Strayl-Project", `${username}/${projectSlug}`);
43
- return fetch(url, { ...init, headers });
44
- }
45
-
46
- // ─── create_database ─────────────────────────────────────────────────
47
-
48
- export const createDatabaseTool: ToolDefinition = {
49
- name: "create_database",
50
- description:
51
- "Create a Neon PostgreSQL database for the project. " +
52
- "Provisions production and development branches. Sets DATABASE_URL in the sandbox.",
53
- parameters: z.object({
54
- name: z.string().describe("Database name"),
55
- region: z
56
- .string()
57
- .optional()
58
- .default("aws-us-east-1")
59
- .describe("Neon region, defaults to aws-us-east-1"),
60
- }),
61
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
62
- const args = rawArgs as { name: string; region?: string };
63
-
64
- if (!args.name.trim()) {
65
- return JSON.stringify({ error: "Missing required parameter: name" });
66
- }
67
-
68
- const res = await dbFetch("", {
69
- method: "POST",
70
- body: JSON.stringify({ name: args.name.trim(), region: args.region ?? "aws-us-east-1" }),
71
- }, ctx);
72
-
73
- if (!res.ok) {
74
- const err = await res.json().catch(() => ({})) as { error?: string };
75
- return JSON.stringify({
76
- error: err.error || `Database API error: ${res.status} ${res.statusText}`,
77
- });
78
- }
79
-
80
- const data = await res.json() as Record<string, unknown>;
81
- return JSON.stringify({
82
- success: true,
83
- ...(data.databases ? { databases: data.databases } : { database: data.database }),
84
- ...(Array.isArray((data as any).warnings) && (data as any).warnings.length ? { warnings: (data as any).warnings } : {}),
85
- });
86
- },
87
- };
88
-
89
- // ─── list_databases ──────────────────────────────────────────────────
90
-
91
- export const listDatabasesTool: ToolDefinition = {
92
- name: "list_databases",
93
- description:
94
- "List all databases in the project with their IDs, environments, and status.",
95
- parameters: z.object({}),
96
- execute: async (_rawArgs: unknown, ctx: ToolContext): Promise<string> => {
97
- const res = await dbFetch("", { method: "GET" }, ctx);
98
-
99
- if (!res.ok) {
100
- const err = await res.json().catch(() => ({})) as { error?: string };
101
- return JSON.stringify({
102
- error: err.error || `Database API error: ${res.status} ${res.statusText}`,
103
- });
104
- }
105
-
106
- const data = await res.json() as { databases?: Array<Record<string, unknown>> };
107
- const databases = (data.databases || []).map((db) => ({
108
- id: db.id,
109
- name: db.name,
110
- environment: db.environment,
111
- region: db.region,
112
- status: db.status,
113
- createdAt: db.createdAt,
114
- }));
115
-
116
- return JSON.stringify({ databases });
117
- },
118
- };
119
-
120
- // ─── run_database_query ──────────────────────────────────────────────
121
-
122
- export const runDatabaseQueryTool: ToolDefinition = {
123
- name: "run_database_query",
124
- description:
125
- "Execute a SQL query against the development database. Use for SELECT, INSERT, schema introspection.",
126
- parameters: z.object({
127
- sql: z.string().describe("SQL query to execute"),
128
- databaseId: z
129
- .string()
130
- .optional()
131
- .describe("Database ID to query. If omitted, the development database is used automatically."),
132
- }),
133
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
134
- const args = rawArgs as { sql: string; databaseId?: string };
135
-
136
- if (!args.sql.trim()) {
137
- return JSON.stringify({ error: "Missing required parameter: sql" });
138
- }
139
-
140
- let databaseId = args.databaseId || "";
141
-
142
- // Auto-find dev database if no ID given
143
- if (!databaseId) {
144
- const listRes = await dbFetch("", { method: "GET" }, ctx);
145
- if (!listRes.ok) {
146
- return JSON.stringify({ error: "Failed to list databases" });
147
- }
148
- const listData = await listRes.json() as { databases: Array<{ id: string; environment: string }> };
149
- const devDb = listData.databases.find((db) => db.environment === "development");
150
- if (!devDb) {
151
- return JSON.stringify({ error: "No development database found. Create a database first." });
152
- }
153
- databaseId = devDb.id;
154
- }
155
-
156
- const res = await dbFetch(`/${databaseId}/query`, {
157
- method: "POST",
158
- body: JSON.stringify({ sql: args.sql }),
159
- }, ctx);
160
-
161
- if (!res.ok) {
162
- const err = await res.json().catch(() => ({})) as { error?: string };
163
- return JSON.stringify({
164
- error: err.error || `Query failed: ${res.status} ${res.statusText}`,
165
- });
166
- }
167
-
168
- const data = await res.json() as { rows: unknown[]; rowCount: number };
169
- return JSON.stringify(data);
170
- },
171
- };
172
-
173
- // ─── prepare_database_migration ──────────────────────────────────────
174
-
175
- export const prepareDatabaseMigrationTool: ToolDefinition = {
176
- name: "prepare_database_migration",
177
- description:
178
- "Prepare a schema migration for review. Returns migration ID and current schema. " +
179
- "One SQL statement per call. Use complete_database_migration to apply.",
180
- parameters: z.object({
181
- migrationSql: z.string().describe("SQL DDL statement for the migration"),
182
- databaseId: z
183
- .string()
184
- .optional()
185
- .describe("Database ID. If omitted, the development database is used automatically."),
186
- }),
187
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
188
- const args = rawArgs as { migrationSql: string; databaseId?: string };
189
-
190
- if (!args.migrationSql.trim()) {
191
- return JSON.stringify({ error: "Missing required parameter: migrationSql" });
192
- }
193
-
194
- let databaseId = args.databaseId || "";
195
-
196
- // Auto-find dev database
197
- if (!databaseId) {
198
- const listRes = await dbFetch("", { method: "GET" }, ctx);
199
- if (!listRes.ok) {
200
- return JSON.stringify({ error: "Failed to list databases" });
201
- }
202
- const listData = await listRes.json() as { databases: Array<{ id: string; environment: string }> };
203
- const devDb = listData.databases.find((db) => db.environment === "development");
204
- if (!devDb) {
205
- return JSON.stringify({ error: "No development database found. Create a database first." });
206
- }
207
- databaseId = devDb.id;
208
- }
209
-
210
- // Fetch current schema for context
211
- let currentSchema: unknown[] = [];
212
- try {
213
- const schemaRes = await dbFetch(`/${databaseId}/query`, {
214
- method: "POST",
215
- body: JSON.stringify({
216
- sql: `SELECT table_name, column_name, data_type, is_nullable, column_default
217
- FROM information_schema.columns
218
- WHERE table_schema = 'public'
219
- ORDER BY table_name, ordinal_position`,
220
- }),
221
- }, ctx);
222
- if (schemaRes.ok) {
223
- const schemaData = await schemaRes.json() as { rows: unknown[] };
224
- currentSchema = schemaData.rows;
225
- }
226
- } catch {
227
- // Non-fatal
228
- }
229
-
230
- const migrationId = `mig_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
231
-
232
- return JSON.stringify({
233
- migrationId,
234
- migrationSql: args.migrationSql,
235
- databaseId,
236
- currentSchema,
237
- message: "Migration prepared. Review the SQL and current schema, then use complete_database_migration to apply.",
238
- });
239
- },
240
- };
241
-
242
- // ─── complete_database_migration ─────────────────────────────────────
243
-
244
- export const completeDatabaseMigrationTool: ToolDefinition = {
245
- name: "complete_database_migration",
246
- description:
247
- "Apply a prepared migration. Pass migrationId, migrationSql, and databaseId from prepare step.",
248
- parameters: z.object({
249
- migrationId: z.string().describe("Migration ID from prepare_database_migration"),
250
- migrationSql: z.string().describe("SQL statement to execute"),
251
- databaseId: z.string().describe("Database ID from prepare_database_migration"),
252
- }),
253
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
254
- const args = rawArgs as { migrationId: string; migrationSql: string; databaseId: string };
255
-
256
- if (!args.migrationId || !args.migrationSql.trim() || !args.databaseId) {
257
- return JSON.stringify({
258
- error: "Missing required parameters: migrationId, migrationSql, databaseId.",
259
- });
260
- }
261
-
262
- const res = await dbFetch(`/${args.databaseId}/query`, {
263
- method: "POST",
264
- body: JSON.stringify({ sql: args.migrationSql }),
265
- }, ctx);
266
-
267
- if (!res.ok) {
268
- const err = await res.json().catch(() => ({})) as { error?: string };
269
- return JSON.stringify({
270
- success: false,
271
- migrationId: args.migrationId,
272
- error: err.error || `Query failed: ${res.status} ${res.statusText}`,
273
- });
274
- }
275
-
276
- const result = await res.json() as { rows: unknown[]; rowCount: number };
277
-
278
- return JSON.stringify({
279
- success: true,
280
- migrationId: args.migrationId,
281
- databaseId: args.databaseId,
282
- result: { rows: result.rows, rowCount: result.rowCount },
283
- });
284
- },
285
- };
@@ -1,34 +0,0 @@
1
- import { z } from "zod";
2
- import type { ToolDefinition } from "../../types.js";
3
-
4
- export const enterPlanModeTool: ToolDefinition = {
5
- name: "enterPlanMode",
6
- description: `Enter plan mode to create a detailed implementation plan before writing code. Activates immediately.
7
-
8
- USE ONLY when ALL of these are true:
9
- - You have NOT just exited plan mode (never re-enter after a plan was confirmed or rejected)
10
- - The user has NOT provided a clear plan, detailed instructions, or step-by-step requirements
11
- - The task genuinely requires architectural decisions with multiple valid approaches
12
-
13
- Examples where you SHOULD use this:
14
- - User says "add authentication" with no specifics
15
- - User says "refactor the API layer" — multiple approaches, need to explore first
16
-
17
- Examples where you MUST NOT use this:
18
- - A plan was just confirmed — implement it immediately
19
- - User provided specific instructions — just execute them
20
- - Simple or single-file tasks (typos, bug fixes, small features)
21
- - You already know what to do — just do it`,
22
- parameters: z.object({
23
- reason: z.string().describe("Why this task needs a plan (brief explanation)"),
24
- }),
25
- execute: async (rawArgs: unknown, ctx): Promise<string> => {
26
- const args = rawArgs as { reason: string };
27
- ctx.emitter.emit({ type: "plan-mode-entered", reason: args.reason });
28
- return JSON.stringify({
29
- success: true,
30
- reason: args.reason,
31
- message: "Plan mode activated. Use read-only tools and interviewUser to gather requirements, then writePlan to create the plan.",
32
- });
33
- },
34
- };
@@ -1,110 +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
- import { proxyFetch } from "./proxy-fetch.js";
6
-
7
- export const generateImageTool: ToolDefinition = {
8
- name: "generate_image",
9
- description:
10
- "Generate images using AI. Provide output paths and prompts. " +
11
- "Saves generated images to the filesystem.",
12
- parameters: z.object({
13
- items: z
14
- .array(
15
- z.object({
16
- path: z.string().describe("Relative output path for the image"),
17
- prompt: z.string().describe("Image generation prompt"),
18
- aspectRatio: z
19
- .enum(["1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16"])
20
- .optional()
21
- .describe("Aspect ratio (default: 1:1)"),
22
- }),
23
- )
24
- .min(1)
25
- .max(10)
26
- .describe("Array of images to generate"),
27
- }),
28
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
29
- const args = rawArgs as {
30
- items: Array<{ path: string; prompt: string; aspectRatio?: string }>;
31
- };
32
-
33
- const results: Array<{ path: string; success: boolean }> = [];
34
- const failed: Array<{ path: string; error: string }> = [];
35
-
36
- for (const item of args.items) {
37
- if (item.path.includes("..") || path.isAbsolute(item.path)) {
38
- failed.push({ path: item.path, error: "Invalid path: must be relative, no .." });
39
- continue;
40
- }
41
-
42
- try {
43
- const response = await proxyFetch(
44
- "openrouter",
45
- "https://openrouter.ai/api/v1/chat/completions",
46
- "OPENROUTER_API_KEY",
47
- {
48
- method: "POST",
49
- headers: { "Content-Type": "application/json" },
50
- body: JSON.stringify({
51
- model: "google/gemini-2.5-flash-image",
52
- messages: [{ role: "user", content: item.prompt }],
53
- modalities: ["image", "text"],
54
- image_config: { aspect_ratio: item.aspectRatio ?? "1:1" },
55
- }),
56
- },
57
- ctx.env,
58
- );
59
-
60
- if (!response.ok) {
61
- failed.push({ path: item.path, error: `API error: ${response.status}` });
62
- continue;
63
- }
64
-
65
- const data = (await response.json()) as {
66
- choices: Array<{
67
- message: {
68
- content: Array<{
69
- type: string;
70
- image_url?: { url: string };
71
- }>;
72
- };
73
- }>;
74
- };
75
-
76
- const imageContent = data.choices?.[0]?.message?.content?.find(
77
- (c) => c.type === "image_url" && c.image_url?.url,
78
- );
79
-
80
- if (!imageContent?.image_url?.url) {
81
- failed.push({ path: item.path, error: "No image in response" });
82
- continue;
83
- }
84
-
85
- const dataUrl = imageContent.image_url.url;
86
- const base64Match = dataUrl.match(/^data:[^;]+;base64,(.+)$/);
87
- if (!base64Match) {
88
- failed.push({ path: item.path, error: "Invalid data URL format" });
89
- continue;
90
- }
91
-
92
- const buffer = Buffer.from(base64Match[1], "base64");
93
- const filePath = path.resolve(ctx.workDir, item.path);
94
- await fs.mkdir(path.dirname(filePath), { recursive: true });
95
- await fs.writeFile(filePath, buffer);
96
-
97
- results.push({ path: item.path, success: true });
98
- } catch (e) {
99
- const msg = e instanceof Error ? e.message : String(e);
100
- failed.push({ path: item.path, error: msg });
101
- }
102
- }
103
-
104
- return JSON.stringify({
105
- success: failed.length === 0,
106
- items: results,
107
- failed: failed.length > 0 ? failed : undefined,
108
- });
109
- },
110
- };
@@ -1,118 +0,0 @@
1
- import { z } from "zod";
2
- import type { ToolDefinition } from "../../types.js";
3
-
4
- export const askUserTool: ToolDefinition = {
5
- name: "askUser",
6
- description:
7
- "Ask the user a clarifying question with selectable options. " +
8
- "Use this when you need the user to make a decision or provide clarification before proceeding. " +
9
- "The user will see an interactive prompt with the options you provide.",
10
- parameters: z.object({
11
- question: z.string().describe("The question to ask the user"),
12
- context: z.string().optional().describe("Why this question matters (shown as subtitle)"),
13
- options: z
14
- .array(
15
- z.object({
16
- label: z.string().describe("Short option label"),
17
- description: z.string().optional().describe("Brief explanation of this option"),
18
- })
19
- )
20
- .min(1)
21
- .max(6)
22
- .describe("1-6 options for the user to choose from"),
23
- allowCustom: z
24
- .boolean()
25
- .optional()
26
- .default(true)
27
- .describe("Whether the user can type a custom answer instead of picking an option"),
28
- answer: z
29
- .string()
30
- .optional()
31
- .describe("The user's answer (provided via HITL — do not set this)"),
32
- }),
33
- hitl: true,
34
- execute: async (rawArgs: unknown): Promise<string> => {
35
- const args = rawArgs as { question: string; answer?: string };
36
- if (!args.answer) {
37
- return JSON.stringify({
38
- success: false,
39
- error: "No answer provided. The user may have rejected the question.",
40
- });
41
- }
42
- return JSON.stringify({
43
- success: true,
44
- question: args.question,
45
- answer: args.answer,
46
- });
47
- },
48
- };
49
-
50
- export const writePlanTool: ToolDefinition = {
51
- name: "writePlan",
52
- description:
53
- "Write an implementation plan as a markdown file to .strayl/plans/ in the project. " +
54
- "Use this after gathering requirements via askUser to present the final plan for user confirmation. " +
55
- "The agent will be paused until the user confirms or provides feedback.",
56
- parameters: z.object({
57
- title: z.string().describe("Plan title (used as heading and filename)"),
58
- content: z.string().describe("Full plan content in markdown format"),
59
- filename: z
60
- .string()
61
- .optional()
62
- .describe("Custom filename (without extension). If omitted, derived from title."),
63
- feedback: z
64
- .string()
65
- .optional()
66
- .describe("Internal: user feedback on the plan (set by HITL resume)"),
67
- }),
68
- hitl: true,
69
- execute: async (rawArgs: unknown): Promise<string> => {
70
- const args = rawArgs as { title: string; content: string; filename?: string; feedback?: string };
71
-
72
- if (args.feedback) {
73
- return JSON.stringify({
74
- success: false,
75
- feedback: args.feedback,
76
- message: `User wants changes to the plan: ${args.feedback}`,
77
- });
78
- }
79
-
80
- const safeName =
81
- args.filename ||
82
- args.title
83
- .toLowerCase()
84
- .replace(/[^a-z0-9]+/g, "-")
85
- .replace(/^-|-$/g, "")
86
- .slice(0, 60);
87
- const filePath = `.strayl/plans/${safeName}.md`;
88
-
89
- return JSON.stringify({
90
- success: true,
91
- filePath,
92
- message: `Plan confirmed by user. Written to ${filePath}. You are now in IMPLEMENTATION MODE. Start executing the plan immediately — do NOT call enterPlanMode again.`,
93
- });
94
- },
95
- };
96
-
97
- export const requestEnvVarTool: ToolDefinition = {
98
- name: "requestEnvVar",
99
- description:
100
- "Request an environment variable or API key from the user. " +
101
- "Use when a tool needs an API key that isn't configured.",
102
- parameters: z.object({
103
- name: z.string().describe("Environment variable name (e.g. STRIPE_API_KEY)"),
104
- reason: z.string().describe("Why this variable is needed"),
105
- value: z
106
- .string()
107
- .optional()
108
- .describe("The value (provided via HITL — do not set this)"),
109
- }),
110
- hitl: true,
111
- execute: async (rawArgs: unknown): Promise<string> => {
112
- const args = rawArgs as { name: string; reason: string; value?: string };
113
- if (args.value) {
114
- return JSON.stringify({ name: args.name, value: args.value, set: true });
115
- }
116
- return JSON.stringify({ name: args.name, error: "User did not provide value" });
117
- },
118
- };
@@ -1,28 +0,0 @@
1
- import { z } from "zod";
2
- import type { ToolDefinition, ToolContext } from "../../types.js";
3
-
4
- export const previewTool: ToolDefinition = {
5
- name: "show_preview",
6
- description:
7
- "Signal that a web preview is available on a given port. " +
8
- "The proxy will resolve this to a publicly accessible URL.",
9
- parameters: z.object({
10
- port: z.number().describe("Port number the preview is running on"),
11
- title: z.string().optional().describe("Title for the preview (default: 'Preview')"),
12
- }),
13
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
14
- const args = rawArgs as { port: number; title?: string };
15
-
16
- ctx.emitter.emit({
17
- type: "preview",
18
- port: args.port,
19
- title: args.title ?? "Preview",
20
- });
21
-
22
- return JSON.stringify({
23
- success: true,
24
- port: args.port,
25
- message: "Preview event emitted. The proxy will provide the URL.",
26
- });
27
- },
28
- };
@@ -1,51 +0,0 @@
1
- /**
2
- * Helper for external tools that need to call third-party APIs.
3
- * In proxy mode: routes through api.strayl.dev/v1/tools/{service}
4
- * In direct mode: calls the API directly with env var keys.
5
- *
6
- * This ensures API keys never live inside the sandbox.
7
- */
8
-
9
- export interface ProxyFetchConfig {
10
- env: Record<string, string>;
11
- }
12
-
13
- /**
14
- * Make an API call, routing through the Strayl proxy in production.
15
- * @param service - Service name for proxy routing (e.g. "tavily", "jina", "openrouter")
16
- * @param directUrl - Direct API URL (used in STRAYL_LLM_DIRECT=1 mode)
17
- * @param directApiKey - Direct API key env var name (e.g. "TAVILY_API_KEY")
18
- * @param init - Fetch init (method, headers, body)
19
- * @param env - Environment variables
20
- */
21
- export async function proxyFetch(
22
- service: string,
23
- directUrl: string,
24
- directApiKey: string,
25
- init: RequestInit,
26
- env: Record<string, string>,
27
- ): Promise<Response> {
28
- if (env.STRAYL_LLM_DIRECT === "1") {
29
- // Direct mode — use raw API key
30
- const key = env[directApiKey];
31
- if (!key) {
32
- throw new Error(`${directApiKey} not configured (direct mode)`);
33
- }
34
- const headers = new Headers(init.headers);
35
- if (!headers.has("Authorization")) {
36
- headers.set("Authorization", `Bearer ${key}`);
37
- }
38
- return fetch(directUrl, { ...init, headers });
39
- }
40
-
41
- // Proxy mode — route through api.strayl.dev
42
- const apiUrl = env.STRAYL_API_URL ?? "https://api.strayl.dev";
43
- const token = env.STRAYL_SESSION_TOKEN ?? "";
44
- const proxyUrl = `${apiUrl}/v1/tools/${service}`;
45
-
46
- const headers = new Headers(init.headers);
47
- headers.set("Authorization", `Bearer ${token}`);
48
- headers.set("X-Strayl-Direct-URL", directUrl);
49
-
50
- return fetch(proxyUrl, { ...init, headers });
51
- }
@@ -1,38 +0,0 @@
1
- import { z } from "zod";
2
- import type { ToolDefinition, ToolContext } from "../../types.js";
3
- import type { SubAgentManager } from "../../subagents/manager.js";
4
-
5
- export function createTaskTool(subAgentManager: SubAgentManager): ToolDefinition {
6
- return {
7
- name: "task",
8
- description:
9
- "Delegate work to a specialized sub-agent. " +
10
- "Use code-explorer for codebase search, web-researcher for docs/APIs, " +
11
- "general-purpose for combined tasks. Sub-agents run in the same process " +
12
- "and return their result when complete.",
13
- parameters: z.object({
14
- agent: z
15
- .enum(["code-explorer", "web-researcher", "general-purpose"])
16
- .describe("Which sub-agent to use"),
17
- description: z.string().describe("Short task title (3-6 words)"),
18
- prompt: z.string().describe("Detailed task instructions"),
19
- }),
20
- execute: async (rawArgs: unknown, ctx: ToolContext): Promise<string> => {
21
- const args = rawArgs as {
22
- agent: string;
23
- description: string;
24
- prompt: string;
25
- };
26
-
27
- const id = subAgentManager.spawn({
28
- agentName: args.agent,
29
- label: args.description,
30
- prompt: args.prompt,
31
- workDir: ctx.workDir,
32
- env: ctx.env,
33
- });
34
-
35
- return await subAgentManager.waitFor(id);
36
- },
37
- };
38
- }
@@ -1,20 +0,0 @@
1
- import { z } from "zod";
2
- import type { ToolDefinition } from "../../types.js";
3
-
4
- export const waitTool: ToolDefinition = {
5
- name: "wait",
6
- description:
7
- "Wait for a specified number of seconds. Useful for waiting for background processes to start. " +
8
- "Clamped between 1 and 120 seconds.",
9
- parameters: z.object({
10
- seconds: z.number().describe("Number of seconds to wait (1-120)"),
11
- }),
12
- execute: async (rawArgs: unknown): Promise<string> => {
13
- const args = rawArgs as { seconds: number };
14
- const seconds = Math.max(1, Math.min(120, args.seconds));
15
-
16
- await new Promise(resolve => setTimeout(resolve, seconds * 1000));
17
-
18
- return JSON.stringify({ waited: seconds });
19
- },
20
- };