@agentsky/sky 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ CLI_VERSION,
4
+ USER_AGENT,
5
+ apiBase,
6
+ apiToken,
7
+ configPath,
8
+ consoleBase,
9
+ currentUniverse,
10
+ loadConfig,
11
+ saveConfig
12
+ } from "./chunk-RWPPIEHH.js";
2
13
  import {
3
14
  AGENT_TYPES,
4
15
  MODEL_SUBSCRIPTION_PLAN_PHRASE,
@@ -8,6 +19,7 @@ import {
8
19
  customDataSchema,
9
20
  defaultLlm,
10
21
  external_exports,
22
+ harnessProfileSchema,
11
23
  isModelEffortSupported,
12
24
  mcpServersFromKept,
13
25
  modelSubscriptionProviderFor,
@@ -16,57 +28,15 @@ import {
16
28
  skillRefSchema,
17
29
  splitMcpServers,
18
30
  supersededBy
19
- } from "./chunk-5QRKU3BR.js";
31
+ } from "./chunk-WC7GJSAJ.js";
32
+ import "./chunk-MLKGABMK.js";
20
33
 
21
34
  // 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";
35
+ import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
36
+ import { basename as basename4, resolve as resolve6 } from "path";
24
37
  import { createInterface as createInterface4 } from "readline";
25
38
  import { Command } from "commander";
26
39
 
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
40
  // src/api.ts
71
41
  var ApiError = class extends Error {
72
42
  constructor(status, code, message2) {
@@ -141,6 +111,15 @@ async function uploadFile(bytes, filename, mediaType, expiresInSeconds) {
141
111
  if (!res.ok) await parseError(res);
142
112
  return await res.json();
143
113
  }
114
+ async function apiForm(method, path, form) {
115
+ const res = await fetch(`${apiBase()}${path}`, {
116
+ method,
117
+ headers: { "user-agent": USER_AGENT, ...authHeaders() },
118
+ body: form
119
+ });
120
+ if (!res.ok) await parseError(res);
121
+ return await res.json();
122
+ }
144
123
  async function downloadFile(fileId) {
145
124
  const res = await fetch(`${apiBase()}/files/${encodeURIComponent(fileId)}/content`, {
146
125
  headers: { "user-agent": USER_AGENT, ...authHeaders() }
@@ -185,7 +164,7 @@ async function resolveSessionRef(ref) {
185
164
  return agent2.defaultSessionId;
186
165
  }
187
166
  async function* streamEvents(sessionId, signal) {
188
- const { parseSseStream } = await import("./src-RYECJN5I.js");
167
+ const { parseSseStream } = await import("./src-HVCP3PPM.js");
189
168
  const res = await fetch(
190
169
  `${apiBase()}/sessions/${encodeURIComponent(sessionId)}/stream`,
191
170
  {
@@ -209,7 +188,7 @@ async function* streamEvents(sessionId, signal) {
209
188
  }
210
189
 
211
190
  // src/files.ts
212
- import { readFileSync as readFileSync2 } from "fs";
191
+ import { readFileSync } from "fs";
213
192
  import { basename, extname } from "path";
214
193
  var MEDIA_TYPES = {
215
194
  ".png": "image/png",
@@ -231,7 +210,7 @@ function mediaTypeFor(path) {
231
210
  return MEDIA_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
232
211
  }
233
212
  async function uploadLocalFile(path, expiresInSeconds) {
234
- const bytes = readFileSync2(path);
213
+ const bytes = readFileSync(path);
235
214
  return uploadFile(new Uint8Array(bytes), basename(path), mediaTypeFor(path), expiresInSeconds);
236
215
  }
237
216
  function fileBlock(file) {
@@ -328,8 +307,8 @@ function renderGoal(goal2) {
328
307
  }
329
308
 
330
309
  // src/agent-file.ts
331
- import { existsSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
332
- import { dirname as dirname2, resolve } from "path";
310
+ import { existsSync, readFileSync as readFileSync2, writeFileSync } from "fs";
311
+ import { dirname, resolve } from "path";
333
312
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
334
313
  var fileInstructionSchema = external_exports.object({
335
314
  name: external_exports.string(),
@@ -352,12 +331,16 @@ var agentFileSchema = external_exports.object({
352
331
  instructions: external_exports.array(fileInstructionSchema).optional(),
353
332
  skills: external_exports.array(skillRefSchema).optional(),
354
333
  customData: external_exports.array(customDataSchema).optional(),
334
+ /** "deny" makes the agent work alone: its sessions may not hire helpers. */
335
+ hiring_policy: external_exports.enum(["allow", "deny"]).optional(),
336
+ /** The environment its sessions boot in when they name none. */
337
+ environment: external_exports.string().min(1).optional(),
355
338
  /** Advanced machine shape (create-only): 1|2|4|8 vCPUs, 256MB-step memory. */
356
339
  vcpus: external_exports.number().int().optional(),
357
340
  memory_mb: external_exports.number().int().optional()
358
341
  });
359
342
  function readAgentFile(path) {
360
- const raw = readFileSync3(path, "utf8");
343
+ const raw = readFileSync2(path, "utf8");
361
344
  const parsed = agentFileSchema.safeParse(parseToml(raw));
362
345
  if (!parsed.success) {
363
346
  const issue = parsed.error.issues[0];
@@ -369,11 +352,11 @@ function readAgentFile(path) {
369
352
  `${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
370
353
  );
371
354
  }
372
- const base = dirname2(resolve(path));
355
+ const base = dirname(resolve(path));
373
356
  const inline = (ref, inlineContent) => {
374
357
  if (inlineContent !== void 0) return inlineContent;
375
358
  if (!ref) return void 0;
376
- return readFileSync3(resolve(base, ref), "utf8");
359
+ return readFileSync2(resolve(base, ref), "utf8");
377
360
  };
378
361
  const spec = {
379
362
  name: file.name,
@@ -391,20 +374,22 @@ function readAgentFile(path) {
391
374
  }),
392
375
  skills: file.skills,
393
376
  customData: file.customData,
377
+ hiringPolicy: file.hiring_policy,
378
+ environmentId: file.environment,
394
379
  vcpus: file.vcpus,
395
380
  memoryMb: file.memory_mb
396
381
  };
397
382
  return { file, spec };
398
383
  }
399
384
  function stampId(path, id) {
400
- const doc = parseToml(readFileSync3(path, "utf8"));
385
+ const doc = parseToml(readFileSync2(path, "utf8"));
401
386
  doc.id = id;
402
387
  delete doc.slug;
403
- writeFileSync2(path, stringifyToml(doc));
388
+ writeFileSync(path, stringifyToml(doc));
404
389
  }
405
390
  function scaffoldAgentFile(path, name) {
406
391
  if (existsSync(path)) throw new Error(`${path} already exists`);
407
- writeFileSync2(
392
+ writeFileSync(
408
393
  path,
409
394
  `# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
410
395
  name = "${name}"
@@ -412,6 +397,11 @@ name = "${name}"
412
397
  # llm = "deepseek-flash" # must be compatible with type
413
398
  # reasoning_effort = "high" # per harness \u2014 ${reasoningEffortDocLine()}
414
399
  # capabilities = []
400
+ # hiring_policy = "deny" # its sessions work alone; "allow" (default) lets them hire helpers
401
+ # environment = "env_..." # the environment its sessions boot in when they name none
402
+ # [[skills]] # a skill from \`sky skills upload <dir>\`
403
+ # type = "skill"
404
+ # skillId = "skill_..."
415
405
 
416
406
  prompt = """
417
407
  You are ${name}, a helpful long-lived agent.
@@ -485,18 +475,18 @@ function renderCatalogAgent(agent2) {
485
475
 
486
476
  // src/clone.ts
487
477
  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";
478
+ import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
479
+ import { homedir } from "os";
480
+ import { basename as basename2, join } from "path";
491
481
  import { createInterface } from "readline";
492
482
  var PROMPT_CAP = 1e5;
493
483
  var HARNESSES = ["claude_code", "codex", "openclaw"];
494
484
  function claudeConfigRoot() {
495
- return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
485
+ return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
496
486
  }
497
487
  function readIfExists(path) {
498
488
  try {
499
- return readFileSync4(path, "utf8");
489
+ return readFileSync3(path, "utf8");
500
490
  } catch {
501
491
  return void 0;
502
492
  }
@@ -512,9 +502,9 @@ function readJson(path) {
512
502
  }
513
503
  function claudeModel(configRoot) {
514
504
  const candidates = [
515
- join2(process.cwd(), ".claude", "settings.local.json"),
516
- join2(process.cwd(), ".claude", "settings.json"),
517
- join2(configRoot, "settings.json")
505
+ join(process.cwd(), ".claude", "settings.local.json"),
506
+ join(process.cwd(), ".claude", "settings.json"),
507
+ join(configRoot, "settings.json")
518
508
  ];
519
509
  for (const path of candidates) {
520
510
  const model = readJson(path)?.model;
@@ -524,11 +514,11 @@ function claudeModel(configRoot) {
524
514
  }
525
515
  function claudeMcpServers() {
526
516
  const merged = {};
527
- const userConfig = readJson(join2(homedir2(), ".claude.json"));
517
+ const userConfig = readJson(join(homedir(), ".claude.json"));
528
518
  Object.assign(merged, userConfig?.mcpServers ?? {});
529
519
  Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
530
520
  const projectConfig = readJson(
531
- join2(process.cwd(), ".mcp.json")
521
+ join(process.cwd(), ".mcp.json")
532
522
  );
533
523
  Object.assign(merged, projectConfig?.mcpServers ?? {});
534
524
  return merged;
@@ -545,13 +535,13 @@ function readClaudeCode() {
545
535
  const configRoot = claudeConfigRoot();
546
536
  const parts = [];
547
537
  const sources = [];
548
- const global = readIfExists(join2(configRoot, "CLAUDE.md"));
538
+ const global = readIfExists(join(configRoot, "CLAUDE.md"));
549
539
  if (global?.trim()) {
550
540
  parts.push({ label: "Global CLAUDE.md", content: global });
551
541
  sources.push("global CLAUDE.md");
552
542
  }
553
543
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
554
- const project = readIfExists(join2(process.cwd(), name));
544
+ const project = readIfExists(join(process.cwd(), name));
555
545
  if (project?.trim()) {
556
546
  parts.push({ label: `Project ${name}`, content: project });
557
547
  sources.push(`project ${name}`);
@@ -592,27 +582,27 @@ function detectHarness(explicit) {
592
582
  return explicit;
593
583
  }
594
584
  if (existsSync2(claudeConfigRoot())) return "claude_code";
595
- if (existsSync2(join2(homedir2(), ".codex"))) return "codex";
596
- if (existsSync2(join2(homedir2(), ".openclaw"))) return "openclaw";
585
+ if (existsSync2(join(homedir(), ".codex"))) return "codex";
586
+ if (existsSync2(join(homedir(), ".openclaw"))) return "openclaw";
597
587
  throw new Error(
598
588
  "no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
599
589
  );
600
590
  }
601
- function readLocal(harness) {
602
- if (harness === "claude_code") return readClaudeCode();
591
+ function readLocal(harness2) {
592
+ if (harness2 === "claude_code") return readClaudeCode();
603
593
  throw new Error(
604
- `${harness} cloning lands next \u2014 claude_code is the first supported harness`
594
+ `${harness2} cloning lands next \u2014 claude_code is the first supported harness`
605
595
  );
606
596
  }
607
597
  function linkPath(configRoot) {
608
- return join2(configRoot, "agentsky-link.json");
598
+ return join(configRoot, "agentsky-link.json");
609
599
  }
610
600
  function readLink(configRoot) {
611
601
  const link = readJson(linkPath(configRoot));
612
602
  return link?.agentId ? link : void 0;
613
603
  }
614
604
  function writeLink(configRoot, link) {
615
- writeFileSync3(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
605
+ writeFileSync2(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
616
606
  `);
617
607
  }
618
608
  var DEFAULT_VAULT_NAME = "default";
@@ -843,17 +833,17 @@ async function syncClone(local, link, opts) {
843
833
  if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentId}`);
844
834
  }
845
835
  async function clone(opts) {
846
- const harness = detectHarness(opts.harness);
847
- const local = readLocal(harness);
836
+ const harness2 = detectHarness(opts.harness);
837
+ const local = readLocal(harness2);
848
838
  const link = opts.new ? void 0 : readLink(local.configRoot);
849
839
  if (link) await syncClone(local, link, opts);
850
840
  else await createClone(local, opts);
851
841
  }
852
842
 
853
843
  // 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";
844
+ import { closeSync, existsSync as existsSync3, openSync, readdirSync, readFileSync as readFileSync4, readSync, statSync } from "fs";
845
+ import { homedir as homedir2 } from "os";
846
+ import { join as join2 } from "path";
857
847
  import { createHash } from "crypto";
858
848
  function redactLine2(raw) {
859
849
  return redactLine(raw).line;
@@ -926,13 +916,13 @@ function sniffTitle(filePath) {
926
916
  }
927
917
  }
928
918
  function listClaudeCodeSessions(limit) {
929
- const root = process.env.CLAUDE_CONFIG_DIR ?? join3(homedir3(), ".claude");
930
- const projectsDir = join3(root, "projects");
919
+ const root = process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
920
+ const projectsDir = join2(root, "projects");
931
921
  if (!existsSync3(projectsDir)) return [];
932
922
  const sessions = [];
933
923
  try {
934
924
  for (const encoded of readdirSync(projectsDir)) {
935
- const projectDir = join3(projectsDir, encoded);
925
+ const projectDir = join2(projectsDir, encoded);
936
926
  let entries;
937
927
  try {
938
928
  entries = readdirSync(projectDir);
@@ -943,7 +933,7 @@ function listClaudeCodeSessions(limit) {
943
933
  for (const entry2 of entries) {
944
934
  if (!entry2.endsWith(".jsonl")) continue;
945
935
  const sessionId = entry2.slice(0, -6);
946
- const mainFile = join3(projectDir, entry2);
936
+ const mainFile = join2(projectDir, entry2);
947
937
  let stat;
948
938
  try {
949
939
  stat = statSync(mainFile);
@@ -953,12 +943,12 @@ function listClaudeCodeSessions(limit) {
953
943
  if (!stat.isFile()) continue;
954
944
  let discoveryError;
955
945
  const subagentFiles = [];
956
- const subagentDir = join3(projectDir, sessionId, "subagents");
946
+ const subagentDir = join2(projectDir, sessionId, "subagents");
957
947
  if (existsSync3(subagentDir)) {
958
948
  try {
959
949
  const subFiles = readdirSync(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
960
950
  for (const f of subFiles) {
961
- subagentFiles.push(join3(subagentDir, f));
951
+ subagentFiles.push(join2(subagentDir, f));
962
952
  }
963
953
  } catch {
964
954
  discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
@@ -980,7 +970,7 @@ function listClaudeCodeSessions(limit) {
980
970
  return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
981
971
  }
982
972
  function listCodexSessions(limit) {
983
- const sessionsDir = join3(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join3(homedir3(), ".codex"), "sessions");
973
+ const sessionsDir = join2(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join2(homedir2(), ".codex"), "sessions");
984
974
  const records = [];
985
975
  function walk(dir) {
986
976
  let entries;
@@ -990,7 +980,7 @@ function listCodexSessions(limit) {
990
980
  return;
991
981
  }
992
982
  for (const entry2 of entries) {
993
- const full = join3(dir, entry2.name);
983
+ const full = join2(dir, entry2.name);
994
984
  if (entry2.isDirectory()) {
995
985
  walk(full);
996
986
  continue;
@@ -1032,9 +1022,9 @@ function listCodexSessions(limit) {
1032
1022
  files: [.../* @__PURE__ */ new Set([...session2.files, ...descendants(session2.sessionId, /* @__PURE__ */ new Set())])]
1033
1023
  })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
1034
1024
  }
1035
- function listRecentSessions(harness, limit) {
1036
- const claudeSessions = harness === "codex" ? [] : listClaudeCodeSessions(limit);
1037
- const codexSessions = harness === "claude_code" ? [] : listCodexSessions(limit);
1025
+ function listRecentSessions(harness2, limit) {
1026
+ const claudeSessions = harness2 === "codex" ? [] : listClaudeCodeSessions(limit);
1027
+ const codexSessions = harness2 === "claude_code" ? [] : listCodexSessions(limit);
1038
1028
  return [...claudeSessions, ...codexSessions].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
1039
1029
  }
1040
1030
  function agentUseBase() {
@@ -1052,7 +1042,7 @@ function currentSessionEnv() {
1052
1042
  if (codexId) return { harness: "codex", sessionId: codexId };
1053
1043
  return void 0;
1054
1044
  }
1055
- async function mintTrace(base, harness) {
1045
+ async function mintTrace(base, harness2) {
1056
1046
  const token = apiToken();
1057
1047
  if (!token) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
1058
1048
  const res = await fetch(`${base}/api/traces`, {
@@ -1064,7 +1054,7 @@ async function mintTrace(base, harness) {
1064
1054
  authorization: `Bearer ${token}`,
1065
1055
  "user-agent": USER_AGENT
1066
1056
  },
1067
- body: JSON.stringify({ harness })
1057
+ body: JSON.stringify({ harness: harness2 })
1068
1058
  });
1069
1059
  if (!res.ok) {
1070
1060
  const body = await res.json().catch(() => null);
@@ -1077,7 +1067,7 @@ async function mintTrace(base, harness) {
1077
1067
  function loadSummaryFile(summaryPath) {
1078
1068
  let raw;
1079
1069
  try {
1080
- raw = readFileSync5(summaryPath, "utf8");
1070
+ raw = readFileSync4(summaryPath, "utf8");
1081
1071
  } catch {
1082
1072
  throw new Error(`Cannot read summary file: ${summaryPath}`);
1083
1073
  }
@@ -1109,7 +1099,7 @@ function prepareImport(session2, summaryPath) {
1109
1099
  for (const path of session2.files) {
1110
1100
  const stat = statSync(path);
1111
1101
  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");
1102
+ const text = readFileSync4(path, "utf8");
1113
1103
  for (const raw of text.split("\n")) {
1114
1104
  if (!raw.trim()) continue;
1115
1105
  let record;
@@ -1254,7 +1244,7 @@ async function login(opts) {
1254
1244
  }
1255
1245
  async function browserLogin(verifier, challenge) {
1256
1246
  const state = b64url(randomBytes(16));
1257
- const code = await new Promise((resolve5, reject) => {
1247
+ const code = await new Promise((resolve7, reject) => {
1258
1248
  const server = createServer((req, res) => {
1259
1249
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
1260
1250
  if (url.pathname !== "/callback") {
@@ -1267,7 +1257,7 @@ async function browserLogin(verifier, challenge) {
1267
1257
  if (url.searchParams.get("state") !== state || !got) {
1268
1258
  reject(new Error("login callback state mismatch"));
1269
1259
  } else {
1270
- resolve5(got);
1260
+ resolve7(got);
1271
1261
  }
1272
1262
  server.close();
1273
1263
  });
@@ -1322,22 +1312,22 @@ async function headlessLogin(verifier, challenge) {
1322
1312
 
1323
1313
  // src/local-agents.ts
1324
1314
  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");
1315
+ import { homedir as homedir3 } from "os";
1316
+ import { join as join3 } from "path";
1317
+ function localAgents(env = process.env, home = homedir3()) {
1318
+ const claude = env.CLAUDE_CONFIG_DIR || join3(home, ".claude");
1319
+ const codex = env.CODEX_HOME || join3(home, ".codex");
1320
+ const cursor = join3(home, ".cursor");
1331
1321
  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") }
1322
+ { id: "claude_code", name: "Claude Code", root: claude, skillsDir: join3(claude, "skills") },
1323
+ { id: "codex", name: "Codex", root: codex, skillsDir: join3(codex, "skills") },
1324
+ { id: "cursor", name: "Cursor", root: cursor, skillsDir: join3(cursor, "skills") }
1335
1325
  ];
1336
1326
  }
1337
- function detectLocalAgents(env = process.env, home = homedir4()) {
1327
+ function detectLocalAgents(env = process.env, home = homedir3()) {
1338
1328
  return localAgents(env, home).filter((agent2) => existsSync4(agent2.root));
1339
1329
  }
1340
- function selectLocalAgents(ids, env = process.env, home = homedir4()) {
1330
+ function selectLocalAgents(ids, env = process.env, home = homedir3()) {
1341
1331
  if (!ids?.length) return detectLocalAgents(env, home);
1342
1332
  const known = localAgents(env, home);
1343
1333
  return ids.map((id) => {
@@ -1431,21 +1421,21 @@ async function readTurn(sessionId, opts = {}) {
1431
1421
  for (; ; ) {
1432
1422
  const reading = latestTurn(await history(sessionId));
1433
1423
  if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
1434
- await new Promise((resolve5) => setTimeout(resolve5, opts.intervalMs ?? 3e3));
1424
+ await new Promise((resolve7) => setTimeout(resolve7, opts.intervalMs ?? 3e3));
1435
1425
  }
1436
1426
  }
1437
1427
 
1438
1428
  // 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";
1429
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1430
+ import { dirname as dirname3, join as join7 } from "path";
1441
1431
  import { createInterface as createInterface3 } from "readline";
1442
1432
 
1443
1433
  // src/mcp-register.ts
1444
1434
  import { spawnSync } from "child_process";
1445
1435
 
1446
1436
  // src/skill.ts
1447
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1448
- import { join as join5 } from "path";
1437
+ import { mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1438
+ import { join as join4 } from "path";
1449
1439
 
1450
1440
  // src/catalog-file.ts
1451
1441
  var CATALOG_FILE = "catalog.md";
@@ -1502,7 +1492,7 @@ var SKILL_MARKER = ".agentsky.json";
1502
1492
  function skillDocument() {
1503
1493
  return `---
1504
1494
  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."
1495
+ 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
1496
  ---
1507
1497
 
1508
1498
  # Working with other agents through AgentSky
@@ -1602,6 +1592,15 @@ it, and name its path in the brief.
1602
1592
  6. Tell the person what was done, which agent did which part, and what it cost
1603
1593
  (\`get_task\` reports the task's cost).
1604
1594
 
1595
+ ## Build an agent that the person's own application runs
1596
+
1597
+ When the person wants an agent their product starts for its customers, built
1598
+ from a skill they supply (for example: a customer uploads a file, an agent
1599
+ reviews it, their application acts on the verdict), follow
1600
+ \`${REFERENCES_DIR}/focused-agents.md\` in this skill's folder. It covers the
1601
+ prerequisites to tell them about first, one locked-down workspace per customer,
1602
+ one fresh session per event, and reading the answer as data.
1603
+
1605
1604
  ## Import this conversation to AgentSky
1606
1605
 
1607
1606
  When the person asks to import, move, or continue this conversation in AgentSky
@@ -1688,6 +1687,160 @@ use \`--session <id>\` instead of \`--current\`, and set \`source_harness\` to t
1688
1687
  harness name (e.g. \`"hermes"\`, \`"openclaw"\`).
1689
1688
  `;
1690
1689
  }
1690
+ function focusedAgentsReferenceDoc() {
1691
+ return `# A focused agent your application runs for its customers
1692
+
1693
+ Use this when the person wants an agent that their own product starts on an
1694
+ event (a customer uploads a file, a form is submitted), working from one
1695
+ skill they supply, with the result going back to their application.
1696
+
1697
+ Everything here is the public API (\`https://api.agentsky.dev/v1\`). The
1698
+ application's server calls it. The \`sky\` commands and MCP tools named
1699
+ alongside do the same things by hand, for trying it out.
1700
+
1701
+ ## What has to exist first
1702
+
1703
+ Say each of these to the person before you build. Each one changes whether it
1704
+ works:
1705
+
1706
+ - **An API key for their server.** They create it at
1707
+ platform.agentsky.dev \u2192 Developer \u2192 API keys and put it in the server's
1708
+ environment (\`AGENTSKY_TOKEN\`). Never put it in code, a page their customers
1709
+ load, or a brief.
1710
+ - **Credit** on their account. Every run is a session they pay for. Give each
1711
+ one a budget.
1712
+ - **The skill**: a directory with \`SKILL.md\` at its root.
1713
+ - **An engine that fits.** Default to \`claude_code\` on its default model. Check
1714
+ \`sky catalog search <words>\` if the skill needs something else.
1715
+ - **Their own customer sign-in.** AgentSky does not know their customers. A
1716
+ customer id their page sends is a label, not proof of who it is.
1717
+
1718
+ ## 1. One workspace per customer
1719
+
1720
+ \`\`\`
1721
+ POST /v1/universes {"slug": "rv-acme", "name": "Uploads from acme"}
1722
+ \`\`\`
1723
+
1724
+ Same by hand: \`sky universe create rv-acme\`. Files, memory, vaults and
1725
+ sessions never cross workspaces. Name every later call's workspace with the
1726
+ \`X-Universe: rv-acme\` header (\`--workspace rv-acme\` on \`sky\`).
1727
+
1728
+ ## 2. Lock the workspace down
1729
+
1730
+ \`\`\`
1731
+ PUT /v1/hook_rules {"rules": [{"point": "tool.call", "action": "block", "match": {}, "enabled": true}]}
1732
+ POST /v1/universes/rv-acme/policies {"name": "reviewer-native-tools", "kind": "TOOL_CALL",
1733
+ "config": {"action": "deny", "matcher": "Bash|BashOutput|KillShell|Edit|MultiEdit|Write|NotebookEdit|WebFetch|WebSearch|Task|mcp__.*"}}
1734
+ POST /v1/universes/rv-acme/policies {"name": "reviewer-no-helpers", "kind": "CAPABILITY", "config": {"platform.orchestration": "deny"}}
1735
+ \`\`\`
1736
+
1737
+ - **The hook rule** blocks every connected-app and capability call. Without it,
1738
+ the account owner's own connected apps reach sessions the owner's key
1739
+ starts.
1740
+ - **The \`TOOL_CALL\` rule** denies Claude Code's shell, write, web, sub-agent
1741
+ and MCP tools. A denied call answers "The platform did not authorize this
1742
+ tool invocation."
1743
+ - **Keep only the tools the skill needs.** A reviewer needs Read.
1744
+
1745
+ Telling the agent not to use a tool is not a boundary. The rule is. By hand:
1746
+ \`sky policy add --universe rv-acme --name ... --kind TOOL_CALL --config '<json>'\`.
1747
+
1748
+ ## 3. Install the skill, create the agent
1749
+
1750
+ \`\`\`
1751
+ POST /v1/skills multipart: one "files" field per file, named "<dir>/<path>", plus "displayName"
1752
+ POST /v1/agents {"name": "upload-reviewer", "agentType": "claude_code", "prompt": "...",
1753
+ "skills": [{"type": "skill", "skillId": "skill_..."}], "hiringPolicy": "deny"}
1754
+ Idempotency-Key: provision-rv-acme-agent
1755
+ \`\`\`
1756
+
1757
+ By hand: \`sky skills upload ./upload-review --workspace rv-acme\`, then
1758
+ \`sky agent create -f agent.toml --workspace rv-acme\` with
1759
+ \`hiring_policy = "deny"\` and a \`[[skills]]\` table naming the skill id.
1760
+
1761
+ The prompt says what to read and what to answer. It also says that the
1762
+ upload is data and that instructions written inside it are not to be
1763
+ followed.
1764
+
1765
+ ## 4. One fresh session per event
1766
+
1767
+ \`\`\`
1768
+ POST /v1/files multipart "file" \u2192 {"id": "file_..."}
1769
+ POST /v1/sessions Idempotency-Key: session-<job id>
1770
+ {"agent": "agent_...", "defaults": "none",
1771
+ "resources": [{"type": "file", "file_id": "file_...", "mount_path": "/upload/request.txt"}],
1772
+ "budget": {"type": "limit", "max_list_cost": {"amount": "50", "currency": "USD"}}}
1773
+ POST /v1/sessions/{id}/events Idempotency-Key: brief-<job id>
1774
+ {"events": [{"type": "user.message", "content": [{"type": "text", "text": "Review /workspace/upload/request.txt with the upload-review skill."}]}]}
1775
+ \`\`\`
1776
+
1777
+ - **\`defaults: "none"\`** keeps every workspace default out of the session:
1778
+ environment, vault, shared files, skills and team memory. The session holds
1779
+ only what the request names.
1780
+ - **Idempotency keys come from the event.** Retrying a timed-out call with the
1781
+ same key returns the first session, or replays the first message. A keyed
1782
+ create cannot carry \`initial_events\`. Create the session, save its id, then
1783
+ send the brief.
1784
+ - **Reuse a session** only for a job that continues for the same customer.
1785
+
1786
+ By hand: \`sky session create agent_... --no-defaults --budget-cents 50 --file ./request.txt:/upload/request.txt --message "..."\`
1787
+ (MCP: \`start_session\`).
1788
+
1789
+ ## 5. Read the answer
1790
+
1791
+ \`\`\`
1792
+ GET /v1/sessions/{id}/events?types[]=user.message&types[]=agent.message&types[]=session.status_idle&types[]=session.status_terminated&types[]=session.error
1793
+ \`\`\`
1794
+
1795
+ - **When the turn is done.** It has ended at a \`session.status_idle\` whose
1796
+ \`stop_reason.type\` is not \`requires_action\`, or at
1797
+ \`session.status_terminated\`.
1798
+ - **Where the answer is.** Take the last \`agent.message\` after the last
1799
+ \`user.message\`. Earlier messages can be notes the agent wrote on the way.
1800
+ - **Treat it as untrusted data.** Parse it against a fixed schema and fail
1801
+ closed on anything else. Never run what it says.
1802
+ - **Errors.** A \`session.error\` (budget reached, engine unavailable) is a
1803
+ failure to report, not an answer.
1804
+
1805
+ By hand: \`sky session read <id> --wait --json\` (MCP: \`read_session\`).
1806
+
1807
+ ## 6. Questions and approvals
1808
+
1809
+ \`GET /v1/tasks/{session id}\` lists the requests waiting on the key's owner:
1810
+ the agent's questions, approvals, and apps to connect.
1811
+ \`POST /v1/tasks/{id}/requests/{request id}/answer\` with \`{"text": ...}\` or
1812
+ \`{"option_id": ...}\` answers one. Pass a customer's answer on only after
1813
+ your application has checked who they are. By hand: \`sky task show <id>\` and
1814
+ \`sky task answer <id> <request> --text "..."\` (MCP: \`get_task\`,
1815
+ \`answer_request\`).
1816
+
1817
+ ## 7. What the application keeps
1818
+
1819
+ The agent decides; the application acts. Keep these in the application's own
1820
+ database:
1821
+
1822
+ - **One job per event**, with a unique event key. The same key with the same
1823
+ payload returns the job it made; with a different payload it is a conflict.
1824
+ - **The session id**, saved before you start polling, so a restart picks the
1825
+ job up where it was.
1826
+ - **A confirmation receipt** from the signed-in customer before any side
1827
+ effect.
1828
+ - **A unique action key per job**, so the side effect happens once. Agent
1829
+ retries do not make it happen once; the key does.
1830
+ - **Webhooks to the customer** with an event id, retried until acknowledged.
1831
+
1832
+ ## Before launch
1833
+
1834
+ Check one real session:
1835
+
1836
+ - \`GET /v1/sessions/{id}\` lists only the upload under \`resources\`.
1837
+ - \`GET /v1/hook_rules\` and \`GET /v1/universes/{slug}/policies\` show the rules
1838
+ above.
1839
+ - Ask the agent directly to run a shell command. The answer has to be the
1840
+ platform's refusal.
1841
+ - A file id from another customer's workspace is refused at session create.
1842
+ `;
1843
+ }
1691
1844
  function workspacesReferenceDoc() {
1692
1845
  return `# Workspaces in AgentSky
1693
1846
 
@@ -1723,7 +1876,7 @@ To set a per-repo default, create \`.agentsky.json\` at the git root:
1723
1876
  }
1724
1877
  function installedVersion(dir) {
1725
1878
  try {
1726
- const marker = JSON.parse(readFileSync6(join5(dir, SKILL_MARKER), "utf8"));
1879
+ const marker = JSON.parse(readFileSync5(join4(dir, SKILL_MARKER), "utf8"));
1727
1880
  return typeof marker.version === "string" ? marker.version : null;
1728
1881
  } catch {
1729
1882
  return null;
@@ -1733,24 +1886,25 @@ function isInstalledSkill(dir) {
1733
1886
  return installedVersion(dir) !== null;
1734
1887
  }
1735
1888
  function installSkill(agent2, opts = {}) {
1736
- const dir = join5(agent2.skillsDir, SKILL_NAME);
1889
+ const dir = join4(agent2.skillsDir, SKILL_NAME);
1737
1890
  const previous = installedVersion(dir);
1738
1891
  const outcome = previous === CLI_VERSION ? "current" : previous ? "updated" : "installed";
1739
1892
  if (opts.dryRun) return { agent: agent2.id, path: dir, outcome };
1740
1893
  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)}
1894
+ mkdirSync(dir, { recursive: true });
1895
+ writeFileSync3(join4(dir, "SKILL.md"), skillDocument());
1896
+ writeFileSync3(join4(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
1744
1897
  `);
1745
1898
  }
1746
1899
  if (opts.catalog) {
1747
- mkdirSync2(dir, { recursive: true });
1748
- writeFileSync4(join5(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
1900
+ mkdirSync(dir, { recursive: true });
1901
+ writeFileSync3(join4(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
1749
1902
  }
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());
1903
+ const refsDir = join4(dir, REFERENCES_DIR);
1904
+ mkdirSync(refsDir, { recursive: true });
1905
+ writeFileSync3(join4(refsDir, "import.md"), importReferenceDoc());
1906
+ writeFileSync3(join4(refsDir, "workspaces.md"), workspacesReferenceDoc());
1907
+ writeFileSync3(join4(refsDir, "focused-agents.md"), focusedAgentsReferenceDoc());
1754
1908
  return { agent: agent2.id, path: dir, outcome };
1755
1909
  }
1756
1910
 
@@ -1801,9 +1955,9 @@ function registerMcpServer(agent2, opts, runner = systemCommandRunner()) {
1801
1955
  }
1802
1956
 
1803
1957
  // 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";
1958
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
1959
+ import { homedir as homedir4 } from "os";
1960
+ import { dirname as dirname2, isAbsolute, join as join5, resolve as resolve3 } from "path";
1807
1961
  import { parse as parseToml2 } from "smol-toml";
1808
1962
  var MAX_SKILL_FILE_BYTES = 1024 * 1024;
1809
1963
  var MAX_IMPORT_DEPTH = 5;
@@ -1819,22 +1973,22 @@ function sensitive(name) {
1819
1973
  function skillFiles(root, prefix) {
1820
1974
  const files2 = [];
1821
1975
  for (const entry2 of readdirSync2(root, { withFileTypes: true })) {
1822
- const path = join6(root, entry2.name);
1976
+ const path = join5(root, entry2.name);
1823
1977
  if (entry2.isDirectory()) {
1824
1978
  if (!SKIPPED_DIRS.has(entry2.name)) files2.push(...skillFiles(path, `${prefix}/${entry2.name}`));
1825
1979
  } 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)) });
1980
+ files2.push({ path: `${prefix}/${entry2.name}`, bytes: new Uint8Array(readFileSync6(path)) });
1827
1981
  }
1828
1982
  }
1829
1983
  return files2;
1830
1984
  }
1831
1985
  function readLocalSkills(agents) {
1832
- const skills = [];
1986
+ const skills2 = [];
1833
1987
  const ignored = [];
1834
1988
  for (const agent2 of agents) {
1835
1989
  if (!existsSync5(agent2.skillsDir)) continue;
1836
1990
  for (const entry2 of readdirSync2(agent2.skillsDir, { withFileTypes: true })) {
1837
- const dir = join6(agent2.skillsDir, entry2.name);
1991
+ const dir = join5(agent2.skillsDir, entry2.name);
1838
1992
  let isDir = entry2.isDirectory();
1839
1993
  if (entry2.isSymbolicLink()) {
1840
1994
  try {
@@ -1843,22 +1997,22 @@ function readLocalSkills(agents) {
1843
1997
  isDir = false;
1844
1998
  }
1845
1999
  }
1846
- if (!isDir || !existsSync5(join6(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
1847
- const name = frontmatterName(readFileSync7(join6(dir, "SKILL.md"), "utf8"));
2000
+ if (!isDir || !existsSync5(join5(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
2001
+ const name = frontmatterName(readFileSync6(join5(dir, "SKILL.md"), "utf8"));
1848
2002
  if (!name) {
1849
2003
  ignored.push({ agent: agent2.id, dir, reason: "SKILL.md has no name" });
1850
2004
  continue;
1851
2005
  }
1852
- const taken = skills.find((skill) => skill.name === name);
2006
+ const taken = skills2.find((skill) => skill.name === name);
1853
2007
  if (taken) {
1854
2008
  ignored.push({ agent: agent2.id, dir, reason: `${taken.agent} already has a skill named ${name}` });
1855
2009
  continue;
1856
2010
  }
1857
2011
  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) });
2012
+ skills2.push({ name, agent: agent2.id, dir, files: files2, bytes: files2.reduce((sum, file) => sum + file.bytes.length, 0) });
1859
2013
  }
1860
2014
  }
1861
- return { skills, ignored };
2015
+ return { skills: skills2, ignored };
1862
2016
  }
1863
2017
  var INSTRUCTION_FILES = { claude_code: "CLAUDE.md", codex: "AGENTS.md" };
1864
2018
  function inlineImports(content, from, home, depth, seen) {
@@ -1872,11 +2026,11 @@ function inlineImports(content, from, home, depth, seen) {
1872
2026
  }
1873
2027
  const target = fence ? void 0 : line.match(/^@(\S+\.(?:md|markdown|txt))$/i)?.[1];
1874
2028
  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);
2029
+ const path = target.startsWith("~/") ? join5(home, target.slice(2)) : isAbsolute(target) ? target : resolve3(dirname2(from), target);
1876
2030
  if (seen.includes(path)) return "";
1877
2031
  let imported;
1878
2032
  try {
1879
- imported = readFileSync7(path, "utf8");
2033
+ imported = readFileSync6(path, "utf8");
1880
2034
  } catch {
1881
2035
  return line;
1882
2036
  }
@@ -1884,15 +2038,15 @@ function inlineImports(content, from, home, depth, seen) {
1884
2038
  return inlineImports(imported, path, home, depth + 1, seen).trim();
1885
2039
  }).join("\n");
1886
2040
  }
1887
- function readGlobalInstructions(agents, home = homedir5()) {
2041
+ function readGlobalInstructions(agents, home = homedir4()) {
1888
2042
  const found = [];
1889
2043
  for (const agent2 of agents) {
1890
2044
  const file = INSTRUCTION_FILES[agent2.id];
1891
2045
  if (!file) continue;
1892
- const path = join6(agent2.root, file);
2046
+ const path = join5(agent2.root, file);
1893
2047
  let raw;
1894
2048
  try {
1895
- raw = readFileSync7(path, "utf8");
2049
+ raw = readFileSync6(path, "utf8");
1896
2050
  } catch {
1897
2051
  continue;
1898
2052
  }
@@ -1911,7 +2065,7 @@ ${item.content.trim()}`).join("\n\n");
1911
2065
  }
1912
2066
  function readJson2(path) {
1913
2067
  try {
1914
- return JSON.parse(readFileSync7(path, "utf8"));
2068
+ return JSON.parse(readFileSync6(path, "utf8"));
1915
2069
  } catch {
1916
2070
  return void 0;
1917
2071
  }
@@ -1925,15 +2079,15 @@ function bearerOf(def) {
1925
2079
  return match?.[1] ?? null;
1926
2080
  }
1927
2081
  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 ?? {};
2082
+ if (agent2.id === "claude_code") return readJson2(join5(home, ".claude.json"))?.mcpServers ?? {};
2083
+ if (agent2.id === "cursor") return readJson2(join5(agent2.root, "mcp.json"))?.mcpServers ?? {};
1930
2084
  try {
1931
- return parseToml2(readFileSync7(join6(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
2085
+ return parseToml2(readFileSync6(join5(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
1932
2086
  } catch {
1933
2087
  return {};
1934
2088
  }
1935
2089
  }
1936
- function findLocalSecrets(agents, home = homedir5()) {
2090
+ function findLocalSecrets(agents, home = homedir4()) {
1937
2091
  const secrets2 = [];
1938
2092
  for (const agent2 of agents) {
1939
2093
  for (const [name, def] of Object.entries(serversOf(agent2, home))) {
@@ -1949,9 +2103,9 @@ function findLocalSecrets(agents, home = homedir5()) {
1949
2103
 
1950
2104
  // src/subscriptions.ts
1951
2105
  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";
2106
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
2107
+ import { homedir as homedir5 } from "os";
2108
+ import { join as join6 } from "path";
1955
2109
  import { createInterface as createInterface2 } from "readline";
1956
2110
  var ALIASES = {
1957
2111
  claude: "anthropic",
@@ -2036,12 +2190,12 @@ async function connectClaude() {
2036
2190
  const expiry = sub.expiresAt ? ` (expires ~${sub.expiresAt.slice(0, 10)})` : "";
2037
2191
  console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
2038
2192
  }
2039
- function codexSignIn(env = process.env, home = homedir6()) {
2040
- const path = join7(env.CODEX_HOME || join7(home, ".codex"), "auth.json");
2193
+ function codexSignIn(env = process.env, home = homedir5()) {
2194
+ const path = join6(env.CODEX_HOME || join6(home, ".codex"), "auth.json");
2041
2195
  if (!existsSync6(path)) {
2042
2196
  throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
2043
2197
  }
2044
- const raw = readFileSync8(path, "utf8");
2198
+ const raw = readFileSync7(path, "utf8");
2045
2199
  try {
2046
2200
  const parsed = JSON.parse(raw);
2047
2201
  return { path, raw, account: parsed.tokens?.account_id };
@@ -2049,12 +2203,12 @@ function codexSignIn(env = process.env, home = homedir6()) {
2049
2203
  throw new ApiError(400, "invalid_codex_auth", `${path} isn't valid JSON \u2014 re-run \`codex login\``);
2050
2204
  }
2051
2205
  }
2052
- function cursorSignIn(env = process.env, home = homedir6()) {
2053
- const path = join7(env.XDG_CONFIG_HOME || join7(home, ".config"), "cursor", "auth.json");
2206
+ function cursorSignIn(env = process.env, home = homedir5()) {
2207
+ const path = join6(env.XDG_CONFIG_HOME || join6(home, ".config"), "cursor", "auth.json");
2054
2208
  if (!existsSync6(path)) {
2055
2209
  throw new ApiError(404, "no_cursor_auth", `no ${path} \u2014 run \`cursor-agent login\` first`);
2056
2210
  }
2057
- const raw = readFileSync8(path, "utf8");
2211
+ const raw = readFileSync7(path, "utf8");
2058
2212
  try {
2059
2213
  const parsed = JSON.parse(raw);
2060
2214
  if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") throw new Error("shape");
@@ -2133,11 +2287,11 @@ function planProviders(names) {
2133
2287
  }));
2134
2288
  }
2135
2289
  function reportPath() {
2136
- return join8(dirname4(configPath()), "setup.json");
2290
+ return join7(dirname3(configPath()), "setup.json");
2137
2291
  }
2138
2292
  function loadReport() {
2139
2293
  try {
2140
- const saved = JSON.parse(readFileSync9(reportPath(), "utf8"));
2294
+ const saved = JSON.parse(readFileSync8(reportPath(), "utf8"));
2141
2295
  return { agents: saved.agents ?? [], plans: saved.plans ?? [], ...saved.secrets ? { secrets: saved.secrets } : {} };
2142
2296
  } catch {
2143
2297
  return { agents: [], plans: [] };
@@ -2150,8 +2304,8 @@ function mergeReport(base, next) {
2150
2304
  return { agents, plans, ...secrets2 ? { secrets: secrets2 } : {} };
2151
2305
  }
2152
2306
  function saveReport(report) {
2153
- mkdirSync3(dirname4(reportPath()), { recursive: true });
2154
- writeFileSync5(reportPath(), `${JSON.stringify(report, null, 2)}
2307
+ mkdirSync2(dirname3(reportPath()), { recursive: true });
2308
+ writeFileSync4(reportPath(), `${JSON.stringify(report, null, 2)}
2155
2309
  `, { mode: 384 });
2156
2310
  }
2157
2311
  function confirm3(question) {
@@ -2321,13 +2475,13 @@ async function runSetup(opts) {
2321
2475
  await connectPlans(agents, opts, interactive, emit, run2);
2322
2476
  await tell();
2323
2477
  }
2324
- const { skills, ignored } = wantsImport ? readLocalSkills(agents) : { skills: [], ignored: [] };
2478
+ const { skills: skills2, ignored } = wantsImport ? readLocalSkills(agents) : { skills: [], ignored: [] };
2325
2479
  const instructions = wantsImport ? readGlobalInstructions(agents) : [];
2326
2480
  const secrets2 = wantsSecrets ? findLocalSecrets(agents) : [];
2327
2481
  if (wantsImport || wantsSecrets) {
2328
2482
  emit({
2329
2483
  type: "found",
2330
- skills: skills.map((skill) => ({ name: skill.name, agent: skill.agent, files: skill.files.length, bytes: skill.bytes })),
2484
+ skills: skills2.map((skill) => ({ name: skill.name, agent: skill.agent, files: skill.files.length, bytes: skill.bytes })),
2331
2485
  ignored: ignored.map((item) => ({ dir: item.dir, reason: item.reason })),
2332
2486
  instructions: instructions.map((item) => ({ agent: item.agent, path: item.path, bytes: Buffer.byteLength(item.content), imports: item.imports })),
2333
2487
  secrets: secrets2.map((secret) => ({ name: secret.name, agent: secret.agent, url: secret.url }))
@@ -2341,8 +2495,8 @@ async function runSetup(opts) {
2341
2495
  const report = mergeReport(earlier, run2);
2342
2496
  saveReport(report);
2343
2497
  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);
2498
+ const sendFiles = wantsImport && (skills2.length > 0 || instructions.length > 0) && await agreed("Send these skills and instructions to your AgentSky workspace?");
2499
+ await importToWorkspace(key, held, sendFiles ? skills2 : [], sendFiles ? combineInstructions(instructions) : "", emit);
2346
2500
  }
2347
2501
  emit({ type: "done", report });
2348
2502
  }
@@ -2421,10 +2575,10 @@ async function saveSecrets(found, opts, interactive, emit) {
2421
2575
  }
2422
2576
  return counts;
2423
2577
  }
2424
- async function importToWorkspace(key, held, skills, instructions, emit) {
2578
+ async function importToWorkspace(key, held, skills2, instructions, emit) {
2425
2579
  const outcomes = [];
2426
2580
  let signedOut = false;
2427
- for (const skill of skills) {
2581
+ for (const skill of skills2) {
2428
2582
  if (signedOut) {
2429
2583
  outcomes.push({ name: skill.name, outcome: "skipped: sign in to agentsky.dev again" });
2430
2584
  continue;
@@ -2499,6 +2653,50 @@ function renderArtifact(artifact2) {
2499
2653
  return `${artifact2.id} ${artifact2.name} v${artifact2.version} ${files2} entry ${artifact2.entry}${artifact2.share_url ? ` shared ${artifact2.share_url}` : ""}`;
2500
2654
  }
2501
2655
 
2656
+ // src/skills-store.ts
2657
+ import { readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
2658
+ import { basename as basename3, join as join8, relative, resolve as resolve4 } from "path";
2659
+ function skillBundle(dir) {
2660
+ const root = resolve4(dir);
2661
+ const top = basename3(root);
2662
+ const files2 = [];
2663
+ const walk = (path) => {
2664
+ for (const name of readdirSync3(path).sort()) {
2665
+ const full = join8(path, name);
2666
+ if (statSync3(full).isDirectory()) walk(full);
2667
+ else files2.push({ path: `${top}/${relative(root, full)}`, bytes: readFileSync9(full) });
2668
+ }
2669
+ };
2670
+ walk(root);
2671
+ if (!files2.some((file) => file.path === `${top}/SKILL.md`)) throw new Error(`${dir}: no SKILL.md at its root`);
2672
+ return files2;
2673
+ }
2674
+ async function uploadSkill(dir, displayName) {
2675
+ const form = new FormData();
2676
+ for (const file of skillBundle(dir)) form.append("files", new Blob([new Uint8Array(file.bytes)]), file.path);
2677
+ form.append("displayName", displayName ?? basename3(resolve4(dir)));
2678
+ return apiForm("POST", "/skills", form);
2679
+ }
2680
+ async function listSkills() {
2681
+ return (await api("GET", "/skills")).skills;
2682
+ }
2683
+ async function getTask(task2) {
2684
+ return (await api("GET", `/tasks/${encodeURIComponent(task2)}`)).task;
2685
+ }
2686
+ function answerBody(answer) {
2687
+ if (!answer.option === !answer.text) throw new Error("give exactly one of --option or --text");
2688
+ return { ...answer.option ? { option_id: answer.option } : { text: answer.text }, ...answer.note ? { note: answer.note } : {} };
2689
+ }
2690
+ async function answerRequest(task2, request, answer) {
2691
+ return api("POST", `/tasks/${encodeURIComponent(task2)}/requests/${encodeURIComponent(request)}/answer`, answerBody(answer));
2692
+ }
2693
+ function renderRequest(request) {
2694
+ const options = request.options.length ? `
2695
+ options: ${request.options.map((option) => `${option.id} (${option.label})`).join(", ")}` : "";
2696
+ return `${request.id} ${request.kind} from ${request.session_id}
2697
+ ${request.question}${options}`;
2698
+ }
2699
+
2502
2700
  // src/workspace.ts
2503
2701
  var KINDS = ["TOOL_CALL", "SPEND_CAP", "CALL_COUNT", "CAPABILITY"];
2504
2702
  async function workspaceSlug(explicit) {
@@ -2593,6 +2791,173 @@ function renderPolicy(rule) {
2593
2791
  return `${state} ${rule.layer.padEnd(9)} ${rule.kind.padEnd(10)} ${rule.name} ${JSON.stringify(rule.config)} ${rule.id}`;
2594
2792
  }
2595
2793
 
2794
+ // src/harness.ts
2795
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
2796
+ import { dirname as dirname4, join as join9, resolve as resolve5 } from "path";
2797
+ var HARNESS_FILE = "harness.json";
2798
+ function harnessFilePath(dir) {
2799
+ return join9(resolve5(dir), HARNESS_FILE);
2800
+ }
2801
+ function readHarnessFile(dir) {
2802
+ const path = harnessFilePath(dir);
2803
+ if (!existsSync7(path)) {
2804
+ throw new Error(`no ${HARNESS_FILE} found in ${resolve5(dir)} \u2014 run \`sky harness init\` first`);
2805
+ }
2806
+ try {
2807
+ return JSON.parse(readFileSync10(path, "utf8"));
2808
+ } catch (err) {
2809
+ throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
2810
+ }
2811
+ }
2812
+ var INIT_SKELETON = {
2813
+ id: "my_harness",
2814
+ name: "My Harness",
2815
+ version: "0.1.0",
2816
+ distribution: {
2817
+ // choose one distribution kind and remove the others:
2818
+ npx: "@your-scope/my-harness@0.1.0"
2819
+ // uvx: "my-harness==0.1.0",
2820
+ // binary: { url: "https://example.com/my-harness-linux-amd64", sha256: "..." },
2821
+ }
2822
+ // optional: declare the tools this harness exposes
2823
+ // tools: [{ id: "bash", kind: "execute", title: "Shell" }],
2824
+ // optional: feature flags
2825
+ // features: ["subagents"],
2826
+ };
2827
+ var INIT_COMMENT = `# harness.json \u2014 workspace harness profile skeleton
2828
+ # Validate locally: sky harness verify .
2829
+ # Submit: sky harness push .
2830
+ # Full schema: https://agentsky.dev/docs/harness-profile
2831
+ `;
2832
+ function harnessInit(dir) {
2833
+ const path = harnessFilePath(dir);
2834
+ if (existsSync7(path)) {
2835
+ throw new Error(`${path} already exists \u2014 delete it to re-init`);
2836
+ }
2837
+ mkdirSync3(dirname4(path), { recursive: true });
2838
+ writeFileSync5(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
2839
+ `);
2840
+ console.log(`Created ${path}`);
2841
+ console.log(INIT_COMMENT);
2842
+ }
2843
+ async function harnessVerify(dir, opts) {
2844
+ const raw = readHarnessFile(dir);
2845
+ const parsed = harnessProfileSchema.safeParse(raw);
2846
+ if (!parsed.success) {
2847
+ console.error("harness.json schema errors:");
2848
+ for (const issue of parsed.error.issues) {
2849
+ const path = issue.path.length ? issue.path.join(".") : "(root)";
2850
+ console.error(` ${path}: ${issue.message}`);
2851
+ }
2852
+ process.exit(1);
2853
+ }
2854
+ console.log(`harness.json ok (id: ${parsed.data.id}, version: ${parsed.data.version})`);
2855
+ if (!opts.handle) return;
2856
+ const rawHandle = opts.handle.replace(/^@/, "");
2857
+ console.log(`Starting conformance run for @${rawHandle}\u2026`);
2858
+ let runId;
2859
+ try {
2860
+ const { run: run2 } = await api(
2861
+ "POST",
2862
+ `/catalog/listings/${encodeURIComponent(rawHandle)}/conformance`
2863
+ );
2864
+ runId = run2.id;
2865
+ console.log(` run ${runId} status: ${run2.status}`);
2866
+ } catch (err) {
2867
+ if (err instanceof ApiError) {
2868
+ console.error(`sky: conformance start failed \u2014 ${err.message} (${err.code})`);
2869
+ } else {
2870
+ console.error(`sky: conformance start failed \u2014 ${String(err)}`);
2871
+ }
2872
+ process.exit(1);
2873
+ }
2874
+ const pollMs = 4e3;
2875
+ const maxWaitMs = 5 * 60 * 1e3;
2876
+ const deadline = Date.now() + maxWaitMs;
2877
+ while (Date.now() < deadline) {
2878
+ await new Promise((r) => setTimeout(r, pollMs));
2879
+ try {
2880
+ const { run: run2 } = await api("GET", `/catalog/listings/${encodeURIComponent(rawHandle)}/conformance`);
2881
+ if (run2.status === "running") {
2882
+ process.stdout.write(".");
2883
+ continue;
2884
+ }
2885
+ process.stdout.write("\n");
2886
+ if (run2.status === "failed") {
2887
+ console.error(`conformance run failed: ${run2.reason ?? "unknown reason"}`);
2888
+ process.exit(1);
2889
+ }
2890
+ console.log(`
2891
+ Conformance report for @${rawHandle}`);
2892
+ console.log(
2893
+ ` required_passed: ${run2.required_passed ? "yes" : "no"} all_passed: ${run2.all_passed ? "yes" : "no"}`
2894
+ );
2895
+ if (run2.report?.checks) {
2896
+ for (const check of run2.report.checks) {
2897
+ const icon = check.status === "pass" ? "\u2713" : check.status === "skipped" ? "\u2013" : "\u2717";
2898
+ const req = check.required ? " [required]" : "";
2899
+ console.log(` ${icon} ${check.id}${req} \u2014 ${check.reason ?? check.status} (${check.seconds}s)`);
2900
+ }
2901
+ }
2902
+ if (!run2.required_passed) process.exit(1);
2903
+ return;
2904
+ } catch (err) {
2905
+ console.error(`
2906
+ sky: conformance poll failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
2907
+ process.exit(1);
2908
+ }
2909
+ }
2910
+ console.error("\nsky: timed out waiting for conformance run (5 min)");
2911
+ process.exit(1);
2912
+ }
2913
+ async function harnessPush(dir, opts) {
2914
+ const raw = readHarnessFile(dir);
2915
+ const parsed = harnessProfileSchema.safeParse(raw);
2916
+ if (!parsed.success) {
2917
+ console.error("harness.json is invalid \u2014 run `sky harness verify` first:");
2918
+ for (const issue of parsed.error.issues) {
2919
+ const path = issue.path.length ? issue.path.join(".") : "(root)";
2920
+ console.error(` ${path}: ${issue.message}`);
2921
+ }
2922
+ process.exit(1);
2923
+ }
2924
+ const profile = parsed.data;
2925
+ const version = opts.version ?? profile.version;
2926
+ const handle = profile.id;
2927
+ let existing;
2928
+ try {
2929
+ const { harnesses } = await api("GET", "/harnesses");
2930
+ existing = harnesses.find((h) => h.handle === handle);
2931
+ } catch (err) {
2932
+ if (!(err instanceof ApiError && err.status === 404)) throw err;
2933
+ }
2934
+ if (!existing) {
2935
+ const { harness: harness2 } = await api("POST", "/harnesses", {
2936
+ handle,
2937
+ name: profile.name,
2938
+ profile,
2939
+ model_billing: "gateway"
2940
+ });
2941
+ existing = harness2;
2942
+ console.log(`Created harness ${harness2.id} handle: ${harness2.handle}`);
2943
+ } else {
2944
+ console.log(`Harness exists ${existing.id} handle: ${existing.handle}`);
2945
+ }
2946
+ const { version: ver } = await api(
2947
+ "POST",
2948
+ `/harnesses/${encodeURIComponent(existing.id)}/versions`,
2949
+ { version, profile }
2950
+ );
2951
+ console.log(
2952
+ `Version ${ver.version} status: ${ver.status} id: ${ver.id} created: ${ver.created_at}`
2953
+ );
2954
+ console.log(
2955
+ `
2956
+ Next step: run conformance
2957
+ sky harness verify . --handle @${handle}`
2958
+ );
2959
+ }
2960
+
2596
2961
  // src/index.ts
2597
2962
  function printModelBilling(session2) {
2598
2963
  if (session2.modelBilling !== "account") return;
@@ -2864,7 +3229,7 @@ function renderResource(r) {
2864
3229
  const ref = r.checkout ? `@${r.checkout.type === "branch" ? r.checkout.name : r.checkout.sha}` : "";
2865
3230
  return `${r.id} repo ${r.mount_path} ${r.url}${ref}`;
2866
3231
  }
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(
3232
+ 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
3233
  run(
2869
3234
  async (agentRef, opts) => {
2870
3235
  const resources = await resourceParams(opts);
@@ -2874,6 +3239,8 @@ session.command("create <agent>").description("create a session (starts provisio
2874
3239
  ...opts.vcpus !== void 0 ? { vcpus: Number(opts.vcpus) } : {},
2875
3240
  ...opts.memory !== void 0 ? { memoryMb: Number(opts.memory) } : {},
2876
3241
  ...opts.deferred ? { provision: "deferred" } : {},
3242
+ ...opts.defaults === false ? { defaults: "none" } : {},
3243
+ ...opts.budgetCents ? { budget: { type: "limit", max_list_cost: { amount: opts.budgetCents, currency: "USD" } } } : {},
2877
3244
  ...resources.length ? { resources } : {},
2878
3245
  ...opts.message ? { initial_events: [{ type: "user.message", content: [{ type: "text", text: opts.message }] }] } : {}
2879
3246
  });
@@ -3206,9 +3573,9 @@ secrets.command("unset <id> <key>").action(
3206
3573
  );
3207
3574
  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
3575
  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";
3576
+ const path = resolve6(opts.file);
3577
+ if (!existsSync8(path)) {
3578
+ const name = basename4(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
3212
3579
  scaffoldAgentFile(path, name);
3213
3580
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
3214
3581
  }
@@ -3239,7 +3606,7 @@ program.command("launch").description("scaffold ./agent.toml if absent, create t
3239
3606
  );
3240
3607
  program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent)").option("-f, --file <path>", "agent file", "agent.toml").action(
3241
3608
  run(async (opts) => {
3242
- const path = resolve4(opts.file);
3609
+ const path = resolve6(opts.file);
3243
3610
  const { file, spec } = readAgentFile(path);
3244
3611
  if (!file.id) {
3245
3612
  console.log("No agent id in the file yet \u2014 running launch instead.");
@@ -3289,7 +3656,7 @@ program.command("import").description("import a local Claude Code or Codex sessi
3289
3656
  if (opts.trace && !opts.key) throw new Error("--trace requires --key <importKey>");
3290
3657
  const limit = Math.max(1, Math.min(50, parseInt(opts.limit ?? "10", 10) || 10));
3291
3658
  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";
3659
+ const harness2 = opts.harness ?? "all";
3293
3660
  const base = opts.url ?? agentUseBase();
3294
3661
  const nonInteractive = !process.stdin.isTTY;
3295
3662
  const autoConfirm = opts.yes || opts.current || !!opts.session && nonInteractive;
@@ -3308,18 +3675,18 @@ program.command("import").description("import a local Claude Code or Codex sessi
3308
3675
  }
3309
3676
  } else {
3310
3677
  const cwd = process.cwd();
3311
- const all = listRecentSessions(harness === "all" ? void 0 : harness, 50);
3678
+ const all = listRecentSessions(harness2 === "all" ? void 0 : harness2, 50);
3312
3679
  const byCwd = all.filter((s) => s.workspace === cwd || s.workspace.startsWith(cwd + "/"));
3313
3680
  candidates = byCwd.length > 0 ? [byCwd[0]] : all.length > 0 ? [all[0]] : [];
3314
3681
  }
3315
3682
  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
3683
  } else if (opts.session) {
3317
- candidates = listRecentSessions(harness === "all" ? void 0 : harness, Number.POSITIVE_INFINITY).filter(
3684
+ candidates = listRecentSessions(harness2 === "all" ? void 0 : harness2, Number.POSITIVE_INFINITY).filter(
3318
3685
  (s) => s.sessionId === opts.session
3319
3686
  );
3320
3687
  if (candidates.length === 0) throw new Error(`Session "${opts.session}" not found in ~/.claude or ~/.codex.`);
3321
3688
  } else {
3322
- candidates = listRecentSessions(harness === "all" ? void 0 : harness, limit);
3689
+ candidates = listRecentSessions(harness2 === "all" ? void 0 : harness2, limit);
3323
3690
  if (candidates.length === 0) {
3324
3691
  console.log("No recent sessions found. Make sure you have run Claude Code (sessions in ~/.claude) or Codex (sessions in ~/.codex).");
3325
3692
  return;
@@ -3342,11 +3709,11 @@ program.command("import").description("import a local Claude Code or Codex sessi
3342
3709
  console.log();
3343
3710
  const rl = createInterface4({ input: process.stdin, output: process.stdout });
3344
3711
  const answer = await new Promise(
3345
- (resolve5) => rl.question(
3712
+ (resolve7) => rl.question(
3346
3713
  `Import which session? [1-${candidates.length}, or comma-separated, or "all"]: `,
3347
3714
  (a) => {
3348
3715
  rl.close();
3349
- resolve5(a.trim());
3716
+ resolve7(a.trim());
3350
3717
  }
3351
3718
  )
3352
3719
  );
@@ -3369,9 +3736,9 @@ program.command("import").description("import a local Claude Code or Codex sessi
3369
3736
  if (!autoConfirm) {
3370
3737
  if (nonInteractive) throw new Error("Use --yes to confirm this transfer in a non-interactive terminal.");
3371
3738
  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) => {
3739
+ const answer = await new Promise((resolve7) => prompt.question("Upload these selected conversations? [y/N] ", (value) => {
3373
3740
  prompt.close();
3374
- resolve5(value);
3741
+ resolve7(value);
3375
3742
  }));
3376
3743
  if (!/^y(?:es)?$/i.test(answer.trim())) {
3377
3744
  console.log("Cancelled. Nothing uploaded.");
@@ -3453,7 +3820,7 @@ files.command("download <id>").description("download a session file's bytes").re
3453
3820
  console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
3454
3821
  })
3455
3822
  );
3456
- var task = program.command("task").description("a task's shared files (the task is its first session's id)");
3823
+ var task = program.command("task").description("a task's shared files and open requests (the task is its first session's id)");
3457
3824
  task.command("files <task>").description("list the task's current files").action(
3458
3825
  run(async (taskId) => {
3459
3826
  const list = await listTaskFiles(taskId);
@@ -3472,7 +3839,7 @@ task.command("get <task> <path>").description("read one task file; prints it, or
3472
3839
  );
3473
3840
  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
3841
  run(async (taskId, path, local) => {
3475
- const { file, conflict } = await writeTaskFile(taskId, path, readFileSync10(local), mediaTypeFor(local));
3842
+ const { file, conflict } = await writeTaskFile(taskId, path, readFileSync11(local), mediaTypeFor(local));
3476
3843
  console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
3477
3844
  })
3478
3845
  );
@@ -3482,6 +3849,45 @@ task.command("rm <task> <path>").description("remove a path from the task's file
3482
3849
  console.log(`Removed ${path}`);
3483
3850
  })
3484
3851
  );
3852
+ 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(
3853
+ run(async (taskId, opts) => {
3854
+ const found = await getTask(taskId);
3855
+ if (opts.json) {
3856
+ console.log(JSON.stringify(found));
3857
+ return;
3858
+ }
3859
+ for (const member of found.sessions ?? []) console.log(`${member.id} ${member.role}${member.status ? ` ${member.status}` : ""}`);
3860
+ if (!found.requests.length) console.log("no open requests");
3861
+ for (const request of found.requests) console.log(renderRequest(request));
3862
+ })
3863
+ );
3864
+ 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(
3865
+ run(async (taskId, requestId, opts) => {
3866
+ await answerRequest(taskId, requestId, opts);
3867
+ console.log(`Answered ${requestId}`);
3868
+ })
3869
+ );
3870
+ var skills = program.command("skills").description("the workspace's skill store: upload a skill, then attach it to an agent by id");
3871
+ 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(
3872
+ run(async (dir, opts) => {
3873
+ const { skill } = await uploadSkill(dir, opts.name);
3874
+ if (opts.json) {
3875
+ console.log(JSON.stringify(skill));
3876
+ return;
3877
+ }
3878
+ console.log(`Uploaded ${skill.id} (${skill.displayName ?? dir}). Attach it in agent.toml:`);
3879
+ console.log(` [[skills]]
3880
+ type = "skill"
3881
+ skillId = "${skill.id}"`);
3882
+ })
3883
+ );
3884
+ skills.command("list").description("the workspace's stored skills").action(
3885
+ run(async () => {
3886
+ const list = await listSkills();
3887
+ if (!list.length) console.log("no skills");
3888
+ for (const skill of list) console.log(`${skill.id} ${skill.displayName ?? ""}`);
3889
+ })
3890
+ );
3485
3891
  var artifact = program.command("artifact").description("artifacts: publish a task's files as a named, shareable result");
3486
3892
  artifact.command("list").description("the workspace's artifacts, or one task's with --task").option("--task <task>", "only this task's artifacts").action(
3487
3893
  run(async (opts) => {
@@ -3692,5 +4098,143 @@ program.command("config").description("show the CLI config location and current
3692
4098
  console.log(` token: ${config.token ? `${config.token.slice(0, 10)}\u2026` : "(none)"}`);
3693
4099
  console.log(` universe: ${config.currentUniverse ?? "(personal)"}`);
3694
4100
  });
4101
+ var harness = program.command("harness").description("workspace harnesses: init, verify, push");
4102
+ harness.command("init [dir]").description("write a harness.json profile skeleton in the given directory (default: .)").action(run(async (dir) => {
4103
+ harnessInit(dir ?? ".");
4104
+ }));
4105
+ 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) => {
4106
+ await harnessVerify(dir ?? ".", { handle: opts.handle });
4107
+ }));
4108
+ 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) => {
4109
+ await harnessPush(dir ?? ".", { version: opts.version });
4110
+ }));
4111
+ 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) => {
4112
+ const { connectHarness, serveHarness, agentCommandFrom } = await import("./harness-connect-DUC7GIBL.js");
4113
+ const token = opts.token ?? process.env.AGENTSKY_HARNESS_TOKEN ?? "";
4114
+ const url = opts.session ? void 0 : process.env.AGENTSKY_HARNESS_RELAY_URL;
4115
+ const apiBase2 = (loadConfig().apiBase ?? "https://api.agentsky.dev").replace(/\/$/, "");
4116
+ const ac = new AbortController();
4117
+ process.on("SIGINT", () => ac.abort());
4118
+ process.on("SIGTERM", () => ac.abort());
4119
+ const options = { url, apiBase: apiBase2, session: opts.session, token, command: agentCommandFrom(process.argv), signal: ac.signal };
4120
+ process.exitCode = opts.once ? await connectHarness(options) : await serveHarness(options);
4121
+ }));
4122
+ var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
4123
+ 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) => {
4124
+ const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-MPGQYXNR.js");
4125
+ if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
4126
+ const scopes = parseContentScopes(opts.capture);
4127
+ const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
4128
+ if ((chosen.files.length || chosen.dirs.length) && !scopes.includes("files")) throw new Error("--file and --dir need --capture files");
4129
+ if (scopes.includes("files") && !chosen.files.length && !chosen.dirs.length) throw new Error("--capture files needs at least one --file or --dir");
4130
+ const { trace } = await api("POST", "/traces", opts.name ? { name: opts.name } : {});
4131
+ const { grant, token } = await api("POST", `/traces/${trace.id}/grants`, {
4132
+ source: opts.source,
4133
+ ...scopes.length ? { content_scopes: scopes } : {}
4134
+ });
4135
+ const root = `task:${trace.id}`;
4136
+ const state = {
4137
+ trace_id: trace.id,
4138
+ grant_id: grant.id,
4139
+ token,
4140
+ source: opts.source,
4141
+ api_base: apiBase(),
4142
+ next_sequence: 0,
4143
+ root_span: root,
4144
+ ...scopes.length ? { content_scopes: scopes, files: chosen.files, dirs: chosen.dirs } : {}
4145
+ };
4146
+ writeState(state);
4147
+ record(() => [
4148
+ { span_id: root, parent_span_id: null, kind: "start", name: opts.name ?? "task" },
4149
+ ...chosen.files.flatMap((path) => fileEvent(root, path, state) ?? [])
4150
+ ]);
4151
+ console.log(`Tracing ${trace.id} as ${opts.source} until ${grant.expires_at}`);
4152
+ console.log(scopes.length ? ` content: ${scopes.join(", ")} (redacted here before it leaves)` : " metadata only");
4153
+ if (opts.hooks) console.log(` hooks: ${installClaudeHooks(resolve6(typeof opts.hooks === "string" ? opts.hooks : ".claude/settings.local.json"))}`);
4154
+ console.log(` a session started elsewhere joins it with metadata {"trace_id": "${trace.id}"}`);
4155
+ console.log(` sky trace show ${trace.id}`);
4156
+ }));
4157
+ traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
4158
+ try {
4159
+ const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-MPGQYXNR.js");
4160
+ const chunks = [];
4161
+ for await (const chunk of process.stdin) chunks.push(chunk);
4162
+ const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
4163
+ if (!record((state2) => [...eventsForClaudeHook(input, state2), ...contentForClaudeHook(input, state2)])) return;
4164
+ const state = readState();
4165
+ if (!state) return;
4166
+ const outcome = await flush(grantSender(state, 3e3));
4167
+ if (outcome.stopped === "grant_revoked" || outcome.stopped === "grant_expired") clearState();
4168
+ } catch {
4169
+ }
4170
+ });
4171
+ traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
4172
+ const { readState, flush, grantSender } = await import("./trace-MPGQYXNR.js");
4173
+ const state = readState();
4174
+ if (!state) throw new Error("no trace is running here");
4175
+ const outcome = await flush(grantSender(state), { attempts: 4 });
4176
+ console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
4177
+ }));
4178
+ traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
4179
+ const { readState, spoolStatus } = await import("./trace-MPGQYXNR.js");
4180
+ const state = readState();
4181
+ if (!state) {
4182
+ console.log("No trace is running here.");
4183
+ return;
4184
+ }
4185
+ const spool = spoolStatus();
4186
+ 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`);
4187
+ }));
4188
+ 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) => {
4189
+ const { readState } = await import("./trace-MPGQYXNR.js");
4190
+ const ref = id ?? readState()?.trace_id;
4191
+ if (!ref) throw new Error("name a trace, or start one here");
4192
+ const bundle = await api("GET", `/traces/${ref}`);
4193
+ if (opts.json) {
4194
+ console.log(JSON.stringify(bundle, null, 2));
4195
+ return;
4196
+ }
4197
+ console.log(`${bundle.trace.id}${bundle.trace.name ? ` ${bundle.trace.name}` : ""}`);
4198
+ const children = /* @__PURE__ */ new Map();
4199
+ for (const span of bundle.spans) {
4200
+ const parent = span.parent_span_id && bundle.spans.some((other) => other.span_id === span.parent_span_id) ? span.parent_span_id : null;
4201
+ children.set(parent, [...children.get(parent) ?? [], span]);
4202
+ }
4203
+ const print = (parent, depth) => {
4204
+ for (const span of children.get(parent) ?? []) {
4205
+ const withContent = span.events.filter((event) => event.content).length;
4206
+ 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` : ""}`);
4207
+ print(span.span_id, depth + 1);
4208
+ }
4209
+ };
4210
+ print(null, 0);
4211
+ 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})`);
4212
+ console.log(` cost: $${bundle.consumed_cost.amount} \xB7 coverage: ${bundle.coverage}`);
4213
+ for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
4214
+ }));
4215
+ traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
4216
+ const { readState, record, flush, grantSender, clearState } = await import("./trace-MPGQYXNR.js");
4217
+ const state = readState();
4218
+ if (!state) throw new Error("no trace is running here");
4219
+ if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
4220
+ const outcome = await flush(grantSender(readState() ?? state), { attempts: 4 });
4221
+ await api("DELETE", `/traces/${state.trace_id}/grants/${state.grant_id}`);
4222
+ clearState();
4223
+ console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
4224
+ }));
4225
+ var workerCmd = program.command("worker").description("run a pod on this machine and connect it to a worker pool");
4226
+ 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) => {
4227
+ const { startWorker } = await import("./worker-6Q4K4HZW.js");
4228
+ const token = options.token ?? process.env.SKY_WORKER_TOKEN ?? "";
4229
+ if (!token) {
4230
+ console.error("error: provide --token or set SKY_WORKER_TOKEN");
4231
+ process.exit(1);
4232
+ }
4233
+ const base = (loadConfig().apiBase ?? "https://api.agentsky.dev").replace(/\/$/, "");
4234
+ const ac = new AbortController();
4235
+ process.on("SIGINT", () => ac.abort());
4236
+ process.on("SIGTERM", () => ac.abort());
4237
+ await startWorker({ poolId, token, label: options.label, apiBase: base, signal: ac.signal });
4238
+ });
3695
4239
  var ARGUMENTS_FOLLOW_THE_SCRIPT_PATH_ON_EVERY_RUNTIME = { from: "node" };
3696
4240
  program.parseAsync(process.argv, ARGUMENTS_FOLLOW_THE_SCRIPT_PATH_ON_EVERY_RUNTIME).catch(fail);