@flue/sdk 0.1.0 → 0.1.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 +32 -15
- package/dist/agent-BYG0nVbQ.mjs +432 -0
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +5 -2
- package/dist/cloudflare/index.d.mts +1 -1
- package/dist/cloudflare/index.mjs +2 -1
- package/dist/index.d.mts +2 -13
- package/dist/index.mjs +88 -72
- package/dist/internal.d.mts +15 -0
- package/dist/internal.mjs +6 -0
- package/dist/sandbox.d.mts +1 -1
- package/dist/sandbox.mjs +2 -1
- package/dist/{session-BD0MEuO3.mjs → session-BRLCNVG1.mjs} +38 -413
- package/dist/{types-xNvqlohs.d.mts → types-C8tsaK1j.d.mts} +18 -9
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
# flue
|
|
2
|
-
|
|
3
1
|
> **Experimental** — Flue is under active development. APIs may change.
|
|
2
|
+
>
|
|
3
|
+
> Looking for `v0.0.x`? [See here.](https://github.com/withastro/flue/tree/v0.0.x)
|
|
4
|
+
|
|
5
|
+
# Flue
|
|
6
|
+
|
|
7
|
+
Flue is **The Sandbox Agent Framework.** If you know how to use Claude Code (or OpenCode, Codex, Gemini, etc)... then you already know the basics of how to build agents with Flue.
|
|
4
8
|
|
|
5
|
-
Agent
|
|
9
|
+
A [Sandbox Agent](https://developers.openai.com/api/docs/guides/agents/sandboxes) pairs an **agent harness** (like Claude Code) with a secure, isolated container workspace. Sandbox Agents can edit files, write and execute code, spin up subagents, run terminal commands, and drive themselves autonomously to solve any given task. This pattern unlocks more powerful, intelligent agents that traditional AI frameworks wouldn't otherwise let you build.
|
|
10
|
+
|
|
11
|
+
Our take is that 1) any agent can be represented as a Sandbox Agent, and 2) any agent is _best_ represented as a Sandbox Agent. So we designed Flue to deliver on this vision.
|
|
6
12
|
|
|
7
13
|
## Packages
|
|
8
14
|
|
|
@@ -18,6 +24,8 @@ Agent framework where agents are directories compiled into deployable server art
|
|
|
18
24
|
|
|
19
25
|
The simplest agent — no container, no tools, just a prompt and a typed result.
|
|
20
26
|
|
|
27
|
+
Unless you opt-in to initializing a full container sandbox, Flue will default to a virtual sandbox for every agent, powered by [just-bash](https://github.com/vercel-labs/just-bash). A virtual sandbox is going to be dramatically faster, cheaper, and more scalable than running a full container for every agent, which makes it perfect for building high-traffic/high-scale agents.
|
|
28
|
+
|
|
21
29
|
```ts
|
|
22
30
|
// .flue/agents/hello-world.ts
|
|
23
31
|
import type { FlueContext } from '@flue/sdk/client';
|
|
@@ -34,8 +42,8 @@ export default async function ({ init, payload, sessionId }: FlueContext) {
|
|
|
34
42
|
const session = await init();
|
|
35
43
|
|
|
36
44
|
// prompt() sends a message in the session, triggering action.
|
|
37
|
-
// You can pass a schema to `result` to get typed, validated JSON back.
|
|
38
45
|
const result = await session.prompt(`Translate this to ${payload.language}: "${payload.text}"`, {
|
|
46
|
+
// Pass a result schema to get typed, schema-validated data back from your agent.
|
|
39
47
|
result: v.object({
|
|
40
48
|
translation: v.string(),
|
|
41
49
|
confidence: v.picklist(['low', 'medium', 'high']),
|
|
@@ -48,9 +56,9 @@ export default async function ({ init, payload, sessionId }: FlueContext) {
|
|
|
48
56
|
|
|
49
57
|
### Support Agent
|
|
50
58
|
|
|
51
|
-
A support agent
|
|
59
|
+
A support agent can also run in a virtual sandbox, but we now add a file-system using an R2 bucket. The knowledge base is stored in R2 and mounted directly into the agent's filesystem — the agent searches it with its built-in tools (grep, glob, read). Skills are also defined in the bucket that help the agent perform its task.
|
|
52
60
|
|
|
53
|
-
|
|
61
|
+
Because this agent is deployed to Cloudflare, message history and session state are automatically persisted for you. So you (or your customer) can revisit this support session days, weeks, or years later and pick up exactly where you left off.
|
|
54
62
|
|
|
55
63
|
```ts
|
|
56
64
|
// .flue/agents/support.ts
|
|
@@ -72,18 +80,17 @@ export default async function ({ init, payload, env }: FlueContext) {
|
|
|
72
80
|
relevant to this request, then write a helpful response.
|
|
73
81
|
|
|
74
82
|
Customer: ${payload.message}`,
|
|
83
|
+
{
|
|
84
|
+
// Provide roles (aka subagents) to guide your agent. Defined in .flue/roles/
|
|
85
|
+
role: 'triager',
|
|
86
|
+
},
|
|
75
87
|
);
|
|
76
88
|
}
|
|
77
89
|
```
|
|
78
90
|
|
|
79
91
|
### Issue Triage (CI)
|
|
80
92
|
|
|
81
|
-
A triage agent that runs whenever
|
|
82
|
-
|
|
83
|
-
Flue was designed to power CI workflows since day one. The `"local"` filesystem sandbox enables two things:
|
|
84
|
-
|
|
85
|
-
1. Mount the current directory to your virtual file system.
|
|
86
|
-
2. Connect privileged CLIs to your agent (`gh`, `glab`, `git`) without leaking sensitive keys and secrets.
|
|
93
|
+
A triage agent that runs in CI whenever an issue is opened on GitHub. The `"local"` sandbox mounts the host filesystem and lets you connect privileged CLIs (`gh`, `npm`, `git`) to the agent without leaking secrets.
|
|
87
94
|
|
|
88
95
|
```ts
|
|
89
96
|
// .flue/agents/triage.ts
|
|
@@ -92,6 +99,8 @@ import { execFile } from 'node:child_process';
|
|
|
92
99
|
import { promisify } from 'node:util';
|
|
93
100
|
import * as v from 'valibot';
|
|
94
101
|
|
|
102
|
+
// Because we are running this in CI, we don't need to expose this as an HTTP endpoint.
|
|
103
|
+
// The CLI can run any agent from the command line, `flue run triage ...`
|
|
95
104
|
export const triggers = {};
|
|
96
105
|
|
|
97
106
|
// Connect privileged CLIs to your agent without leaking sensitive keys and secrets.
|
|
@@ -108,15 +117,23 @@ export default async function ({ init, payload }: FlueContext) {
|
|
|
108
117
|
// 'local' mounts the host filesystem at /workspace — ideal for CI
|
|
109
118
|
// where the repo is already checked out. Skills and AGENTS.md are
|
|
110
119
|
// discovered automatically from the workspace directory.
|
|
111
|
-
|
|
120
|
+
//
|
|
121
|
+
// `model` sets the default model for every prompt/skill call in this
|
|
122
|
+
// session. Override per-call with `{ model: '...' }` on prompt()/skill().
|
|
123
|
+
const session = await init({
|
|
124
|
+
sandbox: 'local',
|
|
125
|
+
model: 'anthropic/claude-opus-4-20250514',
|
|
126
|
+
});
|
|
112
127
|
|
|
128
|
+
// Skills can be referenced either by their frontmatter `name:` (shown below)
|
|
129
|
+
// or by a relative path under `.agents/skills/` — e.g.
|
|
130
|
+
// `session.skill('triage/reproduce.md', ...)`. Path references are handy for
|
|
131
|
+
// skill packs that group multiple stages under one directory.
|
|
113
132
|
const result = await session.skill('triage', {
|
|
114
133
|
// Pass arguments to any prompt or skill.
|
|
115
134
|
args: { issueNumber: payload.issueNumber },
|
|
116
135
|
// Grant access to `gh` and `npm` for the life of this skill.
|
|
117
136
|
commands: [gh, npm],
|
|
118
|
-
// Provide roles (aka subagents) to guide your agent. Defined in .flue/roles/
|
|
119
|
-
role: 'triager',
|
|
120
137
|
// Result schemas are great for being able to act/orchestrate
|
|
121
138
|
// based on the result of your prompt or skill call.
|
|
122
139
|
result: v.object({
|
|
@@ -0,0 +1,432 @@
|
|
|
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
|
+
]);
|
|
142
|
+
function createTools(env) {
|
|
143
|
+
return [
|
|
144
|
+
createReadTool(env),
|
|
145
|
+
createWriteTool(env),
|
|
146
|
+
createEditTool(env),
|
|
147
|
+
createBashTool(env),
|
|
148
|
+
createGrepTool(env),
|
|
149
|
+
createGlobTool(env)
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
function createReadTool(env) {
|
|
153
|
+
return {
|
|
154
|
+
name: "read",
|
|
155
|
+
label: "Read File",
|
|
156
|
+
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.",
|
|
157
|
+
parameters: Type.Object({
|
|
158
|
+
path: Type.String({ description: "Path to the file to read" }),
|
|
159
|
+
offset: Type.Optional(Type.Number({ description: "Line number to start from (1-indexed)" })),
|
|
160
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" }))
|
|
161
|
+
}),
|
|
162
|
+
async execute(_toolCallId, params, signal) {
|
|
163
|
+
throwIfAborted(signal);
|
|
164
|
+
try {
|
|
165
|
+
if ((await env.stat(params.path)).isDirectory) {
|
|
166
|
+
const entries = await env.readdir(params.path);
|
|
167
|
+
return {
|
|
168
|
+
content: [{
|
|
169
|
+
type: "text",
|
|
170
|
+
text: entries.join("\n") || "(empty directory)"
|
|
171
|
+
}],
|
|
172
|
+
details: {
|
|
173
|
+
path: params.path,
|
|
174
|
+
isDirectory: true,
|
|
175
|
+
entries: entries.length
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
} catch {}
|
|
180
|
+
const allLines = (await env.readFile(params.path)).split("\n");
|
|
181
|
+
const startLine = params.offset ? Math.max(0, params.offset - 1) : 0;
|
|
182
|
+
if (startLine >= allLines.length) throw new Error(`Offset ${params.offset} is beyond end of file (${allLines.length} lines total)`);
|
|
183
|
+
const endLine = params.limit ? startLine + params.limit : allLines.length;
|
|
184
|
+
const { text: truncatedText, wasTruncated } = truncateHead(allLines.slice(startLine, endLine), MAX_READ_LINES, MAX_READ_BYTES);
|
|
185
|
+
let output = truncatedText;
|
|
186
|
+
if (wasTruncated) {
|
|
187
|
+
const shownEnd = startLine + truncatedText.split("\n").length;
|
|
188
|
+
output += `\n\n[Showing lines ${startLine + 1}-${shownEnd} of ${allLines.length}. Use offset=${shownEnd + 1} to continue.]`;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
content: [{
|
|
192
|
+
type: "text",
|
|
193
|
+
text: output
|
|
194
|
+
}],
|
|
195
|
+
details: {
|
|
196
|
+
path: params.path,
|
|
197
|
+
lines: allLines.length
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function createWriteTool(env) {
|
|
204
|
+
return {
|
|
205
|
+
name: "write",
|
|
206
|
+
label: "Write File",
|
|
207
|
+
description: "Write content to a file. Creates the file and parent directories if they do not exist.",
|
|
208
|
+
parameters: Type.Object({
|
|
209
|
+
path: Type.String({ description: "Path to the file to write" }),
|
|
210
|
+
content: Type.String({ description: "Content to write to the file" })
|
|
211
|
+
}),
|
|
212
|
+
async execute(_toolCallId, params, signal) {
|
|
213
|
+
throwIfAborted(signal);
|
|
214
|
+
const resolved = env.resolvePath(params.path);
|
|
215
|
+
const dir = resolved.replace(/\/[^/]*$/, "");
|
|
216
|
+
if (dir && dir !== resolved) await env.mkdir(dir, { recursive: true });
|
|
217
|
+
await env.writeFile(resolved, params.content);
|
|
218
|
+
return {
|
|
219
|
+
content: [{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: `Successfully wrote ${params.content.length} bytes to ${params.path}`
|
|
222
|
+
}],
|
|
223
|
+
details: {
|
|
224
|
+
path: params.path,
|
|
225
|
+
size: params.content.length
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function createEditTool(env) {
|
|
232
|
+
return {
|
|
233
|
+
name: "edit",
|
|
234
|
+
label: "Edit File",
|
|
235
|
+
description: "Edit a file using exact text replacement. The oldText must match a unique region of the file. Use replaceAll to replace all occurrences.",
|
|
236
|
+
parameters: Type.Object({
|
|
237
|
+
path: Type.String({ description: "Path to the file to edit" }),
|
|
238
|
+
oldText: Type.String({ description: "Exact text to find (must be unique)" }),
|
|
239
|
+
newText: Type.String({ description: "Replacement text" }),
|
|
240
|
+
replaceAll: Type.Optional(Type.Boolean({ description: "Replace all occurrences" }))
|
|
241
|
+
}),
|
|
242
|
+
async execute(_toolCallId, params, signal) {
|
|
243
|
+
throwIfAborted(signal);
|
|
244
|
+
const content = await env.readFile(params.path);
|
|
245
|
+
if (params.replaceAll) {
|
|
246
|
+
const newContent = content.replaceAll(params.oldText, params.newText);
|
|
247
|
+
if (newContent === content) throw new Error(`Could not find the text in ${params.path}. No changes made.`);
|
|
248
|
+
await env.writeFile(params.path, newContent);
|
|
249
|
+
const count = content.split(params.oldText).length - 1;
|
|
250
|
+
return {
|
|
251
|
+
content: [{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: `Replaced ${count} occurrences in ${params.path}`
|
|
254
|
+
}],
|
|
255
|
+
details: {
|
|
256
|
+
path: params.path,
|
|
257
|
+
replacements: count
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const occurrences = countOccurrences(content, params.oldText);
|
|
262
|
+
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.`);
|
|
263
|
+
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.`);
|
|
264
|
+
const newContent = content.replace(params.oldText, params.newText);
|
|
265
|
+
await env.writeFile(params.path, newContent);
|
|
266
|
+
return {
|
|
267
|
+
content: [{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: `Successfully edited ${params.path}`
|
|
270
|
+
}],
|
|
271
|
+
details: { path: params.path }
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function createBashTool(env) {
|
|
277
|
+
return {
|
|
278
|
+
name: "bash",
|
|
279
|
+
label: "Run Command",
|
|
280
|
+
description: "Execute a bash command. Returns stdout and stderr. Output is truncated to the last 2000 lines or 50KB.",
|
|
281
|
+
parameters: Type.Object({
|
|
282
|
+
command: Type.String({ description: "Bash command to execute" }),
|
|
283
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds" }))
|
|
284
|
+
}),
|
|
285
|
+
async execute(_toolCallId, params, signal) {
|
|
286
|
+
throwIfAborted(signal);
|
|
287
|
+
return formatBashResult(await env.exec(params.command), params.command);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function formatBashResult(result, command) {
|
|
292
|
+
const { text: output } = truncateTail((result.stdout + (result.stderr ? "\n" + result.stderr : "")).trim(), MAX_READ_LINES, MAX_READ_BYTES);
|
|
293
|
+
if (result.exitCode !== 0) throw new Error(`${output}\n\nCommand exited with code ${result.exitCode}`);
|
|
294
|
+
return {
|
|
295
|
+
content: [{
|
|
296
|
+
type: "text",
|
|
297
|
+
text: output || "(no output)"
|
|
298
|
+
}],
|
|
299
|
+
details: {
|
|
300
|
+
command,
|
|
301
|
+
exitCode: result.exitCode
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function createGrepTool(env) {
|
|
306
|
+
return {
|
|
307
|
+
name: "grep",
|
|
308
|
+
label: "Search Files",
|
|
309
|
+
description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers.",
|
|
310
|
+
parameters: Type.Object({
|
|
311
|
+
pattern: Type.String({ description: "Search pattern (regex)" }),
|
|
312
|
+
path: Type.Optional(Type.String({ description: "Directory or file to search (default: .)" })),
|
|
313
|
+
include: Type.Optional(Type.String({ description: "Glob filter, e.g. \"*.ts\"" }))
|
|
314
|
+
}),
|
|
315
|
+
async execute(_toolCallId, params, signal) {
|
|
316
|
+
throwIfAborted(signal);
|
|
317
|
+
const searchPath = params.path || ".";
|
|
318
|
+
let cmd = `grep -rn "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
|
|
319
|
+
if (params.include) cmd = `grep -rn --include="${escapeShellArg(params.include)}" "${escapeShellArg(params.pattern)}" ${escapeShellArg(searchPath)}`;
|
|
320
|
+
const result = await env.exec(cmd);
|
|
321
|
+
if (result.exitCode === 1 && !result.stdout.trim()) return {
|
|
322
|
+
content: [{
|
|
323
|
+
type: "text",
|
|
324
|
+
text: "No matches found."
|
|
325
|
+
}],
|
|
326
|
+
details: { matchCount: 0 }
|
|
327
|
+
};
|
|
328
|
+
if (result.exitCode > 1) throw new Error(`grep failed: ${result.stderr}`);
|
|
329
|
+
const lines = result.stdout.trim().split("\n");
|
|
330
|
+
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");
|
|
331
|
+
if (lines.length > MAX_GREP_MATCHES) finalOutput += `\n\n[Showing ${MAX_GREP_MATCHES} of ${lines.length} matches. Narrow your search.]`;
|
|
332
|
+
return {
|
|
333
|
+
content: [{
|
|
334
|
+
type: "text",
|
|
335
|
+
text: finalOutput
|
|
336
|
+
}],
|
|
337
|
+
details: { matchCount: Math.min(lines.length, MAX_GREP_MATCHES) }
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function createGlobTool(env) {
|
|
343
|
+
return {
|
|
344
|
+
name: "glob",
|
|
345
|
+
label: "Find Files",
|
|
346
|
+
description: "Find files by glob pattern. Returns matching file paths.",
|
|
347
|
+
parameters: Type.Object({
|
|
348
|
+
pattern: Type.String({ description: "Glob pattern, e.g. \"**/*.ts\"" }),
|
|
349
|
+
path: Type.Optional(Type.String({ description: "Directory to search in (default: .)" }))
|
|
350
|
+
}),
|
|
351
|
+
async execute(_toolCallId, params, signal) {
|
|
352
|
+
throwIfAborted(signal);
|
|
353
|
+
const cmd = `find ${escapeShellArg(params.path || ".")} -type f -name "${escapeShellArg(params.pattern)}" 2>/dev/null | head -${MAX_GLOB_RESULTS}`;
|
|
354
|
+
const result = await env.exec(cmd);
|
|
355
|
+
if (result.exitCode !== 0 && !result.stdout.trim()) return {
|
|
356
|
+
content: [{
|
|
357
|
+
type: "text",
|
|
358
|
+
text: "No files found matching pattern."
|
|
359
|
+
}],
|
|
360
|
+
details: { matchCount: 0 }
|
|
361
|
+
};
|
|
362
|
+
const paths = result.stdout.trim().split("\n").filter(Boolean);
|
|
363
|
+
if (paths.length === 0) return {
|
|
364
|
+
content: [{
|
|
365
|
+
type: "text",
|
|
366
|
+
text: "No files found matching pattern."
|
|
367
|
+
}],
|
|
368
|
+
details: { matchCount: 0 }
|
|
369
|
+
};
|
|
370
|
+
return {
|
|
371
|
+
content: [{
|
|
372
|
+
type: "text",
|
|
373
|
+
text: paths.join("\n")
|
|
374
|
+
}],
|
|
375
|
+
details: { matchCount: paths.length }
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function throwIfAborted(signal) {
|
|
381
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
382
|
+
}
|
|
383
|
+
function countOccurrences(str, substr) {
|
|
384
|
+
let count = 0;
|
|
385
|
+
let pos = str.indexOf(substr, 0);
|
|
386
|
+
while (pos !== -1) {
|
|
387
|
+
count++;
|
|
388
|
+
pos = str.indexOf(substr, pos + substr.length);
|
|
389
|
+
}
|
|
390
|
+
return count;
|
|
391
|
+
}
|
|
392
|
+
function escapeShellArg(arg) {
|
|
393
|
+
return arg.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/`/g, "\\`");
|
|
394
|
+
}
|
|
395
|
+
function truncateHead(lines, maxLines, maxBytes) {
|
|
396
|
+
let result = "";
|
|
397
|
+
let lineCount = 0;
|
|
398
|
+
let wasTruncated = false;
|
|
399
|
+
for (const line of lines) {
|
|
400
|
+
if (lineCount >= maxLines) {
|
|
401
|
+
wasTruncated = true;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
const next = lineCount === 0 ? line : "\n" + line;
|
|
405
|
+
if (result.length + next.length > maxBytes) {
|
|
406
|
+
wasTruncated = true;
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
result += next;
|
|
410
|
+
lineCount++;
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
text: result,
|
|
414
|
+
wasTruncated
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function truncateTail(text, maxLines, maxBytes) {
|
|
418
|
+
const lines = text.split("\n");
|
|
419
|
+
if (lines.length <= maxLines && text.length <= maxBytes) return {
|
|
420
|
+
text,
|
|
421
|
+
wasTruncated: false
|
|
422
|
+
};
|
|
423
|
+
let result = lines.slice(-maxLines).join("\n");
|
|
424
|
+
if (result.length > maxBytes) result = result.slice(-maxBytes);
|
|
425
|
+
return {
|
|
426
|
+
text: result,
|
|
427
|
+
wasTruncated: true
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
//#endregion
|
|
432
|
+
export { parseFrontmatterFile as a, loadSkillByPath as i, createTools as n, discoverSessionContext as r, BUILTIN_TOOL_NAMES as t };
|
package/dist/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, b as SessionEnv, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, l as CommandSupport, m as FlueSession, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-
|
|
1
|
+
import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, b as SessionEnv, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, l as CommandSupport, m as FlueSession, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-C8tsaK1j.mjs";
|
|
2
2
|
import { Type } from "@mariozechner/pi-ai";
|
|
3
3
|
|
|
4
4
|
//#region src/client.d.ts
|
package/dist/client.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as discoverSessionContext } from "./agent-BYG0nVbQ.mjs";
|
|
2
|
+
import { n as Session } from "./session-BRLCNVG1.mjs";
|
|
2
3
|
import { bashToSessionEnv } from "./sandbox.mjs";
|
|
3
4
|
import { Type } from "@mariozechner/pi-ai";
|
|
4
5
|
|
|
@@ -24,10 +25,12 @@ function createFlueContext(config) {
|
|
|
24
25
|
const store = options?.persist ?? config.defaultStore;
|
|
25
26
|
const savedData = await store.load(config.sessionId);
|
|
26
27
|
const localContext = await discoverSessionContext(env);
|
|
28
|
+
const sessionModel = options?.model && config.agentConfig.resolveModel ? config.agentConfig.resolveModel(options.model) : config.agentConfig.model;
|
|
27
29
|
const sessionConfig = {
|
|
28
30
|
...config.agentConfig,
|
|
29
31
|
systemPrompt: localContext.systemPrompt,
|
|
30
|
-
skills: localContext.skills
|
|
32
|
+
skills: localContext.skills,
|
|
33
|
+
model: sessionModel
|
|
31
34
|
};
|
|
32
35
|
return new Session(config.sessionId, sessionConfig, env, store, savedData, currentEventCallback);
|
|
33
36
|
},
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, T as Skill, _ as Role, a as BuildOptions, b as SessionEnv, c as CommandDef, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, i as BuildContext, l as CommandSupport, m as FlueSession, n as AgentInfo, o as BuildPlugin, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-
|
|
2
|
-
import { FlueContextConfig, FlueContextInternal, createFlueContext } from "./client.mjs";
|
|
1
|
+
import { C as ShellOptions, D as TaskOptions, E as SkillOptions, O as ToolDef, S as SessionStore, T as Skill, _ as Role, a as BuildOptions, b as SessionEnv, c as CommandDef, d as FlueContext, f as FlueEvent, g as PromptResponse, h as PromptOptions, i as BuildContext, l as CommandSupport, m as FlueSession, n as AgentInfo, o as BuildPlugin, p as FlueEventCallback, r as BashLike, s as Command, t as AgentConfig, u as FileStat, v as SandboxFactory, w as ShellResult, x as SessionInit, y as SessionData } from "./types-C8tsaK1j.mjs";
|
|
3
2
|
import { AgentTool } from "@mariozechner/pi-agent-core";
|
|
4
|
-
import "valibot";
|
|
5
3
|
|
|
6
4
|
//#region src/build.d.ts
|
|
7
5
|
/**
|
|
@@ -10,17 +8,8 @@ import "valibot";
|
|
|
10
8
|
*/
|
|
11
9
|
declare function build(options: BuildOptions): Promise<void>;
|
|
12
10
|
//#endregion
|
|
13
|
-
//#region src/session.d.ts
|
|
14
|
-
/** In-memory session store. Sessions persist for the lifetime of the process. */
|
|
15
|
-
declare class InMemorySessionStore implements SessionStore {
|
|
16
|
-
private store;
|
|
17
|
-
save(id: string, data: SessionData): Promise<void>;
|
|
18
|
-
load(id: string): Promise<SessionData | null>;
|
|
19
|
-
delete(id: string): Promise<void>;
|
|
20
|
-
}
|
|
21
|
-
//#endregion
|
|
22
11
|
//#region src/agent.d.ts
|
|
23
12
|
declare const BUILTIN_TOOL_NAMES: Set<string>;
|
|
24
13
|
declare function createTools(env: SessionEnv): AgentTool<any>[];
|
|
25
14
|
//#endregion
|
|
26
|
-
export { type AgentConfig, type AgentInfo, BUILTIN_TOOL_NAMES, type BashLike, type BuildContext, type BuildOptions, type BuildPlugin, type Command, type CommandDef, type CommandSupport, type FileStat, type FlueContext, type
|
|
15
|
+
export { type AgentConfig, type AgentInfo, BUILTIN_TOOL_NAMES, type BashLike, type BuildContext, type BuildOptions, type BuildPlugin, type Command, type CommandDef, type CommandSupport, type FileStat, type FlueContext, type FlueEvent, type FlueEventCallback, type FlueSession, type PromptOptions, type PromptResponse, type Role, type SandboxFactory, type SessionData, type SessionEnv, type SessionInit, type SessionStore, type ShellOptions, type ShellResult, type Skill, type SkillOptions, type TaskOptions, type ToolDef, build, createTools };
|