pragma-so 0.1.0

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/cli/index.ts +882 -0
  2. package/index.ts +3 -0
  3. package/package.json +53 -0
  4. package/server/connectorBinaries.ts +103 -0
  5. package/server/connectorRegistry.ts +158 -0
  6. package/server/conversation/adapterRegistry.ts +53 -0
  7. package/server/conversation/adapters/claudeAdapter.ts +138 -0
  8. package/server/conversation/adapters/codexAdapter.ts +142 -0
  9. package/server/conversation/adapters.ts +224 -0
  10. package/server/conversation/executeRunner.ts +1191 -0
  11. package/server/conversation/gitWorkflow.ts +1037 -0
  12. package/server/conversation/models.ts +23 -0
  13. package/server/conversation/pragmaCli.ts +34 -0
  14. package/server/conversation/prompts.ts +335 -0
  15. package/server/conversation/store.ts +805 -0
  16. package/server/conversation/titleGenerator.ts +106 -0
  17. package/server/conversation/turnRunner.ts +365 -0
  18. package/server/conversation/types.ts +134 -0
  19. package/server/db.ts +837 -0
  20. package/server/http/middleware.ts +31 -0
  21. package/server/http/schemas.ts +430 -0
  22. package/server/http/validators.ts +38 -0
  23. package/server/index.ts +6560 -0
  24. package/server/process/runCommand.ts +142 -0
  25. package/server/stores/agentStore.ts +167 -0
  26. package/server/stores/connectorStore.ts +299 -0
  27. package/server/stores/humanStore.ts +28 -0
  28. package/server/stores/skillStore.ts +127 -0
  29. package/server/stores/taskStore.ts +371 -0
  30. package/shared/net.ts +24 -0
  31. package/tsconfig.json +14 -0
  32. package/ui/index.html +14 -0
  33. package/ui/public/favicon-32.png +0 -0
  34. package/ui/public/favicon.png +0 -0
  35. package/ui/src/App.jsx +1338 -0
  36. package/ui/src/api.js +954 -0
  37. package/ui/src/components/CodeView.jsx +319 -0
  38. package/ui/src/components/ConnectionsView.jsx +1004 -0
  39. package/ui/src/components/ContextView.jsx +315 -0
  40. package/ui/src/components/ConversationDrawer.jsx +963 -0
  41. package/ui/src/components/EmptyPane.jsx +20 -0
  42. package/ui/src/components/FeedView.jsx +773 -0
  43. package/ui/src/components/FilesView.jsx +257 -0
  44. package/ui/src/components/InlineChatView.jsx +158 -0
  45. package/ui/src/components/InputBar.jsx +476 -0
  46. package/ui/src/components/OnboardingModal.jsx +112 -0
  47. package/ui/src/components/OutputPanel.jsx +658 -0
  48. package/ui/src/components/PlanProposalPanel.jsx +177 -0
  49. package/ui/src/components/RightPanel.jsx +951 -0
  50. package/ui/src/components/SettingsView.jsx +186 -0
  51. package/ui/src/components/Sidebar.jsx +247 -0
  52. package/ui/src/components/TestingPane.jsx +198 -0
  53. package/ui/src/components/testing/ApiTesterPanel.jsx +187 -0
  54. package/ui/src/components/testing/LogViewerPanel.jsx +64 -0
  55. package/ui/src/components/testing/TerminalPanel.jsx +104 -0
  56. package/ui/src/components/testing/WebPreviewPanel.jsx +78 -0
  57. package/ui/src/hooks/useAgents.js +81 -0
  58. package/ui/src/hooks/useConversation.js +252 -0
  59. package/ui/src/hooks/useTasks.js +161 -0
  60. package/ui/src/hooks/useWorkspace.js +259 -0
  61. package/ui/src/lib/agentIcon.js +10 -0
  62. package/ui/src/lib/conversationUtils.js +575 -0
  63. package/ui/src/main.jsx +10 -0
  64. package/ui/src/styles.css +6899 -0
  65. package/ui/vite.config.mjs +6 -0
package/cli/index.ts ADDED
@@ -0,0 +1,882 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { Command } from "commander";
6
+ import open from "open";
7
+ import type { ExecaChildProcess } from "execa";
8
+ import { spawnCommand, spawnNodeCommand } from "../server/process/runCommand";
9
+
10
+ const program = new Command();
11
+ const DEFAULT_API_URL = process.env.PRAGMA_API_URL ?? "http://127.0.0.1:3000";
12
+ const DEFAULT_UI_URL = process.env.PRAGMA_UI_URL ?? "http://127.0.0.1:5173";
13
+
14
+ if (!process.env.PRAGMA_CLI_COMMAND) {
15
+ const entry = process.argv[1] ? quoteShellArg(process.argv[1]) : "pragma";
16
+ process.env.PRAGMA_CLI_COMMAND = `${quoteShellArg(process.execPath)} ${entry}`;
17
+ }
18
+
19
+ program
20
+ .name("pragma")
21
+ .description("Very minimal CLI")
22
+ .version("0.1.0")
23
+ .action(async () => {
24
+ await runAll();
25
+ });
26
+
27
+ program
28
+ .command("setup")
29
+ .description("Call the API setup endpoint")
30
+ .option("-u, --api-url <url>", "Pragma API base URL", DEFAULT_API_URL)
31
+ .action(async (options: { apiUrl: string }) => {
32
+ await apiRequest(options.apiUrl, "/setup", { method: "POST" });
33
+ console.log("Setup complete.");
34
+ });
35
+
36
+ program
37
+ .command("create-task")
38
+ .description("Call the API to create a task")
39
+ .argument("<title>", "Task title")
40
+ .option("-a, --assigned-to <agentId>", "Assigned agent id")
41
+ .option("-o, --output-dir <outputDir>", "Output directory")
42
+ .option("-s, --status <status>", "Task status", "queued")
43
+ .option("-u, --api-url <url>", "Pragma API base URL", DEFAULT_API_URL)
44
+ .action(
45
+ async (
46
+ title: string,
47
+ options: {
48
+ assignedTo?: string;
49
+ outputDir?: string;
50
+ status: string;
51
+ apiUrl: string;
52
+ },
53
+ ) => {
54
+ const result = await apiRequest<{ id: string }>(options.apiUrl, "/tasks", {
55
+ method: "POST",
56
+ headers: { "content-type": "application/json" },
57
+ body: JSON.stringify({
58
+ title,
59
+ status: options.status,
60
+ assigned_to: options.assignedTo,
61
+ output_dir: options.outputDir,
62
+ }),
63
+ });
64
+
65
+ console.log(`Created task ${result.id}`);
66
+ },
67
+ );
68
+
69
+ program
70
+ .command("list-tasks")
71
+ .description("Call the API to list tasks")
72
+ .option("-s, --status <status>", "Filter by status")
73
+ .option("-l, --limit <limit>", "Maximum tasks to return", "25")
74
+ .option("-u, --api-url <url>", "Pragma API base URL", DEFAULT_API_URL)
75
+ .action(
76
+ async (options: { status?: string; limit: string; apiUrl: string }) => {
77
+ const params = new URLSearchParams();
78
+ if (options.status) {
79
+ params.set("status", options.status);
80
+ }
81
+ params.set("limit", options.limit);
82
+
83
+ const result = await apiRequest<{ tasks: Record<string, unknown>[] }>(
84
+ options.apiUrl,
85
+ `/tasks?${params.toString()}`,
86
+ );
87
+
88
+ if (result.tasks.length === 0) {
89
+ console.log("No tasks found.");
90
+ return;
91
+ }
92
+
93
+ console.table(result.tasks);
94
+ },
95
+ );
96
+
97
+ program
98
+ .command("list-agents")
99
+ .description("Call the API to list all agents")
100
+ .option("-u, --api-url <url>", "Pragma API base URL", DEFAULT_API_URL)
101
+ .action(async (options: { apiUrl: string }) => {
102
+ const result = await apiRequest<{
103
+ agents: Array<{
104
+ id: string;
105
+ name: string;
106
+ status: string;
107
+ harness: string;
108
+ model_label: string;
109
+ }>;
110
+ }>(options.apiUrl, "/agents");
111
+
112
+ if (result.agents.length === 0) {
113
+ console.log("No agents found.");
114
+ return;
115
+ }
116
+
117
+ console.table(result.agents);
118
+ });
119
+
120
+ program
121
+ .command("db-query")
122
+ .description("Run a read-only SQL query against the workspace database")
123
+ .requiredOption("--sql <text>", "SQL SELECT statement to execute")
124
+ .option("-u, --api-url <url>", "Pragma API base URL", DEFAULT_API_URL)
125
+ .action(
126
+ async (options: {
127
+ sql: string;
128
+ apiUrl: string;
129
+ }) => {
130
+ const result = await apiRequest<{ rows: Record<string, unknown>[]; rowCount: number }>(
131
+ options.apiUrl,
132
+ "/db/query",
133
+ {
134
+ method: "POST",
135
+ headers: { "content-type": "application/json" },
136
+ body: JSON.stringify({ sql: options.sql }),
137
+ },
138
+ );
139
+
140
+ if (result.rows.length === 0) {
141
+ console.log("No rows returned.");
142
+ return;
143
+ }
144
+
145
+ console.table(result.rows);
146
+ },
147
+ );
148
+
149
+ const taskCommand = program
150
+ .command("task")
151
+ .description("Agent task-control commands");
152
+
153
+ taskCommand
154
+ .command("select-recipient")
155
+ .description("Select a worker recipient for the current orchestrating task")
156
+ .requiredOption("--agent-id <id>", "Worker agent id")
157
+ .requiredOption("--reason <text>", "Selection reason")
158
+ .option("--task-id <id>", "Task id")
159
+ .option("--turn-id <id>", "Turn id")
160
+ .option("--api-url <url>", "Pragma API base URL")
161
+ .action(
162
+ async (options: {
163
+ agentId: string;
164
+ reason: string;
165
+ taskId?: string;
166
+ turnId?: string;
167
+ apiUrl?: string;
168
+ }) => {
169
+ const { apiUrl, taskId, turnId } = resolveTaskCommandContext(options);
170
+ const result = await apiRequest<{ assigned_to?: string }>(
171
+ apiUrl,
172
+ `/tasks/${encodeURIComponent(taskId)}/agent/select-recipient`,
173
+ {
174
+ method: "POST",
175
+ headers: { "content-type": "application/json" },
176
+ body: JSON.stringify({
177
+ agent_id: options.agentId,
178
+ reason: options.reason,
179
+ turn_id: turnId,
180
+ }),
181
+ },
182
+ );
183
+
184
+ const selected = result.assigned_to || options.agentId;
185
+ console.log(`Selected recipient ${selected} for task ${taskId}.`);
186
+ },
187
+ );
188
+
189
+ taskCommand
190
+ .command("plan-select-recipient")
191
+ .description("Select a worker recipient for the current plan turn")
192
+ .requiredOption("--agent-id <id>", "Worker agent id")
193
+ .requiredOption("--reason <text>", "Selection reason")
194
+ .option("--thread-id <id>", "Conversation thread id")
195
+ .option("--turn-id <id>", "Conversation turn id")
196
+ .option("--api-url <url>", "Pragma API base URL")
197
+ .action(
198
+ async (options: {
199
+ agentId: string;
200
+ reason: string;
201
+ threadId?: string;
202
+ turnId?: string;
203
+ apiUrl?: string;
204
+ }) => {
205
+ const { apiUrl, threadId, turnId } = resolveThreadTurnCommandContext(options);
206
+ const result = await apiRequest<{ selected_agent_id?: string }>(
207
+ apiUrl,
208
+ `/conversations/${encodeURIComponent(threadId)}/turns/${encodeURIComponent(turnId)}/agent/select-recipient`,
209
+ {
210
+ method: "POST",
211
+ headers: { "content-type": "application/json" },
212
+ body: JSON.stringify({
213
+ agent_id: options.agentId,
214
+ reason: options.reason,
215
+ }),
216
+ },
217
+ );
218
+
219
+ const selected = result.selected_agent_id || options.agentId;
220
+ console.log(`Selected plan recipient ${selected} for turn ${turnId}.`);
221
+ },
222
+ );
223
+
224
+ taskCommand
225
+ .command("plan-propose")
226
+ .description("Submit a structured plan proposal with a chain of tasks")
227
+ .option("--task <json>", "Task JSON object (repeatable: {title, prompt, recipient})", (val: string, acc: string[]) => { acc.push(val); return acc; }, [] as string[])
228
+ .option("--thread-id <id>", "Conversation thread id")
229
+ .option("--turn-id <id>", "Conversation turn id")
230
+ .option("--api-url <url>", "Pragma API base URL")
231
+ .action(
232
+ async (options: {
233
+ task: string[];
234
+ threadId?: string;
235
+ turnId?: string;
236
+ apiUrl?: string;
237
+ }) => {
238
+ const { apiUrl, threadId, turnId } = resolveThreadTurnCommandContext(options);
239
+
240
+ if (!options.task || options.task.length === 0) {
241
+ console.error("Error: At least one --task flag is required.");
242
+ process.exit(1);
243
+ }
244
+
245
+ const tasks: Array<{ title: string; prompt: string; recipient: string }> = [];
246
+ for (const raw of options.task) {
247
+ try {
248
+ const parsed = JSON.parse(raw);
249
+ if (!parsed.title || !parsed.prompt || !parsed.recipient) {
250
+ console.error(`Error: Each --task JSON must have title, prompt, and recipient fields. Got: ${raw}`);
251
+ process.exit(1);
252
+ }
253
+ tasks.push({ title: parsed.title, prompt: parsed.prompt, recipient: parsed.recipient });
254
+ } catch {
255
+ console.error(`Error: Invalid JSON for --task: ${raw}`);
256
+ process.exit(1);
257
+ }
258
+ }
259
+
260
+ const result = await apiRequest<{ ok?: boolean; task_count?: number }>(
261
+ apiUrl,
262
+ `/conversations/${encodeURIComponent(threadId)}/turns/${encodeURIComponent(turnId)}/agent/plan-propose`,
263
+ {
264
+ method: "POST",
265
+ headers: { "content-type": "application/json" },
266
+ body: JSON.stringify({ tasks }),
267
+ },
268
+ );
269
+
270
+ console.log(`Plan proposal submitted with ${result.task_count ?? tasks.length} task(s) for turn ${turnId}.`);
271
+ },
272
+ );
273
+
274
+ taskCommand
275
+ .command("ask-question")
276
+ .description("Pause execution and ask the human a clarification question")
277
+ .requiredOption("--question <text>", "Question for the human")
278
+ .option("--details <text>", "Optional context details")
279
+ .option("--option <text>", "Add a selectable answer option (repeatable)", (val: string, acc: string[]) => { acc.push(val); return acc; }, [] as string[])
280
+ .option("--task-id <id>", "Task id")
281
+ .option("--turn-id <id>", "Turn id")
282
+ .option("--api-url <url>", "Pragma API base URL")
283
+ .action(
284
+ async (options: {
285
+ question: string;
286
+ details?: string;
287
+ option: string[];
288
+ taskId?: string;
289
+ turnId?: string;
290
+ apiUrl?: string;
291
+ }) => {
292
+ const { apiUrl, taskId, turnId } = resolveTaskCommandContext(options);
293
+ const agentId = normalizeOptionalString(process.env.PRAGMA_AGENT_ID);
294
+ await apiRequest<{ status: string }>(
295
+ apiUrl,
296
+ `/tasks/${encodeURIComponent(taskId)}/agent/ask-question`,
297
+ {
298
+ method: "POST",
299
+ headers: { "content-type": "application/json" },
300
+ body: JSON.stringify({
301
+ question: options.question,
302
+ details: options.details,
303
+ turn_id: turnId,
304
+ agent_id: agentId,
305
+ options: options.option.length > 0 ? options.option : undefined,
306
+ }),
307
+ },
308
+ );
309
+
310
+ console.log(`Question submitted for task ${taskId}.`);
311
+ },
312
+ );
313
+
314
+ taskCommand
315
+ .command("request-help")
316
+ .description("Pause execution and request human help")
317
+ .requiredOption("--summary <text>", "Help summary")
318
+ .option("--details <text>", "Optional context details")
319
+ .option("--task-id <id>", "Task id")
320
+ .option("--turn-id <id>", "Turn id")
321
+ .option("--api-url <url>", "Pragma API base URL")
322
+ .action(
323
+ async (options: {
324
+ summary: string;
325
+ details?: string;
326
+ taskId?: string;
327
+ turnId?: string;
328
+ apiUrl?: string;
329
+ }) => {
330
+ const { apiUrl, taskId, turnId } = resolveTaskCommandContext(options);
331
+ const agentId = normalizeOptionalString(process.env.PRAGMA_AGENT_ID);
332
+ await apiRequest<{ status: string }>(
333
+ apiUrl,
334
+ `/tasks/${encodeURIComponent(taskId)}/agent/request-help`,
335
+ {
336
+ method: "POST",
337
+ headers: { "content-type": "application/json" },
338
+ body: JSON.stringify({
339
+ summary: options.summary,
340
+ details: options.details,
341
+ turn_id: turnId,
342
+ agent_id: agentId,
343
+ }),
344
+ },
345
+ );
346
+
347
+ console.log(`Help request submitted for task ${taskId}.`);
348
+ },
349
+ );
350
+
351
+ taskCommand
352
+ .command("submit-test-commands")
353
+ .description("Submit runnable test commands for the current task (appends by default)")
354
+ .requiredOption(
355
+ "--command <text>",
356
+ "Test command (repeat for multiple commands)",
357
+ (value: string, prev: string[]) => [...prev, value],
358
+ [],
359
+ )
360
+ .requiredOption(
361
+ "--cwd <path>",
362
+ "Run directory aligned to --command order (repeatable, relative to task workspace root)",
363
+ (value: string, prev: string[]) => [...prev, value],
364
+ [],
365
+ )
366
+ .option(
367
+ "--name <text>",
368
+ "Optional button label aligned to --command order (repeatable)",
369
+ (value: string, prev: string[]) => [...prev, value],
370
+ [],
371
+ )
372
+ .option("--task-id <id>", "Task id")
373
+ .option("--turn-id <id>", "Turn id")
374
+ .option("--replace", "Replace existing commands instead of appending")
375
+ .option("--api-url <url>", "Pragma API base URL")
376
+ .action(
377
+ async (options: {
378
+ command: string[];
379
+ cwd: string[];
380
+ name: string[];
381
+ taskId?: string;
382
+ turnId?: string;
383
+ replace?: boolean;
384
+ apiUrl?: string;
385
+ }) => {
386
+ const { apiUrl, taskId, turnId } = resolveTaskCommandContext(options);
387
+ const cwdByIndex = Array.isArray(options.cwd) ? options.cwd : [];
388
+ const commands = (Array.isArray(options.command) ? options.command : [])
389
+ .map((value, index) => {
390
+ const command = value.trim();
391
+ const cwd = (cwdByIndex[index] ?? "").trim();
392
+ const label = (Array.isArray(options.name) ? options.name[index] : "")?.trim() || command;
393
+ return { label, command, cwd };
394
+ })
395
+ .filter((item) => item.command.length > 0 && item.cwd.length > 0);
396
+
397
+ if (commands.length === 0) {
398
+ throw new Error("At least one --command and matching --cwd is required.");
399
+ }
400
+ if (commands.length !== (Array.isArray(options.command) ? options.command.length : 0)) {
401
+ throw new Error("Each --command must include a matching --cwd at the same index.");
402
+ }
403
+
404
+ await apiRequest(
405
+ apiUrl,
406
+ `/tasks/${encodeURIComponent(taskId)}/agent/test-commands`,
407
+ {
408
+ method: "POST",
409
+ headers: { "content-type": "application/json" },
410
+ body: JSON.stringify({
411
+ commands,
412
+ turn_id: turnId,
413
+ agent_id: normalizeOptionalString(process.env.PRAGMA_AGENT_ID),
414
+ replace: Boolean(options.replace),
415
+ }),
416
+ },
417
+ );
418
+
419
+ console.log(`Submitted ${commands.length} test command(s) for task ${taskId}.`);
420
+ },
421
+ );
422
+
423
+ taskCommand
424
+ .command("submit-testing-config")
425
+ .description("Submit a testing config for the current task")
426
+ .requiredOption("--config <json>", "The full testing config as a JSON string")
427
+ .option("--task-id <id>", "Task id")
428
+ .option("--turn-id <id>", "Turn id")
429
+ .option("--api-url <url>", "Pragma API base URL")
430
+ .action(
431
+ async (options: {
432
+ config: string;
433
+ taskId?: string;
434
+ turnId?: string;
435
+ apiUrl?: string;
436
+ }) => {
437
+ const { apiUrl, taskId, turnId } = resolveTaskCommandContext(options);
438
+
439
+ let config: unknown;
440
+ try {
441
+ config = JSON.parse(options.config);
442
+ } catch {
443
+ throw new Error("--config must be valid JSON.");
444
+ }
445
+
446
+ await apiRequest(
447
+ apiUrl,
448
+ `/tasks/${encodeURIComponent(taskId)}/agent/testing-config`,
449
+ {
450
+ method: "POST",
451
+ headers: { "content-type": "application/json" },
452
+ body: JSON.stringify({
453
+ config,
454
+ turn_id: turnId,
455
+ agent_id: normalizeOptionalString(process.env.PRAGMA_AGENT_ID),
456
+ }),
457
+ },
458
+ );
459
+
460
+ console.log(`Submitted testing config for task ${taskId}.`);
461
+ },
462
+ );
463
+
464
+ taskCommand
465
+ .command("plan-summary")
466
+ .description("Submit structured plan summary for the current plan turn")
467
+ .requiredOption("--title <text>", "Plan title")
468
+ .requiredOption("--summary <text>", "Plan summary")
469
+ .option(
470
+ "--step <text>",
471
+ "Plan step (repeat for multiple steps)",
472
+ (value: string, prev: string[]) => [...prev, value],
473
+ [],
474
+ )
475
+ .option("--thread-id <id>", "Conversation thread id")
476
+ .option("--turn-id <id>", "Conversation turn id")
477
+ .option("--api-url <url>", "Pragma API base URL")
478
+ .action(
479
+ async (options: {
480
+ title: string;
481
+ summary: string;
482
+ step: string[];
483
+ threadId?: string;
484
+ turnId?: string;
485
+ apiUrl?: string;
486
+ }) => {
487
+ const { apiUrl, threadId, turnId } = resolveThreadTurnCommandContext(options);
488
+ const steps = (Array.isArray(options.step) ? options.step : [])
489
+ .map((step) => step.trim())
490
+ .filter(Boolean);
491
+ if (steps.length === 0) {
492
+ throw new Error("At least one --step is required.");
493
+ }
494
+
495
+ await apiRequest(
496
+ apiUrl,
497
+ `/conversations/${encodeURIComponent(threadId)}/turns/${encodeURIComponent(turnId)}/agent/plan-summary`,
498
+ {
499
+ method: "POST",
500
+ headers: { "content-type": "application/json" },
501
+ body: JSON.stringify({
502
+ title: options.title.trim(),
503
+ summary: options.summary.trim(),
504
+ steps,
505
+ }),
506
+ },
507
+ );
508
+
509
+ console.log(`Plan summary submitted for turn ${turnId}.`);
510
+ },
511
+ );
512
+
513
+ const agentCommand = program
514
+ .command("agent")
515
+ .description("Agent skill commands");
516
+
517
+ agentCommand
518
+ .command("list-skills")
519
+ .description("List skills assigned to the current agent")
520
+ .option("--agent-id <id>", "Agent id")
521
+ .option("--api-url <url>", "Pragma API base URL")
522
+ .action(
523
+ async (options: {
524
+ agentId?: string;
525
+ apiUrl?: string;
526
+ }) => {
527
+ const apiUrl = resolveRequiredOptionOrEnv(options.apiUrl, "PRAGMA_API_URL", "--api-url");
528
+ const agentId = resolveRequiredOptionOrEnv(options.agentId, "PRAGMA_AGENT_ID", "--agent-id");
529
+ const result = await apiRequest<{
530
+ skills: Array<{ id: string; name: string; description: string | null }>;
531
+ }>(apiUrl, `/agents/${encodeURIComponent(agentId)}/skills`);
532
+
533
+ if (result.skills.length === 0) {
534
+ console.log("No skills assigned.");
535
+ return;
536
+ }
537
+
538
+ console.table(result.skills.map((s) => ({ name: s.name, description: s.description ?? "" })));
539
+ },
540
+ );
541
+
542
+ agentCommand
543
+ .command("get-skill")
544
+ .description("Print the full content of a skill assigned to the current agent")
545
+ .requiredOption("--name <name>", "Skill name")
546
+ .option("--agent-id <id>", "Agent id")
547
+ .option("--api-url <url>", "Pragma API base URL")
548
+ .action(
549
+ async (options: {
550
+ name: string;
551
+ agentId?: string;
552
+ apiUrl?: string;
553
+ }) => {
554
+ const apiUrl = resolveRequiredOptionOrEnv(options.apiUrl, "PRAGMA_API_URL", "--api-url");
555
+ const agentId = resolveRequiredOptionOrEnv(options.agentId, "PRAGMA_AGENT_ID", "--agent-id");
556
+
557
+ const listResult = await apiRequest<{
558
+ skills: Array<{ id: string; name: string; description: string | null }>;
559
+ }>(apiUrl, `/agents/${encodeURIComponent(agentId)}/skills`);
560
+
561
+ const skill = listResult.skills.find(
562
+ (s) => s.name.toLowerCase() === options.name.toLowerCase(),
563
+ );
564
+ if (skill) {
565
+ const response = await fetch(
566
+ `${apiUrl.replace(/\/$/, "")}/agents/${encodeURIComponent(agentId)}/skills/${encodeURIComponent(skill.id)}/content`,
567
+ );
568
+ if (!response.ok) {
569
+ throw new Error(`Failed to fetch skill content: HTTP ${response.status}`);
570
+ }
571
+ const content = await response.text();
572
+ console.log(content);
573
+ return;
574
+ }
575
+
576
+ // Fall back to connectors
577
+ const connectorResult = await apiRequest<{
578
+ connectors: Array<{ id: string; name: string; description: string | null; status: string }>;
579
+ }>(apiUrl, `/agents/${encodeURIComponent(agentId)}/connectors`);
580
+
581
+ const connector = connectorResult.connectors.find(
582
+ (c) => c.name.toLowerCase() === options.name.toLowerCase(),
583
+ );
584
+ if (connector) {
585
+ const response = await fetch(
586
+ `${apiUrl.replace(/\/$/, "")}/agents/${encodeURIComponent(agentId)}/connectors/${encodeURIComponent(connector.id)}/content`,
587
+ );
588
+ if (!response.ok) {
589
+ throw new Error(`Failed to fetch connector content: HTTP ${response.status}`);
590
+ }
591
+ const content = await response.text();
592
+ console.log(content);
593
+ return;
594
+ }
595
+
596
+ throw new Error(`Skill not found: ${options.name}`);
597
+ },
598
+ );
599
+
600
+ program
601
+ .command("server")
602
+ .description("Start the Pragma API server")
603
+ .option("-p, --port <port>", "Port to listen on", "3000")
604
+ .action(async (options: { port: string }) => {
605
+ const port = parsePort(options.port);
606
+ const { startServer } = await import("../server");
607
+ await startServer({ port });
608
+ });
609
+
610
+ program
611
+ .command("ui")
612
+ .description("Start the Pragma UI")
613
+ .option("-p, --port <port>", "UI port", "5173")
614
+ .option("-u, --api-url <url>", "Pragma API base URL", DEFAULT_API_URL)
615
+ .action(async (options: { port: string; apiUrl: string }) => {
616
+ await startUi({
617
+ port: parsePort(options.port),
618
+ apiUrl: options.apiUrl,
619
+ });
620
+ });
621
+
622
+ async function runAll(): Promise<void> {
623
+ const apiUrl = DEFAULT_API_URL;
624
+ const uiUrl = DEFAULT_UI_URL;
625
+ const serverPort = parsePort(new URL(apiUrl).port || "3000");
626
+ const uiPort = parsePort(new URL(uiUrl).port || "5173");
627
+
628
+ const serverProcess = spawnSelfCommand(["server", "--port", String(serverPort)]);
629
+ const serverExit = waitForExit(serverProcess, "server");
630
+
631
+ await waitForHealth(apiUrl);
632
+
633
+ const uiProcess = spawnSelfCommand([
634
+ "ui",
635
+ "--port",
636
+ String(uiPort),
637
+ "--api-url",
638
+ apiUrl,
639
+ ]);
640
+ const uiExit = waitForExit(uiProcess, "ui");
641
+
642
+ try {
643
+ await open(uiUrl, { wait: false });
644
+ } catch (error) {
645
+ console.warn(`Unable to open browser automatically: ${errorMessage(error)}`);
646
+ console.warn(`Open ${uiUrl} manually.`);
647
+ }
648
+
649
+ let shuttingDown = false;
650
+
651
+ const stop = () => {
652
+ if (shuttingDown) {
653
+ return;
654
+ }
655
+ shuttingDown = true;
656
+ serverProcess.kill("SIGTERM");
657
+ uiProcess.kill("SIGTERM");
658
+ };
659
+
660
+ process.once("SIGINT", stop);
661
+ process.once("SIGTERM", stop);
662
+
663
+ const firstExit = await Promise.race([serverExit, uiExit]);
664
+
665
+ if (!shuttingDown) {
666
+ shuttingDown = true;
667
+ serverProcess.kill("SIGTERM");
668
+ uiProcess.kill("SIGTERM");
669
+
670
+ await Promise.allSettled([serverExit, uiExit]);
671
+ throw new Error(
672
+ `${firstExit.name} exited unexpectedly with ${formatExit(firstExit)}.`,
673
+ );
674
+ }
675
+
676
+ await Promise.allSettled([serverExit, uiExit]);
677
+ }
678
+
679
+ async function startUi(options: { port: number; apiUrl: string }): Promise<void> {
680
+ const uiDir = await resolveUiDir();
681
+ const projectRoot = dirname(uiDir);
682
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
683
+
684
+ const child = spawnCommand({
685
+ command: npmCommand,
686
+ args: ["run", "ui:dev", "--", "--host", "127.0.0.1", "--port", String(options.port)],
687
+ cwd: projectRoot,
688
+ stdio: "inherit",
689
+ env: {
690
+ ...process.env,
691
+ VITE_API_URL: options.apiUrl,
692
+ },
693
+ });
694
+
695
+ await waitForExit(child, "ui");
696
+ }
697
+
698
+ async function resolveUiDir(): Promise<string> {
699
+ const candidates = [
700
+ join(__dirname, "..", "..", "ui"),
701
+ join(__dirname, "..", "ui"),
702
+ join(process.cwd(), "ui"),
703
+ ];
704
+
705
+ for (const candidate of candidates) {
706
+ if (await pathExists(candidate)) {
707
+ return candidate;
708
+ }
709
+ }
710
+
711
+ throw new Error("UI folder not found.");
712
+ }
713
+
714
+ async function waitForHealth(apiUrl: string): Promise<void> {
715
+ const timeoutMs = 15000;
716
+ const start = Date.now();
717
+
718
+ while (Date.now() - start < timeoutMs) {
719
+ try {
720
+ await apiRequest(apiUrl, "/health");
721
+ return;
722
+ } catch {
723
+ await sleep(250);
724
+ }
725
+ }
726
+
727
+ throw new Error("Server did not become ready in time.");
728
+ }
729
+
730
+ function spawnSelfCommand(args: string[]) {
731
+ return spawnNodeCommand({
732
+ modulePath: __filename,
733
+ args,
734
+ cwd: process.cwd(),
735
+ stdio: "inherit",
736
+ env: process.env,
737
+ });
738
+ }
739
+
740
+ function waitForExit(
741
+ child: ExecaChildProcess<string>,
742
+ name: string,
743
+ ): Promise<{ name: string; exitCode: number | null; signal: string | undefined }> {
744
+ return child.then((result) => {
745
+ return {
746
+ name,
747
+ exitCode: result.exitCode,
748
+ signal: result.signal,
749
+ };
750
+ });
751
+ }
752
+
753
+ function formatExit(result: {
754
+ exitCode: number | null;
755
+ signal: string | undefined;
756
+ }): string {
757
+ if (result.signal) {
758
+ return `signal ${result.signal}`;
759
+ }
760
+ if (result.exitCode === null) {
761
+ return "unknown exit";
762
+ }
763
+ return `exit code ${result.exitCode}`;
764
+ }
765
+
766
+ async function apiRequest<T = Record<string, unknown>>(
767
+ apiUrl: string,
768
+ path: string,
769
+ init?: RequestInit,
770
+ ): Promise<T> {
771
+ const base = apiUrl.replace(/\/$/, "");
772
+ const response = await fetch(`${base}${path}`, init);
773
+
774
+ if (!response.ok) {
775
+ let message = `HTTP ${response.status}`;
776
+ try {
777
+ const body = (await response.json()) as { error?: string };
778
+ if (body.error) {
779
+ message = body.error;
780
+ }
781
+ } catch {
782
+ // Keep default message.
783
+ }
784
+ throw new Error(message);
785
+ }
786
+
787
+ if (response.status === 204) {
788
+ return {} as T;
789
+ }
790
+
791
+ return (await response.json()) as T;
792
+ }
793
+
794
+ function parsePort(portValue: string): number {
795
+ const port = Number.parseInt(portValue, 10);
796
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
797
+ throw new Error(`Invalid --port value: ${portValue}. Use an integer 1-65535.`);
798
+ }
799
+ return port;
800
+ }
801
+
802
+ function resolveTaskCommandContext(input: {
803
+ apiUrl?: string;
804
+ taskId?: string;
805
+ turnId?: string;
806
+ }): {
807
+ apiUrl: string;
808
+ taskId: string;
809
+ turnId?: string;
810
+ } {
811
+ const apiUrl = resolveRequiredOptionOrEnv(input.apiUrl, "PRAGMA_API_URL", "--api-url");
812
+ const taskId = resolveRequiredOptionOrEnv(input.taskId, "PRAGMA_TASK_ID", "--task-id");
813
+ const turnId = normalizeOptionalString(input.turnId) || normalizeOptionalString(process.env.PRAGMA_TURN_ID);
814
+ return { apiUrl, taskId, turnId };
815
+ }
816
+
817
+ function resolveThreadTurnCommandContext(input: {
818
+ apiUrl?: string;
819
+ threadId?: string;
820
+ turnId?: string;
821
+ }): {
822
+ apiUrl: string;
823
+ threadId: string;
824
+ turnId: string;
825
+ } {
826
+ const apiUrl = resolveRequiredOptionOrEnv(input.apiUrl, "PRAGMA_API_URL", "--api-url");
827
+ const threadId = resolveRequiredOptionOrEnv(input.threadId, "PRAGMA_THREAD_ID", "--thread-id");
828
+ const turnId = resolveRequiredOptionOrEnv(input.turnId, "PRAGMA_TURN_ID", "--turn-id");
829
+ return { apiUrl, threadId, turnId };
830
+ }
831
+
832
+ function resolveRequiredOptionOrEnv(
833
+ optionValue: string | undefined,
834
+ envName: string,
835
+ optionLabel: string,
836
+ ): string {
837
+ const fromOption = normalizeOptionalString(optionValue);
838
+ if (fromOption) {
839
+ return fromOption;
840
+ }
841
+
842
+ const fromEnv = normalizeOptionalString(process.env[envName]);
843
+ if (fromEnv) {
844
+ return fromEnv;
845
+ }
846
+
847
+ throw new Error(`Missing ${optionLabel}. Pass ${optionLabel} or set ${envName}.`);
848
+ }
849
+
850
+ function normalizeOptionalString(value: string | undefined): string | undefined {
851
+ if (typeof value !== "string") {
852
+ return undefined;
853
+ }
854
+ const trimmed = value.trim();
855
+ return trimmed.length > 0 ? trimmed : undefined;
856
+ }
857
+
858
+ function quoteShellArg(value: string): string {
859
+ return `"${value.replace(/["\\$`]/g, "\\$&")}"`;
860
+ }
861
+
862
+ async function pathExists(path: string): Promise<boolean> {
863
+ try {
864
+ await access(path);
865
+ return true;
866
+ } catch {
867
+ return false;
868
+ }
869
+ }
870
+
871
+ function sleep(ms: number): Promise<void> {
872
+ return new Promise((resolve) => setTimeout(resolve, ms));
873
+ }
874
+
875
+ function errorMessage(error: unknown): string {
876
+ return error instanceof Error ? error.message : String(error);
877
+ }
878
+
879
+ program.parseAsync(process.argv).catch((error: unknown) => {
880
+ console.error(errorMessage(error));
881
+ process.exitCode = 1;
882
+ });