engrm 0.4.37 → 0.4.39

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/cli.js CHANGED
@@ -18,10 +18,10 @@ var __export = (target, all) => {
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/cli.ts
21
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, statSync } from "fs";
22
- import { hostname as hostname2, homedir as homedir4, networkInterfaces as networkInterfaces2 } from "os";
23
- import { dirname as dirname5, join as join7 } from "path";
24
- import { createHash as createHash3 } from "crypto";
21
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, statSync } from "fs";
22
+ import { hostname as hostname3, homedir as homedir5, networkInterfaces as networkInterfaces3 } from "os";
23
+ import { dirname as dirname5, join as join8 } from "path";
24
+ import { createHash as createHash4 } from "crypto";
25
25
  import { fileURLToPath as fileURLToPath4 } from "url";
26
26
 
27
27
  // src/config.ts
@@ -104,6 +104,16 @@ function createDefaultConfig() {
104
104
  },
105
105
  transcript_analysis: {
106
106
  enabled: false
107
+ },
108
+ http: {
109
+ enabled: false,
110
+ port: 3767,
111
+ bearer_tokens: []
112
+ },
113
+ fleet: {
114
+ project_name: "shared-experience",
115
+ namespace: "",
116
+ api_key: ""
107
117
  }
108
118
  };
109
119
  }
@@ -165,6 +175,16 @@ function loadConfig() {
165
175
  },
166
176
  transcript_analysis: {
167
177
  enabled: asBool(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
178
+ },
179
+ http: {
180
+ enabled: asBool(config["http"]?.["enabled"], defaults.http.enabled),
181
+ port: asNumber(config["http"]?.["port"], defaults.http.port),
182
+ bearer_tokens: asStringArray(config["http"]?.["bearer_tokens"], defaults.http.bearer_tokens)
183
+ },
184
+ fleet: {
185
+ project_name: asString(config["fleet"]?.["project_name"], defaults.fleet.project_name),
186
+ namespace: asString(config["fleet"]?.["namespace"], defaults.fleet.namespace),
187
+ api_key: asString(config["fleet"]?.["api_key"], defaults.fleet.api_key)
168
188
  }
169
189
  };
170
190
  }
@@ -2583,7 +2603,7 @@ function errorPage(message) {
2583
2603
  }
2584
2604
 
2585
2605
  // src/register.ts
2586
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
2606
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, copyFileSync } from "node:fs";
2587
2607
  import { homedir as homedir2 } from "node:os";
2588
2608
  import { join as join2, dirname } from "node:path";
2589
2609
  import { fileURLToPath } from "node:url";
@@ -2591,6 +2611,8 @@ var CLAUDE_JSON = join2(homedir2(), ".claude.json");
2591
2611
  var CLAUDE_SETTINGS = join2(homedir2(), ".claude", "settings.json");
2592
2612
  var CODEX_CONFIG = join2(homedir2(), ".codex", "config.toml");
2593
2613
  var CODEX_HOOKS = join2(homedir2(), ".codex", "hooks.json");
2614
+ var OPENCODE_CONFIG = join2(homedir2(), ".config", "opencode", "opencode.json");
2615
+ var OPENCODE_PLUGIN = join2(homedir2(), ".config", "opencode", "plugins", "engrm.js");
2594
2616
  var LEGACY_CODEX_SERVER_NAME = `candengo-${"mem"}`;
2595
2617
  function isBuiltDist() {
2596
2618
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -2780,6 +2802,26 @@ function registerCodexHooks() {
2780
2802
  writeFileSync2(CODEX_HOOKS, content, "utf-8");
2781
2803
  return { path: CODEX_HOOKS, added: true };
2782
2804
  }
2805
+ function registerOpenCode() {
2806
+ const root = findPackageRoot();
2807
+ const pluginSource = join2(root, "opencode", "plugin", "engrm-opencode.js");
2808
+ const config = readJsonFile(OPENCODE_CONFIG);
2809
+ const mcp = config["mcp"] ?? {};
2810
+ mcp["engrm"] = {
2811
+ type: "local",
2812
+ command: ["engrm", "serve"],
2813
+ enabled: true,
2814
+ timeout: 5000
2815
+ };
2816
+ config["$schema"] = "https://opencode.ai/config.json";
2817
+ config["mcp"] = mcp;
2818
+ writeJsonFile(OPENCODE_CONFIG, config);
2819
+ ensureParentDir(OPENCODE_PLUGIN);
2820
+ if (existsSync2(pluginSource)) {
2821
+ copyFileSync(pluginSource, OPENCODE_PLUGIN);
2822
+ }
2823
+ return { path: OPENCODE_CONFIG, added: true, pluginPath: OPENCODE_PLUGIN };
2824
+ }
2783
2825
  function registerHooks() {
2784
2826
  const runtime = findRuntime();
2785
2827
  const root = findPackageRoot();
@@ -2830,6 +2872,7 @@ function registerAll() {
2830
2872
  let hooks = { path: CLAUDE_SETTINGS, added: false };
2831
2873
  let codex = { path: CODEX_CONFIG, added: false };
2832
2874
  let codexHooks = { path: CODEX_HOOKS, added: false };
2875
+ let opencode = { path: OPENCODE_CONFIG, added: false, pluginPath: OPENCODE_PLUGIN };
2833
2876
  try {
2834
2877
  mcp = registerMcpServer();
2835
2878
  } catch {}
@@ -2842,11 +2885,15 @@ function registerAll() {
2842
2885
  try {
2843
2886
  codexHooks = registerCodexHooks();
2844
2887
  } catch {}
2888
+ try {
2889
+ opencode = registerOpenCode();
2890
+ } catch {}
2845
2891
  return {
2846
2892
  mcp,
2847
2893
  hooks,
2848
2894
  codex,
2849
- codexHooks
2895
+ codexHooks,
2896
+ opencode
2850
2897
  };
2851
2898
  }
2852
2899
 
@@ -2990,6 +3037,12 @@ function containsSecrets(text, customPatterns = []) {
2990
3037
  }
2991
3038
  return false;
2992
3039
  }
3040
+ var FLEET_HOSTNAME_PATTERN = /\b(?=.{1,253}\b)(?!-)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}\b/gi;
3041
+ var FLEET_IP_PATTERN = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
3042
+ var FLEET_MAC_PATTERN = /\b(?:[0-9a-f]{2}[:-]){5}[0-9a-f]{2}\b/gi;
3043
+ function scrubFleetIdentifiers(text) {
3044
+ return text.replace(FLEET_MAC_PATTERN, "[REDACTED_MAC]").replace(FLEET_IP_PATTERN, "[REDACTED_IP]").replace(FLEET_HOSTNAME_PATTERN, "[REDACTED_HOSTNAME]");
3045
+ }
2993
3046
 
2994
3047
  // src/capture/quality.ts
2995
3048
  var QUALITY_THRESHOLD = 0.1;
@@ -3577,6 +3630,35 @@ function narrativesConflict(narrative1, narrative2) {
3577
3630
  return null;
3578
3631
  }
3579
3632
 
3633
+ // src/sync/targets.ts
3634
+ function isFleetProjectName(projectName, config) {
3635
+ const fleetProjectName = config.fleet?.project_name ?? "shared-experience";
3636
+ if (!projectName || !fleetProjectName)
3637
+ return false;
3638
+ return projectName.trim().toLowerCase() === fleetProjectName.trim().toLowerCase();
3639
+ }
3640
+ function hasFleetTarget(config) {
3641
+ return Boolean(config.fleet?.namespace?.trim() && config.fleet?.api_key?.trim() && (config.fleet?.project_name ?? "shared-experience").trim());
3642
+ }
3643
+ function resolveSyncTarget(config, projectName) {
3644
+ if (isFleetProjectName(projectName, config) && hasFleetTarget(config)) {
3645
+ return {
3646
+ key: `fleet:${config.fleet.namespace}`,
3647
+ apiKey: config.fleet.api_key,
3648
+ namespace: config.fleet.namespace,
3649
+ siteId: config.site_id,
3650
+ isFleet: true
3651
+ };
3652
+ }
3653
+ return {
3654
+ key: `default:${config.namespace}`,
3655
+ apiKey: config.candengo_api_key,
3656
+ namespace: config.namespace,
3657
+ siteId: config.site_id,
3658
+ isFleet: false
3659
+ };
3660
+ }
3661
+
3580
3662
  // src/tools/save.ts
3581
3663
  var VALID_TYPES = [
3582
3664
  "bugfix",
@@ -3625,7 +3707,8 @@ async function saveObservation(db, config, input) {
3625
3707
  const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
3626
3708
  const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
3627
3709
  const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
3628
- let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;
3710
+ const fleetProject = isFleetProjectName(project.name, config);
3711
+ let sensitivity = input.sensitivity ?? (fleetProject ? "shared" : config.scrubbing.default_sensitivity);
3629
3712
  if (config.scrubbing.enabled && containsSecrets([input.title, input.narrative, JSON.stringify(input.facts)].filter(Boolean).join(" "), customPatterns)) {
3630
3713
  if (sensitivity === "shared") {
3631
3714
  sensitivity = "personal";
@@ -3835,26 +3918,250 @@ async function installRulePacks(db, config, packNames) {
3835
3918
  }
3836
3919
 
3837
3920
  // src/tools/capture-status.ts
3838
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
3839
- import { homedir as homedir3 } from "node:os";
3921
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
3922
+ import { homedir as homedir4 } from "node:os";
3923
+ import { join as join7 } from "node:path";
3924
+
3925
+ // src/config.ts
3926
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
3927
+ import { homedir as homedir3, hostname as hostname2, networkInterfaces as networkInterfaces2 } from "node:os";
3840
3928
  import { join as join6 } from "node:path";
3929
+ import { createHash as createHash3 } from "node:crypto";
3930
+ var CONFIG_DIR2 = join6(homedir3(), ".engrm");
3931
+ var SETTINGS_PATH2 = join6(CONFIG_DIR2, "settings.json");
3932
+ var DB_PATH2 = join6(CONFIG_DIR2, "engrm.db");
3933
+ function getDbPath2() {
3934
+ return DB_PATH2;
3935
+ }
3936
+ function generateDeviceId2() {
3937
+ const host = hostname2().toLowerCase().replace(/[^a-z0-9-]/g, "");
3938
+ let mac = "";
3939
+ const ifaces = networkInterfaces2();
3940
+ for (const entries of Object.values(ifaces)) {
3941
+ if (!entries)
3942
+ continue;
3943
+ for (const entry of entries) {
3944
+ if (!entry.internal && entry.mac && entry.mac !== "00:00:00:00:00:00") {
3945
+ mac = entry.mac;
3946
+ break;
3947
+ }
3948
+ }
3949
+ if (mac)
3950
+ break;
3951
+ }
3952
+ const material = `${host}:${mac || "no-mac"}`;
3953
+ const suffix = createHash3("sha256").update(material).digest("hex").slice(0, 8);
3954
+ return `${host}-${suffix}`;
3955
+ }
3956
+ function createDefaultConfig2() {
3957
+ return {
3958
+ candengo_url: "",
3959
+ candengo_api_key: "",
3960
+ site_id: "",
3961
+ namespace: "",
3962
+ user_id: "",
3963
+ user_email: "",
3964
+ device_id: generateDeviceId2(),
3965
+ teams: [],
3966
+ sync: {
3967
+ enabled: true,
3968
+ interval_seconds: 30,
3969
+ batch_size: 50
3970
+ },
3971
+ search: {
3972
+ default_limit: 10,
3973
+ local_boost: 1.2,
3974
+ scope: "all"
3975
+ },
3976
+ scrubbing: {
3977
+ enabled: true,
3978
+ custom_patterns: [],
3979
+ default_sensitivity: "shared"
3980
+ },
3981
+ sentinel: {
3982
+ enabled: false,
3983
+ mode: "advisory",
3984
+ provider: "openai",
3985
+ model: "gpt-4o-mini",
3986
+ api_key: "",
3987
+ base_url: "",
3988
+ skip_patterns: [],
3989
+ daily_limit: 100,
3990
+ tier: "free"
3991
+ },
3992
+ observer: {
3993
+ enabled: true,
3994
+ mode: "per_event",
3995
+ model: "sonnet"
3996
+ },
3997
+ transcript_analysis: {
3998
+ enabled: false
3999
+ },
4000
+ http: {
4001
+ enabled: false,
4002
+ port: 3767,
4003
+ bearer_tokens: []
4004
+ },
4005
+ fleet: {
4006
+ project_name: "shared-experience",
4007
+ namespace: "",
4008
+ api_key: ""
4009
+ }
4010
+ };
4011
+ }
4012
+ function loadConfig2() {
4013
+ if (!existsSync6(SETTINGS_PATH2)) {
4014
+ throw new Error(`Config not found at ${SETTINGS_PATH2}. Run 'engrm init --manual' to configure.`);
4015
+ }
4016
+ const raw = readFileSync6(SETTINGS_PATH2, "utf-8");
4017
+ let parsed;
4018
+ try {
4019
+ parsed = JSON.parse(raw);
4020
+ } catch {
4021
+ throw new Error(`Invalid JSON in ${SETTINGS_PATH2}`);
4022
+ }
4023
+ if (typeof parsed !== "object" || parsed === null) {
4024
+ throw new Error(`Config at ${SETTINGS_PATH2} is not a JSON object`);
4025
+ }
4026
+ const config = parsed;
4027
+ const defaults = createDefaultConfig2();
4028
+ return {
4029
+ candengo_url: asString2(config["candengo_url"], defaults.candengo_url),
4030
+ candengo_api_key: asString2(config["candengo_api_key"], defaults.candengo_api_key),
4031
+ site_id: asString2(config["site_id"], defaults.site_id),
4032
+ namespace: asString2(config["namespace"], defaults.namespace),
4033
+ user_id: asString2(config["user_id"], defaults.user_id),
4034
+ user_email: asString2(config["user_email"], defaults.user_email),
4035
+ device_id: asString2(config["device_id"], defaults.device_id),
4036
+ teams: asTeams2(config["teams"], defaults.teams),
4037
+ sync: {
4038
+ enabled: asBool2(config["sync"]?.["enabled"], defaults.sync.enabled),
4039
+ interval_seconds: asNumber2(config["sync"]?.["interval_seconds"], defaults.sync.interval_seconds),
4040
+ batch_size: asNumber2(config["sync"]?.["batch_size"], defaults.sync.batch_size)
4041
+ },
4042
+ search: {
4043
+ default_limit: asNumber2(config["search"]?.["default_limit"], defaults.search.default_limit),
4044
+ local_boost: asNumber2(config["search"]?.["local_boost"], defaults.search.local_boost),
4045
+ scope: asScope2(config["search"]?.["scope"], defaults.search.scope)
4046
+ },
4047
+ scrubbing: {
4048
+ enabled: asBool2(config["scrubbing"]?.["enabled"], defaults.scrubbing.enabled),
4049
+ custom_patterns: asStringArray2(config["scrubbing"]?.["custom_patterns"], defaults.scrubbing.custom_patterns),
4050
+ default_sensitivity: asSensitivity2(config["scrubbing"]?.["default_sensitivity"], defaults.scrubbing.default_sensitivity)
4051
+ },
4052
+ sentinel: {
4053
+ enabled: asBool2(config["sentinel"]?.["enabled"], defaults.sentinel.enabled),
4054
+ mode: asSentinelMode2(config["sentinel"]?.["mode"], defaults.sentinel.mode),
4055
+ provider: asLlmProvider2(config["sentinel"]?.["provider"], defaults.sentinel.provider),
4056
+ model: asString2(config["sentinel"]?.["model"], defaults.sentinel.model),
4057
+ api_key: asString2(config["sentinel"]?.["api_key"], defaults.sentinel.api_key),
4058
+ base_url: asString2(config["sentinel"]?.["base_url"], defaults.sentinel.base_url),
4059
+ skip_patterns: asStringArray2(config["sentinel"]?.["skip_patterns"], defaults.sentinel.skip_patterns),
4060
+ daily_limit: asNumber2(config["sentinel"]?.["daily_limit"], defaults.sentinel.daily_limit),
4061
+ tier: asTier2(config["sentinel"]?.["tier"], defaults.sentinel.tier)
4062
+ },
4063
+ observer: {
4064
+ enabled: asBool2(config["observer"]?.["enabled"], defaults.observer.enabled),
4065
+ mode: asObserverMode2(config["observer"]?.["mode"], defaults.observer.mode),
4066
+ model: asString2(config["observer"]?.["model"], defaults.observer.model)
4067
+ },
4068
+ transcript_analysis: {
4069
+ enabled: asBool2(config["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
4070
+ },
4071
+ http: {
4072
+ enabled: asBool2(config["http"]?.["enabled"], defaults.http.enabled),
4073
+ port: asNumber2(config["http"]?.["port"], defaults.http.port),
4074
+ bearer_tokens: asStringArray2(config["http"]?.["bearer_tokens"], defaults.http.bearer_tokens)
4075
+ },
4076
+ fleet: {
4077
+ project_name: asString2(config["fleet"]?.["project_name"], defaults.fleet.project_name),
4078
+ namespace: asString2(config["fleet"]?.["namespace"], defaults.fleet.namespace),
4079
+ api_key: asString2(config["fleet"]?.["api_key"], defaults.fleet.api_key)
4080
+ }
4081
+ };
4082
+ }
4083
+ function saveConfig2(config) {
4084
+ if (!existsSync6(CONFIG_DIR2)) {
4085
+ mkdirSync3(CONFIG_DIR2, { recursive: true });
4086
+ }
4087
+ writeFileSync3(SETTINGS_PATH2, JSON.stringify(config, null, 2) + `
4088
+ `, "utf-8");
4089
+ }
4090
+ function configExists2() {
4091
+ return existsSync6(SETTINGS_PATH2);
4092
+ }
4093
+ function asString2(value, fallback) {
4094
+ return typeof value === "string" ? value : fallback;
4095
+ }
4096
+ function asNumber2(value, fallback) {
4097
+ return typeof value === "number" && !Number.isNaN(value) ? value : fallback;
4098
+ }
4099
+ function asBool2(value, fallback) {
4100
+ return typeof value === "boolean" ? value : fallback;
4101
+ }
4102
+ function asStringArray2(value, fallback) {
4103
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : fallback;
4104
+ }
4105
+ function asScope2(value, fallback) {
4106
+ if (value === "personal" || value === "team" || value === "all")
4107
+ return value;
4108
+ return fallback;
4109
+ }
4110
+ function asSensitivity2(value, fallback) {
4111
+ if (value === "shared" || value === "personal" || value === "secret")
4112
+ return value;
4113
+ return fallback;
4114
+ }
4115
+ function asSentinelMode2(value, fallback) {
4116
+ if (value === "advisory" || value === "blocking")
4117
+ return value;
4118
+ return fallback;
4119
+ }
4120
+ function asLlmProvider2(value, fallback) {
4121
+ if (value === "openai" || value === "anthropic" || value === "ollama" || value === "custom")
4122
+ return value;
4123
+ return fallback;
4124
+ }
4125
+ function asTier2(value, fallback) {
4126
+ if (value === "free" || value === "vibe" || value === "solo" || value === "pro" || value === "team" || value === "enterprise")
4127
+ return value;
4128
+ return fallback;
4129
+ }
4130
+ function asObserverMode2(value, fallback) {
4131
+ if (value === "per_event" || value === "per_session")
4132
+ return value;
4133
+ return fallback;
4134
+ }
4135
+ function asTeams2(value, fallback) {
4136
+ if (!Array.isArray(value))
4137
+ return fallback;
4138
+ return value.filter((t) => typeof t === "object" && t !== null && typeof t.id === "string" && typeof t.name === "string" && typeof t.namespace === "string");
4139
+ }
4140
+
4141
+ // src/tools/capture-status.ts
3841
4142
  var LEGACY_CODEX_SERVER_NAME2 = `candengo-${"mem"}`;
3842
4143
  function getCaptureStatus(db, input = {}) {
3843
4144
  const hours = Math.max(1, Math.min(input.lookback_hours ?? 24, 24 * 30));
3844
4145
  const sinceEpoch = Math.floor(Date.now() / 1000) - hours * 3600;
3845
- const home = input.home_dir ?? homedir3();
3846
- const claudeJson = join6(home, ".claude.json");
3847
- const claudeSettings = join6(home, ".claude", "settings.json");
3848
- const codexConfig = join6(home, ".codex", "config.toml");
3849
- const codexHooks = join6(home, ".codex", "hooks.json");
3850
- const claudeJsonContent = existsSync6(claudeJson) ? readFileSync6(claudeJson, "utf-8") : "";
3851
- const claudeSettingsContent = existsSync6(claudeSettings) ? readFileSync6(claudeSettings, "utf-8") : "";
3852
- const codexConfigContent = existsSync6(codexConfig) ? readFileSync6(codexConfig, "utf-8") : "";
3853
- const codexHooksContent = existsSync6(codexHooks) ? readFileSync6(codexHooks, "utf-8") : "";
4146
+ const home = input.home_dir ?? homedir4();
4147
+ const claudeJson = join7(home, ".claude.json");
4148
+ const claudeSettings = join7(home, ".claude", "settings.json");
4149
+ const codexConfig = join7(home, ".codex", "config.toml");
4150
+ const codexHooks = join7(home, ".codex", "hooks.json");
4151
+ const opencodeConfig = join7(home, ".config", "opencode", "opencode.json");
4152
+ const opencodePlugin = join7(home, ".config", "opencode", "plugins", "engrm.js");
4153
+ const config = configExists2() ? loadConfig2() : null;
4154
+ const claudeJsonContent = existsSync7(claudeJson) ? readFileSync7(claudeJson, "utf-8") : "";
4155
+ const claudeSettingsContent = existsSync7(claudeSettings) ? readFileSync7(claudeSettings, "utf-8") : "";
4156
+ const codexConfigContent = existsSync7(codexConfig) ? readFileSync7(codexConfig, "utf-8") : "";
4157
+ const codexHooksContent = existsSync7(codexHooks) ? readFileSync7(codexHooks, "utf-8") : "";
4158
+ const opencodeConfigContent = existsSync7(opencodeConfig) ? readFileSync7(opencodeConfig, "utf-8") : "";
3854
4159
  const claudeMcpRegistered = claudeJsonContent.includes('"engrm"');
3855
4160
  const claudeHooksRegistered = claudeSettingsContent.includes("engrm") || claudeSettingsContent.includes("session-start") || claudeSettingsContent.includes("user-prompt-submit");
3856
4161
  const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME2}]`);
3857
4162
  const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
4163
+ const opencodeMcpRegistered = opencodeConfigContent.includes('"engrm"') && opencodeConfigContent.includes('"type"') && opencodeConfigContent.includes('"local"');
4164
+ const opencodePluginRegistered = existsSync7(opencodePlugin);
3858
4165
  let claudeHookCount = 0;
3859
4166
  let claudeSessionStartHook = false;
3860
4167
  let claudeUserPromptHook = false;
@@ -3927,6 +4234,11 @@ function getCaptureStatus(db, input = {}) {
3927
4234
  return {
3928
4235
  schema_version: schemaVersion,
3929
4236
  schema_current: schemaVersion >= LATEST_SCHEMA_VERSION,
4237
+ http_enabled: Boolean(config?.http?.enabled || process.env.ENGRM_HTTP_PORT),
4238
+ http_port: config?.http?.port ?? (process.env.ENGRM_HTTP_PORT ? Number(process.env.ENGRM_HTTP_PORT) : null),
4239
+ http_bearer_token_count: config?.http?.bearer_tokens?.length ?? 0,
4240
+ fleet_project_name: config?.fleet?.project_name ?? null,
4241
+ fleet_configured: Boolean(config?.fleet?.namespace && config?.fleet?.api_key),
3930
4242
  claude_mcp_registered: claudeMcpRegistered,
3931
4243
  claude_hooks_registered: claudeHooksRegistered,
3932
4244
  claude_hook_count: claudeHookCount,
@@ -3939,6 +4251,8 @@ function getCaptureStatus(db, input = {}) {
3939
4251
  codex_session_start_hook: codexSessionStartHook,
3940
4252
  codex_stop_hook: codexStopHook,
3941
4253
  codex_raw_chronology_supported: false,
4254
+ opencode_mcp_registered: opencodeMcpRegistered,
4255
+ opencode_plugin_registered: opencodePluginRegistered,
3942
4256
  recent_user_prompts: recentUserPrompts,
3943
4257
  recent_tool_events: recentToolEvents,
3944
4258
  recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
@@ -4096,7 +4410,7 @@ async function initWithToken(baseUrl, token) {
4096
4410
  try {
4097
4411
  const result = await provision(baseUrl, {
4098
4412
  token,
4099
- device_name: hostname2()
4413
+ device_name: hostname3()
4100
4414
  });
4101
4415
  writeConfigFromProvision(baseUrl, result);
4102
4416
  console.log(`
@@ -4122,7 +4436,7 @@ async function initWithBrowser(baseUrl) {
4122
4436
  console.log("Exchanging authorization code...");
4123
4437
  const result = await provision(baseUrl, {
4124
4438
  code,
4125
- device_name: hostname2()
4439
+ device_name: hostname3()
4126
4440
  });
4127
4441
  writeConfigFromProvision(baseUrl, result);
4128
4442
  console.log(`
@@ -4159,7 +4473,7 @@ function writeConfigFromProvision(baseUrl, result) {
4159
4473
  namespace: result.namespace,
4160
4474
  user_id: result.user_id,
4161
4475
  user_email: result.user_email,
4162
- device_id: existingDeviceId || generateDeviceId2(),
4476
+ device_id: existingDeviceId || generateDeviceId3(),
4163
4477
  teams: result.teams ?? [],
4164
4478
  sync: {
4165
4479
  enabled: true,
@@ -4203,13 +4517,13 @@ function writeConfigFromProvision(baseUrl, result) {
4203
4517
  console.log(`Database initialised at ${getDbPath()}`);
4204
4518
  }
4205
4519
  function initFromFile(configPath) {
4206
- if (!existsSync7(configPath)) {
4520
+ if (!existsSync8(configPath)) {
4207
4521
  console.error(`Config file not found: ${configPath}`);
4208
4522
  process.exit(1);
4209
4523
  }
4210
4524
  let parsed;
4211
4525
  try {
4212
- const raw = readFileSync7(configPath, "utf-8");
4526
+ const raw = readFileSync8(configPath, "utf-8");
4213
4527
  parsed = JSON.parse(raw);
4214
4528
  } catch {
4215
4529
  console.error(`Invalid JSON in ${configPath}`);
@@ -4241,7 +4555,7 @@ function initFromFile(configPath) {
4241
4555
  namespace: input["namespace"].trim(),
4242
4556
  user_id: input["user_id"].trim(),
4243
4557
  user_email: typeof input["user_email"] === "string" ? input["user_email"].trim() : "",
4244
- device_id: typeof input["device_id"] === "string" ? input["device_id"] : generateDeviceId2(),
4558
+ device_id: typeof input["device_id"] === "string" ? input["device_id"] : generateDeviceId3(),
4245
4559
  teams: [],
4246
4560
  sync: {
4247
4561
  enabled: true,
@@ -4314,7 +4628,7 @@ async function initManual() {
4314
4628
  namespace: namespace.trim(),
4315
4629
  user_id: userId.trim(),
4316
4630
  user_email: userEmail.trim(),
4317
- device_id: generateDeviceId2(),
4631
+ device_id: generateDeviceId3(),
4318
4632
  teams: [],
4319
4633
  sync: {
4320
4634
  enabled: true,
@@ -4391,17 +4705,26 @@ function handleStatus() {
4391
4705
  Integration`);
4392
4706
  console.log(` Server: ${config.candengo_url ? normalizeBaseUrl(config.candengo_url) : "(not set)"}`);
4393
4707
  console.log(` Sync: ${config.sync.enabled ? "enabled" : "disabled"}`);
4394
- const claudeJson = join7(homedir4(), ".claude.json");
4395
- const claudeSettings = join7(homedir4(), ".claude", "settings.json");
4396
- const codexConfig = join7(homedir4(), ".codex", "config.toml");
4397
- const codexHooks = join7(homedir4(), ".codex", "hooks.json");
4398
- const mcpRegistered = existsSync7(claudeJson) && readFileSync7(claudeJson, "utf-8").includes('"engrm"');
4399
- const settingsContent = existsSync7(claudeSettings) ? readFileSync7(claudeSettings, "utf-8") : "";
4400
- const codexContent = existsSync7(codexConfig) ? readFileSync7(codexConfig, "utf-8") : "";
4401
- const codexHooksContent = existsSync7(codexHooks) ? readFileSync7(codexHooks, "utf-8") : "";
4708
+ console.log(` HTTP MCP: ${config.http.enabled ? `enabled (:${config.http.port})` : "disabled"}`);
4709
+ console.log(` HTTP tokens: ${config.http.bearer_tokens.length}`);
4710
+ console.log(` Fleet project: ${config.fleet.project_name || "(not set)"}`);
4711
+ console.log(` Fleet sync: ${config.fleet.namespace && config.fleet.api_key ? "configured" : "not configured"}`);
4712
+ const claudeJson = join8(homedir5(), ".claude.json");
4713
+ const claudeSettings = join8(homedir5(), ".claude", "settings.json");
4714
+ const codexConfig = join8(homedir5(), ".codex", "config.toml");
4715
+ const codexHooks = join8(homedir5(), ".codex", "hooks.json");
4716
+ const opencodeConfig = join8(homedir5(), ".config", "opencode", "opencode.json");
4717
+ const opencodePlugin = join8(homedir5(), ".config", "opencode", "plugins", "engrm.js");
4718
+ const mcpRegistered = existsSync8(claudeJson) && readFileSync8(claudeJson, "utf-8").includes('"engrm"');
4719
+ const settingsContent = existsSync8(claudeSettings) ? readFileSync8(claudeSettings, "utf-8") : "";
4720
+ const codexContent = existsSync8(codexConfig) ? readFileSync8(codexConfig, "utf-8") : "";
4721
+ const codexHooksContent = existsSync8(codexHooks) ? readFileSync8(codexHooks, "utf-8") : "";
4722
+ const opencodeConfigContent = existsSync8(opencodeConfig) ? readFileSync8(opencodeConfig, "utf-8") : "";
4402
4723
  const hooksRegistered = settingsContent.includes("engrm") || settingsContent.includes("session-start") || settingsContent.includes("user-prompt-submit");
4403
4724
  const codexRegistered = codexContent.includes("[mcp_servers.engrm]") || codexContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME3}]`);
4404
4725
  const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
4726
+ const opencodeRegistered = opencodeConfigContent.includes('"engrm"') && opencodeConfigContent.includes('"local"');
4727
+ const opencodePluginRegistered = existsSync8(opencodePlugin);
4405
4728
  let hookCount = 0;
4406
4729
  if (hooksRegistered) {
4407
4730
  try {
@@ -4421,8 +4744,10 @@ function handleStatus() {
4421
4744
  }
4422
4745
  console.log(` MCP server: ${mcpRegistered ? "registered" : "not registered"}`);
4423
4746
  console.log(` Codex MCP: ${codexRegistered ? "registered" : "not registered"}`);
4747
+ console.log(` OpenCode MCP: ${opencodeRegistered ? "registered" : "not registered"}`);
4424
4748
  console.log(` Hooks: ${hooksRegistered ? `registered (${hookCount || "?"} hooks)` : "not registered"}`);
4425
4749
  console.log(` Codex hooks: ${codexHooksRegistered ? "registered (2 hooks)" : "not registered"}`);
4750
+ console.log(` OpenCode plug: ${opencodePluginRegistered ? "registered" : "not registered"}`);
4426
4751
  if (config.sentinel?.enabled) {
4427
4752
  console.log(`
4428
4753
  Sentinel`);
@@ -4431,7 +4756,7 @@ function handleStatus() {
4431
4756
  if (config.sentinel.provider) {
4432
4757
  console.log(` Provider: ${config.sentinel.provider}${config.sentinel.model ? ` (${config.sentinel.model})` : ""}`);
4433
4758
  }
4434
- if (existsSync7(getDbPath())) {
4759
+ if (existsSync8(getDbPath())) {
4435
4760
  try {
4436
4761
  const db = new MemDatabase(getDbPath());
4437
4762
  const todayStart = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000);
@@ -4444,7 +4769,7 @@ function handleStatus() {
4444
4769
  console.log(`
4445
4770
  Sentinel: disabled`);
4446
4771
  }
4447
- if (existsSync7(getDbPath())) {
4772
+ if (existsSync8(getDbPath())) {
4448
4773
  try {
4449
4774
  const db = new MemDatabase(getDbPath());
4450
4775
  const obsCount = db.getActiveObservationCount();
@@ -4549,8 +4874,10 @@ function handleStatus() {
4549
4874
  Files`);
4550
4875
  console.log(` Config: ${getSettingsPath()}`);
4551
4876
  console.log(` Database: ${getDbPath()}`);
4552
- console.log(` Codex config: ${join7(homedir4(), ".codex", "config.toml")}`);
4553
- console.log(` Codex hooks: ${join7(homedir4(), ".codex", "hooks.json")}`);
4877
+ console.log(` Codex config: ${join8(homedir5(), ".codex", "config.toml")}`);
4878
+ console.log(` Codex hooks: ${join8(homedir5(), ".codex", "hooks.json")}`);
4879
+ console.log(` OpenCode cfg: ${join8(homedir5(), ".config", "opencode", "opencode.json")}`);
4880
+ console.log(` OpenCode plug: ${join8(homedir5(), ".config", "opencode", "plugins", "engrm.js")}`);
4554
4881
  }
4555
4882
  function formatTimeAgo(epoch) {
4556
4883
  const ago = Math.floor(Date.now() / 1000) - epoch;
@@ -4572,14 +4899,14 @@ function formatSyncTime(epochStr) {
4572
4899
  }
4573
4900
  function ensureConfigDir() {
4574
4901
  const dir = getConfigDir();
4575
- if (!existsSync7(dir)) {
4576
- mkdirSync3(dir, { recursive: true });
4902
+ if (!existsSync8(dir)) {
4903
+ mkdirSync4(dir, { recursive: true });
4577
4904
  }
4578
4905
  }
4579
- function generateDeviceId2() {
4580
- const host = hostname2().toLowerCase().replace(/[^a-z0-9-]/g, "");
4906
+ function generateDeviceId3() {
4907
+ const host = hostname3().toLowerCase().replace(/[^a-z0-9-]/g, "");
4581
4908
  let mac = "";
4582
- const ifaces = networkInterfaces2();
4909
+ const ifaces = networkInterfaces3();
4583
4910
  for (const entries of Object.values(ifaces)) {
4584
4911
  if (!entries)
4585
4912
  continue;
@@ -4593,7 +4920,7 @@ function generateDeviceId2() {
4593
4920
  break;
4594
4921
  }
4595
4922
  const material = `${host}:${mac || "no-mac"}`;
4596
- const suffix = createHash3("sha256").update(material).digest("hex").slice(0, 8);
4923
+ const suffix = createHash4("sha256").update(material).digest("hex").slice(0, 8);
4597
4924
  return `${host}-${suffix}`;
4598
4925
  }
4599
4926
  async function handleInstallPack(flags) {
@@ -4723,6 +5050,22 @@ async function handleDoctor() {
4723
5050
  printDoctorReport(results);
4724
5051
  return;
4725
5052
  }
5053
+ if (config.http.enabled) {
5054
+ if (config.http.bearer_tokens.length > 0) {
5055
+ pass(`HTTP MCP enabled on port ${config.http.port} with ${config.http.bearer_tokens.length} bearer token(s)`);
5056
+ } else {
5057
+ warn("HTTP MCP is enabled but no bearer tokens are configured");
5058
+ }
5059
+ } else {
5060
+ info("HTTP MCP disabled");
5061
+ }
5062
+ if (config.fleet.project_name) {
5063
+ if (config.fleet.namespace && config.fleet.api_key) {
5064
+ pass(`Fleet project '${config.fleet.project_name}' is configured`);
5065
+ } else {
5066
+ info(`Fleet project '${config.fleet.project_name}' is reserved but not fully configured`);
5067
+ }
5068
+ }
4726
5069
  let db = null;
4727
5070
  try {
4728
5071
  db = new MemDatabase(getDbPath());
@@ -4742,10 +5085,10 @@ async function handleDoctor() {
4742
5085
  } catch {
4743
5086
  warn("Could not check database schema version");
4744
5087
  }
4745
- const claudeJson = join7(homedir4(), ".claude.json");
5088
+ const claudeJson = join8(homedir5(), ".claude.json");
4746
5089
  try {
4747
- if (existsSync7(claudeJson)) {
4748
- const content = readFileSync7(claudeJson, "utf-8");
5090
+ if (existsSync8(claudeJson)) {
5091
+ const content = readFileSync8(claudeJson, "utf-8");
4749
5092
  if (content.includes('"engrm"')) {
4750
5093
  pass("MCP server registered in Claude Code");
4751
5094
  } else {
@@ -4757,10 +5100,10 @@ async function handleDoctor() {
4757
5100
  } catch {
4758
5101
  warn("Could not check MCP server registration");
4759
5102
  }
4760
- const claudeSettings = join7(homedir4(), ".claude", "settings.json");
5103
+ const claudeSettings = join8(homedir5(), ".claude", "settings.json");
4761
5104
  try {
4762
- if (existsSync7(claudeSettings)) {
4763
- const content = readFileSync7(claudeSettings, "utf-8");
5105
+ if (existsSync8(claudeSettings)) {
5106
+ const content = readFileSync8(claudeSettings, "utf-8");
4764
5107
  let hookCount = 0;
4765
5108
  let hasSessionStart = false;
4766
5109
  let hasUserPrompt = false;
@@ -4806,10 +5149,10 @@ async function handleDoctor() {
4806
5149
  } catch {
4807
5150
  warn("Could not check hooks registration");
4808
5151
  }
4809
- const codexConfig = join7(homedir4(), ".codex", "config.toml");
5152
+ const codexConfig = join8(homedir5(), ".codex", "config.toml");
4810
5153
  try {
4811
- if (existsSync7(codexConfig)) {
4812
- const content = readFileSync7(codexConfig, "utf-8");
5154
+ if (existsSync8(codexConfig)) {
5155
+ const content = readFileSync8(codexConfig, "utf-8");
4813
5156
  if (content.includes("[mcp_servers.engrm]") || content.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME3}]`)) {
4814
5157
  pass("MCP server registered in Codex");
4815
5158
  } else {
@@ -4821,10 +5164,10 @@ async function handleDoctor() {
4821
5164
  } catch {
4822
5165
  warn("Could not check Codex MCP registration");
4823
5166
  }
4824
- const codexHooks = join7(homedir4(), ".codex", "hooks.json");
5167
+ const codexHooks = join8(homedir5(), ".codex", "hooks.json");
4825
5168
  try {
4826
- if (existsSync7(codexHooks)) {
4827
- const content = readFileSync7(codexHooks, "utf-8");
5169
+ if (existsSync8(codexHooks)) {
5170
+ const content = readFileSync8(codexHooks, "utf-8");
4828
5171
  if (content.includes('"SessionStart"') && content.includes('"Stop"')) {
4829
5172
  pass("Hooks registered in Codex");
4830
5173
  } else {
@@ -4836,6 +5179,27 @@ async function handleDoctor() {
4836
5179
  } catch {
4837
5180
  warn("Could not check Codex hooks registration");
4838
5181
  }
5182
+ const opencodeConfig = join8(homedir5(), ".config", "opencode", "opencode.json");
5183
+ try {
5184
+ if (existsSync8(opencodeConfig)) {
5185
+ const content = readFileSync8(opencodeConfig, "utf-8");
5186
+ if (content.includes('"engrm"') && content.includes('"local"')) {
5187
+ pass("MCP server registered in OpenCode");
5188
+ } else {
5189
+ warn("MCP server not registered in OpenCode");
5190
+ }
5191
+ } else {
5192
+ warn("OpenCode config not found (~/.config/opencode/opencode.json)");
5193
+ }
5194
+ } catch {
5195
+ warn("Could not check OpenCode MCP registration");
5196
+ }
5197
+ const opencodePlugin = join8(homedir5(), ".config", "opencode", "plugins", "engrm.js");
5198
+ if (existsSync8(opencodePlugin)) {
5199
+ pass("Plugin installed in OpenCode");
5200
+ } else {
5201
+ warn("OpenCode plugin not installed (~/.config/opencode/plugins/engrm.js)");
5202
+ }
4839
5203
  if (config.candengo_url) {
4840
5204
  try {
4841
5205
  const baseUrl = normalizeBaseUrl(config.candengo_url);
@@ -4946,7 +5310,7 @@ async function handleDoctor() {
4946
5310
  }
4947
5311
  try {
4948
5312
  const dbPath = getDbPath();
4949
- if (existsSync7(dbPath)) {
5313
+ if (existsSync8(dbPath)) {
4950
5314
  const stats = statSync(dbPath);
4951
5315
  const sizeMB = stats.size / (1024 * 1024);
4952
5316
  const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${(stats.size / 1024).toFixed(0)} KB`;
@@ -5011,16 +5375,18 @@ Registering with Claude Code and Codex...`);
5011
5375
  console.log(` Claude hooks registered \u2192 ${result.hooks.path}`);
5012
5376
  console.log(` Codex MCP registered \u2192 ${result.codex.path}`);
5013
5377
  console.log(` Codex hooks registered \u2192 ${result.codexHooks.path}`);
5378
+ console.log(` OpenCode MCP registered \u2192 ${result.opencode.path}`);
5379
+ console.log(` OpenCode plugin installed \u2192 ${result.opencode.pluginPath}`);
5014
5380
  console.log(`
5015
- Engrm is ready! Start a new Claude Code or Codex session to use memory.`);
5381
+ Engrm is ready! Start a new Claude Code, Codex, or OpenCode session to use memory.`);
5016
5382
  } catch (error) {
5017
- const packageRoot = join7(THIS_DIR, "..");
5383
+ const packageRoot = join8(THIS_DIR, "..");
5018
5384
  const runtime = IS_BUILT_DIST ? process.execPath : "bun";
5019
- const serverArgs = IS_BUILT_DIST ? [join7(packageRoot, "dist", "server.js")] : ["run", join7(packageRoot, "src", "server.ts")];
5020
- const sessionStartCommand = IS_BUILT_DIST ? `${process.execPath} ${join7(packageRoot, "dist", "hooks", "session-start.js")}` : `bun run ${join7(packageRoot, "hooks", "session-start.ts")}`;
5021
- const codexStopCommand = IS_BUILT_DIST ? `${process.execPath} ${join7(packageRoot, "dist", "hooks", "codex-stop.js")}` : `bun run ${join7(packageRoot, "hooks", "codex-stop.ts")}`;
5385
+ const serverArgs = IS_BUILT_DIST ? [join8(packageRoot, "dist", "server.js")] : ["run", join8(packageRoot, "src", "server.ts")];
5386
+ const sessionStartCommand = IS_BUILT_DIST ? `${process.execPath} ${join8(packageRoot, "dist", "hooks", "session-start.js")}` : `bun run ${join8(packageRoot, "hooks", "session-start.ts")}`;
5387
+ const codexStopCommand = IS_BUILT_DIST ? `${process.execPath} ${join8(packageRoot, "dist", "hooks", "codex-stop.js")}` : `bun run ${join8(packageRoot, "hooks", "codex-stop.ts")}`;
5022
5388
  console.log(`
5023
- Could not auto-register with Claude Code and Codex.`);
5389
+ Could not auto-register with Claude Code, Codex, and OpenCode.`);
5024
5390
  console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
5025
5391
  console.log(`
5026
5392
  Manual setup \u2014 add to ~/.claude.json:`);
@@ -5074,6 +5440,24 @@ And add to ~/.codex/hooks.json:`);
5074
5440
  }
5075
5441
  }
5076
5442
  `);
5443
+ console.log(`
5444
+ And add to ~/.config/opencode/opencode.json:`);
5445
+ console.log(`
5446
+ {
5447
+ "$schema": "https://opencode.ai/config.json",
5448
+ "mcp": {
5449
+ "engrm": {
5450
+ "type": "local",
5451
+ "command": ["engrm", "serve"],
5452
+ "enabled": true,
5453
+ "timeout": 5000
5454
+ }
5455
+ }
5456
+ }
5457
+ `);
5458
+ console.log(`
5459
+ And copy the OpenCode plugin file to ~/.config/opencode/plugins/engrm.js:`);
5460
+ console.log(` ${join8(packageRoot, "opencode", "plugin", "engrm-opencode.js")}`);
5077
5461
  }
5078
5462
  }
5079
5463
  function formatTomlArray(values) {