@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
@@ -14,7 +14,7 @@ import { palIdentityHandle } from '../provider-identity.js';
14
14
  * | message.chat_id | chatName (prefixed `lark:`) |
15
15
  * | message.message_id | idempotencyKey |
16
16
  * | sender.sender_id.open_id | sender |
17
- * | message.mentions[0]?.id.open_id | recipient (first @mention)|
17
+ * | message.mentions[].id.open_id | mentions for group delivery; recipient for non-group direct routing |
18
18
  * | message.content (parsed JSON text)| content |
19
19
  * | message.root_id (thread reply) | parentId via lookup |
20
20
  *
@@ -61,10 +61,16 @@ export interface MapLarkMessageResult {
61
61
  status: 'ok' | 'skipped';
62
62
  reason?: 'missing_message_id' | 'missing_chat_id' | 'missing_sender' | 'unsupported_message_type' | 'empty_text';
63
63
  input?: CreateMessageInput;
64
- /** Lark root_id when present (for thread replies). The router pre-resolves the parentId from it if possible. */
64
+ /** Lark root_id, or parent_id when root_id is absent, used to pre-resolve the local topic parent. */
65
65
  rootMessageId?: string;
66
66
  }
67
67
 
68
+ export interface LarkImageResource {
69
+ fileKey: string;
70
+ filename: string;
71
+ mimeType: string;
72
+ }
73
+
68
74
  export function buildLockChatName(appId: string, chatId: string, chatType?: string): string {
69
75
  if (chatType === 'group') {
70
76
  return `lark:group:${chatId}`;
@@ -96,7 +102,7 @@ export function mentionsAllAgents(envelope: LarkMessageEnvelope): boolean {
96
102
  if (isAllMention(mention)) return true;
97
103
  }
98
104
  const raw = parseLarkTextContent(envelope.message?.content, envelope.message?.message_type).toLowerCase();
99
- return /(^|\s)@(all|所有人)(\s|$)/u.test(raw);
105
+ return /(^|\s)@(_all|all|所有人)(\s|$)/u.test(raw);
100
106
  }
101
107
 
102
108
  function normalizeMappedMentions(values: Array<string | null>): string[] {
@@ -125,6 +131,49 @@ function parseLarkTextContent(rawContent: string | undefined, messageType: strin
125
131
  }
126
132
  }
127
133
 
134
+ function parseJsonObject(rawContent: string | undefined): Record<string, unknown> | null {
135
+ if (!rawContent) return null;
136
+ try {
137
+ const parsed = JSON.parse(rawContent) as unknown;
138
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ export function extractLarkImageResources(envelope: LarkMessageEnvelope): LarkImageResource[] {
145
+ const msg = envelope.message;
146
+ const out: LarkImageResource[] = [];
147
+ const seen = new Set<string>();
148
+ const add = (fileKey: unknown, filename?: unknown) => {
149
+ if (typeof fileKey !== 'string' || !fileKey.trim() || seen.has(fileKey)) return;
150
+ seen.add(fileKey);
151
+ out.push({
152
+ fileKey,
153
+ filename: typeof filename === 'string' && filename.trim() ? filename.trim() : `${fileKey}.jpg`,
154
+ mimeType: 'image/jpeg',
155
+ });
156
+ };
157
+ const parsed = parseJsonObject(msg?.content);
158
+ if (msg?.message_type === 'image') {
159
+ add(parsed?.image_key ?? parsed?.file_key, parsed?.file_name ?? parsed?.name);
160
+ }
161
+ if (msg?.message_type === 'post' && parsed) {
162
+ const content = parsed.content;
163
+ if (Array.isArray(content)) {
164
+ for (const line of content) {
165
+ if (!Array.isArray(line)) continue;
166
+ for (const item of line) {
167
+ if (!item || typeof item !== 'object') continue;
168
+ const record = item as Record<string, unknown>;
169
+ if (record.tag === 'img' || record.tag === 'image') add(record.image_key ?? record.file_key);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ return out;
175
+ }
176
+
128
177
  /**
129
178
  * Pure mapper: no DB access. Returns a `CreateMessageInput` skeleton (without
130
179
  * parentId resolution — that happens in `resolveThreadParent`).
@@ -146,11 +195,14 @@ export function mapLarkMessageToCreateInput(input: MapLarkMessageInput): MapLark
146
195
  ...(mentionsAllAgents(envelope) ? [ALL_AGENTS_MENTION] : []),
147
196
  ...(msg.mentions?.map((m) => m.id?.open_id ? input.recipientByMentionOpenId?.get(m.id.open_id) ?? null : null) ?? []),
148
197
  ]);
198
+ const firstMappedMention = firstMention ? input.recipientByMentionOpenId?.get(firstMention) : undefined;
149
199
  const recipient = input.recipientOverride !== undefined
150
200
  ? input.recipientOverride
151
- : firstMention ? input.recipientByMentionOpenId?.get(firstMention) ?? firstMention : null;
201
+ : msg.chat_type === 'group'
202
+ ? null
203
+ : firstMention ? firstMappedMention ?? firstMention : null;
152
204
  const chatName = buildLockChatName(input.appId, msg.chat_id, msg.chat_type);
153
- const rootMessageId = msg.root_id?.trim() || undefined;
205
+ const rootMessageId = msg.root_id?.trim() || msg.parent_id?.trim() || undefined;
154
206
 
155
207
  const createInput: CreateMessageInput = {
156
208
  chatName,
@@ -291,6 +343,8 @@ export function ingestLarkMessage(input: IngestLarkMessageInput): IngestLarkMess
291
343
  content: mapped.input.content,
292
344
  type: mapped.input.type,
293
345
  idempotencyKey: mapped.input.idempotencyKey,
346
+ provider: 'lark',
347
+ mentions: mapped.input.mentions,
294
348
  }
295
349
  : threadOrphan
296
350
  ? {
@@ -300,6 +354,8 @@ export function ingestLarkMessage(input: IngestLarkMessageInput): IngestLarkMess
300
354
  content: mapped.input.content,
301
355
  type: mapped.input.type,
302
356
  idempotencyKey: mapped.input.idempotencyKey,
357
+ provider: 'lark',
358
+ mentions: mapped.input.mentions,
303
359
  }
304
360
  : { ...mapped.input, sender: senderHandle || mapped.input.sender };
305
361
 
@@ -10,6 +10,8 @@ export interface InboundRawEvent {
10
10
  raw_body_bytes: Uint8Array;
11
11
  }
12
12
 
13
+ export type InboundRawEventSummary = Omit<InboundRawEvent, 'raw_body_bytes'>;
14
+
13
15
  export interface StoreInboundEventInput {
14
16
  appId: string;
15
17
  rawBody: string | Uint8Array;
@@ -24,6 +26,31 @@ export interface StoreInboundEventResult {
24
26
  duplicate: boolean;
25
27
  }
26
28
 
29
+ export interface RepairInboundEventParseFailuresOptions {
30
+ appId?: string;
31
+ limit?: number;
32
+ dryRun?: boolean;
33
+ }
34
+
35
+ export interface RepairInboundEventParseFailuresResult {
36
+ dry_run: boolean;
37
+ scanned: number;
38
+ repaired: number;
39
+ unchanged: number;
40
+ conflicts: number;
41
+ errors: number;
42
+ rows: Array<{
43
+ id: string;
44
+ app_id: string;
45
+ old_event_id: string;
46
+ old_event_type: string;
47
+ new_event_id: string;
48
+ new_event_type: string;
49
+ status: 'repaired' | 'would_repair' | 'unchanged' | 'conflict' | 'error';
50
+ error?: string;
51
+ }>;
52
+ }
53
+
27
54
  const ENCODER = new TextEncoder();
28
55
 
29
56
  function toBytes(body: string | Uint8Array): Uint8Array {
@@ -48,8 +75,9 @@ export async function parseEventEnvelope(rawBytes: Uint8Array): Promise<ParsedEn
48
75
  const text = new TextDecoder('utf8', { fatal: false }).decode(rawBytes);
49
76
  const json = JSON.parse(text) as Record<string, unknown>;
50
77
  const header = (json.header && typeof json.header === 'object') ? json.header as Record<string, unknown> : null;
51
- const eventId = (header?.event_id ?? json.uuid) as unknown;
52
- const eventType = (header?.event_type ?? json.type) as unknown;
78
+ const event = (json.event && typeof json.event === 'object') ? json.event as Record<string, unknown> : null;
79
+ const eventId = (header?.event_id ?? event?.event_id ?? json.uuid) as unknown;
80
+ const eventType = (header?.event_type ?? event?.event_type ?? json.type) as unknown;
53
81
  if (typeof eventId === 'string' && eventId.length > 0 && typeof eventType === 'string' && eventType.length > 0) {
54
82
  return { event_id: eventId, event_type: eventType, parse_ok: 1 };
55
83
  }
@@ -117,10 +145,22 @@ export function getInboundEvent(db: Database, id: string): InboundRawEvent | nul
117
145
 
118
146
  export function listRecentInboundEvents(db: Database, limit = 20): InboundRawEvent[] {
119
147
  return db
120
- .query('SELECT id, received_at, app_id, event_type, event_id, parse_ok, raw_body_bytes FROM channel_inbound_raw_events ORDER BY received_at DESC, id DESC LIMIT ?')
148
+ .query('SELECT id, received_at, app_id, event_type, event_id, parse_ok, raw_body_bytes FROM channel_inbound_raw_events ORDER BY received_at DESC, rowid DESC LIMIT ?')
121
149
  .all(Math.max(1, Math.min(limit, 500))) as InboundRawEvent[];
122
150
  }
123
151
 
152
+ export function latestInboundEventSummary(db: Database, appId: string): InboundRawEventSummary | null {
153
+ return db
154
+ .query("SELECT id, received_at, app_id, event_type, event_id, parse_ok FROM channel_inbound_raw_events WHERE app_id = ? AND event_type NOT LIKE 'pal.probe.%' ORDER BY received_at DESC, rowid DESC LIMIT 1")
155
+ .get(appId) as InboundRawEventSummary | null;
156
+ }
157
+
158
+ export function latestProbeInboundEventSummary(db: Database, appId: string): InboundRawEventSummary | null {
159
+ return db
160
+ .query("SELECT id, received_at, app_id, event_type, event_id, parse_ok FROM channel_inbound_raw_events WHERE app_id = ? AND event_type LIKE 'pal.probe.%' ORDER BY received_at DESC, rowid DESC LIMIT 1")
161
+ .get(appId) as InboundRawEventSummary | null;
162
+ }
163
+
124
164
  export function countInboundEvents(db: Database, appId?: string): number {
125
165
  if (appId) {
126
166
  const row = db.query('SELECT COUNT(*) AS n FROM channel_inbound_raw_events WHERE app_id = ?').get(appId) as { n: number };
@@ -129,3 +169,116 @@ export function countInboundEvents(db: Database, appId?: string): number {
129
169
  const row = db.query('SELECT COUNT(*) AS n FROM channel_inbound_raw_events').get() as { n: number };
130
170
  return row.n;
131
171
  }
172
+
173
+ export function countProviderInboundEvents(db: Database, appId?: string): number {
174
+ if (appId) {
175
+ const row = db.query("SELECT COUNT(*) AS n FROM channel_inbound_raw_events WHERE app_id = ? AND event_type NOT LIKE 'pal.probe.%'").get(appId) as { n: number };
176
+ return row.n;
177
+ }
178
+ const row = db.query("SELECT COUNT(*) AS n FROM channel_inbound_raw_events WHERE event_type NOT LIKE 'pal.probe.%'").get() as { n: number };
179
+ return row.n;
180
+ }
181
+
182
+ export function countProbeInboundEvents(db: Database, appId?: string): number {
183
+ if (appId) {
184
+ const row = db.query("SELECT COUNT(*) AS n FROM channel_inbound_raw_events WHERE app_id = ? AND event_type LIKE 'pal.probe.%'").get(appId) as { n: number };
185
+ return row.n;
186
+ }
187
+ const row = db.query("SELECT COUNT(*) AS n FROM channel_inbound_raw_events WHERE event_type LIKE 'pal.probe.%'").get() as { n: number };
188
+ return row.n;
189
+ }
190
+
191
+ export function countInboundParseFailures(db: Database, appId?: string): number {
192
+ if (appId) {
193
+ const row = db.query('SELECT COUNT(*) AS n FROM channel_inbound_raw_events WHERE app_id = ? AND parse_ok = 0').get(appId) as { n: number };
194
+ return row.n;
195
+ }
196
+ const row = db.query('SELECT COUNT(*) AS n FROM channel_inbound_raw_events WHERE parse_ok = 0').get() as { n: number };
197
+ return row.n;
198
+ }
199
+
200
+ export async function repairInboundEventParseFailures(
201
+ db: Database,
202
+ options: RepairInboundEventParseFailuresOptions = {},
203
+ ): Promise<RepairInboundEventParseFailuresResult> {
204
+ const limit = Math.max(1, Math.min(options.limit ?? 100, 1000));
205
+ const dryRun = options.dryRun ?? false;
206
+ const rows = options.appId
207
+ ? db
208
+ .query('SELECT id, app_id, event_type, event_id, raw_body_bytes FROM channel_inbound_raw_events WHERE app_id = ? AND parse_ok = 0 ORDER BY received_at DESC, rowid DESC LIMIT ?')
209
+ .all(options.appId, limit)
210
+ : db
211
+ .query('SELECT id, app_id, event_type, event_id, raw_body_bytes FROM channel_inbound_raw_events WHERE parse_ok = 0 ORDER BY received_at DESC, rowid DESC LIMIT ?')
212
+ .all(limit);
213
+ const result: RepairInboundEventParseFailuresResult = {
214
+ dry_run: dryRun,
215
+ scanned: rows.length,
216
+ repaired: 0,
217
+ unchanged: 0,
218
+ conflicts: 0,
219
+ errors: 0,
220
+ rows: [],
221
+ };
222
+
223
+ for (const row of rows as Array<{ id: string; app_id: string; event_type: string; event_id: string; raw_body_bytes: Uint8Array }>) {
224
+ const parsed = await parseEventEnvelope(row.raw_body_bytes);
225
+ if (parsed.parse_ok !== 1) {
226
+ result.unchanged += 1;
227
+ result.rows.push({
228
+ id: row.id,
229
+ app_id: row.app_id,
230
+ old_event_id: row.event_id,
231
+ old_event_type: row.event_type,
232
+ new_event_id: parsed.event_id,
233
+ new_event_type: parsed.event_type,
234
+ status: 'unchanged',
235
+ });
236
+ continue;
237
+ }
238
+ if (dryRun) {
239
+ result.repaired += 1;
240
+ result.rows.push({
241
+ id: row.id,
242
+ app_id: row.app_id,
243
+ old_event_id: row.event_id,
244
+ old_event_type: row.event_type,
245
+ new_event_id: parsed.event_id,
246
+ new_event_type: parsed.event_type,
247
+ status: 'would_repair',
248
+ });
249
+ continue;
250
+ }
251
+ try {
252
+ db
253
+ .query('UPDATE channel_inbound_raw_events SET event_id = ?, event_type = ?, parse_ok = 1 WHERE id = ?')
254
+ .run(parsed.event_id, parsed.event_type, row.id);
255
+ result.repaired += 1;
256
+ result.rows.push({
257
+ id: row.id,
258
+ app_id: row.app_id,
259
+ old_event_id: row.event_id,
260
+ old_event_type: row.event_type,
261
+ new_event_id: parsed.event_id,
262
+ new_event_type: parsed.event_type,
263
+ status: 'repaired',
264
+ });
265
+ } catch (err) {
266
+ const message = err instanceof Error ? err.message : String(err);
267
+ const conflict = message.toLowerCase().includes('unique') || message.toLowerCase().includes('constraint');
268
+ if (conflict) result.conflicts += 1;
269
+ else result.errors += 1;
270
+ result.rows.push({
271
+ id: row.id,
272
+ app_id: row.app_id,
273
+ old_event_id: row.event_id,
274
+ old_event_type: row.event_type,
275
+ new_event_id: parsed.event_id,
276
+ new_event_type: parsed.event_type,
277
+ status: conflict ? 'conflict' : 'error',
278
+ error: message,
279
+ });
280
+ }
281
+ }
282
+
283
+ return result;
284
+ }