@feltdb/core 0.7.2 → 0.7.4

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 (96) hide show
  1. package/dist/application-development.d.ts +76 -0
  2. package/dist/application-development.d.ts.map +1 -0
  3. package/dist/application-development.js +137 -0
  4. package/dist/application-manifest.d.ts +5 -1
  5. package/dist/application-manifest.d.ts.map +1 -1
  6. package/dist/application-manifest.js +10 -3
  7. package/dist/canonical-application.d.ts +26 -0
  8. package/dist/canonical-application.d.ts.map +1 -0
  9. package/dist/canonical-application.js +45 -0
  10. package/dist/cli/ai-model-config.js +97 -0
  11. package/dist/cli/commands.js +485 -30
  12. package/dist/cli/index.js +1 -1
  13. package/dist/cli/proposal-client.js +54 -0
  14. package/dist/cli/publish-engine.js +162 -0
  15. package/dist/cli/source-sync.js +139 -0
  16. package/dist/create/cli.js +1 -1
  17. package/dist/create/create.js +30 -6
  18. package/dist/create/managed-account.js +14 -7
  19. package/dist/create/package-versions.js +1 -1
  20. package/dist/create/server-source/crates/feltdb/src/application.rs +31 -1
  21. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +847 -0
  22. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +338 -29
  23. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  24. package/dist/create/server-source/crates/feltdb/src/lib.rs +1 -0
  25. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +46 -3
  26. package/dist/create/server-source/crates/feltdb-server/src/main.rs +315 -16
  27. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +20 -0
  28. package/dist/create/template/default-project/README.md +57 -0
  29. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  30. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  31. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  32. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  33. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  34. package/dist/create/template/default-project/feltdb.flow +169 -0
  35. package/dist/create/template/default-project/src/App.tsx +20 -0
  36. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  37. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  38. package/dist/create/template/default-project/src/index.tsx +14 -0
  39. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  40. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  41. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  42. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  43. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  44. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  45. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  46. package/dist/create/template/default-project/src/styles-application.css +21 -0
  47. package/dist/create/template/default-project/src/styles.css +1 -0
  48. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  49. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  50. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  51. package/dist/db.d.ts +49 -1
  52. package/dist/db.d.ts.map +1 -1
  53. package/dist/db.js +77 -0
  54. package/dist/error-codes.d.ts +20 -1
  55. package/dist/error-codes.d.ts.map +1 -1
  56. package/dist/error-codes.js +25 -0
  57. package/dist/flowspec.d.ts.map +1 -1
  58. package/dist/flowspec.js +27 -6
  59. package/dist/http-db.d.ts +15 -2
  60. package/dist/http-db.d.ts.map +1 -1
  61. package/dist/http-db.js +113 -5
  62. package/dist/index-core.d.ts +3 -0
  63. package/dist/index-core.d.ts.map +1 -1
  64. package/dist/index-core.js +3 -0
  65. package/dist/proposal.d.ts +85 -0
  66. package/dist/proposal.d.ts.map +1 -0
  67. package/dist/proposal.js +105 -0
  68. package/dist/studio/app.d.ts +5 -1
  69. package/dist/studio/app.d.ts.map +1 -1
  70. package/dist/studio/components/ApplicationDesigner.d.ts +10 -1
  71. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  72. package/dist/studio/components/AskFeltDB.d.ts +20 -0
  73. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  74. package/dist/studio/components/ProposalBrowser.d.ts +3 -0
  75. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  76. package/dist/studio/components/index.d.ts +2 -0
  77. package/dist/studio/components/index.d.ts.map +1 -1
  78. package/dist/studio/components/index.js +4 -4
  79. package/dist/studio/{components-C5p2TfIU.js → components-Bm4sjhcX.js} +531 -192
  80. package/dist/studio/index.js +184 -172
  81. package/dist/studio/studio.css +1 -1
  82. package/dist/studio-app/assets/dist-DA1MYSzp.js +1 -0
  83. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-B-Ccgccb.js} +1 -1
  84. package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
  85. package/dist/studio-app/assets/{index-C1GWyazR.css → index-BRrmvaKw.css} +1 -1
  86. package/dist/studio-app/assets/index-CEIqowCh.js +42 -0
  87. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  88. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  89. package/dist/studio-app/index.html +2 -2
  90. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  91. package/dist/workspace/local-development-authority.d.ts +3 -0
  92. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  93. package/dist/workspace/local-development-authority.js +62 -1
  94. package/package.json +1 -1
  95. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  96. package/dist/studio-app/assets/index-C71X92EK.js +0 -28
@@ -14,6 +14,10 @@ import { applicationUrlCandidate, detectApplication, discoverApplicationUrl } fr
14
14
  import { resolveApplicationLifecycle, startManagedApplication, waitForManagedApplication } from './application-lifecycle.js';
15
15
  import { createDevelopmentSession, developmentSessionEnvironment, developmentSessionSummary, transitionDevelopmentSession } from './development-session.js';
16
16
  import { openApplication, openStudio } from './browser-opening.js';
17
+ import { publishApplication } from './publish-engine.js';
18
+ import { applyFlowProposal, describeProposal, loadLocalDevelopmentContract, reviewFlowProposal, syncGeneratedSource } from './source-sync.js';
19
+ import { configureAiModel, generateConfiguredFlowProposal, publicAiModelConfiguration } from './ai-model-config.js';
20
+ import { proposalService } from './proposal-client.js';
17
21
  function loadProjectEnvironment(file = path.resolve('.env.local')) {
18
22
  if (!fs.existsSync(file))
19
23
  return;
@@ -77,11 +81,179 @@ export async function handleCommand(command, args) {
77
81
  return handleFlowDiff(args);
78
82
  case 'deploy':
79
83
  return handleFlowDeploy(args);
84
+ case 'publish':
85
+ return handleManagedPublish(args);
86
+ case 'ai':
87
+ return handleAi(args);
88
+ case 'proposals':
89
+ return handleProposals(args);
90
+ case 'sync':
91
+ return handleSourceSync(args);
80
92
  case 'help':
81
93
  default:
82
94
  return handleHelp();
83
95
  }
84
96
  }
97
+ function proposalFile(args) {
98
+ const value = args.find(argument => !argument.startsWith('--') && !['review', 'apply'].includes(argument));
99
+ if (!value)
100
+ throw new Error('A proposal JSON file is required');
101
+ return path.resolve(value);
102
+ }
103
+ function validateRepositoryBuild(args) {
104
+ const packagePath = path.resolve('package.json');
105
+ const projectPackage = fs.existsSync(packagePath) ? JSON.parse(fs.readFileSync(packagePath, 'utf8')) : {};
106
+ if (!projectPackage.scripts?.build || args.includes('--no-build'))
107
+ return;
108
+ console.log('Validating the updated application before recording applied status…');
109
+ const built = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], { cwd: process.cwd(), stdio: 'inherit' });
110
+ if (built.status !== 0)
111
+ throw new Error(`APPLICATION_VALIDATION_FAILED: build exited ${built.status ?? 'without a status'}; proposal remains approved for recovery`);
112
+ }
113
+ async function handleAi(args) {
114
+ loadProjectEnvironment();
115
+ if (args[0] === 'model') {
116
+ const action = args[1] || 'show';
117
+ if (action === 'show') {
118
+ const model = publicAiModelConfiguration();
119
+ console.log(`Provider: ${model.provider}\nModel: ${model.model}\nLocation: ${model.local ? 'local browser (private)' : model.baseUrl}\nCredential: ${model.credentialConfigured ? 'configured securely' : 'not configured'}`);
120
+ return;
121
+ }
122
+ if (action === 'set') {
123
+ const option = (name) => args.includes(name) ? args[args.indexOf(name) + 1] : undefined;
124
+ const provider = (option('--provider') || 'local-webllm');
125
+ if (!['local-webllm', 'openai-compatible'].includes(provider))
126
+ throw new Error('Provider must be local-webllm or openai-compatible');
127
+ const modelName = option('--model');
128
+ if (!modelName)
129
+ throw new Error('--model is required');
130
+ const apiKey = args.includes('--api-key-stdin') ? fs.readFileSync(0, 'utf8').trim() : process.env.FELTDB_AI_API_KEY;
131
+ const configured = configureAiModel({ provider, model: modelName, baseUrl: option('--base-url'), apiKey: apiKey || undefined,
132
+ credentialRequired: !args.includes('--no-credential') });
133
+ console.log(`✓ AI model configured: ${configured.provider} / ${configured.model}`);
134
+ 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.');
135
+ return;
136
+ }
137
+ throw new Error('Usage: feltdb ai model [show|set --provider <local-webllm|openai-compatible> --model <id> [--base-url <url>] [--api-key-stdin|--no-credential]]');
138
+ }
139
+ if (!args.length || args.every(argument => ['--dry-run', '--preview', '--ephemeral', '--no-open'].includes(argument))) {
140
+ console.log('Opening the local Ask FeltDB experience. Generation stays in your browser via WebGPU.');
141
+ return handleStudio(args, studioUrl => console.log(`Ask FeltDB: ${studioUrl}`));
142
+ }
143
+ const action = args[0];
144
+ if (action === 'context') {
145
+ const context = await loadLocalDevelopmentContract();
146
+ console.log(JSON.stringify({ snapshot: context.snapshot, flowHash: context.flowHash }, null, 2));
147
+ return;
148
+ }
149
+ if (action === 'review') {
150
+ const input = JSON.parse(fs.readFileSync(proposalFile(args.slice(1)), 'utf8'));
151
+ const proposal = input;
152
+ const review = await reviewFlowProposal(proposal);
153
+ console.log(`\n${proposal.summary || 'FeltDB change proposal'}`);
154
+ for (const line of describeProposal(review))
155
+ console.log(` ${line}`);
156
+ if (!describeProposal(review).length)
157
+ console.log(' No contract changes.');
158
+ return;
159
+ }
160
+ if (action === 'apply') {
161
+ const target = args.slice(1).find(value => !value.startsWith('--'));
162
+ if (!target)
163
+ throw new Error('A proposal id or JSON file is required');
164
+ const stored = target.startsWith('p_') ? await proposalService().get(target) : undefined;
165
+ const proposal = stored ? { baseContractHash: stored.base_contract_hash, baseFlowHash: stored.base_flow_hash,
166
+ proposedFlow: stored.proposed_flow, summary: stored.summary, changes: stored.contract_diff, warnings: stored.warnings }
167
+ : JSON.parse(fs.readFileSync(path.resolve(target), 'utf8'));
168
+ if (stored && stored.status !== 'approved')
169
+ throw new Error(`Proposal must be approved before apply; current status is ${stored.status}`);
170
+ if (!stored && !args.includes('--approve'))
171
+ throw new Error('Explicit approval is required: rerun with --approve');
172
+ const current = await loadLocalDevelopmentContract();
173
+ if (stored && (stored.application_id !== current.snapshot.application.application_id || stored.environment !== current.snapshot.application.environment)) {
174
+ throw new Error(`PROPOSAL_SCOPE_MISMATCH: ${stored.application_id}/${stored.environment} cannot apply to ${current.snapshot.application.application_id}/${current.snapshot.application.environment}`);
175
+ }
176
+ const alreadyWritten = stored && await import('@feltdb/core').then(module => module.flowSourceHash(stored.proposed_flow)) === current.flowHash;
177
+ if (alreadyWritten) {
178
+ console.log('Detected an approved proposal whose flow is already present; reconciling the interrupted application.');
179
+ const recoveredReview = await reviewFlowProposal({ ...proposal, baseContractHash: current.snapshot.contract_hash, baseFlowHash: current.flowHash });
180
+ syncGeneratedSource(recoveredReview, process.cwd(), { replace: args.includes('--replace-generated') });
181
+ validateRepositoryBuild(args);
182
+ await proposalService().transition(stored.id, 'applied', 'cli-repository-applier', true);
183
+ console.log(`✓ Reconciled and marked ${stored.id} applied`);
184
+ return;
185
+ }
186
+ const reviewOnly = await reviewFlowProposal(proposal);
187
+ console.log(`\n${proposal.summary || 'FeltDB change proposal'}`);
188
+ for (const line of describeProposal(reviewOnly))
189
+ console.log(` ${line}`);
190
+ console.log(` Source impact: feltdb.flow, src/feltdb/generated-contract.ts`);
191
+ if (args.includes('--dry-run') || args.includes('--preview')) {
192
+ console.log('✓ Preview complete. No files, flow, contract, or published state were changed.');
193
+ return;
194
+ }
195
+ const review = await applyFlowProposal(proposal, process.cwd(), {
196
+ allowDirty: args.includes('--allow-dirty'),
197
+ approveAuthorization: args.includes('--approve-authorization'),
198
+ replaceGenerated: args.includes('--replace-generated'),
199
+ });
200
+ console.log(`✓ Applied validated flow proposal (${review.proposedContract.hashes.application})`);
201
+ for (const file of review.written)
202
+ console.log(` ${file}`);
203
+ validateRepositoryBuild(args);
204
+ if (stored)
205
+ await proposalService().transition(stored.id, 'applied', 'cli-repository-applier', true);
206
+ return;
207
+ }
208
+ throw new Error('Usage: feltdb ai [context|review <proposal.json>|apply <proposal.json>]');
209
+ }
210
+ async function handleProposals(args) {
211
+ loadProjectEnvironment();
212
+ const client = proposalService();
213
+ const action = args[0] || 'list';
214
+ const id = args[1];
215
+ if (action === 'list') {
216
+ const { proposals } = await client.list();
217
+ for (const value of proposals.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at))))
218
+ console.log(`${value.id}\t${value.status}\t${value.summary}`);
219
+ if (!proposals.length)
220
+ console.log('No proposals.');
221
+ return;
222
+ }
223
+ if (!id)
224
+ throw new Error(`feltdb proposals ${action} requires a proposal id`);
225
+ if (action === 'get') {
226
+ console.log(JSON.stringify({ proposal: await client.get(id), history: (await client.history(id)).events }, null, 2));
227
+ return;
228
+ }
229
+ if (action === 'diff') {
230
+ const value = await client.get(id);
231
+ console.log(value.contract_diff.join('\n'));
232
+ console.log(JSON.stringify(value.source_plan, null, 2));
233
+ return;
234
+ }
235
+ if (action === 'approve' || action === 'reject') {
236
+ const value = await client.transition(id, action === 'approve' ? 'approved' : 'rejected', 'local-developer', false, args.includes('--approve-authorization'));
237
+ console.log(`✓ ${value.id} ${value.status}`);
238
+ return;
239
+ }
240
+ throw new Error('Usage: feltdb proposals [list|get|diff|approve|reject] [id]');
241
+ }
242
+ async function handleSourceSync(args) {
243
+ loadProjectEnvironment();
244
+ const context = await loadLocalDevelopmentContract();
245
+ const proposal = {
246
+ baseContractHash: context.snapshot.contract_hash,
247
+ baseFlowHash: context.flowHash,
248
+ proposedFlow: context.source,
249
+ summary: 'Synchronize generated source with feltdb.flow',
250
+ changes: [], warnings: [],
251
+ };
252
+ const review = await reviewFlowProposal(proposal);
253
+ const result = syncGeneratedSource(review, process.cwd(), { replace: args.includes('--replace-generated') });
254
+ for (const file of result.written)
255
+ console.log(`✓ Synchronized ${file}`);
256
+ }
85
257
  /**
86
258
  * Inspect the durable investigation store.
87
259
  *
@@ -231,6 +403,31 @@ async function handleFlowDeploy(args) {
231
403
  await db.close();
232
404
  }
233
405
  }
406
+ async function handleManagedPublish(args) {
407
+ const valueAfter = (flag) => { const index = args.indexOf(flag); return index >= 0 ? args[index + 1] : undefined; };
408
+ const confirm = async (question) => {
409
+ if (!process.stdin.isTTY)
410
+ return false;
411
+ const readline = await import('node:readline/promises');
412
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
413
+ try {
414
+ return !/^(n|no)$/i.test((await prompt.question(`${question} [Y/n] `)).trim());
415
+ }
416
+ finally {
417
+ prompt.close();
418
+ }
419
+ };
420
+ await publishApplication({
421
+ environment: valueAfter('--environment'),
422
+ message: valueAfter('--message'),
423
+ dryRun: args.includes('--dry-run'),
424
+ status: args.includes('--status'),
425
+ yes: args.includes('--yes'),
426
+ approveDestructive: args.includes('--approve-destructive-changes') || args.includes('--allow-destructive'),
427
+ noBuild: args.includes('--no-build'),
428
+ confirm,
429
+ });
430
+ }
234
431
  async function handleServer(args) {
235
432
  const require = createRequire(import.meta.url);
236
433
  const packageRoot = path.dirname(require.resolve('@feltdb/core/package.json'));
@@ -419,17 +616,39 @@ For more info, visit: https://github.com/rkendel1/feltdb
419
616
  `);
420
617
  }
421
618
  async function handleConnect(args) {
422
- const url = args[0];
423
- if (!url) {
424
- console.error('❌ URL required');
425
- console.error('Usage: feltdb connect <url>\n');
426
- process.exit(1);
427
- }
619
+ loadProjectEnvironment();
620
+ const projectFile = path.resolve('feltdb.config.json');
621
+ const project = fs.existsSync(projectFile) ? JSON.parse(fs.readFileSync(projectFile, 'utf8')) : {};
622
+ const url = (args[0]
623
+ || process.env.VITE_FELTDB_MANAGED_URL
624
+ || process.env.VITE_FELTDB_URL
625
+ || project.server?.url
626
+ || (project.runtime === 'browser' ? '' : 'http://localhost:7700')).replace(/\/$/, '');
627
+ if (!url)
628
+ throw new Error('This project uses the embedded browser runtime; there is no server endpoint to connect to. Run npm run dev.');
428
629
  console.log('🔗 Connecting to FeltDB Server\n');
429
630
  console.log(`URL: ${url}`);
430
- console.log('Discovering node...');
431
- console.log('Checking capabilities...');
432
631
  console.log('Verifying connection...\n');
632
+ const controller = new AbortController();
633
+ const timeout = setTimeout(() => controller.abort(), 5000);
634
+ let health;
635
+ try {
636
+ const response = await fetch(`${url}/health`, {
637
+ headers: process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY
638
+ ? { Authorization: `Bearer ${process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY}` }
639
+ : {},
640
+ signal: controller.signal,
641
+ });
642
+ if (!response.ok)
643
+ throw new Error(`HTTP ${response.status}`);
644
+ health = await response.json();
645
+ }
646
+ catch (error) {
647
+ 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}`);
648
+ }
649
+ finally {
650
+ clearTimeout(timeout);
651
+ }
433
652
  // Store connection info
434
653
  const configDir = path.join(process.cwd(), '.feltdb');
435
654
  if (!fs.existsSync(configDir)) {
@@ -439,15 +658,16 @@ async function handleConnect(args) {
439
658
  fs.writeFileSync(connFile, JSON.stringify({
440
659
  url,
441
660
  connected: new Date().toISOString(),
442
- authenticated: false,
661
+ authenticated: Boolean(process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY),
662
+ namespace: project.namespace || process.env.VITE_FELTDB_MANAGED_NAMESPACE || 'default',
663
+ runtime: project.runtime || 'self-hosted',
443
664
  }, null, 2));
444
665
  console.log('✅ Connected to FeltDB\n');
445
666
  console.log('Endpoint: ' + url);
446
- console.log('Namespace: default');
447
- console.log('Authenticated: no');
448
- console.log('Runtime: self-hosted');
449
- console.log('Peers: 0');
450
- console.log('Health: ✓\n');
667
+ console.log(`Namespace: ${project.namespace || process.env.VITE_FELTDB_MANAGED_NAMESPACE || 'default'}`);
668
+ console.log(`Authenticated: ${process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY ? 'yes' : 'no'}`);
669
+ console.log(`Runtime: ${project.runtime || 'self-hosted'}`);
670
+ console.log(`Health: ${health.status || 'healthy'}\n`);
451
671
  console.log('Use: feltdb status (to check connection)\n');
452
672
  }
453
673
  async function handleExplain(args) {
@@ -480,20 +700,39 @@ async function handleStatus() {
480
700
  console.log('Fabric: Enabled');
481
701
  console.log('Authentication: Development');
482
702
  console.log('Health: ✓\n');
703
+ await displayContractStatus();
483
704
  return;
484
705
  }
485
706
  const connection = JSON.parse(fs.readFileSync(connFile, 'utf-8'));
486
707
  console.log('Connected to Server\n');
487
708
  console.log(`Endpoint: ${connection.url}`);
488
- console.log('Namespace: default');
709
+ console.log(`Namespace: ${connection.namespace || 'default'}`);
489
710
  console.log(`Authenticated: ${connection.authenticated ? 'yes' : 'no'}`);
490
- console.log('Runtime: self-hosted');
491
- console.log('Storage: durable');
492
- console.log('Peers: 3');
493
- console.log('Capabilities: 8');
494
- console.log('Agents: 2');
495
- console.log('Workflows: 5');
496
- console.log('Health: ✓\n');
711
+ console.log(`Runtime: ${connection.runtime || 'self-hosted'}`);
712
+ try {
713
+ const response = await fetch(`${String(connection.url).replace(/\/$/, '')}/health`, { signal: AbortSignal.timeout(5000) });
714
+ if (!response.ok)
715
+ throw new Error(`HTTP ${response.status}`);
716
+ const health = await response.json();
717
+ console.log(`Health: ${health.status || 'healthy'}\n`);
718
+ }
719
+ catch {
720
+ console.log('Health: unavailable');
721
+ console.log('Start the configured runtime and run feltdb connect again.\n');
722
+ }
723
+ await displayContractStatus();
724
+ }
725
+ async function displayContractStatus() {
726
+ if (!fs.existsSync(path.resolve('feltdb.flow')))
727
+ return;
728
+ loadProjectEnvironment();
729
+ const current = await loadLocalDevelopmentContract();
730
+ const publishedFile = path.resolve('.feltdb/last-published.json');
731
+ const published = fs.existsSync(publishedFile) ? JSON.parse(fs.readFileSync(publishedFile, 'utf8')) : undefined;
732
+ console.log('Application contract');
733
+ console.log(`Source: ${current.snapshot.contract_hash}`);
734
+ console.log(`Published: ${published?.contractHash || 'unknown'}`);
735
+ console.log(`Drift: ${!published?.contractHash ? 'unknown' : published.contractHash === current.snapshot.contract_hash ? 'synchronized' : 'source differs from published'}\n`);
497
736
  }
498
737
  async function handleWorkspace(args) {
499
738
  const subcommand = args[0] || 'status';
@@ -695,10 +934,16 @@ async function handleDev(args) {
695
934
  if (!process.env.VITE_FELTDB_URL) {
696
935
  const authorityPort = Number(args.includes('--authority-port') ? args[args.indexOf('--authority-port') + 1] || '7700' : '7700');
697
936
  localAuthority = await startLocalDevelopmentAuthority({
698
- dataDir: path.join(projectDir, '.feltdb', 'authority'),
937
+ dataDir: path.join(projectDir, '.feltdb-data', 'authority'),
699
938
  port: authorityPort,
939
+ proposalContext: () => loadLocalDevelopmentContract(projectDir),
700
940
  });
701
941
  process.env.VITE_FELTDB_URL = localAuthority.endpoint;
942
+ const connectionsPath = path.join(projectDir, '.feltdb', 'connections.json');
943
+ const connections = fs.existsSync(connectionsPath) ? JSON.parse(fs.readFileSync(connectionsPath, 'utf8')) : {};
944
+ fs.writeFileSync(connectionsPath, `${JSON.stringify({ ...connections, developmentAuthority: {
945
+ endpoint: localAuthority.endpoint, proposalAuthorityToken: localAuthority.proposalAuthorityToken,
946
+ } }, null, 2)}\n`, { mode: 0o600 });
702
947
  }
703
948
  const authorityUrl = process.env.VITE_FELTDB_URL;
704
949
  if (!authorityUrl)
@@ -1155,9 +1400,13 @@ async function handleDoctor() {
1155
1400
  console.log('\n✅ System healthy\n');
1156
1401
  }
1157
1402
  async function handleStudio(args, onReady) {
1403
+ loadProjectEnvironment();
1404
+ const projectConfigPath = path.resolve('feltdb.config.json');
1405
+ const projectConfig = fs.existsSync(projectConfigPath) ? JSON.parse(fs.readFileSync(projectConfigPath, 'utf8')) : {};
1406
+ const projectIsManaged = projectConfig.runtime === 'managed';
1158
1407
  const connectUrl = args.includes('--connect')
1159
1408
  ? args[args.indexOf('--connect') + 1]
1160
- : undefined;
1409
+ : projectIsManaged ? process.env.VITE_FELTDB_MANAGED_URL : undefined;
1161
1410
  const port = args.includes('--port')
1162
1411
  ? args[args.indexOf('--port') + 1] || '7701'
1163
1412
  : '7701';
@@ -1167,10 +1416,10 @@ async function handleStudio(args, onReady) {
1167
1416
  const open = !args.includes('--no-open');
1168
1417
  const namespace = args.includes('--namespace')
1169
1418
  ? args[args.indexOf('--namespace') + 1] || 'default'
1170
- : 'default';
1419
+ : projectIsManaged ? process.env.VITE_FELTDB_MANAGED_NAMESPACE || projectConfig.namespace || 'default' : 'default';
1171
1420
  const runtime = args.includes('--runtime')
1172
1421
  ? args[args.indexOf('--runtime') + 1] || 'browser'
1173
- : connectUrl ? 'remote' : 'browser';
1422
+ : projectIsManaged ? 'managed' : connectUrl ? 'remote' : 'browser';
1174
1423
  const appUrl = args.includes('--app-url')
1175
1424
  ? args[args.indexOf('--app-url') + 1]
1176
1425
  : undefined;
@@ -1201,9 +1450,13 @@ async function handleStudio(args, onReady) {
1201
1450
  console.log('⚠️ Studio application artifacts not found.');
1202
1451
  return;
1203
1452
  }
1204
- const projectFlow = ['feltdb.flow', ...fs.readdirSync(process.cwd()).filter(value => value.endsWith('.flow'))]
1205
- .map(value => path.resolve(process.cwd(), value))
1206
- .find((value, index, values) => values.indexOf(value) === index && fs.existsSync(value));
1453
+ const canonicalFlow = path.resolve(process.cwd(), 'feltdb.flow');
1454
+ const projectFlow = fs.existsSync(canonicalFlow) ? canonicalFlow : undefined;
1455
+ const studioMutationToken = randomBytes(24).toString('hex');
1456
+ const projectPackage = fs.existsSync(path.resolve('package.json')) ? JSON.parse(fs.readFileSync(path.resolve('package.json'), 'utf8')) : {};
1457
+ const webllmEnabled = Boolean(projectPackage.dependencies?.['@feltdb/webllm'] || projectPackage.devDependencies?.['@feltdb/webllm']);
1458
+ const previewOnly = args.includes('--ephemeral');
1459
+ const proposalClient = proposalService(process.cwd(), session.authorityUrl);
1207
1460
  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' };
1208
1461
  const server = http.createServer(async (request, response) => {
1209
1462
  const pathname = decodeURIComponent(new URL(request.url || '/', 'http://localhost').pathname);
@@ -1211,7 +1464,187 @@ async function handleStudio(args, onReady) {
1211
1464
  response.setHeader('Content-Type', 'application/json; charset=utf-8');
1212
1465
  response.setHeader('Cache-Control', 'no-store');
1213
1466
  response.setHeader('Pragma', 'no-cache');
1214
- response.end(JSON.stringify({ token: connectUrl ? process.env.VITE_FELTDB_API_KEY || '' : '', session }));
1467
+ const managedPublish = runtime === 'managed' ? {
1468
+ url: process.env.VITE_FELTDB_MANAGED_URL || connectUrl || '',
1469
+ namespace: process.env.VITE_FELTDB_MANAGED_NAMESPACE || namespace,
1470
+ tenantId: process.env.VITE_FELTDB_MANAGED_TENANT_ID || '',
1471
+ applicationId: process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || '',
1472
+ environment: process.env.VITE_FELTDB_MANAGED_ENVIRONMENT || 'production',
1473
+ publishEndpoint: '/_feltdb/publish',
1474
+ } : undefined;
1475
+ response.end(JSON.stringify({ token: connectUrl ? process.env.VITE_FELTDB_API_KEY || process.env.VITE_FELTDB_MANAGED_API_KEY || '' : '', managedPublish, session, sourceSync: { enabled: true, webllmEnabled, previewOnly, token: studioMutationToken, aiModel: publicAiModelConfiguration() } }));
1476
+ return;
1477
+ }
1478
+ if (pathname === '/_feltdb/contract') {
1479
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1480
+ response.setHeader('Cache-Control', 'no-store');
1481
+ try {
1482
+ const current = await loadLocalDevelopmentContract();
1483
+ const lastPublished = fs.existsSync(path.resolve('.feltdb/last-published.json'))
1484
+ ? JSON.parse(fs.readFileSync(path.resolve('.feltdb/last-published.json'), 'utf8')) : undefined;
1485
+ let runningContract;
1486
+ let runningSnapshot;
1487
+ const applicationId = process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || lastPublished?.applicationId;
1488
+ if (connectUrl && applicationId) {
1489
+ try {
1490
+ const endpoint = new URL('/v1/application', connectUrl);
1491
+ endpoint.searchParams.set('application_id', applicationId);
1492
+ endpoint.searchParams.set('environment', process.env.VITE_FELTDB_MANAGED_ENVIRONMENT || lastPublished?.environment || 'production');
1493
+ const headers = {};
1494
+ const credential = process.env.FELTDB_MANAGED_CONTROL_API_KEY || process.env.VITE_FELTDB_API_KEY;
1495
+ if (credential)
1496
+ headers.authorization = `Bearer ${credential}`;
1497
+ const discovered = await fetch(endpoint, { headers });
1498
+ if (discovered.ok) {
1499
+ const application = await discovered.json();
1500
+ runningSnapshot = application.snapshot;
1501
+ runningContract = runningSnapshot?.contract_hash ?? application.contract_hash;
1502
+ }
1503
+ }
1504
+ catch { /* Drift remains explicitly unknown while the runtime is unavailable. */ }
1505
+ }
1506
+ const knownContracts = [current.snapshot.contract_hash, runningContract, lastPublished?.contractHash].filter(Boolean);
1507
+ response.end(JSON.stringify({ snapshot: runningSnapshot ?? current.snapshot, flowHash: current.flowHash, source: current.source,
1508
+ drift: { source: current.snapshot.contract_hash, running: runningContract, published: lastPublished?.contractHash,
1509
+ synchronized: new Set(knownContracts).size <= 1 } }));
1510
+ }
1511
+ catch (error) {
1512
+ response.statusCode = 400;
1513
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1514
+ }
1515
+ return;
1516
+ }
1517
+ if (pathname === '/_feltdb/proposals/list' && request.method === 'GET') {
1518
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1519
+ response.setHeader('Cache-Control', 'no-store');
1520
+ try {
1521
+ response.end(JSON.stringify(await proposalClient.list()));
1522
+ }
1523
+ catch (error) {
1524
+ response.statusCode = 503;
1525
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1526
+ }
1527
+ return;
1528
+ }
1529
+ const proposalDetail = pathname.match(/^\/_feltdb\/proposals\/(p_[A-Za-z0-9]+)$/);
1530
+ if (proposalDetail && request.method === 'GET') {
1531
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1532
+ response.setHeader('Cache-Control', 'no-store');
1533
+ try {
1534
+ response.end(JSON.stringify({ proposal: await proposalClient.get(proposalDetail[1]), history: (await proposalClient.history(proposalDetail[1])).events }));
1535
+ }
1536
+ catch (error) {
1537
+ response.statusCode = 404;
1538
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1539
+ }
1540
+ return;
1541
+ }
1542
+ // Studio proposes and previews. CLI applies to the repository. FeltDB validates
1543
+ // and stores the proposal as application state. Human approves. Nothing else writes source.
1544
+ if (pathname === '/_feltdb/proposals' || pathname === '/_feltdb/proposals/approve' || pathname === '/_feltdb/proposals/reject') {
1545
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1546
+ response.setHeader('Cache-Control', 'no-store');
1547
+ if (request.method !== 'POST') {
1548
+ response.statusCode = 405;
1549
+ response.end(JSON.stringify({ error: 'POST required' }));
1550
+ return;
1551
+ }
1552
+ if (request.headers['x-feltdb-studio-token'] !== studioMutationToken) {
1553
+ response.statusCode = 403;
1554
+ response.end(JSON.stringify({ error: 'Invalid Studio source-sync token' }));
1555
+ return;
1556
+ }
1557
+ try {
1558
+ const chunks = [];
1559
+ for await (const chunk of request)
1560
+ chunks.push(Buffer.from(chunk));
1561
+ const input = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {};
1562
+ if (pathname.endsWith('/approve') || pathname.endsWith('/reject')) {
1563
+ if (previewOnly)
1564
+ throw new Error('Preview-only sessions cannot approve or reject persisted proposals');
1565
+ if (input.confirm !== true)
1566
+ throw new Error('Explicit proposal confirmation is required');
1567
+ const proposal = await proposalClient.transition(String(input.proposalId || ''), pathname.endsWith('/approve') ? 'approved' : 'rejected', 'studio-developer', false, input.approveAuthorization === true);
1568
+ response.end(JSON.stringify({ status: proposal.status, proposalId: proposal.id,
1569
+ command: proposal.status === 'approved' ? `feltdb ai apply ${proposal.id}` : undefined }));
1570
+ }
1571
+ else {
1572
+ const proposal = input.proposal;
1573
+ const review = await reviewFlowProposal(proposal);
1574
+ const changes = describeProposal(review);
1575
+ let stored;
1576
+ if (!previewOnly) {
1577
+ const current = await loadLocalDevelopmentContract();
1578
+ stored = await proposalClient.create({ application_id: current.snapshot.application.application_id,
1579
+ environment: current.snapshot.application.environment, base_contract_hash: proposal.baseContractHash,
1580
+ base_flow_hash: proposal.baseFlowHash, proposed_flow: proposal.proposedFlow, summary: proposal.summary,
1581
+ warnings: proposal.warnings, actor: 'studio-developer', metadata: { model: publicAiModelConfiguration().model, created_from: 'studio' } });
1582
+ await proposalClient.validate(stored.id, 'feltdb-validator', review.proposedContract.manifest);
1583
+ stored = (await proposalClient.preview(stored.id, 'studio-developer')).proposal;
1584
+ }
1585
+ response.end(JSON.stringify({ status: 'previewed', proposalId: stored?.id ?? 'ephemeral', changes,
1586
+ sourcePlan: stored?.source_plan.files.map(value => value.path) ?? ['feltdb.flow', 'src/feltdb/generated-contract.ts'], previewOnly,
1587
+ authorizationChanges: review.authorizationChanges, migration: review.migration,
1588
+ contractHash: review.proposedContract.hashes.application }));
1589
+ }
1590
+ }
1591
+ catch (error) {
1592
+ response.statusCode = 400;
1593
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1594
+ }
1595
+ return;
1596
+ }
1597
+ if (pathname === '/_feltdb/ai/generate') {
1598
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1599
+ response.setHeader('Cache-Control', 'no-store');
1600
+ if (request.method !== 'POST') {
1601
+ response.statusCode = 405;
1602
+ response.end(JSON.stringify({ error: 'POST required' }));
1603
+ return;
1604
+ }
1605
+ if (request.headers['x-feltdb-studio-token'] !== studioMutationToken) {
1606
+ response.statusCode = 403;
1607
+ response.end(JSON.stringify({ error: 'Invalid Studio token' }));
1608
+ return;
1609
+ }
1610
+ try {
1611
+ const chunks = [];
1612
+ for await (const chunk of request)
1613
+ chunks.push(Buffer.from(chunk));
1614
+ const input = JSON.parse(Buffer.concat(chunks).toString('utf8'));
1615
+ if (!input.intent?.trim())
1616
+ throw new Error('An AI change request is required');
1617
+ const current = await loadLocalDevelopmentContract();
1618
+ const proposal = await generateConfiguredFlowProposal({ intent: input.intent, snapshot: current.snapshot, currentFlow: current.source });
1619
+ response.end(JSON.stringify({ proposal }));
1620
+ }
1621
+ catch (error) {
1622
+ response.statusCode = 400;
1623
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1624
+ }
1625
+ return;
1626
+ }
1627
+ if (pathname === '/_feltdb/publish') {
1628
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1629
+ response.setHeader('Cache-Control', 'no-store');
1630
+ if (request.method !== 'POST') {
1631
+ response.statusCode = 405;
1632
+ response.end(JSON.stringify({ error: 'POST required' }));
1633
+ return;
1634
+ }
1635
+ try {
1636
+ const chunks = [];
1637
+ for await (const chunk of request)
1638
+ chunks.push(Buffer.from(chunk));
1639
+ const input = chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {};
1640
+ const messages = [];
1641
+ 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) });
1642
+ 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 }));
1643
+ }
1644
+ catch (error) {
1645
+ response.statusCode = 400;
1646
+ response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1647
+ }
1215
1648
  return;
1216
1649
  }
1217
1650
  if (pathname === '/_feltdb/project') {
@@ -1312,6 +1745,20 @@ Commands:
1312
1745
  validate [file] Validate an entire FlowSpec application model
1313
1746
  diff [file] Diff FlowSpec against the last deployed model
1314
1747
  deploy [file] Validate, version, and deploy FlowSpec
1748
+ publish [file] Build and publish/update a managed application
1749
+ ai Open the canonical interactive local-AI workflow
1750
+ ai --dry-run Open an ephemeral preview-only AI session
1751
+ ai model show Show the selected local or user-provided model
1752
+ ai model set Select WebLLM or an OpenAI-compatible model
1753
+ ai context Show the secret-free contract grounding snapshot
1754
+ ai review <file> Validate and review a WebLLM flow proposal
1755
+ ai apply <id> Apply an approved proposal and synchronize source
1756
+ proposals List durable FeltDB application proposals
1757
+ proposals get <id> Show a proposal and its lifecycle history
1758
+ proposals diff <id> Show contract and structured source impact
1759
+ proposals approve <id> Approve a previewed proposal
1760
+ proposals reject <id> Reject a proposed/validated/previewed proposal
1761
+ sync Regenerate owned source from feltdb.flow
1315
1762
  studio Launch FeltDB Studio (developer interface)
1316
1763
  server Start self-hosted FeltDB server
1317
1764
  keys Manage API keys
@@ -1350,6 +1797,13 @@ Connection:
1350
1797
  doctor Check system health
1351
1798
  help Show this help message
1352
1799
 
1800
+ Publish:
1801
+ feltdb publish --dry-run Validate and preview without activation
1802
+ feltdb publish --yes Publish to the configured environment
1803
+ feltdb publish --status Compare local and active cloud versions
1804
+ feltdb publish --environment staging Override the configured environment
1805
+ feltdb publish --approve-destructive-changes --yes
1806
+
1353
1807
  Inspect Sub-commands:
1354
1808
  feltdb inspect Show runtime overview
1355
1809
  feltdb inspect peers Show connected peers
@@ -1373,6 +1827,7 @@ Examples:
1373
1827
  feltdb status
1374
1828
  feltdb dev
1375
1829
  feltdb build
1830
+ feltdb publish --dry-run
1376
1831
  feltdb check
1377
1832
  feltdb inspect
1378
1833
  feltdb inspect peers
package/dist/cli/index.js CHANGED
@@ -23,7 +23,7 @@ import * as path from 'path';
23
23
  import * as readline from 'readline';
24
24
  import { getClient } from './api-client.js';
25
25
  import { loadFeltDBConfig, createDefaultConfig, validateModel, } from './config.js';
26
- const VERSION = '0.7.2';
26
+ const VERSION = '0.7.4';
27
27
  function prompt(question) {
28
28
  const rl = readline.createInterface({
29
29
  input: process.stdin,