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.
- package/cli/index.ts +882 -0
- package/index.ts +3 -0
- package/package.json +53 -0
- package/server/connectorBinaries.ts +103 -0
- package/server/connectorRegistry.ts +158 -0
- package/server/conversation/adapterRegistry.ts +53 -0
- package/server/conversation/adapters/claudeAdapter.ts +138 -0
- package/server/conversation/adapters/codexAdapter.ts +142 -0
- package/server/conversation/adapters.ts +224 -0
- package/server/conversation/executeRunner.ts +1191 -0
- package/server/conversation/gitWorkflow.ts +1037 -0
- package/server/conversation/models.ts +23 -0
- package/server/conversation/pragmaCli.ts +34 -0
- package/server/conversation/prompts.ts +335 -0
- package/server/conversation/store.ts +805 -0
- package/server/conversation/titleGenerator.ts +106 -0
- package/server/conversation/turnRunner.ts +365 -0
- package/server/conversation/types.ts +134 -0
- package/server/db.ts +837 -0
- package/server/http/middleware.ts +31 -0
- package/server/http/schemas.ts +430 -0
- package/server/http/validators.ts +38 -0
- package/server/index.ts +6560 -0
- package/server/process/runCommand.ts +142 -0
- package/server/stores/agentStore.ts +167 -0
- package/server/stores/connectorStore.ts +299 -0
- package/server/stores/humanStore.ts +28 -0
- package/server/stores/skillStore.ts +127 -0
- package/server/stores/taskStore.ts +371 -0
- package/shared/net.ts +24 -0
- package/tsconfig.json +14 -0
- package/ui/index.html +14 -0
- package/ui/public/favicon-32.png +0 -0
- package/ui/public/favicon.png +0 -0
- package/ui/src/App.jsx +1338 -0
- package/ui/src/api.js +954 -0
- package/ui/src/components/CodeView.jsx +319 -0
- package/ui/src/components/ConnectionsView.jsx +1004 -0
- package/ui/src/components/ContextView.jsx +315 -0
- package/ui/src/components/ConversationDrawer.jsx +963 -0
- package/ui/src/components/EmptyPane.jsx +20 -0
- package/ui/src/components/FeedView.jsx +773 -0
- package/ui/src/components/FilesView.jsx +257 -0
- package/ui/src/components/InlineChatView.jsx +158 -0
- package/ui/src/components/InputBar.jsx +476 -0
- package/ui/src/components/OnboardingModal.jsx +112 -0
- package/ui/src/components/OutputPanel.jsx +658 -0
- package/ui/src/components/PlanProposalPanel.jsx +177 -0
- package/ui/src/components/RightPanel.jsx +951 -0
- package/ui/src/components/SettingsView.jsx +186 -0
- package/ui/src/components/Sidebar.jsx +247 -0
- package/ui/src/components/TestingPane.jsx +198 -0
- package/ui/src/components/testing/ApiTesterPanel.jsx +187 -0
- package/ui/src/components/testing/LogViewerPanel.jsx +64 -0
- package/ui/src/components/testing/TerminalPanel.jsx +104 -0
- package/ui/src/components/testing/WebPreviewPanel.jsx +78 -0
- package/ui/src/hooks/useAgents.js +81 -0
- package/ui/src/hooks/useConversation.js +252 -0
- package/ui/src/hooks/useTasks.js +161 -0
- package/ui/src/hooks/useWorkspace.js +259 -0
- package/ui/src/lib/agentIcon.js +10 -0
- package/ui/src/lib/conversationUtils.js +575 -0
- package/ui/src/main.jsx +10 -0
- package/ui/src/styles.css +6899 -0
- 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
|
+
});
|