@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.
- package/README.md +10 -0
- package/bin/memorax-code-codebuddy.mjs +4 -0
- package/bin/memorax-code-setup.mjs +182 -65
- package/bin/memorax-code.mjs +74 -45
- package/docs/configuration.md +85 -22
- package/docs/troubleshooting.md +72 -5
- package/lib/automatic-update.mjs +233 -0
- package/lib/memorax-code-adapter-common/src/automatic-update-state.d.mts +24 -0
- package/lib/memorax-code-adapter-common/src/automatic-update-state.mjs +73 -0
- package/lib/memorax-code-adapter-common/src/clients/codebuddy-command.mjs +82 -0
- package/lib/memorax-code-adapter-common/src/config-utils.d.mts +2 -0
- package/lib/memorax-code-adapter-common/src/config-utils.mjs +11 -0
- package/lib/memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs +3 -0
- package/lib/memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs +45 -10
- package/lib/memorax-code-adapter-common/src/repo-memory/repo-memory-job-worker.mjs +88 -3
- package/lib/memorax-code-adapter-common/src/windows-cli-invocation.mjs +11 -1
- package/lib/memorax-code-backend/dist/app/backend-server.js +2 -2
- package/lib/memorax-code-backend/dist/app/memory-observability.js +2 -2
- package/lib/memorax-code-backend/dist/clients/codebuddy/jsonl-history.js +208 -0
- package/lib/memorax-code-backend/dist/clients/codebuddy/lifecycle.js +44 -0
- package/lib/memorax-code-backend/dist/clients/codebuddy/memory-hook-runtime.js +250 -0
- package/lib/memorax-code-backend/dist/clients/codebuddy/turn-id.js +16 -0
- package/lib/memorax-code-backend/dist/clients/codex/plugin-install.js +129 -7
- package/lib/memorax-code-backend/dist/config/memorax-code.js +14 -1
- package/lib/memorax-code-backend/dist/entrypoints/backend-cli.js +43 -6
- package/lib/memorax-code-backend/dist/lifecycle/active-clients.js +3 -0
- package/lib/memorax-code-backend/dist/lifecycle/automatic-update-scheduler.js +133 -0
- package/lib/memorax-code-backend/dist/lifecycle/client-plugin-removal.js +14 -2
- package/lib/memorax-code-backend/dist/lifecycle/client-selection.js +6 -4
- package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +48 -9
- package/lib/memorax-code-backend/dist/memory/cli.js +4 -2
- package/lib/memorax-code-backend/dist/memory/hook-command.js +31 -1
- package/lib/memorax-code-backend/dist/memory/reminder-trace-recorder.js +5 -1
- package/lib/memorax-code-backend/dist/memory/service.js +12 -0
- package/lib/memorax-code-backend/dist/shared/windows-cli-invocation.js +10 -1
- package/lib/memorax-code-backend/dist/trace/config.js +12 -0
- package/lib/memorax-code-backend/dist/trace/context.js +21 -1
- package/lib/memorax-code-backend/dist/trace/store.js +12 -1
- package/lib/memorax-code-backend/package.json +1 -1
- package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
- package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-claude-adapter/package.json +1 -1
- package/lib/memorax-code-claude-adapter/runtime-hooks/ensure-backend.mjs +2 -0
- package/lib/memorax-code-claude-adapter/src/plugin-install.mjs +3 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/automatic-update-state.d.mts +24 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/automatic-update-state.mjs +73 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/clients/codebuddy-command.mjs +82 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.d.mts +2 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.mjs +11 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs +3 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs +45 -10
- 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
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/windows-cli-invocation.mjs +11 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/ensure-backend.mjs +2 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/src/plugin-install.mjs +3 -0
- package/lib/memorax-code-codebuddy-adapter/.codebuddy-plugin/plugin.json +7 -0
- package/lib/memorax-code-codebuddy-adapter/hooks/common-runtime.mjs +13 -0
- package/lib/memorax-code-codebuddy-adapter/hooks/hooks.json +38 -0
- package/lib/memorax-code-codebuddy-adapter/hooks/repo-memory-job.mjs +40 -0
- package/lib/memorax-code-codebuddy-adapter/hooks/runtime-hook.mjs +289 -0
- package/lib/memorax-code-codebuddy-adapter/package.json +9 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/SKILL.md +85 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/agents/claude.yaml +10 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/agents/openai.yaml +7 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/defaults.json +12 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/memorax-add.md +88 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/memorax-search.md +93 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/personal-read.md +46 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/personal-write.md +120 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-build.md +319 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-read.md +103 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-templates.md +390 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-update.md +127 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/collect_all.py +579 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/detect_updates.py +919 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/git_commit_facets.py +222 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/github_resource_facets.py +512 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/gitlab_resource_facets.py +517 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/prepare_repo_memory.py +411 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/user_profile_memory.py +528 -0
- package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/validate_memory.py +248 -0
- package/lib/memorax-code-codebuddy-adapter/src/cli.mjs +58 -0
- package/lib/memorax-code-codebuddy-adapter/src/config.mjs +340 -0
- package/lib/memorax-code-codebuddy-adapter/src/hook-manifest.mjs +47 -0
- package/lib/memorax-code-codebuddy-adapter/src/runtime-observation.mjs +65 -0
- package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
- package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-codex-adapter/package.json +1 -1
- package/lib/memorax-code-codex-adapter/runtime-hooks/ensure-backend.mjs +4 -1
- package/lib/memorax-code-dsh-adapter/package.json +2 -1
- package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +1 -0
- package/lib/memorax-code-opencode-adapter/package.json +1 -1
- package/lib/npm-invocation.mjs +51 -8
- package/lib/resolve-codebuddy-command.mjs +123 -0
- package/lib/run-entrypoint.mjs +25 -1
- package/lib/windows-cli-invocation.mjs +11 -1
- package/lib/windows-user-path.mjs +218 -0
- package/package.json +5 -3
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate authored repo-memory bundles."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
BASELINE_FILES = [
|
|
13
|
+
Path("PROFILE.md"),
|
|
14
|
+
Path("resources/commits.md"),
|
|
15
|
+
Path("resources/prs.md"),
|
|
16
|
+
Path("resources/issues.md"),
|
|
17
|
+
Path("raw/git-commits.json"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
PROVIDER_RAW_FILES = [Path("raw/github-facets.json"), Path("raw/gitlab-facets.json")]
|
|
21
|
+
PROVIDER_RESOURCE_FILES = [Path("resources/prs.md"), Path("resources/issues.md")]
|
|
22
|
+
DISABLED_RESOURCE_SOURCES = {"history_disabled", "provider_skipped_local_only", "provider_unavailable", "github_resource_facets_unavailable", "gitlab_resource_facets_unavailable", "provider_unavailable_local_only"}
|
|
23
|
+
USER_PROFILE_SIDECAR = Path("user-profile/preferences.md")
|
|
24
|
+
PROCEDURE_SIDECAR_DIRECTORY = "procedure-memory"
|
|
25
|
+
PLACEHOLDER_RE = re.compile(r"\[([^\]\n]+)\]")
|
|
26
|
+
LINK_TARGET_AFTER_BRACKET_RE = re.compile(r"[ \t\r\n]*\(")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def relative(path: Path, memory: Path) -> str:
|
|
30
|
+
return path.relative_to(memory).as_posix()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_memory_root(path: Path) -> Path:
|
|
34
|
+
candidate = path.expanduser().resolve()
|
|
35
|
+
if candidate.name == ".repo_memory":
|
|
36
|
+
return candidate
|
|
37
|
+
return candidate / ".repo_memory"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_frontmatter(text: str) -> dict[str, Any]:
|
|
41
|
+
if not text.startswith("---\n"):
|
|
42
|
+
return {}
|
|
43
|
+
end = text.find("\n---", 4)
|
|
44
|
+
if end == -1:
|
|
45
|
+
return {}
|
|
46
|
+
values: dict[str, Any] = {}
|
|
47
|
+
for line in text[4:end].splitlines():
|
|
48
|
+
if ":" not in line:
|
|
49
|
+
continue
|
|
50
|
+
key, value = line.split(":", 1)
|
|
51
|
+
key = key.strip()
|
|
52
|
+
value = value.strip()
|
|
53
|
+
if not key:
|
|
54
|
+
continue
|
|
55
|
+
if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
|
|
56
|
+
values[key] = value[1:-1]
|
|
57
|
+
elif re.fullmatch(r"-?\d+", value):
|
|
58
|
+
values[key] = int(value)
|
|
59
|
+
elif value.lower() == "true":
|
|
60
|
+
values[key] = True
|
|
61
|
+
elif value.lower() == "false":
|
|
62
|
+
values[key] = False
|
|
63
|
+
else:
|
|
64
|
+
values[key] = value
|
|
65
|
+
return values
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def body_after_frontmatter(text: str) -> str:
|
|
69
|
+
if not text.startswith("---\n"):
|
|
70
|
+
return text
|
|
71
|
+
end = text.find("\n---", 4)
|
|
72
|
+
if end == -1:
|
|
73
|
+
return text
|
|
74
|
+
return text[end + 4 :].lstrip("\n")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def item_section_count(text: str) -> int:
|
|
78
|
+
body = body_after_frontmatter(text)
|
|
79
|
+
lines = body.splitlines()
|
|
80
|
+
title_seen = False
|
|
81
|
+
count = 0
|
|
82
|
+
for line in lines:
|
|
83
|
+
if line.startswith("# ") and not title_seen:
|
|
84
|
+
title_seen = True
|
|
85
|
+
continue
|
|
86
|
+
if title_seen and line.startswith("## "):
|
|
87
|
+
count += 1
|
|
88
|
+
return count
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def placeholder_matches(text: str) -> list[str]:
|
|
92
|
+
text = strip_markdown_code(text)
|
|
93
|
+
matches: list[str] = []
|
|
94
|
+
for match in PLACEHOLDER_RE.finditer(text):
|
|
95
|
+
if LINK_TARGET_AFTER_BRACKET_RE.match(text, match.end()):
|
|
96
|
+
continue
|
|
97
|
+
value = match.group(1).strip()
|
|
98
|
+
if not value or value.startswith("#"):
|
|
99
|
+
continue
|
|
100
|
+
if re.search(r"[A-Za-z]", value):
|
|
101
|
+
matches.append(f"[{value}]")
|
|
102
|
+
return matches
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def strip_markdown_code(text: str) -> str:
|
|
106
|
+
without_fences = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
|
|
107
|
+
return re.sub(r"`[^`\n]*`", "", without_fences)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def check_exists(memory: Path, rel_path: Path, errors: list[str], checked: list[str]) -> bool:
|
|
111
|
+
path = memory / rel_path
|
|
112
|
+
checked.append(rel_path.as_posix())
|
|
113
|
+
if not path.exists():
|
|
114
|
+
errors.append(f"{rel_path.as_posix()}: required file is missing")
|
|
115
|
+
return False
|
|
116
|
+
if not path.is_file():
|
|
117
|
+
errors.append(f"{rel_path.as_posix()}: expected a file")
|
|
118
|
+
return False
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def validate_json(path: Path, memory: Path, errors: list[str], checked: list[str]) -> None:
|
|
123
|
+
checked.append(relative(path, memory))
|
|
124
|
+
try:
|
|
125
|
+
json.loads(path.read_text(encoding="utf-8"))
|
|
126
|
+
except json.JSONDecodeError as exc:
|
|
127
|
+
errors.append(f"{relative(path, memory)}: invalid JSON at line {exc.lineno}, column {exc.colno}: {exc.msg}")
|
|
128
|
+
except OSError as exc:
|
|
129
|
+
errors.append(f"{relative(path, memory)}: could not read JSON: {exc}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def raw_source_points_to_provider(raw_source: Any) -> bool:
|
|
133
|
+
return isinstance(raw_source, str) and raw_source.endswith(("github-facets.json", "gitlab-facets.json"))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def validate_markdown(path: Path, memory: Path, errors: list[str], warnings: list[str], checked: list[str]) -> None:
|
|
137
|
+
checked.append(relative(path, memory))
|
|
138
|
+
try:
|
|
139
|
+
text = path.read_text(encoding="utf-8")
|
|
140
|
+
except OSError as exc:
|
|
141
|
+
errors.append(f"{relative(path, memory)}: could not read Markdown: {exc}")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
frontmatter = parse_frontmatter(text)
|
|
145
|
+
rel = relative(path, memory)
|
|
146
|
+
if not frontmatter.get("schema"):
|
|
147
|
+
errors.append(f"{rel}: frontmatter field 'schema' is missing")
|
|
148
|
+
|
|
149
|
+
placeholders = placeholder_matches(text)
|
|
150
|
+
if placeholders:
|
|
151
|
+
errors.append(f"{rel}: unresolved bracket placeholder(s): {', '.join(placeholders)}")
|
|
152
|
+
|
|
153
|
+
if path.parent.name != "resources":
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
for field in ["source", "resource_count", "trust_state", "raw_source"]:
|
|
157
|
+
if field not in frontmatter:
|
|
158
|
+
errors.append(f"{rel}: frontmatter field '{field}' is missing")
|
|
159
|
+
|
|
160
|
+
expected = frontmatter.get("resource_count")
|
|
161
|
+
actual = item_section_count(text)
|
|
162
|
+
if isinstance(expected, int):
|
|
163
|
+
if expected != actual:
|
|
164
|
+
errors.append(f"{rel}: resource_count is {expected}, but found {actual} item section(s)")
|
|
165
|
+
elif expected is not None:
|
|
166
|
+
errors.append(f"{rel}: resource_count must be an integer")
|
|
167
|
+
|
|
168
|
+
source = frontmatter.get("source")
|
|
169
|
+
raw_source = frontmatter.get("raw_source")
|
|
170
|
+
if "source" in frontmatter and not source:
|
|
171
|
+
errors.append(f"{rel}: frontmatter field 'source' must not be empty")
|
|
172
|
+
if "trust_state" in frontmatter and not frontmatter.get("trust_state"):
|
|
173
|
+
errors.append(f"{rel}: frontmatter field 'trust_state' must not be empty")
|
|
174
|
+
if source in DISABLED_RESOURCE_SOURCES and isinstance(expected, int) and expected != 0:
|
|
175
|
+
errors.append(f"{rel}: disabled or unavailable resource source {source!r} must use resource_count 0")
|
|
176
|
+
if source in DISABLED_RESOURCE_SOURCES and raw_source:
|
|
177
|
+
errors.append(f"{rel}: disabled or unavailable resource source {source!r} must use an empty raw_source")
|
|
178
|
+
if path.name in {"commits.md", "prs.md", "issues.md"} and source not in DISABLED_RESOURCE_SOURCES and raw_source == "":
|
|
179
|
+
errors.append(f"{rel}: empty raw_source requires a disabled or unavailable source")
|
|
180
|
+
if path.name in {"prs.md", "issues.md"} and raw_source_points_to_provider(raw_source):
|
|
181
|
+
provider_path = (path.parent / raw_source).resolve()
|
|
182
|
+
if not provider_path.exists():
|
|
183
|
+
errors.append(f"{rel}: provider raw evidence is missing for raw_source {raw_source!r}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def provider_raw_paths(memory: Path) -> list[Path]:
|
|
187
|
+
return [memory / rel for rel in PROVIDER_RAW_FILES if (memory / rel).exists()]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def validate(memory: Path) -> dict[str, Any]:
|
|
191
|
+
errors: list[str] = []
|
|
192
|
+
warnings: list[str] = []
|
|
193
|
+
checked: list[str] = []
|
|
194
|
+
|
|
195
|
+
if not memory.exists():
|
|
196
|
+
errors.append(f"{memory}: .repo_memory directory is missing")
|
|
197
|
+
return {"ok": False, "errors": errors, "warnings": warnings, "checked": checked}
|
|
198
|
+
if not memory.is_dir():
|
|
199
|
+
errors.append(f"{memory}: expected .repo_memory to be a directory")
|
|
200
|
+
return {"ok": False, "errors": errors, "warnings": warnings, "checked": checked}
|
|
201
|
+
|
|
202
|
+
for rel_path in BASELINE_FILES:
|
|
203
|
+
check_exists(memory, rel_path, errors, checked)
|
|
204
|
+
|
|
205
|
+
markdown_paths = sorted(
|
|
206
|
+
path
|
|
207
|
+
for path in memory.rglob("*.md")
|
|
208
|
+
if path.is_file()
|
|
209
|
+
and path.relative_to(memory) != USER_PROFILE_SIDECAR
|
|
210
|
+
and path.relative_to(memory).parts[0] != PROCEDURE_SIDECAR_DIRECTORY
|
|
211
|
+
)
|
|
212
|
+
for path in markdown_paths:
|
|
213
|
+
if path.exists() and path.is_file():
|
|
214
|
+
validate_markdown(path, memory, errors, warnings, checked)
|
|
215
|
+
|
|
216
|
+
json_paths = sorted(path for path in (memory / "raw").rglob("*.json") if path.is_file())
|
|
217
|
+
provider_raw = provider_raw_paths(memory)
|
|
218
|
+
|
|
219
|
+
if provider_raw:
|
|
220
|
+
for rel_path in PROVIDER_RESOURCE_FILES:
|
|
221
|
+
check_exists(memory, rel_path, errors, checked)
|
|
222
|
+
|
|
223
|
+
seen_json: set[Path] = set()
|
|
224
|
+
for path in json_paths:
|
|
225
|
+
if path.exists() and path.is_file() and path not in seen_json:
|
|
226
|
+
seen_json.add(path)
|
|
227
|
+
validate_json(path, memory, errors, checked)
|
|
228
|
+
|
|
229
|
+
checked = sorted(dict.fromkeys(checked))
|
|
230
|
+
return {"ok": not errors, "errors": errors, "warnings": warnings, "checked": checked}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
234
|
+
parser = argparse.ArgumentParser(description="Validate a repo-memory bundle.")
|
|
235
|
+
parser.add_argument("path", help="Path to a repository or its .repo_memory directory")
|
|
236
|
+
parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output")
|
|
237
|
+
return parser.parse_args(argv)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def main(argv: list[str]) -> int:
|
|
241
|
+
args = parse_args(argv)
|
|
242
|
+
report = validate(resolve_memory_root(Path(args.path)))
|
|
243
|
+
print(json.dumps(report, ensure_ascii=False, indent=2 if args.pretty else None))
|
|
244
|
+
return 0 if report["ok"] else 1
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
if __name__ == "__main__":
|
|
248
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
defaultCodeBuddyHome,
|
|
4
|
+
disableCodeBuddyAdapter,
|
|
5
|
+
enableCodeBuddyAdapter,
|
|
6
|
+
readCodeBuddyAdapterStatus,
|
|
7
|
+
removeCodeBuddyPluginInstallation,
|
|
8
|
+
} from "./config.mjs";
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const parsed = parseCli(process.argv);
|
|
12
|
+
if (parsed.help) {
|
|
13
|
+
console.log("Usage: memorax-code-codebuddy [status|enable|disable|remove] [--codebuddy-home DIR] [--json]");
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
const options = { codeBuddyHome: parsed.home };
|
|
17
|
+
const result = parsed.command === "status"
|
|
18
|
+
? await readCodeBuddyAdapterStatus(options)
|
|
19
|
+
: parsed.command === "enable"
|
|
20
|
+
? await enableCodeBuddyAdapter(options)
|
|
21
|
+
: parsed.command === "disable"
|
|
22
|
+
? await disableCodeBuddyAdapter(options)
|
|
23
|
+
: parsed.command === "remove"
|
|
24
|
+
? await removeCodeBuddyPluginInstallation(options)
|
|
25
|
+
: undefined;
|
|
26
|
+
if (!result) throw new Error(`unknown command: ${parsed.command}`);
|
|
27
|
+
if (parsed.json) console.log(JSON.stringify(result, null, 2));
|
|
28
|
+
else console.log(`${result.action}: ${result.ok ? "ok" : "failed"}\nhome: ${result.codeBuddyHome ?? parsed.home}`);
|
|
29
|
+
const ready = result.ok === true
|
|
30
|
+
&& result.installed === true
|
|
31
|
+
&& result.enabled === true
|
|
32
|
+
&& result.marketplaceReady === true
|
|
33
|
+
&& result.codebuddyHooks?.ok === true
|
|
34
|
+
&& result.codebuddySkills?.ok === true;
|
|
35
|
+
process.exit(parsed.command === "status" ? (ready ? 0 : 1) : (result.ok ? 0 : 1));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseCli(argv) {
|
|
42
|
+
const args = argv.slice(2);
|
|
43
|
+
const command = args[0] && !args[0].startsWith("-") ? args.shift() : "status";
|
|
44
|
+
let home;
|
|
45
|
+
let json = false;
|
|
46
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
47
|
+
const arg = args[index];
|
|
48
|
+
if (arg === "--help") return { command, help: true };
|
|
49
|
+
if (arg === "--json") { json = true; continue; }
|
|
50
|
+
if (arg === "--codebuddy-home") {
|
|
51
|
+
home = args[++index];
|
|
52
|
+
if (!home || home.startsWith("--")) throw new Error("--codebuddy-home requires a value");
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`unknown option: ${arg}`);
|
|
56
|
+
}
|
|
57
|
+
return { command, home: home ?? defaultCodeBuddyHome(), json, help: false };
|
|
58
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { chmod, cp, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join, relative, sep, win32 } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveHookCodeBuddyCommand } from "../../memorax-code-adapter-common/src/clients/codebuddy-command.mjs";
|
|
7
|
+
import {
|
|
8
|
+
codeBuddyHookManifestConfigured,
|
|
9
|
+
materializeCodeBuddyHookManifest,
|
|
10
|
+
} from "./hook-manifest.mjs";
|
|
11
|
+
import {
|
|
12
|
+
codeBuddyRuntimeObservationPath,
|
|
13
|
+
readCodeBuddyRuntimeObservation,
|
|
14
|
+
} from "./runtime-observation.mjs";
|
|
15
|
+
|
|
16
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
17
|
+
const VERSION = readPluginVersion();
|
|
18
|
+
const PLUGIN_NAME = "memorax-code-codebuddy-adapter";
|
|
19
|
+
const MARKETPLACE_NAME = "memorax-code-local";
|
|
20
|
+
const PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
21
|
+
|
|
22
|
+
export function defaultCodeBuddyHome(
|
|
23
|
+
env = process.env,
|
|
24
|
+
homeDir = homedir(),
|
|
25
|
+
platform = process.platform,
|
|
26
|
+
pathExists = existsSync,
|
|
27
|
+
) {
|
|
28
|
+
const configured = env.CODEBUDDY_HOME?.trim() || env.WORKBUDDY_HOME?.trim();
|
|
29
|
+
if (configured) return configured;
|
|
30
|
+
if (platform !== "win32") return join(homeDir, ".workbuddy");
|
|
31
|
+
const workBuddyHome = win32.join(homeDir, ".workbuddy");
|
|
32
|
+
const legacyCodeBuddyHome = win32.join(homeDir, ".codebuddy");
|
|
33
|
+
return pathExists(workBuddyHome) || !pathExists(legacyCodeBuddyHome)
|
|
34
|
+
? workBuddyHome
|
|
35
|
+
: legacyCodeBuddyHome;
|
|
36
|
+
}
|
|
37
|
+
// CodeBuddy stores installed plugin caches under the marketplace namespace.
|
|
38
|
+
export function codeBuddyInstallPath(home = defaultCodeBuddyHome()) { return join(home, "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME, VERSION); }
|
|
39
|
+
export function installedRegistryPath(home = defaultCodeBuddyHome()) { return join(home, "plugins", "installed_plugins.json"); }
|
|
40
|
+
export function codeBuddySettingsPath(home = defaultCodeBuddyHome()) { return join(home, "settings.json"); }
|
|
41
|
+
export function knownMarketplacesPath(home = defaultCodeBuddyHome()) { return join(home, "plugins", "known_marketplaces.json"); }
|
|
42
|
+
export function marketplaceRoot(home = defaultCodeBuddyHome()) { return join(home, "plugins", "marketplaces", MARKETPLACE_NAME); }
|
|
43
|
+
export function marketplacePluginPath(home = defaultCodeBuddyHome()) { return join(marketplaceRoot(home), "plugins", PLUGIN_NAME); }
|
|
44
|
+
|
|
45
|
+
export async function enableCodeBuddyAdapter(options = {}) {
|
|
46
|
+
const home = options.codeBuddyHome ?? defaultCodeBuddyHome();
|
|
47
|
+
const platform = options.platform ?? process.platform;
|
|
48
|
+
const memoraxCodeHome = options.memoraxCodeHome ?? defaultMemoraxCodeHome();
|
|
49
|
+
const installPath = options.installPath ?? codeBuddyInstallPath(home);
|
|
50
|
+
const localPluginPath = marketplacePluginPath(home);
|
|
51
|
+
await rm(codeBuddyRuntimeObservationPath(memoraxCodeHome), { force: true });
|
|
52
|
+
await mkdir(dirname(installPath), { recursive: true });
|
|
53
|
+
await rm(installPath, { recursive: true, force: true });
|
|
54
|
+
await rm(legacyCodeBuddyInstallPath(home), { recursive: true, force: true });
|
|
55
|
+
await cp(ROOT, installPath, { recursive: true, force: true, filter: packageCopyFilter(ROOT) });
|
|
56
|
+
await materializeCommonRuntime(installPath);
|
|
57
|
+
await materializeCodeBuddyHookManifest(installPath, platform);
|
|
58
|
+
await writePackageMetadata(installPath, options.codeBuddyCommand, home);
|
|
59
|
+
await materializeCanonicalSkill(installPath);
|
|
60
|
+
await mkdir(dirname(localPluginPath), { recursive: true });
|
|
61
|
+
await rm(localPluginPath, { recursive: true, force: true });
|
|
62
|
+
await cp(ROOT, localPluginPath, { recursive: true, force: true, filter: packageCopyFilter(ROOT) });
|
|
63
|
+
await materializeCommonRuntime(localPluginPath);
|
|
64
|
+
await materializeCodeBuddyHookManifest(localPluginPath, platform);
|
|
65
|
+
await writePackageMetadata(localPluginPath, options.codeBuddyCommand, home);
|
|
66
|
+
await materializeCanonicalSkill(localPluginPath);
|
|
67
|
+
await writeMarketplaceManifest(home);
|
|
68
|
+
await updateKnownMarketplace(home, true);
|
|
69
|
+
await updateSettings(home, (settings) => {
|
|
70
|
+
settings.enabledPlugins = recordValue(settings.enabledPlugins);
|
|
71
|
+
settings.enabledPlugins[PLUGIN_ID] = true;
|
|
72
|
+
});
|
|
73
|
+
await updateLegacyRegistry(home, { installPath, enabled: true });
|
|
74
|
+
await removeLegacyManagedInstallation(home, platform);
|
|
75
|
+
return { ok: true, action: "enable", runtime: "codebuddy", integration: "hooks", installed: true, enabled: true, codeBuddyHome: home, installPath, marketplace: MARKETPLACE_NAME, pluginId: PLUGIN_ID, marketplacePath: localPluginPath, codebuddyHooks: { ok: true, configured: true, runtimeObserved: false, status: "unverified" }, codebuddySkills: { ok: true, status: "installed", managed: true, memoraxCode: true, path: join(localPluginPath, "skills", "memorax-code", "SKILL.md") } };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function disableCodeBuddyAdapter(options = {}) {
|
|
79
|
+
const home = options.codeBuddyHome ?? defaultCodeBuddyHome();
|
|
80
|
+
const platform = options.platform ?? process.platform;
|
|
81
|
+
const registryPath = installedRegistryPath(home);
|
|
82
|
+
const legacyHome = legacyCodeBuddyHome(home, platform);
|
|
83
|
+
const installed = await managedCodeBuddyInstallationExists(home);
|
|
84
|
+
const legacyInstalled = legacyHome ? await managedCodeBuddyInstallationExists(legacyHome) : false;
|
|
85
|
+
if (!installed && !legacyInstalled) return { ok: true, action: "disable", runtime: "codebuddy", installed: false, enabled: false, codeBuddyHome: home, statePath: registryPath, marketplace: MARKETPLACE_NAME, pluginId: PLUGIN_ID };
|
|
86
|
+
if (installed) await disableManagedCodeBuddyInstallation(home);
|
|
87
|
+
if (legacyInstalled) await disableManagedCodeBuddyInstallation(legacyHome);
|
|
88
|
+
return { ok: true, action: "disable", runtime: "codebuddy", installed: true, enabled: false, codeBuddyHome: home, statePath: registryPath, marketplace: MARKETPLACE_NAME, pluginId: PLUGIN_ID, legacyCodeBuddyHome: legacyInstalled ? legacyHome : undefined, legacyManaged: legacyInstalled };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function readCodeBuddyAdapterStatus(options = {}) {
|
|
92
|
+
const home = options.codeBuddyHome ?? defaultCodeBuddyHome();
|
|
93
|
+
const platform = options.platform ?? process.platform;
|
|
94
|
+
const memoraxCodeHome = options.memoraxCodeHome ?? defaultMemoraxCodeHome();
|
|
95
|
+
const installPath = codeBuddyInstallPath(home);
|
|
96
|
+
const localPluginPath = marketplacePluginPath(home);
|
|
97
|
+
const settings = await readJsonRecord(codeBuddySettingsPath(home));
|
|
98
|
+
const known = await readJsonRecord(knownMarketplacesPath(home));
|
|
99
|
+
const installedRoots = [];
|
|
100
|
+
for (const root of [localPluginPath, installPath]) {
|
|
101
|
+
if (await pathExists(root)) installedRoots.push(root);
|
|
102
|
+
}
|
|
103
|
+
const installed = installedRoots.length > 0;
|
|
104
|
+
const skillPath = join(localPluginPath, "skills", "memorax-code", "SKILL.md");
|
|
105
|
+
const skillInstalled = await pathExists(skillPath);
|
|
106
|
+
const enabled = settings.enabledPlugins?.[PLUGIN_ID] === true;
|
|
107
|
+
const marketplaceReady = Boolean(known[MARKETPLACE_NAME]);
|
|
108
|
+
const legacyHome = legacyCodeBuddyHome(home, platform);
|
|
109
|
+
const legacyManaged = legacyHome ? await managedCodeBuddyInstallationExists(legacyHome) : false;
|
|
110
|
+
const hookConfigured = installedRoots.length > 0
|
|
111
|
+
&& (await Promise.all(installedRoots.map((root) => codeBuddyHookManifestConfigured(root, platform)))).every(Boolean);
|
|
112
|
+
const observation = await readCodeBuddyRuntimeObservation(memoraxCodeHome);
|
|
113
|
+
const runtimeObserved = hookConfigured && observationMatches(observation, home, platform);
|
|
114
|
+
return { ok: true, action: "status", runtime: "codebuddy", integration: "hooks", installed, enabled, managed: installed && marketplaceReady, codeBuddyHome: home, installPath, marketplace: MARKETPLACE_NAME, pluginId: PLUGIN_ID, marketplaceReady, legacyCodeBuddyHome: legacyManaged ? legacyHome : undefined, legacyManaged, codebuddyHooks: { ok: hookConfigured, configured: hookConfigured, runtimeObserved, status: hookConfigured ? (runtimeObserved ? "observed" : "unverified") : "invalid", observationPath: codeBuddyRuntimeObservationPath(memoraxCodeHome) }, codebuddySkills: { ok: skillInstalled, status: skillInstalled ? "installed" : "missing", managed: skillInstalled, memoraxCode: skillInstalled, path: skillPath } };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function removeCodeBuddyPluginInstallation(options = {}) {
|
|
118
|
+
const home = options.codeBuddyHome ?? defaultCodeBuddyHome();
|
|
119
|
+
const platform = options.platform ?? process.platform;
|
|
120
|
+
const legacyHome = legacyCodeBuddyHome(home, platform);
|
|
121
|
+
const removed = await removeManagedCodeBuddyInstallation(home);
|
|
122
|
+
const legacyRemoved = legacyHome ? await removeManagedCodeBuddyInstallation(legacyHome) : false;
|
|
123
|
+
return { ok: true, action: "codebuddy-plugin-remove", runtime: "codebuddy", installed: false, enabled: false, removed: removed || legacyRemoved, codeBuddyHome: home, statePath: installedRegistryPath(home), marketplace: MARKETPLACE_NAME, pluginId: PLUGIN_ID };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function disableManagedCodeBuddyInstallation(home) {
|
|
127
|
+
await updateSettings(home, (settings) => {
|
|
128
|
+
settings.enabledPlugins = recordValue(settings.enabledPlugins);
|
|
129
|
+
settings.enabledPlugins[PLUGIN_ID] = false;
|
|
130
|
+
});
|
|
131
|
+
await updateLegacyRegistry(home, { installPath: codeBuddyInstallPath(home), enabled: false });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function removeLegacyManagedInstallation(home, platform) {
|
|
135
|
+
const legacyHome = legacyCodeBuddyHome(home, platform);
|
|
136
|
+
return legacyHome ? await removeManagedCodeBuddyInstallation(legacyHome) : false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function removeManagedCodeBuddyInstallation(home) {
|
|
140
|
+
if (!await managedCodeBuddyInstallationExists(home)) return false;
|
|
141
|
+
await updateJsonRecordIfPresent(codeBuddySettingsPath(home), (settings) => {
|
|
142
|
+
settings.enabledPlugins = recordValue(settings.enabledPlugins);
|
|
143
|
+
delete settings.enabledPlugins[PLUGIN_ID];
|
|
144
|
+
});
|
|
145
|
+
await updateJsonRecordIfPresent(knownMarketplacesPath(home), (known) => {
|
|
146
|
+
delete known[MARKETPLACE_NAME];
|
|
147
|
+
});
|
|
148
|
+
await updateJsonRecordIfPresent(installedRegistryPath(home), (value) => {
|
|
149
|
+
value.version = 2;
|
|
150
|
+
value.plugins = recordValue(value.plugins);
|
|
151
|
+
delete value.plugins[PLUGIN_ID];
|
|
152
|
+
});
|
|
153
|
+
await rm(codeBuddyPluginCacheRoot(home), { recursive: true, force: true });
|
|
154
|
+
await rm(legacyCodeBuddyPluginCacheRoot(home), { recursive: true, force: true });
|
|
155
|
+
await rm(marketplaceRoot(home), { recursive: true, force: true });
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function managedCodeBuddyInstallationExists(home) {
|
|
160
|
+
if (await pathExists(marketplaceRoot(home))
|
|
161
|
+
|| await pathExists(codeBuddyPluginCacheRoot(home))
|
|
162
|
+
|| await pathExists(legacyCodeBuddyPluginCacheRoot(home))) return true;
|
|
163
|
+
const settings = await readJsonRecord(codeBuddySettingsPath(home));
|
|
164
|
+
const known = await readJsonRecord(knownMarketplacesPath(home));
|
|
165
|
+
const registry = await readJsonRecord(installedRegistryPath(home));
|
|
166
|
+
return Object.hasOwn(recordValue(settings.enabledPlugins), PLUGIN_ID)
|
|
167
|
+
|| Object.hasOwn(known, MARKETPLACE_NAME)
|
|
168
|
+
|| Object.hasOwn(recordValue(registry.plugins), PLUGIN_ID);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function legacyCodeBuddyHome(home, platform) {
|
|
172
|
+
return platform === "win32" && basename(home).toLowerCase() === ".workbuddy"
|
|
173
|
+
? join(dirname(home), ".codebuddy")
|
|
174
|
+
: undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function readRegistry(home) {
|
|
178
|
+
try {
|
|
179
|
+
const value = JSON.parse(await readFile(installedRegistryPath(home), "utf8"));
|
|
180
|
+
if (value?.plugins && typeof value.plugins === "object" && !Array.isArray(value.plugins)) return value.plugins;
|
|
181
|
+
throw new Error(`invalid CodeBuddy plugin registry: ${installedRegistryPath(home)}`);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error?.code === "ENOENT") return {};
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function writeRegistry(home, plugins) { await writeJsonFile(installedRegistryPath(home), { version: 2, plugins }); }
|
|
188
|
+
async function updateLegacyRegistry(home, { installPath, enabled }) {
|
|
189
|
+
await updateRegistry(home, (registry) => {
|
|
190
|
+
registry[PLUGIN_ID] = [{ scope: "user", installPath, version: VERSION, enabled, installedAt: new Date().toISOString(), lastUpdated: new Date().toISOString() }];
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function writeMarketplaceManifest(home) {
|
|
194
|
+
const path = join(marketplaceRoot(home), ".codebuddy-plugin", "marketplace.json");
|
|
195
|
+
await writeJsonFile(path, {
|
|
196
|
+
name: MARKETPLACE_NAME,
|
|
197
|
+
description: "MemoraX Code local integration marketplace",
|
|
198
|
+
plugins: [{ name: PLUGIN_NAME, source: `./plugins/${PLUGIN_NAME}`, version: VERSION, description: "MemoraX Code memory integration for CodeBuddy and WorkBuddy." }],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async function updateKnownMarketplace(home, enabled) {
|
|
202
|
+
const path = knownMarketplacesPath(home);
|
|
203
|
+
const known = await readJsonRecord(path);
|
|
204
|
+
if (enabled) {
|
|
205
|
+
known[MARKETPLACE_NAME] = {
|
|
206
|
+
type: "directory",
|
|
207
|
+
source: { source: "directory", path: marketplaceRoot(home) },
|
|
208
|
+
installLocation: marketplaceRoot(home),
|
|
209
|
+
description: "MemoraX Code local integration marketplace",
|
|
210
|
+
lastUpdated: new Date().toISOString(),
|
|
211
|
+
autoUpdate: false,
|
|
212
|
+
};
|
|
213
|
+
} else {
|
|
214
|
+
delete known[MARKETPLACE_NAME];
|
|
215
|
+
}
|
|
216
|
+
await writeJsonFile(path, known);
|
|
217
|
+
}
|
|
218
|
+
async function updateSettings(home, mutate) {
|
|
219
|
+
const path = codeBuddySettingsPath(home);
|
|
220
|
+
await updateJsonRecord(path, mutate);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function updateRegistry(home, mutate) {
|
|
224
|
+
const path = installedRegistryPath(home);
|
|
225
|
+
await updateJsonRecord(path, (value) => {
|
|
226
|
+
value.version = 2;
|
|
227
|
+
value.plugins = recordValue(value.plugins);
|
|
228
|
+
mutate(value.plugins);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function updateJsonRecord(path, mutate) {
|
|
233
|
+
const lockPath = `${path}.lock`;
|
|
234
|
+
await mkdir(dirname(path), { recursive: true });
|
|
235
|
+
let handle;
|
|
236
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
237
|
+
try {
|
|
238
|
+
handle = await import("node:fs/promises").then(({ open }) => open(lockPath, "wx", 0o600));
|
|
239
|
+
break;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error?.code !== "EEXIST") throw error;
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(50, 5 + attempt)));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!handle) throw new Error(`timed out acquiring lock: ${lockPath}`);
|
|
246
|
+
try {
|
|
247
|
+
const value = await readJsonRecord(path);
|
|
248
|
+
mutate(value);
|
|
249
|
+
await writeJsonFile(path, value);
|
|
250
|
+
} finally {
|
|
251
|
+
await handle.close();
|
|
252
|
+
await rm(lockPath, { force: true });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function updateJsonRecordIfPresent(path, mutate) {
|
|
257
|
+
if (await pathExists(path)) await updateJsonRecord(path, mutate);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function readJsonRecord(path) {
|
|
261
|
+
try {
|
|
262
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
263
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
264
|
+
throw new Error(`invalid JSON object: ${path}`);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error?.code === "ENOENT") return {};
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function writeJsonFile(path, value) {
|
|
271
|
+
await mkdir(dirname(path), { recursive: true });
|
|
272
|
+
let mode = 0o600;
|
|
273
|
+
try { mode = (await stat(path)).mode & 0o777; } catch {}
|
|
274
|
+
const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
275
|
+
await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { mode });
|
|
276
|
+
await chmod(temp, mode);
|
|
277
|
+
await rename(temp, path);
|
|
278
|
+
}
|
|
279
|
+
async function pathExists(path) { try { await stat(path); return true; } catch { return false; } }
|
|
280
|
+
function recordValue(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; }
|
|
281
|
+
function readPluginVersion() {
|
|
282
|
+
const manifest = JSON.parse(readFileSync(join(ROOT, ".codebuddy-plugin", "plugin.json"), "utf8"));
|
|
283
|
+
if (typeof manifest?.version !== "string" || !manifest.version.trim()) {
|
|
284
|
+
throw new Error("MemoraX Code CodeBuddy plugin manifest has no version.");
|
|
285
|
+
}
|
|
286
|
+
return manifest.version.trim();
|
|
287
|
+
}
|
|
288
|
+
function legacyCodeBuddyInstallPath(home) { return join(home, "plugins", "cache", PLUGIN_NAME, VERSION); }
|
|
289
|
+
function codeBuddyPluginCacheRoot(home) { return dirname(codeBuddyInstallPath(home)); }
|
|
290
|
+
function legacyCodeBuddyPluginCacheRoot(home) { return dirname(legacyCodeBuddyInstallPath(home)); }
|
|
291
|
+
|
|
292
|
+
async function materializeCanonicalSkill(destination) {
|
|
293
|
+
const packagedSkill = join(ROOT, "skills", "memorax-code");
|
|
294
|
+
const canonicalSkill = join(ROOT, "..", "memorax-code-codex-adapter", "skills", "memorax-code");
|
|
295
|
+
const source = await pathExists(packagedSkill) ? packagedSkill : canonicalSkill;
|
|
296
|
+
if (!await pathExists(source)) throw new Error(`MemoraX Code canonical skill is unavailable: ${source}`);
|
|
297
|
+
const target = join(destination, "skills", "memorax-code");
|
|
298
|
+
await rm(target, { recursive: true, force: true });
|
|
299
|
+
await mkdir(dirname(target), { recursive: true });
|
|
300
|
+
await cp(source, target, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function writePackageMetadata(destination, configuredCommand, codeBuddyHome) {
|
|
304
|
+
const codeBuddyCommand = typeof configuredCommand === "string" && configuredCommand.trim()
|
|
305
|
+
? configuredCommand.trim()
|
|
306
|
+
: resolveHookCodeBuddyCommand();
|
|
307
|
+
await writeJsonFile(join(destination, ".memorax-code-package.json"), {
|
|
308
|
+
version: 1,
|
|
309
|
+
codeBuddyCommand,
|
|
310
|
+
codeBuddyHome,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function materializeCommonRuntime(destination) {
|
|
315
|
+
const source = join(ROOT, "..", "memorax-code-adapter-common", "src");
|
|
316
|
+
const target = join(destination, "memorax-code-adapter-common", "src");
|
|
317
|
+
await rm(target, { recursive: true, force: true });
|
|
318
|
+
await mkdir(dirname(target), { recursive: true });
|
|
319
|
+
await cp(source, target, { recursive: true, force: true, filter: packageCopyFilter(source) });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function packageCopyFilter(root) {
|
|
323
|
+
return (source) => !relative(root, source).split(sep).some((part) => part === "test" || part === "node_modules");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function defaultMemoraxCodeHome() {
|
|
327
|
+
return process.env.MEMORAX_CODE_HOME?.trim() || join(homedir(), ".memorax-code");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function observationMatches(observation, codeBuddyHome, platform) {
|
|
331
|
+
if (observation?.pluginVersion !== VERSION) return false;
|
|
332
|
+
const left = comparablePath(observation.codeBuddyHome, platform);
|
|
333
|
+
const right = comparablePath(codeBuddyHome, platform);
|
|
334
|
+
return left === right;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function comparablePath(value, platform) {
|
|
338
|
+
const normalized = String(value ?? "").replaceAll("\\", "/").replace(/\/+$/, "");
|
|
339
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
340
|
+
}
|