@controlflow-ai/daemon 0.1.0 → 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/src/console.ts CHANGED
@@ -4,7 +4,10 @@ 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 type { Computer } from './types.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';
10
+ import type { Computer, ProvisionedComputer } from './types.js';
8
11
 
9
12
  interface Prompt {
10
13
  askLine(label: string): Promise<string>;
@@ -21,17 +24,107 @@ Usage:
21
24
  bun run src/console.ts inbox --agent neeko [--after 0] [--limit 50] [--json]
22
25
  bun run src/console.ts runs [--json]
23
26
  bun run src/console.ts run-action <run-id> kill|restart
27
+ bun run src/console.ts computers list [--json]
28
+ bun run src/console.ts computer onboard [--interactive] [--name <display-name>] [--server-url <url>] [--package-name <npm-package>]
24
29
  bun run src/console.ts agents list [--json]
25
30
  bun run src/console.ts agents onboard [--interactive] [--key <agent-key>] [--name <display-name>] [--runtime codex] [--computer-id <machine>]
26
31
  bun run src/console.ts agents create --key <agent-key> --name <display-name> [--runtime neeko|coco|coco-stream-json|codex] [--desc <description>]
27
- bun run src/console.ts agents update --key <agent-key> --runtime neeko|coco|coco-stream-json|codex
28
- bun run src/console.ts lark <setup|list|daemon|events|send> [flags]
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]
34
+ bun run src/console.ts lark-users list [--json]
35
+ bun run src/console.ts lark-users add [--interactive] --user-id <union-id> [--name <display-name>]
36
+ bun run src/console.ts lark-users delete --user-id <union-id>
37
+ bun run src/console.ts lark <list|daemon|events|send> [flags]
29
38
 
30
39
  Environment:
31
40
  PAL_SERVER=${defaultServerUrl()}
32
41
  `;
33
42
  }
34
43
 
44
+ async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
45
+ const response = await fetch(url, init);
46
+ const payload = await response.json() as { data?: T; message?: string; code?: string };
47
+ if (!response.ok) throw new Error(payload.message ?? payload.code ?? `request failed: ${response.status}`);
48
+ return payload.data as T;
49
+ }
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
+
35
128
  function printJson(value: unknown): void {
36
129
  console.log(JSON.stringify(value, null, 2));
37
130
  }
@@ -120,7 +213,123 @@ async function askComputerId(prompt: Prompt, computers: Computer[]): Promise<str
120
213
  }
121
214
  }
122
215
 
123
- async function collectOnboardInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ agentKey: string; displayName: string; runtime: string; desc: string | null; computerId: string | undefined }> {
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
+
239
+ async function collectComputerOnboardInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ name: string; serverUrl: string; packageName: string }> {
240
+ const hasAnyProvisionFlag = Boolean(flag(flags, 'name') || flag(flags, 'server-url') || flag(flags, 'package-name'));
241
+ const interactive = boolFlag(flags, 'interactive') || (!hasAnyProvisionFlag && process.stdin.isTTY === true);
242
+ const defaultName = flag(flags, 'name') ?? 'Local computer';
243
+ const defaultServerUrl = flag(flags, 'server-url') ?? serverClient.baseUrl;
244
+ const defaultPackageName = flag(flags, 'package-name') ?? '@controlflow-ai/daemon@latest';
245
+
246
+ if (!interactive) {
247
+ return {
248
+ name: defaultName,
249
+ serverUrl: defaultServerUrl,
250
+ packageName: defaultPackageName,
251
+ };
252
+ }
253
+
254
+ const prompt = await createPrompt();
255
+ try {
256
+ console.log('Computer onboarding');
257
+ console.log('Provision a computer credential and daemon start command.');
258
+ const name = await askRequired(prompt, 'Computer display name', defaultName);
259
+ const serverUrl = await askRequired(prompt, 'Server URL', defaultServerUrl);
260
+ const packageName = await askRequired(prompt, 'Daemon package', defaultPackageName);
261
+ console.log('');
262
+ console.log('Summary:');
263
+ console.log(` name: ${name}`);
264
+ console.log(` server_url: ${serverUrl}`);
265
+ console.log(` package_name: ${packageName}`);
266
+ if (!await askYesNo(prompt, 'Provision this computer?', true)) throw new Error('computer onboarding cancelled');
267
+ return { name, serverUrl, packageName };
268
+ } finally {
269
+ prompt.close();
270
+ }
271
+ }
272
+
273
+ function printProvisionedComputer(provisioned: ProvisionedComputer): void {
274
+ console.log('Computer onboarded');
275
+ console.log(` id: ${provisioned.computer.id}`);
276
+ console.log(` name: ${provisioned.computer.name}`);
277
+ console.log(` status: ${provisioned.computer.status}`);
278
+ console.log('');
279
+ console.log('Daemon command:');
280
+ console.log(provisioned.command);
281
+ }
282
+
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> }> {
124
333
  const hasRequiredFlags = Boolean(flag(flags, 'key') && flag(flags, 'name'));
125
334
  const interactive = boolFlag(flags, 'interactive') || (!hasRequiredFlags && process.stdin.isTTY === true);
126
335
  if (!interactive) {
@@ -131,6 +340,7 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
131
340
  runtime: flag(flags, 'runtime') ?? 'codex',
132
341
  desc: flag(flags, 'desc') ?? null,
133
342
  computerId: flag(flags, 'computer-id'),
343
+ larkFlags: flag(flags, 'lark-app-id') || flag(flags, 'app-id') ? flags : undefined,
134
344
  };
135
345
  }
136
346
 
@@ -154,18 +364,99 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
154
364
  console.log(` description: ${desc || '-'}`);
155
365
  console.log(` computer_id: ${computerId || '-'}`);
156
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;
157
369
  return {
158
370
  agentKey,
159
371
  displayName,
160
372
  runtime,
161
373
  desc: desc || null,
162
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,
163
429
  };
164
430
  } finally {
165
431
  prompt.close();
166
432
  }
167
433
  }
168
434
 
435
+ async function collectLarkUserInput(flags: Record<string, string | boolean>): Promise<{ userId: string; displayName: string | null }> {
436
+ const userIdFlag = flag(flags, 'user-id') ?? flag(flags, 'id');
437
+ const interactive = boolFlag(flags, 'interactive') || (!userIdFlag && process.stdin.isTTY === true);
438
+ if (!interactive) {
439
+ if (!userIdFlag) throw new Error('lark-users add requires --user-id when not running interactively');
440
+ return { userId: userIdFlag, displayName: flag(flags, 'name') ?? null };
441
+ }
442
+
443
+ const prompt = await createPrompt();
444
+ try {
445
+ console.log('Lark authorized user');
446
+ const userId = await askRequired(prompt, 'Lark user ID', userIdFlag ?? '');
447
+ const displayName = await ask(prompt, 'Display name (optional)', flag(flags, 'name') ?? '');
448
+ console.log('');
449
+ console.log('Summary:');
450
+ console.log(` user_id: ${userId}`);
451
+ console.log(` display_name: ${displayName || '-'}`);
452
+ if (!await askYesNo(prompt, 'Authorize this Lark user?', true)) throw new Error('lark user add cancelled');
453
+ return { userId, displayName: displayName || null };
454
+ } finally {
455
+ prompt.close();
456
+ }
457
+ }
458
+
459
+
169
460
  async function main(): Promise<void> {
170
461
  const args = parseArgs();
171
462
  const serverClient = new LockClient(flag(args.flags, 'server'));
@@ -247,14 +538,45 @@ async function main(): Promise<void> {
247
538
  return;
248
539
  }
249
540
 
541
+ if (args.command === 'computer' || args.command === 'computers') {
542
+ const sub = args.values[0];
543
+
544
+ if (sub === 'list') {
545
+ const computers = await serverClient.listComputers();
546
+ if (args.flags.json) {
547
+ printJson(computers);
548
+ } else {
549
+ for (const computer of computers) {
550
+ const seen = computer.last_seen_at ? ` last_seen=${computer.last_seen_at}` : '';
551
+ console.log(`${computer.id} name="${computer.name}" status=${computer.status}${seen}`);
552
+ }
553
+ }
554
+ return;
555
+ }
556
+
557
+ if (sub === 'onboard') {
558
+ const input = await collectComputerOnboardInput(serverClient, args.flags);
559
+ const provisioned = await serverClient.provisionComputer({
560
+ name: input.name,
561
+ server_url: input.serverUrl,
562
+ package_name: input.packageName,
563
+ });
564
+ if (args.flags.json) {
565
+ printJson(provisioned);
566
+ } else {
567
+ printProvisionedComputer(provisioned);
568
+ }
569
+ return;
570
+ }
571
+
572
+ throw new Error(`unknown ${args.command} subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
573
+ }
574
+
250
575
  if (args.command === 'agents') {
251
576
  const sub = args.values[0];
252
577
 
253
578
  if (sub === 'list') {
254
- const response = await fetch(`${serverClient.baseUrl}/api/agents`);
255
- const payload = await response.json() as { data?: { agents: Array<Record<string, unknown>> }; message?: string };
256
- if (!response.ok) throw new Error(payload.message ?? `list failed: ${response.status}`);
257
- const agents = payload.data?.agents ?? [];
579
+ const agents = await listAgentRecords(serverClient);
258
580
  if (args.flags.json) {
259
581
  printJson(agents);
260
582
  } else {
@@ -290,7 +612,7 @@ async function main(): Promise<void> {
290
612
  }
291
613
 
292
614
  if (sub === 'onboard') {
293
- const { agentKey, displayName, runtime, desc, computerId } = await collectOnboardInput(serverClient, args.flags);
615
+ const { agentKey, displayName, runtime, desc, computerId, larkFlags } = await collectOnboardInput(serverClient, args.flags);
294
616
  if (!agentKey) throw new Error('agent key is required');
295
617
  if (!displayName) throw new Error('display name is required');
296
618
 
@@ -307,28 +629,85 @@ async function main(): Promise<void> {
307
629
  });
308
630
  const payload = await response.json() as { data?: Record<string, unknown>; message?: string };
309
631
  if (!response.ok) throw new Error(payload.message ?? `onboard failed: ${response.status}`);
632
+ if (larkFlags) await bindLarkBotToAgent(serverClient, larkFlags, agentKey);
310
633
  printJson(payload.data);
311
634
  return;
312
635
  }
313
636
 
314
637
  if (sub === 'update') {
315
- const agentKey = flag(args.flags, 'key');
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;
316
643
  const runtime = flag(args.flags, 'runtime');
317
644
  if (!agentKey) throw new Error('--key is required');
318
- if (!runtime) throw new Error('--runtime is required');
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
+ }
663
+
664
+ if (hasLarkUnbind) await unbindLarkBotFromAgent(serverClient, updateFlags, agentKey);
665
+ else await bindLarkBotToAgent(serverClient, updateFlags, agentKey);
666
+ printJson(agent);
667
+ return;
668
+ }
669
+
670
+ throw new Error(`unknown agents subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
671
+ }
319
672
 
320
- const response = await fetch(`${serverClient.baseUrl}/api/agents/${encodeURIComponent(agentKey)}`, {
321
- method: 'PATCH',
673
+ if (args.command === 'lark-users') {
674
+ const sub = args.values[0];
675
+
676
+ if (sub === 'list') {
677
+ const data = await requestJson<{ users: Array<Record<string, unknown>> }>(`${serverClient.baseUrl}/api/lark/authorized-users`);
678
+ if (args.flags.json) {
679
+ printJson(data.users);
680
+ } else if (data.users.length === 0) {
681
+ console.log('No authorized Lark users.');
682
+ } else {
683
+ for (const user of data.users) {
684
+ console.log(`${user.user_id} name="${user.display_name ?? '-'}" added=${user.created_at}`);
685
+ }
686
+ }
687
+ return;
688
+ }
689
+
690
+ if (sub === 'add') {
691
+ const input = await collectLarkUserInput(args.flags);
692
+ const data = await requestJson<{ user: Record<string, unknown> }>(`${serverClient.baseUrl}/api/lark/authorized-users`, {
693
+ method: 'POST',
322
694
  headers: { 'content-type': 'application/json' },
323
- body: JSON.stringify({ runtime }),
695
+ body: JSON.stringify({ user_id: input.userId, display_name: input.displayName }),
324
696
  });
325
- const payload = await response.json() as { data?: { agent: Record<string, unknown> }; message?: string };
326
- if (!response.ok) throw new Error(payload.message ?? `update failed: ${response.status}`);
327
- printJson(payload.data?.agent);
697
+ if (args.flags.json) printJson(data.user);
698
+ else console.log(`Authorized Lark user ${data.user.user_id}`);
328
699
  return;
329
700
  }
330
701
 
331
- throw new Error(`unknown agents subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
702
+ if (sub === 'delete' || sub === 'remove') {
703
+ const userId = flag(args.flags, 'user-id') ?? flag(args.flags, 'id') ?? args.values[1];
704
+ if (!userId) throw new Error('--user-id is required');
705
+ await requestJson<{ deleted: boolean }>(`${serverClient.baseUrl}/api/lark/authorized-users/${encodeURIComponent(userId)}`, { method: 'DELETE' });
706
+ console.log(`Deleted Lark user ${userId}`);
707
+ return;
708
+ }
709
+
710
+ throw new Error(`unknown lark-users subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
332
711
  }
333
712
 
334
713
  if (args.command === 'lark') {
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 roomParticipants = await client.listRoomMembers(message.chat_id)
157
- .then((result) => result.participants)
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: options.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) {