offwatch 0.5.8 → 0.5.10
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/offwatch.js +7 -6
- package/package.json +4 -3
- package/src/__tests__/agent-jwt-env.test.ts +79 -0
- package/src/__tests__/allowed-hostname.test.ts +80 -0
- package/src/__tests__/auth-command-registration.test.ts +16 -0
- package/src/__tests__/board-auth.test.ts +53 -0
- package/src/__tests__/common.test.ts +98 -0
- package/src/__tests__/company-delete.test.ts +95 -0
- package/src/__tests__/company-import-export-e2e.test.ts +502 -0
- package/src/__tests__/company-import-url.test.ts +74 -0
- package/src/__tests__/company-import-zip.test.ts +44 -0
- package/src/__tests__/company.test.ts +599 -0
- package/src/__tests__/context.test.ts +70 -0
- package/src/__tests__/data-dir.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +102 -0
- package/src/__tests__/feedback.test.ts +177 -0
- package/src/__tests__/helpers/embedded-postgres.ts +6 -0
- package/src/__tests__/helpers/zip.ts +87 -0
- package/src/__tests__/home-paths.test.ts +44 -0
- package/src/__tests__/http.test.ts +106 -0
- package/src/__tests__/network-bind.test.ts +62 -0
- package/src/__tests__/onboard.test.ts +166 -0
- package/src/__tests__/routines.test.ts +249 -0
- package/src/__tests__/telemetry.test.ts +117 -0
- package/src/__tests__/worktree-merge-history.test.ts +492 -0
- package/src/__tests__/worktree.test.ts +982 -0
- package/src/adapters/http/format-event.ts +4 -0
- package/src/adapters/http/index.ts +7 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/process/format-event.ts +4 -0
- package/src/adapters/process/index.ts +7 -0
- package/src/adapters/registry.ts +63 -0
- package/src/checks/agent-jwt-secret-check.ts +40 -0
- package/src/checks/config-check.ts +33 -0
- package/src/checks/database-check.ts +59 -0
- package/src/checks/deployment-auth-check.ts +88 -0
- package/src/checks/index.ts +18 -0
- package/src/checks/llm-check.ts +82 -0
- package/src/checks/log-check.ts +30 -0
- package/src/checks/path-resolver.ts +1 -0
- package/src/checks/port-check.ts +24 -0
- package/src/checks/secrets-check.ts +146 -0
- package/src/checks/storage-check.ts +51 -0
- package/src/client/board-auth.ts +282 -0
- package/src/client/command-label.ts +4 -0
- package/src/client/context.ts +175 -0
- package/src/client/http.ts +255 -0
- package/src/commands/allowed-hostname.ts +40 -0
- package/src/commands/auth-bootstrap-ceo.ts +138 -0
- package/src/commands/client/activity.ts +71 -0
- package/src/commands/client/agent.ts +315 -0
- package/src/commands/client/approval.ts +259 -0
- package/src/commands/client/auth.ts +113 -0
- package/src/commands/client/common.ts +221 -0
- package/src/commands/client/company.ts +1578 -0
- package/src/commands/client/context.ts +125 -0
- package/src/commands/client/dashboard.ts +34 -0
- package/src/commands/client/feedback.ts +645 -0
- package/src/commands/client/issue.ts +411 -0
- package/src/commands/client/plugin.ts +374 -0
- package/src/commands/client/zip.ts +129 -0
- package/src/commands/configure.ts +201 -0
- package/src/commands/db-backup.ts +102 -0
- package/src/commands/doctor.ts +203 -0
- package/src/commands/env.ts +411 -0
- package/src/commands/heartbeat-run.ts +344 -0
- package/src/commands/onboard.ts +692 -0
- package/src/commands/routines.ts +352 -0
- package/src/commands/run.ts +216 -0
- package/src/commands/worktree-lib.ts +279 -0
- package/src/commands/worktree-merge-history-lib.ts +764 -0
- package/src/commands/worktree.ts +2876 -0
- package/src/config/data-dir.ts +48 -0
- package/src/config/env.ts +125 -0
- package/src/config/home.ts +80 -0
- package/src/config/hostnames.ts +26 -0
- package/src/config/schema.ts +30 -0
- package/src/config/secrets-key.ts +48 -0
- package/src/config/server-bind.ts +183 -0
- package/src/config/store.ts +120 -0
- package/src/index.ts +182 -0
- package/src/prompts/database.ts +157 -0
- package/src/prompts/llm.ts +43 -0
- package/src/prompts/logging.ts +37 -0
- package/src/prompts/secrets.ts +99 -0
- package/src/prompts/server.ts +221 -0
- package/src/prompts/storage.ts +146 -0
- package/src/telemetry.ts +49 -0
- package/src/utils/banner.ts +24 -0
- package/src/utils/net.ts +18 -0
- package/src/utils/path-resolver.ts +25 -0
- package/src/version.ts +10 -0
- package/lib/downloader.js +0 -112
- package/postinstall.js +0 -23
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import type { Agent, HeartbeatRun, HeartbeatRunEvent, HeartbeatRunStatus } from "@paperclipai/shared";
|
|
4
|
+
import { getCLIAdapter } from "../adapters/index.js";
|
|
5
|
+
import { resolveCommandContext } from "./client/common.js";
|
|
6
|
+
|
|
7
|
+
const HEARTBEAT_SOURCES = ["timer", "assignment", "on_demand", "automation"] as const;
|
|
8
|
+
const HEARTBEAT_TRIGGERS = ["manual", "ping", "callback", "system"] as const;
|
|
9
|
+
const TERMINAL_STATUSES = new Set<HeartbeatRunStatus>(["succeeded", "failed", "cancelled", "timed_out"]);
|
|
10
|
+
const POLL_INTERVAL_MS = 200;
|
|
11
|
+
|
|
12
|
+
type HeartbeatSource = (typeof HEARTBEAT_SOURCES)[number];
|
|
13
|
+
type HeartbeatTrigger = (typeof HEARTBEAT_TRIGGERS)[number];
|
|
14
|
+
type InvokedHeartbeat = HeartbeatRun | { status: "skipped" };
|
|
15
|
+
interface HeartbeatRunEventRecord extends HeartbeatRunEvent {
|
|
16
|
+
type?: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface HeartbeatRunOptions {
|
|
20
|
+
config?: string;
|
|
21
|
+
context?: string;
|
|
22
|
+
profile?: string;
|
|
23
|
+
agentId: string;
|
|
24
|
+
apiBase?: string;
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
source: string;
|
|
27
|
+
trigger: string;
|
|
28
|
+
timeoutMs: string;
|
|
29
|
+
debug?: boolean;
|
|
30
|
+
json?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
35
|
+
? (value as Record<string, unknown>)
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function asErrorText(value: unknown): string {
|
|
40
|
+
if (typeof value === "string") return value;
|
|
41
|
+
const obj = asRecord(value);
|
|
42
|
+
if (!obj) return "";
|
|
43
|
+
const message =
|
|
44
|
+
(typeof obj.message === "string" && obj.message) ||
|
|
45
|
+
(typeof obj.error === "string" && obj.error) ||
|
|
46
|
+
(typeof obj.code === "string" && obj.code) ||
|
|
47
|
+
"";
|
|
48
|
+
if (message) return message;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.stringify(obj);
|
|
51
|
+
} catch {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type AdapterType = string;
|
|
57
|
+
|
|
58
|
+
export async function heartbeatRun(opts: HeartbeatRunOptions): Promise<void> {
|
|
59
|
+
const debug = Boolean(opts.debug);
|
|
60
|
+
const parsedTimeout = Number.parseInt(opts.timeoutMs, 10);
|
|
61
|
+
const timeoutMs = Number.isFinite(parsedTimeout) ? parsedTimeout : 0;
|
|
62
|
+
const source = HEARTBEAT_SOURCES.includes(opts.source as HeartbeatSource)
|
|
63
|
+
? (opts.source as HeartbeatSource)
|
|
64
|
+
: "on_demand";
|
|
65
|
+
const triggerDetail = HEARTBEAT_TRIGGERS.includes(opts.trigger as HeartbeatTrigger)
|
|
66
|
+
? (opts.trigger as HeartbeatTrigger)
|
|
67
|
+
: "manual";
|
|
68
|
+
|
|
69
|
+
const ctx = resolveCommandContext({
|
|
70
|
+
config: opts.config,
|
|
71
|
+
context: opts.context,
|
|
72
|
+
profile: opts.profile,
|
|
73
|
+
apiBase: opts.apiBase,
|
|
74
|
+
apiKey: opts.apiKey,
|
|
75
|
+
json: opts.json,
|
|
76
|
+
});
|
|
77
|
+
const api = ctx.api;
|
|
78
|
+
|
|
79
|
+
const agent = await api.get<Agent>(`/api/agents/${opts.agentId}`);
|
|
80
|
+
if (!agent || typeof agent !== "object" || !agent.id) {
|
|
81
|
+
console.error(pc.red(`Agent not found: ${opts.agentId}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const invokeRes = await api.post<InvokedHeartbeat>(
|
|
86
|
+
`/api/agents/${opts.agentId}/wakeup`,
|
|
87
|
+
{
|
|
88
|
+
source: source,
|
|
89
|
+
triggerDetail: triggerDetail,
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
if (!invokeRes) {
|
|
93
|
+
console.error(pc.red("Failed to invoke heartbeat"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if ((invokeRes as { status?: string }).status === "skipped") {
|
|
97
|
+
console.log(pc.yellow("Heartbeat invocation was skipped"));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const run = invokeRes as HeartbeatRun;
|
|
102
|
+
console.log(pc.cyan(`Invoked heartbeat run ${run.id} for agent ${agent.name} (${agent.id})`));
|
|
103
|
+
|
|
104
|
+
const runId = run.id;
|
|
105
|
+
let activeRunId: string | null = null;
|
|
106
|
+
let lastEventSeq = 0;
|
|
107
|
+
let logOffset = 0;
|
|
108
|
+
let stdoutJsonBuffer = "";
|
|
109
|
+
|
|
110
|
+
const printRawChunk = (stream: "stdout" | "stderr" | "system", chunk: string) => {
|
|
111
|
+
if (stream === "stdout") process.stdout.write(pc.green("[stdout] ") + chunk);
|
|
112
|
+
else if (stream === "stderr") process.stdout.write(pc.red("[stderr] ") + chunk);
|
|
113
|
+
else process.stdout.write(pc.yellow("[system] ") + chunk);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const printAdapterInvoke = (payload: Record<string, unknown>) => {
|
|
117
|
+
const adapterType = typeof payload.adapterType === "string" ? payload.adapterType : "unknown";
|
|
118
|
+
const command = typeof payload.command === "string" ? payload.command : "";
|
|
119
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : "";
|
|
120
|
+
const args =
|
|
121
|
+
Array.isArray(payload.commandArgs) &&
|
|
122
|
+
(payload.commandArgs as unknown[]).every((v) => typeof v === "string")
|
|
123
|
+
? (payload.commandArgs as string[])
|
|
124
|
+
: [];
|
|
125
|
+
const env =
|
|
126
|
+
typeof payload.env === "object" && payload.env !== null && !Array.isArray(payload.env)
|
|
127
|
+
? (payload.env as Record<string, unknown>)
|
|
128
|
+
: null;
|
|
129
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
130
|
+
const context =
|
|
131
|
+
typeof payload.context === "object" && payload.context !== null && !Array.isArray(payload.context)
|
|
132
|
+
? (payload.context as Record<string, unknown>)
|
|
133
|
+
: null;
|
|
134
|
+
|
|
135
|
+
console.log(pc.cyan(`Adapter: ${adapterType}`));
|
|
136
|
+
if (cwd) console.log(pc.cyan(`Working dir: ${cwd}`));
|
|
137
|
+
if (command) {
|
|
138
|
+
const rendered = args.length > 0 ? `${command} ${args.join(" ")}` : command;
|
|
139
|
+
console.log(pc.cyan(`Command: ${rendered}`));
|
|
140
|
+
}
|
|
141
|
+
if (env) {
|
|
142
|
+
console.log(pc.cyan("Env:"));
|
|
143
|
+
console.log(pc.gray(JSON.stringify(env, null, 2)));
|
|
144
|
+
}
|
|
145
|
+
if (context) {
|
|
146
|
+
console.log(pc.cyan("Context:"));
|
|
147
|
+
console.log(pc.gray(JSON.stringify(context, null, 2)));
|
|
148
|
+
}
|
|
149
|
+
if (prompt) {
|
|
150
|
+
console.log(pc.cyan("Prompt:"));
|
|
151
|
+
console.log(prompt);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const adapterType: AdapterType = agent.adapterType ?? "claude_local";
|
|
156
|
+
const cliAdapter = getCLIAdapter(adapterType);
|
|
157
|
+
|
|
158
|
+
const handleStreamChunk = (stream: "stdout" | "stderr" | "system", chunk: string) => {
|
|
159
|
+
if (debug) {
|
|
160
|
+
printRawChunk(stream, chunk);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (stream !== "stdout") {
|
|
165
|
+
printRawChunk(stream, chunk);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const combined = stdoutJsonBuffer + chunk;
|
|
170
|
+
const lines = combined.split(/\r?\n/);
|
|
171
|
+
stdoutJsonBuffer = lines.pop() ?? "";
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
cliAdapter.formatStdoutEvent(line, debug);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const handleEvent = (event: HeartbeatRunEventRecord) => {
|
|
178
|
+
const payload = normalizePayload(event.payload);
|
|
179
|
+
if (event.runId !== runId) return;
|
|
180
|
+
const eventType = typeof event.eventType === "string"
|
|
181
|
+
? event.eventType
|
|
182
|
+
: typeof event.type === "string"
|
|
183
|
+
? event.type
|
|
184
|
+
: "";
|
|
185
|
+
|
|
186
|
+
if (eventType === "heartbeat.run.status") {
|
|
187
|
+
const status = typeof payload.status === "string" ? payload.status : null;
|
|
188
|
+
if (status) {
|
|
189
|
+
console.log(pc.blue(`[status] ${status}`));
|
|
190
|
+
}
|
|
191
|
+
} else if (eventType === "adapter.invoke") {
|
|
192
|
+
printAdapterInvoke(payload);
|
|
193
|
+
} else if (eventType === "heartbeat.run.log") {
|
|
194
|
+
const stream = typeof payload.stream === "string" ? payload.stream : "system";
|
|
195
|
+
const chunk = typeof payload.chunk === "string" ? payload.chunk : "";
|
|
196
|
+
if (!chunk) return;
|
|
197
|
+
if (stream === "stdout" || stream === "stderr" || stream === "system") {
|
|
198
|
+
handleStreamChunk(stream, chunk);
|
|
199
|
+
}
|
|
200
|
+
} else if (typeof event.message === "string") {
|
|
201
|
+
console.log(pc.gray(`[event] ${eventType || "heartbeat.run.event"}: ${event.message}`));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
lastEventSeq = Math.max(lastEventSeq, event.seq ?? 0);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
activeRunId = runId;
|
|
208
|
+
let finalStatus: string | null = null;
|
|
209
|
+
let finalError: string | null = null;
|
|
210
|
+
let finalRun: HeartbeatRun | null = null;
|
|
211
|
+
|
|
212
|
+
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : null;
|
|
213
|
+
if (!activeRunId) {
|
|
214
|
+
console.error(pc.red("Failed to capture heartbeat run id"));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
while (true) {
|
|
219
|
+
const events = await api.get<HeartbeatRunEvent[]>(
|
|
220
|
+
`/api/heartbeat-runs/${activeRunId}/events?afterSeq=${lastEventSeq}&limit=100`,
|
|
221
|
+
);
|
|
222
|
+
for (const event of Array.isArray(events) ? (events as HeartbeatRunEventRecord[]) : []) {
|
|
223
|
+
handleEvent(event);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const runList = (await api.get<(HeartbeatRun | null)[]>(
|
|
227
|
+
`/api/companies/${agent.companyId}/heartbeat-runs?agentId=${agent.id}`,
|
|
228
|
+
)) || [];
|
|
229
|
+
const currentRun = runList.find((r) => r && r.id === activeRunId) ?? null;
|
|
230
|
+
|
|
231
|
+
if (!currentRun) {
|
|
232
|
+
console.error(pc.red("Heartbeat run disappeared"));
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const currentStatus = currentRun.status as HeartbeatRunStatus | undefined;
|
|
237
|
+
if (currentStatus !== finalStatus && currentStatus) {
|
|
238
|
+
finalStatus = currentStatus;
|
|
239
|
+
console.log(pc.blue(`Status: ${currentStatus}`));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (currentStatus && TERMINAL_STATUSES.has(currentStatus)) {
|
|
243
|
+
finalStatus = currentRun.status;
|
|
244
|
+
finalError = currentRun.error;
|
|
245
|
+
finalRun = currentRun;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (deadline && Date.now() >= deadline) {
|
|
250
|
+
finalError = `CLI timed out after ${timeoutMs}ms`;
|
|
251
|
+
finalStatus = "timed_out";
|
|
252
|
+
console.error(pc.yellow(finalError));
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const logResult = await api.get<{ content: string; nextOffset?: number }>(
|
|
257
|
+
`/api/heartbeat-runs/${activeRunId}/log?offset=${logOffset}&limitBytes=16384`,
|
|
258
|
+
{ ignoreNotFound: true },
|
|
259
|
+
);
|
|
260
|
+
if (logResult && logResult.content) {
|
|
261
|
+
for (const chunk of logResult.content.split(/\r?\n/)) {
|
|
262
|
+
if (!chunk) continue;
|
|
263
|
+
const parsed = safeParseLogLine(chunk);
|
|
264
|
+
if (!parsed) continue;
|
|
265
|
+
handleStreamChunk(parsed.stream, parsed.chunk);
|
|
266
|
+
}
|
|
267
|
+
if (typeof logResult.nextOffset === "number") {
|
|
268
|
+
logOffset = logResult.nextOffset;
|
|
269
|
+
} else if (logResult.content) {
|
|
270
|
+
logOffset += Buffer.byteLength(logResult.content, "utf8");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await delay(POLL_INTERVAL_MS);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (finalStatus) {
|
|
278
|
+
if (!debug && stdoutJsonBuffer.trim()) {
|
|
279
|
+
cliAdapter.formatStdoutEvent(stdoutJsonBuffer, debug);
|
|
280
|
+
stdoutJsonBuffer = "";
|
|
281
|
+
}
|
|
282
|
+
const label = `Run ${activeRunId} completed with status ${finalStatus}`;
|
|
283
|
+
if (finalStatus === "succeeded") {
|
|
284
|
+
console.log(pc.green(label));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log(pc.red(label));
|
|
289
|
+
if (finalError) {
|
|
290
|
+
console.log(pc.red(`Error: ${finalError}`));
|
|
291
|
+
}
|
|
292
|
+
if (finalRun) {
|
|
293
|
+
const resultObj = asRecord(finalRun.resultJson);
|
|
294
|
+
if (resultObj) {
|
|
295
|
+
const subtype = typeof resultObj.subtype === "string" ? resultObj.subtype : "";
|
|
296
|
+
const isError = resultObj.is_error === true;
|
|
297
|
+
const errors = Array.isArray(resultObj.errors) ? resultObj.errors.map(asErrorText).filter(Boolean) : [];
|
|
298
|
+
const resultText = typeof resultObj.result === "string" ? resultObj.result.trim() : "";
|
|
299
|
+
if (subtype || isError || errors.length > 0 || resultText) {
|
|
300
|
+
console.log(pc.red("Claude result details:"));
|
|
301
|
+
if (subtype) console.log(pc.red(` subtype: ${subtype}`));
|
|
302
|
+
if (isError) console.log(pc.red(" is_error: true"));
|
|
303
|
+
if (errors.length > 0) console.log(pc.red(` errors: ${errors.join(" | ")}`));
|
|
304
|
+
if (resultText) console.log(pc.red(` result: ${resultText}`));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const stderrExcerpt = typeof finalRun.stderrExcerpt === "string" ? finalRun.stderrExcerpt.trim() : "";
|
|
309
|
+
const stdoutExcerpt = typeof finalRun.stdoutExcerpt === "string" ? finalRun.stdoutExcerpt.trim() : "";
|
|
310
|
+
if (stderrExcerpt) {
|
|
311
|
+
console.log(pc.red("stderr excerpt:"));
|
|
312
|
+
console.log(stderrExcerpt);
|
|
313
|
+
}
|
|
314
|
+
if (stdoutExcerpt && (debug || !stderrExcerpt)) {
|
|
315
|
+
console.log(pc.gray("stdout excerpt:"));
|
|
316
|
+
console.log(stdoutExcerpt);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
process.exitCode = 1;
|
|
320
|
+
} else {
|
|
321
|
+
process.exitCode = 1;
|
|
322
|
+
console.log(pc.gray("Heartbeat stream ended without terminal status"));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function normalizePayload(payload: unknown): Record<string, unknown> {
|
|
327
|
+
return typeof payload === "object" && payload !== null ? (payload as Record<string, unknown>) : {};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function safeParseLogLine(line: string): { stream: "stdout" | "stderr" | "system"; chunk: string } | null {
|
|
331
|
+
try {
|
|
332
|
+
const parsed = JSON.parse(line) as { stream?: unknown; chunk?: unknown };
|
|
333
|
+
const stream =
|
|
334
|
+
parsed.stream === "stdout" || parsed.stream === "stderr" || parsed.stream === "system"
|
|
335
|
+
? parsed.stream
|
|
336
|
+
: "system";
|
|
337
|
+
const chunk = typeof parsed.chunk === "string" ? parsed.chunk : "";
|
|
338
|
+
|
|
339
|
+
if (!chunk) return null;
|
|
340
|
+
return { stream, chunk };
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|