@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/{chunk-DTVZOF77.js → chunk-TMLNYYYY.js} +8766 -2133
- package/dist/index.js +874 -217
- package/dist/src-NNPV5GRJ.js +276 -0
- package/package.json +1 -1
- package/dist/chunk-MLKGABMK.js +0 -9
- package/dist/chunk-NLEDGUN7.js +0 -39
- package/dist/config-HRDRNTUU.js +0 -17
- package/dist/src-CNT7GXZH.js +0 -187
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
|
-
|
|
8
|
+
isEffortSupported,
|
|
9
|
+
mcpServersFromKept,
|
|
10
|
+
modelSubscriptionProviderFor,
|
|
11
|
+
reasoningEffortDocLine,
|
|
17
12
|
skillRefSchema,
|
|
18
|
-
splitMcpServers
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
splitMcpServers,
|
|
14
|
+
supersededBy
|
|
15
|
+
} from "./chunk-TMLNYYYY.js";
|
|
21
16
|
|
|
22
17
|
// src/index.ts
|
|
23
|
-
import { existsSync as
|
|
24
|
-
import { basename as
|
|
25
|
-
import { createInterface as
|
|
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
|
-
`/
|
|
150
|
+
`/agents/${encodeURIComponent(ref)}`
|
|
78
151
|
).catch(async () => {
|
|
79
|
-
await api("GET", `/
|
|
80
|
-
return { agent: {
|
|
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.
|
|
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-
|
|
165
|
+
const { parseSseStream } = await import("./src-NNPV5GRJ.js");
|
|
93
166
|
const res = await fetch(
|
|
94
|
-
`${apiBase()}/
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
178
|
-
const doc = parseToml(
|
|
179
|
-
doc.
|
|
180
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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 ??
|
|
409
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
|
|
209
410
|
}
|
|
210
411
|
function readIfExists(path) {
|
|
211
412
|
try {
|
|
212
|
-
return
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
296
|
-
if (existsSync2(
|
|
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
|
|
509
|
+
return join2(configRoot, "agentsky-link.json");
|
|
309
510
|
}
|
|
310
511
|
function readLink(configRoot) {
|
|
311
512
|
const link = readJson(linkPath(configRoot));
|
|
312
|
-
return link?.
|
|
513
|
+
return link?.agentId ? link : void 0;
|
|
313
514
|
}
|
|
314
515
|
function writeLink(configRoot, link) {
|
|
315
|
-
|
|
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 ${
|
|
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
|
|
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
|
-
...
|
|
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", "/
|
|
574
|
+
({ agent: created } = await api("POST", "/agents", body));
|
|
371
575
|
} catch (error) {
|
|
372
576
|
if (error instanceof ApiError && error.status === 422 && local.model) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
394
|
-
console.log(`Cloned \u2192 ${created.
|
|
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.
|
|
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.
|
|
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
|
-
`/
|
|
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.
|
|
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.
|
|
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
|
-
`/
|
|
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.
|
|
461
|
-
if (opts.open) openBrowser(`${apiBase()}/agents/${link.
|
|
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 = `${
|
|
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
|
-
"/
|
|
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 = `${
|
|
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()}/
|
|
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.
|
|
596
|
-
|
|
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
|
-
|
|
603
|
-
|
|
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
|
-
|
|
606
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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", "/
|
|
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", "/
|
|
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", "/
|
|
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", `/
|
|
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 ? `/
|
|
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(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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", "/
|
|
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.
|
|
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 <
|
|
705
|
-
run(async (
|
|
706
|
-
const { agent: a } = await api("GET", `/
|
|
707
|
-
console.log(`${a.
|
|
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 <
|
|
717
|
-
run(async (
|
|
718
|
-
await api("POST", `/
|
|
719
|
-
console.log(`Archived ${
|
|
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 <
|
|
723
|
-
run(async (
|
|
724
|
-
await api("DELETE", `/
|
|
725
|
-
console.log(`Destroyed ${
|
|
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 <
|
|
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 (
|
|
1121
|
+
async (id, text, opts) => sendAndFollow(await resolveSessionRef(id), text ?? "", opts)
|
|
731
1122
|
)
|
|
732
1123
|
);
|
|
733
|
-
agent.command("chat <
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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", `/
|
|
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
|
-
`/
|
|
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
|
-
|
|
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("
|
|
779
|
-
|
|
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("
|
|
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,
|
|
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
|
-
`/
|
|
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("
|
|
799
|
-
|
|
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
|
|
802
|
-
if (!
|
|
803
|
-
|
|
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
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
842
|
-
|
|
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 =
|
|
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 === "
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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 <
|
|
887
|
-
run(async (
|
|
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", `/
|
|
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 <
|
|
900
|
-
run(async (
|
|
901
|
-
const { secrets: keys } = await api("GET", `/
|
|
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 <
|
|
909
|
-
run(async (
|
|
1378
|
+
secrets.command("unset <id> <key>").action(
|
|
1379
|
+
run(async (id, key) => {
|
|
910
1380
|
await api(
|
|
911
1381
|
"DELETE",
|
|
912
|
-
`/
|
|
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 (!
|
|
921
|
-
const name =
|
|
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.
|
|
927
|
-
console.log(`${file.
|
|
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
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
|
|
938
|
-
console.log(`Launched ${created.
|
|
939
|
-
|
|
940
|
-
console.log(` sky agent
|
|
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.
|
|
948
|
-
console.log("No
|
|
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", "/
|
|
951
|
-
await api("POST", "/
|
|
952
|
-
agent: created.
|
|
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
|
-
|
|
957
|
-
console.log(`Launched ${created.
|
|
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", `/
|
|
1438
|
+
await api("PUT", `/agents/${encodeURIComponent(file.id)}/prompt`, {
|
|
962
1439
|
prompt: spec.prompt
|
|
963
1440
|
});
|
|
964
1441
|
}
|
|
965
|
-
await api("PATCH", `/
|
|
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.
|
|
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()}`);
|