@blockrun/franklin 3.6.20 → 3.6.22
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/dist/agent/error-classifier.js +3 -0
- package/dist/commands/daemon.js +12 -3
- package/dist/commands/panel.js +4 -3
- package/dist/mcp/client.js +35 -4
- package/dist/session/storage.js +19 -2
- package/dist/tools/task.js +2 -0
- package/dist/ui/app.js +1 -1
- package/package.json +1 -1
|
@@ -28,9 +28,12 @@ export function classifyAgentError(message) {
|
|
|
28
28
|
if (includesAny(err, [
|
|
29
29
|
'401',
|
|
30
30
|
'unauthorized',
|
|
31
|
+
'unauthenticated',
|
|
32
|
+
'not authenticated',
|
|
31
33
|
'invalid api key',
|
|
32
34
|
'invalid x-api-key',
|
|
33
35
|
'authentication failed',
|
|
36
|
+
'authentication required',
|
|
34
37
|
])) {
|
|
35
38
|
return {
|
|
36
39
|
category: 'auth', label: 'Auth', isTransient: false,
|
package/dist/commands/daemon.js
CHANGED
|
@@ -22,15 +22,24 @@ function isRunning(pid) {
|
|
|
22
22
|
catch {
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
|
-
// PID may have been recycled to
|
|
26
|
-
//
|
|
25
|
+
// PID may have been recycled. Try to confirm command line looks like Franklin.
|
|
26
|
+
// Platform fallbacks (some env lack `ps`): Linux /proc → ps → give up.
|
|
27
|
+
try {
|
|
28
|
+
// Linux (including Alpine/busybox containers) exposes /proc/<pid>/cmdline
|
|
29
|
+
const procCmdline = `/proc/${pid}/cmdline`;
|
|
30
|
+
if (fs.existsSync(procCmdline)) {
|
|
31
|
+
const raw = fs.readFileSync(procCmdline, 'utf-8').replace(/\0/g, ' ').trim();
|
|
32
|
+
return /franklin|runcode|node.*dist\/index/.test(raw);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch { /* fall through to ps */ }
|
|
27
36
|
try {
|
|
28
37
|
const { execSync } = require('node:child_process');
|
|
29
38
|
const cmd = execSync(`ps -p ${pid} -o command=`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
30
39
|
return /franklin|runcode|node.*dist\/index/.test(cmd);
|
|
31
40
|
}
|
|
32
41
|
catch {
|
|
33
|
-
// ps
|
|
42
|
+
// No ps, no /proc — give up and trust the kill-0 check
|
|
34
43
|
return true;
|
|
35
44
|
}
|
|
36
45
|
}
|
package/dist/commands/panel.js
CHANGED
|
@@ -5,13 +5,13 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { createPanelServer } from '../panel/server.js';
|
|
6
6
|
export async function panelCommand(options) {
|
|
7
7
|
const requestedPort = parseInt(options.port || '3100', 10);
|
|
8
|
-
// Handle port-in-use by trying up to 20 subsequent ports.
|
|
8
|
+
// Handle port-in-use by trying up to 20 subsequent ports silently.
|
|
9
|
+
// Only log when we finally bind (or fail completely) — no per-attempt spam.
|
|
9
10
|
const MAX_ATTEMPTS = 20;
|
|
10
11
|
const tryListen = (port, attempt) => {
|
|
11
12
|
const server = createPanelServer(port);
|
|
12
13
|
server.on('error', (err) => {
|
|
13
14
|
if (err.code === 'EADDRINUSE' && attempt < MAX_ATTEMPTS) {
|
|
14
|
-
console.log(chalk.yellow(` Port ${port} busy — trying ${port + 1}...`));
|
|
15
15
|
tryListen(port + 1, attempt + 1);
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
@@ -25,7 +25,8 @@ export async function panelCommand(options) {
|
|
|
25
25
|
server.listen(port, () => {
|
|
26
26
|
console.log('');
|
|
27
27
|
console.log(chalk.bold(' Franklin Panel'));
|
|
28
|
-
console.log(chalk.dim(` http://localhost:${port}`)
|
|
28
|
+
console.log(chalk.dim(` http://localhost:${port}`) +
|
|
29
|
+
(port !== requestedPort ? chalk.yellow(` (fell back from ${requestedPort})`) : ''));
|
|
29
30
|
console.log('');
|
|
30
31
|
console.log(chalk.dim(' Press Ctrl+C to stop.'));
|
|
31
32
|
console.log('');
|
package/dist/mcp/client.js
CHANGED
|
@@ -7,6 +7,40 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
|
7
7
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
8
|
// ─── Connection Management ────────────────────────────────────────────────
|
|
9
9
|
const connections = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Sanitize a JSON schema for strict LLM providers (OpenAI o3, etc.).
|
|
12
|
+
* Walks the schema tree and adds `items` to any array missing it.
|
|
13
|
+
* Without this, models like o3 reject the tool with:
|
|
14
|
+
* "Invalid schema: In context=(...), array schema missing items."
|
|
15
|
+
*/
|
|
16
|
+
function sanitizeSchema(schema) {
|
|
17
|
+
if (!schema || typeof schema !== 'object') {
|
|
18
|
+
return { type: 'object', properties: {} };
|
|
19
|
+
}
|
|
20
|
+
const s = schema;
|
|
21
|
+
// If array type without items, add a permissive default
|
|
22
|
+
if (s.type === 'array' && !s.items) {
|
|
23
|
+
s.items = {};
|
|
24
|
+
}
|
|
25
|
+
// Recurse into properties
|
|
26
|
+
if (s.properties && typeof s.properties === 'object') {
|
|
27
|
+
const props = s.properties;
|
|
28
|
+
for (const key of Object.keys(props)) {
|
|
29
|
+
props[key] = sanitizeSchema(props[key]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Recurse into items (nested arrays)
|
|
33
|
+
if (s.items && typeof s.items === 'object') {
|
|
34
|
+
s.items = sanitizeSchema(s.items);
|
|
35
|
+
}
|
|
36
|
+
// Recurse into anyOf / oneOf / allOf
|
|
37
|
+
for (const key of ['anyOf', 'oneOf', 'allOf']) {
|
|
38
|
+
if (Array.isArray(s[key])) {
|
|
39
|
+
s[key] = s[key].map(sanitizeSchema);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
10
44
|
/**
|
|
11
45
|
* Connect to an MCP server via stdio transport.
|
|
12
46
|
* Discovers tools and returns them as CapabilityHandlers.
|
|
@@ -47,10 +81,7 @@ async function connectStdio(name, config) {
|
|
|
47
81
|
spec: {
|
|
48
82
|
name: toolName,
|
|
49
83
|
description: toolDescription || `MCP tool from ${name}`,
|
|
50
|
-
input_schema: tool.inputSchema
|
|
51
|
-
type: 'object',
|
|
52
|
-
properties: {},
|
|
53
|
-
},
|
|
84
|
+
input_schema: sanitizeSchema(tool.inputSchema),
|
|
54
85
|
},
|
|
55
86
|
execute: async (input, _ctx) => {
|
|
56
87
|
const MCP_TOOL_TIMEOUT = 30_000;
|
package/dist/session/storage.js
CHANGED
|
@@ -95,10 +95,27 @@ export function updateSessionMeta(sessionId, meta) {
|
|
|
95
95
|
};
|
|
96
96
|
// Atomic write: tmp file + rename. Prevents corruption when parent
|
|
97
97
|
// and sub-agent update the same session meta concurrently.
|
|
98
|
+
// On Windows, renameSync can throw EEXIST/EPERM on older filesystems —
|
|
99
|
+
// fall back to a direct write (non-atomic but still functional) and
|
|
100
|
+
// clean up the orphan tmp file.
|
|
98
101
|
const target = metaPath(sessionId);
|
|
99
102
|
const tmp = target + '.tmp';
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
const payload = JSON.stringify(updated, null, 2);
|
|
104
|
+
try {
|
|
105
|
+
fs.writeFileSync(tmp, payload);
|
|
106
|
+
fs.renameSync(tmp, target);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Best-effort: clean up the orphan tmp, then write target directly.
|
|
110
|
+
try {
|
|
111
|
+
fs.unlinkSync(tmp);
|
|
112
|
+
}
|
|
113
|
+
catch { /* may not exist */ }
|
|
114
|
+
try {
|
|
115
|
+
fs.writeFileSync(target, payload);
|
|
116
|
+
}
|
|
117
|
+
catch { /* give up; stats just get stale */ }
|
|
118
|
+
}
|
|
102
119
|
});
|
|
103
120
|
}
|
|
104
121
|
/**
|
package/dist/tools/task.js
CHANGED
|
@@ -130,10 +130,12 @@ export const taskCapability = {
|
|
|
130
130
|
},
|
|
131
131
|
addBlocks: {
|
|
132
132
|
type: 'array',
|
|
133
|
+
items: { type: 'number' },
|
|
133
134
|
description: 'Task IDs that cannot start until this task completes (for update)',
|
|
134
135
|
},
|
|
135
136
|
addBlockedBy: {
|
|
136
137
|
type: 'array',
|
|
138
|
+
items: { type: 'number' },
|
|
137
139
|
description: 'Task IDs that must complete before this task can start (for update)',
|
|
138
140
|
},
|
|
139
141
|
},
|
package/dist/ui/app.js
CHANGED
|
@@ -657,7 +657,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
|
|
|
657
657
|
const elapsed = Math.round((Date.now() - tool.startTime) / 1000);
|
|
658
658
|
const elapsedStr = elapsed > 0 ? ` ${elapsed}s` : '';
|
|
659
659
|
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), ' ', _jsx(Text, { bold: true, color: "cyan", children: tool.name }), tool.preview ? _jsxs(Text, { dimColor: true, children: ["(", tool.preview.slice(0, 70), ")"] }) : null, _jsx(Text, { dimColor: true, children: elapsedStr })] }), tool.liveLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: tool.liveLines.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['⎿ ', line.slice(0, 120)] }, i))) }))] }, id));
|
|
660
|
-
}), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "magenta", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsx(Text, { bold: true, children: "thinking" }), completedTools.length > 0 ? _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 step ", completedTools.length + 1] }) : null] }), thinkingText && (() => {
|
|
660
|
+
}), thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "magenta", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsx(Text, { bold: true, children: "thinking" }), completedTools.length > 0 ? _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 step ", completedTools.length + 1] }) : null] }), process.env.FRANKLIN_SHOW_THINKING === '1' && thinkingText && (() => {
|
|
661
661
|
const lines = thinkingText.split('\n').filter(Boolean).slice(-3);
|
|
662
662
|
return (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: lines.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['⎿ ', line.slice(0, 120)] }, i))) }));
|
|
663
663
|
})()] })), waiting && !thinking && tools.size === 0 && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), ' ', _jsxs(Text, { dimColor: true, children: [shortModelName(currentModel), completedTools.length > 0 ? ` · step ${completedTools.length + 1}` : ''] })] }) })), streamText && (_jsx(Box, { marginTop: 0, marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(streamText) }) })), responsePreview && !streamText && (_jsx(Box, { flexDirection: "column", marginBottom: 0, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(responsePreview) }) })), inPicker && (() => {
|
package/package.json
CHANGED