@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.
Files changed (51) hide show
  1. package/dist/{chunk-ICTCB4VT.js → chunk-6JMFB3CV.js} +1 -1
  2. package/dist/{chunk-NAD26CDY.js → chunk-GHTAIKQ6.js} +80 -30
  3. package/dist/{chunk-S6A5V7XR.js → chunk-ZZ6UF7QA.js} +4 -3
  4. package/dist/index.js +152 -55
  5. package/dist/{src-KP6YIFNP.js → src-QBXMNV2V.js} +28 -6
  6. package/dist/{trace-SJHQ66JA.js → trace-NMBMU4GS.js} +2 -2
  7. package/dist/{trace-library-IWF7BVNB.js → trace-library-OWYB75AR.js} +97 -11
  8. package/package.json +6 -11
  9. package/vendor/trace-collect/SOURCE.json +6 -0
  10. package/vendor/trace-collect/collect.mjs +75 -0
  11. package/vendor/trace-collect/identity.d.ts +8 -0
  12. package/vendor/trace-collect/identity.mjs +23 -0
  13. package/vendor/trace-collect/muse.mjs +87 -0
  14. package/vendor/trace-collect/normalize.d.ts +6 -0
  15. package/vendor/trace-collect/normalize.mjs +312 -0
  16. package/vendor/trace-collect/scrub.d.ts +2 -0
  17. package/vendor/trace-collect/scrub.mjs +186 -0
  18. package/vendor/trace-collect/sinks/agentsky.mjs +156 -0
  19. package/vendor/trace-collect/sources/_cline-family.mjs +182 -0
  20. package/vendor/trace-collect/sources/_project-root.mjs +40 -0
  21. package/vendor/trace-collect/sources/amp.mjs +18 -0
  22. package/vendor/trace-collect/sources/chatgpt.mjs +203 -0
  23. package/vendor/trace-collect/sources/claude-ai.mjs +94 -0
  24. package/vendor/trace-collect/sources/claude-code.mjs +4 -0
  25. package/vendor/trace-collect/sources/claude-desktop.mjs +87 -0
  26. package/vendor/trace-collect/sources/cline.mjs +14 -0
  27. package/vendor/trace-collect/sources/codex.mjs +4 -0
  28. package/vendor/trace-collect/sources/continue.mjs +117 -0
  29. package/vendor/trace-collect/sources/copilot-chat.mjs +145 -0
  30. package/vendor/trace-collect/sources/cursor-chat.mjs +274 -0
  31. package/vendor/trace-collect/sources/cursor.mjs +4 -0
  32. package/vendor/trace-collect/sources/deepseek.mjs +172 -0
  33. package/vendor/trace-collect/sources/dsh.mjs +145 -0
  34. package/vendor/trace-collect/sources/gemini-cli.mjs +113 -0
  35. package/vendor/trace-collect/sources/gemini.mjs +150 -0
  36. package/vendor/trace-collect/sources/goose.mjs +93 -0
  37. package/vendor/trace-collect/sources/grok-cli.mjs +110 -0
  38. package/vendor/trace-collect/sources/grok.mjs +83 -0
  39. package/vendor/trace-collect/sources/hermes.mjs +4 -0
  40. package/vendor/trace-collect/sources/index.mjs +119 -0
  41. package/vendor/trace-collect/sources/jan.mjs +134 -0
  42. package/vendor/trace-collect/sources/kilo-code.mjs +14 -0
  43. package/vendor/trace-collect/sources/kiro.mjs +77 -0
  44. package/vendor/trace-collect/sources/lm-studio.mjs +84 -0
  45. package/vendor/trace-collect/sources/muse.mjs +11 -0
  46. package/vendor/trace-collect/sources/opencode.mjs +109 -0
  47. package/vendor/trace-collect/sources/qwen-code.mjs +95 -0
  48. package/vendor/trace-collect/sources/roo-code.mjs +14 -0
  49. package/vendor/trace-collect/sources/zed.mjs +70 -0
  50. package/dist/products.d.ts +0 -652
  51. 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-NAD26CDY.js";
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-ICTCB4VT.js";
4
+ } from "./chunk-6JMFB3CV.js";
5
5
  import {
6
6
  TRACE_CONTENT_SCOPES,
7
7
  redactTraceText
8
- } from "./chunk-NAD26CDY.js";
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-S6A5V7XR.js";
3
+ } from "./chunk-ZZ6UF7QA.js";
4
4
  import {
5
5
  USER_AGENT,
6
6
  apiBase,
7
7
  apiToken
8
- } from "./chunk-ICTCB4VT.js";
8
+ } from "./chunk-6JMFB3CV.js";
9
9
  import {
10
10
  SessionSourceCollector,
11
11
  publicRemote,
12
12
  redactTraceText
13
- } from "./chunk-NAD26CDY.js";
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
- function sourceFolders() {
70
- return [claudeRoot(), join(codexRoot(), "sessions"), join(codexRoot(), "archived_sessions")];
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 discoverSessions() {
192
- return [...claudeSessions(), ...await codexSessions()];
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
- let sessions = direct.length && direct.every(Boolean) ? direct : await discoverSessions();
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.parsed.harness, session_id: item.parsed.sessionId, sha256: item.sha256 }))
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.parsed.harness}/${encodeURIComponent(item.parsed.sessionId)}`, workspace, uploadBody(item.parsed, item.sha256, device));
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.20.1",
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 src/products.ts --format esm --target node20 --clean",
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,6 @@
1
+ {
2
+ "repository": "xiaoyinqu/agentgold",
3
+ "path": "skill/trace-market/scripts/lib",
4
+ "commit": "bf0c85cdb0a119d8c91b33b9cf07fad78cc830be",
5
+ "dirty": false
6
+ }
@@ -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 }>;