@nessielabs/daemon 0.3.2 → 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.
- package/README.md +3 -5
- package/dist/api/http.js +19 -0
- package/dist/cli.js +8 -1
- package/dist/commands/setup.js +1 -4
- package/dist/config/paths.js +1 -1
- package/dist/config/store.js +0 -36
- package/dist/localStore/daemonGraphStore.js +494 -0
- package/dist/localStore/messageComparison.js +37 -0
- package/dist/localStore/pushMapping.js +69 -0
- package/dist/localStore/rowTypes.js +1 -0
- package/dist/localStore/schema.js +70 -0
- package/dist/localStore/uuid.js +12 -0
- package/dist/parser/claudeCode.js +139 -21
- package/dist/parser/codex.js +8 -3
- package/dist/parser/types.js +7 -1
- package/dist/sync/client.js +51 -8
- package/dist/sync/exclusions.js +10 -0
- package/dist/sync/hash.js +4 -0
- package/dist/sync/loop.js +83 -53
- package/dist/sync/scan.js +219 -34
- package/dist/sync/slices.js +3 -14
- package/package.json +3 -1
- package/dist/sync/ids.js +0 -11
- package/dist/sync/map.js +0 -111
|
@@ -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
|
+
}
|
|
@@ -10,9 +10,12 @@ export async function parseClaudeCodeSession(path) {
|
|
|
10
10
|
// conversation id and makes their deterministic message ids collide (409).
|
|
11
11
|
const sessionId = basename(path, ".jsonl");
|
|
12
12
|
let title = null;
|
|
13
|
+
let customTitle = null;
|
|
14
|
+
let aiTitle = null;
|
|
13
15
|
let createdAt = null;
|
|
14
16
|
let updatedAt = null;
|
|
15
17
|
let cwd = null;
|
|
18
|
+
const interactiveToolUseIds = new Set();
|
|
16
19
|
for (const value of records) {
|
|
17
20
|
const record = asRecord(value);
|
|
18
21
|
if (!record)
|
|
@@ -21,20 +24,32 @@ export async function parseClaudeCodeSession(path) {
|
|
|
21
24
|
// it before any filtering. The path-encoded project directory name is
|
|
22
25
|
// lossy (dashes vs. path separators), so this is the only reliable source.
|
|
23
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
|
+
}
|
|
24
36
|
if (record.isSidechain === true)
|
|
25
37
|
continue;
|
|
26
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 });
|
|
27
49
|
if (timestamp) {
|
|
28
50
|
createdAt ??= timestamp;
|
|
29
51
|
updatedAt = timestamp;
|
|
30
52
|
}
|
|
31
|
-
const role = normalizeRole(stringValue(record.type) ?? stringValue(record.role));
|
|
32
|
-
if (!role)
|
|
33
|
-
continue;
|
|
34
|
-
const text = extractClaudeText(record);
|
|
35
|
-
if (!text || shouldDropScaffolding(text))
|
|
36
|
-
continue;
|
|
37
|
-
messages.push({ role, content: text, timestamp });
|
|
38
53
|
if (!title && role === "user")
|
|
39
54
|
title = redactSecrets(firstLine(text));
|
|
40
55
|
}
|
|
@@ -44,8 +59,8 @@ export async function parseClaudeCodeSession(path) {
|
|
|
44
59
|
return {
|
|
45
60
|
id: sessionId,
|
|
46
61
|
sourceKind: "claude_code",
|
|
47
|
-
sourceId:
|
|
48
|
-
title: title ?? "Claude Code conversation",
|
|
62
|
+
sourceId: sessionId,
|
|
63
|
+
title: redactSecrets(customTitle ?? aiTitle ?? title ?? "Claude Code conversation"),
|
|
49
64
|
createdAt: createdAt ?? now,
|
|
50
65
|
updatedAt: updatedAt ?? createdAt ?? now,
|
|
51
66
|
cwd,
|
|
@@ -57,28 +72,131 @@ function normalizeRole(role) {
|
|
|
57
72
|
return role;
|
|
58
73
|
return null;
|
|
59
74
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!message)
|
|
66
|
-
return null;
|
|
75
|
+
const interactiveToolNames = new Set([
|
|
76
|
+
"AskUserQuestion",
|
|
77
|
+
"mcp__conductor__AskUserQuestion",
|
|
78
|
+
]);
|
|
79
|
+
function extractAssistantText(message, interactiveToolUseIds) {
|
|
67
80
|
const content = message.content;
|
|
68
81
|
if (typeof content === "string")
|
|
69
|
-
return content;
|
|
82
|
+
return content.trim().length > 0 ? content : null;
|
|
70
83
|
if (!Array.isArray(content))
|
|
71
84
|
return null;
|
|
72
85
|
const parts = content.flatMap((item) => {
|
|
73
86
|
const part = asRecord(item);
|
|
74
|
-
|
|
75
|
-
|
|
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 [];
|
|
76
103
|
});
|
|
77
104
|
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
78
105
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
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;
|
|
81
137
|
}
|
|
82
138
|
function firstLine(text) {
|
|
83
139
|
return text.split(/\r?\n/, 1)[0]?.slice(0, 80) || "Claude Code conversation";
|
|
84
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
|
+
}
|
package/dist/parser/codex.js
CHANGED
|
@@ -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:
|
|
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);
|
package/dist/parser/types.js
CHANGED
|
@@ -6,4 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 1: workspace/repo metadata (cwd, git remote, repo key) on conversations.
|
|
8
8
|
*/
|
|
9
|
-
export const
|
|
9
|
+
export const PARSER_VERSIONS = {
|
|
10
|
+
codex: 1,
|
|
11
|
+
claude_code: 1,
|
|
12
|
+
};
|
|
13
|
+
export function parserVersionForSourceKind(kind) {
|
|
14
|
+
return PARSER_VERSIONS[kind];
|
|
15
|
+
}
|
package/dist/sync/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
}
|
|
36
|
+
};
|
|
18
37
|
}
|
|
19
38
|
export async function pushSyncPayload(config, payload, options = {}) {
|
|
20
|
-
|
|
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
|
|
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
|
+
}
|