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 +197 -5
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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");
|