codemem 0.46.0-alpha.2 → 0.46.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, 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, 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, resolveProject, resolveProjectRoot, resolveRetrievalSession, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, setPeerProjectFilter, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, vacuumDatabase, viewerUrl } from "@codemem/core";
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, 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";
3
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { randomInt, randomUUID } from "node:crypto";
@@ -4949,6 +4949,11 @@ function forgetMemoryAction(idStr, opts) {
4949
4949
  store.close();
4950
4950
  }
4951
4951
  }
4952
+ function parseConfidence(value) {
4953
+ const n = value == null || value.trim() === "" ? .5 : Number(value);
4954
+ if (!Number.isFinite(n)) return .5;
4955
+ return Math.min(1, Math.max(0, n));
4956
+ }
4952
4957
  function rollbackManualMemory(store, sessionId, memoryId) {
4953
4958
  store.db.transaction(() => {
4954
4959
  const row = store.db.prepare("SELECT import_key FROM memory_items WHERE id = ?").get(memoryId);
@@ -4974,7 +4979,7 @@ async function rememberMemoryAction(opts) {
4974
4979
  toolVersion: "manual",
4975
4980
  metadata: { manual: true }
4976
4981
  });
4977
- const memId = store.remember(sessionId, opts.kind, opts.title, opts.body, .5, opts.tags);
4982
+ const memId = store.remember(sessionId, opts.kind, opts.title, opts.body, parseConfidence(opts.confidence), opts.tags);
4978
4983
  if (!store.get(memId)) {
4979
4984
  await store.flushPendingVectorWrites();
4980
4985
  rollbackManualMemory(store, sessionId, memId);
@@ -5017,7 +5022,7 @@ function createForgetMemoryCommand() {
5017
5022
  return cmd;
5018
5023
  }
5019
5024
  function createRememberMemoryCommand() {
5020
- const cmd = new Command("remember").configureHelp(helpStyle).description("Manually add a memory item").requiredOption("-k, --kind <kind>", "memory kind (discovery, decision, feature, bugfix, etc.)").requiredOption("-t, --title <title>", "memory title").requiredOption("-b, --body <body>", "memory body text").option("--tags <tags...>", "tags (space-separated)").option("--project <project>", "project name (defaults to git repo root)");
5025
+ const cmd = new Command("remember").configureHelp(helpStyle).description("Manually add a memory item").requiredOption("-k, --kind <kind>", "memory kind (discovery, decision, feature, bugfix, etc.)").requiredOption("-t, --title <title>", "memory title").requiredOption("-b, --body <body>", "memory body text").option("--tags <tags...>", "tags (space-separated)").option("--project <project>", "project name (defaults to git repo root)").option("--confidence <n>", "confidence 0-1", "0.5");
5021
5026
  addDbOption(cmd);
5022
5027
  addJsonOption(cmd);
5023
5028
  cmd.action(rememberMemoryAction);
@@ -8401,6 +8406,16 @@ function writeJsonConfig(path, data, { createBackup = true } = {}) {
8401
8406
  atomicReplaceConfigFile(path, output, configMetadata(path));
8402
8407
  return true;
8403
8408
  }
8409
+ /**
8410
+ * Write JSON config after copying any existing file to `<path>.codemem.bak`.
8411
+ * Backup failure is non-fatal (write still proceeds).
8412
+ */
8413
+ function writeJsonConfigWithBackup(path, data) {
8414
+ if (existsSync(path)) try {
8415
+ copyFileSync(path, `${path}.codemem.bak`);
8416
+ } catch {}
8417
+ writeJsonConfig(path, data);
8418
+ }
8404
8419
  //#endregion
8405
8420
  //#region src/commands/setup.ts
8406
8421
  /**
@@ -8412,6 +8427,8 @@ function writeJsonConfig(path, data, { createBackup = true } = {}) {
8412
8427
  * 1. Adds "@codemem/opencode-plugin" to the plugin array in ~/.config/opencode/opencode.jsonc
8413
8428
  * 2. Adds/updates the MCP entry in ~/.config/opencode/opencode.jsonc
8414
8429
  * 3. For Claude Code: installs MCP config and guides marketplace plugin install
8430
+ * 4. For Codex: MCP + hooks via CODEX_HOME
8431
+ * 5. For pi: packages entry + observer derivation + optional MCP adapter surface
8415
8432
  *
8416
8433
  * Designed to be safe to run repeatedly (idempotent unless --force).
8417
8434
  */
@@ -8425,6 +8442,23 @@ function claudeConfigDir() {
8425
8442
  function codexConfigDir() {
8426
8443
  return process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
8427
8444
  }
8445
+ /** Resolve the pi agent directory, honoring PI_CODING_AGENT_DIR. */
8446
+ function piConfigDir() {
8447
+ return resolvePiAgentDir();
8448
+ }
8449
+ /** npm packages: entry prefix for the pi extension (version is appended). */
8450
+ var PI_EXTENSION_NPM_NAME = "@codemem/pi-extension";
8451
+ var PI_EXTENSION_NPM_PREFIX = `npm:${PI_EXTENSION_NPM_NAME}@`;
8452
+ var PI_MCP_ADAPTER_MARKER = "pi-mcp-adapter";
8453
+ /** Codemem stdio MCP server entry written to pi's mcp.json under --pi-mcp. */
8454
+ var PI_MCP_CODEMEM_ENTRY = {
8455
+ command: "npx",
8456
+ args: [
8457
+ "-y",
8458
+ "codemem",
8459
+ "mcp"
8460
+ ]
8461
+ };
8428
8462
  /** Remove legacy copied plugin JS file from ~/.config/opencode/plugins/codemem.js */
8429
8463
  function migrateLegacyOpencodePlugin() {
8430
8464
  const legacyPlugin = join(opencodeConfigDir(), "plugins", "codemem.js");
@@ -8949,38 +8983,374 @@ function installCodex(force, options = {}) {
8949
8983
  ok = installCodexHooks(codexHome, force, hookBase) && ok;
8950
8984
  return ok;
8951
8985
  }
8952
- var setupCommand = new Command("setup").configureHelp(helpStyle).description("Install codemem plugin + MCP config for OpenCode and Claude Code").option("--force", "overwrite existing installations").option("--opencode-only", "only install for OpenCode").option("--claude-only", "only install for Claude Code").option("--codex-only", "only install for Codex").action((opts) => {
8953
- p.intro(`codemem setup v${VERSION}`);
8954
- const force = opts.force ?? false;
8986
+ /** Build the packages: entry for the pi extension (npm pin or local path). */
8987
+ function buildPiExtensionPackageSpec(extensionPath, version = VERSION) {
8988
+ if (extensionPath?.trim()) {
8989
+ const raw = extensionPath.trim();
8990
+ return isAbsolute(raw) ? raw : resolve(raw);
8991
+ }
8992
+ return `${PI_EXTENSION_NPM_PREFIX}${version}`;
8993
+ }
8994
+ /** True when a packages: entry refers to @codemem/pi-extension (any version/path form). */
8995
+ function isPiExtensionPackageEntry(entry) {
8996
+ if (typeof entry !== "string" || !entry.trim()) return false;
8997
+ const value = entry.trim();
8998
+ if (value === `npm:${PI_EXTENSION_NPM_NAME}` || value.startsWith(PI_EXTENSION_NPM_PREFIX)) return true;
8999
+ if (value === PI_EXTENSION_NPM_NAME || value.startsWith(`${PI_EXTENSION_NPM_NAME}@`)) return true;
9000
+ return /(?:^|[/\\])@codemem[/\\]pi-extension(?:[/\\]|$)/.test(value) || /(?:^|[/\\])packages[/\\]pi-extension(?:[/\\]|$)/.test(value);
9001
+ }
9002
+ /** True when entry is an npm: pin for @codemem/pi-extension (versioned or bare). */
9003
+ function isNpmPiExtensionPackageEntry(entry) {
9004
+ if (typeof entry !== "string" || !entry.trim()) return false;
9005
+ const value = entry.trim();
9006
+ return value === `npm:${PI_EXTENSION_NPM_NAME}` || value.startsWith(PI_EXTENSION_NPM_PREFIX);
9007
+ }
9008
+ function piBinaryOnPath() {
9009
+ try {
9010
+ const out = execFileSync(process.platform === "win32" ? "where" : "which", ["pi"], { encoding: "utf-8" });
9011
+ return Boolean(out.split(/\r?\n/).map((line) => line.trim()).find(Boolean));
9012
+ } catch {
9013
+ return false;
9014
+ }
9015
+ }
9016
+ /** True when path exists and has non-whitespace content (real install marker). */
9017
+ function isNonEmptyFile(path) {
9018
+ try {
9019
+ if (!existsSync(path)) return false;
9020
+ return readFileSync(path, "utf-8").trim().length > 0;
9021
+ } catch {
9022
+ return false;
9023
+ }
9024
+ }
9025
+ /**
9026
+ * Detect pi via `pi` on PATH, or an agent dir that contains real install
9027
+ * markers (non-empty settings.json or auth.json). A bare empty ~/.pi/agent
9028
+ * directory must not count — users (and tests) create that path incidentally.
9029
+ * Honors PI_CODING_AGENT_DIR.
9030
+ */
9031
+ function isPiDetected() {
9032
+ if (piBinaryOnPath()) return true;
9033
+ const dir = piConfigDir();
9034
+ if (!existsSync(dir)) return false;
9035
+ return isNonEmptyFile(join(dir, "settings.json")) || isNonEmptyFile(join(dir, "auth.json"));
9036
+ }
9037
+ /** Detect pi-mcp-adapter via packages: entry or extensions/ directory name. */
9038
+ function isPiMcpAdapterDetected(piDir = piConfigDir()) {
9039
+ const settingsPath = join(piDir, "settings.json");
9040
+ if (existsSync(settingsPath)) try {
9041
+ const packages = loadJsoncConfig(settingsPath).packages;
9042
+ if (Array.isArray(packages)) {
9043
+ if (packages.some((entry) => typeof entry === "string" && entry.includes(PI_MCP_ADAPTER_MARKER))) return true;
9044
+ }
9045
+ } catch {}
9046
+ const extensionsDir = join(piDir, "extensions");
9047
+ if (!existsSync(extensionsDir)) return false;
9048
+ try {
9049
+ return readdirSync(extensionsDir, { withFileTypes: true }).some((entry) => {
9050
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) return false;
9051
+ const name = entry.name.toLowerCase();
9052
+ return name.includes("mcp-adapter") || name.includes("mcp_adapter");
9053
+ });
9054
+ } catch {
9055
+ return false;
9056
+ }
9057
+ }
9058
+ function readPiJsoncOrAbort(path, untouchedHint) {
9059
+ if (!existsSync(path)) return {};
9060
+ try {
9061
+ return loadJsoncConfig(path);
9062
+ } catch (err) {
9063
+ p.log.error(`Failed to parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
9064
+ p.log.info(`Leaving ${path} untouched. ${untouchedHint}`);
9065
+ return null;
9066
+ }
9067
+ }
9068
+ function shouldKeepExistingPiExtension(existing, force, desired) {
9069
+ if (existing == null || force) return false;
9070
+ return !(isNpmPiExtensionPackageEntry(existing) && existing !== desired);
9071
+ }
9072
+ function writePiExtensionSettings(piDir, settingsPath, settings, existing, desired) {
9073
+ try {
9074
+ mkdirSync(piDir, { recursive: true });
9075
+ writeJsonConfigWithBackup(settingsPath, settings);
9076
+ p.log.success(existing != null ? `Pi extension package updated: ${desired}` : `Pi extension package added: ${desired}`);
9077
+ } catch (err) {
9078
+ p.log.error(`Failed to write ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
9079
+ return false;
9080
+ }
9081
+ return true;
9082
+ }
9083
+ /**
9084
+ * Idempotently ensure the pi extension packages: entry is present in
9085
+ * settings.json. Backs up before write; aborts on parse failure.
9086
+ */
9087
+ function installPiExtensionPackage(piDir, force, extensionPath) {
9088
+ const settingsPath = join(piDir, "settings.json");
9089
+ const desired = buildPiExtensionPackageSpec(extensionPath);
9090
+ const settings = readPiJsoncOrAbort(settingsPath, "Fix or remove the file, then re-run `codemem setup --pi-only`.");
9091
+ if (settings == null) return false;
9092
+ const list = Array.isArray(settings.packages) ? [...settings.packages] : [];
9093
+ const existing = list.find((entry) => isPiExtensionPackageEntry(entry));
9094
+ if (shouldKeepExistingPiExtension(existing, force, desired)) {
9095
+ p.log.info(`Pi extension package already configured in ${settingsPath}`);
9096
+ return true;
9097
+ }
9098
+ settings.packages = [...list.filter((entry) => !isPiExtensionPackageEntry(entry)), desired];
9099
+ return writePiExtensionSettings(piDir, settingsPath, settings, existing, desired);
9100
+ }
9101
+ function isOfficialObserverBaseUrl(url) {
9102
+ const normalized = url.trim().replace(/\/+$/, "").toLowerCase();
9103
+ return normalized === "https://api.openai.com" || normalized === "https://api.openai.com/v1" || normalized === "https://api.anthropic.com" || normalized === "https://api.anthropic.com/v1";
9104
+ }
9105
+ function nextPiToolsBlock(existing, toolsMode) {
9106
+ const piBlock = { ...existing.pi != null && typeof existing.pi === "object" && !Array.isArray(existing.pi) ? { ...existing.pi } : {} };
9107
+ if (toolsMode === "mcp-adapter") {
9108
+ if (piBlock.tools_mode === "mcp-adapter") return {
9109
+ piBlock,
9110
+ toolsModeUpdated: false
9111
+ };
9112
+ piBlock.tools_mode = "mcp-adapter";
9113
+ return {
9114
+ piBlock,
9115
+ toolsModeUpdated: true
9116
+ };
9117
+ }
9118
+ if (piBlock.tools_mode == null || piBlock.tools_mode === "") {
9119
+ piBlock.tools_mode = "native";
9120
+ return {
9121
+ piBlock,
9122
+ toolsModeUpdated: true
9123
+ };
9124
+ }
9125
+ return {
9126
+ piBlock,
9127
+ toolsModeUpdated: false
9128
+ };
9129
+ }
9130
+ function applyResolvedPiObserver(resolved, needs, next, updated) {
9131
+ if (needs.envProvider || needs.envModel) return;
9132
+ const explicitProvider = needs.fileProvider;
9133
+ if (explicitProvider) {
9134
+ if (needs.model && explicitProvider.toLowerCase() === String(resolved.provider).toLowerCase()) {
9135
+ next.observer_model = resolved.model;
9136
+ updated.push("observer_model");
9137
+ }
9138
+ return;
9139
+ }
9140
+ if (needs.fileModel) return;
9141
+ next.observer_provider = resolved.provider;
9142
+ updated.push("observer_provider");
9143
+ next.observer_model = resolved.model;
9144
+ updated.push("observer_model");
9145
+ const customBaseUrl = resolved.baseUrl && !isOfficialObserverBaseUrl(resolved.baseUrl) ? resolved.baseUrl : void 0;
9146
+ if (needs.baseUrl && customBaseUrl) {
9147
+ next.observer_base_url = customBaseUrl;
9148
+ updated.push("observer_base_url");
9149
+ }
9150
+ if (needs.useResponses && customBaseUrl) {
9151
+ next.observer_openai_use_responses = resolved.openAIUseResponses;
9152
+ updated.push("observer_openai_use_responses");
9153
+ }
9154
+ }
9155
+ function unsetObserverSlots(existing) {
9156
+ const envProvider = process.env.CODEMEM_OBSERVER_PROVIDER?.trim();
9157
+ const envModel = process.env.CODEMEM_OBSERVER_MODEL?.trim();
9158
+ const envBaseUrl = process.env.CODEMEM_OBSERVER_BASE_URL?.trim();
9159
+ const envUseResponses = process.env.CODEMEM_OBSERVER_OPENAI_USE_RESPONSES?.trim();
9160
+ const fileProvider = typeof existing.observer_provider === "string" ? existing.observer_provider.trim() : "";
9161
+ const fileModel = typeof existing.observer_model === "string" ? existing.observer_model.trim() : "";
9162
+ const fileBaseUrl = typeof existing.observer_base_url === "string" ? existing.observer_base_url.trim() : "";
9163
+ return {
9164
+ provider: !envProvider && !fileProvider,
9165
+ model: !envModel && !fileModel,
9166
+ baseUrl: !envBaseUrl && !fileBaseUrl,
9167
+ useResponses: !envUseResponses && existing.observer_openai_use_responses == null,
9168
+ fileProvider,
9169
+ fileModel,
9170
+ envProvider,
9171
+ envModel
9172
+ };
9173
+ }
9174
+ function fillUnsetObserverFromPi(existing, next, updated) {
9175
+ const needs = unsetObserverSlots(existing);
9176
+ if (!(needs.provider || needs.model || needs.baseUrl || needs.useResponses)) {
9177
+ p.log.info("Existing codemem observer config left unchanged");
9178
+ return;
9179
+ }
9180
+ const resolved = resolvePiObserverConfig();
9181
+ p.log.info(`Pi observer: ${describePiObserverStatus(resolved)}`);
9182
+ if (resolved.ok) {
9183
+ applyResolvedPiObserver(resolved, needs, next, updated);
9184
+ return;
9185
+ }
9186
+ if (!needs.fileProvider && !needs.fileModel && !needs.envProvider && !needs.envModel) p.log.info("Extraction model left unconfigured — set observer_provider/observer_model when ready.");
9187
+ }
9188
+ function writeUpdatedCodememConfig(next, updated) {
9189
+ if (updated.length === 0) return true;
9190
+ try {
9191
+ const saved = writeCodememConfigFile(next);
9192
+ p.log.success(`Codemem config updated (${updated.join(", ")}): ${saved}`);
9193
+ } catch (err) {
9194
+ p.log.error(`Failed to write codemem config: ${err instanceof Error ? err.message : String(err)}`);
9195
+ return false;
9196
+ }
9197
+ return true;
9198
+ }
9199
+ /**
9200
+ * Derive unset observer_* keys from pi config and ensure pi.tools_mode default.
9201
+ * Never persists API keys or tokens from pi auth.
9202
+ */
9203
+ function wirePiCodememConfig(opts) {
9204
+ let existing;
9205
+ try {
9206
+ existing = readCodememConfigFile();
9207
+ } catch (err) {
9208
+ p.log.error(`Failed to read codemem config: ${err instanceof Error ? err.message : String(err)}`);
9209
+ return false;
9210
+ }
9211
+ const next = { ...existing };
9212
+ const updated = [];
9213
+ const { piBlock, toolsModeUpdated } = nextPiToolsBlock(existing, opts.toolsMode);
9214
+ if (toolsModeUpdated) updated.push("pi.tools_mode");
9215
+ next.pi = piBlock;
9216
+ fillUnsetObserverFromPi(existing, next, updated);
9217
+ return writeUpdatedCodememConfig(next, updated);
9218
+ }
9219
+ /**
9220
+ * Opt-in MCP surface for pi via pi-mcp-adapter. Writes mcp.json only when the
9221
+ * adapter is detected; otherwise explains the prerequisite and writes nothing.
9222
+ */
9223
+ function installPiMcp(piDir, force) {
9224
+ if (!isPiMcpAdapterDetected(piDir)) {
9225
+ p.log.warn("MCP in pi requires the pi-mcp-adapter package. Install it (e.g. `pi install npm:pi-mcp-adapter`), then re-run `codemem setup --pi-only --pi-mcp`.");
9226
+ return {
9227
+ ok: true,
9228
+ wrote: false,
9229
+ adapterPresent: false
9230
+ };
9231
+ }
9232
+ const mcpPath = join(piDir, "mcp.json");
9233
+ let mcp;
9234
+ if (existsSync(mcpPath)) try {
9235
+ mcp = loadJsoncConfig(mcpPath);
9236
+ } catch (err) {
9237
+ p.log.error(`Failed to parse ${mcpPath}: ${err instanceof Error ? err.message : String(err)}`);
9238
+ p.log.info(`Leaving ${mcpPath} untouched. Fix or remove the file, then re-run with --pi-mcp.`);
9239
+ return {
9240
+ ok: false,
9241
+ wrote: false,
9242
+ adapterPresent: true
9243
+ };
9244
+ }
9245
+ else mcp = {};
9246
+ let servers = mcp.mcpServers;
9247
+ if (servers == null || typeof servers !== "object" || Array.isArray(servers)) servers = {};
9248
+ if ("codemem" in servers && !force) {
9249
+ p.log.info(`Pi MCP entry already exists in ${mcpPath}`);
9250
+ return {
9251
+ ok: true,
9252
+ wrote: false,
9253
+ adapterPresent: true
9254
+ };
9255
+ }
9256
+ servers.codemem = { ...PI_MCP_CODEMEM_ENTRY };
9257
+ mcp.mcpServers = servers;
9258
+ try {
9259
+ mkdirSync(piDir, { recursive: true });
9260
+ writeJsonConfigWithBackup(mcpPath, mcp);
9261
+ p.log.success(`Pi MCP entry installed: ${mcpPath}`);
9262
+ } catch (err) {
9263
+ p.log.error(`Failed to write ${mcpPath}: ${err instanceof Error ? err.message : String(err)}`);
9264
+ return {
9265
+ ok: false,
9266
+ wrote: false,
9267
+ adapterPresent: true
9268
+ };
9269
+ }
9270
+ return {
9271
+ ok: true,
9272
+ wrote: true,
9273
+ adapterPresent: true
9274
+ };
9275
+ }
9276
+ /**
9277
+ * Configure pi: packages entry, observer derivation, optional MCP adapter surface.
9278
+ * Idempotent; honors PI_CODING_AGENT_DIR. Returns true on success.
9279
+ */
9280
+ function installPi(options = {}) {
9281
+ const force = options.force ?? false;
9282
+ const piDir = piConfigDir();
9283
+ try {
9284
+ mkdirSync(piDir, { recursive: true });
9285
+ } catch (err) {
9286
+ p.log.error(`Failed to create pi agent dir ${piDir}: ${err instanceof Error ? err.message : String(err)}`);
9287
+ return false;
9288
+ }
9289
+ p.log.info(`Pi agent directory: ${piDir}`);
9290
+ if (!installPiExtensionPackage(piDir, force, options.piExtensionPath)) return false;
8955
9291
  let ok = true;
8956
- const onlyFlag = Boolean(opts.opencodeOnly || opts.claudeOnly || opts.codexOnly);
8957
- const doOpencode = opts.opencodeOnly || !onlyFlag;
8958
- const doClaude = opts.claudeOnly || !onlyFlag;
8959
- const doCodex = opts.codexOnly || !onlyFlag && existsSync(codexConfigDir());
8960
- if (doOpencode) {
8961
- p.log.step("Installing OpenCode plugin...");
8962
- const pluginResult = installPlugin(force);
8963
- ok = pluginResult.ok && ok;
8964
- p.log.step("Installing OpenCode MCP config...");
8965
- ok = installMcp(force, { createBackup: !pluginResult.wrote }) && ok;
8966
- }
8967
- if (doClaude) {
8968
- p.log.step("Installing Claude Code MCP config...");
8969
- ok = installClaudeMcp(force) && ok;
8970
- }
8971
- if (doCodex) {
8972
- p.log.step("Configuring Codex (MCP + hooks)...");
8973
- ok = installCodex(force) && ok;
8974
- p.log.info("Codex next steps:");
8975
- p.log.info(" - Restart Codex to load the new configuration");
8976
- p.log.info(" - On first run, approve the one-time prompt to trust the codemem hooks");
8977
- p.log.info(" - MCP recall works immediately (no trust prompt required)");
8978
- }
8979
- if (ok) p.outro("Setup complete — restart your editor to load the plugin");
8980
- else {
8981
- p.outro("Setup completed with warnings");
8982
- process.exitCode = 1;
9292
+ let toolsMode = "native";
9293
+ if (options.piMcp) {
9294
+ const mcpResult = installPiMcp(piDir, force);
9295
+ ok = mcpResult.ok && ok;
9296
+ if (mcpResult.adapterPresent && mcpResult.ok) toolsMode = "mcp-adapter";
9297
+ }
9298
+ ok = wirePiCodememConfig({ toolsMode }) && ok;
9299
+ if (ok) {
9300
+ p.log.info("Pi next steps:");
9301
+ p.log.info(" - Start (or restart) pi to load the extension package");
9302
+ if (!options.piMcp) p.log.info(" - Optional MCP surface: install pi-mcp-adapter, then re-run with --pi-mcp");
9303
+ p.log.info(" - To disable: remove the @codemem/pi-extension entry from pi settings.json packages");
9304
+ }
9305
+ return ok;
9306
+ }
9307
+ function maybeInstallPi(opts, onlyFlag, force) {
9308
+ if (!(opts.piOnly || !onlyFlag && isPiDetected())) return true;
9309
+ p.log.step("Configuring pi (extension package + observer)...");
9310
+ return installPi({
9311
+ force,
9312
+ piMcp: opts.piMcp,
9313
+ piExtensionPath: opts.piExtensionPath
9314
+ });
9315
+ }
9316
+ function installOpencodeSetup(force) {
9317
+ p.log.step("Installing OpenCode plugin...");
9318
+ const pluginResult = installPlugin(force);
9319
+ p.log.step("Installing OpenCode MCP config...");
9320
+ return pluginResult.ok && installMcp(force, { createBackup: !pluginResult.wrote });
9321
+ }
9322
+ function installClaudeSetup(force) {
9323
+ p.log.step("Installing Claude Code MCP config...");
9324
+ return installClaudeMcp(force);
9325
+ }
9326
+ function installCodexSetup(force) {
9327
+ p.log.step("Configuring Codex (MCP + hooks)...");
9328
+ const ok = installCodex(force);
9329
+ p.log.info("Codex next steps:");
9330
+ p.log.info(" - Restart Codex to load the new configuration");
9331
+ p.log.info(" - On first run, approve the one-time prompt to trust the codemem hooks");
9332
+ p.log.info(" - MCP recall works immediately (no trust prompt required)");
9333
+ return ok;
9334
+ }
9335
+ function runSetupInstalls(opts, force) {
9336
+ const onlyFlag = Boolean(opts.opencodeOnly || opts.claudeOnly || opts.codexOnly || opts.piOnly);
9337
+ let ok = true;
9338
+ if (opts.opencodeOnly || !onlyFlag) ok = installOpencodeSetup(force) && ok;
9339
+ if (opts.claudeOnly || !onlyFlag) ok = installClaudeSetup(force) && ok;
9340
+ if (opts.codexOnly || !onlyFlag && existsSync(codexConfigDir())) ok = installCodexSetup(force) && ok;
9341
+ return maybeInstallPi(opts, onlyFlag, force) && ok;
9342
+ }
9343
+ function runSetup(opts) {
9344
+ p.intro(`codemem setup v${VERSION}`);
9345
+ if (runSetupInstalls(opts, opts.force ?? false)) {
9346
+ p.outro("Setup complete — restart your editor to load the plugin");
9347
+ return;
8983
9348
  }
9349
+ p.outro("Setup completed with warnings");
9350
+ process.exitCode = 1;
9351
+ }
9352
+ var setupCommand = new Command("setup").configureHelp(helpStyle).description("Install codemem plugin + MCP config for OpenCode, Claude Code, Codex, and pi").option("--force", "overwrite existing installations").option("--opencode-only", "only install for OpenCode").option("--claude-only", "only install for Claude Code").option("--codex-only", "only install for Codex").option("--pi-only", "only install for pi").option("--pi-mcp", "opt into pi MCP adapter surface (requires pi-mcp-adapter)").option("--pi-extension-path <path>", "dev: write a local path packages entry instead of the npm pin").action((opts) => {
9353
+ runSetup(opts);
8984
9354
  });
8985
9355
  //#endregion
8986
9356
  //#region src/commands/stats.ts