@flue/sdk 0.1.3 → 0.3.0
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 +135 -15
- package/package.json +3 -2
- package/dist/agent-BYG0nVbQ.mjs +0 -432
- package/dist/client.d.mts +0 -25
- package/dist/client.mjs +0 -59
- package/dist/cloudflare/index.d.mts +0 -40
- package/dist/cloudflare/index.mjs +0 -230
- package/dist/command-helpers-BPcSV93o.d.mts +0 -21
- package/dist/command-helpers-CxRhK1my.mjs +0 -37
- package/dist/index.d.mts +0 -15
- package/dist/index.mjs +0 -791
- package/dist/internal.d.mts +0 -15
- package/dist/internal.mjs +0 -6
- package/dist/node/index.d.mts +0 -14
- package/dist/node/index.mjs +0 -75
- package/dist/sandbox.d.mts +0 -28
- package/dist/sandbox.mjs +0 -102
- package/dist/session-CiAMTsLZ.mjs +0 -943
- package/dist/types-BZPltYah.d.mts +0 -343
package/README.md
CHANGED
|
@@ -35,11 +35,10 @@ import * as v from 'valibot';
|
|
|
35
35
|
export const triggers = { webhook: true };
|
|
36
36
|
|
|
37
37
|
// The agent handler. Where the orchestration of the agent lives.
|
|
38
|
-
export default async function ({ init, payload
|
|
39
|
-
// `
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const session = await init();
|
|
38
|
+
export default async function ({ init, payload }: FlueContext) {
|
|
39
|
+
// `agent` -- Your initialized agent runtime including sandbox, tools, skills, etc.
|
|
40
|
+
const agent = await init({ model: 'anthropic/claude-sonnet-4-6' });
|
|
41
|
+
const session = await agent.session();
|
|
43
42
|
|
|
44
43
|
// prompt() sends a message in the session, triggering action.
|
|
45
44
|
const result = await session.prompt(`Translate this to ${payload.language}: "${payload.text}"`, {
|
|
@@ -73,7 +72,8 @@ export default async function ({ init, payload, env }: FlueContext) {
|
|
|
73
72
|
// The agent can grep, glob, and read articles with bash, but
|
|
74
73
|
// without needing to spin up an entire container sandbox.
|
|
75
74
|
const sandbox = await getVirtualSandbox(env.KNOWLEDGE_BASE);
|
|
76
|
-
const
|
|
75
|
+
const agent = await init({ sandbox, model: 'openrouter/moonshotai/kimi-k2.6' });
|
|
76
|
+
const session = await agent.session();
|
|
77
77
|
|
|
78
78
|
return await session.prompt(
|
|
79
79
|
`You are a support agent. Search the knowledge base for articles
|
|
@@ -114,11 +114,12 @@ export default async function ({ init, payload }: FlueContext) {
|
|
|
114
114
|
// discovered automatically from the workspace directory.
|
|
115
115
|
//
|
|
116
116
|
// `model` sets the default model for every prompt/skill call in this
|
|
117
|
-
//
|
|
118
|
-
const
|
|
117
|
+
// agent. Override per-call with `{ model: '...' }` on prompt()/skill().
|
|
118
|
+
const agent = await init({
|
|
119
119
|
sandbox: 'local',
|
|
120
|
-
model: 'anthropic/claude-opus-4-
|
|
120
|
+
model: 'anthropic/claude-opus-4-7',
|
|
121
121
|
});
|
|
122
|
+
const session = await agent.session();
|
|
122
123
|
|
|
123
124
|
// Skills can be referenced either by their frontmatter `name:` (shown below)
|
|
124
125
|
// or by a relative path under `.agents/skills/` — e.g.
|
|
@@ -158,23 +159,35 @@ import { daytona } from '@flue/connectors/daytona';
|
|
|
158
159
|
export const triggers = { webhook: true };
|
|
159
160
|
|
|
160
161
|
export default async function ({ init, payload, env }: FlueContext) {
|
|
161
|
-
// Each
|
|
162
|
+
// Each agent gets a real container via Daytona. The container has
|
|
162
163
|
// a full Linux environment with persistent filesystem and shell.
|
|
163
164
|
//
|
|
164
165
|
// For simplicity, we always create a new sandbox here. You could also
|
|
165
|
-
// first check for an existing sandbox for the
|
|
166
|
+
// first check for an existing sandbox for the agent id, and reuse that
|
|
166
167
|
// instead to best pick up where you last left off in the conversation.
|
|
167
168
|
const client = new Daytona({ apiKey: env.DAYTONA_API_KEY });
|
|
168
169
|
const sandbox = await client.create();
|
|
169
|
-
const
|
|
170
|
+
const setupAgent = await init({
|
|
170
171
|
sandbox: daytona(sandbox, { cleanup: true }),
|
|
172
|
+
model: 'openai/gpt-5.5',
|
|
171
173
|
});
|
|
174
|
+
const setup = await setupAgent.session();
|
|
172
175
|
|
|
173
176
|
// For simplicity, we clone the target repo into the sandbox here.
|
|
174
177
|
// You could also bake these into the container image snapshot for a
|
|
175
178
|
// faster / near-instant startup.
|
|
176
|
-
await
|
|
177
|
-
await
|
|
179
|
+
await setup.shell(`git clone ${payload.repo} /workspace/project`);
|
|
180
|
+
await setup.shell('npm install', { cwd: '/workspace/project' });
|
|
181
|
+
|
|
182
|
+
// Start a second agent in the cloned repo. It shares the same sandbox, but
|
|
183
|
+
// discovers AGENTS.md and skills from /workspace/project.
|
|
184
|
+
const projectAgent = await init({
|
|
185
|
+
id: 'project',
|
|
186
|
+
sandbox: daytona(sandbox),
|
|
187
|
+
cwd: '/workspace/project',
|
|
188
|
+
model: 'openai/gpt-5.5',
|
|
189
|
+
});
|
|
190
|
+
const session = await projectAgent.session();
|
|
178
191
|
|
|
179
192
|
// Coding agents don't hide the agent DX from the user, so no need to
|
|
180
193
|
// wrap the user's prompt in anything. Just send it to the agent directly
|
|
@@ -183,6 +196,113 @@ export default async function ({ init, payload, env }: FlueContext) {
|
|
|
183
196
|
}
|
|
184
197
|
```
|
|
185
198
|
|
|
199
|
+
### Remote MCP Tools
|
|
200
|
+
|
|
201
|
+
MCP is available as a runtime tool adapter. Connect to a remote MCP server in trusted code, pass its tools to `init()`, and keep secrets in `env` instead of filesystem context or prompts.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// .flue/agents/assistant.ts
|
|
205
|
+
import { connectMcpServer, type FlueContext } from '@flue/sdk/client';
|
|
206
|
+
|
|
207
|
+
export const triggers = { webhook: true };
|
|
208
|
+
|
|
209
|
+
export default async function ({ init, payload, env }: FlueContext) {
|
|
210
|
+
const github = await connectMcpServer('github', {
|
|
211
|
+
url: 'https://mcp.github.com/mcp',
|
|
212
|
+
headers: {
|
|
213
|
+
Authorization: `Bearer ${env.GITHUB_TOKEN}`,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const agent = await init({
|
|
219
|
+
model: 'anthropic/claude-sonnet-4-6',
|
|
220
|
+
tools: github.tools,
|
|
221
|
+
});
|
|
222
|
+
const session = await agent.session();
|
|
223
|
+
return await session.prompt(payload.prompt);
|
|
224
|
+
} finally {
|
|
225
|
+
await github.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`connectMcpServer()` defaults to modern streamable HTTP. For legacy SSE servers, pass `transport: 'sse'`. Flue does not auto-detect transports, spawn local stdio MCP servers, or handle OAuth callbacks in this first version.
|
|
231
|
+
|
|
232
|
+
## Agents And Sessions
|
|
233
|
+
|
|
234
|
+
Every agent invocation runs inside an initialized agent runtime. For HTTP agents, the agent ID is the last path segment:
|
|
235
|
+
|
|
236
|
+
```txt
|
|
237
|
+
POST /agents/<agent-name>/<id>
|
|
238
|
+
```
|
|
239
|
+
|
|
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
|
+
|
|
242
|
+
```bash
|
|
243
|
+
# Start a conversation
|
|
244
|
+
curl http://localhost:8787/agents/hello/session-abc \
|
|
245
|
+
-H "Content-Type: application/json" \
|
|
246
|
+
-d '{"name": "Alice"}'
|
|
247
|
+
|
|
248
|
+
# Continue that conversation
|
|
249
|
+
curl http://localhost:8787/agents/hello/session-abc \
|
|
250
|
+
-H "Content-Type: application/json" \
|
|
251
|
+
-d '{"name": "Alice"}'
|
|
252
|
+
|
|
253
|
+
# Start a separate conversation
|
|
254
|
+
curl http://localhost:8787/agents/hello/session-xyz \
|
|
255
|
+
-H "Content-Type: application/json" \
|
|
256
|
+
-d '{"name": "Alice"}'
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Agents own sandbox state such as files written during a run. Sessions persist message history and conversation metadata inside an agent. On Cloudflare, session data is backed by Durable Objects and survives across requests. On Node.js, sessions are stored in memory by default unless you provide a custom store.
|
|
260
|
+
|
|
261
|
+
In production, generate a stable agent ID for the sandbox/runtime scope you want to preserve. Use `agent.session(threadId)` when you need multiple conversations inside the same agent.
|
|
262
|
+
|
|
263
|
+
### Tasks
|
|
264
|
+
|
|
265
|
+
Use `session.task()` to run a focused, one-shot child agent in a detached session. Tasks share the same sandbox/filesystem, but get their own message history and discover `AGENTS.md` plus `.agents/skills/` from their working directory. The same `task` tool is also available to the LLM during `prompt()` and `skill()` calls, so the agent can delegate parallel research or exploration work itself.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
const session = await agent.session();
|
|
269
|
+
|
|
270
|
+
const research = await session.task('Research the auth flow and summarize the key files.', {
|
|
271
|
+
cwd: '/workspace/project',
|
|
272
|
+
role: 'researcher',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const answer = await session.prompt(
|
|
276
|
+
`Use this research to draft the implementation plan:\n\n${research.text}`,
|
|
277
|
+
);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Roles can be set at the agent, session, or call level. Precedence is `call role > session role > agent role`. Role instructions are applied as call-scoped system prompt overlays, not injected into the persisted user message history.
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
const agent = await init({ model: 'anthropic/claude-sonnet-4-6', role: 'coder' });
|
|
284
|
+
const session = await agent.session('review-thread', { role: 'reviewer' });
|
|
285
|
+
|
|
286
|
+
await session.prompt('Review the latest changes.'); // uses reviewer
|
|
287
|
+
await session.task('Research related issues.', { role: 'researcher' }); // uses researcher
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Custom Virtual Sandboxes
|
|
291
|
+
|
|
292
|
+
For most agents, use the built-in virtual sandbox or `sandbox: 'local'`. If you need to customize just-bash directly, pass a Bash factory. The factory must return a fresh Bash-like runtime each time; share the filesystem object in the closure to persist files across sessions and prompts.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { Bash, InMemoryFs } from 'just-bash';
|
|
296
|
+
|
|
297
|
+
const fs = new InMemoryFs();
|
|
298
|
+
|
|
299
|
+
const agent = await init({
|
|
300
|
+
sandbox: () => new Bash({ fs, cwd: '/workspace', python: true }),
|
|
301
|
+
model: 'anthropic/claude-sonnet-4-6',
|
|
302
|
+
});
|
|
303
|
+
const session = await agent.session();
|
|
304
|
+
```
|
|
305
|
+
|
|
186
306
|
## Running Agents
|
|
187
307
|
|
|
188
308
|
### Trigger From the CLI
|
|
@@ -190,7 +310,7 @@ export default async function ({ init, payload, env }: FlueContext) {
|
|
|
190
310
|
Build and run any agent locally, perfect for fast local testing or running in CI.
|
|
191
311
|
|
|
192
312
|
```bash
|
|
193
|
-
flue run hello --target node --
|
|
313
|
+
flue run hello --target node --id test-1 \
|
|
194
314
|
--payload '{"text": "Hello world", "language": "French"}'
|
|
195
315
|
```
|
|
196
316
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flue/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"exports": {
|
|
@@ -39,10 +39,11 @@
|
|
|
39
39
|
"@hono/node-server": "^1.14.0",
|
|
40
40
|
"@mariozechner/pi-agent-core": "*",
|
|
41
41
|
"@mariozechner/pi-ai": "*",
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
43
|
"@valibot/to-json-schema": "^1.0.0",
|
|
43
|
-
"agentfs-sdk": "^0.6.4",
|
|
44
44
|
"esbuild": "^0.25.0",
|
|
45
45
|
"hono": "^4.7.0",
|
|
46
|
+
"jsonc-parser": "^3.3.1",
|
|
46
47
|
"just-bash": "^2.14.2",
|
|
47
48
|
"package-up": "^5.0.0",
|
|
48
49
|
"valibot": "^1.0.0"
|
package/dist/agent-BYG0nVbQ.mjs
DELETED
|
@@ -1,432 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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-BZPltYah.mjs";
|
|
2
|
-
import { Type } from "@mariozechner/pi-ai";
|
|
3
|
-
|
|
4
|
-
//#region src/client.d.ts
|
|
5
|
-
interface FlueContextConfig {
|
|
6
|
-
sessionId: string;
|
|
7
|
-
payload: any;
|
|
8
|
-
env: Record<string, any>;
|
|
9
|
-
agentConfig: AgentConfig;
|
|
10
|
-
createDefaultEnv: () => Promise<SessionEnv>;
|
|
11
|
-
createLocalEnv: () => Promise<SessionEnv>;
|
|
12
|
-
defaultStore: SessionStore;
|
|
13
|
-
/**
|
|
14
|
-
* Platform-specific sandbox resolver hook. Called before default resolution.
|
|
15
|
-
* Returns SessionEnv to use, or null to fall through to default logic.
|
|
16
|
-
*/
|
|
17
|
-
resolveSandbox?: (sandbox: unknown) => Promise<SessionEnv> | null;
|
|
18
|
-
}
|
|
19
|
-
/** Extends FlueContext with server-only methods. Agent handlers only see FlueContext. */
|
|
20
|
-
interface FlueContextInternal extends FlueContext {
|
|
21
|
-
setEventCallback(callback: FlueEventCallback | undefined): void;
|
|
22
|
-
}
|
|
23
|
-
declare function createFlueContext(config: FlueContextConfig): FlueContextInternal;
|
|
24
|
-
//#endregion
|
|
25
|
-
export { type BashLike, type Command, type CommandSupport, type FileStat, type FlueContext, FlueContextConfig, FlueContextInternal, type FlueEvent, type FlueEventCallback, type FlueSession, type PromptOptions, type PromptResponse, type SandboxFactory, type SessionData, type SessionEnv, type SessionInit, type SessionStore, type ShellOptions, type ShellResult, type SkillOptions, type TaskOptions, type ToolDef, Type, createFlueContext };
|