@mobileaidev/ai-app-bridge 0.3.8 → 0.4.1
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 +54 -39
- package/bin/ai-app-bridge.js +56 -17
- package/bin/command-discovery.js +19 -4
- package/bin/command-registry.js +17 -4
- package/bin/command-request.js +68 -4
- package/bin/execution-host.js +31 -5
- package/bin/execution-runtime.js +17 -10
- package/bin/executors/preparation.js +19 -5
- package/bin/extraction/json-value.js +26 -0
- package/bin/extraction/prepare.js +57 -0
- package/bin/extraction/regex.js +30 -0
- package/bin/extraction/runner.js +77 -0
- package/bin/ios-execution.js +6 -0
- package/bin/ios-provider.js +3 -2
- package/bin/mcp-server.js +15 -20
- package/bin/public-reply.js +184 -0
- package/bin/response-store.js +60 -0
- package/bin/runtime-client.js +32 -15
- package/bin/runtime-directory.js +37 -8
- package/bin/script/node-runtime-adapter.js +139 -123
- package/bin/script/python-runtime-adapter.js +1 -1
- package/bin/script/script-diagnostics.js +21 -0
- package/bin/script/script-durable-restore.js +1 -0
- package/bin/script/script-sdk.js +39 -4
- package/bin/script/script-sdk.py +79 -7
- package/bin/script/script-session-channel.js +27 -10
- package/bin/script/script-supervisor.js +8 -0
- package/bin/shared-kernel/argument-schema.js +44 -12
- package/bin/shared-kernel/evidence-archive.js +2 -2
- package/bin/shared-kernel/evidence-schema.js +16 -1
- package/bin/shared-kernel/evidence-store.js +3 -3
- package/bin/shared-kernel/execution-contracts.js +13 -5
- package/docs/COMMAND_CONTRACT.md +102 -19
- package/docs/EVIDENCE_ARCHIVE.md +14 -1
- package/docs/INSTALLATION.md +74 -0
- package/docs/INTENT_FOREGROUND.md +4 -1
- package/docs/OPTIONAL_EXECUTORS.md +14 -14
- package/docs/RELEASE.md +71 -122
- package/docs/RESPONSE_EXTRACTION.md +126 -0
- package/docs/SCRIPT_AUTHORING.md +125 -6
- package/node_modules/@mobileaidev/segmented-fact-store-native/PREBUILDS.md +29 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding-path.js +29 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +1 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +1 -3
- package/node_modules/@mobileaidev/segmented-fact-store-native/install.js +5 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +11 -5
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/darwin-arm64/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/darwin-x64/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/linux-arm64-glibc/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/linux-x64-glibc/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/manifest.json +27 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/scripts/build-release-prebuilds.js +33 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/scripts/stage-prebuild.js +17 -0
- package/package.json +12 -5
- package/runtime/executors/android/prepare.init.gradle +22 -0
- package/runtime/executors/playwright/package-lock.json +2 -2
- package/runtime/executors/playwright/package.json +1 -1
- package/skills/ai-app-bridge-use/SKILL.md +19 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Response extraction (0.4.0)
|
|
2
|
+
|
|
3
|
+
Every CLI/MCP run must state what to return. Use `extract:null` for the original
|
|
4
|
+
result within the body budget, or select the fields needed for the current task.
|
|
5
|
+
`extract` and `output` are top-level request fields, separate from business
|
|
6
|
+
`arguments`. The old request without extract is rejected before dispatch.
|
|
7
|
+
Internal Script `ctx.call(command, args)` stays unchanged (`ok`, `result`).
|
|
8
|
+
|
|
9
|
+
```json extraction-example
|
|
10
|
+
{"command":"runtime","arguments":{"operation":"status"},"extract":null}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
ai-app-bridge runtime --operation status --extract null
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
A small response can be read in full. Large trees, network records and logs are
|
|
18
|
+
better extracted near the Host. This saves response/context bytes, not the
|
|
19
|
+
original collection time. Capture duration, history limit and extraction solve
|
|
20
|
+
different problems; a one-item page can still exceed the byte budget.
|
|
21
|
+
|
|
22
|
+
## One query, one extraction
|
|
23
|
+
|
|
24
|
+
Regex requires a string at `inputPath` (JSON Pointer). Empty path selects a text
|
|
25
|
+
response; escape `/` as `~1` and `~` as `~0`. Flags are unique `i`, `m`, `s`, `u`;
|
|
26
|
+
all matches are returned with `match`, `groups` and `namedGroups`.
|
|
27
|
+
Unmatched captures are null. At most 1,000 matches are allowed; overflow fails.
|
|
28
|
+
|
|
29
|
+
```json extraction-example
|
|
30
|
+
{"command":"runtime","arguments":{"operation":"status"},"extract":{"mode":"regex","pattern":"stopped|running","inputPath":"/status"}}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For structured responses, both languages receive exactly
|
|
34
|
+
`ctx.inputs = {kind, response, execution, control}`. `response` is the original
|
|
35
|
+
command value, including its final feedback. This is a short local transform;
|
|
36
|
+
it has no Script `ctx.call`, agent decisions or assertion API.
|
|
37
|
+
|
|
38
|
+
```json extraction-example
|
|
39
|
+
{"command":"runtime","arguments":{"operation":"status"},"extract":{"mode":"script","language":"javascript","source":"module.exports.main = ctx => ({status:ctx.inputs.response.status, commandOk:ctx.inputs.execution.ok});"}}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```json extraction-example
|
|
43
|
+
{"command":"runtime","arguments":{"operation":"status"},"extract":{"mode":"script","language":"python","source":"def main(ctx):\n return {\"status\": ctx.inputs[\"response\"][\"status\"], \"commandOk\": ctx.inputs[\"execution\"][\"ok\"]}\n"}}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use exactly one of `source` and `sourcePath` for scripts. Relative paths resolve
|
|
47
|
+
from the caller directory; the Host freezes the file before executing the
|
|
48
|
+
command. Syntax checks do not execute top-level code. Source is at most 64 KiB
|
|
49
|
+
UTF-8. Python requires Python 3.9+; ordinary, JS and regex calls need no Python.
|
|
50
|
+
|
|
51
|
+
Return strict JSON: null, booleans, strings, finite numbers, arrays and objects.
|
|
52
|
+
Integers outside ±9,007,199,254,740,991 are rejected, including Python values
|
|
53
|
+
before serialization. Convert intentionally to a string inside your source.
|
|
54
|
+
Unsupported types, cycles and sparse JS arrays fail rather than being coerced.
|
|
55
|
+
|
|
56
|
+
## Read the three outcomes separately
|
|
57
|
+
|
|
58
|
+
The shared compact body is
|
|
59
|
+
`{command, execution, control, extraction, delivery, kind, value?, failureStage?}`.
|
|
60
|
+
`execution` records the original command outcome and dispatch facts. Successful
|
|
61
|
+
extraction cannot convert a failed or unknown command into success. `control`
|
|
62
|
+
preserves operation IDs, receipts, Intent revisions, Script event cursors and
|
|
63
|
+
current pending questions, and capture coverage/pagination. Histories remain in
|
|
64
|
+
value; only their page controls are protected separately.
|
|
65
|
+
|
|
66
|
+
`extraction.status` is skipped, succeeded or failed. `delivery` reports actual
|
|
67
|
+
UTF-8 body bytes and the limit. `failureStage` gives the first failing stage in
|
|
68
|
+
validation → execution → extraction → delivery order. CLI exit codes are 0 for
|
|
69
|
+
requested delivery, 1 for validation/failed or unknown execution, and 2 when a
|
|
70
|
+
known successful command could not be extracted/delivered. MCP uses `isError`
|
|
71
|
+
consistently. A successful Script execution is separate from its business verdict.
|
|
72
|
+
|
|
73
|
+
A temporary-directory removal failure after the worker exits is reported as
|
|
74
|
+
`extraction.cleanupError`. It does not replace a successful value or the original
|
|
75
|
+
extraction error, change the exit code, or replay the command.
|
|
76
|
+
|
|
77
|
+
Preserve source identity, timestamps, state and coverage needed by the actual
|
|
78
|
+
assertion. A selected successful row does not prove complete business coverage.
|
|
79
|
+
|
|
80
|
+
## Retry extraction without repeating the action
|
|
81
|
+
|
|
82
|
+
Non-null extraction attempts to retain the original response. A null response
|
|
83
|
+
that would exceed the budget also attempts retention. `control.source` is the
|
|
84
|
+
only ref location. Only `persisted:true` makes that ref readable. Null within
|
|
85
|
+
budget normally reports `persisted:false, reason:not_requested`; offline local
|
|
86
|
+
commands report `offline`. Saving and extraction may fail independently.
|
|
87
|
+
|
|
88
|
+
On extraction failure or overflow, copy the original ref and change only the
|
|
89
|
+
extraction in a `response read`. The current read execution is separate from
|
|
90
|
+
original facts in `control.origin`; `control.source` still identifies the same
|
|
91
|
+
snapshot. Repeated reads neither replay devices nor create nested snapshots.
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"command": "response",
|
|
96
|
+
"arguments": {"operation":"read","ref":{"namespace":"response","evidenceId":"<original evidenceId>","checksum":"<original checksum>","operationId":"<original operationId>"}},
|
|
97
|
+
"extract": {"mode":"script","language":"javascript","source":"module.exports.main = ctx => ctx.inputs.response;"},
|
|
98
|
+
"output": {"maxBytes":262144}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A ref is subject to existing evidence retention. Missing/evicted/corrupt sources
|
|
103
|
+
fail explicitly. Reissuing the original action with the same requestId is not a
|
|
104
|
+
substitute for ref recovery: deduplication can expire and does not survive every
|
|
105
|
+
Host lifetime. Snapshot/export JSON preserves original values; binary responses
|
|
106
|
+
are base64 for null delivery and are not extractable in this version.
|
|
107
|
+
|
|
108
|
+
## Limits and diagnosis
|
|
109
|
+
|
|
110
|
+
| Limit | Value |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| Final compact UTF-8 body | 96 KiB default; output.maxBytes 16–256 KiB |
|
|
113
|
+
| Source snapshot / extraction input | 8 MiB original UTF-8 JSON |
|
|
114
|
+
| Returned extraction value | 256 KiB (the enclosing body still has its budget) |
|
|
115
|
+
| Extraction timeout | 2,000 ms default; timeoutMs 1–10,000; includes worker startup/input |
|
|
116
|
+
| Active extractions | 2; no queue; busy still preserves the original action result/ref |
|
|
117
|
+
| Failed worker diagnostics | Most recent 8 KiB stderr; stack at most 20 frames and 8 KiB |
|
|
118
|
+
|
|
119
|
+
No budget error returns a truncated JSON document or dumps the large original
|
|
120
|
+
value. If the protected controls themselves cannot fit, `controlComplete:false`
|
|
121
|
+
and `control_over_budget` prohibit continuing from incomplete controls. Use a
|
|
122
|
+
real ref when available, with a sufficient budget or a narrower command query.
|
|
123
|
+
Discovery/help has a separate fixed 96 KiB limit; follow its command/operation
|
|
124
|
+
hint. Runtime stop drains active extraction workers; CLI/MCP disconnect alone
|
|
125
|
+
keeps the shared Runtime alive. Extraction is trusted local code, with bounded
|
|
126
|
+
worker/channel lifetime rather than an OS/process-tree sandbox.
|
package/docs/SCRIPT_AUTHORING.md
CHANGED
|
@@ -12,6 +12,7 @@ Call MCP `run` with this shape, replacing the target and source path:
|
|
|
12
12
|
```json
|
|
13
13
|
{
|
|
14
14
|
"command": "script",
|
|
15
|
+
"extract": null,
|
|
15
16
|
"arguments": {
|
|
16
17
|
"operation": "start",
|
|
17
18
|
"script": {
|
|
@@ -20,10 +21,21 @@ Call MCP `run` with this shape, replacing the target and source path:
|
|
|
20
21
|
"language": "javascript",
|
|
21
22
|
"sourcePath": "/absolute/flow.js",
|
|
22
23
|
"entrypoint": "main",
|
|
23
|
-
"target": {
|
|
24
|
+
"target": {
|
|
25
|
+
"platform": "android",
|
|
26
|
+
"serial": "explicit-device",
|
|
27
|
+
"packageName": "explicit.package"
|
|
28
|
+
},
|
|
24
29
|
"inputs": {},
|
|
25
|
-
"permissions": [
|
|
26
|
-
|
|
30
|
+
"permissions": [
|
|
31
|
+
"app.read",
|
|
32
|
+
"app.interact",
|
|
33
|
+
"capture.read"
|
|
34
|
+
],
|
|
35
|
+
"policy": {
|
|
36
|
+
"timeoutMs": 180000,
|
|
37
|
+
"restartPolicy": "none"
|
|
38
|
+
}
|
|
27
39
|
}
|
|
28
40
|
}
|
|
29
41
|
}
|
|
@@ -32,9 +44,9 @@ Call MCP `run` with this shape, replacing the target and source path:
|
|
|
32
44
|
The same request is available through CLI:
|
|
33
45
|
|
|
34
46
|
```sh
|
|
35
|
-
ai-app-bridge script --operation start --script '{"schemaVersion":"aab.code-script/v1","language":"javascript","sourcePath":"./flow.js","permissions":["app.read","app.interact","capture.read"]}'
|
|
36
|
-
ai-app-bridge script --operation status --operation-id RETURNED_ID
|
|
37
|
-
ai-app-bridge script --operation result --operation-id RETURNED_ID
|
|
47
|
+
ai-app-bridge script --extract null --operation start --script '{"schemaVersion":"aab.code-script/v1","language":"javascript","sourcePath":"./flow.js","permissions":["app.read","app.interact","capture.read"]}'
|
|
48
|
+
ai-app-bridge script --extract null --operation status --operation-id RETURNED_ID
|
|
49
|
+
ai-app-bridge script --extract null --operation result --operation-id RETURNED_ID
|
|
38
50
|
```
|
|
39
51
|
|
|
40
52
|
The CLI returns the operation under `value`. CLI and MCP share a persistent runtime;
|
|
@@ -138,6 +150,113 @@ Terminal rolling summaries freeze elapsed and active durations at the terminal
|
|
|
138
150
|
event. Later status queries do not add idle time. For end-to-end measurement,
|
|
139
151
|
retain the public start timestamp and terminal event timestamp separately.
|
|
140
152
|
|
|
153
|
+
## Lifecycle: start, wait, result
|
|
154
|
+
|
|
155
|
+
A fixed regression is one start, a wait loop and one result read. The caller
|
|
156
|
+
drives these three `run` calls; the program owns observation, actions and
|
|
157
|
+
assertions. The repository test suite runs the two sources below against the
|
|
158
|
+
current contract with an injected device, so they match the installed package.
|
|
159
|
+
|
|
160
|
+
1. Start with `sourcePath` for repeated runs (or `source`), a platform-qualified
|
|
161
|
+
`target`, and the run's values in `inputs`:
|
|
162
|
+
|
|
163
|
+
```json lifecycle-example
|
|
164
|
+
{"command":"script","extract":null,"arguments":{"operation":"start","script":{"schemaVersion":"aab.code-script/v1","name":"labels-regression","language":"javascript","sourcePath":"./regression.js","target":{"platform":"android","serial":"<serial>","packageName":"<package>"},"inputs":{"entryText":"Labels","expectedText":"New label"}}}}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
2. Wait with the returned `operationId`. `waitMs` is at most 60000 per call.
|
|
168
|
+
While `status` is `running` or `finishing`, call `wait` again with the
|
|
169
|
+
previous response's `eventSequence` as `afterSequence`:
|
|
170
|
+
|
|
171
|
+
```json lifecycle-example
|
|
172
|
+
{"command":"script","extract":null,"arguments":{"operation":"wait","operationId":"<operationId>","waitMs":30000,"afterSequence":0}}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`finishing` means the program has returned and the result is still being
|
|
176
|
+
persisted; `result` answers `result_not_ready` until the status is terminal.
|
|
177
|
+
`waiting_for_agent` means the program called `ctx.askAgent`; the question is
|
|
178
|
+
the `agent_question_created` event with `requestId`, `revision` and `request`.
|
|
179
|
+
Answer it with `decide` or end the run with `cancel`. Keep every event page:
|
|
180
|
+
a later `afterSequence` no longer returns that event. Do not wait it out.
|
|
181
|
+
|
|
182
|
+
3. When `status` is `completed`, `failed` or `cancelled`, read the value:
|
|
183
|
+
|
|
184
|
+
```json lifecycle-example
|
|
185
|
+
{"command":"script","extract":null,"arguments":{"operation":"result","operationId":"<operationId>"}}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`completed` only states that the program returned and its value was
|
|
189
|
+
persisted; the verdicts inside `result` decide each case. `failed` and
|
|
190
|
+
`cancelled` return `result_unavailable`; the terminal event and `error`
|
|
191
|
+
carry the reason.
|
|
192
|
+
|
|
193
|
+
The JavaScript program. Each case records a device assertion against the
|
|
194
|
+
observation it judges, and the run stops at the first case that is not `passed`:
|
|
195
|
+
|
|
196
|
+
```javascript regression-example
|
|
197
|
+
'use strict';
|
|
198
|
+
|
|
199
|
+
// Fixed regression: observe, act once, observe again. Per-run values arrive in
|
|
200
|
+
// ctx.inputs; the returned cases are read with script operation:"result".
|
|
201
|
+
module.exports.main = async function main(ctx) {
|
|
202
|
+
const cases = [];
|
|
203
|
+
async function observe(step) {
|
|
204
|
+
const read = await ctx.call('tree', { compact: true, visibleOnly: true, maxNodes: 1000 });
|
|
205
|
+
if (!read.ok) throw new Error(`${step}:tree:${read.error}`);
|
|
206
|
+
return read;
|
|
207
|
+
}
|
|
208
|
+
async function check(name, read, condition) {
|
|
209
|
+
const verdict = await ctx.assert({ name, condition, requiredEvidence: ['tree'], evidence: read.evidence });
|
|
210
|
+
cases.push({ name, verdict: verdict.verdict, reason: verdict.reason || null });
|
|
211
|
+
if (verdict.verdict !== 'passed') throw new Error(`${name}:${verdict.verdict}`);
|
|
212
|
+
}
|
|
213
|
+
const before = await observe('before');
|
|
214
|
+
await check('entry control is visible', before,
|
|
215
|
+
before.result.nodes.some(node => node.text === ctx.inputs.entryText && node.visible === true));
|
|
216
|
+
const tap = await ctx.call('tap-text', { targetText: ctx.inputs.entryText });
|
|
217
|
+
if (!tap.ok) throw new Error(`tap-text:${tap.error}`);
|
|
218
|
+
const after = await observe('after');
|
|
219
|
+
await check('expected text is shown after the tap', after,
|
|
220
|
+
after.result.nodes.some(node => node.text === ctx.inputs.expectedText));
|
|
221
|
+
await ctx.progress({ phase: 'done', cases: cases.length });
|
|
222
|
+
return { cases, actionId: tap.execution.actionId };
|
|
223
|
+
};
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The same program in Python; `ctx.assert_` takes one dictionary and envelopes are
|
|
227
|
+
dictionaries:
|
|
228
|
+
|
|
229
|
+
```python regression-example
|
|
230
|
+
# Fixed regression: observe, act once, observe again. Per-run values arrive in
|
|
231
|
+
# ctx.inputs; the returned cases are read with script operation:"result".
|
|
232
|
+
def main(ctx):
|
|
233
|
+
cases = []
|
|
234
|
+
|
|
235
|
+
def observe(step):
|
|
236
|
+
read = ctx.call("tree", {"compact": True, "visibleOnly": True, "maxNodes": 1000})
|
|
237
|
+
if not read["ok"]:
|
|
238
|
+
raise Exception(f"{step}:tree:{read['error']}")
|
|
239
|
+
return read
|
|
240
|
+
|
|
241
|
+
def check(name, read, condition):
|
|
242
|
+
verdict = ctx.assert_({"name": name, "condition": condition, "requiredEvidence": ["tree"], "evidence": read["evidence"]})
|
|
243
|
+
cases.append({"name": name, "verdict": verdict["verdict"], "reason": verdict.get("reason")})
|
|
244
|
+
if verdict["verdict"] != "passed":
|
|
245
|
+
raise Exception(f"{name}:{verdict['verdict']}")
|
|
246
|
+
|
|
247
|
+
before = observe("before")
|
|
248
|
+
check("entry control is visible", before,
|
|
249
|
+
any(node.get("text") == ctx.inputs["entryText"] and node.get("visible") is True for node in before["result"]["nodes"]))
|
|
250
|
+
tap = ctx.call("tap-text", {"targetText": ctx.inputs["entryText"]})
|
|
251
|
+
if not tap["ok"]:
|
|
252
|
+
raise Exception(f"tap-text:{tap['error']}")
|
|
253
|
+
after = observe("after")
|
|
254
|
+
check("expected text is shown after the tap", after,
|
|
255
|
+
any(node.get("text") == ctx.inputs["expectedText"] for node in after["result"]["nodes"]))
|
|
256
|
+
ctx.progress({"phase": "done", "cases": len(cases)})
|
|
257
|
+
return {"cases": cases, "actionId": tap["execution"]["actionId"]}
|
|
258
|
+
```
|
|
259
|
+
|
|
141
260
|
## Capability selection
|
|
142
261
|
|
|
143
262
|
Use `capabilities` to inspect each command's `inputSchema` and `entrypoints.script`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Release prebuilds
|
|
2
|
+
|
|
3
|
+
The normal npm install path verifies `prebuilds/manifest.json` and loads the
|
|
4
|
+
selected Node-API 8 artifact. It does not invoke Python, node-gyp or a compiler.
|
|
5
|
+
Unsupported targets and missing/corrupt binaries fail explicitly.
|
|
6
|
+
|
|
7
|
+
The 0.2.0 native package bundled in Bridge 0.4.0 includes macOS arm64/x64
|
|
8
|
+
(deployment target 13.5), and Linux glibc arm64/x64 (target glibc 2.28).
|
|
9
|
+
The Bridge Host still requires Node >=26.3.0 <27. Windows and musl Linux are
|
|
10
|
+
outside this release's native support matrix.
|
|
11
|
+
|
|
12
|
+
Maintainers can recreate all four artifacts on macOS with Apple clang,
|
|
13
|
+
the official Node 26.3.0 headers and Zig 0.15.2:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
node scripts/build-release-prebuilds.js /absolute/path/to/include/node /absolute/path/to/zig
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Verify the Node and Zig downloads against their official checksums before
|
|
20
|
+
building. The script compiles `src/sfs.c` and `bindings/node/sfs_node.c` and
|
|
21
|
+
regenerates SHA-256 metadata. Check the diff, then run the native tests and
|
|
22
|
+
Bridge's `verify:package` on every target before release. Compiler/platform
|
|
23
|
+
differences may change binary hashes; the manifest must describe the files
|
|
24
|
+
actually tested and packaged.
|
|
25
|
+
|
|
26
|
+
`npm run build` is an explicit developer-only node-gyp build for the current
|
|
27
|
+
host. Its staging step marks that artifact as `sourceBuild` and records the
|
|
28
|
+
host glibc version. Such a build is not evidence for the lower release glibc
|
|
29
|
+
baseline. Never substitute it for release matrix testing.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { createHash } = require('node:crypto');
|
|
5
|
+
|
|
6
|
+
function resolveBinding() {
|
|
7
|
+
const platform = process.platform, arch = process.arch, napi = Number(process.versions.napi);
|
|
8
|
+
const glibc = platform === 'linux' ? process.report.getReport().header.glibcVersionRuntime : undefined;
|
|
9
|
+
const target = `${platform}-${arch}${platform === 'linux' ? '-glibc' : ''}`;
|
|
10
|
+
const description = `${platform}/${arch}, Node ${process.versions.node}, Node-API ${napi}, libc ${glibc || 'not-glibc'}`;
|
|
11
|
+
const manifest = require('./prebuilds/manifest.json');
|
|
12
|
+
const artifact = manifest.artifacts[target];
|
|
13
|
+
const minimum = artifact?.minGlibc;
|
|
14
|
+
const reject = (code, message) => { throw Object.assign(new Error(`${message} (${description}). No automatic source build is attempted.`), { code }); };
|
|
15
|
+
if (artifact && platform === 'linux' && !/^\d+\.\d+$/.test(minimum)) reject('native_prebuild_corrupt', 'The packaged Linux artifact has no valid minimum glibc version');
|
|
16
|
+
const olderGlibc = glibc && minimum && (Number(glibc.split('.')[0]) < Number(minimum.split('.')[0]) ||
|
|
17
|
+
(Number(glibc.split('.')[0]) === Number(minimum.split('.')[0]) && Number(glibc.split('.')[1]) < Number(minimum.split('.')[1])));
|
|
18
|
+
if (!artifact || napi < manifest.napi || (platform === 'linux' && (!glibc || olderGlibc))) {
|
|
19
|
+
reject('native_platform_unsupported', 'Supported prebuilds require macOS 13.5+ arm64/x64 or Linux glibc 2.28+ arm64/x64, and Node-API 8+');
|
|
20
|
+
}
|
|
21
|
+
const file = path.join(__dirname, 'prebuilds', target, 'segmented_fact_store.node');
|
|
22
|
+
let bytes;
|
|
23
|
+
try { bytes = fs.readFileSync(file); }
|
|
24
|
+
catch (error) { reject('native_prebuild_missing', `Cannot read packaged native artifact ${file}: ${error.code}`); }
|
|
25
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
26
|
+
if (sha256 !== artifact.sha256) reject('native_prebuild_corrupt', `Checksum mismatch for ${file}; reinstall this package`);
|
|
27
|
+
return { path: file, target, sha256, napi: manifest.napi, ...artifact };
|
|
28
|
+
}
|
|
29
|
+
module.exports = { resolveBinding };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mobileaidev/segmented-fact-store-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Portable segmented mmap fact-store core and Node-API binding for AI App Bridge.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"gypfile":
|
|
6
|
+
"gypfile": false,
|
|
7
7
|
"files": [
|
|
8
8
|
"binding.gyp",
|
|
9
9
|
"index.js",
|
|
@@ -11,11 +11,17 @@
|
|
|
11
11
|
"src",
|
|
12
12
|
"bindings/node/sfs_node.c",
|
|
13
13
|
"LICENSE",
|
|
14
|
-
"NOTICE"
|
|
14
|
+
"NOTICE",
|
|
15
|
+
"binding-path.js",
|
|
16
|
+
"install.js",
|
|
17
|
+
"prebuilds",
|
|
18
|
+
"scripts",
|
|
19
|
+
"PREBUILDS.md"
|
|
15
20
|
],
|
|
16
21
|
"scripts": {
|
|
17
|
-
"build": "node-gyp rebuild",
|
|
18
|
-
"test": "node --test bindings/node/test/*.test.js"
|
|
22
|
+
"build": "node-gyp rebuild && node scripts/stage-prebuild.js",
|
|
23
|
+
"test": "node --test bindings/node/test/*.test.js",
|
|
24
|
+
"install": "node install.js"
|
|
19
25
|
},
|
|
20
26
|
"engines": {
|
|
21
27
|
"node": ">=18"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"napi": 8,
|
|
3
|
+
"artifacts": {
|
|
4
|
+
"darwin-arm64": {
|
|
5
|
+
"sha256": "04c0ce98f12b8628cb6c24dd9193c147fed94ad1b99eae8da4c0abe9aa3825a8",
|
|
6
|
+
"minSystem": "macOS 13.5",
|
|
7
|
+
"compiler": "Apple clang, -O2 -mmacosx-version-min=13.5"
|
|
8
|
+
},
|
|
9
|
+
"darwin-x64": {
|
|
10
|
+
"sha256": "ed95bc5f818de53d479a842125316ac8c2447ab9d80a1c7903384e6818e7d8d8",
|
|
11
|
+
"minSystem": "macOS 13.5",
|
|
12
|
+
"compiler": "Apple clang, -O2 -mmacosx-version-min=13.5"
|
|
13
|
+
},
|
|
14
|
+
"linux-arm64-glibc": {
|
|
15
|
+
"sha256": "f476465bc0e7b8025fb3f92cfc2dc47b2d62f57aa43b48eb2f71f892f92abd59",
|
|
16
|
+
"minGlibc": "2.28",
|
|
17
|
+
"minSystem": "Linux glibc 2.28",
|
|
18
|
+
"compiler": "zig cc 0.15.2, -O2 -target *-linux-gnu.2.28"
|
|
19
|
+
},
|
|
20
|
+
"linux-x64-glibc": {
|
|
21
|
+
"sha256": "dc3d0ab106164a861c2781e7bb6771ea52e35b3735b7a87e87dfcfc5c77ae647",
|
|
22
|
+
"minGlibc": "2.28",
|
|
23
|
+
"minSystem": "Linux glibc 2.28",
|
|
24
|
+
"compiler": "zig cc 0.15.2, -O2 -target *-linux-gnu.2.28"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/node_modules/@mobileaidev/segmented-fact-store-native/scripts/build-release-prebuilds.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Maintainer-only cross-build on macOS. Normal installation only verifies and
|
|
4
|
+
// loads the checked-in artifact; it never runs a compiler.
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { execFileSync } = require('node:child_process');
|
|
8
|
+
const { createHash } = require('node:crypto');
|
|
9
|
+
|
|
10
|
+
const [headers, zig] = process.argv.slice(2);
|
|
11
|
+
if (process.platform !== 'darwin' || !headers || !zig) {
|
|
12
|
+
throw new Error('Usage on macOS: node scripts/build-release-prebuilds.js <Node include/node directory> <Zig 0.15.2 executable>');
|
|
13
|
+
}
|
|
14
|
+
if (execFileSync(zig, ['version'], { encoding: 'utf8' }).trim() !== '0.15.2') throw new Error('Zig 0.15.2 is required');
|
|
15
|
+
const root = path.resolve(__dirname, '..');
|
|
16
|
+
const common = ['-O2', '-std=c11', '-DNAPI_VERSION=8', '-D_POSIX_C_SOURCE=200809L',
|
|
17
|
+
'-I', path.resolve(headers), '-I', path.join(root, 'include'),
|
|
18
|
+
path.join(root, 'src/sfs.c'), path.join(root, 'bindings/node/sfs_node.c')];
|
|
19
|
+
const manifest = { napi: 8, artifacts: {} };
|
|
20
|
+
for (const [target, compiler, args, metadata] of [
|
|
21
|
+
['darwin-arm64', 'clang', ['-arch', 'arm64', '-mmacosx-version-min=13.5', '-bundle', '-undefined', 'dynamic_lookup'], { minSystem: 'macOS 13.5', compiler: 'Apple clang, -O2 -mmacosx-version-min=13.5' }],
|
|
22
|
+
['darwin-x64', 'clang', ['-arch', 'x86_64', '-mmacosx-version-min=13.5', '-bundle', '-undefined', 'dynamic_lookup'], { minSystem: 'macOS 13.5', compiler: 'Apple clang, -O2 -mmacosx-version-min=13.5' }],
|
|
23
|
+
...[['arm64', 'aarch64'], ['x64', 'x86_64']].map(([arch, triple]) => [
|
|
24
|
+
`linux-${arch}-glibc`, zig, ['cc', '-target', `${triple}-linux-gnu.2.28`, '-fPIC', '-shared', '-lpthread'],
|
|
25
|
+
{ minGlibc: '2.28', minSystem: 'Linux glibc 2.28', compiler: 'zig cc 0.15.2, -O2 -target *-linux-gnu.2.28' },
|
|
26
|
+
]),
|
|
27
|
+
]) {
|
|
28
|
+
const file = path.join(root, 'prebuilds', target, 'segmented_fact_store.node');
|
|
29
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
30
|
+
execFileSync(compiler, [...args, ...common, '-o', file], { stdio: 'inherit' });
|
|
31
|
+
manifest.artifacts[target] = { sha256: createHash('sha256').update(fs.readFileSync(file)).digest('hex'), ...metadata };
|
|
32
|
+
}
|
|
33
|
+
fs.writeFileSync(path.join(root, 'prebuilds/manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Explicit developer build only. The release script assembles all matrix artifacts.
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { createHash } = require('node:crypto');
|
|
6
|
+
const root = path.resolve(__dirname, '..');
|
|
7
|
+
const target = `${process.platform}-${process.arch}${process.platform === 'linux' ? '-glibc' : ''}`;
|
|
8
|
+
if (!['darwin-arm64', 'darwin-x64', 'linux-arm64-glibc', 'linux-x64-glibc'].includes(target)) throw new Error(`Unsupported release target: ${target}`);
|
|
9
|
+
const bytes = fs.readFileSync(path.join(root, 'build/Release/segmented_fact_store.node'));
|
|
10
|
+
const directory = path.join(root, 'prebuilds', target); fs.mkdirSync(directory, { recursive: true });
|
|
11
|
+
fs.writeFileSync(path.join(directory, 'segmented_fact_store.node'), bytes);
|
|
12
|
+
const file = path.join(root, 'prebuilds/manifest.json');
|
|
13
|
+
const manifest = JSON.parse(fs.readFileSync(file));
|
|
14
|
+
manifest.artifacts[target] = { sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
15
|
+
...(process.platform === 'linux' ? { minGlibc: process.report.getReport().header.glibcVersionRuntime } : {}),
|
|
16
|
+
minSystem: process.platform === 'darwin' ? 'macOS 13.5' : `glibc ${process.report.getReport().header.glibcVersionRuntime}`, sourceBuild: true };
|
|
17
|
+
fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + '\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mobileaidev/ai-app-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Desktop CLI and MCP server for AI App Bridge across Android, iOS, Flutter, WebView, and Web targets.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
"bin/device-provider.js",
|
|
38
38
|
"bin/command-discovery.js",
|
|
39
39
|
"bin/command-request.js",
|
|
40
|
+
"bin/public-reply.js",
|
|
41
|
+
"bin/response-store.js",
|
|
42
|
+
"bin/extraction",
|
|
40
43
|
"bin/execution-runtime.js",
|
|
41
44
|
"bin/runtime-client.js",
|
|
42
45
|
"bin/runtime-directory.js",
|
|
@@ -74,12 +77,16 @@
|
|
|
74
77
|
"runtime/ios-wda",
|
|
75
78
|
"LICENSE",
|
|
76
79
|
"NOTICE",
|
|
77
|
-
"docs/RELEASE.md"
|
|
80
|
+
"docs/RELEASE.md",
|
|
81
|
+
"docs/RESPONSE_EXTRACTION.md",
|
|
82
|
+
"docs/INSTALLATION.md"
|
|
78
83
|
],
|
|
79
84
|
"scripts": {
|
|
80
|
-
"check": "node -c bin/ai-app-bridge.js && node -c bin/mcp-server.js && node -c bin/ios-provider.js && node
|
|
81
|
-
"test": "node
|
|
82
|
-
"verify:package": "node scripts/validation/verify-package.js"
|
|
85
|
+
"check": "node -c bin/ai-app-bridge.js && node -c bin/mcp-server.js && node -c bin/ios-provider.js && node scripts/validation/test-suite.js",
|
|
86
|
+
"test": "node scripts/validation/test-suite.js",
|
|
87
|
+
"verify:package": "node scripts/validation/verify-package.js",
|
|
88
|
+
"test:functional": "node scripts/validation/test-suite.js functional",
|
|
89
|
+
"test:performance": "node scripts/validation/test-suite.js performance"
|
|
83
90
|
},
|
|
84
91
|
"license": "Apache-2.0",
|
|
85
92
|
"dependencies": {
|
|
@@ -31,6 +31,28 @@ gradle.beforeProject { consumer ->
|
|
|
31
31
|
}
|
|
32
32
|
consumer.pluginManager.apply(io.github.mobileaidev.aiappbridge.gradle.AiAppBridgeTestGradlePlugin)
|
|
33
33
|
consumer.extensions.getByName('androidComponents').finalizeDsl { android ->
|
|
34
|
+
// Only requirements of the dependencies selected by this preparer.
|
|
35
|
+
// Read evaluated DSL; never rewrite compileSdk or guess from text.
|
|
36
|
+
def agp = consumer.plugins.getPlugin('com.android.application').class.classLoader
|
|
37
|
+
.loadClass('com.android.Version').getField('ANDROID_GRADLE_PLUGIN_VERSION').get(null).toString()
|
|
38
|
+
def actual = [compileSdk: android.compileSdk, compileSdkPreview: android.compileSdkPreview,
|
|
39
|
+
agp: agp, gradle: consumer.gradle.gradleVersion, java: System.getProperty('java.version')]
|
|
40
|
+
def issues = []
|
|
41
|
+
def unresolved = actual.compileSdk == null || actual.compileSdkPreview != null || !(agp ==~ /\d+\.\d+\.\d+/)
|
|
42
|
+
if (actual.compileSdk == null || actual.compileSdkPreview != null) issues.add('A numeric stable compileSdk is required to check the selected dependencies')
|
|
43
|
+
else if (actual.compileSdk < config.requirements.minCompileSdk) issues.add('compileSdk ' + actual.compileSdk + ' < required ' + config.requirements.minCompileSdk)
|
|
44
|
+
if (!(agp ==~ /\d+\.\d+\.\d+/)) issues.add('AGP version could not be compared: ' + agp)
|
|
45
|
+
else {
|
|
46
|
+
def got = agp.tokenize('.').collect { it.toInteger() }
|
|
47
|
+
def need = config.requirements.minAgp.tokenize('.').collect { it.toInteger() }
|
|
48
|
+
def different = (0..2).find { got[it] != need[it] }
|
|
49
|
+
if (different != null && got[different] < need[different]) issues.add('AGP ' + agp + ' < required ' + config.requirements.minAgp)
|
|
50
|
+
}
|
|
51
|
+
def report = [module: consumer.path, variant: config.variant, actual: actual, requirements: config.requirements,
|
|
52
|
+
status: issues.empty ? 'compatible' : (unresolved ? 'unresolved' : 'incompatible'), issues: issues,
|
|
53
|
+
impact: 'Selected application module and its androidTest variant; temporary runner, test sources and dependencies are added for this invocation. compileSdk is unchanged.']
|
|
54
|
+
new File(config.directory, 'android-preflight.json').text = groovy.json.JsonOutput.toJson(report)
|
|
55
|
+
if (!issues.empty) throw new GradleException('[AiAppBridge] Executor preflight: ' + issues.join('; ') + '. No project configuration was upgraded.')
|
|
34
56
|
if (!android.defaultConfig.testInstrumentationRunner) {
|
|
35
57
|
android.defaultConfig.testInstrumentationRunner = 'androidx.test.runner.AndroidJUnitRunner'
|
|
36
58
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-app-bridge-playwright-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "ai-app-bridge-playwright-runtime",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.4.1",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"playwright": "1.63.0"
|
|
12
12
|
},
|