@controlflow-ai/daemon 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,32 +103,24 @@ bun run console -- agents list [--json]
103
103
  # Create or update an agent record
104
104
  bun run console -- agents create --key <agent-key> --name <display-name> [--runtime neeko|coco|codex] [--desc <description>]
105
105
 
106
- # Create/update an agent and optionally assign it to a computer
106
+ # Create/update an agent, optionally assign it to a computer, and optionally set up Lark at the end
107
107
  bun run console -- agents onboard --key <agent-key> --name <display-name> [--runtime codex] [--desc <description>] [--computer-id <machine-id>]
108
+ bun run console -- agents onboard --interactive
108
109
 
109
- # Update only the runtime
110
- bun run console -- agents update --key <agent-key> --runtime neeko|coco|codex
110
+ # Update runtime and/or manage the Lark bot bound to the agent
111
+ bun run console -- agents update --key <agent-key> [--runtime neeko|coco|codex]
112
+ bun run console -- agents update --key <agent-key> --lark-app-id <app-id> --lark-app-secret <app-secret> [--lark-label <name>] [--rebind-lark]
113
+ bun run console -- agents update --key <agent-key> --unbind-lark
114
+ bun run console -- agents update --interactive
111
115
  ```
112
116
 
113
117
  `codex` is the validated runtime for the current demo environment. Do not use `neeko` or `coco` until their binaries and adapters are available on the runtime host.
114
118
 
119
+ `agents onboard --interactive` asks at the end whether to set up a Lark bot. Lark setup can create a Feishu app by QR scan/open-link, or accept a pasted App ID/App Secret. `agents update --interactive` manages Lark binding, rebind, and unbind for existing agents.
120
+
115
121
  ### Lark Bots
116
122
 
117
123
  ```bash
118
- # Configure a bot, resolve its bot open_id, bind it to an agent, and ask the running server to reload Lark
119
- bun run console -- lark setup --app-id <app-id> --app-secret <app-secret> --label <name> --agent <agent-key>
120
-
121
- # Configure a bot and create/update the bound agent in one command
122
- bun run console -- lark setup \
123
- --app-id <app-id> \
124
- --app-secret <app-secret> \
125
- --label <name> \
126
- --agent codex \
127
- --create-agent \
128
- --agent-name "Codex" \
129
- --runtime codex \
130
- --computer-id <machine-id>
131
-
132
124
  # List configured bots; secrets are redacted
133
125
  bun run console -- lark list
134
126
 
@@ -139,7 +131,9 @@ bun run console -- lark events --limit 20
139
131
  bun run console -- lark send --app-id <app-id> --to <receive_id> [--to-type chat_id|open_id|union_id|email|user_id] "Hello"
140
132
  ```
141
133
 
142
- `lark setup` writes `~/.pal/lark.json` or `PAL_LARK_CONFIG` with mode `0600`. By default it calls `POST /api/lark/reload` on the running server after a successful write. Use `--no-reload` when you want to edit or validate the file first, then trigger reload manually:
134
+ Lark bot binding is managed as part of agent settings. `agents update --lark-app-id ... --lark-app-secret ...` validates the credential, resolves `bot open_id`, writes `~/.pal/lark.json` or `PAL_LARK_CONFIG`, and asks the running server to reload Lark integration. An agent may only be bound to one Lark bot at a time; use `--rebind-lark` to move that agent from its previous bot to the new bot, or `--unbind-lark` to clear the binding while keeping the credential.
135
+
136
+ The Lark config file is written with mode `0600`. Use `--no-reload` when you want to edit or validate the file first, then trigger reload manually:
143
137
 
144
138
  ```bash
145
139
  curl -s -X POST http://127.0.0.1:4127/api/lark/reload
package/package.json CHANGED
@@ -1,22 +1,33 @@
1
1
  {
2
2
  "name": "@controlflow-ai/daemon",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "server": "bun run src/server.ts",
7
7
  "daemon": "bun run src/daemon.ts",
8
8
  "cli": "bun run src/cli.ts",
9
9
  "console": "bun run src/console.ts",
10
+ "web:dev": "vite --config web/app/vite.config.ts --host 127.0.0.1",
11
+ "web:build": "vite build --config web/app/vite.config.ts",
12
+ "web:preview": "vite preview --config web/app/vite.config.ts --host 127.0.0.1",
10
13
  "typecheck": "tsc --noEmit",
11
14
  "test": "bun test",
12
15
  "check": "bun run typecheck && bun test"
13
16
  },
14
17
  "devDependencies": {
15
18
  "@types/bun": "latest",
16
- "typescript": "^5.9.3"
19
+ "@types/qrcode": "^1.5.6",
20
+ "@types/react": "^19.2.15",
21
+ "@types/react-dom": "^19.2.3",
22
+ "@vitejs/plugin-react": "^6.0.2",
23
+ "typescript": "^5.9.3",
24
+ "vite": "^8.0.14"
17
25
  },
18
26
  "dependencies": {
19
- "@larksuiteoapi/node-sdk": "^1.59.0"
27
+ "@larksuiteoapi/node-sdk": "^1.59.0",
28
+ "qrcode": "^1.5.4",
29
+ "react": "^19.2.6",
30
+ "react-dom": "^19.2.6"
20
31
  },
21
32
  "bin": {
22
33
  "daemon": "bin/daemon.js",
@@ -9,6 +9,15 @@ export interface AgentRuntimeRunInput {
9
9
  cwd: string;
10
10
  agentHome?: string;
11
11
  projectCwd?: string;
12
+ projectContext?: {
13
+ id: string;
14
+ name: string;
15
+ rootPath: string;
16
+ computerId: string;
17
+ computerName?: string | null;
18
+ accessible: boolean;
19
+ currentComputerId?: string | null;
20
+ };
12
21
  extraArgs: string[];
13
22
  localDaemonUrl?: string;
14
23
  localDaemonToken?: string;
@@ -69,6 +78,11 @@ export function buildPalPrompt(input: AgentRuntimeRunInput): string {
69
78
  const chatName = sanitizeProviderIds(input.message.chat_name);
70
79
  const sender = sanitizeProviderIds(input.message.sender);
71
80
  const content = sanitizeProviderIds(input.message.content);
81
+ const projectContext = input.projectContext
82
+ ? input.projectContext.accessible
83
+ ? `\nProject context:\n- You are working in project: ${sanitizeProviderIds(input.projectContext.name)}\n- Project computer: ${sanitizeProviderIds(input.projectContext.computerName || input.projectContext.computerId)}\n- Project path: ${input.projectContext.rootPath}\n- This agent is running on the project computer and can use the project workspace path above.`
84
+ : `\nProject context:\n- This room belongs to project: ${sanitizeProviderIds(input.projectContext.name)}\n- Project computer: ${sanitizeProviderIds(input.projectContext.computerName || input.projectContext.computerId)}\n- Project path: ${input.projectContext.rootPath}\n- This agent is currently running on computer: ${sanitizeProviderIds(input.projectContext.currentComputerId || 'unknown')}\n- The computers differ, so the project path is temporarily not accessible from this agent run. Do not claim to inspect or change that path unless access is restored.`
85
+ : '';
72
86
 
73
87
  return `You are ${input.agent}, a long-running PAL coding agent connected to the pal chat server.
74
88
 
@@ -79,6 +93,7 @@ Workspace contract:
79
93
  - Your cwd is for identity, memory, recovery state, scratch files, and durable local notes.
80
94
  - Your cwd is not the project repository.
81
95
  - The project workspace for source inspection, code changes, tests, and Git work is: ${projectCwd}
96
+ ${projectContext}
82
97
 
83
98
  Startup recovery:
84
99
  - Read MEMORY.md in your cwd first when it exists.
package/src/app.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import { artifactHeaders, artifactViewerHtml } from './artifacts.js';
2
+ import { existsSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { join } from 'node:path';
5
+ import QRCode from 'qrcode';
2
6
  import { MessageStore } from './db.js';
3
7
  import { failure, HttpError, json, numberParam, readJson, stringParam } from './http.js';
4
8
  import { assertServerAuth } from './server-auth.js';
@@ -7,6 +11,7 @@ import type { RunAction, RunStatus } from './types.js';
7
11
  import { createLarkApiClient, sendTextMessage } from './lark/ws-daemon.js';
8
12
  import { boundAgents, defaultLarkConfigPath, loadLarkCredentials, type LarkCredentialStore } from './lark/credentials.js';
9
13
  import { persistLarkCredential, resolveLarkBotInfo } from './lark/setup.js';
14
+ import { beginLarkAppRegistration, pollLarkAppRegistration, type LarkRegistrationComplete } from './lark/app-registration.js';
10
15
  import { tailscaleAddress } from './network.js';
11
16
 
12
17
  interface SendBody {
@@ -29,6 +34,12 @@ interface CreateRoomBody {
29
34
  kind?: 'group' | 'dm';
30
35
  }
31
36
 
37
+ interface CreateProjectBody {
38
+ name?: string;
39
+ computer_id?: string;
40
+ root_path?: string;
41
+ }
42
+
32
43
  interface StartRunBody {
33
44
  message_id?: number;
34
45
  agent?: string;
@@ -71,6 +82,25 @@ interface ConnectComputerBody {
71
82
  agents?: Array<{ agent?: string; cwd?: string; capabilities?: Record<string, unknown> }>;
72
83
  }
73
84
 
85
+ async function fetchDaemonJson<T>(input: { localUrl: string; token: string; path: string }): Promise<T> {
86
+ const base = input.localUrl.replace(/\/$/, '');
87
+ const response = await fetch(`${base}${input.path}`, {
88
+ headers: { authorization: `Bearer ${input.token}` },
89
+ });
90
+ const payload = await response.json().catch(() => ({})) as { ok?: boolean; data?: T; message?: string; code?: string };
91
+ if (!response.ok || payload.ok === false) {
92
+ throw new HttpError(response.status || 502, payload.code || 'DAEMON_REQUEST_FAILED', payload.message || 'daemon request failed');
93
+ }
94
+ return (payload.data ?? {}) as T;
95
+ }
96
+
97
+ async function validateRemoteProjectPath(store: MessageStore, computerId: string, rootPath: string): Promise<void> {
98
+ const control = store.getComputerLocalControl(computerId);
99
+ if (!control) throw new HttpError(409, 'COMPUTER_NOT_BROWSABLE', 'computer is not online or does not expose a local filesystem browser');
100
+ const params = new URLSearchParams({ path: rootPath, validate: 'directory', show_hidden: 'true' });
101
+ await fetchDaemonJson({ localUrl: control.daemon.local_url, token: control.token, path: `/local/files?${params}` });
102
+ }
103
+
74
104
  interface AssignAgentBody {
75
105
  computer_id?: string;
76
106
  }
@@ -86,6 +116,7 @@ interface OnboardAgentBody {
86
116
  interface LarkSetupBody {
87
117
  app_id?: string;
88
118
  app_secret?: string;
119
+ registration_id?: string;
89
120
  label?: string;
90
121
  agent?: string;
91
122
  config?: string;
@@ -169,6 +200,89 @@ function html(body: string): Response {
169
200
  return new Response(body, { headers: { 'content-type': 'text/html; charset=utf-8' } });
170
201
  }
171
202
 
203
+ const webDistDir = fileURLToPath(new URL('../web/app/dist/', import.meta.url));
204
+
205
+ interface PendingLarkRegistration {
206
+ id: string;
207
+ deviceCode: string;
208
+ url: string;
209
+ qrDataUrl: string;
210
+ expiresAt: number;
211
+ interval: number;
212
+ completed?: LarkRegistrationComplete;
213
+ }
214
+
215
+ const larkRegistrations = new Map<string, PendingLarkRegistration>();
216
+
217
+ function pruneLarkRegistrations(): void {
218
+ const now = Date.now();
219
+ for (const [id, registration] of larkRegistrations) {
220
+ if (registration.expiresAt < now - 60_000) larkRegistrations.delete(id);
221
+ }
222
+ }
223
+
224
+ function larkRegistrationPublic(entry: PendingLarkRegistration, status: 'pending' | 'complete' = entry.completed ? 'complete' : 'pending') {
225
+ return {
226
+ id: entry.id,
227
+ status,
228
+ url: entry.url,
229
+ qrDataUrl: entry.qrDataUrl,
230
+ expiresAt: new Date(entry.expiresAt).toISOString(),
231
+ interval: entry.interval,
232
+ appId: entry.completed?.appId ?? null,
233
+ tenantBrand: entry.completed?.tenantBrand ?? null,
234
+ userOpenId: entry.completed?.userOpenId ?? null,
235
+ };
236
+ }
237
+
238
+ async function pollStoredLarkRegistration(id: string): Promise<PendingLarkRegistration> {
239
+ pruneLarkRegistrations();
240
+ const entry = larkRegistrations.get(id);
241
+ if (!entry) throw new HttpError(404, 'LARK_REGISTRATION_NOT_FOUND', 'registration was not found or has expired');
242
+ if (entry.completed) return entry;
243
+ if (Date.now() > entry.expiresAt) {
244
+ larkRegistrations.delete(id);
245
+ throw new HttpError(410, 'LARK_REGISTRATION_EXPIRED', 'registration link expired');
246
+ }
247
+ const result = await pollLarkAppRegistration({ deviceCode: entry.deviceCode });
248
+ if (result.status === 'pending') return entry;
249
+ if (result.status === 'slow_down') {
250
+ entry.interval = Math.max(entry.interval + 5, result.interval);
251
+ return entry;
252
+ }
253
+ if (result.status === 'complete') {
254
+ entry.completed = result.registration;
255
+ return entry;
256
+ }
257
+ throw new HttpError(400, 'LARK_REGISTRATION_FAILED', result.message);
258
+ }
259
+
260
+ function webAssetContentType(pathname: string): string {
261
+ if (pathname.endsWith('.js')) return 'text/javascript; charset=utf-8';
262
+ if (pathname.endsWith('.css')) return 'text/css; charset=utf-8';
263
+ if (pathname.endsWith('.svg')) return 'image/svg+xml';
264
+ if (pathname.endsWith('.png')) return 'image/png';
265
+ if (pathname.endsWith('.jpg') || pathname.endsWith('.jpeg')) return 'image/jpeg';
266
+ if (pathname.endsWith('.webp')) return 'image/webp';
267
+ if (pathname.endsWith('.ico')) return 'image/x-icon';
268
+ return 'application/octet-stream';
269
+ }
270
+
271
+ function builtWebIndex(): Response | null {
272
+ const indexPath = join(webDistDir, 'index.html');
273
+ if (!existsSync(indexPath)) return null;
274
+ return new Response(Bun.file(indexPath), { headers: { 'content-type': 'text/html; charset=utf-8' } });
275
+ }
276
+
277
+ function builtWebAsset(pathname: string): Response | null {
278
+ if (!pathname.startsWith('/assets/')) return null;
279
+ const relative = decodeURIComponent(pathname.slice('/assets/'.length));
280
+ if (!relative || relative.includes('..') || relative.includes('/') || relative.includes('\\')) return null;
281
+ const assetPath = join(webDistDir, 'assets', relative);
282
+ if (!existsSync(assetPath)) return null;
283
+ return new Response(Bun.file(assetPath), { headers: { 'content-type': webAssetContentType(assetPath) } });
284
+ }
285
+
172
286
  function daemonAuthFromRequest(request: Request): { computerId: string; connectionId: string; token: string } | null {
173
287
  const computerId = request.headers.get('x-pal-computer-id')?.trim();
174
288
  const connectionId = request.headers.get('x-pal-connection-id')?.trim();
@@ -200,7 +314,12 @@ export function route(store: MessageStore, request: Request): Promise<Response>
200
314
  const { pathname } = url;
201
315
 
202
316
  if (request.method === 'GET' && pathname === '/') {
203
- return html(dashboardHtml());
317
+ return builtWebIndex() ?? html(dashboardHtml());
318
+ }
319
+
320
+ if (request.method === 'GET') {
321
+ const asset = builtWebAsset(pathname);
322
+ if (asset) return asset;
204
323
  }
205
324
 
206
325
  if (request.method === 'GET' && pathname === '/health') {
@@ -251,7 +370,14 @@ export function route(store: MessageStore, request: Request): Promise<Response>
251
370
  capabilities: agent.capabilities,
252
371
  })),
253
372
  });
254
- return json(result, 201);
373
+ return json({
374
+ computer: result.computer,
375
+ connection: result.connection,
376
+ token: result.token,
377
+ local_control_token: result.localControlToken,
378
+ daemon: result.daemon,
379
+ agents: result.agents,
380
+ }, 201);
255
381
  });
256
382
  }
257
383
 
@@ -329,6 +455,37 @@ export function route(store: MessageStore, request: Request): Promise<Response>
329
455
  return json({ rooms: store.listChats() });
330
456
  }
331
457
 
458
+ if (request.method === 'GET' && pathname === '/api/projects') {
459
+ return json({ projects: store.listProjects(numberParam(url, 'limit', 50)) });
460
+ }
461
+
462
+ if (request.method === 'POST' && pathname === '/api/projects') {
463
+ return readJson<CreateProjectBody>(request).then(async (body) => {
464
+ const name = body.name?.trim();
465
+ const computerId = body.computer_id?.trim();
466
+ const rootPath = body.root_path?.trim();
467
+ if (!name) throw new HttpError(400, 'MISSING_PROJECT_NAME', 'name is required');
468
+ if (!computerId) throw new HttpError(400, 'MISSING_COMPUTER_ID', 'computer_id is required');
469
+ if (!rootPath) throw new HttpError(400, 'MISSING_ROOT_PATH', 'root_path is required');
470
+ await validateRemoteProjectPath(store, computerId, rootPath);
471
+ const project = store.createProject({ name, computerId, rootPath });
472
+ return json({ project }, 201);
473
+ });
474
+ }
475
+
476
+ const computerFilesMatch = pathname.match(/^\/api\/computers\/([^/]+)\/files$/);
477
+ if (request.method === 'GET' && computerFilesMatch) {
478
+ const computerId = decodeURIComponent(computerFilesMatch[1]!);
479
+ const control = store.getComputerLocalControl(computerId);
480
+ if (!control) throw new HttpError(409, 'COMPUTER_NOT_BROWSABLE', 'computer is not online or does not expose a local filesystem browser');
481
+ const params = new URLSearchParams();
482
+ const browsePath = stringParam(url, 'path');
483
+ if (browsePath) params.set('path', browsePath);
484
+ if (url.searchParams.get('show_hidden') === 'true') params.set('show_hidden', 'true');
485
+ return fetchDaemonJson({ localUrl: control.daemon.local_url, token: control.token, path: `/local/files${params.size ? `?${params}` : ''}` })
486
+ .then((data) => json(data));
487
+ }
488
+
332
489
  if (request.method === 'POST' && pathname === '/api/rooms') {
333
490
  return readJson<CreateRoomBody>(request).then((body) => {
334
491
  if (!body.name?.trim()) throw new HttpError(400, 'MISSING_ROOM_NAME', 'name is required');
@@ -338,6 +495,20 @@ export function route(store: MessageStore, request: Request): Promise<Response>
338
495
  });
339
496
  }
340
497
 
498
+ const projectRoomsMatch = pathname.match(/^\/api\/projects\/([^/]+)\/rooms$/);
499
+ if (request.method === 'POST' && projectRoomsMatch) {
500
+ return readJson<CreateRoomBody>(request).then((body) => {
501
+ if (!body.name?.trim()) throw new HttpError(400, 'MISSING_ROOM_NAME', 'name is required');
502
+ if (body.kind && body.kind !== 'group' && body.kind !== 'dm') throw new HttpError(400, 'BAD_ROOM_KIND', 'kind must be group or dm');
503
+ const room = store.createProjectRoom({
504
+ projectId: decodeURIComponent(projectRoomsMatch[1]!),
505
+ name: body.name,
506
+ kind: body.kind ?? 'group',
507
+ });
508
+ return json({ room }, 201);
509
+ });
510
+ }
511
+
341
512
  const roomMembersMatch = pathname.match(/^\/api\/rooms\/([^/]+)\/members$/);
342
513
  if (request.method === 'GET' && roomMembersMatch) {
343
514
  const room = store.resolveRoom(decodeURIComponent(roomMembersMatch[1]!));
@@ -459,6 +630,37 @@ export function route(store: MessageStore, request: Request): Promise<Response>
459
630
  });
460
631
  }
461
632
 
633
+ if (request.method === 'POST' && pathname === '/api/lark/registration') {
634
+ return (async () => {
635
+ pruneLarkRegistrations();
636
+ const begin = await beginLarkAppRegistration({ source: 'pal-web' });
637
+ const id = crypto.randomUUID();
638
+ const qrDataUrl = await QRCode.toDataURL(begin.url, {
639
+ margin: 1,
640
+ width: 220,
641
+ color: { dark: '#181714', light: '#ffffff' },
642
+ });
643
+ const entry: PendingLarkRegistration = {
644
+ id,
645
+ deviceCode: begin.deviceCode,
646
+ url: begin.url,
647
+ qrDataUrl,
648
+ expiresAt: Date.now() + begin.expiresIn * 1000,
649
+ interval: begin.interval,
650
+ };
651
+ larkRegistrations.set(id, entry);
652
+ return json(larkRegistrationPublic(entry), 201);
653
+ })();
654
+ }
655
+
656
+ const larkRegistrationMatch = pathname.match(/^\/api\/lark\/registration\/([^/]+)$/);
657
+ if (request.method === 'GET' && larkRegistrationMatch) {
658
+ return (async () => {
659
+ const entry = await pollStoredLarkRegistration(decodeURIComponent(larkRegistrationMatch[1]!));
660
+ return json(larkRegistrationPublic(entry));
661
+ })();
662
+ }
663
+
462
664
  if (request.method === 'GET' && pathname === '/api/lark/authorized-users') {
463
665
  return json({ users: store.listLarkAuthorizedUsers() });
464
666
  }
@@ -482,11 +684,21 @@ export function route(store: MessageStore, request: Request): Promise<Response>
482
684
 
483
685
  if (request.method === 'POST' && pathname === '/api/lark/setup') {
484
686
  return readJson<LarkSetupBody>(request).then(async (body) => {
485
- const appId = body.app_id?.trim();
486
- const appSecret = body.app_secret?.trim();
687
+ const registrationId = body.registration_id?.trim();
688
+ let appId = body.app_id?.trim();
689
+ let appSecret = body.app_secret?.trim();
487
690
  const agent = body.agent?.trim();
488
691
  const label = body.label?.trim();
489
692
  const configPath = body.config?.trim() || defaultLarkConfigPath();
693
+ if (registrationId) {
694
+ const registration = await pollStoredLarkRegistration(registrationId);
695
+ if (!registration.completed) throw new HttpError(409, 'LARK_REGISTRATION_PENDING', 'registration has not completed yet');
696
+ if (registration.completed.tenantBrand === 'lark') {
697
+ throw new HttpError(400, 'LARK_TENANT_UNSUPPORTED', 'Lark international tenants are not supported yet; use a Feishu tenant');
698
+ }
699
+ appId = registration.completed.appId;
700
+ appSecret = registration.completed.appSecret;
701
+ }
490
702
  if (!appId) throw new HttpError(400, 'MISSING_APP_ID', 'app_id is required');
491
703
  if (!appSecret) throw new HttpError(400, 'MISSING_APP_SECRET', 'app_secret is required');
492
704
  if (!agent) throw new HttpError(400, 'MISSING_AGENT', 'agent is required');
@@ -511,6 +723,7 @@ export function route(store: MessageStore, request: Request): Promise<Response>
511
723
  botOpenId: botInfo.openId,
512
724
  agent,
513
725
  });
726
+ if (registrationId) larkRegistrations.delete(registrationId);
514
727
  return json({ ...result, appName: botInfo.appName ?? null, account }, 201);
515
728
  });
516
729
  }
package/src/client.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { defaultServerToken, defaultServerUrl } from './config.js';
2
2
  import { serverAuthHeaders } from './server-auth.js';
3
- import type { AgentRoomSubscriptionMode, AgentRun, AgentSession, ApiResponse, Chat, Computer, ComputerAgentAssignment, ComputerConnection, DaemonInstance, Message, MessageDelivery, ProvisionedComputer, RoomChannel, RoomParticipant, RunAction } from './types.js';
3
+ import type { AgentRoomSubscriptionMode, AgentRun, AgentSession, ApiResponse, Chat, Computer, ComputerAgentAssignment, ComputerConnection, DaemonInstance, Message, MessageDelivery, ProvisionedComputer, RemoteFileList, RoomChannel, RoomParticipant, RunAction } from './types.js';
4
4
 
5
5
  export interface SendRequest {
6
6
  chat?: string;
@@ -182,8 +182,16 @@ export class LockClient {
182
182
  return data.computers;
183
183
  }
184
184
 
185
- async connectComputer(input: ConnectComputerRequest): Promise<{ computer: Computer; connection: ComputerConnection; token: string; daemon: DaemonInstance; agents: ComputerAgentAssignment[] }> {
186
- const data = await this.post<{ computer: Computer; connection: ComputerConnection; token: string; daemon: DaemonInstance; agents: ComputerAgentAssignment[] }>('/api/computers/connect', input);
185
+ async connectComputer(input: ConnectComputerRequest): Promise<{ computer: Computer; connection: ComputerConnection; token: string; local_control_token?: string; daemon: DaemonInstance; agents: ComputerAgentAssignment[] }> {
186
+ const data = await this.post<{ computer: Computer; connection: ComputerConnection; token: string; local_control_token?: string; daemon: DaemonInstance; agents: ComputerAgentAssignment[] }>('/api/computers/connect', input);
187
+ return data;
188
+ }
189
+
190
+ async listComputerFiles(computerId: string, input: { path?: string; show_hidden?: boolean } = {}): Promise<RemoteFileList> {
191
+ const params = new URLSearchParams();
192
+ if (input.path) params.set('path', input.path);
193
+ if (input.show_hidden) params.set('show_hidden', 'true');
194
+ const data = await this.get<RemoteFileList>(`/api/computers/${encodeURIComponent(computerId)}/files${params.size ? `?${params}` : ''}`);
187
195
  return data;
188
196
  }
189
197