@controlflow-ai/daemon 0.1.2 → 0.1.4
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 +54 -6
- package/bin/daemon.js +6 -1
- package/package.json +3 -1
- package/src/agent-avatar.ts +30 -0
- package/src/agent-key.ts +28 -0
- package/src/agent-permissions.ts +359 -0
- package/src/agent-runtime.ts +795 -28
- package/src/agent-workspace.ts +183 -0
- package/src/app.ts +1970 -79
- package/src/args.ts +54 -7
- package/src/cli.ts +873 -14
- package/src/client.ts +472 -10
- package/src/coco.ts +9 -40
- package/src/codex.ts +33 -5
- package/src/config.ts +28 -4
- package/src/console.ts +230 -20
- package/src/daemon-client.ts +116 -3
- package/src/daemon.ts +937 -99
- package/src/db.ts +3128 -122
- package/src/delivery-ws.ts +269 -0
- package/src/format.ts +4 -1
- package/src/lark/cli.ts +3 -3
- package/src/lark/event-router.ts +60 -4
- package/src/lark/inbound-events.ts +156 -3
- package/src/lark/server-integration.ts +659 -111
- package/src/lark/ws-daemon.ts +136 -10
- package/src/local-api.ts +545 -15
- package/src/local-auth.ts +33 -1
- package/src/message-attachments.ts +71 -0
- package/src/messaging-cli.ts +741 -0
- package/src/messaging-status.ts +669 -0
- package/src/migrations/024_agents_model.ts +10 -0
- package/src/migrations/025_room_archive.ts +44 -0
- package/src/migrations/026_project_archive.ts +44 -0
- package/src/migrations/027_agent_permission_profiles.ts +16 -0
- package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
- package/src/migrations/029_held_message_drafts.ts +32 -0
- package/src/migrations/030_agent_room_read_state.ts +25 -0
- package/src/migrations/031_room_tasks.ts +29 -0
- package/src/migrations/032_room_reminders.ts +29 -0
- package/src/migrations/033_room_saved_messages.ts +25 -0
- package/src/migrations/034_agent_activity_events.ts +27 -0
- package/src/migrations/035_agent_avatars.ts +17 -0
- package/src/migrations/036_project_agent_defaults.ts +21 -0
- package/src/migrations/037_message_attachments.ts +36 -0
- package/src/migrations/038_agent_activity_room_scope.ts +64 -0
- package/src/migrations/039_message_attachments_path.ts +34 -0
- package/src/migrations/040_message_attachments_file_schema.ts +80 -0
- package/src/migrations/041_room_system_events.ts +30 -0
- package/src/migrations/042_message_attachment_file_kind.ts +52 -0
- package/src/migrations/043_room_mode_skill_registry.ts +92 -0
- package/src/migrations/044_workflow_runtime.ts +69 -0
- package/src/migrations/045_skill_repository_ownership.ts +64 -0
- package/src/migrations.ts +69 -1
- package/src/neeko.ts +40 -4
- package/src/runtime-env.ts +179 -0
- package/src/runtime-registry.ts +83 -13
- package/src/server.ts +244 -4
- package/src/token-file.ts +13 -6
- package/src/types.ts +362 -0
- package/src/workflow-runtime.ts +275 -0
- 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 {
|
|
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 [--
|
|
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} --
|
|
60
|
-
`Every later turn should
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
233
|
-
|
|
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(
|
|
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') {
|