@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.
Files changed (58) hide show
  1. package/README.md +54 -39
  2. package/bin/ai-app-bridge.js +56 -17
  3. package/bin/command-discovery.js +19 -4
  4. package/bin/command-registry.js +17 -4
  5. package/bin/command-request.js +68 -4
  6. package/bin/execution-host.js +31 -5
  7. package/bin/execution-runtime.js +17 -10
  8. package/bin/executors/preparation.js +19 -5
  9. package/bin/extraction/json-value.js +26 -0
  10. package/bin/extraction/prepare.js +57 -0
  11. package/bin/extraction/regex.js +30 -0
  12. package/bin/extraction/runner.js +77 -0
  13. package/bin/ios-execution.js +6 -0
  14. package/bin/ios-provider.js +3 -2
  15. package/bin/mcp-server.js +15 -20
  16. package/bin/public-reply.js +184 -0
  17. package/bin/response-store.js +60 -0
  18. package/bin/runtime-client.js +32 -15
  19. package/bin/runtime-directory.js +37 -8
  20. package/bin/script/node-runtime-adapter.js +139 -123
  21. package/bin/script/python-runtime-adapter.js +1 -1
  22. package/bin/script/script-diagnostics.js +21 -0
  23. package/bin/script/script-durable-restore.js +1 -0
  24. package/bin/script/script-sdk.js +39 -4
  25. package/bin/script/script-sdk.py +79 -7
  26. package/bin/script/script-session-channel.js +27 -10
  27. package/bin/script/script-supervisor.js +8 -0
  28. package/bin/shared-kernel/argument-schema.js +44 -12
  29. package/bin/shared-kernel/evidence-archive.js +2 -2
  30. package/bin/shared-kernel/evidence-schema.js +16 -1
  31. package/bin/shared-kernel/evidence-store.js +3 -3
  32. package/bin/shared-kernel/execution-contracts.js +13 -5
  33. package/docs/COMMAND_CONTRACT.md +102 -19
  34. package/docs/EVIDENCE_ARCHIVE.md +14 -1
  35. package/docs/INSTALLATION.md +74 -0
  36. package/docs/INTENT_FOREGROUND.md +4 -1
  37. package/docs/OPTIONAL_EXECUTORS.md +14 -14
  38. package/docs/RELEASE.md +71 -122
  39. package/docs/RESPONSE_EXTRACTION.md +126 -0
  40. package/docs/SCRIPT_AUTHORING.md +125 -6
  41. package/node_modules/@mobileaidev/segmented-fact-store-native/PREBUILDS.md +29 -0
  42. package/node_modules/@mobileaidev/segmented-fact-store-native/binding-path.js +29 -0
  43. package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +1 -0
  44. package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +1 -3
  45. package/node_modules/@mobileaidev/segmented-fact-store-native/install.js +5 -0
  46. package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +11 -5
  47. package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/darwin-arm64/segmented_fact_store.node +0 -0
  48. package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/darwin-x64/segmented_fact_store.node +0 -0
  49. package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/linux-arm64-glibc/segmented_fact_store.node +0 -0
  50. package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/linux-x64-glibc/segmented_fact_store.node +0 -0
  51. package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/manifest.json +27 -0
  52. package/node_modules/@mobileaidev/segmented-fact-store-native/scripts/build-release-prebuilds.js +33 -0
  53. package/node_modules/@mobileaidev/segmented-fact-store-native/scripts/stage-prebuild.js +17 -0
  54. package/package.json +12 -5
  55. package/runtime/executors/android/prepare.init.gradle +22 -0
  56. package/runtime/executors/playwright/package-lock.json +2 -2
  57. package/runtime/executors/playwright/package.json +1 -1
  58. 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.
@@ -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": {"platform":"android", "serial": "explicit-device", "packageName": "explicit.package"},
24
+ "target": {
25
+ "platform": "android",
26
+ "serial": "explicit-device",
27
+ "packageName": "explicit.package"
28
+ },
24
29
  "inputs": {},
25
- "permissions": ["app.read", "app.interact", "capture.read"],
26
- "policy": {"timeoutMs": 180000, "restartPolicy": "none"}
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 };
@@ -19,6 +19,7 @@
19
19
  "-Wpedantic"
20
20
  ],
21
21
  "xcode_settings": {
22
+ "MACOSX_DEPLOYMENT_TARGET": "13.5",
22
23
  "GCC_C_LANGUAGE_STANDARD": "c11",
23
24
  "WARNING_CFLAGS": [
24
25
  "-Wall",
@@ -1,5 +1,3 @@
1
1
  'use strict';
2
2
 
3
- const path = require('node:path');
4
-
5
- module.exports = require(path.join(__dirname, 'build', 'Release', 'segmented_fact_store.node'));
3
+ module.exports = require(require('./binding-path').resolveBinding().path);
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+ // Validate the bundled binary; never invoke a compiler or download a substitute.
3
+ require('./index');
4
+ const info = require('./binding-path').resolveBinding();
5
+ process.stdout.write(`FactStore prebuild ${info.target} ${info.sha256}\n`);
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@mobileaidev/segmented-fact-store-native",
3
- "version": "0.1.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": true,
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"
@@ -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
+ }
@@ -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.8",
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 --import ./test-support/ownership-test-sandbox.js --test",
81
- "test": "node --import ./test-support/ownership-test-sandbox.js --test",
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.8",
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.3.8",
9
+ "version": "0.4.1",
10
10
  "dependencies": {
11
11
  "playwright": "1.63.0"
12
12
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-app-bridge-playwright-runtime",
3
- "version": "0.3.8",
3
+ "version": "0.4.1",
4
4
  "private": true,
5
5
  "description": "Optional, isolated browser runtime managed by AI App Bridge.",
6
6
  "engines": { "node": ">=26.3.0 <27" },