@fiodos/cli 0.1.23 → 0.1.25

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiodos/cli",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Fiodos CLI — analyzes your app's source code and generates the in-app voice-agent manifest, then wires the orb. Powers `npx @fiodos/cli analyze`.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -14,13 +14,14 @@
14
14
  "README.md"
15
15
  ],
16
16
  "scripts": {
17
- "analyze": "node src/index.js"
17
+ "analyze": "node src/index.js",
18
+ "test": "node --test test/*.test.js"
18
19
  },
19
20
  "engines": {
20
21
  "node": ">=18"
21
22
  },
22
23
  "dependencies": {
23
- "@fiodos/core": "^0.1.2",
24
+ "@fiodos/core": "^0.1.10",
24
25
  "esbuild": "^0.28.1",
25
26
  "jsdom": "^24.0.0",
26
27
  "typescript": "^5.6.0"
@@ -214,8 +214,31 @@ function summarizeEnv(result) {
214
214
  return { ...base, applied: false };
215
215
  }
216
216
 
217
+ function summarizeSession(result) {
218
+ if (!result) return { status: 'skipped', applied: false };
219
+ const base = { status: result.status, strategy: result.strategy || null };
220
+ switch (result.status) {
221
+ case 'applied':
222
+ case 'applied-untested':
223
+ return {
224
+ ...base,
225
+ applied: true,
226
+ what: 'Connected the orb to the app\'s real session state (isAuthenticated/getUserId) automatically.',
227
+ revert: 'Delete session.generated.* and remove the FYODOS:SESSION blocks + mount props.',
228
+ };
229
+ case 'review':
230
+ return { ...base, applied: false, note: `Session left for review: ${result.reason || ''}` };
231
+ case 'reverted':
232
+ return { ...base, applied: false, note: `Session wiring reverted: ${result.reason || ''}` };
233
+ case 'no-session':
234
+ return { ...base, applied: false, note: 'No login detected — session wiring not needed.' };
235
+ default:
236
+ return { ...base, applied: false };
237
+ }
238
+ }
239
+
217
240
  function collectFileEntries(appRoot, ctx) {
218
- const { orbResult, handlerResult, registryResult, envResult } = ctx;
241
+ const { orbResult, handlerResult, registryResult, sessionResult, envResult } = ctx;
219
242
  const created = [];
220
243
  const modified = [];
221
244
  const docs = [];
@@ -291,6 +314,36 @@ function collectFileEntries(appRoot, ctx) {
291
314
  });
292
315
  }
293
316
 
317
+ // Session wiring (auth awareness — automatic)
318
+ if (
319
+ sessionResult &&
320
+ (sessionResult.status === 'applied' || sessionResult.status === 'applied-untested')
321
+ ) {
322
+ if (sessionResult.sessionFile) {
323
+ created.push({
324
+ path: normRel(appRoot, sessionResult.sessionFile),
325
+ phase: 'session',
326
+ what: 'Generated session module (`session.generated.*`): isAuthenticated/getUserId for the orb.',
327
+ revert: 'Safe to delete; re-run the installer to regenerate.',
328
+ });
329
+ }
330
+ if (sessionResult.editedFile) {
331
+ modified.push({
332
+ path: sessionResult.editedFile,
333
+ phase: 'session',
334
+ what: 'Registered the live session in the bridge (reversible FYODOS:SESSION block).',
335
+ revert: 'Remove the FYODOS:SESSION blocks.',
336
+ });
337
+ }
338
+ if (sessionResult.docPath) {
339
+ docs.push({
340
+ path: normRel(appRoot, sessionResult.docPath),
341
+ phase: 'session',
342
+ what: 'Session wiring summary: strategy, honesty contract, security notes.',
343
+ });
344
+ }
345
+ }
346
+
294
347
  // Env (never record values)
295
348
  if (envResult?.status === 'written' && envResult.writePlan?.targetRel) {
296
349
  modified.push({
@@ -379,6 +432,7 @@ function buildRunRecord(appRoot, ctx) {
379
432
  orb: summarizeOrb(ctx.orbResult),
380
433
  handlers: summarizeHandlers(ctx.handlerResult, appRoot),
381
434
  registries: summarizeRegistries(ctx.registryResult),
435
+ session: summarizeSession(ctx.sessionResult),
382
436
  env: summarizeEnv(ctx.envResult),
383
437
  },
384
438
  files: collectFileEntries(appRoot, ctx),
package/src/index.js CHANGED
@@ -94,10 +94,14 @@ const { collectFiles } = require('./collect');
94
94
  const { resolveAnalysisRoot } = require('./resolveAnalysisRoot');
95
95
  const { analyzeWithAI, correctActionWiring, generateGuardianWithAI, DEFAULT_MODEL, resolveModel } = require('./aiAnalyze');
96
96
  const { verifyManifest } = require('./verify');
97
- const { wireWebOrb, reportWireResult, connectOrbRegistries, reportRegistriesResult } = require('./wireWeb');
98
- const { wireReactNativeOrb, reportReactNativeWire } = require('./wireReactNative');
97
+ const {
98
+ wireWebOrb, reportWireResult, connectOrbRegistries, reportRegistriesResult,
99
+ connectOrbSession, reportSessionConnectResult,
100
+ } = require('./wireWeb');
101
+ const { wireReactNativeOrb, reportReactNativeWire, connectRnSession } = require('./wireReactNative');
99
102
  const { wireFlutterOrb, writeFlutterEnv, reportFlutterWire } = require('./wireFlutter');
100
103
  const { wireHandlers, reportHandlerResult } = require('./wireHandlers');
104
+ const { wireSession, reportSessionResult } = require('./wireSession');
101
105
  const { offerWriteEnv, printEnvManualReminder } = require('./writeEnv');
102
106
  const { scoreSurface, computeSurface } = require('./scoreSurface');
103
107
  const { buildRunRecord, writeChangeRegistry } = require('./changeRegistry');
@@ -113,6 +117,8 @@ function arg(flag, fallback) {
113
117
  const VALUE_FLAGS = new Set([
114
118
  '--out', '--model', '--max-input-tokens', '--platform', '--api-key', '--api-url',
115
119
  '--spec', '--analysis-root',
120
+ // from-odata (closed-software mode)
121
+ '--app-id', '--app-name', '--app-description', '--auth-ref', '--entities',
116
122
  ]);
117
123
 
118
124
  /**
@@ -251,6 +257,42 @@ async function withSpinner(label, work) {
251
257
  }
252
258
 
253
259
  async function main() {
260
+ // ── Closed-software mode: `fiodos from-odata <metadata.xml|url>` ────────────
261
+ // Generates the manifest from a third-party system's OData $metadata (schema,
262
+ // not source code), so the consent gate about code analysis does not apply.
263
+ {
264
+ const pos = positionalArgs();
265
+ if (pos[0] === 'from-odata') {
266
+ const target = pos[1];
267
+ if (!target) {
268
+ console.error('Usage: fiodos from-odata <metadata.xml | https://…/$metadata> --app-id <id> --app-name <name> [--entities a,b] [--auth-ref name] [--out dir] [--publish --api-key fyd_…]');
269
+ process.exit(1);
270
+ }
271
+ const { runFromOData } = require('./odata');
272
+ const safeName = path.basename(target).replace(/[^a-z0-9_-]/gi, '_') || 'odata';
273
+ const outDirOData = arg('--out', path.join(os.tmpdir(), 'fiodos', 'analysis', safeName));
274
+ const entitiesArg = arg('--entities', '');
275
+ await runFromOData({
276
+ target,
277
+ outDir: outDirOData,
278
+ opts: {
279
+ appId: arg('--app-id', `odata.${safeName}`),
280
+ appName: arg('--app-name', 'Third-party system'),
281
+ appDescription: arg('--app-description', ''),
282
+ authRef: arg('--auth-ref', 'default'),
283
+ entities: entitiesArg ? entitiesArg.split(',').map((s) => s.trim()).filter(Boolean) : [],
284
+ publish: process.argv.includes('--publish'),
285
+ },
286
+ io: {
287
+ logUser,
288
+ logDev,
289
+ publish: (manifest, meta) => publishManifest(manifest, meta),
290
+ },
291
+ });
292
+ return;
293
+ }
294
+ }
295
+
254
296
  // Consent gate: confirm before doing anything. `yes` continues; `no` (or any
255
297
  // other answer) cancels instantly — no analysis is run, the command just ends.
256
298
  const proceed = await askProceed();
@@ -353,11 +395,19 @@ async function main() {
353
395
  let provenance;
354
396
  let evidence = {};
355
397
  let wiring = {};
398
+ // The AI's "session" block: WHERE the app's auth/session state lives, so the
399
+ // orb's isAuthenticated/getUserId are wired automatically (no manual step).
400
+ let sessionWiring = null;
356
401
  let analysisMeta = { engine: 'auto-manifest-v3-ai-first', model: null, costUSD: 0 };
357
402
  // Signed proof returned by the hosted-analysis proxy: it tells the publish
358
403
  // step the quota was already consumed at /v1/developer/analyze (no double
359
404
  // count). Null in own-key / --no-llm mode.
360
405
  let analysisToken = null;
406
+ // Spec-requested content resets declared by the analysis AI: which dashboard
407
+ // context overrides (bubblePrompt/description/examples) the developer's
408
+ // --spec EXPLICITLY asked to regenerate, so the backend clears those (and
409
+ // ONLY those) masking overrides on publish. Null when the spec asked nothing.
410
+ let specResets = null;
361
411
 
362
412
  if (!useLLM) {
363
413
  // Static-only fallback: routes with mechanical naming, no actions.
@@ -410,6 +460,13 @@ async function main() {
410
460
  const proposed = parsed.manifest || {};
411
461
  evidence = parsed.evidence || {};
412
462
  wiring = parsed.wiring || {};
463
+ sessionWiring = parsed.session || null;
464
+ // Only meaningful when the developer passed --spec; the backend sanitizes
465
+ // (context fields only, real intents only) before touching any override.
466
+ specResets = (userSpec && parsed.specResets && typeof parsed.specResets === 'object')
467
+ ? parsed.specResets
468
+ : null;
469
+ if (specResets) logDev(`[ai] specResets=${JSON.stringify(specResets)}`);
413
470
  logDev(`[ai] model=${model} proposed routes=${(proposed.routes || []).length} ` +
414
471
  `actions=${(proposed.actions || []).length} in ${(elapsedMs / 1000).toFixed(1)}s`);
415
472
  logDev(`[ai] tokens in=${usage.prompt_tokens} out=${usage.completion_tokens} cost=$${costUSD.toFixed(4)}`);
@@ -427,7 +484,7 @@ async function main() {
427
484
  logUser('Analysis complete');
428
485
 
429
486
  fs.writeFileSync(path.join(outDir, 'evidence.json'), JSON.stringify({
430
- evidence, wiring, uncertain: parsed.uncertain || [],
487
+ evidence, wiring, session: sessionWiring, uncertain: parsed.uncertain || [],
431
488
  }, null, 2));
432
489
  fs.writeFileSync(path.join(outDir, 'usage.json'), JSON.stringify({
433
490
  model, elapsedMs, usage,
@@ -524,6 +581,14 @@ async function main() {
524
581
  disabledRouteIntents: surfaceScore.disabledRouteIntents || [],
525
582
  disabledActionIntents: surfaceScore.disabledActionIntents || [],
526
583
  },
584
+ // Present ONLY when the developer's --spec explicitly asked to regenerate
585
+ // previously-edited content (e.g. "rewrite all bubble prompts"): the backend
586
+ // clears the matching dashboard overrides so the fresh values become live.
587
+ ...(specResets ? { specResets } : {}),
588
+ // Opt-out of the backend's continuity guard (which keeps intent ids and
589
+ // URL paths stable across re-analyses so dashboard customizations never
590
+ // break). Only for developers who explicitly want a clean slate.
591
+ ...(process.argv.includes('--fresh-intents') ? { freshIntents: true } : {}),
527
592
  };
528
593
 
529
594
  // Informative, non-blocking summary (the dashboard is where scope is managed).
@@ -552,6 +617,7 @@ async function main() {
552
617
  let orbWireResult = null;
553
618
  let handlerWireResult = null;
554
619
  let registryWireResult = null;
620
+ let sessionWireResult = null;
555
621
  const { runPostWireTest } = require('./postWireTest');
556
622
  const selfCorrect = !process.argv.includes('--no-self-correct');
557
623
  // Corrections follow the same key model as the analysis: hosted (proxy)
@@ -653,6 +719,52 @@ async function main() {
653
719
  report(() => reportRegistriesResult(connected, fyodosTermColors(), { quiet: !isVerbose() }));
654
720
  }
655
721
 
722
+ // Third pass — SESSION wiring. Connect the orb to the app's real auth
723
+ // state so the agent KNOWS whether the user is signed in (requiresAuth
724
+ // gate) and keys conversation memory per user. Fully automatic: same AI
725
+ // proposal + mechanical verification + reversible edits + build safety
726
+ // net as handler wiring. NO manual step.
727
+ // · consent: the handler-wiring "yes" covers code edits, so we reuse
728
+ // it. If the developer said NO to handler wiring, we respect that
729
+ // and skip session edits too.
730
+ if (sessionWiring && !process.argv.includes('--no-orb-wire')) {
731
+ const handlerDeclined =
732
+ handlerResult &&
733
+ (handlerResult.status === 'declined' || handlerResult.status === 'declined-noninteractive');
734
+ if (!handlerDeclined) {
735
+ // Only an APPLIED handler wiring proves the developer said "yes" to
736
+ // code edits in every path; anything else makes wireSession ask its
737
+ // own single [yes/no] (or decline on a non-TTY) — never edit silently.
738
+ const handlerConsented =
739
+ handlerResult &&
740
+ (handlerResult.status === 'applied' || handlerResult.status === 'applied-untested');
741
+ const sessionResult = await wireSession(analysisRoot, {
742
+ session: sessionWiring,
743
+ colors: fyodosTermColors(),
744
+ quiet: !isVerbose(),
745
+ assumeYes: assumeYes || Boolean(handlerConsented),
746
+ noWire,
747
+ framework: 'web',
748
+ appName: manifest.appName,
749
+ runTest: !process.argv.includes('--no-wire-test'),
750
+ testRunner: runPostWireTest,
751
+ });
752
+ sessionWireResult = sessionResult;
753
+ report(() => reportSessionResult(sessionResult, fyodosTermColors(), { quiet: !isVerbose() }));
754
+ if (
755
+ orbMounted &&
756
+ (sessionResult.status === 'applied' || sessionResult.status === 'applied-untested')
757
+ ) {
758
+ const connectedSession = connectOrbSession(analysisRoot, {
759
+ sessionFileAbs: sessionResult.sessionFile,
760
+ });
761
+ report(() =>
762
+ reportSessionConnectResult(connectedSession, fyodosTermColors(), { quiet: !isVerbose() }),
763
+ );
764
+ }
765
+ }
766
+ }
767
+
656
768
  // Finalize the install proof: now that the (slow) action wiring is done,
657
769
  // tell the dashboard the terminal status so its "actions connected" tick
658
770
  // turns green for real — never before wiring actually finished. Only do
@@ -720,6 +832,7 @@ async function main() {
720
832
  orbResult: orbWireResult,
721
833
  handlerResult: handlerWireResult,
722
834
  registryResult: registryWireResult,
835
+ sessionResult: sessionWireResult,
723
836
  envResult: envWriteResult,
724
837
  });
725
838
  const paths = writeChangeRegistry(analysisRoot, record);
@@ -739,6 +852,7 @@ async function main() {
739
852
  const noWire = process.argv.includes('--no-wire');
740
853
  let orbWireResult = null;
741
854
  let envWriteResult = null;
855
+ let sessionWireResult = null;
742
856
 
743
857
  await withSpinner('Publishing to your project', async ({ setLabel, pause, resume }) => {
744
858
  await publishManifest(manifest, publishMeta, analysisToken, { skipSuccessLog: true });
@@ -771,6 +885,42 @@ async function main() {
771
885
  logDev(`[orb-wired] mobile install proof not recorded (${proof.status || proof.reason}); dashboard will rely on the runtime ping`);
772
886
  }
773
887
  }
888
+
889
+ // SESSION wiring (auth awareness) — same automatic pass as web. RN has
890
+ // no cheap full-build check, so the safety net is the mechanical symbol
891
+ // verification + the esbuild parse check on every edited file.
892
+ if (
893
+ sessionWiring &&
894
+ (orbWireResult.status === 'added' || orbWireResult.status === 'already')
895
+ ) {
896
+ const sessionResult = await wireSession(analysisRoot, {
897
+ session: sessionWiring,
898
+ colors: fyodosTermColors(),
899
+ quiet: !isVerbose(),
900
+ assumeYes: true, // the orb-mount consent already covered code edits on mobile
901
+ noWire,
902
+ framework: 'mobile',
903
+ appName: manifest.appName,
904
+ runTest: false,
905
+ });
906
+ sessionWireResult = sessionResult;
907
+ pause();
908
+ reportSessionResult(sessionResult, fyodosTermColors(), { quiet: !isVerbose() });
909
+ resume();
910
+ if (sessionResult.status === 'applied' || sessionResult.status === 'applied-untested') {
911
+ const connectedSession = connectRnSession(analysisRoot, {
912
+ sessionFileAbs: sessionResult.sessionFile,
913
+ colors: fyodosTermColors(),
914
+ });
915
+ if (connectedSession.status === 'connected') {
916
+ pause();
917
+ console.error(`${fyodosTermColors().cyan}◉${fyodosTermColors().reset} ${fyodosTermColors().blue}Fiodos${fyodosTermColors().reset} · session connected to the orb mount (${connectedSession.file})`);
918
+ resume();
919
+ } else {
920
+ logDev(`[session] RN mount connection: ${connectedSession.status}${connectedSession.reason ? ` (${connectedSession.reason})` : ''}`);
921
+ }
922
+ }
923
+ }
774
924
  }
775
925
  });
776
926
  logUser('Published to your project');
@@ -805,6 +955,7 @@ async function main() {
805
955
  manifest,
806
956
  publishMeta,
807
957
  orbResult: orbWireResult,
958
+ sessionResult: sessionWireResult,
808
959
  envResult: envWriteResult,
809
960
  });
810
961
  const paths = writeChangeRegistry(analysisRoot, record);
@@ -1377,6 +1528,21 @@ async function publishManifest(manifest, meta, analysisToken = null, opts = {})
1377
1528
  if (!opts.skipSuccessLog) {
1378
1529
  logUser('Published to your project');
1379
1530
  }
1531
+ if (body.overridesCleared > 0) {
1532
+ logUser(
1533
+ `Your specifications asked to regenerate previously-edited content: ` +
1534
+ `${body.overridesCleared} dashboard override(s) were cleared so the new values are live`,
1535
+ );
1536
+ }
1537
+ // Continuity guard feedback: the backend kept the previous intent ids /
1538
+ // repaired degraded route paths, so dashboard customizations keep working.
1539
+ if (body.intentsPreserved > 0 || body.pathsRepaired > 0) {
1540
+ logUser(
1541
+ `Continuity with your previous analysis: ${body.intentsPreserved || 0} intent id(s) kept stable` +
1542
+ (body.pathsRepaired ? `, ${body.pathsRepaired} route path(s) repaired` : '') +
1543
+ ` — your dashboard customizations stay intact (use --fresh-intents to opt out)`,
1544
+ );
1545
+ }
1380
1546
  logDev(`[publish] OK — ${body.routes} routes, ${body.actions} actions received at ${body.receivedAt}`);
1381
1547
 
1382
1548
  // Self-check: confirm the orb will find this manifest (silent unless --verbose).