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.
Files changed (67) hide show
  1. package/README.md +69 -17
  2. package/dist/adapters/claude-code.d.ts +5 -3
  3. package/dist/adapters/claude-code.js +27 -14
  4. package/dist/adapters/codex.d.ts +8 -4
  5. package/dist/adapters/codex.js +47 -16
  6. package/dist/adapters/jsonMcp.d.ts +16 -5
  7. package/dist/adapters/jsonMcp.js +38 -29
  8. package/dist/adapters/kiro.d.ts +5 -3
  9. package/dist/adapters/kiro.js +29 -16
  10. package/dist/adapters/mcpPlan.d.ts +11 -6
  11. package/dist/adapters/mcpPlan.js +40 -7
  12. package/dist/adapters/pi.d.ts +2 -1
  13. package/dist/adapters/pi.js +4 -4
  14. package/dist/adapters/symlinkPlan.d.ts +7 -3
  15. package/dist/adapters/symlinkPlan.js +42 -16
  16. package/dist/cli.js +161 -18
  17. package/dist/commands/init.js +11 -0
  18. package/dist/commands/mcp.d.ts +114 -7
  19. package/dist/commands/mcp.js +258 -17
  20. package/dist/commands/memory.d.ts +39 -0
  21. package/dist/commands/memory.js +78 -0
  22. package/dist/commands/migrate.d.ts +30 -4
  23. package/dist/commands/migrate.js +83 -16
  24. package/dist/commands/onboard.d.ts +52 -7
  25. package/dist/commands/onboard.js +318 -35
  26. package/dist/commands/rollback.d.ts +44 -0
  27. package/dist/commands/rollback.js +201 -0
  28. package/dist/commands/secretsAudit.d.ts +7 -0
  29. package/dist/commands/secretsAudit.js +14 -7
  30. package/dist/commands/skill.d.ts +51 -0
  31. package/dist/commands/skill.js +104 -0
  32. package/dist/commands/sync.d.ts +13 -0
  33. package/dist/commands/sync.js +31 -5
  34. package/dist/core/adapter.d.ts +28 -11
  35. package/dist/core/adapter.js +2 -2
  36. package/dist/core/canonical.d.ts +26 -1
  37. package/dist/core/canonical.js +103 -3
  38. package/dist/core/types.d.ts +29 -1
  39. package/dist/core/types.js +11 -2
  40. package/dist/lib/backup.d.ts +56 -0
  41. package/dist/lib/backup.js +98 -0
  42. package/dist/lib/deepEqual.d.ts +8 -0
  43. package/dist/lib/deepEqual.js +26 -0
  44. package/dist/lib/dirEquals.d.ts +9 -0
  45. package/dist/lib/dirEquals.js +15 -1
  46. package/dist/lib/installAgent.d.ts +26 -0
  47. package/dist/lib/installAgent.js +46 -0
  48. package/dist/lib/mcpMigrateRead.d.ts +69 -0
  49. package/dist/lib/mcpMigrateRead.js +188 -0
  50. package/dist/lib/mcpOwnership.d.ts +25 -0
  51. package/dist/lib/mcpOwnership.js +50 -0
  52. package/dist/lib/memoryGraph.d.ts +60 -0
  53. package/dist/lib/memoryGraph.js +101 -0
  54. package/dist/lib/realHomeSnapshot.d.ts +26 -0
  55. package/dist/lib/realHomeSnapshot.js +77 -0
  56. package/dist/lib/terminalPicker.d.ts +45 -0
  57. package/dist/lib/terminalPicker.js +193 -0
  58. package/dist/lib/tomlSection.d.ts +20 -6
  59. package/dist/lib/tomlSection.js +78 -12
  60. package/dist/pi-bridge/bundle.js +100 -51
  61. package/dist/pi-bridge/index.js +14 -2
  62. package/dist/probes/codex.js +10 -2
  63. package/docs/architecture.md +7 -4
  64. package/docs/getting-started.md +267 -33
  65. package/docs/roadmap.md +444 -0
  66. package/package.json +1 -1
  67. package/schema/servers.example.yaml +39 -2
@@ -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 { dirContentsEqual } from "../lib/dirEquals.js";
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
- if (!existsSync(canonicalDir)) {
31
- return { kind: "skill", name, action: "create", detail: `will copy from ${sourceDir}`, sourceDir };
32
- }
33
- if (dirContentsEqual(sourceDir, canonicalDir)) {
34
- return { kind: "skill", name, action: "already-migrated", detail: "canonical content is byte-identical" };
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
- export async function collectMigratePlan(agent, homeDir = homedir()) {
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
- for (const root of snapshot.skillRoots) {
71
- for (const skill of root.skills) {
72
- items.push(planSkill(skill.name, skill.dir, skill.isSymlink, skill.caseCorrect, join(canonicalRoot, "skills", skill.name)));
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
- const plan = await collectMigratePlan(agent, homeDir);
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
- console.log(` [${item.action}] ${item.kind === "skill" ? `skill "${item.name}"` : "instructions"} ${item.detail}`);
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 → base-agent
3
- * resolution → `migrate` `sync` into one guided flow
4
- * (trellis-cli-onboard). Orchestrates existing commands' own
5
- * plan/apply logic; no new skill-copy, symlink, or conflict-detection
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
- base?: AgentId;
38
- baseReason?: "auto-selected" | "flag" | "prompt";
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