@agentsky/sky 2.20.1 → 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-ICTCB4VT.js → chunk-6JMFB3CV.js} +1 -1
- package/dist/{chunk-NAD26CDY.js → chunk-GHTAIKQ6.js} +80 -30
- package/dist/{chunk-S6A5V7XR.js → chunk-ZZ6UF7QA.js} +4 -3
- package/dist/index.js +152 -55
- package/dist/{src-KP6YIFNP.js → src-QBXMNV2V.js} +28 -6
- package/dist/{trace-SJHQ66JA.js → trace-NMBMU4GS.js} +2 -2
- package/dist/{trace-library-IWF7BVNB.js → trace-library-OWYB75AR.js} +97 -11
- package/package.json +6 -11
- 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/products.d.ts +0 -652
- package/dist/products.js +0 -208
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
admittedInputScopeSchema,
|
|
2
3
|
external_exports,
|
|
3
4
|
messagePartSchema
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-GHTAIKQ6.js";
|
|
5
6
|
import "./chunk-MLKGABMK.js";
|
|
6
7
|
|
|
7
8
|
// ../../packages/chat/src/stream.ts
|
|
@@ -97,6 +98,16 @@ var internalEventSchema = external_exports.object({
|
|
|
97
98
|
* session.deleted) — emitted verbatim; the host already persisted it. */
|
|
98
99
|
lifecycle: external_exports.object({ id: external_exports.string().min(1), type: external_exports.string().min(1) }).passthrough().optional()
|
|
99
100
|
});
|
|
101
|
+
function workspaceIdentityForPod(body, supported) {
|
|
102
|
+
const { workspace_identity: token, ...rest } = body;
|
|
103
|
+
if (!token || supported) return body;
|
|
104
|
+
const text = `This runtime cannot receive the private workspace-identity file. Use this value instead wherever the instructions read the file ~/.config/agentsky/workspace-identity: ${token}. Never print, persist or share it.`;
|
|
105
|
+
const parts = [...rest.parts, { type: "text", index: Math.max(-1, ...rest.parts.map((part) => part.index)) + 1, text }];
|
|
106
|
+
if (!rest.events) return { ...rest, parts };
|
|
107
|
+
const previous = rest.events.find((event) => event.type === "system.message");
|
|
108
|
+
const system = { type: "system.message", content: [{ type: "text", text: [...previous?.type === "system.message" ? previous.content.map((block) => block.text) : [], text].join("\n\n") }] };
|
|
109
|
+
return { ...rest, parts, events: [...rest.events.filter((event) => event.type !== "system.message"), system] };
|
|
110
|
+
}
|
|
100
111
|
function personalContextForPod(body, supported) {
|
|
101
112
|
const { personal_context: grant, ...rest } = body;
|
|
102
113
|
if (!grant || supported) return body;
|
|
@@ -107,15 +118,18 @@ function personalContextForPod(body, supported) {
|
|
|
107
118
|
const system = { type: "system.message", content: [{ type: "text", text: [...previous?.type === "system.message" ? previous.content.map((block) => block.text) : [], text].join("\n\n") }] };
|
|
108
119
|
return { ...rest, parts, events: [...rest.events.filter((event) => event.type !== "system.message"), system] };
|
|
109
120
|
}
|
|
110
|
-
function podMessageBody(messageId, turn, resourcesRevision = 0) {
|
|
121
|
+
function podMessageBody(messageId, turn, resourcesRevision = 0, inputScope) {
|
|
122
|
+
const scope = inputScope ? admittedInputScopeSchema.parse(inputScope) : void 0;
|
|
123
|
+
if (scope && scope.message_id !== messageId) throw new Error("input_scope_identity_mismatch");
|
|
124
|
+
const preparation = scope ? { input_scope: scope } : {};
|
|
111
125
|
if ("events" in turn) {
|
|
112
126
|
const envelope = {
|
|
113
127
|
events: turn.events.system ? [turn.events.message, turn.events.system] : [turn.events.message],
|
|
114
128
|
...resourcesRevision > 0 ? { resources_revision: resourcesRevision } : {}
|
|
115
129
|
};
|
|
116
|
-
return { id: messageId, ...envelope, parts: legacyPartsOf(envelope) };
|
|
130
|
+
return { id: messageId, ...envelope, parts: legacyPartsOf(envelope), ...preparation };
|
|
117
131
|
}
|
|
118
|
-
return { id: messageId, parts: turn.parts, ...resourcesRevision > 0 ? { resources_revision: resourcesRevision } : {} };
|
|
132
|
+
return { id: messageId, parts: turn.parts, ...resourcesRevision > 0 ? { resources_revision: resourcesRevision } : {}, ...preparation };
|
|
119
133
|
}
|
|
120
134
|
var UNREACHABLE_CODES = /* @__PURE__ */ new Set([
|
|
121
135
|
"ECONNREFUSED",
|
|
@@ -155,6 +169,7 @@ function classifyPodSendStatus(status, detail) {
|
|
|
155
169
|
return { kind: "rejected", status, detail };
|
|
156
170
|
}
|
|
157
171
|
var PERSONAL_CONTEXT_PODS = /* @__PURE__ */ new Set();
|
|
172
|
+
var WORKSPACE_IDENTITY_PODS = /* @__PURE__ */ new Set();
|
|
158
173
|
async function sendPodMessage(baseUrl, body, options = {}) {
|
|
159
174
|
const headers = {
|
|
160
175
|
"content-type": "application/json",
|
|
@@ -168,6 +183,11 @@ async function sendPodMessage(baseUrl, body, options = {}) {
|
|
|
168
183
|
if (health.personalContext) PERSONAL_CONTEXT_PODS.add(baseUrl);
|
|
169
184
|
else if (health.ok) body = personalContextForPod(body, false);
|
|
170
185
|
}
|
|
186
|
+
if (body.workspace_identity && !WORKSPACE_IDENTITY_PODS.has(baseUrl)) {
|
|
187
|
+
const health = await getPodHealth(baseUrl, options);
|
|
188
|
+
if (health.workspaceIdentity) WORKSPACE_IDENTITY_PODS.add(baseUrl);
|
|
189
|
+
else if (health.ok) body = workspaceIdentityForPod(body, false);
|
|
190
|
+
}
|
|
171
191
|
const user = body.events?.find((event) => event.type === "user.message");
|
|
172
192
|
const mode = user?.type === "user.message" ? user.interaction_mode : body.interaction_mode;
|
|
173
193
|
if (mode) {
|
|
@@ -214,7 +234,8 @@ async function getPodHealth(baseUrl, options = {}) {
|
|
|
214
234
|
state: typeof body.state === "string" ? body.state : void 0,
|
|
215
235
|
openMessageId: typeof open === "string" && open ? open : null,
|
|
216
236
|
interactionModes: Array.isArray(body.interaction_modes) ? body.interaction_modes.filter((mode) => mode === "plan" || mode === "build") : void 0,
|
|
217
|
-
personalContext: body.personal_context === true
|
|
237
|
+
personalContext: body.personal_context === true,
|
|
238
|
+
workspaceIdentity: body.workspace_identity === true
|
|
218
239
|
};
|
|
219
240
|
} catch {
|
|
220
241
|
return { ok: false, openMessageId: null };
|
|
@@ -317,5 +338,6 @@ export {
|
|
|
317
338
|
postSessionEvents,
|
|
318
339
|
sendPodMessage,
|
|
319
340
|
streamAgentEvents,
|
|
320
|
-
textFromParts
|
|
341
|
+
textFromParts,
|
|
342
|
+
workspaceIdentityForPod
|
|
321
343
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
USER_AGENT,
|
|
3
3
|
configPath
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6JMFB3CV.js";
|
|
5
5
|
import {
|
|
6
6
|
TRACE_CONTENT_SCOPES,
|
|
7
7
|
redactTraceText
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-GHTAIKQ6.js";
|
|
9
9
|
import "./chunk-MLKGABMK.js";
|
|
10
10
|
|
|
11
11
|
// src/trace.ts
|
|
@@ -1,16 +1,16 @@
|
|
|
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,
|
|
12
12
|
redactTraceText
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-GHTAIKQ6.js";
|
|
14
14
|
import "./chunk-MLKGABMK.js";
|
|
15
15
|
|
|
16
16
|
// src/trace-library.ts
|
|
@@ -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,14 +8,15 @@
|
|
|
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"
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
17
18
|
"dev": "tsx src/index.ts",
|
|
18
|
-
"build": "tsup src/index.ts
|
|
19
|
+
"build": "tsup src/index.ts --format esm --target node20 --clean",
|
|
19
20
|
"test": "vitest run",
|
|
20
21
|
"typecheck": "tsc --noEmit"
|
|
21
22
|
},
|
|
@@ -45,15 +46,9 @@
|
|
|
45
46
|
"ignoreDeprecations": "6.0"
|
|
46
47
|
},
|
|
47
48
|
"resolve": [
|
|
48
|
-
"@agent/spec"
|
|
49
|
-
"@agent/spec/products"
|
|
49
|
+
"@agent/spec"
|
|
50
50
|
]
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
|
-
"exports": {
|
|
54
|
-
"./products": {
|
|
55
|
-
"types": "./dist/products.d.ts",
|
|
56
|
-
"import": "./dist/products.js"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
53
|
+
"exports": {}
|
|
59
54
|
}
|
|
@@ -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 }>;
|