flightbox 0.1.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 +21 -0
- package/README.md +78 -0
- package/dist/atf.js +23 -0
- package/dist/browser.js +21 -0
- package/dist/cli.js +102 -0
- package/dist/collector.js +31 -0
- package/dist/commands/install.js +50 -0
- package/dist/commands/list.js +14 -0
- package/dist/commands/show.js +32 -0
- package/dist/commands/stats.js +12 -0
- package/dist/commands/ui.js +35 -0
- package/dist/format.js +10 -0
- package/dist/ingest/hooks.js +80 -0
- package/dist/ingest/ingest.js +84 -0
- package/dist/ingest/transcripts.js +58 -0
- package/dist/paths.js +26 -0
- package/dist/server/server.js +94 -0
- package/dist/server/static.js +35 -0
- package/dist/server/store-api.js +52 -0
- package/dist/store.js +159 -0
- package/dist/version.js +1 -0
- package/dist/web/assets/index-CoqqG1p0.css +1 -0
- package/dist/web/assets/index-DwxTC9or.js +49 -0
- package/dist/web/index.html +20 -0
- package/package.json +64 -0
package/dist/paths.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export function flightboxHome() {
|
|
5
|
+
return process.env.FLIGHTBOX_HOME ?? path.join(os.homedir(), '.flightbox');
|
|
6
|
+
}
|
|
7
|
+
export function rawLogDir() {
|
|
8
|
+
return path.join(flightboxHome(), 'raw');
|
|
9
|
+
}
|
|
10
|
+
export function dbPath() {
|
|
11
|
+
return path.join(flightboxHome(), 'db.sqlite');
|
|
12
|
+
}
|
|
13
|
+
export function claudeHome() {
|
|
14
|
+
return process.env.FLIGHTBOX_CLAUDE_HOME ?? path.join(os.homedir(), '.claude');
|
|
15
|
+
}
|
|
16
|
+
export function claudeProjectsDir() {
|
|
17
|
+
return path.join(claudeHome(), 'projects');
|
|
18
|
+
}
|
|
19
|
+
export function claudeSettingsPath() {
|
|
20
|
+
return path.join(claudeHome(), 'settings.json');
|
|
21
|
+
}
|
|
22
|
+
// Static web assets are emitted by Vite to dist/web, a sibling of this compiled file
|
|
23
|
+
// (dist/paths.js). Resolve relative to the module so it works from any CWD and via npx.
|
|
24
|
+
export function webDistDir() {
|
|
25
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'web');
|
|
26
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { webDistDir } from '../paths.js';
|
|
4
|
+
import { sessionListDto, sessionDetailDto, claimsDto } from './store-api.js';
|
|
5
|
+
import { resolveStaticFile } from './static.js';
|
|
6
|
+
function sendJson(res, status, body) {
|
|
7
|
+
const json = JSON.stringify(body);
|
|
8
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
9
|
+
res.end(json);
|
|
10
|
+
}
|
|
11
|
+
/** Decode a raw path segment into a session id, or null if invalid.
|
|
12
|
+
* Invalid means: URIError on decode, empty string, or contains a GLOB
|
|
13
|
+
* metacharacter (* ? [) that would widen the store's prefix query.
|
|
14
|
+
*/
|
|
15
|
+
function decodeId(raw) {
|
|
16
|
+
let decoded;
|
|
17
|
+
try {
|
|
18
|
+
decoded = decodeURIComponent(raw);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (decoded === '' || /[*?[]/.test(decoded))
|
|
24
|
+
return null;
|
|
25
|
+
return decoded;
|
|
26
|
+
}
|
|
27
|
+
export function createServer(store, distDir = webDistDir()) {
|
|
28
|
+
return http.createServer((req, res) => {
|
|
29
|
+
if (req.method !== 'GET') {
|
|
30
|
+
sendJson(res, 405, { error: 'method not allowed' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
34
|
+
const parts = url.pathname.split('/').filter(Boolean); // e.g. ['api','sessions','abc','claims']
|
|
35
|
+
if (parts[0] === 'api' && parts[1] === 'sessions') {
|
|
36
|
+
if (parts.length === 2) {
|
|
37
|
+
sendJson(res, 200, sessionListDto(store));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const id = decodeId(parts[2]);
|
|
41
|
+
if (id === null) {
|
|
42
|
+
sendJson(res, 404, { error: 'session not found' });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (parts.length === 3) {
|
|
46
|
+
const detail = sessionDetailDto(store, id);
|
|
47
|
+
detail ? sendJson(res, 200, detail) : sendJson(res, 404, { error: 'session not found' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (parts.length === 4 && parts[3] === 'claims') {
|
|
51
|
+
const claims = claimsDto(store, id);
|
|
52
|
+
claims ? sendJson(res, 200, claims) : sendJson(res, 404, { error: 'session not found' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Any unmatched /api/* route is a JSON 404 — never fall through to the SPA.
|
|
57
|
+
if (parts[0] === 'api') {
|
|
58
|
+
sendJson(res, 404, { error: 'not found' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Non-API GET: serve the built SPA (static asset or client-route fallback).
|
|
62
|
+
const resolved = resolveStaticFile(distDir, url.pathname);
|
|
63
|
+
if (resolved) {
|
|
64
|
+
res.writeHead(200, { 'content-type': resolved.contentType });
|
|
65
|
+
fs.createReadStream(resolved.filePath).pipe(res);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
sendJson(res, 404, { error: 'not found' });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function startServer(store, portStart = 51789, distDir = webDistDir()) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const server = createServer(store, distDir);
|
|
74
|
+
let port = portStart;
|
|
75
|
+
const tryListen = () => {
|
|
76
|
+
server.listen(port, '127.0.0.1');
|
|
77
|
+
};
|
|
78
|
+
server.on('listening', () => {
|
|
79
|
+
const addr = server.address();
|
|
80
|
+
const boundPort = typeof addr === 'object' && addr ? addr.port : port;
|
|
81
|
+
resolve({ server, port: boundPort, url: `http://127.0.0.1:${boundPort}` });
|
|
82
|
+
});
|
|
83
|
+
server.on('error', (err) => {
|
|
84
|
+
if (err.code === 'EADDRINUSE' && port !== 0 && port < portStart + 50) {
|
|
85
|
+
port += 1;
|
|
86
|
+
server.close(() => tryListen());
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
reject(err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
tryListen();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const TYPES = {
|
|
4
|
+
'.html': 'text/html; charset=utf-8',
|
|
5
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
6
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
7
|
+
'.css': 'text/css; charset=utf-8',
|
|
8
|
+
'.json': 'application/json; charset=utf-8',
|
|
9
|
+
'.svg': 'image/svg+xml',
|
|
10
|
+
'.png': 'image/png',
|
|
11
|
+
'.ico': 'image/x-icon',
|
|
12
|
+
'.woff2': 'font/woff2',
|
|
13
|
+
};
|
|
14
|
+
export function contentTypeFor(filePath) {
|
|
15
|
+
return TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';
|
|
16
|
+
}
|
|
17
|
+
/** Resolve a URL path to a file inside distDir. Traversal-safe. Client routes
|
|
18
|
+
* (no file extension, no match) fall back to index.html so the SPA can route. */
|
|
19
|
+
export function resolveStaticFile(distDir, urlPath) {
|
|
20
|
+
const root = path.resolve(distDir);
|
|
21
|
+
const rel = urlPath === '/' ? 'index.html' : urlPath.replace(/^\/+/, '');
|
|
22
|
+
const candidate = path.resolve(root, rel);
|
|
23
|
+
if (candidate !== root && !candidate.startsWith(root + path.sep))
|
|
24
|
+
return null; // traversal
|
|
25
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
26
|
+
return { filePath: candidate, contentType: contentTypeFor(candidate) };
|
|
27
|
+
}
|
|
28
|
+
// No extension → treat as a client-side route; serve index.html.
|
|
29
|
+
if (path.extname(candidate) === '') {
|
|
30
|
+
const index = path.join(root, 'index.html');
|
|
31
|
+
if (fs.existsSync(index))
|
|
32
|
+
return { filePath: index, contentType: contentTypeFor(index) };
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function durationMs(start, end) {
|
|
2
|
+
if (!start || !end)
|
|
3
|
+
return null;
|
|
4
|
+
const ms = new Date(end).getTime() - new Date(start).getTime();
|
|
5
|
+
return Number.isNaN(ms) || ms < 0 ? null : ms;
|
|
6
|
+
}
|
|
7
|
+
function discrepancy(store, id) {
|
|
8
|
+
if (store.hookEventCount(id) === 0)
|
|
9
|
+
return false;
|
|
10
|
+
return store.claimsForSession(id).some((c) => c.status !== 'succeeded');
|
|
11
|
+
}
|
|
12
|
+
export function sessionListDto(store) {
|
|
13
|
+
return store.listSessions().map((s) => ({
|
|
14
|
+
id: s.id,
|
|
15
|
+
project: s.project_dir,
|
|
16
|
+
startedAt: s.started_at,
|
|
17
|
+
endedAt: s.ended_at,
|
|
18
|
+
durationMs: durationMs(s.started_at, s.ended_at),
|
|
19
|
+
tokens: s.total_tokens,
|
|
20
|
+
fileCount: store.fileTouchCount(s.id),
|
|
21
|
+
hasDiscrepancy: discrepancy(store, s.id),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
export function sessionDetailDto(store, id) {
|
|
25
|
+
const s = store.findSession(id);
|
|
26
|
+
if (!s)
|
|
27
|
+
return null;
|
|
28
|
+
const events = store.eventsForSession(s.id);
|
|
29
|
+
return {
|
|
30
|
+
id: s.id,
|
|
31
|
+
project: s.project_dir,
|
|
32
|
+
model: s.model,
|
|
33
|
+
startedAt: s.started_at,
|
|
34
|
+
endedAt: s.ended_at,
|
|
35
|
+
durationMs: durationMs(s.started_at, s.ended_at),
|
|
36
|
+
tokens: store.sessionTokens(s.id),
|
|
37
|
+
fileCount: store.fileTouchCount(s.id),
|
|
38
|
+
commandCount: events.filter((e) => e.type === 'command').length,
|
|
39
|
+
subagentCount: events.filter((e) => e.type === 'subagent_spawn').length,
|
|
40
|
+
events: events.map((e) => ({ ts: e.ts, type: e.type, toolName: e.toolName, detail: e.detail, sidechain: e.sidechain })),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function claimsDto(store, id) {
|
|
44
|
+
const s = store.findSession(id);
|
|
45
|
+
if (!s)
|
|
46
|
+
return null;
|
|
47
|
+
return {
|
|
48
|
+
sessionId: s.id,
|
|
49
|
+
hooksPresent: store.hookEventCount(s.id) > 0,
|
|
50
|
+
files: store.claimsForSession(s.id).map(({ path, status }) => ({ path, status })),
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { dbPath } from './paths.js';
|
|
3
|
+
const SCHEMA = `
|
|
4
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
project_dir TEXT,
|
|
7
|
+
started_at TEXT,
|
|
8
|
+
ended_at TEXT,
|
|
9
|
+
model TEXT
|
|
10
|
+
);
|
|
11
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13
|
+
session_id TEXT NOT NULL,
|
|
14
|
+
ts TEXT NOT NULL,
|
|
15
|
+
type TEXT NOT NULL,
|
|
16
|
+
tool_name TEXT,
|
|
17
|
+
detail TEXT NOT NULL,
|
|
18
|
+
source TEXT NOT NULL,
|
|
19
|
+
sidechain INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
uniq_key TEXT NOT NULL UNIQUE
|
|
21
|
+
);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_events_session_ts ON events(session_id, ts);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS file_touches (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
session_id TEXT NOT NULL,
|
|
26
|
+
path TEXT NOT NULL,
|
|
27
|
+
action TEXT NOT NULL,
|
|
28
|
+
ts TEXT NOT NULL,
|
|
29
|
+
uniq_key TEXT NOT NULL UNIQUE
|
|
30
|
+
);
|
|
31
|
+
CREATE TABLE IF NOT EXISTS token_usage (
|
|
32
|
+
message_uuid TEXT PRIMARY KEY,
|
|
33
|
+
session_id TEXT NOT NULL,
|
|
34
|
+
model TEXT,
|
|
35
|
+
ts TEXT NOT NULL,
|
|
36
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
cache_creation_tokens INTEGER NOT NULL DEFAULT 0
|
|
40
|
+
);
|
|
41
|
+
CREATE TABLE IF NOT EXISTS tool_outcomes (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
session_id TEXT NOT NULL,
|
|
44
|
+
path TEXT,
|
|
45
|
+
tool_name TEXT NOT NULL,
|
|
46
|
+
success INTEGER,
|
|
47
|
+
raw_response TEXT NOT NULL,
|
|
48
|
+
ts TEXT NOT NULL,
|
|
49
|
+
uniq_key TEXT NOT NULL UNIQUE
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_outcomes_session_path ON tool_outcomes(session_id, path);
|
|
52
|
+
`;
|
|
53
|
+
const LIST_SQL = `
|
|
54
|
+
SELECT s.*,
|
|
55
|
+
(SELECT COUNT(*) FROM events e WHERE e.session_id = s.id) AS event_count,
|
|
56
|
+
COALESCE((SELECT SUM(input_tokens + output_tokens) FROM token_usage t WHERE t.session_id = s.id), 0) AS total_tokens
|
|
57
|
+
FROM sessions s
|
|
58
|
+
`;
|
|
59
|
+
export function openStore(file = dbPath()) {
|
|
60
|
+
const db = new Database(file);
|
|
61
|
+
db.pragma('journal_mode = WAL');
|
|
62
|
+
db.exec(SCHEMA);
|
|
63
|
+
const upsert = db.prepare(`
|
|
64
|
+
INSERT INTO sessions (id, project_dir, started_at, ended_at, model)
|
|
65
|
+
VALUES (@id, @projectDir, @startedAt, @endedAt, @model)
|
|
66
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
67
|
+
project_dir = COALESCE(excluded.project_dir, project_dir),
|
|
68
|
+
started_at = COALESCE(MIN(excluded.started_at, started_at), excluded.started_at, started_at),
|
|
69
|
+
ended_at = COALESCE(MAX(excluded.ended_at, ended_at), excluded.ended_at, ended_at),
|
|
70
|
+
model = COALESCE(excluded.model, model)
|
|
71
|
+
`);
|
|
72
|
+
const insEvent = db.prepare(`
|
|
73
|
+
INSERT OR IGNORE INTO events (session_id, ts, type, tool_name, detail, source, sidechain, uniq_key)
|
|
74
|
+
VALUES (@sessionId, @ts, @type, @toolName, @detail, @source, @sidechain, @uniqKey)
|
|
75
|
+
`);
|
|
76
|
+
const insTouch = db.prepare(`
|
|
77
|
+
INSERT OR IGNORE INTO file_touches (session_id, path, action, ts, uniq_key)
|
|
78
|
+
VALUES (@sessionId, @path, @action, @ts, @uniqKey)
|
|
79
|
+
`);
|
|
80
|
+
const insUsage = db.prepare(`
|
|
81
|
+
INSERT OR IGNORE INTO token_usage
|
|
82
|
+
(message_uuid, session_id, model, ts, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens)
|
|
83
|
+
VALUES
|
|
84
|
+
(@messageUuid, @sessionId, @model, @ts, @inputTokens, @outputTokens, @cacheReadTokens, @cacheCreationTokens)
|
|
85
|
+
`);
|
|
86
|
+
const insOutcome = db.prepare(`
|
|
87
|
+
INSERT OR IGNORE INTO tool_outcomes (session_id, path, tool_name, success, raw_response, ts, uniq_key)
|
|
88
|
+
VALUES (@sessionId, @path, @toolName, @success, @rawResponse, @ts, @uniqKey)
|
|
89
|
+
`);
|
|
90
|
+
const claimsSql = db.prepare(`
|
|
91
|
+
SELECT ft.path AS path,
|
|
92
|
+
MAX(CASE WHEN o.success = 1 THEN 1 ELSE 0 END) AS any_success,
|
|
93
|
+
MAX(CASE WHEN o.success = 0 THEN 1 ELSE 0 END) AS any_failure
|
|
94
|
+
FROM file_touches ft
|
|
95
|
+
LEFT JOIN tool_outcomes o
|
|
96
|
+
ON o.session_id = ft.session_id AND o.path = ft.path
|
|
97
|
+
WHERE ft.session_id = ? AND ft.action IN ('edit', 'write')
|
|
98
|
+
GROUP BY ft.path
|
|
99
|
+
ORDER BY ft.path
|
|
100
|
+
`);
|
|
101
|
+
const listSql = db.prepare(`${LIST_SQL} ORDER BY s.started_at DESC`);
|
|
102
|
+
const findSessionSql = db.prepare(`${LIST_SQL} WHERE s.id GLOB ? || '*' LIMIT 1`);
|
|
103
|
+
const eventsForSessionSql = db.prepare('SELECT * FROM events WHERE session_id = ? ORDER BY ts, id');
|
|
104
|
+
const hookEventCountSql = db.prepare("SELECT COUNT(*) AS n FROM events WHERE session_id = ? AND source = 'hook'");
|
|
105
|
+
const fileTouchCountSql = db.prepare('SELECT COUNT(DISTINCT path) AS n FROM file_touches WHERE session_id = ?');
|
|
106
|
+
const sessionTokensSql = db.prepare(`
|
|
107
|
+
SELECT COALESCE(SUM(input_tokens),0) AS input, COALESCE(SUM(output_tokens),0) AS output,
|
|
108
|
+
COALESCE(SUM(cache_read_tokens),0) AS cacheRead, COALESCE(SUM(cache_creation_tokens),0) AS cacheCreation
|
|
109
|
+
FROM token_usage WHERE session_id = ?
|
|
110
|
+
`);
|
|
111
|
+
const statsByDaySql = db.prepare(`
|
|
112
|
+
SELECT substr(ts, 1, 10) AS day, SUM(input_tokens + output_tokens) AS tokens
|
|
113
|
+
FROM token_usage GROUP BY day ORDER BY day DESC LIMIT 14
|
|
114
|
+
`);
|
|
115
|
+
const statsByProjectSql = db.prepare(`
|
|
116
|
+
SELECT COALESCE(s.project_dir, '(unknown)') AS project, SUM(t.input_tokens + t.output_tokens) AS tokens
|
|
117
|
+
FROM token_usage t LEFT JOIN sessions s ON s.id = t.session_id
|
|
118
|
+
GROUP BY project ORDER BY tokens DESC
|
|
119
|
+
`);
|
|
120
|
+
const rowToEvent = (r) => ({
|
|
121
|
+
sessionId: r.session_id,
|
|
122
|
+
ts: r.ts,
|
|
123
|
+
type: r.type,
|
|
124
|
+
toolName: r.tool_name,
|
|
125
|
+
detail: r.detail,
|
|
126
|
+
source: r.source,
|
|
127
|
+
sidechain: !!r.sidechain,
|
|
128
|
+
uniqKey: r.uniq_key,
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
upsertSession: (p) => upsert.run({
|
|
132
|
+
id: p.id,
|
|
133
|
+
projectDir: p.projectDir ?? null,
|
|
134
|
+
startedAt: p.startedAt ?? null,
|
|
135
|
+
endedAt: p.endedAt ?? null,
|
|
136
|
+
model: p.model ?? null,
|
|
137
|
+
}),
|
|
138
|
+
insertEvent: (e) => insEvent.run({ ...e, sidechain: e.sidechain ? 1 : 0 }),
|
|
139
|
+
insertFileTouch: (t) => insTouch.run(t),
|
|
140
|
+
insertTokenUsage: (u) => insUsage.run(u),
|
|
141
|
+
listSessions: () => listSql.all(),
|
|
142
|
+
findSession: (idPrefix) => findSessionSql.get(idPrefix),
|
|
143
|
+
eventsForSession: (sessionId) => eventsForSessionSql.all(sessionId).map(rowToEvent),
|
|
144
|
+
hookEventCount: (sessionId) => hookEventCountSql.get(sessionId).n,
|
|
145
|
+
fileTouchCount: (sessionId) => fileTouchCountSql.get(sessionId).n,
|
|
146
|
+
sessionTokens: (sessionId) => {
|
|
147
|
+
const r = sessionTokensSql.get(sessionId);
|
|
148
|
+
return { input: r.input, output: r.output, cacheRead: r.cacheRead, cacheCreation: r.cacheCreation };
|
|
149
|
+
},
|
|
150
|
+
statsByDay: () => statsByDaySql.all(),
|
|
151
|
+
statsByProject: () => statsByProjectSql.all(),
|
|
152
|
+
insertToolOutcome: (o) => insOutcome.run({ ...o, success: o.success === null ? null : o.success ? 1 : 0 }),
|
|
153
|
+
claimsForSession: (sessionId) => claimsSql.all(sessionId).map((r) => ({
|
|
154
|
+
path: r.path,
|
|
155
|
+
status: r.any_success ? 'succeeded' : r.any_failure ? 'failed' : 'attempted',
|
|
156
|
+
})),
|
|
157
|
+
close: () => db.close(),
|
|
158
|
+
};
|
|
159
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = '0.1.0';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg: #07090b;--bg-2: #0b0f12;--panel: #0e1317;--panel-2: #121a20;--line: rgba(120, 200, 220, .09);--line-strong: rgba(255, 168, 76, .22);--amber: #ff9a2e;--amber-soft: #ffb768;--amber-glow: rgba(255, 154, 46, .35);--cyan: #4fe0c4;--cyan-glow: rgba(79, 224, 196, .3);--red: #ff5d5d;--red-glow: rgba(255, 93, 93, .3);--violet: #a98bff;--text: #e7eef2;--muted: #8497a1;--faint: #56676f;--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--hud: "Chakra Petch", system-ui, sans-serif;--radius: 4px}*{box-sizing:border-box}html,body{margin:0;padding:0}body{background:var(--bg);color:var(--text);font-family:var(--hud);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased;min-height:100vh;position:relative;overflow-x:hidden}body:before{content:"";position:fixed;top:0;right:0;bottom:0;left:0;z-index:-2;background:radial-gradient(1200px 600px at 78% -10%,rgba(255,154,46,.12),transparent 60%),radial-gradient(900px 500px at 8% 0%,rgba(79,224,196,.06),transparent 55%),linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px),var(--bg);background-size:100% 100%,100% 100%,44px 44px,44px 44px,100% 100%;background-position:center}body:after{content:"";position:fixed;top:0;right:0;bottom:0;left:0;z-index:-1;pointer-events:none;background-image:repeating-linear-gradient(to bottom,#0000 0,#0000 2px,#00000029 3px);opacity:.4;mix-blend-mode:multiply}.cockpit-header{position:sticky;top:0;z-index:10;display:flex;align-items:center;gap:18px;padding:14px clamp(16px,4vw,42px);background:linear-gradient(180deg,#07090bf5,#07090bd1);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);border-bottom:1px solid var(--line-strong)}.brand{display:flex;align-items:baseline;gap:12px;text-decoration:none;color:var(--text)}.brand .mark{font-family:var(--hud);font-weight:700;font-size:21px;letter-spacing:.34em;text-transform:uppercase;color:var(--text);text-shadow:0 0 18px rgba(255,154,46,.18)}.brand .mark b{color:var(--amber);font-weight:700}.brand .sub{font-family:var(--mono);font-size:10px;letter-spacing:.28em;text-transform:uppercase;color:var(--faint)}.rec{margin-left:auto;display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--muted)}.rec .led{width:9px;height:9px;border-radius:50%;background:var(--red);box-shadow:0 0 10px 1px var(--red-glow);animation:blink 1.6s steps(1,end) infinite}@keyframes blink{0%,55%{opacity:1}56%,to{opacity:.25}}.shell{max-width:1120px;margin:0 auto;padding:clamp(20px,4vw,40px) clamp(16px,4vw,42px) 80px}.section-label{font-family:var(--mono);font-size:11px;letter-spacing:.26em;text-transform:uppercase;color:var(--faint);margin:0 0 14px;display:flex;align-items:center;gap:10px}.section-label:after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--line-strong),transparent)}.state{font-family:var(--mono);letter-spacing:.12em;color:var(--muted);padding:28px 4px}.state.error{color:var(--red)}.back{display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:12px;letter-spacing:.14em;color:var(--muted);text-decoration:none;margin-bottom:22px;transition:color .15s}.back:hover{color:var(--amber)}.flightlog{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:13px}.flightlog thead th{text-align:left;font-family:var(--hud);font-weight:600;font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--faint);padding:0 16px 12px;border-bottom:1px solid var(--line-strong)}.flightlog tbody tr{border-bottom:1px solid var(--line);transition:background .14s;animation:rise .5s cubic-bezier(.2,.7,.2,1) both;animation-delay:calc(var(--i, 0) * 34ms)}.flightlog tbody tr:hover{background:#ff9a2e0d}.flightlog tbody td{padding:13px 16px;color:var(--text);vertical-align:middle}.flightlog td.id a{color:var(--amber);text-decoration:none;letter-spacing:.08em;border-bottom:1px solid transparent;transition:border-color .14s,text-shadow .14s}.flightlog tr:hover td.id a{border-color:var(--amber);text-shadow:0 0 12px var(--amber-glow)}.flightlog td.project{color:var(--text);font-family:var(--hud);letter-spacing:.02em}.flightlog td.dim{color:var(--muted)}.flightlog td.num{color:var(--cyan);text-align:right}.flightlog td.badge{text-align:center;width:42px}.warn-led{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;color:var(--amber);background:#ff9a2e1f;box-shadow:0 0 12px var(--amber-glow),inset 0 0 0 1px var(--line-strong);font-size:12px}.summary-head{margin-bottom:26px}.summary-head .project-name{font-family:var(--hud);font-weight:600;font-size:clamp(22px,3.4vw,34px);letter-spacing:.01em;margin:4px 0 6px;color:var(--text);word-break:break-word}.summary-head .sid{font-family:var(--mono);font-size:12px;letter-spacing:.14em;color:var(--faint)}.gauges{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-bottom:34px}.gauge{position:relative;background:linear-gradient(180deg,var(--panel-2),var(--panel));border:1px solid var(--line);border-radius:var(--radius);padding:14px 15px 15px;overflow:hidden;animation:rise .55s cubic-bezier(.2,.7,.2,1) both;animation-delay:calc(var(--i, 0) * 55ms)}.gauge:before{content:"";position:absolute;top:0;left:0;width:26px;height:1px;background:var(--amber);box-shadow:0 0 8px var(--amber-glow)}.gauge .label{font-family:var(--hud);font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--faint);margin-bottom:9px}.gauge .value{font-family:var(--mono);font-weight:500;font-size:22px;letter-spacing:.01em;color:var(--text);line-height:1.15;word-break:break-word}.gauge .value.amber{color:var(--amber-soft)}.gauge .value.cyan{color:var(--cyan)}.gauge .value small{display:block;font-size:11px;color:var(--muted);letter-spacing:.06em;margin-top:4px}.panel{background:linear-gradient(180deg,#121a2080,#0e131780);border:1px solid var(--line);border-radius:var(--radius);padding:20px clamp(14px,2.5vw,24px);margin-bottom:22px}.controls{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:18px}.controls label{display:flex;flex-direction:column;gap:6px;font-family:var(--hud);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--faint)}.controls select,.controls input{font-family:var(--mono);font-size:13px;color:var(--text);background:var(--bg-2);border:1px solid var(--line-strong);border-radius:var(--radius);padding:9px 12px;min-width:190px;outline:none;transition:border-color .15s,box-shadow .15s}.controls select:focus,.controls input:focus{border-color:var(--amber);box-shadow:0 0 0 1px var(--amber),0 0 16px var(--amber-glow)}.controls input::placeholder{color:var(--faint)}.feed{list-style:none;margin:0;padding:0;position:relative}.feed:before{content:"";position:absolute;left:128px;top:4px;bottom:4px;width:1px;background:linear-gradient(180deg,transparent,var(--line-strong),transparent)}.event{display:grid;grid-template-columns:120px 1fr;gap:16px;align-items:baseline;padding:8px 0;animation:fade .4s ease both;animation-delay:calc(var(--i, 0) * 16ms)}.event time{font-family:var(--mono);font-size:12px;color:var(--faint);text-align:right;padding-right:4px}.event .body{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;position:relative}.event .body:before{content:"";position:absolute;left:-20px;top:8px;width:7px;height:7px;border-radius:50%;background:var(--faint);box-shadow:0 0 0 3px var(--bg)}.event.sidechain{padding-left:26px;opacity:.82}.event.sidechain .body:before{background:var(--violet);box-shadow:0 0 8px #a98bff66,0 0 0 3px var(--bg)}.chip{font-family:var(--mono);font-size:10px;font-weight:500;letter-spacing:.12em;text-transform:uppercase;padding:2px 8px;border-radius:3px;border:1px solid currentColor;white-space:nowrap}.chip.command{color:var(--amber)}.chip.file_touch{color:var(--cyan)}.chip.subagent_spawn{color:var(--violet)}.chip.tool_call{color:var(--muted)}.chip.session_start,.chip.session_end{color:var(--faint)}.event .tool{font-family:var(--mono);font-size:12px;color:var(--amber-soft);background:#ff9a2e14;padding:1px 6px;border-radius:3px}.event .detail{font-family:var(--mono);font-size:13px;color:var(--text);word-break:break-word}.feed-empty{font-family:var(--mono);color:var(--muted);padding:10px 0}.caution{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:12.5px;color:var(--amber-soft);background:repeating-linear-gradient(45deg,#ff9a2e17 0,#ff9a2e17 10px,#ff9a2e08 10px,#ff9a2e08 20px);border:1px solid var(--line-strong);border-left:3px solid var(--amber);border-radius:var(--radius);padding:11px 14px;margin-bottom:16px}.claims{list-style:none;margin:0;padding:0}.claim{display:flex;align-items:center;gap:14px;padding:10px 4px;border-bottom:1px solid var(--line)}.claim:last-child{border-bottom:none}.status-led{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;font-size:13px;flex-shrink:0;border:1px solid currentColor}.status-led.succeeded{color:var(--cyan);box-shadow:0 0 12px var(--cyan-glow)}.status-led.failed{color:var(--red);box-shadow:0 0 12px var(--red-glow)}.status-led.attempted{color:var(--amber);box-shadow:0 0 12px var(--amber-glow)}.claim .path{font-family:var(--mono);font-size:13px;color:var(--text);word-break:break-all}.claims-ok{font-family:var(--mono);font-size:13px;color:var(--cyan);letter-spacing:.04em;margin:14px 0 0}.claims-empty{font-family:var(--mono);color:var(--muted)}@keyframes rise{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}@keyframes fade{0%{opacity:0}to{opacity:1}}@media(max-width:620px){.feed:before{display:none}.event{grid-template-columns:1fr;gap:4px}.event time{text-align:left}.event .body:before{display:none}.flightlog td.project{max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
|