@feltdb/core 0.7.3 → 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 (95) 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 +466 -105
  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 +84 -6
  22. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
  23. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  24. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +46 -3
  25. package/dist/create/server-source/crates/feltdb-server/src/main.rs +315 -16
  26. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +20 -0
  27. package/dist/create/template/default-project/README.md +57 -0
  28. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  29. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  30. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  31. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  32. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  33. package/dist/create/template/default-project/feltdb.flow +169 -0
  34. package/dist/create/template/default-project/src/App.tsx +20 -0
  35. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  36. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  37. package/dist/create/template/default-project/src/index.tsx +14 -0
  38. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  39. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  40. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  41. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  42. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  43. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  44. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  45. package/dist/create/template/default-project/src/styles-application.css +21 -0
  46. package/dist/create/template/default-project/src/styles.css +1 -0
  47. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  48. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  49. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  50. package/dist/db.d.ts +49 -1
  51. package/dist/db.d.ts.map +1 -1
  52. package/dist/db.js +77 -0
  53. package/dist/error-codes.d.ts +20 -1
  54. package/dist/error-codes.d.ts.map +1 -1
  55. package/dist/error-codes.js +25 -0
  56. package/dist/flowspec.d.ts.map +1 -1
  57. package/dist/flowspec.js +27 -6
  58. package/dist/http-db.d.ts +15 -2
  59. package/dist/http-db.d.ts.map +1 -1
  60. package/dist/http-db.js +113 -5
  61. package/dist/index-core.d.ts +3 -0
  62. package/dist/index-core.d.ts.map +1 -1
  63. package/dist/index-core.js +3 -0
  64. package/dist/proposal.d.ts +85 -0
  65. package/dist/proposal.d.ts.map +1 -0
  66. package/dist/proposal.js +105 -0
  67. package/dist/studio/app.d.ts +3 -1
  68. package/dist/studio/app.d.ts.map +1 -1
  69. package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
  70. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  71. package/dist/studio/components/AskFeltDB.d.ts +20 -0
  72. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  73. package/dist/studio/components/ProposalBrowser.d.ts +3 -0
  74. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  75. package/dist/studio/components/index.d.ts +2 -0
  76. package/dist/studio/components/index.d.ts.map +1 -1
  77. package/dist/studio/components/index.js +4 -4
  78. package/dist/studio/{components-Dxuhrv8_.js → components-Bm4sjhcX.js} +335 -5
  79. package/dist/studio/index.js +168 -157
  80. package/dist/studio/studio.css +1 -1
  81. package/dist/studio-app/assets/dist-DA1MYSzp.js +1 -0
  82. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-B-Ccgccb.js} +1 -1
  83. package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
  84. package/dist/studio-app/assets/{index-C1GWyazR.css → index-BRrmvaKw.css} +1 -1
  85. package/dist/studio-app/assets/index-CEIqowCh.js +42 -0
  86. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  87. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  88. package/dist/studio-app/index.html +2 -2
  89. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  90. package/dist/workspace/local-development-authority.d.ts +3 -0
  91. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  92. package/dist/workspace/local-development-authority.js +62 -1
  93. package/package.json +1 -1
  94. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  95. package/dist/studio-app/assets/index-B5tnmvSD.js +0 -28
@@ -8,12 +8,16 @@ 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 { createFeltDB, diffFlowSpec, formatFlowSpec, InvestigationLifecycleManager, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
12
12
  import { discoverWorkspace, ensureWorkspaceGitIgnored, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
13
13
  import { applicationUrlCandidate, detectApplication, discoverApplicationUrl } from './application.js';
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;
@@ -79,11 +83,177 @@ export async function handleCommand(command, args) {
79
83
  return handleFlowDeploy(args);
80
84
  case 'publish':
81
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);
82
92
  case 'help':
83
93
  default:
84
94
  return handleHelp();
85
95
  }
86
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
+ }
87
257
  /**
88
258
  * Inspect the durable investigation store.
89
259
  *
@@ -234,83 +404,29 @@ async function handleFlowDeploy(args) {
234
404
  }
235
405
  }
236
406
  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`);
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());
292
415
  }
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
- }
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
+ });
314
430
  }
315
431
  async function handleServer(args) {
316
432
  const require = createRequire(import.meta.url);
@@ -500,17 +616,39 @@ For more info, visit: https://github.com/rkendel1/feltdb
500
616
  `);
501
617
  }
502
618
  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
- }
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.');
509
629
  console.log('🔗 Connecting to FeltDB Server\n');
510
630
  console.log(`URL: ${url}`);
511
- console.log('Discovering node...');
512
- console.log('Checking capabilities...');
513
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
+ }
514
652
  // Store connection info
515
653
  const configDir = path.join(process.cwd(), '.feltdb');
516
654
  if (!fs.existsSync(configDir)) {
@@ -520,15 +658,16 @@ async function handleConnect(args) {
520
658
  fs.writeFileSync(connFile, JSON.stringify({
521
659
  url,
522
660
  connected: new Date().toISOString(),
523
- 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',
524
664
  }, null, 2));
525
665
  console.log('✅ Connected to FeltDB\n');
526
666
  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');
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`);
532
671
  console.log('Use: feltdb status (to check connection)\n');
533
672
  }
534
673
  async function handleExplain(args) {
@@ -561,20 +700,39 @@ async function handleStatus() {
561
700
  console.log('Fabric: Enabled');
562
701
  console.log('Authentication: Development');
563
702
  console.log('Health: ✓\n');
703
+ await displayContractStatus();
564
704
  return;
565
705
  }
566
706
  const connection = JSON.parse(fs.readFileSync(connFile, 'utf-8'));
567
707
  console.log('Connected to Server\n');
568
708
  console.log(`Endpoint: ${connection.url}`);
569
- console.log('Namespace: default');
709
+ console.log(`Namespace: ${connection.namespace || 'default'}`);
570
710
  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');
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`);
578
736
  }
579
737
  async function handleWorkspace(args) {
580
738
  const subcommand = args[0] || 'status';
@@ -776,10 +934,16 @@ async function handleDev(args) {
776
934
  if (!process.env.VITE_FELTDB_URL) {
777
935
  const authorityPort = Number(args.includes('--authority-port') ? args[args.indexOf('--authority-port') + 1] || '7700' : '7700');
778
936
  localAuthority = await startLocalDevelopmentAuthority({
779
- dataDir: path.join(projectDir, '.feltdb', 'authority'),
937
+ dataDir: path.join(projectDir, '.feltdb-data', 'authority'),
780
938
  port: authorityPort,
939
+ proposalContext: () => loadLocalDevelopmentContract(projectDir),
781
940
  });
782
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 });
783
947
  }
784
948
  const authorityUrl = process.env.VITE_FELTDB_URL;
785
949
  if (!authorityUrl)
@@ -1286,9 +1450,13 @@ async function handleStudio(args, onReady) {
1286
1450
  console.log('⚠️ Studio application artifacts not found.');
1287
1451
  return;
1288
1452
  }
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));
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);
1292
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' };
1293
1461
  const server = http.createServer(async (request, response) => {
1294
1462
  const pathname = decodeURIComponent(new URL(request.url || '/', 'http://localhost').pathname);
@@ -1299,12 +1467,184 @@ async function handleStudio(args, onReady) {
1299
1467
  const managedPublish = runtime === 'managed' ? {
1300
1468
  url: process.env.VITE_FELTDB_MANAGED_URL || connectUrl || '',
1301
1469
  namespace: process.env.VITE_FELTDB_MANAGED_NAMESPACE || namespace,
1302
- token: process.env.VITE_FELTDB_MANAGED_API_KEY || process.env.VITE_FELTDB_API_KEY || '',
1303
1470
  tenantId: process.env.VITE_FELTDB_MANAGED_TENANT_ID || '',
1304
1471
  applicationId: process.env.VITE_FELTDB_MANAGED_APPLICATION_ID || '',
1305
1472
  environment: process.env.VITE_FELTDB_MANAGED_ENVIRONMENT || 'production',
1473
+ publishEndpoint: '/_feltdb/publish',
1306
1474
  } : undefined;
1307
- response.end(JSON.stringify({ token: connectUrl ? process.env.VITE_FELTDB_API_KEY || process.env.VITE_FELTDB_MANAGED_API_KEY || '' : '', managedPublish, session }));
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
+ }
1308
1648
  return;
1309
1649
  }
1310
1650
  if (pathname === '/_feltdb/project') {
@@ -1406,6 +1746,19 @@ Commands:
1406
1746
  diff [file] Diff FlowSpec against the last deployed model
1407
1747
  deploy [file] Validate, version, and deploy FlowSpec
1408
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
1409
1762
  studio Launch FeltDB Studio (developer interface)
1410
1763
  server Start self-hosted FeltDB server
1411
1764
  keys Manage API keys
@@ -1444,6 +1797,13 @@ Connection:
1444
1797
  doctor Check system health
1445
1798
  help Show this help message
1446
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
+
1447
1807
  Inspect Sub-commands:
1448
1808
  feltdb inspect Show runtime overview
1449
1809
  feltdb inspect peers Show connected peers
@@ -1467,6 +1827,7 @@ Examples:
1467
1827
  feltdb status
1468
1828
  feltdb dev
1469
1829
  feltdb build
1830
+ feltdb publish --dry-run
1470
1831
  feltdb check
1471
1832
  feltdb inspect
1472
1833
  feltdb inspect peers