@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/README.md
CHANGED
|
@@ -52,10 +52,12 @@ Create a `.env` in the project working directory (or set the same names in the
|
|
|
52
52
|
shell; shell wins):
|
|
53
53
|
|
|
54
54
|
```dotenv
|
|
55
|
-
FLAMBE_URL=https://
|
|
55
|
+
FLAMBE_URL=https://your-app.fly.dev
|
|
56
56
|
FLAMBE_API_TOKEN=flb_...
|
|
57
57
|
FLAMBE_TRACE_ID=1
|
|
58
58
|
# FLAMBE_THREAD=flambe
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Mint a token in Flambe Settings → API tokens. See the
|
|
61
|
+
Mint a token in Flambe Settings → API tokens. See the
|
|
62
|
+
[repository README](https://github.com/davvidbaker/flambe#connect-coding-agents)
|
|
63
|
+
for commands (`start`, `end`, `message`, `observe`, local `serve`).
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
|
|
5
5
|
import { clientFromEnv, clientFromHostEnv } from './client.mjs';
|
|
6
|
+
import { clearQuestion, readOpenQuestion, rememberQuestion } from './questions.mjs';
|
|
6
7
|
|
|
7
8
|
// `node:sqlite` still prints an ExperimentalWarning on import in some Node 22 releases.
|
|
8
9
|
// Only serve/export need it, so load those modules on demand and keep every other
|
|
@@ -19,7 +20,8 @@ const usage = `Usage:
|
|
|
19
20
|
flambe end <activity-id> [message] [--force]
|
|
20
21
|
flambe suspend <activity-id> [message]
|
|
21
22
|
flambe resume <activity-id> [message]
|
|
22
|
-
flambe
|
|
23
|
+
flambe plan <activity name> [--start <ISO-8601>] [--end <ISO-8601>] [--description <text>] [--weight <n>]
|
|
24
|
+
flambe status [--active | --suspended] [--unstarted] [--json]
|
|
23
25
|
flambe threads [--json]
|
|
24
26
|
flambe categories [--json]
|
|
25
27
|
flambe ping
|
|
@@ -52,6 +54,7 @@ Optional:
|
|
|
52
54
|
still keeps a child on its parent's thread.
|
|
53
55
|
FLAMBE_QUEUE_PATH Local offline queue path (default: ~/.flambe/event-queue.json)
|
|
54
56
|
FLAMBE_LOCAL_DB SQLite path for flambe serve / export (default: ~/.flambe/local.sqlite)
|
|
57
|
+
FLAMBE_RUNTIME_MODE local_self_contained restricts CLI requests to numeric loopback; cloud preserves hosted access
|
|
55
58
|
|
|
56
59
|
serve / export do not need FLAMBE_URL. import needs FLAMBE_URL and FLAMBE_API_TOKEN (the production instance).`;
|
|
57
60
|
|
|
@@ -125,20 +128,55 @@ function parseEnd(args) {
|
|
|
125
128
|
};
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
function parsePlan(args) {
|
|
132
|
+
const nameParts = [];
|
|
133
|
+
let description;
|
|
134
|
+
let weight;
|
|
135
|
+
let start;
|
|
136
|
+
let end;
|
|
137
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
138
|
+
const arg = args[index];
|
|
139
|
+
if (arg === '--description') {
|
|
140
|
+
description = args[index + 1];
|
|
141
|
+
if (description === undefined) throw new Error('--description requires a value');
|
|
142
|
+
index += 1;
|
|
143
|
+
} else if (arg === '--weight') {
|
|
144
|
+
weight = Number(args[index + 1]);
|
|
145
|
+
if (!Number.isInteger(weight)) throw new Error('--weight requires an integer');
|
|
146
|
+
index += 1;
|
|
147
|
+
} else if (arg === '--start' || arg === '--end') {
|
|
148
|
+
const raw = args[index + 1];
|
|
149
|
+
if (raw === undefined) throw new Error(`${arg} requires an ISO-8601 timestamp`);
|
|
150
|
+
const parsed = Date.parse(raw);
|
|
151
|
+
if (!Number.isFinite(parsed)) throw new Error(`${arg} must be a valid ISO-8601 timestamp`);
|
|
152
|
+
if (arg === '--start') start = parsed;
|
|
153
|
+
else end = parsed;
|
|
154
|
+
index += 1;
|
|
155
|
+
} else if (arg.startsWith('--')) {
|
|
156
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
157
|
+
} else nameParts.push(arg);
|
|
158
|
+
}
|
|
159
|
+
const name = nameParts.join(' ').trim();
|
|
160
|
+
if (!name) throw new Error('Usage: flambe plan <activity name> [--start <ISO-8601>] [--end <ISO-8601>]');
|
|
161
|
+
return { name, description, weight, scheduledStart: start, scheduledEnd: end };
|
|
162
|
+
}
|
|
163
|
+
|
|
128
164
|
function parseStatus(args) {
|
|
129
165
|
let activeOnly = false;
|
|
130
166
|
let suspendedOnly = false;
|
|
167
|
+
let includeUnstarted = false;
|
|
131
168
|
let json = false;
|
|
132
169
|
|
|
133
170
|
for (const arg of args) {
|
|
134
171
|
if (arg === '--active') activeOnly = true;
|
|
135
172
|
else if (arg === '--suspended') suspendedOnly = true;
|
|
173
|
+
else if (arg === '--unstarted') includeUnstarted = true;
|
|
136
174
|
else if (arg === '--json') json = true;
|
|
137
175
|
else throw new Error(`Unknown option: ${arg}`);
|
|
138
176
|
}
|
|
139
177
|
|
|
140
178
|
if (activeOnly && suspendedOnly) throw new Error('--active and --suspended cannot be used together');
|
|
141
|
-
return { activeOnly, suspendedOnly, json };
|
|
179
|
+
return { activeOnly, suspendedOnly, includeUnstarted, json };
|
|
142
180
|
}
|
|
143
181
|
|
|
144
182
|
function parseObserve(args) {
|
|
@@ -275,6 +313,14 @@ function parseImport(args) {
|
|
|
275
313
|
function formatAction(action) {
|
|
276
314
|
if (action.type === 'create_child') return `created child ${action.activity_id} under ${action.parent_activity_id}`;
|
|
277
315
|
if (action.type === 'update_activity') return `updated activity ${action.activity_id}`;
|
|
316
|
+
if (action.type === 'reparent') return `reparented ${action.activity_id} under ${action.parent_activity_id}`;
|
|
317
|
+
if (action.type === 'rename') return `renamed ${action.activity_id} to ${action.name}`;
|
|
318
|
+
if (action.type === 'ask') return `asked about ${action.activity_id}`;
|
|
319
|
+
if (action.type === 'resume_existing') return `resumed existing ${action.activity_id}`;
|
|
320
|
+
if (action.type === 'resume_ancestor') return `resumed ancestor ${action.activity_id}`;
|
|
321
|
+
if (action.type === 'no_op') return `left ${action.activity_id} as it was`;
|
|
322
|
+
if (action.type === 'keep') return 'kept the recorded structure';
|
|
323
|
+
if (action.type === 'skipped') return 'recorded the proposal; model review skipped';
|
|
278
324
|
return JSON.stringify(action);
|
|
279
325
|
}
|
|
280
326
|
|
|
@@ -286,6 +332,15 @@ function formatReducerNote(note) {
|
|
|
286
332
|
if (note.type === 'agent_named') {
|
|
287
333
|
return `reducer named this agent "${note.name}"; the CLI will use it from now on\n`;
|
|
288
334
|
}
|
|
335
|
+
if (note.type === 'command_result') {
|
|
336
|
+
const lines = [];
|
|
337
|
+
if (note.direction) lines.push(`direction\t${note.direction}\n`);
|
|
338
|
+
if (note.reply) lines.push(`reply\t${note.reply}\n`);
|
|
339
|
+
if (note.actionsApplied?.length) {
|
|
340
|
+
lines.push(`actions\t${note.actionsApplied.map(formatAction).join('; ')}\n`);
|
|
341
|
+
}
|
|
342
|
+
return lines.join('');
|
|
343
|
+
}
|
|
289
344
|
if (note.type === 'start_reduced') {
|
|
290
345
|
const parts = [];
|
|
291
346
|
if (note.parentSource === 'inferred') parts.push(`nested ${note.activityId} under ${note.parentId}`);
|
|
@@ -298,7 +353,7 @@ function formatReducerNote(note) {
|
|
|
298
353
|
return `reducer: ${JSON.stringify(note)}\n`;
|
|
299
354
|
}
|
|
300
355
|
|
|
301
|
-
export async function run(argv, { env = process.env, stdout = process.stdout, stderr = process.stderr, client } = {}) {
|
|
356
|
+
export async function run(argv, { env = process.env, stdout = process.stdout, stderr = process.stderr, client, questionsPath } = {}) {
|
|
302
357
|
const [command, ...args] = argv;
|
|
303
358
|
|
|
304
359
|
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
@@ -320,6 +375,9 @@ export async function run(argv, { env = process.env, stdout = process.stdout, st
|
|
|
320
375
|
|
|
321
376
|
if (command === 'export') {
|
|
322
377
|
const { file, dbPath } = parseExport(args, env);
|
|
378
|
+
if (!existsSync(dbPath)) {
|
|
379
|
+
throw new Error(`Local database does not exist: ${dbPath}`);
|
|
380
|
+
}
|
|
323
381
|
const { LocalStore } = await loadStore();
|
|
324
382
|
const store = new LocalStore(dbPath);
|
|
325
383
|
try {
|
|
@@ -347,14 +405,32 @@ export async function run(argv, { env = process.env, stdout = process.stdout, st
|
|
|
347
405
|
}
|
|
348
406
|
|
|
349
407
|
// Reducer notes go to stderr so stdout stays a machine-friendly id.
|
|
408
|
+
const questionFile = questionsPath ?? (client ? undefined : env.FLAMBE_QUESTIONS_PATH);
|
|
409
|
+
const persistQuestions = Boolean(questionsPath) || !client;
|
|
410
|
+
const traceId = env.FLAMBE_TRACE_ID;
|
|
411
|
+
const pendingQuestion = !persistQuestions || command === 'message'
|
|
412
|
+
? undefined
|
|
413
|
+
: readOpenQuestion(traceId, questionFile);
|
|
414
|
+
if (pendingQuestion) {
|
|
415
|
+
stderr.write(`reducer still waiting on activity ${pendingQuestion.activityId}: ${pendingQuestion.reply}\n`);
|
|
416
|
+
}
|
|
350
417
|
const onReducerNote = note => {
|
|
351
418
|
const text = formatReducerNote(note);
|
|
352
419
|
if (text) stderr.write(text);
|
|
420
|
+
if (persistQuestions && note.type === 'command_result' && note.asked) {
|
|
421
|
+
rememberQuestion(traceId, { activityId: note.activityId, reply: note.reply }, questionFile);
|
|
422
|
+
}
|
|
353
423
|
};
|
|
354
424
|
const flambe = client ?? clientFromEnv(env, { onReducerNote });
|
|
355
425
|
flambe.onReducerNote ??= onReducerNote;
|
|
356
426
|
await flambe.flushQueue?.();
|
|
357
427
|
|
|
428
|
+
if (command === 'plan') {
|
|
429
|
+
const activityId = await flambe.plan(parsePlan(args));
|
|
430
|
+
stdout.write(`${activityId}\n`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
358
434
|
if (command === 'start') {
|
|
359
435
|
const activityId = await flambe.start(parseStart(args));
|
|
360
436
|
stdout.write(`${activityId}\n`);
|
|
@@ -378,14 +454,20 @@ export async function run(argv, { env = process.env, stdout = process.stdout, st
|
|
|
378
454
|
}
|
|
379
455
|
|
|
380
456
|
if (command === 'status') {
|
|
381
|
-
const { activeOnly, suspendedOnly, json } = parseStatus(args);
|
|
382
|
-
const status = await flambe.status({
|
|
457
|
+
const { activeOnly, suspendedOnly, includeUnstarted, json } = parseStatus(args);
|
|
458
|
+
const status = await flambe.status({
|
|
459
|
+
activeOnly,
|
|
460
|
+
suspendedOnly,
|
|
461
|
+
...(includeUnstarted ? { includeUnstarted } : {}),
|
|
462
|
+
});
|
|
383
463
|
|
|
384
464
|
if (json) {
|
|
385
465
|
stdout.write(`${JSON.stringify(status)}\n`);
|
|
386
466
|
} else {
|
|
387
467
|
for (const activity of status.activities) {
|
|
388
|
-
|
|
468
|
+
const phase = activity.latestEvent?.phase ?? activity.status;
|
|
469
|
+
const when = activity.latestEvent?.timestamp ?? '';
|
|
470
|
+
stdout.write(`${activity.id}\t${activity.path.join(' > ')}\t${activity.threadId}\t${activity.threadName ?? ''}\t${activity.categoryIds.join(',')}\t${phase}\t${when}\n`);
|
|
389
471
|
}
|
|
390
472
|
}
|
|
391
473
|
|
|
@@ -439,6 +521,7 @@ export async function run(argv, { env = process.env, stdout = process.stdout, st
|
|
|
439
521
|
if (command === 'message') {
|
|
440
522
|
const { text, activityId, json } = parseMessage(args);
|
|
441
523
|
const decision = await flambe.message({ activityId, text });
|
|
524
|
+
if (persistQuestions) clearQuestion(traceId, questionFile);
|
|
442
525
|
if (json) {
|
|
443
526
|
stdout.write(`${JSON.stringify(decision)}\n`);
|
|
444
527
|
} else {
|
package/src/client.mjs
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { FlambeQueue } from './queue.mjs';
|
|
2
2
|
import { defaultAgentNamesPath, rememberAgentName, rememberedAgentName } from './agentNames.mjs';
|
|
3
|
+
import { assertRuntimeUrl, runtimeConfig } from './runtime.mjs';
|
|
4
|
+
|
|
5
|
+
const REQUIRED_AGENT_COMMANDS = ['start', 'end', 'suspend', 'resume', 'status', 'message'];
|
|
6
|
+
const ENDED_PHASES = new Set(['E', 'J', 'V']);
|
|
7
|
+
|
|
8
|
+
function endedPhase(phase) {
|
|
9
|
+
return ENDED_PHASES.has(phase);
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
export function hostConfigFromEnv(env = process.env) {
|
|
5
13
|
const required = ['FLAMBE_URL', 'FLAMBE_API_TOKEN'];
|
|
@@ -11,6 +19,7 @@ export function hostConfigFromEnv(env = process.env) {
|
|
|
11
19
|
|
|
12
20
|
return {
|
|
13
21
|
baseUrl: env.FLAMBE_URL.replace(/\/+$/, ''),
|
|
22
|
+
...runtimeConfig(env),
|
|
14
23
|
token: env.FLAMBE_API_TOKEN,
|
|
15
24
|
};
|
|
16
25
|
}
|
|
@@ -32,6 +41,7 @@ export function configFromEnv(env = process.env) {
|
|
|
32
41
|
|
|
33
42
|
return {
|
|
34
43
|
baseUrl: env.FLAMBE_URL.replace(/\/+$/, ''),
|
|
44
|
+
...runtimeConfig(env),
|
|
35
45
|
...agentIdentityFromEnv(env),
|
|
36
46
|
token: env.FLAMBE_API_TOKEN,
|
|
37
47
|
traceId,
|
|
@@ -89,17 +99,31 @@ function claudeAgentId(env) {
|
|
|
89
99
|
return sessionId ? `claude:${sessionId}` : undefined;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
const LIFECYCLE_PHASES = new Set(['B', 'E', 'S', 'R']);
|
|
102
|
+
const LIFECYCLE_PHASES = new Set(['B', 'E', 'S', 'R', 'X', 'J', 'V']);
|
|
93
103
|
|
|
94
104
|
function errorDetail(payload) {
|
|
95
105
|
if (!payload) return null;
|
|
96
106
|
if (typeof payload.error === 'string') return payload.error;
|
|
107
|
+
if (payload.error && typeof payload.error === 'object') {
|
|
108
|
+
const message = payload.error.message ?? payload.error.code;
|
|
109
|
+
const openChildren = payload.error.open_children;
|
|
110
|
+
if (!Array.isArray(openChildren) || openChildren.length === 0) return message ?? JSON.stringify(payload.error);
|
|
111
|
+
|
|
112
|
+
const children = openChildren
|
|
113
|
+
.map(child => `${child.activity_id} (${child.activity_name ?? child.name ?? 'unnamed'})`)
|
|
114
|
+
.join(', ');
|
|
115
|
+
const ids = openChildren.map(child => child.activity_id).join(', ');
|
|
116
|
+
return `${message ?? 'Open children remain'}: ${children}. End ${ids} first (or pass --force).`;
|
|
117
|
+
}
|
|
97
118
|
if (payload.errors) return JSON.stringify(payload.errors);
|
|
98
119
|
return null;
|
|
99
120
|
}
|
|
100
121
|
|
|
101
122
|
export class FlambeClient {
|
|
102
|
-
constructor({ baseUrl, token, traceId, threadId, defaultThread, agentId, agentName, agentPlatform, agentNamesPath, fetchImpl = globalThis.fetch, now = Date.now, queuePath, queue, onReducerNote }) {
|
|
123
|
+
constructor({ baseUrl, token, traceId, threadId, defaultThread, agentId, agentName, agentPlatform, agentNamesPath, runtimeMode, fetchImpl = globalThis.fetch, now = Date.now, queuePath, queue, onReducerNote }) {
|
|
124
|
+
runtimeConfig({ FLAMBE_RUNTIME_MODE: runtimeMode });
|
|
125
|
+
assertRuntimeUrl(baseUrl, runtimeMode);
|
|
126
|
+
this.runtimeMode = runtimeMode;
|
|
103
127
|
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
104
128
|
this.token = token;
|
|
105
129
|
this.agentId = agentId;
|
|
@@ -111,14 +135,62 @@ export class FlambeClient {
|
|
|
111
135
|
this.fetch = fetchImpl;
|
|
112
136
|
this.now = now;
|
|
113
137
|
this.onReducerNote = onReducerNote;
|
|
114
|
-
this.queue = queue ?? new FlambeQueue({
|
|
138
|
+
this.queue = queue ?? new FlambeQueue({
|
|
139
|
+
baseUrl: this.baseUrl,
|
|
140
|
+
traceId: this.traceId,
|
|
141
|
+
agentId: this.agentId,
|
|
142
|
+
path: queuePath,
|
|
143
|
+
});
|
|
144
|
+
this.agentCommandsCapability = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async supportsAgentCommands() {
|
|
148
|
+
if (!this.agentCommandsCapability) {
|
|
149
|
+
const probe = this.request('/api/agent-commands', { responseMetadata: true })
|
|
150
|
+
.then(({ payload, contentType }) => {
|
|
151
|
+
if (contentType.toLowerCase().startsWith('text/html')) return false;
|
|
152
|
+
const commands = payload?.data?.commands;
|
|
153
|
+
if (payload?.data?.version !== 1
|
|
154
|
+
|| !Array.isArray(commands)
|
|
155
|
+
|| REQUIRED_AGENT_COMMANDS.some(command => !commands.includes(command))) {
|
|
156
|
+
throw new Error('Flambe API returned an invalid agent-command capability document');
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
})
|
|
160
|
+
.catch(error => {
|
|
161
|
+
if (error?.status === 404 || error?.status === 405) return false;
|
|
162
|
+
this.agentCommandsCapability = null;
|
|
163
|
+
throw error;
|
|
164
|
+
});
|
|
165
|
+
this.agentCommandsCapability = probe;
|
|
166
|
+
}
|
|
167
|
+
return this.agentCommandsCapability;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async agentCommand(command, arguments_) {
|
|
171
|
+
const payload = await this.request('/api/agent-commands', {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
body: { command, arguments: arguments_ },
|
|
174
|
+
});
|
|
175
|
+
return payload.data;
|
|
115
176
|
}
|
|
116
177
|
|
|
117
|
-
|
|
178
|
+
commandIdentity() {
|
|
179
|
+
return {
|
|
180
|
+
trace_id: this.traceId,
|
|
181
|
+
...(this.agentId ? { agent_id: this.agentId } : {}),
|
|
182
|
+
...(this.agentName ? { agent_name: this.agentName } : {}),
|
|
183
|
+
...(this.agentPlatform ? { agent_platform: this.agentPlatform } : {}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async request(path, { method = 'GET', body, responseMetadata = false } = {}) {
|
|
188
|
+
assertRuntimeUrl(`${this.baseUrl}${path}`, this.runtimeMode);
|
|
118
189
|
let response;
|
|
119
190
|
try {
|
|
120
191
|
response = await this.fetch(`${this.baseUrl}${path}`, {
|
|
121
192
|
method,
|
|
193
|
+
...(this.runtimeMode === 'local_self_contained' ? { redirect: 'manual' } : {}),
|
|
122
194
|
headers: {
|
|
123
195
|
authorization: `Bearer ${this.token}`,
|
|
124
196
|
...(this.agentId ? { 'x-flambe-agent-id': this.agentId } : {}),
|
|
@@ -153,6 +225,13 @@ export class FlambeClient {
|
|
|
153
225
|
}
|
|
154
226
|
|
|
155
227
|
this.adoptAgentName(response.headers);
|
|
228
|
+
if (responseMetadata) {
|
|
229
|
+
return {
|
|
230
|
+
payload,
|
|
231
|
+
contentType: response.headers.get('content-type') ?? '',
|
|
232
|
+
status: response.status,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
156
235
|
return payload;
|
|
157
236
|
}
|
|
158
237
|
|
|
@@ -212,8 +291,31 @@ export class FlambeClient {
|
|
|
212
291
|
return payload.data;
|
|
213
292
|
}
|
|
214
293
|
|
|
215
|
-
async
|
|
294
|
+
async plan({ name, description, weight, scheduledStart, scheduledEnd }) {
|
|
295
|
+
if (!name?.trim()) throw new Error('Activity name is required');
|
|
296
|
+
const result = await this.agentCommand('plan', {
|
|
297
|
+
...this.commandIdentity(),
|
|
298
|
+
name: name.trim(),
|
|
299
|
+
...(description ? { description } : {}),
|
|
300
|
+
...(weight == null ? {} : { weight }),
|
|
301
|
+
...(scheduledStart == null ? {} : { scheduled_start: scheduledStart }),
|
|
302
|
+
...(scheduledEnd == null ? {} : { scheduled_end: scheduledEnd }),
|
|
303
|
+
});
|
|
304
|
+
this.reportCommandResult(result);
|
|
305
|
+
return result.activity_id;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async status({ activeOnly = false, suspendedOnly = false, includeUnstarted = false } = {}) {
|
|
216
309
|
if (activeOnly && suspendedOnly) throw new Error('--active and --suspended cannot be used together');
|
|
310
|
+
if (await this.supportsAgentCommands()) {
|
|
311
|
+
const result = await this.agentCommand('status', {
|
|
312
|
+
...this.commandIdentity(),
|
|
313
|
+
active_only: activeOnly,
|
|
314
|
+
suspended_only: suspendedOnly,
|
|
315
|
+
...(includeUnstarted ? { include_unstarted: true } : {}),
|
|
316
|
+
});
|
|
317
|
+
return result.state;
|
|
318
|
+
}
|
|
217
319
|
const trace = await this.getTrace();
|
|
218
320
|
return this.statusFromTrace(trace, { activeOnly, suspendedOnly });
|
|
219
321
|
}
|
|
@@ -250,7 +352,7 @@ export class FlambeClient {
|
|
|
250
352
|
};
|
|
251
353
|
|
|
252
354
|
const activities = [...latestByActivity.values()]
|
|
253
|
-
.filter(event => !activeOnly ||
|
|
355
|
+
.filter(event => !activeOnly || ['B', 'R', 'X'].includes(event.phase))
|
|
254
356
|
.filter(event => !suspendedOnly || event.phase === 'S')
|
|
255
357
|
.sort(compareEvents)
|
|
256
358
|
.map(event => ({
|
|
@@ -352,6 +454,28 @@ export class FlambeClient {
|
|
|
352
454
|
* sends only what the worker actually said: `parent_id` absent = infer, `null` = root.
|
|
353
455
|
*/
|
|
354
456
|
async postStart({ name, description, threadId, parentId, categoryIds, timestamp }) {
|
|
457
|
+
if (await this.supportsAgentCommands()) {
|
|
458
|
+
const resolvedParentId = parentId === undefined || parentId === null
|
|
459
|
+
? parentId
|
|
460
|
+
: await this.queue.resolveActivityId(parentId);
|
|
461
|
+
if (String(resolvedParentId).startsWith('offline-')) {
|
|
462
|
+
const error = new Error('Parent activity is queued for offline delivery');
|
|
463
|
+
error.retryable = true;
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
const result = await this.agentCommand('start', {
|
|
467
|
+
...this.commandIdentity(),
|
|
468
|
+
name,
|
|
469
|
+
...(description ? { description } : {}),
|
|
470
|
+
...(threadId === undefined ? {} : { thread_id: Number(threadId) }),
|
|
471
|
+
...(resolvedParentId === undefined ? {} : { parent_id: resolvedParentId }),
|
|
472
|
+
category_ids: categoryIds,
|
|
473
|
+
timestamp,
|
|
474
|
+
});
|
|
475
|
+
this.reportCommandResult(result);
|
|
476
|
+
return result.activity_id;
|
|
477
|
+
}
|
|
478
|
+
|
|
355
479
|
const resolvedParentId = parentId === undefined || parentId === null
|
|
356
480
|
? parentId
|
|
357
481
|
: await this.queue.resolveActivityId(parentId);
|
|
@@ -398,8 +522,8 @@ export class FlambeClient {
|
|
|
398
522
|
}
|
|
399
523
|
|
|
400
524
|
async end({ activityId, message, force = false }) {
|
|
401
|
-
|
|
402
|
-
return this.lifecycleEvent({ activityId, message, phase
|
|
525
|
+
const phase = message ? 'V' : 'E';
|
|
526
|
+
return this.lifecycleEvent({ activityId, message, phase, queueType: 'end', force, guardChildren: !force });
|
|
403
527
|
}
|
|
404
528
|
|
|
405
529
|
/**
|
|
@@ -439,14 +563,20 @@ export class FlambeClient {
|
|
|
439
563
|
return this.lifecycleEvent({ activityId, message, phase: 'R', queueType: 'resume' });
|
|
440
564
|
}
|
|
441
565
|
|
|
442
|
-
async lifecycleEvent({ activityId, message, phase, queueType }) {
|
|
566
|
+
async lifecycleEvent({ activityId, message, phase, queueType, force = false, guardChildren = false }) {
|
|
443
567
|
if (!String(activityId).startsWith('offline-')) {
|
|
444
568
|
const id = Number(activityId);
|
|
445
569
|
if (!Number.isInteger(id) || id <= 0) throw new Error('Activity id must be a positive integer');
|
|
446
570
|
}
|
|
447
571
|
|
|
448
572
|
const timestamp = this.now();
|
|
449
|
-
const input = {
|
|
573
|
+
const input = {
|
|
574
|
+
activityId,
|
|
575
|
+
message,
|
|
576
|
+
timestamp,
|
|
577
|
+
phase,
|
|
578
|
+
...(queueType === 'end' ? { force } : {}),
|
|
579
|
+
};
|
|
450
580
|
|
|
451
581
|
try {
|
|
452
582
|
const id = await this.queue.resolveActivityId(activityId);
|
|
@@ -454,8 +584,11 @@ export class FlambeClient {
|
|
|
454
584
|
await this.queue.enqueue(queueType, input);
|
|
455
585
|
return 'queued';
|
|
456
586
|
}
|
|
457
|
-
|
|
458
|
-
|
|
587
|
+
if (guardChildren && !await this.supportsAgentCommands()) {
|
|
588
|
+
await this.assertNoOpenChildren(id);
|
|
589
|
+
}
|
|
590
|
+
const eventId = await this.postLifecycleEvent({ ...input, activityId: id, phase, force });
|
|
591
|
+
if (endedPhase(phase)) await this.queue.removeAlias(activityId);
|
|
459
592
|
return eventId;
|
|
460
593
|
} catch (error) {
|
|
461
594
|
if (!error?.retryable) throw error;
|
|
@@ -464,7 +597,21 @@ export class FlambeClient {
|
|
|
464
597
|
}
|
|
465
598
|
}
|
|
466
599
|
|
|
467
|
-
async postLifecycleEvent({ activityId, message, timestamp, phase }) {
|
|
600
|
+
async postLifecycleEvent({ activityId, message, timestamp, phase, force = false }) {
|
|
601
|
+
if (await this.supportsAgentCommands()) {
|
|
602
|
+
const command = { E: 'end', J: 'end', V: 'end', S: 'suspend', R: 'resume' }[phase];
|
|
603
|
+
const result = await this.agentCommand(command, {
|
|
604
|
+
...this.commandIdentity(),
|
|
605
|
+
activity_id: Number(activityId),
|
|
606
|
+
...(message ? { message } : {}),
|
|
607
|
+
timestamp,
|
|
608
|
+
...(command === 'end' ? { force, ...(phase ? { phase } : {}) } : {}),
|
|
609
|
+
});
|
|
610
|
+
this.reportClosedDescendants(Number(activityId), result.closed_descendants ?? []);
|
|
611
|
+
this.reportCommandResult(result);
|
|
612
|
+
return result.event_id;
|
|
613
|
+
}
|
|
614
|
+
|
|
468
615
|
const payload = await this.request('/api/events', {
|
|
469
616
|
method: 'POST',
|
|
470
617
|
body: {
|
|
@@ -479,10 +626,32 @@ export class FlambeClient {
|
|
|
479
626
|
});
|
|
480
627
|
|
|
481
628
|
const closed = payload.data.reducer?.closed_descendants ?? [];
|
|
629
|
+
this.reportClosedDescendants(Number(activityId), closed);
|
|
630
|
+
|
|
631
|
+
return payload.data.id;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
reportCommandResult(result) {
|
|
635
|
+
if (!result) return;
|
|
636
|
+
const actions = result.actions_applied ?? [];
|
|
637
|
+
const direction = result.direction ?? null;
|
|
638
|
+
const reply = result.reply ?? null;
|
|
639
|
+
if (!direction && !reply && actions.length === 0) return;
|
|
640
|
+
this.onReducerNote?.({
|
|
641
|
+
type: 'command_result',
|
|
642
|
+
activityId: result.activity_id,
|
|
643
|
+
direction,
|
|
644
|
+
reply,
|
|
645
|
+
actionsApplied: actions,
|
|
646
|
+
asked: actions.some(action => action.type === 'ask'),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
reportClosedDescendants(activityId, closed) {
|
|
482
651
|
if (closed.length > 0) {
|
|
483
652
|
this.onReducerNote?.({
|
|
484
653
|
type: 'closed_descendants',
|
|
485
|
-
activityId
|
|
654
|
+
activityId,
|
|
486
655
|
closedDescendants: closed.map(item => ({
|
|
487
656
|
activityId: item.activity_id,
|
|
488
657
|
activityName: item.activity_name,
|
|
@@ -490,14 +659,15 @@ export class FlambeClient {
|
|
|
490
659
|
})),
|
|
491
660
|
});
|
|
492
661
|
}
|
|
493
|
-
|
|
494
|
-
return payload.data.id;
|
|
495
662
|
}
|
|
496
663
|
|
|
497
664
|
async flushQueue() {
|
|
498
665
|
return this.queue.flush({
|
|
499
666
|
start: input => this.postStart(input),
|
|
500
|
-
end: input => this.postLifecycleEvent({
|
|
667
|
+
end: input => this.postLifecycleEvent({
|
|
668
|
+
...input,
|
|
669
|
+
phase: input.phase ?? (input.message ? 'V' : 'E'),
|
|
670
|
+
}),
|
|
501
671
|
suspend: input => this.postLifecycleEvent({ ...input, phase: 'S' }),
|
|
502
672
|
resume: input => this.postLifecycleEvent({ ...input, phase: 'R' }),
|
|
503
673
|
observe: input => this.postObservation(input),
|
|
@@ -538,6 +708,28 @@ export class FlambeClient {
|
|
|
538
708
|
async message({ activityId, text }) {
|
|
539
709
|
if (!text?.trim()) throw new Error('Message text is required');
|
|
540
710
|
|
|
711
|
+
if (await this.supportsAgentCommands()) {
|
|
712
|
+
let resolvedActivityId = activityId;
|
|
713
|
+
if (activityId !== undefined) {
|
|
714
|
+
resolvedActivityId = await this.queue.resolveActivityId(activityId);
|
|
715
|
+
if (String(resolvedActivityId).startsWith('offline-')) {
|
|
716
|
+
throw new Error('Activity is still queued for offline delivery; retry once Flambe is reachable');
|
|
717
|
+
}
|
|
718
|
+
const id = Number(resolvedActivityId);
|
|
719
|
+
if (!Number.isInteger(id) || id <= 0) throw new Error('--activity must be a positive integer');
|
|
720
|
+
resolvedActivityId = id;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const result = await this.agentCommand('message', {
|
|
724
|
+
...this.commandIdentity(),
|
|
725
|
+
...(resolvedActivityId === undefined ? {} : { activity_id: resolvedActivityId }),
|
|
726
|
+
message: text.trim(),
|
|
727
|
+
});
|
|
728
|
+
const decision = { activityId: result.activity_id ?? resolvedActivityId, ...result };
|
|
729
|
+
this.reportCommandResult(result);
|
|
730
|
+
return decision;
|
|
731
|
+
}
|
|
732
|
+
|
|
541
733
|
const resolvedActivityId = activityId === undefined
|
|
542
734
|
? await this.currentActivityId()
|
|
543
735
|
: await this.queue.resolveActivityId(activityId);
|