@agentsky/sky 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-DTVZOF77.js +14957 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NLEDGUN7.js +39 -0
- package/dist/config-HRDRNTUU.js +17 -0
- package/dist/index.js +894 -0
- package/dist/src-KNBIGOZP.js +187 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,894 @@
|
|
|
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
|
+
import {
|
|
11
|
+
AGENT_TYPES,
|
|
12
|
+
capabilitiesSchema,
|
|
13
|
+
customDataSchema,
|
|
14
|
+
customInstallSchema,
|
|
15
|
+
external_exports,
|
|
16
|
+
mcpConfigInstall,
|
|
17
|
+
skillRefSchema,
|
|
18
|
+
splitMcpServers
|
|
19
|
+
} from "./chunk-DTVZOF77.js";
|
|
20
|
+
import "./chunk-MLKGABMK.js";
|
|
21
|
+
|
|
22
|
+
// 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";
|
|
26
|
+
import { Command } from "commander";
|
|
27
|
+
|
|
28
|
+
// src/api.ts
|
|
29
|
+
var ApiError = class extends Error {
|
|
30
|
+
constructor(status, code, message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.status = status;
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
status;
|
|
36
|
+
code;
|
|
37
|
+
};
|
|
38
|
+
function authHeaders() {
|
|
39
|
+
const token = apiToken();
|
|
40
|
+
if (!token) {
|
|
41
|
+
throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
|
|
42
|
+
}
|
|
43
|
+
const headers = { authorization: `Bearer ${token}` };
|
|
44
|
+
const universe2 = currentUniverse();
|
|
45
|
+
if (universe2) headers["x-universe"] = universe2;
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
48
|
+
async function parseError(res) {
|
|
49
|
+
let code = "http_error";
|
|
50
|
+
let message = `${res.status} ${res.statusText}`;
|
|
51
|
+
try {
|
|
52
|
+
const body = await res.json();
|
|
53
|
+
if (body.error?.message) {
|
|
54
|
+
code = body.error.code ?? code;
|
|
55
|
+
message = body.error.message;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
throw new ApiError(res.status, code, message);
|
|
60
|
+
}
|
|
61
|
+
async function api(method, path, body, opts = {}) {
|
|
62
|
+
const res = await fetch(`${apiBase()}${path}`, {
|
|
63
|
+
method,
|
|
64
|
+
headers: {
|
|
65
|
+
...opts.auth === false ? {} : authHeaders(),
|
|
66
|
+
...body !== void 0 ? { "content-type": "application/json" } : {}
|
|
67
|
+
},
|
|
68
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) await parseError(res);
|
|
71
|
+
return await res.json();
|
|
72
|
+
}
|
|
73
|
+
async function* streamEvents(agentSlug, signal) {
|
|
74
|
+
const { parseSseStream } = await import("./src-KNBIGOZP.js");
|
|
75
|
+
const res = await fetch(
|
|
76
|
+
`${apiBase()}/api/v1/agents/${encodeURIComponent(agentSlug)}/stream`,
|
|
77
|
+
{
|
|
78
|
+
headers: { ...authHeaders(), accept: "text/event-stream" },
|
|
79
|
+
signal
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
if (!res.ok) await parseError(res);
|
|
83
|
+
if (!res.body) throw new ApiError(502, "no_body", "stream returned no body");
|
|
84
|
+
try {
|
|
85
|
+
for await (const frame of parseSseStream(res.body)) {
|
|
86
|
+
if (!frame.data) continue;
|
|
87
|
+
try {
|
|
88
|
+
yield { id: frame.id, event: frame.event, data: JSON.parse(frame.data) };
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (!(error instanceof DOMException && error.name === "AbortError")) throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/agent-file.ts
|
|
98
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
99
|
+
import { dirname, resolve } from "path";
|
|
100
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
101
|
+
var fileInstructionSchema = external_exports.object({
|
|
102
|
+
name: external_exports.string(),
|
|
103
|
+
content: external_exports.string().optional(),
|
|
104
|
+
file: external_exports.string().optional()
|
|
105
|
+
});
|
|
106
|
+
var agentFileSchema = external_exports.object({
|
|
107
|
+
name: external_exports.string().min(1),
|
|
108
|
+
slug: external_exports.string().optional(),
|
|
109
|
+
// stamped by launch/deploy; identifies the live agent
|
|
110
|
+
description: external_exports.string().optional(),
|
|
111
|
+
type: external_exports.enum(AGENT_TYPES).optional(),
|
|
112
|
+
llm: external_exports.string().optional(),
|
|
113
|
+
capabilities: capabilitiesSchema.optional(),
|
|
114
|
+
prompt: external_exports.string().optional(),
|
|
115
|
+
prompt_file: external_exports.string().optional(),
|
|
116
|
+
instructions: external_exports.array(fileInstructionSchema).optional(),
|
|
117
|
+
skills: external_exports.array(skillRefSchema).optional(),
|
|
118
|
+
customInstalls: external_exports.array(customInstallSchema).optional(),
|
|
119
|
+
customData: external_exports.array(customDataSchema).optional(),
|
|
120
|
+
/** Advanced machine shape (create-only): 1|2|4|8 vCPUs, 256MB-step memory. */
|
|
121
|
+
vcpus: external_exports.number().int().optional(),
|
|
122
|
+
memory_mb: external_exports.number().int().optional()
|
|
123
|
+
});
|
|
124
|
+
function readAgentFile(path) {
|
|
125
|
+
const raw = readFileSync(path, "utf8");
|
|
126
|
+
const parsed = agentFileSchema.safeParse(parseToml(raw));
|
|
127
|
+
if (!parsed.success) {
|
|
128
|
+
const issue = parsed.error.issues[0];
|
|
129
|
+
throw new Error(`${path}: ${issue?.path.join(".") ?? ""} ${issue?.message ?? "invalid"}`);
|
|
130
|
+
}
|
|
131
|
+
const file = parsed.data;
|
|
132
|
+
const base = dirname(resolve(path));
|
|
133
|
+
const inline = (ref, inlineContent) => {
|
|
134
|
+
if (inlineContent !== void 0) return inlineContent;
|
|
135
|
+
if (!ref) return void 0;
|
|
136
|
+
return readFileSync(resolve(base, ref), "utf8");
|
|
137
|
+
};
|
|
138
|
+
const spec = {
|
|
139
|
+
name: file.name,
|
|
140
|
+
description: file.description,
|
|
141
|
+
agentType: file.type,
|
|
142
|
+
llm: file.llm,
|
|
143
|
+
displayName: file.name,
|
|
144
|
+
capabilities: file.capabilities,
|
|
145
|
+
prompt: inline(file.prompt_file, file.prompt),
|
|
146
|
+
instructions: file.instructions?.map((f) => {
|
|
147
|
+
const content = inline(f.file, f.content);
|
|
148
|
+
if (content === void 0) throw new Error(`instruction ${f.name}: content or file required`);
|
|
149
|
+
return { name: f.name, content };
|
|
150
|
+
}),
|
|
151
|
+
skills: file.skills,
|
|
152
|
+
customInstalls: file.customInstalls,
|
|
153
|
+
customData: file.customData,
|
|
154
|
+
vcpus: file.vcpus,
|
|
155
|
+
memoryMb: file.memory_mb
|
|
156
|
+
};
|
|
157
|
+
return { file, spec };
|
|
158
|
+
}
|
|
159
|
+
function stampSlug(path, slug) {
|
|
160
|
+
const doc = parseToml(readFileSync(path, "utf8"));
|
|
161
|
+
doc.slug = slug;
|
|
162
|
+
writeFileSync(path, stringifyToml(doc));
|
|
163
|
+
}
|
|
164
|
+
function scaffoldAgentFile(path, name) {
|
|
165
|
+
if (existsSync(path)) throw new Error(`${path} already exists`);
|
|
166
|
+
writeFileSync(
|
|
167
|
+
path,
|
|
168
|
+
`# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
|
|
169
|
+
name = "${name}"
|
|
170
|
+
# type = "hermes" # hermes | claude_code | codex | openclaw
|
|
171
|
+
# llm = "deepseek-v4-flash" # must be compatible with type
|
|
172
|
+
# capabilities = []
|
|
173
|
+
|
|
174
|
+
prompt = """
|
|
175
|
+
You are ${name}, a helpful long-lived agent.
|
|
176
|
+
"""
|
|
177
|
+
`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/clone.ts
|
|
182
|
+
import { spawn } from "child_process";
|
|
183
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
184
|
+
import { homedir } from "os";
|
|
185
|
+
import { basename, join } from "path";
|
|
186
|
+
import { createInterface } from "readline";
|
|
187
|
+
var PROMPT_CAP = 1e5;
|
|
188
|
+
var HARNESSES = ["claude_code", "codex", "openclaw"];
|
|
189
|
+
function claudeConfigRoot() {
|
|
190
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
191
|
+
}
|
|
192
|
+
function readIfExists(path) {
|
|
193
|
+
try {
|
|
194
|
+
return readFileSync2(path, "utf8");
|
|
195
|
+
} catch {
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function readJson(path) {
|
|
200
|
+
const raw = readIfExists(path);
|
|
201
|
+
if (raw === void 0) return void 0;
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(raw);
|
|
204
|
+
} catch {
|
|
205
|
+
return void 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function claudeModel(configRoot) {
|
|
209
|
+
const candidates = [
|
|
210
|
+
join(process.cwd(), ".claude", "settings.local.json"),
|
|
211
|
+
join(process.cwd(), ".claude", "settings.json"),
|
|
212
|
+
join(configRoot, "settings.json")
|
|
213
|
+
];
|
|
214
|
+
for (const path of candidates) {
|
|
215
|
+
const model = readJson(path)?.model;
|
|
216
|
+
if (typeof model === "string" && model.trim()) return model.trim();
|
|
217
|
+
}
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
function claudeMcpServers() {
|
|
221
|
+
const merged = {};
|
|
222
|
+
const userConfig = readJson(join(homedir(), ".claude.json"));
|
|
223
|
+
Object.assign(merged, userConfig?.mcpServers ?? {});
|
|
224
|
+
Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
|
|
225
|
+
const projectConfig = readJson(
|
|
226
|
+
join(process.cwd(), ".mcp.json")
|
|
227
|
+
);
|
|
228
|
+
Object.assign(merged, projectConfig?.mcpServers ?? {});
|
|
229
|
+
return merged;
|
|
230
|
+
}
|
|
231
|
+
function assemblePrompt(parts) {
|
|
232
|
+
const joined = parts.map((p) => `# \u2500\u2500 ${p.label} \u2500\u2500
|
|
233
|
+
|
|
234
|
+
${p.content.trim()}`).join("\n\n").trim();
|
|
235
|
+
if (joined.length <= PROMPT_CAP) return { prompt: joined, truncated: false };
|
|
236
|
+
const notice = "\n\n[truncated by sky clone: local instructions exceeded the 100k prompt cap]";
|
|
237
|
+
return { prompt: joined.slice(0, PROMPT_CAP - notice.length) + notice, truncated: true };
|
|
238
|
+
}
|
|
239
|
+
function readClaudeCode() {
|
|
240
|
+
const configRoot = claudeConfigRoot();
|
|
241
|
+
const parts = [];
|
|
242
|
+
const sources = [];
|
|
243
|
+
const global = readIfExists(join(configRoot, "CLAUDE.md"));
|
|
244
|
+
if (global?.trim()) {
|
|
245
|
+
parts.push({ label: "Global CLAUDE.md", content: global });
|
|
246
|
+
sources.push("global CLAUDE.md");
|
|
247
|
+
}
|
|
248
|
+
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
249
|
+
const project = readIfExists(join(process.cwd(), name));
|
|
250
|
+
if (project?.trim()) {
|
|
251
|
+
parts.push({ label: `Project ${name}`, content: project });
|
|
252
|
+
sources.push(`project ${name}`);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const { prompt, truncated } = assemblePrompt(parts);
|
|
257
|
+
const { kept, dropped } = splitMcpServers(claudeMcpServers());
|
|
258
|
+
return {
|
|
259
|
+
harness: "claude_code",
|
|
260
|
+
configRoot,
|
|
261
|
+
prompt,
|
|
262
|
+
promptSources: sources,
|
|
263
|
+
truncated,
|
|
264
|
+
model: claudeModel(configRoot),
|
|
265
|
+
keptServers: kept,
|
|
266
|
+
droppedServers: dropped
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function detectHarness(explicit) {
|
|
270
|
+
if (explicit) {
|
|
271
|
+
if (!HARNESSES.includes(explicit)) {
|
|
272
|
+
throw new Error(`unknown harness ${explicit} \u2014 expected ${HARNESSES.join(" | ")}`);
|
|
273
|
+
}
|
|
274
|
+
return explicit;
|
|
275
|
+
}
|
|
276
|
+
if (existsSync2(claudeConfigRoot())) return "claude_code";
|
|
277
|
+
if (existsSync2(join(homedir(), ".codex"))) return "codex";
|
|
278
|
+
if (existsSync2(join(homedir(), ".openclaw"))) return "openclaw";
|
|
279
|
+
throw new Error(
|
|
280
|
+
"no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
function readLocal(harness) {
|
|
284
|
+
if (harness === "claude_code") return readClaudeCode();
|
|
285
|
+
throw new Error(
|
|
286
|
+
`${harness} cloning lands next \u2014 claude_code is the first supported harness`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
function linkPath(configRoot) {
|
|
290
|
+
return join(configRoot, "agentsky-link.json");
|
|
291
|
+
}
|
|
292
|
+
function readLink(configRoot) {
|
|
293
|
+
const link = readJson(linkPath(configRoot));
|
|
294
|
+
return link?.agentSlug ? link : void 0;
|
|
295
|
+
}
|
|
296
|
+
function writeLink(configRoot, link) {
|
|
297
|
+
writeFileSync2(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
|
|
298
|
+
`);
|
|
299
|
+
}
|
|
300
|
+
function confirm(question) {
|
|
301
|
+
return new Promise((done) => {
|
|
302
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
303
|
+
rl.question(`${question} [Y/n] `, (answer) => {
|
|
304
|
+
rl.close();
|
|
305
|
+
done(/^(y(es)?)?$/i.test(answer.trim()));
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function openBrowser(url) {
|
|
310
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
311
|
+
try {
|
|
312
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function kb(text) {
|
|
317
|
+
return `${Math.max(1, Math.round(text.length / 1024))} KB`;
|
|
318
|
+
}
|
|
319
|
+
async function createClone(local, opts) {
|
|
320
|
+
const keptNames = Object.keys(local.keptServers);
|
|
321
|
+
const summary = [
|
|
322
|
+
local.promptSources.length ? `${local.promptSources.join(" + ")} (${kb(local.prompt)})` : "no instructions found",
|
|
323
|
+
`model ${local.model ?? "(harness default)"}`,
|
|
324
|
+
`${keptNames.length} MCP server${keptNames.length === 1 ? "" : "s"}${keptNames.length ? ` (${keptNames.join(", ")})` : ""}`
|
|
325
|
+
].join(" \xB7 ");
|
|
326
|
+
console.log(`Detected ${local.harness} at ${local.configRoot} (project ${basename(process.cwd())})`);
|
|
327
|
+
console.log(`Will clone: ${summary}`);
|
|
328
|
+
if (local.truncated) console.log(" note: instructions exceeded 100k chars and were truncated");
|
|
329
|
+
if (local.droppedServers.length) {
|
|
330
|
+
console.log(
|
|
331
|
+
`Dropped (need credentials \u2014 not supported yet): ${local.droppedServers.map((n) => `${n} MCP`).join(", ")}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (opts.dryRun) {
|
|
335
|
+
console.log("Dry run \u2014 nothing sent.");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!opts.yes && !await confirm("Launch this as a new cloud agent?")) {
|
|
339
|
+
console.log("Aborted \u2014 nothing sent.");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const installs = keptNames.length ? [mcpConfigInstall(local.keptServers)] : [];
|
|
343
|
+
const body = {
|
|
344
|
+
agentType: local.harness,
|
|
345
|
+
...local.model ? { llm: local.model } : {},
|
|
346
|
+
...local.prompt ? { prompt: local.prompt } : {},
|
|
347
|
+
...installs.length ? { customInstalls: installs } : {}
|
|
348
|
+
};
|
|
349
|
+
let sentLlm = local.model ?? null;
|
|
350
|
+
let created;
|
|
351
|
+
try {
|
|
352
|
+
({ agent: created } = await api("POST", "/api/v1/agents", body));
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error instanceof ApiError && error.status === 422 && local.model) {
|
|
355
|
+
console.log(` note: ${local.model} isn't available for ${local.harness} \u2014 using the harness default`);
|
|
356
|
+
sentLlm = null;
|
|
357
|
+
({ agent: created } = await api("POST", "/api/v1/agents", {
|
|
358
|
+
...body,
|
|
359
|
+
llm: void 0
|
|
360
|
+
}));
|
|
361
|
+
} else {
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
366
|
+
writeLink(local.configRoot, {
|
|
367
|
+
baseUrl: apiBase(),
|
|
368
|
+
agentSlug: created.slug,
|
|
369
|
+
clonedAt: now,
|
|
370
|
+
lastSyncedAt: now,
|
|
371
|
+
llm: sentLlm,
|
|
372
|
+
mcpServers: keptNames
|
|
373
|
+
});
|
|
374
|
+
const url = `${apiBase()}/agents/${created.slug}`;
|
|
375
|
+
console.log(`Cloned \u2192 ${created.slug} ${url}`);
|
|
376
|
+
console.log(
|
|
377
|
+
`Linked this machine (${linkPath(local.configRoot)}). Push prompt changes anytime by running \`sky clone\` again.`
|
|
378
|
+
);
|
|
379
|
+
if (opts.open) openBrowser(url);
|
|
380
|
+
}
|
|
381
|
+
async function syncClone(local, link, opts) {
|
|
382
|
+
const since = link.lastSyncedAt ?? link.clonedAt;
|
|
383
|
+
console.log(
|
|
384
|
+
`Syncing ${link.agentSlug} from ${local.configRoot} (last synced ${since ? new Date(since).toLocaleDateString() : "never"})`
|
|
385
|
+
);
|
|
386
|
+
if (opts.dryRun) {
|
|
387
|
+
console.log(
|
|
388
|
+
` would push prompt (${kb(local.prompt)}) to ${link.agentSlug}. Dry run \u2014 nothing sent.`
|
|
389
|
+
);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
let agent2;
|
|
393
|
+
try {
|
|
394
|
+
({ agent: agent2 } = await api(
|
|
395
|
+
"GET",
|
|
396
|
+
`/api/v1/agents/${encodeURIComponent(link.agentSlug)}`
|
|
397
|
+
));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`linked agent ${link.agentSlug} no longer exists \u2014 run \`sky clone --new\` to launch a fresh one`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
if (agent2.agentType !== local.harness) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`linked agent ${link.agentSlug} is ${agent2.agentType}, this machine runs ${local.harness} \u2014 run \`sky clone --new\` to launch a fresh agent`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const result = await api(
|
|
412
|
+
"PUT",
|
|
413
|
+
`/api/v1/agents/${encodeURIComponent(link.agentSlug)}/prompt`,
|
|
414
|
+
{ prompt: local.prompt }
|
|
415
|
+
);
|
|
416
|
+
console.log(
|
|
417
|
+
result.unchanged ? " prompt unchanged" : ` prompt updated (v${result.version}${result.applied === "live" ? ", hot-reloaded" : ", applies on next boot"})`
|
|
418
|
+
);
|
|
419
|
+
const drift = [];
|
|
420
|
+
const localModel = local.model ?? null;
|
|
421
|
+
const linkedModel = link.llm ?? agent2.llm;
|
|
422
|
+
if (localModel && linkedModel && localModel !== linkedModel) {
|
|
423
|
+
drift.push(`model changed ${linkedModel} \u2192 ${localModel}`);
|
|
424
|
+
}
|
|
425
|
+
const before = new Set(link.mcpServers ?? []);
|
|
426
|
+
const nowKept = Object.keys(local.keptServers);
|
|
427
|
+
const newServers = nowKept.filter((n) => !before.has(n));
|
|
428
|
+
const goneServers = [...before].filter((n) => !nowKept.includes(n));
|
|
429
|
+
if (newServers.length) drift.push(`${newServers.length} new MCP server${newServers.length === 1 ? "" : "s"} (${newServers.join(", ")})`);
|
|
430
|
+
if (goneServers.length) drift.push(`${goneServers.length} removed MCP server${goneServers.length === 1 ? "" : "s"} (${goneServers.join(", ")})`);
|
|
431
|
+
if (drift.length) {
|
|
432
|
+
console.log(` skipped (set at launch): ${drift.join("; ")}`);
|
|
433
|
+
console.log(" \u2192 a running agent can't change these; run `sky clone --new` to launch a fresh agent.");
|
|
434
|
+
}
|
|
435
|
+
if (local.droppedServers.length) {
|
|
436
|
+
console.log(
|
|
437
|
+
` dropped (need credentials \u2014 not supported yet): ${local.droppedServers.map((n) => `${n} MCP`).join(", ")}`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
writeLink(local.configRoot, { ...link, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
441
|
+
console.log(`Synced ${link.agentSlug}.`);
|
|
442
|
+
if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentSlug}`);
|
|
443
|
+
}
|
|
444
|
+
async function clone(opts) {
|
|
445
|
+
const harness = detectHarness(opts.harness);
|
|
446
|
+
const local = readLocal(harness);
|
|
447
|
+
const link = opts.new ? void 0 : readLink(local.configRoot);
|
|
448
|
+
if (link) await syncClone(local, link, opts);
|
|
449
|
+
else await createClone(local, opts);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/login.ts
|
|
453
|
+
import { createHash, randomBytes } from "crypto";
|
|
454
|
+
import { createServer } from "http";
|
|
455
|
+
import { hostname } from "os";
|
|
456
|
+
import { spawn as spawn2 } from "child_process";
|
|
457
|
+
function b64url(buf) {
|
|
458
|
+
return buf.toString("base64url");
|
|
459
|
+
}
|
|
460
|
+
function openBrowser2(url) {
|
|
461
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
462
|
+
try {
|
|
463
|
+
spawn2(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function login(opts) {
|
|
468
|
+
const verifier = b64url(randomBytes(32));
|
|
469
|
+
const challenge = b64url(createHash("sha256").update(verifier).digest());
|
|
470
|
+
const result = opts.noBrowser ? await headlessLogin(verifier, challenge) : await browserLogin(verifier, challenge);
|
|
471
|
+
saveConfig({
|
|
472
|
+
...loadConfig(),
|
|
473
|
+
apiBase: apiBase(),
|
|
474
|
+
token: result.token,
|
|
475
|
+
email: result.user.email ?? void 0,
|
|
476
|
+
currentUniverse: void 0
|
|
477
|
+
// personal by default
|
|
478
|
+
});
|
|
479
|
+
console.log(
|
|
480
|
+
`Logged in as ${result.user.email ?? "you"}${result.universe ? ` (universe: ${result.universe.slug})` : ""}`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
async function browserLogin(verifier, challenge) {
|
|
484
|
+
const state = b64url(randomBytes(16));
|
|
485
|
+
const code = await new Promise((resolve4, reject) => {
|
|
486
|
+
const server = createServer((req, res) => {
|
|
487
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
488
|
+
if (url.pathname !== "/callback") {
|
|
489
|
+
res.writeHead(404).end();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
493
|
+
res.end("<p>Logged in \u2014 you can close this tab and return to your terminal.</p>");
|
|
494
|
+
const got = url.searchParams.get("code");
|
|
495
|
+
if (url.searchParams.get("state") !== state || !got) {
|
|
496
|
+
reject(new Error("login callback state mismatch"));
|
|
497
|
+
} else {
|
|
498
|
+
resolve4(got);
|
|
499
|
+
}
|
|
500
|
+
server.close();
|
|
501
|
+
});
|
|
502
|
+
server.listen(0, "127.0.0.1", () => {
|
|
503
|
+
const address = server.address();
|
|
504
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
505
|
+
const authorizeUrl = `${apiBase()}/cli/authorize?state=${state}&port=${port}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
|
|
506
|
+
console.log("Opening your browser to authorize this machine\u2026");
|
|
507
|
+
console.log(`If it doesn't open, visit:
|
|
508
|
+
${authorizeUrl}`);
|
|
509
|
+
openBrowser2(authorizeUrl);
|
|
510
|
+
});
|
|
511
|
+
setTimeout(() => {
|
|
512
|
+
server.close();
|
|
513
|
+
reject(new Error("login timed out after 10 minutes"));
|
|
514
|
+
}, 10 * 60 * 1e3).unref();
|
|
515
|
+
});
|
|
516
|
+
return api(
|
|
517
|
+
"POST",
|
|
518
|
+
"/api/v1/cli/token-exchange",
|
|
519
|
+
{ code, verifier, clientName: `cli ${hostname()}` },
|
|
520
|
+
{ auth: false }
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
async function headlessLogin(verifier, challenge) {
|
|
524
|
+
const request = b64url(randomBytes(24));
|
|
525
|
+
const authorizeUrl = `${apiBase()}/cli/authorize?request=${request}&challenge=${encodeURIComponent(challenge)}&scopes=read,write`;
|
|
526
|
+
console.log(`Visit this URL on any device to authorize:
|
|
527
|
+
${authorizeUrl}
|
|
528
|
+
`);
|
|
529
|
+
process.stdout.write("Waiting for approval\u2026");
|
|
530
|
+
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
531
|
+
while (Date.now() < deadline) {
|
|
532
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
533
|
+
const res = await fetch(
|
|
534
|
+
`${apiBase()}/api/v1/cli/poll?request=${request}&verifier=${encodeURIComponent(verifier)}&clientName=${encodeURIComponent(`cli ${hostname()}`)}`
|
|
535
|
+
);
|
|
536
|
+
if (!res.ok) {
|
|
537
|
+
const body2 = await res.json().catch(() => null);
|
|
538
|
+
throw new Error(body2?.error?.message ?? `poll failed (${res.status})`);
|
|
539
|
+
}
|
|
540
|
+
const body = await res.json();
|
|
541
|
+
if (body.status === "approved") {
|
|
542
|
+
console.log(" approved.");
|
|
543
|
+
return body;
|
|
544
|
+
}
|
|
545
|
+
process.stdout.write(".");
|
|
546
|
+
}
|
|
547
|
+
throw new Error("login timed out after 10 minutes");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/render.ts
|
|
551
|
+
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
552
|
+
var red = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
553
|
+
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
554
|
+
function clip(value, max = 120) {
|
|
555
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
556
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
557
|
+
}
|
|
558
|
+
function renderEvent(event, data) {
|
|
559
|
+
const part = data.part;
|
|
560
|
+
switch (event) {
|
|
561
|
+
case "agent.message":
|
|
562
|
+
console.log(String(data.text ?? ""));
|
|
563
|
+
break;
|
|
564
|
+
case "agent.reasoning":
|
|
565
|
+
console.log(dim(`\xB7 ${clip(part?.text ?? "")}`));
|
|
566
|
+
break;
|
|
567
|
+
case "agent.tool_use":
|
|
568
|
+
console.log(dim(`\u2192 ${String(part?.tool_name ?? "tool")}(${clip(part?.args ?? {}, 80)})`));
|
|
569
|
+
break;
|
|
570
|
+
case "agent.tool_result":
|
|
571
|
+
console.log(dim(`\u2190 ${String(part?.tool_name ?? "tool")} ${String(part?.status ?? "")}`));
|
|
572
|
+
break;
|
|
573
|
+
case "agent.status":
|
|
574
|
+
console.log(dim(`\u2026 ${String(part?.level ?? "")} ${String(part?.text ?? "")}`.trim()));
|
|
575
|
+
break;
|
|
576
|
+
case "user.message": {
|
|
577
|
+
const channel = String(data.channel ?? "");
|
|
578
|
+
if (channel && channel !== "api") {
|
|
579
|
+
console.log(cyan(`[${channel}] ${clip(data.text ?? "")}`));
|
|
580
|
+
}
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
case "turn.interrupted":
|
|
584
|
+
console.log(dim("\u23F9 turn interrupted"));
|
|
585
|
+
break;
|
|
586
|
+
case "error":
|
|
587
|
+
console.error(red(`error: ${String(data.message ?? data.code ?? "unknown")}`));
|
|
588
|
+
break;
|
|
589
|
+
default:
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/index.ts
|
|
595
|
+
var program = new Command();
|
|
596
|
+
program.name("sky").description("Launch and talk to long-lived agents \u2014 agentsky.dev").version("0.1.0");
|
|
597
|
+
function fail(error) {
|
|
598
|
+
if (error instanceof ApiError) console.error(`sky: ${error.message} (${error.code})`);
|
|
599
|
+
else console.error(`sky: ${error instanceof Error ? error.message : String(error)}`);
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
var run = (fn) => (...args) => fn(...args).catch(fail);
|
|
603
|
+
var auth = program.command("auth").description("login, logout, whoami");
|
|
604
|
+
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 })));
|
|
605
|
+
auth.command("logout").description("forget the stored token (revoke it in Settings \u2192 API tokens)").action(
|
|
606
|
+
run(async () => {
|
|
607
|
+
saveConfig({ ...loadConfig(), token: void 0, email: void 0 });
|
|
608
|
+
console.log(`Logged out. (Token revocation: Settings \u2192 API tokens.)`);
|
|
609
|
+
})
|
|
610
|
+
);
|
|
611
|
+
auth.command("whoami").description("who the current token acts as").action(
|
|
612
|
+
run(async () => {
|
|
613
|
+
const who = await api("GET", "/api/v1/whoami");
|
|
614
|
+
console.log(
|
|
615
|
+
`${who.user.email ?? "unknown"} \xB7 universe ${who.universe.slug}${who.universe.isPersonal ? " (personal)" : ""} \xB7 scopes ${who.scopes.join(",")}`
|
|
616
|
+
);
|
|
617
|
+
})
|
|
618
|
+
);
|
|
619
|
+
var universe = program.command("universe").description("grouping boundary for agents");
|
|
620
|
+
universe.command("list").action(
|
|
621
|
+
run(async () => {
|
|
622
|
+
const { universes } = await api("GET", "/api/v1/universes");
|
|
623
|
+
const current = currentUniverse();
|
|
624
|
+
for (const u of universes) {
|
|
625
|
+
const marker = u.slug === current || !current && u.isPersonal ? "*" : " ";
|
|
626
|
+
console.log(`${marker} ${u.slug}${u.isPersonal ? " (personal)" : ""}`);
|
|
627
|
+
}
|
|
628
|
+
})
|
|
629
|
+
);
|
|
630
|
+
universe.command("create <slug>").action(
|
|
631
|
+
run(async (slug) => {
|
|
632
|
+
const created = await api("POST", "/api/v1/universes", { slug });
|
|
633
|
+
console.log(`Created universe ${created.universe.slug}`);
|
|
634
|
+
})
|
|
635
|
+
);
|
|
636
|
+
universe.command("switch <slug>").action(
|
|
637
|
+
run(async (slug) => {
|
|
638
|
+
await api("GET", `/api/v1/universes/${encodeURIComponent(slug)}`);
|
|
639
|
+
saveConfig({ ...loadConfig(), currentUniverse: slug });
|
|
640
|
+
console.log(`Switched to ${slug}`);
|
|
641
|
+
})
|
|
642
|
+
);
|
|
643
|
+
universe.command("show").action(
|
|
644
|
+
run(async () => {
|
|
645
|
+
const slug = currentUniverse();
|
|
646
|
+
const path = slug ? `/api/v1/universes/${encodeURIComponent(slug)}` : "/api/v1/whoami";
|
|
647
|
+
if (slug) {
|
|
648
|
+
const { universe: u } = await api("GET", path);
|
|
649
|
+
console.log(`${u.slug} \u2014 ${u.name} \xB7 ${u.agents} agents \xB7 ${u.members} members`);
|
|
650
|
+
} else {
|
|
651
|
+
const who = await api("GET", path);
|
|
652
|
+
console.log(`${who.universe.slug} (personal)`);
|
|
653
|
+
}
|
|
654
|
+
})
|
|
655
|
+
);
|
|
656
|
+
var agent = program.command("agent").description("create, inspect, talk to agents");
|
|
657
|
+
agent.command("create").description("create an agent (flags-only; bare create = instant default)").option("--type <type>", "hermes | claude_code | codex | openclaw").option("--llm <llm>", "model id (defaults per type)").option("--name <name>", "agent name").option("--vcpus <n>", "machine vCPUs: 1, 2, 4 or 8 (default 2)").option("--memory <mb>", "machine memory in MB, 256MB steps (default 4096)").option("-f, --file <path>", "create from an agent.toml").action(
|
|
658
|
+
run(async (opts) => {
|
|
659
|
+
const spec = opts.file ? readAgentFile(opts.file).spec : {
|
|
660
|
+
name: opts.name ?? "My agent",
|
|
661
|
+
agentType: opts.type,
|
|
662
|
+
llm: opts.llm,
|
|
663
|
+
displayName: opts.name
|
|
664
|
+
};
|
|
665
|
+
if (opts.vcpus !== void 0) spec.vcpus = Number(opts.vcpus);
|
|
666
|
+
if (opts.memory !== void 0) spec.memoryMb = Number(opts.memory);
|
|
667
|
+
const { agent: created } = await api("POST", "/api/v1/agents", spec);
|
|
668
|
+
console.log(`Created ${created.slug} (${created.agentType} \xB7 ${created.llm})`);
|
|
669
|
+
console.log(`Talk to it: sky agent message ${created.slug} "hello"`);
|
|
670
|
+
})
|
|
671
|
+
);
|
|
672
|
+
agent.command("list").action(
|
|
673
|
+
run(async () => {
|
|
674
|
+
const { agents } = await api("GET", "/api/v1/agents");
|
|
675
|
+
if (!agents.length) {
|
|
676
|
+
console.log("No agents yet. `sky launch` or `sky agent create` to get one.");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
for (const a of agents) {
|
|
680
|
+
console.log(
|
|
681
|
+
`${a.slug.padEnd(24)} ${a.displayName.padEnd(20)} ${a.agentType}/${a.llm} ${a.status.toLowerCase()}`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
})
|
|
685
|
+
);
|
|
686
|
+
agent.command("show <slug>").action(
|
|
687
|
+
run(async (slug) => {
|
|
688
|
+
const { agent: a } = await api("GET", `/api/v1/agents/${encodeURIComponent(slug)}`);
|
|
689
|
+
console.log(`${a.slug} \u2014 ${a.displayName}`);
|
|
690
|
+
console.log(` type ${a.agentType} \xB7 ${a.llm}`);
|
|
691
|
+
console.log(` status ${a.status.toLowerCase()}`);
|
|
692
|
+
console.log(` universe ${a.universe}`);
|
|
693
|
+
if (a.capabilities.length) console.log(` caps ${a.capabilities.join(", ")}`);
|
|
694
|
+
if (a.prompt) console.log(` prompt ${a.prompt.split("\n")[0]?.slice(0, 80)}\u2026`);
|
|
695
|
+
})
|
|
696
|
+
);
|
|
697
|
+
agent.command("destroy <slug>").description("tear down the pod and delete the agent (admin scope)").action(
|
|
698
|
+
run(async (slug) => {
|
|
699
|
+
await api("DELETE", `/api/v1/agents/${encodeURIComponent(slug)}`);
|
|
700
|
+
console.log(`Destroyed ${slug}`);
|
|
701
|
+
})
|
|
702
|
+
);
|
|
703
|
+
agent.command("logs <slug>").option("-f, --follow", "follow (SSE)").option("--since <duration>", "how far back, e.g. 1h, 30m", "1h").action(
|
|
704
|
+
run(async (slug, opts) => {
|
|
705
|
+
const qs = `?since=${encodeURIComponent(opts.since)}${opts.follow ? "&follow=true" : ""}`;
|
|
706
|
+
if (!opts.follow) {
|
|
707
|
+
const { lines } = await api(
|
|
708
|
+
"GET",
|
|
709
|
+
`/api/v1/agents/${encodeURIComponent(slug)}/logs${qs}`
|
|
710
|
+
);
|
|
711
|
+
for (const entry of lines) console.log(`${entry.at} ${entry.line}`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const { parseSseStream } = await import("./src-KNBIGOZP.js");
|
|
715
|
+
const { apiBase: apiBase2 } = await import("./config-HRDRNTUU.js");
|
|
716
|
+
const token = apiToken();
|
|
717
|
+
const res = await fetch(`${apiBase2()}/api/v1/agents/${encodeURIComponent(slug)}/logs${qs}`, {
|
|
718
|
+
headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }
|
|
719
|
+
});
|
|
720
|
+
if (!res.ok || !res.body) throw new ApiError(res.status, "logs_failed", await res.text());
|
|
721
|
+
for await (const frame of parseSseStream(res.body)) {
|
|
722
|
+
try {
|
|
723
|
+
const entry = JSON.parse(frame.data);
|
|
724
|
+
if (entry.line) console.log(`${entry.at ?? ""} ${entry.line}`);
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
})
|
|
729
|
+
);
|
|
730
|
+
async function sendAndFollow(slug, text, detach) {
|
|
731
|
+
const controller = new AbortController();
|
|
732
|
+
let opened = null;
|
|
733
|
+
if (!detach) {
|
|
734
|
+
opened = (async () => {
|
|
735
|
+
for await (const frame of streamEvents(slug, controller.signal)) {
|
|
736
|
+
renderEvent(frame.event, frame.data);
|
|
737
|
+
if (frame.event === "turn.status_idle" || frame.event === "turn.interrupted") {
|
|
738
|
+
controller.abort();
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
})();
|
|
743
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
744
|
+
}
|
|
745
|
+
const { turnId } = await api(
|
|
746
|
+
"POST",
|
|
747
|
+
`/api/v1/agents/${encodeURIComponent(slug)}/messages`,
|
|
748
|
+
{ parts: [{ type: "text", index: 0, text }] }
|
|
749
|
+
);
|
|
750
|
+
if (detach) {
|
|
751
|
+
console.log(`accepted ${turnId}`);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
await opened;
|
|
755
|
+
}
|
|
756
|
+
agent.command("message <slug> <text>").description("send one message and stream the turn until idle").option("--detach", "don't follow the stream; print the turnId and exit").action(
|
|
757
|
+
run(
|
|
758
|
+
async (slug, text, opts) => sendAndFollow(slug, text, Boolean(opts.detach))
|
|
759
|
+
)
|
|
760
|
+
);
|
|
761
|
+
agent.command("chat <slug>").description("interactive REPL over the standing stream").action(
|
|
762
|
+
run(async (slug) => {
|
|
763
|
+
console.log(`Chatting with ${slug} \u2014 Ctrl-C to leave.`);
|
|
764
|
+
const controller = new AbortController();
|
|
765
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout, prompt: "> " });
|
|
766
|
+
void (async () => {
|
|
767
|
+
try {
|
|
768
|
+
for await (const frame of streamEvents(slug, controller.signal)) {
|
|
769
|
+
renderEvent(frame.event, frame.data);
|
|
770
|
+
if (frame.event === "turn.status_idle") rl.prompt();
|
|
771
|
+
}
|
|
772
|
+
} catch (error) {
|
|
773
|
+
fail(error);
|
|
774
|
+
}
|
|
775
|
+
})();
|
|
776
|
+
rl.prompt();
|
|
777
|
+
rl.on("line", (line) => {
|
|
778
|
+
const text = line.trim();
|
|
779
|
+
if (!text) {
|
|
780
|
+
rl.prompt();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
void api("POST", `/api/v1/agents/${encodeURIComponent(slug)}/messages`, {
|
|
784
|
+
parts: [{ type: "text", index: 0, text }]
|
|
785
|
+
}).catch((error) => {
|
|
786
|
+
console.error(`sky: ${error instanceof Error ? error.message : String(error)}`);
|
|
787
|
+
rl.prompt();
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
rl.on("close", () => {
|
|
791
|
+
controller.abort();
|
|
792
|
+
process.exit(0);
|
|
793
|
+
});
|
|
794
|
+
await new Promise(() => {
|
|
795
|
+
});
|
|
796
|
+
})
|
|
797
|
+
);
|
|
798
|
+
agent.command("interrupt <slug>").description("abort the in-flight turn").action(
|
|
799
|
+
run(async (slug) => {
|
|
800
|
+
const res = await api(
|
|
801
|
+
"POST",
|
|
802
|
+
`/api/v1/agents/${encodeURIComponent(slug)}/interrupt`
|
|
803
|
+
);
|
|
804
|
+
console.log(res.status === "no_turn" ? "nothing in flight" : `interrupting ${res.turnId}`);
|
|
805
|
+
})
|
|
806
|
+
);
|
|
807
|
+
var secrets = program.command("secrets").description("agent secrets (values write-only)");
|
|
808
|
+
secrets.command("set <slug> <pairs...>").description("set KEY=VALUE pairs on the agent").action(
|
|
809
|
+
run(async (slug, pairs) => {
|
|
810
|
+
for (const pair of pairs) {
|
|
811
|
+
const eq = pair.indexOf("=");
|
|
812
|
+
if (eq < 1) throw new Error(`expected KEY=VALUE, got ${pair}`);
|
|
813
|
+
const key = pair.slice(0, eq);
|
|
814
|
+
await api("PUT", `/api/v1/agents/${encodeURIComponent(slug)}/secrets/${encodeURIComponent(key)}`, {
|
|
815
|
+
value: pair.slice(eq + 1)
|
|
816
|
+
});
|
|
817
|
+
console.log(`set ${key}`);
|
|
818
|
+
}
|
|
819
|
+
})
|
|
820
|
+
);
|
|
821
|
+
secrets.command("list <slug>").action(
|
|
822
|
+
run(async (slug) => {
|
|
823
|
+
const { secrets: keys } = await api("GET", `/api/v1/agents/${encodeURIComponent(slug)}/secrets`);
|
|
824
|
+
if (!keys.length) console.log("no secrets set");
|
|
825
|
+
for (const s of keys) {
|
|
826
|
+
console.log(`\u25CF ${s.key}${s.description ? ` \u2014 ${s.description}` : ""}`);
|
|
827
|
+
}
|
|
828
|
+
})
|
|
829
|
+
);
|
|
830
|
+
secrets.command("unset <slug> <key>").action(
|
|
831
|
+
run(async (slug, key) => {
|
|
832
|
+
await api(
|
|
833
|
+
"DELETE",
|
|
834
|
+
`/api/v1/agents/${encodeURIComponent(slug)}/secrets/${encodeURIComponent(key)}`
|
|
835
|
+
);
|
|
836
|
+
console.log(`unset ${key}`);
|
|
837
|
+
})
|
|
838
|
+
);
|
|
839
|
+
program.command("launch").description("scaffold ./agent.toml if absent, create the agent, print its address").option("-f, --file <path>", "agent file", "agent.toml").action(
|
|
840
|
+
run(async (opts) => {
|
|
841
|
+
const path = resolve3(opts.file);
|
|
842
|
+
if (!existsSync3(path)) {
|
|
843
|
+
const name = basename2(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
|
|
844
|
+
scaffoldAgentFile(path, name);
|
|
845
|
+
console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
|
|
846
|
+
}
|
|
847
|
+
const { file, spec } = readAgentFile(path);
|
|
848
|
+
if (file.slug) {
|
|
849
|
+
console.log(`${file.slug} already launched \u2014 use \`sky deploy\` to re-apply.`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const { agent: created } = await api("POST", "/api/v1/agents", spec);
|
|
853
|
+
stampSlug(path, created.slug);
|
|
854
|
+
console.log(`Launched ${created.slug} (${created.agentType} \xB7 ${created.llm})`);
|
|
855
|
+
console.log(` sky agent message ${created.slug} "hello"`);
|
|
856
|
+
console.log(` sky agent chat ${created.slug}`);
|
|
857
|
+
})
|
|
858
|
+
);
|
|
859
|
+
program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent)").option("-f, --file <path>", "agent file", "agent.toml").action(
|
|
860
|
+
run(async (opts) => {
|
|
861
|
+
const path = resolve3(opts.file);
|
|
862
|
+
const { file, spec } = readAgentFile(path);
|
|
863
|
+
if (!file.slug) {
|
|
864
|
+
console.log("No slug in the file yet \u2014 running launch instead.");
|
|
865
|
+
const { agent: created } = await api("POST", "/api/v1/agents", spec);
|
|
866
|
+
stampSlug(path, created.slug);
|
|
867
|
+
console.log(`Launched ${created.slug}`);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (spec.prompt !== void 0) {
|
|
871
|
+
await api("PUT", `/api/v1/agents/${encodeURIComponent(file.slug)}/prompt`, {
|
|
872
|
+
prompt: spec.prompt
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
await api("PATCH", `/api/v1/agents/${encodeURIComponent(file.slug)}`, {
|
|
876
|
+
displayName: spec.displayName,
|
|
877
|
+
...spec.capabilities ? { capabilities: spec.capabilities } : {}
|
|
878
|
+
});
|
|
879
|
+
console.log(`Deployed ${file.slug} (prompt + settings re-applied)`);
|
|
880
|
+
})
|
|
881
|
+
);
|
|
882
|
+
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(
|
|
883
|
+
run(
|
|
884
|
+
async (opts) => clone(opts)
|
|
885
|
+
)
|
|
886
|
+
);
|
|
887
|
+
program.command("config").description("show the CLI config location and current values").action(() => {
|
|
888
|
+
const config = loadConfig();
|
|
889
|
+
console.log(`config: ${configPath()}`);
|
|
890
|
+
console.log(` apiBase: ${config.apiBase ?? "(default)"}`);
|
|
891
|
+
console.log(` token: ${config.token ? `${config.token.slice(0, 10)}\u2026` : "(none)"}`);
|
|
892
|
+
console.log(` universe: ${config.currentUniverse ?? "(personal)"}`);
|
|
893
|
+
});
|
|
894
|
+
program.parseAsync().catch(fail);
|