agent-trellis 0.1.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 +69 -17
- package/dist/adapters/claude-code.d.ts +5 -3
- package/dist/adapters/claude-code.js +27 -14
- package/dist/adapters/codex.d.ts +8 -4
- package/dist/adapters/codex.js +47 -16
- package/dist/adapters/jsonMcp.d.ts +16 -5
- package/dist/adapters/jsonMcp.js +38 -29
- package/dist/adapters/kiro.d.ts +5 -3
- package/dist/adapters/kiro.js +29 -16
- package/dist/adapters/mcpPlan.d.ts +11 -6
- package/dist/adapters/mcpPlan.js +40 -7
- package/dist/adapters/pi.d.ts +2 -1
- package/dist/adapters/pi.js +4 -4
- package/dist/adapters/symlinkPlan.d.ts +7 -3
- package/dist/adapters/symlinkPlan.js +42 -16
- package/dist/cli.js +161 -18
- package/dist/commands/init.js +11 -0
- package/dist/commands/mcp.d.ts +114 -7
- package/dist/commands/mcp.js +258 -17
- 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 +52 -7
- package/dist/commands/onboard.js +318 -35
- package/dist/commands/rollback.d.ts +44 -0
- package/dist/commands/rollback.js +201 -0
- package/dist/commands/secretsAudit.d.ts +7 -0
- package/dist/commands/secretsAudit.js +14 -7
- package/dist/commands/skill.d.ts +51 -0
- package/dist/commands/skill.js +104 -0
- package/dist/commands/sync.d.ts +13 -0
- package/dist/commands/sync.js +31 -5
- package/dist/core/adapter.d.ts +28 -11
- package/dist/core/adapter.js +2 -2
- package/dist/core/canonical.d.ts +26 -1
- package/dist/core/canonical.js +103 -3
- package/dist/core/types.d.ts +29 -1
- package/dist/core/types.js +11 -2
- package/dist/lib/backup.d.ts +56 -0
- package/dist/lib/backup.js +98 -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/installAgent.d.ts +26 -0
- package/dist/lib/installAgent.js +46 -0
- 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 +100 -51
- package/dist/pi-bridge/index.js +14 -2
- package/dist/probes/codex.js +10 -2
- package/docs/architecture.md +7 -4
- package/docs/getting-started.md +267 -33
- package/docs/roadmap.md +444 -0
- package/package.json +1 -1
- package/schema/servers.example.yaml +39 -2
package/dist/commands/migrate.js
CHANGED
|
@@ -12,7 +12,10 @@ import * as codexProbe from "../probes/codex.js";
|
|
|
12
12
|
import * as kiroProbe from "../probes/kiro.js";
|
|
13
13
|
import * as piProbe from "../probes/pi.js";
|
|
14
14
|
import { AGENTS_MD_TEMPLATE } from "./init.js";
|
|
15
|
-
import {
|
|
15
|
+
import { decideDirImport } from "../lib/dirEquals.js";
|
|
16
|
+
import { deepEqual } from "../lib/deepEqual.js";
|
|
17
|
+
import { readClaudeCodeMcpDefs, readCodexMcpDefs, readKiroMcpDefs } from "../lib/mcpMigrateRead.js";
|
|
18
|
+
import { loadCanonicalSource, upsertServerYaml } from "../core/canonical.js";
|
|
16
19
|
import { ALL_AGENTS } from "../core/types.js";
|
|
17
20
|
const PROBES = {
|
|
18
21
|
"claude-code": (homeDir) => claudeCodeProbe.probe(homeDir),
|
|
@@ -20,6 +23,14 @@ const PROBES = {
|
|
|
20
23
|
kiro: (homeDir) => kiroProbe.probe(homeDir),
|
|
21
24
|
pi: (homeDir) => piProbe.probe(homeDir),
|
|
22
25
|
};
|
|
26
|
+
/** pi has no static MCP config to read at all (roadmap.md P14/
|
|
27
|
+
* trellis-migrate-mcp-servers) — deliberately absent, not an oversight;
|
|
28
|
+
* `collectMigratePlan` skips the `mcp` category entirely for pi. */
|
|
29
|
+
const MCP_READERS = {
|
|
30
|
+
"claude-code": readClaudeCodeMcpDefs,
|
|
31
|
+
kiro: readKiroMcpDefs,
|
|
32
|
+
codex: readCodexMcpDefs,
|
|
33
|
+
};
|
|
23
34
|
function planSkill(name, sourceDir, isSymlink, caseCorrect, canonicalDir) {
|
|
24
35
|
if (isSymlink) {
|
|
25
36
|
return { kind: "skill", name, action: "skip-symlink", detail: "shared in from elsewhere, not this agent's own content" };
|
|
@@ -27,13 +38,14 @@ function planSkill(name, sourceDir, isSymlink, caseCorrect, canonicalDir) {
|
|
|
27
38
|
if (!caseCorrect) {
|
|
28
39
|
return { kind: "skill", name, action: "skip-case-broken", detail: "already undiscoverable on at least one other agent — fix on the source before migrating" };
|
|
29
40
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
switch (decideDirImport(sourceDir, canonicalDir)) {
|
|
42
|
+
case "create":
|
|
43
|
+
return { kind: "skill", name, action: "create", detail: `will copy from ${sourceDir}`, sourceDir };
|
|
44
|
+
case "already-present":
|
|
45
|
+
return { kind: "skill", name, action: "already-migrated", detail: "canonical content is byte-identical" };
|
|
46
|
+
case "conflict":
|
|
47
|
+
return { kind: "skill", name, action: "conflict", detail: `canonical skills/${name}/ already exists with different content — resolve by hand` };
|
|
35
48
|
}
|
|
36
|
-
return { kind: "skill", name, action: "conflict", detail: `canonical skills/${name}/ already exists with different content — resolve by hand` };
|
|
37
49
|
}
|
|
38
50
|
function planInstructions(snapshot, canonicalAgentsMd) {
|
|
39
51
|
if (!snapshot.instructionsFile)
|
|
@@ -60,21 +72,56 @@ function planInstructions(snapshot, canonicalAgentsMd) {
|
|
|
60
72
|
}
|
|
61
73
|
return { kind: "instructions", name: "agents.md", action: "conflict", detail: "canonical agents.md already has different real content — resolve by hand" };
|
|
62
74
|
}
|
|
63
|
-
|
|
75
|
+
function planMcpServer(name, def, existing) {
|
|
76
|
+
if (existing === undefined) {
|
|
77
|
+
return { kind: "mcp", name, action: "create", detail: "will add to servers.yaml", mcpDef: def };
|
|
78
|
+
}
|
|
79
|
+
if (deepEqual(existing, def)) {
|
|
80
|
+
return { kind: "mcp", name, action: "already-migrated", detail: "canonical definition is already identical" };
|
|
81
|
+
}
|
|
82
|
+
return { kind: "mcp", name, action: "conflict", detail: `canonical mcp/servers.yaml already has a different definition for "${name}" — resolve by hand` };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `only` restricts which kind(s) are even considered — not a post-hoc
|
|
86
|
+
* filter on a fully-computed plan (trellis-migrate-category-selection
|
|
87
|
+
* design.md D3): the excluded kind's canonical path is never read for
|
|
88
|
+
* comparison and never appears in the plan, not even as a suppressed
|
|
89
|
+
* conflict. Omitting `only` (or passing both kinds) is exactly today's
|
|
90
|
+
* behavior.
|
|
91
|
+
*/
|
|
92
|
+
export async function collectMigratePlan(agent, homeDir = homedir(), only) {
|
|
64
93
|
const snapshot = await PROBES[agent](homeDir);
|
|
65
94
|
if (!snapshot.present) {
|
|
66
95
|
return { agent, present: false, items: [] };
|
|
67
96
|
}
|
|
68
97
|
const canonicalRoot = join(homeDir, ".trellis");
|
|
69
98
|
const items = [];
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
99
|
+
const wants = (kind) => !only || only.includes(kind);
|
|
100
|
+
if (wants("skill")) {
|
|
101
|
+
for (const root of snapshot.skillRoots) {
|
|
102
|
+
for (const skill of root.skills) {
|
|
103
|
+
items.push(planSkill(skill.name, skill.dir, skill.isSymlink, skill.caseCorrect, join(canonicalRoot, "skills", skill.name)));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (wants("instructions")) {
|
|
108
|
+
const instructionsItem = planInstructions(snapshot, join(canonicalRoot, "agents.md"));
|
|
109
|
+
if (instructionsItem)
|
|
110
|
+
items.push(instructionsItem);
|
|
111
|
+
}
|
|
112
|
+
if (wants("mcp")) {
|
|
113
|
+
const reader = MCP_READERS[agent];
|
|
114
|
+
if (reader) {
|
|
115
|
+
const canonicalServers = loadCanonicalSource(homeDir).mcp.servers;
|
|
116
|
+
const { entries, unsupported } = reader(homeDir);
|
|
117
|
+
for (const { name, def } of entries) {
|
|
118
|
+
items.push(planMcpServer(name, def, canonicalServers[name]));
|
|
119
|
+
}
|
|
120
|
+
for (const { name, reason } of unsupported) {
|
|
121
|
+
items.push({ kind: "mcp", name, action: "skip-unsupported", detail: reason });
|
|
122
|
+
}
|
|
73
123
|
}
|
|
74
124
|
}
|
|
75
|
-
const instructionsItem = planInstructions(snapshot, join(canonicalRoot, "agents.md"));
|
|
76
|
-
if (instructionsItem)
|
|
77
|
-
items.push(instructionsItem);
|
|
78
125
|
return { agent, present: true, items };
|
|
79
126
|
}
|
|
80
127
|
export function applyMigratePlan(plan, homeDir = homedir()) {
|
|
@@ -91,8 +138,22 @@ export function applyMigratePlan(plan, homeDir = homedir()) {
|
|
|
91
138
|
mkdirSync(canonicalRoot, { recursive: true });
|
|
92
139
|
writeFileSync(join(canonicalRoot, "agents.md"), item.sourceContent);
|
|
93
140
|
}
|
|
141
|
+
else if (item.kind === "mcp" && item.mcpDef) {
|
|
142
|
+
upsertServerYaml(join(canonicalRoot, "mcp", "servers.yaml"), item.name, item.mcpDef);
|
|
143
|
+
}
|
|
94
144
|
}
|
|
95
145
|
}
|
|
146
|
+
const ONLY_VALUES = ["skills", "instructions", "mcp"];
|
|
147
|
+
function isMigrateOnlyValue(value) {
|
|
148
|
+
return ONLY_VALUES.includes(value);
|
|
149
|
+
}
|
|
150
|
+
function toMigrateKinds(only) {
|
|
151
|
+
if (only === "skills")
|
|
152
|
+
return ["skill"];
|
|
153
|
+
if (only === "instructions")
|
|
154
|
+
return ["instructions"];
|
|
155
|
+
return ["mcp"];
|
|
156
|
+
}
|
|
96
157
|
export async function runMigrate(opts = {}) {
|
|
97
158
|
const homeDir = opts.homeDir ?? homedir();
|
|
98
159
|
if (!opts.from || !ALL_AGENTS.includes(opts.from)) {
|
|
@@ -100,7 +161,12 @@ export async function runMigrate(opts = {}) {
|
|
|
100
161
|
return { exitCode: 1 };
|
|
101
162
|
}
|
|
102
163
|
const agent = opts.from;
|
|
103
|
-
|
|
164
|
+
if (opts.only !== undefined && !isMigrateOnlyValue(opts.only)) {
|
|
165
|
+
console.error(`--only must be one of: ${ONLY_VALUES.join(", ")} (got ${opts.only})`);
|
|
166
|
+
return { exitCode: 1 };
|
|
167
|
+
}
|
|
168
|
+
const only = opts.only && isMigrateOnlyValue(opts.only) ? toMigrateKinds(opts.only) : undefined;
|
|
169
|
+
const plan = await collectMigratePlan(agent, homeDir, only);
|
|
104
170
|
if (!plan.present) {
|
|
105
171
|
console.error(`${agent} is not present on this machine — nothing to migrate.`);
|
|
106
172
|
return { exitCode: 1 };
|
|
@@ -127,6 +193,7 @@ export function printPlan(plan, dryRun) {
|
|
|
127
193
|
return;
|
|
128
194
|
}
|
|
129
195
|
for (const item of plan.items) {
|
|
130
|
-
|
|
196
|
+
const label = item.kind === "skill" ? `skill "${item.name}"` : item.kind === "mcp" ? `mcp server "${item.name}"` : "instructions";
|
|
197
|
+
console.log(` [${item.action}] ${label} — ${item.detail}`);
|
|
131
198
|
}
|
|
132
199
|
}
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `trellis onboard` — chains `init` → agent detection →
|
|
3
|
-
* resolution →
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* `trellis onboard` — chains `init` → agent detection → migration-source
|
|
3
|
+
* resolution → migrate-category selection (trellis-migrate-category-
|
|
4
|
+
* selection) → managed-agent-set selection (install-then-manage for a
|
|
5
|
+
* selected, not-yet-present agent) → `migrate` → `sync` → `mcp sync` →
|
|
6
|
+
* `secrets audit` into one guided flow (trellis-cli-onboard,
|
|
7
|
+
* trellis-managed-agents) — a user should never have to type a second
|
|
8
|
+
* command by hand to finish onboarding. Source, categories, and managed
|
|
9
|
+
* set are three independent choices (design.md D2): importing from a
|
|
10
|
+
* source never writes back to it, and it is not implicitly added to the
|
|
11
|
+
* managed set. Orchestrates existing commands' own plan/apply logic; no
|
|
12
|
+
* new skill-copy, symlink, conflict-detection, or secrets-scanning
|
|
6
13
|
* judgment is made here.
|
|
7
14
|
*/
|
|
8
15
|
import type { AgentId } from "../core/types.js";
|
|
9
|
-
import type { MigratePlan } from "./migrate.js";
|
|
16
|
+
import type { MigrateKind, MigratePlan } from "./migrate.js";
|
|
10
17
|
import type { SyncReport } from "./sync.js";
|
|
18
|
+
import type { McpSyncReport } from "./mcp.js";
|
|
19
|
+
import type { SecretsAuditReport } from "./secretsAudit.js";
|
|
20
|
+
import type { ConfirmAndInstallOptions } from "../lib/installAgent.js";
|
|
11
21
|
export interface OnboardAgentSummary {
|
|
12
22
|
agent: AgentId;
|
|
13
23
|
present: boolean;
|
|
@@ -20,7 +30,12 @@ export interface OnboardAgentSummary {
|
|
|
20
30
|
}
|
|
21
31
|
export declare function collectOnboardSummary(homeDir?: string): Promise<OnboardAgentSummary[]>;
|
|
22
32
|
export interface RunOnboardOptions {
|
|
33
|
+
/** Non-interactive migration-source choice. */
|
|
23
34
|
agent?: string;
|
|
35
|
+
/** Non-interactive managed-set choice: comma-separated agent ids, or
|
|
36
|
+
* the literal string "none" for "add nothing new this run" — distinct
|
|
37
|
+
* from omitting the flag, which requires a prompt or refuses. */
|
|
38
|
+
manage?: string;
|
|
24
39
|
dryRun?: boolean;
|
|
25
40
|
json?: boolean;
|
|
26
41
|
/** Defaults to the real `~`; overridable for tests only. */
|
|
@@ -31,13 +46,43 @@ export interface RunOnboardOptions {
|
|
|
31
46
|
/** Test-only: replaces the real readline prompt with a scripted
|
|
32
47
|
* answer, so the prompt path is exercisable without a real terminal. */
|
|
33
48
|
promptForAgent?: (present: OnboardAgentSummary[]) => Promise<string>;
|
|
49
|
+
/** Test-only: replaces the real readline multi-select prompt. Returns
|
|
50
|
+
* the raw answer string (same grammar as `--manage`'s value), not a
|
|
51
|
+
* pre-parsed list — so the same parsing/validation code path is
|
|
52
|
+
* exercised whether the answer came from a flag or a prompt. */
|
|
53
|
+
promptForManagedAgents?: (candidates: OnboardAgentSummary[], alreadyManaged: readonly AgentId[]) => Promise<string>;
|
|
54
|
+
/** Test-only: replaces the real migrate-category picker/default logic
|
|
55
|
+
* (trellis-migrate-category-selection). An empty array is a valid
|
|
56
|
+
* answer — "skip migrate for this run" (design.md D6) — distinct from
|
|
57
|
+
* `source` being unresolved at all. */
|
|
58
|
+
promptForMigrateCategories?: (source: OnboardAgentSummary) => Promise<MigrateKind[]>;
|
|
59
|
+
/** Test-only: injected into every `confirmAndInstall` call for a
|
|
60
|
+
* selected, not-yet-present agent. Never a real terminal prompt or a
|
|
61
|
+
* real `npm install` in a unit test. */
|
|
62
|
+
install?: ConfirmAndInstallOptions;
|
|
63
|
+
}
|
|
64
|
+
export interface OnboardInstallResult {
|
|
65
|
+
agent: AgentId;
|
|
66
|
+
installed: boolean;
|
|
67
|
+
installable: boolean;
|
|
34
68
|
}
|
|
35
69
|
export interface OnboardResult {
|
|
36
70
|
summary: OnboardAgentSummary[];
|
|
37
|
-
|
|
38
|
-
|
|
71
|
+
source?: AgentId;
|
|
72
|
+
sourceReason?: "auto-selected" | "flag" | "prompt";
|
|
73
|
+
/** The full managed set this run acted against — the union of whatever
|
|
74
|
+
* was already in `managed.yaml` plus this run's own new selections
|
|
75
|
+
* that actually resolved (D3: never a subtraction). */
|
|
76
|
+
managedAgents?: AgentId[];
|
|
77
|
+
installResults?: OnboardInstallResult[];
|
|
39
78
|
migratePlan?: MigratePlan;
|
|
79
|
+
/** Set when a source was resolved but zero migrate categories were
|
|
80
|
+
* selected (design.md D6) — distinct from `migratePlan` being absent
|
|
81
|
+
* because no source existed at all, which prints nothing here. */
|
|
82
|
+
migrateSkipped?: string;
|
|
40
83
|
syncReport?: SyncReport;
|
|
84
|
+
mcpSyncReport?: McpSyncReport;
|
|
85
|
+
secretsAuditReport?: SecretsAuditReport;
|
|
41
86
|
refusal?: string;
|
|
42
87
|
/** Only set when no agent is present — the same values `--json` and
|
|
43
88
|
* text output both surface, so a machine caller doesn't have to
|