@feltdb/core 0.7.3 → 0.8.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 (147) hide show
  1. package/README.md +25 -0
  2. package/dist/application-development.d.ts +78 -0
  3. package/dist/application-development.d.ts.map +1 -0
  4. package/dist/application-development.js +153 -0
  5. package/dist/application-manifest.d.ts +6 -1
  6. package/dist/application-manifest.d.ts.map +1 -1
  7. package/dist/application-manifest.js +10 -3
  8. package/dist/canonical-application.d.ts +27 -0
  9. package/dist/canonical-application.d.ts.map +1 -0
  10. package/dist/canonical-application.js +46 -0
  11. package/dist/cli/ai-model-config.js +97 -0
  12. package/dist/cli/cli.js +1 -1
  13. package/dist/cli/commands.js +664 -120
  14. package/dist/cli/index.js +1 -1
  15. package/dist/cli/managed-environment.js +6 -0
  16. package/dist/cli/managed-project.js +6 -0
  17. package/dist/cli/proposal-client.js +63 -0
  18. package/dist/cli/publish-engine.js +165 -0
  19. package/dist/cli/source-sync.js +183 -0
  20. package/dist/create/cli.js +11 -2
  21. package/dist/create/create.js +38 -16
  22. package/dist/create/managed-account.js +82 -11
  23. package/dist/create/package-versions.js +1 -1
  24. package/dist/create/server-source/crates/feltdb/src/application.rs +41 -1
  25. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
  26. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
  27. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  28. package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
  29. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
  30. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
  31. package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
  32. package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
  33. package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
  34. package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
  35. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +111 -3
  36. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
  37. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
  38. package/dist/create/server-source/crates/feltdb-server/src/main.rs +1960 -143
  39. package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
  40. package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
  41. package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
  42. package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
  43. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +195 -13
  44. package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
  45. package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
  46. package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
  47. package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
  48. package/dist/create/template/default-project/README.md +57 -0
  49. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  50. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  51. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  52. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  53. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  54. package/dist/create/template/default-project/feltdb.flow +174 -0
  55. package/dist/create/template/default-project/src/App.tsx +20 -0
  56. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  57. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  58. package/dist/create/template/default-project/src/index.tsx +14 -0
  59. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  60. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  61. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  62. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  63. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  64. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  65. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  66. package/dist/create/template/default-project/src/styles-application.css +21 -0
  67. package/dist/create/template/default-project/src/styles.css +1 -0
  68. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  69. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  70. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  71. package/dist/db.d.ts +49 -1
  72. package/dist/db.d.ts.map +1 -1
  73. package/dist/db.js +77 -0
  74. package/dist/error-codes.d.ts +20 -1
  75. package/dist/error-codes.d.ts.map +1 -1
  76. package/dist/error-codes.js +25 -0
  77. package/dist/flowspec.d.ts +1 -0
  78. package/dist/flowspec.d.ts.map +1 -1
  79. package/dist/flowspec.js +66 -11
  80. package/dist/http-client.d.ts +135 -14
  81. package/dist/http-client.d.ts.map +1 -1
  82. package/dist/http-client.js +79 -31
  83. package/dist/http-db.d.ts +15 -2
  84. package/dist/http-db.d.ts.map +1 -1
  85. package/dist/http-db.js +113 -5
  86. package/dist/index-core.d.ts +5 -0
  87. package/dist/index-core.d.ts.map +1 -1
  88. package/dist/index-core.js +5 -0
  89. package/dist/module-intent.d.ts +37 -0
  90. package/dist/module-intent.d.ts.map +1 -0
  91. package/dist/module-intent.js +81 -0
  92. package/dist/module.d.ts +76 -0
  93. package/dist/module.d.ts.map +1 -0
  94. package/dist/module.js +61 -0
  95. package/dist/proposal.d.ts +161 -0
  96. package/dist/proposal.d.ts.map +1 -0
  97. package/dist/proposal.js +232 -0
  98. package/dist/studio/app.d.ts +5 -1
  99. package/dist/studio/app.d.ts.map +1 -1
  100. package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
  101. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  102. package/dist/studio/components/AskFeltDB.d.ts +21 -0
  103. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  104. package/dist/studio/components/ContractInspector.d.ts +7 -0
  105. package/dist/studio/components/ContractInspector.d.ts.map +1 -0
  106. package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
  107. package/dist/studio/components/ManagedInstancePanel.js +1 -5
  108. package/dist/studio/components/ModuleExplorer.d.ts +6 -0
  109. package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
  110. package/dist/studio/components/ProposalBrowser.d.ts +12 -0
  111. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  112. package/dist/studio/components/StateExplorer.d.ts +6 -7
  113. package/dist/studio/components/StateExplorer.d.ts.map +1 -1
  114. package/dist/studio/components/index.d.ts +4 -0
  115. package/dist/studio/components/index.d.ts.map +1 -1
  116. package/dist/studio/components/index.js +4 -4
  117. package/dist/studio/components-CNSNrmA9.js +2787 -0
  118. package/dist/studio/index.js +214 -157
  119. package/dist/studio/studio.css +1 -1
  120. package/dist/studio/utils/index.d.ts +2 -0
  121. package/dist/studio/utils/index.d.ts.map +1 -1
  122. package/dist/studio/utils/index.js +14 -12
  123. package/dist/studio/utils/managed-instance.d.ts +0 -4
  124. package/dist/studio/utils/managed-instance.d.ts.map +1 -1
  125. package/dist/studio/utils/managed-instance.js +0 -4
  126. package/dist/studio/utils/proposal-api.d.ts +53 -0
  127. package/dist/studio/utils/proposal-api.d.ts.map +1 -0
  128. package/dist/studio/utils/proposal-api.js +91 -0
  129. package/dist/studio/utils/service-api.d.ts +81 -0
  130. package/dist/studio/utils/service-api.d.ts.map +1 -0
  131. package/dist/studio/utils/service-api.js +51 -0
  132. package/dist/studio-app/assets/dist-CCOk39Uc.js +1 -0
  133. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-CSB6KVgw.js} +1 -1
  134. package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
  135. package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
  136. package/dist/studio-app/assets/{index-C1GWyazR.css → index-yazaWMUN.css} +1 -1
  137. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  138. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  139. package/dist/studio-app/index.html +2 -2
  140. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  141. package/dist/workspace/local-development-authority.d.ts +3 -0
  142. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  143. package/dist/workspace/local-development-authority.js +124 -2
  144. package/package.json +1 -1
  145. package/dist/studio/components-Dxuhrv8_.js +0 -1832
  146. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  147. package/dist/studio-app/assets/index-B5tnmvSD.js +0 -28
@@ -8,12 +8,19 @@ import net from 'net';
8
8
  import { createRequire } from 'module';
9
9
  import { spawn, spawnSync } from 'child_process';
10
10
  import { randomBytes } from 'crypto';
11
- import { createFeltDB, diffFlowSpec, emptyFlowSpec, formatFlowSpec, InvestigationLifecycleManager, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
11
+ import { createInterface } from 'node:readline/promises';
12
+ import { createFeltDB, createModuleImplementationProposal, diffFlowSpec, formatFlowSpec, InvestigationLifecycleManager, listFeltDBModules, parseFlowSpec, planFlowSpecMigration, renderProposalDiagnostic, resolveFeltDBModule, resolveModuleIntent, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
12
13
  import { discoverWorkspace, ensureWorkspaceGitIgnored, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
13
14
  import { applicationUrlCandidate, detectApplication, discoverApplicationUrl } from './application.js';
14
15
  import { resolveApplicationLifecycle, startManagedApplication, waitForManagedApplication } from './application-lifecycle.js';
15
16
  import { createDevelopmentSession, developmentSessionEnvironment, developmentSessionSummary, transitionDevelopmentSession } from './development-session.js';
16
17
  import { openApplication, openStudio } from './browser-opening.js';
18
+ import { publishApplication } from './publish-engine.js';
19
+ import { applyFlowProposalAtomic, assertProposalSourcePlan, describeProposal, loadLocalDevelopmentContract, repositoryContext, reviewFlowProposal, syncGeneratedSource } from './source-sync.js';
20
+ import { configureAiModel, generateConfiguredFlowProposal, publicAiModelConfiguration } from './ai-model-config.js';
21
+ import { proposalService } from './proposal-client.js';
22
+ import { resolveManagedAuthorityEnvironment } from './managed-environment.js';
23
+ import { readManagedProjectState } from './managed-project.js';
17
24
  function loadProjectEnvironment(file = path.resolve('.env.local')) {
18
25
  if (!fs.existsSync(file))
19
26
  return;
@@ -79,11 +86,319 @@ export async function handleCommand(command, args) {
79
86
  return handleFlowDeploy(args);
80
87
  case 'publish':
81
88
  return handleManagedPublish(args);
89
+ case 'ai':
90
+ return handleAi(args);
91
+ case 'proposals':
92
+ return handleProposals(args);
93
+ case 'module':
94
+ case 'modules':
95
+ return handleModules(args);
96
+ case 'sync':
97
+ return handleSourceSync(args);
82
98
  case 'help':
83
99
  default:
84
100
  return handleHelp();
85
101
  }
86
102
  }
103
+ function handleModules(args) {
104
+ const action = args[0] || 'list';
105
+ if (action === 'list') {
106
+ for (const module of listFeltDBModules())
107
+ console.log(`${module.id}\t${module.version}\t${module.compatibility.join(', ')}`);
108
+ return;
109
+ }
110
+ if (action === 'inspect' && args[1]) {
111
+ const flowPath = path.resolve('feltdb.flow');
112
+ const spec = args.includes('--application') && fs.existsSync(flowPath) ? parseFlowSpec(fs.readFileSync(flowPath, 'utf8')) : undefined;
113
+ const declaration = spec?.modules?.find(value => value.name === args[1]);
114
+ const module = resolveFeltDBModule(declaration ?? { name: args[1], provider: args.find((value, index) => index > 1 && !value.startsWith('--')) || 'stripe-compatible' });
115
+ const usage = spec?.collections.flatMap(collection => collection.fields.filter(field => field.type.startsWith(`ref ${module.id}.`)).map(field => `${collection.name}.${field.name} -> ${field.type.slice(4)}`)) ?? [];
116
+ console.log(JSON.stringify({ ...module, application_usage: usage }, null, 2));
117
+ return;
118
+ }
119
+ throw new Error('Usage: feltdb module [list|inspect <id> [provider]]');
120
+ }
121
+ function proposalFile(args) {
122
+ const value = args.find(argument => !argument.startsWith('--') && !['review', 'apply'].includes(argument));
123
+ if (!value)
124
+ throw new Error('A proposal JSON file is required');
125
+ return path.resolve(value);
126
+ }
127
+ function validateRepositoryBuild(args) {
128
+ const packagePath = path.resolve('package.json');
129
+ const projectPackage = fs.existsSync(packagePath) ? JSON.parse(fs.readFileSync(packagePath, 'utf8')) : {};
130
+ if (args.includes('--no-build'))
131
+ return;
132
+ const verification = ['typecheck', 'test', 'build'].filter(name => projectPackage.scripts?.[name]);
133
+ if (!verification.length)
134
+ return;
135
+ console.log('Validating the updated application before recording applied status…');
136
+ for (const name of verification) {
137
+ const checked = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', name], { cwd: process.cwd(), stdio: 'inherit' });
138
+ if (checked.status !== 0)
139
+ throw new Error(`APPLICATION_VALIDATION_FAILED: ${name} exited ${checked.status ?? 'without a status'}; repository restored and proposal remains approved`);
140
+ console.log(`✓ ${name} passed`);
141
+ }
142
+ }
143
+ async function handleAi(args) {
144
+ loadProjectEnvironment();
145
+ if (args[0] === 'model') {
146
+ const action = args[1] || 'show';
147
+ if (action === 'show') {
148
+ const model = publicAiModelConfiguration();
149
+ console.log(`Provider: ${model.provider}\nModel: ${model.model}\nLocation: ${model.local ? 'local browser (private)' : model.baseUrl}\nCredential: ${model.credentialConfigured ? 'configured securely' : 'not configured'}`);
150
+ return;
151
+ }
152
+ if (action === 'set') {
153
+ const option = (name) => args.includes(name) ? args[args.indexOf(name) + 1] : undefined;
154
+ const provider = (option('--provider') || 'local-webllm');
155
+ if (!['local-webllm', 'openai-compatible'].includes(provider))
156
+ throw new Error('Provider must be local-webllm or openai-compatible');
157
+ const modelName = option('--model');
158
+ if (!modelName)
159
+ throw new Error('--model is required');
160
+ const apiKey = args.includes('--api-key-stdin') ? fs.readFileSync(0, 'utf8').trim() : process.env.FELTDB_AI_API_KEY;
161
+ const configured = configureAiModel({ provider, model: modelName, baseUrl: option('--base-url'), apiKey: apiKey || undefined,
162
+ credentialRequired: !args.includes('--no-credential') });
163
+ console.log(`✓ AI model configured: ${configured.provider} / ${configured.model}`);
164
+ console.log(provider === 'openai-compatible' ? 'Credential is stored in the operating-system vault; Studio receives only provider metadata.' : 'Inference and prompts stay in the browser.');
165
+ return;
166
+ }
167
+ throw new Error('Usage: feltdb ai model [show|set --provider <local-webllm|openai-compatible> --model <id> [--base-url <url>] [--api-key-stdin|--no-credential]]');
168
+ }
169
+ if (!args.length || args.every(argument => ['--dry-run', '--preview', '--ephemeral', '--no-open'].includes(argument))) {
170
+ console.log('Opening the local Ask FeltDB experience. Generation stays in your browser via WebGPU.');
171
+ return handleStudio(args, studioUrl => console.log(`Ask FeltDB: ${studioUrl}`));
172
+ }
173
+ const action = args[0];
174
+ if (action === 'proposals')
175
+ return handleProposals(['list', ...args.slice(1)]);
176
+ if (action === 'proposal') {
177
+ const id = args[1];
178
+ if (!id)
179
+ throw new Error('Usage: feltdb ai proposal <id> [--history]');
180
+ return handleProposals(['get', id, ...args.slice(2)]);
181
+ }
182
+ if (action === 'approve') {
183
+ const id = args[1];
184
+ if (!id)
185
+ throw new Error('Usage: feltdb ai approve <id> [--yes]');
186
+ const client = proposalService();
187
+ const proposal = await client.get(id);
188
+ const readiness = await client.status(id);
189
+ if (proposal.status !== 'previewed')
190
+ throw new Error(`Cannot approve Proposal ${id}. Current status: ${proposal.status}. A current preview is required.`);
191
+ if (readiness.blockers.length)
192
+ throw new Error(`Cannot approve Proposal ${id}. ${readiness.blockers.join(', ')}. Regenerate the Proposal before approval.`);
193
+ if (!proposal.preview_id)
194
+ throw new Error(`Cannot approve Proposal ${id}. Preview is missing.`);
195
+ await client.getPreview(proposal.preview_id);
196
+ console.log(renderProposalDiagnostic(proposal, { readiness }).join('\n'));
197
+ console.log('Repository source will NOT be modified by approval.');
198
+ let approved = args.includes('--yes');
199
+ if (!approved && process.stdin.isTTY) {
200
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
201
+ approved = /^y(?:es)?$/i.test(await prompt.question('Approve this proposal? [y/N] '));
202
+ prompt.close();
203
+ }
204
+ if (!approved) {
205
+ console.log('Approval cancelled.');
206
+ return;
207
+ }
208
+ const value = await client.approve(id, 'cli-developer', args.includes('--approve-authorization'));
209
+ console.log(`✓ Proposal ${value.id} approved`);
210
+ return;
211
+ }
212
+ if (action === 'context') {
213
+ const context = await loadLocalDevelopmentContract();
214
+ console.log(JSON.stringify({ snapshot: context.snapshot, flowHash: context.flowHash }, null, 2));
215
+ return;
216
+ }
217
+ if (action === 'review') {
218
+ const input = JSON.parse(fs.readFileSync(proposalFile(args.slice(1)), 'utf8'));
219
+ const proposal = input;
220
+ const review = await reviewFlowProposal(proposal);
221
+ console.log(`\n${proposal.summary || 'FeltDB change proposal'}`);
222
+ for (const line of describeProposal(review))
223
+ console.log(` ${line}`);
224
+ if (!describeProposal(review).length)
225
+ console.log(' No contract changes.');
226
+ return;
227
+ }
228
+ if (action === 'apply') {
229
+ const target = args.slice(1).find(value => !value.startsWith('--'));
230
+ if (!target)
231
+ throw new Error('A proposal id or JSON file is required');
232
+ const stored = target.startsWith('p_') ? await proposalService().get(target) : undefined;
233
+ const proposal = stored ? { baseContractHash: stored.base_contract_hash, baseFlowHash: stored.base_flow_hash,
234
+ proposedFlow: stored.proposed_flow, summary: stored.summary, changes: stored.contract_diff, warnings: stored.warnings }
235
+ : JSON.parse(fs.readFileSync(path.resolve(target), 'utf8'));
236
+ if (stored && stored.status !== 'approved')
237
+ throw new Error(`Cannot apply Proposal ${stored.id}. Current status: ${stored.status}. Approval is required. Run: feltdb ai approve ${stored.id}`);
238
+ if (!stored && !args.includes('--approve'))
239
+ throw new Error('Explicit approval is required: rerun with --approve');
240
+ const current = await loadLocalDevelopmentContract();
241
+ if (stored && (stored.application_id !== current.snapshot.application.application_id || stored.environment !== current.snapshot.application.environment)) {
242
+ throw new Error(`PROPOSAL_SCOPE_MISMATCH: ${stored.application_id}/${stored.environment} cannot apply to ${current.snapshot.application.application_id}/${current.snapshot.application.environment}`);
243
+ }
244
+ const alreadyWritten = stored && await import('@feltdb/core').then(module => module.flowSourceHash(stored.proposed_flow)) === current.flowHash;
245
+ if (alreadyWritten) {
246
+ console.log('Detected an approved proposal whose flow is already present; reconciling the interrupted application.');
247
+ const recoveredReview = await reviewFlowProposal({ ...proposal, baseContractHash: current.snapshot.contract_hash, baseFlowHash: current.flowHash });
248
+ await applyFlowProposalAtomic({ ...proposal, baseContractHash: current.snapshot.contract_hash, baseFlowHash: current.flowHash, proposedFlow: current.source }, process.cwd(), {
249
+ approveAuthorization: true, replaceGenerated: args.includes('--replace-generated'), verify: () => validateRepositoryBuild(args),
250
+ });
251
+ await proposalService().transition(stored.id, 'applied', 'cli-repository-applier', true);
252
+ console.log(`✓ Reconciled and marked ${stored.id} applied`);
253
+ return;
254
+ }
255
+ if (stored) {
256
+ const client = proposalService();
257
+ const readiness = await client.status(stored.id);
258
+ if (readiness.blockers.length)
259
+ throw new Error(`Cannot apply Proposal ${stored.id}. Proposal is stale: ${readiness.blockers.join(', ')}. Proposal contract: ${stored.base_contract_hash}. Current contract: ${readiness.current_contract_hash}. Regenerate the Proposal before applying.`);
260
+ if (!stored.preview_id)
261
+ throw new Error(`Cannot apply Proposal ${stored.id}. A current preview is required.`);
262
+ await client.getPreview(stored.preview_id);
263
+ assertProposalSourcePlan(stored.source_plan);
264
+ const repository = await repositoryContext(process.cwd(), stored.source_plan.files.map(value => value.path));
265
+ if (repository.dirty.length && !args.includes('--allow-dirty'))
266
+ throw new Error(`Cannot apply Proposal ${stored.id}. Repository conflict: ${repository.dirty.join(', ')}. Resolve the conflict and retry.`);
267
+ }
268
+ const reviewOnly = await reviewFlowProposal(proposal);
269
+ console.log(`\n${proposal.summary || 'FeltDB change proposal'}`);
270
+ for (const line of describeProposal(reviewOnly))
271
+ console.log(` ${line}`);
272
+ console.log(` Source impact: feltdb.flow, src/feltdb/generated-contract.ts`);
273
+ if (args.includes('--dry-run') || args.includes('--preview')) {
274
+ console.log('✓ Preview complete. No files, flow, contract, or published state were changed.');
275
+ return;
276
+ }
277
+ let confirmed = args.includes('--yes');
278
+ if (!confirmed && process.stdin.isTTY) {
279
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
280
+ confirmed = /^y(?:es)?$/i.test(await prompt.question(`Apply ${stored?.id || 'this proposal'} to repository source? [y/N] `));
281
+ prompt.close();
282
+ }
283
+ if (!confirmed)
284
+ throw new Error('Apply cancelled. Re-run with --yes for non-interactive application.');
285
+ const review = await applyFlowProposalAtomic(proposal, process.cwd(), {
286
+ allowDirty: args.includes('--allow-dirty'),
287
+ approveAuthorization: args.includes('--approve-authorization'),
288
+ replaceGenerated: args.includes('--replace-generated'),
289
+ verify: () => validateRepositoryBuild(args),
290
+ });
291
+ console.log(`✓ Applied validated flow proposal (${review.proposedContract.hashes.application})`);
292
+ for (const file of review.written)
293
+ console.log(` ${file}`);
294
+ if (stored)
295
+ await proposalService().transition(stored.id, 'applied', 'cli-repository-applier', true);
296
+ return;
297
+ }
298
+ const developerIntent = args.filter(value => !value.startsWith('--')).join(' ').trim();
299
+ if (developerIntent) {
300
+ const resolved = resolveModuleIntent(developerIntent);
301
+ if (resolved) {
302
+ console.log('Understanding application...');
303
+ const current = await loadLocalDevelopmentContract();
304
+ console.log('✓ Application contract loaded');
305
+ const proposal = await createModuleImplementationProposal({ intent: developerIntent, snapshot: current.snapshot, currentFlow: current.source });
306
+ console.log(`✓ ${proposal.implementation?.module.name} ${proposal.implementation?.module.version} resolved`);
307
+ console.log(`✓ Billing owner: ${proposal.implementation?.owner}`);
308
+ const review = await reviewFlowProposal(proposal);
309
+ if (proposal.implementation?.alreadyInstalled) {
310
+ console.log(`${proposal.implementation.module.name} is already installed.`);
311
+ const missing = [...review.diff.added, ...review.diff.changed];
312
+ if (!missing.length) {
313
+ console.log('Current implementation is complete. No proposal created.');
314
+ return;
315
+ }
316
+ console.log('Missing:');
317
+ for (const value of missing)
318
+ console.log(` ${value}`);
319
+ console.log('Creating a completion proposal.');
320
+ }
321
+ console.log('Generating implementation proposal...');
322
+ const client = proposalService();
323
+ const parent = (await client.list({ status: 'applied', limit: 100 })).proposals.find(value => value.metadata?.module === resolved.module);
324
+ let stored = await client.create({ application_id: current.snapshot.application.application_id, environment: current.snapshot.application.environment,
325
+ base_contract_hash: proposal.baseContractHash, base_flow_hash: proposal.baseFlowHash, proposed_flow: proposal.proposedFlow,
326
+ proposal_version: 1, contract_diff: [], source_plan: { files: [] },
327
+ summary: proposal.summary, warnings: proposal.warnings, actor: 'cli-developer', parent_proposal_id: parent?.id, metadata: { created_from: 'module-intent', module: resolved.module, module_version: resolved.version } });
328
+ await client.validate(stored.id, 'feltdb-validator', review.proposedContract.manifest);
329
+ const preview = await client.preview(stored.id, 'cli-developer');
330
+ stored = preview.proposal;
331
+ const readiness = await client.status(stored.id);
332
+ console.log(renderProposalDiagnostic(stored, { readiness, preview: preview }).join('\n'));
333
+ console.log(`Use:\n feltdb ai proposal ${stored.id}`);
334
+ return;
335
+ }
336
+ }
337
+ throw new Error('Usage: feltdb ai ["implement <module>"|context|proposals|proposal <id>|approve <id>|review <proposal.json>|apply <id>]');
338
+ }
339
+ async function handleProposals(args) {
340
+ loadProjectEnvironment();
341
+ const client = proposalService();
342
+ const action = args[0] || 'list';
343
+ const id = args[1];
344
+ if (action === 'list') {
345
+ const option = (name) => args.includes(name) ? args[args.indexOf(name) + 1] : undefined;
346
+ const result = await client.list({ status: option('--status'), environment: option('--environment'), limit: option('--limit') ? Number(option('--limit')) : undefined, cursor: option('--cursor') });
347
+ if (args.includes('--json')) {
348
+ console.log(JSON.stringify(result, null, 2));
349
+ return;
350
+ }
351
+ const { proposals } = result;
352
+ console.log('PROPOSALS');
353
+ for (const value of proposals)
354
+ console.log(`${value.id}\t${value.status.toUpperCase()}\t${value.summary}\t${value.created_at}`);
355
+ if (result.pagination?.nextCursor)
356
+ console.log(`Next: --cursor ${result.pagination.nextCursor}`);
357
+ if (!proposals.length)
358
+ console.log('No proposals.');
359
+ return;
360
+ }
361
+ if (!id)
362
+ throw new Error(`feltdb proposals ${action} requires a proposal id`);
363
+ if (action === 'get') {
364
+ const proposal = await client.get(id);
365
+ const [readiness, history] = await Promise.all([client.status(id), client.history(id)]);
366
+ const preview = proposal.preview_id ? await client.getPreview(proposal.preview_id).catch(() => undefined) : undefined;
367
+ const result = { proposal, readiness, preview, history: history.events };
368
+ if (args.includes('--json'))
369
+ console.log(JSON.stringify(result, null, 2));
370
+ else
371
+ console.log(renderProposalDiagnostic(proposal, { readiness, history: history.events, preview: preview }).join('\n'));
372
+ return;
373
+ }
374
+ if (action === 'diff') {
375
+ const value = await client.get(id);
376
+ console.log(value.contract_diff.join('\n'));
377
+ console.log(JSON.stringify(value.source_plan, null, 2));
378
+ return;
379
+ }
380
+ if (action === 'approve' || action === 'reject') {
381
+ const value = action === 'approve' ? await client.approve(id, 'local-developer', args.includes('--approve-authorization')) : await client.reject(id, 'local-developer');
382
+ console.log(`✓ ${value.id} ${value.status}`);
383
+ return;
384
+ }
385
+ throw new Error('Usage: feltdb proposals [list|get|diff|approve|reject] [id]');
386
+ }
387
+ async function handleSourceSync(args) {
388
+ loadProjectEnvironment();
389
+ const context = await loadLocalDevelopmentContract();
390
+ const proposal = {
391
+ baseContractHash: context.snapshot.contract_hash,
392
+ baseFlowHash: context.flowHash,
393
+ proposedFlow: context.source,
394
+ summary: 'Synchronize generated source with feltdb.flow',
395
+ changes: [], warnings: [],
396
+ };
397
+ const review = await reviewFlowProposal(proposal);
398
+ const result = syncGeneratedSource(review, process.cwd(), { replace: args.includes('--replace-generated') });
399
+ for (const file of result.written)
400
+ console.log(`✓ Synchronized ${file}`);
401
+ }
87
402
  /**
88
403
  * Inspect the durable investigation store.
89
404
  *
@@ -234,83 +549,29 @@ async function handleFlowDeploy(args) {
234
549
  }
235
550
  }
236
551
  async function handleManagedPublish(args) {
237
- loadProjectEnvironment();
238
- const configPath = path.resolve('feltdb.config.json');
239
- if (!fs.existsSync(configPath))
240
- throw new Error('feltdb.config.json is required for publish');
241
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
242
- if (config.runtime !== 'managed')
243
- throw new Error('feltdb publish requires a managed runtime in feltdb.config.json');
244
- const url = process.env.VITE_FELTDB_MANAGED_URL;
245
- const token = process.env.VITE_FELTDB_MANAGED_API_KEY;
246
- const tenantId = process.env.VITE_FELTDB_MANAGED_TENANT_ID;
247
- const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID;
248
- const namespace = process.env.VITE_FELTDB_MANAGED_NAMESPACE || config.namespace;
249
- const environment = process.env.VITE_FELTDB_MANAGED_ENVIRONMENT || 'production';
250
- const missing = [
251
- ['VITE_FELTDB_MANAGED_URL', url],
252
- ['VITE_FELTDB_MANAGED_API_KEY', token],
253
- ['VITE_FELTDB_MANAGED_TENANT_ID', tenantId],
254
- ['VITE_FELTDB_MANAGED_APPLICATION_ID', applicationId],
255
- ['VITE_FELTDB_MANAGED_NAMESPACE', namespace],
256
- ].filter(([, value]) => !value).map(([name]) => name);
257
- if (missing.length)
258
- throw new Error(`Managed publish configuration is incomplete in .env.local: ${missing.join(', ')}`);
259
- const file = flowFile(args);
260
- const spec = readFlowSpec(file);
261
- const diagnostics = validateFlowSpec(spec).filter(value => value.severity === 'error');
262
- if (diagnostics.length)
263
- throw new Error(diagnostics.map(value => value.message).join('; '));
264
- console.log('FeltDB Publish');
265
- console.log(`Application: ${spec.app}`);
266
- console.log(`Environment: ${environment}`);
267
- console.log(`Namespace: ${namespace}`);
268
- console.log(`API: ${url}`);
269
- console.log('✓ Configuration loaded');
270
- if (!args.includes('--no-build')) {
271
- const manifestPath = path.resolve('package.json');
272
- if (!fs.existsSync(manifestPath))
273
- throw new Error('package.json is required to build the application');
274
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
275
- if (!manifest.scripts?.build)
276
- throw new Error('package.json must define a build script for managed publish');
277
- const build = spawnSync('npm', ['run', 'build'], { cwd: process.cwd(), stdio: 'inherit' });
278
- if (build.error)
279
- throw build.error;
280
- if (build.status !== 0)
281
- throw new Error(`Application build failed with exit code ${build.status}`);
282
- }
283
- console.log('✓ Build completed');
284
- console.log('✓ Application validated');
285
- const remote = createFeltDB({ namespace, server: { url: url, token: token } });
286
- try {
287
- const current = await remote.collection('_flow_apps').get(spec.app);
288
- const migration = planFlowSpecMigration(current?.spec ?? emptyFlowSpec(spec.app), spec);
289
- const destructive = migration.filter(value => value.safety === 'destructive');
290
- if (destructive.length && !args.includes('--allow-destructive')) {
291
- throw new Error(`Publish contains destructive changes (${destructive.map(value => value.target).join(', ')}); review them and rerun with --allow-destructive`);
552
+ const valueAfter = (flag) => { const index = args.indexOf(flag); return index >= 0 ? args[index + 1] : undefined; };
553
+ const confirm = async (question) => {
554
+ if (!process.stdin.isTTY)
555
+ return false;
556
+ const readline = await import('node:readline/promises');
557
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
558
+ try {
559
+ return !/^(n|no)$/i.test((await prompt.question(`${question} [Y/n] `)).trim());
292
560
  }
293
- const result = await remote.deployFlowSpec(spec, current?.version ?? 0, args.includes('--allow-destructive'));
294
- const publishState = {
295
- tenantId,
296
- applicationId,
297
- namespace,
298
- environment,
299
- deploymentId: `${applicationId}:${environment}:${result.version}`,
300
- version: result.version,
301
- publishedAt: new Date().toISOString(),
302
- };
303
- fs.mkdirSync(path.resolve('.feltdb'), { recursive: true });
304
- fs.writeFileSync(path.resolve('.feltdb/last-published.json'), `${JSON.stringify(publishState, null, 2)}\n`, { mode: 0o600 });
305
- fs.writeFileSync(path.resolve('.feltdb/last-deployed.flow'), formatFlowSpec(spec));
306
- console.log('✓ Published');
307
- console.log(`Deployment: ${publishState.deploymentId}`);
308
- console.log(`Version: ${result.version}`);
309
- console.log('Your app is live.');
310
- }
311
- finally {
312
- await remote.close();
313
- }
561
+ finally {
562
+ prompt.close();
563
+ }
564
+ };
565
+ await publishApplication({
566
+ environment: valueAfter('--environment'),
567
+ message: valueAfter('--message'),
568
+ dryRun: args.includes('--dry-run'),
569
+ status: args.includes('--status'),
570
+ yes: args.includes('--yes'),
571
+ approveDestructive: args.includes('--approve-destructive-changes') || args.includes('--allow-destructive'),
572
+ noBuild: args.includes('--no-build'),
573
+ confirm,
574
+ });
314
575
  }
315
576
  async function handleServer(args) {
316
577
  const require = createRequire(import.meta.url);
@@ -500,17 +761,41 @@ For more info, visit: https://github.com/rkendel1/feltdb
500
761
  `);
501
762
  }
502
763
  async function handleConnect(args) {
503
- const url = args[0];
504
- if (!url) {
505
- console.error('❌ URL required');
506
- console.error('Usage: feltdb connect <url>\n');
507
- process.exit(1);
508
- }
764
+ loadProjectEnvironment();
765
+ const managed = readManagedProjectState();
766
+ const projectFile = path.resolve('feltdb.config.json');
767
+ const project = fs.existsSync(projectFile) ? JSON.parse(fs.readFileSync(projectFile, 'utf8')) : {};
768
+ const url = (args[0]
769
+ || managed.url
770
+ || process.env.VITE_FELTDB_MANAGED_URL
771
+ || process.env.VITE_FELTDB_URL
772
+ || project.server?.url
773
+ || (project.runtime === 'browser' ? '' : 'http://localhost:7700')).replace(/\/$/, '');
774
+ if (!url)
775
+ throw new Error('This project uses the embedded browser runtime; there is no server endpoint to connect to. Run npm run dev.');
509
776
  console.log('🔗 Connecting to FeltDB Server\n');
510
777
  console.log(`URL: ${url}`);
511
- console.log('Discovering node...');
512
- console.log('Checking capabilities...');
513
778
  console.log('Verifying connection...\n');
779
+ const controller = new AbortController();
780
+ const timeout = setTimeout(() => controller.abort(), 5000);
781
+ let health;
782
+ try {
783
+ const response = await fetch(`${url}/health`, {
784
+ headers: process.env.FELTDB_TOKEN || process.env.VITE_FELTDB_API_KEY
785
+ ? { Authorization: `Bearer ${process.env.FELTDB_TOKEN || process.env.VITE_FELTDB_API_KEY}` }
786
+ : {},
787
+ signal: controller.signal,
788
+ });
789
+ if (!response.ok)
790
+ throw new Error(`HTTP ${response.status}`);
791
+ health = await response.json();
792
+ }
793
+ catch (error) {
794
+ throw new Error(`FeltDB is not running at ${url}. Start it with npm run feltdb:studio (or the project's configured runtime command). ${error instanceof Error ? error.message : error}`);
795
+ }
796
+ finally {
797
+ clearTimeout(timeout);
798
+ }
514
799
  // Store connection info
515
800
  const configDir = path.join(process.cwd(), '.feltdb');
516
801
  if (!fs.existsSync(configDir)) {
@@ -520,15 +805,16 @@ async function handleConnect(args) {
520
805
  fs.writeFileSync(connFile, JSON.stringify({
521
806
  url,
522
807
  connected: new Date().toISOString(),
523
- authenticated: false,
808
+ authenticated: Boolean(process.env.FELTDB_TOKEN || process.env.VITE_FELTDB_API_KEY),
809
+ namespace: managed.namespace || project.namespace || 'default',
810
+ runtime: project.runtime || 'self-hosted',
524
811
  }, null, 2));
525
812
  console.log('✅ Connected to FeltDB\n');
526
813
  console.log('Endpoint: ' + url);
527
- console.log('Namespace: default');
528
- console.log('Authenticated: no');
529
- console.log('Runtime: self-hosted');
530
- console.log('Peers: 0');
531
- console.log('Health: ✓\n');
814
+ console.log(`Namespace: ${managed.namespace || project.namespace || 'default'}`);
815
+ console.log(`Authenticated: ${process.env.FELTDB_TOKEN || process.env.VITE_FELTDB_API_KEY ? 'yes' : 'no'}`);
816
+ console.log(`Runtime: ${project.runtime || 'self-hosted'}`);
817
+ console.log(`Health: ${health.status || 'healthy'}\n`);
532
818
  console.log('Use: feltdb status (to check connection)\n');
533
819
  }
534
820
  async function handleExplain(args) {
@@ -561,20 +847,39 @@ async function handleStatus() {
561
847
  console.log('Fabric: Enabled');
562
848
  console.log('Authentication: Development');
563
849
  console.log('Health: ✓\n');
850
+ await displayContractStatus();
564
851
  return;
565
852
  }
566
853
  const connection = JSON.parse(fs.readFileSync(connFile, 'utf-8'));
567
854
  console.log('Connected to Server\n');
568
855
  console.log(`Endpoint: ${connection.url}`);
569
- console.log('Namespace: default');
856
+ console.log(`Namespace: ${connection.namespace || 'default'}`);
570
857
  console.log(`Authenticated: ${connection.authenticated ? 'yes' : 'no'}`);
571
- console.log('Runtime: self-hosted');
572
- console.log('Storage: durable');
573
- console.log('Peers: 3');
574
- console.log('Capabilities: 8');
575
- console.log('Agents: 2');
576
- console.log('Workflows: 5');
577
- console.log('Health: ✓\n');
858
+ console.log(`Runtime: ${connection.runtime || 'self-hosted'}`);
859
+ try {
860
+ const response = await fetch(`${String(connection.url).replace(/\/$/, '')}/health`, { signal: AbortSignal.timeout(5000) });
861
+ if (!response.ok)
862
+ throw new Error(`HTTP ${response.status}`);
863
+ const health = await response.json();
864
+ console.log(`Health: ${health.status || 'healthy'}\n`);
865
+ }
866
+ catch {
867
+ console.log('Health: unavailable');
868
+ console.log('Start the configured runtime and run feltdb connect again.\n');
869
+ }
870
+ await displayContractStatus();
871
+ }
872
+ async function displayContractStatus() {
873
+ if (!fs.existsSync(path.resolve('feltdb.flow')))
874
+ return;
875
+ loadProjectEnvironment();
876
+ const current = await loadLocalDevelopmentContract();
877
+ const publishedFile = path.resolve('.feltdb/last-published.json');
878
+ const published = fs.existsSync(publishedFile) ? JSON.parse(fs.readFileSync(publishedFile, 'utf8')) : undefined;
879
+ console.log('Application contract');
880
+ console.log(`Source: ${current.snapshot.contract_hash}`);
881
+ console.log(`Published: ${published?.contractHash || 'unknown'}`);
882
+ console.log(`Drift: ${!published?.contractHash ? 'unknown' : published.contractHash === current.snapshot.contract_hash ? 'synchronized' : 'source differs from published'}\n`);
578
883
  }
579
884
  async function handleWorkspace(args) {
580
885
  const subcommand = args[0] || 'status';
@@ -622,7 +927,7 @@ async function handleWorkspaceConnect(args) {
622
927
  console.log(' (when @feltdb/client workspace APIs are available)\n');
623
928
  }
624
929
  async function handleDev(args) {
625
- var _a, _b, _c;
930
+ var _a;
626
931
  const projectDir = process.cwd();
627
932
  if (args.includes('--help')) {
628
933
  console.log('Usage: feltdb dev [--app-url URL] [--studio-port 7701] [--authority-port 7700] [--discovery-port 7799] [--no-open]');
@@ -675,14 +980,10 @@ async function handleDev(args) {
675
980
  ? { ...configDefaults, ...JSON.parse(fs.readFileSync(configPath, 'utf-8')) }
676
981
  : configDefaults;
677
982
  if (config.runtime === 'managed') {
678
- (_a = process.env).VITE_FELTDB_URL || (_a.VITE_FELTDB_URL = process.env.VITE_FELTDB_MANAGED_URL);
679
- (_b = process.env).VITE_FELTDB_API_KEY || (_b.VITE_FELTDB_API_KEY = process.env.VITE_FELTDB_MANAGED_API_KEY);
680
- }
681
- if (config.runtime === 'managed' && (!process.env.VITE_FELTDB_URL || !process.env.VITE_FELTDB_API_KEY)) {
682
- throw new Error('Managed runtime requires VITE_FELTDB_MANAGED_URL and VITE_FELTDB_MANAGED_API_KEY in .env.local. Re-run managed setup or add your managed connection.');
983
+ resolveManagedAuthorityEnvironment(process.env);
683
984
  }
684
985
  const runtimeNamespace = config.runtime === 'managed'
685
- ? process.env.VITE_FELTDB_MANAGED_NAMESPACE || config.namespace
986
+ ? readManagedProjectState(projectDir).namespace || config.namespace
686
987
  : config.namespace;
687
988
  const applicationLifecycle = resolveApplicationLifecycle(config);
688
989
  // Application URL precedence:
@@ -771,15 +1072,21 @@ async function handleDev(args) {
771
1072
  if (compose.status !== 0)
772
1073
  throw new Error(`docker compose failed with status ${compose.status ?? 'unknown'}`);
773
1074
  selfHostedStarted = true;
774
- (_c = process.env).VITE_FELTDB_URL || (_c.VITE_FELTDB_URL = 'http://127.0.0.1:7700');
1075
+ (_a = process.env).VITE_FELTDB_URL || (_a.VITE_FELTDB_URL = 'http://127.0.0.1:7700');
775
1076
  }
776
1077
  if (!process.env.VITE_FELTDB_URL) {
777
1078
  const authorityPort = Number(args.includes('--authority-port') ? args[args.indexOf('--authority-port') + 1] || '7700' : '7700');
778
1079
  localAuthority = await startLocalDevelopmentAuthority({
779
- dataDir: path.join(projectDir, '.feltdb', 'authority'),
1080
+ dataDir: path.join(projectDir, '.feltdb-data', 'authority'),
780
1081
  port: authorityPort,
1082
+ proposalContext: () => loadLocalDevelopmentContract(projectDir),
781
1083
  });
782
1084
  process.env.VITE_FELTDB_URL = localAuthority.endpoint;
1085
+ const connectionsPath = path.join(projectDir, '.feltdb', 'connections.json');
1086
+ const connections = fs.existsSync(connectionsPath) ? JSON.parse(fs.readFileSync(connectionsPath, 'utf8')) : {};
1087
+ fs.writeFileSync(connectionsPath, `${JSON.stringify({ ...connections, developmentAuthority: {
1088
+ endpoint: localAuthority.endpoint, proposalAuthorityToken: localAuthority.proposalAuthorityToken,
1089
+ } }, null, 2)}\n`, { mode: 0o600 });
783
1090
  }
784
1091
  const authorityUrl = process.env.VITE_FELTDB_URL;
785
1092
  if (!authorityUrl)
@@ -1239,10 +1546,11 @@ async function handleStudio(args, onReady) {
1239
1546
  loadProjectEnvironment();
1240
1547
  const projectConfigPath = path.resolve('feltdb.config.json');
1241
1548
  const projectConfig = fs.existsSync(projectConfigPath) ? JSON.parse(fs.readFileSync(projectConfigPath, 'utf8')) : {};
1549
+ const managedProject = readManagedProjectState();
1242
1550
  const projectIsManaged = projectConfig.runtime === 'managed';
1243
1551
  const connectUrl = args.includes('--connect')
1244
1552
  ? args[args.indexOf('--connect') + 1]
1245
- : projectIsManaged ? process.env.VITE_FELTDB_MANAGED_URL : undefined;
1553
+ : projectIsManaged ? process.env.VITE_FELTDB_MANAGED_URL || managedProject.url : undefined;
1246
1554
  const port = args.includes('--port')
1247
1555
  ? args[args.indexOf('--port') + 1] || '7701'
1248
1556
  : '7701';
@@ -1252,10 +1560,16 @@ async function handleStudio(args, onReady) {
1252
1560
  const open = !args.includes('--no-open');
1253
1561
  const namespace = args.includes('--namespace')
1254
1562
  ? args[args.indexOf('--namespace') + 1] || 'default'
1255
- : projectIsManaged ? process.env.VITE_FELTDB_MANAGED_NAMESPACE || projectConfig.namespace || 'default' : 'default';
1563
+ : projectIsManaged ? managedProject.namespace || projectConfig.namespace || 'default' : 'default';
1256
1564
  const runtime = args.includes('--runtime')
1257
1565
  ? args[args.indexOf('--runtime') + 1] || 'browser'
1258
1566
  : projectIsManaged ? 'managed' : connectUrl ? 'remote' : 'browser';
1567
+ const applicationId = args.includes('--application')
1568
+ ? args[args.indexOf('--application') + 1] || ''
1569
+ : process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || managedProject.applicationId || '';
1570
+ const environment = args.includes('--environment')
1571
+ ? args[args.indexOf('--environment') + 1] || 'production'
1572
+ : managedProject.environment || 'production';
1259
1573
  const appUrl = args.includes('--app-url')
1260
1574
  ? args[args.indexOf('--app-url') + 1]
1261
1575
  : undefined;
@@ -1286,9 +1600,13 @@ async function handleStudio(args, onReady) {
1286
1600
  console.log('⚠️ Studio application artifacts not found.');
1287
1601
  return;
1288
1602
  }
1289
- const projectFlow = ['feltdb.flow', ...fs.readdirSync(process.cwd()).filter(value => value.endsWith('.flow'))]
1290
- .map(value => path.resolve(process.cwd(), value))
1291
- .find((value, index, values) => values.indexOf(value) === index && fs.existsSync(value));
1603
+ const canonicalFlow = path.resolve(process.cwd(), 'feltdb.flow');
1604
+ const projectFlow = fs.existsSync(canonicalFlow) ? canonicalFlow : undefined;
1605
+ const studioMutationToken = randomBytes(24).toString('hex');
1606
+ const projectPackage = fs.existsSync(path.resolve('package.json')) ? JSON.parse(fs.readFileSync(path.resolve('package.json'), 'utf8')) : {};
1607
+ const webllmEnabled = Boolean(projectPackage.dependencies?.['@feltdb/webllm'] || projectPackage.devDependencies?.['@feltdb/webllm']);
1608
+ const previewOnly = args.includes('--ephemeral');
1609
+ const proposalClient = proposalService(process.cwd(), session.authorityUrl);
1292
1610
  const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.json': 'application/json' };
1293
1611
  const server = http.createServer(async (request, response) => {
1294
1612
  const pathname = decodeURIComponent(new URL(request.url || '/', 'http://localhost').pathname);
@@ -1297,14 +1615,210 @@ async function handleStudio(args, onReady) {
1297
1615
  response.setHeader('Cache-Control', 'no-store');
1298
1616
  response.setHeader('Pragma', 'no-cache');
1299
1617
  const managedPublish = runtime === 'managed' ? {
1300
- url: process.env.VITE_FELTDB_MANAGED_URL || connectUrl || '',
1301
- namespace: process.env.VITE_FELTDB_MANAGED_NAMESPACE || namespace,
1302
- token: process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY || '',
1303
- tenantId: process.env.VITE_FELTDB_MANAGED_TENANT_ID || '',
1304
- applicationId: process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || '',
1305
- environment: process.env.VITE_FELTDB_MANAGED_ENVIRONMENT || 'production',
1618
+ url: connectUrl || managedProject.url || '',
1619
+ namespace: managedProject.namespace || namespace,
1620
+ tenantId: managedProject.tenantId || '',
1621
+ applicationId: process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || managedProject.applicationId || '',
1622
+ environment: managedProject.environment || 'production',
1623
+ publishEndpoint: '/_feltdb/publish',
1306
1624
  } : undefined;
1307
- response.end(JSON.stringify({ token: connectUrl ? process.env.VITE_FELTDB_API_KEY || process.env.VITE_FELTDB_MANAGED_API_KEY || '' : '', managedPublish, session }));
1625
+ response.end(JSON.stringify({ token: connectUrl ? process.env.VITE_FELTDB_API_KEY || '' : '', managedPublish, session, sourceSync: { enabled: true, webllmEnabled, previewOnly, token: studioMutationToken, aiModel: publicAiModelConfiguration() } }));
1626
+ return;
1627
+ }
1628
+ if (pathname === '/_feltdb/contract') {
1629
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1630
+ response.setHeader('Cache-Control', 'no-store');
1631
+ try {
1632
+ const current = await loadLocalDevelopmentContract();
1633
+ const lastPublished = fs.existsSync(path.resolve('.feltdb/last-published.json'))
1634
+ ? JSON.parse(fs.readFileSync(path.resolve('.feltdb/last-published.json'), 'utf8')) : undefined;
1635
+ let runningContract;
1636
+ let runningSnapshot;
1637
+ const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || managedProject.applicationId || lastPublished?.applicationId;
1638
+ if (connectUrl && applicationId) {
1639
+ try {
1640
+ const endpoint = new URL('/v1/application', connectUrl);
1641
+ endpoint.searchParams.set('application_id', applicationId);
1642
+ endpoint.searchParams.set('environment', managedProject.environment || lastPublished?.environment || 'production');
1643
+ const headers = {};
1644
+ const credential = process.env.FELTDB_MANAGED_CONTROL_API_KEY || process.env.VITE_FELTDB_API_KEY;
1645
+ if (credential)
1646
+ headers.authorization = `Bearer ${credential}`;
1647
+ const discovered = await fetch(endpoint, { headers });
1648
+ if (discovered.ok) {
1649
+ const application = await discovered.json();
1650
+ runningSnapshot = application.snapshot;
1651
+ runningContract = runningSnapshot?.contract_hash ?? application.contract_hash;
1652
+ }
1653
+ }
1654
+ catch { /* Drift remains explicitly unknown while the runtime is unavailable. */ }
1655
+ }
1656
+ const knownContracts = [current.snapshot.contract_hash, runningContract, lastPublished?.contractHash].filter(Boolean);
1657
+ response.end(JSON.stringify({ snapshot: runningSnapshot ?? current.snapshot, flowHash: current.flowHash, source: current.source,
1658
+ drift: { source: current.snapshot.contract_hash, running: runningContract, published: lastPublished?.contractHash,
1659
+ synchronized: new Set(knownContracts).size <= 1 } }));
1660
+ }
1661
+ catch (error) {
1662
+ response.statusCode = 400;
1663
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1664
+ }
1665
+ return;
1666
+ }
1667
+ if (pathname === '/_feltdb/repository-readiness' && request.method === 'GET') {
1668
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1669
+ response.setHeader('Cache-Control', 'no-store');
1670
+ try {
1671
+ const proposalId = new URL(request.url || '/', 'http://localhost').searchParams.get('proposal');
1672
+ if (!proposalId)
1673
+ throw new Error('proposal is required');
1674
+ const proposal = await proposalClient.get(proposalId);
1675
+ assertProposalSourcePlan(proposal.source_plan);
1676
+ const local = await loadLocalDevelopmentContract();
1677
+ const repository = await repositoryContext(process.cwd(), proposal.source_plan.files.map(value => value.path));
1678
+ const contractMatches = local.snapshot.contract_hash === proposal.base_contract_hash;
1679
+ const flowMatches = local.flowHash === proposal.base_flow_hash;
1680
+ response.end(JSON.stringify({ ready: contractMatches && flowMatches && repository.dirty.length === 0,
1681
+ contractMatches, flowMatches, gitClean: repository.dirty.length === 0, conflicts: repository.dirty,
1682
+ branch: repository.branch, commit: repository.commit }));
1683
+ }
1684
+ catch (error) {
1685
+ response.statusCode = 409;
1686
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1687
+ }
1688
+ return;
1689
+ }
1690
+ if (pathname === '/_feltdb/proposals/list' && request.method === 'GET') {
1691
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1692
+ response.setHeader('Cache-Control', 'no-store');
1693
+ try {
1694
+ response.end(JSON.stringify(await proposalClient.list()));
1695
+ }
1696
+ catch (error) {
1697
+ response.statusCode = 503;
1698
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1699
+ }
1700
+ return;
1701
+ }
1702
+ const proposalDetail = pathname.match(/^\/_feltdb\/proposals\/(p_[A-Za-z0-9]+)$/);
1703
+ if (proposalDetail && request.method === 'GET') {
1704
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1705
+ response.setHeader('Cache-Control', 'no-store');
1706
+ try {
1707
+ response.end(JSON.stringify({ proposal: await proposalClient.get(proposalDetail[1]), history: (await proposalClient.history(proposalDetail[1])).events }));
1708
+ }
1709
+ catch (error) {
1710
+ response.statusCode = 404;
1711
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1712
+ }
1713
+ return;
1714
+ }
1715
+ // Studio proposes and previews. CLI applies to the repository. FeltDB validates
1716
+ // and stores the proposal as application state. Human approves. Nothing else writes source.
1717
+ if (pathname === '/_feltdb/proposals' || pathname === '/_feltdb/proposals/approve' || pathname === '/_feltdb/proposals/reject') {
1718
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1719
+ response.setHeader('Cache-Control', 'no-store');
1720
+ if (request.method !== 'POST') {
1721
+ response.statusCode = 405;
1722
+ response.end(JSON.stringify({ error: 'POST required' }));
1723
+ return;
1724
+ }
1725
+ if (request.headers['x-feltdb-studio-token'] !== studioMutationToken) {
1726
+ response.statusCode = 403;
1727
+ response.end(JSON.stringify({ error: 'Invalid Studio source-sync token' }));
1728
+ return;
1729
+ }
1730
+ try {
1731
+ const chunks = [];
1732
+ for await (const chunk of request)
1733
+ chunks.push(Buffer.from(chunk));
1734
+ const input = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {};
1735
+ if (pathname.endsWith('/approve') || pathname.endsWith('/reject')) {
1736
+ if (previewOnly)
1737
+ throw new Error('Preview-only sessions cannot approve or reject persisted proposals');
1738
+ if (input.confirm !== true)
1739
+ throw new Error('Explicit proposal confirmation is required');
1740
+ const proposal = await proposalClient.transition(String(input.proposalId || ''), pathname.endsWith('/approve') ? 'approved' : 'rejected', 'studio-developer', false, input.approveAuthorization === true);
1741
+ response.end(JSON.stringify({ status: proposal.status, proposalId: proposal.id,
1742
+ command: proposal.status === 'approved' ? `feltdb ai apply ${proposal.id}` : undefined }));
1743
+ }
1744
+ else {
1745
+ const proposal = input.proposal;
1746
+ const review = await reviewFlowProposal(proposal);
1747
+ const changes = describeProposal(review);
1748
+ let stored;
1749
+ if (!previewOnly) {
1750
+ const current = await loadLocalDevelopmentContract();
1751
+ stored = await proposalClient.create({ application_id: current.snapshot.application.application_id,
1752
+ environment: current.snapshot.application.environment, base_contract_hash: proposal.baseContractHash,
1753
+ base_flow_hash: proposal.baseFlowHash, proposed_flow: proposal.proposedFlow, summary: proposal.summary,
1754
+ proposal_version: 1, contract_diff: [], source_plan: { files: [] },
1755
+ warnings: proposal.warnings, actor: 'studio-developer', metadata: { model: publicAiModelConfiguration().model, created_from: 'studio' } });
1756
+ await proposalClient.validate(stored.id, 'feltdb-validator', review.proposedContract.manifest);
1757
+ stored = (await proposalClient.preview(stored.id, 'studio-developer')).proposal;
1758
+ }
1759
+ response.end(JSON.stringify({ status: 'previewed', proposalId: stored?.id ?? 'ephemeral', changes,
1760
+ sourcePlan: stored?.source_plan.files.map(value => value.path) ?? ['feltdb.flow', 'src/feltdb/generated-contract.ts'], previewOnly,
1761
+ authorizationChanges: review.authorizationChanges, migration: review.migration,
1762
+ contractHash: review.proposedContract.hashes.application }));
1763
+ }
1764
+ }
1765
+ catch (error) {
1766
+ response.statusCode = 400;
1767
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1768
+ }
1769
+ return;
1770
+ }
1771
+ if (pathname === '/_feltdb/ai/generate') {
1772
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1773
+ response.setHeader('Cache-Control', 'no-store');
1774
+ if (request.method !== 'POST') {
1775
+ response.statusCode = 405;
1776
+ response.end(JSON.stringify({ error: 'POST required' }));
1777
+ return;
1778
+ }
1779
+ if (request.headers['x-feltdb-studio-token'] !== studioMutationToken) {
1780
+ response.statusCode = 403;
1781
+ response.end(JSON.stringify({ error: 'Invalid Studio token' }));
1782
+ return;
1783
+ }
1784
+ try {
1785
+ const chunks = [];
1786
+ for await (const chunk of request)
1787
+ chunks.push(Buffer.from(chunk));
1788
+ const input = JSON.parse(Buffer.concat(chunks).toString('utf8'));
1789
+ if (!input.intent?.trim())
1790
+ throw new Error('An AI change request is required');
1791
+ const current = await loadLocalDevelopmentContract();
1792
+ const proposal = await generateConfiguredFlowProposal({ intent: input.intent, snapshot: current.snapshot, currentFlow: current.source });
1793
+ response.end(JSON.stringify({ proposal }));
1794
+ }
1795
+ catch (error) {
1796
+ response.statusCode = 400;
1797
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1798
+ }
1799
+ return;
1800
+ }
1801
+ if (pathname === '/_feltdb/publish') {
1802
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1803
+ response.setHeader('Cache-Control', 'no-store');
1804
+ if (request.method !== 'POST') {
1805
+ response.statusCode = 405;
1806
+ response.end(JSON.stringify({ error: 'POST required' }));
1807
+ return;
1808
+ }
1809
+ try {
1810
+ const chunks = [];
1811
+ for await (const chunk of request)
1812
+ chunks.push(Buffer.from(chunk));
1813
+ const input = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {};
1814
+ const messages = [];
1815
+ const result = await publishApplication({ dryRun: Boolean(input.dryRun), status: Boolean(input.status), yes: true, noBuild: true, environment: input.environment, approveDestructive: Boolean(input.approveDestructive), message: input.message, log: message => messages.push(message) });
1816
+ response.end(JSON.stringify({ status: result.status, messages, version: result.revision?.revision_number, validation: result.validation, changes: result.diff?.changes, securityChanges: result.diff?.security_changes }));
1817
+ }
1818
+ catch (error) {
1819
+ response.statusCode = 400;
1820
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1821
+ }
1308
1822
  return;
1309
1823
  }
1310
1824
  if (pathname === '/_feltdb/project') {
@@ -1375,6 +1889,9 @@ async function handleStudio(args, onReady) {
1375
1889
  const parameters = new URLSearchParams({ namespace, runtime });
1376
1890
  if (connectUrl)
1377
1891
  parameters.set('connect', connectUrl);
1892
+ if (applicationId)
1893
+ parameters.set('application_id', applicationId);
1894
+ parameters.set('environment', environment);
1378
1895
  if (appUrl)
1379
1896
  parameters.set('app', appUrl);
1380
1897
  if (session.workspaceId)
@@ -1406,6 +1923,24 @@ Commands:
1406
1923
  diff [file] Diff FlowSpec against the last deployed model
1407
1924
  deploy [file] Validate, version, and deploy FlowSpec
1408
1925
  publish [file] Build and publish/update a managed application
1926
+ ai Open the canonical interactive local-AI workflow
1927
+ ai --dry-run Open an ephemeral preview-only AI session
1928
+ ai model show Show the selected local or user-provided model
1929
+ ai model set Select WebLLM or an OpenAI-compatible model
1930
+ ai context Show the secret-free contract grounding snapshot
1931
+ ai proposals List bounded proposals [--status/--environment/--json]
1932
+ ai proposal <id> Show canonical proposal, readiness, preview, and history
1933
+ ai approve <id> Approve a current preview [--yes]
1934
+ ai review <file> Validate and review a WebLLM flow proposal
1935
+ ai apply <id> Atomically apply an approved proposal [--yes]
1936
+ proposals List durable FeltDB application proposals
1937
+ module list List deterministic FeltDB modules
1938
+ module inspect <id> Inspect a resolved module semantic contract
1939
+ proposals get <id> Show a proposal and its lifecycle history
1940
+ proposals diff <id> Show contract and structured source impact
1941
+ proposals approve <id> Approve a previewed proposal
1942
+ proposals reject <id> Reject a proposed/validated/previewed proposal
1943
+ sync Regenerate owned source from feltdb.flow
1409
1944
  studio Launch FeltDB Studio (developer interface)
1410
1945
  server Start self-hosted FeltDB server
1411
1946
  keys Manage API keys
@@ -1423,6 +1958,7 @@ Commands:
1423
1958
  Studio:
1424
1959
  feltdb studio Launch local Studio
1425
1960
  feltdb studio --connect <url> Connect to remote instance
1961
+ --application <id> --environment <name> Select one application environment
1426
1962
  feltdb studio --port 7701 --no-open Start on port 7701 without opening browser
1427
1963
 
1428
1964
  Server Options:
@@ -1444,6 +1980,13 @@ Connection:
1444
1980
  doctor Check system health
1445
1981
  help Show this help message
1446
1982
 
1983
+ Publish:
1984
+ feltdb publish --dry-run Validate and preview without activation
1985
+ feltdb publish --yes Publish to the configured environment
1986
+ feltdb publish --status Compare local and active cloud versions
1987
+ feltdb publish --environment staging Override the configured environment
1988
+ feltdb publish --approve-destructive-changes --yes
1989
+
1447
1990
  Inspect Sub-commands:
1448
1991
  feltdb inspect Show runtime overview
1449
1992
  feltdb inspect peers Show connected peers
@@ -1467,6 +2010,7 @@ Examples:
1467
2010
  feltdb status
1468
2011
  feltdb dev
1469
2012
  feltdb build
2013
+ feltdb publish --dry-run
1470
2014
  feltdb check
1471
2015
  feltdb inspect
1472
2016
  feltdb inspect peers