@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
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApiError
|
|
3
|
+
} from "./chunk-7LUCHBX6.js";
|
|
4
|
+
import {
|
|
5
|
+
USER_AGENT,
|
|
6
|
+
apiBase,
|
|
7
|
+
apiToken
|
|
8
|
+
} from "./chunk-RF5JICNX.js";
|
|
9
|
+
import {
|
|
10
|
+
redactTraceText
|
|
11
|
+
} from "./chunk-XXDQKVPS.js";
|
|
12
|
+
import "./chunk-MLKGABMK.js";
|
|
13
|
+
|
|
14
|
+
// src/trace-library.ts
|
|
15
|
+
import { createHash } from "crypto";
|
|
16
|
+
import { createReadStream, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
17
|
+
import { createInterface } from "readline";
|
|
18
|
+
import { homedir, hostname } from "os";
|
|
19
|
+
import { basename, dirname, join, sep } from "path";
|
|
20
|
+
import { createGunzip } from "zlib";
|
|
21
|
+
var MESSAGE_MAX_BYTES = 8 * 1024;
|
|
22
|
+
var TOOL_MAX_BYTES = 2 * 1024;
|
|
23
|
+
var MESSAGES_MAX = 2e4;
|
|
24
|
+
var BODY_MAX_BYTES = 7 * 1024 * 1024;
|
|
25
|
+
var CHECK_BATCH = 200;
|
|
26
|
+
var UPLOAD_CONCURRENCY = 4;
|
|
27
|
+
var FULL_SWEEP_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
28
|
+
var RETRIES = 3;
|
|
29
|
+
var RATE_LIMIT_RETRIES = 30;
|
|
30
|
+
var HARNESS_PREAMBLES = ["<environment_context>", "<user_instructions>", "<permissions instructions>", "# AGENTS.md instructions", "<INSTRUCTIONS>", "<external_codex_apps_open_page>"];
|
|
31
|
+
var titleOf = (text) => /^\s*</.test(text) ? void 0 : text.trim().slice(0, 120) || void 0;
|
|
32
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
33
|
+
var backoffMs = (attempt) => Math.min(3e4, 1e3 * 2 ** attempt);
|
|
34
|
+
function skyDir() {
|
|
35
|
+
return process.env.SKY_CONFIG_DIR ?? join(homedir(), ".config", "sky");
|
|
36
|
+
}
|
|
37
|
+
function syncConfigPath() {
|
|
38
|
+
return join(skyDir(), "trace-sync.json");
|
|
39
|
+
}
|
|
40
|
+
function statePath() {
|
|
41
|
+
return join(skyDir(), "trace-sync-state.json");
|
|
42
|
+
}
|
|
43
|
+
function readJson(path, fallback) {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
46
|
+
} catch {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function writeJson(path, value) {
|
|
51
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
52
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
53
|
+
`, { mode: 384 });
|
|
54
|
+
}
|
|
55
|
+
function loadSyncConfig() {
|
|
56
|
+
return readJson(syncConfigPath(), { enabled: false, rules: [] });
|
|
57
|
+
}
|
|
58
|
+
function saveSyncConfig(config) {
|
|
59
|
+
writeJson(syncConfigPath(), config);
|
|
60
|
+
}
|
|
61
|
+
function claudeRoot() {
|
|
62
|
+
return join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"), "projects");
|
|
63
|
+
}
|
|
64
|
+
function codexRoot() {
|
|
65
|
+
return process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
66
|
+
}
|
|
67
|
+
function sourceFolders() {
|
|
68
|
+
return [claudeRoot(), join(codexRoot(), "sessions"), join(codexRoot(), "archived_sessions")];
|
|
69
|
+
}
|
|
70
|
+
function signatureOf(files) {
|
|
71
|
+
const parts = [];
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
try {
|
|
74
|
+
const stat = statSync(file);
|
|
75
|
+
parts.push(`${file}:${stat.size}:${Math.floor(stat.mtimeMs)}`);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return parts.join("|");
|
|
81
|
+
}
|
|
82
|
+
function claudeSessions() {
|
|
83
|
+
const root = claudeRoot();
|
|
84
|
+
const found = [];
|
|
85
|
+
let projects;
|
|
86
|
+
try {
|
|
87
|
+
projects = readdirSync(root);
|
|
88
|
+
} catch {
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
for (const project of projects) {
|
|
92
|
+
const dir = join(root, project);
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = readdirSync(dir);
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
101
|
+
const sessionId = entry.slice(0, -".jsonl".length);
|
|
102
|
+
const files = [join(dir, entry)];
|
|
103
|
+
const subagents = join(dir, sessionId, "subagents");
|
|
104
|
+
try {
|
|
105
|
+
files.push(...readdirSync(subagents).filter((name) => name.endsWith(".jsonl")).sort().map((name) => join(subagents, name)));
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
const signature = signatureOf(files);
|
|
109
|
+
if (signature) found.push({ harness: "claude_code", sessionId, files, signature });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return found;
|
|
113
|
+
}
|
|
114
|
+
async function* readLines(file) {
|
|
115
|
+
const raw = createReadStream(file);
|
|
116
|
+
const input = file.endsWith(".gz") ? raw.pipe(createGunzip()) : raw;
|
|
117
|
+
const reader = createInterface({ input, crlfDelay: Infinity });
|
|
118
|
+
try {
|
|
119
|
+
for await (const line of reader) yield line;
|
|
120
|
+
} finally {
|
|
121
|
+
reader.close();
|
|
122
|
+
raw.destroy();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function firstLine(file) {
|
|
126
|
+
for await (const line of readLines(file)) return line.length > 1024 * 1024 ? "" : line;
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
function codexIdCachePath() {
|
|
130
|
+
return join(skyDir(), "trace-sync-codex-ids.json");
|
|
131
|
+
}
|
|
132
|
+
async function codexSessions() {
|
|
133
|
+
const byId = /* @__PURE__ */ new Map();
|
|
134
|
+
const cached = readJson(codexIdCachePath(), {});
|
|
135
|
+
const seen = {};
|
|
136
|
+
const walk = async (dir) => {
|
|
137
|
+
let entries;
|
|
138
|
+
try {
|
|
139
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
140
|
+
} catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const full = join(dir, entry.name);
|
|
145
|
+
if (entry.isDirectory()) {
|
|
146
|
+
await walk(full);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (!entry.name.startsWith("rollout-") || !/\.jsonl(\.gz)?$/.test(entry.name)) continue;
|
|
150
|
+
const signature = signatureOf([full]);
|
|
151
|
+
if (!signature) continue;
|
|
152
|
+
let id = cached[full]?.signature === signature ? cached[full].id : void 0;
|
|
153
|
+
if (!id) {
|
|
154
|
+
try {
|
|
155
|
+
const meta = JSON.parse(await firstLine(full));
|
|
156
|
+
if (meta?.type === "session_meta" && typeof meta.payload?.id === "string") id = meta.payload.id;
|
|
157
|
+
} catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!id) continue;
|
|
162
|
+
seen[full] = { signature, id };
|
|
163
|
+
const mtime = statSync(full).mtimeMs;
|
|
164
|
+
const known = byId.get(id);
|
|
165
|
+
if (!known || mtime > known.mtime) byId.set(id, { harness: "codex", sessionId: id, files: [full], signature, mtime });
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
await walk(join(codexRoot(), "sessions"));
|
|
169
|
+
await walk(join(codexRoot(), "archived_sessions"));
|
|
170
|
+
try {
|
|
171
|
+
writeJson(codexIdCachePath(), seen);
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
return [...byId.values()].map(({ mtime: _mtime, ...session }) => session);
|
|
175
|
+
}
|
|
176
|
+
function claudeSessionAt(path) {
|
|
177
|
+
const root = claudeRoot();
|
|
178
|
+
if (!path.startsWith(`${root}${sep}`) || !path.endsWith(".jsonl") || basename(dirname(dirname(path))) !== basename(root)) return null;
|
|
179
|
+
const sessionId = basename(path, ".jsonl");
|
|
180
|
+
const files = [path];
|
|
181
|
+
const subagents = join(dirname(path), sessionId, "subagents");
|
|
182
|
+
try {
|
|
183
|
+
files.push(...readdirSync(subagents).filter((name) => name.endsWith(".jsonl")).sort().map((name) => join(subagents, name)));
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
const signature = signatureOf(files);
|
|
187
|
+
return signature ? { harness: "claude_code", sessionId, files, signature } : null;
|
|
188
|
+
}
|
|
189
|
+
async function discoverSessions() {
|
|
190
|
+
return [...claudeSessions(), ...await codexSessions()];
|
|
191
|
+
}
|
|
192
|
+
function asRecord(value) {
|
|
193
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
194
|
+
}
|
|
195
|
+
function str(value) {
|
|
196
|
+
return typeof value === "string" && value.length ? value : void 0;
|
|
197
|
+
}
|
|
198
|
+
function toolResultText(content) {
|
|
199
|
+
if (typeof content === "string") return content;
|
|
200
|
+
if (Array.isArray(content)) return content.map((part) => str(asRecord(part)?.text) ?? "").filter(Boolean).join("\n");
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
function stringify(value) {
|
|
204
|
+
if (typeof value === "string") return value;
|
|
205
|
+
try {
|
|
206
|
+
return JSON.stringify(value);
|
|
207
|
+
} catch {
|
|
208
|
+
return "";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
var Collector = class {
|
|
212
|
+
messages = [];
|
|
213
|
+
partial = false;
|
|
214
|
+
bytes = 0;
|
|
215
|
+
push(role, text, at) {
|
|
216
|
+
const trimmed = text.replace(/\u0000/g, "").trim();
|
|
217
|
+
if (!trimmed) return;
|
|
218
|
+
if (this.messages.length >= MESSAGES_MAX) {
|
|
219
|
+
this.partial = true;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const safe = redactTraceText(trimmed, role === "tool" ? TOOL_MAX_BYTES : MESSAGE_MAX_BYTES);
|
|
223
|
+
if (safe.truncated) this.partial = true;
|
|
224
|
+
this.bytes += Buffer.byteLength(safe.text) + 80;
|
|
225
|
+
this.messages.push({ role, text: safe.text, ...at ? { at } : {} });
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
function isoOrUndefined(value) {
|
|
229
|
+
const text = str(value);
|
|
230
|
+
if (!text) return void 0;
|
|
231
|
+
const time = Date.parse(text);
|
|
232
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : void 0;
|
|
233
|
+
}
|
|
234
|
+
async function parseClaudeSession(session) {
|
|
235
|
+
const out = { harness: "claude_code", sessionId: session.sessionId, userTurns: 0, toolCalls: 0, partial: false, messages: [] };
|
|
236
|
+
const collector = new Collector();
|
|
237
|
+
for (const [index, file] of session.files.entries()) {
|
|
238
|
+
for await (const line of readLines(file)) {
|
|
239
|
+
if (!line.trim()) continue;
|
|
240
|
+
let record;
|
|
241
|
+
try {
|
|
242
|
+
record = asRecord(JSON.parse(line));
|
|
243
|
+
} catch {
|
|
244
|
+
out.partial = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (!record) continue;
|
|
248
|
+
const at = isoOrUndefined(record.timestamp);
|
|
249
|
+
if (index === 0) {
|
|
250
|
+
out.cwd ??= str(record.cwd);
|
|
251
|
+
out.gitBranch ??= str(record.gitBranch);
|
|
252
|
+
out.harnessVersion ??= str(record.version);
|
|
253
|
+
if (at) {
|
|
254
|
+
out.startedAt ??= at;
|
|
255
|
+
out.endedAt = at;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const message = asRecord(record.message);
|
|
259
|
+
if (!message || record.isMeta === true) continue;
|
|
260
|
+
if (record.type === "user") {
|
|
261
|
+
const content = message.content;
|
|
262
|
+
if (typeof content === "string") {
|
|
263
|
+
collector.push("user", content, at);
|
|
264
|
+
if (index === 0) out.userTurns += 1;
|
|
265
|
+
if (index === 0) out.title ??= titleOf(content);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (!Array.isArray(content)) continue;
|
|
269
|
+
let typed = false;
|
|
270
|
+
for (const part of content.map(asRecord)) {
|
|
271
|
+
if (part?.type === "text" && str(part.text)) {
|
|
272
|
+
collector.push("user", part.text, at);
|
|
273
|
+
typed = true;
|
|
274
|
+
if (index === 0) out.title ??= titleOf(part.text);
|
|
275
|
+
} else if (part?.type === "tool_result") {
|
|
276
|
+
collector.push("tool", toolResultText(part.content), at);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (typed && index === 0) out.userTurns += 1;
|
|
280
|
+
} else if (record.type === "assistant") {
|
|
281
|
+
if (index === 0) out.model ??= str(message.model);
|
|
282
|
+
const content = Array.isArray(message.content) ? message.content.map(asRecord) : [];
|
|
283
|
+
for (const part of content) {
|
|
284
|
+
if (part?.type === "text") collector.push("assistant", str(part.text) ?? "", at);
|
|
285
|
+
else if (part?.type === "tool_use") {
|
|
286
|
+
out.toolCalls += 1;
|
|
287
|
+
collector.push("tool", `${str(part.name) ?? "tool"} ${stringify(part.input)}`, at);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return finish(out, collector);
|
|
294
|
+
}
|
|
295
|
+
async function parseCodexSession(session) {
|
|
296
|
+
const out = { harness: "codex", sessionId: session.sessionId, userTurns: 0, toolCalls: 0, partial: false, messages: [] };
|
|
297
|
+
const collector = new Collector();
|
|
298
|
+
for await (const line of readLines(session.files[0])) {
|
|
299
|
+
if (!line.trim()) continue;
|
|
300
|
+
let record;
|
|
301
|
+
try {
|
|
302
|
+
record = asRecord(JSON.parse(line));
|
|
303
|
+
} catch {
|
|
304
|
+
out.partial = true;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const payload = asRecord(record?.payload);
|
|
308
|
+
if (!record || !payload) continue;
|
|
309
|
+
const at = isoOrUndefined(record.timestamp);
|
|
310
|
+
if (at) {
|
|
311
|
+
out.startedAt ??= at;
|
|
312
|
+
out.endedAt = at;
|
|
313
|
+
}
|
|
314
|
+
if (record.type === "session_meta") {
|
|
315
|
+
out.cwd ??= str(payload.cwd);
|
|
316
|
+
out.harnessVersion ??= str(payload.cli_version);
|
|
317
|
+
const git = asRecord(payload.git);
|
|
318
|
+
out.gitBranch ??= str(git?.branch);
|
|
319
|
+
out.gitCommit ??= str(git?.commit_hash);
|
|
320
|
+
} else if (record.type === "turn_context") {
|
|
321
|
+
out.model ??= str(payload.model);
|
|
322
|
+
out.cwd ??= str(payload.cwd);
|
|
323
|
+
} else if (record.type === "response_item") {
|
|
324
|
+
if (payload.type === "message") {
|
|
325
|
+
const role = payload.role === "user" ? "user" : payload.role === "assistant" ? "assistant" : null;
|
|
326
|
+
if (!role) continue;
|
|
327
|
+
const text = (Array.isArray(payload.content) ? payload.content : []).map((part) => str(asRecord(part)?.text) ?? "").filter(Boolean).join("\n");
|
|
328
|
+
if (role === "user" && HARNESS_PREAMBLES.some((prefix) => text.trimStart().startsWith(prefix))) continue;
|
|
329
|
+
collector.push(role, text, at);
|
|
330
|
+
if (role === "user" && text.trim()) {
|
|
331
|
+
out.userTurns += 1;
|
|
332
|
+
out.title ??= titleOf(text);
|
|
333
|
+
}
|
|
334
|
+
} else if (payload.type === "function_call" || payload.type === "custom_tool_call" || payload.type === "local_shell_call") {
|
|
335
|
+
out.toolCalls += 1;
|
|
336
|
+
collector.push("tool", `${str(payload.name) ?? payload.type} ${stringify(payload.arguments ?? payload.input ?? payload.action)}`, at);
|
|
337
|
+
} else if (payload.type === "function_call_output" || payload.type === "custom_tool_call_output") {
|
|
338
|
+
const output = asRecord(payload.output);
|
|
339
|
+
collector.push("tool", str(output?.content) ?? stringify(payload.output), at);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return finish(out, collector);
|
|
344
|
+
}
|
|
345
|
+
function finish(out, collector) {
|
|
346
|
+
let messages = collector.messages;
|
|
347
|
+
let partial = out.partial || collector.partial;
|
|
348
|
+
let bytes = collector.bytes;
|
|
349
|
+
if (bytes > BODY_MAX_BYTES) {
|
|
350
|
+
messages = messages.map((message) => message.role === "tool" && message.text.length > 256 ? { ...message, text: `${message.text.slice(0, 256)}\u2026` } : message);
|
|
351
|
+
bytes = messages.reduce((sum, message) => sum + Buffer.byteLength(message.text) + 80, 0);
|
|
352
|
+
partial = true;
|
|
353
|
+
}
|
|
354
|
+
while (bytes > BODY_MAX_BYTES && messages.length) {
|
|
355
|
+
const dropped = messages.splice(Math.floor(messages.length / 2), Math.max(1, Math.floor(messages.length / 10)));
|
|
356
|
+
bytes -= dropped.reduce((sum, message) => sum + Buffer.byteLength(message.text) + 80, 0);
|
|
357
|
+
}
|
|
358
|
+
return { ...out, title: out.title ? redactTraceText(out.title, 300).text : void 0, partial, messages };
|
|
359
|
+
}
|
|
360
|
+
function isTrivial(session) {
|
|
361
|
+
if (!session.messages.length) return true;
|
|
362
|
+
const chars = session.messages.reduce((sum, message) => sum + message.text.length, 0);
|
|
363
|
+
return session.toolCalls === 0 && session.messages.length <= 2 && chars < 200;
|
|
364
|
+
}
|
|
365
|
+
function sessionDigest(session) {
|
|
366
|
+
const hash = createHash("sha256");
|
|
367
|
+
hash.update(`${session.cwd ?? ""}${session.model ?? ""}`);
|
|
368
|
+
for (const message of session.messages) hash.update(`${message.role}\0${message.text}`);
|
|
369
|
+
return hash.digest("hex");
|
|
370
|
+
}
|
|
371
|
+
function projectOf(cwd) {
|
|
372
|
+
const parts = cwd.split(sep).filter(Boolean);
|
|
373
|
+
for (const marker of [".worktrees", "worktrees"]) {
|
|
374
|
+
const at = parts.lastIndexOf(marker);
|
|
375
|
+
if (at > 0) {
|
|
376
|
+
const owner = parts[at - 1] === ".claude" ? at - 2 : at - 1;
|
|
377
|
+
if (owner >= 0) return parts[owner];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let dir = cwd;
|
|
381
|
+
while (dir && dir !== dirname(dir)) {
|
|
382
|
+
if (existsSync(join(dir, ".git"))) return basename(dir);
|
|
383
|
+
dir = dirname(dir);
|
|
384
|
+
}
|
|
385
|
+
return parts.at(-1);
|
|
386
|
+
}
|
|
387
|
+
function workspaceFile(cwd) {
|
|
388
|
+
let dir = cwd;
|
|
389
|
+
while (dir && dir !== dirname(dir)) {
|
|
390
|
+
const file = join(dir, ".agentsky.json");
|
|
391
|
+
if (existsSync(file)) {
|
|
392
|
+
const workspace = str(readJson(file, {}).workspace);
|
|
393
|
+
if (workspace) return workspace;
|
|
394
|
+
}
|
|
395
|
+
if (existsSync(join(dir, ".git"))) return void 0;
|
|
396
|
+
dir = dirname(dir);
|
|
397
|
+
}
|
|
398
|
+
return void 0;
|
|
399
|
+
}
|
|
400
|
+
function gitConfigPath(root) {
|
|
401
|
+
const dotGit = join(root, ".git");
|
|
402
|
+
try {
|
|
403
|
+
if (statSync(dotGit).isDirectory()) return join(dotGit, "config");
|
|
404
|
+
const pointer = readFileSync(dotGit, "utf8").match(/^gitdir:\s*(.+)$/m)?.[1]?.trim();
|
|
405
|
+
if (!pointer) return void 0;
|
|
406
|
+
const gitdir = pointer.startsWith("/") ? pointer : join(root, pointer);
|
|
407
|
+
const common = existsSync(join(gitdir, "commondir")) ? readFileSync(join(gitdir, "commondir"), "utf8").trim() : ".";
|
|
408
|
+
return join(common.startsWith("/") ? common : join(gitdir, common), "config");
|
|
409
|
+
} catch {
|
|
410
|
+
return void 0;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function repoRemote(cwd) {
|
|
414
|
+
let dir = cwd;
|
|
415
|
+
while (dir && dir !== dirname(dir)) {
|
|
416
|
+
if (existsSync(join(dir, ".git"))) {
|
|
417
|
+
const config = gitConfigPath(dir);
|
|
418
|
+
if (!config) return void 0;
|
|
419
|
+
try {
|
|
420
|
+
const text = readFileSync(config, "utf8");
|
|
421
|
+
return text.match(/\[remote "origin"\][^[]*?url\s*=\s*(\S+)/)?.[1];
|
|
422
|
+
} catch {
|
|
423
|
+
return void 0;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
dir = dirname(dir);
|
|
427
|
+
}
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
430
|
+
function routeSession(cwd, config) {
|
|
431
|
+
if (cwd) {
|
|
432
|
+
const pinned = workspaceFile(cwd);
|
|
433
|
+
if (pinned) return pinned;
|
|
434
|
+
const lower = `${cwd}
|
|
435
|
+
${repoRemote(cwd) ?? ""}`.toLowerCase();
|
|
436
|
+
const rule = config.rules.find((candidate) => lower.includes(candidate.match.toLowerCase()));
|
|
437
|
+
if (rule) return rule.workspace;
|
|
438
|
+
}
|
|
439
|
+
return config.fallback ?? null;
|
|
440
|
+
}
|
|
441
|
+
function homeLabel(path) {
|
|
442
|
+
if (!path) return void 0;
|
|
443
|
+
const home = homedir();
|
|
444
|
+
return path === home || path.startsWith(`${home}${sep}`) ? `~${path.slice(home.length)}` : path;
|
|
445
|
+
}
|
|
446
|
+
async function libraryRequest(method, path, workspace, body, timeoutMs = 12e4) {
|
|
447
|
+
const token = apiToken();
|
|
448
|
+
if (!token) throw new ApiError(401, "not_logged_in", "not logged in \u2014 run `sky auth login`");
|
|
449
|
+
const payload = body ? JSON.stringify(body) : void 0;
|
|
450
|
+
let res;
|
|
451
|
+
for (let attempt = 0; ; attempt++) {
|
|
452
|
+
try {
|
|
453
|
+
res = await fetch(`${apiBase()}${path}`, {
|
|
454
|
+
method,
|
|
455
|
+
headers: { "user-agent": USER_AGENT, authorization: `Bearer ${token}`, "x-universe": workspace, ...payload ? { "content-type": "application/json" } : {} },
|
|
456
|
+
body: payload,
|
|
457
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
458
|
+
});
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (attempt >= RETRIES) throw error;
|
|
461
|
+
await sleep(backoffMs(attempt));
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (res.status === 429 && attempt < RATE_LIMIT_RETRIES) {
|
|
465
|
+
const wait = Number(res.headers.get("retry-after"));
|
|
466
|
+
await sleep(Number.isFinite(wait) && wait > 0 ? Math.min(wait, 65) * 1e3 : backoffMs(attempt));
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (res.status >= 500 && res.status !== 503 && attempt < RETRIES) {
|
|
470
|
+
await sleep(backoffMs(attempt));
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
if (!res.ok) {
|
|
476
|
+
let code = "http_error";
|
|
477
|
+
let message = `${res.status} ${res.statusText}`;
|
|
478
|
+
try {
|
|
479
|
+
const parsed = await res.json();
|
|
480
|
+
code = parsed.error?.code ?? code;
|
|
481
|
+
message = parsed.error?.message ?? message;
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
throw new ApiError(res.status, code, message);
|
|
485
|
+
}
|
|
486
|
+
return await res.json();
|
|
487
|
+
}
|
|
488
|
+
function uploadBody(session, sha256, device) {
|
|
489
|
+
const cwd = homeLabel(session.cwd);
|
|
490
|
+
return {
|
|
491
|
+
sha256,
|
|
492
|
+
...session.title ? { title: session.title } : {},
|
|
493
|
+
...cwd ? { cwd: cwd.slice(0, 1024) } : {},
|
|
494
|
+
...session.cwd && projectOf(session.cwd) ? { project: projectOf(session.cwd).slice(0, 200) } : {},
|
|
495
|
+
...session.gitBranch ? { git_branch: session.gitBranch.slice(0, 300) } : {},
|
|
496
|
+
...session.gitCommit ? { git_commit: session.gitCommit.slice(0, 80) } : {},
|
|
497
|
+
...session.model ? { model: session.model.slice(0, 200) } : {},
|
|
498
|
+
...session.harnessVersion ? { harness_version: session.harnessVersion.slice(0, 80) } : {},
|
|
499
|
+
device: device.slice(0, 200),
|
|
500
|
+
...session.startedAt ? { started_at: session.startedAt } : {},
|
|
501
|
+
...session.endedAt ? { ended_at: session.endedAt } : {},
|
|
502
|
+
user_turns: session.userTurns,
|
|
503
|
+
tool_calls: session.toolCalls,
|
|
504
|
+
partial: session.partial,
|
|
505
|
+
messages: session.messages
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function lockPath() {
|
|
509
|
+
return join(skyDir(), "trace-sync.lock");
|
|
510
|
+
}
|
|
511
|
+
function takeLock() {
|
|
512
|
+
const path = lockPath();
|
|
513
|
+
mkdirSync(skyDir(), { recursive: true });
|
|
514
|
+
try {
|
|
515
|
+
mkdirSync(path);
|
|
516
|
+
} catch {
|
|
517
|
+
const pid = Number(readJson(join(path, "pid.json"), {}).pid);
|
|
518
|
+
let alive = false;
|
|
519
|
+
try {
|
|
520
|
+
if (pid) {
|
|
521
|
+
process.kill(pid, 0);
|
|
522
|
+
alive = true;
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
525
|
+
alive = false;
|
|
526
|
+
}
|
|
527
|
+
if (alive) return false;
|
|
528
|
+
rmSync(path, { recursive: true, force: true });
|
|
529
|
+
try {
|
|
530
|
+
mkdirSync(path);
|
|
531
|
+
} catch {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
writeJson(join(path, "pid.json"), { pid: process.pid });
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
function releaseLock() {
|
|
539
|
+
rmSync(lockPath(), { recursive: true, force: true });
|
|
540
|
+
}
|
|
541
|
+
async function pool(items, size, fn) {
|
|
542
|
+
let next = 0;
|
|
543
|
+
await Promise.all(Array.from({ length: Math.min(size, items.length) }, async () => {
|
|
544
|
+
while (next < items.length) await fn(items[next++]);
|
|
545
|
+
}));
|
|
546
|
+
}
|
|
547
|
+
async function syncTraces(options = {}) {
|
|
548
|
+
const config = loadSyncConfig();
|
|
549
|
+
if (!config.enabled) throw new Error("trace sync is off on this machine \u2014 turn it on with `sky trace sync --enable`");
|
|
550
|
+
if (!options.dryRun && !takeLock()) return null;
|
|
551
|
+
try {
|
|
552
|
+
return await runSync(config, options);
|
|
553
|
+
} finally {
|
|
554
|
+
if (!options.dryRun) releaseLock();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async function runSync(config, options) {
|
|
558
|
+
const state = readJson(statePath(), { sessions: {} });
|
|
559
|
+
const full = options.full || !state.lastFullAt || Date.now() - Date.parse(state.lastFullAt) > FULL_SWEEP_MS;
|
|
560
|
+
const device = config.device ?? hostname();
|
|
561
|
+
const report = { scanned: 0, changed: 0, uploaded: 0, unchanged: 0, local: 0, owned: 0, empty: 0, failed: 0, byWorkspace: {}, errors: [] };
|
|
562
|
+
const direct = options.only?.length ? options.only.map(claudeSessionAt) : [];
|
|
563
|
+
let sessions = direct.length && direct.every(Boolean) ? direct : await discoverSessions();
|
|
564
|
+
if (options.only?.length) {
|
|
565
|
+
const wanted = new Set(options.only);
|
|
566
|
+
sessions = sessions.filter((s) => wanted.has(s.sessionId) || s.files.some((file) => wanted.has(file)));
|
|
567
|
+
}
|
|
568
|
+
report.scanned = sessions.length;
|
|
569
|
+
const key = (s) => `${s.harness}:${s.sessionId}`;
|
|
570
|
+
let pending = sessions.filter((s) => full || state.sessions[key(s)]?.signature !== s.signature);
|
|
571
|
+
if (options.limit) pending = pending.slice(0, options.limit);
|
|
572
|
+
report.changed = pending.length;
|
|
573
|
+
const record = (s, value) => {
|
|
574
|
+
state.sessions[key(s)] = { signature: s.signature, at: (/* @__PURE__ */ new Date()).toISOString(), ...value };
|
|
575
|
+
};
|
|
576
|
+
const ready = /* @__PURE__ */ new Map();
|
|
577
|
+
for (const local of pending) {
|
|
578
|
+
let parsed;
|
|
579
|
+
try {
|
|
580
|
+
parsed = local.harness === "claude_code" ? await parseClaudeSession(local) : await parseCodexSession(local);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
report.failed += 1;
|
|
583
|
+
report.errors.push(`${key(local)}: unreadable (${error.message})`);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (isTrivial(parsed)) {
|
|
587
|
+
report.empty += 1;
|
|
588
|
+
record(local, { workspace: null, status: "empty" });
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const workspace = routeSession(parsed.cwd, config);
|
|
592
|
+
if (!workspace) {
|
|
593
|
+
report.local += 1;
|
|
594
|
+
record(local, { workspace: null, status: "local" });
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const sha256 = sessionDigest(parsed);
|
|
598
|
+
const previous = state.sessions[key(local)];
|
|
599
|
+
if (!full && previous?.sha256 === sha256 && previous.workspace === workspace && previous.status === "stored") {
|
|
600
|
+
report.unchanged += 1;
|
|
601
|
+
record(local, { workspace, sha256, status: "stored" });
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
ready.set(workspace, [...ready.get(workspace) ?? [], { local, parsed, sha256 }]);
|
|
605
|
+
}
|
|
606
|
+
for (const [workspace, items] of ready) {
|
|
607
|
+
for (let start = 0; start < items.length; start += CHECK_BATCH) {
|
|
608
|
+
const batch = items.slice(start, start + CHECK_BATCH);
|
|
609
|
+
let statuses;
|
|
610
|
+
try {
|
|
611
|
+
statuses = (await libraryRequest("POST", "/trace-library/check", workspace, {
|
|
612
|
+
sessions: batch.map((item) => ({ harness: item.parsed.harness, session_id: item.parsed.sessionId, sha256: item.sha256 }))
|
|
613
|
+
})).sessions;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
report.failed += batch.length;
|
|
616
|
+
report.errors.push(`${workspace}: check failed (${error.message})`);
|
|
617
|
+
if (error instanceof ApiError && (error.status === 401 || error.status === 403 || error.status === 503)) break;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const toSend = batch.filter((item, index) => {
|
|
621
|
+
const status = statuses[index]?.status;
|
|
622
|
+
if (status === "unchanged") {
|
|
623
|
+
report.unchanged += 1;
|
|
624
|
+
record(item.local, { workspace, sha256: item.sha256, status: "stored" });
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
if (status === "owned") {
|
|
628
|
+
report.owned += 1;
|
|
629
|
+
record(item.local, { workspace, sha256: item.sha256, status: "owned" });
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
return true;
|
|
633
|
+
});
|
|
634
|
+
if (options.dryRun) {
|
|
635
|
+
report.uploaded += toSend.length;
|
|
636
|
+
report.byWorkspace[workspace] = (report.byWorkspace[workspace] ?? 0) + toSend.length;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
await pool(toSend, UPLOAD_CONCURRENCY, async (item) => {
|
|
640
|
+
try {
|
|
641
|
+
await libraryRequest("PUT", `/trace-library/${item.parsed.harness}/${encodeURIComponent(item.parsed.sessionId)}`, workspace, uploadBody(item.parsed, item.sha256, device));
|
|
642
|
+
report.uploaded += 1;
|
|
643
|
+
report.byWorkspace[workspace] = (report.byWorkspace[workspace] ?? 0) + 1;
|
|
644
|
+
record(item.local, { workspace, sha256: item.sha256, status: "stored" });
|
|
645
|
+
} catch (error) {
|
|
646
|
+
report.failed += 1;
|
|
647
|
+
report.errors.push(`${key(item.local)} \u2192 ${workspace}: ${error.message}`);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
if (!options.dryRun) writeJson(statePath(), state);
|
|
651
|
+
options.onProgress?.(`${workspace}: ${Math.min(start + CHECK_BATCH, items.length)}/${items.length} checked, ${report.uploaded} uploaded so far`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (!options.dryRun) {
|
|
655
|
+
if (full && !options.only?.length && !options.limit && report.failed === 0) state.lastFullAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
656
|
+
writeJson(statePath(), state);
|
|
657
|
+
}
|
|
658
|
+
return report;
|
|
659
|
+
}
|
|
660
|
+
function hookSessionRef(payload) {
|
|
661
|
+
try {
|
|
662
|
+
const value = asRecord(JSON.parse(payload));
|
|
663
|
+
return str(value?.transcript_path) ?? str(value?.session_id) ?? str(value?.["thread-id"]) ?? str(value?.thread_id);
|
|
664
|
+
} catch {
|
|
665
|
+
return void 0;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
var LAUNCHD_LABEL = "dev.agentsky.trace-sync";
|
|
669
|
+
function installLaunchd(command, logPath) {
|
|
670
|
+
if (process.platform !== "darwin") throw new Error("the schedule installer covers macOS; on Linux add `*/15 * * * * sky trace sync --quiet` to your crontab");
|
|
671
|
+
const plist = join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
672
|
+
const escape = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
673
|
+
writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
|
|
674
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
675
|
+
<plist version="1.0">
|
|
676
|
+
<dict>
|
|
677
|
+
<key>Label</key><string>${LAUNCHD_LABEL}</string>
|
|
678
|
+
<key>ProgramArguments</key>
|
|
679
|
+
<array>
|
|
680
|
+
${command.map((part) => ` <string>${escape(part)}</string>`).join("\n")}
|
|
681
|
+
</array>
|
|
682
|
+
<key>StartInterval</key><integer>900</integer>
|
|
683
|
+
<key>RunAtLoad</key><true/>
|
|
684
|
+
<key>ProcessType</key><string>Background</string>
|
|
685
|
+
<key>StandardOutPath</key><string>${escape(logPath)}</string>
|
|
686
|
+
<key>StandardErrorPath</key><string>${escape(logPath)}</string>
|
|
687
|
+
</dict>
|
|
688
|
+
</plist>
|
|
689
|
+
`);
|
|
690
|
+
return plist;
|
|
691
|
+
}
|
|
692
|
+
export {
|
|
693
|
+
LAUNCHD_LABEL,
|
|
694
|
+
discoverSessions,
|
|
695
|
+
hookSessionRef,
|
|
696
|
+
installLaunchd,
|
|
697
|
+
isTrivial,
|
|
698
|
+
libraryRequest,
|
|
699
|
+
loadSyncConfig,
|
|
700
|
+
parseClaudeSession,
|
|
701
|
+
parseCodexSession,
|
|
702
|
+
projectOf,
|
|
703
|
+
repoRemote,
|
|
704
|
+
routeSession,
|
|
705
|
+
saveSyncConfig,
|
|
706
|
+
sessionDigest,
|
|
707
|
+
sourceFolders,
|
|
708
|
+
syncConfigPath,
|
|
709
|
+
syncTraces
|
|
710
|
+
};
|