@north-light/cli 0.3.436 → 0.3.437

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/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. Otherwise map status→code (`STATUS_TO_CLI_CODE`) with the server message
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 w=Object.defineProperty;var n=(e,t)=>w(e,"name",{value:t,configurable:!0});import{appendFileSync as x,statSync as E}from"node:fs";import{CrtrError as a}from"./errors.js";import{ExitCode as c}from"../types.js";import{PREVIEW_RESULT_PATH_ENV as m}from"./preview-result-path.js";import{renderError as T}from"./render.js";import{ApiError as b}from"../api/index.js";let d=!1,u;const _=256*1024;function W(e){const t=process.env[m];u=t===void 0||t===""?void 0:{path:e,jsonl:[]}}n(W,"beginPreview");function f(e){const t=process.env[m];if(t===void 0||t==="")return;const s=`${JSON.stringify({path:u?.path??"",...e})}
2
- `;try{if((E(t,{throwIfNoEntry:!1})?.size??0)+Buffer.byteLength(s,"utf8")>_)return;x(t,s,"utf8")}catch{}}n(f,"publishPreview");function k(e){u!==void 0&&(e!=null?f({result:e}):(u.jsonl.length>0||u.error!==void 0)&&f({...u.jsonl.length>0?{jsonl:u.jsonl}:{},...u.error===void 0?{}:{error:u.error}}))}n(k,"publishPreviewResult");function y(e){u?.jsonl.push(e)}n(y,"recordPreviewLine");function B(e){try{const t=JSON.parse(e);t!==null&&typeof t=="object"&&!Array.isArray(t)&&y(t)}catch{}}n(B,"recordPreviewJsonLine");function q(e){u!==void 0&&(u.error=e)}n(q,"recordPreviewError");function F(e){d=e}n(F,"setJsonOutput");function G(){return d}n(G,"isJsonOutput");class h extends a{static{n(this,"InputError")}payload;constructor(t,s=c.USAGE){super(t.error,t.message,s,{...t}),this.name="InputError",this.payload=t}}async function K(){if(process.stdin.isTTY)return"";const e=[];for await(const t of process.stdin)e.push(t);return Buffer.concat(e).toString("utf8")}n(K,"readStdinRaw");async function D(e=200){return process.stdin.isTTY?"":await new Promise(t=>{const s=[];let o=!1;const r=n(()=>{o||(o=!0,clearTimeout(g),process.stdin.removeListener("data",i),process.stdin.removeListener("end",p),process.stdin.removeListener("error",l),process.stdin.pause(),t(Buffer.concat(s).toString("utf8")))},"finish"),i=n(S=>s.push(S),"onData"),p=n(()=>r(),"onEnd"),l=n(()=>r(),"onError"),g=setTimeout(r,e);process.stdin.on("data",i),process.stdin.on("end",p),process.stdin.on("error",l),process.stdin.resume()})}n(D,"peekStdinRaw");function V(e){process.stdout.write(JSON.stringify(e,null,2)+`
3
- `)}n(V,"emit");function Y(e){y(e),process.stdout.write(JSON.stringify(e)+`
4
- `)}n(Y,"emitLine");function H(e){return new Promise(t=>{let s=!1;const o=n(i=>{i.code==="EPIPE"&&r(!1)},"onErr"),r=n(i=>{s||(s=!0,process.stdout.off("error",o),t(i))},"finish");process.stdout.on("error",o);try{process.stdout.write(e,i=>r(i==null))}catch{r(!1)}})}n(H,"writeStdout");function M(e){process.stderr.write(e+`
5
- `)}n(M,"diag");function O(e){if(e instanceof h)return e.payload;const t=e.details!==void 0?e.details:{},s=t.primaryCompleted===!0&&t.result!==null&&typeof t.result=="object"&&!Array.isArray(t.result)?N(t.result):void 0,o=t.next!==void 0?t.next:"Inspect the error and adjust the call. See -h for the schema.",r={error:e.code,message:e.message,received:t.received,field:t.field,...typeof t.http_status=="number"?{http_status:t.http_status}:{}};return s===void 0?{...r,next:o}:{...r,primaryCompleted:!0,result:s,next:o}}n(O,"payloadOf");function N(e){const t=new WeakSet;try{const s=JSON.stringify(e,(o,r)=>{if(typeof r=="bigint")return{$crtrType:"bigint",value:r.toString()};if(typeof r=="number"&&!Number.isFinite(r))return{$crtrType:"number",value:String(r)};if(typeof r>"u"||typeof r=="function"||typeof r=="symbol")return{$crtrType:typeof r};if(r!==null&&typeof r=="object"){if(t.has(r))return{$crtrType:"circular"};t.add(r)}return r});if(s!==void 0){const o=JSON.parse(s);if(o!==null&&typeof o=="object"&&!Array.isArray(o))return o}}catch{}return{$crtrType:"unavailable",message:"The completed primary result could not be represented as JSON."}}n(N,"jsonSafeResult");const R={400:"invalid_request",404:"not_found",409:"conflict",422:"unprocessable"};function A(e,t){if(e.code==="daemon_restarting")return new a("daemon_restarting",e.message,c.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,c.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,c.NETWORK,{next:"Start it with `crtr sys daemon start`, then retry."});if(e.status===503)return new a(e.code,e.message,c.NETWORK,{next:t??"Retry the command; if it persists, inspect the unavailable service named in the error."});const s=e.details,o=Array.isArray(s?.violations)?s.violations.map(i=>`${String(i.field)} ${String(i.problem)}${i.received!==void 0?` (received ${JSON.stringify(i.received)})`:""}`):[],r=s&&typeof s.error=="string"&&typeof s.message=="string"&&typeof s.next=="string"?s:{error:R[e.status]??e.code,message:o.length>0?`${e.message}: ${o.join("; ")}`:e.message.trim()!==""?e.message:`request refused (HTTP ${e.status}, ${e.code}) with no message`,next:t??(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(r,P(r.error,e.status))}n(A,"apiErrorToCliError");function P(e,t){return e==="not_found"||t===404?c.NOT_FOUND:e==="ambiguous"?c.AMBIGUOUS:c.USAGE}n(P,"exitForCliCode");function j(e){if(e instanceof b){j(A(e));return}if(e instanceof a){const o=O(e);u!==void 0&&f({error:o});const r=d?JSON.stringify(o,null,2):T(o);process.stdout.write(r+`
6
- `),process.exitCode=e.exitCode;return}const t=e,s=t!=null&&typeof t.message=="string"?t.message:String(e);process.stderr.write(JSON.stringify({error:"internal",message:s,next:"This is a crtr bug, not a bad call. Retry; if it persists, report it."},null,2)+`
7
- `),process.exitCode=c.GENERAL}n(j,"handle");export{h as InputError,_ as PREVIEW_RESULT_MAX_BYTES,A as apiErrorToCliError,W as beginPreview,M as diag,V as emit,Y as emitLine,j as handle,G as isJsonOutput,D as peekStdinRaw,k as publishPreviewResult,K as readStdinRaw,q as recordPreviewError,B as recordPreviewJsonLine,y as recordPreviewLine,F as setJsonOutput,H as writeStdout};
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};
@@ -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 m(e){return String(e).replace(/"/g,"'").replace(/\n/g," ")}u(m,"attrEsc");function l(e){return e==null?"":Array.isArray(e)?e.map(l).join(", "):typeof e=="object"?JSON.stringify(e):String(e)}u(l,"scalarStr");function y(e){const n=JSON.stringify(e,null,2);let o="```";for(;n.includes(o);)o+="`";return`${o}json
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
- ${o}`}u(y,"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 l(e).replace(/\|/g,"\\|").replace(/\n/g," ")}u(j,"cellEsc");function b(e,n){if(n.length===0)return`0 ${e}.`;const o=`${n.length} ${e}:`;if(n.every(s=>s!==null&&typeof s=="object"&&!Array.isArray(s))){const s=n,i=[];for(const p of s)for(const a of Object.keys(p))i.includes(a)||i.push(a);const d=`| ${i.join(" | ")} |`,t=`| ${i.map(()=>"---").join(" | ")} |`,r=s.map(p=>`| ${i.map(a=>j(p[a])).join(" | ")} |`).join(`
5
- `);return`${o}
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(s=>`- ${l(s)}`).join(`
10
- `);return`${o}
11
- ${c}`}u(b,"renderArray");function A(e,n){const o=n.output.map(t=>t.name);for(const t of Object.keys(e))o.includes(t)||o.push(t);const f=[],c=[],s=[];for(const t of o){const r=e[t];if(r!=null){if(Array.isArray(r)){s.push(b(t,r));continue}if(g(t,r)){c.push({name:t,text:r});continue}f.push(`- ${t}: ${l(r)}`)}}const i=[];f.length>0&&i.push(f.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 s)i.push(t);return i.join(`
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(A,"renderResult");function O(e){const n=[e.message];return e.received!==void 0&&e.received!==null&&e.received!==""&&n.push(`received: ${l(e.received)}`),e.field!==void 0&&n.push(`field: ${e.field}`),e.http_status!==void 0&&n.push(`http status: ${e.http_status}`),"primaryCompleted"in e&&e.primaryCompleted===!0&&(n.push("primaryCompleted: true"),n.push(`Primary result (the command already completed):
15
- ${y(e.result)}`)),n.push(`Next: ${e.next}`),`<error code="${m(e.error)}">
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(O,"renderError");export{O as renderError,A as renderResult};
18
+ </error>`}u(A,"renderError");export{A as renderError,_ as renderResult};
@@ -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.434 or later (older versions need `crtr sys daemon stop` first and fail if a viewer is open); `crtr --version` shows yours.
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> --scope crtr:llm --scope crtr:act --scope sms:send
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` replaces the whole list, so repeat `--scope` for every scope you want (`crtr:act` needs `crtr:llm`). 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.
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> --scope …` | Replaces the scope list. Repeat `--scope` for every scope; `--clear` empties it; `crtr:act` needs `crtr:llm`. |
252
- | `crtr cloud apps set-requires --app-id <id> --provider …` | Names providers your app does not work without (for example `context`); `--clear` empties it. |
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.436",
3
+ "version": "0.3.437",
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.436",
140
+ "@north-light/identity": "0.3.437",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.436",
3
+ "version": "0.3.437",
4
4
  "description": "Northlight access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.436",
3
+ "version": "0.3.437",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.436",
9
+ "version": "0.3.437",
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.436",
21
+ "@north-light/identity": "0.3.437",
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.436",
12098
+ "version": "0.3.437",
12099
12099
  "license": "UNLICENSED"
12100
12100
  },
12101
12101
  "packages/crouter-env-docker": {
12102
12102
  "name": "@north-light/env-docker",
12103
- "version": "0.3.436",
12103
+ "version": "0.3.437",
12104
12104
  "license": "UNLICENSED"
12105
12105
  },
12106
12106
  "packages/crouter-identity": {
12107
12107
  "name": "@north-light/identity",
12108
- "version": "0.3.436",
12108
+ "version": "0.3.437",
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.436",
12116
+ "version": "0.3.437",
12117
12117
  "license": "UNLICENSED",
12118
12118
  "dependencies": {
12119
- "@north-light/api": "0.3.436",
12120
- "@north-light/identity": "0.3.436"
12119
+ "@north-light/api": "0.3.437",
12120
+ "@north-light/identity": "0.3.437"
12121
12121
  }
12122
12122
  },
12123
12123
  "packages/crouter-sdk": {
12124
12124
  "name": "@north-light/sdk",
12125
- "version": "0.3.436",
12125
+ "version": "0.3.437",
12126
12126
  "license": "UNLICENSED",
12127
12127
  "dependencies": {
12128
- "@north-light/api": "0.3.436",
12129
- "@north-light/identity": "0.3.436",
12128
+ "@north-light/api": "0.3.437",
12129
+ "@north-light/identity": "0.3.437",
12130
12130
  "jose": "^6.2.1"
12131
12131
  },
12132
12132
  "bin": {