# NetScript documentation — full corpus > Deno-native, polyglot backend framework, pre-1.0 (0.0.1-beta.11). This file concatenates the Markdown twin of every documentation page for AI ingestion. # One contract. A whole production backend. NetScript is the enterprise-grade meta-framework for Deno: contract-first, end-to-end typed, cloud-agnostic. One unified API from typed services and durable workflows to orchestration, observability, and deploy — one toolchain shared by you and the coding agent you work with. [Quickstart](https://rickylabs.github.io/netscript/netscript/quickstart/) [Browse the reference](https://rickylabs.github.io/netscript/netscript/reference/) [GitHub](https://github.com/rickylabs/netscript) > Beta > > NetScript is in beta ( > > 0.0.1-beta.11 > > ) and moving quickly — pin your versions. ## The contract is the product Define it once; the typed service and every typed client derive from it — server and callers cannot drift apart. The tabs below come straight from a workspace scaffolded by `netscript init my-app --db postgres --service` (abridged). ```ts // contracts/versions/v1/users.contract.ts — scaffolded for you. // One entity schema fans out into a whole CRUD contract. // (@database/zod is emitted by `netscript db generate` in tab 4.) import { createCrudContract } from '@netscript/contracts/crud'; import { UserCreateInput, UserSchema, UserUpdateInput } from '@database/zod'; export const UsersCrudContractV1 = createCrudContract({ resource: 'users', entitySchema: UserSchema, createSchema: UserCreateInput, updateSchema: UserUpdateInput, }); ``` ```ts // Runnable once the database types exist (`netscript db generate`, tab 4). // One preset wires CORS, request logging, OpenAPI JSON, Scalar docs, // RPC (served at /api/rpc/*), service info, and health endpoints — // instead of ~40 lines of Hono setup. import { defineService } from '@netscript/service'; import { router } from './router.ts'; const service = await defineService(router, { name: 'users', port: 3000, }); await service.stop(); ``` ```sh # Install the netscript command from JSR (Deno 2.9+) deno install --global --allow-all --name netscript jsr:@netscript/cli@0.0.1-beta.11 # Scaffold the workspace shown in tabs 1-2: contracts + the typed # users service + Postgres + Aspire orchestration. # Postgres is the recommended engine (mysql, mssql, sqlite also supported). netscript init my-app --db postgres --service # Prefer to keep .NET Aspire out of the loop? Opt out: netscript init my-app --db postgres --service --no-aspire ``` ```sh # Aspire is step 2: it brings up Postgres and the cache first cd my-app/aspire aspire restore # one-time: downloads the AppHost SDK modules aspire start # starts everything and prints the dashboard URL # Open the dashboard; once postgres reports healthy, initialize it # from the workspace root: cd .. netscript db init --name init netscript db generate # emits @database/zod, used by tabs 1-2 netscript db seed ``` ## One contract, four moves **Contract → service → plugins → platform.** You author the contract; `defineService` turns it into a running Hono + oRPC app; plugins add durable capabilities behind the same contract style; `@netscript/aspire` provisions and connects the infrastructure — Aspire SDK behind an adapter, no .NET type in any public signature. Service locations resolve at call time from orchestrator-injected environment variables: the same client code runs against local processes, containers, and deployed endpoints. No registry, no config file. ![A NetScript workspace: contracts define services; services, workers, sagas, triggers, streams, and auth plugins register against a host; the AppHost materializes each as an Aspire resource alongside Postgres, Redis, and the dashboard.](https://rickylabs.github.io/netscript/netscript/assets/diagrams/architecture-overview.svg) *The whole system in one picture: contracts at the center, plugins contributing to a host, and the AppHost bringing every resource up together.* Observability is not an add-on: telemetry stitches scheduler → queue → worker → RPC → SSE spans into one distributed trace using Deno's built-in OTLP exporter — zero OpenTelemetry SDK dependency by default — and the Aspire dashboard shows every resource, trace, and log in one place. ## Batteries no frontend framework ships Durable jobs, compensating sagas, trigger ingress, replayable streams, pluggable auth, an in-process AI surface — first-party plugins, in the box, not an integration project. NetScript does not try to be everything: right tool for the right job, reached through the plugin system. A plugin is a validated manifest that can contribute across every layer — CLI verbs, scaffolded code, runtime services, storage, stream topics, telemetry, Aspire resources — and the host materializes whatever it declares. The mechanism behind the six first-party plugins is how a team extends the framework for its own needs, on the same rails. One command, four plugin files scaffolded and twelve Aspire helpers regenerated: ```sh netscript plugin install worker --name workers # Installed worker plugin "workers" on port 8091. # Created 4 plugin files. # Regenerated 12 Aspire helper files. ``` [workers Durable jobs and workflows; tasks in 4 runtimes (Deno, PowerShell, Python, shell) with at-least-once delivery keyed on idempotencyKey.](https://rickylabs.github.io/netscript/netscript/background-processing/) [sagas Durable multi-step workflows with compensation — saga state persists, so a crash mid-flow resumes instead of stranding half-applied effects.](https://rickylabs.github.io/netscript/netscript/durable-workflows/) [triggers Webhook, scheduled, and file-watch ingress with ack-then-process semantics, idempotency, retry, and dead-lettering.](https://rickylabs.github.io/netscript/netscript/durable-workflows/) [streams Durable, replayable typed topics with no database required — the common pipe the other plugins publish to.](https://rickylabs.github.io/netscript/netscript/durable-workflows/) [auth One auth API with swappable backends (kv-oauth, workos, better-auth), auth schema, and durable session streams.](https://rickylabs.github.io/netscript/netscript/identity-access/) [ai An app-owned, in-process chat/tool/agent surface — the agent loop runs inside your server process, no AI gateway, no extra network hop.](https://rickylabs.github.io/netscript/netscript/ai/) ## Ship anywhere One app model, the whole spectrum: a single compiled binary on one machine, or a multi-cloud distributed infrastructure — and, coming next, a native desktop app on a consumer machine. Cloud-agnostic by design. `netscript deploy ` is one thin router over target adapters sharing a canonical lifecycle (`plan`, `up`, `down`, with `status`/`logs` on the targets that honour them — `netscript deploy list` inventories the installed targets; check `netscript deploy --help` for the exact operations each one ships). Shipped lanes today: Docker and Compose, Linux and Windows OS services from a single compiled binary, Deno Deploy with a preflight guard, Kubernetes and Azure (Container Apps, App Service, AKS) delegating to Aspire, and Cloud Run. Cloud auth stays operator-owned — NetScript mints no credentials and hand-authors no Helm/Bicep/Kubernetes manifests. > Coming next — native desktop lane > > A native desktop deploy target is landing: native installers per OS, Ed25519-signed update manifests, and update-ready events surfaced through an SDK seam. See > > Build a desktop frontend > > . ## Built for devs working with agents Not a framework for agents instead of developers — a framework for developers who work with one. The properties that make NetScript easy for you to adopt, review, and debug — one unified API, typed end to end, reference docs generated from source — are exactly what make it legible to a coding agent. High-risk operations like deploy, init, and db reset stay behind an explicit deny list (deny beats allow; unmatched commands are denied). The CLI is the hands, the skills are the playbook, MCP is the eyes; one command wires all three in: ```sh # Auto-detects the agent host, writes the MCP config (.mcp.json for # Claude Code, .vscode/mcp.json for VS Code), and installs the skill # bundle — all pinned to the installed CLI version. netscript agent init ``` [MCP is the eyes 13 token-bounded tools over stdio: app status, run inspection, recent errors, performance analysis, doctor, and docs search. Every result is capped server-side (50 items, 2,000 characters per string) so telemetry never floods the context window.](https://rickylabs.github.io/netscript/netscript/ai/mcp/) [Skills are the playbook agent init installs three content-hashed skills shipped with the same release as the CLI, so the playbook always matches the binary.](https://rickylabs.github.io/netscript/netscript/reference/ai/skills/) [The CLI is the hands The MCP execute_command tool shells the real CLI through a default-deny policy gate: 17 allowed command prefixes, 6 explicit denies, deny beats allow, anything unmatched is denied.](https://rickylabs.github.io/netscript/netscript/ai/) ## Build real systems, not just CRUD [Web Layer Fresh pages, route contracts, forms, islands, streaming, and UI integration.](https://rickylabs.github.io/netscript/netscript/web-layer/) [Services & SDK oRPC services, shared contracts, OpenAPI, Scalar, and typed clients.](https://rickylabs.github.io/netscript/netscript/services-sdk/) [Background Processing Workers, queues, cron, polyglot runtimes, and task permissions.](https://rickylabs.github.io/netscript/netscript/background-processing/) [Durable Workflows Sagas, triggers, streams, retries, and stateful multi-step work.](https://rickylabs.github.io/netscript/netscript/durable-workflows/) [Data & Persistence Database setup, migrations, multiple datasources, KV, and adapters.](https://rickylabs.github.io/netscript/netscript/data-persistence/) [Identity & Access Principals, sessions, auth backends, roles, scopes, and claims.](https://rickylabs.github.io/netscript/netscript/identity-access/) [Orchestration & Runtime Aspire AppHost, plugin resources, runtime config, deployment, and lifecycle.](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/) [Observability OpenTelemetry trace context, structured logs, and the Aspire dashboard.](https://rickylabs.github.io/netscript/netscript/observability/) [AI & Agents MCP server, agent skills, the in-process AI engine, and durable chat.](https://rickylabs.github.io/netscript/netscript/ai/) ## Who it is for Full-stack TypeScript engineers Stay in TypeScript from the database to the UI, with contracts that catch integration breaks before they ship. Platform engineers Standardize internal services on one coherent Deno baseline — multi-resource wiring, a real dashboard, pluggable auth, and tracing already in place. Plugin authors Extend the framework by registering workers, sagas, triggers, auth backends, and streams. No host edits, no forks. Devs working with agents netscript agent init gives your agent MCP eyes, a skills playbook, and a policy-gated CLI — high-risk operations like deploy, init, and db reset stay behind an explicit deny list. ## Start the learning path 1. [Quickstart](https://rickylabs.github.io/netscript/netscript/quickstart/) 2. [Architecture overview](https://rickylabs.github.io/netscript/netscript/concepts/) 3. [Glossary](https://rickylabs.github.io/netscript/netscript/glossary/) 4. [Choose a pillar](https://rickylabs.github.io/netscript/netscript/web-layer/) [API reference Generated from source via deno doc — every package, every public symbol.](https://rickylabs.github.io/netscript/netscript/reference/) [Source on GitHub Read the framework, file issues, follow the road to stable.](https://github.com/rickylabs/netscript) [Packages on JSR The @netscript/* family, installable with deno add.](https://jsr.io/@netscript) _Canonical: https://rickylabs.github.io/netscript/_ --- # AI The NetScript AI stack is a set of composable seams for building agentic chat and tool-calling surfaces on the same contracts-first, hexagonal foundation as the rest of the framework. It is deliberately layered: a provider-agnostic **engine** at the design center, an **MCP client stack** that turns external MCP servers into agent tools, a **durable-chat runtime** that turns a Fresh route into a chat whose history survives reload and reconnect, and an app-owned **chat UI** you copy into your workspace and keep. Each layer stands on its own and each is published — you can adopt one without pulling in the others. That layering is the build-efficiency argument: whether the next slice of your app is written by you or generated by an agent, each capability is one published seam away — install it, wire the port, and the vocabulary already matches the layer above — instead of a bespoke integration to design before the feature work starts. > What ships in 0.0.1-beta.11 > > All four layers are published on JSR and installable now: the > > engine > > ( > > @netscript/ai > > — > > deno add jsr:@netscript/ai > > ), the > > MCP client stack > > (its > > ./mcp > > subpath), the > > durable-chat runtime > > ( > > @netscript/fresh/ai > > + > > @netscript/fresh/ai/sandbox > > ), and the > > chat UI > > (the > > @netscript/fresh-ui > > ai > > copy-registry collection, installed with > > netscript ui:add ai > > ). The AI plugin pair ( > > @netscript/plugin-ai > > , > > @netscript/plugin-ai-core > > ) is published at the same version. ## The story: a chat that stops being a demo The first version of an AI feature is usually a chat box holding its transcript in component state — and it works until it meets reality. A reload mid-answer loses the in-flight tool call. The answer needs a chart, and now model output is being glued into the DOM by hand. The agent needs a capability that lives behind an external MCP server, and suddenly you are writing a protocol client. Each failure lands on a different layer of this stack, and each layer answers exactly one of them: - **history that survives** — the [durable-chat runtime](https://rickylabs.github.io/netscript/ai/durable-chat/) backs the session with a durable stream and one projection reducer, so reload, reconnect, and multi-tab replay all converge on the same transcript; - **model-authored UI without raw HTML** — the [chat UI](https://rickylabs.github.io/netscript/ai/chat-ui/) renders `render_ui` payloads through a curated block vocabulary with depth limits and safe fallbacks; - **external capabilities as ordinary tools** — the [MCP client stack](https://rickylabs.github.io/netscript/ai/mcp/) pools servers, injects auth, and registers remote tools into the same registry the agent loop dispatches; - **one vocabulary underneath** — the [engine](https://rickylabs.github.io/netscript/ai/engine/) owns the contracts all of the above speak, so the layers compose instead of adapting to each other. ## The two planes — never build chat on the wrong primitive NetScript models real-time state in two distinct planes, and the single most common mistake is reaching for the wrong one. A **StreamDB shape** and a **durable chat session** look adjacent — both are live and durable — but they answer different questions. **Two planes — pick by what you are modeling** | Axis | StreamDB shapes (data) | Durable chat sessions (AI chat) | | --- | --- | --- | | Unit | A collection / row shape | One chat session (append-only chunk log) | | Identity | Keyed by row id inside a named shape | Keyed by `sessionId` — one stream per chat | | Write model | CRUD mutations reconciled into the shape | Append-only sanitized chunks | | What survives | The current materialized rows | The full replayable log (messages + tool cards) | | Read primitive | `useLiveQuery` over a shape | `resolveChatSnapshot` + live `useChat` | | Use it for | Lists, boards, tables, dashboards | Conversational, streaming, tool-calling chat | A chat needs the **session plane** because a tool call is a multi-chunk, mid-stream event: it moves through `pending → streaming → complete` and cannot be expressed as a single reconciled row without losing those intermediate states. Reach for [durable streams](https://rickylabs.github.io/netscript/durable-workflows/streams/) when you want live list/board/table data; reach for [durable chat](https://rickylabs.github.io/netscript/ai/durable-chat/) when you want a replayable conversation. Conflating them is the documented root of the plane confusion — keep them distinct. ## The engine is the design center `@netscript/ai` is a **ports-and-adapters** (hexagonal) core with zero `@netscript/*` dependencies. Every capability — telemetry, tool registry, embeddings, vision, MCP transport, the agent loop, memory — is a **port**: an interface the composition root wires at startup, defaulting to a no-op or throwing implementation until you inject a real one. Providers register themselves through **side-effect imports** (`import "@netscript/ai/anthropic"`, mirroring `@netscript/kv/redis`), so the base engine pulls no vendor SDK you did not ask for. Models are addressed by a `"provider:model"` reference string, e.g. `"anthropic:claude-sonnet-4-5"`. Because the engine owns the vocabulary — the `Message`/`ContentPart` content model, the `AgentChunk` streaming union, the tool and MCP contracts — every layer above it speaks the same types. And because telemetry is a port, an agent run is observable the moment you inject a real `TelemetryPort`: the loop opens a `gen_ai.chat` span per run and a `gen_ai.chat.turn` span per model turn, and records tool calls as `gen_ai.tool.call` events. The [engine reference](https://rickylabs.github.io/netscript/ai/engine/) is the full map. ## Thin plugins, centralized convention The AI capability follows the framework's plugin doctrine (Architecture Doctrine ch. 11): **R-PLUGIN-THIN** — every convention-bearing primitive lives in a core `@netscript/*` package, and a plugin carries only its own specifics — and **R-PLUGIN-SEAM** — a plugin's contract lives in its `-core` sibling and conforms to the base contract in `@netscript/plugin`. In practice the engine (`@netscript/ai`) owns the vocabulary, `@netscript/plugin-ai-core` owns the `/v1/ai` oRPC contract, and `@netscript/plugin-ai` stays a thin delivery shell — all three published at `0.0.1-beta.11`. This split is what keeps the published surface coherent: the runtime layers (`@netscript/fresh/ai`, the fresh-ui copy registry) are self-contained, and adopting the engine later changes nothing about them. > Not in this release: netscript generate ai > > A > > netscript generate ai > > codegen (app-owned tool and agent files compiled into engine-ready registries) is planned but > > not in the CLI today > > — the shipped > > generate > > targets are > > plugins > > , > > runtime-schemas > > , and > > aspire > > . Wire tools and agent loops through the engine's registries directly. ## One comparison Medusa's hero names "Agents and Developers" as co-audiences and its agent story is a progressive narrative — agents build a store feature, then optimize it, then operate the running store. Convex ships "backend building blocks for your agents" as components on its reactive backend. NetScript's AI stack occupies the same layered-investment shape with a different center of gravity: the layers here are the **published framework surface itself** — a JSR engine package, subpath exports, and a copy-registry you own — wired to the same contracts-first backend the rest of your app is built on, rather than a product layer alongside it. The trade is explicit: NetScript does not ship an agents-operate-your-app product tier today; it ships the seams those products are made of. ## Where to go next [Guide MCP The MCP client stack: transports, injected auth, multi-server pooling, and safe rendering of ui:// resources from tool results.](https://rickylabs.github.io/netscript/netscript/ai/mcp/) [Guide Durable chat Turn a Fresh route into a durable AI chat: history and in-flight tool calls survive reload, reconnect, and multi-tab.](https://rickylabs.github.io/netscript/netscript/ai/durable-chat/) [Guide Chat UI The fresh-ui `ai` copy-registry: composer, message thread, tool-call cards, and the generative-UI block renderer — copied into your app and owned by you.](https://rickylabs.github.io/netscript/netscript/ai/chat-ui/) [Reference AI engine @netscript/ai: the provider-agnostic runtime, contracts vocabulary, ports, tools, agent loop, MCP transports, and provider adapters.](https://rickylabs.github.io/netscript/netscript/ai/engine/) [Related Durable streams The other real-time plane — live list/board/table data over the durable-stream server.](https://rickylabs.github.io/netscript/netscript/durable-workflows/streams/) [Related Web Layer The Fresh page and island model the durable-chat runtime plugs into.](https://rickylabs.github.io/netscript/netscript/web-layer/) [API Reference @netscript/ai Generated symbols for the AI engine package.](https://rickylabs.github.io/netscript/netscript/reference/ai/) [API Reference @netscript/plugin-ai Generated symbols for the thin AI plugin delivery shell.](https://rickylabs.github.io/netscript/netscript/reference/plugin-ai/) [API Reference @netscript/plugin-ai-core Generated symbols for the AI plugin's reusable contract and composition core.](https://rickylabs.github.io/netscript/netscript/reference/plugin-ai-core/) ## Learn, do, look up [Learn AI chat tutorial A durable, tool-calling chat with MCP and live streaming.](https://rickylabs.github.io/netscript/netscript/tutorials/chat/) [Do Recipes Task-oriented recipes for this area, one problem each.](https://rickylabs.github.io/netscript/netscript/ai/how-to/) [Look up `@netscript/ai` reference Generated API reference. Related units: `plugin-ai`, `mcp`.](https://rickylabs.github.io/netscript/netscript/reference/ai/) [Understand The plugin system The design rationale behind this pillar.](https://rickylabs.github.io/netscript/netscript/explanation/plugin-system/) _Canonical: https://rickylabs.github.io/netscript/ai/_ --- # Agent tooling The rest of this pillar is about the AI you build *into* your app — the [engine](https://rickylabs.github.io/netscript/ai/engine/), the [MCP client stack](https://rickylabs.github.io/netscript/ai/mcp/), [durable chat](https://rickylabs.github.io/netscript/ai/durable-chat/). This page is the mirror image: what NetScript gives the coding agents (and the humans pairing with them) that build your app. One vocabulary across three surfaces: - the `netscript` CLI performs direct, scriptable operations; - installed skills explain which NetScript workflow to use and when to hand off to the CLI; - the MCP server returns compact framework-aware diagnostics, telemetry summaries, and public docs. There is a fourth surface that is easy to miss: this documentation site is itself built to be read by agents — every page has a Markdown twin, and the whole corpus is published behind `llms.txt`. We cover that [below](#reading-these-docs-as-an-agent). Prefer the CLI when a command already expresses the operation — it is easier to reproduce in a terminal or CI job, and the [CLI reference](https://rickylabs.github.io/netscript/netscript/cli-reference/) is the task-oriented map of what exists. Use MCP for interactive investigation, especially when one framework-aware tool replaces several telemetry queries or when an agent needs to discover a document before reading it. > Two different MCP surfaces — don't conflate them > > This page documents the NetScript MCP > > server > > : a standalone stdio server that gives a coding agent diagnostics, telemetry summaries, and doc search > > about > > the project it is editing. That is a separate surface from the > > `@netscript/ai/mcp` client stack > > , which wires > > external > > MCP servers > > into > > your product's own agent loop. Server in, client out. The server's full per-tool contracts live in the > > `@netscript/mcp` reference > > . ## Install into a project ```bash netscript agent init ``` Host detection installs the matching files. If neither host directory exists, NetScript prepares both hosts. Use `--host claude`, `--host vscode`, or `--host all` to select explicitly. | Host | Files written | | --- | --- | | Claude Code | `.mcp.json`, NetScript skills under `.claude/skills/`, and a marked NetScript section in `AGENTS.md` | | VS Code | `.vscode/mcp.json` | The generated MCP configuration runs `netscript agent mcp` for the current project. Re-running `agent init` is idempotent: unchanged files are left alone, and existing host configuration is preserved alongside the `netscript` server entry. The host command includes the absolute project `deno.json` path. Deno 2.9 normally holds newly published registry versions behind a 24-hour minimum dependency age; the generated JSR workspace keeps that policy while excluding only exact-version packages in the matching NetScript release train. Loading the project configuration explicitly lets a newly released `@netscript/cli` MCP server start immediately without changing the age policy for third-party dependencies. ## Run the server The generated host configuration runs `netscript agent mcp`, which starts the MCP server over standard input/output. Its flags: | Flag | Purpose | | --- | --- | | `--project-root ` | NetScript project root used for execution and doctor flows. `agent init` writes this for you. | | `--endpoint ` | Telemetry endpoint URL; overrides discovery (below). Only `http:` and `https:` are accepted. | | `--docs-root ` | Public documentation root for the docs tools; overrides the default corpus (below). | ## What the server exposes Thirteen tools, every one returning a bounded structured result. Grouped by what an agent is trying to do: - **Read the running app** — seven telemetry read models: `get_app_status`, `list_runs`, `get_run`, `get_recent_errors`, `get_last_job_result`, `analyze_service_performance`, and `analyze_db_bottlenecks`. Each replaces a handful of raw telemetry queries with one aggregate answer. - **Diagnose the project** — `doctor` aggregates telemetry reachability, Aspire markers, project wiring, and plugin diagnostics into one verdict; the plugin slice has a direct twin in `netscript plugin doctor`. - **Search the docs** — `search_docs`, `list_docs`, and `get_doc` form a search-to-get funnel over the documentation corpus (next section). - **Bridge to the CLI** — `list_commands` discovers the current command tree (the machine-readable twin of `netscript --help`), and `execute_command` runs an allowlisted `netscript …` command with policy checking, timeout handling, and a bounded output tail. We keep the per-tool schemas, output bounds, and the full `execute_command` policy in the [`@netscript/mcp` reference](https://rickylabs.github.io/netscript/netscript/reference/mcp/) rather than restating them here; for the commands themselves, the [CLI reference](https://rickylabs.github.io/netscript/netscript/cli-reference/) is the cheat-sheet. ## Documentation corpus Without an override, `list_docs`, `search_docs`, and `get_doc` index the documentation shipped with the installed `@netscript/mcp` package (one document under the `mcp` slug). Point the server at a richer corpus — a project docs folder or a checkout of this site — with `--docs-root ` or the `NETSCRIPT_DOCS_ROOT` environment variable. A configured path that does not exist returns a structured `docs_corpus_not_found` error naming the missing path; the server never silently reports an empty corpus. ## Token-efficient use Tool inputs cap result counts, and the server truncates oversized results and command output. Start with the narrowest filter that answers the question. For documentation, use the search-to-get funnel: call `search_docs`, choose a slug, then call `get_doc` for that document or section. ## Data boundary The MCP server reads NetScript telemetry, project metadata and generated registries used for diagnostics, and public documentation. It does not return project source code, environment-variable values, credentials, or secrets. Telemetry requests go only to the resolved dashboard endpoint. Discovery uses, in order, the `--endpoint` option, `NETSCRIPT_TELEMETRY_ENDPOINT`, `ASPIRE_DASHBOARD_PORT`, and the local default `http://localhost:18888`. `execute_command` is default-deny. Explicit rules allow selected database, generation, contract, service-read, plugin, and UI commands. Deny rules take priority; deployment, project initialization, marketplace operations, database reset, plugin removal, and every unmatched command are rejected with a structured denial before a process is started. The rule-by-rule policy lives in the [`@netscript/mcp` reference](https://rickylabs.github.io/netscript/netscript/reference/mcp/). ## Troubleshooting Start with the `doctor` tool: it aggregates four check families — `telemetry`, `aspire`, `project`, and `plugins` — into one verdict. Each check carries a `pass`, `warn`, or `fail` status, and warnings and failures may include a suggested fix. Reading the result: - **`telemetry` warns or fails** — no reachable telemetry endpoint. Verify the app is running, then check the discovery chain: `--endpoint`, `NETSCRIPT_TELEMETRY_ENDPOINT`, `ASPIRE_DASHBOARD_PORT`, and the local default `http://localhost:18888`. Telemetry tools still respond while the endpoint is down — nothing crashes: `get_app_status` reports `status: "warn"` with zero counts, the list and analytics tools (`list_runs`, `get_recent_errors`, `analyze_service_performance`, `analyze_db_bottlenecks`, …) return their ordinary empty or zero-valued results, and `get_run` returns a structured `run_not_found` error. - **`aspire` or `project` warns** — the project root is wrong or the workspace is missing expected markers. Confirm the `--project-root` written into your host configuration points at the project (re-run `netscript agent init` after moving a project). - **`plugins` warns or fails** — plugin diagnostics found an issue; the equivalent direct command is `netscript plugin doctor`. - **Docs tools return `docs_corpus_not_found`** — the configured `--docs-root` / `NETSCRIPT_DOCS_ROOT` path does not exist; fix the path or remove the override to fall back to the packaged corpus. - **`execute_command` returns a denial** — the command did not match the allowlist (see the data boundary above); run it directly in a terminal instead. ## Reading these docs as an agent The site you are reading ships its own agent affordances, no MCP server required: - **Every page has a Markdown twin.** The "View as Markdown" link near the top of each page points at clean Markdown distilled from the rendered page — component markup already resolved, links absolute — so an agent reads source-quality text instead of parsing HTML chrome. The twin lives at the page URL plus `index.md`: this page's twin is [`/ai/agent-tooling/index.md`](https://rickylabs.github.io/netscript/ai/agent-tooling/index.md), and the same suffix works on any page. - **A tiered `llms.txt`.** [`/llms.txt`](https://rickylabs.github.io/netscript/llms.txt) is the index tier: every real page as an absolute canonical link with a one-line summary, grouped by section, plus a short note telling agents how to reach the twins. [`/llms-full.txt`](https://rickylabs.github.io/netscript/llms-full.txt) is the full tier: every page's Markdown twin concatenated into one corpus for bulk ingestion. The ladder, cheapest first: start at `llms.txt`, fetch the twin of the one page you need, and reach for `llms-full.txt` only when you genuinely want everything. And the two halves meet: point the MCP server's `--docs-root` at a checkout of this site and `search_docs` runs over the same corpus. ## Run the protocol smoke ```bash deno test --allow-all packages/cli/e2e/tests/agent/agent-mcp-stdio_test.ts ``` The smoke starts the public CLI binary, initializes MCP over stdio, verifies the 13-tool catalog, and checks docs, diagnostics, unreachable telemetry, and command denial behavior. ## Where to go next [Reference @netscript/mcp Per-tool contracts, output bounds, and the execute_command policy of the agent-facing MCP server.](https://rickylabs.github.io/netscript/netscript/reference/mcp/) [Reference CLI reference The task-oriented cheat-sheet for every netscript command, including agent init and agent mcp.](https://rickylabs.github.io/netscript/netscript/cli-reference/) [Guide MCP client stack The other MCP surface: consume external MCP servers as tools inside your own product's agents.](https://rickylabs.github.io/netscript/netscript/ai/mcp/) _Canonical: https://rickylabs.github.io/netscript/ai/agent-tooling/_ --- # Chat UI The AI chat interface ships as a **copy-registry collection** in `@netscript/fresh-ui`: a composer, a message thread, tool-call cards, a model picker, and a generative-UI block renderer. alpha These are **not** package subpath imports. The published `@netscript/fresh-ui` subpath exports (`.`, `./primitives`, `./interactive`, `./registry`) carry no chat symbols. The chat surface is a set of files the NetScript CLI **copies into your app**, where you own and edit them like any other source file. This is the same copy-source model as the rest of the Fresh UI — see [Fresh UI & design](https://rickylabs.github.io/netscript/capabilities/fresh-ui/) and [Customize Fresh UI](https://rickylabs.github.io/netscript/web-layer/how-to/customize-fresh-ui/). > Copy-registry, not a dependency > > The chat components are delivered through the > > ai > > collection in the fresh-ui copy registry. You do > > not > > import > > them from a > > @netscript/fresh-ui/chat > > subpath — that subpath does not exist. The CLI copies each item into your workspace, and from that point the code is > > yours > > : edit it, restyle it, delete what you do not need. Updates are a re-copy, not a version bump. ## The `ai` collection The collection is described as the "AI / chat surface seams: grounded-agent citations, message thread, composer, model picker, and tool-call disclosure." Its items: **The ai copy-registry collection** | Item | Role | | --- | --- | | `prompt-input` | Chat composer: auto-grow textarea plus a toolbar (research / grounding pills, model picker, attach / screenshot / voice, send). | | `message` | A chat message: author + time, inline-markup body (bold / code / `[n]` citations), tool-call and chart / code blocks, follow-up chips, typing indicator. Exports `renderInline` and `TypingIndicator`. | | `markdown` | Sanitized streaming-markdown component plus its rendering pipeline. | | `chat-render` | The generative-UI block parser (a `lib` item): copies `parse-blocks.ts` into your app. | | `tool-call-card` | Tool-call disclosure card for an agent tool invocation. | | `model-selector` | Model picker used by the composer. | | `citation-chip` | Grounded-agent citation chip. | | `code-block` | Rendered code block. | | `chart-block` | Rendered chart block (a target of the generative-UI renderer). | | `avatar` | Author avatar. | | `command-palette` | Command palette surface. | | `search` | Search surface. | | `theme-seed` | Theme token seed for the chat surface. | ## The generative-UI renderer — `chat-render` / `parse-blocks` The `chat-render` item copies `parse-blocks.ts` into your app (`@lib/chat/parse-blocks.ts`). It is the parser that turns streamed markdown into typed, renderable blocks — the seam that lets an agent emit a chart or a table inside its answer. - **`parseBlocks(input: string): RenderPart[]`** — markdown → typed parts. It **never throws**; an unrecognized fenced block falls through to a verbatim `text` part. - **`blockToText(part: RenderPart): string`** — the exact inverse projection (canonical markdown / plain-text fallback). `parseBlocks` is a boundary-stable fixed point, which is what gives the transcript reload fidelity. ### The presentation RenderPart The renderer's `RenderPart` is a **rich presentation** union — distinct from the **transport** `RenderPart` in [`@netscript/fresh/ai`](https://rickylabs.github.io/netscript/ai/durable-chat/) (which is only `text | tool`). Keep the two separate: one describes how a chunk streams, the other describes how a block is drawn. **Presentation RenderPart (chat-render) — one variant per rendered block** | Name | Type | Description | | --- | --- | --- | | `chart` | `ChartRenderPart` | A chart block (payload `ChartDatum[]`). | | `donut` | `DonutRenderPart` | A donut block (payload `DonutDatum[]`). | | `table` | `TableRenderPart` | A table block (`TableColumn` / `TableRow`, with `TableAlign`). | | `stats` | `StatsRenderPart` | A stats grid (payload `StatsEntry[]`). | | `line` | `LineRenderPart` | A line chart (payload `LinePoint[]`). | | `text` | `TextRenderPart` | Verbatim text — the fallback for any unrecognized fence. | ### The fenced-block grammar `parseBlocks` reads a curated set of fenced-code info-strings — `chart`, `donut`, `table`, `stats`, `line` — and accepts either canonical JSON or a minimal DSL (`label: value @tone`, plus markdown pipe tables). A `RenderTone` colors a value; a fence whose info-string is not in the set is left as a verbatim `text` part, so ordinary code blocks pass through untouched. ```text ```chart [{ "label": "Q1", "value": 42 }, { "label": "Q2", "value": 58 }] ``` ```stats Revenue: 1.2M @positive Churn: 3% @negative ``` ``` `parse-blocks.ts` is self-contained — it does not import `@netscript/fresh/ai`. The two `RenderPart` types are intentionally separate (presentation vs transport); reconciling them is a downstream concern, not something you wire by hand. ## Streaming markdown and the composer Two components carry most of the interactive weight: - **`markdown`** renders **sanitized streaming markdown** — safe to feed partial, mid-stream assistant text as chunks arrive, without waiting for a complete message. - **`prompt-input`** is the composer: an **auto-grow** textarea that expands with the input, plus a toolbar for research/grounding pills, the model picker, attachment / screenshot / voice affordances, and send. Because every item is copied into your workspace, restyling the composer, swapping the chart renderer, or removing the citation chip is a normal source edit — there is no package boundary to work around. ## Reference [Look up — @netscript/fresh-ui The generated reference for the fresh-ui package, including the copy-registry manifest surface.](https://rickylabs.github.io/netscript/netscript/reference/fresh-ui/) [Do — customize Fresh UI How the CLI copies components into your app and how you own them afterward.](https://rickylabs.github.io/netscript/netscript/web-layer/how-to/customize-fresh-ui/) [Back — durable chat The runtime that produces the transcript these components render, and the transport RenderPart they widen.](https://rickylabs.github.io/netscript/netscript/ai/durable-chat/) _Canonical: https://rickylabs.github.io/netscript/ai/chat-ui/_ --- # Durable chat `@netscript/fresh/ai` is the server + island seam that turns a Fresh route into a **durable AI chat**: a chat whose message history and in-flight tool calls survive reload, reconnect, and multi-tab replay because they are backed by a durable session stream rather than component state. alpha It composes three upstream libraries — `@durable-streams/tanstack-ai-transport`, `@tanstack/ai-preact`, and `@tanstack/ai` — and adds only NetScript glue: durable-stream URL resolution, server-side auth headers, and the projection law below. It does **not** import `@netscript/ai`, so it composes with the [AI engine](https://rickylabs.github.io/netscript/ai/engine/) but installs independently — you can adopt durable chat without pulling in the engine. This subpath is published on JSR as part of `@netscript/fresh@0.0.1-beta.11` and is usable now. ## The primitives at a glance **@netscript/fresh/ai — the durable-chat surface** | Name | Type | Description | | --- | --- | --- | | `createNetScriptChatStreamProxy` | `(options) => handler` | Build the single durable chat-stream proxy handler — resolves the session target (static or per-request), proxies to the durable-stream URL with server auth, passes the body through unbuffered, and tears down on client abort. | | `toNetScriptChatResponse` | `(options) => Promise` | Produce a durable-session `Response` from a server chat stream; enforces `authorize` (a denial becomes `403`) before the session stream is touched. | | `resolveChatSnapshot` | `(options) => Promise` | Resolve the seed snapshot for SSR / first paint by materializing the session and reducing it through `projectChatSnapshot`. | | `projectChatSnapshot` | `(messages) => {messages, renderParts}` | THE single projection reducer — deterministic and side-effect-free. Both seed and live paths MUST route through it (the one-projection law). | | `createNetScriptChatConnection` | `(options) => NetScriptChatConnection` | Open a live durable session handle: SR2-tolerant `subscribe`, a `send` that persists client messages, and one idempotent teardown (`close`/`stop`/`dispose`). | ## The one-projection law `resolveChatSnapshot` (the SSR/first-paint seed) and the live island projection MUST run **the same reducer** — `projectChatSnapshot`. They are two entry points into one function applied to one chunk log: ```text chunks --> [ projectChatSnapshot ] --> { messages, renderParts } ``` If the seed path and the live path diverge — for example the server hand-rolls a snapshot while the island reduces chunks differently — **tool cards drift**: a card materialized at seed time renders differently, or vanishes, once the first live chunk arrives, because the two projections disagree about intermediate tool state. This is a hard invariant, not a nicety. Any downstream slice that adds a projection must route both seed and live through it. > Route seed and live through one reducer > > Never build a bespoke snapshot for SSR and a separate live reducer for the island. Seed the first paint with > > resolveChatSnapshot > > (which calls > > projectChatSnapshot > > internally) and hand its > > offset > > to the live subscription so seed and live read one continuous chunk log. The returned > > renderParts > > are the transport render parts described below. ## The canonical chat-stream proxy Every durable chat needs one route that proxies the browser's chat-stream request to the session's durable-stream URL. `createNetScriptChatStreamProxy` is that route — it resolves the session target (static or per-request), attaches server-side streams auth (via `getStreamsAuth` by default), passes the upstream body through **unbuffered**, strips headers that would misdescribe the re-framed bytes, and propagates the client `AbortSignal` so a disconnect tears the upstream fetch down. Use it directly as a Fresh `Handlers` entry. ```ts // routes/api/chat/[sessionId].ts — the one canonical chat-stream proxy import { createNetScriptChatStreamProxy } from "@netscript/fresh/ai"; const proxy = createNetScriptChatStreamProxy({ // Resolve the session per request from the route param. target: (req) => ({ sessionId: new URL(req.url).pathname.split("/").pop()!, }), }); export const handler = { POST: proxy, GET: proxy }; ``` > Why a raw Request here, not a route contract > > This resolver is the one documented exception to NetScript's typed-route rule: > > createNetScriptChatStreamProxy > > 's > > target > > only ever receives the raw > > Request > > , so the session id is parsed from > > req.url > > by hand. Everywhere else you build a URL or read a path param, prefer a bound > > `createRouteReference` > > contract so the pattern and its typed params come from one source of truth. `NetScriptChatStreamProxyOptions` is small: a `target` (a `NetScriptChatSessionTarget` or a `(request) => NetScriptChatSessionTarget` resolver), an optional `auth` header provider (defaults to `getStreamsAuth` from `@netscript/plugin-streams-core`), and an optional `fetch` override for tests. The returned handler accepts a bare `Request` or a Fresh route context (anything with `.req`), so `{ POST: handler }` and a direct call in a test both work. ## Authorizing the session response The proxy moves bytes; **authorization happens where you produce the session response**. `toNetScriptChatResponse` sanitizes a server-side chat stream into durable chunks and returns the session `Response`. Supply an `authorize` hook and the matching `request`: a denial yields `403 Forbidden` and the session stream is never touched. ```ts // The server turn: persist the assistant stream into the durable session, gated by authorize. import { toNetScriptChatResponse } from "@netscript/fresh/ai"; const response = await toNetScriptChatResponse({ target: { sessionId }, request, // REQUIRED in production — return false to deny (=> 403). authorize: (req, id) => sessionBelongsToUser(req, id), newMessages, // client messages to persist before the assistant turn source: assistantChatStream, // AsyncIterable of server chat chunks }); ``` > authorize is required in production — there is no default allow-all > > NetScriptChatAuthorize = (request, sessionId) => boolean | Promise > > is optional at the type level (the framework cannot prove a caller is production), but the factory > > never > > bakes in a default allow-all. Ship a real > > authorize > > before exposing a chat route publicly — without one, > > toNetScriptChatResponse > > cannot gate access to the session stream. Supplying > > authorize > > without a > > request > > is a programming error and throws. ## Seeding first paint (SSR) For SSR and first paint, materialize the session and reduce it through the one projection reducer with `resolveChatSnapshot`, then hand the returned `offset` to the live subscription so seed and live share one continuous log. ```ts import { resolveChatSnapshot } from "@netscript/fresh/ai"; const snapshot = await resolveChatSnapshot({ target: { sessionId } }); // snapshot.messages -> ordered NetScriptChatMessage[] // snapshot.renderParts -> transport RenderPart[] (text + tool cards) // snapshot.offset -> replay cursor for the live subscription (or null if empty) ``` ## The live connection `createNetScriptChatConnection` opens the live session handle the island subscribes to. Its `subscribe` is SR2-tolerant (a first-subscribe that races a not-yet-created stream re-polls with backoff instead of throwing), `send` persists client messages, and `close`/`stop`/`dispose` are one idempotent teardown so no connection leaks. ```ts import { createNetScriptChatConnection } from "@netscript/fresh/ai"; const chat = createNetScriptChatConnection({ target: { sessionId }, authorize: (req, id) => sessionBelongsToUser(req, id), // REQUIRED in prod initialOffset: snapshot.offset ?? undefined, // continue from the seed cursor }); try { for await (const chunk of chat.subscribe(signal)) { render(chunk); } } finally { chat.dispose(); } ``` ## The transport RenderPart The reducer emits a **minimal, transport-level** `RenderPart` — either reduced message `text` or a `tool` card reduced from the same chunk log. This is distinct from the **rich presentation** `RenderPart` owned by the [chat UI](https://rickylabs.github.io/netscript/ai/chat-ui/); the two are intentionally separate and must not be conflated. The transport part is the stable contract the UI widens. **RenderPart (transport) — @netscript/fresh/ai** | Name | Type | Description | | --- | --- | --- | | `kind` | `"text" \| "tool"` | Whether this part renders message text or a tool card. | | `id` | `string` | Stable id of this part within the transcript. | | `role` | `"system" \| "user" \| "assistant" \| "tool"` | Author role the part belongs to. | | `text` | `string?` | Reduced text (present when `kind === 'text'`). | | `toolName` | `string?` | Invoked tool name (present when `kind === 'tool'`). | | `toolState` | `"pending" \| "streaming" \| "complete" \| "error"` | Lifecycle state of the tool card reduced from the chunk log. | | `input` | `unknown?` | Reduced tool input (may be partial while `streaming`). | | `output` | `unknown?` | Reduced tool output, present once `complete`. | The message and session shapes are equally small: `NetScriptChatMessage` (`{ id, role, content }`), `NetScriptChatSessionTarget` (`{ sessionId, baseUrl?, headers? }` — one durable stream per `sessionId`), and `NetScriptChatSnapshot` (`{ messages, renderParts, offset }`). ## The `ui://` sandbox — `@netscript/fresh/ai/sandbox` The sandbox subpath serves themed `ui://` resources for MCP-driven UI. Its shipped piece is `createMcpSandboxHandler`: a Fresh route handler that serves a registered `ui://` resource selected by `?uri=`, injects the active theme's `--ns-*` custom properties, stamps `data-theme`, and applies a per-response CSP derived from the resource URI. `?theme=` selects a token set; an absent or unknown theme falls back to `defaultThemeName` (default `"default"`). ```ts // routes/mcp/sandbox.ts import { createMcpSandboxHandler } from "@netscript/fresh/ai/sandbox"; export const handler = { GET: createMcpSandboxHandler({ resolveResource: (uri, { signal }) => registry.lookup(uri, { signal }), themes: { default: { "--ns-color-surface": "#ffffff" }, dark: { "--ns-color-surface": "#111111" }, }, }), }; ``` > The broader MCP tool sandbox is not yet wired > > createNetScriptMcpSandbox > > (the chat-activity MCP tool sandbox that would merge agent tools and bridge the island > > useChat > > surface) is an FA3 > > skeleton stub > > — it is not yet wired to a working chat activity. Only the > > ui:// > > resource route handler ( > > createMcpSandboxHandler > > ) is real today. Do not build on > > createNetScriptMcpSandbox > > yet. ## Reference This page orients; the generated reference enumerates every exported symbol. [Look up — @netscript/fresh The generated API for the Fresh package, including the /ai and /ai/sandbox subpaths.](https://rickylabs.github.io/netscript/netscript/reference/fresh/) [Next — the chat UI The fresh-ui copy-registry components that render this transcript: composer, message thread, tool cards, and the generative-UI block renderer.](https://rickylabs.github.io/netscript/netscript/ai/chat-ui/) [Understand — the two planes Why chat lives on the durable-session plane and list/board data lives on the StreamDB plane.](https://rickylabs.github.io/netscript/netscript/ai/) _Canonical: https://rickylabs.github.io/netscript/ai/durable-chat/_ --- # AI engine This is the **engine guide** — how the pieces of `@netscript/ai` fit together and when to reach for each one. `@netscript/ai` is the provider-agnostic AI engine at the design center of the stack: a zero-`@netscript/*`-dependency, ports-and-adapters core that owns the domain vocabulary, the capability seams, the model registries, the tool system, the agent loop, MCP transports, and opt-in provider adapters. It wraps `@tanstack/ai*` and `@standard-schema/spec` and adds no schema DSL of its own. We keep this page at the "which piece, and why" altitude; the exact symbol tables live in the generated reference, linked at the end. > Published in 0.0.1-beta.11 > > @netscript/ai > > is > > published on JSR > > and installs today: > > deno add jsr:@netscript/ai > > . Every subpath on this page resolves against the published > > 0.0.1-beta.11 > > surface. The engine carries zero > > @netscript/* > > dependencies, so you can adopt it on its own — the > > durable-chat runtime > > and > > chat UI > > each stand alone and neither requires it. ## The map: which subpath answers which question **@netscript/ai — subpath exports** | Subpath | Purpose | | --- | --- | | `.` | Runtime wiring + the model / embedding / vision registries. | | `./contracts` | Domain vocabulary — pure types and the error hierarchy. | | `./ports` | Capability seams (hexagonal) plus default port / registry factories. | | `./tools` | Tool definition / validation / registry, plus the built-in `render_ui` contract. | | `./agent` | The agent loop. | | `./mcp` | MCP transport adapters (stdio + Streamable-HTTP). | | `./anthropic` | Anthropic provider (side-effect self-register). | | `./openai-compatible` | OpenAI-compatible chat provider. | | `./openrouter` | OpenRouter chat provider (OpenAI-compatible base + reasoning option). | | `./ollama` | Ollama local chat provider (OpenAI-compatible base + reachability preflight). | | `./openai-embeddings` | OpenAI embeddings + vision provider. | | `./testing` | Deterministic port fakes. | A useful way to read that table: the top half is the engine (vocabulary, seams, tools, loop), the bottom half is what you opt into (providers) and how you test without them (fakes). ## The runtime and registries `createAiRuntime(config)` is a **pure wiring** function: it resolves every capability port, defaulting each to a no-op or throwing implementation, with no IO and no global mutation. `getAiRuntime(config?)` is the process singleton (shaped like `getKv()`), with `resetAiRuntime()` and `isAiRuntimeInitialized()` alongside. Use `createAiRuntime` when you want an isolated instance (tests, one runtime per tenant); use `getAiRuntime` when the process should share one. `AiRuntimeConfig` mirrors the resolved runtime field-for-field, all optional, so you inject only the ports you have real implementations for — everything else stays a safe default until you need it. Three registries sit at the root — models, embeddings, and vision — each with the same register / get / list / reset shape. Models are addressed by reference: a `ModelRef` is `string | ModelSelector`, and the string form is `":"`, e.g. `"anthropic:claude-sonnet-4-5"`. Errors are a small hierarchy rooted at `AiError`; the two you will actually catch are `AiNotConfiguredError` (you called a capability whose port was never injected) and `ModelProviderNotFoundError` (the reference names a provider that never registered). ## Contracts — the shared vocabulary `@netscript/ai/contracts` is pure types with no IO — the vocabulary every layer above the engine speaks. It carries the `Message` / `MessageRole` conversation model, the multimodal `ContentPart` union (text, image, audio, video, document, each backed by a base64 or URL `ContentSource`), the model descriptors and capability flags, and the tool-call shapes. The one contract worth internalizing before anything else is the **`AgentChunk` union** — the wire vocabulary the whole stack streams. A run yields text spans, tool calls, tool results, message boundaries, usage, and errors as discriminated chunks, always terminating in a final `done` chunk. The durable-chat runtime persists these chunks and the chat UI renders them, which is why the layers compose without adapters: they all speak `AgentChunk`. The vocabulary also carries the generative-UI contract: `RENDER_UI_TOOL_NAME = "render_ui"`, `RenderUiResult`, and a `UiResource` whose `uri` is a `ui://` string (mirroring the MCP resource shape). ## Ports — the seams you wire `@netscript/ai/ports` exposes the capability interfaces — telemetry, tool registry, embeddings, vision, MCP transport, skill loading, the agent loop, memory, and the chat/model provider seams — plus their registry and default factories. Two are worth calling out because they shape how you wire things: - **`ModelProviderPort`** covers discovery (`listModels()`, `getModel()`, `supports()`), and `createChatClient?()` is optional on it — so a discovery-only provider can omit chat entirely. The agent loop injects the narrower `ChatModelProviderPort`, which requires `createChatClient(modelId)`. - **`AgentMemoryPort`** — `append(threadId, message)` and `load(threadId)` are the base; **`recall?(threadId, query)` is optional and `undefined` by default**. There is no built-in semantic recall — guard `recall` and fall back to `load`. ## Tools — Standard Schema, no DSL The tool system validates with **Standard Schema**, so you bring any conforming validator (zod, valibot, arktype, or hand-rolled) — the core adds no schema language. - **`defineAiTool(name)`** returns a builder: `.describe()`, `.parameters(jsonSchema)`, `.input(schema)`, terminating in either `.server(handler)` or `.client()` (a deferred tool, e.g. `render_ui`). - **`createToolRegistry(defs?)`** implements `ToolRegistryPort`; its `.dispatch(name, input, ctx?)` validates input before your handler runs and throws `ToolNotFoundError` / `ToolInputValidationError` when it should. - **`renderUiTool`** is the built-in `render_ui` wire contract: schema-only and **client-deferred** (`result.deferred === true`). The engine runs **no renderer** — rendering is the [chat UI's](https://rickylabs.github.io/netscript/ai/chat-ui/) job. ## The agent loop `createAgentLoop(deps)` builds the loop from injected collaborators: a required `modelProvider: ChatModelProviderPort`, plus optional `tools`, `history`, and `defaultMaxSteps`. `loop.run(input, options?)` returns an `AsyncIterable`; the loop exposes `loop.state` and `loop.stop()`. The defaults are deliberately conservative: the built-in `slidingWindowHistory` strategy keeps `DEFAULT_HISTORY_WINDOW = 20` messages, and `DEFAULT_MAX_STEPS = 8` bounds how many tool-calling steps a run may take before it must settle. Hitting that bound throws `AgentMaxStepsExceededError` inside the run — the loop settles `errored`, yields an `error` chunk, then the final `done`, so a consumer never hangs on an unterminated stream. ## MCP transports `@netscript/ai/mcp` adapts remote Model Context Protocol servers into the same tool registry the loop dispatches. `createMcpTransport(config)` takes a discriminated config (`kind: "stdio"` or `kind: "streamable-http"`) and returns an `McpTransportPort`; `registerMcpTools(registry, transport)` surfaces the remote tools and returns a registration whose `.stop()` detaches. The Streamable-HTTP transport reconnects with backoff, and auth (`none` / `api-token` / `oauth`) is injected by your app's startup wiring rather than hardcoded. The full client story — pooling multiple servers, auth modes, rendering `ui://` resources — is the [MCP guide](https://rickylabs.github.io/netscript/ai/mcp/). ## Provider adapters — opt-in side-effect imports Providers self-register on import, mirroring `@netscript/kv/redis`. The base engine pulls **no** provider SDK; you opt in per provider, and an import is all it takes: ```ts import "@netscript/ai/anthropic"; // self-registers the "anthropic" provider const model = await getModel("anthropic:claude-sonnet-4-5"); ``` Choosing between them is mostly a question of where your models live: - **`./anthropic`** talks to Anthropic directly, catalog taken verbatim from `@tanstack/ai-anthropic`, `apiKey` defaulting to `ANTHROPIC_API_KEY`. - **`./openai-compatible`** is the workhorse for any endpoint that speaks the OpenAI API: no fixed catalog (the remote endpoint owns its model list), and it throws `AiNotConfiguredError` rather than guessing when `baseURL` / `apiKey` are missing. - **`./openrouter`** builds on the OpenAI-compatible base for OpenRouter (key from `OPENROUTER_API_KEY`) and adds a `reasoningEffort` option (`"low" | "medium" | "high"`) normalized to OpenRouter's wire format. - **`./ollama`** builds on the same base for a local endpoint and runs a reachability preflight against the host first — so a missing local daemon fails fast instead of at the first token. - **`./openai-embeddings`** registers for the embedding and vision seams, not chat: `.embed()` and `.analyze()`. Per-provider config fields and defaults are enumerated in the [generated reference](https://rickylabs.github.io/netscript/netscript/reference/ai/). ## Testing — deterministic fakes `@netscript/ai/testing` supplies port fakes so an agent or tool can be exercised without a network: fake chat model providers and agent loops that replay scripted turns and chunks, fake memory, embedding, and vision ports, an in-memory tool registry, and `createFakeTelemetryPort()` (whose `.records` capture emitted telemetry). The full factory list lives in the generated reference. ## Wiring tool and agent registries Today you wire the engine's registries **directly** at app startup: `createToolRegistry(defs?)` from `@netscript/ai/tools` for tools and `createAgentLoop(deps)` from `@netscript/ai/agent` for agent loops. No codegen step sits between your files and the runtime — you register against the engine factories yourself. > Not in this release: netscript generate ai > > A > > netscript generate ai > > codegen — compiling app-owned tool and agent files into name-keyed and stem-keyed registries — is a planned target but is > > not in the CLI today > > . The shipped > > netscript generate > > targets are > > plugins > > , > > runtime-schemas > > , and > > aspire > > . Until an AI target lands, register tools and agent loops against the engine factories directly. ## Where the exact tables live This guide stops at the altitude of "which piece, and why". For the complete, generated symbol tables — every export, signature, and config field — use: - [`@netscript/ai`](https://rickylabs.github.io/netscript/netscript/reference/ai/) — the engine itself: runtime, contracts, ports, tools, agent loop, MCP transports, provider adapters. - [`@netscript/plugin-ai`](https://rickylabs.github.io/netscript/netscript/reference/plugin-ai/) — the thin AI plugin delivery shell. - [`@netscript/plugin-ai-core`](https://rickylabs.github.io/netscript/netscript/reference/plugin-ai-core/) — the plugin's reusable `/v1/ai` contract core. And for the rest of the stack: the [AI overview](https://rickylabs.github.io/netscript/ai/) tells the layering story, [durable chat](https://rickylabs.github.io/netscript/ai/durable-chat/) covers the Fresh runtime, and the [chat UI](https://rickylabs.github.io/netscript/ai/chat-ui/) covers the components that render what this engine streams. _Canonical: https://rickylabs.github.io/netscript/ai/engine/_ --- # AI & Agents — recipes Task-oriented recipes for this area. Each one solves a single concrete problem and assumes you know the basics from the guides above it in the sidebar. The full cross-area catalog lives at [All how-to recipes](https://rickylabs.github.io/netscript/netscript/how-to/). _Canonical: https://rickylabs.github.io/netscript/ai/how-to/_ --- # Build a durable chat **Goal:** wire an AI chat onto a Fresh route whose message history and in-flight tool calls survive reload, reconnect, and a second tab — because the transcript is backed by a **durable session stream**, not component state. This recipe uses the [`@netscript/fresh/ai`](https://rickylabs.github.io/netscript/reference/fresh/) durable-chat plane (published on JSR in `@netscript/fresh`, usable now): one session route, one stream proxy, one SSR seed, and one client island. The chat surface is built from four seams, each a single function: **@netscript/fresh/ai — the four durable-chat seams** | Name | Type | Description | | --- | --- | --- | | `toNetScriptChatResponse` | `session route` | Turn a server chat stream into a durable session Response; authorize-gated (→ 403 on deny). | | `createNetScriptChatStreamProxy` | `the one proxy` | The single durable chat-stream proxy handler the browser reads through — strips misdescribing headers, attaches streams auth server-side. | | `resolveChatSnapshot` | `SSR seed` | Materialize the transcript so far for first paint, reduced through projectChatSnapshot. | | `createNetScriptChatConnection` | `client island` | Live handle over one session: subscribe / send, with idempotent close / stop / dispose. | ## Prerequisites - A NetScript workspace with a scaffolded `apps/dashboard/` Fresh app (`netscript init`). - The **streams runtime** reachable — durable sessions are addressed through the `@netscript/plugin-streams-core` seam. Add it with `netscript plugin install stream --name streams` and bring it up under Aspire. - A model provider key. This recipe calls Anthropic directly through [`@tanstack/ai`](https://tanstack.com/ai); export `ANTHROPIC_API_KEY` before you start the app (Aspire injects it into the app process). - A stable `sessionId` per conversation — one durable stream lives per `sessionId`. ## The one-projection law (read this first) `resolveChatSnapshot` (the SSR seed) and the live island projection MUST run the **same** reducer — `projectChatSnapshot`. Seed and live are two entry points into one function: ```text messages --> [ projectChatSnapshot ] --> { messages, renderParts } ``` If the seed path and the live path diverge, **tool cards drift**: a card materialized at first paint renders differently — or vanishes — once the first live chunk arrives, because the two projections disagree about intermediate tool state. Route both through `projectChatSnapshot` (the seed already does) and the transcript is reload-stable by construction. ## 1. The durable chat session route This route runs one model turn and persists it to the durable session. Two things are non-negotiable: an `authorize` hook (there is **no** default allow-all), and that the model stream is handed to `toNetScriptChatResponse` as its `source`. ```ts // apps/dashboard/routes/api/chat/[sessionId].ts import { chat } from '@tanstack/ai'; import { anthropicText } from '@tanstack/ai-anthropic'; import { resolveChatSnapshot, toNetScriptChatResponse } from '@netscript/fresh/ai'; // Only the owner of a session may drive its turns. REQUIRED in production — // the factory bakes in no default allow-all. const authorize = (request: Request, sessionId: string): boolean => sessionBelongsToUser(request, sessionId); export const handler = { async POST(ctx: { req: Request; params: { sessionId: string } }): Promise { const { sessionId } = ctx.params; const target = { sessionId } as const; // History so far, reduced through the SAME projection the island uses. const snapshot = await resolveChatSnapshot({ target }); const messages = snapshot.messages.map((m) => ({ role: m.role, content: m.content })); // Direct model wiring: a TanStack adapter + chat(). anthropicText() reads // ANTHROPIC_API_KEY from the environment when no key is passed. const adapter = anthropicText('claude-sonnet-4-5'); const source = chat({ adapter, messages, systemPrompts: ['You are a helpful assistant.'], }); // Persist + stream the assistant turn into the durable session, gated by authorize. return toNetScriptChatResponse({ target, request: ctx.req, authorize, source, }); }, }; ``` > authorize is required — do not skip it > > toNetScriptChatResponse > > only enforces access when you pass an > > authorize > > hook, and it applies > > no > > default. Without one, the session stream is unauthenticated. Supplying > > authorize > > without a > > request > > is a programming error and throws. Return > > false > > to deny — the response becomes > > 403 Forbidden > > and the session stream is never touched. > The @netscript/ai engine is published — this recipe still shows the direct wiring > > This recipe calls the model through > > @tanstack/ai > > + > > @tanstack/ai-anthropic > > directly, the same way the reference chat app wires it on shipped seams. The > > @netscript/ai > > engine (model registry, provider ports, agent loop) is published on JSR as of > > 0.0.1-beta.11 > > ( > > deno add jsr:@netscript/ai@0.0.1-beta.11 > > ) and can own this model-call layer behind > > import '@netscript/ai/anthropic' > > — see the > > AI engine > > page. The direct wiring below remains a valid, dependency-light path. ## 2. The one stream proxy The browser never talks to the durable-streams service directly — it reads through a single proxy that attaches streams auth server-side and keeps every response header accurate. Mount `createNetScriptChatStreamProxy` once, as a catch-all under your API namespace. ```ts // apps/dashboard/routes/api/chat-stream/[...path].ts import { createNetScriptChatStreamProxy } from '@netscript/fresh/ai'; const proxy = createNetScriptChatStreamProxy({ // Derive the session from the request path: /api/chat-stream/ai/chat/{sessionId} target: (req) => ({ sessionId: new URL(req.url).pathname.split('/').pop()! }), }); export const handler = { GET: proxy, POST: proxy }; ``` > Why a raw Request here, not a route contract > > This resolver is the one documented exception to NetScript's typed-route rule: > > createNetScriptChatStreamProxy > > 's > > target > > only ever receives the raw > > Request > > , so the session id is parsed from > > req.url > > by hand. Everywhere else you build a URL or read a path param, prefer a bound > > `createRouteReference` > > contract so the pattern and its typed params come from one source of truth. The proxy passes the durable-stream body through **unbuffered**, strips `content-encoding` / `content-length` (they no longer describe the re-framed bytes) plus the hop-by-hop headers, and propagates the client `AbortSignal` so a disconnect tears the upstream fetch down. The `Authorization` header it overlays lives only on the server → streams hop; it is never echoed to the browser. ## 3. Seed the first paint (SSR) In the page loader, materialize the transcript so the chat renders complete on first paint — no loading flash, and the same content a reload would show. ```ts // apps/dashboard/routes/chat/[sessionId].tsx (loader) import { resolveChatSnapshot } from '@netscript/fresh/ai'; const snapshot = await resolveChatSnapshot({ target: { sessionId } }); // snapshot -> { messages, renderParts, offset } // `offset` seeds the live subscription so seed and live read one continuous log. ``` Pass `snapshot` into the island as its initial state. `renderParts` here is the **transport** shape (`text` | `tool`) — the minimal reducer output. The rich presentation parts (charts, tables) come from `parseBlocks` in the UI layer; see [Customize Fresh UI](https://rickylabs.github.io/netscript/web-layer/how-to/customize-fresh-ui/) and the chat tutorial. ## 4. The client island The island opens a durable connection, seeds from the SSR snapshot, `send`s the user message into the session, fires the model turn, and re-materializes when the turn settles. `close` / `stop` / `dispose` are one idempotent teardown — call it on cleanup. ```tsx // apps/dashboard/islands/Chat.tsx import { useSignal } from '@preact/signals'; import { createNetScriptChatConnection, resolveChatSnapshot } from '@netscript/fresh/ai'; import type { NetScriptChatSnapshot } from '@netscript/fresh/ai'; const Chat = ({ sessionId, seed }: { sessionId: string; seed: NetScriptChatSnapshot }) => { const snapshot = useSignal(seed); const connection = createNetScriptChatConnection({ target: { sessionId, baseUrl: `${location.origin}/api/chat-stream` }, initialOffset: seed.offset ?? undefined, authorize: (req, id) => sessionOwnedInBrowser(id), // REQUIRED in prod }); const refresh = async () => { snapshot.value = await resolveChatSnapshot({ target: { sessionId, baseUrl: `${location.origin}/api/chat-stream` }, }); }; const onSubmit = async (text: string) => { // 1. Append the user message to the durable session. await connection.send([{ id: crypto.randomUUID(), role: 'user', content: text }]); // 2. Fire the model turn (the session route streams + persists the reply). await fetch(`/api/chat/${sessionId}`, { method: 'POST' }); // 3. Re-materialize the settled transcript through the same projection. await refresh(); }; // Live durable updates (reload/second-tab replay); dispose on unmount. // for await (const chunk of connection.subscribe(signal)) { ... } // globalThis.addEventListener('beforeunload', () => connection.dispose()); return renderTranscript(snapshot.value, onSubmit); }; export default Chat; ``` > Transport RenderPart vs presentation RenderPart > > Two > > RenderPart > > types exist and must not be conflated. > > @netscript/fresh/ai > > emits the > > transport > > part ( > > text > > | > > tool > > ) — the durable-session wire shape. > > @netscript/fresh-ui > > 's > > chat-render > > ( > > parseBlocks > > ) owns the > > presentation > > part ( > > chart > > | > > donut > > | > > table > > | > > stats > > | > > line > > | > > text > > ) — the rich blocks you render. The transcript reduces through the first; the UI renders through the second. ## Failure modes - **`403 Forbidden` on the turn route:** `authorize` returned `false`, or the caller is not the session owner. This is the hook doing its job — not a bug. - **`authorize` throws:** you passed `authorize` without a `request`. Thread `ctx.req` in. - **Empty transcript on first subscribe:** a first-subscribe can race a not-yet-created session stream; `createNetScriptChatConnection` re-polls with backoff and returns an empty stream rather than a terminal error. A hard `401` / `403` propagates immediately. - **Streams runtime unreachable:** the durable stream URL does not resolve. Confirm `netscript plugin install stream --name streams` ran and the streams service is up under Aspire. - **Model call fails:** `ANTHROPIC_API_KEY` is unset or invalid — `chat()` surfaces the provider error into the assistant stream. ## Next steps - Walk it end to end in the [AI Chat tutorial](https://rickylabs.github.io/netscript/tutorials/chat/). - Render rich blocks and citation chips: [Customize Fresh UI](https://rickylabs.github.io/netscript/web-layer/how-to/customize-fresh-ui/). - The list/board/table live-data plane is different — see [Publish a durable stream](https://rickylabs.github.io/netscript/durable-workflows/how-to/publish-a-durable-stream/) and [Live Dashboard, chapter 05](https://rickylabs.github.io/netscript/tutorials/live-dashboard/05-live-stream/). - Look up exact signatures in the [fresh reference](https://rickylabs.github.io/netscript/reference/fresh/). [Customize Fresh UI](https://rickylabs.github.io/netscript/netscript/web-layer/how-to/customize-fresh-ui/) [Deploy](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/how-to/deploy/) _Canonical: https://rickylabs.github.io/netscript/ai/how-to/build-a-durable-chat/_ --- # MCP `@netscript/ai/mcp` is the **MCP client stack**: it turns external Model Context Protocol servers into typed tools your agent loop can call — transports, injected auth, multi-server pooling, and a safe rendering path for the `ui://` resources a tool result can carry. It is published as part of `@netscript/ai@0.0.1-beta.11` and installs with the engine: `deno add jsr:@netscript/ai@0.0.1-beta.11`. > Two different MCP surfaces — don't conflate them > > This page documents the > > @netscript/ai/mcp > > client library > > : your app > > consumes > > external MCP servers as agent tools. That is a separate surface from the NetScript MCP > > server > > — a standalone stdio server for coding agents that you run with > > netscript agent mcp > > and install with > > netscript agent init > > . The server exposes framework-aware diagnostics, telemetry summaries, and doc search > > about > > your project; the client library on this page wires > > remote > > MCP servers > > into > > your product's agent loop. See > > Agent tooling > > and the > > `@netscript/mcp` reference > > for the server. ## The story: one config instead of a protocol integration Your agent needs a capability that lives behind an MCP server — a docs-search server, an internal ops server, a vendor's tool endpoint. Wiring that by hand is a protocol integration, not a feature: JSON-RPC framing over stdio or HTTP, session lifecycle, reconnect-with-backoff when the connection drops mid-run, auth headers that must not be hardcoded, translating the server's tool schemas into whatever your agent's tool registry expects, and deciding what to do when a result embeds UI content you did not author. Every one of those steps is a place for an agent — human or AI — to burn a turn on glue that has nothing to do with the feature. The `./mcp` subpath collapses that to configuration. You declare servers as data, register the pool into the same tool registry the [agent loop](https://rickylabs.github.io/netscript/ai/engine/) already dispatches through, and the remote tools become ordinary registry entries: ```ts import { createMcpTransportPool, registerMcpTools } from "@netscript/ai/mcp"; import { createToolRegistry } from "@netscript/ai/tools"; const pool = createMcpTransportPool({ servers: [{ kind: "streamable-http", serverId: "search", url: "https://mcp.example.com", auth: { mode: "api-token", token: Deno.env.get("MCP_TOKEN")!, scheme: "Bearer" }, }], }); const registry = createToolRegistry(); const registration = await registerMcpTools(registry, pool); // registration.toolNames — the remote tools, now dispatchable like any local tool // registration.stop() — detach them again ``` From here the agent loop treats a remote MCP tool exactly like a local `defineAiTool` definition — same registry, same dispatch, same [`AgentChunk` stream](https://rickylabs.github.io/netscript/ai/engine/) on the wire. ## Transports — stdio and reconnectable Streamable-HTTP `createMcpTransport(config)` takes a discriminated config — `{ kind: "stdio" }` or `{ kind: "streamable-http" }` — and returns an `McpTransportPort`. The Streamable-HTTP transport is built for long-lived agent sessions: it reconnects with configurable backoff (`McpBackoffConfig`) and exposes its lifecycle as an observable state machine. **Transport lifecycle — @netscript/ai/mcp** | Name | Type | Description | | --- | --- | --- | | `McpConnectionState` | `"disconnected" \| "connecting" \| "connected" \| "reconnecting" \| "closed"` | The transport lifecycle union; `reconnecting` is a first-class state, not an error. | | `onStateChange(handler)` | `(state, previous) => void` | Subscribe to lifecycle transitions (`McpStateChangeHandler`); returns an unsubscribe function. | | `StreamableHttpMcpTransport` | `class` | The reconnectable Streamable-HTTP transport; configured via `StreamableHttpMcpTransportConfig` (serverId, url, auth, backoff). | | `StdioMcpTransport` | `class` | The stdio transport for local process-based MCP servers. | Auth is **injected at the composition root**, never baked into a transport class. `McpAuthConfig` is a three-mode union — `{ mode: "none" }`, `{ mode: "api-token", token, headerName?, scheme? }`, or `{ mode: "oauth", accessToken, tokenType? }` — so the same server declaration works across environments with only the secret changing. ## The pool — many servers, one tool namespace Real agents talk to more than one server. `createMcpTransportPool(config)` builds a multi-server pool from **serializable** transport configs (so server declarations can live in configuration, not code); `createMcpTransportPoolFromTransports` wraps transports you already own. The pool is itself an `McpTransportPort`, so everything that accepts one transport accepts a pool of them. **McpTransportPool — the multi-server surface** | Name | Type | Description | | --- | --- | --- | | `connect(options)` | `Promise` | Open every pooled transport and return the discovered tools, name-prefixed by server id so two servers' tools never collide. | | `listTools(options)` | `Promise` | Re-list tools from all pooled transports without tearing down warm connections. | | `callTool(name, args, options)` | `Promise` | Route a prefixed tool name to its server and extract any `ui://` resources from the result. | | `state / onStateChange` | `McpConnectionState` | The aggregate lifecycle across all pooled transports. | | `server(serverId) / serverIds` | `McpTransportPort \| undefined` | Reach a single pooled transport when you need it. | | `stop()` | `Promise` | Stop every pooled transport and clear discovered tool routes. | `registerMcpTools(registry, transport)` is the bridge into the agent: it surfaces the remote tools into a `ToolRegistryPort` and returns an `McpToolRegistration` whose `.stop()` detaches them — registration is reversible, not a global mutation. ## `ui://` resources — tool results that carry UI, rendered safely An MCP tool result can embed **`ui://` resources** — UI content authored by the server, not by you. The pool surfaces these on every `McpPooledToolResult` as `uiResources`, and `extractMcpUiResources(result)` pulls the **data-only** `McpUiResource` records from any raw tool result. The engine stops there by design: it extracts, it never renders. Rendering belongs to the web layer, behind two guardrails you install with `netscript ui:add ai`: - the **`McpUiWidget` island** (from the `@netscript/fresh-ui` copy registry) renders a `ui://` resource in a sandboxed frame — sanitized, themed through the sandbox route, `no-referrer`; - the **`createMcpSandboxHandler`** route (on `@netscript/fresh/ai/sandbox`) serves the resource with the active theme's tokens and a per-response CSP — see [the durable-chat sandbox section](https://rickylabs.github.io/netscript/ai/durable-chat/) for the handler itself. This is the same trust boundary as the [`render_ui` tool](https://rickylabs.github.io/netscript/ai/chat-ui/): model- or server-authored UI reaches the page only through a curated renderer or a sandbox, never as raw HTML in your island tree. The whole path is exercised by the framework's own merge gate — the CLI E2E suite drives a real Streamable-HTTP MCP round trip through this client stack into a rendered `McpUiWidget` before a branch can land. ## One comparison Encore's MCP story points **inward**: it ships an MCP server that exposes a running Encore backend's introspection surface to coding agents, so an agent can inspect and verify the backend it is editing. NetScript ships **both halves**. The inward half is the `netscript agent mcp` server (documented under [Agent tooling](https://rickylabs.github.io/netscript/ai/agent-tooling/) and the [`@netscript/mcp` reference](https://rickylabs.github.io/netscript/reference/mcp/)): a stdio server that gives a coding agent framework-aware diagnostics, telemetry summaries, and doc search over the project it is editing. The outward half — this page — is the `@netscript/ai/mcp` client stack for consuming external MCP servers as tools inside your own product's agents, with a sandboxed path for the UI those tools return. Both paths are published and gate-tested. ## Where to go next [The engine — ports, tools, agent loop The @netscript/ai surface this stack plugs into: the tool registry, the agent loop, and the McpTransportPort seam.](https://rickylabs.github.io/netscript/netscript/ai/engine/) [Durable chat — the sandbox route createMcpSandboxHandler on @netscript/fresh/ai/sandbox: themed, CSP-guarded serving of ui:// resources.](https://rickylabs.github.io/netscript/netscript/ai/durable-chat/) [Chat UI — rendering the transcript The fresh-ui copy registry: tool cards, the render_ui block renderer, and the McpUiWidget island.](https://rickylabs.github.io/netscript/netscript/ai/chat-ui/) [Look up — @netscript/ai The generated reference for the engine package, including every ./mcp export.](https://rickylabs.github.io/netscript/netscript/reference/ai/) _Canonical: https://rickylabs.github.io/netscript/ai/mcp/_ --- # CLI reference This is the cheat-sheet: which `netscript` command we reach for, grouped by task. Each section lists the everyday spelling and stops there — every flag, subcommand, and extended verb lives in the [command reference](https://rickylabs.github.io/netscript/reference/cli/commands/), and the embeddable TypeScript surface is on the [`@netscript/cli` package page](https://rickylabs.github.io/netscript/reference/cli/). Every command here uses the public `netscript ` form backed by the published JSR package; the vendored `packages/cli/...` path you may see in a local-source checkout is a contributor-only shape. > Database commands need Aspire running first > > The > > netscript db ... > > commands provision and talk to your database > > through Aspire > > . > > cd aspire && aspire start > > brings up Postgres and Redis via Docker and opens the dashboard at > > :18888 > > — do this > > before > > any > > db > > command ( > > sqlite > > is the file-backed exception with no container). Run a > > db > > command with Aspire down and it fails to find the database — the “aspire start failed: project file does not exist” error almost always means exactly this. See the > > database & migration how-to > > . ## Install The CLI is published to JSR as `@netscript/cli`. Install it globally for a tidy `netscript` command on your PATH, or run it ad-hoc with no install at all. ```bash # Installs a `netscript` command on your PATH deno install --global --allow-all --name netscript jsr:@netscript/cli@0.0.1-beta.11 netscript --help ``` ```bash # Run the same CLI without installing anything deno x jsr:@netscript/cli@0.0.1-beta.11 --help ``` ```bash # Re-run the install with --force to pull the latest published version deno install --global --allow-all --force --name netscript jsr:@netscript/cli@0.0.1-beta.11 ``` `netscript --version` prints the installed CLI version; `netscript --help` and `netscript --help` (for example `netscript db --help`) show the exact flag spelling your installed version ships. ## The everyday flow Most sessions follow the same shape, and the order matters: **Aspire (step 2) must be up before any `db` command (step 3).** 1 · Scaffold netscript init lays down the whole workspace — contracts, an example service, plugins, and the Aspire layer. 2 · Orchestrate cd aspire && aspire start brings up your database and Redis, and opens the dashboard at :18888. Do this before any db command. 3 · Database netscript db init / generate / migrate / seed — only after Aspire is up. 4 · Extend & generate netscript plugin install, then netscript generate plugins to wire the registry. ## Scaffold a workspace **netscript init** | Name | Type | Description | | --- | --- | --- | | `Create a workspace` | `netscript init my-app` | Scaffold everything — contracts, plugin registry, Fresh app, a default Redis cache, and the Aspire layer. On a terminal it prompts for whatever you omit (name, database, service, cache). | | `Preview first` | `netscript init my-app --dry-run` | Print every file and directory the scaffold would create, and write nothing. | | `Fully specified, no prompts` | `netscript init my-app --db postgres --service --service-name users --service-port 3001 --yes` | Postgres database support, an example oRPC `users` service on port 3001, defaults for the rest. `--yes` accepts defaults, `--ci` is non-interactive; both engage automatically when stdin is not a terminal. | | `Pick a database engine` | `netscript init my-app --db postgres` | `postgres` (recommended), `mysql`, `mssql`, `sqlite`, or `none` — the default is no database unless you pass `--db`. | | `Skip Aspire` | `netscript init my-app --no-aspire` | Scaffold without the .NET Aspire footprint; start the Fresh app directly with `deno task --cwd apps/dashboard dev`. | | `Tune the rest` | `--cache-backend garnet · --model-name Product · --path ./apps · --editor zed` | Cache backend (`redis` default, `garnet`, or app-level `deno-kv`; `--cache=false` for none), the Prisma model name for the scaffolded CRUD surface, the target directory, and editor settings. | Every `init` flag — including `--app-name`, `--no-git`, `--force`, `--json`, and `--from ` — is spelled out in the [command reference](https://rickylabs.github.io/netscript/reference/cli/commands/). ## Run & iterate These are workspace `deno task`s, not `netscript` subcommands — the day-to-day loop once the scaffold exists. **Run and gate the workspace** | Name | Type | Description | | --- | --- | --- | | `Orchestrate everything` | `cd aspire && aspire start` | Bring up the database, Redis, services, and plugin processors, with the dashboard at :18888. | | `Run the dashboard alone` | `deno task --cwd apps/dashboard dev` | Start the Fresh frontend directly (or let `aspire start` orchestrate it). | | `Run a service alone` | `deno task --cwd services/users dev` | Start the example `users` oRPC service on port 3001. | | `Check, lint, test` | `deno task check · deno task lint · deno task fmt · deno task test` | Type-check, lint, format, and test the whole workspace. | ## Services & contracts A NetScript workspace is contract-first: you define an oRPC contract, then a service implements it. **Services and contracts** | Name | Type | Description | | --- | --- | --- | | `Add a service` | `netscript service add --name orders --port 3002` | Add a service workspace member, its v1 contract, and the Aspire registration. | | `Add a contract` | `netscript contract add catalog-items` | Create `contracts/versions/v1/catalog-items.contract.ts` from the oRPC contract template and regenerate the v1 aggregate exports. | | `Add a route + handler` | `netscript contract add-route · netscript service add-handler` | Append a typed procedure to a contract, then bind it with a compiling service handler stub. | | `See what exists` | `netscript service list · netscript contract list · netscript contract inspect ` | List services, list v1 contract modules (and whether each has a matching service), and inspect a contract's procedures and schemas. | | `Regenerate Aspire helpers` | `netscript service generate` | Regenerate the Aspire helper files from your service configuration. | The full groups — `service set` / `remove` / `ref add`, `contract remove` / `version add`, and every flag — are in the [command reference](https://rickylabs.github.io/netscript/reference/cli/commands/). ## Plugins Plugins add capabilities — background workers, durable sagas, webhook triggers, durable streams, authentication. Public install adds the plugin package dependency, emits workspace-owned glue that imports it, and registers its contributions; the plugin's internals stay in the installed dependency. **Plugin lifecycle** | Name | Type | Description | | --- | --- | --- | | `Install an official plugin` | `netscript plugin install workers --name workers` | Bare aliases (`workers`, `auth`, …), scoped specs (`@netscript/plugin-workers`), and `jsr:` specs all work. After auth, pick the runtime backend with `NETSCRIPT_AUTH_BACKEND` — see [add authentication](https://rickylabs.github.io/netscript/identity-access/how-to/add-authentication/). | | `Wire the registry` | `netscript generate plugins` | Regenerate the plugin registries from project source. Run this after every `plugin install`. | | `Check health` | `netscript plugin list · netscript plugin doctor · netscript plugin info workers` | List registered plugins, run the wiring sanity check, and show a single plugin's details. | | `Author your own` | `netscript plugin new billing` | Scaffold a new two-tier plugin: a JSR-publishable core package plus a thin connector. See [author a plugin](https://rickylabs.github.io/netscript/orchestration-runtime/how-to/author-a-plugin/). | | `Discover & maintain` | `netscript marketplace search · netscript plugin update · netscript plugin remove ` | Search the plugin marketplace, re-pin and regenerate an installed plugin, or remove one and update workspace registration. | The extended verbs — `plugin sync`, `enable` / `disable` / `setup`, `item-add`, and the `plugin auth` backend/provider/session subcommands — are in the [command reference](https://rickylabs.github.io/netscript/reference/cli/commands/). ## Database The database workflow uses Prisma with a Deno runtime, and every command below requires Aspire to be running first (`cd aspire && aspire start`) — `sqlite` being the file-backed exception. Plugin schemas (`workers`, `sagas`, `triggers`, `auth`) are picked up by the same `generate` / `migrate` pass. The walkthrough is the [database & migration how-to](https://rickylabs.github.io/netscript/data-persistence/how-to/database-migration/). **Database workflow (Aspire must be running)** | Name | Type | Description | | --- | --- | --- | | `Initialize + first migration` | `netscript db init --name init` | Initialize database tooling and create the named migration. | | `Generate the client` | `netscript db generate` | Generate the Deno-runtime Prisma client (and zod) — including plugin schemas such as `auth.prisma`. | | `Migrate & seed` | `netscript db migrate · netscript db seed` | Apply migrations (including each plugin's contributed schema), then run the workspace seed scripts. | | `Inspect` | `netscript db status · netscript db studio` | Show migration/tooling status, or open the database studio for browsing data. | | `Recover` | `netscript db introspect · netscript db reset` | Introspect the configured database, or reset it back to a clean state. | | `Multiple databases` | `netscript db add · netscript db list` | Add a second database workspace to an existing project and list registered targets. | The scaffolded workspace also defines Aspire-less `deno task db:*` tasks (`db:generate`, `db:migrate`, `db:seed`, `db:studio`, …) inside `database//` that run Prisma directly — the form to use in deno-only or CI jobs. The target-management and migration-history verbs (`db deploy`, `validate`, `resolve`, `remove`) are in the [command reference](https://rickylabs.github.io/netscript/reference/cli/commands/). ## Generate After adding or changing plugins or configuration, regenerate the artifacts the project consumes. **Code generation** | Name | Type | Description | | --- | --- | --- | | `Plugin registries` | `netscript generate plugins` | Regenerate the plugin registries from project source — the post-install step. | | `Runtime config schemas` | `netscript generate runtime-schemas` | Generate JSON Schema files for runtime configuration topics. | | `Aspire helpers` | `netscript generate aspire` | Regenerate the Aspire AppHost helpers from `appsettings.json` without re-scaffolding. | Related: `netscript config inspect` / `get` / `set` read and write the resolved project configuration, and `netscript config override` manages versioned runtime overrides — the full subcommand table is in the [command reference](https://rickylabs.github.io/netscript/reference/cli/commands/). ## Fresh UI The frontend is copy-source: components are copied into your repo under `apps/dashboard`, and the code is yours to own and edit. See [customize Fresh UI](https://rickylabs.github.io/netscript/web-layer/how-to/customize-fresh-ui/). **UI registry commands** | Name | Type | Description | | --- | --- | --- | | `Initialize the design system` | `netscript ui:init --project-root apps/dashboard` | Copy the fresh-ui components and tokens into the dashboard app. | | `Add a component` | `netscript ui:add --project-root apps/dashboard` | Copy an additional registry item — you own the copied source from that point. | | `List & maintain` | `netscript ui:list · netscript ui:update · netscript ui:remove ` | List registry items, update only files you have not modified, or remove a copied item. | ## Deploy Two deploy paths are wired today: the **Deno Deploy** cloud target and the **Windows Service** (Servy) path. `netscript deploy docker` and `deploy compose` exist as command groups but are not wired — they only print help. See [deploy](https://rickylabs.github.io/netscript/orchestration-runtime/how-to/deploy/) for the portability story. **Deploy commands** | Name | Type | Description | | --- | --- | --- | | `Deno Deploy: preflight` | `netscript deploy deno-deploy plan` | Run the unstable-API guard (scans for `Deno.openKv`, `Deno.cron`, `BroadcastChannel`, `Temporal`) without pushing. The same guard **blocks** `up --prod` on a violation; a preview push warns but proceeds. | | `Deno Deploy: lifecycle` | `netscript deploy deno-deploy up [--prod] · down · status · logs` | Push, delete, and inspect the deployment. A thin router over the native `deno deploy` CLI — it must be on your PATH and handles authentication. | | `Windows Service: build` | `netscript deploy build` | Build the Windows Service deployment artifacts from a deployment manifest via Servy. | | `Windows Service: lifecycle` | `netscript deploy install · start · stop · status · logs · upgrade · uninstall` | Install, run, inspect, upgrade, and remove Windows Services from the manifest. | The shared flags (`--org`, `--app`, `--entrypoint`, `--env-file`, `--project-root`), the planning-only cloud targets, and the artifact-copy verbs are in the [command reference](https://rickylabs.github.io/netscript/reference/cli/commands/). ## Agent tooling **AI agent commands** | Name | Type | Description | | --- | --- | --- | | `Install agent tooling` | `netscript agent init` | Install NetScript MCP and skills for detected agent hosts (`--host claude`, `vscode`, or `all`). | | `Run the MCP server` | `netscript agent mcp` | Start the NetScript MCP server over standard input/output. | See [Agent tooling](https://rickylabs.github.io/netscript/ai/agent-tooling/) for the mental model. ## The full surface This page is the curated common path. For every command, every subcommand, and every flag — spelled exactly as the installed CLI prints it — go to the [command reference](https://rickylabs.github.io/netscript/reference/cli/commands/); for the embeddable package API, the [`@netscript/cli` package page](https://rickylabs.github.io/netscript/reference/cli/). [Command reference The exhaustive command surface — every command, subcommand, and flag verbatim.](https://rickylabs.github.io/netscript/netscript/reference/cli/commands/) [Quickstart Install → init → aspire start → db → hit an endpoint, in about five minutes.](https://rickylabs.github.io/netscript/netscript/quickstart/) [Database & migration The full db workflow, with the Aspire-up dependency spelled out step by step.](https://rickylabs.github.io/netscript/netscript/data-persistence/how-to/database-migration/) [@netscript/cli package The generated package reference — the embeddable TypeScript surface, not the command tree.](https://rickylabs.github.io/netscript/netscript/reference/cli/) [Glossary](https://rickylabs.github.io/netscript/netscript/glossary/) _Canonical: https://rickylabs.github.io/netscript/cli-reference/_ --- # Three ideas explain almost everything in NetScript. The contract is the source of truth, the host is empty until plugins fill it, and one runtime brings up many resources together. Hold these three and the rest of the docs read as detail. [Quickstart — 5 min](https://rickylabs.github.io/netscript/netscript/quickstart/) [Why NetScript](https://rickylabs.github.io/netscript/netscript/why/) > Read this once > > This is the five-minute mental model — what talks to what, and nothing deeper; the *why* behind this shape lives in the > > architecture essay > > . Every capability hub, tutorial, and reference page assumes the three ideas below — so the time you spend here is paid back on every other page. Nothing here is aspirational: each claim links to the published surface that proves it. NetScript is a Deno-native backend framework that generates a workspace you own and run yourself. It is not a hosted service and not a single library — it is a coordinated package family plus a CLI that wires the boring seams together. To reason about *any* part of it, you only need three ideas. ![A NetScript workspace: contracts define services; services, workers, sagas, triggers, streams, and auth plugins register against a host; the AppHost materializes each as an Aspire resource alongside Postgres, Redis, and the dashboard.](https://rickylabs.github.io/netscript/netscript/assets/diagrams/architecture-overview.svg) *The whole system in one picture: contracts at the center, plugins contributing to a host, and the AppHost bringing every resource up together.* ## Idea 1 — The contract is the source of truth Most type drift in a TypeScript backend comes from having *two* sources of truth: the shape a service returns, and the shape the client believes it returns. NetScript removes the second one. You define an oRPC **contract** once; the typed client is *derived* from it, not generated alongside it. There is no codegen step to fall out of sync, and the type checker becomes your integration test — change the contract and every caller that no longer matches fails to compile. ```ts // One source of truth — imported, not duplicated. import { baseContract, OffsetPaginationQuerySchema } from '@netscript/contracts'; import { z } from 'zod'; export const usersContract = { list: baseContract .route({ method: 'GET', path: '/users' }) .input(OffsetPaginationQuerySchema) .output(z.object({ items: z.array(z.object({ id: z.string(), email: z.string() })) })), }; ``` ```ts // `clients.users.list` is typed FROM the contract above — no second definition. import { defineServices } from '@netscript/sdk'; import { usersContract } from './contracts/users.ts'; const { clients } = defineServices({ users: { contract: usersContract } }); const result = await clients.users.list({ limit: 20, offset: 0 }); console.log(result.items); // fully typed end to end ``` The same contract that types the client also drives the service router, the OpenAPI document, the Scalar docs, and — because every handler is span-wrapped — the traces. One definition, many consumers, zero hand-maintained duplicates. That is what "the contract is the product" means in practice. Go deeper in [contracts & type flow](https://rickylabs.github.io/netscript/netscript/explanation/contracts/) or the [services capability hub](https://rickylabs.github.io/netscript/netscript/services-sdk/services/) . ## Idea 2 — The host is empty; plugins fill it A NetScript application is a **host** that, on its own, does almost nothing. Capabilities — workers, sagas, triggers, streams, auth — arrive as **plugins**, and each plugin *registers* what it contributes rather than asking you to edit a central wiring file. Three nouns carry the whole model: a plugin declares a **manifest**, the manifest names a set of **contributions** against a fixed set of axes, and a generated **registry** turns those declarations into static modules the runtime imports. ```ts // definePlugin(name, version) returns a builder. No behavior runs here — // each .with*() call only NAMES a contribution against a fixed axis. import { definePlugin } from '@netscript/plugin'; export const workersManifest = definePlugin('@netscript/plugin-workers', '0.0.1-beta.11') .withService({ name: 'workers-api', entrypoint: './services/src/main.ts', port: 8091 }) .withBackgroundProcessor({ name: 'workers-worker', entrypoint: './bin/worker.ts', concurrency: 2 }) .withDbSchemas([{ path: './database/workers.prisma', engine: 'postgres' }]) .build(); ``` Because the host only understands a *fixed* set of contribution shapes — a service, a background processor, a schema fragment, a stream topic, a config topic — but *any number* of plugins may contribute against them, capabilities compose without the host ever growing new code. Adding one is a regeneration step (`netscript plugin install …` then regenerate the registry), never an edit to someone else's boundary. And because a plugin's HTTP API and its background work are materialized as *separate* resources, a crash in a worker never takes down the API. ![A single plugin splits into an HTTP API service resource and one or more isolated background-processor resources, each started by the AppHost as a separate Aspire resource with its own permissions.](https://rickylabs.github.io/netscript/netscript/assets/diagrams/plugin-thread-isolation.svg) *"One plugin" is a packaging unit, not a runtime unit: at runtime it fans out into one API service and N isolated background resources.* The full walkthrough — core/plugin split, the contribution axes, and why the registry is generated rather than scanned — is in [the plugin system](https://rickylabs.github.io/netscript/netscript/explanation/plugin-system/) . The auth plugin is the model at its richest: see [the auth model](https://rickylabs.github.io/netscript/netscript/explanation/auth-model/) . ## Idea 3 — One runtime, many resources The third idea is what turns a pile of packages into a system you can *run*. NetScript is Deno-native and distributed on JSR — you import from `jsr:@netscript/*` and use Web Platform APIs you already know. But a real backend is never one process, so the generated workspace ships an **AppHost** that brings every resource up together through .NET Aspire: your database, your cache (`redis` by default, or `garnet` / `deno-kv` via `--cache-backend`), each plugin's service and background processors, and a real dashboard — locally and on the way to deploy. ![The AppHost graph: Postgres and Redis at the base; service, workers-api, sagas, triggers, auth-api, and streams resources above them; the Aspire dashboard observing all of them.](https://rickylabs.github.io/netscript/netscript/assets/diagrams/aspire-resource-graph.svg) *One command brings up the whole graph: data stores first, then every service and background resource, with the dashboard wired in for traces and logs.* The flow is two commands — `cd aspire && aspire start` brings up your database and Redis *before* any `netscript db` command touches the database — and you watch the result in the Aspire dashboard at `:18888`. Postgres is the recommended default; pass `--db mysql`, `--db mssql`, or `--db sqlite` at scaffold time to pick another engine (Postgres, MySQL, and SQL Server run as Aspire container resources, while SQLite is file-backed). **What comes up when you run the workspace (ports are range-allocated conventions; the dashboard is the authority)** | Name | Type | Description | | --- | --- | --- | | `Aspire dashboard` | `:18888` | Every resource, trace, and log in one place — you don't stand up your own observability stack. | | `Your service` | `:3000` | The oRPC API the contract drives, served at /api/rpc/* with OpenAPI and health wired in. | | `Plugin services` | `:8091–:8094` | Each plugin's API runs as its own resource — workers :8091, sagas :8092, triggers :8093, auth :8094. | | `Data stores` | `Postgres · Redis` | Brought up first, before migrations run; provisioned by the AppHost, not a hand-written compose file. Postgres is the recommended engine — pass --db postgres; mysql / mssql / sqlite / none are the other choices. | The whole runtime is opt-out, not lock-in: [scaffold with](https://rickylabs.github.io/netscript/netscript/quickstart/) `netscript init my-app --no-aspire` and the .NET footprint is gone, leaving the Deno workspace you own. How the AppHost reads manifests and materializes resources is covered in [orchestration with Aspire](https://rickylabs.github.io/netscript/netscript/explanation/aspire/) and the [runtime configuration hub](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/runtime-config/) . ## How the three ideas connect They are one idea seen from three angles. The **contract** defines what a capability exposes; a **plugin** packages that capability and declares how it contributes; the **runtime** materializes every contribution as an isolated resource and traces it end to end. Drift can't open up between layers because each layer derives from the one before it — the client from the contract, the registry from the manifests, the resource graph from the registry. [Learn it by building Pick a track and ship one complete app end to end — the fastest way to feel all three ideas at once.](https://rickylabs.github.io/netscript/netscript/tutorials/) [Map the capabilities Every feature, hub by hub, each with a Learn / Do / Reference triplet.](https://rickylabs.github.io/netscript/netscript/capabilities/) [Read the architecture The deep why behind the three ideas — the full essay this page is the five-minute version of.](https://rickylabs.github.io/netscript/netscript/explanation/architecture/) > Still beta > > NetScript is in > > beta > > (targeting a stable release in late 2026). The published package surface is the contract: what you import from > > jsr:@netscript/* > > is what's documented and type-checked. Every > > @netscript/* > > package shares one aligned version ( > > 0.0.1-beta.11 > > ) and releases move in lockstep. Scaffolded JSR imports use exact > > @0.0.1-beta.11 > > pins for the beta train. Build with us — but pin your versions. [Quickstart — 5 minutes](https://rickylabs.github.io/netscript/netscript/quickstart/) _Canonical: https://rickylabs.github.io/netscript/concepts/_ --- # Glossary NetScript is a small framework with a handful of load-bearing words. Most of the friction in learning it is vocabulary, not syntax — once you can map a term to the file it lives in and the API that produces it, the rest of the docs read quickly. This page is the dictionary. Every entry is one to three sentences, grounded in the real scaffold and reconciled to the current framework, and links to the canonical page where the term is *taught* (the Explanation that gives you the mental model, the Capability hub that shows the headline API, or the generated [Reference](https://rickylabs.github.io/netscript/reference/) that lists every export). Use it as a lookup, not a tutorial: when a page drops a word like **contribution**, **single-active-backend**, or **durability tier** and you want the precise meaning, come here, then follow the link back into the learning thread. > How to read an entry > > The middle column is the > > headline API or file > > a term maps to — the concrete thing you type or open. The description links to where it is taught. Terms are alphabetized within each table; related terms cross-reference each other so you can chase a concept across the > > Explanation > > , > > Capabilities > > , and > > Reference > > zones without a dead end. ## Core vocabulary These are the words you will meet first — in the [Quickstart](https://rickylabs.github.io/netscript/quickstart/), the [tutorials ladder](https://rickylabs.github.io/netscript/tutorials/), and the [architecture overview](https://rickylabs.github.io/netscript/explanation/architecture/). **A–C** | Name | Type | Description | | --- | --- | --- | | `AppHost` | `aspire/apphost.mts` | The Aspire orchestration entry point that boots your whole workspace — Postgres, Redis, services, and plugin processors — as one resource graph with a dashboard at https://localhost:18888. In NetScript the AppHost is a **generated Node/TypeScript** file (`aspire/apphost.mts`) driven by `aspire.config.json` and `appsettings.json`, not a dotnet project. You start it with `cd aspire && aspire start`, and it must be up *before* any `netscript db` command. See [Explanation → Aspire](https://rickylabs.github.io/netscript/explanation/aspire/) and [reference/aspire/](https://rickylabs.github.io/netscript/reference/aspire/). | | `archetype` | `doctrine classification` | The architectural category a package or plugin belongs to (for example a contract unit, an adapter, a runtime, or a plugin), which determines its public surface, quality gates, and allowed dependencies. Archetype is a *framework-internal* doctrine term; as an app author you mostly meet it indirectly through the shape of a scaffolded plugin. See [Explanation → Plugin system](https://rickylabs.github.io/netscript/explanation/plugin-system/). | | `Aspire` | `aspire start` | The .NET Aspire orchestration layer NetScript uses for local development. It provisions your database (Postgres is the recommended engine; or `mysql` / `mssql` via `--db`, each an Aspire container resource — `sqlite` is file-backed and adds no container) and Redis as Docker containers and wires every service and plugin processor together with health, logs, and OTLP traces — no manual `docker compose`. The escape hatch is `netscript init --no-aspire` for a leaner single-process loop. See [Explanation → Aspire](https://rickylabs.github.io/netscript/explanation/aspire/) and [How-to → Deploy](https://rickylabs.github.io/netscript/orchestration-runtime/how-to/deploy/). | | `AuthBackendPort` | `@netscript/plugin-auth-core/ports` | The single seam every authentication backend implements — defined by `@netscript/plugin-auth-core`, it composes provider, session-store, crypto, and principal-mapper sub-ports plus an `authenticate(request)` method and an *optional* `interactive` flow. Backends are pure adapters behind this port; the host never special-cases a vendor. See [Capabilities → Authentication](https://rickylabs.github.io/netscript/capabilities/auth/) and [Explanation → Auth model](https://rickylabs.github.io/netscript/explanation/auth-model/). | | `AuthBackendOperationUnsupportedError` | `typed capability boundary` | The typed error a backend throws when you call an operation it does not implement — for example a session mutation on a stateless backend, or an interactive sign-in on a non-interactive one. It makes the **single-active-backend** capability matrix fail loud rather than silently no-op, so missing surface is a visible error, not a mystery. See [Explanation → Auth model](https://rickylabs.github.io/netscript/explanation/auth-model/) and the [Capabilities → Auth](https://rickylabs.github.io/netscript/capabilities/auth/) export list. | | `AuthSession` | `@netscript/plugin-auth-core/domain` | The domain record for an authenticated session — id, user, account, and state (`active` \| `expired` \| `revoked`) — defined as a Zod-validated type in `@netscript/plugin-auth-core`. It is the durable shape the session store reads and writes and the entity the auth stream projects. See [Explanation → Auth model](https://rickylabs.github.io/netscript/explanation/auth-model/) and [Capabilities → Authentication](https://rickylabs.github.io/netscript/capabilities/auth/). | | `capability` | `/capabilities//` | A first-class thing NetScript does — services, background jobs, durable sagas, triggers, streams, authentication, database, KV/queues/cron, telemetry, and the Fresh UI. Each capability has a hub page pairing a one-screen concept with its headline API and a Learn / Do / Reference triplet. See the [Capabilities](https://rickylabs.github.io/netscript/capabilities/) index. | | `composition root` | `netscript.config.ts` | The single place where a NetScript app is assembled — `defineConfig({...})` declares the project name, path layout, logging, databases, and plugin entrypoints such as generated `./auth/mod.ts` glue or author-owned `./plugins//mod.ts`. It is the wiring manifest the runtime reads to know what your app is made of. See [Explanation → Architecture](https://rickylabs.github.io/netscript/explanation/architecture/) and [reference/config/](https://rickylabs.github.io/netscript/reference/config/). | | `contract` | `oc.route().input().output()` | The versioned, schema-first definition of an API — built with [@orpc/contract](https://orpc.unnoq.com) plus [zod](https://zod.dev) in `contracts/versions/v1/` — that locks the request and response shape *before* any handler exists. Passing a contract to `implement(Contract)` produces the object whose `.handler(...)` methods the service router binds, so the client and server share one source of truth. This is the heart of NetScript's contracts-first model. See [Explanation → Contracts](https://rickylabs.github.io/netscript/explanation/contracts/) and [reference/contracts/](https://rickylabs.github.io/netscript/reference/contracts/). | | `contribution` | `plugin manifest export` | A typed unit of functionality a plugin contributes to the host — a job, a saga, a webhook trigger, a route, an Aspire resource, or a database schema — surfaced through the plugin's `mod.ts` **manifest** so the runtime can discover and wire it. The auth plugin, for instance, contributes a service, a Prisma schema, and durable stream events through its manifest. See [Explanation → Plugin system](https://rickylabs.github.io/netscript/explanation/plugin-system/). | **C–J** | Name | Type | Description | | --- | --- | --- | | `compensation-as-effect` | `handler return value` | How the scaffolded saga sample models rollback: instead of an explicit `.step()` / `.compensate()` chain, a message handler *returns an array of effects* (such as `sagaComplete({...})`) that the runtime applies. Compensation is just another effect a handler can emit. See [Explanation → Durability model](https://rickylabs.github.io/netscript/explanation/durability-model/). | | `durability tier` | `.durability('t1')` | The persistence guarantee a saga declares in its builder chain — for example `'t1'` in `defineSaga(id).durability('t1')` — telling the runtime how durably to checkpoint the saga's state across messages. This is distinct from the **saga store backend** (kv \| prisma), which is *where* that state is written. See [Explanation → Durability model](https://rickylabs.github.io/netscript/explanation/durability-model/) and [Capabilities → Durable sagas](https://rickylabs.github.io/netscript/capabilities/durable-sagas/). | | `durable` | `saga state checkpointing` | Describes work that survives process restarts because its progress is persisted, not held only in memory. NetScript's sagas are durable: their state is checkpointed per the chosen durability tier — to the **saga store backend** (kv or prisma) — so a long-running, message-driven workflow can resume after a crash. See [Explanation → Durability model](https://rickylabs.github.io/netscript/explanation/durability-model/). | | `InteractiveFlowPort` | `optional AuthBackendPort member` | The optional sub-port a backend implements when it drives a redirect-based sign-in (the `signIn` / `handleCallback` / `getSessionId` / `signOut` flow). Only the `kv-oauth` backend exposes it; on `workos` and `better-auth` the `signin` / `callback` endpoints return a typed provider error because they have no interactive flow. See [Explanation → Auth model](https://rickylabs.github.io/netscript/explanation/auth-model/). | | `job` | `defineJobHandler(async (ctx) => …)` | A unit of background work authored with `@netscript/plugin-workers-core`, returning `createSuccessResult({...})` or `createFailureResult(...)` and tagged with an id via `Object.assign(handler, { id })`. Jobs run on the workers processor and are triggered over HTTP at `POST /api/v1/workers/jobs/{id}/trigger` on port 8091. Job dispatch and execution are instrumented with real OpenTelemetry spans (see **OTel / observability**). See [Tutorial → Background jobs](https://rickylabs.github.io/netscript/tutorials/erp-sync/) and [Capabilities → Background jobs](https://rickylabs.github.io/netscript/capabilities/background-jobs/). | **M–P** | Name | Type | Description | | --- | --- | --- | | `manifest` | `plugins//mod.ts` | A plugin's public face — its `mod.ts` — which exports the plugin object (for example `workersPlugin` or `authPlugin`), an inspector (`inspectWorkers`, `inspectAuth`), and the contribution types the host discovers. The composition root references each plugin by its manifest path, and a generated **registry** aggregates them. See [Explanation → Plugin system](https://rickylabs.github.io/netscript/explanation/plugin-system/). | | `oRPC` | `@orpc/contract · @orpc/server` | The contract-and-RPC toolkit ([@orpc/*](https://orpc.unnoq.com), pinned at ^1.14.6) that underpins NetScript's typed APIs: contracts are declared with `oc.route().input().output()`, implemented with `implement(...)`, served by `defineService` / `createService`, and consumed by a fully typed client. Workers, sagas, the auth service, and triggers all expose oRPC (mounted under `/api/rpc/*`); triggers' only exception is the raw, HMAC-verifying **webhook ingress endpoint**. See [Explanation → Contracts](https://rickylabs.github.io/netscript/explanation/contracts/). | | `opaque session token (HMAC)` | `createHmacSessionTokenCrypto(secret)` | The default session-token scheme in the auth core — a WebCrypto HMAC-SHA256 token that carries no readable claims (opaque to the client) and is verified server-side against a secret. It is what the `AuthSessionCryptoPort` produces by default, so a stolen token reveals nothing and cannot be forged without the secret. See [Explanation → Auth model](https://rickylabs.github.io/netscript/explanation/auth-model/) and [Capabilities → Auth](https://rickylabs.github.io/netscript/capabilities/auth/). | | `plugin` | `package + generated glue` | An installable, thread-isolated capability. Public workspaces install plugin packages with commands such as `netscript plugin install @netscript/plugin-workers`, which add the dependency and emit user-owned glue/sample files that import the package; local-source contributor samples use `deno run -A packages/cli/bin/netscript-dev.ts plugin install worker --name workers --samples`. The plugin's contracts, services, runtime entrypoints, and Prisma schema stay in the installed package unless you are authoring or syncing full source. See [Explanation → Plugin system](https://rickylabs.github.io/netscript/explanation/plugin-system/) and [How-to → Add a plugin](https://rickylabs.github.io/netscript/orchestration-runtime/how-to/add-a-plugin/). | | `Principal` | `@netscript/service/auth` | The authenticated identity an authenticator resolves from a request — id, scopes, and a `scheme` (`'api-key'` \| `'bearer'` \| `'trusted-header'` \| `'custom'`). The auth-plugin backends map their sessions to a Principal with `scheme: 'custom'`, so service-layer authz treats vendor-backed identities uniformly. See [Explanation → Auth model](https://rickylabs.github.io/netscript/explanation/auth-model/). | **R–S** | Name | Type | Description | | --- | --- | --- | | `registry` | `generated registry file` | A generated index the runtime reads to discover what your app contains without runtime reflection. `netscript plugin list` emits the plugin registry, and the workers profile generates a jobs registry (for example to `.netscript/generated/plugin-workers/job-registry.ts`, keyed by job id). Registries are derived artifacts — regenerate them, do not hand-edit. See [Explanation → Plugin system](https://rickylabs.github.io/netscript/explanation/plugin-system/). | | `saga` | `defineSaga(id)…build()` | A durable, message-driven workflow authored with the fluent builder from `@netscript/plugin-sagas-core`: `defineSaga(id).durability('t1').state({...}).on(type, handler).build()`. Each `.on(...)` handler reacts to a message and returns effects such as `sagaComplete({...})`. Runtime state persists to a chosen **saga store backend** (kv \| prisma). The sagas service lists registered sagas at `/api/v1/sagas/sagas` on port 8092. See [Tutorial → Durable workflow](https://rickylabs.github.io/netscript/tutorials/storefront/04-checkout-saga/) and [Capabilities → Durable sagas](https://rickylabs.github.io/netscript/capabilities/durable-sagas/). | | `saga store backend` | `NETSCRIPT_SAGA_STORE=kv\|prisma` | Where a durable saga runtime persists its checkpointed state — selectable as `'kv'` or `'prisma'` via `NETSCRIPT_SAGA_STORE` (or appsettings `sagas.store.backend`) and constructed with `createDurableSagaRuntime({ backend, prisma })`. The selection is *mandatory* — the runtime throws if it is unset, and Prisma mode requires a client. This is distinct from a saga's **durability tier**. See [Capabilities → Durable sagas](https://rickylabs.github.io/netscript/capabilities/durable-sagas/) and [Explanation → Durability model](https://rickylabs.github.io/netscript/explanation/durability-model/). | | `service` | `defineService(router, {...})` | An oRPC HTTP application. Local app services use a one-shot `defineService(router, { name, version, port, openapi })` call (the `users` service on port 3001), while plugin API services use the fluent `createService(router, {...}).withCors()…serve()` builder — two construction APIs in the same project. oRPC is served at `/api/rpc/*`; the service layer also offers an authn/authz middleware seam (`.withAuthn()` / `.withAuthz()`). See [Capabilities → Services](https://rickylabs.github.io/netscript/capabilities/services/) and [reference/service/](https://rickylabs.github.io/netscript/reference/service/). | | `single-active-backend` | `NETSCRIPT_AUTH_BACKEND` | The hard v1 boundary of the auth plugin: exactly one authentication backend is active at a time — `kv-oauth` (default), `workos`, or `better-auth` — chosen by `NETSCRIPT_AUTH_BACKEND` (or appsettings `auth.backend`); the unified auth oRPC service answers on port `:8094`. There is no multi-active routing, cross-backend account linking, global logout, or historical session replay yet. See [Capabilities → Authentication](https://rickylabs.github.io/netscript/capabilities/auth/) and [Explanation → Auth model](https://rickylabs.github.io/netscript/explanation/auth-model/). | | `stream` | `createDurableStream({...})` | A typed, durable event-stream abstraction. The **producer runtime is real**: `createDurableStream({ streamPath, schema, producerId })` from `@netscript/plugin-streams-core` writes durable entity state, served as an Aspire service on port 4437 and wired into workers, auth, and sagas. The `@netscript/plugin-streams` manifest helpers `defineStreamProducer` / `defineStreamConsumer` are intentional stubs that *throw* `StreamUnsupportedOperationError` and redirect you to the core; there is no in-process consumer `subscribe()` (consumption is HTTP/SSE). See [Capabilities → Streams](https://rickylabs.github.io/netscript/capabilities/streams/) and [reference/streams/](https://rickylabs.github.io/netscript/reference/streams/). | **T–Z** | Name | Type | Description | | --- | --- | --- | | `trigger` | `defineWebhook(handler, {...})` | An inbound event source — most commonly a webhook — authored with `defineWebhook(handler, { id, path, verifier, tags })` from `@netscript/plugin-triggers-core/builders`. The handler returns an array of effects binding an HTTP request to background work. The **supported action is** `enqueueJob(jobRef, { payload, priority })` (live); `defer` is defined-but-unsupported — it *throws* and routes the message to the DLQ, with no deferred replay. Like workers and sagas, the triggers service serves a **typed v1 oRPC contract** for trigger and event introspection plus management on port 8093; the one exception is the raw, HMAC-verifying **webhook ingress endpoint** (`POST /api/v1/webhooks/:triggerId`). See [Tutorial → Ingest a webhook](https://rickylabs.github.io/netscript/tutorials/storefront/05-shipping-webhook/) and [Capabilities → Triggers](https://rickylabs.github.io/netscript/capabilities/triggers/). | ## Less common, still load-bearing A second tier of words you will meet once you go past the happy path — the toolchain, the data layer, the auth seams, and the moving parts under a running plugin. **Infrastructure, data & auth internals** | Name | Type | Description | | --- | --- | --- | | `appsettings.json` | `infra config` | The root infrastructure manifest the Aspire **AppHost** actually reads — declaring `NetScript.Databases` (the database engine — Postgres is the recommended engine, or `mysql` / `mssql` / `sqlite` selected at scaffold with `--db` — container mode, primary database), `NetScript.Cache` (the `redis` cache by default; `garnet` or `deno-kv` via `--cache-backend`), the services, and the per-plugin `Workdir`s. It is also where backend selections like `sagas.store.backend` and `auth.backend` can live. Note that `netscript.config.ts`'s `databases` block is intentionally near-empty; the live DB/cache config lives here. See [Explanation → Aspire](https://rickylabs.github.io/netscript/explanation/aspire/). | | `auth backend` | `@netscript/auth-*` | A pure adapter that implements **AuthBackendPort**: `kv-oauth` (interactive OAuth/OIDC redirect flow, KV sessions — the only one exposing **InteractiveFlowPort**), `workos` (AuthKit sealed cookie, non-interactive), and `better-auth` (Prisma-backed, non-interactive). The plugin composes exactly one — see **single-active-backend** — and serves the unified auth oRPC surface on port `:8094`. See [Explanation → Auth model](https://rickylabs.github.io/netscript/explanation/auth-model/) and [How-to → Add authentication](https://rickylabs.github.io/netscript/identity-access/how-to/add-authentication/). | | `background processor` | `bin/combined.ts` | The separate, long-running process that executes a plugin's work, distinct from its HTTP API service. Workers run from `bin/combined.ts`; sagas run from `src/runtime/saga-runner.ts`; triggers run from `src/runtime/trigger-processor.ts`. The AppHost starts these as their own Aspire resources. See [Explanation → Plugin system](https://rickylabs.github.io/netscript/explanation/plugin-system/). | | `Garnet` | `Cache.garnet (KV)` | One of the Redis-compatible cache/KV backends Aspire can provision as a container alongside Postgres (the default is `redis`; select Garnet with `--cache-backend garnet`), used for executions, saga registry metadata, kv-oauth sessions, and Deno KV-backed state. See [Capabilities → KV / queues / cron](https://rickylabs.github.io/netscript/capabilities/kv-queues-cron/) and [reference/kv/](https://rickylabs.github.io/netscript/reference/kv/). | | `OTel / observability` | `@netscript/telemetry` | OpenTelemetry tracing and structured logging wired into the framework. Job dispatch, execution, step events, progress, scheduler runs, and subprocess trace continuation emit *real* spans — traces show up in Aspire automatically (OTLP endpoint at http://localhost:4318). The one remaining gap is the scaffold `createJobTools(ctx)` handler helpers (`trace.addEvent` / `withChildSpan` / `progress`), which are still no-op stubs (tracked debt, fix planned) — for custom handler spans call `@netscript/telemetry` helpers directly. See [Explanation → Observability](https://rickylabs.github.io/netscript/explanation/observability/) and [Capabilities → Telemetry](https://rickylabs.github.io/netscript/capabilities/telemetry/). | | `Prisma (Deno runtime)` | `database/postgres/schema/` | The ORM layer, backed by a polyglot database. The engine is chosen at scaffold time with `--db` — `postgres` (the recommended default; Prisma provider `postgresql`), `mysql`, `mssql` (provider `sqlserver`), or `sqlite` (file-backed, with no Aspire container resource) — while the Prisma authoring model stays identical across engines. The root schema sets `generator client { runtime = "deno" }` plus a zod generator, and each plugin contributes its own `.prisma` models, aggregated under `schema/plugins//` (the auth plugin installs `auth_users` / `auth_sessions` / `auth_accounts` / `auth_verifications`). The typed client is generated with `netscript db generate` after Aspire is up. See [Capabilities → Database](https://rickylabs.github.io/netscript/capabilities/database/) and [reference/database/](https://rickylabs.github.io/netscript/reference/database/). | | `queue provider` | `QueueProvider.Postgres` | The transport a `createQueue('name', { provider })` binds to — one of **four** backends: Deno KV, Redis, RabbitMQ, and PostgreSQL (`provider: 'postgres'`, `connection.postgres.{url,tableName}`). Postgres is selectable by *explicit provider only*; auto-discovery probes RabbitMQ → Redis → Deno KV and never picks Postgres. See [Capabilities → KV / queues / cron](https://rickylabs.github.io/netscript/capabilities/kv-queues-cron/) and [reference/queue/](https://rickylabs.github.io/netscript/reference/queue/). | ## Where each term is taught The glossary lowers lookup cost; these zones build the understanding. Follow a word into the zone that matches what you need next. [Explanation Mental models for contracts, the plugin model, durable workflows, the auth model, observability, and Aspire — the why behind the vocabulary.](https://rickylabs.github.io/netscript/netscript/explanation/) [Capabilities One hub per capability: concept, headline API, and a Learn / Do / Reference triplet for services, jobs, sagas, triggers, streams, authentication, and more.](https://rickylabs.github.io/netscript/netscript/capabilities/) [Reference The generated @netscript/* API — the authoritative export list for every symbol named above, including the plugin-auth-core port surface.](https://rickylabs.github.io/netscript/netscript/reference/) [Tutorials One continuous app that introduces these terms in order: contract, service, job, saga, then trigger.](https://rickylabs.github.io/netscript/netscript/tutorials/) > Still stuck on a word? > > If a term here is unfamiliar, start with > > Explanation → Architecture > > for the big picture, then jump to the matching > > capability hub > > to see the headline API in context. The > > CLI reference > > covers every command name a glossary entry mentions. [CLI reference](https://rickylabs.github.io/netscript/netscript/cli-reference/) _Canonical: https://rickylabs.github.io/netscript/glossary/_ --- # Quickstart From nothing to a running NetScript workspace you can watch boot — in about five minutes. Three commands: **install** the CLI, **scaffold** a project, then bring it up under **Aspire**, which starts Postgres and a shared cache (`redis` by default, or `garnet` / `deno-kv` via `--cache-backend`) for you *before* anything else and hands you a real dashboard, distributed traces, and a handful of example routes on the very first run. > Prerequisites > > You need > > [Deno](https://docs.deno.com) 2.x > > on your PATH (check with > > deno --version > > ). The default path also uses > > [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/) > > to orchestrate Postgres, Redis, and your services — install the > > aspire > > CLI if you don't have it, or skip orchestration entirely with > > --no-aspire > > (see the aside in step 3). > Beta > > NetScript is in beta. Public package versions are > > 0.0.1-beta.11 > > and the API is subject to change as it ships incrementally. Pin versions in real projects. 1. [Install the CLI](https://rickylabs.github.io/netscript/netscript/) 2. [Scaffold a workspace](https://rickylabs.github.io/netscript/netscript/) 3. [Start it under Aspire](https://rickylabs.github.io/netscript/netscript/) ## 1. Install the CLI The CLI is published to JSR as `@netscript/cli`. Install it globally for a tidy `netscript` command, or run it ad-hoc with no install at all. ```bash # Installs a `netscript` command on your PATH deno install --global --allow-all --name netscript jsr:@netscript/cli@0.0.1-beta.11 netscript --help ``` ```bash # Run the same CLI without installing anything deno x jsr:@netscript/cli@0.0.1-beta.11 --help ``` > The rest of this page assumes the global `netscript` command. If you went ad-hoc, swap `netscript` for `deno x jsr:@netscript/cli@0.0.1-beta.11` in each step. ## 2. Scaffold a workspace One command lays down the whole opinionated workspace — a Fresh 2 app, shared oRPC contracts, a plugin registry, and the Aspire orchestration layer: ```bash netscript init my-app --db postgres ``` > Pick your database engine > > Postgres is the recommended default shown throughout these docs. The database is polyglot — swap > > --db postgres > > for > > mysql > > , > > mssql > > , or > > sqlite > > to scaffold a Prisma-backed schema for that engine instead. ( > > postgres > > / > > mysql > > / > > mssql > > run as an Aspire container resource; > > sqlite > > is file-backed with no container.) > Want to see the plan before anything touches disk? Add `--dry-run` — `netscript init my-app --db postgres --dry-run` previews every file it would create and writes nothing. You'll see a banner, a creating step, and a success summary with numbered next steps. It looks like this: ```text ╔═════════════════════════════════════════════════════════════╗ ║ NetScript — Scaffold New Project ║ ╚═════════════════════════════════════════════════════════════╝ 📁 Creating project "my-app"... ✅ Project scaffolded successfully in 1.4s Created: 47 files, 12 directories Next steps: 1. cd my-app 2. cd aspire # TS AppHost lives here, isolated from the Deno workspace 3. aspire restore # download TypeScript AppHost SDK modules (run once) 4. aspire start # start TypeScript AppHost ``` If you see that success line and the numbered steps, the scaffold worked. The exact file count and the next steps adapt to the options you chose (a database, an example service, and so on) — on the strict happy path above, the four steps are sufficient. > Aspire comes before any database command > > On the default path, > > Aspire is step 2 of running your app > > : > > cd aspire && aspire start > > provisions Postgres and Redis first. Any > > netscript db > > command (init, generate, seed) runs > > after > > Aspire is up, because it needs that database to exist. See > > Database migrations > > for the full flow. ## 3. Start it Follow the next steps the CLI just printed. The Aspire TypeScript AppHost lives in its own `aspire/` folder, isolated from the Deno workspace; restore its modules once, then run it. **This is the step that actually starts your stack** — Postgres, Redis, and every wired service come up together: ```bash cd my-app/aspire aspire restore # one-time: downloads the AppHost SDK modules aspire start # boots Postgres + Redis + services, opens the dashboard ``` > Prefer no orchestration? > > Scaffold with > > netscript init my-app --no-aspire > > and start the Fresh app directly instead: > > deno task --cwd apps/dashboard dev > > . You trade the dashboard, Postgres/Redis provisioning, and multi-resource wiring for a leaner single-process dev loop. ### See the framework code The scaffold is more than bash and URLs — it generates real, typed framework code for you. Here's the kind of code the scaffold writes: a single `defineService` call wires the cross-cutting concerns so you don't have to. Your RPC procedures are then served under `/api/rpc/*`. ```ts import { defineService } from '@netscript/service'; import { router } from './router.ts'; // One call wires CORS, request logging, OpenAPI, RPC (/api/rpc/*), and health. const service = await defineService(router, { name: 'users', port: 3001 }); ``` ```bash # oRPC procedures are served under /api/rpc/* curl http://localhost:3001/api/rpc/users/list # Health and OpenAPI come for free curl http://localhost:3001/health ``` > Fluent alternative > > Prefer a builder? The fluent equivalent of > > defineService(...) > > is the explicit chain > > createService(router, config).withCors().withLogger().withOpenAPI().withDocs().withRPC().withServiceInfo().withHealth().serve() > > — each > > .withXxx() > > adds one layer of the wiring that > > defineService > > bundles, including the > > /api/rpc/* > > mount (added by > > .withRPC() > > ). Calling > > createService(...).serve() > > with no chaining produces a bare app: no RPC handler, no CORS, no health. See > > the Storefront catalog-service chapter > > . ## What you see Once `aspire start` settles, you have a live workspace to poke at: - **The Aspire dashboard** at [https://localhost:18888](https://localhost:18888) — every resource (Postgres, Redis, your services), its health, logs, and distributed traces in one place. - **The Fresh app** at [http://localhost:8000](http://localhost:8000) — your frontend, served by the `dashboard` app. - **The `/design` route** — a living token-and-component showcase: the `--ns-*` design tokens, the fresh-ui components copied into *your* repo, and composition examples. Click a token to copy it. - **The `/examples` routes** — runnable end-to-end paths: `/examples/crud`, `/examples/service`, and `/examples/telemetry`, plus a `/health` check. > If something doesn't come up > > - A port is already in use — `18888` (dashboard) or `8000` (Fresh app). Stop the other process or change the port. > - The first `aspire restore` downloads the AppHost SDK and can take a minute. > - If a `netscript db` command can't connect, confirm `aspire start` is still up — Postgres lives inside Aspire. > - `deno: command not found` means Deno isn't on your PATH. > Start at /design > > The scaffolded > > /design > > showcase is the fastest way to feel what you have: the tokens are real CSS custom properties, and the components are source you own and can edit on the spot. Open > > http://localhost:8000/design > > . ## You now have A complete, type-checked workspace — not a starter you have to assemble: A running app + dashboard A Fresh 2 frontend and the Aspire dashboard, wired together with health, logs, and traces from line one. A database + Redis, provisioned Aspire stands up the database (Postgres is the recommended engine; or mysql / mssql / sqlite via --db) and cache for you — no docker-compose to babysit, no connection strings to copy. Contracts and a plugin registry Shared, versioned oRPC contracts and a plugin registry ready for workers, sagas, triggers, auth, and streams. UI you own fresh-ui components copied into your repo under apps/dashboard — the code is yours to change. ### Next steps > Start here > > New to NetScript? Work through > > the Storefront tutorial > > — a guided track that takes this scaffold to a real feature. [Build the Storefront A guided, end-to-end track that takes this scaffold to a real storefront — services, a durable checkout, and a webhook.](https://rickylabs.github.io/netscript/netscript/tutorials/storefront/) [Build a service Define an oRPC contract, implement it, and serve it under /api/rpc/* with one call.](https://rickylabs.github.io/netscript/netscript/tutorials/storefront/02-catalog-service/) [Capabilities Durability model, background jobs, observability, auth, plugins, and Aspire integration — what NetScript does and why.](https://rickylabs.github.io/netscript/netscript/capabilities/) [Reference The full CLI surface and the @netscript/* package APIs.](https://rickylabs.github.io/netscript/netscript/reference/) [Tutorial — Build the Storefront](https://rickylabs.github.io/netscript/netscript/tutorials/storefront/) _Canonical: https://rickylabs.github.io/netscript/quickstart/_ --- # Aspire quickstart A NetScript workspace is never one process — it is a Fresh app, oRPC services, plugin APIs, background processors, a database, and a cache. Aspire is how we make that whole fleet start with **one command**, wired together, with a real dashboard from the first run. This page is the shortest path to seeing it; the [main Quickstart](https://rickylabs.github.io/netscript/quickstart/) covers installing the CLI and the scaffold in more detail. > Alpha > > NetScript is alpha software and the API is subject to change. Pin versions in real projects. ## What you get - **One workspace, one command up.** `aspire start` boots the database, the cache, every service, every plugin API, and every background processor — in dependency order, no docker-compose to babysit. - **Multi-resource wiring, resolved for you.** Connection strings and neighbour endpoints are computed and injected as environment variables before each process starts, so nothing has to discover anything at runtime. - **The Aspire dashboard.** Live resource list, per-process console logs, and distributed traces in one place — `aspire start` prints its URL and a one-time login token. - **A TypeScript AppHost — not .NET authoring.** The orchestrator entry point is a generated TypeScript program at `aspire/apphost.mts`, running on an isolated Node runtime inside `aspire/` so it never leaks into your Deno workspace. You write no C#. > Prerequisites > > [Deno](https://docs.deno.com) 2.x > > and the > > netscript > > CLI (install steps in the > > Quickstart > > ), the external > > [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/) CLI > > , and a running > > Docker > > daemon — Aspire provisions Postgres and Redis as local containers. ## The commands Scaffold a workspace, then bring it up. The Aspire layer lives in its own `aspire/` folder; restore its SDK modules once, then start: ```bash netscript init my-app --db postgres cd my-app/aspire aspire restore # one-time: downloads the AppHost SDK modules aspire start # boots Postgres + Redis + services, prints the dashboard URL ``` When boot settles, open the dashboard URL `aspire start` printed (conventionally `https://localhost:18888`) and paste the login token. Every resource, its logs, and its traces are one click away. > Database commands come after > > netscript db init > > , > > db generate > > , and > > db seed > > run from the > > workspace root > > only once > > aspire start > > is up — they provision the database > > through > > the running AppHost. With no Aspire up there is no Postgres for them to reach. ## Prefer no orchestration? Aspire is the default, not a requirement. Scaffold with `--no-aspire` to skip the orchestration layer entirely — no `aspire/` folder, no dashboard — and start the Fresh app directly: ```bash netscript init my-app --db postgres --no-aspire deno task --cwd apps/dashboard dev ``` You take over infrastructure and wiring yourself: bring your own Postgres and cache, hand each process its connection strings. When that trade is the right call — and what exactly you give up — is covered in [Orchestration with Aspire](https://rickylabs.github.io/netscript/netscript/explanation/aspire/). ## Where next - **Step-by-step recipe:** [Deploy locally with Aspire](https://rickylabs.github.io/netscript/netscript/orchestration-runtime/how-to/deploy-local-aspire/) — the full local flow, including the database sequence and first-run footguns. - **Why it works this way:** [Orchestration with Aspire](https://rickylabs.github.io/netscript/netscript/explanation/aspire/) — the AppHost, plugin contributions, and the resource graph. - **Exact symbols and the port map:** [the Aspire reference](https://rickylabs.github.io/netscript/netscript/reference/aspire/) and the [CLI reference](https://rickylabs.github.io/netscript/netscript/cli-reference/). _Canonical: https://rickylabs.github.io/netscript/quickstart/aspire/_ --- # You shouldn't have to assemble a backend from a dozen libraries that have never met. NetScript is one coordinated workspace where the contract you define is the client you call, workflows survive crashes by design, and tracing is wired in from line one. [Quickstart — 5 min](https://rickylabs.github.io/netscript/netscript/quickstart/) [Browse the API reference](https://rickylabs.github.io/netscript/netscript/reference/) > Where we are > > NetScript is in > > beta > > ( > > 0.0.1 > > ), shipping incrementally. The API is subject to change — so build with us, but pin your versions. The published package surface is the contract: what you import from > > jsr:@netscript/* > > is what's documented and type-checked. ## The problem The short version: we ship the boring integration seams pre-fitted and type-checked, so you spend your time on product instead of plumbing. That's the answer the rest of this page earns. A real TypeScript backend is rarely one decision. It's a dozen, made on different days, by different libraries that were never designed to sit in the same repo: 1. **A queue here, a tracer there.** You pick a job runner, then a separate OpenTelemetry setup, then glue them together so a job actually shows up in a trace. The glue is yours to maintain forever. 2. **A scaffold script that rots.** Someone wrote a `create-app` once. Two refactors later it generates code nobody recognizes, and updating it is a project of its own. 3. **A `docker-compose.yml` and a prayer.** Your database, your app, your worker, your dashboard — all hand-wired, all drifting from how you actually deploy. 4. **A DI container you didn't want.** To make any of the above testable, you reach for a dependency-injection framework, and now wiring is a layer of indirection instead of a function call. 5. **Drift between the API and the client.** The service returns one shape; the client believes another. You find out at runtime, in production, from a user. Codegen helps until the generated types and the hand-written ones disagree. 6. **Durability model faked with retries.** "It'll just retry" is not a state machine. There's no correlation, no compensation, no record of where a multi-step flow actually stopped when the process died. 7. **Auth bolted on at the end.** You wire OAuth, sessions, and a provider SDK by hand, then discover the provider you chose is the one you now can't swap without a rewrite. None of these are hard problems individually. The cost is the **integration tax** — the standing maintenance of seven unrelated tools pretending to be one system. ## The NetScript answer We didn't invent new primitives. We made a set of opinions about how the boring parts fit together, and shipped them as one coordinated package family plus a CLI that scaffolds the whole workspace. Each value below is a direct answer to a pain above. **What you'd hand-assemble → what NetScript gives you** | Name | Type | Description | | --- | --- | --- | | `Type-safe to the edges` | `API ↔ client drift` | The contract you import is designed, documented, and enforced. oRPC contracts flow from service to client to UI with no codegen step to drift. | | `Predictable builder surface` | `DI containers & glue` | The common path is one chained call. Advanced options live on named builder methods, without hidden container wiring. | | `Standards-first` | `bespoke abstractions` | Built on fetch, URL, streams, and @std/* — skills you already have, not a private runtime to relearn. | | `Durable by design` | `retries pretending to be workflows` | Sagas, triggers, and jobs are explicit state machines with correlation, persistence, and compensation. They survive crashes; failure handling is named, not bolted on. | | `Observable by default` | `a tracer you wire yourself` | OpenTelemetry tracing, structured logs, and health probes are wired into jobs, queues, RPC, and SSE from the first line. Job dispatch, execution, scheduling, and subprocess spans appear in the dashboard automatically. | | `Orchestrated out of the box` | `docker-compose & a prayer` | One workspace, many resources, a real dashboard — local and deployed — via .NET Aspire integration. Opt out with --no-aspire. | | `Composable` | `a scaffold script that rots` | Add workers, sagas, triggers, streams, and auth in any combination. The host never changes; plugins register contributions. | | `Auth as a swappable seam` | `a provider you can't change later` | A pure-backend auth plugin defines the port; pick one backend — KV-OAuth, WorkOS, or better-auth — behind a single switch, without rewriting your service. | ## What makes it different Each differentiator below is paired with a runnable TypeScript path from the public package surface. ### 1. Contract-first, type-safe end to end Define a contract once. The client is *derived* from it — no codegen, no second source of truth. Change the contract and the type checker becomes your integration test: every caller that no longer matches fails to compile. ```ts // This replaces a hand-maintained OpenAPI spec + generated client. import { baseContract, OffsetPaginationMetaSchema, OffsetPaginationQuerySchema, } from '@netscript/contracts'; import { z } from 'zod'; export const usersContract = { list: baseContract .route({ method: 'GET', path: '/users' }) .input(OffsetPaginationQuerySchema) .output(z.object({ items: z.array(z.object({ id: z.string(), email: z.string() })), pagination: OffsetPaginationMetaSchema, })), }; ``` ```ts // The client is derived from the contract above — not generated, not duplicated. import { defineServices } from '@netscript/sdk'; import { usersContract } from './contracts/users.ts'; const { clients } = defineServices({ users: { contract: usersContract }, }); // `result` is fully typed from the contract's output schema. const result = await clients.users.list({ limit: 20, offset: 0 }); console.log(result.items, result.pagination); ``` ### 2. Durability model by design A saga is an explicit state machine, not a retry loop. You declare its state, the messages it reacts to, and what each handler emits — including completion and failure as first-class outcomes. It's authored in plain TypeScript builders, but the model is closer to Temporal than to a job queue. Runtime state persists to a durable store you choose — `kv` or `prisma` — so a saga in flight survives a process restart. ```ts // This replaces an ad-hoc 'mark paid, then hope the retry fires' flow. import { defineSaga, sagaComplete } from '@netscript/plugin-sagas-core'; const orderSaga = defineSaga('order') .state({ paid: false }) .on('order.paid', (saga, _event, context) => { saga.state.paid = true; // Completion is an explicit, recorded outcome — not a fall-through. return [sagaComplete({ orderId: context.sagaId })]; }) .build(); ``` ```ts // Handlers return named effects: send, schedule, complete, or fail. import { defineSaga, send, sagaFail } from '@netscript/plugin-sagas-core'; const checkout = defineSaga('checkout') .state({ attempts: 0 }) .on('payment.attempted', (saga, event, context) => { saga.state.attempts += 1; if (saga.state.attempts > 3) { return [sagaFail('payment retries exhausted')]; } // Fan out the next step as a correlated message. return [send('payment.charge', { orderId: context.sagaId })]; }) .build(); ``` ### 3. Observable by default Tracing isn't a follow-up ticket. Spans wrap the work itself: `withSpan` runs your handler inside a span, marks it OK or records the exception on throw, and closes it for you — so a service handler is traced the moment it exists. Background jobs go further: dispatch, execution, scheduling, and the subprocess that runs a job are span-wrapped automatically, and the trace context propagates into the subprocess — so a job shows up in the Aspire dashboard end to end without you wiring a thing. ```ts // This replaces manual span start/end + try/catch/recordException boilerplate. import { getTracer, withSpan } from '@netscript/telemetry/tracer'; const tracer = getTracer('users'); export const chargeOrder = async (orderId: string) => { // The span is opened, status-tracked, and closed around your work. return await withSpan(tracer, 'order.charge', async (span) => { span.setAttribute('order.id', orderId); const receipt = await processPayment(orderId); return receipt; // span auto-marked OK; a throw is recorded + re-raised. }); }; ``` ```ts // defineService wires CORS, logging, OpenAPI, RPC, and health in one call — // and your traced handlers run inside it with no extra plumbing. import { defineService } from '@netscript/service'; import { router } from './router.ts'; const service = await defineService(router, { name: 'users', port: 3000, }); // ... handlers like chargeOrder() above are already instrumented. await service.stop(); ``` ### 4. Orchestrated with Aspire Your database, services, workers, and a real dashboard come up together — locally and on the way to deploy — through NetScript's .NET Aspire integration. The flow is two commands: `cd aspire && aspire start` brings up your database container (Postgres is the recommended engine — or `mysql` / `mssql` via `--db`; `sqlite` is file-backed and needs no container) and Redis and the dashboard *before* any `netscript db` command touches the database. One workspace wires the resources; you get the Aspire dashboard (`:18888`) for traces and logs without standing up your own. Don't want the .NET footprint? `netscript init --no-aspire` and it's gone. ### 5. Composable plugins Workers, sagas, triggers, streams, and auth are plugins. You add them in any combination and the host application never changes — each one registers its contributions rather than asking you to edit a central wiring file. The saga in proof #2 *is* a plugin contribution; so is the auth service in proof #7. Each official plugin runs as its own Aspire service on its own port — workers `:8091`, sagas `:8092`, triggers `:8093`, auth `:8094`, streams `:4437` — so adding one is additive, never a rewire. ### 6. You own your UI fresh-ui is copy-source. The CLI copies the components into your repo, and from that moment the code is yours to read, fork, and evolve. There's no black-box component dependency to fight when you need a different border-radius. (`@netscript/fresh` — the meta-framework with server, islands, and query subpaths — is a separate, real package; the scaffolded Fresh UI app is the copy-source layer you own outright.) ### 7. Auth as a swappable backend Authentication is a plugin like any other, and its defining opinion is that the *backend is pure*. `@netscript/plugin-auth-core` defines an `AuthBackendPort`; the three backends — `@netscript/auth-kv-oauth` (the full interactive OAuth/OIDC flow), `@netscript/auth-workos`, and `@netscript/auth-better-auth` — are pure adapters behind it. `@netscript/plugin-auth` composes exactly **one** active backend, chosen by `NETSCRIPT_AUTH_BACKEND` (default `kv-oauth`), and exposes an `auth-api` oRPC service on `:8094` with five endpoints: `signin`, `callback`, `signout`, `session`, and `me`. Swap providers by changing a switch, not your service. ```ts // The interactive backend: a full OAuth/OIDC redirect flow with KV sessions. import { createKvOAuthBackend, providers } from '@netscript/auth-kv-oauth'; const backend = await createKvOAuthBackend({ provider: providers.google({ clientId, clientSecret, redirectUri }), }); // One active backend at a time; selected by NETSCRIPT_AUTH_BACKEND. // Swap to 'workos' or 'better-auth' without touching your service. ``` > Framework beta > > NetScript packages share the aligned > > 0.0.1-beta.11 > > version, and scaffolded JSR imports pin that exact beta train. Only > > kv-oauth > > is fully interactive — on WorkOS and better-auth the > > signin > > / > > callback > > endpoints return a typed unsupported-operation error by design. It's one active backend at a time: no multi-active routing, cross-backend linking, or global logout yet. See > > the auth capability hub > > for the full surface. ## Compared to assembling it yourself You can build all of this by hand — most teams have. The comparison that matters isn't NetScript versus any one library; it's NetScript versus the standing cost of integrating several. We're deliberate about what we wrap and what we are *not*: **How NetScript compares** | Name | Type | Description | | --- | --- | --- | | `vs. wiring it yourself` | `the baseline` | You keep full control but own every integration seam — queue↔tracer, scaffold, compose file, client drift, auth provider — indefinitely. NetScript trades a slice of that control for those seams being pre-fitted and type-checked. | | `NestJS` | `opinionated backend framework` | An opinionated, DI-first Node backend framework. NetScript is Deno-native and JSR-distributed, leads with oRPC contract-first typing end to end, and treats durable workflows as first-class — not a backend framework you bolt a workflow engine onto. | | `Encore` | `infra-from-code backend framework` | An infra-from-code Go/TS backend framework. NetScript is Deno-native and JSR-distributed, leads with oRPC contract-first typing end to end, and treats durable workflows as first-class — not a backend framework you bolt a workflow engine onto. | | `tRPC-style stacks` | `end-to-end typing` | NetScript shares the typed-edge goal via oRPC, then adds the rest of the system around it: services, jobs, sagas, streams, auth, orchestration, and observability in one workspace. | | `Temporal` | `durable workflow engine` | Our sagas borrow the state-machine, correlation, and compensation ideas — authored in plain TS builders inside your app, not a separate cluster you operate. | | `Hono` | `HTTP foundation` | We wrap it, not replace it. defineService stands up a Hono/oRPC runtime; you're never cut off from the underlying framework. | NetScript is the opinion that these pieces should arrive already fitted — and the published surface is the product, so what you import is what's documented and type-checked. > When NetScript is NOT the right tool > > - **You need a stable, frozen API today.** We're in beta; surfaces still move. If churn is unacceptable, wait for a later beta or pin hard. > - **You can't take a .NET dependency.** The default scaffold integrates .NET Aspire for orchestration. You can [`--no-aspire`](https://rickylabs.github.io/netscript/explanation/aspire/), but if even the option offends your platform constraints, weigh that first. > - **You want a frontend framework or a hosted PaaS.** NetScript is backend-scoped: services, workflows, jobs, streams, auth, and a generated workspace you run yourself. It ships a Fresh UI, but it is not a React-style frontend framework, and it does not host anything for you. ## Convinced enough to try it? The fastest way to feel the difference is to generate a workspace and watch it come up. From there, the [capabilities](https://rickylabs.github.io/netscript/capabilities/) map the system feature by feature, and the [API reference](https://rickylabs.github.io/netscript/reference/) is generated from the published surface — never duplicated by hand. [Quickstart — 5 minutes](https://rickylabs.github.io/netscript/netscript/quickstart/) _Canonical: https://rickylabs.github.io/netscript/why/_ --- # Tutorials Tutorials are for **learning by building**. Each one walks a fixed path from an empty directory to a running application — no step skipped, every rung proven by a real command or endpoint. You don't need to understand the whole framework before you start; each track introduces its capabilities in the order you'd actually reach for them. Unlike the [how-to guides](https://rickylabs.github.io/netscript/how-to/), which assume you already know the shape of the task, a tutorial follows one continuous example and never leaves you guessing what to do next. > How the four lanes fit together > > Tutorials teach you a path end to end. When you already know the path and just need the recipe, use the > > how-to guides > > . For exact symbols and signatures, go to the > > reference > > . For the design reasoning behind durability, contracts, and plugins, read the > > explanation > > pages. ## Five tracks, five applications There are five independent tracks. **Each builds one complete application** from a fresh `netscript init`, and each ends by running that application **locally under .NET Aspire** — so whichever you pick, you finish with something that boots, serves, and survives a restart. The tracks don't depend on each other; start with the one closest to what you're building. [Storefront Build an e-commerce backend: a typed catalog service, contract-first cart, a durable `checkout` saga with compensation, and an HMAC-verified shipping webhook. The track for **services + durable workflows**. 6 chapters.](https://rickylabs.github.io/netscript/netscript/tutorials/storefront/) [Team Workspace Build an authenticated SaaS backend: add a pluggable auth backend and session, model per-plugin data across a second database, run a provisioning job, and protect routes with the `.withAuthz()` seam. The track for **auth + access control**. 6 chapters.](https://rickylabs.github.io/netscript/netscript/tutorials/workspace/) [ERP Sync Build a legacy-ERP migration sync: watch for incoming data files, ingest them with durable jobs, run a sandboxed transform task, and add a queue provider and a cron schedule. The track for **jobs, queues & polyglot**. 5 chapters.](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/) [Live Dashboard Build a real-time UI that stays current with no polling and no hand-rolled WebSocket: go from a typed contract to an SDK client, a cache-first query, a Fresh `definePage` with a hydrated `QueryIsland`, and finally a durable StreamDB feed that pushes updates into the table live. The track for **the typed-end-to-end Fresh + SDK stack**. 6 chapters.](https://rickylabs.github.io/netscript/netscript/tutorials/live-dashboard/) [AI Chat Build a durable AI chat app whose transcript, streaming markdown, tool-call cards, and MCP widgets survive reload and reconnect: wire a durable chat route on `@netscript/fresh/ai`, hydrate the `fresh-ui` chat components, add a server-side tool, connect a remote MCP server, and turn the island live. The track for **durable AI chat**. 6 chapters.](https://rickylabs.github.io/netscript/netscript/tutorials/chat/) ## Not sure which to pick? **Choose by what you're building** | Name | Type | Description | | --- | --- | --- | | `An API with multi-step business logic` | `Storefront` | You need typed services and a workflow that can't half-complete — orders, payments, fulfillment. Teaches contracts, defineService, sagas, and webhooks. | | `An app behind a login` | `Team Workspace` | You need authentication, sessions, and route-level access control before anything else. Teaches the auth backend, session crypto, and the .withAuthz() seam. | | `Data pipelines and scheduled work` | `ERP Sync` | Your work happens off the request path — file ingestion, batch jobs, scheduled syncs, and tasks in other languages. Teaches triggers, jobs, queues, cron, and the task runtime. | | `A live, reactive frontend` | `Live Dashboard` | You're rendering server data in a Fresh UI that stays current without a refresh. Teaches the SDK client, cache-first queries, the page builder, islands, and durable streams. | | `A durable AI chat app` | `AI Chat` | You're building a chat UI whose transcript, streaming markdown, tool-call cards, and MCP widgets survive reload and reconnect, with replies that stream in live. Teaches the durable chat route on @netscript/fresh/ai, the fresh-ui chat components, a server-side tool, the MCP client stack, and the live subscription. | New to NetScript entirely? Any track starts from zero, but **Storefront** is the broadest tour of the core ideas — to inspect the whole shape of a NetScript backend, start there. ## Before you start Every track assumes a working local toolchain. If you have never run NetScript on this machine, the [quickstart](https://rickylabs.github.io/netscript/quickstart/) installs the CLI and gets a project up in a few commands; each track's first chapter then re-grounds you from the scaffold, so you can start in either place. > What you'll need > > A recent > > Deno > > and the > > .NET Aspire > > CLI on your PATH. Install the NetScript CLI with > > deno install --global --allow-all --name netscript jsr:@netscript/cli@0.0.1-beta.11 > > . Each chapter lists its own prerequisite state, so you always know which earlier chapters it builds on. ## When you finish Once you've completed a track, branch out into the rest of the docs: [How-to guides Task-focused recipes for things the tutorials don't cover — discovering services, exposing OpenAPI, choosing a queue provider, second databases, and production pitfalls.](https://rickylabs.github.io/netscript/netscript/how-to/) [Capabilities One hub per capability (services, workers, sagas, triggers, streams, auth) with the headline API, ports, and endpoints on one screen.](https://rickylabs.github.io/netscript/netscript/capabilities/) [Reference Generated, always-current API surface for every `@netscript/*` unit — exact symbols, signatures, and types.](https://rickylabs.github.io/netscript/netscript/reference/) [Explanation The design reasoning behind contracts-first services, durable execution, the plugin model, and the local Aspire topology.](https://rickylabs.github.io/netscript/netscript/explanation/) _Canonical: https://rickylabs.github.io/netscript/tutorials/_ --- # AI Chat This track builds one thing end to end: a **durable AI chat app** — think a production support-chat surface, the kind of assistant a real product ships next to its docs. By the last chapter you will have a chat whose transcript — messages, streaming markdown, tool-call cards, and MCP widgets — survives reload, reconnect, and a second tab, and whose replies stream in live as the model produces them, because it is backed by a durable session stream rather than component state. That is the differentiator this track proves: most chat UIs keep the conversation in component state, so a refresh, a dropped socket, or a second tab loses it — here the transcript lives in the durable session and the UI is only a view of it, so the same log replays identically on reload, reconnect, and every other tab watching. It runs on shipped NetScript seams: the [`@netscript/fresh/ai`](https://rickylabs.github.io/netscript/reference/fresh/) durable-chat plane (published on JSR in `@netscript/fresh` and usable now, including the `ai/sandbox` MCP-UI subpath), the [`@netscript/fresh-ui`](https://rickylabs.github.io/netscript/reference/fresh-ui/) copy-registry chat components, and the [`@netscript/ai`](https://rickylabs.github.io/netscript/reference/ai/) MCP client stack. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/chat/01-scaffold/) 2. [2 · Durable chat route](https://rickylabs.github.io/netscript/netscript/tutorials/chat/02-durable-chat-route/) 3. [3 · Chat UI](https://rickylabs.github.io/netscript/netscript/tutorials/chat/03-chat-ui/) 4. [4 · Server-side tool call](https://rickylabs.github.io/netscript/netscript/tutorials/chat/04-tool-call/) 5. [5 · MCP tools & widgets](https://rickylabs.github.io/netscript/netscript/tutorials/chat/05-mcp/) 6. [6 · Live streaming](https://rickylabs.github.io/netscript/netscript/tutorials/chat/06-live-streaming/) ## What you will build A `chat-app/` Fresh workspace whose home screen is a working chat. The reader scaffolds a fresh NetScript project with the streams runtime, wires a durable chat session route that calls a model directly on `@tanstack/ai`, copies the fresh-ui `ai` component collection and hydrates a chat island, adds one server-side tool whose result surfaces as a tool-call card with citation chips, connects the chat to a remote MCP server and renders the `ui://` widgets its tools return, and finally turns the island live so replies stream in as they arrive. This is a learning track: the same project grows chapter by chapter, so do them in order. ## Before you begin You need the standard NetScript toolchain — Deno, the Aspire CLI, and Docker — plus a model provider key. Confirm the toolchain: ```sh deno --version && aspire --version && docker info ``` You should see a Deno 2.x version, an Aspire CLI version, and Docker engine details. If any are missing, the [quickstart](https://rickylabs.github.io/netscript/quickstart/) walks through installing them. Install the NetScript CLI once: ```sh deno install --global --allow-all --name netscript jsr:@netscript/cli@0.0.1-beta.11 ``` > You need a model provider key > > This track calls Anthropic directly through > > @tanstack/ai > > . Have an > > ANTHROPIC_API_KEY > > ready before chapter 1 — chapter 1 shows where to set it so Aspire injects it into the app process. Any provider > > @tanstack/ai > > supports works; this track uses Anthropic for concreteness. ## The arc: route → UI → tools → MCP → live streaming Each chapter adds exactly one link in the durable-chat spine: [1 · Scaffold the workspace Create `chat-app/` with `netscript init`, add the `streams` plugin so durable sessions have a runtime, set `ANTHROPIC_API_KEY`, and boot under Aspire.](https://rickylabs.github.io/netscript/netscript/tutorials/chat/01-scaffold/) [2 · The durable chat route Wire the session route with `toNetScriptChatResponse` + a required `authorize` hook, the one `createNetScriptChatStreamProxy`, and a direct model call on `@tanstack/ai`.](https://rickylabs.github.io/netscript/netscript/tutorials/chat/02-durable-chat-route/) [3 · The chat UI Copy the fresh-ui `ai` collection (message, prompt-input, markdown, chat-render) and hydrate an island that seeds from `resolveChatSnapshot` and drives `createNetScriptChatConnection`.](https://rickylabs.github.io/netscript/netscript/tutorials/chat/03-chat-ui/) [4 · A server-side tool call Add one `@tanstack/ai` tool the model can call; surface the invocation as a `tool-call-card` and render its citations as chips.](https://rickylabs.github.io/netscript/netscript/tutorials/chat/04-tool-call/) [5 · MCP tools & widgets Connect a remote MCP server with `createMcpTransportPool` from `@netscript/ai/mcp`, bridge its tools into the chat turn, and render `ui://` widgets sandboxed through `createMcpSandboxHandler` and the copied `McpUiWidget`.](https://rickylabs.github.io/netscript/netscript/tutorials/chat/05-mcp/) [6 · Live streaming Upgrade the island from settle-then-render to a live `connection.subscribe(signal)` read, folding each chunk through the one projection so replies stream in as they arrive, across tabs.](https://rickylabs.github.io/netscript/netscript/tutorials/chat/06-live-streaming/) > What this track deliberately leaves out > > The tutorial stays on the durable-chat spine: durable chat, streaming markdown, tools (local and over MCP), sandboxed > > ui:// > > widgets, and a live subscription. It does > > not > > cover the generative-UI renderer, agent memory / semantic recall, or RAG. The > > @netscript/ai engine > > — model registry, agent loop, tool registry, and MCP transports — is published on JSR as of > > 0.0.1-beta.11 > > ; this track wires the model call directly on > > @tanstack/ai > > , because the durable session plane persists and replays TanStack chunk streams. Chapter 2 shows where the engine fits, and chapter 5 uses its MCP client stack. ## What you built By the end of this track you own a working durable chat app and understand the seams that make it durable — the session route, the stream proxy, the SSR seed, and the client connection's send and live-subscribe sides — plus how the fresh-ui chat components render its transcript, how a tool call (yours or a remote MCP server's) lands as a durable card, and how MCP `ui://` widgets render themed and sandboxed inside the chat. [Tutorials](https://rickylabs.github.io/netscript/netscript/tutorials/) [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/chat/01-scaffold/) After this track, keep building in **Build › [AI & Agents](https://rickylabs.github.io/netscript/ai/)** — the guides and recipes there pick up where these chapters stop. _Canonical: https://rickylabs.github.io/netscript/tutorials/chat/_ --- # Scaffold the chat workspace Every track starts with a real project on disk. In this chapter you create `chat-app/` — a NetScript workspace with a Fresh frontend — add the **streams runtime** so durable chat sessions have somewhere to live, set your model key, and boot the whole thing under Aspire. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/chat/01-scaffold/) 2. [2 · Durable chat route](https://rickylabs.github.io/netscript/netscript/tutorials/chat/02-durable-chat-route/) 3. [3 · Chat UI](https://rickylabs.github.io/netscript/netscript/tutorials/chat/03-chat-ui/) 4. [4 · Server-side tool call](https://rickylabs.github.io/netscript/netscript/tutorials/chat/04-tool-call/) 5. [5 · MCP tools & widgets](https://rickylabs.github.io/netscript/netscript/tutorials/chat/05-mcp/) 6. [6 · Live streaming](https://rickylabs.github.io/netscript/netscript/tutorials/chat/06-live-streaming/) ## What you will build A `chat-app/` workspace on disk: a [Fresh](https://rickylabs.github.io/netscript/web-layer/) app, the `streams` plugin wired for durable sessions, and Aspire orchestration. By the end the Aspire dashboard at `:18888` shows every resource running, `ANTHROPIC_API_KEY` reaches the app process, and the workspace type-checks. You reuse this same workspace for every later chapter. ## Before you begin The only prerequisite is the toolchain from the [track index](https://rickylabs.github.io/netscript/tutorials/chat/) plus an `ANTHROPIC_API_KEY`. Confirm the CLI is installed and reachable: ```sh netscript --help ``` You should see the public command groups: `init`, `contract`, `db`, `deploy`, `generate`, `marketplace`, `plugin`, `service`, `ui:add`, and `ui:init`. If `netscript` is not found, make sure Deno's install directory is on your `PATH` and open a fresh terminal. ## Step 1 — Preview the scaffold with a dry run Before writing files, ask the CLI what it *would* create. `--dry-run` plans the scaffold and prints totals without touching disk: ```sh netscript init chat-app --dry-run ``` A clean dry run confirms your flags are valid and is your green light to scaffold for real. ## Step 2 — Create the workspace Scaffold for real. This track does not need an example service — the chat route you write in chapter 2 is the only backend — so a plain Fresh workspace is enough: ```sh netscript init chat-app cd chat-app ``` This writes `chat-app/`, formats the output with `deno fmt`, and initializes a git repository. On completion the CLI prints a **next steps** summary tailored to your options. ## Step 3 — Add the streams runtime A durable chat session is one append-only stream, addressed through the streams runtime. Add the `streams` plugin so `@netscript/fresh/ai` has a runtime to resolve session URLs against: ```sh netscript plugin install stream --name streams --samples ``` > Why streams, for a chat app? > > @netscript/fresh/ai > > does not reimplement transport — it addresses each chat session as one durable stream under the > > @netscript/plugin-streams-core > > seam (the same seam > > @netscript/fresh/streams > > uses for live tables). No > > streams > > runtime means no place for the session log to live, so > > resolveChatSnapshot > > and the connection have nothing to read. ## Step 4 — Set your model key The chat route calls Anthropic directly; the adapter reads `ANTHROPIC_API_KEY` from the app process environment. Put it where Aspire will inject it — your workspace's local environment file (never commit a real key): ```sh # chat-app/.env (add .env to .gitignore if it is not already) ANTHROPIC_API_KEY=sk-ant-... ``` > The key stays server-side > > ANTHROPIC_API_KEY > > is read only inside the chat > > route > > (server code). It is never bundled into the island or sent to the browser — the model call happens on the server, and the browser only ever sees the durable session stream through the proxy you build in chapter 2. ## Step 5 — Bring up orchestration Boot the whole thing under Aspire. Run it from the `aspire/` subfolder so the CLI finds `apphost.mts`: ```sh cd aspire aspire restore # once: restores the Aspire SDK modules into .aspire/ aspire start # starts the AppHost, the streams runtime, and the Fresh app ``` `aspire start` prints a URL and login token for the **Aspire dashboard**: ``` https://localhost:18888 ``` Leave `aspire start` running in this terminal; it is your control plane for the rest of the track. ## Verify your progress In a second terminal, type-check the whole workspace from the project root: ```sh deno task check ``` A clean check confirms the scaffold and plugin wiring line up. - [ ] `netscript --help` lists the public command groups. - [ ] `chat-app/` exists with `apps/dashboard/` and a `plugins/` entry for `streams`. - [ ] `ANTHROPIC_API_KEY` is set in the app environment. - [ ] `aspire start` is up; the dashboard at `:18888` lists the streams runtime and the Fresh app. - [ ] `deno task check` is clean. > If something is not green > > Three checks cover most first-run snags: (1) is > > aspire start > > still up, with the streams runtime healthy in the > > dashboard > > ? (2) is Docker running ( > > docker info > > )? (3) did you > > cd aspire > > before > > aspire start > > so it found > > apphost.mts > > ? ## What you built A real NetScript workspace — `chat-app/` — with a Fresh app, the streams runtime for durable sessions, and your model key wired to the app process, all orchestrated by Aspire. Next you will wire the durable chat route that turns a prompt into a persisted, streamed reply. [AI Chat](https://rickylabs.github.io/netscript/netscript/tutorials/chat/) [2 · Durable chat route](https://rickylabs.github.io/netscript/netscript/tutorials/chat/02-durable-chat-route/) _Canonical: https://rickylabs.github.io/netscript/tutorials/chat/01-scaffold/_ --- # The durable chat route The backend of a durable chat is two routes and one model call. In this chapter you wire the **session route** (runs a model turn, persists it durably, gated by `authorize`), the **one stream proxy** the browser reads through, and the direct model call on `@tanstack/ai`. When you finish, a `curl` can drive a full turn and a second `curl` replays it — the transcript is durable. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/chat/01-scaffold/) 2. [2 · Durable chat route](https://rickylabs.github.io/netscript/netscript/tutorials/chat/02-durable-chat-route/) 3. [3 · Chat UI](https://rickylabs.github.io/netscript/netscript/tutorials/chat/03-chat-ui/) 4. [4 · Server-side tool call](https://rickylabs.github.io/netscript/netscript/tutorials/chat/04-tool-call/) 5. [5 · MCP tools & widgets](https://rickylabs.github.io/netscript/netscript/tutorials/chat/05-mcp/) 6. [6 · Live streaming](https://rickylabs.github.io/netscript/netscript/tutorials/chat/06-live-streaming/) ## The four seams `@netscript/fresh/ai` gives you one function per job. This chapter uses three of the four; chapter 3 uses the fourth (`createNetScriptChatConnection`) in the island. **@netscript/fresh/ai — the durable-chat seams used here** | Name | Type | Description | | --- | --- | --- | | `toNetScriptChatResponse` | `session route` | Turn a server chat stream into a durable session Response; authorize-gated (→ 403 on deny). | | `resolveChatSnapshot` | `history seed` | Materialize the transcript so far, reduced through projectChatSnapshot — the model's context. | | `createNetScriptChatStreamProxy` | `the one proxy` | The single durable chat-stream proxy the browser reads through; attaches streams auth server-side. | ## Step 1 — The model call The model layer is wired directly on `@tanstack/ai` and a provider adapter. `chat()` returns an async iterable of stream chunks — exactly the `source` shape `toNetScriptChatResponse` persists. `anthropicText()` reads `ANTHROPIC_API_KEY` from the environment, so no key appears in code: ```ts import { chat } from '@tanstack/ai'; import { anthropicText } from '@tanstack/ai-anthropic'; const adapter = anthropicText('claude-sonnet-4-5'); const source = chat({ adapter, messages: [{ role: 'user', content: 'Hello!' }], systemPrompts: ['You are a helpful assistant.'], }); // `source` is an AsyncIterable of TanStack stream chunks. ``` > Where the @netscript/ai engine fits > > NetScript also ships a provider-neutral engine — the > > @netscript/ai > > package, published on JSR as of > > 0.0.1-beta.11 > > . Its entry seam is a model registry with self-registering providers: > > import '@netscript/ai/anthropic' > > registers the Anthropic provider, and > > getModel('anthropic:claude-sonnet-4-5') > > resolves a model handle; an agent loop, a tool registry, and MCP transports live behind its other subpaths ( > > reference > > ). This route keeps the direct > > @tanstack/ai > > wiring because the durable session plane persists and reduces TanStack chunk streams, while the engine's chat clients stream the engine's own event vocabulary — two different wire shapes. ## Step 2 — The session route This route runs one turn and persists it. It pulls the transcript so far through `resolveChatSnapshot` (so the model has context), calls the model, and hands the stream to `toNetScriptChatResponse` — gated by an `authorize` hook. First give that dynamic route a **typed identity**. `createRouteReference` from `@netscript/fresh/route` infers the `{ sessionId }` path param straight from the `[sessionId]` pattern, so the id the server reads and the URL the browser posts to in chapter 3 both come from one declaration — never a string assembled two different ways. Put it in the shared `contracts/` tree so the route and the island import the *same* object: ```ts // contracts/routes/chat-turn.ts import { createRouteReference } from '@netscript/fresh/route'; /** The one place the chat turn route pattern is written. */ export const chatTurnRoute = createRouteReference('/api/chat/[sessionId]'); // chatTurnRoute.parsePath({ sessionId }) -> typed { sessionId: string } // chatTurnRoute.href({ path: { sessionId } }) -> "/api/chat/" (the island posts here in ch. 3) ``` Now the route reads its param **through the contract** instead of touching `ctx.params` by hand: ```ts // apps/dashboard/routes/api/chat/[sessionId].ts import { chat } from '@tanstack/ai'; import { anthropicText } from '@tanstack/ai-anthropic'; import { resolveChatSnapshot, toNetScriptChatResponse } from '@netscript/fresh/ai'; import { chatTurnRoute } from '../../../../contracts/routes/chat-turn.ts'; // REQUIRED in production — no default allow-all. Replace with your real check // (session cookie → owner lookup). Returning false denies the turn with a 403. const authorize = (request: Request, sessionId: string): boolean => Boolean(request) && sessionId.length > 0; export const handler = { async POST(ctx: { req: Request; params: { sessionId: string } }): Promise { // Typed off the one route contract — the same object the island builds its URL from. const { sessionId } = chatTurnRoute.parsePath(ctx.params); const target = { sessionId } as const; // History so far, reduced through the SAME projection the island seeds from. const snapshot = await resolveChatSnapshot({ target }); const messages = snapshot.messages.map((m) => ({ role: m.role, content: m.content })); const source = chat({ adapter: anthropicText('claude-sonnet-4-5'), messages, systemPrompts: ['You are a helpful assistant.'], }); return toNetScriptChatResponse({ target, request: ctx.req, authorize, source }); }, }; ``` > authorize is not optional > > toNetScriptChatResponse > > enforces access only when you pass an > > authorize > > hook, and the factory applies > > no > > default. Omit it and the session stream is unauthenticated. Pass > > authorize > > without a > > request > > and it throws — always thread > > ctx.req > > through. A denial returns > > 403 Forbidden > > and the session stream is never touched. The user message is appended to the durable session by the island in chapter 3 (via `connection.send`), so this route reads it back through `resolveChatSnapshot` and only needs to stream the assistant reply. You could instead pass `newMessages: [userMessage]` to `toNetScriptChatResponse` to persist the prompt here — one place, either way, never both. ## Step 3 — The one stream proxy The browser must not hold streams credentials, so it never talks to the durable-streams service directly. It reads through a single proxy that attaches auth server-side and keeps every response header accurate. Mount `createNetScriptChatStreamProxy` once as a catch-all: ```ts // apps/dashboard/routes/api/chat-stream/[...path].ts import { createNetScriptChatStreamProxy } from '@netscript/fresh/ai'; const proxy = createNetScriptChatStreamProxy({ // The session id is the last path segment: /api/chat-stream/ai/chat/{sessionId} target: (req) => ({ sessionId: new URL(req.url).pathname.split('/').pop()! }), }); export const handler = { GET: proxy, POST: proxy }; ``` > Why a raw Request here, not a route contract > > This resolver is the one documented exception to NetScript's typed-route rule: > > createNetScriptChatStreamProxy > > 's > > target > > only ever receives the raw > > Request > > , so the session id is parsed from > > req.url > > by hand. Everywhere else you build a URL or read a path param, prefer a bound > > `createRouteReference` > > contract so the pattern and its typed params come from one source of truth. > Why a proxy at all? > > The proxy exists so the browser gets a durable stream > > without > > streams credentials. It attaches the > > Authorization > > header only on the server → streams hop (never echoed back), passes the body through unbuffered, and strips > > content-encoding > > / > > content-length > > — which stop describing the bytes once the stream is re-framed across the proxy. A client disconnect aborts the upstream fetch, so no stream is left dangling. ## Verify your progress With `aspire start` running, drive one turn against a fresh session id, then replay it: ```sh # Run a turn (the assistant reply streams back and is persisted durably). curl -N -X POST http://localhost:8010/api/chat/demo-1 # Replay: read the same session again — the transcript is still there. curl -N http://localhost:8010/api/chat-stream/ai/chat/demo-1 ``` The first call streams a reply; the second shows the durable session already holds it — that is durability, not a re-run. Then type-check: ```sh deno task check ``` - [ ] `POST /api/chat/{sessionId}` streams an assistant reply. - [ ] A denied `authorize` returns `403`. - [ ] Re-reading the session through the proxy replays the transcript. - [ ] `deno task check` is clean. > If the turn errors > > A provider error (bad or missing > > ANTHROPIC_API_KEY > > ) surfaces > > into > > the assistant stream rather than as an HTTP error — check the reply body. A > > 403 > > means > > authorize > > returned > > false > > . An empty replay means the streams runtime is not up: confirm it in the Aspire dashboard. ## What you built The durable backend: a session route that runs a model turn and persists it behind a required `authorize` gate, and the one proxy the browser reads through. Notice the discipline underneath — one agreed message shape (`NetScriptChatMessage`) flows through the route, and the turn is written to the durable session *before* anyone renders it, so an accepted reply survives the request that produced it. That "durable delivery" spine is what every later chapter builds on. Next you give it a face — copy the fresh-ui chat components and hydrate an island. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/chat/01-scaffold/) [3 · Chat UI](https://rickylabs.github.io/netscript/netscript/tutorials/chat/03-chat-ui/) _Canonical: https://rickylabs.github.io/netscript/tutorials/chat/02-durable-chat-route/_ --- # The chat UI The backend streams and persists turns; now it needs a face. In this chapter you copy the fresh-ui `ai` component collection into your app, seed the first paint from `resolveChatSnapshot`, and hydrate a chat island that drives `createNetScriptChatConnection` — subscribe, send, dispose. The transcript renders complete on load and updates as turns settle. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/chat/01-scaffold/) 2. [2 · Durable chat route](https://rickylabs.github.io/netscript/netscript/tutorials/chat/02-durable-chat-route/) 3. [3 · Chat UI](https://rickylabs.github.io/netscript/netscript/tutorials/chat/03-chat-ui/) 4. [4 · Server-side tool call](https://rickylabs.github.io/netscript/netscript/tutorials/chat/04-tool-call/) 5. [5 · MCP tools & widgets](https://rickylabs.github.io/netscript/netscript/tutorials/chat/05-mcp/) 6. [6 · Live streaming](https://rickylabs.github.io/netscript/netscript/tutorials/chat/06-live-streaming/) ## Step 1 — Copy the AI component collection The chat primitives are **copy-source**: the CLI copies them into your app, where you own them. The `ai` collection installs the whole chat surface in one command — message, prompt-input, markdown, the `chat-render` block parser, and the tool-call and citation components you use in chapter 4: ```bash netscript ui:add ai ``` This copies component files into `apps/dashboard/components/ui/`, the `chat-render` parser into `apps/dashboard/lib/chat/parse-blocks.ts`, and their CSS into `apps/dashboard/assets/ui/`, then wires the styles and merges any required imports. After the copy, that code is yours to edit — see [Customize Fresh UI](https://rickylabs.github.io/netscript/web-layer/how-to/customize-fresh-ui/) for the ownership model. **The ai collection — the pieces this track uses** | Name | Type | Description | | --- | --- | --- | | `message` | `chat message` | Author + time, inline markup (bold / code / [n] citation chips), tool-call + chart/code blocks, follow-up chips, typing indicator. Exports Message, renderInline, TypingIndicator. | | `prompt-input` | `composer` | Auto-grow textarea + toolbar. Presentational ; onSubmit(text, meta) reads the field. | | `markdown` | `renderer` | Sanitized streaming-markdown component + pipeline for assistant prose. | | `chat-render` | `block parser (lib)` | parseBlocks(input): RenderPart[] — turns assistant markdown into typed rich blocks. Never throws. | | `tool-call-card` | `tool disclosure` | Tool invocation + result as a with a status badge (used in chapter 4). | | `citation-chip` | `citation` | Inline [n] source marker that pairs with a sources list (used in chapter 4). | ## Step 2 — Seed the first paint In the page that hosts the chat, materialize the transcript so far and pass it into the island. `resolveChatSnapshot` returns `{ messages, renderParts, offset }`; the `offset` seeds the live subscription so seed and live read one continuous log: ```ts // In your chat page route (server side) import { resolveChatSnapshot } from '@netscript/fresh/ai'; const sessionId = 'demo-1'; // from the route params / the signed-in user's session const seed = await resolveChatSnapshot({ target: { sessionId } }); // Render as an island. ``` Wire `seed` into your page with the scaffold's page builder the same way the dashboard passes data to a view — see [Customize Fresh UI](https://rickylabs.github.io/netscript/web-layer/how-to/customize-fresh-ui/) for the `definePage` pattern. The important part is that the island receives `seed` as a prop. > The one-projection law, in one sentence > > resolveChatSnapshot > > reduces the session through > > projectChatSnapshot > > — the > > same > > reducer the live path uses. That is why a tool card rendered at first paint does not jump or vanish when the first live chunk arrives: seed and live never disagree about intermediate state. ## Step 3 — The chat island The island is where the browser hydrates. It holds the snapshot in a signal, opens a durable connection pointed at the proxy from chapter 2, and on submit: appends the user message with `connection.send`, fires the model turn, then re-materializes the settled transcript. ```tsx // apps/dashboard/islands/Chat.tsx import { useSignal } from '@preact/signals'; import { createNetScriptChatConnection, resolveChatSnapshot } from '@netscript/fresh/ai'; import type { NetScriptChatMessage, NetScriptChatSnapshot } from '@netscript/fresh/ai'; import { chatTurnRoute } from '@app/contracts/routes/chat-turn.ts'; import { Message, type MessageData } from '@app/components/ui/message.tsx'; import { PromptInput } from '@app/components/ui/prompt-input.tsx'; const toMessageData = (m: NetScriptChatMessage): MessageData => ({ role: m.role === 'assistant' ? 'assistant' : 'user', author: { name: m.role === 'assistant' ? 'Assistant' : 'You', agent: m.role === 'assistant' }, body: m.content, }); interface ChatProps { sessionId: string; seed: NetScriptChatSnapshot; } const Chat = ({ sessionId, seed }: ChatProps) => { const snapshot = useSignal(seed); const pending = useSignal(false); const base = `${location.origin}/api/chat-stream`; const connection = createNetScriptChatConnection({ target: { sessionId, baseUrl: base }, initialOffset: seed.offset ?? undefined, authorize: () => true, // browser half; the server route re-checks ownership }); const onSubmit = async (text: string) => { pending.value = true; // 1. Append the user message to the durable session. await connection.send([{ id: crypto.randomUUID(), role: 'user', content: text }]); // 2. Fire the model turn (chapter 2's route streams + persists the reply). // The URL comes from the same route contract the server reads its param through. await fetch(chatTurnRoute.href({ path: { sessionId } }), { method: 'POST' }); // 3. Re-materialize the settled transcript through the same projection. snapshot.value = await resolveChatSnapshot({ target: { sessionId, baseUrl: base } }); pending.value = false; }; return (
{snapshot.value.messages.map((m) => ( ))} {pending.value ? : null}
); }; export default Chat; ``` A few rules keep the island cheap and correct: keep it leaf-shaped, pass plain serializable props in (the seed snapshot is serializable), and declare handlers as arrow functions. The connection's `close` / `stop` / `dispose` are one idempotent teardown — call `connection.dispose()` when the island unmounts so no subscription leaks. > Live streaming vs settle-then-render > > This island re-materializes each turn once it > > settles > > , which keeps the code mechanical and correct — and reload durability, the headline feature, is fully in hand this way. The connection also exposes > > subscribe(signal) > > for token-by-token live chunks; > > chapter 6 > > upgrades this island to that live subscription so replies stream in as they arrive. > Why not useIslandQuery for the transcript? > > NetScript's typed island-query hooks ( > > useIslandQuery > > / > > useLiveQuery > > ) are the right tool when a page reads a > > typed service contract > > — a cache-first read whose data lives behind an oRPC service, as the > > live-dashboard track > > shows end to end. The chat transcript is different: it is not a service query but a > > durable session stream > > , so it is read through > > createNetScriptChatConnection > > and reduced by the one projection, not fetched through a query key. If > > this > > page also showed, say, a typed list of past conversations from a service, that sidebar is exactly where a > > useIslandQuery > > island would belong — the transcript itself stays on the durable connection. ## Step 4 — Rich blocks with chat-render Assistant replies are markdown, and they can embed fenced data blocks (`chart`, `donut`, `table`, `stats`, `line`). `parseBlocks` from the copied `chat-render` parser turns that markdown into typed **presentation** parts you render with the copied primitives: ```tsx import { parseBlocks, type RenderPart } from '@app/lib/chat/parse-blocks.ts'; import { Markdown } from '@app/components/ui/markdown.tsx'; import { ChartBlock } from '@app/components/ui/chart-block.tsx'; const renderPart = (part: RenderPart) => { switch (part.kind) { case 'text': return {part.text}; case 'chart': return ; // donut / table / stats / line follow the same shape → their primitive. default: return null; } }; // Render an assistant message body as rich blocks: // {parseBlocks(message.content).map(renderPart)} ``` `parseBlocks` never throws — an unrecognized fence falls back to a verbatim `text` part — and its inverse `blockToText` guarantees a parsed message survives a reload without drift. > Two RenderParts — transport vs presentation > > Do not conflate the two > > RenderPart > > types. > > @netscript/fresh/ai > > emits the > > transport > > part ( > > text > > | > > tool > > ) — the durable-session wire shape in > > snapshot.renderParts > > . > > chat-render > > 's > > parseBlocks > > owns the > > presentation > > part ( > > chart > > | > > donut > > | > > table > > | > > stats > > | > > line > > | > > text > > ) — the rich blocks you mount. The transcript reduces through the first; the UI renders through the second. ## Verify your progress With `aspire start` running, open the app in the browser: 1. The chat renders your existing transcript on load — no loading flash. 2. Type a message and send it; the assistant reply appears once the turn settles. 3. **Reload the page.** The full transcript is still there — that is the durable session, not browser state. 4. Type-check the app: ```bash deno task --cwd apps/dashboard check ``` - [ ] `netscript ui:add ai` copied the chat components into `components/ui/` and the parser into `lib/chat/`. - [ ] The chat renders the seeded transcript on first paint. - [ ] Sending a message produces a persisted assistant reply. - [ ] A reload replays the full transcript. - [ ] `deno task --cwd apps/dashboard check` is clean. ## What you built A working chat UI: copied, app-owned components; an SSR-seeded island that sends through the durable connection and re-renders on settle; and `chat-render` turning assistant markdown into rich blocks. Next you give the model a tool to call — and render its result as a tool-call card with citation chips. [2 · Durable chat route](https://rickylabs.github.io/netscript/netscript/tutorials/chat/02-durable-chat-route/) [4 · Server-side tool call](https://rickylabs.github.io/netscript/netscript/tutorials/chat/04-tool-call/) _Canonical: https://rickylabs.github.io/netscript/tutorials/chat/03-chat-ui/_ --- # A server-side tool call A chat gets useful when the model can *do* something. In this chapter you give the model one server-side tool, pass it into the same `chat()` call from chapter 2, and surface the invocation in the UI as a **tool-call card** whose results render as **citation chips**. The tool runs on the server — its result is captured in the durable transcript, so the card survives a reload exactly like the messages around it. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/chat/01-scaffold/) 2. [2 · Durable chat route](https://rickylabs.github.io/netscript/netscript/tutorials/chat/02-durable-chat-route/) 3. [3 · Chat UI](https://rickylabs.github.io/netscript/netscript/tutorials/chat/03-chat-ui/) 4. [4 · Server-side tool call](https://rickylabs.github.io/netscript/netscript/tutorials/chat/04-tool-call/) 5. [5 · MCP tools & widgets](https://rickylabs.github.io/netscript/netscript/tutorials/chat/05-mcp/) 6. [6 · Live streaming](https://rickylabs.github.io/netscript/netscript/tutorials/chat/06-live-streaming/) ## What you will build One tool — `searchDocs` — that the model can call to look something up, returning a short answer plus a list of sources. When the model calls it, the transcript records the invocation as a transport `tool` part, and the UI renders it as a `tool-call-card` with the sources shown as inline `[1]`, `[2]` citation chips. ## Step 1 — Define the tool A tool is a name, a description the model reads to decide when to call it, an input schema, and a server handler. Build it with `toolDefinition(...).server(handler)` from `@tanstack/ai`: `.server()` marks the handler server-side, and `chat()` runs it automatically — on the server, inside the turn — whenever the model calls the tool. Keep the handler pure and fast; return a plain object, including a `citations` array, which is just structured output your UI knows how to render: ```ts // apps/dashboard/lib/tools/search-docs.ts import { toolDefinition } from '@tanstack/ai'; export const searchDocs = toolDefinition({ name: 'searchDocs', description: 'Look up a short factual answer from the docs. Use for product questions.', // Plain JSON Schema is accepted; bring a Standard Schema library (Zod, Valibot, …) // as `inputSchema` instead if you want the handler args typed for you. inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The question to look up.' } }, required: ['query'], }, }).server(async ({ query }: { query: string }) => { const hits = await lookup(query); // your retrieval — DB query, search index, etc. return { answer: hits.map((h) => h.snippet).join(' '), citations: hits.map((h, i) => ({ index: i + 1, source: h.title, href: h.url })), }; }); ``` > Citations are plain tool output — not RAG magic > > The > > citations > > array is ordinary structured data your tool returns. There is no retrieval framework or vector store behind it — you decide what > > lookup > > does, and the UI renders whatever citations the tool hands back. If you want embeddings-backed retrieval under > > lookup > > , the > > @netscript/ai engine > > ships embedding provider ports ( > > @netscript/ai/openai-embeddings > > ); full RAG and agent memory stay out of scope for this track. ## Step 2 — Pass the tool to the model Wire the tool into the same `chat()` call from chapter 2 by adding a `tools` array. The model now decides, per turn, whether to call `searchDocs`; when it does, the runtime executes it and feeds the result back into the turn — and the whole exchange lands in the durable session. ```ts // apps/dashboard/routes/api/chat/[sessionId].ts (add tools to the existing chat() call) import { chat } from '@tanstack/ai'; import { anthropicText } from '@tanstack/ai-anthropic'; import { resolveChatSnapshot, toNetScriptChatResponse } from '@netscript/fresh/ai'; import { searchDocs } from '../../../lib/tools/search-docs.ts'; // ...inside POST, after building `messages` and the authorize check: const source = chat({ adapter: anthropicText('claude-sonnet-4-5'), messages, systemPrompts: ['You are a helpful assistant. Use searchDocs for product questions.'], tools: [searchDocs], }); return toNetScriptChatResponse({ target, request: ctx.req, authorize, source }); ``` > The tool runs server-side — behind authorize > > The tool's server handler runs in the route, on the server, inside the same > > authorize > > -gated turn. Its result is persisted into the durable session as a transport > > tool > > part, so a reload replays the tool call and its result. Never move tool execution into the island — the browser has neither the credentials nor the trust boundary for it. ## Step 3 — Render the tool call as a card The tool invocation arrives in the transcript as a transport `tool` part (`kind: 'tool'`) carrying `toolName`, `toolState`, `input`, and `output`. Map it onto the copied `tool-call-card`, and turn the `output.citations` into the `sources` list that `message`'s inline `[n]` markup renders as chips: ```tsx // In the chat island's assistant renderer import { ToolCallCard } from '@app/components/ui/tool-call-card.tsx'; import { CitationChip } from '@app/components/ui/citation-chip.tsx'; import type { RenderPart } from '@netscript/fresh/ai'; const toStatus = (state?: string) => state === 'error' ? 'error' : state === 'complete' ? 'done' : 'running'; const renderToolPart = (part: RenderPart) => { if (part.kind !== 'tool') return null; const citations = (part.output as { citations?: { index: number; source?: string; href?: string }[] }) ?.citations ?? []; return (
{citations.length ? (
{citations.map((c) => )}
) : null}
); }; // Drive it from the transport render parts the snapshot already gives you: // {snapshot.value.renderParts.filter((p) => p.kind === 'tool').map(renderToolPart)} ``` > One projection feeds the card > > The card reads from > > snapshot.renderParts > > — the transport parts produced by > > projectChatSnapshot > > , the same reducer the SSR seed used. That is the one-projection law paying off: because the tool part is materialized once, the card renders identically on first paint and after a reload, and its > > toolState > > is never ambiguous between the seed and live paths. ## Verify your progress With `aspire start` running, open the app and ask a question that triggers the tool — for example, "what database does a scaffold use by default?": 1. The assistant reply includes a **tool-call card** naming `searchDocs`, expandable to show its arguments and result. 2. The answer carries inline `[1]` / `[2]` **citation chips** matching the tool's sources. 3. **Reload.** The card and its chips are still there — the tool call is in the durable transcript, not transient UI state. 4. Type-check the app: ```bash deno task --cwd apps/dashboard check ``` - [ ] Asking a product question triggers a `searchDocs` tool-call card. - [ ] The card shows the tool arguments and result on expand. - [ ] Sources render as inline citation chips. - [ ] A reload replays the card and chips. - [ ] `deno task --cwd apps/dashboard check` is clean. > If the model never calls the tool > > Models call a tool only when the description makes it obviously relevant. Sharpen the tool > > description > > and add a nudge to > > systemPrompts > > ("use searchDocs for product questions"). If the card renders but stays > > running > > , your tool handler threw — check the route logs; a thrown tool surfaces as an > > error > > card, not a crash. ## What you built A server-side tool the model can call, wired into the same `authorize`-gated turn: its invocation and citations render as a durable `tool-call-card`, and because the tool runs on the server its result is captured in the durable transcript — so the card survives a reload exactly like the messages around it. Next you reach past your own code: chapter 5 connects the chat to a remote MCP server and renders the `ui://` widgets its tools return. [3 · Chat UI](https://rickylabs.github.io/netscript/netscript/tutorials/chat/03-chat-ui/) [5 · MCP tools & widgets](https://rickylabs.github.io/netscript/netscript/tutorials/chat/05-mcp/) _Canonical: https://rickylabs.github.io/netscript/tutorials/chat/04-tool-call/_ --- # MCP tools & widgets Chapter 4's tool was code you wrote. Real products also need capabilities that live behind **Model Context Protocol** servers — a docs-search server, an ops server, a vendor's tool endpoint. In this chapter you connect the chat to a remote MCP server with the `@netscript/ai/mcp` client stack, call one of its tools from the same `authorize`-gated turn, and render the `ui://` **widget** an MCP tool result can carry — sandboxed and themed, never as raw HTML in your island tree. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/chat/01-scaffold/) 2. [2 · Durable chat route](https://rickylabs.github.io/netscript/netscript/tutorials/chat/02-durable-chat-route/) 3. [3 · Chat UI](https://rickylabs.github.io/netscript/netscript/tutorials/chat/03-chat-ui/) 4. [4 · Server-side tool call](https://rickylabs.github.io/netscript/netscript/tutorials/chat/04-tool-call/) 5. [5 · MCP tools & widgets](https://rickylabs.github.io/netscript/netscript/tutorials/chat/05-mcp/) 6. [6 · Live streaming](https://rickylabs.github.io/netscript/netscript/tutorials/chat/06-live-streaming/) ## The pieces — all shipped, one boundary Three seams do the work, and every one of them is published and installable today: **The MCP surface this chapter uses** | Name | Type | Description | | --- | --- | --- | | `@netscript/ai/mcp` | `client stack — jsr:@netscript/ai` | createMcpTransportPool declares remote servers as data (stdio or reconnectable Streamable-HTTP, auth injected); callTool routes a call to its server; extractMcpUiResources pulls the data-only ui:// records out of a result. | | `createMcpSandboxHandler` | `route — @netscript/fresh/ai/sandbox` | Serves a registered ui:// resource themed with the active token set and behind a per-response CSP. The serving half of the render path. | | `McpUiWidget` | `island — fresh-ui copy registry` | The copied island (installed by netscript ui:add ai in chapter 3) that renders a ui:// resource in a sanitized, no-referrer sandboxed frame through the sandbox route. | > Client, not server > > NetScript's shipped MCP surface is > > client-side > > : your app > > consumes > > external MCP servers as tools. There is no scaffolded NetScript MCP server in this release — this chapter points your chat > > at > > a server, it does not make your app one. See > > the MCP guide > > for the full client stack. ## What you will build A `searchRemote` tool whose handler routes through an MCP transport pool instead of your own code: the model calls it like any chapter-4 tool, the pool executes it on the remote server, and the result — including any `ui://` widget the server returns — lands in the durable transcript and renders in the chat, themed and walled off behind a per-response CSP. ## Before you begin You need the working chat from chapters [1](https://rickylabs.github.io/netscript/tutorials/chat/01-scaffold/)–[4](https://rickylabs.github.io/netscript/tutorials/chat/04-tool-call/), plus the engine package, which owns the MCP client stack: ```sh deno add jsr:@netscript/ai ``` You also need an MCP server to talk to — any Streamable-HTTP MCP endpoint you have access to works (a docs-search server is the running example below). Put its URL and token in the app environment next to your model key, never in code. ## Step 1 — Declare the server pool `createMcpTransportPool` turns server declarations into a connected tool surface. The configs are **serializable data** — kind, id, URL, auth — so they can live in configuration. Auth is injected here at the composition root, never baked into a transport: ```ts // apps/dashboard/lib/mcp/pool.ts import { createMcpTransportPool } from '@netscript/ai/mcp'; export const mcpPool = createMcpTransportPool({ servers: [{ kind: 'streamable-http', serverId: 'search', url: Deno.env.get('MCP_SEARCH_URL')!, auth: { mode: 'api-token', token: Deno.env.get('MCP_TOKEN')!, scheme: 'Bearer' }, }], }); // connect() opens every pooled transport and returns the discovered tools, // name-prefixed by server id ("search:…") so two servers' tools never collide. export const mcpTools = await mcpPool.connect({}); ``` The Streamable-HTTP transport is built for long-lived sessions: it reconnects with backoff, and `reconnecting` is a first-class `McpConnectionState`, not an error. Add a second server to the `servers` array and the pool keeps their tools namespaced — many servers, one surface. ## Step 2 — Bridge a pooled tool into the chat turn Your route's model call runs on `@tanstack/ai` (chapter 2), so give the model a chapter-4-style tool whose **handler routes through the pool**. `callTool` sends the prefixed tool name to its server and hands back the result with any `ui://` resources already extracted: ```ts // apps/dashboard/lib/tools/search-remote.ts import { toolDefinition } from '@tanstack/ai'; import { mcpPool } from '../mcp/pool.ts'; export const searchRemote = toolDefinition({ name: 'searchRemote', description: 'Search the product docs. Use for any product or how-to question.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The question to look up.' } }, required: ['query'], }, }).server(async ({ query }: { query: string }) => { // Route the call to the remote server through the pool. const result = await mcpPool.callTool('search:search_docs', { query }, {}); // Plain structured output, like chapter 4 — plus the extracted ui:// records. return { result, uiResources: result.uiResources ?? [] }; }); ``` Add `searchRemote` to the same `tools` array as chapter 4's `searchDocs`. Nothing else in the route changes: the tool runs server-side, inside the `authorize`-gated turn, and the whole exchange — arguments, result, and the `ui://` references — is persisted as a transport `tool` part in the durable session. > Two ways in — this track takes the bridge > > The engine-native path is > > registerMcpTools(registry, pool) > > , which surfaces remote tools into the engine's tool registry for its > > agent loop > > to dispatch — registration is reversible via > > .stop() > > . This track bridges through a > > @tanstack/ai > > tool instead, because the durable session plane persists and replays TanStack chunk streams (the wire-shape note from chapter 2). Same pool, same servers — choose the entry point that matches your model-call layer. ## Step 3 — Render the `ui://` widget, sandboxed An MCP tool result can embed `ui://` resources — **UI authored by the server, not by you**. They reach the page only through two guardrails, both already in your app: 1. **The sandbox route.** Mount `createMcpSandboxHandler` once; it serves a registered resource selected by `?uri=`, injects the active theme's `--ns-*` custom properties, stamps `data-theme`, and applies a per-response CSP derived from the resource URI. `?theme=` picks a token set; an unknown theme falls back to `defaultThemeName` (`"default"`). ```ts // apps/dashboard/routes/mcp/sandbox.ts import { createMcpSandboxHandler } from '@netscript/fresh/ai/sandbox'; import { resourceRegistry } from '../../lib/mcp/registry.ts'; export const handler = { GET: createMcpSandboxHandler({ resolveResource: (uri, { signal }) => resourceRegistry.lookup(uri, { signal }), themes: { default: { '--ns-color-surface': '#ffffff' }, dark: { '--ns-color-surface': '#111111' }, }, }), }; ``` 1. **The widget island.** The `ai` collection you copied in chapter 3 includes the `McpUiWidget` island: hand it a `ui://` resource from the tool part's output and it renders the widget in a sanitized, `no-referrer` sandboxed frame served through the route above. In the chapter-4 render switch, a tool part whose output carries `uiResources` renders those through `McpUiWidget` alongside (or instead of) the plain tool-call card. > Server-authored UI never enters your island tree raw > > This is the same trust boundary as the generative-UI renderer: model- or server-authored UI reaches the page only through a curated renderer or the sandbox — sanitized, themed, CSP-guarded, frame-isolated. Never interpolate a > > ui:// > > resource's content into your own markup. And do not build on > > createNetScriptMcpSandbox > > (the chat-activity tool sandbox): it is a skeleton stub in this release — the route + widget path above is the shipped one. ## Verify your progress With `aspire start` running and your MCP server reachable: 1. Ask a question that triggers the remote tool. The reply carries a tool-call card naming `searchRemote` — its result came over MCP, not from your code. 2. If the server returned a `ui://` resource, the **widget renders** inside the chat, themed like the page around it. 3. Probe the sandbox route directly and check the isolation headers: ```sh curl -i 'http://localhost:8010/mcp/sandbox?uri=ui%3A%2F%2Fwidget%2Fdemo&theme=dark' ``` You should see the resolved resource with `data-theme="dark"` and the dark `--ns-*` tokens, under a `content-security-policy` response header derived from the resource URI. 1. **Reload.** The tool card — and the widget reference in its output — replays from the durable session, exactly like every other part of the transcript. 2. Type-check the app: ```bash deno task --cwd apps/dashboard check ``` - [ ] `deno add jsr:@netscript/ai` resolved and the pool `connect()`s to your server. - [ ] Asking a relevant question triggers the `searchRemote` tool over MCP. - [ ] A `ui://` result renders through `McpUiWidget`, themed and framed. - [ ] The sandbox route answers with theme tokens and a per-response CSP. - [ ] A reload replays the tool card and its widget reference. - [ ] `deno task --cwd apps/dashboard check` is clean. > If the remote tool never fires > > Check the pool first: > > connect() > > returns the discovered tool descriptors — if your tool is missing, the server URL, auth mode, or token is wrong. A tool that fires but errors surfaces as an > > error > > tool card (chapter 4's rule), and a dropped connection shows as the transport's > > reconnecting > > state, not a crash — subscribe with > > onStateChange > > to watch the lifecycle while debugging. ## What you built A chat whose tools no longer stop at your own code: a declared MCP server pool, a bridged tool the model calls like any other, and server-authored `ui://` widgets rendered through the sandbox route and the copied `McpUiWidget` island — durable in the transcript, isolated on the page. Next you make the whole transcript feel live — folding the model's chunks into the UI as they arrive instead of after each turn settles. [4 · Server-side tool call](https://rickylabs.github.io/netscript/netscript/tutorials/chat/04-tool-call/) [6 · Live streaming](https://rickylabs.github.io/netscript/netscript/tutorials/chat/06-live-streaming/) _Canonical: https://rickylabs.github.io/netscript/tutorials/chat/05-mcp/_ --- # Live streaming Your chat works, but it still feels turn-based: chapter 3's island fires a turn, waits for it to **settle**, then re-materializes the transcript in one jump. Real chat streams — the reply appears token by token, the tool card moves through `pending → streaming → complete` in front of you. In this final chapter you switch the island from settle-then-render to a **live subscription**, folding each chunk into the transcript as it arrives on the same durable connection you already opened. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/chat/01-scaffold/) 2. [2 · Durable chat route](https://rickylabs.github.io/netscript/netscript/tutorials/chat/02-durable-chat-route/) 3. [3 · Chat UI](https://rickylabs.github.io/netscript/netscript/tutorials/chat/03-chat-ui/) 4. [4 · Server-side tool call](https://rickylabs.github.io/netscript/netscript/tutorials/chat/04-tool-call/) 5. [5 · MCP tools & widgets](https://rickylabs.github.io/netscript/netscript/tutorials/chat/05-mcp/) 6. [6 · Live streaming](https://rickylabs.github.io/netscript/netscript/tutorials/chat/06-live-streaming/) ## The live half of a durable stream A durable chat session is one append-only log, and it has two readers. The **seed** reader (`resolveChatSnapshot`) materializes everything written so far for first paint. The **live** reader is `createNetScriptChatConnection(...).subscribe(signal)` — an async iterable that yields each new chunk as the server appends it. You already opened the connection in chapter 3 to `send` messages; here you finally read from it. **@netscript/fresh/ai — the live-read seam** | Name | Type | Description | | --- | --- | --- | | `connection.subscribe(signal)` | `AsyncIterable` | Yields each durable chunk as it lands. SR2-tolerant: a first subscribe that races a not-yet-created stream re-polls with backoff instead of throwing. | | `initialOffset` | `connection option` | The seed snapshot's offset — hand it in so the live read continues from where SSR stopped, one continuous log. | | `connection.dispose()` | `() => void` | One idempotent teardown (close / stop / dispose) — call it on unmount so no subscription leaks. | > This is the same producer→consumer split as any durable stream > > The durable chat plane is the > > durable-streams > > plane specialized for conversations: the server > > produces > > chunks into the session log, and the browser > > consumes > > them live over the connection — the log outlives both the turn that wrote it and the tab that was watching. Chapter 2's route is the producer; the subscription below is the consumer. ## Step 1 — Subscribe from the island Replace chapter 3's "fire the turn, then re-materialize on settle" flow with a live loop. On mount, open a subscription seeded at the snapshot's `offset`; for each chunk, re-derive the rendered transcript through the **same projection the seed used**, so live and seed never disagree about an in-flight tool card. The connection is the one from chapter 3 — you are adding the read side. ```tsx // apps/dashboard/islands/Chat.tsx — the live subscription (replaces the settle-then-render onSubmit) import { useSignal } from '@preact/signals'; import { useEffect } from 'preact/hooks'; import { createNetScriptChatConnection, resolveChatSnapshot } from '@netscript/fresh/ai'; import type { NetScriptChatSnapshot } from '@netscript/fresh/ai'; import { chatTurnRoute } from '@app/contracts/routes/chat-turn.ts'; const Chat = ({ sessionId, seed }: { sessionId: string; seed: NetScriptChatSnapshot }) => { const snapshot = useSignal(seed); const base = `${location.origin}/api/chat-stream`; const connection = createNetScriptChatConnection({ target: { sessionId, baseUrl: base }, initialOffset: seed.offset ?? undefined, // continue from the SSR seed cursor authorize: () => true, // browser half; the server route re-checks ownership }); // Live read: fold each chunk into the transcript as it lands. useEffect(() => { const controller = new AbortController(); (async () => { for await (const _chunk of connection.subscribe(controller.signal)) { // Re-materialize through the SAME projection the seed used, so the in-flight // tool card moves pending → streaming → complete without drift. snapshot.value = await resolveChatSnapshot({ target: { sessionId, baseUrl: base } }); } })(); return () => { controller.abort(); connection.dispose(); // idempotent teardown — no leaked subscription }; }, []); const onSubmit = async (text: string) => { // 1. Append the user message to the durable session. await connection.send([{ id: crypto.randomUUID(), role: 'user', content: text }]); // 2. Fire the model turn. Its chunks now arrive through the subscription above — // no manual re-materialize here; the live loop owns rendering. await fetch(chatTurnRoute.href({ path: { sessionId } }), { method: 'POST' }); }; return (
{snapshot.value.messages.map((m) => )}
); }; export default Chat; ``` The shape of the change is the point: `onSubmit` no longer re-reads the transcript itself. It just appends the prompt and fires the turn; the **subscription** is what advances the UI, one chunk at a time, for this tab and any other tab watching the same session. > One projection, seed and live > > The live loop re-derives through > > resolveChatSnapshot > > — which reduces the session through > > projectChatSnapshot > > , the > > same > > reducer that seeded first paint. That is the one-projection law from chapter 3 doing its job under streaming: because both paths route through one reducer, a tool card rendered mid-stream never jumps or vanishes when the next chunk arrives. Never hand-roll a separate live reducer — seed and live must share the projection. ## Step 2 — Tear down cleanly A live subscription is a long-lived resource. The `useEffect` above aborts its `AbortController` and calls `connection.dispose()` on unmount — `close` / `stop` / `dispose` are one idempotent teardown, so calling it twice is harmless and calling it once is mandatory. Skip it and every navigation away from the chat leaks a subscription against the streams runtime. > Backpressure and reconnect are already handled > > subscribe > > is SR2-tolerant: if the island mounts and subscribes before the session stream exists (a fresh chat), it re-polls with backoff instead of throwing. And because reads are seeded from > > initialOffset > > , a reconnect resumes from the last chunk you saw rather than replaying the whole log — the durable offset is the reconnect cursor. ## Verify your progress With `aspire start` running, open the app in two browser tabs pointed at the same session: 1. In tab A, send a message. The assistant reply **streams in** — text grows as chunks land, and a tool call surfaces as a card that fills from `pending` to `complete` — rather than appearing all at once when the turn settles. 2. Watch **tab B**: the same message and reply appear live there too, with no refresh. Two consumers, one durable log. 3. **Reload either tab.** The full transcript is still there and the live read resumes from the seed offset — durability and live streaming on the one connection. 4. Type-check the app: ```bash deno task --cwd apps/dashboard check ``` - [ ] The island opens `connection.subscribe(signal)` seeded at the snapshot `offset`. - [ ] An assistant reply renders incrementally as chunks arrive, not only on settle. - [ ] A second tab on the same session updates live. - [ ] `connection.dispose()` runs on unmount (no leaked subscription). - [ ] A reload replays the transcript and resumes the live read. - [ ] `deno task --cwd apps/dashboard check` is clean. > If the reply does not stream > > If replies still appear only on settle, confirm the subscription loop is actually running — a thrown > > authorize > > on the server route (chapter 2) closes the stream, and an empty subscription usually means the streams runtime is not up (check the > > Aspire dashboard > > ). If a second tab never updates, both tabs must target the > > same > > sessionId > > — the session id is the stream identity. ## What you built A complete, production-shaped durable AI chat: a scaffolded workspace, an `authorize`-gated route that streams and persists each turn, an app-owned chat UI, a server-side tool with citation chips, remote MCP tools whose `ui://` widgets render themed and sandboxed, and now a live subscription that folds the model's chunks into the transcript the moment they land — across tabs and across reloads, because every part lives in the durable session rather than the browser. That progression — **one agreed message shape → durable delivery → live stream** — is the spine every NetScript real-time surface is built from; you just built it in its AI-chat form. Where to go next: - Reach for the durable-chat seams directly with the [Build a durable chat](https://rickylabs.github.io/netscript/ai/how-to/build-a-durable-chat/) recipe. - Restyle any chat or widget component — you own the copied source: [Customize Fresh UI](https://rickylabs.github.io/netscript/web-layer/how-to/customize-fresh-ui/). - Model live list/board/table data (the other real-time plane) with [Publish a durable stream](https://rickylabs.github.io/netscript/durable-workflows/how-to/publish-a-durable-stream/). - Compose the provider-neutral engine — model registry, agent loop, tool registry, MCP transports: [AI engine](https://rickylabs.github.io/netscript/ai/engine/). - Look up exact signatures in the [fresh reference](https://rickylabs.github.io/netscript/reference/fresh/). [5 · MCP tools & widgets](https://rickylabs.github.io/netscript/netscript/tutorials/chat/05-mcp/) [Tutorials](https://rickylabs.github.io/netscript/netscript/tutorials/) _Canonical: https://rickylabs.github.io/netscript/tutorials/chat/06-live-streaming/_ --- # ERP Sync Your team is mid-migration between two ERPs. **SAP**, the legacy system, is still the system of record — production plans against it every day. **Microsoft Dynamics**, its replacement, goes live in stages, which means that for months the two run in parallel and Dynamics is only as correct as the sync that feeds it. The integration you are handed is a **file export**, not an API: SAP drops a nightly file, and your job is to ingest it. This track builds that sync: a back-office service that watches for the SAP export drops, turns each one into a durable background job, transforms legacy rows into Dynamics' shape, absorbs backfill bursts behind a queue, and re-syncs nightly on a cron. It is the durable-processing companion to the [main tutorials ladder](https://rickylabs.github.io/netscript/tutorials/): where that ladder ends at a request/response service plus a webhook, this one is about everything that happens **off** the request path. The differentiator this track proves is **durable background processing you did not hand-roll**: the file-watch trigger, the job, the queue, and the cron are first-class NetScript primitives wired into one orchestrated runtime — in place of the pile of cron entries, ad-hoc `nohup` workers, and bespoke glue scripts most teams reach for, the kind that silently drop a file or a row when a process dies mid-run. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/01-scaffold/) 2. [2 · Import job](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/02-import-job/) 3. [3 · Polyglot transform](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/03-polyglot-transform/) 4. [4 · Queue & cron](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/04-queue-and-cron/) 5. [5 · Deploy](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/05-deploy/) ## What you will build By the end of this five-chapter track you will have `my-erp/` on disk: a NetScript workspace with the **workers** and **triggers** plugins installed, a file-watch trigger that fires the moment the SAP export job drops a `products_*.csv` into the hand-off folder, a background **job** that parses it, a runnable **transform task** — executed as a sandboxed subprocess — that rewrites the legacy columns and integer-cent prices into Dynamics' shape, a **queue** sized for backfill bursts, and a **cron** schedule that re-syncs nightly — all orchestrated locally by Aspire and visible in one dashboard. Along the way you will see how the same task builder targets **polyglot** (non-TypeScript) runtimes, so a Python or shell step can join the pipeline when TypeScript is not the right tool. The spine is one idea repeated at every layer: **durable background processing**. The stakes are concrete: a file the watcher misses is a day of catalog changes Dynamics never sees; a row loaded untransformed puts prices off by a factor of one hundred; a burst that overwhelms a single worker delays the re-sync everyone plans against. An inbound file becomes a queued job; the job runs in an isolated worker; the transform runs in a sandboxed subprocess; recurring work runs on a schedule; and nothing is lost across a restart. ## The arc ``` SAP export job drops products_2024.csv (a file lands in .data/incoming/products) │ defineFileWatch trigger fires on 'create' ▼ enqueueJob('import-products') (a durable worker job, :8091) │ the job parses + records the raw SAP rows ▼ normalize-sap transform task (sandboxed deno subprocess: SAP shape → Dynamics shape) │ ▼ queue provider (Deno KV → Redis/RabbitMQ) (sized for backfill bursts in config) │ ▼ cron / scheduled trigger (nightly re-sync until cutover) ``` Every chapter on this spine is hands-on and closes on something you can observe — a JSON body, a log line, a file on disk. Chapter 3 also carries the track's one deliberately forward-looking section: the Python variant of the transform, marked plainly as a step for your own host because non-Deno runtimes are not sandboxed and need their interpreter installed. ## The chapters [1 · Scaffold the workspace Create `my-erp/` with `netscript init`, add the **workers** and **triggers** plugins, and boot the whole thing under Aspire. Ends at the dashboard on `:18888`.](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/01-scaffold/) [2 · Import job Watch `.data/incoming/products` with `defineFileWatch` and turn every `products_*.csv` the SAP export drops into a durable `defineJobHandler` background job that parses the rows.](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/02-import-job/) [3 · Polyglot transform Build and **run** the SAP→Dynamics transform as a sandboxed `deno` task: `defineTask`, explicit `.permissions()` compiled to `--allow-*` flags, and the executor. The Python variant is the caveated next step.](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/03-polyglot-transform/) [4 · Queue & cron Pick a `QueueProvider`, size worker concurrency in config, and add a `defineScheduledTrigger` cron for the nightly re-sync. Includes the concurrency env-var gotcha.](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/04-queue-and-cron/) [5 · Deploy locally Run the whole sync — workers, triggers, queue, and cron processors — on one machine under `aspire start`, and read it from the dashboard. Shows local-vs-production topology clearly.](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/05-deploy/) ## Before you start This track assumes the same local toolchain as the main ladder: a recent [Deno](https://deno.com/) and the [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/) CLI on your `PATH`, plus Docker running so Aspire can provision Postgres and Redis. This track uses Postgres (the default; swap `--db postgres` for `mysql`, `mssql`, or `sqlite` when you scaffold). If NetScript is brand new to you, walk the [Storefront tutorial](https://rickylabs.github.io/netscript/tutorials/storefront/) first — it explains every generated directory in more depth than we re-cover here. This track then re-grounds you from a fresh scaffold, so you can start either place. > One workspace, five chapters > > Every chapter grows the same > > my-erp/ > > project. Each one opens with a > > Before you begin > > state check and closes with > > What you built > > , so the work compounds instead of resetting between chapters. Follow them in order. [Tutorials](https://rickylabs.github.io/netscript/netscript/tutorials/) [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/01-scaffold/) After this track, keep building in **Build › [Background jobs](https://rickylabs.github.io/netscript/background-processing/)** — the guides and recipes there pick up where these chapters stop. _Canonical: https://rickylabs.github.io/netscript/tutorials/erp-sync/_ --- # Scaffold the workspace This is the first chapter of the ERP Sync track — the service that keeps **Microsoft Dynamics**, the ERP your team is migrating to, fed from the file exports of **SAP**, the legacy system that is still the system of record. Before you can watch the SAP file drops or run background jobs, you need a workspace with the right plugins installed and an orchestrator to run them. In this chapter you create `my-erp/`, add the **workers** and **triggers** plugins, and boot the whole stack under Aspire so the rest of the track has something real to build on. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/01-scaffold/) 2. [2 · Import job](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/02-import-job/) 3. [3 · Polyglot transform](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/03-polyglot-transform/) 4. [4 · Queue & cron](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/04-queue-and-cron/) 5. [5 · Deploy](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/05-deploy/) ## What you will build By the end of this chapter you will have `my-erp/` on disk — a NetScript workspace with a Postgres database, the **workers** plugin at `plugins/workers/` (the background-job engine on `:8091`), and the **triggers** plugin at `plugins/triggers/` (the ingress engine on `:8093`) — all running together under one `aspire start`, with the Aspire dashboard live on `:18888`. ## Before you begin You need the same local toolchain the [main tutorials](https://rickylabs.github.io/netscript/tutorials/) use: - **[Deno](https://deno.com/) 2.x** on your `PATH` — check with `deno --version`. - The **[Aspire CLI](https://aspire.dev)** — check with `aspire --version`. Aspire provisions your database and cache locally so you do not wire up Docker by hand. - **Docker** running, so Aspire can start the Postgres and Redis containers — confirm with `docker info`. Install the NetScript CLI from JSR once, then confirm it: ```sh deno install --global --allow-all --name netscript jsr:@netscript/cli@0.0.1-beta.11 netscript --help ``` You should see the public command groups, including `init`, `plugin`, `generate`, `db`, and `deploy`. If `netscript` is not found, make sure Deno's install directory is on your `PATH` and open a fresh terminal. > Prefer not to install globally? > > Run any command ad-hoc with > > deno x jsr:@netscript/cli@0.0.1-beta.11 > > . The rest of this track assumes the installed > > netscript > > form. ## Step 1 — Preview the scaffold with a dry run Before writing files, ask the CLI what it *would* create. `--dry-run` plans the scaffold and prints the file and directory totals per phase without touching disk: ```sh netscript init my-erp --dry-run ``` A clean dry run means your flag combination is valid — the CLI rejects bad option mixes (an unknown `--db` engine, for instance) here, before any files exist. Treat it as a green light to scaffold for real. ## Step 2 — Create the workspace This track needs a database to anchor durable execution, so scaffold with Postgres — the recommended default. `--db` is polyglot: swap `postgres` for `mysql`, `mssql`, or `sqlite` and the rest of the track works the same (this track uses `postgres` throughout): ```sh netscript init my-erp --db postgres cd my-erp ``` This scaffolds `my-erp/`, formats the output with `deno fmt`, and initializes a git repository. On completion the CLI prints a **next steps** summary tailored to your options. **netscript init options used here (run netscript init --help for the full list)** | Option | What it does | | --- | --- | | --db postgres | Scaffold a Postgres database workspace. Durable execution and queue persistence anchor on it. | | --dry-run | Plan the scaffold and print totals without writing any files. | | --no-aspire | Skip the Aspire orchestration files. Do NOT pass this — the rest of the track runs under Aspire. | > Where is packages/? > > If you scaffolded from a checkout of the NetScript repo you may see a vendored > > packages/ > > directory. A normal JSR install does not have one — your project pulls > > @netscript/* > > from the registry. Ignore > > packages/ > > in this track. ## Step 3 — Add the workers plugin NetScript's background capabilities arrive as plugins. Add the **workers** plugin with its sample jobs so you have a working reference to read and adapt: ```sh netscript plugin install worker --name workers --samples ``` This lands the plugin at **`plugins/workers/`** — the canonical, config-referenced install location — and registers it in `netscript.config.ts` (`./plugins/workers/mod.ts`) and `appsettings.json`. The workers plugin ships an API service on `:8091` and a separate background processor that drains the job queue. ## Step 4 — Add the triggers plugin Now add the **triggers** plugin, which is how NetScript receives events — including the file-watch trigger you build in [Chapter 2](https://rickylabs.github.io/netscript/tutorials/erp-sync/02-import-job/): ```sh netscript plugin install trigger --name triggers --samples ``` This lands a workspace at `plugins/triggers/` and registers it in `netscript.config.ts` (`./plugins/triggers/mod.ts`) and `appsettings.json`. The triggers API runs on `:8093`. Confirm both plugins registered: ```sh netscript plugin list ``` You should see `workers` and `triggers` in the registry. > Two trees, one canonical home > > A scaffold may also create slimmer top-level > > workers/ > > and > > triggers/ > > directories — workspace members that stage a subset of files for the background processors. The real, config-referenced plugins live at > > `plugins/workers/` > > and > > `plugins/triggers/` > > : that is what > > netscript.config.ts > > points at and where you author code. Edit under > > plugins/ > > . ## Step 5 — Bring up orchestration This is the step that turns a folder of files into a running system. **Aspire provisions your database and cache and starts every process; you do not start containers by hand, and you run it before any `netscript db` command.** Run it from the `aspire/` subfolder so the CLI sees `apphost.mts`: ```sh cd aspire aspire restore # once per machine: restores the Aspire SDK modules into .aspire/ aspire start # starts the AppHost and every declared resource ``` `aspire start` brings up Postgres, the Redis cache, the workers API + processor, and the triggers API - processor together, then prints a URL and a one-time login token for the **Aspire dashboard**: ``` https://localhost:18888 ``` The dashboard's **Resources** tab is the authority for which port each resource bound — the conventional assignments (workers `:8091`, triggers `:8093`) are what this two-plugin workspace lands on, allocated from the `:8091–8099` plugin range. Leave `aspire start` going in this terminal; it is your control plane for the rest of the track. > Aspire is step 2 — database commands need it running > > The Postgres container only exists while > > aspire start > > is up. So > > netscript db init > > , > > db generate > > , and > > db seed > > must run > > after > > Aspire has started — never before. There is more on the database sequence in > > Deploy locally with Aspire > > . ## Verify your progress In a second terminal (leave `aspire start` going in the first), confirm both plugin APIs are alive: ```sh curl http://localhost:8091/health # workers API curl http://localhost:8093/health # triggers API ``` Both should return a healthy JSON response. Then type-check the whole workspace from the project root: ```sh deno task check ``` Expected: a clean check with no errors — the scaffold, both plugins, and the database wiring all line up. - [ ] `my-erp/` exists with `plugins/workers/` and `plugins/triggers/` on disk. - [ ] `netscript plugin list` shows both `workers` and `triggers`. - [ ] `aspire start` is up; the dashboard on `:18888` lists `postgres`, `redis`, and both plugin APIs. - [ ] `curl :8091/health` and `curl :8093/health` both return healthy. - [ ] `deno task check` is clean. > If something is not green > > Three checks cover most first-run snags: (1) is > > aspire start > > still up, with > > postgres > > and > > redis > > healthy in the > > dashboard > > ? (2) is Docker running ( > > docker info > > )? (3) did you > > cd aspire > > before > > aspire start > > , so it found > > apphost.mts > > ? A failed > > curl > > usually means a service is still starting — give it a few seconds and retry. ## What you built A real NetScript workspace, `my-erp/`, with the **workers** and **triggers** plugins installed and the whole stack — Postgres, Redis, both plugin APIs and their background processors — running under one `aspire start` and visible in the dashboard. Next, you give it the SAP export to ingest. [ERP Sync](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/) [2 · Import job](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/02-import-job/) _Canonical: https://rickylabs.github.io/netscript/tutorials/erp-sync/01-scaffold/_ --- # A CSV file-watch import job In [Chapter 1](https://rickylabs.github.io/netscript/tutorials/erp-sync/01-scaffold/) you stood up `my-erp/` with the workers and triggers plugins running. Now you wire the first real piece of the pipeline: a **file-watch trigger** that fires the moment the SAP export job drops a CSV into the hand-off folder, and a durable **background job** that parses it. This is the ingest core of the migration — the SAP export reaches you as a file, not an API call, and every file it writes must become durable background work. A file the watcher misses is a day of catalog changes Dynamics never sees; a file parsed twice is a day counted twice. Both failure modes start here, so this chapter is where the pipeline earns the word *durable*. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/01-scaffold/) 2. [2 · Import job](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/02-import-job/) 3. [3 · Polyglot transform](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/03-polyglot-transform/) 4. [4 · Queue & cron](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/04-queue-and-cron/) 5. [5 · Deploy](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/05-deploy/) ## What you will build By the end of this chapter, dropping a file named `products_*.csv` into `my-erp/.data/incoming/products` will automatically enqueue and run a worker job that reads the file, parses its rows, logs a summary, and returns a structured success result — all without any HTTP call. You author two files: a **trigger** (`defineFileWatch`) and a **job** (`defineJobHandler`), then register the job so the trigger can address it by `id`. ## Before you begin You need the workspace from [Chapter 1](https://rickylabs.github.io/netscript/tutorials/erp-sync/01-scaffold/) with **both** the workers and triggers plugins installed, and `aspire start` healthy. Confirm the prior state from the project root: ```sh netscript plugin list ``` Expected: `workers` and `triggers` both appear. Also confirm Aspire is up — the file-watch processor and the workers runtime both depend on it: ```sh curl http://localhost:8091/health # workers API, healthy JSON curl http://localhost:8093/health # triggers API, healthy JSON ``` If either is missing, return to Chapter 1 — do not start over. ## Step 1 — Create the watched folder The trigger watches a directory; create it (and the staging path suppliers will write to) so the watcher has something to attach to: ```sh mkdir -p .data/incoming/products ``` This is just a local folder in your workspace. In production it would be the mounted share the SAP nightly export job writes into; the trigger does not care where the bytes come from, only that a matching file appears. ## Step 2 — Scaffold the import job A NetScript job is a function wrapped by `defineJobHandler`, given a stable `id`, and exported as the module default. Inside the handler you receive a `ctx` carrying the payload, do the work, and return a result built with `createSuccessResult` / `createFailureResult`. Create the job under the workers plugin. Let the CLI create the payload schema, handler wrapper, stable export, and registry entry: > ns-workers / ns-triggers are shorthands you install once > > ns-workers > > and > > ns-triggers > > are names > > you > > give the workers and triggers plugin CLIs — the scaffold does not create them. Install them once, globally, and every shorthand command on this page works as written: > > ```bash > deno install -gArf -n ns-workers jsr:@netscript/plugin-workers@0.0.1-beta.11/cli > deno install -gArf -n ns-triggers jsr:@netscript/plugin-triggers@0.0.1-beta.11/cli > ``` > > Rather not install them? Each > > ns- > > is exactly > > deno x -A jsr:@netscript/plugin-@0.0.1-beta.11/cli > > — run that full form instead. ```sh ns-workers add job import-products ``` In `workers/jobs/import-products.ts`, extend the generated `PayloadSchema` with `filePath` and `fileName`, import `createFailureResult`, and replace only the starter handler body with the CSV logic below: ```ts const handler = defineJobHandler(async (ctx) => { const { filePath, fileName } = PayloadSchema.parse(ctx.payload ?? {}); // 1. Read the staged file. let rawContent: string; try { rawContent = await Deno.readTextFile(filePath); } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); return createFailureResult(`Failed to read ${fileName}: ${message}`); } // 2. Parse a simple CSV (header row + data rows). No external lib needed. const lines = rawContent.trim().split('\n').filter((line) => line.trim().length > 0); if (lines.length < 2) { return createFailureResult('CSV is empty or has no data rows'); } const headers = lines[0].split(',').map((h) => h.trim().toLowerCase()); const rows = lines.slice(1).map((line) => { const values = line.split(',').map((v) => v.trim()); const row: Record = {}; headers.forEach((h, i) => row[h] = values[i] ?? ''); return row; }); // 3. Return a structured result. The runtime records it on the execution. return createSuccessResult({ fileName, rowCount: rows.length, headers }); }); ``` The two things to read off this: the handler is an `async`/arrow function (never a bare `function`), and the stable `id` is attached with `Object.assign` so the runtime registry can address the job by a predictable string rather than its filename. > Jobs do one thing > > This handler reads, parses, and reports — and stops. In a fuller ERP sync the next step (validate, upsert into a service, publish a saga message) would be its own job or a saga, keeping each unit small and independently retryable. The scaffold's > > --samples > > jobs show that fuller chain under > > plugins/workers/jobs/ > > ; for this track one job is enough to prove the pipeline. ## Step 3 — Scaffold the file-watch trigger Create the watcher from the current trigger CLI surface: The `add-file-watch` verb uses the spaced `add file-watch` shell syntax: ```sh ns-triggers add file-watch product-import-trigger \ --path=.data/incoming/products \ --pattern=products_*.csv ``` The command writes `triggers/product-import-trigger.ts` and refreshes the trigger registry. The file-watch command deliberately scaffolds a neutral handler, so replace that generated handler with the enqueue effect shown below. `defineFileWatch(handler, spec)` comes from `@netscript/plugin-triggers-core/builders`. The handler returns an **array of effects**; the one you want is `enqueueJob(jobRef, { payload })`, which hands a worker job the inbound event. The job is referenced by a small typed object — its `id`, `name`, `topic`, and `entrypoint`. ```ts import { defineFileWatch, enqueueJob } from '@netscript/plugin-triggers-core/builders'; import type { JobDefinition } from '@netscript/plugin-workers-core'; // A reference to the worker job authored in Step 2. const importProductsJob = { id: 'import-products' as JobDefinition<'import-products'>['id'], name: 'Import Products', topic: 'default', entrypoint: './workers/jobs/import-products.ts', } satisfies JobDefinition<'import-products'>; export default defineFileWatch( // event.payload carries filePath / fileName for the matched file. (event) => Promise.resolve([enqueueJob(importProductsJob, { payload: event.payload })]), { id: 'product-import-trigger', paths: ['.data/incoming/products'], patterns: ['products_*.csv'], on: ['create'], stabilityThreshold: { checkIntervalMs: 1000, stableChecks: 2 }, description: 'Watches for product CSV files and starts the import job.', tags: ['file-watch', 'product', 'import'], }, ); ``` Read the `spec` carefully — it is the whole behavior of the watcher: **FileWatchSpec — the static fields you set on defineFileWatch** | Name | Type | Description | | --- | --- | --- | | `id` | `string` | Stable identifier for this trigger, used in logs and the events feed. | | `paths` | `string[]` | Directories to watch. Here, the .data/incoming/products folder you created in Step 1. | | `patterns` | `string[]` | Glob patterns a file must match to fire the handler. products_*.csv ignores everything else. | | `on` | `('create' \| 'modify' \| 'remove')[]` | Which filesystem events fire the trigger. 'create' = a new file landing. | | `stabilityThreshold` | `{ checkIntervalMs, stableChecks }` | Debounce: wait until the file size is unchanged across stableChecks polls before firing, so a half-written upload is never parsed. | > stabilityThreshold is not optional polish > > A supplier copying a large file appears on disk before it is fully written. Without a stability check the watcher would fire on the first byte and your job would read a truncated CSV. The > > checkIntervalMs: 1000, stableChecks: 2 > > here waits for the size to hold steady for two one-second polls before enqueuing — set it generously for large files on slow shares. ## Step 4 — Register the job The trigger addresses the job by `id`, which means the workers runtime needs a generated registry that maps each `id` to its handler. Generate the plugin registries: ```sh netscript generate plugins ``` This scans `plugins/workers/jobs` (and the triggers plugin) and writes a registry the running services load. After this, `import-products` is addressable, and `product-import-trigger` is loaded by the file-watch processor. > Restart the processors after generating > > If > > aspire start > > was up before you generated the registry, restart it (or let it hot-reload) so the workers runtime and the file-watch processor pick up the new job and trigger. ## Verify your progress With Aspire up, drop a matching CSV into the watched folder and watch the pipeline run end to end. First create a sample file in the SAP export shape — legacy column names, prices in integer cents (Chapter 3 transforms exactly this file into Dynamics' shape): ```sh cat > .data/incoming/products_2024.csv <<'CSV' material_no,description,price_cents WID-1,Widget,999 GAD-2,Gadget,1999 CSV mv .data/incoming/products_2024.csv .data/incoming/products/products_2024.csv ``` Moving the finished file into the watched folder fires a single `create` event (and avoids the watcher seeing a half-written file). Now confirm both sides of the hand-off: ```sh # 1. The trigger recorded the inbound file event (:8093). curl 'http://localhost:8093/api/v1/events?limit=10' # 2. The job it enqueued executed (:8091). ns-workers executions --limit=10 --json ``` Expected: the events feed lists a `product-import-trigger` event, and the executions feed shows a completed `import-products` run whose result is `{ "fileName": "products_2024.csv", "rowCount": 2, "headers": ["material_no","description","price_cents"] }`. Open the `workers` resource logs in the [Aspire dashboard](https://localhost:18888) to read the job's structured log lines. - [ ] `.data/incoming/products/` exists and you dropped a `products_*.csv` into it. - [ ] The triggers events feed shows a `product-import-trigger` event. - [ ] The workers executions feed shows a completed `import-products` run with `rowCount: 2`. - [ ] `deno task check` is clean. > If the job never runs > > - **Aspire isn't up** — the file-watch processor and the workers runtime are Aspire resources. Start `aspire start` from `aspire/` and retry. > - **The job isn't registered** — re-run `netscript generate plugins` so `import-products` is in the generated registry, then restart Aspire. > - **Filename didn't match** — the pattern is `products_*.csv`. A file named `catalog.csv` in the same folder is ignored by design. > - **Wrong folder** — the file must land inside `.data/incoming/products`, the directory in `paths`, not its parent. ## What you built A file-watch trigger (`defineFileWatch`) that fires on every `products_*.csv` the SAP export drops into the hand-off folder, and a durable background job (`defineJobHandler`) that parses it — wired together by `enqueueJob` and made addressable with `netscript generate plugins`. An inbound SAP export now becomes durable background work with no HTTP in the loop. But the rows you just imported are still in the legacy shape — column names Dynamics does not use and prices in cents. Next, you build the transform stage that fixes that, and run it. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/01-scaffold/) [3 · Polyglot transform](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/03-polyglot-transform/) _Canonical: https://rickylabs.github.io/netscript/tutorials/erp-sync/02-import-job/_ --- # A polyglot transform task In [Chapter 2](https://rickylabs.github.io/netscript/tutorials/erp-sync/02-import-job/) you imported a SAP export as-is. But the SAP export is not a Dynamics import: the legacy system writes `material_no` where Dynamics wants `sku`, `description` where Dynamics wants `name`, and — the one that really hurts — prices as **integer cents** where Dynamics wants decimals. Load a legacy row into Dynamics untransformed and every price in the new system is wrong by a factor of one hundred. The pipeline needs a **transform stage**, and in NetScript that stage is a **task**: a standalone script defined with a builder, spawned as a subprocess, its result captured. In this chapter you build one and **run it**, using the `deno` runtime — the one task runtime NetScript sandboxes. 1. [1 · Scaffold](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/01-scaffold/) 2. [2 · Import job](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/02-import-job/) 3. [3 · Polyglot transform](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/03-polyglot-transform/) 4. [4 · Queue & cron](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/04-queue-and-cron/) 5. [5 · Deploy](https://rickylabs.github.io/netscript/netscript/tutorials/erp-sync/05-deploy/) ## What you will build By the end of this chapter you will have a runnable **`normalize-sap`** task: a transform script that reads the SAP export you dropped in Chapter 2, rewrites its legacy columns into Dynamics' shape, and writes the normalized file to a staging folder — executed through the workers task executor as a **sandboxed subprocess** whose filesystem access you granted explicitly. You will watch its output stream into your terminal, read its structured JSON result, and `cat` the normalized file it produced. You will also see how the **same builder chain** targets Python or shell when a transform belongs in another language — as a clearly-marked forward step for your own host, since those runtimes are not sandboxed and need their interpreter installed. ## Before you begin You need the `my-erp/` workspace from [Chapter 2](https://rickylabs.github.io/netscript/tutorials/erp-sync/02-import-job/) with the workers plugin installed and the SAP export still on disk from that chapter's file drop: ```sh netscript plugin list cat .data/incoming/products/products_2024.csv ``` Expected: `workers` appears in the plugin list, and the CSV prints the SAP legacy shape — `material_no,description,price_cents` and two rows. If the file is missing, re-create it exactly as in Chapter 2's [verify step](https://rickylabs.github.io/netscript/tutorials/erp-sync/02-import-job/). Aspire does not need to be running for this chapter — the task executor runs the transform directly. ## Step 1 — Write the transform script A subprocess task is a contract about two streams. Input goes **in** as argv and environment variables — never stdin. The result comes **back** as exactly one JSON object printed as the **last line of `stdout`**; everything else on `stdout`/`stderr` is captured as logs. Write the transform as a plain Deno script honoring that contract: ```ts // plugins/workers/scripts/normalize-sap.ts // Legacy SAP export rows in -> Microsoft Dynamics-shaped rows out. Runs as a sandboxed subprocess. // Input arrives as argv + env (NOT stdin). const input = Deno.args[Deno.args.indexOf('--input') + 1]; const outDir = Deno.env.get('STAGING_DIR') ?? '.data/staging'; const raw = await Deno.readTextFile(input); const lines = raw.trim().split('\n').filter((line) => line.trim().length > 0); const headers = lines[0].split(',').map((h) => h.trim().toLowerCase()); const col = (name: string) => headers.indexOf(name); // legacy -> target: material_no -> sku, description -> name, price_cents -> price (decimal). const out: string[] = ['sku,name,price']; let skipped = 0; for (const line of lines.slice(1)) { const values = line.split(',').map((v) => v.trim()); const sku = values[col('material_no')] ?? ''; const name = values[col('description')] ?? ''; const cents = Number.parseInt(values[col('price_cents')] ?? '', 10); if (sku === '' || Number.isNaN(cents)) { skipped++; continue; } out.push(`${sku},${name},${(cents / 100).toFixed(2)}`); } await Deno.mkdir(outDir, { recursive: true }); const fileName = input.split('/').pop() ?? 'export.csv'; const output = `${outDir}/${fileName.replace(/\.csv$/, '')}.normalized.csv`; await Deno.writeTextFile(output, out.join('\n') + '\n'); // Diagnostics go to stderr; the RESULT is the last stdout line and must be a // single JSON OBJECT (not an array) to populate result.result. console.error(`normalize-sap: ${lines.length - 1} rows in, ${out.length - 1} written`); console.log(JSON.stringify({ input, output, read: lines.length - 1, written: out.length - 1, skipped })); ``` Nothing here imports NetScript — that is the point. The script is an ordinary program with a narrow I/O contract, which is what lets the same execution model run TypeScript today and Python tomorrow. > The result is the LAST stdout line — and only if it is a JSON object > > A trailing log line, a pretty-print, a JSON > > array > > , or a stray newline after the payload all make the parsed result > > null > > even when the task otherwise succeeds. Emit the JSON object last and send everything else to > > stderr > > , as the script above does. ## Step 2 — Define the task, permissions included Now wrap the script in a task definition. `defineTask(id)` from `@netscript/plugin-workers-core/builders` returns a typestate builder: `.runtime(type)` selects the runtime (default `'deno'`), `.entrypoint(path)` points at the script and unlocks `.build()`, and input crosses as `.args(...)` plus `.env({...})`. For a `deno` task, `.permissions({...})` is the sandbox — each key compiles directly into an `--allow-*` flag on the spawned `deno run` command line: ```ts // plugins/workers/tasks/normalize-sap.ts import { defineTask } from '@netscript/plugin-workers-core/builders'; export const normalizeSap = defineTask('normalize-sap') .runtime('deno') // the default — and the only sandboxed runtime .entrypoint('./plugins/workers/scripts/normalize-sap.ts') .args('--input', '.data/incoming/products/products_2024.csv') .env({ STAGING_DIR: '.data/staging' }) .permissions({ read: ['.data'], // -> --allow-read=.data write: ['.data/staging'], // -> --allow-write=.data/staging env: ['STAGING_DIR'], // -> --allow-env=STAGING_DIR }) .timeout(30_000) // ms; defaults to 300_000 .build(); export default normalizeSap; ``` Read the permission set as a statement about the transform: it may read the incoming drop folder, write only to staging, and see one environment variable — nothing else. If the script ever tries to phone home or touch a file outside those grants, the Deno sandbox refuses at the subprocess boundary, not in your code review. > Omitting .permissions() on a deno task means --allow-all > > Calling > > .build() > > on a > > deno > > task > > without > > .permissions(...) > > produces an > > --allow-all > > command line — full access. Always pass an explicit, least-privilege set. The framework also ships named > > permissions > > presets ( > > minimal > > , > > readOnly > > , > > network > > , …) so you do not hand-roll the object for common cases — see > > Tune the worker runtime > > . ## Step 3 — Run it through the executor `createDefaultTaskExecutor()` from `@netscript/plugin-workers-core/executor` builds the multi-runtime executor wired with every built-in runtime adapter. `executor.execute(task)` resolves the adapter for the task's runtime, spawns the subprocess, streams its output, and returns one `TaskResult`. Write a small runner: ```ts // plugins/workers/run-normalize.ts import { createDefaultTaskExecutor } from '@netscript/plugin-workers-core/executor'; import { normalizeSap } from './tasks/normalize-sap.ts'; const executor = createDefaultTaskExecutor(); const result = await executor.execute(normalizeSap, { onStdout: (line) => console.log('[normalize]', line), onStderr: (line) => console.warn('[normalize:err]', line), }); if (result.success) { // result.result is the parsed JSON object from the LAST stdout line, or null. console.log('normalized', result.result, `in ${result.duration}ms`); } else { // status is 'failed' | 'timeout' | 'cancelled'; exitCode is -1 when the process never ran. console.error('task failed', result.status, result.exitCode, result.error); Deno.exit(1); } ``` Run it from the **workspace root** (the task's relative paths — entrypoint, input, staging — resolve from where you launch the runner): ```sh deno run -A plugins/workers/run-normalize.ts ``` The runner itself is trusted host code, so `-A` is fine here — the sandbox that matters is the **subprocess**: the executor spawns `deno run --allow-read=.data --allow-write=.data/staging --allow-env=STAGING_DIR …` with exactly the flags your permission set compiled to. You should see: ``` [normalize:err] normalize-sap: 2 rows in, 2 written [normalize] {"input":".data/incoming/products/products_2024.csv","output":".data/staging/products_2024.normalized.csv","read":2,"written":2,"skipped":0} normalized { input: ".data/incoming/products/products_2024.csv", output: ".data/staging/products_2024.normalized.csv", read: 2, written: 2, skipped: 0 } in 187ms ``` (Your duration will differ.) Now read the file the task produced: ```sh cat .data/staging/products_2024.normalized.csv ``` ``` sku,name,price WID-1,Widget,9.99 GAD-2,Gadget,19.99 ``` The cents are decimals, the legacy columns are Dynamics' names, and the off-by-100 price bug never gets a chance to exist. This is the transform stage of the pipeline: the [import job](https://rickylabs.github.io/netscript/tutorials/erp-sync/02-import-job/) stages the raw SAP rows, `normalize-sap` rewrites them for Dynamics, and a follow-up job would upsert the staged file. Tasks run through the same workers runtime as jobs and propagate W3C trace context (`TRACEPARENT`/`TRACESTATE`) into the subprocess, so a cross-runtime span still stitches together in the Aspire dashboard. ## Step 4 — The same chain in another language `normalize-sap` is TypeScript because a column rename needs nothing more. But some transforms live more naturally elsewhere — a pandas dedupe across historical SAP exports, a shell pipeline through `jq`, a .NET routine you already own. The builder chain is identical; only the `.runtime(...)` argument and the entrypoint change: ```ts // The Python variant of the same stage — a forward step for your own host. export const dedupeSap = defineTask('dedupe-sap') .runtime('python') .entrypoint('./plugins/workers/scripts/dedupe_sap.py') .args('--input', '.data/staging') .timeout(120_000) .build(); // Spawns: python3 -u ./plugins/workers/scripts/dedupe_sap.py --input .data/staging ``` The process contract is unchanged — argv + env in, one JSON object on the last `stdout` line out (the Python runtime runs `python3 -u`, unbuffered, for exactly that reason). Two things do change, and they are why this step is a **read-now, run-on-your-own-host** capability rather than part of this chapter's checkpoint: > Only the deno runtime is sandboxed > > For > > python > > , > > shell > > , > > powershell > > , > > cmd > > , > > dotnet > > , and > > executable > > tasks, the > > .permissions({...}) > > keys are > > ignored > > — the subprocess inherits the worker process's full OS-level access. A non-Deno task is a trust boundary: pin the entrypoint to a known script, prefer a pinned interpreter or venv ( > > pythonConfig.venvPath > > ) over > > $PATH > > discovery, never interpolate untrusted input into > > args > > , and gate access at the OS layer. > The runtime must exist on the worker host > > A missing interpreter surfaces as a > > failed task > > , not a thrown error: exit code > > 127 > > is reported as > > command not found > > and > > 126 > > as > > command not executable > > . Confirm > > python3 --version > > / > > pwsh --version > > / > > dotnet --version > > on the actual worker host before you ship a task that depends on it. When yours is ready, > > Run a polyglot task > > walks the Python and shell variants end to end, including interpreter pinning. Seven runtime types ship today — the literal members of the `TASK_TYPES` constant in `@netscript/plugin-workers-core`: **Task runtimes (TASK_TYPES) and their adapters** | Runtime | Spawns | Sandboxed? | Reach for it when | | --- | --- | --- | --- | | `deno` | `deno run` with compiled `--allow-*` flags | **Yes** — per-task permissions enforced | TypeScript/JS that should run with least privilege. The default, and what you just ran. | | `python` | `python3 -u