@agentsky/sky 0.2.1 → 2.2.1

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,32 +1,66 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- apiBase,
4
- apiToken,
5
- configPath,
6
- currentUniverse,
7
- loadConfig,
8
- saveConfig
9
- } from "./chunk-NLEDGUN7.js";
10
2
  import {
11
3
  AGENT_TYPES,
4
+ REASONING_EFFORT_VALUES,
12
5
  capabilitiesSchema,
13
6
  customDataSchema,
14
- customInstallSchema,
15
7
  external_exports,
16
- mcpConfigInstall,
8
+ isEffortSupported,
9
+ mcpServersFromKept,
10
+ modelSubscriptionProviderFor,
11
+ reasoningEffortDocLine,
17
12
  skillRefSchema,
18
- splitMcpServers
19
- } from "./chunk-L5LLCKZW.js";
20
- import "./chunk-MLKGABMK.js";
13
+ splitMcpServers,
14
+ supersededBy
15
+ } from "./chunk-TMLNYYYY.js";
21
16
 
22
17
  // src/index.ts
23
- import { existsSync as existsSync4 } from "fs";
24
- import { basename as basename2, resolve as resolve3 } from "path";
18
+ import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
19
+ import { basename as basename3, resolve as resolve3 } from "path";
25
20
  import { createInterface as createInterface3 } from "readline";
26
21
  import { Command } from "commander";
27
22
 
23
+ // src/config.ts
24
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
25
+ import { homedir } from "os";
26
+ import { dirname, join } from "path";
27
+ var DEFAULT_API_BASE = "https://api.agentsky.dev/v1";
28
+ var DEFAULT_CONSOLE_BASE = "https://platform.agentsky.dev";
29
+ function configPath() {
30
+ return join(process.env.SKY_CONFIG_DIR ?? join(homedir(), ".config", "sky"), "config.json");
31
+ }
32
+ function loadConfig() {
33
+ try {
34
+ return JSON.parse(readFileSync(configPath(), "utf8"));
35
+ } catch {
36
+ return {};
37
+ }
38
+ }
39
+ function saveConfig(config) {
40
+ const path = configPath();
41
+ mkdirSync(dirname(path), { recursive: true });
42
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
43
+ `, { mode: 384 });
44
+ }
45
+ function apiBase() {
46
+ return (process.env.SKY_API_URL ?? loadConfig().apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
47
+ }
48
+ function consoleBase() {
49
+ const configured = process.env.SKY_CONSOLE_URL ?? loadConfig().consoleBase;
50
+ if (configured) return configured.replace(/\/$/, "");
51
+ const api2 = apiBase();
52
+ if (api2 === DEFAULT_API_BASE) return DEFAULT_CONSOLE_BASE;
53
+ return api2.replace(/\/(?:api\/)?v1$/, "") || api2;
54
+ }
55
+ function apiToken() {
56
+ return process.env.SKY_API_TOKEN ?? loadConfig().token;
57
+ }
58
+ function currentUniverse() {
59
+ return process.env.SKY_UNIVERSE ?? loadConfig().currentUniverse;
60
+ }
61
+
28
62
  // src/version.ts
29
- var CLI_VERSION = "0.2.1";
63
+ var CLI_VERSION = "2.2.1";
30
64
  var USER_AGENT = `sky-cli/${CLI_VERSION} (${process.platform}; node/${process.versions.node})`;
31
65
 
32
66
  // src/api.ts
@@ -68,35 +102,69 @@ async function api(method, path, body, opts = {}) {
68
102
  headers: {
69
103
  "user-agent": USER_AGENT,
70
104
  ...opts.auth === false ? {} : authHeaders(),
71
- ...body !== void 0 ? { "content-type": "application/json" } : {}
105
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
106
+ ...opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : {}
72
107
  },
73
108
  body: body !== void 0 ? JSON.stringify(body) : void 0
74
109
  });
75
110
  if (!res.ok) await parseError(res);
76
111
  return await res.json();
77
112
  }
113
+ async function postEvents(sessionId, events, idempotencyKey) {
114
+ const res = await fetch(`${apiBase()}/sessions/${encodeURIComponent(sessionId)}/events`, {
115
+ method: "POST",
116
+ headers: {
117
+ "user-agent": USER_AGENT,
118
+ ...authHeaders(),
119
+ "content-type": "application/json",
120
+ ...idempotencyKey ? { "idempotency-key": idempotencyKey } : {}
121
+ },
122
+ body: JSON.stringify({ events })
123
+ });
124
+ if (!res.ok) await parseError(res);
125
+ return await res.json();
126
+ }
127
+ async function uploadFile(bytes, filename, mediaType, expiresInSeconds) {
128
+ const form = new FormData();
129
+ form.append("file", new Blob([bytes], { type: mediaType }), filename);
130
+ if (expiresInSeconds) form.append("expires_in_seconds", String(expiresInSeconds));
131
+ const res = await fetch(`${apiBase()}/files`, {
132
+ method: "POST",
133
+ headers: { "user-agent": USER_AGENT, ...authHeaders() },
134
+ body: form
135
+ });
136
+ if (!res.ok) await parseError(res);
137
+ return await res.json();
138
+ }
139
+ async function downloadFile(fileId) {
140
+ const res = await fetch(`${apiBase()}/files/${encodeURIComponent(fileId)}/content`, {
141
+ headers: { "user-agent": USER_AGENT, ...authHeaders() }
142
+ });
143
+ if (!res.ok) await parseError(res);
144
+ return { bytes: new Uint8Array(await res.arrayBuffer()), mediaType: res.headers.get("content-type") ?? "application/octet-stream" };
145
+ }
78
146
  async function resolveSessionRef(ref) {
79
147
  if (ref.startsWith("sess-")) return ref;
80
148
  const { agent: agent2 } = await api(
81
149
  "GET",
82
- `/api/v1/agents/${encodeURIComponent(ref)}`
150
+ `/agents/${encodeURIComponent(ref)}`
83
151
  ).catch(async () => {
84
- await api("GET", `/api/v1/sessions/${encodeURIComponent(ref)}`);
85
- return { agent: { slug: ref, defaultSessionId: ref } };
152
+ await api("GET", `/sessions/${encodeURIComponent(ref)}`);
153
+ return { agent: { id: ref, defaultSessionId: ref } };
86
154
  });
87
155
  if (!agent2.defaultSessionId) {
88
156
  throw new ApiError(
89
157
  404,
90
158
  "no_sessions",
91
- `agent ${agent2.slug} has no sessions \u2014 create one: sky session create ${agent2.slug}`
159
+ `agent ${agent2.id} has no sessions \u2014 create one: sky session create ${agent2.id}`
92
160
  );
93
161
  }
94
162
  return agent2.defaultSessionId;
95
163
  }
96
164
  async function* streamEvents(sessionId, signal) {
97
- const { parseSseStream } = await import("./src-Y2XOFL3Z.js");
165
+ const { parseSseStream } = await import("./src-NNPV5GRJ.js");
98
166
  const res = await fetch(
99
- `${apiBase()}/api/v1/sessions/${encodeURIComponent(sessionId)}/stream`,
167
+ `${apiBase()}/sessions/${encodeURIComponent(sessionId)}/stream`,
100
168
  {
101
169
  headers: { "user-agent": USER_AGENT, ...authHeaders(), accept: "text/event-stream" },
102
170
  signal
@@ -117,9 +185,128 @@ async function* streamEvents(sessionId, signal) {
117
185
  }
118
186
  }
119
187
 
188
+ // src/files.ts
189
+ import { readFileSync as readFileSync2 } from "fs";
190
+ import { basename, extname } from "path";
191
+ var MEDIA_TYPES = {
192
+ ".png": "image/png",
193
+ ".jpg": "image/jpeg",
194
+ ".jpeg": "image/jpeg",
195
+ ".gif": "image/gif",
196
+ ".webp": "image/webp",
197
+ ".pdf": "application/pdf",
198
+ ".txt": "text/plain",
199
+ ".md": "text/markdown",
200
+ ".csv": "text/csv",
201
+ ".json": "application/json",
202
+ ".html": "text/html",
203
+ ".xml": "application/xml",
204
+ ".yaml": "application/yaml",
205
+ ".yml": "application/yaml"
206
+ };
207
+ function mediaTypeFor(path) {
208
+ return MEDIA_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
209
+ }
210
+ async function uploadLocalFile(path, expiresInSeconds) {
211
+ const bytes = readFileSync2(path);
212
+ return uploadFile(new Uint8Array(bytes), basename(path), mediaTypeFor(path), expiresInSeconds);
213
+ }
214
+ function fileBlock(file) {
215
+ const source = { type: "file", file_id: file.id };
216
+ return file.mime_type.startsWith("image/") ? { type: "image", source } : { type: "document", source, title: file.filename };
217
+ }
218
+ function eventsFor(text, files2, system) {
219
+ const content = [];
220
+ if (text.trim()) content.push({ type: "text", text });
221
+ for (const file of files2) content.push(fileBlock(file));
222
+ if (content.length === 0) throw new Error("nothing to send: pass text, --file or --image");
223
+ const events = [{ type: "user.message", content }];
224
+ if (system?.trim()) events.push({ type: "system.message", content: [{ type: "text", text: system }] });
225
+ return events;
226
+ }
227
+
228
+ // src/goal.ts
229
+ var CHECK_SEPARATOR = " :: ";
230
+ function checkCriterion(value) {
231
+ const at = value.indexOf(CHECK_SEPARATOR);
232
+ const text = at === -1 ? "" : value.slice(0, at).trim();
233
+ const command = (at === -1 ? value : value.slice(at + CHECK_SEPARATOR.length)).trim();
234
+ if (!command) throw new Error(`--check needs a command: ${JSON.stringify(value)}`);
235
+ return { text: text || `\`${command}\` succeeds`, command };
236
+ }
237
+ function wholeNumber(flag, value, min) {
238
+ if (!/^\d+$/.test(value.trim())) throw new Error(`${flag} must be a whole number, got ${JSON.stringify(value)}`);
239
+ const parsed = Number(value.trim());
240
+ if (!Number.isSafeInteger(parsed) || parsed < min) throw new Error(`${flag} must be at least ${min}`);
241
+ return parsed;
242
+ }
243
+ function clearable(value) {
244
+ return value !== void 0;
245
+ }
246
+ function limitFields(opts) {
247
+ const body = {};
248
+ if (opts.maxIterations !== void 0) body.max_iterations = wholeNumber("--max-iterations", opts.maxIterations, 1);
249
+ if (clearable(opts.maxSpendCents)) {
250
+ body.max_spend = opts.maxSpendCents === "none" ? null : { amount: String(wholeNumber("--max-spend-cents", opts.maxSpendCents, 1)), currency: "USD" };
251
+ }
252
+ if (clearable(opts.maxTokens)) {
253
+ body.max_tokens = opts.maxTokens === "none" ? null : wholeNumber("--max-tokens", opts.maxTokens, 1);
254
+ }
255
+ if (clearable(opts.maxMinutes)) {
256
+ body.max_duration_minutes = opts.maxMinutes === "none" ? null : wholeNumber("--max-minutes", opts.maxMinutes, 1);
257
+ }
258
+ return body;
259
+ }
260
+ function criteriaFields(opts) {
261
+ const criteria = [...opts.criterion ?? [], ...(opts.check ?? []).map(checkCriterion)];
262
+ return criteria.length ? criteria : null;
263
+ }
264
+ function setGoalBody(objective, opts) {
265
+ const text = objective.trim();
266
+ if (!text) throw new Error("an objective is required");
267
+ const criteria = criteriaFields(opts);
268
+ return { objective: text, ...criteria ? { criteria } : {}, ...limitFields(opts) };
269
+ }
270
+ function editGoalBody(opts) {
271
+ const criteria = criteriaFields(opts);
272
+ const body = {
273
+ ...opts.objective !== void 0 ? { objective: opts.objective.trim() } : {},
274
+ ...criteria ? { criteria } : {},
275
+ ...limitFields(opts)
276
+ };
277
+ if (!Object.keys(body).length) throw new Error("nothing to change \u2014 pass --objective, --criterion, --check, or a limit");
278
+ return body;
279
+ }
280
+ function dollars(cents) {
281
+ return `$${(Number(cents) / 100).toFixed(2)}`;
282
+ }
283
+ function renderGoal(goal2) {
284
+ if (!goal2) return 'No goal. Set one: sky goal set <session> "<objective>"';
285
+ const lines = [];
286
+ const state = goal2.status === "stopped" && goal2.stop_reason ? `stopped (${goal2.stop_reason})` : goal2.status;
287
+ lines.push(`${goal2.id} ${state} iteration ${goal2.iteration}/${goal2.max_iterations}`);
288
+ lines.push(`objective: ${goal2.objective}`);
289
+ const spend = goal2.max_spend ? `${dollars(goal2.consumed_spend.amount)} of ${dollars(goal2.max_spend.amount)}` : dollars(goal2.consumed_spend.amount);
290
+ const tokens = goal2.max_tokens === null ? `${goal2.consumed_tokens}` : `${goal2.consumed_tokens} of ${goal2.max_tokens}`;
291
+ lines.push(`spent: ${spend} tokens: ${tokens}${goal2.deadline_at ? ` deadline: ${goal2.deadline_at}` : ""}`);
292
+ const verdicts = new Map((goal2.last_evaluation?.criteria ?? []).map((c) => [c.id, c]));
293
+ lines.push(`criteria${goal2.criteria_source === "derived" ? " (written from the objective)" : ""}:`);
294
+ for (const criterion of goal2.criteria) {
295
+ const verdict = verdicts.get(criterion.id);
296
+ const mark = verdict ? verdict.met ? "[x]" : "[ ]" : "[?]";
297
+ lines.push(` ${mark} ${criterion.id} ${criterion.text}`);
298
+ if (criterion.command) lines.push(` $ ${criterion.command}`);
299
+ if (verdict && !verdict.met && verdict.reason) lines.push(` ${verdict.reason}`);
300
+ }
301
+ if (goal2.last_evaluation && goal2.last_evaluation.result !== "satisfied") {
302
+ lines.push(`last check (iteration ${goal2.last_evaluation.iteration}): ${goal2.last_evaluation.explanation}`);
303
+ }
304
+ return lines.join("\n");
305
+ }
306
+
120
307
  // src/agent-file.ts
121
- import { existsSync, readFileSync, writeFileSync } from "fs";
122
- import { dirname, resolve } from "path";
308
+ import { existsSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
309
+ import { dirname as dirname2, resolve } from "path";
123
310
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
124
311
  var fileInstructionSchema = external_exports.object({
125
312
  name: external_exports.string(),
@@ -128,41 +315,49 @@ var fileInstructionSchema = external_exports.object({
128
315
  });
129
316
  var agentFileSchema = external_exports.object({
130
317
  name: external_exports.string().min(1),
131
- slug: external_exports.string().optional(),
318
+ id: external_exports.string().optional(),
132
319
  // stamped by launch/deploy; identifies the live agent
320
+ slug: external_exports.string().optional(),
321
+ // pre-RFC-0079 stamp; ignored (agents are id-addressed)
133
322
  description: external_exports.string().optional(),
134
323
  type: external_exports.enum(AGENT_TYPES).optional(),
135
324
  llm: external_exports.string().optional(),
325
+ reasoning_effort: external_exports.enum(REASONING_EFFORT_VALUES).optional(),
136
326
  capabilities: capabilitiesSchema.optional(),
137
327
  prompt: external_exports.string().optional(),
138
328
  prompt_file: external_exports.string().optional(),
139
329
  instructions: external_exports.array(fileInstructionSchema).optional(),
140
330
  skills: external_exports.array(skillRefSchema).optional(),
141
- customInstalls: external_exports.array(customInstallSchema).optional(),
142
331
  customData: external_exports.array(customDataSchema).optional(),
143
332
  /** Advanced machine shape (create-only): 1|2|4|8 vCPUs, 256MB-step memory. */
144
333
  vcpus: external_exports.number().int().optional(),
145
334
  memory_mb: external_exports.number().int().optional()
146
335
  });
147
336
  function readAgentFile(path) {
148
- const raw = readFileSync(path, "utf8");
337
+ const raw = readFileSync3(path, "utf8");
149
338
  const parsed = agentFileSchema.safeParse(parseToml(raw));
150
339
  if (!parsed.success) {
151
340
  const issue = parsed.error.issues[0];
152
341
  throw new Error(`${path}: ${issue?.path.join(".") ?? ""} ${issue?.message ?? "invalid"}`);
153
342
  }
154
343
  const file = parsed.data;
155
- const base = dirname(resolve(path));
344
+ if (file.reasoning_effort && file.type && !isEffortSupported(file.type, file.reasoning_effort)) {
345
+ throw new Error(
346
+ `${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
347
+ );
348
+ }
349
+ const base = dirname2(resolve(path));
156
350
  const inline = (ref, inlineContent) => {
157
351
  if (inlineContent !== void 0) return inlineContent;
158
352
  if (!ref) return void 0;
159
- return readFileSync(resolve(base, ref), "utf8");
353
+ return readFileSync3(resolve(base, ref), "utf8");
160
354
  };
161
355
  const spec = {
162
356
  name: file.name,
163
357
  description: file.description,
164
358
  agentType: file.type,
165
359
  llm: file.llm,
360
+ reasoningEffort: file.reasoning_effort,
166
361
  displayName: file.name,
167
362
  capabilities: file.capabilities,
168
363
  prompt: inline(file.prompt_file, file.prompt),
@@ -172,26 +367,27 @@ function readAgentFile(path) {
172
367
  return { name: f.name, content };
173
368
  }),
174
369
  skills: file.skills,
175
- customInstalls: file.customInstalls,
176
370
  customData: file.customData,
177
371
  vcpus: file.vcpus,
178
372
  memoryMb: file.memory_mb
179
373
  };
180
374
  return { file, spec };
181
375
  }
182
- function stampSlug(path, slug) {
183
- const doc = parseToml(readFileSync(path, "utf8"));
184
- doc.slug = slug;
185
- writeFileSync(path, stringifyToml(doc));
376
+ function stampId(path, id) {
377
+ const doc = parseToml(readFileSync3(path, "utf8"));
378
+ doc.id = id;
379
+ delete doc.slug;
380
+ writeFileSync2(path, stringifyToml(doc));
186
381
  }
187
382
  function scaffoldAgentFile(path, name) {
188
383
  if (existsSync(path)) throw new Error(`${path} already exists`);
189
- writeFileSync(
384
+ writeFileSync2(
190
385
  path,
191
386
  `# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
192
387
  name = "${name}"
193
- # type = "hermes" # hermes | claude_code | codex | openclaw
194
- # llm = "deepseek-v4-flash" # must be compatible with type
388
+ # type = "hermes" # hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode
389
+ # llm = "deepseek-flash" # must be compatible with type
390
+ # reasoning_effort = "high" # per harness \u2014 ${reasoningEffortDocLine()}
195
391
  # capabilities = []
196
392
 
197
393
  prompt = """
@@ -203,18 +399,18 @@ You are ${name}, a helpful long-lived agent.
203
399
 
204
400
  // src/clone.ts
205
401
  import { spawn } from "child_process";
206
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
207
- import { homedir } from "os";
208
- import { basename, join } from "path";
402
+ import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
403
+ import { homedir as homedir2 } from "os";
404
+ import { basename as basename2, join as join2 } from "path";
209
405
  import { createInterface } from "readline";
210
406
  var PROMPT_CAP = 1e5;
211
407
  var HARNESSES = ["claude_code", "codex", "openclaw"];
212
408
  function claudeConfigRoot() {
213
- return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
409
+ return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
214
410
  }
215
411
  function readIfExists(path) {
216
412
  try {
217
- return readFileSync2(path, "utf8");
413
+ return readFileSync4(path, "utf8");
218
414
  } catch {
219
415
  return void 0;
220
416
  }
@@ -230,9 +426,9 @@ function readJson(path) {
230
426
  }
231
427
  function claudeModel(configRoot) {
232
428
  const candidates = [
233
- join(process.cwd(), ".claude", "settings.local.json"),
234
- join(process.cwd(), ".claude", "settings.json"),
235
- join(configRoot, "settings.json")
429
+ join2(process.cwd(), ".claude", "settings.local.json"),
430
+ join2(process.cwd(), ".claude", "settings.json"),
431
+ join2(configRoot, "settings.json")
236
432
  ];
237
433
  for (const path of candidates) {
238
434
  const model = readJson(path)?.model;
@@ -242,11 +438,11 @@ function claudeModel(configRoot) {
242
438
  }
243
439
  function claudeMcpServers() {
244
440
  const merged = {};
245
- const userConfig = readJson(join(homedir(), ".claude.json"));
441
+ const userConfig = readJson(join2(homedir2(), ".claude.json"));
246
442
  Object.assign(merged, userConfig?.mcpServers ?? {});
247
443
  Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
248
444
  const projectConfig = readJson(
249
- join(process.cwd(), ".mcp.json")
445
+ join2(process.cwd(), ".mcp.json")
250
446
  );
251
447
  Object.assign(merged, projectConfig?.mcpServers ?? {});
252
448
  return merged;
@@ -263,13 +459,13 @@ function readClaudeCode() {
263
459
  const configRoot = claudeConfigRoot();
264
460
  const parts = [];
265
461
  const sources = [];
266
- const global = readIfExists(join(configRoot, "CLAUDE.md"));
462
+ const global = readIfExists(join2(configRoot, "CLAUDE.md"));
267
463
  if (global?.trim()) {
268
464
  parts.push({ label: "Global CLAUDE.md", content: global });
269
465
  sources.push("global CLAUDE.md");
270
466
  }
271
467
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
272
- const project = readIfExists(join(process.cwd(), name));
468
+ const project = readIfExists(join2(process.cwd(), name));
273
469
  if (project?.trim()) {
274
470
  parts.push({ label: `Project ${name}`, content: project });
275
471
  sources.push(`project ${name}`);
@@ -297,8 +493,8 @@ function detectHarness(explicit) {
297
493
  return explicit;
298
494
  }
299
495
  if (existsSync2(claudeConfigRoot())) return "claude_code";
300
- if (existsSync2(join(homedir(), ".codex"))) return "codex";
301
- if (existsSync2(join(homedir(), ".openclaw"))) return "openclaw";
496
+ if (existsSync2(join2(homedir2(), ".codex"))) return "codex";
497
+ if (existsSync2(join2(homedir2(), ".openclaw"))) return "openclaw";
302
498
  throw new Error(
303
499
  "no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
304
500
  );
@@ -310,14 +506,14 @@ function readLocal(harness) {
310
506
  );
311
507
  }
312
508
  function linkPath(configRoot) {
313
- return join(configRoot, "agentsky-link.json");
509
+ return join2(configRoot, "agentsky-link.json");
314
510
  }
315
511
  function readLink(configRoot) {
316
512
  const link = readJson(linkPath(configRoot));
317
- return link?.agentSlug ? link : void 0;
513
+ return link?.agentId ? link : void 0;
318
514
  }
319
515
  function writeLink(configRoot, link) {
320
- writeFileSync2(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
516
+ writeFileSync3(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
321
517
  `);
322
518
  }
323
519
  function confirm(question) {
@@ -346,7 +542,7 @@ async function createClone(local, opts) {
346
542
  `model ${local.model ?? "(harness default)"}`,
347
543
  `${keptNames.length} MCP server${keptNames.length === 1 ? "" : "s"}${keptNames.length ? ` (${keptNames.join(", ")})` : ""}`
348
544
  ].join(" \xB7 ");
349
- console.log(`Detected ${local.harness} at ${local.configRoot} (project ${basename(process.cwd())})`);
545
+ console.log(`Detected ${local.harness} at ${local.configRoot} (project ${basename2(process.cwd())})`);
350
546
  console.log(`Will clone: ${summary}`);
351
547
  if (local.truncated) console.log(" note: instructions exceeded 100k chars and were truncated");
352
548
  if (local.droppedServers.length) {
@@ -362,41 +558,48 @@ async function createClone(local, opts) {
362
558
  console.log("Aborted \u2014 nothing sent.");
363
559
  return;
364
560
  }
365
- const installs = keptNames.length ? [mcpConfigInstall(local.keptServers)] : [];
561
+ const { servers: mcpServers, skipped } = mcpServersFromKept(local.keptServers);
562
+ if (skipped.length) {
563
+ console.log(` note: local (stdio) MCP servers have no cloud equivalent and were skipped: ${skipped.join(", ")}`);
564
+ }
366
565
  const body = {
367
566
  agentType: local.harness,
368
567
  ...local.model ? { llm: local.model } : {},
369
568
  ...local.prompt ? { prompt: local.prompt } : {},
370
- ...installs.length ? { customInstalls: installs } : {}
569
+ ...mcpServers.length ? { mcpServers } : {}
371
570
  };
372
571
  let sentLlm = local.model ?? null;
373
572
  let created;
374
573
  try {
375
- ({ agent: created } = await api("POST", "/api/v1/agents", body));
574
+ ({ agent: created } = await api("POST", "/agents", body));
376
575
  } catch (error) {
377
576
  if (error instanceof ApiError && error.status === 422 && local.model) {
378
- console.log(` note: ${local.model} isn't available for ${local.harness} \u2014 using the harness default`);
379
- sentLlm = null;
380
- ({ agent: created } = await api("POST", "/api/v1/agents", {
577
+ const successor = supersededBy(local.model);
578
+ console.log(
579
+ successor ? ` note: ${local.model} is superseded \u2014 using ${successor}` : ` note: ${local.model} isn't available for ${local.harness} \u2014 using the harness default`
580
+ );
581
+ sentLlm = successor;
582
+ ({ agent: created } = await api("POST", "/agents", {
381
583
  ...body,
382
- llm: void 0
584
+ llm: successor ?? void 0
383
585
  }));
384
586
  } else {
385
587
  throw error;
386
588
  }
387
589
  }
388
- await api("POST", "/api/v1/sessions", { agent: created.slug });
590
+ console.log("Starting its runtime\u2026");
591
+ await api("POST", "/sessions", { agent: created.id });
389
592
  const now = (/* @__PURE__ */ new Date()).toISOString();
390
593
  writeLink(local.configRoot, {
391
594
  baseUrl: apiBase(),
392
- agentSlug: created.slug,
595
+ agentId: created.id,
393
596
  clonedAt: now,
394
597
  lastSyncedAt: now,
395
598
  llm: sentLlm,
396
599
  mcpServers: keptNames
397
600
  });
398
- const url = `${apiBase()}/agents/${created.slug}`;
399
- console.log(`Cloned \u2192 ${created.slug} ${url}`);
601
+ const url = `${apiBase()}/agents/${created.id}`;
602
+ console.log(`Cloned \u2192 ${created.id} ${url}`);
400
603
  console.log(
401
604
  `Linked this machine (${linkPath(local.configRoot)}). Push prompt changes anytime by running \`sky clone\` again.`
402
605
  );
@@ -405,11 +608,11 @@ async function createClone(local, opts) {
405
608
  async function syncClone(local, link, opts) {
406
609
  const since = link.lastSyncedAt ?? link.clonedAt;
407
610
  console.log(
408
- `Syncing ${link.agentSlug} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
611
+ `Syncing ${link.agentId} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
409
612
  );
410
613
  if (opts.dryRun) {
411
614
  console.log(
412
- ` would push prompt (${kb(local.prompt)}) to ${link.agentSlug}. Dry run \u2014 nothing sent.`
615
+ ` would push prompt (${kb(local.prompt)}) to ${link.agentId}. Dry run \u2014 nothing sent.`
413
616
  );
414
617
  return;
415
618
  }
@@ -417,24 +620,24 @@ async function syncClone(local, link, opts) {
417
620
  try {
418
621
  ({ agent: agent2 } = await api(
419
622
  "GET",
420
- `/api/v1/agents/${encodeURIComponent(link.agentSlug)}`
623
+ `/agents/${encodeURIComponent(link.agentId)}`
421
624
  ));
422
625
  } catch (error) {
423
626
  if (error instanceof ApiError && error.status === 404) {
424
627
  throw new Error(
425
- `linked agent ${link.agentSlug} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
628
+ `linked agent ${link.agentId} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
426
629
  );
427
630
  }
428
631
  throw error;
429
632
  }
430
633
  if (agent2.agentType !== local.harness) {
431
634
  throw new Error(
432
- `linked agent ${link.agentSlug} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
635
+ `linked agent ${link.agentId} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
433
636
  );
434
637
  }
435
638
  const result = await api(
436
639
  "PUT",
437
- `/api/v1/agents/${encodeURIComponent(link.agentSlug)}/prompt`,
640
+ `/agents/${encodeURIComponent(link.agentId)}/prompt`,
438
641
  { prompt: local.prompt }
439
642
  );
440
643
  console.log(
@@ -462,8 +665,8 @@ async function syncClone(local, link, opts) {
462
665
  );
463
666
  }
464
667
  writeLink(local.configRoot, { ...link, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
465
- console.log(`Synced ${link.agentSlug}.`);
466
- if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentSlug}`);
668
+ console.log(`Synced ${link.agentId}.`);
669
+ if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentId}`);
467
670
  }
468
671
  async function clone(opts) {
469
672
  const harness = detectHarness(opts.harness);
@@ -526,7 +729,7 @@ async function browserLogin(verifier, challenge) {
526
729
  server.listen(0, "127.0.0.1", () => {
527
730
  const address = server.address();
528
731
  const port = typeof address === "object" && address ? address.port : 0;
529
- const authorizeUrl = `${apiBase()}/cli/authorize?state=${state}&port=${port}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
732
+ const authorizeUrl = `${consoleBase()}/cli/authorize?state=${state}&port=${port}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
530
733
  console.log("Opening your browser to authorize this machine\u2026");
531
734
  console.log(`If it doesn't open, visit:
532
735
  ${authorizeUrl}`);
@@ -539,14 +742,14 @@ async function browserLogin(verifier, challenge) {
539
742
  });
540
743
  return api(
541
744
  "POST",
542
- "/api/v1/cli/token-exchange",
745
+ "/cli/token-exchange",
543
746
  { code, verifier, clientName: `cli ${hostname()}` },
544
747
  { auth: false }
545
748
  );
546
749
  }
547
750
  async function headlessLogin(verifier, challenge) {
548
751
  const request = b64url(randomBytes(24));
549
- const authorizeUrl = `${apiBase()}/cli/authorize?request=${request}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
752
+ const authorizeUrl = `${consoleBase()}/cli/authorize?request=${request}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
550
753
  console.log(`Visit this URL on any device to authorize:
551
754
  ${authorizeUrl}
552
755
  `);
@@ -555,7 +758,7 @@ async function headlessLogin(verifier, challenge) {
555
758
  while (Date.now() < deadline) {
556
759
  await new Promise((r) => setTimeout(r, 3e3));
557
760
  const res = await fetch(
558
- `${apiBase()}/api/v1/cli/poll?request=${request}&verifier=${encodeURIComponent(verifier)}&clientName=${encodeURIComponent(`cli ${hostname()}`)}`,
761
+ `${apiBase()}/cli/poll?request=${request}&verifier=${encodeURIComponent(verifier)}&clientName=${encodeURIComponent(`cli ${hostname()}`)}`,
559
762
  { headers: { "user-agent": USER_AGENT } }
560
763
  );
561
764
  if (!res.ok) {
@@ -575,7 +778,6 @@ async function headlessLogin(verifier, challenge) {
575
778
  // src/render.ts
576
779
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
577
780
  var red = (s) => `\x1B[31m${s}\x1B[0m`;
578
- var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
579
781
  function clip(value, max = 120) {
580
782
  const text = typeof value === "string" ? value : JSON.stringify(value);
581
783
  return text.length > max ? `${text.slice(0, max)}\u2026` : text;
@@ -598,18 +800,18 @@ function renderEvent(event, data) {
598
800
  case "agent.status":
599
801
  console.log(dim(`\u2026 ${String(part?.level ?? "")} ${String(part?.text ?? "")}`.trim()));
600
802
  break;
601
- case "user.message": {
602
- const channel = String(data.channel ?? "");
603
- if (channel && channel !== "api") {
604
- console.log(cyan(`[${channel}] ${clip(data.text ?? "")}`));
605
- }
803
+ case "user.interrupt":
804
+ console.log(dim("\u23F9 interrupting"));
606
805
  break;
607
- }
608
- case "turn.interrupted":
609
- console.log(dim("\u23F9 turn interrupted"));
806
+ case "session.error": {
807
+ const error = data.error;
808
+ const message = String(error?.message ?? error?.type ?? "unknown");
809
+ if (error?.retry_status === "retrying") console.error(dim(`retrying: ${message}`));
810
+ else console.error(red(`error: ${message}`));
610
811
  break;
611
- case "error":
612
- console.error(red(`error: ${String(data.message ?? data.code ?? "unknown")}`));
812
+ }
813
+ case "session.status_terminated":
814
+ console.log(dim("session terminated"));
613
815
  break;
614
816
  default:
615
817
  break;
@@ -618,9 +820,9 @@ function renderEvent(event, data) {
618
820
 
619
821
  // src/subscriptions.ts
620
822
  import { spawn as spawn3 } from "child_process";
621
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
622
- import { homedir as homedir2 } from "os";
623
- import { join as join2 } from "path";
823
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
824
+ import { homedir as homedir3 } from "os";
825
+ import { join as join3 } from "path";
624
826
  import { createInterface as createInterface2 } from "readline";
625
827
  var ALIASES = {
626
828
  claude: "anthropic",
@@ -678,7 +880,7 @@ function mintClaudeToken() {
678
880
  async function put(provider, credential) {
679
881
  const { modelSubscription } = await api(
680
882
  "PUT",
681
- `/api/v1/model-subscriptions/${provider}`,
883
+ `/model-subscriptions/${provider}`,
682
884
  { credential }
683
885
  );
684
886
  return modelSubscription;
@@ -698,12 +900,12 @@ async function connectClaude() {
698
900
  console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
699
901
  }
700
902
  async function connectChatgpt(opts) {
701
- const codexHome = process.env.CODEX_HOME || join2(homedir2(), ".codex");
702
- const path = join2(codexHome, "auth.json");
903
+ const codexHome = process.env.CODEX_HOME || join3(homedir3(), ".codex");
904
+ const path = join3(codexHome, "auth.json");
703
905
  if (!existsSync3(path)) {
704
906
  throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
705
907
  }
706
- const raw = readFileSync3(path, "utf8");
908
+ const raw = readFileSync5(path, "utf8");
707
909
  let accountId;
708
910
  try {
709
911
  const parsed = JSON.parse(raw);
@@ -725,7 +927,7 @@ async function connectChatgpt(opts) {
725
927
  async function listSubscriptions() {
726
928
  const { modelSubscriptions } = await api(
727
929
  "GET",
728
- "/api/v1/model-subscriptions"
930
+ "/model-subscriptions"
729
931
  );
730
932
  if (modelSubscriptions.length === 0) {
731
933
  console.log("No model subscriptions connected. Try `sky auth connect claude` or `sky auth connect chatgpt`.");
@@ -741,7 +943,7 @@ async function listSubscriptions() {
741
943
  s.useForNewAgents ? "used for new agents" : "not used for new agents"
742
944
  ].filter(Boolean);
743
945
  console.log(bits.join(" \xB7 "));
744
- for (const a of s.agents) console.log(` ${a.slug ?? a.id} \u2014 ${a.name}`);
946
+ for (const a of s.agents) console.log(` ${a.agentId ?? a.id} \u2014 ${a.name}`);
745
947
  }
746
948
  }
747
949
  async function disconnect(name, opts) {
@@ -756,18 +958,25 @@ async function disconnect(name, opts) {
756
958
  }
757
959
  const { affectedAgents } = await api(
758
960
  "DELETE",
759
- `/api/v1/model-subscriptions/${provider}`
961
+ `/model-subscriptions/${provider}`
760
962
  );
761
963
  console.log(`Disconnected ${label}.`);
762
964
  if (affectedAgents.length > 0) {
763
965
  console.log(
764
- `${affectedAgents.length} agent${affectedAgents.length === 1 ? "" : "s"} still point at it and will fail: ` + affectedAgents.map((a) => a.slug ?? a.id).join(", ")
966
+ `${affectedAgents.length} agent${affectedAgents.length === 1 ? "" : "s"} still point at it and will fail: ` + affectedAgents.map((a) => a.agentId ?? a.id).join(", ")
765
967
  );
766
968
  console.log("Switch them to Platform in each agent's Configure tab (or reconnect).");
767
969
  }
768
970
  }
769
971
 
770
972
  // src/index.ts
973
+ function printModelBilling(session2) {
974
+ if (session2.modelBilling !== "account") return;
975
+ const provider = modelSubscriptionProviderFor(session2.agentType, session2.llm);
976
+ if (!provider) return;
977
+ const label = provider === "anthropic" ? "your Claude subscription" : "your ChatGPT plan";
978
+ console.log(`Model billing: ${label} \u2014 $0 model usage`);
979
+ }
771
980
  var program = new Command();
772
981
  program.name("sky").description("Launch and talk to long-lived agents \u2014 agentsky.dev").version(CLI_VERSION);
773
982
  function fail(error) {
@@ -778,15 +987,15 @@ function fail(error) {
778
987
  var run = (fn) => (...args) => fn(...args).catch(fail);
779
988
  var auth = program.command("auth").description("login, logout, whoami, model subscriptions");
780
989
  auth.command("login").description("authorize this machine via the browser").option("--no-browser", "print a URL and poll instead of opening a browser").action(run(async (opts) => login({ noBrowser: opts.browser === false })));
781
- auth.command("logout").description("forget the stored token (revoke it in Settings \u2192 API tokens)").action(
990
+ auth.command("logout").description("forget the stored token (revoke it in Developer \u2192 API keys)").action(
782
991
  run(async () => {
783
992
  saveConfig({ ...loadConfig(), token: void 0, email: void 0 });
784
- console.log(`Logged out. (Token revocation: Settings \u2192 API tokens.)`);
993
+ console.log(`Logged out. (Token revocation: Developer \u2192 API keys.)`);
785
994
  })
786
995
  );
787
996
  auth.command("whoami").description("who the current token acts as").action(
788
997
  run(async () => {
789
- const who = await api("GET", "/api/v1/whoami");
998
+ const who = await api("GET", "/whoami");
790
999
  console.log(
791
1000
  `${who.user.email ?? "unknown"} \xB7 universe ${who.universe.slug}${who.universe.isPersonal ? " (personal)" : ""} \xB7 scopes ${who.scopes.join(",")}`
792
1001
  );
@@ -804,7 +1013,7 @@ auth.command("disconnect <provider>").description("disconnect a model subscripti
804
1013
  var universe = program.command("universe").description("grouping boundary for agents");
805
1014
  universe.command("list").action(
806
1015
  run(async () => {
807
- const { universes } = await api("GET", "/api/v1/universes");
1016
+ const { universes } = await api("GET", "/universes");
808
1017
  const current = currentUniverse();
809
1018
  for (const u of universes) {
810
1019
  const marker = u.slug === current || !current && u.isPersonal ? "*" : " ";
@@ -814,13 +1023,13 @@ universe.command("list").action(
814
1023
  );
815
1024
  universe.command("create <slug>").action(
816
1025
  run(async (slug) => {
817
- const created = await api("POST", "/api/v1/universes", { slug });
1026
+ const created = await api("POST", "/universes", { slug });
818
1027
  console.log(`Created universe ${created.universe.slug}`);
819
1028
  })
820
1029
  );
821
1030
  universe.command("switch <slug>").action(
822
1031
  run(async (slug) => {
823
- await api("GET", `/api/v1/universes/${encodeURIComponent(slug)}`);
1032
+ await api("GET", `/universes/${encodeURIComponent(slug)}`);
824
1033
  saveConfig({ ...loadConfig(), currentUniverse: slug });
825
1034
  console.log(`Switched to ${slug}`);
826
1035
  })
@@ -828,7 +1037,7 @@ universe.command("switch <slug>").action(
828
1037
  universe.command("show").action(
829
1038
  run(async () => {
830
1039
  const slug = currentUniverse();
831
- const path = slug ? `/api/v1/universes/${encodeURIComponent(slug)}` : "/api/v1/whoami";
1040
+ const path = slug ? `/universes/${encodeURIComponent(slug)}` : "/whoami";
832
1041
  if (slug) {
833
1042
  const { universe: u } = await api("GET", path);
834
1043
  console.log(`${u.slug} \u2014 ${u.name} \xB7 ${u.agents} agents \xB7 ${u.members} members`);
@@ -839,22 +1048,37 @@ universe.command("show").action(
839
1048
  })
840
1049
  );
841
1050
  var agent = program.command("agent").description("agent specs: create, inspect, talk (via the default session)");
842
- agent.command("create").description("create an agent spec \u2014 no runtime; `sky session create` starts one").option("--type <type>", "hermes | claude_code | codex | openclaw").option("--llm <llm>", "model id (defaults per type)").option("--name <name>", "agent name").option("-f, --file <path>", "create from an agent.toml").action(
843
- run(async (opts) => {
844
- const spec = opts.file ? readAgentFile(opts.file).spec : {
845
- name: opts.name ?? "My agent",
846
- agentType: opts.type,
847
- llm: opts.llm,
848
- displayName: opts.name
849
- };
850
- const { agent: created } = await api("POST", "/api/v1/agents", spec);
851
- console.log(`Created ${created.slug} (${created.agentType} \xB7 ${created.llm}) \u2014 spec only.`);
852
- console.log(`Start it: sky session create ${created.slug}`);
853
- })
1051
+ agent.command("create").description("create an agent spec \u2014 no runtime; `sky session create` starts one").option("--type <type>", "hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode").option("--llm <llm>", "model id (defaults per type)").option(
1052
+ "--reasoning-effort <level>",
1053
+ `harness-native reasoning effort (default: the harness's own) \u2014 ${reasoningEffortDocLine()}`
1054
+ ).option("--name <name>", "agent name").option(
1055
+ "--capability <id>",
1056
+ "grant a built-in capability (repeatable; ids at /docs/capabilities; none by default)",
1057
+ (id, all) => [...all, id],
1058
+ []
1059
+ ).option("-f, --file <path>", "create from an agent.toml").action(
1060
+ run(
1061
+ async (opts) => {
1062
+ const spec = opts.file ? readAgentFile(opts.file).spec : {
1063
+ name: opts.name ?? "My agent",
1064
+ agentType: opts.type,
1065
+ llm: opts.llm,
1066
+ reasoningEffort: opts.reasoningEffort,
1067
+ displayName: opts.name,
1068
+ ...opts.capability.length ? { capabilities: opts.capability } : {}
1069
+ };
1070
+ const { agent: created } = await api("POST", "/agents", spec);
1071
+ console.log(`Created ${created.id} (${created.agentType} \xB7 ${created.llm}) \u2014 spec only.`);
1072
+ console.log(
1073
+ created.capabilities.length ? `Capabilities: ${created.capabilities.join(", ")}` : "Capabilities: none (grant with --capability <id> or PATCH capabilities)"
1074
+ );
1075
+ console.log(`Start it: sky session create ${created.id}`);
1076
+ }
1077
+ )
854
1078
  );
855
1079
  agent.command("list").action(
856
1080
  run(async () => {
857
- const { agents } = await api("GET", "/api/v1/agents");
1081
+ const { agents } = await api("GET", "/agents");
858
1082
  if (!agents.length) {
859
1083
  console.log("No agents yet. `sky launch` or `sky agent create` to get one.");
860
1084
  return;
@@ -862,16 +1086,17 @@ agent.command("list").action(
862
1086
  for (const a of agents) {
863
1087
  const state = a.archived ? "archived" : `${a.sessionCount} session${a.sessionCount === 1 ? "" : "s"}`;
864
1088
  console.log(
865
- `${a.slug.padEnd(24)} ${a.displayName.padEnd(20)} ${a.agentType}/${a.llm} ${state}`
1089
+ `${a.id.padEnd(32)} ${a.displayName.padEnd(20)} ${a.agentType}/${a.llm} ${state}`
866
1090
  );
867
1091
  }
868
1092
  })
869
1093
  );
870
- agent.command("show <slug>").action(
871
- run(async (slug) => {
872
- const { agent: a } = await api("GET", `/api/v1/agents/${encodeURIComponent(slug)}`);
873
- console.log(`${a.slug} \u2014 ${a.displayName}${a.archived ? " (archived)" : ""}`);
1094
+ agent.command("show <id>").action(
1095
+ run(async (id) => {
1096
+ const { agent: a } = await api("GET", `/agents/${encodeURIComponent(id)}`);
1097
+ console.log(`${a.id} \u2014 ${a.displayName}${a.archived ? " (archived)" : ""}`);
874
1098
  console.log(` type ${a.agentType} \xB7 ${a.llm}`);
1099
+ if (a.reasoningEffort) console.log(` effort ${a.reasoningEffort}`);
875
1100
  console.log(` version ${a.version}`);
876
1101
  console.log(` sessions ${a.sessionCount}`);
877
1102
  console.log(` universe ${a.universe}`);
@@ -879,42 +1104,72 @@ agent.command("show <slug>").action(
879
1104
  if (a.prompt) console.log(` prompt ${a.prompt.split("\n")[0]?.slice(0, 80)}\u2026`);
880
1105
  })
881
1106
  );
882
- agent.command("archive <slug>").description("retire the spec \u2014 read-only, running sessions unaffected (admin scope)").action(
883
- run(async (slug) => {
884
- await api("POST", `/api/v1/agents/${encodeURIComponent(slug)}/archive`);
885
- console.log(`Archived ${slug} \u2014 sessions keep running; new sessions are rejected.`);
1107
+ agent.command("archive <id>").description("retire the spec \u2014 read-only, running sessions unaffected (admin scope)").action(
1108
+ run(async (id) => {
1109
+ await api("POST", `/agents/${encodeURIComponent(id)}/archive`);
1110
+ console.log(`Archived ${id} \u2014 sessions keep running; new sessions are rejected.`);
886
1111
  })
887
1112
  );
888
- agent.command("destroy <slug>").description("delete the spec; requires all sessions destroyed first (admin scope)").action(
889
- run(async (slug) => {
890
- await api("DELETE", `/api/v1/agents/${encodeURIComponent(slug)}`);
891
- console.log(`Destroyed ${slug}`);
1113
+ agent.command("destroy <id>").description("delete the spec; requires all sessions deleted first (admin scope)").action(
1114
+ run(async (id) => {
1115
+ await api("DELETE", `/agents/${encodeURIComponent(id)}`);
1116
+ console.log(`Destroyed ${id}`);
892
1117
  })
893
1118
  );
894
- agent.command("message <slug> <text>").description("send one message to the default session and stream the turn until idle").option("--detach", "don't follow the stream; exit after the 202").action(
1119
+ agent.command("message <id> [text]").description("send one message to the default session and stream the turn until idle").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks)").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").action(
895
1120
  run(
896
- async (slug, text, opts) => sendAndFollow(await resolveSessionRef(slug), text, Boolean(opts.detach))
1121
+ async (id, text, opts) => sendAndFollow(await resolveSessionRef(id), text ?? "", opts)
897
1122
  )
898
1123
  );
899
- agent.command("chat <slug>").description("interactive REPL with the default session").action(run(async (slug) => chatLoop(await resolveSessionRef(slug))));
1124
+ agent.command("chat <id>").description("interactive REPL with the default session").option("--system <text>", "per-turn system text sent with the first message").action(run(async (id, opts) => chatLoop(await resolveSessionRef(id), opts)));
900
1125
  var session = program.command("session").description("sessions: the running instances of an agent");
901
- session.command("create <agent>").description("create a session (cheap \u2014 the pod starts on the first message)").option("--title <title>", "session title").option("--message <text>", "seed the session and start 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)").action(
902
- run(async (agentRef, opts) => {
903
- const { session: created } = await api("POST", "/api/v1/sessions", {
904
- agent: agentRef,
905
- ...opts.title ? { title: opts.title } : {},
906
- ...opts.vcpus !== void 0 ? { vcpus: Number(opts.vcpus) } : {},
907
- ...opts.memory !== void 0 ? { memoryMb: Number(opts.memory) } : {},
908
- ...opts.message ? { initial_events: [{ type: "user.message", parts: [{ type: "text", index: 0, text: opts.message }] }] } : {}
909
- });
910
- console.log(`Created ${created.id} (${created.agent} \xB7 ${created.status})${created.default ? " [default]" : ""}`);
911
- console.log(` sky agent message ${created.agent} "hello"`);
912
- })
1126
+ async function fileResource(spec) {
1127
+ const at = spec.lastIndexOf(":/");
1128
+ const path = at > 0 ? spec.slice(0, at) : spec;
1129
+ const mount = at > 0 ? spec.slice(at + 1) : void 0;
1130
+ const file = await uploadLocalFile(path);
1131
+ console.error(`uploaded ${file.filename} \u2192 ${file.id}`);
1132
+ return { type: "file", file_id: file.id, ...mount ? { mount_path: mount } : {} };
1133
+ }
1134
+ function repoResource(spec, token) {
1135
+ const at = spec.lastIndexOf("@");
1136
+ const url = at > "https://".length ? spec.slice(0, at) : spec;
1137
+ const ref = at > "https://".length ? spec.slice(at + 1) : "";
1138
+ const checkout = ref ? /^[0-9a-f]{7,64}$/.test(ref) ? { type: "commit", sha: ref } : { type: "branch", name: ref } : void 0;
1139
+ return { type: "github_repository", url, ...checkout ? { checkout } : {}, ...token ? { authorization_token: token } : {} };
1140
+ }
1141
+ async function resourceParams(opts) {
1142
+ const files2 = await Promise.all((opts.file ?? []).map(fileResource));
1143
+ return [...files2, ...(opts.repo ?? []).map((spec) => repoResource(spec, opts.repoToken))];
1144
+ }
1145
+ function renderResource(r) {
1146
+ if (r.type === "file") return `${r.id} file ${r.mount_path} ${r.file_id}`;
1147
+ const ref = r.checkout ? `@${r.checkout.type === "branch" ? r.checkout.name : r.checkout.sha}` : "";
1148
+ return `${r.id} repo ${r.mount_path} ${r.url}${ref}`;
1149
+ }
1150
+ session.command("create <agent>").description("create a session (the pod is provisioned now)").option("--title <title>", "session title").option("--message <text>", "seed the session and start 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").action(
1151
+ run(
1152
+ async (agentRef, opts) => {
1153
+ const resources = await resourceParams(opts);
1154
+ const { session: created } = await api("POST", "/sessions", {
1155
+ agent: agentRef,
1156
+ ...opts.title ? { title: opts.title } : {},
1157
+ ...opts.vcpus !== void 0 ? { vcpus: Number(opts.vcpus) } : {},
1158
+ ...opts.memory !== void 0 ? { memoryMb: Number(opts.memory) } : {},
1159
+ ...resources.length ? { resources } : {},
1160
+ ...opts.message ? { initial_events: [{ type: "user.message", content: [{ type: "text", text: opts.message }] }] } : {}
1161
+ });
1162
+ console.log(`Created ${created.id} (${created.agent} \xB7 ${created.status})${created.default ? " [default]" : ""}`);
1163
+ printModelBilling(created);
1164
+ for (const r of created.resources ?? []) console.log(` ${renderResource(r)}`);
1165
+ console.log(` sky agent message ${created.agent} "hello"`);
1166
+ }
1167
+ )
913
1168
  );
914
1169
  session.command("list [agent]").action(
915
1170
  run(async (agentRef) => {
916
1171
  const qs = agentRef ? `?agent=${encodeURIComponent(agentRef)}` : "";
917
- const { sessions } = await api("GET", `/api/v1/sessions${qs}`);
1172
+ const { sessions } = await api("GET", `/sessions${qs}`);
918
1173
  if (!sessions.length) {
919
1174
  console.log("No sessions. `sky session create <agent>` to start one.");
920
1175
  return;
@@ -930,7 +1185,7 @@ session.command("show <id>").action(
930
1185
  run(async (id) => {
931
1186
  const { session: s } = await api(
932
1187
  "GET",
933
- `/api/v1/sessions/${encodeURIComponent(id)}`
1188
+ `/sessions/${encodeURIComponent(id)}`
934
1189
  );
935
1190
  console.log(`${s.id}${s.default ? " [default]" : ""}`);
936
1191
  console.log(` agent ${s.agent ?? "-"}`);
@@ -939,68 +1194,113 @@ session.command("show <id>").action(
939
1194
  console.log(` model ${s.agentType} \xB7 ${s.llm}`);
940
1195
  })
941
1196
  );
942
- session.command("destroy <id>").description("end the session and tear down its pod (admin scope)").action(
1197
+ function failWithInterruptHint(error, id) {
1198
+ if (error instanceof ApiError && error.code === "session_running") {
1199
+ console.error(
1200
+ `sky: session is running \u2014 \`sky session interrupt ${id}\`, wait for idle, then retry`
1201
+ );
1202
+ process.exit(1);
1203
+ }
1204
+ throw error;
1205
+ }
1206
+ session.command("archive <id>").description("irreversibly archive: no new messages, pod torn down, history stays readable").action(
1207
+ run(async (id) => {
1208
+ await api("POST", `/sessions/${encodeURIComponent(id)}/archive`).catch(
1209
+ (error) => failWithInterruptHint(error, id)
1210
+ );
1211
+ console.log(`Archived ${id} \u2014 terminated; history stays readable. This is irreversible.`);
1212
+ })
1213
+ );
1214
+ session.command("delete <id>").description("permanently delete the session, its events, and its sandbox").action(
943
1215
  run(async (id) => {
944
- await api("DELETE", `/api/v1/sessions/${encodeURIComponent(id)}`);
945
- console.log(`Destroyed ${id}`);
1216
+ await api("DELETE", `/sessions/${encodeURIComponent(id)}`).catch(
1217
+ (error) => failWithInterruptHint(error, id)
1218
+ );
1219
+ console.log(`Deleted ${id}`);
946
1220
  })
947
1221
  );
948
- session.command("message <ref> <text>").description("send one message (session id, or agent slug = its default session)").option("--detach", "don't follow the stream; exit after the 202").action(
1222
+ session.command("message <ref> [text]").description("send one message (session id, or agent id = its default session)").option("--detach", "don't follow the stream; exit once the message is accepted").option("--file <path...>", "attach local files (uploaded first, sent as document blocks)").option("--image <path...>", "attach local images (uploaded first, sent as image blocks)").option("--system <text>", "per-turn system text sent alongside the message").action(
949
1223
  run(
950
- async (ref, text, opts) => sendAndFollow(await resolveSessionRef(ref), text, Boolean(opts.detach))
1224
+ async (ref, text, opts) => sendAndFollow(await resolveSessionRef(ref), text ?? "", opts)
951
1225
  )
952
1226
  );
953
- session.command("chat <ref>").description("interactive REPL over the standing stream").action(run(async (ref) => chatLoop(await resolveSessionRef(ref))));
1227
+ session.command("chat <ref>").description("interactive REPL over the standing stream").option("--system <text>", "per-turn system text sent with the first message").action(run(async (ref, opts) => chatLoop(await resolveSessionRef(ref), opts)));
954
1228
  session.command("interrupt <ref>").description("abort the in-flight turn").action(
955
1229
  run(async (ref) => {
956
1230
  const id = await resolveSessionRef(ref);
957
1231
  const res = await api(
958
1232
  "POST",
959
- `/api/v1/sessions/${encodeURIComponent(id)}/interrupt`
1233
+ `/sessions/${encodeURIComponent(id)}/interrupt`
960
1234
  );
961
1235
  console.log(res.status === "no_turn" ? "nothing in flight" : "interrupting");
962
1236
  })
963
1237
  );
964
- session.command("logs <ref>").option("-f, --follow", "follow (SSE)").option("--since <duration>", "how far back, e.g. 1h, 30m", "1h").action(
965
- run(async (ref, opts) => {
1238
+ var sessionResources = session.command("resources").description("files and GitHub repositories mounted into a session's working directory");
1239
+ var resourcesPath = (id) => `/sessions/${encodeURIComponent(id)}/resources`;
1240
+ sessionResources.command("list <ref>").description("the session's resources, oldest first").action(
1241
+ run(async (ref) => {
966
1242
  const id = await resolveSessionRef(ref);
967
- const qs = `?since=${encodeURIComponent(opts.since)}${opts.follow ? "&follow=true" : ""}`;
968
- if (!opts.follow) {
969
- const { lines } = await api(
970
- "GET",
971
- `/api/v1/sessions/${encodeURIComponent(id)}/logs${qs}`
972
- );
973
- for (const entry of lines) console.log(`${entry.at} ${entry.line}`);
1243
+ const { data } = await api("GET", resourcesPath(id));
1244
+ if (!data.length) {
1245
+ console.log("No resources. `sky session resources add <session> <path>` mounts a file.");
974
1246
  return;
975
1247
  }
976
- const { parseSseStream } = await import("./src-Y2XOFL3Z.js");
977
- const { apiBase: apiBase2 } = await import("./config-HRDRNTUU.js");
978
- const token = apiToken();
979
- const res = await fetch(`${apiBase2()}/api/v1/sessions/${encodeURIComponent(id)}/logs${qs}`, {
980
- headers: {
981
- "user-agent": USER_AGENT,
982
- authorization: `Bearer ${token}`,
983
- accept: "text/event-stream"
984
- }
1248
+ for (const r of data) console.log(renderResource(r));
1249
+ })
1250
+ );
1251
+ sessionResources.command("get <ref> <rid>").description("one resource, as JSON").action(
1252
+ run(async (ref, rid) => {
1253
+ const id = await resolveSessionRef(ref);
1254
+ console.log(JSON.stringify(await api("GET", `${resourcesPath(id)}/${encodeURIComponent(rid)}`), null, 2));
1255
+ })
1256
+ );
1257
+ sessionResources.command("add <ref> <path>").description("upload a local file and mount it read-only: path[:/mount] \u2014 on disk before the next turn").action(
1258
+ run(async (ref, spec) => {
1259
+ const id = await resolveSessionRef(ref);
1260
+ const added = await api("POST", resourcesPath(id), await fileResource(spec));
1261
+ console.log(renderResource(added));
1262
+ })
1263
+ );
1264
+ sessionResources.command("rm <ref> <rid>").description("remove a mounted file (repositories stay for the session's lifetime)").action(
1265
+ run(async (ref, rid) => {
1266
+ const id = await resolveSessionRef(ref);
1267
+ const gone = await api("DELETE", `${resourcesPath(id)}/${encodeURIComponent(rid)}`);
1268
+ console.log(`Removed ${gone.id}`);
1269
+ })
1270
+ );
1271
+ sessionResources.command("rotate <ref> <rid> <token>").description("replace a repository's token; the sandbox's egress policy is updated in place").action(
1272
+ run(async (ref, rid, token) => {
1273
+ const id = await resolveSessionRef(ref);
1274
+ const updated = await api("POST", `${resourcesPath(id)}/${encodeURIComponent(rid)}`, {
1275
+ authorization_token: token
985
1276
  });
986
- if (!res.ok || !res.body) throw new ApiError(res.status, "logs_failed", await res.text());
987
- for await (const frame of parseSseStream(res.body)) {
988
- try {
989
- const entry = JSON.parse(frame.data);
990
- if (entry.line) console.log(`${entry.at ?? ""} ${entry.line}`);
991
- } catch {
992
- }
993
- }
1277
+ console.log(`Rotated ${updated.id}`);
994
1278
  })
995
1279
  );
996
- async function sendAndFollow(sessionId, text, detach) {
1280
+ async function uploadAttachments(opts) {
1281
+ const files2 = [];
1282
+ for (const path of [...opts.image ?? [], ...opts.file ?? []]) {
1283
+ const file = await uploadLocalFile(path);
1284
+ console.error(`uploaded ${file.filename} \u2192 ${file.id}`);
1285
+ files2.push(file);
1286
+ }
1287
+ return files2;
1288
+ }
1289
+ function turnOver(frame) {
1290
+ if (frame.event === "session.status_terminated") return true;
1291
+ if (frame.event !== "session.status_idle") return false;
1292
+ const stopReason = frame.data.stop_reason;
1293
+ return stopReason?.type !== "requires_action";
1294
+ }
1295
+ async function sendAndFollow(sessionId, text, opts) {
1296
+ const events = eventsFor(text, await uploadAttachments(opts), opts.system);
997
1297
  const controller = new AbortController();
998
1298
  let opened = null;
999
- if (!detach) {
1299
+ if (!opts.detach) {
1000
1300
  opened = (async () => {
1001
1301
  for await (const frame of streamEvents(sessionId, controller.signal)) {
1002
1302
  renderEvent(frame.event, frame.data);
1003
- if (frame.event === "turn.status_idle" || frame.event === "turn.interrupted") {
1303
+ if (turnOver(frame)) {
1004
1304
  controller.abort();
1005
1305
  break;
1006
1306
  }
@@ -1008,24 +1308,24 @@ async function sendAndFollow(sessionId, text, detach) {
1008
1308
  })();
1009
1309
  await new Promise((r) => setTimeout(r, 300));
1010
1310
  }
1011
- await api("POST", `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`, {
1012
- parts: [{ type: "text", index: 0, text }]
1013
- });
1014
- if (detach) {
1311
+ await postEvents(sessionId, events);
1312
+ if (opts.detach) {
1015
1313
  console.log("accepted");
1016
1314
  return;
1017
1315
  }
1018
1316
  await opened;
1019
1317
  }
1020
- async function chatLoop(sessionId) {
1318
+ async function chatLoop(sessionId, opts = {}) {
1021
1319
  console.log(`Chatting with ${sessionId} \u2014 Ctrl-C to leave.`);
1022
1320
  const controller = new AbortController();
1023
1321
  const rl = createInterface3({ input: process.stdin, output: process.stdout, prompt: "> " });
1322
+ let system = opts.system;
1024
1323
  void (async () => {
1025
1324
  try {
1026
1325
  for await (const frame of streamEvents(sessionId, controller.signal)) {
1027
1326
  renderEvent(frame.event, frame.data);
1028
- if (frame.event === "turn.status_idle") rl.prompt();
1327
+ if (frame.event === "session.status_terminated") rl.close();
1328
+ else if (frame.event === "session.status_idle") rl.prompt();
1029
1329
  }
1030
1330
  } catch (error) {
1031
1331
  fail(error);
@@ -1038,9 +1338,9 @@ async function chatLoop(sessionId) {
1038
1338
  rl.prompt();
1039
1339
  return;
1040
1340
  }
1041
- void api("POST", `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`, {
1042
- parts: [{ type: "text", index: 0, text }]
1043
- }).catch((error) => {
1341
+ const events = eventsFor(text, [], system);
1342
+ system = void 0;
1343
+ void postEvents(sessionId, events).catch((error) => {
1044
1344
  console.error(`sky: ${error instanceof Error ? error.message : String(error)}`);
1045
1345
  rl.prompt();
1046
1346
  });
@@ -1053,90 +1353,99 @@ async function chatLoop(sessionId) {
1053
1353
  });
1054
1354
  }
1055
1355
  var secrets = program.command("secrets").description("agent secrets (values write-only)");
1056
- secrets.command("set <slug> <pairs...>").description("set KEY=VALUE pairs on the agent").action(
1057
- run(async (slug, pairs) => {
1356
+ secrets.command("set <id> <pairs...>").description("set KEY=VALUE pairs on the agent").action(
1357
+ run(async (id, pairs) => {
1058
1358
  for (const pair of pairs) {
1059
1359
  const eq = pair.indexOf("=");
1060
1360
  if (eq < 1) throw new Error(`expected KEY=VALUE, got ${pair}`);
1061
1361
  const key = pair.slice(0, eq);
1062
- await api("PUT", `/api/v1/agents/${encodeURIComponent(slug)}/secrets/${encodeURIComponent(key)}`, {
1362
+ await api("PUT", `/agents/${encodeURIComponent(id)}/secrets/${encodeURIComponent(key)}`, {
1063
1363
  value: pair.slice(eq + 1)
1064
1364
  });
1065
1365
  console.log(`set ${key}`);
1066
1366
  }
1067
1367
  })
1068
1368
  );
1069
- secrets.command("list <slug>").action(
1070
- run(async (slug) => {
1071
- const { secrets: keys } = await api("GET", `/api/v1/agents/${encodeURIComponent(slug)}/secrets`);
1369
+ secrets.command("list <id>").action(
1370
+ run(async (id) => {
1371
+ const { secrets: keys } = await api("GET", `/agents/${encodeURIComponent(id)}/secrets`);
1072
1372
  if (!keys.length) console.log("no secrets set");
1073
1373
  for (const s of keys) {
1074
1374
  console.log(`\u25CF ${s.key}${s.description ? ` \u2014 ${s.description}` : ""}`);
1075
1375
  }
1076
1376
  })
1077
1377
  );
1078
- secrets.command("unset <slug> <key>").action(
1079
- run(async (slug, key) => {
1378
+ secrets.command("unset <id> <key>").action(
1379
+ run(async (id, key) => {
1080
1380
  await api(
1081
1381
  "DELETE",
1082
- `/api/v1/agents/${encodeURIComponent(slug)}/secrets/${encodeURIComponent(key)}`
1382
+ `/agents/${encodeURIComponent(id)}/secrets/${encodeURIComponent(key)}`
1083
1383
  );
1084
1384
  console.log(`unset ${key}`);
1085
1385
  })
1086
1386
  );
1087
- program.command("launch").description("scaffold ./agent.toml if absent, create the agent, print its address").option("-f, --file <path>", "agent file", "agent.toml").action(
1387
+ 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(
1088
1388
  run(async (opts) => {
1089
1389
  const path = resolve3(opts.file);
1090
1390
  if (!existsSync4(path)) {
1091
- const name = basename2(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
1391
+ const name = basename3(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
1092
1392
  scaffoldAgentFile(path, name);
1093
1393
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
1094
1394
  }
1095
1395
  const { file, spec } = readAgentFile(path);
1096
- if (file.slug) {
1097
- console.log(`${file.slug} already launched \u2014 use \`sky deploy\` to re-apply.`);
1396
+ if (file.id) {
1397
+ console.log(`${file.id} already launched \u2014 use \`sky deploy\` to re-apply.`);
1098
1398
  return;
1099
1399
  }
1100
1400
  const { vcpus, memoryMb, ...specBody } = spec;
1101
- const { agent: created } = await api("POST", "/api/v1/agents", specBody);
1102
- await api("POST", "/api/v1/sessions", {
1103
- agent: created.slug,
1401
+ const body = {
1402
+ ...specBody,
1403
+ ...opts.type ? { agentType: opts.type } : {},
1404
+ ...opts.llm ? { llm: opts.llm } : {}
1405
+ };
1406
+ const { agent: created } = await api("POST", "/agents", body);
1407
+ console.log("Starting its runtime\u2026");
1408
+ const { session: session2 } = await api("POST", "/sessions", {
1409
+ agent: created.id,
1104
1410
  ...vcpus !== void 0 ? { vcpus } : {},
1105
1411
  ...memoryMb !== void 0 ? { memoryMb } : {}
1106
1412
  });
1107
- stampSlug(path, created.slug);
1108
- console.log(`Launched ${created.slug} (${created.agentType} \xB7 ${created.llm})`);
1109
- console.log(` sky agent message ${created.slug} "hello"`);
1110
- console.log(` sky agent chat ${created.slug}`);
1413
+ stampId(path, created.id);
1414
+ console.log(`Launched ${created.id} (${created.agentType} \xB7 ${created.llm})`);
1415
+ printModelBilling(session2);
1416
+ console.log(` sky agent message ${created.id} "hello"`);
1417
+ console.log(` sky agent chat ${created.id}`);
1111
1418
  })
1112
1419
  );
1113
1420
  program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent)").option("-f, --file <path>", "agent file", "agent.toml").action(
1114
1421
  run(async (opts) => {
1115
1422
  const path = resolve3(opts.file);
1116
1423
  const { file, spec } = readAgentFile(path);
1117
- if (!file.slug) {
1118
- console.log("No slug in the file yet \u2014 running launch instead.");
1424
+ if (!file.id) {
1425
+ console.log("No agent id in the file yet \u2014 running launch instead.");
1119
1426
  const { vcpus, memoryMb, ...specBody } = spec;
1120
- const { agent: created } = await api("POST", "/api/v1/agents", specBody);
1121
- await api("POST", "/api/v1/sessions", {
1122
- agent: created.slug,
1427
+ const { agent: created } = await api("POST", "/agents", specBody);
1428
+ await api("POST", "/sessions", {
1429
+ agent: created.id,
1123
1430
  ...vcpus !== void 0 ? { vcpus } : {},
1124
1431
  ...memoryMb !== void 0 ? { memoryMb } : {}
1125
1432
  });
1126
- stampSlug(path, created.slug);
1127
- console.log(`Launched ${created.slug}`);
1433
+ stampId(path, created.id);
1434
+ console.log(`Launched ${created.id}`);
1128
1435
  return;
1129
1436
  }
1130
1437
  if (spec.prompt !== void 0) {
1131
- await api("PUT", `/api/v1/agents/${encodeURIComponent(file.slug)}/prompt`, {
1438
+ await api("PUT", `/agents/${encodeURIComponent(file.id)}/prompt`, {
1132
1439
  prompt: spec.prompt
1133
1440
  });
1134
1441
  }
1135
- await api("PATCH", `/api/v1/agents/${encodeURIComponent(file.slug)}`, {
1442
+ await api("PATCH", `/agents/${encodeURIComponent(file.id)}`, {
1136
1443
  displayName: spec.displayName,
1137
- ...spec.capabilities ? { capabilities: spec.capabilities } : {}
1444
+ ...spec.capabilities ? { capabilities: spec.capabilities } : {},
1445
+ ...spec.reasoningEffort ? { reasoningEffort: spec.reasoningEffort } : {},
1446
+ ...spec.skills ? { skills: spec.skills } : {}
1138
1447
  });
1139
- console.log(`Deployed ${file.slug} (prompt + settings re-applied)`);
1448
+ console.log(`Deployed ${file.id} (prompt + settings re-applied)`);
1140
1449
  })
1141
1450
  );
1142
1451
  program.command("clone").description("clone your local agent (Claude Code, \u2026) into the cloud; re-run to sync its prompt").option("--harness <type>", "claude_code | codex | openclaw (default: detected)").option("--new", "always launch a fresh agent, even if this machine is linked").option("--dry-run", "print the plan and send nothing").option("-y, --yes", "skip the confirmation").option("--open", "open the agent page when done").action(
@@ -1144,6 +1453,184 @@ program.command("clone").description("clone your local agent (Claude Code, \u202
1144
1453
  async (opts) => clone(opts)
1145
1454
  )
1146
1455
  );
1456
+ var files = program.command("files").description("files: upload once, reference by id from any message");
1457
+ files.command("upload <path>").description("upload a local file; prints the file id to reference from a message").option("--expires <seconds>", "expire the upload after this many seconds (3600\u20137776000)").action(
1458
+ run(async (path, opts) => {
1459
+ const file = await uploadLocalFile(path, opts.expires ? Number(opts.expires) : void 0);
1460
+ console.log(`${file.id} ${file.filename} ${file.mime_type} ${file.size_bytes} bytes`);
1461
+ })
1462
+ );
1463
+ files.command("list").description("list your uploads, or one session's files with --session").option("--session <ref>", "list the files of this session (its message copies and outputs)").option("--limit <n>", "page size (default 20)").option("--page <cursor>", "continue from a previous page").action(
1464
+ run(async (opts) => {
1465
+ const params = new URLSearchParams();
1466
+ if (opts.session) params.set("scope_id", await resolveSessionRef(opts.session));
1467
+ if (opts.limit) params.set("limit", opts.limit);
1468
+ if (opts.page) params.set("page", opts.page);
1469
+ const query = params.toString();
1470
+ const page = await api("GET", `/files${query ? `?${query}` : ""}`);
1471
+ for (const file of page.data) {
1472
+ console.log(`${file.id} ${file.filename} ${file.mime_type} ${file.size_bytes} bytes${file.downloadable ? "" : " (upload)"}`);
1473
+ }
1474
+ if (page.next_page) console.log(`\u2026 more: --page ${page.next_page}`);
1475
+ })
1476
+ );
1477
+ files.command("get <id>").description("show one file's metadata").action(
1478
+ run(
1479
+ async (id) => console.log(JSON.stringify(await api("GET", `/files/${encodeURIComponent(id)}`), null, 2))
1480
+ )
1481
+ );
1482
+ files.command("rm <id>").description("delete a file (a session that already received it keeps its copy)").action(
1483
+ run(async (id) => {
1484
+ await api("DELETE", `/files/${encodeURIComponent(id)}`);
1485
+ console.log(`Deleted ${id}`);
1486
+ })
1487
+ );
1488
+ files.command("download <id>").description("download a session file's bytes").requiredOption("-o, --out <path>", "where to write the bytes").action(
1489
+ run(async (id, opts) => {
1490
+ const { bytes } = await downloadFile(id);
1491
+ writeFileSync4(opts.out, bytes);
1492
+ console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
1493
+ })
1494
+ );
1495
+ function routineLine(r) {
1496
+ const target = r.target.type === "session" ? `\u2192 ${r.target.session_id}` : `+ ${r.target.agent}`;
1497
+ const cron = r.schedule ? `${r.schedule.expression} ${r.schedule.timezone}` : "manual-only";
1498
+ const state = r.archived_at ? "archived" : r.status === "paused" ? `paused (${r.paused_reason?.error?.type ?? r.paused_reason?.type ?? "manual"})` : "active";
1499
+ return `${r.id.padEnd(30)} ${r.name.padEnd(24)} ${cron.padEnd(26)} ${target.padEnd(30)} ${state}`;
1500
+ }
1501
+ var routine = program.command("routine").description("run agents on a schedule: cron-fired sessions or turns into a long-lived session");
1502
+ routine.command("create <name>").description("create a routine; --agent starts a fresh session per fire, --session fires into one session").option("--agent <id>", "target: create a new session from this agent each fire").option("--session <id>", "target: send each fire into this existing session").option("--message <text>", "the user message delivered on every fire (required)").option("--cron <expression>", '5-field POSIX cron, e.g. "0 9 * * 1-5" (omit for manual-only)').option("--timezone <tz>", "IANA timezone for --cron (default UTC)").option("--budget-cents <n>", "per-run spend cap in whole US cents").action(
1503
+ run(
1504
+ async (name, opts) => {
1505
+ if (!opts.message) throw new Error("--message is required");
1506
+ if (!opts.agent === !opts.session) throw new Error("pass exactly one of --agent or --session");
1507
+ const { routine: created } = await api("POST", "/routines", {
1508
+ name,
1509
+ target: opts.agent ? { type: "new_session", agent: opts.agent } : { type: "session", session_id: opts.session },
1510
+ initial_events: [
1511
+ { type: "user.message", content: [{ type: "text", text: opts.message }] }
1512
+ ],
1513
+ ...opts.cron ? { schedule: { type: "cron", expression: opts.cron, timezone: opts.timezone ?? "UTC" } } : {},
1514
+ ...opts.budgetCents ? { budget: { type: "limit", max_list_cost: { amount: opts.budgetCents, currency: "USD" } } } : {}
1515
+ });
1516
+ console.log(`Created ${created.id}`);
1517
+ if (created.schedule) {
1518
+ console.log(` next fires: ${created.schedule.upcoming_runs_at.slice(0, 3).join(", ")}`);
1519
+ } else {
1520
+ console.log(` manual-only \u2014 fire with: sky routine run ${created.id}`);
1521
+ }
1522
+ }
1523
+ )
1524
+ );
1525
+ routine.command("list").action(
1526
+ run(async () => {
1527
+ const { items } = await api("GET", "/routines");
1528
+ if (!items.length) {
1529
+ console.log("No routines. `sky routine create <name> --agent <id> --message <text> --cron <expr>`");
1530
+ return;
1531
+ }
1532
+ for (const r of items) console.log(routineLine(r));
1533
+ })
1534
+ );
1535
+ routine.command("show <id>").action(
1536
+ run(async (id) => {
1537
+ const { routine: r } = await api(
1538
+ "GET",
1539
+ `/routines/${encodeURIComponent(id)}`
1540
+ );
1541
+ console.log(routineLine(r));
1542
+ const { items } = await api(
1543
+ "GET",
1544
+ `/routine-runs?routine=${encodeURIComponent(id)}&limit=5`
1545
+ );
1546
+ for (const runRow of items) {
1547
+ const outcome = runRow.error ? `FAILED ${runRow.error.type}` : runRow.session_id ?? "ok";
1548
+ console.log(` ${runRow.created_at} ${runRow.trigger_context.type.padEnd(8)} ${outcome}`);
1549
+ }
1550
+ })
1551
+ );
1552
+ routine.command("run <id>").description("fire now, outside the schedule (works while paused)").action(
1553
+ run(async (id) => {
1554
+ const { run: fired } = await api(
1555
+ "POST",
1556
+ `/routines/${encodeURIComponent(id)}/run`
1557
+ );
1558
+ if (fired.error) console.log(`Run ${fired.id} failed: ${fired.error.type} \u2014 ${fired.error.message}`);
1559
+ else console.log(`Run ${fired.id} \u2192 ${fired.session_id}`);
1560
+ })
1561
+ );
1562
+ for (const [verb, blurb] of [
1563
+ ["pause", "suppress scheduled fires (manual runs still work)"],
1564
+ ["unpause", "resume from the next occurrence \u2014 missed fires are not backfilled"],
1565
+ ["archive", "terminal: the schedule stops and the routine becomes immutable"]
1566
+ ]) {
1567
+ routine.command(`${verb} <id>`).description(blurb).action(
1568
+ run(async (id) => {
1569
+ const { routine: r } = await api(
1570
+ "POST",
1571
+ `/routines/${encodeURIComponent(id)}/${verb}`
1572
+ );
1573
+ console.log(`${r.id}: ${r.archived_at ? "archived" : r.status}`);
1574
+ })
1575
+ );
1576
+ }
1577
+ var collect = (value, previous = []) => [...previous, value];
1578
+ function goalPath(sessionId, suffix = "") {
1579
+ return `/sessions/${encodeURIComponent(sessionId)}/goal${suffix}`;
1580
+ }
1581
+ function withGoalWriteOptions(command) {
1582
+ return command.option("--criterion <text>", "a requirement the grader checks (repeatable)", collect).option("--check <command>", 'a shell command that must exit 0, optionally "<text> :: <command>" (repeatable)', collect).option("--max-iterations <n>", "self-started turns before the goal stops (default 20)").option("--max-spend-cents <n>", 'spend ceiling in whole US cents, or "none"').option("--max-tokens <n>", 'token ceiling (uncached input + output), or "none"').option("--max-minutes <n>", `wall-clock ceiling from the goal's start, or "none"`).option("--idempotency-key <key>", "make a retried request safe to repeat");
1583
+ }
1584
+ var goal = program.command("goal").description("give a session a standing objective it keeps working toward until the criteria are met");
1585
+ goal.command("get <session>").description("show the session's goal, its limits, and what the last check found").option("--json", "print the raw goal object").action(
1586
+ run(async (ref, opts) => {
1587
+ const { goal: current } = await api("GET", goalPath(await resolveSessionRef(ref)));
1588
+ console.log(opts.json ? JSON.stringify(current, null, 2) : renderGoal(current));
1589
+ })
1590
+ );
1591
+ withGoalWriteOptions(
1592
+ goal.command("set <session> <objective...>").description("set the goal, replacing any existing one; work starts right away")
1593
+ ).action(
1594
+ run(async (ref, words, opts) => {
1595
+ const { goal: created } = await api(
1596
+ "POST",
1597
+ goalPath(await resolveSessionRef(ref)),
1598
+ setGoalBody(words.join(" "), opts),
1599
+ { idempotencyKey: opts.idempotencyKey }
1600
+ );
1601
+ console.log(renderGoal(created));
1602
+ })
1603
+ );
1604
+ withGoalWriteOptions(
1605
+ goal.command("edit <session>").description("change the goal in place; iterations and spend so far carry over").option("--objective <text>", "the new objective")
1606
+ ).action(
1607
+ run(async (ref, opts) => {
1608
+ const { goal: edited } = await api(
1609
+ "PATCH",
1610
+ goalPath(await resolveSessionRef(ref)),
1611
+ editGoalBody(opts),
1612
+ { idempotencyKey: opts.idempotencyKey }
1613
+ );
1614
+ console.log(renderGoal(edited));
1615
+ })
1616
+ );
1617
+ for (const [verb, method, suffix, blurb] of [
1618
+ ["pause", "POST", "/pause", "stop taking turns and interrupt the run the goal started"],
1619
+ ["resume", "POST", "/resume", "start taking turns again; a deadline moves out by the paused time"],
1620
+ ["clear", "DELETE", "", "stop the goal for good and interrupt the run it started"]
1621
+ ]) {
1622
+ goal.command(`${verb} <session>`).description(blurb).option("--idempotency-key <key>", "make a retried request safe to repeat").action(
1623
+ run(async (ref, opts) => {
1624
+ const { goal: changed } = await api(
1625
+ method,
1626
+ goalPath(await resolveSessionRef(ref), suffix),
1627
+ void 0,
1628
+ { idempotencyKey: opts.idempotencyKey }
1629
+ );
1630
+ console.log(`${changed.id}: ${changed.status}${changed.stop_reason ? ` (${changed.stop_reason})` : ""}`);
1631
+ })
1632
+ );
1633
+ }
1147
1634
  program.command("config").description("show the CLI config location and current values").action(() => {
1148
1635
  const config = loadConfig();
1149
1636
  console.log(`config: ${configPath()}`);