@controlflow-ai/daemon 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.
Files changed (65) hide show
  1. package/README.md +66 -24
  2. package/package.json +16 -3
  3. package/src/agent-avatar.ts +30 -0
  4. package/src/agent-key.ts +28 -0
  5. package/src/agent-permissions.ts +359 -0
  6. package/src/agent-runtime.ts +810 -28
  7. package/src/agent-workspace.ts +183 -0
  8. package/src/app.ts +2183 -79
  9. package/src/args.ts +54 -7
  10. package/src/cli.ts +873 -14
  11. package/src/client.ts +482 -12
  12. package/src/coco.ts +9 -40
  13. package/src/codex.ts +33 -5
  14. package/src/config.ts +28 -4
  15. package/src/console.ts +460 -26
  16. package/src/daemon-client.ts +116 -3
  17. package/src/daemon.ts +958 -101
  18. package/src/db.ts +3216 -113
  19. package/src/delivery-ws.ts +269 -0
  20. package/src/format.ts +4 -1
  21. package/src/lark/app-registration.ts +141 -0
  22. package/src/lark/cli.ts +7 -137
  23. package/src/lark/credentials.ts +36 -3
  24. package/src/lark/event-router.ts +61 -5
  25. package/src/lark/inbound-events.ts +156 -3
  26. package/src/lark/server-integration.ts +659 -111
  27. package/src/lark/setup.ts +74 -5
  28. package/src/lark/ws-daemon.ts +136 -10
  29. package/src/local-api.ts +611 -14
  30. package/src/local-auth.ts +36 -3
  31. package/src/message-attachments.ts +71 -0
  32. package/src/messaging-cli.ts +741 -0
  33. package/src/messaging-status.ts +669 -0
  34. package/src/migrations/023_projects.ts +65 -0
  35. package/src/migrations/024_agents_model.ts +10 -0
  36. package/src/migrations/025_room_archive.ts +44 -0
  37. package/src/migrations/026_project_archive.ts +44 -0
  38. package/src/migrations/027_agent_permission_profiles.ts +16 -0
  39. package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
  40. package/src/migrations/029_held_message_drafts.ts +32 -0
  41. package/src/migrations/030_agent_room_read_state.ts +25 -0
  42. package/src/migrations/031_room_tasks.ts +29 -0
  43. package/src/migrations/032_room_reminders.ts +29 -0
  44. package/src/migrations/033_room_saved_messages.ts +25 -0
  45. package/src/migrations/034_agent_activity_events.ts +27 -0
  46. package/src/migrations/035_agent_avatars.ts +17 -0
  47. package/src/migrations/036_project_agent_defaults.ts +21 -0
  48. package/src/migrations/037_message_attachments.ts +36 -0
  49. package/src/migrations/038_agent_activity_room_scope.ts +64 -0
  50. package/src/migrations/039_message_attachments_path.ts +34 -0
  51. package/src/migrations/040_message_attachments_file_schema.ts +80 -0
  52. package/src/migrations/041_room_system_events.ts +30 -0
  53. package/src/migrations/042_message_attachment_file_kind.ts +52 -0
  54. package/src/migrations/043_room_mode_skill_registry.ts +92 -0
  55. package/src/migrations/044_workflow_runtime.ts +69 -0
  56. package/src/migrations/045_skill_repository_ownership.ts +64 -0
  57. package/src/migrations.ts +70 -1
  58. package/src/neeko.ts +40 -4
  59. package/src/runtime-env.ts +179 -0
  60. package/src/runtime-registry.ts +83 -13
  61. package/src/server.ts +244 -4
  62. package/src/token-file.ts +13 -6
  63. package/src/types.ts +394 -0
  64. package/src/workflow-runtime.ts +275 -0
  65. package/src/web.ts +0 -904
package/src/console.ts CHANGED
@@ -4,7 +4,12 @@ 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 { runtimeModelOptions, validateRuntimeModel } from './runtime-registry.js';
10
+ import type { LarkRegistrationComplete } from './lark/app-registration.js';
7
11
  import type { Computer, ProvisionedComputer } from './types.js';
12
+ import { runMessagingCommand } from './messaging-cli.js';
8
13
 
9
14
  interface Prompt {
10
15
  askLine(label: string): Promise<string>;
@@ -23,14 +28,28 @@ Usage:
23
28
  bun run src/console.ts run-action <run-id> kill|restart
24
29
  bun run src/console.ts computers list [--json]
25
30
  bun run src/console.ts computer onboard [--interactive] [--name <display-name>] [--server-url <url>] [--package-name <npm-package>]
31
+ bun run src/console.ts computers reconnect-command [--interactive] [--computer-id <machine>] [--server-url <url>] [--package-name <npm-package>] [--json]
32
+ bun run src/console.ts computers delete [--interactive] [--computer-id <machine>] [--json]
26
33
  bun run src/console.ts agents list [--json]
27
- bun run src/console.ts agents onboard [--interactive] [--key <agent-key>] [--name <display-name>] [--runtime codex] [--computer-id <machine>]
28
- 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
34
+ bun run src/console.ts agents onboard [--interactive] [--key <agent-key>] [--name <display-name>] [--runtime codex] [--model <model>] [--computer-id <machine>]
35
+ bun run src/console.ts agents create --key <agent-key> --name <display-name> [--runtime neeko|coco|codex] [--model <model>] [--desc <description>]
36
+ bun run src/console.ts agents update [--interactive] --key <agent-key> [--runtime neeko|coco|codex] [--model <model>]
37
+ [--lark-app-id <id> --lark-app-secret <secret>] [--lark-label <name>] [--lark-config <path>] [--rebind-lark] [--unbind-lark] [--no-reload]
38
+ bun run src/console.ts agents delete --key <agent-key> [--yes] [--lark-config <path>] [--no-reload] [--json]
30
39
  bun run src/console.ts lark-users list [--json]
31
40
  bun run src/console.ts lark-users add [--interactive] --user-id <union-id> [--name <display-name>]
32
41
  bun run src/console.ts lark-users delete --user-id <union-id>
33
- bun run src/console.ts lark <setup|list|daemon|events|send> [flags]
42
+ bun run src/console.ts messaging health [--json]
43
+ bun run src/console.ts messaging status [--verbose] [--json]
44
+ bun run src/console.ts messaging restart-lark [--app-id <id>] [--json]
45
+ bun run src/console.ts messaging repair-lark [--dry-run] [--json]
46
+ bun run src/console.ts messaging doctor-lark [--app-id <id>] [--config <path>] [--budget-ms 10000] [--json]
47
+ bun run src/console.ts messaging watch-lark [--app-id <id>] [--timeout-ms 60000] [--interval-ms 1000] [--json]
48
+ bun run src/console.ts messaging verify-lark-ingress [--app-id <id>] [--timeout-ms 60000] [--interval-ms 1000] [--json]
49
+ bun run src/console.ts messaging recent-lark [--limit 20] [--json]
50
+ bun run src/console.ts messaging probe-lark --app-id <id> --sender-user-id <union-id> --chat-id <chat-id> [--mention-open-id <open-id>] [--text <message>] [--json]
51
+ bun run src/console.ts messaging probe-delivery [--agent lock] [--room <room>] [--timeout-ms 120000] [--json]
52
+ bun run src/console.ts lark <list|daemon|events|send> [flags]
34
53
 
35
54
  Environment:
36
55
  PAL_SERVER=${defaultServerUrl()}
@@ -44,6 +63,83 @@ async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
44
63
  return payload.data as T;
45
64
  }
46
65
 
66
+ async function reloadLarkIntegration(serverUrl: string): Promise<void> {
67
+ const response = await fetch(`${serverUrl.replace(/\/$/, '')}/api/lark/reload`, {
68
+ method: 'POST',
69
+ headers: { 'content-type': 'application/json' },
70
+ body: JSON.stringify({}),
71
+ });
72
+ if (!response.ok) {
73
+ const text = await response.text();
74
+ console.warn(`Lark bot config saved, but server reload failed (${response.status}): ${text}`);
75
+ return;
76
+ }
77
+ console.log('Lark integration reloaded.');
78
+ }
79
+
80
+ async function resolveBotInfoForConsole(appId: string, appSecret: string, options: { budgetMs?: number } = {}): Promise<LarkBotInfoResult> {
81
+ if (process.env.NODE_ENV === 'test' && process.env.PAL_TEST_LARK_BOT_OPEN_ID) {
82
+ return { ok: true, openId: process.env.PAL_TEST_LARK_BOT_OPEN_ID };
83
+ }
84
+ return resolveLarkBotInfo(appId, appSecret, options);
85
+ }
86
+
87
+ async function registerLarkAppForConsole(): Promise<LarkRegistrationComplete | null> {
88
+ if (process.env.NODE_ENV === 'test' && process.env.PAL_TEST_LARK_REGISTRATION_APP_ID && process.env.PAL_TEST_LARK_REGISTRATION_APP_SECRET) {
89
+ console.log('Create a Feishu app by scanning this QR code or opening the link:');
90
+ console.log('https://example.test/lark-registration');
91
+ return {
92
+ appId: process.env.PAL_TEST_LARK_REGISTRATION_APP_ID,
93
+ appSecret: process.env.PAL_TEST_LARK_REGISTRATION_APP_SECRET,
94
+ tenantBrand: 'feishu',
95
+ userOpenId: process.env.PAL_TEST_LARK_REGISTRATION_USER_OPEN_ID,
96
+ };
97
+ }
98
+ return registerLarkAppFromDeviceFlow({ log: console });
99
+ }
100
+
101
+ function larkConfigPath(flags: Record<string, string | boolean>): string {
102
+ return flag(flags, 'lark-config') ?? flag(flags, 'config') ?? defaultLarkConfigPath();
103
+ }
104
+
105
+ async function unbindLarkBotFromAgent(serverClient: LockClient, flags: Record<string, string | boolean>, agentKey: string): Promise<void> {
106
+ const configPath = larkConfigPath(flags);
107
+ const store = loadLarkCredentials(configPath);
108
+ const result = unbindCredentialAgent(store, agentKey);
109
+ if (!result.changed) {
110
+ console.log(`Agent ${agentKey} has no Lark bot binding.`);
111
+ return;
112
+ }
113
+ saveLarkCredentials(result.store, configPath);
114
+ console.log(`Lark bot ${result.unbound!.appId} unbound from agent ${agentKey}.`);
115
+ if (!boolFlag(flags, 'no-reload')) await reloadLarkIntegration(serverClient.baseUrl);
116
+ }
117
+
118
+ async function bindLarkBotToAgent(serverClient: LockClient, flags: Record<string, string | boolean>, agentKey: string): Promise<void> {
119
+ const appId = flag(flags, 'lark-app-id') ?? flag(flags, 'app-id');
120
+ const appSecret = flag(flags, 'lark-app-secret') ?? flag(flags, 'app-secret');
121
+ if (!appId && !appSecret) return;
122
+ if (!appId || !appSecret) throw new Error('--lark-app-id and --lark-app-secret are required together');
123
+
124
+ const botInfo = await resolveBotInfoForConsole(appId, appSecret);
125
+ if (!botInfo.ok) throw new Error(`could not resolve Lark bot open_id (${botInfo.error}): ${botInfo.message}`);
126
+
127
+ const configPath = larkConfigPath(flags);
128
+ const store = loadLarkCredentials(configPath);
129
+ const result = upsertCredential(store, {
130
+ appId,
131
+ appSecret,
132
+ label: flag(flags, 'lark-label') ?? flag(flags, 'label'),
133
+ agent: agentKey,
134
+ botOpenId: botInfo.openId,
135
+ }, { rebind: boolFlag(flags, 'rebind-lark') });
136
+ saveLarkCredentials(result.store, configPath);
137
+
138
+ const moved = result.unbound.length > 0 ? `; moved from ${result.unbound.map((bot) => bot.appId).join(', ')}` : '';
139
+ console.log(`Lark bot ${appId} bound to agent ${agentKey}${moved}.`);
140
+ if (!boolFlag(flags, 'no-reload')) await reloadLarkIntegration(serverClient.baseUrl);
141
+ }
142
+
47
143
  function printJson(value: unknown): void {
48
144
  console.log(JSON.stringify(value, null, 2));
49
145
  }
@@ -86,7 +182,7 @@ async function askRequired(prompt: Prompt, label: string, defaultValue = ''): Pr
86
182
  }
87
183
 
88
184
  async function askRuntime(prompt: Prompt, defaultValue: string): Promise<string> {
89
- const runtimes = ['codex', 'neeko', 'coco', 'coco-stream-json'];
185
+ const runtimes = ['codex', 'neeko', 'coco'];
90
186
  const fallback = runtimes.includes(defaultValue) ? defaultValue : 'codex';
91
187
  console.log('Runtime:');
92
188
  runtimes.forEach((runtime, index) => {
@@ -102,6 +198,29 @@ async function askRuntime(prompt: Prompt, defaultValue: string): Promise<string>
102
198
  }
103
199
  }
104
200
 
201
+ async function askModel(prompt: Prompt, runtime: string, defaultValue = ''): Promise<string> {
202
+ const models = await runtimeModelOptions(runtime);
203
+ if (models.length === 0) {
204
+ console.log(`Model: ${runtime} has no built-in model list; using runtime default.`);
205
+ return '';
206
+ }
207
+ const fallback = models.includes(defaultValue) ? defaultValue : models[0]!;
208
+ console.log('Model:');
209
+ console.log(' 0. Runtime default');
210
+ models.forEach((model, index) => {
211
+ const marker = model === fallback ? ' (default)' : '';
212
+ console.log(` ${index + 1}. ${model}${marker}`);
213
+ });
214
+
215
+ while (true) {
216
+ const answer = await ask(prompt, `Choose ${runtime} model`, fallback);
217
+ if (answer === '0') return '';
218
+ const selected = models[Number(answer) - 1] ?? answer;
219
+ if (models.includes(selected)) return selected;
220
+ console.log(`Model must be one of: ${models.join(', ')}`);
221
+ }
222
+ }
223
+
105
224
  async function askYesNo(prompt: Prompt, label: string, defaultValue = false): Promise<boolean> {
106
225
  const fallback = defaultValue ? 'Y/n' : 'y/N';
107
226
  while (true) {
@@ -132,12 +251,35 @@ async function askComputerId(prompt: Prompt, computers: Computer[]): Promise<str
132
251
  }
133
252
  }
134
253
 
254
+ async function listAgentRecords(serverClient: LockClient): Promise<Array<Record<string, unknown>>> {
255
+ const response = await fetch(`${serverClient.baseUrl}/api/agents`);
256
+ const payload = await response.json() as { data?: { agents: Array<Record<string, unknown>> }; message?: string };
257
+ if (!response.ok) throw new Error(payload.message ?? `list failed: ${response.status}`);
258
+ return payload.data?.agents ?? [];
259
+ }
260
+
261
+ async function askAgentKey(prompt: Prompt, agents: Array<Record<string, unknown>>, defaultValue = ''): Promise<string> {
262
+ if (agents.length === 0) throw new Error('No agents found. Set up an agent first with "bun run console -- agents onboard".');
263
+
264
+ console.log('Agents:');
265
+ agents.forEach((agent, index) => {
266
+ console.log(` ${index + 1}. ${agent.display_name} (${agent.agent_key}) runtime=${agent.runtime ?? '-'}`);
267
+ });
268
+
269
+ while (true) {
270
+ const answer = await ask(prompt, 'Choose agent', defaultValue);
271
+ const selected = agents[Number(answer) - 1] ?? agents.find((agent) => agent.agent_key === answer);
272
+ if (selected?.agent_key) return String(selected.agent_key);
273
+ console.log('Choose a listed number or agent key.');
274
+ }
275
+ }
276
+
135
277
  async function collectComputerOnboardInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ name: string; serverUrl: string; packageName: string }> {
136
278
  const hasAnyProvisionFlag = Boolean(flag(flags, 'name') || flag(flags, 'server-url') || flag(flags, 'package-name'));
137
279
  const interactive = boolFlag(flags, 'interactive') || (!hasAnyProvisionFlag && process.stdin.isTTY === true);
138
280
  const defaultName = flag(flags, 'name') ?? 'Local computer';
139
281
  const defaultServerUrl = flag(flags, 'server-url') ?? serverClient.baseUrl;
140
- const defaultPackageName = flag(flags, 'package-name') ?? '@slock-ai/daemon@latest';
282
+ const defaultPackageName = flag(flags, 'package-name') ?? 'bun run daemon';
141
283
 
142
284
  if (!interactive) {
143
285
  return {
@@ -153,12 +295,12 @@ async function collectComputerOnboardInput(serverClient: LockClient, flags: Reco
153
295
  console.log('Provision a computer credential and daemon start command.');
154
296
  const name = await askRequired(prompt, 'Computer display name', defaultName);
155
297
  const serverUrl = await askRequired(prompt, 'Server URL', defaultServerUrl);
156
- const packageName = await askRequired(prompt, 'Daemon package', defaultPackageName);
298
+ const packageName = await askRequired(prompt, 'Daemon command/package', defaultPackageName);
157
299
  console.log('');
158
300
  console.log('Summary:');
159
301
  console.log(` name: ${name}`);
160
302
  console.log(` server_url: ${serverUrl}`);
161
- console.log(` package_name: ${packageName}`);
303
+ console.log(` command_prefix: ${packageName}`);
162
304
  if (!await askYesNo(prompt, 'Provision this computer?', true)) throw new Error('computer onboarding cancelled');
163
305
  return { name, serverUrl, packageName };
164
306
  } finally {
@@ -176,7 +318,56 @@ function printProvisionedComputer(provisioned: ProvisionedComputer): void {
176
318
  console.log(provisioned.command);
177
319
  }
178
320
 
179
- async function collectOnboardInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ agentKey: string; displayName: string; runtime: string; desc: string | null; computerId: string | undefined }> {
321
+ async function collectLarkBindFlags(prompt: Prompt, flags: Record<string, string | boolean>, agentKey: string, existingAppId?: string): Promise<Record<string, string | boolean>> {
322
+ const configPath = larkConfigPath(flags);
323
+ console.log('Lark bot setup');
324
+ console.log(' 1. Scan or open link to create app');
325
+ console.log(' 2. Paste App ID / App Secret');
326
+ const method = await ask(prompt, 'Setup method', '1');
327
+
328
+ let appId = flag(flags, 'lark-app-id') ?? flag(flags, 'app-id');
329
+ let appSecret = flag(flags, 'lark-app-secret') ?? flag(flags, 'app-secret');
330
+ let scannedUserOpenId: string | undefined;
331
+ if (method !== '2' && (!appId || !appSecret)) {
332
+ const registration = await registerLarkAppForConsole().catch((error) => {
333
+ console.warn(`App registration failed: ${error instanceof Error ? error.message : String(error)}`);
334
+ return null;
335
+ });
336
+ if (registration?.tenantBrand === 'lark') {
337
+ throw new Error('Lark international tenants are not supported yet; use a Feishu tenant');
338
+ }
339
+ if (registration) {
340
+ appId = registration.appId;
341
+ appSecret = registration.appSecret;
342
+ scannedUserOpenId = registration.userOpenId;
343
+ console.log(`App created: ${appId}`);
344
+ } else {
345
+ console.warn('Falling back to manual App ID / App Secret entry.');
346
+ }
347
+ }
348
+
349
+ appId = appId || await askRequired(prompt, 'Lark App ID', existingAppId ?? '');
350
+ appSecret = appSecret || await askRequired(prompt, 'Lark App Secret');
351
+ const label = await ask(prompt, 'Bot label (optional)', flag(flags, 'lark-label') ?? flag(flags, 'label') ?? '');
352
+ if (scannedUserOpenId) {
353
+ console.log(`Scanner open_id ${scannedUserOpenId} was detected; add it under Lark authorized users if this user should operate Pal.`);
354
+ }
355
+
356
+ const isChangingBot = Boolean(existingAppId && existingAppId !== appId);
357
+ const rebind = boolFlag(flags, 'rebind-lark') || (isChangingBot && await askYesNo(prompt, `Move ${agentKey} from ${existingAppId} to ${appId}?`, false));
358
+ if (isChangingBot && !rebind) throw new Error('Lark bot rebind cancelled');
359
+
360
+ return {
361
+ ...flags,
362
+ 'lark-app-id': appId,
363
+ 'lark-app-secret': appSecret,
364
+ 'lark-config': configPath,
365
+ ...(label ? { 'lark-label': label } : {}),
366
+ ...(rebind ? { 'rebind-lark': true } : {}),
367
+ };
368
+ }
369
+
370
+ async function collectOnboardInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ agentKey: string; displayName: string; runtime: string; model: string | null; desc: string | null; computerId: string | undefined; larkFlags?: Record<string, string | boolean> }> {
180
371
  const hasRequiredFlags = Boolean(flag(flags, 'key') && flag(flags, 'name'));
181
372
  const interactive = boolFlag(flags, 'interactive') || (!hasRequiredFlags && process.stdin.isTTY === true);
182
373
  if (!interactive) {
@@ -185,8 +376,10 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
185
376
  agentKey: flag(flags, 'key')!,
186
377
  displayName: flag(flags, 'name')!,
187
378
  runtime: flag(flags, 'runtime') ?? 'codex',
379
+ model: await validateRuntimeModel(flag(flags, 'runtime') ?? 'codex', flag(flags, 'model')),
188
380
  desc: flag(flags, 'desc') ?? null,
189
381
  computerId: flag(flags, 'computer-id'),
382
+ larkFlags: flag(flags, 'lark-app-id') || flag(flags, 'app-id') ? flags : undefined,
190
383
  };
191
384
  }
192
385
 
@@ -197,6 +390,7 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
197
390
  const agentKey = await askRequired(prompt, 'Agent key', flag(flags, 'key') ?? 'codex');
198
391
  const displayName = await askRequired(prompt, 'Display name', flag(flags, 'name') ?? agentKey);
199
392
  const runtime = await askRuntime(prompt, flag(flags, 'runtime') ?? 'codex');
393
+ const model = await askModel(prompt, runtime, flag(flags, 'model') ?? '');
200
394
  const desc = await ask(prompt, 'Description (optional)', flag(flags, 'desc') ?? '');
201
395
  const shouldAssign = Boolean(flag(flags, 'computer-id')) || await askYesNo(prompt, 'Assign this agent to a computer?', false);
202
396
  const computerId = shouldAssign
@@ -207,21 +401,152 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
207
401
  console.log(` agent_key: ${agentKey}`);
208
402
  console.log(` display_name: ${displayName}`);
209
403
  console.log(` runtime: ${runtime}`);
404
+ console.log(` model: ${model || '-'}`);
210
405
  console.log(` description: ${desc || '-'}`);
211
406
  console.log(` computer_id: ${computerId || '-'}`);
212
407
  if (!await askYesNo(prompt, 'Create/update this agent?', true)) throw new Error('onboarding cancelled');
408
+ const shouldSetupLark = await askYesNo(prompt, 'Set up a Lark bot for this agent?', false);
409
+ const larkFlags = shouldSetupLark ? await collectLarkBindFlags(prompt, flags, agentKey) : undefined;
213
410
  return {
214
411
  agentKey,
215
412
  displayName,
216
413
  runtime,
414
+ model: model || null,
217
415
  desc: desc || null,
218
416
  computerId: computerId || undefined,
417
+ larkFlags,
418
+ };
419
+ } finally {
420
+ prompt.close();
421
+ }
422
+ }
423
+
424
+ async function collectAgentUpdateInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ agentKey: string; flags: Record<string, string | boolean> }> {
425
+ const prompt = await createPrompt();
426
+ try {
427
+ console.log('Agent settings');
428
+ const agents = await listAgentRecords(serverClient);
429
+ const agentKey = await askAgentKey(prompt, agents, flag(flags, 'key') ?? (agents.length === 1 ? '1' : ''));
430
+ const agent = agents.find((candidate) => candidate.agent_key === agentKey);
431
+ if (!agent) throw new Error(`agent ${agentKey} not found`);
432
+
433
+ if (flag(flags, 'model')) return { agentKey, flags };
434
+
435
+ const requestedLarkUpdate = Boolean(
436
+ boolFlag(flags, 'unbind-lark') ||
437
+ flag(flags, 'lark-app-id') ||
438
+ flag(flags, 'app-id') ||
439
+ flag(flags, 'lark-app-secret') ||
440
+ flag(flags, 'app-secret'),
441
+ );
442
+ let updateAction = requestedLarkUpdate ? (boolFlag(flags, 'unbind-lark') ? '3' : '2') : '';
443
+ if (!updateAction) {
444
+ console.log('Update:');
445
+ console.log(' 1. Model');
446
+ console.log(' 2. Bind/rebind Lark bot');
447
+ console.log(' 3. Unbind Lark bot');
448
+ updateAction = await ask(prompt, 'Choose update', '1');
449
+ }
450
+
451
+ if (updateAction === '1' || updateAction.toLowerCase() === 'model') {
452
+ const runtime = typeof agent.runtime === 'string' ? agent.runtime : '';
453
+ if (!runtime) throw new Error(`agent ${agentKey} has no runtime configured`);
454
+ const model = await askModel(prompt, runtime, typeof agent.model === 'string' ? agent.model : '');
455
+ console.log('');
456
+ console.log('Summary:');
457
+ console.log(` agent_key: ${agentKey}`);
458
+ console.log(` runtime: ${runtime}`);
459
+ console.log(` model: ${model || '-'}`);
460
+ if (!await askYesNo(prompt, 'Update this agent model?', true)) throw new Error('agent update cancelled');
461
+ return { agentKey, flags: { ...flags, model } };
462
+ }
463
+
464
+ if (updateAction !== '2' && updateAction !== '3') throw new Error('Choose 1, 2, or 3.');
465
+
466
+ const configPath = larkConfigPath(flags);
467
+ const store = loadLarkCredentials(configPath);
468
+ const existing = findCredentialByAgent(store, agentKey);
469
+ if (existing) {
470
+ console.log(`Current Lark bot: ${existing.label ?? existing.appId} (${existing.appId})`);
471
+ } else {
472
+ console.log('Current Lark bot: -');
473
+ }
474
+
475
+ if (updateAction === '3') {
476
+ if (!existing) throw new Error(`agent ${agentKey} has no Lark bot binding`);
477
+ if (!await askYesNo(prompt, `Unbind ${existing.appId} from ${agentKey}?`, false)) throw new Error('Lark bot unbind cancelled');
478
+ return {
479
+ agentKey,
480
+ flags: {
481
+ ...flags,
482
+ 'lark-config': configPath,
483
+ 'unbind-lark': true,
484
+ },
485
+ };
486
+ }
487
+
488
+ const defaultAction = '1';
489
+ console.log('Lark action:');
490
+ console.log(` 1. ${existing ? 'Bind/rebind bot' : 'Bind bot'}`);
491
+ if (existing) console.log(' 2. Unbind bot');
492
+ const action = await ask(prompt, 'Choose action', defaultAction);
493
+ if (existing && (action === '2' || action.toLowerCase() === 'unbind')) {
494
+ if (!await askYesNo(prompt, `Unbind ${existing.appId} from ${agentKey}?`, false)) throw new Error('Lark bot unbind cancelled');
495
+ return {
496
+ agentKey,
497
+ flags: {
498
+ ...flags,
499
+ 'lark-config': configPath,
500
+ 'unbind-lark': true,
501
+ },
502
+ };
503
+ }
504
+
505
+ const larkFlags = await collectLarkBindFlags(prompt, flags, agentKey, existing?.appId);
506
+
507
+ console.log('');
508
+ console.log('Summary:');
509
+ console.log(` agent_key: ${agentKey}`);
510
+ console.log(` lark_app_id: ${flag(larkFlags, 'lark-app-id')}`);
511
+ console.log(` lark_label: ${flag(larkFlags, 'lark-label') || '-'}`);
512
+ console.log(` rebind: ${boolFlag(larkFlags, 'rebind-lark') ? 'yes' : 'no'}`);
513
+ if (!await askYesNo(prompt, 'Bind this Lark bot?', true)) throw new Error('Lark bot binding cancelled');
514
+
515
+ return {
516
+ agentKey,
517
+ flags: larkFlags,
219
518
  };
220
519
  } finally {
221
520
  prompt.close();
222
521
  }
223
522
  }
224
523
 
524
+ async function collectAgentDeleteInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<string> {
525
+ const agents = await listAgentRecords(serverClient);
526
+ const explicitKey = flag(flags, 'key') ?? flag(flags, 'agent');
527
+ const interactive = boolFlag(flags, 'interactive') || (!explicitKey && process.stdin.isTTY === true);
528
+ if (!interactive) {
529
+ if (!explicitKey) throw new Error('agents delete requires --key when not running interactively');
530
+ if (!boolFlag(flags, 'yes') && !boolFlag(flags, 'force')) throw new Error('agents delete requires --yes to confirm non-interactively');
531
+ return explicitKey;
532
+ }
533
+
534
+ const prompt = await createPrompt();
535
+ try {
536
+ console.log('Delete agent');
537
+ const agentKey = await askAgentKey(prompt, agents, explicitKey ?? (agents.length === 1 ? '1' : ''));
538
+ const agent = agents.find((candidate) => candidate.agent_key === agentKey);
539
+ console.log('');
540
+ console.log('This will remove the agent record, computer assignment, room memberships, room delivery subscriptions, provider bindings, and active deliveries.');
541
+ console.log(` agent_key: ${agentKey}`);
542
+ console.log(` display_name: ${agent?.display_name ?? '-'}`);
543
+ if (!await askYesNo(prompt, `Delete ${agentKey}?`, false)) throw new Error('agent delete cancelled');
544
+ return agentKey;
545
+ } finally {
546
+ prompt.close();
547
+ }
548
+ }
549
+
225
550
  async function collectLarkUserInput(flags: Record<string, string | boolean>): Promise<{ userId: string; displayName: string | null }> {
226
551
  const userIdFlag = flag(flags, 'user-id') ?? flag(flags, 'id');
227
552
  const interactive = boolFlag(flags, 'interactive') || (!userIdFlag && process.stdin.isTTY === true);
@@ -359,6 +684,52 @@ async function main(): Promise<void> {
359
684
  return;
360
685
  }
361
686
 
687
+ if (sub === 'reconnect-command') {
688
+ let computerId = flag(args.flags, 'computer-id');
689
+ const interactive = boolFlag(args.flags, 'interactive') || (!computerId && process.stdin.isTTY === true);
690
+ if (interactive) {
691
+ const prompt = await createPrompt();
692
+ try {
693
+ computerId = await askComputerId(prompt, await serverClient.listComputers());
694
+ } finally {
695
+ prompt.close();
696
+ }
697
+ }
698
+ if (!computerId) throw new Error('--computer-id is required');
699
+ const provisioned = await serverClient.regenerateComputerCommand(computerId, {
700
+ server_url: flag(args.flags, 'server-url') ?? serverClient.baseUrl,
701
+ package_name: flag(args.flags, 'package-name') ?? 'bun run daemon',
702
+ });
703
+ if (args.flags.json) {
704
+ printJson(provisioned);
705
+ } else {
706
+ console.log('Computer reconnect command generated');
707
+ printProvisionedComputer(provisioned);
708
+ }
709
+ return;
710
+ }
711
+
712
+ if (sub === 'delete') {
713
+ let computerId = flag(args.flags, 'computer-id');
714
+ const interactive = boolFlag(args.flags, 'interactive') || (!computerId && process.stdin.isTTY === true);
715
+ if (interactive) {
716
+ const prompt = await createPrompt();
717
+ try {
718
+ computerId = await askComputerId(prompt, await serverClient.listComputers());
719
+ } finally {
720
+ prompt.close();
721
+ }
722
+ }
723
+ if (!computerId) throw new Error('--computer-id is required');
724
+ const result = await serverClient.deleteComputer(computerId);
725
+ if (args.flags.json) {
726
+ printJson(result);
727
+ } else {
728
+ console.log(`Computer deleted: ${result.computer.id}`);
729
+ }
730
+ return;
731
+ }
732
+
362
733
  throw new Error(`unknown ${args.command} subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
363
734
  }
364
735
 
@@ -366,15 +737,12 @@ async function main(): Promise<void> {
366
737
  const sub = args.values[0];
367
738
 
368
739
  if (sub === 'list') {
369
- const response = await fetch(`${serverClient.baseUrl}/api/agents`);
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 ?? [];
740
+ const agents = await listAgentRecords(serverClient);
373
741
  if (args.flags.json) {
374
742
  printJson(agents);
375
743
  } else {
376
744
  for (const agent of agents) {
377
- console.log(`${agent.agent_key} name="${agent.display_name}" runtime=${agent.runtime ?? '-'} desc=${agent.description ?? '-'}`);
745
+ console.log(`${agent.agent_key} name="${agent.display_name}" runtime=${agent.runtime ?? '-'} model=${agent.model ?? '-'} desc=${agent.description ?? '-'}`);
378
746
  }
379
747
  }
380
748
  return;
@@ -384,6 +752,7 @@ async function main(): Promise<void> {
384
752
  const agentKey = flag(args.flags, 'key');
385
753
  const displayName = flag(args.flags, 'name');
386
754
  const runtime = flag(args.flags, 'runtime');
755
+ const model = flag(args.flags, 'model');
387
756
  const desc = flag(args.flags, 'desc');
388
757
  if (!agentKey) throw new Error('--key is required');
389
758
  if (!displayName) throw new Error('--name is required');
@@ -395,6 +764,7 @@ async function main(): Promise<void> {
395
764
  agent_key: agentKey,
396
765
  display_name: displayName,
397
766
  runtime: runtime ?? null,
767
+ model: await validateRuntimeModel(runtime ?? '', model),
398
768
  description: desc ?? null,
399
769
  }),
400
770
  });
@@ -405,7 +775,7 @@ async function main(): Promise<void> {
405
775
  }
406
776
 
407
777
  if (sub === 'onboard') {
408
- const { agentKey, displayName, runtime, desc, computerId } = await collectOnboardInput(serverClient, args.flags);
778
+ const { agentKey, displayName, runtime, model, desc, computerId, larkFlags } = await collectOnboardInput(serverClient, args.flags);
409
779
  if (!agentKey) throw new Error('agent key is required');
410
780
  if (!displayName) throw new Error('display name is required');
411
781
 
@@ -416,30 +786,89 @@ async function main(): Promise<void> {
416
786
  agent_key: agentKey,
417
787
  display_name: displayName,
418
788
  runtime,
789
+ model,
419
790
  description: desc,
420
791
  computer_id: computerId,
421
792
  }),
422
793
  });
423
794
  const payload = await response.json() as { data?: Record<string, unknown>; message?: string };
424
795
  if (!response.ok) throw new Error(payload.message ?? `onboard failed: ${response.status}`);
796
+ if (larkFlags) await bindLarkBotToAgent(serverClient, larkFlags, agentKey);
425
797
  printJson(payload.data);
426
798
  return;
427
799
  }
428
800
 
429
801
  if (sub === 'update') {
430
- const agentKey = flag(args.flags, 'key');
431
- const runtime = flag(args.flags, 'runtime');
802
+ const updateInput = boolFlag(args.flags, 'interactive')
803
+ ? await collectAgentUpdateInput(serverClient, args.flags)
804
+ : { agentKey: flag(args.flags, 'key') ?? '', flags: args.flags };
805
+ const agentKey = updateInput.agentKey;
806
+ const updateFlags = updateInput.flags;
807
+ const runtime = flag(updateFlags, 'runtime');
808
+ const hasModel = Object.prototype.hasOwnProperty.call(updateFlags, 'model');
809
+ const model = flag(updateFlags, 'model');
432
810
  if (!agentKey) throw new Error('--key is required');
433
- if (!runtime) throw new Error('--runtime is required');
811
+ const hasLarkBinding = Boolean(flag(updateFlags, 'lark-app-id') || flag(updateFlags, 'app-id') || flag(updateFlags, 'lark-app-secret') || flag(updateFlags, 'app-secret'));
812
+ const hasLarkUnbind = boolFlag(updateFlags, 'unbind-lark');
813
+ if (!runtime && !hasModel && !hasLarkBinding && !hasLarkUnbind) throw new Error('--runtime, --model, --lark-app-id/--lark-app-secret, or --unbind-lark is required');
814
+
815
+ let agent: Record<string, unknown> | undefined;
816
+ if (runtime || hasModel) {
817
+ if (hasModel && typeof model !== 'string') throw new Error('--model requires a value');
818
+ const existingAgent = hasModel && !runtime
819
+ ? (await listAgentRecords(serverClient)).find((candidate) => candidate.agent_key === agentKey)
820
+ : undefined;
821
+ if (hasModel && !runtime && !existingAgent) throw new Error(`agent ${agentKey} not found`);
822
+ const runtimeForModel = runtime ?? (typeof existingAgent?.runtime === 'string' ? existingAgent.runtime : '');
823
+ const response = await fetch(`${serverClient.baseUrl}/api/agents/${encodeURIComponent(agentKey)}`, {
824
+ method: 'PATCH',
825
+ headers: { 'content-type': 'application/json' },
826
+ body: JSON.stringify({
827
+ ...(runtime ? { runtime } : {}),
828
+ ...(hasModel ? { model: await validateRuntimeModel(runtimeForModel, model) } : {}),
829
+ }),
830
+ });
831
+ const payload = await response.json() as { data?: { agent: Record<string, unknown> }; message?: string };
832
+ if (!response.ok) throw new Error(payload.message ?? `update failed: ${response.status}`);
833
+ agent = payload.data?.agent;
834
+ } else {
835
+ agent = (await listAgentRecords(serverClient)).find((candidate) => candidate.agent_key === agentKey);
836
+ if (!agent) throw new Error(`agent ${agentKey} not found`);
837
+ }
434
838
 
435
- const response = await fetch(`${serverClient.baseUrl}/api/agents/${encodeURIComponent(agentKey)}`, {
436
- method: 'PATCH',
437
- headers: { 'content-type': 'application/json' },
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);
839
+ if (hasLarkUnbind) await unbindLarkBotFromAgent(serverClient, updateFlags, agentKey);
840
+ else await bindLarkBotToAgent(serverClient, updateFlags, agentKey);
841
+ printJson(agent);
842
+ return;
843
+ }
844
+
845
+ if (sub === 'delete' || sub === 'remove') {
846
+ const agentKey = await collectAgentDeleteInput(serverClient, args.flags);
847
+ const params = new URLSearchParams();
848
+ const configPath = larkConfigPath(args.flags);
849
+ if (configPath) params.set('lark_config', configPath);
850
+ const suffix = params.toString() ? `?${params}` : '';
851
+ const data = await requestJson<{ deletion: Record<string, unknown>; lark: { unbound: boolean; app_id: string | null; error?: string | null } }>(
852
+ `${serverClient.baseUrl}/api/agents/${encodeURIComponent(agentKey)}${suffix}`,
853
+ {
854
+ method: 'DELETE',
855
+ headers: { 'content-type': 'application/json' },
856
+ body: JSON.stringify({ confirm_delete: true, leave_rooms: true, unbind_lark: true }),
857
+ },
858
+ );
859
+ if (data.lark.unbound && !boolFlag(args.flags, 'no-reload')) await reloadLarkIntegration(serverClient.baseUrl);
860
+ if (args.flags.json) {
861
+ printJson(data);
862
+ } else {
863
+ const deletion = data.deletion;
864
+ console.log(`Deleted agent ${agentKey}.`);
865
+ console.log(` rooms_left: ${deletion.rooms_left}`);
866
+ console.log(` subscriptions_removed: ${deletion.subscriptions_removed}`);
867
+ console.log(` active_deliveries_canceled: ${deletion.active_deliveries_canceled}`);
868
+ console.log(` running_runs_marked_kill: ${deletion.running_runs_marked_kill}`);
869
+ if (data.lark.unbound) console.log(` lark_unbound: ${data.lark.app_id}`);
870
+ if (data.lark.error) console.log(` lark_unbind_error: ${data.lark.error}`);
871
+ }
443
872
  return;
444
873
  }
445
874
 
@@ -486,6 +915,11 @@ async function main(): Promise<void> {
486
915
  throw new Error(`unknown lark-users subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
487
916
  }
488
917
 
918
+ if (args.command === 'messaging') {
919
+ await runMessagingCommand(serverClient, args, usage);
920
+ return;
921
+ }
922
+
489
923
  if (args.command === 'lark') {
490
924
  const { runLarkCli } = await import('./lark/cli.js');
491
925
  const code = await runLarkCli({