@mcowger/oh-my-pi-plexus 1.5.0 → 1.6.0

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 (3) hide show
  1. package/README.md +23 -26
  2. package/dist/extension.js +46 -174
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,7 +12,7 @@ Exposes models from a self-hosted [Plexus](https://github.com/mcowger/plexus) AI
12
12
  ## Prerequisites
13
13
 
14
14
  - A running Plexus instance
15
- - pi 0.81.0 or later (for `plexus-pi`)
15
+ - pi 0.85.1 or later (for `plexus-pi`)
16
16
 
17
17
  ## Installation
18
18
 
@@ -109,13 +109,14 @@ To force a model refresh:
109
109
  /plexus refresh
110
110
  ```
111
111
 
112
- Select a Plexus model explicitly for the current session. With no model ID, Pi opens a selector; you can also pass the exact Plexus model ID directly. This explicit choice is not applied automatically when starting a new session:
112
+ Inspect the effective URL, authentication availability, and catalog source without exposing a credential:
113
113
 
114
114
  ```
115
- /plexus set-default-model
116
- /plexus set-default-model claude-sonnet-4-5
115
+ /plexus status
117
116
  ```
118
117
 
118
+ Select a Plexus model in `/model`. Save its startup default with the host's normal model-picker action; Plexus does not maintain a separate default-model setting.
119
+
119
120
  Use `/login plexus` for setup and `/logout plexus` to remove stored credentials.
120
121
 
121
122
  ### Oh My Pi
@@ -126,35 +127,32 @@ Run inside Oh My Pi using the native login flow:
126
127
  /login plexus
127
128
  ```
128
129
 
129
- Same prompts and `/plexus refresh` / `/plexus set-default-model` commands as pi (see above) — `plexus-oh-my-pi` mirrors the pi adapter's behavior, adjusted for Oh My Pi's own runtime packages and built-in model registry.
130
+ Same prompts and `/plexus refresh` / `/plexus status` commands as pi (see above). Save the selected startup model through OMP's normal `/model` or `/models` flow.
130
131
 
131
132
  ## Configuration files
132
133
 
133
- ### pi
134
+ ### Connection state
134
135
 
135
- ```
136
- ~/.pi/agent/extensions/plexus/
137
- config.json # base URL and model preference metadata
138
- plexus-models-cache.json # last-fetched model list (startup cache)
139
- plexus-models-response.json # raw API response (diagnostics)
140
- plexus.log # extension activity log
136
+ Each adapter stores only non-secret Plexus settings in its own `config.json`:
137
+
138
+ ```text
139
+ <agent-dir>/extensions/plexus/config.json # base URL and optional model suppression
141
140
  ```
142
141
 
143
- The API key is stored through pi's own auth storage. `PLEXUS_API_URL` or `PLEXUS_BASE_URL` can be used as an environment override.
142
+ Credentials stay in the host credential store (`auth.json` for Pi, `agent.db` for OMP). Model catalogs stay in the host model registry/store. The extension no longer writes a second model cache, ETag file, raw API response, or default-model preference.
144
143
 
145
- ### Oh My Pi
144
+ At runtime, configuration resolves as follows:
146
145
 
147
- ```
148
- ~/.omp/agent/extensions/plexus/
149
- config.json # base URL and model preference metadata
150
- plexus-models-cache.json # last-fetched model list (startup cache)
151
- plexus-models-response.json # raw API response (diagnostics)
152
- plexus.log # extension activity log
146
+ ```text
147
+ base URL: PLEXUS_API_URL → PLEXUS_BASE_URL → saved Plexus base URL
148
+ API key: host credential store → PLEXUS_API_KEY process fallback
149
+ catalog: host model store → live Plexus refresh
150
+ startup model: host model-picker preference
153
151
  ```
154
152
 
155
- Same layout as pi, rooted under `~/.omp/agent` instead of `~/.pi/agent` since Oh My Pi resolves its own agent directory.
153
+ `PLEXUS_API_URL` and `PLEXUS_BASE_URL` are process-only URL overrides; `PLEXUS_API_KEY` is a process-only credential fallback. None are persisted. `/plexus status` reports the effective URL source, whether auth is available, and catalog state without exposing credentials.
156
154
 
157
- Both adapters also accept pi-style environment interpolation in configured strings, such as `${PLEXUS_API_URL}` or `$PLEXUS_API_KEY`. This is useful when checking non-secret config into an agent config file while keeping the actual values in the environment.
155
+ Plexus accepts a root URL or a `/v1` API URL. The adapter normalizes that only for the Plexus discovery request. Each model then receives its own API-specific base URL: OpenAI stays on `/v1`, Anthropic uses the root, and Google uses `/v1beta`.
158
156
 
159
157
  ---
160
158
 
@@ -200,16 +198,15 @@ packages/
200
198
  src/
201
199
  extension.ts # entry point: commands, session refresh, auth flow
202
200
  mapper.ts # PlexusModelDescriptor → pi ProviderModelConfig
203
- config.ts # base URL / default model config I/O
204
- cache.ts # model cache I/O
201
+ config.ts # base URL / suppression config I/O
202
+ cache.ts # Pi native model-store restore
205
203
  log.ts # append-only log
206
204
  package.json # declares pi.extensions entry point
207
205
  plexus-oh-my-pi/ # Oh My Pi host adapter (fork of pi; own runtime packages + catalog)
208
206
  src/
209
207
  extension.ts # entry point: commands, session refresh, auth flow
210
208
  mapper.ts # PlexusModelDescriptor → Oh My Pi ProviderModelConfig
211
- config.ts # base URL / default model config I/O
212
- cache.ts # model cache I/O
209
+ config.ts # base URL / suppression config I/O
213
210
  log.ts # append-only log
214
211
  package.json # declares omp.extensions entry point
215
212
  ```
package/dist/extension.js CHANGED
@@ -432682,43 +432682,6 @@ function getEnvSuppressedModels() {
432682
432682
  const raw = env.PLEXUS_SUPPRESS_MODELS ?? env.PLEXUS_EXCLUDE_MODELS;
432683
432683
  return parseSuppressionPatterns(raw);
432684
432684
  }
432685
- function isModelSuppressed(model, patterns) {
432686
- const envPatterns = getEnvSuppressedModels();
432687
- const explicitPatterns = parseSuppressionPatterns(patterns);
432688
- const allPatterns = [...envPatterns, ...explicitPatterns];
432689
- if (allPatterns.length === 0)
432690
- return false;
432691
- const id = model.id.toLowerCase();
432692
- const name = (model.name ?? "").toLowerCase();
432693
- const shortId = id.includes("/") ? id.split("/").pop() : id.includes(":") ? id.split(":").pop() : id;
432694
- for (const pattern of allPatterns) {
432695
- if (matchesPattern(id, name, shortId, pattern)) {
432696
- return true;
432697
- }
432698
- }
432699
- return false;
432700
- }
432701
- function matchesPattern(id, name, shortId, pattern) {
432702
- const p = pattern.toLowerCase();
432703
- if (p.startsWith("regex:")) {
432704
- try {
432705
- const re = new RegExp(pattern.slice(6), "i");
432706
- return re.test(id) || re.test(name) || re.test(shortId);
432707
- } catch {
432708
- return false;
432709
- }
432710
- }
432711
- if (p.includes("*") || p.includes("?")) {
432712
- const regexStr = "^" + p.replace(/([.+^${}()|[\]\\])/g, "\\$1").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
432713
- try {
432714
- const re = new RegExp(regexStr, "i");
432715
- return re.test(id) || re.test(name) || re.test(shortId);
432716
- } catch {
432717
- return false;
432718
- }
432719
- }
432720
- return id === p || name === p || shortId === p;
432721
- }
432722
432685
  // ../plexus-models/src/convert.ts
432723
432686
  var REASONING_PARAMS = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
432724
432687
  var NON_CHAT_PATTERN = /(?:^|[\W_])(?:embed(?:ding|dings)?|transcri(?:be[ds]?|ptions?)|whisper|speech[\W_]*to[\W_]*text|stt|text[\W_]*to[\W_]*speech|tts|image(?:[\W_]*(?:gen(?:eration)?|\d+))?|diffusion|dall[\W_]*e|stable[\W_]*diffusion|sdxl|dream)(?:$|[\W_])/i;
@@ -433092,7 +433055,7 @@ var normalizeConfigBaseUrl = (raw) => {
433092
433055
  const root = normalizeRoot(raw);
433093
433056
  return root.endsWith("/v1") ? root.slice(0, -3) : root;
433094
433057
  };
433095
- var normalizeApiBase = (raw) => {
433058
+ var toPlexusApiBase = (raw) => {
433096
433059
  const root = normalizeConfigBaseUrl(raw);
433097
433060
  return root ? `${root}/v1` : "";
433098
433061
  };
@@ -433109,39 +433072,38 @@ function getConfigSync() {
433109
433072
  cachedConfig = {};
433110
433073
  return cachedConfig;
433111
433074
  }
433112
- async function saveBaseUrl(baseUrl, defaultModel) {
433075
+ async function saveBaseUrl(baseUrl) {
433113
433076
  await mkdir(getConfigDir(), { recursive: true });
433114
- const existing = getConfigSync();
433077
+ const { defaultModel: _defaultModel, ...existing } = getConfigSync();
433115
433078
  const config = {
433116
433079
  ...existing,
433117
- baseUrl: normalizeConfigBaseUrl(baseUrl),
433118
- ...defaultModel !== undefined && { defaultModel }
433080
+ baseUrl: normalizeConfigBaseUrl(baseUrl)
433119
433081
  };
433120
433082
  await writeFile(getConfigPath(), `${JSON.stringify(config, null, 2)}
433121
433083
  `, "utf8");
433122
433084
  cachedConfig = config;
433123
433085
  }
433124
- async function saveDefaultModel(defaultModel) {
433125
- await mkdir(getConfigDir(), { recursive: true });
433126
- const config = { ...getConfigSync(), defaultModel };
433127
- await writeFile(getConfigPath(), `${JSON.stringify(config, null, 2)}
433128
- `, "utf8");
433129
- cachedConfig = config;
433130
- }
433131
- function getRawBaseUrl() {
433086
+ function getBaseUrlResolution() {
433132
433087
  const config = getConfigSync();
433133
- return resolveStringOption(process.env[ENV_API_URL]) ?? resolveStringOption(process.env[ENV_BASE_URL]) ?? resolveStringOption(config.baseUrl) ?? null;
433088
+ const apiUrl = resolveStringOption(process.env[ENV_API_URL]);
433089
+ if (apiUrl)
433090
+ return { baseUrl: apiUrl, source: ENV_API_URL };
433091
+ const baseUrl = resolveStringOption(process.env[ENV_BASE_URL]);
433092
+ if (baseUrl)
433093
+ return { baseUrl, source: ENV_BASE_URL };
433094
+ const saved = resolveStringOption(config.baseUrl);
433095
+ return { baseUrl: saved ?? null, source: saved ? "saved" : "none" };
433134
433096
  }
433135
433097
  function getEnvApiKey() {
433136
433098
  return resolveStringOption(process.env[ENV_API_KEY]) ?? null;
433137
433099
  }
433138
433100
  function getModelsUrl() {
433139
- const raw = getRawBaseUrl();
433140
- return raw ? `${normalizeApiBase(raw)}/models` : null;
433101
+ const { baseUrl } = getBaseUrlResolution();
433102
+ return baseUrl ? `${toPlexusApiBase(baseUrl)}/models` : null;
433141
433103
  }
433142
433104
  function getBaseUrl() {
433143
- const raw = getRawBaseUrl();
433144
- return raw ? normalizeApiBase(raw) : null;
433105
+ const { baseUrl } = getBaseUrlResolution();
433106
+ return baseUrl ? toPlexusApiBase(baseUrl) : null;
433145
433107
  }
433146
433108
  function getSuppressedModels() {
433147
433109
  const config = getConfigSync();
@@ -433150,59 +433112,12 @@ function getSuppressedModels() {
433150
433112
  return [...envSuppressed, ...configSuppressed];
433151
433113
  }
433152
433114
 
433153
- // src/cache.ts
433154
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
433155
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
433115
+ // src/log.ts
433116
+ import { mkdir as mkdir2, appendFile } from "fs/promises";
433156
433117
  import { join as join2 } from "path";
433157
433118
  import { getAgentDir as getAgentDir2 } from "@oh-my-pi/pi-utils";
433158
433119
  var getCacheDir = () => join2(getAgentDir2(), "extensions", "plexus");
433159
- var getModelsCachePath = () => join2(getCacheDir(), "plexus-models-cache.json");
433160
- var getRawResponsePath = () => join2(getCacheDir(), "plexus-models-response.json");
433161
- function parseCacheData(raw) {
433162
- try {
433163
- const parsed = JSON.parse(raw);
433164
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
433165
- return null;
433166
- const obj = parsed;
433167
- if (!Array.isArray(obj["models"]))
433168
- return null;
433169
- return {
433170
- models: obj["models"],
433171
- timestamp: typeof obj["timestamp"] === "number" ? obj["timestamp"] : 0,
433172
- etag: typeof obj["etag"] === "string" ? obj["etag"] : undefined
433173
- };
433174
- } catch {
433175
- return null;
433176
- }
433177
- }
433178
- function readCachedModelsSync() {
433179
- try {
433180
- const p = getModelsCachePath();
433181
- if (!existsSync2(p))
433182
- return null;
433183
- return parseCacheData(readFileSync2(p, "utf8"));
433184
- } catch {
433185
- return null;
433186
- }
433187
- }
433188
- async function writeCachedModels(models, etag) {
433189
- await mkdir2(getCacheDir(), { recursive: true });
433190
- const payload = { models, timestamp: Date.now(), etag };
433191
- await writeFile2(getModelsCachePath(), `${JSON.stringify(payload, null, 2)}
433192
- `, "utf8");
433193
- }
433194
- async function writeRawResponse(data) {
433195
- await mkdir2(getCacheDir(), { recursive: true });
433196
- await writeFile2(getRawResponsePath(), `${JSON.stringify(data, null, 2)}
433197
- `, "utf8");
433198
- }
433199
-
433200
- // src/log.ts
433201
- import { mkdir as mkdir3, appendFile } from "fs/promises";
433202
- import { join as join3 } from "path";
433203
- import { getAgentDir as getAgentDir3 } from "@oh-my-pi/pi-utils";
433204
- var getCacheDir2 = () => join3(getAgentDir3(), "extensions", "plexus");
433205
- var getLogPath = () => join3(getCacheDir2(), "plexus.log");
433120
+ var getLogPath = () => join2(getCacheDir(), "plexus.log");
433206
433121
  var dirEnsured = false;
433207
433122
  function log(message, data) {
433208
433123
  writeLogLine(message, data);
@@ -433210,7 +433125,7 @@ function log(message, data) {
433210
433125
  async function writeLogLine(message, data) {
433211
433126
  try {
433212
433127
  if (!dirEnsured) {
433213
- await mkdir3(getCacheDir2(), { recursive: true });
433128
+ await mkdir2(getCacheDir(), { recursive: true });
433214
433129
  dirEnsured = true;
433215
433130
  }
433216
433131
  const ts = new Date().toISOString();
@@ -440221,22 +440136,16 @@ function getProviderApiKeyConfig() {
440221
440136
  return Bun.env[ENV_API_KEY]?.trim() ? { apiKey: ENV_API_KEY, authHeader: true } : {};
440222
440137
  }
440223
440138
  var currentModels = [];
440139
+ var catalogSource = "none";
440224
440140
  function plexusExtension(pi) {
440225
- const cached = readCachedModelsSync();
440226
- const suppressPatterns = getSuppressedModels();
440227
- const startupBaseUrl = getBaseUrl() ?? "http://localhost/v1";
440228
- const startupModels = (cached?.models ?? []).filter((m) => !isModelSuppressed({ id: m.id, name: m.name }, suppressPatterns)).map(descriptorToOhMyPiModel);
440229
- log("startup", {
440230
- cachedModelCount: startupModels.length,
440231
- startupBaseUrl
440232
- });
440141
+ const startupBaseUrl = getBaseUrl();
440142
+ log("startup", { catalogSource, startupBaseUrl });
440233
440143
  pi.registerProvider(PROVIDER_NAME, {
440234
440144
  api: "openai-completions",
440235
440145
  ...getProviderApiKeyConfig(),
440236
- ...startupModels.length > 0 ? { baseUrl: startupBaseUrl, models: startupModels } : {},
440146
+ ...startupBaseUrl ? { baseUrl: startupBaseUrl } : {},
440237
440147
  oauth: createPlexusLoginProvider(pi)
440238
440148
  });
440239
- currentModels = startupModels;
440240
440149
  pi.on("message_end", (event) => {
440241
440150
  normalizeProviderConnectionClosed(event.message, PROVIDER_NAME);
440242
440151
  });
@@ -440251,38 +440160,21 @@ function plexusExtension(pi) {
440251
440160
  await doRefresh(pi, apiKey, ctx);
440252
440161
  });
440253
440162
  pi.registerCommand("plexus", {
440254
- description: "Plexus provider commands: refresh, set-default-model (setup: /login plexus)",
440163
+ description: "Plexus provider commands: refresh, status (setup: /login plexus)",
440255
440164
  getArgumentCompletions: (prefix) => {
440256
440165
  const subcommands = [
440257
440166
  { value: "refresh", label: "refresh", description: "Refresh Plexus models from the API" },
440258
- { value: "set-default-model", label: "set-default-model", description: "Choose the model Oh My Pi should use by default" }
440167
+ { value: "status", label: "status", description: "Show Plexus configuration and catalog status" }
440259
440168
  ];
440260
- if (!prefix.includes(" ")) {
440261
- return subcommands.filter((command) => command.value.startsWith(prefix));
440262
- }
440263
- const [subcommand, ...rest] = prefix.split(/\s+/);
440264
- if (subcommand !== "set-default-model")
440265
- return null;
440266
- const modelPrefix = rest.join(" ");
440267
- const choices = currentModels.map((model) => ({
440268
- value: model.id,
440269
- label: model.name === model.id ? model.id : `${model.name} (${model.id})`
440270
- }));
440271
- const filtered = choices.filter((choice) => choice.value.toLowerCase().startsWith(modelPrefix.toLowerCase()));
440272
- return filtered.length > 0 ? filtered : null;
440169
+ return prefix.includes(" ") ? null : subcommands.filter((command) => command.value.startsWith(prefix));
440273
440170
  },
440274
440171
  handler: async (args, ctx) => {
440275
- const trimmed = args.trim();
440276
- const sub = trimmed.toLowerCase();
440277
- if (sub === "refresh" || sub === "") {
440278
- await handleRefresh(pi, ctx);
440279
- return;
440280
- }
440281
- if (sub === "set-default-model" || sub.startsWith("set-default-model ")) {
440282
- await handleSetDefaultModel(pi, ctx, trimmed.slice("set-default-model".length).trim());
440283
- return;
440284
- }
440285
- ctx.ui.notify(`Unknown sub-command: "${args}". Use /login plexus, /plexus refresh, or /plexus set-default-model.`, "warning");
440172
+ const sub = args.trim().toLowerCase();
440173
+ if (sub === "refresh" || sub === "")
440174
+ return handleRefresh(pi, ctx);
440175
+ if (sub === "status")
440176
+ return handleStatus(ctx);
440177
+ ctx.ui.notify(`Unknown sub-command: "${args}". Use /login plexus, /plexus refresh, or /plexus status.`, "warning");
440286
440178
  }
440287
440179
  });
440288
440180
  }
@@ -440315,29 +440207,16 @@ async function handleRefresh(pi, ctx) {
440315
440207
  ctx.ui.notify("Refreshing Plexus models\u2026", "info");
440316
440208
  await doRefresh(pi, apiKey, ctx);
440317
440209
  }
440318
- async function handleSetDefaultModel(pi, ctx, requestedModelId) {
440319
- let modelId = requestedModelId;
440320
- if (!modelId) {
440321
- if (currentModels.length === 0) {
440322
- ctx.ui.notify("No Plexus models are available. Run /plexus refresh first.", "warning");
440323
- return;
440324
- }
440325
- const choices = currentModels.map((model) => model.name === model.id ? model.id : `${model.name} (${model.id})`);
440326
- const selected = await ctx.ui.select("Select the Plexus default model:", choices);
440327
- if (!selected)
440328
- return;
440329
- const selectedIndex = choices.indexOf(selected);
440330
- modelId = currentModels[selectedIndex]?.id ?? "";
440331
- }
440332
- const model = currentModels.find((candidate) => candidate.id === modelId);
440333
- if (!model) {
440334
- ctx.ui.notify(`Plexus model not found: "${modelId}". Run /plexus refresh and choose a model from the available list.`, "error");
440335
- return;
440336
- }
440337
- await saveDefaultModel(model.id);
440338
- const registryModel = ctx.modelRegistry.find(PROVIDER_NAME, model.id) ?? model;
440339
- const active = await pi.setModel(registryModel);
440340
- ctx.ui.notify(active ? `Plexus model selected: ${model.id}.` : `Plexus model ${model.id} was saved but could not be selected in this session.`, active ? "info" : "warning");
440210
+ async function handleStatus(ctx) {
440211
+ const baseUrl = getBaseUrlResolution();
440212
+ const apiKey = await ctx.modelRegistry.authStorage.getApiKey(PROVIDER_NAME);
440213
+ ctx.ui.notify([
440214
+ `Plexus base URL: ${baseUrl.baseUrl ?? "not configured"} (${baseUrl.source})`,
440215
+ `API key: ${apiKey ? getEnvApiKey() ? "host credential or PLEXUS_API_KEY fallback" : "host credential" : "not configured"}`,
440216
+ `Catalog: ${currentModels.length} models (${catalogSource})`,
440217
+ "Default model: managed by OMP. Use /model or /models to save the selection there."
440218
+ ].join(`
440219
+ `), "info");
440341
440220
  }
440342
440221
  async function doRefresh(pi, apiKey, ctx) {
440343
440222
  const modelsUrl = getModelsUrl();
@@ -440349,19 +440228,12 @@ async function doRefresh(pi, apiKey, ctx) {
440349
440228
  return;
440350
440229
  }
440351
440230
  try {
440352
- const cached = readCachedModelsSync();
440353
- const { models: apiModels, raw, etag, notModified } = await fetchPlexusModels(apiKey, modelsUrl, undefined, cached?.etag);
440354
- if (notModified) {
440355
- log("doRefresh: not modified", { etag: cached?.etag });
440356
- if (ctx)
440357
- ctx.ui.notify(`Refreshed ${currentModels.length} Plexus models (not modified)`, "info");
440358
- return;
440359
- }
440231
+ const { models: apiModels } = await fetchPlexusModels(apiKey, modelsUrl);
440360
440232
  const suppressPatterns = getSuppressedModels();
440361
440233
  const descriptors = convertDescriptors(apiModels, baseUrl, suppressPatterns);
440362
440234
  const ohMyPiModels = descriptors.map(descriptorToOhMyPiModel);
440363
- await Promise.all([writeCachedModels(descriptors, etag), raw ? writeRawResponse(raw) : Promise.resolve()]);
440364
440235
  currentModels = ohMyPiModels;
440236
+ catalogSource = "live refresh";
440365
440237
  pi.registerProvider(PROVIDER_NAME, {
440366
440238
  api: "openai-completions",
440367
440239
  ...getProviderApiKeyConfig(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcowger/oh-my-pi-plexus",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Plexus AI proxy plugin for the Oh My Pi coding agent",
5
5
  "type": "module",
6
6
  "files": [