agent-trellis 0.13.0 → 0.14.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/dist/adapters/pi.d.ts +46 -7
- package/dist/adapters/pi.js +151 -8
- package/dist/adapters/piMcp.d.ts +35 -0
- package/dist/adapters/piMcp.js +58 -0
- package/dist/commands/mcp.js +6 -0
- package/dist/probes/pi.d.ts +7 -4
- package/dist/probes/pi.js +16 -11
- package/docs/getting-started.md +22 -0
- package/docs/mcp-oauth-renewal.md +3 -2
- package/docs/research.md +7 -4
- package/package.json +1 -1
package/dist/adapters/pi.d.ts
CHANGED
|
@@ -3,22 +3,61 @@
|
|
|
3
3
|
* symlinked to canonical `agents.md` — specifically `AGENTS.md`, not
|
|
4
4
|
* `AGENTS.override.md` (design.md D3, trellis-sync-p1: pi checks the
|
|
5
5
|
* override name first, and Trellis's managed file is the baseline, not an
|
|
6
|
-
* override of something else).
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* override of something else).
|
|
7
|
+
*
|
|
8
|
+
* MCP delivery is version-gated (trellis-pi-native-mcp-migration):
|
|
9
|
+
*
|
|
10
|
+
* - pi >= 1.1.0 has a native MCP client, so `trellis mcp sync` merges
|
|
11
|
+
* Trellis-managed entries into `~/.pi/agent/mcp.json` through the shared
|
|
12
|
+
* `planJsonMcp`/`applyJsonMcp` machinery (ownership ledger, conflict
|
|
13
|
+
* detection, user entries preserved) with the pi-shaped renderer from
|
|
14
|
+
* `piMcp.ts`. Gateway route → the single `trellis` runtime entry;
|
|
15
|
+
* agent-owned OAuth servers → bare `{url}` entries the user authorizes
|
|
16
|
+
* with `pi mcp login`.
|
|
17
|
+
* - Older pis (or an unprobeable version) keep the legacy delivery: a
|
|
18
|
+
* single symlinked bridge extension (`dist/pi-bridge/bundle.js`,
|
|
19
|
+
* trellis-pi-mcp-bridge-p4). The version gate never moves a machine
|
|
20
|
+
* FROM the bridge TO native until the mcp.json write succeeds — the
|
|
21
|
+
* bridge-removal item rides along in the same plan, ordered after the
|
|
22
|
+
* config write, and is withheld entirely when any write conflicts
|
|
23
|
+
* (design.md D3: the two deliveries may never be active at once, every
|
|
24
|
+
* tool would register twice).
|
|
11
25
|
*/
|
|
12
26
|
import type { AdapterPlanItem, AdapterProbeResult, AdapterVerifyResult, TrellisAdapter } from "../core/adapter.js";
|
|
13
27
|
import type { CanonicalSource } from "../core/types.js";
|
|
14
|
-
import type
|
|
28
|
+
import { type BackupSession } from "../lib/backup.js";
|
|
15
29
|
export declare class PiAdapter implements TrellisAdapter {
|
|
16
30
|
private readonly homeDir;
|
|
31
|
+
/** Test seam: the version gate must not depend on whatever pi happens
|
|
32
|
+
* to be installed on the machine running the tests. */
|
|
33
|
+
private readonly versionProbe;
|
|
17
34
|
readonly name = "pi";
|
|
18
35
|
readonly id: "pi";
|
|
19
|
-
constructor(homeDir?: string
|
|
36
|
+
constructor(homeDir?: string,
|
|
37
|
+
/** Test seam: the version gate must not depend on whatever pi happens
|
|
38
|
+
* to be installed on the machine running the tests. */
|
|
39
|
+
versionProbe?: () => string | undefined);
|
|
20
40
|
probe(): Promise<AdapterProbeResult>;
|
|
21
41
|
plan(canonical: CanonicalSource): Promise<AdapterPlanItem[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Native-delivery plan (design.md D1/D3). The write items come from the
|
|
44
|
+
* shared JSON machinery unchanged; the bridge-removal item is appended
|
|
45
|
+
* last and re-cast to kind "mcp" so it travels through `trellis mcp
|
|
46
|
+
* sync` — the same command that writes mcp.json — never through bare
|
|
47
|
+
* `trellis sync`. That is what makes the migration atomic from the
|
|
48
|
+
* user's point of view: the config lands and the bridge leaves in one
|
|
49
|
+
* apply, and a bare skills/instructions sync can neither remove the
|
|
50
|
+
* bridge early nor resurrect it afterwards.
|
|
51
|
+
*/
|
|
52
|
+
private planNativeMcp;
|
|
53
|
+
/**
|
|
54
|
+
* Removal ownership for the bridge, mirroring `planSymlinks`' rule: the
|
|
55
|
+
* raw readlink target must resolve under this installation's
|
|
56
|
+
* `dist/pi-bridge/` directory, or be a byte-identical bundle at the
|
|
57
|
+
* Trellis artifact shape left by a previous install root. Anything else
|
|
58
|
+
* is the user's and stays.
|
|
59
|
+
*/
|
|
60
|
+
private isOwnedBridgeSymlink;
|
|
22
61
|
apply(plan: AdapterPlanItem[], backup: BackupSession): Promise<void>;
|
|
23
62
|
verify(canonical: CanonicalSource): Promise<AdapterVerifyResult>;
|
|
24
63
|
}
|
package/dist/adapters/pi.js
CHANGED
|
@@ -3,20 +3,39 @@
|
|
|
3
3
|
* symlinked to canonical `agents.md` — specifically `AGENTS.md`, not
|
|
4
4
|
* `AGENTS.override.md` (design.md D3, trellis-sync-p1: pi checks the
|
|
5
5
|
* override name first, and Trellis's managed file is the baseline, not an
|
|
6
|
-
* override of something else).
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* override of something else).
|
|
7
|
+
*
|
|
8
|
+
* MCP delivery is version-gated (trellis-pi-native-mcp-migration):
|
|
9
|
+
*
|
|
10
|
+
* - pi >= 1.1.0 has a native MCP client, so `trellis mcp sync` merges
|
|
11
|
+
* Trellis-managed entries into `~/.pi/agent/mcp.json` through the shared
|
|
12
|
+
* `planJsonMcp`/`applyJsonMcp` machinery (ownership ledger, conflict
|
|
13
|
+
* detection, user entries preserved) with the pi-shaped renderer from
|
|
14
|
+
* `piMcp.ts`. Gateway route → the single `trellis` runtime entry;
|
|
15
|
+
* agent-owned OAuth servers → bare `{url}` entries the user authorizes
|
|
16
|
+
* with `pi mcp login`.
|
|
17
|
+
* - Older pis (or an unprobeable version) keep the legacy delivery: a
|
|
18
|
+
* single symlinked bridge extension (`dist/pi-bridge/bundle.js`,
|
|
19
|
+
* trellis-pi-mcp-bridge-p4). The version gate never moves a machine
|
|
20
|
+
* FROM the bridge TO native until the mcp.json write succeeds — the
|
|
21
|
+
* bridge-removal item rides along in the same plan, ordered after the
|
|
22
|
+
* config write, and is withheld entirely when any write conflicts
|
|
23
|
+
* (design.md D3: the two deliveries may never be active at once, every
|
|
24
|
+
* tool would register twice).
|
|
11
25
|
*/
|
|
26
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync } from "node:fs";
|
|
12
27
|
import { dirname, join, sep } from "node:path";
|
|
13
28
|
import { fileURLToPath } from "node:url";
|
|
14
29
|
import { homedir } from "node:os";
|
|
15
|
-
import { readFileSync } from "node:fs";
|
|
16
30
|
import { isInScope } from "../core/adapter.js";
|
|
17
31
|
import { usesNativeCapabilityDelivery } from "../core/types.js";
|
|
18
32
|
import * as piProbe from "../probes/pi.js";
|
|
33
|
+
import { probePiVersion } from "../probes/pi.js";
|
|
19
34
|
import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
|
|
35
|
+
import { applyJsonMcp, planJsonMcp } from "./jsonMcp.js";
|
|
36
|
+
import { piRenderer, supportsNativeMcp } from "./piMcp.js";
|
|
37
|
+
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
38
|
+
import { currentLinkTarget } from "../lib/backup.js";
|
|
20
39
|
const BRIDGE_SYMLINK_NAME = "trellis-mcp-bridge.js";
|
|
21
40
|
/**
|
|
22
41
|
* A source-checkout bridge can survive a switch to the packaged CLI. Adopt it
|
|
@@ -61,10 +80,15 @@ function resolveBridgeFile() {
|
|
|
61
80
|
}
|
|
62
81
|
export class PiAdapter {
|
|
63
82
|
homeDir;
|
|
83
|
+
versionProbe;
|
|
64
84
|
name = "pi";
|
|
65
85
|
id = "pi";
|
|
66
|
-
constructor(homeDir = homedir()
|
|
86
|
+
constructor(homeDir = homedir(),
|
|
87
|
+
/** Test seam: the version gate must not depend on whatever pi happens
|
|
88
|
+
* to be installed on the machine running the tests. */
|
|
89
|
+
versionProbe = probePiVersion) {
|
|
67
90
|
this.homeDir = homeDir;
|
|
91
|
+
this.versionProbe = versionProbe;
|
|
68
92
|
}
|
|
69
93
|
async probe() {
|
|
70
94
|
const snapshot = await piProbe.probe(this.homeDir);
|
|
@@ -88,6 +112,9 @@ export class PiAdapter {
|
|
|
88
112
|
canonicalRoot,
|
|
89
113
|
kind: "instructions",
|
|
90
114
|
});
|
|
115
|
+
if (supportsNativeMcp(this.versionProbe())) {
|
|
116
|
+
return [...skillItems, ...instructionsItems, ...this.planNativeMcp(canonical)];
|
|
117
|
+
}
|
|
91
118
|
const { file: bridgeFile, symlinkName } = resolveBridgeFile();
|
|
92
119
|
const extensionItems = planSymlinks({
|
|
93
120
|
rootDir: join(agentDir, "extensions"),
|
|
@@ -101,8 +128,104 @@ export class PiAdapter {
|
|
|
101
128
|
});
|
|
102
129
|
return [...skillItems, ...instructionsItems, ...extensionItems];
|
|
103
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Native-delivery plan (design.md D1/D3). The write items come from the
|
|
133
|
+
* shared JSON machinery unchanged; the bridge-removal item is appended
|
|
134
|
+
* last and re-cast to kind "mcp" so it travels through `trellis mcp
|
|
135
|
+
* sync` — the same command that writes mcp.json — never through bare
|
|
136
|
+
* `trellis sync`. That is what makes the migration atomic from the
|
|
137
|
+
* user's point of view: the config lands and the bridge leaves in one
|
|
138
|
+
* apply, and a bare skills/instructions sync can neither remove the
|
|
139
|
+
* bridge early nor resurrect it afterwards.
|
|
140
|
+
*/
|
|
141
|
+
planNativeMcp(canonical) {
|
|
142
|
+
const agentDir = join(this.homeDir, ".pi", "agent");
|
|
143
|
+
const configPath = join(agentDir, "mcp.json");
|
|
144
|
+
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
145
|
+
const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
|
|
146
|
+
const items = planJsonMcp({
|
|
147
|
+
configPath,
|
|
148
|
+
parsed,
|
|
149
|
+
mcp: canonical.mcp,
|
|
150
|
+
agentId: this.id,
|
|
151
|
+
managedAgents: canonical.managedAgents,
|
|
152
|
+
policy: canonical.secretsPolicy,
|
|
153
|
+
ownership,
|
|
154
|
+
render: piRenderer,
|
|
155
|
+
}).map((item) => {
|
|
156
|
+
// pi speaks stdio and streamable HTTP only — an sse server gets an
|
|
157
|
+
// honest conflict instead of an entry pi could never connect.
|
|
158
|
+
const write = item.mcpWrite;
|
|
159
|
+
if (write?.def.transport !== "sse")
|
|
160
|
+
return item;
|
|
161
|
+
return {
|
|
162
|
+
action: "conflict",
|
|
163
|
+
kind: "mcp",
|
|
164
|
+
target: item.target,
|
|
165
|
+
description: `MCP server "${write.name}" uses the sse transport, which pi does not support (stdio and streamable HTTP only) — no entry written to ${item.target}`,
|
|
166
|
+
remediation: "switch the server to streamable HTTP or stdio, or scope it away from pi",
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
const bridgePath = join(agentDir, "extensions", BRIDGE_SYMLINK_NAME);
|
|
170
|
+
const hasConflict = items.some((item) => item.action === "conflict");
|
|
171
|
+
if (!hasConflict && this.isOwnedBridgeSymlink(bridgePath)) {
|
|
172
|
+
items.push({
|
|
173
|
+
action: "remove",
|
|
174
|
+
kind: "mcp",
|
|
175
|
+
target: bridgePath,
|
|
176
|
+
description: `remove Trellis MCP bridge extension ${bridgePath} — pi >= 1.1.0 receives MCP natively via mcp.json`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return items;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Removal ownership for the bridge, mirroring `planSymlinks`' rule: the
|
|
183
|
+
* raw readlink target must resolve under this installation's
|
|
184
|
+
* `dist/pi-bridge/` directory, or be a byte-identical bundle at the
|
|
185
|
+
* Trellis artifact shape left by a previous install root. Anything else
|
|
186
|
+
* is the user's and stays.
|
|
187
|
+
*/
|
|
188
|
+
isOwnedBridgeSymlink(bridgePath) {
|
|
189
|
+
const stat = lstatSync(bridgePath, { throwIfNoEntry: false });
|
|
190
|
+
if (!stat?.isSymbolicLink())
|
|
191
|
+
return false;
|
|
192
|
+
const rawTarget = readlinkSync(bridgePath);
|
|
193
|
+
const { file } = resolveBridgeFile();
|
|
194
|
+
const bridgeRoot = dirname(file);
|
|
195
|
+
const resolved = rawTarget.startsWith(sep) ? rawTarget : join(dirname(bridgePath), rawTarget);
|
|
196
|
+
if (resolved === file || resolved.startsWith(bridgeRoot.endsWith(sep) ? bridgeRoot : `${bridgeRoot}${sep}`))
|
|
197
|
+
return true;
|
|
198
|
+
return isEquivalentTrellisBridge(resolved, file);
|
|
199
|
+
}
|
|
104
200
|
async apply(plan, backup) {
|
|
105
|
-
await applySymlinkPlan(plan, backup);
|
|
201
|
+
await applySymlinkPlan(plan.filter((item) => item.kind !== "mcp"), backup);
|
|
202
|
+
// Real config writes carry mcpWrite/mcpRemove; the bridge-removal
|
|
203
|
+
// item is the only kind:"mcp" entry without either, and it must run
|
|
204
|
+
// strictly after the config write (design.md D3).
|
|
205
|
+
const mcpWrites = plan.filter((item) => item.kind === "mcp" && (item.mcpWrite || item.mcpRemove));
|
|
206
|
+
const bridgeRemovals = plan.filter((item) => item.kind === "mcp" && item.action === "remove" && !item.mcpWrite && !item.mcpRemove);
|
|
207
|
+
if (mcpWrites.length > 0) {
|
|
208
|
+
const configPath = mcpWrites[0].target;
|
|
209
|
+
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
210
|
+
const merged = applyJsonMcp(parsed, mcpWrites, piRenderer);
|
|
211
|
+
backup.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
212
|
+
let ownership = loadMcpOwnership(this.homeDir);
|
|
213
|
+
for (const item of mcpWrites) {
|
|
214
|
+
if (item.action === "create" && item.mcpWrite) {
|
|
215
|
+
ownership = recordOwned(ownership, this.id, item.mcpWrite.name, piRenderer(item.mcpWrite.def, item.mcpWrite.name));
|
|
216
|
+
}
|
|
217
|
+
else if (item.action === "remove" && item.mcpRemove) {
|
|
218
|
+
ownership = forgetOwned(ownership, this.id, item.mcpRemove.name);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
saveMcpOwnership(this.homeDir, ownership);
|
|
222
|
+
}
|
|
223
|
+
for (const item of bridgeRemovals) {
|
|
224
|
+
const oldTarget = await currentLinkTarget(item.target);
|
|
225
|
+
if (oldTarget === undefined)
|
|
226
|
+
continue; // already gone — nothing to record
|
|
227
|
+
await backup.removeSymlink(item.target, oldTarget);
|
|
228
|
+
}
|
|
106
229
|
}
|
|
107
230
|
async verify(canonical) {
|
|
108
231
|
const snapshot = await piProbe.probe(this.homeDir);
|
|
@@ -122,6 +245,26 @@ export class PiAdapter {
|
|
|
122
245
|
mismatches.push(`skill "${name}" is present on disk but not in canonical scope for pi`);
|
|
123
246
|
}
|
|
124
247
|
}
|
|
248
|
+
if (supportsNativeMcp(this.versionProbe())) {
|
|
249
|
+
// Native mode drift: anything the plan still wants to change is by
|
|
250
|
+
// definition drift. The plan only ever names Trellis-owned entries
|
|
251
|
+
// (the ownership ledger gates removals), so user-authored servers
|
|
252
|
+
// can never surface here.
|
|
253
|
+
for (const item of this.planNativeMcp(canonical)) {
|
|
254
|
+
if (item.action === "create" && item.mcpWrite) {
|
|
255
|
+
mismatches.push(`mcp.json entry "${item.mcpWrite.name}" is missing or differs from canonical — run trellis mcp sync`);
|
|
256
|
+
}
|
|
257
|
+
else if (item.action === "remove" && item.mcpRemove) {
|
|
258
|
+
mismatches.push(`mcp.json entry "${item.mcpRemove.name}" is Trellis-owned but no longer in canonical — run trellis mcp sync`);
|
|
259
|
+
}
|
|
260
|
+
else if (item.action === "remove") {
|
|
261
|
+
mismatches.push(`the Trellis MCP bridge extension is still installed but pi >= 1.1.0 uses native mcp.json — run trellis mcp sync`);
|
|
262
|
+
}
|
|
263
|
+
else if (item.action === "conflict") {
|
|
264
|
+
mismatches.push(item.description);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
125
268
|
return mismatches.length === 0 ? { ok: true } : { ok: false, mismatches };
|
|
126
269
|
}
|
|
127
270
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi's native MCP config (pi >= 1.1.0) — the entry renderer and the
|
|
3
|
+
* version gate that chooses between it and the legacy bridge extension
|
|
4
|
+
* (openspec/changes/pi-native-mcp-migration design.md D1/D2).
|
|
5
|
+
*
|
|
6
|
+
* pi's mcp.json has no `type` discriminator: an entry is stdio when it
|
|
7
|
+
* carries `command`, streamable HTTP when it carries `url`. `${VAR}`
|
|
8
|
+
* references in `env`/`headers` values are expanded by pi at spawn
|
|
9
|
+
* (verified empirically against pi 1.1.0 on 2026-10-11), so the
|
|
10
|
+
* zero-plaintext rendering every other JSON agent relies on works
|
|
11
|
+
* unchanged. sse is not a pi transport — the adapter turns sse servers
|
|
12
|
+
* into conflict items rather than writing entries pi cannot connect.
|
|
13
|
+
*/
|
|
14
|
+
import type { McpServerDef } from "../core/types.js";
|
|
15
|
+
/** The pi release that introduced the native MCP client (`pi mcp`,
|
|
16
|
+
* `~/.pi/agent/mcp.json`). */
|
|
17
|
+
export declare const PI_NATIVE_MCP_SINCE = "1.1.0";
|
|
18
|
+
/**
|
|
19
|
+
* Version gate (design.md D2): native delivery only when the probed
|
|
20
|
+
* version is known to understand mcp.json. Anything unparseable —
|
|
21
|
+
* including "binary not on PATH" (undefined) — falls back to the bridge,
|
|
22
|
+
* never the other way round: writing config a pi cannot read is worse
|
|
23
|
+
* than keeping a working bridge.
|
|
24
|
+
*/
|
|
25
|
+
export declare function supportsNativeMcp(version: string | undefined): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* pi-shaped rendering of a canonical server definition — the same shape
|
|
28
|
+
* `pi mcp add` writes (observed 2026-10-11): stdio as
|
|
29
|
+
* `{command, args, env?}`, http as `{url, headers?}`, no `type` field.
|
|
30
|
+
* `env` names render as `${VAR}` references exactly like the other JSON
|
|
31
|
+
* agents; OAuth `auth` metadata is Trellis-side routing information and
|
|
32
|
+
* is never rendered (an agent-owned OAuth server becomes a bare `{url}`
|
|
33
|
+
* entry that `pi mcp login <name>` authorizes into pi's own token store).
|
|
34
|
+
*/
|
|
35
|
+
export declare function piRenderer(def: McpServerDef, _name: string): Record<string, unknown>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi's native MCP config (pi >= 1.1.0) — the entry renderer and the
|
|
3
|
+
* version gate that chooses between it and the legacy bridge extension
|
|
4
|
+
* (openspec/changes/pi-native-mcp-migration design.md D1/D2).
|
|
5
|
+
*
|
|
6
|
+
* pi's mcp.json has no `type` discriminator: an entry is stdio when it
|
|
7
|
+
* carries `command`, streamable HTTP when it carries `url`. `${VAR}`
|
|
8
|
+
* references in `env`/`headers` values are expanded by pi at spawn
|
|
9
|
+
* (verified empirically against pi 1.1.0 on 2026-10-11), so the
|
|
10
|
+
* zero-plaintext rendering every other JSON agent relies on works
|
|
11
|
+
* unchanged. sse is not a pi transport — the adapter turns sse servers
|
|
12
|
+
* into conflict items rather than writing entries pi cannot connect.
|
|
13
|
+
*/
|
|
14
|
+
import { resolvedEnvTextMap } from "../lib/mcpMigrateRead.js";
|
|
15
|
+
/** The pi release that introduced the native MCP client (`pi mcp`,
|
|
16
|
+
* `~/.pi/agent/mcp.json`). */
|
|
17
|
+
export const PI_NATIVE_MCP_SINCE = "1.1.0";
|
|
18
|
+
/**
|
|
19
|
+
* Version gate (design.md D2): native delivery only when the probed
|
|
20
|
+
* version is known to understand mcp.json. Anything unparseable —
|
|
21
|
+
* including "binary not on PATH" (undefined) — falls back to the bridge,
|
|
22
|
+
* never the other way round: writing config a pi cannot read is worse
|
|
23
|
+
* than keeping a working bridge.
|
|
24
|
+
*/
|
|
25
|
+
export function supportsNativeMcp(version) {
|
|
26
|
+
if (version === undefined)
|
|
27
|
+
return false;
|
|
28
|
+
const match = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(version.trim());
|
|
29
|
+
if (!match)
|
|
30
|
+
return false;
|
|
31
|
+
const major = Number(match[1]);
|
|
32
|
+
const minor = Number(match[2]);
|
|
33
|
+
return major > 1 || (major === 1 && minor >= 1);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* pi-shaped rendering of a canonical server definition — the same shape
|
|
37
|
+
* `pi mcp add` writes (observed 2026-10-11): stdio as
|
|
38
|
+
* `{command, args, env?}`, http as `{url, headers?}`, no `type` field.
|
|
39
|
+
* `env` names render as `${VAR}` references exactly like the other JSON
|
|
40
|
+
* agents; OAuth `auth` metadata is Trellis-side routing information and
|
|
41
|
+
* is never rendered (an agent-owned OAuth server becomes a bare `{url}`
|
|
42
|
+
* entry that `pi mcp login <name>` authorizes into pi's own token store).
|
|
43
|
+
*/
|
|
44
|
+
export function piRenderer(def, _name) {
|
|
45
|
+
if (def.transport === "http" || def.transport === "sse") {
|
|
46
|
+
const entry = { url: def.url };
|
|
47
|
+
if (def.headers && Object.keys(def.headers).length > 0) {
|
|
48
|
+
entry.headers = def.headers;
|
|
49
|
+
}
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
52
|
+
const entry = { command: def.command, args: def.args ?? [] };
|
|
53
|
+
const env = resolvedEnvTextMap(def);
|
|
54
|
+
if (Object.keys(env).length > 0) {
|
|
55
|
+
entry.env = env;
|
|
56
|
+
}
|
|
57
|
+
return entry;
|
|
58
|
+
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -111,6 +111,12 @@ export function printReport(report, dryRun) {
|
|
|
111
111
|
for (const item of [...created, ...removed, ...conflicts]) {
|
|
112
112
|
console.log(` - [${item.action}] ${item.description}`);
|
|
113
113
|
}
|
|
114
|
+
// pi does not watch mcp.json (verified 2026-10-11): without this hint
|
|
115
|
+
// a changed config silently only takes effect on the next pi launch
|
|
116
|
+
// (trellis-pi-native-mcp-migration design.md D5).
|
|
117
|
+
if (agent === "pi" && (created.length > 0 || removed.length > 0)) {
|
|
118
|
+
console.log(" pi does not watch mcp.json — restart pi or run /reload to pick up MCP changes");
|
|
119
|
+
}
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
function serversYamlPath(homeDir) {
|
package/dist/probes/pi.d.ts
CHANGED
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
* and `dist/core/resource-loader.js` (`loadContextFileFromDir(resolvedAgentDir)`),
|
|
6
6
|
* not by assumption. See design.md D5 (openspec/changes/trellis-doctor-p0).
|
|
7
7
|
*
|
|
8
|
-
* pi has
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* pi >= 1.1.0 has a native MCP client (`~/.pi/agent/mcp.json`), which the
|
|
9
|
+
* adapter manages (trellis-pi-native-mcp-migration); the probe still
|
|
10
|
+
* reports `mcpServers: []` because server health is the runtime's job,
|
|
11
|
+
* not the probe's. `probePiVersion` is split out so the adapter's
|
|
12
|
+
* native-vs-bridge version gate shares exactly this exec and tests can
|
|
13
|
+
* inject a seam.
|
|
12
14
|
*/
|
|
13
15
|
import type { AgentSnapshot } from "../core/types.js";
|
|
16
|
+
export declare function probePiVersion(): string | undefined;
|
|
14
17
|
export declare function probe(homeDir?: string): Promise<AgentSnapshot>;
|
package/dist/probes/pi.js
CHANGED
|
@@ -5,16 +5,26 @@
|
|
|
5
5
|
* and `dist/core/resource-loader.js` (`loadContextFileFromDir(resolvedAgentDir)`),
|
|
6
6
|
* not by assumption. See design.md D5 (openspec/changes/trellis-doctor-p0).
|
|
7
7
|
*
|
|
8
|
-
* pi has
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* pi >= 1.1.0 has a native MCP client (`~/.pi/agent/mcp.json`), which the
|
|
9
|
+
* adapter manages (trellis-pi-native-mcp-migration); the probe still
|
|
10
|
+
* reports `mcpServers: []` because server health is the runtime's job,
|
|
11
|
+
* not the probe's. `probePiVersion` is split out so the adapter's
|
|
12
|
+
* native-vs-bridge version gate shares exactly this exec and tests can
|
|
13
|
+
* inject a seam.
|
|
12
14
|
*/
|
|
13
15
|
import { execFileSync } from "node:child_process";
|
|
14
16
|
import { existsSync } from "node:fs";
|
|
15
17
|
import { homedir } from "node:os";
|
|
16
18
|
import { join } from "node:path";
|
|
17
19
|
import { pathRef, scanSkillRoot } from "../lib/probeCommon.js";
|
|
20
|
+
export function probePiVersion() {
|
|
21
|
+
try {
|
|
22
|
+
return execFileSync("pi", ["--version"], { encoding: "utf-8", timeout: 5_000 }).trim();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
18
28
|
/** Priority order pi itself checks (`loadContextFileFromDir`), first match wins. */
|
|
19
29
|
const INSTRUCTIONS_CANDIDATES = ["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"];
|
|
20
30
|
export async function probe(homeDir = homedir()) {
|
|
@@ -25,13 +35,8 @@ export async function probe(homeDir = homedir()) {
|
|
|
25
35
|
if (!present) {
|
|
26
36
|
return { agent: "pi", present: false, skillRoots: [], mcpServers: [], diagnostics: [] };
|
|
27
37
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
version = execFileSync("pi", ["--version"], { encoding: "utf-8", timeout: 5_000 }).trim();
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
// config exists without the binary on PATH — unusual, still probeable
|
|
34
|
-
}
|
|
38
|
+
// config can exist without the binary on PATH — unusual, still probeable
|
|
39
|
+
const version = probePiVersion();
|
|
35
40
|
const skillRoot = scanSkillRoot(skillsPath);
|
|
36
41
|
let instructionsFile;
|
|
37
42
|
for (const candidate of INSTRUCTIONS_CANDIDATES) {
|
package/docs/getting-started.md
CHANGED
|
@@ -291,6 +291,28 @@ that advertises `--prompt`, `--resume`, and `--output-format`. A desktop-only
|
|
|
291
291
|
profile can still receive managed configuration; Trellis never launches the
|
|
292
292
|
desktop application's private helper or bundled runtime path.
|
|
293
293
|
|
|
294
|
+
## pi native MCP (pi ≥ 1.1.0)
|
|
295
|
+
|
|
296
|
+
pi 1.1.0 added a native MCP client, and Trellis delivers to it the same way
|
|
297
|
+
as every other JSON-config agent: `trellis mcp sync` merges Trellis-managed
|
|
298
|
+
entries into `~/.pi/agent/mcp.json` — the single `trellis` edge entry in
|
|
299
|
+
gateway mode, plus a bare `{url}` entry for each agent-owned OAuth server.
|
|
300
|
+
Servers you added yourself (anything Trellis never wrote) are preserved
|
|
301
|
+
untouched, and the legacy bridge extension (`~/.pi/agent/extensions/
|
|
302
|
+
trellis-mcp-bridge.js`) is removed in the same run, after the config write.
|
|
303
|
+
|
|
304
|
+
Two pi-specific behaviors to know:
|
|
305
|
+
|
|
306
|
+
- pi does not watch `mcp.json`: after a sync that changed it, restart pi or
|
|
307
|
+
run `/reload` in an open session (sync prints this hint).
|
|
308
|
+
- Agent-owned OAuth servers (e.g. `figma`) are authorized by pi itself:
|
|
309
|
+
run `pi mcp login <name>` once; the grant lives in pi's own token store
|
|
310
|
+
(`~/.pi/agent/mcp-auth.json`), and `trellis.mcp.status` reports them as
|
|
311
|
+
`agent-managed`.
|
|
312
|
+
|
|
313
|
+
On pi older than 1.1.0 nothing changes: the bridge extension remains the
|
|
314
|
+
delivery mechanism and `mcp.json` is never written.
|
|
315
|
+
|
|
294
316
|
## Trellis Runtime Skill
|
|
295
317
|
|
|
296
318
|
`trellis-runtime` is the built-in operating guide that Trellis installs into
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Scheduled OAuth renewal — `trellis mcp renew`
|
|
2
2
|
|
|
3
|
-
The gateway
|
|
3
|
+
The gateway/runtime edge already refreshes OAuth tokens silently before
|
|
4
4
|
every upstream call. `trellis mcp renew` moves that renewal *earlier*: run
|
|
5
5
|
on a schedule, it refreshes whatever is expiring and reports — with a
|
|
6
6
|
non-zero exit code — anything that needs a human, before an Agent session
|
|
@@ -17,7 +17,8 @@ classification is explicit (mcp-auth-routing-policy), and ownership decides
|
|
|
17
17
|
whose problem the credential is (trellis-desktop-oauth-ownership): an
|
|
18
18
|
agent-owned server (the default, e.g. scalar `auth: oauth`) is authorized by
|
|
19
19
|
the Agent's own client, never reads Trellis's token store, and is neither
|
|
20
|
-
renewed nor reported here
|
|
20
|
+
renewed nor reported here — for pi ≥ 1.1.0 that client-side authorization is
|
|
21
|
+
`pi mcp login <name>`, stored in pi's own `~/.pi/agent/mcp-auth.json`. The command never opens a browser and never
|
|
21
22
|
performs an initial grant; that remains `trellis mcp auth <name>`
|
|
22
23
|
(mcp-gateway-hosting design.md D9). Exit code: `0` when every Trellis-owned
|
|
23
24
|
server is valid or refreshed, `1` when any needs `trellis mcp auth`, has
|
package/docs/research.md
CHANGED
|
@@ -236,10 +236,13 @@ Reverse-engineered directly from the installed binary
|
|
|
236
236
|
- pi **natively discovers** `AGENTS.override.md`, `AGENTS.md`, `CLAUDE.md`
|
|
237
237
|
(in that priority order) — instructions and `SKILL.md`-format skills need
|
|
238
238
|
no adapter work at all.
|
|
239
|
-
- pi
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
- pi **gained a native MCP client in 1.1.0** (`pi mcp add/list/login`,
|
|
240
|
+
`~/.pi/agent/mcp.json`, stdio + streamable HTTP, `${VAR}` expansion in
|
|
241
|
+
`env`/`headers`, OAuth into its own `mcp-auth.json` store) — Trellis delivers
|
|
242
|
+
to it natively (trellis-pi-native-mcp-migration). Before 1.1.0 the only
|
|
243
|
+
`mcpServers` string in its bundle belonged to the Google Gen AI SDK's Vertex
|
|
244
|
+
tool-schema translator, which explicitly throws `"mcpServers parameter is
|
|
245
|
+
not supported"` — that era is why the bridge extension below exists.
|
|
243
246
|
- pi's capability surface is **extensions**: JS modules loaded via
|
|
244
247
|
`--extension`/discovery, with a real API (`createExtension`,
|
|
245
248
|
`registerTool`). Giving pi access to the same MCP servers every other agent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-trellis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "A unified Code Agent Runtime for Skills, MCP, Memory, and shared instructions across Claude Code, Codex, Kiro, pi, Kimi Code, and ZCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Paul Leo",
|