sessiongraph-app-darwin-x64 1.0.7 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessiongraph-app-darwin-x64",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Explore & visualize all your LLM sessions — darwin/x64",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,7 @@
10
10
  "access": "public",
11
11
  "registry": "https://registry.npmjs.org/"
12
12
  },
13
- "sessionGraphCommit": "917fd2aa2b7485b278838d1af1588b3182e8cc10",
13
+ "sessionGraphCommit": "d18168fb41a555de144bb65599ed05b58db761f1",
14
14
  "os": [
15
15
  "darwin"
16
16
  ],
@@ -17,9 +17,9 @@
17
17
  <key>CFBundlePackageType</key>
18
18
  <string>APPL</string>
19
19
  <key>CFBundleShortVersionString</key>
20
- <string>1.0.7</string>
20
+ <string>1.0.8</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>1.0.7</string>
22
+ <string>1.0.8</string>
23
23
  <key>CSResourcesFileMapped</key>
24
24
  <true/>
25
25
  <key>LSApplicationCategoryType</key>
@@ -2140,7 +2140,7 @@ import { createReadStream } from "node:fs";
2140
2140
  import { stat as stat5 } from "node:fs/promises";
2141
2141
  import { fileURLToPath as fileURLToPath5 } from "node:url";
2142
2142
  import { resolve as resolve2, extname, sep as sep4 } from "node:path";
2143
- import { randomBytes, timingSafeEqual } from "node:crypto";
2143
+ import { randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
2144
2144
 
2145
2145
  // node_modules/.pnpm/@opencode+client@2.2.1_effect@4.0.1/node_modules/@opencode/client/dist/chunks/mcp-ve4aws36.js
2146
2146
  import { spawn } from "node:child_process";
@@ -2784,7 +2784,7 @@ function extractContent(messages) {
2784
2784
  for (const raw of messages) {
2785
2785
  if (!raw || typeof raw !== "object") continue;
2786
2786
  const message2 = raw;
2787
- const time3 = Number(message2.time?.created) || 0;
2787
+ const time4 = Number(message2.time?.created) || 0;
2788
2788
  const seen = /* @__PURE__ */ new Set();
2789
2789
  const add = (value, kind) => {
2790
2790
  if (typeof value !== "string" || !value.trim() || value.startsWith("data:") || seen.has(value)) return;
@@ -2794,7 +2794,7 @@ function extractContent(messages) {
2794
2794
  if (size <= 0) return;
2795
2795
  const text2 = value.slice(0, size);
2796
2796
  remaining -= text2.length;
2797
- fragments.push({ text: text2, kind, time: time3 });
2797
+ fragments.push({ text: text2, kind, time: time4 });
2798
2798
  };
2799
2799
  const strings = (value, depth = 0) => {
2800
2800
  if (depth > 5 || remaining <= 0) {
@@ -2873,7 +2873,7 @@ var blankSession = (source, nativeID, directory2 = "") => ({
2873
2873
  cost: null,
2874
2874
  tokens: null
2875
2875
  });
2876
- var blankMessage = (id2, time3, role) => ({ id: id2, time: time3, role, fragments: [], files: [], tools: [] });
2876
+ var blankMessage = (id2, time4, role) => ({ id: id2, time: time4, role, fragments: [], files: [], tools: [] });
2877
2877
  function addText(message2, value, kind) {
2878
2878
  if (typeof value !== "string" || !value.trim() || value.startsWith("data:")) return;
2879
2879
  const remaining = Math.max(0, 128e3 - message2.fragments.reduce((n, f) => n + f.text.length, 0));
@@ -2897,7 +2897,7 @@ function toolText(message2, value, depth = 0) {
2897
2897
  }
2898
2898
 
2899
2899
  // server/activity.ts
2900
- var messageTime = (time3) => Number.isFinite(time3) && time3 > 0 && time3 <= 864e13 ? time3 : null;
2900
+ var messageTime = (time4) => Number.isFinite(time4) && time4 > 0 && time4 <= 864e13 ? time4 : null;
2901
2901
  var isUserMessage = (message2) => message2.role === "user" && message2.fragments.some((f) => f.kind === "You" && f.text.trim());
2902
2902
  var activitySQL = `SELECT min(time) AS firstMessageAt, max(time) AS lastMessageAt,
2903
2903
  coalesce(sum(is_user), 0) AS userMessages,
@@ -2928,8 +2928,8 @@ function eventText(value, limit = 32e3) {
2928
2928
  }
2929
2929
  }
2930
2930
  function eventFor(message2, id2, kind, name, text2 = "", fields2 = {}) {
2931
- const time3 = messageTime(message2.time);
2932
- const event = { id: id2, kind, name, role: message2.role, time: time3, timeSource: time3 ? "message" : "unknown", text: eventText(text2, 64e3), ...fields2 };
2931
+ const time4 = messageTime(message2.time);
2932
+ const event = { id: id2, kind, name, role: message2.role, time: time4, timeSource: time4 ? "message" : "unknown", text: eventText(text2, 64e3), ...fields2 };
2933
2933
  event.partial ||= [event.text, event.input, event.output].some((value) => /\[(shortened|omitted|unavailable)\]/.test(value || "")) || text2.length > 64e3;
2934
2934
  return event;
2935
2935
  }
@@ -2997,7 +2997,7 @@ function normalizedEvents(message2, sessionID) {
2997
2997
  const files = message2.files.map((file, i) => eventFor(message2, `file:${i}`, "file_change", file.path, `${file.source} \xB7 ${file.steps} recorded step(s)`, { nativeType: file.source }));
2998
2998
  return [...events, ...files].slice(0, 200).map((event) => {
2999
2999
  const text2 = String(event.text || "").slice(0, 64e3), input = event.input?.slice(0, 32e3), output = event.output?.slice(0, 64e3);
3000
- const time3 = messageTime(event.time || 0);
3000
+ const time4 = messageTime(event.time || 0);
3001
3001
  return {
3002
3002
  ...event,
3003
3003
  id: digest([message2.id, event.id]),
@@ -3009,8 +3009,8 @@ function normalizedEvents(message2, sessionID) {
3009
3009
  callID: event.callID ? String(event.callID).slice(0, 512) : void 0,
3010
3010
  parentID: event.parentID ? String(event.parentID).slice(0, 512) : void 0,
3011
3011
  endTime: messageTime(event.endTime || 0),
3012
- time: time3,
3013
- timeSource: time3 ? event.timeSource : "unknown",
3012
+ time: time4,
3013
+ timeSource: time4 ? event.timeSource : "unknown",
3014
3014
  text: text2,
3015
3015
  input,
3016
3016
  output,
@@ -3031,8 +3031,8 @@ function opencodeEvents(raw, message2) {
3031
3031
  const events = [];
3032
3032
  for (const [i, part] of (Array.isArray(raw.content) ? raw.content.slice(0, 200) : []).entries()) {
3033
3033
  const id2 = `${part.type}:${part.id || i}`, native = { nativeID: part.id, nativeType: String(part.type) };
3034
- const time3 = messageTime(part.time?.created);
3035
- const timing = time3 ? { time: time3, timeSource: "recorded" } : {};
3034
+ const time4 = messageTime(part.time?.created);
3035
+ const timing = time4 ? { time: time4, timeSource: "recorded" } : {};
3036
3036
  if (part.type === "text") events.push(eventFor(message2, id2, "message", "Assistant message", part.text || "", { ...native, ...timing }));
3037
3037
  else if (part.type === "reasoning") events.push(eventFor(message2, id2, "reasoning", "Recorded reasoning", part.text || "", { ...native, ...timing }));
3038
3038
  else if (part.type === "tool") {
@@ -30449,8 +30449,8 @@ async function claudeTranscript(path, revision2) {
30449
30449
  }
30450
30450
  entries2.push(row);
30451
30451
  if (typeof row.uuid === "string") {
30452
- const time3 = typeof row.timestamp === "string" ? Date.parse(row.timestamp) : NaN;
30453
- times.set(row.uuid, Number.isFinite(time3) && time3 > 0 ? time3 : 0);
30452
+ const time4 = typeof row.timestamp === "string" ? Date.parse(row.timestamp) : NaN;
30453
+ times.set(row.uuid, Number.isFinite(time4) && time4 > 0 ? time4 : 0);
30454
30454
  }
30455
30455
  } catch {
30456
30456
  partial = true;
@@ -30482,7 +30482,7 @@ function claudeTranscriptInfo(transcript, sessionId) {
30482
30482
  }).find(Boolean);
30483
30483
  const title = rows2.findLast((row) => ["custom-title", "ai-title", "summary"].includes(String(row.type)) && typeof (row.customTitle || row.aiTitle || row.summary) === "string");
30484
30484
  const cwd2 = rows2.findLast((row) => row.type === "relocated" && typeof row.relocatedCwd === "string")?.relocatedCwd || messages.find((row) => typeof row.cwd === "string")?.cwd;
30485
- const createdAt = messages.map((row) => transcript.times.get(String(row.uuid))).find((time3) => time3 && time3 > 0);
30485
+ const createdAt = messages.map((row) => transcript.times.get(String(row.uuid))).find((time4) => time4 && time4 > 0);
30486
30486
  return { sessionId, summary: String(title?.customTitle || title?.aiTitle || title?.summary || firstPrompt || "Claude Code session"), firstPrompt, cwd: typeof cwd2 === "string" ? cwd2 : void 0, createdAt, lastModified: transcript.modified, fileSize: transcript.size };
30487
30487
  }
30488
30488
  function claudeTranscriptStore(transcript, sessionId, agent) {
@@ -31908,8 +31908,8 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
31908
31908
 
31909
31909
  // server/adapters/cursor-message.ts
31910
31910
  function cursorTime(value) {
31911
- const time3 = typeof value === "number" ? value : typeof value === "string" && /(?:Z|[+-]\d\d:\d\d)$/.test(value) ? Date.parse(value) : 0;
31912
- return Number.isFinite(time3) && time3 > 0 && time3 <= 864e13 ? time3 : 0;
31911
+ const time4 = typeof value === "number" ? value : typeof value === "string" && /(?:Z|[+-]\d\d:\d\d)$/.test(value) ? Date.parse(value) : 0;
31912
+ return Number.isFinite(time4) && time4 > 0 && time4 <= 864e13 ? time4 : 0;
31913
31913
  }
31914
31914
  function cursorEnvelopeTime(text2) {
31915
31915
  const value = text2.match(/^\s*<timestamp>([^<]+)<\/timestamp>/)?.[1];
@@ -31937,9 +31937,9 @@ function cursorMessage(raw, fallbackID) {
31937
31937
  if (!["user", "assistant", "tool", "system"].includes(role)) return;
31938
31938
  const content = body2.content;
31939
31939
  const firstText = typeof content === "string" ? content : Array.isArray(content) && content[0]?.type === "text" ? content[0].text : "";
31940
- const time3 = cursorTime(raw.timestamp) || cursorTime(body2.timestamp) || cursorTime(body2.createdAt) || (role === "user" && typeof firstText === "string" ? cursorEnvelopeTime(firstText) : 0);
31940
+ const time4 = cursorTime(raw.timestamp) || cursorTime(body2.timestamp) || cursorTime(body2.createdAt) || (role === "user" && typeof firstText === "string" ? cursorEnvelopeTime(firstText) : 0);
31941
31941
  const native = [raw.uuid, raw.id, body2.uuid, body2.id].find((id2) => typeof id2 === "string" && id2.length && id2.length <= 512);
31942
- const msg = blankMessage(native || fallbackID, time3, role);
31942
+ const msg = blankMessage(native || fallbackID, time4, role);
31943
31943
  msg.events = role === "tool" ? [eventFor(msg, "result", "tool_result", body2.name || "Tool result", "", { callID: body2.tool_call_id, output: eventText(content, 64e3), status: "unknown" })] : blockEvents(msg, content);
31944
31944
  const kind = role === "user" ? "You" : role === "assistant" ? "Assistant" : "Tool";
31945
31945
  const text2 = (value) => addText(msg, role === "user" && typeof value === "string" ? cursorUserText(value) : value, kind);
@@ -33675,7 +33675,7 @@ function readEvents(db2, session, options = {}) {
33675
33675
  };
33676
33676
  }
33677
33677
 
33678
- // src/preferences.ts
33678
+ // packages/core/src/preferences.ts
33679
33679
  var DEFAULT_PREFERENCES = Object.freeze({
33680
33680
  autoImport: true,
33681
33681
  muted: false,
@@ -33717,7 +33717,7 @@ function normalizePreferences(value) {
33717
33717
  return result;
33718
33718
  }
33719
33719
 
33720
- // src/view-state.ts
33720
+ // packages/core/src/view-state.ts
33721
33721
  var DEFAULT_VIEW_STATE = Object.freeze({
33722
33722
  sourceID: "",
33723
33723
  projectID: "",
@@ -33913,12 +33913,12 @@ var SessionGraphStore = class {
33913
33913
  const count = this.db.prepare("SELECT COUNT(*) AS n FROM sessions WHERE source=?").get(id2);
33914
33914
  return { sessions: Number(count.n), lastImport: row?.last_import ? Number(row.last_import) : void 0, error: row?.error ? String(row.error) : void 0 };
33915
33915
  }
33916
- library(sources, importing) {
33916
+ library(sources2, importing) {
33917
33917
  return {
33918
33918
  sourceID: this.setting("libraryID"),
33919
33919
  onboarding: this.setting("selected") === void 0,
33920
33920
  autoImport: this.autoImportEnabled(),
33921
- sources,
33921
+ sources: sources2,
33922
33922
  importing,
33923
33923
  preferences: this.setting("preferences") ? { ...normalizePreferences(this.setting("preferences")), autoImport: this.autoImportEnabled() } : void 0,
33924
33924
  viewState: normalizeViewState(this.setting("viewState")),
@@ -34305,14 +34305,14 @@ function handoffDocument(snapshot2) {
34305
34305
  ${text3.slice(-half)}`;
34306
34306
  };
34307
34307
  const block = (message2, limit) => {
34308
- const time3 = messageTime(message2.time) ? new Date(message2.time).toISOString() : "time unavailable";
34308
+ const time4 = messageTime(message2.time) ? new Date(message2.time).toISOString() : "time unavailable";
34309
34309
  const content = message2.fragments.map((f) => `[${f.kind}]
34310
34310
  ${clip(f.text, f.kind === "Tool" ? 2e3 : 12e3)}`).join("\n\n");
34311
34311
  const files = message2.files.length ? `
34312
34312
 
34313
34313
  Recorded file changes: ${message2.files.slice(0, 60).map((f) => JSON.stringify(f.path)).join(", ")}` : "";
34314
34314
  shortened ||= message2.files.length > 60 || Boolean(message2.partial);
34315
- return `## ${message2.role} \xB7 ${time3}
34315
+ return `## ${message2.role} \xB7 ${time4}
34316
34316
 
34317
34317
  ${clip(content + files, limit)}
34318
34318
  `;
@@ -34441,7 +34441,349 @@ async function launchTerminal(session, prepared) {
34441
34441
  } else await exec(invocation.file, invocation.args, { timeout: 15e3, maxBuffer: 16384 });
34442
34442
  }
34443
34443
 
34444
+ // server/publishing.ts
34445
+ import { createHash as createHash5, randomBytes } from "node:crypto";
34446
+ import { chmod, mkdir as mkdir2, readFile as readFile5, rename as rename3, rm as rm5, writeFile as writeFile3 } from "node:fs/promises";
34447
+ import { dirname as dirname10, join as join25 } from "node:path";
34448
+
34449
+ // packages/core/src/layout.ts
34450
+ var directoryPath = (directory2) => (/^(?:[a-z]:[\\/]|\\\\)/i.test(directory2) ? directory2.replace(/\\/g, "/") : directory2).replace(/\/+$/, "") || (directory2.startsWith("/") ? "/" : "");
34451
+ var laneKey = (session) => session.directory ? JSON.stringify(["directory", directoryPath(session.directory)]) : JSON.stringify(["project", session.source || "opencode", session.projectID || session.id]);
34452
+
34453
+ // packages/core/src/sources.ts
34454
+ var sources = {
34455
+ opencode: { name: "OpenCode", icon: "opencode" },
34456
+ "claude-code": { name: "Claude Code", icon: "claude" },
34457
+ codex: { name: "Codex", icon: "openai" },
34458
+ "t3-code": { name: "T3 Code", icon: "t3" },
34459
+ cursor: { name: "Cursor", icon: "cursor" },
34460
+ gemini: { name: "Gemini CLI", icon: "gemini" },
34461
+ "qwen-code": { name: "Qwen Code", icon: "qwen" },
34462
+ pi: { name: "Pi", icon: "pi" },
34463
+ copilot: { name: "GitHub Copilot CLI", icon: "copilot" },
34464
+ "copilot-chat": { name: "GitHub Copilot Chat (VS Code)", icon: "copilot" },
34465
+ cline: { name: "Cline", icon: "cline" },
34466
+ "roo-code": { name: "Roo Code", icon: "roo" },
34467
+ "kilo-code": { name: "Kilo Code", icon: "kilo" },
34468
+ continue: { name: "Continue", icon: "continue" },
34469
+ "prime-agent": { name: "Prime Agent", icon: "prime" },
34470
+ omp: { name: "Oh My Pi", icon: "pi" },
34471
+ senpi: { name: "Senpi / OmO Native", icon: "senpi" },
34472
+ kimchi: { name: "Kimchi", icon: "kimchi" },
34473
+ gajae: { name: "Gajae-Code", icon: "gajae" },
34474
+ openclaw: { name: "OpenClaw", icon: "openclaw" },
34475
+ hermes: { name: "Hermes Agent", icon: "hermes" },
34476
+ amp: { name: "Amp", icon: "amp" },
34477
+ droid: { name: "Factory Droid", icon: "droid" },
34478
+ codebuff: { name: "Codebuff", icon: "codebuff" },
34479
+ freebuff: { name: "Freebuff", icon: "freebuff" },
34480
+ kimi: { name: "Kimi CLI / Code", icon: "kimi" },
34481
+ grok: { name: "Grok Build", icon: "grok" },
34482
+ jcode: { name: "Jcode", icon: "jcode" },
34483
+ "cherry-studio": { name: "Cherry Studio", icon: "cherry" },
34484
+ "command-code": { name: "Command Code", icon: "commandcode" },
34485
+ zcode: { name: "ZCode", icon: "zcode" },
34486
+ codebuddy: { name: "CodeBuddy", icon: "codebuddy" },
34487
+ workbuddy: { name: "WorkBuddy", icon: "codebuddy" },
34488
+ proma: { name: "Proma", icon: "proma" },
34489
+ "deepseek-harness": { name: "DeepSeek Harness", icon: "deepseek" },
34490
+ aside: { name: "Aside", icon: "aside" },
34491
+ vibe: { name: "Mistral Vibe", icon: "mistral" },
34492
+ muse: { name: "Muse Code", icon: "muse" },
34493
+ junie: { name: "Junie", icon: "junie" },
34494
+ augment: { name: "Augment Code", icon: "augment" },
34495
+ fx: { name: "Fx", icon: "fx" },
34496
+ "minimax-code": { name: "MiniMax Code", icon: "minimax" },
34497
+ opencodereview: { name: "OpenCodeReview", icon: "opencode" },
34498
+ goose: { name: "Goose", icon: "goose" },
34499
+ zed: { name: "Zed Agent", icon: "zed" },
34500
+ devin: { name: "Devin CLI", icon: "devin" },
34501
+ "devin-desktop": { name: "Devin Desktop", icon: "devin" },
34502
+ crush: { name: "Crush", icon: "crush" },
34503
+ octofriend: { name: "Octofriend", icon: "octofriend" },
34504
+ "kilo-cli": { name: "Kilo CLI", icon: "kilo" },
34505
+ "mimo-code": { name: "MiMo Code", icon: "mimo" },
34506
+ "mimo-desktop": { name: "Xiaomi MiMo AI", icon: "mimo" },
34507
+ unsloth: { name: "Unsloth Studio", icon: "unsloth" },
34508
+ kiro: { name: "Kiro", icon: "kiro" },
34509
+ mux: { name: "Mux", icon: "mux" },
34510
+ antigravity: { name: "Antigravity", icon: "antigravity" },
34511
+ "antigravity-cli": { name: "Antigravity CLI", icon: "antigravity" },
34512
+ "antigravity-extension": { name: "Antigravity IDE Extension", icon: "antigravity" },
34513
+ reasonix: { name: "Reasonix", icon: "reasonix" },
34514
+ trae: { name: "Trae / Trae Solo", icon: "trae" },
34515
+ warp: { name: "Warp / Oz", icon: "warp" },
34516
+ "lm-studio": { name: "LM Studio", icon: "lmstudio" },
34517
+ hindsight: { name: "Hindsight", icon: "hindsight" }
34518
+ };
34519
+ var isKnownSource = (id2) => typeof id2 === "string" && Object.hasOwn(sources, id2);
34520
+
34521
+ // packages/core/src/public-graph.ts
34522
+ var DEFAULT_PUBLISHING = { tokens: true, timestamps: true, messageCounts: true, messageTimestamps: true, model: true, harness: true };
34523
+ function publishingSettings(value) {
34524
+ const result = { ...DEFAULT_PUBLISHING };
34525
+ if (object2(value)) {
34526
+ for (const key of Object.keys(result)) if (typeof value[key] === "boolean") result[key] = value[key];
34527
+ }
34528
+ if (!result.timestamps || !result.messageCounts) result.messageTimestamps = false;
34529
+ return result;
34530
+ }
34531
+ var PUBLIC_GRAPH_MAX_SESSIONS = 1e4;
34532
+ var PUBLIC_GRAPH_MAX_BYTES = 2e6;
34533
+ var PUBLIC_GRAPH_MAX_MESSAGE_TIMES = 1e5;
34534
+ var modelPattern = /^(?:gpt-[3-6](?:\.[0-9])?(?:-(?:turbo|mini|nano|pro|codex|astra|instant|thinking|chat|latest|preview|[0-9]{4,8}))*|o[1-4](?:-(?:mini|pro|preview|[0-9]{4,8}))*|claude-(?:(?:sonnet|opus|haiku)-[3-5](?:[.-][0-9])?|[3-5](?:-[0-9])?-(?:sonnet|opus|haiku))(?:-(?:latest|thinking|[0-9]{8}))*|gemini-[1-4](?:\.[0-9])?-(?:pro|flash)(?:-(?:lite|preview|exp|latest|[0-9]{2,4}))*|deepseek-(?:chat|reasoner|v[3-4](?:\.[0-9])?|r1)|grok-[2-5](?:\.[0-9])?(?:-(?:fast|code|reasoning|non-reasoning|[0-9]))*|kimi-k[1-3](?:\.[0-9])?(?:-thinking)?|minimax-m[1-3](?:\.[0-9])?)$/i;
34535
+ var publicModel = (value) => {
34536
+ const name = value.split("/").at(-1) || "";
34537
+ return name.length <= 100 && modelPattern.test(name) ? name.toLowerCase() : null;
34538
+ };
34539
+ function publishableGraph(sessions, settings, messages = /* @__PURE__ */ new Map()) {
34540
+ const fields2 = publishingSettings(settings);
34541
+ const ordered = [...sessions].sort((a, b) => a.created - b.created || a.id.localeCompare(b.id));
34542
+ const ids = new Map(ordered.map((session, index) => [session.id, index]));
34543
+ const groups = /* @__PURE__ */ new Map();
34544
+ return validatePublicGraph({ version: 1, settings: fields2, sessions: ordered.map((session) => {
34545
+ const key = laneKey(session);
34546
+ if (!groups.has(key)) groups.set(key, groups.size);
34547
+ const result = {
34548
+ id: ids.get(session.id),
34549
+ group: groups.get(key),
34550
+ parent: session.parentID ? ids.get(session.parentID) ?? null : null,
34551
+ fork: session.forkID ? ids.get(session.forkID) ?? null : null
34552
+ };
34553
+ if (fields2.timestamps) result.timestamps = { created: session.created, lastMessageAt: session.activity?.lastMessageAt ?? null };
34554
+ if (fields2.tokens) result.tokens = session.tokens;
34555
+ const stats = messages.get(session.id);
34556
+ if (fields2.messageCounts) result.messages = { count: stats?.count ?? null, untimed: stats?.untimed ?? null, partial: stats?.partial ?? true };
34557
+ if (fields2.messageTimestamps) result.messageTimes = { values: stats?.times ?? [], limited: stats?.timesLimited ?? true };
34558
+ if (fields2.model) result.model = publicModel(session.model);
34559
+ if (fields2.harness) result.harness = isKnownSource(session.source) ? session.source : null;
34560
+ return result;
34561
+ }) });
34562
+ }
34563
+ var object2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
34564
+ var exact = (value, keys) => object2(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
34565
+ var integer = (value, max = Number.MAX_SAFE_INTEGER) => Number.isSafeInteger(value) && value >= 0 && value <= max;
34566
+ var time3 = (value) => integer(value, Date.UTC(2100, 0, 1));
34567
+ var nullableCount = (value) => value === null || integer(value);
34568
+ function validatePublicGraph(value) {
34569
+ const fail2 = () => {
34570
+ throw new Error("Invalid anonymous graph. Reopen the publishing preview and try again.");
34571
+ };
34572
+ if (!exact(value, ["version", "settings", "sessions"]) || value.version !== 1 || !Array.isArray(value.sessions)) return fail2();
34573
+ if (!exact(value.settings, Object.keys(DEFAULT_PUBLISHING)) || Object.values(value.settings).some((v3) => typeof v3 !== "boolean")) return fail2();
34574
+ const settings = value.settings;
34575
+ if (settings.messageTimestamps && (!settings.timestamps || !settings.messageCounts)) return fail2();
34576
+ if (value.sessions.length < 1 || value.sessions.length > PUBLIC_GRAPH_MAX_SESSIONS) throw new Error(`Choose between 1 and ${PUBLIC_GRAPH_MAX_SESSIONS.toLocaleString("en")} sessions to publish.`);
34577
+ const keys = ["id", "group", "parent", "fork", ...settings.timestamps ? ["timestamps"] : [], ...settings.tokens ? ["tokens"] : [], ...settings.messageCounts ? ["messages"] : [], ...settings.messageTimestamps ? ["messageTimes"] : [], ...settings.model ? ["model"] : [], ...settings.harness ? ["harness"] : []];
34578
+ let times = 0;
34579
+ for (let index = 0; index < value.sessions.length; index++) {
34580
+ const s = value.sessions[index];
34581
+ if (!exact(s, keys) || s.id !== index || !integer(s.group, value.sessions.length - 1)) return fail2();
34582
+ for (const key of ["parent", "fork"]) if (s[key] !== null && (!integer(s[key], value.sessions.length - 1) || s[key] === index)) return fail2();
34583
+ if (settings.timestamps && (!exact(s.timestamps, ["created", "lastMessageAt"]) || !time3(s.timestamps.created) || s.timestamps.lastMessageAt !== null && !time3(s.timestamps.lastMessageAt))) return fail2();
34584
+ if (settings.tokens && !nullableCount(s.tokens)) return fail2();
34585
+ if (settings.messageCounts && (!exact(s.messages, ["count", "untimed", "partial"]) || !nullableCount(s.messages.count) || !nullableCount(s.messages.untimed) || typeof s.messages.partial !== "boolean")) return fail2();
34586
+ if (settings.messageTimestamps) {
34587
+ if (!exact(s.messageTimes, ["values", "limited"]) || !Array.isArray(s.messageTimes.values) || s.messageTimes.values.length > 2e3 || typeof s.messageTimes.limited !== "boolean" || s.messageTimes.values.some((v3) => !time3(v3))) return fail2();
34588
+ times += s.messageTimes.values.length;
34589
+ if (times > PUBLIC_GRAPH_MAX_MESSAGE_TIMES) return fail2();
34590
+ }
34591
+ if (settings.model && s.model !== null && (typeof s.model !== "string" || publicModel(s.model) !== s.model)) return fail2();
34592
+ if (settings.harness && s.harness !== null && (typeof s.harness !== "string" || !isKnownSource(s.harness))) return fail2();
34593
+ }
34594
+ const sessions = value.sessions;
34595
+ const colors = new Uint8Array(sessions.length);
34596
+ for (let i = 0; i < sessions.length; i++) {
34597
+ if (colors[i]) continue;
34598
+ const stack = [[i, false]];
34599
+ while (stack.length) {
34600
+ const [id2, exit] = stack.pop();
34601
+ if (exit) {
34602
+ colors[id2] = 2;
34603
+ continue;
34604
+ }
34605
+ if (colors[id2] === 1) return fail2();
34606
+ if (colors[id2] === 2) continue;
34607
+ colors[id2] = 1;
34608
+ stack.push([id2, true]);
34609
+ for (const parent2 of [sessions[id2].parent, sessions[id2].fork]) if (parent2 !== null) stack.push([parent2, false]);
34610
+ }
34611
+ }
34612
+ if (new TextEncoder().encode(JSON.stringify(value)).length > PUBLIC_GRAPH_MAX_BYTES) throw new Error("This graph is too large. Choose a smaller date range.");
34613
+ return value;
34614
+ }
34615
+
34616
+ // server/publishing.ts
34617
+ var PublishingError = class extends Error {
34618
+ constructor(message2, status2 = 400) {
34619
+ super(message2);
34620
+ this.status = status2;
34621
+ }
34622
+ status;
34623
+ };
34624
+ var SITE = "https://sessiongraph.ai";
34625
+ var hash = (value) => createHash5("sha256").update(value).digest("hex");
34626
+ var token = () => randomBytes(32).toString("base64url");
34627
+ function preparePublicGraph(store, ids, inputSettings) {
34628
+ if (!Array.isArray(ids) || !ids.length || ids.length > PUBLIC_GRAPH_MAX_SESSIONS || ids.some((id2) => typeof id2 !== "string") || new Set(ids).size !== ids.length) throw new PublishingError("Choose a valid set of sessions.");
34629
+ const settings = publishingSettings(inputSettings);
34630
+ const stats = /* @__PURE__ */ new Map();
34631
+ const count = store.db.prepare("SELECT count(*) AS count, count(*) FILTER (WHERE time IS NULL) AS untimed FROM timeline_items WHERE session_id=? AND kind='message' AND role IN ('user','assistant')");
34632
+ const times = store.db.prepare("SELECT time FROM timeline_items WHERE session_id=? AND kind='message' AND role IN ('user','assistant') AND time IS NOT NULL ORDER BY time,ordinal LIMIT ?");
34633
+ const coverage2 = store.db.prepare("SELECT partial FROM timeline_coverage WHERE session_id=?");
34634
+ let remaining = PUBLIC_GRAPH_MAX_MESSAGE_TIMES;
34635
+ store.db.exec("BEGIN");
34636
+ try {
34637
+ const sessions = ids.map((id2) => {
34638
+ const session = store.getSession(id2);
34639
+ if (!session) throw new PublishingError("Your session selection changed. Reopen publishing.");
34640
+ if (settings.messageCounts) {
34641
+ const known = session.coverage?.history !== "pending" && !!coverage2.get(id2);
34642
+ const totals = count.get(id2);
34643
+ const rows2 = settings.messageTimestamps ? times.all(id2, Math.min(remaining, 2e3)) : [];
34644
+ remaining -= rows2.length;
34645
+ const values = rows2.map((row) => Number(row.time));
34646
+ stats.set(id2, {
34647
+ count: known ? Number(totals.count) : null,
34648
+ untimed: known ? Number(totals.untimed) : null,
34649
+ partial: !known || session.coverage?.history !== "complete" || Boolean(coverage2.get(id2)?.partial),
34650
+ times: values,
34651
+ timesLimited: !known || Number(totals.count) - Number(totals.untimed) > values.length
34652
+ });
34653
+ }
34654
+ return session;
34655
+ });
34656
+ const result = publishableGraph(sessions, settings, stats);
34657
+ store.db.exec("COMMIT");
34658
+ return result;
34659
+ } catch (error) {
34660
+ store.db.exec("ROLLBACK");
34661
+ throw error;
34662
+ }
34663
+ }
34664
+ var PublishingClient = class {
34665
+ constructor(credentialPath = join25(dirname10(databasePath()), "account.json"), fetcher = fetch) {
34666
+ this.credentialPath = credentialPath;
34667
+ this.fetcher = fetcher;
34668
+ }
34669
+ credentialPath;
34670
+ fetcher;
34671
+ pending;
34672
+ preview;
34673
+ busy = false;
34674
+ async exclusive(work) {
34675
+ if (this.busy) throw new PublishingError("Wait for the current account operation to finish.", 409);
34676
+ this.busy = true;
34677
+ try {
34678
+ return await work();
34679
+ } finally {
34680
+ this.busy = false;
34681
+ }
34682
+ }
34683
+ async credential() {
34684
+ try {
34685
+ const saved = JSON.parse(await readFile5(this.credentialPath, "utf8"));
34686
+ if (typeof saved.token !== "string" || !/^[\w-]{43}$/.test(saved.token) || typeof saved.username !== "string" || !Number.isFinite(Date.parse(saved.expiresAt)) || Date.parse(saved.expiresAt) <= Date.now()) return null;
34687
+ return saved;
34688
+ } catch (error) {
34689
+ if (error.code === "ENOENT") return null;
34690
+ throw new PublishingError("Could not read your saved login. Sign out and try again.");
34691
+ }
34692
+ }
34693
+ async save(value) {
34694
+ await mkdir2(dirname10(this.credentialPath), { recursive: true, mode: 448 });
34695
+ const temporary = `${this.credentialPath}.${randomBytes(8).toString("hex")}`;
34696
+ try {
34697
+ await writeFile3(temporary, JSON.stringify(value), { mode: 384, flag: "wx" });
34698
+ await rename3(temporary, this.credentialPath);
34699
+ await chmod(this.credentialPath, 384);
34700
+ } finally {
34701
+ await rm5(temporary, { force: true });
34702
+ }
34703
+ }
34704
+ async call(path, method = "GET", value, credential) {
34705
+ const response = await this.fetcher(`${SITE}/api/${path}`, { method, redirect: "error", signal: AbortSignal.timeout(25e3), headers: { "Content-Type": "application/json", "x-sessiongraph-action": "1", ...credential ? { Authorization: `Bearer ${credential.token}` } : {} }, ...value === void 0 ? {} : { body: JSON.stringify(value) } });
34706
+ const data = await response.json().catch(() => ({}));
34707
+ if (!response.ok) {
34708
+ if (response.status === 401) await rm5(this.credentialPath, { force: true });
34709
+ throw new PublishingError(typeof data.error === "string" ? data.error : "The publishing service is unavailable.", response.status);
34710
+ }
34711
+ return data;
34712
+ }
34713
+ async status() {
34714
+ const credential = await this.credential();
34715
+ if (!credential) return { username: null, publication: null };
34716
+ return this.call("account", "GET", void 0, credential);
34717
+ }
34718
+ login() {
34719
+ if (this.busy) throw new PublishingError("Wait for the current account operation to finish.", 409);
34720
+ const verifier = token(), challenge = hash(verifier);
34721
+ this.pending = { verifier, expires: Date.now() + 6e5, nextPoll: 0 };
34722
+ return { url: `${SITE}/account?device=${challenge}`, code: challenge.slice(0, 8).toUpperCase() };
34723
+ }
34724
+ async poll() {
34725
+ const pending2 = this.pending;
34726
+ if (!pending2 || pending2.expires <= Date.now()) {
34727
+ this.pending = void 0;
34728
+ throw new PublishingError("Login expired. Start again.");
34729
+ }
34730
+ if (this.busy || pending2.nextPoll > Date.now()) return { pending: true };
34731
+ pending2.nextPoll = Date.now() + 3e3;
34732
+ return this.exclusive(async () => {
34733
+ const result = await this.call("auth/device", "POST", { verifier: pending2.verifier });
34734
+ if (this.pending !== pending2 || result.pending) return { pending: true };
34735
+ if (typeof result.token !== "string" || !/^[\w-]{43}$/.test(result.token) || typeof result.username !== "string" || !Number.isFinite(Date.parse(result.expiresAt))) throw new PublishingError("Invalid login response.");
34736
+ await this.save(result);
34737
+ this.pending = void 0;
34738
+ return { username: result.username };
34739
+ });
34740
+ }
34741
+ prepare(store, ids, settings) {
34742
+ this.preview = void 0;
34743
+ let graph;
34744
+ try {
34745
+ graph = preparePublicGraph(store, ids, settings);
34746
+ } catch (error) {
34747
+ if (error instanceof PublishingError) throw error;
34748
+ throw new PublishingError(error instanceof Error ? error.message : "Could not prepare the public graph.");
34749
+ }
34750
+ const id2 = token();
34751
+ this.preview = { id: id2, graph, expires: Date.now() + 18e5 };
34752
+ store.setting("publishing", graph.settings);
34753
+ return { id: id2, graph };
34754
+ }
34755
+ async publish(previewID, confirmed) {
34756
+ return this.exclusive(async () => {
34757
+ const preview = this.preview;
34758
+ if (confirmed !== true || !preview || preview.id !== previewID || preview.expires < Date.now()) throw new PublishingError("Review a fresh preview before publishing.");
34759
+ const credential = await this.credential();
34760
+ if (!credential) throw new PublishingError("Sign in before publishing.", 401);
34761
+ const result = await this.call("publication", "PUT", { confirmed: true, graph: preview.graph }, credential);
34762
+ if (this.preview === preview) this.preview = void 0;
34763
+ return result;
34764
+ });
34765
+ }
34766
+ async unpublish() {
34767
+ return this.exclusive(async () => {
34768
+ const credential = await this.credential();
34769
+ if (!credential) throw new PublishingError("Sign in before unpublishing.", 401);
34770
+ return this.call("publication", "DELETE", void 0, credential);
34771
+ });
34772
+ }
34773
+ async logout() {
34774
+ return this.exclusive(async () => {
34775
+ this.pending = void 0;
34776
+ this.preview = void 0;
34777
+ const credential = await this.credential();
34778
+ if (credential) await this.call("auth/logout", "POST", {}, credential);
34779
+ await rm5(this.credentialPath, { force: true });
34780
+ return { signedOut: true };
34781
+ });
34782
+ }
34783
+ };
34784
+
34444
34785
  // server/middleware.ts
34786
+ var publishing = new PublishingClient();
34445
34787
  var generation = /* @__PURE__ */ Symbol("sessiongraph-backend");
34446
34788
  var runtime = globalThis;
34447
34789
  function library() {
@@ -34494,6 +34836,24 @@ async function sessionGraphMiddleware(req, res, next) {
34494
34836
  if (req.method !== "GET" && typeof req.headers["x-sessiongraph-action"] !== "string") return json4(403, { error: "Use the SessionGraph application." });
34495
34837
  try {
34496
34838
  const { store, importer } = library();
34839
+ if (url.pathname.startsWith("/api/sessiongraph/publishing")) {
34840
+ const action = url.pathname.slice("/api/sessiongraph/publishing".length);
34841
+ if (action === "/settings" && req.method === "GET") return json4(200, publishingSettings(store.setting("publishing")));
34842
+ if (action === "/status" && req.method === "GET") return json4(200, await publishing.status());
34843
+ if (action === "/login" && req.method === "POST") return json4(200, publishing.login());
34844
+ if (action === "/poll" && req.method === "POST") return json4(200, await publishing.poll());
34845
+ if (action === "/logout" && req.method === "POST") return json4(200, await publishing.logout());
34846
+ if (action === "/preview" && req.method === "POST") {
34847
+ const value = await body(req);
34848
+ return json4(200, publishing.prepare(store, value?.ids, value?.settings));
34849
+ }
34850
+ if (action === "/publish" && req.method === "POST") {
34851
+ const value = await body(req);
34852
+ return json4(200, await publishing.publish(value?.id, value?.confirmed));
34853
+ }
34854
+ if (action === "/unpublish" && req.method === "POST") return json4(200, await publishing.unpublish());
34855
+ return json4(405, { error: "Method not allowed." });
34856
+ }
34497
34857
  if (url.pathname === "/api/sessiongraph/settings" && req.method === "POST") {
34498
34858
  const value = await body(req);
34499
34859
  if (!value || typeof value !== "object" || Array.isArray(value) || !["autoImport", "preferences", "viewState"].some((key) => value[key] !== void 0)) return json4(400, { error: "Choose valid settings." });
@@ -34535,8 +34895,8 @@ async function sessionGraphMiddleware(req, res, next) {
34535
34895
  }
34536
34896
  }
34537
34897
  if (url.pathname === "/api/sessiongraph" && req.method === "GET") {
34538
- const sources = adapters.map((a) => ({ id: a.id, name: a.name, description: a.description, capabilities: a.capabilities, selected: store.selected().includes(a.id), available: false, ...store.sourceInfo(a.id) }));
34539
- return json4(200, store.library(sources, importer.status.running));
34898
+ const sources2 = adapters.map((a) => ({ id: a.id, name: a.name, description: a.description, capabilities: a.capabilities, selected: store.selected().includes(a.id), available: false, ...store.sourceInfo(a.id) }));
34899
+ return json4(200, store.library(sources2, importer.status.running));
34540
34900
  }
34541
34901
  const match2 = /^\/api\/sessiongraph\/session\/([^/]+)(?:\/(content|timeline|events|event-markers|connection|prepare-launch|open-terminal))?$/.exec(url.pathname);
34542
34902
  if (match2) {
@@ -34568,6 +34928,7 @@ async function sessionGraphMiddleware(req, res, next) {
34568
34928
  }
34569
34929
  return json4(404, { error: "Not found." });
34570
34930
  } catch (error) {
34931
+ if (error instanceof PublishingError) return json4(error.status, { error: error.message });
34571
34932
  if (error instanceof EventReadError) return json4(error.status, { error: error.message });
34572
34933
  console.error("[sessiongraph] Local operation failed:", error instanceof Error ? error.name : "Unknown error");
34573
34934
  return json4(500, { error: "Local operation failed. Check the import status or retry." });
@@ -34580,9 +34941,9 @@ var arg = (name) => {
34580
34941
  return at2 >= 0 ? process.argv[at2 + 1] : void 0;
34581
34942
  };
34582
34943
  var assets = resolve2(arg("--assets") || fileURLToPath5(new URL("../dist", import.meta.url)));
34583
- var token = randomBytes(32).toString("hex");
34944
+ var token2 = randomBytes2(32).toString("hex");
34584
34945
  var ipc = process.argv.includes("--desktop") ? " ipc: http://ipc.localhost" : "";
34585
- var equals = (value) => /^[a-f0-9]{64}$/.test(value) && timingSafeEqual(Buffer.from(value), Buffer.from(token));
34946
+ var equals = (value) => /^[a-f0-9]{64}$/.test(value) && timingSafeEqual(Buffer.from(value), Buffer.from(token2));
34586
34947
  var types = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png", ".woff": "font/woff", ".woff2": "font/woff2", ".json": "application/json" };
34587
34948
  var server = createServer(async (req, res) => {
34588
34949
  if (req.headers.host !== `127.0.0.1:${server.address().port}`) {
@@ -34595,7 +34956,7 @@ var server = createServer(async (req, res) => {
34595
34956
  res.setHeader("X-Content-Type-Options", "nosniff");
34596
34957
  res.setHeader("Content-Security-Policy", `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self'; connect-src 'self'${ipc}; worker-src 'self' blob:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'`);
34597
34958
  if (url.pathname === "/__sessiongraph" && equals(url.searchParams.get("token") || "")) {
34598
- res.setHeader("Set-Cookie", `sessiongraph=${token}; HttpOnly; SameSite=Strict; Path=/`);
34959
+ res.setHeader("Set-Cookie", `sessiongraph=${token2}; HttpOnly; SameSite=Strict; Path=/`);
34599
34960
  res.setHeader("Cache-Control", "no-store");
34600
34961
  res.writeHead(303, { Location: "/" });
34601
34962
  res.end();
@@ -34636,7 +34997,7 @@ var server = createServer(async (req, res) => {
34636
34997
  });
34637
34998
  server.listen(0, "127.0.0.1", () => {
34638
34999
  const port = server.address().port;
34639
- process.stdout.write(JSON.stringify({ url: `http://127.0.0.1:${port}/__sessiongraph?token=${token}` }) + "\n");
35000
+ process.stdout.write(JSON.stringify({ url: `http://127.0.0.1:${port}/__sessiongraph?token=${token2}` }) + "\n");
34640
35001
  });
34641
35002
  var parent = Number(arg("--parent-pid"));
34642
35003
  if (parent > 0) setInterval(() => {