@north-light/cli 0.3.487 → 0.3.489

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.
Files changed (49) hide show
  1. package/dist/api/contract.d.ts +1 -1
  2. package/dist/api/contract.js +1 -1
  3. package/dist/api/index.d.ts +2 -1
  4. package/dist/build-root.js +1 -1
  5. package/dist/builtin-memory/crouter-sdk/release-notes.md +17 -0
  6. package/dist/builtin-memory/crouter-sdk/resources.md +2 -1
  7. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  8. package/dist/builtin-memory/northlight-apps/cli.md +20 -6
  9. package/dist/builtin-memory/northlight-apps/coding-agents.md +9 -7
  10. package/dist/builtin-memory/northlight-apps/guides/manage-your-app.md +1 -0
  11. package/dist/builtin-memory/northlight-apps/quickstart.md +5 -3
  12. package/dist/clients/attach/viewer.js +505 -505
  13. package/dist/commands/doc/shared.js +2 -2
  14. package/dist/commands/profile/project.js +1 -1
  15. package/dist/core/canvas/recovery.d.ts +5 -0
  16. package/dist/core/canvas/recovery.js +2 -2
  17. package/dist/core/command-plugins/endpoint.js +1 -1
  18. package/dist/core/graph/repo-sync/sync.d.ts +4 -3
  19. package/dist/core/graph/repo-sync/sync.js +2 -2
  20. package/dist/core/graph/spaces/repo.d.ts +7 -7
  21. package/dist/core/graph/spaces/repo.js +1 -1
  22. package/dist/core/graph/types.d.ts +3 -2
  23. package/dist/core/human/page-errors.d.ts +4 -0
  24. package/dist/core/human/page-errors.js +1 -1
  25. package/dist/core/human/page.d.ts +10 -3
  26. package/dist/core/human/page.js +1 -1
  27. package/dist/core/peer/ingest.js +1 -1
  28. package/dist/core/peer/stubs.d.ts +4 -0
  29. package/dist/core/peer/stubs.js +1 -1
  30. package/dist/core/peer/version.d.ts +2 -2
  31. package/dist/core/peer/version.js +1 -1
  32. package/dist/core/render.js +1 -2
  33. package/dist/core/runtime/revive.js +2 -2
  34. package/dist/daemon/api/handlers/docs.js +1 -1
  35. package/dist/daemon/api/handlers/human-requests.js +1 -1
  36. package/dist/launcher/accepted-socket.d.ts +11 -0
  37. package/dist/launcher/accepted-socket.js +1 -0
  38. package/dist/launcher/main.js +3 -3
  39. package/dist/launcher/validate.js +1 -1
  40. package/docs/apps/cli.md +20 -6
  41. package/docs/apps/coding-agents.md +9 -7
  42. package/docs/apps/guides/manage-your-app.md +1 -0
  43. package/docs/apps/quickstart.md +5 -3
  44. package/docs/ci-runner.md +2 -1
  45. package/docs/sdk/release-notes.md +17 -0
  46. package/docs/sdk/resources.md +2 -1
  47. package/package.json +3 -3
  48. package/packages/crouter-identity/package.json +1 -1
  49. package/runtime.lock.json +12 -12
@@ -1,5 +1,5 @@
1
1
  /** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
2
- export declare const CONTRACT_LEVEL = "1.11";
2
+ export declare const CONTRACT_LEVEL = "1.12";
3
3
  /** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
4
4
  export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
5
5
  /** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
@@ -1 +1 @@
1
- var c=Object.defineProperty;var e=(r,t)=>c(r,"name",{value:t,configurable:!0});const f="1.11",i="Crouter-Required-Contract",p="Crouter-Contract";function l(r){const t=/^(\d+)\.(\d+)$/.exec(r.trim());return t===null?null:{major:Number(t[1]),minor:Number(t[2])}}e(l,"parseContractLevel");function a(r){return r==null?[]:r.split(",").map(t=>l(t)).filter(t=>t!==null)}e(a,"parseServedContracts");function s(r,t){const n=l(r);return n===null?!1:(typeof t=="string"||t===null||t===void 0?a(t):t).some(u=>u.major===n.major&&u.minor>=n.minor)}e(s,"contractSatisfies");function C(r,t,n={}){if(r===void 0)return null;if(l(r)===null)return{status:400,code:"invalid_request",param:i,message:`${i} must be a contract level like 1.0.`};if(s(r,t))return null;const o=r.trim();return{status:426,code:"runtime_version_unsupported",message:t===null||a(t).length===0?`The runtime did not name its contract level; the caller requires contract ${o} or later. Roll the runtime forward.`:`The runtime serves contract ${t}; the caller requires contract ${o} or later within the same major. Roll the runtime forward, or pin an SDK whose crouterContract the runtime serves.`,details:{sdk_version:(n.legacyMinimum??"").trim(),runtime_version:n.runtimeVersion??null,required_contract:o,runtime_contract:t}}}e(C,"contractRefusal");export{p as CONTRACT_HEADER,f as CONTRACT_LEVEL,i as REQUIRED_CONTRACT_HEADER,C as contractRefusal,s as contractSatisfies,l as parseContractLevel,a as parseServedContracts};
1
+ var c=Object.defineProperty;var e=(r,t)=>c(r,"name",{value:t,configurable:!0});const f="1.12",i="Crouter-Required-Contract",p="Crouter-Contract";function l(r){const t=/^(\d+)\.(\d+)$/.exec(r.trim());return t===null?null:{major:Number(t[1]),minor:Number(t[2])}}e(l,"parseContractLevel");function a(r){return r==null?[]:r.split(",").map(t=>l(t)).filter(t=>t!==null)}e(a,"parseServedContracts");function s(r,t){const n=l(r);return n===null?!1:(typeof t=="string"||t===null||t===void 0?a(t):t).some(u=>u.major===n.major&&u.minor>=n.minor)}e(s,"contractSatisfies");function C(r,t,n={}){if(r===void 0)return null;if(l(r)===null)return{status:400,code:"invalid_request",param:i,message:`${i} must be a contract level like 1.0.`};if(s(r,t))return null;const o=r.trim();return{status:426,code:"runtime_version_unsupported",message:t===null||a(t).length===0?`The runtime did not name its contract level; the caller requires contract ${o} or later. Roll the runtime forward.`:`The runtime serves contract ${t}; the caller requires contract ${o} or later within the same major. Roll the runtime forward, or pin an SDK whose crouterContract the runtime serves.`,details:{sdk_version:(n.legacyMinimum??"").trim(),runtime_version:n.runtimeVersion??null,required_contract:o,runtime_contract:t}}}e(C,"contractRefusal");export{p as CONTRACT_HEADER,f as CONTRACT_LEVEL,i as REQUIRED_CONTRACT_HEADER,C as contractRefusal,s as contractSatisfies,l as parseContractLevel,a as parseServedContracts};
@@ -49,4 +49,5 @@ export * from './dto/chat-inventory.js';
49
49
  export * from './dto/analytics.js';
50
50
  export * from './dto/custom-objects.js';
51
51
  export * from './dto/plugins.js';
52
- export type { WatchRequest, WatchResultDTO, UnwatchQuery, UnwatchResultDTO } from './dto/objects.js';
52
+ export type { DeliveryRuleDTO, DocEditRequest, DocEditedDTO, DocKindName, DocUnsetField, DocWriteRequest, DocWrittenDTO, DocLinkReportDTO, DocStaleDTO, RuleDeliverName, RuleOnName, } from './dto/docs.js';
53
+ export type { DocumentReadData, ObjectDTO, ObjectReadDTO, WatchRequest, WatchResultDTO, UnwatchQuery, UnwatchResultDTO } from './dto/objects.js';
@@ -1 +1 @@
1
- var H=Object.defineProperty;var n=(e,t)=>H(e,"name",{value:t,configurable:!0});import{defineBranch as I,defineRoot as y,GLOBAL_TOKENS as P}from"./core/command.js";import{createCoreHookCatalog as x}from"./core/command-hooks/catalog.js";import{composeCoreHooks as R}from"./core/command-hooks/compose.js";import{mark as d}from"./core/timing.js";import{envNodeId as O,envScopes as D,isSandboxedNode as G,scopeAllowed as U}from"./shared/env.js";const S="crtr: agentic runtime.",b=[{name:"--json",desc:"stdout as the leaf's declared outputs in raw JSON instead of prose \u2014 for scripts (a cron's bash, external tooling) that must branch on canvas state; prose stays the contract for what you read yourself."}],E=Object.assign(Object.create(null),{doc:n(async()=>(await import("./commands/doc.js")).registerDoc(),"doc"),"custom-object":n(async()=>(await import("./commands/custom-object.js")).registerCustomObject(),"custom-object"),profile:n(async()=>(await import("./commands/profile.js")).registerProfile(),"profile"),pkg:n(async()=>(await import("./commands/pkg.js")).registerPkg(),"pkg"),human:n(async()=>(await import("./commands/human.js")).registerHuman(),"human"),sys:n(async()=>(await import("./commands/sys.js")).registerSys(),"sys"),node:n(async()=>(await import("./commands/node.js")).registerNode(),"node"),push:n(async()=>(await import("./commands/push.js")).registerPush(),"push"),cron:n(async()=>(await import("./commands/cron.js")).registerCron(),"cron"),canvas:n(async()=>(await import("./commands/canvas.js")).registerCanvas(),"canvas"),surface:n(async()=>(await import("./commands/surface.js")).registerSurface(),"surface")}),p=Object.freeze(Object.keys(E)),q={human:["crtr:act"],cron:["crtr:schedule"],doc:["crtr:memory:read:user","crtr:memory:rw:user"]};function B(e){const t=D();return t===null?e:e.filter(r=>{const o=q[r.name];return o===void 0||o.some(s=>U(t,s))||r.name==="doc"&&process.env.CRTR_GRANTEE?.startsWith("app:")===!0})}n(B,"allowedSubtrees");const N=new Map;function j(e){let t=N.get(e);return t===void 0&&(t=E[e](),N.set(e,t)),t}n(j,"loadSubtree");function v(e){return Promise.all(p.filter(t=>e.has(t)).map(j))}n(v,"loadCoreSubtreesNamed");function C(){return v(new Set(p))}n(C,"loadCoreSubtrees");function k(e){const t=new Set;for(const r of e){const o=r.split(" ")[0];p.includes(o)&&t.add(o)}return t}n(k,"subtreesNamedBy");let T;function ne(){return d("cli.hooks.catalog"),T??=C().then(x),T}n(ne,"coreHookCatalog");function L(){let e;return()=>(e??=(async()=>{const{compileHookRegistry:t,declaredCommandHookTargets:r,effectiveHookPlugins:o}=await import("./core/command-hooks/discovery.js"),s=o(),a=process.platform==="linux"&&O()?await(await import("./core/command-hooks/transport/plan.js")).remoteHookPlan(process.cwd()):[];if(d("cli.hooks.effective_plugins",{count:s.length+a.length}),s.length===0&&a.length===0)return d("cli.hooks.empty_registry"),t(x([]),[]);const c=process.platform==="linux"&&O()?s.filter(l=>l.scope==="project"):s,i=k([...r(c),...a.map(l=>l.target)]);return d("cli.hooks.catalog",{subtrees:[...i].join(",")}),t(x(await v(i)),c,a)})(),e)}n(L,"hookRegistryLoader");function g(e){const t=new Set,r=n((o,s)=>{if(!(o.kind==="branch"&&o.passthrough!==void 0)&&(t.add(s),o.kind==="branch"))for(const a of o.children)r(a,`${s} ${a.name}`)},"visit");for(const o of e)r(o,o.name);return t}n(g,"commandPathsFor");async function oe(){return g(await C())}n(oe,"coreCommandPaths");async function re(e){return g(await v(k(e)))}n(re,"coreCommandPathsCovering");function z(e){const t=[],r=n((o,s)=>{o.extensible===!0&&t.push(s);for(const a of o.children)a.kind==="branch"&&r(a,[...s,a.name])},"visit");for(const o of e)r(o,[o.name]);return t}n(z,"extensibleBranchPaths");const $=Object.freeze(["human","sys tutorial"]);async function M(){return new Set($)}n(M,"extensibleCoreBranches");async function se(){return new Set(z(await C()).map(e=>e.join(" ")))}n(se,"declaredExtensibleCoreBranches");function F(e,t){const{listing:r,...o}=e.help;return I({name:e.name,description:e.description,whenToUse:e.whenToUse,tier:e.tier,help:o,rootEntry:e.rootEntry,extensible:e.extensible,plugin:e.plugin,slash:e.slash,passthrough:e.passthrough,children:t})}n(F,"rebuildBranch");async function _(e,t,r){if(t.size===0)return e;const[{buildExternalCommandSnapshot:o},{composeCoreBranchMounts:s}]=await Promise.all([import("./core/command-plugins/discovery.js"),import("./core/command-plugins/compose.js")]),a=o(new Set(p),void 0,void 0,r,t),c=n((i,l)=>{let h=!1;const w=i.children.map(m=>{if(m.kind!=="branch")return m;const u=c(m,[...l,m.name]);return u!==m&&(h=!0),u}),f=i.extensible===!0?s(a.coreMounts,l):[];return h||f.length>0?F(i,[...w,...f]):i},"composeBranch");return e.map(i=>c(i,[i.name]))}n(_,"composeCorePluginMounts");async function J(e,t){const{validateEffectiveCommandPlugins:r}=await import("./core/command-plugins/discovery.js"),s=r(new Set(p),void 0,void 0,void 0,t).flatMap(c=>c.manifest===void 0?[]:[c.manifest]).filter(c=>c.coreMounts.some(i=>i.parent[0]===e));if(s.length===0)return null;const a=k(s.flatMap(c=>[...c.coreMounts.map(i=>i.parent.join(" ")),...Object.keys(c.helpAddenda??{})]));return a.add(e),g(await v(a))}n(J,"dispatchCommandPaths");function A(e,t){const r=e.slice(2).filter(o=>!P.has(o));return r[0]==="sys"&&r[1]===t}n(A,"isSysInvocation");const K=[["surface"],["sys"],["pkg"],["profile","default"],["canvas","chord"],["canvas","config"],["canvas","use"],["canvas","browse"],["canvas","issue"]];function ae(e){const t=e.slice(2).filter(o=>!P.has(o)),r=K.find(o=>o.every((s,a)=>t[a]===s));return r===void 0?null:r.join(" ")}n(ae,"personOnlyInvocation");function ie(e){return A(e,"daemon")}n(ie,"isDaemonControlInvocation");function ce(e){return A(e,"migrate")}n(ce,"isLayoutMigrationInvocation");async function ue(){const{registerDaemonControlSys:e}=await import("./commands/sys/daemon.js");return y({tagline:S,globals:b,subtrees:[e()]})}n(ue,"resolveDaemonControlRoot");async function le(){const[{defineSysBranch:e},{sysMigrateLeaf:t}]=await Promise.all([import("./commands/sys/branch.js"),import("./commands/sys/migrate.js")]);return y({tagline:S,globals:b,subtrees:[e([t])]})}n(le,"resolveLayoutMigrationRoot");async function me(e){return(e!==void 0?await W(e):null)??X()}n(me,"resolveRoot");async function W(e){if(E[e]===void 0)return null;const t=await j(e),r=await M(),o=[...r].some(a=>a.split(" ")[0]===e)?await J(e,r):null,s=B(o===null?[t]:await _([t],r,o));return s.length>0?y({tagline:S,globals:b,subtrees:R(s,L())}):null}n(W,"resolveCoreRoot");async function X(){const e=await C(),[{buildExternalCommandSnapshot:t},{composeExternalSubtrees:r,composeSandboxProviderSubtrees:o},{qualifiedTopLevelCommandName:s}]=await Promise.all([import("./core/command-plugins/discovery.js"),import("./core/command-plugins/compose.js"),import("./core/command-manifests/registry.js")]),a=await M(),c=await _(e,a,g(e)),i=t(new Set(p),void 0,void 0,g(e),a);let l=r(i);if(G()){const{cliClient:h}=await import("./commands/api-client.js");d("cli.providers.request");const{providers:w}=await h().request("GET","/v1/providers/tools");d("cli.providers.fetched",{providers:w.length});const f=o(w,new Set(p)),m=new Set(f.map(u=>u.name));l=[...f,...r({...i,contributions:i.contributions.map(u=>m.has(u.node.name)?{...u,node:{...u.node,name:s(u.contributor,u.node.name)}}:u)})]}return y({tagline:S,globals:b,subtrees:[...R(B(c),L()),...l]})}n(X,"buildRoot");export{$ as EXTENSIBLE_CORE_BRANCHES,p as SUBTREE_NAMES,X as buildRoot,oe as coreCommandPaths,re as coreCommandPathsCovering,ne as coreHookCatalog,se as declaredExtensibleCoreBranches,M as extensibleCoreBranches,ie as isDaemonControlInvocation,ce as isLayoutMigrationInvocation,ae as personOnlyInvocation,W as resolveCoreRoot,ue as resolveDaemonControlRoot,le as resolveLayoutMigrationRoot,me as resolveRoot};
1
+ var H=Object.defineProperty;var n=(e,t)=>H(e,"name",{value:t,configurable:!0});import{defineBranch as I,defineRoot as y,GLOBAL_TOKENS as R}from"./core/command.js";import{createCoreHookCatalog as x}from"./core/command-hooks/catalog.js";import{composeCoreHooks as O}from"./core/command-hooks/compose.js";import{mark as p}from"./core/timing.js";import{envScopes as D,isSandboxedNode as E,scopeAllowed as G}from"./shared/env.js";const S="crtr: agentic runtime.",b=[{name:"--json",desc:"stdout as the leaf's declared outputs in raw JSON instead of prose \u2014 for scripts (a cron's bash, external tooling) that must branch on canvas state; prose stays the contract for what you read yourself."}],k=Object.assign(Object.create(null),{doc:n(async()=>(await import("./commands/doc.js")).registerDoc(),"doc"),"custom-object":n(async()=>(await import("./commands/custom-object.js")).registerCustomObject(),"custom-object"),profile:n(async()=>(await import("./commands/profile.js")).registerProfile(),"profile"),pkg:n(async()=>(await import("./commands/pkg.js")).registerPkg(),"pkg"),human:n(async()=>(await import("./commands/human.js")).registerHuman(),"human"),sys:n(async()=>(await import("./commands/sys.js")).registerSys(),"sys"),node:n(async()=>(await import("./commands/node.js")).registerNode(),"node"),push:n(async()=>(await import("./commands/push.js")).registerPush(),"push"),cron:n(async()=>(await import("./commands/cron.js")).registerCron(),"cron"),canvas:n(async()=>(await import("./commands/canvas.js")).registerCanvas(),"canvas"),surface:n(async()=>(await import("./commands/surface.js")).registerSurface(),"surface")}),d=Object.freeze(Object.keys(k)),U={human:["crtr:act"],cron:["crtr:schedule"],doc:["crtr:memory:read:user","crtr:memory:rw:user"]};function B(e){const t=D();return t===null?e:e.filter(r=>{const o=U[r.name];return o===void 0||o.some(s=>G(t,s))||r.name==="doc"&&process.env.CRTR_GRANTEE?.startsWith("app:")===!0})}n(B,"allowedSubtrees");const j=new Map;function N(e){let t=j.get(e);return t===void 0&&(t=k[e](),j.set(e,t)),t}n(N,"loadSubtree");function v(e){return Promise.all(d.filter(t=>e.has(t)).map(N))}n(v,"loadCoreSubtreesNamed");function C(){return v(new Set(d))}n(C,"loadCoreSubtrees");function P(e){const t=new Set;for(const r of e){const o=r.split(" ")[0];d.includes(o)&&t.add(o)}return t}n(P,"subtreesNamedBy");let T;function te(){return p("cli.hooks.catalog"),T??=C().then(x),T}n(te,"coreHookCatalog");function L(){let e;return()=>(e??=(async()=>{const{compileHookRegistry:t,declaredCommandHookTargets:r,effectiveHookPlugins:o}=await import("./core/command-hooks/discovery.js"),s=o(),a=E()?await(await import("./core/command-hooks/transport/plan.js")).remoteHookPlan(process.cwd()):[];if(p("cli.hooks.effective_plugins",{count:s.length+a.length}),s.length===0&&a.length===0)return p("cli.hooks.empty_registry"),t(x([]),[]);const c=E()?s.filter(l=>l.scope==="project"):s,i=P([...r(c),...a.map(l=>l.target)]);return p("cli.hooks.catalog",{subtrees:[...i].join(",")}),t(x(await v(i)),c,a)})(),e)}n(L,"hookRegistryLoader");function g(e){const t=new Set,r=n((o,s)=>{if(!(o.kind==="branch"&&o.passthrough!==void 0)&&(t.add(s),o.kind==="branch"))for(const a of o.children)r(a,`${s} ${a.name}`)},"visit");for(const o of e)r(o,o.name);return t}n(g,"commandPathsFor");async function ne(){return g(await C())}n(ne,"coreCommandPaths");async function oe(e){return g(await v(P(e)))}n(oe,"coreCommandPathsCovering");function q(e){const t=[],r=n((o,s)=>{o.extensible===!0&&t.push(s);for(const a of o.children)a.kind==="branch"&&r(a,[...s,a.name])},"visit");for(const o of e)r(o,[o.name]);return t}n(q,"extensibleBranchPaths");const z=Object.freeze(["human","sys tutorial"]);async function M(){return new Set(z)}n(M,"extensibleCoreBranches");async function re(){return new Set(q(await C()).map(e=>e.join(" ")))}n(re,"declaredExtensibleCoreBranches");function $(e,t){const{listing:r,...o}=e.help;return I({name:e.name,description:e.description,whenToUse:e.whenToUse,tier:e.tier,help:o,rootEntry:e.rootEntry,extensible:e.extensible,plugin:e.plugin,slash:e.slash,passthrough:e.passthrough,children:t})}n($,"rebuildBranch");async function _(e,t,r){if(t.size===0)return e;const[{buildExternalCommandSnapshot:o},{composeCoreBranchMounts:s}]=await Promise.all([import("./core/command-plugins/discovery.js"),import("./core/command-plugins/compose.js")]),a=o(new Set(d),void 0,void 0,r,t),c=n((i,l)=>{let h=!1;const w=i.children.map(m=>{if(m.kind!=="branch")return m;const u=c(m,[...l,m.name]);return u!==m&&(h=!0),u}),f=i.extensible===!0?s(a.coreMounts,l):[];return h||f.length>0?$(i,[...w,...f]):i},"composeBranch");return e.map(i=>c(i,[i.name]))}n(_,"composeCorePluginMounts");async function F(e,t){const{validateEffectiveCommandPlugins:r}=await import("./core/command-plugins/discovery.js"),s=r(new Set(d),void 0,void 0,void 0,t).flatMap(c=>c.manifest===void 0?[]:[c.manifest]).filter(c=>c.coreMounts.some(i=>i.parent[0]===e));if(s.length===0)return null;const a=P(s.flatMap(c=>[...c.coreMounts.map(i=>i.parent.join(" ")),...Object.keys(c.helpAddenda??{})]));return a.add(e),g(await v(a))}n(F,"dispatchCommandPaths");function A(e,t){const r=e.slice(2).filter(o=>!R.has(o));return r[0]==="sys"&&r[1]===t}n(A,"isSysInvocation");const J=[["surface"],["sys"],["pkg"],["profile","default"],["canvas","chord"],["canvas","config"],["canvas","use"],["canvas","browse"],["canvas","issue"]];function se(e){const t=e.slice(2).filter(o=>!R.has(o)),r=J.find(o=>o.every((s,a)=>t[a]===s));return r===void 0?null:r.join(" ")}n(se,"personOnlyInvocation");function ae(e){return A(e,"daemon")}n(ae,"isDaemonControlInvocation");function ie(e){return A(e,"migrate")}n(ie,"isLayoutMigrationInvocation");async function ce(){const{registerDaemonControlSys:e}=await import("./commands/sys/daemon.js");return y({tagline:S,globals:b,subtrees:[e()]})}n(ce,"resolveDaemonControlRoot");async function ue(){const[{defineSysBranch:e},{sysMigrateLeaf:t}]=await Promise.all([import("./commands/sys/branch.js"),import("./commands/sys/migrate.js")]);return y({tagline:S,globals:b,subtrees:[e([t])]})}n(ue,"resolveLayoutMigrationRoot");async function le(e){return(e!==void 0?await K(e):null)??W()}n(le,"resolveRoot");async function K(e){if(k[e]===void 0)return null;const t=await N(e),r=await M(),o=[...r].some(a=>a.split(" ")[0]===e)?await F(e,r):null,s=B(o===null?[t]:await _([t],r,o));return s.length>0?y({tagline:S,globals:b,subtrees:O(s,L())}):null}n(K,"resolveCoreRoot");async function W(){const e=await C(),[{buildExternalCommandSnapshot:t},{composeExternalSubtrees:r,composeSandboxProviderSubtrees:o},{qualifiedTopLevelCommandName:s}]=await Promise.all([import("./core/command-plugins/discovery.js"),import("./core/command-plugins/compose.js"),import("./core/command-manifests/registry.js")]),a=await M(),c=await _(e,a,g(e)),i=t(new Set(d),void 0,void 0,g(e),a);let l=r(i);if(E()){const{cliClient:h}=await import("./commands/api-client.js");p("cli.providers.request");const{providers:w}=await h().request("GET","/v1/providers/tools");p("cli.providers.fetched",{providers:w.length});const f=o(w,new Set(d)),m=new Set(f.map(u=>u.name));l=[...f,...r({...i,contributions:i.contributions.map(u=>m.has(u.node.name)?{...u,node:{...u.node,name:s(u.contributor,u.node.name)}}:u)})]}return y({tagline:S,globals:b,subtrees:[...O(B(c),L()),...l]})}n(W,"buildRoot");export{z as EXTENSIBLE_CORE_BRANCHES,d as SUBTREE_NAMES,W as buildRoot,ne as coreCommandPaths,oe as coreCommandPathsCovering,te as coreHookCatalog,re as declaredExtensibleCoreBranches,M as extensibleCoreBranches,ae as isDaemonControlInvocation,ie as isLayoutMigrationInvocation,se as personOnlyInvocation,K as resolveCoreRoot,ce as resolveDaemonControlRoot,ue as resolveLayoutMigrationRoot,le as resolveRoot};
@@ -9,6 +9,23 @@ 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.488
13
+
14
+ ### Developer CLI: `--help` exits 0, `login` is safe to re-run (CLI only, no app change)
15
+
16
+ - `npx @north-light/sdk <command> --help` and `-h` print the command's usage and exit 0 for every command. A bad flag still exits 1.
17
+ - `login` run again while this computer is logged in prints `Already logged in as …` and exits 0 instead of starting a second login (`--new` still does). A failed poll (a dropped connection, a sleeping laptop) is retried for up to 3 minutes; if the directory is still unreachable, `login` exits 1 and says the code is still saved and to run `login` again. It no longer ends on the first failed poll with a console fallback message.
18
+ - `init` writes `"allowScripts": {"esbuild": false}` into a new app's `package.json`, so npm 11.20+ no longer warns that esbuild's postinstall script is not allowed.
19
+ - `apps set-logo --file <path>` uploads a logo image (PNG, JPEG, WebP or SVG, square, at least 256x256, at most 512 KB) and `apps set-brand-color --color <#rrggbb or none>` sets or clears the brand colour. Both work on testing and verified apps. The directory side must be live first: until it is, they answer with the directory's refusal.
20
+ - `apps set-listing`, `set-spend-cap` and `set-allows` refuse with the same plain wording as the docs. They still make no request.
21
+
22
+ ### `docs`: an app writes, reads and edits documents it owns (additive, no app change)
23
+
24
+ - `docs.write({name, body, preview, kind?, rules?, privacy?, summary?})` creates a document owned by your app and returns its canonical `name` (`<app>/<name>`). `kind: 'preference'` with `rules: [{on: 'boot', deliver: 'content'}]` puts the body into the context of every node at boot. A name already taken throws `ConflictError` (`name_taken`).
25
+ - `docs.get(name)` returns `{body, revision, base, rules, kind, owner, needsMerge, …}`; no such document throws `NotFoundError`.
26
+ - `docs.edit(name, {body | replace, rationale, base?, …})` changes it in place and returns the new `revision`. Pass the `base` from `docs.get`; a base that is no longer current throws `ConflictError` (`stale_revision`) and writes nothing.
27
+ - Served on the app listener, so `oauth.client(userId)` offers it. An app can write only as itself (`owner: 'app'`, the default), and needs no scope for documents it owns.
28
+
12
29
  ## 0.3.487
13
30
 
14
31
  ### Run notices and the app listener verify one call-token shape (additive, no app change)
@@ -30,6 +30,7 @@ Every namespace exported by the client. Namespaces are camelCase. Verbs are `cre
30
30
  | `models.config` | `update` | `PUT /v1/model-config` |
31
31
  | `customObjects` | `register`, `publish`, `unregister`, `ack`, `receive` | `/v1/custom-objects…` |
32
32
  | `memory` | `list`, `listAll`, `get` | `GET /v1/memory`, `GET /v1/memory/document` |
33
+ | `docs` | `write`, `edit`, `get` | `POST /v1/docs`, `PATCH /v1/docs/:ref`, `GET /v1/objects/:ref?fields=1` |
33
34
  | `runs` | `start`, `get`, `list`, `listAll`, `message`, `interrupt`, `cancel`, `delete`, `rename`, `trace`, `events`, `reply`, `stream`, `wait`, `parse` | `/v1/runs…` |
34
35
  | `uploads` | `create` | `POST /v1/uploads` |
35
36
  | `attachments` | `link` | `GET /v1/attachments/link` |
@@ -39,7 +40,7 @@ Every namespace exported by the client. Namespaces are camelCase. Verbs are `cre
39
40
  | `grant` | `get` (`wait`, `since`: hold until `version` changes) | `GET /v1/grant` |
40
41
  | `health` | `get` | `GET /v1/health` |
41
42
 
42
- `runs`, `uploads`, `attachments`, `shares`, `questions` and the app side of `grant` and `providers` are the application-facing API an app reaches through the app listener; `oauth.client(userId)` returns a client typed to exactly that set (`AppClient`); `nodes`, `bash`, `canvas`, `crons`, `human`, `models`, `customObjects`, `structuredRequests`, `system`, `auth`, `files.read/write/list` and `profiles.ensure/retrieve` are the owner's own daemon and are not served to an app. An app schedules work with its own timer calling `runs.start`, or a run's agent runs `crtr cron add` under `crtr:schedule`. See [[crouter-sdk/guides/apps-on-northlight]] and [[crouter-sdk/guides/apps-with-many-users]]. `memory` is the person's `public` documents ([[crouter-sdk/memory]]). `system.health` (`/healthz`) is the daemon's liveness check; `health.get` is the app listener's.
43
+ `runs`, `uploads`, `attachments`, `shares`, `questions`, `docs` and the app side of `grant` and `providers` are the application-facing API an app reaches through the app listener; `oauth.client(userId)` returns a client typed to exactly that set (`AppClient`); `nodes`, `bash`, `canvas`, `crons`, `human`, `models`, `customObjects`, `structuredRequests`, `system`, `auth`, `files.read/write/list` and `profiles.ensure/retrieve` are the owner's own daemon and are not served to an app. An app schedules work with its own timer calling `runs.start`, or a run's agent runs `crtr cron add` under `crtr:schedule`. See [[crouter-sdk/guides/apps-on-northlight]] and [[crouter-sdk/guides/apps-with-many-users]]. `memory` is the person's `public` documents ([[crouter-sdk/memory]]). `docs` writes, reads and edits documents the calling app owns: `docs.write({name, body, preview, kind?, rules?})` returns the canonical `name`; `docs.get(name)` returns `body`, `revision`, `rules` and the edit `base`; `docs.edit(name, {body, rationale, base?})` changes it in place and refuses a stale `base` with `ConflictError` (`stale_revision`). To keep one document current, `get` it (catch `NotFoundError` on first run to `write`), compare `body` to your text, and `edit` only when they differ. `system.health` (`/healthz`) is the daemon's liveness check; `health.get` is the app listener's.
43
44
 
44
45
  Action methods keep the product's literal name (`fork`, `revive`, `promote`, `yield`) rather than being renamed into a generic verb. Every request-capable method takes `RequestOptions` as its final argument after its path, body, or query arguments. `RequestOptions` is `{ headers?, signal?, timeout?, maxRetries? }`; stream event options exclude `timeout` and add `after`.
45
46
 
@@ -49,7 +49,7 @@ Every delivery registers the document or listing and its level in the loaded con
49
49
 
50
50
  ## The reader's view
51
51
 
52
- A rule applies to every node whose view includes the document's owner. A node's view is: its own node, the repos it works in (those enclosing its cwd, nearest first), the repos enclosing each project path of its profile (in manifest order; a path that is gone or is not in a repo adds nothing, and a Linux node sandbox sees none), its profile, its app, the user, and installed plugins (each an app that owns its shipped documents), with crtr's own documents (owned by the app `crtr`) last. A bare name resolves in that same order, which is what lets a repo's document shadow one of crtr's own. A rule's `if: {repo}` condition holds when that repo is among the node's eligible repos (cwd repos plus project repos), so a rule on any document, including a user- or profile-owned one, can target a repo the profile covers; match `cwd` to condition on where the node works. `--owner repo` and write access follow only the repos enclosing the cwd, and a repo-relative `./` `file-read` glob stays anchored to the checkout of the repo that owns the rule. A node's own documents are in no other node's view, so their rules apply only to that node.
52
+ A rule applies to every node whose view includes the document's owner. A node's view is: its own node, the repos it works in (those enclosing its cwd, nearest first), the repos enclosing each project path of its profile (in manifest order; a path that is gone or is not in a repo adds nothing, and a Linux node sandbox sees none), its profile, its app, the user, and installed plugins (each an app that owns its shipped documents), with crtr's own documents (owned by the app `crtr`) last. A bare name resolves in that same order, which is what lets a repo's document shadow one of crtr's own. A rule's `if: {repo}` condition holds when that repo is among the node's eligible repos (cwd repos plus project repos), so a rule on any document, including a user- or profile-owned one, can target a repo the profile covers; match `cwd` to condition on where the node works. `--owner repo` follows only the repos enclosing the cwd. A node may read a repo's documents when its file permissions reach a checkout of the repo (enclosing its cwd or one of its profile's project paths), and change them when its file permissions allow writing that checkout. A repo-relative `./` `file-read` glob stays anchored to the checkout of the repo that owns the rule. A node's own documents are in no other node's view, so their rules apply only to that node.
53
53
 
54
54
  Each owner the selected profile manages carries a delivery limit — `none`, `name`, `preview`, or `content` — and that value is the maximum level anything that owner holds delivers at `boot` and `workspace-open`, whatever directory the node is working in and whether the repo is in view through its cwd or the profile's project list. `crtr profile project add` starts the added repos at `preview`. It only lowers: a rule authored below the maximum delivers at its authored level. `none` contributes nothing to either automatic event, so a `none` owner cannot shadow a same-named document from a wider owner — that wider document becomes the winner. An owner with no limit set delivers exactly what it authored.
55
55
 
@@ -7,7 +7,9 @@ when-and-why-to-read: When you need CLI reference, this knowledge should be read
7
7
 
8
8
  # CLI reference
9
9
 
10
- `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.
10
+ `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. `npx @north-light/sdk <command> --help` (or `-h`) prints that command's usage and exits 0. An unknown command, or a bad flag, prints the usage and exits 1.
11
+
12
+ With no dependencies installed, `npx` downloads the newest published release of the SDK (the one tagged latest on npm), not the version your project pins. Run `npm i` in the project first if the pinned version must run; `init` pins the version of the CLI that ran it.
11
13
 
12
14
  | Command | What it does | Needs crtr |
13
15
  |---|---|---|
@@ -15,11 +17,11 @@ when-and-why-to-read: When you need CLI reference, this knowledge should be read
15
17
  | `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 |
16
18
  | `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` |
17
19
  | `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` |
18
- | `npx @north-light/sdk login [--no-browser] [--new] [--issuer <url>]` | Log this computer in to your Northlight account: prints a link and a code, you approve once in the browser, and a revocable login is saved to `~/.config/northlight/credentials.json`. Run it again if it times out: it resumes the same code. Calls only the Northlight directory. | No |
19
- | `npx @north-light/sdk logout [--issuer <url>]` | Revoke this computer's login on the server and delete it locally. | No |
20
+ | `npx @north-light/sdk login [--no-browser] [--new] [--issuer <url>]` | Log this computer in to your Northlight account: prints a link and a code, you approve once in the browser, and a revocable login is saved to `~/.config/northlight/credentials.json`. Run it again if it times out or is interrupted: it resumes the same code. When this computer is already logged in it says so and exits 0; `--new` logs in again. Calls only the Northlight directory. | No |
21
+ | `npx @north-light/sdk logout [--issuer <url>]` | Revoke this computer's login on the server and remove it from the credentials file. The file stays, without that login. | No |
20
22
  | `npx @north-light/sdk whoami [--issuer <url>]` | Show the account this computer is logged in as and when the login expires. Exits 1 when not logged in. | No |
21
23
  | `npx @north-light/sdk register [--name <text>] [--scope <scope>]... [--logo <url>] [--justification <text>] [--redirect-uri <uri>]... [--issuer <url>]` | Run in a project made by `init`, after `login`: registers the app with the public key from `local/app-key.json`, adds each `--scope` beyond `crtr:llm crtr:act`, and writes `CROUTER_CLIENT_ID` and `CROUTER_ISSUER` to `local/app.env`. | No |
22
- | `npx @north-light/sdk apps <leaf> [--app <client id>] [--issuer <url>] [leaf flags] [--json]` | Call one app-management leaf with the login and print its answer as JSON. Leaves: `mine`, `get`, `update`, `set-redirect-uris`, `asks`, `set-scopes`, `set-requires`, `set-run-notice-url`, `keys list\|add\|retire\|delete\|set-jwks-uri\|remove-jwks-uri`, `testers list\|add\|remove`. `--app` defaults to `CROUTER_CLIENT_ID` in `local/app.env`; `keys add` takes `--key-file <path>` and sends only the public half. Deleting an app, its marketplace listing, spend cap, sharing and provider role are console-only. | No |
24
+ | `npx @north-light/sdk apps <leaf> [--app <client id>] [--issuer <url>] [leaf flags] [--json]` | Call one app-management leaf with the login and print its answer as JSON. Leaves: `mine`, `get`, `update`, `set-logo --file <path>`, `set-brand-color --color <#rrggbb|none>`, `set-redirect-uris`, `asks`, `set-scopes`, `set-requires`, `set-run-notice-url`, `keys list\|add\|retire\|delete\|set-jwks-uri\|remove-jwks-uri`, `testers list\|add\|remove`. `--app` defaults to `CROUTER_CLIENT_ID` in `local/app.env`; `keys add` takes `--key-file <path>` and sends only the public half. Deleting an app, its marketplace listing, monthly spend cap, sharing and provider role are console-only: those leaves exit 1 and name the console page, with no request. | No |
23
25
 
24
26
  `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. `login`, `logout`, `whoami`, `register` and `apps` call only the Northlight directory.
25
27
 
@@ -35,11 +37,23 @@ It never overwrites a file. On a refused step it exits non-zero and names the fi
35
37
 
36
38
  ### `login`, `logout`, `whoami`
37
39
 
38
- `login` prints a link and a code and waits while you approve in the browser, then saves a revocable login to `~/.config/northlight/credentials.json`. `--no-browser` does not open the link; `--new` discards a pending code and starts a fresh one. `whoami` shows the account and when the login expires, and exits 1 when this computer is not logged in. `logout` revokes the login on the server and deletes the local file. The login can manage your apps; it cannot delete one (not yet checked on production).
40
+ `login` prints a link and a code and waits while you approve in the browser, then saves a revocable login to `~/.config/northlight/credentials.json`. `--no-browser` does not open the link; `--new` discards a pending code and starts a fresh one.
41
+
42
+ What `login` does when it runs again:
43
+
44
+ - **A code is still pending** (the earlier run was killed, timed out or lost its connection): it prints "Still waiting for the login started at HH:MM." with the same link and code, and keeps polling. Do not pass `--new`.
45
+ - **This computer is already logged in**: it prints "Already logged in as … Nothing to do." and exits 0, without starting another login. `--new` makes a new login and revokes the old one once the new one is approved.
46
+ - **The saved login was revoked or has expired**: it says so and starts a new login.
47
+ - **The directory cannot be reached while it waits**: a failed poll is retried for up to 3 minutes. If it still cannot reach the directory it exits 1 and says the code is still saved, until when, and to run `login` again. Nothing is lost.
48
+ - **The code expired** (10 minutes) **or was declined**: it exits 1 and says which. Run `login` again for a new code.
49
+
50
+ It exits 0 only when the login is saved or was already there.
51
+
52
+ `whoami` shows the account and when the login expires, and exits 1 when this computer is not logged in. `logout` revokes the login on the server and removes it from the credentials file, which stays on disk as `{"version":1,"issuers":{}}` when no other login is in it. The login can manage your apps; it cannot delete one: `apps delete` exits 1 and names the console page, and a raw call to the directory's delete answers 403 `provider_refused`.
39
53
 
40
54
  ### `register`
41
55
 
42
- Run it in the project `init` made, after `login`. It sends the app's origin from `local/app.env`, the public half of `local/app-key.json` and the permissions you pass with `--scope`, and writes the client ID and issuer it gets back into `local/app.env`. `crtr:llm` and `crtr:act` are always requested (not yet checked on production).
56
+ Run it in the project `init` made, after `login`. It sends the app's origin from `local/app.env`, the public half of `local/app-key.json` and the permissions you pass with `--scope`, and writes the client ID and issuer it gets back into `local/app.env`. `crtr:llm` and `crtr:act` are always requested.
43
57
 
44
58
  ### `apps`
45
59
 
@@ -49,9 +49,11 @@ An agent that has no project yet can read the hosted copy:
49
49
 
50
50
  The agent runs `npx @north-light/sdk@latest init`, then `npx @north-light/sdk login`, which prints a link and a code. It shows you both. You open the link, check the page shows the same code, and choose **Allow**. The agent then runs `npx @north-light/sdk register --scope sms:send`, installs, starts the app, and asks you to open `/auth/login` and choose **Allow** on the consent page. You sign up for Northlight first. The hosted copy matches the SDK version the docs site was last built from.
51
51
 
52
- If the agent's shell call times out while you approve, it runs `login` again and the same code resumes. If it cannot reach `account.northlight.ai`, it falls back to asking you to register in the console.
52
+ If the agent's shell call times out or is interrupted while you approve, it runs `login` again and the same code resumes. If you approved already, `login` says "Already logged in" and exits 0: the agent goes on to `register`. A code left unapproved for 10 minutes expires; `login` then exits 1 saying the code expired, and the agent runs it again for a new code and asks you to approve that one. Only a message that says it could not reach `account.northlight.ai` means a network problem, and even then `login` says the code is still saved and can be resumed. An expired code or a failed run is not a reason to fall back to the console.
53
53
 
54
- Checked with Claude Code (`claude -p`, no crtr on `PATH`, an empty directory) before `login` existed: given only the skill URL plus "I want the quickstart app: sign in with Northlight, start a run, text me", it ran `init my-app`, read the generated `server.ts`, and stopped with the console steps and the public key for you to paste. Name what you want built in the prompt. The journey with `login` and `register` has not been run by an agent on production (not yet checked on production).
54
+ The skill tells the agent to read the whole message `login` printed and its exit code before it tells you what happened.
55
+
56
+ Checked on production with Claude Code (an interactive session, no crtr on `PATH`, an empty directory), given only the skill URL plus the quickstart request: a fresh session ran `init`, `login`, `npm i`, `register --scope sms:send` and `npm start` by itself. The person's part was choosing **Allow** on the login page and on the app's consent page, and pressing the button; the run replied `pong` and the text was sent (whether it reached the phone is not checked). When the first login code expired unapproved, that session said so and ran `login` again for a new code. In an earlier session on the same day, before this guidance, the agent misread a failed second `login` as a network timeout and fell back to the console steps. Name what you want built in the prompt.
55
57
 
56
58
  ## What the login can do
57
59
 
@@ -60,19 +62,19 @@ Checked with Claude Code (`claude -p`, no crtr on `PATH`, an empty directory) be
60
62
  | It can | It cannot |
61
63
  |---|---|
62
64
  | Register new apps (at most 10 a day) | Delete an app |
63
- | Change a name, origin, logo and description | List an app in the marketplace, or change its spend cap or sharing |
65
+ | Change a name, origin, logo and description, upload a logo image, set the brand colour (also on a verified app) | List an app in the marketplace, or change its spend cap or sharing |
64
66
  | Change what an app asks for and requires | Set the provider role |
65
67
  | Add and remove testers | See private keys, or use your runtime, your AI subscription, your texts or your data |
66
68
  | Change signing keys, redirect URIs and the run notice URL on an app in testing | Change those on a verified app: do it in the console |
67
69
  | | Approve another login |
68
70
 
69
- Deleting an app, its listing, spend cap and sharing stay in the console on purpose. The CLI refuses them without sending a request, and the directory refuses them if a call is made by hand.
71
+ Deleting an app, its listing, spend cap and sharing stay in the console on purpose. The CLI refuses them without sending a request, and the directory refuses them (403 `provider_refused`) if a call is made by hand. Checked on production for delete, the marketplace listing and the spend cap.
70
72
 
71
- The login lives in `~/.config/northlight/credentials.json` (`$XDG_CONFIG_HOME/northlight/credentials.json` when set), mode `0600`, outside your project. The CLI refuses a file that other users can read. It lasts 30 days, or until it has gone 14 days unused, and then you run `login` again. An account holds at most 10 logins; an eleventh replaces the oldest.
73
+ The login lives in `~/.config/northlight/credentials.json` (`$XDG_CONFIG_HOME/northlight/credentials.json` when set), mode `0600`, outside your project. The CLI refuses a file that other users can read. It lasts 30 days, or until it has gone 14 days unused, and then you run `login` again. An account holds at most 10 logins; an eleventh replaces the oldest. The Developer access page shows the date the login goes unused, "Expires Oct 31" for a login approved on Oct 17, not the 30-day date the CLI prints.
72
74
 
73
- Revoke it on your account's Developer access page (`https://account.northlight.ai/account/developer`), or run `npx @north-light/sdk logout`, which revokes it on the server and deletes the file. The next command then fails and asks for `login`.
75
+ Revoke it on your account's Developer access page (`https://account.northlight.ai/account/developer`), or run `npx @north-light/sdk logout`, which revokes it on the server and removes it from the file. The file is not deleted: it is rewritten as `{"version":1,"issuers":{}}`, with no secret in it. A revoked login stops working at once: `whoami` and every other command fail with "This login was revoked" and ask for `login`, and a token minted before the revoke is refused with 401 `credential_revoked` even before it expires. Checked on production: the Developer access page, Revoke, `logout`, and what the next command prints.
74
76
 
75
- Not yet checked on production: every statement in this section, including the limits and the page.
77
+ Not yet checked on production: the 10-login limit and the eleventh login replacing the oldest, and the 14-day and 30-day expiries.
76
78
 
77
79
  ## How it stays current
78
80
 
@@ -53,6 +53,7 @@ Until you retire the old key it is your rollback. After you retire it, a sign-in
53
53
  ## Profile
54
54
 
55
55
  - **How people see your app**: Name, Origin URL, Logo URL, and **Why your app needs access** (quoted on the consent page as your words). **Save details**. From the terminal: `apps update`.
56
+ - **Logo image and brand colour from the terminal.** `apps update --logo <url>` sets the logo URL. `apps set-logo --file <path>` uploads an image instead (PNG, JPEG, WebP or SVG, square, at least 256x256, at most 512 KB), and `apps set-brand-color --color <#rrggbb>` sets the colour (`--color none` clears it). Both work on testing and verified apps.
56
57
  - **Marketplace**: **Listed in the marketplace**, Price, **Sign-in start URL** and **Text welcome**. Agents can install a listed app for a person by text. A testing app stays out of the marketplace even when listed. The sign-in start URL is where Install sends people; your `/auth/login` route is a valid one, and a `localhost` URL is accepted. The text welcome is one or two plain sentences, at most 120 characters, with no links.
57
58
  - **Delete this app.** At the bottom of the tab; see [Delete an app](#delete-an-app).
58
59
 
@@ -91,7 +91,9 @@ and "Done. Go back to your terminal." in the browser. The name, id and dates are
91
91
 
92
92
  The login is saved outside your project, in `~/.config/northlight/credentials.json` (mode `0600`). Never commit, paste or print it.
93
93
 
94
- Not yet checked on production: this whole step, including the confirm page and its wording (not yet checked on production). The command, its output and its files are checked against a fake directory in the package's tests.
94
+ Checked on production: this whole step, including the confirm page and its wording. The confirm page reads "Let a coding agent manage your Northlight apps?", shows the code, the computer's name and platform and the address it asked from ("The computer named itself; Northlight did not check it"), and lists what the login can and can't do. After **Allow** the page reads "Done / Go back to your terminal." and the terminal prints the output above.
95
+
96
+ If you run `login` again, it does not start over. While a code is pending it prints "Still waiting for the login started at HH:MM." with the same link and code. Once you are logged in it prints "Already logged in as …" and exits 0. If the code expired unapproved it exits 1 and says so: run it again for a new code. See [[northlight-apps/cli]].
95
97
 
96
98
  If `login` cannot reach `account.northlight.ai`, or you would rather use the browser, skip to [Register in the browser instead](#register-in-the-browser-instead).
97
99
 
@@ -101,7 +103,7 @@ If `login` cannot reach `account.northlight.ai`, or you would rather use the bro
101
103
  npx @north-light/sdk register --scope sms:send
102
104
  ```
103
105
 
104
- Run it in the folder `init` made. It registers the app with the public key from `local/app-key.json`, the name from `package.json`, the origin from `local/app.env` and the redirect URI `<origin>/auth/callback`, then adds `sms:send` to what the app asks for. A new app starts asking for `crtr:llm` and `crtr:act`. The server in this guide also asks for `sms:send`, so it needs the `--scope`. Take scope strings from `npx @north-light/sdk apps asks` or from `providers.tools`. Do not invent them. See [[northlight-apps/concepts/scopes-and-grants]].
106
+ Run it in the folder `init` made. With no dependencies installed, `npx` downloads the newest published release of the SDK, not the version your `package.json` pins, so run `npm i` first if the pinned version should run. It registers the app with the public key from `local/app-key.json`, the name from `package.json`, the origin from `local/app.env` and the redirect URI `<origin>/auth/callback`, then adds `sms:send` to what the app asks for. A new app starts asking for `crtr:llm` and `crtr:act`. The server in this guide also asks for `sms:send`, so it needs the `--scope`. Take scope strings from `npx @north-light/sdk apps asks` or from `providers.tools`. Do not invent them. See [[northlight-apps/concepts/scopes-and-grants]].
105
107
 
106
108
  You should see:
107
109
 
@@ -118,7 +120,7 @@ Next: npm i && npm start, then open http://localhost:3000/auth/login
118
120
 
119
121
  `local/app.env` now has all four settings. The name, client ID and key ID are examples. Change the defaults with `--name`, `--logo`, `--justification` and `--redirect-uri`; the default logo is a placeholder, `https://sdk.docs.northlight.ai/app-logo.png`. `register` refuses when `CROUTER_CLIENT_ID` is already set. A login can register at most 10 apps a day.
120
122
 
121
- Not yet checked on production: this step, including that the directory accepts the default logo (not yet checked on production). Its output and its effect on `local/app.env` are checked against a fake directory in the package's tests.
123
+ Checked on production: this step, including that the directory accepts the default logo.
122
124
 
123
125
  ## 5. Run it
124
126