@omercnet/paseo-omp 0.2.1-next.72.1
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/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/SUPPORT.md +42 -0
- package/TESTING.md +150 -0
- package/client/composer-pill-settings.tsx +157 -0
- package/client/hub-icon.tsx +12 -0
- package/client/hub-popover.tsx +132 -0
- package/client/hub-status.ts +29 -0
- package/client/mcp-authorization.tsx +168 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +76 -0
- package/client/memory-popover.tsx +74 -0
- package/client/omp-config-surface.tsx +1433 -0
- package/client/omp-doc-links.ts +117 -0
- package/client/omp-plugin-manager.tsx +1004 -0
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/provider-diagnostics-state.ts +262 -0
- package/client/provider-icon.tsx +27 -0
- package/client/provider-image.tsx +66 -0
- package/client/quota-popover.tsx +155 -0
- package/client/quota-state.ts +140 -0
- package/client/sessions-popover.tsx +78 -0
- package/docs/alpha-release-checklist.md +68 -0
- package/docs/configuration.md +126 -0
- package/docs/core-provider-issue-audit.md +108 -0
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +67 -0
- package/index.client.tsx +488 -0
- package/index.server.ts +81 -0
- package/package.json +84 -0
- package/paseo-plugin.json +5 -0
- package/scripts/prepare-dependencies.mjs +20 -0
- package/server/hub.ts +145 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +86 -0
- package/server/mutation-queue.ts +12 -0
- package/server/omp-config.ts +135 -0
- package/server/omp-plugins.ts +676 -0
- package/server/omp-settings.ts +499 -0
- package/server/paths.ts +181 -0
- package/server/provider/catalog.ts +172 -0
- package/server/provider/config-normalization.ts +148 -0
- package/server/provider/connection.ts +1196 -0
- package/server/provider/host-tools.ts +777 -0
- package/server/provider/image.ts +143 -0
- package/server/provider/mcp-transport.ts +394 -0
- package/server/provider/omp-rpc.ts +2806 -0
- package/server/provider/omp.svg +5 -0
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/provider-options.ts +27 -0
- package/server/provider/registration.ts +162 -0
- package/server/provider/security.ts +317 -0
- package/server/provider/session-descriptors.ts +736 -0
- package/server/provider/session.ts +4796 -0
- package/server/provider/settings.ts +78 -0
- package/server/provider/subsessions.ts +850 -0
- package/server/provider/timeline-projector.ts +1801 -0
- package/server/provider-diagnostics.ts +1143 -0
- package/server/quota.ts +55 -0
- package/server/sessions.ts +58 -0
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/hub.ts +43 -0
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +24 -0
- package/shared/omp-config.ts +85 -0
- package/shared/omp-plugins.ts +264 -0
- package/shared/omp-settings.ts +214 -0
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +126 -0
- package/shared/provider-image.ts +160 -0
- package/shared/quota.ts +23 -0
- package/shared/sessions.ts +24 -0
- package/tsconfig.json +16 -0
package/server/hub.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { HubProcess, listHubProcesses, tailHubLog } from "../shared/hub";
|
|
7
|
+
|
|
8
|
+
const MAX_LOG_BYTES = 64 * 1024;
|
|
9
|
+
|
|
10
|
+
const ScopeFileSchema = z.object({ projectDir: z.string() });
|
|
11
|
+
const MetaFileSchema = z.object({
|
|
12
|
+
daemon: z.object({
|
|
13
|
+
state: z.string(),
|
|
14
|
+
owner: z.string().optional(),
|
|
15
|
+
restartCount: z.number().optional(),
|
|
16
|
+
persist: z.boolean().optional(),
|
|
17
|
+
detached: z.boolean().optional(),
|
|
18
|
+
createdAt: z.number().optional(),
|
|
19
|
+
startedAt: z.number().optional(),
|
|
20
|
+
readyAt: z.number().optional(),
|
|
21
|
+
exitedAt: z.number().optional(),
|
|
22
|
+
exitCode: z.number().optional(),
|
|
23
|
+
}),
|
|
24
|
+
spec: z.object({
|
|
25
|
+
application: z.string(),
|
|
26
|
+
args: z.array(z.string()).optional(),
|
|
27
|
+
cwd: z.string().optional(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* omp's hub keeps per-project process state at
|
|
33
|
+
* ~/.omp/run/daemons/<projectHash>/{scope.json, daemons/<name>/{meta.json,output.log}}.
|
|
34
|
+
* `scope.json.projectDir` matches a workspace's cwd exactly, so a project's hub processes can
|
|
35
|
+
* be resolved without any cooperation from the omp process itself.
|
|
36
|
+
*
|
|
37
|
+
* This is an internal, unversioned implementation detail of the omp harness: every read below
|
|
38
|
+
* is best-effort and degrades to an empty/partial result instead of throwing when a file is
|
|
39
|
+
* missing, unreadable, or shaped differently than expected (a future omp release is free to
|
|
40
|
+
* change or remove this layout).
|
|
41
|
+
*/
|
|
42
|
+
function ompRunDir(): string {
|
|
43
|
+
return process.env.PASEO_OMP_RUN_DIR ?? join(homedir(), ".omp", "run", "daemons");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readJsonFile(path: string): Promise<unknown | undefined> {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function listDirNames(path: string): Promise<string[]> {
|
|
55
|
+
try {
|
|
56
|
+
return await readdir(path);
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function findProjectDaemonRoots(root: string, cwd: string): Promise<string[]> {
|
|
63
|
+
const hashes = await listDirNames(root);
|
|
64
|
+
const matches: string[] = [];
|
|
65
|
+
await Promise.all(
|
|
66
|
+
hashes.map(async (hash) => {
|
|
67
|
+
const scope = ScopeFileSchema.safeParse(await readJsonFile(join(root, hash, "scope.json")));
|
|
68
|
+
if (scope.success && scope.data.projectDir === cwd) matches.push(join(root, hash));
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
return matches;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toHubProcess(name: string, value: unknown): HubProcess | undefined {
|
|
75
|
+
const parsed = MetaFileSchema.safeParse(value);
|
|
76
|
+
if (!parsed.success) return undefined;
|
|
77
|
+
const { daemon, spec } = parsed.data;
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
application: spec.application,
|
|
81
|
+
args: spec.args ?? [],
|
|
82
|
+
cwd: spec.cwd ?? "",
|
|
83
|
+
state: daemon.state,
|
|
84
|
+
owner: daemon.owner ?? null,
|
|
85
|
+
restartCount: daemon.restartCount ?? 0,
|
|
86
|
+
persist: daemon.persist ?? false,
|
|
87
|
+
detached: daemon.detached ?? false,
|
|
88
|
+
createdAt: daemon.createdAt ?? null,
|
|
89
|
+
startedAt: daemon.startedAt ?? null,
|
|
90
|
+
readyAt: daemon.readyAt ?? null,
|
|
91
|
+
exitedAt: daemon.exitedAt ?? null,
|
|
92
|
+
exitCode: daemon.exitCode ?? null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function listHubProcessesFrom(root: string, cwd: string): Promise<HubProcess[]> {
|
|
97
|
+
const roots = await findProjectDaemonRoots(root, cwd);
|
|
98
|
+
const processes: HubProcess[] = [];
|
|
99
|
+
await Promise.all(
|
|
100
|
+
roots.map(async (projectRoot) => {
|
|
101
|
+
const names = await listDirNames(join(projectRoot, "daemons"));
|
|
102
|
+
await Promise.all(
|
|
103
|
+
names.map(async (name) => {
|
|
104
|
+
const meta = await readJsonFile(join(projectRoot, "daemons", name, "meta.json"));
|
|
105
|
+
const process = toHubProcess(name, meta);
|
|
106
|
+
if (process) processes.push(process);
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
processes.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
|
|
112
|
+
return processes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function resolveListHubProcesses({
|
|
116
|
+
cwd,
|
|
117
|
+
}: RpcInput<typeof listHubProcesses>): Promise<{ processes: HubProcess[] }> {
|
|
118
|
+
return { processes: await listHubProcessesFrom(ompRunDir(), cwd) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function tailHubLogFrom(
|
|
122
|
+
root: string,
|
|
123
|
+
cwd: string,
|
|
124
|
+
name: string,
|
|
125
|
+
): Promise<{ content: string; truncated: boolean }> {
|
|
126
|
+
const roots = await findProjectDaemonRoots(root, cwd);
|
|
127
|
+
for (const projectRoot of roots) {
|
|
128
|
+
try {
|
|
129
|
+
const buffer = await readFile(join(projectRoot, "daemons", name, "output.log"));
|
|
130
|
+
const truncated = buffer.byteLength > MAX_LOG_BYTES;
|
|
131
|
+
const slice = truncated ? buffer.subarray(buffer.byteLength - MAX_LOG_BYTES) : buffer;
|
|
132
|
+
return { content: slice.toString("utf8"), truncated };
|
|
133
|
+
} catch {
|
|
134
|
+
// This root doesn't have that process (or its log vanished); try the next match.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { content: "", truncated: false };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function resolveTailHubLog({
|
|
141
|
+
cwd,
|
|
142
|
+
name,
|
|
143
|
+
}: RpcInput<typeof tailHubLog>): Promise<{ content: string; truncated: boolean }> {
|
|
144
|
+
return tailHubLogFrom(ompRunDir(), cwd, name);
|
|
145
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
3
|
+
import type { openOmpMcpAuthorizationInPaseoBrowser } from "../shared/mcp";
|
|
4
|
+
import { OmpPublicError } from "./provider/security";
|
|
5
|
+
|
|
6
|
+
const MAX_REGISTERED_SESSIONS = 32;
|
|
7
|
+
const MAX_AUTHORIZATIONS_PER_SESSION = 16;
|
|
8
|
+
|
|
9
|
+
type BrowserAuthorizationOpener = (url: string) => Promise<void>;
|
|
10
|
+
|
|
11
|
+
type RegisteredSession = {
|
|
12
|
+
token: symbol;
|
|
13
|
+
open: BrowserAuthorizationOpener;
|
|
14
|
+
authorizations: Set<string>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type RegisteredAuthorization = {
|
|
18
|
+
agentId: string;
|
|
19
|
+
sessionToken: symbol;
|
|
20
|
+
url: string;
|
|
21
|
+
open: BrowserAuthorizationOpener;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface OmpBrowserAuthorizationRegistration {
|
|
25
|
+
issue(url: string): string | undefined;
|
|
26
|
+
remove(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class OmpBrowserAuthorizationRegistry {
|
|
30
|
+
private readonly sessions = new Map<string, RegisteredSession>();
|
|
31
|
+
private readonly authorizations = new Map<string, RegisteredAuthorization>();
|
|
32
|
+
|
|
33
|
+
register(agentId: string, open: BrowserAuthorizationOpener): OmpBrowserAuthorizationRegistration {
|
|
34
|
+
if (this.sessions.has(agentId)) {
|
|
35
|
+
throw new OmpPublicError("OMP browser authorization is already registered for this agent");
|
|
36
|
+
}
|
|
37
|
+
if (this.sessions.size >= MAX_REGISTERED_SESSIONS) {
|
|
38
|
+
throw new OmpPublicError("OMP browser authorization session limit reached");
|
|
39
|
+
}
|
|
40
|
+
const session: RegisteredSession = { token: Symbol(agentId), open, authorizations: new Set() };
|
|
41
|
+
this.sessions.set(agentId, session);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
issue: (url) => {
|
|
45
|
+
if (this.sessions.get(agentId) !== session) return;
|
|
46
|
+
if (session.authorizations.size >= MAX_AUTHORIZATIONS_PER_SESSION) {
|
|
47
|
+
const oldest = session.authorizations.values().next().value;
|
|
48
|
+
if (oldest) {
|
|
49
|
+
session.authorizations.delete(oldest);
|
|
50
|
+
this.authorizations.delete(oldest);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const authorizationToken = randomUUID();
|
|
54
|
+
session.authorizations.add(authorizationToken);
|
|
55
|
+
this.authorizations.set(authorizationToken, {
|
|
56
|
+
agentId,
|
|
57
|
+
sessionToken: session.token,
|
|
58
|
+
url,
|
|
59
|
+
open,
|
|
60
|
+
});
|
|
61
|
+
return authorizationToken;
|
|
62
|
+
},
|
|
63
|
+
remove: () => {
|
|
64
|
+
if (this.sessions.get(agentId) !== session) return;
|
|
65
|
+
this.sessions.delete(agentId);
|
|
66
|
+
for (const authorizationToken of session.authorizations) {
|
|
67
|
+
this.authorizations.delete(authorizationToken);
|
|
68
|
+
}
|
|
69
|
+
session.authorizations.clear();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async open(authorizationToken: string): Promise<void> {
|
|
75
|
+
const authorization = this.authorizations.get(authorizationToken);
|
|
76
|
+
const session = authorization ? this.sessions.get(authorization.agentId) : undefined;
|
|
77
|
+
if (!authorization || session?.token !== authorization.sessionToken) {
|
|
78
|
+
throw new OmpPublicError("The OMP browser authorization is no longer available");
|
|
79
|
+
}
|
|
80
|
+
await authorization.open(authorization.url);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
clear(): void {
|
|
84
|
+
this.authorizations.clear();
|
|
85
|
+
this.sessions.clear();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function resolveOpenOmpMcpAuthorizationInPaseoBrowser(
|
|
90
|
+
input: RpcInput<typeof openOmpMcpAuthorizationInPaseoBrowser>,
|
|
91
|
+
registry: OmpBrowserAuthorizationRegistry,
|
|
92
|
+
): Promise<{ opened: true }> {
|
|
93
|
+
await registry.open(input.authorizationToken);
|
|
94
|
+
return { opened: true };
|
|
95
|
+
}
|
package/server/memory.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { listOmpMemory, OmpMemoryFact } from "../shared/memory";
|
|
7
|
+
import { ompStateDir } from "./paths";
|
|
8
|
+
|
|
9
|
+
const FactRowSchema = z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
subject: z.string(),
|
|
12
|
+
predicate: z.string(),
|
|
13
|
+
object: z.string(),
|
|
14
|
+
confidence: z.number().min(0).max(1).nullable(),
|
|
15
|
+
timestamp: z.string().nullable(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
type CandidateBank = { name: string; modifiedAt: number };
|
|
19
|
+
|
|
20
|
+
async function newestBank(root: string, cwd: string): Promise<string | undefined> {
|
|
21
|
+
const prefix = `${basename(cwd)}-`;
|
|
22
|
+
try {
|
|
23
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
24
|
+
const candidates = await Promise.all(
|
|
25
|
+
entries
|
|
26
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix))
|
|
27
|
+
.map(async (entry): Promise<CandidateBank | undefined> => {
|
|
28
|
+
try {
|
|
29
|
+
return { name: entry.name, modifiedAt: (await stat(join(root, entry.name))).mtimeMs };
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
return candidates
|
|
36
|
+
.flatMap((candidate) => (candidate ? [candidate] : []))
|
|
37
|
+
.sort((a, b) => b.modifiedAt - a.modifiedAt)[0]?.name;
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function listOmpFactsFrom(path: string): OmpMemoryFact[] {
|
|
44
|
+
try {
|
|
45
|
+
const database = new DatabaseSync(path, { readOnly: true, timeout: 500 });
|
|
46
|
+
try {
|
|
47
|
+
const rows = database
|
|
48
|
+
.prepare(
|
|
49
|
+
`SELECT fact_id AS id, subject, predicate, object, confidence, timestamp
|
|
50
|
+
FROM facts
|
|
51
|
+
ORDER BY COALESCE(timestamp, created_at) DESC
|
|
52
|
+
LIMIT 100`,
|
|
53
|
+
)
|
|
54
|
+
.all();
|
|
55
|
+
return rows.flatMap((row) => {
|
|
56
|
+
const parsed = FactRowSchema.safeParse(row);
|
|
57
|
+
if (!parsed.success) return [];
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
...parsed.data,
|
|
61
|
+
confidence: parsed.data.confidence ?? 1,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
});
|
|
65
|
+
} finally {
|
|
66
|
+
database.close();
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function listOmpMemoryFrom(
|
|
74
|
+
root: string,
|
|
75
|
+
cwd: string,
|
|
76
|
+
): Promise<{ bank: string | null; facts: OmpMemoryFact[] }> {
|
|
77
|
+
const bank = await newestBank(root, cwd);
|
|
78
|
+
if (!bank) return { bank: null, facts: [] };
|
|
79
|
+
return { bank, facts: listOmpFactsFrom(join(root, bank, "mnemopi.db")) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function resolveListOmpMemory({
|
|
83
|
+
cwd,
|
|
84
|
+
}: RpcInput<typeof listOmpMemory>): Promise<{ bank: string | null; facts: OmpMemoryFact[] }> {
|
|
85
|
+
return listOmpMemoryFrom(join(ompStateDir(), "memories", "mnemopi", "banks"), cwd);
|
|
86
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class SerialMutationQueue {
|
|
2
|
+
private tail: Promise<void> = Promise.resolve();
|
|
3
|
+
|
|
4
|
+
run<T>(operation: () => Promise<T>): Promise<T> {
|
|
5
|
+
const result = this.tail.then(operation, operation);
|
|
6
|
+
this.tail = result.then(
|
|
7
|
+
() => undefined,
|
|
8
|
+
() => undefined,
|
|
9
|
+
);
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
import type { ZodType } from "zod";
|
|
6
|
+
import {
|
|
7
|
+
type listOmpConfig,
|
|
8
|
+
type OmpConfig,
|
|
9
|
+
OmpConfigSchema,
|
|
10
|
+
OmpDevSectionSchema,
|
|
11
|
+
OmpGithubSectionSchema,
|
|
12
|
+
OmpMemorySectionSchema,
|
|
13
|
+
OmpModelRolesSchema,
|
|
14
|
+
OmpRetrySectionSchema,
|
|
15
|
+
OmpThemeSectionSchema,
|
|
16
|
+
} from "../shared/omp-config";
|
|
17
|
+
import { ompAgentDir } from "./paths";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* omp writes its global settings to `~/.omp/agent/config.yml`, falling back to legacy
|
|
21
|
+
* `config.yaml` (mirrors omp's `MAIN_CONFIG_FILENAMES`, canonical filename first). This is an
|
|
22
|
+
* internal, unversioned file: reading it here is best-effort and degrades to "unavailable"
|
|
23
|
+
* instead of throwing when the file is missing, unreadable, or not a YAML mapping.
|
|
24
|
+
*/
|
|
25
|
+
const CONFIG_FILENAMES = ["config.yml", "config.yaml"] as const;
|
|
26
|
+
|
|
27
|
+
type OmpConfigResult = { path: string; available: boolean; config: OmpConfig | null };
|
|
28
|
+
|
|
29
|
+
async function readFirstExisting(
|
|
30
|
+
dir: string,
|
|
31
|
+
filenames: readonly string[] = CONFIG_FILENAMES,
|
|
32
|
+
): Promise<{ path: string; text: string } | undefined> {
|
|
33
|
+
for (const filename of filenames) {
|
|
34
|
+
const path = join(dir, filename);
|
|
35
|
+
try {
|
|
36
|
+
return { path, text: await readFile(path, "utf8") };
|
|
37
|
+
} catch {
|
|
38
|
+
// Try the next candidate filename.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Validates one top-level section independently so an unrelated malformed field never hides the rest. */
|
|
45
|
+
function section<Schema extends ZodType>(
|
|
46
|
+
schema: Schema,
|
|
47
|
+
value: unknown,
|
|
48
|
+
): Schema["_output"] | undefined {
|
|
49
|
+
if (value === undefined) return undefined;
|
|
50
|
+
const result = schema.safeParse(value);
|
|
51
|
+
return result.success ? result.data : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Maps a parsed YAML document onto the safe allowlist in `shared/omp-config.ts`. Every field is
|
|
56
|
+
* read explicitly by name; nothing on the source object is spread, stringified, or forwarded
|
|
57
|
+
* unchecked, so an unrecognized or credential-shaped key never reaches the caller.
|
|
58
|
+
*/
|
|
59
|
+
export function parseOmpConfig(raw: unknown): OmpConfig {
|
|
60
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
61
|
+
const record = raw as Record<string, unknown>;
|
|
62
|
+
const config: OmpConfig = {};
|
|
63
|
+
|
|
64
|
+
const setupVersion = section(OmpConfigSchema.shape.setupVersion, record.setupVersion);
|
|
65
|
+
if (setupVersion !== undefined) config.setupVersion = setupVersion;
|
|
66
|
+
|
|
67
|
+
const symbolPreset = section(OmpConfigSchema.shape.symbolPreset, record.symbolPreset);
|
|
68
|
+
if (symbolPreset !== undefined) config.symbolPreset = symbolPreset;
|
|
69
|
+
|
|
70
|
+
const defaultThinkingLevel = section(
|
|
71
|
+
OmpConfigSchema.shape.defaultThinkingLevel,
|
|
72
|
+
record.defaultThinkingLevel,
|
|
73
|
+
);
|
|
74
|
+
if (defaultThinkingLevel !== undefined) config.defaultThinkingLevel = defaultThinkingLevel;
|
|
75
|
+
|
|
76
|
+
const theme = section(OmpThemeSectionSchema, record.theme);
|
|
77
|
+
if (theme !== undefined) config.theme = theme;
|
|
78
|
+
|
|
79
|
+
const memory = section(OmpMemorySectionSchema, record.memory);
|
|
80
|
+
if (memory !== undefined) config.memory = memory;
|
|
81
|
+
|
|
82
|
+
const github = section(OmpGithubSectionSchema, record.github);
|
|
83
|
+
if (github !== undefined) config.github = github;
|
|
84
|
+
|
|
85
|
+
const disabledProviders = section(
|
|
86
|
+
OmpConfigSchema.shape.disabledProviders,
|
|
87
|
+
record.disabledProviders,
|
|
88
|
+
);
|
|
89
|
+
if (disabledProviders !== undefined) config.disabledProviders = disabledProviders;
|
|
90
|
+
|
|
91
|
+
const modelProviderOrder = section(
|
|
92
|
+
OmpConfigSchema.shape.modelProviderOrder,
|
|
93
|
+
record.modelProviderOrder,
|
|
94
|
+
);
|
|
95
|
+
if (modelProviderOrder !== undefined) config.modelProviderOrder = modelProviderOrder;
|
|
96
|
+
|
|
97
|
+
const modelRoles = section(OmpModelRolesSchema, record.modelRoles);
|
|
98
|
+
if (modelRoles !== undefined) config.modelRoles = modelRoles;
|
|
99
|
+
|
|
100
|
+
const enabledModels = section(OmpConfigSchema.shape.enabledModels, record.enabledModels);
|
|
101
|
+
if (enabledModels !== undefined) config.enabledModels = enabledModels;
|
|
102
|
+
|
|
103
|
+
const retry = section(OmpRetrySectionSchema, record.retry);
|
|
104
|
+
if (retry !== undefined) config.retry = retry;
|
|
105
|
+
|
|
106
|
+
const dev = section(OmpDevSectionSchema, record.dev);
|
|
107
|
+
if (dev !== undefined) config.dev = dev;
|
|
108
|
+
|
|
109
|
+
return config;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function readOmpConfigFrom(
|
|
113
|
+
dir: string,
|
|
114
|
+
filenames: readonly string[] = CONFIG_FILENAMES,
|
|
115
|
+
): Promise<OmpConfigResult> {
|
|
116
|
+
const defaultPath = join(dir, filenames[0] ?? CONFIG_FILENAMES[0]);
|
|
117
|
+
const found = await readFirstExisting(dir, filenames);
|
|
118
|
+
if (!found) return { path: defaultPath, available: false, config: null };
|
|
119
|
+
try {
|
|
120
|
+
const raw: unknown = parseYaml(found.text);
|
|
121
|
+
return { path: found.path, available: true, config: parseOmpConfig(raw) };
|
|
122
|
+
} catch {
|
|
123
|
+
// Malformed YAML: never log the raw text, degrade to a clear unavailable result.
|
|
124
|
+
return { path: found.path, available: false, config: null };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function resolveListOmpConfig(
|
|
129
|
+
input: RpcInput<typeof listOmpConfig>,
|
|
130
|
+
): Promise<OmpConfigResult> {
|
|
131
|
+
if (input.cwd && isAbsolute(input.cwd) && !input.cwd.includes("\0")) {
|
|
132
|
+
return readOmpConfigFrom(join(input.cwd, ".omp"), [CONFIG_FILENAMES[0]]);
|
|
133
|
+
}
|
|
134
|
+
return readOmpConfigFrom(ompAgentDir());
|
|
135
|
+
}
|