@davvidbaker/flambe-cli 0.1.1 → 0.1.3
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 +4 -2
- package/package.json +1 -1
- package/src/cli.mjs +90 -7
- package/src/client.mjs +209 -17
- package/src/local/commands.mjs +291 -0
- package/src/local/http.mjs +89 -11
- package/src/local/serve.mjs +2 -1
- package/src/local/store.mjs +169 -55
- package/src/questions.mjs +48 -0
- package/src/queue.mjs +15 -3
- package/src/runtime.mjs +23 -0
package/src/local/store.mjs
CHANGED
|
@@ -5,8 +5,8 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
5
5
|
|
|
6
6
|
import { pickName, usableName } from './agents.mjs';
|
|
7
7
|
|
|
8
|
-
const LIFECYCLE_PHASES = ['B', 'E', 'S', 'R'];
|
|
9
|
-
const OPEN_PHASES = ['B', 'R'];
|
|
8
|
+
const LIFECYCLE_PHASES = ['B', 'E', 'S', 'R', 'X', 'J', 'V'];
|
|
9
|
+
const OPEN_PHASES = ['B', 'R', 'X'];
|
|
10
10
|
|
|
11
11
|
const SCHEMA = `
|
|
12
12
|
PRAGMA foreign_keys = ON;
|
|
@@ -60,6 +60,10 @@ CREATE TABLE IF NOT EXISTS activities (
|
|
|
60
60
|
weight INTEGER,
|
|
61
61
|
agent_id TEXT,
|
|
62
62
|
agent_name TEXT,
|
|
63
|
+
scheduled_start_ms INTEGER,
|
|
64
|
+
scheduled_end_ms INTEGER,
|
|
65
|
+
proposed_by_agent_id TEXT,
|
|
66
|
+
proposed_by_agent_name TEXT,
|
|
63
67
|
export_id TEXT NOT NULL UNIQUE
|
|
64
68
|
);
|
|
65
69
|
|
|
@@ -79,13 +83,6 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
79
83
|
export_id TEXT NOT NULL UNIQUE
|
|
80
84
|
);
|
|
81
85
|
|
|
82
|
-
CREATE TABLE IF NOT EXISTS todos (
|
|
83
|
-
id INTEGER PRIMARY KEY,
|
|
84
|
-
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
85
|
-
name TEXT NOT NULL,
|
|
86
|
-
description TEXT
|
|
87
|
-
);
|
|
88
|
-
|
|
89
86
|
CREATE TABLE IF NOT EXISTS mantras (
|
|
90
87
|
id INTEGER PRIMARY KEY,
|
|
91
88
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
@@ -142,11 +139,25 @@ export const EXPORT_FORMAT = 'flambe-local-export';
|
|
|
142
139
|
export const EXPORT_VERSION = 1;
|
|
143
140
|
|
|
144
141
|
export const DEFAULT_CATEGORIES = [
|
|
145
|
-
{ name: '
|
|
146
|
-
{ name: '
|
|
147
|
-
{ name: '
|
|
148
|
-
{ name: '
|
|
149
|
-
{ name: '
|
|
142
|
+
{ name: 'bug fixing', color_background: '#ff4747', color_text: '#000000' },
|
|
143
|
+
{ name: 'research', color_background: '#cd94f1', color_text: '#000000' },
|
|
144
|
+
{ name: 'cleaning', color_background: '#ffbd69', color_text: '#000000' },
|
|
145
|
+
{ name: 'bug hunting', color_background: '#ef60ab', color_text: '#000000' },
|
|
146
|
+
{ name: 'design', color_background: '#dce7ea', color_text: '#000000' },
|
|
147
|
+
{ name: 'writing tests', color_background: '#50f035', color_text: '#000000' },
|
|
148
|
+
{ name: 'toil', color_background: '#8b6125', color_text: '#ffffff' },
|
|
149
|
+
{ name: 'analytics', color_background: '#f9f9f9', color_text: '#4598d6' },
|
|
150
|
+
{ name: 'oss', color_background: '#60ef6c', color_text: '#000000' },
|
|
151
|
+
{ name: 'enhancements', color_background: '#55defb', color_text: '#ffffff' },
|
|
152
|
+
{ name: 'refactoring components', color_background: '#ffd368', color_text: '#000000' },
|
|
153
|
+
{ name: 'Fundamentals', color_background: '#acefdb', color_text: '#000000' },
|
|
154
|
+
{ name: 'jarring ui', color_background: '#f22d6a', color_text: '#ffffff' },
|
|
155
|
+
{ name: 'writing', color_background: '#ffefce', color_text: '#000000' },
|
|
156
|
+
{ name: 'risky', color_background: '#ff2a22', color_text: '#ffffff' },
|
|
157
|
+
{ name: 'dependency upgrades', color_background: '#b58e38', color_text: '#ffffff' },
|
|
158
|
+
{ name: 'shiny', color_background: '#f5ff86', color_text: '#000000' },
|
|
159
|
+
{ name: 'performance', color_background: '#60efc7', color_text: '#000000' },
|
|
160
|
+
{ name: 'learning', color_background: '#d3f791', color_text: '#000000' },
|
|
150
161
|
];
|
|
151
162
|
|
|
152
163
|
export function hashToken(rawToken) {
|
|
@@ -169,6 +180,17 @@ export class LocalStore {
|
|
|
169
180
|
this.db = new DatabaseSync(dbPath);
|
|
170
181
|
this.db.exec('PRAGMA foreign_keys = ON');
|
|
171
182
|
this.db.exec(SCHEMA);
|
|
183
|
+
this.db.exec('DROP TABLE IF EXISTS todos');
|
|
184
|
+
for (const column of [
|
|
185
|
+
'scheduled_start_ms INTEGER',
|
|
186
|
+
'scheduled_end_ms INTEGER',
|
|
187
|
+
'proposed_by_agent_id TEXT',
|
|
188
|
+
'proposed_by_agent_name TEXT',
|
|
189
|
+
]) {
|
|
190
|
+
const name = column.split(' ')[0];
|
|
191
|
+
const exists = this.db.prepare('PRAGMA table_info(activities)').all().some(row => row.name === name);
|
|
192
|
+
if (!exists) this.db.exec(`ALTER TABLE activities ADD COLUMN ${column}`);
|
|
193
|
+
}
|
|
172
194
|
this.#seed();
|
|
173
195
|
}
|
|
174
196
|
|
|
@@ -224,7 +246,9 @@ export class LocalStore {
|
|
|
224
246
|
const events = this.db.prepare(`
|
|
225
247
|
SELECT events.*, activities.name AS activity_name, activities.parent_id,
|
|
226
248
|
activities.thread_id, activities.weight, activities.agent_id, activities.agent_name,
|
|
227
|
-
activities.description AS activity_description
|
|
249
|
+
activities.description AS activity_description,
|
|
250
|
+
activities.scheduled_start_ms, activities.scheduled_end_ms,
|
|
251
|
+
activities.proposed_by_agent_id, activities.proposed_by_agent_name
|
|
228
252
|
FROM events
|
|
229
253
|
LEFT JOIN activities ON activities.id = events.activity_id
|
|
230
254
|
WHERE events.trace_id = ?
|
|
@@ -236,6 +260,7 @@ export class LocalStore {
|
|
|
236
260
|
name: trace.name,
|
|
237
261
|
threads: threads.map(thread => ({ id: thread.id, name: thread.name, rank: thread.rank })),
|
|
238
262
|
events: events.map(event => this.#eventPayload(event)),
|
|
263
|
+
unstarted: this.listUnstarted(trace.id).map(activity => this.#traceActivityPayload(activity)),
|
|
239
264
|
};
|
|
240
265
|
}
|
|
241
266
|
|
|
@@ -317,6 +342,7 @@ export class LocalStore {
|
|
|
317
342
|
}
|
|
318
343
|
|
|
319
344
|
listCategories(userId) {
|
|
345
|
+
this.#ensureDefaultCategories(userId);
|
|
320
346
|
return this.db.prepare(
|
|
321
347
|
'SELECT id, name, color_background, color_text FROM categories WHERE user_id = ? ORDER BY id',
|
|
322
348
|
).all(userId);
|
|
@@ -383,7 +409,7 @@ export class LocalStore {
|
|
|
383
409
|
.all(userId);
|
|
384
410
|
}
|
|
385
411
|
|
|
386
|
-
/** Newest activity in the trace
|
|
412
|
+
/** Newest running activity in the trace, optionally for one agent or thread. */
|
|
387
413
|
newestActiveActivity(traceId, { agentId, threadId } = {}) {
|
|
388
414
|
const placeholders = LIFECYCLE_PHASES.map(() => '?').join(',');
|
|
389
415
|
const open = OPEN_PHASES.map(() => '?').join(',');
|
|
@@ -414,7 +440,19 @@ export class LocalStore {
|
|
|
414
440
|
* rules apply: an absent parent_id infers this agent's newest active activity, a child
|
|
415
441
|
* lives in its parent's thread and inherits categories, and thread_id may be omitted.
|
|
416
442
|
*/
|
|
417
|
-
createActivity(userId,
|
|
443
|
+
createActivity(userId, options) {
|
|
444
|
+
this.db.exec('SAVEPOINT create_activity');
|
|
445
|
+
try {
|
|
446
|
+
const result = this.#createActivity(userId, options);
|
|
447
|
+
this.db.exec('RELEASE create_activity');
|
|
448
|
+
return result;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
this.db.exec('ROLLBACK TO create_activity; RELEASE create_activity');
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#createActivity(userId, { traceId, threadId, activity, event, agent, reduce = false }) {
|
|
418
456
|
const trace = this.db.prepare('SELECT id FROM traces WHERE id = ? AND user_id = ?').get(Number(traceId), userId);
|
|
419
457
|
if (!trace) return { error: 'not_found' };
|
|
420
458
|
|
|
@@ -471,8 +509,11 @@ export class LocalStore {
|
|
|
471
509
|
: { agent_id: null, agent_name: null };
|
|
472
510
|
|
|
473
511
|
const inserted = this.db.prepare(`
|
|
474
|
-
INSERT INTO activities (
|
|
475
|
-
|
|
512
|
+
INSERT INTO activities (
|
|
513
|
+
thread_id, parent_id, name, description, weight, agent_id, agent_name,
|
|
514
|
+
scheduled_start_ms, scheduled_end_ms, proposed_by_agent_id, proposed_by_agent_name, export_id
|
|
515
|
+
)
|
|
516
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
476
517
|
`).run(
|
|
477
518
|
thread.id,
|
|
478
519
|
parentId,
|
|
@@ -481,6 +522,10 @@ export class LocalStore {
|
|
|
481
522
|
activity.weight ?? null,
|
|
482
523
|
agentColumns.agent_id,
|
|
483
524
|
agentColumns.agent_name,
|
|
525
|
+
activity.scheduled_start_integer ?? activity.scheduled_start_ms ?? null,
|
|
526
|
+
activity.scheduled_end_integer ?? activity.scheduled_end_ms ?? null,
|
|
527
|
+
activity.proposed_by_agent_id ?? agentColumns.agent_id,
|
|
528
|
+
activity.proposed_by_agent_name ?? agentColumns.agent_name,
|
|
484
529
|
randomUUID(),
|
|
485
530
|
);
|
|
486
531
|
const activityId = Number(inserted.lastInsertRowid);
|
|
@@ -490,11 +535,15 @@ export class LocalStore {
|
|
|
490
535
|
).run(activityId, categoryId);
|
|
491
536
|
}
|
|
492
537
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
538
|
+
let createdEvent = null;
|
|
539
|
+
if (event) {
|
|
540
|
+
const timestamp = Number(event.timestamp_integer);
|
|
541
|
+
const eventRow = this.db.prepare(`
|
|
542
|
+
INSERT INTO events (trace_id, activity_id, timestamp_ms, phase, message, export_id)
|
|
543
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
544
|
+
`).run(thread.trace_id, activityId, timestamp, event.phase, event.message ?? null, randomUUID());
|
|
545
|
+
createdEvent = { id: Number(eventRow.lastInsertRowid), phase: event.phase };
|
|
546
|
+
}
|
|
498
547
|
|
|
499
548
|
return {
|
|
500
549
|
activity: {
|
|
@@ -505,11 +554,43 @@ export class LocalStore {
|
|
|
505
554
|
parent_id: parentId,
|
|
506
555
|
thread_id: thread.id,
|
|
507
556
|
},
|
|
508
|
-
event:
|
|
557
|
+
event: createdEvent,
|
|
509
558
|
...(reduce ? { reducer: notes } : {}),
|
|
510
559
|
};
|
|
511
560
|
}
|
|
512
561
|
|
|
562
|
+
createUnstarted(userId, { traceId, name, description, weight, agent, scheduledStart, scheduledEnd }) {
|
|
563
|
+
const thread = this.#defaultThread(Number(traceId));
|
|
564
|
+
if (!thread) return { error: 'not_found' };
|
|
565
|
+
const trace = this.db.prepare('SELECT id FROM traces WHERE id = ? AND user_id = ?').get(thread.trace_id, userId);
|
|
566
|
+
if (!trace) return { error: 'not_found' };
|
|
567
|
+
const inserted = this.db.prepare(`
|
|
568
|
+
INSERT INTO activities (
|
|
569
|
+
thread_id, parent_id, name, description, weight, agent_id, agent_name,
|
|
570
|
+
scheduled_start_ms, scheduled_end_ms, proposed_by_agent_id, proposed_by_agent_name, export_id
|
|
571
|
+
) VALUES (?, NULL, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?)
|
|
572
|
+
`).run(
|
|
573
|
+
thread.id, name, description ?? null, weight ?? null,
|
|
574
|
+
scheduledStart ?? null, scheduledEnd ?? null,
|
|
575
|
+
agent?.agent_id ?? null, agent?.name ?? null, randomUUID(),
|
|
576
|
+
);
|
|
577
|
+
return { activity: { id: Number(inserted.lastInsertRowid), name, thread_id: thread.id, parent_id: null } };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
listUnstarted(traceId) {
|
|
581
|
+
return this.db.prepare(`
|
|
582
|
+
SELECT activities.* FROM activities
|
|
583
|
+
JOIN threads ON threads.id = activities.thread_id
|
|
584
|
+
WHERE threads.trace_id = ?
|
|
585
|
+
AND NOT EXISTS (
|
|
586
|
+
SELECT 1 FROM events
|
|
587
|
+
WHERE events.activity_id = activities.id
|
|
588
|
+
AND events.phase IN ('B','Q','R','X','S','E','J','V')
|
|
589
|
+
)
|
|
590
|
+
ORDER BY activities.id
|
|
591
|
+
`).all(Number(traceId));
|
|
592
|
+
}
|
|
593
|
+
|
|
513
594
|
#defaultThread(traceId) {
|
|
514
595
|
return this.db.prepare('SELECT id, trace_id FROM threads WHERE trace_id = ? ORDER BY rank, id LIMIT 1').get(traceId) ?? null;
|
|
515
596
|
}
|
|
@@ -530,6 +611,8 @@ export class LocalStore {
|
|
|
530
611
|
weight: row.weight,
|
|
531
612
|
parent_id: row.parent_id,
|
|
532
613
|
thread_id: row.thread_id,
|
|
614
|
+
agent_id: row.agent_id ?? null,
|
|
615
|
+
agent_name: row.agent_name ?? null,
|
|
533
616
|
};
|
|
534
617
|
}
|
|
535
618
|
|
|
@@ -545,6 +628,13 @@ export class LocalStore {
|
|
|
545
628
|
WHERE id = ?
|
|
546
629
|
`).run(attrs.name ?? null, attrs.description ?? null, attrs.weight ?? null, attrs.parent_id ?? null, current.id);
|
|
547
630
|
|
|
631
|
+
if (Object.prototype.hasOwnProperty.call(attrs, 'agent_id')) {
|
|
632
|
+
const agentId = attrs.agent_id == null || attrs.agent_id === '' ? null : String(attrs.agent_id);
|
|
633
|
+
const agentName = agentId == null ? null : this.#activityAgentName(userId, agentId, attrs);
|
|
634
|
+
this.db.prepare('UPDATE activities SET agent_id = ?, agent_name = ? WHERE id = ?')
|
|
635
|
+
.run(agentId, agentName, current.id);
|
|
636
|
+
}
|
|
637
|
+
|
|
548
638
|
if (Array.isArray(attrs.category_ids)) {
|
|
549
639
|
this.db.prepare('DELETE FROM activities_categories WHERE activity_id = ?').run(current.id);
|
|
550
640
|
for (const categoryId of attrs.category_ids) {
|
|
@@ -628,7 +718,6 @@ export class LocalStore {
|
|
|
628
718
|
username: user.username,
|
|
629
719
|
traces: this.listTraces(userId),
|
|
630
720
|
categories: this.listCategories(userId),
|
|
631
|
-
todos: this.db.prepare('SELECT id, name, description FROM todos WHERE user_id = ?').all(userId),
|
|
632
721
|
mantras: this.db.prepare('SELECT id, name, timestamp_ms FROM mantras WHERE user_id = ? ORDER BY id').all(userId)
|
|
633
722
|
.map(row => ({ id: row.id, name: row.name, timestamp: isoFromMs(row.timestamp_ms) })),
|
|
634
723
|
attentionShifts: this.db.prepare(
|
|
@@ -654,17 +743,14 @@ export class LocalStore {
|
|
|
654
743
|
timestamp: isoFromMs(row.timestamp_ms),
|
|
655
744
|
})),
|
|
656
745
|
observations: this.listObservations(userId),
|
|
746
|
+
agents: this.listAgents(userId).map(agent => ({
|
|
747
|
+
agent_id: agent.agent_id,
|
|
748
|
+
name: agent.name,
|
|
749
|
+
platform: agent.platform ?? null,
|
|
750
|
+
})),
|
|
657
751
|
};
|
|
658
752
|
}
|
|
659
753
|
|
|
660
|
-
createTodo(userId, attrs) {
|
|
661
|
-
const result = this.db.prepare(
|
|
662
|
-
'INSERT INTO todos (user_id, name, description) VALUES (?, ?, ?)',
|
|
663
|
-
).run(userId, attrs.name, attrs.description ?? null);
|
|
664
|
-
return this.db.prepare('SELECT id, name, description FROM todos WHERE id = ?')
|
|
665
|
-
.get(Number(result.lastInsertRowid));
|
|
666
|
-
}
|
|
667
|
-
|
|
668
754
|
createMantra(userId, attrs) {
|
|
669
755
|
const timestamp = Number(attrs.timestamp_integer ?? Date.now());
|
|
670
756
|
const result = this.db.prepare(
|
|
@@ -792,28 +878,55 @@ export class LocalStore {
|
|
|
792
878
|
};
|
|
793
879
|
}
|
|
794
880
|
|
|
795
|
-
#
|
|
796
|
-
const
|
|
797
|
-
?
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
881
|
+
#activityAgentName(userId, agentId, attrs) {
|
|
882
|
+
const known = this.db.prepare(
|
|
883
|
+
'SELECT name FROM agents WHERE user_id = ? AND agent_id = ?',
|
|
884
|
+
).get(userId, agentId);
|
|
885
|
+
if (known?.name) return known.name;
|
|
886
|
+
const snapshot = this.db.prepare(`
|
|
887
|
+
SELECT activities.agent_name FROM activities
|
|
888
|
+
JOIN threads ON threads.id = activities.thread_id
|
|
889
|
+
JOIN traces ON traces.id = threads.trace_id
|
|
890
|
+
WHERE traces.user_id = ? AND activities.agent_id = ?
|
|
891
|
+
AND activities.agent_name IS NOT NULL AND activities.agent_name != ''
|
|
892
|
+
LIMIT 1
|
|
893
|
+
`).get(userId, agentId);
|
|
894
|
+
if (snapshot?.agent_name) return snapshot.agent_name;
|
|
895
|
+
if (typeof attrs.agent_name === 'string' && attrs.agent_name !== '') return attrs.agent_name;
|
|
896
|
+
return agentId;
|
|
897
|
+
}
|
|
801
898
|
|
|
899
|
+
#eventPayload(event) {
|
|
802
900
|
return {
|
|
803
901
|
id: event.id,
|
|
804
902
|
timestamp: isoFromMs(event.timestamp_ms),
|
|
805
903
|
phase: event.phase,
|
|
806
904
|
message: event.message ?? null,
|
|
807
|
-
activity: event.activity_id == null ? null : {
|
|
808
|
-
id: event.activity_id,
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
905
|
+
activity: event.activity_id == null ? null : this.#traceActivityPayload({
|
|
906
|
+
...event, id: event.activity_id, name: event.activity_name,
|
|
907
|
+
description: event.activity_description,
|
|
908
|
+
}),
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
#traceActivityPayload(activity) {
|
|
913
|
+
const categoryIds = this.db.prepare(
|
|
914
|
+
'SELECT category_id FROM activities_categories WHERE activity_id = ?',
|
|
915
|
+
).all(activity.id).map(row => row.category_id);
|
|
916
|
+
return {
|
|
917
|
+
id: activity.id,
|
|
918
|
+
name: activity.name,
|
|
919
|
+
agent_id: activity.agent_id ?? null,
|
|
920
|
+
agent_name: activity.agent_name ?? null,
|
|
921
|
+
parent_id: activity.parent_id ?? null,
|
|
922
|
+
thread: { id: activity.thread_id },
|
|
923
|
+
categories: categoryIds,
|
|
924
|
+
weight: activity.weight ?? null,
|
|
925
|
+
description: activity.description ?? null,
|
|
926
|
+
scheduled_start: activity.scheduled_start_ms ?? null,
|
|
927
|
+
scheduled_end: activity.scheduled_end_ms ?? null,
|
|
928
|
+
proposed_by_agent_id: activity.proposed_by_agent_id ?? null,
|
|
929
|
+
proposed_by_agent_name: activity.proposed_by_agent_name ?? null,
|
|
817
930
|
};
|
|
818
931
|
}
|
|
819
932
|
|
|
@@ -830,15 +943,16 @@ export class LocalStore {
|
|
|
830
943
|
}
|
|
831
944
|
|
|
832
945
|
#ensureDefaultCategories(userId) {
|
|
833
|
-
const
|
|
834
|
-
'SELECT
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
946
|
+
const have = new Set(
|
|
947
|
+
this.db.prepare('SELECT lower(name) AS name FROM categories WHERE user_id = ?')
|
|
948
|
+
.all(userId)
|
|
949
|
+
.map(row => row.name),
|
|
950
|
+
);
|
|
838
951
|
const insert = this.db.prepare(
|
|
839
952
|
'INSERT INTO categories (user_id, name, color_background, color_text, export_id) VALUES (?, ?, ?, ?, ?)',
|
|
840
953
|
);
|
|
841
954
|
for (const category of DEFAULT_CATEGORIES) {
|
|
955
|
+
if (have.has(category.name.toLowerCase())) continue;
|
|
842
956
|
insert.run(
|
|
843
957
|
userId,
|
|
844
958
|
category.name,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
// A question the reducer asked and the worker has not answered yet (ADR-014).
|
|
7
|
+
// One open question per trace. The CLI reminds; it never refuses the next command.
|
|
8
|
+
|
|
9
|
+
export function defaultQuestionsPath() {
|
|
10
|
+
return join(homedir(), '.flambe', 'open-questions.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readOpenQuestion(traceId, path = defaultQuestionsPath()) {
|
|
14
|
+
if (!traceId) return undefined;
|
|
15
|
+
const stored = readAll(path)[String(traceId)];
|
|
16
|
+
if (!stored || typeof stored.reply !== 'string' || !stored.reply.trim()) return undefined;
|
|
17
|
+
return { activityId: stored.activityId, reply: stored.reply };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rememberQuestion(traceId, { activityId, reply }, path = defaultQuestionsPath()) {
|
|
21
|
+
if (!traceId || !reply) return;
|
|
22
|
+
const all = { ...readAll(path), [String(traceId)]: { activityId, reply } };
|
|
23
|
+
writeAll(path, all);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function clearQuestion(traceId, path = defaultQuestionsPath()) {
|
|
27
|
+
if (!traceId) return;
|
|
28
|
+
const all = readAll(path);
|
|
29
|
+
if (!(String(traceId) in all)) return;
|
|
30
|
+
delete all[String(traceId)];
|
|
31
|
+
writeAll(path, all);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readAll(path) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
37
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeAll(path, questions) {
|
|
44
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
46
|
+
writeFileSync(tmp, `${JSON.stringify(questions, null, 2)}\n`);
|
|
47
|
+
renameSync(tmp, path);
|
|
48
|
+
}
|
package/src/queue.mjs
CHANGED
|
@@ -28,14 +28,19 @@ function emptyState() {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export class FlambeQueue {
|
|
31
|
-
constructor({ baseUrl, traceId, path = defaultQueuePath() }) {
|
|
31
|
+
constructor({ baseUrl, traceId, agentId, path = defaultQueuePath() }) {
|
|
32
32
|
this.baseUrl = baseUrl;
|
|
33
33
|
this.traceId = Number(traceId);
|
|
34
|
+
this.agentId = agentId;
|
|
34
35
|
this.path = path;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
matches(entry) {
|
|
38
|
-
return entry.baseUrl === this.baseUrl
|
|
39
|
+
return entry.baseUrl === this.baseUrl
|
|
40
|
+
&& entry.traceId === this.traceId
|
|
41
|
+
// Version-1 entries created before identity was recorded remain replayable.
|
|
42
|
+
// New entries may only be replayed by the worker that queued them.
|
|
43
|
+
&& (entry.agentId === undefined || entry.agentId === this.agentId);
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
async read() {
|
|
@@ -70,6 +75,7 @@ export class FlambeQueue {
|
|
|
70
75
|
type,
|
|
71
76
|
baseUrl: this.baseUrl,
|
|
72
77
|
traceId: this.traceId,
|
|
78
|
+
...(this.agentId ? { agentId: this.agentId } : {}),
|
|
73
79
|
...(localId ? { localId } : {}),
|
|
74
80
|
...payload,
|
|
75
81
|
});
|
|
@@ -107,7 +113,13 @@ export class FlambeQueue {
|
|
|
107
113
|
} else if (entry.type === 'end' || entry.type === 'suspend' || entry.type === 'resume') {
|
|
108
114
|
const activityId = state.aliases[entry.activityId] ?? entry.activityId;
|
|
109
115
|
const post = { end, suspend, resume }[entry.type];
|
|
110
|
-
await post({
|
|
116
|
+
await post({
|
|
117
|
+
activityId,
|
|
118
|
+
message: entry.message,
|
|
119
|
+
timestamp: entry.timestamp,
|
|
120
|
+
force: entry.force ?? false,
|
|
121
|
+
phase: entry.phase,
|
|
122
|
+
});
|
|
111
123
|
if (entry.type === 'end') delete state.aliases[entry.activityId];
|
|
112
124
|
} else if (entry.type === 'observe') {
|
|
113
125
|
await observe(entry.input);
|
package/src/runtime.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Explicit transport boundary; never infer local-only intent from a URL. */
|
|
2
|
+
export function runtimeConfig(env) {
|
|
3
|
+
const mode = env.FLAMBE_RUNTIME_MODE?.trim();
|
|
4
|
+
if (!mode) return {};
|
|
5
|
+
if (!['cloud', 'local_self_contained'].includes(mode)) {
|
|
6
|
+
throw new Error('FLAMBE_RUNTIME_MODE must be cloud or local_self_contained');
|
|
7
|
+
}
|
|
8
|
+
return { runtimeMode: mode };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function assertRuntimeUrl(value, mode) {
|
|
12
|
+
if (mode !== 'local_self_contained') return;
|
|
13
|
+
let url;
|
|
14
|
+
try { url = new URL(value); } catch {
|
|
15
|
+
throw new Error('Self-contained mode requires a loopback HTTP URL');
|
|
16
|
+
}
|
|
17
|
+
// Numeric addresses avoid DNS resolution changing the trust boundary.
|
|
18
|
+
if (!['http:', 'https:'].includes(url.protocol)
|
|
19
|
+
|| !['127.0.0.1', '[::1]'].includes(url.hostname)
|
|
20
|
+
|| url.username || url.password) {
|
|
21
|
+
throw new Error('Self-contained mode requires 127.0.0.1 or [::1]; remote destinations are disabled');
|
|
22
|
+
}
|
|
23
|
+
}
|