@memorax/memorax-code 0.1.9 → 0.1.10

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 +82 -22
  6. package/docs/troubleshooting.md +69 -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 +279 -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 +277 -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 +112 -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
@@ -21,7 +21,7 @@ let requestedSignal;
21
21
  for (const signal of ["SIGINT", "SIGTERM"]) {
22
22
  process.on(signal, () => {
23
23
  requestedSignal = signal;
24
- activeChild?.kill(signal);
24
+ if (activeChild) terminateClient(activeChild, signal);
25
25
  });
26
26
  }
27
27
 
@@ -56,12 +56,16 @@ async function main(args) {
56
56
  runId: workerContext.runId,
57
57
  });
58
58
  if (initial.status !== "started") throw new Error(`repo memory job cannot start from status ${initial.status}`);
59
+ const clientTimeoutMs = repoMemoryClientTimeoutMs();
60
+ const clientKillGraceMs = repoMemoryClientKillGraceMs();
59
61
  let state = writeJobState(request.jobPath, {
60
62
  ...initial,
61
63
  status: "running",
62
64
  pid: process.pid,
63
65
  workerPid: process.pid,
64
66
  workerStartedAt: new Date().toISOString(),
67
+ clientTimeoutMs,
68
+ clientKillGraceMs,
65
69
  });
66
70
 
67
71
  if (requestedSignal) {
@@ -85,9 +89,18 @@ async function main(args) {
85
89
  });
86
90
  },
87
91
  finalMessagePath: state.finalMessagePath,
92
+ timeoutMs: clientTimeoutMs,
93
+ killGraceMs: clientKillGraceMs,
88
94
  });
89
95
  state = readJobState(request.jobPath);
90
96
 
97
+ if (childResult.timedOut) {
98
+ return finishFailed(request, state, workerContext, `${runner}_timeout`, {
99
+ timeoutMs: childResult.timeoutMs,
100
+ elapsedMs: childResult.elapsedMs,
101
+ signal: childResult.signal,
102
+ });
103
+ }
91
104
  if (childResult.error) {
92
105
  return finishFailed(request, state, workerContext, `${runner}_spawn_failed`, {
93
106
  error: childResult.error.message,
@@ -213,6 +226,11 @@ async function waitForOwnedMarker(input) {
213
226
  function runClient(command, options) {
214
227
  return new Promise((resolveResult) => {
215
228
  let settled = false;
229
+ let timeoutHandle;
230
+ let killHandle;
231
+ let timedOut = false;
232
+ let timeoutMs;
233
+ const startedAt = Date.now();
216
234
  let invocation;
217
235
  try {
218
236
  invocation = process.platform === "win32" && /\.[cm]?js$/i.test(command[0])
@@ -228,6 +246,7 @@ function runClient(command, options) {
228
246
  const child = spawn(invocation.command, invocation.args, {
229
247
  cwd: options.cwd,
230
248
  env: options.env,
249
+ detached: process.platform !== "win32",
231
250
  stdio: options.captureStdout ? ["ignore", "pipe", "inherit"] : "inherit",
232
251
  });
233
252
  activeChild = child;
@@ -243,8 +262,10 @@ function runClient(command, options) {
243
262
  const finish = (result) => {
244
263
  if (settled) return;
245
264
  settled = true;
265
+ if (timeoutHandle) clearTimeout(timeoutHandle);
266
+ if (killHandle) clearTimeout(killHandle);
246
267
  activeChild = undefined;
247
- resolveResult(result);
268
+ resolveResult({ ...result, timedOut, timeoutMs, elapsedMs: Date.now() - startedAt });
248
269
  };
249
270
  child.once("error", (error) => finish({ code: undefined, signal: undefined, error }));
250
271
  child.once("close", (code, signal) => {
@@ -258,10 +279,74 @@ function runClient(command, options) {
258
279
  }
259
280
  finish({ code, signal, error: undefined });
260
281
  });
261
- if (requestedSignal) child.kill(requestedSignal);
282
+ if (Number.isFinite(options.timeoutMs) && options.timeoutMs > 0) {
283
+ timeoutHandle = setTimeout(() => {
284
+ timeoutMs = options.timeoutMs;
285
+ timedOut = true;
286
+ const terminated = terminateClient(child, "SIGTERM");
287
+ // A broken or ignored CLI must not leave a detached job behind forever.
288
+ // The grace period is deliberately short and only targets this child.
289
+ killHandle = setTimeout(() => {
290
+ if (!settled) terminateClient(child, "SIGKILL");
291
+ }, options.killGraceMs);
292
+ if (!terminated && !settled) {
293
+ finish({
294
+ code: undefined,
295
+ signal: "SIGTERM",
296
+ error: undefined,
297
+ timedOut: true,
298
+ timeoutMs,
299
+ });
300
+ }
301
+ }, options.timeoutMs);
302
+ }
303
+ if (requestedSignal) terminateClient(child, requestedSignal);
262
304
  });
263
305
  }
264
306
 
307
+ function terminateClient(child, signal) {
308
+ if (!child || !Number.isInteger(child.pid) || child.pid <= 0) return false;
309
+ if (process.platform !== "win32") {
310
+ try {
311
+ process.kill(-child.pid, signal);
312
+ return true;
313
+ } catch {
314
+ // The process group may have exited between the timeout and this call.
315
+ }
316
+ }
317
+ if (process.platform === "win32") {
318
+ try {
319
+ const result = spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
320
+ encoding: "utf8",
321
+ windowsHide: true,
322
+ stdio: ["ignore", "ignore", "ignore"],
323
+ });
324
+ if (result.status === 0) return true;
325
+ } catch {
326
+ // Fall through to the direct child termination below.
327
+ }
328
+ }
329
+ try {
330
+ return child.kill(signal);
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
335
+
336
+ function repoMemoryClientTimeoutMs(env = process.env) {
337
+ return positiveEnvInteger(env.MEMORAX_CODE_REPO_MEMORY_JOB_TIMEOUT_MS, 10 * 60 * 1000);
338
+ }
339
+
340
+ function repoMemoryClientKillGraceMs(env = process.env) {
341
+ return positiveEnvInteger(env.MEMORAX_CODE_REPO_MEMORY_JOB_KILL_GRACE_MS, 5000);
342
+ }
343
+
344
+ function positiveEnvInteger(value, fallback) {
345
+ if (typeof value !== "string" || !/^\d+$/.test(value.trim())) return fallback;
346
+ const parsed = Number(value.trim());
347
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
348
+ }
349
+
265
350
  function validateBundle(repo, validator) {
266
351
  const python = process.env.MEMORAX_CODE_REPO_MEMORY_PYTHON_COMMAND || "python3";
267
352
  process.stdout.write("\n[repo-memory-worker] validating generated repo memory\n");
@@ -24,6 +24,9 @@ export function resolveWindowsCliInvocation(command, args, options = {}) {
24
24
  }
25
25
  resolvedCommand ??= command;
26
26
  if (/\.(?:exe|com)$/i.test(resolvedCommand)) return { command: resolvedCommand, args };
27
+ if (isBareCodeBuddyEntrypoint(resolvedCommand, pathApi) && fileExists(resolvedCommand)) {
28
+ return { command: options.nodePath ?? process.execPath, args: [resolvedCommand, ...args] };
29
+ }
27
30
  if (!/\.(?:cmd|bat)$/i.test(resolvedCommand)) {
28
31
  throw new Error(`refusing to execute unsupported Windows command shim: ${resolvedCommand}`);
29
32
  }
@@ -104,9 +107,16 @@ export function selectWindowsCommandCandidate(command, output) {
104
107
  .map((value) => value.trim())
105
108
  .filter(Boolean);
106
109
  const selected = candidates.find((value) => /\.(?:cmd|bat)$/i.test(value))
107
- ?? candidates.find((value) => /\.(?:exe|com)$/i.test(value));
110
+ ?? candidates.find((value) => /\.(?:exe|com)$/i.test(value))
111
+ ?? (path.win32.basename(command).toLowerCase() === "codebuddy"
112
+ ? candidates.find((value) => isBareCodeBuddyEntrypoint(value, path.win32))
113
+ : undefined);
108
114
  if (!selected) {
109
115
  throw new Error(`where.exe did not return a safe executable or command shim for ${command}`);
110
116
  }
111
117
  return selected;
112
118
  }
119
+
120
+ function isBareCodeBuddyEntrypoint(value, pathApi) {
121
+ return pathApi.basename(value).toLowerCase() === "codebuddy" && pathApi.extname(value) === "";
122
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorax-code/claude-adapter",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "memorax-code-codebuddy-adapter",
3
+ "version": "0.1.10",
4
+ "description": "MemoraX Code memory integration for CodeBuddy and WorkBuddy.",
5
+ "hooks": "./hooks/hooks.json",
6
+ "skills": ["./skills/memorax-code"]
7
+ }
@@ -0,0 +1,13 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export function resolveCommonSourceRoot(pluginRoot) {
5
+ const candidates = [
6
+ join(pluginRoot, "memorax-code-adapter-common", "src"),
7
+ join(pluginRoot, "..", "memorax-code-adapter-common", "src"),
8
+ join(pluginRoot, "..", "..", "memorax-code-adapter-common", "src"),
9
+ ];
10
+ const root = candidates.find((candidate) => existsSync(join(candidate, "repo-memory", "repo-memory-job-supervisor.mjs")));
11
+ if (!root) throw new Error(`MemoraX Code shared runtime is unavailable for CodeBuddy plugin: ${pluginRoot}`);
12
+ return root;
13
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "startup|resume|clear|compact",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${CODEBUDDY_PLUGIN_ROOT}/hooks/runtime-hook.mjs\" turn",
10
+ "timeout": 35
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "UserPromptSubmit": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "node \"${CODEBUDDY_PLUGIN_ROOT}/hooks/runtime-hook.mjs\" turn",
21
+ "timeout": 15
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "Stop": [
27
+ {
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "node \"${CODEBUDDY_PLUGIN_ROOT}/hooks/runtime-hook.mjs\" turn",
32
+ "timeout": 15
33
+ }
34
+ ]
35
+ }
36
+ ]
37
+ }
38
+ }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { pathToFileURL } from "node:url";
5
+ import { resolveCommonSourceRoot } from "./common-runtime.mjs";
6
+
7
+ const hookDir = dirname(fileURLToPath(import.meta.url));
8
+ const pluginRoot = dirname(hookDir);
9
+ const commonRoot = resolveCommonSourceRoot(pluginRoot);
10
+ const { resolveHookCodeBuddyCommand } = await import(pathToFileURL(join(commonRoot, "clients", "codebuddy-command.mjs")).href);
11
+ const { runRepoMemoryJob } = await import(pathToFileURL(join(commonRoot, "repo-memory", "repo-memory-job-supervisor.mjs")).href);
12
+ const { evaluateRepository } = await import(pathToFileURL(join(commonRoot, "repo-memory", "repo-memory-update-policy-evaluator.mjs")).href);
13
+
14
+ try {
15
+ const payload = runRepoMemoryJob(process.argv.slice(2), {
16
+ runner: "codebuddy",
17
+ finalMessageSource: "stdout",
18
+ memorySkillInvocation: "the `memorax-code` skill",
19
+ validatorPath: resolve(pluginRoot, "skills/memorax-code/scripts/validate_memory.py"),
20
+ evaluateRepository,
21
+ createCommand({ prompt }) {
22
+ const codeBuddy = resolveHookCodeBuddyCommand({
23
+ pluginRoot,
24
+ });
25
+ return [
26
+ codeBuddy,
27
+ "--print",
28
+ "--output-format",
29
+ "text",
30
+ "--dangerously-skip-permissions",
31
+ "--no-session-persistence",
32
+ prompt,
33
+ ];
34
+ },
35
+ });
36
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
37
+ } catch (error) {
38
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
39
+ process.exit(1);
40
+ }
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { appendFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { dirname } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { pathToFileURL } from "node:url";
9
+ import { resolveCommonSourceRoot } from "./common-runtime.mjs";
10
+ import { writeCodeBuddyRuntimeObservation } from "../src/runtime-observation.mjs";
11
+
12
+ const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
13
+ const commonRoot = resolveCommonSourceRoot(pluginRoot);
14
+ const { scheduleMissingRepoMemoryBuild } = await import(pathToFileURL(join(commonRoot, "repo-memory", "repo-memory-auto-build.mjs")).href);
15
+ const { isRepoMemoryJobWorker } = await import(pathToFileURL(join(commonRoot, "repo-memory", "repo-memory-job-context.mjs")).href);
16
+ const { buildRepoProcedureMemoryContext } = await import(pathToFileURL(join(commonRoot, "repo-memory", "repo-procedure-memory-context.mjs")).href);
17
+ const { buildRepoUserProfilePreferencesContext } = await import(pathToFileURL(join(commonRoot, "repo-memory", "repo-user-profile-context.mjs")).href);
18
+ const { resolveBackendConnection } = await import(pathToFileURL(join(commonRoot, "backend-connection.mjs")).href);
19
+ const { ensureBackendAvailable, stringValue: commonStringValue } = await import(pathToFileURL(join(commonRoot, "hooks", "ensure-backend-runner.mjs")).href);
20
+ const {
21
+ evaluateMemorySkillReminder,
22
+ markSupplementalReminderAfterCompact,
23
+ personalMemoryReminderContext,
24
+ } = await import(pathToFileURL(join(commonRoot, "hooks", "memory-skill-reminder-hook.mjs")).href);
25
+
26
+ if (isRepoMemoryJobWorker()) process.exit(0);
27
+
28
+ const MEMORY_SKILL_INVOCATION = "the `memorax-code` skill";
29
+ const REMINDER_TRACE_TIMEOUT_MS = 1000;
30
+
31
+ const input = await readJsonStdin();
32
+ const event = stringValue(input?.hook_event_name) ?? stringValue(input?.hookEventName);
33
+ const sessionId = stringValue(input?.session_id) ?? stringValue(input?.sessionId);
34
+ const transcriptPath = stringValue(input?.transcript_path) ?? stringValue(input?.transcriptPath);
35
+ if (!event || !sessionId || !transcriptPath) process.exit(0);
36
+ const home = process.env.MEMORAX_CODE_HOME?.trim() || join(homedir(), ".memorax-code");
37
+ const packageMetadata = await readRecord(join(pluginRoot, ".memorax-code-package.json"));
38
+ const codeBuddyHome = commonStringValue(process.env.CODEBUDDY_HOME)
39
+ ?? commonStringValue(process.env.WORKBUDDY_HOME)
40
+ ?? commonStringValue(packageMetadata.codeBuddyHome)
41
+ ?? commonStringValue(input?.codebuddy_home)
42
+ ?? commonStringValue(input?.codeBuddyHome)
43
+ ?? join(homedir(), process.platform === "win32" ? ".codebuddy" : ".workbuddy");
44
+ try {
45
+ await writeCodeBuddyRuntimeObservation({ memoraxCodeHome: home, codeBuddyHome, pluginRoot });
46
+ } catch (error) {
47
+ if (process.env.MEMORAX_CODE_CODEBUDDY_HOOK_DEBUG === "1") {
48
+ console.error(error instanceof Error ? error.message : String(error));
49
+ }
50
+ }
51
+ const pendingPath = join(home, "adapters", "codebuddy", "pending.json");
52
+ const reminderOptions = {
53
+ adapterDir: "codebuddy",
54
+ debugEnv: "MEMORAX_CODE_CODEBUDDY_HOOK_DEBUG",
55
+ memoraxCodeHome: home,
56
+ runtime: "codebuddy",
57
+ supplementalReminderAfterCompact: true,
58
+ };
59
+ const personalMemoryContextOptions = {
60
+ adapterDir: "codebuddy",
61
+ debugEnv: "MEMORAX_CODE_CODEBUDDY_HOOK_DEBUG",
62
+ sessionKeyPrefix: "codebuddy",
63
+ };
64
+ if (event === "SessionStart") await bindMemoryCliTraceSession(sessionId);
65
+ await ensureBackendAvailable({
66
+ ensureBackendValue: process.env.MEMORAX_CODE_CODEBUDDY_ENSURE_BACKEND
67
+ ?? process.env.MEMORAX_CODE_CODEBUDDY_HOOK_ENSURE_BACKEND,
68
+ healthTimeoutValue: process.env.MEMORAX_CODE_CODEBUDDY_ENSURE_TIMEOUT_MS,
69
+ startTimeoutValue: process.env.MEMORAX_CODE_CODEBUDDY_START_TIMEOUT_MS,
70
+ memoraxCodeCommand: commonStringValue(process.env.MEMORAX_CODE_CODEBUDDY_LIFECYCLE_COMMAND)
71
+ ?? commonStringValue(process.env.MEMORAX_CODE_COMMAND),
72
+ pluginRoot,
73
+ resolveHomes: () => ({
74
+ memoraxCodeHome: home,
75
+ codeBuddyHome,
76
+ }),
77
+ buildStartArgs: (homes, recoveryArguments) => [
78
+ "start",
79
+ "--home", homes.memoraxCodeHome,
80
+ "--clients", "codebuddy",
81
+ "--codebuddy-home", homes.codeBuddyHome,
82
+ ...recoveryArguments,
83
+ ],
84
+ debug: (message) => { if (process.env.MEMORAX_CODE_CODEBUDDY_HOOK_DEBUG === "1") console.error(message); },
85
+ }, input);
86
+ if (event === "SessionStart") {
87
+ markSupplementalReminderAfterCompact(reminderOptions, input);
88
+ } else if (event === "UserPromptSubmit") {
89
+ const prompt = stringValue(input.prompt);
90
+ if (!prompt) process.exit(0);
91
+ const boundary = await fileBoundary(transcriptPath);
92
+ const turnId = provisionalTurnId(sessionId, boundary, prompt);
93
+ const workspaceKind = stringValue(input.workspace_kind) ?? stringValue(input.workspaceKind);
94
+ await updatePending(pendingPath, (state) => {
95
+ const now = Date.now();
96
+ const existing = state[sessionId];
97
+ state[sessionId] = {
98
+ version: 1,
99
+ turnId,
100
+ transcriptPath,
101
+ cwd: stringValue(input.cwd),
102
+ workspaceKind,
103
+ createdAt: existing?.turnId === turnId ? existing.createdAt : now,
104
+ updatedAt: now,
105
+ };
106
+ });
107
+ const response = await post("/memory/turn-start", { version: 1, client: "codebuddy", sessionId, turnId, transcriptPath, prompt, cwd: stringValue(input.cwd), workspaceKind });
108
+ const repoMemoryWorktree = stringValue(response?.repoMemoryWorktree);
109
+ scheduleMissingRepoMemoryBuild(repoMemoryWorktree, {
110
+ debugEnv: "MEMORAX_CODE_CODEBUDDY_HOOK_DEBUG",
111
+ pluginRoot,
112
+ });
113
+ const reminderResult = await evaluateMemorySkillReminder({
114
+ ...reminderOptions,
115
+ additionalReminderContext: personalMemoryReminderContext(MEMORY_SKILL_INVOCATION),
116
+ memorySkillInvocation: MEMORY_SKILL_INVOCATION,
117
+ remindOnFirstTurn: true,
118
+ requireTranscriptPath: true,
119
+ ...(repoMemoryWorktree ? {
120
+ buildCadenceReminderContext: (hookInput) => buildRepoProcedureMemoryContext({
121
+ ...hookInput,
122
+ cwd: repoMemoryWorktree,
123
+ }, personalMemoryContextOptions),
124
+ buildPersonalMemoryContext: (hookInput) => buildRepoUserProfilePreferencesContext({
125
+ ...hookInput,
126
+ cwd: repoMemoryWorktree,
127
+ }, personalMemoryContextOptions),
128
+ } : {}),
129
+ }, { ...input, turnId, workspaceKind });
130
+ const context = [
131
+ stringValue(response?.additionalContext),
132
+ stringValue(reminderResult?.additionalContext),
133
+ ].filter(Boolean).join("\n\n");
134
+ const systemMessage = stringValue(response?.userNotice);
135
+ if (context || systemMessage) process.stdout.write(`${JSON.stringify({
136
+ ...(systemMessage ? { systemMessage } : {}),
137
+ ...(context ? { hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } } : {}),
138
+ })}\n`);
139
+ if (response !== undefined && reminderResult?.reminder) await recordReminder(reminderResult.reminder);
140
+ } else if (event === "Stop") {
141
+ const record = (await readPending(pendingPath))[sessionId];
142
+ if (!record || record.transcriptPath !== transcriptPath) process.exit(0);
143
+ const response = await post("/memory/writeback", { version: 1, client: "codebuddy", sessionId, turnId: record.turnId, transcriptPath, cwd: record.cwd ?? stringValue(input.cwd), workspaceKind: record.workspaceKind ?? stringValue(input.workspaceKind) });
144
+ if (response?.ok === true && response?.scheduled === true) {
145
+ await updatePending(pendingPath, (state) => {
146
+ if (state[sessionId]?.turnId === record.turnId) delete state[sessionId];
147
+ });
148
+ }
149
+ }
150
+
151
+ async function recordReminder(reminder) {
152
+ if (!reminder.turnId || !reminder.transcriptPath) return;
153
+ await post("/memory/skill-reminder", {
154
+ version: 1,
155
+ client: "codebuddy",
156
+ sessionId: reminder.sessionId,
157
+ turnId: reminder.turnId,
158
+ transcriptPath: reminder.transcriptPath,
159
+ cwd: reminder.cwd,
160
+ workspaceKind: reminder.workspaceKind,
161
+ content: reminder.content,
162
+ triggers: reminder.triggers,
163
+ }, REMINDER_TRACE_TIMEOUT_MS);
164
+ }
165
+
166
+ async function post(path, body, timeoutMs = 12_000) {
167
+ let connection;
168
+ try {
169
+ connection = resolveBackendConnection({ memoraxCodeHome: home });
170
+ } catch (error) {
171
+ if (process.env.MEMORAX_CODE_CODEBUDDY_HOOK_DEBUG === "1") console.error(error instanceof Error ? error.message : String(error));
172
+ return undefined;
173
+ }
174
+ const headers = { "content-type": "application/json", connection: "close" };
175
+ if (connection.token) headers["x-memorax-code-backend-token"] = connection.token;
176
+ try { const response = await fetch(new URL(path, connection.url), { method: "POST", headers, body: JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs) }); return response.ok ? await response.json().catch(() => undefined) : undefined; } catch { return undefined; }
177
+ }
178
+ async function fileBoundary(path) { try { return (await stat(path)).size; } catch { return 0; } }
179
+ async function readJsonStdin() { try { let text = ""; for await (const chunk of process.stdin) text += chunk; return JSON.parse(text); } catch { return {}; } }
180
+ async function readRecord(path) { try { const value = JSON.parse(await readFile(path, "utf8")); return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } catch { return {}; } }
181
+ async function writeRecord(path, value) { await mkdir(dirname(path), { recursive: true }); await writeFile(path, `${JSON.stringify(value)}\n`, { mode: 0o600 }); }
182
+ async function readPending(path) {
183
+ return await withJsonFileLockAsync(path, async () => {
184
+ const state = await readRecord(path);
185
+ prunePendingState(state);
186
+ await writeRecord(path, state);
187
+ return state;
188
+ });
189
+ }
190
+ async function updatePending(path, mutate) {
191
+ await withJsonFileLockAsync(path, async () => {
192
+ const state = await readRecord(path);
193
+ prunePendingState(state);
194
+ mutate(state);
195
+ prunePendingState(state);
196
+ await writeRecord(path, state);
197
+ });
198
+ }
199
+ async function withJsonFileLockAsync(path, operation) {
200
+ const lockPath = `${path}.lock`;
201
+ const deadline = Date.now() + 1500;
202
+ let acquired = false;
203
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
204
+ while (!acquired) {
205
+ try {
206
+ await mkdir(lockPath, { recursive: false, mode: 0o700 });
207
+ acquired = true;
208
+ } catch (error) {
209
+ if (error?.code !== "EEXIST") throw error;
210
+ try {
211
+ const lock = await stat(lockPath);
212
+ if (Date.now() - lock.mtimeMs > 30_000) await rm(lockPath, { recursive: true, force: true });
213
+ } catch {}
214
+ if (Date.now() >= deadline) {
215
+ const timeout = new Error(`timed out waiting for CodeBuddy Hook state lock: ${lockPath}`);
216
+ timeout.code = "JSON_FILE_LOCK_TIMEOUT";
217
+ throw timeout;
218
+ }
219
+ await new Promise((resolve) => setTimeout(resolve, 10));
220
+ }
221
+ }
222
+ try { return await operation(); } finally { await rm(lockPath, { recursive: true, force: true }); }
223
+ }
224
+ async function bindMemoryCliTraceSession(sessionId) {
225
+ const envFile = stringValue(process.env.CODEBUDDY_ENV_FILE);
226
+ if (!envFile) return;
227
+ try {
228
+ await appendFile(envFile, [
229
+ "export MEMORAX_CODE_MEMORY_CLI_TRACE_CLIENT='codebuddy'",
230
+ `export MEMORAX_CODE_MEMORY_CLI_TRACE_SESSION_ID=${shellSingleQuote(sessionId)}`,
231
+ "",
232
+ ].join("\n"), "utf8");
233
+ } catch (error) {
234
+ if (process.env.MEMORAX_CODE_CODEBUDDY_HOOK_DEBUG === "1") {
235
+ console.error(error instanceof Error ? error.message : String(error));
236
+ }
237
+ }
238
+ }
239
+ function shellSingleQuote(value) { return `'${value.replaceAll("'", "'\"'\"'")}'`; }
240
+ function stringValue(value) { return typeof value === "string" && value.trim() ? value.trim() : undefined; }
241
+
242
+ function provisionalTurnId(sessionId, boundary, prompt) {
243
+ return `${sessionId}:${boundary}:${createHash("sha256").update(prompt.trim()).digest("hex")}`;
244
+ }
245
+
246
+ function validProvisionalTurnId(sessionId, turnId) {
247
+ const prefix = `${sessionId}:`;
248
+ if (typeof turnId !== "string" || !turnId.startsWith(prefix)) return false;
249
+ const match = /^(0|[1-9]\d*):[0-9a-f]{64}$/.exec(turnId.slice(prefix.length));
250
+ return Boolean(match && Number.isSafeInteger(Number(match[1])));
251
+ }
252
+
253
+ function prunePendingState(state) {
254
+ const now = Date.now();
255
+ const ttl = positiveInteger(process.env.MEMORAX_CODE_CODEBUDDY_PENDING_TTL_MS, 24 * 60 * 60 * 1000);
256
+ const maxEntries = positiveInteger(process.env.MEMORAX_CODE_CODEBUDDY_PENDING_MAX_ENTRIES, 200);
257
+ for (const [sessionId, record] of Object.entries(state)) {
258
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
259
+ delete state[sessionId];
260
+ continue;
261
+ }
262
+ if (record.version !== 1 || !validProvisionalTurnId(sessionId, record.turnId) || !stringValue(record.transcriptPath)) {
263
+ delete state[sessionId];
264
+ continue;
265
+ }
266
+ if (!Number.isFinite(record.createdAt) || !Number.isFinite(record.updatedAt) || now - record.updatedAt > ttl) {
267
+ delete state[sessionId];
268
+ }
269
+ }
270
+ const entries = Object.entries(state).sort(([, left], [, right]) => Number(left.updatedAt ?? left.createdAt ?? 0) - Number(right.updatedAt ?? right.createdAt ?? 0));
271
+ while (entries.length > maxEntries) {
272
+ const [sessionId] = entries.shift();
273
+ delete state[sessionId];
274
+ }
275
+ }
276
+ function positiveInteger(value, fallback) {
277
+ const parsed = Number(value);
278
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
279
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@memorax-code/codebuddy-adapter",
3
+ "version": "0.1.10",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "CodeBuddy and WorkBuddy Hook adapter for MemoraX Code memory services.",
7
+ "bin": { "memorax-code-codebuddy": "src/cli.mjs" },
8
+ "scripts": { "test": "node --test test/*.test.mjs" }
9
+ }