@memorax/memorax-code 0.1.9 → 0.1.11

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 (101) hide show
  1. package/README.md +10 -0
  2. package/bin/memorax-code-codebuddy.mjs +4 -0
  3. package/bin/memorax-code-setup.mjs +182 -65
  4. package/bin/memorax-code.mjs +74 -45
  5. package/docs/configuration.md +85 -22
  6. package/docs/troubleshooting.md +72 -5
  7. package/lib/automatic-update.mjs +233 -0
  8. package/lib/memorax-code-adapter-common/src/automatic-update-state.d.mts +24 -0
  9. package/lib/memorax-code-adapter-common/src/automatic-update-state.mjs +73 -0
  10. package/lib/memorax-code-adapter-common/src/clients/codebuddy-command.mjs +82 -0
  11. package/lib/memorax-code-adapter-common/src/config-utils.d.mts +2 -0
  12. package/lib/memorax-code-adapter-common/src/config-utils.mjs +11 -0
  13. package/lib/memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs +3 -0
  14. package/lib/memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs +45 -10
  15. package/lib/memorax-code-adapter-common/src/repo-memory/repo-memory-job-worker.mjs +88 -3
  16. package/lib/memorax-code-adapter-common/src/windows-cli-invocation.mjs +11 -1
  17. package/lib/memorax-code-backend/dist/app/backend-server.js +2 -2
  18. package/lib/memorax-code-backend/dist/app/memory-observability.js +2 -2
  19. package/lib/memorax-code-backend/dist/clients/codebuddy/jsonl-history.js +208 -0
  20. package/lib/memorax-code-backend/dist/clients/codebuddy/lifecycle.js +44 -0
  21. package/lib/memorax-code-backend/dist/clients/codebuddy/memory-hook-runtime.js +250 -0
  22. package/lib/memorax-code-backend/dist/clients/codebuddy/turn-id.js +16 -0
  23. package/lib/memorax-code-backend/dist/clients/codex/plugin-install.js +129 -7
  24. package/lib/memorax-code-backend/dist/config/memorax-code.js +14 -1
  25. package/lib/memorax-code-backend/dist/entrypoints/backend-cli.js +43 -6
  26. package/lib/memorax-code-backend/dist/lifecycle/active-clients.js +3 -0
  27. package/lib/memorax-code-backend/dist/lifecycle/automatic-update-scheduler.js +133 -0
  28. package/lib/memorax-code-backend/dist/lifecycle/client-plugin-removal.js +14 -2
  29. package/lib/memorax-code-backend/dist/lifecycle/client-selection.js +6 -4
  30. package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +48 -9
  31. package/lib/memorax-code-backend/dist/memory/cli.js +4 -2
  32. package/lib/memorax-code-backend/dist/memory/hook-command.js +31 -1
  33. package/lib/memorax-code-backend/dist/memory/reminder-trace-recorder.js +5 -1
  34. package/lib/memorax-code-backend/dist/memory/service.js +12 -0
  35. package/lib/memorax-code-backend/dist/shared/windows-cli-invocation.js +10 -1
  36. package/lib/memorax-code-backend/dist/trace/config.js +12 -0
  37. package/lib/memorax-code-backend/dist/trace/context.js +21 -1
  38. package/lib/memorax-code-backend/dist/trace/store.js +12 -1
  39. package/lib/memorax-code-backend/package.json +1 -1
  40. package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  41. package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  42. package/lib/memorax-code-claude-adapter/package.json +1 -1
  43. package/lib/memorax-code-claude-adapter/runtime-hooks/ensure-backend.mjs +2 -0
  44. package/lib/memorax-code-claude-adapter/src/plugin-install.mjs +3 -0
  45. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  46. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  47. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/automatic-update-state.d.mts +24 -0
  48. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/automatic-update-state.mjs +73 -0
  49. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/clients/codebuddy-command.mjs +82 -0
  50. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.d.mts +2 -0
  51. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.mjs +11 -0
  52. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs +3 -0
  53. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs +45 -10
  54. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/repo-memory/repo-memory-job-worker.mjs +88 -3
  55. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/windows-cli-invocation.mjs +11 -1
  56. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
  57. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/ensure-backend.mjs +2 -0
  58. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/src/plugin-install.mjs +3 -0
  59. package/lib/memorax-code-codebuddy-adapter/.codebuddy-plugin/plugin.json +7 -0
  60. package/lib/memorax-code-codebuddy-adapter/hooks/common-runtime.mjs +13 -0
  61. package/lib/memorax-code-codebuddy-adapter/hooks/hooks.json +38 -0
  62. package/lib/memorax-code-codebuddy-adapter/hooks/repo-memory-job.mjs +40 -0
  63. package/lib/memorax-code-codebuddy-adapter/hooks/runtime-hook.mjs +289 -0
  64. package/lib/memorax-code-codebuddy-adapter/package.json +9 -0
  65. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/SKILL.md +85 -0
  66. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/agents/claude.yaml +10 -0
  67. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/agents/openai.yaml +7 -0
  68. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/defaults.json +12 -0
  69. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/memorax-add.md +88 -0
  70. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/memorax-search.md +93 -0
  71. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/personal-read.md +46 -0
  72. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/personal-write.md +120 -0
  73. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-build.md +319 -0
  74. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-read.md +103 -0
  75. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-templates.md +390 -0
  76. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-update.md +127 -0
  77. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/collect_all.py +579 -0
  78. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/detect_updates.py +919 -0
  79. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/git_commit_facets.py +222 -0
  80. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/github_resource_facets.py +512 -0
  81. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/gitlab_resource_facets.py +517 -0
  82. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/prepare_repo_memory.py +411 -0
  83. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/user_profile_memory.py +528 -0
  84. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/validate_memory.py +248 -0
  85. package/lib/memorax-code-codebuddy-adapter/src/cli.mjs +58 -0
  86. package/lib/memorax-code-codebuddy-adapter/src/config.mjs +340 -0
  87. package/lib/memorax-code-codebuddy-adapter/src/hook-manifest.mjs +47 -0
  88. package/lib/memorax-code-codebuddy-adapter/src/runtime-observation.mjs +65 -0
  89. package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
  90. package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
  91. package/lib/memorax-code-codex-adapter/package.json +1 -1
  92. package/lib/memorax-code-codex-adapter/runtime-hooks/ensure-backend.mjs +4 -1
  93. package/lib/memorax-code-dsh-adapter/package.json +2 -1
  94. package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +1 -0
  95. package/lib/memorax-code-opencode-adapter/package.json +1 -1
  96. package/lib/npm-invocation.mjs +51 -8
  97. package/lib/resolve-codebuddy-command.mjs +123 -0
  98. package/lib/run-entrypoint.mjs +25 -1
  99. package/lib/windows-cli-invocation.mjs +11 -1
  100. package/lib/windows-user-path.mjs +218 -0
  101. package/package.json +5 -3
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { loadMemoraxCodeConfig } from "../config/memorax-code.js";
5
5
  import { isTraceClient } from "./context.js";
6
6
  export const TRACE_CLIENTS = ["codex", "claude", "dsh", "opencode"];
7
+ export const TRACE_RUNTIME_CLIENTS = [...TRACE_CLIENTS, "codebuddy"];
7
8
  export const CODEX_TRACE_DEFAULT_CONFIG = {
8
9
  enabled: true,
9
10
  captureContent: true,
@@ -32,17 +33,22 @@ export const OPENCODE_TRACE_DEFAULT_CONFIG = {
32
33
  maxEventChars: 20_000,
33
34
  maxFileBytes: 52_428_800,
34
35
  };
36
+ export const CODEBUDDY_TRACE_DEFAULT_CONFIG = {
37
+ ...OPENCODE_TRACE_DEFAULT_CONFIG,
38
+ };
35
39
  const TRACE_DEFAULT_CONFIGS = {
36
40
  codex: CODEX_TRACE_DEFAULT_CONFIG,
37
41
  claude: CLAUDE_TRACE_DEFAULT_CONFIG,
38
42
  dsh: DSH_TRACE_DEFAULT_CONFIG,
39
43
  opencode: OPENCODE_TRACE_DEFAULT_CONFIG,
44
+ codebuddy: CODEBUDDY_TRACE_DEFAULT_CONFIG,
40
45
  };
41
46
  const TRACE_ENV_PREFIXES = {
42
47
  codex: "MEMORAX_CODE_CODEX_TRACE",
43
48
  claude: "MEMORAX_CODE_CLAUDE_TRACE",
44
49
  dsh: "MEMORAX_CODE_DSH_TRACE",
45
50
  opencode: "MEMORAX_CODE_OPENCODE_TRACE",
51
+ codebuddy: "MEMORAX_CODE_CODEBUDDY_TRACE",
46
52
  };
47
53
  export const TRACE_CLEANUP_DEBOUNCE_MS = 60 * 60 * 1000;
48
54
  export const TRACE_CURRENT_TURN_TTL_MS = 30 * 60 * 1000;
@@ -62,6 +68,9 @@ export function dshTraceConfigFromEnv(env = process.env, fileConfig) {
62
68
  export function openCodeTraceConfigFromEnv(env = process.env, fileConfig) {
63
69
  return clientTraceConfigFromEnv("opencode", env, fileConfig);
64
70
  }
71
+ export function codeBuddyTraceConfigFromEnv(env = process.env, fileConfig) {
72
+ return clientTraceConfigFromEnv("codebuddy", env, fileConfig);
73
+ }
65
74
  export function clientTraceConfigFromEnv(client, env = process.env, fileConfig) {
66
75
  assertTraceClient(client);
67
76
  const config = fileConfig ?? loadMemoraxCodeConfig(memoraxCodeHomeForTrace(env), { warn: () => undefined });
@@ -91,6 +100,9 @@ export function dshTracePaths(memoraxCodeHome = memoraxCodeHomeForTrace(process.
91
100
  export function openCodeTracePaths(memoraxCodeHome = memoraxCodeHomeForTrace(process.env)) {
92
101
  return clientTracePaths("opencode", memoraxCodeHome);
93
102
  }
103
+ export function codeBuddyTracePaths(memoraxCodeHome = memoraxCodeHomeForTrace(process.env)) {
104
+ return clientTracePaths("codebuddy", memoraxCodeHome);
105
+ }
94
106
  export function clientTracePaths(client, memoraxCodeHome = memoraxCodeHomeForTrace(process.env)) {
95
107
  assertTraceClient(client);
96
108
  const root = join(memoraxCodeHome, "debug", "traces", client);
@@ -62,6 +62,26 @@ export function traceContextFromOpenCodeHookBody(body, capturedAt = new Date().t
62
62
  capturedAt,
63
63
  });
64
64
  }
65
+ export function traceContextFromCodeBuddyHookBody(body, capturedAt = new Date().toISOString()) {
66
+ if (!isRecord(body))
67
+ return undefined;
68
+ const sessionId = stringField(body, "session_id") ?? stringField(body, "sessionId");
69
+ if (!sessionId)
70
+ return undefined;
71
+ const cwd = stringField(body, "cwd");
72
+ return pruneTraceContext({
73
+ schemaVersion: "1",
74
+ client: "codebuddy",
75
+ sessionId,
76
+ turnId: stringField(body, "turn_id") ?? stringField(body, "turnId"),
77
+ transcriptPath: stringField(body, "transcript_path") ?? stringField(body, "transcriptPath"),
78
+ cwd,
79
+ memoryProject: resolveMemoryProject(cwd),
80
+ workspaceKind: stringField(body, "workspace_kind") ?? stringField(body, "workspaceKind"),
81
+ contextOrigin: "codebuddy-hook-body",
82
+ capturedAt,
83
+ });
84
+ }
65
85
  export function traceContextFromDshTurnStart(body, capturedAt = new Date().toISOString()) {
66
86
  return traceContextFromDshBody(body, "dsh-cordis-turn-start", capturedAt);
67
87
  }
@@ -129,7 +149,7 @@ function pruneRecord(value) {
129
149
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
130
150
  }
131
151
  export function isTraceClient(value) {
132
- return value === "codex" || value === "claude" || value === "dsh" || value === "opencode";
152
+ return value === "codex" || value === "claude" || value === "dsh" || value === "opencode" || value === "codebuddy";
133
153
  }
134
154
  function traceContextFromDshBody(body, contextOrigin, capturedAt) {
135
155
  if (!isRecord(body))
@@ -60,6 +60,9 @@ export async function recordCodexTraceEvent(input) {
60
60
  export async function recordClaudeTraceEvent(input) {
61
61
  return recordTraceEventForClient("claude", input);
62
62
  }
63
+ export async function recordCodeBuddyTraceEvent(input) {
64
+ return recordTraceEventForClient("codebuddy", input);
65
+ }
63
66
  export function traceTurnEventId(traceContext, type) {
64
67
  if (!traceContext?.sessionId || !traceContext.turnId)
65
68
  return undefined;
@@ -168,6 +171,12 @@ export async function markCurrentCodexTurnOutcome(traceContext, outcome, options
168
171
  export async function markCurrentClaudeTurnOutcome(traceContext, outcome, options = {}) {
169
172
  return markCurrentTraceTurnOutcome(traceContext, outcome, { ...options, client: "claude" });
170
173
  }
174
+ export async function writeCurrentCodeBuddyTurn(traceContext, options = {}) {
175
+ return writeCurrentTraceTurn(traceContext, { ...options, client: "codebuddy" });
176
+ }
177
+ export async function markCurrentCodeBuddyTurnOutcome(traceContext, outcome, options = {}) {
178
+ return markCurrentTraceTurnOutcome(traceContext, outcome, { ...options, client: "codebuddy" });
179
+ }
171
180
  async function readCurrentTraceTurnRecord(options) {
172
181
  const client = options.client;
173
182
  if (!isTraceClient(client))
@@ -303,7 +312,9 @@ function buildTraceEvent(input, config, now, eventId) {
303
312
  ok: input.ok,
304
313
  outcome: input.outcome,
305
314
  related_turns: input.relatedTurns?.map(relatedTurnJson),
306
- activities: input.activities,
315
+ activities: input.activities === undefined
316
+ ? undefined
317
+ : sanitizeTraceValue(input.activities, config, captureContent),
307
318
  usage: input.usage,
308
319
  session_turn_index: input.sessionTurnIndex,
309
320
  request: input.request === undefined ? undefined : sanitizeTraceValue(input.request, config, captureContent),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorax-code/backend",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Local memory integration, lifecycle, trace, and diagnostics for MemoraX Code.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorax-code-claude-adapter",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Memory skill and local hooks for MemoraX Code in Claude Code.",
5
5
  "author": {
6
6
  "name": "MemoraX Code Maintainers"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "version": 1,
3
- "shellVersion": "0.1.9",
3
+ "shellVersion": "0.1.11",
4
4
  "runtimeAbi": 1
5
5
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorax-code/claude-adapter",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Claude Code Hook adapter for MemoraX Code memory services.",
@@ -2,6 +2,7 @@
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { readClientHookPluginRoot } from "../../memorax-code-adapter-common/src/config-utils.mjs";
5
6
  import {
6
7
  runEnsureBackendHook,
7
8
  stringValue,
@@ -31,6 +32,7 @@ process.exit(0);
31
32
  function pluginRoot() {
32
33
  return stringValue(process.env.CLAUDE_PLUGIN_ROOT)
33
34
  ?? stringValue(process.env.PLUGIN_ROOT)
35
+ ?? readClientHookPluginRoot()
34
36
  ?? dirname(dirname(fileURLToPath(import.meta.url)));
35
37
  }
36
38
 
@@ -32,6 +32,7 @@ const STABLE_SHELL_REQUIRED_FILES = Object.freeze([
32
32
  "memorax-code-adapter-common/src/hooks/capture-cwd-hook.mjs",
33
33
  "memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs",
34
34
  "memorax-code-adapter-common/src/config-utils.mjs",
35
+ "memorax-code-adapter-common/src/setup-completion.mjs",
35
36
  "memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs",
36
37
  "memorax-code-adapter-common/src/hooks/hook-runtime-generation.mjs",
37
38
  "memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs",
@@ -226,10 +227,12 @@ export function ensureClaudePluginInstalled(options = {}) {
226
227
  }
227
228
 
228
229
  function writeInstalledPluginMetadata(installPath, claudeCommand) {
230
+ const npmExecPath = stringOption(process.env.MEMORAX_CODE_NPM_EXEC_PATH);
229
231
  atomicWriteJson(join(installPath, ".memorax-code-package.json"), {
230
232
  version: 1,
231
233
  memoraxCodeCommand: process.argv[1],
232
234
  claudeCommand,
235
+ ...(npmExecPath ? { npmExecPath } : {}),
233
236
  writtenAt: new Date().toISOString(),
234
237
  });
235
238
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorax-code-claude-adapter",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Memory skill and local hooks for MemoraX Code in Claude Code.",
5
5
  "author": {
6
6
  "name": "MemoraX Code Maintainers"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "version": 1,
3
- "shellVersion": "0.1.9",
3
+ "shellVersion": "0.1.11",
4
4
  "runtimeAbi": 1
5
5
  }
@@ -0,0 +1,24 @@
1
+ export type AutomaticUpdateRecord = Readonly<{
2
+ version: 1;
3
+ installedVersion: string;
4
+ nextCheckAt: string;
5
+ }>;
6
+
7
+ export type AutomaticUpdateState =
8
+ | Readonly<{ status: "absent" }>
9
+ | Readonly<{ status: "valid"; record: AutomaticUpdateRecord }>
10
+ | Readonly<{ status: "invalid"; reason: string }>
11
+ | Readonly<{ status: "unsupported"; version: number }>;
12
+
13
+ export const AUTOMATIC_UPDATE_RECORD_VERSION: 1;
14
+ export const AUTOMATIC_UPDATE_CHECK_INTERVAL_MS: number;
15
+ export const AUTOMATIC_UPDATE_RETRY_INTERVAL_MS: number;
16
+
17
+ export function automaticUpdateStatePath(memoraxCodeHome: string): string;
18
+ export function readAutomaticUpdateState(memoraxCodeHome: string): AutomaticUpdateState;
19
+ export function writeAutomaticUpdateState(options: {
20
+ memoraxCodeHome: string;
21
+ nowMs: number;
22
+ installedVersion: string;
23
+ retry: boolean;
24
+ }): AutomaticUpdateRecord;
@@ -0,0 +1,73 @@
1
+ import { join, resolve } from "node:path";
2
+ import {
3
+ readJsonRuntimeRecord,
4
+ writePrivateJsonRecord,
5
+ } from "./runtime-record.mjs";
6
+
7
+ export const AUTOMATIC_UPDATE_RECORD_VERSION = 1;
8
+ export const AUTOMATIC_UPDATE_CHECK_INTERVAL_MS = 8 * 60 * 60 * 1_000;
9
+ export const AUTOMATIC_UPDATE_RETRY_INTERVAL_MS = 15 * 60 * 1_000;
10
+
11
+ const RECORD_KEYS = new Set(["version", "installedVersion", "nextCheckAt"]);
12
+
13
+ export function automaticUpdateStatePath(memoraxCodeHome) {
14
+ return join(resolve(memoraxCodeHome), "runtime", "install", "automatic-update.json");
15
+ }
16
+
17
+ export function readAutomaticUpdateState(memoraxCodeHome) {
18
+ const state = readJsonRuntimeRecord(automaticUpdateStatePath(memoraxCodeHome));
19
+ if (state.status !== "present") return state;
20
+ const value = state.value;
21
+ if (value.version !== AUTOMATIC_UPDATE_RECORD_VERSION) {
22
+ return Number.isSafeInteger(value.version) && value.version > 0
23
+ ? { status: "unsupported", version: value.version }
24
+ : { status: "invalid", reason: "invalid_version" };
25
+ }
26
+ if (Object.keys(value).length !== RECORD_KEYS.size
27
+ || Object.keys(value).some((key) => !RECORD_KEYS.has(key))) {
28
+ return { status: "invalid", reason: "unknown_or_missing_fields" };
29
+ }
30
+ const installedVersion = nonEmptyString(value.installedVersion);
31
+ const nextCheckAt = timestamp(value.nextCheckAt);
32
+ if (!installedVersion || !nextCheckAt) {
33
+ return { status: "invalid", reason: "invalid_fields" };
34
+ }
35
+ return {
36
+ status: "valid",
37
+ record: {
38
+ version: AUTOMATIC_UPDATE_RECORD_VERSION,
39
+ installedVersion,
40
+ nextCheckAt,
41
+ },
42
+ };
43
+ }
44
+
45
+ export function writeAutomaticUpdateState({ memoraxCodeHome, nowMs, installedVersion, retry }) {
46
+ const timestampMs = Number(nowMs);
47
+ const version = nonEmptyString(installedVersion);
48
+ if (!Number.isFinite(timestampMs) || !version) {
49
+ throw new TypeError("automatic update state requires a timestamp and installed version");
50
+ }
51
+ const record = {
52
+ version: AUTOMATIC_UPDATE_RECORD_VERSION,
53
+ installedVersion: version,
54
+ nextCheckAt: new Date(
55
+ timestampMs + (retry ? AUTOMATIC_UPDATE_RETRY_INTERVAL_MS : AUTOMATIC_UPDATE_CHECK_INTERVAL_MS),
56
+ ).toISOString(),
57
+ };
58
+ writePrivateJsonRecord(automaticUpdateStatePath(memoraxCodeHome), record, {
59
+ durableBoundary: memoraxCodeHome,
60
+ });
61
+ return record;
62
+ }
63
+
64
+ function nonEmptyString(value) {
65
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
66
+ }
67
+
68
+ function timestamp(value) {
69
+ const candidate = nonEmptyString(value);
70
+ return candidate && Number.isFinite(Date.parse(candidate))
71
+ ? new Date(Date.parse(candidate)).toISOString()
72
+ : undefined;
73
+ }
@@ -0,0 +1,82 @@
1
+ import { accessSync, constants, existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, win32 } from "node:path";
4
+
5
+ const APP_NAMES = ["WorkBuddy.app", "CodeBuddy.app"];
6
+ const BUNDLED_SEGMENTS = ["Contents", "Resources", "app.asar.unpacked", "cli", "bin", "codebuddy"];
7
+ const WINDOWS_SEGMENTS = [
8
+ ["resources", "app.asar.unpacked", "cli", "bin", "codebuddy"],
9
+ ["resources", "app.asar.unpacked", "cli", "bin", "codebuddy.exe"],
10
+ ];
11
+
12
+ /**
13
+ * Resolve the CodeBuddy executable from the environment, installed adapter
14
+ * metadata, or the WorkBuddy/CodeBuddy application bundle. Hooks are often
15
+ * launched by a GUI process and therefore cannot rely on the user's shell
16
+ * PATH or on npm's environment setup.
17
+ */
18
+ export function resolveHookCodeBuddyCommand({
19
+ env = process.env,
20
+ pluginRoot,
21
+ homeDir = homedir(),
22
+ platform = process.platform,
23
+ pathExists = commandPathExists,
24
+ } = {}) {
25
+ const configured = stringValue(env.MEMORAX_CODE_CODEBUDDY_COMMAND)
26
+ ?? stringValue(env.CODEBUDDY_CLI_PATH)
27
+ ?? stringValue(env.WORKBUDDY_CODEBUDDY_PATH);
28
+ if (configured) return configured;
29
+
30
+ const metadataCommand = readMetadataCommand(stringValue(pluginRoot) ?? stringValue(env.CODEBUDDY_PLUGIN_ROOT));
31
+ if (metadataCommand && pathExists(metadataCommand, platform)) return metadataCommand;
32
+
33
+ if (platform === "darwin") {
34
+ for (const root of [join(homeDir, "Applications"), "/Applications"]) {
35
+ for (const appName of APP_NAMES) {
36
+ const command = join(root, appName, ...BUNDLED_SEGMENTS);
37
+ if (pathExists(command, platform)) return command;
38
+ }
39
+ }
40
+ }
41
+ if (platform === "win32") {
42
+ for (const root of [
43
+ env.LOCALAPPDATA && win32.join(env.LOCALAPPDATA, "Programs", "WorkBuddy"),
44
+ env.LOCALAPPDATA && win32.join(env.LOCALAPPDATA, "Programs", "CodeBuddy"),
45
+ env.LOCALAPPDATA && win32.join(env.LOCALAPPDATA, "WorkBuddy"),
46
+ env.LOCALAPPDATA && win32.join(env.LOCALAPPDATA, "CodeBuddy"),
47
+ env.ProgramFiles && win32.join(env.ProgramFiles, "WorkBuddy"),
48
+ env.ProgramFiles && win32.join(env.ProgramFiles, "CodeBuddy"),
49
+ ].filter(Boolean)) {
50
+ for (const segments of WINDOWS_SEGMENTS) {
51
+ const command = win32.join(root, ...segments);
52
+ if (pathExists(command, platform)) return command;
53
+ }
54
+ }
55
+ }
56
+ return "codebuddy";
57
+ }
58
+
59
+ function readMetadataCommand(pluginRoot) {
60
+ if (!pluginRoot) return undefined;
61
+ try {
62
+ const metadata = JSON.parse(readFileSync(join(pluginRoot, ".memorax-code-package.json"), "utf8"));
63
+ return stringValue(metadata?.codeBuddyCommand);
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ }
68
+
69
+ function commandPathExists(command, platform) {
70
+ if (!command.includes("/") && !command.includes("\\")) return true;
71
+ if (!existsSync(command)) return false;
72
+ try {
73
+ accessSync(command, platform === "win32" ? constants.F_OK : constants.X_OK);
74
+ return true;
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ function stringValue(value) {
81
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
82
+ }
@@ -13,6 +13,8 @@ export function readJsonFile(path: string): any;
13
13
  export function readJsonValue(path: string): any;
14
14
  export function readStdinJson(): Promise<any>;
15
15
  export function injectClientHookInput(input: Record<string, unknown>): void;
16
+ export function injectClientHookPluginRoot(pluginRoot: string): void;
17
+ export function readClientHookPluginRoot(): string | undefined;
16
18
  export function sha256(value: string | NodeJS.ArrayBufferView): string;
17
19
  export function stringOption(value: unknown): string | undefined;
18
20
  export function atomicWriteJson(path: string, value: unknown): void;
@@ -24,6 +24,7 @@ const JSON_FILE_LOCK_RETRY_MS = 10;
24
24
  const PROCESS_START_TOLERANCE_MS = 5000;
25
25
  const LOCK_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
26
26
  const HOOK_INPUT_SYMBOL = Symbol.for("memorax-code.client-hook.input.v1");
27
+ const HOOK_PLUGIN_ROOT_SYMBOL = Symbol.for("memorax-code.client-hook.plugin-root.v1");
27
28
 
28
29
  export function readAdapterState(path) {
29
30
  const state = readJsonFile(path);
@@ -64,6 +65,16 @@ export function injectClientHookInput(input) {
64
65
  globalThis[HOOK_INPUT_SYMBOL] = input;
65
66
  }
66
67
 
68
+ export function injectClientHookPluginRoot(pluginRoot) {
69
+ const root = stringOption(pluginRoot);
70
+ if (!root) throw new TypeError("client Hook plugin root must be a non-empty string");
71
+ globalThis[HOOK_PLUGIN_ROOT_SYMBOL] = root;
72
+ }
73
+
74
+ export function readClientHookPluginRoot() {
75
+ return stringOption(globalThis[HOOK_PLUGIN_ROOT_SYMBOL]);
76
+ }
77
+
67
78
  export function sha256(value) {
68
79
  return createHash("sha256").update(value).digest("hex");
69
80
  }
@@ -5,6 +5,7 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import {
7
7
  injectClientHookInput,
8
+ injectClientHookPluginRoot,
8
9
  readStdinJson,
9
10
  stringOption,
10
11
  withJsonFileLock,
@@ -47,6 +48,8 @@ export async function runClientHookLauncher({
47
48
  });
48
49
  if (!selection) return;
49
50
  injectClientHookInput(input);
51
+ const originalPluginRoot = stringOption(pluginRoot);
52
+ if (originalPluginRoot) injectClientHookPluginRoot(originalPluginRoot);
50
53
  await import(pathToFileURL(selection.modulePath).href);
51
54
  } catch (error) {
52
55
  if (process.env[debugEnv] === "1") {
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { basename, delimiter, join } from "node:path";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { basename, delimiter, isAbsolute, join } from "node:path";
4
4
  import {
5
5
  localBackendRecoveryArguments,
6
6
  resolveBackendConnection,
@@ -16,8 +16,17 @@ export async function runEnsureBackendHook(options) {
16
16
  }
17
17
 
18
18
  export async function ensureBackendAvailable(options, input = {}) {
19
- if (isRepoMemoryJobWorker() || ensureDisabled(options.ensureBackendValue)) return;
19
+ if (isRepoMemoryJobWorker()) return;
20
20
  const homes = options.resolveHomes(input);
21
+ const metadata = packageMetadata(options.pluginRoot);
22
+ const command = memoraxCodeCommandInfo(
23
+ options.memoraxCodeCommand,
24
+ metadata,
25
+ options.platform,
26
+ );
27
+ if (ensureDisabled(options.ensureBackendValue)) {
28
+ return;
29
+ }
21
30
  let connection;
22
31
  try {
23
32
  connection = options.backendConnection
@@ -34,19 +43,19 @@ export async function ensureBackendAvailable(options, input = {}) {
34
43
 
35
44
  const recoveryArguments = localBackendRecoveryArguments(connection);
36
45
  if (recoveryArguments === undefined) return;
37
- const command = memoraxCodeCommandInfo(options.memoraxCodeCommand, options.pluginRoot);
38
46
  if (!command.value || command.removed === true || !memoraxCodeCommandAvailable(command.value)) return;
39
47
  const result = await runMemoraxCode(
40
48
  command.value,
41
49
  options.buildStartArgs(homes, recoveryArguments),
42
50
  parsePositiveInt(options.startTimeoutValue, DEFAULT_ENSURE_BACKEND_START_TIMEOUT_MS),
43
51
  options.nodePath,
44
- options.recoveryEnv,
52
+ recoveryEnvironment(options.recoveryEnv, metadata),
45
53
  );
46
54
  if (result.code !== 0) {
47
55
  options.debug?.(
48
56
  `MemoraX Code backend start failed with code ${result.code}${result.stderr ? `: ${result.stderr}` : ""}`,
49
57
  );
58
+ return;
50
59
  }
51
60
  }
52
61
 
@@ -79,22 +88,48 @@ async function backendHealthy(connection, timeoutMs) {
79
88
  }
80
89
  }
81
90
 
82
- function memoraxCodeCommandInfo(explicitCommand, pluginRoot) {
91
+ function memoraxCodeCommandInfo(explicitCommand, metadata, platform = process.platform) {
83
92
  const explicit = stringValue(explicitCommand);
84
93
  if (explicit) return { value: explicit, removed: pathLooksRemoved(explicit) };
85
- const metadata = metadataCommand(pluginRoot);
86
- if (metadata) return { value: metadata, removed: pathLooksRemoved(metadata) };
94
+ const command = stringValue(metadata?.memoraxCodeCommand);
95
+ if (command) return { value: command, removed: pathLooksRemoved(command) };
96
+ if (platform === "win32") return { value: undefined, removed: false };
87
97
  return { value: "memorax-code", removed: false };
88
98
  }
89
99
 
90
- function metadataCommand(pluginRoot) {
100
+ function packageMetadata(pluginRoot) {
91
101
  const root = stringValue(pluginRoot);
92
102
  if (!root) return undefined;
93
103
  const path = join(root, ".memorax-code-package.json");
94
104
  if (!existsSync(path)) return undefined;
95
105
  try {
96
106
  const metadata = JSON.parse(readFileSync(path, "utf8"));
97
- return stringValue(metadata.memoraxCodeCommand);
107
+ return metadata && typeof metadata === "object" && !Array.isArray(metadata)
108
+ ? metadata
109
+ : undefined;
110
+ } catch {
111
+ return undefined;
112
+ }
113
+ }
114
+
115
+ function recoveryEnvironment(recoveryEnv, metadata) {
116
+ if (stringValue(recoveryEnv?.MEMORAX_CODE_NPM_EXEC_PATH)
117
+ || stringValue(process.env.MEMORAX_CODE_NPM_EXEC_PATH)) {
118
+ return recoveryEnv;
119
+ }
120
+ const npmExecPath = metadataNpmExecPath(metadata);
121
+ return npmExecPath
122
+ ? { ...(recoveryEnv ?? {}), MEMORAX_CODE_NPM_EXEC_PATH: npmExecPath }
123
+ : recoveryEnv;
124
+ }
125
+
126
+ function metadataNpmExecPath(metadata) {
127
+ const npmExecPath = stringValue(metadata?.npmExecPath);
128
+ if (!npmExecPath || !isAbsolute(npmExecPath) || !/\.(?:cjs|js|mjs)$/i.test(npmExecPath)) {
129
+ return undefined;
130
+ }
131
+ try {
132
+ return statSync(npmExecPath).isFile() ? npmExecPath : undefined;
98
133
  } catch {
99
134
  return undefined;
100
135
  }