mobile-ai-agents 1.0.41 → 1.0.42
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/README.md +14 -0
- package/cli/commands/network.js +9 -0
- package/cli/fixtures/mobile-handoff/App.jsx +27 -0
- package/cli/fixtures/mobile-handoff/client.js +25 -0
- package/cli/fixtures/mobile-handoff/model.js +22 -0
- package/cli/index.js +1 -0
- package/cli/lib/mobile-handoff.js +102 -0
- package/docs/mobile-integration-handoff.md +86 -0
- package/package.json +1 -1
- package/skills/cross-platform/web-network-to-mobile.md +4 -0
package/README.md
CHANGED
|
@@ -159,6 +159,20 @@ Available in `mobile-ai-agents@1.0.39` and newer. Use `npx mobile-ai-agents@late
|
|
|
159
159
|
|
|
160
160
|
---
|
|
161
161
|
|
|
162
|
+
## Keep your mobile app’s architecture
|
|
163
|
+
|
|
164
|
+
Turn browser API contracts into scoped tasks mapped to your existing app files:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx mobile-ai-agents@latest network handoff \
|
|
168
|
+
--contract /tmp/mobile-api-contract-1/contract.json \
|
|
169
|
+
--mapping mapping.json --repo /path/to/mobile-app --out /tmp/mobile-handoff-1
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Review networking, auth, domain, state and navigation ownership across Android, iOS, Flutter, React Native and KMP. Native auth or endpoint gaps block dependent tasks; generated plans never claim device verification. Includes a React Native reference flow with HTTP contract tests. Available starting in `1.0.42`. [Mapping format, reference flow and verification limits](docs/mobile-integration-handoff.md).
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
162
176
|
## Who It Helps
|
|
163
177
|
|
|
164
178
|
| Role | How Mobile AI Agents Helps |
|
package/cli/commands/network.js
CHANGED
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
const fs = require('node:fs');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { parseOptions } = require('../lib/options');
|
|
5
|
+
const { buildHandoff, writeHandoff } = require('../lib/mobile-handoff');
|
|
5
6
|
const { capture } = require('../lib/network-capture');
|
|
6
7
|
const { readJson, buildContract, writeContract } = require('../lib/api-contract');
|
|
7
8
|
|
|
8
9
|
async function cmdNetwork(args) {
|
|
9
10
|
const opts = parseOptions(args);
|
|
11
|
+
if (opts._.join(' ') === 'handoff') {
|
|
12
|
+
if (!opts.contract || !opts.mapping || !opts.repo || !opts.out || Object.keys(opts).some(k => !['_', 'contract', 'mapping', 'repo', 'out'].includes(k))) throw new Error('Usage: network handoff --contract contract.json --mapping mapping.json --repo APP_ROOT --out NEW_DIRECTORY');
|
|
13
|
+
for (const key of Object.keys(opts)) if (key !== '_' && typeof opts[key] !== 'string') throw new Error(`--${key} needs a value.`);
|
|
14
|
+
const report = buildHandoff(readJson(opts.contract), readJson(opts.mapping), opts.repo);
|
|
15
|
+
writeHandoff(report, path.resolve(opts.out));
|
|
16
|
+
console.log(`${report.status}: ${report.tasks.length} scoped tasks. Mobile verification: unverified. ${path.resolve(opts.out)}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
10
19
|
if (opts._.join(' ') === 'contract') {
|
|
11
20
|
if ((!opts.capture && !opts.har) || (opts.capture && opts.har) || !opts.out || Object.keys(opts).some(key => !['_', 'capture', 'har', 'openapi', 'out'].includes(key))) throw new Error('Usage: network contract --capture capture.json | --har session.har --out NEW_DIRECTORY [--openapi openapi.json]');
|
|
12
21
|
for (const key of Object.keys(opts)) if (key !== '_' && typeof opts[key] !== 'string') throw new Error(`--${key} needs a value.`);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { View, Text, TextInput, Button, ScrollView } from 'react-native';
|
|
3
|
+
const { createClient } = require('./client');
|
|
4
|
+
const { createModel } = require('./model');
|
|
5
|
+
|
|
6
|
+
// Mount inside an existing React Native test app, passing its reachable demo origin.
|
|
7
|
+
export default function ReferenceFlow({ baseUrl }) {
|
|
8
|
+
const model = useMemo(() => createModel(createClient({ baseUrl })), [baseUrl]);
|
|
9
|
+
const [state, setState] = useState(model.getState());
|
|
10
|
+
const [title, setTitle] = useState('');
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
setState(model.getState());
|
|
13
|
+
const unsubscribe = model.subscribe(setState);
|
|
14
|
+
return () => { unsubscribe(); model.dispose(); };
|
|
15
|
+
}, [model]);
|
|
16
|
+
const busy = state.status === 'loading';
|
|
17
|
+
return <ScrollView contentContainerStyle={{ padding: 24, gap: 12 }}>
|
|
18
|
+
<Text accessibilityRole="header">Website API → mobile reference</Text>
|
|
19
|
+
<Button title="Load listings" disabled={busy} onPress={() => model.run('list')} />
|
|
20
|
+
<Button title="Open detail" disabled={busy} onPress={() => model.run('detail')} />
|
|
21
|
+
<TextInput accessibilityLabel="Listing title" placeholder="Title" value={title} onChangeText={setTitle} />
|
|
22
|
+
<Button title="Save test listing" disabled={busy || !title.trim()} onPress={() => model.run('save', title)} />
|
|
23
|
+
<Button title="Cancel" disabled={!busy} onPress={model.cancel} />
|
|
24
|
+
<View accessibilityLiveRegion="polite"><Text>{state.status}{state.error ? `: ${state.error}` : ''}</Text></View>
|
|
25
|
+
<Text selectable>{state.data ? JSON.stringify(state.data, null, 2) : ''}</Text>
|
|
26
|
+
</ScrollView>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Same business flow as network-demo; transport is injected from the existing app.
|
|
3
|
+
function createClient({ baseUrl, transport = fetch }) {
|
|
4
|
+
const base = new URL(baseUrl);
|
|
5
|
+
if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password || base.search || base.hash || base.pathname !== '/') throw new Error('Use an explicit HTTP(S) test-server origin.');
|
|
6
|
+
async function request(route, { signal, ...options } = {}) {
|
|
7
|
+
const response = await transport(new URL(route, base).href, { ...options, signal });
|
|
8
|
+
if (response.status === 401) throw Object.assign(new Error('Session expired'), { kind: 'auth-expired' });
|
|
9
|
+
if (!response.ok) throw Object.assign(new Error('Request failed'), { kind: response.status === 422 ? 'validation' : 'http' });
|
|
10
|
+
return response.json();
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
list: signal => request('/api/items', { signal }),
|
|
14
|
+
detail: async signal => {
|
|
15
|
+
const [item, options] = await Promise.all([request('/api/detail', { signal }), request('/api/options', { signal })]);
|
|
16
|
+
return { item, options };
|
|
17
|
+
},
|
|
18
|
+
save: (title, signal) => {
|
|
19
|
+
if (!title.trim()) throw Object.assign(new Error('Title required'), { kind: 'validation' });
|
|
20
|
+
// The owned demo does not require auth. Never copy its captured bearer token.
|
|
21
|
+
return request('/api/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: title.trim() }), signal });
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
module.exports = { createClient };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
function createModel(client) {
|
|
3
|
+
let state = { status: 'idle', data: null, error: null }, active, generation = 0;
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
function set(next) { state = next; listeners.forEach(fn => fn(state)); }
|
|
6
|
+
async function run(operation, value) {
|
|
7
|
+
if (!['list', 'detail', 'save'].includes(operation)) throw new Error('Unknown operation');
|
|
8
|
+
if (state.status === 'loading') return; // No duplicate submission, no automatic retries.
|
|
9
|
+
const current = ++generation;
|
|
10
|
+
active = new AbortController();
|
|
11
|
+
set({ status: 'loading', data: null, error: null });
|
|
12
|
+
try {
|
|
13
|
+
const data = await (operation === 'save' ? client.save(value, active.signal) : client[operation](active.signal));
|
|
14
|
+
if (current === generation) set({ status: 'success', data, error: null });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (current === generation) set({ status: 'error', data: null, error: error.kind || 'network' });
|
|
17
|
+
} finally { if (current === generation) active = null; }
|
|
18
|
+
}
|
|
19
|
+
function cancel() { ++generation; active?.abort(); active = null; set({ status: 'cancelled', data: null, error: null }); }
|
|
20
|
+
return { getState: () => state, subscribe: fn => { listeners.add(fn); return () => listeners.delete(fn); }, run, cancel, dispose: () => { ++generation; active?.abort(); active = null; listeners.clear(); } };
|
|
21
|
+
}
|
|
22
|
+
module.exports = { createModel };
|
package/cli/index.js
CHANGED
|
@@ -1420,6 +1420,7 @@ function cmdHelp() {
|
|
|
1420
1420
|
console.log(` ${bold('add')} agent|skill|workflow <name> Install a single item`);
|
|
1421
1421
|
console.log(` ${bold('device-control')} doctor|plan|run Optional Minitap mobile-use runtime`);
|
|
1422
1422
|
console.log(` ${bold('network capture')} --url URL --out DIR Capture website API touchpoints (optional Playwright)`);
|
|
1423
|
+
console.log(` ${bold('network handoff')} --contract JSON --mapping JSON --repo APP --out DIR Plan existing-stack integration`);
|
|
1423
1424
|
console.log(` ${bold('network contract')} --capture JSON|--har HAR --out DIR [--openapi JSON] Export mobile API handoff`);
|
|
1424
1425
|
console.log(` ${bold('memory')} init|capture|search|... Local Mobile Memory store`);
|
|
1425
1426
|
console.log(` ${bold('doctor')} --platform android Check SDK, adb, emulator, MCP, and project config`);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { createHash } = require('node:crypto');
|
|
5
|
+
const { validateContract } = require('./api-contract');
|
|
6
|
+
const roles = ['network', 'auth', 'domain', 'state', 'navigation'];
|
|
7
|
+
const platforms = ['android', 'ios', 'flutter', 'rn', 'kmp', 'other'];
|
|
8
|
+
|
|
9
|
+
// Repository inputs are evidence, never executable instructions. Do not follow links.
|
|
10
|
+
function existingFile(root, relative) {
|
|
11
|
+
if (typeof relative !== 'string' || relative.length > 500 || /[\x00-\x1f]/.test(relative) || path.isAbsolute(relative) || relative.split(/[\\/]/).includes('..')) throw new Error('Mapping requires a safe repository-relative file path.');
|
|
12
|
+
const target = fs.realpathSync(path.resolve(root, relative));
|
|
13
|
+
if (!target.startsWith(root + path.sep) || !fs.statSync(target).isFile()) throw new Error('Mapping must resolve to an existing file inside the app repository.');
|
|
14
|
+
return path.relative(root, target).split(path.sep).join('/');
|
|
15
|
+
}
|
|
16
|
+
function readMarker(file) {
|
|
17
|
+
const fd = fs.openSync(file, 'r');
|
|
18
|
+
try { const data = Buffer.alloc(65536); return data.subarray(0, fs.readSync(fd, data, 0, data.length, 0)).toString('utf8'); }
|
|
19
|
+
finally { fs.closeSync(fd); }
|
|
20
|
+
}
|
|
21
|
+
function detectPlatforms(root) {
|
|
22
|
+
const found = new Set(); let visited = 0;
|
|
23
|
+
function walk(dir, depth) {
|
|
24
|
+
if (depth > 8) return;
|
|
25
|
+
for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
26
|
+
if (++visited > 20000) throw new Error('Repository scan exceeds 20000 entries; use a narrower app root.');
|
|
27
|
+
if (item.isSymbolicLink()) continue;
|
|
28
|
+
if (item.isDirectory()) {
|
|
29
|
+
if (item.name.endsWith('.xcodeproj')) found.add('ios');
|
|
30
|
+
if (!['.git', 'node_modules', 'build', '.gradle', '.dart_tool', 'Pods', 'vendor'].includes(item.name)) walk(path.join(dir, item.name), depth + 1);
|
|
31
|
+
} else if (item.isFile()) {
|
|
32
|
+
if (item.name === 'AndroidManifest.xml') found.add('android');
|
|
33
|
+
if (item.name.endsWith('.swift')) found.add('ios');
|
|
34
|
+
if (item.name === 'pubspec.yaml' && /(?:^|\n)\s*sdk:\s*flutter\s*(?:\n|$)/.test(readMarker(path.join(dir, item.name)))) found.add('flutter');
|
|
35
|
+
if (item.name === 'package.json') {
|
|
36
|
+
let pkg; try { pkg = JSON.parse(readMarker(path.join(dir, item.name))); } catch { continue; }
|
|
37
|
+
if (pkg.dependencies?.['react-native'] || pkg.devDependencies?.['react-native']) found.add('rn');
|
|
38
|
+
}
|
|
39
|
+
if (/^build\.gradle(?:\.kts)?$/.test(item.name) && /kotlin\(["']multiplatform["']\)|org\.jetbrains\.kotlin\.multiplatform/.test(readMarker(path.join(dir, item.name)))) found.add('kmp');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
walk(root, 0); return [...found].sort();
|
|
44
|
+
}
|
|
45
|
+
function buildHandoff(contract, manifest, repository) {
|
|
46
|
+
validateContract(contract);
|
|
47
|
+
if (contract.requests.length > 500 || contract.touchpoints.length > 100 || !Array.isArray(contract.findings)) throw new Error('Invalid or oversized contract.');
|
|
48
|
+
if (manifest?.version !== 1 || !platforms.includes(manifest.platform) || !Array.isArray(manifest.touchpoints) || manifest.touchpoints.length > 100) throw new Error('Expected mapping version 1, platform and touchpoints.');
|
|
49
|
+
const root = fs.realpathSync(repository), detectedPlatforms = detectPlatforms(root);
|
|
50
|
+
if (manifest.platform !== 'other' && !detectedPlatforms.includes(manifest.platform)) throw new Error('Selected platform has no detected repository marker.');
|
|
51
|
+
const points = new Set(contract.touchpoints.map(p => p.id)), mapped = new Set();
|
|
52
|
+
const mappings = manifest.touchpoints.map(mapping => {
|
|
53
|
+
if (!points.has(mapping.id) || mapped.has(mapping.id)) throw new Error('Unknown or duplicate mapped touchpoint.');
|
|
54
|
+
mapped.add(mapping.id);
|
|
55
|
+
const files = {};
|
|
56
|
+
for (const role of roles) {
|
|
57
|
+
const file = mapping.files?.[role];
|
|
58
|
+
if (file) {
|
|
59
|
+
if (!['shared', 'native'].includes(file.ownership)) throw new Error('File ownership must be shared or native.');
|
|
60
|
+
files[role] = { path: existingFile(root, file.path), ownership: file.ownership };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const reviews = mapping.requests || [];
|
|
64
|
+
if (!Array.isArray(reviews)) throw new Error('Request reviews must be an array.');
|
|
65
|
+
const expected = contract.touchpoints.find(p => p.id === mapping.id).requestIds;
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
const requestReviews = reviews.map(review => {
|
|
68
|
+
if (!expected.includes(review.id) || seen.has(review.id)) throw new Error('Unknown or duplicate reviewed request.');
|
|
69
|
+
seen.add(review.id);
|
|
70
|
+
if (!['supported', 'gap'].includes(review.nativeEndpoint) || !['supported', 'gap', 'not-required'].includes(review.nativeAuth)) throw new Error('Request review requires explicit native endpoint and auth decisions.');
|
|
71
|
+
return { id: review.id, nativeEndpoint: review.nativeEndpoint, nativeAuth: review.nativeAuth, evidence: existingFile(root, review.evidence), findingsReviewed: review.findingsReviewed === true };
|
|
72
|
+
});
|
|
73
|
+
return { id: mapping.id, files, requestReviews };
|
|
74
|
+
});
|
|
75
|
+
const tasks = contract.touchpoints.map(point => {
|
|
76
|
+
const mapping = mappings.find(m => m.id === point.id);
|
|
77
|
+
const gaps = roles.filter(role => !mapping?.files[role]).map(role => `map-existing-${role}-ownership`);
|
|
78
|
+
if (contract.source.captureStatus && contract.source.captureStatus !== 'captured') gaps.push('capture-incomplete');
|
|
79
|
+
if (contract.findings.some(f => !f.requestId)) gaps.push('global-contract-findings-unreviewed');
|
|
80
|
+
if (manifest.platform === 'other') gaps.push('confirm-custom-stack-and-tooling');
|
|
81
|
+
if (point.status !== 'completed') gaps.push('browser-action-incomplete');
|
|
82
|
+
if (point.provenance === 'unattributed') gaps.push('establish-user-action-attribution');
|
|
83
|
+
for (const id of point.requestIds) {
|
|
84
|
+
const review = mapping?.requestReviews.find(r => r.id === id);
|
|
85
|
+
if (!review || review.nativeEndpoint !== 'supported') gaps.push(`${id}:native-endpoint-unresolved`);
|
|
86
|
+
if (!review || review.nativeAuth === 'gap') gaps.push(`${id}:native-auth-unresolved`);
|
|
87
|
+
if (contract.findings.some(f => f.requestId === id) && !review?.findingsReviewed) gaps.push(`${id}:contract-findings-unreviewed`);
|
|
88
|
+
}
|
|
89
|
+
return { id: `implement-${point.id}`, touchpointId: point.id, requestIds: [...point.requestIds], status: gaps.length ? 'blocked' : 'ready-for-review', gaps,
|
|
90
|
+
steps: ['Review mapped client/auth/domain/state/navigation boundaries; reuse current stack.', 'Select business calls; omit preflight, analytics and duplicate fetches only after review.', 'Resolve native auth/endpoint and schema conflicts before dependent wiring.', 'Implement scoped DTO/domain/state changes in mapped files; propose new files explicitly during code review.', 'Preserve no-network validation actions and documented native navigation differences.'],
|
|
91
|
+
acceptance: ['Matching request/response contract and UI states with equivalent fixtures.', 'Cancellation reaches transport; stale results do not replace current UI state.', 'Auth expiry follows existing refresh/logout policy; no captured browser credentials.', 'Retry only supported safe/idempotent operations; no blind write retries.', 'Validate input locally and map server validation errors.', 'Uploads: document stages, progress, cancellation and retry if applicable; otherwise record not applicable.'],
|
|
92
|
+
verification: { contract: 'unverified', mobileNetwork: 'unverified', mobileUI: 'unverified' } };
|
|
93
|
+
});
|
|
94
|
+
return { handoffVersion: '1.0.0', platform: manifest.platform, detectedPlatforms, contractSha256: createHash('sha256').update(JSON.stringify(contract)).digest('hex'), status: tasks.some(t => t.status === 'blocked') ? 'blocked' : 'needs-review', mobileVerification: 'unverified', mappings, tasks,
|
|
95
|
+
limitations: ['Mappings and native support decisions are user-reviewed inputs, not automatically proven semantics.', 'Detection uses bounded markers, not architecture inference; nested app markers can overlap.', 'No app source is modified and no requests are replayed.', 'Contract findings remain in the source contract; reviewed does not mean resolved.', 'Device/network/UI evidence is required separately for every target.'] };
|
|
96
|
+
}
|
|
97
|
+
function writeHandoff(report, directory) {
|
|
98
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
99
|
+
fs.writeFileSync(path.join(directory, 'handoff.json'), JSON.stringify(report, null, 2) + '\n', { mode: 0o600 });
|
|
100
|
+
fs.writeFileSync(path.join(directory, 'handoff.md'), ['# Mobile integration handoff', '', `Status: ${report.status}; platform: ${report.platform}; mobile verification: unverified.`, '', ...report.tasks.flatMap(t => [`## ${t.id}`, `Status: ${t.status}`, ...t.gaps.map(g => `- Gap: ${g}`), ...t.steps.map(s => `- ${s}`), ...t.acceptance.map(a => `- Check: ${a}`), '']), ...report.limitations.map(l => `- ${l}`)].join('\n') + '\n', { mode: 0o600 });
|
|
101
|
+
}
|
|
102
|
+
module.exports = { existingFile, detectPlatforms, buildHandoff, writeHandoff };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Website API contract → existing mobile app implementation
|
|
2
|
+
|
|
3
|
+
`network handoff` (starting in v1.0.42) turns the [API contract](api-contracts.md) into scoped implementation tasks. It preserves the app's existing stack by requiring reviewed source-file mappings instead of generating a replacement networking layer. It reads local inputs only, never replays requests and never changes the target app.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx mobile-ai-agents@latest network handoff \
|
|
9
|
+
--contract /tmp/mobile-api-contract-1/contract.json \
|
|
10
|
+
--mapping mapping.json --repo /path/to/mobile-app \
|
|
11
|
+
--out /tmp/mobile-handoff-1
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Every output directory must be new, with an existing parent. Inputs use the contract command's 10 MiB limit. Scanning skips dependency/build folders and symlinks, stops after 20,000 entries and eight directory levels, and reads only supported marker files. Choose a focused app root. Detection is a hint; nested Android modules inside Flutter or RN projects can also produce Android markers. The selected platform must have a marker; `other` always requires a tooling review.
|
|
15
|
+
|
|
16
|
+
Supply mappings for the actual touchpoint IDs in your contract. This example shows a reviewed Android mapping; replace paths with **existing files in your app**, not invented scaffolding:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"version": 1,
|
|
21
|
+
"platform": "android",
|
|
22
|
+
"touchpoints": [{
|
|
23
|
+
"id": "T1",
|
|
24
|
+
"files": {
|
|
25
|
+
"network": {"path": "app/src/main/java/com/example/data/ItemsApi.kt", "ownership": "native"},
|
|
26
|
+
"auth": {"path": "app/src/main/java/com/example/data/Session.kt", "ownership": "native"},
|
|
27
|
+
"domain": {"path": "app/src/main/java/com/example/domain/Item.kt", "ownership": "native"},
|
|
28
|
+
"state": {"path": "app/src/main/java/com/example/ui/ItemsViewModel.kt", "ownership": "native"},
|
|
29
|
+
"navigation": {"path": "app/src/main/java/com/example/ui/Navigation.kt", "ownership": "native"}
|
|
30
|
+
},
|
|
31
|
+
"requests": [{
|
|
32
|
+
"id": "R1",
|
|
33
|
+
"nativeEndpoint": "supported",
|
|
34
|
+
"nativeAuth": "supported",
|
|
35
|
+
"evidence": "docs/native-api-review.md",
|
|
36
|
+
"findingsReviewed": true
|
|
37
|
+
}]
|
|
38
|
+
}]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`platform`: `android`, `ios`, `flutter`, `rn`, `kmp`, or `other`. File ownership is `native` or `shared`. Multiple roles may map to one existing file when that matches the real app. Evidence is a local review file, not a remote URL. Keep it free of secrets. Paths must resolve inside the repository; missing files, escapes and external symlinks are rejected. No source text, credentials or captured header values are copied to the report. Paths and hashes are still project metadata: review before sharing.
|
|
43
|
+
|
|
44
|
+
Record native endpoint and auth support based on backend/mobile documentation and the existing client. `nativeEndpoint` is `supported` or `gap`; `nativeAuth` is `supported`, `not-required` or `gap`. Review evidence should identify the supported native route/auth flow, DTO/domain ownership, schema conflicts, business-call grouping and justified native differences. `findingsReviewed` is a human assertion that findings were reviewed, **not proof that discrepancies are resolved**. Do not set it merely to make a task ready. Contract findings remain authoritative until separately resolved.
|
|
45
|
+
|
|
46
|
+
Missing role mappings, missing request reviews, auth/endpoint gaps, incomplete captures, unattributed HAR actions or unreviewed contract findings block dependent tasks. No-network touchpoints still get tasks and ownership mappings. Output consists of private `handoff.json` and `handoff.md` with stable touchpoint/request IDs, existing files, gaps, steps and acceptance criteria. `ready-for-review` means there is enough supplied mapping to review a task; it never means implementation or mobile verification passed. Unresolved global findings conservatively block all tasks.
|
|
47
|
+
|
|
48
|
+
## Platform ownership
|
|
49
|
+
|
|
50
|
+
| Target | Preserve and map | Runtime status for this feature |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| Android | Current Kotlin/Java HTTP client, session, DTO/domain, ViewModel and navigation | Unverified |
|
|
53
|
+
| iOS | Current Swift/ObjC session/client, models, state and navigation | Unverified |
|
|
54
|
+
| Flutter | Current Dart client, repositories, state and router | Unverified |
|
|
55
|
+
| React Native | Current JS/TS client, auth, models, hooks/store and navigation | Reference source and Node HTTP tests; device UI unverified |
|
|
56
|
+
| KMP | Existing shared client/domain ownership separately from target auth/UI/navigation | Unverified |
|
|
57
|
+
| Other | Reviewed existing toolchain and equivalent boundaries | Unverified; explicit review required |
|
|
58
|
+
|
|
59
|
+
The CLI does not parse every language, infer architecture from filenames, create universal SDKs, or copy browser headers, analytics, preflight calls and duplicate fetches into a client. Review business-call selection and causal dependencies before wiring code. Independent tasks can proceed while dependent tasks remain blocked.
|
|
60
|
+
|
|
61
|
+
## Runnable reference: list → detail → validate → save
|
|
62
|
+
|
|
63
|
+
The packaged [reference files](../cli/fixtures/mobile-handoff/App.jsx) contain a React Native screen, injected-transport client and cancellable state model. They target the repository's owned [HTTP demo](../cli/fixtures/network-demo.js), not a production API. The screen can be mounted inside an existing React Native test project:
|
|
64
|
+
|
|
65
|
+
1. Start the owned server from this repository: `node cli/fixtures/network-demo.js /tmp/mobile-demo-flow.json` (new flow-file path).
|
|
66
|
+
2. Copy `cli/fixtures/mobile-handoff/` into that project's source tree. Import `ReferenceFlow` from `App.jsx` and mount `<ReferenceFlow baseUrl="http://YOUR_TEST_HOST:4173" />` in the existing app. Android emulators normally reach their host using `10.0.2.2`; use a reachable host for your actual target. Configure local HTTP only in the existing debug/test configuration. Do not weaken production TLS or certificate pinning.
|
|
67
|
+
3. Tap Load listings, Open detail, enter a title, Save test listing, and Cancel during an outstanding operation. Connect independent device and network evidence to the same touchpoint IDs before claiming parity.
|
|
68
|
+
|
|
69
|
+
The client issues GET `/api/items`; parallel GET `/api/detail` and `/api/options`; and a single POST `/api/save` with JSON `{title}`. Empty titles are rejected before transport. The browser demo's incidental password field and demonstration bearer token are not native requirements; the owned server accepts the title without them. This is a documented fixture difference, not a strategy for bypassing real auth.
|
|
70
|
+
|
|
71
|
+
The client accepts the existing app transport for integration. It does not implement token storage or refresh: 401 produces `auth-expired` for the app's established session policy. HTTP 422 produces `validation`; other HTTP failures and network failures stay errors. The model prevents duplicate submissions, makes **no automatic retries**, forwards cancellation via AbortSignal and suppresses late results after cancellation/unmount. The screen is deliberately separate from production navigation and auth ownership.
|
|
72
|
+
|
|
73
|
+
Uploads are not present in this fixture and remain **not applicable**, rather than tested. If a reference contains uploads, map initialization, signed URL, transfer, completion, progress, cancellation and safe retry/idempotency policy before dependent implementation. Production refresh, upload, retry semantics and platform-specific transport cancellation need their own evidence.
|
|
74
|
+
|
|
75
|
+
## Reproducible checks and remaining verification
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node --test test/mobile-handoff.test.js
|
|
79
|
+
npm test
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The HTTP tests use a real local server and exercise the same client used by the reference screen. They check list/detail/save, local validation, auth expiry, server validation, cancellation, duplicate prevention and no write retries. Mapping tests cover ownership, KMP boundaries, missing evidence, path containment and output preservation. They do **not** mount a mobile UI, run Metro, build a native binary or establish browser/mobile device parity. The reference is runnable source for an existing React Native test app; no native test harness or dependency installation is bundled.
|
|
83
|
+
|
|
84
|
+
Continue with [#72](https://github.com/salmanashraf/mobile-ai-agents/issues/72) for actual mobile network/UI parity, reusing Mobile MCP/device QA work in #54/#55. Run relevant build/contract checks, then capture actual mobile requests/responses and UI on each requested target. Use equivalent fixtures/environment/accounts. Screenshots alone do not prove network parity. Implementation source, mocked responses and Node HTTP tests must never be reported as a verified mobile runtime.
|
|
85
|
+
|
|
86
|
+
Implementation: [#71](https://github.com/salmanashraf/mobile-ai-agents/issues/71). Follow [web-network-to-mobile](../skills/cross-platform/web-network-to-mobile.md) and PRD verification before declaring the user flow complete.
|
package/package.json
CHANGED
|
@@ -25,6 +25,10 @@ Trace an authorized deployed website's network behavior at each user interaction
|
|
|
25
25
|
|
|
26
26
|
This repository includes `node cli/index.js network capture --url URL --out NEW_DIRECTORY --flow flow.json`. Read [the capture guide](../../docs/website-api-discovery.md) for optional Playwright setup, allowed origins and controlled write actions. It produces sanitized request/response shapes and temporal touchpoint evidence; it does not export full payload values, perform mobile implementation or verify mobile traffic. Use it when that evidence is sufficient, and preserve its capture limitations in the handoff. The command requires mobile-ai-agents 1.0.39 or newer.
|
|
27
27
|
|
|
28
|
+
## Existing-App Handoff Adapter
|
|
29
|
+
|
|
30
|
+
After exporting `contract.json`, use `network handoff --contract contract.json --mapping mapping.json --repo APP_ROOT --out NEW_DIRECTORY`. Read [the handoff guide](../../docs/mobile-integration-handoff.md). Supply reviewed existing-file ownership and native auth/endpoint evidence; detection markers alone do not establish architecture. Missing mappings block dependent tasks. Use KMP `shared` versus `native` ownership explicitly. Requires mobile-ai-agents 1.0.42 or newer. The command writes a scoped plan and never edits the target app or claims mobile parity. Review all remaining contract findings before implementing.
|
|
31
|
+
|
|
28
32
|
## Skill Prompt
|
|
29
33
|
|
|
30
34
|
For versioned artifacts, use `npx mobile-ai-agents@latest network contract --capture capture.json --out NEW_DIRECTORY`, or `--har session.har`, with optional `--openapi openapi.json`. Read [the contract guide](../../docs/api-contracts.md) for provenance, supported cross-checks and capture gaps. This exporter stays `needs-review` and does not establish a supported mobile contract or app parity. It requires mobile-ai-agents 1.0.40 or newer.
|