pilotswarm-cli 0.1.12 → 0.1.13
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/bin/tui.js +4 -279
- package/package.json +16 -12
- package/src/app.js +595 -0
- package/src/bootstrap-env.js +187 -0
- package/src/embedded-workers.js +65 -0
- package/src/index.js +152 -0
- package/src/node-sdk-transport.js +702 -0
- package/src/platform.js +899 -0
- package/tui-splash.txt +11 -0
- package/cli/context-usage.js +0 -80
- package/cli/tui.js +0 -8656
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
FilesystemArtifactStore,
|
|
7
|
+
loadAgentFiles,
|
|
8
|
+
PilotSwarmClient,
|
|
9
|
+
PilotSwarmManagementClient,
|
|
10
|
+
SessionBlobStore,
|
|
11
|
+
} from "pilotswarm-sdk";
|
|
12
|
+
import { startEmbeddedWorkers, stopEmbeddedWorkers } from "./embedded-workers.js";
|
|
13
|
+
|
|
14
|
+
const EXPORTS_DIR = path.join(os.homedir(), "pilotswarm-exports");
|
|
15
|
+
fs.mkdirSync(EXPORTS_DIR, { recursive: true });
|
|
16
|
+
|
|
17
|
+
function stripAnsi(value) {
|
|
18
|
+
return String(value || "").replace(/\x1b\[[0-9;]*m/g, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function trimLogText(value, maxLength = 2_000) {
|
|
22
|
+
const text = String(value || "");
|
|
23
|
+
return text.length > maxLength
|
|
24
|
+
? `${text.slice(0, maxLength - 1)}…`
|
|
25
|
+
: text;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractPrettyLogMessage(rawLine) {
|
|
29
|
+
const source = trimLogText(stripAnsi(rawLine)).trim();
|
|
30
|
+
if (!source) return "";
|
|
31
|
+
|
|
32
|
+
let message = source
|
|
33
|
+
.replace(/^\d{4}-\d{2}-\d{2}T\S+\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+\S+:\s*/i, "")
|
|
34
|
+
.replace(/^(TRACE|DEBUG|INFO|WARN|ERROR)\s+/i, "")
|
|
35
|
+
.replace(/^\[v[^\]]+\]\s*/i, "")
|
|
36
|
+
.trim();
|
|
37
|
+
|
|
38
|
+
const metadataMarkers = [
|
|
39
|
+
" instance_id=",
|
|
40
|
+
" orchestration_id=",
|
|
41
|
+
" execution_id=",
|
|
42
|
+
" orchestration_name=",
|
|
43
|
+
" orchestration_version=",
|
|
44
|
+
" activity_name=",
|
|
45
|
+
" activity_id=",
|
|
46
|
+
" worker_id=",
|
|
47
|
+
" filter=",
|
|
48
|
+
" options=",
|
|
49
|
+
" instances_deleted=",
|
|
50
|
+
" executions_deleted=",
|
|
51
|
+
" events_deleted=",
|
|
52
|
+
" queue_messages_deleted=",
|
|
53
|
+
" instances_processed=",
|
|
54
|
+
" instance=",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
let cutIndex = -1;
|
|
58
|
+
for (const marker of metadataMarkers) {
|
|
59
|
+
const nextIndex = message.indexOf(marker);
|
|
60
|
+
if (nextIndex <= 0) continue;
|
|
61
|
+
if (cutIndex === -1 || nextIndex < cutIndex) {
|
|
62
|
+
cutIndex = nextIndex;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (cutIndex > 0) {
|
|
67
|
+
message = message.slice(0, cutIndex).trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return message || source;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeLogLevel(line) {
|
|
74
|
+
const match = stripAnsi(line).match(/\b(ERROR|WARN|INFO|DEBUG|TRACE)\b/i);
|
|
75
|
+
return match ? match[1].toLowerCase() : "info";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function extractLogTime(line) {
|
|
79
|
+
const plain = stripAnsi(line);
|
|
80
|
+
const hhmmss = plain.match(/\b(\d{2}:\d{2}:\d{2})(?:\.\d+)?\b/);
|
|
81
|
+
if (hhmmss) return hhmmss[1];
|
|
82
|
+
|
|
83
|
+
const iso = plain.match(/\b(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)\b/);
|
|
84
|
+
if (iso) {
|
|
85
|
+
const parsed = new Date(iso[1]);
|
|
86
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
87
|
+
return parsed.toLocaleTimeString("en-US", {
|
|
88
|
+
hour12: false,
|
|
89
|
+
hour: "2-digit",
|
|
90
|
+
minute: "2-digit",
|
|
91
|
+
second: "2-digit",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new Date().toLocaleTimeString("en-US", {
|
|
97
|
+
hour12: false,
|
|
98
|
+
hour: "2-digit",
|
|
99
|
+
minute: "2-digit",
|
|
100
|
+
second: "2-digit",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildLogEntry(line, counter) {
|
|
105
|
+
const prefixMatch = line.match(/^\[pod\/([^/\]]+)/);
|
|
106
|
+
const podName = prefixMatch ? prefixMatch[1] : "unknown";
|
|
107
|
+
const rawLine = trimLogText(stripAnsi(line.replace(/^\[pod\/[^\]]+\]\s*/, "")).trim());
|
|
108
|
+
const orchMatch = rawLine.match(/\b(instance_id|orchestration_id)=(session-[^\s,]+)/i)
|
|
109
|
+
|| rawLine.match(/\b(session-[0-9a-f-]{8,})\b/i);
|
|
110
|
+
const orchId = orchMatch ? orchMatch[2] || orchMatch[1] : null;
|
|
111
|
+
const category = rawLine.includes("duroxide::activity")
|
|
112
|
+
? "activity"
|
|
113
|
+
: rawLine.includes("duroxide::orchestration") || rawLine.includes("::orchestration")
|
|
114
|
+
? "orchestration"
|
|
115
|
+
: "log";
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
id: `log:${Date.now()}:${counter}`,
|
|
119
|
+
time: extractLogTime(rawLine),
|
|
120
|
+
podName,
|
|
121
|
+
level: normalizeLogLevel(rawLine),
|
|
122
|
+
orchId,
|
|
123
|
+
category,
|
|
124
|
+
rawLine,
|
|
125
|
+
message: extractPrettyLogMessage(rawLine),
|
|
126
|
+
prettyMessage: extractPrettyLogMessage(rawLine),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sanitizeArtifactFilename(filename) {
|
|
131
|
+
return String(filename || "").replace(/[/\\]/g, "_");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function expandUserPath(filePath) {
|
|
135
|
+
const value = String(filePath || "").trim();
|
|
136
|
+
if (!value) return "";
|
|
137
|
+
return value.startsWith("~")
|
|
138
|
+
? path.join(os.homedir(), value.slice(1))
|
|
139
|
+
: value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function guessArtifactContentType(filename) {
|
|
143
|
+
const ext = path.extname(String(filename || "")).toLowerCase();
|
|
144
|
+
if (ext === ".md" || ext === ".markdown" || ext === ".mdx") return "text/markdown";
|
|
145
|
+
if (ext === ".json" || ext === ".jsonl") return "application/json";
|
|
146
|
+
if (ext === ".html" || ext === ".htm") return "text/html";
|
|
147
|
+
if (ext === ".csv") return "text/csv";
|
|
148
|
+
if (ext === ".yaml" || ext === ".yml") return "application/yaml";
|
|
149
|
+
return "text/plain";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function spawnDetached(command, args) {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
let settled = false;
|
|
155
|
+
const child = spawn(command, args, {
|
|
156
|
+
detached: true,
|
|
157
|
+
stdio: "ignore",
|
|
158
|
+
});
|
|
159
|
+
child.once("error", (error) => {
|
|
160
|
+
if (settled) return;
|
|
161
|
+
settled = true;
|
|
162
|
+
reject(error);
|
|
163
|
+
});
|
|
164
|
+
child.once("spawn", () => {
|
|
165
|
+
if (settled) return;
|
|
166
|
+
settled = true;
|
|
167
|
+
child.unref();
|
|
168
|
+
resolve();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isTerminalOrchestrationStatus(status) {
|
|
174
|
+
return status === "Completed" || status === "Failed" || status === "Terminated";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isTerminalSendError(error) {
|
|
178
|
+
const message = String(error?.message || error || "");
|
|
179
|
+
return /instance is terminal|terminal orchestration|cannot accept new messages/i.test(message);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getPluginDirsFromEnv() {
|
|
183
|
+
return String(process.env.PLUGIN_DIRS || "")
|
|
184
|
+
.split(",")
|
|
185
|
+
.map((value) => value.trim())
|
|
186
|
+
.filter(Boolean);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeCreatableAgent(agent) {
|
|
190
|
+
const name = String(agent?.name || "").trim();
|
|
191
|
+
if (!name) return null;
|
|
192
|
+
return {
|
|
193
|
+
name,
|
|
194
|
+
title: String(agent?.title || "").trim() || (name.charAt(0).toUpperCase() + name.slice(1)),
|
|
195
|
+
description: String(agent?.description || "").trim(),
|
|
196
|
+
splash: typeof agent?.splash === "string" && agent.splash.trim() ? agent.splash : null,
|
|
197
|
+
initialPrompt: typeof agent?.initialPrompt === "string" && agent.initialPrompt.trim() ? agent.initialPrompt : null,
|
|
198
|
+
tools: Array.isArray(agent?.tools) ? agent.tools.filter(Boolean) : [],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function loadSessionCreationMetadataFromPluginDirs(pluginDirs = []) {
|
|
203
|
+
let sessionPolicy = null;
|
|
204
|
+
const agentsByName = new Map();
|
|
205
|
+
|
|
206
|
+
for (const pluginDir of pluginDirs) {
|
|
207
|
+
const absDir = path.resolve(pluginDir);
|
|
208
|
+
if (!fs.existsSync(absDir)) continue;
|
|
209
|
+
|
|
210
|
+
const policyPath = path.join(absDir, "session-policy.json");
|
|
211
|
+
if (fs.existsSync(policyPath)) {
|
|
212
|
+
try {
|
|
213
|
+
sessionPolicy = JSON.parse(fs.readFileSync(policyPath, "utf-8"));
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const agentsDir = path.join(absDir, "agents");
|
|
218
|
+
if (!fs.existsSync(agentsDir)) continue;
|
|
219
|
+
try {
|
|
220
|
+
for (const agent of loadAgentFiles(agentsDir)) {
|
|
221
|
+
if (!agent || agent.system || agent.name === "default") continue;
|
|
222
|
+
const normalized = normalizeCreatableAgent(agent);
|
|
223
|
+
if (!normalized) continue;
|
|
224
|
+
agentsByName.set(normalized.name, normalized);
|
|
225
|
+
}
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const creatableAgents = [...agentsByName.values()];
|
|
230
|
+
return {
|
|
231
|
+
sessionPolicy,
|
|
232
|
+
allowedAgentNames: creatableAgents.map((agent) => agent.name),
|
|
233
|
+
creatableAgents,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildTerminalSendError(sessionId, session) {
|
|
238
|
+
if (session?.status === "failed" || session?.orchestrationStatus === "Failed") {
|
|
239
|
+
return `Session ${sessionId.slice(0, 8)} is a failed terminal orchestration and cannot accept new messages.`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const statusLabel = String(session?.orchestrationStatus || session?.status || "Unknown");
|
|
243
|
+
return `Session ${sessionId.slice(0, 8)} is a terminal orchestration instance (${statusLabel}) and cannot accept new messages.`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export class NodeSdkTransport {
|
|
247
|
+
constructor({ store, mode }) {
|
|
248
|
+
this.store = store;
|
|
249
|
+
this.mode = mode;
|
|
250
|
+
this.client = null;
|
|
251
|
+
this.mgmt = new PilotSwarmManagementClient({ store });
|
|
252
|
+
this.artifactStore = createArtifactStore();
|
|
253
|
+
this.sessionHandles = new Map();
|
|
254
|
+
this.workers = [];
|
|
255
|
+
this.sessionPolicy = null;
|
|
256
|
+
this.allowedAgentNames = [];
|
|
257
|
+
this.creatableAgents = [];
|
|
258
|
+
this.logProc = null;
|
|
259
|
+
this.logBuffer = "";
|
|
260
|
+
this.logRestartTimer = null;
|
|
261
|
+
this.logSubscribers = new Set();
|
|
262
|
+
this.logEntryCounter = 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async start() {
|
|
266
|
+
const workerCount = this.mode === "remote" ? 0 : parseInt(process.env.WORKERS || "4", 10);
|
|
267
|
+
if (workerCount > 0) {
|
|
268
|
+
this.workers = await startEmbeddedWorkers({
|
|
269
|
+
count: workerCount,
|
|
270
|
+
store: this.store,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const sessionCreationMetadata = this.resolveSessionCreationMetadata();
|
|
274
|
+
this.sessionPolicy = sessionCreationMetadata.sessionPolicy;
|
|
275
|
+
this.allowedAgentNames = sessionCreationMetadata.allowedAgentNames;
|
|
276
|
+
this.creatableAgents = sessionCreationMetadata.creatableAgents;
|
|
277
|
+
this.client = new PilotSwarmClient({
|
|
278
|
+
store: this.store,
|
|
279
|
+
...(this.sessionPolicy ? { sessionPolicy: this.sessionPolicy } : {}),
|
|
280
|
+
...(this.allowedAgentNames.length > 0 ? { allowedAgentNames: this.allowedAgentNames } : {}),
|
|
281
|
+
});
|
|
282
|
+
await this.client.start();
|
|
283
|
+
await this.mgmt.start();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async stop() {
|
|
287
|
+
this.sessionHandles.clear();
|
|
288
|
+
await this.stopLogTail();
|
|
289
|
+
await Promise.allSettled([
|
|
290
|
+
this.client ? this.client.stop() : Promise.resolve(),
|
|
291
|
+
this.mgmt.stop(),
|
|
292
|
+
stopEmbeddedWorkers(this.workers),
|
|
293
|
+
]);
|
|
294
|
+
this.client = null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
resolveSessionCreationMetadata() {
|
|
298
|
+
if (this.workers.length > 0) {
|
|
299
|
+
const firstWorker = this.workers[0];
|
|
300
|
+
const creatableAgents = Array.isArray(firstWorker?.loadedAgents)
|
|
301
|
+
? firstWorker.loadedAgents.map((agent) => normalizeCreatableAgent(agent)).filter(Boolean)
|
|
302
|
+
: [];
|
|
303
|
+
return {
|
|
304
|
+
sessionPolicy: firstWorker?.sessionPolicy || null,
|
|
305
|
+
allowedAgentNames: Array.isArray(firstWorker?.allowedAgentNames) ? firstWorker.allowedAgentNames.filter(Boolean) : creatableAgents.map((agent) => agent.name),
|
|
306
|
+
creatableAgents,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return loadSessionCreationMetadataFromPluginDirs(getPluginDirsFromEnv());
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
getWorkerCount() {
|
|
313
|
+
return this.workers.length || (this.mode === "remote" ? 0 : parseInt(process.env.WORKERS || "4", 10));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
getLogConfig() {
|
|
317
|
+
const hasK8sConfig = Boolean((process.env.K8S_CONTEXT || "").trim() || (process.env.KUBECONFIG || "").trim());
|
|
318
|
+
return {
|
|
319
|
+
available: hasK8sConfig,
|
|
320
|
+
availabilityReason: hasK8sConfig
|
|
321
|
+
? ""
|
|
322
|
+
: "Log tailing disabled: no K8S_CONTEXT configured in the env file.",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async listSessions() {
|
|
327
|
+
return this.mgmt.listSessions();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async getSession(sessionId) {
|
|
331
|
+
return this.mgmt.getSession(sessionId);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async getOrchestrationStats(sessionId) {
|
|
335
|
+
return this.mgmt.getOrchestrationStats(sessionId);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async createSession({ model } = {}) {
|
|
339
|
+
const effectiveModel = model || this.mgmt.getDefaultModel();
|
|
340
|
+
const session = await this.client.createSession(effectiveModel ? { model: effectiveModel } : undefined);
|
|
341
|
+
this.sessionHandles.set(session.sessionId, session);
|
|
342
|
+
return { sessionId: session.sessionId, model: effectiveModel };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async createSessionForAgent(agentName, { model, title, splash, initialPrompt } = {}) {
|
|
346
|
+
const effectiveModel = model || this.mgmt.getDefaultModel();
|
|
347
|
+
const session = await this.client.createSessionForAgent(agentName, {
|
|
348
|
+
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
349
|
+
...(title ? { title } : {}),
|
|
350
|
+
...(splash ? { splash } : {}),
|
|
351
|
+
...(initialPrompt ? { initialPrompt } : {}),
|
|
352
|
+
});
|
|
353
|
+
this.sessionHandles.set(session.sessionId, session);
|
|
354
|
+
return {
|
|
355
|
+
sessionId: session.sessionId,
|
|
356
|
+
model: effectiveModel,
|
|
357
|
+
agentName,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
listCreatableAgents() {
|
|
362
|
+
return [...this.creatableAgents];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
getSessionCreationPolicy() {
|
|
366
|
+
return this.sessionPolicy;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async sendMessage(sessionId, prompt, options = {}) {
|
|
370
|
+
const session = await this.mgmt.getSession(sessionId);
|
|
371
|
+
if (!session) {
|
|
372
|
+
throw new Error(`Session ${sessionId.slice(0, 8)} was not found.`);
|
|
373
|
+
}
|
|
374
|
+
if (session.status === "failed" || session.orchestrationStatus === "Failed") {
|
|
375
|
+
throw new Error(buildTerminalSendError(sessionId, session));
|
|
376
|
+
}
|
|
377
|
+
if (
|
|
378
|
+
session.status === "completed"
|
|
379
|
+
&& session.parentSessionId
|
|
380
|
+
&& !session.isSystem
|
|
381
|
+
&& !session.cronActive
|
|
382
|
+
&& !session.cronInterval
|
|
383
|
+
) {
|
|
384
|
+
throw new Error(buildTerminalSendError(sessionId, session));
|
|
385
|
+
}
|
|
386
|
+
if (this.mode === "remote" && isTerminalOrchestrationStatus(session.orchestrationStatus)) {
|
|
387
|
+
throw new Error(buildTerminalSendError(sessionId, session));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (options?.enqueueOnly) {
|
|
391
|
+
await this.mgmt.sendMessage(sessionId, prompt);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const sessionHandle = await this.getSessionHandle(sessionId);
|
|
397
|
+
await sessionHandle.send(prompt);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (isTerminalSendError(error)) {
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
await this.mgmt.sendMessage(sessionId, prompt);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async sendAnswer(sessionId, answer) {
|
|
407
|
+
await this.mgmt.sendAnswer(sessionId, answer);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async renameSession(sessionId, title) {
|
|
411
|
+
await this.mgmt.renameSession(sessionId, title);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async cancelSession(sessionId) {
|
|
415
|
+
await this.mgmt.cancelSession(sessionId);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async completeSession(sessionId, reason = "Completed by user") {
|
|
419
|
+
await this.mgmt.sendCommand(sessionId, {
|
|
420
|
+
cmd: "done",
|
|
421
|
+
id: `done-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
422
|
+
args: { reason },
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async deleteSession(sessionId) {
|
|
427
|
+
await this.mgmt.deleteSession(sessionId);
|
|
428
|
+
this.sessionHandles.delete(sessionId);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async listModels() {
|
|
432
|
+
return this.mgmt.listModels();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async listArtifacts(sessionId) {
|
|
436
|
+
if (!this.artifactStore || !sessionId) return [];
|
|
437
|
+
const artifacts = await this.artifactStore.listArtifacts(sessionId);
|
|
438
|
+
return Array.isArray(artifacts) ? [...artifacts].sort((left, right) => left.localeCompare(right)) : [];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async downloadArtifact(sessionId, filename) {
|
|
442
|
+
if (!this.artifactStore) {
|
|
443
|
+
throw new Error("Artifact store is not available for this transport.");
|
|
444
|
+
}
|
|
445
|
+
return this.artifactStore.downloadArtifact(sessionId, filename);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async uploadArtifactFromPath(sessionId, filePath) {
|
|
449
|
+
if (!this.artifactStore) {
|
|
450
|
+
throw new Error("Artifact store is not available for this transport.");
|
|
451
|
+
}
|
|
452
|
+
const resolvedPath = path.resolve(expandUserPath(filePath));
|
|
453
|
+
if (!resolvedPath) {
|
|
454
|
+
throw new Error("File path cannot be empty.");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const stat = await fs.promises.stat(resolvedPath).catch(() => null);
|
|
458
|
+
if (!stat) {
|
|
459
|
+
throw new Error(`File not found: ${filePath}`);
|
|
460
|
+
}
|
|
461
|
+
if (!stat.isFile()) {
|
|
462
|
+
throw new Error(`Not a file: ${filePath}`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const filename = path.basename(resolvedPath);
|
|
466
|
+
const content = await fs.promises.readFile(resolvedPath, "utf8");
|
|
467
|
+
const contentType = guessArtifactContentType(filename);
|
|
468
|
+
await this.artifactStore.uploadArtifact(sessionId, filename, content, contentType);
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
sessionId,
|
|
472
|
+
filename,
|
|
473
|
+
resolvedPath,
|
|
474
|
+
sizeBytes: Buffer.byteLength(content, "utf8"),
|
|
475
|
+
contentType,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
getArtifactExportDirectory() {
|
|
480
|
+
return EXPORTS_DIR;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async saveArtifactDownload(sessionId, filename) {
|
|
484
|
+
if (!this.artifactStore) {
|
|
485
|
+
throw new Error("Artifact store is not available for this transport.");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const content = await this.artifactStore.downloadArtifact(sessionId, filename);
|
|
489
|
+
const sessionDir = path.join(EXPORTS_DIR, String(sessionId || "").slice(0, 8));
|
|
490
|
+
const localPath = path.join(sessionDir, sanitizeArtifactFilename(filename));
|
|
491
|
+
await fs.promises.mkdir(sessionDir, { recursive: true });
|
|
492
|
+
await fs.promises.writeFile(localPath, content, "utf8");
|
|
493
|
+
return {
|
|
494
|
+
localPath,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async openPathInDefaultApp(targetPath) {
|
|
499
|
+
const resolvedPath = path.resolve(expandUserPath(targetPath));
|
|
500
|
+
if (!resolvedPath) {
|
|
501
|
+
throw new Error("File path cannot be empty.");
|
|
502
|
+
}
|
|
503
|
+
const stat = await fs.promises.stat(resolvedPath).catch(() => null);
|
|
504
|
+
if (!stat || !stat.isFile()) {
|
|
505
|
+
throw new Error(`File not found: ${targetPath}`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (process.platform === "darwin") {
|
|
509
|
+
await spawnDetached("open", [resolvedPath]);
|
|
510
|
+
} else if (process.platform === "win32") {
|
|
511
|
+
await spawnDetached("cmd", ["/c", "start", "", resolvedPath]);
|
|
512
|
+
} else {
|
|
513
|
+
await spawnDetached("xdg-open", [resolvedPath]);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return { localPath: resolvedPath };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
getModelsByProvider() {
|
|
520
|
+
return this.mgmt.getModelsByProvider();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
getDefaultModel() {
|
|
524
|
+
return this.mgmt.getDefaultModel();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async getSessionEvents(sessionId, afterSeq, limit) {
|
|
528
|
+
return this.mgmt.getSessionEvents(sessionId, afterSeq, limit);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async getSessionEventsBefore(sessionId, beforeSeq, limit) {
|
|
532
|
+
if (typeof this.mgmt.getSessionEventsBefore !== "function") return [];
|
|
533
|
+
return this.mgmt.getSessionEventsBefore(sessionId, beforeSeq, limit);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
emitLogEntry(entry) {
|
|
537
|
+
for (const handler of this.logSubscribers) {
|
|
538
|
+
try {
|
|
539
|
+
handler(entry);
|
|
540
|
+
} catch {}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
scheduleLogRestart() {
|
|
545
|
+
if (this.logRestartTimer || this.logSubscribers.size === 0) return;
|
|
546
|
+
this.logRestartTimer = setTimeout(() => {
|
|
547
|
+
this.logRestartTimer = null;
|
|
548
|
+
if (this.logSubscribers.size > 0) {
|
|
549
|
+
this.startLogProcess();
|
|
550
|
+
}
|
|
551
|
+
}, 5000);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
startLogProcess() {
|
|
555
|
+
const config = this.getLogConfig();
|
|
556
|
+
if (!config.available || this.logProc) return;
|
|
557
|
+
|
|
558
|
+
const k8sContext = process.env.K8S_CONTEXT || "";
|
|
559
|
+
const k8sNamespace = process.env.K8S_NAMESPACE || "copilot-runtime";
|
|
560
|
+
const k8sPodLabel = process.env.K8S_POD_LABEL || "app.kubernetes.io/component=worker";
|
|
561
|
+
const k8sCtxArgs = k8sContext ? ["--context", k8sContext] : [];
|
|
562
|
+
this.logBuffer = "";
|
|
563
|
+
this.logProc = spawn("kubectl", [
|
|
564
|
+
...k8sCtxArgs,
|
|
565
|
+
"logs",
|
|
566
|
+
"--follow=true",
|
|
567
|
+
"-n", k8sNamespace,
|
|
568
|
+
"-l", k8sPodLabel,
|
|
569
|
+
"--prefix",
|
|
570
|
+
"--tail=500",
|
|
571
|
+
"--max-log-requests=20",
|
|
572
|
+
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
573
|
+
|
|
574
|
+
this.logProc.stdout.on("data", (chunk) => {
|
|
575
|
+
this.logBuffer += chunk.toString();
|
|
576
|
+
const lines = this.logBuffer.split("\n");
|
|
577
|
+
this.logBuffer = lines.pop() || "";
|
|
578
|
+
for (const line of lines) {
|
|
579
|
+
if (!line.trim()) continue;
|
|
580
|
+
this.logEntryCounter += 1;
|
|
581
|
+
this.emitLogEntry(buildLogEntry(line, this.logEntryCounter));
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
this.logProc.stderr.on("data", (chunk) => {
|
|
586
|
+
const text = stripAnsi(chunk.toString()).trim();
|
|
587
|
+
if (!text) return;
|
|
588
|
+
this.logEntryCounter += 1;
|
|
589
|
+
this.emitLogEntry({
|
|
590
|
+
id: `log:${Date.now()}:${this.logEntryCounter}`,
|
|
591
|
+
time: extractLogTime(text),
|
|
592
|
+
podName: "kubectl",
|
|
593
|
+
level: "warn",
|
|
594
|
+
orchId: null,
|
|
595
|
+
category: "log",
|
|
596
|
+
rawLine: trimLogText(text),
|
|
597
|
+
message: trimLogText(text),
|
|
598
|
+
prettyMessage: trimLogText(text),
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
this.logProc.on("error", (error) => {
|
|
603
|
+
this.logEntryCounter += 1;
|
|
604
|
+
this.emitLogEntry({
|
|
605
|
+
id: `log:${Date.now()}:${this.logEntryCounter}`,
|
|
606
|
+
time: extractLogTime(""),
|
|
607
|
+
podName: "kubectl",
|
|
608
|
+
level: "error",
|
|
609
|
+
orchId: null,
|
|
610
|
+
category: "log",
|
|
611
|
+
rawLine: trimLogText(`kubectl error: ${error.message}`),
|
|
612
|
+
message: trimLogText(`kubectl error: ${error.message}`),
|
|
613
|
+
prettyMessage: trimLogText(`kubectl error: ${error.message}`),
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
this.logProc.on("exit", (code, signal) => {
|
|
618
|
+
this.logProc = null;
|
|
619
|
+
this.logEntryCounter += 1;
|
|
620
|
+
this.emitLogEntry({
|
|
621
|
+
id: `log:${Date.now()}:${this.logEntryCounter}`,
|
|
622
|
+
time: extractLogTime(""),
|
|
623
|
+
podName: "kubectl",
|
|
624
|
+
level: "warn",
|
|
625
|
+
orchId: null,
|
|
626
|
+
category: "log",
|
|
627
|
+
rawLine: trimLogText(`kubectl exited (code=${code} signal=${signal})`),
|
|
628
|
+
message: trimLogText(`kubectl exited (code=${code} signal=${signal})`),
|
|
629
|
+
prettyMessage: trimLogText(`kubectl exited (code=${code} signal=${signal})`),
|
|
630
|
+
});
|
|
631
|
+
this.scheduleLogRestart();
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
startLogTail(handler) {
|
|
636
|
+
if (typeof handler === "function") {
|
|
637
|
+
this.logSubscribers.add(handler);
|
|
638
|
+
}
|
|
639
|
+
this.startLogProcess();
|
|
640
|
+
|
|
641
|
+
return () => {
|
|
642
|
+
if (typeof handler === "function") {
|
|
643
|
+
this.logSubscribers.delete(handler);
|
|
644
|
+
}
|
|
645
|
+
if (this.logSubscribers.size === 0) {
|
|
646
|
+
this.stopLogTail().catch(() => {});
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async stopLogTail() {
|
|
652
|
+
if (this.logRestartTimer) {
|
|
653
|
+
clearTimeout(this.logRestartTimer);
|
|
654
|
+
this.logRestartTimer = null;
|
|
655
|
+
}
|
|
656
|
+
if (this.logProc) {
|
|
657
|
+
try {
|
|
658
|
+
this.logProc.kill("SIGKILL");
|
|
659
|
+
} catch {}
|
|
660
|
+
this.logProc = null;
|
|
661
|
+
}
|
|
662
|
+
this.logBuffer = "";
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
subscribeSession(sessionId, handler) {
|
|
666
|
+
let unsubscribe = () => {};
|
|
667
|
+
let active = true;
|
|
668
|
+
this.getSessionHandle(sessionId)
|
|
669
|
+
.then((session) => {
|
|
670
|
+
if (!active) return;
|
|
671
|
+
unsubscribe = session.on((event) => handler(event));
|
|
672
|
+
})
|
|
673
|
+
.catch(() => {});
|
|
674
|
+
|
|
675
|
+
return () => {
|
|
676
|
+
active = false;
|
|
677
|
+
unsubscribe();
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async getSessionHandle(sessionId) {
|
|
682
|
+
if (this.sessionHandles.has(sessionId)) {
|
|
683
|
+
return this.sessionHandles.get(sessionId);
|
|
684
|
+
}
|
|
685
|
+
const session = await this.client.resumeSession(sessionId);
|
|
686
|
+
this.sessionHandles.set(sessionId, session);
|
|
687
|
+
return session;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function createArtifactStore() {
|
|
692
|
+
const blobConnectionString = (process.env.AZURE_STORAGE_CONNECTION_STRING || "").trim();
|
|
693
|
+
const blobContainer = (process.env.AZURE_STORAGE_CONTAINER || "copilot-sessions").trim() || "copilot-sessions";
|
|
694
|
+
const sessionStateDir = (process.env.SESSION_STATE_DIR || "").trim() || undefined;
|
|
695
|
+
const artifactDir = (process.env.ARTIFACT_DIR || "").trim() || undefined;
|
|
696
|
+
|
|
697
|
+
if (blobConnectionString) {
|
|
698
|
+
return new SessionBlobStore(blobConnectionString, blobContainer, sessionStateDir);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return new FilesystemArtifactStore(artifactDir);
|
|
702
|
+
}
|