@agentsky/sky 2.10.0 → 2.12.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/dist/index.js CHANGED
@@ -1,4 +1,26 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ ApiError,
4
+ api,
5
+ apiBytes,
6
+ apiForm,
7
+ downloadFile,
8
+ postEvents,
9
+ resolveSessionRef,
10
+ streamEvents,
11
+ uploadFile
12
+ } from "./chunk-7LUCHBX6.js";
13
+ import {
14
+ CLI_VERSION,
15
+ USER_AGENT,
16
+ apiBase,
17
+ apiToken,
18
+ configPath,
19
+ consoleBase,
20
+ currentUniverse,
21
+ loadConfig,
22
+ saveConfig
23
+ } from "./chunk-RF5JICNX.js";
2
24
  import {
3
25
  AGENT_TYPES,
4
26
  MODEL_SUBSCRIPTION_PLAN_PHRASE,
@@ -8,6 +30,7 @@ import {
8
30
  customDataSchema,
9
31
  defaultLlm,
10
32
  external_exports,
33
+ harnessProfileSchema,
11
34
  isModelEffortSupported,
12
35
  mcpServersFromKept,
13
36
  modelSubscriptionProviderFor,
@@ -16,200 +39,17 @@ import {
16
39
  skillRefSchema,
17
40
  splitMcpServers,
18
41
  supersededBy
19
- } from "./chunk-5QRKU3BR.js";
42
+ } from "./chunk-XXDQKVPS.js";
43
+ import "./chunk-MLKGABMK.js";
20
44
 
21
45
  // src/index.ts
22
- import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
23
- import { basename as basename3, resolve as resolve4 } from "path";
46
+ import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
47
+ import { basename as basename4, resolve as resolve6 } from "path";
24
48
  import { createInterface as createInterface4 } from "readline";
25
49
  import { Command } from "commander";
26
50
 
27
- // src/config.ts
28
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
29
- import { homedir } from "os";
30
- import { dirname, join } from "path";
31
- var DEFAULT_API_BASE = "https://api.agentsky.dev/v1";
32
- var DEFAULT_CONSOLE_BASE = "https://platform.agentsky.dev";
33
- function configPath() {
34
- return join(process.env.SKY_CONFIG_DIR ?? join(homedir(), ".config", "sky"), "config.json");
35
- }
36
- function loadConfig() {
37
- try {
38
- return JSON.parse(readFileSync(configPath(), "utf8"));
39
- } catch {
40
- return {};
41
- }
42
- }
43
- function saveConfig(config) {
44
- const path = configPath();
45
- mkdirSync(dirname(path), { recursive: true });
46
- writeFileSync(path, `${JSON.stringify(config, null, 2)}
47
- `, { mode: 384 });
48
- }
49
- function apiBase() {
50
- return (process.env.SKY_API_URL ?? loadConfig().apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
51
- }
52
- function consoleBase() {
53
- const configured = process.env.SKY_CONSOLE_URL ?? loadConfig().consoleBase;
54
- if (configured) return configured.replace(/\/$/, "");
55
- const api2 = apiBase();
56
- if (api2 === DEFAULT_API_BASE) return DEFAULT_CONSOLE_BASE;
57
- return api2.replace(/\/(?:api\/)?v1$/, "") || api2;
58
- }
59
- function apiToken() {
60
- return process.env.SKY_API_TOKEN ?? loadConfig().token;
61
- }
62
- function currentUniverse() {
63
- return process.env.SKY_WORKSPACE ?? process.env.SKY_UNIVERSE ?? loadConfig().currentUniverse;
64
- }
65
-
66
- // src/version.ts
67
- var CLI_VERSION = "2.10.0";
68
- var USER_AGENT = `sky-cli/${CLI_VERSION} (${process.platform}; node/${process.versions.node})`;
69
-
70
- // src/api.ts
71
- var ApiError = class extends Error {
72
- constructor(status, code, message2) {
73
- super(message2);
74
- this.status = status;
75
- this.code = code;
76
- }
77
- status;
78
- code;
79
- };
80
- function authHeaders() {
81
- const token = apiToken();
82
- if (!token) {
83
- throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
84
- }
85
- const headers = { authorization: `Bearer ${token}` };
86
- const universe2 = currentUniverse();
87
- if (universe2) headers["x-universe"] = universe2;
88
- return headers;
89
- }
90
- async function parseError(res) {
91
- let code = "http_error";
92
- let message2 = `${res.status} ${res.statusText}`;
93
- try {
94
- const body = await res.json();
95
- if (body.error?.message) {
96
- code = body.error.code ?? code;
97
- message2 = body.error.message;
98
- }
99
- } catch {
100
- }
101
- throw new ApiError(res.status, code, message2);
102
- }
103
- async function api(method, path, body, opts = {}) {
104
- const res = await fetch(`${apiBase()}${path}`, {
105
- method,
106
- headers: {
107
- "user-agent": USER_AGENT,
108
- ...opts.auth === false ? {} : authHeaders(),
109
- ...body !== void 0 ? { "content-type": "application/json" } : {},
110
- ...opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : {}
111
- },
112
- body: body !== void 0 ? JSON.stringify(body) : void 0
113
- });
114
- if (!res.ok) await parseError(res);
115
- if (res.status === 204) return void 0;
116
- return await res.json();
117
- }
118
- async function postEvents(sessionId, events, idempotencyKey) {
119
- const res = await fetch(`${apiBase()}/sessions/${encodeURIComponent(sessionId)}/events`, {
120
- method: "POST",
121
- headers: {
122
- "user-agent": USER_AGENT,
123
- ...authHeaders(),
124
- "content-type": "application/json",
125
- ...idempotencyKey ? { "idempotency-key": idempotencyKey } : {}
126
- },
127
- body: JSON.stringify({ events })
128
- });
129
- if (!res.ok) await parseError(res);
130
- return await res.json();
131
- }
132
- async function uploadFile(bytes, filename, mediaType, expiresInSeconds) {
133
- const form = new FormData();
134
- form.append("file", new Blob([new Uint8Array(bytes)], { type: mediaType }), filename);
135
- if (expiresInSeconds) form.append("expires_in_seconds", String(expiresInSeconds));
136
- const res = await fetch(`${apiBase()}/files`, {
137
- method: "POST",
138
- headers: { "user-agent": USER_AGENT, ...authHeaders() },
139
- body: form
140
- });
141
- if (!res.ok) await parseError(res);
142
- return await res.json();
143
- }
144
- async function downloadFile(fileId) {
145
- const res = await fetch(`${apiBase()}/files/${encodeURIComponent(fileId)}/content`, {
146
- headers: { "user-agent": USER_AGENT, ...authHeaders() }
147
- });
148
- if (!res.ok) await parseError(res);
149
- return { bytes: new Uint8Array(await res.arrayBuffer()), mediaType: res.headers.get("content-type") ?? "application/octet-stream" };
150
- }
151
- async function apiBytes(method, path, body) {
152
- const res = await fetch(`${apiBase()}${path}`, {
153
- method,
154
- headers: {
155
- "user-agent": USER_AGENT,
156
- ...authHeaders(),
157
- ...body ? { "content-type": body.mediaType } : {}
158
- },
159
- body: body ? new Uint8Array(body.bytes) : void 0
160
- });
161
- if (!res.ok) await parseError(res);
162
- const bytes = new Uint8Array(await res.arrayBuffer());
163
- return {
164
- bytes,
165
- mediaType: res.headers.get("content-type") ?? "application/octet-stream",
166
- json: () => JSON.parse(new TextDecoder().decode(bytes))
167
- };
168
- }
169
- async function resolveSessionRef(ref) {
170
- if (ref.startsWith("sess-")) return ref;
171
- const { agent: agent2 } = await api(
172
- "GET",
173
- `/agents/${encodeURIComponent(ref)}`
174
- ).catch(async () => {
175
- await api("GET", `/sessions/${encodeURIComponent(ref)}`);
176
- return { agent: { id: ref, defaultSessionId: ref } };
177
- });
178
- if (!agent2.defaultSessionId) {
179
- throw new ApiError(
180
- 404,
181
- "no_sessions",
182
- `agent ${agent2.id} has no sessions \u2014 create one: sky session create ${agent2.id}`
183
- );
184
- }
185
- return agent2.defaultSessionId;
186
- }
187
- async function* streamEvents(sessionId, signal) {
188
- const { parseSseStream } = await import("./src-RYECJN5I.js");
189
- const res = await fetch(
190
- `${apiBase()}/sessions/${encodeURIComponent(sessionId)}/stream`,
191
- {
192
- headers: { "user-agent": USER_AGENT, ...authHeaders(), accept: "text/event-stream" },
193
- signal
194
- }
195
- );
196
- if (!res.ok) await parseError(res);
197
- if (!res.body) throw new ApiError(502, "no_body", "stream returned no body");
198
- try {
199
- for await (const frame of parseSseStream(res.body)) {
200
- if (!frame.data) continue;
201
- try {
202
- yield { id: frame.id, event: frame.event, data: JSON.parse(frame.data) };
203
- } catch {
204
- }
205
- }
206
- } catch (error) {
207
- if (!(error instanceof DOMException && error.name === "AbortError")) throw error;
208
- }
209
- }
210
-
211
51
  // src/files.ts
212
- import { readFileSync as readFileSync2 } from "fs";
52
+ import { readFileSync } from "fs";
213
53
  import { basename, extname } from "path";
214
54
  var MEDIA_TYPES = {
215
55
  ".png": "image/png",
@@ -231,7 +71,7 @@ function mediaTypeFor(path) {
231
71
  return MEDIA_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
232
72
  }
233
73
  async function uploadLocalFile(path, expiresInSeconds) {
234
- const bytes = readFileSync2(path);
74
+ const bytes = readFileSync(path);
235
75
  return uploadFile(new Uint8Array(bytes), basename(path), mediaTypeFor(path), expiresInSeconds);
236
76
  }
237
77
  function fileBlock(file) {
@@ -328,8 +168,8 @@ function renderGoal(goal2) {
328
168
  }
329
169
 
330
170
  // src/agent-file.ts
331
- import { existsSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
332
- import { dirname as dirname2, resolve } from "path";
171
+ import { existsSync, readFileSync as readFileSync2, writeFileSync } from "fs";
172
+ import { dirname, resolve } from "path";
333
173
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
334
174
  var fileInstructionSchema = external_exports.object({
335
175
  name: external_exports.string(),
@@ -352,12 +192,16 @@ var agentFileSchema = external_exports.object({
352
192
  instructions: external_exports.array(fileInstructionSchema).optional(),
353
193
  skills: external_exports.array(skillRefSchema).optional(),
354
194
  customData: external_exports.array(customDataSchema).optional(),
195
+ /** "deny" makes the agent work alone: its sessions may not hire helpers. */
196
+ hiring_policy: external_exports.enum(["allow", "deny"]).optional(),
197
+ /** The environment its sessions boot in when they name none. */
198
+ environment: external_exports.string().min(1).optional(),
355
199
  /** Advanced machine shape (create-only): 1|2|4|8 vCPUs, 256MB-step memory. */
356
200
  vcpus: external_exports.number().int().optional(),
357
201
  memory_mb: external_exports.number().int().optional()
358
202
  });
359
203
  function readAgentFile(path) {
360
- const raw = readFileSync3(path, "utf8");
204
+ const raw = readFileSync2(path, "utf8");
361
205
  const parsed = agentFileSchema.safeParse(parseToml(raw));
362
206
  if (!parsed.success) {
363
207
  const issue = parsed.error.issues[0];
@@ -369,11 +213,11 @@ function readAgentFile(path) {
369
213
  `${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
370
214
  );
371
215
  }
372
- const base = dirname2(resolve(path));
216
+ const base = dirname(resolve(path));
373
217
  const inline = (ref, inlineContent) => {
374
218
  if (inlineContent !== void 0) return inlineContent;
375
219
  if (!ref) return void 0;
376
- return readFileSync3(resolve(base, ref), "utf8");
220
+ return readFileSync2(resolve(base, ref), "utf8");
377
221
  };
378
222
  const spec = {
379
223
  name: file.name,
@@ -391,20 +235,22 @@ function readAgentFile(path) {
391
235
  }),
392
236
  skills: file.skills,
393
237
  customData: file.customData,
238
+ hiringPolicy: file.hiring_policy,
239
+ environmentId: file.environment,
394
240
  vcpus: file.vcpus,
395
241
  memoryMb: file.memory_mb
396
242
  };
397
243
  return { file, spec };
398
244
  }
399
245
  function stampId(path, id) {
400
- const doc = parseToml(readFileSync3(path, "utf8"));
246
+ const doc = parseToml(readFileSync2(path, "utf8"));
401
247
  doc.id = id;
402
248
  delete doc.slug;
403
- writeFileSync2(path, stringifyToml(doc));
249
+ writeFileSync(path, stringifyToml(doc));
404
250
  }
405
251
  function scaffoldAgentFile(path, name) {
406
252
  if (existsSync(path)) throw new Error(`${path} already exists`);
407
- writeFileSync2(
253
+ writeFileSync(
408
254
  path,
409
255
  `# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
410
256
  name = "${name}"
@@ -412,6 +258,11 @@ name = "${name}"
412
258
  # llm = "deepseek-flash" # must be compatible with type
413
259
  # reasoning_effort = "high" # per harness \u2014 ${reasoningEffortDocLine()}
414
260
  # capabilities = []
261
+ # hiring_policy = "deny" # its sessions work alone; "allow" (default) lets them hire helpers
262
+ # environment = "env_..." # the environment its sessions boot in when they name none
263
+ # [[skills]] # a skill from \`sky skills upload <dir>\`
264
+ # type = "skill"
265
+ # skillId = "skill_..."
415
266
 
416
267
  prompt = """
417
268
  You are ${name}, a helpful long-lived agent.
@@ -485,18 +336,18 @@ function renderCatalogAgent(agent2) {
485
336
 
486
337
  // src/clone.ts
487
338
  import { spawn } from "child_process";
488
- import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
489
- import { homedir as homedir2 } from "os";
490
- import { basename as basename2, join as join2 } from "path";
339
+ import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
340
+ import { homedir } from "os";
341
+ import { basename as basename2, join } from "path";
491
342
  import { createInterface } from "readline";
492
343
  var PROMPT_CAP = 1e5;
493
344
  var HARNESSES = ["claude_code", "codex", "openclaw"];
494
345
  function claudeConfigRoot() {
495
- return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
346
+ return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
496
347
  }
497
348
  function readIfExists(path) {
498
349
  try {
499
- return readFileSync4(path, "utf8");
350
+ return readFileSync3(path, "utf8");
500
351
  } catch {
501
352
  return void 0;
502
353
  }
@@ -512,9 +363,9 @@ function readJson(path) {
512
363
  }
513
364
  function claudeModel(configRoot) {
514
365
  const candidates = [
515
- join2(process.cwd(), ".claude", "settings.local.json"),
516
- join2(process.cwd(), ".claude", "settings.json"),
517
- join2(configRoot, "settings.json")
366
+ join(process.cwd(), ".claude", "settings.local.json"),
367
+ join(process.cwd(), ".claude", "settings.json"),
368
+ join(configRoot, "settings.json")
518
369
  ];
519
370
  for (const path of candidates) {
520
371
  const model = readJson(path)?.model;
@@ -524,11 +375,11 @@ function claudeModel(configRoot) {
524
375
  }
525
376
  function claudeMcpServers() {
526
377
  const merged = {};
527
- const userConfig = readJson(join2(homedir2(), ".claude.json"));
378
+ const userConfig = readJson(join(homedir(), ".claude.json"));
528
379
  Object.assign(merged, userConfig?.mcpServers ?? {});
529
380
  Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
530
381
  const projectConfig = readJson(
531
- join2(process.cwd(), ".mcp.json")
382
+ join(process.cwd(), ".mcp.json")
532
383
  );
533
384
  Object.assign(merged, projectConfig?.mcpServers ?? {});
534
385
  return merged;
@@ -545,13 +396,13 @@ function readClaudeCode() {
545
396
  const configRoot = claudeConfigRoot();
546
397
  const parts = [];
547
398
  const sources = [];
548
- const global = readIfExists(join2(configRoot, "CLAUDE.md"));
399
+ const global = readIfExists(join(configRoot, "CLAUDE.md"));
549
400
  if (global?.trim()) {
550
401
  parts.push({ label: "Global CLAUDE.md", content: global });
551
402
  sources.push("global CLAUDE.md");
552
403
  }
553
404
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
554
- const project = readIfExists(join2(process.cwd(), name));
405
+ const project = readIfExists(join(process.cwd(), name));
555
406
  if (project?.trim()) {
556
407
  parts.push({ label: `Project ${name}`, content: project });
557
408
  sources.push(`project ${name}`);
@@ -592,27 +443,27 @@ function detectHarness(explicit) {
592
443
  return explicit;
593
444
  }
594
445
  if (existsSync2(claudeConfigRoot())) return "claude_code";
595
- if (existsSync2(join2(homedir2(), ".codex"))) return "codex";
596
- if (existsSync2(join2(homedir2(), ".openclaw"))) return "openclaw";
446
+ if (existsSync2(join(homedir(), ".codex"))) return "codex";
447
+ if (existsSync2(join(homedir(), ".openclaw"))) return "openclaw";
597
448
  throw new Error(
598
449
  "no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
599
450
  );
600
451
  }
601
- function readLocal(harness) {
602
- if (harness === "claude_code") return readClaudeCode();
452
+ function readLocal(harness2) {
453
+ if (harness2 === "claude_code") return readClaudeCode();
603
454
  throw new Error(
604
- `${harness} cloning lands next \u2014 claude_code is the first supported harness`
455
+ `${harness2} cloning lands next \u2014 claude_code is the first supported harness`
605
456
  );
606
457
  }
607
458
  function linkPath(configRoot) {
608
- return join2(configRoot, "agentsky-link.json");
459
+ return join(configRoot, "agentsky-link.json");
609
460
  }
610
461
  function readLink(configRoot) {
611
462
  const link = readJson(linkPath(configRoot));
612
463
  return link?.agentId ? link : void 0;
613
464
  }
614
465
  function writeLink(configRoot, link) {
615
- writeFileSync3(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
466
+ writeFileSync2(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
616
467
  `);
617
468
  }
618
469
  var DEFAULT_VAULT_NAME = "default";
@@ -843,17 +694,17 @@ async function syncClone(local, link, opts) {
843
694
  if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentId}`);
844
695
  }
845
696
  async function clone(opts) {
846
- const harness = detectHarness(opts.harness);
847
- const local = readLocal(harness);
697
+ const harness2 = detectHarness(opts.harness);
698
+ const local = readLocal(harness2);
848
699
  const link = opts.new ? void 0 : readLink(local.configRoot);
849
700
  if (link) await syncClone(local, link, opts);
850
701
  else await createClone(local, opts);
851
702
  }
852
703
 
853
704
  // src/import-local.ts
854
- import { closeSync, existsSync as existsSync3, openSync, readdirSync, readFileSync as readFileSync5, readSync, statSync } from "fs";
855
- import { homedir as homedir3 } from "os";
856
- import { join as join3 } from "path";
705
+ import { closeSync, existsSync as existsSync3, openSync, readdirSync, readFileSync as readFileSync4, readSync, statSync } from "fs";
706
+ import { homedir as homedir2 } from "os";
707
+ import { join as join2 } from "path";
857
708
  import { createHash } from "crypto";
858
709
  function redactLine2(raw) {
859
710
  return redactLine(raw).line;
@@ -926,13 +777,13 @@ function sniffTitle(filePath) {
926
777
  }
927
778
  }
928
779
  function listClaudeCodeSessions(limit) {
929
- const root = process.env.CLAUDE_CONFIG_DIR ?? join3(homedir3(), ".claude");
930
- const projectsDir = join3(root, "projects");
780
+ const root = process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
781
+ const projectsDir = join2(root, "projects");
931
782
  if (!existsSync3(projectsDir)) return [];
932
783
  const sessions = [];
933
784
  try {
934
785
  for (const encoded of readdirSync(projectsDir)) {
935
- const projectDir = join3(projectsDir, encoded);
786
+ const projectDir = join2(projectsDir, encoded);
936
787
  let entries;
937
788
  try {
938
789
  entries = readdirSync(projectDir);
@@ -943,7 +794,7 @@ function listClaudeCodeSessions(limit) {
943
794
  for (const entry2 of entries) {
944
795
  if (!entry2.endsWith(".jsonl")) continue;
945
796
  const sessionId = entry2.slice(0, -6);
946
- const mainFile = join3(projectDir, entry2);
797
+ const mainFile = join2(projectDir, entry2);
947
798
  let stat;
948
799
  try {
949
800
  stat = statSync(mainFile);
@@ -953,12 +804,12 @@ function listClaudeCodeSessions(limit) {
953
804
  if (!stat.isFile()) continue;
954
805
  let discoveryError;
955
806
  const subagentFiles = [];
956
- const subagentDir = join3(projectDir, sessionId, "subagents");
807
+ const subagentDir = join2(projectDir, sessionId, "subagents");
957
808
  if (existsSync3(subagentDir)) {
958
809
  try {
959
810
  const subFiles = readdirSync(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
960
811
  for (const f of subFiles) {
961
- subagentFiles.push(join3(subagentDir, f));
812
+ subagentFiles.push(join2(subagentDir, f));
962
813
  }
963
814
  } catch {
964
815
  discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
@@ -980,7 +831,7 @@ function listClaudeCodeSessions(limit) {
980
831
  return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
981
832
  }
982
833
  function listCodexSessions(limit) {
983
- const sessionsDir = join3(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join3(homedir3(), ".codex"), "sessions");
834
+ const sessionsDir = join2(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join2(homedir2(), ".codex"), "sessions");
984
835
  const records = [];
985
836
  function walk(dir) {
986
837
  let entries;
@@ -990,7 +841,7 @@ function listCodexSessions(limit) {
990
841
  return;
991
842
  }
992
843
  for (const entry2 of entries) {
993
- const full = join3(dir, entry2.name);
844
+ const full = join2(dir, entry2.name);
994
845
  if (entry2.isDirectory()) {
995
846
  walk(full);
996
847
  continue;
@@ -1032,9 +883,9 @@ function listCodexSessions(limit) {
1032
883
  files: [.../* @__PURE__ */ new Set([...session2.files, ...descendants(session2.sessionId, /* @__PURE__ */ new Set())])]
1033
884
  })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
1034
885
  }
1035
- function listRecentSessions(harness, limit) {
1036
- const claudeSessions = harness === "codex" ? [] : listClaudeCodeSessions(limit);
1037
- const codexSessions = harness === "claude_code" ? [] : listCodexSessions(limit);
886
+ function listRecentSessions(harness2, limit) {
887
+ const claudeSessions = harness2 === "codex" ? [] : listClaudeCodeSessions(limit);
888
+ const codexSessions = harness2 === "claude_code" ? [] : listCodexSessions(limit);
1038
889
  return [...claudeSessions, ...codexSessions].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
1039
890
  }
1040
891
  function agentUseBase() {
@@ -1052,7 +903,7 @@ function currentSessionEnv() {
1052
903
  if (codexId) return { harness: "codex", sessionId: codexId };
1053
904
  return void 0;
1054
905
  }
1055
- async function mintTrace(base, harness) {
906
+ async function mintTrace(base, harness2) {
1056
907
  const token = apiToken();
1057
908
  if (!token) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
1058
909
  const res = await fetch(`${base}/api/traces`, {
@@ -1064,7 +915,7 @@ async function mintTrace(base, harness) {
1064
915
  authorization: `Bearer ${token}`,
1065
916
  "user-agent": USER_AGENT
1066
917
  },
1067
- body: JSON.stringify({ harness })
918
+ body: JSON.stringify({ harness: harness2 })
1068
919
  });
1069
920
  if (!res.ok) {
1070
921
  const body = await res.json().catch(() => null);
@@ -1077,7 +928,7 @@ async function mintTrace(base, harness) {
1077
928
  function loadSummaryFile(summaryPath) {
1078
929
  let raw;
1079
930
  try {
1080
- raw = readFileSync5(summaryPath, "utf8");
931
+ raw = readFileSync4(summaryPath, "utf8");
1081
932
  } catch {
1082
933
  throw new Error(`Cannot read summary file: ${summaryPath}`);
1083
934
  }
@@ -1109,7 +960,7 @@ function prepareImport(session2, summaryPath) {
1109
960
  for (const path of session2.files) {
1110
961
  const stat = statSync(path);
1111
962
  if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
1112
- const text = readFileSync5(path, "utf8");
963
+ const text = readFileSync4(path, "utf8");
1113
964
  for (const raw of text.split("\n")) {
1114
965
  if (!raw.trim()) continue;
1115
966
  let record;
@@ -1254,7 +1105,7 @@ async function login(opts) {
1254
1105
  }
1255
1106
  async function browserLogin(verifier, challenge) {
1256
1107
  const state = b64url(randomBytes(16));
1257
- const code = await new Promise((resolve5, reject) => {
1108
+ const code = await new Promise((resolve7, reject) => {
1258
1109
  const server = createServer((req, res) => {
1259
1110
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
1260
1111
  if (url.pathname !== "/callback") {
@@ -1267,7 +1118,7 @@ async function browserLogin(verifier, challenge) {
1267
1118
  if (url.searchParams.get("state") !== state || !got) {
1268
1119
  reject(new Error("login callback state mismatch"));
1269
1120
  } else {
1270
- resolve5(got);
1121
+ resolve7(got);
1271
1122
  }
1272
1123
  server.close();
1273
1124
  });
@@ -1322,22 +1173,22 @@ async function headlessLogin(verifier, challenge) {
1322
1173
 
1323
1174
  // src/local-agents.ts
1324
1175
  import { existsSync as existsSync4 } from "fs";
1325
- import { homedir as homedir4 } from "os";
1326
- import { join as join4 } from "path";
1327
- function localAgents(env = process.env, home = homedir4()) {
1328
- const claude = env.CLAUDE_CONFIG_DIR || join4(home, ".claude");
1329
- const codex = env.CODEX_HOME || join4(home, ".codex");
1330
- const cursor = join4(home, ".cursor");
1176
+ import { homedir as homedir3 } from "os";
1177
+ import { join as join3 } from "path";
1178
+ function localAgents(env = process.env, home = homedir3()) {
1179
+ const claude = env.CLAUDE_CONFIG_DIR || join3(home, ".claude");
1180
+ const codex = env.CODEX_HOME || join3(home, ".codex");
1181
+ const cursor = join3(home, ".cursor");
1331
1182
  return [
1332
- { id: "claude_code", name: "Claude Code", root: claude, skillsDir: join4(claude, "skills") },
1333
- { id: "codex", name: "Codex", root: codex, skillsDir: join4(codex, "skills") },
1334
- { id: "cursor", name: "Cursor", root: cursor, skillsDir: join4(cursor, "skills") }
1183
+ { id: "claude_code", name: "Claude Code", root: claude, skillsDir: join3(claude, "skills") },
1184
+ { id: "codex", name: "Codex", root: codex, skillsDir: join3(codex, "skills") },
1185
+ { id: "cursor", name: "Cursor", root: cursor, skillsDir: join3(cursor, "skills") }
1335
1186
  ];
1336
1187
  }
1337
- function detectLocalAgents(env = process.env, home = homedir4()) {
1188
+ function detectLocalAgents(env = process.env, home = homedir3()) {
1338
1189
  return localAgents(env, home).filter((agent2) => existsSync4(agent2.root));
1339
1190
  }
1340
- function selectLocalAgents(ids, env = process.env, home = homedir4()) {
1191
+ function selectLocalAgents(ids, env = process.env, home = homedir3()) {
1341
1192
  if (!ids?.length) return detectLocalAgents(env, home);
1342
1193
  const known = localAgents(env, home);
1343
1194
  return ids.map((id) => {
@@ -1431,21 +1282,21 @@ async function readTurn(sessionId, opts = {}) {
1431
1282
  for (; ; ) {
1432
1283
  const reading = latestTurn(await history(sessionId));
1433
1284
  if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
1434
- await new Promise((resolve5) => setTimeout(resolve5, opts.intervalMs ?? 3e3));
1285
+ await new Promise((resolve7) => setTimeout(resolve7, opts.intervalMs ?? 3e3));
1435
1286
  }
1436
1287
  }
1437
1288
 
1438
1289
  // src/setup.ts
1439
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
1440
- import { dirname as dirname4, join as join8 } from "path";
1290
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1291
+ import { dirname as dirname3, join as join7 } from "path";
1441
1292
  import { createInterface as createInterface3 } from "readline";
1442
1293
 
1443
1294
  // src/mcp-register.ts
1444
1295
  import { spawnSync } from "child_process";
1445
1296
 
1446
1297
  // src/skill.ts
1447
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1448
- import { join as join5 } from "path";
1298
+ import { mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1299
+ import { join as join4 } from "path";
1449
1300
 
1450
1301
  // src/catalog-file.ts
1451
1302
  var CATALOG_FILE = "catalog.md";
@@ -1502,7 +1353,7 @@ var SKILL_MARKER = ".agentsky.json";
1502
1353
  function skillDocument() {
1503
1354
  return `---
1504
1355
  name: ${SKILL_NAME}
1505
- description: "Hire cloud agents from the AgentSky catalog (included as ${CATALOG_FILE}) and coordinate them on one task. Use when: the person names an agent by @handle or wants to orchestrate agents; asks another engine (Codex, Claude Code, Gemini, Hermes) to do, test or review part of the work; wants to import, move or continue this conversation in AgentSky or the cloud; names a workspace; or the work splits into independent parts that can run at the same time. Works through the agentsky MCP server, or the sky command where those tools are missing."
1356
+ description: "Hire cloud agents from the AgentSky catalog (included as ${CATALOG_FILE}) and coordinate them on one task. Use when: the person names an agent by @handle or wants to orchestrate agents; asks another engine (Codex, Claude Code, Gemini, Hermes) to do, test or review part of the work; wants to import, move or continue this conversation in AgentSky or the cloud; wants an agent their own application starts for its customers from a skill they supply; names a workspace; or the work splits into independent parts that can run at the same time. Works through the agentsky MCP server, or the sky command where those tools are missing."
1506
1357
  ---
1507
1358
 
1508
1359
  # Working with other agents through AgentSky
@@ -1521,7 +1372,7 @@ at the end; they do the same things, one agent at a time.
1521
1372
 
1522
1373
  Use the workspace the person named. If they named none, check the git root for
1523
1374
  \`.agentsky.json\` \u2014 if it has a \`"workspace"\` key, use that. Otherwise use the
1524
- account default (personal). Never ask. Pass the chosen slug as \`workspace\` on
1375
+ account default workspace. Never ask. Pass the chosen slug as \`workspace\` on
1525
1376
  every MCP tool that accepts it, or as \`--workspace <slug>\` on sky commands.
1526
1377
  Details: \`${REFERENCES_DIR}/workspaces.md\` in this skill's folder.
1527
1378
 
@@ -1602,6 +1453,15 @@ it, and name its path in the brief.
1602
1453
  6. Tell the person what was done, which agent did which part, and what it cost
1603
1454
  (\`get_task\` reports the task's cost).
1604
1455
 
1456
+ ## Build an agent that the person's own application runs
1457
+
1458
+ When the person wants an agent their product starts for its customers, built
1459
+ from a skill they supply (for example: a customer uploads a file, an agent
1460
+ reviews it, their application acts on the verdict), follow
1461
+ \`${REFERENCES_DIR}/focused-agents.md\` in this skill's folder. It covers the
1462
+ prerequisites to tell them about first, one locked-down workspace per customer,
1463
+ one fresh session per event, and reading the answer as data.
1464
+
1605
1465
  ## Import this conversation to AgentSky
1606
1466
 
1607
1467
  When the person asks to import, move, or continue this conversation in AgentSky
@@ -1688,6 +1548,160 @@ use \`--session <id>\` instead of \`--current\`, and set \`source_harness\` to t
1688
1548
  harness name (e.g. \`"hermes"\`, \`"openclaw"\`).
1689
1549
  `;
1690
1550
  }
1551
+ function focusedAgentsReferenceDoc() {
1552
+ return `# A focused agent your application runs for its customers
1553
+
1554
+ Use this when the person wants an agent that their own product starts on an
1555
+ event (a customer uploads a file, a form is submitted), working from one
1556
+ skill they supply, with the result going back to their application.
1557
+
1558
+ Everything here is the public API (\`https://api.agentsky.dev/v1\`). The
1559
+ application's server calls it. The \`sky\` commands and MCP tools named
1560
+ alongside do the same things by hand, for trying it out.
1561
+
1562
+ ## What has to exist first
1563
+
1564
+ Say each of these to the person before you build. Each one changes whether it
1565
+ works:
1566
+
1567
+ - **An API key for their server.** They create it at
1568
+ platform.agentsky.dev \u2192 Developer \u2192 API keys and put it in the server's
1569
+ environment (\`AGENTSKY_TOKEN\`). Never put it in code, a page their customers
1570
+ load, or a brief.
1571
+ - **Credit** on their account. Every run is a session they pay for. Give each
1572
+ one a budget.
1573
+ - **The skill**: a directory with \`SKILL.md\` at its root.
1574
+ - **An engine that fits.** Default to \`claude_code\` on its default model. Check
1575
+ \`sky catalog search <words>\` if the skill needs something else.
1576
+ - **Their own customer sign-in.** AgentSky does not know their customers. A
1577
+ customer id their page sends is a label, not proof of who it is.
1578
+
1579
+ ## 1. One workspace per customer
1580
+
1581
+ \`\`\`
1582
+ POST /v1/universes {"slug": "rv-acme", "name": "Uploads from acme"}
1583
+ \`\`\`
1584
+
1585
+ Same by hand: \`sky universe create rv-acme\`. Files, memory, vaults and
1586
+ sessions never cross workspaces. Name every later call's workspace with the
1587
+ \`X-Universe: rv-acme\` header (\`--workspace rv-acme\` on \`sky\`).
1588
+
1589
+ ## 2. Lock the workspace down
1590
+
1591
+ \`\`\`
1592
+ PUT /v1/hook_rules {"rules": [{"point": "tool.call", "action": "block", "match": {}, "enabled": true}]}
1593
+ POST /v1/universes/rv-acme/policies {"name": "reviewer-native-tools", "kind": "TOOL_CALL",
1594
+ "config": {"action": "deny", "matcher": "Bash|BashOutput|KillShell|Edit|MultiEdit|Write|NotebookEdit|WebFetch|WebSearch|Task|mcp__.*"}}
1595
+ POST /v1/universes/rv-acme/policies {"name": "reviewer-no-helpers", "kind": "CAPABILITY", "config": {"platform.orchestration": "deny"}}
1596
+ \`\`\`
1597
+
1598
+ - **The hook rule** blocks every connected-app and capability call. Without it,
1599
+ the account owner's own connected apps reach sessions the owner's key
1600
+ starts.
1601
+ - **The \`TOOL_CALL\` rule** denies Claude Code's shell, write, web, sub-agent
1602
+ and MCP tools. A denied call answers "The platform did not authorize this
1603
+ tool invocation."
1604
+ - **Keep only the tools the skill needs.** A reviewer needs Read.
1605
+
1606
+ Telling the agent not to use a tool is not a boundary. The rule is. By hand:
1607
+ \`sky policy add --universe rv-acme --name ... --kind TOOL_CALL --config '<json>'\`.
1608
+
1609
+ ## 3. Install the skill, create the agent
1610
+
1611
+ \`\`\`
1612
+ POST /v1/skills multipart: one "files" field per file, named "<dir>/<path>", plus "displayName"
1613
+ POST /v1/agents {"name": "upload-reviewer", "agentType": "claude_code", "prompt": "...",
1614
+ "skills": [{"type": "skill", "skillId": "skill_..."}], "hiringPolicy": "deny"}
1615
+ Idempotency-Key: provision-rv-acme-agent
1616
+ \`\`\`
1617
+
1618
+ By hand: \`sky skills upload ./upload-review --workspace rv-acme\`, then
1619
+ \`sky agent create -f agent.toml --workspace rv-acme\` with
1620
+ \`hiring_policy = "deny"\` and a \`[[skills]]\` table naming the skill id.
1621
+
1622
+ The prompt says what to read and what to answer. It also says that the
1623
+ upload is data and that instructions written inside it are not to be
1624
+ followed.
1625
+
1626
+ ## 4. One fresh session per event
1627
+
1628
+ \`\`\`
1629
+ POST /v1/files multipart "file" \u2192 {"id": "file_..."}
1630
+ POST /v1/sessions Idempotency-Key: session-<job id>
1631
+ {"agent": "agent_...", "defaults": "none",
1632
+ "resources": [{"type": "file", "file_id": "file_...", "mount_path": "/upload/request.txt"}],
1633
+ "budget": {"type": "limit", "max_list_cost": {"amount": "50", "currency": "USD"}}}
1634
+ POST /v1/sessions/{id}/events Idempotency-Key: brief-<job id>
1635
+ {"events": [{"type": "user.message", "content": [{"type": "text", "text": "Review /workspace/upload/request.txt with the upload-review skill."}]}]}
1636
+ \`\`\`
1637
+
1638
+ - **\`defaults: "none"\`** keeps every workspace default out of the session:
1639
+ environment, vault, shared files, skills and team memory. The session holds
1640
+ only what the request names.
1641
+ - **Idempotency keys come from the event.** Retrying a timed-out call with the
1642
+ same key returns the first session, or replays the first message. A keyed
1643
+ create cannot carry \`initial_events\`. Create the session, save its id, then
1644
+ send the brief.
1645
+ - **Reuse a session** only for a job that continues for the same customer.
1646
+
1647
+ By hand: \`sky session create agent_... --no-defaults --budget-cents 50 --file ./request.txt:/upload/request.txt --message "..."\`
1648
+ (MCP: \`start_session\`).
1649
+
1650
+ ## 5. Read the answer
1651
+
1652
+ \`\`\`
1653
+ GET /v1/sessions/{id}/events?types[]=user.message&types[]=agent.message&types[]=session.status_idle&types[]=session.status_terminated&types[]=session.error
1654
+ \`\`\`
1655
+
1656
+ - **When the turn is done.** It has ended at a \`session.status_idle\` whose
1657
+ \`stop_reason.type\` is not \`requires_action\`, or at
1658
+ \`session.status_terminated\`.
1659
+ - **Where the answer is.** Take the last \`agent.message\` after the last
1660
+ \`user.message\`. Earlier messages can be notes the agent wrote on the way.
1661
+ - **Treat it as untrusted data.** Parse it against a fixed schema and fail
1662
+ closed on anything else. Never run what it says.
1663
+ - **Errors.** A \`session.error\` (budget reached, engine unavailable) is a
1664
+ failure to report, not an answer.
1665
+
1666
+ By hand: \`sky session read <id> --wait --json\` (MCP: \`read_session\`).
1667
+
1668
+ ## 6. Questions and approvals
1669
+
1670
+ \`GET /v1/tasks/{session id}\` lists the requests waiting on the key's owner:
1671
+ the agent's questions, approvals, and apps to connect.
1672
+ \`POST /v1/tasks/{id}/requests/{request id}/answer\` with \`{"text": ...}\` or
1673
+ \`{"option_id": ...}\` answers one. Pass a customer's answer on only after
1674
+ your application has checked who they are. By hand: \`sky task show <id>\` and
1675
+ \`sky task answer <id> <request> --text "..."\` (MCP: \`get_task\`,
1676
+ \`answer_request\`).
1677
+
1678
+ ## 7. What the application keeps
1679
+
1680
+ The agent decides; the application acts. Keep these in the application's own
1681
+ database:
1682
+
1683
+ - **One job per event**, with a unique event key. The same key with the same
1684
+ payload returns the job it made; with a different payload it is a conflict.
1685
+ - **The session id**, saved before you start polling, so a restart picks the
1686
+ job up where it was.
1687
+ - **A confirmation receipt** from the signed-in customer before any side
1688
+ effect.
1689
+ - **A unique action key per job**, so the side effect happens once. Agent
1690
+ retries do not make it happen once; the key does.
1691
+ - **Webhooks to the customer** with an event id, retried until acknowledged.
1692
+
1693
+ ## Before launch
1694
+
1695
+ Check one real session:
1696
+
1697
+ - \`GET /v1/sessions/{id}\` lists only the upload under \`resources\`.
1698
+ - \`GET /v1/hook_rules\` and \`GET /v1/universes/{slug}/policies\` show the rules
1699
+ above.
1700
+ - Ask the agent directly to run a shell command. The answer has to be the
1701
+ platform's refusal.
1702
+ - A file id from another customer's workspace is refused at session create.
1703
+ `;
1704
+ }
1691
1705
  function workspacesReferenceDoc() {
1692
1706
  return `# Workspaces in AgentSky
1693
1707
 
@@ -1723,7 +1737,7 @@ To set a per-repo default, create \`.agentsky.json\` at the git root:
1723
1737
  }
1724
1738
  function installedVersion(dir) {
1725
1739
  try {
1726
- const marker = JSON.parse(readFileSync6(join5(dir, SKILL_MARKER), "utf8"));
1740
+ const marker = JSON.parse(readFileSync5(join4(dir, SKILL_MARKER), "utf8"));
1727
1741
  return typeof marker.version === "string" ? marker.version : null;
1728
1742
  } catch {
1729
1743
  return null;
@@ -1733,24 +1747,25 @@ function isInstalledSkill(dir) {
1733
1747
  return installedVersion(dir) !== null;
1734
1748
  }
1735
1749
  function installSkill(agent2, opts = {}) {
1736
- const dir = join5(agent2.skillsDir, SKILL_NAME);
1750
+ const dir = join4(agent2.skillsDir, SKILL_NAME);
1737
1751
  const previous = installedVersion(dir);
1738
1752
  const outcome = previous === CLI_VERSION ? "current" : previous ? "updated" : "installed";
1739
1753
  if (opts.dryRun) return { agent: agent2.id, path: dir, outcome };
1740
1754
  if (outcome !== "current") {
1741
- mkdirSync2(dir, { recursive: true });
1742
- writeFileSync4(join5(dir, "SKILL.md"), skillDocument());
1743
- writeFileSync4(join5(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
1755
+ mkdirSync(dir, { recursive: true });
1756
+ writeFileSync3(join4(dir, "SKILL.md"), skillDocument());
1757
+ writeFileSync3(join4(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
1744
1758
  `);
1745
1759
  }
1746
1760
  if (opts.catalog) {
1747
- mkdirSync2(dir, { recursive: true });
1748
- writeFileSync4(join5(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
1761
+ mkdirSync(dir, { recursive: true });
1762
+ writeFileSync3(join4(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
1749
1763
  }
1750
- const refsDir = join5(dir, REFERENCES_DIR);
1751
- mkdirSync2(refsDir, { recursive: true });
1752
- writeFileSync4(join5(refsDir, "import.md"), importReferenceDoc());
1753
- writeFileSync4(join5(refsDir, "workspaces.md"), workspacesReferenceDoc());
1764
+ const refsDir = join4(dir, REFERENCES_DIR);
1765
+ mkdirSync(refsDir, { recursive: true });
1766
+ writeFileSync3(join4(refsDir, "import.md"), importReferenceDoc());
1767
+ writeFileSync3(join4(refsDir, "workspaces.md"), workspacesReferenceDoc());
1768
+ writeFileSync3(join4(refsDir, "focused-agents.md"), focusedAgentsReferenceDoc());
1754
1769
  return { agent: agent2.id, path: dir, outcome };
1755
1770
  }
1756
1771
 
@@ -1801,9 +1816,9 @@ function registerMcpServer(agent2, opts, runner = systemCommandRunner()) {
1801
1816
  }
1802
1817
 
1803
1818
  // src/setup-local.ts
1804
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
1805
- import { homedir as homedir5 } from "os";
1806
- import { dirname as dirname3, isAbsolute, join as join6, resolve as resolve3 } from "path";
1819
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
1820
+ import { homedir as homedir4 } from "os";
1821
+ import { dirname as dirname2, isAbsolute, join as join5, resolve as resolve3 } from "path";
1807
1822
  import { parse as parseToml2 } from "smol-toml";
1808
1823
  var MAX_SKILL_FILE_BYTES = 1024 * 1024;
1809
1824
  var MAX_IMPORT_DEPTH = 5;
@@ -1819,22 +1834,22 @@ function sensitive(name) {
1819
1834
  function skillFiles(root, prefix) {
1820
1835
  const files2 = [];
1821
1836
  for (const entry2 of readdirSync2(root, { withFileTypes: true })) {
1822
- const path = join6(root, entry2.name);
1837
+ const path = join5(root, entry2.name);
1823
1838
  if (entry2.isDirectory()) {
1824
1839
  if (!SKIPPED_DIRS.has(entry2.name)) files2.push(...skillFiles(path, `${prefix}/${entry2.name}`));
1825
1840
  } else if (entry2.isFile() && !sensitive(entry2.name) && statSync2(path).size <= MAX_SKILL_FILE_BYTES) {
1826
- files2.push({ path: `${prefix}/${entry2.name}`, bytes: new Uint8Array(readFileSync7(path)) });
1841
+ files2.push({ path: `${prefix}/${entry2.name}`, bytes: new Uint8Array(readFileSync6(path)) });
1827
1842
  }
1828
1843
  }
1829
1844
  return files2;
1830
1845
  }
1831
1846
  function readLocalSkills(agents) {
1832
- const skills = [];
1847
+ const skills2 = [];
1833
1848
  const ignored = [];
1834
1849
  for (const agent2 of agents) {
1835
1850
  if (!existsSync5(agent2.skillsDir)) continue;
1836
1851
  for (const entry2 of readdirSync2(agent2.skillsDir, { withFileTypes: true })) {
1837
- const dir = join6(agent2.skillsDir, entry2.name);
1852
+ const dir = join5(agent2.skillsDir, entry2.name);
1838
1853
  let isDir = entry2.isDirectory();
1839
1854
  if (entry2.isSymbolicLink()) {
1840
1855
  try {
@@ -1843,22 +1858,22 @@ function readLocalSkills(agents) {
1843
1858
  isDir = false;
1844
1859
  }
1845
1860
  }
1846
- if (!isDir || !existsSync5(join6(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
1847
- const name = frontmatterName(readFileSync7(join6(dir, "SKILL.md"), "utf8"));
1861
+ if (!isDir || !existsSync5(join5(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
1862
+ const name = frontmatterName(readFileSync6(join5(dir, "SKILL.md"), "utf8"));
1848
1863
  if (!name) {
1849
1864
  ignored.push({ agent: agent2.id, dir, reason: "SKILL.md has no name" });
1850
1865
  continue;
1851
1866
  }
1852
- const taken = skills.find((skill) => skill.name === name);
1867
+ const taken = skills2.find((skill) => skill.name === name);
1853
1868
  if (taken) {
1854
1869
  ignored.push({ agent: agent2.id, dir, reason: `${taken.agent} already has a skill named ${name}` });
1855
1870
  continue;
1856
1871
  }
1857
1872
  const files2 = skillFiles(dir, entry2.name);
1858
- skills.push({ name, agent: agent2.id, dir, files: files2, bytes: files2.reduce((sum, file) => sum + file.bytes.length, 0) });
1873
+ skills2.push({ name, agent: agent2.id, dir, files: files2, bytes: files2.reduce((sum, file) => sum + file.bytes.length, 0) });
1859
1874
  }
1860
1875
  }
1861
- return { skills, ignored };
1876
+ return { skills: skills2, ignored };
1862
1877
  }
1863
1878
  var INSTRUCTION_FILES = { claude_code: "CLAUDE.md", codex: "AGENTS.md" };
1864
1879
  function inlineImports(content, from, home, depth, seen) {
@@ -1872,11 +1887,11 @@ function inlineImports(content, from, home, depth, seen) {
1872
1887
  }
1873
1888
  const target = fence ? void 0 : line.match(/^@(\S+\.(?:md|markdown|txt))$/i)?.[1];
1874
1889
  if (!target || depth >= MAX_IMPORT_DEPTH) return line;
1875
- const path = target.startsWith("~/") ? join6(home, target.slice(2)) : isAbsolute(target) ? target : resolve3(dirname3(from), target);
1890
+ const path = target.startsWith("~/") ? join5(home, target.slice(2)) : isAbsolute(target) ? target : resolve3(dirname2(from), target);
1876
1891
  if (seen.includes(path)) return "";
1877
1892
  let imported;
1878
1893
  try {
1879
- imported = readFileSync7(path, "utf8");
1894
+ imported = readFileSync6(path, "utf8");
1880
1895
  } catch {
1881
1896
  return line;
1882
1897
  }
@@ -1884,15 +1899,15 @@ function inlineImports(content, from, home, depth, seen) {
1884
1899
  return inlineImports(imported, path, home, depth + 1, seen).trim();
1885
1900
  }).join("\n");
1886
1901
  }
1887
- function readGlobalInstructions(agents, home = homedir5()) {
1902
+ function readGlobalInstructions(agents, home = homedir4()) {
1888
1903
  const found = [];
1889
1904
  for (const agent2 of agents) {
1890
1905
  const file = INSTRUCTION_FILES[agent2.id];
1891
1906
  if (!file) continue;
1892
- const path = join6(agent2.root, file);
1907
+ const path = join5(agent2.root, file);
1893
1908
  let raw;
1894
1909
  try {
1895
- raw = readFileSync7(path, "utf8");
1910
+ raw = readFileSync6(path, "utf8");
1896
1911
  } catch {
1897
1912
  continue;
1898
1913
  }
@@ -1911,7 +1926,7 @@ ${item.content.trim()}`).join("\n\n");
1911
1926
  }
1912
1927
  function readJson2(path) {
1913
1928
  try {
1914
- return JSON.parse(readFileSync7(path, "utf8"));
1929
+ return JSON.parse(readFileSync6(path, "utf8"));
1915
1930
  } catch {
1916
1931
  return void 0;
1917
1932
  }
@@ -1925,15 +1940,15 @@ function bearerOf(def) {
1925
1940
  return match?.[1] ?? null;
1926
1941
  }
1927
1942
  function serversOf(agent2, home) {
1928
- if (agent2.id === "claude_code") return readJson2(join6(home, ".claude.json"))?.mcpServers ?? {};
1929
- if (agent2.id === "cursor") return readJson2(join6(agent2.root, "mcp.json"))?.mcpServers ?? {};
1943
+ if (agent2.id === "claude_code") return readJson2(join5(home, ".claude.json"))?.mcpServers ?? {};
1944
+ if (agent2.id === "cursor") return readJson2(join5(agent2.root, "mcp.json"))?.mcpServers ?? {};
1930
1945
  try {
1931
- return parseToml2(readFileSync7(join6(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
1946
+ return parseToml2(readFileSync6(join5(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
1932
1947
  } catch {
1933
1948
  return {};
1934
1949
  }
1935
1950
  }
1936
- function findLocalSecrets(agents, home = homedir5()) {
1951
+ function findLocalSecrets(agents, home = homedir4()) {
1937
1952
  const secrets2 = [];
1938
1953
  for (const agent2 of agents) {
1939
1954
  for (const [name, def] of Object.entries(serversOf(agent2, home))) {
@@ -1949,9 +1964,9 @@ function findLocalSecrets(agents, home = homedir5()) {
1949
1964
 
1950
1965
  // src/subscriptions.ts
1951
1966
  import { spawn as spawn3 } from "child_process";
1952
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
1953
- import { homedir as homedir6 } from "os";
1954
- import { join as join7 } from "path";
1967
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
1968
+ import { homedir as homedir5 } from "os";
1969
+ import { join as join6 } from "path";
1955
1970
  import { createInterface as createInterface2 } from "readline";
1956
1971
  var ALIASES = {
1957
1972
  claude: "anthropic",
@@ -2036,12 +2051,12 @@ async function connectClaude() {
2036
2051
  const expiry = sub.expiresAt ? ` (expires ~${sub.expiresAt.slice(0, 10)})` : "";
2037
2052
  console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
2038
2053
  }
2039
- function codexSignIn(env = process.env, home = homedir6()) {
2040
- const path = join7(env.CODEX_HOME || join7(home, ".codex"), "auth.json");
2054
+ function codexSignIn(env = process.env, home = homedir5()) {
2055
+ const path = join6(env.CODEX_HOME || join6(home, ".codex"), "auth.json");
2041
2056
  if (!existsSync6(path)) {
2042
2057
  throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
2043
2058
  }
2044
- const raw = readFileSync8(path, "utf8");
2059
+ const raw = readFileSync7(path, "utf8");
2045
2060
  try {
2046
2061
  const parsed = JSON.parse(raw);
2047
2062
  return { path, raw, account: parsed.tokens?.account_id };
@@ -2049,12 +2064,12 @@ function codexSignIn(env = process.env, home = homedir6()) {
2049
2064
  throw new ApiError(400, "invalid_codex_auth", `${path} isn't valid JSON \u2014 re-run \`codex login\``);
2050
2065
  }
2051
2066
  }
2052
- function cursorSignIn(env = process.env, home = homedir6()) {
2053
- const path = join7(env.XDG_CONFIG_HOME || join7(home, ".config"), "cursor", "auth.json");
2067
+ function cursorSignIn(env = process.env, home = homedir5()) {
2068
+ const path = join6(env.XDG_CONFIG_HOME || join6(home, ".config"), "cursor", "auth.json");
2054
2069
  if (!existsSync6(path)) {
2055
2070
  throw new ApiError(404, "no_cursor_auth", `no ${path} \u2014 run \`cursor-agent login\` first`);
2056
2071
  }
2057
- const raw = readFileSync8(path, "utf8");
2072
+ const raw = readFileSync7(path, "utf8");
2058
2073
  try {
2059
2074
  const parsed = JSON.parse(raw);
2060
2075
  if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") throw new Error("shape");
@@ -2133,11 +2148,11 @@ function planProviders(names) {
2133
2148
  }));
2134
2149
  }
2135
2150
  function reportPath() {
2136
- return join8(dirname4(configPath()), "setup.json");
2151
+ return join7(dirname3(configPath()), "setup.json");
2137
2152
  }
2138
2153
  function loadReport() {
2139
2154
  try {
2140
- const saved = JSON.parse(readFileSync9(reportPath(), "utf8"));
2155
+ const saved = JSON.parse(readFileSync8(reportPath(), "utf8"));
2141
2156
  return { agents: saved.agents ?? [], plans: saved.plans ?? [], ...saved.secrets ? { secrets: saved.secrets } : {} };
2142
2157
  } catch {
2143
2158
  return { agents: [], plans: [] };
@@ -2150,8 +2165,8 @@ function mergeReport(base, next) {
2150
2165
  return { agents, plans, ...secrets2 ? { secrets: secrets2 } : {} };
2151
2166
  }
2152
2167
  function saveReport(report) {
2153
- mkdirSync3(dirname4(reportPath()), { recursive: true });
2154
- writeFileSync5(reportPath(), `${JSON.stringify(report, null, 2)}
2168
+ mkdirSync2(dirname3(reportPath()), { recursive: true });
2169
+ writeFileSync4(reportPath(), `${JSON.stringify(report, null, 2)}
2155
2170
  `, { mode: 384 });
2156
2171
  }
2157
2172
  function confirm3(question) {
@@ -2321,13 +2336,13 @@ async function runSetup(opts) {
2321
2336
  await connectPlans(agents, opts, interactive, emit, run2);
2322
2337
  await tell();
2323
2338
  }
2324
- const { skills, ignored } = wantsImport ? readLocalSkills(agents) : { skills: [], ignored: [] };
2339
+ const { skills: skills2, ignored } = wantsImport ? readLocalSkills(agents) : { skills: [], ignored: [] };
2325
2340
  const instructions = wantsImport ? readGlobalInstructions(agents) : [];
2326
2341
  const secrets2 = wantsSecrets ? findLocalSecrets(agents) : [];
2327
2342
  if (wantsImport || wantsSecrets) {
2328
2343
  emit({
2329
2344
  type: "found",
2330
- skills: skills.map((skill) => ({ name: skill.name, agent: skill.agent, files: skill.files.length, bytes: skill.bytes })),
2345
+ skills: skills2.map((skill) => ({ name: skill.name, agent: skill.agent, files: skill.files.length, bytes: skill.bytes })),
2331
2346
  ignored: ignored.map((item) => ({ dir: item.dir, reason: item.reason })),
2332
2347
  instructions: instructions.map((item) => ({ agent: item.agent, path: item.path, bytes: Buffer.byteLength(item.content), imports: item.imports })),
2333
2348
  secrets: secrets2.map((secret) => ({ name: secret.name, agent: secret.agent, url: secret.url }))
@@ -2341,8 +2356,8 @@ async function runSetup(opts) {
2341
2356
  const report = mergeReport(earlier, run2);
2342
2357
  saveReport(report);
2343
2358
  if (key) {
2344
- const sendFiles = wantsImport && (skills.length > 0 || instructions.length > 0) && await agreed("Send these skills and instructions to your AgentSky workspace?");
2345
- await importToWorkspace(key, held, sendFiles ? skills : [], sendFiles ? combineInstructions(instructions) : "", emit);
2359
+ const sendFiles = wantsImport && (skills2.length > 0 || instructions.length > 0) && await agreed("Send these skills and instructions to your AgentSky workspace?");
2360
+ await importToWorkspace(key, held, sendFiles ? skills2 : [], sendFiles ? combineInstructions(instructions) : "", emit);
2346
2361
  }
2347
2362
  emit({ type: "done", report });
2348
2363
  }
@@ -2421,10 +2436,10 @@ async function saveSecrets(found, opts, interactive, emit) {
2421
2436
  }
2422
2437
  return counts;
2423
2438
  }
2424
- async function importToWorkspace(key, held, skills, instructions, emit) {
2439
+ async function importToWorkspace(key, held, skills2, instructions, emit) {
2425
2440
  const outcomes = [];
2426
2441
  let signedOut = false;
2427
- for (const skill of skills) {
2442
+ for (const skill of skills2) {
2428
2443
  if (signedOut) {
2429
2444
  outcomes.push({ name: skill.name, outcome: "skipped: sign in to agentsky.dev again" });
2430
2445
  continue;
@@ -2499,6 +2514,50 @@ function renderArtifact(artifact2) {
2499
2514
  return `${artifact2.id} ${artifact2.name} v${artifact2.version} ${files2} entry ${artifact2.entry}${artifact2.share_url ? ` shared ${artifact2.share_url}` : ""}`;
2500
2515
  }
2501
2516
 
2517
+ // src/skills-store.ts
2518
+ import { readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
2519
+ import { basename as basename3, join as join8, relative, resolve as resolve4 } from "path";
2520
+ function skillBundle(dir) {
2521
+ const root = resolve4(dir);
2522
+ const top = basename3(root);
2523
+ const files2 = [];
2524
+ const walk = (path) => {
2525
+ for (const name of readdirSync3(path).sort()) {
2526
+ const full = join8(path, name);
2527
+ if (statSync3(full).isDirectory()) walk(full);
2528
+ else files2.push({ path: `${top}/${relative(root, full)}`, bytes: readFileSync9(full) });
2529
+ }
2530
+ };
2531
+ walk(root);
2532
+ if (!files2.some((file) => file.path === `${top}/SKILL.md`)) throw new Error(`${dir}: no SKILL.md at its root`);
2533
+ return files2;
2534
+ }
2535
+ async function uploadSkill(dir, displayName) {
2536
+ const form = new FormData();
2537
+ for (const file of skillBundle(dir)) form.append("files", new Blob([new Uint8Array(file.bytes)]), file.path);
2538
+ form.append("displayName", displayName ?? basename3(resolve4(dir)));
2539
+ return apiForm("POST", "/skills", form);
2540
+ }
2541
+ async function listSkills() {
2542
+ return (await api("GET", "/skills")).skills;
2543
+ }
2544
+ async function getTask(task2) {
2545
+ return (await api("GET", `/tasks/${encodeURIComponent(task2)}`)).task;
2546
+ }
2547
+ function answerBody(answer) {
2548
+ if (!answer.option === !answer.text) throw new Error("give exactly one of --option or --text");
2549
+ return { ...answer.option ? { option_id: answer.option } : { text: answer.text }, ...answer.note ? { note: answer.note } : {} };
2550
+ }
2551
+ async function answerRequest(task2, request, answer) {
2552
+ return api("POST", `/tasks/${encodeURIComponent(task2)}/requests/${encodeURIComponent(request)}/answer`, answerBody(answer));
2553
+ }
2554
+ function renderRequest(request) {
2555
+ const options = request.options.length ? `
2556
+ options: ${request.options.map((option) => `${option.id} (${option.label})`).join(", ")}` : "";
2557
+ return `${request.id} ${request.kind} from ${request.session_id}
2558
+ ${request.question}${options}`;
2559
+ }
2560
+
2502
2561
  // src/workspace.ts
2503
2562
  var KINDS = ["TOOL_CALL", "SPEND_CAP", "CALL_COUNT", "CAPABILITY"];
2504
2563
  async function workspaceSlug(explicit) {
@@ -2593,6 +2652,173 @@ function renderPolicy(rule) {
2593
2652
  return `${state} ${rule.layer.padEnd(9)} ${rule.kind.padEnd(10)} ${rule.name} ${JSON.stringify(rule.config)} ${rule.id}`;
2594
2653
  }
2595
2654
 
2655
+ // src/harness.ts
2656
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
2657
+ import { dirname as dirname4, join as join9, resolve as resolve5 } from "path";
2658
+ var HARNESS_FILE = "harness.json";
2659
+ function harnessFilePath(dir) {
2660
+ return join9(resolve5(dir), HARNESS_FILE);
2661
+ }
2662
+ function readHarnessFile(dir) {
2663
+ const path = harnessFilePath(dir);
2664
+ if (!existsSync7(path)) {
2665
+ throw new Error(`no ${HARNESS_FILE} found in ${resolve5(dir)} \u2014 run \`sky harness init\` first`);
2666
+ }
2667
+ try {
2668
+ return JSON.parse(readFileSync10(path, "utf8"));
2669
+ } catch (err) {
2670
+ throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
2671
+ }
2672
+ }
2673
+ var INIT_SKELETON = {
2674
+ id: "my_harness",
2675
+ name: "My Harness",
2676
+ version: "0.1.0",
2677
+ distribution: {
2678
+ // choose one distribution kind and remove the others:
2679
+ npx: "@your-scope/my-harness@0.1.0"
2680
+ // uvx: "my-harness==0.1.0",
2681
+ // binary: { url: "https://example.com/my-harness-linux-amd64", sha256: "..." },
2682
+ }
2683
+ // optional: declare the tools this harness exposes
2684
+ // tools: [{ id: "bash", kind: "execute", title: "Shell" }],
2685
+ // optional: feature flags
2686
+ // features: ["subagents"],
2687
+ };
2688
+ var INIT_COMMENT = `# harness.json \u2014 workspace harness profile skeleton
2689
+ # Validate locally: sky harness verify .
2690
+ # Submit: sky harness push .
2691
+ # Full schema: https://agentsky.dev/docs/harness-profile
2692
+ `;
2693
+ function harnessInit(dir) {
2694
+ const path = harnessFilePath(dir);
2695
+ if (existsSync7(path)) {
2696
+ throw new Error(`${path} already exists \u2014 delete it to re-init`);
2697
+ }
2698
+ mkdirSync3(dirname4(path), { recursive: true });
2699
+ writeFileSync5(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
2700
+ `);
2701
+ console.log(`Created ${path}`);
2702
+ console.log(INIT_COMMENT);
2703
+ }
2704
+ async function harnessVerify(dir, opts) {
2705
+ const raw = readHarnessFile(dir);
2706
+ const parsed = harnessProfileSchema.safeParse(raw);
2707
+ if (!parsed.success) {
2708
+ console.error("harness.json schema errors:");
2709
+ for (const issue of parsed.error.issues) {
2710
+ const path = issue.path.length ? issue.path.join(".") : "(root)";
2711
+ console.error(` ${path}: ${issue.message}`);
2712
+ }
2713
+ process.exit(1);
2714
+ }
2715
+ console.log(`harness.json ok (id: ${parsed.data.id}, version: ${parsed.data.version})`);
2716
+ if (!opts.handle) return;
2717
+ const rawHandle = opts.handle.replace(/^@/, "");
2718
+ console.log(`Starting conformance run for @${rawHandle}\u2026`);
2719
+ let runId;
2720
+ try {
2721
+ const { run: run2 } = await api(
2722
+ "POST",
2723
+ `/catalog/listings/${encodeURIComponent(rawHandle)}/conformance`
2724
+ );
2725
+ runId = run2.id;
2726
+ console.log(` run ${runId} status: ${run2.status}`);
2727
+ } catch (err) {
2728
+ if (err instanceof ApiError) {
2729
+ console.error(`sky: conformance start failed \u2014 ${err.message} (${err.code})`);
2730
+ } else {
2731
+ console.error(`sky: conformance start failed \u2014 ${String(err)}`);
2732
+ }
2733
+ process.exit(1);
2734
+ }
2735
+ const pollMs = 4e3;
2736
+ const maxWaitMs = 5 * 60 * 1e3;
2737
+ const deadline = Date.now() + maxWaitMs;
2738
+ while (Date.now() < deadline) {
2739
+ await new Promise((r) => setTimeout(r, pollMs));
2740
+ try {
2741
+ const { run: run2 } = await api("GET", `/catalog/listings/${encodeURIComponent(rawHandle)}/conformance`);
2742
+ if (run2.status === "running") {
2743
+ process.stdout.write(".");
2744
+ continue;
2745
+ }
2746
+ process.stdout.write("\n");
2747
+ if (run2.status === "failed") {
2748
+ console.error(`conformance run failed: ${run2.reason ?? "unknown reason"}`);
2749
+ process.exit(1);
2750
+ }
2751
+ console.log(`
2752
+ Conformance report for @${rawHandle}`);
2753
+ console.log(
2754
+ ` required_passed: ${run2.required_passed ? "yes" : "no"} all_passed: ${run2.all_passed ? "yes" : "no"}`
2755
+ );
2756
+ if (run2.report?.checks) {
2757
+ for (const check of run2.report.checks) {
2758
+ const icon = check.status === "pass" ? "\u2713" : check.status === "skipped" ? "\u2013" : "\u2717";
2759
+ const req = check.required ? " [required]" : "";
2760
+ console.log(` ${icon} ${check.id}${req} \u2014 ${check.reason ?? check.status} (${check.seconds}s)`);
2761
+ }
2762
+ }
2763
+ if (!run2.required_passed) process.exit(1);
2764
+ return;
2765
+ } catch (err) {
2766
+ console.error(`
2767
+ sky: conformance poll failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
2768
+ process.exit(1);
2769
+ }
2770
+ }
2771
+ console.error("\nsky: timed out waiting for conformance run (5 min)");
2772
+ process.exit(1);
2773
+ }
2774
+ async function harnessPush(dir, opts) {
2775
+ const raw = readHarnessFile(dir);
2776
+ const parsed = harnessProfileSchema.safeParse(raw);
2777
+ if (!parsed.success) {
2778
+ console.error("harness.json is invalid \u2014 run `sky harness verify` first:");
2779
+ for (const issue of parsed.error.issues) {
2780
+ const path = issue.path.length ? issue.path.join(".") : "(root)";
2781
+ console.error(` ${path}: ${issue.message}`);
2782
+ }
2783
+ process.exit(1);
2784
+ }
2785
+ const profile = parsed.data;
2786
+ const version = opts.version ?? profile.version;
2787
+ const handle = profile.id;
2788
+ let existing;
2789
+ try {
2790
+ const { harnesses } = await api("GET", "/harnesses");
2791
+ existing = harnesses.find((h) => h.handle === handle);
2792
+ } catch (err) {
2793
+ if (!(err instanceof ApiError && err.status === 404)) throw err;
2794
+ }
2795
+ if (!existing) {
2796
+ const { harness: harness2 } = await api("POST", "/harnesses", {
2797
+ handle,
2798
+ name: profile.name,
2799
+ profile,
2800
+ model_billing: "gateway"
2801
+ });
2802
+ existing = harness2;
2803
+ console.log(`Created harness ${harness2.id} handle: ${harness2.handle}`);
2804
+ } else {
2805
+ console.log(`Harness exists ${existing.id} handle: ${existing.handle}`);
2806
+ }
2807
+ const { version: ver } = await api(
2808
+ "POST",
2809
+ `/harnesses/${encodeURIComponent(existing.id)}/versions`,
2810
+ { version, profile }
2811
+ );
2812
+ console.log(
2813
+ `Version ${ver.version} status: ${ver.status} id: ${ver.id} created: ${ver.created_at}`
2814
+ );
2815
+ console.log(
2816
+ `
2817
+ Next step: run conformance
2818
+ sky harness verify . --handle @${handle}`
2819
+ );
2820
+ }
2821
+
2596
2822
  // src/index.ts
2597
2823
  function printModelBilling(session2) {
2598
2824
  if (session2.modelBilling !== "account") return;
@@ -2625,7 +2851,7 @@ auth.command("whoami").description("who the current token acts as").action(
2625
2851
  run(async () => {
2626
2852
  const who = await api("GET", "/whoami");
2627
2853
  console.log(
2628
- `${who.user.email ?? "unknown"} \xB7 universe ${who.universe.slug}${who.universe.isPersonal ? " (personal)" : ""} \xB7 scopes ${who.scopes.join(",")}`
2854
+ `${who.user.email ?? "unknown"} \xB7 universe ${who.universe.slug}${who.universe.isPersonal ? " (default)" : ""} \xB7 scopes ${who.scopes.join(",")}`
2629
2855
  );
2630
2856
  })
2631
2857
  );
@@ -2645,7 +2871,7 @@ universe.command("list").action(
2645
2871
  const current = currentUniverse();
2646
2872
  for (const u of universes) {
2647
2873
  const marker = u.slug === current || !current && u.isPersonal ? "*" : " ";
2648
- console.log(`${marker} ${u.slug}${u.isPersonal ? " (personal)" : ""}`);
2874
+ console.log(`${marker} ${u.slug}${u.isPersonal ? " (default)" : ""}`);
2649
2875
  }
2650
2876
  })
2651
2877
  );
@@ -2671,7 +2897,7 @@ universe.command("show").action(
2671
2897
  console.log(`${u.slug} \u2014 ${u.name} \xB7 ${u.agents} agents \xB7 ${u.members} members`);
2672
2898
  } else {
2673
2899
  const who = await api("GET", path);
2674
- console.log(`${who.universe.slug} (personal)`);
2900
+ console.log(`${who.universe.slug} (default)`);
2675
2901
  }
2676
2902
  })
2677
2903
  );
@@ -2719,7 +2945,7 @@ workspace.command("list").action(
2719
2945
  const current = currentUniverse();
2720
2946
  for (const u of universes) {
2721
2947
  const marker = u.slug === current || !current && u.isPersonal ? "*" : " ";
2722
- console.log(`${marker} ${u.slug}${u.isPersonal ? " (personal)" : ""}`);
2948
+ console.log(`${marker} ${u.slug}${u.isPersonal ? " (default)" : ""}`);
2723
2949
  }
2724
2950
  })
2725
2951
  );
@@ -2864,7 +3090,7 @@ function renderResource(r) {
2864
3090
  const ref = r.checkout ? `@${r.checkout.type === "branch" ? r.checkout.name : r.checkout.sha}` : "";
2865
3091
  return `${r.id} repo ${r.mount_path} ${r.url}${ref}`;
2866
3092
  }
2867
- session.command("create <agent>").description("create a session (starts provisioning by default)").option("--title <title>", "session title").option("--message <text>", "start the first turn immediately, provisioning even with --deferred").option("--deferred", "leave the session idle without a pod until its first turn").option("--vcpus <n>", "machine vCPUs: 1, 2, 4 or 8 (default 2)").option("--memory <mb>", "machine memory in MB, 256MB steps (default 4096)").option("--file <path...>", "mount local files read-only in the working directory: path[:/mount] (uploaded first)").option("--repo <url...>", "clone GitHub repositories into the working directory: https://github.com/{owner}/{repo}[@branch|@sha]").option("--repo-token <token>", "token for the --repo clones; compiled into the sandbox's egress policy, never sent to it").option("--json", "print the created session as JSON").action(
3093
+ session.command("create <agent>").description("create a session (starts provisioning by default)").option("--title <title>", "session title").option("--message <text>", "start the first turn immediately, provisioning even with --deferred").option("--deferred", "leave the session idle without a pod until its first turn").option("--vcpus <n>", "machine vCPUs: 1, 2, 4 or 8 (default 2)").option("--memory <mb>", "machine memory in MB, 256MB steps (default 4096)").option("--file <path...>", "mount local files read-only in the working directory: path[:/mount] (uploaded first)").option("--repo <url...>", "clone GitHub repositories into the working directory: https://github.com/{owner}/{repo}[@branch|@sha]").option("--repo-token <token>", "token for the --repo clones; compiled into the sandbox's egress policy, never sent to it").option("--json", "print the created session as JSON").option("--no-defaults", "attach only what this command names: no workspace default environment, vault, files, skills or memory").option("--budget-cents <cents>", "the most the session may spend, in whole US cents").action(
2868
3094
  run(
2869
3095
  async (agentRef, opts) => {
2870
3096
  const resources = await resourceParams(opts);
@@ -2874,6 +3100,8 @@ session.command("create <agent>").description("create a session (starts provisio
2874
3100
  ...opts.vcpus !== void 0 ? { vcpus: Number(opts.vcpus) } : {},
2875
3101
  ...opts.memory !== void 0 ? { memoryMb: Number(opts.memory) } : {},
2876
3102
  ...opts.deferred ? { provision: "deferred" } : {},
3103
+ ...opts.defaults === false ? { defaults: "none" } : {},
3104
+ ...opts.budgetCents ? { budget: { type: "limit", max_list_cost: { amount: opts.budgetCents, currency: "USD" } } } : {},
2877
3105
  ...resources.length ? { resources } : {},
2878
3106
  ...opts.message ? { initial_events: [{ type: "user.message", content: [{ type: "text", text: opts.message }] }] } : {}
2879
3107
  });
@@ -3206,9 +3434,9 @@ secrets.command("unset <id> <key>").action(
3206
3434
  );
3207
3435
  program.command("launch").description("scaffold ./agent.toml if absent, create the agent, print its address").option("-f, --file <path>", "agent file", "agent.toml").option("--type <type>", "override the file's harness: hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode").option("--llm <llm>", "override the file's model (must suit the harness)").action(
3208
3436
  run(async (opts) => {
3209
- const path = resolve4(opts.file);
3210
- if (!existsSync7(path)) {
3211
- const name = basename3(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
3437
+ const path = resolve6(opts.file);
3438
+ if (!existsSync8(path)) {
3439
+ const name = basename4(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
3212
3440
  scaffoldAgentFile(path, name);
3213
3441
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
3214
3442
  }
@@ -3239,7 +3467,7 @@ program.command("launch").description("scaffold ./agent.toml if absent, create t
3239
3467
  );
3240
3468
  program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent)").option("-f, --file <path>", "agent file", "agent.toml").action(
3241
3469
  run(async (opts) => {
3242
- const path = resolve4(opts.file);
3470
+ const path = resolve6(opts.file);
3243
3471
  const { file, spec } = readAgentFile(path);
3244
3472
  if (!file.id) {
3245
3473
  console.log("No agent id in the file yet \u2014 running launch instead.");
@@ -3289,7 +3517,7 @@ program.command("import").description("import a local Claude Code or Codex sessi
3289
3517
  if (opts.trace && !opts.key) throw new Error("--trace requires --key <importKey>");
3290
3518
  const limit = Math.max(1, Math.min(50, parseInt(opts.limit ?? "10", 10) || 10));
3291
3519
  if (opts.harness && !["claude_code", "codex", "all"].includes(opts.harness)) throw new Error("Use --harness claude_code, codex, or all.");
3292
- const harness = opts.harness ?? "all";
3520
+ const harness2 = opts.harness ?? "all";
3293
3521
  const base = opts.url ?? agentUseBase();
3294
3522
  const nonInteractive = !process.stdin.isTTY;
3295
3523
  const autoConfirm = opts.yes || opts.current || !!opts.session && nonInteractive;
@@ -3308,18 +3536,18 @@ program.command("import").description("import a local Claude Code or Codex sessi
3308
3536
  }
3309
3537
  } else {
3310
3538
  const cwd = process.cwd();
3311
- const all = listRecentSessions(harness === "all" ? void 0 : harness, 50);
3539
+ const all = listRecentSessions(harness2 === "all" ? void 0 : harness2, 50);
3312
3540
  const byCwd = all.filter((s) => s.workspace === cwd || s.workspace.startsWith(cwd + "/"));
3313
3541
  candidates = byCwd.length > 0 ? [byCwd[0]] : all.length > 0 ? [all[0]] : [];
3314
3542
  }
3315
3543
  if (candidates.length === 0) throw new Error("No local session found for the current environment. Run with --session <id> or start an agent session first.");
3316
3544
  } else if (opts.session) {
3317
- candidates = listRecentSessions(harness === "all" ? void 0 : harness, Number.POSITIVE_INFINITY).filter(
3545
+ candidates = listRecentSessions(harness2 === "all" ? void 0 : harness2, Number.POSITIVE_INFINITY).filter(
3318
3546
  (s) => s.sessionId === opts.session
3319
3547
  );
3320
3548
  if (candidates.length === 0) throw new Error(`Session "${opts.session}" not found in ~/.claude or ~/.codex.`);
3321
3549
  } else {
3322
- candidates = listRecentSessions(harness === "all" ? void 0 : harness, limit);
3550
+ candidates = listRecentSessions(harness2 === "all" ? void 0 : harness2, limit);
3323
3551
  if (candidates.length === 0) {
3324
3552
  console.log("No recent sessions found. Make sure you have run Claude Code (sessions in ~/.claude) or Codex (sessions in ~/.codex).");
3325
3553
  return;
@@ -3342,11 +3570,11 @@ program.command("import").description("import a local Claude Code or Codex sessi
3342
3570
  console.log();
3343
3571
  const rl = createInterface4({ input: process.stdin, output: process.stdout });
3344
3572
  const answer = await new Promise(
3345
- (resolve5) => rl.question(
3573
+ (resolve7) => rl.question(
3346
3574
  `Import which session? [1-${candidates.length}, or comma-separated, or "all"]: `,
3347
3575
  (a) => {
3348
3576
  rl.close();
3349
- resolve5(a.trim());
3577
+ resolve7(a.trim());
3350
3578
  }
3351
3579
  )
3352
3580
  );
@@ -3369,9 +3597,9 @@ program.command("import").description("import a local Claude Code or Codex sessi
3369
3597
  if (!autoConfirm) {
3370
3598
  if (nonInteractive) throw new Error("Use --yes to confirm this transfer in a non-interactive terminal.");
3371
3599
  const prompt = createInterface4({ input: process.stdin, output: process.stdout });
3372
- const answer = await new Promise((resolve5) => prompt.question("Upload these selected conversations? [y/N] ", (value) => {
3600
+ const answer = await new Promise((resolve7) => prompt.question("Upload these selected conversations? [y/N] ", (value) => {
3373
3601
  prompt.close();
3374
- resolve5(value);
3602
+ resolve7(value);
3375
3603
  }));
3376
3604
  if (!/^y(?:es)?$/i.test(answer.trim())) {
3377
3605
  console.log("Cancelled. Nothing uploaded.");
@@ -3453,7 +3681,7 @@ files.command("download <id>").description("download a session file's bytes").re
3453
3681
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
3454
3682
  })
3455
3683
  );
3456
- var task = program.command("task").description("a task's shared files (the task is its first session's id)");
3684
+ var task = program.command("task").description("a task's shared files and open requests (the task is its first session's id)");
3457
3685
  task.command("files <task>").description("list the task's current files").action(
3458
3686
  run(async (taskId) => {
3459
3687
  const list = await listTaskFiles(taskId);
@@ -3472,7 +3700,7 @@ task.command("get <task> <path>").description("read one task file; prints it, or
3472
3700
  );
3473
3701
  task.command("put <task> <path> <local>").description("write a local file to <path> in the task's files (every session of the task sees it)").action(
3474
3702
  run(async (taskId, path, local) => {
3475
- const { file, conflict } = await writeTaskFile(taskId, path, readFileSync10(local), mediaTypeFor(local));
3703
+ const { file, conflict } = await writeTaskFile(taskId, path, readFileSync11(local), mediaTypeFor(local));
3476
3704
  console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
3477
3705
  })
3478
3706
  );
@@ -3482,6 +3710,45 @@ task.command("rm <task> <path>").description("remove a path from the task's file
3482
3710
  console.log(`Removed ${path}`);
3483
3711
  })
3484
3712
  );
3713
+ task.command("show <task>").description("the task's sessions and the requests waiting on you (questions, approvals, apps to connect)").option("--json", "print the task as JSON").action(
3714
+ run(async (taskId, opts) => {
3715
+ const found = await getTask(taskId);
3716
+ if (opts.json) {
3717
+ console.log(JSON.stringify(found));
3718
+ return;
3719
+ }
3720
+ for (const member of found.sessions ?? []) console.log(`${member.id} ${member.role}${member.status ? ` ${member.status}` : ""}`);
3721
+ if (!found.requests.length) console.log("no open requests");
3722
+ for (const request of found.requests) console.log(renderRequest(request));
3723
+ })
3724
+ );
3725
+ task.command("answer <task> <request>").description("answer an open request; the asking session continues with the answer").option("--option <id>", "the id of one of the request's options").option("--text <text>", "a free-text answer").option("--note <text>", "sent to the agent after the answer, in the same turn").action(
3726
+ run(async (taskId, requestId, opts) => {
3727
+ await answerRequest(taskId, requestId, opts);
3728
+ console.log(`Answered ${requestId}`);
3729
+ })
3730
+ );
3731
+ var skills = program.command("skills").description("the workspace's skill store: upload a skill, then attach it to an agent by id");
3732
+ skills.command("upload <dir>").description("upload a skill directory (SKILL.md at its root) as a new skill").option("--name <name>", "display name (default: the directory's name)").option("--json", "print the stored skill as JSON").action(
3733
+ run(async (dir, opts) => {
3734
+ const { skill } = await uploadSkill(dir, opts.name);
3735
+ if (opts.json) {
3736
+ console.log(JSON.stringify(skill));
3737
+ return;
3738
+ }
3739
+ console.log(`Uploaded ${skill.id} (${skill.displayName ?? dir}). Attach it in agent.toml:`);
3740
+ console.log(` [[skills]]
3741
+ type = "skill"
3742
+ skillId = "${skill.id}"`);
3743
+ })
3744
+ );
3745
+ skills.command("list").description("the workspace's stored skills").action(
3746
+ run(async () => {
3747
+ const list = await listSkills();
3748
+ if (!list.length) console.log("no skills");
3749
+ for (const skill of list) console.log(`${skill.id} ${skill.displayName ?? ""}`);
3750
+ })
3751
+ );
3485
3752
  var artifact = program.command("artifact").description("artifacts: publish a task's files as a named, shareable result");
3486
3753
  artifact.command("list").description("the workspace's artifacts, or one task's with --task").option("--task <task>", "only this task's artifacts").action(
3487
3754
  run(async (opts) => {
@@ -3690,7 +3957,274 @@ program.command("config").description("show the CLI config location and current
3690
3957
  console.log(`config: ${configPath()}`);
3691
3958
  console.log(` apiBase: ${config.apiBase ?? "(default)"}`);
3692
3959
  console.log(` token: ${config.token ? `${config.token.slice(0, 10)}\u2026` : "(none)"}`);
3693
- console.log(` universe: ${config.currentUniverse ?? "(personal)"}`);
3960
+ console.log(` universe: ${config.currentUniverse ?? "(default)"}`);
3961
+ });
3962
+ var harness = program.command("harness").description("workspace harnesses: init, verify, push");
3963
+ harness.command("init [dir]").description("write a harness.json profile skeleton in the given directory (default: .)").action(run(async (dir) => {
3964
+ harnessInit(dir ?? ".");
3965
+ }));
3966
+ harness.command("verify [dir]").description("validate harness.json against the profile schema; with --handle, also run conformance").option("--handle <@listing>", "catalog listing handle to run conformance against").action(run(async (dir, opts) => {
3967
+ await harnessVerify(dir ?? ".", { handle: opts.handle });
3968
+ }));
3969
+ harness.command("push [dir]").description("create or update the workspace harness from harness.json").option("--version <semver>", "override the version (default: the version in harness.json)").action(run(async (dir, opts) => {
3970
+ await harnessPush(dir ?? ".", { version: opts.version });
3971
+ }));
3972
+ harness.command("connect").description("run an ACP agent (the command after --) as a session's brain, over the platform relay").option("--session <id>", "the session to be the brain of (default: $AGENTSKY_HARNESS_RELAY_URL names it)").option("--token <token>", "the harness connect token, hct_\u2026 (default: $AGENTSKY_HARNESS_TOKEN)").option("--once", "exit when the connection ends instead of reconnecting").allowExcessArguments(true).action(run(async (opts) => {
3973
+ const { connectHarness, serveHarness, agentCommandFrom } = await import("./harness-connect-DUC7GIBL.js");
3974
+ const token = opts.token ?? process.env.AGENTSKY_HARNESS_TOKEN ?? "";
3975
+ const url = opts.session ? void 0 : process.env.AGENTSKY_HARNESS_RELAY_URL;
3976
+ const apiBase2 = (loadConfig().apiBase ?? "https://api.agentsky.dev").replace(/\/$/, "");
3977
+ const ac = new AbortController();
3978
+ process.on("SIGINT", () => ac.abort());
3979
+ process.on("SIGTERM", () => ac.abort());
3980
+ const options = { url, apiBase: apiBase2, session: opts.session, token, command: agentCommandFrom(process.argv), signal: ac.signal };
3981
+ process.exitCode = opts.once ? await connectHarness(options) : await serveHarness(options);
3982
+ }));
3983
+ var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
3984
+ traceCmd.command("start").description("start a trace: a grant for this machine, a root span, and the spool the hooks write to").option("--name <name>", "a name for the task").option("--source <source>", "how this machine reports itself", "claude-code").option("--hooks [settings]", "also install the Claude Code hooks (default: ./.claude/settings.local.json)").option("--capture <scopes>", "also send content, redacted on this machine first: any of prompts,tool_output,files").option("--file <path>", "a file the files scope may send (repeatable)", collect).option("--dir <path>", "a folder whose files the files scope may send when a file tool touches them (repeatable)", collect).action(run(async (opts) => {
3985
+ const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-UJTFSXNM.js");
3986
+ if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
3987
+ const scopes = parseContentScopes(opts.capture);
3988
+ const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
3989
+ if ((chosen.files.length || chosen.dirs.length) && !scopes.includes("files")) throw new Error("--file and --dir need --capture files");
3990
+ if (scopes.includes("files") && !chosen.files.length && !chosen.dirs.length) throw new Error("--capture files needs at least one --file or --dir");
3991
+ const { trace } = await api("POST", "/traces", opts.name ? { name: opts.name } : {});
3992
+ const { grant, token } = await api("POST", `/traces/${trace.id}/grants`, {
3993
+ source: opts.source,
3994
+ ...scopes.length ? { content_scopes: scopes } : {}
3995
+ });
3996
+ const root = `task:${trace.id}`;
3997
+ const state = {
3998
+ trace_id: trace.id,
3999
+ grant_id: grant.id,
4000
+ token,
4001
+ source: opts.source,
4002
+ api_base: apiBase(),
4003
+ next_sequence: 0,
4004
+ root_span: root,
4005
+ ...scopes.length ? { content_scopes: scopes, files: chosen.files, dirs: chosen.dirs } : {}
4006
+ };
4007
+ writeState(state);
4008
+ record(() => [
4009
+ { span_id: root, parent_span_id: null, kind: "start", name: opts.name ?? "task" },
4010
+ ...chosen.files.flatMap((path) => fileEvent(root, path, state) ?? [])
4011
+ ]);
4012
+ console.log(`Tracing ${trace.id} as ${opts.source} until ${grant.expires_at}`);
4013
+ console.log(scopes.length ? ` content: ${scopes.join(", ")} (redacted here before it leaves)` : " metadata only");
4014
+ if (opts.hooks) console.log(` hooks: ${installClaudeHooks(resolve6(typeof opts.hooks === "string" ? opts.hooks : ".claude/settings.local.json"))}`);
4015
+ console.log(` a session started elsewhere joins it with metadata {"trace_id": "${trace.id}"}`);
4016
+ console.log(` sky trace show ${trace.id}`);
4017
+ }));
4018
+ traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
4019
+ try {
4020
+ const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-UJTFSXNM.js");
4021
+ const chunks = [];
4022
+ for await (const chunk of process.stdin) chunks.push(chunk);
4023
+ const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
4024
+ if (!record((state2) => [...eventsForClaudeHook(input, state2), ...contentForClaudeHook(input, state2)])) return;
4025
+ const state = readState();
4026
+ if (!state) return;
4027
+ const outcome = await flush(grantSender(state, 3e3));
4028
+ if (outcome.stopped === "grant_revoked" || outcome.stopped === "grant_expired") clearState();
4029
+ } catch {
4030
+ }
4031
+ });
4032
+ traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
4033
+ const { readState, flush, grantSender } = await import("./trace-UJTFSXNM.js");
4034
+ const state = readState();
4035
+ if (!state) throw new Error("no trace is running here");
4036
+ const outcome = await flush(grantSender(state), { attempts: 4 });
4037
+ console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
4038
+ }));
4039
+ traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
4040
+ const { readState, spoolStatus } = await import("./trace-UJTFSXNM.js");
4041
+ const state = readState();
4042
+ if (!state) {
4043
+ console.log("No trace is running here.");
4044
+ return;
4045
+ }
4046
+ const spool = spoolStatus();
4047
+ console.log(`${state.trace_id} as ${state.source}: ${spool.pending} pending, ${spool.dropped_full} dropped (spool full), ${spool.dropped_expired} given up, ${spool.refused} refused`);
4048
+ }));
4049
+ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions they reach, cost counted once per session").option("--json", "print the bundle as JSON").action(run(async (id, opts) => {
4050
+ const { readState } = await import("./trace-UJTFSXNM.js");
4051
+ const ref = id ?? readState()?.trace_id;
4052
+ if (!ref) throw new Error("name a trace, or start one here");
4053
+ const bundle = await api("GET", `/traces/${ref}`);
4054
+ if (opts.json) {
4055
+ console.log(JSON.stringify(bundle, null, 2));
4056
+ return;
4057
+ }
4058
+ console.log(`${bundle.trace.id}${bundle.trace.name ? ` ${bundle.trace.name}` : ""}`);
4059
+ const children = /* @__PURE__ */ new Map();
4060
+ for (const span of bundle.spans) {
4061
+ const parent = span.parent_span_id && bundle.spans.some((other) => other.span_id === span.parent_span_id) ? span.parent_span_id : null;
4062
+ children.set(parent, [...children.get(parent) ?? [], span]);
4063
+ }
4064
+ const print = (parent, depth) => {
4065
+ for (const span of children.get(parent) ?? []) {
4066
+ const withContent = span.events.filter((event) => event.content).length;
4067
+ console.log(`${" ".repeat(depth + 1)}${span.name ?? span.span_id} [${span.source} \xB7 ${span.outcome}${span.outcome_source === "platform" ? " with its session" : ""}${span.edge === "root" || span.edge === "resolved" ? "" : ` \xB7 parent ${span.edge}`}] ${span.events.length} events${withContent ? `, ${withContent} with content` : ""}`);
4068
+ print(span.span_id, depth + 1);
4069
+ }
4070
+ };
4071
+ print(null, 0);
4072
+ for (const session2 of bundle.sessions) console.log(` session ${session2.id} ${session2.agent_type} ${session2.status} ${session2.role} of ${session2.task_id} (${session2.linked_by})`);
4073
+ console.log(` cost: $${bundle.consumed_cost.amount} \xB7 coverage: ${bundle.coverage}`);
4074
+ for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
4075
+ }));
4076
+ traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
4077
+ const { readState, record, flush, grantSender, clearState } = await import("./trace-UJTFSXNM.js");
4078
+ const state = readState();
4079
+ if (!state) throw new Error("no trace is running here");
4080
+ if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
4081
+ const outcome = await flush(grantSender(readState() ?? state), { attempts: 4 });
4082
+ await api("DELETE", `/traces/${state.trace_id}/grants/${state.grant_id}`);
4083
+ clearState();
4084
+ console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
4085
+ }));
4086
+ traceCmd.command("sync [payload]").description("upload this machine's Claude Code and Codex sessions to the matching workspace's trace library, redacted here first").option("--enable", "turn sync on for this machine").option("--disable", "turn sync off; nothing more is uploaded").option("--route <match=workspace>", "send sessions whose folder path contains <match> to <workspace> (repeatable; replaces the saved rules)", collect).option("--default <workspace>", "where a session no rule matches goes").option("--keep-local", "keep sessions no rule matches on this machine (the default)").option("--session <ref>", "only this session: its id or transcript path (repeatable)", collect).option("--full", "re-read every session, not only files that changed").option("--limit <n>", "at most this many changed sessions this run", (v) => Number.parseInt(v, 10)).option("--dry-run", "show what would be uploaded without uploading").option("--hook", "run from a Claude Code SessionEnd hook or Codex notify: upload that session in the background and return at once").option("--install-schedule", "also sweep every 15 minutes (macOS launchd)").option("--status", "what sync has done on this machine").option("--quiet", "print only failures").action(async (payload, opts) => {
4087
+ const lib = await import("./trace-library-474QJTAZ.js");
4088
+ if (opts.hook) {
4089
+ try {
4090
+ let raw = payload ?? "";
4091
+ if (!raw && !process.stdin.isTTY) {
4092
+ const chunks = [];
4093
+ for await (const chunk of process.stdin) chunks.push(chunk);
4094
+ raw = Buffer.concat(chunks).toString("utf8");
4095
+ }
4096
+ if (!lib.loadSyncConfig().enabled) return;
4097
+ const ref = lib.hookSessionRef(raw);
4098
+ const { spawn: spawn4 } = await import("child_process");
4099
+ spawn4(process.execPath, [process.argv[1], "trace", "sync", "--quiet", ...ref ? ["--session", ref] : []], { detached: true, stdio: "ignore" }).unref();
4100
+ } catch {
4101
+ }
4102
+ return;
4103
+ }
4104
+ await run(async () => {
4105
+ const config = lib.loadSyncConfig();
4106
+ if (opts.disable) {
4107
+ lib.saveSyncConfig({ ...config, enabled: false });
4108
+ console.log("Trace sync is off on this machine. Uploaded traces stay in their workspaces until deleted there.");
4109
+ return;
4110
+ }
4111
+ const changedSettings = opts.enable || opts.route || opts.default || opts.keepLocal;
4112
+ if (changedSettings) {
4113
+ if (opts.enable && !apiToken()) throw new Error("not logged in \u2014 run `sky auth login` first");
4114
+ const rules = opts.route ? opts.route.map((spec) => {
4115
+ const at = spec.lastIndexOf("=");
4116
+ if (at <= 0 || at === spec.length - 1) throw new Error(`--route takes match=workspace, got "${spec}"`);
4117
+ return { match: spec.slice(0, at), workspace: spec.slice(at + 1) };
4118
+ }) : config.rules;
4119
+ const next = {
4120
+ ...config,
4121
+ rules,
4122
+ enabled: opts.enable ? true : config.enabled,
4123
+ enabledAt: opts.enable ? (/* @__PURE__ */ new Date()).toISOString() : config.enabledAt,
4124
+ fallback: opts.keepLocal ? void 0 : opts.default ?? config.fallback
4125
+ };
4126
+ lib.saveSyncConfig(next);
4127
+ console.log(`Trace sync is ${next.enabled ? "on" : "off"} on this machine. It reads only:`);
4128
+ for (const folder of lib.sourceFolders()) console.log(` ${folder}`);
4129
+ console.log("and sends each session, redacted here first, to:");
4130
+ for (const rule of next.rules) console.log(` folder path contains "${rule.match}" \u2192 ${rule.workspace}`);
4131
+ console.log(` anything else \u2192 ${next.fallback ?? "stays on this machine"}`);
4132
+ console.log(`A repository's .agentsky.json "workspace" wins over these rules.`);
4133
+ }
4134
+ if (opts.installSchedule) {
4135
+ const { join: join10 } = await import("path");
4136
+ const { homedir: homedir6 } = await import("os");
4137
+ const { execFileSync } = await import("child_process");
4138
+ const log = join10(homedir6(), ".config", "sky", "trace-sync.log");
4139
+ const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
4140
+ try {
4141
+ execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
4142
+ } catch {
4143
+ }
4144
+ execFileSync("launchctl", ["load", plist]);
4145
+ console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
4146
+ }
4147
+ if (opts.status) {
4148
+ const { readFileSync: readFileSync12 } = await import("fs");
4149
+ const { join: join10 } = await import("path");
4150
+ let state = { sessions: {} };
4151
+ try {
4152
+ state = JSON.parse(readFileSync12(join10(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
4153
+ } catch {
4154
+ }
4155
+ const counts = /* @__PURE__ */ new Map();
4156
+ for (const entry2 of Object.values(state.sessions)) {
4157
+ const label = entry2.status === "stored" ? `stored in ${entry2.workspace}` : entry2.status;
4158
+ counts.set(label, (counts.get(label) ?? 0) + 1);
4159
+ }
4160
+ console.log(`Trace sync is ${config.enabled ? "on" : "off"}${config.enabledAt ? ` (since ${config.enabledAt})` : ""}. Last full sweep: ${state.lastFullAt ?? "never"}.`);
4161
+ for (const [label, count] of [...counts].sort((a, b) => b[1] - a[1])) console.log(` ${count} ${label}`);
4162
+ return;
4163
+ }
4164
+ if (changedSettings && !opts.full && !opts.session) return;
4165
+ const report = await lib.syncTraces({
4166
+ full: opts.full,
4167
+ only: opts.session,
4168
+ dryRun: opts.dryRun,
4169
+ limit: opts.limit,
4170
+ onProgress: opts.quiet ? void 0 : (line) => console.log(line)
4171
+ });
4172
+ if (!report) {
4173
+ if (!opts.quiet) console.log("Another sync is running on this machine; it will pick these up.");
4174
+ return;
4175
+ }
4176
+ if (!opts.quiet || report.failed) {
4177
+ const where = Object.entries(report.byWorkspace).map(([workspace2, count]) => `${count} \u2192 ${workspace2}`).join(", ");
4178
+ console.log(`${opts.dryRun ? "Would upload" : "Uploaded"} ${report.uploaded}${where ? ` (${where})` : ""}; unchanged ${report.unchanged}; kept local ${report.local}; empty ${report.empty}; another member's ${report.owned}; failed ${report.failed}. Scanned ${report.scanned}, ${report.changed} changed.`);
4179
+ for (const error of report.errors.slice(0, 20)) console.error(` ${error}`);
4180
+ }
4181
+ if (report.failed) process.exitCode = 1;
4182
+ })();
4183
+ });
4184
+ traceCmd.command("search <query...>").description("search every session in a workspace's trace library (literal substring)").option("--workspace <slug>", "the workspace to search (default: the current one)").option("--limit <n>", "hits per page", (v) => Number.parseInt(v, 10), 20).option("--cursor <cursor>", "the next page of an earlier search").option("--json", "print the page as JSON").action(run(async (words, opts) => {
4185
+ const { libraryRequest } = await import("./trace-library-474QJTAZ.js");
4186
+ const workspace2 = opts.workspace ?? currentUniverse();
4187
+ if (!workspace2) throw new Error("name a workspace with --workspace");
4188
+ const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
4189
+ const page = await libraryRequest("GET", `/context/search?${params}`, workspace2);
4190
+ if (opts.json) {
4191
+ console.log(JSON.stringify(page, null, 2));
4192
+ return;
4193
+ }
4194
+ if (!page.hits.length) console.log(page.partial ? `No hits, but the search was cut short (${page.coverage}).` : "No hits.");
4195
+ for (const { hit } of page.hits) {
4196
+ console.log(`${hit.occurredAt?.slice(0, 16).replace("T", " ") ?? " "} ${hit.title}`);
4197
+ console.log(` ${hit.snippet.replace(/\s+/g, " ")}`);
4198
+ }
4199
+ if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
4200
+ }));
4201
+ traceCmd.command("library").description("list the sessions in a workspace's trace library, most recently updated first").option("--workspace <slug>", "the workspace (default: the current one)").option("--limit <n>", "how many", (v) => Number.parseInt(v, 10), 20).option("--json", "print the page as JSON").action(run(async (opts) => {
4202
+ const { libraryRequest } = await import("./trace-library-474QJTAZ.js");
4203
+ const workspace2 = opts.workspace ?? currentUniverse();
4204
+ if (!workspace2) throw new Error("name a workspace with --workspace");
4205
+ const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);
4206
+ if (opts.json) {
4207
+ console.log(JSON.stringify(page, null, 2));
4208
+ return;
4209
+ }
4210
+ console.log(`${page.total} sessions in ${workspace2}'s trace library`);
4211
+ for (const trace of page.traces) {
4212
+ console.log(`${trace.updated_at.slice(0, 16).replace("T", " ")} ${trace.harness.padEnd(11)} ${(trace.project ?? "").padEnd(16)} ${String(trace.messages).padStart(5)} msgs ${trace.title ?? trace.session_id}`);
4213
+ }
4214
+ }));
4215
+ var workerCmd = program.command("worker").description("run a pod on this machine and connect it to a worker pool");
4216
+ workerCmd.command("start <pool>").description("start a worker connected to the given pool (wpool_\u2026 id)").option("--token <token>", "bearer token (default: $SKY_WORKER_TOKEN)").option("--label <label>", "a human-readable name for this machine", "cli").action(async (poolId, options) => {
4217
+ const { startWorker } = await import("./worker-6Q4K4HZW.js");
4218
+ const token = options.token ?? process.env.SKY_WORKER_TOKEN ?? "";
4219
+ if (!token) {
4220
+ console.error("error: provide --token or set SKY_WORKER_TOKEN");
4221
+ process.exit(1);
4222
+ }
4223
+ const base = (loadConfig().apiBase ?? "https://api.agentsky.dev").replace(/\/$/, "");
4224
+ const ac = new AbortController();
4225
+ process.on("SIGINT", () => ac.abort());
4226
+ process.on("SIGTERM", () => ac.abort());
4227
+ await startWorker({ poolId, token, label: options.label, apiBase: base, signal: ac.signal });
3694
4228
  });
3695
4229
  var ARGUMENTS_FOLLOW_THE_SCRIPT_PATH_ON_EVERY_RUNTIME = { from: "node" };
3696
4230
  program.parseAsync(process.argv, ARGUMENTS_FOLLOW_THE_SCRIPT_PATH_ON_EVERY_RUNTIME).catch(fail);