agent-trellis 0.2.0 → 0.4.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 +12 -0
- package/dist/adapters/claude-code.d.ts +3 -2
- package/dist/adapters/claude-code.js +21 -8
- package/dist/adapters/codex.d.ts +6 -3
- package/dist/adapters/codex.js +41 -10
- package/dist/adapters/jsonMcp.d.ts +15 -5
- package/dist/adapters/jsonMcp.js +38 -29
- package/dist/adapters/kiro.d.ts +3 -2
- package/dist/adapters/kiro.js +22 -9
- package/dist/adapters/mcpPlan.d.ts +11 -6
- package/dist/adapters/mcpPlan.js +39 -6
- package/dist/cli.js +123 -10
- package/dist/commands/mcp.d.ts +101 -7
- package/dist/commands/mcp.js +227 -10
- package/dist/commands/memory.d.ts +47 -0
- package/dist/commands/memory.js +89 -0
- package/dist/commands/migrate.d.ts +30 -4
- package/dist/commands/migrate.js +83 -16
- package/dist/commands/onboard.d.ts +27 -8
- package/dist/commands/onboard.js +144 -17
- package/dist/commands/skill.d.ts +51 -0
- package/dist/commands/skill.js +104 -0
- package/dist/core/adapter.d.ts +18 -8
- package/dist/core/canonical.d.ts +26 -1
- package/dist/core/canonical.js +81 -3
- package/dist/core/types.d.ts +17 -0
- package/dist/lib/deepEqual.d.ts +8 -0
- package/dist/lib/deepEqual.js +26 -0
- package/dist/lib/dirEquals.d.ts +9 -0
- package/dist/lib/dirEquals.js +15 -1
- package/dist/lib/mcpMigrateRead.d.ts +69 -0
- package/dist/lib/mcpMigrateRead.js +188 -0
- package/dist/lib/mcpOwnership.d.ts +25 -0
- package/dist/lib/mcpOwnership.js +50 -0
- package/dist/lib/memoryGraph.d.ts +60 -0
- package/dist/lib/memoryGraph.js +101 -0
- package/dist/lib/realHomeSnapshot.d.ts +26 -0
- package/dist/lib/realHomeSnapshot.js +77 -0
- package/dist/lib/terminalPicker.d.ts +45 -0
- package/dist/lib/terminalPicker.js +193 -0
- package/dist/lib/tomlSection.d.ts +20 -6
- package/dist/lib/tomlSection.js +78 -12
- package/dist/pi-bridge/bundle.js +76 -46
- package/dist/pi-bridge/index.js +7 -2
- package/dist/probes/codex.js +10 -2
- package/docs/architecture.md +7 -4
- package/docs/getting-started.md +180 -15
- package/docs/roadmap.md +341 -0
- package/package.json +1 -1
- package/schema/servers.example.yaml +39 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, dependency-free arrow-key/checkbox terminal picker
|
|
3
|
+
* (trellis-onboard-interactive-picker design.md D1) — the interaction
|
|
4
|
+
* surface `trellis onboard`'s two prompts need (at most four rows, no
|
|
5
|
+
* search, no pagination) doesn't need a full prompt library; this
|
|
6
|
+
* hand-rolls just enough raw-mode key handling and ANSI rendering for
|
|
7
|
+
* that bounded case, matching the project's existing narrow-dependency
|
|
8
|
+
* precedent (src/lib/envVarNames.ts, src/lib/secretEnv.ts).
|
|
9
|
+
*
|
|
10
|
+
* `input`/`output` are injectable (never a CLI flag) purely so tests can
|
|
11
|
+
* drive the real key-parsing/render loop against a plain stream instead
|
|
12
|
+
* of a real TTY — same seam pattern (`homeDir`, etc.) used everywhere
|
|
13
|
+
* else in this project.
|
|
14
|
+
*/
|
|
15
|
+
function defaultStreams() {
|
|
16
|
+
return { input: process.stdin, output: process.stdout };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* True only when both streams are real, raw-mode-capable terminals — the
|
|
20
|
+
* exact gate deciding picker vs. the pre-existing numbered-typing prompt
|
|
21
|
+
* (design.md D2). A stream lacking `setRawMode` (piped input, some
|
|
22
|
+
* minimal TTYs) always falls back, never hangs or guesses.
|
|
23
|
+
*/
|
|
24
|
+
export function canUseInteractivePicker(streams = defaultStreams()) {
|
|
25
|
+
const input = streams.input;
|
|
26
|
+
const output = streams.output;
|
|
27
|
+
return Boolean(input.isTTY && output.isTTY && typeof input.setRawMode === "function");
|
|
28
|
+
}
|
|
29
|
+
/** Wrapping index navigation — pure, exported for direct unit testing. */
|
|
30
|
+
export function nextIndex(current, delta, length) {
|
|
31
|
+
return ((current + delta) % length + length) % length;
|
|
32
|
+
}
|
|
33
|
+
/** Pure single-index toggle — exported for direct unit testing. */
|
|
34
|
+
export function toggled(checked, index) {
|
|
35
|
+
const next = [...checked];
|
|
36
|
+
next[index] = !next[index];
|
|
37
|
+
return next;
|
|
38
|
+
}
|
|
39
|
+
const ESC = "\u001b";
|
|
40
|
+
const CTRL_C = "\u0003";
|
|
41
|
+
const HIDE_CURSOR = `${ESC}[?25l`;
|
|
42
|
+
const SHOW_CURSOR = `${ESC}[?25h`;
|
|
43
|
+
/** Parses one raw input chunk into a single logical key — arrow escape
|
|
44
|
+
* sequences, j/k, space, enter, or Ctrl+C. Anything else is ignored. */
|
|
45
|
+
function parseKey(chunk) {
|
|
46
|
+
if (chunk === CTRL_C)
|
|
47
|
+
return "cancel";
|
|
48
|
+
if (chunk === "\r" || chunk === "\n")
|
|
49
|
+
return "confirm";
|
|
50
|
+
if (chunk === " ")
|
|
51
|
+
return "toggle";
|
|
52
|
+
if (chunk === `${ESC}[A` || chunk === "k")
|
|
53
|
+
return "up";
|
|
54
|
+
if (chunk === `${ESC}[B` || chunk === "j")
|
|
55
|
+
return "down";
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function withRawMode(streams, body) {
|
|
59
|
+
const input = streams.input;
|
|
60
|
+
const output = streams.output;
|
|
61
|
+
const canSetRawMode = typeof input.setRawMode === "function";
|
|
62
|
+
if (canSetRawMode)
|
|
63
|
+
input.setRawMode(true);
|
|
64
|
+
input.resume();
|
|
65
|
+
input.setEncoding("utf-8");
|
|
66
|
+
output.write(HIDE_CURSOR);
|
|
67
|
+
const restore = () => {
|
|
68
|
+
output.write(SHOW_CURSOR);
|
|
69
|
+
if (canSetRawMode)
|
|
70
|
+
input.setRawMode(false);
|
|
71
|
+
input.pause();
|
|
72
|
+
};
|
|
73
|
+
// Second-layer safety net (design.md D6): a thrown error the caller's
|
|
74
|
+
// own try/finally doesn't get a chance to run for (process killed
|
|
75
|
+
// externally) must still not leave the terminal in raw mode/cursor
|
|
76
|
+
// hidden.
|
|
77
|
+
process.once("exit", restore);
|
|
78
|
+
return body().finally(() => {
|
|
79
|
+
restore();
|
|
80
|
+
process.removeListener("exit", restore);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function moveCursorUp(output, lines) {
|
|
84
|
+
if (lines > 0)
|
|
85
|
+
output.write(`${ESC}[${lines}A`);
|
|
86
|
+
}
|
|
87
|
+
function clearLines(output, lines) {
|
|
88
|
+
for (let i = 0; i < lines; i++) {
|
|
89
|
+
output.write(`${ESC}[2K`);
|
|
90
|
+
if (i < lines - 1)
|
|
91
|
+
output.write(`${ESC}[1B`);
|
|
92
|
+
}
|
|
93
|
+
moveCursorUp(output, lines - 1);
|
|
94
|
+
}
|
|
95
|
+
function renderRows(output, rows, previousRowCount) {
|
|
96
|
+
if (previousRowCount > 0) {
|
|
97
|
+
clearLines(output, previousRowCount);
|
|
98
|
+
}
|
|
99
|
+
for (const row of rows) {
|
|
100
|
+
output.write(`${row}\n`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function highlightRow(text, isHighlighted) {
|
|
104
|
+
return isHighlighted ? `> ${ESC}[7m${text}${ESC}[0m` : ` ${text}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Single-select: Up/Down/j/k moves the highlight, Enter confirms. Resolves
|
|
108
|
+
* the confirmed index, or `null` on Ctrl+C cancel. Caller (onboard.ts)
|
|
109
|
+
* must have already confirmed `canUseInteractivePicker()` — this function
|
|
110
|
+
* does not re-check, and assumes `streams.input` genuinely supports raw
|
|
111
|
+
* mode.
|
|
112
|
+
*/
|
|
113
|
+
export async function runSingleSelectPicker(items, streams = defaultStreams()) {
|
|
114
|
+
const { input, output } = streams;
|
|
115
|
+
return withRawMode(streams, () => {
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
let highlighted = 0;
|
|
118
|
+
let rowCount = 0;
|
|
119
|
+
const draw = () => {
|
|
120
|
+
const rows = items.map((label, i) => highlightRow(label, i === highlighted));
|
|
121
|
+
renderRows(output, rows, rowCount);
|
|
122
|
+
rowCount = rows.length;
|
|
123
|
+
};
|
|
124
|
+
draw();
|
|
125
|
+
const onData = (chunk) => {
|
|
126
|
+
const key = parseKey(chunk.toString("utf-8"));
|
|
127
|
+
if (key === "up") {
|
|
128
|
+
highlighted = nextIndex(highlighted, -1, items.length);
|
|
129
|
+
draw();
|
|
130
|
+
}
|
|
131
|
+
else if (key === "down") {
|
|
132
|
+
highlighted = nextIndex(highlighted, 1, items.length);
|
|
133
|
+
draw();
|
|
134
|
+
}
|
|
135
|
+
else if (key === "confirm") {
|
|
136
|
+
input.removeListener("data", onData);
|
|
137
|
+
resolve(highlighted);
|
|
138
|
+
}
|
|
139
|
+
else if (key === "cancel") {
|
|
140
|
+
input.removeListener("data", onData);
|
|
141
|
+
resolve(null);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
input.on("data", onData);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Multi-select (checkbox): Up/Down/j/k moves the highlight, Space toggles
|
|
150
|
+
* the current row, Enter confirms. Resolves the checked indices at
|
|
151
|
+
* confirm time, or `null` on Ctrl+C cancel. Same precondition as
|
|
152
|
+
* `runSingleSelectPicker`.
|
|
153
|
+
*/
|
|
154
|
+
export async function runMultiSelectPicker(items, initiallyChecked, streams = defaultStreams()) {
|
|
155
|
+
const { input, output } = streams;
|
|
156
|
+
return withRawMode(streams, () => {
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
let highlighted = 0;
|
|
159
|
+
let checked = [...initiallyChecked];
|
|
160
|
+
let rowCount = 0;
|
|
161
|
+
const draw = () => {
|
|
162
|
+
const rows = items.map((label, i) => highlightRow(`[${checked[i] ? "x" : " "}] ${label}`, i === highlighted));
|
|
163
|
+
renderRows(output, rows, rowCount);
|
|
164
|
+
rowCount = rows.length;
|
|
165
|
+
};
|
|
166
|
+
draw();
|
|
167
|
+
const onData = (chunk) => {
|
|
168
|
+
const key = parseKey(chunk.toString("utf-8"));
|
|
169
|
+
if (key === "up") {
|
|
170
|
+
highlighted = nextIndex(highlighted, -1, items.length);
|
|
171
|
+
draw();
|
|
172
|
+
}
|
|
173
|
+
else if (key === "down") {
|
|
174
|
+
highlighted = nextIndex(highlighted, 1, items.length);
|
|
175
|
+
draw();
|
|
176
|
+
}
|
|
177
|
+
else if (key === "toggle") {
|
|
178
|
+
checked = toggled(checked, highlighted);
|
|
179
|
+
draw();
|
|
180
|
+
}
|
|
181
|
+
else if (key === "confirm") {
|
|
182
|
+
input.removeListener("data", onData);
|
|
183
|
+
resolve(checked.flatMap((isChecked, i) => (isChecked ? [i] : [])));
|
|
184
|
+
}
|
|
185
|
+
else if (key === "cancel") {
|
|
186
|
+
input.removeListener("data", onData);
|
|
187
|
+
resolve(null);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
input.on("data", onData);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -23,10 +23,11 @@ export interface SectionRange {
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function findSection(content: string, header: string): SectionRange | null;
|
|
25
25
|
/**
|
|
26
|
-
* Returns the current stored text of
|
|
27
|
-
* it doesn't exist
|
|
28
|
-
* to decide create/repair vs.
|
|
29
|
-
* no parsing needed on either
|
|
26
|
+
* Returns the current stored text of a server's full owned range (see
|
|
27
|
+
* `findServerRange`) — or `null` if it doesn't exist — for comparing
|
|
28
|
+
* against `renderServerSection`'s output to decide create/repair vs.
|
|
29
|
+
* no-op. Exact text equality is enough here, no parsing needed on either
|
|
30
|
+
* side.
|
|
30
31
|
*/
|
|
31
32
|
export declare function currentServerSectionText(content: string, name: string): string | null;
|
|
32
33
|
/**
|
|
@@ -40,9 +41,22 @@ export declare function currentServerSectionText(content: string, name: string):
|
|
|
40
41
|
* than this function silently rendering nothing for it.
|
|
41
42
|
*/
|
|
42
43
|
export declare function codexBearerTokenEnvVar(def: McpServerDef): string | undefined;
|
|
43
|
-
/** Renders a `[mcp_servers.<name>]` block
|
|
44
|
-
*
|
|
44
|
+
/** Renders a `[mcp_servers.<name>]` block (plus, when `staticEnv` is set,
|
|
45
|
+
* an immediately-adjacent `[mcp_servers.<name>.env]` block — see
|
|
46
|
+
* `findServerRange`) for a bounded, known shape — this is templating,
|
|
47
|
+
* not general TOML serialization. */
|
|
45
48
|
export declare function renderServerSection(name: string, def: McpServerDef): string;
|
|
49
|
+
/**
|
|
50
|
+
* Reads a server's `[mcp_servers.<name>.env]` table's literal key-value
|
|
51
|
+
* pairs directly from real `config.toml` text — the exact inverse of
|
|
52
|
+
* `renderServerSection`'s own `static_env` block, the only piece of a
|
|
53
|
+
* Codex server's `static_env` that `codex mcp list --json` cannot ever
|
|
54
|
+
* report (it only exposes `env_vars`, i.e. names, from the *main*
|
|
55
|
+
* table — trellis-migrate-mcp-servers design.md D3). Returns `undefined`
|
|
56
|
+
* if the table doesn't exist; a malformed line inside it is skipped, not
|
|
57
|
+
* fatal, matching `parseMemoryGraph`'s own tolerant-parse precedent.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readServerEnvTable(content: string, name: string): Record<string, string> | undefined;
|
|
46
60
|
/**
|
|
47
61
|
* Replaces an existing section in place, or appends a new one at EOF
|
|
48
62
|
* (with a leading blank-line separator) if none exists yet. Never touches
|
package/dist/lib/tomlSection.js
CHANGED
|
@@ -36,6 +36,9 @@ function tomlStringArray(values) {
|
|
|
36
36
|
function serverHeader(name) {
|
|
37
37
|
return `mcp_servers.${tomlKeySegment(name)}`;
|
|
38
38
|
}
|
|
39
|
+
function envHeader(name) {
|
|
40
|
+
return `${serverHeader(name)}.env`;
|
|
41
|
+
}
|
|
39
42
|
/**
|
|
40
43
|
* Finds an existing `[header]` table's exact line range: `start` is the
|
|
41
44
|
* header line itself, `end` is the line before the next table header (any
|
|
@@ -70,14 +73,42 @@ export function findSection(content, header) {
|
|
|
70
73
|
return { start, end };
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
76
|
+
* A server's full owned range: its main `[mcp_servers.<name>]` table,
|
|
77
|
+
* extended to also cover an immediately-following `[mcp_servers.<name>.env]`
|
|
78
|
+
* table (skipping only blank/comment padding in between — the same skip
|
|
79
|
+
* rule `findSection`'s own trailing trim already applies) when one is
|
|
80
|
+
* present. The two are rendered and managed as one atomic unit whenever
|
|
81
|
+
* `staticEnv` is set (trellis-mcp-static-env-and-disabled-servers
|
|
82
|
+
* design.md D3) — create/repair/remove must never touch one without the
|
|
83
|
+
* other.
|
|
84
|
+
*/
|
|
85
|
+
function findServerRange(content, name) {
|
|
86
|
+
const main = findSection(content, serverHeader(name));
|
|
87
|
+
if (!main) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const lines = content.split("\n");
|
|
91
|
+
let i = main.end + 1;
|
|
92
|
+
while (i < lines.length && (lines[i].trim() === "" || lines[i].trim().startsWith("#"))) {
|
|
93
|
+
i += 1;
|
|
94
|
+
}
|
|
95
|
+
if (i < lines.length && lines[i].trim() === `[${envHeader(name)}]`) {
|
|
96
|
+
const envRange = findSection(content, envHeader(name));
|
|
97
|
+
if (envRange) {
|
|
98
|
+
return { start: main.start, end: envRange.end };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return main;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Returns the current stored text of a server's full owned range (see
|
|
105
|
+
* `findServerRange`) — or `null` if it doesn't exist — for comparing
|
|
106
|
+
* against `renderServerSection`'s output to decide create/repair vs.
|
|
107
|
+
* no-op. Exact text equality is enough here, no parsing needed on either
|
|
108
|
+
* side.
|
|
77
109
|
*/
|
|
78
110
|
export function currentServerSectionText(content, name) {
|
|
79
|
-
const
|
|
80
|
-
const range = findSection(content, header);
|
|
111
|
+
const range = findServerRange(content, name);
|
|
81
112
|
if (!range) {
|
|
82
113
|
return null;
|
|
83
114
|
}
|
|
@@ -103,8 +134,10 @@ export function codexBearerTokenEnvVar(def) {
|
|
|
103
134
|
return undefined;
|
|
104
135
|
return BEARER_TOKEN_VALUE_RE.exec(value)?.[1];
|
|
105
136
|
}
|
|
106
|
-
/** Renders a `[mcp_servers.<name>]` block
|
|
107
|
-
*
|
|
137
|
+
/** Renders a `[mcp_servers.<name>]` block (plus, when `staticEnv` is set,
|
|
138
|
+
* an immediately-adjacent `[mcp_servers.<name>.env]` block — see
|
|
139
|
+
* `findServerRange`) for a bounded, known shape — this is templating,
|
|
140
|
+
* not general TOML serialization. */
|
|
108
141
|
export function renderServerSection(name, def) {
|
|
109
142
|
const lines = [`[${serverHeader(name)}]`];
|
|
110
143
|
if (def.transport === "stdio") {
|
|
@@ -122,16 +155,50 @@ export function renderServerSection(name, def) {
|
|
|
122
155
|
if (bearerEnvVar)
|
|
123
156
|
lines.push(`bearer_token_env_var = ${tomlString(bearerEnvVar)}`);
|
|
124
157
|
}
|
|
158
|
+
const staticEnvEntries = Object.entries(def.staticEnv ?? {});
|
|
159
|
+
if (staticEnvEntries.length > 0) {
|
|
160
|
+
lines.push(`[${envHeader(name)}]`);
|
|
161
|
+
for (const [key, value] of staticEnvEntries) {
|
|
162
|
+
lines.push(`${tomlKeySegment(key)} = ${tomlString(value)}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
125
165
|
return lines.join("\n");
|
|
126
166
|
}
|
|
167
|
+
const ENV_TABLE_LINE_RE = /^("(?:[^"\\]|\\.)*"|[A-Za-z0-9_-]+)\s*=\s*("(?:[^"\\]|\\.)*")\s*$/;
|
|
168
|
+
/**
|
|
169
|
+
* Reads a server's `[mcp_servers.<name>.env]` table's literal key-value
|
|
170
|
+
* pairs directly from real `config.toml` text — the exact inverse of
|
|
171
|
+
* `renderServerSection`'s own `static_env` block, the only piece of a
|
|
172
|
+
* Codex server's `static_env` that `codex mcp list --json` cannot ever
|
|
173
|
+
* report (it only exposes `env_vars`, i.e. names, from the *main*
|
|
174
|
+
* table — trellis-migrate-mcp-servers design.md D3). Returns `undefined`
|
|
175
|
+
* if the table doesn't exist; a malformed line inside it is skipped, not
|
|
176
|
+
* fatal, matching `parseMemoryGraph`'s own tolerant-parse precedent.
|
|
177
|
+
*/
|
|
178
|
+
export function readServerEnvTable(content, name) {
|
|
179
|
+
const range = findSection(content, envHeader(name));
|
|
180
|
+
if (!range) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
const lines = content.split("\n").slice(range.start + 1, range.end + 1);
|
|
184
|
+
const result = {};
|
|
185
|
+
for (const line of lines) {
|
|
186
|
+
const match = ENV_TABLE_LINE_RE.exec(line.trim());
|
|
187
|
+
if (!match)
|
|
188
|
+
continue;
|
|
189
|
+
const rawKey = match[1];
|
|
190
|
+
const key = rawKey.startsWith('"') ? JSON.parse(rawKey) : rawKey;
|
|
191
|
+
result[key] = JSON.parse(match[2]);
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
127
195
|
/**
|
|
128
196
|
* Replaces an existing section in place, or appends a new one at EOF
|
|
129
197
|
* (with a leading blank-line separator) if none exists yet. Never touches
|
|
130
198
|
* any line outside the section it locates or the single appended block.
|
|
131
199
|
*/
|
|
132
200
|
export function upsertSection(content, name, def) {
|
|
133
|
-
const
|
|
134
|
-
const existing = findSection(content, header);
|
|
201
|
+
const existing = findServerRange(content, name);
|
|
135
202
|
const newLines = renderServerSection(name, def).split("\n");
|
|
136
203
|
if (existing) {
|
|
137
204
|
const lines = content.split("\n");
|
|
@@ -147,8 +214,7 @@ export function upsertSection(content, name, def) {
|
|
|
147
214
|
* No-op if the section doesn't exist — idempotent.
|
|
148
215
|
*/
|
|
149
216
|
export function removeSection(content, name) {
|
|
150
|
-
const
|
|
151
|
-
const existing = findSection(content, header);
|
|
217
|
+
const existing = findServerRange(content, name);
|
|
152
218
|
if (!existing) {
|
|
153
219
|
return content;
|
|
154
220
|
}
|
package/dist/pi-bridge/bundle.js
CHANGED
|
@@ -7818,7 +7818,7 @@ var require_identity = __commonJS({
|
|
|
7818
7818
|
var NODE_TYPE = /* @__PURE__ */ Symbol.for("yaml.node.type");
|
|
7819
7819
|
var isAlias = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === ALIAS;
|
|
7820
7820
|
var isDocument = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === DOC;
|
|
7821
|
-
var
|
|
7821
|
+
var isMap2 = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === MAP;
|
|
7822
7822
|
var isPair = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === PAIR;
|
|
7823
7823
|
var isScalar = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === SCALAR;
|
|
7824
7824
|
var isSeq = (node2) => !!node2 && typeof node2 === "object" && node2[NODE_TYPE] === SEQ;
|
|
@@ -7854,7 +7854,7 @@ var require_identity = __commonJS({
|
|
|
7854
7854
|
exports.isAlias = isAlias;
|
|
7855
7855
|
exports.isCollection = isCollection;
|
|
7856
7856
|
exports.isDocument = isDocument;
|
|
7857
|
-
exports.isMap =
|
|
7857
|
+
exports.isMap = isMap2;
|
|
7858
7858
|
exports.isNode = isNode;
|
|
7859
7859
|
exports.isPair = isPair;
|
|
7860
7860
|
exports.isScalar = isScalar;
|
|
@@ -11966,9 +11966,9 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
11966
11966
|
var blockMsg = "Block collections are not allowed within flow collections";
|
|
11967
11967
|
var isBlock = (token) => token && (token.type === "block-map" || token.type === "block-seq");
|
|
11968
11968
|
function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) {
|
|
11969
|
-
const
|
|
11970
|
-
const fcName =
|
|
11971
|
-
const NodeClass = tag?.nodeClass ?? (
|
|
11969
|
+
const isMap2 = fc.start.source === "{";
|
|
11970
|
+
const fcName = isMap2 ? "flow map" : "flow sequence";
|
|
11971
|
+
const NodeClass = tag?.nodeClass ?? (isMap2 ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq);
|
|
11972
11972
|
const coll = new NodeClass(ctx.schema);
|
|
11973
11973
|
coll.flow = true;
|
|
11974
11974
|
const atRoot = ctx.atRoot;
|
|
@@ -12004,7 +12004,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
12004
12004
|
offset = props.end;
|
|
12005
12005
|
continue;
|
|
12006
12006
|
}
|
|
12007
|
-
if (!
|
|
12007
|
+
if (!isMap2 && ctx.options.strict && utilContainsNewline.containsNewline(key))
|
|
12008
12008
|
onError(
|
|
12009
12009
|
key,
|
|
12010
12010
|
// checked by containsNewline()
|
|
@@ -12044,7 +12044,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
12044
12044
|
}
|
|
12045
12045
|
}
|
|
12046
12046
|
}
|
|
12047
|
-
if (!
|
|
12047
|
+
if (!isMap2 && !sep && !props.found) {
|
|
12048
12048
|
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError);
|
|
12049
12049
|
coll.items.push(valueNode);
|
|
12050
12050
|
offset = valueNode.range[2];
|
|
@@ -12067,7 +12067,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
12067
12067
|
startOnNewline: false
|
|
12068
12068
|
});
|
|
12069
12069
|
if (valueProps.found) {
|
|
12070
|
-
if (!
|
|
12070
|
+
if (!isMap2 && !props.found && ctx.options.strict) {
|
|
12071
12071
|
if (sep)
|
|
12072
12072
|
for (const st of sep) {
|
|
12073
12073
|
if (st === valueProps.found)
|
|
@@ -12099,7 +12099,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
12099
12099
|
const pair = new Pair.Pair(keyNode, valueNode);
|
|
12100
12100
|
if (ctx.options.keepSourceTokens)
|
|
12101
12101
|
pair.srcToken = collItem;
|
|
12102
|
-
if (
|
|
12102
|
+
if (isMap2) {
|
|
12103
12103
|
const map = coll;
|
|
12104
12104
|
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
|
12105
12105
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
@@ -12115,7 +12115,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
12115
12115
|
offset = valueNode ? valueNode.range[2] : valueProps.end;
|
|
12116
12116
|
}
|
|
12117
12117
|
}
|
|
12118
|
-
const expectedEnd =
|
|
12118
|
+
const expectedEnd = isMap2 ? "}" : "]";
|
|
12119
12119
|
const [ce, ...ee] = fc.end;
|
|
12120
12120
|
let cePos = offset;
|
|
12121
12121
|
if (ce?.source === expectedEnd)
|
|
@@ -15016,7 +15016,7 @@ var require_public_api = __commonJS({
|
|
|
15016
15016
|
return docs;
|
|
15017
15017
|
return Object.assign([], { empty: true }, composer$1.streamInfo());
|
|
15018
15018
|
}
|
|
15019
|
-
function
|
|
15019
|
+
function parseDocument2(source, options = {}) {
|
|
15020
15020
|
const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options);
|
|
15021
15021
|
const parser$1 = new parser.Parser(lineCounter2?.addNewLine);
|
|
15022
15022
|
const composer$1 = new composer.Composer(options);
|
|
@@ -15042,7 +15042,7 @@ var require_public_api = __commonJS({
|
|
|
15042
15042
|
} else if (options === void 0 && reviver && typeof reviver === "object") {
|
|
15043
15043
|
options = reviver;
|
|
15044
15044
|
}
|
|
15045
|
-
const doc =
|
|
15045
|
+
const doc = parseDocument2(src, options);
|
|
15046
15046
|
if (!doc)
|
|
15047
15047
|
return null;
|
|
15048
15048
|
doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
|
|
@@ -15078,7 +15078,7 @@ var require_public_api = __commonJS({
|
|
|
15078
15078
|
}
|
|
15079
15079
|
exports.parse = parse3;
|
|
15080
15080
|
exports.parseAllDocuments = parseAllDocuments;
|
|
15081
|
-
exports.parseDocument =
|
|
15081
|
+
exports.parseDocument = parseDocument2;
|
|
15082
15082
|
exports.stringify = stringify;
|
|
15083
15083
|
}
|
|
15084
15084
|
});
|
|
@@ -27243,7 +27243,7 @@ import { homedir as homedir2 } from "node:os";
|
|
|
27243
27243
|
|
|
27244
27244
|
// src/core/canonical.ts
|
|
27245
27245
|
var import_yaml = __toESM(require_dist2(), 1);
|
|
27246
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
27246
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
27247
27247
|
import { homedir } from "node:os";
|
|
27248
27248
|
import { basename, join } from "node:path";
|
|
27249
27249
|
|
|
@@ -27260,6 +27260,10 @@ function resolveScope(scope, managedAgents) {
|
|
|
27260
27260
|
}
|
|
27261
27261
|
|
|
27262
27262
|
// src/core/canonical.ts
|
|
27263
|
+
function fromServerDefYaml(def) {
|
|
27264
|
+
const { static_env, ...rest } = def;
|
|
27265
|
+
return static_env ? { ...rest, staticEnv: static_env } : rest;
|
|
27266
|
+
}
|
|
27263
27267
|
function trellisRoot(homeDir) {
|
|
27264
27268
|
return join(homeDir, ".trellis");
|
|
27265
27269
|
}
|
|
@@ -27303,8 +27307,9 @@ function loadServersYaml(path) {
|
|
|
27303
27307
|
return { servers: {}, knownHostInjected: [] };
|
|
27304
27308
|
}
|
|
27305
27309
|
const parsed = (0, import_yaml.parse)(readFileSync(path, "utf-8")) ?? {};
|
|
27310
|
+
const servers = Object.fromEntries(Object.entries(parsed.servers ?? {}).map(([name, def]) => [name, fromServerDefYaml(def)]));
|
|
27306
27311
|
return {
|
|
27307
|
-
servers
|
|
27312
|
+
servers,
|
|
27308
27313
|
knownHostInjected: parsed.known_host_injected ?? [],
|
|
27309
27314
|
hub: parsed.hub
|
|
27310
27315
|
};
|
|
@@ -27416,6 +27421,33 @@ function codexBearerTokenEnvVar(def) {
|
|
|
27416
27421
|
return BEARER_TOKEN_VALUE_RE.exec(value)?.[1];
|
|
27417
27422
|
}
|
|
27418
27423
|
|
|
27424
|
+
// src/lib/secretEnv.ts
|
|
27425
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
27426
|
+
var LINE_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
|
|
27427
|
+
function parseDotenv(content) {
|
|
27428
|
+
const result = {};
|
|
27429
|
+
for (const rawLine of content.split("\n")) {
|
|
27430
|
+
const line = rawLine.trim();
|
|
27431
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
27432
|
+
const match = LINE_RE.exec(line);
|
|
27433
|
+
if (match) result[match[1]] = match[2];
|
|
27434
|
+
}
|
|
27435
|
+
return result;
|
|
27436
|
+
}
|
|
27437
|
+
function loadEnvFile(path) {
|
|
27438
|
+
return existsSync2(path) ? parseDotenv(readFileSync2(path, "utf-8")) : {};
|
|
27439
|
+
}
|
|
27440
|
+
function resolveSecretEnv(names, policy) {
|
|
27441
|
+
const result = {};
|
|
27442
|
+
if (policy.envFile) {
|
|
27443
|
+
const map = loadEnvFile(policy.envFile);
|
|
27444
|
+
for (const name of names) result[name] = map[name];
|
|
27445
|
+
} else {
|
|
27446
|
+
for (const name of names) result[name] = process.env[name];
|
|
27447
|
+
}
|
|
27448
|
+
return result;
|
|
27449
|
+
}
|
|
27450
|
+
|
|
27419
27451
|
// src/adapters/mcpPlan.ts
|
|
27420
27452
|
var HUB_ENTRY_NAME = "trellis-hub";
|
|
27421
27453
|
var CODEX_STDIO_URL_CRASH_NOTE = ' On Codex specifically, this crashes the entire process at startup ("url is not supported for stdio"), not just this one server \u2014 see docs/research.md.';
|
|
@@ -27429,7 +27461,9 @@ var DANGEROUS_LITERAL_PATTERNS = [
|
|
|
27429
27461
|
{ label: "mcp-router token (mcpr_)", pattern: /mcpr_/ }
|
|
27430
27462
|
];
|
|
27431
27463
|
function findLiteralSecret(def) {
|
|
27432
|
-
const candidates = [def.command, def.url, ...def.args ?? [], ...Object.values(def.headers ?? {})
|
|
27464
|
+
const candidates = [def.command, def.url, ...def.args ?? [], ...Object.values(def.headers ?? {}), ...Object.values(def.staticEnv ?? {})].filter(
|
|
27465
|
+
(v) => typeof v === "string"
|
|
27466
|
+
);
|
|
27433
27467
|
for (const candidate of candidates) {
|
|
27434
27468
|
for (const { label, pattern } of DANGEROUS_LITERAL_PATTERNS) {
|
|
27435
27469
|
if (pattern.test(candidate)) {
|
|
@@ -27439,7 +27473,13 @@ function findLiteralSecret(def) {
|
|
|
27439
27473
|
}
|
|
27440
27474
|
return void 0;
|
|
27441
27475
|
}
|
|
27442
|
-
function
|
|
27476
|
+
function findUnresolvedEnvName(def, policy) {
|
|
27477
|
+
const names = def.env ?? [];
|
|
27478
|
+
if (names.length === 0) return void 0;
|
|
27479
|
+
const resolved = resolveSecretEnv(names, policy);
|
|
27480
|
+
return names.find((name) => !resolved[name]);
|
|
27481
|
+
}
|
|
27482
|
+
function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
27443
27483
|
if (mcp.hub) {
|
|
27444
27484
|
if (mcp.knownHostInjected.includes(HUB_ENTRY_NAME)) {
|
|
27445
27485
|
return { desired: [], conflicts: [{ name: HUB_ENTRY_NAME, message: collisionMessage(HUB_ENTRY_NAME, agentId) }] };
|
|
@@ -27449,6 +27489,9 @@ function resolveMcpPlan(agentId, mcp, managedAgents) {
|
|
|
27449
27489
|
const desired = [];
|
|
27450
27490
|
const conflicts = [];
|
|
27451
27491
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
27492
|
+
if (def.enabled === false) {
|
|
27493
|
+
continue;
|
|
27494
|
+
}
|
|
27452
27495
|
if (!isInScope(agentId, def.agents, managedAgents)) {
|
|
27453
27496
|
continue;
|
|
27454
27497
|
}
|
|
@@ -27471,38 +27514,20 @@ function resolveMcpPlan(agentId, mcp, managedAgents) {
|
|
|
27471
27514
|
});
|
|
27472
27515
|
continue;
|
|
27473
27516
|
}
|
|
27517
|
+
const unresolvedName = findUnresolvedEnvName(def, policy);
|
|
27518
|
+
if (unresolvedName) {
|
|
27519
|
+
const source = policy.envFile ?? "process environment";
|
|
27520
|
+
conflicts.push({
|
|
27521
|
+
name,
|
|
27522
|
+
message: `refusing to write MCP server "${name}": its declared env var "${unresolvedName}" has no resolvable value in ${source} \u2014 writing it now would silently break this server's connection once the agent starts it (trellis-mcp-static-env-and-disabled-servers)`
|
|
27523
|
+
});
|
|
27524
|
+
continue;
|
|
27525
|
+
}
|
|
27474
27526
|
desired.push({ name, def });
|
|
27475
27527
|
}
|
|
27476
27528
|
return { desired, conflicts };
|
|
27477
27529
|
}
|
|
27478
27530
|
|
|
27479
|
-
// src/lib/secretEnv.ts
|
|
27480
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
27481
|
-
var LINE_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
|
|
27482
|
-
function parseDotenv(content) {
|
|
27483
|
-
const result = {};
|
|
27484
|
-
for (const rawLine of content.split("\n")) {
|
|
27485
|
-
const line = rawLine.trim();
|
|
27486
|
-
if (line === "" || line.startsWith("#")) continue;
|
|
27487
|
-
const match = LINE_RE.exec(line);
|
|
27488
|
-
if (match) result[match[1]] = match[2];
|
|
27489
|
-
}
|
|
27490
|
-
return result;
|
|
27491
|
-
}
|
|
27492
|
-
function loadEnvFile(path) {
|
|
27493
|
-
return existsSync2(path) ? parseDotenv(readFileSync2(path, "utf-8")) : {};
|
|
27494
|
-
}
|
|
27495
|
-
function resolveSecretEnv(names, policy) {
|
|
27496
|
-
const result = {};
|
|
27497
|
-
if (policy.envFile) {
|
|
27498
|
-
const map = loadEnvFile(policy.envFile);
|
|
27499
|
-
for (const name of names) result[name] = map[name];
|
|
27500
|
-
} else {
|
|
27501
|
-
for (const name of names) result[name] = process.env[name];
|
|
27502
|
-
}
|
|
27503
|
-
return result;
|
|
27504
|
-
}
|
|
27505
|
-
|
|
27506
27531
|
// src/lib/envVarNames.ts
|
|
27507
27532
|
var TEMPLATE_VAR_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
27508
27533
|
function extractTemplateVarNames(value) {
|
|
@@ -31974,7 +31999,12 @@ async function connectStdio(def, secretsPolicy, timeoutMs) {
|
|
|
31974
31999
|
const transport = new StdioClientTransport({
|
|
31975
32000
|
command: def.command,
|
|
31976
32001
|
args: def.args,
|
|
31977
|
-
|
|
32002
|
+
// staticEnv merges last: a literal, intentionally-plain value (an
|
|
32003
|
+
// email, an environment tag) always wins over an unresolved name-only
|
|
32004
|
+
// entry's empty-string fallback for the same key — though in practice
|
|
32005
|
+
// resolveMcpPlan's D6 refusal never lets an unresolved name reach
|
|
32006
|
+
// this point at all (trellis-mcp-static-env-and-disabled-servers).
|
|
32007
|
+
env: { ...getDefaultEnvironment(), ...namedEnv, ...def.staticEnv ?? {} }
|
|
31978
32008
|
});
|
|
31979
32009
|
return connectWithCleanup(client, transport, timeoutMs, `connect timed out after ${timeoutMs}ms`);
|
|
31980
32010
|
}
|
|
@@ -32029,7 +32059,7 @@ function registerServerTools(pi, serverName, client, timeoutMs) {
|
|
|
32029
32059
|
}
|
|
32030
32060
|
async function trellisMcpBridge(pi, homeDir = homedir2(), connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS) {
|
|
32031
32061
|
const canonical = loadCanonicalSource(homeDir);
|
|
32032
|
-
const { desired } = resolveMcpPlan("pi", canonical.mcp, ALL_AGENTS);
|
|
32062
|
+
const { desired } = resolveMcpPlan("pi", canonical.mcp, ALL_AGENTS, canonical.secretsPolicy);
|
|
32033
32063
|
const clients = /* @__PURE__ */ new Set();
|
|
32034
32064
|
const closeClient = async (client) => {
|
|
32035
32065
|
if (!clients.delete(client)) return;
|
package/dist/pi-bridge/index.js
CHANGED
|
@@ -75,7 +75,12 @@ async function connectStdio(def, secretsPolicy, timeoutMs) {
|
|
|
75
75
|
const transport = new StdioClientTransport({
|
|
76
76
|
command: def.command,
|
|
77
77
|
args: def.args,
|
|
78
|
-
|
|
78
|
+
// staticEnv merges last: a literal, intentionally-plain value (an
|
|
79
|
+
// email, an environment tag) always wins over an unresolved name-only
|
|
80
|
+
// entry's empty-string fallback for the same key — though in practice
|
|
81
|
+
// resolveMcpPlan's D6 refusal never lets an unresolved name reach
|
|
82
|
+
// this point at all (trellis-mcp-static-env-and-disabled-servers).
|
|
83
|
+
env: { ...getDefaultEnvironment(), ...namedEnv, ...(def.staticEnv ?? {}) },
|
|
79
84
|
});
|
|
80
85
|
return connectWithCleanup(client, transport, timeoutMs, `connect timed out after ${timeoutMs}ms`);
|
|
81
86
|
}
|
|
@@ -135,7 +140,7 @@ export default async function trellisMcpBridge(pi, homeDir = homedir(), connectT
|
|
|
135
140
|
// independent of whether `trellis onboard` was ever run to add pi to
|
|
136
141
|
// managed.yaml. managedAgents governs static config-file writes; this
|
|
137
142
|
// is pi reading canonical directly at its own runtime, P4's own concern.
|
|
138
|
-
const { desired } = resolveMcpPlan("pi", canonical.mcp, ALL_AGENTS);
|
|
143
|
+
const { desired } = resolveMcpPlan("pi", canonical.mcp, ALL_AGENTS, canonical.secretsPolicy);
|
|
139
144
|
const clients = new Set();
|
|
140
145
|
const closeClient = async (client) => {
|
|
141
146
|
if (!clients.delete(client))
|