@blockrun/franklin 3.0.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/LICENSE +190 -0
- package/README.md +256 -0
- package/dist/agent/commands.d.ts +27 -0
- package/dist/agent/commands.js +659 -0
- package/dist/agent/compact.d.ts +31 -0
- package/dist/agent/compact.js +366 -0
- package/dist/agent/context.d.ts +11 -0
- package/dist/agent/context.js +184 -0
- package/dist/agent/error-classifier.d.ts +10 -0
- package/dist/agent/error-classifier.js +61 -0
- package/dist/agent/llm.d.ts +63 -0
- package/dist/agent/llm.js +448 -0
- package/dist/agent/loop.d.ts +12 -0
- package/dist/agent/loop.js +346 -0
- package/dist/agent/optimize.d.ts +53 -0
- package/dist/agent/optimize.js +262 -0
- package/dist/agent/permissions.d.ts +39 -0
- package/dist/agent/permissions.js +226 -0
- package/dist/agent/reduce.d.ts +49 -0
- package/dist/agent/reduce.js +317 -0
- package/dist/agent/streaming-executor.d.ts +36 -0
- package/dist/agent/streaming-executor.js +149 -0
- package/dist/agent/tokens.d.ts +53 -0
- package/dist/agent/tokens.js +185 -0
- package/dist/agent/types.d.ts +125 -0
- package/dist/agent/types.js +5 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +27 -0
- package/dist/commands/balance.d.ts +1 -0
- package/dist/commands/balance.js +40 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.js +107 -0
- package/dist/commands/daemon.d.ts +3 -0
- package/dist/commands/daemon.js +117 -0
- package/dist/commands/history.d.ts +5 -0
- package/dist/commands/history.js +31 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +92 -0
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.js +89 -0
- package/dist/commands/models.d.ts +1 -0
- package/dist/commands/models.js +56 -0
- package/dist/commands/plugin.d.ts +14 -0
- package/dist/commands/plugin.js +176 -0
- package/dist/commands/proxy.d.ts +13 -0
- package/dist/commands/proxy.js +106 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +292 -0
- package/dist/commands/stats.d.ts +10 -0
- package/dist/commands/stats.js +94 -0
- package/dist/commands/uninit.d.ts +1 -0
- package/dist/commands/uninit.js +63 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +179 -0
- package/dist/mcp/client.d.ts +44 -0
- package/dist/mcp/client.js +147 -0
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.js +138 -0
- package/dist/plugin-sdk/channel.d.ts +100 -0
- package/dist/plugin-sdk/channel.js +10 -0
- package/dist/plugin-sdk/index.d.ts +14 -0
- package/dist/plugin-sdk/index.js +9 -0
- package/dist/plugin-sdk/plugin.d.ts +87 -0
- package/dist/plugin-sdk/plugin.js +7 -0
- package/dist/plugin-sdk/search.d.ts +13 -0
- package/dist/plugin-sdk/search.js +4 -0
- package/dist/plugin-sdk/tracker.d.ts +27 -0
- package/dist/plugin-sdk/tracker.js +5 -0
- package/dist/plugin-sdk/workflow.d.ts +126 -0
- package/dist/plugin-sdk/workflow.js +11 -0
- package/dist/plugins/registry.d.ts +33 -0
- package/dist/plugins/registry.js +155 -0
- package/dist/plugins/runner.d.ts +21 -0
- package/dist/plugins/runner.js +453 -0
- package/dist/plugins-bundled/social/index.d.ts +10 -0
- package/dist/plugins-bundled/social/index.js +363 -0
- package/dist/plugins-bundled/social/plugin.json +14 -0
- package/dist/plugins-bundled/social/prompts.d.ts +19 -0
- package/dist/plugins-bundled/social/prompts.js +67 -0
- package/dist/plugins-bundled/social/types.d.ts +58 -0
- package/dist/plugins-bundled/social/types.js +16 -0
- package/dist/pricing.d.ts +21 -0
- package/dist/pricing.js +91 -0
- package/dist/proxy/fallback.d.ts +38 -0
- package/dist/proxy/fallback.js +144 -0
- package/dist/proxy/server.d.ts +18 -0
- package/dist/proxy/server.js +576 -0
- package/dist/proxy/sse-translator.d.ts +29 -0
- package/dist/proxy/sse-translator.js +270 -0
- package/dist/router/index.d.ts +22 -0
- package/dist/router/index.js +269 -0
- package/dist/session/search.d.ts +33 -0
- package/dist/session/search.js +229 -0
- package/dist/session/storage.d.ts +48 -0
- package/dist/session/storage.js +173 -0
- package/dist/stats/insights.d.ts +55 -0
- package/dist/stats/insights.js +195 -0
- package/dist/stats/tracker.d.ts +54 -0
- package/dist/stats/tracker.js +165 -0
- package/dist/tools/askuser.d.ts +6 -0
- package/dist/tools/askuser.js +76 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +336 -0
- package/dist/tools/edit.d.ts +5 -0
- package/dist/tools/edit.js +148 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/glob.js +158 -0
- package/dist/tools/grep.d.ts +5 -0
- package/dist/tools/grep.js +194 -0
- package/dist/tools/imagegen.d.ts +6 -0
- package/dist/tools/imagegen.js +172 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.js +30 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.js +90 -0
- package/dist/tools/subagent.d.ts +5 -0
- package/dist/tools/subagent.js +116 -0
- package/dist/tools/task.d.ts +5 -0
- package/dist/tools/task.js +91 -0
- package/dist/tools/webfetch.d.ts +5 -0
- package/dist/tools/webfetch.js +166 -0
- package/dist/tools/websearch.d.ts +5 -0
- package/dist/tools/websearch.js +103 -0
- package/dist/tools/write.d.ts +5 -0
- package/dist/tools/write.js +114 -0
- package/dist/ui/app.d.ts +26 -0
- package/dist/ui/app.js +545 -0
- package/dist/ui/model-picker.d.ts +14 -0
- package/dist/ui/model-picker.js +161 -0
- package/dist/ui/terminal.d.ts +35 -0
- package/dist/ui/terminal.js +337 -0
- package/dist/wallet/manager.d.ts +10 -0
- package/dist/wallet/manager.js +23 -0
- package/package.json +79 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Generation capability — generate images via BlockRun API.
|
|
3
|
+
* Uses x402 payment on Solana or Base.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
8
|
+
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
9
|
+
async function execute(input, ctx) {
|
|
10
|
+
const { prompt, output_path, size, model } = input;
|
|
11
|
+
if (!prompt) {
|
|
12
|
+
return { output: 'Error: prompt is required', isError: true };
|
|
13
|
+
}
|
|
14
|
+
const chain = loadChain();
|
|
15
|
+
const apiUrl = API_URLS[chain];
|
|
16
|
+
const endpoint = `${apiUrl}/v1/images/generations`;
|
|
17
|
+
const imageModel = model || 'dall-e-3';
|
|
18
|
+
const imageSize = size || '1024x1024';
|
|
19
|
+
// Default output path
|
|
20
|
+
const outPath = output_path
|
|
21
|
+
? (path.isAbsolute(output_path) ? output_path : path.resolve(ctx.workingDir, output_path))
|
|
22
|
+
: path.resolve(ctx.workingDir, `generated-${Date.now()}.png`);
|
|
23
|
+
const body = JSON.stringify({
|
|
24
|
+
model: imageModel,
|
|
25
|
+
prompt,
|
|
26
|
+
n: 1,
|
|
27
|
+
size: imageSize,
|
|
28
|
+
response_format: 'b64_json',
|
|
29
|
+
});
|
|
30
|
+
const headers = {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'User-Agent': `runcode/${VERSION}`,
|
|
33
|
+
};
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timeout = setTimeout(() => controller.abort(), 60_000); // 60s timeout
|
|
36
|
+
try {
|
|
37
|
+
// First request — will get 402
|
|
38
|
+
let response = await fetch(endpoint, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
headers,
|
|
42
|
+
body,
|
|
43
|
+
});
|
|
44
|
+
// Handle x402 payment
|
|
45
|
+
if (response.status === 402) {
|
|
46
|
+
const paymentHeaders = await signPayment(response, chain, endpoint);
|
|
47
|
+
if (!paymentHeaders) {
|
|
48
|
+
return { output: 'Payment failed. Check wallet balance with: runcode balance', isError: true };
|
|
49
|
+
}
|
|
50
|
+
response = await fetch(endpoint, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
signal: controller.signal,
|
|
53
|
+
headers: { ...headers, ...paymentHeaders },
|
|
54
|
+
body,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const errText = await response.text().catch(() => '');
|
|
59
|
+
return { output: `Image generation failed (${response.status}): ${errText.slice(0, 200)}`, isError: true };
|
|
60
|
+
}
|
|
61
|
+
const result = await response.json();
|
|
62
|
+
const imageData = result.data?.[0];
|
|
63
|
+
if (!imageData) {
|
|
64
|
+
return { output: 'No image data returned from API', isError: true };
|
|
65
|
+
}
|
|
66
|
+
// Save image
|
|
67
|
+
if (imageData.b64_json) {
|
|
68
|
+
const buffer = Buffer.from(imageData.b64_json, 'base64');
|
|
69
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
70
|
+
fs.writeFileSync(outPath, buffer);
|
|
71
|
+
}
|
|
72
|
+
else if (imageData.url) {
|
|
73
|
+
// Download from URL (with 30s timeout)
|
|
74
|
+
const dlCtrl = new AbortController();
|
|
75
|
+
const dlTimeout = setTimeout(() => dlCtrl.abort(), 30_000);
|
|
76
|
+
const imgResp = await fetch(imageData.url, { signal: dlCtrl.signal });
|
|
77
|
+
clearTimeout(dlTimeout);
|
|
78
|
+
const buffer = Buffer.from(await imgResp.arrayBuffer());
|
|
79
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
80
|
+
fs.writeFileSync(outPath, buffer);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
return { output: 'No image data (b64_json or url) in response', isError: true };
|
|
84
|
+
}
|
|
85
|
+
const fileSize = fs.statSync(outPath).size;
|
|
86
|
+
const sizeKB = (fileSize / 1024).toFixed(1);
|
|
87
|
+
const revisedPrompt = imageData.revised_prompt ? `\nRevised prompt: ${imageData.revised_prompt}` : '';
|
|
88
|
+
return {
|
|
89
|
+
output: `Image saved to ${outPath} (${sizeKB}KB, ${imageSize})${revisedPrompt}\n\nOpen with: open ${outPath}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const msg = err.message || '';
|
|
94
|
+
if (msg.includes('abort')) {
|
|
95
|
+
return { output: 'Image generation timed out (60s limit). Try a simpler prompt.', isError: true };
|
|
96
|
+
}
|
|
97
|
+
return { output: `Error: ${msg}`, isError: true };
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ─── Payment ───────────────────────────────────────────────────────────────
|
|
104
|
+
async function signPayment(response, chain, endpoint) {
|
|
105
|
+
try {
|
|
106
|
+
const paymentHeader = await extractPaymentReq(response);
|
|
107
|
+
if (!paymentHeader)
|
|
108
|
+
return null;
|
|
109
|
+
if (chain === 'solana') {
|
|
110
|
+
const wallet = await getOrCreateSolanaWallet();
|
|
111
|
+
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
112
|
+
const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
|
|
113
|
+
const secretBytes = await solanaKeyToBytes(wallet.privateKey);
|
|
114
|
+
const feePayer = details.extra?.feePayer || details.recipient;
|
|
115
|
+
const payload = await createSolanaPaymentPayload(secretBytes, wallet.address, details.recipient, details.amount, feePayer, {
|
|
116
|
+
resourceUrl: details.resource?.url || endpoint,
|
|
117
|
+
resourceDescription: details.resource?.description || 'RunCode image generation',
|
|
118
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
|
|
119
|
+
extra: details.extra,
|
|
120
|
+
});
|
|
121
|
+
return { 'PAYMENT-SIGNATURE': payload };
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const wallet = getOrCreateWallet();
|
|
125
|
+
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
126
|
+
const details = extractPaymentDetails(paymentRequired);
|
|
127
|
+
const payload = await createPaymentPayload(wallet.privateKey, wallet.address, details.recipient, details.amount, details.network || 'eip155:8453', {
|
|
128
|
+
resourceUrl: details.resource?.url || endpoint,
|
|
129
|
+
resourceDescription: details.resource?.description || 'RunCode image generation',
|
|
130
|
+
maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
|
|
131
|
+
extra: details.extra,
|
|
132
|
+
});
|
|
133
|
+
return { 'PAYMENT-SIGNATURE': payload };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.error(`[runcode] Image payment error: ${err.message}`);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function extractPaymentReq(response) {
|
|
142
|
+
let header = response.headers.get('payment-required');
|
|
143
|
+
if (!header) {
|
|
144
|
+
try {
|
|
145
|
+
const body = (await response.json());
|
|
146
|
+
if (body.x402 || body.accepts) {
|
|
147
|
+
header = btoa(JSON.stringify(body));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch { /* ignore */ }
|
|
151
|
+
}
|
|
152
|
+
return header;
|
|
153
|
+
}
|
|
154
|
+
// ─── Export ────────────────────────────────────────────────────────────────
|
|
155
|
+
export const imageGenCapability = {
|
|
156
|
+
spec: {
|
|
157
|
+
name: 'ImageGen',
|
|
158
|
+
description: 'Generate an image from a text prompt using AI (DALL-E, etc). Saves the image to a file.',
|
|
159
|
+
input_schema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {
|
|
162
|
+
prompt: { type: 'string', description: 'Text description of the image to generate' },
|
|
163
|
+
output_path: { type: 'string', description: 'Where to save the image. Default: generated-<timestamp>.png in working directory' },
|
|
164
|
+
size: { type: 'string', description: 'Image size: 1024x1024, 1792x1024, or 1024x1792. Default: 1024x1024' },
|
|
165
|
+
model: { type: 'string', description: 'Image model to use. Default: dall-e-3' },
|
|
166
|
+
},
|
|
167
|
+
required: ['prompt'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
execute,
|
|
171
|
+
concurrent: false,
|
|
172
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry — exports all available capabilities for the agent.
|
|
3
|
+
*/
|
|
4
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
5
|
+
import { readCapability } from './read.js';
|
|
6
|
+
import { writeCapability } from './write.js';
|
|
7
|
+
import { editCapability } from './edit.js';
|
|
8
|
+
import { bashCapability } from './bash.js';
|
|
9
|
+
import { globCapability } from './glob.js';
|
|
10
|
+
import { grepCapability } from './grep.js';
|
|
11
|
+
import { webFetchCapability } from './webfetch.js';
|
|
12
|
+
import { webSearchCapability } from './websearch.js';
|
|
13
|
+
import { taskCapability } from './task.js';
|
|
14
|
+
/** All capabilities available to the runcode agent (excluding sub-agent, which needs config). */
|
|
15
|
+
export declare const allCapabilities: CapabilityHandler[];
|
|
16
|
+
export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
|
|
17
|
+
export { createSubAgentCapability } from './subagent.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry — exports all available capabilities for the agent.
|
|
3
|
+
*/
|
|
4
|
+
import { readCapability } from './read.js';
|
|
5
|
+
import { writeCapability } from './write.js';
|
|
6
|
+
import { editCapability } from './edit.js';
|
|
7
|
+
import { bashCapability } from './bash.js';
|
|
8
|
+
import { globCapability } from './glob.js';
|
|
9
|
+
import { grepCapability } from './grep.js';
|
|
10
|
+
import { webFetchCapability } from './webfetch.js';
|
|
11
|
+
import { webSearchCapability } from './websearch.js';
|
|
12
|
+
import { taskCapability } from './task.js';
|
|
13
|
+
import { imageGenCapability } from './imagegen.js';
|
|
14
|
+
import { askUserCapability } from './askuser.js';
|
|
15
|
+
/** All capabilities available to the runcode agent (excluding sub-agent, which needs config). */
|
|
16
|
+
export const allCapabilities = [
|
|
17
|
+
readCapability,
|
|
18
|
+
writeCapability,
|
|
19
|
+
editCapability,
|
|
20
|
+
bashCapability,
|
|
21
|
+
globCapability,
|
|
22
|
+
grepCapability,
|
|
23
|
+
webFetchCapability,
|
|
24
|
+
webSearchCapability,
|
|
25
|
+
taskCapability,
|
|
26
|
+
imageGenCapability,
|
|
27
|
+
askUserCapability,
|
|
28
|
+
];
|
|
29
|
+
export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
|
|
30
|
+
export { createSubAgentCapability } from './subagent.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read capability — reads files with line numbers.
|
|
3
|
+
*/
|
|
4
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Tracks files that were only partially read (offset or limit applied).
|
|
7
|
+
* Edit tool uses this to warn when editing without full context.
|
|
8
|
+
* Exported so edit.ts can check and clear entries.
|
|
9
|
+
*/
|
|
10
|
+
export declare const partiallyReadFiles: Set<string>;
|
|
11
|
+
export declare const readCapability: CapabilityHandler;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read capability — reads files with line numbers.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* Tracks files that were only partially read (offset or limit applied).
|
|
8
|
+
* Edit tool uses this to warn when editing without full context.
|
|
9
|
+
* Exported so edit.ts can check and clear entries.
|
|
10
|
+
*/
|
|
11
|
+
export const partiallyReadFiles = new Set();
|
|
12
|
+
async function execute(input, ctx) {
|
|
13
|
+
const { file_path: filePath, offset, limit } = input;
|
|
14
|
+
if (!filePath) {
|
|
15
|
+
return { output: 'Error: file_path is required', isError: true };
|
|
16
|
+
}
|
|
17
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(ctx.workingDir, filePath);
|
|
18
|
+
try {
|
|
19
|
+
const stat = fs.statSync(resolved);
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
// Helpfully list directory contents instead of just erroring
|
|
22
|
+
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
23
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name + '/');
|
|
24
|
+
const files = entries.filter(e => e.isFile()).map(e => e.name);
|
|
25
|
+
const listing = [...dirs.sort(), ...files.sort()].slice(0, 100);
|
|
26
|
+
return { output: `Directory: ${resolved}\n${listing.join('\n')}${entries.length > 100 ? `\n... (${entries.length - 100} more)` : ''}` };
|
|
27
|
+
}
|
|
28
|
+
// Size guard: skip huge files
|
|
29
|
+
const maxBytes = 2 * 1024 * 1024; // 2MB
|
|
30
|
+
if (stat.size > maxBytes) {
|
|
31
|
+
return { output: `Error: file is too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Use offset/limit to read a portion.`, isError: true };
|
|
32
|
+
}
|
|
33
|
+
// Detect binary files
|
|
34
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
35
|
+
const binaryExts = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', '.pdf', '.zip', '.tar', '.gz', '.woff', '.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.wav', '.avi', '.mov', '.exe', '.dll', '.so', '.dylib']);
|
|
36
|
+
if (binaryExts.has(ext)) {
|
|
37
|
+
const sizeStr = stat.size >= 1024 ? `${(stat.size / 1024).toFixed(1)}KB` : `${stat.size}B`;
|
|
38
|
+
return { output: `Binary file: ${resolved} (${ext}, ${sizeStr}). Cannot display contents.` };
|
|
39
|
+
}
|
|
40
|
+
const raw = fs.readFileSync(resolved, 'utf-8');
|
|
41
|
+
const allLines = raw.split('\n');
|
|
42
|
+
const startLine = Math.max(0, (Math.max(1, offset ?? 1)) - 1);
|
|
43
|
+
const maxLines = limit ?? 2000;
|
|
44
|
+
const endLine = Math.min(allLines.length, startLine + maxLines);
|
|
45
|
+
const slice = allLines.slice(startLine, endLine);
|
|
46
|
+
// Track partial reads — file was not read from the beginning or was truncated
|
|
47
|
+
const isPartial = startLine > 0 || endLine < allLines.length;
|
|
48
|
+
if (isPartial) {
|
|
49
|
+
partiallyReadFiles.add(resolved);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Full read — clear any stale partial flag
|
|
53
|
+
partiallyReadFiles.delete(resolved);
|
|
54
|
+
}
|
|
55
|
+
// Format with line numbers (cat -n style)
|
|
56
|
+
const numbered = slice.map((line, i) => `${startLine + i + 1}\t${line}`);
|
|
57
|
+
let result = numbered.join('\n');
|
|
58
|
+
if (endLine < allLines.length) {
|
|
59
|
+
result += `\n\n... (${allLines.length - endLine} more lines. Use offset=${endLine + 1} to continue.)`;
|
|
60
|
+
}
|
|
61
|
+
return { output: result || '(empty file)' };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
65
|
+
if (msg.includes('ENOENT')) {
|
|
66
|
+
return { output: `Error: file not found: ${resolved}`, isError: true };
|
|
67
|
+
}
|
|
68
|
+
if (msg.includes('EACCES')) {
|
|
69
|
+
return { output: `Error: permission denied: ${resolved}`, isError: true };
|
|
70
|
+
}
|
|
71
|
+
return { output: `Error reading file: ${msg}`, isError: true };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export const readCapability = {
|
|
75
|
+
spec: {
|
|
76
|
+
name: 'Read',
|
|
77
|
+
description: 'Read file with line numbers. Use offset/limit for large files.',
|
|
78
|
+
input_schema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
file_path: { type: 'string', description: 'Absolute path' },
|
|
82
|
+
offset: { type: 'number', description: 'Start line (1-based)' },
|
|
83
|
+
limit: { type: 'number', description: 'Max lines (default 2000)' },
|
|
84
|
+
},
|
|
85
|
+
required: ['file_path'],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
execute,
|
|
89
|
+
concurrent: true,
|
|
90
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubAgent capability — spawn a child agent for independent tasks.
|
|
3
|
+
*/
|
|
4
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
5
|
+
export declare function createSubAgentCapability(apiUrl: string, chain: 'base' | 'solana', capabilities: CapabilityHandler[]): CapabilityHandler;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubAgent capability — spawn a child agent for independent tasks.
|
|
3
|
+
*/
|
|
4
|
+
import { ModelClient } from '../agent/llm.js';
|
|
5
|
+
import { assembleInstructions } from '../agent/context.js';
|
|
6
|
+
// These will be injected at registration time
|
|
7
|
+
let registeredApiUrl = '';
|
|
8
|
+
let registeredChain = 'base';
|
|
9
|
+
let registeredCapabilities = [];
|
|
10
|
+
async function execute(input, ctx) {
|
|
11
|
+
const { prompt, description, model } = input;
|
|
12
|
+
if (!prompt) {
|
|
13
|
+
return { output: 'Error: prompt is required', isError: true };
|
|
14
|
+
}
|
|
15
|
+
const client = new ModelClient({
|
|
16
|
+
apiUrl: registeredApiUrl,
|
|
17
|
+
chain: registeredChain,
|
|
18
|
+
});
|
|
19
|
+
const capabilityMap = new Map();
|
|
20
|
+
// Sub-agents get a subset of tools (no sub-agent recursion)
|
|
21
|
+
const subTools = registeredCapabilities.filter(c => c.spec.name !== 'Agent');
|
|
22
|
+
for (const cap of subTools) {
|
|
23
|
+
capabilityMap.set(cap.spec.name, cap);
|
|
24
|
+
}
|
|
25
|
+
const toolDefs = subTools.map(c => c.spec);
|
|
26
|
+
const systemInstructions = assembleInstructions(ctx.workingDir);
|
|
27
|
+
const systemPrompt = systemInstructions.join('\n\n');
|
|
28
|
+
const history = [
|
|
29
|
+
{ role: 'user', content: prompt },
|
|
30
|
+
];
|
|
31
|
+
const maxTurns = 30;
|
|
32
|
+
const SUB_AGENT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minute total timeout
|
|
33
|
+
const deadline = Date.now() + SUB_AGENT_TIMEOUT_MS;
|
|
34
|
+
let turn = 0;
|
|
35
|
+
let finalText = '';
|
|
36
|
+
while (turn < maxTurns) {
|
|
37
|
+
if (Date.now() > deadline) {
|
|
38
|
+
return { output: `[${description || 'sub-agent'}] timed out after 5 minutes (${turn} turns completed).`, isError: true };
|
|
39
|
+
}
|
|
40
|
+
turn++;
|
|
41
|
+
const { content: parts } = await client.complete({
|
|
42
|
+
model: model || 'anthropic/claude-sonnet-4.6',
|
|
43
|
+
messages: history,
|
|
44
|
+
system: systemPrompt,
|
|
45
|
+
tools: toolDefs,
|
|
46
|
+
max_tokens: 16384,
|
|
47
|
+
stream: true,
|
|
48
|
+
}, ctx.abortSignal);
|
|
49
|
+
history.push({ role: 'assistant', content: parts });
|
|
50
|
+
// Collect text and invocations
|
|
51
|
+
const invocations = [];
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
if (part.type === 'text') {
|
|
54
|
+
finalText = part.text;
|
|
55
|
+
}
|
|
56
|
+
else if (part.type === 'tool_use') {
|
|
57
|
+
invocations.push(part);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (invocations.length === 0)
|
|
61
|
+
break;
|
|
62
|
+
// Execute tools
|
|
63
|
+
const outcomes = [];
|
|
64
|
+
for (const inv of invocations) {
|
|
65
|
+
const handler = capabilityMap.get(inv.name);
|
|
66
|
+
let result;
|
|
67
|
+
if (handler) {
|
|
68
|
+
try {
|
|
69
|
+
result = await handler.execute(inv.input, ctx);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
result = {
|
|
73
|
+
output: `Error: ${err.message}`,
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
result = { output: `Unknown tool: ${inv.name}`, isError: true };
|
|
80
|
+
}
|
|
81
|
+
outcomes.push({
|
|
82
|
+
type: 'tool_result',
|
|
83
|
+
tool_use_id: inv.id,
|
|
84
|
+
content: result.output,
|
|
85
|
+
is_error: result.isError,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
history.push({ role: 'user', content: outcomes });
|
|
89
|
+
}
|
|
90
|
+
const label = description || 'sub-agent';
|
|
91
|
+
return {
|
|
92
|
+
output: finalText || `[${label}] completed after ${turn} turn(s) with no text output.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function createSubAgentCapability(apiUrl, chain, capabilities) {
|
|
96
|
+
registeredApiUrl = apiUrl;
|
|
97
|
+
registeredChain = chain;
|
|
98
|
+
registeredCapabilities = capabilities;
|
|
99
|
+
return {
|
|
100
|
+
spec: {
|
|
101
|
+
name: 'Agent',
|
|
102
|
+
description: 'Launch a sub-agent for independent tasks. The sub-agent has its own context and tools.',
|
|
103
|
+
input_schema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
prompt: { type: 'string', description: 'The task for the sub-agent to perform' },
|
|
107
|
+
description: { type: 'string', description: 'Short description of what the sub-agent will do' },
|
|
108
|
+
model: { type: 'string', description: 'Model for the sub-agent. Default: claude-sonnet-4.6' },
|
|
109
|
+
},
|
|
110
|
+
required: ['prompt'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
execute,
|
|
114
|
+
concurrent: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task capability — in-session task tracking for the agent.
|
|
3
|
+
*/
|
|
4
|
+
// In-memory task store (per session)
|
|
5
|
+
const tasks = [];
|
|
6
|
+
let nextId = 1;
|
|
7
|
+
async function execute(input, _ctx) {
|
|
8
|
+
const { action, subject, description, task_id, status } = input;
|
|
9
|
+
switch (action) {
|
|
10
|
+
case 'create': {
|
|
11
|
+
if (!subject) {
|
|
12
|
+
return { output: 'Error: subject is required for create', isError: true };
|
|
13
|
+
}
|
|
14
|
+
const task = {
|
|
15
|
+
id: nextId++,
|
|
16
|
+
subject,
|
|
17
|
+
status: 'pending',
|
|
18
|
+
description,
|
|
19
|
+
};
|
|
20
|
+
tasks.push(task);
|
|
21
|
+
return { output: `Task #${task.id} created: ${task.subject}` };
|
|
22
|
+
}
|
|
23
|
+
case 'update': {
|
|
24
|
+
if (!task_id) {
|
|
25
|
+
return { output: 'Error: task_id is required for update', isError: true };
|
|
26
|
+
}
|
|
27
|
+
const task = tasks.find(t => t.id === task_id);
|
|
28
|
+
if (!task) {
|
|
29
|
+
return { output: `Error: task #${task_id} not found`, isError: true };
|
|
30
|
+
}
|
|
31
|
+
if (status)
|
|
32
|
+
task.status = status;
|
|
33
|
+
if (subject)
|
|
34
|
+
task.subject = subject;
|
|
35
|
+
if (description)
|
|
36
|
+
task.description = description;
|
|
37
|
+
return { output: `Task #${task.id} updated: ${task.status} — ${task.subject}` };
|
|
38
|
+
}
|
|
39
|
+
case 'list': {
|
|
40
|
+
if (tasks.length === 0) {
|
|
41
|
+
return { output: 'No tasks.' };
|
|
42
|
+
}
|
|
43
|
+
const pending = tasks.filter(t => t.status !== 'completed').length;
|
|
44
|
+
const done = tasks.filter(t => t.status === 'completed').length;
|
|
45
|
+
const lines = tasks.map(t => {
|
|
46
|
+
const icon = t.status === 'completed' ? '✓' : t.status === 'in_progress' ? '→' : '○';
|
|
47
|
+
return `${icon} #${t.id} [${t.status}] ${t.subject}`;
|
|
48
|
+
});
|
|
49
|
+
lines.push(`\n${done} done, ${pending} remaining`);
|
|
50
|
+
return { output: lines.join('\n') };
|
|
51
|
+
}
|
|
52
|
+
case 'delete': {
|
|
53
|
+
if (!task_id) {
|
|
54
|
+
return { output: 'Error: task_id is required for delete', isError: true };
|
|
55
|
+
}
|
|
56
|
+
const idx = tasks.findIndex(t => t.id === task_id);
|
|
57
|
+
if (idx === -1) {
|
|
58
|
+
return { output: `Error: task #${task_id} not found`, isError: true };
|
|
59
|
+
}
|
|
60
|
+
const removed = tasks.splice(idx, 1)[0];
|
|
61
|
+
return { output: `Task #${removed.id} deleted: ${removed.subject}` };
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
return { output: `Error: unknown action "${action}". Use create, update, or list.`, isError: true };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export const taskCapability = {
|
|
68
|
+
spec: {
|
|
69
|
+
name: 'Task',
|
|
70
|
+
description: 'Manage in-session tasks. Actions: create, update (status/subject), list (with summary), delete.',
|
|
71
|
+
input_schema: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
action: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: 'Action: "create", "update", "list", or "delete"',
|
|
77
|
+
},
|
|
78
|
+
subject: { type: 'string', description: 'Task title (for create/update)' },
|
|
79
|
+
description: { type: 'string', description: 'Task description (for create/update)' },
|
|
80
|
+
task_id: { type: 'number', description: 'Task ID (for update)' },
|
|
81
|
+
status: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
description: 'New status: "pending", "in_progress", or "completed" (for update)',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: ['action'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
execute,
|
|
90
|
+
concurrent: false,
|
|
91
|
+
};
|