@blockrun/franklin 3.9.6 → 3.10.1
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/context.js +1 -1
- package/dist/commands/task.d.ts +11 -0
- package/dist/commands/task.js +134 -0
- package/dist/index.js +16 -0
- package/dist/panel/html.js +492 -21
- package/dist/panel/server.js +127 -0
- package/dist/tasks/lost-detection.d.ts +15 -0
- package/dist/tasks/lost-detection.js +51 -0
- package/dist/tasks/paths.d.ts +12 -0
- package/dist/tasks/paths.js +32 -0
- package/dist/tasks/runner.d.ts +21 -0
- package/dist/tasks/runner.js +191 -0
- package/dist/tasks/spawn.d.ts +26 -0
- package/dist/tasks/spawn.js +72 -0
- package/dist/tasks/store.d.ts +24 -0
- package/dist/tasks/store.js +124 -0
- package/dist/tasks/types.d.ts +32 -0
- package/dist/tasks/types.js +14 -0
- package/dist/tools/detach.d.ts +9 -0
- package/dist/tools/detach.js +53 -0
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/tool-categories.js +4 -0
- package/package.json +1 -1
package/dist/agent/context.js
CHANGED
|
@@ -174,7 +174,7 @@ function getToolPatternsSection() {
|
|
|
174
174
|
- **Research**: WebSearch for discovery → WebFetch for specific URLs from search results. Don't WebFetch URLs you invented.
|
|
175
175
|
- **Complex tasks**: Use Agent to spawn sub-agents for 2+ independent research or implementation tasks. Don't do sequentially what can be done in parallel.
|
|
176
176
|
- **Multiple independent lookups**: Call all tools in a single response. NEVER make sequential calls when parallel calls would work.
|
|
177
|
-
- **Long-running iteration (>20 items)**:
|
|
177
|
+
- **Long-running iteration (>20 items)**: Use the **Detach** tool, not turn-by-turn loops. Write a script that iterates and persists a checkpoint file (e.g. \`./.franklin/<task>.checkpoint.json\` with cursor + processedCount), then start it via Detach — \`{ label: "scrape stargazers", command: "node fetch.mjs" }\`. Detach returns a runId immediately and the work continues even if Franklin exits. Inspect with \`franklin task tail <runId> --follow\` / \`task wait <runId>\` / \`task cancel <runId>\`. The agent's job is to design and orchestrate, not to be the for-loop. Pattern fits paginated APIs, batch enrichment, large CSV emit, anything where the loop body is deterministic.
|
|
178
178
|
|
|
179
179
|
# Grounding Before Answering
|
|
180
180
|
Your training data is frozen in the past. Live-world questions MUST be answered from tool results, not memory.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `franklin task` CLI surface — human-facing operations on detached background
|
|
3
|
+
* tasks. Mirrors the on-disk shape under `~/.franklin/tasks/<runId>/` that the
|
|
4
|
+
* runner / store layers maintain. Subcommands grow incrementally over T10–T13:
|
|
5
|
+
* - list : recent tasks, newest first
|
|
6
|
+
* - tail : print log + status; --follow polls until terminal
|
|
7
|
+
* - cancel : SIGTERM the runner pid
|
|
8
|
+
* - wait : block until terminal, exit 0/1/2 by outcome
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
export declare function buildTaskCommand(): Command;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `franklin task` CLI surface — human-facing operations on detached background
|
|
3
|
+
* tasks. Mirrors the on-disk shape under `~/.franklin/tasks/<runId>/` that the
|
|
4
|
+
* runner / store layers maintain. Subcommands grow incrementally over T10–T13:
|
|
5
|
+
* - list : recent tasks, newest first
|
|
6
|
+
* - tail : print log + status; --follow polls until terminal
|
|
7
|
+
* - cancel : SIGTERM the runner pid
|
|
8
|
+
* - wait : block until terminal, exit 0/1/2 by outcome
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { listTasks, readTaskMeta } from '../tasks/store.js';
|
|
13
|
+
import { reconcileLostTasks } from '../tasks/lost-detection.js';
|
|
14
|
+
import { taskLogPath } from '../tasks/paths.js';
|
|
15
|
+
import { isTerminalTaskStatus } from '../tasks/types.js';
|
|
16
|
+
function fmtAge(ms) {
|
|
17
|
+
const s = Math.floor(ms / 1000);
|
|
18
|
+
if (s < 60)
|
|
19
|
+
return `${s}s`;
|
|
20
|
+
const m = Math.floor(s / 60);
|
|
21
|
+
if (m < 60)
|
|
22
|
+
return `${m}m`;
|
|
23
|
+
return `${Math.floor(m / 60)}h${m % 60}m`;
|
|
24
|
+
}
|
|
25
|
+
export function buildTaskCommand() {
|
|
26
|
+
const cmd = new Command('task').description('Manage long-running detached tasks');
|
|
27
|
+
cmd
|
|
28
|
+
.command('list')
|
|
29
|
+
.description('List recent tasks (newest first)')
|
|
30
|
+
.action(() => {
|
|
31
|
+
reconcileLostTasks();
|
|
32
|
+
const tasks = listTasks();
|
|
33
|
+
if (tasks.length === 0) {
|
|
34
|
+
console.log('No tasks. Start one via the Task agent tool.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
for (const t of tasks) {
|
|
39
|
+
const age = fmtAge(now - (t.lastEventAt ?? t.createdAt));
|
|
40
|
+
console.log(`${t.runId} ${t.status.padEnd(10)} ${age.padStart(5)} ${t.label}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
cmd
|
|
44
|
+
.command('tail <runId>')
|
|
45
|
+
.description('Print log + current status for a task')
|
|
46
|
+
.option('-f, --follow', 'Poll until task reaches terminal state')
|
|
47
|
+
.action(async (runId, opts) => {
|
|
48
|
+
const meta0 = readTaskMeta(runId);
|
|
49
|
+
if (!meta0) {
|
|
50
|
+
console.error(`No task: ${runId}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
let printed = 0;
|
|
54
|
+
const printNew = () => {
|
|
55
|
+
try {
|
|
56
|
+
const buf = fs.readFileSync(taskLogPath(runId));
|
|
57
|
+
if (buf.length > printed) {
|
|
58
|
+
process.stdout.write(buf.subarray(printed));
|
|
59
|
+
printed = buf.length;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* log not yet written */
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
printNew();
|
|
67
|
+
if (opts.follow) {
|
|
68
|
+
while (true) {
|
|
69
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
70
|
+
printNew();
|
|
71
|
+
const meta = readTaskMeta(runId);
|
|
72
|
+
if (meta && isTerminalTaskStatus(meta.status))
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const meta = readTaskMeta(runId);
|
|
77
|
+
if (meta) {
|
|
78
|
+
console.log(`\n--- ${meta.status} ---`);
|
|
79
|
+
if (meta.terminalSummary)
|
|
80
|
+
console.log(meta.terminalSummary);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
cmd
|
|
84
|
+
.command('wait <runId>')
|
|
85
|
+
.description('Block until task reaches terminal state, then exit')
|
|
86
|
+
.option('--timeout <ms>', 'Max wait, default 30 minutes', '1800000')
|
|
87
|
+
.action(async (runId, opts) => {
|
|
88
|
+
const cap = parseInt(opts.timeout, 10);
|
|
89
|
+
const t0 = Date.now();
|
|
90
|
+
while (true) {
|
|
91
|
+
const meta = readTaskMeta(runId);
|
|
92
|
+
if (!meta) {
|
|
93
|
+
console.error(`No task: ${runId}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (isTerminalTaskStatus(meta.status)) {
|
|
97
|
+
console.log(`${meta.status}: ${meta.terminalSummary ?? ''}`);
|
|
98
|
+
process.exit(meta.status === 'succeeded' ? 0 : 1);
|
|
99
|
+
}
|
|
100
|
+
if (Date.now() - t0 > cap) {
|
|
101
|
+
console.error(`Timed out after ${cap}ms; task still ${meta.status}.`);
|
|
102
|
+
process.exit(2);
|
|
103
|
+
}
|
|
104
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
cmd
|
|
108
|
+
.command('cancel <runId>')
|
|
109
|
+
.description('Cancel a running task (SIGTERM to runner)')
|
|
110
|
+
.action((runId) => {
|
|
111
|
+
const meta = readTaskMeta(runId);
|
|
112
|
+
if (!meta) {
|
|
113
|
+
console.error(`No task: ${runId}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
if (isTerminalTaskStatus(meta.status)) {
|
|
117
|
+
console.log(`Task already ${meta.status}.`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (typeof meta.pid !== 'number') {
|
|
121
|
+
console.error('Task has no recorded pid (likely still queued).');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
process.kill(meta.pid, 'SIGTERM');
|
|
126
|
+
console.log(`SIGTERM sent to ${meta.pid}.`);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error(`Could not signal pid ${meta.pid}: ${err.message}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return cmd;
|
|
134
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { daemonCommand } from './commands/daemon.js';
|
|
|
23
23
|
import { initCommand } from './commands/init.js';
|
|
24
24
|
import { uninitCommand } from './commands/uninit.js';
|
|
25
25
|
import { proxyCommand } from './commands/proxy.js';
|
|
26
|
+
import { buildTaskCommand } from './commands/task.js';
|
|
26
27
|
import { VERSION as version } from './config.js';
|
|
27
28
|
const program = new Command();
|
|
28
29
|
program
|
|
@@ -215,6 +216,21 @@ program
|
|
|
215
216
|
const { listAvailablePlugins } = await import('./commands/plugin.js');
|
|
216
217
|
listAvailablePlugins();
|
|
217
218
|
});
|
|
219
|
+
// `franklin task <subcmd>` — human-facing CLI for detached background tasks.
|
|
220
|
+
// Defined in src/commands/task.ts; subcommands: list, tail, cancel, wait.
|
|
221
|
+
program.addCommand(buildTaskCommand());
|
|
222
|
+
// Hidden internal subcommand — invoked by startDetachedTask via spawn(detached).
|
|
223
|
+
// The underscore prefix signals "not for humans"; we still register it via
|
|
224
|
+
// commander so exit codes and arg parsing stay consistent with the rest of
|
|
225
|
+
// the CLI.
|
|
226
|
+
program
|
|
227
|
+
.command('_task-runner <runId>')
|
|
228
|
+
.description('(internal) execute a detached task by runId')
|
|
229
|
+
.action(async (runId) => {
|
|
230
|
+
const { runDetachedTask } = await import('./tasks/runner.js');
|
|
231
|
+
const code = await runDetachedTask(runId);
|
|
232
|
+
process.exit(code);
|
|
233
|
+
});
|
|
218
234
|
// Default action: if no subcommand given, run 'start'
|
|
219
235
|
const args = process.argv.slice(2);
|
|
220
236
|
const firstArg = args[0];
|