@fiodos/cli 0.1.22 → 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 +5 -3
- package/src/aiAnalyze.js +151 -274
- package/src/changeRegistry.js +57 -6
- package/src/index.js +338 -5
- package/src/odata.js +529 -0
- package/src/wireHandlers.js +120 -34
- package/src/wireReactNative.js +52 -0
- package/src/wireSession.js +619 -0
- package/src/wireWeb.js +102 -0
- package/src/wireWebMount.js +91 -8
- package/src/llm.js +0 -144
package/src/wireWeb.js
CHANGED
|
@@ -27,6 +27,8 @@ const {
|
|
|
27
27
|
registryRelImport,
|
|
28
28
|
addRegistriesToMountSource,
|
|
29
29
|
addRegistriesToBootstrapSource,
|
|
30
|
+
addSessionToMountSource,
|
|
31
|
+
addSessionToBootstrapSource,
|
|
30
32
|
buildEntryBootstrapBlock,
|
|
31
33
|
IMPORT_NAME,
|
|
32
34
|
WRAPPER_BASENAME,
|
|
@@ -499,6 +501,104 @@ function connectOrbRegistries(appRoot, opts = {}) {
|
|
|
499
501
|
}
|
|
500
502
|
}
|
|
501
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Third pass — connect the generated SESSION module to the mounted orb, so the
|
|
506
|
+
* agent KNOWS whether the user is signed in (requiresAuth gate) and keys the
|
|
507
|
+
* conversation memory per user. Mirrors connectOrbRegistries: idempotent,
|
|
508
|
+
* reversible, never overrides a developer-wired isAuthenticated.
|
|
509
|
+
*/
|
|
510
|
+
function connectOrbSession(appRoot, opts = {}) {
|
|
511
|
+
const { sessionFileAbs } = opts;
|
|
512
|
+
if (!sessionFileAbs || !fs.existsSync(sessionFileAbs)) return { status: 'no-session-file' };
|
|
513
|
+
|
|
514
|
+
const framework = detectFramework(appRoot);
|
|
515
|
+
const target = detectTarget(appRoot, framework);
|
|
516
|
+
if (!target || !target.file) return { status: 'no-target' };
|
|
517
|
+
|
|
518
|
+
const bootstrapKinds = new Set(['vue-entry', 'svelte-entry', 'sveltekit-hooks']);
|
|
519
|
+
if (bootstrapKinds.has(target.kind)) {
|
|
520
|
+
let source;
|
|
521
|
+
try {
|
|
522
|
+
source = fs.readFileSync(target.file, 'utf8');
|
|
523
|
+
} catch {
|
|
524
|
+
return { status: 'not-mounted' };
|
|
525
|
+
}
|
|
526
|
+
const importPath = registryRelImport(target.file, sessionFileAbs);
|
|
527
|
+
const fileRel = path.relative(appRoot, target.file);
|
|
528
|
+
const backups = backupFiles([target.file]);
|
|
529
|
+
try {
|
|
530
|
+
const res = addSessionToBootstrapSource(source, importPath);
|
|
531
|
+
if (!res.changed) {
|
|
532
|
+
return { status: res.reason === 'already' ? 'already' : res.reason || 'not-mounted', file: fileRel };
|
|
533
|
+
}
|
|
534
|
+
fs.writeFileSync(target.file, res.source);
|
|
535
|
+
return { status: 'connected', file: fileRel };
|
|
536
|
+
} catch (e) {
|
|
537
|
+
revertFiles(backups);
|
|
538
|
+
return { status: 'failed', file: fileRel, error: e };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const reactKinds = new Set(['vite', 'next-pages', 'next-app']);
|
|
543
|
+
if (!reactKinds.has(target.kind)) return { status: 'unsupported-framework', framework };
|
|
544
|
+
|
|
545
|
+
let mountFile = target.file;
|
|
546
|
+
if (target.kind === 'next-app') {
|
|
547
|
+
const dir = path.dirname(target.file);
|
|
548
|
+
mountFile =
|
|
549
|
+
[path.join(dir, `${WRAPPER_BASENAME}.tsx`), path.join(dir, `${WRAPPER_BASENAME}.jsx`)].find((f) =>
|
|
550
|
+
fs.existsSync(f),
|
|
551
|
+
) || null;
|
|
552
|
+
if (!mountFile) return { status: 'not-mounted' };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let source;
|
|
556
|
+
try {
|
|
557
|
+
source = fs.readFileSync(mountFile, 'utf8');
|
|
558
|
+
} catch {
|
|
559
|
+
return { status: 'not-mounted' };
|
|
560
|
+
}
|
|
561
|
+
if (!/<FiodosAgent\b/.test(source)) return { status: 'not-mounted' };
|
|
562
|
+
|
|
563
|
+
const importPath = registryRelImport(mountFile, sessionFileAbs);
|
|
564
|
+
const fileRel = path.relative(appRoot, mountFile);
|
|
565
|
+
const backups = backupFiles([mountFile]);
|
|
566
|
+
try {
|
|
567
|
+
const res = addSessionToMountSource(source, importPath);
|
|
568
|
+
if (!res.changed) {
|
|
569
|
+
return { status: res.reason || 'not-mounted', file: fileRel };
|
|
570
|
+
}
|
|
571
|
+
fs.writeFileSync(mountFile, res.source);
|
|
572
|
+
return { status: 'connected', file: fileRel };
|
|
573
|
+
} catch (e) {
|
|
574
|
+
revertFiles(backups);
|
|
575
|
+
return { status: 'failed', file: fileRel, error: e };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function reportSessionConnectResult(result, colors = {}, opts = {}) {
|
|
580
|
+
const quiet = opts.quiet !== false;
|
|
581
|
+
if (!result) return;
|
|
582
|
+
const { blue, cyan, dim, reset } = colors;
|
|
583
|
+
const tag = `${cyan || ''}◉${reset || ''} ${blue || ''}Fiodos${reset || ''}`;
|
|
584
|
+
switch (result.status) {
|
|
585
|
+
case 'connected':
|
|
586
|
+
console.error(`${tag} · session connected to the orb mount (${result.file})`);
|
|
587
|
+
break;
|
|
588
|
+
case 'already':
|
|
589
|
+
if (!quiet) console.error(`${tag} · ${dim || ''}session already connected to the mount${reset || ''}`);
|
|
590
|
+
break;
|
|
591
|
+
case 'dev-wired':
|
|
592
|
+
if (!quiet) {
|
|
593
|
+
console.error(`${tag} · ${dim || ''}the mount already has its own isAuthenticated — left as is${reset || ''}`);
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
default:
|
|
597
|
+
if (!quiet) console.error(`${tag} · ${dim || ''}session not connected to the mount (${result.status})${reset || ''}`);
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
502
602
|
function reportRegistriesResult(result, colors = {}, opts = {}) {
|
|
503
603
|
const quiet = opts.quiet !== false;
|
|
504
604
|
if (!result) return;
|
|
@@ -530,6 +630,8 @@ module.exports = {
|
|
|
530
630
|
reportWireResult,
|
|
531
631
|
connectOrbRegistries,
|
|
532
632
|
reportRegistriesResult,
|
|
633
|
+
connectOrbSession,
|
|
634
|
+
reportSessionConnectResult,
|
|
533
635
|
detectFramework,
|
|
534
636
|
detectTarget,
|
|
535
637
|
};
|
package/src/wireWebMount.js
CHANGED
|
@@ -83,19 +83,32 @@ function insertBlockAfterLastImport(source, block) {
|
|
|
83
83
|
// FYODOS:ORB markers → idempotent (re-runs strip & rewrite) and reversible.
|
|
84
84
|
const BOOTSTRAP_ALIAS = '__fiodosCreateAgent';
|
|
85
85
|
const BOOTSTRAP_REGISTRY_EXPORT = 'fyodosGeneratedRegistries';
|
|
86
|
+
// Exports of the generated session module (see wireSession.js). Connected to
|
|
87
|
+
// the mount automatically so auth awareness needs NO manual step.
|
|
88
|
+
const SESSION_IS_AUTH_EXPORT = 'fyodosIsAuthenticated';
|
|
89
|
+
const SESSION_USER_ID_EXPORT = 'fyodosGetUserId';
|
|
86
90
|
|
|
87
91
|
function buildEntryBootstrapBlock(opts = {}) {
|
|
88
|
-
const { registryImportPath } = opts;
|
|
92
|
+
const { registryImportPath, sessionImportPath } = opts;
|
|
89
93
|
const lines = [];
|
|
90
94
|
lines.push(ORB_SCRIPT_START);
|
|
91
95
|
lines.push(`import { createFiodosAgent as ${BOOTSTRAP_ALIAS} } from '@fiodos/web-core';`);
|
|
92
96
|
if (registryImportPath) {
|
|
93
97
|
lines.push(`import { ${BOOTSTRAP_REGISTRY_EXPORT} } from '${registryImportPath}';`);
|
|
94
98
|
}
|
|
99
|
+
if (sessionImportPath) {
|
|
100
|
+
lines.push(
|
|
101
|
+
`import { ${SESSION_IS_AUTH_EXPORT}, ${SESSION_USER_ID_EXPORT} } from '${sessionImportPath}';`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
95
104
|
lines.push(`${BOOTSTRAP_ALIAS}({`);
|
|
96
105
|
lines.push(' apiKey: import.meta.env.VITE_FYODOS_API_KEY,');
|
|
97
106
|
lines.push(' baseUrl: import.meta.env.VITE_FYODOS_API_URL,');
|
|
98
107
|
if (registryImportPath) lines.push(` registries: ${BOOTSTRAP_REGISTRY_EXPORT},`);
|
|
108
|
+
if (sessionImportPath) {
|
|
109
|
+
lines.push(` isAuthenticated: ${SESSION_IS_AUTH_EXPORT},`);
|
|
110
|
+
lines.push(` getUserId: ${SESSION_USER_ID_EXPORT},`);
|
|
111
|
+
}
|
|
99
112
|
lines.push(' mount: true,');
|
|
100
113
|
lines.push('});');
|
|
101
114
|
lines.push(ORB_SCRIPT_END);
|
|
@@ -123,8 +136,35 @@ function addRegistriesToBootstrapSource(source, registryImportPath) {
|
|
|
123
136
|
if (/\n\s*registries:\s/.test(source.slice(source.indexOf(ORB_SCRIPT_START)))) {
|
|
124
137
|
return { changed: false, source, reason: 'already' };
|
|
125
138
|
}
|
|
139
|
+
// Preserve a session connection a previous pass already added to the block.
|
|
140
|
+
const sessionImportPath = extractBootstrapImportPath(source, SESSION_IS_AUTH_EXPORT);
|
|
141
|
+
const stripped = stripMarkedRegion(source, ORB_SCRIPT_START, ORB_SCRIPT_END).replace(/\n{3,}$/, '\n');
|
|
142
|
+
const block = buildEntryBootstrapBlock({ registryImportPath, sessionImportPath });
|
|
143
|
+
const next = insertBlockAfterLastImport(stripped.trimEnd(), block);
|
|
144
|
+
return { changed: true, source: next.endsWith('\n') ? next : `${next}\n` };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Import path of the module that exports `name` inside our bootstrap block (or null).
|
|
148
|
+
function extractBootstrapImportPath(source, name) {
|
|
149
|
+
const startIdx = source.indexOf(ORB_SCRIPT_START);
|
|
150
|
+
if (startIdx === -1) return null;
|
|
151
|
+
const endIdx = source.indexOf(ORB_SCRIPT_END, startIdx);
|
|
152
|
+
const block = source.slice(startIdx, endIdx === -1 ? undefined : endIdx);
|
|
153
|
+
const m = block.match(new RegExp(`import\\s*\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`));
|
|
154
|
+
return m ? m[1] : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Idempotently rewrite an existing entry-bootstrap block to carry the generated
|
|
158
|
+
// SESSION connection (third pass, after session wiring). Preserves the
|
|
159
|
+
// registries the second pass added. Returns { changed, source, reason? }.
|
|
160
|
+
function addSessionToBootstrapSource(source, sessionImportPath) {
|
|
161
|
+
if (!source.includes(ORB_SCRIPT_START)) return { changed: false, source, reason: 'not-mounted' };
|
|
162
|
+
if (/\n\s*isAuthenticated:\s/.test(source.slice(source.indexOf(ORB_SCRIPT_START)))) {
|
|
163
|
+
return { changed: false, source, reason: 'already' };
|
|
164
|
+
}
|
|
165
|
+
const registryImportPath = extractBootstrapImportPath(source, BOOTSTRAP_REGISTRY_EXPORT);
|
|
126
166
|
const stripped = stripMarkedRegion(source, ORB_SCRIPT_START, ORB_SCRIPT_END).replace(/\n{3,}$/, '\n');
|
|
127
|
-
const block = buildEntryBootstrapBlock({ registryImportPath });
|
|
167
|
+
const block = buildEntryBootstrapBlock({ registryImportPath, sessionImportPath });
|
|
128
168
|
const next = insertBlockAfterLastImport(stripped.trimEnd(), block);
|
|
129
169
|
return { changed: true, source: next.endsWith('\n') ? next : `${next}\n` };
|
|
130
170
|
}
|
|
@@ -199,6 +239,41 @@ function addRegistriesToMountSource(source, exportName, importPath) {
|
|
|
199
239
|
return { changed: true, source: next };
|
|
200
240
|
}
|
|
201
241
|
|
|
242
|
+
// Idempotently connect the generated SESSION module to the mounted
|
|
243
|
+
// <FiodosAgent/>: adds `isAuthenticated={fyodosIsAuthenticated}` and
|
|
244
|
+
// `getUserId={fyodosGetUserId}` + the import. Skips (never overrides) a mount
|
|
245
|
+
// where the developer already wired their own isAuthenticated. Same
|
|
246
|
+
// reversibility model as addRegistriesToMountSource.
|
|
247
|
+
function addSessionToMountSource(source, importPath) {
|
|
248
|
+
const tagRe = /([ \t]*)<FiodosAgent\b[\s\S]*?\/>/;
|
|
249
|
+
const m = source.match(tagRe);
|
|
250
|
+
if (!m) return { changed: false, source, reason: 'not-mounted' };
|
|
251
|
+
const tag = m[0];
|
|
252
|
+
if (new RegExp(`isAuthenticated=\\{${escapeRe(SESSION_IS_AUTH_EXPORT)}\\}`).test(tag)) {
|
|
253
|
+
return { changed: false, source, reason: 'already' };
|
|
254
|
+
}
|
|
255
|
+
if (/\bisAuthenticated=/.test(tag)) {
|
|
256
|
+
return { changed: false, source, reason: 'dev-wired' };
|
|
257
|
+
}
|
|
258
|
+
const baseIndent = m[1] || '';
|
|
259
|
+
const propMatch = tag.match(/\n([ \t]*)(?:apiKey|api-key|apiUrl|baseUrl|registries)/);
|
|
260
|
+
const propIndent = propMatch ? propMatch[1] : `${baseIndent} `;
|
|
261
|
+
const hasUserId = /\bgetUserId=/.test(tag);
|
|
262
|
+
const props =
|
|
263
|
+
`\n${propIndent}isAuthenticated={${SESSION_IS_AUTH_EXPORT}}` +
|
|
264
|
+
(hasUserId ? '' : `\n${propIndent}getUserId={${SESSION_USER_ID_EXPORT}}`);
|
|
265
|
+
const newTag = tag.replace(/\n?[ \t]*\/>$/, `${props}\n${baseIndent}/>`);
|
|
266
|
+
let next = source.replace(tag, newTag);
|
|
267
|
+
const names = hasUserId
|
|
268
|
+
? SESSION_IS_AUTH_EXPORT
|
|
269
|
+
: `${SESSION_IS_AUTH_EXPORT}, ${SESSION_USER_ID_EXPORT}`;
|
|
270
|
+
const importLine = `import { ${names} } from '${importPath}';`;
|
|
271
|
+
if (!next.includes(importLine)) {
|
|
272
|
+
next = insertImportAfterLastImport(next, importLine);
|
|
273
|
+
}
|
|
274
|
+
return { changed: true, source: next };
|
|
275
|
+
}
|
|
276
|
+
|
|
202
277
|
/**
|
|
203
278
|
* Assess whether we can inject safely. Returns { ok, reason?, strategy, files, rel }.
|
|
204
279
|
*/
|
|
@@ -601,13 +676,15 @@ function writeConsentDoc(appRoot, plan) {
|
|
|
601
676
|
'Edits sit between `FYODOS:ORB:START` / `FYODOS:ORB:END` markers. Remove those',
|
|
602
677
|
'blocks (and the Fiodos import) to revert.',
|
|
603
678
|
'',
|
|
604
|
-
'## Apps with login (
|
|
679
|
+
'## Apps with login (auth + identity — wired AUTOMATICALLY)',
|
|
680
|
+
'',
|
|
681
|
+
'For apps with login the installer also detects WHERE your session state lives',
|
|
682
|
+
'and connects it on its own: it generates `src/fyodos/session.generated.*` and',
|
|
683
|
+
'adds `isAuthenticated={fyodosIsAuthenticated}` / `getUserId={fyodosGetUserId}`',
|
|
684
|
+
'to the mount (see `src/fyodos/FYODOS_SESSION.md`). Nothing to do by hand.',
|
|
605
685
|
'',
|
|
606
|
-
'
|
|
607
|
-
'
|
|
608
|
-
'connect that in one place. Without it the agent can loop ("do you want to sign',
|
|
609
|
-
"in?\") and can't tell it already signed you in. Add these props to",
|
|
610
|
-
'`<FiodosAgent/>` (a commented scaffold is already next to the mount):',
|
|
686
|
+
'ONLY if the installer reported it could not verify your session source (or you',
|
|
687
|
+
'want to override it), wire the props yourself:',
|
|
611
688
|
'',
|
|
612
689
|
'```tsx',
|
|
613
690
|
'<FiodosAgent',
|
|
@@ -619,6 +696,10 @@ function writeConsentDoc(appRoot, plan) {
|
|
|
619
696
|
' getCurrentRoute={() => window.location.pathname}',
|
|
620
697
|
' // optional one-line human context (session + screen):',
|
|
621
698
|
" getUserContextText={() => (session ? 'Session: signed in.' : 'Session: signed out.')}",
|
|
699
|
+
' // RECOMMENDED: a stable id for the signed-in user (any string; it is',
|
|
700
|
+
' // hashed locally). Keys the conversation memory per user, so a logout or',
|
|
701
|
+
' // account switch NEVER leaks the previous conversation on shared devices:',
|
|
702
|
+
' getUserId={() => session?.userId ?? null}',
|
|
622
703
|
'/>',
|
|
623
704
|
'```',
|
|
624
705
|
'',
|
|
@@ -649,6 +730,8 @@ module.exports = {
|
|
|
649
730
|
registryRelImport,
|
|
650
731
|
addRegistriesToMountSource,
|
|
651
732
|
addRegistriesToBootstrapSource,
|
|
733
|
+
addSessionToMountSource,
|
|
734
|
+
addSessionToBootstrapSource,
|
|
652
735
|
buildEntryBootstrapBlock,
|
|
653
736
|
IMPORT_NAME,
|
|
654
737
|
WRAPPER_BASENAME,
|
package/src/llm.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 3 — EXPLICIT LLM calls inside the pipeline.
|
|
3
|
-
*
|
|
4
|
-
* The static layers (1+2) extract facts; this layer makes judgment calls the
|
|
5
|
-
* code cannot express, and every call is LOGGED so the final report can show
|
|
6
|
-
* exactly which decisions came from the model vs from static analysis:
|
|
7
|
-
*
|
|
8
|
-
* 1. classifyApp — app type vs the pattern catalog
|
|
9
|
-
* 2. curateActions — which detected callables deserve agent exposure,
|
|
10
|
-
* category, sensitivity, parameters, label
|
|
11
|
-
* 3. writeRouteMeta — intent ids, labels and example phrases for routes
|
|
12
|
-
* 4. patternGapFill — given the matched pattern, propose what's missing
|
|
13
|
-
*
|
|
14
|
-
* Model: gpt-4o-mini via the OpenAI REST API (OPENAI_API_KEY env var).
|
|
15
|
-
*/
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
const LLM_LOG = [];
|
|
19
|
-
|
|
20
|
-
function getLog() {
|
|
21
|
-
return LLM_LOG;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function callLLM(taskName, system, user, { maxTokens = 2000 } = {}) {
|
|
25
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
26
|
-
if (!apiKey) throw new Error('OPENAI_API_KEY is required for the LLM layer');
|
|
27
|
-
|
|
28
|
-
const t0 = Date.now();
|
|
29
|
-
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
30
|
-
method: 'POST',
|
|
31
|
-
headers: {
|
|
32
|
-
'Content-Type': 'application/json',
|
|
33
|
-
Authorization: `Bearer ${apiKey}`,
|
|
34
|
-
},
|
|
35
|
-
body: JSON.stringify({
|
|
36
|
-
model: process.env.AUTO_MANIFEST_MODEL || 'gpt-4o-mini',
|
|
37
|
-
messages: [
|
|
38
|
-
{ role: 'system', content: system },
|
|
39
|
-
{ role: 'user', content: user },
|
|
40
|
-
],
|
|
41
|
-
temperature: 0.2,
|
|
42
|
-
max_tokens: maxTokens,
|
|
43
|
-
response_format: { type: 'json_object' },
|
|
44
|
-
}),
|
|
45
|
-
});
|
|
46
|
-
if (!res.ok) {
|
|
47
|
-
const body = await res.text();
|
|
48
|
-
throw new Error(`LLM call '${taskName}' failed: ${res.status} ${body.slice(0, 300)}`);
|
|
49
|
-
}
|
|
50
|
-
const data = await res.json();
|
|
51
|
-
const content = data.choices?.[0]?.message?.content || '{}';
|
|
52
|
-
let parsed;
|
|
53
|
-
try {
|
|
54
|
-
parsed = JSON.parse(content);
|
|
55
|
-
} catch {
|
|
56
|
-
throw new Error(`LLM call '${taskName}' returned non-JSON output`);
|
|
57
|
-
}
|
|
58
|
-
LLM_LOG.push({
|
|
59
|
-
task: taskName,
|
|
60
|
-
ms: Date.now() - t0,
|
|
61
|
-
promptChars: system.length + user.length,
|
|
62
|
-
usage: data.usage || null,
|
|
63
|
-
});
|
|
64
|
-
return parsed;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 1. Classify the app against the available pattern catalog. */
|
|
68
|
-
async function classifyApp(appSummary, patternSummaries) {
|
|
69
|
-
const system =
|
|
70
|
-
'You classify a mobile app into one of the available integration patterns, or "none". ' +
|
|
71
|
-
'Answer JSON: {"pattern": "<id-or-none>", "confidence": 0..1, "appKind": "<free text>", "reason": "<short>"}';
|
|
72
|
-
const user =
|
|
73
|
-
`Available patterns:\n${patternSummaries.map((p) => `- id=${p.id}: ${p.summary}`).join('\n')}\n\n` +
|
|
74
|
-
`App evidence (from static analysis):\n${JSON.stringify(appSummary, null, 1).slice(0, 4000)}`;
|
|
75
|
-
return callLLM('classifyApp', system, user, { maxTokens: 300 });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 2. Curate detected callables into agent actions.
|
|
80
|
-
* Batched: large candidate lists are processed in chunks so no candidate is
|
|
81
|
-
* silently dropped by prompt-size truncation.
|
|
82
|
-
*/
|
|
83
|
-
async function curateActions(appKind, candidates) {
|
|
84
|
-
const BATCH = 14;
|
|
85
|
-
const all = [];
|
|
86
|
-
for (let i = 0; i < candidates.length; i += BATCH) {
|
|
87
|
-
const chunk = candidates.slice(i, i + BATCH);
|
|
88
|
-
const res = await curateActionsBatch(appKind, chunk);
|
|
89
|
-
all.push(...(res.actions || []));
|
|
90
|
-
}
|
|
91
|
-
return { actions: all };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function curateActionsBatch(appKind, candidates) {
|
|
95
|
-
const system =
|
|
96
|
-
'You design the action surface of an in-app voice agent. Input: callables detected by static ' +
|
|
97
|
-
'analysis of a mobile app (handlers, store actions, buttons, alerts). For EACH candidate decide:\n' +
|
|
98
|
-
'- expose: should the voice agent be able to trigger it? (skip pure-UI plumbing like toggling pickers, ' +
|
|
99
|
-
'rendering callbacks, backdrop handlers, text-input onChange)\n' +
|
|
100
|
-
'- intent: snake_case id; label: short human label (English); category: one of discovery|workout|history|account|navigation|other\n' +
|
|
101
|
-
'- requireConfirmation: true only for destructive/sensitive operations (deleting data, losing progress, signing out, spending money)\n' +
|
|
102
|
-
'- confirmationMessage: required question if requireConfirmation\n' +
|
|
103
|
-
'- parameters: object of {name:{type,required,description}} ONLY when the user must provide a value by voice (e.g. a search query)\n' +
|
|
104
|
-
'- requiresVisibleContent: true when the action operates on an item the user is looking at\n' +
|
|
105
|
-
'- examples: 3-4 natural English voice phrases\n' +
|
|
106
|
-
'- evidence_used: which input evidence justified your decision\n' +
|
|
107
|
-
'Be conservative: when a candidate looks like internal plumbing, expose=false. ' +
|
|
108
|
-
'Sign-in/sign-up/verification flows requiring typed credentials should be expose=false ' +
|
|
109
|
-
'(a voice agent cannot dictate passwords safely).\n' +
|
|
110
|
-
'Return a decision for EVERY candidate. "intent" MUST be a plain string.\n' +
|
|
111
|
-
'Answer JSON: {"actions": [{...candidate fields above, "source_name": "<input name>"}]}';
|
|
112
|
-
const user =
|
|
113
|
-
`App kind: ${appKind}\n\nCandidates (with static evidence):\n` +
|
|
114
|
-
JSON.stringify(candidates, null, 1).slice(0, 14000);
|
|
115
|
-
return callLLM('curateActions', system, user, { maxTokens: 4000 });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** 3. Route intents, labels, examples. */
|
|
119
|
-
async function writeRouteMeta(appKind, routes) {
|
|
120
|
-
const system =
|
|
121
|
-
'You name navigation intents for an in-app voice agent. For each route give: ' +
|
|
122
|
-
'intent (snake_case), label (short English), examples (3-4 natural English voice phrases). ' +
|
|
123
|
-
'Skip auth screens the agent should not navigate to mid-session if any seem like sign-in/sign-up (mark skip=true). ' +
|
|
124
|
-
'For dynamic routes ([param]) mark dynamic=true and skip=true (they need a runtime parameter the static manifest cannot provide). ' +
|
|
125
|
-
'Answer JSON: {"routes": [{"routePath": "...", "intent": "...", "label": "...", "examples": [...], "skip": false, "skipReason": ""}]}';
|
|
126
|
-
const user = `App kind: ${appKind}\n\nRoutes:\n${JSON.stringify(routes, null, 1)}`;
|
|
127
|
-
return callLLM('writeRouteMeta', system, user, { maxTokens: 2500 });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** 4. Pattern gap-fill: what does the matched pattern expect that we missed? */
|
|
131
|
-
async function patternGapFill(patternId, patternText, draftManifestSummary) {
|
|
132
|
-
const system =
|
|
133
|
-
'You compare a draft voice-agent manifest against an integration pattern document. ' +
|
|
134
|
-
'List actions/routes the pattern suggests SHOULD usually exist for this app type but are missing or ' +
|
|
135
|
-
'under-specified in the draft. Only suggest things genuinely implied by the pattern; do not invent. ' +
|
|
136
|
-
'Answer JSON: {"suggestions": [{"kind": "action"|"route", "intent": "...", "label": "...", "why": "...", ' +
|
|
137
|
-
'"confidence": 0..1}], "patternUseful": true|false, "notes": "<honest assessment of how much this pattern helped>"}';
|
|
138
|
-
const user =
|
|
139
|
-
`Pattern (${patternId}):\n${patternText.slice(0, 5000)}\n\nDraft manifest summary:\n` +
|
|
140
|
-
JSON.stringify(draftManifestSummary, null, 1).slice(0, 5000);
|
|
141
|
-
return callLLM('patternGapFill', system, user, { maxTokens: 1200 });
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
module.exports = { classifyApp, curateActions, writeRouteMeta, patternGapFill, getLog };
|