@memorax/memorax-code 0.1.4 → 0.1.6

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.
Files changed (87) hide show
  1. package/README.md +8 -19
  2. package/bin/memorax-code-npm-preinstall.mjs +28 -39
  3. package/bin/memorax-code-plugin-postinstall.mjs +23 -1537
  4. package/bin/memorax-code-setup.mjs +1615 -0
  5. package/bin/memorax-code.mjs +289 -5
  6. package/docs/configuration.md +66 -17
  7. package/docs/troubleshooting.md +105 -7
  8. package/lib/memorax-code-adapter-common/src/config-utils.d.mts +5 -1
  9. package/lib/memorax-code-adapter-common/src/config-utils.mjs +36 -3
  10. package/lib/memorax-code-adapter-common/src/credentials/linux-secret-service.mjs +110 -0
  11. package/lib/memorax-code-adapter-common/src/credentials/macos-keychain.mjs +224 -0
  12. package/lib/memorax-code-adapter-common/src/credentials/secure-command.mjs +340 -0
  13. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-record.d.mts +82 -0
  14. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-record.mjs +183 -0
  15. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-store.d.mts +82 -0
  16. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-store.mjs +271 -0
  17. package/lib/memorax-code-adapter-common/src/credentials/windows-dpapi.mjs +440 -0
  18. package/lib/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +7 -4
  19. package/lib/memorax-code-adapter-common/src/setup-completion.d.mts +81 -0
  20. package/lib/memorax-code-adapter-common/src/setup-completion.mjs +154 -0
  21. package/lib/memorax-code-backend/dist/clients/claude/memory-hook-runtime.js +21 -2
  22. package/lib/memorax-code-backend/dist/clients/codex/memory-hook-runtime.js +23 -3
  23. package/lib/memorax-code-backend/dist/clients/opencode/memory-hook-runtime.js +19 -1
  24. package/lib/memorax-code-backend/dist/entrypoints/backend-cli.js +17 -16
  25. package/lib/memorax-code-backend/dist/lifecycle/backend/process.js +21 -0
  26. package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +81 -22
  27. package/lib/memorax-code-backend/dist/memorax-cli.js +3 -0
  28. package/lib/memorax-code-backend/dist/memory/automatic-retrieval.js +8 -2
  29. package/lib/memorax-code-backend/dist/memory/automatic-writeback.js +13 -1
  30. package/lib/memorax-code-backend/dist/memory/cli.js +10 -0
  31. package/lib/memorax-code-backend/dist/memory/quota-notice.js +208 -0
  32. package/lib/memorax-code-backend/dist/memory/service.js +11 -0
  33. package/lib/memorax-code-backend/dist/provider/memorax/adapter.js +4 -2
  34. package/lib/memorax-code-backend/dist/provider/memorax/http.js +10 -3
  35. package/lib/memorax-code-backend/dist/provider/memorax/quota.js +26 -0
  36. package/lib/memorax-code-backend/package.json +2 -2
  37. package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  38. package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  39. package/lib/memorax-code-claude-adapter/package.json +1 -1
  40. package/lib/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -3
  41. package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  42. package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  43. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  44. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  45. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.d.mts +5 -1
  46. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.mjs +36 -3
  47. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/linux-secret-service.mjs +110 -0
  48. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/macos-keychain.mjs +224 -0
  49. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/secure-command.mjs +340 -0
  50. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-record.d.mts +82 -0
  51. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-record.mjs +183 -0
  52. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-store.d.mts +82 -0
  53. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-store.mjs +271 -0
  54. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/windows-dpapi.mjs +440 -0
  55. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +7 -4
  56. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/setup-completion.d.mts +81 -0
  57. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/setup-completion.mjs +154 -0
  58. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
  59. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -3
  60. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  61. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  62. package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
  63. package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
  64. package/lib/memorax-code-codex-adapter/package.json +1 -1
  65. package/lib/memorax-code-codex-adapter/runtime-hooks/memory-skill-reminder.mjs +2 -0
  66. package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  67. package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  68. package/lib/memorax-code-dsh-adapter/hooks/repo-memory-job.mjs +5 -2
  69. package/lib/memorax-code-dsh-adapter/package.json +1 -1
  70. package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  71. package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  72. package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +158 -7
  73. package/lib/memorax-code-dsh-adapter/src/runtime-state.mjs +21 -0
  74. package/lib/memorax-code-opencode-adapter/package.json +1 -1
  75. package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  76. package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  77. package/lib/memorax-code-opencode-adapter/src/plugin.mjs +20 -0
  78. package/lib/node-version.mjs +1 -1
  79. package/lib/package-transition.mjs +311 -0
  80. package/lib/resolve-codex-command.mjs +32 -6
  81. package/lib/setup-memory-preferences.mjs +113 -0
  82. package/lib/setup-reconcile.mjs +128 -0
  83. package/lib/trial-plugin-mark.mjs +171 -0
  84. package/lib/trial-provision-client.mjs +307 -0
  85. package/lib/trial-provision-flow.mjs +215 -0
  86. package/lib/trial-setup.mjs +53 -0
  87. package/package.json +2 -2
@@ -6,6 +6,7 @@ import {
6
6
  lstatSync,
7
7
  mkdirSync,
8
8
  readFileSync,
9
+ realpathSync,
9
10
  readdirSync,
10
11
  renameSync,
11
12
  rmSync,
@@ -22,6 +23,7 @@ import {
22
23
  parseDshVersion,
23
24
  } from "./dsh-version.mjs";
24
25
  import {
26
+ buildDshCommand,
25
27
  requireDshRuntimeAuthority,
26
28
  requireEnabledDshRuntime,
27
29
  } from "./runtime-state.mjs";
@@ -49,6 +51,7 @@ const { resolveWindowsCliInvocation } = await import(
49
51
 
50
52
  const STATE_VERSION = 1;
51
53
  const RUNTIME = "dsh";
54
+ const DSH_PACKAGE_NAME = "@deepseek-ai/dsh";
52
55
  const ADAPTER_PACKAGE_NAME = "@memorax-code/dsh-memorax-code";
53
56
  const HEADLESS_PROFILE_NAME = "headless";
54
57
  const HEADLESS_BUNDLE_NAME = "@deepseek-ai/dsh-headless";
@@ -160,14 +163,18 @@ export function collectDshAdapterStatus(options = {}) {
160
163
  return { ok: true, ...base, skipped: true, reason: "no_existing_profiles" };
161
164
  }
162
165
 
163
- const dshCommand = resolveDshCommand(options, paths, state);
164
- const compatibility = inspectDshCompatibility(options, paths, dshCommand);
166
+ const { dshCommand, compatibility } = resolveDshStatusCompatibility(
167
+ options,
168
+ paths,
169
+ state,
170
+ );
165
171
  const version = compatibility.dshVersion;
166
172
  const versionStatus = {
167
173
  dshVersionTested: compatibility.dshVersionTested,
168
174
  testedDshVersions: [...DSH_TESTED_VERSIONS],
169
175
  };
170
- if (compatibility.reason === "dsh_version_unavailable") {
176
+ if (compatibility.reason === "dsh_version_unavailable"
177
+ || compatibility.reason === "dsh_profile_runtime_stale") {
171
178
  return {
172
179
  ok: false,
173
180
  ...base,
@@ -327,8 +334,7 @@ function ensureDshPluginInstalledUnlocked(paths, options) {
327
334
  };
328
335
  }
329
336
 
330
- const dshCommand = resolveDshCommand(options, paths, state);
331
- const compatibility = inspectDshCompatibility(options, paths, dshCommand);
337
+ const { dshCommand, compatibility } = resolveDshCompatibility(options, paths, state);
332
338
  if (compatibility.compatible !== true) {
333
339
  const nextState = state?.enabled === true
334
340
  ? disabledState(state, state.profiles)
@@ -805,7 +811,13 @@ function disableDshPluginInstallationUnlocked(paths, options, removeState) {
805
811
  };
806
812
  }
807
813
 
808
- function removeProfileAdapterPackages(paths, options, name, packageNames, dshCommand) {
814
+ function removeProfileAdapterPackages(
815
+ paths,
816
+ options,
817
+ name,
818
+ packageNames,
819
+ dshCommand,
820
+ ) {
809
821
  const profilePath = join(paths.profilesRoot, name);
810
822
  let profile = inspectProfile(name, profilePath);
811
823
  let failure;
@@ -1049,14 +1061,153 @@ function resolveDshCommand(options, paths, state) {
1049
1061
  ?? nonEmpty(paths.env.MEMORAX_CODE_DSH_COMMAND)
1050
1062
  ?? nonEmpty(state?.dshCommand)
1051
1063
  ?? "dsh";
1064
+ const normalized = normalizeDshCommand(command);
1065
+ if (normalized) return normalized;
1066
+ const profileRuntime = inspectProfileDshRuntime(paths, state);
1067
+ return profileRuntime?.compatibility.compatible === true
1068
+ ? profileRuntime.dshCommand
1069
+ : "dsh";
1070
+ }
1071
+
1072
+ function resolveDshStatusCompatibility(options, paths, state) {
1073
+ if (state?.enabled !== true) return resolveDshCompatibility(options, paths, state);
1074
+ const dshCommand = normalizeDshCommand(state.dshCommand);
1075
+ if (!dshCommand) {
1076
+ return {
1077
+ dshCommand: state.dshCommand,
1078
+ compatibility: unavailableDshCompatibility("dsh_version_unavailable"),
1079
+ };
1080
+ }
1081
+ const profileRuntime = inspectProfileDshRuntime(paths, state);
1082
+ return profileRuntime?.dshCommand === dshCommand
1083
+ ? profileRuntime
1084
+ : { dshCommand, compatibility: inspectDshCompatibility(options, paths, dshCommand) };
1085
+ }
1086
+
1087
+ function resolveDshCompatibility(options, paths, state) {
1088
+ const configured = nonEmpty(options.dshCommand)
1089
+ ?? nonEmpty(paths.env.MEMORAX_CODE_DSH_COMMAND);
1090
+ if (configured) {
1091
+ const dshCommand = normalizeDshCommand(configured);
1092
+ return dshCommand
1093
+ ? { dshCommand, compatibility: inspectDshCompatibility(options, paths, dshCommand) }
1094
+ : {
1095
+ dshCommand: configured,
1096
+ compatibility: unavailableDshCompatibility("dsh_version_unavailable"),
1097
+ };
1098
+ }
1099
+
1100
+ const profileRuntime = inspectProfileDshRuntime(paths, state);
1101
+ const attemptedCommands = new Set();
1102
+ let unavailable;
1103
+ for (const value of [state?.dshCommand, "dsh"]) {
1104
+ const dshCommand = normalizeDshCommand(value);
1105
+ if (!dshCommand || attemptedCommands.has(dshCommand)) continue;
1106
+ attemptedCommands.add(dshCommand);
1107
+ if (dshCommand === profileRuntime?.dshCommand) {
1108
+ if (profileRuntime.compatibility.compatible === true) return profileRuntime;
1109
+ continue;
1110
+ }
1111
+ const compatibility = inspectDshCompatibility(options, paths, dshCommand);
1112
+ if (compatibility.compatible === true) return { dshCommand, compatibility };
1113
+ unavailable ??= { dshCommand, compatibility };
1114
+ }
1115
+ if (profileRuntime?.compatibility.compatible === true) return profileRuntime;
1116
+ if (profileRuntime?.compatibility.reason === "dsh_profile_runtime_stale") {
1117
+ return {
1118
+ dshCommand: profileRuntime.dshCommand ?? unavailable?.dshCommand ?? "dsh",
1119
+ compatibility: profileRuntime.compatibility,
1120
+ };
1121
+ }
1122
+ return unavailable ?? {
1123
+ dshCommand: "dsh",
1124
+ compatibility: unavailableDshCompatibility("dsh_version_unavailable"),
1125
+ };
1126
+ }
1127
+
1128
+ function normalizeDshCommand(value) {
1129
+ const command = nonEmpty(value);
1130
+ if (!command) return undefined;
1131
+ const name = command
1132
+ .split(/[\\/]/)
1133
+ .at(-1)
1134
+ .replace(/\.(?:cmd|bat|exe|com)$/i, "")
1135
+ .toLowerCase();
1136
+ if (name === "npx") return undefined;
1052
1137
  return command.includes("/") || command.includes("\\") ? resolve(command) : command;
1053
1138
  }
1054
1139
 
1140
+ function inspectProfileDshRuntime(paths, state) {
1141
+ const packageRoot = join(paths.profilesRoot, "node_modules", ...DSH_PACKAGE_NAME.split("/"));
1142
+ const persistedCommand = normalizeDshCommand(state?.dshCommand);
1143
+ const profileCommand = persistedCommand && isPathInside(persistedCommand, packageRoot)
1144
+ ? persistedCommand
1145
+ : undefined;
1146
+ try {
1147
+ lstatSync(packageRoot);
1148
+ } catch (error) {
1149
+ return error?.code === "ENOENT" && !profileCommand
1150
+ ? undefined
1151
+ : {
1152
+ dshCommand: profileCommand,
1153
+ compatibility: unavailableDshCompatibility("dsh_profile_runtime_stale"),
1154
+ };
1155
+ }
1156
+
1157
+ try {
1158
+ const realPackageRoot = realpathSync(packageRoot);
1159
+ if (!lstatSync(realPackageRoot).isDirectory()) throw new Error("DSH package root is not a directory");
1160
+ const manifest = readJsonObject(join(realPackageRoot, "package.json"));
1161
+ const version = manifest?.name === DSH_PACKAGE_NAME
1162
+ ? parseDshVersion(manifest.version)
1163
+ : undefined;
1164
+ const bin = manifest?.bin !== null
1165
+ && typeof manifest?.bin === "object"
1166
+ && !Array.isArray(manifest.bin)
1167
+ ? nonEmpty(manifest.bin.dsh)
1168
+ : undefined;
1169
+ if (!version || !bin || isAbsolute(bin)) throw new Error("DSH package metadata is invalid");
1170
+
1171
+ const dshCommand = resolve(packageRoot, bin);
1172
+ const realCommand = realpathSync(dshCommand);
1173
+ if (!isPathInside(dshCommand, packageRoot)
1174
+ || !isPathInside(realCommand, realPackageRoot)
1175
+ || !lstatSync(realCommand).isFile()) {
1176
+ throw new Error("DSH package entrypoint is invalid");
1177
+ }
1178
+ return {
1179
+ dshCommand,
1180
+ compatibility: {
1181
+ compatible: true,
1182
+ dshVersion: version,
1183
+ dshVersionTested: isTestedDshVersion(version),
1184
+ testedDshVersions: [...DSH_TESTED_VERSIONS],
1185
+ },
1186
+ };
1187
+ } catch {
1188
+ return {
1189
+ dshCommand: profileCommand,
1190
+ compatibility: unavailableDshCompatibility("dsh_profile_runtime_stale"),
1191
+ };
1192
+ }
1193
+ }
1194
+
1195
+ function unavailableDshCompatibility(reason) {
1196
+ return {
1197
+ compatible: false,
1198
+ reason,
1199
+ testedDshVersions: [...DSH_TESTED_VERSIONS],
1200
+ };
1201
+ }
1202
+
1055
1203
  function runDsh(options, paths, args, command) {
1056
1204
  const env = { ...paths.env, DSH_HOME: paths.dshHome };
1057
1205
  let executable;
1058
1206
  try {
1059
- executable = resolveWindowsCliInvocation(command, args, {
1207
+ const [launcher, ...launcherArgs] = buildDshCommand(command, args, {
1208
+ nodePath: options.windowsCliResolution?.nodePath,
1209
+ });
1210
+ executable = resolveWindowsCliInvocation(launcher, launcherArgs, {
1060
1211
  ...options.windowsCliResolution,
1061
1212
  env,
1062
1213
  });
@@ -5,6 +5,17 @@ import { parseDshVersion } from "./dsh-version.mjs";
5
5
 
6
6
  const PACKAGE_METADATA_VERSION = 1;
7
7
  const STATE_VERSION = 1;
8
+
9
+ export function buildDshCommand(command, args, options = {}) {
10
+ const executable = nonEmptyString(command);
11
+ if (!executable) throw new TypeError("DSH command must be a non-empty string");
12
+ if (!Array.isArray(args)) throw new TypeError("DSH arguments must be an array");
13
+ if (isNpxCommand(executable)) throw disabledError();
14
+ return isAbsolute(executable) && /\.(?:cjs|js|mjs)$/i.test(executable)
15
+ ? [nonEmptyString(options.nodePath) ?? process.execPath, executable, ...args]
16
+ : [executable, ...args];
17
+ }
18
+
8
19
  /** Read the current durable enablement authority for this installed DSH bundle. */
9
20
  export function requireEnabledDshRuntime(pluginRoot) {
10
21
  const authority = requireDshRuntimeAuthority(pluginRoot);
@@ -28,6 +39,7 @@ export function requireDshRuntimeAuthority(pluginRoot) {
28
39
  || !nonEmptyString(metadata.memoraxCodeCommand)
29
40
  || !nonEmptyString(metadata.memoraxCodeHome)
30
41
  || !nonEmptyString(metadata.dshCommand)
42
+ || isNpxCommand(metadata.dshCommand)
31
43
  || !nonEmptyString(metadata.dshHome)
32
44
  || !parseDshVersion(metadata.dshVersion)
33
45
  || !nonEmptyString(metadata.sourceAdapterRoot)
@@ -102,6 +114,15 @@ function nonEmptyString(value) {
102
114
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
103
115
  }
104
116
 
117
+ function isNpxCommand(value) {
118
+ const command = nonEmptyString(value);
119
+ return Boolean(command && command
120
+ .split(/[\\/]/)
121
+ .at(-1)
122
+ .replace(/\.(?:cmd|bat|exe|com)$/i, "")
123
+ .toLowerCase() === "npx");
124
+ }
125
+
105
126
  function timestampString(value) {
106
127
  const normalized = nonEmptyString(value);
107
128
  return normalized && Number.isFinite(Date.parse(normalized)) ? normalized : undefined;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorax-code/opencode-adapter",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "OpenCode plugin adapter for MemoraX Code memory services.",
@@ -83,4 +83,6 @@ Do not add speculation, assistant-only praise, or a current instruction merely b
83
83
 
84
84
  ## Output
85
85
 
86
+ If a successful Add returns `quotaNotice`, or prints it as a quota-reminder line in the default CLI output, present the complete reminder once and prominently before the normal result summary. Do not reduce it to only a percentage or omit its account URL or conditional local Mark ID retrieval instructions. Never run `memorax-code account --show-mark-id` for the user, ask for its output, or reproduce a Mark ID in chat. Treat the reminder as user-facing operational output, not coding memory content, and continue the current task.
87
+
86
88
  After an add request is accepted, confirm briefly that it was submitted for processing and identify the reusable coding lesson at a high level. If add is disabled or fails, report the issue and continue without bypassing the CLI.
@@ -6,11 +6,11 @@ Use these instructions only to search reusable coding memory through `memorax-cl
6
6
 
7
7
  Run the CLI from the active task workspace. The installed Hook and session binding supply the authoritative workspace root; do not run `git rev-parse`, infer the root from Git metadata, or substitute an unrelated working directory. Do not make `memorax-cli status` a mandatory preflight; use it only to diagnose a configuration or scope failure.
8
8
 
9
- Coding memory uses `<MemoraX base user ID>@<normalized repository name>` for Git workspaces and the normalized folder name for genuine non-Git directories. MemoraX Code resolves `.git`, `gitdir`, and `commondir` without executing Git. Linked worktrees share one repository scope; another clone, repository, or non-Git directory retains a different local session key even when its readable name matches.
9
+ Coding memory uses `<MemoraX base username>@<normalized repository name>` for Git workspaces and the normalized folder name for genuine non-Git directories. MemoraX Code resolves `.git`, `gitdir`, and `commondir` without executing Git. Linked worktrees share one repository scope; another clone, repository, or non-Git directory retains a different local session key even when its readable name matches.
10
10
 
11
11
  If a successful Search result reports `workspaceScopeFallbackReason: git_metadata_invalid`, malformed or incomplete metadata inside a direct `.git` directory was downgraded to the normalized local folder scope. Search has already run with the reported `effectiveUserId`. Present its `userNotice` once without pausing the current task or asking the user to repair Git first, then continue with the returned memory and live evidence. After the repository or `.git` metadata is repaired, later Search, Add, and automatic writeback in the same client session automatically use the restored Git repository scope.
12
12
 
13
- Require a readable active workspace binding. A CLI command from a linked worktree of the bound repository is valid. If `memorax-cli search` reports `workspace_scope_mismatch` or `workspace_scope_unavailable`, do not bypass the scope or fall back to an unscoped user id. Do not change the CLI working directory and retry. Tell the user that memory search was not executed and no request was sent to MemoraX, then present the CLI's `userAction` in natural language. Continue the current task using only live code and documentation.
13
+ Require a readable active workspace binding. A CLI command from a linked worktree of the bound repository is valid. If `memorax-cli search` reports `workspace_scope_mismatch` or `workspace_scope_unavailable`, do not bypass the scope or fall back to an unscoped username. Do not change the CLI working directory and retry. Tell the user that memory search was not executed and no request was sent to MemoraX, then present the CLI's `userAction` in natural language. Continue the current task using only live code and documentation.
14
14
 
15
15
  If `memorax-cli` is not on `PATH`, or memory is disabled, unconfigured, or unavailable, report that briefly and continue with live code or documentation. Authenticate through MemoraX Code configuration; never recover credentials from shell history or place tokens in prompts. Treat injected memory as a hypothesis and verify it against the current checkout.
16
16
 
@@ -88,4 +88,6 @@ Do not apply this retry to `memorax-cli add`, authentication or configuration fa
88
88
 
89
89
  ## Output
90
90
 
91
+ If a successful Search returns `quotaNotice`, or prints it as a quota-reminder line in the default CLI output, present the complete reminder once and prominently before the normal result summary. Do not reduce it to only a percentage or omit its account URL or conditional local Mark ID retrieval instructions. Never run `memorax-code account --show-mark-id` for the user, ask for its output, or reproduce a Mark ID in chat. Treat the reminder as user-facing operational output, not recalled memory, and continue the current task.
92
+
91
93
  Mention only an invariant, pitfall, convention, or validation idea that materially affects the answer. Ground claims about current implementation behavior in live code and checks.
@@ -210,6 +210,7 @@ export function createMemoraxOpenCodePlugin(options = {}) {
210
210
  }, TURN_START_TIMEOUT_MS);
211
211
  turnStartAccepted = true;
212
212
  if (!pluginEnabled(options)) return;
213
+ void showUserNotice(client, directory, result?.userNotice, options);
213
214
  repositoryWorktree = stringValue(result?.repoMemoryWorktree);
214
215
  const repoMemoryEnv = openCodeRepoMemoryEnv(options, openCodeServerUrl, sessionId);
215
216
  if (repoMemoryEnv) {
@@ -384,6 +385,25 @@ function appendSystemContexts(output, ...contexts) {
384
385
  output.message.system = [existing, ...additions].filter(Boolean).join("\n\n");
385
386
  }
386
387
 
388
+ async function showUserNotice(client, directory, notice, options) {
389
+ const message = stringValue(notice);
390
+ if (!message || typeof client?.tui?.showToast !== "function") return;
391
+ try {
392
+ await client.tui.showToast({
393
+ body: {
394
+ title: "MemoraX Code",
395
+ message,
396
+ variant: "warning",
397
+ duration: 10_000,
398
+ },
399
+ query: { directory },
400
+ throwOnError: true,
401
+ });
402
+ } catch (error) {
403
+ debug(options, "opencode quota reminder failed", error);
404
+ }
405
+ }
406
+
387
407
  function recordReminder(options, reminder) {
388
408
  if (!reminder.turnId) return Promise.resolve();
389
409
  return postBackend(options, "/memory/skill-reminder", {
@@ -1,4 +1,4 @@
1
- export const MINIMUM_NODE_MAJOR = 24;
1
+ export const MINIMUM_NODE_MAJOR = 20;
2
2
 
3
3
  export function unsupportedNodeVersionMessage(version = process.versions.node) {
4
4
  const value = String(version ?? "").trim();
@@ -0,0 +1,311 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, unlinkSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { withJsonFileLock, withJsonFileLockAsync } from "./memorax-code-adapter-common/src/config-utils.mjs";
7
+ import {
8
+ readJsonRuntimeRecord,
9
+ RuntimeRecordError,
10
+ writePrivateJsonRecord,
11
+ } from "./memorax-code-adapter-common/src/runtime-record.mjs";
12
+
13
+ export const PACKAGE_TRANSITION_RECORD_VERSION = 1;
14
+ export const PACKAGE_TRANSITION_FRESHNESS_MS = 15 * 60 * 1_000;
15
+ export const PACKAGE_TRANSITION_COMMAND_TIMEOUT_MS = 45_000;
16
+
17
+ const RETIRING_KEYS = new Set([
18
+ "version",
19
+ "state",
20
+ "transitionId",
21
+ "startedAt",
22
+ "sourceVersion",
23
+ ]);
24
+ const RETIRED_KEYS = new Set([...RETIRING_KEYS, "retiredAt"]);
25
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
26
+ const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
27
+
28
+ export class PackageTransitionRecordError extends RuntimeRecordError {
29
+ constructor(state, path) {
30
+ super({
31
+ name: "Package transition record",
32
+ path,
33
+ state,
34
+ codePrefix: "PACKAGE_TRANSITION_RECORD",
35
+ recovery: state.status === "unsupported"
36
+ ? "upgrade MemoraX Code or restore a supported package transition record"
37
+ : "inspect the package transition record before retrying installation",
38
+ });
39
+ this.name = "PackageTransitionRecordError";
40
+ }
41
+ }
42
+
43
+ export function packageTransitionPath(memoraxCodeHome = defaultMemoraxCodeHome()) {
44
+ return join(memoraxCodeHome, "runtime", "install", "package-transition.json");
45
+ }
46
+
47
+ export function readPackageTransitionRecord(memoraxCodeHome = defaultMemoraxCodeHome()) {
48
+ const path = packageTransitionPath(memoraxCodeHome);
49
+ const state = readJsonRuntimeRecord(path);
50
+ if (state.status !== "present") return state;
51
+ const value = state.value;
52
+ if (value.version !== PACKAGE_TRANSITION_RECORD_VERSION) {
53
+ if (Number.isSafeInteger(value.version) && value.version > 0) {
54
+ return { status: "unsupported", version: value.version };
55
+ }
56
+ return { status: "invalid", reason: "invalid_version" };
57
+ }
58
+ if (value.state !== "retiring" && value.state !== "retired") {
59
+ return { status: "invalid", reason: "invalid_state" };
60
+ }
61
+ const allowedKeys = value.state === "retiring" ? RETIRING_KEYS : RETIRED_KEYS;
62
+ if (Object.keys(value).length !== allowedKeys.size
63
+ || Object.keys(value).some((key) => !allowedKeys.has(key))) {
64
+ return { status: "invalid", reason: "unknown_or_missing_fields" };
65
+ }
66
+ if (typeof value.transitionId !== "string" || !UUID_PATTERN.test(value.transitionId)) {
67
+ return { status: "invalid", reason: "invalid_transition_id" };
68
+ }
69
+ const startedAt = isoTimestamp(value.startedAt);
70
+ if (!startedAt) return { status: "invalid", reason: "invalid_started_at" };
71
+ const sourceVersion = nonEmptyString(value.sourceVersion);
72
+ if (!sourceVersion) return { status: "invalid", reason: "invalid_source_version" };
73
+ const record = {
74
+ version: PACKAGE_TRANSITION_RECORD_VERSION,
75
+ state: value.state,
76
+ transitionId: value.transitionId,
77
+ startedAt,
78
+ sourceVersion,
79
+ };
80
+ if (value.state === "retired") {
81
+ const retiredAt = isoTimestamp(value.retiredAt);
82
+ if (!retiredAt || Date.parse(retiredAt) < Date.parse(startedAt)) {
83
+ return { status: "invalid", reason: "invalid_retired_at" };
84
+ }
85
+ record.retiredAt = retiredAt;
86
+ }
87
+ return { status: "valid", record };
88
+ }
89
+
90
+ export function runNpmPreinstallPackageTransition(options = {}) {
91
+ const memoraxCodeHome = nonEmptyString(options.memoraxCodeHome) ?? defaultMemoraxCodeHome();
92
+ const memoraxCodeBin = requiredString(options.memoraxCodeBin, "memoraxCodeBin");
93
+ const packageVersion = requiredString(options.packageVersion, "packageVersion");
94
+ const pidPath = backendPidPath(memoraxCodeHome);
95
+ const dshStatePath = join(memoraxCodeHome, "adapters", "dsh", "state.json");
96
+ if (!existsSync(pidPath) && !existsSync(dshStatePath)) return { disposition: "noop" };
97
+
98
+ const transitionPath = packageTransitionPath(memoraxCodeHome);
99
+ let transition;
100
+ let shouldStop = false;
101
+ withJsonFileLock(transitionPath, () => {
102
+ const existing = readPackageTransitionRecord(memoraxCodeHome);
103
+ if (existing.status !== "absent") throw packageTransitionStateError(existing, transitionPath);
104
+ const pidState = readBackendPidState(pidPath);
105
+ const hasDshState = existsSync(dshStatePath);
106
+ if (pidState.status === "absent" && !hasDshState) return;
107
+ shouldStop = true;
108
+ if (pidState.status !== "alive" && !hasDshState) return;
109
+ transition = {
110
+ version: PACKAGE_TRANSITION_RECORD_VERSION,
111
+ state: "retiring",
112
+ transitionId: randomUUID(),
113
+ startedAt: nowIso(options),
114
+ sourceVersion: packageVersion,
115
+ };
116
+ const write = writePrivateJsonRecord(transitionPath, transition, {
117
+ durableBoundary: memoraxCodeHome,
118
+ });
119
+ if (write.durability !== "confirmed") {
120
+ throw transitionError("PACKAGE_TRANSITION_DURABILITY_UNCERTAIN", "durable persistence of the retiring package transition could not be confirmed");
121
+ }
122
+ });
123
+
124
+ if (!shouldStop) return { disposition: "noop" };
125
+ const stopped = runLifecycleCommand({
126
+ ...options,
127
+ memoraxCodeHome,
128
+ memoraxCodeBin,
129
+ args: ["stop", "--home", memoraxCodeHome, "--clients", "none", "--json"],
130
+ env: { ...options.env, MEMORAX_CODE_PACKAGE_REPLACEMENT: "1" },
131
+ label: "memorax-code stop",
132
+ });
133
+ if (existsSync(pidPath)) {
134
+ throw transitionError(
135
+ "PACKAGE_TRANSITION_PID_REMAINS",
136
+ `managed Backend PID authority still exists at ${pidPath}`,
137
+ stopped.result,
138
+ );
139
+ }
140
+ if (!transition) return { disposition: "cleaned" };
141
+
142
+ const retired = withJsonFileLock(transitionPath, () => {
143
+ const current = requireValidTransition(memoraxCodeHome);
144
+ if (current.record.state !== "retiring"
145
+ || current.record.transitionId !== transition.transitionId) {
146
+ throw transitionError("PACKAGE_TRANSITION_REPLACED", "package transition changed while the Backend was being retired");
147
+ }
148
+ const record = { ...current.record, state: "retired", retiredAt: nowIso(options) };
149
+ const write = writePrivateJsonRecord(transitionPath, record, {
150
+ durableBoundary: memoraxCodeHome,
151
+ });
152
+ if (write.durability !== "confirmed") {
153
+ throw transitionError("PACKAGE_TRANSITION_DURABILITY_UNCERTAIN", "durable persistence of the retired package transition could not be confirmed");
154
+ }
155
+ return record;
156
+ });
157
+ return { disposition: "retired", transition: retired };
158
+ }
159
+
160
+ export async function runNpmPostinstallPackageTransition(options = {}) {
161
+ const memoraxCodeHome = nonEmptyString(options.memoraxCodeHome) ?? defaultMemoraxCodeHome();
162
+ const memoraxCodeBin = requiredString(options.memoraxCodeBin, "memoraxCodeBin");
163
+ const transitionPath = packageTransitionPath(memoraxCodeHome);
164
+ if (readJsonRuntimeRecord(transitionPath).status === "absent") {
165
+ return { disposition: "noop" };
166
+ }
167
+
168
+ return await withJsonFileLockAsync(transitionPath, async () => {
169
+ const reloaded = readPackageTransitionRecord(memoraxCodeHome);
170
+ if (reloaded.status === "absent") return { disposition: "noop" };
171
+ if (reloaded.status !== "valid") {
172
+ throw new PackageTransitionRecordError(reloaded, transitionPath);
173
+ }
174
+ const current = reloaded;
175
+ if (current.record.state !== "retired") {
176
+ throw transitionError("PACKAGE_TRANSITION_NOT_RETIRED", "package transition is still retiring");
177
+ }
178
+ const ageMs = nowMs(options) - Date.parse(current.record.retiredAt);
179
+ if (ageMs < 0 || ageMs > PACKAGE_TRANSITION_FRESHNESS_MS) {
180
+ throw transitionError("PACKAGE_TRANSITION_STALE", "retired package transition is stale");
181
+ }
182
+
183
+ runLifecycleCommand({
184
+ ...options,
185
+ memoraxCodeHome,
186
+ memoraxCodeBin,
187
+ args: ["start", "--home", memoraxCodeHome, "--json"],
188
+ env: { ...options.env, MEMORAX_CODE_PACKAGE_REPLACEMENT: "1" },
189
+ label: "memorax-code start",
190
+ });
191
+ runLifecycleCommand({
192
+ ...options,
193
+ memoraxCodeHome,
194
+ memoraxCodeBin,
195
+ args: ["status", "--home", memoraxCodeHome, "--json"],
196
+ label: "memorax-code status",
197
+ });
198
+
199
+ const finalState = requireValidTransition(memoraxCodeHome);
200
+ if (finalState.record.state !== "retired"
201
+ || finalState.record.transitionId !== current.record.transitionId) {
202
+ throw transitionError("PACKAGE_TRANSITION_REPLACED", "package transition changed before it could be consumed");
203
+ }
204
+ unlinkSync(transitionPath);
205
+ return { disposition: "restored", transitionId: current.record.transitionId };
206
+ });
207
+ }
208
+
209
+ function requireValidTransition(memoraxCodeHome) {
210
+ const path = packageTransitionPath(memoraxCodeHome);
211
+ const state = readPackageTransitionRecord(memoraxCodeHome);
212
+ if (state.status !== "valid") throw new PackageTransitionRecordError(state, path);
213
+ return state;
214
+ }
215
+
216
+ function packageTransitionStateError(state, path) {
217
+ if (state.status === "valid") {
218
+ return transitionError("PACKAGE_TRANSITION_PENDING", `package transition is already ${state.record.state}: ${path}`);
219
+ }
220
+ return new PackageTransitionRecordError(state, path);
221
+ }
222
+
223
+ function readBackendPidState(path) {
224
+ const state = readJsonRuntimeRecord(path);
225
+ if (state.status === "absent") return state;
226
+ if (state.status !== "present" || !Number.isSafeInteger(state.value.pid) || state.value.pid <= 0) {
227
+ return { status: "unknown" };
228
+ }
229
+ try {
230
+ process.kill(state.value.pid, 0);
231
+ return { status: "alive", pid: state.value.pid };
232
+ } catch (error) {
233
+ if (error?.code === "ESRCH") return { status: "dead", pid: state.value.pid };
234
+ if (error?.code === "EPERM") return { status: "alive", pid: state.value.pid };
235
+ return { status: "unknown", pid: state.value.pid };
236
+ }
237
+ }
238
+
239
+ function runLifecycleCommand(options) {
240
+ const spawn = options.spawnSyncImpl ?? spawnSync;
241
+ const result = spawn(process.execPath, [options.memoraxCodeBin, ...options.args], {
242
+ encoding: "utf8",
243
+ env: { ...process.env, ...options.env, MEMORAX_CODE_HOME: options.memoraxCodeHome },
244
+ stdio: ["ignore", "pipe", "pipe"],
245
+ timeout: positiveInteger(options.commandTimeoutMs, PACKAGE_TRANSITION_COMMAND_TIMEOUT_MS),
246
+ killSignal: "SIGKILL",
247
+ windowsHide: true,
248
+ });
249
+ if (result.error || result.signal || result.status !== 0) {
250
+ const detail = result.error?.code === "ETIMEDOUT"
251
+ ? `${options.label} timed out after ${positiveInteger(options.commandTimeoutMs, PACKAGE_TRANSITION_COMMAND_TIMEOUT_MS)} ms`
252
+ : result.error?.message
253
+ ?? (result.signal ? `${options.label} exited from signal ${result.signal}` : `${options.label} exited with status ${result.status ?? "unknown"}`);
254
+ throw transitionError("PACKAGE_TRANSITION_COMMAND_FAILED", detail, result);
255
+ }
256
+ let report;
257
+ try {
258
+ report = JSON.parse(String(result.stdout ?? ""));
259
+ } catch {
260
+ throw transitionError("PACKAGE_TRANSITION_COMMAND_INVALID_JSON", `${options.label} did not return valid JSON`, result);
261
+ }
262
+ if (!report || typeof report !== "object" || Array.isArray(report) || report.ok !== true) {
263
+ throw transitionError("PACKAGE_TRANSITION_COMMAND_NOT_OK", `${options.label} did not report ok=true`, result);
264
+ }
265
+ return { result, report };
266
+ }
267
+
268
+ function transitionError(code, message, command) {
269
+ const error = new Error(message);
270
+ error.name = "PackageTransitionError";
271
+ error.code = code;
272
+ if (command) error.command = command;
273
+ return error;
274
+ }
275
+
276
+ function backendPidPath(memoraxCodeHome) {
277
+ return join(memoraxCodeHome, "runtime", "backend", "backend.pid.json");
278
+ }
279
+
280
+ function defaultMemoraxCodeHome() {
281
+ return join(homedir(), ".memorax-code");
282
+ }
283
+
284
+ function requiredString(value, name) {
285
+ const result = nonEmptyString(value);
286
+ if (!result) throw new TypeError(`package transition requires ${name}`);
287
+ return result;
288
+ }
289
+
290
+ function nonEmptyString(value) {
291
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
292
+ }
293
+
294
+ function isoTimestamp(value) {
295
+ const candidate = nonEmptyString(value);
296
+ return candidate && ISO_TIMESTAMP_PATTERN.test(candidate) && Number.isFinite(Date.parse(candidate))
297
+ ? candidate
298
+ : undefined;
299
+ }
300
+
301
+ function positiveInteger(value, fallback) {
302
+ return Number.isSafeInteger(value) && value > 0 ? value : fallback;
303
+ }
304
+
305
+ function nowMs(options) {
306
+ return typeof options.now === "function" ? Number(options.now()) : Date.now();
307
+ }
308
+
309
+ function nowIso(options) {
310
+ return new Date(nowMs(options)).toISOString();
311
+ }