@agentsky/sky 2.20.2 → 2.21.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-HB7SPKQ5.js → chunk-6JMFB3CV.js} +1 -1
- package/dist/{chunk-WTFBVTMI.js → chunk-ZZ6UF7QA.js} +1 -1
- package/dist/index.js +24 -13
- package/dist/{trace-HTWXINXI.js → trace-NMBMU4GS.js} +1 -1
- package/dist/{trace-library-QR54Q4Z3.js → trace-library-OWYB75AR.js} +96 -10
- package/package.json +3 -2
- package/vendor/trace-collect/SOURCE.json +6 -0
- package/vendor/trace-collect/collect.mjs +75 -0
- package/vendor/trace-collect/identity.d.ts +8 -0
- package/vendor/trace-collect/identity.mjs +23 -0
- package/vendor/trace-collect/muse.mjs +87 -0
- package/vendor/trace-collect/normalize.d.ts +6 -0
- package/vendor/trace-collect/normalize.mjs +312 -0
- package/vendor/trace-collect/scrub.d.ts +2 -0
- package/vendor/trace-collect/scrub.mjs +186 -0
- package/vendor/trace-collect/sinks/agentsky.mjs +156 -0
- package/vendor/trace-collect/sources/_cline-family.mjs +182 -0
- package/vendor/trace-collect/sources/_project-root.mjs +40 -0
- package/vendor/trace-collect/sources/amp.mjs +18 -0
- package/vendor/trace-collect/sources/chatgpt.mjs +203 -0
- package/vendor/trace-collect/sources/claude-ai.mjs +94 -0
- package/vendor/trace-collect/sources/claude-code.mjs +4 -0
- package/vendor/trace-collect/sources/claude-desktop.mjs +87 -0
- package/vendor/trace-collect/sources/cline.mjs +14 -0
- package/vendor/trace-collect/sources/codex.mjs +4 -0
- package/vendor/trace-collect/sources/continue.mjs +117 -0
- package/vendor/trace-collect/sources/copilot-chat.mjs +145 -0
- package/vendor/trace-collect/sources/cursor-chat.mjs +274 -0
- package/vendor/trace-collect/sources/cursor.mjs +4 -0
- package/vendor/trace-collect/sources/deepseek.mjs +172 -0
- package/vendor/trace-collect/sources/dsh.mjs +145 -0
- package/vendor/trace-collect/sources/gemini-cli.mjs +113 -0
- package/vendor/trace-collect/sources/gemini.mjs +150 -0
- package/vendor/trace-collect/sources/goose.mjs +93 -0
- package/vendor/trace-collect/sources/grok-cli.mjs +110 -0
- package/vendor/trace-collect/sources/grok.mjs +83 -0
- package/vendor/trace-collect/sources/hermes.mjs +4 -0
- package/vendor/trace-collect/sources/index.mjs +119 -0
- package/vendor/trace-collect/sources/jan.mjs +134 -0
- package/vendor/trace-collect/sources/kilo-code.mjs +14 -0
- package/vendor/trace-collect/sources/kiro.mjs +77 -0
- package/vendor/trace-collect/sources/lm-studio.mjs +84 -0
- package/vendor/trace-collect/sources/muse.mjs +11 -0
- package/vendor/trace-collect/sources/opencode.mjs +109 -0
- package/vendor/trace-collect/sources/qwen-code.mjs +95 -0
- package/vendor/trace-collect/sources/roo-code.mjs +14 -0
- package/vendor/trace-collect/sources/zed.mjs +70 -0
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
resolveSessionRef,
|
|
10
10
|
streamEvents,
|
|
11
11
|
uploadFile
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-ZZ6UF7QA.js";
|
|
13
13
|
import {
|
|
14
14
|
CLI_VERSION,
|
|
15
15
|
USER_AGENT,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
currentUniverse,
|
|
21
21
|
loadConfig,
|
|
22
22
|
saveConfig
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-6JMFB3CV.js";
|
|
24
24
|
import {
|
|
25
25
|
AGENT_TYPES,
|
|
26
26
|
MODEL_SUBSCRIPTION_PLAN_PHRASE,
|
|
@@ -7093,7 +7093,7 @@ harness.command("connect").description("run an ACP agent (the command after --)
|
|
|
7093
7093
|
}));
|
|
7094
7094
|
var traceCmd = program.command("trace").description("correlate this machine's agent work with the cloud sessions it starts (metadata only unless you choose content)");
|
|
7095
7095
|
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) => {
|
|
7096
|
-
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-
|
|
7096
|
+
const { readState, writeState, record, installClaudeHooks, parseContentScopes, chosenPaths, fileEvent } = await import("./trace-NMBMU4GS.js");
|
|
7097
7097
|
if (readState()) throw new Error("a trace is already running here \u2014 `sky trace stop` it first");
|
|
7098
7098
|
const scopes = parseContentScopes(opts.capture);
|
|
7099
7099
|
const chosen = chosenPaths(opts.file ?? [], opts.dir ?? []);
|
|
@@ -7128,7 +7128,7 @@ traceCmd.command("start").description("start a trace: a grant for this machine,
|
|
|
7128
7128
|
}));
|
|
7129
7129
|
traceCmd.command("hook").description("record one Claude Code hook event read from stdin (run by the installed hooks; never fails, prints nothing)").action(async () => {
|
|
7130
7130
|
try {
|
|
7131
|
-
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-
|
|
7131
|
+
const { readState, record, flush, grantSender, eventsForClaudeHook, contentForClaudeHook, clearState } = await import("./trace-NMBMU4GS.js");
|
|
7132
7132
|
const chunks = [];
|
|
7133
7133
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7134
7134
|
const input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
@@ -7141,14 +7141,14 @@ traceCmd.command("hook").description("record one Claude Code hook event read fro
|
|
|
7141
7141
|
}
|
|
7142
7142
|
});
|
|
7143
7143
|
traceCmd.command("flush").description("send what the spool holds, retrying with backoff").action(run(async () => {
|
|
7144
|
-
const { readState, flush, grantSender } = await import("./trace-
|
|
7144
|
+
const { readState, flush, grantSender } = await import("./trace-NMBMU4GS.js");
|
|
7145
7145
|
const state = readState();
|
|
7146
7146
|
if (!state) throw new Error("no trace is running here");
|
|
7147
7147
|
const outcome = await flush(grantSender(state), { attempts: 4 });
|
|
7148
7148
|
console.log(`sent ${outcome.sent}, refused ${outcome.refused}, given up ${outcome.expired}, still pending ${outcome.pending}${outcome.stopped ? ` (stopped: ${outcome.stopped})` : ""}`);
|
|
7149
7149
|
}));
|
|
7150
7150
|
traceCmd.command("status").description("the running trace and its spool").action(run(async () => {
|
|
7151
|
-
const { readState, spoolStatus } = await import("./trace-
|
|
7151
|
+
const { readState, spoolStatus } = await import("./trace-NMBMU4GS.js");
|
|
7152
7152
|
const state = readState();
|
|
7153
7153
|
if (!state) {
|
|
7154
7154
|
console.log("No trace is running here.");
|
|
@@ -7158,7 +7158,7 @@ traceCmd.command("status").description("the running trace and its spool").action
|
|
|
7158
7158
|
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`);
|
|
7159
7159
|
}));
|
|
7160
7160
|
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) => {
|
|
7161
|
-
const { readState } = await import("./trace-
|
|
7161
|
+
const { readState } = await import("./trace-NMBMU4GS.js");
|
|
7162
7162
|
const ref = id ?? readState()?.trace_id;
|
|
7163
7163
|
if (!ref) throw new Error("name a trace, or start one here");
|
|
7164
7164
|
const bundle = await api("GET", `/traces/${ref}`);
|
|
@@ -7185,7 +7185,7 @@ traceCmd.command("show [id]").description("the trace bundle: spans, the sessions
|
|
|
7185
7185
|
for (const gap of bundle.gaps) console.log(` gap: ${gap.span_id} ${gap.reason}`);
|
|
7186
7186
|
}));
|
|
7187
7187
|
traceCmd.command("stop").description("end the root span, send what is left, and revoke this machine's grant").action(run(async () => {
|
|
7188
|
-
const { readState, record, flush, grantSender, clearState } = await import("./trace-
|
|
7188
|
+
const { readState, record, flush, grantSender, clearState } = await import("./trace-NMBMU4GS.js");
|
|
7189
7189
|
const state = readState();
|
|
7190
7190
|
if (!state) throw new Error("no trace is running here");
|
|
7191
7191
|
if (state.root_span) record(() => [{ span_id: state.root_span, kind: "end", outcome: "completed" }]);
|
|
@@ -7194,8 +7194,8 @@ traceCmd.command("stop").description("end the root span, send what is left, and
|
|
|
7194
7194
|
clearState();
|
|
7195
7195
|
console.log(`Stopped ${state.trace_id}${outcome.pending ? ` (${outcome.pending} events could not be sent)` : ""}`);
|
|
7196
7196
|
}));
|
|
7197
|
-
traceCmd.command("sync [payload]").description("upload this machine's Claude Code and
|
|
7198
|
-
const lib = await import("./trace-library-
|
|
7197
|
+
traceCmd.command("sync [payload]").description("upload this machine's agent sessions (Claude Code, Codex, Cursor, Gemini CLI and every other agent `--agents` lists) 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("--import <export>", "add a downloaded cloud export (claude.ai, ChatGPT, Gemini, Grok, DeepSeek \u2026: its .zip, folder or file) so sync uploads it (repeatable)", collect).option("--agents", "list the agents sync reads and which are on this machine").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) => {
|
|
7198
|
+
const lib = await import("./trace-library-OWYB75AR.js");
|
|
7199
7199
|
if (opts.hook) {
|
|
7200
7200
|
try {
|
|
7201
7201
|
let raw = payload ?? "";
|
|
@@ -7236,12 +7236,23 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
|
|
|
7236
7236
|
};
|
|
7237
7237
|
lib.saveSyncConfig(next);
|
|
7238
7238
|
console.log(`Trace sync is ${next.enabled ? "on" : "off"} on this machine. It reads only:`);
|
|
7239
|
-
for (const folder of lib.sourceFolders()) console.log(` ${folder}`);
|
|
7239
|
+
for (const folder of await lib.sourceFolders()) console.log(` ${folder}`);
|
|
7240
7240
|
console.log("and sends each session, redacted here first, to:");
|
|
7241
7241
|
for (const rule of next.rules) console.log(` folder path contains "${rule.match}" \u2192 ${rule.workspace}`);
|
|
7242
7242
|
console.log(` anything else \u2192 ${next.fallback ?? "stays on this machine"}`);
|
|
7243
7243
|
console.log(`A repository's .agentsky.json "workspace" wins over these rules.`);
|
|
7244
7244
|
}
|
|
7245
|
+
if (opts.agents) {
|
|
7246
|
+
for (const agent2 of await lib.supportedAgents()) {
|
|
7247
|
+
const how = agent2.kind === "export" ? "cloud export: sky trace sync --import <file>" : agent2.found ? "found on this machine" : "not on this machine";
|
|
7248
|
+
console.log(` ${agent2.name.padEnd(46)} ${how}`);
|
|
7249
|
+
}
|
|
7250
|
+
return;
|
|
7251
|
+
}
|
|
7252
|
+
for (const input of opts.import ?? []) {
|
|
7253
|
+
const imported = await lib.importExport(input);
|
|
7254
|
+
console.log(`Added ${imported.sessions} ${imported.agent} session${imported.sessions === 1 ? "" : "s"} from ${input}.`);
|
|
7255
|
+
}
|
|
7245
7256
|
if (opts.installSchedule) {
|
|
7246
7257
|
const { join: join17 } = await import("path");
|
|
7247
7258
|
const { homedir: homedir8 } = await import("os");
|
|
@@ -7293,7 +7304,7 @@ traceCmd.command("sync [payload]").description("upload this machine's Claude Cod
|
|
|
7293
7304
|
})();
|
|
7294
7305
|
});
|
|
7295
7306
|
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) => {
|
|
7296
|
-
const { libraryRequest } = await import("./trace-library-
|
|
7307
|
+
const { libraryRequest } = await import("./trace-library-OWYB75AR.js");
|
|
7297
7308
|
const workspace2 = opts.workspace ?? currentUniverse();
|
|
7298
7309
|
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
7299
7310
|
const params = new URLSearchParams({ q: words.join(" "), scope: "traces", limit: String(opts.limit), ...opts.cursor ? { cursor: opts.cursor } : {} });
|
|
@@ -7310,7 +7321,7 @@ traceCmd.command("search <query...>").description("search every session in a wor
|
|
|
7310
7321
|
if (page.nextCursor) console.log(`More: sky trace search ${JSON.stringify(words.join(" "))} --workspace ${workspace2} --cursor ${page.nextCursor}`);
|
|
7311
7322
|
}));
|
|
7312
7323
|
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) => {
|
|
7313
|
-
const { libraryRequest } = await import("./trace-library-
|
|
7324
|
+
const { libraryRequest } = await import("./trace-library-OWYB75AR.js");
|
|
7314
7325
|
const workspace2 = opts.workspace ?? currentUniverse();
|
|
7315
7326
|
if (!workspace2) throw new Error("name a workspace with --workspace");
|
|
7316
7327
|
const page = await libraryRequest("GET", `/trace-library?limit=${opts.limit}`, workspace2);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ApiError
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZZ6UF7QA.js";
|
|
4
4
|
import {
|
|
5
5
|
USER_AGENT,
|
|
6
6
|
apiBase,
|
|
7
7
|
apiToken
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-6JMFB3CV.js";
|
|
9
9
|
import {
|
|
10
10
|
SessionSourceCollector,
|
|
11
11
|
publicRemote,
|
|
@@ -19,6 +19,7 @@ import { createReadStream, existsSync, mkdirSync, readdirSync, readFileSync, rmS
|
|
|
19
19
|
import { createInterface } from "readline";
|
|
20
20
|
import { homedir, hostname } from "os";
|
|
21
21
|
import { basename, dirname, join, sep } from "path";
|
|
22
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
22
23
|
import { createGunzip } from "zlib";
|
|
23
24
|
var MESSAGE_MAX_BYTES = 8 * 1024;
|
|
24
25
|
var TOOL_MAX_BYTES = 2 * 1024;
|
|
@@ -66,8 +67,34 @@ function claudeRoot() {
|
|
|
66
67
|
function codexRoot() {
|
|
67
68
|
return process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
var SKY_PARSED = /* @__PURE__ */ new Set(["claude-code", "codex"]);
|
|
71
|
+
var COLLECTOR_HARNESS = { "claude-code": "claude_code", codex: "codex" };
|
|
72
|
+
var collectorModules;
|
|
73
|
+
async function traceCollector() {
|
|
74
|
+
const dir = join(dirname(fileURLToPath(import.meta.url)), "..", "vendor", "trace-collect");
|
|
75
|
+
collectorModules ??= Promise.all([
|
|
76
|
+
import(pathToFileURL(join(dir, "collect.mjs")).href),
|
|
77
|
+
import(pathToFileURL(join(dir, "sinks", "agentsky.mjs")).href)
|
|
78
|
+
]).then(([collector, sink]) => ({ collector, sink }));
|
|
79
|
+
return collectorModules;
|
|
80
|
+
}
|
|
81
|
+
function collectorHarness(harness) {
|
|
82
|
+
return COLLECTOR_HARNESS[harness] ?? harness.replace(/[^a-z0-9_-]/g, "_").slice(0, 40);
|
|
83
|
+
}
|
|
84
|
+
async function sourceFolders() {
|
|
85
|
+
const { collector } = await traceCollector();
|
|
86
|
+
const others = (await collector.describeSources()).filter((source) => !SKY_PARSED.has(source.id)).flatMap((source) => source.found);
|
|
87
|
+
return [claudeRoot(), join(codexRoot(), "sessions"), join(codexRoot(), "archived_sessions"), ...others, collector.STORE];
|
|
88
|
+
}
|
|
89
|
+
async function supportedAgents() {
|
|
90
|
+
const { collector } = await traceCollector();
|
|
91
|
+
return (await collector.describeSources()).map(({ id, name, kind, found }) => ({ id, name, kind, found: found.length > 0 }));
|
|
92
|
+
}
|
|
93
|
+
async function importExport(input) {
|
|
94
|
+
const { collector } = await traceCollector();
|
|
95
|
+
const result = await collector.importFile(input);
|
|
96
|
+
if (result.error) throw new Error(result.error);
|
|
97
|
+
return { agent: result.name ?? result.id ?? "export", sessions: result.total ?? 0 };
|
|
71
98
|
}
|
|
72
99
|
function signatureOf(files) {
|
|
73
100
|
const parts = [];
|
|
@@ -188,8 +215,28 @@ function claudeSessionAt(path) {
|
|
|
188
215
|
const signature = signatureOf(files);
|
|
189
216
|
return signature ? { harness: "claude_code", sessionId, files, signature } : null;
|
|
190
217
|
}
|
|
191
|
-
async function
|
|
192
|
-
|
|
218
|
+
async function collectorSessions() {
|
|
219
|
+
const { collector } = await traceCollector();
|
|
220
|
+
await collector.collect();
|
|
221
|
+
const harness = (await collector.loadSources()).map((source) => source.id).filter((id) => !SKY_PARSED.has(id));
|
|
222
|
+
const found = /* @__PURE__ */ new Map();
|
|
223
|
+
for await (const { file, trace, id } of collector.sessions({ harness, children: true })) {
|
|
224
|
+
const signature = signatureOf([file]);
|
|
225
|
+
if (!signature) continue;
|
|
226
|
+
const session = { harness: collectorHarness(trace.harness), sessionId: String(id).slice(0, 200), files: [file], signature, trace };
|
|
227
|
+
found.set(`${session.harness}:${session.sessionId}`, session);
|
|
228
|
+
}
|
|
229
|
+
return [...found.values()];
|
|
230
|
+
}
|
|
231
|
+
async function discoverSessions(onCollectorError) {
|
|
232
|
+
let others = [];
|
|
233
|
+
try {
|
|
234
|
+
others = await collectorSessions();
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (!onCollectorError) throw error;
|
|
237
|
+
onCollectorError(error);
|
|
238
|
+
}
|
|
239
|
+
return [...claudeSessions(), ...await codexSessions(), ...others];
|
|
193
240
|
}
|
|
194
241
|
function asRecord(value) {
|
|
195
242
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -233,6 +280,11 @@ function isoOrUndefined(value) {
|
|
|
233
280
|
const time = Date.parse(text);
|
|
234
281
|
return Number.isFinite(time) ? new Date(time).toISOString() : void 0;
|
|
235
282
|
}
|
|
283
|
+
async function collectorBody(trace, device, scrubber) {
|
|
284
|
+
const { collector, sink } = await traceCollector();
|
|
285
|
+
const scrubbed = (scrubber ?? collector.scrubberFor()).scrub(trace);
|
|
286
|
+
return sink.toLibrary({ ...scrubbed, cwd: trace.cwd, stats: trace.stats }, device);
|
|
287
|
+
}
|
|
236
288
|
async function parseClaudeSession(session) {
|
|
237
289
|
const out = { harness: "claude_code", sessionId: session.sessionId, userTurns: 0, toolCalls: 0, partial: false, messages: [] };
|
|
238
290
|
const collector = new Collector();
|
|
@@ -580,7 +632,11 @@ async function runSync(config, options) {
|
|
|
580
632
|
const device = config.device ?? hostname();
|
|
581
633
|
const report = { scanned: 0, changed: 0, uploaded: 0, unchanged: 0, local: 0, owned: 0, empty: 0, failed: 0, byWorkspace: {}, errors: [] };
|
|
582
634
|
const direct = options.only?.length ? options.only.map(claudeSessionAt) : [];
|
|
583
|
-
|
|
635
|
+
const collectorFailed = (error) => {
|
|
636
|
+
report.failed += 1;
|
|
637
|
+
report.errors.push(`other agents: unreadable (${error.message})`);
|
|
638
|
+
};
|
|
639
|
+
let sessions = direct.length && direct.every(Boolean) ? direct : await discoverSessions(collectorFailed);
|
|
584
640
|
if (options.only?.length) {
|
|
585
641
|
const wanted = new Set(options.only);
|
|
586
642
|
sessions = sessions.filter((s) => wanted.has(s.sessionId) || s.files.some((file) => wanted.has(file)));
|
|
@@ -594,7 +650,33 @@ async function runSync(config, options) {
|
|
|
594
650
|
state.sessions[key(s)] = { signature: s.signature, at: (/* @__PURE__ */ new Date()).toISOString(), ...value };
|
|
595
651
|
};
|
|
596
652
|
const ready = /* @__PURE__ */ new Map();
|
|
653
|
+
let scrubber;
|
|
597
654
|
for (const local of pending) {
|
|
655
|
+
if (local.trace) {
|
|
656
|
+
const workspace2 = routeSession(local.trace.cwd, config);
|
|
657
|
+
if (!workspace2) {
|
|
658
|
+
report.local += 1;
|
|
659
|
+
record(local, { workspace: null, status: "local" });
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
let body;
|
|
663
|
+
try {
|
|
664
|
+
scrubber ??= (await traceCollector()).collector.scrubberFor();
|
|
665
|
+
body = await collectorBody(local.trace, device, scrubber);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
report.failed += 1;
|
|
668
|
+
report.errors.push(`${key(local)}: unreadable (${error.message})`);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const previous2 = state.sessions[key(local)];
|
|
672
|
+
if (!full && previous2?.sha256 === body.sha256 && previous2.workspace === workspace2 && previous2.status === "stored") {
|
|
673
|
+
report.unchanged += 1;
|
|
674
|
+
record(local, { workspace: workspace2, sha256: body.sha256, status: "stored" });
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
ready.set(workspace2, [...ready.get(workspace2) ?? [], { local, sha256: body.sha256, body: () => body }]);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
598
680
|
let parsed;
|
|
599
681
|
try {
|
|
600
682
|
parsed = local.harness === "claude_code" ? await parseClaudeSession(local) : await parseCodexSession(local);
|
|
@@ -621,7 +703,7 @@ async function runSync(config, options) {
|
|
|
621
703
|
record(local, { workspace, sha256, status: "stored" });
|
|
622
704
|
continue;
|
|
623
705
|
}
|
|
624
|
-
ready.set(workspace, [...ready.get(workspace) ?? [], { local, parsed, sha256 }]);
|
|
706
|
+
ready.set(workspace, [...ready.get(workspace) ?? [], { local, sha256, body: () => uploadBody(parsed, sha256, device) }]);
|
|
625
707
|
}
|
|
626
708
|
for (const [workspace, items] of ready) {
|
|
627
709
|
for (let start = 0; start < items.length; start += CHECK_BATCH) {
|
|
@@ -629,7 +711,7 @@ async function runSync(config, options) {
|
|
|
629
711
|
let statuses;
|
|
630
712
|
try {
|
|
631
713
|
statuses = (await libraryRequest("POST", "/trace-library/check", workspace, {
|
|
632
|
-
sessions: batch.map((item) => ({ harness: item.
|
|
714
|
+
sessions: batch.map((item) => ({ harness: item.local.harness, session_id: item.local.sessionId, sha256: item.sha256 }))
|
|
633
715
|
})).sessions;
|
|
634
716
|
} catch (error) {
|
|
635
717
|
report.failed += batch.length;
|
|
@@ -658,7 +740,7 @@ async function runSync(config, options) {
|
|
|
658
740
|
}
|
|
659
741
|
await pool(toSend, UPLOAD_CONCURRENCY, async (item) => {
|
|
660
742
|
try {
|
|
661
|
-
await libraryRequest("PUT", `/trace-library/${item.
|
|
743
|
+
await libraryRequest("PUT", `/trace-library/${item.local.harness}/${encodeURIComponent(item.local.sessionId)}`, workspace, item.body());
|
|
662
744
|
report.uploaded += 1;
|
|
663
745
|
report.byWorkspace[workspace] = (report.byWorkspace[workspace] ?? 0) + 1;
|
|
664
746
|
record(item.local, { workspace, sha256: item.sha256, status: "stored" });
|
|
@@ -711,8 +793,11 @@ ${command.map((part) => ` <string>${escape(part)}</string>`).join("\n")}
|
|
|
711
793
|
}
|
|
712
794
|
export {
|
|
713
795
|
LAUNCHD_LABEL,
|
|
796
|
+
collectorBody,
|
|
797
|
+
collectorHarness,
|
|
714
798
|
discoverSessions,
|
|
715
799
|
hookSessionRef,
|
|
800
|
+
importExport,
|
|
716
801
|
installLaunchd,
|
|
717
802
|
isTrivial,
|
|
718
803
|
libraryRequest,
|
|
@@ -725,6 +810,7 @@ export {
|
|
|
725
810
|
saveSyncConfig,
|
|
726
811
|
sessionDigest,
|
|
727
812
|
sourceFolders,
|
|
813
|
+
supportedAgents,
|
|
728
814
|
syncConfigPath,
|
|
729
815
|
syncTraces
|
|
730
816
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentsky/sky",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"description": "sky \u2014 the AgentSky developer CLI: launch and talk to long-lived agents from your terminal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"sky": "./dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"vendor"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=20"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The trace collector as a library: every agent in, one format out, any destination.
|
|
2
|
+
//
|
|
3
|
+
// sources (lib/sources/*.mjs) → collect / import → trace/v1 sessions → sinks (lib/sinks/*.mjs)
|
|
4
|
+
//
|
|
5
|
+
// Nothing here knows about AgentGold or AgentSky; those are sinks. Any product that wants every agent's sessions
|
|
6
|
+
// imports this module (zero dependencies, Node 22) instead of writing its own readers.
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
+
import { loadSources, collectLocal, importExport, describeSources } from "./sources/index.mjs";
|
|
13
|
+
import { normalizeAny } from "./normalize.mjs";
|
|
14
|
+
import { createScrubber } from "./scrub.mjs";
|
|
15
|
+
|
|
16
|
+
export { loadSources, describeSources };
|
|
17
|
+
|
|
18
|
+
const HOME = os.homedir();
|
|
19
|
+
/** Where converted sessions live. Shared with the AgentGold CLI so one conversion serves every destination. */
|
|
20
|
+
export const STORE = path.join(process.env.TRACE_MARKET_HOME || path.join(HOME, ".trace-market"), "sources");
|
|
21
|
+
|
|
22
|
+
export const collect = (opts = {}) => collectLocal(STORE, opts);
|
|
23
|
+
export const importFile = (input, opts = {}) => importExport(path.resolve(input), STORE, opts);
|
|
24
|
+
|
|
25
|
+
/** Every session on this computer, normalized: native agent folders read in place plus everything converted into STORE. */
|
|
26
|
+
export async function* sessions({ harness = null, since = 0, minBytes = 2000, children = false } = {}) {
|
|
27
|
+
// With a harness filter, only open the folders those agents write to.
|
|
28
|
+
const natives = (await loadSources()).filter((s) => s.kind === "native" && (!harness || harness.includes(s.id)));
|
|
29
|
+
const converted = harness ? harness.map((h) => path.join(STORE, h)) : [STORE];
|
|
30
|
+
const roots = natives.flatMap((s) => s.roots(HOME)).filter((d) => fs.existsSync(d));
|
|
31
|
+
const files = [];
|
|
32
|
+
const walk = (d) => { let ents; try { ents = fs.readdirSync(d, { withFileTypes: true }); } catch { return; } for (const e of ents) { const p = path.join(d, e.name); if (e.isDirectory()) { if (e.name !== "subagents") walk(p); } else if (/\.jsonl?$/.test(e.name) && !e.name.startsWith(".")) files.push(p); } };
|
|
33
|
+
for (const r of [...roots, ...converted]) walk(r);
|
|
34
|
+
for (const f of files) {
|
|
35
|
+
let st; try { st = fs.statSync(f); } catch { continue; }
|
|
36
|
+
if (st.size < minBytes || (since && st.mtimeMs < since)) continue;
|
|
37
|
+
let t; try { t = normalizeAny(fs.readFileSync(f, "utf8"), f); } catch { continue; }
|
|
38
|
+
if (harness && !harness.includes(t.harness)) continue;
|
|
39
|
+
if (!t.stats.user_turns && !(children && t.parent_session_id)) continue;
|
|
40
|
+
if (t.parent_session_id && !children) continue; // children travel with their parent unless a sink wants them on their own
|
|
41
|
+
yield { file: f, trace: t, id: t.native_session_id || t.session_id };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One scrubber per run, configured from the AgentGold config when present so both destinations redact the same names. */
|
|
46
|
+
export function scrubberFor(extra = {}) {
|
|
47
|
+
let redact = {};
|
|
48
|
+
try { redact = JSON.parse(fs.readFileSync(path.join(path.dirname(STORE), "config.json"), "utf8")).redact ?? {}; } catch {}
|
|
49
|
+
const names = [...new Set([...(redact.names ?? []), os.userInfo().username, ...(extra.names ?? [])])].filter((s) => s && s.length >= 3);
|
|
50
|
+
return createScrubber({ ...redact, names, terms: [...(redact.terms ?? []), ...(extra.terms ?? [])] });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SINKS = path.join(path.dirname(fileURLToPath(import.meta.url)), "sinks");
|
|
54
|
+
export async function loadSinks() {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const f of fs.readdirSync(SINKS).filter((f) => f.endsWith(".mjs") && !f.startsWith("_")).sort()) {
|
|
57
|
+
const m = (await import(pathToFileURL(path.join(SINKS, f)).href)).default;
|
|
58
|
+
if (m?.id && typeof m.push === "function") out.push(m);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Send sessions to one destination. A sink gets raw sessions plus `scrub`; it must scrub before anything leaves the computer. */
|
|
64
|
+
export async function push(sinkId, opts = {}) {
|
|
65
|
+
const sink = (await loadSinks()).find((s) => s.id === sinkId);
|
|
66
|
+
if (!sink) throw new Error(`unknown destination ${sinkId}; available: ${(await loadSinks()).map((s) => s.id).join(", ")}`);
|
|
67
|
+
const ready = await sink.ready?.(opts);
|
|
68
|
+
if (ready && !ready.ok) return { sink: sink.id, error: ready.why };
|
|
69
|
+
const sc = scrubberFor(opts.redact);
|
|
70
|
+
const harness = opts.harness ?? sink.defaultHarness?.(opts) ?? null;
|
|
71
|
+
const skip = new Set(sink.skipHarness?.(opts) ?? []);
|
|
72
|
+
const list = [];
|
|
73
|
+
for await (const s of sessions({ harness, since: opts.since || 0, children: !!sink.children })) if (!skip.has(s.trace.harness)) list.push(s);
|
|
74
|
+
return { sink: sink.id, ...(await sink.push(list, { ...opts, scrub: (v) => sc.scrub(v), report: sc.report })) };
|
|
75
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const IDENTITY_VERSION: number;
|
|
2
|
+
export type SourceIdentity = { version: number; kind: 'session' | 'file'; key: string; token?: string };
|
|
3
|
+
export function sourceIdentity(harness: string, sessionId: string, source?: string): SourceIdentity;
|
|
4
|
+
export function nativeIdentity(harness: string, token: string): string;
|
|
5
|
+
export function legacyStableIdentity(harness: string, id: string): string;
|
|
6
|
+
export function fileIdentity(harness: string, token: string): string;
|
|
7
|
+
export function legacyHashes(seeds: Array<{harness: string; seed: string; native?: boolean}>, count: number): Generator<string>;
|
|
8
|
+
export function matchingHashes(c: any, known: Set<string>): string[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Stable opaque session keys. Legacy count hashes are compatibility lookups, never new identities.
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
export const IDENTITY_VERSION = 3;
|
|
4
|
+
const hash = (s) => createHash('sha256').update(s).digest('hex').slice(0, 32);
|
|
5
|
+
export function sourceIdentity(harness, sessionId, source = '') {
|
|
6
|
+
if (sessionId) { const token = hash(`native-session:${harness}:${sessionId}`); return { version: IDENTITY_VERSION, kind: 'session', token, key: nativeIdentity(harness, token) }; }
|
|
7
|
+
const token = hash(source);
|
|
8
|
+
return { version: IDENTITY_VERSION, kind: 'file', token, key: fileIdentity(harness, token) };
|
|
9
|
+
}
|
|
10
|
+
export const nativeIdentity = (harness, token) => hash(`session/v3:${harness}:${token}`);
|
|
11
|
+
export const legacyStableIdentity = (harness, id) => hash(`session/v2:${harness}:${id}`);
|
|
12
|
+
export const fileIdentity = (harness, token) => hash(`file/v2:${harness}:${token}`);
|
|
13
|
+
export function* legacyHashes(seeds, count) {
|
|
14
|
+
for (const { harness, seed, native } of seeds) {
|
|
15
|
+
if (native) yield legacyStableIdentity(harness, seed);
|
|
16
|
+
for (let n = 0; n <= count; n++) yield hash(`${harness}:${seed}:${n}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function matchingHashes(c, known) {
|
|
20
|
+
const found = new Set(known.has(c.hash) ? [c.hash] : []);
|
|
21
|
+
if (known.size) for (const h of legacyHashes(c.legacy?.seeds ?? [], c.legacy?.count ?? 0)) if (known.has(h)) found.add(h);
|
|
22
|
+
return [...found];
|
|
23
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Muse (Meta) keeps conversations in the cloud; its "Download your agent data" export is a zip whose
|
|
2
|
+
// "Conversation with Muse AI_<date>.txt" is one long transcript of "[YYYY-MM-DD HH:MM:SS] You: …" /
|
|
3
|
+
// "[…] Muse AI: …" lines (continuation lines belong to the line above). This turns it into trace/v1
|
|
4
|
+
// sessions, split wherever the conversation pauses, so the normal scan → scrub → draft flow can take them.
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { FORMAT } from "./normalize.mjs";
|
|
11
|
+
|
|
12
|
+
const LINE = /^\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\] (You|Muse AI|[^:\]]{1,40}): ?(.*)$/;
|
|
13
|
+
const WIDGET = /^\[\[hatch_widget:[^\]]*\]\]$/;
|
|
14
|
+
/** Topics that are about the person's private life rather than work: such sessions are never uploaded. Broad on purpose:
|
|
15
|
+
* a Muse chat is a personal assistant, so a false positive only keeps a session at home. */
|
|
16
|
+
const SENSITIVE = /\b(health|medical|doctor|clinic|hospital|diagnos|symptom|physical exam|blood test|therapy|therapist|medication|prescription|pregnan|fertility|mental health|depress|anxiety|relationship|dating|boyfriend|girlfriend|husband|wife|divorce|father|mother|dad|mom|parents?|family|baby|child(ren)?|kids?|salary|paystub|bank account|bank|loan|mortgage|invest\w*|robinhood|interest rate|ssn|passport|\d+ \w+ (rd|road|st|street|ave|avenue|dr|drive|ln|lane|ct|blvd))\b|体检|查体|医生|医院|看病|吃药|健康|父亲|母亲|爸|妈|家人|孩子|儿子|女儿|恋爱|约会|感情|男朋友|女朋友|老公|老婆|结婚|离婚|工资|存款|理财|利率|贷款|股票|摔/i;
|
|
17
|
+
|
|
18
|
+
/** The transcript text from an export zip, a folder it was unpacked into, or the .txt itself. */
|
|
19
|
+
export function readMuseTranscript(input) {
|
|
20
|
+
const st = fs.statSync(input);
|
|
21
|
+
if (st.isDirectory()) {
|
|
22
|
+
const f = fs.readdirSync(input).find((n) => /^Conversation with Muse.*\.txt$/i.test(n));
|
|
23
|
+
if (!f) throw new Error(`no "Conversation with Muse …txt" in ${input}`);
|
|
24
|
+
return fs.readFileSync(path.join(input, f), "utf8");
|
|
25
|
+
}
|
|
26
|
+
if (input.endsWith(".zip")) {
|
|
27
|
+
const list = execFileSync("unzip", ["-Z1", input], { encoding: "utf8" }).split("\n");
|
|
28
|
+
const f = list.find((n) => /(^|\/)Conversation with Muse.*\.txt$/i.test(n));
|
|
29
|
+
if (!f) throw new Error(`no "Conversation with Muse …txt" in ${input}`);
|
|
30
|
+
return execFileSync("unzip", ["-p", input, f], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
31
|
+
}
|
|
32
|
+
return fs.readFileSync(input, "utf8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Parse the transcript into messages with timestamps. Widget placeholders are dropped. */
|
|
36
|
+
export function parseMuseTranscript(raw) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
39
|
+
const m = LINE.exec(line);
|
|
40
|
+
if (m) {
|
|
41
|
+
const [, d, t, who, rest] = m;
|
|
42
|
+
out.push({ at: `${d}T${t}Z`, role: who === "You" ? "user" : "assistant", text: rest });
|
|
43
|
+
} else if (out.length && !WIDGET.test(line.trim())) {
|
|
44
|
+
out[out.length - 1].text += `\n${line}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const m of out) m.text = m.text.split("\n").filter((l) => !WIDGET.test(l.trim())).join("\n").trim();
|
|
48
|
+
return out.filter((m) => m.text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Split into sessions wherever nothing was said for `gapMinutes`; each becomes one trace/v1 object. */
|
|
52
|
+
export function museSessions(raw, { gapMinutes = 120 } = {}) {
|
|
53
|
+
const msgs = parseMuseTranscript(raw);
|
|
54
|
+
const groups = [];
|
|
55
|
+
for (const m of msgs) {
|
|
56
|
+
const last = groups.at(-1)?.at(-1);
|
|
57
|
+
if (!last || Date.parse(m.at) - Date.parse(last.at) > gapMinutes * 60_000) groups.push([m]);
|
|
58
|
+
else groups.at(-1).push(m);
|
|
59
|
+
}
|
|
60
|
+
return groups
|
|
61
|
+
.filter((g) => g.some((m) => m.role === "user"))
|
|
62
|
+
.map((g) => {
|
|
63
|
+
const firstUser = g.find((m) => m.role === "user");
|
|
64
|
+
const id = createHash("sha256").update(`muse:${g[0].at}:${firstUser.text.slice(0, 200)}`).digest("hex").slice(0, 16);
|
|
65
|
+
const all = g.map((m) => m.text).join("\n");
|
|
66
|
+
return {
|
|
67
|
+
format: FORMAT, harness: "muse", model: "muse", session_id: `muse-${id}`, title: firstUser.text.split("\n")[0].slice(0, 120),
|
|
68
|
+
started_at: g[0].at, ended_at: g.at(-1).at, cwd: "", source: "muse-export",
|
|
69
|
+
sensitive: SENSITIVE.test(all),
|
|
70
|
+
messages: g.map((m) => ({ role: m.role, text: m.text, at: m.at })),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Write each session as <dir>/muse-<id>.json (idempotent: same conversation, same file). */
|
|
76
|
+
export function writeMuseSessions(sessions, dir) {
|
|
77
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
78
|
+
let written = 0;
|
|
79
|
+
for (const s of sessions) {
|
|
80
|
+
const p = path.join(dir, `${s.session_id}.json`);
|
|
81
|
+
const body = JSON.stringify(s);
|
|
82
|
+
if (fs.existsSync(p) && fs.readFileSync(p, "utf8") === body) continue;
|
|
83
|
+
fs.writeFileSync(p, body, { mode: 0o600 });
|
|
84
|
+
written++;
|
|
85
|
+
}
|
|
86
|
+
return written;
|
|
87
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const FORMAT: string;
|
|
2
|
+
export function normalizeAny(raw: string, filePath?: string): any;
|
|
3
|
+
export function makePreview(t: any, max?: number): string;
|
|
4
|
+
export function parseJsonl(raw: string): any[];
|
|
5
|
+
export function detectHarness(rows: any[], filePath?: string): string;
|
|
6
|
+
export function makeSamples(t: any, max?: number): Array<{ label: string; text: string }>;
|