@chengchenccc/oh-my-agent 0.2.0-rc.1 → 0.2.0-rc.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/dist/cli/gateway-commands.d.ts +14 -2
- package/dist/cli/gateway-commands.d.ts.map +1 -1
- package/dist/cli/gateway-commands.js +102 -7
- package/dist/core/gateway/artifact.d.ts +3 -1
- package/dist/core/gateway/artifact.d.ts.map +1 -1
- package/dist/core/gateway/artifact.js +61 -9
- package/dist/core/gateway/daemon.d.ts.map +1 -1
- package/dist/core/gateway/daemon.js +3 -1
- package/dist/core/gateway/doctor.d.ts +21 -0
- package/dist/core/gateway/doctor.d.ts.map +1 -0
- package/dist/core/gateway/doctor.js +405 -0
- package/dist/protocol/transport.d.ts +14 -14
- package/package.json +11 -11
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/** The gateway domain's verbs. `oma gateway <verb>` is the CLI surface for
|
|
2
2
|
* backend + web: fetch their artifact, run it, report on it. */
|
|
3
|
-
export declare const GATEWAY_VERBS: readonly ["up", "down", "fetch", "status"];
|
|
3
|
+
export declare const GATEWAY_VERBS: readonly ["up", "down", "fetch", "status", "doctor", "passwd"];
|
|
4
4
|
export type GatewayCommand = (typeof GATEWAY_VERBS)[number];
|
|
5
|
-
export declare const GATEWAY_USAGE = "oma gateway <command>\n\n up start backend + web; stays in the foreground (Ctrl-C stops it)\n up -d same, but detached: returns once it is healthy\n down stop a detached gateway\n fetch download, verify and unpack the gateway artifact for a version\n status which version is installed, is it running, is it answering\n\n --version <v> artifact version for up/fetch (default: oma's own)\n\nNote: 'gateway' is a reserved first word. To send it as a prompt, use\n'oma -p \"gateway ...\"'.";
|
|
5
|
+
export declare const GATEWAY_USAGE = "oma gateway <command>\n\n up start backend + web; stays in the foreground (Ctrl-C stops it)\n up -d same, but detached: returns once it is healthy\n down stop a detached gateway\n fetch download, verify and unpack the gateway artifact for a version\n status which version is installed, is it running, is it answering\n doctor why nothing can run: artifact, tools, catalog, keys, ports, oma bin\n passwd replace the login password with a fresh random one\n\n --version <v> artifact version for up/fetch (default: oma's own)\n\nNote: 'gateway' is a reserved first word. To send it as a prompt, use\n'oma -p \"gateway ...\"'.";
|
|
6
6
|
export declare function isGatewayCommand(value: string): value is GatewayCommand;
|
|
7
|
+
/** What to tell the operator about logging in. The password is only echoed on
|
|
8
|
+
* an interactive terminal: a detached run's stdout is redirected into a log
|
|
9
|
+
* file, and a secret must not be filed away there. The pointer is always
|
|
10
|
+
* printed, so a missed banner is never a dead end. */
|
|
11
|
+
export declare function credentialLines(home: string, secrets: Record<string, string>, interactive: boolean): string[];
|
|
7
12
|
export interface GatewayCommandOptions {
|
|
8
13
|
home?: string;
|
|
9
14
|
version?: string;
|
|
@@ -21,6 +26,13 @@ export declare function runGatewayStatus(opts?: GatewayCommandOptions): Promise<
|
|
|
21
26
|
/** `oma gateway up`: fetch the version if it is missing, then run the whole gateway in
|
|
22
27
|
* the foreground (Ctrl-C stops it, children are signalled in reverse order). */
|
|
23
28
|
export declare function runGatewayUp(opts?: GatewayCommandOptions): Promise<number>;
|
|
29
|
+
/** `oma gateway passwd`: rotate the login password. Generated, never read from
|
|
30
|
+
* argv — an argument would land in shell history and process listings.
|
|
31
|
+
* Takes effect on the next start, because the web component reads it from its
|
|
32
|
+
* environment. */
|
|
33
|
+
export declare function runGatewayPasswd(opts?: GatewayCommandOptions): Promise<number>;
|
|
34
|
+
/** `oma gateway doctor`: check the deployment and say how to fix what is wrong. */
|
|
35
|
+
export declare function runGatewayDoctor(opts?: GatewayCommandOptions): Promise<number>;
|
|
24
36
|
/** `oma gateway down`: stop whatever `up -d` started. */
|
|
25
37
|
export declare function runGatewayDown(opts?: GatewayCommandOptions): Promise<number>;
|
|
26
38
|
/** `oma gateway <verb>`: single entry point so the CLI owns one code path. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway-commands.d.ts","sourceRoot":"","sources":["../../src/cli/gateway-commands.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gateway-commands.d.ts","sourceRoot":"","sources":["../../src/cli/gateway-commands.ts"],"names":[],"mappings":"AAwBA;iEACiE;AACjE,eAAO,MAAM,aAAa,gEAAiE,CAAC;AAC5F,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,eAAO,MAAM,aAAa,0pBAaF,CAAC;AAEzB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,cAAc,CAKvE;AAED;;;uDAGuD;AACvD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,WAAW,EAAE,OAAO,GACnB,MAAM,EAAE,CAMV;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAyBD;qEACqE;AACrE,wBAAsB,eAAe,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBvF;AAED;qEACqE;AACrE,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAgExF;AAED;iFACiF;AACjF,wBAAsB,YAAY,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAoCpF;AAED;;;mBAGmB;AACnB,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CA0BxF;AA2BD,mFAAmF;AACnF,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBxF;AAED,yDAAyD;AACzD,wBAAsB,cAAc,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAUtF;AA+DD,8EAA8E;AAC9E,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,cAAc,EACpB,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,MAAM,CAAC,CAOjB"}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
2
3
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { currentVersion, fetchGatewayArtifact, gatewayPaths, installedVersions, omaHome, omaVersion, readSecrets, } from "../core/gateway/artifact.js";
|
|
4
|
+
import { currentVersion, fetchGatewayArtifact, gatewayPaths, generatePassword, installedVersions, omaHome, omaVersion, readSecrets, } from "../core/gateway/artifact.js";
|
|
4
5
|
import { isProcessAlive, readDaemon, startDetachedGateway, stopDetachedGateway, tailDaemonLog, } from "../core/gateway/daemon.js";
|
|
6
|
+
import { diagnoseGateway } from "../core/gateway/doctor.js";
|
|
5
7
|
import { readGatewayManifest } from "../core/gateway/manifest.js";
|
|
6
8
|
import { runGateway } from "../core/gateway/supervisor.js";
|
|
7
9
|
/** The gateway domain's verbs. `oma gateway <verb>` is the CLI surface for
|
|
8
10
|
* backend + web: fetch their artifact, run it, report on it. */
|
|
9
|
-
export const GATEWAY_VERBS = ["up", "down", "fetch", "status"];
|
|
11
|
+
export const GATEWAY_VERBS = ["up", "down", "fetch", "status", "doctor", "passwd"];
|
|
10
12
|
export const GATEWAY_USAGE = `oma gateway <command>
|
|
11
13
|
|
|
12
14
|
up start backend + web; stays in the foreground (Ctrl-C stops it)
|
|
@@ -14,6 +16,8 @@ export const GATEWAY_USAGE = `oma gateway <command>
|
|
|
14
16
|
down stop a detached gateway
|
|
15
17
|
fetch download, verify and unpack the gateway artifact for a version
|
|
16
18
|
status which version is installed, is it running, is it answering
|
|
19
|
+
doctor why nothing can run: artifact, tools, catalog, keys, ports, oma bin
|
|
20
|
+
passwd replace the login password with a fresh random one
|
|
17
21
|
|
|
18
22
|
--version <v> artifact version for up/fetch (default: oma's own)
|
|
19
23
|
|
|
@@ -26,6 +30,18 @@ export function isGatewayCommand(value) {
|
|
|
26
30
|
}
|
|
27
31
|
return false;
|
|
28
32
|
}
|
|
33
|
+
/** What to tell the operator about logging in. The password is only echoed on
|
|
34
|
+
* an interactive terminal: a detached run's stdout is redirected into a log
|
|
35
|
+
* file, and a secret must not be filed away there. The pointer is always
|
|
36
|
+
* printed, so a missed banner is never a dead end. */
|
|
37
|
+
export function credentialLines(home, secrets, interactive) {
|
|
38
|
+
const lines = [];
|
|
39
|
+
const password = secrets.MOCK_PASSWORD;
|
|
40
|
+
if (password && interactive)
|
|
41
|
+
lines.push(`login: user-001 / ${password}`);
|
|
42
|
+
lines.push(`login password: oma gateway status (or read ${gatewayPaths(home).secrets})`);
|
|
43
|
+
return lines;
|
|
44
|
+
}
|
|
29
45
|
/** A source checkout runs the gateway from the repo, so the release artifact it
|
|
30
46
|
* would download is unused. Only stay quiet there: an explicit CLI call still
|
|
31
47
|
* fetches. `npm_lifecycle_event` is set while a package's postinstall runs. */
|
|
@@ -81,6 +97,13 @@ export async function runGatewayStatus(opts = {}) {
|
|
|
81
97
|
log(`gateway home: ${paths.home}`);
|
|
82
98
|
log(`versions: ${versions.length > 0 ? versions.join(", ") : "(none installed)"}`);
|
|
83
99
|
log(`current: ${current ?? "(none)"}`);
|
|
100
|
+
// Which catalog the gateway's children will read: product runs deliberately
|
|
101
|
+
// ignore ~/.oma/models.yml (the agent's own bash tool can write there), so a
|
|
102
|
+
// custom provider only counts when OMA_HOME points at its models.yml.
|
|
103
|
+
const catalogHome = process.env.OMA_HOME;
|
|
104
|
+
log(catalogHome
|
|
105
|
+
? `catalog: ${catalogHome}/models.yml via OMA_HOME`
|
|
106
|
+
: "catalog: built-in providers only (OMA_HOME unset — a custom models.yml is not read)");
|
|
84
107
|
if (versions.length === 0) {
|
|
85
108
|
log(`install one: oma gateway fetch (oma ${omaVersion()})`);
|
|
86
109
|
return 1;
|
|
@@ -154,8 +177,9 @@ export async function runGatewayUp(opts = {}) {
|
|
|
154
177
|
log("");
|
|
155
178
|
log(`gateway ${handle.manifest.version} is up (${handle.manifest.components.length} processes)`);
|
|
156
179
|
log(`open http://127.0.0.1:${web?.port ?? 3001}/login`);
|
|
157
|
-
|
|
158
|
-
log(
|
|
180
|
+
for (const line of credentialLines(home, handle.secrets, process.stdout.isTTY === true)) {
|
|
181
|
+
log(line);
|
|
182
|
+
}
|
|
159
183
|
log("Ctrl-C stops the gateway");
|
|
160
184
|
},
|
|
161
185
|
});
|
|
@@ -165,6 +189,73 @@ export async function runGatewayUp(opts = {}) {
|
|
|
165
189
|
return 1;
|
|
166
190
|
}
|
|
167
191
|
}
|
|
192
|
+
/** `oma gateway passwd`: rotate the login password. Generated, never read from
|
|
193
|
+
* argv — an argument would land in shell history and process listings.
|
|
194
|
+
* Takes effect on the next start, because the web component reads it from its
|
|
195
|
+
* environment. */
|
|
196
|
+
export async function runGatewayPasswd(opts = {}) {
|
|
197
|
+
const home = opts.home ?? omaHome();
|
|
198
|
+
const log = loggerFor(opts, "stdout");
|
|
199
|
+
const secretsPath = gatewayPaths(home).secrets;
|
|
200
|
+
if (!existsSync(secretsPath)) {
|
|
201
|
+
log(`no secrets at ${secretsPath} — run 'oma gateway up' once first`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
const password = generatePassword();
|
|
205
|
+
const secrets = { ...readSecrets(home), MOCK_PASSWORD: password };
|
|
206
|
+
await writeFile(secretsPath, `${JSON.stringify(secrets, null, 2)}\n`, { mode: 0o600 });
|
|
207
|
+
log(`login password replaced: user-001 / ${password}`);
|
|
208
|
+
// A password set in the console is stored as a hash and wins over this file,
|
|
209
|
+
// so push the new one there too when the gateway is up — otherwise rotating
|
|
210
|
+
// here would silently not take effect.
|
|
211
|
+
const pushed = await pushPasswordToRunningGateway(secrets, password);
|
|
212
|
+
if (pushed) {
|
|
213
|
+
log("the running gateway took it too (console-set password updated)");
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
log("restart to apply: oma gateway down && oma gateway up -d");
|
|
217
|
+
log("note: a password set in the web console wins over this file — change it there if you used that");
|
|
218
|
+
}
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
/** Update the console-set password (a hash in the backend's settings) when the
|
|
222
|
+
* gateway is reachable. Returns false when it is not: the file value then
|
|
223
|
+
* applies after the next start. */
|
|
224
|
+
async function pushPasswordToRunningGateway(secrets, password) {
|
|
225
|
+
const token = secrets.BACKEND_AUTH_TOKEN;
|
|
226
|
+
if (!token)
|
|
227
|
+
return false;
|
|
228
|
+
try {
|
|
229
|
+
const res = await fetch(`${process.env.BACKEND_URL ?? "http://127.0.0.1:3000"}/api/auth/password`, {
|
|
230
|
+
method: "PUT",
|
|
231
|
+
headers: { "content-type": "application/json", "x-auth-token": token },
|
|
232
|
+
body: JSON.stringify({ password }),
|
|
233
|
+
signal: AbortSignal.timeout(2000),
|
|
234
|
+
});
|
|
235
|
+
return res.ok;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** `oma gateway doctor`: check the deployment and say how to fix what is wrong. */
|
|
242
|
+
export async function runGatewayDoctor(opts = {}) {
|
|
243
|
+
const home = opts.home ?? omaHome();
|
|
244
|
+
const log = loggerFor(opts, "stdout");
|
|
245
|
+
const checks = await diagnoseGateway({ home });
|
|
246
|
+
log(`oma gateway doctor (home: ${home})`);
|
|
247
|
+
for (const check of checks) {
|
|
248
|
+
log(`${check.ok ? "✓" : "✗"} ${check.id.padEnd(9)} ${check.detail}`);
|
|
249
|
+
if (check.fix)
|
|
250
|
+
log(` ${check.ok ? "note" : "fix"}: ${check.fix}`);
|
|
251
|
+
}
|
|
252
|
+
const failed = checks.filter((check) => !check.ok);
|
|
253
|
+
log("");
|
|
254
|
+
log(failed.length === 0
|
|
255
|
+
? "no problems found"
|
|
256
|
+
: `${failed.length} problem(s): ${failed.map((c) => c.id).join(", ")}`);
|
|
257
|
+
return failed.length === 0 ? 0 : 1;
|
|
258
|
+
}
|
|
168
259
|
/** `oma gateway down`: stop whatever `up -d` started. */
|
|
169
260
|
export async function runGatewayDown(opts = {}) {
|
|
170
261
|
const home = opts.home ?? omaHome();
|
|
@@ -206,12 +297,12 @@ async function startGatewayDetached(opts) {
|
|
|
206
297
|
try {
|
|
207
298
|
const res = await fetch(healthUrl, { signal: AbortSignal.timeout(2000) });
|
|
208
299
|
if (res.ok) {
|
|
209
|
-
const password = readSecrets(opts.home).MOCK_PASSWORD;
|
|
210
300
|
opts.log("");
|
|
211
301
|
opts.log(`gateway ${opts.version} is up in the background (pid ${daemon.pid})`);
|
|
212
302
|
opts.log(`open http://127.0.0.1:${web?.port ?? 3001}/login`);
|
|
213
|
-
|
|
214
|
-
opts.log(
|
|
303
|
+
for (const line of credentialLines(opts.home, readSecrets(opts.home), process.stdout.isTTY === true)) {
|
|
304
|
+
opts.log(line);
|
|
305
|
+
}
|
|
215
306
|
opts.log("stop it with: oma gateway down");
|
|
216
307
|
return 0;
|
|
217
308
|
}
|
|
@@ -238,5 +329,9 @@ export async function runGatewayCommand(verb, opts = {}) {
|
|
|
238
329
|
return runGatewayDown(opts);
|
|
239
330
|
if (verb === "fetch")
|
|
240
331
|
return runGatewayFetch(opts);
|
|
332
|
+
if (verb === "doctor")
|
|
333
|
+
return runGatewayDoctor(opts);
|
|
334
|
+
if (verb === "passwd")
|
|
335
|
+
return runGatewayPasswd(opts);
|
|
241
336
|
return runGatewayStatus(opts);
|
|
242
337
|
}
|
|
@@ -29,7 +29,7 @@ export declare function releaseLocation(version: string, env?: NodeJS.ProcessEnv
|
|
|
29
29
|
export declare function sha256File(path: string): Promise<string>;
|
|
30
30
|
/** Parse the SHA256SUMS entry for one file (GNU coreutils format). */
|
|
31
31
|
export declare function parseSums(sums: string, fileName: string): string;
|
|
32
|
-
|
|
32
|
+
export declare function generatePassword(): string;
|
|
33
33
|
export declare function readSecrets(home: string): Record<string, string>;
|
|
34
34
|
/** Read the launcher's secrets, generating any missing one. 0600: they
|
|
35
35
|
* authenticate the local single-user gateway. */
|
|
@@ -39,6 +39,8 @@ export interface FetchOptions {
|
|
|
39
39
|
version?: string;
|
|
40
40
|
env?: NodeJS.ProcessEnv;
|
|
41
41
|
log?: (line: string) => void;
|
|
42
|
+
/** No-byte allowance before a download is declared dead (test seam). */
|
|
43
|
+
stallMs?: number;
|
|
42
44
|
}
|
|
43
45
|
export interface FetchResult {
|
|
44
46
|
version: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../../../src/core/gateway/artifact.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAAuB,MAAM,eAAe,CAAC;AAE1E,qBAAa,oBAAqB,SAAQ,KAAK;CAAG;AAElD,eAAO,MAAM,kBAAkB,4BAA4B,CAAC;AAM5D,wBAAgB,OAAO,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAEpE;AAED;;sDAEsD;AACtD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,YAAY,CAAC,IAAI,GAAE,MAAkB,GAAG,YAAY,CAUnE;AAED;qDACqD;AACrD,wBAAgB,UAAU,IAAI,MAAM,CAQnC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;oFACoF;AACpF,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAMjB;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI9D;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMhE;
|
|
1
|
+
{"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../../../src/core/gateway/artifact.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAAuB,MAAM,eAAe,CAAC;AAE1E,qBAAa,oBAAqB,SAAQ,KAAK;CAAG;AAElD,eAAO,MAAM,kBAAkB,4BAA4B,CAAC;AAM5D,wBAAgB,OAAO,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAEpE;AAED;;sDAEsD;AACtD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,YAAY,CAAC,IAAI,GAAE,MAAkB,GAAG,YAAY,CAUnE;AAED;qDACqD;AACrD,wBAAgB,UAAU,IAAI,MAAM,CAQnC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;oFACoF;AACpF,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAMjB;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI9D;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMhE;AAiFD,wBAAgB,gBAAgB,IAAI,MAAM,CAYzC;AASD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBhE;AAED;kDACkD;AAClD,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiBjC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,eAAe,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;6EAC6E;AAC7E,wBAAsB,oBAAoB,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAuDxF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,EAAE,CAMpE;AAED,wBAAgB,cAAc,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,GAAG,SAAS,CAO3E"}
|
|
@@ -56,7 +56,24 @@ export function parseSums(sums, fileName) {
|
|
|
56
56
|
}
|
|
57
57
|
throw new GatewayArtifactError(`SHA256SUMS has no entry for ${fileName}`);
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
/** How long a download may go without delivering a byte before it is called
|
|
60
|
+
* dead. A slow-but-moving transfer is fine; a wedged one must not hang an
|
|
61
|
+
* install (a postinstall sat for 10 minutes before this existed). */
|
|
62
|
+
const DEFAULT_STALL_MS = 60_000;
|
|
63
|
+
async function withStallTimeout(work, ms, onStall) {
|
|
64
|
+
let timer;
|
|
65
|
+
const stall = new Promise((_, reject) => {
|
|
66
|
+
timer = setTimeout(() => reject(onStall()), ms);
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
return await Promise.race([work, stall]);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (timer)
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function download(url, dest, stallMs) {
|
|
60
77
|
let res;
|
|
61
78
|
try {
|
|
62
79
|
res = await fetch(url, { redirect: "follow" });
|
|
@@ -73,11 +90,17 @@ async function download(url, dest) {
|
|
|
73
90
|
// also keeps a 60MB artifact out of memory.
|
|
74
91
|
const writer = Bun.file(dest).writer();
|
|
75
92
|
const reader = res.body.getReader();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
93
|
+
try {
|
|
94
|
+
for (;;) {
|
|
95
|
+
const { done, value } = await withStallTimeout(reader.read(), stallMs, () => new GatewayArtifactError(`download stalled for ${Math.round(stallMs / 1000)}s at ${url} — check the connection, then retry`));
|
|
96
|
+
if (done)
|
|
97
|
+
break;
|
|
98
|
+
writer.write(value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
await reader.cancel().catch(() => { });
|
|
103
|
+
throw err;
|
|
81
104
|
}
|
|
82
105
|
await writer.end();
|
|
83
106
|
}
|
|
@@ -99,6 +122,34 @@ async function unpack(tarball, dest) {
|
|
|
99
122
|
}
|
|
100
123
|
}
|
|
101
124
|
/** Read the launcher's secrets without generating anything (status paths). */
|
|
125
|
+
/** Human-typed login password. Entropy comes from length, not from requiring
|
|
126
|
+
* character classes: 22 chars over a 56-symbol alphabet is ~128 bits. The
|
|
127
|
+
* alphabet drops look-alikes (l, 1, I, O, 0) and shell-hostile punctuation,
|
|
128
|
+
* because the person typing it may be reading it off a terminal on another
|
|
129
|
+
* device. */
|
|
130
|
+
const PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
|
|
131
|
+
const PASSWORD_LENGTH = 22;
|
|
132
|
+
export function generatePassword() {
|
|
133
|
+
// Rejection sampling keeps the alphabet uniform (256 % 56 is not zero).
|
|
134
|
+
const ceiling = Math.floor(256 / PASSWORD_ALPHABET.length) * PASSWORD_ALPHABET.length;
|
|
135
|
+
let password = "";
|
|
136
|
+
while (password.length < PASSWORD_LENGTH) {
|
|
137
|
+
for (const byte of crypto.getRandomValues(new Uint8Array(PASSWORD_LENGTH))) {
|
|
138
|
+
if (byte >= ceiling)
|
|
139
|
+
continue;
|
|
140
|
+
password += PASSWORD_ALPHABET.charAt(byte % PASSWORD_ALPHABET.length);
|
|
141
|
+
if (password.length === PASSWORD_LENGTH)
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return password;
|
|
146
|
+
}
|
|
147
|
+
/** Internal tokens (backend auth, session signing): never typed, so the cheap
|
|
148
|
+
* hex encoding is fine. */
|
|
149
|
+
function generateToken() {
|
|
150
|
+
const bytes = crypto.getRandomValues(new Uint8Array(24));
|
|
151
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
152
|
+
}
|
|
102
153
|
export function readSecrets(home) {
|
|
103
154
|
const path = gatewayPaths(home).secrets;
|
|
104
155
|
if (!existsSync(path))
|
|
@@ -127,8 +178,9 @@ export async function ensureSecrets(home, names) {
|
|
|
127
178
|
for (const name of names) {
|
|
128
179
|
if (existing[name])
|
|
129
180
|
continue;
|
|
130
|
-
|
|
131
|
-
|
|
181
|
+
// MOCK_PASSWORD is the one a human retypes at the login page; the rest are
|
|
182
|
+
// machine-to-machine tokens.
|
|
183
|
+
existing[name] = name === "MOCK_PASSWORD" ? generatePassword() : generateToken();
|
|
132
184
|
generated = true;
|
|
133
185
|
}
|
|
134
186
|
if (generated) {
|
|
@@ -163,7 +215,7 @@ export async function fetchGatewayArtifact(opts = {}) {
|
|
|
163
215
|
if (!sums.ok)
|
|
164
216
|
throw new GatewayArtifactError(`${sumsUrl} -> HTTP ${sums.status}`);
|
|
165
217
|
const expected = parseSums(await sums.text(), `oma-gateway-${version}.tar.zst`);
|
|
166
|
-
await download(tarballUrl, tarball);
|
|
218
|
+
await download(tarballUrl, tarball, opts.stallMs ?? DEFAULT_STALL_MS);
|
|
167
219
|
const actual = await sha256File(tarball);
|
|
168
220
|
if (actual !== expected) {
|
|
169
221
|
throw new GatewayArtifactError(`checksum mismatch for ${version}: expected ${expected.slice(0, 12)}…, got ${actual.slice(0, 12)}…`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../../src/core/gateway/daemon.ts"],"names":[],"mappings":"AAaA;mDACmD;AACnD,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAGrD;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CASlE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASnD;AAED;;;gFAGgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAexD;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;6CAE6C;AAC7C,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../../src/core/gateway/daemon.ts"],"names":[],"mappings":"AAaA;mDACmD;AACnD,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAGrD;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CASlE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASnD;AAED;;;gFAGgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAexD;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;6CAE6C;AAC7C,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CA+B7F;AAED;uBACuB;AACvB,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CA4B7F;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,EAAE,CAKhE"}
|
|
@@ -68,7 +68,9 @@ export async function startDetachedGateway(opts) {
|
|
|
68
68
|
const gateway = gatewayPaths(opts.home);
|
|
69
69
|
await mkdir(gateway.root, { recursive: true });
|
|
70
70
|
const paths = daemonPaths(opts.home);
|
|
71
|
-
|
|
71
|
+
// 0600: the log carries the supervisor's own output, and nothing that runs
|
|
72
|
+
// unattended should be world-readable.
|
|
73
|
+
const logFd = openSync(paths.log, "a", 0o600);
|
|
72
74
|
let pid;
|
|
73
75
|
try {
|
|
74
76
|
const proc = Bun.spawn([...opts.command], {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** One diagnosis line: what was checked, what was found, and how to fix it. */
|
|
2
|
+
export interface GatewayCheck {
|
|
3
|
+
id: string;
|
|
4
|
+
ok: boolean;
|
|
5
|
+
detail: string;
|
|
6
|
+
fix?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DoctorOptions {
|
|
9
|
+
home?: string;
|
|
10
|
+
/** Seam for tests: replaces the `oma --list-models` probe. */
|
|
11
|
+
probeModels?: () => Promise<{
|
|
12
|
+
providers: string[];
|
|
13
|
+
}>;
|
|
14
|
+
/** Seam for tests: replaces the release HEAD probe. Returns the HTTP status,
|
|
15
|
+
* or undefined when the host could not be reached. */
|
|
16
|
+
probeRelease?: (url: string) => Promise<number | undefined>;
|
|
17
|
+
}
|
|
18
|
+
/** Diagnose why a gateway may not be able to run anything: artifact, tools,
|
|
19
|
+
* catalog source, runnable providers, ports, and the per-run oma binary. */
|
|
20
|
+
export declare function diagnoseGateway(opts?: DoctorOptions): Promise<GatewayCheck[]>;
|
|
21
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/core/gateway/doctor.ts"],"names":[],"mappings":"AAeA,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACrD;2DACuD;IACvD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC7D;AAoED;6EAC6E;AAC7E,wBAAsB,eAAe,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA2UvF"}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statfsSync, statSync } from "node:fs";
|
|
2
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { currentVersion, gatewayPaths, omaHome, omaVersion, readSecrets, releaseLocation, } from "./artifact.js";
|
|
5
|
+
import { daemonPaths, isProcessAlive, readDaemon } from "./daemon.js";
|
|
6
|
+
import { readGatewayManifest } from "./manifest.js";
|
|
7
|
+
import { resolveOmaBin } from "./supervisor.js";
|
|
8
|
+
/** Provider env names oma's builtin catalog knows (the product backend
|
|
9
|
+
* forwards every *_API_KEY, but these work with no models.yml at all). */
|
|
10
|
+
const BUILTIN_KEY_ENVS = [
|
|
11
|
+
"ANTHROPIC_API_KEY",
|
|
12
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
13
|
+
"OPENAI_API_KEY",
|
|
14
|
+
"DEEPSEEK_API_KEY",
|
|
15
|
+
"GROQ_API_KEY",
|
|
16
|
+
"OPENROUTER_API_KEY",
|
|
17
|
+
];
|
|
18
|
+
/** Hosts that are not reachable from another machine. */
|
|
19
|
+
const LOOPBACK_HOSTS = ["127.0.0.1", "::1", "localhost"];
|
|
20
|
+
async function probeRunnableProviders() {
|
|
21
|
+
const entry = resolveOmaBin();
|
|
22
|
+
const proc = Bun.spawn([entry, "--list-models"], {
|
|
23
|
+
// Exactly the switches the product backend uses for its children, so the
|
|
24
|
+
// answer matches what a Run would see.
|
|
25
|
+
env: { ...process.env, OMA_WORKSPACE_CATALOG: "0" },
|
|
26
|
+
stdin: "ignore",
|
|
27
|
+
stdout: "pipe",
|
|
28
|
+
stderr: "ignore",
|
|
29
|
+
});
|
|
30
|
+
const text = await new Response(proc.stdout).text();
|
|
31
|
+
const code = await proc.exited;
|
|
32
|
+
if (code !== 0)
|
|
33
|
+
return { providers: [] };
|
|
34
|
+
const parsed = JSON.parse(text);
|
|
35
|
+
if (typeof parsed !== "object" || parsed === null || !("models" in parsed)) {
|
|
36
|
+
return { providers: [] };
|
|
37
|
+
}
|
|
38
|
+
const models = parsed.models;
|
|
39
|
+
if (!Array.isArray(models))
|
|
40
|
+
return { providers: [] };
|
|
41
|
+
const seen = {};
|
|
42
|
+
for (const model of models) {
|
|
43
|
+
if (typeof model !== "object" || model === null || !("id" in model))
|
|
44
|
+
continue;
|
|
45
|
+
const id = model.id;
|
|
46
|
+
if (typeof id !== "string")
|
|
47
|
+
continue;
|
|
48
|
+
const provider = id.split("/")[0];
|
|
49
|
+
if (provider)
|
|
50
|
+
seen[provider] = true;
|
|
51
|
+
}
|
|
52
|
+
return { providers: Object.keys(seen).sort() };
|
|
53
|
+
}
|
|
54
|
+
/** `{status:"ok"}` from :3000/health is our backend; anything else on that port
|
|
55
|
+
* is somebody else's process. */
|
|
56
|
+
async function backendAnswers(url) {
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
59
|
+
if (!res.ok)
|
|
60
|
+
return false;
|
|
61
|
+
const body = await res.json();
|
|
62
|
+
return typeof body === "object" && body !== null && "status" in body;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function anyPortAnswers(url) {
|
|
69
|
+
try {
|
|
70
|
+
await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Diagnose why a gateway may not be able to run anything: artifact, tools,
|
|
78
|
+
* catalog source, runnable providers, ports, and the per-run oma binary. */
|
|
79
|
+
export async function diagnoseGateway(opts = {}) {
|
|
80
|
+
const home = opts.home ?? omaHome();
|
|
81
|
+
const paths = gatewayPaths(home);
|
|
82
|
+
const checks = [];
|
|
83
|
+
// 1. artifact
|
|
84
|
+
const version = currentVersion(home) ?? omaVersion();
|
|
85
|
+
const dir = join(paths.versions, version);
|
|
86
|
+
const manifestPath = join(dir, "gateway.json");
|
|
87
|
+
let manifest;
|
|
88
|
+
if (existsSync(manifestPath)) {
|
|
89
|
+
manifest = readGatewayManifest(manifestPath);
|
|
90
|
+
checks.push({
|
|
91
|
+
id: "artifact",
|
|
92
|
+
ok: true,
|
|
93
|
+
detail: `${version} installed (${manifest.components.length} components) at ${dir}`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// A source checkout carries the repo's own version, which was never
|
|
98
|
+
// published: pointing at it would 404, so say what to do instead.
|
|
99
|
+
const fromSource = import.meta.filename.endsWith(".ts");
|
|
100
|
+
checks.push({
|
|
101
|
+
id: "artifact",
|
|
102
|
+
ok: false,
|
|
103
|
+
detail: `no gateway artifact for ${version} at ${dir}`,
|
|
104
|
+
fix: fromSource
|
|
105
|
+
? "running from a checkout: fetch a released version (oma gateway fetch --version <released>), or run the stack from the repo"
|
|
106
|
+
: `oma gateway fetch --version ${version}`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// 2. unpack tools (only needed by fetch)
|
|
110
|
+
const missingTools = ["tar", "zstd"].filter((tool) => !Bun.which(tool));
|
|
111
|
+
checks.push({
|
|
112
|
+
id: "tools",
|
|
113
|
+
ok: missingTools.length === 0,
|
|
114
|
+
detail: missingTools.length === 0
|
|
115
|
+
? "tar and zstd are on PATH"
|
|
116
|
+
: `missing: ${missingTools.join(", ")}`,
|
|
117
|
+
fix: missingTools.length > 0 ? `install: ${missingTools.join(" ")}` : undefined,
|
|
118
|
+
});
|
|
119
|
+
// 3. catalog source — informational, but it is the usual reason a custom
|
|
120
|
+
// provider is invisible: product children read ONLY $OMA_HOME/models.yml.
|
|
121
|
+
const catalogHome = process.env.OMA_HOME;
|
|
122
|
+
const customCatalog = catalogHome ? join(catalogHome, "models.yml") : undefined;
|
|
123
|
+
if (customCatalog) {
|
|
124
|
+
checks.push({
|
|
125
|
+
id: "catalog",
|
|
126
|
+
ok: true,
|
|
127
|
+
detail: existsSync(customCatalog)
|
|
128
|
+
? `custom catalog: ${customCatalog}`
|
|
129
|
+
: `OMA_HOME is set but ${customCatalog} does not exist (builtin providers only)`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
checks.push({
|
|
134
|
+
id: "catalog",
|
|
135
|
+
ok: true,
|
|
136
|
+
detail: "OMA_HOME is unset: no models.yml is read, so only builtin providers can be used",
|
|
137
|
+
fix: "to use a custom provider: put models.yml in a directory and start the gateway with OMA_HOME pointing at it",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// 4. provider credentials present in THIS environment (what a gateway
|
|
141
|
+
// started from here would hand to the backend).
|
|
142
|
+
const present = BUILTIN_KEY_ENVS.filter((key) => (process.env[key] ?? "").length > 0);
|
|
143
|
+
checks.push({
|
|
144
|
+
id: "keys",
|
|
145
|
+
ok: present.length > 0,
|
|
146
|
+
detail: present.length > 0
|
|
147
|
+
? `provider keys in this environment: ${present.join(", ")}`
|
|
148
|
+
: "no builtin provider key in this environment",
|
|
149
|
+
...(present.length === 0
|
|
150
|
+
? {
|
|
151
|
+
fix: "add one in the web UI (Settings -> Provider keys: stored server-side, no restart), or export it and restart the gateway",
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
});
|
|
155
|
+
// 5. the decisive one: what can actually run, asked the way the backend asks.
|
|
156
|
+
let providers;
|
|
157
|
+
try {
|
|
158
|
+
providers = (await (opts.probeModels ?? probeRunnableProviders)()).providers;
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
providers = [];
|
|
162
|
+
checks.push({
|
|
163
|
+
id: "models",
|
|
164
|
+
ok: false,
|
|
165
|
+
detail: `could not list models: ${err instanceof Error ? err.message : String(err)}`,
|
|
166
|
+
fix: "check that oma runs: oma --list-models",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (providers.length > 0) {
|
|
170
|
+
checks.push({ id: "models", ok: true, detail: `runnable providers: ${providers.join(", ")}` });
|
|
171
|
+
}
|
|
172
|
+
else if (checks.every((c) => c.id !== "models")) {
|
|
173
|
+
checks.push({
|
|
174
|
+
id: "models",
|
|
175
|
+
ok: false,
|
|
176
|
+
detail: "no runnable model: an agent Run would fail here",
|
|
177
|
+
fix: "for a builtin provider add its key in the web UI (Settings -> Provider keys); for a custom provider put models.yml where OMA_HOME points and restart",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// 6. download channel — only matters when the artifact is missing.
|
|
181
|
+
if (!manifest) {
|
|
182
|
+
const { sums } = releaseLocation(version, process.env);
|
|
183
|
+
const probe = opts.probeRelease ??
|
|
184
|
+
(async (url) => {
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(5000) });
|
|
187
|
+
return res.status;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const status = await probe(sums);
|
|
194
|
+
if (status === undefined) {
|
|
195
|
+
checks.push({
|
|
196
|
+
id: "download",
|
|
197
|
+
ok: false,
|
|
198
|
+
detail: `cannot reach the release host: ${sums}`,
|
|
199
|
+
fix: "check the network/proxy; a slow link shows up as a stalled download when fetching",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Any HTTP answer means the channel works; a 404 just means this version
|
|
204
|
+
// was never published (routine in a source checkout).
|
|
205
|
+
checks.push({
|
|
206
|
+
id: "download",
|
|
207
|
+
ok: true,
|
|
208
|
+
detail: status === 200
|
|
209
|
+
? `release reachable: ${sums}`
|
|
210
|
+
: `release host reachable, but ${version} has no release (HTTP ${status})`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// 7. secrets: present, parseable, and not readable by anyone else.
|
|
215
|
+
const secretsPath = paths.secrets;
|
|
216
|
+
if (!existsSync(secretsPath)) {
|
|
217
|
+
checks.push({
|
|
218
|
+
id: "secrets",
|
|
219
|
+
ok: true,
|
|
220
|
+
detail: `no secrets yet at ${secretsPath} (generated on first up)`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const mode = statSync(secretsPath).mode & 0o777;
|
|
225
|
+
let parsed;
|
|
226
|
+
let parseError;
|
|
227
|
+
try {
|
|
228
|
+
parsed = readSecrets(home);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
parseError = err instanceof Error ? err.message : String(err);
|
|
232
|
+
}
|
|
233
|
+
if (parseError) {
|
|
234
|
+
checks.push({
|
|
235
|
+
id: "secrets",
|
|
236
|
+
ok: false,
|
|
237
|
+
detail: `${secretsPath} is unreadable: ${parseError}`,
|
|
238
|
+
fix: `fix or delete ${secretsPath} (a new one is generated on the next up)`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const keys = Object.keys(parsed ?? {});
|
|
243
|
+
checks.push({
|
|
244
|
+
id: "secrets",
|
|
245
|
+
ok: mode === 0o600,
|
|
246
|
+
detail: `${secretsPath} holds ${keys.join(", ") || "nothing"} (mode ${mode.toString(8)})`,
|
|
247
|
+
fix: mode === 0o600 ? undefined : `chmod 600 ${secretsPath}`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// 8. data directory: the backend writes its DB, workspaces and workflows here.
|
|
252
|
+
const dataDir = paths.data;
|
|
253
|
+
if (!existsSync(dataDir)) {
|
|
254
|
+
checks.push({
|
|
255
|
+
id: "data-dir",
|
|
256
|
+
ok: true,
|
|
257
|
+
detail: `${dataDir} does not exist yet (created on first up)`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
let writable = true;
|
|
262
|
+
const probe = join(dataDir, ".doctor-probe");
|
|
263
|
+
try {
|
|
264
|
+
await writeFile(probe, "");
|
|
265
|
+
await rm(probe, { force: true });
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
writable = false;
|
|
269
|
+
}
|
|
270
|
+
let freeMb;
|
|
271
|
+
try {
|
|
272
|
+
const fs = statfsSync(dataDir);
|
|
273
|
+
freeMb = Math.round((Number(fs.bavail) * Number(fs.bsize)) / (1024 * 1024));
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
freeMb = -1;
|
|
277
|
+
}
|
|
278
|
+
const lowSpace = freeMb >= 0 && freeMb < 500;
|
|
279
|
+
checks.push({
|
|
280
|
+
id: "data-dir",
|
|
281
|
+
ok: writable && !lowSpace,
|
|
282
|
+
detail: writable
|
|
283
|
+
? `${dataDir} (free ${freeMb >= 0 ? `${freeMb}MB` : "unknown"})`
|
|
284
|
+
: `${dataDir} is not writable`,
|
|
285
|
+
fix: writable && !lowSpace ? undefined : `free space or fix ownership: ${dataDir}`,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// 9. ports: is our stack answering, or is something else sitting on them?
|
|
289
|
+
const backendUp = await backendAnswers("http://127.0.0.1:3000/health");
|
|
290
|
+
const webAnswers = await anyPortAnswers("http://127.0.0.1:3001/login");
|
|
291
|
+
const daemon = readDaemon(home);
|
|
292
|
+
const detached = daemon && isProcessAlive(daemon.pid) ? `detached, pid ${daemon.pid}` : "not detached";
|
|
293
|
+
if (backendUp) {
|
|
294
|
+
checks.push({
|
|
295
|
+
id: "ports",
|
|
296
|
+
ok: true,
|
|
297
|
+
detail: `${detached}; our backend answers on 3000${webAnswers ? " and 3001 answers too" : ""}`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
else if (webAnswers) {
|
|
301
|
+
checks.push({
|
|
302
|
+
id: "ports",
|
|
303
|
+
ok: false,
|
|
304
|
+
detail: `something answers on 3001 but 3000 is not our backend (${detached})`,
|
|
305
|
+
fix: "stop whatever holds 3001, or start the gateway with different ports",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
checks.push({ id: "ports", ok: true, detail: `${detached}; ports quiet` });
|
|
310
|
+
}
|
|
311
|
+
// 10. the last start's log: migration failures are the classic first-boot
|
|
312
|
+
// death and they only live here.
|
|
313
|
+
const logPath = daemonPaths(home).log;
|
|
314
|
+
if (existsSync(logPath)) {
|
|
315
|
+
const tail = readFileSync(logPath, "utf8").split("\n").slice(-200);
|
|
316
|
+
const failure = tail.find((line) => line.includes("_journal") ||
|
|
317
|
+
/\bmigrate\b/i.test(line) ||
|
|
318
|
+
line.includes("SQLITE_") ||
|
|
319
|
+
line.includes("no such table"));
|
|
320
|
+
checks.push({
|
|
321
|
+
id: "last-start",
|
|
322
|
+
ok: failure === undefined,
|
|
323
|
+
detail: failure === undefined
|
|
324
|
+
? `${logPath}: no migration or SQLite errors in the last start`
|
|
325
|
+
: `${logPath} shows: ${failure.trim().slice(0, 120)}`,
|
|
326
|
+
fix: failure === undefined ? undefined : `read ${logPath} for the full start log`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
checks.push({ id: "last-start", ok: true, detail: "no detached start log yet" });
|
|
331
|
+
}
|
|
332
|
+
// 11. exposure: the artifact decides what the components bind.
|
|
333
|
+
if (manifest) {
|
|
334
|
+
const hosts = [];
|
|
335
|
+
for (const component of manifest.components) {
|
|
336
|
+
const host = component.env.HOSTNAME ?? component.env.BACKEND_HOST;
|
|
337
|
+
hosts.push(host ?? "unset(0.0.0.0)");
|
|
338
|
+
}
|
|
339
|
+
const exposed = hosts.filter((host) => !LOOPBACK_HOSTS.includes(host));
|
|
340
|
+
const tokenIsDefault = (() => {
|
|
341
|
+
try {
|
|
342
|
+
const token = readSecrets(home).BACKEND_AUTH_TOKEN;
|
|
343
|
+
return token === undefined || token === "dev-token";
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
})();
|
|
349
|
+
if (exposed.length === 0) {
|
|
350
|
+
checks.push({ id: "exposure", ok: true, detail: "all components bind loopback" });
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
checks.push({
|
|
354
|
+
id: "exposure",
|
|
355
|
+
ok: !tokenIsDefault,
|
|
356
|
+
detail: tokenIsDefault
|
|
357
|
+
? `components bind non-loopback (${exposed.join(", ")}) while the auth token is missing or the documented default`
|
|
358
|
+
: `components bind non-loopback (${exposed.join(", ")})`,
|
|
359
|
+
fix: tokenIsDefault
|
|
360
|
+
? "set a random token before exposing this gateway, and see docs/architecture/security"
|
|
361
|
+
: undefined,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// 12. which password wins: the console's hash or the launcher's secret.
|
|
366
|
+
if (backendUp) {
|
|
367
|
+
try {
|
|
368
|
+
const token = readSecrets(home).BACKEND_AUTH_TOKEN ?? "";
|
|
369
|
+
const res = await fetch("http://127.0.0.1:3000/api/auth/password", {
|
|
370
|
+
headers: { "x-auth-token": token },
|
|
371
|
+
signal: AbortSignal.timeout(2000),
|
|
372
|
+
});
|
|
373
|
+
const body = await res.json();
|
|
374
|
+
const configured = typeof body === "object" && body !== null && "configured" in body
|
|
375
|
+
? body.configured === true
|
|
376
|
+
: undefined;
|
|
377
|
+
if (configured !== undefined) {
|
|
378
|
+
checks.push({
|
|
379
|
+
id: "password",
|
|
380
|
+
ok: true,
|
|
381
|
+
detail: configured
|
|
382
|
+
? "a console-set password exists (it wins over the launcher's)"
|
|
383
|
+
: "no console-set password: the launcher's secret is the one that logs in",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// The backend answered /health but not this route (older artifact).
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// 13. the binary the backend spawns per Run
|
|
392
|
+
try {
|
|
393
|
+
const entry = resolveOmaBin();
|
|
394
|
+
checks.push({ id: "oma-bin", ok: true, detail: `runs will spawn ${entry}` });
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
checks.push({
|
|
398
|
+
id: "oma-bin",
|
|
399
|
+
ok: false,
|
|
400
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
401
|
+
fix: "install oma (npm/bun) or run 'bun run build' in the repo",
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return checks;
|
|
405
|
+
}
|
|
@@ -365,6 +365,7 @@ export declare const executeCommandSchema: z.ZodObject<{
|
|
|
365
365
|
}>;
|
|
366
366
|
}, "strip", z.ZodTypeAny, {
|
|
367
367
|
type: "execute";
|
|
368
|
+
id: string;
|
|
368
369
|
input: {
|
|
369
370
|
run: {
|
|
370
371
|
model: {
|
|
@@ -399,9 +400,9 @@ export declare const executeCommandSchema: z.ZodObject<{
|
|
|
399
400
|
args?: unknown;
|
|
400
401
|
} | undefined;
|
|
401
402
|
};
|
|
402
|
-
id: string;
|
|
403
403
|
}, {
|
|
404
404
|
type: "execute";
|
|
405
|
+
id: string;
|
|
405
406
|
input: {
|
|
406
407
|
run: {
|
|
407
408
|
model: {
|
|
@@ -436,7 +437,6 @@ export declare const executeCommandSchema: z.ZodObject<{
|
|
|
436
437
|
args?: unknown;
|
|
437
438
|
} | undefined;
|
|
438
439
|
};
|
|
439
|
-
id: string;
|
|
440
440
|
}>;
|
|
441
441
|
export type ExecuteCommand = z.infer<typeof executeCommandSchema>;
|
|
442
442
|
export declare const steerCommandSchema: z.ZodObject<{
|
|
@@ -458,21 +458,21 @@ export declare const steerCommandSchema: z.ZodObject<{
|
|
|
458
458
|
}>;
|
|
459
459
|
}, "strip", z.ZodTypeAny, {
|
|
460
460
|
type: "steer";
|
|
461
|
+
id: string;
|
|
461
462
|
input: {
|
|
462
463
|
message: Record<string, unknown>;
|
|
463
464
|
inputId: string;
|
|
464
465
|
productEntryId?: string | undefined;
|
|
465
466
|
};
|
|
466
|
-
id: string;
|
|
467
467
|
runId: string;
|
|
468
468
|
}, {
|
|
469
469
|
type: "steer";
|
|
470
|
+
id: string;
|
|
470
471
|
input: {
|
|
471
472
|
message: Record<string, unknown>;
|
|
472
473
|
inputId: string;
|
|
473
474
|
productEntryId?: string | undefined;
|
|
474
475
|
};
|
|
475
|
-
id: string;
|
|
476
476
|
runId: string;
|
|
477
477
|
}>;
|
|
478
478
|
export type SteerCommand = z.infer<typeof steerCommandSchema>;
|
|
@@ -498,14 +498,14 @@ export declare const resolveApprovalCommandSchema: z.ZodObject<{
|
|
|
498
498
|
decision: z.ZodEnum<["allow", "deny"]>;
|
|
499
499
|
}, "strip", z.ZodTypeAny, {
|
|
500
500
|
type: "resolve_approval";
|
|
501
|
-
decision: "deny" | "allow";
|
|
502
501
|
id: string;
|
|
502
|
+
decision: "deny" | "allow";
|
|
503
503
|
callId: string;
|
|
504
504
|
runId: string;
|
|
505
505
|
}, {
|
|
506
506
|
type: "resolve_approval";
|
|
507
|
-
decision: "deny" | "allow";
|
|
508
507
|
id: string;
|
|
508
|
+
decision: "deny" | "allow";
|
|
509
509
|
callId: string;
|
|
510
510
|
runId: string;
|
|
511
511
|
}>;
|
|
@@ -682,6 +682,7 @@ export declare const codingAgentCommandSchema: z.ZodDiscriminatedUnion<"type", [
|
|
|
682
682
|
}>;
|
|
683
683
|
}, "strip", z.ZodTypeAny, {
|
|
684
684
|
type: "execute";
|
|
685
|
+
id: string;
|
|
685
686
|
input: {
|
|
686
687
|
run: {
|
|
687
688
|
model: {
|
|
@@ -716,9 +717,9 @@ export declare const codingAgentCommandSchema: z.ZodDiscriminatedUnion<"type", [
|
|
|
716
717
|
args?: unknown;
|
|
717
718
|
} | undefined;
|
|
718
719
|
};
|
|
719
|
-
id: string;
|
|
720
720
|
}, {
|
|
721
721
|
type: "execute";
|
|
722
|
+
id: string;
|
|
722
723
|
input: {
|
|
723
724
|
run: {
|
|
724
725
|
model: {
|
|
@@ -753,7 +754,6 @@ export declare const codingAgentCommandSchema: z.ZodDiscriminatedUnion<"type", [
|
|
|
753
754
|
args?: unknown;
|
|
754
755
|
} | undefined;
|
|
755
756
|
};
|
|
756
|
-
id: string;
|
|
757
757
|
}>, z.ZodObject<{
|
|
758
758
|
id: z.ZodString;
|
|
759
759
|
type: z.ZodLiteral<"steer">;
|
|
@@ -773,21 +773,21 @@ export declare const codingAgentCommandSchema: z.ZodDiscriminatedUnion<"type", [
|
|
|
773
773
|
}>;
|
|
774
774
|
}, "strip", z.ZodTypeAny, {
|
|
775
775
|
type: "steer";
|
|
776
|
+
id: string;
|
|
776
777
|
input: {
|
|
777
778
|
message: Record<string, unknown>;
|
|
778
779
|
inputId: string;
|
|
779
780
|
productEntryId?: string | undefined;
|
|
780
781
|
};
|
|
781
|
-
id: string;
|
|
782
782
|
runId: string;
|
|
783
783
|
}, {
|
|
784
784
|
type: "steer";
|
|
785
|
+
id: string;
|
|
785
786
|
input: {
|
|
786
787
|
message: Record<string, unknown>;
|
|
787
788
|
inputId: string;
|
|
788
789
|
productEntryId?: string | undefined;
|
|
789
790
|
};
|
|
790
|
-
id: string;
|
|
791
791
|
runId: string;
|
|
792
792
|
}>, z.ZodObject<{
|
|
793
793
|
id: z.ZodString;
|
|
@@ -809,14 +809,14 @@ export declare const codingAgentCommandSchema: z.ZodDiscriminatedUnion<"type", [
|
|
|
809
809
|
decision: z.ZodEnum<["allow", "deny"]>;
|
|
810
810
|
}, "strip", z.ZodTypeAny, {
|
|
811
811
|
type: "resolve_approval";
|
|
812
|
-
decision: "deny" | "allow";
|
|
813
812
|
id: string;
|
|
813
|
+
decision: "deny" | "allow";
|
|
814
814
|
callId: string;
|
|
815
815
|
runId: string;
|
|
816
816
|
}, {
|
|
817
817
|
type: "resolve_approval";
|
|
818
|
-
decision: "deny" | "allow";
|
|
819
818
|
id: string;
|
|
819
|
+
decision: "deny" | "allow";
|
|
820
820
|
callId: string;
|
|
821
821
|
runId: string;
|
|
822
822
|
}>]>;
|
|
@@ -1358,7 +1358,6 @@ export declare const modelCatalogResponseSchema: z.ZodObject<{
|
|
|
1358
1358
|
};
|
|
1359
1359
|
}>, "many">;
|
|
1360
1360
|
}, "strip", z.ZodTypeAny, {
|
|
1361
|
-
backendKind: "omp" | "pi" | "oma" | "claude_code";
|
|
1362
1361
|
models: {
|
|
1363
1362
|
id: string;
|
|
1364
1363
|
reasoning: boolean;
|
|
@@ -1374,8 +1373,8 @@ export declare const modelCatalogResponseSchema: z.ZodObject<{
|
|
|
1374
1373
|
cacheWrite: number;
|
|
1375
1374
|
};
|
|
1376
1375
|
}[];
|
|
1377
|
-
}, {
|
|
1378
1376
|
backendKind: "omp" | "pi" | "oma" | "claude_code";
|
|
1377
|
+
}, {
|
|
1379
1378
|
models: {
|
|
1380
1379
|
id: string;
|
|
1381
1380
|
reasoning: boolean;
|
|
@@ -1391,6 +1390,7 @@ export declare const modelCatalogResponseSchema: z.ZodObject<{
|
|
|
1391
1390
|
cacheWrite: number;
|
|
1392
1391
|
};
|
|
1393
1392
|
}[];
|
|
1393
|
+
backendKind: "omp" | "pi" | "oma" | "claude_code";
|
|
1394
1394
|
}>;
|
|
1395
1395
|
export type ModelCatalogResponse = z.infer<typeof modelCatalogResponseSchema>;
|
|
1396
1396
|
//# sourceMappingURL=transport.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chengchenccc/oh-my-agent",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,23 +21,23 @@
|
|
|
21
21
|
"lint": "biome check . && eslint ."
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@chengchenccc/adapter-mcp": "^0.2.0-rc.
|
|
25
|
-
"@chengchenccc/agent-contract": "^0.2.0-rc.
|
|
26
|
-
"@chengchenccc/ai": "^0.2.0-rc.
|
|
27
|
-
"@chengchenccc/config": "^0.2.0-rc.
|
|
28
|
-
"@chengchenccc/message": "^0.2.0-rc.
|
|
29
|
-
"@chengchenccc/sandbox": "^0.2.0-rc.
|
|
30
|
-
"@chengchenccc/source-fetch": "^0.2.0-rc.
|
|
31
|
-
"@chengchenccc/tui": "^0.2.0-rc.
|
|
24
|
+
"@chengchenccc/adapter-mcp": "^0.2.0-rc.2",
|
|
25
|
+
"@chengchenccc/agent-contract": "^0.2.0-rc.2",
|
|
26
|
+
"@chengchenccc/ai": "^0.2.0-rc.2",
|
|
27
|
+
"@chengchenccc/config": "^0.2.0-rc.2",
|
|
28
|
+
"@chengchenccc/message": "^0.2.0-rc.2",
|
|
29
|
+
"@chengchenccc/sandbox": "^0.2.0-rc.2",
|
|
30
|
+
"@chengchenccc/source-fetch": "^0.2.0-rc.2",
|
|
31
|
+
"@chengchenccc/tui": "^0.2.0-rc.2",
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
33
33
|
"puppeteer-core": "25.10.0",
|
|
34
34
|
"zod": "^3.23.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@chengchenccc/test-helpers": "^0.2.0-rc.
|
|
37
|
+
"@chengchenccc/test-helpers": "^0.2.0-rc.2",
|
|
38
38
|
"bun-types": "latest",
|
|
39
39
|
"typescript": "^5.7.0",
|
|
40
|
-
"@chengchenccc/adapter-oma-agent": "^0.2.0-rc.
|
|
40
|
+
"@chengchenccc/adapter-oma-agent": "^0.2.0-rc.2"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"dist",
|