@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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebFetch capability — fetch web page content.
|
|
3
|
+
*/
|
|
4
|
+
import { USER_AGENT } from '../config.js';
|
|
5
|
+
const MAX_BODY_BYTES = 256 * 1024; // 256KB
|
|
6
|
+
// ─── Session cache ──────────────────────────────────────────────────────────
|
|
7
|
+
// Avoids re-fetching the same URL within a session (common in research tasks).
|
|
8
|
+
// 15-min TTL, max 50 entries.
|
|
9
|
+
const CACHE_TTL_MS = 15 * 60 * 1000;
|
|
10
|
+
const MAX_CACHE_ENTRIES = 50;
|
|
11
|
+
const fetchCache = new Map();
|
|
12
|
+
function getCached(url) {
|
|
13
|
+
const entry = fetchCache.get(url);
|
|
14
|
+
if (!entry)
|
|
15
|
+
return null;
|
|
16
|
+
if (Date.now() > entry.expiresAt) {
|
|
17
|
+
fetchCache.delete(url);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return entry.output;
|
|
21
|
+
}
|
|
22
|
+
function setCached(url, output) {
|
|
23
|
+
// Evict oldest entry if at capacity
|
|
24
|
+
if (fetchCache.size >= MAX_CACHE_ENTRIES) {
|
|
25
|
+
const firstKey = fetchCache.keys().next().value;
|
|
26
|
+
if (firstKey)
|
|
27
|
+
fetchCache.delete(firstKey);
|
|
28
|
+
}
|
|
29
|
+
fetchCache.set(url, { output, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
30
|
+
}
|
|
31
|
+
// ─── Execute ────────────────────────────────────────────────────────────────
|
|
32
|
+
async function execute(input, _ctx) {
|
|
33
|
+
const { url, max_length } = input;
|
|
34
|
+
if (!url) {
|
|
35
|
+
return { output: 'Error: url is required', isError: true };
|
|
36
|
+
}
|
|
37
|
+
// Basic URL validation
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(url);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { output: `Error: invalid URL: ${url}`, isError: true };
|
|
44
|
+
}
|
|
45
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
46
|
+
return { output: `Error: only http/https URLs are supported`, isError: true };
|
|
47
|
+
}
|
|
48
|
+
// Check cache first
|
|
49
|
+
const cached = getCached(url);
|
|
50
|
+
if (cached) {
|
|
51
|
+
return { output: cached + '\n\n(cached)' };
|
|
52
|
+
}
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(url, {
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
headers: {
|
|
59
|
+
'User-Agent': USER_AGENT,
|
|
60
|
+
'Accept': 'text/html,application/json,text/plain,*/*',
|
|
61
|
+
},
|
|
62
|
+
redirect: 'follow',
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
return {
|
|
66
|
+
output: `HTTP ${response.status} ${response.statusText} for ${url}`,
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const contentType = response.headers.get('content-type') || '';
|
|
71
|
+
const maxLen = Math.min(max_length ?? MAX_BODY_BYTES, MAX_BODY_BYTES);
|
|
72
|
+
// Read body with size limit
|
|
73
|
+
const reader = response.body?.getReader();
|
|
74
|
+
if (!reader) {
|
|
75
|
+
return { output: 'Error: no response body', isError: true };
|
|
76
|
+
}
|
|
77
|
+
const chunks = [];
|
|
78
|
+
let totalBytes = 0;
|
|
79
|
+
try {
|
|
80
|
+
while (totalBytes < maxLen) {
|
|
81
|
+
const { done, value } = await reader.read();
|
|
82
|
+
if (done)
|
|
83
|
+
break;
|
|
84
|
+
chunks.push(value);
|
|
85
|
+
totalBytes += value.length;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
reader.releaseLock();
|
|
90
|
+
}
|
|
91
|
+
const decoder = new TextDecoder();
|
|
92
|
+
let body = decoder.decode(Buffer.concat(chunks)).slice(0, maxLen);
|
|
93
|
+
// Format response based on content type
|
|
94
|
+
if (contentType.includes('json')) {
|
|
95
|
+
try {
|
|
96
|
+
const parsedJson = JSON.parse(body);
|
|
97
|
+
body = JSON.stringify(parsedJson, null, 2).slice(0, maxLen);
|
|
98
|
+
}
|
|
99
|
+
catch { /* leave as-is if not valid JSON */ }
|
|
100
|
+
}
|
|
101
|
+
else if (contentType.includes('html')) {
|
|
102
|
+
body = stripHtml(body);
|
|
103
|
+
}
|
|
104
|
+
let output = `URL: ${url}\nStatus: ${response.status}\nContent-Type: ${contentType}\n\n${body}`;
|
|
105
|
+
if (totalBytes >= maxLen) {
|
|
106
|
+
output += '\n\n... (content truncated)';
|
|
107
|
+
}
|
|
108
|
+
// Cache successful responses
|
|
109
|
+
setCached(url, output);
|
|
110
|
+
return { output };
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
114
|
+
if (msg.includes('abort')) {
|
|
115
|
+
return { output: `Error: request timed out after 30s for ${url}`, isError: true };
|
|
116
|
+
}
|
|
117
|
+
return { output: `Error fetching ${url}: ${msg}`, isError: true };
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function stripHtml(html) {
|
|
124
|
+
return html
|
|
125
|
+
// Remove non-content elements
|
|
126
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
127
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
128
|
+
.replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, '')
|
|
129
|
+
.replace(/<header[^>]*>[\s\S]*?<\/header>/gi, '')
|
|
130
|
+
.replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, '')
|
|
131
|
+
.replace(/<aside[^>]*>[\s\S]*?<\/aside>/gi, '')
|
|
132
|
+
.replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, '')
|
|
133
|
+
.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '')
|
|
134
|
+
.replace(/<form[^>]*>[\s\S]*?<\/form>/gi, '')
|
|
135
|
+
// Convert block elements to newlines for readability
|
|
136
|
+
.replace(/<\/?(p|div|h[1-6]|li|br|tr)[^>]*>/gi, '\n')
|
|
137
|
+
// Strip remaining tags
|
|
138
|
+
.replace(/<[^>]+>/g, ' ')
|
|
139
|
+
// Decode entities
|
|
140
|
+
.replace(/ /g, ' ')
|
|
141
|
+
.replace(/&/g, '&')
|
|
142
|
+
.replace(/</g, '<')
|
|
143
|
+
.replace(/>/g, '>')
|
|
144
|
+
.replace(/"/g, '"')
|
|
145
|
+
.replace(/'/g, "'")
|
|
146
|
+
// Clean whitespace
|
|
147
|
+
.replace(/[ \t]+/g, ' ')
|
|
148
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
149
|
+
.trim();
|
|
150
|
+
}
|
|
151
|
+
export const webFetchCapability = {
|
|
152
|
+
spec: {
|
|
153
|
+
name: 'WebFetch',
|
|
154
|
+
description: 'Fetch a web page and return its content. HTML tags are stripped for readability. Results are cached for 15 minutes.',
|
|
155
|
+
input_schema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
url: { type: 'string', description: 'The URL to fetch' },
|
|
159
|
+
max_length: { type: 'number', description: 'Max content bytes to return. Default: 256KB' },
|
|
160
|
+
},
|
|
161
|
+
required: ['url'],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
execute,
|
|
165
|
+
concurrent: true,
|
|
166
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSearch capability — search the web via BlockRun API or DuckDuckGo fallback.
|
|
3
|
+
*/
|
|
4
|
+
import { VERSION } from '../config.js';
|
|
5
|
+
async function execute(input, _ctx) {
|
|
6
|
+
const { query, max_results } = input;
|
|
7
|
+
if (!query) {
|
|
8
|
+
return { output: 'Error: query is required', isError: true };
|
|
9
|
+
}
|
|
10
|
+
const maxResults = Math.min(Math.max(max_results ?? 5, 1), 20);
|
|
11
|
+
// Try DuckDuckGo HTML search (no API key needed)
|
|
12
|
+
try {
|
|
13
|
+
const encoded = encodeURIComponent(query);
|
|
14
|
+
const url = `https://html.duckduckgo.com/html/?q=${encoded}`;
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
signal: controller.signal,
|
|
19
|
+
headers: {
|
|
20
|
+
'User-Agent': `runcode/${VERSION} (coding-agent)`,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
clearTimeout(timeout);
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
return { output: `Search failed: HTTP ${response.status}`, isError: true };
|
|
26
|
+
}
|
|
27
|
+
const html = await response.text();
|
|
28
|
+
const results = parseDuckDuckGoResults(html, maxResults);
|
|
29
|
+
if (results.length === 0) {
|
|
30
|
+
return { output: `No results found for: ${query}` };
|
|
31
|
+
}
|
|
32
|
+
const formatted = results
|
|
33
|
+
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`)
|
|
34
|
+
.join('\n\n');
|
|
35
|
+
return { output: `Search results for "${query}":\n\n${formatted}` };
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39
|
+
if (msg.includes('abort')) {
|
|
40
|
+
return { output: `Search timed out after 15s for: ${query}`, isError: true };
|
|
41
|
+
}
|
|
42
|
+
return { output: `Search error: ${msg}`, isError: true };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function parseDuckDuckGoResults(html, maxResults) {
|
|
46
|
+
const results = [];
|
|
47
|
+
// Primary parser: match result blocks by class names
|
|
48
|
+
const linkRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
49
|
+
const snippetRegex = /<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
50
|
+
let links = [...html.matchAll(linkRegex)];
|
|
51
|
+
let snippets = [...html.matchAll(snippetRegex)];
|
|
52
|
+
// Fallback parser if primary finds nothing (DDG may have updated HTML)
|
|
53
|
+
if (links.length === 0) {
|
|
54
|
+
const fallbackLink = /<a[^>]*class="[^"]*result[^"]*"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
55
|
+
links = [...html.matchAll(fallbackLink)];
|
|
56
|
+
}
|
|
57
|
+
for (let i = 0; i < Math.min(links.length, maxResults); i++) {
|
|
58
|
+
const link = links[i];
|
|
59
|
+
const snippet = snippets[i];
|
|
60
|
+
let url = link[1] || '';
|
|
61
|
+
// DuckDuckGo wraps URLs in redirect — extract the actual URL
|
|
62
|
+
const uddgMatch = url.match(/uddg=([^&]+)/);
|
|
63
|
+
if (uddgMatch) {
|
|
64
|
+
url = decodeURIComponent(uddgMatch[1]);
|
|
65
|
+
}
|
|
66
|
+
// Skip internal DDG links
|
|
67
|
+
if (url.startsWith('/') || url.includes('duckduckgo.com'))
|
|
68
|
+
continue;
|
|
69
|
+
results.push({
|
|
70
|
+
title: stripTags(link[2] || '').trim(),
|
|
71
|
+
url,
|
|
72
|
+
snippet: stripTags(snippet?.[1] || '').trim(),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
function stripTags(html) {
|
|
78
|
+
return html
|
|
79
|
+
.replace(/<[^>]+>/g, '')
|
|
80
|
+
.replace(/&/g, '&')
|
|
81
|
+
.replace(/</g, '<')
|
|
82
|
+
.replace(/>/g, '>')
|
|
83
|
+
.replace(/"/g, '"')
|
|
84
|
+
.replace(/'/g, "'")
|
|
85
|
+
.replace(/ /g, ' ')
|
|
86
|
+
.replace(/\s+/g, ' ');
|
|
87
|
+
}
|
|
88
|
+
export const webSearchCapability = {
|
|
89
|
+
spec: {
|
|
90
|
+
name: 'WebSearch',
|
|
91
|
+
description: 'Search the web and return results with titles, URLs, and snippets.',
|
|
92
|
+
input_schema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
query: { type: 'string', description: 'The search query' },
|
|
96
|
+
max_results: { type: 'number', description: 'Max number of results. Default: 5' },
|
|
97
|
+
},
|
|
98
|
+
required: ['query'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
execute,
|
|
102
|
+
concurrent: true,
|
|
103
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write capability — creates or overwrites files.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
function withTrailingSep(value) {
|
|
8
|
+
return value.endsWith(path.sep) ? value : value + path.sep;
|
|
9
|
+
}
|
|
10
|
+
function isWithinDir(target, dir) {
|
|
11
|
+
const normalizedTarget = path.resolve(target);
|
|
12
|
+
const normalizedDir = withTrailingSep(path.resolve(dir));
|
|
13
|
+
return normalizedTarget === normalizedDir.slice(0, -1) || normalizedTarget.startsWith(normalizedDir);
|
|
14
|
+
}
|
|
15
|
+
function getAllowedTempDirs() {
|
|
16
|
+
const candidates = new Set([path.resolve(os.tmpdir())]);
|
|
17
|
+
for (const dir of [...candidates]) {
|
|
18
|
+
try {
|
|
19
|
+
candidates.add(path.resolve(fs.realpathSync(dir)));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Best effort only.
|
|
23
|
+
}
|
|
24
|
+
if (dir.startsWith('/private/')) {
|
|
25
|
+
candidates.add(dir.slice('/private'.length));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
candidates.add(path.join('/private', dir));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...candidates];
|
|
32
|
+
}
|
|
33
|
+
async function execute(input, ctx) {
|
|
34
|
+
const { file_path: filePath, content } = input;
|
|
35
|
+
if (!filePath) {
|
|
36
|
+
return { output: 'Error: file_path is required', isError: true };
|
|
37
|
+
}
|
|
38
|
+
if (content === undefined || content === null) {
|
|
39
|
+
return { output: 'Error: content is required', isError: true };
|
|
40
|
+
}
|
|
41
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(ctx.workingDir, filePath);
|
|
42
|
+
// Safety: block system paths and sensitive home directories
|
|
43
|
+
// Resolve symlinks to prevent traversal attacks
|
|
44
|
+
const home = os.homedir();
|
|
45
|
+
const allowedTempDirs = getAllowedTempDirs();
|
|
46
|
+
const dangerousPaths = [
|
|
47
|
+
'/etc/', '/usr/', '/bin/', '/sbin/', '/var/', '/System/',
|
|
48
|
+
path.join(home, '.ssh') + '/',
|
|
49
|
+
path.join(home, '.aws') + '/',
|
|
50
|
+
path.join(home, '.kube') + '/',
|
|
51
|
+
path.join(home, '.gnupg') + '/',
|
|
52
|
+
path.join(home, '.config/gcloud') + '/',
|
|
53
|
+
];
|
|
54
|
+
// Check both the resolved path and the real path (after symlink resolution)
|
|
55
|
+
const checkPath = (p) => !allowedTempDirs.some(dir => isWithinDir(p, dir)) &&
|
|
56
|
+
dangerousPaths.some(dp => p.startsWith(dp));
|
|
57
|
+
if (checkPath(resolved)) {
|
|
58
|
+
return { output: `Error: refusing to write to sensitive path: ${resolved}`, isError: true };
|
|
59
|
+
}
|
|
60
|
+
// Also check parent dir's real path if it already exists (symlink protection)
|
|
61
|
+
const parentDir = path.dirname(resolved);
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(parentDir)) {
|
|
64
|
+
const realParent = fs.realpathSync(parentDir);
|
|
65
|
+
if (checkPath(realParent + '/')) {
|
|
66
|
+
return { output: `Error: refusing to write — path resolves to sensitive location: ${realParent}`, isError: true };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { /* parent doesn't exist yet, will be created */ }
|
|
71
|
+
// Also check if target file itself is a symlink to a sensitive location
|
|
72
|
+
try {
|
|
73
|
+
if (fs.existsSync(resolved) && fs.lstatSync(resolved).isSymbolicLink()) {
|
|
74
|
+
const realTarget = fs.realpathSync(resolved);
|
|
75
|
+
if (checkPath(realTarget)) {
|
|
76
|
+
return { output: `Error: refusing to write — symlink resolves to sensitive location: ${realTarget}`, isError: true };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { /* file doesn't exist yet, ok */ }
|
|
81
|
+
try {
|
|
82
|
+
// Ensure parent directory exists
|
|
83
|
+
const parentDir = path.dirname(resolved);
|
|
84
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
85
|
+
const existed = fs.existsSync(resolved);
|
|
86
|
+
fs.writeFileSync(resolved, content, 'utf-8');
|
|
87
|
+
const lineCount = content.split('\n').length;
|
|
88
|
+
const byteCount = Buffer.byteLength(content, 'utf-8');
|
|
89
|
+
const sizeStr = byteCount >= 1024 ? `${(byteCount / 1024).toFixed(1)}KB` : `${byteCount}B`;
|
|
90
|
+
return {
|
|
91
|
+
output: `${existed ? 'Updated' : 'Created'} ${resolved} (${lineCount} lines, ${sizeStr})`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
return { output: `Error writing file: ${msg}`, isError: true };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export const writeCapability = {
|
|
100
|
+
spec: {
|
|
101
|
+
name: 'Write',
|
|
102
|
+
description: 'Create or overwrite a file.',
|
|
103
|
+
input_schema: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
file_path: { type: 'string', description: 'Absolute path' },
|
|
107
|
+
content: { type: 'string', description: 'File content' },
|
|
108
|
+
},
|
|
109
|
+
required: ['file_path', 'content'],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
execute,
|
|
113
|
+
concurrent: false,
|
|
114
|
+
};
|
package/dist/ui/app.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunCode ink-based terminal UI.
|
|
3
|
+
* Real-time streaming, thinking animation, tool progress, slash commands.
|
|
4
|
+
*/
|
|
5
|
+
import type { StreamEvent } from '../agent/types.js';
|
|
6
|
+
export interface InkUIHandle {
|
|
7
|
+
handleEvent: (event: StreamEvent) => void;
|
|
8
|
+
updateModel: (model: string) => void;
|
|
9
|
+
updateBalance: (balance: string) => void;
|
|
10
|
+
onTurnDone: (cb: () => void) => void;
|
|
11
|
+
waitForInput: () => Promise<string | null>;
|
|
12
|
+
onAbort: (cb: () => void) => void;
|
|
13
|
+
cleanup: () => void;
|
|
14
|
+
requestPermission: (toolName: string, description: string) => Promise<'yes' | 'no' | 'always'>;
|
|
15
|
+
requestAskUser: (question: string, options?: string[]) => Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
export declare function launchInkUI(opts: {
|
|
18
|
+
model: string;
|
|
19
|
+
workDir: string;
|
|
20
|
+
version: string;
|
|
21
|
+
walletAddress?: string;
|
|
22
|
+
walletBalance?: string;
|
|
23
|
+
chain?: string;
|
|
24
|
+
showPicker?: boolean;
|
|
25
|
+
onModelChange?: (model: string) => void;
|
|
26
|
+
}): InkUIHandle;
|