@davvidbaker/flambe-cli 0.1.0 → 0.1.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/README.md +61 -0
- package/package.json +17 -3
- package/src/agentNames.mjs +35 -0
- package/src/cli.mjs +34 -3
- package/src/client.mjs +152 -26
- package/src/local/agents.mjs +18 -12
- package/src/local/http.mjs +43 -13
- package/src/local/store.mjs +158 -21
- package/test/client.test.mjs +0 -951
- package/test/local.test.mjs +0 -136
- package/test/queue.test.mjs +0 -135
package/src/local/store.mjs
CHANGED
|
@@ -3,7 +3,10 @@ import { mkdirSync } from 'node:fs';
|
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { pickName, usableName } from './agents.mjs';
|
|
7
|
+
|
|
8
|
+
const LIFECYCLE_PHASES = ['B', 'E', 'S', 'R'];
|
|
9
|
+
const OPEN_PHASES = ['B', 'R'];
|
|
7
10
|
|
|
8
11
|
const SCHEMA = `
|
|
9
12
|
PRAGMA foreign_keys = ON;
|
|
@@ -112,6 +115,17 @@ CREATE TABLE IF NOT EXISTS search_terms (
|
|
|
112
115
|
timestamp_ms INTEGER NOT NULL
|
|
113
116
|
);
|
|
114
117
|
|
|
118
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
119
|
+
id INTEGER PRIMARY KEY,
|
|
120
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
121
|
+
agent_id TEXT NOT NULL,
|
|
122
|
+
name TEXT NOT NULL,
|
|
123
|
+
name_source TEXT NOT NULL,
|
|
124
|
+
platform TEXT,
|
|
125
|
+
last_seen_ms INTEGER NOT NULL,
|
|
126
|
+
UNIQUE (user_id, agent_id)
|
|
127
|
+
);
|
|
128
|
+
|
|
115
129
|
CREATE TABLE IF NOT EXISTS observations (
|
|
116
130
|
id INTEGER PRIMARY KEY,
|
|
117
131
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
@@ -127,6 +141,14 @@ CREATE TABLE IF NOT EXISTS observations (
|
|
|
127
141
|
export const EXPORT_FORMAT = 'flambe-local-export';
|
|
128
142
|
export const EXPORT_VERSION = 1;
|
|
129
143
|
|
|
144
|
+
export const DEFAULT_CATEGORIES = [
|
|
145
|
+
{ name: 'coding', color_background: '#efc360', color_text: '#000000' },
|
|
146
|
+
{ name: 'investigation', color_background: '#60a5fa', color_text: '#000000' },
|
|
147
|
+
{ name: 'review', color_background: '#a78bfa', color_text: '#ffffff' },
|
|
148
|
+
{ name: 'operations', color_background: '#34d399', color_text: '#000000' },
|
|
149
|
+
{ name: 'failure', color_background: '#fb7185', color_text: '#000000' },
|
|
150
|
+
];
|
|
151
|
+
|
|
130
152
|
export function hashToken(rawToken) {
|
|
131
153
|
return createHash('sha256').update(rawToken).digest('hex');
|
|
132
154
|
}
|
|
@@ -225,6 +247,7 @@ export class LocalStore {
|
|
|
225
247
|
this.db.prepare(
|
|
226
248
|
'INSERT INTO threads (trace_id, name, rank, export_id) VALUES (?, ?, 0, ?)',
|
|
227
249
|
).run(traceId, 'Main', randomUUID());
|
|
250
|
+
this.#ensureDefaultCategories(userId);
|
|
228
251
|
return this.getTrace(userId, traceId);
|
|
229
252
|
}
|
|
230
253
|
|
|
@@ -329,34 +352,122 @@ export class LocalStore {
|
|
|
329
352
|
return result.changes > 0;
|
|
330
353
|
}
|
|
331
354
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Resolve (and if needed name) the agent behind `agentId` for this user, mirroring
|
|
357
|
+
* FlambeNext.Agents.identify. `assigned` is true only when a name was coined now.
|
|
358
|
+
*/
|
|
359
|
+
identifyAgent(userId, agentId, providedName, providedPlatform) {
|
|
360
|
+
if (typeof agentId !== 'string' || agentId.length === 0 || agentId.length > 200) return null;
|
|
361
|
+
const provided = usableName(providedName) ? providedName.trim() : null;
|
|
362
|
+
const platformGiven = usableName(providedPlatform) ? providedPlatform.trim() : null;
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
const existing = this.db.prepare('SELECT * FROM agents WHERE user_id = ? AND agent_id = ?').get(userId, agentId);
|
|
365
|
+
|
|
366
|
+
if (existing) {
|
|
367
|
+
const name = provided && provided !== existing.name ? provided : existing.name;
|
|
368
|
+
const platform = platformGiven ?? existing.platform ?? null;
|
|
369
|
+
this.db.prepare('UPDATE agents SET name = ?, name_source = ?, platform = ?, last_seen_ms = ? WHERE id = ?')
|
|
370
|
+
.run(name, provided && provided !== existing.name ? 'provided' : existing.name_source, platform, now, existing.id);
|
|
371
|
+
return { agent_id: agentId, name, platform, assigned: false };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const taken = this.db.prepare('SELECT name FROM agents WHERE user_id = ?').all(userId).map(row => row.name);
|
|
375
|
+
const name = provided ?? pickName(agentId, taken);
|
|
376
|
+
this.db.prepare('INSERT INTO agents (user_id, agent_id, name, name_source, platform, last_seen_ms) VALUES (?, ?, ?, ?, ?, ?)')
|
|
377
|
+
.run(userId, agentId, name, provided ? 'provided' : 'assigned', platformGiven, now);
|
|
378
|
+
return { agent_id: agentId, name, platform: platformGiven, assigned: provided === null };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
listAgents(userId) {
|
|
382
|
+
return this.db.prepare('SELECT agent_id, name, name_source, platform, last_seen_ms FROM agents WHERE user_id = ? ORDER BY last_seen_ms DESC, id')
|
|
383
|
+
.all(userId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Newest activity in the trace whose latest lifecycle event is B/R, optionally for one agent or thread. */
|
|
387
|
+
newestActiveActivity(traceId, { agentId, threadId } = {}) {
|
|
388
|
+
const placeholders = LIFECYCLE_PHASES.map(() => '?').join(',');
|
|
389
|
+
const open = OPEN_PHASES.map(() => '?').join(',');
|
|
390
|
+
const rows = this.db.prepare(`
|
|
391
|
+
WITH latest AS (
|
|
392
|
+
SELECT activity_id, phase, timestamp_ms, id,
|
|
393
|
+
ROW_NUMBER() OVER (PARTITION BY activity_id ORDER BY timestamp_ms DESC, id DESC) AS rn
|
|
394
|
+
FROM events
|
|
395
|
+
WHERE trace_id = ? AND activity_id IS NOT NULL AND phase IN (${placeholders})
|
|
396
|
+
)
|
|
397
|
+
SELECT activities.id, activities.parent_id, activities.thread_id, activities.agent_id
|
|
398
|
+
FROM latest
|
|
399
|
+
JOIN activities ON activities.id = latest.activity_id
|
|
400
|
+
WHERE latest.rn = 1 AND latest.phase IN (${open})
|
|
401
|
+
${agentId ? 'AND activities.agent_id = ?' : ''}
|
|
402
|
+
${threadId ? 'AND activities.thread_id = ?' : ''}
|
|
403
|
+
ORDER BY latest.timestamp_ms DESC, latest.id DESC
|
|
404
|
+
LIMIT 1
|
|
405
|
+
`).all(
|
|
406
|
+
Number(traceId), ...LIFECYCLE_PHASES, ...OPEN_PHASES,
|
|
407
|
+
...(agentId ? [agentId] : []), ...(threadId ? [Number(threadId)] : []),
|
|
408
|
+
);
|
|
409
|
+
return rows[0] ?? null;
|
|
410
|
+
}
|
|
340
411
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
412
|
+
/**
|
|
413
|
+
* Create an activity. With `reduce` (agent requests, ADR-012) the deterministic reducer
|
|
414
|
+
* rules apply: an absent parent_id infers this agent's newest active activity, a child
|
|
415
|
+
* lives in its parent's thread and inherits categories, and thread_id may be omitted.
|
|
416
|
+
*/
|
|
417
|
+
createActivity(userId, { traceId, threadId, activity, event, agent, reduce = false }) {
|
|
418
|
+
const trace = this.db.prepare('SELECT id FROM traces WHERE id = ? AND user_id = ?').get(Number(traceId), userId);
|
|
419
|
+
if (!trace) return { error: 'not_found' };
|
|
420
|
+
|
|
421
|
+
const notes = { parent_source: 'root', thread_source: 'request', categories_source: 'none' };
|
|
422
|
+
let parentRow = null;
|
|
423
|
+
let parentId = null;
|
|
424
|
+
|
|
425
|
+
if (Object.hasOwn(activity, 'parent_id') && activity.parent_id != null) {
|
|
426
|
+
parentRow = this.db.prepare(`
|
|
427
|
+
SELECT activities.id, activities.thread_id FROM activities
|
|
428
|
+
JOIN threads ON threads.id = activities.thread_id
|
|
429
|
+
WHERE activities.id = ? AND threads.trace_id = ?
|
|
430
|
+
`).get(Number(activity.parent_id), trace.id);
|
|
431
|
+
if (!parentRow) return { error: 'not_found' };
|
|
432
|
+
notes.parent_source = 'explicit';
|
|
433
|
+
} else if (reduce && !Object.hasOwn(activity, 'parent_id')) {
|
|
434
|
+
const requestedThread = threadId == null ? this.#defaultThread(trace.id)?.id : Number(threadId);
|
|
435
|
+
parentRow = agent?.agent_id
|
|
436
|
+
? this.newestActiveActivity(trace.id, { agentId: agent.agent_id })
|
|
437
|
+
: this.newestActiveActivity(trace.id, { threadId: requestedThread });
|
|
438
|
+
if (parentRow) notes.parent_source = 'inferred';
|
|
348
439
|
}
|
|
349
440
|
|
|
350
|
-
|
|
441
|
+
let thread;
|
|
442
|
+
if (parentRow) {
|
|
443
|
+
thread = this.db.prepare('SELECT id, trace_id FROM threads WHERE id = ?').get(parentRow.thread_id);
|
|
444
|
+
notes.thread_source = 'parent';
|
|
445
|
+
parentId = parentRow.id;
|
|
446
|
+
} else if (threadId == null) {
|
|
447
|
+
if (!reduce) return { error: 'not_found' };
|
|
448
|
+
thread = this.#defaultThread(trace.id);
|
|
449
|
+
notes.thread_source = 'default';
|
|
450
|
+
} else {
|
|
451
|
+
thread = this.db.prepare('SELECT id, trace_id FROM threads WHERE id = ? AND trace_id = ?').get(Number(threadId), trace.id);
|
|
452
|
+
}
|
|
453
|
+
if (!thread) return { error: 'not_found' };
|
|
454
|
+
if (!reduce && parentRow && parentRow.thread_id !== thread.id) return { error: 'not_found' };
|
|
455
|
+
|
|
456
|
+
let categoryIds = Array.isArray(activity.categories) ? activity.categories.map(Number) : [];
|
|
351
457
|
if (categoryIds.length > 0) {
|
|
352
458
|
const found = this.db.prepare(
|
|
353
459
|
`SELECT id FROM categories WHERE user_id = ? AND id IN (${categoryIds.map(() => '?').join(',')})`,
|
|
354
460
|
).all(userId, ...categoryIds);
|
|
355
461
|
if (found.length !== new Set(categoryIds).size) return { error: 'not_found' };
|
|
462
|
+
notes.categories_source = 'request';
|
|
463
|
+
} else if (reduce && parentId != null) {
|
|
464
|
+
categoryIds = this.db.prepare('SELECT category_id FROM activities_categories WHERE activity_id = ?')
|
|
465
|
+
.all(parentId).map(row => row.category_id);
|
|
466
|
+
if (categoryIds.length > 0) notes.categories_source = 'parent';
|
|
356
467
|
}
|
|
357
468
|
|
|
358
|
-
const
|
|
359
|
-
? { agent_id:
|
|
469
|
+
const agentColumns = agent?.agent_id
|
|
470
|
+
? { agent_id: agent.agent_id, agent_name: agent.name }
|
|
360
471
|
: { agent_id: null, agent_name: null };
|
|
361
472
|
|
|
362
473
|
const inserted = this.db.prepare(`
|
|
@@ -368,8 +479,8 @@ export class LocalStore {
|
|
|
368
479
|
activity.name,
|
|
369
480
|
activity.description ?? null,
|
|
370
481
|
activity.weight ?? null,
|
|
371
|
-
|
|
372
|
-
|
|
482
|
+
agentColumns.agent_id,
|
|
483
|
+
agentColumns.agent_name,
|
|
373
484
|
randomUUID(),
|
|
374
485
|
);
|
|
375
486
|
const activityId = Number(inserted.lastInsertRowid);
|
|
@@ -395,9 +506,14 @@ export class LocalStore {
|
|
|
395
506
|
thread_id: thread.id,
|
|
396
507
|
},
|
|
397
508
|
event: { id: Number(eventRow.lastInsertRowid), phase: event.phase },
|
|
509
|
+
...(reduce ? { reducer: notes } : {}),
|
|
398
510
|
};
|
|
399
511
|
}
|
|
400
512
|
|
|
513
|
+
#defaultThread(traceId) {
|
|
514
|
+
return this.db.prepare('SELECT id, trace_id FROM threads WHERE trace_id = ? ORDER BY rank, id LIMIT 1').get(traceId) ?? null;
|
|
515
|
+
}
|
|
516
|
+
|
|
401
517
|
getActivity(userId, id) {
|
|
402
518
|
const row = this.db.prepare(`
|
|
403
519
|
SELECT activities.*
|
|
@@ -713,6 +829,26 @@ export class LocalStore {
|
|
|
713
829
|
};
|
|
714
830
|
}
|
|
715
831
|
|
|
832
|
+
#ensureDefaultCategories(userId) {
|
|
833
|
+
const existing = this.db.prepare(
|
|
834
|
+
'SELECT COUNT(*) AS count FROM categories WHERE user_id = ?',
|
|
835
|
+
).get(userId);
|
|
836
|
+
if (existing.count > 0) return;
|
|
837
|
+
|
|
838
|
+
const insert = this.db.prepare(
|
|
839
|
+
'INSERT INTO categories (user_id, name, color_background, color_text, export_id) VALUES (?, ?, ?, ?, ?)',
|
|
840
|
+
);
|
|
841
|
+
for (const category of DEFAULT_CATEGORIES) {
|
|
842
|
+
insert.run(
|
|
843
|
+
userId,
|
|
844
|
+
category.name,
|
|
845
|
+
category.color_background,
|
|
846
|
+
category.color_text,
|
|
847
|
+
randomUUID(),
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
716
852
|
#seed() {
|
|
717
853
|
const user = this.db.prepare('SELECT id FROM users WHERE id = 1').get();
|
|
718
854
|
if (user) return;
|
|
@@ -730,6 +866,7 @@ export class LocalStore {
|
|
|
730
866
|
this.db.prepare(
|
|
731
867
|
'INSERT INTO threads (trace_id, name, rank, export_id) VALUES (?, ?, 0, ?)',
|
|
732
868
|
).run(Number(trace.lastInsertRowid), 'Main', randomUUID());
|
|
869
|
+
this.#ensureDefaultCategories(1);
|
|
733
870
|
this.db.exec('COMMIT');
|
|
734
871
|
} catch (error) {
|
|
735
872
|
this.db.exec('ROLLBACK');
|