@omercnet/paseo-omp 0.2.1 → 0.3.0-next.100.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 (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +25 -13
  3. package/SUPPORT.md +7 -3
  4. package/TESTING.md +21 -18
  5. package/client/composer-pill-settings.tsx +157 -0
  6. package/client/external-url.ts +15 -0
  7. package/client/mcp-authorization.tsx +169 -0
  8. package/client/mcp-popover.tsx +155 -0
  9. package/client/memory-panel.tsx +8 -3
  10. package/client/memory-popover.tsx +8 -4
  11. package/client/omp-config-surface.tsx +189 -29
  12. package/client/omp-plugin-manager.tsx +302 -131
  13. package/client/omp-store-picker.tsx +89 -0
  14. package/client/omp-store-state.ts +45 -0
  15. package/client/paseo-types.ts +9 -0
  16. package/client/provider-diagnostics-state.ts +18 -7
  17. package/client/quota-popover.tsx +8 -3
  18. package/client/quota-state.ts +16 -7
  19. package/client/sessions-popover.tsx +8 -3
  20. package/docs/alpha-release-checklist.md +6 -8
  21. package/docs/configuration.md +8 -4
  22. package/docs/core-provider-issue-audit.md +3 -2
  23. package/docs/images/mcp-authorization-compact.png +0 -0
  24. package/docs/images/mcp-controls-wide.png +0 -0
  25. package/docs/images/plugin-manager.png +0 -0
  26. package/docs/images/workspace-settings.png +0 -0
  27. package/docs/installation.md +35 -19
  28. package/index.client.tsx +339 -123
  29. package/index.server.ts +44 -14
  30. package/package.json +7 -8
  31. package/paseo-plugin.json +2 -2
  32. package/scripts/prepare-dependencies.mjs +24 -0
  33. package/server/mcp-browser.ts +95 -0
  34. package/server/memory.ts +2 -2
  35. package/server/omp-config.ts +16 -7
  36. package/server/omp-plugins.ts +70 -21
  37. package/server/omp-settings.ts +232 -24
  38. package/server/paths.ts +128 -11
  39. package/server/provider/catalog.ts +3 -4
  40. package/server/provider/connection.ts +248 -17
  41. package/server/provider/host-tools.ts +294 -26
  42. package/server/provider/omp-rpc.ts +498 -72
  43. package/server/provider/profile-providers.ts +249 -0
  44. package/server/provider/registration.ts +11 -0
  45. package/server/provider/security.ts +8 -10
  46. package/server/provider/session-descriptors.ts +306 -1
  47. package/server/provider/session.ts +704 -249
  48. package/server/provider/subsessions.ts +25 -2
  49. package/server/provider/timeline-projector.ts +104 -44
  50. package/server/provider-diagnostics.ts +122 -36
  51. package/server/quota.ts +3 -2
  52. package/server/sessions.ts +2 -2
  53. package/shared/composer-pill-settings.ts +28 -0
  54. package/shared/external-url.ts +21 -0
  55. package/shared/hub.ts +3 -3
  56. package/shared/mcp.ts +47 -0
  57. package/shared/memory.ts +2 -1
  58. package/shared/omp-config.ts +5 -1
  59. package/shared/omp-plugins.ts +74 -33
  60. package/shared/omp-settings.ts +8 -1
  61. package/shared/omp-store.ts +58 -0
  62. package/shared/provider-diagnostics.ts +12 -3
  63. package/shared/quota.ts +2 -1
  64. package/shared/sessions.ts +2 -1
@@ -17,7 +17,7 @@ import type {
17
17
  OmpVersionStatus,
18
18
  PathState,
19
19
  } from "../shared/provider-diagnostics";
20
- import { ompAgentDir } from "./paths";
20
+ import { currentOmpEnvironment, ompAgentDir, ompDataDir, ompSessionDir } from "./paths";
21
21
 
22
22
  const VERSION_TIMEOUT_MS = 3_000;
23
23
  const HELP_TIMEOUT_MS = 3_000;
@@ -190,7 +190,11 @@ export async function killWindowsProcessTree(
190
190
 
191
191
  let child: ProbeChildProcess;
192
192
  try {
193
- child = spawnFn(taskkillPath, ["/pid", String(pid), "/t", "/f"], buildProbeEnv(process.env));
193
+ child = spawnFn(
194
+ taskkillPath,
195
+ ["/pid", String(pid), "/t", "/f"],
196
+ buildProbeEnv(currentOmpEnvironment()),
197
+ );
194
198
  } catch {
195
199
  return false;
196
200
  }
@@ -243,7 +247,7 @@ export function defaultSpawn(
243
247
  return killWindowsProcessTree(
244
248
  child.pid,
245
249
  defaultSpawn,
246
- process.env.SystemRoot ?? WINDOWS_DEFAULT_SYSTEM_ROOT,
250
+ currentOmpEnvironment().SystemRoot ?? WINDOWS_DEFAULT_SYSTEM_ROOT,
247
251
  graceMs,
248
252
  );
249
253
  }
@@ -569,7 +573,7 @@ export async function probeOmpAvailability(options: OmpAvailabilityProbeOptions)
569
573
  status: "missing" | "unrunnable" | "incompatible" | "available";
570
574
  diagnostic?: string;
571
575
  }> {
572
- const environment = options.environment ?? process.env;
576
+ const environment = options.environment ?? currentOmpEnvironment();
573
577
  const [command, ...prefixArgs] = options.command;
574
578
  const resolvedPath = await resolveExecutablePath(
575
579
  command,
@@ -810,31 +814,57 @@ async function computeMcpDiagnostics(agentDir: string): Promise<OmpMcpDiagnostic
810
814
  export interface ProcessDiagnosticsFs {
811
815
  readdir(path: string): Promise<string[]>;
812
816
  lstat(path: string): Promise<Stats>;
817
+ readMetadata(path: string, maxBytes: number): Promise<BoundedFileRead>;
813
818
  }
814
819
 
815
- const processDiagnosticsFs: ProcessDiagnosticsFs = { readdir, lstat };
820
+ const processDiagnosticsFs: ProcessDiagnosticsFs = {
821
+ readdir,
822
+ lstat,
823
+ readMetadata: readBoundedNoSymlinkFile,
824
+ };
825
+ const MAX_PROCESS_METADATA_BYTES = 64 * 1024;
826
+ const MAX_PROCESS_METADATA_ENTRIES = 4_096;
827
+ const ProcessStateSchema = z.object({
828
+ daemon: z.object({
829
+ state: z.string().max(32),
830
+ exitedAt: z.number().finite().nonnegative().nullish(),
831
+ }),
832
+ });
833
+ const ACTIVE_PROCESS_STATES = new Set(["starting", "running", "ready", "restarting"]);
834
+ const HISTORICAL_PROCESS_STATES = new Set(["exited", "failed", "stopped"]);
816
835
 
817
836
  /**
818
- * Counts only regular, non-symlink meta.json entries. Missing/wrong entries are expected and
819
- * ignored; any other read/stat failure makes the result partial rather than silently hiding it.
837
+ * Classifies bounded, regular, non-symlink Hub metadata. A recorded active state is not a
838
+ * liveness check. Historical files remain useful evidence and are never counted as running.
839
+ * Only aggregate counts leave this reader; metadata arguments, paths and values are discarded.
820
840
  */
821
841
  export async function computeProcessDiagnostics(
822
842
  hubRunRoot: string,
823
843
  fs: ProcessDiagnosticsFs = processDiagnosticsFs,
824
844
  ): Promise<OmpProcessDiagnostics> {
845
+ const unavailableCounts = {
846
+ trackedCount: null,
847
+ activeCount: null,
848
+ historicalCount: null,
849
+ unknownCount: null,
850
+ };
825
851
  let projectHashes: string[];
826
852
  try {
827
853
  const rootStats = await fs.lstat(hubRunRoot);
828
854
  if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
829
- return { status: "unavailable", trackedCount: null };
855
+ return { status: "unavailable", ...unavailableCounts };
830
856
  }
831
857
  projectHashes = await fs.readdir(hubRunRoot);
832
858
  } catch (error) {
833
- return { status: isEnoent(error) ? "unavailable" : "unknown", trackedCount: null };
859
+ return { status: isEnoent(error) ? "unavailable" : "unknown", ...unavailableCounts };
834
860
  }
835
861
  let trackedCount = 0;
836
- let partial = false;
837
- for (const hash of projectHashes) {
862
+ let activeCount = 0;
863
+ let historicalCount = 0;
864
+ let unknownCount = 0;
865
+ let partial = projectHashes.length > MAX_PROCESS_METADATA_ENTRIES;
866
+ let visitedEntries = 0;
867
+ for (const hash of projectHashes.slice(0, MAX_PROCESS_METADATA_ENTRIES)) {
838
868
  const projectDir = join(hubRunRoot, hash);
839
869
  const daemonsDir = join(projectDir, "daemons");
840
870
  try {
@@ -855,15 +885,51 @@ export async function computeProcessDiagnostics(
855
885
  continue;
856
886
  }
857
887
  for (const name of daemonNames) {
888
+ if (++visitedEntries > MAX_PROCESS_METADATA_ENTRIES) {
889
+ partial = true;
890
+ break;
891
+ }
858
892
  try {
859
- const metaStats = await fs.lstat(join(daemonsDir, name, "meta.json"));
860
- if (!metaStats.isSymbolicLink() && metaStats.isFile()) trackedCount += 1;
893
+ const daemonDir = join(daemonsDir, name);
894
+ const directoryStats = await fs.lstat(daemonDir);
895
+ if (directoryStats.isSymbolicLink() || !directoryStats.isDirectory()) continue;
896
+ const metaPath = join(daemonDir, "meta.json");
897
+ const metaStats = await fs.lstat(metaPath);
898
+ if (metaStats.isSymbolicLink() || !metaStats.isFile()) continue;
899
+ trackedCount += 1;
900
+ let category: "active" | "historical" | "unknown" = "unknown";
901
+ try {
902
+ const file = await fs.readMetadata(metaPath, MAX_PROCESS_METADATA_BYTES);
903
+ if (file.state === "available") {
904
+ const parsed = ProcessStateSchema.safeParse(JSON.parse(file.text));
905
+ if (parsed.success) {
906
+ const { state, exitedAt } = parsed.data.daemon;
907
+ if (HISTORICAL_PROCESS_STATES.has(state) || exitedAt != null) category = "historical";
908
+ else if (ACTIVE_PROCESS_STATES.has(state)) category = "active";
909
+ }
910
+ }
911
+ } catch {
912
+ // Read/access/parse errors have an honest unknown bucket, never a guessed active state.
913
+ }
914
+ if (category === "historical") historicalCount += 1;
915
+ else if (category === "active") activeCount += 1;
916
+ else {
917
+ unknownCount += 1;
918
+ partial = true;
919
+ }
861
920
  } catch (error) {
862
921
  if (!isEnoent(error)) partial = true;
863
922
  }
864
923
  }
924
+ if (visitedEntries > MAX_PROCESS_METADATA_ENTRIES) break;
865
925
  }
866
- return { status: partial ? "partial" : "ok", trackedCount };
926
+ return {
927
+ status: partial ? "partial" : "ok",
928
+ trackedCount,
929
+ activeCount,
930
+ historicalCount,
931
+ unknownCount,
932
+ };
867
933
  }
868
934
 
869
935
  function homeRelative(path: string, homeDir: string): string | null {
@@ -880,16 +946,23 @@ function sanitizeRootPath(path: string, homeDir: string): string {
880
946
  return homeRelative(path, homeDir) ?? "<custom path>";
881
947
  }
882
948
 
883
- function sanitizeDerivedPath(rawRoot: string, sanitizedRoot: string, fullPath: string): string {
949
+ function sanitizeDerivedPath(
950
+ rawRoot: string,
951
+ sanitizedRoot: string,
952
+ fullPath: string,
953
+ fallbackHomeDir?: string,
954
+ ): string {
884
955
  const suffix = relative(rawRoot, fullPath);
885
956
  if (suffix === "" || suffix === ".." || suffix.startsWith(`..${sep}`) || isAbsolute(suffix)) {
886
- return sanitizedRoot;
957
+ return fallbackHomeDir ? sanitizeRootPath(fullPath, fallbackHomeDir) : sanitizedRoot;
887
958
  }
888
959
  return `${sanitizedRoot}/${suffix.split(sep).join("/")}`;
889
960
  }
890
961
 
891
962
  export interface ProviderDiagnosticsDeps {
892
963
  agentDir: string;
964
+ dataDir?: string;
965
+ sessionDir?: string;
893
966
  command: string;
894
967
  pathDirs: readonly string[];
895
968
  cwd: string;
@@ -910,6 +983,8 @@ export async function computeOmpProviderHealth(
910
983
  deps: ProviderDiagnosticsDeps,
911
984
  ): Promise<OmpProviderHealth> {
912
985
  const agentDir = resolve(deps.agentDir);
986
+ const dataDir = resolve(deps.dataDir ?? agentDir);
987
+ const sessionRoot = resolve(deps.sessionDir ?? join(dataDir, SESSION_DIR_NAME));
913
988
  const homeDir = resolve(deps.homeDir);
914
989
  const versionTimeoutMs = deps.versionTimeoutMs ?? VERSION_TIMEOUT_MS;
915
990
  const helpTimeoutMs = deps.helpTimeoutMs ?? HELP_TIMEOUT_MS;
@@ -935,6 +1010,7 @@ export async function computeOmpProviderHealth(
935
1010
  versionTimeoutMs,
936
1011
  killGraceMs,
937
1012
  maxVersionBytes,
1013
+ deps.cwd,
938
1014
  )
939
1015
  : null,
940
1016
  installed
@@ -946,6 +1022,7 @@ export async function computeOmpProviderHealth(
946
1022
  helpTimeoutMs,
947
1023
  killGraceMs,
948
1024
  maxHelpBytes,
1025
+ deps.cwd,
949
1026
  )
950
1027
  : null,
951
1028
  ]);
@@ -957,9 +1034,8 @@ export async function computeOmpProviderHealth(
957
1034
  const lsp = helpRun ? computeLspDiagnostics(helpRun) : { status: "unknown" as const };
958
1035
  const processCleanupFailed = Boolean(versionRun?.cleanupFailed || helpRun?.cleanupFailed);
959
1036
 
960
- const agentDbPath = join(agentDir, AGENT_DB_FILENAME);
961
- const historyDbPath = join(agentDir, HISTORY_DB_FILENAME);
962
- const sessionRoot = join(agentDir, SESSION_DIR_NAME);
1037
+ const agentDbPath = join(dataDir, AGENT_DB_FILENAME);
1038
+ const historyDbPath = join(dataDir, HISTORY_DB_FILENAME);
963
1039
  const [
964
1040
  configResult,
965
1041
  agentRootState,
@@ -1000,7 +1076,7 @@ export async function computeOmpProviderHealth(
1000
1076
  agentRootState,
1001
1077
  configPath: sanitizeDerivedPath(agentDir, sanitizedAgentRoot, configResult.path),
1002
1078
  configState,
1003
- sessionRoot: sanitizeDerivedPath(agentDir, sanitizedAgentRoot, sessionRoot),
1079
+ sessionRoot: sanitizeDerivedPath(agentDir, sanitizedAgentRoot, sessionRoot, homeDir),
1004
1080
  sessionRootState,
1005
1081
  },
1006
1082
  databases: {
@@ -1013,11 +1089,11 @@ export async function computeOmpProviderHealth(
1013
1089
  };
1014
1090
  }
1015
1091
 
1016
- let cachedHealth: { value: OmpProviderHealth; expiresAt: number } | null = null;
1017
- let inFlightHealth: Promise<OmpProviderHealth> | null = null;
1092
+ const cachedHealth = new Map<string, { value: OmpProviderHealth; expiresAt: number }>();
1093
+ const inFlightHealth = new Map<string, Promise<OmpProviderHealth>>();
1018
1094
 
1019
- function defaultHubRunRoot(): string {
1020
- return process.env.PASEO_OMP_RUN_DIR ?? join(homedir(), ".omp", "run", "daemons");
1095
+ function defaultHubRunRoot(environment: NodeJS.ProcessEnv = currentOmpEnvironment()): string {
1096
+ return environment.PASEO_OMP_RUN_DIR ?? join(homedir(), ".omp", "run", "daemons");
1021
1097
  }
1022
1098
 
1023
1099
  /**
@@ -1029,29 +1105,39 @@ function defaultHubRunRoot(): string {
1029
1105
  export async function resolveGetOmpProviderHealth(
1030
1106
  input: RpcInput<typeof getOmpProviderHealth>,
1031
1107
  ): Promise<OmpProviderHealth> {
1108
+ const cwd = input.cwd ?? process.cwd();
1109
+ const environment = currentOmpEnvironment();
1110
+ const agentDir = ompAgentDir(environment);
1111
+ const dataDir = ompDataDir(environment);
1112
+ const sessionDir = ompSessionDir(environment);
1032
1113
  const now = Date.now();
1033
- if (!input.force && cachedHealth && cachedHealth.expiresAt > now) return cachedHealth.value;
1034
- if (inFlightHealth) return inFlightHealth;
1114
+ const key = JSON.stringify([cwd, agentDir, dataDir, sessionDir]);
1115
+ const cached = cachedHealth.get(key);
1116
+ if (!input.force && cached && cached.expiresAt > now) return cached.value;
1117
+ const inFlight = inFlightHealth.get(key);
1118
+ if (inFlight) return inFlight;
1035
1119
 
1036
1120
  const computation = computeOmpProviderHealth({
1037
- agentDir: ompAgentDir(),
1038
- command: process.env.OMP_COMMAND ?? "omp",
1039
- pathDirs: (process.env.PATH ?? "").split(delimiter),
1040
- cwd: process.cwd(),
1121
+ agentDir,
1122
+ dataDir,
1123
+ sessionDir,
1124
+ command: environment.OMP_COMMAND ?? "omp",
1125
+ pathDirs: (environment.PATH ?? "").split(delimiter),
1126
+ cwd,
1041
1127
  platform: process.platform,
1042
- pathExt: process.env.PATHEXT ?? WINDOWS_DEFAULT_PATHEXT,
1043
- env: process.env,
1128
+ pathExt: environment.PATHEXT ?? WINDOWS_DEFAULT_PATHEXT,
1129
+ env: environment,
1044
1130
  spawnFn: defaultSpawn,
1045
- hubRunRoot: defaultHubRunRoot(),
1131
+ hubRunRoot: defaultHubRunRoot(environment),
1046
1132
  homeDir: homedir(),
1047
1133
  })
1048
1134
  .then((value) => {
1049
- cachedHealth = { value, expiresAt: Date.now() + HEALTH_CACHE_TTL_MS };
1135
+ cachedHealth.set(key, { value, expiresAt: Date.now() + HEALTH_CACHE_TTL_MS });
1050
1136
  return value;
1051
1137
  })
1052
1138
  .finally(() => {
1053
- inFlightHealth = null;
1139
+ inFlightHealth.delete(key);
1054
1140
  });
1055
- inFlightHealth = computation;
1141
+ inFlightHealth.set(key, computation);
1056
1142
  return computation;
1057
1143
  }
package/server/quota.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { join } from "node:path";
1
2
  import { DatabaseSync } from "node:sqlite";
2
3
  import type { RpcInput } from "@getpaseo/plugin";
3
4
  import { z } from "zod";
4
5
  import type { listOmpQuotas, OmpQuota } from "../shared/quota";
5
- import { ompAgentDir } from "./paths";
6
+ import { ompDataDir } from "./paths";
6
7
 
7
8
  const QuotaRowSchema = z.object({
8
9
  provider: z.string(),
@@ -50,5 +51,5 @@ export function listOmpQuotasFrom(path: string): OmpQuota[] {
50
51
  export function resolveListOmpQuotas(_input: RpcInput<typeof listOmpQuotas>): {
51
52
  quotas: OmpQuota[];
52
53
  } {
53
- return { quotas: listOmpQuotasFrom(`${ompAgentDir()}/agent.db`) };
54
+ return { quotas: listOmpQuotasFrom(join(ompDataDir(), "agent.db")) };
54
55
  }
@@ -3,7 +3,7 @@ import { DatabaseSync } from "node:sqlite";
3
3
  import type { RpcInput } from "@getpaseo/plugin";
4
4
  import { z } from "zod";
5
5
  import type { listOmpSessions, OmpSessionEntry } from "../shared/sessions";
6
- import { ompAgentDir } from "./paths";
6
+ import { ompDataDir } from "./paths";
7
7
 
8
8
  const PROMPT_LIMIT = 400;
9
9
  const ROW_LIMIT = 100;
@@ -54,5 +54,5 @@ export function listOmpSessionsFrom(path: string, cwd: string): OmpSessionEntry[
54
54
  export function resolveListOmpSessions({ cwd }: RpcInput<typeof listOmpSessions>): {
55
55
  sessions: OmpSessionEntry[];
56
56
  } {
57
- return { sessions: listOmpSessionsFrom(join(ompAgentDir(), "history.db"), cwd) };
57
+ return { sessions: listOmpSessionsFrom(join(ompDataDir(), "history.db"), cwd) };
58
58
  }
@@ -0,0 +1,28 @@
1
+ import { defineSettings } from "@getpaseo/plugin";
2
+ import { z } from "zod";
3
+
4
+ export const DEFAULT_COMPOSER_PILL_SETTINGS = {
5
+ mcp: true,
6
+ hub: true,
7
+ memory: true,
8
+ sessions: true,
9
+ quota: true,
10
+ } as const;
11
+
12
+ export const composerPillSettingsSchema = z.object({
13
+ mcp: z.boolean().default(DEFAULT_COMPOSER_PILL_SETTINGS.mcp),
14
+ hub: z.boolean().default(DEFAULT_COMPOSER_PILL_SETTINGS.hub),
15
+ memory: z.boolean().default(DEFAULT_COMPOSER_PILL_SETTINGS.memory),
16
+ sessions: z.boolean().default(DEFAULT_COMPOSER_PILL_SETTINGS.sessions),
17
+ quota: z.boolean().default(DEFAULT_COMPOSER_PILL_SETTINGS.quota),
18
+ });
19
+
20
+ export type ComposerPillSettings = z.infer<typeof composerPillSettingsSchema>;
21
+ export type ComposerPillKey = keyof ComposerPillSettings;
22
+
23
+ export const composerPillSettings = defineSettings({
24
+ id: "composer-pills",
25
+ scope: "host",
26
+ version: 1,
27
+ schema: composerPillSettingsSchema,
28
+ });
@@ -0,0 +1,21 @@
1
+ export type ExternalUrlOpener = (url: string) => Promise<void>;
2
+
3
+ export function selectExternalUrlOpener(
4
+ paseoOpener: ExternalUrlOpener | undefined,
5
+ fallback: ExternalUrlOpener,
6
+ ): ExternalUrlOpener {
7
+ return paseoOpener ?? fallback;
8
+ }
9
+
10
+ export function validatedHttpUrl(value: string): string {
11
+ let url: URL;
12
+ try {
13
+ url = new URL(value);
14
+ } catch {
15
+ throw new Error("Only absolute HTTP(S) URLs are supported.");
16
+ }
17
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
18
+ throw new Error("Only absolute HTTP(S) URLs are supported.");
19
+ }
20
+ return url.href;
21
+ }
package/shared/hub.ts CHANGED
@@ -23,7 +23,7 @@ export const HubProcessSchema = z.object({
23
23
  exitCode: z.number().nullable(),
24
24
  });
25
25
  export type HubProcess = z.infer<typeof HubProcessSchema>;
26
- const CwdSchema = z.string().min(1).max(4_096);
26
+ export const OmpWorkspaceCwdSchema = z.string().min(1).max(4_096);
27
27
  const ProcessNameSchema = z
28
28
  .string()
29
29
  .min(1)
@@ -32,12 +32,12 @@ const ProcessNameSchema = z
32
32
 
33
33
  export const listHubProcesses = defineRpc({
34
34
  name: "paseo-omp.list-processes",
35
- input: z.object({ cwd: CwdSchema }),
35
+ input: z.object({ cwd: OmpWorkspaceCwdSchema }),
36
36
  output: z.object({ processes: z.array(HubProcessSchema) }),
37
37
  });
38
38
 
39
39
  export const tailHubLog = defineRpc({
40
40
  name: "paseo-omp.tail-log",
41
- input: z.object({ cwd: CwdSchema, name: ProcessNameSchema }),
41
+ input: z.object({ cwd: OmpWorkspaceCwdSchema, name: ProcessNameSchema }),
42
42
  output: z.object({ content: z.string(), truncated: z.boolean() }),
43
43
  });
package/shared/mcp.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { defineRpc } from "@getpaseo/plugin";
2
+ import { z } from "zod";
3
+
4
+ export const OMP_MCP_AUTH_TIMELINE_KIND = "omp-mcp-authorization";
5
+ const OmpMcpAuthorizationUrlSchema = z
6
+ .string()
7
+ .max(16_384)
8
+ .refine((value) => {
9
+ try {
10
+ const url = new URL(value);
11
+ return (
12
+ (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password
13
+ );
14
+ } catch {
15
+ return false;
16
+ }
17
+ }, "Authorization URL must be an HTTP URL without embedded credentials");
18
+
19
+ export const ompMcpAuthorizationTimelineSchema = z.object({
20
+ url: OmpMcpAuthorizationUrlSchema,
21
+ instructions: z
22
+ .string()
23
+ .max(64 * 1024)
24
+ .optional(),
25
+ loopbackCallback: z.boolean(),
26
+ browserAuthorizationToken: z.string().uuid().optional(),
27
+ });
28
+
29
+ export type OmpMcpAuthorizationTimeline = z.infer<typeof ompMcpAuthorizationTimelineSchema>;
30
+ export const openOmpMcpAuthorizationInPaseoBrowser = defineRpc({
31
+ name: "paseo-omp.open-mcp-authorization-in-browser",
32
+ input: z.object({ authorizationToken: z.string().uuid() }),
33
+ output: z.object({ opened: z.literal(true) }),
34
+ });
35
+
36
+ const OMP_MCP_SERVER_NAME = /^[a-zA-Z0-9_.:-]{1,100}$/u;
37
+
38
+ export type OmpMcpServerAction = "test" | "reauth" | "enable" | "disable";
39
+
40
+ export function buildOmpMcpServerCommand(
41
+ action: OmpMcpServerAction,
42
+ serverName: string,
43
+ ): string | undefined {
44
+ const name = serverName.trim();
45
+ if (!OMP_MCP_SERVER_NAME.test(name)) return;
46
+ return `/mcp ${action} ${name}`;
47
+ }
package/shared/memory.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { defineRpc } from "@getpaseo/plugin";
2
2
  import { z } from "zod";
3
+ import { OmpStoreSchema } from "./omp-store";
3
4
 
4
5
  const CwdSchema = z.string().min(1).max(4_096);
5
6
 
@@ -15,7 +16,7 @@ export type OmpMemoryFact = z.infer<typeof OmpMemoryFactSchema>;
15
16
 
16
17
  export const listOmpMemory = defineRpc({
17
18
  name: "paseo-omp.list-memory",
18
- input: z.object({ cwd: CwdSchema }),
19
+ input: z.object({ store: OmpStoreSchema.optional(), cwd: CwdSchema }),
19
20
  output: z.object({
20
21
  bank: z.string().nullable(),
21
22
  facts: z.array(OmpMemoryFactSchema),
@@ -1,5 +1,7 @@
1
1
  import { defineRpc } from "@getpaseo/plugin";
2
2
  import { z } from "zod";
3
+ import { OmpWorkspaceCwdSchema } from "./hub";
4
+ import { OmpStoreSchema } from "./omp-store";
3
5
 
4
6
  // Mirrors the safe, non-secret subset of omp's on-disk ~/.omp/agent/config.yml. That file is an
5
7
  // internal, unversioned config format owned by the omp harness (source: omp's
@@ -72,7 +74,9 @@ export type OmpConfig = z.infer<typeof OmpConfigSchema>;
72
74
 
73
75
  export const listOmpConfig = defineRpc({
74
76
  name: "paseo-omp.list-config",
75
- input: z.object({}),
77
+ input: z
78
+ .object({ store: OmpStoreSchema.optional(), cwd: OmpWorkspaceCwdSchema.optional() })
79
+ .strict(),
76
80
  output: z.object({
77
81
  path: z.string(),
78
82
  available: z.boolean(),
@@ -1,5 +1,7 @@
1
1
  import { defineRpc } from "@getpaseo/plugin";
2
2
  import { z } from "zod";
3
+ import { OmpWorkspaceCwdSchema } from "./hub";
4
+ import { OmpStoreSchema } from "./omp-store";
3
5
 
4
6
  export const OMP_PLUGIN_LIMIT = 256;
5
7
  export const OMP_PLUGIN_ARGUMENT_LIMIT = 512;
@@ -72,6 +74,7 @@ export const OmpInstalledPluginSchema = z
72
74
  availableFeatures: z.array(z.string().min(1).max(128)).max(128),
73
75
  configurable: z.boolean(),
74
76
  ambiguous: z.boolean(),
77
+ configAmbiguous: z.boolean(),
75
78
  usesDefaultFeatures: z.boolean(),
76
79
  })
77
80
  .strict();
@@ -89,7 +92,9 @@ export type OmpPluginState = z.infer<typeof OmpPluginStateSchema>;
89
92
 
90
93
  export const listOmpPlugins = defineRpc({
91
94
  name: "paseo-omp.list-plugins",
92
- input: z.object({}).strict(),
95
+ input: z
96
+ .object({ store: OmpStoreSchema.optional(), cwd: OmpWorkspaceCwdSchema.optional() })
97
+ .strict(),
93
98
  output: OmpPluginStateSchema,
94
99
  });
95
100
 
@@ -121,7 +126,13 @@ export type OmpPluginConfigState = z.infer<typeof OmpPluginConfigStateSchema>;
121
126
 
122
127
  export const inspectOmpPluginConfig = defineRpc({
123
128
  name: "paseo-omp.inspect-plugin-config",
124
- input: z.object({ plugin: OmpPluginNameSchema }).strict(),
129
+ input: z
130
+ .object({
131
+ store: OmpStoreSchema.optional(),
132
+ plugin: OmpPluginNameSchema,
133
+ cwd: OmpWorkspaceCwdSchema.optional(),
134
+ })
135
+ .strict(),
125
136
  output: OmpPluginConfigStateSchema,
126
137
  });
127
138
 
@@ -151,6 +162,8 @@ export const OmpPluginConfigMutationSchema = z.discriminatedUnion("action", [
151
162
  plugin: OmpPluginNameSchema,
152
163
  key: OmpPluginConfigKeySchema,
153
164
  value: z.union([OmpPluginConfigStringValueSchema, z.number().finite(), z.boolean()]),
165
+ store: OmpStoreSchema.optional(),
166
+ cwd: OmpWorkspaceCwdSchema.optional(),
154
167
  })
155
168
  .strict(),
156
169
  z
@@ -158,6 +171,8 @@ export const OmpPluginConfigMutationSchema = z.discriminatedUnion("action", [
158
171
  action: z.literal("delete"),
159
172
  plugin: OmpPluginNameSchema,
160
173
  key: OmpPluginConfigKeySchema,
174
+ store: OmpStoreSchema.optional(),
175
+ cwd: OmpWorkspaceCwdSchema.optional(),
161
176
  })
162
177
  .strict(),
163
178
  ]);
@@ -176,38 +191,64 @@ export const mutateOmpPluginConfig = defineRpc({
176
191
  });
177
192
 
178
193
  const ScopedMutationShape = {
179
- scope: z.literal("user").optional(),
194
+ scope: OmpPluginScopeSchema.optional(),
195
+ store: OmpStoreSchema.optional(),
196
+ cwd: OmpWorkspaceCwdSchema.optional(),
180
197
  };
181
-
182
- export const OmpPluginMutationSchema = z.discriminatedUnion("action", [
183
- z
184
- .object({
185
- action: z.literal("install"),
186
- source: OmpPluginInstallSourceSchema,
187
- ...ScopedMutationShape,
188
- })
189
- .strict(),
190
- z
191
- .object({ action: z.literal("enable"), plugin: OmpPluginTargetSchema, ...ScopedMutationShape })
192
- .strict(),
193
- z
194
- .object({ action: z.literal("disable"), plugin: OmpPluginTargetSchema, ...ScopedMutationShape })
195
- .strict(),
196
- z
197
- .object({
198
- action: z.literal("uninstall"),
199
- plugin: OmpPluginTargetSchema,
200
- ...ScopedMutationShape,
201
- })
202
- .strict(),
203
- z
204
- .object({
205
- action: z.literal("upgrade"),
206
- plugin: OmpMarketplacePluginIdSchema,
207
- ...ScopedMutationShape,
208
- })
209
- .strict(),
210
- ]);
198
+ export const OmpPluginMutationSchema = z
199
+ .discriminatedUnion("action", [
200
+ z
201
+ .object({
202
+ action: z.literal("install"),
203
+ source: OmpPluginInstallSourceSchema,
204
+ ...ScopedMutationShape,
205
+ })
206
+ .strict(),
207
+ z
208
+ .object({
209
+ action: z.literal("enable"),
210
+ plugin: OmpPluginTargetSchema,
211
+ ...ScopedMutationShape,
212
+ })
213
+ .strict(),
214
+ z
215
+ .object({
216
+ action: z.literal("disable"),
217
+ plugin: OmpPluginTargetSchema,
218
+ ...ScopedMutationShape,
219
+ })
220
+ .strict(),
221
+ z
222
+ .object({
223
+ action: z.literal("uninstall"),
224
+ plugin: OmpPluginTargetSchema,
225
+ ...ScopedMutationShape,
226
+ })
227
+ .strict(),
228
+ z
229
+ .object({
230
+ action: z.literal("upgrade"),
231
+ plugin: OmpMarketplacePluginIdSchema,
232
+ ...ScopedMutationShape,
233
+ })
234
+ .strict(),
235
+ ])
236
+ .superRefine((input, context) => {
237
+ if (input.scope === "project" && input.cwd === undefined) {
238
+ context.addIssue({
239
+ code: "custom",
240
+ message: "Project-scoped plugin actions require a workspace",
241
+ path: ["cwd"],
242
+ });
243
+ }
244
+ if (input.action === "install" && input.scope === "project") {
245
+ context.addIssue({
246
+ code: "custom",
247
+ message: "Project-scoped installation is not supported through this API",
248
+ path: ["scope"],
249
+ });
250
+ }
251
+ });
211
252
  export type OmpPluginMutation = z.infer<typeof OmpPluginMutationSchema>;
212
253
 
213
254
  export const mutateOmpPlugin = defineRpc({