@omercnet/paseo-omp 0.2.1 → 0.3.0-next.101.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 +499 -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 +340 -1
  47. package/server/provider/session.ts +716 -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
@@ -1,6 +1,9 @@
1
- import { createHash } from "node:crypto";
2
- import { delimiter, isAbsolute } from "node:path";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { delimiter, dirname, isAbsolute, join } from "node:path";
3
5
  import type { RpcInput } from "@getpaseo/plugin";
6
+ import { parseDocument, parse as parseYaml } from "yaml";
4
7
  import {
5
8
  type listOmpSettings,
6
9
  OMP_SETTINGS_CATALOG_VERSION,
@@ -12,6 +15,7 @@ import {
12
15
  } from "../shared/omp-settings";
13
16
  import { SerialMutationQueue } from "./mutation-queue";
14
17
  import { readOmpConfigFrom } from "./omp-config";
18
+ import { currentOmpEnvironment } from "./paths";
15
19
  import {
16
20
  type BoundedRun,
17
21
  buildStatefulCommandEnv,
@@ -54,7 +58,103 @@ export type OmpSettingsUpdateResult = {
54
58
 
55
59
  export interface OmpSettingsDependencies {
56
60
  resolveExecutable(): Promise<string | null>;
57
- runConfig(executable: string, args: readonly string[]): Promise<BoundedRun>;
61
+ runConfig(executable: string, args: readonly string[], cwd?: string): Promise<BoundedRun>;
62
+ validateProjectConfig(executable: string, path: string): Promise<BoundedRun>;
63
+ }
64
+
65
+ async function readProjectConfigText(path: string): Promise<string> {
66
+ try {
67
+ return await readFile(path, "utf8");
68
+ } catch (error) {
69
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return "{}\n";
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ function runSucceeded(result: BoundedRun): boolean {
75
+ return (
76
+ result.outcome === "exited" &&
77
+ result.exitCode === 0 &&
78
+ result.signal === null &&
79
+ !result.truncated &&
80
+ !result.cleanupFailed
81
+ );
82
+ }
83
+
84
+ async function writeProjectChanges(
85
+ cwd: string,
86
+ expectedText: string,
87
+ changes: RpcInput<typeof updateOmpSettings>["changes"],
88
+ executable: string,
89
+ dependencies: OmpSettingsDependencies,
90
+ ): Promise<"applied" | "conflict"> {
91
+ const path = join(cwd, ".omp", "config.yml");
92
+ try {
93
+ const metadata = await lstat(path);
94
+ if (metadata.isSymbolicLink()) {
95
+ throw new Error("Symlinked workspace OMP configuration cannot be edited through Paseo.");
96
+ }
97
+ if (!metadata.isFile()) throw new Error("Workspace OMP configuration is not a regular file.");
98
+ } catch (error) {
99
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
100
+ }
101
+
102
+ const snapshot = await readProjectConfigText(path);
103
+ if (snapshot !== expectedText) return "conflict";
104
+ const document = parseDocument(snapshot);
105
+ if (document.errors.length > 0) throw new Error("Workspace OMP configuration is invalid YAML.");
106
+ for (const change of changes) {
107
+ const segments = change.path.split(".");
108
+ if (change.operation === "reset") document.deleteIn(segments);
109
+ else document.setIn(segments, change.value);
110
+ }
111
+
112
+ await mkdir(dirname(path), { recursive: true });
113
+ const temporaryPath = `${path}.${randomUUID()}.tmp`;
114
+ try {
115
+ await writeFile(temporaryPath, document.toString(), { encoding: "utf8", mode: 0o600 });
116
+ const validation = await dependencies.validateProjectConfig(executable, temporaryPath);
117
+ if (!runSucceeded(validation)) {
118
+ throw new Error("OMP rejected the workspace configuration change.");
119
+ }
120
+ if ((await readProjectConfigText(path)) !== snapshot) return "conflict";
121
+ await rename(temporaryPath, path);
122
+ return "applied";
123
+ } finally {
124
+ await unlink(temporaryPath).catch(() => {});
125
+ }
126
+ }
127
+
128
+ function hasConfiguredPath(value: unknown, path: string): boolean {
129
+ let current = value;
130
+ for (const segment of path.split(".")) {
131
+ if (current === null || typeof current !== "object" || Array.isArray(current)) return false;
132
+ const record = current as Record<string, unknown>;
133
+ if (!Object.hasOwn(record, segment)) return false;
134
+ current = record[segment];
135
+ }
136
+ return true;
137
+ }
138
+
139
+ async function markWorkspaceOverrides(cwd: string, settings: OmpSetting[]): Promise<OmpSetting[]> {
140
+ const directory = join(cwd, ".omp");
141
+ const sources: unknown[] = [];
142
+ try {
143
+ sources.push(JSON.parse(await readFile(join(directory, "settings.json"), "utf8")) as unknown);
144
+ } catch {
145
+ // Legacy project settings are optional.
146
+ }
147
+ try {
148
+ sources.push(parseYaml(await readFile(join(directory, "config.yml"), "utf8")) as unknown);
149
+ } catch {
150
+ // Canonical project settings are optional.
151
+ }
152
+ if (sources.length === 0) return settings;
153
+ return settings.map((setting) =>
154
+ sources.some((source) => hasConfiguredPath(source, setting.path))
155
+ ? { ...setting, workspaceOverride: true }
156
+ : setting,
157
+ );
58
158
  }
59
159
 
60
160
  function isCredentialSetting(path: string, type: OmpSettingType): boolean {
@@ -101,37 +201,52 @@ export function parseOmpSettingsList(raw: unknown): ParsedOmpSettings {
101
201
 
102
202
  async function resolveOmpExecutable(): Promise<string | null> {
103
203
  return resolveExecutablePath(
104
- process.env.OMP_COMMAND ?? "omp",
105
- (process.env.PATH ?? "").split(delimiter),
204
+ currentOmpEnvironment().OMP_COMMAND ?? "omp",
205
+ (currentOmpEnvironment().PATH ?? "").split(delimiter),
106
206
  {
107
207
  cwd: process.cwd(),
108
208
  platform: process.platform,
109
- pathExt: process.env.PATHEXT ?? WINDOWS_DEFAULT_PATHEXT,
209
+ pathExt: currentOmpEnvironment().PATHEXT ?? WINDOWS_DEFAULT_PATHEXT,
110
210
  },
111
211
  );
112
212
  }
113
213
 
114
- async function runOmpConfig(executable: string, args: readonly string[]) {
214
+ async function runOmpConfig(executable: string, args: readonly string[], cwd = process.cwd()) {
115
215
  return runBounded(
116
216
  defaultSpawn,
117
217
  executable,
118
218
  ["config", ...args],
119
- buildStatefulCommandEnv(process.env),
219
+ buildStatefulCommandEnv(currentOmpEnvironment()),
120
220
  CONFIG_TIMEOUT_MS,
121
221
  KILL_GRACE_MS,
122
222
  MAX_CONFIG_OUTPUT_BYTES,
123
- process.cwd(),
223
+ cwd,
224
+ );
225
+ }
226
+
227
+ async function validateOmpProjectConfig(executable: string, path: string): Promise<BoundedRun> {
228
+ return runBounded(
229
+ defaultSpawn,
230
+ executable,
231
+ ["--config", path, "config", "list", "--json"],
232
+ buildStatefulCommandEnv(currentOmpEnvironment()),
233
+ CONFIG_TIMEOUT_MS,
234
+ KILL_GRACE_MS,
235
+ MAX_CONFIG_OUTPUT_BYTES,
236
+ tmpdir(),
124
237
  );
125
238
  }
126
239
 
127
240
  const DEFAULT_DEPENDENCIES: OmpSettingsDependencies = {
128
241
  resolveExecutable: resolveOmpExecutable,
129
242
  runConfig: runOmpConfig,
243
+ validateProjectConfig: validateOmpProjectConfig,
130
244
  };
131
245
 
132
246
  async function loadCatalog(
133
247
  executable: string | null | undefined,
134
248
  dependencies: OmpSettingsDependencies,
249
+ cwd?: string,
135
250
  ): Promise<CatalogResult> {
136
251
  const resolved = executable === undefined ? await dependencies.resolveExecutable() : executable;
137
252
  if (!resolved) {
@@ -144,7 +259,7 @@ async function loadCatalog(
144
259
  };
145
260
  }
146
261
 
147
- const result = await dependencies.runConfig(resolved, ["list", "--json"]);
262
+ const result = await dependencies.runConfig(resolved, ["list", "--json"], cwd);
148
263
  if (result.outcome !== "exited" || result.exitCode !== 0 || result.truncated) {
149
264
  return {
150
265
  catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
@@ -159,23 +274,25 @@ async function loadCatalog(
159
274
  }
160
275
 
161
276
  let path: string | undefined;
162
- const pathResult = await dependencies.runConfig(resolved, ["path"]);
277
+ const pathResult = await dependencies.runConfig(resolved, ["path"], cwd);
163
278
  const agentDir =
164
279
  pathResult.outcome === "exited" && pathResult.exitCode === 0 ? pathResult.stdout.trim() : "";
165
280
  if (agentDir && isAbsolute(agentDir) && !agentDir.includes("\0")) {
166
281
  path = (await readOmpConfigFrom(agentDir)).path;
167
282
  }
168
-
169
283
  try {
170
284
  const parsed: unknown = JSON.parse(result.stdout);
171
285
  const catalog = parseOmpSettingsList(parsed);
286
+ const projectPath = cwd ? join(cwd, ".omp", "config.yml") : null;
287
+ const projectText = projectPath ? await readProjectConfigText(projectPath) : "";
288
+ const revision = createHash("sha256").update(result.stdout).update(projectText).digest("hex");
172
289
  return {
173
290
  catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
174
291
  available: true,
175
- revision: createHash("sha256").update(result.stdout).digest("hex"),
292
+ revision,
176
293
  droppedCount: catalog.droppedCount,
177
- ...(path ? { path } : {}),
178
- settings: catalog.settings,
294
+ ...(projectPath ? { path: projectPath } : path ? { path } : {}),
295
+ settings: cwd ? await markWorkspaceOverrides(cwd, catalog.settings) : catalog.settings,
179
296
  };
180
297
  } catch {
181
298
  return {
@@ -197,10 +314,19 @@ function serializeScalar(type: OmpSettingType, value: OmpScalarValue): string |
197
314
  }
198
315
 
199
316
  export async function listOmpSettingsWithDependencies(
200
- _input: RpcInput<typeof listOmpSettings>,
317
+ input: RpcInput<typeof listOmpSettings>,
201
318
  dependencies: OmpSettingsDependencies,
202
319
  ): Promise<CatalogResult> {
203
- return loadCatalog(undefined, dependencies);
320
+ if (input.cwd && (!isAbsolute(input.cwd) || input.cwd.includes("\0"))) {
321
+ return {
322
+ catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
323
+ available: false,
324
+ droppedCount: 0,
325
+ settings: [],
326
+ error: "The workspace path is invalid.",
327
+ };
328
+ }
329
+ return loadCatalog(undefined, dependencies, input.cwd);
204
330
  }
205
331
 
206
332
  export async function resolveListOmpSettings(
@@ -213,8 +339,22 @@ export async function updateOmpSettingsWithDependencies(
213
339
  input: RpcInput<typeof updateOmpSettings>,
214
340
  dependencies: OmpSettingsDependencies,
215
341
  ): Promise<OmpSettingsUpdateResult> {
342
+ if (input.cwd && (!isAbsolute(input.cwd) || input.cwd.includes("\0"))) {
343
+ return {
344
+ conflict: false,
345
+ appliedPaths: [],
346
+ failed: { path: "configuration", message: "The workspace path is invalid." },
347
+ catalog: {
348
+ catalogVersion: OMP_SETTINGS_CATALOG_VERSION,
349
+ available: false,
350
+ droppedCount: 0,
351
+ settings: [],
352
+ error: "The workspace path is invalid.",
353
+ },
354
+ };
355
+ }
216
356
  const executable = await dependencies.resolveExecutable();
217
- const current = await loadCatalog(executable, dependencies);
357
+ const current = await loadCatalog(executable, dependencies, input.cwd);
218
358
  if (!executable || !current.available || !current.revision) {
219
359
  return {
220
360
  conflict: false,
@@ -229,8 +369,74 @@ export async function updateOmpSettingsWithDependencies(
229
369
  if (current.revision !== input.revision) {
230
370
  return { conflict: true, appliedPaths: [], catalog: current };
231
371
  }
232
-
233
372
  const byPath = new Map(current.settings.map((setting) => [setting.path, setting]));
373
+ if (input.cwd) {
374
+ for (const change of input.changes) {
375
+ const setting = byPath.get(change.path);
376
+ if (
377
+ !setting ||
378
+ setting.redacted ||
379
+ !["boolean", "number", "string", "enum"].includes(setting.type)
380
+ ) {
381
+ return {
382
+ conflict: false,
383
+ appliedPaths: [],
384
+ failed: {
385
+ path: change.path,
386
+ message: "This setting cannot be edited as a workspace scalar value.",
387
+ },
388
+ catalog: current,
389
+ };
390
+ }
391
+ if (change.operation === "set" && serializeScalar(setting.type, change.value) === null) {
392
+ return {
393
+ conflict: false,
394
+ appliedPaths: [],
395
+ failed: { path: change.path, message: `Expected a ${setting.type} value.` },
396
+ catalog: current,
397
+ };
398
+ }
399
+ }
400
+
401
+ const projectPath = join(input.cwd, ".omp", "config.yml");
402
+ const beforeValidation = await readProjectConfigText(projectPath);
403
+ const verified = await loadCatalog(executable, dependencies, input.cwd);
404
+ const afterValidation = await readProjectConfigText(projectPath);
405
+ if (verified.revision !== input.revision || beforeValidation !== afterValidation) {
406
+ return { conflict: true, appliedPaths: [], catalog: verified };
407
+ }
408
+ try {
409
+ const outcome = await writeProjectChanges(
410
+ input.cwd,
411
+ afterValidation,
412
+ input.changes,
413
+ executable,
414
+ dependencies,
415
+ );
416
+ if (outcome === "conflict") {
417
+ return {
418
+ conflict: true,
419
+ appliedPaths: [],
420
+ catalog: await loadCatalog(executable, dependencies, input.cwd),
421
+ };
422
+ }
423
+ } catch (error) {
424
+ return {
425
+ conflict: false,
426
+ appliedPaths: [],
427
+ failed: {
428
+ path: input.changes[0]?.path ?? "configuration",
429
+ message: error instanceof Error ? error.message : "Could not update workspace settings.",
430
+ },
431
+ catalog: await loadCatalog(executable, dependencies, input.cwd),
432
+ };
433
+ }
434
+ return {
435
+ conflict: false,
436
+ appliedPaths: input.changes.map((change) => change.path),
437
+ catalog: await loadCatalog(executable, dependencies, input.cwd),
438
+ };
439
+ }
234
440
  const appliedPaths: string[] = [];
235
441
  for (const change of input.changes) {
236
442
  const setting = byPath.get(change.path);
@@ -243,7 +449,7 @@ export async function updateOmpSettingsWithDependencies(
243
449
  conflict: false,
244
450
  appliedPaths,
245
451
  failed: { path: change.path, message: "This setting cannot be edited as a scalar value." },
246
- catalog: await loadCatalog(executable, dependencies),
452
+ catalog: await loadCatalog(executable, dependencies, input.cwd),
247
453
  };
248
454
  }
249
455
  let args: string[] | null;
@@ -258,16 +464,18 @@ export async function updateOmpSettingsWithDependencies(
258
464
  conflict: false,
259
465
  appliedPaths,
260
466
  failed: { path: change.path, message: `Expected a ${setting.type} value.` },
261
- catalog: appliedPaths.length ? await loadCatalog(executable, dependencies) : current,
467
+ catalog: appliedPaths.length
468
+ ? await loadCatalog(executable, dependencies, input.cwd)
469
+ : current,
262
470
  };
263
471
  }
264
- const result = await dependencies.runConfig(executable, args);
472
+ const result = await dependencies.runConfig(executable, args, input.cwd);
265
473
  if (result.outcome !== "exited" || result.exitCode !== 0) {
266
474
  return {
267
475
  conflict: false,
268
476
  appliedPaths,
269
477
  failed: { path: change.path, message: "OMP rejected this setting change." },
270
- catalog: await loadCatalog(executable, dependencies),
478
+ catalog: await loadCatalog(executable, dependencies, input.cwd),
271
479
  };
272
480
  }
273
481
  appliedPaths.push(change.path);
@@ -276,7 +484,7 @@ export async function updateOmpSettingsWithDependencies(
276
484
  return {
277
485
  conflict: false,
278
486
  appliedPaths,
279
- catalog: await loadCatalog(executable, dependencies),
487
+ catalog: await loadCatalog(executable, dependencies, input.cwd),
280
488
  };
281
489
  }
282
490
 
package/server/paths.ts CHANGED
@@ -1,18 +1,132 @@
1
- import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { closeSync, constants, existsSync, fstatSync, openSync, readSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { isOmpProfileName, type OmpStore, OmpStoreSchema } from "../shared/omp-store";
4
6
 
5
7
  const MAX_SETTINGS_BYTES = 64 * 1024;
8
+ const storeContext = new AsyncLocalStorage<OmpStore>();
9
+ const ServerOmpStoreSchema = OmpStoreSchema.refine(
10
+ (store) => !store.agentDir || isAbsolute(store.agentDir),
11
+ "OMP agent directory must be absolute on the server platform",
12
+ );
6
13
 
7
- /** Root of omp's per-machine agent state (`agent.db`, `history.db`, `memories/`). */
8
- export function ompAgentDir(environment: NodeJS.ProcessEnv = process.env): string {
14
+ export function withOmpStore<T>(store: OmpStore | undefined, operation: () => T): T {
15
+ return storeContext.run(ServerOmpStoreSchema.parse(store ?? {}), operation);
16
+ }
17
+
18
+ /** Per-request selection, never process.env mutation: concurrent profiles stay isolated. */
19
+ export function currentOmpEnvironment(source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
20
+ const store = storeContext.getStore();
21
+ if (!store?.profile && !store?.agentDir) return source;
22
+ const environment = { ...source };
23
+ for (const name of [
24
+ "PASEO_OMP_AGENT_DIR",
25
+ "OMP_AGENT_DIR",
26
+ "PI_CODING_AGENT_DIR",
27
+ "OMP_SESSION_DIR",
28
+ "PI_CODING_AGENT_SESSION_DIR",
29
+ "OMP_PROFILE",
30
+ "PI_PROFILE",
31
+ "PI_CONFIG_FILES",
32
+ ])
33
+ delete environment[name];
34
+ if (store.profile) environment.OMP_PROFILE = store.profile;
35
+ else if (store.agentDir) {
36
+ environment.PI_CODING_AGENT_DIR = store.agentDir;
37
+ }
38
+ return environment;
39
+ }
40
+
41
+ type OmpStorageKind = "data" | "state" | "cache";
42
+
43
+ const XDG_HOME_BY_KIND: Record<OmpStorageKind, string> = {
44
+ data: "XDG_DATA_HOME",
45
+ state: "XDG_STATE_HOME",
46
+ cache: "XDG_CACHE_HOME",
47
+ };
48
+
49
+ function ompProfile(environment: NodeJS.ProcessEnv): string | undefined {
50
+ const raw =
51
+ environment.OMP_PROFILE !== undefined ? environment.OMP_PROFILE : environment.PI_PROFILE;
52
+ const profile = raw?.trim();
53
+ if (!profile || profile === "default") return;
54
+ if (!isOmpProfileName(profile)) throw new Error("Invalid OMP profile name");
55
+ return profile;
56
+ }
57
+
58
+ function defaultOmpAgentDir(environment: NodeJS.ProcessEnv, profile?: string): string {
9
59
  const home = environment.HOME ?? environment.USERPROFILE ?? homedir();
10
- return (
11
- environment.PASEO_OMP_AGENT_DIR ??
12
- environment.OMP_AGENT_DIR ??
13
- environment.PI_CODING_AGENT_DIR ??
14
- join(home, environment.PI_CONFIG_DIR ?? ".omp", "agent")
15
- );
60
+ return profile
61
+ ? join(home, environment.PI_CONFIG_DIR || ".omp", "profiles", profile, "agent")
62
+ : join(home, environment.PI_CONFIG_DIR || ".omp", "agent");
63
+ }
64
+
65
+ function explicitOmpAgentDir(
66
+ environment: NodeJS.ProcessEnv,
67
+ profile: string | undefined,
68
+ ): string | undefined {
69
+ const pluginOverride = environment.PASEO_OMP_AGENT_DIR ?? environment.OMP_AGENT_DIR;
70
+ if (pluginOverride) return pluginOverride;
71
+ // OMP deliberately ignores PI_CODING_AGENT_DIR while a named profile is active.
72
+ if (profile) return;
73
+ const override = environment.PI_CODING_AGENT_DIR;
74
+ const inheritedProfile = environment.PI_PROFILE?.trim();
75
+ // OMP_PROFILE=""/"default" overrides PI_PROFILE, including an agent path
76
+ // propagated by the parent profile. Such a path is not a custom default store.
77
+ if (
78
+ inheritedProfile &&
79
+ isOmpProfileName(inheritedProfile) &&
80
+ override === defaultOmpAgentDir(environment, inheritedProfile)
81
+ )
82
+ return;
83
+ return override ? resolve(override) : undefined;
84
+ }
85
+
86
+ /** OMP's configuration agent directory. Data/state/cache may use XDG-specific roots. */
87
+ export function ompAgentDir(environment: NodeJS.ProcessEnv = currentOmpEnvironment()): string {
88
+ const profile = ompProfile(environment);
89
+ return explicitOmpAgentDir(environment, profile) ?? defaultOmpAgentDir(environment, profile);
90
+ }
91
+
92
+ function ompStorageDir(
93
+ kind: OmpStorageKind,
94
+ environment: NodeJS.ProcessEnv,
95
+ platform: NodeJS.Platform,
96
+ ): string {
97
+ const profile = ompProfile(environment);
98
+ const agentDir = ompAgentDir(environment);
99
+ if (
100
+ agentDir !== defaultOmpAgentDir(environment, profile) ||
101
+ (platform !== "linux" && platform !== "darwin")
102
+ ) {
103
+ return agentDir;
104
+ }
105
+ const xdgHome = environment[XDG_HOME_BY_KIND[kind]];
106
+ if (!xdgHome) return agentDir;
107
+ const root = profile ? join(xdgHome, "omp", "profiles", profile) : join(xdgHome, "omp");
108
+ return existsSync(root) ? root : agentDir;
109
+ }
110
+
111
+ export function ompDataDir(
112
+ environment: NodeJS.ProcessEnv = currentOmpEnvironment(),
113
+ platform: NodeJS.Platform = process.platform,
114
+ ): string {
115
+ return ompStorageDir("data", environment, platform);
116
+ }
117
+
118
+ export function ompCacheDir(
119
+ environment: NodeJS.ProcessEnv = currentOmpEnvironment(),
120
+ platform: NodeJS.Platform = process.platform,
121
+ ): string {
122
+ return ompStorageDir("cache", environment, platform);
123
+ }
124
+
125
+ export function ompStateDir(
126
+ environment: NodeJS.ProcessEnv = currentOmpEnvironment(),
127
+ platform: NodeJS.Platform = process.platform,
128
+ ): string {
129
+ return ompStorageDir("state", environment, platform);
16
130
  }
17
131
 
18
132
  function configuredSessionDir(agentDir: string): string | undefined {
@@ -56,9 +170,12 @@ function configuredSessionDir(agentDir: string): string | undefined {
56
170
  }
57
171
 
58
172
  /** OMP's effective session root, honoring its documented environment and settings precedence. */
59
- export function ompSessionDir(environment: NodeJS.ProcessEnv = process.env): string {
173
+ export function ompSessionDir(
174
+ environment: NodeJS.ProcessEnv = currentOmpEnvironment(),
175
+ platform: NodeJS.Platform = process.platform,
176
+ ): string {
60
177
  const explicit = environment.OMP_SESSION_DIR ?? environment.PI_CODING_AGENT_SESSION_DIR;
61
178
  if (explicit) return resolve(explicit);
62
179
  const agentDir = ompAgentDir(environment);
63
- return configuredSessionDir(agentDir) ?? join(agentDir, "sessions");
180
+ return configuredSessionDir(agentDir) ?? join(ompDataDir(environment, platform), "sessions");
64
181
  }
@@ -18,7 +18,7 @@ export const OMP_MODES: readonly ProviderMode[] = [
18
18
  {
19
19
  id: "full",
20
20
  label: "Full Access",
21
- description: "Launches OMP with yolo approval mode so tools run without prompts.",
21
+ description: "Runs all tools without approval prompts.",
22
22
  icon: "ShieldOff",
23
23
  colorTier: "dangerous",
24
24
  isUnattended: true,
@@ -26,15 +26,14 @@ export const OMP_MODES: readonly ProviderMode[] = [
26
26
  {
27
27
  id: "write",
28
28
  label: "Write Approval",
29
- description:
30
- "Launches OMP with write approval mode; reads are free and writes require approval.",
29
+ description: "Runs reads without approval; writes require approval.",
31
30
  icon: "ShieldAlert",
32
31
  colorTier: "moderate",
33
32
  },
34
33
  {
35
34
  id: "ask",
36
35
  label: "Always Ask",
37
- description: "Launches OMP with always-ask approval mode for write and exec tools.",
36
+ description: "Requires approval for write and execution tools.",
38
37
  icon: "ShieldCheck",
39
38
  colorTier: "safe",
40
39
  },