@roku-sdk/ai 0.0.0-stage → 2.19.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/LICENSE +21 -0
- package/README.md +88 -2
- package/package.json +47 -3
- package/skills/ads-and-raf/SKILL.md +231 -0
- package/skills/ads-and-raf/references/integration-flows.md +332 -0
- package/skills/app-packaging/SKILL.md +107 -0
- package/skills/app-performance/SKILL.md +133 -0
- package/skills/cli-dev-loop/SKILL.md +217 -0
- package/skills/cli-dev-loop/references/logs-and-crashes.md +114 -0
- package/skills/cpu-and-heap-profiling/SKILL.md +108 -0
- package/skills/cpu-and-heap-profiling/references/reading-profiles.md +52 -0
- package/skills/data-entry-keyboards/SKILL.md +160 -0
- package/skills/device-management/SKILL.md +158 -0
- package/skills/dialogs-and-modals/SKILL.md +210 -0
- package/skills/effects/SKILL.md +183 -0
- package/skills/fetch-and-data-loading/SKILL.md +164 -0
- package/skills/fetch-and-data-loading/references/https-and-certificates.md +79 -0
- package/skills/flex-grid/SKILL.md +236 -0
- package/skills/flex-grid/references/focus-integration.md +355 -0
- package/skills/flex-grid/references/plugins.md +201 -0
- package/skills/focus-management/SKILL.md +210 -0
- package/skills/focus-management/references/focus-recipes.md +290 -0
- package/skills/fonts-and-glyphs/SKILL.md +231 -0
- package/skills/fonts-and-glyphs/references/glyph-tables.md +128 -0
- package/skills/logging-and-errors/SKILL.md +227 -0
- package/skills/logging-and-errors/references/uncaught-exceptions.md +137 -0
- package/skills/mcp-ai-dev-loop/SKILL.md +189 -0
- package/skills/mcp-ai-dev-loop/references/crash-and-console.md +68 -0
- package/skills/mcp-ai-dev-loop/references/live-debugging.md +129 -0
- package/skills/mcp-setup/SKILL.md +131 -0
- package/skills/project-setup/SKILL.md +210 -0
- package/skills/project-setup/references/roku-config.md +120 -0
- package/skills/screen-navigation/SKILL.md +166 -0
- package/skills/sdk-components/SKILL.md +213 -0
- package/skills/sdk-components/references/dialogs.md +148 -0
- package/skills/sdk-components/references/scenegraph-node-spelling.md +123 -0
- package/skills/sdk-installation/SKILL.md +129 -0
- package/skills/slideshow/SKILL.md +157 -0
- package/skills/slideshow/references/focus-and-handoff.md +187 -0
- package/skills/solid-on-roku/SKILL.md +181 -0
- package/skills/solid-on-roku/references/hermes-and-exports.md +130 -0
- package/skills/testing/SKILL.md +147 -0
- package/skills/testing/references/async-and-timing.md +97 -0
- package/skills/user-accounts-and-purchases/SKILL.md +164 -0
- package/skills-manifest.json +316 -0
- package/skills-manifest.schema.json +62 -0
- package/src/__tests__/manifest.test.ts +267 -0
- package/src/__tests__/snippets.test.ts +221 -0
- package/src/__tests__/trigger-routing.test.ts +77 -0
- package/src/index.ts +36 -0
- package/src/manifest.ts +215 -0
- package/src/router.ts +147 -0
- package/src/snippets.ts +212 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Uncaught exceptions
|
|
2
|
+
|
|
3
|
+
`setUncaughtExceptionHandler` is the only hook for a throw that nothing in the app caught. It is a
|
|
4
|
+
narrow API with four behaviours that are not guessable from its signature, each of which produces
|
|
5
|
+
a handler that looks correct and is not.
|
|
6
|
+
|
|
7
|
+
## What it does and does not see
|
|
8
|
+
|
|
9
|
+
It is registered against the runtime's uncaught-exception capture callback. That means:
|
|
10
|
+
|
|
11
|
+
- **Nothing thrown before the call reaches it.** Until `setUncaughtExceptionHandler` runs, only
|
|
12
|
+
the SDK's default handles a throw: it logs the error and `source_map_sha`, then the app exits.
|
|
13
|
+
The whole bundle is evaluated before `main()` is called. Each module's top-level code runs in
|
|
14
|
+
import order, and a module's imports run before its own body. So a handler registered inside
|
|
15
|
+
`main()` is too late for a module whose top level throws, such as a constant initialized from
|
|
16
|
+
a configuration read that fails. To cover that, register in a module of its own, and make it
|
|
17
|
+
the entry file's first import: `import "./install-exception-reporting";`. Keep it a bare
|
|
18
|
+
side-effect import. Import sorting keeps side-effect imports first and in the order written,
|
|
19
|
+
but sorts named imports alphabetically, which can move the handler below the module that
|
|
20
|
+
throws.
|
|
21
|
+
- **An `ErrorBoundary` catch never reaches it.** A boundary handles the throw, so as far as the
|
|
22
|
+
runtime is concerned nothing escaped. Since the SDK wraps the whole app in a boundary already,
|
|
23
|
+
a render-time throw is caught before this handler is a possibility. If you want a monitoring
|
|
24
|
+
service to hear about render failures, report them from the boundary's `fallback`.
|
|
25
|
+
- **Unhandled promise rejections never reach it.** They go to a separate runtime callback, which
|
|
26
|
+
the SDK sets to log the rejection at `error` and stop there. `setUncaughtExceptionHandler` does
|
|
27
|
+
not forward rejections. To act on one, register your own callback with
|
|
28
|
+
`globalThis.Process.setUnhandledRejectionCallback`, which replaces the SDK's logging.
|
|
29
|
+
|
|
30
|
+
So the handler's actual scope is small. It covers a throw that happens after registration and
|
|
31
|
+
outside any boundary and the reactive render path: module-level work in modules evaluated after
|
|
32
|
+
it, the `main()` callback, a timer, a callback the SDK invoked directly, a listener.
|
|
33
|
+
|
|
34
|
+
## The handler's contract
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
setUncaughtExceptionHandler(
|
|
38
|
+
async error => {
|
|
39
|
+
// error: Error. The only argument; the returned Promise is awaited.
|
|
40
|
+
},
|
|
41
|
+
{ timeout: 5000, resume: false }
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Element | Behaviour |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `error` | The only argument. Never null: if the runtime hands over a falsy error, the SDK synthesizes one with the message `Uncaught exception (no error object provided)`, so reading `.message` or `.stack` is always safe. With `enableExceptionTagging()` active, `error.stack` ends in `[SourceMap: <sha>]`, the build's source-map identity. Send the stack as is, or it cannot be resolved later |
|
|
48
|
+
| Return value | A Promise. The SDK awaits it for up to `timeout`, then tells the runtime to exit or resume |
|
|
49
|
+
| `timeout` | Milliseconds the returned Promise is awaited for. Default **5000**, floor **100** — a smaller value is raised to the floor rather than rejected |
|
|
50
|
+
| `resume` | Chooses what happens after. Default `false` |
|
|
51
|
+
|
|
52
|
+
There is no `sha` argument and no `done` argument. A handler written as `(error, sha, done)` does
|
|
53
|
+
not typecheck. The SDK logs the error message and `source_map_sha` itself, before your handler
|
|
54
|
+
runs, and registering a handler does not suppress that line. The SDK also calls the runtime's
|
|
55
|
+
`done` callback for you.
|
|
56
|
+
|
|
57
|
+
## The `done` flag is inverted relative to `resume`
|
|
58
|
+
|
|
59
|
+
This is the single easiest thing to get backwards, because the two names read as synonyms and
|
|
60
|
+
mean opposites:
|
|
61
|
+
|
|
62
|
+
| You pass | The SDK calls | The app |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| nothing, or `resume: false` | `done(true)` | exits |
|
|
65
|
+
| `resume: true` | `done(false)` | resumes its event loop |
|
|
66
|
+
|
|
67
|
+
`done(true)` means "yes, we are finished — exit", not "yes, the handler succeeded". Passing
|
|
68
|
+
`resume: true` to keep an app alive through a background failure is legitimate, but understand
|
|
69
|
+
that the app then continues from a state something already failed in.
|
|
70
|
+
|
|
71
|
+
Both outcomes are unconditional: whether your handler resolves, rejects, or runs out its timeout,
|
|
72
|
+
`done` is invoked with the flag chosen by `resume`. There is no path in which a failing handler
|
|
73
|
+
changes the exit decision.
|
|
74
|
+
|
|
75
|
+
## Registration is permanent
|
|
76
|
+
|
|
77
|
+
Calling `setUncaughtExceptionHandler` replaces the currently registered handler, including the
|
|
78
|
+
SDK's default. Only the last registration is active, and **there is no way to restore the SDK
|
|
79
|
+
default afterwards** — no unregister, no argument that means "back to normal".
|
|
80
|
+
|
|
81
|
+
The consequence for structure: register once, before anything can throw, and branch inside your own handler
|
|
82
|
+
rather than swapping handlers per screen. A test that registers one has no clean way to undo it,
|
|
83
|
+
which is why the module keeps an internal reset that is explicitly test-only and not exported for
|
|
84
|
+
application use.
|
|
85
|
+
|
|
86
|
+
## It can silently do nothing
|
|
87
|
+
|
|
88
|
+
If `globalThis.Process` is unavailable when you call it, the function logs at `warn` and returns
|
|
89
|
+
without registering anything. Two things make that hard to notice:
|
|
90
|
+
|
|
91
|
+
- Nothing throws and nothing returns a failure — the call looks like it worked.
|
|
92
|
+
- The default runtime log filter is `ERROR`, so the `warn` line explaining what happened is
|
|
93
|
+
itself filtered out.
|
|
94
|
+
|
|
95
|
+
If a handler appears to be registered and never fires, distinguish this case from the
|
|
96
|
+
`ErrorBoundary` case by lowering `currentLogLevel` to `WARN` and looking for that line before
|
|
97
|
+
concluding the handler is wired up wrong.
|
|
98
|
+
|
|
99
|
+
## Shape to copy
|
|
100
|
+
|
|
101
|
+
A module of its own, `src/install-exception-reporting.ts`, that registers as it is evaluated:
|
|
102
|
+
|
|
103
|
+
```ts rsg-skill=complete
|
|
104
|
+
import { enableExceptionTagging, setUncaughtExceptionHandler } from "@roku-sdk/rsg-ts";
|
|
105
|
+
|
|
106
|
+
enableExceptionTagging();
|
|
107
|
+
|
|
108
|
+
setUncaughtExceptionHandler(
|
|
109
|
+
async error => {
|
|
110
|
+
await fetch("https://monitoring.example.com/exceptions", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({ name: error.name, message: error.message, stack: error.stack })
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
{ timeout: 2000 }
|
|
117
|
+
);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The entry file imports it before anything else:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import "./install-exception-reporting";
|
|
124
|
+
|
|
125
|
+
import { main, registerAnchorNode } from "@roku-sdk/rsg-ts";
|
|
126
|
+
|
|
127
|
+
import MyApp from "./my-app";
|
|
128
|
+
|
|
129
|
+
main(() => registerAnchorNode(MyApp));
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`enableExceptionTagging()` comes first, so a trace captured by the handler is already tagged.
|
|
133
|
+
The handler has nothing to call when it is done: the SDK finishes once the Promise settles or
|
|
134
|
+
2000 ms elapse, whichever comes first. Omitting `resume` means the app exits afterwards, which
|
|
135
|
+
is what you want unless you have a specific reason to keep running. It is also the only sensible
|
|
136
|
+
choice for a throw during boot, because there is nothing to resume into: the throwing module
|
|
137
|
+
never finished, and `main()` never registered the app.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# MCP AI Dev Loop
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The SDK ships an MCP server, and it is how an agent reaches a Roku: put the current code on
|
|
6
|
+
the device, see what it did, drive it, read what it said, and turn a crash back into
|
|
7
|
+
TypeScript. Everything below is one of those five steps. The server exposes far more tools
|
|
8
|
+
than this skill names — the surface grows — so enumerate what the server offers and match it
|
|
9
|
+
to the step you are on, rather than trusting any list, including this one.
|
|
10
|
+
|
|
11
|
+
## Read the reference first for these tasks
|
|
12
|
+
|
|
13
|
+
| If the request is | Read now, before answering |
|
|
14
|
+
|---|---|
|
|
15
|
+
| A crash, an uncaught exception, a stack trace, or console output from the device | `references/crash-and-console.md` |
|
|
16
|
+
| Setting a breakpoint, pausing the app, or reading values in a paused frame | `references/live-debugging.md` |
|
|
17
|
+
|
|
18
|
+
Decoding is the step with the most ways to look finished and be wrong — a stack decoded
|
|
19
|
+
against the wrong build reads perfectly and points at the wrong line.
|
|
20
|
+
|
|
21
|
+
A debug session also breakpoints and steps, which is worth knowing before you reach for a
|
|
22
|
+
`console.log` and a redeploy. One rule governs it and costs a rebuild to learn the hard way:
|
|
23
|
+
`cdp_eval` evaluates in the running app, but the first debugger tool of any kind — arming a
|
|
24
|
+
breakpoint included — disables it for the rest of that connection, and only a new session
|
|
25
|
+
brings it back. Read whatever you can with `cdp_eval` first. A session that has spent it is
|
|
26
|
+
not stuck, though: `cdp_debugger_eval_frame` reads values in a stopped frame's own scope for
|
|
27
|
+
that session's whole life, and the reference is how you reach a frame worth reading.
|
|
28
|
+
|
|
29
|
+
## When to Use
|
|
30
|
+
|
|
31
|
+
- deploying an app under development to a device and confirming what landed
|
|
32
|
+
- capturing the screen, or reading the rendered node tree, of a running app
|
|
33
|
+
- sending remote keys to walk the app somewhere without touching hardware
|
|
34
|
+
- collecting device logs, or finding a line in them
|
|
35
|
+
- a device the tools cannot see, or a deploy that lands stale code
|
|
36
|
+
|
|
37
|
+
### When not to use
|
|
38
|
+
|
|
39
|
+
| Situation | Use instead | Because |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| No Roku tools in the session at all, or a client that will not connect | The `mcp-setup` skill | That is transport and editor config; everything here assumes a session that already lists tools |
|
|
42
|
+
| A shell script, a CI step, or anything with no agent in it | The `cli-dev-loop` skill | These tools exist inside an agent session; a build runner has to call `rk` itself, and the CLI's directories and flags differ from what the tools expose |
|
|
43
|
+
| Registering a Roku with the CLI, or which box a *shell* command reaches | The `device-management` skill | A tool call names its own target, so a tool that answered the wrong box is fixed here; the CLI's stored list is fixed there |
|
|
44
|
+
| Installing a signed package through the device's developer web page | Roku's own documentation | That is a browser workflow, and nothing here drives it |
|
|
45
|
+
| Debugging a web, mobile or desktop app | That platform's tooling | The tools here speak to a Roku and nothing else |
|
|
46
|
+
| Writing the app's code or its project configuration | The `sdk-components` and `project-setup` skills | This skill starts once there is something to run |
|
|
47
|
+
| Testing a component without a device | The `testing` skill | That renders into an in-process tree; nothing here is involved |
|
|
48
|
+
| An app that janks, drops frames, or grows its memory, and the question is what it costs | The `app-performance` skill | These tools report what the app did, not how long it took to do it |
|
|
49
|
+
|
|
50
|
+
## Quick Reference
|
|
51
|
+
|
|
52
|
+
| Step | Call | Notes |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Find the device | `list_devices` | Always first. Nothing else can target sensibly until you know what is registered and which is active |
|
|
55
|
+
| Register one | `add_device`, then `activate_device` | Needed once per device; `update_default_auth` sets the password used for sideloading |
|
|
56
|
+
| Build and deploy | `deploy_app` | Builds **and** sideloads in one call. This is the normal way to get code onto a device |
|
|
57
|
+
| Build only | `build_app` | Produces an artifact without touching the device |
|
|
58
|
+
| Sideload an existing artifact | `sideload_app` | Does not build. Reads the directory you point it at |
|
|
59
|
+
| See the screen | `capture_screen` | Returns an image, and writes a file |
|
|
60
|
+
| Read the node tree | `ecp` with `command: "query/app-ui"` | The rendered tree of the running app, as JSON |
|
|
61
|
+
| Read device facts | `ecp` with `command: "query/device-info"` | Model, firmware, resolution |
|
|
62
|
+
| Press a key | `ecp` with `command: "keypress"` | One key |
|
|
63
|
+
| Press several | `ecp` with `command: "keysequence"` | One call for a whole path through the UI. Prefer it over a run of single presses |
|
|
64
|
+
| Name a key for either of those | The **wire** name, not the runtime one | `Select` (not `OK`), `Rev` (not `rewind`), `Fwd` (not `fastforward`), `Info` (not `options`). Every element is validated before anything reaches the device, so one wrong name fails the whole call with `ECP_INVALID_KEY` |
|
|
65
|
+
| Deep-link | `ecp` with `command: "input"` | Launch parameters, without navigating there by hand |
|
|
66
|
+
| Collect logs | `logs` — `start`, then `get` or `search` | A session must exist before there is anything to read |
|
|
67
|
+
| Decode a crash | `decode_stack_trace` | `references/crash-and-console.md` |
|
|
68
|
+
| Confirm the server is up | `get_server_health` | The fastest way to tell whether `rk server` is running, which decides what the rest of the surface can do |
|
|
69
|
+
| Start or stop a cloud device | `device_start`, `device_stop` | Roku Cloud Emulator only, billed while it runs. The server stops what it started; see instruction 11 |
|
|
70
|
+
|
|
71
|
+
## Instructions
|
|
72
|
+
|
|
73
|
+
1. **`list_devices` first, every session.** It returns what is registered and which one is
|
|
74
|
+
active, and every other tool's targeting is defined against that. Without a device the
|
|
75
|
+
failure arrives several steps later, worded as though the app were at fault.
|
|
76
|
+
|
|
77
|
+
2. **Target with `device` or with `host`, never both.** `device` selects a registered device
|
|
78
|
+
by id, IP or name; `host` addresses one directly and skips the store. Passing both is an
|
|
79
|
+
error. Passing neither uses the active device, and if none is active the tool says so.
|
|
80
|
+
The targeting parameters are not uniform, though — `capture_screen` takes `device` and not
|
|
81
|
+
`host`, so a screenshot needs the device to be in the store even when a deploy to the same
|
|
82
|
+
box by `host` has just worked. Read the schema of the tool you are calling rather than
|
|
83
|
+
assuming this one. A Roku Cloud Emulator device is named `rce:<name>` in `device`; it has
|
|
84
|
+
no address, so `host` can never reach it.
|
|
85
|
+
|
|
86
|
+
3. **Deploy with `deploy_app` and stop there.** It builds and sideloads in one call, into
|
|
87
|
+
`dist-dev`. Since `sideload_app` reads `dist-dev` too, following a deploy with a sideload
|
|
88
|
+
only re-sends what was just sent; reach for `sideload_app` when you have an artifact and no
|
|
89
|
+
reason to rebuild, not as a second half of deploying.
|
|
90
|
+
|
|
91
|
+
4. **The two directories are the one thing to keep straight.** `deploy_app` writes and reads
|
|
92
|
+
`dist-dev`. `build_app` writes `dist-build`. `sideload_app` reads `dist-dev` unless you
|
|
93
|
+
pass `outDir`. So `build_app` followed by a bare `sideload_app` is the pairing that sends
|
|
94
|
+
stale code, or fails outright when `dist-dev` was never built: pass
|
|
95
|
+
`outDir: "dist-build"` to match them, or use `deploy_app` and think about neither.
|
|
96
|
+
|
|
97
|
+
That pairing now reports itself rather than passing silently. A bare `sideload_app` whose
|
|
98
|
+
`dist-dev` is older than `dist-build` returns a warning naming both paths, their build
|
|
99
|
+
times, and the `outDir` to pass — it still sends the artifact you asked for, so read the
|
|
100
|
+
warning rather than assuming the newer one went. When the directory holds nothing at all,
|
|
101
|
+
the error names a command that could have created it and reports where a `pkg-dir` was
|
|
102
|
+
actually found. A project that sets `outDir` in `roku-config.ts` avoids the split entirely:
|
|
103
|
+
the sideload path reads project config, so an `outDir` from config or from the parameter is
|
|
104
|
+
treated as deliberate and draws no warning. Sideloading Hermes bytecode (`index.hbc`) also
|
|
105
|
+
returns a warning, since a device without the `rsg_typescript` token refuses to launch it.
|
|
106
|
+
|
|
107
|
+
5. **Never reach the device's network endpoints yourself.** No `curl`, no hand-built request
|
|
108
|
+
to a device port. The tools carry the device's password, retry sensibly and report failure
|
|
109
|
+
in a way you can act on; a raw request does none of that and its silence looks like success.
|
|
110
|
+
|
|
111
|
+
6. **Look before you conclude.** `capture_screen` says what a person would see;
|
|
112
|
+
`ecp` with `query/app-ui` says what the app actually built. They answer different questions
|
|
113
|
+
and disagree in the interesting cases — a node present in the tree but invisible on screen
|
|
114
|
+
is a layout or a colour bug, not a rendering one.
|
|
115
|
+
|
|
116
|
+
7. **Screenshot pixels are not layout coordinates.** The app lays out in design space, usually
|
|
117
|
+
1920×1080; `capture_screen` returns the device's own resolution, which is 1280×720 on an HD
|
|
118
|
+
box. Multiply by 1.5 going from a measured screenshot pixel to a coordinate in the code, and
|
|
119
|
+
report positions back to a human in design space. Confirm both ends before you rely on the
|
|
120
|
+
ratio: the design space is the app's `ui_resolutions`, and the device's output is the
|
|
121
|
+
`ui-resolution` field of `query/device-info`. On a device already in FHD they match 1:1.
|
|
122
|
+
A frame from the installer rather than ECP (`source: "installer"`, always on a Roku Cloud
|
|
123
|
+
Emulator device and on a secure retail device without an ECP token) is the exception to
|
|
124
|
+
reading the ratio off the device: its size is not fixed, so take the ratio from the
|
|
125
|
+
returned image's own dimensions. That path also captures only while the sideloaded app is
|
|
126
|
+
in the foreground, unless the device's auth token grants the `screenshot` permission.
|
|
127
|
+
|
|
128
|
+
8. **Prefer one `keysequence` to a run of `keypress` calls.** Each call is a round trip, and a
|
|
129
|
+
sequence keeps the timing consistent. After the last key, capture or query rather than
|
|
130
|
+
assuming the app kept up.
|
|
131
|
+
|
|
132
|
+
9. **Some tools need the server, and say so.** `logs`, `get_server_health` and the profiling
|
|
133
|
+
and debug-session families only work when `rk server` is running; the rest fall back to
|
|
134
|
+
running the CLI in process. `get_server_health` is the quickest way to tell which side of
|
|
135
|
+
that line a session is on, and a tool that needs a server names one in its failure. Getting
|
|
136
|
+
a server running, and pointing a client at it, is the `mcp-setup` skill's subject.
|
|
137
|
+
|
|
138
|
+
10. **Clean up what you instrumented.** Log lines added to chase a bug, probe nodes injected
|
|
139
|
+
into the tree, stopwatch wrappers around a function: tag them all with one prefix when you
|
|
140
|
+
add them, and remove them by that prefix before you finish.
|
|
141
|
+
|
|
142
|
+
11. **A Roku Cloud Emulator device costs money while it runs, so start one only when asked.**
|
|
143
|
+
No tool starts a stopped device implicitly; a call that
|
|
144
|
+
needs it fails with `RCE_DEVICE_NOT_RUNNING`, worded for a shell (`rk device start`). In a
|
|
145
|
+
session the equivalent is `device_start`, which takes a minute or two and makes the server
|
|
146
|
+
the device's owner. The server stops it on `device_stop`, after a stretch with no tool call or
|
|
147
|
+
session touching it, and whenever the server exits — but call `device_stop` yourself when
|
|
148
|
+
the work is done rather than leaning on the idle timer. `device_stop` refuses a device the
|
|
149
|
+
server did not start (`RCE_NOT_SERVER_OWNED`); that one belongs to whoever started it. It also
|
|
150
|
+
refuses one another MCP session started (`RCE_NOT_SESSION_OWNED`): pass `force: true` only when
|
|
151
|
+
the user means to stop it. If a stop fails, the server keeps the device and retries about
|
|
152
|
+
every 30 seconds and on exit, but call `device_stop` again rather than assume it went. Both
|
|
153
|
+
tools need a running `rk server`, and the device must already be registered with
|
|
154
|
+
`rk device add --rce`, which is the `device-management` skill's subject.
|
|
155
|
+
|
|
156
|
+
## Example
|
|
157
|
+
|
|
158
|
+
A session that gets code onto a device and confirms it, in the order the tools are called:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
list_devices -> which device, and is one active
|
|
162
|
+
deploy_app { root } -> builds dist-dev, sideloads, launches
|
|
163
|
+
capture_screen {} -> the screen as it is now
|
|
164
|
+
ecp { command: "keysequence", keys: ["right", "right", "Select"] }
|
|
165
|
+
ecp { command: "query/app-ui" } -> the tree after the presses
|
|
166
|
+
logs { action: "start" }
|
|
167
|
+
logs { action: "search", filter: "MyScreen" }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Each step answers one question. If a step returns something you did not expect, stop and find
|
|
171
|
+
out why before sending the next key — a crashed app accepts keypresses silently, and every
|
|
172
|
+
observation after that point describes a device that is no longer running your app.
|
|
173
|
+
|
|
174
|
+
## Common Mistakes
|
|
175
|
+
|
|
176
|
+
| Symptom | Cause | Fix |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| The device is running older code than the source | `build_app` wrote `dist-build` and a bare `sideload_app` read `dist-dev` | Use `deploy_app`, or pass `outDir: "dist-build"` to `sideload_app`. The sideload now warns when the directory it read is older than the other one |
|
|
179
|
+
| A tool reports no device | Nothing registered, or nothing active | `list_devices`, then `activate_device`; `add_device` if the list is empty |
|
|
180
|
+
| Sideloading is refused | Developer mode is off, or the device password is wrong | Enable developer mode on the box, then set the password with `update_default_auth` |
|
|
181
|
+
| `logs` fails before it reads anything | It requires server mode, and there was no session | Start `rk server`, then `logs` with `action: "start"` before `get` or `search` |
|
|
182
|
+
| Keys appear to do nothing | The app crashed, or focus is not where you assumed | `capture_screen`, then `ecp` with `query/app-ui`; check the logs for an exception |
|
|
183
|
+
| A screenshot looks right but the test of it fails | Measured in device pixels, compared against design coordinates | Convert before comparing |
|
|
184
|
+
| A tool fails with `RCE_DEVICE_NOT_RUNNING` and suggests `rk device start` | Nothing starts a billed device implicitly; the message is worded for a shell | `device_start` if the user wants it running, then `device_stop` when done |
|
|
185
|
+
| `device_stop` fails with `RCE_NOT_SESSION_OWNED` | Another MCP session (another editor window, another agent) started the device | Leave it, or pass `force: true` if the user asked for it stopped |
|
|
186
|
+
| `device_start` fails with `RCE_DEVICE_STOPPING` | A stop of that device is still under way | Wait for it, then call `device_start` again |
|
|
187
|
+
| `capture_screen` fails with `SCREENSHOT_NOT_OK` on a cloud or secure retail device | The sideloaded app is not in the foreground | Deploy or relaunch the app, then capture |
|
|
188
|
+
| `capture_screen` on a LAN device fails with `SCREEN_CAPTURE_ERROR`, naming an auth token | A secure device, every retail Roku among them, serves no ECP capture without a Roku-issued token, and no developer password was at hand for the installer fallback | `update_default_auth`, or `rk device update <device> --password <password>` from a shell, then capture again |
|
|
189
|
+
| Sideloading to a cloud device is refused for its password | Its password is kept with its registration; `update_default_auth` never applies to it | `rk device update rce:<name>` from a shell, as the error names |
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Crashes and console output
|
|
2
|
+
|
|
3
|
+
A thrown error reaches you by one of two routes, and which one you have decides how much work
|
|
4
|
+
is left.
|
|
5
|
+
|
|
6
|
+
## Route 1: a live debug session, where the stacks arrive decoded
|
|
7
|
+
|
|
8
|
+
`cdp_session_create` builds the app in debug mode, deploys it, loads the source map and
|
|
9
|
+
connects. With a session up:
|
|
10
|
+
|
|
11
|
+
- `cdp_errors_read` returns the exceptions the app has thrown, **with their stacks already
|
|
12
|
+
mapped back to TypeScript**. Nothing else to do.
|
|
13
|
+
- `cdp_console_read` returns console output, so a `console.log` added to narrow something down
|
|
14
|
+
is readable without starting a log session at all.
|
|
15
|
+
- `cdp_console_clear_errors` and `cdp_console_clear_console` empty those buffers. Clear before
|
|
16
|
+
reproducing, and what you read afterwards is only what this attempt produced.
|
|
17
|
+
|
|
18
|
+
One session per device. Redeploying through `cdp_session_update` clears both buffers as part
|
|
19
|
+
of the update, so read what you need *before* you redeploy, not after.
|
|
20
|
+
|
|
21
|
+
This route is strictly better than searching a log buffer when you can have it: the stack
|
|
22
|
+
comes back resolved, and console output is separated from every other line the device emits.
|
|
23
|
+
|
|
24
|
+
## Route 2: the log stream, where the stack arrives raw
|
|
25
|
+
|
|
26
|
+
Without a session, an uncaught exception is written to the device log along with a
|
|
27
|
+
`source_map_sha=` line naming the build it came from. That is what `logs` gives you, and the
|
|
28
|
+
stack in it contains bundle positions rather than TypeScript positions: `index.js` for plain
|
|
29
|
+
`rk dev`, or `index.hbc` for a build using `--compile-ts`.
|
|
30
|
+
|
|
31
|
+
Hand the whole thing to `decode_stack_trace`. Do not try to read it, and do not open a source
|
|
32
|
+
map yourself: JS frames need the ESBuild map; bytecode frames need a composed map or the
|
|
33
|
+
Hermes and ESBuild maps.
|
|
34
|
+
|
|
35
|
+
## Getting the decode right
|
|
36
|
+
|
|
37
|
+
`stackTrace` is the only required parameter. Paste the raw text — surrounding log noise is
|
|
38
|
+
fine, since only `index.hbc` and `index.js` frames are read — and the tool finds the build's
|
|
39
|
+
SHA in the `source_map_sha=` or `[SourceMap: ...]` marker the runtime emitted.
|
|
40
|
+
|
|
41
|
+
The other four parameters are the difference between a decode you can trust and one that
|
|
42
|
+
merely succeeds:
|
|
43
|
+
|
|
44
|
+
| Parameter | When you need it |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `buildDir` | When the app is not at the root the server is running from. Resolution is anchored at that root, so in a monorepo an app in a subdirectory reports `No HBC source map found` for every frame until you pass this — point it at the build's `bundle` directory |
|
|
47
|
+
| `sha` | When the trace has no marker, or you are deliberately decoding against an older build |
|
|
48
|
+
| `context` | Lines of source shown around each frame; defaults to 2 |
|
|
49
|
+
| `dclDirs` | When frames name a Dynamic Component Library rather than `index` — map each library name to its own build directory |
|
|
50
|
+
|
|
51
|
+
**The failure to watch for**: a `sha` that names a directory which does not exist does not
|
|
52
|
+
error. It falls back to auto-detection and decodes against whatever build is newest locally,
|
|
53
|
+
which produces a confident answer pointing at the wrong line. If a decoded frame does not
|
|
54
|
+
match the code you think ran, suspect this before suspecting the frame.
|
|
55
|
+
|
|
56
|
+
Source maps never leave your machine — only the JavaScript bundle (or bytecode with `--compile-ts`) is on the device — so a decode
|
|
57
|
+
is only as good as the build directory still sitting in your working tree. Decode before you
|
|
58
|
+
rebuild.
|
|
59
|
+
|
|
60
|
+
## Tagging exceptions
|
|
61
|
+
|
|
62
|
+
`enableExceptionTagging()` appends a `[SourceMap: <sha>]` marker to every `Error.stack`, which
|
|
63
|
+
is what makes an error caught and logged by the app's own code decodable later. It is opt-in
|
|
64
|
+
and idempotent.
|
|
65
|
+
|
|
66
|
+
Uncaught exceptions do not need it: the runtime logs the SHA line for those on its own. Turn it
|
|
67
|
+
on when the app catches errors and reports them itself — that is the case where the stack
|
|
68
|
+
reaches your log without the runtime's marker attached.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Live debugging: breakpoints and paused frames
|
|
2
|
+
|
|
3
|
+
A debug session can do more than report what already went wrong. It can stop the app on a line
|
|
4
|
+
you choose and let you read the values that reached it — which is the difference between
|
|
5
|
+
inferring why a handler misbehaved and watching it happen.
|
|
6
|
+
|
|
7
|
+
Creating a session, and reading errors or console output from one, is
|
|
8
|
+
`references/crash-and-console.md`. This file starts once a session is up.
|
|
9
|
+
|
|
10
|
+
## `cdp_eval` keeps working alongside the debugger
|
|
11
|
+
|
|
12
|
+
`cdp_eval` evaluates an expression inside the running app without stopping it, and it keeps
|
|
13
|
+
working for the whole life of the connection — including after a `cdp_debugger_*` tool has
|
|
14
|
+
enabled the Debugger domain, arming a breakpoint, or being genuinely paused.
|
|
15
|
+
|
|
16
|
+
While paused, `cdp_debugger_eval_frame` is still the better tool for inspecting the paused
|
|
17
|
+
code itself: it evaluates in the frame's own scope, so a handler's arguments and locals are in
|
|
18
|
+
scope by name, which `cdp_eval` (global scope only) can't see. It requires a `callFrameId`,
|
|
19
|
+
which `cdp_debugger_state` returns for each frame of the current pause. Reach for it because of
|
|
20
|
+
that extra visibility, not because `cdp_eval` has stopped working.
|
|
21
|
+
|
|
22
|
+
A breakpoint, not a blind pause, is still the reliable way into a stopped app: `cdp_debugger_pause`
|
|
23
|
+
against an idle app (nothing running, no timers/animation/input) simply times out after about
|
|
24
|
+
five seconds — there's no JavaScript execution to interrupt. A breakpoint on a line you know
|
|
25
|
+
executes, followed by driving the app to it, always lands where you intended.
|
|
26
|
+
|
|
27
|
+
## The flow
|
|
28
|
+
|
|
29
|
+
Six steps, and the order matters more than any single call:
|
|
30
|
+
|
|
31
|
+
1. **`cdp_source_grep`** — find the line by searching the *deployed* bundle, matched by
|
|
32
|
+
TypeScript path. This is also how you get a path in the form the next step demands.
|
|
33
|
+
2. **`cdp_breakpoint_set`** — arm it. The response reports the compiled line it mapped to.
|
|
34
|
+
3. **Drive the app to that line** — `ecp` with `command: "keypress"`, or whatever reaches the
|
|
35
|
+
code. Once it stops the app itself is frozen: its UI stops responding, and anything that waits
|
|
36
|
+
on the app to do something will wait forever. The debugger tools are what still answer.
|
|
37
|
+
4. **`cdp_debugger_state`** — read the pause. It returns a `callFrameId` per frame, each already
|
|
38
|
+
mapped back to a TypeScript file and line, with the SDK's own frames below yours.
|
|
39
|
+
5. **`cdp_debugger_eval_frame`** — read values in that frame's scope, by name. A handler's
|
|
40
|
+
arguments and locals are simply in scope; nothing has to be logged to be visible.
|
|
41
|
+
6. **`cdp_debugger_resume`** — let it go.
|
|
42
|
+
|
|
43
|
+
`cdp_debugger_pause` is deliberately absent from that list. It interrupts at the next statement
|
|
44
|
+
the app happens to execute, which means it either times out on an idle app or lands somewhere
|
|
45
|
+
you did not choose on a busy one. It answers exactly one question — whether the app is executing
|
|
46
|
+
JavaScript at all — and is not a way to reach a line.
|
|
47
|
+
|
|
48
|
+
## When a breakpoint does not stop where you expect
|
|
49
|
+
|
|
50
|
+
Keyed on the symptom, because that is the order you meet them in:
|
|
51
|
+
|
|
52
|
+
| What you see | What it means | What to do |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `Could not map src/file.tsx:120 to a compiled location: the file exists on disk but is not part of the bundle`, HTTP 400 | The path resolved, but the file is not in the bundle that is on the device: tree-shaken out, or not reachable from the entry | Confirm with `cdp_source_list` or `cdp_source_grep`. Nothing is wrong with the path or the line |
|
|
55
|
+
| `Could not map …` ending `(project root: …)`, or `: no compiled code at or after this line` | The first: the path matches no source in the map. A path is accepted as absolute, as relative to the session's project root, or as the raw source-map path. The second: the file is in the bundle but nothing is compiled at or after that line, as at the end of a file | First: take the path from `cdp_source_list` or `cdp_source_resolve`. Second: use an earlier line, and confirm the extent with `cdp_source_get` |
|
|
56
|
+
| `Breakpoint creation failed` | The location exists but the engine refused it — an import or declaration that produced no reachable instruction | Move to a statement that executes at run time |
|
|
57
|
+
| The call **succeeds**, reports `active: true`, and nothing ever stops | Two different causes, below. This is the expensive one, because nothing surfaces it | Do not trust `active: true`. It means the line was *mapped*, not that it can be reached |
|
|
58
|
+
|
|
59
|
+
**Read that first message carefully, because its wording is the trap.** `Could not map … to a
|
|
60
|
+
compiled location` sounds like *this line is not executable*, and the natural reaction is to go
|
|
61
|
+
looking for a different line. It is almost always the path. A relative path fails this way even
|
|
62
|
+
when the line is perfect:
|
|
63
|
+
|
|
64
|
+
- Fails: `src/components/player.tsx`
|
|
65
|
+
- Works: `/Users/you/project/src/components/player.tsx`
|
|
66
|
+
|
|
67
|
+
For the accepted-but-silent case, there are two causes and they need different fixes:
|
|
68
|
+
|
|
69
|
+
| Cause | How to confirm |
|
|
70
|
+
|---|---|
|
|
71
|
+
| The line number matches your working tree, not the build on the device | `cdp_source_get` reads the deployed bundle. If what it shows at that line is not what your editor shows, the breakpoint is on whatever used to be there. Rebuild with `cdp_session_update` rather than by hand: it redeploys into the same session and re-maps every breakpoint against the new build, **reporting the ones it can no longer place** — which turns this silent case into a named failure |
|
|
72
|
+
| The line carries no executable statement — a comment, a blank line, a type-only declaration | The mapping snaps to a nearby compiled offset and reports success for a location that can never be reached. Move to a statement that runs |
|
|
73
|
+
|
|
74
|
+
One more message worth recognising rather than fearing: `the app's compiled script has not been
|
|
75
|
+
observed yet`. It is close to unreachable in practice, because enabling the domain makes the
|
|
76
|
+
engine replay its already-loaded scripts before acknowledging. If it does appear, the session
|
|
77
|
+
attached earlier than the app's own bundle loaded.
|
|
78
|
+
|
|
79
|
+
## Two habits
|
|
80
|
+
|
|
81
|
+
**Resume before concluding.** A component that renders more than once per interaction stops once
|
|
82
|
+
per render, and the first stop need not be the interesting one — a single key press stops twice on
|
|
83
|
+
a handler that runs on both the press and the release edge, and the values differ between them.
|
|
84
|
+
Read the first frame, resume, and see whether another stop follows before deciding what happened.
|
|
85
|
+
|
|
86
|
+
**Step only once you are in the right frame.** `cdp_debugger_step_over`, `_step_into` and
|
|
87
|
+
`_step_out` are cheap once a pause is where you want it and nearly useless as a way of getting
|
|
88
|
+
there. Prefer a second breakpoint over stepping toward a distant line.
|
|
89
|
+
|
|
90
|
+
## Before a long session: the request limiter
|
|
91
|
+
|
|
92
|
+
The server rate-limits itself, and a debugging sweep is the one workflow that reaches the ceiling:
|
|
93
|
+
**100 requests per rolling 15 minutes**, counted across every route but the health check, so tool
|
|
94
|
+
calls and CDP traffic share the budget. Exceeding it returns HTTP 429 with `RATE_LIMIT_EXCEEDED`.
|
|
95
|
+
|
|
96
|
+
That is worth knowing in advance because of how it presents. Calls start failing partway through a
|
|
97
|
+
session that was working, which reads like the app having crashed or the device having dropped off
|
|
98
|
+
— and the instinct is to redeploy, which spends more requests.
|
|
99
|
+
|
|
100
|
+
Raise it before you start, not after: `--no-rate-limit` when launching the server, or
|
|
101
|
+
`RK_DISABLE_RATE_LIMIT=1` in its environment. Both are read once at startup, so a server that is
|
|
102
|
+
already running has to be restarted for either to matter.
|
|
103
|
+
|
|
104
|
+
## Reading the deployed source
|
|
105
|
+
|
|
106
|
+
`cdp_source_list`, `cdp_source_get`, `cdp_source_grep` and `cdp_source_resolve` all read the
|
|
107
|
+
bundle **that is on the device**, addressed by TypeScript path. That is the point of them: they are
|
|
108
|
+
not a convenience over opening the file locally, they are the only way to see what the device
|
|
109
|
+
actually has. Every stale-line problem above is diagnosed with them, and `cdp_source_list` is where
|
|
110
|
+
an exact path comes from when a project-relative one does not match.
|
|
111
|
+
|
|
112
|
+
Two things to expect from `cdp_source_list`. It returns the SDK's and dependencies' sources
|
|
113
|
+
alongside the app's own, so the app's files are a small minority of a long list — filter rather
|
|
114
|
+
than read it. And the session summary's file count is not the length of that list; treat it as
|
|
115
|
+
"a source map loaded" and nothing more.
|
|
116
|
+
|
|
117
|
+
## Not to be confused: the `dap_*` family
|
|
118
|
+
|
|
119
|
+
`dap_*` speaks Roku's own socket-based BrightScript debug protocol on TCP 8081. It is **not** the
|
|
120
|
+
Microsoft Debug Adapter Protocol, and the resemblance of the name is the whole problem — every one
|
|
121
|
+
of those tool descriptions says so in capitals. Nothing on this page uses that family: TypeScript
|
|
122
|
+
running under the JavaScript engine is debugged through the `cdp_*` tools.
|
|
123
|
+
|
|
124
|
+
## What belongs elsewhere
|
|
125
|
+
|
|
126
|
+
| If the question is | Go to |
|
|
127
|
+
|---|---|
|
|
128
|
+
| An exception that has already been thrown, or console output | `references/crash-and-console.md` — a session reports those with stacks already mapped, and no breakpoint is involved |
|
|
129
|
+
| What something costs — memory, frame time, why the app janks | The `app-performance` skill. It owns the measurement tools, and this page is about correctness, not cost |
|