@controlflow-ai/daemon 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -18
- package/package.json +14 -3
- package/src/agent-runtime.ts +15 -0
- package/src/app.ts +217 -4
- package/src/client.ts +11 -3
- package/src/console.ts +243 -19
- package/src/daemon.ts +25 -6
- package/src/db.ts +101 -4
- package/src/lark/app-registration.ts +141 -0
- package/src/lark/cli.ts +4 -134
- package/src/lark/credentials.ts +36 -3
- package/src/lark/event-router.ts +1 -1
- package/src/lark/setup.ts +74 -5
- package/src/local-api.ts +69 -2
- package/src/local-auth.ts +4 -3
- package/src/migrations/023_projects.ts +65 -0
- package/src/migrations.ts +2 -1
- package/src/types.ts +32 -0
package/src/console.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { boolFlag, parseArgs, flag, numberFlag } from './args.js';
|
|
|
4
4
|
import { LockClient } from './client.js';
|
|
5
5
|
import { defaultServerUrl } from './config.js';
|
|
6
6
|
import { formatMessages } from './format.js';
|
|
7
|
+
import { defaultLarkConfigPath, findCredentialByAgent, loadLarkCredentials, saveLarkCredentials, unbindCredentialAgent, upsertCredential } from './lark/credentials.js';
|
|
8
|
+
import { registerLarkAppFromDeviceFlow, resolveLarkBotInfo, type LarkBotInfoResult } from './lark/setup.js';
|
|
9
|
+
import type { LarkRegistrationComplete } from './lark/app-registration.js';
|
|
7
10
|
import type { Computer, ProvisionedComputer } from './types.js';
|
|
8
11
|
|
|
9
12
|
interface Prompt {
|
|
@@ -26,11 +29,12 @@ Usage:
|
|
|
26
29
|
bun run src/console.ts agents list [--json]
|
|
27
30
|
bun run src/console.ts agents onboard [--interactive] [--key <agent-key>] [--name <display-name>] [--runtime codex] [--computer-id <machine>]
|
|
28
31
|
bun run src/console.ts agents create --key <agent-key> --name <display-name> [--runtime neeko|coco|coco-stream-json|codex] [--desc <description>]
|
|
29
|
-
bun run src/console.ts agents update --key <agent-key> --runtime neeko|coco|coco-stream-json|codex
|
|
32
|
+
bun run src/console.ts agents update [--interactive] --key <agent-key> [--runtime neeko|coco|coco-stream-json|codex]
|
|
33
|
+
[--lark-app-id <id> --lark-app-secret <secret>] [--lark-label <name>] [--lark-config <path>] [--rebind-lark] [--unbind-lark] [--no-reload]
|
|
30
34
|
bun run src/console.ts lark-users list [--json]
|
|
31
35
|
bun run src/console.ts lark-users add [--interactive] --user-id <union-id> [--name <display-name>]
|
|
32
36
|
bun run src/console.ts lark-users delete --user-id <union-id>
|
|
33
|
-
bun run src/console.ts lark <
|
|
37
|
+
bun run src/console.ts lark <list|daemon|events|send> [flags]
|
|
34
38
|
|
|
35
39
|
Environment:
|
|
36
40
|
PAL_SERVER=${defaultServerUrl()}
|
|
@@ -44,6 +48,83 @@ async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
|
44
48
|
return payload.data as T;
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
async function reloadLarkIntegration(serverUrl: string): Promise<void> {
|
|
52
|
+
const response = await fetch(`${serverUrl.replace(/\/$/, '')}/api/lark/reload`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'content-type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({}),
|
|
56
|
+
});
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const text = await response.text();
|
|
59
|
+
console.warn(`Lark bot config saved, but server reload failed (${response.status}): ${text}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log('Lark integration reloaded.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function resolveBotInfoForConsole(appId: string, appSecret: string): Promise<LarkBotInfoResult> {
|
|
66
|
+
if (process.env.NODE_ENV === 'test' && process.env.PAL_TEST_LARK_BOT_OPEN_ID) {
|
|
67
|
+
return { ok: true, openId: process.env.PAL_TEST_LARK_BOT_OPEN_ID };
|
|
68
|
+
}
|
|
69
|
+
return resolveLarkBotInfo(appId, appSecret);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function registerLarkAppForConsole(): Promise<LarkRegistrationComplete | null> {
|
|
73
|
+
if (process.env.NODE_ENV === 'test' && process.env.PAL_TEST_LARK_REGISTRATION_APP_ID && process.env.PAL_TEST_LARK_REGISTRATION_APP_SECRET) {
|
|
74
|
+
console.log('Create a Feishu app by scanning this QR code or opening the link:');
|
|
75
|
+
console.log('https://example.test/lark-registration');
|
|
76
|
+
return {
|
|
77
|
+
appId: process.env.PAL_TEST_LARK_REGISTRATION_APP_ID,
|
|
78
|
+
appSecret: process.env.PAL_TEST_LARK_REGISTRATION_APP_SECRET,
|
|
79
|
+
tenantBrand: 'feishu',
|
|
80
|
+
userOpenId: process.env.PAL_TEST_LARK_REGISTRATION_USER_OPEN_ID,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return registerLarkAppFromDeviceFlow({ log: console });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function larkConfigPath(flags: Record<string, string | boolean>): string {
|
|
87
|
+
return flag(flags, 'lark-config') ?? flag(flags, 'config') ?? defaultLarkConfigPath();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function unbindLarkBotFromAgent(serverClient: LockClient, flags: Record<string, string | boolean>, agentKey: string): Promise<void> {
|
|
91
|
+
const configPath = larkConfigPath(flags);
|
|
92
|
+
const store = loadLarkCredentials(configPath);
|
|
93
|
+
const result = unbindCredentialAgent(store, agentKey);
|
|
94
|
+
if (!result.changed) {
|
|
95
|
+
console.log(`Agent ${agentKey} has no Lark bot binding.`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
saveLarkCredentials(result.store, configPath);
|
|
99
|
+
console.log(`Lark bot ${result.unbound!.appId} unbound from agent ${agentKey}.`);
|
|
100
|
+
if (!boolFlag(flags, 'no-reload')) await reloadLarkIntegration(serverClient.baseUrl);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function bindLarkBotToAgent(serverClient: LockClient, flags: Record<string, string | boolean>, agentKey: string): Promise<void> {
|
|
104
|
+
const appId = flag(flags, 'lark-app-id') ?? flag(flags, 'app-id');
|
|
105
|
+
const appSecret = flag(flags, 'lark-app-secret') ?? flag(flags, 'app-secret');
|
|
106
|
+
if (!appId && !appSecret) return;
|
|
107
|
+
if (!appId || !appSecret) throw new Error('--lark-app-id and --lark-app-secret are required together');
|
|
108
|
+
|
|
109
|
+
const botInfo = await resolveBotInfoForConsole(appId, appSecret);
|
|
110
|
+
if (!botInfo.ok) throw new Error(`could not resolve Lark bot open_id (${botInfo.error}): ${botInfo.message}`);
|
|
111
|
+
|
|
112
|
+
const configPath = larkConfigPath(flags);
|
|
113
|
+
const store = loadLarkCredentials(configPath);
|
|
114
|
+
const result = upsertCredential(store, {
|
|
115
|
+
appId,
|
|
116
|
+
appSecret,
|
|
117
|
+
label: flag(flags, 'lark-label') ?? flag(flags, 'label'),
|
|
118
|
+
agent: agentKey,
|
|
119
|
+
botOpenId: botInfo.openId,
|
|
120
|
+
}, { rebind: boolFlag(flags, 'rebind-lark') });
|
|
121
|
+
saveLarkCredentials(result.store, configPath);
|
|
122
|
+
|
|
123
|
+
const moved = result.unbound.length > 0 ? `; moved from ${result.unbound.map((bot) => bot.appId).join(', ')}` : '';
|
|
124
|
+
console.log(`Lark bot ${appId} bound to agent ${agentKey}${moved}.`);
|
|
125
|
+
if (!boolFlag(flags, 'no-reload')) await reloadLarkIntegration(serverClient.baseUrl);
|
|
126
|
+
}
|
|
127
|
+
|
|
47
128
|
function printJson(value: unknown): void {
|
|
48
129
|
console.log(JSON.stringify(value, null, 2));
|
|
49
130
|
}
|
|
@@ -132,12 +213,35 @@ async function askComputerId(prompt: Prompt, computers: Computer[]): Promise<str
|
|
|
132
213
|
}
|
|
133
214
|
}
|
|
134
215
|
|
|
216
|
+
async function listAgentRecords(serverClient: LockClient): Promise<Array<Record<string, unknown>>> {
|
|
217
|
+
const response = await fetch(`${serverClient.baseUrl}/api/agents`);
|
|
218
|
+
const payload = await response.json() as { data?: { agents: Array<Record<string, unknown>> }; message?: string };
|
|
219
|
+
if (!response.ok) throw new Error(payload.message ?? `list failed: ${response.status}`);
|
|
220
|
+
return payload.data?.agents ?? [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function askAgentKey(prompt: Prompt, agents: Array<Record<string, unknown>>, defaultValue = ''): Promise<string> {
|
|
224
|
+
if (agents.length === 0) throw new Error('No agents found. Set up an agent first with "bun run console -- agents onboard".');
|
|
225
|
+
|
|
226
|
+
console.log('Agents:');
|
|
227
|
+
agents.forEach((agent, index) => {
|
|
228
|
+
console.log(` ${index + 1}. ${agent.display_name} (${agent.agent_key}) runtime=${agent.runtime ?? '-'}`);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
while (true) {
|
|
232
|
+
const answer = await ask(prompt, 'Choose agent', defaultValue);
|
|
233
|
+
const selected = agents[Number(answer) - 1] ?? agents.find((agent) => agent.agent_key === answer);
|
|
234
|
+
if (selected?.agent_key) return String(selected.agent_key);
|
|
235
|
+
console.log('Choose a listed number or agent key.');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
135
239
|
async function collectComputerOnboardInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ name: string; serverUrl: string; packageName: string }> {
|
|
136
240
|
const hasAnyProvisionFlag = Boolean(flag(flags, 'name') || flag(flags, 'server-url') || flag(flags, 'package-name'));
|
|
137
241
|
const interactive = boolFlag(flags, 'interactive') || (!hasAnyProvisionFlag && process.stdin.isTTY === true);
|
|
138
242
|
const defaultName = flag(flags, 'name') ?? 'Local computer';
|
|
139
243
|
const defaultServerUrl = flag(flags, 'server-url') ?? serverClient.baseUrl;
|
|
140
|
-
const defaultPackageName = flag(flags, 'package-name') ?? '@
|
|
244
|
+
const defaultPackageName = flag(flags, 'package-name') ?? '@controlflow-ai/daemon@latest';
|
|
141
245
|
|
|
142
246
|
if (!interactive) {
|
|
143
247
|
return {
|
|
@@ -176,7 +280,56 @@ function printProvisionedComputer(provisioned: ProvisionedComputer): void {
|
|
|
176
280
|
console.log(provisioned.command);
|
|
177
281
|
}
|
|
178
282
|
|
|
179
|
-
async function
|
|
283
|
+
async function collectLarkBindFlags(prompt: Prompt, flags: Record<string, string | boolean>, agentKey: string, existingAppId?: string): Promise<Record<string, string | boolean>> {
|
|
284
|
+
const configPath = larkConfigPath(flags);
|
|
285
|
+
console.log('Lark bot setup');
|
|
286
|
+
console.log(' 1. Scan or open link to create app');
|
|
287
|
+
console.log(' 2. Paste App ID / App Secret');
|
|
288
|
+
const method = await ask(prompt, 'Setup method', '1');
|
|
289
|
+
|
|
290
|
+
let appId = flag(flags, 'lark-app-id') ?? flag(flags, 'app-id');
|
|
291
|
+
let appSecret = flag(flags, 'lark-app-secret') ?? flag(flags, 'app-secret');
|
|
292
|
+
let scannedUserOpenId: string | undefined;
|
|
293
|
+
if (method !== '2' && (!appId || !appSecret)) {
|
|
294
|
+
const registration = await registerLarkAppForConsole().catch((error) => {
|
|
295
|
+
console.warn(`App registration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
296
|
+
return null;
|
|
297
|
+
});
|
|
298
|
+
if (registration?.tenantBrand === 'lark') {
|
|
299
|
+
throw new Error('Lark international tenants are not supported yet; use a Feishu tenant');
|
|
300
|
+
}
|
|
301
|
+
if (registration) {
|
|
302
|
+
appId = registration.appId;
|
|
303
|
+
appSecret = registration.appSecret;
|
|
304
|
+
scannedUserOpenId = registration.userOpenId;
|
|
305
|
+
console.log(`App created: ${appId}`);
|
|
306
|
+
} else {
|
|
307
|
+
console.warn('Falling back to manual App ID / App Secret entry.');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
appId = appId || await askRequired(prompt, 'Lark App ID', existingAppId ?? '');
|
|
312
|
+
appSecret = appSecret || await askRequired(prompt, 'Lark App Secret');
|
|
313
|
+
const label = await ask(prompt, 'Bot label (optional)', flag(flags, 'lark-label') ?? flag(flags, 'label') ?? '');
|
|
314
|
+
if (scannedUserOpenId) {
|
|
315
|
+
console.log(`Scanner open_id ${scannedUserOpenId} was detected; add it under Lark authorized users if this user should operate Pal.`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const isChangingBot = Boolean(existingAppId && existingAppId !== appId);
|
|
319
|
+
const rebind = boolFlag(flags, 'rebind-lark') || (isChangingBot && await askYesNo(prompt, `Move ${agentKey} from ${existingAppId} to ${appId}?`, false));
|
|
320
|
+
if (isChangingBot && !rebind) throw new Error('Lark bot rebind cancelled');
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
...flags,
|
|
324
|
+
'lark-app-id': appId,
|
|
325
|
+
'lark-app-secret': appSecret,
|
|
326
|
+
'lark-config': configPath,
|
|
327
|
+
...(label ? { 'lark-label': label } : {}),
|
|
328
|
+
...(rebind ? { 'rebind-lark': true } : {}),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function collectOnboardInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ agentKey: string; displayName: string; runtime: string; desc: string | null; computerId: string | undefined; larkFlags?: Record<string, string | boolean> }> {
|
|
180
333
|
const hasRequiredFlags = Boolean(flag(flags, 'key') && flag(flags, 'name'));
|
|
181
334
|
const interactive = boolFlag(flags, 'interactive') || (!hasRequiredFlags && process.stdin.isTTY === true);
|
|
182
335
|
if (!interactive) {
|
|
@@ -187,6 +340,7 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
|
|
|
187
340
|
runtime: flag(flags, 'runtime') ?? 'codex',
|
|
188
341
|
desc: flag(flags, 'desc') ?? null,
|
|
189
342
|
computerId: flag(flags, 'computer-id'),
|
|
343
|
+
larkFlags: flag(flags, 'lark-app-id') || flag(flags, 'app-id') ? flags : undefined,
|
|
190
344
|
};
|
|
191
345
|
}
|
|
192
346
|
|
|
@@ -210,12 +364,68 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
|
|
|
210
364
|
console.log(` description: ${desc || '-'}`);
|
|
211
365
|
console.log(` computer_id: ${computerId || '-'}`);
|
|
212
366
|
if (!await askYesNo(prompt, 'Create/update this agent?', true)) throw new Error('onboarding cancelled');
|
|
367
|
+
const shouldSetupLark = await askYesNo(prompt, 'Set up a Lark bot for this agent?', false);
|
|
368
|
+
const larkFlags = shouldSetupLark ? await collectLarkBindFlags(prompt, flags, agentKey) : undefined;
|
|
213
369
|
return {
|
|
214
370
|
agentKey,
|
|
215
371
|
displayName,
|
|
216
372
|
runtime,
|
|
217
373
|
desc: desc || null,
|
|
218
374
|
computerId: computerId || undefined,
|
|
375
|
+
larkFlags,
|
|
376
|
+
};
|
|
377
|
+
} finally {
|
|
378
|
+
prompt.close();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function collectAgentUpdateInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ agentKey: string; flags: Record<string, string | boolean> }> {
|
|
383
|
+
if (flag(flags, 'runtime')) throw new Error('agents update --interactive currently supports only Lark bot binding or unbinding');
|
|
384
|
+
|
|
385
|
+
const prompt = await createPrompt();
|
|
386
|
+
try {
|
|
387
|
+
console.log('Agent Lark settings');
|
|
388
|
+
const agents = await listAgentRecords(serverClient);
|
|
389
|
+
const agentKey = await askAgentKey(prompt, agents, flag(flags, 'key') ?? (agents.length === 1 ? '1' : ''));
|
|
390
|
+
const configPath = larkConfigPath(flags);
|
|
391
|
+
const store = loadLarkCredentials(configPath);
|
|
392
|
+
const existing = findCredentialByAgent(store, agentKey);
|
|
393
|
+
if (existing) {
|
|
394
|
+
console.log(`Current Lark bot: ${existing.label ?? existing.appId} (${existing.appId})`);
|
|
395
|
+
} else {
|
|
396
|
+
console.log('Current Lark bot: -');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const defaultAction = existing ? '1' : '1';
|
|
400
|
+
console.log('Lark action:');
|
|
401
|
+
console.log(` 1. ${existing ? 'Bind/rebind bot' : 'Bind bot'}`);
|
|
402
|
+
if (existing) console.log(' 2. Unbind bot');
|
|
403
|
+
const action = await ask(prompt, 'Choose action', defaultAction);
|
|
404
|
+
if (existing && (action === '2' || action.toLowerCase() === 'unbind')) {
|
|
405
|
+
if (!await askYesNo(prompt, `Unbind ${existing.appId} from ${agentKey}?`, false)) throw new Error('Lark bot unbind cancelled');
|
|
406
|
+
return {
|
|
407
|
+
agentKey,
|
|
408
|
+
flags: {
|
|
409
|
+
...flags,
|
|
410
|
+
'lark-config': configPath,
|
|
411
|
+
'unbind-lark': true,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const larkFlags = await collectLarkBindFlags(prompt, flags, agentKey, existing?.appId);
|
|
417
|
+
|
|
418
|
+
console.log('');
|
|
419
|
+
console.log('Summary:');
|
|
420
|
+
console.log(` agent_key: ${agentKey}`);
|
|
421
|
+
console.log(` lark_app_id: ${flag(larkFlags, 'lark-app-id')}`);
|
|
422
|
+
console.log(` lark_label: ${flag(larkFlags, 'lark-label') || '-'}`);
|
|
423
|
+
console.log(` rebind: ${boolFlag(larkFlags, 'rebind-lark') ? 'yes' : 'no'}`);
|
|
424
|
+
if (!await askYesNo(prompt, 'Bind this Lark bot?', true)) throw new Error('Lark bot binding cancelled');
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
agentKey,
|
|
428
|
+
flags: larkFlags,
|
|
219
429
|
};
|
|
220
430
|
} finally {
|
|
221
431
|
prompt.close();
|
|
@@ -366,10 +576,7 @@ async function main(): Promise<void> {
|
|
|
366
576
|
const sub = args.values[0];
|
|
367
577
|
|
|
368
578
|
if (sub === 'list') {
|
|
369
|
-
const
|
|
370
|
-
const payload = await response.json() as { data?: { agents: Array<Record<string, unknown>> }; message?: string };
|
|
371
|
-
if (!response.ok) throw new Error(payload.message ?? `list failed: ${response.status}`);
|
|
372
|
-
const agents = payload.data?.agents ?? [];
|
|
579
|
+
const agents = await listAgentRecords(serverClient);
|
|
373
580
|
if (args.flags.json) {
|
|
374
581
|
printJson(agents);
|
|
375
582
|
} else {
|
|
@@ -405,7 +612,7 @@ async function main(): Promise<void> {
|
|
|
405
612
|
}
|
|
406
613
|
|
|
407
614
|
if (sub === 'onboard') {
|
|
408
|
-
const { agentKey, displayName, runtime, desc, computerId } = await collectOnboardInput(serverClient, args.flags);
|
|
615
|
+
const { agentKey, displayName, runtime, desc, computerId, larkFlags } = await collectOnboardInput(serverClient, args.flags);
|
|
409
616
|
if (!agentKey) throw new Error('agent key is required');
|
|
410
617
|
if (!displayName) throw new Error('display name is required');
|
|
411
618
|
|
|
@@ -422,24 +629,41 @@ async function main(): Promise<void> {
|
|
|
422
629
|
});
|
|
423
630
|
const payload = await response.json() as { data?: Record<string, unknown>; message?: string };
|
|
424
631
|
if (!response.ok) throw new Error(payload.message ?? `onboard failed: ${response.status}`);
|
|
632
|
+
if (larkFlags) await bindLarkBotToAgent(serverClient, larkFlags, agentKey);
|
|
425
633
|
printJson(payload.data);
|
|
426
634
|
return;
|
|
427
635
|
}
|
|
428
636
|
|
|
429
637
|
if (sub === 'update') {
|
|
430
|
-
const
|
|
638
|
+
const updateInput = boolFlag(args.flags, 'interactive')
|
|
639
|
+
? await collectAgentUpdateInput(serverClient, args.flags)
|
|
640
|
+
: { agentKey: flag(args.flags, 'key') ?? '', flags: args.flags };
|
|
641
|
+
const agentKey = updateInput.agentKey;
|
|
642
|
+
const updateFlags = updateInput.flags;
|
|
431
643
|
const runtime = flag(args.flags, 'runtime');
|
|
432
644
|
if (!agentKey) throw new Error('--key is required');
|
|
433
|
-
|
|
645
|
+
const hasLarkBinding = Boolean(flag(updateFlags, 'lark-app-id') || flag(updateFlags, 'app-id') || flag(updateFlags, 'lark-app-secret') || flag(updateFlags, 'app-secret'));
|
|
646
|
+
const hasLarkUnbind = boolFlag(updateFlags, 'unbind-lark');
|
|
647
|
+
if (!runtime && !hasLarkBinding && !hasLarkUnbind) throw new Error('--runtime, --lark-app-id/--lark-app-secret, or --unbind-lark is required');
|
|
648
|
+
|
|
649
|
+
let agent: Record<string, unknown> | undefined;
|
|
650
|
+
if (runtime) {
|
|
651
|
+
const response = await fetch(`${serverClient.baseUrl}/api/agents/${encodeURIComponent(agentKey)}`, {
|
|
652
|
+
method: 'PATCH',
|
|
653
|
+
headers: { 'content-type': 'application/json' },
|
|
654
|
+
body: JSON.stringify({ runtime }),
|
|
655
|
+
});
|
|
656
|
+
const payload = await response.json() as { data?: { agent: Record<string, unknown> }; message?: string };
|
|
657
|
+
if (!response.ok) throw new Error(payload.message ?? `update failed: ${response.status}`);
|
|
658
|
+
agent = payload.data?.agent;
|
|
659
|
+
} else {
|
|
660
|
+
agent = (await listAgentRecords(serverClient)).find((candidate) => candidate.agent_key === agentKey);
|
|
661
|
+
if (!agent) throw new Error(`agent ${agentKey} not found`);
|
|
662
|
+
}
|
|
434
663
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
body: JSON.stringify({ runtime }),
|
|
439
|
-
});
|
|
440
|
-
const payload = await response.json() as { data?: { agent: Record<string, unknown> }; message?: string };
|
|
441
|
-
if (!response.ok) throw new Error(payload.message ?? `update failed: ${response.status}`);
|
|
442
|
-
printJson(payload.data?.agent);
|
|
664
|
+
if (hasLarkUnbind) await unbindLarkBotFromAgent(serverClient, updateFlags, agentKey);
|
|
665
|
+
else await bindLarkBotToAgent(serverClient, updateFlags, agentKey);
|
|
666
|
+
printJson(agent);
|
|
443
667
|
return;
|
|
444
668
|
}
|
|
445
669
|
|
package/src/daemon.ts
CHANGED
|
@@ -153,12 +153,22 @@ async function executeRun(client: LockClient, delivery: MessageDelivery, message
|
|
|
153
153
|
console.log(`${logPrefix} run=${run.id} status=${run.status}`);
|
|
154
154
|
|
|
155
155
|
const { runAgentRuntime } = await import('./agent-runtime.js');
|
|
156
|
-
const
|
|
157
|
-
.then((result) => result
|
|
156
|
+
const roomSnapshot = await client.listRoomMembers(message.chat_id)
|
|
157
|
+
.then((result) => result)
|
|
158
158
|
.catch((error) => {
|
|
159
159
|
console.warn(`${logPrefix} room participant snapshot unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
160
|
-
return
|
|
160
|
+
return null;
|
|
161
161
|
});
|
|
162
|
+
const roomParticipants = roomSnapshot?.participants ?? [];
|
|
163
|
+
const roomProject = roomSnapshot?.room.project_id ? {
|
|
164
|
+
id: roomSnapshot.room.project_id,
|
|
165
|
+
name: roomSnapshot.room.project_name ?? roomSnapshot.room.project_id,
|
|
166
|
+
rootPath: roomSnapshot.room.project_root_path ?? '',
|
|
167
|
+
computerId: roomSnapshot.room.project_computer_id ?? '',
|
|
168
|
+
computerName: roomSnapshot.room.project_computer_name,
|
|
169
|
+
} : null;
|
|
170
|
+
const projectAccessible = Boolean(roomProject && options.computerId && roomProject.computerId === options.computerId);
|
|
171
|
+
const effectiveProjectCwd = projectAccessible && roomProject?.rootPath ? roomProject.rootPath : options.projectCwd;
|
|
162
172
|
const recentMessages = await client.getMessages(new URLSearchParams({
|
|
163
173
|
chat_id: message.chat_id,
|
|
164
174
|
after: String(Math.max(0, message.id - 50)),
|
|
@@ -176,7 +186,12 @@ async function executeRun(client: LockClient, delivery: MessageDelivery, message
|
|
|
176
186
|
message,
|
|
177
187
|
cwd: options.agentHome,
|
|
178
188
|
agentHome: options.agentHome,
|
|
179
|
-
projectCwd:
|
|
189
|
+
projectCwd: effectiveProjectCwd,
|
|
190
|
+
projectContext: roomProject ? {
|
|
191
|
+
...roomProject,
|
|
192
|
+
accessible: projectAccessible,
|
|
193
|
+
currentComputerId: options.computerId,
|
|
194
|
+
} : undefined,
|
|
180
195
|
extraArgs: options.extraArgs,
|
|
181
196
|
localDaemonUrl: options.localDaemonUrl,
|
|
182
197
|
localDaemonToken: options.localDaemonToken,
|
|
@@ -377,6 +392,7 @@ async function reconcileManagedAgents(input: {
|
|
|
377
392
|
computerId: string;
|
|
378
393
|
serverUrl: string;
|
|
379
394
|
defaultCwd: string;
|
|
395
|
+
localUrl?: string;
|
|
380
396
|
}): Promise<void> {
|
|
381
397
|
const desiredAssignedAgents = new Set(input.assignments.map((assignment) => assignment.agent));
|
|
382
398
|
|
|
@@ -403,6 +419,7 @@ async function reconcileManagedAgents(input: {
|
|
|
403
419
|
await input.client.registerDaemon({
|
|
404
420
|
id: input.daemonId,
|
|
405
421
|
name: input.computerId,
|
|
422
|
+
local_url: input.localUrl,
|
|
406
423
|
server_url: input.serverUrl,
|
|
407
424
|
agents: daemonAgentPayload(input.managedAgents),
|
|
408
425
|
});
|
|
@@ -473,6 +490,8 @@ async function main(): Promise<void> {
|
|
|
473
490
|
|
|
474
491
|
const daemonAuth = { computer_id: connected.computer.id, connection_id: connected.connection.id, token: connected.token };
|
|
475
492
|
const client = new LockClient(serverUrl, daemonAuth);
|
|
493
|
+
const localServer = startLocalApi({ host: localHost, port: localPort, serverUrl, token: localToken, controlToken: connected.local_control_token, daemonAuth });
|
|
494
|
+
const localUrl = `http://${localServer.hostname}:${localServer.port}`;
|
|
476
495
|
await reconcileManagedAgents({
|
|
477
496
|
assignments: connected.agents ?? [],
|
|
478
497
|
explicitAgents,
|
|
@@ -483,6 +502,7 @@ async function main(): Promise<void> {
|
|
|
483
502
|
computerId: connected.computer.id,
|
|
484
503
|
serverUrl,
|
|
485
504
|
defaultCwd: cwd,
|
|
505
|
+
localUrl,
|
|
486
506
|
});
|
|
487
507
|
if (managedAgents.size === 0) {
|
|
488
508
|
console.log(`[daemon] no agents currently assigned to computer ${connected.computer.id}; waiting for assignments`);
|
|
@@ -491,8 +511,6 @@ async function main(): Promise<void> {
|
|
|
491
511
|
let connectionRevoked = false;
|
|
492
512
|
const heartbeatMs = numberFlag(args.flags, 'heartbeat-interval', 5000)!;
|
|
493
513
|
|
|
494
|
-
const localServer = startLocalApi({ host: localHost, port: localPort, serverUrl, token: localToken, daemonAuth });
|
|
495
|
-
|
|
496
514
|
console.log(`pal daemon computer=${connected.computer.id} connection=${connected.connection.id} agents=${Array.from(managedAgents.values()).map((managed) => `${managed.agent}:${managed.runtimeProvider}`).join(',') || 'none'} server=${serverUrl}`);
|
|
497
515
|
console.log(`local api=http://${localServer.hostname}:${localServer.port} token=${localToken ? 'set' : 'missing'}`);
|
|
498
516
|
console.log(`state=${statePath} lastSeenId=${state.lastSeenId}`);
|
|
@@ -525,6 +543,7 @@ async function main(): Promise<void> {
|
|
|
525
543
|
computerId: connected.computer.id,
|
|
526
544
|
serverUrl,
|
|
527
545
|
defaultCwd: cwd,
|
|
546
|
+
localUrl,
|
|
528
547
|
});
|
|
529
548
|
writeState(statePath, state);
|
|
530
549
|
} catch (error) {
|
package/src/db.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Database, type SQLQueryBindings } from 'bun:sqlite';
|
|
2
2
|
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
3
4
|
import { ensureParentDir } from './config.js';
|
|
4
5
|
import { runMigrations } from './migrations.js';
|
|
5
6
|
import { artifactExpiry, generateArtifactToken, hashArtifactToken, validateArtifactContent } from './artifacts.js';
|
|
6
7
|
import { palIdentityHandle } from './provider-identity.js';
|
|
7
|
-
import type { AgentDefinition, AgentRun, AgentSession, AgentValidationResult, Artifact, ArtifactMetadata, ChannelAccount, ChannelConversation, ChannelMessageMapping, ChannelOutboxRecord, Chat, ChatKind, Computer, ComputerAgentAssignment, ComputerConnection, DaemonInstance, LarkAuthorizedUser, LarkGroupRoomMapping, LockProviderEvidence, LockTranscriptMessage, Message, MessageDelivery, MessageType, AgentRoomSubscription, AgentRoomSubscriptionMode, PendingInboundEvent, ProvisionedComputer, ProviderExternalType, ProviderIdentityBinding, PalIdentity, RoomChannel, RoomParticipant, RoomParticipantKind, RoomParticipantSource, RoomProvider, RunAction, RunStatus, TranscriptReadModel, WorkbenchArtifact } from './types.js';
|
|
8
|
+
import type { AgentDefinition, AgentRun, AgentSession, AgentValidationResult, Artifact, ArtifactMetadata, ChannelAccount, ChannelConversation, ChannelMessageMapping, ChannelOutboxRecord, Chat, ChatKind, Computer, ComputerAgentAssignment, ComputerConnection, DaemonInstance, LarkAuthorizedUser, LarkGroupRoomMapping, LockProviderEvidence, LockTranscriptMessage, Message, MessageDelivery, MessageType, AgentRoomSubscription, AgentRoomSubscriptionMode, PendingInboundEvent, Project, ProvisionedComputer, ProviderExternalType, ProviderIdentityBinding, PalIdentity, RoomChannel, RoomParticipant, RoomParticipantKind, RoomParticipantSource, RoomProvider, RunAction, RunStatus, TranscriptReadModel, WorkbenchArtifact } from './types.js';
|
|
8
9
|
|
|
9
10
|
export const ALL_AGENTS_MENTION = '__pal_all_agents__';
|
|
10
11
|
|
|
@@ -85,6 +86,7 @@ export interface ConnectComputerResult {
|
|
|
85
86
|
computer: Computer;
|
|
86
87
|
connection: ComputerConnection;
|
|
87
88
|
token: string;
|
|
89
|
+
localControlToken: string;
|
|
88
90
|
daemon: DaemonInstance;
|
|
89
91
|
agents: ComputerAgentAssignment[];
|
|
90
92
|
}
|
|
@@ -356,6 +358,19 @@ function rowToComputerAgentAssignment(row: Record<string, unknown>): ComputerAge
|
|
|
356
358
|
};
|
|
357
359
|
}
|
|
358
360
|
|
|
361
|
+
function rowToProject(row: Record<string, unknown>): Project {
|
|
362
|
+
return {
|
|
363
|
+
id: String(row.id),
|
|
364
|
+
name: String(row.name),
|
|
365
|
+
computer_id: String(row.computer_id),
|
|
366
|
+
computer_name: row.computer_name === null || row.computer_name === undefined ? null : String(row.computer_name),
|
|
367
|
+
root_path: String(row.root_path),
|
|
368
|
+
room_count: Number(row.room_count ?? 0),
|
|
369
|
+
created_at: String(row.created_at),
|
|
370
|
+
updated_at: String(row.updated_at),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
359
374
|
function rowToMessage(row: Record<string, unknown>): Message {
|
|
360
375
|
return {
|
|
361
376
|
id: Number(row.id),
|
|
@@ -812,6 +827,68 @@ export class MessageStore {
|
|
|
812
827
|
return this.getChatById(id)!;
|
|
813
828
|
}
|
|
814
829
|
|
|
830
|
+
createProject(input: { name: string; computerId: string; rootPath: string }): Project {
|
|
831
|
+
const name = input.name.trim();
|
|
832
|
+
const computerId = input.computerId.trim();
|
|
833
|
+
const rootPath = input.rootPath.trim();
|
|
834
|
+
if (!name) throw new Error('project name is required');
|
|
835
|
+
if (!computerId) throw new Error('computer_id is required');
|
|
836
|
+
if (!rootPath) throw new Error('root_path is required');
|
|
837
|
+
if (!isAbsolute(rootPath)) throw new Error('root_path must be absolute');
|
|
838
|
+
const computer = this.getComputer(computerId);
|
|
839
|
+
if (!computer) throw new Error(`computer ${computerId} not found`);
|
|
840
|
+
|
|
841
|
+
const id = crypto.randomUUID();
|
|
842
|
+
this.db.query(`
|
|
843
|
+
INSERT INTO projects (id, name, computer_id, root_path, created_at, updated_at)
|
|
844
|
+
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
845
|
+
`).run(id, name, computerId, rootPath);
|
|
846
|
+
return this.getProject(id)!;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
getProject(id: string): Project | null {
|
|
850
|
+
const row = this.db.query(`
|
|
851
|
+
SELECT p.*, c.name AS computer_name, COUNT(ch.id) AS room_count
|
|
852
|
+
FROM projects p
|
|
853
|
+
LEFT JOIN computers c ON c.id = p.computer_id
|
|
854
|
+
LEFT JOIN chats ch ON ch.project_id = p.id
|
|
855
|
+
WHERE p.id = ?
|
|
856
|
+
GROUP BY p.id
|
|
857
|
+
`).get(id.trim()) as Record<string, unknown> | null;
|
|
858
|
+
return row ? rowToProject(row) : null;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
listProjects(limit = 50): Project[] {
|
|
862
|
+
const rows = this.db.query(`
|
|
863
|
+
SELECT p.*, c.name AS computer_name, COUNT(ch.id) AS room_count
|
|
864
|
+
FROM projects p
|
|
865
|
+
LEFT JOIN computers c ON c.id = p.computer_id
|
|
866
|
+
LEFT JOIN chats ch ON ch.project_id = p.id
|
|
867
|
+
GROUP BY p.id
|
|
868
|
+
ORDER BY p.updated_at DESC, p.created_at DESC
|
|
869
|
+
LIMIT ?
|
|
870
|
+
`).all(limitValue(limit, 50)) as Record<string, unknown>[];
|
|
871
|
+
return rows.map(rowToProject);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
createProjectRoom(input: { projectId: string; name: string; kind?: ChatKind }): Chat {
|
|
875
|
+
const project = this.getProject(input.projectId);
|
|
876
|
+
if (!project) throw new Error(`project ${input.projectId} was not found`);
|
|
877
|
+
const kind = input.kind ?? 'group';
|
|
878
|
+
if (kind !== 'group' && kind !== 'dm') throw new Error('kind must be group or dm');
|
|
879
|
+
const chatName = normalizeChatName(input.name);
|
|
880
|
+
if (!chatName) throw new Error('room name is required');
|
|
881
|
+
if (this.getChatByName(chatName)) throw new Error(`room ${chatName} already exists`);
|
|
882
|
+
|
|
883
|
+
const id = crypto.randomUUID();
|
|
884
|
+
this.db.query(`
|
|
885
|
+
INSERT INTO chats (id, name, kind, provider, capabilities_json, project_id)
|
|
886
|
+
VALUES (?, ?, ?, 'web', ?, ?)
|
|
887
|
+
`).run(id, chatName, kind, kind === 'dm' ? '{"topics":"unsupported"}' : '{"topics":"native"}', project.id);
|
|
888
|
+
this.db.query(`UPDATE projects SET updated_at = datetime('now') WHERE id = ?`).run(project.id);
|
|
889
|
+
return this.getChatById(id)!;
|
|
890
|
+
}
|
|
891
|
+
|
|
815
892
|
updateRoomDisplayName(roomId: string, displayName: string | null): Chat {
|
|
816
893
|
const room = this.getChatById(roomId);
|
|
817
894
|
if (!room) throw new Error(`room ${roomId} was not found`);
|
|
@@ -2216,6 +2293,7 @@ export class MessageStore {
|
|
|
2216
2293
|
|
|
2217
2294
|
const connectionId = crypto.randomUUID();
|
|
2218
2295
|
const token = generateConnectionToken();
|
|
2296
|
+
const localControlToken = generateConnectionToken();
|
|
2219
2297
|
const tokenHash = hashSecret(token);
|
|
2220
2298
|
const revokedRows = this.db.query(`
|
|
2221
2299
|
SELECT id FROM computer_connections
|
|
@@ -2270,9 +2348,9 @@ export class MessageStore {
|
|
|
2270
2348
|
}
|
|
2271
2349
|
}
|
|
2272
2350
|
this.db.query(`
|
|
2273
|
-
INSERT INTO computer_connections (id, computer_id, token_hash, epoch, status, connected_at, last_heartbeat_at)
|
|
2274
|
-
VALUES (?, ?, ?, ?, 'active', datetime('now'), datetime('now'))
|
|
2275
|
-
`).run(connectionId, computerId, tokenHash, epoch);
|
|
2351
|
+
INSERT INTO computer_connections (id, computer_id, token_hash, local_control_token, epoch, status, connected_at, last_heartbeat_at)
|
|
2352
|
+
VALUES (?, ?, ?, ?, ?, 'active', datetime('now'), datetime('now'))
|
|
2353
|
+
`).run(connectionId, computerId, tokenHash, localControlToken, epoch);
|
|
2276
2354
|
this.db.query(`
|
|
2277
2355
|
INSERT INTO daemon_instances (id, name, host, local_url, server_url, status, last_seen_at)
|
|
2278
2356
|
VALUES (?, ?, ?, ?, ?, 'online', datetime('now'))
|
|
@@ -2303,11 +2381,30 @@ export class MessageStore {
|
|
|
2303
2381
|
computer: this.getComputer(computerId)!,
|
|
2304
2382
|
connection: this.getComputerConnection(connectionId)!,
|
|
2305
2383
|
token,
|
|
2384
|
+
localControlToken,
|
|
2306
2385
|
daemon: this.getDaemon(connectionId)!,
|
|
2307
2386
|
agents: this.listComputerAgentAssignments(computerId),
|
|
2308
2387
|
};
|
|
2309
2388
|
}
|
|
2310
2389
|
|
|
2390
|
+
getComputerLocalControl(computerId: string): { computer: Computer; connection: ComputerConnection; daemon: DaemonInstance; token: string } | null {
|
|
2391
|
+
const computer = this.getComputer(computerId);
|
|
2392
|
+
if (!computer || computer.status !== 'online' || !computer.active_connection_id) return null;
|
|
2393
|
+
const connection = this.getComputerConnection(computer.active_connection_id);
|
|
2394
|
+
if (!connection || connection.status !== 'active') return null;
|
|
2395
|
+
const row = this.db.query(`
|
|
2396
|
+
SELECT local_control_token
|
|
2397
|
+
FROM computer_connections
|
|
2398
|
+
WHERE id = ? AND computer_id = ? AND status = 'active'
|
|
2399
|
+
LIMIT 1
|
|
2400
|
+
`).get(connection.id, computer.id) as { local_control_token: string | null } | null;
|
|
2401
|
+
const token = row?.local_control_token?.trim();
|
|
2402
|
+
if (!token) return null;
|
|
2403
|
+
const daemon = this.getDaemon(connection.id);
|
|
2404
|
+
if (!daemon || daemon.status !== 'online' || !daemon.local_url.trim()) return null;
|
|
2405
|
+
return { computer, connection, daemon, token };
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2311
2408
|
closeStaleComputerConnections(timeoutMs: number, now = new Date()): number {
|
|
2312
2409
|
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new Error('timeoutMs must be non-negative');
|
|
2313
2410
|
const cutoff = new Date(now.getTime() - timeoutMs).toISOString();
|