@prodero/curator 0.0.0-stage → 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 (148) hide show
  1. package/ARCHITECTURE.md +298 -0
  2. package/CHANGELOG.md +33 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +93 -2
  6. package/SECURITY.md +7 -0
  7. package/bin/prodero +7 -0
  8. package/dist/cli.js +266 -0
  9. package/dist/context-instructions.js +2 -0
  10. package/dist/contracts.js +51 -0
  11. package/dist/conversation-presentation.js +297 -0
  12. package/dist/core/agent-instructions.js +52 -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 +117 -0
  24. package/dist/core/curation-question.js +28 -0
  25. package/dist/core/curation-review-presentation.js +4 -0
  26. package/dist/core/curation-review-state.js +62 -0
  27. package/dist/core/curation-review.js +573 -0
  28. package/dist/core/curation-revision.js +106 -0
  29. package/dist/core/curation-semantic-contract.js +242 -0
  30. package/dist/core/curation-semantic-jobs.js +340 -0
  31. package/dist/core/diagnostics.js +17 -0
  32. package/dist/core/format.js +114 -0
  33. package/dist/core/ids.js +6 -0
  34. package/dist/core/imports.js +169 -0
  35. package/dist/core/index.js +93 -0
  36. package/dist/core/instruction-review.js +86 -0
  37. package/dist/core/integrity-files.js +174 -0
  38. package/dist/core/intelligence.js +95 -0
  39. package/dist/core/knowledge-analysis.js +46 -0
  40. package/dist/core/knowledge-authority.js +143 -0
  41. package/dist/core/knowledge-curation.js +316 -0
  42. package/dist/core/knowledge-engine.js +475 -0
  43. package/dist/core/knowledge-graph.js +192 -0
  44. package/dist/core/knowledge-lifecycle.js +212 -0
  45. package/dist/core/knowledge-management.js +113 -0
  46. package/dist/core/knowledge-migration.js +84 -0
  47. package/dist/core/knowledge-policy.js +38 -0
  48. package/dist/core/knowledge-provenance.js +90 -0
  49. package/dist/core/knowledge-write-policy.js +85 -0
  50. package/dist/core/language.js +14 -0
  51. package/dist/core/local-preferences.js +76 -0
  52. package/dist/core/matching.js +6 -0
  53. package/dist/core/memory-index.js +241 -0
  54. package/dist/core/memory-intelligence.js +288 -0
  55. package/dist/core/memory-link-repair.js +70 -0
  56. package/dist/core/memory-operations.js +122 -0
  57. package/dist/core/memory-pressure.js +212 -0
  58. package/dist/core/memory-sync-curator.js +82 -0
  59. package/dist/core/memory-sync.js +558 -0
  60. package/dist/core/memory-transformation-bundle.js +163 -0
  61. package/dist/core/memory-transformation-proposal.js +135 -0
  62. package/dist/core/memory-transformation.js +108 -0
  63. package/dist/core/memory.js +186 -0
  64. package/dist/core/migrations.js +569 -0
  65. package/dist/core/npm-update.js +289 -0
  66. package/dist/core/path-scope.js +27 -0
  67. package/dist/core/plugin-files.js +116 -0
  68. package/dist/core/project-root.js +39 -0
  69. package/dist/core/project.js +634 -0
  70. package/dist/core/promotion.js +279 -0
  71. package/dist/core/reconciliation.js +61 -0
  72. package/dist/core/secrets.js +67 -0
  73. package/dist/core/semantic-safety.js +33 -0
  74. package/dist/core/session-index.js +116 -0
  75. package/dist/core/session-review.js +150 -0
  76. package/dist/core/sessions.js +289 -0
  77. package/dist/core/settings.js +94 -0
  78. package/dist/core/skill-curation.js +239 -0
  79. package/dist/core/skill-polish.js +41 -0
  80. package/dist/core/skills.js +387 -0
  81. package/dist/core/storage.js +462 -0
  82. package/dist/core/sync-transaction.js +290 -0
  83. package/dist/core/telemetry.js +64 -0
  84. package/dist/core/templates.js +53 -0
  85. package/dist/core/types.js +2 -0
  86. package/dist/core/update.js +326 -0
  87. package/dist/curation-review-adapter.js +92 -0
  88. package/dist/diagnostic-boundary.js +55 -0
  89. package/dist/filesystem/permissions.js +39 -0
  90. package/dist/hosts/adapters.js +119 -0
  91. package/dist/hosts/files.js +133 -0
  92. package/dist/hosts/manager.js +150 -0
  93. package/dist/hosts/public-entry.js +47 -0
  94. package/dist/hosts/runtime-command.js +66 -0
  95. package/dist/hosts/runtime.js +139 -0
  96. package/dist/hosts/wizard.js +114 -0
  97. package/dist/knowledge-table.js +38 -0
  98. package/dist/mcp-development.js +69 -0
  99. package/dist/mcp-workspace.js +141 -0
  100. package/dist/memory-presentation.js +182 -0
  101. package/dist/memory-sync-operation.js +88 -0
  102. package/dist/plugins/api.js +2 -0
  103. package/dist/plugins/context.js +20 -0
  104. package/dist/plugins/mcp.js +72 -0
  105. package/dist/plugins/registry.js +106 -0
  106. package/dist/presentation-cards.js +32 -0
  107. package/dist/presentation-server.js +94 -0
  108. package/dist/presentation-trace.js +174 -0
  109. package/dist/presentation.js +443 -0
  110. package/dist/providers/mcp-sampling.js +45 -0
  111. package/dist/server.js +664 -0
  112. package/dist/settings-definition.js +22 -0
  113. package/dist/skill-instructions.js +2 -0
  114. package/dist/status-presentation.js +20 -0
  115. package/dist/text-layout.js +278 -0
  116. package/dist/update-command.js +53 -0
  117. package/dist/update-instructions.js +4 -0
  118. package/dist-ui/index.html +216 -0
  119. package/docs/APPLICABILITY.md +151 -0
  120. package/docs/COMPATIBILITY.md +68 -0
  121. package/docs/CONTEXT-SELECTION-V2.md +61 -0
  122. package/docs/CURATION-APP-REVIEW.md +156 -0
  123. package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
  124. package/docs/IMPORTER-CURATION.md +142 -0
  125. package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
  126. package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
  127. package/docs/MIGRATIONS.md +103 -0
  128. package/docs/PRIVACY.md +28 -0
  129. package/docs/RECOVERY.md +58 -0
  130. package/docs/RELEASING.md +98 -0
  131. package/docs/hosts.md +152 -0
  132. package/docs/images/prodero-context-ready-cropped.png +0 -0
  133. package/docs/images/prodero-overview-cropped.png +0 -0
  134. package/docs/images/prodero-session-learned-cropped.png +0 -0
  135. package/docs/plugins.md +83 -0
  136. package/docs/ui-themes.md +49 -0
  137. package/instructions/AGENTS.md +39 -0
  138. package/package.json +92 -4
  139. package/plugins/migrate_ai_studio/README.md +101 -0
  140. package/plugins/migrate_ai_studio/index.cjs +396 -0
  141. package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
  142. package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
  143. package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
  144. package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
  145. package/plugins/migrate_ai_studio/plugin.json +11 -0
  146. package/plugins/migrate_ai_studio/schema.cjs +20 -0
  147. package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
  148. package/prodero.release.json +5 -0
@@ -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
@@ -0,0 +1,139 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { adapters, block, replaceBlock, LAUNCHER, GLOBAL_START, GLOBAL_END, MCP_START, MCP_END } from './adapters.js';
5
+ import { environment, expectedEntry, inspect, receiptSchema } from './manager.js';
6
+ import { verifyPublicEntry, resolveNpmCli } from './public-entry.js';
7
+ import { read, fail, transact } from './files.js';
8
+ const stable = (value) => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
9
+ async function packageEntry(host, entry, env) {
10
+ // Accept historical receipt targets only to migrate them to the public executable.
11
+ const cli = entry.args.length === 2 && entry.args[1] === 'mcp' && path.isAbsolute(entry.args[0])
12
+ ? entry.args[0] : path.resolve(path.dirname(entry.command), '../dist/cli.js');
13
+ const adapter = { id: host };
14
+ const result = expectedEntry(adapter, cli, env);
15
+ await verifyPublicEntry(result);
16
+ return result;
17
+ }
18
+ async function readPackage(root) {
19
+ try {
20
+ return JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
21
+ }
22
+ catch {
23
+ return fail('HOST_RUNTIME_MISSING', 'Cannot read the selected runtime package.json. No settings changed.');
24
+ }
25
+ }
26
+ async function developmentEntry(host, checkout, env) {
27
+ if (!path.isAbsolute(checkout))
28
+ fail('HOST_UNSUPPORTED_PATH', '--checkout must be an absolute Prodero repository path.');
29
+ const root = await fs.realpath(checkout).catch(() => fail('HOST_RUNTIME_MISSING', 'Development checkout does not exist.'));
30
+ const metadata = await readPackage(root);
31
+ if (!['@prodero/curator', '@prodero/cli'].includes(metadata.name ?? '') || !metadata.scripts?.['mcp:dev'])
32
+ fail('HOST_RUNTIME_MISSING', 'Select a Prodero checkout with an mcp:dev script.');
33
+ for (const relative of ['src/cli.ts', 'scripts/mcp-dev.ts', 'scripts/mcp-dev-launcher.mjs', 'node_modules/tsx/package.json']) {
34
+ if (!(await fs.stat(path.join(root, relative)).catch(() => null))?.isFile())
35
+ fail('HOST_RUNTIME_MISSING', 'Development source or dependencies are missing. Install checkout dependencies first.');
36
+ }
37
+ await resolveNpmCli(env);
38
+ return { root };
39
+ }
40
+ /** Refuse to silently switch a global registration shadowed by workspace policy. */
41
+ async function checkWorkspace(host, workspace, globalConfig, nativeConfig) {
42
+ if (!path.isAbsolute(workspace))
43
+ fail('HOST_UNSUPPORTED_PATH', '--workspace must be absolute.');
44
+ workspace = await fs.realpath(workspace).catch(() => fail('HOST_SCOPE_CONFLICT', 'The selected workspace does not exist.'));
45
+ if (!(await fs.stat(workspace)).isDirectory())
46
+ fail('HOST_SCOPE_CONFLICT', 'The selected workspace is not a directory.');
47
+ const globalReal = await fs.realpath(globalConfig).catch(() => globalConfig);
48
+ for (let dir = path.resolve(workspace);; dir = path.dirname(dir)) {
49
+ const relatives = host === 'codex' ? ['.codex/config.toml'] : ['.mcp.json', '.claude/settings.json', '.claude/settings.local.json'];
50
+ for (const relative of relatives) {
51
+ const file = path.join(dir, relative);
52
+ if (file === globalConfig || await fs.realpath(file).catch(() => null) === globalReal)
53
+ continue;
54
+ const text = await fs.readFile(file, 'utf8').catch((error) => {
55
+ if (error.code === 'ENOENT')
56
+ return '';
57
+ return fail('HOST_SCOPE_CONFLICT', 'Cannot inspect workspace MCP overrides. No settings changed.');
58
+ });
59
+ if (/prodero/i.test(text))
60
+ fail('HOST_SCOPE_CONFLICT', `Workspace Prodero settings in ${file} require review before switching the user-level connection.`);
61
+ }
62
+ if (dir === path.dirname(dir))
63
+ break;
64
+ }
65
+ if (host === 'claude') {
66
+ const projects = JSON.parse(nativeConfig).projects ?? {};
67
+ for (const [project, settings] of Object.entries(projects)) {
68
+ const relative = path.relative(project, workspace);
69
+ if ((relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) && /prodero/i.test(JSON.stringify(settings))) {
70
+ fail('HOST_SCOPE_CONFLICT', 'Claude project-specific Prodero settings require review before switching the user-level connection.');
71
+ }
72
+ }
73
+ }
74
+ }
75
+ /** Explicit single-host operation. Never discovers a target host or updates the running process. */
76
+ export async function switchRuntime(hostId, mode, options = {}) {
77
+ if (hostId !== 'codex' && hostId !== 'claude')
78
+ fail('HOST_UNKNOWN', 'Choose exactly one host: codex or claude.');
79
+ if (mode !== 'package' && mode !== 'dev')
80
+ fail('HOST_RUNTIME_MODE', 'Choose package or dev.');
81
+ if (options.packageRoot && !path.isAbsolute(options.packageRoot))
82
+ fail('HOST_UNSUPPORTED_PATH', '--package-root must be absolute.');
83
+ const ctx = environment(options);
84
+ const host = (await adapters(ctx.home, ctx.env, hostId))[0];
85
+ const view = await inspect(host, ctx, false, true);
86
+ await checkWorkspace(hostId, options.workspace ?? process.cwd(), host.config, view.text);
87
+ const { receipt } = view;
88
+ const migrating = !receipt && ['externally-configured', 'external-incomplete'].includes(view.base.state);
89
+ if (!receipt && !migrating)
90
+ fail('HOST_NOT_INSTALLED', 'Configure this agent with prodero install before switching runtimes.');
91
+ if (migrating && !options.adoptDev && !options.dryRun)
92
+ fail('HOST_ADOPTION_REQUIRED', 'Preview with --dry-run, then use --adopt-dev to migrate this manual prodero-dev registration.');
93
+ const stored = receipt?.format === 2 ? receipt.runtime : undefined;
94
+ const selectedPackage = options.packageRoot
95
+ ? expectedEntry(host, path.join(options.packageRoot, 'dist/cli.js'))
96
+ : stored?.packageEntry ?? receipt?.entry ?? expectedEntry(host, fileURLToPath(new URL('../cli.js', import.meta.url)));
97
+ const packaged = await packageEntry(hostId, selectedPackage, ctx.env);
98
+ let devRoot = stored?.devRoot ?? null, devEntry = stored?.devEntry ?? null;
99
+ if (mode === 'dev') {
100
+ const checkout = options.checkout ?? devRoot;
101
+ if (!checkout)
102
+ fail('HOST_RUNTIME_MISSING', 'Supply --checkout with the absolute Prodero repository path.');
103
+ const development = await developmentEntry(hostId, checkout, ctx.env);
104
+ devRoot = development.root;
105
+ let explicit = false;
106
+ const other = hostId === 'codex' ? 'claude' : 'codex';
107
+ const saved = await read(ctx.home, path.join(ctx.state, other, 'receipt.json'));
108
+ if (saved !== null) {
109
+ const otherReceipt = receiptSchema.parse(JSON.parse(saved));
110
+ explicit = otherReceipt.format === 2 && Boolean(otherReceipt.runtime.devRoot) && otherReceipt.runtime.devRoot !== devRoot;
111
+ }
112
+ devEntry = { ...packaged, args: ['mcp', '--dev', ...(explicit ? ['--checkout', devRoot] : [])] };
113
+ }
114
+ const entry = mode === 'dev' ? devEntry : packaged;
115
+ const config = host.edit(migrating ? host.removeAlternate(view.text) : view.text, entry);
116
+ const launcher = replaceBlock(view.launcher ?? '', GLOBAL_START, GLOBAL_END, LAUNCHER);
117
+ const next = {
118
+ format: 2, host: hostId, config: host.config, launcherPath: view.launcherPath,
119
+ entry, launcher: LAUNCHER, mcpBlock: hostId === 'codex' ? block(config, MCP_START, MCP_END).text : null,
120
+ originalConfig: receipt ? receipt.originalConfig : view.config,
121
+ originalLauncher: receipt ? receipt.originalLauncher : view.launcher,
122
+ installedConfig: config, installedLauncher: launcher,
123
+ runtime: { mode, packageEntry: packaged, devRoot, devEntry },
124
+ };
125
+ receiptSchema.parse(next);
126
+ host.inspectConfig(config);
127
+ const unchanged = receipt?.format === 2 && stable(receipt.runtime) === stable(next.runtime) && stable(host.entry(view.text)) === stable(entry) && launcher === view.launcher;
128
+ const plan = { host: hostId, mode, previousMode: stored?.mode ?? (migrating ? 'manual-dev' : 'package'), changed: !unchanged, migration: migrating,
129
+ config: host.config, launcher: view.launcherPath, runtimePath: mode === 'dev' ? devRoot : packaged.command, restartRequired: true };
130
+ if (options.dryRun || unchanged)
131
+ return { ...plan, dryRun: Boolean(options.dryRun) };
132
+ await transact(ctx.home, view.dir, [
133
+ { path: host.config, before: view.config, after: config },
134
+ { path: view.launcherPath, before: view.launcher, after: launcher },
135
+ { path: view.receiptPath, before: view.saved, after: JSON.stringify(next, null, 2) + '\n' },
136
+ ]);
137
+ return { ...plan, dryRun: false };
138
+ }
139
+ //# sourceMappingURL=runtime.js.map