@prodero/curator 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/CHANGELOG.md +29 -3
  3. package/README.md +64 -10
  4. package/dist/chat-text.js +139 -0
  5. package/dist/cli.js +9 -2
  6. package/dist/contracts.js +2 -0
  7. package/dist/conversation-presentation.js +101 -142
  8. package/dist/core/agent-instructions.js +7 -4
  9. package/dist/core/context.js +4 -2
  10. package/dist/core/curation-graphical.js +1 -1
  11. package/dist/core/curation-review.js +61 -17
  12. package/dist/core/curation-revision.js +2 -0
  13. package/dist/core/curation-semantic-jobs.js +2 -2
  14. package/dist/core/json-schema.js +6 -0
  15. package/dist/core/memory-index.js +1 -1
  16. package/dist/core/memory-pressure.js +16 -5
  17. package/dist/core/memory-sync.js +16 -9
  18. package/dist/core/memory-transformation-proposal.js +2 -2
  19. package/dist/core/project.js +18 -4
  20. package/dist/core/session-index.js +2 -2
  21. package/dist/core/session-reconciliation.js +320 -0
  22. package/dist/curation-review-adapter.js +1 -1
  23. package/dist/hosts/install-command.js +130 -0
  24. package/dist/hosts/manager.js +1 -1
  25. package/dist/memory-presentation.js +5 -175
  26. package/dist/presentation-server.js +12 -6
  27. package/dist/presentation-trace.js +3 -2
  28. package/dist/presentation.js +57 -136
  29. package/dist/providers/mcp-sampling.js +6 -6
  30. package/dist/server.js +63 -29
  31. package/dist/status-presentation.js +4 -2
  32. package/dist/text-layout.js +16 -24
  33. package/dist-ui/index.html +140 -128
  34. package/docs/COMPATIBILITY.md +28 -16
  35. package/docs/INSTALL-WITH-AGENT.md +64 -0
  36. package/docs/RELEASING.md +6 -6
  37. package/docs/V0.3.0-CHAT-TEXT.md +75 -0
  38. package/docs/V0.3.0-MAINTENANCE.md +23 -0
  39. package/docs/hosts.md +21 -7
  40. package/docs/images/prodero-overview-cropped.png +0 -0
  41. package/instructions/AGENTS.md +6 -4
  42. package/package.json +8 -5
  43. package/plugins/migrate_ai_studio/schema.cjs +1 -2
  44. package/plugins/migrate_ai_studio/ui/dist/index.html +75 -42
  45. package/setup.md +151 -0
@@ -1,182 +1,12 @@
1
- /** Pure product copy shared by Open, Curate Memory and text clients. Core owns decisions. */
2
- import { memorySyncOperation } from './memory-sync-operation.js';
3
- const count = (n, one, many = one + 's') => `${n} ${n === 1 ? one : many}`;
4
- const tokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(2)}K` : String(n);
5
1
  export const MEMORY_COPY = {
6
2
  automatic: 'Prodero remembers automatically. Sessions save useful knowledge into Local Knowledge when they close.',
7
- purpose: 'Curate Memory keeps that memory useful and shareable.',
8
- noHistory: 'Not enough usage history yet to evaluate memory optimization.',
9
- stale: 'Project memory changed since this curation started. Re-analyze before continuing.',
10
3
  incompleteProvenance: 'Some provenance fields are incomplete; this is not proof of lost history. Existing records are preserved. Verify actual sources before proposing repairs; diagnostics do not infer or backfill provenance.',
11
- shared: 'Versioned Knowledge is Git-shareable project state that your agent can change. Curator does not commit or push; review the repository diff before doing so.',
12
4
  };
13
- /** Display only: never infer a topic from a knowledge title. Full labels remain in text/details. */
14
- export function memoryAreaLabel(j) {
15
- const refs = [...new Set((j.records ?? []).map((r) => r.scopeRef).filter(Boolean))];
16
- const path = refs.length === 1 ? String(refs[0]) : String(j.label ?? '');
17
- const reliablePath = /^(?:[\w.@-]+\/)+[\w.@-]+\/?$/.test(path);
18
- if (reliablePath)
19
- return path.length <= 48 ? path : `${path.slice(0, 20)}…${path.slice(-27)}`;
20
- return `Area ${String(j.id ?? j.candidateId ?? '').replace(/^area_/, '').slice(0, 8) || 'overview'}`;
21
- }
22
- export function memoryArea(j) {
23
- const signals = j.signals ?? [], c = j.counts ?? {};
24
- const reasons = [];
25
- if (signals.includes('context_pressure'))
26
- reasons.push(c.budgetOmissionSessions ? `Some relevant knowledge did not fit into ${count(c.budgetOmissionSessions, 'recent session')}.` : 'Some relevant knowledge repeatedly did not fit into recent task context.');
27
- if (signals.includes('fragmentation'))
28
- reasons.push('Related memories are spread across several records used together.');
29
- if (signals.includes('recent_evolution'))
30
- reasons.push('Knowledge has evolved through related updates.');
31
- if (signals.includes('redundancy'))
32
- reasons.push('Repeated information may be easier to use together.');
33
- if (signals.includes('unresolved'))
34
- reasons.push('Some related information still needs clarification.');
35
- const conflict = signals.includes('potential_conflict');
36
- if (conflict)
37
- reasons.push('Prodero found incompatible current knowledge and could not determine which state is correct.');
38
- const statuses = { unselected: '', pending: 'Waiting', semantic_work: 'Improving…', awaiting_review: 'Ready for your review', applied: 'Processed', skipped: 'Skipped', failed: 'Could not finish this area' };
39
- return { id: j.id, title: j.label, displayLabel: memoryAreaLabel(j), body: `${count(j.recordCount, 'related memory', 'related memories')} · ${j.layers?.local ?? 0} Local / ${j.layers?.versioned ?? 0} Shared`, reasons, conflict, status: statuses[j.status] ?? '', error: j.error ? 'This area could not be completed. Retry it or continue with other areas.' : null };
40
- }
41
- export function memoryOpenPresentation(p) {
42
- const o = p.memoryOverview, areas = (o?.areas ?? []).map(memoryArea), conflicts = areas.filter((a) => a.conflict).length;
43
- const hasHistory = o?.available && o.sessions > 0;
44
- const headline = !o?.available ? 'Memory overview unavailable' : areas.length ? `${count(areas.length, 'area')} can be improved` : hasHistory ? 'No improvements suggested by recent usage' : 'Memory is ready to learn';
45
- const notes = [MEMORY_COPY.automatic, MEMORY_COPY.purpose];
46
- if (!o?.available)
47
- notes.push('Memory analysis is temporarily unavailable. Try Curate Memory again when current work has finished.');
48
- else if (!hasHistory)
49
- notes.push(MEMORY_COPY.noHistory);
50
- else if (!areas.length)
51
- notes.push('No transformation is suggested by the available recent usage.');
52
- if (p.knowledgeCount === 0)
5
+ export function memoryOpenPresentation(project) {
6
+ const headline = project.knowledgeCount > 0 ? 'Memory saves automatically.' : 'Memory is ready to learn';
7
+ const notes = [MEMORY_COPY.automatic];
8
+ if (project.knowledgeCount === 0)
53
9
  notes.push('No current memories yet. Normal work will build Local Knowledge.');
54
- if (p.promotionRecommendedCount)
55
- notes.push(`${count(p.promotionRecommendedCount, 'Local memory', 'Local memories')} can be reviewed for team sharing.`);
56
- if (areas.some((a) => a.reasons.some((r) => r.includes('did not fit'))))
57
- notes.push('Some relevant knowledge has not fit into recent task context.');
58
- if (o?.areasOmitted)
59
- notes.push(`${count(o.areasOmitted, 'additional area')} can be inspected in a later Curate Memory run.`);
60
- const workflow = o?.workflow;
61
- if (workflow)
62
- notes.push(workflow.state === 'stale' ? MEMORY_COPY.stale : 'Curate Memory is in progress. Continue when you are ready.');
63
- const signal = areas.some((a) => a.reasons.some((r) => r.includes('did not fit'))) ? 'Relevant knowledge has not fit into recent task context.'
64
- : areas.length ? areas[0].reasons.find((r) => !r.includes('incompatible')) ?? 'Related knowledge is ready for review.'
65
- : !o?.available ? 'Memory analysis is temporarily unavailable.' : !hasHistory ? 'Not enough usage history yet to evaluate improvements.' : 'No improvements suggested by recent usage.';
66
- const total = areas.length + (o?.areasOmitted ?? 0);
67
- const dashboard = { headline: areas.length ? 'Memory can be improved' : headline, areasLabel: total ? `${count(total, 'area')} identified` : null, signal,
68
- sharing: typeof p.localKnowledgeCount === 'number' ? p.localKnowledgeCount ? `${count(p.localKnowledgeCount, 'Local memory', 'Local memories')} available for sharing review.` : 'No Local memories to share.' : null,
69
- established: p.knowledgeCount > 0, workflowNotice: workflow ? workflow.state === 'stale' ? MEMORY_COPY.stale : 'Curate Memory is in progress.' : null };
70
- return { headline, notes, areas, conflicts, workflow, dashboard, button: workflow ? (workflow.state === 'stale' ? 'Re-analyze memory' : 'Continue curation') : 'Curate Memory' };
71
- }
72
- export function memorySyncPresentation(w) {
73
- const areas = (w.jobs ?? []).map(memoryArea), s = w.summary ?? {}, p = w.review?.preview;
74
- const operation = w.operation ?? memorySyncOperation(w);
75
- const headlines = { awaiting_selection: 'Curate Memory', semantic_work: 'Improving memory…', awaiting_clarification: 'A question about your project', awaiting_review: w.review?.kind === 'promotion' ? 'Review knowledge for team sharing' : p?.requiresVersionedReview ? 'Review shared-memory changes' : 'Review memory changes', applying: 'Approved changes pending verification', awaiting_promotion_selection: 'Review Local knowledge for sharing', completed: 'Memory curated', stale: 'Re-analyze memory' };
76
- const notes = [], warnings = [], metrics = [], sections = [];
77
- const section = (title, body) => sections.push({ title, items: [{ title: '', body, notes: [] }] });
78
- if (w.state === 'awaiting_selection') {
79
- // Operation choices are the entry surface; planner data is diagnostic.
80
- }
81
- if (['semantic_work', 'awaiting_clarification'].includes(w.state) && operation.phase !== 'question')
82
- notes.push(operation.phase === 'submitted' ? 'The result was submitted and is awaiting validation.' : operation.phase === 'awaiting_agent' ? 'Prepared evidence is waiting for host-agent analysis. No background worker is confirmed.' : 'Analysis is queued. Continue with the prepared evidence; you may be asked about missing project facts.');
83
- if (w.state === 'applying')
84
- notes.push('The approved intention is saved. Check progress to verify the result or resume recovery; no active writer is assumed.');
85
- if (w.state === 'stale')
86
- notes.push(MEMORY_COPY.stale);
87
- if (w.state === 'awaiting_promotion_selection')
88
- notes.push('Select Local knowledge to move to Versioned Knowledge.');
89
- if (w.state !== 'stale' && w.review?.kind === 'transformation') {
90
- const r = p.summary, before = r.consolidated.length + r.superseded.length + r.kept.length + r.unresolved.length, after = r.created.length + r.kept.length + r.unresolved.length;
91
- metrics.push({ label: 'Before · current memories', value: before }, { label: 'After · current memories', value: after }, { label: 'Estimated tokens · before → after', value: `${tokens(r.estimatedTokens.before)} → ${tokens(r.estimatedTokens.after)}` });
92
- section(r.label, `${count(r.consolidated.length, 'memory', 'memories')} consolidated; ${count(r.superseded.length, 'memory', 'memories')} replaced by newer project state; ${count(r.kept.length + r.unresolved.length, 'previous memory', 'previous memories')} remain current.\n${count(r.archives.length, 'previous memory', 'previous memories')} will be preserved in history.`);
93
- for (const category of ['instruction', 'principle', 'invariant', 'constraint', 'decision']) {
94
- const n = r.protectedSources.filter((x) => x.category === category && x.action === 'keep').length;
95
- if (n)
96
- notes.push(`${count(n, `project ${category}`)} ${n === 1 ? 'was' : 'were'} preserved unchanged.`);
97
- }
98
- if (r.unresolved.length)
99
- notes.push(`Prodero could not safely determine how ${count(r.unresolved.length, 'memory', 'memories')} should be handled. It will remain current.`);
100
- const l = p.limitations;
101
- notes.push(...(l.warnings ?? []), ...(l.additionalEvidenceRequired ?? []));
102
- if (l.omittedAsRedundant?.length)
103
- section('Information proposed as repetition', l.omittedAsRedundant.flatMap((x) => x.information).join('\n'));
104
- if (l.unresolved?.length)
105
- section('Information still unresolved', l.unresolved.flatMap((x) => x.information).join('\n'));
106
- if (p.requiresVersionedReview) {
107
- notes.push(MEMORY_COPY.shared, 'Local and shared changes will be saved together after approval.');
108
- const v = p.versionedEffects;
109
- const sharedIds = new Set(r.created.filter((x) => x.layer === 'versioned').map((x) => x.id));
110
- const localIds = new Set(r.archives.filter((x) => x.layer === 'local').map((x) => x.id));
111
- const sharedLocal = new Set(r.provenance.filter((x) => sharedIds.has(x.from) && localIds.has(x.to)).map((x) => x.to));
112
- if (sharedLocal.size)
113
- notes.push(`${count(sharedLocal.size, 'Local memory', 'Local memories')} will be represented in the new shared knowledge; the originals stay in history.`);
114
- section('Shared-memory changes', `${count(v.filter((x) => x.action === 'create').length, 'new current memory', 'new current memories')}; ${count(v.filter((x) => x.action === 'consolidate').length, 'memory', 'memories')} consolidated; ${count(v.filter((x) => x.action === 'supersede').length, 'memory', 'memories')} replaced by newer project state.\n${count(r.archives.filter((x) => x.layer === 'versioned').length, 'previous shared memory', 'previous shared memories')} preserved in Versioned archive files.`);
115
- }
116
- notes.push('Please review the proposed wording. Prodero checks data integrity but cannot guarantee that every meaning was preserved.');
117
- sections.push({ title: 'Proposed current knowledge', items: p.proposal.records.map((x) => ({ title: x.title, body: x.body, notes: [`${x.category} · ${x.scope}${x.path ? ' · ' + x.path : ''} · ${r.created.find((c) => c.key === x.key)?.layer === 'versioned' ? 'Shared / Versioned' : 'Local'}`] })) });
118
- }
119
- if (w.state !== 'stale' && w.review?.kind === 'link-repair') {
120
- notes.push(`Remove ${count(p.count, 'broken link')} to missing memories. No replacement memory is inferred.`, 'Memory text and valid links stay unchanged. Original records, including removed links, are saved in a checkpoint.');
121
- if (p.requiresVersionedReview)
122
- notes.push(MEMORY_COPY.shared);
123
- if (p.remaining)
124
- notes.push(`${count(p.remaining, 'additional broken link')} will remain for a later curation run.`);
125
- sections.push({ title: 'Broken links to remove', items: p.changes.map((c) => ({ title: c.title, body: c.links.map((l) => `${l.type} → ${l.target}`).join('\n'), notes: [`${c.layer === 'versioned' ? 'Shared / Versioned' : 'Local'} · ${c.id}`] })) });
126
- }
127
- if (w.state !== 'stale' && w.review?.kind === 'promotion') {
128
- notes.push(MEMORY_COPY.shared, 'This operation applies the knowledge shown here to Versioned files; it does not share it through Git.');
129
- const labels = { new: 'New shared memory', merge: 'Update existing shared memory', supersede: 'Replace older shared state', covered: 'Already represented in shared knowledge', conflict: 'Needs review · remains Local' };
130
- sections.push({ title: 'Sharing review', items: p.actions.flatMap((a) => [
131
- ...(a.before ? [{ title: `Current · ${a.before.title}`, body: a.before.content, notes: [] }] : []),
132
- { title: a.draft?.title ?? a.before?.title ?? 'Local knowledge', body: a.draft?.content ?? a.reason, notes: [labels[a.disposition], a.reason] },
133
- ]) });
134
- if (p.actions.some((a) => a.disposition === 'conflict'))
135
- notes.push('Some selected knowledge cannot be shared safely and will remain Local. Review the reasons below.');
136
- }
137
- if (w.state === 'completed') {
138
- if (s.areasProcessed && s.archived)
139
- notes.push(`${s.sourcesBefore} previous memories → ${s.currentAfter} current memories.`, `${count(s.archived, 'previous memory', 'previous memories')} preserved in history.`);
140
- else if (s.areasProcessed)
141
- notes.push('Memory reviewed. No previous memories needed to be replaced.');
142
- else if (w.jobs?.some((j) => j.selected) || s.areasSkipped || s.areasFailed)
143
- notes.push('No memory improvements were applied.');
144
- else if (w.improveEnabled === false)
145
- notes.push('Memory improvement was not selected.');
146
- else if (w.jobs?.length)
147
- notes.push('No memory improvements were selected.');
148
- else
149
- notes.push(w.usage?.available && w.usage?.sessions ? 'No transformation was needed based on recent usage.' : MEMORY_COPY.noHistory);
150
- if (s.brokenLinksFixed)
151
- notes.push(`${count(s.brokenLinksFixed, 'broken link')} fixed.`);
152
- if (s.brokenLinksRemaining)
153
- notes.push(`${count(s.brokenLinksRemaining, 'broken link')} remain unchanged.`);
154
- if (s.promoted)
155
- notes.push(`${count(s.promoted, 'Local memory', 'Local memories')} prepared in Versioned files. Review the Git diff before sharing; Curator did not commit or push.`);
156
- if (s.areasProcessed)
157
- notes.push(`Estimated context for reviewed knowledge: ${tokens(s.estimatedTokens.before)} → ${tokens(s.estimatedTokens.after)} tokens.`);
158
- if (s.unresolved)
159
- notes.push(`${count(s.unresolved, 'unresolved item')} still need clarification and remain current.`);
160
- if (s.areasSkipped)
161
- notes.push('Some knowledge was retained because a safe improvement could not be established.');
162
- if (s.areasFailed)
163
- notes.push('Some analysis could not be completed. Saved knowledge is unchanged for those items.');
164
- if (s.questionsDeferred)
165
- notes.push('Deferred questions are saved for later. Independent work continued.');
166
- if (s.promotionConflicts)
167
- notes.push(`${count(s.promotionConflicts, 'sharing item')} still need review and remain Local.`);
168
- notes.push('Previous knowledge remains available through history.');
169
- }
170
- notes.push('Your place and saved answers are retained. Leave and return with Continue curation.');
171
- if (['semantic_work', 'awaiting_clarification', 'awaiting_review'].includes(w.state))
172
- notes.push('Skip or Answer later retains original knowledge and saved answers, then continues other selected work.');
173
- if (w.state === 'stale' || w.state === 'completed')
174
- notes.push('Re-analyze starts a fresh workflow from current project data. Changes already saved remain in place.');
175
- if (w.error && w.state !== 'stale')
176
- notes.push('This step could not finish. Continue to check saved progress before retrying.');
177
- if (w.state === 'awaiting_promotion_selection' && w.coverage?.promotionCandidatesOmitted)
178
- notes.push(`${count(w.coverage.promotionCandidatesOmitted, 'additional Local memory', 'additional Local memories')} can be reviewed in a later Curate Memory run.`);
179
- const headline = w.state === 'completed' && operation.outcome === 'remaining' ? 'Completed with remaining work' : operation.phase === 'awaiting_agent' ? 'Awaiting agent analysis' : operation.phase === 'submitted' ? 'Submitted · validation pending' : w.state === 'semantic_work' ? 'Analysis queued' : headlines[w.state] ?? 'Curate Memory';
180
- return { headline, graphicalHeadline: headline, operation, reviewAreaLabel: 'Memory changes', notes, warnings, metrics, sections, areas };
10
+ return { headline, notes };
181
11
  }
182
12
  //# sourceMappingURL=memory-presentation.js.map
@@ -7,8 +7,14 @@ import { conversationOperation } from './conversation-presentation.js';
7
7
  import { PRODERO_TOOLS } from './contracts.js';
8
8
  export const PRESENTED_TOOLS = ["prodero_initialize", "prodero_context", "prodero_curation", "prodero_curation_question", "prodero_migrate", "prodero_open", "prodero_status", "prodero_status_details", "prodero_prepare", "prodero_session_close", "prodero_knowledge", "prodero_knowledge_mutate", "prodero_instruction_review", "prodero_instruction_optimize", "prodero_session_review", "prodero_session_review_mutate", "prodero_promotion_preview", "prodero_promote", "prodero_sync", "prodero_memory_sync", "prodero_skill_preview", "prodero_skill_review"];
9
9
  const modeSchema = z.enum(["auto", "ui", "text"]).optional().describe("Optional invocation override; defaults to effective presentation.mode on this checkout (local preference, legacy project default, then built-in default). Auto uses advertised MCP Apps capability; UI prefers App; text supplies a compact conversational summary with details on request. Use prodero_text to guarantee no App attachment.");
10
+ /** Accept explicit MCP Apps HTML support, including hosts advertising the base MIME.
11
+ * A generic HTML declaration outside the UI extension is never sufficient. */
12
+ export function supportsMcpAppHtml(capabilities) {
13
+ const mimeTypes = getUiCapability(capabilities)?.mimeTypes;
14
+ return Array.isArray(mimeTypes) && mimeTypes.some(type => type === RESOURCE_MIME_TYPE || type === "text/html");
15
+ }
10
16
  export function resolvePresentation(mode, capabilities) {
11
- return mode === "auto" ? (getUiCapability(capabilities)?.mimeTypes?.includes(RESOURCE_MIME_TYPE) ? "ui" : "text") : mode;
17
+ return mode === "auto" ? (supportsMcpAppHtml(capabilities) ? "ui" : "text") : mode;
12
18
  }
13
19
  /** Shared validated callbacks; project defaults are read afresh for each invocation. */
14
20
  export function presentationTools(server, resolveRoot) {
@@ -51,7 +57,7 @@ export function presentationTools(server, resolveRoot) {
51
57
  const sessionId = ['prepare', 'session-close'].includes(String(payload.action)) ? payload.result?.session?.id : undefined;
52
58
  const group = presentationGroup ? `conversation:${presentationGroup}` : sessionId ? `session:${sessionId}` : undefined;
53
59
  const card = resolved === 'ui' && hasUi && group ? cards.issue(root, group) : undefined;
54
- const operational = conversationOperation(payload, viewModel, details || diagnostics, diagnostics);
60
+ const operational = conversationOperation(payload, viewModel, details || diagnostics, diagnostics, plain);
55
61
  // Skill preview already supplies its complete exact Markdown and approval guidance.
56
62
  // A generic compact projection must not discard that reviewed procedure.
57
63
  const skillCompanion = ['skill-preview', 'skill-review'].includes(String(payload.action)) ? result.content.find(c => c.type === 'text') : undefined;
@@ -64,12 +70,12 @@ export function presentationTools(server, resolveRoot) {
64
70
  // exact relay/action bindings alongside the fallback, avoiding duplication.
65
71
  const { context: _context, session: _session, automaticSync: _sync, ...fallbackOperational } = operational;
66
72
  return { ...result, content: resolved === "text" ? [{ type: "text", text: humanText }, { type: "text", text: JSON.stringify({ proderoOperation: operational }), annotations: { audience: ["assistant"] } }] : [...result.content, { type: "text", text: PRESENTATION_REPLY + fallback + (viewModel.knowledgeTable ? `\n\n${humanText}\n\n${JSON.stringify({ knowledgeRows: operational.knowledgeRows, numberedActions: operational.numberedActions })}` : ""), annotations: { audience: ["assistant"] } }],
67
- structuredContent: { ...payload, projectSettings: { root: settings.root, presentation: settings.presentation, presentationSources: settings.presentationSources, diagnostics: settings.diagnostics }, presentation: { requested, resolved, scope: input.presentation === undefined ? "project" : "invocation", actions: resolved === "text" ? operational.availableActions : viewModel.actions, ...(resolved === "text" || lifecycle ? { text: humanText, operational: resolved === "text" ? operational : fallbackOperational } : {}), ...(card ? { card } : {}) } } };
73
+ structuredContent: { ...payload, projectSettings: { root: settings.root, presentation: settings.presentation, presentationSources: settings.presentationSources, diagnostics: settings.diagnostics }, presentation: { requested, resolved, scope: input.presentation === undefined ? "project" : "invocation", textFormat: plain ? "terminal" : "markdown", actions: resolved === "text" ? operational.availableActions : viewModel.actions, ...(resolved === "text" || lifecycle ? { text: humanText, operational: resolved === "text" ? operational : fallbackOperational } : {}), ...(card ? { card } : {}) } } };
68
74
  };
69
75
  handlers.set(name, (args, extra) => run({ ...args, presentation: "text" }, extra));
70
76
  if (name === PRODERO_TOOLS.curationQuestion)
71
77
  return target.registerTool(name, { ...config, inputSchema: z.object({ ...config.inputSchema, presentation: modeSchema, presentationGroup: groupSchema, details: z.boolean().optional(), diagnostics: z.boolean().optional(), plain: z.boolean().optional() }).strict() }, run);
72
- return registerAppTool(target, name, { ...config, _meta: config._meta ?? {}, inputSchema: { ...config.inputSchema, presentation: modeSchema, presentationGroup: groupSchema, details: z.boolean().optional().describe("Show full details and proposed wording instead of the compact summary."), plain: z.boolean().optional().describe("Use ASCII frame and meter glyphs when Unicode rendering is unsuitable."), diagnostics: z.boolean().optional().describe("Include technical diagnostics, allocation evidence and exact workflow data. Implies details.") } }, run);
78
+ return registerAppTool(target, name, { ...config, _meta: config._meta ?? {}, inputSchema: { ...config.inputSchema, presentation: modeSchema, presentationGroup: groupSchema, details: z.boolean().optional().describe("Show full details and proposed wording instead of the compact summary."), plain: z.boolean().optional().describe("Explicit terminal-compatible ASCII fallback with frame and navigation aliases; default chat text is native Markdown."), diagnostics: z.boolean().optional().describe("Include technical diagnostics, allocation evidence and exact workflow data. Implies details.") } }, run);
73
79
  }
74
80
  function registerGateway() {
75
81
  // Register after the server installs its diagnostic boundary. Registering a
@@ -83,8 +89,8 @@ export function presentationTools(server, resolveRoot) {
83
89
  }, async ({ cardId, activate }) => ({ content: [], structuredContent: cards.inspect(cardId, activate) }));
84
90
  server.registerTool("prodero_text", {
85
91
  title: "Prodero Text Presentation",
86
- description: PRESENTATION_REPLY + " Open Prodero as compact conversational text (details:true for full inspection), or present a covered lifecycle/knowledge tool without an MCP App attachment. Uses the original tool schema, Core operation and approval rules. Pass its normal arguments unchanged. Mutating operations require the same explicit human review. No preference is persisted. Relay presentation.text exactly as supplied, preserving code fences, entry letters and the external Pulse menu. Healthy Prepare continues automatically; details use its returned context snapshot, never repeat retrieval. Operational data are for the agent; use exact continuations for follow-up replies. Show full proposed wording before approval.",
87
- inputSchema: { tool: z.enum(PRESENTED_TOOLS).default("prodero_open"), arguments: z.record(z.unknown()).default({}) },
92
+ description: PRESENTATION_REPLY + " Open Prodero as compact conversational text (details:true for full inspection), or present a covered lifecycle/knowledge tool without an MCP App attachment. Uses the original tool schema, Core operation and approval rules. Pass its normal arguments unchanged. Mutating operations require the same explicit human review. No preference is persisted. Relay presentation.text as native Markdown without wrapping the interface in a code fence. Preserve exact source/code bodies and named action bindings. Explicit plain:true retains the ASCII terminal fallback. Healthy Prepare continues automatically; details use its returned context snapshot, never repeat retrieval. Operational data are for the agent; use exact continuations for follow-up replies. Show full proposed wording before approval.",
93
+ inputSchema: { tool: z.enum(PRESENTED_TOOLS).default("prodero_open"), arguments: z.record(z.string(), z.unknown()).default({}) },
88
94
  // Conservatively describe the entire gateway; never mark a mixed operation tool read-only.
89
95
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
90
96
  }, async ({ tool, arguments: args }, extra) => handlers.get(tool)(args, extra));
@@ -4,6 +4,7 @@ import { closeSync, constants, fstatSync, openSync, writeSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { getUiCapability, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
6
6
  import { PRODERO_RESOURCE_URI, PRODERO_TOOLS } from "./contracts.js";
7
+ import { supportsMcpAppHtml } from "./presentation-server.js";
7
8
  const publicKeys = new Set(["experimental", "extensions", "io.modelcontextprotocol/ui", "mimeTypes", "roots", "listChanged", "sampling", "context", "tools", "elicitation", "form", "url", "tasks", "requests", "call", "createMessage", "create", "list", "cancel", "subscribe", "name", "version", "title", "platform", "userAgent", "clientInfo", "hostInfo", "clientId", "sessionId", "progressToken", "io.modelcontextprotocol/related-task", "taskId", "openai/userAgent", "openai/userLocation", "openai/locale", "openai/widgetSessionId", "ui", "protocolVersion"]);
8
9
  const publicNames = new Set(["codex", "codex-mcp-client", "Codex", "claude-desktop", "Claude", "Cursor", "cursor", "claude-code"]);
9
10
  let processFingerprintSalt;
@@ -23,7 +24,7 @@ export function installPresentationTrace(server) {
23
24
  if (value === null || typeof value === "boolean")
24
25
  return value;
25
26
  if (typeof value === "string") {
26
- if ((key === "mimeTypes" && value === RESOURCE_MIME_TYPE) || (key === "platform" && ["desktop", "mobile", "web"].includes(value)) || (key === "name" && publicNames.has(value)) || (key === "version" && /^\d+\.\d+\.\d+$/.test(value)) || (key === "protocolVersion" && versions.has(value)))
27
+ if ((key === "mimeTypes" && (value === RESOURCE_MIME_TYPE || value === "text/html")) || (key === "platform" && ["desktop", "mobile", "web"].includes(value)) || (key === "name" && publicNames.has(value)) || (key === "version" && /^\d+\.\d+\.\d+$/.test(value)) || (key === "protocolVersion" && versions.has(value)))
27
28
  return value;
28
29
  return { type: "string", fingerprint: fingerprint(value) };
29
30
  }
@@ -77,7 +78,7 @@ export function installPresentationTrace(server) {
77
78
  const snapshot = () => {
78
79
  const capabilities = server.server.getClientCapabilities();
79
80
  const ui = getUiCapability(capabilities);
80
- return { transportSession: safe(transport.sessionId), clientInfo: safe(server.server.getClientVersion()), capabilities: safe(capabilities), uiExtensionPresent: ui !== undefined, declaredMimeTypes: safe(ui?.mimeTypes, "mimeTypes"), supportsMcpAppHtml: Array.isArray(ui?.mimeTypes) && ui.mimeTypes.includes(RESOURCE_MIME_TYPE) };
81
+ return { transportSession: safe(transport.sessionId), clientInfo: safe(server.server.getClientVersion()), capabilities: safe(capabilities), uiExtensionPresent: ui !== undefined, declaredMimeTypes: safe(ui?.mimeTypes, "mimeTypes"), supportsMcpAppHtml: supportsMcpAppHtml(capabilities) };
81
82
  };
82
83
  const pending = new Map();
83
84
  let call = 0;