@controlflow-ai/daemon 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +66 -24
  2. package/package.json +16 -3
  3. package/src/agent-avatar.ts +30 -0
  4. package/src/agent-key.ts +28 -0
  5. package/src/agent-permissions.ts +359 -0
  6. package/src/agent-runtime.ts +810 -28
  7. package/src/agent-workspace.ts +183 -0
  8. package/src/app.ts +2183 -79
  9. package/src/args.ts +54 -7
  10. package/src/cli.ts +873 -14
  11. package/src/client.ts +482 -12
  12. package/src/coco.ts +9 -40
  13. package/src/codex.ts +33 -5
  14. package/src/config.ts +28 -4
  15. package/src/console.ts +460 -26
  16. package/src/daemon-client.ts +116 -3
  17. package/src/daemon.ts +958 -101
  18. package/src/db.ts +3216 -113
  19. package/src/delivery-ws.ts +269 -0
  20. package/src/format.ts +4 -1
  21. package/src/lark/app-registration.ts +141 -0
  22. package/src/lark/cli.ts +7 -137
  23. package/src/lark/credentials.ts +36 -3
  24. package/src/lark/event-router.ts +61 -5
  25. package/src/lark/inbound-events.ts +156 -3
  26. package/src/lark/server-integration.ts +659 -111
  27. package/src/lark/setup.ts +74 -5
  28. package/src/lark/ws-daemon.ts +136 -10
  29. package/src/local-api.ts +611 -14
  30. package/src/local-auth.ts +36 -3
  31. package/src/message-attachments.ts +71 -0
  32. package/src/messaging-cli.ts +741 -0
  33. package/src/messaging-status.ts +669 -0
  34. package/src/migrations/023_projects.ts +65 -0
  35. package/src/migrations/024_agents_model.ts +10 -0
  36. package/src/migrations/025_room_archive.ts +44 -0
  37. package/src/migrations/026_project_archive.ts +44 -0
  38. package/src/migrations/027_agent_permission_profiles.ts +16 -0
  39. package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
  40. package/src/migrations/029_held_message_drafts.ts +32 -0
  41. package/src/migrations/030_agent_room_read_state.ts +25 -0
  42. package/src/migrations/031_room_tasks.ts +29 -0
  43. package/src/migrations/032_room_reminders.ts +29 -0
  44. package/src/migrations/033_room_saved_messages.ts +25 -0
  45. package/src/migrations/034_agent_activity_events.ts +27 -0
  46. package/src/migrations/035_agent_avatars.ts +17 -0
  47. package/src/migrations/036_project_agent_defaults.ts +21 -0
  48. package/src/migrations/037_message_attachments.ts +36 -0
  49. package/src/migrations/038_agent_activity_room_scope.ts +64 -0
  50. package/src/migrations/039_message_attachments_path.ts +34 -0
  51. package/src/migrations/040_message_attachments_file_schema.ts +80 -0
  52. package/src/migrations/041_room_system_events.ts +30 -0
  53. package/src/migrations/042_message_attachment_file_kind.ts +52 -0
  54. package/src/migrations/043_room_mode_skill_registry.ts +92 -0
  55. package/src/migrations/044_workflow_runtime.ts +69 -0
  56. package/src/migrations/045_skill_repository_ownership.ts +64 -0
  57. package/src/migrations.ts +70 -1
  58. package/src/neeko.ts +40 -4
  59. package/src/runtime-env.ts +179 -0
  60. package/src/runtime-registry.ts +83 -13
  61. package/src/server.ts +244 -4
  62. package/src/token-file.ts +13 -6
  63. package/src/types.ts +394 -0
  64. package/src/workflow-runtime.ts +275 -0
  65. package/src/web.ts +0 -904
package/src/lark/setup.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { createInterface } from 'node:readline';
2
+ import QRCode from 'qrcode';
2
3
  import type { LarkCredential } from './credentials.js';
3
4
  import { loadLarkCredentials, saveLarkCredentials, upsertCredential } from './credentials.js';
5
+ import { beginLarkAppRegistration, pollLarkAppRegistration, type LarkRegistrationComplete } from './app-registration.js';
4
6
 
5
7
  export interface CredentialValidationOk {
6
8
  ok: true;
@@ -62,6 +64,7 @@ export interface InteractiveSetupOptions {
62
64
  listAgents?: () => Promise<LarkSetupAgentOption[]>;
63
65
  validateCredentials?: typeof validateLarkCredentials;
64
66
  resolveBotInfo?: typeof resolveLarkBotInfo;
67
+ registerApp?: typeof registerLarkAppFromDeviceFlow;
65
68
  }
66
69
 
67
70
  function clean(value: string | undefined): string | undefined {
@@ -69,6 +72,41 @@ function clean(value: string | undefined): string | undefined {
69
72
  return trimmed ? trimmed : undefined;
70
73
  }
71
74
 
75
+ export async function registerLarkAppFromDeviceFlow(options: {
76
+ log: Pick<Console, 'log' | 'warn' | 'error'>;
77
+ qrCode?: (url: string) => Promise<string>;
78
+ fetchImpl?: FetchLike;
79
+ }): Promise<LarkRegistrationComplete | null> {
80
+ const begin = await beginLarkAppRegistration({ fetchImpl: options.fetchImpl, source: 'pal' });
81
+ options.log.log('Create a Feishu app by scanning this QR code or opening the link:');
82
+ options.log.log('');
83
+ const terminalQr = options.qrCode
84
+ ? await options.qrCode(begin.url)
85
+ : await QRCode.toString(begin.url, { type: 'terminal', small: true });
86
+ options.log.log(terminalQr);
87
+ options.log.log(begin.url);
88
+ options.log.log('');
89
+ options.log.log(`Waiting for confirmation. The code expires in about ${Math.max(1, Math.round(begin.expiresIn / 60))} minutes.`);
90
+
91
+ let intervalMs = Math.max(1, begin.interval) * 1000;
92
+ const deadline = Date.now() + begin.expiresIn * 1000;
93
+ while (Date.now() < deadline) {
94
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
95
+ const result = await pollLarkAppRegistration({ deviceCode: begin.deviceCode, fetchImpl: options.fetchImpl });
96
+ if (result.status === 'pending') continue;
97
+ if (result.status === 'slow_down') {
98
+ intervalMs = Math.max(intervalMs + 5000, result.interval * 1000);
99
+ options.log.warn(`Feishu asked us to slow polling; retrying every ${Math.round(intervalMs / 1000)}s.`);
100
+ continue;
101
+ }
102
+ if (result.status === 'complete') return result.registration;
103
+ options.log.warn(`App registration did not complete (${result.status}): ${result.message}`);
104
+ return null;
105
+ }
106
+ options.log.warn('App registration expired before it completed.');
107
+ return null;
108
+ }
109
+
72
110
  async function chooseAgent(options: InteractiveSetupOptions, ask: (question: string) => Promise<string>): Promise<string | null> {
73
111
  if (!options.listAgents) {
74
112
  return clean(await ask('Bind to agent key [codex]: ')) ?? 'codex';
@@ -79,7 +117,7 @@ async function chooseAgent(options: InteractiveSetupOptions, ask: (question: str
79
117
  agents = await options.listAgents();
80
118
  } catch (error) {
81
119
  options.log.error(`Could not load agent list: ${error instanceof Error ? error.message : String(error)}`);
82
- options.log.error('Set up an agent first, then rerun lark setup.');
120
+ options.log.error('Set up an agent first, then bind the bot from agent settings.');
83
121
  return null;
84
122
  }
85
123
 
@@ -275,9 +313,37 @@ export async function runInteractiveLarkSetup(options: InteractiveSetupOptions):
275
313
  const agent = await chooseAgent(options, ask);
276
314
  if (!agent) return null;
277
315
  options.log.log('');
278
- options.log.log('Paste an existing Feishu app credential. Pal will validate it before writing lark.json.');
279
- const appId = clean(await ask('App ID (cli_xxx): '));
280
- const appSecret = clean(await ask('App Secret: '));
316
+ options.log.log('Create a Feishu app by scan/link, or paste an existing credential.');
317
+ options.log.log(' 1. Scan or open link to create app');
318
+ options.log.log(' 2. Paste App ID / App Secret');
319
+ const method = clean(await ask('Setup method [1]: ')) ?? '1';
320
+ let appId: string | undefined;
321
+ let appSecret: string | undefined;
322
+ let scannedUserOpenId: string | undefined;
323
+ if (method !== '2') {
324
+ const registerApp = options.registerApp ?? registerLarkAppFromDeviceFlow;
325
+ const registration = await registerApp({ log: options.log }).catch((error) => {
326
+ options.log.warn(`App registration failed: ${error instanceof Error ? error.message : String(error)}`);
327
+ return null;
328
+ });
329
+ if (!registration) {
330
+ options.log.warn('Falling back to manual App ID / App Secret entry.');
331
+ } else if (registration.tenantBrand === 'lark') {
332
+ options.log.error('Lark international tenants are not supported yet; Pal currently expects Feishu (feishu.cn) credentials.');
333
+ options.log.error('No config was written.');
334
+ return null;
335
+ } else {
336
+ appId = registration.appId;
337
+ appSecret = registration.appSecret;
338
+ scannedUserOpenId = registration.userOpenId;
339
+ options.log.log(`App created: ${appId}`);
340
+ if (scannedUserOpenId) options.log.log(`Scanner open_id: ${scannedUserOpenId}`);
341
+ }
342
+ }
343
+ if (!appId || !appSecret) {
344
+ appId = clean(await ask('App ID (cli_xxx): '));
345
+ appSecret = clean(await ask('App Secret: '));
346
+ }
281
347
  if (!appId || !appSecret) {
282
348
  options.log.error('App ID and App Secret are required; no config was written.');
283
349
  return null;
@@ -315,8 +381,11 @@ export async function runInteractiveLarkSetup(options: InteractiveSetupOptions):
315
381
  agent,
316
382
  configPath: options.configPath,
317
383
  });
384
+ if (scannedUserOpenId) {
385
+ options.log.log(`Scanner open_id ${scannedUserOpenId} was detected; add it under Lark authorized users if this user should operate Pal.`);
386
+ }
318
387
  if (result.replaced) {
319
- options.log.warn(`[lark setup] overwrote existing credential for appId=${appId} in ${options.configPath}`);
388
+ options.log.warn(`[lark] overwrote existing credential for appId=${appId} in ${options.configPath}`);
320
389
  }
321
390
  options.log.log(formatLarkSetupNextSteps(result));
322
391
  return result;
@@ -10,13 +10,22 @@ export type LarkEventCallback = (event: {
10
10
  storeResult: StoreInboundEventResult;
11
11
  }) => void | Promise<void>;
12
12
 
13
+ export type LarkWebSocketLifecycleCallback = (event: {
14
+ appId: string;
15
+ type: 'ready' | 'reconnecting' | 'reconnected' | 'error';
16
+ error?: string;
17
+ }) => void;
18
+
13
19
  export interface StartLarkDaemonOptions {
14
20
  appId: string;
15
21
  appSecret: string;
16
22
  db: Database;
17
23
  onEvent?: LarkEventCallback;
24
+ onLifecycle?: LarkWebSocketLifecycleCallback;
18
25
  logger?: Partial<Pick<Console, 'info' | 'warn' | 'error'>>;
19
26
  loggerLevel?: Lark.LoggerLevel;
27
+ handshakeTimeoutMs?: number;
28
+ pingTimeoutSec?: number;
20
29
  }
21
30
 
22
31
  export interface LarkDaemonHandle {
@@ -26,6 +35,44 @@ export interface LarkDaemonHandle {
26
35
  }
27
36
 
28
37
  const SUPPORTED_EVENTS = ['im.message.receive_v1', 'card.action.trigger'] as const;
38
+ const silentLarkLogger = {
39
+ error() {},
40
+ warn() {},
41
+ info() {},
42
+ debug() {},
43
+ trace() {},
44
+ };
45
+
46
+ function nestedString(value: unknown, path: string[]): string | null {
47
+ let current = value;
48
+ for (const key of path) {
49
+ if (!current || typeof current !== 'object') return null;
50
+ current = (current as Record<string, unknown>)[key];
51
+ }
52
+ return typeof current === 'string' && current.length > 0 ? current : null;
53
+ }
54
+
55
+ export function stableLarkEventId(eventName: string, data: unknown): string | null {
56
+ const headerId = nestedString(data, ['header', 'event_id'])
57
+ ?? nestedString(data, ['event_id']);
58
+ if (headerId) return headerId;
59
+ const derivedId = nestedString(data, ['message', 'message_id'])
60
+ ?? nestedString(data, ['message', 'open_message_id'])
61
+ ?? nestedString(data, ['open_message_id']);
62
+ if (derivedId) return `${eventName}:${derivedId}`;
63
+ const operatorId = nestedString(data, ['operator', 'operator_id', 'open_id']);
64
+ if (operatorId) return `${eventName}:${operatorId}:${nestedString(data, ['token']) ?? ''}`;
65
+ return null;
66
+ }
67
+
68
+ export function buildLarkEventRawBody(eventName: string, data: unknown): string {
69
+ const eventId = stableLarkEventId(eventName, data);
70
+ return JSON.stringify({
71
+ schema: '2.0',
72
+ header: eventId ? { event_id: eventId, event_type: eventName } : { event_type: eventName },
73
+ event: data,
74
+ });
75
+ }
29
76
 
30
77
  /**
31
78
  * Start a Lark WSClient that subscribes to im.message.receive_v1 +
@@ -45,7 +92,7 @@ export function startLarkDaemon(options: StartLarkDaemonOptions): LarkDaemonHand
45
92
  const handlers: Record<string, (data: unknown) => Promise<unknown>> = {};
46
93
  for (const eventName of SUPPORTED_EVENTS) {
47
94
  handlers[eventName] = async (data) => {
48
- const rawBody = JSON.stringify({ schema: '2.0', header: { event_type: eventName }, event: data });
95
+ const rawBody = buildLarkEventRawBody(eventName, data);
49
96
  try {
50
97
  const storeResult = await storeInboundEvent(options.db, { appId: options.appId, rawBody });
51
98
  if (storeResult.duplicate) {
@@ -75,6 +122,24 @@ export function startLarkDaemon(options: StartLarkDaemonOptions): LarkDaemonHand
75
122
  appId: options.appId,
76
123
  appSecret: options.appSecret,
77
124
  loggerLevel: options.loggerLevel ?? Lark.LoggerLevel.warn,
125
+ handshakeTimeoutMs: options.handshakeTimeoutMs ?? 15_000,
126
+ wsConfig: { pingTimeout: options.pingTimeoutSec ?? 10 },
127
+ onReady: () => {
128
+ options.onLifecycle?.({ appId: options.appId, type: 'ready' });
129
+ log.info?.(`[lark/${options.appId}] WSClient ready`);
130
+ },
131
+ onReconnecting: () => {
132
+ options.onLifecycle?.({ appId: options.appId, type: 'reconnecting' });
133
+ log.warn?.(`[lark/${options.appId}] WSClient reconnecting`);
134
+ },
135
+ onReconnected: () => {
136
+ options.onLifecycle?.({ appId: options.appId, type: 'reconnected' });
137
+ log.info?.(`[lark/${options.appId}] WSClient reconnected`);
138
+ },
139
+ onError: (err) => {
140
+ options.onLifecycle?.({ appId: options.appId, type: 'error', error: err.message });
141
+ log.error?.(`[lark/${options.appId}] WSClient error: ${err.message}`);
142
+ },
78
143
  });
79
144
 
80
145
  wsClient.start({ eventDispatcher });
@@ -103,35 +168,96 @@ export function createLarkApiClient(appId: string, appSecret: string): Lark.Clie
103
168
  return new Lark.Client({
104
169
  appId,
105
170
  appSecret,
106
- loggerLevel: Lark.LoggerLevel.warn,
171
+ loggerLevel: Lark.LoggerLevel.fatal,
172
+ logger: silentLarkLogger,
107
173
  });
108
174
  }
109
175
 
110
- export interface SendTextMessageInput {
176
+ export type LarkReceiveIdType = 'chat_id' | 'open_id' | 'union_id' | 'email' | 'user_id';
177
+
178
+ export interface LarkMentionTarget {
179
+ openId: string;
180
+ displayName?: string | null;
181
+ }
182
+
183
+ interface SendMessageBaseInput {
111
184
  client: Lark.Client;
112
- receiveIdType: 'chat_id' | 'open_id' | 'union_id' | 'email' | 'user_id';
185
+ receiveIdType: LarkReceiveIdType;
113
186
  receiveId: string;
114
187
  text: string;
188
+ mention?: LarkMentionTarget | null;
115
189
  }
116
190
 
191
+ export interface SendTextMessageInput extends SendMessageBaseInput {}
192
+
193
+ export interface SendCardMessageInput extends SendMessageBaseInput {}
194
+
117
195
  export interface SendTextMessageResult {
118
196
  messageId?: string;
119
197
  raw: unknown;
120
198
  }
121
199
 
122
- export async function sendTextMessage(input: SendTextMessageInput): Promise<SendTextMessageResult> {
200
+ export interface LarkMessageCreatePayload {
201
+ receive_id: string;
202
+ msg_type: 'text' | 'interactive';
203
+ content: string;
204
+ }
205
+
206
+ function escapeLarkText(value: string): string {
207
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
208
+ }
209
+
210
+ function larkMentionSuffix(mention: LarkMentionTarget | null | undefined, tag: 'text' | 'card'): string {
211
+ if (!mention?.openId) return '';
212
+ const idAttribute = tag === 'text' ? 'user_id' : 'id';
213
+ return `\n\n<at ${idAttribute}="${mention.openId}">${escapeLarkText(mention.displayName ?? '')}</at>`;
214
+ }
215
+
216
+ function larkMessageBody(input: Pick<SendMessageBaseInput, 'text' | 'mention'>, tag: 'text' | 'card'): string {
217
+ return `${input.text}${larkMentionSuffix(input.mention, tag)}`;
218
+ }
219
+
220
+ export function buildLarkTextMessagePayload(input: Pick<SendTextMessageInput, 'receiveId' | 'text' | 'mention'>): LarkMessageCreatePayload {
221
+ return {
222
+ receive_id: input.receiveId,
223
+ msg_type: 'text',
224
+ content: JSON.stringify({ text: larkMessageBody(input, 'text') }),
225
+ };
226
+ }
227
+
228
+ export function buildLarkMentionCardPayload(input: Pick<SendCardMessageInput, 'receiveId' | 'text' | 'mention'>): LarkMessageCreatePayload {
229
+ return {
230
+ receive_id: input.receiveId,
231
+ msg_type: 'interactive',
232
+ content: JSON.stringify({
233
+ config: { wide_screen_mode: true },
234
+ elements: [
235
+ {
236
+ tag: 'markdown',
237
+ content: larkMessageBody(input, 'card'),
238
+ },
239
+ ],
240
+ }),
241
+ };
242
+ }
243
+
244
+ async function createLarkMessage(input: SendMessageBaseInput, payload: LarkMessageCreatePayload): Promise<SendTextMessageResult> {
123
245
  const response = await input.client.im.message.create({
124
246
  params: { receive_id_type: input.receiveIdType },
125
- data: {
126
- receive_id: input.receiveId,
127
- msg_type: 'text',
128
- content: JSON.stringify({ text: input.text }),
129
- },
247
+ data: payload,
130
248
  });
131
249
  const messageId = (response?.data as { message_id?: string } | undefined)?.message_id;
132
250
  return { messageId, raw: response };
133
251
  }
134
252
 
253
+ export async function sendTextMessage(input: SendTextMessageInput): Promise<SendTextMessageResult> {
254
+ return createLarkMessage(input, buildLarkTextMessagePayload(input));
255
+ }
256
+
257
+ export async function sendCardMessage(input: SendCardMessageInput): Promise<SendTextMessageResult> {
258
+ return createLarkMessage(input, buildLarkMentionCardPayload(input));
259
+ }
260
+
135
261
  export interface AddMessageReactionInput {
136
262
  client: Lark.Client;
137
263
  messageId: string;