@prodero/curator 0.0.0-stage → 0.1.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/ARCHITECTURE.md +298 -0
  2. package/CHANGELOG.md +16 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +94 -2
  6. package/SECURITY.md +7 -0
  7. package/bin/prodero +7 -0
  8. package/dist/cli.js +228 -0
  9. package/dist/context-instructions.js +2 -0
  10. package/dist/contracts.js +50 -0
  11. package/dist/conversation-presentation.js +262 -0
  12. package/dist/core/agent-instructions.js +39 -0
  13. package/dist/core/applicability-label.js +11 -0
  14. package/dist/core/applicability.js +143 -0
  15. package/dist/core/bootstrap.js +129 -0
  16. package/dist/core/consolidation.js +315 -0
  17. package/dist/core/constants.js +21 -0
  18. package/dist/core/context-allocation.js +54 -0
  19. package/dist/core/context-snapshot.js +12 -0
  20. package/dist/core/context.js +489 -0
  21. package/dist/core/curation-clauses.js +68 -0
  22. package/dist/core/curation-evidence.js +98 -0
  23. package/dist/core/curation-graphical.js +105 -0
  24. package/dist/core/curation-review-presentation.js +4 -0
  25. package/dist/core/curation-review-state.js +62 -0
  26. package/dist/core/curation-review.js +573 -0
  27. package/dist/core/curation-revision.js +106 -0
  28. package/dist/core/curation-semantic-contract.js +239 -0
  29. package/dist/core/curation-semantic-jobs.js +280 -0
  30. package/dist/core/diagnostics.js +17 -0
  31. package/dist/core/format.js +114 -0
  32. package/dist/core/ids.js +6 -0
  33. package/dist/core/imports.js +169 -0
  34. package/dist/core/index.js +93 -0
  35. package/dist/core/instruction-review.js +86 -0
  36. package/dist/core/integrity-files.js +174 -0
  37. package/dist/core/intelligence.js +95 -0
  38. package/dist/core/knowledge-analysis.js +46 -0
  39. package/dist/core/knowledge-authority.js +143 -0
  40. package/dist/core/knowledge-curation.js +316 -0
  41. package/dist/core/knowledge-engine.js +475 -0
  42. package/dist/core/knowledge-graph.js +191 -0
  43. package/dist/core/knowledge-lifecycle.js +212 -0
  44. package/dist/core/knowledge-management.js +113 -0
  45. package/dist/core/knowledge-migration.js +84 -0
  46. package/dist/core/knowledge-policy.js +38 -0
  47. package/dist/core/knowledge-provenance.js +90 -0
  48. package/dist/core/knowledge-write-policy.js +85 -0
  49. package/dist/core/language.js +14 -0
  50. package/dist/core/matching.js +6 -0
  51. package/dist/core/memory-index.js +239 -0
  52. package/dist/core/memory-intelligence.js +288 -0
  53. package/dist/core/memory-link-repair.js +66 -0
  54. package/dist/core/memory-operations.js +122 -0
  55. package/dist/core/memory-pressure.js +212 -0
  56. package/dist/core/memory-sync-curator.js +62 -0
  57. package/dist/core/memory-sync.js +463 -0
  58. package/dist/core/memory-transformation-bundle.js +163 -0
  59. package/dist/core/memory-transformation-proposal.js +135 -0
  60. package/dist/core/memory-transformation.js +108 -0
  61. package/dist/core/memory.js +186 -0
  62. package/dist/core/migrations.js +569 -0
  63. package/dist/core/path-scope.js +27 -0
  64. package/dist/core/plugin-files.js +116 -0
  65. package/dist/core/project-root.js +39 -0
  66. package/dist/core/project.js +634 -0
  67. package/dist/core/promotion.js +279 -0
  68. package/dist/core/reconciliation.js +61 -0
  69. package/dist/core/secrets.js +67 -0
  70. package/dist/core/semantic-safety.js +33 -0
  71. package/dist/core/session-index.js +112 -0
  72. package/dist/core/session-review.js +150 -0
  73. package/dist/core/sessions.js +289 -0
  74. package/dist/core/settings.js +76 -0
  75. package/dist/core/skill-curation.js +239 -0
  76. package/dist/core/skill-polish.js +41 -0
  77. package/dist/core/skills.js +368 -0
  78. package/dist/core/storage.js +408 -0
  79. package/dist/core/sync-transaction.js +254 -0
  80. package/dist/core/telemetry.js +64 -0
  81. package/dist/core/templates.js +53 -0
  82. package/dist/core/types.js +2 -0
  83. package/dist/core/update.js +320 -0
  84. package/dist/curation-review-adapter.js +92 -0
  85. package/dist/diagnostic-boundary.js +54 -0
  86. package/dist/filesystem/permissions.js +39 -0
  87. package/dist/hosts/adapters.js +119 -0
  88. package/dist/hosts/files.js +133 -0
  89. package/dist/hosts/manager.js +150 -0
  90. package/dist/hosts/public-entry.js +47 -0
  91. package/dist/hosts/runtime-command.js +66 -0
  92. package/dist/hosts/runtime.js +139 -0
  93. package/dist/hosts/wizard.js +114 -0
  94. package/dist/knowledge-table.js +38 -0
  95. package/dist/mcp-development.js +69 -0
  96. package/dist/mcp-workspace.js +141 -0
  97. package/dist/memory-presentation.js +172 -0
  98. package/dist/plugins/api.js +2 -0
  99. package/dist/plugins/context.js +20 -0
  100. package/dist/plugins/mcp.js +72 -0
  101. package/dist/plugins/registry.js +106 -0
  102. package/dist/presentation-cards.js +32 -0
  103. package/dist/presentation-server.js +79 -0
  104. package/dist/presentation-trace.js +174 -0
  105. package/dist/presentation.js +412 -0
  106. package/dist/providers/mcp-sampling.js +45 -0
  107. package/dist/server.js +656 -0
  108. package/dist/settings-definition.js +22 -0
  109. package/dist/skill-instructions.js +2 -0
  110. package/dist-ui/index.html +214 -0
  111. package/docs/APPLICABILITY.md +151 -0
  112. package/docs/COMPATIBILITY.md +49 -0
  113. package/docs/CONTEXT-SELECTION-V2.md +61 -0
  114. package/docs/CURATION-APP-REVIEW.md +156 -0
  115. package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
  116. package/docs/IMPORTER-CURATION.md +142 -0
  117. package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
  118. package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
  119. package/docs/MIGRATIONS.md +99 -0
  120. package/docs/PRIVACY.md +28 -0
  121. package/docs/RECOVERY.md +56 -0
  122. package/docs/RELEASING.md +40 -0
  123. package/docs/hosts.md +134 -0
  124. package/docs/plugins.md +83 -0
  125. package/docs/ui-themes.md +43 -0
  126. package/instructions/AGENTS.md +33 -0
  127. package/package.json +88 -4
  128. package/plugins/migrate_ai_studio/README.md +101 -0
  129. package/plugins/migrate_ai_studio/index.cjs +396 -0
  130. package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
  131. package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
  132. package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
  133. package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
  134. package/plugins/migrate_ai_studio/plugin.json +11 -0
  135. package/plugins/migrate_ai_studio/schema.cjs +20 -0
  136. package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
  137. package/prodero.release.json +5 -0
@@ -0,0 +1,119 @@
1
+ import { PRESENTATION_LAUNCHER } from "../presentation.js";
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { parse as parseToml } from 'smol-toml';
5
+ import { parseTree, modify, applyEdits } from 'jsonc-parser';
6
+ import { fail } from './files.js';
7
+ export const MCP_START = '# prodero:mcp-start', MCP_END = '# prodero:mcp-end';
8
+ export const GLOBAL_START = '<!-- prodero:global-start -->', GLOBAL_END = '<!-- prodero:global-end -->';
9
+ export const LAUNCHER = `${GLOBAL_START}\n## Prodero launcher\n\n${PRESENTATION_LAUNCHER} The connected server resolves the current workspace from the host. Omit \`projectRoot\` normally; use it only to select among host-authorized roots. New projects initialize through Prodero MCP without shell setup.\n${GLOBAL_END}`;
10
+ export function block(text, start, end) {
11
+ const a = text.indexOf(start), b = text.indexOf(end);
12
+ if (a < 0 && b < 0)
13
+ return null;
14
+ if (a < 0 || b < a || text.indexOf(start, a + start.length) >= 0 || text.indexOf(end, b + end.length) >= 0)
15
+ fail('HOST_CONFLICT', 'Managed boundaries are missing, duplicated or ambiguous. Review them manually.');
16
+ return { text: text.slice(a, b + end.length), start: a, end: b + end.length };
17
+ }
18
+ export function replaceBlock(text, start, end, value) {
19
+ const found = block(text, start, end);
20
+ if (found)
21
+ return text.slice(0, found.start) + (value ?? '') + text.slice(found.end);
22
+ return value === undefined ? text : text + (text && !text.endsWith('\n') ? '\n' : '') + (text ? '\n' : '') + value + '\n';
23
+ }
24
+ export function strictJson(text) {
25
+ if (!text.trim())
26
+ fail('HOST_INVALID_CONFIG', 'Empty JSON host configuration. Restore valid JSON before retrying.');
27
+ const errors = [];
28
+ const tree = parseTree(text, errors, { allowTrailingComma: false, disallowComments: true });
29
+ const check = (node) => { if (node.type === 'object') {
30
+ const keys = node.children?.map(c => c.children[0].value) ?? [];
31
+ if (new Set(keys).size !== keys.length)
32
+ fail('HOST_INVALID_CONFIG', 'Duplicate keys in host JSON. Resolve ambiguity before retrying.');
33
+ } node.children?.forEach(check); };
34
+ if (errors.length || !tree || tree.type !== 'object')
35
+ fail('HOST_INVALID_CONFIG', 'Malformed JSON host configuration. Restore valid JSON before retrying.');
36
+ check(tree);
37
+ return JSON.parse(text);
38
+ }
39
+ function toml(text) { try {
40
+ return parseToml(text);
41
+ }
42
+ catch {
43
+ fail('HOST_INVALID_CONFIG', 'Malformed or ambiguous Codex TOML. Restore valid configuration before retrying.');
44
+ } }
45
+ function map(value) { if (value !== undefined && (!value || typeof value !== 'object' || Array.isArray(value)))
46
+ fail('HOST_UNSUPPORTED_CONFIG', 'MCP settings must be an object/table. No files changed.'); return value; }
47
+ export async function adapters(home, env, selection) {
48
+ // Relative overrides would make machine installation depend on the current project.
49
+ for (const key of (selection === 'codex' ? ['CODEX_HOME'] : selection === 'claude' ? ['CLAUDE_CONFIG_DIR'] : ['CODEX_HOME', 'CLAUDE_CONFIG_DIR']))
50
+ if (env[key] && !path.isAbsolute(env[key]))
51
+ fail('HOST_UNSUPPORTED_PATH', 'Host configuration overrides must be absolute paths inside the user home. No files changed.');
52
+ const codex = env.CODEX_HOME ? path.resolve(env.CODEX_HOME) : path.join(home, '.codex');
53
+ const claude = env.CLAUDE_CONFIG_DIR ? path.resolve(env.CLAUDE_CONFIG_DIR) : path.join(home, '.claude');
54
+ const launchers = [path.join(codex, 'AGENTS.md'), path.join(codex, 'AGENTS.override.md')];
55
+ const override = selection === 'claude' ? null : await fs.lstat(launchers[1]).catch(() => null);
56
+ const result = [{ id: 'codex', name: 'Codex', support: 'supported', config: path.join(codex, 'config.toml'), launchers, launcher: override && override.size > 0 ? launchers[1] : launchers[0],
57
+ async detect() { return await executable('codex', env) || Boolean(await fs.lstat(this.config).catch(() => null)); },
58
+ inspectConfig: toml, entry(text) { return map(toml(text).mcp_servers)?.prodero; },
59
+ alternateEntry(text) { return map(toml(text).mcp_servers)?.['prodero-dev']; },
60
+ removeAlternate: removeCodexDev,
61
+ edit(text, value) {
62
+ const stanza = value ? `${MCP_START}\n[mcp_servers.prodero]\ncommand = ${JSON.stringify(value.command)}\nargs = ${JSON.stringify(value.args)}\n${value.env ? `env = { ${Object.entries(value.env).map(([k, v]) => `${JSON.stringify(k)} = ${JSON.stringify(v)}`).join(", ")} }\n` : ""}${MCP_END}` : undefined;
63
+ const next = replaceBlock(text, MCP_START, MCP_END, stanza);
64
+ toml(next);
65
+ return next;
66
+ } },
67
+ { id: 'claude', name: 'Claude Code', support: 'experimental', config: env.CLAUDE_CONFIG_DIR ? path.join(claude, '.claude.json') : path.join(home, '.claude.json'), launchers: [path.join(claude, 'CLAUDE.md')], launcher: path.join(claude, 'CLAUDE.md'),
68
+ async detect() { return await executable('claude', env) || Boolean(await fs.lstat(this.config).catch(() => null)); },
69
+ inspectConfig: strictJson, entry(text) { return map(strictJson(text).mcpServers)?.prodero; },
70
+ alternateEntry(text) { return map(strictJson(text).mcpServers)?.['prodero-dev']; },
71
+ removeAlternate(text) { return applyEdits(text, modify(text, ['mcpServers', 'prodero-dev'], undefined, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' } })); },
72
+ edit(text, value) { const next = applyEdits(text, modify(text, ['mcpServers', 'prodero'], value, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' } })); strictJson(next); return next; }
73
+ }];
74
+ return selection && selection !== 'all' ? result.filter(host => host.id === selection) : result;
75
+ }
76
+ export async function executable(name, env) {
77
+ for (const dir of (env.PATH ?? '').split(path.delimiter).filter(d => path.isAbsolute(d))) {
78
+ try {
79
+ const p = path.join(dir, name);
80
+ await fs.access(p, fs.constants.X_OK);
81
+ if ((await fs.stat(p)).isFile())
82
+ return true;
83
+ }
84
+ catch { }
85
+ }
86
+ return false;
87
+ }
88
+ /** Migrate an unambiguous dev table and optional adjacent env table; preserve unrelated bytes. */
89
+ function removeCodexDev(text) {
90
+ const before = toml(text);
91
+ const headers = [...text.matchAll(/^\s*\[mcp_servers\.prodero-dev\][ \t]*(?:#[^\r\n]*)?\r?$/gm)];
92
+ if (headers.length !== 1)
93
+ fail('HOST_UNSUPPORTED_CONFIG', 'Migration requires one standalone [mcp_servers.prodero-dev] table. Review other TOML forms manually.');
94
+ const start = headers[0].index, bodyStart = start + headers[0][0].length;
95
+ const rest = text.slice(bodyStart);
96
+ let next = /^\s*\[/m.exec(rest), end = next ? bodyStart + next.index : text.length;
97
+ // Native Codex configurations commonly serialize env as a separate child table.
98
+ // Remove only this exact adjacent child; the semantic check below rejects any
99
+ // remaining descendants or other structure we cannot preserve unambiguously.
100
+ const envHeader = /^\s*\[mcp_servers\.prodero-dev\.env\][ \t]*(?:#[^\r\n]*)?\r?$/m.exec(text.slice(end));
101
+ if (envHeader?.index === 0) {
102
+ const afterEnv = end + envHeader[0].length;
103
+ next = /^\s*\[/m.exec(text.slice(afterEnv));
104
+ end = next ? afterEnv + next.index : text.length;
105
+ }
106
+ const result = text.slice(0, start) + text.slice(end);
107
+ delete before.mcp_servers['prodero-dev'];
108
+ const after = toml(result);
109
+ // Empty parent maps can disappear when their last child table is removed.
110
+ if (before.mcp_servers && Object.keys(before.mcp_servers).length === 0)
111
+ delete before.mcp_servers;
112
+ if (after.mcp_servers && Object.keys(after.mcp_servers).length === 0)
113
+ delete after.mcp_servers;
114
+ const stable = (v) => JSON.stringify(v, (_k, value) => value && typeof value === 'object' && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value);
115
+ if (stable(before) !== stable(after))
116
+ fail('HOST_UNSUPPORTED_CONFIG', 'Cannot remove the dev table without changing other TOML settings. Review the registration manually.');
117
+ return result;
118
+ }
119
+ //# sourceMappingURL=adapters.js.map
@@ -0,0 +1,133 @@
1
+ import { PRIVATE_DIRECTORY_MODE, PRIVATE_FILE_MODE } from "../filesystem/permissions.js";
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ export class IntegrationError extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(`${code}: ${message}`);
9
+ this.code = code;
10
+ }
11
+ }
12
+ export function fail(code, message) { throw new IntegrationError(code, message); }
13
+ /** Resolve only OS aliases above HOME; never follow links in user configuration. */
14
+ export async function safePath(home, target) {
15
+ const relative = path.relative(home, target);
16
+ if (relative.startsWith('..') || path.isAbsolute(relative))
17
+ fail('HOST_UNSUPPORTED_PATH', 'Host configuration must be inside the user home. No files changed.');
18
+ let cursor = home;
19
+ for (const part of ['', ...relative.split(path.sep).filter(Boolean)]) {
20
+ if (part)
21
+ cursor = path.join(cursor, part);
22
+ try {
23
+ const stat = await fs.lstat(cursor);
24
+ if (stat.isSymbolicLink() || (!stat.isDirectory() && cursor !== target) || (stat.isFile() && stat.nlink > 1))
25
+ fail('HOST_UNSAFE_PATH', 'Symlink, hard link or invalid configuration path. Review the host settings manually.');
26
+ if (cursor === target && !stat.isDirectory() && !stat.isFile())
27
+ fail('HOST_UNSAFE_PATH', 'Unsupported configuration file type.');
28
+ }
29
+ catch (e) {
30
+ if (e.code !== 'ENOENT')
31
+ throw e;
32
+ }
33
+ }
34
+ }
35
+ export async function read(home, target) {
36
+ await safePath(home, target);
37
+ try {
38
+ const s = await fs.stat(target);
39
+ if (!s.isFile() || s.size > 8_000_000)
40
+ fail('HOST_UNSUPPORTED_CONFIG', 'Host configuration is not a supported regular text file.');
41
+ return await fs.readFile(target, 'utf8');
42
+ }
43
+ catch (e) {
44
+ if (e.code === 'ENOENT')
45
+ return null;
46
+ throw e;
47
+ }
48
+ }
49
+ export async function directory(home, target) {
50
+ await safePath(home, target);
51
+ await fs.mkdir(target, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
52
+ await safePath(home, target);
53
+ }
54
+ async function atomic(home, target, content) {
55
+ await safePath(home, target);
56
+ if (content === null) {
57
+ await fs.rm(target, { force: true });
58
+ return;
59
+ }
60
+ await directory(home, path.dirname(target));
61
+ const mode = (await fs.stat(target).catch(() => null))?.mode ?? PRIVATE_FILE_MODE;
62
+ const temporary = target + '.prodero-' + crypto.randomUUID();
63
+ try {
64
+ await fs.writeFile(temporary, content, { flag: 'wx', mode: mode & 0o777 });
65
+ await safePath(home, target);
66
+ await fs.rename(temporary, target);
67
+ }
68
+ finally {
69
+ await fs.rm(temporary, { force: true });
70
+ }
71
+ }
72
+ /** One per-host lock and private before/after journal. Never roll back over concurrent edits. */
73
+ export async function transact(home, state, changes) {
74
+ const writes = changes.filter(c => c.before !== c.after);
75
+ if (!writes.length)
76
+ return;
77
+ await directory(home, state);
78
+ const lock = path.join(state, 'lock'), pending = path.join(state, 'pending.json');
79
+ await safePath(home, lock);
80
+ try {
81
+ await fs.mkdir(lock, { mode: PRIVATE_DIRECTORY_MODE });
82
+ }
83
+ catch {
84
+ fail('HOST_BUSY', 'Another integration operation or interrupted lock exists. Close host processes and inspect the private integration state before retrying.');
85
+ }
86
+ let journalWritten = false;
87
+ try {
88
+ if (await read(home, pending) !== null)
89
+ fail('HOST_RECOVERY_REQUIRED', 'A preserved integration journal requires review before retrying.');
90
+ for (const c of changes)
91
+ if (await read(home, c.path) !== c.before)
92
+ fail('HOST_CONCURRENT_CHANGE', 'Host settings changed during inspection. Retry after closing the host.');
93
+ const journal = JSON.stringify({ format: 1, changes: writes }, null, 2) + '\n';
94
+ await atomic(home, pending, journal);
95
+ journalWritten = true;
96
+ // Backups can contain host credentials; keep them private and never print their contents.
97
+ await fs.writeFile(path.join(state, 'backup-' + crypto.randomUUID() + '.json'), journal, { flag: 'wx', mode: PRIVATE_FILE_MODE });
98
+ for (const c of writes) {
99
+ if (await read(home, c.path) !== c.before)
100
+ fail('HOST_CONCURRENT_CHANGE', 'Host settings changed while applying integration.');
101
+ await atomic(home, c.path, c.after);
102
+ }
103
+ for (const c of changes)
104
+ if (await read(home, c.path) !== c.after)
105
+ fail('HOST_CONCURRENT_CHANGE', 'Host settings changed during verification.');
106
+ await fs.rm(pending);
107
+ }
108
+ catch (e) {
109
+ if (journalWritten) {
110
+ let restored = true;
111
+ for (const c of [...writes].reverse())
112
+ try {
113
+ const now = await read(home, c.path);
114
+ if (now === c.after)
115
+ await atomic(home, c.path, c.before);
116
+ else if (now !== c.before)
117
+ restored = false;
118
+ }
119
+ catch {
120
+ restored = false;
121
+ }
122
+ if (restored)
123
+ await fs.rm(pending, { force: true });
124
+ else
125
+ fail('HOST_RECOVERY_REQUIRED', 'Integration interrupted with concurrent changes. Preserve the private pending.json and backups; review before retrying.');
126
+ }
127
+ throw e;
128
+ }
129
+ finally {
130
+ await fs.rmdir(lock).catch(() => { });
131
+ }
132
+ }
133
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1,150 @@
1
+ import { publicEntry, verifyPublicEntry } from './public-entry.js';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { z } from 'zod';
7
+ import { PRODERO_VERSION } from '../core/constants.js';
8
+ import { adapters, executable, block, replaceBlock, LAUNCHER, GLOBAL_START, GLOBAL_END, MCP_START, MCP_END } from './adapters.js';
9
+ import { IntegrationError, fail, read, transact } from './files.js';
10
+ const legacyReceiptSchema = z.object({ format: z.literal(1), host: z.enum(['codex', 'claude']), config: z.string(), launcherPath: z.string(), entry: z.object({ command: z.string(), args: z.array(z.string()), type: z.string().optional(), env: z.record(z.string()).optional() }).strict(), launcher: z.string(), mcpBlock: z.string().nullable(), originalConfig: z.string().nullable(), originalLauncher: z.string().nullable(), installedConfig: z.string(), installedLauncher: z.string() }).strict();
11
+ const runtimeEntrySchema = legacyReceiptSchema.shape.entry;
12
+ export const receiptSchema = z.union([
13
+ legacyReceiptSchema,
14
+ legacyReceiptSchema.extend({ format: z.literal(2), runtime: z.object({ mode: z.enum(['package', 'dev']), packageEntry: runtimeEntrySchema, devRoot: z.string().nullable(), devEntry: runtimeEntrySchema.nullable() }).strict() }).strict(),
15
+ ]);
16
+ const stable = (v) => JSON.stringify(v, (_k, value) => value && typeof value === 'object' && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value);
17
+ const errorStatus = (base, e) => ({ ...base, mcpConfigured: null, launcherInstalled: null, state: e instanceof IntegrationError ? e.code === 'HOST_CONFLICT' ? 'conflicted' : e.code === 'HOST_RECOVERY_REQUIRED' ? 'recovery-required' : 'unsupported-configuration' : 'unsupported-configuration', message: e instanceof IntegrationError ? e.message : 'HOST_CONFIGURATION_ERROR: Cannot safely read or update host settings. Check file access and restore valid configuration; no configuration contents are shown.' });
18
+ export function environment(options) {
19
+ const env = options.env ?? process.env, home = options.home ?? env.HOME ?? os.homedir();
20
+ if (!path.isAbsolute(home) || (env.XDG_STATE_HOME && !path.isAbsolute(env.XDG_STATE_HOME)))
21
+ fail('HOST_UNSUPPORTED_PATH', 'HOME and integration state overrides must be absolute paths. No files changed.');
22
+ return { env, home: path.resolve(home), state: path.join(env.XDG_STATE_HOME || path.join(home, '.local/state'), 'prodero/host-integrations'), entryPoint: options.entryPoint ?? fileURLToPath(new URL('../cli.js', import.meta.url)) };
23
+ }
24
+ export function expectedEntry(host, entryPoint, env = process.env) { return publicEntry(host.id, entryPoint, env); }
25
+ export async function inspect(host, options, remove = false, adoptDev = false) {
26
+ const { home, env, state, entryPoint } = options, dir = path.join(state, host.id), receiptPath = path.join(dir, 'receipt.json');
27
+ const detected = await host.detect();
28
+ const base = { id: host.id, name: host.name, support: host.support, detected, mcpConfigured: false, launcherInstalled: false, state: 'not-detected', message: 'Agent not detected.' };
29
+ let receipt;
30
+ const saved = await read(home, receiptPath);
31
+ if (saved !== null) {
32
+ const parsed = receiptSchema.safeParse(JSON.parse(saved));
33
+ if (!parsed.success)
34
+ fail('HOST_UNSUPPORTED_CONFIG', 'Invalid integration receipt. Restore the private receipt or review integration manually.');
35
+ receipt = parsed.data;
36
+ if (receipt.host !== host.id || receipt.config !== host.config || !host.launchers.includes(receipt.launcherPath))
37
+ fail('HOST_CONFLICT', 'Host configuration location changed. Review the original managed integration before reinstalling.');
38
+ }
39
+ if (await read(home, path.join(dir, 'pending.json')) !== null)
40
+ fail('HOST_RECOVERY_REQUIRED', 'Preserved private integration journal requires manual recovery before another write.');
41
+ if (await fs.lstat(path.join(dir, 'lock')).catch(() => null))
42
+ fail('HOST_RECOVERY_REQUIRED', 'An integration lock exists. Close other installers; inspect private state if an operation was interrupted.');
43
+ const launcherPath = receipt?.launcherPath ?? host.launcher;
44
+ if (!remove && receipt && launcherPath !== host.launcher)
45
+ fail('HOST_CONFLICT', 'A global instruction override now shadows the installed launcher. Uninstall first, then reinstall for the active global file.');
46
+ const config = await read(home, host.config), launcher = await read(home, launcherPath);
47
+ // Do not install a second block hidden in Codex's inactive global instruction file.
48
+ // Removal can still clean up the receipt-owned file without touching the other one.
49
+ if (!remove)
50
+ for (const other of host.launchers.filter(p => p !== launcherPath)) {
51
+ if (block(await read(home, other) ?? '', GLOBAL_START, GLOBAL_END))
52
+ fail('HOST_CONFLICT', 'Another global instruction file contains a Prodero launcher. Reconcile the existing launcher before installing.');
53
+ }
54
+ const text = config ?? (host.id === 'claude' ? '{}\n' : '');
55
+ host.inspectConfig(text);
56
+ if (receipt?.originalConfig !== null && receipt?.originalConfig !== undefined)
57
+ host.inspectConfig(receipt.originalConfig);
58
+ const current = host.entry(text), owned = block(launcher ?? '', GLOBAL_START, GLOBAL_END), mcp = host.id === 'codex' ? block(text, MCP_START, MCP_END) : null;
59
+ base.mcpConfigured = current !== undefined;
60
+ base.launcherInstalled = Boolean(owned);
61
+ const alternate = host.alternateEntry?.(text);
62
+ if (!remove && alternate !== undefined && (host.id === 'claude' || alternate?.enabled !== false || (adoptDev && !receipt && current === undefined && !mcp))) {
63
+ const valid = alternate && typeof alternate === 'object' && ((typeof alternate.command === 'string' && alternate.command.trim() && (alternate.args === undefined || (Array.isArray(alternate.args) && alternate.args.every(a => typeof a === 'string')))) || (typeof alternate.url === 'string' && /^https?:\/\//.test(alternate.url)));
64
+ if (!valid)
65
+ fail('HOST_UNSUPPORTED_CONFIG', `Invalid prodero-dev connection in ${host.config}. Review its command or URL before installing.`);
66
+ if (current !== undefined || receipt || mcp)
67
+ fail('HOST_CONFLICT', `Both prodero-dev and a prodero installation are present in ${host.config}. Disable one connection before continuing.`);
68
+ base.mcpConfigured = true;
69
+ base.state = owned ? 'externally-configured' : 'external-incomplete';
70
+ base.message = owned ? `Already configured through prodero-dev (managed manually). Settings: ${host.config}. No second server will be installed.` : `prodero-dev is configured in ${host.config}, but the global launcher is missing in ${launcherPath}. Review this manual setup before installing; no second server will be added.`;
71
+ return { base, receipt, receiptPath, saved, dir, config, text, launcher, launcherPath, desired: expectedEntry(host, entryPoint, env) };
72
+ }
73
+ if (!receipt && (current !== undefined || owned || mcp))
74
+ fail('HOST_CONFLICT', `Existing Prodero configuration has no ownership receipt. Review ${current !== undefined || mcp ? host.config : launcherPath}; back up and reconcile only the Prodero entry/block before installing.`);
75
+ if (receipt) {
76
+ if (current !== undefined && stable(current) !== stable(receipt.entry))
77
+ fail('HOST_CONFLICT', 'The managed Prodero MCP entry was modified. Preserve your changes and review before reinstalling/uninstalling.');
78
+ if (owned && owned.text !== receipt.launcher)
79
+ fail('HOST_CONFLICT', 'The managed launcher was modified. Preserve your changes and review its block before reinstalling/uninstalling.');
80
+ if (host.id === 'codex' && ((mcp && mcp.text !== receipt.mcpBlock) || (current !== undefined && !mcp)))
81
+ fail('HOST_CONFLICT', 'The managed Codex MCP block was modified or lost its boundaries.');
82
+ }
83
+ if (receipt?.format === 2) {
84
+ const runtime = receipt.runtime;
85
+ if ((runtime.mode === 'dev' && (!runtime.devRoot || !path.isAbsolute(runtime.devRoot) || !runtime.devEntry)) || stable(receipt.entry) !== stable(runtime.mode === 'dev' ? runtime.devEntry : runtime.packageEntry))
86
+ fail('HOST_UNSUPPORTED_CONFIG', 'Runtime receipt is inconsistent. Restore the private receipt before switching.');
87
+ }
88
+ base.runtimeMode = receipt?.format === 2 ? receipt.runtime.mode : receipt ? 'package' : undefined;
89
+ const desired = receipt?.format === 2 && receipt.runtime.mode === 'dev' ? { ...expectedEntry(host, entryPoint, env), args: receipt.runtime.devEntry.args[0] === 'mcp' ? receipt.runtime.devEntry.args : ['mcp', '--dev'] } : expectedEntry(host, entryPoint, env);
90
+ base.state = receipt ? (current !== undefined && owned && stable(current) === stable(desired) && owned.text === LAUNCHER ? 'installed' : 'outdated') : detected ? 'not-configured' : 'not-detected';
91
+ base.message = base.state === 'installed' ? 'MCP configured; launcher installed. Restart the host after package updates.' : base.state === 'outdated' ? 'Managed integration is missing or outdated. Run prodero install to update it.' : detected ? 'Agent detected; Prodero not configured.' : 'Agent not detected.';
92
+ return { base, receipt, receiptPath, saved, dir, config, text, launcher, launcherPath, desired };
93
+ }
94
+ export async function machineIntegrations(action, selection, options = {}) {
95
+ if (selection && !['all', 'codex', 'claude', 'cursor'].includes(selection))
96
+ fail('HOST_UNKNOWN', 'Unknown host. Choose codex or claude, or use --all.');
97
+ const ctx = environment(options);
98
+ if (action === 'install' && !options.entryPoint && await fs.stat(path.resolve(path.dirname(ctx.entryPoint), '../src/cli.ts')).catch(() => null))
99
+ fail('HOST_UNPACKAGED_RUNTIME', 'Install @prodero/curator from npm or a release tarball before configuring hosts. Development checkout launchers are not registered.');
100
+ const hosts = await adapters(ctx.home, ctx.env, selection);
101
+ const results = [];
102
+ for (const host of hosts) {
103
+ if (selection && selection !== 'all' && selection !== host.id)
104
+ continue;
105
+ let base = { id: host.id, name: host.name, support: host.support, detected: await host.detect(), mcpConfigured: false, launcherInstalled: false, state: 'not-configured', message: '' };
106
+ try {
107
+ const view = await inspect(host, ctx, action === 'uninstall');
108
+ base = view.base;
109
+ if (action === 'status' || (action === 'install' && (base.state === 'externally-configured' || base.state === 'external-incomplete' || (!base.detected && !view.receipt)))) {
110
+ results.push(base);
111
+ continue;
112
+ }
113
+ const { receipt, receiptPath, saved, dir, config, text, launcher, launcherPath, desired } = view;
114
+ if (action === 'uninstall' && !receipt) {
115
+ results.push({ ...base, message: 'No managed integration to remove. Existing project knowledge remains intact.' });
116
+ continue;
117
+ }
118
+ let nextConfig, nextLauncher, nextReceipt;
119
+ if (action === 'install') {
120
+ // All parsing and ownership checks happen before the first write.
121
+ await verifyPublicEntry(desired);
122
+ nextConfig = host.edit(text, desired);
123
+ nextLauncher = replaceBlock(launcher ?? '', GLOBAL_START, GLOBAL_END, LAUNCHER);
124
+ const next = { ...(receipt?.format === 2 ? { format: 2, runtime: { ...receipt.runtime, packageEntry: expectedEntry(host, ctx.entryPoint, ctx.env), ...(receipt.runtime.mode === 'dev' ? { devEntry: desired } : {}) } } : { format: 1 }), host: host.id, config: host.config, launcherPath, entry: desired, launcher: LAUNCHER, mcpBlock: host.id === 'codex' ? block(nextConfig, MCP_START, MCP_END).text : null, originalConfig: receipt ? receipt.originalConfig : config, originalLauncher: receipt ? receipt.originalLauncher : launcher, installedConfig: nextConfig, installedLauncher: nextLauncher };
125
+ // An unchanged installation must not rewrite JSON formatting or the ownership receipt.
126
+ if (base.state === 'installed') {
127
+ results.push(base);
128
+ continue;
129
+ }
130
+ nextReceipt = JSON.stringify(next, null, 2) + '\n';
131
+ }
132
+ else {
133
+ nextConfig = config === receipt.installedConfig ? receipt.originalConfig : config === null ? null : host.edit(text, undefined);
134
+ nextLauncher = launcher === receipt.installedLauncher ? receipt.originalLauncher : launcher === null ? null : replaceBlock(launcher, GLOBAL_START, GLOBAL_END, undefined);
135
+ nextReceipt = null;
136
+ }
137
+ const changes = [{ path: host.config, before: config, after: nextConfig }, { path: launcherPath, before: launcher, after: nextLauncher }, { path: receiptPath, before: saved, after: nextReceipt }];
138
+ await transact(ctx.home, dir, changes);
139
+ results.push({ ...base, mcpConfigured: action === 'install', launcherInstalled: action === 'install', state: action === 'install' ? 'installed' : 'not-configured', message: action === 'install' ? 'Managed MCP and minimal launcher installed. Restart the host; open a repository and type prodero.' : 'Managed MCP and launcher removed. Existing project knowledge and project instructions remain intact.' });
140
+ }
141
+ catch (e) {
142
+ results.push(errorStatus(base, e));
143
+ }
144
+ }
145
+ if (!selection || selection === 'all' || selection === 'cursor')
146
+ results.push({ id: 'cursor', name: 'Cursor', support: 'unimplemented', detected: await executable('cursor', ctx.env) || Boolean(await fs.lstat(path.join(ctx.home, '.cursor')).catch(() => null)), mcpConfigured: false, launcherInstalled: false, state: 'unsupported', message: 'Detection only; Cursor configuration is not modified.' });
147
+ const failed = results.some(r => ['conflicted', 'unsupported-configuration', 'recovery-required', 'external-incomplete'].includes(r.state)) || (action === 'install' && selection !== undefined && selection !== 'all' && results.every(r => !r.detected || r.support === 'unimplemented'));
148
+ return { ok: !failed, version: PRODERO_VERSION, scope: 'machine', action, hosts: results, ...(action === 'uninstall' ? { message: 'Existing project knowledge and project instructions remain intact.' } : {}) };
149
+ }
150
+ //# sourceMappingURL=manager.js.map
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fail } from './files.js';
4
+ /** Public package executable, never resolved through the current workspace/PATH. */
5
+ export function publicEntry(host, cli, env = process.env) {
6
+ const command = path.resolve(path.dirname(cli), '../bin/prodero');
7
+ const paths = [path.dirname(process.execPath), ...(env.PATH ?? '').split(path.delimiter), '/usr/bin', '/bin'];
8
+ const PATH = [...new Set(paths.filter(p => path.isAbsolute(p)))].join(path.delimiter);
9
+ return { ...(host === 'claude' ? { type: 'stdio' } : {}), command, args: ['mcp'], env: { PATH } };
10
+ }
11
+ export async function verifyPublicEntry(entry) {
12
+ if (!path.isAbsolute(entry.command) || path.basename(entry.command) !== 'prodero')
13
+ fail('HOST_RUNTIME_MISSING', 'Expected an absolute public Prodero executable.');
14
+ const executable = await fs.realpath(entry.command).catch(() => fail('HOST_RUNTIME_MISSING', 'Public Prodero executable is missing. Install the updated package.'));
15
+ const root = path.resolve(path.dirname(executable), '..');
16
+ let metadata;
17
+ try {
18
+ metadata = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
19
+ }
20
+ catch {
21
+ fail('HOST_RUNTIME_MISSING', 'Cannot read the installed Prodero package.');
22
+ }
23
+ if (!['@prodero/curator', '@prodero/cli'].includes(metadata.name ?? '') || metadata.bin?.prodero !== './bin/prodero' || executable !== path.join(root, 'bin/prodero'))
24
+ fail('HOST_RUNTIME_UNSUPPORTED', 'The package does not expose the supported public Prodero executable.');
25
+ if (await fs.stat(path.join(root, 'src/cli.ts')).catch(() => null))
26
+ fail('HOST_UNPACKAGED_RUNTIME', 'Use an installed Prodero package for the public executable, not a source checkout.');
27
+ await fs.access(executable, fs.constants.X_OK).catch(() => fail('HOST_RUNTIME_MISSING', 'Public Prodero executable is not executable.'));
28
+ if (!(await fs.stat(executable)).isFile() || !(await fs.readFile(executable, 'utf8')).startsWith('#!/usr/bin/env node\n'))
29
+ fail('HOST_RUNTIME_UNSUPPORTED', 'Unsupported public Prodero executable.');
30
+ for (const relative of ['dist/cli.js', 'dist/hosts/runtime.js'])
31
+ if (!(await fs.stat(path.join(root, relative)).catch(() => null))?.isFile())
32
+ fail('HOST_RUNTIME_UNSUPPORTED', 'The installed package is missing runtime switching support.');
33
+ // The saved PATH must resolve the shebang to this installation's Node, even in GUI hosts.
34
+ const first = entry.env?.PATH?.split(path.delimiter)[0];
35
+ if (!first || !path.isAbsolute(first) || await fs.realpath(path.join(first, 'node')).catch(() => null) !== await fs.realpath(process.execPath))
36
+ fail('HOST_RUNTIME_MISSING', 'The public launcher requires the current Node executable on its managed PATH.');
37
+ return root;
38
+ }
39
+ export async function resolveNpmCli(env) {
40
+ for (const dir of (env.PATH ?? '').split(path.delimiter).filter(p => path.isAbsolute(p))) {
41
+ const candidate = await fs.realpath(path.join(dir, 'npm')).catch(() => null);
42
+ if (candidate && /\.(?:c?js|mjs)$/.test(candidate) && (await fs.stat(candidate)).isFile())
43
+ return candidate;
44
+ }
45
+ return fail('HOST_RUNTIME_MISSING', 'Cannot resolve an npm JavaScript executable for development builds.');
46
+ }
47
+ //# sourceMappingURL=public-entry.js.map
@@ -0,0 +1,66 @@
1
+ import { switchRuntime } from './runtime.js';
2
+ const help = `Usage: prodero runtime <dev|package> --host <codex|claude> [options]
3
+
4
+ --checkout PATH Absolute Prodero checkout path (required for first dev switch)
5
+ --package-root PATH Absolute installed package directory (if not already recorded)
6
+ --workspace PATH Workspace to check for overrides (defaults to current directory)
7
+ --dry-run Show the plan without changing any files
8
+ --adopt-dev Explicitly migrate a manual prodero-dev registration
9
+
10
+ Only the named host is changed. Restart that host after switching.
11
+ Use prodero install for initial setup; runtime switching never installs packages.`;
12
+ export async function runtimeCommand(args) {
13
+ if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
14
+ console.log(help);
15
+ return;
16
+ }
17
+ const mode = args[0];
18
+ if (!['dev', 'package'].includes(mode))
19
+ throw new Error(help);
20
+ let host;
21
+ const options = {};
22
+ const seen = new Set();
23
+ for (let i = 1; i < args.length; i++) {
24
+ const flag = args[i];
25
+ if (seen.has(flag))
26
+ throw new Error(`Specify ${flag} only once.`);
27
+ seen.add(flag);
28
+ if (flag === '--dry-run') {
29
+ options.dryRun = true;
30
+ continue;
31
+ }
32
+ if (flag === '--adopt-dev') {
33
+ options.adoptDev = true;
34
+ continue;
35
+ }
36
+ if (!['--host', '--checkout', '--package-root', '--workspace'].includes(flag))
37
+ throw new Error(help);
38
+ const value = args[++i];
39
+ if (!value || value.startsWith('-'))
40
+ throw new Error(`${flag} requires a value.`);
41
+ if (flag === '--host')
42
+ host = value;
43
+ else if (flag === '--checkout')
44
+ options.checkout = value;
45
+ else if (flag === '--package-root')
46
+ options.packageRoot = value;
47
+ else
48
+ options.workspace = value;
49
+ }
50
+ if (!host || !['codex', 'claude'].includes(host))
51
+ throw new Error('Specify exactly one --host: codex or claude.');
52
+ if (mode === 'package' && options.checkout)
53
+ throw new Error('--checkout is only used with runtime dev.');
54
+ const result = await switchRuntime(host, mode, options);
55
+ const safe = (value) => value.replace(/[\x00-\x1f\x7f-\x9f]/g, ' ');
56
+ console.log(`\nProdero · ${host === 'codex' ? 'Codex' : 'Claude Code'} runtime\n`);
57
+ console.log(`${result.dryRun ? 'Preview' : result.changed ? 'Configured' : 'Already configured'}: ${result.previousMode} → ${result.mode}`);
58
+ console.log(`Runtime: ${safe(result.runtimePath)}\nSettings: ${safe(result.config)}`);
59
+ if (result.migration)
60
+ console.log('Migration: replace the manual prodero-dev registration with managed prodero; retain private originals for uninstall.');
61
+ if (result.dryRun)
62
+ console.log(`No files changed.${result.migration ? ' Apply with --adopt-dev after reviewing this migration.' : ''}`);
63
+ else
64
+ console.log('Restart or reconnect this agent to load the selected runtime. The running server has not been restarted.');
65
+ }
66
+ //# sourceMappingURL=runtime-command.js.map