@nessielabs/daemon 0.3.1 → 0.4.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.
@@ -0,0 +1,37 @@
1
+ import { redactSecrets } from "../redact/secrets.js";
2
+ import { contentHashesFor } from "../sync/slices.js";
3
+ export function messageMatches(existing, draft, kind) {
4
+ return existing.node.kind === kind &&
5
+ existing.role === draft.role &&
6
+ existing.author === null &&
7
+ existing.toolCallId === null &&
8
+ existing.toolName === null &&
9
+ existing.toolStatus === null &&
10
+ arrayEquals(existing.contentHashes, contentHashesFor(redactSecrets(draft.content)));
11
+ }
12
+ export function messageMetadataChanged(node, draft, integrationId, kind, timestamp) {
13
+ return node.integrationId?.equals(integrationId) !== true ||
14
+ node.name !== messageNodeName(draft) ||
15
+ node.kind !== kind ||
16
+ node.originalCreatedAt !== timestamp ||
17
+ node.originalUpdatedAt !== timestamp ||
18
+ node.deletedAt !== null;
19
+ }
20
+ export function messageTimestamp(message, session) {
21
+ return message.timestamp ?? session.updatedAt;
22
+ }
23
+ export function messageNodeName(message) {
24
+ switch (message.role) {
25
+ case "user":
26
+ return "User Message";
27
+ case "assistant":
28
+ return "Assistant Message";
29
+ case "tool":
30
+ return "Tool Message";
31
+ case "system":
32
+ return "System Message";
33
+ }
34
+ }
35
+ function arrayEquals(left, right) {
36
+ return left.length === right.length && left.every((value, index) => value === right[index]);
37
+ }
@@ -0,0 +1,69 @@
1
+ import { uuidFromBytes } from "./uuid.js";
2
+ export function mapNodePushItem(db, row) {
3
+ const id = uuidFromBytes(row.id);
4
+ return {
5
+ node: {
6
+ id,
7
+ integrationId: row.integrationId ? uuidFromBytes(row.integrationId) : null,
8
+ sourceId: row.sourceId,
9
+ name: row.name,
10
+ kind: row.kind,
11
+ emoji: row.emoji,
12
+ pinned: row.pinned === 1,
13
+ label: row.label,
14
+ labelSliceCount: row.labelSliceCount,
15
+ workspacePath: row.workspacePath,
16
+ repoRemoteUrl: row.repoRemoteUrl,
17
+ repoKey: row.repoKey,
18
+ deletedAt: row.deletedAt,
19
+ originalCreatedAt: row.originalCreatedAt,
20
+ originalUpdatedAt: row.originalUpdatedAt,
21
+ },
22
+ baseCloudUpdatedAt: row.cloudUpdatedAt,
23
+ slices: nodeSlices(db, row.id),
24
+ aiChatMessage: aiChatMessage(db, id, row.id),
25
+ nessieChatMessage: null,
26
+ };
27
+ }
28
+ export function mapEdgePushItem(row) {
29
+ return {
30
+ edge: {
31
+ id: uuidFromBytes(row.id),
32
+ fromNodeId: uuidFromBytes(row.fromNodeId),
33
+ toNodeId: uuidFromBytes(row.toNodeId),
34
+ type: row.type,
35
+ sortIndex: row.sortIndex,
36
+ deletedAt: row.deletedAt,
37
+ },
38
+ baseCloudUpdatedAt: row.cloudUpdatedAt,
39
+ };
40
+ }
41
+ function nodeSlices(db, nodeId) {
42
+ const rows = db.prepare(`
43
+ SELECT id, nodeId, "index", content, contentHash
44
+ FROM node_content_slice
45
+ WHERE nodeId = ?
46
+ ORDER BY "index" ASC
47
+ `).all(nodeId);
48
+ return rows.map((row) => ({
49
+ id: uuidFromBytes(row.id),
50
+ nodeId: uuidFromBytes(row.nodeId),
51
+ index: row.index,
52
+ content: row.content,
53
+ contentHash: row.contentHash,
54
+ }));
55
+ }
56
+ function aiChatMessage(db, id, nodeId) {
57
+ const row = db.prepare("SELECT * FROM ai_chat_message WHERE nodeId = ?").get(nodeId);
58
+ if (!row)
59
+ return null;
60
+ return {
61
+ nodeId: id,
62
+ role: row.role,
63
+ author: row.author,
64
+ toolCallId: row.toolCallId,
65
+ toolName: row.toolName,
66
+ toolFriendlyName: row.toolFriendlyName,
67
+ toolStatus: row.toolStatus,
68
+ };
69
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ export function createDaemonGraphSchema(db) {
2
+ db.pragma("foreign_keys = ON");
3
+ db.exec(`
4
+ CREATE TABLE IF NOT EXISTS node (
5
+ id BLOB PRIMARY KEY,
6
+ integrationId BLOB,
7
+ sourceId TEXT,
8
+ name TEXT NOT NULL,
9
+ kind TEXT NOT NULL,
10
+ emoji TEXT,
11
+ pinned BOOLEAN NOT NULL DEFAULT 0,
12
+ label TEXT,
13
+ labelSliceCount INTEGER,
14
+ workspacePath TEXT,
15
+ repoRemoteUrl TEXT,
16
+ repoKey TEXT,
17
+ createdAt DATETIME NOT NULL,
18
+ updatedAt DATETIME NOT NULL,
19
+ lastPushedAt DATETIME,
20
+ cloudUpdatedAt DATETIME,
21
+ parserVersion INTEGER,
22
+ deletedAt DATETIME,
23
+ originalCreatedAt DATETIME NOT NULL,
24
+ originalUpdatedAt DATETIME NOT NULL
25
+ );
26
+ CREATE INDEX IF NOT EXISTS node_kind_source_id_idx ON node(kind, sourceId);
27
+
28
+ CREATE TABLE IF NOT EXISTS node_edge (
29
+ id BLOB PRIMARY KEY,
30
+ fromNodeId BLOB NOT NULL REFERENCES node(id) ON DELETE CASCADE,
31
+ toNodeId BLOB NOT NULL REFERENCES node(id) ON DELETE CASCADE,
32
+ type TEXT NOT NULL,
33
+ sortIndex INTEGER,
34
+ createdAt DATETIME NOT NULL,
35
+ updatedAt DATETIME NOT NULL,
36
+ lastPushedAt DATETIME,
37
+ cloudUpdatedAt DATETIME,
38
+ deletedAt DATETIME
39
+ );
40
+ CREATE INDEX IF NOT EXISTS node_edge_from_to_type_idx ON node_edge(fromNodeId, toNodeId, type);
41
+ CREATE INDEX IF NOT EXISTS node_edge_from_type_sort_null_to_idx
42
+ ON node_edge("fromNodeId", "type", "sortIndex" IS NULL, "sortIndex", "toNodeId");
43
+ CREATE INDEX IF NOT EXISTS node_edge_to_type_idx ON node_edge(toNodeId, type);
44
+
45
+ CREATE TABLE IF NOT EXISTS node_content_slice (
46
+ id BLOB PRIMARY KEY,
47
+ nodeId BLOB NOT NULL REFERENCES node(id) ON DELETE CASCADE,
48
+ "index" INTEGER NOT NULL,
49
+ content TEXT NOT NULL,
50
+ contentHash TEXT NOT NULL
51
+ );
52
+ CREATE UNIQUE INDEX IF NOT EXISTS node_content_slice_node_index_idx ON node_content_slice(nodeId, "index");
53
+
54
+ CREATE TABLE IF NOT EXISTS ai_chat_message (
55
+ nodeId BLOB PRIMARY KEY REFERENCES node(id) ON DELETE CASCADE,
56
+ role TEXT NOT NULL,
57
+ author TEXT,
58
+ toolCallId TEXT,
59
+ toolName TEXT,
60
+ toolFriendlyName TEXT,
61
+ toolStatus TEXT
62
+ );
63
+
64
+ CREATE TABLE IF NOT EXISTS app_setting (
65
+ key TEXT PRIMARY KEY,
66
+ value TEXT NOT NULL
67
+ );
68
+
69
+ `);
70
+ }
@@ -0,0 +1,12 @@
1
+ export function uuidToBytes(uuid) {
2
+ const hex = uuid.replaceAll("-", "");
3
+ if (!/^[0-9a-fA-F]{32}$/.test(hex))
4
+ throw new Error(`Invalid UUID: ${uuid}`);
5
+ return Buffer.from(hex, "hex");
6
+ }
7
+ export function uuidFromBytes(bytes) {
8
+ const hex = bytes.toString("hex");
9
+ if (hex.length !== 32)
10
+ throw new Error(`Invalid UUID byte length: ${bytes.length}`);
11
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
12
+ }
@@ -4,11 +4,18 @@ import { asRecord, readJsonl, stringValue } from "./jsonl.js";
4
4
  export async function parseClaudeCodeSession(path) {
5
5
  const records = await readJsonl(path);
6
6
  const messages = [];
7
- let sessionId = basename(path, ".jsonl");
7
+ // Anchor session identity to the JSONL filename, matching the macOS client.
8
+ // The in-file `sessionId` is reused across files for worktree/resumed
9
+ // sessions, so keying on it collapses distinct transcripts into one
10
+ // conversation id and makes their deterministic message ids collide (409).
11
+ const sessionId = basename(path, ".jsonl");
8
12
  let title = null;
13
+ let customTitle = null;
14
+ let aiTitle = null;
9
15
  let createdAt = null;
10
16
  let updatedAt = null;
11
17
  let cwd = null;
18
+ const interactiveToolUseIds = new Set();
12
19
  for (const value of records) {
13
20
  const record = asRecord(value);
14
21
  if (!record)
@@ -17,21 +24,32 @@ export async function parseClaudeCodeSession(path) {
17
24
  // it before any filtering. The path-encoded project directory name is
18
25
  // lossy (dashes vs. path separators), so this is the only reliable source.
19
26
  cwd ??= stringValue(record.cwd);
27
+ const type = stringValue(record.type);
28
+ if (type === "custom-title") {
29
+ customTitle = trimmedTitle(record.customTitle) ?? customTitle;
30
+ continue;
31
+ }
32
+ if (type === "ai-title") {
33
+ aiTitle = trimmedTitle(record.aiTitle) ?? aiTitle;
34
+ continue;
35
+ }
20
36
  if (record.isSidechain === true)
21
37
  continue;
22
- sessionId = stringValue(record.sessionId) ?? stringValue(record.session_id) ?? sessionId;
23
38
  const timestamp = stringValue(record.timestamp);
39
+ const message = asRecord(record.message);
40
+ const role = normalizeRole(stringValue(message?.role) ?? type ?? stringValue(record.role));
41
+ if (!role || !message)
42
+ continue;
43
+ const text = role === "assistant"
44
+ ? extractAssistantText(message, interactiveToolUseIds)
45
+ : extractUserText(message, interactiveToolUseIds);
46
+ if (!text)
47
+ continue;
48
+ messages.push({ role, content: text, timestamp });
24
49
  if (timestamp) {
25
50
  createdAt ??= timestamp;
26
51
  updatedAt = timestamp;
27
52
  }
28
- const role = normalizeRole(stringValue(record.type) ?? stringValue(record.role));
29
- if (!role)
30
- continue;
31
- const text = extractClaudeText(record);
32
- if (!text || shouldDropScaffolding(text))
33
- continue;
34
- messages.push({ role, content: text, timestamp });
35
53
  if (!title && role === "user")
36
54
  title = redactSecrets(firstLine(text));
37
55
  }
@@ -41,8 +59,8 @@ export async function parseClaudeCodeSession(path) {
41
59
  return {
42
60
  id: sessionId,
43
61
  sourceKind: "claude_code",
44
- sourceId: path,
45
- title: title ?? "Claude Code conversation",
62
+ sourceId: sessionId,
63
+ title: redactSecrets(customTitle ?? aiTitle ?? title ?? "Claude Code conversation"),
46
64
  createdAt: createdAt ?? now,
47
65
  updatedAt: updatedAt ?? createdAt ?? now,
48
66
  cwd,
@@ -54,28 +72,131 @@ function normalizeRole(role) {
54
72
  return role;
55
73
  return null;
56
74
  }
57
- function extractClaudeText(record) {
58
- const direct = stringValue(record.message);
59
- if (direct)
60
- return direct;
61
- const message = asRecord(record.message);
62
- if (!message)
63
- return null;
75
+ const interactiveToolNames = new Set([
76
+ "AskUserQuestion",
77
+ "mcp__conductor__AskUserQuestion",
78
+ ]);
79
+ function extractAssistantText(message, interactiveToolUseIds) {
64
80
  const content = message.content;
65
81
  if (typeof content === "string")
66
- return content;
82
+ return content.trim().length > 0 ? content : null;
67
83
  if (!Array.isArray(content))
68
84
  return null;
69
85
  const parts = content.flatMap((item) => {
70
86
  const part = asRecord(item);
71
- const text = part ? stringValue(part.text) : null;
72
- return text ? [text] : [];
87
+ if (!part)
88
+ return [];
89
+ if (stringValue(part.type) === "text") {
90
+ const text = stringValue(part.text);
91
+ return text ? [text] : [];
92
+ }
93
+ if (stringValue(part.type) === "tool_use") {
94
+ const toolName = stringValue(part.name);
95
+ const toolUseId = stringValue(part.id);
96
+ if (!toolName || !toolUseId || !interactiveToolNames.has(toolName))
97
+ return [];
98
+ interactiveToolUseIds.add(toolUseId);
99
+ const questionText = extractAskUserQuestionText(part);
100
+ return questionText ? [questionText] : [];
101
+ }
102
+ return [];
73
103
  });
74
104
  return parts.length > 0 ? parts.join("\n\n") : null;
75
105
  }
76
- function shouldDropScaffolding(text) {
77
- return text.startsWith("<command-name>") || text.startsWith("<local-command-stdout>");
106
+ function extractUserText(message, interactiveToolUseIds) {
107
+ const content = message.content;
108
+ if (typeof content === "string") {
109
+ if (isSkillDefinition(content))
110
+ return null;
111
+ const cleaned = stripScaffolding(content);
112
+ return cleaned.length > 0 ? cleaned : null;
113
+ }
114
+ if (!Array.isArray(content))
115
+ return null;
116
+ const parts = content.flatMap((item) => {
117
+ const part = asRecord(item);
118
+ if (!part)
119
+ return [];
120
+ if (stringValue(part.type) === "text") {
121
+ const text = stringValue(part.text);
122
+ if (!text || isSkillDefinition(text))
123
+ return [];
124
+ const cleaned = stripScaffolding(text);
125
+ return cleaned.length > 0 ? [cleaned] : [];
126
+ }
127
+ if (stringValue(part.type) === "tool_result") {
128
+ const toolUseId = stringValue(part.tool_use_id);
129
+ if (!toolUseId || !interactiveToolUseIds.has(toolUseId))
130
+ return [];
131
+ const text = extractToolResultText(part);
132
+ return text ? [text] : [];
133
+ }
134
+ return [];
135
+ });
136
+ return parts.length > 0 ? parts.join("\n") : null;
78
137
  }
79
138
  function firstLine(text) {
80
139
  return text.split(/\r?\n/, 1)[0]?.slice(0, 80) || "Claude Code conversation";
81
140
  }
141
+ function trimmedTitle(value) {
142
+ const title = stringValue(value);
143
+ if (!title)
144
+ return null;
145
+ const trimmed = title.trim();
146
+ return trimmed.length > 0 ? trimmed.slice(0, 80) : null;
147
+ }
148
+ function isSkillDefinition(text) {
149
+ return text.trim().startsWith("Base directory for this skill:");
150
+ }
151
+ function stripScaffolding(text) {
152
+ let result = text;
153
+ for (const pattern of [
154
+ /<system_instruction>[\s\S]*?<\/system_instruction>/g,
155
+ /<system-instruction>[\s\S]*?<\/system-instruction>/g,
156
+ /<system-reminder>[\s\S]*?<\/system-reminder>/g,
157
+ /<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g,
158
+ /<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g,
159
+ /<command-name>[\s\S]*?<\/command-name>/g,
160
+ /<command-message>[\s\S]*?<\/command-message>/g,
161
+ ]) {
162
+ result = result.replace(pattern, "");
163
+ }
164
+ result = result.replace(/<command-args>([\s\S]*?)<\/command-args>/g, "$1");
165
+ while (result.includes("\n\n\n"))
166
+ result = result.replaceAll("\n\n\n", "\n\n");
167
+ return result.trim();
168
+ }
169
+ function extractAskUserQuestionText(block) {
170
+ const input = asRecord(block.input);
171
+ const questions = input?.questions;
172
+ if (!Array.isArray(questions))
173
+ return null;
174
+ const parts = questions.flatMap((item) => {
175
+ const question = asRecord(item);
176
+ const text = stringValue(question?.question);
177
+ if (!text)
178
+ return [];
179
+ const options = question?.options;
180
+ if (!Array.isArray(options))
181
+ return [text];
182
+ const formatted = options.flatMap((option, index) => {
183
+ const optionText = stringValue(option);
184
+ return optionText ? [`${String.fromCharCode(65 + index)}) ${optionText}`] : [];
185
+ });
186
+ return [formatted.length > 0 ? `${text}\n${formatted.join("\n")}` : text];
187
+ });
188
+ return parts.length > 0 ? parts.join("\n\n") : null;
189
+ }
190
+ function extractToolResultText(block) {
191
+ const content = block.content;
192
+ if (typeof content === "string" && content.length > 0)
193
+ return content;
194
+ if (!Array.isArray(content))
195
+ return null;
196
+ const texts = content.flatMap((item) => {
197
+ const record = asRecord(item);
198
+ const text = stringValue(record?.type) === "text" ? stringValue(record?.text) : null;
199
+ return text ? [text] : [];
200
+ });
201
+ return texts.length > 0 ? texts.join("\n") : null;
202
+ }
@@ -20,9 +20,9 @@ export async function parseCodexSession(path) {
20
20
  updatedAt = timestamp;
21
21
  }
22
22
  if (type === "session_meta") {
23
- sessionId = stringValue(record.id) ?? stringValue(record.session_id) ?? sessionId;
24
- title = redactTitle(stringValue(record.title)) ?? title;
25
23
  const meta = asRecord(record.payload) ?? record;
24
+ sessionId = stringValue(meta.id) ?? stringValue(record.id) ?? stringValue(record.session_id) ?? sessionId;
25
+ title = redactTitle(stringValue(record.title)) ?? title;
26
26
  cwd ??= stringValue(meta.cwd);
27
27
  continue;
28
28
  }
@@ -44,7 +44,7 @@ export async function parseCodexSession(path) {
44
44
  return {
45
45
  id: sessionId,
46
46
  sourceKind: "codex",
47
- sourceId: path,
47
+ sourceId: sessionId,
48
48
  title: title ?? "Codex conversation",
49
49
  createdAt: createdAt ?? now,
50
50
  updatedAt: updatedAt ?? createdAt ?? now,
@@ -58,6 +58,8 @@ function roleForCodexMessage(messageType, payload) {
58
58
  return role;
59
59
  if (messageType?.includes("user"))
60
60
  return "user";
61
+ if (messageType === "task_complete")
62
+ return "assistant";
61
63
  if (messageType?.includes("agent") || messageType?.includes("assistant"))
62
64
  return "assistant";
63
65
  if (messageType?.includes("tool"))
@@ -70,6 +72,9 @@ function extractText(payload) {
70
72
  if (direct)
71
73
  return direct;
72
74
  }
75
+ const taskComplete = stringValue(payload.last_agent_message);
76
+ if (taskComplete)
77
+ return taskComplete;
73
78
  const message = asRecord(payload.message);
74
79
  if (message) {
75
80
  const content = stringValue(message.content);
@@ -6,4 +6,10 @@
6
6
  *
7
7
  * 1: workspace/repo metadata (cwd, git remote, repo key) on conversations.
8
8
  */
9
- export const PARSER_VERSION = 1;
9
+ export const PARSER_VERSIONS = {
10
+ codex: 1,
11
+ claude_code: 1,
12
+ };
13
+ export function parserVersionForSourceKind(kind) {
14
+ return PARSER_VERSIONS[kind];
15
+ }
@@ -1,9 +1,28 @@
1
- import { ApiError, postJson } from "../api/http.js";
1
+ import { ApiError, getJson, postJson } from "../api/http.js";
2
2
  import { refreshAccessToken } from "../auth/tokens.js";
3
3
  import { syncApiUrl } from "../config/endpoints.js";
4
4
  import { writeConfig } from "../config/store.js";
5
5
  export async function runPreflightV3(config, options = {}) {
6
- return postJsonWithRefresh(config, "/sync/v3/preflight", {
6
+ const result = await postJsonWithRefresh(config, "/sync/v3/preflight", preflightBody(config), options);
7
+ return result.config;
8
+ }
9
+ export async function runPreflightV4(config, options = {}) {
10
+ const result = await postJsonWithRefresh(config, "/sync/v4/preflight", preflightBody(config), options);
11
+ return result.config;
12
+ }
13
+ export async function listSyncExclusions(config, options = {}) {
14
+ return getJsonWithRefresh(config, "/sync/v4/sync-exclusions", { kind: options.kind }, options);
15
+ }
16
+ export async function pullCloudSyncPage(config, input, options = {}) {
17
+ return postJsonWithRefresh(config, "/sync/pull/v5", {
18
+ deviceId: config.deviceId,
19
+ nodeCursor: input.nodeCursor,
20
+ edgeCursor: input.edgeCursor,
21
+ limit: input.limit,
22
+ }, options);
23
+ }
24
+ function preflightBody(config) {
25
+ return {
7
26
  device: {
8
27
  id: config.deviceId,
9
28
  displayName: config.deviceName,
@@ -14,19 +33,43 @@ export async function runPreflightV3(config, options = {}) {
14
33
  stableKey: source.stableKey,
15
34
  displayName: source.displayName,
16
35
  })),
17
- }, options);
36
+ };
18
37
  }
19
38
  export async function pushSyncPayload(config, payload, options = {}) {
20
- return postJsonWithRefresh(config, "/sync/v2/push", {
39
+ const result = await postJsonWithRefresh(config, "/sync/v2/push", {
21
40
  deviceId: config.deviceId,
22
41
  ...payload,
23
42
  }, options);
43
+ return {
44
+ config: result.config,
45
+ response: {
46
+ nodes: Array.isArray(result.response.nodes) ? result.response.nodes : [],
47
+ edges: Array.isArray(result.response.edges) ? result.response.edges : [],
48
+ },
49
+ };
24
50
  }
25
51
  async function postJsonWithRefresh(config, path, body, options) {
26
52
  const token = authToken(config);
27
53
  try {
28
- await postJson(syncApiUrl, path, body, { token, fetchImpl: options.fetchImpl });
29
- return config;
54
+ const response = await postJson(syncApiUrl, path, body, { token, fetchImpl: options.fetchImpl });
55
+ return { config, response };
56
+ }
57
+ catch (error) {
58
+ if (config.apiKey)
59
+ throw error;
60
+ if (!(error instanceof ApiError) || error.status !== 401)
61
+ throw error;
62
+ }
63
+ const refreshed = await refreshAccessToken(config, { fetchImpl: options.fetchImpl });
64
+ await (options.persistConfig ?? writeConfig)(refreshed);
65
+ const response = await postJson(syncApiUrl, path, body, { token: authToken(refreshed), fetchImpl: options.fetchImpl });
66
+ return { config: refreshed, response };
67
+ }
68
+ async function getJsonWithRefresh(config, path, query, options) {
69
+ const token = authToken(config);
70
+ try {
71
+ const response = await getJson(syncApiUrl, path, { token, fetchImpl: options.fetchImpl, query });
72
+ return { config, response };
30
73
  }
31
74
  catch (error) {
32
75
  if (config.apiKey)
@@ -36,8 +79,8 @@ async function postJsonWithRefresh(config, path, body, options) {
36
79
  }
37
80
  const refreshed = await refreshAccessToken(config, { fetchImpl: options.fetchImpl });
38
81
  await (options.persistConfig ?? writeConfig)(refreshed);
39
- await postJson(syncApiUrl, path, body, { token: authToken(refreshed), fetchImpl: options.fetchImpl });
40
- return refreshed;
82
+ const response = await getJson(syncApiUrl, path, { token: authToken(refreshed), fetchImpl: options.fetchImpl, query });
83
+ return { config: refreshed, response };
41
84
  }
42
85
  function authToken(config) {
43
86
  const token = config.apiKey ?? config.accessToken;
@@ -0,0 +1,10 @@
1
+ export function transcriptKindForSourceKind(kind) {
2
+ return kind === "codex" ? "codex_chat" : "claude_code_chat";
3
+ }
4
+ export function filterExcludedConversations(conversations, exclusions) {
5
+ const excludedIdentities = new Set(exclusions.map((exclusion) => identityKey(exclusion.kind, exclusion.sourceId)));
6
+ return conversations.filter((conversation) => !excludedIdentities.has(identityKey(transcriptKindForSourceKind(conversation.sourceKind), conversation.sourceId)));
7
+ }
8
+ function identityKey(kind, sourceId) {
9
+ return `${kind}\0${sourceId}`;
10
+ }
@@ -0,0 +1,4 @@
1
+ import { createHash } from "node:crypto";
2
+ export function contentHash(content) {
3
+ return createHash("sha256").update(content).digest("hex");
4
+ }