@memorax/memorax-code 0.1.3 → 0.1.5
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 +1 -1
- package/docs/configuration.md +5 -1
- package/lib/memorax-code-backend/dist/lifecycle/backend/process.js +21 -0
- package/lib/memorax-code-backend/package.json +2 -2
- 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-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/package.json +1 -1
- 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-dsh-adapter/package.json +1 -1
- package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +78 -9
- package/lib/memorax-code-opencode-adapter/package.json +1 -1
- package/lib/memorax-code-opencode-adapter/src/plugin.mjs +3 -0
- package/lib/memorax-code-opencode-adapter/src/repo-memory-server-runner.mjs +253 -18
- package/lib/node-version.mjs +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Harness, and OpenCode.
|
|
|
5
5
|
|
|
6
6
|
## Requirements
|
|
7
7
|
|
|
8
|
-
- Node.js
|
|
8
|
+
- Node.js 20 or newer (Node.js 24 LTS recommended) and npm.
|
|
9
9
|
- At least one of Codex, Claude Code, DeepSeek Harness, OpenCode Desktop, or
|
|
10
10
|
the OpenCode CLI.
|
|
11
11
|
- A MemoraX account, Base User ID, and API key for memory features.
|
package/docs/configuration.md
CHANGED
|
@@ -96,7 +96,11 @@ $DSH_HOME/profiles/<profile-name>/
|
|
|
96
96
|
under `$MEMORAX_CODE_HOME/adapters/dsh/runtime/generations/` and installed into
|
|
97
97
|
Profiles through DSH's native plugin command. The globally installed npm
|
|
98
98
|
package remains immutable; do not copy or edit the generated state or runtime
|
|
99
|
-
directories by hand.
|
|
99
|
+
directories by hand. When at least one valid Profile exists but none provides
|
|
100
|
+
a loadable `@deepseek-ai/dsh-headless` bundle, MemoraX Code asks the same native
|
|
101
|
+
plugin command to initialize the standard `headless` Profile when that name is
|
|
102
|
+
available. Stop and uninstall remove the MemoraX Code adapter from this Profile
|
|
103
|
+
but preserve the Profile and its native data.
|
|
100
104
|
|
|
101
105
|
MemoraX Code is tested with DSH `0.1.0-rc.6`. Other valid semantic versions are
|
|
102
106
|
accepted but appear as untested in status output; compatibility is not
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
const POSIX_PROCESS_PROBE_TIMEOUT_MS = 2_000;
|
|
4
5
|
const WINDOWS_PROCESS_PROBE_TIMEOUT_MS = 10_000;
|
|
@@ -48,6 +49,26 @@ export function probeProcessCommandLine(pid, runtime = {}) {
|
|
|
48
49
|
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
49
50
|
return { status: "inconclusive", reason: "invalid_pid", timeoutMs };
|
|
50
51
|
}
|
|
52
|
+
if (platform === "linux") {
|
|
53
|
+
try {
|
|
54
|
+
const rawCommandLine = (runtime.readFileSync ?? readFileSync)(`/proc/${pid}/cmdline`);
|
|
55
|
+
const commandLine = rawCommandLine.toString("utf8").replaceAll("\0", " ").trim();
|
|
56
|
+
return commandLine
|
|
57
|
+
? { status: "ok", commandLine }
|
|
58
|
+
: { status: "not_found" };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const code = error instanceof Error
|
|
62
|
+
? error.code
|
|
63
|
+
: undefined;
|
|
64
|
+
return {
|
|
65
|
+
status: "inconclusive",
|
|
66
|
+
reason: "read_error",
|
|
67
|
+
timeoutMs,
|
|
68
|
+
...(code ? { code } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
51
72
|
const run = runtime.spawnSync ?? runProbeCommand;
|
|
52
73
|
let command;
|
|
53
74
|
let args;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memorax-code/backend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Local memory integration, lifecycle, trace, and diagnostics for MemoraX Code.",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"test:memory-viewer": "npm run build && node --test --test-timeout=5000 --test-force-exit test/viewer/http/memory-viewer-routes.test.mjs"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@types/node": "^
|
|
20
|
+
"@types/node": "^20.0.0",
|
|
21
21
|
"typescript": "^5.9.0"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
@@ -50,6 +50,8 @@ const { resolveWindowsCliInvocation } = await import(
|
|
|
50
50
|
const STATE_VERSION = 1;
|
|
51
51
|
const RUNTIME = "dsh";
|
|
52
52
|
const ADAPTER_PACKAGE_NAME = "@memorax-code/dsh-memorax-code";
|
|
53
|
+
const HEADLESS_PROFILE_NAME = "headless";
|
|
54
|
+
const HEADLESS_BUNDLE_NAME = "@deepseek-ai/dsh-headless";
|
|
53
55
|
const LEGACY_ADAPTER_PACKAGE_NAMES = Object.freeze(["@memorax-code/dsh-adapter"]);
|
|
54
56
|
const MANAGED_ADAPTER_PACKAGE_NAMES = Object.freeze([
|
|
55
57
|
ADAPTER_PACKAGE_NAME,
|
|
@@ -142,7 +144,8 @@ export function collectDshAdapterStatus(options = {}) {
|
|
|
142
144
|
);
|
|
143
145
|
const managed = Boolean(state) && !stateProblem;
|
|
144
146
|
const installed = profiles.length > 0
|
|
145
|
-
&& profiles.every((profile) => profile.managed && profile.exists && profile.installed)
|
|
147
|
+
&& profiles.every((profile) => profile.managed && profile.exists && profile.installed)
|
|
148
|
+
&& managedProfilesHaveInstalledHeadlessBundle(discoveredProfiles, managedNames);
|
|
146
149
|
const base = {
|
|
147
150
|
integration: "plugin",
|
|
148
151
|
managed,
|
|
@@ -283,7 +286,8 @@ function readDshPluginStatusUnlocked(paths, options) {
|
|
|
283
286
|
state,
|
|
284
287
|
);
|
|
285
288
|
const installed = state.profiles.length > 0
|
|
286
|
-
&& managedProfiles.every((profile) => profile.installed)
|
|
289
|
+
&& managedProfiles.every((profile) => profile.installed)
|
|
290
|
+
&& managedProfilesHaveInstalledHeadlessBundle(profiles, new Set(state.profiles));
|
|
287
291
|
return {
|
|
288
292
|
ok: true,
|
|
289
293
|
action: "dsh-plugin-status",
|
|
@@ -358,6 +362,38 @@ function ensureDshPluginInstalledUnlocked(paths, options) {
|
|
|
358
362
|
};
|
|
359
363
|
}
|
|
360
364
|
|
|
365
|
+
let workerProfileName = profiles.find(profileHasInstalledHeadlessBundle)?.name;
|
|
366
|
+
let targetProfiles = profiles;
|
|
367
|
+
let initializeWorkerProfile = false;
|
|
368
|
+
if (!workerProfileName) {
|
|
369
|
+
const headlessPath = join(paths.profilesRoot, HEADLESS_PROFILE_NAME);
|
|
370
|
+
const headless = inspectProfile(HEADLESS_PROFILE_NAME, headlessPath);
|
|
371
|
+
if (headless.status === "manifest_unreadable") {
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
action: "dsh-plugin-install",
|
|
375
|
+
runtime: RUNTIME,
|
|
376
|
+
reason: "profile_manifest_unreadable",
|
|
377
|
+
profiles: [HEADLESS_PROFILE_NAME],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (headless.status === "valid") {
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
action: "dsh-plugin-install",
|
|
384
|
+
runtime: RUNTIME,
|
|
385
|
+
reason: "headless_profile_not_capable",
|
|
386
|
+
profiles: [HEADLESS_PROFILE_NAME],
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
workerProfileName = HEADLESS_PROFILE_NAME;
|
|
390
|
+
initializeWorkerProfile = true;
|
|
391
|
+
targetProfiles = [
|
|
392
|
+
...profiles,
|
|
393
|
+
{ name: HEADLESS_PROFILE_NAME, path: headlessPath },
|
|
394
|
+
].sort((left, right) => left.name.localeCompare(right.name));
|
|
395
|
+
}
|
|
396
|
+
|
|
361
397
|
const memoraxCodeCommand = resolveMemoraxCodeCommand(options.memoraxCodeCommand);
|
|
362
398
|
const metadata = {
|
|
363
399
|
version: 1,
|
|
@@ -387,7 +423,7 @@ function ensureDshPluginInstalledUnlocked(paths, options) {
|
|
|
387
423
|
// interrupted or partial native add remains repairable and removable.
|
|
388
424
|
profiles: [...new Set([
|
|
389
425
|
...previouslyManaged,
|
|
390
|
-
...
|
|
426
|
+
...targetProfiles.map((profile) => profile.name),
|
|
391
427
|
])].sort(),
|
|
392
428
|
updatedAt: now,
|
|
393
429
|
};
|
|
@@ -396,9 +432,12 @@ function ensureDshPluginInstalledUnlocked(paths, options) {
|
|
|
396
432
|
const installedProfiles = [];
|
|
397
433
|
const failedProfiles = [];
|
|
398
434
|
const mutatedProfiles = [];
|
|
399
|
-
for (const profile of
|
|
435
|
+
for (const profile of targetProfiles) {
|
|
400
436
|
let current = inspectProfile(profile.name, profile.path);
|
|
401
|
-
if (current.status !== "valid"
|
|
437
|
+
if (current.status !== "valid"
|
|
438
|
+
&& !(initializeWorkerProfile
|
|
439
|
+
&& profile.name === workerProfileName
|
|
440
|
+
&& current.status === "directory_missing")) {
|
|
402
441
|
failedProfiles.push(current.status === "directory_missing"
|
|
403
442
|
? { name: profile.name, reason: "profile_disappeared" }
|
|
404
443
|
: profileManifestFailure(profile.name));
|
|
@@ -420,12 +459,16 @@ function ensureDshPluginInstalledUnlocked(paths, options) {
|
|
|
420
459
|
if (result.status !== 0
|
|
421
460
|
|| result.error
|
|
422
461
|
|| current.status !== "valid"
|
|
423
|
-
|| !profileHasInstalledAdapter(current.profile, runtimeBundleRoot, pendingState)
|
|
462
|
+
|| !profileHasInstalledAdapter(current.profile, runtimeBundleRoot, pendingState)
|
|
463
|
+
|| (profile.name === workerProfileName
|
|
464
|
+
&& !profileHasInstalledHeadlessBundle(current.profile))) {
|
|
424
465
|
failedProfiles.push(profileMutationFailure(
|
|
425
466
|
profile.name,
|
|
426
467
|
result,
|
|
427
468
|
current,
|
|
428
|
-
|
|
469
|
+
profile.name === workerProfileName
|
|
470
|
+
? "headless_profile_not_capable"
|
|
471
|
+
: "dsh_bundle_not_activated",
|
|
429
472
|
));
|
|
430
473
|
continue;
|
|
431
474
|
}
|
|
@@ -450,7 +493,9 @@ function ensureDshPluginInstalledUnlocked(paths, options) {
|
|
|
450
493
|
}
|
|
451
494
|
|
|
452
495
|
if (current.status === "valid"
|
|
453
|
-
&& profileHasInstalledAdapter(current.profile, runtimeBundleRoot, pendingState)
|
|
496
|
+
&& profileHasInstalledAdapter(current.profile, runtimeBundleRoot, pendingState)
|
|
497
|
+
&& (profile.name !== workerProfileName
|
|
498
|
+
|| profileHasInstalledHeadlessBundle(current.profile))) {
|
|
454
499
|
installedProfiles.push(profile.name);
|
|
455
500
|
} else {
|
|
456
501
|
failedProfiles.push(profileMutationFailure(
|
|
@@ -474,6 +519,11 @@ function ensureDshPluginInstalledUnlocked(paths, options) {
|
|
|
474
519
|
failedProfiles.push({ name: profile.name, reason: "dsh_bundle_not_activated" });
|
|
475
520
|
}
|
|
476
521
|
}
|
|
522
|
+
if (!finalProfiles.some((profile) => (
|
|
523
|
+
profile.status === "valid" && profileHasInstalledHeadlessBundle(profile.profile)
|
|
524
|
+
)) && !failedProfiles.some((failure) => failure.name === workerProfileName)) {
|
|
525
|
+
failedProfiles.push({ name: workerProfileName, reason: "headless_profile_not_capable" });
|
|
526
|
+
}
|
|
477
527
|
const managedProfiles = finalProfiles
|
|
478
528
|
.filter((profile) => profile.status !== "directory_missing")
|
|
479
529
|
.map((profile) => profile.name);
|
|
@@ -644,7 +694,8 @@ function activateDshPluginInstallationUnlocked(paths, options) {
|
|
|
644
694
|
if (state.profiles.length === 0
|
|
645
695
|
|| state.profiles.some((name) => (
|
|
646
696
|
!profileHasInstalledAdapter(profileByName.get(name), state.runtimeBundleRoot, state)
|
|
647
|
-
))
|
|
697
|
+
))
|
|
698
|
+
|| !managedProfilesHaveInstalledHeadlessBundle(profiles, new Set(state.profiles))) {
|
|
648
699
|
return {
|
|
649
700
|
ok: false,
|
|
650
701
|
action: "dsh-plugin-activate",
|
|
@@ -1162,6 +1213,24 @@ function profileHasAdapter(profile, packageName = ADAPTER_PACKAGE_NAME) {
|
|
|
1162
1213
|
&& profile.bundles.includes(packageName));
|
|
1163
1214
|
}
|
|
1164
1215
|
|
|
1216
|
+
function managedProfilesHaveInstalledHeadlessBundle(profiles, managedNames) {
|
|
1217
|
+
return profiles.some((profile) => (
|
|
1218
|
+
managedNames.has(profile.name) && profileHasInstalledHeadlessBundle(profile)
|
|
1219
|
+
));
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function profileHasInstalledHeadlessBundle(profile) {
|
|
1223
|
+
if (!profile?.bundles.includes(HEADLESS_BUNDLE_NAME)) return false;
|
|
1224
|
+
try {
|
|
1225
|
+
// DSH exposes built-in bundles through the Profile's shared resolution tree.
|
|
1226
|
+
const requireFromProfile = createRequire(join(profile.path, "package.json"));
|
|
1227
|
+
readFileSync(requireFromProfile.resolve(HEADLESS_BUNDLE_NAME), "utf8");
|
|
1228
|
+
return true;
|
|
1229
|
+
} catch {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1165
1234
|
function profileHasInstalledAdapter(
|
|
1166
1235
|
profile,
|
|
1167
1236
|
runtimeBundleRoot,
|
|
@@ -254,6 +254,8 @@ export function createMemoraxOpenCodePlugin(options = {}) {
|
|
|
254
254
|
output.env.MEMORAX_CODE_MEMORY_CLI_TRACE_CLIENT = "opencode";
|
|
255
255
|
if (openCodeServerUrl) {
|
|
256
256
|
output.env.MEMORAX_CODE_OPENCODE_SERVER_URL = openCodeServerUrl;
|
|
257
|
+
output.env.MEMORAX_CODE_OPENCODE_COMMAND = stringValue(options.openCodeCommand)
|
|
258
|
+
?? process.execPath;
|
|
257
259
|
}
|
|
258
260
|
const sessionId = stringValue(input?.sessionID);
|
|
259
261
|
delete output.env.MEMORAX_CODE_MEMORY_CLI_TRACE_SESSION_ID;
|
|
@@ -441,6 +443,7 @@ function openCodeRepoMemoryEnv(options, serverUrl, sessionId) {
|
|
|
441
443
|
...process.env,
|
|
442
444
|
MEMORAX_CODE_HOME: memoraxCodeHome,
|
|
443
445
|
MEMORAX_CODE_MEMORY_CLI_SESSION_ID: sessionId,
|
|
446
|
+
MEMORAX_CODE_OPENCODE_COMMAND: stringValue(options.openCodeCommand) ?? process.execPath,
|
|
444
447
|
MEMORAX_CODE_OPENCODE_SERVER_URL: serverUrl,
|
|
445
448
|
};
|
|
446
449
|
}
|
|
@@ -1,39 +1,103 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
2
4
|
import { resolve } from "node:path";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
|
|
5
7
|
export const OPENCODE_REPO_MEMORY_AGENT = "memorax-code-repo-memory";
|
|
6
8
|
|
|
9
|
+
const OWNED_SERVER_USERNAME = "memorax-code";
|
|
10
|
+
const DEFAULT_SERVER_START_TIMEOUT_MS = 20_000;
|
|
11
|
+
const DEFAULT_SERVER_STOP_TIMEOUT_MS = 5_000;
|
|
12
|
+
const MAX_SERVER_START_OUTPUT = 8_192;
|
|
13
|
+
|
|
14
|
+
class OpenCodeTransportError extends Error {
|
|
15
|
+
constructor(operation, cause) {
|
|
16
|
+
super(`OpenCode ${operation} request failed: ${errorMessage(cause)}`);
|
|
17
|
+
this.name = "OpenCodeTransportError";
|
|
18
|
+
this.operation = operation;
|
|
19
|
+
this.cause = cause;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
export async function runOpenCodeRepoMemory(input, options = {}) {
|
|
8
24
|
const env = options.env ?? process.env;
|
|
9
25
|
const managedServerUrl = stringValue(env.MEMORAX_CODE_OPENCODE_SERVER_URL);
|
|
10
26
|
const serverUrl = stringValue(input?.serverUrl) ?? managedServerUrl;
|
|
11
|
-
if (!serverUrl) {
|
|
12
|
-
throw new Error(
|
|
13
|
-
"OpenCode repo memory runner requires --server-url or MEMORAX_CODE_OPENCODE_SERVER_URL",
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
27
|
const repo = stringValue(input?.repo);
|
|
17
28
|
if (!repo) throw new Error("OpenCode repo memory runner requires --repo");
|
|
18
29
|
const prompt = stringValue(input?.prompt);
|
|
19
30
|
if (!prompt) throw new Error("OpenCode repo memory runner requires --prompt");
|
|
20
31
|
|
|
21
|
-
const baseUrl = normalizedServerUrl(serverUrl);
|
|
22
|
-
const authorization = serverAuthorization(env, baseUrl, managedServerUrl);
|
|
23
32
|
const directory = resolve(repo);
|
|
24
33
|
const parentID = stringValue(input?.parentID)
|
|
25
34
|
?? stringValue(env.MEMORAX_CODE_MEMORY_CLI_SESSION_ID);
|
|
26
35
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
36
|
+
let inheritedError;
|
|
37
|
+
if (serverUrl) {
|
|
38
|
+
const baseUrl = normalizedServerUrl(serverUrl);
|
|
39
|
+
try {
|
|
40
|
+
return await runOpenCodeRepoMemorySession({
|
|
41
|
+
authorization: serverAuthorization(env, baseUrl, managedServerUrl),
|
|
42
|
+
baseUrl,
|
|
43
|
+
directory,
|
|
44
|
+
fetchImpl,
|
|
45
|
+
parentID,
|
|
46
|
+
prompt,
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (!isSessionCreationTransportError(error)) throw error;
|
|
50
|
+
inheritedError = error;
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
inheritedError = new Error(
|
|
54
|
+
"OpenCode repo memory runner requires --server-url or MEMORAX_CODE_OPENCODE_SERVER_URL",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const openCodeCommand = stringValue(env.MEMORAX_CODE_OPENCODE_COMMAND);
|
|
59
|
+
if (!openCodeCommand) throw inheritedError;
|
|
60
|
+
|
|
61
|
+
let ownedServer;
|
|
62
|
+
try {
|
|
63
|
+
ownedServer = await startOwnedOpenCodeServer({
|
|
64
|
+
command: openCodeCommand,
|
|
65
|
+
directory,
|
|
66
|
+
env,
|
|
67
|
+
options,
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`${errorMessage(inheritedError)}; managed OpenCode server fallback failed: ${errorMessage(error)}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return await runOpenCodeRepoMemorySession({
|
|
77
|
+
authorization: ownedServer.authorization,
|
|
78
|
+
baseUrl: ownedServer.baseUrl,
|
|
79
|
+
directory,
|
|
80
|
+
fetchImpl,
|
|
81
|
+
parentID,
|
|
82
|
+
prompt,
|
|
83
|
+
signal: ownedServer.signal,
|
|
84
|
+
});
|
|
85
|
+
} finally {
|
|
86
|
+
await ownedServer.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runOpenCodeRepoMemorySession(input) {
|
|
27
91
|
let sessionID;
|
|
28
92
|
try {
|
|
29
|
-
const session = await requestJson(fetchImpl, endpoint(baseUrl, "/session", directory), {
|
|
93
|
+
const session = await requestJson(input.fetchImpl, endpoint(input.baseUrl, "/session", input.directory), {
|
|
30
94
|
method: "POST",
|
|
31
95
|
headers: {
|
|
32
96
|
"content-type": "application/json",
|
|
33
|
-
...(authorization ? { authorization } : {}),
|
|
97
|
+
...(input.authorization ? { authorization: input.authorization } : {}),
|
|
34
98
|
},
|
|
35
99
|
body: JSON.stringify({
|
|
36
|
-
...(parentID ? { parentID } : {}),
|
|
100
|
+
...(input.parentID ? { parentID: input.parentID } : {}),
|
|
37
101
|
title: "MemoraX Code Repo Memory",
|
|
38
102
|
permission: [
|
|
39
103
|
{ permission: "edit", pattern: "*", action: "allow" },
|
|
@@ -43,23 +107,25 @@ export async function runOpenCodeRepoMemory(input, options = {}) {
|
|
|
43
107
|
{ permission: "external_directory", pattern: "*", action: "allow" },
|
|
44
108
|
],
|
|
45
109
|
}),
|
|
110
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
46
111
|
}, "session creation");
|
|
47
112
|
sessionID = stringValue(session?.id);
|
|
48
113
|
if (!sessionID) throw new Error("OpenCode session creation returned no session id");
|
|
49
114
|
|
|
50
115
|
const message = await requestJson(
|
|
51
|
-
fetchImpl,
|
|
52
|
-
endpoint(baseUrl, `/session/${encodeURIComponent(sessionID)}/message`, directory),
|
|
116
|
+
input.fetchImpl,
|
|
117
|
+
endpoint(input.baseUrl, `/session/${encodeURIComponent(sessionID)}/message`, input.directory),
|
|
53
118
|
{
|
|
54
119
|
method: "POST",
|
|
55
120
|
headers: {
|
|
56
121
|
"content-type": "application/json",
|
|
57
|
-
...(authorization ? { authorization } : {}),
|
|
122
|
+
...(input.authorization ? { authorization: input.authorization } : {}),
|
|
58
123
|
},
|
|
59
124
|
body: JSON.stringify({
|
|
60
125
|
agent: OPENCODE_REPO_MEMORY_AGENT,
|
|
61
|
-
parts: [{ type: "text", text: prompt }],
|
|
126
|
+
parts: [{ type: "text", text: input.prompt }],
|
|
62
127
|
}),
|
|
128
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
63
129
|
},
|
|
64
130
|
"blocking prompt",
|
|
65
131
|
);
|
|
@@ -69,14 +135,170 @@ export async function runOpenCodeRepoMemory(input, options = {}) {
|
|
|
69
135
|
} finally {
|
|
70
136
|
if (sessionID) {
|
|
71
137
|
await bestEffortDelete(
|
|
72
|
-
fetchImpl,
|
|
73
|
-
endpoint(baseUrl, `/session/${encodeURIComponent(sessionID)}`, directory),
|
|
74
|
-
authorization,
|
|
138
|
+
input.fetchImpl,
|
|
139
|
+
endpoint(input.baseUrl, `/session/${encodeURIComponent(sessionID)}`, input.directory),
|
|
140
|
+
input.authorization,
|
|
75
141
|
);
|
|
76
142
|
}
|
|
77
143
|
}
|
|
78
144
|
}
|
|
79
145
|
|
|
146
|
+
async function startOwnedOpenCodeServer(input) {
|
|
147
|
+
const username = OWNED_SERVER_USERNAME;
|
|
148
|
+
const password = randomBytes(24).toString("base64url");
|
|
149
|
+
const spawnImpl = input.options.spawnImpl ?? spawn;
|
|
150
|
+
const child = spawnImpl(input.command, [
|
|
151
|
+
"serve",
|
|
152
|
+
"--hostname=127.0.0.1",
|
|
153
|
+
"--port=0",
|
|
154
|
+
], {
|
|
155
|
+
cwd: input.directory,
|
|
156
|
+
env: {
|
|
157
|
+
...input.env,
|
|
158
|
+
OPENCODE_SERVER_USERNAME: username,
|
|
159
|
+
OPENCODE_SERVER_PASSWORD: password,
|
|
160
|
+
},
|
|
161
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
162
|
+
windowsHide: true,
|
|
163
|
+
});
|
|
164
|
+
const lifecycle = ownedServerLifecycle(child);
|
|
165
|
+
let closePromise;
|
|
166
|
+
const close = () => {
|
|
167
|
+
closePromise ??= stopOwnedServer(
|
|
168
|
+
child,
|
|
169
|
+
positiveInteger(input.options.serverStopTimeoutMs, DEFAULT_SERVER_STOP_TIMEOUT_MS),
|
|
170
|
+
).finally(lifecycle.dispose);
|
|
171
|
+
return closePromise;
|
|
172
|
+
};
|
|
173
|
+
try {
|
|
174
|
+
const baseUrl = await waitForOwnedServer(
|
|
175
|
+
child,
|
|
176
|
+
positiveInteger(input.options.serverStartTimeoutMs, DEFAULT_SERVER_START_TIMEOUT_MS),
|
|
177
|
+
);
|
|
178
|
+
return {
|
|
179
|
+
authorization: basicAuthorization(username, password),
|
|
180
|
+
baseUrl,
|
|
181
|
+
close,
|
|
182
|
+
signal: lifecycle.signal,
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
await close();
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function waitForOwnedServer(child, timeoutMs) {
|
|
191
|
+
return new Promise((resolveReady, rejectReady) => {
|
|
192
|
+
let output = "";
|
|
193
|
+
let settled = false;
|
|
194
|
+
let timer;
|
|
195
|
+
const finish = (error, value) => {
|
|
196
|
+
if (settled) return;
|
|
197
|
+
settled = true;
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
child.off("error", onError);
|
|
200
|
+
child.off("exit", onExit);
|
|
201
|
+
child.stdout?.off("data", onOutput);
|
|
202
|
+
child.stderr?.off("data", onOutput);
|
|
203
|
+
child.stdout?.resume();
|
|
204
|
+
child.stderr?.resume();
|
|
205
|
+
if (error) rejectReady(error);
|
|
206
|
+
else resolveReady(value);
|
|
207
|
+
};
|
|
208
|
+
const onOutput = (chunk) => {
|
|
209
|
+
output = `${output}${String(chunk)}`.slice(-MAX_SERVER_START_OUTPUT);
|
|
210
|
+
const match = output.match(/opencode server listening on (https?:\/\/[^\s\u001b]+)/i);
|
|
211
|
+
if (!match) return;
|
|
212
|
+
let url;
|
|
213
|
+
try {
|
|
214
|
+
url = normalizedOwnedServerUrl(match[1]);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
finish(error);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
finish(undefined, url);
|
|
220
|
+
};
|
|
221
|
+
const onError = (error) => finish(error);
|
|
222
|
+
const onExit = (code, signal) => finish(new Error(
|
|
223
|
+
`OpenCode managed server exited before readiness${code === null ? "" : ` with code ${code}`}`
|
|
224
|
+
+ `${signal ? ` after ${signal}` : ""}${output.trim() ? `: ${output.trim().slice(-500)}` : ""}`,
|
|
225
|
+
));
|
|
226
|
+
timer = setTimeout(() => finish(new Error(
|
|
227
|
+
`timed out waiting for OpenCode managed server readiness after ${timeoutMs} ms`,
|
|
228
|
+
)), timeoutMs);
|
|
229
|
+
child.once("error", onError);
|
|
230
|
+
child.once("exit", onExit);
|
|
231
|
+
child.stdout?.on("data", onOutput);
|
|
232
|
+
child.stderr?.on("data", onOutput);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizedOwnedServerUrl(value) {
|
|
237
|
+
const url = normalizedServerUrl(value);
|
|
238
|
+
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port) {
|
|
239
|
+
throw new Error("OpenCode managed server did not bind to the requested loopback address");
|
|
240
|
+
}
|
|
241
|
+
return url;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function ownedServerLifecycle(child) {
|
|
245
|
+
const controller = new AbortController();
|
|
246
|
+
const handlers = new Map();
|
|
247
|
+
const onExit = (code, signal) => {
|
|
248
|
+
if (controller.signal.aborted) return;
|
|
249
|
+
controller.abort(new Error(
|
|
250
|
+
`OpenCode managed server exited${code === null ? "" : ` with code ${code}`}`
|
|
251
|
+
+ `${signal ? ` after ${signal}` : ""}`,
|
|
252
|
+
));
|
|
253
|
+
};
|
|
254
|
+
child.once("exit", onExit);
|
|
255
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
256
|
+
const handler = () => {
|
|
257
|
+
controller.abort(new Error(`OpenCode managed server interrupted by ${signal}`));
|
|
258
|
+
if (!childStopped(child)) child.kill(signal);
|
|
259
|
+
};
|
|
260
|
+
handlers.set(signal, handler);
|
|
261
|
+
process.once(signal, handler);
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
signal: controller.signal,
|
|
265
|
+
dispose() {
|
|
266
|
+
child.off("exit", onExit);
|
|
267
|
+
for (const [signal, handler] of handlers) process.off(signal, handler);
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function stopOwnedServer(child, timeoutMs) {
|
|
273
|
+
if (childStopped(child)) return;
|
|
274
|
+
const exited = new Promise((resolveExit) => child.once("exit", resolveExit));
|
|
275
|
+
child.kill("SIGTERM");
|
|
276
|
+
if (await settlesWithin(exited, timeoutMs) || childStopped(child)) return;
|
|
277
|
+
child.kill("SIGKILL");
|
|
278
|
+
if (await settlesWithin(exited, timeoutMs) || childStopped(child)) return;
|
|
279
|
+
child.stdout?.destroy();
|
|
280
|
+
child.stderr?.destroy();
|
|
281
|
+
child.unref?.();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function settlesWithin(promise, timeoutMs) {
|
|
285
|
+
let timer;
|
|
286
|
+
try {
|
|
287
|
+
return await Promise.race([
|
|
288
|
+
promise.then(() => true, () => true),
|
|
289
|
+
new Promise((resolveTimeout) => {
|
|
290
|
+
timer = setTimeout(() => resolveTimeout(false), timeoutMs);
|
|
291
|
+
}),
|
|
292
|
+
]);
|
|
293
|
+
} finally {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function childStopped(child) {
|
|
299
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
300
|
+
}
|
|
301
|
+
|
|
80
302
|
function parseArgs(values) {
|
|
81
303
|
const result = {};
|
|
82
304
|
for (let index = 0; index < values.length; index += 1) {
|
|
@@ -131,7 +353,7 @@ async function requestJson(fetchImpl, url, init, operation) {
|
|
|
131
353
|
try {
|
|
132
354
|
response = await fetchImpl(url, init);
|
|
133
355
|
} catch (error) {
|
|
134
|
-
throw new
|
|
356
|
+
throw new OpenCodeTransportError(operation, error);
|
|
135
357
|
}
|
|
136
358
|
const body = await response.text();
|
|
137
359
|
if (!response.ok) {
|
|
@@ -148,6 +370,19 @@ async function requestJson(fetchImpl, url, init, operation) {
|
|
|
148
370
|
}
|
|
149
371
|
}
|
|
150
372
|
|
|
373
|
+
function isSessionCreationTransportError(error) {
|
|
374
|
+
return error instanceof OpenCodeTransportError && error.operation === "session creation";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function basicAuthorization(username, password) {
|
|
378
|
+
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function positiveInteger(value, fallback) {
|
|
382
|
+
const parsed = Number(value);
|
|
383
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
384
|
+
}
|
|
385
|
+
|
|
151
386
|
async function bestEffortDelete(fetchImpl, url, authorization) {
|
|
152
387
|
try {
|
|
153
388
|
await fetchImpl(url, {
|
package/lib/node-version.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memorax/memorax-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Persistent coding memory for Codex, Claude Code, DeepSeek Harness, and OpenCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"README.md"
|
|
38
38
|
],
|
|
39
39
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
40
|
+
"node": ">=20"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"smol-toml": "^1.7.0"
|