codemem 0.35.0 → 0.35.1

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
@@ -2,14 +2,14 @@
2
2
  import { DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, REF_BACKFILL_JOB, RawEventSweeper, RefBackfillRunner, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, ensureDeviceIdentity, ensureSchemaBootstrapped, exportMemories, extractApplyPatchPaths, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, listMaintenanceJobs, listPerPeerScopeSyncState, loadObserverConfig, loadPublicKey, loadSqliteVec, mdnsEnabled, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import { homedir, networkInterfaces } from "node:os";
7
7
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { styleText } from "node:util";
9
9
  import { randomInt, randomUUID } from "node:crypto";
10
10
  import * as p from "@clack/prompts";
11
11
  import { serve } from "@hono/node-server";
12
- import { spawn, spawnSync } from "node:child_process";
12
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
13
13
  import net from "node:net";
14
14
  import { desc, eq } from "drizzle-orm";
15
15
  import { drizzle } from "drizzle-orm/better-sqlite3";
@@ -5807,6 +5807,10 @@ function opencodeConfigDir() {
5807
5807
  function claudeConfigDir() {
5808
5808
  return join(homedir(), ".claude");
5809
5809
  }
5810
+ /** Resolve the Codex home directory, honoring CODEX_HOME. */
5811
+ function codexConfigDir() {
5812
+ return process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
5813
+ }
5810
5814
  /** The npm package name used in the OpenCode plugin array. */
5811
5815
  var OPENCODE_PLUGIN_SPEC = "@codemem/opencode-plugin";
5812
5816
  var LEGACY_OPENCODE_PLUGIN_SPECS = ["codemem", "@kunickiaj/codemem"];
@@ -5986,20 +5990,208 @@ function installClaudeMcp(force) {
5986
5990
  } else p.log.info("Claude Code hooks plugin appears to be installed");
5987
5991
  return true;
5988
5992
  }
5989
- 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").action((opts) => {
5993
+ /** The MCP server table appended to Codex config.toml. */
5994
+ var CODEX_MCP_BLOCK = [
5995
+ "[mcp_servers.codemem]",
5996
+ "command = \"npx\"",
5997
+ "args = [\"-y\", \"codemem\", \"mcp\"]",
5998
+ "startup_timeout_sec = 30",
5999
+ "tool_timeout_sec = 60"
6000
+ ].join("\n");
6001
+ var CODEX_MCP_TABLE_RE = /^[ \t]*\[[ \t]*mcp_servers[ \t]*\.[ \t]*("?)codemem\1[ \t]*\]/m;
6002
+ /** Marker substring identifying codemem-owned hook commands. */
6003
+ var CODEMEM_HOOK_MARKER = "codemem codex-hook-";
6004
+ /**
6005
+ * Resolve how Codex hooks should invoke codemem. Prefer a direct `codemem` call
6006
+ * when it's on PATH (fast — no per-hook resolution); fall back to `npx -y codemem`
6007
+ * only when codemem isn't installed (e.g. setup was run via `npx codemem setup`),
6008
+ * so capture/recall still work without a global install. Mirrors the plugin
6009
+ * wrapper's `codemem`-first / `npx` fallback model.
6010
+ */
6011
+ function codememCodexHookBase() {
6012
+ return codememOnPath() ? "codemem" : "npx -y codemem";
6013
+ }
6014
+ /**
6015
+ * Build the codemem-owned hook groups keyed by Codex event name, given the
6016
+ * resolved command base (`codemem` or `npx -y codemem`). Timeouts are ceilings,
6017
+ * not expected runtimes; npx gets more headroom to absorb a cold resolve.
6018
+ */
6019
+ function buildCodememCodexHookGroups(base) {
6020
+ const isNpx = base !== "codemem";
6021
+ const ingestTimeout = isNpx ? 30 : 10;
6022
+ const injectTimeout = isNpx ? 20 : 10;
6023
+ const ingest = {
6024
+ type: "command",
6025
+ command: `${base} codex-hook-ingest`,
6026
+ timeout: ingestTimeout,
6027
+ statusMessage: "codemem"
6028
+ };
6029
+ return {
6030
+ SessionStart: [{ hooks: [{ ...ingest }] }],
6031
+ UserPromptSubmit: [{ hooks: [{
6032
+ type: "command",
6033
+ command: `${base} codex-hook-ingest`,
6034
+ timeout: ingestTimeout,
6035
+ statusMessage: "codemem capture"
6036
+ }, {
6037
+ type: "command",
6038
+ command: `${base} codex-hook-inject`,
6039
+ timeout: injectTimeout,
6040
+ statusMessage: "codemem recall"
6041
+ }] }],
6042
+ PostToolUse: [{ hooks: [{ ...ingest }] }],
6043
+ Stop: [{ hooks: [{ ...ingest }] }]
6044
+ };
6045
+ }
6046
+ /** True if a matcher group contains a codemem-owned hook command. */
6047
+ function isCodememHookGroup(group) {
6048
+ if (group == null || typeof group !== "object") return false;
6049
+ const hooks = group.hooks;
6050
+ if (!Array.isArray(hooks)) return false;
6051
+ return hooks.some((h) => h != null && typeof h === "object" && typeof h.command === "string" && h.command.includes(CODEMEM_HOOK_MARKER));
6052
+ }
6053
+ /**
6054
+ * True if a resolved bin path is a transient npx/dlx cache bin. When setup runs
6055
+ * via `npx -y codemem setup --codex`, npx exposes this package's bin on PATH for
6056
+ * the duration of the run, then removes it — so Codex would later fail to find a
6057
+ * bare `codemem`. Such paths must NOT count as "on PATH" for hook command baking.
6058
+ */
6059
+ function isTransientNpxBinPath(resolved) {
6060
+ return /[/\\]_npx[/\\]/.test(resolved) || /[/\\]\.pnpm[/\\]dlx[/\\]/.test(resolved);
6061
+ }
6062
+ /**
6063
+ * Detect whether a durable `codemem` resolves on PATH (excluding a transient
6064
+ * npx/dlx bin that vanishes after this process exits).
6065
+ */
6066
+ function codememOnPath() {
6067
+ try {
6068
+ const resolved = execFileSync(process.platform === "win32" ? "where" : "which", ["codemem"], { encoding: "utf-8" }).split(/\r?\n/).map((line) => line.trim()).find(Boolean);
6069
+ if (!resolved) return false;
6070
+ return !isTransientNpxBinPath(resolved);
6071
+ } catch {
6072
+ return false;
6073
+ }
6074
+ }
6075
+ /**
6076
+ * Append the codemem MCP server table to Codex config.toml without rewriting
6077
+ * unrelated content. Returns true on success.
6078
+ */
6079
+ function installCodexMcp(codexHome, force) {
6080
+ const configPath = join(codexHome, "config.toml");
6081
+ const existing = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
6082
+ if (CODEX_MCP_TABLE_RE.test(existing)) {
6083
+ if (force) p.log.info(`Codex MCP entry already exists in ${configPath} — left as-is (TOML is not rewritten in place)`);
6084
+ else p.log.info(`Codex MCP entry already exists in ${configPath}`);
6085
+ return true;
6086
+ }
6087
+ if (existsSync(configPath)) try {
6088
+ copyFileSync(configPath, `${configPath}.codemem.bak`);
6089
+ } catch {}
6090
+ let next = existing;
6091
+ if (next.length > 0 && !next.endsWith("\n\n")) next += next.endsWith("\n") ? "\n" : "\n\n";
6092
+ next += `${CODEX_MCP_BLOCK}\n`;
6093
+ try {
6094
+ writeFileSync(configPath, next, "utf-8");
6095
+ p.log.success(`Codex MCP entry installed: ${configPath}`);
6096
+ } catch (err) {
6097
+ p.log.error(`Failed to write ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
6098
+ return false;
6099
+ }
6100
+ return true;
6101
+ }
6102
+ /**
6103
+ * Write/merge codemem hook registrations into Codex hooks.json, preserving any
6104
+ * unrelated user hooks. Returns true on success.
6105
+ */
6106
+ function installCodexHooks(codexHome, force) {
6107
+ const hooksPath = join(codexHome, "hooks.json");
6108
+ let config = {};
6109
+ if (existsSync(hooksPath)) try {
6110
+ config = JSON.parse(readFileSync(hooksPath, "utf-8"));
6111
+ } catch (err) {
6112
+ p.log.error(`Failed to parse ${hooksPath}: ${err instanceof Error ? err.message : String(err)}`);
6113
+ p.log.info(`Leaving ${hooksPath} untouched. Fix or remove the file, then re-run \`codemem setup --codex-only\`.`);
6114
+ return false;
6115
+ }
6116
+ let hooks = config.hooks;
6117
+ if (hooks == null || typeof hooks !== "object" || Array.isArray(hooks)) hooks = {};
6118
+ const ours = buildCodememCodexHookGroups(codememCodexHookBase());
6119
+ let changed = false;
6120
+ for (const [event, ourGroups] of Object.entries(ours)) {
6121
+ const current = hooks[event];
6122
+ const existingGroups = Array.isArray(current) ? [...current] : [];
6123
+ if (existingGroups.some(isCodememHookGroup) && !force) continue;
6124
+ const preserved = existingGroups.filter((g) => !isCodememHookGroup(g));
6125
+ hooks[event] = [...preserved, ...ourGroups];
6126
+ changed = true;
6127
+ }
6128
+ if (!changed && !force) {
6129
+ p.log.info(`Codex hooks already configured in ${hooksPath}`);
6130
+ config.hooks = hooks;
6131
+ return true;
6132
+ }
6133
+ config.hooks = hooks;
6134
+ if (existsSync(hooksPath)) try {
6135
+ copyFileSync(hooksPath, `${hooksPath}.codemem.bak`);
6136
+ } catch {}
6137
+ try {
6138
+ mkdirSync(codexHome, { recursive: true });
6139
+ writeFileSync(hooksPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
6140
+ p.log.success(`Codex hooks installed: ${hooksPath}`);
6141
+ } catch (err) {
6142
+ p.log.error(`Failed to write ${hooksPath}: ${err instanceof Error ? err.message : String(err)}`);
6143
+ return false;
6144
+ }
6145
+ return true;
6146
+ }
6147
+ /**
6148
+ * Configure Codex via direct config files (MCP in config.toml + hooks in
6149
+ * hooks.json) without relying on the Codex plugin marketplace. Idempotent;
6150
+ * honors CODEX_HOME. Returns true on success.
6151
+ */
6152
+ function installCodex(force) {
6153
+ const codexHome = codexConfigDir();
6154
+ try {
6155
+ mkdirSync(codexHome, { recursive: true });
6156
+ } catch (err) {
6157
+ p.log.error(`Failed to create Codex home ${codexHome}: ${err instanceof Error ? err.message : String(err)}`);
6158
+ return false;
6159
+ }
6160
+ if (codememOnPath()) p.log.info("Codex hooks will call `codemem` directly (found on PATH).");
6161
+ else p.log.info("`codemem` is not on PATH, so Codex hooks will run via `npx -y codemem` (works without a global install). For lower hook latency: npm i -g codemem");
6162
+ let ok = true;
6163
+ ok = installCodexMcp(codexHome, force) && ok;
6164
+ ok = installCodexHooks(codexHome, force) && ok;
6165
+ return ok;
6166
+ }
6167
+ 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").option("--codex", "configure Codex only (alias for --codex-only)").action((opts) => {
5990
6168
  p.intro(`codemem setup v${VERSION}`);
5991
6169
  const force = opts.force ?? false;
5992
6170
  let ok = true;
5993
- if (!opts.claudeOnly) {
6171
+ const codexOnly = Boolean(opts.codexOnly || opts.codex);
6172
+ const onlyFlag = Boolean(opts.opencodeOnly || opts.claudeOnly || codexOnly);
6173
+ const doOpencode = opts.opencodeOnly || !onlyFlag;
6174
+ const doClaude = opts.claudeOnly || !onlyFlag;
6175
+ const doCodex = codexOnly || !onlyFlag && existsSync(codexConfigDir());
6176
+ if (doOpencode) {
5994
6177
  p.log.step("Installing OpenCode plugin...");
5995
6178
  ok = installPlugin(force) && ok;
5996
6179
  p.log.step("Installing OpenCode MCP config...");
5997
6180
  ok = installMcp(force) && ok;
5998
6181
  }
5999
- if (!opts.opencodeOnly) {
6182
+ if (doClaude) {
6000
6183
  p.log.step("Installing Claude Code MCP config...");
6001
6184
  ok = installClaudeMcp(force) && ok;
6002
6185
  }
6186
+ if (doCodex) {
6187
+ p.log.step("Configuring Codex (MCP + hooks)...");
6188
+ ok = installCodex(force) && ok;
6189
+ p.log.info("Codex next steps:");
6190
+ p.log.info(" - Restart Codex to load the new configuration");
6191
+ p.log.info(" - On first run, approve the one-time prompt to trust the codemem hooks");
6192
+ p.log.info(" - MCP recall works immediately (no trust prompt required)");
6193
+ p.log.info(" - Disable prompt-time injection with CODEMEM_INJECT_CONTEXT=0");
6194
+ }
6003
6195
  if (ok) p.outro("Setup complete — restart your editor to load the plugin");
6004
6196
  else {
6005
6197
  p.outro("Setup completed with warnings");