@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/README.md +21 -19
- package/package.json +14 -3
- package/src/agent-runtime.ts +15 -0
- package/src/app.ts +314 -3
- package/src/client.ts +11 -3
- package/src/console.ts +397 -18
- package/src/daemon.ts +25 -6
- package/src/db.ts +181 -6
- package/src/lark/app-registration.ts +141 -0
- package/src/lark/cli.ts +4 -134
- package/src/lark/credentials.ts +36 -3
- package/src/lark/event-router.ts +22 -2
- package/src/lark/server-integration.ts +9 -16
- package/src/lark/setup.ts +74 -5
- package/src/local-api.ts +69 -2
- package/src/local-auth.ts +4 -3
- package/src/migrations/022_lark_authorized_users.ts +16 -0
- package/src/migrations/023_projects.ts +65 -0
- package/src/migrations.ts +3 -1
- package/src/network.ts +24 -0
- package/src/server.ts +21 -7
- package/src/types.ts +40 -0
- package/src/web.ts +368 -29
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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({
|
|
695
|
+
body: JSON.stringify({ user_id: input.userId, display_name: input.displayName }),
|
|
324
696
|
});
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
|
157
|
-
.then((result) => result
|
|
156
|
+
const roomSnapshot = await client.listRoomMembers(message.chat_id)
|
|
157
|
+
.then((result) => result)
|
|
158
158
|
.catch((error) => {
|
|
159
159
|
console.warn(`${logPrefix} room participant snapshot unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
160
|
-
return
|
|
160
|
+
return null;
|
|
161
161
|
});
|
|
162
|
+
const roomParticipants = roomSnapshot?.participants ?? [];
|
|
163
|
+
const roomProject = roomSnapshot?.room.project_id ? {
|
|
164
|
+
id: roomSnapshot.room.project_id,
|
|
165
|
+
name: roomSnapshot.room.project_name ?? roomSnapshot.room.project_id,
|
|
166
|
+
rootPath: roomSnapshot.room.project_root_path ?? '',
|
|
167
|
+
computerId: roomSnapshot.room.project_computer_id ?? '',
|
|
168
|
+
computerName: roomSnapshot.room.project_computer_name,
|
|
169
|
+
} : null;
|
|
170
|
+
const projectAccessible = Boolean(roomProject && options.computerId && roomProject.computerId === options.computerId);
|
|
171
|
+
const effectiveProjectCwd = projectAccessible && roomProject?.rootPath ? roomProject.rootPath : options.projectCwd;
|
|
162
172
|
const recentMessages = await client.getMessages(new URLSearchParams({
|
|
163
173
|
chat_id: message.chat_id,
|
|
164
174
|
after: String(Math.max(0, message.id - 50)),
|
|
@@ -176,7 +186,12 @@ async function executeRun(client: LockClient, delivery: MessageDelivery, message
|
|
|
176
186
|
message,
|
|
177
187
|
cwd: options.agentHome,
|
|
178
188
|
agentHome: options.agentHome,
|
|
179
|
-
projectCwd:
|
|
189
|
+
projectCwd: effectiveProjectCwd,
|
|
190
|
+
projectContext: roomProject ? {
|
|
191
|
+
...roomProject,
|
|
192
|
+
accessible: projectAccessible,
|
|
193
|
+
currentComputerId: options.computerId,
|
|
194
|
+
} : undefined,
|
|
180
195
|
extraArgs: options.extraArgs,
|
|
181
196
|
localDaemonUrl: options.localDaemonUrl,
|
|
182
197
|
localDaemonToken: options.localDaemonToken,
|
|
@@ -377,6 +392,7 @@ async function reconcileManagedAgents(input: {
|
|
|
377
392
|
computerId: string;
|
|
378
393
|
serverUrl: string;
|
|
379
394
|
defaultCwd: string;
|
|
395
|
+
localUrl?: string;
|
|
380
396
|
}): Promise<void> {
|
|
381
397
|
const desiredAssignedAgents = new Set(input.assignments.map((assignment) => assignment.agent));
|
|
382
398
|
|
|
@@ -403,6 +419,7 @@ async function reconcileManagedAgents(input: {
|
|
|
403
419
|
await input.client.registerDaemon({
|
|
404
420
|
id: input.daemonId,
|
|
405
421
|
name: input.computerId,
|
|
422
|
+
local_url: input.localUrl,
|
|
406
423
|
server_url: input.serverUrl,
|
|
407
424
|
agents: daemonAgentPayload(input.managedAgents),
|
|
408
425
|
});
|
|
@@ -473,6 +490,8 @@ async function main(): Promise<void> {
|
|
|
473
490
|
|
|
474
491
|
const daemonAuth = { computer_id: connected.computer.id, connection_id: connected.connection.id, token: connected.token };
|
|
475
492
|
const client = new LockClient(serverUrl, daemonAuth);
|
|
493
|
+
const localServer = startLocalApi({ host: localHost, port: localPort, serverUrl, token: localToken, controlToken: connected.local_control_token, daemonAuth });
|
|
494
|
+
const localUrl = `http://${localServer.hostname}:${localServer.port}`;
|
|
476
495
|
await reconcileManagedAgents({
|
|
477
496
|
assignments: connected.agents ?? [],
|
|
478
497
|
explicitAgents,
|
|
@@ -483,6 +502,7 @@ async function main(): Promise<void> {
|
|
|
483
502
|
computerId: connected.computer.id,
|
|
484
503
|
serverUrl,
|
|
485
504
|
defaultCwd: cwd,
|
|
505
|
+
localUrl,
|
|
486
506
|
});
|
|
487
507
|
if (managedAgents.size === 0) {
|
|
488
508
|
console.log(`[daemon] no agents currently assigned to computer ${connected.computer.id}; waiting for assignments`);
|
|
@@ -491,8 +511,6 @@ async function main(): Promise<void> {
|
|
|
491
511
|
let connectionRevoked = false;
|
|
492
512
|
const heartbeatMs = numberFlag(args.flags, 'heartbeat-interval', 5000)!;
|
|
493
513
|
|
|
494
|
-
const localServer = startLocalApi({ host: localHost, port: localPort, serverUrl, token: localToken, daemonAuth });
|
|
495
|
-
|
|
496
514
|
console.log(`pal daemon computer=${connected.computer.id} connection=${connected.connection.id} agents=${Array.from(managedAgents.values()).map((managed) => `${managed.agent}:${managed.runtimeProvider}`).join(',') || 'none'} server=${serverUrl}`);
|
|
497
515
|
console.log(`local api=http://${localServer.hostname}:${localServer.port} token=${localToken ? 'set' : 'missing'}`);
|
|
498
516
|
console.log(`state=${statePath} lastSeenId=${state.lastSeenId}`);
|
|
@@ -525,6 +543,7 @@ async function main(): Promise<void> {
|
|
|
525
543
|
computerId: connected.computer.id,
|
|
526
544
|
serverUrl,
|
|
527
545
|
defaultCwd: cwd,
|
|
546
|
+
localUrl,
|
|
528
547
|
});
|
|
529
548
|
writeState(statePath, state);
|
|
530
549
|
} catch (error) {
|