@condorinst/mobile-bridge 0.22.0 → 0.23.0
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/AGENTS.md +133 -34
- package/CHANGELOG.md +175 -0
- package/README.md +30 -481
- package/contract/bridge-contract.json +32 -21
- package/dist/cli.cjs +126 -29
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +126 -29
- package/dist/cli.js.map +1 -1
- package/dist/dev-harness.global.js +388 -55
- package/dist/dev.cjs +389 -55
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.d.cts +239 -9
- package/dist/dev.d.ts +239 -9
- package/dist/dev.js +388 -55
- package/dist/dev.js.map +1 -1
- package/dist/index.cjs +30 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -26
- package/dist/index.d.ts +154 -26
- package/dist/index.js +29 -8
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +9 -265
- package/docs/bridge-contract.md +313 -26
- package/docs/client-guide.md +34 -400
- package/docs/device-testing.md +234 -0
- package/docs/pilot-onboarding.md +41 -115
- package/docs/shell-integration.md +292 -0
- package/docs/troubleshooting.md +19 -11
- package/examples/minimal-home/README.md +2 -2
- package/examples/minimal-home/package.json +1 -1
- package/examples/minimal-home/src/main.ts +23 -3
- package/llms-full.txt +764 -865
- package/llms.txt +13 -15
- package/package.json +1 -1
package/docs/agent-guide.md
CHANGED
|
@@ -1,267 +1,11 @@
|
|
|
1
|
-
# Agent guide —
|
|
1
|
+
# Agent guide — merged into AGENTS.md
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
This page used to repeat `AGENTS.md` as a staged procedure. Two copies of the
|
|
4
|
+
same rules drifted apart, so there is now one of each:
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
it, verifies its SHA-256, and renders it inside a sandboxed iframe as the Home
|
|
13
|
-
screen. Login, settings, Bluetooth and background collection are the app's job.
|
|
14
|
-
Yours is what the person sees when the app opens.
|
|
15
|
-
|
|
16
|
-
You are not building a website. You have no storage, no network, and no origin.
|
|
17
|
-
Everything you display comes through the bridge, and everything you hold lives
|
|
18
|
-
in memory until the next reload.
|
|
19
|
-
|
|
20
|
-
## Stage 0 — set up so you can see what you are doing
|
|
21
|
-
|
|
22
|
-
Start from `examples/minimal-home/` — a complete, working client with the
|
|
23
|
-
reference boot sequence, and a build script that produces a single-file bundle.
|
|
24
|
-
Copy it first, then install, so its `package.json` is the one you keep:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
mkdir my-home && cd my-home
|
|
28
|
-
npm pack @condorinst/mobile-bridge@^0.22.0 && tar -xzf condorinst-mobile-bridge-*.tgz
|
|
29
|
-
cp -r package/examples/minimal-home/. . && rm -rf package condorinst-mobile-bridge-*.tgz
|
|
30
|
-
npm install
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Or, in a project you already have: `npm install @condorinst/mobile-bridge@^0.22.0`
|
|
34
|
-
and copy only `src/main.ts`, `build.mjs` and `index.html` from
|
|
35
|
-
`node_modules/@condorinst/mobile-bridge/examples/minimal-home/`.
|
|
36
|
-
|
|
37
|
-
Pin the SDK to the minor you are building against. Below `1.0.0`, a minor can
|
|
38
|
-
change the contract — `^0.22.0` gives you patches only, which is what you want.
|
|
39
|
-
|
|
40
|
-
**Run it in the harness**, not in a bare browser tab:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npm run build
|
|
44
|
-
npx condor-mobile-bridge dev -i dist/index.html --scenario default
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
The harness serves your bundle inside the same sandboxed iframe, shell, policy
|
|
48
|
-
and CSP as the app, with a simulated native side behind it, and a dev panel to
|
|
49
|
-
emit events by hand. It re-reads the file on every reload. Opening
|
|
50
|
-
`dist/index.html` directly does **not** work: there is no shell, so
|
|
51
|
-
`BridgeClient.handshake()` rejects at once.
|
|
52
|
-
|
|
53
|
-
The scenarios reproduce what the device does to you — switch with the selector
|
|
54
|
-
at the top or `--scenario`:
|
|
55
|
-
|
|
56
|
-
| Scenario | Your bundle must… |
|
|
57
|
-
| --- | --- |
|
|
58
|
-
| `default` | render two devices |
|
|
59
|
-
| `cold-start` | leave the loading screen although the first `userDevice` requests are lost |
|
|
60
|
-
| `no-devices` | show an empty state when `userDevice` never answers |
|
|
61
|
-
| `shared-serial` | keep two devices with the same `serialNo` apart |
|
|
62
|
-
| `offline` | explain that history is unavailable without internet |
|
|
63
|
-
|
|
64
|
-
Add `--native-version null` (or an older version such as `0.15.0`) to test
|
|
65
|
-
your capability branches, and use the **◀ OS back** button at the top to press
|
|
66
|
-
the OS back button.
|
|
67
|
-
|
|
68
|
-
If you already have a dev server (Vite), mount the same harness from your own
|
|
69
|
-
page with `createDevHarness()` from `@condorinst/mobile-bridge/dev` — in the
|
|
70
|
-
host page only, never in the bundle (`validate` rejects that).
|
|
71
|
-
|
|
72
|
-
## Stage 1 — render before you connect
|
|
73
|
-
|
|
74
|
-
Write the screen so it paints correctly with **no data at all**: empty roster,
|
|
75
|
-
unknown status, default language. Then fill it as events arrive.
|
|
76
|
-
|
|
77
|
-
This is not a nicety. Events are emitted on attach, before your bundle exists,
|
|
78
|
-
and again after the handshake — so there is always a window where you have
|
|
79
|
-
nothing. A Home whose first paint depends on an event shows a blank screen on
|
|
80
|
-
every cold start.
|
|
81
|
-
|
|
82
|
-
## Stage 2 — handshake, and learn what this device can do
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
86
|
-
|
|
87
|
-
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
|
|
88
|
-
|
|
89
|
-
const native = ack.nativeVersion; // string | null
|
|
90
|
-
const has = (min: string) => native !== null && cmp(native, min) >= 0;
|
|
91
|
-
|
|
92
|
-
const canReadRawChannels = has('0.16.0');
|
|
93
|
-
const canQueryTimeRange = has('0.15.0');
|
|
94
|
-
const hasLanguageKey = has('0.14.0');
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Or use the SDK's `versionAtLeast(ack.nativeVersion, '0.16.0')`, which does
|
|
98
|
-
exactly that and returns `false` for `null`. Do not compare version strings
|
|
99
|
-
lexically — `'0.9.0' > '0.16.0'` is `true` as strings and false as versions.
|
|
100
|
-
|
|
101
|
-
**Do not branch on `ack.accepted`.** It is always `true`; the native side never
|
|
102
|
-
refuses a handshake. Anything you gate on it is ungated.
|
|
103
|
-
|
|
104
|
-
## Stage 3 — subscribe, then ask
|
|
105
|
-
|
|
106
|
-
Subscribe before sending the command that triggers the reply. The reply can
|
|
107
|
-
arrive before your next line runs.
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
BridgeClient.on(BRIDGE_KEYS.userDevice, (p) => { setDevices(p); render(); });
|
|
111
|
-
BridgeClient.on(BRIDGE_KEYS.systemStatus, (s) => { setStatus(s); render(); });
|
|
112
|
-
BridgeClient.on(BRIDGE_KEYS.deviceConnection, (c) => { setConnected(c); render(); });
|
|
113
|
-
|
|
114
|
-
const result = await BridgeClient.requestUserDevices();
|
|
115
|
-
if (result.status === 'timeout') renderEmptyState(result.hasDevices === false);
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
**Do not replace the helper with `send('userDevice')`.** The device list is
|
|
119
|
-
served by the app's background service, which starts after your bundle; a
|
|
120
|
-
request sent before it is up is lost, and an account with no device never gets
|
|
121
|
-
a reply. The helper resends with backoff for 30 s and then tells you to show the
|
|
122
|
-
empty state. Keep the `userDevice` subscription: roster changes arrive on it
|
|
123
|
-
later without a request. `docs/troubleshooting.md` has the details.
|
|
124
|
-
|
|
125
|
-
Read device data from the typed items: the list is `devicesConfig`, and the
|
|
126
|
-
name, battery and last sync are in `devicesTelemetry` — join them on
|
|
127
|
-
`deviceId`, never on `serialNo` alone.
|
|
128
|
-
|
|
129
|
-
Then follow the connection state of each device:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
for (const d of result.devices.devicesConfig) {
|
|
133
|
-
BridgeClient.send('deviceConnection', { type: 'add', serialNo: d.serialNo, deviceId: d.deviceId });
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
`on` returns an unsubscribe function. If your UI has views that mount and
|
|
138
|
-
unmount, call it — otherwise you accumulate handlers that all re-render.
|
|
139
|
-
|
|
140
|
-
## Stage 4 — handle the environment honestly
|
|
141
|
-
|
|
142
|
-
`systemStatus` tells you why things are not working. Surface it; do not swallow
|
|
143
|
-
it.
|
|
144
|
-
|
|
145
|
-
```ts
|
|
146
|
-
if (status.missingEssentialPermissions.length > 0) {
|
|
147
|
-
// Background collection is OFF. This is blocking — say so.
|
|
148
|
-
renderBlockingCard(status.missingEssentialPermissions, (name) =>
|
|
149
|
-
BridgeClient.requestPermission(name));
|
|
150
|
-
} else if (!status.bluetooth) {
|
|
151
|
-
renderNotice('Bluetooth is off — devices cannot be reached.');
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
There is no permission-result event. After the person answers the OS prompt,
|
|
156
|
-
the native side re-emits `systemStatus`; your card disappears because the array
|
|
157
|
-
emptied. If you wait for a dedicated ack, you wait forever.
|
|
158
|
-
|
|
159
|
-
## Stage 5 — real-time, if you need it
|
|
160
|
-
|
|
161
|
-
Real-time is **off by default**, per device. Turn it on, wait for the ack, and
|
|
162
|
-
only then expect the stream.
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
BridgeClient.on(BRIDGE_KEYS.realTime, (ack) => setRealTime(ack.serialNo, ack.value));
|
|
166
|
-
BridgeClient.on(BRIDGE_KEYS.deviceLightSensors, (p) => {
|
|
167
|
-
if (p.type === 'actlumus2') { /* 14 channels */ } else { /* 10 channels */ }
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
BridgeClient.send('realTime', { serialNo, value: true });
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
`serialNo` is a **number**. A string is dropped in silence.
|
|
174
|
-
|
|
175
|
-
Two devices can share a `serialNo` (an ActLumus and an ActLumus Plus). To act on
|
|
176
|
-
one of them, send its `deviceId` **together with** `serialNo`, taken from the
|
|
177
|
-
`userDevice` list: `BridgeClient.send('realTime', { serialNo, deviceId, value })`.
|
|
178
|
-
A command with only the shared `serialNo` is ignored on purpose. Gate this on
|
|
179
|
-
`nativeVersion` ≥ 0.17.0; details in `docs/bridge-contract.md`.
|
|
180
|
-
|
|
181
|
-
In a raw channel, `null` means the channel does not exist on that hardware
|
|
182
|
-
generation. It does not mean zero — `0.0` is a valid reading. Coercing `null`
|
|
183
|
-
to `0` invents data, and has previously led to a confident, wrong conclusion
|
|
184
|
-
about a device.
|
|
185
|
-
|
|
186
|
-
## Stage 6 — history, if you need it
|
|
187
|
-
|
|
188
|
-
```ts
|
|
189
|
-
const page = await BridgeClient.fetchSensorLog({
|
|
190
|
-
requestId: crypto.randomUUID(),
|
|
191
|
-
serialNo,
|
|
192
|
-
limit: 200,
|
|
193
|
-
});
|
|
194
|
-
if (!page.success) return showError(page.error); // resolves, does not reject
|
|
195
|
-
renderLogs(page.logs ?? []);
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Timestamps are **device-local**. Build ranges with local `Date` getters, never
|
|
199
|
-
`toISOString()` — converting to UTC shifts every series by the timezone offset
|
|
200
|
-
and produces data that looks entirely plausible.
|
|
201
|
-
|
|
202
|
-
Paginate with the cursors exactly as received. They are opaque. Without a range
|
|
203
|
-
the first page is the **most recent** one (go older with `endingBefore`); with
|
|
204
|
-
`startTime` it is the **oldest** of the range (go newer with `startingAfter`).
|
|
205
|
-
A device writes one log per period — programmable, 60 s by default — so a week
|
|
206
|
-
is about 10 000 logs: use `limit: 1000` and do not assume the cadence.
|
|
207
|
-
|
|
208
|
-
## Stage 7 — back button
|
|
209
|
-
|
|
210
|
-
```ts
|
|
211
|
-
BridgeClient.onBack(() => goBackOnePage());
|
|
212
|
-
// call this on every internal navigation, not once at startup
|
|
213
|
-
BridgeClient.reportNavigation(hasInternalHistory);
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
If you never report `true`, the OS back button stays with the app — which is
|
|
217
|
-
the correct behaviour for a single-screen Home.
|
|
218
|
-
|
|
219
|
-
## Stage 8 — build and validate
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
npm run build # your bundler → dist/index.html, single chunk
|
|
223
|
-
npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
|
|
224
|
-
npx condor-mobile-bridge validate -i dist/bundle.zip --strict
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
`validate` applies the gates the app applies plus a lint for APIs the sandbox
|
|
228
|
-
blocks. A non-zero exit is a build failure. Errors mean the app would reject
|
|
229
|
-
the bundle outright; warnings mean it installs and then misbehaves on the
|
|
230
|
-
device, which is worse to debug.
|
|
231
|
-
|
|
232
|
-
Validation is textual, so it cannot see through minification and it cannot know
|
|
233
|
-
whether you checked `nativeVersion` before using a capability. A clean run is
|
|
234
|
-
necessary, not sufficient.
|
|
235
|
-
|
|
236
|
-
## Stage 9 — before you call it done
|
|
237
|
-
|
|
238
|
-
- [ ] `validate --strict` exits 0.
|
|
239
|
-
- [ ] First paint works with no events received.
|
|
240
|
-
- [ ] Every capability past the baseline is guarded by a `nativeVersion` check,
|
|
241
|
-
and the `null` branch renders something sensible.
|
|
242
|
-
- [ ] No spinner can hang forever — the three confirmable commands
|
|
243
|
-
(`syncTime`, `deleteMemory`, `logout`) may never ack, because the person
|
|
244
|
-
can decline.
|
|
245
|
-
- [ ] No `localStorage`, no `fetch`, no `eval`. Not even behind a `try`.
|
|
246
|
-
- [ ] `serialNo` is a number everywhere, and a command for a device whose `serialNo`
|
|
247
|
-
repeats on the account also carries its `deviceId` (`nativeVersion` ≥ 0.17.0).
|
|
248
|
-
- [ ] Errors are reported through `BridgeClient.send('jsError', …)`, the only
|
|
249
|
-
channel out.
|
|
250
|
-
- [ ] Every harness scenario passes: `default`, `cold-start`, `no-devices`,
|
|
251
|
-
`shared-serial`, `offline`.
|
|
252
|
-
- [ ] The `nativeVersion === null` branch renders something sensible.
|
|
253
|
-
|
|
254
|
-
## Delivery
|
|
255
|
-
|
|
256
|
-
Hand Condor the `.zip` along with its size and `sha256-<hex>` hash, both printed
|
|
257
|
-
by the `build` command. The app verifies the hash against the manifest and
|
|
258
|
-
rejects any mismatch. Condor publishes it for your company, which reaches every
|
|
259
|
-
user of that company — test on a development test company first.
|
|
260
|
-
`docs/device-testing.md` explains the path to the phone and how to inspect the
|
|
261
|
-
bundle there; `docs/pilot-onboarding.md` has the full checklist.
|
|
262
|
-
|
|
263
|
-
## When something does not work
|
|
264
|
-
|
|
265
|
-
`docs/troubleshooting.md`, which starts from the symptom. The most common one
|
|
266
|
-
is that a command did nothing and said nothing — there are six distinct causes
|
|
267
|
-
for that, and they are enumerated there.
|
|
6
|
+
- **The procedure** — setup, boot sequence, harness, validation, definition of
|
|
7
|
+
done: [`AGENTS.md`](../AGENTS.md).
|
|
8
|
+
- **What every key, command, ack and error means**:
|
|
9
|
+
[`docs/bridge-contract.md`](./bridge-contract.md).
|
|
10
|
+
- **Harness, scripted tests, `validate`, publishing, the phone**:
|
|
11
|
+
[`docs/device-testing.md`](./device-testing.md).
|