typeclaw 0.3.0 → 0.4.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/README.md +20 -15
- package/auth.schema.json +113 -0
- package/package.json +2 -1
- package/scripts/dump-system-prompt.ts +401 -0
- package/secrets.schema.json +113 -0
- package/src/agent/index.ts +149 -30
- package/src/agent/provider-error.ts +44 -0
- package/src/agent/session-meta.ts +43 -0
- package/src/agent/session-origin.ts +3 -2
- package/src/agent/subagents.ts +8 -0
- package/src/agent/system-prompt.ts +70 -35
- package/src/bundled-plugins/security/index.ts +3 -2
- package/src/channels/adapters/github/auth-app.ts +120 -0
- package/src/channels/adapters/github/auth-pat.ts +50 -0
- package/src/channels/adapters/github/auth.ts +33 -0
- package/src/channels/adapters/github/channel-resolver.ts +30 -0
- package/src/channels/adapters/github/dedup.ts +26 -0
- package/src/channels/adapters/github/event-allowlist.ts +8 -0
- package/src/channels/adapters/github/fetch-attachment.ts +5 -0
- package/src/channels/adapters/github/history.ts +63 -0
- package/src/channels/adapters/github/inbound.ts +286 -0
- package/src/channels/adapters/github/index.ts +286 -0
- package/src/channels/adapters/github/managed-path.ts +54 -0
- package/src/channels/adapters/github/membership.ts +35 -0
- package/src/channels/adapters/github/outbound.ts +145 -0
- package/src/channels/adapters/github/webhook-register.ts +349 -0
- package/src/channels/manager.ts +94 -9
- package/src/channels/router.ts +28 -2
- package/src/channels/schema.ts +31 -1
- package/src/channels/tunnel-bridge.ts +51 -0
- package/src/cli/builtins.ts +28 -0
- package/src/cli/channel.ts +511 -25
- package/src/cli/container-command-client.ts +244 -0
- package/src/cli/cron.ts +173 -0
- package/src/cli/host-command-runner.ts +150 -0
- package/src/cli/index.ts +42 -1
- package/src/cli/init.ts +256 -27
- package/src/cli/model.ts +4 -2
- package/src/cli/plugin-command-help.ts +49 -0
- package/src/cli/plugin-commands-dispatch.ts +112 -0
- package/src/cli/plugin-commands.ts +118 -0
- package/src/cli/tui.ts +10 -2
- package/src/cli/tunnel.ts +533 -0
- package/src/cli/ui.ts +8 -3
- package/src/cli/usage.ts +30 -2
- package/src/config/config.ts +90 -4
- package/src/config/reloadable.ts +22 -4
- package/src/container/start.ts +30 -3
- package/src/cron/bridge.ts +136 -0
- package/src/cron/consumer.ts +62 -6
- package/src/cron/index.ts +19 -2
- package/src/cron/list.ts +105 -0
- package/src/cron/scheduler.ts +12 -3
- package/src/cron/schema.ts +11 -3
- package/src/doctor/checks.ts +0 -50
- package/src/init/dockerfile.ts +59 -13
- package/src/init/ensure-deps.ts +15 -4
- package/src/init/github-webhook-install.ts +109 -0
- package/src/init/index.ts +505 -9
- package/src/init/run-bun-install.ts +17 -3
- package/src/init/run-owner-claim.ts +11 -2
- package/src/permissions/builtins.ts +6 -1
- package/src/permissions/match-rule.ts +24 -2
- package/src/permissions/resolve.ts +1 -0
- package/src/plugin/define.ts +42 -1
- package/src/plugin/index.ts +18 -3
- package/src/plugin/manager.ts +2 -0
- package/src/plugin/registry.ts +85 -3
- package/src/plugin/types.ts +138 -1
- package/src/plugin/zod-introspect.ts +100 -0
- package/src/role-claim/match-rule.ts +2 -1
- package/src/run/index.ts +119 -4
- package/src/secrets/index.ts +1 -1
- package/src/secrets/schema.ts +21 -0
- package/src/server/command-runner.ts +476 -0
- package/src/server/index.ts +393 -15
- package/src/shared/index.ts +8 -0
- package/src/shared/protocol.ts +80 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
- package/src/skills/typeclaw-config/SKILL.md +27 -26
- package/src/skills/typeclaw-cron/SKILL.md +234 -3
- package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
- package/src/skills/typeclaw-permissions/SKILL.md +5 -4
- package/src/skills/typeclaw-plugins/SKILL.md +251 -5
- package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
- package/src/test-helpers/wait-for.ts +50 -0
- package/src/tui/index.ts +35 -4
- package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
- package/src/tunnels/events.ts +14 -0
- package/src/tunnels/index.ts +12 -0
- package/src/tunnels/log-ring.ts +54 -0
- package/src/tunnels/manager.ts +139 -0
- package/src/tunnels/providers/cloudflare-quick.ts +189 -0
- package/src/tunnels/providers/external.ts +53 -0
- package/src/tunnels/quick-url-parser.ts +5 -0
- package/src/tunnels/types.ts +43 -0
- package/src/usage/aggregate.ts +30 -1
- package/src/usage/index.ts +3 -2
- package/src/usage/report.ts +103 -3
- package/src/usage/scan.ts +59 -4
- package/typeclaw.schema.json +254 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: typeclaw-plugins
|
|
3
|
-
description: TypeClaw plugin authoring and operation guide. Use when writing, editing, configuring, debugging, or installing a TypeClaw plugin — including any work with definePlugin, defineTool, defineSubagent, plugin hooks (session.start/end/idle/prompt, tool.before/after), plugin cron jobs, plugin skills, the typeclaw/plugin import path, or per-plugin config blocks in typeclaw.json. Triggers on mentions of 'TypeClaw plugin', 'definePlugin', 'plugin hook', 'plugin cron', 'plugins[]', 'typeclaw-plugin-', or any file under src/plugin/ or plugins/.
|
|
3
|
+
description: TypeClaw plugin authoring and operation guide. Use when writing, editing, configuring, debugging, or installing a TypeClaw plugin — including any work with definePlugin, defineTool, defineSubagent, plugin hooks (session.start/end/idle/prompt, tool.before/after), plugin cron jobs, plugin commands (host/container/either CLI subcommands callable as `typeclaw <name>`), plugin skills, the typeclaw/plugin import path, or per-plugin config blocks in typeclaw.json. Also use when you need to bridge a cron `exec` job to LLM-driven work — the canonical pattern is a `surface: 'container'` plugin command whose `run` calls `ctx.prompt(...)`, invoked as `typeclaw <command>` from cron's `command` array. Triggers on mentions of 'TypeClaw plugin', 'definePlugin', 'plugin hook', 'plugin cron', 'plugin command', 'PluginCommand', 'ContainerCommand', 'HostCommand', 'EitherCommand', 'ctx.prompt', 'ctx.subagent', 'ctx.exec', 'plugins[]', 'typeclaw-plugin-', or any file under src/plugin/ or plugins/.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# TypeClaw Plugins
|
|
@@ -252,13 +252,59 @@ cronJobs: {
|
|
|
252
252
|
kind: 'exec',
|
|
253
253
|
command: ['bun', 'run', 'scripts/rotate.ts'],
|
|
254
254
|
},
|
|
255
|
+
// The canonical shape for scheduled imperative LLM work: a handler
|
|
256
|
+
// function the cron consumer invokes directly. No shell-out, no WS
|
|
257
|
+
// round-trip, no Bun.spawn — the handler runs in-process with the same
|
|
258
|
+
// ctx.prompt / ctx.exec surface a container command sees.
|
|
259
|
+
'inbox-watch': {
|
|
260
|
+
schedule: '*/15 * * * *',
|
|
261
|
+
kind: 'handler',
|
|
262
|
+
handler: async (ctx) => {
|
|
263
|
+
const { stdout } = await ctx.exec`gmail unread --count`
|
|
264
|
+
if (Number(stdout.trim()) === 0) return
|
|
265
|
+
await ctx.prompt(`Triage ${stdout.trim()} new emails…`)
|
|
266
|
+
},
|
|
267
|
+
},
|
|
255
268
|
}
|
|
256
269
|
```
|
|
257
270
|
|
|
258
271
|
- The map key is a **suffix**. The runtime constructs the global cron id as `__plugin_<plugin-name>_<key>` (e.g., `__plugin_standup-log_weekly-digest`).
|
|
259
272
|
- `cron.json` user job ids cannot start with underscore, so collision is impossible by construction.
|
|
260
273
|
- A `prompt` job's `subagent` and `payload` are **validated against the registry at boot** — bad references fail loudly on disk, not 6 hours later when the job fires.
|
|
261
|
-
-
|
|
274
|
+
- Three kinds: `prompt`, `exec`, `handler`. **`handler` is plugin-only** — it cannot appear in `cron.json` because the handler is a TypeScript function reference (not JSON-serializable). User-authored cron files are validated by `parseCronFile` which rejects anything outside `prompt | exec`.
|
|
275
|
+
|
|
276
|
+
#### `kind: 'handler'` — direct function dispatch
|
|
277
|
+
|
|
278
|
+
When the cron job needs imperative control flow (probe → maybe prompt → write file) and the logic lives in the same plugin as the schedule, declare it as a `handler`. The consumer invokes the function directly with a `CronHandlerContext`:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
type CronHandlerContext = {
|
|
282
|
+
readonly jobId: string // __plugin_<name>_<key>
|
|
283
|
+
readonly name: string // plugin name that registered the job
|
|
284
|
+
readonly agentDir: string // /agent in container
|
|
285
|
+
readonly logger: PluginLogger
|
|
286
|
+
readonly signal: AbortSignal // reserved for future cancellation; currently inert
|
|
287
|
+
readonly permissions: PermissionService
|
|
288
|
+
readonly origin: SessionOrigin // { kind: 'cron', jobKind: 'handler', ... }
|
|
289
|
+
readonly prompt: (text: string) => Promise<string> // full agent session, slim system prompt mode
|
|
290
|
+
readonly subagent: (name, payload?) => Promise<void>
|
|
291
|
+
readonly exec: (cmd, ...vals) => Promise<CommandExecResult> // tagged template
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The `prompt` / `subagent` / `exec` surface is identical to `ContainerCommandContext` (§5.7) and reuses the same underlying implementation — abort semantics, process-group kill, slim system prompt mode are all shared. Differences from `ContainerCommandContext`: no `stdin` / `stdout` / `stderr` (cron has no caller piping bytes), no `args` (handlers are scheduled, not invoked with flags), no return value (throw to signal failure, the consumer logs).
|
|
296
|
+
|
|
297
|
+
#### When to use which `kind`
|
|
298
|
+
|
|
299
|
+
| `kind` | Use for |
|
|
300
|
+
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
301
|
+
| `'prompt'` | One-shot natural-language prompts. Stable instruction, no shell pre-work, no conditional logic. |
|
|
302
|
+
| `'exec'` | Pure shell work — `git commit`, log rotation, calling a script. Can also point at a plugin's `surface: 'host'` command via `['typeclaw', '<cmd>']`. |
|
|
303
|
+
| **`'handler'`** | **The default for plugin-internal scheduled imperative work.** Probe + maybe prompt, multi-step orchestration, anything mixing shell and LLM calls. |
|
|
304
|
+
|
|
305
|
+
A plugin that exposes a `surface: 'container'` command (§5.7) often does NOT need a corresponding cron handler — if the command's whole `run` body is the scheduled work, just factor the body into a shared private function and have BOTH the command and the cron handler call it. The command stays callable from the TUI / manual shell; the cron handler stays callable from the scheduler without shelling out.
|
|
306
|
+
|
|
307
|
+
The pre-handler workaround (cron `kind: 'exec'` with `command: ['typeclaw', '<plugin-cmd>']` shelling out to its own container) is still valid but no longer the default — reach for it only when the user owns the cadence (`cron.json` scheduling someone else's command) or when the scheduled work is genuinely a host-side command. See `typeclaw-cron` for the full decision tree.
|
|
262
308
|
|
|
263
309
|
### 5.4 `skills` — string-form (per-session tmpdir)
|
|
264
310
|
|
|
@@ -326,6 +372,198 @@ Provider prompt caching makes the **prefix** of the system prompt 5–10× cheap
|
|
|
326
372
|
|
|
327
373
|
If your content varies per session, **append**. If it's stable across sessions, prepending is fine but understand the cost.
|
|
328
374
|
|
|
375
|
+
### 5.7 `commands` — typeclaw CLI subcommands
|
|
376
|
+
|
|
377
|
+
A plugin can register top-level CLI commands invocable as `typeclaw <name>` from any shell sitting in the agent folder. **Unlike every other contribution in §5, `commands` is declared by-value on `definePlugin(...)`, NOT inside the factory return.** This is so the host-stage CLI can dispatch commands without booting the plugin runtime (no `bun install`, no factory, no engine spin-up just to print `--help`).
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
import { z } from 'zod'
|
|
381
|
+
import { definePlugin } from 'typeclaw/plugin'
|
|
382
|
+
|
|
383
|
+
export default definePlugin({
|
|
384
|
+
commands: {
|
|
385
|
+
'standup-now': {
|
|
386
|
+
surface: 'container',
|
|
387
|
+
description: 'Generate a standup write-up for today from sessions/.',
|
|
388
|
+
args: z.object({
|
|
389
|
+
date: z.string().optional().describe('YYYY-MM-DD; defaults to today'),
|
|
390
|
+
}),
|
|
391
|
+
async run(ctx, args) {
|
|
392
|
+
const text = await ctx.prompt(
|
|
393
|
+
`Read sessions/ for ${args.date ?? 'today'} and write a 3-bullet standup to standup/${args.date ?? 'today'}.md.`,
|
|
394
|
+
)
|
|
395
|
+
const writer = ctx.stdout.getWriter()
|
|
396
|
+
await writer.write(new TextEncoder().encode(text + '\n'))
|
|
397
|
+
writer.releaseLock()
|
|
398
|
+
return 0
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
plugin: async (ctx) => ({
|
|
403
|
+
/* tools, hooks, cron, ... */
|
|
404
|
+
}),
|
|
405
|
+
})
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Once installed, the user (or a cron `exec` job) runs `typeclaw standup-now --date=2026-05-18`. `typeclaw --help` lists all discovered plugin commands automatically — no separate registration.
|
|
409
|
+
|
|
410
|
+
#### The three surfaces
|
|
411
|
+
|
|
412
|
+
| `surface` | Runs where | `ctx` has | Use when |
|
|
413
|
+
| ------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
414
|
+
| `'container'` | Inside the running agent container, proxied over WS by the host CLI | `prompt`, `subagent`, `exec`, `permissions`, `origin`, `signal` + streams | The command needs the agent runtime — LLM calls (`ctx.prompt`), subagent invocation, permission checks. Reusable from TUI, manual shell, `compose`, and (as a narrower fallback when reusability is required) cron `exec`. For plugin-internal scheduled `exec → LLM` work, prefer `kind: 'handler'` (§5.3) — see "Cron usage" below. |
|
|
415
|
+
| `'host'` | On the user's machine, no container required | `streams`, `signal`, `logger`, `agentDir` (host path) | The command only touches host-side state (files, host binaries, prompts the user). Container does NOT need to be running. |
|
|
416
|
+
| `'either'` | Whichever stage invoked it — same author code runs in both | The intersection (`streams`, `signal`, `logger`, `agentDir`) | The command's logic is stage-agnostic. `agentDir` resolves to `/agent` in the container and the host path on the host, automatically. |
|
|
417
|
+
|
|
418
|
+
The `surface: 'container'` command requires the container to be running. The host CLI opens a WebSocket to `/commands` on the agent's port, sends `exec_command`, and streams stdout/stderr back. Ctrl-C on the host propagates as `AbortSignal` to `ctx.signal` inside the container.
|
|
419
|
+
|
|
420
|
+
#### `ContainerCommandContext` — what you get inside `run`
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
type ContainerCommandContext = {
|
|
424
|
+
readonly name: string // plugin name (e.g. 'standup-log'), NOT command name
|
|
425
|
+
readonly version: string | undefined
|
|
426
|
+
readonly agentDir: string // /agent inside the container
|
|
427
|
+
readonly logger: PluginLogger
|
|
428
|
+
readonly permissions: PermissionService
|
|
429
|
+
readonly origin: SessionOrigin // caller's origin — cron job, TUI op, etc.
|
|
430
|
+
readonly signal: AbortSignal // aborts on ws close or host Ctrl-C
|
|
431
|
+
readonly stdin: ReadableStream<Uint8Array>
|
|
432
|
+
readonly stdout: WritableStream<Uint8Array>
|
|
433
|
+
readonly stderr: WritableStream<Uint8Array>
|
|
434
|
+
readonly prompt: (text: string) => Promise<string> // full LLM session, full toolset, returns last assistant text
|
|
435
|
+
readonly subagent: (name: string, payload?: unknown) => Promise<void>
|
|
436
|
+
readonly exec: (cmd: TemplateStringsArray, ...values: unknown[]) => Promise<CommandExecResult>
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Key facts about each capability:
|
|
441
|
+
|
|
442
|
+
- **`ctx.prompt(text)`** opens a brand-new `AgentSession` with the full agent toolset (read/bash/edit/write/grep/find/ls + plugin tools), sends `text` as if a user typed it, and returns the last assistant message. The session is created and disposed inside the call. The session uses **slim system prompt mode** (subagent-shaped origin) so you save ~2000 tokens per LLM call versus a normal TUI session.
|
|
443
|
+
- **`ctx.subagent(name, payload)`** invokes a registered subagent (yours or another plugin's). Returns when the subagent's `runSession` resolves.
|
|
444
|
+
- **`ctx.exec` is a tagged template** — `await ctx.exec\`git log --oneline -10\``runs the command in the agent folder with`ctx.signal` threaded through. Aborts kill the entire process group (SIGTERM → 5s grace → SIGKILL) so daemonized grandchildren don't outlive the abort.
|
|
445
|
+
- **`ctx.origin`** carries the caller's `SessionOrigin`. For host-invoked (TUI op) calls it's `{ kind: 'tui', ... }`; for cron-invoked calls it's the cron job's origin including `scheduledByRole`. **No silent role elevation** — a cron job running as `scheduledByRole: 'member'` invokes the command with that same role, and permission checks inside the command resolve accordingly.
|
|
446
|
+
|
|
447
|
+
#### Cron usage: prefer `kind: 'handler'` over shelling out to your own command
|
|
448
|
+
|
|
449
|
+
Plugin cron jobs support `kind: 'handler'` (§5.3) which invokes a TypeScript function directly with a `CronHandlerContext`. The handler ctx exposes the SAME `ctx.prompt` / `ctx.subagent` / `ctx.exec` surface a container command sees — same slim-mode session, same process-group abort semantics — but without the shell-out, the WS round-trip, or the args-parse round-trip.
|
|
450
|
+
|
|
451
|
+
**If the cron job and the command both live in the same plugin, prefer a handler.** Factor any shared logic into a private function and have BOTH the command's `run` body and the cron handler call it. The command stays callable from the TUI / manual `typeclaw` invocations; the cron handler stays callable from the scheduler with zero shell-out cost.
|
|
452
|
+
|
|
453
|
+
The shell-out pattern below (cron `exec` → `typeclaw <plugin-cmd>`) is still supported and stays valid in three narrow cases, all rooted in **the same logic needing a callable surface beyond cron**:
|
|
454
|
+
|
|
455
|
+
1. **The logic is also a reusable CLI command.** The user wants to run it manually as `typeclaw <cmd> --flag=...` from the TUI / shell / `compose`, or another caller needs the same args contract. Write the logic once inside a `surface: 'container'` command's `run`; reuse it from cron by pointing an `exec` job at the same command. "Scheduled work that needs LLM judgement" alone is NOT this case — without an external caller, prefer `kind: 'handler'` and avoid the shell-out overhead.
|
|
456
|
+
2. **The user owns the cadence.** `cron.json` schedules someone else's plugin command at a custom cadence the plugin author didn't anticipate. The user doesn't fork the plugin to change the schedule.
|
|
457
|
+
3. **The scheduled work needs a `surface: 'host'` command.** Host commands run outside the container with no agent runtime, so `ctx.prompt` is unavailable; the shell-out via `typeclaw <host-cmd>` is the only path.
|
|
458
|
+
|
|
459
|
+
#### The cron-exec → typeclaw shell-out (narrower use case)
|
|
460
|
+
|
|
461
|
+
For plugin-internal scheduled `exec → LLM` work, `kind: 'handler'` (§5.3) is the best practice — see "Cron usage" above. The pattern below — write a `surface: 'container'` plugin command whose `run` calls `ctx.prompt(...)`, then point a `cron.json` `exec` job at it — is the fallback when **reusability is the actual requirement**: the same logic must also be invocable as a CLI command from TUI / manual shell / `compose`, or the user owns the cadence for a command they didn't write, or the work needs `surface: 'host'` (where `ctx.prompt` doesn't exist).
|
|
462
|
+
|
|
463
|
+
User-authored `cron.json` itself supports only `prompt` and `exec` — that's by design. `kind: 'handler'` is plugin-only because the handler is a function reference, not JSON-serializable.
|
|
464
|
+
|
|
465
|
+
```json
|
|
466
|
+
// cron.json
|
|
467
|
+
{
|
|
468
|
+
"jobs": [
|
|
469
|
+
{
|
|
470
|
+
"id": "daily-standup",
|
|
471
|
+
"schedule": "30 9 * * 1-5",
|
|
472
|
+
"timezone": "Asia/Seoul",
|
|
473
|
+
"kind": "exec",
|
|
474
|
+
"command": ["typeclaw", "standup-now"]
|
|
475
|
+
}
|
|
476
|
+
]
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
// packages/standup-log/index.ts
|
|
482
|
+
export default definePlugin({
|
|
483
|
+
commands: {
|
|
484
|
+
'standup-now': {
|
|
485
|
+
surface: 'container',
|
|
486
|
+
description: 'Generate today’s standup.',
|
|
487
|
+
async run(ctx) {
|
|
488
|
+
await ctx.prompt(
|
|
489
|
+
`Read sessions/$(date +%F)*.jsonl and append a 3-bullet standup to memory/standups/$(date +%F).md.`,
|
|
490
|
+
)
|
|
491
|
+
return 0
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
plugin: async () => ({}),
|
|
496
|
+
})
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
This `cron.json → typeclaw <cmd>` shape is the right choice in the three narrow cases listed above. For plugin-internal scheduled work where the cadence belongs to the plugin author and nothing outside cron needs to invoke the logic, write a `kind: 'handler'` job (§5.3) instead — same `ctx.prompt` / `ctx.exec` shape, none of the shell-out overhead.
|
|
500
|
+
|
|
501
|
+
Why a CLI command is worth defining (and exposing via cron `exec`) when one of the three cases applies:
|
|
502
|
+
|
|
503
|
+
- The command is reusable from the TUI, from `compose` orchestration, or from a manual `typeclaw standup-now` invocation by the user.
|
|
504
|
+
- Args (`--date`, `--dry-run`, etc.) are declared once via `args: z.object({...})` and parsed/validated by the runtime — both at the host CLI and as defense-in-depth in the container.
|
|
505
|
+
- A user who wants a different cadence than the plugin's default can drop a `cron.json` entry pointing at the command without forking the plugin.
|
|
506
|
+
|
|
507
|
+
If none of those benefits actually apply to your case, the CLI command shape is overhead — write a handler.
|
|
508
|
+
|
|
509
|
+
For the cron-side decision rules (when to pick `handler` vs `prompt` vs `exec → typeclaw <cmd>`, and how to gate `ctx.prompt` behind a cheap `ctx.exec` probe) read `typeclaw-cron`.
|
|
510
|
+
|
|
511
|
+
#### `args` — Zod object schema with primitive leaves
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
args: z.object({
|
|
515
|
+
date: z.string().optional().describe('YYYY-MM-DD; defaults to today'),
|
|
516
|
+
dryRun: z.boolean().default(false),
|
|
517
|
+
count: z.number().int().min(1).max(100).default(10),
|
|
518
|
+
})
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
- The top level **MUST** be `z.object({...})`. Leaves should be primitives (`string`, `number`, `boolean`, `literal`, `enum`) so `--help` can render `--<name>=<type>`.
|
|
522
|
+
- Args are validated locally by the host CLI **before** any WS round-trip, so bad args fail fast with a clean error and exit code 2. The container re-validates as defense-in-depth.
|
|
523
|
+
- `.describe(...)` populates `--help` output. Use it.
|
|
524
|
+
- Omit `args` entirely if the command takes no flags.
|
|
525
|
+
|
|
526
|
+
#### `permissions: [...]` on the command
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
{
|
|
530
|
+
surface: 'container',
|
|
531
|
+
permissions: ['standup-log.write.standup'],
|
|
532
|
+
async run(ctx, args) {
|
|
533
|
+
ctx.permissions.assert(ctx.origin, 'standup-log.write.standup')
|
|
534
|
+
// ...
|
|
535
|
+
},
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Declared permissions are surfaced in `--help` and (for container commands) checked against the caller's origin. Same `<plugin>.<verb>.<noun>` shape as the rest of the permission system; see `typeclaw-permissions`.
|
|
540
|
+
|
|
541
|
+
#### `isolated: true` (container surface only)
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
{
|
|
545
|
+
surface: 'container',
|
|
546
|
+
isolated: true, // currently degrades to in-process with a warning on stderr
|
|
547
|
+
async run(ctx, args) { /* ... */ },
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Reserved for a future subprocess sandbox. Today the runtime accepts the flag and emits a warning on the per-command stderr (visible to the invoking CLI) but executes in-process anyway. Set it now if you genuinely want the isolation when it lands; otherwise omit.
|
|
552
|
+
|
|
553
|
+
#### Discovery and naming
|
|
554
|
+
|
|
555
|
+
- Command names are **global across all plugins**. Two plugins registering `standup-now` is a discovery error — the second one is dropped and logged on `--help`.
|
|
556
|
+
- Command names are NOT auto-prefixed with the plugin name. Pick discriminating names (`standup-now`, not `run`).
|
|
557
|
+
- `typeclaw --help` (in any agent folder) lists every discovered plugin command with description, surface, and which plugin owns it.
|
|
558
|
+
- `typeclaw <name> --help` renders args, surface, plugin name + version. Free.
|
|
559
|
+
|
|
560
|
+
#### What's NOT supported
|
|
561
|
+
|
|
562
|
+
- **No host-stage CLI commands that mutate the live container without going through `restart` / `reload`.** A host command can `Bun.spawn('typeclaw', ['reload'])` if it needs to push a config change, but there's no privileged backdoor.
|
|
563
|
+
- **No tool-style `content: ContentPart[]` return.** Commands write to `ctx.stdout` and return an exit code. They are CLI processes, not LLM tool calls.
|
|
564
|
+
- **No streaming token output from `ctx.prompt`** yet — the full LLM response arrives as one stdout burst. Chunked streaming is on the roadmap.
|
|
565
|
+
- **No nested command dispatch.** A command cannot invoke `typeclaw <other-cmd>` and expect to share state; spawn a subprocess or share a subagent instead.
|
|
566
|
+
|
|
329
567
|
---
|
|
330
568
|
|
|
331
569
|
## 6. PluginContext
|
|
@@ -530,9 +768,8 @@ If you find yourself wanting any of these, the design has gone wrong somewhere
|
|
|
530
768
|
- **Stream subscriptions**. Plugins observe through the typed `hooks` surface; they cannot subscribe to the in-process pub/sub directly.
|
|
531
769
|
- **Server-side TUI push notifications** from plugin code. Tool calls reach the TUI via existing `tool_start`/`tool_end` events.
|
|
532
770
|
- **Dockerfile fragments** contributed by plugins. The Dockerfile is core-managed.
|
|
533
|
-
- **New cron job kinds
|
|
771
|
+
- **New cron job kinds for user-authored `cron.json`** beyond `prompt` and `exec`. (Subagent invocation is a `prompt` variant, not a separate kind. Plugin cron jobs additionally support `kind: 'handler'` — see §5.3 — but that's plugin-only because the handler is a TypeScript function reference, not JSON-serializable.)
|
|
534
772
|
- **Reload-registry scopes** for plugin-owned state.
|
|
535
|
-
- **Host-stage CLI commands** registered by plugins. Plugins are container-stage only.
|
|
536
773
|
- **`extendConfig`** for arbitrary top-level fields outside the plugin's own config block.
|
|
537
774
|
- **Per-LLM-call hooks** (`llm.params` / `llm.headers`). Wait until a real plugin needs them.
|
|
538
775
|
|
|
@@ -575,6 +812,10 @@ export default definePlugin({
|
|
|
575
812
|
configSchema: z.object({
|
|
576
813
|
/* ... */
|
|
577
814
|
}), // optional
|
|
815
|
+
commands: {
|
|
816
|
+
/* name: { surface, run, args?, ... } */
|
|
817
|
+
}, // optional, declared BY-VALUE (not inside factory)
|
|
818
|
+
permissions: ['my-plugin.write.x'], // optional
|
|
578
819
|
plugin: async (ctx) => ({
|
|
579
820
|
// required
|
|
580
821
|
tools,
|
|
@@ -582,7 +823,8 @@ export default definePlugin({
|
|
|
582
823
|
cronJobs,
|
|
583
824
|
skills,
|
|
584
825
|
skillsDirs,
|
|
585
|
-
hooks,
|
|
826
|
+
hooks,
|
|
827
|
+
doctorChecks, // all optional
|
|
586
828
|
}),
|
|
587
829
|
})
|
|
588
830
|
```
|
|
@@ -591,4 +833,8 @@ export default definePlugin({
|
|
|
591
833
|
|
|
592
834
|
**Plugin name = derived**: scope-stripped, `typeclaw-plugin-` prefix stripped (npm), or basename minus extension (local).
|
|
593
835
|
|
|
836
|
+
**Command name = global**: NOT prefixed with plugin name. Two plugins registering the same command name is a discovery error (second is dropped, logged on `--help`).
|
|
837
|
+
|
|
838
|
+
**`exec → LLM` from cron** (best practice): plugin `cronJobs` entry with `kind: 'handler'` — a TypeScript function the cron consumer invokes directly with `ctx.prompt` / `ctx.subagent` / `ctx.exec`. No shell-out, no WS round-trip. Fall back to `surface: 'container'` command + cron `exec` pointing at `["typeclaw", "<cmd>"]` ONLY when the same logic must also be invocable as a reusable CLI command, the user owns the cadence for someone else's command, or the work needs `surface: 'host'`.
|
|
839
|
+
|
|
594
840
|
**Boundary**: `src/plugin/**` MUST NOT import `@mariozechner/*`.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typeclaw-tunnels
|
|
3
|
+
description: Use when the user mentions tunnel, ngrok, webhook URL, cloudflared, expose to internet, show my friend, public URL, GitHub webhook, port forward to public, reverse proxy, trycloudflare, or making a container-local service reachable from the internet. Read it before suggesting tunnel add/remove/status/logs or editing typeclaw.json tunnels[].
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# typeclaw-tunnels
|
|
7
|
+
|
|
8
|
+
TypeClaw tunnels expose a container-private HTTP/TCP service to the public internet. Use them for inbound webhooks (especially GitHub) or short-lived demos where the user wants a public URL for a local port.
|
|
9
|
+
|
|
10
|
+
## When to suggest `typeclaw tunnel add`
|
|
11
|
+
|
|
12
|
+
Suggest a tunnel when the user asks for any of these:
|
|
13
|
+
|
|
14
|
+
- "give GitHub a webhook URL", "make GitHub webhooks work", or similar webhook delivery work.
|
|
15
|
+
- "expose this to the internet", "show my friend", "public URL", or "share this dashboard".
|
|
16
|
+
- "use ngrok" or "cloudflared" for a port running inside the agent container.
|
|
17
|
+
|
|
18
|
+
Do **not** suggest a tunnel for host-local browsing only. If the user just needs to open a dev server on their own machine, TypeClaw's port-forwarder usually already maps container LISTEN ports to `127.0.0.1:<port>` on the host. Tunnels are for public internet ingress.
|
|
19
|
+
|
|
20
|
+
## Provider choices
|
|
21
|
+
|
|
22
|
+
### Cloudflare Quick Tunnel
|
|
23
|
+
|
|
24
|
+
Choose Cloudflare Quick when the user wants the easiest path:
|
|
25
|
+
|
|
26
|
+
- No Cloudflare account or signup.
|
|
27
|
+
- No host-side binary install; `cloudflared` runs inside the container.
|
|
28
|
+
- The URL looks like `https://<random>.trycloudflare.com`.
|
|
29
|
+
- The URL rotates on container restart or tunnel restart. That is expected.
|
|
30
|
+
|
|
31
|
+
For GitHub channel setup, `typeclaw channel add github` can write a channel-owned Cloudflare Quick tunnel named `github-webhook` and set `docker.file.cloudflared: true`. The first `typeclaw start` or `restart` after that rebuilds the image with `cloudflared` installed.
|
|
32
|
+
|
|
33
|
+
### External URL
|
|
34
|
+
|
|
35
|
+
Choose External when the user already has their own reverse proxy or tunnel:
|
|
36
|
+
|
|
37
|
+
- ngrok, a Cloudflare named tunnel managed outside TypeClaw, Caddy on a VPS, Tailscale Funnel, or any HTTPS reverse proxy.
|
|
38
|
+
- The URL is stable because the user owns it.
|
|
39
|
+
- TypeClaw does not spawn a subprocess for this provider; it records the URL and broadcasts it to channel consumers.
|
|
40
|
+
|
|
41
|
+
Use `provider: "external"` with `externalUrl: "https://..."`. External URLs must be HTTPS.
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
- `typeclaw tunnel add <name>` — add a manual tunnel to `typeclaw.json`.
|
|
46
|
+
- `typeclaw tunnel list` — show all configured tunnels and their current URL/health.
|
|
47
|
+
- `typeclaw tunnel status <name>` — inspect one tunnel in detail.
|
|
48
|
+
- `typeclaw tunnel logs <name>` — print the tunnel's recent log ring.
|
|
49
|
+
- `typeclaw tunnel logs <name> -f` — follow live tunnel logs.
|
|
50
|
+
- `typeclaw tunnel remove <name>` — remove a manual tunnel. Channel-owned tunnels should be removed through the owning channel flow, not by hand.
|
|
51
|
+
|
|
52
|
+
Tunnel config is **restart-required**. After adding/removing/changing `tunnels[]` or `docker.file.cloudflared`, the user must run `typeclaw restart` from the host stage.
|
|
53
|
+
|
|
54
|
+
## Reading `tunnel status`
|
|
55
|
+
|
|
56
|
+
Healthy Cloudflare Quick tunnels should show:
|
|
57
|
+
|
|
58
|
+
- provider `cloudflare-quick`.
|
|
59
|
+
- a current `https://...trycloudflare.com` URL after cloudflared has emitted one.
|
|
60
|
+
- health like `healthy` or equivalent live/running state.
|
|
61
|
+
- restart count near zero for a stable tunnel.
|
|
62
|
+
|
|
63
|
+
Unhealthy signs:
|
|
64
|
+
|
|
65
|
+
- no URL after startup.
|
|
66
|
+
- repeated restarts or increasing restart count.
|
|
67
|
+
- `unhealthy` / `permanently-failed` state.
|
|
68
|
+
- last error mentioning spawn failure, missing binary, or cloudflared exit.
|
|
69
|
+
|
|
70
|
+
For External tunnels, the URL should be the configured `externalUrl`; there may be no subprocess health to inspect.
|
|
71
|
+
|
|
72
|
+
## Reading `tunnel logs`
|
|
73
|
+
|
|
74
|
+
Healthy Cloudflare Quick logs usually include:
|
|
75
|
+
|
|
76
|
+
- cloudflared startup lines.
|
|
77
|
+
- a line containing the public `https://...trycloudflare.com` URL.
|
|
78
|
+
- no rapid repeated exit/restart sequence.
|
|
79
|
+
|
|
80
|
+
Unhealthy logs often show:
|
|
81
|
+
|
|
82
|
+
- `cloudflared` not found or spawn failure.
|
|
83
|
+
- repeated process exits followed by backoff/restart lines.
|
|
84
|
+
- Cloudflare connection errors or network failures.
|
|
85
|
+
- no URL emission before the process exits.
|
|
86
|
+
|
|
87
|
+
Use `typeclaw tunnel logs <name> -f` while restarting the agent if you need to watch URL discovery live.
|
|
88
|
+
|
|
89
|
+
## Common failure modes
|
|
90
|
+
|
|
91
|
+
### `cloudflared` is not installed
|
|
92
|
+
|
|
93
|
+
The Cloudflare Quick provider requires `docker.file.cloudflared: true`. If it is missing, add it to `typeclaw.json` or re-run the GitHub channel setup choosing Cloudflare Quick, then run `typeclaw restart` so the Dockerfile is regenerated and the image rebuilds.
|
|
94
|
+
|
|
95
|
+
### Quick tunnel URL changed
|
|
96
|
+
|
|
97
|
+
This is normal. Quick Tunnel URLs rotate on restart. GitHub channel-owned tunnels handle this by flowing the resolved URL through the channel manager's `tunnelUrl()` callback and restarting the adapter; do not persist the rotating URL into `typeclaw.json`.
|
|
98
|
+
|
|
99
|
+
### Repeated restarts
|
|
100
|
+
|
|
101
|
+
Inspect `typeclaw tunnel status <name>` and `typeclaw tunnel logs <name>`. Look for spawn errors, network restrictions, or cloudflared exiting before URL discovery. The tunnel manager backs off and eventually stops retrying after repeated failures without a URL.
|
|
102
|
+
|
|
103
|
+
### GitHub webhooks are not delivered
|
|
104
|
+
|
|
105
|
+
Check in this order:
|
|
106
|
+
|
|
107
|
+
1. `typeclaw tunnel status github-webhook` has a current URL.
|
|
108
|
+
2. `typeclaw tunnel logs github-webhook` shows a URL and no crash loop.
|
|
109
|
+
3. The GitHub channel config has repos listed under `channels.github.repos`.
|
|
110
|
+
4. The channel adapter was restarted after the URL arrived. Channel-owned tunnel URLs should flow through `tunnelUrl()` into adapter `start()`, not through config mutation.
|
|
111
|
+
5. GitHub repo webhook settings point at the current URL if using External; for Cloudflare Quick, expect TypeClaw to re-register on URL changes.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type WaitForOptions = {
|
|
2
|
+
timeoutMs?: number
|
|
3
|
+
intervalMs?: number
|
|
4
|
+
description?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 1_000
|
|
8
|
+
const DEFAULT_INTERVAL_MS = 1
|
|
9
|
+
|
|
10
|
+
export async function waitFor<T>(
|
|
11
|
+
predicate: () => T | Promise<T>,
|
|
12
|
+
options: WaitForOptions = {},
|
|
13
|
+
): Promise<NonNullable<T>> {
|
|
14
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
15
|
+
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS
|
|
16
|
+
const deadline = Date.now() + timeoutMs
|
|
17
|
+
|
|
18
|
+
const initial = await predicate()
|
|
19
|
+
if (initial) return initial as NonNullable<T>
|
|
20
|
+
|
|
21
|
+
while (Date.now() < deadline) {
|
|
22
|
+
await new Promise<void>((resolve) => setTimeout(resolve, intervalMs))
|
|
23
|
+
const result = await predicate()
|
|
24
|
+
if (result) return result as NonNullable<T>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const label = options.description ?? 'condition'
|
|
28
|
+
throw new Error(`waitFor: ${label} did not become truthy within ${timeoutMs}ms`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Asserts that `predicate` STAYS falsy for the full `durationMs`. Unlike
|
|
32
|
+
// `waitFor`, this MUST pay the full duration — you cannot observe the absence
|
|
33
|
+
// of an event faster than waiting for it. Use sparingly, and keep durations
|
|
34
|
+
// tight.
|
|
35
|
+
export async function expectStable<T>(
|
|
36
|
+
predicate: () => T | Promise<T>,
|
|
37
|
+
options: { durationMs: number; intervalMs?: number; description?: string },
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const intervalMs = options.intervalMs ?? 5
|
|
40
|
+
const deadline = Date.now() + options.durationMs
|
|
41
|
+
|
|
42
|
+
while (Date.now() < deadline) {
|
|
43
|
+
const result = await predicate()
|
|
44
|
+
if (result) {
|
|
45
|
+
const label = options.description ?? 'condition'
|
|
46
|
+
throw new Error(`expectStable: ${label} became truthy before ${options.durationMs}ms elapsed`)
|
|
47
|
+
}
|
|
48
|
+
await new Promise<void>((resolve) => setTimeout(resolve, intervalMs))
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/tui/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type TerminalFactory = () => Terminal
|
|
|
9
9
|
|
|
10
10
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 30_000
|
|
11
11
|
|
|
12
|
+
export type VersionMismatch = { expected: string; actual: string }
|
|
13
|
+
|
|
12
14
|
export type TuiOptions = {
|
|
13
15
|
url: string
|
|
14
16
|
initialPrompt?: string
|
|
@@ -16,6 +18,13 @@ export type TuiOptions = {
|
|
|
16
18
|
createTerminal?: TerminalFactory
|
|
17
19
|
handshakeTimeoutMs?: number
|
|
18
20
|
exit?: (code: number) => void
|
|
21
|
+
// Locally-known typeclaw version the host CLI is running. When provided
|
|
22
|
+
// and the connected frame's serverVersion is defined and differs,
|
|
23
|
+
// onVersionMismatch is invoked AND a yellow warning line is rendered
|
|
24
|
+
// into the TUI history. The container-side local TUI omits this so no
|
|
25
|
+
// mismatch check fires when client and server are guaranteed in lockstep.
|
|
26
|
+
expectedVersion?: string
|
|
27
|
+
onVersionMismatch?: (info: VersionMismatch) => void
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
export function createTui({
|
|
@@ -25,6 +34,8 @@ export function createTui({
|
|
|
25
34
|
createTerminal = () => new ProcessTerminal(),
|
|
26
35
|
handshakeTimeoutMs = DEFAULT_HANDSHAKE_TIMEOUT_MS,
|
|
27
36
|
exit = process.exit.bind(process),
|
|
37
|
+
expectedVersion,
|
|
38
|
+
onVersionMismatch,
|
|
28
39
|
}: TuiOptions) {
|
|
29
40
|
async function run(): Promise<void> {
|
|
30
41
|
const terminal = createTerminal()
|
|
@@ -44,7 +55,7 @@ export function createTui({
|
|
|
44
55
|
throw err
|
|
45
56
|
})
|
|
46
57
|
|
|
47
|
-
const
|
|
58
|
+
const handshake = await waitForConnected(client, displayUrl, handshakeTimeoutMs).catch((err) => {
|
|
48
59
|
status.setText(colors.red(`connection error: ${err instanceof Error ? err.message : String(err)}`))
|
|
49
60
|
tui.requestRender()
|
|
50
61
|
client.close()
|
|
@@ -52,6 +63,7 @@ export function createTui({
|
|
|
52
63
|
exit(1)
|
|
53
64
|
throw err
|
|
54
65
|
})
|
|
66
|
+
const { sessionId, serverVersion } = handshake
|
|
55
67
|
status.setText(colors.dim(`session: ${sessionId}`))
|
|
56
68
|
tui.requestRender()
|
|
57
69
|
|
|
@@ -217,6 +229,14 @@ export function createTui({
|
|
|
217
229
|
tui.setFocus(editor)
|
|
218
230
|
tui.requestRender()
|
|
219
231
|
|
|
232
|
+
if (expectedVersion !== undefined && serverVersion !== undefined && serverVersion !== expectedVersion) {
|
|
233
|
+
const mismatch: VersionMismatch = { expected: expectedVersion, actual: serverVersion }
|
|
234
|
+
const warning = formatVersionMismatchWarning(mismatch)
|
|
235
|
+
appendHistory(new Text(colors.yellow(warning), 0, 0))
|
|
236
|
+
tui.requestRender()
|
|
237
|
+
onVersionMismatch?.(mismatch)
|
|
238
|
+
}
|
|
239
|
+
|
|
220
240
|
if (initialPrompt) {
|
|
221
241
|
await send(initialPrompt)
|
|
222
242
|
}
|
|
@@ -238,8 +258,12 @@ function redactUrl(url: string): string {
|
|
|
238
258
|
}
|
|
239
259
|
}
|
|
240
260
|
|
|
241
|
-
async function waitForConnected(
|
|
242
|
-
|
|
261
|
+
async function waitForConnected(
|
|
262
|
+
client: Client,
|
|
263
|
+
url: string,
|
|
264
|
+
timeoutMs: number,
|
|
265
|
+
): Promise<{ sessionId: string; serverVersion?: string }> {
|
|
266
|
+
return await new Promise<{ sessionId: string; serverVersion?: string }>((resolve, reject) => {
|
|
243
267
|
const timer = setTimeout(() => {
|
|
244
268
|
cleanup()
|
|
245
269
|
reject(new Error(`timed out waiting for connected message from ${url} after ${timeoutMs}ms`))
|
|
@@ -253,7 +277,10 @@ async function waitForConnected(client: Client, url: string, timeoutMs: number):
|
|
|
253
277
|
client.onMessage((msg) => {
|
|
254
278
|
if (msg.type === 'connected') {
|
|
255
279
|
cleanup()
|
|
256
|
-
resolve(
|
|
280
|
+
resolve({
|
|
281
|
+
sessionId: msg.sessionId,
|
|
282
|
+
...(msg.serverVersion !== undefined ? { serverVersion: msg.serverVersion } : {}),
|
|
283
|
+
})
|
|
257
284
|
}
|
|
258
285
|
if (msg.type === 'error') {
|
|
259
286
|
cleanup()
|
|
@@ -275,3 +302,7 @@ async function waitForConnected(client: Client, url: string, timeoutMs: number):
|
|
|
275
302
|
)
|
|
276
303
|
})
|
|
277
304
|
}
|
|
305
|
+
|
|
306
|
+
export function formatVersionMismatchWarning({ expected, actual }: VersionMismatch): string {
|
|
307
|
+
return `WARN: host CLI is v${expected}, agent container is v${actual}. Some commands may hang or fail. Try \`typeclaw restart --build\`.`
|
|
308
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Fixture source: cloudflare/cloudflared cmd/cloudflared/tunnel/quick_tunnel.go
|
|
2
|
+
# RunQuickTunnel logs the disclaimer, "Requesting new quick Tunnel on trycloudflare.com...",
|
|
3
|
+
# then AsciiBox(...) lines containing the resolved https://<subdomain>.trycloudflare.com URL.
|
|
4
|
+
# Reference fetched from:
|
|
5
|
+
# https://raw.githubusercontent.com/cloudflare/cloudflared/master/cmd/cloudflared/tunnel/quick_tunnel.go
|
|
6
|
+
2026-01-01T00:00:00Z INF Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee, are subject to the Cloudflare Online Services Terms of Use (https://www.cloudflare.com/website-terms/), and Cloudflare reserves the right to investigate your use of Tunnels for violations of such terms. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
|
7
|
+
2026-01-01T00:00:00Z INF Requesting new quick Tunnel on trycloudflare.com...
|
|
8
|
+
2026-01-01T00:00:00Z INF +--------------------------------------------------------------------------------------------------+
|
|
9
|
+
2026-01-01T00:00:00Z INF | Your quick Tunnel has been created! Visit it at (it may take some time to be reachable): |
|
|
10
|
+
2026-01-01T00:00:00Z INF | https://wave-one-fixture.trycloudflare.com |
|
|
11
|
+
2026-01-01T00:00:00Z INF +--------------------------------------------------------------------------------------------------+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TunnelUrlChangedPayload } from './types'
|
|
2
|
+
|
|
3
|
+
export function isTunnelUrlChangedPayload(value: unknown): value is TunnelUrlChangedPayload {
|
|
4
|
+
if (value === null || typeof value !== 'object') return false
|
|
5
|
+
const v = value as Record<string, unknown>
|
|
6
|
+
if (v.kind !== 'tunnel-url-changed') return false
|
|
7
|
+
if (typeof v.tunnelName !== 'string') return false
|
|
8
|
+
if (typeof v.url !== 'string') return false
|
|
9
|
+
if (typeof v.rotatedAt !== 'string') return false
|
|
10
|
+
if (v.for === null || typeof v.for !== 'object') return false
|
|
11
|
+
const forKind = (v.for as Record<string, unknown>).kind
|
|
12
|
+
if (forKind !== 'channel' && forKind !== 'manual') return false
|
|
13
|
+
return true
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { createTunnelManager, type TunnelManager, type TunnelManagerOptions, type TunnelManagerLogger } from './manager'
|
|
2
|
+
export { createCloudflareQuickProvider, type CloudflareQuickProviderOptions } from './providers/cloudflare-quick'
|
|
3
|
+
export {
|
|
4
|
+
type TunnelConfig,
|
|
5
|
+
type TunnelFor,
|
|
6
|
+
type TunnelProvider,
|
|
7
|
+
type TunnelProviderHandle,
|
|
8
|
+
type TunnelState,
|
|
9
|
+
type TunnelStatus,
|
|
10
|
+
type TunnelUrlChangedPayload,
|
|
11
|
+
} from './types'
|
|
12
|
+
export { isTunnelUrlChangedPayload } from './events'
|