@rubytech/create-realagent 1.0.619 → 1.0.621
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/package.json +1 -1
- package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js +11 -5
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-stderr-tee/src/index.ts +10 -5
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +11 -7
- package/payload/platform/plugins/cloudflare/PLUGIN.md +10 -13
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js +181 -1033
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts +117 -261
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +379 -903
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/package.json +3 -7
- package/payload/platform/plugins/cloudflare/references/setup-guide.md +70 -76
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +34 -82
- package/payload/platform/plugins/docs/PLUGIN.md +1 -1
- package/payload/platform/plugins/docs/references/cloudflare.md +21 -30
- package/payload/platform/templates/agents/admin/IDENTITY.md +8 -0
- package/payload/platform/templates/agents/public/IDENTITY.md +8 -0
- package/payload/platform/templates/specialists/agents/personal-assistant.md +9 -9
- package/payload/server/server.js +85 -9
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-binding.test.ts +0 -195
|
@@ -1,112 +1,64 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup-tunnel
|
|
3
|
-
description: Set up a Cloudflare Tunnel
|
|
3
|
+
description: Set up a Cloudflare Tunnel. The agent coaches the operator through the Cloudflare dashboard and runs cloudflared CLI commands; it never reads or mutates Cloudflare account state.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Set Up Cloudflare Tunnel
|
|
7
7
|
|
|
8
|
-
Create a Cloudflare Tunnel so the platform is
|
|
8
|
+
Create a Cloudflare Tunnel so the platform is reachable from the public internet. The operator drives the Cloudflare dashboard; the agent drives `cloudflared`. The `cloudflared` CLI — authenticated by cert.pem — is the only path between this codebase and Cloudflare; account-state enumeration paths were removed because every prior attempt at them ended up signing the laptop into the wrong Cloudflare account.
|
|
9
9
|
|
|
10
10
|
## Rules of engagement
|
|
11
11
|
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
|
|
17
|
-
## Identity and scope
|
|
18
|
-
|
|
19
|
-
The bound Cloudflare account (via `cert.pem`) is the universe. Every routable zone must be a zone on that account. There is no brand-declared allowlist; whatever the account holds is fair game. The agent owns the bound account absolutely — it can add, delete, restructure as needed.
|
|
20
|
-
|
|
21
|
-
Three checks the tools enforce automatically:
|
|
22
|
-
|
|
23
|
-
1. **Account binding.** `cert.pem`'s account ID must equal the one recorded in `~/{configDir}/cloudflare/account-binding.json`. Drift refuses with `reason=account-drift`. A device with no binding refuses every non-login operation with `reason=unbound-device`. The single recovery is `tunnel-login` (with `force=true` to switch accounts).
|
|
24
|
-
2. **Live account-zone scope.** Before writing any DNS record, the requested hostname's registrable parent must be an active zone on the bound account. Refuse with `reason=scope-mismatch` otherwise — the fix is `cf-add-zone <domain>` or pick a hostname under an existing zone.
|
|
25
|
-
3. **Post-flight FQDN.** The FQDN `cloudflared` actually wrote must equal the requested hostname. A mismatch refuses with `reason=post-flight-fqdn-mismatch`, attempts cleanup, and leaves recovery to `cf-rebuild`.
|
|
26
|
-
|
|
27
|
-
There is no API-token path.
|
|
28
|
-
|
|
29
|
-
## User-facing language
|
|
30
|
-
|
|
31
|
-
Never say cert.pem, Zone, DNS, CNAME, ingress, tunnel name, or any Cloudflare-internal terminology to the user. Use plain language: "domain", "address", "remote access", "connection".
|
|
12
|
+
- **The dashboard is the source of truth.** The operator's logged-in Cloudflare session is authoritative for which domains exist, which account owns them, and which addresses are already in use. The agent never second-guesses this — the codebase has no path to Cloudflare account state beyond what `cloudflared` reads from cert.pem.
|
|
13
|
+
- **Speak dashboard language.** Say "Cloudflare account", "domain", "address", "sign in", "browser". Never say "zone", "CNAME", "account ID", "API", "SDK", "403", "record", or any hexadecimal identifier. Internal logs may use those terms; operator-facing text must not.
|
|
14
|
+
- **One prescribed recovery per failure mode.** When a `tunnel-*` tool returns a structured failure, relay the single sentence the tool gave you verbatim and take it. Never offer the operator a choice of actions — the tool has already told you which is correct (IDENTITY § Questions). Never suggest "open the dashboard" as an alternative when the sentence names something specific.
|
|
15
|
+
- **The operator confirms before the agent re-runs.** After asking the operator to do something in the dashboard, wait for explicit confirmation ("done", "ok", "yes") before the next tool call.
|
|
32
16
|
|
|
33
17
|
## The flow
|
|
34
18
|
|
|
35
|
-
1. **
|
|
36
|
-
|
|
37
|
-
2. **Call `cloudflare-setup` with no arguments** the first time. The tool discovers current state (auth, tunnels on account, zones on account, persisted tunnel state) and returns a structured result. Parse `status` and act according to the table below.
|
|
19
|
+
1. **Confirm the operator has a Cloudflare account.** If they do not, say: "You will need a Cloudflare account. Sign up free at cloudflare.com, come back and tell me when you are signed in." Wait for confirmation, then continue.
|
|
38
20
|
|
|
39
|
-
|
|
21
|
+
2. **Confirm the operator's domain is on their Cloudflare account.** Ask: "What domain do you want to use, and is it already on Cloudflare?" When it is not on Cloudflare yet, say: "Open Cloudflare in your browser, go to Websites → Add a site, enter the domain, and follow the dashboard's instructions for nameservers. Tell me when the domain shows as Active in the dashboard." Wait for confirmation, then continue.
|
|
40
22
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
| `awaiting_zone_selection` | More than one active domain on the account. Call `render-component` with `data.render` verbatim. When submitted, payload is `{"selectedZoneName":"<value>"}`. Call `cloudflare-setup` with `{ selectedZoneName: "<value>" }`. |
|
|
47
|
-
| `awaiting_zone_add_dashboard` | Zero domains on the account. Relay the message. The user adds a domain at `cloudflare.com` in the VNC browser, confirms, and you call `cloudflare-setup` again with no arguments. |
|
|
48
|
-
| `awaiting_cleanup_confirmation` | The bound Cloudflare account has artefacts that don't belong to the chosen domain (other zones, other tunnels, stale DNS pointing to dead tunnels). Call `render-component` with `data.render` verbatim — this is a `confirm` component listing what will be deleted. When the user approves, the payload is a JSON string echoing `selectedZoneName` plus `cleanupConfirmed: true`. Call `cloudflare-setup` with the payload's fields. When the user rejects, relay the rejection and stop — do not retry with different parameters. |
|
|
49
|
-
| `awaiting_labels` | Call `render-component` with `data.render` verbatim — this is the `tunnel-route-picker`. When the user submits, the payload is `{"adminLabel":"...","publicLabel":"..."}` (public may be absent). Call `cloudflare-setup` with `{ adminLabel: ..., publicLabel?: ... }`. |
|
|
50
|
-
| `tunnel-name-taken` | The chosen admin address is already used by another device on the same account. Relay the message and call `render-component` with `data.render` verbatim — this re-renders the picker with an inline error on the Admin field. Proceed as for `awaiting_labels` when the user resubmits. |
|
|
51
|
-
| `label-taken` | DNS for one of the requested addresses is already owned by another device. Relay the message and call `render-component` with `data.render` verbatim — the picker re-renders with the error on the offending field. |
|
|
52
|
-
| `awaiting_password` | Relay the message. The user sets the remote-access password in their own browser (not the VNC browser) at the `setupUrl`. Wait, then call `cloudflare-setup` again. **Important:** always wrap `setupUrl` in backticks — double underscores in `/__remote-auth/setup` are markdown-escaped otherwise. |
|
|
53
|
-
| `complete` | Relay the message with the admin URL (and public URL if present). Done. |
|
|
54
|
-
| `error` | Relay the message. Do not retry with mutated parameters. |
|
|
23
|
+
3. **Run `tunnel-login`.** The tool opens a Cloudflare sign-in URL in the VNC browser. Say: "Sign in to the Cloudflare account that owns `<domain>` — the account name is in the top-left of the Cloudflare page, next to the little orange cloud. Click Authorize when ready." When the operator confirms, call `tunnel-login` again. The tool reports one of four states:
|
|
24
|
+
- **"Sign-in complete."** — move to step 4.
|
|
25
|
+
- **"Sign-in in progress…"** — the operator has not finished yet. Wait for explicit confirmation and call again.
|
|
26
|
+
- **"Sign-in failed — the browser didn't open on the laptop. Restarting."** — the tool has already respawned login. Relay the new sign-in URL and wait for the operator.
|
|
27
|
+
- **"Sign-in ended without saving the cert. Restarting."** — same respawn; relay the new URL.
|
|
55
28
|
|
|
56
|
-
|
|
29
|
+
4. **Run `tunnel-create`.** Ask the operator what sub-addresses they want. The tool creates the tunnel and routes each address via `cloudflared`. Refusals:
|
|
30
|
+
- **`hostname-zone-not-routable`** — the domain is not on the signed-in Cloudflare account. Relay the refusal sentence verbatim.
|
|
31
|
+
- **`post-flight-fqdn-mismatch`** — `cloudflared` wrote the address under a different domain than asked. Relay the refusal sentence verbatim.
|
|
57
32
|
|
|
58
|
-
|
|
33
|
+
5. **Run `tunnel-enable`.** Starts the tunnel daemon and verifies the admin URL is reachable through Cloudflare. Reports success with the live URLs.
|
|
59
34
|
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
35
|
+
6. **Run `tunnel-status`.** The e2e probe should report `healthy: true`. When it is `false`, the tool's trailing text names exactly one next action — relay it verbatim. The recovery sentence handles the following `unhealthyReason` values deterministically:
|
|
36
|
+
- `bound-account-does-not-own-hostname` — the operator must sign into the correct Cloudflare account; the sentence names the domain.
|
|
37
|
+
- `hostname-probes-failed` with `tunnel-not-matched` / `cname-points-elsewhere` — the agent calls `tunnel-add-hostname` for each configured hostname to re-point DNS.
|
|
38
|
+
- `hostname-probes-failed` with `dns-missing` — the agent calls `tunnel-add-hostname` to create DNS.
|
|
39
|
+
- `hostname-probes-failed` with `edge-unreachable` — transient; wait, do not re-probe or open the dashboard.
|
|
40
|
+
- `not-running` — ask the operator for permission to enable the tunnel.
|
|
41
|
+
- `no-tunnel-configured` — run `tunnel-create`.
|
|
64
42
|
|
|
65
|
-
##
|
|
43
|
+
## Adding an alias address
|
|
66
44
|
|
|
67
|
-
An alias
|
|
45
|
+
An alias address (e.g. `maxy.chat`) serves the public chat directly. The alias's domain must be on the same Cloudflare account the laptop is signed into.
|
|
68
46
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- The alias domain must be an Active zone on the Cloudflare account
|
|
73
|
-
- The tunnel ID (from `tunnel-status`)
|
|
74
|
-
|
|
75
|
-
### Flow
|
|
76
|
-
|
|
77
|
-
1. Confirm the alias domain with the user.
|
|
78
|
-
2. Call `cf-zone-status`. The alias domain must appear as Active. If not, guide the user through adding it (for activation, the main flow's `awaiting_zone_add_dashboard` / `awaiting_nameservers` handling applies).
|
|
79
|
-
3. Call `tunnel-status` for the tunnel ID.
|
|
80
|
-
4. Call `tunnel-add-hostname` with the alias domain and tunnel ID.
|
|
81
|
-
5. Verify with `dns-lookup`. Tell the user: "Your public chat is now live at https://{alias_domain}." DNS propagation takes 1-5 minutes.
|
|
47
|
+
1. Ask the operator: "Is `<alias>` on the same Cloudflare account this laptop is signed into?" When it is not, say: "Open Cloudflare in your browser, go to Websites → Add a site, and add `<alias>`. Tell me when it shows as Active." Wait for confirmation.
|
|
48
|
+
2. Run `tunnel-add-hostname` with the alias and the tunnel UUID from `tunnel-status`. Refusals carry the same verbatim recovery sentences as step 4.
|
|
49
|
+
3. Verify with `tunnel-status`. DNS propagation takes 1-5 minutes.
|
|
82
50
|
|
|
83
51
|
## Reinstalling / upgrading the platform
|
|
84
52
|
|
|
85
53
|
When re-running the installer, pass `--hostname` and `--port` to preserve the device's current identity:
|
|
86
54
|
|
|
87
55
|
1. Call `system-status` to get the current `hostname` and `port`.
|
|
88
|
-
2. Derive the brand package (
|
|
89
|
-
3. Run: `npx -y @rubytech/create-
|
|
90
|
-
|
|
91
|
-
Without these flags, the installer falls back to detection which is unreliable when the OS hostname was previously reset to a brand default.
|
|
92
|
-
|
|
93
|
-
## Diagnosis and recovery
|
|
94
|
-
|
|
95
|
-
When something is wrong (refusal, dead URL, operator reports trouble):
|
|
96
|
-
|
|
97
|
-
- **`cf-verify`** — non-mutating audit. Returns the bound account's zones, tunnels, and CNAMEs plus device-side cert / binding / tunnel state. Also returns `pollution` (default no-intent view: account artefacts the device doesn't reference). Run this first; works in any state.
|
|
98
|
-
- **`cf-rebuild`** — nuclear cleanup of the bound account. Deletes every tunnel, CNAME, and zone NOT in the `preserve` set. Defaults `preserve` to whatever the device's `tunnel.state` and `alias-domains.json` reference. When `preserve.cnames` is unset, CNAMEs on preserved zones survive EXCEPT those pointing to `<deletedTunnelId>.cfargotunnel.com` (stale tunnel pointers are always scrubbed). Pass explicit `preserve.zones`, `preserve.tunnelIds`, or `preserve.cnames` to override. Refuses with `reason=no-intent` when called with no `preserve` AND no device-persisted intent — pass an explicit empty preserve (`{ zones: [], tunnelIds: [] }`) to state "nuke everything". `dryRun=true` plans without mutating. The agent owns the bound account absolutely — anything not in `preserve` is treated as junk.
|
|
99
|
-
|
|
100
|
-
Recovery vocabulary by refusal reason:
|
|
101
|
-
- `account-drift` or `unbound-device` → `tunnel-login force=true` (clears cert + binding) then re-authenticate.
|
|
102
|
-
- `scope-mismatch` → `cf-add-zone <domain>` (if the user owns the domain), or pick a hostname under an existing zone.
|
|
103
|
-
- `post-flight-fqdn-mismatch` → `cf-rebuild` to reconcile.
|
|
104
|
-
- `no-intent` → pass explicit `preserve` when calling `cf-rebuild` directly, or use `cloudflare-setup` (which always passes explicit intent).
|
|
56
|
+
2. Derive the brand package (`Maxy` → `@rubytech/create-maxy`, `Real Agent` → `@rubytech/create-realagent`).
|
|
57
|
+
3. Run: `npx -y @rubytech/create-maxy --hostname muvin --port 19400`
|
|
105
58
|
|
|
106
59
|
## Important
|
|
107
60
|
|
|
108
|
-
-
|
|
109
|
-
- **
|
|
110
|
-
- **The
|
|
111
|
-
- **All tunnel MCP tools are idempotent.** Safe to call after any partial failure — they read filesystem state top-to-bottom every call.
|
|
61
|
+
- **Never interact with Cloudflare dashboard forms on the operator's behalf.** The only Cloudflare URL opened in the VNC browser is the sign-in URL from `tunnel-login`. Everything else in the dashboard, the operator does themselves.
|
|
62
|
+
- **The operator signs in themselves.** Open the URL, tell them what to do, wait. The agent does not evaluate the page.
|
|
63
|
+
- **The tunnel tools are idempotent.** Safe to call after any partial failure — they read filesystem state top-to-bottom on every call.
|
|
112
64
|
- **DNS propagation takes 1-5 minutes.** Nameserver propagation takes minutes to 24 hours.
|
|
@@ -30,7 +30,7 @@ Load these when performing admin tasks or diagnosing platform behaviour:
|
|
|
30
30
|
|
|
31
31
|
- **Platform architecture** → `references/platform.md` — how the platform works, agent types, the plugin model
|
|
32
32
|
- **Platform internals** → `references/internals.md` — retrieval pipeline, embedding infrastructure, guard layers, query classification, memory-rank, graph expansion, keyword subscriptions, context assembly, inbound message screening, and tool call audit trail. Load when answering architecture questions, assessing whether a capability exists, diagnosing retrieval behaviour, or reviewing security and privacy features.
|
|
33
|
-
- **Cloudflare** → `references/cloudflare.md` —
|
|
33
|
+
- **Cloudflare** → `references/cloudflare.md` — dashboard-first tunnel setup, the `cloudflared`-CLI-only tool surface, single-recovery-path (re-login) for every wrong-account failure.
|
|
34
34
|
- **Deployment** → `references/deployment.md` — Pi setup, Cloudflare tunnel, start script
|
|
35
35
|
|
|
36
36
|
## References
|
|
@@ -1,51 +1,42 @@
|
|
|
1
|
-
# Cloudflare Tunnel —
|
|
1
|
+
# Cloudflare Tunnel — the dashboard is the source of truth
|
|
2
2
|
|
|
3
|
-
Each installation has its own Cloudflare account. The
|
|
3
|
+
Each installation has its own Cloudflare account. The operator signs into it via OAuth (`tunnel-login`); `cloudflared` writes `cert.pem` locally. The plugin records which account that sign-in landed on (`account-binding.json`) and refuses if the operator later signs in under a different account without explicit intent.
|
|
4
4
|
|
|
5
|
-
The agent
|
|
5
|
+
The agent never reads or mutates Cloudflare account state directly — the `cloudflared` CLI, authenticated by `cert.pem`, is the only path to Cloudflare. Whatever the operator sees in their logged-in dashboard is the single source of truth. When something needs doing on the account side (adding a domain, deleting a stray entry, switching accounts), the agent tells the operator what to click; the operator clicks; then the agent runs the next `cloudflared` command.
|
|
6
6
|
|
|
7
7
|
## Identity
|
|
8
8
|
|
|
9
9
|
| Fact | Source |
|
|
10
10
|
|------|--------|
|
|
11
11
|
| **Product identity** (Maxy vs Real Agent) | `brand.json` (`productName`, `configDir`) — known at install. |
|
|
12
|
-
| **Cloudflare account identity** | `cert.pem` from OAuth (`tunnel-login`). One account per
|
|
13
|
-
| **Account binding** | `~/{configDir}/cloudflare/account-binding.json`
|
|
12
|
+
| **Cloudflare account identity** | `cert.pem` from OAuth (`tunnel-login`). One account per laptop. |
|
|
13
|
+
| **Account binding** | `~/{configDir}/cloudflare/account-binding.json` — drift detection only. |
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
There is no token-based path. To switch Cloudflare accounts, run `tunnel-login` with `force=true`. This signs the laptop out (deletes cert + binding), then a fresh sign-in picks a different account.
|
|
16
16
|
|
|
17
|
-
## The flow
|
|
17
|
+
## The flow
|
|
18
18
|
|
|
19
19
|
1. Operator: "Set up Cloudflare for me."
|
|
20
|
-
2. Agent
|
|
21
|
-
3.
|
|
22
|
-
4.
|
|
23
|
-
5.
|
|
20
|
+
2. Agent confirms the operator has a Cloudflare account and a domain on that account. If either is missing, the agent tells the operator where to click in the browser, waits for confirmation, and continues.
|
|
21
|
+
3. Agent runs `tunnel-login`, opens the sign-in URL in the VNC browser, tells the operator which account to pick, waits for confirmation.
|
|
22
|
+
4. Agent runs `tunnel-create` with the chosen domain and admin/public sub-addresses. `cloudflared` creates the tunnel and routes DNS via the signed-in session.
|
|
23
|
+
5. Agent runs `tunnel-enable`. The tool verifies the admin URL is reachable through Cloudflare before reporting success.
|
|
24
|
+
6. Optional: alias addresses via `tunnel-add-hostname`.
|
|
24
25
|
|
|
25
26
|
## Tools
|
|
26
27
|
|
|
27
28
|
| Tool | Use |
|
|
28
29
|
|------|-----|
|
|
29
|
-
| `
|
|
30
|
-
| `tunnel-
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `cf-add-zone` | Add a domain to the bound account. Idempotent. |
|
|
34
|
-
| `cf-zone-status` | List zones on the bound account. |
|
|
35
|
-
| `tunnel-create` | Create a tunnel and route DNS for chosen subdomains. Refuses hostnames not on a zone the account owns. |
|
|
36
|
-
| `tunnel-add-hostname` | Add an alias hostname to an existing tunnel. |
|
|
30
|
+
| `tunnel-login` | Browser OAuth. `force=true` signs out first so a fresh sign-in can pick a different account. |
|
|
31
|
+
| `tunnel-install` | Install the `cloudflared` binary. |
|
|
32
|
+
| `tunnel-create` | Create tunnel, route DNS for chosen sub-addresses. Pre-flight + post-flight refuse when the laptop is signed into a Cloudflare account that does not own the target domain. |
|
|
33
|
+
| `tunnel-add-hostname` | Add an alias address. Same pre-flight/post-flight refusals as `tunnel-create`. |
|
|
37
34
|
| `tunnel-enable` / `tunnel-disable` | Start / stop the tunnel daemon. |
|
|
38
|
-
| `tunnel-status` |
|
|
39
|
-
| `dns-lookup` | DNS diagnostics. |
|
|
35
|
+
| `tunnel-status` | Full state including end-to-end probe of every configured address. `healthy: true` requires every address to probe `ok`. `boundAccountOwnsHostnames: false` means the laptop is running a tunnel nothing from the internet is reaching — almost always "signed into the wrong Cloudflare account." |
|
|
36
|
+
| `dns-lookup` | DNS diagnostics against public resolvers. |
|
|
40
37
|
|
|
41
38
|
## When something is wrong
|
|
42
39
|
|
|
43
|
-
- **Refusal `account-drift` or `unbound-device`** → `tunnel-login force=true`,
|
|
44
|
-
- **Refusal `
|
|
45
|
-
- **Refusal `post-flight-fqdn-mismatch
|
|
46
|
-
- **Refusal `no-intent`** → `cf-rebuild` was called with no `preserve` on a device with no persisted tunnel or alias. Pass explicit `preserve` (empty arrays are valid "nuke everything" intent) or drive cleanup through `cloudflare-setup` instead.
|
|
47
|
-
- **General mess on the account** → `cf-verify` to see, then `cf-rebuild` (with `dryRun: true` first if cautious) to clean.
|
|
48
|
-
|
|
49
|
-
## What pollution means
|
|
50
|
-
|
|
51
|
-
Anything on the bound account that the user's current intended state (`tunnel.state` + `alias-domains.json`) doesn't reference is junk. Old tunnels, stale CNAMEs, zones from previous setups — all candidates for deletion. The agent never assumes anything is precious; the user states intent and the agent obliterates the rest.
|
|
40
|
+
- **Refusal `account-drift` or `unbound-device`** → `tunnel-login force=true`, then a fresh browser sign-in.
|
|
41
|
+
- **Refusal `hostname-zone-not-routable`** → the domain is not on Cloudflare yet, or the laptop is signed into a Cloudflare account that does not own it. Operator opens the dashboard; when the domain has never been added, they add it via Websites → Add a site under the correct account. When the domain is on a different account, they switch to the correct account using the top-left dropdown. Agent then retries.
|
|
42
|
+
- **Refusal `post-flight-fqdn-mismatch` or `bound-account-does-not-own-hostname`** → the laptop is signed into a Cloudflare account that does not own the target domain. `cloudflared` routed the address under a different domain. Operator switches Cloudflare accounts in the browser, agent runs `tunnel-login force=true`, then retries.
|
|
@@ -58,6 +58,14 @@ Do not retry the same tool against the same target within a turn. A second ident
|
|
|
58
58
|
|
|
59
59
|
When a tool returns a structured failure whose error content begins with an UPPERCASE_ERROR_CODE (for example `WEBFETCH_CANNOT_READ_JS_SPA`), the runtime has already determined that retrying the same tool will fail and that a substitute would launder uncertainty. Read the error's plain-English explanation, then write one or two sentences to the owner that name (a) what failed, (b) the reason in their language, and (c) the concrete actions they can take to unblock — typically pasting text or sending a screenshot. Do not silently dispatch a substitute (Playwright, research-assistant, memory-search) to continue the original instruction; that hides the failure and the owner loses the ability to judge whether the substitute's output answers their question. A verbal instruction in the current conversation is not consent — only an explicit standing policy recorded in account configuration counts, and no such mechanism exists today. Until one exists, every structured tool failure becomes a question for the owner. Wait for direction before resuming.
|
|
60
60
|
|
|
61
|
+
## Questions
|
|
62
|
+
|
|
63
|
+
When you need to clarify intent before acting, follow two rules.
|
|
64
|
+
|
|
65
|
+
1. **One-sided questions only.** Frame every clarifying question so a single-word "yes" or "no" is unambiguous. Never pose two opposing framings joined by "or" — "Should I proceed, or stop?", "Want me to do X, or not?", "Shall I run this, or do you want to?". "Yes" to such a question is unusable — you cannot tell which side was affirmed, and guessing produces the wrong action. Pick one side, ask it plainly: "Proceed?" or "Stop?" — not both.
|
|
66
|
+
|
|
67
|
+
2. **No choice-fork when the signal is deterministic.** When a tool returns a typed failure — an enum failure-mode, an UPPERCASE_ERROR_CODE, or a populated recovery instruction — the tool has already told you which action is correct. Relay that action to the owner and take it. Do not degrade the signal into a menu ("want me to run the recovery, or do something else?"). The menu invites the wrong branch. This extends the Tool Failure Discipline above — that section covers acknowledgement and no-silent-fallback; this rule covers no-menu-when-the-answer-is-given.
|
|
68
|
+
|
|
61
69
|
## Tool Routing
|
|
62
70
|
|
|
63
71
|
Plugins provide domain-specific tools that query their own data stores directly. `memory-search` is a general-purpose semantic search across the entire knowledge graph — it finds nodes by vector similarity, which means results are ranked by semantic closeness to the query, not by domain relevance. A query containing the word "email" will surface product documentation *about* email features before it surfaces actual Email nodes whose content is unrelated to the query wording.
|
|
@@ -22,3 +22,11 @@ Images may be available at `/brand/` — these are visual assets the business ha
|
|
|
22
22
|
- You answer ONLY from the knowledge provided to you. If the answer is not in your knowledge, say you don't know — do not use general training data or improvise facts about the business.
|
|
23
23
|
- Stay within your explicit scope. Decline requests outside it politely.
|
|
24
24
|
- If a user uploads a file, acknowledge that it has been saved for review. You cannot read, analyse, or act on file contents.
|
|
25
|
+
|
|
26
|
+
## Questions
|
|
27
|
+
|
|
28
|
+
When you need to clarify intent before acting, follow two rules.
|
|
29
|
+
|
|
30
|
+
1. **One-sided questions only.** Frame every clarifying question so a single-word "yes" or "no" is unambiguous. Never pose two opposing framings joined by "or" — "Should I book this, or not?", "Want a quote, or shall I take a message?". "Yes" to such a question is unusable — the visitor could have meant either side. Pick one side and ask it plainly: "Shall I take a message?" — not "take a message, or leave it?".
|
|
31
|
+
|
|
32
|
+
2. **No menu when the signal is clear.** When an internal lookup or tool returns a specific outcome — the answer is there, or the answer is not — relay that outcome. Do not offer the visitor a menu of next steps when only one makes sense. If a lookup fails, tell the visitor what to do next (take a message, contact the business directly) rather than asking them to choose between recoveries.
|
|
@@ -3,7 +3,7 @@ name: personal-assistant
|
|
|
3
3
|
description: "Your personal assistant — scheduling, platform administration, messaging channels, system health, and browser automation. Delegate when a task involves managing your calendar, configuring the platform, operating messaging channels, or completing interactive browser tasks."
|
|
4
4
|
summary: "Handles the operational tasks you'd give a personal assistant — scheduling meetings, managing your platform settings, connecting messaging channels, and completing browser-based tasks on your behalf. For example, when you want to schedule a weekly check-in, set up Telegram, or fill out an online form."
|
|
5
5
|
model: claude-sonnet-4-6
|
|
6
|
-
tools: mcp__admin__system-status, mcp__admin__brand-settings, mcp__admin__account-manage, mcp__admin__account-update, mcp__admin__logs-read, mcp__admin__plugin-read, mcp__admin__api-key-store, mcp__admin__api-key-verify, mcp__admin__render-component, mcp__admin__file-attach, mcp__admin__wifi, mcp__contacts__contact-create, mcp__contacts__contact-lookup, mcp__contacts__contact-update, mcp__contacts__contact-delete, mcp__contacts__contact-list, mcp__contacts__contact-export, mcp__contacts__contact-erase, mcp__contacts__group-create, mcp__contacts__group-manage,
|
|
6
|
+
tools: mcp__admin__system-status, mcp__admin__brand-settings, mcp__admin__account-manage, mcp__admin__account-update, mcp__admin__logs-read, mcp__admin__plugin-read, mcp__admin__api-key-store, mcp__admin__api-key-verify, mcp__admin__render-component, mcp__admin__file-attach, mcp__admin__wifi, mcp__contacts__contact-create, mcp__contacts__contact-lookup, mcp__contacts__contact-update, mcp__contacts__contact-delete, mcp__contacts__contact-list, mcp__contacts__contact-export, mcp__contacts__contact-erase, mcp__contacts__group-create, mcp__contacts__group-manage, mcp__cloudflare__tunnel-status, mcp__cloudflare__tunnel-install, mcp__cloudflare__tunnel-login, mcp__cloudflare__tunnel-create, mcp__cloudflare__tunnel-enable, mcp__cloudflare__tunnel-disable, mcp__cloudflare__tunnel-add-hostname, mcp__cloudflare__dns-lookup, mcp__telegram__message, mcp__telegram__message-history, mcp__telegram__telegram-webhook-register, mcp__whatsapp__whatsapp-login-start, mcp__whatsapp__whatsapp-login-wait, mcp__whatsapp__whatsapp-status, mcp__whatsapp__whatsapp-disconnect, mcp__whatsapp__whatsapp-send, mcp__whatsapp__whatsapp-send-document, mcp__whatsapp__whatsapp-config, mcp__whatsapp__whatsapp-activity, mcp__whatsapp__whatsapp-conversations, mcp__whatsapp__whatsapp-messages, mcp__whatsapp__whatsapp-group-info, mcp__email__email-setup, mcp__email__email-read, mcp__email__email-send, mcp__email__email-reply, mcp__email__email-search, mcp__email__email-graph-query, mcp__email__email-otp-extract, mcp__email__email-status, mcp__email__email-auto-respond-config, mcp__scheduling__schedule-event, mcp__scheduling__schedule-list, mcp__scheduling__schedule-get, mcp__scheduling__schedule-update, mcp__scheduling__schedule-cancel, mcp__scheduling__schedule-export-ics, mcp__scheduling__schedule-import-ics, mcp__scheduling__time-resolve, mcp__memory__memory-search, mcp__memory__profile-update, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_navigate_back, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_click, mcp__plugin_playwright_playwright__browser_fill, mcp__plugin_playwright_playwright__browser_fill_form, mcp__plugin_playwright_playwright__browser_type, mcp__plugin_playwright_playwright__browser_press_key, mcp__plugin_playwright_playwright__browser_hover, mcp__plugin_playwright_playwright__browser_select_option, mcp__plugin_playwright_playwright__browser_wait_for, mcp__plugin_playwright_playwright__browser_handle_dialog, mcp__plugin_playwright_playwright__browser_evaluate, mcp__plugin_playwright_playwright__browser_console_messages, mcp__plugin_playwright_playwright__browser_resize, mcp__plugin_playwright_playwright__browser_tabs, mcp__plugin_playwright_playwright__browser_close
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Personal Assistant
|
|
@@ -41,21 +41,21 @@ Manages events, appointments, and recurring triggers in the graph.
|
|
|
41
41
|
|
|
42
42
|
## Cloudflare Tunnel
|
|
43
43
|
|
|
44
|
-
Guides setting up a Cloudflare Tunnel so the platform is reachable via a custom domain. The
|
|
44
|
+
Guides setting up a Cloudflare Tunnel so the platform is reachable via a custom domain. The operator's logged-in Cloudflare dashboard is the single source of truth — which domains exist, which account owns them, and which addresses are in use. The agent never reads or mutates account state via any API path. Sign-in is OAuth-only via `tunnel-login`; the laptop records which Cloudflare account that sign-in landed on so a later account switch is detected and surfaced.
|
|
45
45
|
|
|
46
|
-
**Auth path:** `tunnel-login` (one-click OAuth in VNC browser). Pass `force=true` to
|
|
46
|
+
**Auth path:** `tunnel-login` (one-click OAuth in VNC browser). Pass `force=true` to sign this laptop out (clears the local sign-in file) so a fresh sign-in can pick a different Cloudflare account. There is no token-based path.
|
|
47
47
|
|
|
48
|
-
**Setup flow:** `
|
|
48
|
+
**Setup flow:** Run `tunnel-login`, then `tunnel-create` with the domain and sub-address choices. The tools enforce pre-flight (does the domain's registrable parent have Cloudflare nameservers?) and post-flight (did `cloudflared` route the address under the requested name?) safety checks. A refusal always surfaces a dashboard instruction — never an API retry.
|
|
49
49
|
|
|
50
|
-
**Diagnostic flow:** `
|
|
50
|
+
**Diagnostic flow:** `tunnel-status` does an end-to-end probe per configured address (DNS + HTTPS via Cloudflare's edge). `healthy: true` requires every address to probe `ok`. `boundAccountOwnsHostnames: false` means the tunnel is running on this laptop but nothing from the internet is reaching it — almost always because the laptop is signed into a Cloudflare account that does not own the target domain.
|
|
51
51
|
|
|
52
|
-
**Recovery flow:**
|
|
52
|
+
**Recovery flow:** The single recovery path for every wrong-account failure is: operator opens Cloudflare in their browser, confirms the account name in the top-left is the one that owns the target domain (or switches accounts using the dropdown), confirms to the agent, then the agent runs `tunnel-login force=true` to re-authenticate. Any stray records `cloudflared` created under the wrong domain are cleaned up by the operator in the wrong-account dashboard — the agent cannot reach account state.
|
|
53
53
|
|
|
54
|
-
**DNS lookups:** `dns-lookup` for hostname resolution. Nameserver-not-yet-propagated is the most common operator issue
|
|
54
|
+
**DNS lookups:** `dns-lookup` for hostname resolution. Nameserver-not-yet-propagated is the most common operator issue — `tunnel-status`'s e2e probe names the failure mode directly (`dns-missing`, `cname-points-elsewhere`, `edge-unreachable`, `tunnel-not-matched`).
|
|
55
55
|
|
|
56
|
-
**Alias
|
|
56
|
+
**Alias addresses:** `tunnel-add-hostname` adds an additional address to an existing tunnel. The alias's domain must be on the same Cloudflare account this laptop is signed into — if not, the tool refuses with dashboard guidance.
|
|
57
57
|
|
|
58
|
-
**User-facing language:** Say "
|
|
58
|
+
**User-facing language:** Say "Cloudflare account", "domain", "address", "sign in", "browser". Never say "zone", "CNAME", "account ID", "API", "SDK", or any hexadecimal identifier.
|
|
59
59
|
|
|
60
60
|
**Verification:** Always verify URLs work by navigating to them in the browser. Never claim a URL works without browser verification.
|
|
61
61
|
|
package/payload/server/server.js
CHANGED
|
@@ -7684,11 +7684,6 @@ var ADMIN_CORE_TOOLS = [
|
|
|
7684
7684
|
"mcp__admin__action-approve",
|
|
7685
7685
|
"mcp__admin__action-reject",
|
|
7686
7686
|
"mcp__admin__action-edit",
|
|
7687
|
-
"mcp__cloudflare__cloudflare-setup",
|
|
7688
|
-
"mcp__cloudflare__cf-add-zone",
|
|
7689
|
-
"mcp__cloudflare__cf-zone-status",
|
|
7690
|
-
"mcp__cloudflare__cf-verify",
|
|
7691
|
-
"mcp__cloudflare__cf-rebuild",
|
|
7692
7687
|
"mcp__cloudflare__tunnel-status",
|
|
7693
7688
|
"mcp__cloudflare__tunnel-install",
|
|
7694
7689
|
"mcp__cloudflare__tunnel-login",
|
|
@@ -10395,6 +10390,7 @@ var VALID_SOURCES = /* @__PURE__ */ new Set([
|
|
|
10395
10390
|
"session",
|
|
10396
10391
|
"public",
|
|
10397
10392
|
"mcp",
|
|
10393
|
+
"cloudflared",
|
|
10398
10394
|
"config-dir"
|
|
10399
10395
|
]);
|
|
10400
10396
|
var VALID_SCOPES = /* @__PURE__ */ new Set(["global", "session"]);
|
|
@@ -10548,8 +10544,9 @@ function defaultRules() {
|
|
|
10548
10544
|
// Task 533: surface every Cloudflare-plugin refusal. The plugin emits
|
|
10549
10545
|
// exactly one [cloudflare:refuse] line per refusal with a structured
|
|
10550
10546
|
// reason field; any single occurrence on a previously-clean device
|
|
10551
|
-
// means
|
|
10552
|
-
//
|
|
10547
|
+
// means the bound Cloudflare account does not match the operator's
|
|
10548
|
+
// intent (or the post-flight FQDN drifted) and the operator needs to
|
|
10549
|
+
// act in the dashboard.
|
|
10553
10550
|
id: "cloudflare-refuse",
|
|
10554
10551
|
name: "Cloudflare plugin refusal",
|
|
10555
10552
|
type: "silent-catch",
|
|
@@ -10557,7 +10554,81 @@ function defaultRules() {
|
|
|
10557
10554
|
pattern: "\\[cloudflare:refuse\\]|\\[cloudflare:post-flight-mismatch\\]",
|
|
10558
10555
|
thresholdCount: 0,
|
|
10559
10556
|
thresholdWindowMinutes: 0,
|
|
10560
|
-
suggestedAction: "The Cloudflare plugin refused an operation
|
|
10557
|
+
suggestedAction: "The Cloudflare plugin refused an operation. Read the refusal `reason` field in the adjacent log line. For `account-drift` or `unbound-device`, run `tunnel-login force=true` while the operator is signed into the correct Cloudflare account in the browser. For `hostname-zone-not-routable`, the domain is not on Cloudflare yet \u2014 guide the operator to add it via the Cloudflare dashboard. For `post-flight-fqdn-mismatch` or `bound-account-does-not-own-hostname`, the laptop is signed into the wrong Cloudflare account \u2014 guide the operator to switch accounts in the dashboard, then re-run `tunnel-login force=true`."
|
|
10558
|
+
},
|
|
10559
|
+
{
|
|
10560
|
+
// Task 540: the single highest-priority refusal — surface it immediately
|
|
10561
|
+
// and independently of the generic cloudflare-refuse rule so the admin
|
|
10562
|
+
// agent sees it on the very next turn. This is the exact class that
|
|
10563
|
+
// burned the operator for 8 days across 9+ sessions (Apr 11–18, 2026):
|
|
10564
|
+
// tunnel running locally, dashboard serving the wrong account, nothing
|
|
10565
|
+
// from the internet reaches the laptop, and no prior telemetry surfaced
|
|
10566
|
+
// it in time for the agent to self-correct.
|
|
10567
|
+
id: "cloudflare-bound-account-mismatch",
|
|
10568
|
+
name: "Cloudflare bound account does not own the configured hostnames",
|
|
10569
|
+
type: "silent-catch",
|
|
10570
|
+
logSource: "any",
|
|
10571
|
+
pattern: '"reason":"bound-account-does-not-own-hostname"',
|
|
10572
|
+
thresholdCount: 0,
|
|
10573
|
+
thresholdWindowMinutes: 0,
|
|
10574
|
+
suggestedAction: "This laptop is signed into a Cloudflare account that does not own the hostnames the tunnel is configured to serve. Run `tunnel-status` to confirm, then tell the operator verbatim: 'The tunnel is running on this laptop but nothing from the internet is reaching it. The Cloudflare account this laptop is signed into doesn't own your domain. Open Cloudflare in your browser \u2014 is the account name in the top-left the one that owns your domain? If not, switch to the correct one, then tell me and I will re-sign-in.' When the operator confirms the correct account is selected, run `tunnel-login force=true`."
|
|
10575
|
+
},
|
|
10576
|
+
{
|
|
10577
|
+
// Task 541: tunnel-login's browser-launch failure class — cloudflared
|
|
10578
|
+
// reports "Failed to fetch resource" into cloudflared-login.log when
|
|
10579
|
+
// its browser launcher cannot reach the default browser, and the login
|
|
10580
|
+
// process lingers with a live PID holding the log open. Prior to Task
|
|
10581
|
+
// 541, `tunnel-login` inferred liveness from PID alone and kept
|
|
10582
|
+
// reporting "Sign-in in progress" against a dead auth flow. The MCP
|
|
10583
|
+
// handler now parses the log and emits this line on the first call
|
|
10584
|
+
// that detects the failure; the rule surfaces it to the admin agent on
|
|
10585
|
+
// the next turn so the 44-tool-call self-discovery loop (session
|
|
10586
|
+
// 0e30b69e) cannot reopen.
|
|
10587
|
+
id: "cloudflare-tunnel-login-failed",
|
|
10588
|
+
name: "Cloudflare tunnel-login failed (browser launcher fetch error)",
|
|
10589
|
+
type: "silent-catch",
|
|
10590
|
+
logSource: "any",
|
|
10591
|
+
pattern: "\\[cloudflare:tunnel-login:failed\\] reason=browser-launch-fetch-error",
|
|
10592
|
+
thresholdCount: 0,
|
|
10593
|
+
thresholdWindowMinutes: 0,
|
|
10594
|
+
suggestedAction: "cloudflared's browser launcher failed to open the sign-in URL on the laptop. The tunnel-login tool has already detected this and restarted the login \u2014 relay the new sign-in URL to the operator and wait for them to confirm authorization in the VNC browser. Do not open the Cloudflare dashboard in any other surface; the only path is the sign-in URL the tool produces."
|
|
10595
|
+
},
|
|
10596
|
+
{
|
|
10597
|
+
// Task 540: cloudflared.log is the one file most likely to carry the
|
|
10598
|
+
// "tunnel is having real-world connectivity problems" signal — QUIC
|
|
10599
|
+
// connection failures, connector drops, edge unreachability. Prior to
|
|
10600
|
+
// this rule it was written but never read (the review-detector had no
|
|
10601
|
+
// rule coverage for it). A single ERR line is worth surfacing; the
|
|
10602
|
+
// tee'd output is typically noise-free.
|
|
10603
|
+
id: "cloudflared-edge-errors",
|
|
10604
|
+
name: "cloudflared edge connectivity errors",
|
|
10605
|
+
type: "silent-catch",
|
|
10606
|
+
logSource: "cloudflared",
|
|
10607
|
+
pattern: "^\\S+ ERR (Failed to refresh protocol|no more connections active|Failed to dial a quic connection)",
|
|
10608
|
+
thresholdCount: 0,
|
|
10609
|
+
thresholdWindowMinutes: 0,
|
|
10610
|
+
suggestedAction: "cloudflared is reporting edge connectivity errors in its daemon log. Read the last 20 lines of `cloudflared.log` to see the surrounding context. Transient QUIC drops are normal; sustained failures (more than a handful in a minute) point to either a network issue on this laptop or an edge-side routing problem. Run `tunnel-status` to check whether end-to-end probing still succeeds."
|
|
10611
|
+
},
|
|
10612
|
+
{
|
|
10613
|
+
// Task 543: fires when an agent turn contains an opposing-axis choice-fork
|
|
10614
|
+
// question ("Want me to X, or Y?"). Every occurrence is a violation of the
|
|
10615
|
+
// IDENTITY § Questions rule — Rule A (one-sided questions) catches them
|
|
10616
|
+
// all, and the sharper sub-class (Rule B — no menu when a tool returned a
|
|
10617
|
+
// deterministic signal) is isolated by Task 544's `preceded-by` tightening.
|
|
10618
|
+
// logSource is "any" so the rule catches violations on both the admin
|
|
10619
|
+
// stream (claude-agent-stream-*) and the public agent stream
|
|
10620
|
+
// (public-agent-stream-*); the regex is agent-phrasing-specific and has
|
|
10621
|
+
// not been observed in other log contexts. Session scope groups matches
|
|
10622
|
+
// by conversationId so a single offending turn fires exactly once.
|
|
10623
|
+
id: "agent-choice-fork",
|
|
10624
|
+
name: "Agent emitted a choice-fork question",
|
|
10625
|
+
type: "repeated-error",
|
|
10626
|
+
logSource: "any",
|
|
10627
|
+
pattern: "Want me to [^\\n]+, or [^\\n]+\\?",
|
|
10628
|
+
thresholdCount: 1,
|
|
10629
|
+
thresholdWindowMinutes: 60,
|
|
10630
|
+
scope: "session",
|
|
10631
|
+
suggestedAction: 'Agent emitted a choice-fork question ("Want me to X, or Y?") instead of a one-sided question or the prescribed action. Review Task 543 IDENTITY \xA7 Questions \u2014 the agent asked an opposing-axis question, or degraded a deterministic tool signal into a menu. The log sample shows the offending turn verbatim.'
|
|
10561
10632
|
}
|
|
10562
10633
|
];
|
|
10563
10634
|
}
|
|
@@ -10786,6 +10857,10 @@ function discoverSourceFiles(configDir2, accountLogDir2, logicalSource) {
|
|
|
10786
10857
|
const p = resolve8(configDir2, "logs", "vnc-boot.log");
|
|
10787
10858
|
return existsSync8(p) ? [{ logicalSource: "vnc", filepath: p }] : [];
|
|
10788
10859
|
}
|
|
10860
|
+
if (logicalSource === "cloudflared") {
|
|
10861
|
+
const p = resolve8(configDir2, "logs", "cloudflared.log");
|
|
10862
|
+
return existsSync8(p) ? [{ logicalSource: "cloudflared", filepath: p }] : [];
|
|
10863
|
+
}
|
|
10789
10864
|
const prefix = {
|
|
10790
10865
|
system: "claude-agent-stream-",
|
|
10791
10866
|
error: "claude-agent-stderr-",
|
|
@@ -10830,7 +10905,8 @@ function discoverAllSources(configDir2, accountLogDir2) {
|
|
|
10830
10905
|
...discoverSourceFiles(configDir2, accountLogDir2, "error"),
|
|
10831
10906
|
...discoverSourceFiles(configDir2, accountLogDir2, "session"),
|
|
10832
10907
|
...discoverSourceFiles(configDir2, accountLogDir2, "public"),
|
|
10833
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "mcp")
|
|
10908
|
+
...discoverSourceFiles(configDir2, accountLogDir2, "mcp"),
|
|
10909
|
+
...discoverSourceFiles(configDir2, accountLogDir2, "cloudflared")
|
|
10834
10910
|
];
|
|
10835
10911
|
}
|
|
10836
10912
|
function readNewLines(filepath, prev) {
|