@north-light/cli 0.3.436 → 0.3.438
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/builtin-memory/crouter-sdk/guides/apps-on-northlight.md +22 -6
- package/dist/clients/attach/viewer.js +177 -177
- package/dist/core/io.d.ts +8 -1
- package/dist/core/io.js +7 -7
- package/dist/core/render.js +11 -11
- package/docs/ci-runner.md +58 -0
- package/docs/sdk/guides/apps-on-northlight.md +22 -6
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
package/dist/core/io.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ interface ErrorPayloadBase {
|
|
|
57
57
|
* of `received` on purpose: `received` names the caller's offending VALUE, and
|
|
58
58
|
* reporting an HTTP status there is a false claim about what was sent. */
|
|
59
59
|
http_status?: number;
|
|
60
|
+
/** Seconds the provider asked the caller to wait before retrying (a rate limit). */
|
|
61
|
+
retry_after_s?: number;
|
|
60
62
|
next: string;
|
|
61
63
|
}
|
|
62
64
|
export type ErrorPayload = ErrorPayloadBase & ({
|
|
@@ -109,7 +111,12 @@ export declare function diag(message: string): void;
|
|
|
109
111
|
* reconstruct it verbatim so a gate/guard error (revive reopen-gate, push
|
|
110
112
|
* worktree/finalization guard, …) surfaces byte-for-byte as the leaf raised
|
|
111
113
|
* it before the re-plumb, preserving its SPECIFIC code + next.
|
|
112
|
-
* 3.
|
|
114
|
+
* 3. A provider-origin refusal (`err.origin === 'provider'`) carries the
|
|
115
|
+
* provider's own error in `details` (`{code,message,field?,next?,received?}`)
|
|
116
|
+
* under the daemon's coarse `provider_*` code and the provider's HTTP
|
|
117
|
+
* status. The provider's code, `next`, `field` and `Retry-After` reach the
|
|
118
|
+
* caller unchanged; the status→code map would flatten them to `conflict`.
|
|
119
|
+
* 4. Otherwise map status→code (`STATUS_TO_CLI_CODE`) with the server message
|
|
113
120
|
* and a caller-supplied-or-default `next`. */
|
|
114
121
|
export declare function apiErrorToCliError(err: ApiError, next?: string): CrtrError;
|
|
115
122
|
/** Terminal error handler. Command-level failures (bad input, not-found,
|
package/dist/core/io.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
`;try{if((
|
|
3
|
-
`)}
|
|
4
|
-
`)}
|
|
5
|
-
`)}
|
|
6
|
-
`),process.exitCode=e.exitCode;return}const t=e,
|
|
7
|
-
`),process.exitCode=
|
|
1
|
+
var x=Object.defineProperty;var s=(e,t)=>x(e,"name",{value:t,configurable:!0});import{appendFileSync as _,statSync as w}from"node:fs";import{CrtrError as a}from"./errors.js";import{ExitCode as u}from"../types.js";import{PREVIEW_RESULT_PATH_ENV as y}from"./preview-result-path.js";import{renderError as E}from"./render.js";import{ApiError as b}from"../api/index.js";let p=!1,o;const T=256*1024;function k(e){const t=process.env[y];o=t===void 0||t===""?void 0:{path:e,jsonl:[]}}s(k,"beginPreview");function l(e){const t=process.env[y];if(t===void 0||t==="")return;const n=`${JSON.stringify({path:o?.path??"",...e})}
|
|
2
|
+
`;try{if((w(t,{throwIfNoEntry:!1})?.size??0)+Buffer.byteLength(n,"utf8")>T)return;_(t,n,"utf8")}catch{}}s(l,"publishPreview");function B(e){o!==void 0&&(e!=null?l({result:e}):(o.jsonl.length>0||o.error!==void 0)&&l({...o.jsonl.length>0?{jsonl:o.jsonl}:{},...o.error===void 0?{}:{error:o.error}}))}s(B,"publishPreviewResult");function m(e){o?.jsonl.push(e)}s(m,"recordPreviewLine");function q(e){try{const t=JSON.parse(e);t!==null&&typeof t=="object"&&!Array.isArray(t)&&m(t)}catch{}}s(q,"recordPreviewJsonLine");function F(e){o!==void 0&&(o.error=e)}s(F,"recordPreviewError");function G(e){p=e}s(G,"setJsonOutput");function K(){return p}s(K,"isJsonOutput");class h extends a{static{s(this,"InputError")}payload;constructor(t,n=u.USAGE){super(t.error,t.message,n,{...t}),this.name="InputError",this.payload=t}}async function z(){if(process.stdin.isTTY)return"";const e=[];for await(const t of process.stdin)e.push(t);return Buffer.concat(e).toString("utf8")}s(z,"readStdinRaw");async function D(e=200){return process.stdin.isTTY?"":await new Promise(t=>{const n=[];let r=!1;const i=s(()=>{r||(r=!0,clearTimeout(g),process.stdin.removeListener("data",f),process.stdin.removeListener("end",d),process.stdin.removeListener("error",c),process.stdin.pause(),t(Buffer.concat(n).toString("utf8")))},"finish"),f=s(S=>n.push(S),"onData"),d=s(()=>i(),"onEnd"),c=s(()=>i(),"onError"),g=setTimeout(i,e);process.stdin.on("data",f),process.stdin.on("end",d),process.stdin.on("error",c),process.stdin.resume()})}s(D,"peekStdinRaw");function V(e){process.stdout.write(JSON.stringify(e,null,2)+`
|
|
3
|
+
`)}s(V,"emit");function Y(e){m(e),process.stdout.write(JSON.stringify(e)+`
|
|
4
|
+
`)}s(Y,"emitLine");function H(e){return new Promise(t=>{let n=!1;const r=s(f=>{f.code==="EPIPE"&&i(!1)},"onErr"),i=s(f=>{n||(n=!0,process.stdout.off("error",r),t(f))},"finish");process.stdout.on("error",r);try{process.stdout.write(e,f=>i(f==null))}catch{i(!1)}})}s(H,"writeStdout");function M(e){process.stderr.write(e+`
|
|
5
|
+
`)}s(M,"diag");function v(e){if(e instanceof h)return e.payload;const t=e.details!==void 0?e.details:{},n=t.primaryCompleted===!0&&t.result!==null&&typeof t.result=="object"&&!Array.isArray(t.result)?A(t.result):void 0,r=t.next!==void 0?t.next:"Inspect the error and adjust the call. See -h for the schema.",i={error:e.code,message:e.message,received:t.received,field:t.field,...typeof t.http_status=="number"?{http_status:t.http_status}:{},...typeof t.retry_after_s=="number"?{retry_after_s:t.retry_after_s}:{}};return n===void 0?{...i,next:r}:{...i,primaryCompleted:!0,result:n,next:r}}s(v,"payloadOf");function A(e){const t=new WeakSet;try{const n=JSON.stringify(e,(r,i)=>{if(typeof i=="bigint")return{$crtrType:"bigint",value:i.toString()};if(typeof i=="number"&&!Number.isFinite(i))return{$crtrType:"number",value:String(i)};if(typeof i>"u"||typeof i=="function"||typeof i=="symbol")return{$crtrType:typeof i};if(i!==null&&typeof i=="object"){if(t.has(i))return{$crtrType:"circular"};t.add(i)}return i});if(n!==void 0){const r=JSON.parse(n);if(r!==null&&typeof r=="object"&&!Array.isArray(r))return r}}catch{}return{$crtrType:"unavailable",message:"The completed primary result could not be represented as JSON."}}s(A,"jsonSafeResult");const O={400:"invalid_request",404:"not_found",409:"conflict",422:"unprocessable"};function N(e,t){if(e.code==="daemon_restarting")return new a("daemon_restarting",e.message,u.NETWORK,{next:"crtrd is now on the new generation \u2014 retry the command."});if(e.code==="layout_migration_required")return new a("layout_migration_required",e.message,u.NETWORK,{next:"Run `crtr sys migrate` while the runtime is offline, then restart it."});if(e.code==="daemon_unavailable")return new a("daemon_unavailable",e.message,u.NETWORK,{next:"Start it with `crtr sys daemon start`, then retry."});const n=e.details,r=R(e,n);if(e.status===503){const d=r.code??e.code,c={next:r.next??t??"Retry the command; if it persists, inspect the unavailable service named in the error."};return r.code!==void 0&&(r.field!==void 0&&(c.field=r.field),r.received!==void 0&&(c.received=r.received),r.retryAfterS!==void 0&&(c.retry_after_s=r.retryAfterS)),new a(d,e.message,u.NETWORK,c)}const i=Array.isArray(n?.violations)?n.violations.map(d=>`${String(d.field)} ${String(d.problem)}${d.received!==void 0?` (received ${JSON.stringify(d.received)})`:""}`):[],f=n&&typeof n.error=="string"&&typeof n.message=="string"&&typeof n.next=="string"?n:{error:r.code??O[e.status]??e.code,...r.field!==void 0?{field:r.field}:{},...r.received!==void 0?{received:r.received}:{},...r.retryAfterS!==void 0?{retry_after_s:r.retryAfterS}:{},message:i.length>0?`${e.message}: ${i.join("; ")}`:e.message.trim()!==""?e.message:`request refused (HTTP ${e.status}, ${e.code}) with no message`,next:r.next??t??(r.retryAfterS!==void 0?`Wait ${r.retryAfterS}s, then retry; do not retry sooner.`:e.status>=500?"This is a crtr bug, not a bad call. Retry; if it persists, report it (the daemon log holds the full cause).":e.status===404?"List nodes with `crtr node inspect list`.":"Inspect the error and adjust the call. See -h for the schema.")};return new h(f,P(f.error,e.status))}s(N,"apiErrorToCliError");function R(e,t){if(e.origin!=="provider"||t===void 0||t===null||typeof t!="object")return{};const n={};return typeof t.status=="number"&&typeof t.code=="string"&&/^[a-z][a-z0-9_]*$/.test(t.code)&&(n.code=t.code),typeof t.next=="string"&&t.next.trim()!==""&&(n.next=t.next),typeof t.field=="string"&&t.field!==""?n.field=t.field:e.param!==void 0&&(n.field=e.param),t.received!==void 0&&t.received!==null&&(n.received=t.received),typeof e.retryAfterS=="number"&&(n.retryAfterS=e.retryAfterS),n}s(R,"providerSupplied");function P(e,t){return e==="not_found"||t===404?u.NOT_FOUND:e==="ambiguous"?u.AMBIGUOUS:u.USAGE}s(P,"exitForCliCode");function $(e){if(e instanceof b){$(N(e));return}if(e instanceof a){const r=v(e);o!==void 0&&l({error:r});const i=p?JSON.stringify(r,null,2):E(r);process.stdout.write(i+`
|
|
6
|
+
`),process.exitCode=e.exitCode;return}const t=e,n=t!=null&&typeof t.message=="string"?t.message:String(e);process.stderr.write(JSON.stringify({error:"internal",message:n,next:"This is a crtr bug, not a bad call. Retry; if it persists, report it."},null,2)+`
|
|
7
|
+
`),process.exitCode=u.GENERAL}s($,"handle");export{h as InputError,T as PREVIEW_RESULT_MAX_BYTES,N as apiErrorToCliError,k as beginPreview,M as diag,V as emit,Y as emitLine,$ as handle,K as isJsonOutput,D as peekStdinRaw,B as publishPreviewResult,z as readStdinRaw,F as recordPreviewError,q as recordPreviewJsonLine,m as recordPreviewLine,G as setJsonOutput,H as writeStdout};
|
package/dist/core/render.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
var h=Object.defineProperty;var u=(e,n)=>h(e,"name",{value:n,configurable:!0});const $=new Set(["follow_up","digest","message","next","result","content","body","guide","summary","note","reason","detail","details","hint","instruction","instructions","prompt","roadmap","report","answer","response","text"]);function
|
|
1
|
+
var h=Object.defineProperty;var u=(e,n)=>h(e,"name",{value:n,configurable:!0});const $=new Set(["follow_up","digest","message","next","result","content","body","guide","summary","note","reason","detail","details","hint","instruction","instructions","prompt","roadmap","report","answer","response","text"]);function y(e){return String(e).replace(/"/g,"'").replace(/\n/g," ")}u(y,"attrEsc");function f(e){return e==null?"":Array.isArray(e)?e.map(f).join(", "):typeof e=="object"?JSON.stringify(e):String(e)}u(f,"scalarStr");function m(e){const n=JSON.stringify(e,null,2);let s="```";for(;n.includes(s);)s+="`";return`${s}json
|
|
2
2
|
${n}
|
|
3
|
-
${
|
|
4
|
-
`)||n.length>80}u(g,"isProse");function j(e){return
|
|
5
|
-
`);return`${
|
|
3
|
+
${s}`}u(m,"fencedJson");function g(e,n){return typeof n!="string"?!1:$.has(e)?!0:n.includes(`
|
|
4
|
+
`)||n.length>80}u(g,"isProse");function j(e){return f(e).replace(/\|/g,"\\|").replace(/\n/g," ")}u(j,"cellEsc");function b(e,n){if(n.length===0)return`0 ${e}.`;const s=`${n.length} ${e}:`;if(n.every(o=>o!==null&&typeof o=="object"&&!Array.isArray(o))){const o=n,i=[];for(const p of o)for(const a of Object.keys(p))i.includes(a)||i.push(a);const d=`| ${i.join(" | ")} |`,t=`| ${i.map(()=>"---").join(" | ")} |`,r=o.map(p=>`| ${i.map(a=>j(p[a])).join(" | ")} |`).join(`
|
|
5
|
+
`);return`${s}
|
|
6
6
|
|
|
7
7
|
${d}
|
|
8
8
|
${t}
|
|
9
|
-
${r}`}const c=n.map(
|
|
10
|
-
`);return`${
|
|
11
|
-
${c}`}u(b,"renderArray");function
|
|
12
|
-
`));const d=c.length>1;for(const{name:t,text:r}of c)i.push(d?`**${t}:** ${r}`:r);for(const t of
|
|
9
|
+
${r}`}const c=n.map(o=>`- ${f(o)}`).join(`
|
|
10
|
+
`);return`${s}
|
|
11
|
+
${c}`}u(b,"renderArray");function _(e,n){const s=n.output.map(t=>t.name);for(const t of Object.keys(e))s.includes(t)||s.push(t);const l=[],c=[],o=[];for(const t of s){const r=e[t];if(r!=null){if(Array.isArray(r)){o.push(b(t,r));continue}if(g(t,r)){c.push({name:t,text:r});continue}l.push(`- ${t}: ${f(r)}`)}}const i=[];l.length>0&&i.push(l.join(`
|
|
12
|
+
`));const d=c.length>1;for(const{name:t,text:r}of c)i.push(d?`**${t}:** ${r}`:r);for(const t of o)i.push(t);return i.join(`
|
|
13
13
|
|
|
14
|
-
`)}u(
|
|
15
|
-
${
|
|
14
|
+
`)}u(_,"renderResult");function A(e){const n=[e.message];return e.received!==void 0&&e.received!==null&&e.received!==""&&n.push(`received: ${f(e.received)}`),e.field!==void 0&&n.push(`field: ${e.field}`),e.http_status!==void 0&&n.push(`http status: ${e.http_status}`),e.retry_after_s!==void 0&&n.push(`retry after: ${e.retry_after_s}s`),"primaryCompleted"in e&&e.primaryCompleted===!0&&(n.push("primaryCompleted: true"),n.push(`Primary result (the command already completed):
|
|
15
|
+
${m(e.result)}`)),n.push(`Next: ${e.next}`),`<error code="${y(e.error)}">
|
|
16
16
|
${n.join(`
|
|
17
17
|
`)}
|
|
18
|
-
</error>`}u(
|
|
18
|
+
</error>`}u(A,"renderError");export{A as renderError,_ as renderResult};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# CI runners and the double-run gate
|
|
2
|
+
|
|
3
|
+
Crouter's suite runs on macOS (see the header of `.github/workflows/test.yml`). GitHub-hosted `macos-latest` is billed per minute and a full run is ~50 billed minutes, so the suite can run on a self-hosted Mac instead, and the push to `main` does not re-run it for a tree that already passed.
|
|
4
|
+
|
|
5
|
+
## Where the macOS jobs run: `CRTR_CI_RUNNER`
|
|
6
|
+
|
|
7
|
+
Repository variable `CRTR_CI_RUNNER` selects `runs-on` for the `build`, `build-output` and `lane` jobs of `test.yml`:
|
|
8
|
+
|
|
9
|
+
| Value | Runs on |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| unset, or anything other than `self-hosted` | GitHub-hosted `macos-latest` (billed) |
|
|
12
|
+
| `self-hosted` | runners labelled `[self-hosted, macOS, crouter]` (the Mac mini) |
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
gh variable set CRTR_CI_RUNNER --body self-hosted -R Northlight-Systems/crouter # use the mini
|
|
16
|
+
gh variable delete CRTR_CI_RUNNER -R Northlight-Systems/crouter # fall back to macos-latest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It takes effect on the next run; no commit is needed. **If the mini is offline, jobs queue until it returns (they do not fall back by themselves) — delete the variable.** Cancel and re-run anything already queued. The variable also changes the shard matrix: `self-hosted` uses 2 unit + 3 integration + 3 seam shards (8 lane jobs, 3 runner instances), hosted uses 3 + 6 + 6 (15).
|
|
20
|
+
|
|
21
|
+
A pull request from a fork always runs on `macos-latest`, whatever the variable says: a self-hosted runner must never execute fork code. The repository is private and forking is disabled today, so this only guards a later change.
|
|
22
|
+
|
|
23
|
+
## The Mac mini runners
|
|
24
|
+
|
|
25
|
+
`scripts/ci-runner/install.sh` installs N runner instances (default 3). The mini has 10 cores and 16 GB; a lane job runs up to 8 test files at once, each with a broker and tmux, so three concurrent jobs is the ceiling before the seam lane's timeouts become the failure.
|
|
26
|
+
|
|
27
|
+
Isolation, per instance `~/ci-runners/crouter-N/`:
|
|
28
|
+
|
|
29
|
+
- its own runner directory and `_work` checkout;
|
|
30
|
+
- its own `HOME` (`~/ci-runners/crouter-N/home`), never the account's. The suite refuses to start if `$HOME/.crouter` exists, so the account's own crouter install (`~/.crouter`, the selected runtime, `~/.local/bin/crtr`, any running `crtrd`) is neither read nor written;
|
|
31
|
+
- a minimal `PATH` (Homebrew + system) that does not include the account's crtr or npm globals;
|
|
32
|
+
- `hook-job-started.sh` / `hook-job-completed.sh` (`ACTIONS_RUNNER_HOOK_JOB_*`), which kill every process whose environment or command line points into that instance's directory (brokers, `crtrd`, test tmux servers, orphaned node processes) and wipe `home/.crouter*` and `_temp`. They match only that instance's path, so one instance's cleanup cannot touch another's job or the account's processes.
|
|
33
|
+
|
|
34
|
+
The tests bind unix sockets under per-test temp dirs and tmux servers under the per-test `HOME`, with no fixed TCP ports, so instances do not collide.
|
|
35
|
+
|
|
36
|
+
### Service
|
|
37
|
+
|
|
38
|
+
Each instance is a boot-time LaunchDaemon (survives reboot and logout, runs as `silasrhyneer`): `ai.northlight.ci-runner.crouter-1` … `-3`.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sudo launchctl kickstart -k system/ai.northlight.ci-runner.crouter-1 # restart one
|
|
42
|
+
sudo launchctl print system/ai.northlight.ci-runner.crouter-1 | head # state
|
|
43
|
+
tail -f ~/ci-runners/crouter-1/runner.log # log
|
|
44
|
+
gh api repos/Northlight-Systems/crouter/actions/runners --jq '.runners[]|[.name,.status,.busy]|@tsv'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Installing and upgrading: get a registration token (`gh api -X POST repos/Northlight-Systems/crouter/actions/runners/registration-token --jq .token`), run `RUNNER_TOKEN=… scripts/ci-runner/install.sh 3` on the mini, then run the `sudo launchctl bootstrap` lines it prints. Prerequisites on the mini: `tmux`, `uv`, `git` (Homebrew); Node comes from `actions/setup-node`.
|
|
48
|
+
|
|
49
|
+
## No double run on `main`
|
|
50
|
+
|
|
51
|
+
`publish.yml` starts with a `gate` job. It reads the pushed commit's **git tree** and looks for a workflow artifact named `tested-tree-<tree>`. `test.yml`'s final `tested` job creates that artifact only after `build`, `build-output` and every `lane` shard pass, on every pull request run and on every run of Publish's own `tests` job.
|
|
52
|
+
|
|
53
|
+
- Rebase-merging a PR whose tested merge ref has the same tree as the resulting `main` commit → artifact found → `tests` is skipped and `publish` proceeds.
|
|
54
|
+
- A direct push to `main`, or a merge after `main` moved (a different tree from the one the PR run tested) → no artifact → the full suite runs exactly as before. A release commit (`chore: release …`, pushed by Publish itself) changes the tree, so a PR not updated since before the latest release runs the suite once on merge; update the branch to avoid it.
|
|
55
|
+
- The gate can only skip; if the lookup errors it runs the suite. `publish` runs only when `tests` passed, or was skipped *because* the gate found a record. A failed or cancelled suite never publishes.
|
|
56
|
+
- Branch protection is not enabled on `main` and the gate does not depend on it: the proof is the tree, not a required check. Artifacts are kept 30 days; older trees are simply re-tested.
|
|
57
|
+
|
|
58
|
+
The OIDC constraint at the top of `publish.yml` is unchanged: every publishing job is still in that file.
|
|
@@ -21,7 +21,7 @@ The example app texts you, so it asks for the `sms` provider's `sms:send` scope.
|
|
|
21
21
|
|
|
22
22
|
Open `https://account.northlight.ai/signup` and create your account. Signup texts a confirmation code to your phone, and that text is sent over iMessage, so use a phone number that has iMessage. A number that cannot receive iMessage gets "We couldn't send the text", and retrying does not help. One phone number belongs to one account.
|
|
23
23
|
|
|
24
|
-
Install the CLI (`npm i -g @north-light/cli`). Joining needs `crtr` 0.3.
|
|
24
|
+
Install the CLI (`npm i -g @north-light/cli`). Joining needs `crtr` 0.3.436 or later (older versions need `crtr sys daemon stop` first and fail if a viewer is open); `crtr --version` shows yours.
|
|
25
25
|
|
|
26
26
|
Then join your laptop to the account:
|
|
27
27
|
|
|
@@ -94,10 +94,10 @@ An app can only ask for scopes it has been given. It starts with `crtr:llm` and
|
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
crtr cloud apps asks --app-id <client id> --source sms
|
|
97
|
-
crtr cloud apps set-scopes --app-id <client id> --
|
|
97
|
+
crtr cloud apps set-scopes --app-id <client id> --add sms:send
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
`asks` lists, per source, what the app may ask for and whether it asks now; `sms:send` is under providers, with the consent sentence "Send you text messages from Northlight's number". `set-scopes`
|
|
100
|
+
`asks` lists, per source, what the app may ask for and whether it asks now; `sms:send` is under providers, with the consent sentence "Send you text messages from Northlight's number". `set-scopes --add` adds to the list the app has now and leaves the rest alone; `--remove` drops a scope that is in it (`crtr:act` needs `crtr:llm`, so drop both together). `crtr cloud apps asks --app-id <client id> --requested` shows exactly what the app asks for now. To replace the whole list instead, repeat `--scope` for every scope you want, or pass `--clear`. The console's **What it asks for** tab does the same with an **Add permissions** search. Without `sms:send` the consent page does not offer it and `providers.call` for `sms` is refused. Take scope strings from `asks` and do not invent them.
|
|
101
101
|
|
|
102
102
|
If your app does not work without a provider being set up first (Doppel needs Context), name it:
|
|
103
103
|
|
|
@@ -247,9 +247,9 @@ Your Northlight chat agent at `https://chat.northlight.ai` can run the same comm
|
|
|
247
247
|
| `crtr cloud apps get --app-id <id>` | Shows the registration. |
|
|
248
248
|
| `crtr cloud apps update --app-id <id> --name … / --origin … / --logo … / --justification …` | Changes the profile fields you pass. |
|
|
249
249
|
| `crtr cloud apps set-redirect-uris --app-id <id> --redirect-uri …` | Replaces the whole redirect URI list; repeat the flag for each. |
|
|
250
|
-
| `crtr cloud apps asks --app-id <id>` | Lists what an app may ask for, with whether it asks now. Large sources such as `integrations` print as a count. Narrow it with `--query sms:send`, `--source sms`, `--group providers\|apps\|agents\|space`, or `--query <text>`; for example `crtr cloud apps asks --app-id <id> --source sms`. |
|
|
251
|
-
| `crtr cloud apps set-scopes --app-id <id> --
|
|
252
|
-
| `crtr cloud apps set-requires --app-id <id> --
|
|
250
|
+
| `crtr cloud apps asks --app-id <id>` | Lists what an app may ask for, with whether it asks now. Large sources such as `integrations` print as a count. Narrow it with `--query sms:send`, `--source sms`, `--group providers\|apps\|agents\|space`, or `--query <text>`; `--requested` lists only what the app asks for now. For example `crtr cloud apps asks --app-id <id> --source sms`. |
|
|
251
|
+
| `crtr cloud apps set-scopes --app-id <id> --add … --remove …` | Edits the scope list the app has now; repeat `--add`/`--remove` per scope. Removing a scope that is not in the list is refused. Or replace the whole list with repeated `--scope`; `--clear` empties it. `crtr:act` needs `crtr:llm`. |
|
|
252
|
+
| `crtr cloud apps set-requires --app-id <id> --add … --remove …` | Edits the providers your app does not work without (for example `context`). Or replace the list with repeated `--provider`; `--clear` empties it. |
|
|
253
253
|
| `crtr cloud apps keys add --app-id <id> --jwk '<public JWK>'` | Adds a signing key beside any existing one. Answers `created: false` if that key is already on the app. `keygen --app` calls it for you. |
|
|
254
254
|
| `crtr cloud apps keys list --app-id <id>` | Lists the keys with their `kid` and `active` or `retired` status. |
|
|
255
255
|
| `crtr cloud apps keys retire --app-id <id> --kid <kid>` | Retires a key so it no longer signs in. |
|
|
@@ -282,6 +282,22 @@ crtr cloud apps set-listing --app-id <client id> --listed true --price free \
|
|
|
282
282
|
|
|
283
283
|
`--text-welcome` is one or two plain sentences, at most 120 characters, with no links. Agents can install an app for people by text only while it is listed (`--listed true`). `crtr cloud apps install --app-id <client id>` sends the signed-in person a text with the install link. Run it only for yourself or for a person who asked: it sends a real text.
|
|
284
284
|
|
|
285
|
+
### Developing alongside your live app
|
|
286
|
+
|
|
287
|
+
Once your app is live, you can keep developing on your laptop against production while the live app keeps running. There are two ways to do it.
|
|
288
|
+
|
|
289
|
+
**Add your localhost callback to the live app.** This is the simplest, and it is fine for most apps. An app can hold several redirect URIs, and each sign-in returns to the one its request names, so your deployed server and your laptop can both sign people in at once:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
crtr cloud apps set-redirect-uris --app-id <client id> \
|
|
293
|
+
--redirect-uri https://app.example.com/auth/callback \
|
|
294
|
+
--redirect-uri http://localhost:3000/auth/callback
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`set-redirect-uris` replaces the whole list, so name the live URI too. Give your laptop its own signing key beside the deployed one (`keygen --app` adds it; an app holds at most 2 active keys), so you never copy the production private key to your laptop. Both servers share everything else: the same people, their grants, the app's space and the scopes the app asks for. A scope you add while developing changes what the live app asks of everyone. The origin, `login_start_url` and run notice URL each hold one value, so they keep pointing at the deployed app.
|
|
298
|
+
|
|
299
|
+
**Register a separate development app.** Do this when you are changing what the app asks for, or need data and grants of its own that live people never touch. Run `crtr cloud apps register` again with a name such as "Texty (dev)", a `localhost` origin and redirect URI, and its own key. It starts in testing and stays there, so only you and the testers you add can sign in, and it never appears in the marketplace. Point your laptop's environment file at its client id. This is how Google recommends separating tiers too: a separate project for development and for production.
|
|
300
|
+
|
|
285
301
|
### Rotate keys
|
|
286
302
|
|
|
287
303
|
Add a new key beside the old one, switch, verify with a real sign-in, then retire the old key:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.438",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
},
|
|
138
138
|
"license": "UNLICENSED",
|
|
139
139
|
"dependencies": {
|
|
140
|
-
"@north-light/identity": "0.3.
|
|
140
|
+
"@north-light/identity": "0.3.438",
|
|
141
141
|
"@earendil-works/pi-agent-core": "1.1.0",
|
|
142
142
|
"@earendil-works/pi-ai": "1.1.0",
|
|
143
143
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.438",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/cli",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.438",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
12
|
"workspaces": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@earendil-works/pi-ai": "1.1.0",
|
|
19
19
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
20
20
|
"@earendil-works/pi-tui": "1.1.0",
|
|
21
|
-
"@north-light/identity": "0.3.
|
|
21
|
+
"@north-light/identity": "0.3.438",
|
|
22
22
|
"cron-parser": "^5.6.0",
|
|
23
23
|
"esbuild": "^0.27.7",
|
|
24
24
|
"jose": "^6.2.12",
|
|
@@ -12095,17 +12095,17 @@
|
|
|
12095
12095
|
},
|
|
12096
12096
|
"packages/crouter-api": {
|
|
12097
12097
|
"name": "@north-light/api",
|
|
12098
|
-
"version": "0.3.
|
|
12098
|
+
"version": "0.3.438",
|
|
12099
12099
|
"license": "UNLICENSED"
|
|
12100
12100
|
},
|
|
12101
12101
|
"packages/crouter-env-docker": {
|
|
12102
12102
|
"name": "@north-light/env-docker",
|
|
12103
|
-
"version": "0.3.
|
|
12103
|
+
"version": "0.3.438",
|
|
12104
12104
|
"license": "UNLICENSED"
|
|
12105
12105
|
},
|
|
12106
12106
|
"packages/crouter-identity": {
|
|
12107
12107
|
"name": "@north-light/identity",
|
|
12108
|
-
"version": "0.3.
|
|
12108
|
+
"version": "0.3.438",
|
|
12109
12109
|
"license": "UNLICENSED",
|
|
12110
12110
|
"dependencies": {
|
|
12111
12111
|
"jose": "^6.2.1"
|
|
@@ -12113,20 +12113,20 @@
|
|
|
12113
12113
|
},
|
|
12114
12114
|
"packages/crouter-plugin": {
|
|
12115
12115
|
"name": "@north-light/plugin",
|
|
12116
|
-
"version": "0.3.
|
|
12116
|
+
"version": "0.3.438",
|
|
12117
12117
|
"license": "UNLICENSED",
|
|
12118
12118
|
"dependencies": {
|
|
12119
|
-
"@north-light/api": "0.3.
|
|
12120
|
-
"@north-light/identity": "0.3.
|
|
12119
|
+
"@north-light/api": "0.3.438",
|
|
12120
|
+
"@north-light/identity": "0.3.438"
|
|
12121
12121
|
}
|
|
12122
12122
|
},
|
|
12123
12123
|
"packages/crouter-sdk": {
|
|
12124
12124
|
"name": "@north-light/sdk",
|
|
12125
|
-
"version": "0.3.
|
|
12125
|
+
"version": "0.3.438",
|
|
12126
12126
|
"license": "UNLICENSED",
|
|
12127
12127
|
"dependencies": {
|
|
12128
|
-
"@north-light/api": "0.3.
|
|
12129
|
-
"@north-light/identity": "0.3.
|
|
12128
|
+
"@north-light/api": "0.3.438",
|
|
12129
|
+
"@north-light/identity": "0.3.438",
|
|
12130
12130
|
"jose": "^6.2.1"
|
|
12131
12131
|
},
|
|
12132
12132
|
"bin": {
|