allye-pi 2.10.0 → 2.11.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.
package/README.md CHANGED
@@ -93,7 +93,7 @@ pi install npm:allye-pi@<version> # Pi
93
93
  omp plugin install allye-pi@<version> # OMP
94
94
  ```
95
95
 
96
- Use a pinned `<version>` (the guides resolve it for you). Then add the `allye` MCP server as described in [install-pi.md](docs/install-pi.md) or [install-omp.md](docs/install-omp.md).
96
+ Use a pinned `<version>` (the guides resolve it for you). Then add the `allye` MCP server as described in [install-pi.md](docs/install-pi.md) or [install-omp.md](docs/install-omp.md). In OMP, `allye-pi` shows one notice at session start with the exact command `/mcp add allye --scope user --url https://mcp.allye.app/mcp --transport http` when no `allye` server is found in OMP's own `mcp.json` files; it never writes configuration.
97
97
 
98
98
  ## Your organization's skills
99
99
 
@@ -48,6 +48,8 @@ OMP has a native MCP client (no `pi-mcp-adapter`). Add the server once, at user
48
48
  /mcp add allye --scope user --url https://mcp.allye.app/mcp --transport http
49
49
  ```
50
50
 
51
+ If the `allye` server is not found in OMP's own `mcp.json` files (user and project), the `allye-pi` extension shows one notice with this command when a session starts. It only reads those files, never writes them, and servers defined in other tools' configs are not checked.
52
+
51
53
  OMP also loads servers from a project `.mcp.json`. Do not add tokens or OAuth client metadata: OMP discovers the OAuth endpoints and opens the browser on first use.
52
54
 
53
55
  Because OMP's MCP client is not the in-process `pi-mcp-adapter` bridge (as in OMP today), the extension does not resolve the project. It injects only the git remote and any unverified `.allye/project.json` claim; the bootstrap tells the agent to call `projects.project_resolve` itself. The team follows the project, so no team selection is needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allye-pi",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Allye extension for Pi and OMP: the Allye bootstrap, the Bridge skill and the repository-to-project link",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1,6 +1,7 @@
1
- import { dirname, join, resolve } from "node:path";
1
+ import { homedir } from "node:os";
2
+ import { basename, dirname, join, resolve, sep } from "node:path";
2
3
  import { execFile } from "node:child_process";
3
- import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readSync } from "node:fs";
4
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readSync, realpathSync } from "node:fs";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
7
 
@@ -14,6 +15,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
15
  * `.allye/project.json` claim against the git remote. It loads no startup or
15
16
  * per-prompt memory context.
16
17
  * The team follows the project: nothing is gated on team selection.
18
+ * - In OMP, when no `allye` MCP server is configured, shows one notice with the
19
+ * `/mcp add` command. It is informational only and never writes configuration.
17
20
  */
18
21
 
19
22
  const MCP_SERVER = "allye";
@@ -21,6 +24,7 @@ const LINK_FILE = ".allye/project.json";
21
24
  const PROJECT_KEY = /^[A-Z][A-Z0-9]{1,9}$/;
22
25
  const APP_NAME = /^[A-Za-z0-9._-]{1,120}$/;
23
26
  const MAX_LINK_BYTES = 4096;
27
+ const MAX_MCP_CONFIG_BYTES = 256 * 1024;
24
28
  const MAX_CANDIDATES = 10;
25
29
  const MAX_REPO_CHARS = 4_000;
26
30
 
@@ -139,15 +143,15 @@ async function readRemote(cwd: string): Promise<string | null> {
139
143
  }
140
144
  }
141
145
 
142
- /** Reads at most MAX_LINK_BYTES from a regular file, refusing symlinks and FIFOs even if swapped in after lstat. */
143
- function readSmallRegularFile(path: string): string {
146
+ /** Reads at most `maxBytes` from a regular file, refusing symlinks and FIFOs even if swapped in after lstat. */
147
+ function readSmallRegularFile(path: string, maxBytes: number = MAX_LINK_BYTES): string {
144
148
  const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
145
149
  try {
146
150
  const stat = fstatSync(fd);
147
- if (!stat.isFile() || stat.size > MAX_LINK_BYTES) throw new Error("not a small regular file");
148
- const buffer = Buffer.alloc(MAX_LINK_BYTES + 1);
151
+ if (!stat.isFile() || stat.size > maxBytes) throw new Error("not a small regular file");
152
+ const buffer = Buffer.alloc(maxBytes + 1);
149
153
  const length = readSync(fd, buffer, 0, buffer.length, 0);
150
- if (length > MAX_LINK_BYTES) throw new Error("file too large");
154
+ if (length > maxBytes) throw new Error("file too large");
151
155
  return buffer.subarray(0, length).toString("utf8");
152
156
  } finally {
153
157
  closeSync(fd);
@@ -295,6 +299,73 @@ async function buildRepoContext(cwd: string, online: boolean): Promise<string> {
295
299
  }
296
300
  }
297
301
 
302
+ export const OMP_MCP_ADD_COMMAND = "/mcp add allye --scope user --url https://mcp.allye.app/mcp --transport http";
303
+ export const OMP_MCP_NOTICE = `The Allye MCP server was not found in OMP's own mcp.json files. If it is not configured, add it once with: ${OMP_MCP_ADD_COMMAND}`;
304
+
305
+ /** OMP reads MCP servers from <agentDir>/{mcp,.mcp}.json (user) and <cwd>/.omp/{mcp,.mcp}.json plus <cwd>/{mcp,.mcp}.json (project). */
306
+ export function ompAgentDir(env: NodeJS.ProcessEnv = process.env, home: string = homedir()): string {
307
+ return env.PI_CODING_AGENT_DIR && env.PI_CODING_AGENT_DIR.startsWith("/") ? env.PI_CODING_AGENT_DIR : join(home, ".omp", "agent");
308
+ }
309
+
310
+ export function ompMcpConfigPaths(cwd: string, env: NodeJS.ProcessEnv = process.env, home: string = homedir()): string[] {
311
+ const agentDir = ompAgentDir(env, home);
312
+ return [
313
+ join(agentDir, "mcp.json"),
314
+ join(agentDir, ".mcp.json"),
315
+ join(cwd, ".omp", "mcp.json"),
316
+ join(cwd, ".omp", ".mcp.json"),
317
+ join(cwd, ".mcp.json"),
318
+ join(cwd, "mcp.json"),
319
+ ];
320
+ }
321
+
322
+ /** True when running inside OMP (oh-my-pi): the executable is `omp` or the agent dir is OMP's `.omp/agent`. */
323
+ export function isOmp(env: NodeJS.ProcessEnv = process.env, argv: string[] = process.argv, execPath: string = process.execPath): boolean {
324
+ const names = [argv[1], execPath].map((value) => basename(value ?? "").replace(/\.(exe|js|mjs)$/, ""));
325
+ if (names.includes("omp")) return true;
326
+ const dir = env.PI_CODING_AGENT_DIR ?? "";
327
+ return /(^|\/)\.omp\/agent\/?$/.test(dir);
328
+ }
329
+
330
+ /**
331
+ * True when any readable config file defines the `allye` server. `readConfig` returns the file text or null
332
+ * when absent; unreadable or invalid files count as "not defined here". Never throws, never writes.
333
+ */
334
+ export { readConfigFile as readMcpConfigFile };
335
+
336
+ export function allyeMcpConfigured(paths: string[], readConfig: (path: string) => string | null): boolean {
337
+ for (const path of paths) {
338
+ try {
339
+ const text = readConfig(path);
340
+ if (text === null) continue;
341
+ const servers = (JSON.parse(text) as { mcpServers?: unknown } | null)?.mcpServers;
342
+ if (!servers || typeof servers !== "object" || !Object.prototype.hasOwnProperty.call(servers, MCP_SERVER)) continue;
343
+ const entry = (servers as Record<string, unknown>)[MCP_SERVER];
344
+ if (entry && typeof entry === "object" && (entry as { enabled?: unknown }).enabled === false) continue;
345
+ return true;
346
+ } catch {
347
+ continue;
348
+ }
349
+ }
350
+ return false;
351
+ }
352
+
353
+ /**
354
+ * Config paths are cwd-controlled: symlinks, non-regular and oversized files read as absent, and so does a file
355
+ * whose real path (a symlinked parent directory) leaves the real path of `base`.
356
+ */
357
+ function readConfigFile(path: string, base: string): string | null {
358
+ try {
359
+ const realBase = realpathSync(base);
360
+ const real = realpathSync(path);
361
+ if (real !== realBase && !real.startsWith(realBase.endsWith(sep) ? realBase : realBase + sep)) return null;
362
+ if (!lstatSync(path).isFile()) return null;
363
+ return readSmallRegularFile(path, MAX_MCP_CONFIG_BYTES);
364
+ } catch {
365
+ return null;
366
+ }
367
+ }
368
+
298
369
  export function buildSystemSections(bootstrap: string, repo: string, nativeBootstrap = true): string[] {
299
370
  const sections: string[] = [];
300
371
  if (bootstrap) sections.push(`<allye-bootstrap>\n${bootstrap.trim()}\n</allye-bootstrap>`);
@@ -306,11 +377,22 @@ export default function allyePiExtension(pi: ExtensionAPI): void {
306
377
  const nativeBootstrap = process.env.ALLYE_PI_NATIVE_BOOTSTRAP !== "0";
307
378
  let bootstrap = "";
308
379
  let repo = "";
380
+ let mcpNoticeShown = false;
309
381
 
310
382
  pi.on("resources_discover", () => ({ skillPaths: [skillsPath()] }));
311
383
 
312
384
  pi.on("session_start", async (_event, ctx) => {
313
385
  bootstrap = loadBootstrap();
386
+ if (!mcpNoticeShown && isOmp()) {
387
+ mcpNoticeShown = true;
388
+ try {
389
+ const cwd = ctx.cwd ?? process.cwd();
390
+ const agentDir = ompAgentDir();
391
+ if (!allyeMcpConfigured(ompMcpConfigPaths(cwd), (path) => readConfigFile(path, path.startsWith(agentDir + sep) ? agentDir : cwd))) ctx.ui?.notify?.(OMP_MCP_NOTICE, "warning");
392
+ } catch {
393
+ // The notice is best effort; never block the session.
394
+ }
395
+ }
314
396
  if (nativeBootstrap) repo = await loadRepoContext(ctx.cwd ?? process.cwd(), mcpBridgeAvailable());
315
397
  });
316
398