@blockrun/franklin 3.10.0 → 3.10.2
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/commands/start.d.ts +4 -0
- package/dist/commands/start.js +57 -5
- package/dist/index.js +12 -2
- package/dist/panel/html.js +501 -23
- package/dist/panel/server.js +127 -0
- package/dist/session/from-import.d.ts +18 -0
- package/dist/session/from-import.js +553 -0
- package/dist/stats/tracker.d.ts +4 -0
- package/dist/stats/tracker.js +30 -4
- package/dist/ui/app.js +6 -12
- package/package.json +1 -1
package/dist/panel/server.js
CHANGED
|
@@ -15,6 +15,10 @@ import { loadLearnings } from '../learnings/store.js';
|
|
|
15
15
|
import { readAudit } from '../stats/audit.js';
|
|
16
16
|
import { snapshot as marketsSnapshot } from '../trading/providers/telemetry.js';
|
|
17
17
|
import { describeWiring } from '../trading/providers/registry.js';
|
|
18
|
+
import { listTasks, readTaskMeta, readTaskEvents, } from '../tasks/store.js';
|
|
19
|
+
import { reconcileLostTasks } from '../tasks/lost-detection.js';
|
|
20
|
+
import { taskLogPath } from '../tasks/paths.js';
|
|
21
|
+
import { isTerminalTaskStatus } from '../tasks/types.js';
|
|
18
22
|
import { getHTML } from './html.js';
|
|
19
23
|
const sseClients = new Set();
|
|
20
24
|
function json(res, data, status = 200) {
|
|
@@ -380,6 +384,129 @@ export function createPanelServer(port) {
|
|
|
380
384
|
json(res, learnings);
|
|
381
385
|
return;
|
|
382
386
|
}
|
|
387
|
+
// ─── Tasks ─────────────────────────────────────────────────────────
|
|
388
|
+
// Background tasks dispatched via the Detach tool / `franklin task`.
|
|
389
|
+
// The list endpoint reconciles lost tasks (dead pids) before snapshot
|
|
390
|
+
// so the UI never displays a zombie as "running". Detail / log /
|
|
391
|
+
// events endpoints power the per-task drawer in the Tasks tab.
|
|
392
|
+
if (p === '/api/tasks') {
|
|
393
|
+
try {
|
|
394
|
+
reconcileLostTasks();
|
|
395
|
+
}
|
|
396
|
+
catch { /* best-effort */ }
|
|
397
|
+
json(res, { tasks: listTasks() });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (p.startsWith('/api/tasks/')) {
|
|
401
|
+
const rest = p.slice('/api/tasks/'.length);
|
|
402
|
+
const segments = rest.split('/');
|
|
403
|
+
const runId = decodeURIComponent(segments[0] || '');
|
|
404
|
+
const sub = segments[1];
|
|
405
|
+
if (!runId) {
|
|
406
|
+
res.writeHead(404);
|
|
407
|
+
res.end('Not found');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// GET /api/tasks/:runId
|
|
411
|
+
if (!sub) {
|
|
412
|
+
const meta = readTaskMeta(runId);
|
|
413
|
+
if (!meta) {
|
|
414
|
+
res.writeHead(404);
|
|
415
|
+
res.end('Not found');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
json(res, meta);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
// GET /api/tasks/:runId/log — supports Range: bytes=N- for tail polling.
|
|
422
|
+
// Brand-new tasks may not have created log.txt yet — return empty 200
|
|
423
|
+
// rather than 404 so the panel UI's tail loop doesn't surface noise.
|
|
424
|
+
if (sub === 'log') {
|
|
425
|
+
const logPath = taskLogPath(runId);
|
|
426
|
+
let content;
|
|
427
|
+
try {
|
|
428
|
+
content = fs.readFileSync(logPath);
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
if (err.code === 'ENOENT') {
|
|
432
|
+
res.writeHead(200, {
|
|
433
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
434
|
+
'Cache-Control': 'no-store',
|
|
435
|
+
});
|
|
436
|
+
res.end('');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
throw err;
|
|
440
|
+
}
|
|
441
|
+
const total = content.length;
|
|
442
|
+
const range = req.headers['range'];
|
|
443
|
+
if (typeof range === 'string') {
|
|
444
|
+
const m = range.match(/^bytes=(\d+)-$/);
|
|
445
|
+
if (m) {
|
|
446
|
+
const start = Math.min(parseInt(m[1], 10), total);
|
|
447
|
+
const slice = content.subarray(start);
|
|
448
|
+
res.writeHead(206, {
|
|
449
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
450
|
+
'Cache-Control': 'no-store',
|
|
451
|
+
'Content-Range': `bytes ${start}-${Math.max(total - 1, start)}/${total}`,
|
|
452
|
+
});
|
|
453
|
+
res.end(slice);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
res.writeHead(200, {
|
|
458
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
459
|
+
'Cache-Control': 'no-store',
|
|
460
|
+
});
|
|
461
|
+
res.end(content);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
// GET /api/tasks/:runId/events
|
|
465
|
+
if (sub === 'events') {
|
|
466
|
+
json(res, { events: readTaskEvents(runId) });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
// POST /api/tasks/:runId/cancel — loopback only.
|
|
470
|
+
// Sends SIGTERM to the recorded pid; the runner then writes a
|
|
471
|
+
// `cancelled` event itself. This endpoint never mutates meta
|
|
472
|
+
// directly to avoid racing the runner (see store.ts contract).
|
|
473
|
+
if (sub === 'cancel' && req.method === 'POST') {
|
|
474
|
+
if (!isLoopback(req)) {
|
|
475
|
+
json(res, { error: 'forbidden' }, 403);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const meta = readTaskMeta(runId);
|
|
480
|
+
if (!meta) {
|
|
481
|
+
res.writeHead(404);
|
|
482
|
+
res.end('Not found');
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (isTerminalTaskStatus(meta.status)) {
|
|
486
|
+
json(res, { ok: false, reason: `already ${meta.status}` });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (typeof meta.pid !== 'number') {
|
|
490
|
+
json(res, { ok: false, reason: 'no pid recorded' });
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
process.kill(meta.pid, 'SIGTERM');
|
|
495
|
+
json(res, { ok: true });
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
json(res, { ok: false, reason: err.message });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
json(res, { ok: false, reason: err.message });
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
res.writeHead(404);
|
|
507
|
+
res.end('Not found');
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
383
510
|
// 404
|
|
384
511
|
res.writeHead(404);
|
|
385
512
|
res.end('Not found');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type ExternalAgentSource = 'claude' | 'codex';
|
|
2
|
+
export interface ExternalSessionCandidate {
|
|
3
|
+
id: string;
|
|
4
|
+
source: ExternalAgentSource;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
summary?: string;
|
|
7
|
+
updatedAt: number;
|
|
8
|
+
filePath: string;
|
|
9
|
+
bytes: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseExternalAgentSource(input: string): ExternalAgentSource | null;
|
|
12
|
+
export declare function importExternalSessionAsFranklin(source: ExternalAgentSource, externalSessionId: string | undefined, opts: {
|
|
13
|
+
model: string;
|
|
14
|
+
workDir: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
sessionId: string;
|
|
17
|
+
imported: ExternalSessionCandidate;
|
|
18
|
+
}>;
|