@milpa/comal 0.1.6 → 0.2.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 (137) hide show
  1. package/README.md +19 -1
  2. package/backend/contracts/remote-commands-v1.json +12 -0
  3. package/backend/contracts/remote-workspace-v1.schema.json +287 -0
  4. package/governance/chrome-task-v10.json +323 -0
  5. package/governance/chrome-task-v11.json +355 -0
  6. package/governance/chrome-task-v12.json +387 -0
  7. package/governance/chrome-task-v13.json +419 -0
  8. package/governance/chrome-task-v3.json +99 -0
  9. package/governance/chrome-task-v4.json +131 -0
  10. package/governance/chrome-task-v5.json +163 -0
  11. package/governance/chrome-task-v6.json +195 -0
  12. package/governance/chrome-task-v7.json +227 -0
  13. package/governance/chrome-task-v8.json +259 -0
  14. package/governance/chrome-task-v9.json +291 -0
  15. package/governance/source-task-v1.json +1 -0
  16. package/governance/source-task-v2.json +1 -0
  17. package/package.json +15 -1
  18. package/src/agent-recovery-proof.mjs +12 -1
  19. package/src/agent.mjs +94 -30
  20. package/src/audit.mjs +21 -6
  21. package/src/auto-guard.mjs +48 -1
  22. package/src/chrome-candidate-recovery-proof.mjs +47 -0
  23. package/src/chrome-recovery-proof.mjs +6 -2
  24. package/src/chrome-source-syntax.mjs +93 -0
  25. package/src/cli.mjs +161 -56
  26. package/src/codex-provider.mjs +3 -2
  27. package/src/connection-settings.mjs +204 -0
  28. package/src/context-pressure.mjs +2 -1
  29. package/src/durable-agent-runs.mjs +5 -11
  30. package/src/event-channels.mjs +8 -0
  31. package/src/file-recovery-proof.mjs +11 -3
  32. package/src/file-recovery.mjs +24 -0
  33. package/src/gemini-provider.mjs +6 -1
  34. package/src/git-agent-revision.mjs +12 -0
  35. package/src/git-agent-workspaces.mjs +143 -0
  36. package/src/git-integration-lineage.mjs +20 -0
  37. package/src/git-integration-operations.mjs +200 -0
  38. package/src/git-integration-recovery-proof.mjs +70 -0
  39. package/src/git-page-cli-bridge.mjs +61 -0
  40. package/src/git-page-evidence.mjs +30 -0
  41. package/src/git-page-identity.mjs +10 -0
  42. package/src/git-page-proof.mjs +96 -0
  43. package/src/git-page-settlement.mjs +21 -0
  44. package/src/git-page-verification.mjs +134 -0
  45. package/src/git-plumbing.mjs +212 -0
  46. package/src/git-process-worker.mjs +50 -0
  47. package/src/git-source.mjs +110 -0
  48. package/src/git-workspaces.mjs +519 -0
  49. package/src/goals.mjs +13 -7
  50. package/src/house.mjs +246 -20
  51. package/src/i18n/cli-es.mjs +4 -0
  52. package/src/i18n/es.mjs +35 -0
  53. package/src/judges.mjs +27 -11
  54. package/src/mcp-runtime.mjs +15 -3
  55. package/src/model-controls.mjs +11 -1
  56. package/src/model-deadline.mjs +4 -3
  57. package/src/native-refusal.mjs +38 -0
  58. package/src/onboarding.mjs +88 -24
  59. package/src/openai-provider.mjs +14 -7
  60. package/src/operations.mjs +9 -2
  61. package/src/output-recovery.mjs +48 -0
  62. package/src/peer-tools.mjs +18 -8
  63. package/src/permission-controls.mjs +232 -0
  64. package/src/permission-rules.mjs +312 -0
  65. package/src/permissions.mjs +113 -30
  66. package/src/plugin-catalog.mjs +84 -0
  67. package/src/plugin-config.mjs +48 -6
  68. package/src/plugin-controls.mjs +332 -0
  69. package/src/plugin-notices.mjs +88 -0
  70. package/src/plugin-trust.mjs +104 -0
  71. package/src/plugins/chrome-lab/bridge.mjs +38 -12
  72. package/src/plugins/chrome-lab/client.mjs +103 -25
  73. package/src/plugins/chrome-lab/diagnostics.mjs +19 -0
  74. package/src/plugins/chrome-lab/index.mjs +116 -26
  75. package/src/plugins/chrome-lab/project.mjs +30 -2
  76. package/src/plugins/chrome-lab/scoped-input.mjs +135 -0
  77. package/src/plugins/chrome-lab/worker-diagnostics.mjs +51 -0
  78. package/src/plugins/open-relay/actions.mjs +205 -0
  79. package/src/plugins/open-relay/bound.mjs +47 -0
  80. package/src/plugins/open-relay/client.mjs +158 -0
  81. package/src/plugins/open-relay/dossier.mjs +119 -0
  82. package/src/plugins/open-relay/index.mjs +153 -0
  83. package/src/plugins/open-relay/lookup.mjs +45 -0
  84. package/src/plugins/open-relay/notices.mjs +79 -0
  85. package/src/plugins/open-relay/watch.mjs +127 -0
  86. package/src/plugins.mjs +72 -29
  87. package/src/private-fs.mjs +40 -0
  88. package/src/process-lock.mjs +24 -13
  89. package/src/provider-auth.mjs +14 -3
  90. package/src/provider.mjs +11 -2
  91. package/src/read-only-check.mjs +7 -3
  92. package/src/remote-control.mjs +29 -4
  93. package/src/remote-workspace.mjs +50 -0
  94. package/src/scope-ceiling.mjs +56 -0
  95. package/src/scoped-policy.mjs +39 -0
  96. package/src/secret-login.mjs +68 -0
  97. package/src/session-bus.mjs +41 -7
  98. package/src/session-projections-candidate.mjs +119 -0
  99. package/src/session-projections.mjs +119 -0
  100. package/src/session-read-cache.mjs +23 -3
  101. package/src/sessions.mjs +35 -11
  102. package/src/source-editor-role.mjs +13 -0
  103. package/src/source-ownership.mjs +73 -0
  104. package/src/source-proposal-proof.mjs +55 -0
  105. package/src/source-proposals.mjs +118 -0
  106. package/src/source-requests.mjs +148 -0
  107. package/src/source-task-proof.mjs +24 -0
  108. package/src/source-task-revision-proof.mjs +44 -0
  109. package/src/subagents.mjs +187 -87
  110. package/src/tool-packs.mjs +5 -5
  111. package/src/tools.mjs +82 -15
  112. package/src/trusted-context.mjs +5 -1
  113. package/src/tui/connection-transition.mjs +1 -1
  114. package/src/tui/controller.mjs +8 -4
  115. package/src/tui/discovery.mjs +24 -12
  116. package/src/tui/inbound-blocks.mjs +55 -0
  117. package/src/tui/present.mjs +504 -0
  118. package/src/tui/screen.mjs +49 -13
  119. package/src/windows-private.ps1 +61 -0
  120. package/src/work-assertions.mjs +6 -2
  121. package/src/work-plan.mjs +34 -8
  122. package/src/work-state.mjs +15 -9
  123. package/src/workflows/chrome-page-cases.mjs +156 -0
  124. package/src/workflows/chrome-page-checkpoint-reader.mjs +10 -0
  125. package/src/workflows/chrome-page-diagnostics.mjs +113 -0
  126. package/src/workflows/chrome-page-law.mjs +48 -0
  127. package/src/workflows/chrome-page-recovery-host-v13.mjs +14 -0
  128. package/src/workflows/chrome-page-recovery-v13.mjs +104 -0
  129. package/src/workflows/chrome-page-task.mjs +429 -0
  130. package/src/workflows/chrome-page-verifier.mjs +123 -0
  131. package/src/workflows/chrome-source.mjs +73 -0
  132. package/src/workflows/chrome-supplied-inputs.mjs +43 -0
  133. package/src/workflows/chrome-task-adoption.mjs +122 -0
  134. package/src/workflows/chrome-task.mjs +379 -74
  135. package/src/workflows/source-task-v1.mjs +124 -0
  136. package/src/workflows/source-task-v2.mjs +227 -0
  137. package/src/workflows/source-task.mjs +13 -0
@@ -0,0 +1,504 @@
1
+ // Presentación legible de resultados estructurados para la terminal.
2
+ // Solo presenta: nunca cambia el valor recibido, no traduce datos (solo las
3
+ // etiquetas que escribe el host) y sanea todo texto externo antes de pintarlo.
4
+ // El JSON exacto sigue disponible con /raw (o «r» en la tarjeta de permiso).
5
+ import { t, languageInfo, formatNumber, registerMessages } from '../i18n/index.mjs';
6
+ import { DataTableParser } from './markdown.mjs';
7
+ import { safeText, stripANSI, createTones, tone as globalTone, wrap, width, clip, pad, graphemes } from './text.mjs';
8
+
9
+ registerMessages({
10
+ 'yes': 'sí', 'no': 'no', 'none': 'ninguno', '(empty)': '(vacío)', '(no fields)': '(sin campos)',
11
+ '… ({v0} more)': '… ({v0} más)', '… ({v0} more lines)': '… ({v0} líneas más)', '… ({v0} more characters)': '… ({v0} caracteres más)',
12
+ '{v0} fields': '{v0} campos', '{v0} items': '{v0} elementos', 'Other fields: {v0}': 'Otros campos: {v0}',
13
+ '/raw shows the exact JSON': '/raw muestra el JSON exacto', 'exact JSON': 'JSON exacto',
14
+ 'Raw view on: results and permissions show the exact JSON. /raw returns to the readable view.': 'Vista cruda activada: resultados y permisos muestran el JSON exacto. /raw vuelve a la vista legible.',
15
+ 'Readable view on. /raw shows the exact JSON again.': 'Vista legible activada. /raw vuelve a mostrar el JSON exacto.',
16
+ 'Use /raw without arguments.': 'Usa /raw sin argumentos.',
17
+ 'Toggle the exact JSON of results and permissions': 'Alterna el JSON exacto de resultados y permisos',
18
+ ' · r shows JSON': ' · r muestra el JSON', ' · r readable view': ' · r vista legible',
19
+ 'Summary declared by the operation (the arguments below are what runs):': 'Resumen declarado por la operación (lo que se ejecuta son los argumentos de abajo):',
20
+ 'Contains control characters, shown as \\u{…}.': 'Contiene caracteres de control, mostrados como \\u{…}.',
21
+ '{v0} lines · {v1}': '{v0} líneas · {v1}', 'Replace {v0} lines with {v1} lines': 'Reemplaza {v0} líneas por {v1} líneas',
22
+ 'all occurrences': 'todas las apariciones', 'Removed': 'Sale', 'Added': 'Entra',
23
+ '{v0} tokens': '{v0} tokens', 'levels {v0}': 'niveles {v0}', 'source {v0}': 'origen {v0}', 'announced {v0}': 'anunciada {v0}', 'limit {v0}': 'límite {v0}',
24
+ '{v0} returned · {v1} failed · {v2} refused': '{v0} devueltas · {v1} fallidas · {v2} rechazadas',
25
+ 'Receipts ({v0})': 'Recibos ({v0})', 'Plugins ({v0})': 'Plugins ({v0})', '{v0} operations': '{v0} operaciones',
26
+ 'Digests and complete receipts: /raw': 'Huellas y recibos completos: /raw', 'built-in': 'integrado', 'local': 'local', 'core': 'núcleo',
27
+ 'F6 → Tab shows each operation with its description and arguments.': 'F6 → Tab muestra cada operación con su descripción y argumentos.',
28
+ 'Ctrl+O expands lists and long texts.': 'Ctrl+O amplía listas y textos largos.',
29
+ });
30
+
31
+ // Etiquetas frecuentes en español. Solo se aplican a etiquetas derivadas de
32
+ // claves; los valores nunca se traducen.
33
+ const LABELS_ES = Object.freeze({
34
+ 'Status': 'Estado', 'State': 'Estado', 'Version': 'Versión', 'Model': 'Modelo', 'Operations': 'Operaciones', 'Operation': 'Operación',
35
+ 'Services': 'Servicios', 'Browser': 'Navegador', 'Extensions': 'Extensiones', 'Remote root': 'Raíz remota', 'Connection ID': 'ID de conexión',
36
+ 'Protocol version': 'Versión del protocolo', 'Extension commands': 'Comandos de extensión', 'SSH host': 'Host SSH', 'Can deploy': 'Puede desplegar',
37
+ 'Console collection': 'Captura de consola', 'Other extensions count': 'Otras extensiones', 'Extensions truncated': 'Extensiones recortadas',
38
+ 'Browser endpoint ID': 'ID del endpoint del navegador', 'Config path': 'Configuración', 'Context budget': 'Presupuesto de contexto',
39
+ 'Context window': 'Ventana de contexto', 'Announced window': 'Ventana anunciada', 'Configured limit': 'Límite configurado', 'Kind': 'Tipo',
40
+ 'Source': 'Origen', 'Description': 'Descripción', 'Name': 'Nombre', 'Effects': 'Efectos', 'Audience': 'Audiencia', 'Origin': 'Procedencia', 'Trust': 'Confianza',
41
+ 'Counts': 'Conteos', 'Receipts': 'Recibos', 'Unresolved': 'Sin resolver', 'Running': 'En curso', 'Error': 'Error', 'Code': 'Código',
42
+ 'Details': 'Detalles', 'Message': 'Mensaje', 'Path': 'Ruta', 'File path': 'Archivo', 'Command': 'Comando', 'Content': 'Contenido',
43
+ 'Timeout': 'Tiempo límite', 'Verified': 'Verificado', 'Violations': 'Violaciones', 'Returned': 'Devueltas', 'Failed': 'Fallidas',
44
+ 'Refused': 'Rechazadas', 'Label': 'Etiqueta', 'Method': 'Método', 'Protocol': 'Protocolo', 'Subscription': 'Suscripción',
45
+ 'Profiles': 'Perfiles', 'Default profile': 'Perfil por omisión', 'Login providers': 'Proveedores de acceso', 'Documentation': 'Documentación',
46
+ 'Active calls': 'Llamadas activas', 'Tool loading': 'Carga', 'Builtin': 'Integrado', 'Requires': 'Requiere', 'Provides': 'Provee',
47
+ 'External': 'Externo', 'Process': 'Proceso', 'Authority': 'Autoridad', 'Capabilities': 'Capacidades', 'Effort': 'Esfuerzo',
48
+ 'Levels': 'Niveles', 'Selected': 'Elegido', 'Supported': 'Soportado', 'Timestamp': 'Fecha', 'Cards': 'Tarjetas', 'Next action': 'Siguiente acción',
49
+ 'Pattern': 'Patrón', 'Query': 'Consulta', 'Executions': 'Ejecuciones', 'Asked': 'Juicio', 'Permission refusals': 'Permisos rechazados',
50
+ 'Actor': 'Actor', 'Plugin': 'Plugin', 'Calls': 'Llamadas', 'Loading': 'Carga', 'Sources': 'Fuentes', 'Scope': 'Alcance', 'Plan': 'Plan',
51
+ 'Rule': 'Regla', 'List': 'Lista', 'Warning': 'Advertencia', 'Effect': 'Efecto', 'Note': 'Nota', 'Allow': 'Autorizar', 'Still ask': 'Siguen preguntando',
52
+ 'Changed': 'Cambios', 'Module': 'Módulo', 'Project': 'Proyecto', 'Files': 'Archivos', 'Code fingerprint': 'Huella del código', 'Uncovered packages': 'Paquetes sin cubrir',
53
+ 'Replace all': 'Reemplazar todas', 'Offset': 'Desde', 'Limit': 'Límite', 'Recent': 'Recientes', 'Text': 'Texto', 'Result': 'Resultado',
54
+ });
55
+ const ACRONYMS = new Map([['id', 'ID'], ['ids', 'IDs'], ['url', 'URL'], ['uri', 'URI'], ['cdp', 'CDP'], ['ssh', 'SSH'], ['api', 'API'], ['mcp', 'MCP'],
56
+ ['json', 'JSON'], ['pid', 'PID'], ['uuid', 'UUID'], ['sha256', 'SHA-256'], ['cpu', 'CPU'], ['dom', 'DOM'], ['http', 'HTTP'], ['tui', 'TUI'],
57
+ ['cli', 'CLI'], ['llm', 'LLM'], ['os', 'OS'], ['ui', 'UI'], ['html', 'HTML'], ['css', 'CSS']]);
58
+
59
+ const LIMITS = Object.freeze({
60
+ normal: Object.freeze({ items: 12, rows: 20, lines: 6, chars: 600, depth: 4, fields: 60, textLines: 400 }),
61
+ expanded: Object.freeze({ items: 500, rows: 500, lines: 200, chars: 20000, depth: 8, fields: 500, textLines: 5000 }),
62
+ });
63
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})$/;
64
+ const DIGEST = /^(?:sha256:)?[0-9a-f]{40,128}$/i;
65
+ const DURATION_KEY = /(?:_ms|Ms)$|^ms$|^timeout$/;
66
+ const BYTES_KEY = /(?:bytes|Bytes|_size|Size)$|^size$/;
67
+ const EPOCH_KEY = /(?:_at|At|timestamp|Timestamp)$/;
68
+ const PLAIN_NUMBER_KEY = /(?:^|_)(?:pid|port|id|row|sequence|version|year|start)$|(?:Id|Port|Row)$/i;
69
+ const MAX_PARSE = 4_000_000;
70
+
71
+ // Texto visible: en las tarjetas de permiso un carácter de control no se borra
72
+ // en silencio (cambiaría lo que el humano cree aprobar); se muestra escapado.
73
+ const INVISIBLE = /[\x00-\x08\x0b-\x1f\x7f-\x9f\u200b-\u200f\u2028\u2029\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/g;
74
+ export function visibleText(value) {
75
+ return String(value ?? '').replace(/\t/g, ' ').replace(INVISIBLE, char => `\\u{${char.codePointAt(0).toString(16)}}`);
76
+ }
77
+ const INVISIBLE_TEST = new RegExp(INVISIBLE.source);
78
+ // Recorre claves y valores (acotado) buscando caracteres invisibles o de control.
79
+ function containsInvisible(value, depth = 0, budget = { nodes: 20000 }) {
80
+ if (--budget.nodes < 0 || depth > 32) return false;
81
+ if (typeof value === 'string') return INVISIBLE_TEST.test(value);
82
+ if (Array.isArray(value)) return value.some(item => containsInvisible(item, depth + 1, budget));
83
+ if (value && typeof value === 'object') return Object.entries(value).some(([key, item]) => INVISIBLE_TEST.test(key) || containsInvisible(item, depth + 1, budget));
84
+ return false;
85
+ }
86
+
87
+ function options(input = {}) {
88
+ const expanded = Boolean(input.expanded), limits = { ...(expanded ? LIMITS.expanded : LIMITS.normal), ...(input.limits || {}) };
89
+ const tones = input.color === false ? createTones({ colorMode: 'none' }) : globalTone;
90
+ return { columns: Math.max(4, Math.floor(input.columns || 80)), expanded, limits, tones, color: input.color !== false,
91
+ sanitize: input.sanitize || safeText, now: input.now ?? Date.now(), timeZone: input.timeZone };
92
+ }
93
+ const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
94
+ const isScalar = value => value === null || value === undefined || ['string', 'number', 'boolean', 'bigint'].includes(typeof value);
95
+ const more = (n, o) => o.tones.muted(t('… ({v0} more)', { v0: formatNumber(n) }));
96
+
97
+ export function humanizeKey(key, sanitize = safeText) {
98
+ const raw = sanitize(String(key)).replace(/\s+/g, ' ').trim();
99
+ const words = raw.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2').replace(/[_.\-]+/g, ' ').trim().split(/\s+/).filter(Boolean);
100
+ if (!words.length) return raw || '—';
101
+ const text = words.map((word, index) => {
102
+ const known = ACRONYMS.get(word.toLowerCase()); if (known) return known;
103
+ if (/^[A-Z0-9]{2,}$/.test(word)) return word;
104
+ const lower = word.toLowerCase(); return index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower;
105
+ }).join(' ');
106
+ return languageInfo().language === 'es' ? LABELS_ES[text] || text : text;
107
+ }
108
+
109
+ export function formatDuration(ms) {
110
+ const value = Math.abs(Number(ms)), sign = ms < 0 ? '-' : '';
111
+ if (!Number.isFinite(value)) return String(ms);
112
+ if (value < 1000) return `${sign}${formatNumber(Math.round(value))} ms`;
113
+ if (value < 60000) return `${sign}${formatNumber(value / 1000, { maximumFractionDigits: 1 })} s`;
114
+ if (value < 3600000) { const m = Math.floor(value / 60000), s = Math.round((value % 60000) / 1000); return `${sign}${m} min${s ? ` ${s} s` : ''}`; }
115
+ const h = Math.floor(value / 3600000), m = Math.round((value % 3600000) / 60000); return `${sign}${formatNumber(h)} h${m ? ` ${m} min` : ''}`;
116
+ }
117
+ export function formatBytes(bytes) {
118
+ const value = Number(bytes); if (!Number.isFinite(value)) return String(bytes);
119
+ const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = Math.abs(value), unit = 0;
120
+ while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit++; }
121
+ return `${value < 0 ? '-' : ''}${formatNumber(size, { maximumFractionDigits: unit ? 1 : 0 })} ${units[unit]}`;
122
+ }
123
+ export function formatDate(value, { now = Date.now(), timeZone, compact = false } = {}) {
124
+ const date = value instanceof Date ? value : new Date(value);
125
+ if (Number.isNaN(date.getTime())) return String(value);
126
+ const language = languageInfo().language;
127
+ let absolute;
128
+ try { absolute = new Intl.DateTimeFormat(language, { dateStyle: 'medium', timeStyle: 'medium', ...(timeZone ? { timeZone } : {}) }).format(date); }
129
+ catch { absolute = date.toISOString(); }
130
+ const seconds = Math.round((date.getTime() - now) / 1000), size = Math.abs(seconds);
131
+ if (compact && (!Number.isFinite(seconds) || size >= 86400)) { try { return new Intl.DateTimeFormat(language, { dateStyle: 'short', timeStyle: 'short', ...(timeZone ? { timeZone } : {}) }).format(date); } catch { return absolute; } }
132
+ if (!Number.isFinite(seconds) || size > 30 * 86400) return absolute;
133
+ const [amount, unit] = size < 60 ? [seconds, 'second'] : size < 3600 ? [Math.round(seconds / 60), 'minute'] : size < 86400 ? [Math.round(seconds / 3600), 'hour'] : [Math.round(seconds / 86400), 'day'];
134
+ const relative = new Intl.RelativeTimeFormat(language, { numeric: 'auto' }).format(amount, unit);
135
+ return compact ? relative : `${absolute} (${relative})`;
136
+ }
137
+
138
+ // Escalares en texto plano + un tono semántico; las tablas usan solo el texto.
139
+ function scalar(value, key, o) {
140
+ key = String(key || '');
141
+ if (value === null || value === undefined) return { text: '—', kind: 'muted' };
142
+ if (typeof value === 'boolean') return { text: value ? t('yes') : t('no'), kind: value ? 'good' : 'muted' };
143
+ if (typeof value === 'bigint') return { text: value.toString(), kind: 'text' };
144
+ if (typeof value === 'number') {
145
+ if (!Number.isFinite(value)) return { text: String(value), kind: 'text' };
146
+ if (DURATION_KEY.test(key)) return { text: formatDuration(value), kind: 'text' };
147
+ if (BYTES_KEY.test(key)) return { text: formatBytes(value), kind: 'text' };
148
+ if (EPOCH_KEY.test(key) && value > 1e11 && value < 1e14) return { text: formatDate(value, o), kind: 'text' };
149
+ return { text: PLAIN_NUMBER_KEY.test(key) || !Number.isInteger(value) && Math.abs(value) < 1 ? String(value) : formatNumber(value), kind: 'text' };
150
+ }
151
+ const text = o.sanitize(value);
152
+ if (!text) return { text: t('(empty)'), kind: 'muted' };
153
+ if (ISO_DATE.test(text)) return { text: formatDate(text, o), kind: 'text' };
154
+ if (DIGEST.test(text) && !o.expanded) return { text: text.replace(/^sha256:/, '').slice(0, 12) + '…', kind: 'muted' };
155
+ return { text, kind: 'text' };
156
+ }
157
+ const paint = (item, o) => item.kind === 'text' ? item.text : o.tones[item.kind](item.text);
158
+
159
+ // Recorta por caracteres y por líneas con indicador «… (N más)».
160
+ function textBlock(item, columns, o) {
161
+ let text = item.text; const glyphs = graphemes(text), extra = glyphs.length - o.limits.chars;
162
+ if (extra > 0) text = glyphs.slice(0, o.limits.chars).join('');
163
+ let lines = wrap(text, Math.max(1, columns));
164
+ const hidden = lines.length - o.limits.lines;
165
+ if (hidden > 0) lines = lines.slice(0, o.limits.lines);
166
+ lines = lines.map(line => paint({ ...item, text: line }, o));
167
+ if (hidden > 0) lines.push(o.tones.muted(t('… ({v0} more lines)', { v0: formatNumber(hidden) })));
168
+ else if (extra > 0) lines.push(o.tones.muted(t('… ({v0} more characters)', { v0: formatNumber(extra) })));
169
+ return lines;
170
+ }
171
+
172
+ function inlineList(values, key, o) {
173
+ const shown = values.slice(0, o.limits.items).map(value => scalar(value, key, o).text);
174
+ return shown.join(', ') + (values.length > shown.length ? ' ' + t('… ({v0} more)', { v0: formatNumber(values.length - shown.length) }) : '');
175
+ }
176
+ // Un valor cabe en línea si es escalar, vacío o una lista corta de escalares.
177
+ function inline(value, key, o, depth, room) {
178
+ if (isScalar(value)) return scalar(value, key, o);
179
+ if (Array.isArray(value)) {
180
+ if (!value.length) return { text: t('none'), kind: 'muted' };
181
+ if (value.every(isScalar)) { const text = inlineList(value, key, o); if (!text.includes('\n') && width(text) <= room * 3) return { text, kind: 'text' }; }
182
+ if (depth >= o.limits.depth) return { text: t('{v0} items', { v0: formatNumber(value.length) }), kind: 'muted' };
183
+ return null;
184
+ }
185
+ if (plain(value) || typeof value === 'object') {
186
+ const keys = Object.keys(value);
187
+ if (!keys.length) return { text: '—', kind: 'muted' };
188
+ if (depth >= o.limits.depth) return { text: t('{v0} fields', { v0: formatNumber(keys.length) }), kind: 'muted' };
189
+ return null;
190
+ }
191
+ return { text: o.sanitize(String(value)), kind: 'text' };
192
+ }
193
+
194
+ function errorShaped(value) {
195
+ return plain(value) && (typeof value.error === 'string' && value.error || typeof value.message === 'string' && (value.code !== undefined || value.is_error === true || value.details !== undefined));
196
+ }
197
+ export function presentError(error, input = {}) {
198
+ const o = options(input), message = typeof error === 'string' ? error : error?.message || (typeof error?.error === 'string' ? error.error : '') || String(error);
199
+ const lines = wrap(o.tones.bad('✗ ' + o.sanitize(message)), o.columns);
200
+ const rest = {};
201
+ if (error && typeof error === 'object') {
202
+ if (error.code !== undefined) rest.code = error.code;
203
+ if (error.details !== undefined) rest.details = error.details;
204
+ for (const [key, value] of Object.entries(error)) if (!['message', 'error', 'code', 'details', 'stack', 'is_error'].includes(key) && value !== undefined) rest[key] = value;
205
+ }
206
+ if (Object.keys(rest).length) lines.push(...objectLines(rest, o, 0));
207
+ return lines.map(line => clip(line, o.columns));
208
+ }
209
+
210
+ function objectLines(object, o, depth) {
211
+ const entries = Object.entries(object).filter(([, value]) => value !== undefined);
212
+ if (!entries.length) return [o.tones.muted(t('(no fields)'))];
213
+ const shown = entries.slice(0, o.limits.fields), labels = shown.map(([key]) => humanizeKey(key, o.sanitize));
214
+ const labelWidth = Math.max(4, Math.min(Math.max(...labels.map(width)) + 1, 28, Math.floor(o.columns * 0.4)));
215
+ const valueColumn = labelWidth + 2, room = o.columns - valueColumn, narrow = room < 16, output = [];
216
+ shown.forEach(([key, value], index) => {
217
+ const label = width(labels[index]) + 1 > labelWidth ? clip(labels[index], labelWidth - 1, '…') + ':' : labels[index] + ':';
218
+ const item = inline(value, key, o, depth, narrow ? o.columns - 2 : room);
219
+ if (!item) {
220
+ output.push(o.tones.bold(label));
221
+ output.push(...valueLines(value, key, { ...o, columns: o.columns - 2 }, depth + 1).map(line => ' ' + line));
222
+ return;
223
+ }
224
+ if (narrow) { output.push(label, ...textBlock(item, o.columns - 2, o).map(line => ' ' + line)); return; }
225
+ const parts = textBlock(item, room, o);
226
+ output.push(pad(label, valueColumn) + parts[0], ...parts.slice(1).map(line => ' '.repeat(valueColumn) + line));
227
+ });
228
+ if (entries.length > shown.length) output.push(more(entries.length - shown.length, o));
229
+ return output;
230
+ }
231
+
232
+ function cell(value, key, o) {
233
+ let text;
234
+ if (isScalar(value)) text = scalar(value, key, { ...o, compact: true }).text;
235
+ else if (Array.isArray(value)) text = !value.length ? '—' : value.every(isScalar) ? (value.slice(0, 5).map(item => scalar(item, key, o).text).join(', ') + (value.length > 5 ? ` +${value.length - 5}` : '')) : t('{v0} items', { v0: formatNumber(value.length) });
236
+ else if (value && typeof value === 'object') {
237
+ const parts = Object.entries(value).filter(([, item]) => isScalar(item) && item !== null && item !== 'none' && item !== false).map(([name, item]) => `${o.sanitize(name)}=${scalar(item, name, o).text}`);
238
+ text = parts.length ? parts.join(' ') : '—';
239
+ } else text = o.sanitize(String(value));
240
+ return clip(text.replace(/\s*\n\s*/g, ' ⏎ '), 60, '…');
241
+ }
242
+ const PRIORITY = ['name', 'id', 'operation', 'label', 'title', 'type', 'kind', 'status', 'state'];
243
+ const rank = key => { const index = PRIORITY.indexOf(String(key).toLowerCase()); return index < 0 ? PRIORITY.length : index; };
244
+
245
+ function tableLines(rows, o, { keys: wanted } = {}) {
246
+ const order = [], frequency = new Map();
247
+ for (const row of rows) for (const key of Object.keys(row)) { if (!frequency.has(key)) order.push(key); frequency.set(key, (frequency.get(key) || 0) + 1); }
248
+ const shared = wanted || order.filter(key => frequency.get(key) >= Math.ceil(rows.length / 2));
249
+ const coverage = rows.reduce((sum, row) => { const keys = Object.keys(row); return sum + (keys.length ? keys.filter(key => shared.includes(key)).length / keys.length : 1); }, 0) / rows.length;
250
+ if (!shared.length || !wanted && coverage < 0.5) return null;
251
+ const tableable = key => rows.some(row => row[key] !== undefined && (isScalar(row[key]) || Array.isArray(row[key]) && row[key].every(isScalar) || plain(row[key])));
252
+ let candidates = shared.filter(tableable);
253
+ if (!wanted) {
254
+ const nonDigest = candidates.filter(key => !/digest|sha256|hash$/i.test(key) && !rows.every(row => typeof row[key] === 'string' && DIGEST.test(row[key])));
255
+ if (nonDigest.length) candidates = nonDigest;
256
+ candidates = [...candidates].sort((a, b) => rank(a) - rank(b) || order.indexOf(a) - order.indexOf(b));
257
+ }
258
+ if (!candidates.length) return null;
259
+ const maxColumns = Math.max(1, Math.min(6, Math.floor((o.columns - 1) / 11)));
260
+ const chosen = candidates.slice(0, maxColumns), shownRows = rows.slice(0, o.limits.rows);
261
+ if (chosen.length === 1) {
262
+ const key = chosen[0], lines = shownRows.map(row => '• ' + cell(row[key], key, o));
263
+ if (rows.length > shownRows.length) lines.push(more(rows.length - shownRows.length, o));
264
+ const others = order.filter(item => item !== key);
265
+ if (others.length) lines.push(...wrap(o.tones.muted(t('Other fields: {v0}', { v0: others.map(item => humanizeKey(item, o.sanitize)).join(', ') })), o.columns));
266
+ return lines;
267
+ }
268
+ const data = { headers: chosen.map(key => humanizeKey(key, o.sanitize)), rows: shownRows.map(row => chosen.map(key => cell(row[key], key, o))) };
269
+ const lines = new DataTableParser().render(data, o.columns);
270
+ if (rows.length > shownRows.length) lines.push(more(rows.length - shownRows.length, o));
271
+ const hidden = order.filter(key => !chosen.includes(key));
272
+ if (hidden.length) lines.push(...wrap(o.tones.muted(t('Other fields: {v0}', { v0: hidden.map(key => humanizeKey(key, o.sanitize)).join(', ') })), o.columns));
273
+ return lines;
274
+ }
275
+
276
+ function arrayLines(array, key, o, depth) {
277
+ if (!array.length) return [o.tones.muted(t('none'))];
278
+ if (array.every(isScalar)) {
279
+ const shown = array.slice(0, o.limits.items), output = [];
280
+ for (const value of shown) { const parts = textBlock(scalar(value, key, o), o.columns - 2, o); output.push('• ' + parts[0], ...parts.slice(1).map(line => ' ' + line)); }
281
+ if (array.length > shown.length) output.push(more(array.length - shown.length, o));
282
+ return output;
283
+ }
284
+ if (array.every(plain)) { const table = tableLines(array, o); if (table) return table; }
285
+ const shown = array.slice(0, o.limits.rows), output = [];
286
+ shown.forEach((value, index) => {
287
+ const marker = `${index + 1}.`, indent = ' '.repeat(width(marker) + 1);
288
+ if (isScalar(value)) { const parts = textBlock(scalar(value, key, o), o.columns - indent.length, o); output.push(o.tones.muted(marker) + ' ' + parts[0], ...parts.slice(1).map(line => indent + line)); return; }
289
+ const lines = valueLines(value, key, { ...o, columns: o.columns - indent.length }, depth + 1);
290
+ output.push(o.tones.muted(marker) + ' ' + (lines[0] ?? ''), ...lines.slice(1).map(line => indent + line));
291
+ });
292
+ if (array.length > shown.length) output.push(more(array.length - shown.length, o));
293
+ return output;
294
+ }
295
+
296
+ function valueLines(value, key, o, depth = 0) {
297
+ if (isScalar(value)) return textBlock(scalar(value, key, o), o.columns, o);
298
+ if (Array.isArray(value)) return depth > o.limits.depth ? [o.tones.muted(t('{v0} items', { v0: formatNumber(value.length) }))] : arrayLines(value, key, o, depth);
299
+ if (depth > o.limits.depth) return [o.tones.muted(t('{v0} fields', { v0: formatNumber(Object.keys(value).length) }))];
300
+ if (depth === 0 && errorShaped(value)) return presentError(value, { ...o, color: o.color });
301
+ return objectLines(value, o, depth);
302
+ }
303
+
304
+ const finish = (lines, o) => lines.map(line => clip(o.color ? line : stripANSI(line), o.columns));
305
+
306
+ /** Valor estructurado → líneas de terminal, todas de ancho ≤ columns. */
307
+ export function presentValue(value, input = {}) {
308
+ const o = options(input);
309
+ return finish(valueLines(value, '', o, 0), o);
310
+ }
311
+
312
+ export function parseStructured(text) {
313
+ if (typeof text !== 'string') return text !== null && typeof text === 'object' ? { ok: true, value: text } : { ok: false };
314
+ const trimmed = text.trim();
315
+ if (trimmed.length > MAX_PARSE || !/^[[{]/.test(trimmed)) return { ok: false };
316
+ try { const value = JSON.parse(trimmed); return value !== null && typeof value === 'object' ? { ok: true, value } : { ok: false }; } catch { return { ok: false }; }
317
+ }
318
+
319
+ // Texto que ya viene en «Etiqueta: valor» (p. ej. /status) se alinea; lo que
320
+ // ya está alineado a mano (/config) o es prosa se deja tal cual.
321
+ const LABEL_LINE = /^([^\s:][^:]{0,31}): (\S.*)$/;
322
+ function textLines(text, o) {
323
+ const clean = o.sanitize(text), source = clean.split('\n');
324
+ const nonEmpty = source.filter(line => line.trim()), matched = source.filter(line => LABEL_LINE.test(line));
325
+ const align = matched.length >= 2 && matched.length >= nonEmpty.length * 0.6;
326
+ const labelWidth = align ? Math.min(28, Math.floor(o.columns * 0.4), Math.max(...matched.map(line => width(LABEL_LINE.exec(line)[1]) + 1))) : 0;
327
+ const output = [];
328
+ for (const line of source) {
329
+ const match = align && LABEL_LINE.exec(line);
330
+ if (match && width(match[1]) + 1 <= labelWidth && o.columns - labelWidth - 2 >= 16) {
331
+ const parts = wrap(match[2], o.columns - labelWidth - 2);
332
+ output.push(pad(match[1] + ':', labelWidth + 2) + parts[0], ...parts.slice(1).map(part => ' '.repeat(labelWidth + 2) + part));
333
+ } else output.push(...wrap(line, o.columns));
334
+ }
335
+ if (output.length > o.limits.textLines) { const hidden = output.length - o.limits.textLines; output.length = o.limits.textLines; output.push(o.tones.muted(t('… ({v0} more lines)', { v0: formatNumber(hidden) }))); }
336
+ return output;
337
+ }
338
+
339
+ /** Texto de salida (JSON o no) → { lines, structured }. */
340
+ export function presentOutput(text, input = {}) {
341
+ const o = options(input), parsed = parseStructured(text);
342
+ if (parsed.ok) return { lines: finish(valueLines(parsed.value, '', o, 0), o), structured: true, value: parsed.value };
343
+ return { lines: finish(textLines(String(text ?? ''), o), o), structured: false };
344
+ }
345
+
346
+ // ── Presentadores específicos de comandos ──────────────────────────────────
347
+ function section(title, o) { return o.tones.bold(o.sanitize(title)); }
348
+
349
+ function presentCapabilities(value, o) {
350
+ if (!plain(value) || !('model' in value)) return null;
351
+ const { model, contextBudget, capabilities, provenance = {}, effort, source, ...rest } = value, summary = { model };
352
+ if (plain(contextBudget)) {
353
+ const window = contextBudget.contextWindow, parts = [Number.isFinite(window) ? t('{v0} tokens', { v0: formatNumber(window) }) : '—'];
354
+ if (contextBudget.source || contextBudget.kind) parts.push(o.sanitize(contextBudget.source || contextBudget.kind));
355
+ if (contextBudget.announcedWindow != null) parts.push(t('announced {v0}', { v0: formatNumber(contextBudget.announcedWindow) }));
356
+ if (contextBudget.configuredLimit != null) parts.push(t('limit {v0}', { v0: formatNumber(contextBudget.configuredLimit) }));
357
+ summary.context_window = parts.join(' · ');
358
+ }
359
+ if (plain(capabilities)) summary.capabilities = Object.entries(capabilities).map(([name, supported]) => `${supported === true ? '✓' : supported === false ? '✗' : '?'} ${humanizeKey(name, o.sanitize).toLowerCase()}${provenance?.[name]?.kind ? ` (${o.sanitize(provenance[name].kind)})` : ''}`).join(' · ');
360
+ if (plain(effort)) summary.effort = [o.sanitize(effort.selected || 'auto'), Array.isArray(effort.levels) ? t('levels {v0}', { v0: effort.levels.map(item => o.sanitize(item)).join(', ') }) : null, effort.source ? t('source {v0}', { v0: o.sanitize(effort.source) }) : null].filter(Boolean).join(' · ');
361
+ if (Array.isArray(source)) summary.sources = source;
362
+ if (!plain(contextBudget) && contextBudget !== undefined) rest.context_budget = contextBudget;
363
+ return objectLines({ ...summary, ...rest }, o, 0);
364
+ }
365
+
366
+ function presentPlugins(value, o) {
367
+ if (!plain(value) || !Array.isArray(value.plugins)) return null;
368
+ const { plugins, ...rest } = value, output = Object.keys(rest).length ? [...objectLines(rest, o, 1), ''] : [];
369
+ output.push(section(t('Plugins ({v0})', { v0: plugins.length }), o));
370
+ if (!plugins.length) { output.push(o.tones.muted(t('none'))); return output; }
371
+ const rows = plugins.map(item => plain(item) ? {
372
+ plugin: item.id, version: item.version, state: item.state, operations: Array.isArray(item.operations) ? item.operations.length : item.operations,
373
+ loading: item.tool_loading, calls: item.active_calls, origin: item.builtin ? t('built-in') : t('local'),
374
+ trust: item.builtin ? undefined : item.trust?.status ?? item.trust,
375
+ } : { plugin: item });
376
+ output.push(...(tableLines(rows, o, { keys: ['plugin', 'state', 'version', 'operations', 'origin', 'trust', 'loading', 'calls'] }) || arrayLines(plugins, 'plugins', o, 1)));
377
+ for (const item of plugins) if (plain(item) && (item.error || item.warning)) output.push(...wrap(o.tones[item.error ? 'bad' : 'warning'](`${item.error ? '✗' : '!'} ${o.sanitize(item.id)}: ${o.sanitize(item.error || item.warning)}`), o.columns));
378
+ return output;
379
+ }
380
+
381
+ function presentHouse(value, o) {
382
+ if (!plain(value) || !Array.isArray(value.receipts)) return null;
383
+ const { counts, receipts, scope, ...rest } = value, summary = {};
384
+ for (const key of ['status', 'asked']) if (rest[key] !== undefined) { summary[key] = rest[key]; delete rest[key]; }
385
+ if (plain(counts)) summary.executions = t('{v0} returned · {v1} failed · {v2} refused', { v0: formatNumber(counts.returned || 0), v1: formatNumber(counts.failed || 0), v2: formatNumber(counts.refused || 0) });
386
+ const output = objectLines({ ...summary, ...rest }, o, 1);
387
+ output.push('', section(t('Receipts ({v0})', { v0: receipts.length }), o));
388
+ if (!receipts.length) output.push(o.tones.muted(t('none')));
389
+ else {
390
+ const rows = receipts.map(item => plain(item) ? { operation: item.operation, status: item.status, authority: item.authority, actor: item.actor?.kind || item.actor?.id, timestamp: item.timestamp } : { operation: item });
391
+ output.push(...(tableLines(rows, o, { keys: ['operation', 'status', 'authority', 'actor', 'timestamp'] }) || []));
392
+ }
393
+ output.push(o.tones.muted(t('Digests and complete receipts: /raw')));
394
+ return output;
395
+ }
396
+
397
+ function presentOperations(value, o) {
398
+ if (!Array.isArray(value) || !value.every(item => plain(item) && typeof item.name === 'string')) return null;
399
+ const rows = value.map(item => ({ name: item.name, effects: item.effects, audience: item.audience, origin: item.origin?.plugin || item.origin?.server || item.origin?.kind || t('core') }));
400
+ const wide = { ...o, limits: { ...o.limits, rows: Math.max(o.limits.rows, 100) } };
401
+ return [section(t('{v0} operations', { v0: value.length }), o), ...(tableLines(rows, wide, { keys: ['name', 'effects', 'audience', 'origin'] }) || []), o.tones.muted(t('F6 → Tab shows each operation with its description and arguments.'))];
402
+ }
403
+
404
+ const COMMAND_PRESENTERS = Object.freeze({
405
+ capabilities: presentCapabilities,
406
+ plugins: (value, o, arg) => /^(?:status|list)?$/.test(arg.trim()) ? presentPlugins(value, o) : null,
407
+ house: presentHouse,
408
+ operations: presentOperations,
409
+ });
410
+
411
+ /** Salida de un comando de barra → { lines, structured }. Nunca lanza. */
412
+ export function presentCommand(command, arg, output, input = {}) {
413
+ const o = options(input), parsed = parseStructured(output);
414
+ if (parsed.ok) {
415
+ const specific = COMMAND_PRESENTERS[command];
416
+ if (specific) { try { const lines = specific(parsed.value, o, String(arg || '')); if (lines) return { lines: finish(lines, o), structured: true }; } catch { /* cae al genérico */ } }
417
+ }
418
+ return presentOutput(output, input);
419
+ }
420
+ /** Para -p con salida texto en una terminal: texto sin ANSI. */
421
+ export function presentCommandText(command, arg, output, input = {}) {
422
+ return presentCommand(command, arg, output, { ...input, color: false }).lines.join('\n');
423
+ }
424
+
425
+ // Vista cruda: el texto original, sin perder nada; si es JSON, con sangría.
426
+ export function rawLines(text, columns, sanitize = safeText) {
427
+ const parsed = parseStructured(text);
428
+ const source = parsed.ok && typeof text === 'string' ? JSON.stringify(parsed.value, null, 2) : typeof text === 'string' ? text : JSON.stringify(text, null, 2) ?? String(text);
429
+ return wrap(sanitize(source), Math.max(1, columns));
430
+ }
431
+
432
+ /** Bloque de resultado de comando en la TUI: encabezado, cuerpo y pista /raw. */
433
+ export function presentResultBlock(block, { columns = 80, raw = false, expanded = false } = {}) {
434
+ const o = options({ columns, expanded }), title = `▸ /${safeText(block.command)}${block.arg ? ' ' + safeText(block.arg) : ''}`;
435
+ if (raw) return [o.tones.accent(clip(title, columns - 14)) + o.tones.muted(' · ' + t('exact JSON')), ...rawLines(block.text, columns - 2).map(line => ' ' + line)];
436
+ const presented = presentCommand(block.command, block.arg || '', block.text, { columns: columns - 2, expanded });
437
+ const lines = [o.tones.accent(clip(title, columns)), ...presented.lines.map(line => ' ' + line)];
438
+ if (presented.structured) lines.push(' ' + o.tones.muted(clip(t('/raw shows the exact JSON') + (expanded ? '' : ' · ' + t('Ctrl+O expands lists and long texts.')), columns - 2)));
439
+ return lines;
440
+ }
441
+
442
+ function toolOutputText(output) {
443
+ if (typeof output === 'string') return output;
444
+ if (Array.isArray(output)) return output.map(part => part?.type === 'text' ? String(part.text ?? '') : `[${safeText(part?.type || '?')}]`).join('\n');
445
+ return JSON.stringify(output) ?? String(output);
446
+ }
447
+ /** Detalle de un bloque de herramienta (Ctrl+O o error). */
448
+ export function presentToolBlock(block, { columns = 80, raw = false, expanded = true } = {}) {
449
+ const hasOutput = block.output !== undefined && block.output !== null && block.output !== '';
450
+ const o = options({ columns, expanded });
451
+ if (raw || !hasOutput && !plain(block.input)) {
452
+ const text = hasOutput ? toolOutputText(block.output) : JSON.stringify(block.input) ?? '';
453
+ return wrap(safeText(text).slice(0, 6000), columns).map(line => o.tones.muted(line));
454
+ }
455
+ if (!hasOutput) return presentValue(block.input, { columns, expanded });
456
+ const text = toolOutputText(block.output), parsed = parseStructured(text);
457
+ if (parsed.ok) return presentValue(parsed.value, { columns, expanded });
458
+ const clean = safeText(text), cut = clean.length > 6000;
459
+ const lines = wrap(cut ? clean.slice(0, 6000) : clean, columns).map(line => o.tones.muted(line));
460
+ if (cut) lines.push(o.tones.muted(t('… ({v0} more characters)', { v0: formatNumber(clean.length - 6000) })));
461
+ return lines;
462
+ }
463
+
464
+ // ── Tarjeta de permiso ─────────────────────────────────────────────────────
465
+ const PREVIEW_LINES = 12;
466
+ const lineCount = text => text ? String(text).split('\n').length : 0;
467
+ function preview(text, prefix, o, toneName) {
468
+ const lines = String(text ?? '').split('\n'), shown = lines.slice(0, PREVIEW_LINES), output = [];
469
+ for (const line of shown) output.push(...wrap(prefix + visibleText(line), o.columns).map(row => toneName ? o.tones[toneName](row) : row));
470
+ if (lines.length > shown.length) output.push(o.tones.muted(t('… ({v0} more lines)', { v0: formatNumber(lines.length - shown.length) })));
471
+ return output;
472
+ }
473
+ const baseName = name => String(name || '').split('__').at(-1);
474
+
475
+ /**
476
+ * Argumentos de una confirmación en legible. `summary` es opcional y debe venir
477
+ * del host o de la definición de la operación (p. ej. un futuro
478
+ * `confirm_summary(input)` de plugin), nunca de la entrada del modelo; aun con
479
+ * resumen, los argumentos reales se muestran debajo.
480
+ */
481
+ export function presentPermission(name, input, { summary, columns = 80, raw = false } = {}) {
482
+ const o = options({ columns, expanded: true, sanitize: visibleText });
483
+ if (raw) return wrap(visibleText(JSON.stringify(input, null, 2) ?? String(input)), o.columns);
484
+ const output = [];
485
+ if (typeof summary === 'string' && summary.trim()) output.push(o.tones.accent(t('Summary declared by the operation (the arguments below are what runs):')), ...wrap(visibleText(summary.trim()), o.columns - 2).map(line => ' ' + line), '');
486
+ if (containsInvisible(input) || typeof summary === 'string' && INVISIBLE_TEST.test(summary)) output.push(o.tones.warning('! ' + t('Contains control characters, shown as \\u{…}.')));
487
+ const tool = baseName(name);
488
+ if (plain(input) && tool === 'Write' && typeof input.content === 'string') {
489
+ const { file_path, content, ...rest } = input;
490
+ output.push(...objectLines({ ...(file_path !== undefined ? { file_path } : {}), content: t('{v0} lines · {v1}', { v0: formatNumber(lineCount(content)), v1: formatBytes(Buffer.byteLength(content)) }), ...rest }, o, 1));
491
+ output.push('', ...preview(content, '│ ', o));
492
+ } else if (plain(input) && tool === 'Edit' && typeof input.old_string === 'string' && typeof input.new_string === 'string') {
493
+ const { file_path, old_string, new_string, replace_all, ...rest } = input;
494
+ output.push(...objectLines({ ...(file_path !== undefined ? { file_path } : {}), change: t('Replace {v0} lines with {v1} lines', { v0: formatNumber(lineCount(old_string)), v1: formatNumber(lineCount(new_string)) }) + (replace_all ? ' · ' + t('all occurrences') : ''), ...rest }, o, 1));
495
+ output.push('', o.tones.muted(t('Removed')), ...preview(old_string, '- ', o, 'bad'), o.tones.muted(t('Added')), ...preview(new_string, '+ ', o, 'good'));
496
+ } else if (plain(input) && tool === 'Bash' && typeof input.command === 'string') {
497
+ // El comando completo, siempre: es exactamente lo que se aprueba.
498
+ const { command, ...rest } = input;
499
+ output.push(o.tones.bold(humanizeKey('command') + ':'), ...command.split('\n').flatMap((line, index) => wrap((index ? ' ' : '$ ') + visibleText(line), o.columns)));
500
+ if (Object.keys(rest).length) output.push(...objectLines(rest, o, 1));
501
+ } else if (plain(input)) output.push(...(Object.keys(input).length ? objectLines(input, o, 0) : [o.tones.muted(t('(no fields)'))]));
502
+ else output.push(...valueLines(input, '', o, 0));
503
+ return output.map(line => clip(line, o.columns));
504
+ }