@agentsky/sky 2.10.0 → 2.11.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.
@@ -85,6 +85,7 @@ declare const agentAddonsSchema: z.ZodObject<{
85
85
  "happyhorse.t2v": "happyhorse.t2v";
86
86
  "minimax.h3": "minimax.h3";
87
87
  "fish-audio.transcribe": "fish-audio.transcribe";
88
+ "code-review": "code-review";
88
89
  "exa.answer": "exa.answer";
89
90
  "googlemap.geocode": "googlemap.geocode";
90
91
  "googlemap.places": "googlemap.places";
@@ -224,6 +225,7 @@ declare const createProductAgentSchema: z.ZodObject<{
224
225
  "happyhorse.t2v": "happyhorse.t2v";
225
226
  "minimax.h3": "minimax.h3";
226
227
  "fish-audio.transcribe": "fish-audio.transcribe";
228
+ "code-review": "code-review";
227
229
  "exa.answer": "exa.answer";
228
230
  "googlemap.geocode": "googlemap.geocode";
229
231
  "googlemap.places": "googlemap.places";
@@ -363,6 +365,7 @@ declare const prepareProductSessionSchema: z.ZodObject<{
363
365
  "happyhorse.t2v": "happyhorse.t2v";
364
366
  "minimax.h3": "minimax.h3";
365
367
  "fish-audio.transcribe": "fish-audio.transcribe";
368
+ "code-review": "code-review";
366
369
  "exa.answer": "exa.answer";
367
370
  "googlemap.geocode": "googlemap.geocode";
368
371
  "googlemap.places": "googlemap.places";
package/dist/products.js CHANGED
@@ -8,7 +8,8 @@ import {
8
8
  skillsArraySchema,
9
9
  systemMessageEventSchema,
10
10
  userMessageEventSchema
11
- } from "./chunk-5QRKU3BR.js";
11
+ } from "./chunk-WC7GJSAJ.js";
12
+ import "./chunk-MLKGABMK.js";
12
13
 
13
14
  // ../../packages/spec/src/compatibility.ts
14
15
  var capabilityInputsSchema = capabilitiesSchema;
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  external_exports,
3
3
  messagePartSchema
4
- } from "./chunk-5QRKU3BR.js";
4
+ } from "./chunk-WC7GJSAJ.js";
5
+ import "./chunk-MLKGABMK.js";
5
6
 
6
7
  // ../../packages/chat/src/stream.ts
7
8
  async function* parseSseStream(body) {
@@ -0,0 +1,367 @@
1
+ import {
2
+ USER_AGENT,
3
+ configPath
4
+ } from "./chunk-RWPPIEHH.js";
5
+ import {
6
+ TRACE_CONTENT_SCOPES,
7
+ redactTraceText
8
+ } from "./chunk-WC7GJSAJ.js";
9
+ import "./chunk-MLKGABMK.js";
10
+
11
+ // src/trace.ts
12
+ import { randomBytes } from "crypto";
13
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
14
+ import { basename, dirname, join, relative, resolve, sep } from "path";
15
+ var SPOOL_MAX_BYTES = 32 * 1024 * 1024;
16
+ var RETRY_WINDOW_MS = 24 * 60 * 60 * 1e3;
17
+ var BATCH_MAX = 256;
18
+ function traceDir() {
19
+ return process.env.SKY_TRACE_DIR ?? join(dirname(configPath()), "trace");
20
+ }
21
+ var statePath = (dir) => join(dir, "current.json");
22
+ var spoolPath = (dir) => join(dir, "spool.jsonl");
23
+ var gapPath = (dir) => join(dir, "gaps.json");
24
+ var lockPath = (dir) => join(dir, "spool.lock");
25
+ var LOCK_WAIT_MS = 3e3;
26
+ var LOCK_STALE_MS = 3e4;
27
+ function withLock(dir, fn) {
28
+ mkdirSync(dir, { recursive: true, mode: 448 });
29
+ const deadline = Date.now() + LOCK_WAIT_MS;
30
+ for (; ; ) {
31
+ try {
32
+ mkdirSync(lockPath(dir));
33
+ break;
34
+ } catch {
35
+ const age = existsSync(lockPath(dir)) ? Date.now() - statSync(lockPath(dir)).mtimeMs : 0;
36
+ if (age > LOCK_STALE_MS) rmSync(lockPath(dir), { recursive: true, force: true });
37
+ else if (Date.now() > deadline) throw new Error("the trace spool is busy");
38
+ else Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
39
+ }
40
+ }
41
+ try {
42
+ return fn();
43
+ } finally {
44
+ rmSync(lockPath(dir), { recursive: true, force: true });
45
+ }
46
+ }
47
+ function readState(dir = traceDir()) {
48
+ try {
49
+ return JSON.parse(readFileSync(statePath(dir), "utf8"));
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ function writeState(state, dir = traceDir()) {
55
+ mkdirSync(dir, { recursive: true, mode: 448 });
56
+ const tmp = `${statePath(dir)}.${process.pid}.tmp`;
57
+ writeFileSync(tmp, JSON.stringify(state), { mode: 384 });
58
+ renameSync(tmp, statePath(dir));
59
+ }
60
+ function clearState(dir = traceDir()) {
61
+ rmSync(statePath(dir), { force: true });
62
+ }
63
+ function stamp(state, events, now = /* @__PURE__ */ new Date()) {
64
+ return events.map((event) => {
65
+ const sequence = state.next_sequence++;
66
+ return { event_id: `${sequence}-${randomBytes(4).toString("hex")}`, sequence, source_time: now.toISOString(), coverage: "metadata_only", ...event };
67
+ });
68
+ }
69
+ function readGaps(dir) {
70
+ try {
71
+ return JSON.parse(readFileSync(gapPath(dir), "utf8"));
72
+ } catch {
73
+ return { dropped_full: 0, dropped_expired: 0, refused: 0 };
74
+ }
75
+ }
76
+ function addGap(dir, key, count) {
77
+ if (!count) return;
78
+ const gaps = readGaps(dir);
79
+ gaps[key] += count;
80
+ writeFileSync(gapPath(dir), JSON.stringify(gaps), { mode: 384 });
81
+ }
82
+ function append(dir, events, now, maxBytes) {
83
+ const size = existsSync(spoolPath(dir)) ? statSync(spoolPath(dir)).size : 0;
84
+ const lines = events.map((event) => `${JSON.stringify({ spooled_at: now, event })}
85
+ `);
86
+ const bytes = lines.reduce((sum, line) => sum + Buffer.byteLength(line), 0);
87
+ if (size + bytes > maxBytes) {
88
+ addGap(dir, "dropped_full", events.length);
89
+ return 0;
90
+ }
91
+ appendFileSync(spoolPath(dir), lines.join(""), { mode: 384 });
92
+ return events.length;
93
+ }
94
+ function record(build, { dir = traceDir(), now = /* @__PURE__ */ new Date(), maxBytes = SPOOL_MAX_BYTES } = {}) {
95
+ return withLock(dir, () => {
96
+ const state = readState(dir);
97
+ if (!state) return 0;
98
+ const events = stamp(state, build(state), now);
99
+ if (!events.length) return 0;
100
+ writeState(state, dir);
101
+ return append(dir, events, now.getTime(), maxBytes);
102
+ });
103
+ }
104
+ function readSpool(dir) {
105
+ if (!existsSync(spoolPath(dir))) return [];
106
+ return readFileSync(spoolPath(dir), "utf8").split("\n").filter(Boolean).flatMap((line) => {
107
+ try {
108
+ return [JSON.parse(line)];
109
+ } catch {
110
+ return [];
111
+ }
112
+ });
113
+ }
114
+ function settle(dir, done, now) {
115
+ withLock(dir, () => {
116
+ const keep = readSpool(dir).filter((entry) => !done.has(entry.event.event_id) && now - entry.spooled_at < RETRY_WINDOW_MS);
117
+ const tmp = `${spoolPath(dir)}.${process.pid}.tmp`;
118
+ writeFileSync(tmp, keep.map((entry) => `${JSON.stringify(entry)}
119
+ `).join(""), { mode: 384 });
120
+ renameSync(tmp, spoolPath(dir));
121
+ });
122
+ }
123
+ function grantSender(state, timeoutMs = 1e4) {
124
+ return async (events) => {
125
+ const res = await fetch(`${state.api_base.replace(/\/$/, "")}/traces/${state.trace_id}/events`, {
126
+ method: "POST",
127
+ headers: { authorization: `Bearer ${state.token}`, "content-type": "application/json", "user-agent": USER_AGENT },
128
+ body: JSON.stringify({ events }),
129
+ signal: AbortSignal.timeout(timeoutMs)
130
+ });
131
+ if (res.ok) return (await res.json()).results;
132
+ if (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {
133
+ const code = await res.json().then((body) => body.error?.code ?? `http_${res.status}`, () => `http_${res.status}`);
134
+ throw new FinalRefusal(code, res.status);
135
+ }
136
+ throw new Error(`trace upload answered ${res.status}`);
137
+ };
138
+ }
139
+ var FinalRefusal = class extends Error {
140
+ constructor(code, status) {
141
+ super(`trace upload refused: ${code}`);
142
+ this.code = code;
143
+ this.status = status;
144
+ }
145
+ code;
146
+ status;
147
+ };
148
+ async function flush(send, { dir = traceDir(), now = Date.now(), attempts = 1, backoffMs = 500, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
149
+ const all = readSpool(dir);
150
+ const fresh = all.filter((entry) => now - entry.spooled_at < RETRY_WINDOW_MS);
151
+ const expired = all.length - fresh.length;
152
+ addGap(dir, "dropped_expired", expired);
153
+ let pending = fresh;
154
+ let sent = 0;
155
+ let refused = 0;
156
+ let stopped;
157
+ const done = /* @__PURE__ */ new Set();
158
+ for (let attempt = 0; attempt < attempts && pending.length && !stopped; attempt++) {
159
+ if (attempt) await sleep(backoffMs * 2 ** (attempt - 1));
160
+ while (pending.length) {
161
+ const batch = pending.slice(0, BATCH_MAX);
162
+ let results;
163
+ try {
164
+ results = await send(batch.map((entry) => entry.event));
165
+ } catch (error) {
166
+ if (error instanceof FinalRefusal) stopped = error.code;
167
+ break;
168
+ }
169
+ for (const result of results) done.add(result.event_id);
170
+ refused += results.filter((result) => result.status === "refused").length;
171
+ sent += results.filter((result) => result.status !== "refused").length;
172
+ const settledNow = new Set(results.map((result) => result.event_id));
173
+ pending = [...batch.filter((entry) => !settledNow.has(entry.event.event_id)), ...pending.slice(BATCH_MAX)];
174
+ settle(dir, done, now);
175
+ if (settledNow.size === 0) break;
176
+ }
177
+ }
178
+ if (expired && !done.size) settle(dir, done, now);
179
+ addGap(dir, "refused", refused);
180
+ return { sent, refused, expired, pending: pending.length, ...stopped ? { stopped } : {} };
181
+ }
182
+ function spoolStatus(dir = traceDir()) {
183
+ return { pending: readSpool(dir).length, ...readGaps(dir) };
184
+ }
185
+ var SESSION_REF = /\bsess_[A-Za-z0-9]{8,48}\b/g;
186
+ var startsCloudWork = (input) => Boolean(input.tool_name?.startsWith("mcp__agentsky")) || input.tool_name === "Bash" && /(^|[\s;&|(])(sky|npx\s+(-y\s+)?@agentsky\/sky)\s/.test(String(input.tool_input?.command ?? ""));
187
+ function eventsForClaudeHook(input, state) {
188
+ if (!input.session_id) return [];
189
+ const span = `claude-code:${input.session_id}`;
190
+ const tool = input.tool_name ?? null;
191
+ const useId = input.tool_use_id ?? null;
192
+ switch (input.hook_event_name) {
193
+ case "SessionStart":
194
+ return [{ span_id: span, parent_span_id: state.root_span ?? null, kind: "start", name: "claude code session", attributes: { start: input.source ?? null } }];
195
+ case "UserPromptSubmit":
196
+ return [{ span_id: span, kind: "event", name: "turn.start" }];
197
+ case "Stop":
198
+ return [{ span_id: span, kind: "event", name: "turn.end" }];
199
+ case "SessionEnd":
200
+ return [{ span_id: span, kind: "end", name: "claude code session", outcome: "completed", attributes: { reason: input.reason ?? null } }];
201
+ case "PreToolUse":
202
+ if (tool === "Task" && useId) return [{ span_id: `claude-code:${useId}`, parent_span_id: span, kind: "start", name: "subagent" }];
203
+ return [{ span_id: span, kind: "event", name: "tool.start", attributes: { tool, tool_use_id: useId } }];
204
+ case "PostToolUse": {
205
+ if (tool === "Task" && useId) return [{ span_id: `claude-code:${useId}`, kind: "end", name: "subagent", outcome: "completed" }];
206
+ const events = [{ span_id: span, kind: "event", name: "tool.end", attributes: { tool, tool_use_id: useId } }];
207
+ if (startsCloudWork(input)) {
208
+ const named = new Set(JSON.stringify(input.tool_response ?? "").match(SESSION_REF) ?? []);
209
+ for (const session of named) {
210
+ events.push({ span_id: `cloud:${session}`, parent_span_id: span, kind: "start", name: "cloud session", session_id: session, attributes: { via: tool } });
211
+ }
212
+ }
213
+ return events;
214
+ }
215
+ default:
216
+ return [];
217
+ }
218
+ }
219
+ function claudeHooksConfig(command = "sky trace hook") {
220
+ const entry = [{ hooks: [{ type: "command", command, timeout: 10 }] }];
221
+ const matched = [{ matcher: "*", hooks: [{ type: "command", command, timeout: 10 }] }];
222
+ return {
223
+ SessionStart: entry,
224
+ UserPromptSubmit: entry,
225
+ PreToolUse: matched,
226
+ PostToolUse: matched,
227
+ Stop: entry,
228
+ SessionEnd: entry
229
+ };
230
+ }
231
+ function installClaudeHooks(settingsPath, command = "sky trace hook") {
232
+ const settings = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, "utf8")) : {};
233
+ const hooks = settings.hooks ?? {};
234
+ for (const [event, entries] of Object.entries(claudeHooksConfig(command))) {
235
+ const existing = (hooks[event] ?? []).filter((group) => !group.hooks?.some((hook) => hook.command === command));
236
+ hooks[event] = [...existing, ...entries];
237
+ }
238
+ mkdirSync(dirname(settingsPath), { recursive: true });
239
+ writeFileSync(settingsPath, `${JSON.stringify({ ...settings, hooks }, null, 2)}
240
+ `);
241
+ return settingsPath;
242
+ }
243
+ var FILE_READ_MAX_BYTES = 1024 * 1024;
244
+ var FILE_TOOLS = /* @__PURE__ */ new Set(["Read", "Write", "Edit", "MultiEdit", "NotebookEdit"]);
245
+ var CONTENT_ESCAPED_MAX_BYTES = 56 * 1024;
246
+ var NEVER_READ = [
247
+ /(^|\/)\.env(\.|$)/,
248
+ /(^|\/)\.(ssh|aws|gnupg|docker|kube|gcloud|azure|password-store|git)\//,
249
+ /(^|\/)\.(netrc|npmrc|pypirc|pgpass|git-credentials)$/,
250
+ /\.(pem|key|p12|pfx|keystore|jks|kdbx)$/i,
251
+ /(^|\/)id_(rsa|dsa|ecdsa|ed25519)/,
252
+ /(^|\/)(credentials|secrets?)(\.[a-z]+)?$/i,
253
+ /cookies/i,
254
+ /(^|\/)\.(claude|codex|gemini|cursor)\//,
255
+ /Library\/Keychains\//
256
+ ];
257
+ function parseContentScopes(value) {
258
+ if (!value) return [];
259
+ const scopes = [...new Set(value.split(",").map((part) => part.trim()).filter(Boolean))];
260
+ for (const scope of scopes) {
261
+ if (!TRACE_CONTENT_SCOPES.includes(scope)) throw new Error(`unknown capture scope ${scope} (use ${TRACE_CONTENT_SCOPES.join(", ")})`);
262
+ }
263
+ return scopes;
264
+ }
265
+ function realPath(path) {
266
+ try {
267
+ return realpathSync(path);
268
+ } catch {
269
+ return resolve(path);
270
+ }
271
+ }
272
+ function fileAllowed(path, state) {
273
+ const real = realPath(path);
274
+ if (NEVER_READ.some((pattern) => pattern.test(real.split(sep).join("/")))) return false;
275
+ if (state.files?.includes(real)) return true;
276
+ return (state.dirs ?? []).some((dir) => {
277
+ const rel = relative(dir, real);
278
+ return rel !== "" && !rel.startsWith("..") && !rel.startsWith(sep);
279
+ });
280
+ }
281
+ function contentEvent(span, name, scope, text, extra = {}) {
282
+ try {
283
+ let safe = redactTraceText(text);
284
+ const escaped = Buffer.byteLength(JSON.stringify(safe.text));
285
+ if (escaped > CONTENT_ESCAPED_MAX_BYTES) {
286
+ const fitted = redactTraceText(text, Math.floor(Buffer.byteLength(safe.text) * (CONTENT_ESCAPED_MAX_BYTES / escaped)));
287
+ safe = { ...fitted, truncated: true };
288
+ }
289
+ return {
290
+ span_id: span,
291
+ kind: "event",
292
+ name,
293
+ coverage: safe.redactions ? "redacted" : safe.truncated ? "partial" : "complete",
294
+ content: { scope, text: safe.text, ...extra, ...safe.truncated ? { truncated: true } : {}, ...safe.redactions ? { redactions: safe.redactions } : {} }
295
+ };
296
+ } catch {
297
+ return { span_id: span, kind: "event", name, coverage: "metadata_only", attributes: { content_omitted: "redaction_failed" } };
298
+ }
299
+ }
300
+ function fileEvent(span, path, state) {
301
+ if (!state.content_scopes?.includes("files") || !fileAllowed(path, state)) return null;
302
+ try {
303
+ const real = realPath(path);
304
+ if (!statSync(real).isFile()) return null;
305
+ const bytes = readFileSync(real).subarray(0, FILE_READ_MAX_BYTES);
306
+ if (bytes.includes(0)) return null;
307
+ return contentEvent(span, "file", "files", bytes.toString("utf8"), { path: real });
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+ function contentForClaudeHook(input, state) {
313
+ const scopes = state.content_scopes ?? [];
314
+ if (!input.session_id || !scopes.length) return [];
315
+ const span = `claude-code:${input.session_id}`;
316
+ if (input.hook_event_name === "UserPromptSubmit") {
317
+ return scopes.includes("prompts") && input.prompt ? [contentEvent(span, "prompt", "prompts", input.prompt)] : [];
318
+ }
319
+ if (input.hook_event_name !== "PostToolUse") return [];
320
+ const events = [];
321
+ if (scopes.includes("tool_output") && input.tool_response !== void 0 && input.tool_name !== "Task") {
322
+ const text = typeof input.tool_response === "string" ? input.tool_response : JSON.stringify(input.tool_response);
323
+ const event = contentEvent(span, "tool.output", "tool_output", text);
324
+ events.push({ ...event, attributes: { ...event.attributes, tool: input.tool_name ?? null, tool_use_id: input.tool_use_id ?? null } });
325
+ }
326
+ const touched = input.tool_input;
327
+ const path = typeof touched?.file_path === "string" ? touched.file_path : typeof touched?.notebook_path === "string" ? touched.notebook_path : null;
328
+ if (path && FILE_TOOLS.has(input.tool_name ?? "")) {
329
+ const event = fileEvent(span, path, state);
330
+ if (event) events.push(event);
331
+ }
332
+ return events;
333
+ }
334
+ function chosenPaths(files, dirs) {
335
+ const check = (path, kind) => {
336
+ const real = realPath(path);
337
+ if (!existsSync(real)) throw new Error(`${path} does not exist`);
338
+ if (kind === "file" ? !statSync(real).isFile() : !statSync(real).isDirectory()) throw new Error(`${path} is not a ${kind}`);
339
+ if (kind === "file" && !fileAllowed(real, { files: [real] })) throw new Error(`${basename(path)} looks like a credential or agent-state file and is never captured`);
340
+ return real;
341
+ };
342
+ return { files: files.map((path) => check(path, "file")), dirs: dirs.map((path) => check(path, "folder")) };
343
+ }
344
+ export {
345
+ BATCH_MAX,
346
+ FinalRefusal,
347
+ RETRY_WINDOW_MS,
348
+ SPOOL_MAX_BYTES,
349
+ chosenPaths,
350
+ claudeHooksConfig,
351
+ clearState,
352
+ contentEvent,
353
+ contentForClaudeHook,
354
+ eventsForClaudeHook,
355
+ fileAllowed,
356
+ fileEvent,
357
+ flush,
358
+ grantSender,
359
+ installClaudeHooks,
360
+ parseContentScopes,
361
+ readState,
362
+ record,
363
+ spoolStatus,
364
+ stamp,
365
+ traceDir,
366
+ writeState
367
+ };
@@ -0,0 +1,190 @@
1
+ import "./chunk-MLKGABMK.js";
2
+
3
+ // src/worker.ts
4
+ import { mkdirSync, writeFileSync, rmSync } from "fs";
5
+ import { tmpdir } from "os";
6
+ import { join } from "path";
7
+ import { randomBytes } from "crypto";
8
+ var RealDockerRunner = class {
9
+ async run({ image, envFile, port, signal }) {
10
+ const { spawn } = await import("child_process");
11
+ return new Promise((resolve, reject) => {
12
+ const child = spawn(
13
+ "docker",
14
+ [
15
+ "run",
16
+ "--rm",
17
+ "--pull=always",
18
+ `--env-file=${envFile}`,
19
+ `-p`,
20
+ `127.0.0.1:${port}:8080`,
21
+ image
22
+ ],
23
+ { stdio: "inherit" }
24
+ );
25
+ signal.addEventListener("abort", () => {
26
+ child.kill("SIGTERM");
27
+ });
28
+ child.on("error", reject);
29
+ child.on("exit", (code) => {
30
+ if (code === 0 || signal.aborted) resolve();
31
+ else reject(new Error(`docker exited with code ${code}`));
32
+ });
33
+ });
34
+ }
35
+ };
36
+ var POD_PORT = 18080;
37
+ var HEARTBEAT_INTERVAL_MS = 15e3;
38
+ async function startWorker(opts) {
39
+ const docker = opts.docker ?? new RealDockerRunner();
40
+ const signal = opts.signal ?? new AbortController().signal;
41
+ const wsUrl = opts.apiBase.replace(/^https?:\/\//, (m) => m === "https://" ? "wss://" : "ws://").replace(/\/$/, "");
42
+ const connectUrl = `${wsUrl}/api/v1/worker-pools/${opts.poolId}/connect`;
43
+ return new Promise((resolve, reject) => {
44
+ import("ws").then(({ default: WS }) => {
45
+ const ws = new WS(connectUrl, {
46
+ headers: { authorization: `Bearer ${opts.token}` }
47
+ });
48
+ let currentContainer = null;
49
+ const heartbeatTimer = setInterval(() => {
50
+ if (ws.readyState === WS.OPEN) {
51
+ ws.send(JSON.stringify({ type: "heartbeat" }));
52
+ }
53
+ }, HEARTBEAT_INTERVAL_MS);
54
+ signal.addEventListener("abort", () => {
55
+ clearInterval(heartbeatTimer);
56
+ currentContainer?.abort();
57
+ ws.close();
58
+ resolve();
59
+ });
60
+ ws.on("open", () => {
61
+ ws.send(JSON.stringify({
62
+ type: "hello",
63
+ label: opts.label,
64
+ imageDigest: "unknown",
65
+ capacity: 1
66
+ }));
67
+ console.log(`[sky worker] connected to pool ${opts.poolId}`);
68
+ });
69
+ ws.on("message", async (raw) => {
70
+ let msg;
71
+ try {
72
+ msg = JSON.parse(raw.toString());
73
+ } catch {
74
+ return;
75
+ }
76
+ if (msg.type === "start") {
77
+ await handleStart(msg, ws, WS, docker, currentContainer);
78
+ } else if (msg.type === "stop") {
79
+ currentContainer?.abort();
80
+ currentContainer = null;
81
+ } else if (msg.type === "http") {
82
+ await handleHttp(msg, ws);
83
+ } else if (msg.type === "stream_open") {
84
+ void handleStreamOpen(msg, ws);
85
+ }
86
+ });
87
+ ws.on("close", () => {
88
+ clearInterval(heartbeatTimer);
89
+ currentContainer?.abort();
90
+ resolve();
91
+ });
92
+ ws.on("error", (err) => {
93
+ clearInterval(heartbeatTimer);
94
+ currentContainer?.abort();
95
+ reject(err);
96
+ });
97
+ }).catch(reject);
98
+ });
99
+ async function handleStart(msg, ws, WS, docker2, existingContainer) {
100
+ if (existingContainer) existingContainer.abort();
101
+ const dir = join(tmpdir(), `sky-worker-${randomBytes(6).toString("hex")}`);
102
+ mkdirSync(dir, { mode: 448 });
103
+ const envFile = join(dir, "pod.env");
104
+ const envLines = Object.entries(msg.env).map(([k, v]) => `${k}=${v.replace(/\n/g, "\\n")}`).join("\n");
105
+ writeFileSync(envFile, envLines, { mode: 384 });
106
+ const containerAbort = new AbortController();
107
+ try {
108
+ void docker2.run({
109
+ image: msg.image,
110
+ envFile,
111
+ port: POD_PORT,
112
+ signal: containerAbort.signal
113
+ }).finally(() => {
114
+ try {
115
+ rmSync(dir, { recursive: true });
116
+ } catch {
117
+ }
118
+ });
119
+ } catch (err) {
120
+ try {
121
+ rmSync(dir, { recursive: true });
122
+ } catch {
123
+ }
124
+ console.error("[sky worker] docker run failed:", err);
125
+ }
126
+ }
127
+ async function handleHttp(msg, ws) {
128
+ const url = `http://127.0.0.1:${POD_PORT}${msg.path}`;
129
+ try {
130
+ const response = await fetch(url, {
131
+ method: msg.method,
132
+ headers: msg.headers,
133
+ body: msg.body || void 0,
134
+ signal: AbortSignal.timeout(3e4)
135
+ });
136
+ const body = await response.text();
137
+ const headers = {};
138
+ response.headers.forEach((v, k) => {
139
+ headers[k] = v;
140
+ });
141
+ const resp = {
142
+ type: "http_response",
143
+ id: msg.id,
144
+ status: response.status,
145
+ headers,
146
+ body
147
+ };
148
+ ws.send(JSON.stringify(resp));
149
+ } catch (err) {
150
+ ws.send(JSON.stringify({
151
+ type: "http_response",
152
+ id: msg.id,
153
+ status: 502,
154
+ headers: {},
155
+ body: String(err)
156
+ }));
157
+ }
158
+ }
159
+ async function handleStreamOpen(msg, ws) {
160
+ const url = `http://127.0.0.1:${POD_PORT}${msg.path}`;
161
+ try {
162
+ const response = await fetch(url, {
163
+ headers: { accept: "text/event-stream" },
164
+ signal: AbortSignal.timeout(3e5)
165
+ });
166
+ const reader = response.body?.getReader();
167
+ if (!reader) {
168
+ ws.send(JSON.stringify({ type: "stream_close", id: msg.id }));
169
+ return;
170
+ }
171
+ const decoder = new TextDecoder();
172
+ for (; ; ) {
173
+ const { done, value } = await reader.read();
174
+ if (done) break;
175
+ ws.send(JSON.stringify({
176
+ type: "stream_event",
177
+ id: msg.id,
178
+ data: decoder.decode(value, { stream: true })
179
+ }));
180
+ }
181
+ ws.send(JSON.stringify({ type: "stream_close", id: msg.id }));
182
+ } catch {
183
+ ws.send(JSON.stringify({ type: "stream_close", id: msg.id }));
184
+ }
185
+ }
186
+ }
187
+ export {
188
+ RealDockerRunner,
189
+ startWorker
190
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentsky/sky",
3
- "version": "2.10.0",
4
- "description": "sky — the AgentSky developer CLI: launch and talk to long-lived agents from your terminal.",
3
+ "version": "2.11.0",
4
+ "description": "sky \u2014 the AgentSky developer CLI: launch and talk to long-lived agents from your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -22,7 +22,8 @@
22
22
  "dependencies": {
23
23
  "commander": "^14.0.0",
24
24
  "smol-toml": "^1.4.2",
25
- "zod": "4.5.4"
25
+ "zod": "4.5.4",
26
+ "ws": "^8.18.0"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@agent/chat": "*",