Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions agentsbox.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
"type": "object",
"additionalProperties": { "type": "string" },
"description": "Environment variables for the process. Use {env:VAR_NAME} for interpolation."
},
"inheritProcessEnv": {
"type": "boolean",
"default": true,
"description": "If true, pass the full parent process.env to the local server process (default: true)"
}
},
"required": ["type", "command"],
Expand Down
11 changes: 8 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@
},
"dependencies": {
"@mariozechner/pi-coding-agent": "^0.49.2",
"@modelcontextprotocol/sdk": "^1.25.1",
"@modelcontextprotocol/sdk": "^1.25.3",
"@opencode-ai/plugin": "^1.1.6",
"@sinclair/typebox": "^0.34.41",
"jsonc-parser": "^3.3.1",
"safe-regex2": "^5.0.0",
"zod": "^4.3.5"
}
}
5 changes: 4 additions & 1 deletion scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ console.log("Building project...");
// Externalize native modules to prevent bundling issues
// - @mariozechner/clipboard-*: platform specific clipboard binaries
// - @silvia-odwyer/photon-node: native image processing addon
await $`bun build src/index.ts src/opencode.ts src/pi.ts src/cli.ts --outdir dist --target node --external '@mariozechner/clipboard-*' --external '@silvia-odwyer/photon-node'`;
await $`bun build src/index.ts src/opencode.ts src/pi.ts src/phi.ts src/cli.ts --outdir dist --target node --external '@mariozechner/clipboard-*' --external '@silvia-odwyer/photon-node'`;

console.log("Preparing pi extension...");
await $`bun scripts/prepare-pi-extension.ts`;

console.log("Preparing phi extension...");
await $`bun scripts/prepare-phi-extension.ts`;

console.log("Build complete.");
53 changes: 53 additions & 0 deletions scripts/prepare-phi-extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bun

import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";

/**
* Prepares dist/phi-extension/ so it can be symlinked into phi's extensions dir.
*
* Goals:
* - no POSIX shell dependencies (mkdir -p / ln -sf / echo >)
* - idempotent + fails build on errors
*/
async function main() {
const pkgRoot = process.cwd();

const distDir = join(pkgRoot, "dist");
const extDir = join(distDir, "phi-extension");
const pkgJsonPath = join(extDir, "package.json");
const indexJsPath = join(extDir, "index.js");

await mkdir(extDir, { recursive: true });

// Ensure we never leave a dangling symlink behind from previous builds.
// (On Windows, symlink creation may require elevated privileges.)
await rm(indexJsPath, { force: true });

// Minimal package.json: ensure ESM semantics for index.js.
await writeFile(pkgJsonPath, '{"type":"module"}\n', "utf8");

// Avoid symlink entirely for portability: create a tiny re-export shim.
// This replaces the previous `ln -sf ../phi.js dist/phi-extension/index.js`.
// NOTE: We resolve realpath to ensure symlinked extension dirs can find dist/phi.js.
await writeFile(
indexJsPath,
`import { realpath } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

const here = dirname(fileURLToPath(import.meta.url));
const realHere = await realpath(here);
const targetUrl = pathToFileURL(join(realHere, "..", "phi.js")).href;
const mod = await import(targetUrl);

export default mod.default;
`,
"utf8",
);
}

main().catch((err) => {
console.error(err);
process.exitCode = 1;
});
7 changes: 6 additions & 1 deletion src/catalog/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ function buildSearchableText(
}
}

return parts.join(" ");
const text = parts.join(" ");

// Bound match input size to mitigate worst-case regex/runtime behavior.
// (Regex DoS can happen even with short patterns if the input is huge.)
const MAX_SEARCHABLE_TEXT = 4000;
return text.length > MAX_SEARCHABLE_TEXT ? text.slice(0, MAX_SEARCHABLE_TEXT) : text;
Comment on lines +89 to +94
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_SEARCHABLE_TEXT is reallocated on every call. Since it's a constant policy value, consider moving it to module scope (or a shared constants module) to avoid repeated allocation and to make the limit easier to reuse/tune.

Copilot uses AI. Check for mistakes.
}

/**
Expand Down
67 changes: 44 additions & 23 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Usage:
agentsbox init [--dry-run] [--force]
agentsbox setup opencode [--dry-run] [--force]
agentsbox setup pi [--dry-run] [--force]
agentsbox setup phi [--dry-run] [--force]

Commands:
init
Expand All @@ -58,11 +59,15 @@ Commands:
setup pi
Install agentsbox as a local pi extension (auto-discovered).

setup phi
Install agentsbox as a local phi extension (auto-discovered).

Defaults:
agentsbox config dir: ~/.config/agentsbox
OpenCode plugins dir: ~/.config/opencode/plugins
pi extensions dir: ~/.pi/agent/extensions
pi skills dir: ~/.pi/agent/skills
phi extensions dir: ~/.phi/agent/extensions
skills dir: ~/.agents/skills

Options:
--dry-run Print planned filesystem changes only
Expand Down Expand Up @@ -565,50 +570,51 @@ async function planSetupOpencode(opts: {
return actions;
}

async function planSetupPi(opts: {
async function planSetupAgent(opts: {
agentName: "pi" | "phi";
configDir: string;
force: boolean;
pkgRoot: string;
piExtensionsDir: string;
piSkillsDir: string;
extensionsDir: string;
sharedSkillsDir: string;
}): Promise<Action[]> {
const { configDir, force, pkgRoot, piExtensionsDir, piSkillsDir } = opts;
const { agentName, configDir, force, pkgRoot, extensionsDir, sharedSkillsDir } = opts;

const actions: Action[] = [];

// dist/pi.js is a fully-bundled entrypoint — no wrapper needed.
const srcPiEntrypoint = join(pkgRoot, "dist", "pi.js");
actions.push({ kind: "assert-exists", path: srcPiEntrypoint, hint: "Run: bun run build" });
// dist/{agent}.js is a fully-bundled entrypoint — no wrapper needed.
const srcEntrypoint = join(pkgRoot, "dist", `${agentName}.js`);
actions.push({ kind: "assert-exists", path: srcEntrypoint, hint: "Run: bun run build" });

// Ensure agentsbox config + skill
actions.push(...(await planInit({ configDir, force, pkgRoot })));

// Clean up legacy flat symlink (pre-v0.3 created agentsbox.js directly).
const legacyFlatSymlink = join(piExtensionsDir, "agentsbox.js");
const legacyFlatSymlink = join(extensionsDir, "agentsbox.js");
actions.push({ kind: "remove", path: legacyFlatSymlink, onlyIfSymlink: true });

// Symlink entire extension directory: ~/.pi/agent/extensions/agentsbox/ → dist/pi-extension/
// dist/pi-extension/ contains: package.json (type:module) + index.js → ../pi.js
const srcExtDir = join(pkgRoot, "dist", "pi-extension");
// Symlink entire extension directory: ~/.{agent}/agent/extensions/agentsbox/ → dist/{agent}-extension/
// dist/{agent}-extension/ contains: package.json (type:module) + index.js → ../{agent}.js
const srcExtDir = join(pkgRoot, "dist", `${agentName}-extension`);
actions.push({ kind: "assert-exists", path: srcExtDir, hint: "Run: bun run build" });

actions.push({ kind: "mkdir", path: piExtensionsDir });
actions.push({ kind: "mkdir", path: extensionsDir });
actions.push({
kind: "symlink",
from: srcExtDir,
to: join(piExtensionsDir, "agentsbox"),
to: join(extensionsDir, "agentsbox"),
mode: force ? "overwrite" : "link-if-different",
});

// Symlink skill into pi's skill discovery path (~/.pi/agent/skills/agentsbox).
// Pi discovers skills from ~/.pi/agent/skills/**/SKILL.md recursively.
// Symlink skill into shared skills dir (~/.agents/skills/agentsbox).
// As of Feb 2026, all coding agents use this unified location.
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time-anchored comments like "As of Feb 2026" can become stale quickly and are harder to evaluate later. Consider rephrasing to a timeless rationale (e.g., "All supported agents use this unified location") or linking to the source of truth (docs/spec) if available.

Suggested change
// As of Feb 2026, all coding agents use this unified location.
// All supported coding agents use this unified location for shared skills.

Copilot uses AI. Check for mistakes.
const destSkillDir = join(configDir, "skill", "agentsbox");
const piSkillLink = join(piSkillsDir, "agentsbox");
actions.push({ kind: "mkdir", path: piSkillsDir });
const skillLink = join(sharedSkillsDir, "agentsbox");
actions.push({ kind: "mkdir", path: sharedSkillsDir });
actions.push({
kind: "symlink",
from: destSkillDir,
to: piSkillLink,
to: skillLink,
mode: force ? "overwrite" : "link-if-different",
});

Expand All @@ -632,7 +638,8 @@ async function main() {
const configDir = join(xdgConfigHome, "agentsbox");
const opencodePluginsDir = join(xdgConfigHome, "opencode", "plugins");
const piExtensionsDir = join(homedir(), ".pi", "agent", "extensions");
const piSkillsDir = join(homedir(), ".pi", "agent", "skills");
const phiExtensionsDir = join(homedir(), ".phi", "agent", "extensions");
const sharedSkillsDir = join(homedir(), ".agents", "skills");

if (cmd === "init") {
const actions = await planInit({ configDir, force, pkgRoot });
Expand All @@ -650,12 +657,26 @@ async function main() {
}

if (target === "pi") {
const actions = await planSetupPi({
const actions = await planSetupAgent({
agentName: "pi",
configDir,
force,
pkgRoot,
extensionsDir: piExtensionsDir,
sharedSkillsDir,
});
await applyPlannedActions(actions, { dryRun, force });
return;
}

if (target === "phi") {
const actions = await planSetupAgent({
agentName: "phi",
configDir,
force,
pkgRoot,
piExtensionsDir,
piSkillsDir,
extensionsDir: phiExtensionsDir,
sharedSkillsDir,
});
await applyPlannedActions(actions, { dryRun, force });
return;
Expand Down
6 changes: 6 additions & 0 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const LocalServerConfigSchema = z.object({
.record(z.string(), z.string())
.optional()
.describe("Environment variables for the process"),
inheritProcessEnv: z
.boolean()
.default(true)
.describe(
"If true, pass the full parent process.env to the local server process (default: true)",
),
});

/**
Expand Down
35 changes: 33 additions & 2 deletions src/mcp-client/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import type { LocalMCPServerConfig, MCPClient } from "./types";

const MINIMAL_ENV_KEYS = [
// Common
"PATH",
"HOME",
"USER",
"LOGNAME",
"SHELL",
"TMPDIR",
"TMP",
"TEMP",
// Windows (harmless on unix)
"SystemRoot",
"WINDIR",
"ComSpec",
"PATHEXT",
];

function getMinimalProcessEnv(): Record<string, string> {
const out: Record<string, string> = {};
for (const k of MINIMAL_ENV_KEYS) {
const v = process.env[k];
if (typeof v === "string") out[k] = v;
}
return out;
}

/**
* Transport-like interface for DI/testing
*/
Expand Down Expand Up @@ -65,12 +91,17 @@ export class LocalMCPClient implements MCPClient {
throw new Error(`Local MCP server ${this.name} has no command`);
}

const baseEnv =
this.config.inheritProcessEnv === false
? getMinimalProcessEnv()
: (process.env as Record<string, string>);

this.transport = this.transportFactory({
command: this.config.command[0]!,
args: this.config.command.slice(1),
env: {
...(process.env as Record<string, string>),
...this.config.environment,
...baseEnv,
...(this.config.environment ?? {}),
},
stderr: "pipe" as const,
});
Expand Down
Loading
Loading