codemem 0.46.2 → 0.47.0-beta.2

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { ContextOnlyReplayError, DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, DeviceIdentityError, EXTRACTION_BENCHMARK_QUALITY_WEIGHTS, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, ObserverOutputError, ObserverOutputTransportError, PROMPT_TRANSPORT_PROTOCOL_RANGE, REF_BACKFILL_JOB, RawEventIngestValidationError, RawEventSweeper, RefBackfillRunner, 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, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, describePiObserverStatus, detectInstallKind, draftDistillRule, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, 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, ingestRawEvents, initDatabase, isAutomaticRecallMeasurement, isEmbeddingDisabled, isExplicitUpdateInstallEligible, isReleaseVersionForChannel, judgeDistillReport, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadSqliteVec, mdnsEnabled, mutateCodememConfigFile, normalizePromptTransportProtocolRange, observerBaseUrlForProviderOverride, planReplicationOpsAgePrune, probeCodememViewerLiveness, probeCodememViewerLiveness as probeCodememViewerLiveness$1, probeRequiredNativeRuntime, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, recordAutomaticRecall, recordPromptPackArtifacts, recordPromptPackTerminal, recordRetrievalSurface, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveComparablePath, resolveDbPath, resolveHookProject, resolvePiAgentDir, resolvePiObserverConfig, resolveProject, resolveProjectRoot, resolveRetrievalSession, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, setPeerProjectFilter, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, vacuumDatabase, viewerUrl, writeCodememConfigFile } from "@codemem/core";
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, recordRetrievalSurface, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveComparablePath, resolveDbPath, resolveHookProject, resolveKeyPaths, resolvePiAgentDir, resolvePiObserverConfig, resolveProject, resolveProjectRoot, resolveRetrievalSession, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, searchPiSessions, setPeerProjectFilter, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, upgradeCoordinatorAccountSchemaFile, upgradeCoordinatorAuthSchemaFile, vacuumDatabase, viewerUrl, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { randomInt, randomUUID } from "node:crypto";
@@ -2583,6 +2583,937 @@ whereCmd.action((opts) => {
2583
2583
  });
2584
2584
  configCommand.addCommand(whereCmd);
2585
2585
  //#endregion
2586
+ //#region src/commands/coordinator-auth-schema.ts
2587
+ function failure$4(options, message, exitCode, errorCode = "auth_schema_upgrade_failed") {
2588
+ if (options.json) {
2589
+ emitJsonError(exitCode === 2 ? "usage_error" : errorCode, message, exitCode);
2590
+ return;
2591
+ }
2592
+ p.log.error(`Coordinator auth upgrade stopped: ${message}`);
2593
+ process.exitCode = exitCode;
2594
+ }
2595
+ function upgrade(options) {
2596
+ const path = resolveDbOpt(options);
2597
+ if (options.exportSql && path) {
2598
+ failure$4(options, "Use --export-sql without --db-path; export opens no database.", 2);
2599
+ return;
2600
+ }
2601
+ if (!options.exportSql && !path) {
2602
+ failure$4(options, "Select an existing coordinator database with --db-path. Pause auth writers, drain attempts and back up the database first.", 2);
2603
+ return;
2604
+ }
2605
+ try {
2606
+ if (options.exportSql) {
2607
+ const sql = `${AUTH_LINK_AUTHORITY_CUTOVER_SQL}\n`;
2608
+ process.stdout.write(options.json ? `${JSON.stringify({
2609
+ sql,
2610
+ applied: false
2611
+ })}\n` : sql);
2612
+ return;
2613
+ }
2614
+ if (!path) return;
2615
+ upgradeCoordinatorAuthSchemaFile(path);
2616
+ process.stdout.write(options.json ? `${JSON.stringify({ upgraded: true })}\n` : "Coordinator authentication schema upgraded.\n");
2617
+ } catch (error) {
2618
+ failure$4(options, `Upgrade aborted without committing changes. Check that attempts are drained and the legacy schema and provenance match the reviewed source. ${error instanceof Error ? error.message : "Coordinator auth upgrade failed."}`, 1);
2619
+ }
2620
+ }
2621
+ function buildCoordinatorAuthSchemaCommand() {
2622
+ const command = new Command("upgrade-auth-schema").configureHelp(helpStyle).description("Upgrade local coordinator auth or export reviewed D1 SQL").option("-e, --export-sql", "print D1 cutover SQL without opening a database");
2623
+ addDbOption(command);
2624
+ addJsonOption(command);
2625
+ command.action(upgrade);
2626
+ return command;
2627
+ }
2628
+ var accountPrerequisites = "Pause auth writers, drain pending attempts and back up the selected coordinator database first. This command does not pause writers or create a backup.";
2629
+ function hasDatabaseSelection(options) {
2630
+ return options.dbPath !== void 0 || options.db !== void 0;
2631
+ }
2632
+ function exportAccountSchema(options) {
2633
+ const statements = options.deviceUnlink ? ACCOUNT_UNLINK_MIGRATION_STATEMENTS : ACCOUNT_CONNECTION_MIGRATION_STATEMENTS;
2634
+ const text = `-- ${accountPrerequisites}\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
+ process.stdout.write(options.json ? `${JSON.stringify({
2636
+ statements,
2637
+ applied: false
2638
+ })}\n` : text);
2639
+ }
2640
+ async function upgradeAccount(options) {
2641
+ if (options.deviceUnlink && !options.exportSql) {
2642
+ failure$4(options, "Use --device-unlink only with --export-sql; local upgrades detect the installed schema.", 2, "account_schema_upgrade_failed");
2643
+ return;
2644
+ }
2645
+ const path = resolveDbOpt(options);
2646
+ const selected = hasDatabaseSelection(options);
2647
+ if (options.exportSql && selected || !options.exportSql && !path) {
2648
+ 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
+ return;
2650
+ }
2651
+ try {
2652
+ if (options.exportSql) {
2653
+ exportAccountSchema(options);
2654
+ return;
2655
+ }
2656
+ if (!path) return;
2657
+ if (!options.json) process.stderr.write(`${accountPrerequisites}\n`);
2658
+ await upgradeCoordinatorAccountSchemaFile(path);
2659
+ process.stdout.write(options.json ? `${JSON.stringify({ upgraded: true })}\n` : "Coordinator account schema upgraded.\n");
2660
+ } catch (error) {
2661
+ failure$4(options, `Account schema upgrade stopped. Inspect the selected database before retrying. ${error instanceof Error ? error.message : "Coordinator account schema upgrade failed."}`, 1, "account_schema_upgrade_failed");
2662
+ }
2663
+ }
2664
+ 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`);
2666
+ addDbOption(command);
2667
+ addJsonOption(command);
2668
+ command.action(upgradeAccount);
2669
+ return command;
2670
+ }
2671
+ //#endregion
2672
+ //#region src/commands/coordinator-enroll-owner.ts
2673
+ var CAPTURE_WARNING = "Stop OpenCode capture, the Viewer and the sync daemon before preparing or resuming. Keep them stopped until Verified coordinator Identity is shown. Any capture/history change during the browser ceremony can stop local adoption after the server commits, leaving DB and config separate and requiring operator review.";
2674
+ function showPrivateBrowserLink$1(privateUrl) {
2675
+ p.log.warn("Keep this browser link private. Open it to review the Google account and device, then return here to finish. No Team or Project access is added.", { output: process.stderr });
2676
+ p.log.message(privateUrl, { output: process.stderr });
2677
+ }
2678
+ function showAccountAdmissionLink(url, request) {
2679
+ if (!request) throw new OwnerProfileEnrollmentError("review_required");
2680
+ p.log.warn("Open this account admission page while signed in to the intended account. Upload the private capability-request.json file below unchanged, compare the local fingerprint, and review the device. Keep this command running and capture stopped; browser confirmation alone does not finish enrollment. No Team or Project access is added.", { output: process.stderr });
2681
+ p.log.message(`${url}\nPrivate request file: ${request.requestFile}\nDevice: ${request.deviceId}\nLocal fingerprint: ${request.fingerprint}`, { output: process.stderr });
2682
+ }
2683
+ function admissionOptions(options) {
2684
+ if (options.accountAdmission) return {
2685
+ admissionMode: "account",
2686
+ onBrowserStart: showAccountAdmissionLink
2687
+ };
2688
+ return {
2689
+ admissionMode: "operator",
2690
+ onBrowserStart: showPrivateBrowserLink$1
2691
+ };
2692
+ }
2693
+ function reportUsageError(options, message) {
2694
+ if (options.json) emitJsonError("usage_error", message, 2);
2695
+ else {
2696
+ p.log.error(message, { output: process.stderr });
2697
+ process.exitCode = 2;
2698
+ }
2699
+ return false;
2700
+ }
2701
+ function usage(options) {
2702
+ if (options.prepare === options.resume || options.prepare && (options.capability || options.reviewDigest || options.accountAdmission) || options.accountAdmission && options.capability !== void 0 || options.resume && !options.reviewDigest || options.resume && (options.json || !process.stderr.isTTY)) return reportUsageError(options, "Use --prepare first, then review its device, current Identity, capture counts and digest. Resume interactively with --resume --review-digest; use either --capability for operator admission or --account-admission for signed-in account admission, never both.");
2703
+ if (options.capability !== void 0 && !/^[A-Za-z0-9._-]{1,512}$/.test(options.capability)) return reportUsageError(options, "Supply --capability exactly as issued, without whitespace or unsupported characters, and at most 512 characters. The prepared attempt has not been consumed.");
2704
+ if (options.reviewDigest !== void 0 && !/^[0-9a-f]{64}$/.test(options.reviewDigest)) return reportUsageError(options, "Supply --review-digest as the exact 64-character lowercase hexadecimal digest from --prepare. Enrollment has not started.");
2705
+ return environmentUsage(options);
2706
+ }
2707
+ function environmentUsage(options) {
2708
+ const overrides = [
2709
+ "CODEMEM_SYNC_COORDINATOR_URL",
2710
+ "CODEMEM_SYNC_COORDINATOR_GROUP",
2711
+ "CODEMEM_SYNC_COORDINATOR_GROUPS",
2712
+ "CODEMEM_WORKSPACE_ID",
2713
+ "CODEMEM_ACTOR_ID",
2714
+ "CODEMEM_DEVICE_ID",
2715
+ "CODEMEM_ACTOR_DISPLAY_NAME"
2716
+ ].filter((name) => process.env[name] !== void 0);
2717
+ if (overrides.length) return reportUsageError(options, `Unset ${overrides.join(" and ")} before owner enrollment. Use --coordinator and the group argument, or the selected config; environment overrides cannot be adopted safely.`);
2718
+ if (process.env.CODEMEM_SYNC_KEY_STORE?.trim().toLowerCase() === "keychain") return reportUsageError(options, "CODEMEM_SYNC_KEY_STORE=keychain is not supported for owner enrollment. Keep signing keys unchanged; select a supported existing file-key profile.");
2719
+ return true;
2720
+ }
2721
+ function coordinatorOriginUsage(options, value) {
2722
+ if (typeof value !== "string") return reportUsageError(options, "Set sync_coordinator_url to a valid HTTPS origin string before owner enrollment.");
2723
+ try {
2724
+ const url = new URL(value);
2725
+ if (value.trim() === value && url.protocol === "https:" && !url.username && !url.password && (value === url.origin || value === `${url.origin}/`)) return true;
2726
+ } catch {}
2727
+ return reportUsageError(options, "Set --coordinator or sync_coordinator_url to an HTTPS origin without credentials, paths, query strings, fragments, or surrounding whitespace.");
2728
+ }
2729
+ function groupUsage(options, groupId) {
2730
+ if (groupId.length > 0 && groupId.length <= 256 && groupId.trim() === groupId && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(groupId)) return true;
2731
+ return reportUsageError(options, "Use a nonempty group ID of at most 256 characters, without surrounding whitespace or control characters.");
2732
+ }
2733
+ function pathEnvironmentUsage(options, selected) {
2734
+ for (const [name, path, flag] of [
2735
+ [
2736
+ "CODEMEM_CONFIG",
2737
+ selected.config,
2738
+ "--config"
2739
+ ],
2740
+ [
2741
+ "CODEMEM_DB",
2742
+ selected.db,
2743
+ "--db-path"
2744
+ ],
2745
+ [
2746
+ "CODEMEM_KEYS_DIR",
2747
+ selected.keys,
2748
+ "--keys-dir"
2749
+ ]
2750
+ ]) {
2751
+ const selectedPath = normalizeEnrollmentPath(name, path);
2752
+ const override = normalizeEnrollmentPath(name, name === "CODEMEM_KEYS_DIR" ? process.env[name]?.trim() || void 0 : process.env[name]);
2753
+ if (selectedPath !== void 0 && override !== void 0 && resolve(selectedPath) !== resolve(override)) return reportUsageError(options, `Unset ${name} or make it match ${flag} before owner enrollment. Conflicting profile paths cannot be adopted safely.`);
2754
+ }
2755
+ return true;
2756
+ }
2757
+ function normalizeEnrollmentPath(name, path) {
2758
+ if (path === void 0) return void 0;
2759
+ if (name === "CODEMEM_KEYS_DIR") return path;
2760
+ const value = name === "CODEMEM_CONFIG" ? path.trim() : path;
2761
+ return value ? expandUserPath(value) : void 0;
2762
+ }
2763
+ function resolveEnrollmentProfile(options) {
2764
+ const configPath = resolveCodememConfigPath(options.config, "write").resolved.path;
2765
+ const config = readCodememConfigFileAtPath(configPath);
2766
+ if (config.sync_coordinator_url !== void 0 && !coordinatorOriginUsage(options, config.sync_coordinator_url)) return null;
2767
+ const coordinatorUrl = options.coordinator ?? readCoordinatorSyncConfig(config).syncCoordinatorUrl;
2768
+ if (!coordinatorUrl) {
2769
+ reportUsageError(options, "Set --coordinator to your coordinator HTTPS URL, or set sync_coordinator_url in the selected config.");
2770
+ return null;
2771
+ }
2772
+ if (!coordinatorOriginUsage(options, coordinatorUrl)) return null;
2773
+ const dbPath = resolveDbPath(resolveDbOpt(options));
2774
+ const keysDir = options.keysDir ?? (process.env.CODEMEM_KEYS_DIR?.trim() || void 0) ?? dirname(resolveKeyPaths()[0]);
2775
+ if (!pathEnvironmentUsage(options, {
2776
+ config: configPath,
2777
+ db: dbPath,
2778
+ keys: keysDir
2779
+ })) return null;
2780
+ return {
2781
+ configPath,
2782
+ coordinatorUrl,
2783
+ dbPath,
2784
+ keysDir
2785
+ };
2786
+ }
2787
+ async function enroll(groupId, options) {
2788
+ if (!usage(options)) return;
2789
+ if (!groupUsage(options, groupId)) return;
2790
+ if (!pathEnvironmentUsage(options, {
2791
+ config: options.config,
2792
+ db: resolveDbOpt(options),
2793
+ keys: options.keysDir
2794
+ })) return;
2795
+ const controller = new AbortController();
2796
+ const stop = () => controller.abort();
2797
+ process.once("SIGINT", stop);
2798
+ process.once("SIGTERM", stop);
2799
+ try {
2800
+ if (!options.json) p.log.warn(CAPTURE_WARNING, { output: process.stderr });
2801
+ const profile = resolveEnrollmentProfile(options);
2802
+ if (!profile) return;
2803
+ showResult(await enrollOwnerProfile({
2804
+ ...profile,
2805
+ groupId,
2806
+ stateDir: options.stateDir ?? join(dirname(profile.dbPath), "owner-enrollment"),
2807
+ mode: options.prepare ? "prepare" : "resume",
2808
+ ...admissionOptions(options),
2809
+ capability: options.capability,
2810
+ reviewedDigest: options.reviewDigest,
2811
+ signal: controller.signal
2812
+ }), options);
2813
+ } catch (error) {
2814
+ const safe = error instanceof OwnerProfileEnrollmentError ? new OwnerProfileEnrollmentError(error.code) : new OwnerProfileEnrollmentError("review_required");
2815
+ const message = safe.message;
2816
+ if (options.json) emitJsonError(safe.code, message);
2817
+ else {
2818
+ p.log.error(message, { output: process.stderr });
2819
+ process.exitCode = 1;
2820
+ }
2821
+ } finally {
2822
+ process.removeListener("SIGINT", stop);
2823
+ process.removeListener("SIGTERM", stop);
2824
+ }
2825
+ }
2826
+ function showResult(result, options) {
2827
+ if (options.json) {
2828
+ process.stdout.write(`${JSON.stringify({
2829
+ ...result,
2830
+ warning: CAPTURE_WARNING
2831
+ })}\n`);
2832
+ return;
2833
+ }
2834
+ if (result.kind === "prepared") {
2835
+ p.log.message(`Current Identity: ${result.review.actorId}\nDevice: ${result.review.deviceId}\nPublic key: ${result.review.publicKey}\nCapture: ${result.review.captureId ?? "none"} (${result.review.captureAssociation})\nOwn local memories: ${result.review.localMemoryCount}\nForeign rows retained without new access: ${result.review.retainedForeignMemoryCount}\nReview digest: ${result.reviewDigest}`, { output: process.stderr });
2836
+ p.log.message(`Private request file for operator admission or upload during --resume --account-admission: ${result.requestFile}`, { output: process.stderr });
2837
+ p.log.warn("Only the current Identity/device assignment will change. Signing keys, memory authorship and history stay unchanged; no membership is inferred.", { output: process.stderr });
2838
+ return;
2839
+ }
2840
+ p.log.success(`Verified coordinator Identity: ${result.identityId}\nOwner binding: ${result.bindingId}`, { output: process.stderr });
2841
+ }
2842
+ function buildCoordinatorEnrollOwnerCommand() {
2843
+ const command = new Command("enroll-owner").configureHelp(helpStyle).description("Review and enroll this existing profile's device owner").argument("<group>", "group id").option("-u, --coordinator <url>", "coordinator HTTPS origin (defaults to config)").option("--keys-dir <path>", "existing signing-key directory").option("-s, --state-dir <path>", "private retained-request directory").option("--prepare", "save a reviewed request for operator admission").option("--resume", "resume the saved request or config-only adoption").option("--capability <token>", "admin-issued capability for this exact start").option("--account-admission", "use signed-in account admission with --resume only").option("-v, --review-digest <digest>", "explicitly approve the prepared profile review");
2844
+ addDbOption(command);
2845
+ addConfigOption(command);
2846
+ addJsonOption(command);
2847
+ command.action(enroll);
2848
+ return command;
2849
+ }
2850
+ //#endregion
2851
+ //#region src/commands/coordinator-grant-identity-group.ts
2852
+ function failure$3(options, message, exitCode = 1) {
2853
+ if (options.json) {
2854
+ emitJsonError("identity_group_grant_unavailable", message, exitCode);
2855
+ return;
2856
+ }
2857
+ p.log.error(message, { output: process.stderr });
2858
+ process.exitCode = exitCode;
2859
+ }
2860
+ function grantErrorMessage(error) {
2861
+ if (error instanceof RemoteCoordinatorRequestError) {
2862
+ if (error.status === 401 || error.status === 403) return "Coordinator refused the configured admin credential. Ask the operator to check it; do not pass credentials on the command line.";
2863
+ if (error.code === "invalid_grant_input") return "Coordinator rejected the reference. Use the stored controller attestation reference returned by device-owner review.";
2864
+ if (error.code === "grant_authority_unavailable") return "The stored review cannot authorize this grant. Ask the operator to check current enrollment, review revocation, and group status; do not edit the database.";
2865
+ if (error.status === 429 || error.code === "rate_limited") return "Coordinator rate limit reached. Wait before previewing and confirming again.";
2866
+ if (error.status === 404) return "This coordinator does not support the grant endpoint. Ask the operator to check its deployment.";
2867
+ }
2868
+ return "Transport grant could not be confirmed. Its outcome may be unknown; check coordinator setup and the stored review before retrying. No local profile was changed.";
2869
+ }
2870
+ function configuredTarget(options) {
2871
+ const config = options.config ? readCodememConfigFileAtPath(options.config) : readCodememConfigFile();
2872
+ const sync = readCoordinatorSyncConfig(config);
2873
+ return {
2874
+ remoteUrl: normalizeCoordinatorAccountLinkOrigin(sync.syncCoordinatorUrl),
2875
+ adminSecret: sync.syncCoordinatorAdminSecret,
2876
+ timeoutS: Math.max(1, sync.syncCoordinatorTimeoutS)
2877
+ };
2878
+ }
2879
+ async function confirmGrant(request, options) {
2880
+ const approved = await p.confirm({
2881
+ message: "Issue the reviewed Identity's transport enrollment grant?",
2882
+ initialValue: false,
2883
+ output: process.stderr
2884
+ });
2885
+ if (p.isCancel(approved) || approved !== true) {
2886
+ p.log.info("Nothing changed.", { output: process.stderr });
2887
+ return;
2888
+ }
2889
+ const current = configuredTarget(options);
2890
+ if (current.remoteUrl !== request.remoteUrl || current.adminSecret !== request.adminSecret) return failure$3(options, "Coordinator configuration changed. Nothing changed; preview again.");
2891
+ const result = await coordinatorGrantIdentityGroupAction(request);
2892
+ p.log.success(`Transport enrollment grant ${result.kind}: coordinator ${result.grant.coordinator_id}; Identity ${result.grant.identity_id}; group ${result.grant.group_id}. No Team/Project access or local profile changes.`, { output: process.stderr });
2893
+ }
2894
+ async function grantIdentityGroup(attestationId, options) {
2895
+ try {
2896
+ if (!isCoordinatorControllerAttestationReference(attestationId)) return failure$3(options, "Use a stored controller-v1: reference followed by exactly 64 lowercase hexadecimal characters.", 2);
2897
+ const target = configuredTarget(options);
2898
+ if (!target.remoteUrl) return failure$3(options, "Configure a coordinator HTTPS origin without credentials, path, query, or fragment (HTTP only on literal loopback). Nothing changed.", 2);
2899
+ if (!target.adminSecret) return failure$3(options, "Ask the coordinator operator to configure the existing admin credential. Nothing changed.");
2900
+ const preview = {
2901
+ state: "preview",
2902
+ coordinator_url: target.remoteUrl,
2903
+ attestation_id: attestationId,
2904
+ unchanged: true,
2905
+ eligibility_checked: false,
2906
+ message: "Explicit transport enrollment grant only, not Team/Project access. The server derives Identity and group from the stored review. No Google flow or local profile changes. Run in a terminal without --json to confirm."
2907
+ };
2908
+ if (options.json) {
2909
+ process.stdout.write(`${JSON.stringify(preview)}\n`);
2910
+ return;
2911
+ }
2912
+ p.log.info(`Coordinator: ${target.remoteUrl}\nController attestation: ${attestationId}\n${preview.message}\nThis is a local request preview, not a check of server eligibility.`, { output: process.stderr });
2913
+ if (!process.stdin.isTTY || !process.stderr.isTTY) return;
2914
+ await confirmGrant({
2915
+ ...target,
2916
+ remoteUrl: target.remoteUrl,
2917
+ attestationId
2918
+ }, options);
2919
+ } catch (error) {
2920
+ failure$3(options, grantErrorMessage(error));
2921
+ }
2922
+ }
2923
+ function buildCoordinatorGrantIdentityGroupCommand() {
2924
+ const command = new Command("grant-identity-group").configureHelp(helpStyle).description("Preview and confirm an explicit transport enrollment grant").argument("<attestation-id>", "stored controller attestation reference");
2925
+ addConfigOption(command);
2926
+ addDbOption(command);
2927
+ addJsonOption(command);
2928
+ command.action(grantIdentityGroup);
2929
+ return command;
2930
+ }
2931
+ //#endregion
2932
+ //#region src/commands/coordinator-link-account.ts
2933
+ function reportLinkFailure(error, coordinatorOrigin) {
2934
+ const message = error instanceof CoordinatorAccountLinkError ? error.message : "Account linking could not finish. Check your existing device setup and coordinator configuration, then try again.";
2935
+ p.log.error(message, { output: process.stderr });
2936
+ if (!(error instanceof CoordinatorAccountLinkError)) return;
2937
+ if (error.code !== "link_conflict") return;
2938
+ let guidance = "Replacing or removing coordinator account links is not supported. ";
2939
+ if (coordinatorOrigin) {
2940
+ const signInUrl = new URL("/auth/sign-in", coordinatorOrigin).href;
2941
+ guidance += `If this Google account already belongs to the intended coordinator Identity, sign in at ${signInUrl}. Otherwise, ask your coordinator operator.`;
2942
+ } else guidance += "Ask your coordinator operator.";
2943
+ p.log.message(guidance, { output: process.stderr });
2944
+ }
2945
+ function showPrivateBrowserLink(privateUrl) {
2946
+ p.log.warn("Keep this browser link private. Open it to review account linking, then return to the coordinator tab to finish.", { output: process.stderr });
2947
+ p.log.message(privateUrl, { output: process.stderr });
2948
+ }
2949
+ function requireInteractive(options) {
2950
+ if (options.json) {
2951
+ emitJsonError("interactive_flow_only", "Account linking is interactive. Run without --json and keep the browser link private.", 2);
2952
+ return false;
2953
+ }
2954
+ if (!process.stderr.isTTY) {
2955
+ p.log.error("Account linking requires a terminal. Run this command interactively and keep the browser link private.", { output: process.stderr });
2956
+ process.exitCode = 2;
2957
+ return false;
2958
+ }
2959
+ return true;
2960
+ }
2961
+ function readLinkConfiguration(options) {
2962
+ const config = options.config ? readCodememConfigFileAtPath(options.config) : readCodememConfigFile();
2963
+ const coordinatorUrl = normalizeCoordinatorAccountLinkOrigin(options.coordinator ?? readCoordinatorSyncConfig(config).syncCoordinatorUrl);
2964
+ if (!coordinatorUrl || options.loopbackHost !== "127.0.0.1" && options.loopbackHost !== "::1") return null;
2965
+ return {
2966
+ config,
2967
+ coordinatorUrl,
2968
+ loopbackHost: options.loopbackHost
2969
+ };
2970
+ }
2971
+ function readLinkProfile(options, selected) {
2972
+ try {
2973
+ const dbPath = resolveDbPath(resolveDbOpt(options));
2974
+ const before = statSync(dbPath, { bigint: true });
2975
+ const device = readAccountLinkDevice(dbPath);
2976
+ const after = statSync(dbPath, { bigint: true });
2977
+ const override = process.env.CODEMEM_DEVICE_ID?.trim() || void 0;
2978
+ if (!before.isFile() || before.dev !== after.dev || before.ino !== after.ino || override !== void 0 && override !== device.deviceId) throw new CoordinatorAccountLinkError("device_unavailable");
2979
+ let actor = selected.config.actor_id;
2980
+ if (Object.hasOwn(process.env, "CODEMEM_ACTOR_ID")) actor = process.env.CODEMEM_ACTOR_ID;
2981
+ const identityId = (typeof actor === "string" ? actor.trim() : "") || `local:${device.deviceId}`;
2982
+ return {
2983
+ dbPath,
2984
+ keysDir: process.env.CODEMEM_KEYS_DIR?.trim() || void 0,
2985
+ coordinatorUrl: selected.coordinatorUrl,
2986
+ expectedIdentityId: identityId,
2987
+ deviceId: device.deviceId,
2988
+ fingerprint: device.fingerprint,
2989
+ fileIdentity: {
2990
+ dev: after.dev.toString(),
2991
+ ino: after.ino.toString()
2992
+ }
2993
+ };
2994
+ } catch {
2995
+ throw new CoordinatorAccountLinkError("device_unavailable");
2996
+ }
2997
+ }
2998
+ function profileMatches(options, snapshot) {
2999
+ const selected = readLinkConfiguration(options);
3000
+ return selected !== null && JSON.stringify(readLinkProfile(options, selected)) === snapshot;
3001
+ }
3002
+ function eligibleStatus(remote, identityId) {
3003
+ if (remote.kind !== "status") {
3004
+ if (remote.kind === "unavailable" && remote.code === "coordinator_auth_schema_upgrade_required") throw new CoordinatorAccountLinkError("schema_upgrade_required");
3005
+ throw new CoordinatorAccountLinkError("invalid_response");
3006
+ }
3007
+ const status = remote.status;
3008
+ if (status.state === "conflict" || status.state === "revoked") throw new CoordinatorAccountLinkError("link_conflict");
3009
+ if (status.state === "review_required") throw new CoordinatorAccountLinkError("review_required");
3010
+ if (status.identityId !== identityId) throw new CoordinatorAccountLinkError("identity_mismatch");
3011
+ return status;
3012
+ }
3013
+ async function linkAccount(group, options) {
3014
+ if (!requireInteractive(options)) return;
3015
+ const controller = new AbortController();
3016
+ const stop = () => controller.abort();
3017
+ process.once("SIGINT", stop);
3018
+ process.once("SIGTERM", stop);
3019
+ let coordinatorOrigin = null;
3020
+ try {
3021
+ const selected = readLinkConfiguration(options);
3022
+ if (!selected) {
3023
+ p.log.error("Set --coordinator to your coordinator HTTPS origin and use --loopback-host 127.0.0.1 or ::1.", { output: process.stderr });
3024
+ process.exitCode = 2;
3025
+ return;
3026
+ }
3027
+ const { coordinatorUrl } = selected;
3028
+ coordinatorOrigin = coordinatorUrl;
3029
+ const profile = readLinkProfile(options, selected);
3030
+ const status = eligibleStatus(await readCoordinatorAccountLinkStatus({
3031
+ ...profile,
3032
+ coordinatorUrl,
3033
+ identityId: profile.expectedIdentityId,
3034
+ groupId: group,
3035
+ signal: controller.signal
3036
+ }), profile.expectedIdentityId);
3037
+ if (status.state === "linked") {
3038
+ p.log.success("This Identity is already linked. No account-link request was started.", { output: process.stderr });
3039
+ return;
3040
+ }
3041
+ const snapshot = JSON.stringify(profile);
3042
+ const result = await linkCoordinatorAccount({
3043
+ ...profile,
3044
+ groupId: group,
3045
+ coordinatorUrl,
3046
+ expectedAuthority: status.authority,
3047
+ expectedCoordinatorId: status.coordinatorId,
3048
+ assertCurrentContext: () => profileMatches(options, snapshot),
3049
+ loopbackHost: selected.loopbackHost,
3050
+ signal: controller.signal,
3051
+ onBrowserStart: showPrivateBrowserLink
3052
+ });
3053
+ p.log.success(`Coordinator account linked to coordinator Identity ${result.identityId}. Your local Identity has not changed.`, { output: process.stderr });
3054
+ } catch (error) {
3055
+ reportLinkFailure(error, coordinatorOrigin);
3056
+ process.exitCode = 1;
3057
+ } finally {
3058
+ process.removeListener("SIGINT", stop);
3059
+ process.removeListener("SIGTERM", stop);
3060
+ }
3061
+ }
3062
+ function buildCoordinatorLinkAccountCommand() {
3063
+ const command = new Command("link-account").configureHelp(helpStyle).description("Link a coordinator account to your existing Identity").argument("<group>", "group id").option("-u, --coordinator <url>", "coordinator HTTPS origin (defaults to saved config)").option("-l, --loopback-host <host>", "literal loopback host", "127.0.0.1");
3064
+ addDbOption(command);
3065
+ addConfigOption(command);
3066
+ addJsonOption(command);
3067
+ command.action(linkAccount);
3068
+ return command;
3069
+ }
3070
+ //#endregion
3071
+ //#region src/commands/coordinator-qualify-owner.ts
3072
+ var isAttestation = (value) => typeof value === "string" && /^controller-v1:[a-f0-9]{64}$/u.test(value);
3073
+ var isHash$1 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
3074
+ function isRef(value) {
3075
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
3076
+ }
3077
+ function setup$1(options) {
3078
+ const config = options.config ? readCodememConfigFileAtPath(options.config) : readCodememConfigFile();
3079
+ const sync = readCoordinatorSyncConfig(config);
3080
+ const remoteUrl = normalizeCoordinatorAccountLinkOrigin(sync.syncCoordinatorUrl);
3081
+ if (!remoteUrl || !sync.syncCoordinatorAdminSecret) throw new Error("configuration_unavailable");
3082
+ return {
3083
+ remoteUrl,
3084
+ adminSecret: sync.syncCoordinatorAdminSecret,
3085
+ timeoutS: Math.max(1, sync.syncCoordinatorTimeoutS)
3086
+ };
3087
+ }
3088
+ function parsePreview$1(reply, attestationId) {
3089
+ if (reply.state !== "ready" || reply.attestation_id !== attestationId || !isHash$1(reply.evidence_digest) || !isHash$1(reply.fingerprint) || ![
3090
+ "coordinator_id",
3091
+ "review_receipt_id",
3092
+ "link_id",
3093
+ "claim_id",
3094
+ "identity_id",
3095
+ "group_id",
3096
+ "device_id"
3097
+ ].every((key) => isRef(reply[key]))) throw new Error("invalid_preview");
3098
+ return {
3099
+ state: "ready",
3100
+ attestation_id: attestationId,
3101
+ evidence_digest: reply.evidence_digest,
3102
+ coordinator_id: reply.coordinator_id,
3103
+ review_receipt_id: reply.review_receipt_id,
3104
+ link_id: reply.link_id,
3105
+ claim_id: reply.claim_id,
3106
+ identity_id: reply.identity_id,
3107
+ group_id: reply.group_id,
3108
+ device_id: reply.device_id,
3109
+ fingerprint: reply.fingerprint
3110
+ };
3111
+ }
3112
+ var QUALIFICATION_DENIAL_CODES = [
3113
+ "invalid_input",
3114
+ "preview_changed",
3115
+ "qualification_conflict",
3116
+ "unavailable"
3117
+ ];
3118
+ function failure$2(options, error, message, exitCode = 1) {
3119
+ if (options.json) return emitJsonError(error, message, exitCode);
3120
+ p.log.error(message, { output: process.stderr });
3121
+ process.exitCode = exitCode;
3122
+ }
3123
+ function sameConfiguration$1(options, request) {
3124
+ try {
3125
+ const current = setup$1(options);
3126
+ return current.remoteUrl === request.remoteUrl && current.adminSecret === request.adminSecret;
3127
+ } catch {
3128
+ return false;
3129
+ }
3130
+ }
3131
+ async function confirmQualification(options, request, preview, stage) {
3132
+ const approved = await p.confirm({
3133
+ message: "Allow this linked account to enroll new devices under this Identity?",
3134
+ initialValue: false,
3135
+ output: process.stderr
3136
+ });
3137
+ if (p.isCancel(approved) || !approved) {
3138
+ p.log.info("Nothing changed.", { output: process.stderr });
3139
+ return;
3140
+ }
3141
+ if (!sameConfiguration$1(options, request)) return failure$2(options, "qualification_configuration_changed", "Operator configuration changed. Preview again before confirming.");
3142
+ stage.onDispatch();
3143
+ const result = await coordinatorQualifyOwnerAction({
3144
+ ...request,
3145
+ confirmEvidenceDigest: preview.evidence_digest
3146
+ });
3147
+ if (result.state !== "created" && result.state !== "existing" || !isRef(result.qualification_id) || result.attestation_id !== request.attestationId || result.coordinator_id !== preview.coordinator_id || result.evidence_digest !== preview.evidence_digest) throw new Error("invalid_commit_response");
3148
+ p.log.success("Linked account qualified for device enrollment. Team and Project access are unchanged.", { output: process.stderr });
3149
+ }
3150
+ async function qualifyOwner(attestationId, options) {
3151
+ let writeAttempted = false;
3152
+ try {
3153
+ if (!isAttestation(attestationId)) return failure$2(options, "qualification_invalid_input", "Use the exact controller-v1 reference from a real device-owner review.", 2);
3154
+ const request = {
3155
+ ...setup$1(options),
3156
+ attestationId
3157
+ };
3158
+ const preview = parsePreview$1(await coordinatorQualifyOwnerAction(request), attestationId);
3159
+ if (options.json) {
3160
+ process.stdout.write(`${JSON.stringify({
3161
+ preview,
3162
+ network_request: true,
3163
+ unchanged: true
3164
+ })}\n`);
3165
+ return;
3166
+ }
3167
+ p.log.info(`Network preview from ${preview.coordinator_id}\nIdentity: ${preview.identity_id}\nLinked account reference: ${preview.link_id}\nDevice: ${preview.device_id}; fingerprint: ${preview.fingerprint}\nReview: ${preview.attestation_id}\nReceipt: ${preview.review_receipt_id}\nClaim: ${preview.claim_id}; group: ${preview.group_id}`, { output: process.stderr });
3168
+ p.log.warn("Independently verify the real operator review. Qualification permits new devices under this Identity; an independently trusted transport-group grant is still separate. This does not create a Team, issue a grant, or rewrite a device claim.", { output: process.stderr });
3169
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
3170
+ p.log.info("Preview only; no qualification issued. Use a terminal without --json to confirm.", { output: process.stderr });
3171
+ return;
3172
+ }
3173
+ await confirmQualification(options, request, preview, { onDispatch: () => {
3174
+ writeAttempted = true;
3175
+ } });
3176
+ } catch (error) {
3177
+ if (error instanceof RemoteCoordinatorRequestError && error.status === 409 && QUALIFICATION_DENIAL_CODES.includes(error.code)) return failure$2(options, "qualification_rejected", "Qualification denied: evidence is invalid, changed, was revoked, conflicts, or is unavailable. Preview again; no automatic retry.");
3178
+ let message = "Qualification server unavailable or preview invalid. Ask the coordinator operator to check the configured Worker's browser authentication and owner-enrollment settings, required migrations 0035 and 0036, and the saved trusted HTTPS coordinator origin and admin credential.";
3179
+ if (writeAttempted) message = "Qualification outcome is unknown. It may have committed; do not assume rollback or retry automatically. Ask the coordinator operator to inspect the outcome.";
3180
+ failure$2(options, writeAttempted ? "qualification_outcome_unknown" : "qualification_unavailable", message);
3181
+ }
3182
+ }
3183
+ function buildCoordinatorQualifyOwnerCommand() {
3184
+ const command = new Command("qualify-owner").configureHelp(helpStyle).description("Preview and confirm linked-account device enrollment permission").argument("<attestation-id>", "controller-v1 reference from a real operator review");
3185
+ addConfigOption(command);
3186
+ addJsonOption(command);
3187
+ command.action(qualifyOwner);
3188
+ return command;
3189
+ }
3190
+ //#endregion
3191
+ //#region src/commands/coordinator-review-device-owner.ts
3192
+ var REVIEW_REASONS = [
3193
+ "group_unavailable",
3194
+ "enrollment_unavailable",
3195
+ "key_mismatch",
3196
+ "enrollment_identity_mismatch",
3197
+ "invite_identity_mismatch",
3198
+ "invite_evidence_invalid"
3199
+ ];
3200
+ function validId$1(value) {
3201
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
3202
+ }
3203
+ function isHash(value) {
3204
+ return typeof value === "string" && value.length === 64 && /^[a-f0-9]{64}$/.test(value);
3205
+ }
3206
+ function reviewErrorMessage(error) {
3207
+ if (error instanceof RemoteCoordinatorRequestError) {
3208
+ if (error.code === "review_stale") return "Evidence changed since the preview. Nothing changed; run the review again.";
3209
+ if (error.code === "already_reviewed_or_needs_review") return "This device already has a reviewed owner, or needs a separate review. Nothing changed.";
3210
+ if (error.code === "needs_review") return "Coordinator ownership evidence needs review. Nothing changed; ask the coordinator operator.";
3211
+ }
3212
+ return "Device owner review could not finish. Check the existing device, admin credential, and coordinator setup; preview again before retrying.";
3213
+ }
3214
+ function failure$1(options, message, exitCode = 1) {
3215
+ if (options.json) {
3216
+ emitJsonError("owner_review_unavailable", message, exitCode);
3217
+ return;
3218
+ }
3219
+ p.log.error(message, { output: process.stderr });
3220
+ process.exitCode = exitCode;
3221
+ }
3222
+ function localOptions(options) {
3223
+ const config = options.config ? readCodememConfigFileAtPath(options.config) : readCodememConfigFile();
3224
+ const sync = readCoordinatorSyncConfig(config);
3225
+ let actorId;
3226
+ let identitySource = "config";
3227
+ if (Object.hasOwn(process.env, "CODEMEM_ACTOR_ID")) {
3228
+ actorId = process.env.CODEMEM_ACTOR_ID?.trim();
3229
+ identitySource = "env";
3230
+ } else if (typeof config.actor_id === "string") actorId = config.actor_id.trim();
3231
+ return {
3232
+ local: {
3233
+ dbPath: resolveDbPath(resolveDbOpt(options)),
3234
+ actorId,
3235
+ identitySource,
3236
+ deviceIdOverride: process.env.CODEMEM_DEVICE_ID?.trim()
3237
+ },
3238
+ remoteUrl: normalizeCoordinatorAccountLinkOrigin(options.coordinator ?? sync.syncCoordinatorUrl),
3239
+ adminSecret: sync.syncCoordinatorAdminSecret,
3240
+ timeoutS: Math.max(1, sync.syncCoordinatorTimeoutS)
3241
+ };
3242
+ }
3243
+ function displayLocal(evidence) {
3244
+ const device = evidence.device;
3245
+ return {
3246
+ ...evidence,
3247
+ device: device ? {
3248
+ deviceId: device.deviceId,
3249
+ fingerprint: device.fingerprint,
3250
+ label: device.label
3251
+ } : null
3252
+ };
3253
+ }
3254
+ function showLocal(evidence) {
3255
+ const count = (value) => value === null ? "unknown" : String(value);
3256
+ const device = evidence.device;
3257
+ const identity = evidence.identity;
3258
+ p.log.message([
3259
+ `Device: ${device?.label ?? device?.deviceId ?? "unavailable"}`,
3260
+ `Device ID: ${device?.deviceId ?? "unknown"}; fingerprint: ${device?.fingerprint ?? "unknown"}`,
3261
+ `Identity: ${identity?.label ?? identity?.identityId ?? "unavailable"} (${identity?.source ?? "unknown"})`,
3262
+ `Identity ID: ${identity?.identityId ?? "unknown"}`,
3263
+ `Memory authors: current ${count(evidence.memoryCounts.current)}, other ${count(evidence.memoryCounts.others)}, unknown ${count(evidence.memoryCounts.unknown)} — unchanged`,
3264
+ `Active Teams: ${count(evidence.teamCount)}; direct Projects: ${count(evidence.projectCount)} — unchanged`,
3265
+ "This review does not move memories or change access. Google account linking is a separate step."
3266
+ ].join("\n"), { output: process.stderr });
3267
+ }
3268
+ function validInviteCount(reply, invites) {
3269
+ return invites.length <= 10 && Number.isSafeInteger(reply.reviewed_invite_count) && reply.reviewed_invite_count >= invites.length;
3270
+ }
3271
+ function validInviteRef(invite) {
3272
+ return !!invite && validId$1(invite.invite_id) && (invite.kind === "team_member" || invite.kind === "add_device");
3273
+ }
3274
+ function parsePreview(payload, request) {
3275
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid_preview");
3276
+ const reply = payload;
3277
+ const enrollment = reply.enrollment;
3278
+ const invites = reply.reviewed_invites;
3279
+ 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");
3280
+ if (enrollment !== null && (!enrollment || enrollment.device_id !== request.deviceId || !isHash(enrollment.fingerprint) || !["none", "matches"].includes(enrollment.identity_label))) throw new Error("invalid_preview");
3281
+ if (reply.state === "ready" && (!enrollment || enrollment.device_id !== request.deviceId || enrollment.fingerprint !== request.fingerprint || !["none", "matches"].includes(enrollment.identity_label) || !isHash(reply.evidence_digest) || reply.reasons.length)) throw new Error("invalid_preview");
3282
+ return {
3283
+ state: reply.state,
3284
+ coordinator_id: reply.coordinator_id,
3285
+ reasons: reply.reasons,
3286
+ 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,
3293
+ reviewed_invites: invites.map((invite) => ({
3294
+ invite_id: invite.invite_id,
3295
+ kind: invite.kind
3296
+ })),
3297
+ reviewed_invite_count: reply.reviewed_invite_count
3298
+ };
3299
+ }
3300
+ function sameLocalOwner(before, after) {
3301
+ return after.state === "ready" && before.ownershipRecords.actorPresent === after.ownershipRecords.actorPresent && before.ownershipRecords.deviceAssignmentPresent === after.ownershipRecords.deviceAssignmentPresent && before.device?.deviceId === after.device?.deviceId && before.device?.publicKey === after.device?.publicKey && before.device?.fingerprint === after.device?.fingerprint && before.identity?.identityId === after.identity?.identityId && before.identity?.source === after.identity?.source;
3302
+ }
3303
+ async function confirmReview(group, options, evidence, request, preview) {
3304
+ if (!process.stdin.isTTY || !process.stderr.isTTY) return failure$1(options, "Preview only. Run in a terminal without --json to confirm this review.", 2);
3305
+ const approved = await p.confirm({
3306
+ message: "Review this device owner?",
3307
+ initialValue: false,
3308
+ output: process.stderr
3309
+ });
3310
+ if (p.isCancel(approved) || !approved) {
3311
+ p.log.info("Nothing changed.", { output: process.stderr });
3312
+ return;
3313
+ }
3314
+ const current = localOptions(options);
3315
+ if (current.remoteUrl !== request.remoteUrl || !sameLocalOwner(evidence, readCoordinatorOwnerReviewLocalEvidence(current.local))) return failure$1(options, "Your device or Identity changed. Nothing changed on the coordinator; preview again.");
3316
+ const result = await coordinatorAuthControllerReviewAction({
3317
+ ...request,
3318
+ confirmEvidenceDigest: preview.evidence_digest
3319
+ });
3320
+ if (result.state !== "created" && result.state !== "existing" || typeof result.attestation_id !== "string" || result.attestation_id.length !== 78 || !/^controller-v1:[a-f0-9]{64}$/.test(result.attestation_id) || result.coordinator_id !== preview.coordinator_id || result.group_id !== group || result.device_id !== request.deviceId || result.identity_id !== request.identityId || result.fingerprint !== request.fingerprint || typeof result.created_at !== "string") throw new Error("invalid_commit_response");
3321
+ p.log.success("Device owner reviewed. Memories and access are unchanged.", { output: process.stderr });
3322
+ p.log.info(`Controller attestation reference: ${result.attestation_id}\nKeep this noncredential reference for a separate, explicit operator-issued transport group grant. This review does not issue that grant or grant Team/Project access.`, { output: process.stderr });
3323
+ if (showTargetedLinkFollowUp(options)) return;
3324
+ showLinkFollowUp(group);
3325
+ }
3326
+ function showLinkFollowUp(group) {
3327
+ if (group.includes("\\")) {
3328
+ p.log.info(`Next, run codemem coordinator link-account with group ID ${JSON.stringify(group)}. Quote the ID for your shell.`, { output: process.stderr });
3329
+ return;
3330
+ }
3331
+ const quotedGroup = `'${group.replaceAll("'", "'\\''")}'`;
3332
+ p.log.info(`Next, link your Google account separately: codemem coordinator link-account ${quotedGroup}`, { output: process.stderr });
3333
+ }
3334
+ function showTargetedLinkFollowUp(options) {
3335
+ if (!options.coordinator && !options.config && !resolveDbOpt(options)) return false;
3336
+ p.log.info("Next, link your Google account separately with codemem coordinator link-account for this group. Reuse the same --coordinator, --config, and --db-path options from this review.", { output: process.stderr });
3337
+ return true;
3338
+ }
3339
+ function showLocalStop(evidence, options) {
3340
+ if (options.json) {
3341
+ process.stdout.write(`${JSON.stringify({
3342
+ ...displayLocal(evidence),
3343
+ unchanged: true,
3344
+ error: "needs_review",
3345
+ message: "Local device ownership needs review. Nothing changed."
3346
+ })}\n`);
3347
+ return;
3348
+ }
3349
+ p.log.warn(`Review stopped: ${evidence.reasons.join(", ")}. Nothing changed.`, { output: process.stderr });
3350
+ }
3351
+ function emitReviewPreview(evidence, preview) {
3352
+ let stopped = {};
3353
+ if (preview.state !== "ready") {
3354
+ stopped = {
3355
+ error: "needs_review",
3356
+ message: "Coordinator device ownership needs review. Nothing changed."
3357
+ };
3358
+ process.exitCode = 1;
3359
+ }
3360
+ process.stdout.write(`${JSON.stringify({
3361
+ local: displayLocal(evidence),
3362
+ review: preview,
3363
+ unchanged: true,
3364
+ ...stopped
3365
+ })}\n`);
3366
+ }
3367
+ async function reviewDeviceOwner(group, options) {
3368
+ try {
3369
+ if (!validId$1(group)) return failure$1(options, "Use a valid coordinator group ID.", 2);
3370
+ const setup = localOptions(options);
3371
+ const evidence = readCoordinatorOwnerReviewLocalEvidence(setup.local);
3372
+ if (!options.json) showLocal(evidence);
3373
+ if (evidence.state !== "ready" || !evidence.device || !evidence.identity) {
3374
+ showLocalStop(evidence, options);
3375
+ process.exitCode = 1;
3376
+ return;
3377
+ }
3378
+ if (!setup.adminSecret) return failure$1(options, "Ask the coordinator operator to configure the existing admin credential. Nothing changed.");
3379
+ if (!setup.remoteUrl) return failure$1(options, "Set --coordinator to a coordinator HTTPS origin (HTTP is allowed only on literal loopback).", 2);
3380
+ const request = {
3381
+ remoteUrl: setup.remoteUrl,
3382
+ adminSecret: setup.adminSecret,
3383
+ timeoutS: setup.timeoutS,
3384
+ groupId: group,
3385
+ deviceId: evidence.device.deviceId,
3386
+ identityId: evidence.identity.identityId,
3387
+ fingerprint: evidence.device.fingerprint
3388
+ };
3389
+ const preview = parsePreview(await coordinatorAuthControllerReviewAction(request), request);
3390
+ if (options.json) {
3391
+ emitReviewPreview(evidence, preview);
3392
+ return;
3393
+ }
3394
+ if (preview.state !== "ready") return failure$1(options, `Coordinator review stopped: ${preview.reasons.join(", ")}. Nothing changed.`);
3395
+ p.log.info(`Coordinator: ${preview.coordinator_id}; reviewed invitations: ${preview.reviewed_invite_count}`, { output: process.stderr });
3396
+ await confirmReview(group, options, evidence, request, preview);
3397
+ } catch (error) {
3398
+ failure$1(options, reviewErrorMessage(error));
3399
+ }
3400
+ }
3401
+ function buildCoordinatorReviewDeviceOwnerCommand() {
3402
+ const command = new Command("review-device-owner").configureHelp(helpStyle).description("Preview and confirm the owner of your existing device").argument("<group>", "group id").option("-u, --coordinator <url>", "coordinator HTTPS origin (defaults to saved config)");
3403
+ addDbOption(command);
3404
+ addConfigOption(command);
3405
+ addJsonOption(command);
3406
+ command.action(reviewDeviceOwner);
3407
+ return command;
3408
+ }
3409
+ //#endregion
3410
+ //#region src/commands/coordinator-revoke-device.ts
3411
+ function validId(value) {
3412
+ return value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
3413
+ }
3414
+ function setup(options) {
3415
+ const config = options.config ? readCodememConfigFileAtPath(options.config) : readCodememConfigFile();
3416
+ const remoteUrl = normalizeCoordinatorAccountLinkOrigin(config.sync_coordinator_url);
3417
+ const adminSecret = config.sync_coordinator_admin_secret;
3418
+ const timeoutS = config.sync_coordinator_timeout_s ?? 3;
3419
+ if (!remoteUrl || typeof adminSecret !== "string" || !adminSecret.trim() || /[\r\n\0]/u.test(adminSecret) || typeof timeoutS !== "number" || !Number.isFinite(timeoutS) || timeoutS < 1 || timeoutS > 3600) throw new Error("revocation_configuration_unavailable");
3420
+ return {
3421
+ remoteUrl,
3422
+ adminSecret,
3423
+ timeoutS
3424
+ };
3425
+ }
3426
+ function sameConfiguration(options, request) {
3427
+ try {
3428
+ const current = setup(options);
3429
+ return current.remoteUrl === request.remoteUrl && current.adminSecret === request.adminSecret && current.timeoutS === request.timeoutS;
3430
+ } catch {
3431
+ return false;
3432
+ }
3433
+ }
3434
+ function failure(options, code, message, exitCode = 1) {
3435
+ if (options.json) {
3436
+ emitJsonError(code, message, exitCode);
3437
+ return;
3438
+ }
3439
+ p.log.error(message, { output: process.stderr });
3440
+ process.exitCode = exitCode;
3441
+ }
3442
+ var IMPACT = "Across the whole coordinator database: revoke this device ID with ANY key and this canonical key under ANY device ID, in ALL groups. The selected group supplies evidence only. Another device continues only when BOTH its device ID and key differ. No immediate offline enforcement, manual/direct-peer trust revocation, or erasure of remote copies. Account links, sessions, grants, memberships and stored memories are not rewritten.";
3443
+ function receiptLabel(value) {
3444
+ return JSON.stringify(value).replace(/[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/gu, (character) => `\\u{${(character.codePointAt(0) ?? 0).toString(16)}}`);
3445
+ }
3446
+ function receiptJson(result) {
3447
+ return JSON.stringify(result).replace(/[\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/gu, (character) => character.split("").map((unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, "0")}`).join(""));
3448
+ }
3449
+ async function confirmRevocation(options, request, preview, onDispatch) {
3450
+ const approved = await p.confirm({
3451
+ message: "Revoke BOTH subjects across the whole coordinator database?",
3452
+ initialValue: false,
3453
+ output: process.stderr
3454
+ });
3455
+ if (p.isCancel(approved) || approved !== true) {
3456
+ p.log.info("No revocation requested.", { output: process.stderr });
3457
+ return;
3458
+ }
3459
+ if (!sameConfiguration(options, request)) return failure(options, "revocation_configuration_changed", "Operator configuration changed or is unavailable. No revocation requested; preview again.");
3460
+ const result = await coordinatorRevokeDeviceAction({
3461
+ ...request,
3462
+ expectedDigest: preview.expected_digest,
3463
+ expectedContext: {
3464
+ coordinatorId: preview.coordinator_id,
3465
+ keyId: preview.key_id
3466
+ },
3467
+ onDispatch
3468
+ });
3469
+ if (result.state !== "revoked") throw new Error("revocation_response_invalid");
3470
+ process.stdout.write(`${receiptJson(result)}\n`);
3471
+ p.log.success(`Coordinator ${result.coordinator_id}: both whole-database subjects revoked.`, { output: process.stderr });
3472
+ for (const row of result.revocations) p.log.info(`${row.subject_kind}: ${row.subject_value}; retained reference: ${receiptLabel(row.revocation_id)}; created: ${receiptLabel(row.created_at)}`, { output: process.stderr });
3473
+ }
3474
+ async function revokeDevice(groupId, deviceId, options) {
3475
+ let writeDispatched = false;
3476
+ try {
3477
+ if (!validId(groupId) || !validId(deviceId)) return failure(options, "revocation_invalid_input", "Use exact group and device IDs without whitespace or control characters (maximum 256 characters).", 2);
3478
+ const request = {
3479
+ ...setup(options),
3480
+ groupId,
3481
+ deviceId
3482
+ };
3483
+ const preview = await coordinatorRevokeDeviceAction(request);
3484
+ if (preview.state !== "ready") throw new Error("revocation_response_invalid");
3485
+ if (options.json) {
3486
+ process.stdout.write(`${JSON.stringify({
3487
+ preview,
3488
+ network_request: true,
3489
+ unchanged: true,
3490
+ message: IMPACT
3491
+ })}\n`);
3492
+ return;
3493
+ }
3494
+ p.log.info(`Coordinator: ${preview.coordinator_id}\nEvidence group: ${preview.group_id}\nGlobal device ID: ${preview.device_id}\nCanonical key ID: ${preview.key_id}\nScope: ${preview.scope}`, { output: process.stderr });
3495
+ p.log.warn(IMPACT, { output: process.stderr });
3496
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
3497
+ p.log.info("Network preview only; no revocation requested. Use a terminal without --json to confirm.", { output: process.stderr });
3498
+ return;
3499
+ }
3500
+ await confirmRevocation(options, request, preview, () => {
3501
+ writeDispatched = true;
3502
+ });
3503
+ } catch (error) {
3504
+ if (error instanceof RemoteCoordinatorRequestError && error.status === 409 && (error.code === "revocation_preview_stale" || error.code === "revocation_target_unavailable")) return failure(options, error.code, "Revocation rejected: the target is unavailable or preview evidence changed. Preview again; do not retry automatically.");
3505
+ if (writeDispatched) return failure(options, "revocation_outcome_unknown", "Revocation outcome is unknown. It may have committed; do not assume rollback or retry automatically. Ask the coordinator operator to inspect both retained subjects.");
3506
+ failure(options, "revocation_unavailable", "Revocation preview unavailable or invalid. Check the saved HTTPS origin (HTTP only on literal loopback), operator credential and operator-enabled server context. The CLI does not activate the server. No revocation requested.");
3507
+ }
3508
+ }
3509
+ function buildCoordinatorRevokeDeviceCommand() {
3510
+ const command = new Command("revoke-device").configureHelp(helpStyle).description("Preview and confirm whole-database device ID and key revocation").argument("<group>", "group supplying current enrollment evidence only").argument("<device>", "device ID to revoke across all groups");
3511
+ addConfigOption(command);
3512
+ addJsonOption(command);
3513
+ command.action(revokeDevice);
3514
+ return command;
3515
+ }
3516
+ //#endregion
2586
3517
  //#region src/commands/coordinator.ts
2587
3518
  /**
2588
3519
  * Coordinator CLI commands — manage coordinator invites, join requests, and relay server.
@@ -2616,7 +3547,7 @@ function parseOptionalInteger(value, name) {
2616
3547
  }
2617
3548
  function formatImportInviteError(error) {
2618
3549
  const message = error instanceof Error ? error.message : String(error);
2619
- const recipientInviteViewerGuidance = "Accept Team/add-device recipient invitations through the Viewer: run `codemem serve`, review the access details, and confirm the invitation there.";
3550
+ const recipientInviteReviewGuidance = "Review this invitation with `codemem coordinator import-invite <invite> --review`, then accept with --reviewed-onboarding-digest <digest> using the same profile and device name. You can also review and accept through the Viewer with `codemem serve`.";
2620
3551
  switch (message) {
2621
3552
  case "invite_already_bound": return "This invite was already used. Ask the sender for a new one.";
2622
3553
  case "invite_expired": return "This invite has expired. Ask the sender for a new one.";
@@ -2631,16 +3562,66 @@ function formatImportInviteError(error) {
2631
3562
  case "invite_identity_conflict":
2632
3563
  case "recipient_invite_intent_mismatch":
2633
3564
  case "recipient_invite_review_unavailable":
2634
- case "reviewed_onboarding_digest_required": return recipientInviteViewerGuidance;
3565
+ case "reviewed_onboarding_digest_required":
3566
+ case "reviewed_onboarding_stale": return recipientInviteReviewGuidance;
3567
+ case "invite_review_profile_required": return "Invite review needs an initialized device and matching keys. Run `codemem sync pair --payload-only` with the intended --db-path, --config, and CODEMEM_KEYS_DIR first, then review that profile with --keys-dir.";
3568
+ case "recipient_invite_review_required": return "Only Team/add-device recipient invitations need --review. Import this legacy invitation without --review.";
3569
+ case "invite_inspection_transport_failed": return "Could not inspect the invitation at its coordinator. Check network access and use the original coordinator URL; redirects are not accepted.";
3570
+ case "invite_coordinator_conflict": return "This profile already uses another coordinator. Use the intended device profile and review again.";
2635
3571
  default: return message;
2636
3572
  }
2637
3573
  }
3574
+ function inviteImportProfile(invite, opts) {
3575
+ return {
3576
+ inviteValue: invite,
3577
+ dbPath: resolveDbOpt(opts) ?? null,
3578
+ keysDir: opts.keysDir ?? null,
3579
+ configPath: opts.config ?? null,
3580
+ recipientDisplayName: opts.recipientName?.trim() || null,
3581
+ deviceDisplayName: opts.deviceName?.trim() || null
3582
+ };
3583
+ }
3584
+ async function acceptInviteWithProfileCheck(profile, reviewedOnboardingDigest) {
3585
+ const digest = reviewedOnboardingDigest?.trim() || null;
3586
+ if (digest) {
3587
+ if ((await coordinatorReviewInviteAction(profile)).reviewed_onboarding_digest !== digest) throw new Error("reviewed_onboarding_stale");
3588
+ }
3589
+ return coordinatorImportInviteAction({
3590
+ ...profile,
3591
+ reviewedOnboardingDigest: digest
3592
+ });
3593
+ }
3594
+ function rejectConflictingInviteReviewOptions(opts) {
3595
+ if (!opts.review || opts.reviewedOnboardingDigest === void 0) return false;
3596
+ const message = "Use --review or --reviewed-onboarding-digest, not both.";
3597
+ if (opts.json) emitJsonError("usage_error", message);
3598
+ else p.log.error(message);
3599
+ process.exitCode = 2;
3600
+ return true;
3601
+ }
3602
+ function printInviteReview(review, opts) {
3603
+ console.log(JSON.stringify(review, null, 2));
3604
+ if (opts.json) return;
3605
+ p.outro("Review only: no invitation accepted or local profile changed. To accept, rerun with --reviewed-onboarding-digest <digest> and the same profile and device name. A Team recipient display name is cosmetic and may be changed before acceptance.");
3606
+ }
2638
3607
  /**
2639
3608
  * Build a fresh coordinator command tree. Each call returns independent
2640
3609
  * Commander instances so the tree can be mounted under multiple parents.
2641
3610
  */
2642
- function buildCoordinatorCommand() {
3611
+ function createCoordinatorCommandRoot() {
2643
3612
  const cmd = new Command("coordinator").configureHelp(helpStyle).description("Manage coordinator invites, join requests, and relay server");
3613
+ cmd.addCommand(buildCoordinatorLinkAccountCommand());
3614
+ cmd.addCommand(buildCoordinatorEnrollOwnerCommand());
3615
+ cmd.addCommand(buildCoordinatorReviewDeviceOwnerCommand());
3616
+ cmd.addCommand(buildCoordinatorQualifyOwnerCommand());
3617
+ cmd.addCommand(buildCoordinatorGrantIdentityGroupCommand());
3618
+ cmd.addCommand(buildCoordinatorRevokeDeviceCommand());
3619
+ cmd.addCommand(buildCoordinatorAuthSchemaCommand());
3620
+ cmd.addCommand(buildCoordinatorAccountSchemaCommand());
3621
+ return cmd;
3622
+ }
3623
+ function buildCoordinatorCommand() {
3624
+ const cmd = createCoordinatorCommandRoot();
2644
3625
  const groupCreateCmd = new Command("group-create").configureHelp(helpStyle).description("Create a coordinator group in the local store").argument("<group>", "group id").option("--name <name>", "display name override");
2645
3626
  addDbOption(groupCreateCmd);
2646
3627
  addJsonOption(groupCreateCmd);
@@ -3265,20 +4246,19 @@ function buildCoordinatorCommand() {
3265
4246
  }
3266
4247
  });
3267
4248
  cmd.addCommand(createInviteCmd);
3268
- const importInviteCmd = new Command("import-invite").configureHelp(helpStyle).description("Import a coordinator invite").argument("<invite>", "invite value or link").option("-R, --recipient-name <name>", "recipient Identity display name").option("-N, --device-name <name>", "device display name").option("--keys-dir <path>", "keys directory");
4249
+ const importInviteCmd = new Command("import-invite").configureHelp(helpStyle).description("Import a coordinator invite").argument("<invite>", "invite value or link").option("-R, --recipient-name <name>", "recipient Identity display name").option("-N, --device-name <name>", "device display name").option("--review", "Review recipient access without accepting").option("--reviewed-onboarding-digest <digest>", "Accept the reviewed recipient access").option("--keys-dir <path>", "keys directory");
3269
4250
  addDbOption(importInviteCmd);
3270
4251
  addConfigOption(importInviteCmd);
3271
4252
  addJsonOption(importInviteCmd);
3272
4253
  importInviteCmd.action(async (invite, opts) => {
3273
4254
  try {
3274
- const result = await coordinatorImportInviteAction({
3275
- inviteValue: invite,
3276
- dbPath: resolveDbOpt(opts) ?? null,
3277
- keysDir: opts.keysDir ?? null,
3278
- configPath: opts.config ?? null,
3279
- recipientDisplayName: opts.recipientName?.trim() || null,
3280
- deviceDisplayName: opts.deviceName?.trim() || null
3281
- });
4255
+ if (rejectConflictingInviteReviewOptions(opts)) return;
4256
+ const profile = inviteImportProfile(invite, opts);
4257
+ if (opts.review) {
4258
+ printInviteReview(await coordinatorReviewInviteAction(profile), opts);
4259
+ return;
4260
+ }
4261
+ const result = await acceptInviteWithProfileCheck(profile, opts.reviewedOnboardingDigest);
3282
4262
  if (opts.json) {
3283
4263
  console.log(JSON.stringify(result, null, 2));
3284
4264
  return;
@@ -4250,11 +5230,11 @@ async function distillAction(opts) {
4250
5230
  store?.close();
4251
5231
  }
4252
5232
  }
4253
- var cmd$3 = new Command("distill").configureHelp(helpStyle).description("Mine recurring memories into reviewable context candidates").option("-p, --project <project>", "project identifier (defaults to git repo root)").option("-A, --all-projects", "mine memories across all projects").option("-k, --kind <kind>", "memory kind to mine (repeat or comma-separate)", collectKind, []).option("-m, --min-recurrence <n>", "minimum member count per candidate", "2").option("-l, --limit <n>", "max candidates", "10").option("-e, --explain", "include evidence snippets in human output").option("--include-documented", "include candidates already represented in context files").option("--no-judge", "skip the observer-model worthiness judgment that drops routine-activity clusters").option("-D, --draft", "draft an AGENTS.md rule for the top candidate and show the diff").option("--apply", "write the drafted rule to the target file (implies --draft; prompts to confirm; with --json applies immediately without prompting)");
4254
- addDbOption(cmd$3);
4255
- addJsonOption(cmd$3);
4256
- cmd$3.action(distillAction);
4257
- var distillCommand = cmd$3;
5233
+ var cmd$5 = new Command("distill").configureHelp(helpStyle).description("Mine recurring memories into reviewable context candidates").option("-p, --project <project>", "project identifier (defaults to git repo root)").option("-A, --all-projects", "mine memories across all projects").option("-k, --kind <kind>", "memory kind to mine (repeat or comma-separate)", collectKind, []).option("-m, --min-recurrence <n>", "minimum member count per candidate", "2").option("-l, --limit <n>", "max candidates", "10").option("-e, --explain", "include evidence snippets in human output").option("--include-documented", "include candidates already represented in context files").option("--no-judge", "skip the observer-model worthiness judgment that drops routine-activity clusters").option("-D, --draft", "draft an AGENTS.md rule for the top candidate and show the diff").option("--apply", "write the drafted rule to the target file (implies --draft; prompts to confirm; with --json applies immediately without prompting)");
5234
+ addDbOption(cmd$5);
5235
+ addJsonOption(cmd$5);
5236
+ cmd$5.action(distillAction);
5237
+ var distillCommand = cmd$5;
4258
5238
  //#endregion
4259
5239
  //#region src/commands/embed.ts
4260
5240
  function parseOptionalPositiveInt(value) {
@@ -4353,12 +5333,12 @@ function invokedAsTopLevelAlias(name, argv = process.argv.slice(2)) {
4353
5333
  }
4354
5334
  //#endregion
4355
5335
  //#region src/commands/export-memories.ts
4356
- function expandUserPath(value) {
5336
+ function expandUserPath$1(value) {
4357
5337
  return value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
4358
5338
  }
4359
- var cmd$2 = new Command("export-memories").configureHelp(helpStyle).description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--project <project>", "filter by project (defaults to git repo root)").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp");
4360
- addDbOption(cmd$2);
4361
- cmd$2.action((output, opts) => {
5339
+ var cmd$4 = new Command("export-memories").configureHelp(helpStyle).description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--project <project>", "filter by project (defaults to git repo root)").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp");
5340
+ addDbOption(cmd$4);
5341
+ cmd$4.action((output, opts) => {
4362
5342
  if (output !== "-" && invokedAsTopLevelAlias("export-memories")) emitDeprecationWarning("codemem export-memories", "codemem memory export");
4363
5343
  const payload = exportMemories({
4364
5344
  dbPath: resolveDbPath(resolveDbOpt(opts)),
@@ -4372,7 +5352,7 @@ cmd$2.action((output, opts) => {
4372
5352
  process.stdout.write(text);
4373
5353
  return;
4374
5354
  }
4375
- const outputPath = expandUserPath(output);
5355
+ const outputPath = expandUserPath$1(output);
4376
5356
  writeFileSync(outputPath, text, "utf8");
4377
5357
  p.intro("codemem export-memories");
4378
5358
  p.log.success([
@@ -4384,13 +5364,13 @@ cmd$2.action((output, opts) => {
4384
5364
  ].join("\n"));
4385
5365
  p.outro("done");
4386
5366
  });
4387
- var exportMemoriesCommand = cmd$2;
5367
+ var exportMemoriesCommand = cmd$4;
4388
5368
  //#endregion
4389
5369
  //#region src/commands/import-memories.ts
4390
- var cmd$1 = new Command("import-memories").configureHelp(helpStyle).description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing");
4391
- addDbOption(cmd$1);
4392
- addJsonOption(cmd$1);
4393
- cmd$1.action((inputFile, opts) => {
5370
+ var cmd$3 = new Command("import-memories").configureHelp(helpStyle).description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing");
5371
+ addDbOption(cmd$3);
5372
+ addJsonOption(cmd$3);
5373
+ cmd$3.action((inputFile, opts) => {
4394
5374
  if (!opts.json && invokedAsTopLevelAlias("import-memories")) emitDeprecationWarning("codemem import-memories", "codemem memory import");
4395
5375
  let payload;
4396
5376
  try {
@@ -4451,7 +5431,7 @@ cmd$1.action((inputFile, opts) => {
4451
5431
  ].join("\n"));
4452
5432
  p.outro("done");
4453
5433
  });
4454
- var importMemoriesCommand = cmd$1;
5434
+ var importMemoriesCommand = cmd$3;
4455
5435
  //#endregion
4456
5436
  //#region src/maintenance-worker-runtime.ts
4457
5437
  var defaultLogger = {
@@ -4957,7 +5937,7 @@ function forgetMemoryAction(idStr, opts) {
4957
5937
  }
4958
5938
  const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
4959
5939
  try {
4960
- if (!store.get(memoryId)) {
5940
+ if (!store.forgetForUser(memoryId)) {
4961
5941
  if (opts.json) emitJsonError("not_found", `Memory ${memoryId} not found`);
4962
5942
  else {
4963
5943
  p.log.error(`Memory ${memoryId} not found`);
@@ -4965,7 +5945,6 @@ function forgetMemoryAction(idStr, opts) {
4965
5945
  }
4966
5946
  return;
4967
5947
  }
4968
- store.forget(memoryId);
4969
5948
  if (opts.json) console.log(JSON.stringify({
4970
5949
  id: memoryId,
4971
5950
  status: "forgotten"
@@ -4980,39 +5959,35 @@ function parseConfidence(value) {
4980
5959
  if (!Number.isFinite(n)) return .5;
4981
5960
  return Math.min(1, Math.max(0, n));
4982
5961
  }
4983
- function rollbackManualMemory(store, sessionId, memoryId) {
4984
- store.db.transaction(() => {
4985
- const row = store.db.prepare("SELECT import_key FROM memory_items WHERE id = ?").get(memoryId);
4986
- store.db.prepare("DELETE FROM memory_vectors WHERE memory_id = ?").run(memoryId);
4987
- store.db.prepare("DELETE FROM memory_file_refs WHERE memory_id = ?").run(memoryId);
4988
- store.db.prepare("DELETE FROM memory_concept_refs WHERE memory_id = ?").run(memoryId);
4989
- store.db.prepare("DELETE FROM replication_ops WHERE entity_type = 'memory_item' AND (entity_id = ? OR entity_id = ?)").run(row?.import_key ?? "", String(memoryId));
4990
- store.db.prepare("DELETE FROM memory_items WHERE id = ?").run(memoryId);
4991
- store.db.prepare(`DELETE FROM sessions
4992
- WHERE id = ?
4993
- AND NOT EXISTS (SELECT 1 FROM memory_items WHERE session_id = ?)`).run(sessionId, sessionId);
4994
- })();
4995
- }
4996
- async function rememberMemoryAction(opts) {
4997
- const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
4998
- let sessionId = null;
4999
- try {
5000
- const project = resolveProject(process.cwd(), opts.project ?? null);
5001
- sessionId = store.startSession({
5962
+ function createManualMemory(store, opts) {
5963
+ const project = resolveProject(process.cwd(), opts.project ?? null);
5964
+ const createdMemories = [];
5965
+ return store.db.transaction(() => {
5966
+ const newSessionId = store.startSession({
5002
5967
  cwd: process.cwd(),
5003
5968
  project,
5004
5969
  user: process.env.USER ?? "unknown",
5005
5970
  toolVersion: "manual",
5006
5971
  metadata: { manual: true }
5007
5972
  });
5008
- const memId = store.remember(sessionId, opts.kind, opts.title, opts.body, parseConfidence(opts.confidence), opts.tags);
5009
- if (!store.get(memId)) {
5010
- await store.flushPendingVectorWrites();
5011
- rollbackManualMemory(store, sessionId, memId);
5012
- sessionId = null;
5013
- throw new Error("unauthorized_scope");
5014
- }
5015
- store.endSession(sessionId, { manual: true });
5973
+ const memId = store.rememberForUser(newSessionId, opts.kind, opts.title, opts.body, parseConfidence(opts.confidence), opts.tags, void 0, { createdMemories });
5974
+ if (!store.get(memId)) throw new Error("unauthorized_scope");
5975
+ store.endSession(newSessionId, { manual: true });
5976
+ return {
5977
+ memId,
5978
+ sessionId: newSessionId,
5979
+ createdMemories
5980
+ };
5981
+ }).immediate();
5982
+ }
5983
+ async function rememberMemoryAction(opts) {
5984
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
5985
+ let sessionId = null;
5986
+ try {
5987
+ const result = createManualMemory(store, opts);
5988
+ sessionId = result.sessionId;
5989
+ const memId = result.memId;
5990
+ store.enqueueCommittedVectorWrites(result.createdMemories);
5016
5991
  await store.flushPendingVectorWrites();
5017
5992
  if (opts.json) console.log(JSON.stringify({ id: memId }));
5018
5993
  else p.log.success(`Stored memory ${memId}`);
@@ -7172,6 +8147,218 @@ var piHookInjectCommand = piHookInjectCmd.action(async (opts) => {
7172
8147
  }
7173
8148
  });
7174
8149
  //#endregion
8150
+ //#region src/commands/pi-import-sessions.ts
8151
+ /**
8152
+ * codemem pi-import-sessions — backfill pi agent session history (design D3/D4).
8153
+ *
8154
+ * Thin wrapper over core importPiSessions: walks every session JSONL file under
8155
+ * <pi-agent-dir>/sessions (project subdirs included, PI_CODING_AGENT_DIR honored).
8156
+ *
8157
+ * Extraction is opt-in (--extract, off by default): it runs the observer model over
8158
+ * the imported history, which costs tokens proportional to backlog size. With
8159
+ * --extract the STANDARD flush path (flushRawEvents — the same function the
8160
+ * raw-event sweeper calls each tick) runs over the imported pi sessions; there is
8161
+ * no bespoke extraction pipeline. Without it, imported events are stored searchable
8162
+ * and the running viewer's sweeper picks them up like any other backlog.
8163
+ */
8164
+ var EXTRACT_COST_NOTE = "also extract memories (runs the observer model over imported history — LLM cost scales with backlog; default: off, imported events stay searchable)";
8165
+ /**
8166
+ * Import sessions, then — when --extract is set — drain the imported pi
8167
+ * sessions through the standard flush path. Returns the import summary plus
8168
+ * extraction counts and failures.
8169
+ */
8170
+ async function runPiImportSessions(opts, deps = {}) {
8171
+ const dbPath = resolveDbPath(resolveDbOpt(opts));
8172
+ return {
8173
+ summary: importPiSessions({
8174
+ dbPath,
8175
+ onProgress: deps.onProgress
8176
+ }),
8177
+ extraction: opts.extract ? await flushImportedPiSessions(dbPath, deps.observer) : {
8178
+ requested: false,
8179
+ flushedEvents: 0,
8180
+ failedSessions: 0,
8181
+ pendingSessions: null,
8182
+ error: null
8183
+ }
8184
+ };
8185
+ }
8186
+ /**
8187
+ * Run the existing raw-event flush (the sweeper's own flushRawEvents) over
8188
+ * every pending source-"pi" session — never a bespoke extraction path. Fails
8189
+ * open per session: a failed flush batch stays retryable by the sweeper's
8190
+ * pending-queue phase, exactly like live-session flush failures.
8191
+ */
8192
+ async function flushImportedPiSessions(dbPath, observer) {
8193
+ const store = new MemoryStore(dbPath);
8194
+ const extraction = {
8195
+ requested: true,
8196
+ flushedEvents: 0,
8197
+ failedSessions: 0,
8198
+ pendingSessions: 0,
8199
+ error: null
8200
+ };
8201
+ try {
8202
+ let resolved;
8203
+ try {
8204
+ resolved = observer ?? new ObserverClient();
8205
+ } catch (err) {
8206
+ extraction.error = `observer init failed: ${err instanceof Error ? err.message : String(err)}`;
8207
+ extraction.pendingSessions = piPendingSessions(store).length;
8208
+ return extraction;
8209
+ }
8210
+ for (const streamId of piPendingSessions(store)) try {
8211
+ const result = await flushRawEvents(store, { observer: resolved }, {
8212
+ opencodeSessionId: streamId,
8213
+ source: "pi",
8214
+ cwd: null,
8215
+ project: null,
8216
+ startedAt: null,
8217
+ maxEvents: null
8218
+ });
8219
+ extraction.flushedEvents += result.flushed;
8220
+ } catch (err) {
8221
+ extraction.failedSessions++;
8222
+ extraction.error ??= `session ${streamId}: ${err instanceof Error ? err.message : String(err)}`;
8223
+ }
8224
+ extraction.pendingSessions = piPendingSessions(store).length;
8225
+ return extraction;
8226
+ } finally {
8227
+ store.close();
8228
+ }
8229
+ }
8230
+ /** Pending-flush sessions attributed to source "pi", without a page cap. */
8231
+ function piPendingSessions(store) {
8232
+ return store.rawEventSessionsPendingFlush(null, "pi").map((session) => session.streamId);
8233
+ }
8234
+ /** Human-readable summary line; per-file progress is streamed by the caller. */
8235
+ function formatPiImportHuman(result) {
8236
+ const { summary, extraction } = result;
8237
+ const lines = [`Scanned ${summary.filesScanned} files: ${summary.filesImported} imported, ${summary.filesUnchanged} unchanged, ${summary.filesEmpty} empty, ${summary.filesErrored} errored — ${summary.inserted} events inserted, ${summary.skipped} skipped.`];
8238
+ if (extraction.requested) {
8239
+ lines.push(`Extraction: observer flushed ${extraction.flushedEvents} events into memories.`);
8240
+ if (extraction.failedSessions > 0 || (extraction.pendingSessions ?? 0) > 0) lines.push(`${extraction.failedSessions} sessions failed; ${extraction.pendingSessions} remain pending.`);
8241
+ if (extraction.error) lines.push(`Extraction error: ${extraction.error}`);
8242
+ }
8243
+ return lines.join("\n");
8244
+ }
8245
+ function logProgress(progress) {
8246
+ const name = progress.file;
8247
+ if (progress.status === "imported") p.log.info(`imported ${name} (+${progress.inserted} events)`);
8248
+ else if (progress.status === "unchanged") p.log.info(`unchanged ${name}`);
8249
+ else if (progress.status === "empty") p.log.info(`empty ${name}`);
8250
+ else p.log.warn(`error ${name}: ${progress.error ?? "unknown error"}`);
8251
+ }
8252
+ var cmd$2 = new Command("pi-import-sessions").configureHelp(helpStyle).description("Import pi agent session history into the searchable store").option("--extract", EXTRACT_COST_NOTE);
8253
+ addDbOption(cmd$2);
8254
+ addJsonOption(cmd$2);
8255
+ var piImportSessionsCommand = cmd$2.action(async (opts) => {
8256
+ try {
8257
+ const result = await runPiImportSessions(opts, opts.json ? {} : { onProgress: logProgress });
8258
+ const { extraction } = result;
8259
+ let extractionFailure;
8260
+ if (extraction.error || extraction.failedSessions > 0 || (extraction.pendingSessions ?? 0) > 0) {
8261
+ extractionFailure = extraction.error ?? `${extraction.failedSessions} sessions failed; ${extraction.pendingSessions} remain pending.`;
8262
+ process.exitCode = 1;
8263
+ }
8264
+ if (opts.json) {
8265
+ console.log(JSON.stringify({
8266
+ ...result.summary,
8267
+ extraction,
8268
+ error: extractionFailure ? "pi_extraction_incomplete" : void 0,
8269
+ message: extractionFailure
8270
+ }, null, 2));
8271
+ return;
8272
+ }
8273
+ p.log.message(formatPiImportHuman(result));
8274
+ } catch (error) {
8275
+ const message = error instanceof Error ? error.message : "pi-import-sessions failed";
8276
+ if (opts.json) emitJsonError("pi_import_sessions_failed", message);
8277
+ else {
8278
+ p.log.error(message);
8279
+ process.exitCode = 1;
8280
+ }
8281
+ }
8282
+ });
8283
+ //#endregion
8284
+ //#region src/commands/pi-session-search.ts
8285
+ /**
8286
+ * codemem pi-session-search — search stored pi session conversations.
8287
+ *
8288
+ * Queries the raw-event store (source "pi") via core searchPiSessions — the
8289
+ * same contract as GET /api/pi/sessions/search and the pi-extension
8290
+ * memory_session_search tool (design D5). --json prints the exact response
8291
+ * object those surfaces return, which is what the extension's CLI fallback
8292
+ * parses.
8293
+ *
8294
+ * Empty results usually mean the index only covers sessions captured since
8295
+ * codemem was installed; `codemem pi-import-sessions` backfills history.
8296
+ */
8297
+ var EMPTY_INDEX_HINT = "The index may only cover sessions captured since codemem was installed; backfill history with: codemem pi-import-sessions";
8298
+ function intOption(value) {
8299
+ const parsed = Number.parseInt(String(value ?? ""), 10);
8300
+ return Number.isFinite(parsed) ? parsed : void 0;
8301
+ }
8302
+ /**
8303
+ * Run the session search and return the shared response object.
8304
+ * Throws on invalid input or store failure; printing stays with the caller.
8305
+ */
8306
+ function runPiSessionSearch(query, opts) {
8307
+ if (!query.trim()) throw new Error("query required");
8308
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
8309
+ try {
8310
+ return searchPiSessions(store.db, query, {
8311
+ project: opts.project,
8312
+ session_id: opts.sessionId,
8313
+ limit: intOption(opts.limit),
8314
+ snippet_chars: intOption(opts.snippetChars)
8315
+ });
8316
+ } finally {
8317
+ store.close();
8318
+ }
8319
+ }
8320
+ /** Human-readable output: "Found N results" header + attributed snippet lines. */
8321
+ function formatPiSessionSearchHuman(response) {
8322
+ const warning = response.query_truncated ? "Warning: matching used only part of the query. Shorten it to at most 8,192 characters and 64 search terms, then retry." : null;
8323
+ if (response.results.length === 0) return `No results found for "${response.query}". ${warning ?? EMPTY_INDEX_HINT}`;
8324
+ const lines = [`Found ${response.returned} results for "${response.query}"`];
8325
+ if (warning) lines.push(warning);
8326
+ for (const [index, match] of response.results.entries()) {
8327
+ const attribution = [
8328
+ match.role,
8329
+ match.project ?? "no project",
8330
+ `session ${match.session_id}`,
8331
+ match.timestamp ?? "unknown time"
8332
+ ].join(" · ");
8333
+ const snippet = match.snippet_truncated ? `${match.snippet}…` : match.snippet;
8334
+ lines.push(`${index + 1}. ${attribution}`, snippet);
8335
+ }
8336
+ if (response.truncated) lines.push(`Showing ${response.returned} of ${response.total_matches} matches.`);
8337
+ return lines.join("\n");
8338
+ }
8339
+ var cmd$1 = new Command("pi-session-search").configureHelp(helpStyle).description("Search stored pi session conversations by text query").argument("<query>", "search query").option("--project <project>", "filter by project label").option("--session-id <id>", "filter by pi session id").option("--limit <n>", "max results (1-20)", "10").option("--snippet-chars <n>", "per-result snippet cap in chars (100-4000)", "1200");
8340
+ addDbOption(cmd$1);
8341
+ addJsonOption(cmd$1);
8342
+ var piSessionSearchCommand = cmd$1.action((query, opts) => {
8343
+ try {
8344
+ const response = runPiSessionSearch(query, opts);
8345
+ if (opts.json) {
8346
+ console.log(JSON.stringify(response, null, 2));
8347
+ return;
8348
+ }
8349
+ const text = formatPiSessionSearchHuman(response);
8350
+ if (response.results.length === 0) p.log.warn(text);
8351
+ else p.log.message(text);
8352
+ } catch (error) {
8353
+ const message = error instanceof Error ? error.message : "pi-session-search failed";
8354
+ if (opts.json) emitJsonError("pi_session_search_failed", message);
8355
+ else {
8356
+ p.log.error(message);
8357
+ process.exitCode = 1;
8358
+ }
8359
+ }
8360
+ });
8361
+ //#endregion
7175
8362
  //#region src/commands/recent.ts
7176
8363
  var cmd = new Command("recent").configureHelp(helpStyle).description("Show recent memories").option("--limit <n>", "max results", "5").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").option("--kind <kind>", "filter by memory kind");
7177
8364
  addDbOption(cmd);
@@ -8020,6 +9207,49 @@ function createRawEventTargetState(store) {
8020
9207
  stop
8021
9208
  };
8022
9209
  }
9210
+ function createViewerAccountLink(store, invocation, serverModule) {
9211
+ const configPath = resolveCodememConfigPath(invocation.configPath ?? void 0).resolved.path;
9212
+ const keysDir = dirname(resolveKeyPaths(process.env.CODEMEM_KEYS_DIR?.trim() || void 0)[0]);
9213
+ const readContext = serverModule.createAccountContextReader({
9214
+ getProfile: () => ({
9215
+ dbPath: store.dbPath,
9216
+ deviceId: store.deviceId,
9217
+ identityId: store.actorId,
9218
+ identityLabel: store.actorDisplayName
9219
+ }),
9220
+ readConfig: () => readCodememConfigFileAtPath(configPath),
9221
+ configPath,
9222
+ keysDir
9223
+ });
9224
+ const registry = serverModule.createAccountLinkRegistry({ readContext });
9225
+ let actualBoundPort = null;
9226
+ return {
9227
+ registry,
9228
+ setBoundPort: (port) => {
9229
+ actualBoundPort = port;
9230
+ },
9231
+ options: {
9232
+ readContext,
9233
+ registry,
9234
+ getListenerAddress: () => actualBoundPort === null ? null : {
9235
+ host: invocation.host,
9236
+ port: actualBoundPort
9237
+ }
9238
+ }
9239
+ };
9240
+ }
9241
+ function announceViewerStartup(options) {
9242
+ const { pidPath, invocation, preparedDb, info } = options;
9243
+ writeFileSync(pidPath, JSON.stringify({
9244
+ pid: process.pid,
9245
+ host: invocation.host,
9246
+ port: invocation.port
9247
+ }), "utf-8");
9248
+ p.intro("codemem viewer");
9249
+ p.log.success(`Listening on http://${info.address}:${info.port}`);
9250
+ p.log.info(`Database: ${preparedDb}`);
9251
+ p.log.step("Raw event sweeper started");
9252
+ }
8023
9253
  async function startForegroundViewer(invocation) {
8024
9254
  const serverModule = await import("@codemem/server");
8025
9255
  const { createApp, createSyncApp, closeStore, createViewerRawEventInbox, getStore } = serverModule;
@@ -8053,11 +9283,13 @@ async function startForegroundViewer(invocation) {
8053
9283
  const config = invocation.configPath ? readCodememConfigFileAtPath(invocation.configPath) : readCodememConfigFile();
8054
9284
  const syncConfig = readCoordinatorSyncConfig(config);
8055
9285
  const syncEnabled = syncConfig.syncEnabled;
9286
+ const accountLink = createViewerAccountLink(store, invocation, serverModule);
8056
9287
  const syncRuntimeStatus = {
8057
9288
  phase: syncEnabled ? "starting" : "disabled",
8058
9289
  detail: syncEnabled ? "Waiting for viewer startup to finish" : "Sync is disabled"
8059
9290
  };
8060
9291
  const appOpts = {
9292
+ accountLink: accountLink.options,
8061
9293
  storeFactory: () => store,
8062
9294
  ...rawEventProcessing.appOptions,
8063
9295
  getSyncRuntimeStatus: () => syncRuntimeStatus
@@ -8086,15 +9318,13 @@ async function startForegroundViewer(invocation) {
8086
9318
  hostname: invocation.host,
8087
9319
  port: invocation.port
8088
9320
  }, (info) => {
8089
- writeFileSync(pidPath, JSON.stringify({
8090
- pid: process.pid,
8091
- host: invocation.host,
8092
- port: invocation.port
8093
- }), "utf-8");
8094
- p.intro("codemem viewer");
8095
- p.log.success(`Listening on http://${info.address}:${info.port}`);
8096
- p.log.info(`Database: ${preparedDb}`);
8097
- p.log.step("Raw event sweeper started");
9321
+ accountLink.setBoundPort(info.port);
9322
+ announceViewerStartup({
9323
+ pidPath,
9324
+ invocation,
9325
+ preparedDb,
9326
+ info
9327
+ });
8098
9328
  maintenanceWorker = startMaintenanceWorkerProcess(invocation);
8099
9329
  if (syncEnabled) {
8100
9330
  const syncStartDelayMs = 3e3;
@@ -8143,6 +9373,7 @@ async function startForegroundViewer(invocation) {
8143
9373
  const shutdown = async () => {
8144
9374
  p.outro("shutting down");
8145
9375
  syncAbort.abort();
9376
+ await accountLink.registry.close();
8146
9377
  await stopMaintenanceWorkerProcess(maintenanceWorker, dbPath);
8147
9378
  await new Promise((resolve) => {
8148
9379
  let remaining = syncServer ? 2 : 1;
@@ -9422,6 +10653,7 @@ function installPi(options = {}) {
9422
10653
  p.log.info(" - Start (or restart) pi to load the extension package");
9423
10654
  if (!options.piMcp) p.log.info(" - Optional MCP surface: install pi-mcp-adapter, then re-run with --pi-mcp");
9424
10655
  p.log.info(" - To disable: remove the @codemem/pi-extension entry from pi settings.json packages");
10656
+ p.log.info(" - Pre-install pi session history is not imported automatically; backfill it with: codemem pi-import-sessions");
9425
10657
  }
9426
10658
  return ok;
9427
10659
  }
@@ -10470,37 +11702,99 @@ doctorCmd.action(async (opts) => {
10470
11702
  }
10471
11703
  });
10472
11704
  syncCommand.addCommand(doctorCmd);
10473
- var statusCmd = new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary");
10474
- addDbOption(statusCmd);
10475
- addConfigOption(statusCmd);
10476
- addJsonOption(statusCmd);
10477
- statusCmd.action((opts) => {
10478
- const config = readCliConfig(opts.config);
10479
- const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
10480
- try {
10481
- const d = drizzle(store.db, { schema });
10482
- const deviceRow = d.select({
10483
- device_id: schema.syncDevice.device_id,
10484
- fingerprint: schema.syncDevice.fingerprint
10485
- }).from(schema.syncDevice).limit(1).get();
10486
- const peers = d.select({
10487
- peer_device_id: schema.syncPeers.peer_device_id,
10488
- name: schema.syncPeers.name,
10489
- last_sync_at: schema.syncPeers.last_sync_at,
10490
- last_error: schema.syncPeers.last_error
10491
- }).from(schema.syncPeers).all();
10492
- const semanticIndex = getSemanticIndexDiagnostics(store.db);
10493
- const localDeviceId = deviceRow?.device_id ?? null;
10494
- const peersWithScopes = peers.map((peer) => {
11705
+ function syncStatusIdentityAndPeers(store) {
11706
+ const d = drizzle(store.db, { schema });
11707
+ const deviceRow = d.select({
11708
+ device_id: schema.syncDevice.device_id,
11709
+ fingerprint: schema.syncDevice.fingerprint
11710
+ }).from(schema.syncDevice).limit(1).get();
11711
+ const peers = d.select({
11712
+ peer_device_id: schema.syncPeers.peer_device_id,
11713
+ name: schema.syncPeers.name,
11714
+ last_sync_at: schema.syncPeers.last_sync_at,
11715
+ last_error: schema.syncPeers.last_error,
11716
+ public_key: schema.syncPeers.public_key
11717
+ }).from(schema.syncPeers).all();
11718
+ const localDeviceId = deviceRow?.device_id ?? null;
11719
+ const localSigningPublicKey = localDeviceId && peers.length > 0 && store.db.prepare(`SELECT 1 FROM scope_memberships m
11720
+ JOIN replication_scopes s ON s.scope_id = m.scope_id
11721
+ WHERE m.device_id = ? AND m.status = 'active'
11722
+ AND s.authority_type = 'coordinator' LIMIT 1`).get(localDeviceId) ? loadRuntimeSigningPublicKey(store.db, {
11723
+ deviceId: localDeviceId,
11724
+ keysDir: process.env.CODEMEM_KEYS_DIR?.trim() || void 0
11725
+ }) : null;
11726
+ return {
11727
+ deviceRow,
11728
+ peers,
11729
+ peersWithScopes: peers.map((peer) => {
10495
11730
  const peerDeviceId = String(peer.peer_device_id ?? "").trim();
10496
11731
  return {
10497
11732
  peer,
10498
11733
  scopes: listPerPeerScopeSyncState(store.db, {
10499
11734
  localDeviceId,
10500
- peerDeviceId
11735
+ peerDeviceId,
11736
+ localSigningPublicKey,
11737
+ authenticatedPeerSigningKey: peer.public_key
10501
11738
  })
10502
11739
  };
11740
+ })
11741
+ };
11742
+ }
11743
+ function printSyncStatusPeers(peersWithScopes) {
11744
+ if (peersWithScopes.length === 0) {
11745
+ p.log.info("Peers: none");
11746
+ return;
11747
+ }
11748
+ for (const { peer, scopes } of peersWithScopes) {
11749
+ const peerHeader = ` ${peer.name || peer.peer_device_id}: last_sync=${peer.last_sync_at ?? "never"}, status=${peer.last_error ?? "ok"}`;
11750
+ if (scopes.length === 0) {
11751
+ p.log.message(peerHeader);
11752
+ continue;
11753
+ }
11754
+ const scopeLines = scopes.map((scope) => {
11755
+ const state = scope.bootstrapped ? "received" : "pending";
11756
+ return ` - ${scope.label} (${scope.scope_id}): ${state}`;
10503
11757
  });
11758
+ p.log.message([
11759
+ peerHeader,
11760
+ " Spaces:",
11761
+ ...scopeLines
11762
+ ].join("\n"));
11763
+ }
11764
+ }
11765
+ function printSyncStatus(config, identityAndPeers, semanticIndex) {
11766
+ const { deviceRow, peers, peersWithScopes } = identityAndPeers;
11767
+ p.intro("codemem sync status");
11768
+ p.log.info([
11769
+ `Enabled: ${config.sync_enabled === true ? "yes" : "no"}`,
11770
+ `Host: ${config.sync_host ?? "0.0.0.0"}`,
11771
+ `Port: ${config.sync_port ?? 7337}`,
11772
+ `Interval: ${config.sync_interval_s ?? 120}s`,
11773
+ `Coordinator: ${config.sync_coordinator_url ?? "(not configured)"}`
11774
+ ].join("\n"));
11775
+ if (deviceRow) p.log.info(`Device ID: ${deviceRow.device_id}\nFingerprint: ${deviceRow.fingerprint}`);
11776
+ else p.log.warn("Device identity not initialized (run `codemem sync enable`)");
11777
+ printSyncStatusPeers(peersWithScopes);
11778
+ p.log.info([
11779
+ "Semantic index:",
11780
+ ` State: ${semanticIndex.state}`,
11781
+ ` Summary: ${semanticIndex.summary}`,
11782
+ ` Coverage: ${semanticIndex.indexed_memory_count}/${semanticIndex.embeddable_memory_count}`,
11783
+ ` Mode: ${semanticIndex.mode === "semantic" ? `semantic (${semanticIndex.semantic_search_model ?? semanticIndex.current_model})` : "keyword-only"}`
11784
+ ].join("\n"));
11785
+ p.outro(`${peers.length} peer(s)`);
11786
+ }
11787
+ var statusCmd = new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary");
11788
+ addDbOption(statusCmd);
11789
+ addConfigOption(statusCmd);
11790
+ addJsonOption(statusCmd);
11791
+ statusCmd.action(function showSyncStatus(opts) {
11792
+ const config = readCliConfig(opts.config);
11793
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
11794
+ try {
11795
+ const identityAndPeers = syncStatusIdentityAndPeers(store);
11796
+ const { deviceRow, peersWithScopes } = identityAndPeers;
11797
+ const semanticIndex = getSemanticIndexDiagnostics(store.db);
10504
11798
  if (opts.json) {
10505
11799
  console.log(JSON.stringify({
10506
11800
  enabled: config.sync_enabled === true,
@@ -10521,40 +11815,7 @@ statusCmd.action((opts) => {
10521
11815
  }, null, 2));
10522
11816
  return;
10523
11817
  }
10524
- p.intro("codemem sync status");
10525
- p.log.info([
10526
- `Enabled: ${config.sync_enabled === true ? "yes" : "no"}`,
10527
- `Host: ${config.sync_host ?? "0.0.0.0"}`,
10528
- `Port: ${config.sync_port ?? 7337}`,
10529
- `Interval: ${config.sync_interval_s ?? 120}s`,
10530
- `Coordinator: ${config.sync_coordinator_url ?? "(not configured)"}`
10531
- ].join("\n"));
10532
- if (deviceRow) p.log.info(`Device ID: ${deviceRow.device_id}\nFingerprint: ${deviceRow.fingerprint}`);
10533
- else p.log.warn("Device identity not initialized (run `codemem sync enable`)");
10534
- if (peers.length === 0) p.log.info("Peers: none");
10535
- else for (const { peer, scopes } of peersWithScopes) {
10536
- const peerHeader = ` ${peer.name || peer.peer_device_id}: last_sync=${peer.last_sync_at ?? "never"}, status=${peer.last_error ?? "ok"}`;
10537
- if (scopes.length === 0) p.log.message(peerHeader);
10538
- else {
10539
- const scopeLines = scopes.map((scope) => {
10540
- const state = scope.bootstrapped ? "received" : "pending";
10541
- return ` - ${scope.label} (${scope.scope_id}): ${state}`;
10542
- });
10543
- p.log.message([
10544
- peerHeader,
10545
- " Spaces:",
10546
- ...scopeLines
10547
- ].join("\n"));
10548
- }
10549
- }
10550
- p.log.info([
10551
- "Semantic index:",
10552
- ` State: ${semanticIndex.state}`,
10553
- ` Summary: ${semanticIndex.summary}`,
10554
- ` Coverage: ${semanticIndex.indexed_memory_count}/${semanticIndex.embeddable_memory_count}`,
10555
- ` Mode: ${semanticIndex.mode === "semantic" ? `semantic (${semanticIndex.semantic_search_model ?? semanticIndex.current_model})` : "keyword-only"}`
10556
- ].join("\n"));
10557
- p.outro(`${peers.length} peer(s)`);
11818
+ printSyncStatus(config, identityAndPeers, semanticIndex);
10558
11819
  } finally {
10559
11820
  store.close();
10560
11821
  }
@@ -10902,6 +12163,7 @@ connectCmd.action((url, opts) => {
10902
12163
  syncCommand.addCommand(connectCmd);
10903
12164
  var syncCoordinatorAlias = buildCoordinatorCommand();
10904
12165
  syncCoordinatorAlias.hook("preAction", (_thisCmd, actionCmd) => {
12166
+ if (actionCmd.optsWithGlobals().json) return;
10905
12167
  const subName = actionCmd.name();
10906
12168
  emitDeprecationWarning(`codemem sync coordinator ${subName}`, `codemem coordinator ${subName}`);
10907
12169
  });
@@ -11726,6 +12988,8 @@ var ROOT_COMPLETION_COMMANDS = [
11726
12988
  "mcp",
11727
12989
  "memory",
11728
12990
  "pack",
12991
+ "pi-import-sessions",
12992
+ "pi-session-search",
11729
12993
  "recent",
11730
12994
  "search",
11731
12995
  "serve",
@@ -11790,6 +13054,8 @@ function registerRootCommands(program) {
11790
13054
  program.addCommand(recentCommand);
11791
13055
  program.addCommand(searchCommand);
11792
13056
  program.addCommand(packCommand);
13057
+ program.addCommand(piSessionSearchCommand);
13058
+ program.addCommand(piImportSessionsCommand);
11793
13059
  program.addCommand(promptPackLedgerCommand, { hidden: true });
11794
13060
  program.addCommand(showMemoryCommand, { hidden: true });
11795
13061
  program.addCommand(forgetMemoryCommand, { hidden: true });