@tacuchi/agent-workflow-cli 23.0.0 → 25.0.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 (164) hide show
  1. package/README.md +37 -1
  2. package/dist/adapters/postgres-readonly-tools.js +277 -0
  3. package/dist/adapters/postgres-readonly-tools.js.map +1 -0
  4. package/dist/application/checkpoint/markdown.js +85 -5
  5. package/dist/application/checkpoint/markdown.js.map +1 -1
  6. package/dist/application/checkpoint-service.js +7 -6
  7. package/dist/application/checkpoint-service.js.map +1 -1
  8. package/dist/application/checkpoint-write-service.js +270 -16
  9. package/dist/application/checkpoint-write-service.js.map +1 -1
  10. package/dist/application/database-mcp-server.js +168 -0
  11. package/dist/application/database-mcp-server.js.map +1 -0
  12. package/dist/application/database-mcp-stdio.js +137 -0
  13. package/dist/application/database-mcp-stdio.js.map +1 -0
  14. package/dist/application/database-tool-catalog.js +226 -0
  15. package/dist/application/database-tool-catalog.js.map +1 -0
  16. package/dist/application/dsn-reader-service.js +1 -1
  17. package/dist/application/flow/advance.js +60 -10
  18. package/dist/application/flow/advance.js.map +1 -1
  19. package/dist/application/flow/internal-actions.js +1 -1
  20. package/dist/application/flow/internal-actions.js.map +1 -1
  21. package/dist/application/flow/submit.js +256 -9
  22. package/dist/application/flow/submit.js.map +1 -1
  23. package/dist/application/lifecycle-target.js +16 -19
  24. package/dist/application/lifecycle-target.js.map +1 -1
  25. package/dist/application/lock-service.js +96 -31
  26. package/dist/application/lock-service.js.map +1 -1
  27. package/dist/application/logging/log-events.js +19 -3
  28. package/dist/application/logging/log-events.js.map +1 -1
  29. package/dist/application/logging/logger.js +2 -2
  30. package/dist/application/logging/logger.js.map +1 -1
  31. package/dist/application/markdown.js +6 -2
  32. package/dist/application/markdown.js.map +1 -1
  33. package/dist/application/mcp-connections-service.js +115 -41
  34. package/dist/application/mcp-connections-service.js.map +1 -1
  35. package/dist/application/mcp-doctor-service.js +98 -54
  36. package/dist/application/mcp-doctor-service.js.map +1 -1
  37. package/dist/application/mcp-entry-classification.js +60 -0
  38. package/dist/application/mcp-entry-classification.js.map +1 -0
  39. package/dist/application/mcp-host-reader.js +102 -86
  40. package/dist/application/mcp-host-reader.js.map +1 -1
  41. package/dist/application/mcp-host-receipt-service.js +399 -0
  42. package/dist/application/mcp-host-receipt-service.js.map +1 -0
  43. package/dist/application/mcp-host-receipt-store.js +122 -0
  44. package/dist/application/mcp-host-receipt-store.js.map +1 -0
  45. package/dist/application/mcp-host-receipts.js +28 -0
  46. package/dist/application/mcp-host-receipts.js.map +1 -0
  47. package/dist/application/mcp-host-writer.js +209 -38
  48. package/dist/application/mcp-host-writer.js.map +1 -1
  49. package/dist/application/mcp-launch-probe-service.js +142 -0
  50. package/dist/application/mcp-launch-probe-service.js.map +1 -0
  51. package/dist/application/mcp-migration-service.js +351 -0
  52. package/dist/application/mcp-migration-service.js.map +1 -0
  53. package/dist/application/mcp-native-host-check-service.js +154 -0
  54. package/dist/application/mcp-native-host-check-service.js.map +1 -0
  55. package/dist/application/mcp-receipt-registration-service.js +105 -0
  56. package/dist/application/mcp-receipt-registration-service.js.map +1 -0
  57. package/dist/application/mcp-receipt-removal-service.js +62 -0
  58. package/dist/application/mcp-receipt-removal-service.js.map +1 -0
  59. package/dist/application/mcp-remove-service.js +83 -24
  60. package/dist/application/mcp-remove-service.js.map +1 -1
  61. package/dist/application/mcp-scope-common.js +1 -1
  62. package/dist/application/mcp-scope-common.js.map +1 -1
  63. package/dist/application/mcp-setup-service.js +59 -24
  64. package/dist/application/mcp-setup-service.js.map +1 -1
  65. package/dist/application/mcp-stdio-probe.js +526 -0
  66. package/dist/application/mcp-stdio-probe.js.map +1 -0
  67. package/dist/application/mcp-test-connection-service.js +23 -50
  68. package/dist/application/mcp-test-connection-service.js.map +1 -1
  69. package/dist/application/parsers/spec-functional.js +220 -0
  70. package/dist/application/parsers/spec-functional.js.map +1 -0
  71. package/dist/application/parsers/spec-relation.js +142 -5
  72. package/dist/application/parsers/spec-relation.js.map +1 -1
  73. package/dist/application/paths-service.js +13 -0
  74. package/dist/application/paths-service.js.map +1 -1
  75. package/dist/application/plan-exec-decision-service.js +57 -4
  76. package/dist/application/plan-exec-decision-service.js.map +1 -1
  77. package/dist/application/reseal-service.js +300 -0
  78. package/dist/application/reseal-service.js.map +1 -0
  79. package/dist/application/self/host-states.js.map +1 -1
  80. package/dist/application/self/install-skill.js +4 -34
  81. package/dist/application/self/install-skill.js.map +1 -1
  82. package/dist/application/self/mcp-config.js +369 -59
  83. package/dist/application/self/mcp-config.js.map +1 -1
  84. package/dist/application/self/mcp-via-state.js +9 -6
  85. package/dist/application/self/mcp-via-state.js.map +1 -1
  86. package/dist/application/self/uninstall-skill.js +3 -18
  87. package/dist/application/self/uninstall-skill.js.map +1 -1
  88. package/dist/application/self/uninstall.js +4 -23
  89. package/dist/application/self/uninstall.js.map +1 -1
  90. package/dist/application/workline-index-service.js +64 -6
  91. package/dist/application/workline-index-service.js.map +1 -1
  92. package/dist/application/workspace-init-service.js +4 -6
  93. package/dist/application/workspace-init-service.js.map +1 -1
  94. package/dist/cli/commands/checkpoint-write.js +24 -22
  95. package/dist/cli/commands/checkpoint-write.js.map +1 -1
  96. package/dist/cli/commands/index.js +7 -0
  97. package/dist/cli/commands/index.js.map +1 -1
  98. package/dist/cli/commands/mcp.js +642 -55
  99. package/dist/cli/commands/mcp.js.map +1 -1
  100. package/dist/cli/commands/reseal.js +140 -0
  101. package/dist/cli/commands/reseal.js.map +1 -0
  102. package/dist/cli/commands/status.js +5 -1
  103. package/dist/cli/commands/status.js.map +1 -1
  104. package/dist/cli/commands/tool.js +141 -0
  105. package/dist/cli/commands/tool.js.map +1 -0
  106. package/dist/cli/help-groups.js +7 -1
  107. package/dist/cli/help-groups.js.map +1 -1
  108. package/dist/cli/main.js +189 -57
  109. package/dist/cli/main.js.map +1 -1
  110. package/dist/cli/parser.js +5 -1
  111. package/dist/cli/parser.js.map +1 -1
  112. package/dist/cli/registry.js.map +1 -1
  113. package/dist/cli/render.js +15 -2
  114. package/dist/cli/render.js.map +1 -1
  115. package/dist/cli/tui/components/host-admin-section.js +2 -46
  116. package/dist/cli/tui/components/host-admin-section.js.map +1 -1
  117. package/dist/cli/tui/tabs/mcp-tab-helpers.js +71 -0
  118. package/dist/cli/tui/tabs/mcp-tab-helpers.js.map +1 -1
  119. package/dist/cli/tui/tabs/mcp-tab.js +101 -42
  120. package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
  121. package/dist/domain/database-tools.js +445 -0
  122. package/dist/domain/database-tools.js.map +1 -0
  123. package/dist/domain/effective-contract.js +10 -2
  124. package/dist/domain/effective-contract.js.map +1 -1
  125. package/dist/domain/flow/answer.js +4 -0
  126. package/dist/domain/flow/answer.js.map +1 -1
  127. package/dist/domain/flow/authority.js +103 -14
  128. package/dist/domain/flow/authority.js.map +1 -1
  129. package/dist/domain/flow/directive.js +2 -0
  130. package/dist/domain/flow/directive.js.map +1 -1
  131. package/dist/domain/flow/run-state.js +42 -0
  132. package/dist/domain/flow/run-state.js.map +1 -1
  133. package/dist/domain/lineage.js +38 -11
  134. package/dist/domain/lineage.js.map +1 -1
  135. package/dist/domain/mcp-entry.js +131 -17
  136. package/dist/domain/mcp-entry.js.map +1 -1
  137. package/dist/domain/postgres-object-search.js +320 -0
  138. package/dist/domain/postgres-object-search.js.map +1 -0
  139. package/dist/domain/redaction.js +47 -0
  140. package/dist/domain/redaction.js.map +1 -0
  141. package/dist/domain/workline-mcp-entry.js +6 -9
  142. package/dist/domain/workline-mcp-entry.js.map +1 -1
  143. package/dist/ports/postgres-tools.js +10 -0
  144. package/dist/ports/postgres-tools.js.map +1 -0
  145. package/package.json +5 -1
  146. package/skills/w/commands/plan-exec.md +1 -1
  147. package/skills/w/commands/spec-new.md +6 -5
  148. package/skills/w/context/MANIFEST.json +1 -1
  149. package/skills/w/harness/HARNESS.md +1 -1
  150. package/skills/w/hooks/README.md +13 -9
  151. package/skills/w/hooks/hooks.template.json +2 -2
  152. package/skills/w/loops/CHASSIS.md +1 -1
  153. package/skills/w/loops/plan-exec-loop/LOOP.md +7 -5
  154. package/skills/w/loops/plan-new-loop/LOOP.md +46 -41
  155. package/skills/w/loops/plan-refine-loop/LOOP.md +4 -4
  156. package/skills/w/loops/quick-loop/LOOP.md +12 -13
  157. package/skills/w/loops/spec-refine-loop/LOOP.md +7 -3
  158. package/skills/w/modules/COMPACTION.md +1 -1
  159. package/skills/w/modules/PLAN-INPUT.md +3 -3
  160. package/skills/w/modules/PROMPT-CONTINUITY.md +1 -1
  161. package/dist/application/mcp-dbhub-launcher.js +0 -216
  162. package/dist/application/mcp-dbhub-launcher.js.map +0 -1
  163. package/dist/application/self/mcp-offer.js +0 -96
  164. package/dist/application/self/mcp-offer.js.map +0 -1
@@ -1,10 +1,17 @@
1
- import { isDeepStrictEqual } from "node:util";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { MCP_FILE_HOSTS, harnessForMcpHost } from "../../domain/harnesses.js";
3
3
  import { buildMcpEntry, mcpEntryNameFor, normalizeDsnVarName, validateDsnVarName, validateMcpInstance, } from "../../domain/mcp-entry.js";
4
4
  import { dsnKeyForInstance, readDsnFile } from "../dsn-reader-service.js";
5
- import { deleteMcpConnection, readMcpConnections, upsertMcpConnection, } from "../mcp-connections-service.js";
5
+ import { McpConnectionsError, deleteMcpConnection, readMcpConnections, upsertMcpConnection, } from "../mcp-connections-service.js";
6
6
  import { runMcpDoctor } from "../mcp-doctor-service.js";
7
+ import { classifyMcpEntry } from "../mcp-entry-classification.js";
7
8
  import { readMcpEntry } from "../mcp-host-reader.js";
9
+ import { McpHostReceiptError, digestMcpReceiptDescriptor, parseMcpHostReceiptBook, } from "../mcp-host-receipt-service.js";
10
+ import { mcpHostReceiptFile } from "../mcp-host-receipt-store.js";
11
+ import { probePersistedMcpSetupEntries } from "../mcp-launch-probe-service.js";
12
+ import { checkNativeMcpHosts, recordNativeMcpHostChecks, } from "../mcp-native-host-check-service.js";
13
+ import { registerMcpSetupReceipts } from "../mcp-receipt-registration-service.js";
14
+ import { removeMcpRemoveReceipts } from "../mcp-receipt-removal-service.js";
8
15
  import { runMcpRemove } from "../mcp-remove-service.js";
9
16
  import { runMcpSetup } from "../mcp-setup-service.js";
10
17
  import { buildWarpPostInstallHint, } from "../mcp-warp-postinstall-hint.js";
@@ -33,26 +40,39 @@ function isPromptSeparator(choice) {
33
40
  export async function selfMcpConfig(args, ctx, prompts) {
34
41
  const prompt = prompts ?? (await loadPrompts());
35
42
  const resolved = await resolveAction(args, prompt);
36
- switch (resolved.action) {
37
- case "list":
38
- return resolved.fromArgs ? listConnections(ctx) : listConnectionsMenu(args, ctx, prompt);
39
- case "use-env":
40
- return useExistingDsnVar(args, ctx, prompt);
41
- case "create-env":
42
- return createDsnEnvHelp(args, prompt);
43
- case "cancel":
44
- return {
45
- ok: true,
46
- data: { action: "cancel", connection: null, summary: "Operación cancelada." },
47
- exitCode: 0,
48
- };
49
- default:
50
- // install-<host> | doctor | remove — all operate on a registered connection.
51
- return runConnectionAction(args, ctx, prompt, resolved.action);
43
+ try {
44
+ switch (resolved.action) {
45
+ case "list":
46
+ return resolved.fromArgs
47
+ ? listConnections(ctx)
48
+ : await listConnectionsMenu(args, ctx, prompt);
49
+ case "use-env":
50
+ return await useExistingDsnVar(args, ctx, prompt);
51
+ case "create-env":
52
+ return await createDsnEnvHelp(args, prompt);
53
+ case "cancel":
54
+ return {
55
+ ok: true,
56
+ data: { action: "cancel", connection: null, summary: "Operación cancelada." },
57
+ exitCode: 0,
58
+ };
59
+ default:
60
+ // install-<host> | doctor | remove — all operate on a registered connection.
61
+ return await runConnectionAction(args, ctx, prompt, resolved.action);
62
+ }
63
+ }
64
+ catch (error) {
65
+ if (error instanceof McpConnectionsError) {
66
+ return invalidRegistryResult(resolved.action, mcpRegistryIssue(ctx));
67
+ }
68
+ throw error;
52
69
  }
53
70
  }
54
71
  function listConnections(ctx) {
55
- const connections = connectionViews(ctx);
72
+ const views = connectionViews(ctx);
73
+ if (views.kind === "invalid")
74
+ return invalidRegistryResult("list", views.issue);
75
+ const { connections } = views;
56
76
  return {
57
77
  ok: true,
58
78
  data: {
@@ -68,7 +88,10 @@ function listConnections(ctx) {
68
88
  };
69
89
  }
70
90
  async function listConnectionsMenu(args, ctx, prompts) {
71
- const connections = connectionViews(ctx);
91
+ const views = connectionViews(ctx);
92
+ if (views.kind === "invalid")
93
+ return invalidRegistryResult("list", views.issue);
94
+ const { connections } = views;
72
95
  if (connections.length === 0) {
73
96
  return {
74
97
  ok: true,
@@ -136,15 +159,19 @@ async function useExistingDsnVar(args, ctx, prompts) {
136
159
  const connection = connectionView(ctx, {
137
160
  name: write.connection.name,
138
161
  dsnVar: write.connection.dsnVar,
162
+ provider: write.connection.provider,
139
163
  dsnPresent: true,
140
164
  });
165
+ const views = connectionViews(ctx);
166
+ if (views.kind === "invalid")
167
+ return invalidRegistryResult("use-env", views.issue);
141
168
  return {
142
169
  ok: true,
143
170
  data: {
144
171
  action: "use-env",
145
172
  connection,
146
- connections: connectionViews(ctx),
147
- table: formatConnectionsTable(connectionViews(ctx)),
173
+ connections: views.connections,
174
+ table: formatConnectionsTable(views.connections),
148
175
  registry: { path: write.path, changed: true },
149
176
  summary: `Conexión '${connection.nombre}' registrada con ${connection.dsn_var}.`,
150
177
  },
@@ -166,8 +193,10 @@ async function createDsnEnvHelp(args, prompts) {
166
193
  };
167
194
  }
168
195
  async function runConnectionAction(args, ctx, prompts, action) {
169
- const connection = await resolveRegisteredConnection(args, ctx, prompts);
170
- if (connection === null) {
196
+ const resolved = await resolveRegisteredConnection(args, ctx, prompts);
197
+ if (resolved.kind === "invalid")
198
+ return invalidRegistryResult(action, resolved.issue);
199
+ if (resolved.kind === "none") {
171
200
  return {
172
201
  ok: false,
173
202
  error: {
@@ -178,6 +207,7 @@ async function runConnectionAction(args, ctx, prompts, action) {
178
207
  exitCode: 1,
179
208
  };
180
209
  }
210
+ const { connection } = resolved;
181
211
  switch (action) {
182
212
  case "doctor":
183
213
  return doctorConnection(ctx, connection);
@@ -188,20 +218,25 @@ async function runConnectionAction(args, ctx, prompts, action) {
188
218
  return installConnection(args, ctx, connection, action.slice("install-".length));
189
219
  }
190
220
  }
191
- function installConnection(args, ctx, connection, host) {
221
+ async function installConnection(args, ctx, connection, host) {
192
222
  // The explicit install action (CLI/TUI) carries its own narrow global consent;
193
223
  // it is not the public broad `--force` escape hatch.
194
- const setup = runMcpSetup(ctx.env, {
224
+ const setupInput = {
195
225
  hosts: [host],
196
226
  connections: [connection],
227
+ namespace: ctx.paths.namespace,
197
228
  scope: "global",
198
229
  globalApproval: "explicit-self-action",
199
230
  dryRun: args.flags.has("--dry-run"),
200
- });
231
+ };
232
+ const setup = await runGlobalSetupWithEvidence(ctx, setupInput);
201
233
  if ("ok" in setup)
202
234
  return refusal(hostAction(host), connectionView(ctx, connection), setup.hint);
203
235
  const doctor = runDoctor(ctx, connection, [host]);
204
236
  const hasProblems = setup.errors.length > 0 || setup.conflicts.length > 0;
237
+ const views = connectionViews(ctx);
238
+ if (views.kind === "invalid")
239
+ return invalidRegistryResult(hostAction(host), views.issue);
205
240
  // The hint cites the file actually written (per-platform global path).
206
241
  const warpTarget = [...setup.applied, ...setup.skipped].find((r) => r.host === "warp")?.target;
207
242
  const warpHint = host === "warp" && !hasProblems && warpTarget
@@ -212,8 +247,8 @@ function installConnection(args, ctx, connection, host) {
212
247
  data: {
213
248
  action: hostAction(host),
214
249
  connection: connectionView(ctx, connection),
215
- connections: connectionViews(ctx),
216
- table: formatConnectionsTable(connectionViews(ctx)),
250
+ connections: views.connections,
251
+ table: formatConnectionsTable(views.connections),
217
252
  setup,
218
253
  doctor,
219
254
  ...(warpHint ? { warp_hint: warpHint } : {}),
@@ -232,6 +267,49 @@ function installConnection(args, ctx, connection, host) {
232
267
  exitCode: hasProblems ? 1 : 0,
233
268
  };
234
269
  }
270
+ /**
271
+ * The self wizard writes user-scope descriptors, so it must finish the same
272
+ * durable lifecycle as `mcp setup --global`: receipt, exact persisted launch
273
+ * probe, then native host visibility. Keeping that sequence behind one local
274
+ * boundary prevents a future wizard branch from treating a file write as host
275
+ * readiness.
276
+ */
277
+ async function runGlobalSetupWithEvidence(ctx, input) {
278
+ const setup = runMcpSetup(ctx.env, input);
279
+ if ("ok" in setup)
280
+ return setup;
281
+ const receiptRegistration = await registerMcpSetupReceipts(ctx.paths, input, setup);
282
+ const withReceipts = {
283
+ ...setup,
284
+ errors: [...setup.errors, ...receiptRegistration.errors],
285
+ ...(receiptRegistration.registered.length === 0
286
+ ? {}
287
+ : { receipts: receiptRegistration.registered }),
288
+ };
289
+ const launch = await probePersistedMcpSetupEntries(ctx.paths, withReceipts, receiptRegistration.probeTargets);
290
+ const native = await checkNativeMcpHosts(receiptRegistration.probeTargets);
291
+ await recordNativeMcpHostChecks({
292
+ paths: ctx.paths,
293
+ scope: withReceipts.scope,
294
+ scopeDir: withReceipts.scope_dir,
295
+ targets: receiptRegistration.probeTargets,
296
+ checks: native.checks,
297
+ });
298
+ if (receiptRegistration.registered.length === 0 &&
299
+ receiptRegistration.errors.length === 0 &&
300
+ launch.probes.length === 0 &&
301
+ launch.errors.length === 0 &&
302
+ native.checks.length === 0 &&
303
+ native.errors.length === 0) {
304
+ return setup;
305
+ }
306
+ return {
307
+ ...withReceipts,
308
+ errors: [...withReceipts.errors, ...launch.errors, ...native.errors],
309
+ ...(launch.probes.length === 0 ? {} : { launch_probes: launch.probes }),
310
+ ...(native.checks.length === 0 ? {} : { native_checks: native.checks }),
311
+ };
312
+ }
235
313
  function doctorConnection(ctx, connection) {
236
314
  const doctor = runDoctor(ctx, connection, [...FILE_HOSTS]);
237
315
  const allOk = doctor.summary.ok === doctor.reports.length;
@@ -254,85 +332,307 @@ function doctorConnection(ctx, connection) {
254
332
  exitCode: allOk ? 0 : 1,
255
333
  };
256
334
  }
257
- function removeConnection(args, ctx, connection) {
335
+ async function removeConnection(args, ctx, connection) {
258
336
  const dryRun = args.flags.has("--dry-run");
259
337
  // The explicit remove action carries its own narrow global consent.
260
338
  // The writer verifies exact ownership per host and returns a conflict for an
261
339
  // homonymous foreign entry, so this never has to infer ownership from a word
262
340
  // in the command line.
263
- const remove = runMcpRemove(ctx.env, {
341
+ const removeInput = {
264
342
  hosts: [...FILE_HOSTS],
265
343
  connections: [connection],
344
+ namespace: ctx.paths.namespace,
266
345
  scope: "global",
267
346
  globalApproval: "explicit-self-action",
268
347
  dryRun,
269
- });
348
+ };
349
+ const remove = runMcpRemove(ctx.env, removeInput);
270
350
  if ("ok" in remove)
271
351
  return refusal("remove", connectionView(ctx, connection), remove.hint);
272
- const hasProblems = remove.errors.length > 0 || remove.conflicts.length > 0;
273
- const preservedForeign = [...new Set(remove.conflicts.map((conflict) => conflict.host))];
352
+ const receiptErrors = await removeMcpRemoveReceipts(ctx.paths, removeInput, remove);
353
+ const removeWithReceipts = {
354
+ ...remove,
355
+ errors: [...remove.errors, ...receiptErrors],
356
+ };
357
+ const hasProblems = removeWithReceipts.errors.length > 0 || removeWithReceipts.conflicts.length > 0;
358
+ const preservedForeign = [
359
+ ...new Set(removeWithReceipts.conflicts.map((conflict) => conflict.host)),
360
+ ];
274
361
  const deleted = !dryRun && !hasProblems ? deleteMcpConnection(ctx.paths, connection) : null;
275
362
  const preservedNote = preservedForeign.length > 0
276
363
  ? ` Se conservó la entrada ajena homónima en: ${preservedForeign.join(", ")}.`
277
364
  : "";
365
+ const reloadNotice = removalReloadNotice(removeWithReceipts);
366
+ const views = connectionViews(ctx);
367
+ if (views.kind === "invalid")
368
+ return invalidRegistryResult("remove", views.issue);
278
369
  return {
279
370
  ok: !hasProblems,
280
371
  data: {
281
372
  action: "remove",
282
373
  connection: connectionView(ctx, connection),
283
- connections: connectionViews(ctx),
284
- table: formatConnectionsTable(connectionViews(ctx)),
285
- remove,
374
+ connections: views.connections,
375
+ table: formatConnectionsTable(views.connections),
376
+ remove: removeWithReceipts,
286
377
  ...(preservedForeign.length > 0 ? { preserved_foreign: preservedForeign } : {}),
287
378
  ...(deleted ? { registry: { path: deleted.path, changed: deleted.removed } } : {}),
288
379
  summary: dryRun
289
380
  ? `Previsualización de eliminación para '${connection.name}'.${preservedNote}`
290
381
  : hasProblems
291
- ? `Eliminación parcial de '${connection.name}'.${preservedNote}`
292
- : `Conexión '${connection.name}' eliminada de los hosts con MCP y del registro local.${preservedNote}`,
382
+ ? `Eliminación parcial de '${connection.name}'.${preservedNote}${reloadNotice}`
383
+ : `Conexión '${connection.name}' eliminada de los hosts con MCP y del registro local.${preservedNote}${reloadNotice}`,
293
384
  },
294
385
  ...(hasProblems
295
386
  ? {
296
387
  error: {
297
388
  code: "MCP_REMOVE_PARTIAL",
298
- message: `${remove.errors.length} error(es) y ${remove.conflicts.length} conflicto(s) durante remove; ver data.remove.errors y data.remove.conflicts`,
389
+ message: `${removeWithReceipts.errors.length} error(es) y ${removeWithReceipts.conflicts.length} conflicto(s) durante remove; ver data.remove.errors y data.remove.conflicts`,
299
390
  },
300
391
  }
301
392
  : {}),
302
393
  exitCode: hasProblems ? 1 : 0,
303
394
  };
304
395
  }
396
+ function removalReloadNotice(remove) {
397
+ const requirements = remove.reload_required ?? [];
398
+ if (requirements.length === 0)
399
+ return "";
400
+ return ` Recarga requerida: ${requirements
401
+ .map((requirement) => `${hostLabel(requirement.host)}: ${requirement.next_step}`)
402
+ .join(" ")}`;
403
+ }
305
404
  function runDoctor(ctx, connection, hosts) {
306
405
  return runMcpDoctor(ctx.env, ctx.paths, {
307
406
  hosts,
308
407
  connections: [connection],
408
+ namespace: ctx.paths.namespace,
309
409
  scope: "global",
310
410
  });
311
411
  }
312
412
  function connectionViews(ctx) {
313
- return readMcpConnections(ctx.paths, ctx.env).map((connection) => connectionView(ctx, connection));
413
+ try {
414
+ const receiptLedger = readHostReceipts(ctx);
415
+ return {
416
+ kind: "ok",
417
+ connections: readMcpConnections(ctx.paths, ctx.env).map((connection) => connectionView(ctx, connection, receiptLedger)),
418
+ };
419
+ }
420
+ catch (error) {
421
+ if (error instanceof McpConnectionsError) {
422
+ return { kind: "invalid", issue: mcpRegistryIssue(ctx) };
423
+ }
424
+ throw error;
425
+ }
426
+ }
427
+ function mcpRegistryIssue(ctx) {
428
+ return {
429
+ code: "MCP_CONNECTION_INVALID",
430
+ path: ctx.paths.userMcpConnectionsFile(),
431
+ recovery: "Restaurá mcp-connections.json desde una copia válida o corregí su forma antes de volver a operar.",
432
+ };
433
+ }
434
+ function invalidRegistryResult(action, issue) {
435
+ return {
436
+ ok: false,
437
+ error: {
438
+ code: issue.code,
439
+ message: "El registro MCP local no es válido y requiere reparación antes de operar.",
440
+ },
441
+ data: {
442
+ action,
443
+ connection: null,
444
+ connections: [],
445
+ table: formatConnectionsTable([]),
446
+ registry_error: issue,
447
+ summary: issue.recovery,
448
+ },
449
+ exitCode: 2,
450
+ };
314
451
  }
315
- function connectionView(ctx, connection) {
452
+ function connectionView(ctx, connection, receiptLedger = readHostReceipts(ctx)) {
453
+ const hostStatus = Object.fromEntries(FILE_HOSTS.map((host) => [host, installStatus(ctx, connection, host, receiptLedger)]));
316
454
  return {
317
455
  nombre: connection.name,
318
456
  server_name: mcpEntryNameFor(connection.name),
319
457
  dsn_var: connection.dsnVar,
320
458
  dsn_visible: isDsnVisible(ctx, connection.dsnVar),
321
- instalado: Object.fromEntries(FILE_HOSTS.map((h) => [h, installStatus(ctx, connection, h)])),
459
+ instalado: Object.fromEntries(FILE_HOSTS.map((host) => [host, installMark(hostStatus[host])])),
460
+ host_status: hostStatus,
322
461
  };
323
462
  }
324
- function installStatus(ctx, connection, host) {
325
- const entry = buildMcpEntry(connection.name, connection.dsnVar);
463
+ /**
464
+ * Status must be derived from the exact descriptor that this host would load,
465
+ * not from a host-agnostic default. Global descriptors carry the resolved
466
+ * namespace and host identity in their argv, so both are part of ownership.
467
+ */
468
+ function installStatus(ctx, connection, host, receiptLedger) {
469
+ const entry = buildMcpEntry(connection.name, connection.dsnVar, {
470
+ host,
471
+ scope: "global",
472
+ namespace: ctx.paths.namespace,
473
+ });
326
474
  const snapshot = readMcpEntry(host, ctx.env.homeDir(), entry.name, "global");
327
- if (!snapshot.exists)
328
- return "no";
329
- if (snapshot.command !== entry.command)
330
- return "drift";
331
- if (!isDeepStrictEqual(snapshot.args ?? [], entry.args))
332
- return "drift";
333
- if (!isDeepStrictEqual(snapshot.env ?? {}, entry.env))
334
- return "drift";
335
- return "si";
475
+ const classification = classifyMcpEntry(host, snapshot, entry, connection);
476
+ const ledgerFailure = receiptLedgerFailureStatus(classification.state, receiptLedger);
477
+ if (ledgerFailure !== undefined)
478
+ return ledgerFailure;
479
+ const entryStatus = nonCurrentEntryStatus(classification.state);
480
+ if (entryStatus !== undefined)
481
+ return entryStatus;
482
+ const receipt = receiptLedger.receipts.find((candidate) => candidate.host === host &&
483
+ candidate.scope === "global" &&
484
+ candidate.connection === connection.name);
485
+ if (receipt !== undefined && !receiptMatchesDescriptor(receipt, snapshot)) {
486
+ return staleReceiptStatus(classification.state);
487
+ }
488
+ return currentDescriptorStatus(classification.state, receipt);
489
+ }
490
+ function receiptLedgerFailureStatus(entryState, receiptLedger) {
491
+ if (receiptLedger.failure === undefined)
492
+ return undefined;
493
+ return {
494
+ state: "failed",
495
+ entry_state: entryState,
496
+ launchable: false,
497
+ reload_required: false,
498
+ receipt_failure: { phase: "receipt", code: receiptLedger.failure.code },
499
+ };
500
+ }
501
+ function nonCurrentEntryStatus(entryState) {
502
+ switch (entryState) {
503
+ case "missing":
504
+ return {
505
+ state: "registered",
506
+ entry_state: entryState,
507
+ launchable: false,
508
+ reload_required: false,
509
+ };
510
+ case "known-legacy":
511
+ return {
512
+ state: "legacy",
513
+ entry_state: entryState,
514
+ launchable: false,
515
+ reload_required: true,
516
+ };
517
+ case "foreign":
518
+ return {
519
+ state: "conflict",
520
+ entry_state: entryState,
521
+ launchable: false,
522
+ reload_required: false,
523
+ };
524
+ case "malformed":
525
+ return {
526
+ state: "failed",
527
+ entry_state: entryState,
528
+ launchable: false,
529
+ reload_required: false,
530
+ };
531
+ case "current":
532
+ return undefined;
533
+ }
534
+ }
535
+ function staleReceiptStatus(entryState) {
536
+ return {
537
+ state: "failed",
538
+ entry_state: entryState,
539
+ launchable: false,
540
+ reload_required: true,
541
+ receipt_failure: { phase: "descriptor", code: "MCP_RECEIPT_DESCRIPTOR_STALE" },
542
+ };
543
+ }
544
+ function currentDescriptorStatus(entryState, receipt) {
545
+ // A current descriptor without a receipt may be the aftermath of a busy
546
+ // receipt ledger after the host file was already written. It has no durable
547
+ // descriptor digest, lifecycle evidence, or way for a later host reload to
548
+ // clear reload_required, so presenting it as merely configured would hide
549
+ // the required recovery action.
550
+ if (receipt === undefined) {
551
+ return {
552
+ state: "failed",
553
+ entry_state: entryState,
554
+ launchable: false,
555
+ reload_required: true,
556
+ receipt_failure: { phase: "receipt", code: "MCP_RECEIPT_NOT_FOUND" },
557
+ };
558
+ }
559
+ const lastProbe = receipt?.last_launch_probe;
560
+ const nativeCheckFailure = receipt?.last_native_check_failure;
561
+ const launchable = lastProbe?.outcome === "passed";
562
+ const reloadRequired = receipt?.reload_required ?? false;
563
+ const evidence = {
564
+ entry_state: entryState,
565
+ launchable,
566
+ reload_required: reloadRequired,
567
+ ...(lastProbe === undefined
568
+ ? {}
569
+ : {
570
+ last_probe: {
571
+ outcome: lastProbe.outcome,
572
+ phase: lastProbe.phase,
573
+ observed_at: lastProbe.observed_at,
574
+ },
575
+ }),
576
+ ...(receipt?.last_host_load_observed === undefined
577
+ ? {}
578
+ : { last_host_load_observed: receipt.last_host_load_observed.observed_at }),
579
+ ...(nativeCheckFailure === undefined
580
+ ? {}
581
+ : {
582
+ native_check_failure: {
583
+ code: nativeCheckFailure.code,
584
+ observed_at: nativeCheckFailure.observed_at,
585
+ },
586
+ }),
587
+ };
588
+ if (lastProbe?.outcome === "failed" || nativeCheckFailure !== undefined) {
589
+ return { state: "failed", ...evidence };
590
+ }
591
+ if (receipt?.last_host_load_observed !== undefined && !reloadRequired) {
592
+ return { state: "host-load-observed", ...evidence };
593
+ }
594
+ if (reloadRequired)
595
+ return { state: "reload-required", ...evidence };
596
+ if (launchable)
597
+ return { state: "launchable", ...evidence };
598
+ return { state: "configured", ...evidence };
599
+ }
600
+ function receiptMatchesDescriptor(receipt, snapshot) {
601
+ if (snapshot.command === undefined || snapshot.args === undefined)
602
+ return false;
603
+ try {
604
+ return (receipt.descriptor_digest ===
605
+ digestMcpReceiptDescriptor({ command: snapshot.command, args: snapshot.args }));
606
+ }
607
+ catch {
608
+ return false;
609
+ }
610
+ }
611
+ function installMark(status) {
612
+ return status.entry_state === "missing"
613
+ ? "no"
614
+ : status.entry_state === "current"
615
+ ? "si"
616
+ : "drift";
617
+ }
618
+ function readHostReceipts(ctx) {
619
+ const file = mcpHostReceiptFile(ctx.paths);
620
+ if (!existsSync(file))
621
+ return { receipts: [] };
622
+ try {
623
+ return { receipts: parseMcpHostReceiptBook(readFileSync(file, "utf8")).receipts };
624
+ }
625
+ catch (error) {
626
+ // The registry remains readable, but no lifecycle claim survives a corrupt
627
+ // receipt ledger. Every host becomes explicitly actionable rather than
628
+ // quietly falling back to an unproven "configured" state.
629
+ return {
630
+ receipts: [],
631
+ failure: {
632
+ code: error instanceof McpHostReceiptError ? error.code : "MCP_RECEIPT_UNREADABLE",
633
+ },
634
+ };
635
+ }
336
636
  }
337
637
  export function isDsnVisible(ctx, dsnVar) {
338
638
  if (ctx.env.get(dsnVar))
@@ -383,9 +683,18 @@ function renderBoxTable(headers, rows) {
383
683
  return out.join("\n");
384
684
  }
385
685
  async function resolveRegisteredConnection(args, ctx, prompts) {
386
- const connections = readMcpConnections(ctx.paths, ctx.env);
686
+ let connections;
687
+ try {
688
+ connections = readMcpConnections(ctx.paths, ctx.env);
689
+ }
690
+ catch (error) {
691
+ if (error instanceof McpConnectionsError) {
692
+ return { kind: "invalid", issue: mcpRegistryIssue(ctx) };
693
+ }
694
+ throw error;
695
+ }
387
696
  if (connections.length === 0)
388
- return null;
697
+ return { kind: "none" };
389
698
  const raw = args.values.get("name") ?? args.values.get("instance");
390
699
  if (raw !== undefined) {
391
700
  const validation = validateMcpInstance(raw);
@@ -395,7 +704,7 @@ async function resolveRegisteredConnection(args, ctx, prompts) {
395
704
  if (found === undefined) {
396
705
  throw new Error(`conexión MCP no registrada: '${validation.value}'`);
397
706
  }
398
- return found;
707
+ return { kind: "found", connection: found };
399
708
  }
400
709
  const defaultConnection = connections[0]?.name;
401
710
  const selected = await prompts.select({
@@ -406,7 +715,8 @@ async function resolveRegisteredConnection(args, ctx, prompts) {
406
715
  value: item.name,
407
716
  })),
408
717
  });
409
- return connections.find((item) => item.name === selected) ?? null;
718
+ const connection = connections.find((item) => item.name === selected);
719
+ return connection === undefined ? { kind: "none" } : { kind: "found", connection };
410
720
  }
411
721
  async function resolveConnectionName(args, prompts) {
412
722
  const raw = args.values.get("name") ?? args.values.get("instance");
@@ -504,7 +814,7 @@ async function resolveAction(args, prompts) {
504
814
  return { action: raw, fromArgs: true };
505
815
  return {
506
816
  action: await prompts.select({
507
- message: "Configurar MCP database (dbhub)",
817
+ message: "Configurar MCP database (PostgreSQL Workline)",
508
818
  default: "list",
509
819
  choices: [
510
820
  { type: "separator", separator: "── Conexiones existentes ──" },