@agentsky/sky 2.10.0 → 2.12.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-7LUCHBX6.js +168 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-RF5JICNX.js +54 -0
- package/dist/{chunk-5QRKU3BR.js → chunk-XXDQKVPS.js} +2690 -2566
- package/dist/harness-connect-DUC7GIBL.js +102 -0
- package/dist/index.js +862 -328
- package/dist/products.d.ts +9 -0
- package/dist/products.js +2 -1
- package/dist/{src-RYECJN5I.js → src-S777KCS5.js} +21 -2
- package/dist/trace-UJTFSXNM.js +367 -0
- package/dist/trace-library-474QJTAZ.js +710 -0
- package/dist/worker-6Q4K4HZW.js +190 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ApiError,
|
|
4
|
+
api,
|
|
5
|
+
apiBytes,
|
|
6
|
+
apiForm,
|
|
7
|
+
downloadFile,
|
|
8
|
+
postEvents,
|
|
9
|
+
resolveSessionRef,
|
|
10
|
+
streamEvents,
|
|
11
|
+
uploadFile
|
|
12
|
+
} from "./chunk-7LUCHBX6.js";
|
|
13
|
+
import {
|
|
14
|
+
CLI_VERSION,
|
|
15
|
+
USER_AGENT,
|
|
16
|
+
apiBase,
|
|
17
|
+
apiToken,
|
|
18
|
+
configPath,
|
|
19
|
+
consoleBase,
|
|
20
|
+
currentUniverse,
|
|
21
|
+
loadConfig,
|
|
22
|
+
saveConfig
|
|
23
|
+
} from "./chunk-RF5JICNX.js";
|
|
2
24
|
import {
|
|
3
25
|
AGENT_TYPES,
|
|
4
26
|
MODEL_SUBSCRIPTION_PLAN_PHRASE,
|
|
@@ -8,6 +30,7 @@ import {
|
|
|
8
30
|
customDataSchema,
|
|
9
31
|
defaultLlm,
|
|
10
32
|
external_exports,
|
|
33
|
+
harnessProfileSchema,
|
|
11
34
|
isModelEffortSupported,
|
|
12
35
|
mcpServersFromKept,
|
|
13
36
|
modelSubscriptionProviderFor,
|
|
@@ -16,200 +39,17 @@ import {
|
|
|
16
39
|
skillRefSchema,
|
|
17
40
|
splitMcpServers,
|
|
18
41
|
supersededBy
|
|
19
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-XXDQKVPS.js";
|
|
43
|
+
import "./chunk-MLKGABMK.js";
|
|
20
44
|
|
|
21
45
|
// src/index.ts
|
|
22
|
-
import { existsSync as
|
|
23
|
-
import { basename as
|
|
46
|
+
import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
|
|
47
|
+
import { basename as basename4, resolve as resolve6 } from "path";
|
|
24
48
|
import { createInterface as createInterface4 } from "readline";
|
|
25
49
|
import { Command } from "commander";
|
|
26
50
|
|
|
27
|
-
// src/config.ts
|
|
28
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
29
|
-
import { homedir } from "os";
|
|
30
|
-
import { dirname, join } from "path";
|
|
31
|
-
var DEFAULT_API_BASE = "https://api.agentsky.dev/v1";
|
|
32
|
-
var DEFAULT_CONSOLE_BASE = "https://platform.agentsky.dev";
|
|
33
|
-
function configPath() {
|
|
34
|
-
return join(process.env.SKY_CONFIG_DIR ?? join(homedir(), ".config", "sky"), "config.json");
|
|
35
|
-
}
|
|
36
|
-
function loadConfig() {
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(readFileSync(configPath(), "utf8"));
|
|
39
|
-
} catch {
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
function saveConfig(config) {
|
|
44
|
-
const path = configPath();
|
|
45
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
46
|
-
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
47
|
-
`, { mode: 384 });
|
|
48
|
-
}
|
|
49
|
-
function apiBase() {
|
|
50
|
-
return (process.env.SKY_API_URL ?? loadConfig().apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
51
|
-
}
|
|
52
|
-
function consoleBase() {
|
|
53
|
-
const configured = process.env.SKY_CONSOLE_URL ?? loadConfig().consoleBase;
|
|
54
|
-
if (configured) return configured.replace(/\/$/, "");
|
|
55
|
-
const api2 = apiBase();
|
|
56
|
-
if (api2 === DEFAULT_API_BASE) return DEFAULT_CONSOLE_BASE;
|
|
57
|
-
return api2.replace(/\/(?:api\/)?v1$/, "") || api2;
|
|
58
|
-
}
|
|
59
|
-
function apiToken() {
|
|
60
|
-
return process.env.SKY_API_TOKEN ?? loadConfig().token;
|
|
61
|
-
}
|
|
62
|
-
function currentUniverse() {
|
|
63
|
-
return process.env.SKY_WORKSPACE ?? process.env.SKY_UNIVERSE ?? loadConfig().currentUniverse;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// src/version.ts
|
|
67
|
-
var CLI_VERSION = "2.10.0";
|
|
68
|
-
var USER_AGENT = `sky-cli/${CLI_VERSION} (${process.platform}; node/${process.versions.node})`;
|
|
69
|
-
|
|
70
|
-
// src/api.ts
|
|
71
|
-
var ApiError = class extends Error {
|
|
72
|
-
constructor(status, code, message2) {
|
|
73
|
-
super(message2);
|
|
74
|
-
this.status = status;
|
|
75
|
-
this.code = code;
|
|
76
|
-
}
|
|
77
|
-
status;
|
|
78
|
-
code;
|
|
79
|
-
};
|
|
80
|
-
function authHeaders() {
|
|
81
|
-
const token = apiToken();
|
|
82
|
-
if (!token) {
|
|
83
|
-
throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
|
|
84
|
-
}
|
|
85
|
-
const headers = { authorization: `Bearer ${token}` };
|
|
86
|
-
const universe2 = currentUniverse();
|
|
87
|
-
if (universe2) headers["x-universe"] = universe2;
|
|
88
|
-
return headers;
|
|
89
|
-
}
|
|
90
|
-
async function parseError(res) {
|
|
91
|
-
let code = "http_error";
|
|
92
|
-
let message2 = `${res.status} ${res.statusText}`;
|
|
93
|
-
try {
|
|
94
|
-
const body = await res.json();
|
|
95
|
-
if (body.error?.message) {
|
|
96
|
-
code = body.error.code ?? code;
|
|
97
|
-
message2 = body.error.message;
|
|
98
|
-
}
|
|
99
|
-
} catch {
|
|
100
|
-
}
|
|
101
|
-
throw new ApiError(res.status, code, message2);
|
|
102
|
-
}
|
|
103
|
-
async function api(method, path, body, opts = {}) {
|
|
104
|
-
const res = await fetch(`${apiBase()}${path}`, {
|
|
105
|
-
method,
|
|
106
|
-
headers: {
|
|
107
|
-
"user-agent": USER_AGENT,
|
|
108
|
-
...opts.auth === false ? {} : authHeaders(),
|
|
109
|
-
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
110
|
-
...opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : {}
|
|
111
|
-
},
|
|
112
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
113
|
-
});
|
|
114
|
-
if (!res.ok) await parseError(res);
|
|
115
|
-
if (res.status === 204) return void 0;
|
|
116
|
-
return await res.json();
|
|
117
|
-
}
|
|
118
|
-
async function postEvents(sessionId, events, idempotencyKey) {
|
|
119
|
-
const res = await fetch(`${apiBase()}/sessions/${encodeURIComponent(sessionId)}/events`, {
|
|
120
|
-
method: "POST",
|
|
121
|
-
headers: {
|
|
122
|
-
"user-agent": USER_AGENT,
|
|
123
|
-
...authHeaders(),
|
|
124
|
-
"content-type": "application/json",
|
|
125
|
-
...idempotencyKey ? { "idempotency-key": idempotencyKey } : {}
|
|
126
|
-
},
|
|
127
|
-
body: JSON.stringify({ events })
|
|
128
|
-
});
|
|
129
|
-
if (!res.ok) await parseError(res);
|
|
130
|
-
return await res.json();
|
|
131
|
-
}
|
|
132
|
-
async function uploadFile(bytes, filename, mediaType, expiresInSeconds) {
|
|
133
|
-
const form = new FormData();
|
|
134
|
-
form.append("file", new Blob([new Uint8Array(bytes)], { type: mediaType }), filename);
|
|
135
|
-
if (expiresInSeconds) form.append("expires_in_seconds", String(expiresInSeconds));
|
|
136
|
-
const res = await fetch(`${apiBase()}/files`, {
|
|
137
|
-
method: "POST",
|
|
138
|
-
headers: { "user-agent": USER_AGENT, ...authHeaders() },
|
|
139
|
-
body: form
|
|
140
|
-
});
|
|
141
|
-
if (!res.ok) await parseError(res);
|
|
142
|
-
return await res.json();
|
|
143
|
-
}
|
|
144
|
-
async function downloadFile(fileId) {
|
|
145
|
-
const res = await fetch(`${apiBase()}/files/${encodeURIComponent(fileId)}/content`, {
|
|
146
|
-
headers: { "user-agent": USER_AGENT, ...authHeaders() }
|
|
147
|
-
});
|
|
148
|
-
if (!res.ok) await parseError(res);
|
|
149
|
-
return { bytes: new Uint8Array(await res.arrayBuffer()), mediaType: res.headers.get("content-type") ?? "application/octet-stream" };
|
|
150
|
-
}
|
|
151
|
-
async function apiBytes(method, path, body) {
|
|
152
|
-
const res = await fetch(`${apiBase()}${path}`, {
|
|
153
|
-
method,
|
|
154
|
-
headers: {
|
|
155
|
-
"user-agent": USER_AGENT,
|
|
156
|
-
...authHeaders(),
|
|
157
|
-
...body ? { "content-type": body.mediaType } : {}
|
|
158
|
-
},
|
|
159
|
-
body: body ? new Uint8Array(body.bytes) : void 0
|
|
160
|
-
});
|
|
161
|
-
if (!res.ok) await parseError(res);
|
|
162
|
-
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
163
|
-
return {
|
|
164
|
-
bytes,
|
|
165
|
-
mediaType: res.headers.get("content-type") ?? "application/octet-stream",
|
|
166
|
-
json: () => JSON.parse(new TextDecoder().decode(bytes))
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
async function resolveSessionRef(ref) {
|
|
170
|
-
if (ref.startsWith("sess-")) return ref;
|
|
171
|
-
const { agent: agent2 } = await api(
|
|
172
|
-
"GET",
|
|
173
|
-
`/agents/${encodeURIComponent(ref)}`
|
|
174
|
-
).catch(async () => {
|
|
175
|
-
await api("GET", `/sessions/${encodeURIComponent(ref)}`);
|
|
176
|
-
return { agent: { id: ref, defaultSessionId: ref } };
|
|
177
|
-
});
|
|
178
|
-
if (!agent2.defaultSessionId) {
|
|
179
|
-
throw new ApiError(
|
|
180
|
-
404,
|
|
181
|
-
"no_sessions",
|
|
182
|
-
`agent ${agent2.id} has no sessions \u2014 create one: sky session create ${agent2.id}`
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
return agent2.defaultSessionId;
|
|
186
|
-
}
|
|
187
|
-
async function* streamEvents(sessionId, signal) {
|
|
188
|
-
const { parseSseStream } = await import("./src-RYECJN5I.js");
|
|
189
|
-
const res = await fetch(
|
|
190
|
-
`${apiBase()}/sessions/${encodeURIComponent(sessionId)}/stream`,
|
|
191
|
-
{
|
|
192
|
-
headers: { "user-agent": USER_AGENT, ...authHeaders(), accept: "text/event-stream" },
|
|
193
|
-
signal
|
|
194
|
-
}
|
|
195
|
-
);
|
|
196
|
-
if (!res.ok) await parseError(res);
|
|
197
|
-
if (!res.body) throw new ApiError(502, "no_body", "stream returned no body");
|
|
198
|
-
try {
|
|
199
|
-
for await (const frame of parseSseStream(res.body)) {
|
|
200
|
-
if (!frame.data) continue;
|
|
201
|
-
try {
|
|
202
|
-
yield { id: frame.id, event: frame.event, data: JSON.parse(frame.data) };
|
|
203
|
-
} catch {
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
} catch (error) {
|
|
207
|
-
if (!(error instanceof DOMException && error.name === "AbortError")) throw error;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
51
|
// src/files.ts
|
|
212
|
-
import { readFileSync
|
|
52
|
+
import { readFileSync } from "fs";
|
|
213
53
|
import { basename, extname } from "path";
|
|
214
54
|
var MEDIA_TYPES = {
|
|
215
55
|
".png": "image/png",
|
|
@@ -231,7 +71,7 @@ function mediaTypeFor(path) {
|
|
|
231
71
|
return MEDIA_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
232
72
|
}
|
|
233
73
|
async function uploadLocalFile(path, expiresInSeconds) {
|
|
234
|
-
const bytes =
|
|
74
|
+
const bytes = readFileSync(path);
|
|
235
75
|
return uploadFile(new Uint8Array(bytes), basename(path), mediaTypeFor(path), expiresInSeconds);
|
|
236
76
|
}
|
|
237
77
|
function fileBlock(file) {
|
|
@@ -328,8 +168,8 @@ function renderGoal(goal2) {
|
|
|
328
168
|
}
|
|
329
169
|
|
|
330
170
|
// src/agent-file.ts
|
|
331
|
-
import { existsSync, readFileSync as
|
|
332
|
-
import { dirname
|
|
171
|
+
import { existsSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
172
|
+
import { dirname, resolve } from "path";
|
|
333
173
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
334
174
|
var fileInstructionSchema = external_exports.object({
|
|
335
175
|
name: external_exports.string(),
|
|
@@ -352,12 +192,16 @@ var agentFileSchema = external_exports.object({
|
|
|
352
192
|
instructions: external_exports.array(fileInstructionSchema).optional(),
|
|
353
193
|
skills: external_exports.array(skillRefSchema).optional(),
|
|
354
194
|
customData: external_exports.array(customDataSchema).optional(),
|
|
195
|
+
/** "deny" makes the agent work alone: its sessions may not hire helpers. */
|
|
196
|
+
hiring_policy: external_exports.enum(["allow", "deny"]).optional(),
|
|
197
|
+
/** The environment its sessions boot in when they name none. */
|
|
198
|
+
environment: external_exports.string().min(1).optional(),
|
|
355
199
|
/** Advanced machine shape (create-only): 1|2|4|8 vCPUs, 256MB-step memory. */
|
|
356
200
|
vcpus: external_exports.number().int().optional(),
|
|
357
201
|
memory_mb: external_exports.number().int().optional()
|
|
358
202
|
});
|
|
359
203
|
function readAgentFile(path) {
|
|
360
|
-
const raw =
|
|
204
|
+
const raw = readFileSync2(path, "utf8");
|
|
361
205
|
const parsed = agentFileSchema.safeParse(parseToml(raw));
|
|
362
206
|
if (!parsed.success) {
|
|
363
207
|
const issue = parsed.error.issues[0];
|
|
@@ -369,11 +213,11 @@ function readAgentFile(path) {
|
|
|
369
213
|
`${path}: reasoning_effort '${file.reasoning_effort}' is not available for ${file.type} \u2014 ${reasoningEffortDocLine()}`
|
|
370
214
|
);
|
|
371
215
|
}
|
|
372
|
-
const base =
|
|
216
|
+
const base = dirname(resolve(path));
|
|
373
217
|
const inline = (ref, inlineContent) => {
|
|
374
218
|
if (inlineContent !== void 0) return inlineContent;
|
|
375
219
|
if (!ref) return void 0;
|
|
376
|
-
return
|
|
220
|
+
return readFileSync2(resolve(base, ref), "utf8");
|
|
377
221
|
};
|
|
378
222
|
const spec = {
|
|
379
223
|
name: file.name,
|
|
@@ -391,20 +235,22 @@ function readAgentFile(path) {
|
|
|
391
235
|
}),
|
|
392
236
|
skills: file.skills,
|
|
393
237
|
customData: file.customData,
|
|
238
|
+
hiringPolicy: file.hiring_policy,
|
|
239
|
+
environmentId: file.environment,
|
|
394
240
|
vcpus: file.vcpus,
|
|
395
241
|
memoryMb: file.memory_mb
|
|
396
242
|
};
|
|
397
243
|
return { file, spec };
|
|
398
244
|
}
|
|
399
245
|
function stampId(path, id) {
|
|
400
|
-
const doc = parseToml(
|
|
246
|
+
const doc = parseToml(readFileSync2(path, "utf8"));
|
|
401
247
|
doc.id = id;
|
|
402
248
|
delete doc.slug;
|
|
403
|
-
|
|
249
|
+
writeFileSync(path, stringifyToml(doc));
|
|
404
250
|
}
|
|
405
251
|
function scaffoldAgentFile(path, name) {
|
|
406
252
|
if (existsSync(path)) throw new Error(`${path} already exists`);
|
|
407
|
-
|
|
253
|
+
writeFileSync(
|
|
408
254
|
path,
|
|
409
255
|
`# agent.toml \u2014 everything the create wizard can set. Only \`name\` is required.
|
|
410
256
|
name = "${name}"
|
|
@@ -412,6 +258,11 @@ name = "${name}"
|
|
|
412
258
|
# llm = "deepseek-flash" # must be compatible with type
|
|
413
259
|
# reasoning_effort = "high" # per harness \u2014 ${reasoningEffortDocLine()}
|
|
414
260
|
# capabilities = []
|
|
261
|
+
# hiring_policy = "deny" # its sessions work alone; "allow" (default) lets them hire helpers
|
|
262
|
+
# environment = "env_..." # the environment its sessions boot in when they name none
|
|
263
|
+
# [[skills]] # a skill from \`sky skills upload <dir>\`
|
|
264
|
+
# type = "skill"
|
|
265
|
+
# skillId = "skill_..."
|
|
415
266
|
|
|
416
267
|
prompt = """
|
|
417
268
|
You are ${name}, a helpful long-lived agent.
|
|
@@ -485,18 +336,18 @@ function renderCatalogAgent(agent2) {
|
|
|
485
336
|
|
|
486
337
|
// src/clone.ts
|
|
487
338
|
import { spawn } from "child_process";
|
|
488
|
-
import { existsSync as existsSync2, readFileSync as
|
|
489
|
-
import { homedir
|
|
490
|
-
import { basename as basename2, join
|
|
339
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
340
|
+
import { homedir } from "os";
|
|
341
|
+
import { basename as basename2, join } from "path";
|
|
491
342
|
import { createInterface } from "readline";
|
|
492
343
|
var PROMPT_CAP = 1e5;
|
|
493
344
|
var HARNESSES = ["claude_code", "codex", "openclaw"];
|
|
494
345
|
function claudeConfigRoot() {
|
|
495
|
-
return process.env.CLAUDE_CONFIG_DIR ??
|
|
346
|
+
return process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
496
347
|
}
|
|
497
348
|
function readIfExists(path) {
|
|
498
349
|
try {
|
|
499
|
-
return
|
|
350
|
+
return readFileSync3(path, "utf8");
|
|
500
351
|
} catch {
|
|
501
352
|
return void 0;
|
|
502
353
|
}
|
|
@@ -512,9 +363,9 @@ function readJson(path) {
|
|
|
512
363
|
}
|
|
513
364
|
function claudeModel(configRoot) {
|
|
514
365
|
const candidates = [
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
366
|
+
join(process.cwd(), ".claude", "settings.local.json"),
|
|
367
|
+
join(process.cwd(), ".claude", "settings.json"),
|
|
368
|
+
join(configRoot, "settings.json")
|
|
518
369
|
];
|
|
519
370
|
for (const path of candidates) {
|
|
520
371
|
const model = readJson(path)?.model;
|
|
@@ -524,11 +375,11 @@ function claudeModel(configRoot) {
|
|
|
524
375
|
}
|
|
525
376
|
function claudeMcpServers() {
|
|
526
377
|
const merged = {};
|
|
527
|
-
const userConfig = readJson(
|
|
378
|
+
const userConfig = readJson(join(homedir(), ".claude.json"));
|
|
528
379
|
Object.assign(merged, userConfig?.mcpServers ?? {});
|
|
529
380
|
Object.assign(merged, userConfig?.projects?.[process.cwd()]?.mcpServers ?? {});
|
|
530
381
|
const projectConfig = readJson(
|
|
531
|
-
|
|
382
|
+
join(process.cwd(), ".mcp.json")
|
|
532
383
|
);
|
|
533
384
|
Object.assign(merged, projectConfig?.mcpServers ?? {});
|
|
534
385
|
return merged;
|
|
@@ -545,13 +396,13 @@ function readClaudeCode() {
|
|
|
545
396
|
const configRoot = claudeConfigRoot();
|
|
546
397
|
const parts = [];
|
|
547
398
|
const sources = [];
|
|
548
|
-
const global = readIfExists(
|
|
399
|
+
const global = readIfExists(join(configRoot, "CLAUDE.md"));
|
|
549
400
|
if (global?.trim()) {
|
|
550
401
|
parts.push({ label: "Global CLAUDE.md", content: global });
|
|
551
402
|
sources.push("global CLAUDE.md");
|
|
552
403
|
}
|
|
553
404
|
for (const name of ["CLAUDE.md", "AGENTS.md"]) {
|
|
554
|
-
const project = readIfExists(
|
|
405
|
+
const project = readIfExists(join(process.cwd(), name));
|
|
555
406
|
if (project?.trim()) {
|
|
556
407
|
parts.push({ label: `Project ${name}`, content: project });
|
|
557
408
|
sources.push(`project ${name}`);
|
|
@@ -592,27 +443,27 @@ function detectHarness(explicit) {
|
|
|
592
443
|
return explicit;
|
|
593
444
|
}
|
|
594
445
|
if (existsSync2(claudeConfigRoot())) return "claude_code";
|
|
595
|
-
if (existsSync2(
|
|
596
|
-
if (existsSync2(
|
|
446
|
+
if (existsSync2(join(homedir(), ".codex"))) return "codex";
|
|
447
|
+
if (existsSync2(join(homedir(), ".openclaw"))) return "openclaw";
|
|
597
448
|
throw new Error(
|
|
598
449
|
"no local harness found (~/.claude, ~/.codex, ~/.openclaw) \u2014 pass --harness to override"
|
|
599
450
|
);
|
|
600
451
|
}
|
|
601
|
-
function readLocal(
|
|
602
|
-
if (
|
|
452
|
+
function readLocal(harness2) {
|
|
453
|
+
if (harness2 === "claude_code") return readClaudeCode();
|
|
603
454
|
throw new Error(
|
|
604
|
-
`${
|
|
455
|
+
`${harness2} cloning lands next \u2014 claude_code is the first supported harness`
|
|
605
456
|
);
|
|
606
457
|
}
|
|
607
458
|
function linkPath(configRoot) {
|
|
608
|
-
return
|
|
459
|
+
return join(configRoot, "agentsky-link.json");
|
|
609
460
|
}
|
|
610
461
|
function readLink(configRoot) {
|
|
611
462
|
const link = readJson(linkPath(configRoot));
|
|
612
463
|
return link?.agentId ? link : void 0;
|
|
613
464
|
}
|
|
614
465
|
function writeLink(configRoot, link) {
|
|
615
|
-
|
|
466
|
+
writeFileSync2(linkPath(configRoot), `${JSON.stringify(link, null, 2)}
|
|
616
467
|
`);
|
|
617
468
|
}
|
|
618
469
|
var DEFAULT_VAULT_NAME = "default";
|
|
@@ -843,17 +694,17 @@ async function syncClone(local, link, opts) {
|
|
|
843
694
|
if (opts.open) openBrowser(`${apiBase()}/agents/${link.agentId}`);
|
|
844
695
|
}
|
|
845
696
|
async function clone(opts) {
|
|
846
|
-
const
|
|
847
|
-
const local = readLocal(
|
|
697
|
+
const harness2 = detectHarness(opts.harness);
|
|
698
|
+
const local = readLocal(harness2);
|
|
848
699
|
const link = opts.new ? void 0 : readLink(local.configRoot);
|
|
849
700
|
if (link) await syncClone(local, link, opts);
|
|
850
701
|
else await createClone(local, opts);
|
|
851
702
|
}
|
|
852
703
|
|
|
853
704
|
// src/import-local.ts
|
|
854
|
-
import { closeSync, existsSync as existsSync3, openSync, readdirSync, readFileSync as
|
|
855
|
-
import { homedir as
|
|
856
|
-
import { join as
|
|
705
|
+
import { closeSync, existsSync as existsSync3, openSync, readdirSync, readFileSync as readFileSync4, readSync, statSync } from "fs";
|
|
706
|
+
import { homedir as homedir2 } from "os";
|
|
707
|
+
import { join as join2 } from "path";
|
|
857
708
|
import { createHash } from "crypto";
|
|
858
709
|
function redactLine2(raw) {
|
|
859
710
|
return redactLine(raw).line;
|
|
@@ -926,13 +777,13 @@ function sniffTitle(filePath) {
|
|
|
926
777
|
}
|
|
927
778
|
}
|
|
928
779
|
function listClaudeCodeSessions(limit) {
|
|
929
|
-
const root = process.env.CLAUDE_CONFIG_DIR ??
|
|
930
|
-
const projectsDir =
|
|
780
|
+
const root = process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
|
|
781
|
+
const projectsDir = join2(root, "projects");
|
|
931
782
|
if (!existsSync3(projectsDir)) return [];
|
|
932
783
|
const sessions = [];
|
|
933
784
|
try {
|
|
934
785
|
for (const encoded of readdirSync(projectsDir)) {
|
|
935
|
-
const projectDir =
|
|
786
|
+
const projectDir = join2(projectsDir, encoded);
|
|
936
787
|
let entries;
|
|
937
788
|
try {
|
|
938
789
|
entries = readdirSync(projectDir);
|
|
@@ -943,7 +794,7 @@ function listClaudeCodeSessions(limit) {
|
|
|
943
794
|
for (const entry2 of entries) {
|
|
944
795
|
if (!entry2.endsWith(".jsonl")) continue;
|
|
945
796
|
const sessionId = entry2.slice(0, -6);
|
|
946
|
-
const mainFile =
|
|
797
|
+
const mainFile = join2(projectDir, entry2);
|
|
947
798
|
let stat;
|
|
948
799
|
try {
|
|
949
800
|
stat = statSync(mainFile);
|
|
@@ -953,12 +804,12 @@ function listClaudeCodeSessions(limit) {
|
|
|
953
804
|
if (!stat.isFile()) continue;
|
|
954
805
|
let discoveryError;
|
|
955
806
|
const subagentFiles = [];
|
|
956
|
-
const subagentDir =
|
|
807
|
+
const subagentDir = join2(projectDir, sessionId, "subagents");
|
|
957
808
|
if (existsSync3(subagentDir)) {
|
|
958
809
|
try {
|
|
959
810
|
const subFiles = readdirSync(subagentDir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
960
811
|
for (const f of subFiles) {
|
|
961
|
-
subagentFiles.push(
|
|
812
|
+
subagentFiles.push(join2(subagentDir, f));
|
|
962
813
|
}
|
|
963
814
|
} catch {
|
|
964
815
|
discoveryError = "Cannot read all recorded subagent conversations. Fix local file access before importing this session.";
|
|
@@ -980,7 +831,7 @@ function listClaudeCodeSessions(limit) {
|
|
|
980
831
|
return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
981
832
|
}
|
|
982
833
|
function listCodexSessions(limit) {
|
|
983
|
-
const sessionsDir =
|
|
834
|
+
const sessionsDir = join2(process.env.CODEX_HOME ?? process.env.CODEX_CONFIG_DIR ?? join2(homedir2(), ".codex"), "sessions");
|
|
984
835
|
const records = [];
|
|
985
836
|
function walk(dir) {
|
|
986
837
|
let entries;
|
|
@@ -990,7 +841,7 @@ function listCodexSessions(limit) {
|
|
|
990
841
|
return;
|
|
991
842
|
}
|
|
992
843
|
for (const entry2 of entries) {
|
|
993
|
-
const full =
|
|
844
|
+
const full = join2(dir, entry2.name);
|
|
994
845
|
if (entry2.isDirectory()) {
|
|
995
846
|
walk(full);
|
|
996
847
|
continue;
|
|
@@ -1032,9 +883,9 @@ function listCodexSessions(limit) {
|
|
|
1032
883
|
files: [.../* @__PURE__ */ new Set([...session2.files, ...descendants(session2.sessionId, /* @__PURE__ */ new Set())])]
|
|
1033
884
|
})).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
1034
885
|
}
|
|
1035
|
-
function listRecentSessions(
|
|
1036
|
-
const claudeSessions =
|
|
1037
|
-
const codexSessions =
|
|
886
|
+
function listRecentSessions(harness2, limit) {
|
|
887
|
+
const claudeSessions = harness2 === "codex" ? [] : listClaudeCodeSessions(limit);
|
|
888
|
+
const codexSessions = harness2 === "claude_code" ? [] : listCodexSessions(limit);
|
|
1038
889
|
return [...claudeSessions, ...codexSessions].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
1039
890
|
}
|
|
1040
891
|
function agentUseBase() {
|
|
@@ -1052,7 +903,7 @@ function currentSessionEnv() {
|
|
|
1052
903
|
if (codexId) return { harness: "codex", sessionId: codexId };
|
|
1053
904
|
return void 0;
|
|
1054
905
|
}
|
|
1055
|
-
async function mintTrace(base,
|
|
906
|
+
async function mintTrace(base, harness2) {
|
|
1056
907
|
const token = apiToken();
|
|
1057
908
|
if (!token) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
|
|
1058
909
|
const res = await fetch(`${base}/api/traces`, {
|
|
@@ -1064,7 +915,7 @@ async function mintTrace(base, harness) {
|
|
|
1064
915
|
authorization: `Bearer ${token}`,
|
|
1065
916
|
"user-agent": USER_AGENT
|
|
1066
917
|
},
|
|
1067
|
-
body: JSON.stringify({ harness })
|
|
918
|
+
body: JSON.stringify({ harness: harness2 })
|
|
1068
919
|
});
|
|
1069
920
|
if (!res.ok) {
|
|
1070
921
|
const body = await res.json().catch(() => null);
|
|
@@ -1077,7 +928,7 @@ async function mintTrace(base, harness) {
|
|
|
1077
928
|
function loadSummaryFile(summaryPath) {
|
|
1078
929
|
let raw;
|
|
1079
930
|
try {
|
|
1080
|
-
raw =
|
|
931
|
+
raw = readFileSync4(summaryPath, "utf8");
|
|
1081
932
|
} catch {
|
|
1082
933
|
throw new Error(`Cannot read summary file: ${summaryPath}`);
|
|
1083
934
|
}
|
|
@@ -1109,7 +960,7 @@ function prepareImport(session2, summaryPath) {
|
|
|
1109
960
|
for (const path of session2.files) {
|
|
1110
961
|
const stat = statSync(path);
|
|
1111
962
|
if (!stat.isFile() || (inputBytes += stat.size) > maximum) throw new Error("Selected transcript exceeds the 16 MiB limit or is not a regular file.");
|
|
1112
|
-
const text =
|
|
963
|
+
const text = readFileSync4(path, "utf8");
|
|
1113
964
|
for (const raw of text.split("\n")) {
|
|
1114
965
|
if (!raw.trim()) continue;
|
|
1115
966
|
let record;
|
|
@@ -1254,7 +1105,7 @@ async function login(opts) {
|
|
|
1254
1105
|
}
|
|
1255
1106
|
async function browserLogin(verifier, challenge) {
|
|
1256
1107
|
const state = b64url(randomBytes(16));
|
|
1257
|
-
const code = await new Promise((
|
|
1108
|
+
const code = await new Promise((resolve7, reject) => {
|
|
1258
1109
|
const server = createServer((req, res) => {
|
|
1259
1110
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1260
1111
|
if (url.pathname !== "/callback") {
|
|
@@ -1267,7 +1118,7 @@ async function browserLogin(verifier, challenge) {
|
|
|
1267
1118
|
if (url.searchParams.get("state") !== state || !got) {
|
|
1268
1119
|
reject(new Error("login callback state mismatch"));
|
|
1269
1120
|
} else {
|
|
1270
|
-
|
|
1121
|
+
resolve7(got);
|
|
1271
1122
|
}
|
|
1272
1123
|
server.close();
|
|
1273
1124
|
});
|
|
@@ -1322,22 +1173,22 @@ async function headlessLogin(verifier, challenge) {
|
|
|
1322
1173
|
|
|
1323
1174
|
// src/local-agents.ts
|
|
1324
1175
|
import { existsSync as existsSync4 } from "fs";
|
|
1325
|
-
import { homedir as
|
|
1326
|
-
import { join as
|
|
1327
|
-
function localAgents(env = process.env, home =
|
|
1328
|
-
const claude = env.CLAUDE_CONFIG_DIR ||
|
|
1329
|
-
const codex = env.CODEX_HOME ||
|
|
1330
|
-
const cursor =
|
|
1176
|
+
import { homedir as homedir3 } from "os";
|
|
1177
|
+
import { join as join3 } from "path";
|
|
1178
|
+
function localAgents(env = process.env, home = homedir3()) {
|
|
1179
|
+
const claude = env.CLAUDE_CONFIG_DIR || join3(home, ".claude");
|
|
1180
|
+
const codex = env.CODEX_HOME || join3(home, ".codex");
|
|
1181
|
+
const cursor = join3(home, ".cursor");
|
|
1331
1182
|
return [
|
|
1332
|
-
{ id: "claude_code", name: "Claude Code", root: claude, skillsDir:
|
|
1333
|
-
{ id: "codex", name: "Codex", root: codex, skillsDir:
|
|
1334
|
-
{ id: "cursor", name: "Cursor", root: cursor, skillsDir:
|
|
1183
|
+
{ id: "claude_code", name: "Claude Code", root: claude, skillsDir: join3(claude, "skills") },
|
|
1184
|
+
{ id: "codex", name: "Codex", root: codex, skillsDir: join3(codex, "skills") },
|
|
1185
|
+
{ id: "cursor", name: "Cursor", root: cursor, skillsDir: join3(cursor, "skills") }
|
|
1335
1186
|
];
|
|
1336
1187
|
}
|
|
1337
|
-
function detectLocalAgents(env = process.env, home =
|
|
1188
|
+
function detectLocalAgents(env = process.env, home = homedir3()) {
|
|
1338
1189
|
return localAgents(env, home).filter((agent2) => existsSync4(agent2.root));
|
|
1339
1190
|
}
|
|
1340
|
-
function selectLocalAgents(ids, env = process.env, home =
|
|
1191
|
+
function selectLocalAgents(ids, env = process.env, home = homedir3()) {
|
|
1341
1192
|
if (!ids?.length) return detectLocalAgents(env, home);
|
|
1342
1193
|
const known = localAgents(env, home);
|
|
1343
1194
|
return ids.map((id) => {
|
|
@@ -1431,21 +1282,21 @@ async function readTurn(sessionId, opts = {}) {
|
|
|
1431
1282
|
for (; ; ) {
|
|
1432
1283
|
const reading = latestTurn(await history(sessionId));
|
|
1433
1284
|
if (reading.finished || !opts.wait || Date.now() >= deadline) return reading;
|
|
1434
|
-
await new Promise((
|
|
1285
|
+
await new Promise((resolve7) => setTimeout(resolve7, opts.intervalMs ?? 3e3));
|
|
1435
1286
|
}
|
|
1436
1287
|
}
|
|
1437
1288
|
|
|
1438
1289
|
// src/setup.ts
|
|
1439
|
-
import { mkdirSync as
|
|
1440
|
-
import { dirname as
|
|
1290
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
1291
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
1441
1292
|
import { createInterface as createInterface3 } from "readline";
|
|
1442
1293
|
|
|
1443
1294
|
// src/mcp-register.ts
|
|
1444
1295
|
import { spawnSync } from "child_process";
|
|
1445
1296
|
|
|
1446
1297
|
// src/skill.ts
|
|
1447
|
-
import { mkdirSync
|
|
1448
|
-
import { join as
|
|
1298
|
+
import { mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1299
|
+
import { join as join4 } from "path";
|
|
1449
1300
|
|
|
1450
1301
|
// src/catalog-file.ts
|
|
1451
1302
|
var CATALOG_FILE = "catalog.md";
|
|
@@ -1502,7 +1353,7 @@ var SKILL_MARKER = ".agentsky.json";
|
|
|
1502
1353
|
function skillDocument() {
|
|
1503
1354
|
return `---
|
|
1504
1355
|
name: ${SKILL_NAME}
|
|
1505
|
-
description: "Hire cloud agents from the AgentSky catalog (included as ${CATALOG_FILE}) and coordinate them on one task. Use when: the person names an agent by @handle or wants to orchestrate agents; asks another engine (Codex, Claude Code, Gemini, Hermes) to do, test or review part of the work; wants to import, move or continue this conversation in AgentSky or the cloud; names a workspace; or the work splits into independent parts that can run at the same time. Works through the agentsky MCP server, or the sky command where those tools are missing."
|
|
1356
|
+
description: "Hire cloud agents from the AgentSky catalog (included as ${CATALOG_FILE}) and coordinate them on one task. Use when: the person names an agent by @handle or wants to orchestrate agents; asks another engine (Codex, Claude Code, Gemini, Hermes) to do, test or review part of the work; wants to import, move or continue this conversation in AgentSky or the cloud; wants an agent their own application starts for its customers from a skill they supply; names a workspace; or the work splits into independent parts that can run at the same time. Works through the agentsky MCP server, or the sky command where those tools are missing."
|
|
1506
1357
|
---
|
|
1507
1358
|
|
|
1508
1359
|
# Working with other agents through AgentSky
|
|
@@ -1521,7 +1372,7 @@ at the end; they do the same things, one agent at a time.
|
|
|
1521
1372
|
|
|
1522
1373
|
Use the workspace the person named. If they named none, check the git root for
|
|
1523
1374
|
\`.agentsky.json\` \u2014 if it has a \`"workspace"\` key, use that. Otherwise use the
|
|
1524
|
-
account default
|
|
1375
|
+
account default workspace. Never ask. Pass the chosen slug as \`workspace\` on
|
|
1525
1376
|
every MCP tool that accepts it, or as \`--workspace <slug>\` on sky commands.
|
|
1526
1377
|
Details: \`${REFERENCES_DIR}/workspaces.md\` in this skill's folder.
|
|
1527
1378
|
|
|
@@ -1602,6 +1453,15 @@ it, and name its path in the brief.
|
|
|
1602
1453
|
6. Tell the person what was done, which agent did which part, and what it cost
|
|
1603
1454
|
(\`get_task\` reports the task's cost).
|
|
1604
1455
|
|
|
1456
|
+
## Build an agent that the person's own application runs
|
|
1457
|
+
|
|
1458
|
+
When the person wants an agent their product starts for its customers, built
|
|
1459
|
+
from a skill they supply (for example: a customer uploads a file, an agent
|
|
1460
|
+
reviews it, their application acts on the verdict), follow
|
|
1461
|
+
\`${REFERENCES_DIR}/focused-agents.md\` in this skill's folder. It covers the
|
|
1462
|
+
prerequisites to tell them about first, one locked-down workspace per customer,
|
|
1463
|
+
one fresh session per event, and reading the answer as data.
|
|
1464
|
+
|
|
1605
1465
|
## Import this conversation to AgentSky
|
|
1606
1466
|
|
|
1607
1467
|
When the person asks to import, move, or continue this conversation in AgentSky
|
|
@@ -1688,6 +1548,160 @@ use \`--session <id>\` instead of \`--current\`, and set \`source_harness\` to t
|
|
|
1688
1548
|
harness name (e.g. \`"hermes"\`, \`"openclaw"\`).
|
|
1689
1549
|
`;
|
|
1690
1550
|
}
|
|
1551
|
+
function focusedAgentsReferenceDoc() {
|
|
1552
|
+
return `# A focused agent your application runs for its customers
|
|
1553
|
+
|
|
1554
|
+
Use this when the person wants an agent that their own product starts on an
|
|
1555
|
+
event (a customer uploads a file, a form is submitted), working from one
|
|
1556
|
+
skill they supply, with the result going back to their application.
|
|
1557
|
+
|
|
1558
|
+
Everything here is the public API (\`https://api.agentsky.dev/v1\`). The
|
|
1559
|
+
application's server calls it. The \`sky\` commands and MCP tools named
|
|
1560
|
+
alongside do the same things by hand, for trying it out.
|
|
1561
|
+
|
|
1562
|
+
## What has to exist first
|
|
1563
|
+
|
|
1564
|
+
Say each of these to the person before you build. Each one changes whether it
|
|
1565
|
+
works:
|
|
1566
|
+
|
|
1567
|
+
- **An API key for their server.** They create it at
|
|
1568
|
+
platform.agentsky.dev \u2192 Developer \u2192 API keys and put it in the server's
|
|
1569
|
+
environment (\`AGENTSKY_TOKEN\`). Never put it in code, a page their customers
|
|
1570
|
+
load, or a brief.
|
|
1571
|
+
- **Credit** on their account. Every run is a session they pay for. Give each
|
|
1572
|
+
one a budget.
|
|
1573
|
+
- **The skill**: a directory with \`SKILL.md\` at its root.
|
|
1574
|
+
- **An engine that fits.** Default to \`claude_code\` on its default model. Check
|
|
1575
|
+
\`sky catalog search <words>\` if the skill needs something else.
|
|
1576
|
+
- **Their own customer sign-in.** AgentSky does not know their customers. A
|
|
1577
|
+
customer id their page sends is a label, not proof of who it is.
|
|
1578
|
+
|
|
1579
|
+
## 1. One workspace per customer
|
|
1580
|
+
|
|
1581
|
+
\`\`\`
|
|
1582
|
+
POST /v1/universes {"slug": "rv-acme", "name": "Uploads from acme"}
|
|
1583
|
+
\`\`\`
|
|
1584
|
+
|
|
1585
|
+
Same by hand: \`sky universe create rv-acme\`. Files, memory, vaults and
|
|
1586
|
+
sessions never cross workspaces. Name every later call's workspace with the
|
|
1587
|
+
\`X-Universe: rv-acme\` header (\`--workspace rv-acme\` on \`sky\`).
|
|
1588
|
+
|
|
1589
|
+
## 2. Lock the workspace down
|
|
1590
|
+
|
|
1591
|
+
\`\`\`
|
|
1592
|
+
PUT /v1/hook_rules {"rules": [{"point": "tool.call", "action": "block", "match": {}, "enabled": true}]}
|
|
1593
|
+
POST /v1/universes/rv-acme/policies {"name": "reviewer-native-tools", "kind": "TOOL_CALL",
|
|
1594
|
+
"config": {"action": "deny", "matcher": "Bash|BashOutput|KillShell|Edit|MultiEdit|Write|NotebookEdit|WebFetch|WebSearch|Task|mcp__.*"}}
|
|
1595
|
+
POST /v1/universes/rv-acme/policies {"name": "reviewer-no-helpers", "kind": "CAPABILITY", "config": {"platform.orchestration": "deny"}}
|
|
1596
|
+
\`\`\`
|
|
1597
|
+
|
|
1598
|
+
- **The hook rule** blocks every connected-app and capability call. Without it,
|
|
1599
|
+
the account owner's own connected apps reach sessions the owner's key
|
|
1600
|
+
starts.
|
|
1601
|
+
- **The \`TOOL_CALL\` rule** denies Claude Code's shell, write, web, sub-agent
|
|
1602
|
+
and MCP tools. A denied call answers "The platform did not authorize this
|
|
1603
|
+
tool invocation."
|
|
1604
|
+
- **Keep only the tools the skill needs.** A reviewer needs Read.
|
|
1605
|
+
|
|
1606
|
+
Telling the agent not to use a tool is not a boundary. The rule is. By hand:
|
|
1607
|
+
\`sky policy add --universe rv-acme --name ... --kind TOOL_CALL --config '<json>'\`.
|
|
1608
|
+
|
|
1609
|
+
## 3. Install the skill, create the agent
|
|
1610
|
+
|
|
1611
|
+
\`\`\`
|
|
1612
|
+
POST /v1/skills multipart: one "files" field per file, named "<dir>/<path>", plus "displayName"
|
|
1613
|
+
POST /v1/agents {"name": "upload-reviewer", "agentType": "claude_code", "prompt": "...",
|
|
1614
|
+
"skills": [{"type": "skill", "skillId": "skill_..."}], "hiringPolicy": "deny"}
|
|
1615
|
+
Idempotency-Key: provision-rv-acme-agent
|
|
1616
|
+
\`\`\`
|
|
1617
|
+
|
|
1618
|
+
By hand: \`sky skills upload ./upload-review --workspace rv-acme\`, then
|
|
1619
|
+
\`sky agent create -f agent.toml --workspace rv-acme\` with
|
|
1620
|
+
\`hiring_policy = "deny"\` and a \`[[skills]]\` table naming the skill id.
|
|
1621
|
+
|
|
1622
|
+
The prompt says what to read and what to answer. It also says that the
|
|
1623
|
+
upload is data and that instructions written inside it are not to be
|
|
1624
|
+
followed.
|
|
1625
|
+
|
|
1626
|
+
## 4. One fresh session per event
|
|
1627
|
+
|
|
1628
|
+
\`\`\`
|
|
1629
|
+
POST /v1/files multipart "file" \u2192 {"id": "file_..."}
|
|
1630
|
+
POST /v1/sessions Idempotency-Key: session-<job id>
|
|
1631
|
+
{"agent": "agent_...", "defaults": "none",
|
|
1632
|
+
"resources": [{"type": "file", "file_id": "file_...", "mount_path": "/upload/request.txt"}],
|
|
1633
|
+
"budget": {"type": "limit", "max_list_cost": {"amount": "50", "currency": "USD"}}}
|
|
1634
|
+
POST /v1/sessions/{id}/events Idempotency-Key: brief-<job id>
|
|
1635
|
+
{"events": [{"type": "user.message", "content": [{"type": "text", "text": "Review /workspace/upload/request.txt with the upload-review skill."}]}]}
|
|
1636
|
+
\`\`\`
|
|
1637
|
+
|
|
1638
|
+
- **\`defaults: "none"\`** keeps every workspace default out of the session:
|
|
1639
|
+
environment, vault, shared files, skills and team memory. The session holds
|
|
1640
|
+
only what the request names.
|
|
1641
|
+
- **Idempotency keys come from the event.** Retrying a timed-out call with the
|
|
1642
|
+
same key returns the first session, or replays the first message. A keyed
|
|
1643
|
+
create cannot carry \`initial_events\`. Create the session, save its id, then
|
|
1644
|
+
send the brief.
|
|
1645
|
+
- **Reuse a session** only for a job that continues for the same customer.
|
|
1646
|
+
|
|
1647
|
+
By hand: \`sky session create agent_... --no-defaults --budget-cents 50 --file ./request.txt:/upload/request.txt --message "..."\`
|
|
1648
|
+
(MCP: \`start_session\`).
|
|
1649
|
+
|
|
1650
|
+
## 5. Read the answer
|
|
1651
|
+
|
|
1652
|
+
\`\`\`
|
|
1653
|
+
GET /v1/sessions/{id}/events?types[]=user.message&types[]=agent.message&types[]=session.status_idle&types[]=session.status_terminated&types[]=session.error
|
|
1654
|
+
\`\`\`
|
|
1655
|
+
|
|
1656
|
+
- **When the turn is done.** It has ended at a \`session.status_idle\` whose
|
|
1657
|
+
\`stop_reason.type\` is not \`requires_action\`, or at
|
|
1658
|
+
\`session.status_terminated\`.
|
|
1659
|
+
- **Where the answer is.** Take the last \`agent.message\` after the last
|
|
1660
|
+
\`user.message\`. Earlier messages can be notes the agent wrote on the way.
|
|
1661
|
+
- **Treat it as untrusted data.** Parse it against a fixed schema and fail
|
|
1662
|
+
closed on anything else. Never run what it says.
|
|
1663
|
+
- **Errors.** A \`session.error\` (budget reached, engine unavailable) is a
|
|
1664
|
+
failure to report, not an answer.
|
|
1665
|
+
|
|
1666
|
+
By hand: \`sky session read <id> --wait --json\` (MCP: \`read_session\`).
|
|
1667
|
+
|
|
1668
|
+
## 6. Questions and approvals
|
|
1669
|
+
|
|
1670
|
+
\`GET /v1/tasks/{session id}\` lists the requests waiting on the key's owner:
|
|
1671
|
+
the agent's questions, approvals, and apps to connect.
|
|
1672
|
+
\`POST /v1/tasks/{id}/requests/{request id}/answer\` with \`{"text": ...}\` or
|
|
1673
|
+
\`{"option_id": ...}\` answers one. Pass a customer's answer on only after
|
|
1674
|
+
your application has checked who they are. By hand: \`sky task show <id>\` and
|
|
1675
|
+
\`sky task answer <id> <request> --text "..."\` (MCP: \`get_task\`,
|
|
1676
|
+
\`answer_request\`).
|
|
1677
|
+
|
|
1678
|
+
## 7. What the application keeps
|
|
1679
|
+
|
|
1680
|
+
The agent decides; the application acts. Keep these in the application's own
|
|
1681
|
+
database:
|
|
1682
|
+
|
|
1683
|
+
- **One job per event**, with a unique event key. The same key with the same
|
|
1684
|
+
payload returns the job it made; with a different payload it is a conflict.
|
|
1685
|
+
- **The session id**, saved before you start polling, so a restart picks the
|
|
1686
|
+
job up where it was.
|
|
1687
|
+
- **A confirmation receipt** from the signed-in customer before any side
|
|
1688
|
+
effect.
|
|
1689
|
+
- **A unique action key per job**, so the side effect happens once. Agent
|
|
1690
|
+
retries do not make it happen once; the key does.
|
|
1691
|
+
- **Webhooks to the customer** with an event id, retried until acknowledged.
|
|
1692
|
+
|
|
1693
|
+
## Before launch
|
|
1694
|
+
|
|
1695
|
+
Check one real session:
|
|
1696
|
+
|
|
1697
|
+
- \`GET /v1/sessions/{id}\` lists only the upload under \`resources\`.
|
|
1698
|
+
- \`GET /v1/hook_rules\` and \`GET /v1/universes/{slug}/policies\` show the rules
|
|
1699
|
+
above.
|
|
1700
|
+
- Ask the agent directly to run a shell command. The answer has to be the
|
|
1701
|
+
platform's refusal.
|
|
1702
|
+
- A file id from another customer's workspace is refused at session create.
|
|
1703
|
+
`;
|
|
1704
|
+
}
|
|
1691
1705
|
function workspacesReferenceDoc() {
|
|
1692
1706
|
return `# Workspaces in AgentSky
|
|
1693
1707
|
|
|
@@ -1723,7 +1737,7 @@ To set a per-repo default, create \`.agentsky.json\` at the git root:
|
|
|
1723
1737
|
}
|
|
1724
1738
|
function installedVersion(dir) {
|
|
1725
1739
|
try {
|
|
1726
|
-
const marker = JSON.parse(
|
|
1740
|
+
const marker = JSON.parse(readFileSync5(join4(dir, SKILL_MARKER), "utf8"));
|
|
1727
1741
|
return typeof marker.version === "string" ? marker.version : null;
|
|
1728
1742
|
} catch {
|
|
1729
1743
|
return null;
|
|
@@ -1733,24 +1747,25 @@ function isInstalledSkill(dir) {
|
|
|
1733
1747
|
return installedVersion(dir) !== null;
|
|
1734
1748
|
}
|
|
1735
1749
|
function installSkill(agent2, opts = {}) {
|
|
1736
|
-
const dir =
|
|
1750
|
+
const dir = join4(agent2.skillsDir, SKILL_NAME);
|
|
1737
1751
|
const previous = installedVersion(dir);
|
|
1738
1752
|
const outcome = previous === CLI_VERSION ? "current" : previous ? "updated" : "installed";
|
|
1739
1753
|
if (opts.dryRun) return { agent: agent2.id, path: dir, outcome };
|
|
1740
1754
|
if (outcome !== "current") {
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1755
|
+
mkdirSync(dir, { recursive: true });
|
|
1756
|
+
writeFileSync3(join4(dir, "SKILL.md"), skillDocument());
|
|
1757
|
+
writeFileSync3(join4(dir, SKILL_MARKER), `${JSON.stringify({ version: CLI_VERSION }, null, 2)}
|
|
1744
1758
|
`);
|
|
1745
1759
|
}
|
|
1746
1760
|
if (opts.catalog) {
|
|
1747
|
-
|
|
1748
|
-
|
|
1761
|
+
mkdirSync(dir, { recursive: true });
|
|
1762
|
+
writeFileSync3(join4(dir, CATALOG_FILE), renderCatalog(opts.catalog, opts.now ?? /* @__PURE__ */ new Date()));
|
|
1749
1763
|
}
|
|
1750
|
-
const refsDir =
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1764
|
+
const refsDir = join4(dir, REFERENCES_DIR);
|
|
1765
|
+
mkdirSync(refsDir, { recursive: true });
|
|
1766
|
+
writeFileSync3(join4(refsDir, "import.md"), importReferenceDoc());
|
|
1767
|
+
writeFileSync3(join4(refsDir, "workspaces.md"), workspacesReferenceDoc());
|
|
1768
|
+
writeFileSync3(join4(refsDir, "focused-agents.md"), focusedAgentsReferenceDoc());
|
|
1754
1769
|
return { agent: agent2.id, path: dir, outcome };
|
|
1755
1770
|
}
|
|
1756
1771
|
|
|
@@ -1801,9 +1816,9 @@ function registerMcpServer(agent2, opts, runner = systemCommandRunner()) {
|
|
|
1801
1816
|
}
|
|
1802
1817
|
|
|
1803
1818
|
// src/setup-local.ts
|
|
1804
|
-
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as
|
|
1805
|
-
import { homedir as
|
|
1806
|
-
import { dirname as
|
|
1819
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
1820
|
+
import { homedir as homedir4 } from "os";
|
|
1821
|
+
import { dirname as dirname2, isAbsolute, join as join5, resolve as resolve3 } from "path";
|
|
1807
1822
|
import { parse as parseToml2 } from "smol-toml";
|
|
1808
1823
|
var MAX_SKILL_FILE_BYTES = 1024 * 1024;
|
|
1809
1824
|
var MAX_IMPORT_DEPTH = 5;
|
|
@@ -1819,22 +1834,22 @@ function sensitive(name) {
|
|
|
1819
1834
|
function skillFiles(root, prefix) {
|
|
1820
1835
|
const files2 = [];
|
|
1821
1836
|
for (const entry2 of readdirSync2(root, { withFileTypes: true })) {
|
|
1822
|
-
const path =
|
|
1837
|
+
const path = join5(root, entry2.name);
|
|
1823
1838
|
if (entry2.isDirectory()) {
|
|
1824
1839
|
if (!SKIPPED_DIRS.has(entry2.name)) files2.push(...skillFiles(path, `${prefix}/${entry2.name}`));
|
|
1825
1840
|
} else if (entry2.isFile() && !sensitive(entry2.name) && statSync2(path).size <= MAX_SKILL_FILE_BYTES) {
|
|
1826
|
-
files2.push({ path: `${prefix}/${entry2.name}`, bytes: new Uint8Array(
|
|
1841
|
+
files2.push({ path: `${prefix}/${entry2.name}`, bytes: new Uint8Array(readFileSync6(path)) });
|
|
1827
1842
|
}
|
|
1828
1843
|
}
|
|
1829
1844
|
return files2;
|
|
1830
1845
|
}
|
|
1831
1846
|
function readLocalSkills(agents) {
|
|
1832
|
-
const
|
|
1847
|
+
const skills2 = [];
|
|
1833
1848
|
const ignored = [];
|
|
1834
1849
|
for (const agent2 of agents) {
|
|
1835
1850
|
if (!existsSync5(agent2.skillsDir)) continue;
|
|
1836
1851
|
for (const entry2 of readdirSync2(agent2.skillsDir, { withFileTypes: true })) {
|
|
1837
|
-
const dir =
|
|
1852
|
+
const dir = join5(agent2.skillsDir, entry2.name);
|
|
1838
1853
|
let isDir = entry2.isDirectory();
|
|
1839
1854
|
if (entry2.isSymbolicLink()) {
|
|
1840
1855
|
try {
|
|
@@ -1843,22 +1858,22 @@ function readLocalSkills(agents) {
|
|
|
1843
1858
|
isDir = false;
|
|
1844
1859
|
}
|
|
1845
1860
|
}
|
|
1846
|
-
if (!isDir || !existsSync5(
|
|
1847
|
-
const name = frontmatterName(
|
|
1861
|
+
if (!isDir || !existsSync5(join5(dir, "SKILL.md")) || isInstalledSkill(dir)) continue;
|
|
1862
|
+
const name = frontmatterName(readFileSync6(join5(dir, "SKILL.md"), "utf8"));
|
|
1848
1863
|
if (!name) {
|
|
1849
1864
|
ignored.push({ agent: agent2.id, dir, reason: "SKILL.md has no name" });
|
|
1850
1865
|
continue;
|
|
1851
1866
|
}
|
|
1852
|
-
const taken =
|
|
1867
|
+
const taken = skills2.find((skill) => skill.name === name);
|
|
1853
1868
|
if (taken) {
|
|
1854
1869
|
ignored.push({ agent: agent2.id, dir, reason: `${taken.agent} already has a skill named ${name}` });
|
|
1855
1870
|
continue;
|
|
1856
1871
|
}
|
|
1857
1872
|
const files2 = skillFiles(dir, entry2.name);
|
|
1858
|
-
|
|
1873
|
+
skills2.push({ name, agent: agent2.id, dir, files: files2, bytes: files2.reduce((sum, file) => sum + file.bytes.length, 0) });
|
|
1859
1874
|
}
|
|
1860
1875
|
}
|
|
1861
|
-
return { skills, ignored };
|
|
1876
|
+
return { skills: skills2, ignored };
|
|
1862
1877
|
}
|
|
1863
1878
|
var INSTRUCTION_FILES = { claude_code: "CLAUDE.md", codex: "AGENTS.md" };
|
|
1864
1879
|
function inlineImports(content, from, home, depth, seen) {
|
|
@@ -1872,11 +1887,11 @@ function inlineImports(content, from, home, depth, seen) {
|
|
|
1872
1887
|
}
|
|
1873
1888
|
const target = fence ? void 0 : line.match(/^@(\S+\.(?:md|markdown|txt))$/i)?.[1];
|
|
1874
1889
|
if (!target || depth >= MAX_IMPORT_DEPTH) return line;
|
|
1875
|
-
const path = target.startsWith("~/") ?
|
|
1890
|
+
const path = target.startsWith("~/") ? join5(home, target.slice(2)) : isAbsolute(target) ? target : resolve3(dirname2(from), target);
|
|
1876
1891
|
if (seen.includes(path)) return "";
|
|
1877
1892
|
let imported;
|
|
1878
1893
|
try {
|
|
1879
|
-
imported =
|
|
1894
|
+
imported = readFileSync6(path, "utf8");
|
|
1880
1895
|
} catch {
|
|
1881
1896
|
return line;
|
|
1882
1897
|
}
|
|
@@ -1884,15 +1899,15 @@ function inlineImports(content, from, home, depth, seen) {
|
|
|
1884
1899
|
return inlineImports(imported, path, home, depth + 1, seen).trim();
|
|
1885
1900
|
}).join("\n");
|
|
1886
1901
|
}
|
|
1887
|
-
function readGlobalInstructions(agents, home =
|
|
1902
|
+
function readGlobalInstructions(agents, home = homedir4()) {
|
|
1888
1903
|
const found = [];
|
|
1889
1904
|
for (const agent2 of agents) {
|
|
1890
1905
|
const file = INSTRUCTION_FILES[agent2.id];
|
|
1891
1906
|
if (!file) continue;
|
|
1892
|
-
const path =
|
|
1907
|
+
const path = join5(agent2.root, file);
|
|
1893
1908
|
let raw;
|
|
1894
1909
|
try {
|
|
1895
|
-
raw =
|
|
1910
|
+
raw = readFileSync6(path, "utf8");
|
|
1896
1911
|
} catch {
|
|
1897
1912
|
continue;
|
|
1898
1913
|
}
|
|
@@ -1911,7 +1926,7 @@ ${item.content.trim()}`).join("\n\n");
|
|
|
1911
1926
|
}
|
|
1912
1927
|
function readJson2(path) {
|
|
1913
1928
|
try {
|
|
1914
|
-
return JSON.parse(
|
|
1929
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
1915
1930
|
} catch {
|
|
1916
1931
|
return void 0;
|
|
1917
1932
|
}
|
|
@@ -1925,15 +1940,15 @@ function bearerOf(def) {
|
|
|
1925
1940
|
return match?.[1] ?? null;
|
|
1926
1941
|
}
|
|
1927
1942
|
function serversOf(agent2, home) {
|
|
1928
|
-
if (agent2.id === "claude_code") return readJson2(
|
|
1929
|
-
if (agent2.id === "cursor") return readJson2(
|
|
1943
|
+
if (agent2.id === "claude_code") return readJson2(join5(home, ".claude.json"))?.mcpServers ?? {};
|
|
1944
|
+
if (agent2.id === "cursor") return readJson2(join5(agent2.root, "mcp.json"))?.mcpServers ?? {};
|
|
1930
1945
|
try {
|
|
1931
|
-
return parseToml2(
|
|
1946
|
+
return parseToml2(readFileSync6(join5(agent2.root, "config.toml"), "utf8")).mcp_servers ?? {};
|
|
1932
1947
|
} catch {
|
|
1933
1948
|
return {};
|
|
1934
1949
|
}
|
|
1935
1950
|
}
|
|
1936
|
-
function findLocalSecrets(agents, home =
|
|
1951
|
+
function findLocalSecrets(agents, home = homedir4()) {
|
|
1937
1952
|
const secrets2 = [];
|
|
1938
1953
|
for (const agent2 of agents) {
|
|
1939
1954
|
for (const [name, def] of Object.entries(serversOf(agent2, home))) {
|
|
@@ -1949,9 +1964,9 @@ function findLocalSecrets(agents, home = homedir5()) {
|
|
|
1949
1964
|
|
|
1950
1965
|
// src/subscriptions.ts
|
|
1951
1966
|
import { spawn as spawn3 } from "child_process";
|
|
1952
|
-
import { existsSync as existsSync6, readFileSync as
|
|
1953
|
-
import { homedir as
|
|
1954
|
-
import { join as
|
|
1967
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
1968
|
+
import { homedir as homedir5 } from "os";
|
|
1969
|
+
import { join as join6 } from "path";
|
|
1955
1970
|
import { createInterface as createInterface2 } from "readline";
|
|
1956
1971
|
var ALIASES = {
|
|
1957
1972
|
claude: "anthropic",
|
|
@@ -2036,12 +2051,12 @@ async function connectClaude() {
|
|
|
2036
2051
|
const expiry = sub.expiresAt ? ` (expires ~${sub.expiresAt.slice(0, 10)})` : "";
|
|
2037
2052
|
console.log(`Connected Claude subscription${expiry}. Powers new eligible agents.`);
|
|
2038
2053
|
}
|
|
2039
|
-
function codexSignIn(env = process.env, home =
|
|
2040
|
-
const path =
|
|
2054
|
+
function codexSignIn(env = process.env, home = homedir5()) {
|
|
2055
|
+
const path = join6(env.CODEX_HOME || join6(home, ".codex"), "auth.json");
|
|
2041
2056
|
if (!existsSync6(path)) {
|
|
2042
2057
|
throw new ApiError(404, "no_codex_auth", `no ${path} \u2014 run \`codex login\` first`);
|
|
2043
2058
|
}
|
|
2044
|
-
const raw =
|
|
2059
|
+
const raw = readFileSync7(path, "utf8");
|
|
2045
2060
|
try {
|
|
2046
2061
|
const parsed = JSON.parse(raw);
|
|
2047
2062
|
return { path, raw, account: parsed.tokens?.account_id };
|
|
@@ -2049,12 +2064,12 @@ function codexSignIn(env = process.env, home = homedir6()) {
|
|
|
2049
2064
|
throw new ApiError(400, "invalid_codex_auth", `${path} isn't valid JSON \u2014 re-run \`codex login\``);
|
|
2050
2065
|
}
|
|
2051
2066
|
}
|
|
2052
|
-
function cursorSignIn(env = process.env, home =
|
|
2053
|
-
const path =
|
|
2067
|
+
function cursorSignIn(env = process.env, home = homedir5()) {
|
|
2068
|
+
const path = join6(env.XDG_CONFIG_HOME || join6(home, ".config"), "cursor", "auth.json");
|
|
2054
2069
|
if (!existsSync6(path)) {
|
|
2055
2070
|
throw new ApiError(404, "no_cursor_auth", `no ${path} \u2014 run \`cursor-agent login\` first`);
|
|
2056
2071
|
}
|
|
2057
|
-
const raw =
|
|
2072
|
+
const raw = readFileSync7(path, "utf8");
|
|
2058
2073
|
try {
|
|
2059
2074
|
const parsed = JSON.parse(raw);
|
|
2060
2075
|
if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") throw new Error("shape");
|
|
@@ -2133,11 +2148,11 @@ function planProviders(names) {
|
|
|
2133
2148
|
}));
|
|
2134
2149
|
}
|
|
2135
2150
|
function reportPath() {
|
|
2136
|
-
return
|
|
2151
|
+
return join7(dirname3(configPath()), "setup.json");
|
|
2137
2152
|
}
|
|
2138
2153
|
function loadReport() {
|
|
2139
2154
|
try {
|
|
2140
|
-
const saved = JSON.parse(
|
|
2155
|
+
const saved = JSON.parse(readFileSync8(reportPath(), "utf8"));
|
|
2141
2156
|
return { agents: saved.agents ?? [], plans: saved.plans ?? [], ...saved.secrets ? { secrets: saved.secrets } : {} };
|
|
2142
2157
|
} catch {
|
|
2143
2158
|
return { agents: [], plans: [] };
|
|
@@ -2150,8 +2165,8 @@ function mergeReport(base, next) {
|
|
|
2150
2165
|
return { agents, plans, ...secrets2 ? { secrets: secrets2 } : {} };
|
|
2151
2166
|
}
|
|
2152
2167
|
function saveReport(report) {
|
|
2153
|
-
|
|
2154
|
-
|
|
2168
|
+
mkdirSync2(dirname3(reportPath()), { recursive: true });
|
|
2169
|
+
writeFileSync4(reportPath(), `${JSON.stringify(report, null, 2)}
|
|
2155
2170
|
`, { mode: 384 });
|
|
2156
2171
|
}
|
|
2157
2172
|
function confirm3(question) {
|
|
@@ -2321,13 +2336,13 @@ async function runSetup(opts) {
|
|
|
2321
2336
|
await connectPlans(agents, opts, interactive, emit, run2);
|
|
2322
2337
|
await tell();
|
|
2323
2338
|
}
|
|
2324
|
-
const { skills, ignored } = wantsImport ? readLocalSkills(agents) : { skills: [], ignored: [] };
|
|
2339
|
+
const { skills: skills2, ignored } = wantsImport ? readLocalSkills(agents) : { skills: [], ignored: [] };
|
|
2325
2340
|
const instructions = wantsImport ? readGlobalInstructions(agents) : [];
|
|
2326
2341
|
const secrets2 = wantsSecrets ? findLocalSecrets(agents) : [];
|
|
2327
2342
|
if (wantsImport || wantsSecrets) {
|
|
2328
2343
|
emit({
|
|
2329
2344
|
type: "found",
|
|
2330
|
-
skills:
|
|
2345
|
+
skills: skills2.map((skill) => ({ name: skill.name, agent: skill.agent, files: skill.files.length, bytes: skill.bytes })),
|
|
2331
2346
|
ignored: ignored.map((item) => ({ dir: item.dir, reason: item.reason })),
|
|
2332
2347
|
instructions: instructions.map((item) => ({ agent: item.agent, path: item.path, bytes: Buffer.byteLength(item.content), imports: item.imports })),
|
|
2333
2348
|
secrets: secrets2.map((secret) => ({ name: secret.name, agent: secret.agent, url: secret.url }))
|
|
@@ -2341,8 +2356,8 @@ async function runSetup(opts) {
|
|
|
2341
2356
|
const report = mergeReport(earlier, run2);
|
|
2342
2357
|
saveReport(report);
|
|
2343
2358
|
if (key) {
|
|
2344
|
-
const sendFiles = wantsImport && (
|
|
2345
|
-
await importToWorkspace(key, held, sendFiles ?
|
|
2359
|
+
const sendFiles = wantsImport && (skills2.length > 0 || instructions.length > 0) && await agreed("Send these skills and instructions to your AgentSky workspace?");
|
|
2360
|
+
await importToWorkspace(key, held, sendFiles ? skills2 : [], sendFiles ? combineInstructions(instructions) : "", emit);
|
|
2346
2361
|
}
|
|
2347
2362
|
emit({ type: "done", report });
|
|
2348
2363
|
}
|
|
@@ -2421,10 +2436,10 @@ async function saveSecrets(found, opts, interactive, emit) {
|
|
|
2421
2436
|
}
|
|
2422
2437
|
return counts;
|
|
2423
2438
|
}
|
|
2424
|
-
async function importToWorkspace(key, held,
|
|
2439
|
+
async function importToWorkspace(key, held, skills2, instructions, emit) {
|
|
2425
2440
|
const outcomes = [];
|
|
2426
2441
|
let signedOut = false;
|
|
2427
|
-
for (const skill of
|
|
2442
|
+
for (const skill of skills2) {
|
|
2428
2443
|
if (signedOut) {
|
|
2429
2444
|
outcomes.push({ name: skill.name, outcome: "skipped: sign in to agentsky.dev again" });
|
|
2430
2445
|
continue;
|
|
@@ -2499,6 +2514,50 @@ function renderArtifact(artifact2) {
|
|
|
2499
2514
|
return `${artifact2.id} ${artifact2.name} v${artifact2.version} ${files2} entry ${artifact2.entry}${artifact2.share_url ? ` shared ${artifact2.share_url}` : ""}`;
|
|
2500
2515
|
}
|
|
2501
2516
|
|
|
2517
|
+
// src/skills-store.ts
|
|
2518
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
|
|
2519
|
+
import { basename as basename3, join as join8, relative, resolve as resolve4 } from "path";
|
|
2520
|
+
function skillBundle(dir) {
|
|
2521
|
+
const root = resolve4(dir);
|
|
2522
|
+
const top = basename3(root);
|
|
2523
|
+
const files2 = [];
|
|
2524
|
+
const walk = (path) => {
|
|
2525
|
+
for (const name of readdirSync3(path).sort()) {
|
|
2526
|
+
const full = join8(path, name);
|
|
2527
|
+
if (statSync3(full).isDirectory()) walk(full);
|
|
2528
|
+
else files2.push({ path: `${top}/${relative(root, full)}`, bytes: readFileSync9(full) });
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
walk(root);
|
|
2532
|
+
if (!files2.some((file) => file.path === `${top}/SKILL.md`)) throw new Error(`${dir}: no SKILL.md at its root`);
|
|
2533
|
+
return files2;
|
|
2534
|
+
}
|
|
2535
|
+
async function uploadSkill(dir, displayName) {
|
|
2536
|
+
const form = new FormData();
|
|
2537
|
+
for (const file of skillBundle(dir)) form.append("files", new Blob([new Uint8Array(file.bytes)]), file.path);
|
|
2538
|
+
form.append("displayName", displayName ?? basename3(resolve4(dir)));
|
|
2539
|
+
return apiForm("POST", "/skills", form);
|
|
2540
|
+
}
|
|
2541
|
+
async function listSkills() {
|
|
2542
|
+
return (await api("GET", "/skills")).skills;
|
|
2543
|
+
}
|
|
2544
|
+
async function getTask(task2) {
|
|
2545
|
+
return (await api("GET", `/tasks/${encodeURIComponent(task2)}`)).task;
|
|
2546
|
+
}
|
|
2547
|
+
function answerBody(answer) {
|
|
2548
|
+
if (!answer.option === !answer.text) throw new Error("give exactly one of --option or --text");
|
|
2549
|
+
return { ...answer.option ? { option_id: answer.option } : { text: answer.text }, ...answer.note ? { note: answer.note } : {} };
|
|
2550
|
+
}
|
|
2551
|
+
async function answerRequest(task2, request, answer) {
|
|
2552
|
+
return api("POST", `/tasks/${encodeURIComponent(task2)}/requests/${encodeURIComponent(request)}/answer`, answerBody(answer));
|
|
2553
|
+
}
|
|
2554
|
+
function renderRequest(request) {
|
|
2555
|
+
const options = request.options.length ? `
|
|
2556
|
+
options: ${request.options.map((option) => `${option.id} (${option.label})`).join(", ")}` : "";
|
|
2557
|
+
return `${request.id} ${request.kind} from ${request.session_id}
|
|
2558
|
+
${request.question}${options}`;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2502
2561
|
// src/workspace.ts
|
|
2503
2562
|
var KINDS = ["TOOL_CALL", "SPEND_CAP", "CALL_COUNT", "CAPABILITY"];
|
|
2504
2563
|
async function workspaceSlug(explicit) {
|
|
@@ -2593,6 +2652,173 @@ function renderPolicy(rule) {
|
|
|
2593
2652
|
return `${state} ${rule.layer.padEnd(9)} ${rule.kind.padEnd(10)} ${rule.name} ${JSON.stringify(rule.config)} ${rule.id}`;
|
|
2594
2653
|
}
|
|
2595
2654
|
|
|
2655
|
+
// src/harness.ts
|
|
2656
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
2657
|
+
import { dirname as dirname4, join as join9, resolve as resolve5 } from "path";
|
|
2658
|
+
var HARNESS_FILE = "harness.json";
|
|
2659
|
+
function harnessFilePath(dir) {
|
|
2660
|
+
return join9(resolve5(dir), HARNESS_FILE);
|
|
2661
|
+
}
|
|
2662
|
+
function readHarnessFile(dir) {
|
|
2663
|
+
const path = harnessFilePath(dir);
|
|
2664
|
+
if (!existsSync7(path)) {
|
|
2665
|
+
throw new Error(`no ${HARNESS_FILE} found in ${resolve5(dir)} \u2014 run \`sky harness init\` first`);
|
|
2666
|
+
}
|
|
2667
|
+
try {
|
|
2668
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
2669
|
+
} catch (err) {
|
|
2670
|
+
throw new Error(`could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
var INIT_SKELETON = {
|
|
2674
|
+
id: "my_harness",
|
|
2675
|
+
name: "My Harness",
|
|
2676
|
+
version: "0.1.0",
|
|
2677
|
+
distribution: {
|
|
2678
|
+
// choose one distribution kind and remove the others:
|
|
2679
|
+
npx: "@your-scope/my-harness@0.1.0"
|
|
2680
|
+
// uvx: "my-harness==0.1.0",
|
|
2681
|
+
// binary: { url: "https://example.com/my-harness-linux-amd64", sha256: "..." },
|
|
2682
|
+
}
|
|
2683
|
+
// optional: declare the tools this harness exposes
|
|
2684
|
+
// tools: [{ id: "bash", kind: "execute", title: "Shell" }],
|
|
2685
|
+
// optional: feature flags
|
|
2686
|
+
// features: ["subagents"],
|
|
2687
|
+
};
|
|
2688
|
+
var INIT_COMMENT = `# harness.json \u2014 workspace harness profile skeleton
|
|
2689
|
+
# Validate locally: sky harness verify .
|
|
2690
|
+
# Submit: sky harness push .
|
|
2691
|
+
# Full schema: https://agentsky.dev/docs/harness-profile
|
|
2692
|
+
`;
|
|
2693
|
+
function harnessInit(dir) {
|
|
2694
|
+
const path = harnessFilePath(dir);
|
|
2695
|
+
if (existsSync7(path)) {
|
|
2696
|
+
throw new Error(`${path} already exists \u2014 delete it to re-init`);
|
|
2697
|
+
}
|
|
2698
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
2699
|
+
writeFileSync5(path, `${JSON.stringify(INIT_SKELETON, null, 2)}
|
|
2700
|
+
`);
|
|
2701
|
+
console.log(`Created ${path}`);
|
|
2702
|
+
console.log(INIT_COMMENT);
|
|
2703
|
+
}
|
|
2704
|
+
async function harnessVerify(dir, opts) {
|
|
2705
|
+
const raw = readHarnessFile(dir);
|
|
2706
|
+
const parsed = harnessProfileSchema.safeParse(raw);
|
|
2707
|
+
if (!parsed.success) {
|
|
2708
|
+
console.error("harness.json schema errors:");
|
|
2709
|
+
for (const issue of parsed.error.issues) {
|
|
2710
|
+
const path = issue.path.length ? issue.path.join(".") : "(root)";
|
|
2711
|
+
console.error(` ${path}: ${issue.message}`);
|
|
2712
|
+
}
|
|
2713
|
+
process.exit(1);
|
|
2714
|
+
}
|
|
2715
|
+
console.log(`harness.json ok (id: ${parsed.data.id}, version: ${parsed.data.version})`);
|
|
2716
|
+
if (!opts.handle) return;
|
|
2717
|
+
const rawHandle = opts.handle.replace(/^@/, "");
|
|
2718
|
+
console.log(`Starting conformance run for @${rawHandle}\u2026`);
|
|
2719
|
+
let runId;
|
|
2720
|
+
try {
|
|
2721
|
+
const { run: run2 } = await api(
|
|
2722
|
+
"POST",
|
|
2723
|
+
`/catalog/listings/${encodeURIComponent(rawHandle)}/conformance`
|
|
2724
|
+
);
|
|
2725
|
+
runId = run2.id;
|
|
2726
|
+
console.log(` run ${runId} status: ${run2.status}`);
|
|
2727
|
+
} catch (err) {
|
|
2728
|
+
if (err instanceof ApiError) {
|
|
2729
|
+
console.error(`sky: conformance start failed \u2014 ${err.message} (${err.code})`);
|
|
2730
|
+
} else {
|
|
2731
|
+
console.error(`sky: conformance start failed \u2014 ${String(err)}`);
|
|
2732
|
+
}
|
|
2733
|
+
process.exit(1);
|
|
2734
|
+
}
|
|
2735
|
+
const pollMs = 4e3;
|
|
2736
|
+
const maxWaitMs = 5 * 60 * 1e3;
|
|
2737
|
+
const deadline = Date.now() + maxWaitMs;
|
|
2738
|
+
while (Date.now() < deadline) {
|
|
2739
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
2740
|
+
try {
|
|
2741
|
+
const { run: run2 } = await api("GET", `/catalog/listings/${encodeURIComponent(rawHandle)}/conformance`);
|
|
2742
|
+
if (run2.status === "running") {
|
|
2743
|
+
process.stdout.write(".");
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
process.stdout.write("\n");
|
|
2747
|
+
if (run2.status === "failed") {
|
|
2748
|
+
console.error(`conformance run failed: ${run2.reason ?? "unknown reason"}`);
|
|
2749
|
+
process.exit(1);
|
|
2750
|
+
}
|
|
2751
|
+
console.log(`
|
|
2752
|
+
Conformance report for @${rawHandle}`);
|
|
2753
|
+
console.log(
|
|
2754
|
+
` required_passed: ${run2.required_passed ? "yes" : "no"} all_passed: ${run2.all_passed ? "yes" : "no"}`
|
|
2755
|
+
);
|
|
2756
|
+
if (run2.report?.checks) {
|
|
2757
|
+
for (const check of run2.report.checks) {
|
|
2758
|
+
const icon = check.status === "pass" ? "\u2713" : check.status === "skipped" ? "\u2013" : "\u2717";
|
|
2759
|
+
const req = check.required ? " [required]" : "";
|
|
2760
|
+
console.log(` ${icon} ${check.id}${req} \u2014 ${check.reason ?? check.status} (${check.seconds}s)`);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
if (!run2.required_passed) process.exit(1);
|
|
2764
|
+
return;
|
|
2765
|
+
} catch (err) {
|
|
2766
|
+
console.error(`
|
|
2767
|
+
sky: conformance poll failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
2768
|
+
process.exit(1);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
console.error("\nsky: timed out waiting for conformance run (5 min)");
|
|
2772
|
+
process.exit(1);
|
|
2773
|
+
}
|
|
2774
|
+
async function harnessPush(dir, opts) {
|
|
2775
|
+
const raw = readHarnessFile(dir);
|
|
2776
|
+
const parsed = harnessProfileSchema.safeParse(raw);
|
|
2777
|
+
if (!parsed.success) {
|
|
2778
|
+
console.error("harness.json is invalid \u2014 run `sky harness verify` first:");
|
|
2779
|
+
for (const issue of parsed.error.issues) {
|
|
2780
|
+
const path = issue.path.length ? issue.path.join(".") : "(root)";
|
|
2781
|
+
console.error(` ${path}: ${issue.message}`);
|
|
2782
|
+
}
|
|
2783
|
+
process.exit(1);
|
|
2784
|
+
}
|
|
2785
|
+
const profile = parsed.data;
|
|
2786
|
+
const version = opts.version ?? profile.version;
|
|
2787
|
+
const handle = profile.id;
|
|
2788
|
+
let existing;
|
|
2789
|
+
try {
|
|
2790
|
+
const { harnesses } = await api("GET", "/harnesses");
|
|
2791
|
+
existing = harnesses.find((h) => h.handle === handle);
|
|
2792
|
+
} catch (err) {
|
|
2793
|
+
if (!(err instanceof ApiError && err.status === 404)) throw err;
|
|
2794
|
+
}
|
|
2795
|
+
if (!existing) {
|
|
2796
|
+
const { harness: harness2 } = await api("POST", "/harnesses", {
|
|
2797
|
+
handle,
|
|
2798
|
+
name: profile.name,
|
|
2799
|
+
profile,
|
|
2800
|
+
model_billing: "gateway"
|
|
2801
|
+
});
|
|
2802
|
+
existing = harness2;
|
|
2803
|
+
console.log(`Created harness ${harness2.id} handle: ${harness2.handle}`);
|
|
2804
|
+
} else {
|
|
2805
|
+
console.log(`Harness exists ${existing.id} handle: ${existing.handle}`);
|
|
2806
|
+
}
|
|
2807
|
+
const { version: ver } = await api(
|
|
2808
|
+
"POST",
|
|
2809
|
+
`/harnesses/${encodeURIComponent(existing.id)}/versions`,
|
|
2810
|
+
{ version, profile }
|
|
2811
|
+
);
|
|
2812
|
+
console.log(
|
|
2813
|
+
`Version ${ver.version} status: ${ver.status} id: ${ver.id} created: ${ver.created_at}`
|
|
2814
|
+
);
|
|
2815
|
+
console.log(
|
|
2816
|
+
`
|
|
2817
|
+
Next step: run conformance
|
|
2818
|
+
sky harness verify . --handle @${handle}`
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2596
2822
|
// src/index.ts
|
|
2597
2823
|
function printModelBilling(session2) {
|
|
2598
2824
|
if (session2.modelBilling !== "account") return;
|
|
@@ -2625,7 +2851,7 @@ auth.command("whoami").description("who the current token acts as").action(
|
|
|
2625
2851
|
run(async () => {
|
|
2626
2852
|
const who = await api("GET", "/whoami");
|
|
2627
2853
|
console.log(
|
|
2628
|
-
`${who.user.email ?? "unknown"} \xB7 universe ${who.universe.slug}${who.universe.isPersonal ? " (
|
|
2854
|
+
`${who.user.email ?? "unknown"} \xB7 universe ${who.universe.slug}${who.universe.isPersonal ? " (default)" : ""} \xB7 scopes ${who.scopes.join(",")}`
|
|
2629
2855
|
);
|
|
2630
2856
|
})
|
|
2631
2857
|
);
|
|
@@ -2645,7 +2871,7 @@ universe.command("list").action(
|
|
|
2645
2871
|
const current = currentUniverse();
|
|
2646
2872
|
for (const u of universes) {
|
|
2647
2873
|
const marker = u.slug === current || !current && u.isPersonal ? "*" : " ";
|
|
2648
|
-
console.log(`${marker} ${u.slug}${u.isPersonal ? " (
|
|
2874
|
+
console.log(`${marker} ${u.slug}${u.isPersonal ? " (default)" : ""}`);
|
|
2649
2875
|
}
|
|
2650
2876
|
})
|
|
2651
2877
|
);
|
|
@@ -2671,7 +2897,7 @@ universe.command("show").action(
|
|
|
2671
2897
|
console.log(`${u.slug} \u2014 ${u.name} \xB7 ${u.agents} agents \xB7 ${u.members} members`);
|
|
2672
2898
|
} else {
|
|
2673
2899
|
const who = await api("GET", path);
|
|
2674
|
-
console.log(`${who.universe.slug} (
|
|
2900
|
+
console.log(`${who.universe.slug} (default)`);
|
|
2675
2901
|
}
|
|
2676
2902
|
})
|
|
2677
2903
|
);
|
|
@@ -2719,7 +2945,7 @@ workspace.command("list").action(
|
|
|
2719
2945
|
const current = currentUniverse();
|
|
2720
2946
|
for (const u of universes) {
|
|
2721
2947
|
const marker = u.slug === current || !current && u.isPersonal ? "*" : " ";
|
|
2722
|
-
console.log(`${marker} ${u.slug}${u.isPersonal ? " (
|
|
2948
|
+
console.log(`${marker} ${u.slug}${u.isPersonal ? " (default)" : ""}`);
|
|
2723
2949
|
}
|
|
2724
2950
|
})
|
|
2725
2951
|
);
|
|
@@ -2864,7 +3090,7 @@ function renderResource(r) {
|
|
|
2864
3090
|
const ref = r.checkout ? `@${r.checkout.type === "branch" ? r.checkout.name : r.checkout.sha}` : "";
|
|
2865
3091
|
return `${r.id} repo ${r.mount_path} ${r.url}${ref}`;
|
|
2866
3092
|
}
|
|
2867
|
-
session.command("create <agent>").description("create a session (starts provisioning by default)").option("--title <title>", "session title").option("--message <text>", "start the first turn immediately, provisioning even with --deferred").option("--deferred", "leave the session idle without a pod until its first turn").option("--vcpus <n>", "machine vCPUs: 1, 2, 4 or 8 (default 2)").option("--memory <mb>", "machine memory in MB, 256MB steps (default 4096)").option("--file <path...>", "mount local files read-only in the working directory: path[:/mount] (uploaded first)").option("--repo <url...>", "clone GitHub repositories into the working directory: https://github.com/{owner}/{repo}[@branch|@sha]").option("--repo-token <token>", "token for the --repo clones; compiled into the sandbox's egress policy, never sent to it").option("--json", "print the created session as JSON").action(
|
|
3093
|
+
session.command("create <agent>").description("create a session (starts provisioning by default)").option("--title <title>", "session title").option("--message <text>", "start the first turn immediately, provisioning even with --deferred").option("--deferred", "leave the session idle without a pod until its first turn").option("--vcpus <n>", "machine vCPUs: 1, 2, 4 or 8 (default 2)").option("--memory <mb>", "machine memory in MB, 256MB steps (default 4096)").option("--file <path...>", "mount local files read-only in the working directory: path[:/mount] (uploaded first)").option("--repo <url...>", "clone GitHub repositories into the working directory: https://github.com/{owner}/{repo}[@branch|@sha]").option("--repo-token <token>", "token for the --repo clones; compiled into the sandbox's egress policy, never sent to it").option("--json", "print the created session as JSON").option("--no-defaults", "attach only what this command names: no workspace default environment, vault, files, skills or memory").option("--budget-cents <cents>", "the most the session may spend, in whole US cents").action(
|
|
2868
3094
|
run(
|
|
2869
3095
|
async (agentRef, opts) => {
|
|
2870
3096
|
const resources = await resourceParams(opts);
|
|
@@ -2874,6 +3100,8 @@ session.command("create <agent>").description("create a session (starts provisio
|
|
|
2874
3100
|
...opts.vcpus !== void 0 ? { vcpus: Number(opts.vcpus) } : {},
|
|
2875
3101
|
...opts.memory !== void 0 ? { memoryMb: Number(opts.memory) } : {},
|
|
2876
3102
|
...opts.deferred ? { provision: "deferred" } : {},
|
|
3103
|
+
...opts.defaults === false ? { defaults: "none" } : {},
|
|
3104
|
+
...opts.budgetCents ? { budget: { type: "limit", max_list_cost: { amount: opts.budgetCents, currency: "USD" } } } : {},
|
|
2877
3105
|
...resources.length ? { resources } : {},
|
|
2878
3106
|
...opts.message ? { initial_events: [{ type: "user.message", content: [{ type: "text", text: opts.message }] }] } : {}
|
|
2879
3107
|
});
|
|
@@ -3206,9 +3434,9 @@ secrets.command("unset <id> <key>").action(
|
|
|
3206
3434
|
);
|
|
3207
3435
|
program.command("launch").description("scaffold ./agent.toml if absent, create the agent, print its address").option("-f, --file <path>", "agent file", "agent.toml").option("--type <type>", "override the file's harness: hermes | claude_code | codex | openclaw | pi | dsh | kimi_code | opencode").option("--llm <llm>", "override the file's model (must suit the harness)").action(
|
|
3208
3436
|
run(async (opts) => {
|
|
3209
|
-
const path =
|
|
3210
|
-
if (!
|
|
3211
|
-
const name =
|
|
3437
|
+
const path = resolve6(opts.file);
|
|
3438
|
+
if (!existsSync8(path)) {
|
|
3439
|
+
const name = basename4(process.cwd()).replace(/[^a-zA-Z0-9 _-]/g, "") || "my-agent";
|
|
3212
3440
|
scaffoldAgentFile(path, name);
|
|
3213
3441
|
console.log(`Scaffolded ${opts.file} \u2014 edit it anytime; re-apply with \`sky deploy\`.`);
|
|
3214
3442
|
}
|
|
@@ -3239,7 +3467,7 @@ program.command("launch").description("scaffold ./agent.toml if absent, create t
|
|
|
3239
3467
|
);
|
|
3240
3468
|
program.command("deploy").description("re-apply ./agent.toml to the existing agent (idempotent)").option("-f, --file <path>", "agent file", "agent.toml").action(
|
|
3241
3469
|
run(async (opts) => {
|
|
3242
|
-
const path =
|
|
3470
|
+
const path = resolve6(opts.file);
|
|
3243
3471
|
const { file, spec } = readAgentFile(path);
|
|
3244
3472
|
if (!file.id) {
|
|
3245
3473
|
console.log("No agent id in the file yet \u2014 running launch instead.");
|
|
@@ -3289,7 +3517,7 @@ program.command("import").description("import a local Claude Code or Codex sessi
|
|
|
3289
3517
|
if (opts.trace && !opts.key) throw new Error("--trace requires --key <importKey>");
|
|
3290
3518
|
const limit = Math.max(1, Math.min(50, parseInt(opts.limit ?? "10", 10) || 10));
|
|
3291
3519
|
if (opts.harness && !["claude_code", "codex", "all"].includes(opts.harness)) throw new Error("Use --harness claude_code, codex, or all.");
|
|
3292
|
-
const
|
|
3520
|
+
const harness2 = opts.harness ?? "all";
|
|
3293
3521
|
const base = opts.url ?? agentUseBase();
|
|
3294
3522
|
const nonInteractive = !process.stdin.isTTY;
|
|
3295
3523
|
const autoConfirm = opts.yes || opts.current || !!opts.session && nonInteractive;
|
|
@@ -3308,18 +3536,18 @@ program.command("import").description("import a local Claude Code or Codex sessi
|
|
|
3308
3536
|
}
|
|
3309
3537
|
} else {
|
|
3310
3538
|
const cwd = process.cwd();
|
|
3311
|
-
const all = listRecentSessions(
|
|
3539
|
+
const all = listRecentSessions(harness2 === "all" ? void 0 : harness2, 50);
|
|
3312
3540
|
const byCwd = all.filter((s) => s.workspace === cwd || s.workspace.startsWith(cwd + "/"));
|
|
3313
3541
|
candidates = byCwd.length > 0 ? [byCwd[0]] : all.length > 0 ? [all[0]] : [];
|
|
3314
3542
|
}
|
|
3315
3543
|
if (candidates.length === 0) throw new Error("No local session found for the current environment. Run with --session <id> or start an agent session first.");
|
|
3316
3544
|
} else if (opts.session) {
|
|
3317
|
-
candidates = listRecentSessions(
|
|
3545
|
+
candidates = listRecentSessions(harness2 === "all" ? void 0 : harness2, Number.POSITIVE_INFINITY).filter(
|
|
3318
3546
|
(s) => s.sessionId === opts.session
|
|
3319
3547
|
);
|
|
3320
3548
|
if (candidates.length === 0) throw new Error(`Session "${opts.session}" not found in ~/.claude or ~/.codex.`);
|
|
3321
3549
|
} else {
|
|
3322
|
-
candidates = listRecentSessions(
|
|
3550
|
+
candidates = listRecentSessions(harness2 === "all" ? void 0 : harness2, limit);
|
|
3323
3551
|
if (candidates.length === 0) {
|
|
3324
3552
|
console.log("No recent sessions found. Make sure you have run Claude Code (sessions in ~/.claude) or Codex (sessions in ~/.codex).");
|
|
3325
3553
|
return;
|
|
@@ -3342,11 +3570,11 @@ program.command("import").description("import a local Claude Code or Codex sessi
|
|
|
3342
3570
|
console.log();
|
|
3343
3571
|
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
3344
3572
|
const answer = await new Promise(
|
|
3345
|
-
(
|
|
3573
|
+
(resolve7) => rl.question(
|
|
3346
3574
|
`Import which session? [1-${candidates.length}, or comma-separated, or "all"]: `,
|
|
3347
3575
|
(a) => {
|
|
3348
3576
|
rl.close();
|
|
3349
|
-
|
|
3577
|
+
resolve7(a.trim());
|
|
3350
3578
|
}
|
|
3351
3579
|
)
|
|
3352
3580
|
);
|
|
@@ -3369,9 +3597,9 @@ program.command("import").description("import a local Claude Code or Codex sessi
|
|
|
3369
3597
|
if (!autoConfirm) {
|
|
3370
3598
|
if (nonInteractive) throw new Error("Use --yes to confirm this transfer in a non-interactive terminal.");
|
|
3371
3599
|
const prompt = createInterface4({ input: process.stdin, output: process.stdout });
|
|
3372
|
-
const answer = await new Promise((
|
|
3600
|
+
const answer = await new Promise((resolve7) => prompt.question("Upload these selected conversations? [y/N] ", (value) => {
|
|
3373
3601
|
prompt.close();
|
|
3374
|
-
|
|
3602
|
+
resolve7(value);
|
|
3375
3603
|
}));
|
|
3376
3604
|
if (!/^y(?:es)?$/i.test(answer.trim())) {
|
|
3377
3605
|
console.log("Cancelled. Nothing uploaded.");
|
|
@@ -3453,7 +3681,7 @@ files.command("download <id>").description("download a session file's bytes").re
|
|
|
3453
3681
|
console.log(`Wrote ${bytes.length} bytes to ${opts.out}`);
|
|
3454
3682
|
})
|
|
3455
3683
|
);
|
|
3456
|
-
var task = program.command("task").description("a task's shared files (the task is its first session's id)");
|
|
3684
|
+
var task = program.command("task").description("a task's shared files and open requests (the task is its first session's id)");
|
|
3457
3685
|
task.command("files <task>").description("list the task's current files").action(
|
|
3458
3686
|
run(async (taskId) => {
|
|
3459
3687
|
const list = await listTaskFiles(taskId);
|
|
@@ -3472,7 +3700,7 @@ task.command("get <task> <path>").description("read one task file; prints it, or
|
|
|
3472
3700
|
);
|
|
3473
3701
|
task.command("put <task> <path> <local>").description("write a local file to <path> in the task's files (every session of the task sees it)").action(
|
|
3474
3702
|
run(async (taskId, path, local) => {
|
|
3475
|
-
const { file, conflict } = await writeTaskFile(taskId, path,
|
|
3703
|
+
const { file, conflict } = await writeTaskFile(taskId, path, readFileSync11(local), mediaTypeFor(local));
|
|
3476
3704
|
console.log(`${renderTaskFile(file)}${conflict ? " (replaced a version you had not seen)" : ""}`);
|
|
3477
3705
|
})
|
|
3478
3706
|
);
|
|
@@ -3482,6 +3710,45 @@ task.command("rm <task> <path>").description("remove a path from the task's file
|
|
|
3482
3710
|
console.log(`Removed ${path}`);
|
|
3483
3711
|
})
|
|
3484
3712
|
);
|
|
3713
|
+
task.command("show <task>").description("the task's sessions and the requests waiting on you (questions, approvals, apps to connect)").option("--json", "print the task as JSON").action(
|
|
3714
|
+
run(async (taskId, opts) => {
|
|
3715
|
+
const found = await getTask(taskId);
|
|
3716
|
+
if (opts.json) {
|
|
3717
|
+
console.log(JSON.stringify(found));
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
for (const member of found.sessions ?? []) console.log(`${member.id} ${member.role}${member.status ? ` ${member.status}` : ""}`);
|
|
3721
|
+
if (!found.requests.length) console.log("no open requests");
|
|
3722
|
+
for (const request of found.requests) console.log(renderRequest(request));
|
|
3723
|
+
})
|
|
3724
|
+
);
|
|
3725
|
+
task.command("answer <task> <request>").description("answer an open request; the asking session continues with the answer").option("--option <id>", "the id of one of the request's options").option("--text <text>", "a free-text answer").option("--note <text>", "sent to the agent after the answer, in the same turn").action(
|
|
3726
|
+
run(async (taskId, requestId, opts) => {
|
|
3727
|
+
await answerRequest(taskId, requestId, opts);
|
|
3728
|
+
console.log(`Answered ${requestId}`);
|
|
3729
|
+
})
|
|
3730
|
+
);
|
|
3731
|
+
var skills = program.command("skills").description("the workspace's skill store: upload a skill, then attach it to an agent by id");
|
|
3732
|
+
skills.command("upload <dir>").description("upload a skill directory (SKILL.md at its root) as a new skill").option("--name <name>", "display name (default: the directory's name)").option("--json", "print the stored skill as JSON").action(
|
|
3733
|
+
run(async (dir, opts) => {
|
|
3734
|
+
const { skill } = await uploadSkill(dir, opts.name);
|
|
3735
|
+
if (opts.json) {
|
|
3736
|
+
console.log(JSON.stringify(skill));
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
console.log(`Uploaded ${skill.id} (${skill.displayName ?? dir}). Attach it in agent.toml:`);
|
|
3740
|
+
console.log(` [[skills]]
|
|
3741
|
+
type = "skill"
|
|
3742
|
+
skillId = "${skill.id}"`);
|
|
3743
|
+
})
|
|
3744
|
+
);
|
|
3745
|
+
skills.command("list").description("the workspace's stored skills").action(
|
|
3746
|
+
run(async () => {
|
|
3747
|
+
const list = await listSkills();
|
|
3748
|
+
if (!list.length) console.log("no skills");
|
|
3749
|
+
for (const skill of list) console.log(`${skill.id} ${skill.displayName ?? ""}`);
|
|
3750
|
+
})
|
|
3751
|
+
);
|
|
3485
3752
|
var artifact = program.command("artifact").description("artifacts: publish a task's files as a named, shareable result");
|
|
3486
3753
|
artifact.command("list").description("the workspace's artifacts, or one task's with --task").option("--task <task>", "only this task's artifacts").action(
|
|
3487
3754
|
run(async (opts) => {
|
|
@@ -3690,7 +3957,274 @@ program.command("config").description("show the CLI config location and current
|
|
|
3690
3957
|
console.log(`config: ${configPath()}`);
|
|
3691
3958
|
console.log(` apiBase: ${config.apiBase ?? "(default)"}`);
|
|
3692
3959
|
console.log(` token: ${config.token ? `${config.token.slice(0, 10)}\u2026` : "(none)"}`);
|
|
3693
|
-
console.log(` universe: ${config.currentUniverse ?? "(
|
|
3960
|
+
console.log(` universe: ${config.currentUniverse ?? "(default)"}`);
|
|
3961
|
+
});
|
|
3962
|
+
var harness = program.command("harness").description("workspace harnesses: init, verify, push");
|
|
3963
|
+
harness.command("init [dir]").description("write a harness.json profile skeleton in the given directory (default: .)").action(run(async (dir) => {
|
|
3964
|
+
harnessInit(dir ?? ".");
|
|
3965
|
+
}));
|
|
3966
|
+
harness.command("verify [dir]").description("validate harness.json against the profile schema; with --handle, also run conformance").option("--handle <@listing>", "catalog listing handle to run conformance against").action(run(async (dir, opts) => {
|
|
3967
|
+
await harnessVerify(dir ?? ".", { handle: opts.handle });
|
|
3968
|
+
}));
|
|
3969
|
+
harness.command("push [dir]").description("create or update the workspace harness from harness.json").option("--version <semver>", "override the version (default: the version in harness.json)").action(run(async (dir, opts) => {
|
|
3970
|
+
await harnessPush(dir ?? ".", { version: opts.version });
|
|
3971
|
+
}));
|
|
3972
|
+
harness.command("connect").description("run an ACP agent (the command after --) as a session's brain, over the platform relay").option("--session <id>", "the session to be the brain of (default: $AGENTSKY_HARNESS_RELAY_URL names it)").option("--token <token>", "the harness connect token, hct_\u2026 (default: $AGENTSKY_HARNESS_TOKEN)").option("--once", "exit when the connection ends instead of reconnecting").allowExcessArguments(true).action(run(async (opts) => {
|
|
3973
|
+
const { connectHarness, serveHarness, agentCommandFrom } = await import("./harness-connect-DUC7GIBL.js");
|
|
3974
|
+
const token = opts.token ?? process.env.AGENTSKY_HARNESS_TOKEN ?? "";
|
|
3975
|
+
const url = opts.session ? void 0 : process.env.AGENTSKY_HARNESS_RELAY_URL;
|
|
3976
|
+
const apiBase2 = (loadConfig().apiBase ?? "https://api.agentsky.dev").replace(/\/$/, "");
|
|
3977
|
+
const ac = new AbortController();
|
|
3978
|
+
process.on("SIGINT", () => ac.abort());
|
|
3979
|
+
process.on("SIGTERM", () => ac.abort());
|
|
3980
|
+
const options = { url, apiBase: apiBase2, session: opts.session, token, command: agentCommandFrom(process.argv), signal: ac.signal };
|
|
3981
|
+
process.exitCode = opts.once ? await connectHarness(options) : await serveHarness(options);
|
|
3982
|
+
}));
|
|
3983
|
+
var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
|
|
3984
|
+
traceCmd.command("start").description("start a trace: a grant for this machine, a root span, and the spool the hooks write to").option("--name <name>", "a name for the task").option("--source <source>", "how this machine reports itself", "claude-code").option("--hooks [settings]", "also install the Claude Code hooks (default: ./.claude/settings.local.json)").option("--capture <scopes>", "also send content, redacted on this machine first: any of prompts,tool_output,files").option("--file <path>", "a file the files scope may send (repeatable)", collect).option("--dir <path>", "a folder whose files the files scope may send when a file tool touches them (repeatable)", collect).action(run(async (opts) => {
|
|
3985
|
+
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-UJTFSXNM.js");
|
|
3986
|
+
if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
|
|
3987
|
+
const scopes = parseContentScopes(opts.capture);
|
|
3988
|
+
const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
|
|
3989
|
+
if ((chosen.files.length || chosen.dirs.length) && !scopes.includes("files")) throw new Error("--file and --dir need --capture files");
|
|
3990
|
+
if (scopes.includes("files") && !chosen.files.length && !chosen.dirs.length) throw new Error("--capture files needs at least one --file or --dir");
|
|
3991
|
+
const { trace } = await api("POST", "/traces", opts.name ? { name: opts.name } : {});
|
|
3992
|
+
const { grant, token } = await api("POST", `/traces/${trace.id}/grants`, {
|
|
3993
|
+
source: opts.source,
|
|
3994
|
+
...scopes.length ? { content_scopes: scopes } : {}
|
|
3995
|
+
});
|
|
3996
|
+
const root = `task:${trace.id}`;
|
|
3997
|
+
const state = {
|
|
3998
|
+
trace_id: trace.id,
|
|
3999
|
+
grant_id: grant.id,
|
|
4000
|
+
token,
|
|
4001
|
+
source: opts.source,
|
|
4002
|
+
api_base: apiBase(),
|
|
4003
|
+
next_sequence: 0,
|
|
4004
|
+
root_span: root,
|
|
4005
|
+
...scopes.length ? { content_scopes: scopes, files: chosen.files, dirs: chosen.dirs } : {}
|
|
4006
|
+
};
|
|
4007
|
+
writeState(state);
|
|
4008
|
+
record(() => [
|
|
4009
|
+
{ span_id: root, parent_span_id: null, kind: "start", name: opts.name ?? "task" },
|
|
4010
|
+
...chosen.files.flatMap((path) => fileEvent(root, path, state) ?? [])
|
|
4011
|
+
]);
|
|
4012
|
+
console.log(`Tracing ${trace.id} as ${opts.source} until ${grant.expires_at}`);
|
|
4013
|
+
console.log(scopes.length ? ` content: ${scopes.join(", ")} (redacted here before it leaves)` : " metadata only");
|
|
4014
|
+
if (opts.hooks) console.log(` hooks: ${installClaudeHooks(resolve6(typeof opts.hooks === "string" ? opts.hooks : ".claude/settings.local.json"))}`);
|
|
4015
|
+
console.log(` a session started elsewhere joins it with metadata {"trace_id": "${trace.id}"}`);
|
|
4016
|
+
console.log(` sky trace show ${trace.id}`);
|
|
4017
|
+
}));
|
|
4018
|
+
traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
|
|
4019
|
+
try {
|
|
4020
|
+
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-UJTFSXNM.js");
|
|
4021
|
+
const chunks = [];
|
|
4022
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
4023
|
+
const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
4024
|
+
if (!record((state2) => [...eventsForClaudeHook(input, state2), ...contentForClaudeHook(input, state2)])) return;
|
|
4025
|
+
const state = readState();
|
|
4026
|
+
if (!state) return;
|
|
4027
|
+
const outcome = await flush(grantSender(state, 3e3));
|
|
4028
|
+
if (outcome.stopped === "grant_revoked" || outcome.stopped === "grant_expired") clearState();
|
|
4029
|
+
} catch {
|
|
4030
|
+
}
|
|
4031
|
+
});
|
|
4032
|
+
traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
|
|
4033
|
+
const { readState, flush, grantSender } = await import("./trace-UJTFSXNM.js");
|
|
4034
|
+
const state = readState();
|
|
4035
|
+
if (!state) throw new Error("no trace is running here");
|
|
4036
|
+
const outcome = await flush(grantSender(state), { attempts: 4 });
|
|
4037
|
+
console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
|
|
4038
|
+
}));
|
|
4039
|
+
traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
|
|
4040
|
+
const { readState, spoolStatus } = await import("./trace-UJTFSXNM.js");
|
|
4041
|
+
const state = readState();
|
|
4042
|
+
if (!state) {
|
|
4043
|
+
console.log("No trace is running here.");
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
const spool = spoolStatus();
|
|
4047
|
+
console.log(`${state.trace_id} as ${state.source}: ${spool.pending} pending, ${spool.dropped_full} dropped (spool full), ${spool.dropped_expired} given up, ${spool.refused} refused`);
|
|
4048
|
+
}));
|
|
4049
|
+
traceCmd.command("show [id]").description("the trace bundle: spans, the sessions they reach, cost counted once per session").option("--json", "print the bundle as JSON").action(run(async (id, opts) => {
|
|
4050
|
+
const { readState } = await import("./trace-UJTFSXNM.js");
|
|
4051
|
+
const ref = id ?? readState()?.trace_id;
|
|
4052
|
+
if (!ref) throw new Error("name a trace, or start one here");
|
|
4053
|
+
const bundle = await api("GET", `/traces/${ref}`);
|
|
4054
|
+
if (opts.json) {
|
|
4055
|
+
console.log(JSON.stringify(bundle, null, 2));
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
console.log(`${bundle.trace.id}${bundle.trace.name ? ` ${bundle.trace.name}` : ""}`);
|
|
4059
|
+
const children = /* @__PURE__ */ new Map();
|
|
4060
|
+
for (const span of bundle.spans) {
|
|
4061
|
+
const parent = span.parent_span_id && bundle.spans.some((other) => other.span_id === span.parent_span_id) ? span.parent_span_id : null;
|
|
4062
|
+
children.set(parent, [...children.get(parent) ?? [], span]);
|
|
4063
|
+
}
|
|
4064
|
+
const print = (parent, depth) => {
|
|
4065
|
+
for (const span of children.get(parent) ?? []) {
|
|
4066
|
+
const withContent = span.events.filter((event) => event.content).length;
|
|
4067
|
+
console.log(`${" ".repeat(depth + 1)}${span.name ?? span.span_id} [${span.source} \xB7 ${span.outcome}${span.outcome_source === "platform" ? " with its session" : ""}${span.edge === "root" || span.edge === "resolved" ? "" : ` \xB7 parent ${span.edge}`}] ${span.events.length} events${withContent ? `, ${withContent} with content` : ""}`);
|
|
4068
|
+
print(span.span_id, depth + 1);
|
|
4069
|
+
}
|
|
4070
|
+
};
|
|
4071
|
+
print(null, 0);
|
|
4072
|
+
for (const session2 of bundle.sessions) console.log(` session ${session2.id} ${session2.agent_type} ${session2.status} ${session2.role} of ${session2.task_id} (${session2.linked_by})`);
|
|
4073
|
+
console.log(` cost: $${bundle.consumed_cost.amount} \xB7 coverage: ${bundle.coverage}`);
|
|
4074
|
+
for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
|
|
4075
|
+
}));
|
|
4076
|
+
traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
|
|
4077
|
+
const { readState, record, flush, grantSender, clearState } = await import("./trace-UJTFSXNM.js");
|
|
4078
|
+
const state = readState();
|
|
4079
|
+
if (!state) throw new Error("no trace is running here");
|
|
4080
|
+
if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
|
|
4081
|
+
const outcome = await flush(grantSender(readState() ?? state), { attempts: 4 });
|
|
4082
|
+
await api("DELETE", `/traces/${state.trace_id}/grants/${state.grant_id}`);
|
|
4083
|
+
clearState();
|
|
4084
|
+
console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
|
|
4085
|
+
}));
|
|
4086
|
+
traceCmd.command("sync [payload]").description("upload this machine's Claude Code and Codex sessions to the matching workspace's trace library, redacted here first").option("--enable", "turn sync on for this machine").option("--disable", "turn sync off; nothing more is uploaded").option("--route <match=workspace>", "send sessions whose folder path contains <match> to <workspace> (repeatable; replaces the saved rules)", collect).option("--default <workspace>", "where a session no rule matches goes").option("--keep-local", "keep sessions no rule matches on this machine (the default)").option("--session <ref>", "only this session: its id or transcript path (repeatable)", collect).option("--full", "re-read every session, not only files that changed").option("--limit <n>", "at most this many changed sessions this run", (v) => Number.parseInt(v, 10)).option("--dry-run", "show what would be uploaded without uploading").option("--hook", "run from a Claude Code SessionEnd hook or Codex notify: upload that session in the background and return at once").option("--install-schedule", "also sweep every 15 minutes (macOS launchd)").option("--status", "what sync has done on this machine").option("--quiet", "print only failures").action(async (payload, opts) => {
|
|
4087
|
+
const lib = await import("./trace-library-474QJTAZ.js");
|
|
4088
|
+
if (opts.hook) {
|
|
4089
|
+
try {
|
|
4090
|
+
let raw = payload ?? "";
|
|
4091
|
+
if (!raw && !process.stdin.isTTY) {
|
|
4092
|
+
const chunks = [];
|
|
4093
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
4094
|
+
raw = Buffer.concat(chunks).toString("utf8");
|
|
4095
|
+
}
|
|
4096
|
+
if (!lib.loadSyncConfig().enabled) return;
|
|
4097
|
+
const ref = lib.hookSessionRef(raw);
|
|
4098
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
4099
|
+
spawn4(process.execPath, [process.argv[1], "trace", "sync", "--quiet", ...ref ? ["--session", ref] : []], { detached: true, stdio: "ignore" }).unref();
|
|
4100
|
+
} catch {
|
|
4101
|
+
}
|
|
4102
|
+
return;
|
|
4103
|
+
}
|
|
4104
|
+
await run(async () => {
|
|
4105
|
+
const config = lib.loadSyncConfig();
|
|
4106
|
+
if (opts.disable) {
|
|
4107
|
+
lib.saveSyncConfig({ ...config, enabled: false });
|
|
4108
|
+
console.log("Trace sync is off on this machine. Uploaded traces stay in their workspaces until deleted there.");
|
|
4109
|
+
return;
|
|
4110
|
+
}
|
|
4111
|
+
const changedSettings = opts.enable || opts.route || opts.default || opts.keepLocal;
|
|
4112
|
+
if (changedSettings) {
|
|
4113
|
+
if (opts.enable && !apiToken()) throw new Error("not logged in \u2014 run `sky auth login` first");
|
|
4114
|
+
const rules = opts.route ? opts.route.map((spec) => {
|
|
4115
|
+
const at = spec.lastIndexOf("=");
|
|
4116
|
+
if (at <= 0 || at === spec.length - 1) throw new Error(`--route takes match=workspace, got "${spec}"`);
|
|
4117
|
+
return { match: spec.slice(0, at), workspace: spec.slice(at + 1) };
|
|
4118
|
+
}) : config.rules;
|
|
4119
|
+
const next = {
|
|
4120
|
+
...config,
|
|
4121
|
+
rules,
|
|
4122
|
+
enabled: opts.enable ? true : config.enabled,
|
|
4123
|
+
enabledAt: opts.enable ? (/* @__PURE__ */ new Date()).toISOString() : config.enabledAt,
|
|
4124
|
+
fallback: opts.keepLocal ? void 0 : opts.default ?? config.fallback
|
|
4125
|
+
};
|
|
4126
|
+
lib.saveSyncConfig(next);
|
|
4127
|
+
console.log(`Trace sync is ${next.enabled ? "on" : "off"} on this machine. It reads only:`);
|
|
4128
|
+
for (const folder of lib.sourceFolders()) console.log(` ${folder}`);
|
|
4129
|
+
console.log("and sends each session, redacted here first, to:");
|
|
4130
|
+
for (const rule of next.rules) console.log(` folder path contains "${rule.match}" \u2192 ${rule.workspace}`);
|
|
4131
|
+
console.log(` anything else \u2192 ${next.fallback ?? "stays on this machine"}`);
|
|
4132
|
+
console.log(`A repository's .agentsky.json "workspace" wins over these rules.`);
|
|
4133
|
+
}
|
|
4134
|
+
if (opts.installSchedule) {
|
|
4135
|
+
const { join: join10 } = await import("path");
|
|
4136
|
+
const { homedir: homedir6 } = await import("os");
|
|
4137
|
+
const { execFileSync } = await import("child_process");
|
|
4138
|
+
const log = join10(homedir6(), ".config", "sky", "trace-sync.log");
|
|
4139
|
+
const plist = lib.installLaunchd([process.execPath, process.argv[1], "trace", "sync", "--quiet"], log);
|
|
4140
|
+
try {
|
|
4141
|
+
execFileSync("launchctl", ["unload", plist], { stdio: "ignore" });
|
|
4142
|
+
} catch {
|
|
4143
|
+
}
|
|
4144
|
+
execFileSync("launchctl", ["load", plist]);
|
|
4145
|
+
console.log(`Sweeping every 15 minutes (${plist}); a full re-read runs weekly. Log: ${log}`);
|
|
4146
|
+
}
|
|
4147
|
+
if (opts.status) {
|
|
4148
|
+
const { readFileSync: readFileSync12 } = await import("fs");
|
|
4149
|
+
const { join: join10 } = await import("path");
|
|
4150
|
+
let state = { sessions: {} };
|
|
4151
|
+
try {
|
|
4152
|
+
state = JSON.parse(readFileSync12(join10(lib.syncConfigPath(), "..", "trace-sync-state.json"), "utf8"));
|
|
4153
|
+
} catch {
|
|
4154
|
+
}
|
|
4155
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4156
|
+
for (const entry2 of Object.values(state.sessions)) {
|
|
4157
|
+
const label = entry2.status === "stored" ? `stored in ${entry2.workspace}` : entry2.status;
|
|
4158
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
4159
|
+
}
|
|
4160
|
+
console.log(`Trace sync is ${config.enabled ? "on" : "off"}${config.enabledAt ? ` (since ${config.enabledAt})` : ""}. Last full sweep: ${state.lastFullAt ?? "never"}.`);
|
|
4161
|
+
for (const [label, count] of [...counts].sort((a, b) => b[1] - a[1])) console.log(` ${count} ${label}`);
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4164
|
+
if (changedSettings && !opts.full && !opts.session) return;
|
|
4165
|
+
const report = await lib.syncTraces({
|
|
4166
|
+
full: opts.full,
|
|
4167
|
+
only: opts.session,
|
|
4168
|
+
dryRun: opts.dryRun,
|
|
4169
|
+
limit: opts.limit,
|
|
4170
|
+
onProgress: opts.quiet ? void 0 : (line) => console.log(line)
|
|
4171
|
+
});
|
|
4172
|
+
if (!report) {
|
|
4173
|
+
if (!opts.quiet) console.log("Another sync is running on this machine; it will pick these up.");
|
|
4174
|
+
return;
|
|
4175
|
+
}
|
|
4176
|
+
if (!opts.quiet || report.failed) {
|
|
4177
|
+
const where = Object.entries(report.byWorkspace).map(([workspace2, count]) => `${count} \u2192 ${workspace2}`).join(", ");
|
|
4178
|
+
console.log(`${opts.dryRun ? "Would upload" : "Uploaded"} ${report.uploaded}${where ? ` (${where})` : ""}; unchanged ${report.unchanged}; kept local ${report.local}; empty ${report.empty}; another member's ${report.owned}; failed ${report.failed}. Scanned ${report.scanned}, ${report.changed} changed.`);
|
|
4179
|
+
for (const error of report.errors.slice(0, 20)) console.error(` ${error}`);
|
|
4180
|
+
}
|
|
4181
|
+
if (report.failed) process.exitCode = 1;
|
|
4182
|
+
})();
|
|
4183
|
+
});
|
|
4184
|
+
traceCmd.command("search <query...>").description("search every session in a workspace's trace library (literal substring)").option("--workspace <slug>", "the workspace to search (default: the current one)").option("--limit <n>", "hits per page", (v) => Number.parseInt(v, 10), 20).option("--cursor <cursor>", "the next page of an earlier search").option("--json", "print the page as JSON").action(run(async (words, opts) => {
|
|
4185
|
+
const { libraryRequest } = await import("./trace-library-474QJTAZ.js");
|
|
4186
|
+
const workspace2 = opts.workspace ?? currentUniverse();
|
|
4187
|
+
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
4188
|
+
const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
|
|
4189
|
+
const page = await libraryRequest("GET", `/context/search?${params}`, workspace2);
|
|
4190
|
+
if (opts.json) {
|
|
4191
|
+
console.log(JSON.stringify(page, null, 2));
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
if (!page.hits.length) console.log(page.partial ? `No hits, but the search was cut short (${page.coverage}).` : "No hits.");
|
|
4195
|
+
for (const { hit } of page.hits) {
|
|
4196
|
+
console.log(`${hit.occurredAt?.slice(0, 16).replace("T", " ") ?? " "} ${hit.title}`);
|
|
4197
|
+
console.log(` ${hit.snippet.replace(/\s+/g, " ")}`);
|
|
4198
|
+
}
|
|
4199
|
+
if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
|
|
4200
|
+
}));
|
|
4201
|
+
traceCmd.command("library").description("list the sessions in a workspace's trace library, most recently updated first").option("--workspace <slug>", "the workspace (default: the current one)").option("--limit <n>", "how many", (v) => Number.parseInt(v, 10), 20).option("--json", "print the page as JSON").action(run(async (opts) => {
|
|
4202
|
+
const { libraryRequest } = await import("./trace-library-474QJTAZ.js");
|
|
4203
|
+
const workspace2 = opts.workspace ?? currentUniverse();
|
|
4204
|
+
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
4205
|
+
const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);
|
|
4206
|
+
if (opts.json) {
|
|
4207
|
+
console.log(JSON.stringify(page, null, 2));
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
console.log(`${page.total} sessions in ${workspace2}'s trace library`);
|
|
4211
|
+
for (const trace of page.traces) {
|
|
4212
|
+
console.log(`${trace.updated_at.slice(0, 16).replace("T", " ")} ${trace.harness.padEnd(11)} ${(trace.project ?? "").padEnd(16)} ${String(trace.messages).padStart(5)} msgs ${trace.title ?? trace.session_id}`);
|
|
4213
|
+
}
|
|
4214
|
+
}));
|
|
4215
|
+
var workerCmd = program.command("worker").description("run a pod on this machine and connect it to a worker pool");
|
|
4216
|
+
workerCmd.command("start <pool>").description("start a worker connected to the given pool (wpool_\u2026 id)").option("--token <token>", "bearer token (default: $SKY_WORKER_TOKEN)").option("--label <label>", "a human-readable name for this machine", "cli").action(async (poolId, options) => {
|
|
4217
|
+
const { startWorker } = await import("./worker-6Q4K4HZW.js");
|
|
4218
|
+
const token = options.token ?? process.env.SKY_WORKER_TOKEN ?? "";
|
|
4219
|
+
if (!token) {
|
|
4220
|
+
console.error("error: provide --token or set SKY_WORKER_TOKEN");
|
|
4221
|
+
process.exit(1);
|
|
4222
|
+
}
|
|
4223
|
+
const base = (loadConfig().apiBase ?? "https://api.agentsky.dev").replace(/\/$/, "");
|
|
4224
|
+
const ac = new AbortController();
|
|
4225
|
+
process.on("SIGINT", () => ac.abort());
|
|
4226
|
+
process.on("SIGTERM", () => ac.abort());
|
|
4227
|
+
await startWorker({ poolId, token, label: options.label, apiBase: base, signal: ac.signal });
|
|
3694
4228
|
});
|
|
3695
4229
|
var ARGUMENTS_FOLLOW_THE_SCRIPT_PATH_ON_EVERY_RUNTIME = { from: "node" };
|
|
3696
4230
|
program.parseAsync(process.argv, ARGUMENTS_FOLLOW_THE_SCRIPT_PATH_ON_EVERY_RUNTIME).catch(fail);
|