@agentsky/sky 0.2.0 → 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,30 +1,68 @@
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-DTVZOF77.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 existsSync3 } from "fs";
24
- import { basename as basename2, resolve as resolve3 } from "path";
25
- import { createInterface as createInterface2 } from "readline";
18
+ import { existsSync as existsSync4, writeFileSync as writeFileSync4 } from "fs";
19
+ import { basename as basename3, resolve as resolve3 } from "path";
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
+
62
+ // src/version.ts
63
+ var CLI_VERSION = "2.2.1";
64
+ var USER_AGENT = `sky-cli/${CLI_VERSION} (${process.platform}; node/${process.versions.node})`;
65
+
28
66
  // src/api.ts
29
67
  var ApiError = class extends Error {
30
68
  constructor(status, code, message) {
@@ -62,38 +100,73 @@ async function api(method, path, body, opts = {}) {
62
100
  const res = await fetch(`${apiBase()}${path}`, {
63
101
  method,
64
102
  headers: {
103
+ "user-agent": USER_AGENT,
65
104
  ...opts.auth === false ? {} : authHeaders(),
66
- ...body !== void 0 ? { "content-type": "application/json" } : {}
105
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
106
+ ...opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : {}
67
107
  },
68
108
  body: body !== void 0 ? JSON.stringify(body) : void 0
69
109
  });
70
110
  if (!res.ok) await parseError(res);
71
111
  return await res.json();
72
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
+ }
73
146
  async function resolveSessionRef(ref) {
74
147
  if (ref.startsWith("sess-")) return ref;
75
148
  const { agent: agent2 } = await api(
76
149
  "GET",
77
- `/api/v1/agents/${encodeURIComponent(ref)}`
150
+ `/agents/${encodeURIComponent(ref)}`
78
151
  ).catch(async () => {
79
- await api("GET", `/api/v1/sessions/${encodeURIComponent(ref)}`);
80
- return { agent: { slug: ref, defaultSessionId: ref } };
152
+ await api("GET", `/sessions/${encodeURIComponent(ref)}`);
153
+ return { agent: { id: ref, defaultSessionId: ref } };
81
154
  });
82
155
  if (!agent2.defaultSessionId) {
83
156
  throw new ApiError(
84
157
  404,
85
158
  "no_sessions",
86
- `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}`
87
160
  );
88
161
  }
89
162
  return agent2.defaultSessionId;
90
163
  }
91
164
  async function* streamEvents(sessionId, signal) {
92
- const { parseSseStream } = await import("./src-CNT7GXZH.js");
165
+ const { parseSseStream } = await import("./src-NNPV5GRJ.js");
93
166
  const res = await fetch(
94
- `${apiBase()}/api/v1/sessions/${encodeURIComponent(sessionId)}/stream`,
167
+ `${apiBase()}/sessions/${encodeURIComponent(sessionId)}/stream`,
95
168
  {
96
- headers: { ...authHeaders(), accept: "text/event-stream" },
169
+ headers: { "user-agent": USER_AGENT, ...authHeaders(), accept: "text/event-stream" },
97
170
  signal
98
171
  }
99
172
  );
@@ -112,9 +185,128 @@ async function* streamEvents(sessionId, signal) {
112
185
  }
113
186
  }
114
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
+
115
307
  // src/agent-file.ts
116
- import { existsSync, readFileSync, writeFileSync } from "fs";
117
- import { dirname, resolve } from "path";
308
+ import { existsSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
309
+ import { dirname as dirname2, resolve } from "path";
118
310
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
119
311
  var fileInstructionSchema = external_exports.object({
120
312
  name: external_exports.string(),
@@ -123,41 +315,49 @@ var fileInstructionSchema = external_exports.object({
123
315
  });
124
316
  var agentFileSchema = external_exports.object({
125
317
  name: external_exports.string().min(1),
126
- slug: external_exports.string().optional(),
318
+ id: external_exports.string().optional(),
127
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)
128
322
  description: external_exports.string().optional(),
129
323
  type: external_exports.enum(AGENT_TYPES).optional(),
130
324
  llm: external_exports.string().optional(),
325
+ reasoning_effort: external_exports.enum(REASONING_EFFORT_VALUES).optional(),
131
326
  capabilities: capabilitiesSchema.optional(),
132
327
  prompt: external_exports.string().optional(),
133
328
  prompt_file: external_exports.string().optional(),
134
329
  instructions: external_exports.array(fileInstructionSchema).optional(),
135
330
  skills: external_exports.array(skillRefSchema).optional(),
136
- customInstalls: external_exports.array(customInstallSchema).optional(),
137
331
  customData: external_exports.array(customDataSchema).optional(),
138
332
  /** Advanced machine shape (create-only): 1|2|4|8 vCPUs, 256MB-step memory. */
139
333
  vcpus: external_exports.number().int().optional(),
140
334
  memory_mb: external_exports.number().int().optional()
141
335
  });
142
336
  function readAgentFile(path) {
143
- const raw = readFileSync(path, "utf8");
337
+ const raw = readFileSync3(path, "utf8");
144
338
  const parsed = agentFileSchema.safeParse(parseToml(raw));
145
339
  if (!parsed.success) {
146
340
  const issue = parsed.error.issues[0];
147
341
  throw new Error(`${path}: ${issue?.path.join(".") ?? ""} ${issue?.message ?? "invalid"}`);
148
342
  }
149
343
  const file = parsed.data;
150
- 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));
151
350
  const inline = (ref, inlineContent) => {
152
351
  if (inlineContent !== void 0) return inlineContent;
153
352
  if (!ref) return void 0;
154
- return readFileSync(resolve(base, ref), "utf8");
353
+ return readFileSync3(resolve(base, ref), "utf8");
155
354
  };
156
355
  const spec = {
157
356
  name: file.name,
158
357
  description: file.description,
159
358
  agentType: file.type,
160
359
  llm: file.llm,
360
+ reasoningEffort: file.reasoning_effort,
161
361
  displayName: file.name,
162
362
  capabilities: file.capabilities,
163
363
  prompt: inline(file.prompt_file, file.prompt),
@@ -167,26 +367,27 @@ function readAgentFile(path) {
167
367
  return { name: f.name, content };
168
368
  }),
169
369
  skills: file.skills,
170
- customInstalls: file.customInstalls,
171
370
  customData: file.customData,
172
371
  vcpus: file.vcpus,
173
372
  memoryMb: file.memory_mb
174
373
  };
175
374
  return { file, spec };
176
375
  }
177
- function stampSlug(path, slug) {
178
- const doc = parseToml(readFileSync(path, "utf8"));
179
- doc.slug = slug;
180
- 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));
181
381
  }
182
382
  function scaffoldAgentFile(path, name) {
183
383
  if (existsSync(path)) throw new Error(`${path} already exists`);
184
- writeFileSync(
384
+ writeFileSync2(
185
385
  path,
186
386
  `# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
187
387
  name = "${name}"
188
- # type = "hermes" # hermes | claude_code | codex | openclaw
189
- # 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()}
190
391
  # capabilities = []
191
392
 
192
393
  prompt = """
@@ -198,18 +399,18 @@ You are ${name}, a helpful long-lived agent.
198
399
 
199
400
  // src/clone.ts
200
401
  import { spawn } from "child_process";
201
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
202
- import { homedir } from "os";
203
- 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";
204
405
  import { createInterface } from "readline";
205
406
  var PROMPT_CAP = 1e5;
206
407
  var HARNESSES = ["claude_code", "codex", "openclaw"];
207
408
  function claudeConfigRoot() {
208
- return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
409
+ return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
209
410
  }
210
411
  function readIfExists(path) {
211
412
  try {
212
- return readFileSync2(path, "utf8");
413
+ return readFileSync4(path, "utf8");
213
414
  } catch {
214
415
  return void 0;
215
416
  }
@@ -225,9 +426,9 @@ function readJson(path) {
225
426
  }
226
427
  function claudeModel(configRoot) {
227
428
  const candidates = [
228
- join(process.cwd(), ".claude", "settings.local.json"),
229
- join(process.cwd(), ".claude", "settings.json"),
230
- join(configRoot, "settings.json")
429
+ join2(process.cwd(), ".claude", "settings.local.json"),
430
+ join2(process.cwd(), ".claude", "settings.json"),
431
+ join2(configRoot, "settings.json")
231
432
  ];
232
433
  for (const path of candidates) {
233
434
  const model = readJson(path)?.model;
@@ -237,11 +438,11 @@ function claudeModel(configRoot) {
237
438
  }
238
439
  function claudeMcpServers() {
239
440
  const merged = {};
240
- const userConfig = readJson(join(homedir(), ".claude.json"));
441
+ const userConfig = readJson(join2(homedir2(), ".claude.json"));
241
442
  Object.assign(merged, userConfig?.mcpServers ?? {});
242
443
  Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
243
444
  const projectConfig = readJson(
244
- join(process.cwd(), ".mcp.json")
445
+ join2(process.cwd(), ".mcp.json")
245
446
  );
246
447
  Object.assign(merged, projectConfig?.mcpServers ?? {});
247
448
  return merged;
@@ -258,13 +459,13 @@ function readClaudeCode() {
258
459
  const configRoot = claudeConfigRoot();
259
460
  const parts = [];
260
461
  const sources = [];
261
- const global = readIfExists(join(configRoot, "CLAUDE.md"));
462
+ const global = readIfExists(join2(configRoot, "CLAUDE.md"));
262
463
  if (global?.trim()) {
263
464
  parts.push({ label: "Global CLAUDE.md", content: global });
264
465
  sources.push("global CLAUDE.md");
265
466
  }
266
467
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
267
- const project = readIfExists(join(process.cwd(), name));
468
+ const project = readIfExists(join2(process.cwd(), name));
268
469
  if (project?.trim()) {
269
470
  parts.push({ label: `Project ${name}`, content: project });
270
471
  sources.push(`project ${name}`);
@@ -292,8 +493,8 @@ function detectHarness(explicit) {
292
493
  return explicit;
293
494
  }
294
495
  if (existsSync2(claudeConfigRoot())) return "claude_code";
295
- if (existsSync2(join(homedir(), ".codex"))) return "codex";
296
- if (existsSync2(join(homedir(), ".openclaw"))) return "openclaw";
496
+ if (existsSync2(join2(homedir2(), ".codex"))) return "codex";
497
+ if (existsSync2(join2(homedir2(), ".openclaw"))) return "openclaw";
297
498
  throw new Error(
298
499
  "no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
299
500
  );
@@ -305,14 +506,14 @@ function readLocal(harness) {
305
506
  );
306
507
  }
307
508
  function linkPath(configRoot) {
308
- return join(configRoot, "agentsky-link.json");
509
+ return join2(configRoot, "agentsky-link.json");
309
510
  }
310
511
  function readLink(configRoot) {
311
512
  const link = readJson(linkPath(configRoot));
312
- return link?.agentSlug ? link : void 0;
513
+ return link?.agentId ? link : void 0;
313
514
  }
314
515
  function writeLink(configRoot, link) {
315
- writeFileSync2(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
516
+ writeFileSync3(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
316
517
  `);
317
518
  }
318
519
  function confirm(question) {
@@ -341,7 +542,7 @@ async function createClone(local, opts) {
341
542
  `model ${local.model ?? "(harness default)"}`,
342
543
  `${keptNames.length} MCP server${keptNames.length === 1 ? "" : "s"}${keptNames.length ? ` (${keptNames.join(", ")})` : ""}`
343
544
  ].join(" \xB7 ");
344
- 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())})`);
345
546
  console.log(`Will clone: ${summary}`);
346
547
  if (local.truncated) console.log(" note: instructions exceeded 100k chars and were truncated");
347
548
  if (local.droppedServers.length) {
@@ -357,41 +558,48 @@ async function createClone(local, opts) {
357
558
  console.log("Aborted \u2014 nothing sent.");
358
559
  return;
359
560
  }
360
- 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
+ }
361
565
  const body = {
362
566
  agentType: local.harness,
363
567
  ...local.model ? { llm: local.model } : {},
364
568
  ...local.prompt ? { prompt: local.prompt } : {},
365
- ...installs.length ? { customInstalls: installs } : {}
569
+ ...mcpServers.length ? { mcpServers } : {}
366
570
  };
367
571
  let sentLlm = local.model ?? null;
368
572
  let created;
369
573
  try {
370
- ({ agent: created } = await api("POST", "/api/v1/agents", body));
574
+ ({ agent: created } = await api("POST", "/agents", body));
371
575
  } catch (error) {
372
576
  if (error instanceof ApiError && error.status === 422 && local.model) {
373
- console.log(` note: ${local.model} isn't available for ${local.harness} \u2014 using the harness default`);
374
- sentLlm = null;
375
- ({ 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", {
376
583
  ...body,
377
- llm: void 0
584
+ llm: successor ?? void 0
378
585
  }));
379
586
  } else {
380
587
  throw error;
381
588
  }
382
589
  }
383
- await api("POST", "/api/v1/sessions", { agent: created.slug });
590
+ console.log("Starting its runtime\u2026");
591
+ await api("POST", "/sessions", { agent: created.id });
384
592
  const now = (/* @__PURE__ */ new Date()).toISOString();
385
593
  writeLink(local.configRoot, {
386
594
  baseUrl: apiBase(),
387
- agentSlug: created.slug,
595
+ agentId: created.id,
388
596
  clonedAt: now,
389
597
  lastSyncedAt: now,
390
598
  llm: sentLlm,
391
599
  mcpServers: keptNames
392
600
  });
393
- const url = `${apiBase()}/agents/${created.slug}`;
394
- console.log(`Cloned \u2192 ${created.slug} ${url}`);
601
+ const url = `${apiBase()}/agents/${created.id}`;
602
+ console.log(`Cloned \u2192 ${created.id} ${url}`);
395
603
  console.log(
396
604
  `Linked this machine (${linkPath(local.configRoot)}). Push prompt changes anytime by running \`sky clone\` again.`
397
605
  );
@@ -400,11 +608,11 @@ async function createClone(local, opts) {
400
608
  async function syncClone(local, link, opts) {
401
609
  const since = link.lastSyncedAt ?? link.clonedAt;
402
610
  console.log(
403
- `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"})`
404
612
  );
405
613
  if (opts.dryRun) {
406
614
  console.log(
407
- ` 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.`
408
616
  );
409
617
  return;
410
618
  }
@@ -412,24 +620,24 @@ async function syncClone(local, link, opts) {
412
620
  try {
413
621
  ({ agent: agent2 } = await api(
414
622
  "GET",
415
- `/api/v1/agents/${encodeURIComponent(link.agentSlug)}`
623
+ `/agents/${encodeURIComponent(link.agentId)}`
416
624
  ));
417
625
  } catch (error) {
418
626
  if (error instanceof ApiError && error.status === 404) {
419
627
  throw new Error(
420
- `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`
421
629
  );
422
630
  }
423
631
  throw error;
424
632
  }
425
633
  if (agent2.agentType !== local.harness) {
426
634
  throw new Error(
427
- `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`
428
636
  );
429
637
  }
430
638
  const result = await api(
431
639
  "PUT",
432
- `/api/v1/agents/${encodeURIComponent(link.agentSlug)}/prompt`,
640
+ `/agents/${encodeURIComponent(link.agentId)}/prompt`,
433
641
  { prompt: local.prompt }
434
642
  );
435
643
  console.log(
@@ -457,8 +665,8 @@ async function syncClone(local, link, opts) {
457
665
  );
458
666
  }
459
667
  writeLink(local.configRoot, { ...link, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
460
- console.log(`Synced ${link.agentSlug}.`);
461
- if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentSlug}`);
668
+ console.log(`Synced ${link.agentId}.`);
669
+ if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentId}`);
462
670
  }
463
671
  async function clone(opts) {
464
672
  const harness = detectHarness(opts.harness);
@@ -521,7 +729,7 @@ async function browserLogin(verifier, challenge) {
521
729
  server.listen(0, "127.0.0.1", () => {
522
730
  const address = server.address();
523
731
  const port = typeof address === "object" && address ? address.port : 0;
524
- 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`;
525
733
  console.log("Opening your browser to authorize this machine\u2026");
526
734
  console.log(`If it doesn't open, visit:
527
735
  ${authorizeUrl}`);
@@ -534,14 +742,14 @@ async function browserLogin(verifier, challenge) {
534
742
  });
535
743
  return api(
536
744
  "POST",
537
- "/api/v1/cli/token-exchange",
745
+ "/cli/token-exchange",
538
746
  { code, verifier, clientName: `cli ${hostname()}` },
539
747
  { auth: false }
540
748
  );
541
749
  }
542
750
  async function headlessLogin(verifier, challenge) {
543
751
  const request = b64url(randomBytes(24));
544
- 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`;
545
753
  console.log(`Visit this URL on any device to authorize:
546
754
  ${authorizeUrl}
547
755
  `);
@@ -550,7 +758,8 @@ async function headlessLogin(verifier, challenge) {
550
758
  while (Date.now() < deadline) {
551
759
  await new Promise((r) => setTimeout(r, 3e3));
552
760
  const res = await fetch(
553
- `${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()}`)}`,
762
+ { headers: { "user-agent": USER_AGENT } }
554
763
  );
555
764
  if (!res.ok) {
556
765
  const body2 = await res.json().catch(() => null);
@@ -569,7 +778,6 @@ async function headlessLogin(verifier, challenge) {
569
778
  // src/render.ts
570
779
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
571
780
  var red = (s) => `\x1B[31m${s}\x1B[0m`;
572
- var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
573
781
  function clip(value, max = 120) {
574
782
  const text = typeof value === "string" ? value : JSON.stringify(value);
575
783
  return text.length > max ? `${text.slice(0, max)}\u2026` : text;
@@ -592,53 +800,220 @@ function renderEvent(event, data) {
592
800
  case "agent.status":
593
801
  console.log(dim(`\u2026 ${String(part?.level ?? "")} ${String(part?.text ?? "")}`.trim()));
594
802
  break;
595
- case "user.message": {
596
- const channel = String(data.channel ?? "");
597
- if (channel && channel !== "api") {
598
- console.log(cyan(`[${channel}] ${clip(data.text ?? "")}`));
599
- }
803
+ case "user.interrupt":
804
+ console.log(dim("\u23F9 interrupting"));
600
805
  break;
601
- }
602
- case "turn.interrupted":
603
- 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}`));
604
811
  break;
605
- case "error":
606
- console.error(red(`error: ${String(data.message ?? data.code ?? "unknown")}`));
812
+ }
813
+ case "session.status_terminated":
814
+ console.log(dim("session terminated"));
607
815
  break;
608
816
  default:
609
817
  break;
610
818
  }
611
819
  }
612
820
 
821
+ // src/subscriptions.ts
822
+ import { spawn as spawn3 } from "child_process";
823
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
824
+ import { homedir as homedir3 } from "os";
825
+ import { join as join3 } from "path";
826
+ import { createInterface as createInterface2 } from "readline";
827
+ var ALIASES = {
828
+ claude: "anthropic",
829
+ anthropic: "anthropic",
830
+ chatgpt: "openai",
831
+ openai: "openai",
832
+ codex: "openai"
833
+ };
834
+ function providerFor(name) {
835
+ const provider = ALIASES[name.toLowerCase()];
836
+ if (!provider) {
837
+ throw new ApiError(400, "unknown_provider", `unknown provider "${name}" \u2014 use claude or chatgpt`);
838
+ }
839
+ return provider;
840
+ }
841
+ function ask(question) {
842
+ return new Promise((done) => {
843
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
844
+ rl.question(question, (answer) => {
845
+ rl.close();
846
+ done(answer.trim());
847
+ });
848
+ });
849
+ }
850
+ function confirm2(question) {
851
+ return new Promise((done) => {
852
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
853
+ rl.question(`${question} [y/N] `, (answer) => {
854
+ rl.close();
855
+ done(/^y(es)?$/i.test(answer.trim()));
856
+ });
857
+ });
858
+ }
859
+ function mintClaudeToken() {
860
+ return new Promise((done) => {
861
+ let child;
862
+ try {
863
+ child = spawn3("claude", ["setup-token"], { stdio: ["inherit", "pipe", "inherit"] });
864
+ } catch {
865
+ done(null);
866
+ return;
867
+ }
868
+ let buffer = "";
869
+ child.stdout?.on("data", (chunk) => {
870
+ buffer += chunk.toString();
871
+ process.stdout.write(chunk);
872
+ });
873
+ child.on("error", () => done(null));
874
+ child.on("close", () => {
875
+ const match = buffer.match(/sk-ant-oat[A-Za-z0-9_-]+/);
876
+ done(match ? match[0] : null);
877
+ });
878
+ });
879
+ }
880
+ async function put(provider, credential) {
881
+ const { modelSubscription } = await api(
882
+ "PUT",
883
+ `/model-subscriptions/${provider}`,
884
+ { credential }
885
+ );
886
+ return modelSubscription;
887
+ }
888
+ async function connectClaude() {
889
+ let token = await mintClaudeToken();
890
+ if (token) {
891
+ console.log("\nCaptured the minted token \u2014 uploading.");
892
+ } else {
893
+ console.log("Couldn't mint a token via `claude setup-token` \u2014 paste one instead.");
894
+ console.log("(Run `claude setup-token` on a machine with Claude Code and paste its sk-ant-oat\u2026 output.)");
895
+ token = await ask("Token: ");
896
+ }
897
+ if (!token) throw new ApiError(400, "no_token", "no token provided");
898
+ const sub = await put("anthropic", token);
899
+ const expiry = sub.expiresAt ? ` (expires ~${sub.expiresAt.slice(0, 10)})` : "";
900
+ console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
901
+ }
902
+ async function connectChatgpt(opts) {
903
+ const codexHome = process.env.CODEX_HOME || join3(homedir3(), ".codex");
904
+ const path = join3(codexHome, "auth.json");
905
+ if (!existsSync3(path)) {
906
+ throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
907
+ }
908
+ const raw = readFileSync5(path, "utf8");
909
+ let accountId;
910
+ try {
911
+ const parsed = JSON.parse(raw);
912
+ accountId = parsed.tokens?.account_id;
913
+ } catch {
914
+ throw new ApiError(400, "invalid_codex_auth", `${path} isn't valid JSON \u2014 re-run \`codex login\``);
915
+ }
916
+ console.log(`Found Codex sign-in at ${path}${accountId ? ` (account ${accountId})` : ""}.`);
917
+ if (!opts.yes) {
918
+ const ok = await confirm2("Upload it so your cloud Codex agents run on your ChatGPT plan?");
919
+ if (!ok) {
920
+ console.log("Aborted \u2014 nothing uploaded.");
921
+ return;
922
+ }
923
+ }
924
+ await put("openai", raw);
925
+ console.log("Connected ChatGPT plan. Powers new eligible agents.");
926
+ }
927
+ async function listSubscriptions() {
928
+ const { modelSubscriptions } = await api(
929
+ "GET",
930
+ "/model-subscriptions"
931
+ );
932
+ if (modelSubscriptions.length === 0) {
933
+ console.log("No model subscriptions connected. Try `sky auth connect claude` or `sky auth connect chatgpt`.");
934
+ return;
935
+ }
936
+ for (const s of modelSubscriptions) {
937
+ const bits = [
938
+ s.provider === "anthropic" ? "claude" : "chatgpt",
939
+ s.status,
940
+ s.expiresAt ? `expires ~${s.expiresAt.slice(0, 10)}` : null,
941
+ s.lastUsedAt ? `last used ${s.lastUsedAt.slice(0, 10)}` : null,
942
+ `${s.agents.length} agent${s.agents.length === 1 ? "" : "s"}`,
943
+ s.useForNewAgents ? "used for new agents" : "not used for new agents"
944
+ ].filter(Boolean);
945
+ console.log(bits.join(" \xB7 "));
946
+ for (const a of s.agents) console.log(` ${a.agentId ?? a.id} \u2014 ${a.name}`);
947
+ }
948
+ }
949
+ async function disconnect(name, opts) {
950
+ const provider = providerFor(name);
951
+ const label = provider === "anthropic" ? "Claude" : "ChatGPT";
952
+ if (!opts.yes) {
953
+ const ok = await confirm2(`Disconnect ${label}? Agents running on it will fail until reconnected or switched to Platform.`);
954
+ if (!ok) {
955
+ console.log("Aborted.");
956
+ return;
957
+ }
958
+ }
959
+ const { affectedAgents } = await api(
960
+ "DELETE",
961
+ `/model-subscriptions/${provider}`
962
+ );
963
+ console.log(`Disconnected ${label}.`);
964
+ if (affectedAgents.length > 0) {
965
+ console.log(
966
+ `${affectedAgents.length} agent${affectedAgents.length === 1 ? "" : "s"} still point at it and will fail: ` + affectedAgents.map((a) => a.agentId ?? a.id).join(", ")
967
+ );
968
+ console.log("Switch them to Platform in each agent's Configure tab (or reconnect).");
969
+ }
970
+ }
971
+
613
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
+ }
614
980
  var program = new Command();
615
- program.name("sky").description("Launch and talk to long-lived agents \u2014 agentsky.dev").version("0.2.0");
981
+ program.name("sky").description("Launch and talk to long-lived agents \u2014 agentsky.dev").version(CLI_VERSION);
616
982
  function fail(error) {
617
983
  if (error instanceof ApiError) console.error(`sky: ${error.message} (${error.code})`);
618
984
  else console.error(`sky: ${error instanceof Error ? error.message : String(error)}`);
619
985
  process.exit(1);
620
986
  }
621
987
  var run = (fn) => (...args) => fn(...args).catch(fail);
622
- var auth = program.command("auth").description("login, logout, whoami");
988
+ var auth = program.command("auth").description("login, logout, whoami, model subscriptions");
623
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 })));
624
- 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(
625
991
  run(async () => {
626
992
  saveConfig({ ...loadConfig(), token: void 0, email: void 0 });
627
- console.log(`Logged out. (Token revocation: Settings \u2192 API tokens.)`);
993
+ console.log(`Logged out. (Token revocation: Developer \u2192 API keys.)`);
628
994
  })
629
995
  );
630
996
  auth.command("whoami").description("who the current token acts as").action(
631
997
  run(async () => {
632
- const who = await api("GET", "/api/v1/whoami");
998
+ const who = await api("GET", "/whoami");
633
999
  console.log(
634
1000
  `${who.user.email ?? "unknown"} \xB7 universe ${who.universe.slug}${who.universe.isPersonal ? " (personal)" : ""} \xB7 scopes ${who.scopes.join(",")}`
635
1001
  );
636
1002
  })
637
1003
  );
1004
+ auth.command("connect <provider>").description("connect a model subscription: claude (setup-token) or chatgpt (Codex sign-in)").option("-y, --yes", "skip the consent prompt").action(
1005
+ run(async (name, opts) => {
1006
+ const provider = providerFor(name);
1007
+ if (provider === "anthropic") await connectClaude();
1008
+ else await connectChatgpt(opts);
1009
+ })
1010
+ );
1011
+ auth.command("accounts").description("list connected model subscriptions and the agents they power").action(run(async () => listSubscriptions()));
1012
+ auth.command("disconnect <provider>").description("disconnect a model subscription (agents on it fail until switched)").option("-y, --yes", "skip the confirmation prompt").action(run(async (name, opts) => disconnect(name, opts)));
638
1013
  var universe = program.command("universe").description("grouping boundary for agents");
639
1014
  universe.command("list").action(
640
1015
  run(async () => {
641
- const { universes } = await api("GET", "/api/v1/universes");
1016
+ const { universes } = await api("GET", "/universes");
642
1017
  const current = currentUniverse();
643
1018
  for (const u of universes) {
644
1019
  const marker = u.slug === current || !current && u.isPersonal ? "*" : " ";
@@ -648,13 +1023,13 @@ universe.command("list").action(
648
1023
  );
649
1024
  universe.command("create <slug>").action(
650
1025
  run(async (slug) => {
651
- const created = await api("POST", "/api/v1/universes", { slug });
1026
+ const created = await api("POST", "/universes", { slug });
652
1027
  console.log(`Created universe ${created.universe.slug}`);
653
1028
  })
654
1029
  );
655
1030
  universe.command("switch <slug>").action(
656
1031
  run(async (slug) => {
657
- await api("GET", `/api/v1/universes/${encodeURIComponent(slug)}`);
1032
+ await api("GET", `/universes/${encodeURIComponent(slug)}`);
658
1033
  saveConfig({ ...loadConfig(), currentUniverse: slug });
659
1034
  console.log(`Switched to ${slug}`);
660
1035
  })
@@ -662,7 +1037,7 @@ universe.command("switch <slug>").action(
662
1037
  universe.command("show").action(
663
1038
  run(async () => {
664
1039
  const slug = currentUniverse();
665
- const path = slug ? `/api/v1/universes/${encodeURIComponent(slug)}` : "/api/v1/whoami";
1040
+ const path = slug ? `/universes/${encodeURIComponent(slug)}` : "/whoami";
666
1041
  if (slug) {
667
1042
  const { universe: u } = await api("GET", path);
668
1043
  console.log(`${u.slug} \u2014 ${u.name} \xB7 ${u.agents} agents \xB7 ${u.members} members`);
@@ -673,22 +1048,37 @@ universe.command("show").action(
673
1048
  })
674
1049
  );
675
1050
  var agent = program.command("agent").description("agent specs: create, inspect, talk (via the default session)");
676
- 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(
677
- run(async (opts) => {
678
- const spec = opts.file ? readAgentFile(opts.file).spec : {
679
- name: opts.name ?? "My agent",
680
- agentType: opts.type,
681
- llm: opts.llm,
682
- displayName: opts.name
683
- };
684
- const { agent: created } = await api("POST", "/api/v1/agents", spec);
685
- console.log(`Created ${created.slug} (${created.agentType} \xB7 ${created.llm}) \u2014 spec only.`);
686
- console.log(`Start it: sky session create ${created.slug}`);
687
- })
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
+ )
688
1078
  );
689
1079
  agent.command("list").action(
690
1080
  run(async () => {
691
- const { agents } = await api("GET", "/api/v1/agents");
1081
+ const { agents } = await api("GET", "/agents");
692
1082
  if (!agents.length) {
693
1083
  console.log("No agents yet. `sky launch` or `sky agent create` to get one.");
694
1084
  return;
@@ -696,16 +1086,17 @@ agent.command("list").action(
696
1086
  for (const a of agents) {
697
1087
  const state = a.archived ? "archived" : `${a.sessionCount} session${a.sessionCount === 1 ? "" : "s"}`;
698
1088
  console.log(
699
- `${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}`
700
1090
  );
701
1091
  }
702
1092
  })
703
1093
  );
704
- agent.command("show <slug>").action(
705
- run(async (slug) => {
706
- const { agent: a } = await api("GET", `/api/v1/agents/${encodeURIComponent(slug)}`);
707
- 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)" : ""}`);
708
1098
  console.log(` type ${a.agentType} \xB7 ${a.llm}`);
1099
+ if (a.reasoningEffort) console.log(` effort ${a.reasoningEffort}`);
709
1100
  console.log(` version ${a.version}`);
710
1101
  console.log(` sessions ${a.sessionCount}`);
711
1102
  console.log(` universe ${a.universe}`);
@@ -713,42 +1104,72 @@ agent.command("show <slug>").action(
713
1104
  if (a.prompt) console.log(` prompt ${a.prompt.split("\n")[0]?.slice(0, 80)}\u2026`);
714
1105
  })
715
1106
  );
716
- agent.command("archive <slug>").description("retire the spec \u2014 read-only, running sessions unaffected (admin scope)").action(
717
- run(async (slug) => {
718
- await api("POST", `/api/v1/agents/${encodeURIComponent(slug)}/archive`);
719
- 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.`);
720
1111
  })
721
1112
  );
722
- agent.command("destroy <slug>").description("delete the spec; requires all sessions destroyed first (admin scope)").action(
723
- run(async (slug) => {
724
- await api("DELETE", `/api/v1/agents/${encodeURIComponent(slug)}`);
725
- 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}`);
726
1117
  })
727
1118
  );
728
- 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(
729
1120
  run(
730
- async (slug, text, opts) => sendAndFollow(await resolveSessionRef(slug), text, Boolean(opts.detach))
1121
+ async (id, text, opts) => sendAndFollow(await resolveSessionRef(id), text ?? "", opts)
731
1122
  )
732
1123
  );
733
- 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)));
734
1125
  var session = program.command("session").description("sessions: the running instances of an agent");
735
- 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(
736
- run(async (agentRef, opts) => {
737
- const { session: created } = await api("POST", "/api/v1/sessions", {
738
- agent: agentRef,
739
- ...opts.title ? { title: opts.title } : {},
740
- ...opts.vcpus !== void 0 ? { vcpus: Number(opts.vcpus) } : {},
741
- ...opts.memory !== void 0 ? { memoryMb: Number(opts.memory) } : {},
742
- ...opts.message ? { initial_events: [{ type: "user.message", parts: [{ type: "text", index: 0, text: opts.message }] }] } : {}
743
- });
744
- console.log(`Created ${created.id} (${created.agent} \xB7 ${created.status})${created.default ? " [default]" : ""}`);
745
- console.log(` sky agent message ${created.agent} "hello"`);
746
- })
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
+ )
747
1168
  );
748
1169
  session.command("list [agent]").action(
749
1170
  run(async (agentRef) => {
750
1171
  const qs = agentRef ? `?agent=${encodeURIComponent(agentRef)}` : "";
751
- const { sessions } = await api("GET", `/api/v1/sessions${qs}`);
1172
+ const { sessions } = await api("GET", `/sessions${qs}`);
752
1173
  if (!sessions.length) {
753
1174
  console.log("No sessions. `sky session create <agent>` to start one.");
754
1175
  return;
@@ -764,7 +1185,7 @@ session.command("show <id>").action(
764
1185
  run(async (id) => {
765
1186
  const { session: s } = await api(
766
1187
  "GET",
767
- `/api/v1/sessions/${encodeURIComponent(id)}`
1188
+ `/sessions/${encodeURIComponent(id)}`
768
1189
  );
769
1190
  console.log(`${s.id}${s.default ? " [default]" : ""}`);
770
1191
  console.log(` agent ${s.agent ?? "-"}`);
@@ -773,64 +1194,113 @@ session.command("show <id>").action(
773
1194
  console.log(` model ${s.agentType} \xB7 ${s.llm}`);
774
1195
  })
775
1196
  );
776
- 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(
777
1207
  run(async (id) => {
778
- await api("DELETE", `/api/v1/sessions/${encodeURIComponent(id)}`);
779
- console.log(`Destroyed ${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.`);
780
1212
  })
781
1213
  );
782
- 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(
1214
+ session.command("delete <id>").description("permanently delete the session, its events, and its sandbox").action(
1215
+ run(async (id) => {
1216
+ await api("DELETE", `/sessions/${encodeURIComponent(id)}`).catch(
1217
+ (error) => failWithInterruptHint(error, id)
1218
+ );
1219
+ console.log(`Deleted ${id}`);
1220
+ })
1221
+ );
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(
783
1223
  run(
784
- async (ref, text, opts) => sendAndFollow(await resolveSessionRef(ref), text, Boolean(opts.detach))
1224
+ async (ref, text, opts) => sendAndFollow(await resolveSessionRef(ref), text ?? "", opts)
785
1225
  )
786
1226
  );
787
- 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)));
788
1228
  session.command("interrupt <ref>").description("abort the in-flight turn").action(
789
1229
  run(async (ref) => {
790
1230
  const id = await resolveSessionRef(ref);
791
1231
  const res = await api(
792
1232
  "POST",
793
- `/api/v1/sessions/${encodeURIComponent(id)}/interrupt`
1233
+ `/sessions/${encodeURIComponent(id)}/interrupt`
794
1234
  );
795
1235
  console.log(res.status === "no_turn" ? "nothing in flight" : "interrupting");
796
1236
  })
797
1237
  );
798
- session.command("logs <ref>").option("-f, --follow", "follow (SSE)").option("--since <duration>", "how far back, e.g. 1h, 30m", "1h").action(
799
- 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) => {
800
1242
  const id = await resolveSessionRef(ref);
801
- const qs = `?since=${encodeURIComponent(opts.since)}${opts.follow ? "&follow=true" : ""}`;
802
- if (!opts.follow) {
803
- const { lines } = await api(
804
- "GET",
805
- `/api/v1/sessions/${encodeURIComponent(id)}/logs${qs}`
806
- );
807
- 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.");
808
1246
  return;
809
1247
  }
810
- const { parseSseStream } = await import("./src-CNT7GXZH.js");
811
- const { apiBase: apiBase2 } = await import("./config-HRDRNTUU.js");
812
- const token = apiToken();
813
- const res = await fetch(`${apiBase2()}/api/v1/sessions/${encodeURIComponent(id)}/logs${qs}`, {
814
- headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }
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
815
1276
  });
816
- if (!res.ok || !res.body) throw new ApiError(res.status, "logs_failed", await res.text());
817
- for await (const frame of parseSseStream(res.body)) {
818
- try {
819
- const entry = JSON.parse(frame.data);
820
- if (entry.line) console.log(`${entry.at ?? ""} ${entry.line}`);
821
- } catch {
822
- }
823
- }
1277
+ console.log(`Rotated ${updated.id}`);
824
1278
  })
825
1279
  );
826
- 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);
827
1297
  const controller = new AbortController();
828
1298
  let opened = null;
829
- if (!detach) {
1299
+ if (!opts.detach) {
830
1300
  opened = (async () => {
831
1301
  for await (const frame of streamEvents(sessionId, controller.signal)) {
832
1302
  renderEvent(frame.event, frame.data);
833
- if (frame.event === "turn.status_idle" || frame.event === "turn.interrupted") {
1303
+ if (turnOver(frame)) {
834
1304
  controller.abort();
835
1305
  break;
836
1306
  }
@@ -838,24 +1308,24 @@ async function sendAndFollow(sessionId, text, detach) {
838
1308
  })();
839
1309
  await new Promise((r) => setTimeout(r, 300));
840
1310
  }
841
- await api("POST", `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`, {
842
- parts: [{ type: "text", index: 0, text }]
843
- });
844
- if (detach) {
1311
+ await postEvents(sessionId, events);
1312
+ if (opts.detach) {
845
1313
  console.log("accepted");
846
1314
  return;
847
1315
  }
848
1316
  await opened;
849
1317
  }
850
- async function chatLoop(sessionId) {
1318
+ async function chatLoop(sessionId, opts = {}) {
851
1319
  console.log(`Chatting with ${sessionId} \u2014 Ctrl-C to leave.`);
852
1320
  const controller = new AbortController();
853
- const rl = createInterface2({ input: process.stdin, output: process.stdout, prompt: "> " });
1321
+ const rl = createInterface3({ input: process.stdin, output: process.stdout, prompt: "> " });
1322
+ let system = opts.system;
854
1323
  void (async () => {
855
1324
  try {
856
1325
  for await (const frame of streamEvents(sessionId, controller.signal)) {
857
1326
  renderEvent(frame.event, frame.data);
858
- 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();
859
1329
  }
860
1330
  } catch (error) {
861
1331
  fail(error);
@@ -868,9 +1338,9 @@ async function chatLoop(sessionId) {
868
1338
  rl.prompt();
869
1339
  return;
870
1340
  }
871
- void api("POST", `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`, {
872
- parts: [{ type: "text", index: 0, text }]
873
- }).catch((error) => {
1341
+ const events = eventsFor(text, [], system);
1342
+ system = void 0;
1343
+ void postEvents(sessionId, events).catch((error) => {
874
1344
  console.error(`sky: ${error instanceof Error ? error.message : String(error)}`);
875
1345
  rl.prompt();
876
1346
  });
@@ -883,90 +1353,99 @@ async function chatLoop(sessionId) {
883
1353
  });
884
1354
  }
885
1355
  var secrets = program.command("secrets").description("agent secrets (values write-only)");
886
- secrets.command("set <slug> <pairs...>").description("set KEY=VALUE pairs on the agent").action(
887
- run(async (slug, pairs) => {
1356
+ secrets.command("set <id> <pairs...>").description("set KEY=VALUE pairs on the agent").action(
1357
+ run(async (id, pairs) => {
888
1358
  for (const pair of pairs) {
889
1359
  const eq = pair.indexOf("=");
890
1360
  if (eq < 1) throw new Error(`expected KEY=VALUE, got ${pair}`);
891
1361
  const key = pair.slice(0, eq);
892
- await api("PUT", `/api/v1/agents/${encodeURIComponent(slug)}/secrets/${encodeURIComponent(key)}`, {
1362
+ await api("PUT", `/agents/${encodeURIComponent(id)}/secrets/${encodeURIComponent(key)}`, {
893
1363
  value: pair.slice(eq + 1)
894
1364
  });
895
1365
  console.log(`set ${key}`);
896
1366
  }
897
1367
  })
898
1368
  );
899
- secrets.command("list <slug>").action(
900
- run(async (slug) => {
901
- 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`);
902
1372
  if (!keys.length) console.log("no secrets set");
903
1373
  for (const s of keys) {
904
1374
  console.log(`\u25CF ${s.key}${s.description ? ` \u2014 ${s.description}` : ""}`);
905
1375
  }
906
1376
  })
907
1377
  );
908
- secrets.command("unset <slug> <key>").action(
909
- run(async (slug, key) => {
1378
+ secrets.command("unset <id> <key>").action(
1379
+ run(async (id, key) => {
910
1380
  await api(
911
1381
  "DELETE",
912
- `/api/v1/agents/${encodeURIComponent(slug)}/secrets/${encodeURIComponent(key)}`
1382
+ `/agents/${encodeURIComponent(id)}/secrets/${encodeURIComponent(key)}`
913
1383
  );
914
1384
  console.log(`unset ${key}`);
915
1385
  })
916
1386
  );
917
- 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(
918
1388
  run(async (opts) => {
919
1389
  const path = resolve3(opts.file);
920
- if (!existsSync3(path)) {
921
- const name = basename2(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
1390
+ if (!existsSync4(path)) {
1391
+ const name = basename3(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
922
1392
  scaffoldAgentFile(path, name);
923
1393
  console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
924
1394
  }
925
1395
  const { file, spec } = readAgentFile(path);
926
- if (file.slug) {
927
- 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.`);
928
1398
  return;
929
1399
  }
930
1400
  const { vcpus, memoryMb, ...specBody } = spec;
931
- const { agent: created } = await api("POST", "/api/v1/agents", specBody);
932
- await api("POST", "/api/v1/sessions", {
933
- 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,
934
1410
  ...vcpus !== void 0 ? { vcpus } : {},
935
1411
  ...memoryMb !== void 0 ? { memoryMb } : {}
936
1412
  });
937
- stampSlug(path, created.slug);
938
- console.log(`Launched ${created.slug} (${created.agentType} \xB7 ${created.llm})`);
939
- console.log(` sky agent message ${created.slug} "hello"`);
940
- 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}`);
941
1418
  })
942
1419
  );
943
1420
  program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent)").option("-f, --file <path>", "agent file", "agent.toml").action(
944
1421
  run(async (opts) => {
945
1422
  const path = resolve3(opts.file);
946
1423
  const { file, spec } = readAgentFile(path);
947
- if (!file.slug) {
948
- 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.");
949
1426
  const { vcpus, memoryMb, ...specBody } = spec;
950
- const { agent: created } = await api("POST", "/api/v1/agents", specBody);
951
- await api("POST", "/api/v1/sessions", {
952
- agent: created.slug,
1427
+ const { agent: created } = await api("POST", "/agents", specBody);
1428
+ await api("POST", "/sessions", {
1429
+ agent: created.id,
953
1430
  ...vcpus !== void 0 ? { vcpus } : {},
954
1431
  ...memoryMb !== void 0 ? { memoryMb } : {}
955
1432
  });
956
- stampSlug(path, created.slug);
957
- console.log(`Launched ${created.slug}`);
1433
+ stampId(path, created.id);
1434
+ console.log(`Launched ${created.id}`);
958
1435
  return;
959
1436
  }
960
1437
  if (spec.prompt !== void 0) {
961
- await api("PUT", `/api/v1/agents/${encodeURIComponent(file.slug)}/prompt`, {
1438
+ await api("PUT", `/agents/${encodeURIComponent(file.id)}/prompt`, {
962
1439
  prompt: spec.prompt
963
1440
  });
964
1441
  }
965
- await api("PATCH", `/api/v1/agents/${encodeURIComponent(file.slug)}`, {
1442
+ await api("PATCH", `/agents/${encodeURIComponent(file.id)}`, {
966
1443
  displayName: spec.displayName,
967
- ...spec.capabilities ? { capabilities: spec.capabilities } : {}
1444
+ ...spec.capabilities ? { capabilities: spec.capabilities } : {},
1445
+ ...spec.reasoningEffort ? { reasoningEffort: spec.reasoningEffort } : {},
1446
+ ...spec.skills ? { skills: spec.skills } : {}
968
1447
  });
969
- console.log(`Deployed ${file.slug} (prompt + settings re-applied)`);
1448
+ console.log(`Deployed ${file.id} (prompt + settings re-applied)`);
970
1449
  })
971
1450
  );
972
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(
@@ -974,6 +1453,184 @@ program.command("clone").description("clone your local agent (Claude Code, \u202
974
1453
  async (opts) => clone(opts)
975
1454
  )
976
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
+ }
977
1634
  program.command("config").description("show the CLI config location and current values").action(() => {
978
1635
  const config = loadConfig();
979
1636
  console.log(`config: ${configPath()}`);