@controlflow-ai/daemon 0.1.2 → 0.1.4

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 (62) hide show
  1. package/README.md +54 -6
  2. package/bin/daemon.js +6 -1
  3. package/package.json +3 -1
  4. package/src/agent-avatar.ts +30 -0
  5. package/src/agent-key.ts +28 -0
  6. package/src/agent-permissions.ts +359 -0
  7. package/src/agent-runtime.ts +795 -28
  8. package/src/agent-workspace.ts +183 -0
  9. package/src/app.ts +1970 -79
  10. package/src/args.ts +54 -7
  11. package/src/cli.ts +873 -14
  12. package/src/client.ts +472 -10
  13. package/src/coco.ts +9 -40
  14. package/src/codex.ts +33 -5
  15. package/src/config.ts +28 -4
  16. package/src/console.ts +230 -20
  17. package/src/daemon-client.ts +116 -3
  18. package/src/daemon.ts +937 -99
  19. package/src/db.ts +3128 -122
  20. package/src/delivery-ws.ts +269 -0
  21. package/src/format.ts +4 -1
  22. package/src/lark/cli.ts +3 -3
  23. package/src/lark/event-router.ts +60 -4
  24. package/src/lark/inbound-events.ts +156 -3
  25. package/src/lark/server-integration.ts +659 -111
  26. package/src/lark/ws-daemon.ts +136 -10
  27. package/src/local-api.ts +545 -15
  28. package/src/local-auth.ts +33 -1
  29. package/src/message-attachments.ts +71 -0
  30. package/src/messaging-cli.ts +741 -0
  31. package/src/messaging-status.ts +669 -0
  32. package/src/migrations/024_agents_model.ts +10 -0
  33. package/src/migrations/025_room_archive.ts +44 -0
  34. package/src/migrations/026_project_archive.ts +44 -0
  35. package/src/migrations/027_agent_permission_profiles.ts +16 -0
  36. package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
  37. package/src/migrations/029_held_message_drafts.ts +32 -0
  38. package/src/migrations/030_agent_room_read_state.ts +25 -0
  39. package/src/migrations/031_room_tasks.ts +29 -0
  40. package/src/migrations/032_room_reminders.ts +29 -0
  41. package/src/migrations/033_room_saved_messages.ts +25 -0
  42. package/src/migrations/034_agent_activity_events.ts +27 -0
  43. package/src/migrations/035_agent_avatars.ts +17 -0
  44. package/src/migrations/036_project_agent_defaults.ts +21 -0
  45. package/src/migrations/037_message_attachments.ts +36 -0
  46. package/src/migrations/038_agent_activity_room_scope.ts +64 -0
  47. package/src/migrations/039_message_attachments_path.ts +34 -0
  48. package/src/migrations/040_message_attachments_file_schema.ts +80 -0
  49. package/src/migrations/041_room_system_events.ts +30 -0
  50. package/src/migrations/042_message_attachment_file_kind.ts +52 -0
  51. package/src/migrations/043_room_mode_skill_registry.ts +92 -0
  52. package/src/migrations/044_workflow_runtime.ts +69 -0
  53. package/src/migrations/045_skill_repository_ownership.ts +64 -0
  54. package/src/migrations.ts +69 -1
  55. package/src/neeko.ts +40 -4
  56. package/src/runtime-env.ts +179 -0
  57. package/src/runtime-registry.ts +83 -13
  58. package/src/server.ts +244 -4
  59. package/src/token-file.ts +13 -6
  60. package/src/types.ts +362 -0
  61. package/src/workflow-runtime.ts +275 -0
  62. package/src/web.ts +0 -904
package/src/codex.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { buildPalPrompt, runtimeCwd, type AgentRuntime, type AgentRuntimeRunInput } from './agent-runtime.js';
1
+ import { buildPalPrompt, runtimeLaunchRoot, runtimeCwd, type AgentRuntime, type AgentRuntimeRunInput } from './agent-runtime.js';
2
+ import { buildCodexPermissionArgs, buildRuntimeLaunchContext, effectivePermissionProfile, normalizeWritableRoot, writableRootsForProfile, type AgentPermissionProfile, type RuntimeLaunchContext } from './agent-permissions.js';
2
3
  export { runAgentRuntime } from './agent-runtime.js';
3
4
 
4
5
  function extractThreadId(stdout: string): string | null {
@@ -17,19 +18,46 @@ function extractThreadId(stdout: string): string | null {
17
18
  return null;
18
19
  }
19
20
 
20
- export function makeCodexRuntime(_agentUuid: string): AgentRuntime {
21
+ function tomlString(value: string): string {
22
+ return JSON.stringify(value);
23
+ }
24
+
25
+ function codexResumeConfigArgs(profile: AgentPermissionProfile, context: RuntimeLaunchContext): string[] {
26
+ if (profile.filesystemMode === 'full-access') {
27
+ return ['-c', 'sandbox_mode="danger-full-access"'];
28
+ }
29
+
30
+ const writableRoots = writableRootsForProfile(profile, context)
31
+ .filter((root) => normalizeWritableRoot(root) !== normalizeWritableRoot(context.runtimeRoot));
32
+ const args = [
33
+ '-c',
34
+ 'sandbox_mode="workspace-write"',
35
+ '-c',
36
+ 'sandbox_workspace_write.network_access=true',
37
+ ];
38
+ if (writableRoots.length > 0) {
39
+ args.push('-c', `sandbox_workspace_write.writable_roots=[${writableRoots.map(tomlString).join(',')}]`);
40
+ }
41
+ return args;
42
+ }
43
+
44
+ export function makeCodexRuntime(_agentUuid: string, model?: string | null): AgentRuntime {
45
+ const modelArgs = model?.trim() ? ['--model', model.trim()] : [];
21
46
  return {
22
47
  name: 'codex',
23
- capabilities: { protocol: 'json-stream', resume: 'runtime-session-id', busyDeliveryMode: 'queue', supportsMcp: false },
48
+ capabilities: { protocol: 'json-stream', resume: 'runtime-session-id', busyDeliveryMode: 'queue', supportsMcp: false, supportsSteer: false },
24
49
  command: 'codex',
25
50
  buildPrompt: buildPalPrompt,
26
51
  buildCwd: runtimeCwd,
27
52
  buildArgs(input: AgentRuntimeRunInput): string[] {
28
53
  const prompt = buildPalPrompt(input);
54
+ const context = input.launchContext ?? buildRuntimeLaunchContext(input);
55
+ const permissionProfile = effectivePermissionProfile(input);
56
+ const permissionArgs = buildCodexPermissionArgs(permissionProfile, context);
29
57
  if (input.runtimeSessionId) {
30
- return ['exec', 'resume', '--json', '--dangerously-bypass-approvals-and-sandbox', ...input.extraArgs, input.runtimeSessionId, prompt];
58
+ return ['exec', 'resume', '--json', '--skip-git-repo-check', ...codexResumeConfigArgs(permissionProfile, context), ...modelArgs, ...input.extraArgs, input.runtimeSessionId, prompt];
31
59
  }
32
- return ['exec', '--json', '--dangerously-bypass-approvals-and-sandbox', '--cd', runtimeCwd(input), ...input.extraArgs, prompt];
60
+ return ['exec', '--json', ...permissionArgs, '--skip-git-repo-check', '--cd', runtimeLaunchRoot(input), ...modelArgs, ...input.extraArgs, prompt];
33
61
  },
34
62
  parseOutput({ stdout, stderr, input }) {
35
63
  return {
package/src/config.ts CHANGED
@@ -96,11 +96,35 @@ export function privatePalCliBinDir(): string {
96
96
  return join(homeDir(), 'bin');
97
97
  }
98
98
 
99
- export function ensurePrivatePalCliBin(): string {
99
+ export interface EnsurePrivatePalCliBinOptions {
100
+ platform?: NodeJS.Platform;
101
+ }
102
+
103
+ function cmdQuoted(path: string): string {
104
+ return `"${path.replace(/"/g, '""')}"`;
105
+ }
106
+
107
+ function psQuoted(path: string): string {
108
+ return `'${path.replace(/'/g, "''")}'`;
109
+ }
110
+
111
+ export function privatePalCliExecutableName(platform: NodeJS.Platform = process.platform): string {
112
+ return platform === 'win32' ? 'pal.cmd' : 'pal';
113
+ }
114
+
115
+ export function ensurePrivatePalCliBin(options: EnsurePrivatePalCliBinOptions = {}): string {
116
+ const platform = options.platform ?? process.platform;
100
117
  const binDir = privatePalCliBinDir();
101
118
  ensureDir(binDir);
102
- const linkPath = join(binDir, 'pal');
103
- rmSync(linkPath, { force: true });
104
- symlinkSync(repoCliPath(), linkPath);
119
+ const cliPath = repoCliPath();
120
+ for (const name of ['pal', 'pal.cmd', 'pal.ps1']) {
121
+ rmSync(join(binDir, name), { force: true });
122
+ }
123
+ if (platform === 'win32') {
124
+ writeFileSync(join(binDir, 'pal.cmd'), `@echo off\r\nbun ${cmdQuoted(cliPath)} %*\r\n`);
125
+ writeFileSync(join(binDir, 'pal.ps1'), `& bun ${psQuoted(cliPath)} @args\r\n`);
126
+ } else {
127
+ symlinkSync(cliPath, join(binDir, 'pal'));
128
+ }
105
129
  return binDir;
106
130
  }
package/src/console.ts CHANGED
@@ -6,8 +6,10 @@ import { defaultServerUrl } from './config.js';
6
6
  import { formatMessages } from './format.js';
7
7
  import { defaultLarkConfigPath, findCredentialByAgent, loadLarkCredentials, saveLarkCredentials, unbindCredentialAgent, upsertCredential } from './lark/credentials.js';
8
8
  import { registerLarkAppFromDeviceFlow, resolveLarkBotInfo, type LarkBotInfoResult } from './lark/setup.js';
9
+ import { runtimeModelOptions, validateRuntimeModel } from './runtime-registry.js';
9
10
  import type { LarkRegistrationComplete } from './lark/app-registration.js';
10
11
  import type { Computer, ProvisionedComputer } from './types.js';
12
+ import { runMessagingCommand } from './messaging-cli.js';
11
13
 
12
14
  interface Prompt {
13
15
  askLine(label: string): Promise<string>;
@@ -26,14 +28,27 @@ Usage:
26
28
  bun run src/console.ts run-action <run-id> kill|restart
27
29
  bun run src/console.ts computers list [--json]
28
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]
29
33
  bun run src/console.ts agents list [--json]
30
- bun run src/console.ts agents onboard [--interactive] [--key <agent-key>] [--name <display-name>] [--runtime codex] [--computer-id <machine>]
31
- bun run src/console.ts agents create --key <agent-key> --name <display-name> [--runtime neeko|coco|coco-stream-json|codex] [--desc <description>]
32
- bun run src/console.ts agents update [--interactive] --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>]
33
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]
34
39
  bun run src/console.ts lark-users list [--json]
35
40
  bun run src/console.ts lark-users add [--interactive] --user-id <union-id> [--name <display-name>]
36
41
  bun run src/console.ts lark-users delete --user-id <union-id>
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]
37
52
  bun run src/console.ts lark <list|daemon|events|send> [flags]
38
53
 
39
54
  Environment:
@@ -62,11 +77,11 @@ async function reloadLarkIntegration(serverUrl: string): Promise<void> {
62
77
  console.log('Lark integration reloaded.');
63
78
  }
64
79
 
65
- async function resolveBotInfoForConsole(appId: string, appSecret: string): Promise<LarkBotInfoResult> {
80
+ async function resolveBotInfoForConsole(appId: string, appSecret: string, options: { budgetMs?: number } = {}): Promise<LarkBotInfoResult> {
66
81
  if (process.env.NODE_ENV === 'test' && process.env.PAL_TEST_LARK_BOT_OPEN_ID) {
67
82
  return { ok: true, openId: process.env.PAL_TEST_LARK_BOT_OPEN_ID };
68
83
  }
69
- return resolveLarkBotInfo(appId, appSecret);
84
+ return resolveLarkBotInfo(appId, appSecret, options);
70
85
  }
71
86
 
72
87
  async function registerLarkAppForConsole(): Promise<LarkRegistrationComplete | null> {
@@ -167,7 +182,7 @@ async function askRequired(prompt: Prompt, label: string, defaultValue = ''): Pr
167
182
  }
168
183
 
169
184
  async function askRuntime(prompt: Prompt, defaultValue: string): Promise<string> {
170
- const runtimes = ['codex', 'neeko', 'coco', 'coco-stream-json'];
185
+ const runtimes = ['codex', 'neeko', 'coco'];
171
186
  const fallback = runtimes.includes(defaultValue) ? defaultValue : 'codex';
172
187
  console.log('Runtime:');
173
188
  runtimes.forEach((runtime, index) => {
@@ -183,6 +198,29 @@ async function askRuntime(prompt: Prompt, defaultValue: string): Promise<string>
183
198
  }
184
199
  }
185
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
+
186
224
  async function askYesNo(prompt: Prompt, label: string, defaultValue = false): Promise<boolean> {
187
225
  const fallback = defaultValue ? 'Y/n' : 'y/N';
188
226
  while (true) {
@@ -241,7 +279,7 @@ async function collectComputerOnboardInput(serverClient: LockClient, flags: Reco
241
279
  const interactive = boolFlag(flags, 'interactive') || (!hasAnyProvisionFlag && process.stdin.isTTY === true);
242
280
  const defaultName = flag(flags, 'name') ?? 'Local computer';
243
281
  const defaultServerUrl = flag(flags, 'server-url') ?? serverClient.baseUrl;
244
- const defaultPackageName = flag(flags, 'package-name') ?? '@controlflow-ai/daemon@latest';
282
+ const defaultPackageName = flag(flags, 'package-name') ?? 'bun run daemon';
245
283
 
246
284
  if (!interactive) {
247
285
  return {
@@ -257,12 +295,12 @@ async function collectComputerOnboardInput(serverClient: LockClient, flags: Reco
257
295
  console.log('Provision a computer credential and daemon start command.');
258
296
  const name = await askRequired(prompt, 'Computer display name', defaultName);
259
297
  const serverUrl = await askRequired(prompt, 'Server URL', defaultServerUrl);
260
- const packageName = await askRequired(prompt, 'Daemon package', defaultPackageName);
298
+ const packageName = await askRequired(prompt, 'Daemon command/package', defaultPackageName);
261
299
  console.log('');
262
300
  console.log('Summary:');
263
301
  console.log(` name: ${name}`);
264
302
  console.log(` server_url: ${serverUrl}`);
265
- console.log(` package_name: ${packageName}`);
303
+ console.log(` command_prefix: ${packageName}`);
266
304
  if (!await askYesNo(prompt, 'Provision this computer?', true)) throw new Error('computer onboarding cancelled');
267
305
  return { name, serverUrl, packageName };
268
306
  } finally {
@@ -329,7 +367,7 @@ async function collectLarkBindFlags(prompt: Prompt, flags: Record<string, string
329
367
  };
330
368
  }
331
369
 
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> }> {
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> }> {
333
371
  const hasRequiredFlags = Boolean(flag(flags, 'key') && flag(flags, 'name'));
334
372
  const interactive = boolFlag(flags, 'interactive') || (!hasRequiredFlags && process.stdin.isTTY === true);
335
373
  if (!interactive) {
@@ -338,6 +376,7 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
338
376
  agentKey: flag(flags, 'key')!,
339
377
  displayName: flag(flags, 'name')!,
340
378
  runtime: flag(flags, 'runtime') ?? 'codex',
379
+ model: await validateRuntimeModel(flag(flags, 'runtime') ?? 'codex', flag(flags, 'model')),
341
380
  desc: flag(flags, 'desc') ?? null,
342
381
  computerId: flag(flags, 'computer-id'),
343
382
  larkFlags: flag(flags, 'lark-app-id') || flag(flags, 'app-id') ? flags : undefined,
@@ -351,6 +390,7 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
351
390
  const agentKey = await askRequired(prompt, 'Agent key', flag(flags, 'key') ?? 'codex');
352
391
  const displayName = await askRequired(prompt, 'Display name', flag(flags, 'name') ?? agentKey);
353
392
  const runtime = await askRuntime(prompt, flag(flags, 'runtime') ?? 'codex');
393
+ const model = await askModel(prompt, runtime, flag(flags, 'model') ?? '');
354
394
  const desc = await ask(prompt, 'Description (optional)', flag(flags, 'desc') ?? '');
355
395
  const shouldAssign = Boolean(flag(flags, 'computer-id')) || await askYesNo(prompt, 'Assign this agent to a computer?', false);
356
396
  const computerId = shouldAssign
@@ -361,6 +401,7 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
361
401
  console.log(` agent_key: ${agentKey}`);
362
402
  console.log(` display_name: ${displayName}`);
363
403
  console.log(` runtime: ${runtime}`);
404
+ console.log(` model: ${model || '-'}`);
364
405
  console.log(` description: ${desc || '-'}`);
365
406
  console.log(` computer_id: ${computerId || '-'}`);
366
407
  if (!await askYesNo(prompt, 'Create/update this agent?', true)) throw new Error('onboarding cancelled');
@@ -370,6 +411,7 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
370
411
  agentKey,
371
412
  displayName,
372
413
  runtime,
414
+ model: model || null,
373
415
  desc: desc || null,
374
416
  computerId: computerId || undefined,
375
417
  larkFlags,
@@ -380,13 +422,47 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
380
422
  }
381
423
 
382
424
  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
425
  const prompt = await createPrompt();
386
426
  try {
387
- console.log('Agent Lark settings');
427
+ console.log('Agent settings');
388
428
  const agents = await listAgentRecords(serverClient);
389
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
+
390
466
  const configPath = larkConfigPath(flags);
391
467
  const store = loadLarkCredentials(configPath);
392
468
  const existing = findCredentialByAgent(store, agentKey);
@@ -396,7 +472,20 @@ async function collectAgentUpdateInput(serverClient: LockClient, flags: Record<s
396
472
  console.log('Current Lark bot: -');
397
473
  }
398
474
 
399
- const defaultAction = existing ? '1' : '1';
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';
400
489
  console.log('Lark action:');
401
490
  console.log(` 1. ${existing ? 'Bind/rebind bot' : 'Bind bot'}`);
402
491
  if (existing) console.log(' 2. Unbind bot');
@@ -432,6 +521,32 @@ async function collectAgentUpdateInput(serverClient: LockClient, flags: Record<s
432
521
  }
433
522
  }
434
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
+
435
550
  async function collectLarkUserInput(flags: Record<string, string | boolean>): Promise<{ userId: string; displayName: string | null }> {
436
551
  const userIdFlag = flag(flags, 'user-id') ?? flag(flags, 'id');
437
552
  const interactive = boolFlag(flags, 'interactive') || (!userIdFlag && process.stdin.isTTY === true);
@@ -569,6 +684,52 @@ async function main(): Promise<void> {
569
684
  return;
570
685
  }
571
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
+
572
733
  throw new Error(`unknown ${args.command} subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
573
734
  }
574
735
 
@@ -581,7 +742,7 @@ async function main(): Promise<void> {
581
742
  printJson(agents);
582
743
  } else {
583
744
  for (const agent of agents) {
584
- 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 ?? '-'}`);
585
746
  }
586
747
  }
587
748
  return;
@@ -591,6 +752,7 @@ async function main(): Promise<void> {
591
752
  const agentKey = flag(args.flags, 'key');
592
753
  const displayName = flag(args.flags, 'name');
593
754
  const runtime = flag(args.flags, 'runtime');
755
+ const model = flag(args.flags, 'model');
594
756
  const desc = flag(args.flags, 'desc');
595
757
  if (!agentKey) throw new Error('--key is required');
596
758
  if (!displayName) throw new Error('--name is required');
@@ -602,6 +764,7 @@ async function main(): Promise<void> {
602
764
  agent_key: agentKey,
603
765
  display_name: displayName,
604
766
  runtime: runtime ?? null,
767
+ model: await validateRuntimeModel(runtime ?? '', model),
605
768
  description: desc ?? null,
606
769
  }),
607
770
  });
@@ -612,7 +775,7 @@ async function main(): Promise<void> {
612
775
  }
613
776
 
614
777
  if (sub === 'onboard') {
615
- const { agentKey, displayName, runtime, desc, computerId, larkFlags } = await collectOnboardInput(serverClient, args.flags);
778
+ const { agentKey, displayName, runtime, model, desc, computerId, larkFlags } = await collectOnboardInput(serverClient, args.flags);
616
779
  if (!agentKey) throw new Error('agent key is required');
617
780
  if (!displayName) throw new Error('display name is required');
618
781
 
@@ -623,6 +786,7 @@ async function main(): Promise<void> {
623
786
  agent_key: agentKey,
624
787
  display_name: displayName,
625
788
  runtime,
789
+ model,
626
790
  description: desc,
627
791
  computer_id: computerId,
628
792
  }),
@@ -640,18 +804,29 @@ async function main(): Promise<void> {
640
804
  : { agentKey: flag(args.flags, 'key') ?? '', flags: args.flags };
641
805
  const agentKey = updateInput.agentKey;
642
806
  const updateFlags = updateInput.flags;
643
- const runtime = flag(args.flags, 'runtime');
807
+ const runtime = flag(updateFlags, 'runtime');
808
+ const hasModel = Object.prototype.hasOwnProperty.call(updateFlags, 'model');
809
+ const model = flag(updateFlags, 'model');
644
810
  if (!agentKey) throw new Error('--key is required');
645
811
  const hasLarkBinding = Boolean(flag(updateFlags, 'lark-app-id') || flag(updateFlags, 'app-id') || flag(updateFlags, 'lark-app-secret') || flag(updateFlags, 'app-secret'));
646
812
  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');
813
+ if (!runtime && !hasModel && !hasLarkBinding && !hasLarkUnbind) throw new Error('--runtime, --model, --lark-app-id/--lark-app-secret, or --unbind-lark is required');
648
814
 
649
815
  let agent: Record<string, unknown> | undefined;
650
- if (runtime) {
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 : '');
651
823
  const response = await fetch(`${serverClient.baseUrl}/api/agents/${encodeURIComponent(agentKey)}`, {
652
824
  method: 'PATCH',
653
825
  headers: { 'content-type': 'application/json' },
654
- body: JSON.stringify({ runtime }),
826
+ body: JSON.stringify({
827
+ ...(runtime ? { runtime } : {}),
828
+ ...(hasModel ? { model: await validateRuntimeModel(runtimeForModel, model) } : {}),
829
+ }),
655
830
  });
656
831
  const payload = await response.json() as { data?: { agent: Record<string, unknown> }; message?: string };
657
832
  if (!response.ok) throw new Error(payload.message ?? `update failed: ${response.status}`);
@@ -667,6 +842,36 @@ async function main(): Promise<void> {
667
842
  return;
668
843
  }
669
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
+ }
872
+ return;
873
+ }
874
+
670
875
  throw new Error(`unknown agents subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
671
876
  }
672
877
 
@@ -710,6 +915,11 @@ async function main(): Promise<void> {
710
915
  throw new Error(`unknown lark-users subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
711
916
  }
712
917
 
918
+ if (args.command === 'messaging') {
919
+ await runMessagingCommand(serverClient, args, usage);
920
+ return;
921
+ }
922
+
713
923
  if (args.command === 'lark') {
714
924
  const { runLarkCli } = await import('./lark/cli.js');
715
925
  const code = await runLarkCli({