@north-light/cli 0.3.471 → 0.3.473
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 +1 -1
- package/dist/builtin-memory/crouter-sdk/errors.md +1 -1
- package/dist/builtin-memory/crouter-sdk/getting-started.md +1 -1
- package/dist/builtin-memory/crouter-sdk/guides/apps-on-northlight.md +1 -1
- package/dist/builtin-memory/crouter-sdk/nodes.md +1 -1
- package/dist/builtin-memory/crouter-sdk/release-notes.md +32 -20
- package/dist/commands/node/create.js +2 -2
- package/dist/commands/sys/panels/provider-panel.d.ts +2 -0
- package/dist/commands/sys/panels/provider-panel.js +1 -1
- package/dist/commands/sys/setup-wizard.js +1 -1
- 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/apps/guides/test-your-app.md +191 -0
- 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 +126 -0
- package/docs/sdk/README.md +1 -1
- package/docs/sdk/errors.md +1 -1
- package/docs/sdk/getting-started.md +1 -1
- package/docs/sdk/guides/apps-on-northlight.md +1 -1
- package/docs/sdk/nodes.md +1 -1
- package/docs/sdk/release-notes.md +32 -20
- package/package.json +3 -3
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +32 -12
package/dist/api/dto/nodes.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface CreateNodeRequest {
|
|
|
44
44
|
parent?: NodeIdDTO | null;
|
|
45
45
|
/** Node that created this request; absent for an external process. */
|
|
46
46
|
creator?: NodeIdDTO;
|
|
47
|
-
/** Per-run allow-list. Absent inherits everything.
|
|
47
|
+
/** Per-run allow-list. Absent inherits everything. A provider group (`<provider>:<group>`, e.g. `device:messages_send`) is enforced: a call to a group the list omits is refused `scope_missing` before any request leaves the machine; children inherit the list. Enforced on the broker route; on an unsandboxed host a same-UID process can bypass it by calling the daemon socket directly (crouter#146). */
|
|
48
48
|
scopes?: string[];
|
|
49
49
|
/** Sticky isolation; a child cannot turn off an isolated run. */
|
|
50
50
|
isolated?: boolean;
|
|
@@ -84,7 +84,7 @@ The directory rejects the key until its public JWK is added to the app. To chang
|
|
|
84
84
|
| [[crouter-sdk/errors]] | The error class table and the retry policy |
|
|
85
85
|
| [[crouter-sdk/docker]] | `start()`, `attach()`, `connection()` from `@north-light/env-docker` |
|
|
86
86
|
| [[crouter-sdk/testing]] | `FakeCloud` from `@north-light/sdk/testing`: a fake directory and runtime your real `OAuth2Client` and `Crouter` talk to through `fetch` |
|
|
87
|
-
| [
|
|
87
|
+
| [Release notes](https://sdk.docs.northlight.ai/docs/release-notes) | Behavior changes an existing app must act on, newest first |
|
|
88
88
|
| [[crouter-sdk/guides/apps-on-northlight]] | Building and registering an app against production (joining a laptop, `crtr cloud apps` registration, keys, env file, scopes, running locally, the `crtr cloud apps` commands); typed results from app runs with `runs.parse`, declines, bounding a run, `run_idle`, and waiting on a provider your app requires |
|
|
89
89
|
| [[crouter-sdk/guides/apps-with-many-users]] | Per-person runtime connections, app-owned user access, privacy, and spending |
|
|
90
90
|
|
|
@@ -26,7 +26,7 @@ Every class below extends `APIError`, which carries `status`, `code`, `message`,
|
|
|
26
26
|
| Condition | Class |
|
|
27
27
|
|---|---|
|
|
28
28
|
| SDK configuration error | `CrouterError` with `status: 0`, `code: 'crouter_error'` |
|
|
29
|
-
| The runtime does not serve the contract level the SDK requires, or names none. The hosting router or the runtime refuses this *before* handling (the SDK sends `Crouter-Required-Contract` and a frozen `Crouter-Minimum-Runtime-Version`): nothing was accepted and no idempotency key was recorded. A runtime that predates the contract level handles the request first, and the SDK then raises after | `RuntimeVersionError` with `status: 426`, `code: 'runtime_version_unsupported'`; `requiredContract` and `runtimeContract` (or `null`) name both levels, `sdkVersion` and `runtimeVersion` stay for logging, and it is never retryable. A level is `MAJOR.MINOR`; the runtime satisfies it when it serves the same major and a minor at or above. See [
|
|
29
|
+
| The runtime does not serve the contract level the SDK requires, or names none. The hosting router or the runtime refuses this *before* handling (the SDK sends `Crouter-Required-Contract` and a frozen `Crouter-Minimum-Runtime-Version`): nothing was accepted and no idempotency key was recorded. A runtime that predates the contract level handles the request first, and the SDK then raises after | `RuntimeVersionError` with `status: 426`, `code: 'runtime_version_unsupported'`; `requiredContract` and `runtimeContract` (or `null`) name both levels, `sdkVersion` and `runtimeVersion` stay for logging, and it is never retryable. A level is `MAJOR.MINOR`; the runtime satisfies it when it serves the same major and a minor at or above. See [Release notes](https://sdk.docs.northlight.ai/docs/release-notes) |
|
|
30
30
|
| HTTP 400 | `BadRequestError` |
|
|
31
31
|
| HTTP 401 | `AuthenticationError` |
|
|
32
32
|
| HTTP 403 | `PermissionDeniedError` |
|
|
@@ -27,7 +27,7 @@ npm i --save-exact @north-light/sdk@0.3.451
|
|
|
27
27
|
|
|
28
28
|
One dependency. It re-exports every data type you need, so you do not also install `@north-light/api`.
|
|
29
29
|
|
|
30
|
-
`@latest` always works against production: a release reaches `latest` only once production serves its contract level. The `next` tag carries every release the moment it is published and may need a newer runtime than production runs (see [[crouter-sdk/errors]] for `RuntimeVersionError`). Keep the exact pin: the compatibility gate protects the wire, not the TypeScript API. To move it, check `npm view @north-light/sdk dist-tags` and read the [
|
|
30
|
+
`@latest` always works against production: a release reaches `latest` only once production serves its contract level. The `next` tag carries every release the moment it is published and may need a newer runtime than production runs (see [[crouter-sdk/errors]] for `RuntimeVersionError`). Keep the exact pin: the compatibility gate protects the wire, not the TypeScript API. To move it, check `npm view @north-light/sdk dist-tags` and read the [release notes](https://sdk.docs.northlight.ai/docs/release-notes) first.
|
|
31
31
|
|
|
32
32
|
## 3. Node, on the owner's own daemon
|
|
33
33
|
|
|
@@ -54,7 +54,7 @@ npm init -y
|
|
|
54
54
|
npm i --save-exact @north-light/sdk@0.3.451
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
`0.3.451` is the version this guide was last checked against on production. To move on purpose, run `npm view @north-light/sdk dist-tags`, read the [
|
|
57
|
+
`0.3.451` is the version this guide was last checked against on production. To move on purpose, run `npm view @north-light/sdk dist-tags`, read the [release notes](https://sdk.docs.northlight.ai/docs/release-notes) for the versions in between, and install the new exact version the same way.
|
|
58
58
|
|
|
59
59
|
Pin an exact version of `@north-light/sdk` rather than a range or `latest`. A compatibility gate between the SDK and Northlight's runtimes protects the wire, so an SDK that is too new for the runtime is refused cleanly. It does not protect the TypeScript API: a later SDK release can change a type or a method your code uses, and an unpinned install picks that up on its next `npm i`.
|
|
60
60
|
If a call fails with `426 runtime_version_unsupported`, the SDK is newer than the Northlight runtimes. This can happen briefly right after a new SDK release, until production runtimes are upgraded. Install the previous version or wait, and tell Northlight at support@northlight.ai.
|
|
@@ -64,7 +64,7 @@ Wire fields are `snake_case`. Every `NodeCreateParams` property is optional; `pa
|
|
|
64
64
|
| `description` | `string` | Display description. |
|
|
65
65
|
| `parent` | node id | Graph placement. An external caller leaves this unset. |
|
|
66
66
|
| `creator` | node id | Graph placement. An external caller leaves this unset. |
|
|
67
|
-
| `scopes` | `string[]` | Per-run allow-list. Omit it to inherit every scope, or under a scoped token to receive that token's ceiling; a list outside the ceiling answers `403 scope_denied` with the offending scopes in `details.scopes`. Scopes are prefixed (`crtr:llm`, `crtr:act`, `crtr:schedule`, `crtr:memory:…`, `crtr:files:…`); `crtr:act` requires `crtr:llm`, and the list must stay inside what the creator's grant holds.
|
|
67
|
+
| `scopes` | `string[]` | Per-run allow-list. Omit it to inherit every scope, or under a scoped token to receive that token's ceiling; a list outside the ceiling answers `403 scope_denied` with the offending scopes in `details.scopes`. Scopes are prefixed (`crtr:llm`, `crtr:act`, `crtr:schedule`, `crtr:memory:…`, `crtr:files:…`); `crtr:act` requires `crtr:llm`, and the list must stay inside what the creator's grant holds. Provider-group scopes (`device:messages_send`) are enforced on the broker route; on an unsandboxed host a same-UID process can bypass the list by calling the daemon socket directly, 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. See [[crouter-sdk/getting-started]]. |
|
|
68
68
|
| `worktree` | `string \| boolean` | Create a managed git worktree for the run. |
|
|
69
69
|
| `fork_from` | `string` | Start from an existing conversation. |
|
|
70
70
|
| `no_kickoff` | `boolean` | Create the node without sending the first message. |
|
|
@@ -9,6 +9,18 @@ when-and-why-to-read: When you need Release notes, this knowledge should be read
|
|
|
9
9
|
|
|
10
10
|
Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
|
|
11
11
|
|
|
12
|
+
## 0.3.472
|
|
13
|
+
|
|
14
|
+
### `npx @north-light/sdk init`, `skill` and `env --client-id` (additive, no app change)
|
|
15
|
+
|
|
16
|
+
The `crouter-sdk` bin gains three commands that need neither crtr nor a joined machine:
|
|
17
|
+
|
|
18
|
+
- `init [dir] [--agent claude|agents|all] [--global-skill]` scaffolds an app in an empty or new directory (`package.json` with `@north-light/sdk` pinned to the CLI's own version plus `tsx`, `server.ts`, `.gitignore`, `local/app.env`, a signing key in `local/app-key.json`) and installs the `northlight` Agent Skill into `.agents/skills` and `.claude/skills`. In a directory that already has a `package.json` it writes no app code: it runs `npm i --save-exact @north-light/sdk@<this version>`, makes a key if `local/app-key.json` is missing, and installs the skill. It never overwrites a file; it exits 1 and names the file it refused.
|
|
19
|
+
- `skill [--agent claude|agents|all] [--global] [--print]` writes the skill that ships in the package, replacing an older copy. The skill's `metadata.sdk-version` is the package version; run `skill` again after changing the pinned SDK version.
|
|
20
|
+
- `env --client-id <id> [--origin <url>] [--issuer <url>] [--out <path>]` writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env` and keeps every other line, with no network call. `env --app <id>` still reads them through crtr; passing both is a usage error.
|
|
21
|
+
|
|
22
|
+
`keygen` is unchanged. The package still has one bin, so `npx @north-light/sdk <command>` keeps working. A bare `npx @north-light/sdk`, `--help` and an unknown command print the command list (exit 1 for the unknown command).
|
|
23
|
+
|
|
12
24
|
## 0.3.462
|
|
13
25
|
|
|
14
26
|
### `ProfileDTO.memory_path` is removed (profile objects carry only `profile_path`)
|
|
@@ -61,28 +73,28 @@ The SDK no longer refuses a runtime because its release number is lower than the
|
|
|
61
73
|
|
|
62
74
|
Not breaking: no type or wire shape an app reads is removed or changed.
|
|
63
75
|
|
|
64
|
-
- An app Northlight has not verified is in testing: only its admin and named testers can sign in. A refused sign-in redirects with `error=access_denied`, `error_description=app_in_testing`; a refused refresh answers `invalid_grant` `app_in_testing`. The refresh refusal is a new `Refusal` with `code: 'app_in_testing'` and `kind: 'paused'`, like `grant_suspended`: keep the person's data. If you `switch` exhaustively on `refusal.code`, add the case. See [
|
|
65
|
-
- New `authorizationRefusalOf(callback)` reads the sign-in refusal from the callback's query and returns `AuthorizationRefusal` (`{code: 'app_in_testing', error: 'access_denied', state}`), else `null`. See [
|
|
66
|
-
- `@north-light/sdk/testing`: `cloud.testing` (`enable`, `disable`, `active`, `addTester`, `removeTester`, `testers`, `allowed`), `cloud.authorize(userId, {redirectUri, state})`, `user.setAccount({email, emailVerified, phone})`, and `'app_in_testing'` in `FakeRefusalCode`. The fake directory now serves `grant_type=authorization_code` for codes `cloud.authorize` issued. See [
|
|
76
|
+
- An app Northlight has not verified is in testing: only its admin and named testers can sign in. A refused sign-in redirects with `error=access_denied`, `error_description=app_in_testing`; a refused refresh answers `invalid_grant` `app_in_testing`. The refresh refusal is a new `Refusal` with `code: 'app_in_testing'` and `kind: 'paused'`, like `grant_suspended`: keep the person's data. If you `switch` exhaustively on `refusal.code`, add the case. See [When a refresh is refused](/docs/concepts/connections#when-a-refresh-is-refused).
|
|
77
|
+
- New `authorizationRefusalOf(callback)` reads the sign-in refusal from the callback's query and returns `AuthorizationRefusal` (`{code: 'app_in_testing', error: 'access_denied', state}`), else `null`. See [When sign-in is refused](/docs/concepts/apps-and-sign-in#when-the-directory-refuses-to-sign-someone-in).
|
|
78
|
+
- `@north-light/sdk/testing`: `cloud.testing` (`enable`, `disable`, `active`, `addTester`, `removeTester`, `testers`, `allowed`), `cloud.authorize(userId, {redirectUri, state})`, `user.setAccount({email, emailVerified, phone})`, and `'app_in_testing'` in `FakeRefusalCode`. The fake directory now serves `grant_type=authorization_code` for codes `cloud.authorize` issued. See [Testing mode](/docs/guides/test-your-app#testing-mode).
|
|
67
79
|
|
|
68
80
|
## 0.3.415
|
|
69
81
|
|
|
70
82
|
### Monthly spend cap enforced; `usage.get` (additive; one behavior change)
|
|
71
83
|
|
|
72
84
|
- **Behavior change:** user-paid model calls (`/v1/llm`) and provider calls now count against the grant's `spend_cap` (from `grant.get()`; Northlight sets $50) for the current UTC month. Once the month reaches it, the next call answers 402 `cap_exhausted` (a `RateLimitError`, `retryable: false`, `userAction: 'raise_cap'`, `resetAt` the first instant of next month) before any model or provider is called. A run whose model call is refused this way ends with outcome reason `cap_exhausted`. The check runs before each call, so the call that crosses the cap still completes. App-paid usage never counts. A grant with `spend_cap: null` is not capped.
|
|
73
|
-
- New `client.usage.get({month?})` (`month` is `YYYY-MM`, UTC; default the current month) answers `SpendUsageDTO`: `usd_equivalent` (user-paid spend, provider receipts included), `by_model`, `app_paid`, `spend_cap` and `reset_at`. See [
|
|
85
|
+
- New `client.usage.get({month?})` (`month` is `YYYY-MM`, UTC; default the current month) answers `SpendUsageDTO`: `usd_equivalent` (user-paid spend, provider receipts included), `by_model`, `app_paid`, `spend_cap` and `reset_at`. See [Show what a person's month has cost](/docs/guides/grant-changes#show-what-a-persons-month-has-cost).
|
|
74
86
|
- A completion's `usage.usd_equivalent` is now the served model's list-price cost; before it was always `null`. It stays `null` for a model with no list price, and that usage adds nothing to the month.
|
|
75
87
|
|
|
76
88
|
### `npx @north-light/sdk env` (additive); keygen on an unjoined machine exits 2
|
|
77
89
|
|
|
78
|
-
- New `npx @north-light/sdk env --app <client id> [--out <path>]` writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` from `crtr cloud apps get` into `local/app.env`, keeping every other line. See [
|
|
90
|
+
- New `npx @north-light/sdk env --app <client id> [--out <path>]` writes `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` from `crtr cloud apps get` into `local/app.env`, keeping every other line. See [Go live with an app key](/docs/guides/go-live).
|
|
79
91
|
- `keygen --app` on a machine whose `crtr` isn't joined to a Northlight account now prints the join step and the manual steps and exits 2. A missing `crtr` keeps its message and exit 0.
|
|
80
92
|
|
|
81
93
|
### One deadline per SDK call, and a deadline on every OAuth/JWKS fetch (additive, with one behaviour change)
|
|
82
94
|
|
|
83
95
|
Not breaking: no type or wire shape an app reads is removed or changed. Apps pinned to the previous release keep working unchanged.
|
|
84
96
|
|
|
85
|
-
- `RequestOptions` gains `budgetMs` and `deadlineAt`: one absolute deadline for the call covering every attempt, every retry wait (including a `Retry-After`) and the token fetch/refresh. A wait that would pass the deadline rejects at once with the new `DeadlineExceededError` (`code: 'deadline_exceeded'`, `reason: 'elapsed' | 'retry_after'`, `deadlineAt`, `cause`), which extends `APIConnectionTimeoutError`, so code catching that still catches it. A transport that ignores its `AbortSignal` can no longer outlive the deadline. Without `budgetMs`/`deadlineAt` nothing about retries changes. See [
|
|
97
|
+
- `RequestOptions` gains `budgetMs` and `deadlineAt`: one absolute deadline for the call covering every attempt, every retry wait (including a `Retry-After`) and the token fetch/refresh. A wait that would pass the deadline rejects at once with the new `DeadlineExceededError` (`code: 'deadline_exceeded'`, `reason: 'elapsed' | 'retry_after'`, `deadlineAt`, `cause`), which extends `APIConnectionTimeoutError`, so code catching that still catches it. A transport that ignores its `AbortSignal` can no longer outlive the deadline. Without `budgetMs`/`deadlineAt` nothing about retries changes. See [One deadline per call](/docs/errors#retries).
|
|
86
98
|
- An app that holds a lease or lock while it makes an SDK call should pass `budgetMs` and keep the lease longer than it, with margin, and state that in code.
|
|
87
99
|
- `OAuth2Options.timeoutMs` (default 10 s): one deadline per directory operation (discovery, token exchange, refresh, revoke, `runtime_provisioning` retries and their `retry_after_s`). A refresh under the connection-store lock now holds the lock at most `timeoutMs`. `JwksCache` (and so `verifyIdToken`) takes `{timeoutMs}` (default 10 s) over its discovery and key-set reads together.
|
|
88
100
|
- **Behaviour change:** these directory fetches used to have no deadline (only discovery had one). A directory that takes longer than 10 s now fails with `DeadlineExceededError` where it used to wait; set `timeoutMs` to change it.
|
|
@@ -102,11 +114,11 @@ Not breaking: no type or wire shape an app reads is removed or changed. Apps pin
|
|
|
102
114
|
|
|
103
115
|
### `@north-light/sdk/testing` (additive)
|
|
104
116
|
|
|
105
|
-
New `@north-light/sdk/testing`: `FakeCloud`, a fake directory and runtime your real `OAuth2Client` and `Crouter` talk to through `fetch`. It covers runs (events, replies, long-poll, idempotency, endings), grant watch, refusals, directory failures, memory, providers and run notices, and is checked against the real runtime on every release. Additive: `verifyRunNotice` accepts `fetch`. No behaviour change. See [
|
|
117
|
+
New `@north-light/sdk/testing`: `FakeCloud`, a fake directory and runtime your real `OAuth2Client` and `Crouter` talk to through `fetch`. It covers runs (events, replies, long-poll, idempotency, endings), grant watch, refusals, directory failures, memory, providers and run notices, and is checked against the real runtime on every release. Additive: `verifyRunNotice` accepts `fetch`. No behaviour change. See [Testing your app](/docs/guides/test-your-app).
|
|
106
118
|
|
|
107
119
|
### `oauth.accountAppUrl()` (additive), and a failed discovery read is retried (behaviour fix)
|
|
108
120
|
|
|
109
|
-
- New `OAuth2Client.accountAppUrl()` returns the app's page on the signed-in person's Northlight account, as the directory's discovery document advertises it (`account_app_uri_template`). Use it instead of building `<issuer>/account/apps/<client id>` yourself. It needs no user or connection and throws `CrouterError` naming the fault when the directory advertises no template or an invalid one. See [
|
|
121
|
+
- New `OAuth2Client.accountAppUrl()` returns the app's page on the signed-in person's Northlight account, as the directory's discovery document advertises it (`account_app_uri_template`). Use it instead of building `<issuer>/account/apps/<client id>` yourself. It needs no user or connection and throws `CrouterError` naming the fault when the directory advertises no template or an invalid one. See [Link to the app's settings on Northlight](/docs/guides/grant-changes#link-to-the-apps-settings-on-northlight).
|
|
110
122
|
- **Behaviour fix:** `OAuth2Client`'s discovery read now times out after 10 seconds, and a read that failed or timed out is retried by the next call that needs it. Before, one failed read was kept for the client's life, so every later `authorizeUrl`, `exchangeCode`, refresh and `verifyIdToken` failed until the process restarted, and a hung directory stalled them indefinitely. A successful read is still kept for the client's life.
|
|
111
123
|
|
|
112
124
|
## 0.3.405
|
|
@@ -157,25 +169,25 @@ The runtime gains what a sandbox that sleeps needs; no app-facing type or route
|
|
|
157
169
|
|
|
158
170
|
### Follow a person's grant with `oauth.watchGrants` (additive)
|
|
159
171
|
|
|
160
|
-
`grant.get()` answers a new field, `version`: opaque, it changes exactly when `rev`, `resolved_scopes`, `spend_cap`, `payer` or `dependencies` changes. `grant.get({wait, since})` (`wait` an integer 1–25; anything else throws `RangeError` before sending) holds the read until the grant's `version` differs from `since` or `wait` seconds pass, and extends the request timeout by `wait`. A grant revoked or removed during the hold answers 401 `grant_revoked`. `oauth.watchGrants({users, onChanged, onError?, sweepMs?, concurrency?, wait?})` returns a `GrantWatcher` that follows the grants of the people `users()` names and calls `onChanged({userId, grant, previous})` level-triggered, at least once per version, so an app learns when a required provider's setup turns `ready` without a browser tab or a poll loop. New exports `GrantWatcher`, `GrantWatchOptions`, `GrantChanged`. Not breaking: nothing existing changes shape, and `RunWatcher` behaves exactly as before. The hold needs a runtime at this release and, for a hosted runtime, a hosting router that lets `GET /v1/grant?wait=N` through (before then a held read answers 504 after 10 s and the watcher backs off). See [
|
|
172
|
+
`grant.get()` answers a new field, `version`: opaque, it changes exactly when `rev`, `resolved_scopes`, `spend_cap`, `payer` or `dependencies` changes. `grant.get({wait, since})` (`wait` an integer 1–25; anything else throws `RangeError` before sending) holds the read until the grant's `version` differs from `since` or `wait` seconds pass, and extends the request timeout by `wait`. A grant revoked or removed during the hold answers 401 `grant_revoked`. `oauth.watchGrants({users, onChanged, onError?, sweepMs?, concurrency?, wait?})` returns a `GrantWatcher` that follows the grants of the people `users()` names and calls `onChanged({userId, grant, previous})` level-triggered, at least once per version, so an app learns when a required provider's setup turns `ready` without a browser tab or a poll loop. New exports `GrantWatcher`, `GrantWatchOptions`, `GrantChanged`. Not breaking: nothing existing changes shape, and `RunWatcher` behaves exactly as before. The hold needs a runtime at this release and, for a hosted runtime, a hosting router that lets `GET /v1/grant?wait=N` through (before then a held read answers 504 after 10 s and the watcher backs off). See [Learn when a person's grant changes](/docs/guides/grant-changes#follow-grants-with-watchgrants).
|
|
161
173
|
|
|
162
174
|
## 0.3.399
|
|
163
175
|
|
|
164
176
|
### `onActivity` no longer blocks the watcher
|
|
165
177
|
|
|
166
|
-
In 0.3.398 the watcher awaited `onActivity` before its next read, so a slow or hung hook delayed that run's `onEnded`, held its `concurrency` slot, and kept `stop()` from resolving. The watcher now never awaits it: at most one call per run runs at a time, a newer reading taken meanwhile replaces any still waiting, and `stop()` does not wait for a call already running. A call can now finish after that run's `onEnded`: guard the write on the row still waiting on the run. Runs that leave `pending()` without an ending no longer keep their last phase in memory. See [
|
|
178
|
+
In 0.3.398 the watcher awaited `onActivity` before its next read, so a slow or hung hook delayed that run's `onEnded`, held its `concurrency` slot, and kept `stop()` from resolving. The watcher now never awaits it: at most one call per run runs at a time, a newer reading taken meanwhile replaces any still waiting, and `stop()` does not wait for a call already running. A call can now finish after that run's `onEnded`: guard the write on the row still waiting on the run. Runs that leave `pending()` without an ending no longer keep their last phase in memory. See [Learn when a background run ends](/docs/guides/background-runs#follow-runs-with-runwatcher).
|
|
167
179
|
|
|
168
180
|
## 0.3.398
|
|
169
181
|
|
|
170
182
|
### `RunWatcher` reports live progress with `onActivity` (additive)
|
|
171
183
|
|
|
172
|
-
`oauth.watchRuns({..., onActivity, activityWait?})` calls `onActivity({userId, runId, activity, run})` with each read of a still-`running` run whose `activity` differs from the last one handed over for that run (compared as JSON: `phase`, `tool.name`, `tool.summary`), so an app can show a progress badge from the watcher it already runs instead of a second `runs.get` per page read. It is never called for a run at rest. With `onActivity` set, running runs are read with `wait: activityWait` (integer 1–25, default 2) rather than 25, because the runtime's long-poll answers early only at rest. Best effort: a throwing `onActivity` goes to `onError` and is not retried, and `onEnded` delivery is unchanged. Not breaking: without `onActivity` the watcher behaves exactly as before. New exported type `RunActivityEvent`. See [
|
|
184
|
+
`oauth.watchRuns({..., onActivity, activityWait?})` calls `onActivity({userId, runId, activity, run})` with each read of a still-`running` run whose `activity` differs from the last one handed over for that run (compared as JSON: `phase`, `tool.name`, `tool.summary`), so an app can show a progress badge from the watcher it already runs instead of a second `runs.get` per page read. It is never called for a run at rest. With `onActivity` set, running runs are read with `wait: activityWait` (integer 1–25, default 2) rather than 25, because the runtime's long-poll answers early only at rest. Best effort: a throwing `onActivity` goes to `onError` and is not retried, and `onEnded` delivery is unchanged. Not breaking: without `onActivity` the watcher behaves exactly as before. New exported type `RunActivityEvent`. See [Learn when a background run ends](/docs/guides/background-runs#follow-runs-with-runwatcher).
|
|
173
185
|
|
|
174
186
|
## 0.3.397
|
|
175
187
|
|
|
176
188
|
### A runtime older than the SDK is refused before it handles the request
|
|
177
189
|
|
|
178
|
-
The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every request except `/healthz`. The hosting router in front of a hosted runtime, and a runtime at this release or later, answer a request that names a newer release than the runtime's with `RuntimeVersionError` (`status` 426, `code` `runtime_version_unsupported`) **before any handler runs**: nothing is accepted, no `Idempotency-Key` is recorded, and a retry after the runtime is rolled forward is a first send. Previously an older runtime handled the request and the SDK raised afterwards. Not breaking: no app code changes. An app that worked around the late check, such as a `runs.list` read before every send, can delete it once its runtimes are at or after this release behind a hosting router that has the check. Back off on `RuntimeVersionError` and retry under the same key. A browser app's preflight now lists `crouter-minimum-runtime-version` in `Access-Control-Allow-Headers`. See [
|
|
190
|
+
The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every request except `/healthz`. The hosting router in front of a hosted runtime, and a runtime at this release or later, answer a request that names a newer release than the runtime's with `RuntimeVersionError` (`status` 426, `code` `runtime_version_unsupported`) **before any handler runs**: nothing is accepted, no `Idempotency-Key` is recorded, and a retry after the runtime is rolled forward is a first send. Previously an older runtime handled the request and the SDK raised afterwards. Not breaking: no app code changes. An app that worked around the late check, such as a `runs.list` read before every send, can delete it once its runtimes are at or after this release behind a hosting router that has the check. Back off on `RuntimeVersionError` and retry under the same key. A browser app's preflight now lists `crouter-minimum-runtime-version` in `Access-Control-Allow-Headers`. See [Errors](/docs/errors).
|
|
179
191
|
|
|
180
192
|
## 0.3.396
|
|
181
193
|
|
|
@@ -189,7 +201,7 @@ The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every requ
|
|
|
189
201
|
|
|
190
202
|
### `OAuth2Client` owns the connection store; one `onRefusal` for a dead connection (breaking)
|
|
191
203
|
|
|
192
|
-
`OAuth2Client` now takes the app's `ConnectionStore` once, as `store` (required), plus an optional `onRefusal(refusal)` handler, and every refused refresh goes through that one handler: a request on a connected client, `refreshIdleConnections`, and the new `RunWatcher`. Apps no longer keep their own refusal table or delete dead connections themselves. See [
|
|
204
|
+
`OAuth2Client` now takes the app's `ConnectionStore` once, as `store` (required), plus an optional `onRefusal(refusal)` handler, and every refused refresh goes through that one handler: a request on a connected client, `refreshIdleConnections`, and the new `RunWatcher`. Apps no longer keep their own refusal table or delete dead connections themselves. See [When a refresh is refused](/docs/concepts/connections#when-a-refresh-is-refused).
|
|
193
205
|
|
|
194
206
|
- **`oauth.client(connection, store)` is now `await oauth.client(userId)`.** It loads the stored connection and returns a `ConnectedCrouter` (a `Crouter` with `userId` and `runtimeUrl`). Nothing stored throws `CrouterError` code `not_connected`. Save `exchangeCode`'s connection with `store.save(connection)` first.
|
|
195
207
|
- **`oauth.refreshIdleConnections(store, {olderThanMs})` is now `oauth.refreshIdleConnections({olderThanMs})`** and returns `{refreshed, refused, failures}`. `RefreshFailure.reason` is removed: a refused connection is handled by `onRefusal`, removed, and counted in `refused`; `failures` holds only connections still stored (any other error, or a refusal whose handler threw).
|
|
@@ -203,25 +215,25 @@ The SDK now sends `Crouter-Minimum-Runtime-Version: <SDK_VERSION>` on every requ
|
|
|
203
215
|
|
|
204
216
|
### `RunWatcher`: learn when a background run ends (additive)
|
|
205
217
|
|
|
206
|
-
`oauth.watchRuns({pending, onEnded, onError?, sweepMs?, concurrency?})` returns a `RunWatcher` (`start()`, `nudge()`, `stop()`) that follows the runs `pending()` reads from the app's own rows with `runs.get(wait)` and calls `onEnded({userId, runId, ended, run})` when each comes to rest (`settled`, `idle`, `waiting_on_user`) or is deleted (`gone`, only for a 404 `not_found` from the runtime, never the router). Delivery is at least once per `(runId, status, latest_sequence)`: a delivered run is re-read once per sweep and delivered again only when that key changes (an `idle` run later canceled is delivered as `settled`). A throwing `onEnded` is retried after 5 s, doubling to 5 min. Runs are opened with `oauth.client(userId)`, so a refused connection reaches `onRefusal`. There is no runtime-to-app webhook: the runtime cannot mint an app-audience token, a directory relay would add a hop, and the app's rows already make a pull durable and work on localhost. See [
|
|
218
|
+
`oauth.watchRuns({pending, onEnded, onError?, sweepMs?, concurrency?})` returns a `RunWatcher` (`start()`, `nudge()`, `stop()`) that follows the runs `pending()` reads from the app's own rows with `runs.get(wait)` and calls `onEnded({userId, runId, ended, run})` when each comes to rest (`settled`, `idle`, `waiting_on_user`) or is deleted (`gone`, only for a 404 `not_found` from the runtime, never the router). Delivery is at least once per `(runId, status, latest_sequence)`: a delivered run is re-read once per sweep and delivered again only when that key changes (an `idle` run later canceled is delivered as `settled`). A throwing `onEnded` is retried after 5 s, doubling to 5 min. Runs are opened with `oauth.client(userId)`, so a refused connection reaches `onRefusal`. There is no runtime-to-app webhook: the runtime cannot mint an app-audience token, a directory relay would add a hop, and the app's rows already make a pull durable and work on localhost. See [Learn when a background run ends](/docs/guides/background-runs#follow-runs-with-runwatcher).
|
|
207
219
|
|
|
208
220
|
## 0.3.394
|
|
209
221
|
|
|
210
222
|
### `grant.get` dependencies are one `{ state, message?, next }` entry per provider (breaking)
|
|
211
223
|
|
|
212
|
-
`GrantGetDTO.dependencies[provider]` was a bare `SetupState` string; it is now a `ProviderDependency` (exported from `@north-light/sdk` and `@north-light/api`): `{ state, message?, next }`. `state` is the setup state the string used to be. `message` is the provider's own sentence for `pending` and `failed` (its `message_pending` / `message_failed`), so delete any copy of a provider's prose your app hard-codes and show `message` instead. `next` is the absolute URL of the directory's setup page for that provider and your app; link the person there (append `return_to` with your registered redirect origin) instead of building the URL yourself. **Change every read of `dependencies[p]` to `dependencies[p].state`** (or `dependencies[p]?.state` where the entry may be missing), and carry `message` and `next` into your `provider_setup_pending` details. The runtime reads only this shape from the directory's grant sync: a runtime on this release paired with a directory that still sends bare strings reports `dependencies` as `{}`, so roll the directory change with it. See [
|
|
224
|
+
`GrantGetDTO.dependencies[provider]` was a bare `SetupState` string; it is now a `ProviderDependency` (exported from `@north-light/sdk` and `@north-light/api`): `{ state, message?, next }`. `state` is the setup state the string used to be. `message` is the provider's own sentence for `pending` and `failed` (its `message_pending` / `message_failed`), so delete any copy of a provider's prose your app hard-codes and show `message` instead. `next` is the absolute URL of the directory's setup page for that provider and your app; link the person there (append `return_to` with your registered redirect origin) instead of building the URL yourself. **Change every read of `dependencies[p]` to `dependencies[p].state`** (or `dependencies[p]?.state` where the entry may be missing), and carry `message` and `next` into your `provider_setup_pending` details. The runtime reads only this shape from the directory's grant sync: a runtime on this release paired with a directory that still sends bare strings reports `dependencies` as `{}`, so roll the directory change with it. See [Waiting on a provider your app requires](/docs/guides/grant-changes#wait-on-a-provider-your-app-requires).
|
|
213
225
|
|
|
214
226
|
## 0.3.393
|
|
215
227
|
|
|
216
228
|
### The runtime orders grant state by revision; a failed grant check is `service_unavailable` (breaking)
|
|
217
229
|
|
|
218
|
-
A runtime on this release requires the `rev` claim in every app access token, so it needs a directory that issues it; a token without `rev` is refused 401 `unauthorized`, "access token has no rev claim: the directory is older than this runtime". A freshly consented or re-consented grant is now authorized on the first call, with no wait: the runtime pulls grant state once whenever a token is newer than its mirror. **Delete any retry your app added for a 401 `grant_revoked` right after consent.** When that pull fails, the call answers 503 `service_unavailable` (`retryable: true`) instead of 401 `grant_revoked`, so a directory outage no longer looks like a refused connection. `service_unavailable` was already in `errorCodes`; app calls can now receive it. See [
|
|
230
|
+
A runtime on this release requires the `rev` claim in every app access token, so it needs a directory that issues it; a token without `rev` is refused 401 `unauthorized`, "access token has no rev claim: the directory is older than this runtime". A freshly consented or re-consented grant is now authorized on the first call, with no wait: the runtime pulls grant state once whenever a token is newer than its mirror. **Delete any retry your app added for a 401 `grant_revoked` right after consent.** When that pull fails, the call answers 503 `service_unavailable` (`retryable: true`) instead of 401 `grant_revoked`, so a directory outage no longer looks like a refused connection. `service_unavailable` was already in `errorCodes`; app calls can now receive it. See [An app connection and refresh](/docs/concepts/connections).
|
|
219
231
|
|
|
220
232
|
## 0.3.391
|
|
221
233
|
|
|
222
234
|
### The SDK refuses a runtime older than itself (breaking)
|
|
223
235
|
|
|
224
|
-
`@north-light/sdk` now accepts only a runtime at or above its own version, and fails loudly otherwise instead of handing an app data its types do not describe. Every runtime answer carries `Crouter-Runtime-Version`; when that version is older than `SDK_VERSION`, or the header is absent (every runtime before this release), the call throws `RuntimeVersionError` (`code: 'runtime_version_unsupported'`, `status: 426`, `retryable: false`) whose message and `sdkVersion` / `runtimeVersion` name both versions. A refusal without the header (a router in front of the runtime answering 401 or 503) is still raised as that refusal. There is no capability negotiation: `RunObject.activity` and every other field are typed for a runtime at this release. **Upgrade every runtime the app talks to before upgrading the SDK**, and pin the SDK to an exact version. A test double that fakes the runtime with `fetch` must send `Crouter-Runtime-Version` at the SDK's version on its 2xx answers. See [
|
|
236
|
+
`@north-light/sdk` now accepts only a runtime at or above its own version, and fails loudly otherwise instead of handing an app data its types do not describe. Every runtime answer carries `Crouter-Runtime-Version`; when that version is older than `SDK_VERSION`, or the header is absent (every runtime before this release), the call throws `RuntimeVersionError` (`code: 'runtime_version_unsupported'`, `status: 426`, `retryable: false`) whose message and `sdkVersion` / `runtimeVersion` name both versions. A refusal without the header (a router in front of the runtime answering 401 or 503) is still raised as that refusal. There is no capability negotiation: `RunObject.activity` and every other field are typed for a runtime at this release. **Upgrade every runtime the app talks to before upgrading the SDK**, and pin the SDK to an exact version. A test double that fakes the runtime with `fetch` must send `Crouter-Runtime-Version` at the SDK's version on its 2xx answers. See [Errors](/docs/errors).
|
|
225
237
|
|
|
226
238
|
### One release is one version set
|
|
227
239
|
|
|
@@ -249,7 +261,7 @@ A runtime on this release requires the `rev` claim in every app access token, so
|
|
|
249
261
|
|
|
250
262
|
### `runs.get` reports what a running run is doing (additive)
|
|
251
263
|
|
|
252
|
-
`RunObject.activity` is `{phase: 'starting' | 'thinking' | 'writing' | 'between_turns'}` or `{phase: 'tool', tool: {name, summary}}` while the run is `running`, and `null` otherwise, so a polling app can show progress from `runs.get` without reading `runs.trace`. `runs.list` entries carry it too. See [
|
|
264
|
+
`RunObject.activity` is `{phase: 'starting' | 'thinking' | 'writing' | 'between_turns'}` or `{phase: 'tool', tool: {name, summary}}` while the run is `running`, and `null` otherwise, so a polling app can show progress from `runs.get` without reading `runs.trace`. `runs.list` entries carry it too. See [Showing a background run's progress](/docs/guides/typed-results#showing-progress). A test double that builds a `RunObject` literal must add `activity`.
|
|
253
265
|
|
|
254
266
|
## 0.3.374
|
|
255
267
|
|
|
@@ -261,7 +273,7 @@ A sign-in that grants the `profile` scope returns `name`, `givenName`, and `fami
|
|
|
261
273
|
|
|
262
274
|
### `CrouterError.code` is typed `SdkErrorCode`
|
|
263
275
|
|
|
264
|
-
Every code the SDK raises itself is now listed in `sdkErrorCodes` (exported with `SdkErrorCode` and `mapError` from `@north-light/sdk/errors`), and `CrouterError`'s `code` and constructor are typed with it. Runtime behavior is unchanged. **An app that constructs `new CrouterError(message, code)` with a code of its own, for example in a test double, no longer typechecks:** throw its own error class or an `APIError` instead. See [
|
|
276
|
+
Every code the SDK raises itself is now listed in `sdkErrorCodes` (exported with `SdkErrorCode` and `mapError` from `@north-light/sdk/errors`), and `CrouterError`'s `code` and constructor are typed with it. Runtime behavior is unchanged. **An app that constructs `new CrouterError(message, code)` with a code of its own, for example in a test double, no longer typechecks:** throw its own error class or an `APIError` instead. See [Errors](/docs/errors).
|
|
265
277
|
|
|
266
278
|
### Run ergonomics (additive)
|
|
267
279
|
|
|
@@ -277,7 +289,7 @@ When the directory refuses a connection's refresh token, the SDK now raises the
|
|
|
277
289
|
- `grant_suspended`: your app is paused for that person. Nothing was deleted; keep their data.
|
|
278
290
|
- `refresh_token_expired`: the refresh token went 90 days unused. The grant is still active; reconnecting skips the consent page.
|
|
279
291
|
|
|
280
|
-
Each is a 401 `AuthenticationError` with `retryable: false`, `userAction: 'reconnect'`, and `origin: 'directory'`, and the connected client from `oauth.client(...)` stops refreshing after it, as it already did for `grant_revoked` and `refresh_token_reused`. `errorCodes` and the `ErrorCode` type gain the three codes. The table of codes and app actions is in [
|
|
292
|
+
Each is a 401 `AuthenticationError` with `retryable: false`, `userAction: 'reconnect'`, and `origin: 'directory'`, and the connected client from `oauth.client(...)` stops refreshing after it, as it already did for `grant_revoked` and `refresh_token_reused`. `errorCodes` and the `ErrorCode` type gain the three codes. The table of codes and app actions is in [When a refresh is refused](/docs/concepts/connections#when-a-refresh-is-refused).
|
|
281
293
|
|
|
282
294
|
**Behavior changes for existing apps:**
|
|
283
295
|
|
|
@@ -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};
|
|
@@ -51,6 +51,8 @@ export declare class ProviderPanel implements SetupPanel {
|
|
|
51
51
|
private openBrowser;
|
|
52
52
|
private openCatalog;
|
|
53
53
|
private startLogin;
|
|
54
|
+
/** True when the cursor sits on the `+ Add provider` row, where Enter opens the catalog. */
|
|
55
|
+
isAddRowSelected(): boolean;
|
|
54
56
|
isNestedFlowActive(): boolean;
|
|
55
57
|
dispose(): void;
|
|
56
58
|
private runMutation;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var R=Object.defineProperty;var
|
|
1
|
+
var R=Object.defineProperty;var g=(u,e)=>R(u,"name",{value:e,configurable:!0});import{LoginDialogComponent as E}from"@earendil-works/pi-coding-agent";import{Input as B,matchesKey as d,truncateToWidth as D}from"@earendil-works/pi-tui";import{execFile as N}from"node:child_process";import{dialogAuthInteraction as T}from"../../../core/auth-interaction.js";import{isProviderCredentialConfigured as F}from"../../../core/provider-credential.js";import{formatCooldowns as _,isManagedProvider as L,promoteSubscription as U,readSubscriptionPool as S,renameSubscription as V}from"../../../core/subscription-state.js";import{removeProviderRow as j}from"../../../core/provider-management.js";import{cliClient as q}from"../../api-client.js";import{loginProvider as K}from"../provider-login.js";import{fitTail as P,theme as a,visibleRange as k}from"../../../core/tui/panel.js";function M(u,e){const t=u.getProviders().filter(i=>i.auth.oauth!==void 0),n=new Set(t.map(i=>i.id)),o=new Set;return[...t.map(i=>({providerId:i.id,name:i.name,managed:L(i.id),authType:"oauth"})),...e.getAll().map(i=>({providerId:i.provider,name:e.getProviderDisplayName(i.provider),managed:L(i.provider),authType:n.has(i.provider)?"oauth":"api_key"}))].filter(i=>!o.has(i.providerId)&&!!o.add(i.providerId))}g(M,"buildProviderCatalog");function O(u,e){return e||u instanceof Error&&u.name==="AbortError"?"Login cancelled":`Login error: ${u instanceof Error?u.message:String(u)}`}g(O,"classifyLoginFailure");class ie{static{g(this,"ProviderPanel")}tui;agentDir;runtime;registry;renameInput=new B;rows=[];selected=0;mode="list";notice="";catalog=[];catalogIndex=0;loginProvider;loginFlow;mutationInFlight=!1;onChange;constructor(e,t,n,o){this.tui=e,this.agentDir=t,this.runtime=n,this.registry=o,this.renameInput.onSubmit=()=>this.rename(),this.renameInput.onEscape=()=>{this.mode="list"},this.reload()}notifyChange(){this.onChange?.(),this.tui.requestRender()}reloadAfterAuthChange(){this.reload(),this.registry.refresh().then(()=>{this.reload(),this.notifyChange()})}reload(){const e=this.registry,t=[];for(const n of M(this.runtime,e)){const o=e.getProviderAuthStatus(n.providerId);if(n.managed){const i=S(n.providerId);if(i.length===0&&!o.source)continue;t.push({...n,configured:F(n.providerId,e),source:o.source,sourceLabel:o.label}),i.forEach((r,c)=>t.push({...n,label:r.label,preferred:c===0,cooldownUntil:r.rateLimitedUntil,...r.modelCooldowns?{modelCooldowns:r.modelCooldowns}:{},accountId:r.accountId}));continue}o.source&&t.push({...n,configured:F(n.providerId,e),source:o.source,sourceLabel:o.label})}this.rows=t,this.selected=Math.min(this.selected,Math.max(0,t.length))}selectedRow(){return this.rows[this.selected]}openBrowser(e){const t=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",n=process.platform==="win32"?["/c","start","",e]:[e];N(t,n,{timeout:5e3},()=>{})}openCatalog(){this.catalog=M(this.runtime,this.registry),this.catalogIndex=0,this.mode="catalog"}startLogin(e,t){this.loginProvider=e;let n;const o=g(s=>{!n.active||this.loginFlow!==n||(n.active=!1,this.loginFlow=void 0,this.mode="list",this.notice=s,this.reloadAfterAuthChange(),this.notifyChange())},"complete"),i=new E(this.tui,e.providerId,(s,p)=>{s||o(p?`Login failed: ${p}`:"Login cancelled")},e.name);n={dialog:i,active:!0,committing:!1},this.loginFlow=n,this.mode="login";const r=i.handleInput.bind(i);i.handleInput=s=>{n.committing||r(s)};const c=g(()=>n.active&&this.loginFlow===n&&!i.signal.aborted,"isActive"),h=g(()=>c()?(n.committing=!0,i.showDetails(["Saving credentials\u2026"]),!0):!1,"beginCommit"),m=g(s=>{o(O(s,i.signal.aborted))},"fail");if(e.authType==="api_key"){i.showPrompt(`Enter API key for ${e.name}:`,"sk-...").then(async s=>{if(c()){if(!s.trim()){o("No key entered \u2014 login cancelled");return}h()&&(await q().installCredential(e.providerId,{kind:"api_key",api_key:s.trim()}),o(`API key saved for ${e.name}`))}}).catch(m);return}const $=T(i,{openBrowser:g(s=>this.openBrowser(s),"openBrowser"),onChange:g(()=>this.notifyChange(),"onChange"),isActive:c});K(this.runtime,e.providerId,$,{label:t,beforeInstall:h}).then(s=>{s&&c()&&o(`Logged in to ${e.name}`)}).catch(m)}isAddRowSelected(){return this.mode==="list"&&this.selectedRow()===void 0}isNestedFlowActive(){return this.mode==="login"}dispose(){const e=this.loginFlow;e&&(e.active=!1,this.loginFlow=void 0,e.dialog.handleInput("\x1B"))}runMutation(e){this.mutationInFlight||(this.mutationInFlight=!0,e().catch(t=>{this.notice=t instanceof Error?t.message:String(t)}).finally(()=>{this.mutationInFlight=!1,this.notifyChange()}))}suggestedAccountLabel(e){const t=new Set((L(e)?S(e):[]).map(o=>o.label.toLowerCase()));if(!t.has("default"))return"default";let n=2;for(;t.has(`account-${n}`);)n+=1;return`account-${n}`}rename(){const e=this.selectedRow();!e?.managed||!e.label||(this.mode="list",this.runMutation(async()=>{await V(e.providerId,e.label,this.renameInput.getValue()),this.notice=`Renamed ${e.name} account`,this.reload()}))}remove(e){this.runMutation(async()=>{this.notice=await j(this.agentDir,e),this.reloadAfterAuthChange()})}promote(e){this.runMutation(async()=>{await U(e.providerId,e.label),this.notice=`Preferred ${e.name} account`,this.reload()})}handleInput(e){if(this.mode==="login")return this.loginFlow?.dialog.handleInput(e),!0;if(this.mutationInFlight)return!0;if(this.mode==="rename"||this.mode==="label")return this.renameInput.handleInput(e),!0;if(this.mode==="catalog"){if(d(e,"escape"))return this.mode="list",!0;if(d(e,"j")||d(e,"down"))return this.catalogIndex=Math.min(this.catalog.length-1,this.catalogIndex+1),!0;if(d(e,"k")||d(e,"up"))return this.catalogIndex=Math.max(0,this.catalogIndex-1),!0;if(d(e,"enter")){const o=this.catalog[this.catalogIndex];if(o.managed){this.loginProvider=o;const i=this.suggestedAccountLabel(o.providerId);this.renameInput.setValue(i),this.renameInput.onSubmit=()=>{this.startLogin(o,this.renameInput.getValue().trim()||i)},this.mode="label"}else this.startLogin(o);return!0}return!0}if(this.mode==="remove"){if(d(e,"y")||d(e,"enter")){const o=this.selectedRow();o&&(o.managed||o.configured)&&this.remove(o)}return this.mode="list",!0}if(d(e,"j")||d(e,"down"))return this.selected=Math.min(this.rows.length,this.selected+1),!0;if(d(e,"k")||d(e,"up"))return this.selected=Math.max(0,this.selected-1),!0;const t=this.selectedRow();if(d(e,"p")&&t?.managed&&t.label)return this.promote(t),!0;if(d(e,"r")&&t?.managed&&t.label)return this.renameInput.setValue(t.label),this.renameInput.onSubmit=()=>this.rename(),this.mode="rename",!0;if(d(e,"x")&&(t?.managed||t?.configured))return this.mode="remove",!0;if(d(e,"a"))return this.openCatalog(),!0;const n=t?.source==="environment"||t?.source==="runtime";return d(e,"l")&&t&&!n?(t.managed&&t.label?this.startLogin(t,t.label):this.startLogin(t),!0):d(e,"enter")&&!t?(this.openCatalog(),!0):!1}badge(){return String(this.rows.filter(e=>e.label===void 0).length)}render(e,t){const n=a.fg("accent",a.bold("Providers")),o=a.fg("dim",`${this.rows.filter(c=>c.label===void 0).length} configured`),i=Math.max(1,t);let r;if(this.mode==="login")r=P(this.loginFlow?.dialog.render(e)??[],i);else if(this.mode==="rename"||this.mode==="label")r=P([n,o,"",a.fg("accent",this.mode==="rename"?"Rename account":`Name the ${this.loginProvider?.name??"provider"} account`),...this.mode==="label"?[a.fg("dim","Names this credential \u2014 keep work and personal accounts on one provider apart")]:[],...this.renameInput.render(Math.max(1,e-8))],i);else if(this.mode==="remove"){const c=this.selectedRow(),h=c?.label??(c?.managed?`${c.name} \xB7 all accounts`:c?.name??"account");r=P([n,o,"",a.fg("warning",`Remove ${h}? y/enter confirm \xB7 any other key cancel`)],i)}else{const c=[n,o].slice(0,Math.max(0,i-1)),h=i-c.length;if(r=[...c],this.mode==="catalog"){const m=this.catalog.length,s=m>h&&h>=2,p=s?h-1:h,{start:I,end:v}=k(m,this.catalogIndex,p);let w=h-(v-I)-(s?1:0);w>0&&(r.push(a.fg("dim","Select a provider to add \xB7 esc back")),w-=1);for(let f=I;f<v;f++){const C=this.catalog[f];r.push(`${f===this.catalogIndex?a.fg("accent","\u25B6"):" "} ${C.name}`)}s&&r.push(a.fg("dim",`(${this.catalogIndex+1}/${m})`))}else{const m=this.rows.length+1,s=m>h&&h>=2,p=s?h-1:h,{start:I,end:v}=k(m,this.selected,p);let w=h-(v-I)-(s?1:0);const f=this.rows.length===0&&w>0;f&&(w-=1);const C=!!this.notice&&w>0;f&&r.push(a.fg("warning","No provider configured yet."));for(let b=I;b<v;b++){if(b===this.rows.length){r.push(`${this.selected===this.rows.length?a.fg("accent","\u25B6"):" "} ${a.fg("accent","\uFF0B Add provider")}`);continue}const l=this.rows[b],A=b===this.selected?a.fg("accent","\u25B6"):" ";if(l.label){const x=_({rateLimitedUntil:l.cooldownUntil??0,modelCooldowns:l.modelCooldowns})??"ready",y=l.accountId?` \xB7 ${l.accountId.length>12?`${l.accountId.slice(0,8)}\u2026${l.accountId.slice(-4)}`:l.accountId}`:"";r.push(`${A} ${l.preferred?a.fg("success","\u25CF"):" "} ${l.label}${a.fg("dim",y)} ${a.fg("dim",x)}${l.preferred?a.fg("accent"," preferred"):""}`)}else{const x=l.managed?`${this.rows.filter(y=>y.providerId===l.providerId&&y.label).length} accounts`:l.source==="environment"||l.source==="runtime"?`${l.source}${l.sourceLabel?` \xB7 ${l.sourceLabel}`:""} (view only)`:"configured";r.push(`${A} ${a.fg("accent",l.name)}${a.fg("dim",` ${x}`)}`)}}s&&r.push(a.fg("dim",`(${this.selected+1}/${m})`)),C&&r.push(a.fg("dim",this.notice))}}return r.map(c=>D(c,e))}}export{ie as ProviderPanel,M as buildProviderCatalog,O as classifyLoginFailure};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var P=Object.defineProperty;var p=(h,e)=>P(h,"name",{value:e,configurable:!0});import{matchesKey as b,truncateToWidth as
|
|
1
|
+
var P=Object.defineProperty;var p=(h,e)=>P(h,"name",{value:e,configurable:!0});import{matchesKey as b,truncateToWidth as o,visibleWidth as g}from"@earendil-works/pi-tui";import{formatGesture as v,formatMovePair as k,matchesPiTuiInput as $}from"../../core/keybindings/index.js";import{buildSetupInitialState as y,executeSetupSubmission as I,formatSetupExecutionSummary as w,packagePlanAction as A,reconcileInboxFrontDoor as x,renderSetupStaticInstructions as T,setupPluginEnvFields as C}from"./setup-core.js";import{runSetupSettingsProcess as W}from"./setup-settings-runner.js";import{DependenciesPanel as D}from"./panels/dependencies-panel.js";import{PAGE_CHROME_ROWS as F,pageFrame as L,theme as r}from"../../core/tui/panel.js";import{PluginsPanel as H}from"./panels/plugins-panel.js";import{ProviderPanel as O}from"./panels/provider-panel.js";import{PluginEnvPanel as _}from"./panels/plugin-env-panel.js";import{TerminalPanel as N}from"./panels/terminal-panel.js";import{describeEdit as R,planOptionAsAlt as z}from"./terminal-option.js";import{homedir as M}from"node:os";import{cliClient as S}from"../api-client.js";const E=["Providers","Plugins","Plugin env","Dependencies"],j="Option key";class B{static{p(this,"SetupWizard")}bindings;terminalRows;providers;plugins;pluginEnv;dependencies;terminal;steps;step=0;confirming=!1;cachedWidth;cachedHeight;cachedLines;_focused=!1;installedPackageIds;onSubmit;onCancel;constructor(e,i,t,n,s,c,a,l,f,m,d=null){this.bindings=l,this.terminalRows=f,this.installedPackageIds=i;const u=p(()=>this.invalidate(),"invalidate");this.providers=new O(n,s,c,a),this.providers.onChange=u,this.plugins=new H(e,i,u),this.pluginEnv=new _(m,u),this.dependencies=new D(t,u),this.terminal=d===null?void 0:new N(d,u),this.steps=this.terminal===void 0?E:[...E,j]}set focused(e){this._focused=e}get focused(){return this._focused}invalidate(){this.cachedWidth=void 0,this.cachedHeight=void 0,this.cachedLines=void 0}matches(e,i){return $(this.bindings,e,i,(t,n)=>b(t,n))}label(e){const i=this.bindings.gestures(e);return i.length===0?"disabled":v(i[0].split(" > ").pop())}fieldSafeLabel(e){const i=this.bindings.gestures(e).filter(t=>{if(t.includes(" > "))return!0;const n=t.split("+").pop();return!(n==="up"||n==="down"||n==="left"||n==="right")&&(n.length>1||t.includes("+"))});return i.length===0?"Disabled":i.map(v).join(" / ")}setStep(e){e===2&&this.step!==2&&this.pluginEnv.setFields(C(this.plugins.selected)),this.step=e,this.invalidate()}moveStep(e){this.setStep((this.step+e+this.steps.length)%this.steps.length)}advance(){this.step===this.steps.length-1?this.finalize():(this.step!==2||this.pluginEnv.complete)&&this.moveStep(1)}cancel(){if(this.confirming){this.confirming=!1,this.invalidate();return}this.onCancel?.()}dispose(){this.providers.dispose()}hasPendingInstalls(){return this.plugins.selected.some(e=>!this.installedPackageIds.has(e.id))||this.dependencies.selected.length>0||this.terminal?.selectedEdit!==void 0}buildSubmission(){return{packages:[...this.plugins.selected],installedPackageIds:new Set(this.installedPackageIds),systemDeps:[...this.dependencies.selected],pluginEnv:new Map(this.pluginEnv.values),...this.terminal?.selectedEdit===void 0?{}:{terminalEdit:this.terminal.selectedEdit}}}finalize(){if(this.confirming||!this.hasPendingInstalls()){this.onSubmit?.(this.buildSubmission());return}this.confirming=!0,this.invalidate()}currentPanel(){return this.step===0?this.providers:this.step===1?this.plugins:this.step===2?this.pluginEnv:this.step===3?this.dependencies:this.terminal}handleInput(e){const i=this.currentPanel(),t=p((s,c)=>this.matches(s,c),"match");if(this.step===0&&this.providers.isNestedFlowActive()){i.handleInput(e,t)&&this.invalidate();return}if(this.confirming){this.matches("crtr.setup.confirm.accept",e)?this.onSubmit?.(this.buildSubmission()):this.matches("crtr.setup.confirm.back",e)&&(this.confirming=!1,this.invalidate());return}if(this.step===2&&this.pluginEnv.handleInput(e)){this.invalidate();return}if(this.matches("crtr.setup.cancel",e)){this.cancel();return}if(this.matches("crtr.setup.tab-next",e)){if(this.step===2&&!this.pluginEnv.complete)return;this.moveStep(1);return}if(this.matches("crtr.setup.tab-previous",e)){this.moveStep(-1);return}if(i.handleInput(e,t)){this.invalidate();return}this.matches("crtr.setup.advance",e)&&this.advance()}renderTrail(e){const t=this.steps.map((n,s)=>{if(s===this.step)return r.bg("selectedBg",` ${s+1} ${n} `);const c=s<this.step?r.fg("success","\u2713"):`${s+1}`;return r.fg("dim",` ${c} ${n} `)}).join(r.fg("dim","\u203A"));return g(t)<=e?[o(t,e),""]:[o(`${r.fg("dim",`Step ${this.step+1} of ${this.steps.length}`)} ${r.fg("accent",r.bold(this.steps[this.step]))}`,e),""]}footer(e){const i=`${this.fieldSafeLabel("crtr.setup.tab-next")}/${this.fieldSafeLabel("crtr.setup.tab-previous")} step \xB7 ${this.label("crtr.setup.cancel")} cancel`,t=k(this.bindings,"crtr.setup.list-down","crtr.setup.list-up"),n=this.label("crtr.setup.advance");let s,c;if(this.confirming)s=`${this.label("crtr.setup.confirm.accept")} confirm \xB7 ${this.label("crtr.setup.confirm.back")} go back`;else if(this.step===0){const a=this.providers.isAddRowSelected()?`${n} add provider`:`${n} next`;s=this.providers.isNestedFlowActive()?"OAuth in progress \xB7 esc cancel":`${t} select \xB7 ${a} \xB7 a add \xB7 p prefer \xB7 r rename \xB7 l re-login \xB7 x remove`,this.providers.isNestedFlowActive()||(c=`${t} select \xB7 ${a} \xB7 a add \xB7 l re-login`)}else this.step===1?s=`${t} move \xB7 ${this.label("crtr.setup.toggle")} toggle \xB7 ${this.label("crtr.setup.details")} details \xB7 ${this.label("crtr.setup.advance")} next`:this.step===2?s=`type value \xB7 Enter save/skip \xB7 ${this.pluginEnv.complete?`${this.label("crtr.setup.advance")} next`:"complete keys to continue"}`:this.step===3?s=`${t} move \xB7 ${this.label("crtr.setup.toggle")} toggle \xB7 ${this.label("crtr.setup.advance")} ${this.steps.length>4?"next":"finish"}`:s=`${this.terminal?.canApply?`${this.label("crtr.setup.toggle")} apply for me \xB7 `:""}${this.label("crtr.setup.advance")} finish`;for(const a of c===void 0?[s]:[s,c]){const l=e-g(a)-g(i);if(l>=2)return`${r.fg("dim",a)}${" ".repeat(l)}${r.fg("dim",i)}`}return r.fg("dim",c??s)}renderConfirm(e){const i=this.buildSubmission(),t=[o(r.fg("warning",r.bold("Confirm installs")),e),""];if(i.packages.length===0&&i.systemDeps.length===0&&i.terminalEdit===void 0)return[...t,o(r.fg("success",`Nothing to install; ${this.label("crtr.setup.confirm.accept")} finish.`),e)];if(i.packages.length){t.push(o(r.fg("accent","Packages"),e));for(const n of i.packages)t.push(o(` \u2022 ${n.name} ${r.fg("dim","\u2192")} ${this.installedPackageIds.has(n.id)?r.fg("success","skip (already installed)"):r.fg("muted",A(n))}`,e))}if(i.systemDeps.length){t.push(o(r.fg("accent","System deps"),e));for(const n of i.systemDeps)t.push(o(` \u2022 ${n.tool} ${r.fg("dim","\u2192")} ${r.fg("muted",n.command??n.hint)}`,e))}if(i.terminalEdit!==void 0){t.push(o(r.fg("accent","Terminal config"),e));for(const n of R(i.terminalEdit))t.push(o(r.fg("muted",` ${n}`),e))}return t}render(e){const i=this.step===0,t=this.terminalRows();if(!i&&this.cachedLines&&this.cachedWidth===e&&this.cachedHeight===t)return this.cachedLines;const n=this.currentPanel(),s=Math.max(1,t-F),c=this.confirming||s<=2?[]:this.renderTrail(e),a=Math.max(1,s-c.length),l=L({width:e,height:t,title:"crouter setup",meta:this.confirming?"review":`step ${this.step+1} of ${this.steps.length}`,body:[...c,...this.confirming?this.renderConfirm(e):n.render(e,a)],footer:this.footer(e)});return i||(this.cachedWidth=e,this.cachedHeight=t,this.cachedLines=l),l}}function G(){process.stdout.write("\x1B[2J\x1B[H")}p(G,"clearWizardFrame");async function re(){const h=y();let e=!1;if(await x(),!process.stdout.isTTY)return process.stdout.write(T(h)),"skipped";const i=new Set((await S().listAppEnv()).names),t=await W(({tui:s,terminal:c,agentDir:a,runtime:l,registry:f,bindings:m},d)=>{const u=new B(h.manifest,h.installedPackageIds,h.systemPlans,s,a,l,f,m,()=>c.rows,i,z({env:process.env,platform:process.platform,home:M()}));return u.onCancel=d.cancel,u.onSubmit=d.submit,u},(s,c)=>s===""?(e=!0,!0):s.length>0&&s.charCodeAt(0)<32&&$(c,"crtr.setup.cancel",s,(a,l)=>b(a,l)));if(G(),e){const s=new Error("Interrupted");throw s.name="AbortError",s}if(t===void 0)return"skipped";for(const[s,c]of t.pluginEnv)await S().setAppEnv(s,c);const n=await I(t);return process.stdout.write(w(n)),"completed"}p(re,"runSetup");export{B as SetupWizard,re as runSetup};
|
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
|
+
|