@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/SUPPORT.md +42 -0
  5. package/TESTING.md +150 -0
  6. package/client/composer-pill-settings.tsx +157 -0
  7. package/client/hub-icon.tsx +12 -0
  8. package/client/hub-popover.tsx +132 -0
  9. package/client/hub-status.ts +29 -0
  10. package/client/mcp-authorization.tsx +168 -0
  11. package/client/mcp-popover.tsx +155 -0
  12. package/client/memory-panel.tsx +76 -0
  13. package/client/memory-popover.tsx +74 -0
  14. package/client/omp-config-surface.tsx +1433 -0
  15. package/client/omp-doc-links.ts +117 -0
  16. package/client/omp-plugin-manager.tsx +1004 -0
  17. package/client/omp-store-picker.tsx +89 -0
  18. package/client/omp-store-state.ts +45 -0
  19. package/client/provider-diagnostics-state.ts +262 -0
  20. package/client/provider-icon.tsx +27 -0
  21. package/client/provider-image.tsx +66 -0
  22. package/client/quota-popover.tsx +155 -0
  23. package/client/quota-state.ts +140 -0
  24. package/client/sessions-popover.tsx +78 -0
  25. package/docs/alpha-release-checklist.md +68 -0
  26. package/docs/configuration.md +126 -0
  27. package/docs/core-provider-issue-audit.md +108 -0
  28. package/docs/images/mcp-authorization-compact.png +0 -0
  29. package/docs/images/mcp-controls-wide.png +0 -0
  30. package/docs/images/plugin-manager.png +0 -0
  31. package/docs/images/workspace-settings.png +0 -0
  32. package/docs/installation.md +67 -0
  33. package/index.client.tsx +488 -0
  34. package/index.server.ts +81 -0
  35. package/package.json +84 -0
  36. package/paseo-plugin.json +5 -0
  37. package/scripts/prepare-dependencies.mjs +20 -0
  38. package/server/hub.ts +145 -0
  39. package/server/mcp-browser.ts +95 -0
  40. package/server/memory.ts +86 -0
  41. package/server/mutation-queue.ts +12 -0
  42. package/server/omp-config.ts +135 -0
  43. package/server/omp-plugins.ts +676 -0
  44. package/server/omp-settings.ts +499 -0
  45. package/server/paths.ts +181 -0
  46. package/server/provider/catalog.ts +172 -0
  47. package/server/provider/config-normalization.ts +148 -0
  48. package/server/provider/connection.ts +1196 -0
  49. package/server/provider/host-tools.ts +777 -0
  50. package/server/provider/image.ts +143 -0
  51. package/server/provider/mcp-transport.ts +394 -0
  52. package/server/provider/omp-rpc.ts +2806 -0
  53. package/server/provider/omp.svg +5 -0
  54. package/server/provider/profile-providers.ts +249 -0
  55. package/server/provider/provider-options.ts +27 -0
  56. package/server/provider/registration.ts +162 -0
  57. package/server/provider/security.ts +317 -0
  58. package/server/provider/session-descriptors.ts +736 -0
  59. package/server/provider/session.ts +4796 -0
  60. package/server/provider/settings.ts +78 -0
  61. package/server/provider/subsessions.ts +850 -0
  62. package/server/provider/timeline-projector.ts +1801 -0
  63. package/server/provider-diagnostics.ts +1143 -0
  64. package/server/quota.ts +55 -0
  65. package/server/sessions.ts +58 -0
  66. package/shared/composer-pill-settings.ts +28 -0
  67. package/shared/hub.ts +43 -0
  68. package/shared/mcp.ts +47 -0
  69. package/shared/memory.ts +24 -0
  70. package/shared/omp-config.ts +85 -0
  71. package/shared/omp-plugins.ts +264 -0
  72. package/shared/omp-settings.ts +214 -0
  73. package/shared/omp-store.ts +58 -0
  74. package/shared/provider-diagnostics.ts +126 -0
  75. package/shared/provider-image.ts +160 -0
  76. package/shared/quota.ts +23 -0
  77. package/shared/sessions.ts +24 -0
  78. 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
+ }
@@ -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
+ }