@nessielabs/daemon 0.4.0 → 0.5.26
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 +8 -17
- package/dist/cli.js +42 -21
- package/dist/commands/auth.js +116 -0
- package/dist/commands/integrations.js +84 -0
- package/dist/commands/login.js +145 -0
- package/dist/commands/run.js +21 -6
- package/dist/commands/service.js +18 -36
- package/dist/commands/sharing.js +399 -0
- package/dist/commands/status.js +67 -0
- package/dist/commands/teams.js +64 -0
- package/dist/nera/client.js +156 -0
- package/dist/nera/process.js +78 -0
- package/dist/nera/runtimeFile.js +25 -0
- package/dist/platform/binary.js +21 -0
- package/dist/platform/openBrowser.js +14 -0
- package/dist/platform/paths.js +13 -0
- package/dist/platform/table.js +85 -0
- package/package.json +9 -5
- package/vendor/nera/darwin-arm64/nera +0 -0
- package/vendor/nera/darwin-x64/nera +0 -0
- package/vendor/nera/linux-arm64/nera +0 -0
- package/vendor/nera/linux-x64/nera +0 -0
- package/vendor/nera/win32-arm64/nera.exe +0 -0
- package/vendor/nera/win32-x64/nera.exe +0 -0
- package/vendor/proto/nessie.edge.v1.proto +475 -0
- package/dist/api/http.js +0 -44
- package/dist/auth/deviceFlow.js +0 -36
- package/dist/auth/tokens.js +0 -30
- package/dist/commands/setup.js +0 -75
- package/dist/config/endpoints.js +0 -2
- package/dist/config/paths.js +0 -19
- package/dist/config/store.js +0 -27
- package/dist/git/repoResolver.js +0 -215
- package/dist/localStore/daemonGraphStore.js +0 -494
- package/dist/localStore/messageComparison.js +0 -37
- package/dist/localStore/pushMapping.js +0 -69
- package/dist/localStore/rowTypes.js +0 -1
- package/dist/localStore/schema.js +0 -70
- package/dist/localStore/uuid.js +0 -12
- package/dist/parser/claudeCode.js +0 -202
- package/dist/parser/codex.js +0 -91
- package/dist/parser/jsonl.js +0 -24
- package/dist/parser/types.js +0 -15
- package/dist/redact/secrets.js +0 -60
- package/dist/service/process.js +0 -134
- package/dist/service/systemd.js +0 -38
- package/dist/sources/detect.js +0 -43
- package/dist/sync/batch.js +0 -50
- package/dist/sync/client.js +0 -90
- package/dist/sync/exclusions.js +0 -10
- package/dist/sync/hash.js +0 -4
- package/dist/sync/loop.js +0 -106
- package/dist/sync/scan.js +0 -245
- package/dist/sync/slices.js +0 -63
- package/dist/sync/types.js +0 -1
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
|
-
import { redactSecrets } from "../redact/secrets.js";
|
|
3
|
-
import { asRecord, readJsonl, stringValue } from "./jsonl.js";
|
|
4
|
-
export async function parseClaudeCodeSession(path) {
|
|
5
|
-
const records = await readJsonl(path);
|
|
6
|
-
const messages = [];
|
|
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");
|
|
12
|
-
let title = null;
|
|
13
|
-
let customTitle = null;
|
|
14
|
-
let aiTitle = null;
|
|
15
|
-
let createdAt = null;
|
|
16
|
-
let updatedAt = null;
|
|
17
|
-
let cwd = null;
|
|
18
|
-
const interactiveToolUseIds = new Set();
|
|
19
|
-
for (const value of records) {
|
|
20
|
-
const record = asRecord(value);
|
|
21
|
-
if (!record)
|
|
22
|
-
continue;
|
|
23
|
-
// Every transcript line stamps cwd, including sidechain lines, so capture
|
|
24
|
-
// it before any filtering. The path-encoded project directory name is
|
|
25
|
-
// lossy (dashes vs. path separators), so this is the only reliable source.
|
|
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
|
-
}
|
|
36
|
-
if (record.isSidechain === true)
|
|
37
|
-
continue;
|
|
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 });
|
|
49
|
-
if (timestamp) {
|
|
50
|
-
createdAt ??= timestamp;
|
|
51
|
-
updatedAt = timestamp;
|
|
52
|
-
}
|
|
53
|
-
if (!title && role === "user")
|
|
54
|
-
title = redactSecrets(firstLine(text));
|
|
55
|
-
}
|
|
56
|
-
if (messages.length === 0)
|
|
57
|
-
return null;
|
|
58
|
-
const now = new Date().toISOString();
|
|
59
|
-
return {
|
|
60
|
-
id: sessionId,
|
|
61
|
-
sourceKind: "claude_code",
|
|
62
|
-
sourceId: sessionId,
|
|
63
|
-
title: redactSecrets(customTitle ?? aiTitle ?? title ?? "Claude Code conversation"),
|
|
64
|
-
createdAt: createdAt ?? now,
|
|
65
|
-
updatedAt: updatedAt ?? createdAt ?? now,
|
|
66
|
-
cwd,
|
|
67
|
-
messages,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function normalizeRole(role) {
|
|
71
|
-
if (role === "user" || role === "assistant" || role === "system" || role === "tool")
|
|
72
|
-
return role;
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
const interactiveToolNames = new Set([
|
|
76
|
-
"AskUserQuestion",
|
|
77
|
-
"mcp__conductor__AskUserQuestion",
|
|
78
|
-
]);
|
|
79
|
-
function extractAssistantText(message, interactiveToolUseIds) {
|
|
80
|
-
const content = message.content;
|
|
81
|
-
if (typeof content === "string")
|
|
82
|
-
return content.trim().length > 0 ? content : null;
|
|
83
|
-
if (!Array.isArray(content))
|
|
84
|
-
return null;
|
|
85
|
-
const parts = content.flatMap((item) => {
|
|
86
|
-
const part = asRecord(item);
|
|
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 [];
|
|
103
|
-
});
|
|
104
|
-
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
105
|
-
}
|
|
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;
|
|
137
|
-
}
|
|
138
|
-
function firstLine(text) {
|
|
139
|
-
return text.split(/\r?\n/, 1)[0]?.slice(0, 80) || "Claude Code conversation";
|
|
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
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
|
-
import { redactSecrets } from "../redact/secrets.js";
|
|
3
|
-
import { asRecord, readJsonl, stringValue } from "./jsonl.js";
|
|
4
|
-
export async function parseCodexSession(path) {
|
|
5
|
-
const records = await readJsonl(path);
|
|
6
|
-
const messages = [];
|
|
7
|
-
let sessionId = basename(path, ".jsonl");
|
|
8
|
-
let title = null;
|
|
9
|
-
let createdAt = null;
|
|
10
|
-
let updatedAt = null;
|
|
11
|
-
let cwd = null;
|
|
12
|
-
for (const value of records) {
|
|
13
|
-
const record = asRecord(value);
|
|
14
|
-
if (!record)
|
|
15
|
-
continue;
|
|
16
|
-
const type = stringValue(record.type);
|
|
17
|
-
const timestamp = stringValue(record.timestamp) ?? stringValue(record.created_at);
|
|
18
|
-
if (timestamp) {
|
|
19
|
-
createdAt ??= timestamp;
|
|
20
|
-
updatedAt = timestamp;
|
|
21
|
-
}
|
|
22
|
-
if (type === "session_meta") {
|
|
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
|
-
cwd ??= stringValue(meta.cwd);
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
if (type !== "event_msg")
|
|
30
|
-
continue;
|
|
31
|
-
const payload = asRecord(record.payload) ?? record;
|
|
32
|
-
const messageType = stringValue(payload.type) ?? stringValue(payload.kind);
|
|
33
|
-
const text = extractText(payload);
|
|
34
|
-
if (!text)
|
|
35
|
-
continue;
|
|
36
|
-
const role = roleForCodexMessage(messageType, payload);
|
|
37
|
-
messages.push({ role, content: text, timestamp });
|
|
38
|
-
if (!title && role === "user")
|
|
39
|
-
title = redactTitle(firstLine(text));
|
|
40
|
-
}
|
|
41
|
-
if (messages.length === 0)
|
|
42
|
-
return null;
|
|
43
|
-
const now = new Date().toISOString();
|
|
44
|
-
return {
|
|
45
|
-
id: sessionId,
|
|
46
|
-
sourceKind: "codex",
|
|
47
|
-
sourceId: sessionId,
|
|
48
|
-
title: title ?? "Codex conversation",
|
|
49
|
-
createdAt: createdAt ?? now,
|
|
50
|
-
updatedAt: updatedAt ?? createdAt ?? now,
|
|
51
|
-
cwd,
|
|
52
|
-
messages,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
function roleForCodexMessage(messageType, payload) {
|
|
56
|
-
const role = stringValue(payload.role);
|
|
57
|
-
if (role === "user" || role === "assistant" || role === "system" || role === "tool")
|
|
58
|
-
return role;
|
|
59
|
-
if (messageType?.includes("user"))
|
|
60
|
-
return "user";
|
|
61
|
-
if (messageType === "task_complete")
|
|
62
|
-
return "assistant";
|
|
63
|
-
if (messageType?.includes("agent") || messageType?.includes("assistant"))
|
|
64
|
-
return "assistant";
|
|
65
|
-
if (messageType?.includes("tool"))
|
|
66
|
-
return "tool";
|
|
67
|
-
return "assistant";
|
|
68
|
-
}
|
|
69
|
-
function extractText(payload) {
|
|
70
|
-
for (const key of ["message", "text", "content", "delta"]) {
|
|
71
|
-
const direct = stringValue(payload[key]);
|
|
72
|
-
if (direct)
|
|
73
|
-
return direct;
|
|
74
|
-
}
|
|
75
|
-
const taskComplete = stringValue(payload.last_agent_message);
|
|
76
|
-
if (taskComplete)
|
|
77
|
-
return taskComplete;
|
|
78
|
-
const message = asRecord(payload.message);
|
|
79
|
-
if (message) {
|
|
80
|
-
const content = stringValue(message.content);
|
|
81
|
-
if (content)
|
|
82
|
-
return content;
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
function firstLine(text) {
|
|
87
|
-
return text.split(/\r?\n/, 1)[0]?.slice(0, 80) || "Codex conversation";
|
|
88
|
-
}
|
|
89
|
-
function redactTitle(title) {
|
|
90
|
-
return title ? redactSecrets(title) : null;
|
|
91
|
-
}
|
package/dist/parser/jsonl.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
export async function readJsonl(path) {
|
|
3
|
-
const content = await readFile(path, "utf8");
|
|
4
|
-
const records = [];
|
|
5
|
-
for (const line of content.split(/\r?\n/)) {
|
|
6
|
-
const trimmed = line.trim();
|
|
7
|
-
if (!trimmed)
|
|
8
|
-
continue;
|
|
9
|
-
try {
|
|
10
|
-
records.push(JSON.parse(trimmed));
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
// Agent history files are written while active. A trailing partial JSONL
|
|
14
|
-
// record should not prevent earlier complete records from syncing.
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return records;
|
|
18
|
-
}
|
|
19
|
-
export function asRecord(value) {
|
|
20
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
21
|
-
}
|
|
22
|
-
export function stringValue(value) {
|
|
23
|
-
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
24
|
-
}
|
package/dist/parser/types.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Version of the daemon's transcript parsing + enrichment semantics. Bumping
|
|
3
|
-
* it makes the next sync reparse every session file and re-push conversation
|
|
4
|
-
* headers as field-masked merges, so previously synced conversations pick up
|
|
5
|
-
* newly extracted metadata. Messages are append-only and are never re-pushed.
|
|
6
|
-
*
|
|
7
|
-
* 1: workspace/repo metadata (cwd, git remote, repo key) on conversations.
|
|
8
|
-
*/
|
|
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/redact/secrets.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
export const secretReplacement = "[REDACTED]";
|
|
2
|
-
const literalIndicators = [
|
|
3
|
-
"ghp_",
|
|
4
|
-
"github_pat_",
|
|
5
|
-
"napi_",
|
|
6
|
-
"npg_",
|
|
7
|
-
"sk-",
|
|
8
|
-
];
|
|
9
|
-
const caseInsensitiveIndicators = [
|
|
10
|
-
"bearer ",
|
|
11
|
-
"begin private key",
|
|
12
|
-
"begin rsa private key",
|
|
13
|
-
"begin ec private key",
|
|
14
|
-
"begin openssh private key",
|
|
15
|
-
"token=",
|
|
16
|
-
"key=",
|
|
17
|
-
"api_key=",
|
|
18
|
-
"connection_string=",
|
|
19
|
-
"database_url=",
|
|
20
|
-
"db_url=",
|
|
21
|
-
"password=",
|
|
22
|
-
"postgres_url=",
|
|
23
|
-
"postgresql_url=",
|
|
24
|
-
"postgres://",
|
|
25
|
-
"postgresql://",
|
|
26
|
-
"secret=",
|
|
27
|
-
"supabase",
|
|
28
|
-
];
|
|
29
|
-
const fullReplacementPatterns = [
|
|
30
|
-
/\bghp_[A-Za-z0-9_]{20,}\b/g,
|
|
31
|
-
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
32
|
-
/\bnapi_[A-Za-z0-9_-]{16,}\b/g,
|
|
33
|
-
/\bnpg_[A-Za-z0-9_-]{16,}\b/g,
|
|
34
|
-
/\bsk-[A-Za-z0-9_-]{16,}\b/g,
|
|
35
|
-
/\bsb_(?:publishable|secret)_[A-Za-z0-9_-]{16,}\b/g,
|
|
36
|
-
/\bsbp_[A-Za-z0-9_-]{16,}\b/g,
|
|
37
|
-
/\bpostgres(?:ql)?:\/\/[^\s'"`<>]+/gi,
|
|
38
|
-
/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}\b/gi,
|
|
39
|
-
/-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/gi,
|
|
40
|
-
];
|
|
41
|
-
const uuidPattern = /\b[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\b/gi;
|
|
42
|
-
const sensitiveKeyNamePattern = String.raw `[A-Za-z0-9_]*(?:token|api[_-]?key|apikey|password|secret|[_-]key|connection[_-]?string|database[_-]?url|db[_-]?url|postgres(?:ql)?[_-]?url)|key`;
|
|
43
|
-
const unquotedAssignmentValuePattern = "[^\\s&;,'\"`}\\[\\]]+";
|
|
44
|
-
const assignmentPattern = new RegExp(String.raw `\b(${sensitiveKeyNamePattern})\b(\s*=\s*)(["']?)(${unquotedAssignmentValuePattern})\3?`, "gi");
|
|
45
|
-
const quotedAssignmentPattern = new RegExp(String.raw `(["'])(${sensitiveKeyNamePattern})\1(\s*:\s*)(["'])([^"']+)\4`, "gi");
|
|
46
|
-
export function redactSecrets(text) {
|
|
47
|
-
const withoutUuids = text.replace(uuidPattern, secretReplacement);
|
|
48
|
-
if (!mightContainSecret(withoutUuids))
|
|
49
|
-
return withoutUuids;
|
|
50
|
-
const withoutWholeSecrets = fullReplacementPatterns.reduce((result, pattern) => result.replace(pattern, secretReplacement), withoutUuids);
|
|
51
|
-
return withoutWholeSecrets
|
|
52
|
-
.replace(quotedAssignmentPattern, (_match, keyQuote, key, separator, valueQuote) => `${keyQuote}${key}${keyQuote}${separator}${valueQuote}${secretReplacement}${valueQuote}`)
|
|
53
|
-
.replace(assignmentPattern, (_match, key, separator, valueQuote) => `${key}${separator}${valueQuote}${secretReplacement}${valueQuote}`);
|
|
54
|
-
}
|
|
55
|
-
export function mightContainSecret(text) {
|
|
56
|
-
if (literalIndicators.some((indicator) => text.includes(indicator)))
|
|
57
|
-
return true;
|
|
58
|
-
const lowered = text.toLowerCase();
|
|
59
|
-
return caseInsensitiveIndicators.some((indicator) => lowered.includes(indicator));
|
|
60
|
-
}
|
package/dist/service/process.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { closeSync, openSync } from "node:fs";
|
|
3
|
-
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import { getDaemonPaths } from "../config/paths.js";
|
|
5
|
-
const defaultProcessDeps = {
|
|
6
|
-
spawn,
|
|
7
|
-
argv: process.argv,
|
|
8
|
-
execPath: process.execPath,
|
|
9
|
-
kill: process.kill,
|
|
10
|
-
readProcessCmdline,
|
|
11
|
-
};
|
|
12
|
-
export async function startBackgroundDaemon(paths = getDaemonPaths(), depsOverride = {}) {
|
|
13
|
-
const deps = resolveProcessDeps(depsOverride);
|
|
14
|
-
const existingPid = await readPid(paths);
|
|
15
|
-
if (existingPid && await isDaemonRunning(existingPid, deps)) {
|
|
16
|
-
return `nessie-daemon is already running with pid ${existingPid}.`;
|
|
17
|
-
}
|
|
18
|
-
if (existingPid)
|
|
19
|
-
await removePid(paths);
|
|
20
|
-
await mkdir(paths.stateDir, { recursive: true });
|
|
21
|
-
if (!await reservePidFile(paths))
|
|
22
|
-
return "nessie-daemon is already starting.";
|
|
23
|
-
const out = openSync(paths.logFile, "a");
|
|
24
|
-
const err = openSync(paths.logFile, "a");
|
|
25
|
-
let child;
|
|
26
|
-
try {
|
|
27
|
-
child = deps.spawn(deps.execPath, [cliPath(deps.argv), "run"], {
|
|
28
|
-
detached: true,
|
|
29
|
-
stdio: ["ignore", out, err],
|
|
30
|
-
});
|
|
31
|
-
child.unref();
|
|
32
|
-
}
|
|
33
|
-
finally {
|
|
34
|
-
closeSync(out);
|
|
35
|
-
closeSync(err);
|
|
36
|
-
}
|
|
37
|
-
if (!child.pid) {
|
|
38
|
-
await removePid(paths);
|
|
39
|
-
throw new Error("Failed to start nessie-daemon in background.");
|
|
40
|
-
}
|
|
41
|
-
await writeFile(paths.pidFile, `${child.pid}\n`, { mode: 0o600 });
|
|
42
|
-
return `Started nessie-daemon with pid ${child.pid}. Logs: ${paths.logFile}`;
|
|
43
|
-
}
|
|
44
|
-
export async function stopBackgroundDaemon(paths = getDaemonPaths(), depsOverride = {}) {
|
|
45
|
-
const deps = resolveProcessDeps(depsOverride);
|
|
46
|
-
const pid = await readPid(paths);
|
|
47
|
-
if (!pid)
|
|
48
|
-
return "nessie-daemon is not running.";
|
|
49
|
-
if (!await isDaemonRunning(pid, deps)) {
|
|
50
|
-
await removePid(paths);
|
|
51
|
-
return "nessie-daemon is not running.";
|
|
52
|
-
}
|
|
53
|
-
deps.kill(pid, "SIGTERM");
|
|
54
|
-
await removePid(paths);
|
|
55
|
-
return `Stopped nessie-daemon with pid ${pid}.`;
|
|
56
|
-
}
|
|
57
|
-
export async function backgroundDaemonStatus(paths = getDaemonPaths(), depsOverride = {}) {
|
|
58
|
-
const deps = resolveProcessDeps(depsOverride);
|
|
59
|
-
const pid = await readPid(paths);
|
|
60
|
-
if (!pid)
|
|
61
|
-
return null;
|
|
62
|
-
if (!await isDaemonRunning(pid, deps)) {
|
|
63
|
-
await removePid(paths);
|
|
64
|
-
return "nessie-daemon is not running.";
|
|
65
|
-
}
|
|
66
|
-
return `nessie-daemon is running with pid ${pid}. Logs: ${paths.logFile}`;
|
|
67
|
-
}
|
|
68
|
-
async function readPid(paths) {
|
|
69
|
-
try {
|
|
70
|
-
const raw = await readFile(paths.pidFile, "utf8");
|
|
71
|
-
const pid = Number.parseInt(raw.trim(), 10);
|
|
72
|
-
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
|
|
76
|
-
return null;
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async function removePid(paths) {
|
|
81
|
-
try {
|
|
82
|
-
await unlink(paths.pidFile);
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
|
|
86
|
-
return;
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
async function reservePidFile(paths) {
|
|
91
|
-
try {
|
|
92
|
-
await writeFile(paths.pidFile, "starting\n", { mode: 0o600, flag: "wx" });
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST")
|
|
97
|
-
return false;
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
async function isDaemonRunning(pid, deps) {
|
|
102
|
-
try {
|
|
103
|
-
deps.kill(pid, 0);
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
const cmdline = await deps.readProcessCmdline(pid);
|
|
109
|
-
if (cmdline === null)
|
|
110
|
-
return false;
|
|
111
|
-
return cmdline.includes(cliPath(deps.argv)) && cmdline.includes(" run");
|
|
112
|
-
}
|
|
113
|
-
async function readProcessCmdline(pid) {
|
|
114
|
-
try {
|
|
115
|
-
const raw = await readFile(`/proc/${pid}/cmdline`);
|
|
116
|
-
const cmdline = raw.toString("utf8").replace(/\0/g, " ").trim();
|
|
117
|
-
return cmdline || null;
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
function resolveProcessDeps(overrides) {
|
|
124
|
-
return {
|
|
125
|
-
...defaultProcessDeps,
|
|
126
|
-
...overrides,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function cliPath(argv) {
|
|
130
|
-
const path = argv[1];
|
|
131
|
-
if (!path)
|
|
132
|
-
throw new Error("Cannot determine nessie-daemon CLI path.");
|
|
133
|
-
return path;
|
|
134
|
-
}
|
package/dist/service/systemd.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
|
-
import { getDaemonPaths } from "../config/paths.js";
|
|
5
|
-
const execFileAsync = promisify(execFile);
|
|
6
|
-
export async function installUserService(paths = getDaemonPaths()) {
|
|
7
|
-
await mkdir(paths.systemdUserDir, { recursive: true });
|
|
8
|
-
const service = buildUserServiceFile(process.execPath, process.argv[1] ?? "nessie-daemon");
|
|
9
|
-
await writeFile(paths.systemdServiceFile, service);
|
|
10
|
-
await systemctl("daemon-reload");
|
|
11
|
-
await systemctl("enable", "nessie-daemon.service");
|
|
12
|
-
await systemctl("start", "nessie-daemon.service");
|
|
13
|
-
}
|
|
14
|
-
export function buildUserServiceFile(nodePath, cliPath) {
|
|
15
|
-
return [
|
|
16
|
-
"[Unit]",
|
|
17
|
-
"Description=Nessie daemon",
|
|
18
|
-
"Wants=network-online.target",
|
|
19
|
-
"After=network-online.target",
|
|
20
|
-
"",
|
|
21
|
-
"[Service]",
|
|
22
|
-
"Type=simple",
|
|
23
|
-
`ExecStart=${systemdQuote(nodePath)} ${systemdQuote(cliPath)} run`,
|
|
24
|
-
"Restart=always",
|
|
25
|
-
"RestartSec=30",
|
|
26
|
-
"",
|
|
27
|
-
"[Install]",
|
|
28
|
-
"WantedBy=default.target",
|
|
29
|
-
"",
|
|
30
|
-
].join("\n");
|
|
31
|
-
}
|
|
32
|
-
export async function systemctl(...args) {
|
|
33
|
-
const { stdout, stderr } = await execFileAsync("systemctl", ["--user", ...args]);
|
|
34
|
-
return stdout || stderr;
|
|
35
|
-
}
|
|
36
|
-
function systemdQuote(value) {
|
|
37
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
38
|
-
}
|
package/dist/sources/detect.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { access } from "node:fs/promises";
|
|
2
|
-
import { homedir, hostname } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
export async function detectSources() {
|
|
5
|
-
return detectSourcesInHome(homedir(), hostname());
|
|
6
|
-
}
|
|
7
|
-
export async function detectSourcesInHome(home, host) {
|
|
8
|
-
const candidates = [
|
|
9
|
-
{
|
|
10
|
-
kind: "claude_code",
|
|
11
|
-
displayName: `Claude Code on ${host}`,
|
|
12
|
-
basePath: join(home, ".claude"),
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
kind: "codex",
|
|
16
|
-
displayName: `Codex on ${host}`,
|
|
17
|
-
basePath: join(home, ".codex"),
|
|
18
|
-
},
|
|
19
|
-
];
|
|
20
|
-
const detected = [];
|
|
21
|
-
for (const candidate of candidates) {
|
|
22
|
-
if (await exists(sourceProbePath(candidate)))
|
|
23
|
-
detected.push(candidate);
|
|
24
|
-
}
|
|
25
|
-
return detected;
|
|
26
|
-
}
|
|
27
|
-
function sourceProbePath(source) {
|
|
28
|
-
switch (source.kind) {
|
|
29
|
-
case "claude_code":
|
|
30
|
-
return join(source.basePath, "projects");
|
|
31
|
-
case "codex":
|
|
32
|
-
return join(source.basePath, "sessions");
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
async function exists(path) {
|
|
36
|
-
try {
|
|
37
|
-
await access(path);
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
package/dist/sync/batch.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Target number of content slices per node push request.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `CloudSyncPipeline.targetSlicesPerPush` in the macOS app. Batching by
|
|
5
|
-
* slice count keeps each request well under Cloud Run's 32 MiB body limit, which
|
|
6
|
-
* a single all-in-one push of a large cold sync would blow past (413).
|
|
7
|
-
*/
|
|
8
|
-
export const TARGET_SLICES_PER_PUSH = 200;
|
|
9
|
-
/**
|
|
10
|
-
* Maximum edges per push request. Mirrors the macOS app's `edgeLimit`. Edges
|
|
11
|
-
* carry only ids, so this bound is about request count, not size.
|
|
12
|
-
*/
|
|
13
|
-
export const EDGE_PUSH_LIMIT = 500;
|
|
14
|
-
/**
|
|
15
|
-
* Split a push into request-sized batches, mirroring the macOS client.
|
|
16
|
-
*
|
|
17
|
-
* All node batches are emitted before any edge batch: edges carry foreign keys
|
|
18
|
-
* to their endpoint nodes, so every node pushed in this sync must land before
|
|
19
|
-
* the edges that reference it. Within a sync, node ordering is otherwise
|
|
20
|
-
* irrelevant — `node.integration_id` is not a foreign key — so nodes are batched
|
|
21
|
-
* purely by accumulated slice count.
|
|
22
|
-
*
|
|
23
|
-
* A node whose own slice count exceeds `targetSliceCount` is still emitted as
|
|
24
|
-
* its own batch rather than dropped, matching the macOS app's "a single large
|
|
25
|
-
* node is kept intact" behaviour.
|
|
26
|
-
*/
|
|
27
|
-
export function planSyncPushBatches(nodes, edges, options = {}) {
|
|
28
|
-
const targetSliceCount = options.targetSliceCount ?? TARGET_SLICES_PER_PUSH;
|
|
29
|
-
const edgeLimit = options.edgeLimit ?? EDGE_PUSH_LIMIT;
|
|
30
|
-
const batches = [];
|
|
31
|
-
let currentNodes = [];
|
|
32
|
-
let sliceCount = 0;
|
|
33
|
-
for (const node of nodes) {
|
|
34
|
-
const nodeSliceCount = node.slices.length;
|
|
35
|
-
if (currentNodes.length > 0 && sliceCount + nodeSliceCount > targetSliceCount) {
|
|
36
|
-
batches.push({ nodes: currentNodes, edges: [] });
|
|
37
|
-
currentNodes = [];
|
|
38
|
-
sliceCount = 0;
|
|
39
|
-
}
|
|
40
|
-
currentNodes.push(node);
|
|
41
|
-
sliceCount += nodeSliceCount;
|
|
42
|
-
}
|
|
43
|
-
if (currentNodes.length > 0) {
|
|
44
|
-
batches.push({ nodes: currentNodes, edges: [] });
|
|
45
|
-
}
|
|
46
|
-
for (let offset = 0; offset < edges.length; offset += edgeLimit) {
|
|
47
|
-
batches.push({ nodes: [], edges: edges.slice(offset, offset + edgeLimit) });
|
|
48
|
-
}
|
|
49
|
-
return batches;
|
|
50
|
-
}
|