agent-trellis 0.2.0 → 0.3.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 +39 -0
- package/dist/commands/memory.js +78 -0
- package/dist/commands/migrate.d.ts +30 -4
- package/dist/commands/migrate.js +83 -16
- package/dist/commands/onboard.d.ts +18 -8
- package/dist/commands/onboard.js +124 -14
- 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 +166 -10
- package/docs/roadmap.md +311 -0
- package/package.json +1 -1
- package/schema/servers.example.yaml +39 -2
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))
|
package/dist/probes/codex.js
CHANGED
|
@@ -36,16 +36,24 @@ export async function probe(homeDir = homedir(), opts = {}) {
|
|
|
36
36
|
return { agent: "codex", present: false, skillRoots: [], mcpServers: [], diagnostics: [] };
|
|
37
37
|
}
|
|
38
38
|
const diagnostics = [];
|
|
39
|
+
// `codex` resolves its own config via $HOME (confirmed by running it with
|
|
40
|
+
// an overridden HOME against an empty scratch dir — it returns `[]`, not
|
|
41
|
+
// the real machine's servers), so every subprocess call here must be
|
|
42
|
+
// scoped to `homeDir` explicitly — otherwise a caller probing a non-
|
|
43
|
+
// default `homeDir` would silently get this real machine's real MCP
|
|
44
|
+
// server list instead (trellis-migrate-mcp-servers found this gap in
|
|
45
|
+
// src/lib/mcpMigrateRead.ts's own equivalent call; fixed there first).
|
|
46
|
+
const codexEnv = { ...process.env, HOME: homeDir };
|
|
39
47
|
let version;
|
|
40
48
|
try {
|
|
41
|
-
version = execFileSync("codex", ["--version"], { encoding: "utf-8", timeout: 5_000 }).trim();
|
|
49
|
+
version = execFileSync("codex", ["--version"], { encoding: "utf-8", timeout: 5_000, env: codexEnv }).trim();
|
|
42
50
|
}
|
|
43
51
|
catch {
|
|
44
52
|
// config exists without the binary on PATH — unusual, still probeable
|
|
45
53
|
}
|
|
46
54
|
let mcpEntries = [];
|
|
47
55
|
try {
|
|
48
|
-
const raw = execFileSync("codex", ["mcp", "list", "--json"], { encoding: "utf-8", timeout: 5_000 });
|
|
56
|
+
const raw = execFileSync("codex", ["mcp", "list", "--json"], { encoding: "utf-8", timeout: 5_000, env: codexEnv });
|
|
49
57
|
mcpEntries = JSON.parse(raw);
|
|
50
58
|
}
|
|
51
59
|
catch (err) {
|
package/docs/architecture.md
CHANGED
|
@@ -45,10 +45,13 @@ Aligned to the `.agents Protocol` draft, extended where the draft is silent:
|
|
|
45
45
|
├── memories/*.md # shared memory entries (server-memory backed)
|
|
46
46
|
├── scope.yaml # exceptions to "shared with all agents" —
|
|
47
47
|
│ # see "Private / agent-specific capabilities"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
└── secrets.policy.yaml # which var names are allowed, nothing else
|
|
49
|
+
|
|
50
|
+
# Not user-authored, so not shown in the tree above — Trellis's own
|
|
51
|
+
# bookkeeping, written/read only by `trellis mcp sync` itself:
|
|
52
|
+
# mcp/ownership.json # what Trellis last wrote per (agent, server name) —
|
|
53
|
+
# # the ownership marker MCP removal needed
|
|
54
|
+
# # (docs/roadmap.md P14, src/lib/mcpOwnership.ts)
|
|
52
55
|
```
|
|
53
56
|
|
|
54
57
|
## Global vs. workspace scope
|
package/docs/getting-started.md
CHANGED
|
@@ -24,11 +24,14 @@ commands to type by hand:
|
|
|
24
24
|
- **No agent has real content**: skipped — canonical starts from `init`'s
|
|
25
25
|
placeholder.
|
|
26
26
|
- **Exactly one agent has real content**: auto-selected, no prompt.
|
|
27
|
-
- **Two or more**: prompts you
|
|
28
|
-
terminal
|
|
27
|
+
- **Two or more**: prompts you to choose (Up/Down or j/k, Enter to confirm,
|
|
28
|
+
on a real terminal that supports it — falls back to a numbered
|
|
29
|
+
type-a-digit prompt otherwise), or pass `--agent <id>` to skip the
|
|
30
|
+
prompt entirely.
|
|
29
31
|
2. **Managed set** — zero or more agents to actually write to. Always an
|
|
30
32
|
explicit choice: pass `--manage <ids>` (comma-separated, e.g. `--manage
|
|
31
|
-
pi,codex`) or `--manage none`, or answer the
|
|
33
|
+
pi,codex`) or `--manage none`, or answer the interactive checkbox prompt
|
|
34
|
+
(Space to toggle, Enter to confirm — same numbered fallback as above).
|
|
32
35
|
**The source is not included by default** — migrating from Claude Code
|
|
33
36
|
doesn't mean Trellis starts managing Claude Code too, unless you say so.
|
|
34
37
|
Selecting an agent that isn't installed yet is itself the authorization to
|
|
@@ -134,27 +137,133 @@ Add `--dry-run` to see the plan without writing anything:
|
|
|
134
137
|
$ trellis migrate --from codex --dry-run
|
|
135
138
|
```
|
|
136
139
|
|
|
140
|
+
Add `--only skills`, `--only instructions`, or `--only mcp` to migrate
|
|
141
|
+
just one category — useful when you only want part of it brought in
|
|
142
|
+
right now. Omit it to migrate all three, exactly as above:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
$ trellis migrate --from codex --only instructions
|
|
146
|
+
```
|
|
147
|
+
|
|
137
148
|
**What each action means:**
|
|
138
149
|
|
|
139
150
|
| Action | Meaning |
|
|
140
151
|
|---|---|
|
|
141
152
|
| `create` | New to canonical source — copied in. |
|
|
142
|
-
| `already-migrated` | Canonical already has byte-identical content (safe re-run, nothing happens). |
|
|
153
|
+
| `already-migrated` | Canonical already has byte-identical (or, for MCP servers, structurally identical) content (safe re-run, nothing happens). |
|
|
143
154
|
| `conflict` | Canonical already has *different* real content — **left untouched**, resolve by hand. |
|
|
144
155
|
| `skip-symlink` | That agent's own copy is itself a symlink (already shared in from elsewhere) — nothing of that agent's own to import. |
|
|
145
156
|
| `skip-case-broken` | Found as `skill.md` instead of `SKILL.md` — fix the case on the source agent first. |
|
|
157
|
+
| `skip-unsupported` | MCP servers only — that agent's real definition can't be safely represented (see below); nothing was written for it. |
|
|
146
158
|
|
|
147
159
|
Migrate never overwrites a genuine conflict, and never scopes a migrated
|
|
148
|
-
skill to just the source agent — once in canonical, it's
|
|
149
|
-
agent by default (see `sync`, below). If migrate reports
|
|
150
|
-
the two files it names and decide by hand
|
|
151
|
-
canonical, then re-run.
|
|
160
|
+
skill (or MCP server) to just the source agent — once in canonical, it's
|
|
161
|
+
visible to every agent by default (see `sync`, below). If migrate reports
|
|
162
|
+
a `conflict`, open the two files/entries it names and decide by hand
|
|
163
|
+
which content should actually be canonical, then re-run.
|
|
164
|
+
|
|
165
|
+
**MCP servers** (claude-code, kiro, codex — not pi, which has no static
|
|
166
|
+
MCP config to read at all) migrate the same way, into
|
|
167
|
+
`~/.trellis/mcp/servers.yaml`:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
$ trellis migrate --from claude-code --only mcp
|
|
171
|
+
migrate --from claude-code
|
|
172
|
+
[create] mcp server "gitlab" — will add to servers.yaml
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Two known fidelity limits, named rather than silently worked around:
|
|
176
|
+
|
|
177
|
+
- **Codex remote servers migrate when they only use `url` and
|
|
178
|
+
`bearer_token_env_var`** — the one shape this codebase has verified
|
|
179
|
+
against a real `codex` binary, and the only shape Trellis's own writer
|
|
180
|
+
ever produces for Codex. Codex's own config schema has no way to tell
|
|
181
|
+
`http` apart from `sse`, so a migrated remote server always comes back
|
|
182
|
+
as `http` — not a guess, that distinction was never stored in the
|
|
183
|
+
first place. A server using Codex's other header mechanisms
|
|
184
|
+
(`http_headers`/`env_http_headers`/`http_headers_helper` — real fields
|
|
185
|
+
this project has no verified shape for) is reported `skip-unsupported`
|
|
186
|
+
rather than guessed at; use `trellis mcp add` for that one server as a
|
|
187
|
+
workaround.
|
|
188
|
+
- **`headers` recovery depends on that agent's own real on-disk shape.**
|
|
189
|
+
claude-code/kiro read `headers` from the exact same JSON field Trellis
|
|
190
|
+
itself writes (`schema/servers.example.yaml`'s `figma` example) — if a
|
|
191
|
+
server was hand-authored with some other shape, it migrates whatever
|
|
192
|
+
is actually there, same as any other field.
|
|
152
193
|
|
|
153
194
|
## Starting from nothing
|
|
154
195
|
|
|
155
196
|
Skip migrate. Edit `~/.trellis/agents.md` and add skills under
|
|
156
|
-
`~/.trellis/skills/<name>/SKILL.md` directly
|
|
157
|
-
|
|
197
|
+
`~/.trellis/skills/<name>/SKILL.md` directly, or use
|
|
198
|
+
`trellis skill add` (below) instead of hand-editing. There's nothing else
|
|
199
|
+
to set up before moving on to `sync`.
|
|
200
|
+
|
|
201
|
+
The same applies to MCP servers — `~/.trellis/mcp/servers.yaml` can be
|
|
202
|
+
hand-authored the same way (see
|
|
203
|
+
[`schema/servers.example.yaml`](../schema/servers.example.yaml) for the
|
|
204
|
+
full shape), or use `trellis mcp add` (below). Either way, move on to
|
|
205
|
+
[`trellis mcp sync`](#trellis-mcp-sync) once you've added what you want.
|
|
206
|
+
|
|
207
|
+
## `trellis skill` / `trellis mcp` — canonical CRUD via the CLI
|
|
208
|
+
|
|
209
|
+
An alternative to hand-editing canonical files directly — useful for
|
|
210
|
+
scripting, or when you'd rather not open a text editor for a one-line
|
|
211
|
+
change. Both commands are canonical-side only: they never touch any
|
|
212
|
+
agent's native config (that stays `sync`/`mcp sync`'s job).
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
$ trellis skill list
|
|
216
|
+
my-skill — claude-code, codex, pi
|
|
217
|
+
|
|
218
|
+
$ trellis skill add my-other-skill --from ./some/local/dir
|
|
219
|
+
skill add my-other-skill
|
|
220
|
+
[create] will copy from ./some/local/dir
|
|
221
|
+
|
|
222
|
+
$ trellis skill remove my-other-skill
|
|
223
|
+
removed skill "my-other-skill" from canonical source.
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`skill add` refuses (no write) if the name already exists with different
|
|
227
|
+
content — same conflict posture as `migrate`, never silently overwritten.
|
|
228
|
+
`skill remove` deletes the canonical directory; the *next* `trellis sync`
|
|
229
|
+
then auto-removes the now-stale symlink on every agent that had it (skills
|
|
230
|
+
carry their own ownership marker — the symlink itself — so this
|
|
231
|
+
propagates automatically, unlike MCP servers below).
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
$ trellis mcp list
|
|
235
|
+
tanka (http) — codex, pi
|
|
236
|
+
env: TANKA_TOKEN (values never read/printed)
|
|
237
|
+
|
|
238
|
+
$ trellis mcp add local-server --transport stdio --command node --args server.js --env API_KEY
|
|
239
|
+
mcp add local-server
|
|
240
|
+
[create] will add to servers.yaml
|
|
241
|
+
|
|
242
|
+
$ trellis mcp remove local-server
|
|
243
|
+
removed MCP server "local-server" from canonical source.
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`mcp add` takes `--transport stdio|http|sse`; stdio requires `--command`
|
|
247
|
+
(plus optional `--args a,b`, `--env NAME,...`, `--static-env k=v,...`),
|
|
248
|
+
http/sse require `--url` (plus optional `--headers k=v,...`, values
|
|
249
|
+
expected as `${VAR}` references, never literal secrets). Both accept
|
|
250
|
+
`--agents id,...` (scope) and `--enabled true|false`. Same no-overwrite
|
|
251
|
+
conflict posture as `skill add` — there is no `--force`.
|
|
252
|
+
|
|
253
|
+
`mcp remove` is **canonical-only** — it does not remove the server from
|
|
254
|
+
any agent that already has it from an earlier `mcp sync` (the same
|
|
255
|
+
no-automatic-removal gap `mcp sync` itself has — see
|
|
256
|
+
[README's Known limitations](../README.md#status)); remove it by hand on
|
|
257
|
+
each agent in the meantime.
|
|
258
|
+
|
|
259
|
+
`mcp list` never resolves or prints a secret value: `env` names are shown
|
|
260
|
+
as bare names (the actual value is never read from your shell), and
|
|
261
|
+
`static_env` values are shown in full since those were never secrets in
|
|
262
|
+
the first place (see `mcp sync`'s own explanation of `env` vs.
|
|
263
|
+
`static_env`, further below).
|
|
264
|
+
|
|
265
|
+
All four of `skill add`/`skill remove`/`mcp add`/`mcp remove` support
|
|
266
|
+
`--dry-run` and `--json`, same convention as every other command.
|
|
158
267
|
|
|
159
268
|
## `trellis sync`
|
|
160
269
|
|
|
@@ -220,6 +329,53 @@ already populates them. See
|
|
|
220
329
|
for exactly how that resolution works for each agent, including the one
|
|
221
330
|
narrow exception (pi's bridge has to read a value into its own process).
|
|
222
331
|
|
|
332
|
+
Before writing any name-only `env` entry, `mcp sync` checks it actually
|
|
333
|
+
resolves — a name with no value anywhere `secrets audit` would also
|
|
334
|
+
check is refused as a conflict for that one server, not written and left
|
|
335
|
+
to silently break that server once the agent tries to use it. Every
|
|
336
|
+
other server, and every other agent, still syncs normally.
|
|
337
|
+
|
|
338
|
+
For a value that isn't a secret at all — an email address, an
|
|
339
|
+
environment tag — use `static_env` instead of `env`: written into the
|
|
340
|
+
agent's config verbatim, never treated as a name to resolve (still
|
|
341
|
+
scanned for an accidental real credential, same as every other literal
|
|
342
|
+
field). `enabled: false` keeps a server's definition in canonical
|
|
343
|
+
without writing it to any agent — for something you want configured but
|
|
344
|
+
not currently active anywhere; remove the line (or set it `true`) to
|
|
345
|
+
turn it back on everywhere at once. See
|
|
346
|
+
[`schema/servers.example.yaml`](../schema/servers.example.yaml) for both.
|
|
347
|
+
|
|
348
|
+
## `trellis memory sync`
|
|
349
|
+
|
|
350
|
+
Ingests `~/.trellis/memories/*.md` into the actual on-disk file
|
|
351
|
+
`@modelcontextprotocol/server-memory` reads at its own startup — closing
|
|
352
|
+
the gap between "memory entries exist in canonical" and "the running
|
|
353
|
+
memory server actually knows about them." Requires a `memory` server in
|
|
354
|
+
`servers.yaml` with `static_env.MEMORY_FILE_PATH` set explicitly (see
|
|
355
|
+
[`schema/servers.example.yaml`](../schema/servers.example.yaml)); without
|
|
356
|
+
one, this is a no-op, not an error.
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
$ trellis memory sync
|
|
360
|
+
memory sync — /Users/you/.trellis/memories/graph.jsonl
|
|
361
|
+
[create] "sprint-tasks" — will create a new entity
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Every entry Trellis creates is tagged internally so a later sync can
|
|
365
|
+
safely update or remove it; anything else already in that file — an
|
|
366
|
+
entity or relation an agent added itself while actually using the memory
|
|
367
|
+
server — is never touched. If a name collides with something already in
|
|
368
|
+
the graph that Trellis didn't create, the command refuses that one entry
|
|
369
|
+
(reported as a conflict) rather than overwriting it, same posture as
|
|
370
|
+
every other conflict in this project.
|
|
371
|
+
|
|
372
|
+
**Known limitation, named rather than silently worked around:** this only
|
|
373
|
+
ingests canonical's *own* `memories/*.md` files into the shared store —
|
|
374
|
+
it does not (yet) extract an agent's own already-accumulated memory
|
|
375
|
+
content (e.g. Claude Code's own per-project memory feature) back into
|
|
376
|
+
canonical. That extraction is a real, separate, still-open gap — see
|
|
377
|
+
`docs/roadmap.md`'s P15 entry.
|
|
378
|
+
|
|
223
379
|
## `trellis secrets audit`
|
|
224
380
|
|
|
225
381
|
```
|