@flue/sdk 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -9
- package/dist/agent-BB4lwAd5.mjs +453 -0
- package/dist/client.d.mts +26 -0
- package/dist/client.mjs +78 -0
- package/dist/cloudflare/index.d.mts +35 -0
- package/dist/cloudflare/index.mjs +241 -0
- package/dist/command-helpers-DdAfbnom.d.mts +21 -0
- package/dist/command-helpers-hTZKWK13.mjs +37 -0
- package/dist/index.d.mts +116 -0
- package/dist/index.mjs +1664 -0
- package/dist/internal.d.mts +29 -0
- package/dist/internal.mjs +39 -0
- package/dist/mcp-BVF-sOBZ.d.mts +22 -0
- package/dist/mcp-DOgMtp8y.mjs +285 -0
- package/dist/node/index.d.mts +14 -0
- package/dist/node/index.mjs +75 -0
- package/dist/sandbox.d.mts +29 -0
- package/dist/sandbox.mjs +132 -0
- package/dist/session-DukL3zwF.mjs +1303 -0
- package/dist/types-T8pE1xIS.d.mts +461 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -240,18 +240,18 @@ POST /agents/<agent-name>/<id>
|
|
|
240
240
|
By default, `agent.session()` opens the default session for that agent ID. Reuse the same agent ID to continue the same default conversation. Use a new agent ID to start fresh.
|
|
241
241
|
|
|
242
242
|
```bash
|
|
243
|
-
# Start a conversation
|
|
244
|
-
curl http://localhost:
|
|
243
|
+
# Start a conversation (port 3583 is `flue dev`'s default)
|
|
244
|
+
curl http://localhost:3583/agents/hello/session-abc \
|
|
245
245
|
-H "Content-Type: application/json" \
|
|
246
246
|
-d '{"name": "Alice"}'
|
|
247
247
|
|
|
248
248
|
# Continue that conversation
|
|
249
|
-
curl http://localhost:
|
|
249
|
+
curl http://localhost:3583/agents/hello/session-abc \
|
|
250
250
|
-H "Content-Type: application/json" \
|
|
251
251
|
-d '{"name": "Alice"}'
|
|
252
252
|
|
|
253
253
|
# Start a separate conversation
|
|
254
|
-
curl http://localhost:
|
|
254
|
+
curl http://localhost:3583/agents/hello/session-xyz \
|
|
255
255
|
-H "Content-Type: application/json" \
|
|
256
256
|
-d '{"name": "Alice"}'
|
|
257
257
|
```
|
|
@@ -305,22 +305,48 @@ const session = await agent.session();
|
|
|
305
305
|
|
|
306
306
|
## Running Agents
|
|
307
307
|
|
|
308
|
-
###
|
|
308
|
+
### Local Development (`flue dev`)
|
|
309
309
|
|
|
310
|
-
|
|
310
|
+
Long-running watch-mode dev server. Rebuilds and reloads on file changes — edit an agent, re-run `curl`, see your change.
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
flue dev --target node # Node.js dev server
|
|
314
|
+
flue dev --target cloudflare # Cloudflare Workers (via wrangler) dev server
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Defaults to port `3583` ("FLUE" on a phone keypad). Override with `--port`.
|
|
318
|
+
|
|
319
|
+
`flue dev --target cloudflare` requires `wrangler` as a peer dependency in your project (`npm install --save-dev wrangler`).
|
|
320
|
+
|
|
321
|
+
#### Loading environment variables
|
|
322
|
+
|
|
323
|
+
Pass `--env <path>` to load a `.env`-format file. Works for both targets:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
flue dev --target node --env .env
|
|
327
|
+
flue dev --target cloudflare --env .env
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Repeatable; later files override earlier ones on key collision. Shell-set env vars win over file values. Edits to the file trigger a reload. Same flag works for `flue run`.
|
|
331
|
+
|
|
332
|
+
### Trigger From the CLI (`flue run`)
|
|
333
|
+
|
|
334
|
+
Build and run any agent locally, perfect for running in CI or for one-shot scripted invocations. Production-shaped — builds the deployable artifact and starts it once.
|
|
311
335
|
|
|
312
336
|
```bash
|
|
313
337
|
flue run hello --target node --id test-1 \
|
|
314
338
|
--payload '{"text": "Hello world", "language": "French"}'
|
|
315
339
|
```
|
|
316
340
|
|
|
317
|
-
### Trigger From HTTP Endpoint
|
|
341
|
+
### Trigger From HTTP Endpoint (`flue build`)
|
|
318
342
|
|
|
319
343
|
Build and deploy your agents as a web server, perfect for hosted agents.
|
|
320
344
|
|
|
321
|
-
`flue build` builds to a `./dist` directory, which you can then deploy
|
|
345
|
+
`flue build` builds to a `./dist` directory, which you can then deploy. Cloudflare and any Node.js host are supported today, with more coming in the future.
|
|
322
346
|
|
|
323
347
|
```
|
|
324
|
-
flue build --target node # Node.js server
|
|
348
|
+
flue build --target node # Node.js server (single bundled .mjs)
|
|
325
349
|
flue build --target cloudflare # Cloudflare Workers + Durable Objects
|
|
326
350
|
```
|
|
351
|
+
|
|
352
|
+
For Cloudflare, `flue build` produces an unbundled TypeScript entry that `wrangler deploy` bundles itself — the same path `flue dev --target cloudflare` uses. Dev and deploy go through the same bundler, so what works in dev will work in production.
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { Type } from "@mariozechner/pi-ai";
|
|
2
|
+
|
|
3
|
+
//#region src/context.ts
|
|
4
|
+
/** Parse optional YAML frontmatter (--- delimited). Basic `key: value` only. */
|
|
5
|
+
function parseFrontmatterFile(content, defaultName) {
|
|
6
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
7
|
+
if (!frontmatterMatch) return {
|
|
8
|
+
name: defaultName,
|
|
9
|
+
description: "",
|
|
10
|
+
body: content.trim(),
|
|
11
|
+
frontmatter: {}
|
|
12
|
+
};
|
|
13
|
+
const rawFrontmatter = frontmatterMatch[1] ?? "";
|
|
14
|
+
const body = frontmatterMatch[2] ?? "";
|
|
15
|
+
const frontmatter = {};
|
|
16
|
+
for (const line of rawFrontmatter.split("\n")) {
|
|
17
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
18
|
+
if (match?.[1] && match[2]) frontmatter[match[1]] = match[2].trim();
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
name: frontmatter.name || defaultName,
|
|
22
|
+
description: frontmatter.description || "",
|
|
23
|
+
body: body.trim(),
|
|
24
|
+
frontmatter
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Read AGENTS.md (and CLAUDE.md if present) from a directory. Returns concatenated contents. */
|
|
28
|
+
async function readAgentsMd(env, basePath) {
|
|
29
|
+
const parts = [];
|
|
30
|
+
for (const filename of ["AGENTS.md", "CLAUDE.md"]) {
|
|
31
|
+
const filePath = basePath.endsWith("/") ? basePath + filename : `${basePath}/${filename}`;
|
|
32
|
+
if (await env.exists(filePath)) {
|
|
33
|
+
const content = await env.readFile(filePath);
|
|
34
|
+
parts.push(content.trim());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return parts.join("\n\n");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Load a skill directly by relative path under `.agents/skills/`.
|
|
41
|
+
*
|
|
42
|
+
* The path is taken as-is — no extension is auto-appended. Callers reference
|
|
43
|
+
* the full filename, e.g. `'triage/reproduce.md'`. Returns `null` if the file
|
|
44
|
+
* doesn't exist.
|
|
45
|
+
*
|
|
46
|
+
* Used as a fallback by `session.skill()` when the requested name doesn't match
|
|
47
|
+
* a discovered skill's frontmatter `name:` field. Lets users organise skills as
|
|
48
|
+
* a pack of sibling markdown files under one directory (orchestration SKILL.md
|
|
49
|
+
* + stage files) without forcing each stage into its own `SKILL.md` subdirectory.
|
|
50
|
+
*/
|
|
51
|
+
async function loadSkillByPath(env, basePath, relPath) {
|
|
52
|
+
const filePath = `${basePath.endsWith("/") ? `${basePath}.agents/skills` : `${basePath}/.agents/skills`}/${relPath}`;
|
|
53
|
+
if (!await env.exists(filePath)) return null;
|
|
54
|
+
const parsed = parseFrontmatterFile(await env.readFile(filePath), relPath.replace(/\.(md|markdown)$/i, ""));
|
|
55
|
+
return {
|
|
56
|
+
name: parsed.name,
|
|
57
|
+
description: parsed.description,
|
|
58
|
+
instructions: parsed.body
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Discover skills from .agents/skills/<name>/SKILL.md under basePath. */
|
|
62
|
+
async function discoverLocalSkills(env, basePath) {
|
|
63
|
+
const skillsDir = basePath.endsWith("/") ? `${basePath}.agents/skills` : `${basePath}/.agents/skills`;
|
|
64
|
+
if (!await env.exists(skillsDir)) return {};
|
|
65
|
+
const skills = {};
|
|
66
|
+
const entries = await env.readdir(skillsDir);
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const skillDir = `${skillsDir}/${entry}`;
|
|
69
|
+
try {
|
|
70
|
+
if (!(await env.stat(skillDir)).isDirectory) continue;
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const skillMdPath = `${skillDir}/SKILL.md`;
|
|
75
|
+
if (!await env.exists(skillMdPath)) continue;
|
|
76
|
+
const parsed = parseFrontmatterFile(await env.readFile(skillMdPath), entry);
|
|
77
|
+
skills[parsed.name] = {
|
|
78
|
+
name: parsed.name,
|
|
79
|
+
description: parsed.description,
|
|
80
|
+
instructions: parsed.body
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return skills;
|
|
84
|
+
}
|
|
85
|
+
function composeSystemPrompt(agentsMd, skills, env) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
if (agentsMd) parts.push(agentsMd);
|
|
88
|
+
const skillEntries = Object.values(skills);
|
|
89
|
+
if (skillEntries.length > 0) {
|
|
90
|
+
parts.push("", "## Available Skills", "");
|
|
91
|
+
for (const skill of skillEntries) {
|
|
92
|
+
const desc = skill.description ? ` - ${skill.description}` : "";
|
|
93
|
+
parts.push(`- **${skill.name}**${desc}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (env) {
|
|
97
|
+
const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
|
|
98
|
+
weekday: "short",
|
|
99
|
+
year: "numeric",
|
|
100
|
+
month: "short",
|
|
101
|
+
day: "numeric"
|
|
102
|
+
});
|
|
103
|
+
parts.push("", `Date: ${date}`);
|
|
104
|
+
parts.push(`Working directory: ${env.cwd}`);
|
|
105
|
+
if (env.directoryListing && env.directoryListing.length > 0) parts.push("", "Directory structure:", env.directoryListing.join("\n"));
|
|
106
|
+
}
|
|
107
|
+
return parts.join("\n");
|
|
108
|
+
}
|
|
109
|
+
/** Discover AGENTS.md, local skills, and directory listing from the session's cwd. */
|
|
110
|
+
async function discoverSessionContext(env) {
|
|
111
|
+
const cwd = env.cwd;
|
|
112
|
+
const agentsMd = await readAgentsMd(env, cwd);
|
|
113
|
+
const skills = await discoverLocalSkills(env, cwd);
|
|
114
|
+
let directoryListing;
|
|
115
|
+
try {
|
|
116
|
+
directoryListing = await env.readdir(cwd);
|
|
117
|
+
} catch {}
|
|
118
|
+
return {
|
|
119
|
+
systemPrompt: composeSystemPrompt(agentsMd, skills, {
|
|
120
|
+
cwd,
|
|
121
|
+
directoryListing
|
|
122
|
+
}),
|
|
123
|
+
skills
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/agent.ts
|
|
129
|
+
const MAX_READ_LINES = 2e3;
|
|
130
|
+
const MAX_READ_BYTES = 50 * 1024;
|
|
131
|
+
const MAX_GREP_MATCHES = 100;
|
|
132
|
+
const MAX_GREP_LINE_LENGTH = 500;
|
|
133
|
+
const MAX_GLOB_RESULTS = 1e3;
|
|
134
|
+
const BUILTIN_TOOL_NAMES = new Set([
|
|
135
|
+
"read",
|
|
136
|
+
"write",
|
|
137
|
+
"edit",
|
|
138
|
+
"bash",
|
|
139
|
+
"grep",
|
|
140
|
+
"glob",
|
|
141
|
+
"task"
|
|
142
|
+
]);
|
|
143
|
+
function createTools(env, options) {
|
|
144
|
+
const tools = [
|
|
145
|
+
createReadTool(env),
|
|
146
|
+
createWriteTool(env),
|
|
147
|
+
createEditTool(env),
|
|
148
|
+
createBashTool(env),
|
|
149
|
+
createGrepTool(env),
|
|
150
|
+
createGlobTool(env)
|
|
151
|
+
];
|
|
152
|
+
if (options?.task) tools.push(createTaskTool(options.task, options.roles ?? {}));
|
|
153
|
+
return tools;
|
|
154
|
+
}
|
|
155
|
+
function createReadTool(env) {
|
|
156
|
+
return {
|
|
157
|
+
name: "read",
|
|
158
|
+
label: "Read File",
|
|
159
|
+
description: "Read a file or list a directory. For files, output is truncated to 2000 lines or 50KB — use offset/limit for large files. For directories, returns the list of entries.",
|
|
160
|
+
parameters: Type.Object({
|
|
161
|
+
path: Type.String({ description: "Path to the file to read" }),
|
|
162
|
+
offset: Type.Optional(Type.Number({ description: "Line number to start from (1-indexed)" })),
|
|
163
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" }))
|
|
164
|
+
}),
|
|
165
|
+
async execute(_toolCallId, params, signal) {
|
|
166
|
+
throwIfAborted(signal);
|
|
167
|
+
try {
|
|
168
|
+
if ((await env.stat(params.path)).isDirectory) {
|
|
169
|
+
const entries = await env.readdir(params.path);
|
|
170
|
+
return {
|
|
171
|
+
content: [{
|
|
172
|
+
type: "text",
|
|
173
|
+
text: entries.join("\n") || "(empty directory)"
|
|
174
|
+
}],
|
|
175
|
+
details: {
|
|
176
|
+
path: params.path,
|
|
177
|
+
isDirectory: true,
|
|
178
|
+
entries: entries.length
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
} catch {}
|
|
183
|
+
const allLines = (await env.readFile(params.path)).split("\n");
|
|
184
|
+
const startLine = params.offset ? Math.max(0, params.offset - 1) : 0;
|
|
185
|
+
if (startLine >= allLines.length) throw new Error(`Offset ${params.offset} is beyond end of file (${allLines.length} lines total)`);
|
|
186
|
+
const endLine = params.limit ? startLine + params.limit : allLines.length;
|
|
187
|
+
const { text: truncatedText, wasTruncated } = truncateHead(allLines.slice(startLine, endLine), MAX_READ_LINES, MAX_READ_BYTES);
|
|
188
|
+
let output = truncatedText;
|
|
189
|
+
if (wasTruncated) {
|
|
190
|
+
const shownEnd = startLine + truncatedText.split("\n").length;
|
|
191
|
+
output += `\n\n[Showing lines ${startLine + 1}-${shownEnd} of ${allLines.length}. Use offset=${shownEnd + 1} to continue.]`;
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
content: [{
|
|
195
|
+
type: "text",
|
|
196
|
+
text: output
|
|
197
|
+
}],
|
|
198
|
+
details: {
|
|
199
|
+
path: params.path,
|
|
200
|
+
lines: allLines.length
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function createWriteTool(env) {
|
|
207
|
+
return {
|
|
208
|
+
name: "write",
|
|
209
|
+
label: "Write File",
|
|
210
|
+
description: "Write content to a file. Creates the file and parent directories if they do not exist.",
|
|
211
|
+
parameters: Type.Object({
|
|
212
|
+
path: Type.String({ description: "Path to the file to write" }),
|
|
213
|
+
content: Type.String({ description: "Content to write to the file" })
|
|
214
|
+
}),
|
|
215
|
+
async execute(_toolCallId, params, signal) {
|
|
216
|
+
throwIfAborted(signal);
|
|
217
|
+
const resolved = env.resolvePath(params.path);
|
|
218
|
+
const dir = resolved.replace(/\/[^/]*$/, "");
|
|
219
|
+
if (dir && dir !== resolved) await env.mkdir(dir, { recursive: true });
|
|
220
|
+
await env.writeFile(resolved, params.content);
|
|
221
|
+
return {
|
|
222
|
+
content: [{
|
|
223
|
+
type: "text",
|
|
224
|
+
text: `Successfully wrote ${params.content.length} bytes to ${params.path}`
|
|
225
|
+
}],
|
|
226
|
+
details: {
|
|
227
|
+
path: params.path,
|
|
228
|
+
size: params.content.length
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function createEditTool(env) {
|
|
235
|
+
return {
|
|
236
|
+
name: "edit",
|
|
237
|
+
label: "Edit File",
|
|
238
|
+
description: "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.",
|
|
239
|
+
parameters: Type.Object({
|
|
240
|
+
path: Type.String({ description: "Path to the file to edit" }),
|
|
241
|
+
oldText: Type.String({ description: "Exact text to find (must be unique)" }),
|
|
242
|
+
newText: Type.String({ description: "Replacement text" }),
|
|
243
|
+
replaceAll: Type.Optional(Type.Boolean({ description: "Replace all occurrences" }))
|
|
244
|
+
}),
|
|
245
|
+
async execute(_toolCallId, params, signal) {
|
|
246
|
+
throwIfAborted(signal);
|
|
247
|
+
const content = await env.readFile(params.path);
|
|
248
|
+
if (params.replaceAll) {
|
|
249
|
+
const newContent = content.replaceAll(params.oldText, params.newText);
|
|
250
|
+
if (newContent === content) throw new Error(`Could not find the text in ${params.path}. No changes made.`);
|
|
251
|
+
await env.writeFile(params.path, newContent);
|
|
252
|
+
const count = content.split(params.oldText).length - 1;
|
|
253
|
+
return {
|
|
254
|
+
content: [{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: `Replaced ${count} occurrences in ${params.path}`
|
|
257
|
+
}],
|
|
258
|
+
details: {
|
|
259
|
+
path: params.path,
|
|
260
|
+
replacements: count
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const occurrences = countOccurrences(content, params.oldText);
|
|
265
|
+
if (occurrences === 0) throw new Error(`Could not find the exact text in ${params.path}. Make sure your oldText matches exactly, including whitespace and indentation.`);
|
|
266
|
+
if (occurrences > 1) throw new Error(`Found ${occurrences} occurrences of the text in ${params.path}. Provide more surrounding context to make the match unique, or use replaceAll.`);
|
|
267
|
+
const newContent = content.replace(params.oldText, params.newText);
|
|
268
|
+
await env.writeFile(params.path, newContent);
|
|
269
|
+
return {
|
|
270
|
+
content: [{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: `Successfully edited ${params.path}`
|
|
273
|
+
}],
|
|
274
|
+
details: { path: params.path }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function createBashTool(env) {
|
|
280
|
+
return {
|
|
281
|
+
name: "bash",
|
|
282
|
+
label: "Run Command",
|
|
283
|
+
description: "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.",
|
|
284
|
+
parameters: Type.Object({
|
|
285
|
+
command: Type.String({ description: "Bash command to execute" }),
|
|
286
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" }))
|
|
287
|
+
}),
|
|
288
|
+
async execute(_toolCallId, params, signal) {
|
|
289
|
+
throwIfAborted(signal);
|
|
290
|
+
return formatBashResult(await env.exec(params.command), params.command);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function createTaskTool(runTask, roles) {
|
|
295
|
+
const roleNames = Object.keys(roles);
|
|
296
|
+
return {
|
|
297
|
+
name: "task",
|
|
298
|
+
label: "Run Task",
|
|
299
|
+
description: "Delegate a focused task to a detached child agent with its own context. Use this for independent research, file exploration, or parallel work. The task returns only its final answer to this conversation." + (roleNames.length > 0 ? ` Available roles: ${roleNames.join(", ")}.` : " No roles are currently defined."),
|
|
300
|
+
parameters: Type.Object({
|
|
301
|
+
description: Type.Optional(Type.String({ description: "Short human-readable label for the delegated work" })),
|
|
302
|
+
prompt: Type.String({ description: "Focused instructions for the child agent" }),
|
|
303
|
+
role: Type.Optional(Type.String({ description: "Role to use for the child agent" })),
|
|
304
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the child agent. AGENTS.md and skills are discovered from here." }))
|
|
305
|
+
}),
|
|
306
|
+
async execute(_toolCallId, params, signal) {
|
|
307
|
+
throwIfAborted(signal);
|
|
308
|
+
return runTask(params, signal);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function formatBashResult(result, command) {
|
|
313
|
+
const { text: output } = truncateTail((result.stdout + (result.stderr ? "\n" + result.stderr : "")).trim(), MAX_READ_LINES, MAX_READ_BYTES);
|
|
314
|
+
if (result.exitCode !== 0) throw new Error(`${output}\n\nCommand exited with code ${result.exitCode}`);
|
|
315
|
+
return {
|
|
316
|
+
content: [{
|
|
317
|
+
type: "text",
|
|
318
|
+
text: output || "(no output)"
|
|
319
|
+
}],
|
|
320
|
+
details: {
|
|
321
|
+
command,
|
|
322
|
+
exitCode: result.exitCode
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function createGrepTool(env) {
|
|
327
|
+
return {
|
|
328
|
+
name: "grep",
|
|
329
|
+
label: "Search Files",
|
|
330
|
+
description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.",
|
|
331
|
+
parameters: Type.Object({
|
|
332
|
+
pattern: Type.String({ description: "Search pattern (regex)" }),
|
|
333
|
+
path: Type.Optional(Type.String({ description: "Directory or file to search (default: .)" })),
|
|
334
|
+
include: Type.Optional(Type.String({ description: "Glob filter, e.g. \"*.ts\"" }))
|
|
335
|
+
}),
|
|
336
|
+
async execute(_toolCallId, params, signal) {
|
|
337
|
+
throwIfAborted(signal);
|
|
338
|
+
const searchPath = params.path || ".";
|
|
339
|
+
let cmd = `grep -rn "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
|
|
340
|
+
if (params.include) cmd = `grep -rn --include="${escapeShellArg(params.include)}" "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
|
|
341
|
+
const result = await env.exec(cmd);
|
|
342
|
+
if (result.exitCode === 1 && !result.stdout.trim()) return {
|
|
343
|
+
content: [{
|
|
344
|
+
type: "text",
|
|
345
|
+
text: "No matches found."
|
|
346
|
+
}],
|
|
347
|
+
details: { matchCount: 0 }
|
|
348
|
+
};
|
|
349
|
+
if (result.exitCode > 1) throw new Error(`grep failed: ${result.stderr}`);
|
|
350
|
+
const lines = result.stdout.trim().split("\n");
|
|
351
|
+
let finalOutput = lines.slice(0, MAX_GREP_MATCHES).map((line) => line.length > MAX_GREP_LINE_LENGTH ? line.slice(0, MAX_GREP_LINE_LENGTH) + "..." : line).join("\n");
|
|
352
|
+
if (lines.length > MAX_GREP_MATCHES) finalOutput += `\n\n[Showing ${MAX_GREP_MATCHES} of ${lines.length} matches. Narrow your search.]`;
|
|
353
|
+
return {
|
|
354
|
+
content: [{
|
|
355
|
+
type: "text",
|
|
356
|
+
text: finalOutput
|
|
357
|
+
}],
|
|
358
|
+
details: { matchCount: Math.min(lines.length, MAX_GREP_MATCHES) }
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function createGlobTool(env) {
|
|
364
|
+
return {
|
|
365
|
+
name: "glob",
|
|
366
|
+
label: "Find Files",
|
|
367
|
+
description: "Find files by glob pattern. Returns matching file paths.",
|
|
368
|
+
parameters: Type.Object({
|
|
369
|
+
pattern: Type.String({ description: "Glob pattern, e.g. \"**/*.ts\"" }),
|
|
370
|
+
path: Type.Optional(Type.String({ description: "Directory to search in (default: .)" }))
|
|
371
|
+
}),
|
|
372
|
+
async execute(_toolCallId, params, signal) {
|
|
373
|
+
throwIfAborted(signal);
|
|
374
|
+
const cmd = `find ${escapeShellArg(params.path || ".")} -type f -name "${escapeShellArg(params.pattern)}" 2>/dev/null | head -${MAX_GLOB_RESULTS}`;
|
|
375
|
+
const result = await env.exec(cmd);
|
|
376
|
+
if (result.exitCode !== 0 && !result.stdout.trim()) return {
|
|
377
|
+
content: [{
|
|
378
|
+
type: "text",
|
|
379
|
+
text: "No files found matching pattern."
|
|
380
|
+
}],
|
|
381
|
+
details: { matchCount: 0 }
|
|
382
|
+
};
|
|
383
|
+
const paths = result.stdout.trim().split("\n").filter(Boolean);
|
|
384
|
+
if (paths.length === 0) return {
|
|
385
|
+
content: [{
|
|
386
|
+
type: "text",
|
|
387
|
+
text: "No files found matching pattern."
|
|
388
|
+
}],
|
|
389
|
+
details: { matchCount: 0 }
|
|
390
|
+
};
|
|
391
|
+
return {
|
|
392
|
+
content: [{
|
|
393
|
+
type: "text",
|
|
394
|
+
text: paths.join("\n")
|
|
395
|
+
}],
|
|
396
|
+
details: { matchCount: paths.length }
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function throwIfAborted(signal) {
|
|
402
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
403
|
+
}
|
|
404
|
+
function countOccurrences(str, substr) {
|
|
405
|
+
let count = 0;
|
|
406
|
+
let pos = str.indexOf(substr, 0);
|
|
407
|
+
while (pos !== -1) {
|
|
408
|
+
count++;
|
|
409
|
+
pos = str.indexOf(substr, pos + substr.length);
|
|
410
|
+
}
|
|
411
|
+
return count;
|
|
412
|
+
}
|
|
413
|
+
function escapeShellArg(arg) {
|
|
414
|
+
return arg.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/`/g, "\\`");
|
|
415
|
+
}
|
|
416
|
+
function truncateHead(lines, maxLines, maxBytes) {
|
|
417
|
+
let result = "";
|
|
418
|
+
let lineCount = 0;
|
|
419
|
+
let wasTruncated = false;
|
|
420
|
+
for (const line of lines) {
|
|
421
|
+
if (lineCount >= maxLines) {
|
|
422
|
+
wasTruncated = true;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
const next = lineCount === 0 ? line : "\n" + line;
|
|
426
|
+
if (result.length + next.length > maxBytes) {
|
|
427
|
+
wasTruncated = true;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
result += next;
|
|
431
|
+
lineCount++;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
text: result,
|
|
435
|
+
wasTruncated
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function truncateTail(text, maxLines, maxBytes) {
|
|
439
|
+
const lines = text.split("\n");
|
|
440
|
+
if (lines.length <= maxLines && text.length <= maxBytes) return {
|
|
441
|
+
text,
|
|
442
|
+
wasTruncated: false
|
|
443
|
+
};
|
|
444
|
+
let result = lines.slice(-maxLines).join("\n");
|
|
445
|
+
if (result.length > maxBytes) result = result.slice(-maxBytes);
|
|
446
|
+
return {
|
|
447
|
+
text: result,
|
|
448
|
+
wasTruncated: true
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
//#endregion
|
|
453
|
+
export { parseFrontmatterFile as a, loadSkillByPath as i, createTools as n, discoverSessionContext as r, BUILTIN_TOOL_NAMES as t };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { A as TaskOptions, C as SessionEnv, D as ShellResult, E as ShellOptions, M as ToolParameters, S as SessionData, T as SessionStore, _ as FlueSessions, a as BashLike, d as FileStat, f as FlueAgent, g as FlueSession, h as FlueEventCallback, i as BashFactory, j as ToolDef, k as SkillOptions, l as Command, m as FlueEvent, p as FlueContext, r as AgentInit, t as AgentConfig, v as PromptOptions, w as SessionOptions, x as SandboxFactory, y as PromptResponse } from "./types-T8pE1xIS.mjs";
|
|
2
|
+
import { i as connectMcpServer, n as McpServerOptions, r as McpTransport, t as McpServerConnection } from "./mcp-BVF-sOBZ.mjs";
|
|
3
|
+
import { Type } from "@mariozechner/pi-ai";
|
|
4
|
+
|
|
5
|
+
//#region src/client.d.ts
|
|
6
|
+
interface FlueContextConfig {
|
|
7
|
+
id: string;
|
|
8
|
+
payload: any;
|
|
9
|
+
env: Record<string, any>;
|
|
10
|
+
agentConfig: AgentConfig;
|
|
11
|
+
createDefaultEnv: () => Promise<SessionEnv>;
|
|
12
|
+
createLocalEnv: () => Promise<SessionEnv>;
|
|
13
|
+
defaultStore: SessionStore;
|
|
14
|
+
/**
|
|
15
|
+
* Platform-specific sandbox resolver hook. Called before default resolution.
|
|
16
|
+
* Returns SessionEnv to use, or null to fall through to default logic.
|
|
17
|
+
*/
|
|
18
|
+
resolveSandbox?: (sandbox: unknown) => Promise<SessionEnv> | null;
|
|
19
|
+
}
|
|
20
|
+
/** Extends FlueContext with server-only methods. Agent handlers only see FlueContext. */
|
|
21
|
+
interface FlueContextInternal extends FlueContext {
|
|
22
|
+
setEventCallback(callback: FlueEventCallback | undefined): void;
|
|
23
|
+
}
|
|
24
|
+
declare function createFlueContext(config: FlueContextConfig): FlueContextInternal;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { type AgentInit, type BashFactory, type BashLike, type Command, type FileStat, type FlueAgent, type FlueContext, FlueContextConfig, FlueContextInternal, type FlueEvent, type FlueEventCallback, type FlueSession, type FlueSessions, type McpServerConnection, type McpServerOptions, type McpTransport, type PromptOptions, type PromptResponse, type SandboxFactory, type SessionData, type SessionEnv, type SessionOptions, type SessionStore, type ShellOptions, type ShellResult, type SkillOptions, type TaskOptions, type ToolDef, type ToolParameters, Type, connectMcpServer, createFlueContext };
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { r as discoverSessionContext } from "./agent-BB4lwAd5.mjs";
|
|
2
|
+
import { a as assertRoleExists } from "./session-DukL3zwF.mjs";
|
|
3
|
+
import { bashFactoryToSessionEnv, createCwdSessionEnv } from "./sandbox.mjs";
|
|
4
|
+
import { n as AgentClient, t as connectMcpServer } from "./mcp-DOgMtp8y.mjs";
|
|
5
|
+
import { Type } from "@mariozechner/pi-ai";
|
|
6
|
+
|
|
7
|
+
//#region src/client.ts
|
|
8
|
+
function createFlueContext(config) {
|
|
9
|
+
let currentEventCallback;
|
|
10
|
+
const initializedAgentIds = /* @__PURE__ */ new Set();
|
|
11
|
+
return {
|
|
12
|
+
get id() {
|
|
13
|
+
return config.id;
|
|
14
|
+
},
|
|
15
|
+
get payload() {
|
|
16
|
+
return config.payload;
|
|
17
|
+
},
|
|
18
|
+
get env() {
|
|
19
|
+
return config.env;
|
|
20
|
+
},
|
|
21
|
+
async init(options) {
|
|
22
|
+
const id = options?.id ?? config.id;
|
|
23
|
+
if (initializedAgentIds.has(id)) throw new Error(`[flue] init() has already been called for agent "${id}" in this request.`);
|
|
24
|
+
initializedAgentIds.add(id);
|
|
25
|
+
try {
|
|
26
|
+
assertRoleExists(config.agentConfig.roles, options?.role);
|
|
27
|
+
const sandbox = options?.sandbox;
|
|
28
|
+
const baseEnv = await resolveSessionEnv(id, sandbox, config, options?.cwd);
|
|
29
|
+
const env = options?.cwd ? createCwdSessionEnv(baseEnv, options.cwd) : baseEnv;
|
|
30
|
+
const store = options?.persist ?? config.defaultStore;
|
|
31
|
+
const localContext = await discoverSessionContext(env);
|
|
32
|
+
const agentModel = options?.model && config.agentConfig.resolveModel ? config.agentConfig.resolveModel(options.model) : config.agentConfig.model;
|
|
33
|
+
return new AgentClient(id, {
|
|
34
|
+
...config.agentConfig,
|
|
35
|
+
systemPrompt: localContext.systemPrompt,
|
|
36
|
+
skills: localContext.skills,
|
|
37
|
+
model: agentModel,
|
|
38
|
+
role: options?.role ?? config.agentConfig.role
|
|
39
|
+
}, env, store, currentEventCallback, options?.commands, options?.tools);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
initializedAgentIds.delete(id);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
setEventCallback(callback) {
|
|
46
|
+
currentEventCallback = callback;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Duck-type detection for just-bash Bash instances. */
|
|
51
|
+
function isBashLike(value) {
|
|
52
|
+
return typeof value === "object" && value !== null && "exec" in value && "getCwd" in value && "fs" in value && typeof value.exec === "function" && typeof value.getCwd === "function" && typeof value.fs === "object";
|
|
53
|
+
}
|
|
54
|
+
function isBashFactory(value) {
|
|
55
|
+
return typeof value === "function";
|
|
56
|
+
}
|
|
57
|
+
function isSandboxFactory(value) {
|
|
58
|
+
return typeof value === "object" && value !== null && "createSessionEnv" in value && typeof value.createSessionEnv === "function";
|
|
59
|
+
}
|
|
60
|
+
/** Resolve sandbox option to SessionEnv: empty → local → BashFactory → platform hook → SandboxFactory. */
|
|
61
|
+
async function resolveSessionEnv(id, sandbox, config, cwd) {
|
|
62
|
+
if (sandbox === void 0 || sandbox === "empty") return config.createDefaultEnv();
|
|
63
|
+
if (sandbox === "local") return config.createLocalEnv();
|
|
64
|
+
if (isBashFactory(sandbox)) return bashFactoryToSessionEnv(sandbox);
|
|
65
|
+
if (isBashLike(sandbox)) throw new Error("[flue] init({ sandbox }) no longer accepts a Bash-like object directly. Pass a BashFactory instead, e.g. `sandbox: () => new Bash({ fs })`.");
|
|
66
|
+
if (config.resolveSandbox) {
|
|
67
|
+
const resolved = await config.resolveSandbox(sandbox);
|
|
68
|
+
if (resolved) return resolved;
|
|
69
|
+
}
|
|
70
|
+
if (isSandboxFactory(sandbox)) return sandbox.createSessionEnv({
|
|
71
|
+
id,
|
|
72
|
+
cwd
|
|
73
|
+
});
|
|
74
|
+
throw new Error("[flue] Invalid sandbox option passed to init().");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { Type, connectMcpServer, createFlueContext };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { C as SessionEnv, T as SessionStore, l as Command } from "../types-T8pE1xIS.mjs";
|
|
2
|
+
import { t as CommandExecutor } from "../command-helpers-DdAfbnom.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/cloudflare/virtual-sandbox.d.ts
|
|
5
|
+
interface VirtualSandboxOptions {
|
|
6
|
+
/** R2 key prefix for session isolation. */
|
|
7
|
+
prefix?: string;
|
|
8
|
+
}
|
|
9
|
+
declare function getVirtualSandbox(): Promise<any>;
|
|
10
|
+
declare function getVirtualSandbox(bucket: unknown, options?: VirtualSandboxOptions): Promise<any>;
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/cloudflare/define-command.d.ts
|
|
13
|
+
declare function defineCommand(name: string, execute: CommandExecutor): Command;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/cloudflare/cf-sandbox.d.ts
|
|
16
|
+
declare function cfSandboxToSessionEnv(sandbox: any, cwd?: string): Promise<SessionEnv>;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/cloudflare/session-store.d.ts
|
|
19
|
+
declare function store(): SessionStore;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/cloudflare/context.d.ts
|
|
22
|
+
interface CloudflareContext {
|
|
23
|
+
env: Record<string, any>;
|
|
24
|
+
agentInstance: {
|
|
25
|
+
state: any;
|
|
26
|
+
setState(state: any): void;
|
|
27
|
+
};
|
|
28
|
+
storage: {
|
|
29
|
+
sql: any;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
declare function runWithCloudflareContext<T>(ctx: CloudflareContext, fn: () => T): T;
|
|
33
|
+
declare function getCloudflareContext(): CloudflareContext;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { type CloudflareContext, type VirtualSandboxOptions, cfSandboxToSessionEnv, defineCommand, getCloudflareContext, getVirtualSandbox, runWithCloudflareContext, store };
|