@kisev/memomatic 1.0.0-dev.93.g810c3f0460fa → 1.0.0
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 +2 -3
- package/README.ru.md +2 -3
- package/dist/executor.js +77 -55
- package/dist/ingest.js +0 -7
- package/dist/ingestion.js +64 -34
- package/package.json +4 -5
- package/dist/opencode-config.d.ts +0 -1
- package/dist/opencode-config.js +0 -84
package/README.md
CHANGED
|
@@ -30,9 +30,8 @@ Nightly runs are scheduled by the systemd user units in `assets/systemd/`
|
|
|
30
30
|
`memomatic-dream.service`/`.timer` for consolidation).
|
|
31
31
|
|
|
32
32
|
Configure `sessions.model` (or keep a legacy `dream.model`) and an OpenCode
|
|
33
|
-
|
|
34
|
-
watermark is preserved.
|
|
35
|
-
OpenCode `2.x`; a pre-V2 session database is not parsed. Follow the
|
|
33
|
+
provider before expecting session extraction; without a model, the session
|
|
34
|
+
watermark is preserved. Follow the
|
|
36
35
|
[setup, first-entry, and scheduling guide](../../docs/how-to/memomatic.md).
|
|
37
36
|
|
|
38
37
|
## CLI, Sessions, and Dream
|
package/README.ru.md
CHANGED
|
@@ -30,9 +30,8 @@ memomatic dream # консолидация: inbox + продвижение
|
|
|
30
30
|
`memomatic-dream.service`/`.timer` для консолидации).
|
|
31
31
|
|
|
32
32
|
Перед извлечением сессий настройте `sessions.model` (или оставьте старый
|
|
33
|
-
`dream.model`) и провайдера OpenCode
|
|
34
|
-
сохраняется.
|
|
35
|
-
база сессий до V2 не разбирается. См.
|
|
33
|
+
`dream.model`) и провайдера OpenCode; без модели отметка обработанных сессий
|
|
34
|
+
сохраняется. См.
|
|
36
35
|
[настройку, первую запись и расписание](../../docs/ru/how-to/memomatic.md).
|
|
37
36
|
|
|
38
37
|
## CLI, Sessions и Dream
|
package/dist/executor.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdtemp, rm
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
|
-
import { isolatedProviderConfig } from "./opencode-config.js";
|
|
7
6
|
export function extractJson(response) {
|
|
8
7
|
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(response);
|
|
9
8
|
for (const candidate of [fenced?.[1], response]) {
|
|
@@ -50,34 +49,30 @@ export class OpenCodeExecutor {
|
|
|
50
49
|
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
|
|
51
50
|
throw new Error("invalid OpenCode URL");
|
|
52
51
|
this.url = url.href.replace(/\/$/, "");
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
|
|
52
|
+
if (process.env.OPENCODE_SERVER_PASSWORD)
|
|
53
|
+
this.authorization = `Basic ${Buffer.from(`${process.env.OPENCODE_SERVER_USERNAME ?? "opencode"}:${process.env.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`;
|
|
56
54
|
return;
|
|
57
55
|
}
|
|
58
|
-
|
|
56
|
+
let inherited = {};
|
|
57
|
+
if (process.env.OPENCODE_CONFIG_CONTENT) {
|
|
58
|
+
try {
|
|
59
|
+
const value = JSON.parse(process.env.OPENCODE_CONFIG_CONTENT);
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
61
|
+
throw new Error();
|
|
62
|
+
inherited = value;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new Error("OpenCode inline configuration must be a JSON object");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
59
68
|
this.directory = await mkdtemp(join(tmpdir(), "memomatic-opencode-"));
|
|
60
|
-
const configFile = join(this.directory, "opencode.json");
|
|
61
|
-
await writeFile(configFile, JSON.stringify({
|
|
62
|
-
...inherited,
|
|
63
|
-
plugins: ["-opencode.skill", "-opencode.config.skill", "-opencode.tool.skill"],
|
|
64
|
-
permissions: [{ action: "*", resource: "*", effect: "deny" }],
|
|
65
|
-
agents: {
|
|
66
|
-
memomatic: {
|
|
67
|
-
mode: "primary",
|
|
68
|
-
description: "Bounded memory extraction",
|
|
69
|
-
system: "Return only the requested JSON. Do not use tools.",
|
|
70
|
-
permissions: [{ action: "*", resource: "*", effect: "deny" }],
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
}), { mode: 0o600 });
|
|
74
69
|
const password = randomBytes(24).toString("hex");
|
|
75
70
|
this.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
|
|
76
71
|
this.options.observe?.({
|
|
77
72
|
phase: "opencode.start",
|
|
78
73
|
message: "Starting one isolated OpenCode server for this run",
|
|
79
74
|
});
|
|
80
|
-
const child = spawn(this.options.binary ?? "opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
|
|
75
|
+
const child = spawn(this.options.binary ?? "opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0", "--pure"], {
|
|
81
76
|
cwd: this.directory,
|
|
82
77
|
detached: process.platform !== "win32",
|
|
83
78
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -85,11 +80,25 @@ export class OpenCodeExecutor {
|
|
|
85
80
|
...process.env,
|
|
86
81
|
OPENCODE_SERVER_USERNAME: "opencode",
|
|
87
82
|
OPENCODE_SERVER_PASSWORD: password,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: "1",
|
|
84
|
+
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
|
|
85
|
+
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: "1",
|
|
86
|
+
OPENCODE_DISABLE_DEFAULT_PLUGINS: "1",
|
|
87
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
88
|
+
...inherited,
|
|
89
|
+
permission: "deny",
|
|
90
|
+
tools: { "*": false },
|
|
91
|
+
agent: {
|
|
92
|
+
...inherited.agent,
|
|
93
|
+
memomatic: {
|
|
94
|
+
mode: "primary",
|
|
95
|
+
description: "Bounded memory extraction",
|
|
96
|
+
prompt: "Return only the requested JSON. Do not use tools.",
|
|
97
|
+
permission: "deny",
|
|
98
|
+
tools: { "*": false },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
93
102
|
},
|
|
94
103
|
});
|
|
95
104
|
this.child = child;
|
|
@@ -132,9 +141,9 @@ export class OpenCodeExecutor {
|
|
|
132
141
|
child.stderr?.resume();
|
|
133
142
|
this.options.observe?.({ phase: "opencode.ready", message: "OpenCode server ready" });
|
|
134
143
|
}
|
|
135
|
-
async request(path, body, signal
|
|
144
|
+
async request(path, body, signal) {
|
|
136
145
|
const response = await fetch(`${this.url}${path}`, {
|
|
137
|
-
method,
|
|
146
|
+
method: "POST",
|
|
138
147
|
headers: {
|
|
139
148
|
"content-type": "application/json",
|
|
140
149
|
...(this.authorization ? { authorization: this.authorization } : {}),
|
|
@@ -143,8 +152,8 @@ export class OpenCodeExecutor {
|
|
|
143
152
|
signal,
|
|
144
153
|
});
|
|
145
154
|
if (!response.ok)
|
|
146
|
-
throw new Error(`OpenCode ${path.includes("
|
|
147
|
-
return
|
|
155
|
+
throw new Error(`OpenCode ${path.includes("message") ? "model request" : "session request"} failed: HTTP ${response.status}`);
|
|
156
|
+
return (await response.json());
|
|
148
157
|
}
|
|
149
158
|
async complete(request) {
|
|
150
159
|
this.options.signal?.throwIfAborted();
|
|
@@ -165,43 +174,56 @@ export class OpenCodeExecutor {
|
|
|
165
174
|
}), 15000);
|
|
166
175
|
heartbeat.unref();
|
|
167
176
|
try {
|
|
177
|
+
const session = await this.request("/session", {
|
|
178
|
+
title: "[memomatic-internal] memory extraction",
|
|
179
|
+
permission: [{ permission: "*", pattern: "*", action: "deny" }],
|
|
180
|
+
}, signal);
|
|
181
|
+
if (typeof session.id !== "string")
|
|
182
|
+
throw new Error("OpenCode session response is malformed");
|
|
183
|
+
id = session.id;
|
|
168
184
|
const slash = this.options.model?.indexOf("/") ?? -1;
|
|
169
185
|
if (this.options.model && slash < 1)
|
|
170
186
|
throw new Error("model must use provider/model format");
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
this.usage.calls++;
|
|
188
|
+
this.usage.charactersSent += request.system.length + request.prompt.length;
|
|
189
|
+
this.options.observe?.({
|
|
190
|
+
phase: "model.request",
|
|
191
|
+
message: "Sending prepared fragment to OpenCode",
|
|
192
|
+
characters: request.prompt.length,
|
|
193
|
+
attempt: attempt + 1,
|
|
194
|
+
});
|
|
195
|
+
const response = await this.request(`/session/${encodeURIComponent(id)}/message`, {
|
|
173
196
|
agent: this.options.url ? "build" : "memomatic",
|
|
174
|
-
|
|
175
|
-
|
|
197
|
+
system: request.system,
|
|
198
|
+
tools: { "*": false },
|
|
176
199
|
...(this.options.model
|
|
177
200
|
? {
|
|
178
201
|
model: {
|
|
179
202
|
providerID: this.options.model.slice(0, slash),
|
|
180
|
-
|
|
181
|
-
...(this.options.variant ? { variant: this.options.variant } : {}),
|
|
203
|
+
modelID: this.options.model.slice(slash + 1),
|
|
182
204
|
},
|
|
183
205
|
}
|
|
184
206
|
: {}),
|
|
207
|
+
...(this.options.variant ? { variant: this.options.variant } : {}),
|
|
208
|
+
parts: [{ type: "text", text: `[memomatic-internal] ${request.prompt}` }],
|
|
185
209
|
}, signal);
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
throw new Error("OpenCode
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
});
|
|
199
|
-
const response = await this.request(`/api/session/${encodeURIComponent(id)}/generate`, { prompt: `[memomatic-internal] ${request.prompt}` }, signal);
|
|
200
|
-
const generated = response.data;
|
|
201
|
-
if (typeof generated?.text !== "string")
|
|
210
|
+
const info = response.info;
|
|
211
|
+
if (info?.error)
|
|
212
|
+
throw new Error("OpenCode model returned an error; inspect the configured provider");
|
|
213
|
+
if (info?.tokens) {
|
|
214
|
+
this.usage.usageAvailable = true;
|
|
215
|
+
this.usage.inputTokens += info.tokens.input ?? 0;
|
|
216
|
+
this.usage.outputTokens += info.tokens.output ?? 0;
|
|
217
|
+
this.usage.cacheReadTokens += info.tokens.cache?.read ?? 0;
|
|
218
|
+
this.usage.cacheWriteTokens += info.tokens.cache?.write ?? 0;
|
|
219
|
+
}
|
|
220
|
+
const parts = response.parts;
|
|
221
|
+
if (!Array.isArray(parts))
|
|
202
222
|
throw new Error("OpenCode model response is malformed");
|
|
203
|
-
|
|
204
|
-
|
|
223
|
+
const parsed = extractJson(parts
|
|
224
|
+
.filter((part) => part.type === "text")
|
|
225
|
+
.map((part) => part.text ?? "")
|
|
226
|
+
.join("\n"));
|
|
205
227
|
this.options.observe?.({
|
|
206
228
|
phase: "model.done",
|
|
207
229
|
message: "Model response received",
|
|
@@ -212,7 +234,7 @@ export class OpenCodeExecutor {
|
|
|
212
234
|
}
|
|
213
235
|
catch (error) {
|
|
214
236
|
if (id)
|
|
215
|
-
await this.request(`/
|
|
237
|
+
await this.request(`/session/${encodeURIComponent(id)}/abort`, {}, AbortSignal.timeout(3000)).catch(() => undefined);
|
|
216
238
|
if (this.options.signal?.aborted)
|
|
217
239
|
throw this.options.signal.reason;
|
|
218
240
|
if (attempt === (this.options.retries ?? 1))
|
package/dist/ingest.js
CHANGED
|
@@ -4,12 +4,5 @@ export function opencodeDatabasePath() {
|
|
|
4
4
|
const base = process.env.XDG_DATA_HOME ?? join(homedir(), ".local/share");
|
|
5
5
|
if (!isAbsolute(base))
|
|
6
6
|
throw new Error("XDG_DATA_HOME must be an absolute path");
|
|
7
|
-
if (process.env.OPENCODE_DB) {
|
|
8
|
-
if (process.env.OPENCODE_DB === ":memory:")
|
|
9
|
-
throw new Error("An in-memory OpenCode database cannot be ingested from another process");
|
|
10
|
-
return isAbsolute(process.env.OPENCODE_DB)
|
|
11
|
-
? process.env.OPENCODE_DB
|
|
12
|
-
: join(base, "opencode", process.env.OPENCODE_DB);
|
|
13
|
-
}
|
|
14
7
|
return join(base, "opencode", "opencode.db");
|
|
15
8
|
}
|
package/dist/ingestion.js
CHANGED
|
@@ -21,20 +21,24 @@ export function planIngestion(file, store, options) {
|
|
|
21
21
|
const db = new DatabaseSync(file, { readOnly: true });
|
|
22
22
|
try {
|
|
23
23
|
db.exec("BEGIN");
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const hasParts = Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE name='part' AND type='table'").get());
|
|
25
|
+
const hasUpdated = db
|
|
26
|
+
.prepare("PRAGMA table_info(session)")
|
|
27
|
+
.all()
|
|
28
|
+
.some((row) => row.name === "time_updated");
|
|
29
|
+
const fastRevision = hasParts &&
|
|
30
|
+
hasUpdated &&
|
|
31
|
+
["message", "part"].every((table) => db
|
|
30
32
|
.prepare(`PRAGMA table_info(${table})`)
|
|
31
33
|
.all()
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
.some((row) => row.name === "time_updated"));
|
|
35
|
+
const partHasSession = hasParts &&
|
|
36
|
+
db
|
|
37
|
+
.prepare("PRAGMA table_info(part)")
|
|
38
|
+
.all()
|
|
39
|
+
.some((row) => row.name === "session_id");
|
|
36
40
|
const sessions = db
|
|
37
|
-
.prepare(
|
|
41
|
+
.prepare(`SELECT id,title,directory,time_created,${hasUpdated ? "time_updated" : "time_created"} AS revision FROM session WHERE ${hasUpdated ? "time_updated" : "time_created"} < ? ORDER BY time_created,id`)
|
|
38
42
|
.all(options.before);
|
|
39
43
|
for (const [position, session] of sessions.entries()) {
|
|
40
44
|
if (session.title.startsWith("[memomatic-internal]")) {
|
|
@@ -43,40 +47,66 @@ export function planIngestion(file, store, options) {
|
|
|
43
47
|
}
|
|
44
48
|
const sealKey = `ingest-session-v2:${digest(session.id)}`;
|
|
45
49
|
let seal;
|
|
46
|
-
{
|
|
47
|
-
const
|
|
48
|
-
.prepare("SELECT
|
|
49
|
-
.
|
|
50
|
-
|
|
50
|
+
if (fastRevision) {
|
|
51
|
+
const messages = db
|
|
52
|
+
.prepare("SELECT COUNT(*) AS count,MAX(time_updated) AS updated FROM message WHERE session_id=?")
|
|
53
|
+
.get(session.id);
|
|
54
|
+
const parts = db
|
|
55
|
+
.prepare(`SELECT COUNT(*) AS count,MAX(time_updated) AS updated FROM part WHERE ${partHasSession ? "session_id=?" : "message_id IN (SELECT id FROM message WHERE session_id=?)"}`)
|
|
56
|
+
.get(session.id);
|
|
57
|
+
if (Math.max(Number(messages.updated), Number(parts.updated)) >= options.before) {
|
|
51
58
|
result.skipped++;
|
|
52
59
|
continue;
|
|
53
60
|
}
|
|
54
|
-
const revision = digest(JSON.stringify([session.revision, session.title, session.directory,
|
|
61
|
+
const revision = digest(JSON.stringify([session.revision, session.title, session.directory, messages, parts]));
|
|
55
62
|
if (store.getMeta(sealKey) === revision) {
|
|
56
63
|
result.cachedSessions++;
|
|
57
64
|
continue;
|
|
58
65
|
}
|
|
59
66
|
seal = [sealKey, revision];
|
|
60
67
|
}
|
|
61
|
-
const rows =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
const rows = hasParts
|
|
69
|
+
? db
|
|
70
|
+
.prepare(`SELECT m.id,m.data AS message,p.data AS part FROM message m JOIN part p ON p.message_id=m.id
|
|
71
|
+
WHERE ${partHasSession ? "p" : "m"}.session_id=?
|
|
72
|
+
AND CASE WHEN json_valid(p.data) THEN json_extract(p.data,'$.type') END='text'
|
|
73
|
+
AND CASE WHEN json_valid(m.data) THEN json_extract(m.data,'$.role') END IN ('user','assistant')
|
|
74
|
+
ORDER BY m.time_created,m.id,p.time_created,p.id`)
|
|
75
|
+
.all(session.id)
|
|
76
|
+
: db
|
|
77
|
+
.prepare("SELECT rowid AS id,data FROM message WHERE session_id=? ORDER BY time_created,rowid")
|
|
78
|
+
.all(session.id).flatMap((row) => {
|
|
79
|
+
try {
|
|
80
|
+
const message = JSON.parse(row.data);
|
|
81
|
+
return (message.parts ?? []).map((part) => ({
|
|
82
|
+
id: String(row.id),
|
|
83
|
+
message: row.data,
|
|
84
|
+
part: JSON.stringify(part),
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
});
|
|
75
91
|
const messages = new Map();
|
|
76
92
|
for (const row of rows) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
93
|
+
try {
|
|
94
|
+
const message = JSON.parse(row.message);
|
|
95
|
+
const part = JSON.parse(row.part);
|
|
96
|
+
if (!["user", "assistant"].includes(message.role) ||
|
|
97
|
+
part.type !== "text" ||
|
|
98
|
+
typeof part.text !== "string")
|
|
99
|
+
continue;
|
|
100
|
+
const item = messages.get(row.id) ?? {
|
|
101
|
+
role: message.role,
|
|
102
|
+
texts: [],
|
|
103
|
+
};
|
|
104
|
+
item.texts.push(part.text);
|
|
105
|
+
messages.set(row.id, item);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
result.skipped++;
|
|
109
|
+
}
|
|
80
110
|
}
|
|
81
111
|
const firstUser = [...messages.values()].find((message) => message.role === "user");
|
|
82
112
|
if (firstUser?.texts.join("\n").trimStart().startsWith("[memomatic-internal]")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kisev/memomatic",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Personal learning memory for agents: tiered Markdown corpus, rebuildable index, dream sweep.",
|
|
5
5
|
"homepage": "https://github.com/kisev/skills#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -30,13 +30,12 @@
|
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "node scripts/clean-dist.mjs && tsc --project tsconfig.json",
|
|
33
|
-
"test": "node --test test
|
|
33
|
+
"test": "node --test test/",
|
|
34
34
|
"pack:check": "node test/pack-allowlist.mjs"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@kisev/safe-fs": "1.0.0
|
|
38
|
-
"commander": "15.0.0"
|
|
39
|
-
"jsonc-parser": "3.3.1"
|
|
37
|
+
"@kisev/safe-fs": "^1.0.0",
|
|
38
|
+
"commander": "15.0.0"
|
|
40
39
|
},
|
|
41
40
|
"devDependencies": {
|
|
42
41
|
"@types/node": "22.15.30",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function isolatedProviderConfig(): Promise<Record<string, unknown>>;
|
package/dist/opencode-config.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import { parse } from "jsonc-parser";
|
|
5
|
-
const PROVIDER_FIELDS = [
|
|
6
|
-
"providers",
|
|
7
|
-
"provider",
|
|
8
|
-
"model",
|
|
9
|
-
"enabled_providers",
|
|
10
|
-
"disabled_providers",
|
|
11
|
-
"experimental",
|
|
12
|
-
"enterprise",
|
|
13
|
-
];
|
|
14
|
-
function record(value) {
|
|
15
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
16
|
-
}
|
|
17
|
-
function merge(left, right, key = "") {
|
|
18
|
-
if (key === "policies" && Array.isArray(left) && Array.isArray(right))
|
|
19
|
-
return [...left, ...right];
|
|
20
|
-
if (!record(left) || !record(right))
|
|
21
|
-
return right;
|
|
22
|
-
return Object.fromEntries([...new Set([...Object.keys(left), ...Object.keys(right)])].map((key) => [
|
|
23
|
-
key,
|
|
24
|
-
key in right ? merge(left[key], right[key], key) : left[key],
|
|
25
|
-
]));
|
|
26
|
-
}
|
|
27
|
-
function anchorFiles(value, directory) {
|
|
28
|
-
if (typeof value === "string")
|
|
29
|
-
return value.replace(/\{file:([^}]+)\}/g, (original, path) => isAbsolute(path) || path.startsWith("~/") ? original : `{file:${resolve(directory, path)}}`);
|
|
30
|
-
if (Array.isArray(value))
|
|
31
|
-
return value.map((entry) => anchorFiles(entry, directory));
|
|
32
|
-
if (record(value))
|
|
33
|
-
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, anchorFiles(entry, directory)]));
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
export async function isolatedProviderConfig() {
|
|
37
|
-
const root = process.env.OPENCODE_CONFIG_DIR ??
|
|
38
|
-
join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "opencode");
|
|
39
|
-
const paths = [
|
|
40
|
-
join(root, "opencode.json"),
|
|
41
|
-
join(root, "opencode.jsonc"),
|
|
42
|
-
...(process.env.OPENCODE_CONFIG ? [resolve(process.env.OPENCODE_CONFIG)] : []),
|
|
43
|
-
];
|
|
44
|
-
let result = {};
|
|
45
|
-
const include = (value, directory) => {
|
|
46
|
-
if (!record(value))
|
|
47
|
-
throw new Error("OpenCode configuration must be a JSON object");
|
|
48
|
-
const selected = Object.fromEntries(PROVIDER_FIELDS.filter((key) => key in value).map((key) => [
|
|
49
|
-
key,
|
|
50
|
-
anchorFiles(value[key], directory),
|
|
51
|
-
]));
|
|
52
|
-
if (record(selected.experimental))
|
|
53
|
-
selected.experimental = { policies: selected.experimental.policies ?? [] };
|
|
54
|
-
result = merge(result, selected);
|
|
55
|
-
};
|
|
56
|
-
for (const path of [...new Set(paths)]) {
|
|
57
|
-
let text;
|
|
58
|
-
try {
|
|
59
|
-
text = await readFile(path, "utf8");
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
if (error.code === "ENOENT" &&
|
|
63
|
-
path !== (process.env.OPENCODE_CONFIG ? resolve(process.env.OPENCODE_CONFIG) : undefined))
|
|
64
|
-
continue;
|
|
65
|
-
throw new Error("Cannot read OpenCode provider configuration");
|
|
66
|
-
}
|
|
67
|
-
const errors = [];
|
|
68
|
-
const value = parse(text, errors, { allowTrailingComma: true });
|
|
69
|
-
if (errors.length)
|
|
70
|
-
throw new Error("OpenCode provider configuration is not valid JSON/JSONC");
|
|
71
|
-
include(value, dirname(path));
|
|
72
|
-
}
|
|
73
|
-
if (process.env.OPENCODE_CONFIG_CONTENT) {
|
|
74
|
-
let value;
|
|
75
|
-
try {
|
|
76
|
-
value = JSON.parse(process.env.OPENCODE_CONFIG_CONTENT);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
throw new Error("OpenCode inline configuration must be a JSON object");
|
|
80
|
-
}
|
|
81
|
-
include(value, process.cwd());
|
|
82
|
-
}
|
|
83
|
-
return result;
|
|
84
|
-
}
|