@north-light/cli 0.3.472 → 0.3.474
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/dto/nodes.d.ts +1 -1
- package/dist/builtin-memory/crouter-sdk/README.md +4 -47
- package/dist/builtin-memory/crouter-sdk/client.md +2 -231
- package/dist/builtin-memory/crouter-sdk/errors.md +9 -50
- package/dist/builtin-memory/crouter-sdk/getting-started.md +3 -65
- package/dist/builtin-memory/crouter-sdk/guides/README.md +1 -3
- package/dist/builtin-memory/crouter-sdk/guides/typed-extraction.md +1 -1
- package/dist/builtin-memory/crouter-sdk/nodes.md +1 -1
- package/dist/builtin-memory/crouter-sdk/release-notes.md +20 -20
- package/dist/builtin-memory/crouter-sdk/streaming.md +1 -70
- package/dist/builtin-memory/northlight-apps/INDEX.md +15 -0
- package/dist/builtin-memory/northlight-apps/cli.md +41 -0
- package/dist/builtin-memory/northlight-apps/coding-agents.md +75 -0
- package/dist/builtin-memory/northlight-apps/concepts/apps-and-sign-in.md +109 -0
- package/dist/builtin-memory/northlight-apps/concepts/connections.md +109 -0
- package/dist/builtin-memory/northlight-apps/concepts/providers.md +76 -0
- package/dist/builtin-memory/northlight-apps/concepts/runs.md +101 -0
- package/dist/builtin-memory/northlight-apps/concepts/scopes-and-grants.md +64 -0
- package/dist/builtin-memory/northlight-apps/concepts/testing-mode.md +54 -0
- package/dist/builtin-memory/northlight-apps/errors.md +119 -0
- package/dist/builtin-memory/northlight-apps/guides/background-runs.md +80 -0
- package/dist/builtin-memory/northlight-apps/guides/files-and-documents.md +61 -0
- package/dist/builtin-memory/northlight-apps/guides/go-live.md +39 -0
- package/dist/builtin-memory/northlight-apps/guides/grant-changes.md +100 -0
- package/dist/builtin-memory/northlight-apps/guides/manage-your-app.md +77 -0
- package/dist/builtin-memory/northlight-apps/guides/many-users.md +63 -0
- package/dist/builtin-memory/northlight-apps/guides/stream-a-run.md +84 -0
- package/dist/builtin-memory/{crouter-sdk/testing.md → northlight-apps/guides/test-your-app.md} +6 -6
- package/dist/builtin-memory/northlight-apps/guides/typed-results.md +99 -0
- package/dist/builtin-memory/northlight-apps/how-it-fits.md +49 -0
- package/dist/builtin-memory/northlight-apps/providers/README.md +39 -0
- package/dist/builtin-memory/northlight-apps/providers/context.md +34 -0
- package/dist/builtin-memory/northlight-apps/providers/device.md +28 -0
- package/dist/builtin-memory/northlight-apps/providers/integrations.md +28 -0
- package/dist/builtin-memory/northlight-apps/providers/search.md +31 -0
- package/dist/builtin-memory/northlight-apps/providers/sms.md +39 -0
- package/dist/builtin-memory/northlight-apps/quickstart.md +210 -0
- package/dist/commands/node/create.js +2 -2
- package/docs/apps/cli.md +40 -0
- package/docs/apps/coding-agents.md +72 -0
- package/docs/apps/concepts/apps-and-sign-in.md +82 -0
- package/docs/apps/concepts/connections.md +106 -0
- package/docs/apps/concepts/providers.md +59 -0
- package/docs/apps/concepts/runs.md +94 -0
- package/docs/apps/concepts/scopes-and-grants.md +62 -0
- package/docs/apps/concepts/testing-mode.md +36 -0
- package/docs/apps/errors.md +117 -0
- package/docs/apps/guides/background-runs.md +78 -0
- package/docs/apps/guides/files-and-documents.md +59 -0
- package/docs/apps/guides/go-live.md +37 -0
- package/docs/apps/guides/grant-changes.md +98 -0
- package/docs/apps/guides/manage-your-app.md +75 -0
- package/docs/apps/guides/many-users.md +61 -0
- package/docs/apps/guides/stream-a-run.md +82 -0
- package/docs/{sdk/testing.md → apps/guides/test-your-app.md} +5 -5
- package/docs/apps/guides/typed-results.md +72 -0
- package/docs/apps/how-it-fits.md +46 -0
- package/docs/apps/providers/README.md +26 -0
- package/docs/apps/providers/context.md +32 -0
- package/docs/apps/providers/device.md +26 -0
- package/docs/apps/providers/integrations.md +26 -0
- package/docs/apps/providers/search.md +29 -0
- package/docs/apps/providers/sms.md +37 -0
- package/docs/apps/quickstart.md +120 -2
- package/docs/cli/README.md +1 -0
- package/docs/cli/join-northlight.md +58 -0
- package/docs/sdk/README.md +4 -47
- package/docs/sdk/client.md +2 -231
- package/docs/sdk/errors.md +8 -49
- package/docs/sdk/getting-started.md +3 -65
- package/docs/sdk/guides/README.md +1 -3
- package/docs/sdk/guides/typed-extraction.md +1 -1
- package/docs/sdk/nodes.md +1 -1
- package/docs/sdk/release-notes.md +20 -20
- package/docs/sdk/streaming.md +1 -70
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
- package/dist/builtin-memory/crouter-sdk/error-envelope.md +0 -38
- package/dist/builtin-memory/crouter-sdk/guides/apps-on-northlight.md +0 -423
- package/dist/builtin-memory/crouter-sdk/guides/apps-with-many-users.md +0 -61
- package/dist/builtin-memory/crouter-sdk/memory.md +0 -49
- package/docs/sdk/error-envelope.md +0 -36
- package/docs/sdk/guides/apps-on-northlight.md +0 -419
- package/docs/sdk/guides/apps-with-many-users.md +0 -59
- package/docs/sdk/memory.md +0 -47
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Where your app runs, this knowledge should
|
|
4
|
+
be read because Your app runs on your server, people sign in at
|
|
5
|
+
account.northlight.ai, and the agent work runs on each person's own Northlight
|
|
6
|
+
runtime. Northlight does not host your app's code.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Where your app runs
|
|
10
|
+
|
|
11
|
+
Your app runs on a server you choose. Northlight hosts two other things: the directory at `account.northlight.ai`, and each person's agent runtime. Northlight does not host your app's code, and there is no flow for deploying it to Northlight.
|
|
12
|
+
|
|
13
|
+
| Piece | Runs where | What it does |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| **Your server** | Anywhere you choose. Your laptop while you develop. | Your pages and logic. It holds the app's private signing key and a stored connection for each person. |
|
|
16
|
+
| **The directory** | `https://account.northlight.ai`, hosted by Northlight | Accounts, app registration (the console at `/console`), consent, tokens and the marketplace. |
|
|
17
|
+
| **The person's runtime** | Hosted by Northlight, one per person | Their agents, runs, files, documents and providers (sms, search and others), using their own AI subscription. |
|
|
18
|
+
|
|
19
|
+
"Build on Northlight" therefore means three things. Your app's agent work runs on the person's Northlight-hosted runtime. People sign in to your app with their Northlight account. And your app can be listed in the Northlight marketplace.
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
sequenceDiagram
|
|
23
|
+
participant P as Person's browser
|
|
24
|
+
participant A as Your server
|
|
25
|
+
participant D as account.northlight.ai
|
|
26
|
+
participant R as Person's runtime
|
|
27
|
+
P->>A: GET /auth/login
|
|
28
|
+
A->>P: redirect to the directory (client ID, scopes, PKCE challenge)
|
|
29
|
+
P->>D: sign in, consent
|
|
30
|
+
D->>P: redirect to your /auth/callback with a code
|
|
31
|
+
P->>A: GET /auth/callback?code=…
|
|
32
|
+
A->>D: exchange the code, signed with your private key
|
|
33
|
+
D->>A: access token, refresh token, runtime URL
|
|
34
|
+
Note over A: store the connection for this person
|
|
35
|
+
A->>R: oauth.client(userId).runs.start(…)
|
|
36
|
+
R->>A: the run, its status and its result
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
1. **Sign-in.** Your `/auth/login` route sends the person to the directory with your client ID and the scopes you ask for. The directory shows its consent page and returns them to your registered redirect URI with a one-use code.
|
|
40
|
+
2. **Token.** Your server exchanges the code for tokens. It proves it is your app by signing the request with its private key. No client secret exists. The result is a connection: a short-lived access token, a rotating refresh token, and the URL of that person's runtime. See [[northlight-apps/concepts/apps-and-sign-in]].
|
|
41
|
+
3. **Calls.** `oauth.client(userId)` loads the stored connection, refreshes the access token when it needs to, and returns a client on that person's runtime. You call `runs.*` and `providers.*` on it. See [[northlight-apps/concepts/connections]] and [[northlight-apps/concepts/runs]].
|
|
42
|
+
|
|
43
|
+
The person's AI subscription pays for model calls, and what your app may do on their runtime is limited by the scopes they approved. See [[northlight-apps/concepts/scopes-and-grants]].
|
|
44
|
+
|
|
45
|
+
## What you operate
|
|
46
|
+
|
|
47
|
+
You operate your server, its sessions, its database and its `ConnectionStore`. You register the app in the console and keep its key pair. Northlight operates the directory and the runtimes; you do not run either, and there is no local Northlight to start. If you run crouter yourself and want a runtime of your own, that is a different path: see [crouter.docs.northlight.ai](https://crouter.docs.northlight.ai).
|
|
48
|
+
|
|
49
|
+
The directory's discovery document at `https://account.northlight.ai/.well-known/openid-configuration` lists the endpoints, the `S256` PKCE method and `private_key_jwt` as the only token authentication method (checked on production).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Providers, this knowledge should be read
|
|
4
|
+
because The first-party providers an app can call on a person's runtime, their
|
|
5
|
+
ids, how scope strings are named, and how to list a provider's tools.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Providers
|
|
9
|
+
|
|
10
|
+
A provider is a service the person's runtime calls for your app. Your app asks the runtime with `runtime.providers.call(...)`, and the runtime checks that the person approved the provider's scope for your app. See [[northlight-apps/concepts/providers]] for the model.
|
|
11
|
+
|
|
12
|
+
| Provider id | What it does | Scopes | Page |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
| `sms` | Texts the signed-in person's own verified phone | `sms:send`, `sms:status` | [[northlight-apps/providers/sms]] |
|
|
15
|
+
| `search` | Web search, sourced answers, page contents and paid data endpoints | `search:<group>` | [[northlight-apps/providers/search]] |
|
|
16
|
+
| `integrations` | The person's own accounts at other services | `integrations:discover`, `integrations:<service>_read`, `integrations:<service>_write` | [[northlight-apps/providers/integrations]] |
|
|
17
|
+
| `context` | Ask about the person from their memory, and look up public profiles | `context:<group>` | [[northlight-apps/providers/context]] |
|
|
18
|
+
| `device` | The person's Mac: browser, files, Messages and shell | `device:<group>` | [[northlight-apps/providers/device]] |
|
|
19
|
+
|
|
20
|
+
## Where names come from
|
|
21
|
+
|
|
22
|
+
- **Scope strings** come from the console's **What it asks for → Add permissions** list. A scope is `<provider id>:<group>`. Do not guess them.
|
|
23
|
+
- **Tool names and arguments** come from `runtime.providers.tools('<provider id>')`. It answers the provider's groups, and each group's tools with a description, parameters and output. It lists only the groups your app's grant holds, so ask for the scope first, then list.
|
|
24
|
+
- **Required providers.** If your app does not work without a provider being set up first, add it under **What your app requires** in the console. See [[northlight-apps/guides/grant-changes]].
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import type {ConnectedCrouter} from '@north-light/sdk';
|
|
28
|
+
|
|
29
|
+
/** List the tools a provider offers this person, then call one. Scope strings come from the console's
|
|
30
|
+
* "Add permissions" list; tool names and arguments come from `providers.tools`. Never invent either. */
|
|
31
|
+
export async function textMe(runtime: ConnectedCrouter, body: string) {
|
|
32
|
+
const sms = await runtime.providers.tools('sms');
|
|
33
|
+
console.log(JSON.stringify(sms.groups, null, 2)); // each leaf: tool, description, params
|
|
34
|
+
|
|
35
|
+
return runtime.providers.call({provider: 'sms', tool: 'send', arguments: {body}});
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Each provider page below shows the tools of a provider as captured on **2026-10-16**. The capture is not an app's view: it came from the runtime of a person's own Northlight account (`GET /v1/providers/<id>/tools` on `crtr` 0.3.462, joined to production), which shows the groups that person's own grant holds. What `providers.tools` answers for your app depends on the scopes your app was granted. Tools can change between releases, so treat a table as a starting point and list the tools at run time.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Context, this knowledge should be read
|
|
4
|
+
because Ask about the person from their memory, and look up public profiles,
|
|
5
|
+
through the context provider.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Context
|
|
9
|
+
|
|
10
|
+
The `context` provider answers questions about the person from what their agent knows, and looks up public profiles.
|
|
11
|
+
|
|
12
|
+
## Scopes and setup
|
|
13
|
+
|
|
14
|
+
Scopes are `context:<group>`. The console's **Add permissions** list shows `context:ask_informed` and `context:ask_public` on 2026-10-16; the tool list also shows a `research` group (`context:research`) that was not in that console list when we checked (not yet checked on production).
|
|
15
|
+
|
|
16
|
+
Context needs setup before it works: the person gives a profile URL on the provider's setup page, and its state stays `pending` for a few minutes afterwards. If your app depends on it, add `context` under **What your app requires** and follow the state with `grant.get().dependencies['context']`. See [[northlight-apps/guides/grant-changes]].
|
|
17
|
+
|
|
18
|
+
## Tools
|
|
19
|
+
|
|
20
|
+
Captured 2026-10-16 from `GET /v1/providers/context/tools` on a joined runtime (provider major version 1). `*` marks a required parameter. Tool names with a space are written as the provider lists them; check how to pass a multi-word `tool` to `providers.call` against your own `providers.tools` output before relying on it (not yet checked on production).
|
|
21
|
+
|
|
22
|
+
| Tool | Group | Parameters | What it does |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `ask informed` | `ask_informed` | `request`\*, `question` (repeatable, up to 10) | Advice about the person shaped by everything their agent knows, private memories included, without being told private details |
|
|
25
|
+
| `ask public` | `ask_public` | `request`\*, `question` (repeatable, up to 10) | Answers about the person from public memories only |
|
|
26
|
+
| `linkedin profile` | `research` | `profile`\* | A person's LinkedIn profile |
|
|
27
|
+
| `linkedin posts` | `research` | `profile`\*, `limit` | Recent LinkedIn posts |
|
|
28
|
+
| `x profile` | `research` | `profile`\* | A person's X profile |
|
|
29
|
+
| `x posts` | `research` | `profile`\*, `limit` | Not available |
|
|
30
|
+
| `github profile` | `research` | `profile`\* | A person's GitHub profile |
|
|
31
|
+
| `github repos` | `research` | `profile`\*, `limit` | Public repositories |
|
|
32
|
+
| `search people` | `research` | `query`\* | Search LinkedIn people |
|
|
33
|
+
|
|
34
|
+
`request` is the task you are doing, 1 to 4000 characters. Each `question` is 1 to 500 characters. Run `runtime.providers.tools('context')` for the current list and each tool's output shape.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: "When you need Device, this knowledge should be read
|
|
4
|
+
because Reach the person's Mac from your app: browser, files, Messages and
|
|
5
|
+
shell, each behind its own scope."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Device
|
|
9
|
+
|
|
10
|
+
The `device` provider reaches the person's own Mac. Its tools run on the person's machine (the provider's transport is `machine`), and each group is its own scope, so ask only for the groups your app needs. What happens when the Mac is asleep or offline is not yet checked on production.
|
|
11
|
+
|
|
12
|
+
## Scopes
|
|
13
|
+
|
|
14
|
+
`device:browser`, `device:files`, `device:messages_read`, `device:messages_send` and `device:shell`. Add them under **What it asks for → Add permissions** (provider `device`).
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
|
|
18
|
+
Captured 2026-10-16 from `GET /v1/providers/device/tools` on a joined runtime (provider major version 2, transport `machine`). `*` marks a required parameter.
|
|
19
|
+
|
|
20
|
+
| Group (scope) | Tools |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `browser` | `browser profiles`, `browser executor create` (`url`, `mode`, `profile`, `target-pattern`), `browser executor list`, `browser executor navigate` (`executor`\*, `url`\*), `browser executor exec` (`executor`\*, `code`\*, `timeout-ms`, `short-description`), `browser executor result` (`run`\*), `browser executor close` (`executor`\*), `browser libs` (`query`, `limit`, `service`, `fn`), `browser cdp` (`method`\*, `params`, `tab`) |
|
|
23
|
+
| `files` | `files list` (`path`\*, `limit`), `files stat` (`path`\*), `files read` (`path`\*, `encoding`, `offset`, `length`), `files write` (`path`\*, `text`, `base64`, `from`, `append`, `parents`), `files delete` (`path`\*, `recursive`), `files move` (`from`\*, `to`\*, `overwrite`), `files mkdir` (`path`\*) |
|
|
24
|
+
| `messages_read` | `messages chats` (`limit`), `messages history` (`chat`, `participant`, `since`, `until`, `limit`, `attachments`), `messages search` (`text`\*, `limit`), `messages contacts` (`query`\*, `limit`), `messages permissions` (`open`) |
|
|
25
|
+
| `messages_send` | `messages send` (`recipient`\*, `text`\*, `file`, `service`) |
|
|
26
|
+
| `shell` | `shell run` (`command`\*, `cwd`, `timeout`, `stdin`) |
|
|
27
|
+
|
|
28
|
+
Tool names with spaces are written as the provider lists them; check how to pass them to `providers.call` against your own `providers.tools` output (not yet checked on production). The capture is of a person's own grant, not an app's. Run `runtime.providers.tools('device')` for the list your app can use.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Integrations, this knowledge should be read
|
|
4
|
+
because Call the person's own accounts at other services through the
|
|
5
|
+
integrations provider, and find the tool you need.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Integrations
|
|
9
|
+
|
|
10
|
+
The `integrations` provider lets your app act in the person's own accounts at other services, with the person's consent for each service.
|
|
11
|
+
|
|
12
|
+
## Scopes
|
|
13
|
+
|
|
14
|
+
- `integrations:discover` allows `search` and `schema`, which find services and their tools, and the `link` group's `accounts`, which lists the person's linked accounts.
|
|
15
|
+
- `integrations:<service>_read` and `integrations:<service>_write` gate each service's tools. For example, a service named `gmail` has `integrations:gmail_read` and `integrations:gmail_write`.
|
|
16
|
+
|
|
17
|
+
Scope strings come from **What it asks for → Add permissions** (provider `integrations`). The catalog is large, so search it by service name.
|
|
18
|
+
|
|
19
|
+
## Find the tool you need
|
|
20
|
+
|
|
21
|
+
The provider has thousands of tools: on 2026-10-16 the runtime we captured from listed 3,061 tools across 3,060 groups, one `_read` group and one `_write` group per service, plus `discover` and `link`. This page does not table them. List them from your app:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const integrations = await runtime.providers.tools('integrations');
|
|
25
|
+
for (const group of integrations.groups) console.log(group.group, group.leaves.map((leaf) => leaf.tool));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`providers.tools('integrations')` is large (several megabytes on that runtime), and it lists only groups your app's grant holds, so it stays small until you add service scopes. To find a service or a tool, call the provider's `search` and `schema` tools (group `discover`) instead of reading the whole list.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Search, this knowledge should be read
|
|
4
|
+
because Web search, sourced answers, page contents, and paid data endpoints
|
|
5
|
+
through the search provider.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Search
|
|
9
|
+
|
|
10
|
+
The `search` provider runs web search and returns ranked pages, sourced answers, page contents and deep research, and it finds and calls paid data endpoints.
|
|
11
|
+
|
|
12
|
+
## Scopes
|
|
13
|
+
|
|
14
|
+
Each scope is `search:<group>`: `search:search`, `search:answer`, `search:contents`, `search:discover`, `search:research` and `search:run`. Add them under **What it asks for → Add permissions** (provider `search`). A tool is available when your app holds its group's scope.
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
|
|
18
|
+
Captured 2026-10-16 from `GET /v1/providers/search/tools` on a joined runtime (provider major version 1). `*` marks a required parameter.
|
|
19
|
+
|
|
20
|
+
| Tool | Group (scope) | Parameters | What it does |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `search` | `search` | `query`\*, `type`, `category`, `num-results`, `start-published-date`, `end-published-date`, `include-domains`, `exclude-domains`, `no-text` | Ranked web pages |
|
|
23
|
+
| `answer` | `answer` | `query`\*, `structured` | A sourced answer |
|
|
24
|
+
| `contents` | `contents` | `urls`\*, `no-text`, `highlights`, `summary`, `livecrawl` | Contents of known pages |
|
|
25
|
+
| `research` | `research` | `query`\*, `depth` | Deep web research |
|
|
26
|
+
| `discover` | `discover` | `query`\*, `limit`, `min-score` | Matching data endpoints and prices |
|
|
27
|
+
| `inspect` | `discover` | `provider`\*, `endpoint`\* | Data endpoint details and price |
|
|
28
|
+
| `run` | `run` | `provider`\*, `endpoint`\*, `input`\* | A paid data result, or a pending run ID |
|
|
29
|
+
| `status` | `run` | `run-id`\* | State and result of a data run |
|
|
30
|
+
|
|
31
|
+
The capture is of a person's own grant, not an app's. Run `runtime.providers.tools('search')` for what your app's grant holds. A call is billed to the person under your app's spend cap ([[northlight-apps/guides/grant-changes]]).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Texting (sms), this knowledge should be read
|
|
4
|
+
because Text the signed-in person's own verified phone from your app with the
|
|
5
|
+
sms provider.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Texting (sms)
|
|
9
|
+
|
|
10
|
+
The `sms` provider sends a text from Northlight's number to the verified phone of the person who signed in to your app. It has no recipient argument, so an app cannot text anyone else.
|
|
11
|
+
|
|
12
|
+
## Scopes
|
|
13
|
+
|
|
14
|
+
| Scope | Consent sentence | Gates |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `sms:send` | Send you text messages from Northlight's number | `send`, `react` |
|
|
17
|
+
| `sms:status` | Not captured | `status` |
|
|
18
|
+
|
|
19
|
+
Add them under **What it asks for → Add permissions** (provider `sms`). Without `sms:send` the consent page does not offer it and `providers.call` for `sms` is refused with `scope_missing`.
|
|
20
|
+
|
|
21
|
+
## Send a text
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
await runtime.providers.call({provider: 'sms', tool: 'send', arguments: {body: 'Your summary is ready.'}});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`body` is required. The text goes from Northlight's number to the person's verified phone, and each person can receive at most 50 texts from your app per UTC day. These limits are the provider's documented behavior (the 50-per-day limit is not yet checked on production).
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
Captured 2026-10-16 from `GET /v1/providers/sms/tools` on a joined runtime (provider major version 1). `*` marks a required parameter.
|
|
32
|
+
|
|
33
|
+
| Tool | Group (scope) | Parameters | What it does |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| `send` | `send` (`sms:send`) | `body`\*, `file`, `reply-to` | Send a text to your verified phone. `file` is a signed URL of at most 5 MiB sent after the text. `reply-to` is the label of a message in the conversation, such as `m3`, to send a threaded reply. Answers `message_id`. |
|
|
36
|
+
| `react` | `send` (`sms:send`) | `label`\*, `emoji`\* | React to a text in the conversation with a tapback or any emoji. |
|
|
37
|
+
| `status` | `status` (`sms:status`) | `message-id`\* | Check the delivery state of a text. |
|
|
38
|
+
|
|
39
|
+
Parameter names are the provider's own. In a `providers.call`, `arguments` is an object keyed by those names, such as `{body, file}`. Run `runtime.providers.tools('sms')` for the current list.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need Quick setup, this knowledge should be read
|
|
4
|
+
because From no account to a running app that signs a person in with
|
|
5
|
+
Northlight, starts a run on their runtime and sends them a text.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Quick setup
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx @north-light/sdk@latest init my-app
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This command creates a small Node server, a signing key and an env file. About ten minutes later you have an app on your laptop that signs you in with Northlight, starts an agent run on your own Northlight runtime, and texts your phone. The app runs on your machine and talks to Northlight's production directory at `https://account.northlight.ai`. There is no local Northlight stack to run, and you do not need crouter.
|
|
15
|
+
|
|
16
|
+
You need Node.js 22.19 or later and a phone that can receive iMessage.
|
|
17
|
+
|
|
18
|
+
Using a coding agent? Run the same `init` command, or tell your agent: *Read https://sdk.docs.northlight.ai/SKILL.md and follow it.* See [[northlight-apps/coding-agents]].
|
|
19
|
+
|
|
20
|
+
Each step below ends with what you should see when it worked. A step marked "(not yet checked on production)" is written from the source code and has not been run end to end against `account.northlight.ai`.
|
|
21
|
+
|
|
22
|
+
## 1. Create an account
|
|
23
|
+
|
|
24
|
+
Open `https://account.northlight.ai/signup` and create your account. Signup texts a confirmation code to your phone, and that text is sent over iMessage. A number that cannot receive iMessage gets "We couldn't send the text", and retrying does not help. One phone number belongs to one account.
|
|
25
|
+
|
|
26
|
+
You should see: your account's page after you enter the code. Opening `https://account.northlight.ai/console` while signed in shows the console, where your apps are listed. Checked on production: `/signup` redirects to the phone step at `/signup/phone`, and `/console` while signed out redirects to sign-in. Completing signup and the signed-in console are not yet checked on production.
|
|
27
|
+
|
|
28
|
+
## 2. Scaffold the app
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @north-light/sdk@latest init my-app
|
|
32
|
+
cd my-app
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`init` writes these files into `my-app/`:
|
|
36
|
+
|
|
37
|
+
| File | What it is |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `package.json` | Pins `@north-light/sdk` to the exact version of the CLI you ran, and adds `tsx`. |
|
|
40
|
+
| `server.ts` | The server shown at the end of this page. |
|
|
41
|
+
| `local/app-key.json` | Your app's private signing key, mode `0600`. Never commit it or paste it anywhere. |
|
|
42
|
+
| `local/app.env` | The settings the server reads: `CROUTER_ISSUER`, `APP_ORIGIN`, an empty `CROUTER_CLIENT_ID=` and `CROUTER_KEY_FILE`. |
|
|
43
|
+
| `.gitignore` | Keeps `local/` and `node_modules/` out of git. |
|
|
44
|
+
| `.agents/skills/northlight/SKILL.md` and `.claude/skills/northlight/SKILL.md` | The Northlight skill for your coding agent. |
|
|
45
|
+
|
|
46
|
+
You should see, on the terminal: `Created a Northlight app in my-app:` with the file names, the path of the private key, the next steps in order, and last, on its own line, the **public key** as one line of JSON:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{"kty":"EC","x":"…","y":"…","crv":"P-256","kid":"…","alg":"ES256","use":"sig"}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Copy that line. You paste it into the console in the next step. If a file already exists, `init` refuses, names it and exits non-zero without writing anything.
|
|
53
|
+
|
|
54
|
+
Checked: this output and these files, from the SDK source built on this branch in an empty directory with no crtr on `PATH`. `npx @north-light/sdk@latest` fetches the npm `latest` release, and that release does not include `init` until the site goes live (not yet checked on production).
|
|
55
|
+
|
|
56
|
+
## 3. Register the app in the console
|
|
57
|
+
|
|
58
|
+
Open `https://account.northlight.ai/console/new`. The page is titled **Register an app**. Fill in:
|
|
59
|
+
|
|
60
|
+
| Field | Value |
|
|
61
|
+
|---|---|
|
|
62
|
+
| **Name** | Shown to people on the consent page and in their account. |
|
|
63
|
+
| **Origin URL** | `http://localhost:3000` |
|
|
64
|
+
| **Logo URL** | Required. A square image shown beside your app's name. |
|
|
65
|
+
| **Why your app needs access** | One sentence. It is quoted on the consent page as your words. |
|
|
66
|
+
| **Redirect URIs** | `http://localhost:3000/auth/callback`, one URL per line. Sign-in returns only to a URI listed here, matched exactly. `localhost` and `127.0.0.1` are different URIs, and the port counts. |
|
|
67
|
+
| **Public key (optional)** | The JSON line `init` printed. |
|
|
68
|
+
|
|
69
|
+
Choose **Register app**.
|
|
70
|
+
|
|
71
|
+
You should see: a page titled "*your app's name* is registered" with a **Client ID** and, under it, the key ID of the key you pasted. Copy the client ID. If it says "Next: add your app's public key on the Sign-in tab", the key was left out: open the app's **Sign-in** tab and choose **Add key**. The app starts asking for `crtr:llm` and `crtr:act`.
|
|
72
|
+
|
|
73
|
+
The field labels are from the console's source. Pasting a key at registration is not yet checked on production.
|
|
74
|
+
|
|
75
|
+
## 4. Add the permission to text
|
|
76
|
+
|
|
77
|
+
On the app's page, open the **What it asks for** tab and choose **Add permissions**. Add `sms:send` from the list.
|
|
78
|
+
|
|
79
|
+
You should see: `sms:send` under what your app asks for, next to `crtr:llm` and `crtr:act`. The server in this guide asks for all three, so without `sms:send` the consent page does not offer it and the text in step 6 is refused. Take scope strings from this list. Do not invent them. See [[northlight-apps/concepts/scopes-and-grants]].
|
|
80
|
+
|
|
81
|
+
Not yet checked on production.
|
|
82
|
+
|
|
83
|
+
## 5. Write the client ID into the env file
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx @north-light/sdk env --client-id <client id>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
You should see:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
Set in local/app.env:
|
|
93
|
+
CROUTER_ISSUER=https://account.northlight.ai
|
|
94
|
+
CROUTER_CLIENT_ID=<client id>
|
|
95
|
+
APP_ORIGIN=http://localhost:3000
|
|
96
|
+
Key file: local/app-key.json (set CROUTER_KEY_FILE to change it).
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`local/app.env` now has all four settings. The command makes no network call and keeps every other line in the file. Add `--origin <url>` if your app does not run on `http://localhost:3000`.
|
|
100
|
+
|
|
101
|
+
Checked from the SDK source built on this branch.
|
|
102
|
+
|
|
103
|
+
## 6. Run it
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm i
|
|
107
|
+
node --env-file=local/app.env --import tsx server.ts
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
(`npm start` runs the same command.) You should see `Listening on http://localhost:3000`.
|
|
111
|
+
|
|
112
|
+
Open `http://localhost:3000/auth/login`. Your browser goes to `account.northlight.ai`. Sign in if asked, read the consent page, and choose Allow. You land back on `http://localhost:3000/` with "Signed in" and your Northlight user id. Press **Start a run and send me a text**. The page answers "Done", shows the run's status `idle`, and your phone receives "Hello from my first Northlight app." from Northlight's number.
|
|
113
|
+
|
|
114
|
+
What was checked: the server starts, `/` shows the sign-in link, and `/auth/login` answers `303` to `https://account.northlight.ai/authorize?client_id=…&scope=openid+profile+crtr%3Allm+crtr%3Aact+sms%3Asend…&code_challenge_method=S256…`. The consent page, the callback, the run and the text are not yet checked on production.
|
|
115
|
+
|
|
116
|
+
If `/` shows "One step left", `CROUTER_CLIENT_ID` is empty: repeat step 5 and restart. If a call fails with `426 runtime_version_unsupported`, your SDK is newer than the Northlight runtimes. Install the previous version with `npm i --save-exact @north-light/sdk@<version>` and tell support@northlight.ai.
|
|
117
|
+
|
|
118
|
+
## 7. What next
|
|
119
|
+
|
|
120
|
+
- Only you can sign in until you add testers: see [[northlight-apps/concepts/testing-mode]].
|
|
121
|
+
- Keep each person's sign-in across restarts and processes: see [[northlight-apps/concepts/connections]].
|
|
122
|
+
- See what `server.ts` does and where each piece runs: [[northlight-apps/how-it-fits]].
|
|
123
|
+
|
|
124
|
+
## The server `init` writes
|
|
125
|
+
|
|
126
|
+
This is `examples/apps/quickstart-server.ts`. The SDK reads no environment: the server passes the values in.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// The server `npx @north-light/sdk init` writes as server.ts: sign a person in with Northlight, then start a run on
|
|
130
|
+
// their runtime and send them a text. It keeps sign-in state in memory, so it is a sketch for one process, not production.
|
|
131
|
+
import {createServer, type IncomingMessage, type ServerResponse} from 'node:http';
|
|
132
|
+
import {randomBytes} from 'node:crypto';
|
|
133
|
+
import {readFile} from 'node:fs/promises';
|
|
134
|
+
import {MemoryConnectionStore, OAuth2Client, authorizationRefusalOf} from '@north-light/sdk';
|
|
135
|
+
|
|
136
|
+
const origin = process.env.APP_ORIGIN ?? 'http://localhost:3000';
|
|
137
|
+
|
|
138
|
+
const store = new MemoryConnectionStore(); // one process only; use a persistent ConnectionStore in production
|
|
139
|
+
const oauth = new OAuth2Client({
|
|
140
|
+
clientId: process.env.CROUTER_CLIENT_ID ?? '',
|
|
141
|
+
issuer: process.env.CROUTER_ISSUER ?? 'https://account.northlight.ai',
|
|
142
|
+
redirectUri: `${origin}/auth/callback`,
|
|
143
|
+
privateKey: JSON.parse(await readFile(process.env.CROUTER_KEY_FILE ?? 'local/app-key.json', 'utf8')),
|
|
144
|
+
store,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const pending = new Map<string, {codeVerifier: string; nonce: string}>(); // sign-ins in flight, keyed by state
|
|
148
|
+
const sessions = new Map<string, string>(); // session id -> userId; use a real session store in production
|
|
149
|
+
|
|
150
|
+
const escapeHtml = (text: string) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
151
|
+
const page = (res: ServerResponse, status: number, body: string) => {
|
|
152
|
+
res.writeHead(status, {'content-type': 'text/html; charset=utf-8'}).end(`<!doctype html><meta charset="utf-8"><title>Northlight app</title><body style="font-family:system-ui;max-width:40rem;margin:3rem auto">${body}`);
|
|
153
|
+
};
|
|
154
|
+
const userOf = (req: IncomingMessage) => {
|
|
155
|
+
const sid = /(?:^|; )sid=([\w-]+)/.exec(req.headers.cookie ?? '')?.[1];
|
|
156
|
+
return sid ? sessions.get(sid) : undefined;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const server = createServer(async (req, res) => {
|
|
160
|
+
const url = new URL(req.url ?? '/', origin);
|
|
161
|
+
try {
|
|
162
|
+
if (!process.env.CROUTER_CLIENT_ID) {
|
|
163
|
+
return page(res, 200, '<h1>One step left</h1><p>Register the app in the console, then run <code>npx @north-light/sdk env --client-id <client id></code> and restart this server.</p>');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (url.pathname === '/auth/login') {
|
|
167
|
+
const {url: authorizeUrl, codeVerifier, state, nonce} = await oauth.authorizeUrl({
|
|
168
|
+
scopes: ['openid', 'profile', 'crtr:llm', 'crtr:act', 'sms:send'],
|
|
169
|
+
state: randomBytes(16).toString('base64url'),
|
|
170
|
+
});
|
|
171
|
+
pending.set(state, {codeVerifier, nonce});
|
|
172
|
+
return void res.writeHead(303, {location: authorizeUrl}).end();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (url.pathname === '/auth/callback') {
|
|
176
|
+
if (authorizationRefusalOf(url)?.code === 'app_in_testing') {
|
|
177
|
+
return page(res, 403, '<h1>This app is still in testing</h1><p>Ask its owner to add you on the Testers tab in the console.</p>');
|
|
178
|
+
}
|
|
179
|
+
const state = url.searchParams.get('state') ?? '';
|
|
180
|
+
const code = url.searchParams.get('code');
|
|
181
|
+
const saved = pending.get(state);
|
|
182
|
+
if (!saved || !code) return page(res, 400, '<p>Start sign-in again at <a href="/auth/login">/auth/login</a>.</p>');
|
|
183
|
+
pending.delete(state);
|
|
184
|
+
const connection = await oauth.exchangeCode({code, codeVerifier: saved.codeVerifier, state, expectedState: state, nonce: saved.nonce});
|
|
185
|
+
await store.save(connection);
|
|
186
|
+
const sid = randomBytes(16).toString('base64url');
|
|
187
|
+
sessions.set(sid, connection.userId);
|
|
188
|
+
return void res.writeHead(303, {location: '/', 'set-cookie': `sid=${sid}; HttpOnly; SameSite=Lax; Path=/`}).end();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const userId = userOf(req);
|
|
192
|
+
if (!userId) return page(res, 200, '<h1>Northlight app</h1><p><a href="/auth/login">Sign in with Northlight</a></p>');
|
|
193
|
+
|
|
194
|
+
if (url.pathname === '/run' && req.method === 'POST') {
|
|
195
|
+
const runtime = await oauth.client(userId); // a client on this person's runtime, from their stored connection
|
|
196
|
+
const started = await runtime.runs.start({prompt: 'Reply with exactly the word: pong', scopes: ['crtr:llm', 'crtr:act']});
|
|
197
|
+
const run = await runtime.runs.wait(started.run_id); // status is 'idle' once the reply is done
|
|
198
|
+
// Texts the signed-in person's own verified phone through the sms provider (50 per person per UTC day).
|
|
199
|
+
const sent = await runtime.providers.call({provider: 'sms', tool: 'send', arguments: {body: 'Hello from my first Northlight app.'}});
|
|
200
|
+
return page(res, 200, `<h1>Done</h1><p>The run finished with status <code>${escapeHtml(run.status)}</code>. A text is on its way to your phone.</p><pre>${escapeHtml(JSON.stringify({run: run.run_id, outcome: run.outcome, report: run.last_report, text: sent}, null, 2))}</pre><p><a href="/">Back</a></p>`);
|
|
201
|
+
}
|
|
202
|
+
return page(res, 200, `<h1>Signed in</h1><p>You are <code>${escapeHtml(userId)}</code>.</p><form method="post" action="/run"><button>Start a run and send me a text</button></form>`);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error(error);
|
|
205
|
+
return page(res, 500, `<h1>Something failed</h1><pre>${escapeHtml(error instanceof Error ? error.message : String(error))}</pre>`);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
server.listen(Number(new URL(origin).port) || 3000, () => console.log(`Listening on ${origin}`));
|
|
210
|
+
```
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var K=Object.defineProperty;var a=(e,o)=>K(e,"name",{value:o,configurable:!0});import{envNodeId as Y}from"../../shared/env.js";import{defineLeaf as G}from"../../core/command.js";import{InputError as n}from"../../core/io.js";import{requireScope as V}from"../../core/errors.js";import{mark as q}from"../../core/timing.js";import{OUTPUT_SCHEMA_CONSTRAINTS as X,OUTPUT_SCHEMA_TRANSPORT as Q,parseOutputSchemaValue as Z}from"../../core/runtime/structured-output.js";import{MODEL_SPEC_FORMS as ee,MODEL_SPEC_NEXT as te}from"../../core/runtime/model-selection.js";import{parseDurationMs as oe}from"../../core/wake.js";import{kindsStateBlock as ne,profilesStateBlock as re,resolveProfileViaDaemon as ie}from"../profile/daemon-profiles.js";import{cliClient as F,getNodeOrNull as ae,rethrowAsCliError as H}from"../api-client.js";import{inTmux as L}from"../../core/runtime/placement-tmux.js";import{openSpawnViewer as se}from"../surface/node/placement.js";import{randomUUID as de}from"node:crypto";import{resolve as le}from"node:path";import{isDaemonTransportApiError as $}from"../../api/errors.js";import{ExitCode as ce}from"../../types.js";const ue=1e5,z='--output-schema writes the accepted result to the node\u2019s context/result.json and pushes it as the node\u2019s final report; a node that cannot honestly satisfy the schema declines with `crtr push result --decline "<reason>" --code <token>`, which finishes it with a failure outcome (reason `declined`) whose `declined` field carries { reason, code, retryable } and no result. Name the code tokens you accept in the prompt; crouter carries the code verbatim and you validate it.',W=["A resident is told never to push a final, so the spawner gets no report wake when the work is done.","If it stops mid-task, nothing prompts it to continue.","After 15 unattended minutes it is concluded with only a deferred update, which does not wake the spawner."],B="To re-task a terminal child after its final, use `crtr node message send --reopen`; to make an existing node resident later, use `crtr node config --lifecycle resident`.",N="Do not wait, poll, or duplicate this child's assignment in your own context \u2014 there is no result to await, stopping will not strand you, and duplicated work wastes the delegated context. You're auto-subscribed, so its finish wakes you on its own. Two moves only: do other independent work right now, or stop and end your turn \u2014 the wake brings you back.";async function he(e){if(e===void 0||e==="")return N;const o=await ae(e);if(o===null||o.mode!=="orchestrator")return N;const t=o.context_tokens??null;return t===null||t<ue?N:`Child spawned \u2014 you're auto-subscribed, so its finish wakes you on its own; never wait, poll, or duplicate the child's assignment in your own context. But you're an orchestrator already carrying ~${Math.round(t/1e3)}k of context: rather than hold this window open for the result, bring your document \`roadmap\` current (\`crtr doc edit roadmap\`) and \`crtr node yield\` now. Yielding ends this turn, and the child's completion then revives you fresh against your roadmap \u2014 let that clean revive absorb the result instead of this bloated context.`}a(he,"childFollowUp");function fe(e,o){throw $(e)||H(e),new n({error:"node_create_effect_unknown",message:`Node creation did not receive an acknowledgement after automatic recovery with request key ${o}; its effect is unknown.`,next:"Do not create a replacement node until the daemon is reachable and this request can be recovered."},ce.NETWORK)}a(fe,"createTransportFailure");function me(){return[{kind:"stdin",name:"prompt",required:!1,constraint:"First user message."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Persona kind. Defaults to the profile's default kind; `general` when the profile has none. The <kinds> list below names every top-level installable kind."},{kind:"flag",name:"mode",type:"enum",choices:["base","orchestrator"],required:!1,default:"base",constraint:"Persona mode. base for one hands-on unit; orchestrator when the unit itself needs decomposition across children. When you hold several parallel units of one kind, spawn ONE orchestrator child of that kind to own the fan-out rather than spawning the units yourself \u2014 unless you are already an orchestrator of that kind, whose job is exactly that fan-out."},{kind:"flag",name:"cwd",type:"path",required:!1,constraint:"Pin the node to this directory. Without --worktree, defaults to the spawner\u2019s directory for a managed child and the invocation directory for a root. With --worktree, selects the repository used for creation, defaulting to the invocation directory; the child runs in the new checkout."},{kind:"flag",name:"name",type:"string",required:!1,constraint:"Display name."},{kind:"flag",name:"parent",type:"string",required:!1,constraint:"Parent node id. Defaults to the calling node."},{kind:"flag",name:"node-id",type:"string",required:!1,constraint:"Exact id for the new node; lowercase letters, digits, and hyphens only, at most 128 characters. Rejected if already present."},{kind:"flag",name:"root",type:"bool",required:!1,constraint:"Spawn an independent root with no parent or subscription. Mutually exclusive with --worktree."},{kind:"flag",name:"resident",type:"bool",required:!1,constraint:"Birth the node as resident, a conversation partner for a person rather than a worker. Omit unless the user explicitly asks for a node they can talk with back and forth.",focusedHelp:{whenToUse:"Pass it ONLY when the user explicitly asks for a node they can talk with back and forth. Never infer it from task size, from expecting the user to look at the node, or from wanting to follow up later. "+B,value:"Boolean. Roots default to resident, so with --root it changes nothing. Managed children default to terminal; for a managed child the command first refuses, explains the cost, and says how to confirm.",effects:[...W,"It ends by going dormant or by being closed."]}},{kind:"flag",name:"terminal",type:"bool",required:!1,constraint:"Birth the node as terminal: it finalizes when its work is done and leaves nothing wakeable behind. Only meaningful with --root, whose default is resident; managed children are already terminal. Pass it for bounded programmatic work no one will open a viewer on. Mutually exclusive with --resident."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,hidden:!0,constraint:"Confirm a resident managed child (--resident without --root) after the user explicitly asked for one."},{kind:"flag",name:"worktree",type:"string",required:!1,constraint:"Existing local base branch for this child\u2019s managed worktree. When invoking from a managed worktree, also pass --cwd pointing at the repository\u2019s primary checkout; keep --worktree set to the desired parent branch. Mutually exclusive with --root."},{kind:"flag",name:"fork-from",type:"string",required:!1,constraint:"Fork the new node from an existing node id, absolute session path, or partial pi session uuid."},{kind:"flag",name:"model",type:"string",required:!1,constraint:"Durable model override. Omit unless the task or governing guidance requires a specific model.",focusedHelp:{whenToUse:"Use only when the user, task, or governing agent guidance already requires a specific provider, model, capability tier, or family. Omit it for routine delegation: the selected persona kind and profile own normal model selection, and review personas may enforce a quality floor.",value:`Accepted forms: ${ee}. Concrete provider/id requests must be registered. Portable provider/tier, bare-tier, and family-alias requests resolve through the configured model ladders.`,effects:["The resolved concrete model is stored on the node and in its launch recipe.","A concrete provider/id runs exactly that model on every launch and revive. A portable provider/tier, bare tier, or family alias keeps its tier: when that model's provider is rate-limited or rejects its credential, the node moves to the next model in its route: another provider's model at the same tier first, then the configured fallback tier (strong by default).","A persona quality floor may raise a weaker request before its resolved model is stored."]}},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile override. Use only when the task needs different project purview, owned documents, or environment; otherwise omit for normal inheritance or selection.",focusedHelp:{whenToUse:"Use only when the work must run under a different profile because it needs that profile\u2019s project purview, owned documents, stored environment, or defaults. Do not use it merely to change the working directory, persona kind, model, or graph ownership; those have separate parameters.",value:"Pass a profile id or name from the <profiles> list below; a close-enough name resolves to the nearest profile and the choice is reported on stderr. Omit it inside a node to inherit the caller\u2019s profile; outside a node, omission runs normal cwd-based profile selection.",effects:["The selected profile id is stored on the node and supplies its profile-scoped config, documents, environment, and injected bearings.","It does not change the parent or subscription; --cwd separately pins the working directory."]}},{kind:"flag",name:"situational-context",type:"string",required:!1,constraint:"Hidden ambient context appended to injected bearings, never visible chat."},{kind:"flag",name:"no-kickoff",type:"bool",required:!1,hidden:!0,constraint:"With --root, start the broker and session idle without a first message or model turn. Mutually exclusive with a prompt and --mode orchestrator."},{kind:"flag",name:"deadline",type:"string",required:!1,constraint:"Positive, representable wall-clock duration from spawn. Grammar: one or more positive integer-unit parts; units: s, m, h, d; adjacent parts are summed. Omit for no deadline."},{kind:"flag",name:"outcome-action",type:"string",required:!1,constraint:"Declared human-action name that receives the node outcome. Grammar: 1\u2013128 ASCII letters, digits, dots, underscores, or hyphens; starts with a letter or digit. Omit when no external delivery target is needed."},{kind:"flag",name:"outcome-payload",type:"string",required:!1,constraint:"JSON value, serialized at most 64 KiB, frozen and echoed to --outcome-action. Requires --outcome-action."},{kind:"flag",name:"output-schema",type:"string",required:!1,constraint:"Replace prose completion with a schema-constrained final result. Use only when its consumer requires fixed fields.",focusedHelp:{whenToUse:"Use only when the node\u2019s consumer needs a machine-checkable final object with fixed fields. Omit it for ordinary delegation or when a prose report is enough; structured output replaces the completion protocol, and an over-constrained schema can force retries.",value:`Pass the JSON Schema the result must satisfy. ${Q} ${X}`,effects:["The node must answer with `crtr push result` and cannot finish until the result is accepted; a plain `crtr push final` is blocked while the request is pending.",z,"The managed parent wakes through its existing subscription when the final report arrives; an independent root has no subscriber to wake.","An accepted result, or a decline, removes the request and terminates the node."]}}]}a(me,"nodeNewParams");function pe(){return[{name:"node_id",type:"string",required:!0,constraint:"New node id."},{name:"name",type:"string",required:!0,constraint:"Display name."},{name:"frozen_at",type:"string",required:!1,constraint:"Present when a managed child exists but capacity deferred its broker start."},...L()?[{name:"window",type:"string",required:!1,constraint:"tmux window id when a viewer opens."},{name:"session",type:"string",required:!1,constraint:"tmux session when a viewer opens."}]:[],{name:"follow_up",type:"string",required:!0,constraint:"Managed child: continue unrelated work or stop; its final report wakes you. Root: hand it off; no report wakes you."}]}a(pe,"nodeNewOutput");function we(){return["Creates nodes/<id>/ in the canvas home, indexes the node in canvas.db, and starts its detached broker engine.","A managed child creates an active parent subscription; its pushes wake the parent. --root creates an independent node with spawned_by provenance and no parent subscription.","--worktree creates a managed worktree at ~/.crouter/canvas/worktrees/<node-id>/ on a new crtr/<node-id> branch.",z,"--deadline persists a wall-clock deadline from spawn; expiry cancels the node and records a failure outcome with reason deadline_exceeded.","--outcome-action registers one resolved local human-action target. When the node settles, crouter retries delivery of its outcome document until the target accepts it or permanently rejects it; --outcome-payload is frozen and echoed in that document.",...L()?["--root opens a viewer for the new node in the current tmux session."]:[]]}a(we,"nodeNewEffects");async function ke(e){V("crtr:act","crtr node new");const o=e.prompt??"",t=e.noKickoff===!0;if(!t&&o.trim()==="")throw new n({error:"empty_prompt",message:"a prompt is required (stdin or positional)",next:"Pipe a task on stdin or pass it as an argument, or pass --no-kickoff to spawn an idle node with none."});if(t&&o.trim()!=="")throw new n({error:"no_kickoff_with_prompt",message:"--no-kickoff cannot be combined with a prompt",field:"no-kickoff",next:"Drop --no-kickoff, or drop the prompt."});const l=e.kind,T=e.mode??"base",c=e.cwd?.trim(),u=c!==void 0&&c!==""?le(c):void 0,O=e.name,J=e.parent,r=e.root===!0,h=e.resident===!0,S=e.terminal===!0;if(h&&S)throw new n({error:"lifecycle_conflict",message:"--resident and --terminal cannot both be passed",field:"terminal",next:"Pass the one birth lifecycle you want, or neither to take the default (resident for --root, terminal for a managed child)."});if(h&&!r&&e.force!==!0)throw new n({error:"resident_child_requires_force",message:`--resident on a managed child makes it a conversation partner for a person, not a worker that reports back. ${W.join(" ")}`,field:"resident",next:`If the user did not explicitly ask for a node they can talk with back and forth, drop --resident; a terminal child's final report wakes you. ${B} If the user did explicitly ask for one, re-run with --force.`});const f=e.worktree;if(f!==void 0&&f.trim()==="")throw new n({error:"worktree_base_required",message:"--worktree requires a base branch",field:"worktree",next:"Pass an existing local branch that landing can fast-forward."});const m=f?.trim(),P=e.forkFrom,p=e.model?.trim();if(p==="")throw new n({error:"empty_spec",message:"a model spec is required",field:"model",next:`Run \`crtr node new --model -h\` and read the focused contract. ${te}`});const E=e.profile,D=E===void 0?void 0:(await ie(E)).id,w=e.situationalContext?.trim(),C=w!==void 0&&w!==""?w:void 0,A=Z(e.outputSchema),U=A!==null?JSON.stringify(A):null,d=e.deadline;if(d!==void 0){const s=oe(d),x=s===null?Number.NaN:new Date(Date.now()+s).getTime();if(s===null||!Number.isFinite(s)||s<=0||!Number.isFinite(x))throw new n({error:"invalid_deadline",message:"--deadline must be a positive representable duration",field:"deadline",next:"Pass positive integer-unit parts using s, m, h, or d."})}const k=e.outcomeAction,y=e.outcomePayload;if(y!==void 0&&k===void 0)throw new n({error:"outcome_payload_requires_action",message:"--outcome-payload requires --outcome-action",field:"outcome-payload",next:"Pass the declared delivery action with --outcome-action."});let g;if(y!==void 0)try{g=JSON.parse(y)}catch{throw new n({error:"invalid_outcome_payload",message:"--outcome-payload must be valid JSON",field:"outcome-payload",next:"Pass one valid JSON value."})}const v=e.nodeId?.trim();if(r&&m!==void 0)throw new n({error:"worktree_root_conflict",message:"--worktree is mutually exclusive with --root",field:"worktree",next:"Drop either --root or --worktree."});if(t&&!r)throw new n({error:"no_kickoff_requires_root",message:"--no-kickoff is only valid with --root",field:"no-kickoff",next:"Add --root, or drop --no-kickoff and provide a prompt."});if(t&&T==="orchestrator")throw new n({error:"no_kickoff_orchestrator_unsupported",message:"--no-kickoff cannot be combined with --mode orchestrator",field:"no-kickoff",next:"Drop --no-kickoff and provide a prompt, or spawn in base mode."});const b=Y(),I=J??b;q("node_new.create_start",{root:r});const R=de(),M={...l!==void 0?{kind:l}:{},mode:T,cwd:process.cwd(),...u!==void 0&&u!==""?{pin_cwd:u}:{},...O!==void 0?{name:O}:{},prompt:o,...r?{root:!0}:{parent:I??null},...b!==void 0?{creator:b}:{},...h?{root_lifecycle:"resident"}:{},...S?{root_lifecycle:"terminal"}:{},...m!==void 0?{worktree:m}:{},...P!==void 0?{fork_from:P}:{},...p!==void 0?{model:p}:{},...D!==void 0?{profile:D}:{},...C!==void 0?{situational_context:C}:{},...t?{no_kickoff:!0}:{},...U!==null?{output_schema:U}:{},...d!==void 0?{deadline:d}:{},...k!==void 0?{outcome_delivery:{action:k,...g===void 0?{}:{payload:g}}}:{},...v!==void 0&&v!==""?{node_id:v}:{},...r?{prefer_warm:!0}:{},request_key:R};let i;try{i=await F().createNode(M)}catch(s){$(s)||H(s);try{i=await F().createNode(M)}catch(x){fe(x,R)}}q("node_new.node_created",{node:i.node_id});let _={};r&&(_=se(i.node_id,i.cwd,i.name)),r&&q("node_new.viewer_window_opened");const j=!r&&i.frozen_at!==null;return{node_id:i.node_id,name:i.name,...j?{frozen_at:i.frozen_at}:{},window:_.window,session:_.session,follow_up:r?t?"Independent root spawned idle. No kickoff turn ran; hand it off or attach and prompt it. No report wakes you.":"Independent root spawned. You are not subscribed, so its finish does not wake you; hand it off and move on.":j?"Child row exists, but no broker started because capacity is full. It is frozen and will launch when a broker slot frees; its final report wakes you then.":await he(I)}}a(ke,"runNodeCreation");const Re=G({name:"new",description:"create and start a node immediately",whenToUse:"a unit of work is genuinely separable from what you are doing: a bounded subtask you would otherwise grind out inline, several independent units worth running in parallel (usually via one orchestrator child of the matching kind rather than a flat fan-out from you), a scouting pass over unfamiliar code, a change that wants its own git worktree, or an answer you need in a fixed shape rather than as prose",tier:"important",help:{name:"node new",summary:"create a node immediately and start its detached broker",guide:"The prompt is the child\u2019s entire brief: it wakes with this prompt, its persona, and ambient bearings \u2014 none of your conversation carries over. State the outcome and constraints. Pass an implementation plan only when it is already settled or necessary to preserve an approved decision; otherwise, let the child investigate and choose how. Give the reason along with the request: the larger goal this serves and what the child's output enables, so the child resolves ambiguity toward your intent instead of guessing. Point at every document the task depends on (a spec, an explore map, an earlier child's findings) as `[[<node-id>/<name>]]`, or `[[name]]` for a repo, profile or user document; the child reads each with `crtr canvas read` and receives nothing the prompt does not point at. Name every file or directory as an absolute host path, never `~`-relative: the child's `~` is its own HOME, not yours, and a hook cwd outside its mounts (e.g. `/tmp`) is refused. When the report must carry something specific\u2014findings inline, or pointers to documents it wrote\u2014say so in the prompt.",params:me(),output:pe(),outputKind:"object",effects:we()},helpState:{leaf:ne,flags:{profile:re}},run:a(e=>ke(e),"run"),render:a(e=>{const o=e.window?` in window ${e.window}`:"",t=[`Spawned "${e.name}" (${e.node_id})${o}.`];return e.session&&t.push(`- session: ${e.session}`),t.push("",String(e.follow_up)),t.join(`
|
|
2
|
-
`)},"render")});export{
|
|
1
|
+
var G=Object.defineProperty;var s=(e,n)=>G(e,"name",{value:n,configurable:!0});import{envNodeId as V}from"../../shared/env.js";import{defineLeaf as X}from"../../core/command.js";import{InputError as o}from"../../core/io.js";import{requireScope as Q}from"../../core/errors.js";import{mark as N}from"../../core/timing.js";import{OUTPUT_SCHEMA_CONSTRAINTS as Z,OUTPUT_SCHEMA_TRANSPORT as ee,parseOutputSchemaValue as te}from"../../core/runtime/structured-output.js";import{MODEL_SPEC_FORMS as oe,MODEL_SPEC_NEXT as ne}from"../../core/runtime/model-selection.js";import{parseDurationMs as re}from"../../core/wake.js";import{kindsStateBlock as ie,profilesStateBlock as ae,resolveProfileViaDaemon as se}from"../profile/daemon-profiles.js";import{cliClient as H,getNodeOrNull as de,rethrowAsCliError as $}from"../api-client.js";import{inTmux as z}from"../../core/runtime/placement-tmux.js";import{openSpawnViewer as ce}from"../surface/node/placement.js";import{randomUUID as le}from"node:crypto";import{resolve as ue}from"node:path";import{isDaemonTransportApiError as W}from"../../api/errors.js";import{ExitCode as he}from"../../types.js";const fe=1e5,B='--output-schema writes the accepted result to the node\u2019s context/result.json and pushes it as the node\u2019s final report; a node that cannot honestly satisfy the schema declines with `crtr push result --decline "<reason>" --code <token>`, which finishes it with a failure outcome (reason `declined`) whose `declined` field carries { reason, code, retryable } and no result. Name the code tokens you accept in the prompt; crouter carries the code verbatim and you validate it.',K=["A resident is told never to push a final, so the spawner gets no report wake when the work is done.","If it stops mid-task, nothing prompts it to continue.","After 15 unattended minutes it is concluded with only a deferred update, which does not wake the spawner."],J="To re-task a terminal child after its final, use `crtr node message send --reopen`; to make an existing node resident later, use `crtr node config --lifecycle resident`.",T="Do not wait, poll, or duplicate this child's assignment in your own context \u2014 there is no result to await, stopping will not strand you, and duplicated work wastes the delegated context. You're auto-subscribed, so its finish wakes you on its own. Two moves only: do other independent work right now, or stop and end your turn \u2014 the wake brings you back.";async function me(e){if(e===void 0||e==="")return T;const n=await de(e);if(n===null||n.mode!=="orchestrator")return T;const t=n.context_tokens??null;return t===null||t<fe?T:`Child spawned \u2014 you're auto-subscribed, so its finish wakes you on its own; never wait, poll, or duplicate the child's assignment in your own context. But you're an orchestrator already carrying ~${Math.round(t/1e3)}k of context: rather than hold this window open for the result, bring your document \`roadmap\` current (\`crtr doc edit roadmap\`) and \`crtr node yield\` now. Yielding ends this turn, and the child's completion then revives you fresh against your roadmap \u2014 let that clean revive absorb the result instead of this bloated context.`}s(me,"childFollowUp");function pe(e,n){throw W(e)||$(e),new o({error:"node_create_effect_unknown",message:`Node creation did not receive an acknowledgement after automatic recovery with request key ${n}; its effect is unknown.`,next:"Do not create a replacement node until the daemon is reachable and this request can be recovered."},he.NETWORK)}s(pe,"createTransportFailure");function we(){return[{kind:"stdin",name:"prompt",required:!1,constraint:"First user message."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Persona kind. Defaults to the profile's default kind; `general` when the profile has none. The <kinds> list below names every top-level installable kind."},{kind:"flag",name:"mode",type:"enum",choices:["base","orchestrator"],required:!1,default:"base",constraint:"Persona mode. base for one hands-on unit; orchestrator when the unit itself needs decomposition across children. When you hold several parallel units of one kind, spawn ONE orchestrator child of that kind to own the fan-out rather than spawning the units yourself \u2014 unless you are already an orchestrator of that kind, whose job is exactly that fan-out."},{kind:"flag",name:"cwd",type:"path",required:!1,constraint:"Pin the node to this directory. Without --worktree, defaults to the spawner\u2019s directory for a managed child and the invocation directory for a root. With --worktree, selects the repository used for creation, defaulting to the invocation directory; the child runs in the new checkout."},{kind:"flag",name:"name",type:"string",required:!1,constraint:"Display name."},{kind:"flag",name:"parent",type:"string",required:!1,constraint:"Parent node id. Defaults to the calling node."},{kind:"flag",name:"node-id",type:"string",required:!1,constraint:"Exact id for the new node; lowercase letters, digits, and hyphens only, at most 128 characters. Rejected if already present."},{kind:"flag",name:"root",type:"bool",required:!1,constraint:"Spawn an independent root with no parent or subscription. Mutually exclusive with --worktree."},{kind:"flag",name:"resident",type:"bool",required:!1,constraint:"Birth the node as resident, a conversation partner for a person rather than a worker. Omit unless the user explicitly asks for a node they can talk with back and forth.",focusedHelp:{whenToUse:"Pass it ONLY when the user explicitly asks for a node they can talk with back and forth. Never infer it from task size, from expecting the user to look at the node, or from wanting to follow up later. "+J,value:"Boolean. Roots default to resident, so with --root it changes nothing. Managed children default to terminal; for a managed child the command first refuses, explains the cost, and says how to confirm.",effects:[...K,"It ends by going dormant or by being closed."]}},{kind:"flag",name:"terminal",type:"bool",required:!1,constraint:"Birth the node as terminal: it finalizes when its work is done and leaves nothing wakeable behind. Only meaningful with --root, whose default is resident; managed children are already terminal. Pass it for bounded programmatic work no one will open a viewer on. Mutually exclusive with --resident."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,hidden:!0,constraint:"Confirm a resident managed child (--resident without --root) after the user explicitly asked for one."},{kind:"flag",name:"worktree",type:"string",required:!1,constraint:"Existing local base branch for this child\u2019s managed worktree. When invoking from a managed worktree, also pass --cwd pointing at the repository\u2019s primary checkout; keep --worktree set to the desired parent branch. Mutually exclusive with --root."},{kind:"flag",name:"fork-from",type:"string",required:!1,constraint:"Fork the new node from an existing node id, absolute session path, or partial pi session uuid."},{kind:"flag",name:"model",type:"string",required:!1,constraint:"Durable model override. Omit unless the task or governing guidance requires a specific model.",focusedHelp:{whenToUse:"Use only when the user, task, or governing agent guidance already requires a specific provider, model, capability tier, or family. Omit it for routine delegation: the selected persona kind and profile own normal model selection, and review personas may enforce a quality floor.",value:`Accepted forms: ${oe}. Concrete provider/id requests must be registered. Portable provider/tier, bare-tier, and family-alias requests resolve through the configured model ladders.`,effects:["The resolved concrete model is stored on the node and in its launch recipe.","A concrete provider/id runs exactly that model on every launch and revive. A portable provider/tier, bare tier, or family alias keeps its tier: when that model's provider is rate-limited or rejects its credential, the node moves to the next model in its route: another provider's model at the same tier first, then the configured fallback tier (strong by default).","A persona quality floor may raise a weaker request before its resolved model is stored."]}},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile override. Use only when the task needs different project purview, owned documents, or environment; otherwise omit for normal inheritance or selection.",focusedHelp:{whenToUse:"Use only when the work must run under a different profile because it needs that profile\u2019s project purview, owned documents, stored environment, or defaults. Do not use it merely to change the working directory, persona kind, model, or graph ownership; those have separate parameters.",value:"Pass a profile id or name from the <profiles> list below; a close-enough name resolves to the nearest profile and the choice is reported on stderr. Omit it inside a node to inherit the caller\u2019s profile; outside a node, omission runs normal cwd-based profile selection.",effects:["The selected profile id is stored on the node and supplies its profile-scoped config, documents, environment, and injected bearings.","It does not change the parent or subscription; --cwd separately pins the working directory."]}},{kind:"flag",name:"situational-context",type:"string",required:!1,constraint:"Hidden ambient context appended to injected bearings, never visible chat."},{kind:"flag",name:"no-kickoff",type:"bool",required:!1,hidden:!0,constraint:"With --root, start the broker and session idle without a first message or model turn. Mutually exclusive with a prompt and --mode orchestrator."},{kind:"flag",name:"scopes",type:"string",required:!1,constraint:"Comma-separated scope allow-list for the node and every descendant (they inherit it and cannot widen it), e.g. `crtr:llm,crtr:act,device:shell,device:messages_read`. A provider-group call (`crtr device messages send` needs `device:messages_send`) is refused with scope_missing before any request leaves the machine when the group is absent. Must be a subset of what you hold; `crtr:act` requires `crtr:llm`. Enforced on the broker route (the node's own `crtr` calls); on an unsandboxed host a same-UID process can bypass it by calling the daemon socket directly (crouter#146), so it guards against a mistaken call, not a hostile node. Keep the list short: it travels in a request header on every device call (crouter#147)."},{kind:"flag",name:"deadline",type:"string",required:!1,constraint:"Positive, representable wall-clock duration from spawn. Grammar: one or more positive integer-unit parts; units: s, m, h, d; adjacent parts are summed. Omit for no deadline."},{kind:"flag",name:"outcome-action",type:"string",required:!1,constraint:"Declared human-action name that receives the node outcome. Grammar: 1\u2013128 ASCII letters, digits, dots, underscores, or hyphens; starts with a letter or digit. Omit when no external delivery target is needed."},{kind:"flag",name:"outcome-payload",type:"string",required:!1,constraint:"JSON value, serialized at most 64 KiB, frozen and echoed to --outcome-action. Requires --outcome-action."},{kind:"flag",name:"output-schema",type:"string",required:!1,constraint:"Replace prose completion with a schema-constrained final result. Use only when its consumer requires fixed fields.",focusedHelp:{whenToUse:"Use only when the node\u2019s consumer needs a machine-checkable final object with fixed fields. Omit it for ordinary delegation or when a prose report is enough; structured output replaces the completion protocol, and an over-constrained schema can force retries.",value:`Pass the JSON Schema the result must satisfy. ${ee} ${Z}`,effects:["The node must answer with `crtr push result` and cannot finish until the result is accepted; a plain `crtr push final` is blocked while the request is pending.",B,"The managed parent wakes through its existing subscription when the final report arrives; an independent root has no subscriber to wake.","An accepted result, or a decline, removes the request and terminates the node."]}}]}s(we,"nodeNewParams");function ke(){return[{name:"node_id",type:"string",required:!0,constraint:"New node id."},{name:"name",type:"string",required:!0,constraint:"Display name."},{name:"frozen_at",type:"string",required:!1,constraint:"Present when a managed child exists but capacity deferred its broker start."},...z()?[{name:"window",type:"string",required:!1,constraint:"tmux window id when a viewer opens."},{name:"session",type:"string",required:!1,constraint:"tmux session when a viewer opens."}]:[],{name:"follow_up",type:"string",required:!0,constraint:"Managed child: continue unrelated work or stop; its final report wakes you. Root: hand it off; no report wakes you."}]}s(ke,"nodeNewOutput");function ye(){return["Creates nodes/<id>/ in the canvas home, indexes the node in canvas.db, and starts its detached broker engine.","A managed child creates an active parent subscription; its pushes wake the parent. --root creates an independent node with spawned_by provenance and no parent subscription.","--worktree creates a managed worktree at ~/.crouter/canvas/worktrees/<node-id>/ on a new crtr/<node-id> branch.",B,"--deadline persists a wall-clock deadline from spawn; expiry cancels the node and records a failure outcome with reason deadline_exceeded.","--outcome-action registers one resolved local human-action target. When the node settles, crouter retries delivery of its outcome document until the target accepts it or permanently rejects it; --outcome-payload is frozen and echoed in that document.",...z()?["--root opens a viewer for the new node in the current tmux session."]:[]]}s(ye,"nodeNewEffects");async function ge(e){Q("crtr:act","crtr node new");const n=e.prompt??"",t=e.noKickoff===!0;if(!t&&n.trim()==="")throw new o({error:"empty_prompt",message:"a prompt is required (stdin or positional)",next:"Pipe a task on stdin or pass it as an argument, or pass --no-kickoff to spawn an idle node with none."});if(t&&n.trim()!=="")throw new o({error:"no_kickoff_with_prompt",message:"--no-kickoff cannot be combined with a prompt",field:"no-kickoff",next:"Drop --no-kickoff, or drop the prompt."});const l=e.kind,O=e.mode??"base",u=e.cwd?.trim(),h=u!==void 0&&u!==""?ue(u):void 0,S=e.name,Y=e.parent,i=e.root===!0,f=e.resident===!0,P=e.terminal===!0;if(f&&P)throw new o({error:"lifecycle_conflict",message:"--resident and --terminal cannot both be passed",field:"terminal",next:"Pass the one birth lifecycle you want, or neither to take the default (resident for --root, terminal for a managed child)."});if(f&&!i&&e.force!==!0)throw new o({error:"resident_child_requires_force",message:`--resident on a managed child makes it a conversation partner for a person, not a worker that reports back. ${K.join(" ")}`,field:"resident",next:`If the user did not explicitly ask for a node they can talk with back and forth, drop --resident; a terminal child's final report wakes you. ${J} If the user did explicitly ask for one, re-run with --force.`});const m=e.worktree;if(m!==void 0&&m.trim()==="")throw new o({error:"worktree_base_required",message:"--worktree requires a base branch",field:"worktree",next:"Pass an existing local branch that landing can fast-forward."});const p=m?.trim(),E=e.forkFrom,w=e.model?.trim();if(w==="")throw new o({error:"empty_spec",message:"a model spec is required",field:"model",next:`Run \`crtr node new --model -h\` and read the focused contract. ${ne}`});const D=e.profile,C=D===void 0?void 0:(await se(D)).id,k=e.situationalContext?.trim(),A=k!==void 0&&k!==""?k:void 0,U=te(e.outputSchema),I=U!==null?JSON.stringify(U):null,d=e.deadline;if(d!==void 0){const r=re(d),q=r===null?Number.NaN:new Date(Date.now()+r).getTime();if(r===null||!Number.isFinite(r)||r<=0||!Number.isFinite(q))throw new o({error:"invalid_deadline",message:"--deadline must be a positive representable duration",field:"deadline",next:"Pass positive integer-unit parts using s, m, h, or d."})}const y=e.outcomeAction,g=e.outcomePayload;if(g!==void 0&&y===void 0)throw new o({error:"outcome_payload_requires_action",message:"--outcome-payload requires --outcome-action",field:"outcome-payload",next:"Pass the declared delivery action with --outcome-action."});let v;if(g!==void 0)try{v=JSON.parse(g)}catch{throw new o({error:"invalid_outcome_payload",message:"--outcome-payload must be valid JSON",field:"outcome-payload",next:"Pass one valid JSON value."})}const R=e.scopes,c=R===void 0?void 0:R.split(",").map(r=>r.trim()).filter(r=>r!=="");if(c!==void 0&&c.length===0)throw new o({error:"empty_scopes",message:"--scopes names no scope",field:"scopes",next:"Pass a comma-separated list such as crtr:llm,crtr:act, or omit --scopes to inherit."});const b=e.nodeId?.trim();if(i&&p!==void 0)throw new o({error:"worktree_root_conflict",message:"--worktree is mutually exclusive with --root",field:"worktree",next:"Drop either --root or --worktree."});if(t&&!i)throw new o({error:"no_kickoff_requires_root",message:"--no-kickoff is only valid with --root",field:"no-kickoff",next:"Add --root, or drop --no-kickoff and provide a prompt."});if(t&&O==="orchestrator")throw new o({error:"no_kickoff_orchestrator_unsupported",message:"--no-kickoff cannot be combined with --mode orchestrator",field:"no-kickoff",next:"Drop --no-kickoff and provide a prompt, or spawn in base mode."});const _=V(),M=Y??_;N("node_new.create_start",{root:i});const j=le(),L={...l!==void 0?{kind:l}:{},mode:O,cwd:process.cwd(),...h!==void 0&&h!==""?{pin_cwd:h}:{},...S!==void 0?{name:S}:{},prompt:n,...i?{root:!0}:{parent:M??null},..._!==void 0?{creator:_}:{},...f?{root_lifecycle:"resident"}:{},...P?{root_lifecycle:"terminal"}:{},...p!==void 0?{worktree:p}:{},...E!==void 0?{fork_from:E}:{},...w!==void 0?{model:w}:{},...C!==void 0?{profile:C}:{},...A!==void 0?{situational_context:A}:{},...t?{no_kickoff:!0}:{},...I!==null?{output_schema:I}:{},...c!==void 0?{scopes:c}:{},...d!==void 0?{deadline:d}:{},...y!==void 0?{outcome_delivery:{action:y,...v===void 0?{}:{payload:v}}}:{},...b!==void 0&&b!==""?{node_id:b}:{},...i?{prefer_warm:!0}:{},request_key:j};let a;try{a=await H().createNode(L)}catch(r){W(r)||$(r);try{a=await H().createNode(L)}catch(q){pe(q,j)}}N("node_new.node_created",{node:a.node_id});let x={};i&&(x=ce(a.node_id,a.cwd,a.name)),i&&N("node_new.viewer_window_opened");const F=!i&&a.frozen_at!==null;return{node_id:a.node_id,name:a.name,...F?{frozen_at:a.frozen_at}:{},window:x.window,session:x.session,follow_up:i?t?"Independent root spawned idle. No kickoff turn ran; hand it off or attach and prompt it. No report wakes you.":"Independent root spawned. You are not subscribed, so its finish does not wake you; hand it off and move on.":F?"Child row exists, but no broker started because capacity is full. It is frozen and will launch when a broker slot frees; its final report wakes you then.":await me(M)}}s(ge,"runNodeCreation");const je=X({name:"new",description:"create and start a node immediately",whenToUse:"a unit of work is genuinely separable from what you are doing: a bounded subtask you would otherwise grind out inline, several independent units worth running in parallel (usually via one orchestrator child of the matching kind rather than a flat fan-out from you), a scouting pass over unfamiliar code, a change that wants its own git worktree, or an answer you need in a fixed shape rather than as prose",tier:"important",help:{name:"node new",summary:"create a node immediately and start its detached broker",guide:"The prompt is the child\u2019s entire brief: it wakes with this prompt, its persona, and ambient bearings \u2014 none of your conversation carries over. State the outcome and constraints. Pass an implementation plan only when it is already settled or necessary to preserve an approved decision; otherwise, let the child investigate and choose how. Give the reason along with the request: the larger goal this serves and what the child's output enables, so the child resolves ambiguity toward your intent instead of guessing. Point at every document the task depends on (a spec, an explore map, an earlier child's findings) as `[[<node-id>/<name>]]`, or `[[name]]` for a repo, profile or user document; the child reads each with `crtr canvas read` and receives nothing the prompt does not point at. Name every file or directory as an absolute host path, never `~`-relative: the child's `~` is its own HOME, not yours, and a hook cwd outside its mounts (e.g. `/tmp`) is refused. When the report must carry something specific\u2014findings inline, or pointers to documents it wrote\u2014say so in the prompt.",params:we(),output:ke(),outputKind:"object",effects:ye()},helpState:{leaf:ie,flags:{profile:ae}},run:s(e=>ge(e),"run"),render:s(e=>{const n=e.window?` in window ${e.window}`:"",t=[`Spawned "${e.name}" (${e.node_id})${n}.`];return e.session&&t.push(`- session: ${e.session}`),t.push("",String(e.follow_up)),t.join(`
|
|
2
|
+
`)},"render")});export{me as childFollowUp,je as nodeNew};
|
package/docs/apps/cli.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: CLI reference
|
|
3
|
+
description: The npx @north-light/sdk commands for scaffolding an app, writing its signing key and environment file, and installing the agent skill.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CLI reference
|
|
7
|
+
|
|
8
|
+
`npx @north-light/sdk <command>` runs the package's one command-line tool. Run it with no command, or with `--help`, to print the list below. An unknown command prints the list and exits 1.
|
|
9
|
+
|
|
10
|
+
| Command | What it does | Needs crtr |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `npx @north-light/sdk init [dir] [--agent claude\|agents\|all] [--global-skill]` | Scaffold a Northlight app in an empty or new directory (server.ts, key, env file, skill), or add the SDK, a key and the skill to an existing project. Never overwrites a file. | No |
|
|
13
|
+
| `npx @north-light/sdk skill [--agent claude\|agents\|all] [--global] [--print]` | Write the packaged `northlight` skill into `.agents/skills` and `.claude/skills` (or `~/` with `--global`), replacing an older copy. `--print` writes it to stdout. | No |
|
|
14
|
+
| `npx @north-light/sdk keygen [--out <path>] [--app <client id>] [--yes]` | Write a private signing key to `local/app-key.json` (mode 0600) and print the public JWK to register. | Only with `--app` |
|
|
15
|
+
| `npx @north-light/sdk env --client-id <client id> [--origin <url>] [--issuer <url>] [--out <path>]`<br/>`npx @north-light/sdk env --app <client id> [--out <path>]` | Write `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env`, keeping every other line. `--client-id` makes no network call; `--app` reads them through crtr. | Only with `--app` |
|
|
16
|
+
|
|
17
|
+
`keygen` and `env` are the only commands that can call `crtr`, and only with `--app`. The documented path on this site never uses `--app`, so you do not need crtr.
|
|
18
|
+
|
|
19
|
+
## Details
|
|
20
|
+
|
|
21
|
+
### `init`
|
|
22
|
+
|
|
23
|
+
In an empty or new directory it writes `package.json` (with `@north-light/sdk` pinned to the CLI's own exact version, plus `tsx`), `server.ts` (the quickstart server), `.gitignore` (`local/`, `node_modules/`) and `local/app.env`, and runs `keygen` into `local/app-key.json`. It installs the skill. Then it prints the public JWK, the console URL `https://account.northlight.ai/console/new`, the values to type, and `npx @north-light/sdk env --client-id <client id>` as the next step.
|
|
24
|
+
|
|
25
|
+
In a directory that already has a `package.json` it writes no app code. It runs `npm i --save-exact @north-light/sdk@<its own version>`, runs `keygen` unless `local/app-key.json` exists, and installs the skill.
|
|
26
|
+
|
|
27
|
+
It never overwrites a file. On a refused step it exits non-zero and names the file it refused.
|
|
28
|
+
|
|
29
|
+
### `skill`
|
|
30
|
+
|
|
31
|
+
Writes the `northlight` Agent Skill that ships in the package, replacing an older copy. `--agent` picks `.claude/skills`, `.agents/skills` or both. `--global` writes under your home directory instead of the project. The skill's `metadata.sdk-version` is the package version, so run `skill` again after you change the pinned SDK version.
|
|
32
|
+
|
|
33
|
+
### `keygen`
|
|
34
|
+
|
|
35
|
+
Writes a private signing key to `local/app-key.json` with mode `0600`, refuses to overwrite an existing file (pass `--out` for another path), and prints the public JWK on stdout. Register the JWK in the console with **Add key** on the app's **Sign-in** tab, or paste it under **Register an app**. With `--app <client id>`, it offers to add the key to your app through your own `crtr` (`crtr cloud apps keys add`); `--yes` skips the question. This needs a joined crtr, which this site does not otherwise require.
|
|
36
|
+
|
|
37
|
+
### `env`
|
|
38
|
+
|
|
39
|
+
`env --client-id <client id>` writes `CROUTER_ISSUER` (default `https://account.northlight.ai`), `CROUTER_CLIENT_ID` and `APP_ORIGIN` (default: the existing value in the file, else `http://localhost:3000`) into `local/app.env`. It keeps every other line and makes no network call. `--out <path>` writes another file. `env --app <client id>` reads the same values through crtr instead. Passing both `--client-id` and `--app` is a usage error.
|
|
40
|
+
|