codemem 0.47.0-beta.2 → 0.47.0-beta.4
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/dist/index.js +382 -348
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ACCOUNT_CONNECTION_MIGRATION_STATEMENTS, ACCOUNT_UNLINK_MIGRATION_STATEMENTS, AUTH_LINK_AUTHORITY_CUTOVER_SQL, ContextOnlyReplayError, CoordinatorAccountLinkError, DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, DeviceIdentityError, EXTRACTION_BENCHMARK_QUALITY_WEIGHTS, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, ObserverOutputError, ObserverOutputTransportError, OwnerProfileEnrollmentError, PROMPT_TRANSPORT_PROTOCOL_RANGE, REF_BACKFILL_JOB, RawEventIngestValidationError, RawEventSweeper, RefBackfillRunner, RemoteCoordinatorRequestError, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, TRUSTED_HOOK_MAPPER_OPTIONS, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, applyDistillRule, arePromptTransportProtocolRangesCompatible, atomicReplaceConfigFile, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildTieredObserverSelection, buildViewerIdentityTarget, classifyPromptTransportFailure, clonePromptPackAttempt, collectOperationalStatus, compareMemoryRoleReports, connect, connectReadOnly, coordinatorAuthControllerReviewAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantIdentityGroupAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorQualifyOwnerAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewInviteAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeDeviceAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, describePiObserverStatus, detectInstallKind, draftDistillRule, enrollOwnerProfile, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, expandUserPath, exportMemories, extractApplyPatchPaths, extractionReplayObserverIdentity, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getAttributionDiagnostics, getCodememConfigPath, getExtractionBenchmarkProfile, getExtractionModelPricing, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryArtifactReport, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getUpdateStatus, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, importPiSessions, ingestRawEvents, initDatabase, isAutomaticRecallMeasurement, isCoordinatorControllerAttestationReference, isEmbeddingDisabled, isExplicitUpdateInstallEligible, isReleaseVersionForChannel, judgeDistillReport, linkCoordinatorAccount, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadRuntimeSigningPublicKey, loadSqliteVec, mdnsEnabled, mutateCodememConfigFile, normalizeCoordinatorAccountLinkOrigin, normalizePromptTransportProtocolRange, observerBaseUrlForProviderOverride, planReplicationOpsAgePrune, probeCodememViewerLiveness, probeCodememViewerLiveness as probeCodememViewerLiveness$1, probeRequiredNativeRuntime, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readAccountLinkDevice, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorAccountLinkStatus, readCoordinatorOwnerReviewLocalEvidence, readCoordinatorSyncConfig, readImportPayload, recordAutomaticRecall, recordPromptPackArtifacts, recordPromptPackTerminal,
|
|
2
|
+
import { ACCOUNT_CONNECTION_MIGRATION_STATEMENTS, ACCOUNT_UNLINK_MIGRATION_STATEMENTS, AUTH_LINK_AUTHORITY_CUTOVER_SQL, ContextOnlyReplayError, CoordinatorAccountLinkError, DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, DeviceIdentityError, EXTRACTION_BENCHMARK_QUALITY_WEIGHTS, FILE_CONTEXT_TRANSPORT_MAX_BYTES, FILE_CONTEXT_TRANSPORT_PROFILE, LEGACY_OWNER_RESOLUTION_SCHEMA_STATEMENTS, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, ObserverOutputError, ObserverOutputTransportError, OwnerProfileEnrollmentError, PROMPT_TRANSPORT_PROTOCOL_RANGE, REF_BACKFILL_JOB, RawEventIngestValidationError, RawEventSweeper, RefBackfillRunner, RemoteCoordinatorRequestError, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, TRUSTED_HOOK_MAPPER_OPTIONS, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, applyDistillRule, arePromptTransportProtocolRangesCompatible, atomicReplaceConfigFile, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildTieredObserverSelection, buildViewerIdentityTarget, classifyPromptTransportFailure, clonePromptPackAttempt, collectOperationalStatus, compareMemoryRoleReports, connect, connectReadOnly, coordinatorAuthControllerReviewAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantIdentityGroupAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorQualifyOwnerAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewInviteAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeDeviceAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, createFileContextRunner, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, describePiObserverStatus, detectInstallKind, dispatchFileContextTransport, draftDistillRule, enrollOwnerProfile, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, expandUserPath, exportMemories, extractApplyPatchPaths, extractionReplayObserverIdentity, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getAttributionDiagnostics, getCodememConfigPath, getExtractionBenchmarkProfile, getExtractionModelPricing, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryArtifactReport, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getUpdateStatus, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, importPiSessions, ingestRawEvents, initDatabase, isAutomaticRecallMeasurement, isCoordinatorControllerAttestationReference, isEmbeddingDisabled, isExplicitUpdateInstallEligible, isReleaseVersionForChannel, judgeDistillReport, linkCoordinatorAccount, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadRuntimeSigningPublicKey, loadSqliteVec, mdnsEnabled, mutateCodememConfigFile, normalizeCoordinatorAccountLinkOrigin, normalizePromptTransportProtocolRange, observerBaseUrlForProviderOverride, parseControllerHistoryEvidence, parseFileContextTransportRequest, planReplicationOpsAgePrune, probeCodememViewerLiveness, probeCodememViewerLiveness as probeCodememViewerLiveness$1, probeRequiredNativeRuntime, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readAccountLinkDevice, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorAccountLinkStatus, readCoordinatorOwnerReviewLocalEvidence, readCoordinatorSyncConfig, readImportPayload, recordAutomaticRecall, recordPromptPackArtifacts, recordPromptPackTerminal, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveComparablePath, resolveDbPath, resolveHookProject, resolveKeyPaths, resolvePiAgentDir, resolvePiObserverConfig, resolveProject, resolveProjectRoot, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, searchPiSessions, setPeerProjectFilter, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, upgradeCoordinatorAccountSchemaFile, upgradeCoordinatorAuthSchemaFile, upgradeCoordinatorOwnerResolutionSchemaFile, vacuumDatabase, viewerUrl, writeCodememConfigFile } from "@codemem/core";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import omelette from "omelette";
|
|
5
|
-
import { randomInt, randomUUID } from "node:crypto";
|
|
6
5
|
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
6
|
import { homedir, networkInterfaces } from "node:os";
|
|
8
7
|
import { dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
9
|
-
import { styleText } from "node:util";
|
|
8
|
+
import { isDeepStrictEqual, styleText } from "node:util";
|
|
9
|
+
import { randomInt, randomUUID } from "node:crypto";
|
|
10
10
|
import * as p from "@clack/prompts";
|
|
11
11
|
import { serve } from "@hono/node-server";
|
|
12
12
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -79,6 +79,54 @@ function emitJsonError(errorCode, message, exitCode = 1) {
|
|
|
79
79
|
process.exitCode = exitCode;
|
|
80
80
|
}
|
|
81
81
|
//#endregion
|
|
82
|
+
//#region src/commands/claude-hook-file-context-transport.ts
|
|
83
|
+
/** Internal packaged-hook transport. Normal CLI invocation keeps its original guards/output. */
|
|
84
|
+
function runClaudeFileContextTransport(payload, opts) {
|
|
85
|
+
const parsed = parseFileContextTransportRequest(payload);
|
|
86
|
+
if (!parsed.success) throw new Error("invalid file-context transport request");
|
|
87
|
+
let request = parsed.data;
|
|
88
|
+
if ([
|
|
89
|
+
"1",
|
|
90
|
+
"true",
|
|
91
|
+
"yes",
|
|
92
|
+
"on"
|
|
93
|
+
].includes(String(process.env.CODEMEM_PLUGIN_IGNORE ?? "").trim().toLowerCase())) return {
|
|
94
|
+
ok: true,
|
|
95
|
+
attempt_id: request.attempt_id,
|
|
96
|
+
request_id: request.request_id,
|
|
97
|
+
result: { continue: true },
|
|
98
|
+
delivery_pending: false
|
|
99
|
+
};
|
|
100
|
+
const disabled = (value) => [
|
|
101
|
+
"0",
|
|
102
|
+
"false",
|
|
103
|
+
"off"
|
|
104
|
+
].includes(String(value ?? "").trim().toLowerCase());
|
|
105
|
+
if (disabled(process.env.CODEMEM_RETRIEVAL_LEDGER)) request = {
|
|
106
|
+
...request,
|
|
107
|
+
ledger_enabled: false
|
|
108
|
+
};
|
|
109
|
+
if (request.action === "lookup" && disabled(process.env.CODEMEM_FILE_CONTEXT)) {
|
|
110
|
+
const { relative_path: _path, project: _project, file_mtime_ms: _mtime, ...base } = request;
|
|
111
|
+
request = {
|
|
112
|
+
...base,
|
|
113
|
+
action: "skip",
|
|
114
|
+
reason: "file_context_disabled"
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const dbPath = resolveDbPath(resolveDbOpt(opts));
|
|
118
|
+
if (resolve(dbPath) !== resolve(request.db_path) || !isDeepStrictEqual(buildViewerIdentityTarget(), request.identity_target)) throw new Error("file-context target mismatch");
|
|
119
|
+
const store = new MemoryStore(dbPath);
|
|
120
|
+
try {
|
|
121
|
+
if (!store.hasCurrentIdentity()) throw new Error("file-context identity changed");
|
|
122
|
+
return dispatchFileContextTransport(store, request);
|
|
123
|
+
} finally {
|
|
124
|
+
try {
|
|
125
|
+
store.close();
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
82
130
|
//#region src/commands/claude-hook-plugin-log.ts
|
|
83
131
|
/**
|
|
84
132
|
* Append-only plugin event log used by `claude-hook-inject` and
|
|
@@ -130,24 +178,12 @@ function logHookEvent(message) {
|
|
|
130
178
|
//#endregion
|
|
131
179
|
//#region src/commands/claude-hook-file-context.ts
|
|
132
180
|
var FILE_GATE_MIN_BYTES = 1500;
|
|
133
|
-
var FETCH_LIMIT = 40;
|
|
134
|
-
var DISPLAY_LIMIT = 15;
|
|
135
|
-
var MTIME_FRESH_TOLERANCE_MS = 3e5;
|
|
136
181
|
var SMALL_FILE_BYPASS_PATTERNS = [
|
|
137
182
|
/\.(json|jsonc|toml|ya?ml)$/i,
|
|
138
183
|
/\.env(\.|$)/i,
|
|
139
184
|
/(^|\/)dockerfile(\.|$)/i,
|
|
140
185
|
/\.config\.(js|ts|mjs|cjs|json)$/i
|
|
141
186
|
];
|
|
142
|
-
var KIND_ICONS = {
|
|
143
|
-
decision: "⚖️",
|
|
144
|
-
bugfix: "🔴",
|
|
145
|
-
feature: "🟢",
|
|
146
|
-
refactor: "🔄",
|
|
147
|
-
discovery: "🔵",
|
|
148
|
-
change: "✅",
|
|
149
|
-
exploration: "🔬"
|
|
150
|
-
};
|
|
151
187
|
function emitJson$2(value) {
|
|
152
188
|
console.log(JSON.stringify(value));
|
|
153
189
|
}
|
|
@@ -187,105 +223,6 @@ function statFile(absPath) {
|
|
|
187
223
|
return null;
|
|
188
224
|
}
|
|
189
225
|
}
|
|
190
|
-
function parseJsonArray(value) {
|
|
191
|
-
if (!value) return [];
|
|
192
|
-
try {
|
|
193
|
-
const parsed = JSON.parse(value);
|
|
194
|
-
if (!Array.isArray(parsed)) return [];
|
|
195
|
-
return parsed.filter((item) => typeof item === "string");
|
|
196
|
-
} catch {
|
|
197
|
-
return [];
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function normalizePathForCompare(path) {
|
|
201
|
-
return path.replace(/\\/g, "/");
|
|
202
|
-
}
|
|
203
|
-
function scoreRow(row, normalizedTarget, idx) {
|
|
204
|
-
const filesModified = parseJsonArray(row.files_modified);
|
|
205
|
-
const inModified = filesModified.some((f) => normalizePathForCompare(f) === normalizedTarget);
|
|
206
|
-
let score = 0;
|
|
207
|
-
if (inModified) score += 2;
|
|
208
|
-
if (filesModified.length <= 1) score += 2;
|
|
209
|
-
else if (filesModified.length <= 3) score += 1;
|
|
210
|
-
return {
|
|
211
|
-
row,
|
|
212
|
-
score,
|
|
213
|
-
idx
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
function scoreAndDedupe(rows, targetPath, limit) {
|
|
217
|
-
const normalizedTarget = normalizePathForCompare(targetPath);
|
|
218
|
-
const scored = rows.map((row, idx) => scoreRow(row, normalizedTarget, idx));
|
|
219
|
-
const bestPerSession = /* @__PURE__ */ new Map();
|
|
220
|
-
for (const item of scored) {
|
|
221
|
-
const existing = bestPerSession.get(item.row.session_id);
|
|
222
|
-
if (!existing || item.score > existing.score || item.score === existing.score && item.idx < existing.idx) bestPerSession.set(item.row.session_id, item);
|
|
223
|
-
}
|
|
224
|
-
const deduped = Array.from(bestPerSession.values());
|
|
225
|
-
deduped.sort((a, b) => b.score - a.score || a.idx - b.idx);
|
|
226
|
-
return deduped.slice(0, limit).map((s) => s.row);
|
|
227
|
-
}
|
|
228
|
-
function compactTime(timeStr) {
|
|
229
|
-
return timeStr.toLowerCase().replace(" am", "a").replace(" pm", "p");
|
|
230
|
-
}
|
|
231
|
-
function formatTime(epochMs) {
|
|
232
|
-
return new Date(epochMs).toLocaleString("en-US", {
|
|
233
|
-
hour: "numeric",
|
|
234
|
-
minute: "2-digit",
|
|
235
|
-
hour12: true
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
function formatDate(epochMs) {
|
|
239
|
-
return new Date(epochMs).toLocaleString("en-US", {
|
|
240
|
-
month: "short",
|
|
241
|
-
day: "numeric",
|
|
242
|
-
year: "numeric"
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
function formatTimeline(rows, filePath, staleness) {
|
|
246
|
-
const safePath = filePath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
247
|
-
const enriched = rows.map((row) => ({
|
|
248
|
-
row,
|
|
249
|
-
epochMs: Date.parse(row.created_at)
|
|
250
|
-
})).filter((item) => Number.isFinite(item.epochMs) && item.epochMs > 0);
|
|
251
|
-
const byDay = /* @__PURE__ */ new Map();
|
|
252
|
-
for (const item of enriched) {
|
|
253
|
-
const day = formatDate(item.epochMs);
|
|
254
|
-
const bucket = byDay.get(day);
|
|
255
|
-
if (bucket) bucket.push(item);
|
|
256
|
-
else byDay.set(day, [item]);
|
|
257
|
-
}
|
|
258
|
-
const sortedDays = Array.from(byDay.entries()).sort((a, b) => {
|
|
259
|
-
return Math.min(...a[1].map((i) => i.epochMs)) - Math.min(...b[1].map((i) => i.epochMs));
|
|
260
|
-
});
|
|
261
|
-
const ids = rows.map((r) => r.id);
|
|
262
|
-
const lines = [`This file (${safePath}) has prior codemem observations. The Read result below is unchanged.`, `- Fetch full bodies on demand: memory.get_observations([${ids.join(", ")}]).`];
|
|
263
|
-
if (staleness) {
|
|
264
|
-
const driftMinutes = Math.max(1, Math.round((staleness.fileMtimeMs - staleness.newestObservationMs) / 6e4));
|
|
265
|
-
lines.unshift(`Heads up: this file was modified ~${driftMinutes} min after the most recent observation below. Past entries may be partially stale — verify against the Read result before relying on them.`);
|
|
266
|
-
}
|
|
267
|
-
for (const [day, dayItems] of sortedDays) {
|
|
268
|
-
const chronological = [...dayItems].sort((a, b) => a.epochMs - b.epochMs);
|
|
269
|
-
lines.push(`### ${day}`);
|
|
270
|
-
for (const { row, epochMs } of chronological) {
|
|
271
|
-
const title = (row.title || "Untitled").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
|
|
272
|
-
const icon = KIND_ICONS[row.kind] ?? "❔";
|
|
273
|
-
const time = compactTime(formatTime(epochMs));
|
|
274
|
-
lines.push(`${row.id} ${time} ${icon} (${row.kind}) ${title}`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return lines.join("\n");
|
|
278
|
-
}
|
|
279
|
-
function queryByFile(dbPath, relativePath, project, limit) {
|
|
280
|
-
const store = new MemoryStore(dbPath);
|
|
281
|
-
try {
|
|
282
|
-
const opts = { limit };
|
|
283
|
-
if (project) opts.project = project;
|
|
284
|
-
return store.findByFile(relativePath, opts);
|
|
285
|
-
} finally {
|
|
286
|
-
store.close();
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
226
|
function resolveProject$1(payload) {
|
|
290
227
|
const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
|
|
291
228
|
return resolveHookProject(cwd, payload.project);
|
|
@@ -294,73 +231,45 @@ function sourceSessionId(payload) {
|
|
|
294
231
|
const value = payload.session_id;
|
|
295
232
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
296
233
|
}
|
|
297
|
-
|
|
298
|
-
if (envTruthy$4(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$2();
|
|
299
|
-
const filePath = extractFilePath(payload);
|
|
300
|
-
if (!filePath) return continueResult$2();
|
|
301
|
-
const startedAt = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
302
|
-
const attemptId = (deps.createAttemptId ?? randomUUID)();
|
|
303
|
-
const resolveDb = deps.resolveDb ?? resolveDbPath;
|
|
234
|
+
function fileContextStore(opts, deps) {
|
|
304
235
|
let resolvedDbPath = null;
|
|
305
236
|
let activeStore = null;
|
|
306
|
-
const
|
|
307
|
-
resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
|
|
308
|
-
|
|
309
|
-
return activeStore;
|
|
237
|
+
const dbPath = () => {
|
|
238
|
+
resolvedDbPath ??= (deps.resolveDb ?? resolveDbPath)(resolveDbOpt(opts));
|
|
239
|
+
return resolvedDbPath;
|
|
310
240
|
};
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
} catch {}
|
|
346
|
-
};
|
|
347
|
-
const updateDelivery = (status) => {
|
|
348
|
-
if (!envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1")) return;
|
|
349
|
-
try {
|
|
350
|
-
resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
|
|
351
|
-
if (deps.updateDelivery) deps.updateDelivery(resolvedDbPath, attemptId, status);
|
|
352
|
-
else tryUpdateRetrievalDelivery(getStore().db, attemptId, status);
|
|
353
|
-
} catch {}
|
|
354
|
-
};
|
|
355
|
-
if (!envNotDisabled$3(process.env.CODEMEM_FILE_CONTEXT || "1")) {
|
|
356
|
-
record({
|
|
357
|
-
retrievalStatus: "skipped",
|
|
358
|
-
deliveryStatus: "not_attempted",
|
|
359
|
-
failureCode: "file_context_disabled",
|
|
360
|
-
failureStage: "configuration"
|
|
361
|
-
});
|
|
362
|
-
return finish(continueResult$2());
|
|
363
|
-
}
|
|
241
|
+
return {
|
|
242
|
+
dbPath,
|
|
243
|
+
getStore: () => {
|
|
244
|
+
activeStore ??= (deps.createStore ?? ((path) => new MemoryStore(path)))(dbPath());
|
|
245
|
+
return activeStore;
|
|
246
|
+
},
|
|
247
|
+
finish: (result) => {
|
|
248
|
+
try {
|
|
249
|
+
activeStore?.close();
|
|
250
|
+
} catch {}
|
|
251
|
+
activeStore = null;
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function fileContextRunner(payload, store, deps) {
|
|
257
|
+
const findByFile = deps.queryByFile;
|
|
258
|
+
const recordAttempt = deps.recordAttempt;
|
|
259
|
+
const updateDelivery = deps.updateDelivery;
|
|
260
|
+
return createFileContextRunner({
|
|
261
|
+
getStore: store.getStore,
|
|
262
|
+
sourceSessionId: sourceSessionId(payload),
|
|
263
|
+
ledgerEnabled: () => envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1"),
|
|
264
|
+
now: deps.now,
|
|
265
|
+
createAttemptId: deps.createAttemptId,
|
|
266
|
+
log: logHookEvent,
|
|
267
|
+
...findByFile ? { findByFile: (path, options) => findByFile(store.dbPath(), path, options.project ?? null, options.limit) } : {},
|
|
268
|
+
...recordAttempt ? { recordAttempt: (input) => recordAttempt(store.dbPath(), input) } : {},
|
|
269
|
+
...updateDelivery ? { updateDelivery: (attemptId, status) => updateDelivery(store.dbPath(), attemptId, status) } : {}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function fileContextPath(filePath, payload) {
|
|
364
273
|
const cwd = typeof payload.cwd === "string" && payload.cwd.trim() ? payload.cwd : process.cwd();
|
|
365
274
|
const expandedPath = expandHome$2(filePath);
|
|
366
275
|
const absolutePath = isAbsolute(expandedPath) ? expandedPath : resolve(cwd, expandedPath);
|
|
@@ -368,133 +277,78 @@ async function buildClaudeFileContext(payload, opts, deps = {}) {
|
|
|
368
277
|
const escapesCwd = relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath);
|
|
369
278
|
if (!relativePath || escapesCwd) {
|
|
370
279
|
logHookEvent(`file_context.skip reason=outside_cwd path=${JSON.stringify(filePath)} cwd=${JSON.stringify(cwd)}`);
|
|
371
|
-
|
|
372
|
-
retrievalStatus: "skipped",
|
|
373
|
-
deliveryStatus: "not_attempted",
|
|
374
|
-
failureCode: "outside_cwd",
|
|
375
|
-
failureStage: "path_validation"
|
|
376
|
-
});
|
|
377
|
-
return finish(continueResult$2());
|
|
280
|
+
return null;
|
|
378
281
|
}
|
|
282
|
+
return {
|
|
283
|
+
absolutePath,
|
|
284
|
+
relativePath
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async function buildClaudeFileContext(payload, opts, deps = {}) {
|
|
288
|
+
if (envTruthy$4(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$2();
|
|
289
|
+
const filePath = extractFilePath(payload);
|
|
290
|
+
if (!filePath) return continueResult$2();
|
|
291
|
+
const store = fileContextStore(opts, deps);
|
|
292
|
+
const runner = fileContextRunner(payload, store, deps);
|
|
293
|
+
if (!envNotDisabled$3(process.env.CODEMEM_FILE_CONTEXT || "1")) return store.finish(runner.skip({ reason: "file_context_disabled" }));
|
|
294
|
+
const path = fileContextPath(filePath, payload);
|
|
295
|
+
if (!path) return store.finish(runner.skip({ reason: "outside_cwd" }));
|
|
296
|
+
const { absolutePath, relativePath } = path;
|
|
379
297
|
const minBytes = Number.parseInt(process.env.CODEMEM_FILE_CONTEXT_MIN_BYTES ?? `${FILE_GATE_MIN_BYTES}`, 10);
|
|
380
298
|
const minBytesEffective = Number.isFinite(minBytes) && minBytes >= 0 ? minBytes : FILE_GATE_MIN_BYTES;
|
|
381
299
|
const stat = (deps.statFile ?? statFile)(absolutePath);
|
|
382
300
|
if (!stat) {
|
|
383
301
|
logHookEvent(`file_context.skip reason=stat_failed path=${JSON.stringify(relativePath)}`);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
deliveryStatus: "not_attempted",
|
|
387
|
-
failureCode: "stat_failed",
|
|
388
|
-
failureStage: "file_access",
|
|
302
|
+
return store.finish(runner.skip({
|
|
303
|
+
reason: "stat_failed",
|
|
389
304
|
repositoryPaths: [relativePath]
|
|
390
|
-
});
|
|
391
|
-
return finish(continueResult$2());
|
|
305
|
+
}));
|
|
392
306
|
}
|
|
393
307
|
const bypassSizeGate = SMALL_FILE_BYPASS_PATTERNS.some((p) => p.test(relativePath));
|
|
394
308
|
if (stat.sizeBytes < minBytesEffective && !bypassSizeGate) {
|
|
395
309
|
logHookEvent(`file_context.skip reason=below_size_gate path=${JSON.stringify(relativePath)} size=${stat.sizeBytes} gate=${minBytesEffective}`);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
deliveryStatus: "not_attempted",
|
|
399
|
-
failureCode: "below_size_gate",
|
|
400
|
-
failureStage: "size_gate",
|
|
401
|
-
repositoryPaths: [relativePath]
|
|
402
|
-
});
|
|
403
|
-
return finish(continueResult$2());
|
|
404
|
-
}
|
|
405
|
-
const project = resolveProject$1(payload);
|
|
406
|
-
const queryFn = deps.queryByFile ?? queryByFile;
|
|
407
|
-
let rows = [];
|
|
408
|
-
try {
|
|
409
|
-
resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
|
|
410
|
-
rows = deps.queryByFile ? queryFn(resolvedDbPath, relativePath, project, FETCH_LIMIT) : getStore().findByFile(relativePath, {
|
|
411
|
-
limit: FETCH_LIMIT,
|
|
412
|
-
...project ? { project } : {}
|
|
413
|
-
});
|
|
414
|
-
} catch (err) {
|
|
415
|
-
logHookEvent(`codemem claude-hook-file-context query failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
416
|
-
record({
|
|
417
|
-
retrievalStatus: "failed",
|
|
418
|
-
deliveryStatus: "not_attempted",
|
|
419
|
-
failureCode: "query_failed",
|
|
420
|
-
failureStage: "retrieval",
|
|
421
|
-
project,
|
|
422
|
-
filters: project ? { project } : void 0,
|
|
310
|
+
return store.finish(runner.skip({
|
|
311
|
+
reason: "below_size_gate",
|
|
423
312
|
repositoryPaths: [relativePath]
|
|
424
|
-
});
|
|
425
|
-
return finish(continueResult$2());
|
|
426
|
-
}
|
|
427
|
-
if (rows.length === 0) {
|
|
428
|
-
logHookEvent(`file_context.skip reason=no_observations path=${JSON.stringify(relativePath)} project=${JSON.stringify(project ?? "")}`);
|
|
429
|
-
record({
|
|
430
|
-
retrievalStatus: "no_results",
|
|
431
|
-
deliveryStatus: "not_attempted",
|
|
432
|
-
project,
|
|
433
|
-
filters: project ? { project } : void 0,
|
|
434
|
-
repositoryPaths: [relativePath]
|
|
435
|
-
});
|
|
436
|
-
return finish(continueResult$2());
|
|
437
|
-
}
|
|
438
|
-
const top = scoreAndDedupe(rows, relativePath, DISPLAY_LIMIT);
|
|
439
|
-
if (top.length === 0) {
|
|
440
|
-
logHookEvent(`file_context.skip reason=no_top_after_dedupe path=${JSON.stringify(relativePath)} candidates=${rows.length}`);
|
|
441
|
-
record({
|
|
442
|
-
retrievalStatus: "succeeded",
|
|
443
|
-
deliveryStatus: "not_attempted",
|
|
444
|
-
candidateIds: rows.map((row) => row.id),
|
|
445
|
-
candidateCount: rows.length,
|
|
446
|
-
selectedIds: [],
|
|
447
|
-
failureCode: "no_top_after_dedupe",
|
|
448
|
-
failureStage: "selection",
|
|
449
|
-
project,
|
|
450
|
-
filters: project ? { project } : void 0,
|
|
451
|
-
repositoryPaths: [relativePath]
|
|
452
|
-
});
|
|
453
|
-
return finish(continueResult$2());
|
|
454
|
-
}
|
|
455
|
-
let staleness = null;
|
|
456
|
-
if (stat.mtimeMs > 0) {
|
|
457
|
-
const newestObservationMs = top.reduce((max, row) => {
|
|
458
|
-
const epoch = Date.parse(row.created_at);
|
|
459
|
-
return Number.isFinite(epoch) && epoch > max ? epoch : max;
|
|
460
|
-
}, 0);
|
|
461
|
-
if (newestObservationMs > 0 && stat.mtimeMs > newestObservationMs + MTIME_FRESH_TOLERANCE_MS) staleness = {
|
|
462
|
-
fileMtimeMs: stat.mtimeMs,
|
|
463
|
-
newestObservationMs
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
record({
|
|
467
|
-
retrievalStatus: "succeeded",
|
|
468
|
-
deliveryStatus: "not_attempted",
|
|
469
|
-
candidateIds: rows.map((row) => row.id),
|
|
470
|
-
candidateCount: rows.length,
|
|
471
|
-
selectedIds: top.map((row) => row.id),
|
|
472
|
-
project,
|
|
473
|
-
filters: project ? { project } : void 0,
|
|
474
|
-
repositoryPaths: [relativePath]
|
|
475
|
-
});
|
|
476
|
-
let timeline;
|
|
477
|
-
try {
|
|
478
|
-
timeline = formatTimeline(top, relativePath, staleness);
|
|
479
|
-
} catch {
|
|
480
|
-
updateDelivery("failed");
|
|
481
|
-
return finish(continueResult$2());
|
|
313
|
+
}));
|
|
482
314
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
additionalContext: timeline
|
|
489
|
-
} });
|
|
315
|
+
return store.finish(runner.run({
|
|
316
|
+
relativePath,
|
|
317
|
+
project: resolveProject$1(payload),
|
|
318
|
+
fileMtimeMs: stat.mtimeMs
|
|
319
|
+
}));
|
|
490
320
|
}
|
|
491
321
|
var claudeHookFileContextCmd = new Command("claude-hook-file-context").configureHelp(helpStyle).description("Return Claude PreToolUse:Read additionalContext from per-file observation timeline");
|
|
492
322
|
addDbOption(claudeHookFileContextCmd);
|
|
323
|
+
claudeHookFileContextCmd.addOption(new Option("--transport-profile").hideHelp());
|
|
324
|
+
claudeHookFileContextCmd.addOption(new Option("--transport").hideHelp());
|
|
493
325
|
var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts) => {
|
|
326
|
+
if (opts.transportProfile) {
|
|
327
|
+
process.stdout.write(`${JSON.stringify(FILE_CONTEXT_TRANSPORT_PROFILE)}\n`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
494
330
|
let raw = "";
|
|
495
|
-
for await (const chunk of process.stdin)
|
|
331
|
+
for await (const chunk of process.stdin) {
|
|
332
|
+
raw += String(chunk);
|
|
333
|
+
if (opts.transport && Buffer.byteLength(raw) > FILE_CONTEXT_TRANSPORT_MAX_BYTES) {
|
|
334
|
+
emitError$2({
|
|
335
|
+
error: "parse_error",
|
|
336
|
+
message: "transport payload too large"
|
|
337
|
+
});
|
|
338
|
+
process.exitCode = 1;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
496
342
|
const trimmed = raw.trim();
|
|
497
343
|
if (!trimmed) {
|
|
344
|
+
if (opts.transport) {
|
|
345
|
+
process.stdout.write(`${JSON.stringify({
|
|
346
|
+
error: "parse_error",
|
|
347
|
+
message: "transport request required"
|
|
348
|
+
})}\n`);
|
|
349
|
+
process.exitCode = 1;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
498
352
|
emitJson$2(continueResult$2());
|
|
499
353
|
return;
|
|
500
354
|
}
|
|
@@ -518,8 +372,23 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
|
|
|
518
372
|
process.exitCode = 1;
|
|
519
373
|
return;
|
|
520
374
|
}
|
|
521
|
-
|
|
375
|
+
await emitFileContext(payload, opts);
|
|
522
376
|
});
|
|
377
|
+
async function emitFileContext(payload, opts) {
|
|
378
|
+
try {
|
|
379
|
+
if (opts.transport) {
|
|
380
|
+
process.stdout.write(`${JSON.stringify(runClaudeFileContextTransport(payload, opts))}\n`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
emitJson$2(await buildClaudeFileContext(payload, opts));
|
|
384
|
+
} catch {
|
|
385
|
+
emitError$2({
|
|
386
|
+
error: "file_context_failed",
|
|
387
|
+
message: "file context unavailable"
|
|
388
|
+
});
|
|
389
|
+
process.exitCode = 1;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
523
392
|
//#endregion
|
|
524
393
|
//#region src/commands/hook-ingest-live-spool.ts
|
|
525
394
|
function spoolLiveHookPayload(dir, payload, logPrefix) {
|
|
@@ -1727,7 +1596,7 @@ async function buildLocalPack$2(context, project, dbPath, workingSetPaths = [])
|
|
|
1727
1596
|
packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics.pack_tokens) : 0
|
|
1728
1597
|
};
|
|
1729
1598
|
} finally {
|
|
1730
|
-
store.
|
|
1599
|
+
await store.closeAsync();
|
|
1731
1600
|
}
|
|
1732
1601
|
}
|
|
1733
1602
|
async function buildClaudeHookInjection(payload, opts, deps = {}) {
|
|
@@ -2368,7 +2237,7 @@ async function buildLocalPack$1(context, project, dbPath) {
|
|
|
2368
2237
|
packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics?.pack_tokens) : 0
|
|
2369
2238
|
};
|
|
2370
2239
|
} finally {
|
|
2371
|
-
store.
|
|
2240
|
+
await store.closeAsync();
|
|
2372
2241
|
}
|
|
2373
2242
|
}
|
|
2374
2243
|
async function prepareCodexHookInjection(payload, opts, deps = {}) {
|
|
@@ -2630,21 +2499,34 @@ function hasDatabaseSelection(options) {
|
|
|
2630
2499
|
return options.dbPath !== void 0 || options.db !== void 0;
|
|
2631
2500
|
}
|
|
2632
2501
|
function exportAccountSchema(options) {
|
|
2633
|
-
|
|
2634
|
-
|
|
2502
|
+
let statements = options.deviceUnlink ? ACCOUNT_UNLINK_MIGRATION_STATEMENTS : ACCOUNT_CONNECTION_MIGRATION_STATEMENTS;
|
|
2503
|
+
let prerequisites = accountPrerequisites;
|
|
2504
|
+
if (options.ownerResolution) {
|
|
2505
|
+
statements = LEGACY_OWNER_RESOLUTION_SCHEMA_STATEMENTS;
|
|
2506
|
+
prerequisites += " Requires the current account-management schema first; installs inert receipt storage only, not ownership or access.";
|
|
2507
|
+
}
|
|
2508
|
+
const text = `-- ${prerequisites}\n-- Execute in ONE atomic D1 batch; do not split into autocommitted requests. Nothing has been applied.\n${statements.map((s) => `${s.trim().replace(/;$/u, "")};`).join("\n")}\n`;
|
|
2635
2509
|
process.stdout.write(options.json ? `${JSON.stringify({
|
|
2636
2510
|
statements,
|
|
2637
2511
|
applied: false
|
|
2638
2512
|
})}\n` : text);
|
|
2639
2513
|
}
|
|
2514
|
+
function invalidAccountSelection(options, path) {
|
|
2515
|
+
if (options.exportSql) return hasDatabaseSelection(options);
|
|
2516
|
+
if (!path) return true;
|
|
2517
|
+
return Boolean(options.ownerResolution && !hasDatabaseSelection(options));
|
|
2518
|
+
}
|
|
2640
2519
|
async function upgradeAccount(options) {
|
|
2520
|
+
if (options.ownerResolution && options.deviceUnlink) {
|
|
2521
|
+
failure$4(options, "Use --owner-resolution without --device-unlink.", 2, "account_schema_upgrade_failed");
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2641
2524
|
if (options.deviceUnlink && !options.exportSql) {
|
|
2642
2525
|
failure$4(options, "Use --device-unlink only with --export-sql; local upgrades detect the installed schema.", 2, "account_schema_upgrade_failed");
|
|
2643
2526
|
return;
|
|
2644
2527
|
}
|
|
2645
2528
|
const path = resolveDbOpt(options);
|
|
2646
|
-
|
|
2647
|
-
if (options.exportSql && selected || !options.exportSql && !path) {
|
|
2529
|
+
if (invalidAccountSelection(options, path)) {
|
|
2648
2530
|
failure$4(options, options.exportSql ? "Use --export-sql without --db-path; export opens no database." : `Select an existing coordinator database with --db-path. ${accountPrerequisites}`, 2, "account_schema_upgrade_failed");
|
|
2649
2531
|
return;
|
|
2650
2532
|
}
|
|
@@ -2655,14 +2537,23 @@ async function upgradeAccount(options) {
|
|
|
2655
2537
|
}
|
|
2656
2538
|
if (!path) return;
|
|
2657
2539
|
if (!options.json) process.stderr.write(`${accountPrerequisites}\n`);
|
|
2658
|
-
await
|
|
2659
|
-
|
|
2540
|
+
if (options.ownerResolution) await upgradeCoordinatorOwnerResolutionSchemaFile(path);
|
|
2541
|
+
else await upgradeCoordinatorAccountSchemaFile(path);
|
|
2542
|
+
process.stdout.write(options.json ? `${JSON.stringify({ upgraded: true })}\n` : accountUpgradeSuccess(options));
|
|
2660
2543
|
} catch (error) {
|
|
2661
|
-
failure$4(options, `Account schema upgrade stopped. Inspect the selected database before retrying. ${
|
|
2544
|
+
failure$4(options, `Account schema upgrade stopped. Inspect the selected database before retrying. ${accountUpgradeFailure(options, error)}`, 1, "account_schema_upgrade_failed");
|
|
2662
2545
|
}
|
|
2663
2546
|
}
|
|
2547
|
+
function accountUpgradeSuccess(options) {
|
|
2548
|
+
if (options.ownerResolution) return "Coordinator owner-resolution receipt storage ready; ownership and access unchanged.\n";
|
|
2549
|
+
return "Coordinator account schema upgraded.\n";
|
|
2550
|
+
}
|
|
2551
|
+
function accountUpgradeFailure(options, error) {
|
|
2552
|
+
if (options.ownerResolution) return "Owner-resolution storage requires the current account schema: first use upgrade-account-schema without --owner-resolution. Partial, unsupported or colliding receipt schemas cannot be repaired by this command. Check file access and writer locks.";
|
|
2553
|
+
return error instanceof Error ? error.message : "Coordinator account schema upgrade failed.";
|
|
2554
|
+
}
|
|
2664
2555
|
function buildCoordinatorAccountSchemaCommand() {
|
|
2665
|
-
const command = new Command("upgrade-account-schema").configureHelp(helpStyle).description("Explicitly install optional coordinator account management on an existing V2 database").option("--device-unlink", "export only the delta for the prior account-management schema (requires --export-sql)").option("-e, --export-sql", "export statement boundaries for one atomic D1 batch without opening a database").addHelpText("after", `\n${accountPrerequisites}\nD1 export does not apply SQL or verify remote execution limits.\n`);
|
|
2556
|
+
const command = new Command("upgrade-account-schema").configureHelp(helpStyle).description("Explicitly install optional coordinator account management on an existing V2 database").option("-o, --owner-resolution", "install inert owner-resolution receipt storage (requires current account schema)").option("--device-unlink", "export only the delta for the prior account-management schema (requires --export-sql)").option("-e, --export-sql", "export statement boundaries for one atomic D1 batch without opening a database").addHelpText("after", `\n${accountPrerequisites}\n--owner-resolution installs storage only; it does not resolve an owner or change access. It requires the current account schema and an explicit --db-path, or --export-sql without a database. It cannot be combined with --device-unlink.\nD1 export does not apply SQL or verify remote execution limits.\n`);
|
|
2666
2557
|
addDbOption(command);
|
|
2667
2558
|
addJsonOption(command);
|
|
2668
2559
|
command.action(upgradeAccount);
|
|
@@ -3195,7 +3086,8 @@ var REVIEW_REASONS = [
|
|
|
3195
3086
|
"key_mismatch",
|
|
3196
3087
|
"enrollment_identity_mismatch",
|
|
3197
3088
|
"invite_identity_mismatch",
|
|
3198
|
-
"invite_evidence_invalid"
|
|
3089
|
+
"invite_evidence_invalid",
|
|
3090
|
+
"legacy_history_invalid"
|
|
3199
3091
|
];
|
|
3200
3092
|
function validId$1(value) {
|
|
3201
3093
|
return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
|
|
@@ -3204,6 +3096,7 @@ function isHash(value) {
|
|
|
3204
3096
|
return typeof value === "string" && value.length === 64 && /^[a-f0-9]{64}$/.test(value);
|
|
3205
3097
|
}
|
|
3206
3098
|
function reviewErrorMessage(error) {
|
|
3099
|
+
if (error instanceof CoordinatorHistoryUnavailableError) return "Update the coordinator before reviewing this device owner: retained history evidence was missing or invalid in its response. Nothing changed.";
|
|
3207
3100
|
if (error instanceof RemoteCoordinatorRequestError) {
|
|
3208
3101
|
if (error.code === "review_stale") return "Evidence changed since the preview. Nothing changed; run the review again.";
|
|
3209
3102
|
if (error.code === "already_reviewed_or_needs_review") return "This device already has a reviewed owner, or needs a separate review. Nothing changed.";
|
|
@@ -3271,9 +3164,27 @@ function validInviteCount(reply, invites) {
|
|
|
3271
3164
|
function validInviteRef(invite) {
|
|
3272
3165
|
return !!invite && validId$1(invite.invite_id) && (invite.kind === "team_member" || invite.kind === "add_device");
|
|
3273
3166
|
}
|
|
3167
|
+
function previewEnrollment(enrollment) {
|
|
3168
|
+
if (!enrollment) return null;
|
|
3169
|
+
return {
|
|
3170
|
+
device_id: enrollment.device_id,
|
|
3171
|
+
fingerprint: enrollment.fingerprint,
|
|
3172
|
+
display_name: null,
|
|
3173
|
+
identity_label: enrollment.identity_label
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
var CoordinatorHistoryUnavailableError = class extends Error {};
|
|
3177
|
+
function parsePreviewHistory(value, coordinatorId) {
|
|
3178
|
+
try {
|
|
3179
|
+
return parseControllerHistoryEvidence(value, coordinatorId);
|
|
3180
|
+
} catch {
|
|
3181
|
+
throw new CoordinatorHistoryUnavailableError();
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3274
3184
|
function parsePreview(payload, request) {
|
|
3275
3185
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid_preview");
|
|
3276
3186
|
const reply = payload;
|
|
3187
|
+
if (!Array.isArray(reply.legacy_history)) throw new CoordinatorHistoryUnavailableError();
|
|
3277
3188
|
const enrollment = reply.enrollment;
|
|
3278
3189
|
const invites = reply.reviewed_invites;
|
|
3279
3190
|
if (reply.state !== "ready" && reply.state !== "needs_review" || !validId$1(reply.coordinator_id) || !Array.isArray(reply.reasons) || !reply.reasons.every((reason) => typeof reason === "string" && REVIEW_REASONS.includes(reason)) || !Array.isArray(invites) || !validInviteCount(reply, invites) || !invites.every(validInviteRef)) throw new Error("invalid_preview");
|
|
@@ -3282,14 +3193,10 @@ function parsePreview(payload, request) {
|
|
|
3282
3193
|
return {
|
|
3283
3194
|
state: reply.state,
|
|
3284
3195
|
coordinator_id: reply.coordinator_id,
|
|
3196
|
+
legacy_history: parsePreviewHistory(reply.legacy_history, reply.coordinator_id),
|
|
3285
3197
|
reasons: reply.reasons,
|
|
3286
3198
|
evidence_digest: reply.state === "ready" ? reply.evidence_digest : void 0,
|
|
3287
|
-
enrollment: enrollment
|
|
3288
|
-
device_id: enrollment.device_id,
|
|
3289
|
-
fingerprint: enrollment.fingerprint,
|
|
3290
|
-
display_name: null,
|
|
3291
|
-
identity_label: enrollment.identity_label
|
|
3292
|
-
} : null,
|
|
3199
|
+
enrollment: previewEnrollment(enrollment),
|
|
3293
3200
|
reviewed_invites: invites.map((invite) => ({
|
|
3294
3201
|
invite_id: invite.invite_id,
|
|
3295
3202
|
kind: invite.kind
|
|
@@ -3364,6 +3271,86 @@ function emitReviewPreview(evidence, preview) {
|
|
|
3364
3271
|
...stopped
|
|
3365
3272
|
})}\n`);
|
|
3366
3273
|
}
|
|
3274
|
+
function terminalIdentifier(value) {
|
|
3275
|
+
return JSON.stringify(value).replace(/[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/gu, (character) => `\\u{${character.codePointAt(0)?.toString(16)}}`);
|
|
3276
|
+
}
|
|
3277
|
+
function historyAssociation(deviceId, fingerprint, subject) {
|
|
3278
|
+
if (deviceId === subject.deviceId) {
|
|
3279
|
+
if (fingerprint === subject.fingerprint) return "Current device; current fingerprint";
|
|
3280
|
+
return "Current device; different recorded key";
|
|
3281
|
+
}
|
|
3282
|
+
if (fingerprint === subject.fingerprint) return "Other device; current fingerprint";
|
|
3283
|
+
return "Other recorded device/key; canonical-key equivalence is not identified in this summary";
|
|
3284
|
+
}
|
|
3285
|
+
function* controllerHistoryRows(preview, subject) {
|
|
3286
|
+
for (const record of preview.legacy_history) {
|
|
3287
|
+
yield {
|
|
3288
|
+
text: `Historical group ${terminalIdentifier(record.group_id)}; Team ${terminalIdentifier(record.manifest.team_id)}; completed ${terminalIdentifier(record.completed_at)}\nCandidate: ${terminalIdentifier(record.candidate_ref)}; manifest version: ${record.manifest_version}; recorded: ${terminalIdentifier(record.created_at)}\nOriginal retained manifest JSON: ${terminalIdentifier(record.manifest_json)}`,
|
|
3289
|
+
records: 1,
|
|
3290
|
+
decisions: 0
|
|
3291
|
+
};
|
|
3292
|
+
for (const decision of record.manifest.device_decisions) {
|
|
3293
|
+
const identity = decision.identity_id === null ? "none" : terminalIdentifier(decision.identity_id);
|
|
3294
|
+
yield {
|
|
3295
|
+
text: ` ${decision.decision}; Identity: ${identity}; enabled: ${decision.enabled ? "yes" : "no"}\n Device: ${terminalIdentifier(decision.device_id)}; fingerprint: ${terminalIdentifier(decision.key_fingerprint)}\n ${historyAssociation(decision.device_id, decision.key_fingerprint, subject)}`,
|
|
3296
|
+
records: 0,
|
|
3297
|
+
decisions: 1
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
/** A truncated summary must never authorize the terminal confirmation. */
|
|
3303
|
+
function formatControllerReviewHistory(preview, subject) {
|
|
3304
|
+
const lines = [
|
|
3305
|
+
`Proposed current owner Identity: ${terminalIdentifier(subject.identityId)}`,
|
|
3306
|
+
"Retained legacy sharing assignments (not ownership changes):",
|
|
3307
|
+
"Records may contain other devices. Different fingerprints may represent known key aliases; no row below is assumed to belong to this device unless labeled as an exact device/fingerprint match."
|
|
3308
|
+
];
|
|
3309
|
+
let records = 0;
|
|
3310
|
+
let decisions = 0;
|
|
3311
|
+
let characters = lines.join("\n").length;
|
|
3312
|
+
let complete = true;
|
|
3313
|
+
for (const row of controllerHistoryRows(preview, subject)) {
|
|
3314
|
+
if (records + row.records > 20 || decisions + row.decisions > 40 || characters + row.text.length + 1 > 24e3) {
|
|
3315
|
+
complete = false;
|
|
3316
|
+
break;
|
|
3317
|
+
}
|
|
3318
|
+
lines.push(row.text);
|
|
3319
|
+
records += row.records;
|
|
3320
|
+
decisions += row.decisions;
|
|
3321
|
+
characters += row.text.length + 1;
|
|
3322
|
+
}
|
|
3323
|
+
if (preview.legacy_history.length === 0) lines.push("No relevant retained legacy assignments.");
|
|
3324
|
+
if (!complete) {
|
|
3325
|
+
const totalDecisions = preview.legacy_history.reduce((total, record) => total + record.manifest.device_decisions.length, 0);
|
|
3326
|
+
lines.push(`TRUNCATED: showing ${records} of ${preview.legacy_history.length} records and ${decisions} of ${totalDecisions} device decisions. Terminal confirmation is disabled.`);
|
|
3327
|
+
}
|
|
3328
|
+
lines.push("Previous assignments are retained. This review records current ownership; it does not migrate sharing, move memories, merge Identities, or grant Team/Project access.", "Approval grants the existing controller authority, including account management when the coordinator permits it. Google account linking remains a separate step.");
|
|
3329
|
+
return {
|
|
3330
|
+
text: lines.join("\n"),
|
|
3331
|
+
complete
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
function showCoordinatorReview(preview, request, options) {
|
|
3335
|
+
p.log.info(`Coordinator: ${preview.coordinator_id}; reviewed invitations: ${preview.reviewed_invite_count}`, { output: process.stderr });
|
|
3336
|
+
const history = formatControllerReviewHistory(preview, request);
|
|
3337
|
+
p.log.message(history.text, { output: process.stderr });
|
|
3338
|
+
if (history.complete) return true;
|
|
3339
|
+
failure$1(options, "Nothing changed. Run the same command with --json to inspect all retained evidence. Ask the coordinator operator to use the administrator review API for evidence exceeding this terminal's display limit.", 2);
|
|
3340
|
+
return false;
|
|
3341
|
+
}
|
|
3342
|
+
function reportReviewFailure(options, error) {
|
|
3343
|
+
if (options.json && error instanceof CoordinatorHistoryUnavailableError) {
|
|
3344
|
+
process.stdout.write(`${JSON.stringify({
|
|
3345
|
+
error: "owner_review_unavailable",
|
|
3346
|
+
message: reviewErrorMessage(error),
|
|
3347
|
+
unchanged: true
|
|
3348
|
+
})}\n`);
|
|
3349
|
+
process.exitCode = 1;
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
failure$1(options, reviewErrorMessage(error));
|
|
3353
|
+
}
|
|
3367
3354
|
async function reviewDeviceOwner(group, options) {
|
|
3368
3355
|
try {
|
|
3369
3356
|
if (!validId$1(group)) return failure$1(options, "Use a valid coordinator group ID.", 2);
|
|
@@ -3392,10 +3379,10 @@ async function reviewDeviceOwner(group, options) {
|
|
|
3392
3379
|
return;
|
|
3393
3380
|
}
|
|
3394
3381
|
if (preview.state !== "ready") return failure$1(options, `Coordinator review stopped: ${preview.reasons.join(", ")}. Nothing changed.`);
|
|
3395
|
-
|
|
3382
|
+
if (!showCoordinatorReview(preview, request, options)) return;
|
|
3396
3383
|
await confirmReview(group, options, evidence, request, preview);
|
|
3397
3384
|
} catch (error) {
|
|
3398
|
-
|
|
3385
|
+
reportReviewFailure(options, error);
|
|
3399
3386
|
}
|
|
3400
3387
|
}
|
|
3401
3388
|
function buildCoordinatorReviewDeviceOwnerCommand() {
|
|
@@ -6040,7 +6027,7 @@ function createInjectMemoryCommand() {
|
|
|
6040
6027
|
const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
|
|
6041
6028
|
console.log(pack.pack_text ?? "");
|
|
6042
6029
|
} finally {
|
|
6043
|
-
store.
|
|
6030
|
+
await store.closeAsync();
|
|
6044
6031
|
}
|
|
6045
6032
|
});
|
|
6046
6033
|
return cmd;
|
|
@@ -7207,7 +7194,7 @@ async function withStore(opts, errorCode, run) {
|
|
|
7207
7194
|
p.log.error(message);
|
|
7208
7195
|
process.exitCode = usageError ? 2 : 1;
|
|
7209
7196
|
} finally {
|
|
7210
|
-
store?.
|
|
7197
|
+
await store?.closeAsync();
|
|
7211
7198
|
}
|
|
7212
7199
|
}
|
|
7213
7200
|
function ledgerAutomaticContext(payload) {
|
|
@@ -7900,7 +7887,7 @@ async function buildLocalPack(context, project, dbPath) {
|
|
|
7900
7887
|
packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics?.pack_tokens) : 0
|
|
7901
7888
|
};
|
|
7902
7889
|
} finally {
|
|
7903
|
-
store.
|
|
7890
|
+
await store.closeAsync();
|
|
7904
7891
|
}
|
|
7905
7892
|
}
|
|
7906
7893
|
function isRecord$1(value) {
|
|
@@ -8789,6 +8776,12 @@ function readViewerPidRecord(dbPath) {
|
|
|
8789
8776
|
};
|
|
8790
8777
|
return null;
|
|
8791
8778
|
}
|
|
8779
|
+
function removeOwnedViewerPidFile(dbPath) {
|
|
8780
|
+
try {
|
|
8781
|
+
if (readViewerPidRecord(dbPath)?.pid !== process.pid) return;
|
|
8782
|
+
rmSync(pidFilePath(dbPath));
|
|
8783
|
+
} catch {}
|
|
8784
|
+
}
|
|
8792
8785
|
function normalizeViewerHost(host) {
|
|
8793
8786
|
const normalized = host.trim().toLowerCase();
|
|
8794
8787
|
if (normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]") return "loopback";
|
|
@@ -9207,6 +9200,10 @@ function createRawEventTargetState(store) {
|
|
|
9207
9200
|
stop
|
|
9208
9201
|
};
|
|
9209
9202
|
}
|
|
9203
|
+
function createOptionalOwnerRecovery(serverModule, options) {
|
|
9204
|
+
if (!("createAccountOwnerRecoveryService" in serverModule) || typeof serverModule.createAccountOwnerRecoveryService !== "function") return void 0;
|
|
9205
|
+
return serverModule.createAccountOwnerRecoveryService(options);
|
|
9206
|
+
}
|
|
9210
9207
|
function createViewerAccountLink(store, invocation, serverModule) {
|
|
9211
9208
|
const configPath = resolveCodememConfigPath(invocation.configPath ?? void 0).resolved.path;
|
|
9212
9209
|
const keysDir = dirname(resolveKeyPaths(process.env.CODEMEM_KEYS_DIR?.trim() || void 0)[0]);
|
|
@@ -9222,15 +9219,31 @@ function createViewerAccountLink(store, invocation, serverModule) {
|
|
|
9222
9219
|
keysDir
|
|
9223
9220
|
});
|
|
9224
9221
|
const registry = serverModule.createAccountLinkRegistry({ readContext });
|
|
9222
|
+
const ownerRecovery = createOptionalOwnerRecovery(serverModule, {
|
|
9223
|
+
readContext,
|
|
9224
|
+
registry,
|
|
9225
|
+
readAdminSecret: () => {
|
|
9226
|
+
const envSecret = process.env.CODEMEM_SYNC_COORDINATOR_ADMIN_SECRET?.trim();
|
|
9227
|
+
if (envSecret) return envSecret;
|
|
9228
|
+
const secret = readCodememConfigFileAtPath(configPath).sync_coordinator_admin_secret;
|
|
9229
|
+
return typeof secret === "string" ? secret.trim() : null;
|
|
9230
|
+
}
|
|
9231
|
+
});
|
|
9225
9232
|
let actualBoundPort = null;
|
|
9226
9233
|
return {
|
|
9227
9234
|
registry,
|
|
9235
|
+
ownerRecovery,
|
|
9236
|
+
close: async () => {
|
|
9237
|
+
if (ownerRecovery) await ownerRecovery.close();
|
|
9238
|
+
await registry.close();
|
|
9239
|
+
},
|
|
9228
9240
|
setBoundPort: (port) => {
|
|
9229
9241
|
actualBoundPort = port;
|
|
9230
9242
|
},
|
|
9231
9243
|
options: {
|
|
9232
9244
|
readContext,
|
|
9233
9245
|
registry,
|
|
9246
|
+
ownerRecovery,
|
|
9234
9247
|
getListenerAddress: () => actualBoundPort === null ? null : {
|
|
9235
9248
|
host: invocation.host,
|
|
9236
9249
|
port: actualBoundPort
|
|
@@ -9252,7 +9265,7 @@ function announceViewerStartup(options) {
|
|
|
9252
9265
|
}
|
|
9253
9266
|
async function startForegroundViewer(invocation) {
|
|
9254
9267
|
const serverModule = await import("@codemem/server");
|
|
9255
|
-
const { createApp, createSyncApp,
|
|
9268
|
+
const { createApp, createSyncApp, closeStoreAsync, createViewerRawEventInbox, getStore } = serverModule;
|
|
9256
9269
|
const createRawEventInbox = createViewerRawEventInbox;
|
|
9257
9270
|
const coordinatorMaintenanceDependencies = createServeCoordinatorMaintenanceDependencies(serverModule);
|
|
9258
9271
|
const { serve } = await import("@hono/node-server");
|
|
@@ -9274,7 +9287,7 @@ async function startForegroundViewer(invocation) {
|
|
|
9274
9287
|
p.log.warn("sqlite-vec failed to load; retrying viewer startup with embeddings disabled");
|
|
9275
9288
|
for (const line of sqliteVecFailureDiagnostics(err, resolveDbPath(invocation.dbPath ?? void 0))) p.log.warn(`sqlite-vec diagnostic: ${line}`);
|
|
9276
9289
|
process.env.CODEMEM_EMBEDDING_DISABLED = "1";
|
|
9277
|
-
|
|
9290
|
+
await closeStoreAsync();
|
|
9278
9291
|
store = getStore();
|
|
9279
9292
|
p.log.warn("Embeddings disabled for this viewer process; raw-event ingestion remains active.");
|
|
9280
9293
|
}
|
|
@@ -9368,51 +9381,72 @@ async function startForegroundViewer(invocation) {
|
|
|
9368
9381
|
server.on("error", (err) => {
|
|
9369
9382
|
if (err.code === "EADDRINUSE") p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
|
|
9370
9383
|
else p.log.error(err.message);
|
|
9371
|
-
|
|
9384
|
+
requestShutdown(1);
|
|
9372
9385
|
});
|
|
9373
|
-
|
|
9386
|
+
let shutdownPromise;
|
|
9387
|
+
let shutdownExitCode = 0;
|
|
9388
|
+
const shutdown = (exitCode = 0) => {
|
|
9389
|
+
shutdownExitCode = Math.max(shutdownExitCode, exitCode);
|
|
9390
|
+
shutdownPromise ??= finishShutdown();
|
|
9391
|
+
return shutdownPromise;
|
|
9392
|
+
};
|
|
9393
|
+
const finishShutdown = async () => {
|
|
9374
9394
|
p.outro("shutting down");
|
|
9375
9395
|
syncAbort.abort();
|
|
9376
|
-
await accountLink.registry.close();
|
|
9377
|
-
await stopMaintenanceWorkerProcess(maintenanceWorker, dbPath);
|
|
9378
|
-
await new Promise((resolve) => {
|
|
9379
|
-
let remaining = syncServer ? 2 : 1;
|
|
9380
|
-
const done = () => {
|
|
9381
|
-
if (--remaining === 0) resolve();
|
|
9382
|
-
};
|
|
9383
|
-
syncServer?.close(done);
|
|
9384
|
-
server.close(done);
|
|
9385
|
-
}).catch(() => {});
|
|
9386
|
-
await rawEventProcessing.stop();
|
|
9387
9396
|
try {
|
|
9388
|
-
rmSync(pidPath);
|
|
9389
|
-
} catch {}
|
|
9390
|
-
closeStore();
|
|
9391
|
-
process.exit(0);
|
|
9392
|
-
};
|
|
9393
|
-
const forceShutdown = () => {
|
|
9394
|
-
setTimeout(() => {
|
|
9395
|
-
if (maintenanceWorker?.pid && isProcessRunning(maintenanceWorker.pid)) try {
|
|
9396
|
-
process.kill(maintenanceWorker.pid, "SIGKILL");
|
|
9397
|
-
} catch {}
|
|
9398
9397
|
try {
|
|
9399
|
-
|
|
9400
|
-
|
|
9398
|
+
await accountLink.close();
|
|
9399
|
+
await stopMaintenanceWorkerProcess(maintenanceWorker, dbPath);
|
|
9400
|
+
} finally {
|
|
9401
|
+
await closeViewerListeners(syncServer ? [syncServer, server] : [server]);
|
|
9402
|
+
await rawEventProcessing.stop();
|
|
9403
|
+
}
|
|
9404
|
+
} catch (error) {
|
|
9405
|
+
shutdownExitCode = 1;
|
|
9406
|
+
logViewerShutdownError(error);
|
|
9407
|
+
} finally {
|
|
9408
|
+
removeOwnedViewerPidFile(dbPath);
|
|
9401
9409
|
try {
|
|
9402
|
-
|
|
9403
|
-
} catch {
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9410
|
+
await closeStoreAsync();
|
|
9411
|
+
} catch (error) {
|
|
9412
|
+
shutdownExitCode = 1;
|
|
9413
|
+
logViewerShutdownError(error);
|
|
9414
|
+
}
|
|
9415
|
+
}
|
|
9416
|
+
process.exit(shutdownExitCode);
|
|
9407
9417
|
};
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
shutdown()
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9418
|
+
const requestShutdown = (exitCode = 0) => {
|
|
9419
|
+
if (!shutdownPromise) forceViewerShutdown(maintenanceWorker, dbPath);
|
|
9420
|
+
shutdown(exitCode).catch((error) => {
|
|
9421
|
+
logViewerShutdownError(error);
|
|
9422
|
+
});
|
|
9423
|
+
};
|
|
9424
|
+
process.on("SIGINT", () => requestShutdown());
|
|
9425
|
+
process.on("SIGTERM", () => requestShutdown());
|
|
9426
|
+
}
|
|
9427
|
+
function logViewerShutdownError(error) {
|
|
9428
|
+
p.log.error(error instanceof Error ? error.message : String(error));
|
|
9429
|
+
}
|
|
9430
|
+
function closeViewerListeners(listeners) {
|
|
9431
|
+
return new Promise((resolve) => {
|
|
9432
|
+
let remaining = listeners.length;
|
|
9433
|
+
const done = () => {
|
|
9434
|
+
if (--remaining === 0) resolve();
|
|
9435
|
+
};
|
|
9436
|
+
for (const listener of listeners) listener.close(done);
|
|
9437
|
+
}).catch(() => {});
|
|
9438
|
+
}
|
|
9439
|
+
function forceViewerShutdown(maintenanceWorker, dbPath) {
|
|
9440
|
+
setTimeout(() => {
|
|
9441
|
+
if (maintenanceWorker?.pid && isProcessRunning(maintenanceWorker.pid)) try {
|
|
9442
|
+
process.kill(maintenanceWorker.pid, "SIGKILL");
|
|
9443
|
+
} catch {}
|
|
9444
|
+
removeOwnedViewerPidFile(dbPath);
|
|
9445
|
+
try {
|
|
9446
|
+
rmSync(maintenanceWorkerPidFilePath(dbPath));
|
|
9447
|
+
} catch {}
|
|
9448
|
+
process.exit(1);
|
|
9449
|
+
}, 3e4).unref();
|
|
9416
9450
|
}
|
|
9417
9451
|
async function runServeInvocation(invocation, dependencies = {
|
|
9418
9452
|
nativeRuntimeAllowsServeStart,
|