@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/cli.ts CHANGED
@@ -1,32 +1,71 @@
1
1
  #!/usr/bin/env bun
2
2
  import { readFileSync, statSync } from 'node:fs';
3
- import { parseArgs, flag, numberFlag } from './args.js';
3
+ import { basename, extname } from 'node:path';
4
+ import { parseArgs, flag, flagValues, numberFlag } from './args.js';
5
+ import { LockClient, type HeldDraftResolutionResult } from './client.js';
4
6
  import { DaemonClient } from './daemon-client.js';
5
- import { defaultDaemonUrl } from './config.js';
7
+ import { defaultDaemonUrl, defaultServerUrl, defaultDbPath, defaultServerToken } from './config.js';
8
+ import { MessageStore } from './db.js';
6
9
  import { formatMessages } from './format.js';
10
+ import { runMessagingCommand } from './messaging-cli.js';
7
11
  import { sanitizeProviderIds } from './provider-identity.js';
12
+ import type { HeldMessageDraft, MessageDelivery, RoomReminder, RoomReminderStatus, RoomSavedMessage, RoomTask, RoomTaskStatus, SkillBinding, SkillBindingScope, SkillDefinition, SkillSource, WorkflowRun, WorkflowRunRecord, WorkflowRunStatus } from './types.js';
13
+ import { runWorkflowFile, type WorkflowAgentExecutionInput, type WorkflowAgentExecutionResult } from './workflow-runtime.js';
8
14
 
9
15
  function usage(): string {
10
16
  return `pal cli (agent CLI)
11
17
 
12
18
  Usage:
13
- bun run src/cli.ts send --room general --from alice [--to codex] [--channel <channel-id>] <message>
19
+ bun run src/cli.ts send --room general --from alice [--mention codex ...] [--literal-mentions|--infer-mentions] [--channel <channel-id>] <message>
20
+ bun run src/cli.ts send --chat-id <room-id> --from alice [--mention codex ...] [--base-message-id <id> --hold-if-stale] [--attach <path> ...] <message>
21
+ bun run src/cli.ts send --chat-id <room-id> --from alice --stdin < message.md
14
22
  bun run src/cli.ts topics create --room general <name>
23
+ bun run src/cli.ts rooms handoff --from-room <room-id> --from <agent> --purpose <goal> [--source-message <id>] [--name <room>] [--team agent:mode ...] [--stdin]
15
24
  bun run src/cli.ts rooms invite --room general --agent codex [--mode mentions|all|periodic|muted|off]
25
+ bun run src/cli.ts rooms restart-agent --room general --agent codex [--json]
16
26
  bun run src/cli.ts debate start --chat general --a palbeta --b clawed [--turns 6] <topic>
17
27
  bun run src/cli.ts rooms list [--json]
18
28
  bun run src/cli.ts rooms members --room <room-id-or-name> [--json]
29
+ bun run src/cli.ts tasks list --chat-id <room-id> [--status all|todo|in_progress|in_review|done|closed] [--json]
30
+ bun run src/cli.ts tasks create --chat-id <room-id> [--from <agent-or-user>] <title> [title...]
31
+ bun run src/cli.ts tasks claim --chat-id <room-id> --tasks 1,2 [--assignee <agent>]
32
+ bun run src/cli.ts tasks unclaim --chat-id <room-id> --task 1
33
+ bun run src/cli.ts tasks status --chat-id <room-id> --task 1 --status in_review
34
+ bun run src/cli.ts reminders schedule --msg-id <message-id> --title <title> (--delay-seconds <seconds>|--fire-at <iso-time>) [--repeat daily|weekly|every:2h] [--json]
35
+ bun run src/cli.ts reminders list [--status scheduled|fired|canceled|all] [--created-by <agent>] [--chat-id <room-id>] [--json]
36
+ bun run src/cli.ts reminders cancel <reminder-id> [--json]
37
+ bun run src/cli.ts saved save --msg-id <message-id> [--from <agent-or-user>] [--note <note>] [--json]
38
+ bun run src/cli.ts saved list [--chat-id <room-id>] [--saved-by <agent-or-user>] [--json]
39
+ bun run src/cli.ts saved remove <saved-message-id> [--json]
40
+ bun run src/cli.ts drafts list [--agent <agent>] [--chat-id <room-id>] [--json]
41
+ bun run src/cli.ts drafts revise <draft-id> <message> [--json]
42
+ bun run src/cli.ts drafts stay-silent <draft-id> [--json]
43
+ bun run src/cli.ts drafts send-anyway <draft-id> [--json]
44
+ bun run src/cli.ts skills list [--source system|user|project] [--include-disabled] [--json]
45
+ bun run src/cli.ts skills create --key <key> --name <name> [--owner-user <id>] [--stdin|--instruction <text>] [--json]
46
+ bun run src/cli.ts skills edit --key <key> [--name <name>] [--status active|disabled] [--stdin|--instruction <text>] [--json]
47
+ bun run src/cli.ts skills bind --scope project|room|agent --scope-id <id> --skill <key> [--priority <n>] [--disabled] [--json]
48
+ bun run src/cli.ts skills unbind --scope project|room|agent --scope-id <id> --skill <key> [--json]
49
+ bun run src/cli.ts skills enabled [--project-id <id>] [--room-id <id>] [--agent <key>] [--json]
50
+ bun run src/cli.ts workflow run <file> [--goal <goal>] [--from <agent>] [--mock-agent-executor|--real-agent-executor --executor-agent <agent> --executor-room <room>] [--db <path>] [--json]
51
+ bun run src/cli.ts workflow list [--status running|completed|failed|all] [--db <path>] [--json]
52
+ bun run src/cli.ts workflow show <run-id> [--db <path>] [--json]
19
53
  bun run src/cli.ts messages list [--room general] [--topic 1] [--after 0] [--limit 50] [--q text] [--json]
20
54
  bun run src/cli.ts messages show <message-id> [--json]
21
55
  bun run src/cli.ts runs [--json]
22
56
  bun run src/cli.ts run-action <run-id> kill|restart
23
57
  bun run src/cli.ts chats [--json]
24
58
  bun run src/cli.ts health
59
+ bun run src/cli.ts messaging health [--json]
60
+ bun run src/cli.ts messaging status [--verbose] [--json]
61
+ bun run src/cli.ts messaging verify-lark-ingress [--app-id <id>] [--timeout-ms 60000] [--interval-ms 1000] [--json]
62
+ bun run src/cli.ts messaging probe-delivery [--agent lock] [--room <room>] [--timeout-ms 120000] [--json]
25
63
  bun run src/cli.ts http file <path> [--mime <type>] [--title <title>] [--ttl <seconds>]
26
64
 
27
65
  Environment:
28
66
  LOCK_DAEMON_URL=${defaultDaemonUrl()}
29
67
  LOCK_DAEMON_TOKEN=<token>
68
+ PAL_SERVER=${defaultServerUrl()}
30
69
  `;
31
70
  }
32
71
 
@@ -46,21 +85,427 @@ function printJson(value: unknown): void {
46
85
  console.log(JSON.stringify(value, null, 2));
47
86
  }
48
87
 
88
+ const MIME_BY_EXTENSION = new Map([
89
+ ['.csv', 'text/csv'],
90
+ ['.gif', 'image/gif'],
91
+ ['.htm', 'text/html'],
92
+ ['.html', 'text/html'],
93
+ ['.jpeg', 'image/jpeg'],
94
+ ['.jpg', 'image/jpeg'],
95
+ ['.json', 'application/json'],
96
+ ['.md', 'text/markdown'],
97
+ ['.pdf', 'application/pdf'],
98
+ ['.png', 'image/png'],
99
+ ['.txt', 'text/plain'],
100
+ ['.webp', 'image/webp'],
101
+ ['.zip', 'application/zip'],
102
+ ]);
103
+
104
+ function mimeForPath(path: string, explicit?: string): string {
105
+ const override = explicit?.trim();
106
+ if (override) return override;
107
+ return MIME_BY_EXTENSION.get(extname(path).toLowerCase()) ?? 'application/octet-stream';
108
+ }
109
+
110
+ function attachmentKindForMime(mimeType: string): 'image' | 'file' {
111
+ return mimeType.toLowerCase().startsWith('image/') ? 'image' : 'file';
112
+ }
113
+
114
+ function loadSendAttachments(paths: string[], explicitMime?: string): Array<{ kind: 'image' | 'file'; mime_type: string; filename: string; content_base64: string }> | undefined {
115
+ if (paths.length === 0) return undefined;
116
+ return paths.map((path) => {
117
+ const stat = statSync(path);
118
+ if (!stat.isFile()) throw new Error(`--attach path is not a file: ${path}`);
119
+ const mimeType = mimeForPath(path, explicitMime);
120
+ return {
121
+ kind: attachmentKindForMime(mimeType),
122
+ mime_type: mimeType,
123
+ filename: basename(path),
124
+ content_base64: Buffer.from(readFileSync(path)).toString('base64'),
125
+ };
126
+ });
127
+ }
128
+
49
129
  function looksLikeId(value: string): boolean {
50
130
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
51
131
  }
52
132
 
133
+ function roomNameFromPurpose(purpose: string): string {
134
+ const slug = purpose
135
+ .toLowerCase()
136
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
137
+ .replace(/^-+|-+$/g, '')
138
+ .slice(0, 48);
139
+ return `handoff-${slug || Date.now().toString(36)}`;
140
+ }
141
+
142
+ function parseTeamFlags(values: string[]): Array<{ agent: string; mode?: 'all' | 'periodic' | 'mentions' | 'muted' | 'off' }> | undefined {
143
+ if (!values.length) return undefined;
144
+ return values.map((value) => {
145
+ const [agent, mode] = value.split(':');
146
+ if (!agent?.trim()) throw new Error('--team requires agent or agent:mode');
147
+ if (mode && !['all', 'periodic', 'mentions', 'muted', 'off'].includes(mode)) {
148
+ throw new Error('--team mode must be all, periodic, mentions, muted, or off');
149
+ }
150
+ return { agent: agent.trim(), mode: mode as 'all' | 'periodic' | 'mentions' | 'muted' | 'off' | undefined };
151
+ });
152
+ }
153
+
53
154
  function buildDebateOpening(input: { topic: string; firstAgent: string; secondAgent: string; turns: number; chat: string }): string {
54
155
  return [
55
156
  'Start an agent-to-agent debate in this room.',
56
157
  `Debate topic: ${input.topic}`,
57
158
  `Participants: @${input.firstAgent} and @${input.secondAgent}.`,
58
159
  `Turn budget: ${input.turns} total agent turns.`,
59
- `You are @${input.firstAgent}. Make the first argument, then send the next turn to @${input.secondAgent} with: bun run src/cli.ts send --chat ${input.chat} --from ${input.firstAgent} --to ${input.secondAgent} <message>`,
60
- `Every later turn should address the other agent with --to so their daemon runtime is triggered. Stop naturally when the turn budget is reached.`,
160
+ `You are @${input.firstAgent}. Make the first argument, then send the next turn to @${input.secondAgent} with: bun run src/cli.ts send --chat ${input.chat} --from ${input.firstAgent} --mention ${input.secondAgent} <message>`,
161
+ `Every later turn should mention the other agent with --mention so their daemon runtime is triggered. Stop naturally when the turn budget is reached.`,
61
162
  ].join('\n');
62
163
  }
63
164
 
165
+ function taskRoomRef(args: ReturnType<typeof parseArgs>): string {
166
+ const room = flag(args.flags, 'chat-id')
167
+ ?? flag(args.flags, 'room-id')
168
+ ?? flag(args.flags, 'room')
169
+ ?? flag(args.flags, 'chat');
170
+ if (!room) throw new Error('tasks command requires --chat-id <room-id> or --room <room>');
171
+ return room;
172
+ }
173
+
174
+ function taskActor(args: ReturnType<typeof parseArgs>, kind: 'creator' | 'assignee'): string {
175
+ const value = kind === 'assignee'
176
+ ? flag(args.flags, 'assignee') ?? flag(args.flags, 'agent') ?? flag(args.flags, 'from') ?? flag(args.flags, 'sender')
177
+ : flag(args.flags, 'created-by') ?? flag(args.flags, 'from') ?? flag(args.flags, 'sender') ?? flag(args.flags, 'agent');
178
+ return value?.trim() || process.env.PAL_AGENT || process.env.USER || 'human';
179
+ }
180
+
181
+ function taskNumbers(args: ReturnType<typeof parseArgs>): number[] {
182
+ const rawValues = [
183
+ flag(args.flags, 'tasks'),
184
+ flag(args.flags, 'task'),
185
+ flag(args.flags, 'task-number'),
186
+ ...args.values.slice(1),
187
+ ].filter((value): value is string => Boolean(value));
188
+ const numbers = rawValues
189
+ .flatMap((value) => value.split(','))
190
+ .map((value) => Number(value.trim()))
191
+ .filter((value) => Number.isInteger(value) && value > 0);
192
+ if (!numbers.length) throw new Error('tasks command requires --task <number> or --tasks <numbers>');
193
+ return Array.from(new Set(numbers));
194
+ }
195
+
196
+ function formatTasks(tasks: RoomTask[]): string {
197
+ return tasks.map((task) => [
198
+ `#${task.task_number}`,
199
+ `[${task.status}]`,
200
+ sanitizeProviderIds(task.title),
201
+ task.assignee ? `@${sanitizeProviderIds(task.assignee)}` : '',
202
+ ].filter(Boolean).join(' ')).join('\n');
203
+ }
204
+
205
+ function reminderActor(args: ReturnType<typeof parseArgs>): string {
206
+ return flag(args.flags, 'created-by')
207
+ ?? flag(args.flags, 'from')
208
+ ?? flag(args.flags, 'sender')
209
+ ?? flag(args.flags, 'agent')
210
+ ?? process.env.PAL_AGENT
211
+ ?? process.env.USER
212
+ ?? 'human';
213
+ }
214
+
215
+ function formatReminders(reminders: RoomReminder[]): string {
216
+ return reminders.map((reminder) => [
217
+ reminder.id,
218
+ `[${reminder.status}]`,
219
+ sanitizeProviderIds(reminder.title),
220
+ `room=${sanitizeProviderIds(reminder.room_name || reminder.room_id)}`,
221
+ `fire=${reminder.fire_at}`,
222
+ reminder.repeat ? `repeat=${sanitizeProviderIds(reminder.repeat)}` : '',
223
+ reminder.created_by ? `from=${sanitizeProviderIds(reminder.created_by)}` : '',
224
+ ].filter(Boolean).join(' ')).join('\n');
225
+ }
226
+
227
+ function savedActor(args: ReturnType<typeof parseArgs>): string {
228
+ return flag(args.flags, 'saved-by')
229
+ ?? flag(args.flags, 'from')
230
+ ?? flag(args.flags, 'sender')
231
+ ?? flag(args.flags, 'agent')
232
+ ?? process.env.PAL_AGENT
233
+ ?? process.env.USER
234
+ ?? 'human';
235
+ }
236
+
237
+ function formatSavedMessages(savedMessages: RoomSavedMessage[]): string {
238
+ return savedMessages.map((savedMessage) => [
239
+ savedMessage.id,
240
+ `msg #${savedMessage.message_id}`,
241
+ sanitizeProviderIds(savedMessage.message_content),
242
+ `room=${sanitizeProviderIds(savedMessage.room_name || savedMessage.room_id)}`,
243
+ `by=${sanitizeProviderIds(savedMessage.saved_by)}`,
244
+ savedMessage.note ? `note=${sanitizeProviderIds(savedMessage.note)}` : '',
245
+ ].filter(Boolean).join(' ')).join('\n');
246
+ }
247
+
248
+ function draftActor(args: ReturnType<typeof parseArgs>): string {
249
+ return flag(args.flags, 'agent')
250
+ ?? flag(args.flags, 'from')
251
+ ?? flag(args.flags, 'sender')
252
+ ?? process.env.PAL_AGENT
253
+ ?? '';
254
+ }
255
+
256
+ function formatHeldDrafts(drafts: HeldMessageDraft[]): string {
257
+ return drafts.map((draft) => [
258
+ draft.id,
259
+ `[${draft.status}]`,
260
+ `room=${sanitizeProviderIds(draft.chat_name || draft.chat_id)}`,
261
+ `base=#${draft.base_message_id}`,
262
+ `intervening=${draft.intervening_message_count}`,
263
+ sanitizeProviderIds(draft.content).replace(/\s+/g, ' ').trim().slice(0, 180),
264
+ ].filter(Boolean).join(' ')).join('\n');
265
+ }
266
+
267
+ function formatWorkflowRuns(runs: WorkflowRun[]): string {
268
+ return runs.map((run) => [
269
+ run.id,
270
+ `[${run.status}]`,
271
+ sanitizeProviderIds(run.goal ?? run.file_path),
272
+ `nodes-final=${run.final_output ? 'yes' : 'no'}`,
273
+ `updated=${run.updated_at}`,
274
+ ].join(' ')).join('\n');
275
+ }
276
+
277
+ function formatSkills(skills: SkillDefinition[]): string {
278
+ if (!skills.length) return 'No skills.';
279
+ return skills.map((skill) => [
280
+ skill.key,
281
+ `[${skill.status}]`,
282
+ `source=${skill.source}`,
283
+ skill.owner_user_id ? `owner=${sanitizeProviderIds(skill.owner_user_id)}` : '',
284
+ skill.project_id ? `project=${sanitizeProviderIds(skill.project_id)}` : '',
285
+ sanitizeProviderIds(skill.name),
286
+ skill.repository_path ? `path=${sanitizeProviderIds(skill.repository_path)}` : '',
287
+ ].filter(Boolean).join(' ')).join('\n');
288
+ }
289
+
290
+ function formatSkillBindings(bindings: SkillBinding[]): string {
291
+ if (!bindings.length) return 'No skill bindings.';
292
+ return bindings.map((binding) => [
293
+ `${binding.scope}:${sanitizeProviderIds(binding.scope_id)}`,
294
+ binding.skill_key,
295
+ binding.enabled ? 'enabled' : 'disabled',
296
+ `priority=${binding.priority}`,
297
+ ].join(' ')).join('\n');
298
+ }
299
+
300
+ function parseSkillScopeFlag(value: string | undefined): SkillBindingScope {
301
+ if (value !== 'project' && value !== 'room' && value !== 'agent') throw new Error('skill scope must be project, room, or agent');
302
+ return value;
303
+ }
304
+
305
+ function parseSkillSourceFlag(value: string | undefined): SkillSource | undefined {
306
+ if (!value) return undefined;
307
+ if (value !== 'system' && value !== 'user' && value !== 'project') throw new Error('skill source must be system, user, or project');
308
+ return value;
309
+ }
310
+
311
+ function formatWorkflowRunRecord(record: WorkflowRunRecord): string {
312
+ const lines = [
313
+ `${record.run.id} [${record.run.status}] ${sanitizeProviderIds(record.run.goal ?? record.run.file_path)}`,
314
+ ];
315
+ for (const node of record.nodes) {
316
+ const role = node.role ? ` role=${sanitizeProviderIds(node.role)}` : '';
317
+ const parent = node.parent_id ? ` parent=${node.parent_id}` : '';
318
+ const evidence = node.evidence ? ' evidence=yes' : '';
319
+ lines.push(`- ${node.kind} [${node.status}] ${sanitizeProviderIds(node.title)}${role}${parent}${evidence}`);
320
+ }
321
+ if (record.run.final_output) {
322
+ lines.push(`final=${sanitizeProviderIds(JSON.stringify(record.run.final_output))}`);
323
+ }
324
+ if (record.run.error) {
325
+ lines.push(`error=${sanitizeProviderIds(record.run.error)}`);
326
+ }
327
+ return lines.join('\n');
328
+ }
329
+
330
+ function mockWorkflowAgentExecutor(input: WorkflowAgentExecutionInput): WorkflowAgentExecutionResult {
331
+ return {
332
+ output: {
333
+ role: input.role,
334
+ summary: `Mock ${input.role} completed: ${input.instruction}`,
335
+ recommendation: input.role === 'evaluator' ? 'ship with verification' : 'produce a concise plan',
336
+ },
337
+ evidence: {
338
+ executor: 'mock',
339
+ node_id: input.nodeId,
340
+ capabilities: input.capabilities ?? [],
341
+ },
342
+ };
343
+ }
344
+
345
+ function isTerminalDeliveryStatus(status: MessageDelivery['status']): boolean {
346
+ return status === 'acked' || status === 'failed' || status === 'canceled';
347
+ }
348
+
349
+ function sleep(ms: number): Promise<void> {
350
+ return new Promise((resolve) => setTimeout(resolve, ms));
351
+ }
352
+
353
+ function realWorkflowAgentExecutor(input: {
354
+ serverUrl?: string;
355
+ daemonUrl?: string;
356
+ agent?: string;
357
+ room?: string;
358
+ sender?: string;
359
+ timeoutMs?: number;
360
+ intervalMs?: number;
361
+ }): (node: WorkflowAgentExecutionInput) => Promise<WorkflowAgentExecutionResult> {
362
+ const client = new LockClient(input.serverUrl);
363
+ const timeoutMs = input.timeoutMs ?? 120_000;
364
+ const intervalMs = input.intervalMs ?? 1_000;
365
+ return async (node) => {
366
+ const token = crypto.randomUUID();
367
+ const agent = input.agent?.trim() || node.role;
368
+ const room = input.room?.trim() || `workflow-runtime-${agent}`;
369
+ const content = [
370
+ `PAL workflow real runtime node ${token}.`,
371
+ `Role: ${node.role}`,
372
+ `Title: ${node.title}`,
373
+ `Instruction: ${node.instruction}`,
374
+ `Output contract: ${JSON.stringify(node.outputContract ?? {})}`,
375
+ `Reply with a concise result and include workflow-ok ${token}.`,
376
+ ].join('\n');
377
+ if (!defaultServerToken()) {
378
+ const daemon = new DaemonClient(input.daemonUrl);
379
+ const roomRef = looksLikeId(room) ? { chat_id: room } : { chat: room };
380
+ const sent = await daemon.sendMessageResult({
381
+ ...roomRef,
382
+ sender: input.sender?.trim() || 'pal-workflow',
383
+ recipient: agent,
384
+ content,
385
+ mentions: [agent],
386
+ idempotency_key: `pal.workflow.node:${node.runId}:${node.nodeId}:${token}`,
387
+ });
388
+ if (!sent.message) throw new Error('REAL_WORKFLOW_LOCAL_SEND_FAILED');
389
+ let replies = [] as Awaited<ReturnType<DaemonClient['getMessages']>>;
390
+ const startedAt = Date.now();
391
+ while (Date.now() - startedAt < timeoutMs) {
392
+ const params = new URLSearchParams({ q: token, limit: '20' });
393
+ if (looksLikeId(room)) params.set('chat_id', room);
394
+ else params.set('chat', room);
395
+ replies = (await daemon.getMessages(params)).filter((message) => message.id !== sent.message!.id);
396
+ if (replies.length > 0) break;
397
+ await sleep(Math.min(intervalMs, Math.max(1, timeoutMs - (Date.now() - startedAt))));
398
+ }
399
+ if (replies.length === 0) throw new Error('REAL_WORKFLOW_LOCAL_REPLY_TIMEOUT');
400
+ const firstReply = replies[0]!;
401
+ return {
402
+ output: {
403
+ role: node.role,
404
+ agent,
405
+ room,
406
+ summary: firstReply.content,
407
+ ...(node.role === 'evaluator' ? { recommendation: 'ship with verification' } : {}),
408
+ delivery_status: sent.deliveries?.length ? 'created' : 'unknown',
409
+ reply_count: replies.length,
410
+ reply: firstReply.content,
411
+ },
412
+ evidence: {
413
+ executor: 'pal-local-message',
414
+ token,
415
+ message_id: sent.message.id,
416
+ delivery_ids: sent.deliveries?.map((delivery) => delivery.id) ?? [],
417
+ notify: sent.notify ?? null,
418
+ reply_message_ids: replies.map((reply) => reply.id),
419
+ },
420
+ };
421
+ }
422
+ const created = await client.probeDelivery({
423
+ agent,
424
+ room,
425
+ sender: input.sender?.trim() || 'pal-workflow',
426
+ content,
427
+ idempotencyKey: `pal.workflow.node:${node.runId}:${node.nodeId}:${token}`,
428
+ });
429
+ const deliveryIds = created.deliveries.map((delivery) => delivery.id);
430
+ let deliveries = created.deliveries;
431
+ const startedAt = Date.now();
432
+ while (deliveryIds.length > 0 && Date.now() - startedAt < timeoutMs) {
433
+ deliveries = await Promise.all(deliveryIds.map((id) => client.getDelivery(id)));
434
+ if (deliveries.every((delivery) => isTerminalDeliveryStatus(delivery.status))) break;
435
+ await sleep(Math.min(intervalMs, Math.max(1, timeoutMs - (Date.now() - startedAt))));
436
+ }
437
+ if (deliveryIds.length === 0) throw new Error('REAL_WORKFLOW_EXECUTOR_NO_DELIVERY');
438
+ const pending = deliveries.find((delivery) => !isTerminalDeliveryStatus(delivery.status));
439
+ if (pending) throw new Error(`REAL_WORKFLOW_EXECUTOR_TIMEOUT: delivery ${pending.id} status=${pending.status}`);
440
+ const failed = deliveries.find((delivery) => delivery.status !== 'acked');
441
+ if (failed) throw new Error(`REAL_WORKFLOW_EXECUTOR_DELIVERY_FAILED: delivery ${failed.id} status=${failed.status}`);
442
+
443
+ const params = new URLSearchParams({ chat: created.probe.room, q: token, limit: '20' });
444
+ const replies = (await client.getMessages(params)).filter((message) => message.id !== created.message.id);
445
+ const firstReply = replies[0] ?? null;
446
+ return {
447
+ output: {
448
+ role: node.role,
449
+ agent,
450
+ room: created.probe.room,
451
+ summary: firstReply?.content ?? `Delivery acked by ${agent}`,
452
+ ...(node.role === 'evaluator' ? { recommendation: 'ship with verification' } : {}),
453
+ delivery_status: 'acked',
454
+ reply_count: replies.length,
455
+ reply: firstReply?.content ?? null,
456
+ },
457
+ evidence: {
458
+ executor: 'pal-delivery',
459
+ token,
460
+ message_id: created.message.id,
461
+ delivery_ids: deliveryIds,
462
+ run_ids: deliveries.map((delivery) => delivery.run_id).filter(Boolean),
463
+ reply_message_ids: replies.map((reply) => reply.id),
464
+ },
465
+ };
466
+ };
467
+ }
468
+
469
+ function printHeldDraftResolution(result: HeldDraftResolutionResult, json: boolean): void {
470
+ if (json) {
471
+ printJson(result);
472
+ return;
473
+ }
474
+ if (result.message) {
475
+ console.log(formatMessages([result.message]));
476
+ return;
477
+ }
478
+ console.log(formatHeldDrafts([result.draft]));
479
+ }
480
+
481
+ function mentionFlags(flags: ReturnType<typeof parseArgs>['flags']): string[] {
482
+ const values = [
483
+ ...flagValues(flags, 'mention'),
484
+ ...flagValues(flags, 'mentions').flatMap((value) => value.split(',')),
485
+ ...flagValues(flags, 'to'),
486
+ ...flagValues(flags, 'recipient'),
487
+ ];
488
+ const seen = new Set<string>();
489
+ const mentions: string[] = [];
490
+ for (const value of values) {
491
+ const trimmed = value.trim().replace(/^@/, '');
492
+ if (!trimmed || seen.has(trimmed)) continue;
493
+ seen.add(trimmed);
494
+ mentions.push(trimmed);
495
+ }
496
+ return mentions;
497
+ }
498
+
499
+ function mentionLintMode(flags: ReturnType<typeof parseArgs>['flags']): 'strict' | 'literal' | 'infer' {
500
+ if (flags['infer-mentions']) return 'infer';
501
+ if (flags['literal-mentions'] || flags['no-mention-lint']) return 'literal';
502
+ return 'strict';
503
+ }
504
+
505
+ function mentionDisplayForCli(mention: string): string {
506
+ return mention === '__pal_all_agents__' ? 'all' : mention;
507
+ }
508
+
64
509
  async function main(): Promise<void> {
65
510
  const args = parseArgs();
66
511
  const daemonClient = new DaemonClient(flag(args.flags, 'daemon'));
@@ -75,6 +520,12 @@ async function main(): Promise<void> {
75
520
  return;
76
521
  }
77
522
 
523
+ if (args.command === 'messaging') {
524
+ const serverClient = new LockClient(flag(args.flags, 'server'));
525
+ await runMessagingCommand(serverClient, args, usage);
526
+ return;
527
+ }
528
+
78
529
  if (args.command === 'chats') {
79
530
  const chats = await daemonClient.listChats();
80
531
  if (args.flags.json) {
@@ -100,6 +551,31 @@ async function main(): Promise<void> {
100
551
  }
101
552
  return;
102
553
  }
554
+ if (sub === 'handoff') {
555
+ const sourceRoomId = flag(args.flags, 'from-room') ?? flag(args.flags, 'source-room') ?? flag(args.flags, 'chat-id') ?? flag(args.flags, 'room-id');
556
+ const actor = flag(args.flags, 'from') ?? flag(args.flags, 'agent') ?? process.env.PAL_AGENT;
557
+ const purpose = flag(args.flags, 'purpose') ?? flag(args.flags, 'goal') ?? args.values.slice(1).join(' ');
558
+ if (!sourceRoomId) throw new Error('rooms handoff requires --from-room <room-id>');
559
+ if (!actor) throw new Error('rooms handoff requires --from <agent>');
560
+ if (!purpose?.trim()) throw new Error('rooms handoff requires --purpose <goal>');
561
+ const handoffContent = args.flags.stdin ? await readStdin() : flag(args.flags, 'context') ?? '';
562
+ const result = await daemonClient.createHandoffRoom({
563
+ source_room_id: sourceRoomId,
564
+ source_message_id: numberFlag(args.flags, 'source-message') ?? numberFlag(args.flags, 'source-message-id'),
565
+ actor,
566
+ purpose,
567
+ name: flag(args.flags, 'name') ?? roomNameFromPurpose(purpose),
568
+ team: parseTeamFlags(flagValues(args.flags, 'team')),
569
+ handoff_content: handoffContent,
570
+ });
571
+ if (args.flags.json) {
572
+ printJson(result);
573
+ } else {
574
+ console.log(`#${result.room.name} (${result.room.id}) created by @${sanitizeProviderIds(actor)}`);
575
+ if (result.message) console.log(`handoff message #${result.message.id}`);
576
+ }
577
+ return;
578
+ }
103
579
  if (sub === 'invite') {
104
580
  const roomRef = flag(args.flags, 'room') ?? args.values[1];
105
581
  const agent = flag(args.flags, 'agent') ?? args.values[2];
@@ -125,9 +601,193 @@ async function main(): Promise<void> {
125
601
  }
126
602
  return;
127
603
  }
604
+ if (sub === 'restart-agent') {
605
+ const roomRef = flag(args.flags, 'room') ?? flag(args.flags, 'chat-id') ?? args.values[1];
606
+ const agent = flag(args.flags, 'agent') ?? args.values[2];
607
+ if (!roomRef) throw new Error('rooms restart-agent requires --room <room-id-or-name>');
608
+ if (!agent) throw new Error('rooms restart-agent requires --agent <agent-key>');
609
+ const run = await daemonClient.restartRoomAgent(roomRef, agent);
610
+ if (args.flags.json) {
611
+ printJson(run);
612
+ } else {
613
+ console.log(`restart requested for @${sanitizeProviderIds(agent)} in ${roomRef}: run=${run.id} status=${run.status}`);
614
+ }
615
+ return;
616
+ }
128
617
  throw new Error(`unknown rooms subcommand: ${sub}`);
129
618
  }
130
619
 
620
+ if (args.command === 'tasks') {
621
+ const sub = args.values[0] ?? 'list';
622
+ const roomRef = taskRoomRef(args);
623
+ if (sub === 'list') {
624
+ const tasks = await daemonClient.listRoomTasks(
625
+ roomRef,
626
+ (flag(args.flags, 'status') ?? 'all') as RoomTaskStatus | 'all',
627
+ numberFlag(args.flags, 'limit', 50),
628
+ );
629
+ if (args.flags.json) printJson(tasks);
630
+ else console.log(formatTasks(tasks));
631
+ return;
632
+ }
633
+ if (sub === 'create') {
634
+ const titles = args.values.slice(1).map((value) => value.trim()).filter(Boolean);
635
+ const titleFlag = flag(args.flags, 'title');
636
+ if (titleFlag) titles.unshift(titleFlag);
637
+ if (!titles.length) throw new Error('tasks create requires at least one title');
638
+ const tasks = await daemonClient.createRoomTasks(roomRef, {
639
+ created_by: taskActor(args, 'creator'),
640
+ tasks: titles.map((title) => ({ title })),
641
+ });
642
+ if (args.flags.json) printJson(tasks);
643
+ else console.log(formatTasks(tasks));
644
+ return;
645
+ }
646
+ if (sub === 'claim') {
647
+ const assignee = taskActor(args, 'assignee');
648
+ const tasks: RoomTask[] = [];
649
+ for (const taskNumber of taskNumbers(args)) {
650
+ tasks.push(await daemonClient.claimRoomTask(roomRef, taskNumber, { assignee }));
651
+ }
652
+ if (args.flags.json) printJson(tasks);
653
+ else console.log(formatTasks(tasks));
654
+ return;
655
+ }
656
+ if (sub === 'unclaim') {
657
+ const [taskNumber] = taskNumbers(args);
658
+ const task = await daemonClient.unclaimRoomTask(roomRef, taskNumber!);
659
+ if (args.flags.json) printJson(task);
660
+ else console.log(formatTasks([task]));
661
+ return;
662
+ }
663
+ if (sub === 'status') {
664
+ const [taskNumber] = taskNumbers(args);
665
+ const status = flag(args.flags, 'status') as RoomTaskStatus | undefined;
666
+ if (!status) throw new Error('tasks status requires --status <status>');
667
+ const task = await daemonClient.updateRoomTaskStatus(roomRef, taskNumber!, status);
668
+ if (args.flags.json) printJson(task);
669
+ else console.log(formatTasks([task]));
670
+ return;
671
+ }
672
+ throw new Error(`unknown tasks subcommand: ${sub}`);
673
+ }
674
+
675
+ if (args.command === 'reminders') {
676
+ const sub = args.values[0] ?? 'list';
677
+ if (sub === 'schedule') {
678
+ const messageIdFlag = numberFlag(args.flags, 'msg-id') ?? numberFlag(args.flags, 'message-id');
679
+ const msgId = messageIdFlag ?? Number(args.values[1]);
680
+ if (!Number.isInteger(msgId) || msgId < 1) throw new Error('reminders schedule requires --msg-id <message-id>');
681
+ const title = flag(args.flags, 'title') ?? positionalContent(args.values.slice(messageIdFlag === undefined ? 2 : 1));
682
+ if (!title) throw new Error('reminders schedule requires --title <title>');
683
+ const fireAt = flag(args.flags, 'fire-at');
684
+ const delaySeconds = numberFlag(args.flags, 'delay-seconds');
685
+ if (!fireAt && delaySeconds === undefined) throw new Error('reminders schedule requires --delay-seconds <seconds> or --fire-at <iso-time>');
686
+ const reminder = await daemonClient.scheduleReminder({
687
+ msg_id: msgId,
688
+ title,
689
+ created_by: reminderActor(args),
690
+ fire_at: fireAt,
691
+ delay_seconds: delaySeconds,
692
+ repeat: flag(args.flags, 'repeat'),
693
+ });
694
+ if (args.flags.json) printJson(reminder);
695
+ else console.log(formatReminders([reminder]));
696
+ return;
697
+ }
698
+ if (sub === 'list') {
699
+ const reminders = await daemonClient.listReminders({
700
+ status: (flag(args.flags, 'status') ?? 'scheduled') as RoomReminderStatus | 'all',
701
+ created_by: flag(args.flags, 'created-by'),
702
+ room_id: flag(args.flags, 'chat-id') ?? flag(args.flags, 'room-id'),
703
+ limit: numberFlag(args.flags, 'limit', 50),
704
+ });
705
+ if (args.flags.json) printJson(reminders);
706
+ else console.log(formatReminders(reminders));
707
+ return;
708
+ }
709
+ if (sub === 'cancel') {
710
+ const reminderId = args.values[1] ?? flag(args.flags, 'id');
711
+ if (!reminderId) throw new Error('reminders cancel requires <reminder-id>');
712
+ const reminder = await daemonClient.cancelReminder(reminderId);
713
+ if (args.flags.json) printJson(reminder);
714
+ else console.log(formatReminders([reminder]));
715
+ return;
716
+ }
717
+ throw new Error(`unknown reminders subcommand: ${sub}`);
718
+ }
719
+
720
+ if (args.command === 'saved') {
721
+ const sub = args.values[0] ?? 'list';
722
+ if (sub === 'save') {
723
+ const messageIdFlag = numberFlag(args.flags, 'msg-id') ?? numberFlag(args.flags, 'message-id');
724
+ const msgId = messageIdFlag ?? Number(args.values[1]);
725
+ if (!Number.isInteger(msgId) || msgId < 1) throw new Error('saved save requires --msg-id <message-id>');
726
+ const savedMessage = await daemonClient.saveMessage({
727
+ msg_id: msgId,
728
+ saved_by: savedActor(args),
729
+ note: flag(args.flags, 'note') ?? null,
730
+ });
731
+ if (args.flags.json) printJson(savedMessage);
732
+ else console.log(formatSavedMessages([savedMessage]));
733
+ return;
734
+ }
735
+ if (sub === 'list') {
736
+ const savedMessages = await daemonClient.listSavedMessages({
737
+ saved_by: flag(args.flags, 'saved-by'),
738
+ room_id: flag(args.flags, 'chat-id') ?? flag(args.flags, 'room-id'),
739
+ limit: numberFlag(args.flags, 'limit', 50),
740
+ });
741
+ if (args.flags.json) printJson(savedMessages);
742
+ else console.log(formatSavedMessages(savedMessages));
743
+ return;
744
+ }
745
+ if (sub === 'remove' || sub === 'delete') {
746
+ const savedMessageId = args.values[1] ?? flag(args.flags, 'id');
747
+ if (!savedMessageId) throw new Error('saved remove requires <saved-message-id>');
748
+ const savedMessage = await daemonClient.removeSavedMessage(savedMessageId);
749
+ if (args.flags.json) printJson(savedMessage);
750
+ else console.log(formatSavedMessages([savedMessage]));
751
+ return;
752
+ }
753
+ throw new Error(`unknown saved subcommand: ${sub}`);
754
+ }
755
+
756
+ if (args.command === 'drafts' || args.command === 'held-drafts') {
757
+ const sub = args.values[0] ?? 'list';
758
+ if (sub === 'list') {
759
+ const drafts = await daemonClient.listHeldDrafts({
760
+ agent: draftActor(args),
761
+ room_id: flag(args.flags, 'chat-id') ?? flag(args.flags, 'room-id'),
762
+ status: (flag(args.flags, 'status') ?? 'held') as never,
763
+ limit: numberFlag(args.flags, 'limit', 50),
764
+ });
765
+ if (args.flags.json) printJson(drafts);
766
+ else console.log(formatHeldDrafts(drafts));
767
+ return;
768
+ }
769
+
770
+ const draftId = args.values[1] ?? flag(args.flags, 'id');
771
+ if (!draftId) throw new Error(`drafts ${sub} requires <draft-id>`);
772
+ if (sub === 'revise') {
773
+ const inlineContent = flag(args.flags, 'content') ?? positionalContent(args.values.slice(2));
774
+ const stdinContent = !inlineContent && !process.stdin.isTTY ? (await readStdin()).trim() : '';
775
+ const content = inlineContent || stdinContent;
776
+ if (!content) throw new Error('drafts revise requires a revised message');
777
+ printHeldDraftResolution(await daemonClient.reviseHeldDraft(draftId, content), Boolean(args.flags.json));
778
+ return;
779
+ }
780
+ if (sub === 'stay-silent' || sub === 'abandon') {
781
+ printHeldDraftResolution(await daemonClient.staySilentHeldDraft(draftId), Boolean(args.flags.json));
782
+ return;
783
+ }
784
+ if (sub === 'send-anyway') {
785
+ printHeldDraftResolution(await daemonClient.sendHeldDraftAnyway(draftId), Boolean(args.flags.json));
786
+ return;
787
+ }
788
+ throw new Error(`unknown drafts subcommand: ${sub}`);
789
+ }
790
+
131
791
  if (args.command === 'runs') {
132
792
  const runs = await daemonClient.listRuns();
133
793
  if (args.flags.json) {
@@ -166,8 +826,8 @@ async function main(): Promise<void> {
166
826
  const message = await daemonClient.sendMessage({
167
827
  chat,
168
828
  sender: 'debate',
169
- recipient: firstAgent,
170
829
  content: buildDebateOpening({ topic, firstAgent, secondAgent, turns, chat }),
830
+ mentions: [firstAgent],
171
831
  });
172
832
 
173
833
  if (args.flags.json) {
@@ -212,31 +872,230 @@ async function main(): Promise<void> {
212
872
 
213
873
  if (args.command === 'send') {
214
874
  const file = flag(args.flags, 'file');
875
+ const attachments = loadSendAttachments(flagValues(args.flags, 'attach'), flag(args.flags, 'attach-mime') ?? flag(args.flags, 'mime'));
876
+ const useStdin = Boolean(args.flags.stdin);
215
877
  const inlineContent = positionalContent(args.values);
216
- const fileContent = !inlineContent && file ? readFileSync(file, 'utf8').trim() : '';
217
- const stdinContent = !inlineContent && !fileContent && !process.stdin.isTTY ? (await readStdin()).trim() : '';
878
+ if (useStdin && inlineContent) throw new Error('send --stdin cannot be combined with a positional message');
879
+ if (useStdin && file) throw new Error('send --stdin cannot be combined with --file');
880
+ const fileContent = !inlineContent && !useStdin && file ? readFileSync(file, 'utf8') : '';
881
+ const stdinContent = !inlineContent && !fileContent && (useStdin || !process.stdin.isTTY) ? await readStdin() : '';
218
882
  const content = inlineContent || fileContent || stdinContent;
219
883
  const sender = flag(args.flags, 'from') ?? flag(args.flags, 'sender') ?? process.env.USER ?? 'human';
220
- if (!content) throw new Error('message content is required');
884
+ const baseMessageId = numberFlag(args.flags, 'base-message-id') ?? numberFlag(args.flags, 'freshness-base-message-id');
885
+ const holdIfStale = Boolean(args.flags['hold-if-stale']);
886
+ const sendAnywayIfStale = Boolean(args.flags['send-anyway-if-stale']);
887
+ if (!content.trim() && !attachments?.length) throw new Error('message content or --attach is required');
888
+ if (holdIfStale && sendAnywayIfStale) throw new Error('choose only one of --hold-if-stale or --send-anyway-if-stale');
889
+ if ((holdIfStale || sendAnywayIfStale) && baseMessageId === undefined) throw new Error('--base-message-id is required with stale-message handling');
890
+ const freshness = baseMessageId === undefined
891
+ ? undefined
892
+ : { base_message_id: baseMessageId, on_stale: sendAnywayIfStale ? 'send_anyway' as const : 'hold' as const };
221
893
 
222
- const message = await daemonClient.sendMessage({
894
+ const result = await daemonClient.sendMessageResult({
223
895
  chat: flag(args.flags, 'room') ?? flag(args.flags, 'chat') ?? 'general',
224
896
  room: flag(args.flags, 'room'),
897
+ chat_id: flag(args.flags, 'chat-id') ?? flag(args.flags, 'room-id'),
898
+ room_id: flag(args.flags, 'room-id'),
225
899
  parent_id: numberFlag(args.flags, 'parent'),
226
900
  channel_id: flag(args.flags, 'channel') ?? flag(args.flags, 'channel-id') ?? null,
227
901
  sender,
228
- recipient: flag(args.flags, 'to') ?? flag(args.flags, 'recipient'),
229
902
  content,
903
+ freshness,
904
+ mentions: mentionFlags(args.flags),
905
+ mention_lint: mentionLintMode(args.flags),
906
+ attachments,
230
907
  });
231
908
 
232
- if (args.flags.json) {
233
- printJson(message);
909
+ if (result.mention_lint?.missing_mentions?.length) {
910
+ if (args.flags.json) {
911
+ printJson(result);
912
+ } else {
913
+ const missing = result.mention_lint.missing_mentions.map(mentionDisplayForCli);
914
+ console.error(result.mention_lint.message);
915
+ console.error(`Re-run with ${missing.map((mention) => `--mention ${mention}`).join(' ')}, or pass --literal-mentions if this is only quoted text.`);
916
+ const available = result.mention_lint.available_mentions?.length ? result.mention_lint.available_mentions.join(', ') : '(none)';
917
+ console.error(`Available agent mention keys for this room: ${available}`);
918
+ }
919
+ process.exitCode = 1;
920
+ } else if (result.invalid_mentions?.length) {
921
+ if (args.flags.json) {
922
+ printJson(result);
923
+ } else {
924
+ const available = result.available_mentions?.length ? result.available_mentions.join(', ') : '(none)';
925
+ console.error(`Invalid mention key(s): ${result.invalid_mentions.join(', ')}`);
926
+ console.error(`Available mention keys for this room: ${available}`);
927
+ }
928
+ process.exitCode = 1;
929
+ } else if (args.flags.json) {
930
+ printJson(result);
931
+ } else if (result.message) {
932
+ console.log(formatMessages([result.message]));
933
+ } else if (result.draft) {
934
+ console.log(`Held draft ${result.draft.id} for #${result.draft.chat_name}.`);
935
+ console.log(`Room changed after message ${result.draft.base_message_id}; ${result.intervening_messages?.length ?? result.draft.intervening_message_count} intervening message(s) were returned.`);
936
+ for (const message of result.intervening_messages ?? []) {
937
+ console.log(`- #${message.id} @${sanitizeProviderIds(message.sender)}: ${sanitizeProviderIds(message.content).replace(/\s+/g, ' ').trim().slice(0, 160)}`);
938
+ }
234
939
  } else {
235
- console.log(formatMessages([message]));
940
+ console.log('Message send completed without a visible message.');
236
941
  }
237
942
  return;
238
943
  }
239
944
 
945
+ if (args.command === 'skills') {
946
+ const sub = args.values[0] ?? 'list';
947
+ const store = new MessageStore(flag(args.flags, 'db') ?? defaultDbPath());
948
+ try {
949
+ if (sub === 'list') {
950
+ const skills = store.listSkillDefinitions({
951
+ source: parseSkillSourceFlag(flag(args.flags, 'source')),
952
+ includeDisabled: Boolean(args.flags['include-disabled']),
953
+ ownerUserId: flag(args.flags, 'owner-user') ?? undefined,
954
+ projectId: flag(args.flags, 'project-id') ?? undefined,
955
+ });
956
+ const bindings = store.listSkillBindings({
957
+ scope: flag(args.flags, 'scope') ? parseSkillScopeFlag(flag(args.flags, 'scope')) : undefined,
958
+ scopeId: flag(args.flags, 'scope-id') ?? undefined,
959
+ });
960
+ const warning = 'PAL-managed skills are injected by PAL. Native runtime-discovered global skills may still be loaded by Codex/OpenCode and may not be fully blockable from PAL.';
961
+ if (args.flags.json) printJson({ skills, bindings, native_runtime_warning: warning });
962
+ else {
963
+ console.log(warning);
964
+ console.log(formatSkills(skills));
965
+ if (bindings.length) {
966
+ console.log('');
967
+ console.log(formatSkillBindings(bindings));
968
+ }
969
+ }
970
+ return;
971
+ }
972
+ if (sub === 'create') {
973
+ const instruction = args.flags.stdin ? await readStdin() : flag(args.flags, 'instruction') ?? positionalContent(args.values.slice(1));
974
+ const skill = store.createSkillDefinition({
975
+ key: flag(args.flags, 'key') ?? '',
976
+ name: flag(args.flags, 'name') ?? '',
977
+ description: flag(args.flags, 'description') ?? '',
978
+ instructionContent: instruction,
979
+ source: parseSkillSourceFlag(flag(args.flags, 'source')) ?? 'user',
980
+ ownerUserId: flag(args.flags, 'owner-user') ?? flag(args.flags, 'owner-user-id') ?? 'owner',
981
+ projectId: flag(args.flags, 'project-id') ?? null,
982
+ repositoryPath: flag(args.flags, 'repository-path') ?? null,
983
+ });
984
+ if (args.flags.json) printJson(skill);
985
+ else console.log(formatSkills([skill]));
986
+ return;
987
+ }
988
+ if (sub === 'edit') {
989
+ const key = flag(args.flags, 'key') ?? args.values[1];
990
+ if (!key) throw new Error('skills edit requires --key <key>');
991
+ const instruction = args.flags.stdin ? await readStdin() : flag(args.flags, 'instruction');
992
+ const status = flag(args.flags, 'status');
993
+ if (status && status !== 'active' && status !== 'disabled') throw new Error('skill status must be active or disabled');
994
+ const skill = store.updateSkillDefinition(key, {
995
+ name: flag(args.flags, 'name'),
996
+ description: flag(args.flags, 'description'),
997
+ instructionContent: instruction,
998
+ status: status as SkillDefinition['status'] | undefined,
999
+ repositoryPath: flag(args.flags, 'repository-path') ?? undefined,
1000
+ });
1001
+ if (args.flags.json) printJson(skill);
1002
+ else console.log(formatSkills([skill]));
1003
+ return;
1004
+ }
1005
+ if (sub === 'bind') {
1006
+ const binding = store.upsertSkillBinding({
1007
+ scope: parseSkillScopeFlag(flag(args.flags, 'scope')),
1008
+ scopeId: flag(args.flags, 'scope-id') ?? '',
1009
+ skillKey: flag(args.flags, 'skill') ?? flag(args.flags, 'skill-key') ?? '',
1010
+ enabled: !args.flags.disabled,
1011
+ priority: numberFlag(args.flags, 'priority'),
1012
+ });
1013
+ if (args.flags.json) printJson(binding);
1014
+ else console.log(formatSkillBindings([binding]));
1015
+ return;
1016
+ }
1017
+ if (sub === 'unbind') {
1018
+ const deleted = store.deleteSkillBinding({
1019
+ scope: parseSkillScopeFlag(flag(args.flags, 'scope')),
1020
+ scopeId: flag(args.flags, 'scope-id') ?? '',
1021
+ skillKey: flag(args.flags, 'skill') ?? flag(args.flags, 'skill-key') ?? '',
1022
+ });
1023
+ if (args.flags.json) printJson({ deleted });
1024
+ else console.log(deleted ? 'Skill binding removed.' : 'No matching skill binding.');
1025
+ return;
1026
+ }
1027
+ if (sub === 'enabled') {
1028
+ const skills = store.listEnabledSkills({
1029
+ projectId: flag(args.flags, 'project-id') ?? null,
1030
+ roomId: flag(args.flags, 'room-id') ?? flag(args.flags, 'chat-id') ?? null,
1031
+ agent: flag(args.flags, 'agent') ?? null,
1032
+ });
1033
+ if (args.flags.json) printJson(skills);
1034
+ else console.log(formatSkills(skills));
1035
+ return;
1036
+ }
1037
+ throw new Error(`unknown skills subcommand: ${sub}`);
1038
+ } finally {
1039
+ store.close();
1040
+ }
1041
+ }
1042
+
1043
+ if (args.command === 'workflow' || args.command === 'workflows') {
1044
+ const sub = args.values[0] ?? 'list';
1045
+ const store = new MessageStore(flag(args.flags, 'db') ?? defaultDbPath());
1046
+ try {
1047
+ if (sub === 'run') {
1048
+ const filePath = args.values[1] ?? flag(args.flags, 'file');
1049
+ if (!filePath) throw new Error('workflow run requires <file>');
1050
+ if (args.flags['mock-agent-executor'] && args.flags['real-agent-executor']) {
1051
+ throw new Error('choose only one of --mock-agent-executor or --real-agent-executor');
1052
+ }
1053
+ const record = await runWorkflowFile(store, {
1054
+ filePath,
1055
+ goal: flag(args.flags, 'goal') ?? null,
1056
+ createdBy: flag(args.flags, 'from') ?? flag(args.flags, 'created-by') ?? process.env.PAL_AGENT ?? process.env.USER ?? null,
1057
+ executor: args.flags['mock-agent-executor']
1058
+ ? mockWorkflowAgentExecutor
1059
+ : args.flags['real-agent-executor']
1060
+ ? realWorkflowAgentExecutor({
1061
+ serverUrl: flag(args.flags, 'server'),
1062
+ daemonUrl: flag(args.flags, 'daemon'),
1063
+ agent: flag(args.flags, 'executor-agent') ?? flag(args.flags, 'agent'),
1064
+ room: flag(args.flags, 'executor-room') ?? flag(args.flags, 'room') ?? flag(args.flags, 'chat'),
1065
+ sender: flag(args.flags, 'executor-sender') ?? flag(args.flags, 'from'),
1066
+ timeoutMs: numberFlag(args.flags, 'timeout-ms', 120_000),
1067
+ intervalMs: numberFlag(args.flags, 'interval-ms', 1_000),
1068
+ })
1069
+ : undefined,
1070
+ });
1071
+ if (args.flags.json) printJson(record);
1072
+ else console.log(formatWorkflowRunRecord(record));
1073
+ return;
1074
+ }
1075
+ if (sub === 'list') {
1076
+ const runs = store.listWorkflowRuns({
1077
+ status: (flag(args.flags, 'status') ?? 'all') as WorkflowRunStatus | 'all',
1078
+ limit: numberFlag(args.flags, 'limit', 50),
1079
+ });
1080
+ if (args.flags.json) printJson(runs);
1081
+ else console.log(formatWorkflowRuns(runs));
1082
+ return;
1083
+ }
1084
+ if (sub === 'show') {
1085
+ const runId = args.values[1] ?? flag(args.flags, 'run-id') ?? flag(args.flags, 'id');
1086
+ if (!runId) throw new Error('workflow show requires <run-id>');
1087
+ const record = store.getWorkflowRunRecord(runId);
1088
+ if (!record) throw new Error(`workflow run ${runId} was not found`);
1089
+ if (args.flags.json) printJson(record);
1090
+ else console.log(formatWorkflowRunRecord(record));
1091
+ return;
1092
+ }
1093
+ throw new Error(`unknown workflow subcommand: ${sub}`);
1094
+ } finally {
1095
+ store.close();
1096
+ }
1097
+ }
1098
+
240
1099
  if (args.command === 'messages') {
241
1100
  const sub = args.values[0];
242
1101
  if (sub === 'show') {