@fiodos/cli 0.1.29 → 0.1.31

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.29",
3
+ "version": "0.1.31",
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": {
@@ -21,7 +21,7 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "dependencies": {
24
- "@fiodos/core": "^0.1.10",
24
+ "@fiodos/core": "^0.1.13",
25
25
  "esbuild": "^0.28.1",
26
26
  "jsdom": "^24.0.0",
27
27
  "typescript": "^5.6.0"
package/src/collect.js CHANGED
@@ -80,14 +80,27 @@ function tierFor(rel, platform) {
80
80
  if (/^sections\//.test(p)) return 2;
81
81
  return 3; // snippets/, config/ (settings_data.json can be huge), rest
82
82
  }
83
+ return tierForApp(p);
84
+ }
85
+
86
+ function tierForApp(p) {
83
87
  if (/^(src\/)?app\//.test(p)) return 0; // expo-router / Next screens
84
- if (/(^|\/)(screens?|pages?|views?|routes?|activities|fragments)\//i.test(p)) return 0;
85
- if (/(screen|page|view|activity|fragment)\.(dart|kt|swift)$/i.test(p)) return 0;
88
+ // `sections/` and `panels/` are SCREEN BODIES in shell-based apps (a Shell
89
+ // page switches between section components): they carry the visible controls
90
+ // (color pickers, toggles) the analyzer must read to emit their actions.
91
+ // Ranking them as generic components (tier 3) silently dropped them under
92
+ // budget pressure and whole screens became invisible to the AI.
93
+ if (/(^|\/)(screens?|pages?|views?|routes?|sections?|panels?|activities|fragments)\//i.test(p)) return 0;
94
+ if (/(screen|page|view|activity|fragment|section)\.(dart|kt|swift)$/i.test(p)) return 0;
95
+ // Central state gets its OWN tier right after screens: the providers/stores
96
+ // define the setters that make screen controls wire-able actions. Sharing a
97
+ // tier with the (numerous) service/lib files let alphabetical fill starve
98
+ // out AppProvider-style files, and the AI had to guess every handler name.
86
99
  if (/(^|\/)(state|store|stores|context|contexts|redux|providers?|blocs?|cubits?|viewmodels?)\//i.test(p)) return 1;
87
100
  if (/(viewmodel|notifier|bloc|cubit|store|controller)\.(dart|kt|swift)$/i.test(p)) return 1;
88
- if (/(^|\/)(services?|api|db|data|domain|agent|handlers?|actions?|lib|utils|repositories|repository|usecases?)\//i.test(p)) return 1;
89
- if (/(^|\/)(hooks)\//.test(p)) return 2;
90
- return 3; // generic components / ui
101
+ if (/(^|\/)(services?|api|db|data|domain|agent|handlers?|actions?|lib|utils|repositories|repository|usecases?)\//i.test(p)) return 2;
102
+ if (/(^|\/)(hooks)\//.test(p)) return 3;
103
+ return 4; // generic components / ui
91
104
  }
92
105
 
93
106
  function collectFiles(appRoot, { maxInputTokens = 150_000, platform = 'mobile' } = {}) {
@@ -108,7 +121,21 @@ function collectFiles(appRoot, { maxInputTokens = 150_000, platform = 'mobile' }
108
121
  if (!codeExt.has(path.extname(entry.name))) continue;
109
122
  if (SKIP_FILES.test(rel)) continue;
110
123
  if (isNative && SKIP_NATIVE_FILES.test(rel)) continue;
111
- const content = fs.readFileSync(full, 'utf8');
124
+ let content = fs.readFileSync(full, 'utf8');
125
+ if (platform === 'shopify' && /^locales\//.test(rel.replace(/\\/g, '/'))) {
126
+ // Theme-editor translations (merchant-facing settings strings), not
127
+ // storefront language evidence — pure noise for the analysis.
128
+ if (/\.schema\.json$/.test(rel)) continue;
129
+ // Non-default storefront locales: what matters is THAT the language is
130
+ // published (the filename carries the code) — that is the evidence the
131
+ // MULTILINGUAL rule needs to emit appLanguages/bubblePrompts. Sending
132
+ // each file whole (tens of KB × dozens of locales) blew the budget and
133
+ // the alphabetical cut dropped exactly the important ones (en/es
134
+ // arrived after bg/cs/da…), so the AI saw a "monolingual" store.
135
+ if (!/\.default\.json$/.test(rel)) {
136
+ content = '{"__fyodos_note": "published storefront locale (content elided; this language IS offered to shoppers — count it in appLanguages/bubblePrompts)"}';
137
+ }
138
+ }
112
139
  files.push({ rel, content, tier: tierFor(rel, platform), chars: content.length });
113
140
  }
114
141
  };
@@ -118,10 +145,48 @@ function collectFiles(appRoot, { maxInputTokens = 150_000, platform = 'mobile' }
118
145
  files.sort((a, b) => a.tier - b.tier || a.rel.localeCompare(b.rel));
119
146
 
120
147
  const budgetChars = maxInputTokens * CHARS_PER_TOKEN;
148
+
149
+ // Floor-reserved fill. A pure greedy fill let a fat tier 0 (many large
150
+ // screen files) consume the WHOLE budget and push out tier 1 entirely —
151
+ // i.e. the app's central state/providers (AppProvider, stores) vanished
152
+ // from the analysis, so the AI could not see the setters that make
153
+ // configuration controls real actions and had to guess every handler name.
154
+ // Each lower tier gets a guaranteed FLOOR (share of the total budget,
155
+ // capped at its real demand); screens still get everything left over, and a
156
+ // final backfill hands unused room to deferred files in tier order.
157
+ const FLOOR_SHARE = { 1: 0.2, 2: 0.05, 3: 0.02, 4: 0.02 };
158
+ const demand = new Map();
159
+ for (const f of files) demand.set(f.tier, (demand.get(f.tier) ?? 0) + f.chars);
160
+
121
161
  const included = [];
122
- const omitted = [];
162
+ const deferred = [];
123
163
  let used = 0;
124
- for (const f of files) {
164
+ const tiers = [...demand.keys()].sort((a, b) => a - b);
165
+ for (const tier of tiers) {
166
+ // Reserve, for every LOWER-priority tier that still has files, the smaller
167
+ // of its floor and what it actually needs.
168
+ let reserved = 0;
169
+ for (const t of tiers) {
170
+ if (t <= tier) continue;
171
+ reserved += Math.min((FLOOR_SHARE[t] ?? 0) * budgetChars, demand.get(t));
172
+ }
173
+ const available = budgetChars - used - reserved;
174
+ let tierUsed = 0;
175
+ for (const f of files) {
176
+ if (f.tier !== tier) continue;
177
+ if (tierUsed + f.chars <= available) {
178
+ included.push(f);
179
+ tierUsed += f.chars;
180
+ } else {
181
+ deferred.push(f);
182
+ }
183
+ }
184
+ used += tierUsed;
185
+ }
186
+ // Backfill: any room the floors did not consume goes back to the deferred
187
+ // files, highest tier priority first.
188
+ const omitted = [];
189
+ for (const f of deferred) {
125
190
  if (used + f.chars <= budgetChars) {
126
191
  included.push(f);
127
192
  used += f.chars;
@@ -129,6 +194,8 @@ function collectFiles(appRoot, { maxInputTokens = 150_000, platform = 'mobile' }
129
194
  omitted.push({ rel: f.rel, chars: f.chars, tier: f.tier });
130
195
  }
131
196
  }
197
+ // Restore deterministic tier/path order after the backfill.
198
+ included.sort((a, b) => a.tier - b.tier || a.rel.localeCompare(b.rel));
132
199
 
133
200
  return {
134
201
  included,
@@ -953,13 +953,20 @@ function buildRegistryModule(plan, opts = {}) {
953
953
  // Shared runner: one place normalizes any wired call into an action result,
954
954
  // so each handler below stays a one-liner. `unknown` keeps the truthiness
955
955
  // test legal even when the wired call returns void (e.g. db.delete(...)).
956
+ // "is not a function" here means a BRIDGE method whose host screen is not
957
+ // mounted yet — the SDK executor recognizes FYODOS_HANDLER_UNREADY, walks
958
+ // the user to the screen that hosts the action and retries automatically.
956
959
  const runHelper = ts
957
960
  ? 'async function __fyodosRun(call: () => unknown): Promise<FiodosActionResult> {\n' +
958
961
  ' try {\n' +
959
962
  ' const result: unknown = await Promise.resolve(call());\n' +
960
963
  " return { success: true, data: (result && typeof result === 'object') ? result as Record<string, unknown> : { result } };\n" +
961
964
  ' } catch (err) {\n' +
962
- ' return { success: false, error: err instanceof Error ? err.message : String(err) };\n' +
965
+ ' const msg = err instanceof Error ? err.message : String(err);\n' +
966
+ " if (err instanceof TypeError && /is not a function/.test(msg)) {\n" +
967
+ " return { success: false, error: 'FYODOS_HANDLER_UNREADY: ' + msg };\n" +
968
+ ' }\n' +
969
+ ' return { success: false, error: msg };\n' +
963
970
  ' }\n' +
964
971
  '}\n'
965
972
  : 'async function __fyodosRun(call) {\n' +
@@ -967,7 +974,11 @@ function buildRegistryModule(plan, opts = {}) {
967
974
  ' const result = await Promise.resolve(call());\n' +
968
975
  " return { success: true, data: (result && typeof result === 'object') ? result : { result } };\n" +
969
976
  ' } catch (err) {\n' +
970
- ' return { success: false, error: err instanceof Error ? err.message : String(err) };\n' +
977
+ ' const msg = err instanceof Error ? err.message : String(err);\n' +
978
+ " if (err instanceof TypeError && /is not a function/.test(msg)) {\n" +
979
+ " return { success: false, error: 'FYODOS_HANDLER_UNREADY: ' + msg };\n" +
980
+ ' }\n' +
981
+ ' return { success: false, error: msg };\n' +
971
982
  ' }\n' +
972
983
  '}\n';
973
984
 
@@ -1334,9 +1345,14 @@ function stripFiodosBlocks(content) {
1334
1345
  );
1335
1346
  }
1336
1347
 
1337
- /** Insert an import block right after the file's last top-level import line. */
1348
+ /** Insert an import block right after the file's last top-level import statement.
1349
+ * Matches the WHOLE statement through its module string — a per-line regex put
1350
+ * the block INSIDE multi-line imports (`import {\n a,\n} from 'x';`), corrupting
1351
+ * the file (TS1003 "Identifier expected") and failing the wiring of every action
1352
+ * in that file. `[^'"]*` cannot skip a quote, so the match always stops at the
1353
+ * first quoted module specifier, then runs to that line's end. */
1338
1354
  function insertImportBlock(content, importBlock) {
1339
- const importLineRe = /^[ \t]*import\b[^\n]*\n/gm;
1355
+ const importLineRe = /^[ \t]*import\b[^'"]*['"][^'"]+['"][^\n]*\n/gm;
1340
1356
  let lastEnd = -1;
1341
1357
  let m;
1342
1358
  while ((m = importLineRe.exec(content))) lastEnd = m.index + m[0].length;