opencode-gateway 0.2.0 → 0.2.2
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 +6 -4
- package/dist/cli/doctor.js +7 -19
- package/dist/cli/init.js +5 -3
- package/dist/cli/opencode-config-file.d.ts +5 -0
- package/dist/cli/opencode-config-file.js +18 -0
- package/dist/cli/opencode-config.d.ts +2 -0
- package/dist/cli/opencode-config.js +141 -9
- package/dist/cli/templates.js +1 -1
- package/dist/cli.js +172 -33
- package/dist/config/gateway.js +3 -2
- package/dist/config/memory.d.ts +1 -1
- package/dist/config/memory.js +7 -7
- package/dist/config/paths.d.ts +2 -0
- package/dist/config/paths.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,13 +12,14 @@ npx opencode-gateway init
|
|
|
12
12
|
|
|
13
13
|
This ensures:
|
|
14
14
|
|
|
15
|
-
- `plugin
|
|
16
|
-
- `opencode-gateway.toml` exists next to
|
|
15
|
+
- `plugin` contains `opencode-gateway@latest`
|
|
16
|
+
- `opencode-gateway.toml` exists next to the preferred OpenCode config file
|
|
17
17
|
|
|
18
18
|
By default the CLI uses `OPENCODE_CONFIG_DIR` when it is set, otherwise it
|
|
19
19
|
writes to:
|
|
20
20
|
|
|
21
|
-
- `~/.config/opencode/opencode.json`
|
|
21
|
+
- an existing `~/.config/opencode/opencode.jsonc` or `~/.config/opencode/opencode.json`
|
|
22
|
+
- otherwise a new `~/.config/opencode/opencode.jsonc`
|
|
22
23
|
- `~/.config/opencode/opencode-gateway.toml`
|
|
23
24
|
|
|
24
25
|
Check what it resolved:
|
|
@@ -92,5 +93,6 @@ Memory rules:
|
|
|
92
93
|
- `inject_markdown_contents = true` recursively injects `*.md` and `*.markdown`
|
|
93
94
|
- `globs` are relative to the configured directory and may match other UTF-8
|
|
94
95
|
text files
|
|
95
|
-
- relative paths are resolved from `opencode-gateway
|
|
96
|
+
- relative paths are resolved from `opencode-gateway-workspace`
|
|
97
|
+
- absolute paths are still allowed
|
|
96
98
|
- memory is injected only into gateway-managed sessions
|
package/dist/cli/doctor.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { GATEWAY_CONFIG_FILE,
|
|
4
|
-
import { parseOpencodeConfig } from "./opencode-config";
|
|
3
|
+
import { GATEWAY_CONFIG_FILE, resolveGatewayWorkspacePath } from "../config/paths";
|
|
4
|
+
import { inspectGatewayPlugin, parseOpencodeConfig } from "./opencode-config";
|
|
5
|
+
import { resolveOpencodeConfigFile } from "./opencode-config-file";
|
|
5
6
|
import { pathExists, resolveCliConfigDir } from "./paths";
|
|
6
7
|
export async function runDoctor(options, env) {
|
|
7
8
|
const configDir = resolveCliConfigDir(options, env);
|
|
8
|
-
const opencodeConfigPath = join(configDir, OPENCODE_CONFIG_FILE);
|
|
9
9
|
const gatewayConfigPath = join(configDir, GATEWAY_CONFIG_FILE);
|
|
10
10
|
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
11
|
-
const
|
|
11
|
+
const opencodeConfig = await resolveOpencodeConfigFile(configDir);
|
|
12
|
+
const opencodeStatus = await inspectOpencodeConfig(opencodeConfig.path);
|
|
12
13
|
const gatewayOverride = env.OPENCODE_GATEWAY_CONFIG?.trim() || null;
|
|
13
14
|
console.log("doctor report");
|
|
14
15
|
console.log(` config dir: ${configDir}`);
|
|
15
|
-
console.log(` opencode config: ${await describePath(
|
|
16
|
+
console.log(` opencode config: ${await describePath(opencodeConfig.path)}`);
|
|
16
17
|
console.log(` gateway config: ${await describePath(gatewayConfigPath)}`);
|
|
17
18
|
console.log(` gateway workspace: ${await describePath(workspaceDirPath)}`);
|
|
18
19
|
console.log(` gateway config override: ${gatewayOverride ?? "not set"}`);
|
|
@@ -34,21 +35,8 @@ async function inspectOpencodeConfig(path) {
|
|
|
34
35
|
}
|
|
35
36
|
try {
|
|
36
37
|
const parsed = parseOpencodeConfig(await readFile(path, "utf8"), path);
|
|
37
|
-
const plugins = parsed.plugin;
|
|
38
|
-
if (plugins === undefined) {
|
|
39
|
-
return {
|
|
40
|
-
pluginConfigured: "no",
|
|
41
|
-
error: null,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
if (!Array.isArray(plugins)) {
|
|
45
|
-
return {
|
|
46
|
-
pluginConfigured: "invalid",
|
|
47
|
-
error: "`plugin` is not an array",
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
38
|
return {
|
|
51
|
-
pluginConfigured:
|
|
39
|
+
pluginConfigured: inspectGatewayPlugin(parsed),
|
|
52
40
|
error: null,
|
|
53
41
|
};
|
|
54
42
|
}
|
package/dist/cli/init.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { defaultGatewayStateDbPath, GATEWAY_CONFIG_FILE,
|
|
3
|
+
import { defaultGatewayStateDbPath, GATEWAY_CONFIG_FILE, resolveGatewayWorkspacePath } from "../config/paths";
|
|
4
4
|
import { createDefaultOpencodeConfig, ensureGatewayPlugin, parseOpencodeConfig, stringifyOpencodeConfig, } from "./opencode-config";
|
|
5
|
+
import { resolveOpencodeConfigFile } from "./opencode-config-file";
|
|
5
6
|
import { pathExists, resolveCliConfigDir } from "./paths";
|
|
6
7
|
import { buildGatewayConfigTemplate } from "./templates";
|
|
7
8
|
export async function runInit(options, env) {
|
|
8
9
|
const configDir = resolveCliConfigDir(options, env);
|
|
9
|
-
const opencodeConfigPath = join(configDir, OPENCODE_CONFIG_FILE);
|
|
10
10
|
const gatewayConfigPath = join(configDir, GATEWAY_CONFIG_FILE);
|
|
11
11
|
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
12
|
+
const opencodeConfig = await resolveOpencodeConfigFile(configDir);
|
|
13
|
+
const opencodeConfigPath = opencodeConfig.path;
|
|
12
14
|
await mkdir(configDir, { recursive: true });
|
|
13
15
|
await mkdir(workspaceDirPath, { recursive: true });
|
|
14
16
|
let opencodeStatus = "already present";
|
|
15
|
-
if (!
|
|
17
|
+
if (!opencodeConfig.exists) {
|
|
16
18
|
await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
|
|
17
19
|
opencodeStatus = "created";
|
|
18
20
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { OPENCODE_CONFIG_FILE_CANDIDATES, OPENCODE_CONFIG_FILE_JSONC } from "../config/paths";
|
|
3
|
+
import { pathExists } from "./paths";
|
|
4
|
+
export async function resolveOpencodeConfigFile(configDir) {
|
|
5
|
+
for (const fileName of OPENCODE_CONFIG_FILE_CANDIDATES) {
|
|
6
|
+
const path = join(configDir, fileName);
|
|
7
|
+
if (await pathExists(path)) {
|
|
8
|
+
return {
|
|
9
|
+
path,
|
|
10
|
+
exists: true,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
path: join(configDir, OPENCODE_CONFIG_FILE_JSONC),
|
|
16
|
+
exists: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -3,8 +3,10 @@ export type EnsurePluginResult = {
|
|
|
3
3
|
changed: boolean;
|
|
4
4
|
document: OpencodeConfigDocument;
|
|
5
5
|
};
|
|
6
|
+
export type GatewayPluginStatus = "yes" | "no" | "needs_update";
|
|
6
7
|
export declare function createDefaultOpencodeConfig(managed: boolean): OpencodeConfigDocument;
|
|
7
8
|
export declare function ensureGatewayPlugin(document: OpencodeConfigDocument): EnsurePluginResult;
|
|
8
9
|
export declare function parseOpencodeConfig(source: string, path: string): OpencodeConfigDocument;
|
|
9
10
|
export declare function stringifyOpencodeConfig(document: OpencodeConfigDocument): string;
|
|
11
|
+
export declare function inspectGatewayPlugin(document: OpencodeConfigDocument): GatewayPluginStatus;
|
|
10
12
|
export {};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
2
2
|
const PACKAGE_NAME = "opencode-gateway";
|
|
3
|
+
const PACKAGE_SPEC = "opencode-gateway@latest";
|
|
3
4
|
export function createDefaultOpencodeConfig(managed) {
|
|
4
5
|
const document = {
|
|
5
6
|
$schema: OPENCODE_SCHEMA_URL,
|
|
6
|
-
plugin: [
|
|
7
|
+
plugin: [PACKAGE_SPEC],
|
|
7
8
|
};
|
|
8
9
|
if (managed) {
|
|
9
10
|
document.server = {
|
|
@@ -14,20 +15,17 @@ export function createDefaultOpencodeConfig(managed) {
|
|
|
14
15
|
return document;
|
|
15
16
|
}
|
|
16
17
|
export function ensureGatewayPlugin(document) {
|
|
17
|
-
const plugins = document
|
|
18
|
+
const plugins = readPluginArray(document);
|
|
18
19
|
if (plugins === undefined) {
|
|
19
20
|
return {
|
|
20
21
|
changed: true,
|
|
21
22
|
document: {
|
|
22
23
|
...document,
|
|
23
|
-
plugin: [
|
|
24
|
+
plugin: [PACKAGE_SPEC],
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
|
-
if (
|
|
28
|
-
throw new Error("opencode.json field `plugin` must be an array when present");
|
|
29
|
-
}
|
|
30
|
-
if (plugins.some((entry) => entry === PACKAGE_NAME)) {
|
|
28
|
+
if (plugins.includes(PACKAGE_SPEC)) {
|
|
31
29
|
return {
|
|
32
30
|
changed: false,
|
|
33
31
|
document,
|
|
@@ -37,14 +35,14 @@ export function ensureGatewayPlugin(document) {
|
|
|
37
35
|
changed: true,
|
|
38
36
|
document: {
|
|
39
37
|
...document,
|
|
40
|
-
plugin: [...plugins,
|
|
38
|
+
plugin: [...plugins, PACKAGE_SPEC],
|
|
41
39
|
},
|
|
42
40
|
};
|
|
43
41
|
}
|
|
44
42
|
export function parseOpencodeConfig(source, path) {
|
|
45
43
|
let parsed;
|
|
46
44
|
try {
|
|
47
|
-
parsed = JSON.parse(source);
|
|
45
|
+
parsed = JSON.parse(toStrictJson(source));
|
|
48
46
|
}
|
|
49
47
|
catch (error) {
|
|
50
48
|
throw new Error(`failed to parse opencode config ${path}: ${formatError(error)}`);
|
|
@@ -57,6 +55,140 @@ export function parseOpencodeConfig(source, path) {
|
|
|
57
55
|
export function stringifyOpencodeConfig(document) {
|
|
58
56
|
return `${JSON.stringify(document, null, 2)}\n`;
|
|
59
57
|
}
|
|
58
|
+
export function inspectGatewayPlugin(document) {
|
|
59
|
+
const plugins = readPluginArray(document);
|
|
60
|
+
if (plugins === undefined) {
|
|
61
|
+
return "no";
|
|
62
|
+
}
|
|
63
|
+
if (plugins.includes(PACKAGE_SPEC)) {
|
|
64
|
+
return "yes";
|
|
65
|
+
}
|
|
66
|
+
return plugins.some((entry) => isGatewayPluginReference(entry)) ? "needs_update" : "no";
|
|
67
|
+
}
|
|
60
68
|
function formatError(error) {
|
|
61
69
|
return error instanceof Error ? error.message : String(error);
|
|
62
70
|
}
|
|
71
|
+
function readPluginArray(document) {
|
|
72
|
+
const plugins = document.plugin;
|
|
73
|
+
if (plugins === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(plugins)) {
|
|
77
|
+
throw new Error("opencode config field `plugin` must be an array when present");
|
|
78
|
+
}
|
|
79
|
+
const normalized = [];
|
|
80
|
+
for (const [index, entry] of plugins.entries()) {
|
|
81
|
+
if (typeof entry !== "string") {
|
|
82
|
+
throw new Error(`opencode config field \`plugin[${index}]\` must be a string`);
|
|
83
|
+
}
|
|
84
|
+
normalized.push(entry);
|
|
85
|
+
}
|
|
86
|
+
return normalized;
|
|
87
|
+
}
|
|
88
|
+
function toStrictJson(source) {
|
|
89
|
+
return stripTrailingCommas(stripJsonComments(source));
|
|
90
|
+
}
|
|
91
|
+
function stripJsonComments(source) {
|
|
92
|
+
let result = "";
|
|
93
|
+
let inString = false;
|
|
94
|
+
let escaped = false;
|
|
95
|
+
let inLineComment = false;
|
|
96
|
+
let inBlockComment = false;
|
|
97
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
98
|
+
const current = source[index];
|
|
99
|
+
const next = source[index + 1];
|
|
100
|
+
if (inLineComment) {
|
|
101
|
+
if (current === "\n") {
|
|
102
|
+
inLineComment = false;
|
|
103
|
+
result += current;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (inBlockComment) {
|
|
108
|
+
if (current === "*" && next === "/") {
|
|
109
|
+
inBlockComment = false;
|
|
110
|
+
index += 1;
|
|
111
|
+
}
|
|
112
|
+
else if (current === "\n") {
|
|
113
|
+
result += current;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (inString) {
|
|
118
|
+
result += current;
|
|
119
|
+
if (escaped) {
|
|
120
|
+
escaped = false;
|
|
121
|
+
}
|
|
122
|
+
else if (current === "\\") {
|
|
123
|
+
escaped = true;
|
|
124
|
+
}
|
|
125
|
+
else if (current === '"') {
|
|
126
|
+
inString = false;
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (current === '"') {
|
|
131
|
+
inString = true;
|
|
132
|
+
result += current;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (current === "/" && next === "/") {
|
|
136
|
+
inLineComment = true;
|
|
137
|
+
index += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (current === "/" && next === "*") {
|
|
141
|
+
inBlockComment = true;
|
|
142
|
+
index += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
result += current;
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
function stripTrailingCommas(source) {
|
|
150
|
+
let result = "";
|
|
151
|
+
let inString = false;
|
|
152
|
+
let escaped = false;
|
|
153
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
154
|
+
const current = source[index];
|
|
155
|
+
if (inString) {
|
|
156
|
+
result += current;
|
|
157
|
+
if (escaped) {
|
|
158
|
+
escaped = false;
|
|
159
|
+
}
|
|
160
|
+
else if (current === "\\") {
|
|
161
|
+
escaped = true;
|
|
162
|
+
}
|
|
163
|
+
else if (current === '"') {
|
|
164
|
+
inString = false;
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (current === '"') {
|
|
169
|
+
inString = true;
|
|
170
|
+
result += current;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (current === ",") {
|
|
174
|
+
const nextSignificant = findNextSignificantCharacter(source, index + 1);
|
|
175
|
+
if (nextSignificant === "]" || nextSignificant === "}") {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
result += current;
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
function findNextSignificantCharacter(source, startIndex) {
|
|
184
|
+
for (let index = startIndex; index < source.length; index += 1) {
|
|
185
|
+
const current = source[index];
|
|
186
|
+
if (!/\s/.test(current)) {
|
|
187
|
+
return current;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
function isGatewayPluginReference(entry) {
|
|
193
|
+
return entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`);
|
|
194
|
+
}
|
package/dist/cli/templates.js
CHANGED
|
@@ -21,7 +21,7 @@ export function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
21
21
|
"allowed_users = []",
|
|
22
22
|
"",
|
|
23
23
|
"# Optional long-lived memory sources injected into gateway-managed sessions.",
|
|
24
|
-
"# Relative paths are resolved from
|
|
24
|
+
"# Relative paths are resolved from opencode-gateway-workspace.",
|
|
25
25
|
"#",
|
|
26
26
|
"# [[memory.entries]]",
|
|
27
27
|
'# path = "memory/project.md"',
|
package/dist/cli.js
CHANGED
|
@@ -58,13 +58,15 @@ function formatCliHelp() {
|
|
|
58
58
|
|
|
59
59
|
// src/cli/doctor.ts
|
|
60
60
|
import { readFile } from "node:fs/promises";
|
|
61
|
-
import { join as
|
|
61
|
+
import { join as join3 } from "node:path";
|
|
62
62
|
|
|
63
63
|
// src/config/paths.ts
|
|
64
64
|
import { homedir } from "node:os";
|
|
65
65
|
import { dirname, join, resolve as resolve2 } from "node:path";
|
|
66
66
|
var GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
67
67
|
var OPENCODE_CONFIG_FILE = "opencode.json";
|
|
68
|
+
var OPENCODE_CONFIG_FILE_JSONC = "opencode.jsonc";
|
|
69
|
+
var OPENCODE_CONFIG_FILE_CANDIDATES = [OPENCODE_CONFIG_FILE_JSONC, OPENCODE_CONFIG_FILE];
|
|
68
70
|
var GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
69
71
|
function resolveOpencodeConfigDir(env) {
|
|
70
72
|
const explicit = env.OPENCODE_CONFIG_DIR;
|
|
@@ -95,10 +97,11 @@ function defaultOpencodeConfigDir(env) {
|
|
|
95
97
|
// src/cli/opencode-config.ts
|
|
96
98
|
var OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
97
99
|
var PACKAGE_NAME = "opencode-gateway";
|
|
100
|
+
var PACKAGE_SPEC = "opencode-gateway@latest";
|
|
98
101
|
function createDefaultOpencodeConfig(managed) {
|
|
99
102
|
const document = {
|
|
100
103
|
$schema: OPENCODE_SCHEMA_URL,
|
|
101
|
-
plugin: [
|
|
104
|
+
plugin: [PACKAGE_SPEC]
|
|
102
105
|
};
|
|
103
106
|
if (managed) {
|
|
104
107
|
document.server = {
|
|
@@ -109,20 +112,17 @@ function createDefaultOpencodeConfig(managed) {
|
|
|
109
112
|
return document;
|
|
110
113
|
}
|
|
111
114
|
function ensureGatewayPlugin(document) {
|
|
112
|
-
const plugins = document
|
|
115
|
+
const plugins = readPluginArray(document);
|
|
113
116
|
if (plugins === undefined) {
|
|
114
117
|
return {
|
|
115
118
|
changed: true,
|
|
116
119
|
document: {
|
|
117
120
|
...document,
|
|
118
|
-
plugin: [
|
|
121
|
+
plugin: [PACKAGE_SPEC]
|
|
119
122
|
}
|
|
120
123
|
};
|
|
121
124
|
}
|
|
122
|
-
if (
|
|
123
|
-
throw new Error("opencode.json field `plugin` must be an array when present");
|
|
124
|
-
}
|
|
125
|
-
if (plugins.some((entry) => entry === PACKAGE_NAME)) {
|
|
125
|
+
if (plugins.includes(PACKAGE_SPEC)) {
|
|
126
126
|
return {
|
|
127
127
|
changed: false,
|
|
128
128
|
document
|
|
@@ -132,14 +132,14 @@ function ensureGatewayPlugin(document) {
|
|
|
132
132
|
changed: true,
|
|
133
133
|
document: {
|
|
134
134
|
...document,
|
|
135
|
-
plugin: [...plugins,
|
|
135
|
+
plugin: [...plugins, PACKAGE_SPEC]
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
function parseOpencodeConfig(source, path) {
|
|
140
140
|
let parsed;
|
|
141
141
|
try {
|
|
142
|
-
parsed = JSON.parse(source);
|
|
142
|
+
parsed = JSON.parse(toStrictJson(source));
|
|
143
143
|
} catch (error) {
|
|
144
144
|
throw new Error(`failed to parse opencode config ${path}: ${formatError(error)}`);
|
|
145
145
|
}
|
|
@@ -152,9 +152,143 @@ function stringifyOpencodeConfig(document) {
|
|
|
152
152
|
return `${JSON.stringify(document, null, 2)}
|
|
153
153
|
`;
|
|
154
154
|
}
|
|
155
|
+
function inspectGatewayPlugin(document) {
|
|
156
|
+
const plugins = readPluginArray(document);
|
|
157
|
+
if (plugins === undefined) {
|
|
158
|
+
return "no";
|
|
159
|
+
}
|
|
160
|
+
if (plugins.includes(PACKAGE_SPEC)) {
|
|
161
|
+
return "yes";
|
|
162
|
+
}
|
|
163
|
+
return plugins.some((entry) => isGatewayPluginReference(entry)) ? "needs_update" : "no";
|
|
164
|
+
}
|
|
155
165
|
function formatError(error) {
|
|
156
166
|
return error instanceof Error ? error.message : String(error);
|
|
157
167
|
}
|
|
168
|
+
function readPluginArray(document) {
|
|
169
|
+
const plugins = document.plugin;
|
|
170
|
+
if (plugins === undefined) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!Array.isArray(plugins)) {
|
|
174
|
+
throw new Error("opencode config field `plugin` must be an array when present");
|
|
175
|
+
}
|
|
176
|
+
const normalized = [];
|
|
177
|
+
for (const [index, entry] of plugins.entries()) {
|
|
178
|
+
if (typeof entry !== "string") {
|
|
179
|
+
throw new Error(`opencode config field \`plugin[${index}]\` must be a string`);
|
|
180
|
+
}
|
|
181
|
+
normalized.push(entry);
|
|
182
|
+
}
|
|
183
|
+
return normalized;
|
|
184
|
+
}
|
|
185
|
+
function toStrictJson(source) {
|
|
186
|
+
return stripTrailingCommas(stripJsonComments(source));
|
|
187
|
+
}
|
|
188
|
+
function stripJsonComments(source) {
|
|
189
|
+
let result = "";
|
|
190
|
+
let inString = false;
|
|
191
|
+
let escaped = false;
|
|
192
|
+
let inLineComment = false;
|
|
193
|
+
let inBlockComment = false;
|
|
194
|
+
for (let index = 0;index < source.length; index += 1) {
|
|
195
|
+
const current = source[index];
|
|
196
|
+
const next = source[index + 1];
|
|
197
|
+
if (inLineComment) {
|
|
198
|
+
if (current === `
|
|
199
|
+
`) {
|
|
200
|
+
inLineComment = false;
|
|
201
|
+
result += current;
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (inBlockComment) {
|
|
206
|
+
if (current === "*" && next === "/") {
|
|
207
|
+
inBlockComment = false;
|
|
208
|
+
index += 1;
|
|
209
|
+
} else if (current === `
|
|
210
|
+
`) {
|
|
211
|
+
result += current;
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (inString) {
|
|
216
|
+
result += current;
|
|
217
|
+
if (escaped) {
|
|
218
|
+
escaped = false;
|
|
219
|
+
} else if (current === "\\") {
|
|
220
|
+
escaped = true;
|
|
221
|
+
} else if (current === '"') {
|
|
222
|
+
inString = false;
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (current === '"') {
|
|
227
|
+
inString = true;
|
|
228
|
+
result += current;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (current === "/" && next === "/") {
|
|
232
|
+
inLineComment = true;
|
|
233
|
+
index += 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (current === "/" && next === "*") {
|
|
237
|
+
inBlockComment = true;
|
|
238
|
+
index += 1;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
result += current;
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
function stripTrailingCommas(source) {
|
|
246
|
+
let result = "";
|
|
247
|
+
let inString = false;
|
|
248
|
+
let escaped = false;
|
|
249
|
+
for (let index = 0;index < source.length; index += 1) {
|
|
250
|
+
const current = source[index];
|
|
251
|
+
if (inString) {
|
|
252
|
+
result += current;
|
|
253
|
+
if (escaped) {
|
|
254
|
+
escaped = false;
|
|
255
|
+
} else if (current === "\\") {
|
|
256
|
+
escaped = true;
|
|
257
|
+
} else if (current === '"') {
|
|
258
|
+
inString = false;
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (current === '"') {
|
|
263
|
+
inString = true;
|
|
264
|
+
result += current;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (current === ",") {
|
|
268
|
+
const nextSignificant = findNextSignificantCharacter(source, index + 1);
|
|
269
|
+
if (nextSignificant === "]" || nextSignificant === "}") {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
result += current;
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
function findNextSignificantCharacter(source, startIndex) {
|
|
278
|
+
for (let index = startIndex;index < source.length; index += 1) {
|
|
279
|
+
const current = source[index];
|
|
280
|
+
if (!/\s/.test(current)) {
|
|
281
|
+
return current;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
function isGatewayPluginReference(entry) {
|
|
287
|
+
return entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/cli/opencode-config-file.ts
|
|
291
|
+
import { join as join2 } from "node:path";
|
|
158
292
|
|
|
159
293
|
// src/cli/paths.ts
|
|
160
294
|
import { constants } from "node:fs";
|
|
@@ -178,17 +312,34 @@ async function pathExists(path) {
|
|
|
178
312
|
}
|
|
179
313
|
}
|
|
180
314
|
|
|
315
|
+
// src/cli/opencode-config-file.ts
|
|
316
|
+
async function resolveOpencodeConfigFile(configDir) {
|
|
317
|
+
for (const fileName of OPENCODE_CONFIG_FILE_CANDIDATES) {
|
|
318
|
+
const path = join2(configDir, fileName);
|
|
319
|
+
if (await pathExists(path)) {
|
|
320
|
+
return {
|
|
321
|
+
path,
|
|
322
|
+
exists: true
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
path: join2(configDir, OPENCODE_CONFIG_FILE_JSONC),
|
|
328
|
+
exists: false
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
181
332
|
// src/cli/doctor.ts
|
|
182
333
|
async function runDoctor(options, env) {
|
|
183
334
|
const configDir = resolveCliConfigDir(options, env);
|
|
184
|
-
const
|
|
185
|
-
const gatewayConfigPath = join2(configDir, GATEWAY_CONFIG_FILE);
|
|
335
|
+
const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
|
|
186
336
|
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
187
|
-
const
|
|
337
|
+
const opencodeConfig = await resolveOpencodeConfigFile(configDir);
|
|
338
|
+
const opencodeStatus = await inspectOpencodeConfig(opencodeConfig.path);
|
|
188
339
|
const gatewayOverride = env.OPENCODE_GATEWAY_CONFIG?.trim() || null;
|
|
189
340
|
console.log("doctor report");
|
|
190
341
|
console.log(` config dir: ${configDir}`);
|
|
191
|
-
console.log(` opencode config: ${await describePath(
|
|
342
|
+
console.log(` opencode config: ${await describePath(opencodeConfig.path)}`);
|
|
192
343
|
console.log(` gateway config: ${await describePath(gatewayConfigPath)}`);
|
|
193
344
|
console.log(` gateway workspace: ${await describePath(workspaceDirPath)}`);
|
|
194
345
|
console.log(` gateway config override: ${gatewayOverride ?? "not set"}`);
|
|
@@ -210,21 +361,8 @@ async function inspectOpencodeConfig(path) {
|
|
|
210
361
|
}
|
|
211
362
|
try {
|
|
212
363
|
const parsed = parseOpencodeConfig(await readFile(path, "utf8"), path);
|
|
213
|
-
const plugins = parsed.plugin;
|
|
214
|
-
if (plugins === undefined) {
|
|
215
|
-
return {
|
|
216
|
-
pluginConfigured: "no",
|
|
217
|
-
error: null
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
if (!Array.isArray(plugins)) {
|
|
221
|
-
return {
|
|
222
|
-
pluginConfigured: "invalid",
|
|
223
|
-
error: "`plugin` is not an array"
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
364
|
return {
|
|
227
|
-
pluginConfigured:
|
|
365
|
+
pluginConfigured: inspectGatewayPlugin(parsed),
|
|
228
366
|
error: null
|
|
229
367
|
};
|
|
230
368
|
} catch (error) {
|
|
@@ -237,7 +375,7 @@ async function inspectOpencodeConfig(path) {
|
|
|
237
375
|
|
|
238
376
|
// src/cli/init.ts
|
|
239
377
|
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
240
|
-
import { dirname as dirname2, join as
|
|
378
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
241
379
|
|
|
242
380
|
// src/cli/templates.ts
|
|
243
381
|
function buildGatewayConfigTemplate(stateDbPath) {
|
|
@@ -263,7 +401,7 @@ function buildGatewayConfigTemplate(stateDbPath) {
|
|
|
263
401
|
"allowed_users = []",
|
|
264
402
|
"",
|
|
265
403
|
"# Optional long-lived memory sources injected into gateway-managed sessions.",
|
|
266
|
-
"# Relative paths are resolved from
|
|
404
|
+
"# Relative paths are resolved from opencode-gateway-workspace.",
|
|
267
405
|
"#",
|
|
268
406
|
"# [[memory.entries]]",
|
|
269
407
|
'# path = "memory/project.md"',
|
|
@@ -286,13 +424,14 @@ function escapeTomlString(value) {
|
|
|
286
424
|
// src/cli/init.ts
|
|
287
425
|
async function runInit(options, env) {
|
|
288
426
|
const configDir = resolveCliConfigDir(options, env);
|
|
289
|
-
const
|
|
290
|
-
const gatewayConfigPath = join3(configDir, GATEWAY_CONFIG_FILE);
|
|
427
|
+
const gatewayConfigPath = join4(configDir, GATEWAY_CONFIG_FILE);
|
|
291
428
|
const workspaceDirPath = resolveGatewayWorkspacePath(gatewayConfigPath);
|
|
429
|
+
const opencodeConfig = await resolveOpencodeConfigFile(configDir);
|
|
430
|
+
const opencodeConfigPath = opencodeConfig.path;
|
|
292
431
|
await mkdir(configDir, { recursive: true });
|
|
293
432
|
await mkdir(workspaceDirPath, { recursive: true });
|
|
294
433
|
let opencodeStatus = "already present";
|
|
295
|
-
if (!
|
|
434
|
+
if (!opencodeConfig.exists) {
|
|
296
435
|
await writeFile(opencodeConfigPath, stringifyOpencodeConfig(createDefaultOpencodeConfig(options.managed)));
|
|
297
436
|
opencodeStatus = "created";
|
|
298
437
|
} else {
|
package/dist/config/gateway.js
CHANGED
|
@@ -7,6 +7,7 @@ import { defaultGatewayStateDbPath, resolveGatewayConfigPath, resolveGatewayWork
|
|
|
7
7
|
import { parseTelegramConfig } from "./telegram";
|
|
8
8
|
export async function loadGatewayConfig(env = process.env) {
|
|
9
9
|
const configPath = resolveGatewayConfigPath(env);
|
|
10
|
+
const workspaceDirPath = resolveGatewayWorkspacePath(configPath);
|
|
10
11
|
const rawConfig = await readGatewayConfigFile(configPath);
|
|
11
12
|
const stateDbValue = rawConfig?.gateway?.state_db;
|
|
12
13
|
if (stateDbValue !== undefined && typeof stateDbValue !== "string") {
|
|
@@ -17,12 +18,12 @@ export async function loadGatewayConfig(env = process.env) {
|
|
|
17
18
|
configPath,
|
|
18
19
|
stateDbPath,
|
|
19
20
|
mediaRootPath: resolveMediaRootPath(stateDbPath),
|
|
20
|
-
workspaceDirPath
|
|
21
|
+
workspaceDirPath,
|
|
21
22
|
logLevel: parseGatewayLogLevel(rawConfig?.gateway?.log_level, "gateway.log_level"),
|
|
22
23
|
hasLegacyGatewayTimezone: rawConfig?.gateway?.timezone !== undefined,
|
|
23
24
|
legacyGatewayTimezone: readLegacyGatewayTimezone(rawConfig?.gateway?.timezone),
|
|
24
25
|
mailbox: parseMailboxConfig(rawConfig?.gateway?.mailbox),
|
|
25
|
-
memory: await parseMemoryConfig(rawConfig?.memory,
|
|
26
|
+
memory: await parseMemoryConfig(rawConfig?.memory, workspaceDirPath),
|
|
26
27
|
cron: parseCronConfig(rawConfig?.cron),
|
|
27
28
|
telegram: parseTelegramConfig(rawConfig?.channels?.telegram, env),
|
|
28
29
|
};
|
package/dist/config/memory.d.ts
CHANGED
|
@@ -15,4 +15,4 @@ export type GatewayMemoryEntryConfig = {
|
|
|
15
15
|
injectMarkdownContents: boolean;
|
|
16
16
|
globs: string[];
|
|
17
17
|
};
|
|
18
|
-
export declare function parseMemoryConfig(value: unknown,
|
|
18
|
+
export declare function parseMemoryConfig(value: unknown, workspaceDirPath: string): Promise<GatewayMemoryConfig>;
|
package/dist/config/memory.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
|
-
import {
|
|
3
|
-
export async function parseMemoryConfig(value,
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
export async function parseMemoryConfig(value, workspaceDirPath) {
|
|
4
4
|
const table = readMemoryTable(value);
|
|
5
|
-
const entries = await readMemoryEntries(table.entries,
|
|
5
|
+
const entries = await readMemoryEntries(table.entries, workspaceDirPath);
|
|
6
6
|
return { entries };
|
|
7
7
|
}
|
|
8
8
|
function readMemoryTable(value) {
|
|
@@ -14,16 +14,16 @@ function readMemoryTable(value) {
|
|
|
14
14
|
}
|
|
15
15
|
return value;
|
|
16
16
|
}
|
|
17
|
-
async function readMemoryEntries(value,
|
|
17
|
+
async function readMemoryEntries(value, workspaceDirPath) {
|
|
18
18
|
if (value === undefined) {
|
|
19
19
|
return [];
|
|
20
20
|
}
|
|
21
21
|
if (!Array.isArray(value)) {
|
|
22
22
|
throw new Error("memory.entries must be an array when present");
|
|
23
23
|
}
|
|
24
|
-
return await Promise.all(value.map((entry, index) => readMemoryEntry(entry, index,
|
|
24
|
+
return await Promise.all(value.map((entry, index) => readMemoryEntry(entry, index, workspaceDirPath)));
|
|
25
25
|
}
|
|
26
|
-
async function readMemoryEntry(value, index,
|
|
26
|
+
async function readMemoryEntry(value, index, workspaceDirPath) {
|
|
27
27
|
const field = `memory.entries[${index}]`;
|
|
28
28
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
29
29
|
throw new Error(`${field} must be a table`);
|
|
@@ -31,7 +31,7 @@ async function readMemoryEntry(value, index, configPath) {
|
|
|
31
31
|
const entry = value;
|
|
32
32
|
const displayPath = readRequiredString(entry.path, `${field}.path`);
|
|
33
33
|
const description = readRequiredString(entry.description, `${field}.description`);
|
|
34
|
-
const resolvedPath = resolve(
|
|
34
|
+
const resolvedPath = resolve(workspaceDirPath, displayPath);
|
|
35
35
|
const metadata = await statPath(resolvedPath, `${field}.path`);
|
|
36
36
|
if (metadata.isFile()) {
|
|
37
37
|
ensureDirectoryOnlyFieldIsAbsent(entry.inject_markdown_contents, `${field}.inject_markdown_contents`);
|
package/dist/config/paths.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
2
2
|
export declare const OPENCODE_CONFIG_FILE = "opencode.json";
|
|
3
|
+
export declare const OPENCODE_CONFIG_FILE_JSONC = "opencode.jsonc";
|
|
4
|
+
export declare const OPENCODE_CONFIG_FILE_CANDIDATES: readonly ["opencode.jsonc", "opencode.json"];
|
|
3
5
|
export declare const GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
4
6
|
type EnvSource = Record<string, string | undefined>;
|
|
5
7
|
export declare function resolveGatewayConfigPath(env: EnvSource): string;
|
package/dist/config/paths.js
CHANGED
|
@@ -2,6 +2,8 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
export const GATEWAY_CONFIG_FILE = "opencode-gateway.toml";
|
|
4
4
|
export const OPENCODE_CONFIG_FILE = "opencode.json";
|
|
5
|
+
export const OPENCODE_CONFIG_FILE_JSONC = "opencode.jsonc";
|
|
6
|
+
export const OPENCODE_CONFIG_FILE_CANDIDATES = [OPENCODE_CONFIG_FILE_JSONC, OPENCODE_CONFIG_FILE];
|
|
5
7
|
export const GATEWAY_WORKSPACE_DIR = "opencode-gateway-workspace";
|
|
6
8
|
export function resolveGatewayConfigPath(env) {
|
|
7
9
|
const explicit = env.OPENCODE_GATEWAY_CONFIG;
|