@kisev/memomatic 1.0.0-dev.86.g5908817e39ee → 1.0.0-dev.87.g5a92e74c6754
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 +3 -2
- package/README.ru.md +3 -2
- package/dist/executor.js +55 -77
- package/dist/ingest.js +7 -0
- package/dist/ingestion.js +34 -64
- package/dist/opencode-config.d.ts +1 -0
- package/dist/opencode-config.js +84 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -30,8 +30,9 @@ 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
|
-
provider before expecting session extraction; without a model, the session
|
|
34
|
-
watermark is preserved.
|
|
33
|
+
V2 provider before expecting session extraction; without a model, the session
|
|
34
|
+
watermark is preserved. Session extraction and its model server require
|
|
35
|
+
OpenCode `2.x`; a pre-V2 session database is not parsed. Follow the
|
|
35
36
|
[setup, first-entry, and scheduling guide](../../docs/how-to/memomatic.md).
|
|
36
37
|
|
|
37
38
|
## CLI, Sessions, and Dream
|
package/README.ru.md
CHANGED
|
@@ -30,8 +30,9 @@ memomatic dream # консолидация: inbox + продвижение
|
|
|
30
30
|
`memomatic-dream.service`/`.timer` для консолидации).
|
|
31
31
|
|
|
32
32
|
Перед извлечением сессий настройте `sessions.model` (или оставьте старый
|
|
33
|
-
`dream.model`) и провайдера OpenCode; без модели отметка обработанных сессий
|
|
34
|
-
сохраняется.
|
|
33
|
+
`dream.model`) и провайдера OpenCode V2; без модели отметка обработанных сессий
|
|
34
|
+
сохраняется. Извлечение сессий и его сервер модели требуют OpenCode `2.x`;
|
|
35
|
+
база сессий до V2 не разбирается. См.
|
|
35
36
|
[настройку, первую запись и расписание](../../docs/ru/how-to/memomatic.md).
|
|
36
37
|
|
|
37
38
|
## CLI, Sessions и Dream
|
package/dist/executor.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, rm, writeFile } 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";
|
|
6
7
|
export function extractJson(response) {
|
|
7
8
|
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(response);
|
|
8
9
|
for (const candidate of [fenced?.[1], response]) {
|
|
@@ -49,30 +50,34 @@ export class OpenCodeExecutor {
|
|
|
49
50
|
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
|
|
50
51
|
throw new Error("invalid OpenCode URL");
|
|
51
52
|
this.url = url.href.replace(/\/$/, "");
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const password = process.env.OPENCODE_PASSWORD ?? process.env.OPENCODE_SERVER_PASSWORD;
|
|
54
|
+
if (password)
|
|
55
|
+
this.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
|
|
54
56
|
return;
|
|
55
57
|
}
|
|
56
|
-
|
|
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
|
-
}
|
|
58
|
+
const inherited = await isolatedProviderConfig();
|
|
68
59
|
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 });
|
|
69
74
|
const password = randomBytes(24).toString("hex");
|
|
70
75
|
this.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
|
|
71
76
|
this.options.observe?.({
|
|
72
77
|
phase: "opencode.start",
|
|
73
78
|
message: "Starting one isolated OpenCode server for this run",
|
|
74
79
|
});
|
|
75
|
-
const child = spawn(this.options.binary ?? "opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"
|
|
80
|
+
const child = spawn(this.options.binary ?? "opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
|
|
76
81
|
cwd: this.directory,
|
|
77
82
|
detached: process.platform !== "win32",
|
|
78
83
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -80,25 +85,11 @@ export class OpenCodeExecutor {
|
|
|
80
85
|
...process.env,
|
|
81
86
|
OPENCODE_SERVER_USERNAME: "opencode",
|
|
82
87
|
OPENCODE_SERVER_PASSWORD: password,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
}),
|
|
88
|
+
OPENCODE_CONFIG_PROJECT_DISABLE: "1",
|
|
89
|
+
OPENCODE_CONFIG_DIR: this.directory,
|
|
90
|
+
OPENCODE_CONFIG: configFile,
|
|
91
|
+
OPENCODE_CONFIG_CONTENT: "{}",
|
|
92
|
+
OPENCODE_PASSWORD: password,
|
|
102
93
|
},
|
|
103
94
|
});
|
|
104
95
|
this.child = child;
|
|
@@ -141,9 +132,9 @@ export class OpenCodeExecutor {
|
|
|
141
132
|
child.stderr?.resume();
|
|
142
133
|
this.options.observe?.({ phase: "opencode.ready", message: "OpenCode server ready" });
|
|
143
134
|
}
|
|
144
|
-
async request(path, body, signal) {
|
|
135
|
+
async request(path, body, signal, method = "POST") {
|
|
145
136
|
const response = await fetch(`${this.url}${path}`, {
|
|
146
|
-
method
|
|
137
|
+
method,
|
|
147
138
|
headers: {
|
|
148
139
|
"content-type": "application/json",
|
|
149
140
|
...(this.authorization ? { authorization: this.authorization } : {}),
|
|
@@ -152,8 +143,8 @@ export class OpenCodeExecutor {
|
|
|
152
143
|
signal,
|
|
153
144
|
});
|
|
154
145
|
if (!response.ok)
|
|
155
|
-
throw new Error(`OpenCode ${path.includes("
|
|
156
|
-
return (await response.json());
|
|
146
|
+
throw new Error(`OpenCode ${path.includes("generate") ? "model request" : "session request"} failed: HTTP ${response.status}`);
|
|
147
|
+
return response.status === 204 ? {} : (await response.json());
|
|
157
148
|
}
|
|
158
149
|
async complete(request) {
|
|
159
150
|
this.options.signal?.throwIfAborted();
|
|
@@ -174,56 +165,43 @@ export class OpenCodeExecutor {
|
|
|
174
165
|
}), 15000);
|
|
175
166
|
heartbeat.unref();
|
|
176
167
|
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;
|
|
184
168
|
const slash = this.options.model?.indexOf("/") ?? -1;
|
|
185
169
|
if (this.options.model && slash < 1)
|
|
186
170
|
throw new Error("model must use provider/model format");
|
|
187
|
-
this.
|
|
188
|
-
|
|
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`, {
|
|
171
|
+
const session = await this.request("/api/session", {
|
|
172
|
+
title: "[memomatic-internal] memory extraction",
|
|
196
173
|
agent: this.options.url ? "build" : "memomatic",
|
|
197
|
-
|
|
198
|
-
|
|
174
|
+
...(this.directory ? { location: { directory: this.directory } } : {}),
|
|
175
|
+
permissions: [{ action: "*", resource: "*", effect: "deny" }],
|
|
199
176
|
...(this.options.model
|
|
200
177
|
? {
|
|
201
178
|
model: {
|
|
202
179
|
providerID: this.options.model.slice(0, slash),
|
|
203
|
-
|
|
180
|
+
id: this.options.model.slice(slash + 1),
|
|
181
|
+
...(this.options.variant ? { variant: this.options.variant } : {}),
|
|
204
182
|
},
|
|
205
183
|
}
|
|
206
184
|
: {}),
|
|
207
|
-
...(this.options.variant ? { variant: this.options.variant } : {}),
|
|
208
|
-
parts: [{ type: "text", text: `[memomatic-internal] ${request.prompt}` }],
|
|
209
185
|
}, signal);
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
throw new Error("OpenCode
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
186
|
+
const data = session.data;
|
|
187
|
+
if (typeof data?.id !== "string")
|
|
188
|
+
throw new Error("OpenCode session response is malformed");
|
|
189
|
+
id = data.id;
|
|
190
|
+
await this.request(`/api/experimental/session/${encodeURIComponent(id)}/instructions/entries/memomatic`, { value: request.system }, signal, "PUT");
|
|
191
|
+
this.usage.calls++;
|
|
192
|
+
this.usage.charactersSent += request.system.length + request.prompt.length;
|
|
193
|
+
this.options.observe?.({
|
|
194
|
+
phase: "model.request",
|
|
195
|
+
message: "Sending prepared fragment to OpenCode",
|
|
196
|
+
characters: request.prompt.length,
|
|
197
|
+
attempt: attempt + 1,
|
|
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")
|
|
222
202
|
throw new Error("OpenCode model response is malformed");
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
.map((part) => part.text ?? "")
|
|
226
|
-
.join("\n"));
|
|
203
|
+
// V2 transient generation returns text only, not exact token usage.
|
|
204
|
+
const parsed = extractJson(generated.text);
|
|
227
205
|
this.options.observe?.({
|
|
228
206
|
phase: "model.done",
|
|
229
207
|
message: "Model response received",
|
|
@@ -234,7 +212,7 @@ export class OpenCodeExecutor {
|
|
|
234
212
|
}
|
|
235
213
|
catch (error) {
|
|
236
214
|
if (id)
|
|
237
|
-
await this.request(`/session/${encodeURIComponent(id)}/
|
|
215
|
+
await this.request(`/api/session/${encodeURIComponent(id)}/interrupt?resume=false`, {}, AbortSignal.timeout(3000)).catch(() => undefined);
|
|
238
216
|
if (this.options.signal?.aborted)
|
|
239
217
|
throw this.options.signal.reason;
|
|
240
218
|
if (attempt === (this.options.retries ?? 1))
|
package/dist/ingest.js
CHANGED
|
@@ -4,5 +4,12 @@ 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
|
+
}
|
|
7
14
|
return join(base, "opencode", "opencode.db");
|
|
8
15
|
}
|
package/dist/ingestion.js
CHANGED
|
@@ -21,24 +21,20 @@ 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
|
-
|
|
30
|
-
hasUpdated &&
|
|
31
|
-
["message", "part"].every((table) => db
|
|
24
|
+
const required = {
|
|
25
|
+
session_v2: ["id", "title", "directory", "time_created", "time_updated"],
|
|
26
|
+
session_message: ["id", "session_id", "type", "seq", "data", "time_created", "time_updated"],
|
|
27
|
+
};
|
|
28
|
+
for (const [table, fields] of Object.entries(required)) {
|
|
29
|
+
const columns = new Set(db
|
|
32
30
|
.prepare(`PRAGMA table_info(${table})`)
|
|
33
31
|
.all()
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.all()
|
|
39
|
-
.some((row) => row.name === "session_id");
|
|
32
|
+
.map((row) => row.name));
|
|
33
|
+
if (!fields.every((field) => columns.has(field)))
|
|
34
|
+
throw new Error("OpenCode V2 session projections are unavailable; start V2 to migrate history before ingestion");
|
|
35
|
+
}
|
|
40
36
|
const sessions = db
|
|
41
|
-
.prepare(
|
|
37
|
+
.prepare("SELECT id,COALESCE(title,'') AS title,directory,time_created,time_updated AS revision FROM session_v2 WHERE time_updated < ? ORDER BY time_created,id")
|
|
42
38
|
.all(options.before);
|
|
43
39
|
for (const [position, session] of sessions.entries()) {
|
|
44
40
|
if (session.title.startsWith("[memomatic-internal]")) {
|
|
@@ -47,66 +43,40 @@ export function planIngestion(file, store, options) {
|
|
|
47
43
|
}
|
|
48
44
|
const sealKey = `ingest-session-v2:${digest(session.id)}`;
|
|
49
45
|
let seal;
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
.prepare("SELECT
|
|
53
|
-
.
|
|
54
|
-
|
|
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) {
|
|
46
|
+
{
|
|
47
|
+
const revisions = db
|
|
48
|
+
.prepare("SELECT id,seq,time_updated FROM session_message WHERE session_id=? ORDER BY seq,id")
|
|
49
|
+
.all(session.id);
|
|
50
|
+
if (revisions.some((message) => Number(message.time_updated) >= options.before)) {
|
|
58
51
|
result.skipped++;
|
|
59
52
|
continue;
|
|
60
53
|
}
|
|
61
|
-
const revision = digest(JSON.stringify([session.revision, session.title, session.directory,
|
|
54
|
+
const revision = digest(JSON.stringify([session.revision, session.title, session.directory, revisions]));
|
|
62
55
|
if (store.getMeta(sealKey) === revision) {
|
|
63
56
|
result.cachedSessions++;
|
|
64
57
|
continue;
|
|
65
58
|
}
|
|
66
59
|
seal = [sealKey, revision];
|
|
67
60
|
}
|
|
68
|
-
const rows =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
id: String(row.id),
|
|
83
|
-
message: row.data,
|
|
84
|
-
part: JSON.stringify(part),
|
|
85
|
-
}));
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
});
|
|
61
|
+
const rows = db
|
|
62
|
+
.prepare(`
|
|
63
|
+
SELECT id,type AS role,json_extract(data,'$.text') AS text,seq,0 AS part_index
|
|
64
|
+
FROM session_message WHERE session_id=? AND type='user'
|
|
65
|
+
AND CASE WHEN json_valid(data) THEN json_type(data,'$.text') END='text'
|
|
66
|
+
UNION ALL
|
|
67
|
+
SELECT m.id,m.type AS role,json_extract(p.value,'$.text') AS text,m.seq,CAST(p.key AS INTEGER) AS part_index
|
|
68
|
+
FROM session_message m JOIN json_each(CASE WHEN json_valid(m.data) THEN json_extract(m.data,'$.content') ELSE '[]' END) p
|
|
69
|
+
WHERE m.session_id=? AND m.type='assistant'
|
|
70
|
+
AND CASE WHEN json_valid(p.value) THEN json_extract(p.value,'$.type') END='text'
|
|
71
|
+
AND CASE WHEN json_valid(p.value) THEN json_type(p.value,'$.text') END='text'
|
|
72
|
+
ORDER BY seq,part_index
|
|
73
|
+
`)
|
|
74
|
+
.all(session.id, session.id);
|
|
91
75
|
const messages = new Map();
|
|
92
76
|
for (const row of rows) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
}
|
|
77
|
+
const item = messages.get(row.id) ?? { role: row.role, texts: [] };
|
|
78
|
+
item.texts.push(row.text);
|
|
79
|
+
messages.set(row.id, item);
|
|
110
80
|
}
|
|
111
81
|
const firstUser = [...messages.values()].find((message) => message.role === "user");
|
|
112
82
|
if (firstUser?.texts.join("\n").trimStart().startsWith("[memomatic-internal]")) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isolatedProviderConfig(): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kisev/memomatic",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.87.g5a92e74c6754",
|
|
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,12 +30,13 @@
|
|
|
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/*.test.mjs",
|
|
34
34
|
"pack:check": "node test/pack-allowlist.mjs"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@kisev/safe-fs": "1.0.0-dev.
|
|
38
|
-
"commander": "15.0.0"
|
|
37
|
+
"@kisev/safe-fs": "1.0.0-dev.87.g5a92e74c6754",
|
|
38
|
+
"commander": "15.0.0",
|
|
39
|
+
"jsonc-parser": "3.3.1"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/node": "22.15.30",
|