@north-light/crouter 0.3.331 → 0.3.333

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 (128) hide show
  1. package/README.md +2 -2
  2. package/dist/api/command-manifest/manifest.js +1 -1
  3. package/dist/build-root.js +1 -1
  4. package/dist/builtin-memory/crouter-concepts/INDEX.md +13 -0
  5. package/dist/builtin-memory/crouter-concepts/README.md +19 -0
  6. package/dist/builtin-memory/crouter-concepts/lifecycle-and-wakes.md +28 -0
  7. package/dist/builtin-memory/crouter-concepts/memory.md +36 -0
  8. package/dist/builtin-memory/crouter-concepts/nodes-and-the-canvas.md +30 -0
  9. package/dist/builtin-memory/crouter-concepts/profiles-kinds-and-modes.md +33 -0
  10. package/dist/builtin-memory/crouter-concepts/scopes-and-trust.md +34 -0
  11. package/dist/builtin-memory/crouter-concepts/why-a-daemon.md +31 -0
  12. package/dist/builtin-memory/crouter-plugin/README.md +10 -13
  13. package/dist/builtin-memory/crouter-plugin/bundles-and-memory.md +2 -4
  14. package/dist/builtin-memory/crouter-plugin/commands.md +4 -7
  15. package/dist/builtin-memory/crouter-plugin/deploying.md +4 -7
  16. package/dist/builtin-memory/crouter-plugin/errors.md +2 -5
  17. package/dist/builtin-memory/crouter-plugin/getting-started.md +3 -6
  18. package/dist/builtin-memory/crouter-plugin/output.md +1 -3
  19. package/dist/builtin-memory/crouter-plugin/parameters.md +3 -4
  20. package/dist/builtin-memory/crouter-sdk/README.md +3 -4
  21. package/dist/builtin-memory/crouter-sdk/bash.md +2 -3
  22. package/dist/builtin-memory/crouter-sdk/client.md +3 -4
  23. package/dist/builtin-memory/crouter-sdk/docker.md +2 -4
  24. package/dist/builtin-memory/crouter-sdk/errors.md +9 -4
  25. package/dist/builtin-memory/crouter-sdk/files.md +2 -3
  26. package/dist/builtin-memory/crouter-sdk/getting-started.md +19 -6
  27. package/dist/builtin-memory/crouter-sdk/memory.md +3 -4
  28. package/dist/builtin-memory/crouter-sdk/migration.md +2 -6
  29. package/dist/builtin-memory/crouter-sdk/nodes.md +3 -3
  30. package/dist/builtin-memory/crouter-sdk/resources.md +2 -4
  31. package/dist/builtin-memory/crouter-sdk/streaming.md +3 -4
  32. package/dist/clients/attach/input/capabilities.js +1 -1
  33. package/dist/clients/attach/overlays/help.js +1 -1
  34. package/dist/clients/attach/slash/dispatch.js +1 -1
  35. package/dist/clients/attach/viewer.js +762 -765
  36. package/dist/commands/sys/branch.js +1 -1
  37. package/dist/commands/sys/connect.js +3 -3
  38. package/dist/commands/sys/setup-core.d.ts +1 -1
  39. package/dist/commands/sys/setup-core.js +4 -4
  40. package/dist/commands/sys/tutorial/branch.d.ts +1 -0
  41. package/dist/commands/sys/tutorial/branch.js +1 -0
  42. package/dist/commands/sys/tutorial/lessons.d.ts +6 -0
  43. package/dist/commands/sys/tutorial/lessons.js +12 -0
  44. package/dist/commands/sys/tutorial/scenario.d.ts +1 -0
  45. package/dist/commands/sys/tutorial/scenario.js +2 -0
  46. package/dist/commands/sys/tutorial/tracks.d.ts +2 -0
  47. package/dist/commands/sys/tutorial/tracks.js +4 -0
  48. package/dist/commands/sys.js +1 -1
  49. package/dist/core/runtime/boot-root.d.ts +7 -0
  50. package/dist/core/runtime/boot-root.js +4 -4
  51. package/dist/core/runtime/first-run-offer.d.ts +11 -0
  52. package/dist/core/runtime/first-run-offer.js +5 -0
  53. package/dist/core/runtime/front-door.js +3 -3
  54. package/dist/core/runtime/spawn.d.ts +3 -1
  55. package/dist/core/runtime/spawn.js +2 -2
  56. package/dist/core/scopes.d.ts +4 -2
  57. package/dist/core/scopes.js +1 -1
  58. package/dist/core/secrets.d.ts +11 -0
  59. package/dist/core/secrets.js +2 -2
  60. package/dist/daemon/api/bridge.d.ts +4 -0
  61. package/dist/daemon/api/bridge.js +2 -2
  62. package/dist/daemon/api/handlers/attach.d.ts +1 -1
  63. package/dist/daemon/api/handlers/attach.js +1 -1
  64. package/dist/daemon/api/handlers/bash.js +1 -1
  65. package/dist/daemon/api/handlers/broker-ops.d.ts +1 -1
  66. package/dist/daemon/api/handlers/broker-ops.js +1 -1
  67. package/dist/daemon/api/handlers/broker-recovery.d.ts +1 -1
  68. package/dist/daemon/api/handlers/broker-recovery.js +1 -1
  69. package/dist/daemon/api/handlers/canvas.js +4 -4
  70. package/dist/daemon/api/handlers/crons.d.ts +1 -1
  71. package/dist/daemon/api/handlers/crons.js +1 -1
  72. package/dist/daemon/api/handlers/daemon.d.ts +1 -1
  73. package/dist/daemon/api/handlers/daemon.js +1 -1
  74. package/dist/daemon/api/handlers/files.js +1 -1
  75. package/dist/daemon/api/handlers/focus.d.ts +1 -1
  76. package/dist/daemon/api/handlers/focus.js +1 -1
  77. package/dist/daemon/api/handlers/human-requests.js +1 -1
  78. package/dist/daemon/api/handlers/memory.js +1 -1
  79. package/dist/daemon/api/handlers/messages.d.ts +1 -1
  80. package/dist/daemon/api/handlers/messages.js +2 -2
  81. package/dist/daemon/api/handlers/model-config.d.ts +1 -1
  82. package/dist/daemon/api/handlers/model-config.js +1 -1
  83. package/dist/daemon/api/handlers/modelauth.js +1 -1
  84. package/dist/daemon/api/handlers/nodes.js +1 -1
  85. package/dist/daemon/api/handlers/profiles.js +1 -1
  86. package/dist/daemon/api/handlers/reports.js +1 -1
  87. package/dist/daemon/api/handlers/reviews.js +1 -1
  88. package/dist/daemon/api/handlers/worktree.d.ts +1 -1
  89. package/dist/daemon/api/handlers/worktree.js +1 -1
  90. package/dist/daemon/api/router.d.ts +20 -1
  91. package/dist/daemon/api/router.js +1 -1
  92. package/dist/daemon/api/server.js +1 -11
  93. package/dist/types.js +1 -1
  94. package/docs/cli/README.md +16 -0
  95. package/docs/cli/canvas-and-dashboard.md +56 -0
  96. package/docs/cli/first-session.md +55 -0
  97. package/docs/cli/human-inbox.md +34 -0
  98. package/docs/cli/memory-and-preferences.md +50 -0
  99. package/docs/cli/the-viewer.md +79 -0
  100. package/docs/concepts/README.md +17 -0
  101. package/docs/concepts/lifecycle-and-wakes.md +26 -0
  102. package/docs/concepts/memory.md +34 -0
  103. package/docs/concepts/nodes-and-the-canvas.md +28 -0
  104. package/docs/concepts/profiles-kinds-and-modes.md +31 -0
  105. package/docs/concepts/scopes-and-trust.md +32 -0
  106. package/docs/concepts/why-a-daemon.md +29 -0
  107. package/docs/plugin/README.md +5 -0
  108. package/docs/plugin/bundles-and-memory.md +5 -0
  109. package/docs/plugin/commands.md +5 -0
  110. package/docs/plugin/deploying.md +5 -0
  111. package/docs/plugin/errors.md +5 -0
  112. package/docs/plugin/getting-started.md +5 -0
  113. package/docs/plugin/output.md +5 -0
  114. package/docs/plugin/parameters.md +5 -0
  115. package/docs/sdk/README.md +5 -0
  116. package/docs/sdk/bash.md +5 -0
  117. package/docs/sdk/client.md +6 -1
  118. package/docs/sdk/docker.md +5 -0
  119. package/docs/sdk/errors.md +12 -0
  120. package/docs/sdk/files.md +5 -0
  121. package/docs/sdk/getting-started.md +21 -1
  122. package/docs/sdk/memory.md +5 -0
  123. package/docs/sdk/migration.md +5 -0
  124. package/docs/sdk/nodes.md +6 -1
  125. package/docs/sdk/resources.md +5 -0
  126. package/docs/sdk/streaming.md +5 -0
  127. package/package.json +10 -9
  128. package/runtime.lock.json +6734 -670
@@ -1 +1 @@
1
- var s=Object.defineProperty;var e=(n,t)=>s(n,"name",{value:t,configurable:!0});import{defineBranch as o}from"../../core/command.js";const r={concept:"crtr configuration, diagnostics, and self-management",desc:"settings, config, connect, setup, doctor, context, sync, migrate, update, version, feedback, logs, support, daemon",useWhen:"managing the crtr installation, inspecting diagnostics, or operating the crtrd supervisor process"},a={name:"sys",summary:"crtr system configuration, settings, diagnostics, and self-management",model:"Configuration and settings own durable behavior; diagnostics and support observe it. Setup, migration, sync, and update change installed or generated state, while daemon control owns the supervisor lifecycle."};function d(n){return o({name:"sys",rootEntry:r,help:{...a},children:n})}e(d,"defineSysBranch");export{d as defineSysBranch};
1
+ var s=Object.defineProperty;var e=(n,t)=>s(n,"name",{value:t,configurable:!0});import{defineBranch as o}from"../../core/command.js";const r={concept:"crtr configuration, diagnostics, and self-management",desc:"settings, config, connect, setup, tutorial, doctor, context, sync, migrate, update, version, feedback, logs, support, daemon",useWhen:"managing the crtr installation, inspecting diagnostics, or operating the crtrd supervisor process"},a={name:"sys",summary:"crtr system configuration, settings, diagnostics, and self-management",model:"Configuration and settings own durable behavior; diagnostics and support observe it. Setup, migration, sync, and update change installed or generated state, while daemon control owns the supervisor lifecycle."};function d(n){return o({name:"sys",rootEntry:r,help:{...a},children:n})}e(d,"defineSysBranch");export{d as defineSysBranch};
@@ -1,3 +1,3 @@
1
- var v=Object.defineProperty;var s=(e,t)=>v(e,"name",{value:t,configurable:!0});import{randomBytes as k}from"node:crypto";import C from"node:path";import{defineLeaf as _}from"../../core/command.js";import{readConfig as h,updateConfig as m}from"../../core/config.js";import{emit as P,InputError as u,isJsonOutput as $}from"../../core/io.js";import{renderResult as R}from"../../core/render.js";import{getApiToken as g,setApiToken as x}from"../../core/secrets.js";import{envCrtrdTcp as L,envCrtrdToken as w,envNodeId as I}from"../../shared/env.js";import{shellQuote as d}from"../../shared/shell-quote.js";import{cliClient as y}from"../api-client.js";const A="127.0.0.1:8787",b=3e4,E=100;function D(){return k(32).toString("hex")}s(D,"token");function N(e){$()?P(e):process.stdout.write(R(e,O.help)+`
2
- `)}s(N,"printCredentials");function S(e){const t=e.lastIndexOf(":");return t===-1||Number(e.slice(t+1))!==0?e:`${e.slice(0,t+1)}8787`}s(S,"fixedTcpPort");async function q(){const e=y(),t=Date.now()+b;let n;for(;Date.now()<t;){try{const r=await e.status();if(r.listeners.tcp!==null)return r}catch(r){n=r}await new Promise(r=>setTimeout(r,E))}throw new u({error:"listener_unavailable",message:`the successor did not expose a TCP listener within ${b}ms${n instanceof Error?`: ${n.message}`:""}`,next:"Run `crtr sys daemon status` to inspect the daemon, then retry `crtr sys connect`."})}s(q,"awaitTcpListener");async function T(e,t){const n=await e.getModelAuthReadiness(t);if(n.credential==="ready")return;process.stderr.write(`Logging in to ${n.provider} for ${n.model}.
3
- `);const{loginProvider:r,openProviderLoginRuntime:o,terminalAuthInteraction:a}=await import("./provider-login.js");await r(await o(),n.provider,a());const i=await e.getModelAuthReadiness(t);if(i.credential!=="ready")throw new u({error:"provider_login_incomplete",message:`provider ${i.provider} credential is ${i.credential} after login`,next:`Run \`${M(t)}\` again and complete the provider login.`})}s(T,"ensureProviderCredential");function M(e){return["crtr sys connect",e.profile===void 0?void 0:`--profile ${d(e.profile)}`,e.cwd===void 0?void 0:`--cwd ${d(e.cwd)}`,e.kind===void 0?void 0:`--kind ${d(e.kind)}`,e.model===void 0?void 0:`--model ${d(e.model)}`].filter(t=>t!==void 0).join(" ")}s(M,"connectCommand");const O=_({name:"connect",description:"enable browser access to this daemon",whenToUse:"an application needs a browser-reachable daemon URL and bearer token; this enables the local TCP listener and reports the credentials to paste into that application.",help:{name:"sys connect",summary:"enable authenticated browser access and log in to the selected model provider when needed; a handover reports browser credentials before it begins",params:[{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile setting selecting the provider a root launch with these settings would use."},{kind:"flag",name:"cwd",type:"string",required:!1,constraint:"Directory setting selecting the provider a root launch with these settings would use. Default: this directory."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Node kind setting selecting the provider a root launch with these settings would use."},{kind:"flag",name:"model",type:"string",required:!1,constraint:"Model setting selecting the provider a root launch with these settings would use."}],output:[{name:"base_url",type:"string",required:!0,constraint:"The HTTP URL configured for the TCP listener."},{name:"token",type:"string",required:!0,constraint:"The bearer token accepted by that listener; paste it only into the application that needs daemon access."}],outputKind:"object",effects:["Stores a missing listener address in user settings and a missing bearer token in the 0600 user secrets store.","When the listener is not already up with both settings present, asks the daemon to hand over to a successor so it starts the listener.","After the listener is available, resolves the provider selected by these params (default: this directory) and runs its existing OAuth login flow when that provider has no usable credential."]},run:s(async e=>{const t={profile:e.profile,cwd:C.resolve(e.cwd??process.cwd()),kind:e.kind,model:e.model},n=I(),o=h("user").api.tcp,a=g("user"),i=y(),p=await i.status();o===""?m("user",l=>{l.api.tcp=A}):Number(o.slice(o.lastIndexOf(":")+1))===0&&m("user",l=>{l.api.tcp=S(o)});const c=L()??h("user").api.tcp;if(Number(c.slice(c.lastIndexOf(":")+1))===0)throw new u({error:"dynamic_tcp_port",message:"CRTRD_TCP must use a fixed port for `crtr sys connect`",next:"Set CRTRD_TCP to a host and fixed port, then run `crtr sys connect` again."});if(o!==""&&a!==void 0&&p.listeners.tcp!==null)return await T(i,t),{base_url:`http://${p.listeners.tcp}`,token:w()??a};a===void 0&&x(D(),"user");const f=w()??g("user");if(f===void 0)throw new Error("TCP listener cannot start without a bearer token");N({base_url:`http://${c}`,token:f}),await i.restartDaemon(),await q(),await T(i,t),n!==void 0&&await i.sendMessage(n,{body:"Browser access is ready.",tier:"urgent",from:n})},"run")});export{O as sysConnectLeaf};
1
+ var _=Object.defineProperty;var o=(e,t)=>_(e,"name",{value:t,configurable:!0});import{randomBytes as $}from"node:crypto";import x from"node:path";import{defineLeaf as R}from"../../core/command.js";import{readConfig as w,updateConfig as g}from"../../core/config.js";import{emit as S,InputError as d,isJsonOutput as A}from"../../core/io.js";import{renderResult as L}from"../../core/render.js";import{addScopedApiToken as E,getApiToken as y,setApiToken as I}from"../../core/secrets.js";import{SCOPE_VOCABULARY as k,validateScopes as O}from"../../core/scopes.js";import{envCrtrdTcp as D,envCrtrdToken as b,envNodeId as N}from"../../shared/env.js";import{shellQuote as c}from"../../shared/shell-quote.js";import{cliClient as v}from"../api-client.js";const q="127.0.0.1:8787",T=3e4,M=100;function C(){return $(32).toString("hex")}o(C,"token");function U(e){if(e===void 0)return;const t=String(e).split(",").map(n=>n.trim()).filter(n=>n!=="");if(t.length===0)throw new d({error:"empty_scopes",message:"--scopes names no scope",received:e,field:"scopes",next:`Pass a comma-separated list from ${k.join(", ")}, or prefix:operand; omit the flag for the owner token.`});return O(t)}o(U,"parseScopes");function j(e){A()?S(e):process.stdout.write(L(e,H.help)+`
2
+ `)}o(j,"printCredentials");function W(e){const t=e.lastIndexOf(":");return t===-1||Number(e.slice(t+1))!==0?e:`${e.slice(0,t+1)}8787`}o(W,"fixedTcpPort");async function B(){const e=v(),t=Date.now()+T;let n;for(;Date.now()<t;){try{const r=await e.status();if(r.listeners.tcp!==null)return r}catch(r){n=r}await new Promise(r=>setTimeout(r,M))}throw new d({error:"listener_unavailable",message:`the successor did not expose a TCP listener within ${T}ms${n instanceof Error?`: ${n.message}`:""}`,next:"Run `crtr sys daemon status` to inspect the daemon, then retry `crtr sys connect`."})}o(B,"awaitTcpListener");async function P(e,t){const n=await e.getModelAuthReadiness(t);if(n.credential==="ready")return;process.stderr.write(`Logging in to ${n.provider} for ${n.model}.
3
+ `);const{loginProvider:r,openProviderLoginRuntime:f,terminalAuthInteraction:i}=await import("./provider-login.js");await r(await f(),n.provider,i());const s=await e.getModelAuthReadiness(t);if(s.credential!=="ready")throw new d({error:"provider_login_incomplete",message:`provider ${s.provider} credential is ${s.credential} after login`,next:`Run \`${F(t)}\` again and complete the provider login.`})}o(P,"ensureProviderCredential");function F(e){return["crtr sys connect",e.profile===void 0?void 0:`--profile ${c(e.profile)}`,e.cwd===void 0?void 0:`--cwd ${c(e.cwd)}`,e.kind===void 0?void 0:`--kind ${c(e.kind)}`,e.model===void 0?void 0:`--model ${c(e.model)}`].filter(t=>t!==void 0).join(" ")}o(F,"connectCommand");const H=R({name:"connect",description:"enable browser access to this daemon",whenToUse:"an application needs a browser-reachable daemon URL and bearer token; this enables the local TCP listener and reports the credentials to paste into that application. Add --scopes when that application should hold less than the owner.",help:{name:"sys connect",summary:"enable authenticated browser access and log in to the selected model provider when needed; a handover reports browser credentials before it begins",params:[{kind:"flag",name:"scopes",type:"string",required:!1,constraint:`Comma-separated list from ${k.join(", ")}, or prefix:operand. Mints a NEW bearer token whose list is the ceiling for every scope-gated daemon operation it performs and every node it creates; owner-only operations (daemon restart, attach, broker internals) refuse it. Omit for the owner token, which has no ceiling.`},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile setting selecting the provider a root launch with these settings would use."},{kind:"flag",name:"cwd",type:"string",required:!1,constraint:"Directory setting selecting the provider a root launch with these settings would use. Default: this directory."},{kind:"flag",name:"kind",type:"string",required:!1,constraint:"Node kind setting selecting the provider a root launch with these settings would use."},{kind:"flag",name:"model",type:"string",required:!1,constraint:"Model setting selecting the provider a root launch with these settings would use."}],output:[{name:"base_url",type:"string",required:!0,constraint:"The HTTP URL configured for the TCP listener."},{name:"token",type:"string",required:!0,constraint:"The bearer token accepted by that listener: the owner token, or with --scopes the newly minted scoped token. Paste it only into the application that needs daemon access."}],outputKind:"object",effects:["Stores a missing listener address in user settings and a missing owner bearer token in the 0600 user secrets store.","With --scopes, appends a new scoped token to that secrets store; there is no revoke verb yet, so removing one means editing the store by hand.","When the listener is not already up with both settings present, or a scoped token was minted (the daemon reads tokens only at boot), asks the daemon to hand over to a successor.","After the listener is available, resolves the provider selected by these params (default: this directory) and runs its existing OAuth login flow when that provider has no usable credential."]},run:o(async e=>{const t={profile:e.profile,cwd:x.resolve(e.cwd??process.cwd()),kind:e.kind,model:e.model},n=U(e.scopes),r=N(),i=w("user").api.tcp,s=y("user"),a=v(),h=await a.status();i===""?g("user",u=>{u.api.tcp=q}):Number(i.slice(i.lastIndexOf(":")+1))===0&&g("user",u=>{u.api.tcp=W(i)});const l=D()??w("user").api.tcp;if(Number(l.slice(l.lastIndexOf(":")+1))===0)throw new d({error:"dynamic_tcp_port",message:"CRTRD_TCP must use a fixed port for `crtr sys connect`",next:"Set CRTRD_TCP to a host and fixed port, then run `crtr sys connect` again."});if(n===void 0&&i!==""&&s!==void 0&&h.listeners.tcp!==null)return await P(a,t),{base_url:`http://${h.listeners.tcp}`,token:b()??s};s===void 0&&I(C(),"user");const m=b()??y("user");if(m===void 0)throw new Error("TCP listener cannot start without a bearer token");let p=m;n!==void 0&&(p=C(),E({token:p,scopes:n,created:new Date().toISOString()},"user")),j({base_url:`http://${l}`,token:p}),await a.restartDaemon(),await B(),await P(a,t),r!==void 0&&await a.sendMessage(r,{body:"Browser access is ready.",tier:"urgent",from:r})},"run")});export{H as sysConnectLeaf};
@@ -2,7 +2,7 @@ import { type BindingId, type BindingResolution, type KeybindingDiagnostic, type
2
2
  export declare const OFFICIAL_MARKETPLACE_NAME = "crouter-official-marketplace";
3
3
  export declare const OFFICIAL_MARKETPLACE_URL = "https://github.com/crouton-labs/crouter-official-marketplace.git";
4
4
  export declare const OFFICIAL_MARKETPLACE_REF = "main";
5
- export type SetupPackageId = 'official-marketplace' | 'crtr-extensions' | 'oauth-adapter' | 'capture-plugin' | 'search-plugin';
5
+ export type SetupPackageId = 'official-marketplace' | 'crtr-extensions' | 'oauth-adapter' | 'capture-plugin' | 'search-plugin' | 'dev-plugin';
6
6
  /** `nerd-font` is not a command on PATH like the other two — it is an installed
7
7
  * font face, detected by fontconfig/font-directory scan. The viewer renders
8
8
  * every tool and command icon with Nerd Font codepoints, so without it a fresh
@@ -1,9 +1,9 @@
1
- var W=Object.defineProperty;var a=(e,t)=>W(e,"name",{value:t,configurable:!0});import{spawn as q,spawnSync as H}from"node:child_process";import{homedir as g}from"node:os";import{basename as V,dirname as J,join as S,resolve as X}from"node:path";import{isDeepStrictEqual as k}from"node:util";import{BINDING_CATALOG as Q,normalizeGesture as Z,splitGestureExpression as ee,inboxShortcut as te,resolveKeybindings as P}from"../../core/keybindings/index.js";import{builtinPiPackageDir as b}from"../../core/scope.js";import{readJsonIfExists as ne,readJsonOrNull as re}from"../../core/fs-utils.js";import{resolveBundledPiCliPath as ie}from"../../core/runtime/pi-cli.js";import{crtrVersion as se}from"../../shared/crtr-version.js";import{installTmuxBindings as ae}from"../../core/runtime/tmux-chrome.js";import{normalizeTmuxPassthrough as oe}from"../../core/config.js";import{detectNerdFont as ce,nerdFontInstallCommand as ue,createCurlPercentReader as le,NERD_FONT_MANUAL_LINUX_COMMAND as de,NERD_FONT_SELECT_HINT as pe}from"../../core/runtime/nerd-font.js";import{startProgressBar as he}from"../../core/runtime/progress-bar.js";import{ensureRenderer as fe,isRendererReady as A}from"../../core/termrender/termrender.js";import{installFromMarketplace as me}from"../pkg/plugin-manage.js";import{addMarketplace as $}from"../pkg/market-manage.js";import{findPluginByName as ge,findMarketplaceByName as E}from"../../core/resolver.js";import{expandTilde as ke}from"../../core/fs-utils.js";const R="crouter-official-marketplace",D="https://github.com/crouton-labs/crouter-official-marketplace.git",O="main";function T(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}a(T,"isKeybindingsObject");function h(e){return T(e)?Object.fromEntries(Object.entries(e).map(([t,n])=>[t,Array.isArray(n)?[...n]:n])):{}}a(h,"cloneKeybindings");class Je{static{a(this,"SetupKeybindingsDraft")}expected;baseline;current;baselineInvalidRoot;invalidRoot;resolutionCache;expectedPassthrough;baselinePassthrough;currentPassthrough;constructor(t,n=void 0){this.expected=t,this.baseline=h(t),this.current=h(t),this.baselineInvalidRoot=t!==void 0&&!T(t)?t:void 0,this.invalidRoot=this.baselineInvalidRoot,this.expectedPassthrough=n,this.baselinePassthrough=oe(n),this.currentPassthrough=[...this.baselinePassthrough]}get dirty(){return!k(this.current,this.baseline)||!k(this.invalidRoot,this.baselineInvalidRoot)||!k(this.currentPassthrough,this.baselinePassthrough)}overrides(){return h(this.current)}expectedKeybindings(){return this.expected}tmuxPassthrough(){return[...this.currentPassthrough]}expectedTmuxPassthrough(){return this.expectedPassthrough}usesTmuxPassthrough(t){return this.currentPassthrough.includes(t)}toggleTmuxPassthrough(t){this.currentPassthrough=this.currentPassthrough.includes(t)?this.currentPassthrough.filter(n=>n!==t):[...this.currentPassthrough,t]}resolution(){return this.resolutionCache===void 0&&(this.resolutionCache=P(this.invalidRoot===void 0?this.current:this.invalidRoot)),this.resolutionCache}hasInvalidRoot(){return this.invalidRoot!==void 0}expression(t){return this.resolution().gestures(t).join(", ")}edit(t,n){if(this.invalidRoot!==void 0)return{ok:!1,diagnostics:[{severity:"error",code:"invalid-keybindings",offendingValue:this.invalidRoot,message:"Reset all before editing: the saved keybindings root is invalid."}]};const r=Q.find(p=>p.id===t);let o;try{o=n.trim()===""?[]:ee(n).map(p=>Z(p.trim(),r))}catch(p){return{ok:!1,diagnostics:[{severity:"error",code:"invalid-override",bindingId:t,offendingValue:n,message:p instanceof Error?p.message:String(p)}]}}const i=[...new Set(o)],u={...this.current};delete u[t];const c=[...P(u).gestures(t)],l=k(i,c)?u:{...this.current,[t]:i},s=P(l),d=s.diagnostics.filter(p=>p.bindingId===t||p.relatedBindingIds?.includes(t));return d.length>0?{ok:!1,diagnostics:d}:(this.current=l,this.resolutionCache=void 0,{ok:!0,diagnostics:s.diagnostics})}unbind(t){return this.edit(t,"")}resetRow(t){delete this.current[t],this.resolutionCache=void 0}resetAll(){this.current={},this.currentPassthrough=[],this.invalidRoot=void 0,this.resolutionCache=void 0}discard(){this.current=h(this.baseline),this.currentPassthrough=[...this.baselinePassthrough],this.invalidRoot=this.baselineInvalidRoot,this.resolutionCache=void 0}markSaved(t,n=[]){this.expected=Object.keys(t).length===0?void 0:h(t),this.baseline=h(t),this.current=h(t),this.expectedPassthrough=n.length===0?void 0:[...n],this.baselinePassthrough=[...n],this.currentPassthrough=[...n],this.baselineInvalidRoot=void 0,this.invalidRoot=void 0,this.resolutionCache=void 0}}const be={"official-marketplace":{name:R,shortDescription:"the official crtr plugin catalog \u2014 registered as a source so its plugins are installable by ref.",longDescription:"Shallow-clones github.com/crouton-labs/crouter-official-marketplace into the user scope and registers it, which is what makes `crtr pkg plugin install crouter-official-marketplace/<name>` resolve and what fills `crtr pkg market browse`. Registering the source installs no plugin by itself. Uncheck it on a machine that should reach no external catalog \u2014 a plugin selected below still registers the marketplace it comes from, because its ref cannot resolve otherwise."},"crtr-extensions":{name:"pi-crtr-extensions",shortDescription:"crtr companion extensions \u2014 command palette, help gating, subscription/OAuth rotation, nested context.",longDescription:"Bundles the extensions built for crtr: a crtr slash-command palette, crtr/crouter help gating, surfacing of Claude Code marketplace plugin commands, nested AGENTS.md/CLAUDE.md loading, OAuth login + rotation across multiple Claude subscriptions (provider-rotation), and system-prompt trimming. The OAuth/rotation piece is what lets crtr authenticate your Claude account."},"oauth-adapter":{name:"pi-claude-oauth-adapter",shortDescription:"Anthropic OAuth / Claude Code credential compatibility for pi.",longDescription:"Third-party adapter (minzique/dotfiles-agents) letting pi authenticate with Claude using Claude Code OAuth credentials. Installed from npm, pinned to 0.1.2."},"capture-plugin":{name:"capture",shortDescription:"crtr plugin mounting `crtr capture` \u2014 browser automation and UI measurement over CDP.",longDescription:"Installs the `capture` plugin from the official crtr marketplace. It mounts `crtr capture` as a transparent passthrough to the external Capture CLI (`npm install -g @crouton-kit/capture`) for CDP screenshots, rendered-geometry measurement, HAR, accessibility trees, and JS execution against a live page. Uncheck it on a machine where agents never drive a browser \u2014 the command then costs nothing in their root prompt."},"search-plugin":{name:"search",shortDescription:"crtr plugin mounting `crtr search` \u2014 public-web discovery, grounded answers, and content extraction.",longDescription:"Installs the `search` plugin from the official crtr marketplace. It mounts `crtr search` and calls Exa directly, resolving its own API key from EXA_API_KEY or ~/.crouter/exa.key \u2014 crouter never handles that credential. Uncheck it on a machine whose agents get web search from somewhere else, such as a Northlight home where the platform serves `crtr search` from its own plugin."}},v={"official-marketplace":{kind:"marketplace-source",source:D},"crtr-extensions":{kind:"local-path",source:b("pi-crtr-extensions")},"oauth-adapter":{kind:"npm",source:"npm:pi-claude-oauth-adapter@0.1.2"},"capture-plugin":{kind:"marketplace-plugin",source:"crouter-official-marketplace/capture"},"search-plugin":{kind:"marketplace-plugin",source:"crouter-official-marketplace/search"}},we={"official-marketplace":[],"crtr-extensions":["pi-crtr-extensions"],"oauth-adapter":[],"capture-plugin":[],"search-plugin":[]};function I(e=b){return["official-marketplace","crtr-extensions","oauth-adapter","capture-plugin","search-plugin"].map(t=>{const n=be[t],r=v[t].kind==="local-path"?e(n.name):v[t].source;return{id:t,name:n.name,sourceKind:v[t].kind,installSource:r,shortDescription:n.shortDescription,longDescription:n.longDescription}})}a(I,"buildSetupManifest");function Xe(e){switch(e.sourceKind){case"marketplace-source":return`clone + register ${e.installSource}`;case"marketplace-plugin":return`crtr pkg plugin install ${e.installSource}`;default:return`pi install ${e.installSource}`}}a(Xe,"packagePlanAction");function Pe(e){if(!e.startsWith("npm:"))return null;const t=e.slice(4),n=t.startsWith("@")?t.indexOf("@",1):t.lastIndexOf("@");return n>0?t.slice(0,n):t}a(Pe,"npmPackageName");function N(e,t,n){const r=Pe(e);if(r!==null)return`npm:${r}`;const o=ke(e,n);return X(t,o)}a(N,"normalizePackageIdentity");function x(e){return`package:${e}`}a(x,"packageNameIdentity");function ve(e){return e.startsWith("npm:")?[]:[x(V(e))]}a(ve,"localPackageNameIdentities");function Ie(e,t,n){const r=[N(e.installSource,t,n)];if(e.sourceKind==="local-path"){r.push(x(e.name));for(const o of we[e.id])r.push(x(o))}return r}a(Ie,"manifestIdentities");function L(e,t=I(),n=S(g(),".pi","agent","settings.json"),r=g()){const o=J(n),i=new Set,u=new Map;for(const l of t)for(const s of Ie(l,o,r))u.set(s,l.id);const c=e&&typeof e=="object"?e.packages:void 0;if(!Array.isArray(c))return i;for(const l of c){const s=typeof l=="string"?l:l&&typeof l=="object"&&typeof l.source=="string"?l.source:null;if(s===null)continue;const d=[N(s,o,r),...ve(s)];for(const p of d){const f=u.get(p);f!==void 0&&i.add(f)}}return i}a(L,"detectInstalledSetupPackageIds");function m(e){return H("sh",["-lc",`command -v ${e} >/dev/null 2>&1`],{stdio:"ignore"}).status===0}a(m,"detectCommandOnPath");function xe(e=process.platform,t=m){switch(e){case"darwin":return t("brew")?"brew":"manual";case"linux":return t("apt-get")?"apt-get":t("dnf")?"dnf":t("pacman")?"pacman":"manual";default:return"manual"}}a(xe,"detectPackageManager");function ye(e,t,n=m){if(e==="nerd-font"?ce().installed:n(e))return{tool:e,installed:!0,command:null,hint:"already installed",autoInstall:!1};if(e==="nerd-font"){const i=ue(t);return{tool:e,installed:!1,command:i.command,hint:`${i.hint} \u2014 ${pe}`,autoInstall:i.autoInstall}}const o=e==="nvim"?"neovim":"tmux";switch(t){case"brew":return{tool:e,installed:!1,command:`brew install ${o}`,hint:"Homebrew install",autoInstall:!0};case"apt-get":return{tool:e,installed:!1,command:`sudo apt-get update && sudo apt-get install -y ${o}`,hint:"apt-get install",autoInstall:!0};case"dnf":return{tool:e,installed:!1,command:`sudo dnf install -y ${o}`,hint:"dnf install",autoInstall:!0};case"pacman":return{tool:e,installed:!1,command:`sudo pacman -S --noconfirm ${o}`,hint:"pacman install",autoInstall:!0};default:return{tool:e,installed:!1,command:null,hint:`manual install required for ${e}`,autoInstall:!1}}}a(ye,"buildSystemInstallPlan");function _(e=g()){return S(e,".pi","agent","settings.json")}a(_,"resolveSettingsPath");function Qe(e={}){const t=e.homeDir??g(),n=e.settingsPath??_(t),r=e.manifest??I(b),o=ne(n),i=L(o,r,n,t);for(const s of G(r))i.add(s);const u=e.commandExists??m,c=e.packageManager??xe(process.platform,u),l=["tmux","nvim","nerd-font"].map(s=>ye(s,c,u));return{manifest:r,installedPackageIds:i,systemPlans:l}}a(Qe,"buildSetupInitialState");function Ze(e){const t=e.manifest.map(r=>{const o=e.installedPackageIds.has(r.id)?"installed":"not installed";return`- ${r.name} \u2014 ${r.shortDescription} (${o})`}),n=e.systemPlans.map(r=>r.installed?`- ${r.tool}: \u2713 installed`:r.command?`- ${r.tool}: ${r.command}`:`- ${r.tool}: ${r.hint}`);return["crtr sys setup (non-interactive)","","Provider login: open the wizard in a TTY and use the Providers tab (same flow as /login) to authenticate a model provider.","","Packages:",...t,"","System deps:",...n,"",`Inbox: the inbox-toggle key (${te()??"disabled"}) is a crouter keybinding \u2014 view or rebind it in \`crtr sys settings\`; it is installed on the same tmux server as every other crouter binding.`,"Run `crtr sys setup` in a TTY to open the wizard."].join(`
1
+ var q=Object.defineProperty;var a=(e,t)=>q(e,"name",{value:t,configurable:!0});import{spawn as W,spawnSync as H}from"node:child_process";import{homedir as g}from"node:os";import{basename as V,dirname as J,join as S,resolve as X}from"node:path";import{isDeepStrictEqual as k}from"node:util";import{BINDING_CATALOG as Q,normalizeGesture as Z,splitGestureExpression as ee,inboxShortcut as te,resolveKeybindings as w}from"../../core/keybindings/index.js";import{builtinPiPackageDir as b}from"../../core/scope.js";import{readJsonIfExists as ne,readJsonOrNull as re}from"../../core/fs-utils.js";import{resolveBundledPiCliPath as ie}from"../../core/runtime/pi-cli.js";import{crtrVersion as se}from"../../shared/crtr-version.js";import{installTmuxBindings as ae}from"../../core/runtime/tmux-chrome.js";import{normalizeTmuxPassthrough as oe}from"../../core/config.js";import{detectNerdFont as ce,nerdFontInstallCommand as ue,createCurlPercentReader as le,NERD_FONT_MANUAL_LINUX_COMMAND as de,NERD_FONT_SELECT_HINT as pe}from"../../core/runtime/nerd-font.js";import{startProgressBar as he}from"../../core/runtime/progress-bar.js";import{ensureRenderer as fe,isRendererReady as A}from"../../core/termrender/termrender.js";import{installFromMarketplace as me}from"../pkg/plugin-manage.js";import{addMarketplace as $}from"../pkg/market-manage.js";import{findPluginByName as ge,findMarketplaceByName as E}from"../../core/resolver.js";import{expandTilde as ke}from"../../core/fs-utils.js";const R="crouter-official-marketplace",D="https://github.com/crouton-labs/crouter-official-marketplace.git",O="main";function T(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}a(T,"isKeybindingsObject");function h(e){return T(e)?Object.fromEntries(Object.entries(e).map(([t,n])=>[t,Array.isArray(n)?[...n]:n])):{}}a(h,"cloneKeybindings");class Je{static{a(this,"SetupKeybindingsDraft")}expected;baseline;current;baselineInvalidRoot;invalidRoot;resolutionCache;expectedPassthrough;baselinePassthrough;currentPassthrough;constructor(t,n=void 0){this.expected=t,this.baseline=h(t),this.current=h(t),this.baselineInvalidRoot=t!==void 0&&!T(t)?t:void 0,this.invalidRoot=this.baselineInvalidRoot,this.expectedPassthrough=n,this.baselinePassthrough=oe(n),this.currentPassthrough=[...this.baselinePassthrough]}get dirty(){return!k(this.current,this.baseline)||!k(this.invalidRoot,this.baselineInvalidRoot)||!k(this.currentPassthrough,this.baselinePassthrough)}overrides(){return h(this.current)}expectedKeybindings(){return this.expected}tmuxPassthrough(){return[...this.currentPassthrough]}expectedTmuxPassthrough(){return this.expectedPassthrough}usesTmuxPassthrough(t){return this.currentPassthrough.includes(t)}toggleTmuxPassthrough(t){this.currentPassthrough=this.currentPassthrough.includes(t)?this.currentPassthrough.filter(n=>n!==t):[...this.currentPassthrough,t]}resolution(){return this.resolutionCache===void 0&&(this.resolutionCache=w(this.invalidRoot===void 0?this.current:this.invalidRoot)),this.resolutionCache}hasInvalidRoot(){return this.invalidRoot!==void 0}expression(t){return this.resolution().gestures(t).join(", ")}edit(t,n){if(this.invalidRoot!==void 0)return{ok:!1,diagnostics:[{severity:"error",code:"invalid-keybindings",offendingValue:this.invalidRoot,message:"Reset all before editing: the saved keybindings root is invalid."}]};const r=Q.find(p=>p.id===t);let o;try{o=n.trim()===""?[]:ee(n).map(p=>Z(p.trim(),r))}catch(p){return{ok:!1,diagnostics:[{severity:"error",code:"invalid-override",bindingId:t,offendingValue:n,message:p instanceof Error?p.message:String(p)}]}}const i=[...new Set(o)],u={...this.current};delete u[t];const c=[...w(u).gestures(t)],l=k(i,c)?u:{...this.current,[t]:i},s=w(l),d=s.diagnostics.filter(p=>p.bindingId===t||p.relatedBindingIds?.includes(t));return d.length>0?{ok:!1,diagnostics:d}:(this.current=l,this.resolutionCache=void 0,{ok:!0,diagnostics:s.diagnostics})}unbind(t){return this.edit(t,"")}resetRow(t){delete this.current[t],this.resolutionCache=void 0}resetAll(){this.current={},this.currentPassthrough=[],this.invalidRoot=void 0,this.resolutionCache=void 0}discard(){this.current=h(this.baseline),this.currentPassthrough=[...this.baselinePassthrough],this.invalidRoot=this.baselineInvalidRoot,this.resolutionCache=void 0}markSaved(t,n=[]){this.expected=Object.keys(t).length===0?void 0:h(t),this.baseline=h(t),this.current=h(t),this.expectedPassthrough=n.length===0?void 0:[...n],this.baselinePassthrough=[...n],this.currentPassthrough=[...n],this.baselineInvalidRoot=void 0,this.invalidRoot=void 0,this.resolutionCache=void 0}}const be={"official-marketplace":{name:R,shortDescription:"the official crtr plugin catalog \u2014 registered as a source so its plugins are installable by ref.",longDescription:"Shallow-clones github.com/crouton-labs/crouter-official-marketplace into the user scope and registers it, which is what makes `crtr pkg plugin install crouter-official-marketplace/<name>` resolve and what fills `crtr pkg market browse`. Registering the source installs no plugin by itself. Uncheck it on a machine that should reach no external catalog \u2014 a plugin selected below still registers the marketplace it comes from, because its ref cannot resolve otherwise."},"crtr-extensions":{name:"pi-crtr-extensions",shortDescription:"crtr companion extensions \u2014 command palette, help gating, subscription/OAuth rotation, nested context.",longDescription:"Bundles the extensions built for crtr: a crtr slash-command palette, crtr/crouter help gating, surfacing of Claude Code marketplace plugin commands, nested AGENTS.md/CLAUDE.md loading, OAuth login + rotation across multiple Claude subscriptions (provider-rotation), and system-prompt trimming. The OAuth/rotation piece is what lets crtr authenticate your Claude account."},"oauth-adapter":{name:"pi-claude-oauth-adapter",shortDescription:"Anthropic OAuth / Claude Code credential compatibility for pi.",longDescription:"Third-party adapter (minzique/dotfiles-agents) letting pi authenticate with Claude using Claude Code OAuth credentials. Installed from npm, pinned to 0.1.2."},"capture-plugin":{name:"capture",shortDescription:"crtr plugin mounting `crtr capture` \u2014 browser automation and UI measurement over CDP.",longDescription:"Installs the `capture` plugin from the official crtr marketplace. It mounts `crtr capture` as a transparent passthrough to the external Capture CLI (`npm install -g @crouton-kit/capture`) for CDP screenshots, rendered-geometry measurement, HAR, accessibility trees, and JS execution against a live page. Uncheck it on a machine where agents never drive a browser \u2014 the command then costs nothing in their root prompt."},"search-plugin":{name:"search",shortDescription:"crtr plugin mounting `crtr search` \u2014 public-web discovery, grounded answers, and content extraction.",longDescription:"Installs the `search` plugin from the official crtr marketplace. It mounts `crtr search` and calls Exa directly, resolving its own API key from EXA_API_KEY or ~/.crouter/exa.key \u2014 crouter never handles that credential. Uncheck it on a machine whose agents get web search from somewhere else, such as a Northlight home where the platform serves `crtr search` from its own plugin."},"dev-plugin":{name:"dev",shortDescription:"crtr plugin mounting the development workflow \u2014 repository setup, worktrees, pull requests, and a dev track in the tutorial.",longDescription:"Installs the dev plugin from the official crtr marketplace. It adds the /dev: workflow commands (init, spec, design, plan, pr-loop), mounts crtr dev for a repository's own lifecycle commands and Grove instances, and contributes the crtr sys tutorial dev track. Uncheck it on a machine whose agents do not work in source repositories."}},y={"official-marketplace":{kind:"marketplace-source",source:D},"crtr-extensions":{kind:"local-path",source:b("pi-crtr-extensions")},"oauth-adapter":{kind:"npm",source:"npm:pi-claude-oauth-adapter@0.1.2"},"capture-plugin":{kind:"marketplace-plugin",source:"crouter-official-marketplace/capture"},"search-plugin":{kind:"marketplace-plugin",source:"crouter-official-marketplace/search"},"dev-plugin":{kind:"marketplace-plugin",source:"crouter-official-marketplace/dev"}},ve={"official-marketplace":[],"crtr-extensions":["pi-crtr-extensions"],"oauth-adapter":[],"capture-plugin":[],"search-plugin":[],"dev-plugin":[]};function P(e=b){return["official-marketplace","crtr-extensions","oauth-adapter","capture-plugin","search-plugin","dev-plugin"].map(t=>{const n=be[t],r=y[t].kind==="local-path"?e(n.name):y[t].source;return{id:t,name:n.name,sourceKind:y[t].kind,installSource:r,shortDescription:n.shortDescription,longDescription:n.longDescription}})}a(P,"buildSetupManifest");function Xe(e){switch(e.sourceKind){case"marketplace-source":return`clone + register ${e.installSource}`;case"marketplace-plugin":return`crtr pkg plugin install ${e.installSource}`;default:return`pi install ${e.installSource}`}}a(Xe,"packagePlanAction");function we(e){if(!e.startsWith("npm:"))return null;const t=e.slice(4),n=t.startsWith("@")?t.indexOf("@",1):t.lastIndexOf("@");return n>0?t.slice(0,n):t}a(we,"npmPackageName");function N(e,t,n){const r=we(e);if(r!==null)return`npm:${r}`;const o=ke(e,n);return X(t,o)}a(N,"normalizePackageIdentity");function I(e){return`package:${e}`}a(I,"packageNameIdentity");function ye(e){return e.startsWith("npm:")?[]:[I(V(e))]}a(ye,"localPackageNameIdentities");function Pe(e,t,n){const r=[N(e.installSource,t,n)];if(e.sourceKind==="local-path"){r.push(I(e.name));for(const o of ve[e.id])r.push(I(o))}return r}a(Pe,"manifestIdentities");function L(e,t=P(),n=S(g(),".pi","agent","settings.json"),r=g()){const o=J(n),i=new Set,u=new Map;for(const l of t)for(const s of Pe(l,o,r))u.set(s,l.id);const c=e&&typeof e=="object"?e.packages:void 0;if(!Array.isArray(c))return i;for(const l of c){const s=typeof l=="string"?l:l&&typeof l=="object"&&typeof l.source=="string"?l.source:null;if(s===null)continue;const d=[N(s,o,r),...ye(s)];for(const p of d){const f=u.get(p);f!==void 0&&i.add(f)}}return i}a(L,"detectInstalledSetupPackageIds");function m(e){return H("sh",["-lc",`command -v ${e} >/dev/null 2>&1`],{stdio:"ignore"}).status===0}a(m,"detectCommandOnPath");function Ie(e=process.platform,t=m){switch(e){case"darwin":return t("brew")?"brew":"manual";case"linux":return t("apt-get")?"apt-get":t("dnf")?"dnf":t("pacman")?"pacman":"manual";default:return"manual"}}a(Ie,"detectPackageManager");function xe(e,t,n=m){if(e==="nerd-font"?ce().installed:n(e))return{tool:e,installed:!0,command:null,hint:"already installed",autoInstall:!1};if(e==="nerd-font"){const i=ue(t);return{tool:e,installed:!1,command:i.command,hint:`${i.hint} \u2014 ${pe}`,autoInstall:i.autoInstall}}const o=e==="nvim"?"neovim":"tmux";switch(t){case"brew":return{tool:e,installed:!1,command:`brew install ${o}`,hint:"Homebrew install",autoInstall:!0};case"apt-get":return{tool:e,installed:!1,command:`sudo apt-get update && sudo apt-get install -y ${o}`,hint:"apt-get install",autoInstall:!0};case"dnf":return{tool:e,installed:!1,command:`sudo dnf install -y ${o}`,hint:"dnf install",autoInstall:!0};case"pacman":return{tool:e,installed:!1,command:`sudo pacman -S --noconfirm ${o}`,hint:"pacman install",autoInstall:!0};default:return{tool:e,installed:!1,command:null,hint:`manual install required for ${e}`,autoInstall:!1}}}a(xe,"buildSystemInstallPlan");function _(e=g()){return S(e,".pi","agent","settings.json")}a(_,"resolveSettingsPath");function Qe(e={}){const t=e.homeDir??g(),n=e.settingsPath??_(t),r=e.manifest??P(b),o=ne(n),i=L(o,r,n,t);for(const s of G(r))i.add(s);const u=e.commandExists??m,c=e.packageManager??Ie(process.platform,u),l=["tmux","nvim","nerd-font"].map(s=>xe(s,c,u));return{manifest:r,installedPackageIds:i,systemPlans:l}}a(Qe,"buildSetupInitialState");function Ze(e){const t=e.manifest.map(r=>{const o=e.installedPackageIds.has(r.id)?"installed":"not installed";return`- ${r.name} \u2014 ${r.shortDescription} (${o})`}),n=e.systemPlans.map(r=>r.installed?`- ${r.tool}: \u2713 installed`:r.command?`- ${r.tool}: ${r.command}`:`- ${r.tool}: ${r.hint}`);return["crtr sys setup (non-interactive)","","Provider login: open the wizard in a TTY and use the Providers tab (same flow as /login) to authenticate a model provider.","","Packages:",...t,"","System deps:",...n,"",`Inbox: the inbox-toggle key (${te()??"disabled"}) is a crouter keybinding \u2014 view or rebind it in \`crtr sys settings\`; it is installed on the same tmux server as every other crouter binding.`,"Run `crtr sys setup` in a TTY to open the wizard."].join(`
2
2
  `)+`
3
- `}a(Ze,"renderSetupStaticInstructions");class w extends Error{static{a(this,"SetupCommandError")}output;constructor(t,n){super(t),this.output=n}}function et(e){return`${e.stdout?.toString()??""}${e.stderr?.toString()??""}`}a(et,"commandOutput");function M(e,t,n){return new Promise((r,o)=>{const i=q(e,t,{stdio:["ignore","pipe","pipe"]});let u="";i.stdout.setEncoding("utf8"),i.stderr.setEncoding("utf8"),i.stdout.on("data",c=>{u+=c}),i.stderr.on("data",c=>{u+=c,n?.(c)}),i.on("error",c=>o(new w(`${e} failed: ${c.message}`,u))),i.on("close",c=>{c===0?r():o(new w(`${e} failed with status ${c??"unknown"}`,u))})})}a(M,"runCapturedCommand");function y(e,t){return M("sh",["-lc",e],t)}a(y,"runShellCommand");function Ce(e,t=ie()){return M(process.execPath,[t,"install",e])}a(Ce,"installPiPackage");async function K(){m("capture")||await y("npm install -g @crouton-kit/capture")}a(K,"installCaptureCliIfMissing");function F(e){return e instanceof w?e.output:void 0}a(F,"capturedFailureOutput");function Se(e){const n=F(e)?.split(/\r?\n/).find(r=>r.trim()!=="");return n!==void 0?n.trim():e instanceof Error?e.message:String(e)}a(Se,"firstFailureLine");function Ae(e){const t=F(e);t===void 0||t.length===0||process.stderr.write(t.endsWith(`
3
+ `}a(Ze,"renderSetupStaticInstructions");class v extends Error{static{a(this,"SetupCommandError")}output;constructor(t,n){super(t),this.output=n}}function et(e){return`${e.stdout?.toString()??""}${e.stderr?.toString()??""}`}a(et,"commandOutput");function M(e,t,n){return new Promise((r,o)=>{const i=W(e,t,{stdio:["ignore","pipe","pipe"]});let u="";i.stdout.setEncoding("utf8"),i.stderr.setEncoding("utf8"),i.stdout.on("data",c=>{u+=c}),i.stderr.on("data",c=>{u+=c,n?.(c)}),i.on("error",c=>o(new v(`${e} failed: ${c.message}`,u))),i.on("close",c=>{c===0?r():o(new v(`${e} failed with status ${c??"unknown"}`,u))})})}a(M,"runCapturedCommand");function x(e,t){return M("sh",["-lc",e],t)}a(x,"runShellCommand");function Ce(e,t=ie()){return M(process.execPath,[t,"install",e])}a(Ce,"installPiPackage");async function K(){m("capture")||await x("npm install -g @crouton-kit/capture")}a(K,"installCaptureCliIfMissing");function F(e){return e instanceof v?e.output:void 0}a(F,"capturedFailureOutput");function Se(e){const n=F(e)?.split(/\r?\n/).find(r=>r.trim()!=="");return n!==void 0?n.trim():e instanceof Error?e.message:String(e)}a(Se,"firstFailureLine");function Ae(e){const t=F(e);t===void 0||t.length===0||process.stderr.write(t.endsWith(`
4
4
  `)?t:`${t}
5
5
  `)}a(Ae,"printCapturedFailure");class $e{static{a(this,"SetupProgress")}interactive;bar=null;constructor(t=!!process.stdout.isTTY){this.interactive=t}start(t){return this.interactive?(this.bar=he(`Installing ${t}\u2026`),this.bar):null}async clear(){await this.bar?.stop(),this.bar=null}async succeed(t){await this.clear(),this.interactive&&process.stdout.write(`\r\x1B[2K\u2713 ${t}
6
6
  `)}async fail(t,n){await this.clear(),this.interactive&&process.stdout.write(`\r\x1B[2K\u2717 ${t} \u2014 ${Se(n)}
7
- `),Ae(n)}async run(t,n){const r=this.start(t);try{const o=await n(r);return await this.succeed(t),o}catch(o){throw await this.fail(t,o),o}}}function j(e,t){return A()?"current":(e?.(),fe(t),A()?"provisioned":"unavailable")}a(j,"ensureManagedRenderer");async function Ee(){try{ae()}catch{}}a(Ee,"reconcileInboxFrontDoor");function B(e,t){return t.length===0?[`${e}: none`]:[`${e}:`,...t.map(n=>` - ${n}`)]}a(B,"summarySection");function tt(e){return["crtr sys setup complete",...B("installed",e.installed),...B("skipped",e.skipped)].join(`
7
+ `),Ae(n)}async run(t,n){const r=this.start(t);try{const o=await n(r);return await this.succeed(t),o}catch(o){throw await this.fail(t,o),o}}}function j(e,t){return A()?"current":(e?.(),fe(t),A()?"provisioned":"unavailable")}a(j,"ensureManagedRenderer");async function Ee(){try{ae()}catch{}}a(Ee,"reconcileInboxFrontDoor");function B(e,t){return t.length===0?[`${e}: none`]:[`${e}:`,...t.map(n=>` - ${n}`)]}a(B,"summarySection");function tt(e){return["crtr sys setup complete",...B("installed",e.installed),...B("skipped",e.skipped),"New to crouter? Ask for the tutorial in your first conversation, or run /tutorial any time."].join(`
8
8
  `)+`
9
- `}a(tt,"formatSetupExecutionSummary");function G(e,t){const n=[];for(const r of e)try{r.sourceKind==="marketplace-source"?E(r.name,t)!==null&&n.push(r.id):r.sourceKind==="marketplace-plugin"&&ge(r.name,t)!==null&&n.push(r.id)}catch{}return n}a(G,"detectInstalledMarketplaceEntries");function Re(e,t="user"){if(E(e,t)!==null)return null;if(e!==R)throw new Error(`marketplace not registered: ${e}`);return $({url:D,ref:O,scope:t}),e}a(Re,"ensureMarketplaceRegistered");async function U(e){if(e.sourceKind==="marketplace-source")return $({url:e.installSource,ref:O,scope:"user"}),[e.name];if(e.sourceKind==="marketplace-plugin"){const[t,n]=e.installSource.split("/"),r=Re(t);return await me({marketplace:t,plugin:n},"user"),e.id==="capture-plugin"&&await K(),r===null?[e.name]:[r,e.name]}return await Ce(e.installSource),[e.name]}a(U,"installSetupPackage");async function nt(e){const t=I(b),n=L(re(_()),t);for(const i of G(t,"user"))n.add(i);const r=new Map(t.map(i=>[i.id,i])),o=process.stdin.isTTY&&process.stdout.isTTY?["crtr-extensions","official-marketplace","oauth-adapter","capture-plugin","search-plugin"]:["crtr-extensions","official-marketplace","capture-plugin","search-plugin"];for(const i of o){const u=r.get(i);if(!n.has(i))try{await U(u)}catch(c){e(u.name,c)}}try{j(void 0,()=>{})==="unavailable"&&e("termrender renderer",new Error("unavailable \u2014 install uv"))}catch(i){e("termrender renderer",i)}return se()}a(nt,"reconcileFirstRun");async function rt(e){const t=[],n=[],r=[],o=[],i=new $e;for(const s of e.packages){if(e.installedPackageIds.has(s.id)){if(n.push(`${s.name} (already installed)`),s.id==="capture-plugin"&&!m("capture"))try{await i.run("Capture CLI",()=>K()),t.push("Capture CLI")}catch(d){n.push(`Capture CLI (${d instanceof Error?d.message:String(d)})`)}continue}try{t.push(...await i.run(s.name,()=>U(s)))}catch(d){if(s.sourceKind!=="marketplace-source"&&s.sourceKind!=="marketplace-plugin")throw d;n.push(`${s.name} (${d instanceof Error?d.message:String(d)})`)}}for(const s of e.systemDeps){if(s.installed){o.push(`${s.tool} (already installed)`);continue}if(!s.autoInstall||!s.command){o.push(`${s.tool} (${s.hint})`);continue}const d=s.command,p=d===de;await i.run(s.tool,f=>{if(!p||f===null)return y(d);const Y=le();return y(d,z=>{const C=Y(z);C!==null&&f.setFraction(C/100)})}),r.push(s.tool)}const u=[];let c=!1,l;try{l=j(()=>{c=!0,i.start("termrender renderer")},s=>u.push(s))}catch(s){throw c&&await i.fail("termrender renderer",s),s}switch(l){case"provisioned":c&&await i.succeed("termrender renderer"),t.push("termrender renderer");break;case"current":n.push("termrender renderer (current)");break;case"unavailable":{const s=new w("unavailable \u2014 install uv",u.join(""));c&&await i.fail("termrender renderer",s),n.push("termrender renderer (unavailable \u2014 install uv)");break}}return await Ee(),{installed:[...t,...r],skipped:[...n,...o]}}a(rt,"executeSetupSubmission");export{R as OFFICIAL_MARKETPLACE_NAME,O as OFFICIAL_MARKETPLACE_REF,D as OFFICIAL_MARKETPLACE_URL,Je as SetupKeybindingsDraft,Qe as buildSetupInitialState,I as buildSetupManifest,ye as buildSystemInstallPlan,m as detectCommandOnPath,L as detectInstalledSetupPackageIds,xe as detectPackageManager,rt as executeSetupSubmission,tt as formatSetupExecutionSummary,N as normalizePackageIdentity,Xe as packagePlanAction,nt as reconcileFirstRun,Ee as reconcileInboxFrontDoor,Ze as renderSetupStaticInstructions,_ as resolveSettingsPath};
9
+ `}a(tt,"formatSetupExecutionSummary");function G(e,t){const n=[];for(const r of e)try{r.sourceKind==="marketplace-source"?E(r.name,t)!==null&&n.push(r.id):r.sourceKind==="marketplace-plugin"&&ge(r.name,t)!==null&&n.push(r.id)}catch{}return n}a(G,"detectInstalledMarketplaceEntries");function Re(e,t="user"){if(E(e,t)!==null)return null;if(e!==R)throw new Error(`marketplace not registered: ${e}`);return $({url:D,ref:O,scope:t}),e}a(Re,"ensureMarketplaceRegistered");async function U(e){if(e.sourceKind==="marketplace-source")return $({url:e.installSource,ref:O,scope:"user"}),[e.name];if(e.sourceKind==="marketplace-plugin"){const[t,n]=e.installSource.split("/"),r=Re(t);return await me({marketplace:t,plugin:n},"user"),e.id==="capture-plugin"&&await K(),r===null?[e.name]:[r,e.name]}return await Ce(e.installSource),[e.name]}a(U,"installSetupPackage");async function nt(e){const t=P(b),n=L(re(_()),t);for(const i of G(t,"user"))n.add(i);const r=new Map(t.map(i=>[i.id,i])),o=process.stdin.isTTY&&process.stdout.isTTY?["crtr-extensions","official-marketplace","oauth-adapter","capture-plugin","search-plugin"]:["crtr-extensions","official-marketplace","capture-plugin","search-plugin"];for(const i of o){const u=r.get(i);if(!n.has(i))try{await U(u)}catch(c){e(u.name,c)}}try{j(void 0,()=>{})==="unavailable"&&e("termrender renderer",new Error("unavailable \u2014 install uv"))}catch(i){e("termrender renderer",i)}return se()}a(nt,"reconcileFirstRun");async function rt(e){const t=[],n=[],r=[],o=[],i=new $e;for(const s of e.packages){if(e.installedPackageIds.has(s.id)){if(n.push(`${s.name} (already installed)`),s.id==="capture-plugin"&&!m("capture"))try{await i.run("Capture CLI",()=>K()),t.push("Capture CLI")}catch(d){n.push(`Capture CLI (${d instanceof Error?d.message:String(d)})`)}continue}try{t.push(...await i.run(s.name,()=>U(s)))}catch(d){if(s.sourceKind!=="marketplace-source"&&s.sourceKind!=="marketplace-plugin")throw d;n.push(`${s.name} (${d instanceof Error?d.message:String(d)})`)}}for(const s of e.systemDeps){if(s.installed){o.push(`${s.tool} (already installed)`);continue}if(!s.autoInstall||!s.command){o.push(`${s.tool} (${s.hint})`);continue}const d=s.command,p=d===de;await i.run(s.tool,f=>{if(!p||f===null)return x(d);const Y=le();return x(d,z=>{const C=Y(z);C!==null&&f.setFraction(C/100)})}),r.push(s.tool)}const u=[];let c=!1,l;try{l=j(()=>{c=!0,i.start("termrender renderer")},s=>u.push(s))}catch(s){throw c&&await i.fail("termrender renderer",s),s}switch(l){case"provisioned":c&&await i.succeed("termrender renderer"),t.push("termrender renderer");break;case"current":n.push("termrender renderer (current)");break;case"unavailable":{const s=new v("unavailable \u2014 install uv",u.join(""));c&&await i.fail("termrender renderer",s),n.push("termrender renderer (unavailable \u2014 install uv)");break}}return await Ee(),{installed:[...t,...r],skipped:[...n,...o]}}a(rt,"executeSetupSubmission");export{R as OFFICIAL_MARKETPLACE_NAME,O as OFFICIAL_MARKETPLACE_REF,D as OFFICIAL_MARKETPLACE_URL,Je as SetupKeybindingsDraft,Qe as buildSetupInitialState,P as buildSetupManifest,xe as buildSystemInstallPlan,m as detectCommandOnPath,L as detectInstalledSetupPackageIds,Ie as detectPackageManager,rt as executeSetupSubmission,tt as formatSetupExecutionSummary,N as normalizePackageIdentity,Xe as packagePlanAction,nt as reconcileFirstRun,Ee as reconcileInboxFrontDoor,Ze as renderSetupStaticInstructions,_ as resolveSettingsPath};
@@ -0,0 +1 @@
1
+ export declare const tutorialBranch: import("../../../core/command.js").BranchDef;
@@ -0,0 +1 @@
1
+ import{defineBranch as e}from"../../../core/command.js";import{advancedLeaf as a,basicLeaf as r}from"./tracks.js";import{scenarioBranch as s}from"./scenario.js";const c=e({name:"tutorial",description:"guided crouter lessons and disposable practice scenarios",whenToUse:"you need to learn crouter operation conversationally, one lesson at a time, or create the disposable canvas state a lesson asks you to inspect.",extensible:!0,help:{name:"sys tutorial",summary:"guided crouter lessons and disposable practice scenarios",model:"Tracks serve one lesson per invocation and never record progress, so --step can start or resume anywhere. Scenarios create only the temporary canvas state a lesson needs and clean exactly the recorded rows afterward."},children:[r,a,s]});export{c as tutorialBranch};
@@ -0,0 +1,6 @@
1
+ export interface TutorialLesson {
2
+ readonly title: string;
3
+ readonly body: string;
4
+ }
5
+ export declare const BASIC_LESSONS: readonly TutorialLesson[];
6
+ export declare const ADVANCED_LESSONS: readonly TutorialLesson[];
@@ -0,0 +1,12 @@
1
+ import{resolveNodeVisual as e}from"../../../core/canvas/status-glyph.js";const t=e("active",{}),n=e("idle",{}),a=e("done",{}),o=e("dead",{}),i=e("canceled",{}),s=e("active",{streaming:!0}),r=e("active",{frozen:!0}),h=e("active",{hanging:{kind:"wedged"}}),l=[{title:"Terminal Option key \u2014 do this first",body:["Every crouter global is an Alt chord, and on macOS terminals Option defaults to emitting accented characters. Until this is fixed, `Alt+C` types `\xE7` and the user concludes crouter is broken. Fix it before teaching anything else.","","Read `TERM_PROGRAM` to identify the terminal, then give the exact path:","","- **iTerm2** \u2014 Settings \u2192 Profiles \u2192 Keys \u2192 General \u2192 set **Left option** and **Right option** to `Esc+`.","- **Ghostty** \u2014 `macos-option-as-alt = true` in `~/.config/ghostty/config`.","- **kitty** \u2014 `macos_option_as_alt yes` in `~/.config/kitty/kitty.conf`.","- **WezTerm** \u2014 `send_composed_key_when_left_alt_is_pressed = false`.",'- **Alacritty** \u2014 `window.option_as_alt = "Both"`.','- **Terminal.app** \u2014 Settings \u2192 Profiles \u2192 Keyboard \u2192 check "Use Option as Meta key".',"","For the file-based terminals, offer to make the edit yourself: show the exact line you would add, wait for a yes, then write it and tell them to reload. iTerm2 and Terminal.app are GUI-only, so walk them through it.","","**Verify interactively before moving on.** Ask the user to press `Alt+C` and tell you what happened. A menu means it works. A `\xE7`, or nothing, means the setting did not take \u2014 a GUI terminal usually needs a new window, and inside tmux it needs the terminal restarted. Do not advance until they confirm the menu opened; every remaining lesson depends on it."].join(`
2
+ `)},{title:"tmux \u2014 meet the user where they are",body:["crouter runs its viewers as tmux panes, so a user who cannot split, close, and move between panes cannot drive it. Teach the four verbs that matter: **new pane**, **new window**, **close pane**, **next pane / next window**.","","Adapt to what is already there. Check for `~/.tmux.conf` and `~/.config/tmux/tmux.conf`, and run `tmux show-options -g prefix` if a server is up.","","- **No config at all** \u2014 offer the starter config below wholesale.","- **An existing config** \u2014 read it first. Teach in *their* prefix and *their* bindings, and only offer to fill genuine gaps. Someone who already bound `|` and `-` for splits does not want your opinion; they want to know how their muscle memory maps onto crouter.","","The starter config, for a user who has nothing:","","```tmux","# prefix on Ctrl-a \u2014 easier to reach than the default Ctrl-b","set -g prefix C-a","unbind C-b","bind C-a send-prefix","","# splits that look like what they do, opening in the current directory",'bind | split-window -h -c "#{pane_current_path}"','bind - split-window -v -c "#{pane_current_path}"','bind c new-window -c "#{pane_current_path}"',"","# move between panes without the prefix","bind -n M-Left select-pane -L","bind -n M-Right select-pane -R","bind -n M-Up select-pane -U","bind -n M-Down select-pane -D","","# sane defaults","set -g mouse on","set -g base-index 1","setw -g pane-base-index 1","set -g escape-time 0","set -g history-limit 50000","```","","**Never write this silently.** Show the user exactly what you propose to add, as a diff against what is already in the file, and wait for an explicit yes. If they have a config, back it up first and say where the backup went. After writing, tell them to run `tmux source-file <path>` or start a fresh session.","","One caveat worth stating out loud: `escape-time 0` matters for crouter specifically, because a nonzero value makes tmux wait after every Escape and Alt chord, which shows up as the Alt+C menu feeling laggy or dropping keys."].join(`
3
+ `)},{title:"The crouter menu \u2014 Alt+C",body:["Teach `Alt+C` as **the** crouter prefix. It opens an action menu, and nearly everything is `Alt+C` then one key. Frame it as a menu the user opens and reads, not a table to memorize \u2014 the menu shows its own options, so the only thing they truly have to remember is `Alt+C`.","","Have them press `Alt+C` now and read the menu aloud to you. Then walk the entries that matter early:","","| Chord | What it does |","|---|---|","| `Alt+C` `/` | **Find node** \u2014 the canvas navigator |","| `Alt+C` `g` | **Canvas graph** |","| `Alt+C` `i` | **Inspect** \u2014 opens a second menu (metadata, context directory) for the focused node |","| `Alt+C` `s` | Generate a **recap** \u2014 goal and state, plus a refreshed title |","| `Alt+C` `,` | **Settings** |","| `Alt+C` `x` | **Close** this node and its subtree |","","Two globals skip the menu entirely, because they are used constantly:","","| Chord | What it does |","|---|---|","| `Alt+I` | Toggle the human **inbox** |","| `Alt+]` / `Alt+[` | Next / previous node |","","Mention that `Alt+C` `,` is where every one of these bindings can be rebound, so nothing you just taught is a permanent constraint. Do not walk the settings panel now; just plant that it exists."].join(`
4
+ `)},{title:"The words crouter uses",body:["The menu and graph use a few words precisely. Learn them against what is already in front of you, not as a glossary to memorize: this conversation is a **root node**, this pane is a **viewer**, and the process behind it that runs the agent is the **broker**.","","A **node** is one conversation or unit of work. The **canvas** is every node on this machine and the links between them. A root has no manager above it; when it delegates, it creates child nodes below it.","","An **orchestrator** is a node that breaks work into independent pieces and manages children. A base node does the work itself. **Kinds** specialize a node with a particular role and instructions, such as development or research; they do not change which conversation the node is.","","The **spine** is the reporting path from a child to the manager that subscribed to it. `crtr push` sends a report up that path and wakes the manager, so a manager can end its turn instead of polling every child.","","A **yield** refreshes a node that is running out of context while keeping its identity and mandate. A **wedged** node is one the runtime has detected as stuck rather than merely waiting; its fault state tells you it needs attention. These distinctions matter because an idle node may simply be waiting for a person, while a wedged one needs recovery."].join(`
5
+ `)},{title:"Resuming a conversation",body:["`Alt+C` then `/` opens the canvas navigator: a searchable list of every conversation on this machine. Pick one, land in it, keep going.","","Have the user open it and look around. On a fresh install there may be only one entry \u2014 that is fine, the point is the reflex, not the contents.","","The idea to land, which matters more than the keystroke: **conversations are durable and addressable.** A crouter conversation is not a terminal session that dies when the window closes. It is a node on the canvas with an id, a saved transcript, and a context directory. Walking away does not lose it, and finding it again is a search rather than an act of archaeology.","","**Say this part explicitly \u2014 it changes how people use crouter.** The viewer and the node are decoupled. Closing the tmux pane, closing the window, or quitting the terminal outright detaches the viewer and leaves the node running. Nothing is killed by walking away, and reattaching later picks up exactly where it was. New users assume the opposite and babysit panes they do not need to; tell them plainly that they can close it. Ending a node is a deliberate, separate action (`Alt+C` `x`). Say it once, here \u2014 later lessons assume it and do not repeat it.","","Worth mentioning while they are in the navigator: `p` pins a conversation so it sorts to the top, which is how people keep their two or three live threads reachable without searching."].join(`
6
+ `)},{title:"Memory",body:["Keep this abstract. The user does not need the storage model; they need to know memory exists, that it is scoped, and that they can drive it by just asking.","","Cover four points:","","1. **Two kinds.** *Knowledge* is how to do something \u2014 a playbook, a technique, a fact about a system. *Preference* is how the user wants the agent to behave \u2014 a standing instruction or a correction.","","2. **Where a document lives decides who sees it.** A memory can be scoped to everything the user does, to one project, to one profile, or to a single node. That scope choice is what keeps a work convention from leaking into a personal project.","","3. **Documents are routed, not loaded.** Each one carries a condition describing when it is worth reading. An agent pulls in the handful that match the task instead of loading everything, which is why a large memory store does not slow anything down or crowd out the actual work.","",'4. **The practical instruction is one sentence: just say "remember that."** The agent decides the kind, writes the document, and picks the scope. The user can correct any of those in follow-up. This is most of what a new user needs; the rest is detail they can pick up when they care.',"","If you have genuinely learned something durable about this user during the tutorial \u2014 their terminal, their tmux prefix, a convention they stated \u2014 offer to save it now as a real demonstration. A memory the user watched get created from a real fact beats any explanation of the feature."].join(`
7
+ `)},{title:"The graph \u2014 Alt+C then g",body:["The graph shows the node tree: who spawned whom, what is running, what has finished.","","A tour against a single row teaches nothing, so put a few nodes on the canvas first:","","```bash","crtr sys tutorial scenario start idle-nodes","```","","That creates three idle nodes purely for the demo. They never run a turn, so it costs nothing. Then have the user press `Alt+C` `g` and navigate:","","| Key | Action |","|---|---|","| `j` / `k` | Move down / up |","| `Enter` | Focus the selected node \u2014 open it in this pane |","| `l` / `h` | Move to a child / back to the manager |","| `m` | Jump to the selected node\u2019s manager |","| `r` | Revive a dormant node |","| `x` | Close the selected subtree |","| `Esc` | Dismiss the graph |","","**Be honest about what the demo shows.** The scenario creates independent roots, not a tree, so `j`/`k`/`Enter`/`Esc` are the keys they can actually practice here. Explain `h` and `l` as what they will use once they delegate work and have real parent-child structure \u2014 the advanced track covers where that structure comes from.","","Then tear the demo down:","","```bash","crtr sys tutorial scenario clean idle-nodes","```"].join(`
8
+ `)},{title:"Status glyphs",body:["The graph gives every node a status glyph, so you can read the canvas before opening each conversation. The five ordinary statuses are:","","| Glyph | Status | What it means |","|---|---|---|",`| ${t.glyph} | active | The node is working. |`,`| ${n.glyph} | idle | The node is not currently running a turn. |`,`| ${a.glyph} | done | The node finished its work. |`,`| ${o.glyph} | dead | The node ended without remaining live work. |`,`| ${i.glyph} | canceled | The node was canceled. |`,"",`Three overlays replace that ordinary status when they are more important: ${s.glyph} live means a turn is streaming now; ${r.glyph} frozen means the node is waiting for broker capacity; ${h.glyph} fault means the runtime found a problem that needs attention.`,"","The precedence is `fault > frozen > live > status`. Read the leftmost applicable condition: a fault hides frozen or live, frozen hides live, and live hides the ordinary status.","","Put a few rows in the graph so the glyphs have something to describe:","","```bash","crtr sys tutorial scenario start idle-nodes","```","","Open `Alt+C` then `g`, identify the idle rows, then clean up the demo yourself:","","```bash","crtr sys tutorial scenario clean idle-nodes","```","","Do not leave this state for another lesson. A user may jump to any lesson, and every demo must leave the canvas as it found it."].join(`
9
+ `)},{title:"The inbox \u2014 Alt+I",body:["The inbox is how a node reaches the user. When an agent needs a decision, wants a document reviewed, or is blocked on something only a person can settle, it posts there and goes dormant. Without the inbox that agent looks stuck; with it, the user can see it is simply waiting on them.","","Give them something real to answer:","","```bash","crtr sys tutorial scenario start pending-ask","```","","Tell them the entry is titled **A question from the tutorial**, so they can find it even if real requests are already waiting. Then have them press `Alt+I`, read it, and respond to it. Walk them through selecting the item and answering, and point out that a real answer wakes the waiting node immediately \u2014 a reply is not filed for later, it resumes the work that was blocked on it. (This demo request has no node behind it, so nothing wakes; say so rather than implying otherwise.)","","Clean up when the point has landed:","","```bash","crtr sys tutorial scenario clean pending-ask","```","","The habit worth leaving them with: **check the inbox when something seems idle.** An agent that appears to have stopped has very often asked a question and is waiting."].join(`
10
+ `)}],c=[{title:"Promotion to orchestrator",body:["Every node is one of two modes. A **base node** does the work itself \u2014 reads, edits, runs commands. An **orchestrator** holds a roadmap and delegates, spawning children for the pieces and integrating what comes back.","","- `Alt+C` `o` promotes the focused node to orchestrator.","- `Alt+C` `Shift+O` demotes it back to a base node.","- From inside a conversation, `/promote` does the same thing, and `/promote <kind>` specializes it at the same time.","","Teach *when*, because the keystroke is the easy part. Promotion earns its cost when the work has **genuinely independent parts** that can run at the same time, or **many large phases** that would otherwise fill a single context window. It is the wrong move for a two-file change: spinning up a child and waiting for its report costs more than doing the work directly.","","The mechanism worth naming: a child reports up to whoever subscribes to it, and that report **wakes the manager**. Nobody polls. An orchestrator delegates, ends its turn, and comes back when there is something to integrate \u2014 which is why a fan-out of five children costs one manager context window rather than five.","","The failure mode to warn about is recursive delegation: an orchestrator that hands its entire assignment to one child has not decomposed anything, it has added a layer. Each child needs a bounded outcome genuinely smaller than the whole."].join(`
11
+ `)},{title:"Terminal and resident nodes",body:["Lifecycle is the other axis, orthogonal to mode. It answers: what does this node owe, and when is it allowed to end?","","- A **terminal** node owes a final report up the spine. It does its work, delivers, and reaps itself. This is the shape for bounded delegated work.","- A **resident** node is interactable, never forced to submit a final, and stays wakeable indefinitely. This is the shape for a conversation with a person.","","`Alt+C` `d` demotes a node to terminal. `Alt+C` `Shift+R` makes one resident. Roots default to resident; managed children default to terminal.","","**Why it works this way** \u2014 this is the part worth understanding, not memorizing.","","A terminal node is *depended upon to finish*. Its manager subscribed to it and will be woken by its report, so the guarantee that it must eventually deliver one is exactly what makes delegation safe. Without it, an orchestrator could wait forever on a child that quietly decided it was done.","","A resident node must never be forced to close mid-sentence. A conversation with a person has no natural completion \u2014 the user may come back tomorrow \u2014 so requiring a final report would either truncate the conversation or produce a meaningless one. Residency removes that requirement, and going dormant costs nothing: the node holds no window and burns no compute while it waits.","","This is why **the user's own conversation is resident and cannot be finished on its behalf**, while the children it spawns are terminal and reap themselves when they report. The user sees this as agents that clean up after themselves while their own thread persists \u2014 that is the lifecycle axis doing its job.","","Related and worth a mention: a **yield** lets a resident node whose context is filling revive fresh under the same identity and mandate, carrying a note to its future self. The conversation survives; only the context window is recycled."].join(`
12
+ `)}];export{c as ADVANCED_LESSONS,l as BASIC_LESSONS};
@@ -0,0 +1 @@
1
+ export declare const scenarioBranch: import("../../../core/command.js").BranchDef;
@@ -0,0 +1,2 @@
1
+ var S=Object.defineProperty;var a=(e,t)=>S(e,"name",{value:t,configurable:!0});import{mkdirSync as w,readFileSync as A}from"node:fs";import{dirname as y,join as R}from"node:path";import{ApiError as b}from"../../../api/index.js";import{withMetaLock as q,writeMetaObjectLocked as O}from"../../../core/canvas/meta-file.js";import{defineBranch as T,defineLeaf as f}from"../../../core/command.js";import{InputError as u}from"../../../core/io.js";import{userScopeRoot as E}from"../../../core/scope.js";import{cliClient as c,getNodeOrNull as L,rethrowAsCliError as l}from"../../api-client.js";const _={"idle-nodes":{title:"Three idle nodes on the canvas",teaches:"the graph lesson \u2014 j/k navigation is unteachable against a single row",builds:"three independent nodes that never run a turn, so they cost nothing"},"pending-ask":{title:"A question waiting in the inbox",teaches:"the inbox lesson \u2014 reading a pending item and answering it",builds:"one durable pending request in the human inbox"}},C=["export default function TutorialAsk() {"," return (",' <Page title="A question from the tutorial" subtitle="This is what an agent waiting on you looks like.">'," <UserQuestion",' id="answer"',' label="Which of these should the tutorial cover next?"',' body="Nothing depends on this answer \u2014 it exists so you can practice responding to a real inbox item."',' mode="single"'," options={["," { id: 'orchestration', label: 'Delegating work to other agents' },"," { id: 'memory', label: 'Teaching it my preferences' },"," { id: 'done', label: 'Nothing \u2014 I want to start working' },"," ]}"," />"," </Page>"," );","}"].join(`
2
+ `);function h(){return R(E(),"tutorial-scenarios.json")}a(h,"ledgerPath");function k(e){return Array.isArray(e)&&e.every(t=>typeof t=="string")}a(k,"isStringArray");function v(){let e;try{e=JSON.parse(A(h(),"utf8"))}catch(r){if(r.code==="ENOENT")return{};throw r}if(typeof e!="object"||e===null||Array.isArray(e))throw new u({error:"invalid_scenario_ledger",message:`tutorial scenario ledger at ${h()} must be an object`,next:"Remove or repair the ledger file, then retry the scenario command."});const t={};for(const r of Object.keys(_)){const n=e[r];if(n===void 0)continue;if(typeof n!="object"||n===null||typeof n.started!="string")throw new u({error:"invalid_scenario_ledger",message:`tutorial scenario ledger has an invalid ${r} record`,next:"Remove or repair the ledger file, then retry the scenario command."});const s=n;if(s.nodes!==void 0&&!k(s.nodes)||s.requests!==void 0&&!k(s.requests))throw new u({error:"invalid_scenario_ledger",message:`tutorial scenario ledger has invalid ids for ${r}`,next:"Remove or repair the ledger file, then retry the scenario command."});t[r]={started:s.started,...s.nodes===void 0?{}:{nodes:s.nodes},...s.requests===void 0?{}:{requests:s.requests}}}return t}a(v,"readLedger");function x(e){const t=h();return w(y(t),{recursive:!0}),q(t,()=>e(v()))}a(x,"withLedger");function p(e){const t=h();return w(y(t),{recursive:!0}),q(t,()=>{const r=v(),n=e(r);return O(t,r),n})}a(p,"updateLedger");function I(e){return p(t=>{if(t[e]!==void 0)throw new u({error:"scenario_already_standing",message:`the '${e}' scenario is already standing (started ${t[e].started})`,field:"scenario",next:`Run \`crtr sys tutorial scenario clean ${e}\` first, then start it again.`});const r={started:new Date().toISOString(),...e==="idle-nodes"?{nodes:[]}:{requests:[]}};return t[e]=r,r})}a(I,"reserveScenario");async function N(e,t,r){if(!p(s=>{const i=s[e];return i===void 0?!1:(i[t]=[...i[t]??[],r],!0)})){try{t==="nodes"?await c().closeNode(r):await c().cancelHumanRequest(r,{reason:"Tutorial scenario start interrupted."})}catch(s){m(s)||l(s)}throw new Error(`tutorial scenario '${e}' disappeared while it was starting`)}}a(N,"appendScenarioId");function m(e){return e instanceof b&&(e.status===404||e.code==="already_settled")}a(m,"isAlreadyCleaned");function $(e){return e==="done"||e==="dead"||e==="canceled"}a($,"isTerminalNode");function j(e){const t=e.scenario;if(t==="idle-nodes"||t==="pending-ask")return t;throw new u({error:"unknown_scenario",message:`no tutorial scenario named '${String(t)}'`,field:"scenario",next:"Use idle-nodes or pending-ask. Run `crtr sys tutorial scenario list` for what each one builds."})}a(j,"scenario");async function P(e){const t=["tutorial-demo-one","tutorial-demo-two","tutorial-demo-three"];for(const r of t){const n=await c().createNode({root:!0,no_kickoff:!0,prompt:"",cwd:process.cwd(),prefer_warm:!0,name:r}).catch(l);await N("idle-nodes","nodes",n.node_id),e.nodes.push(n.node_id)}}a(P,"startIdleNodes");async function U(e){const t=await c().createHumanRequest({page:{dialect:"jsx",source:C},delivery:{placement:"inline",inbox:!0,reply:!1},source:{askedBy:"crtr sys tutorial",sessionName:"tutorial"},creator_cwd:process.cwd()}).catch(l);await N("pending-ask","requests",t.request_id),e.requests.push(t.request_id)}a(U,"startPendingAsk");const B=f({name:"list",description:"list the tutorial practice scenarios",whenToUse:"you need to choose a disposable canvas scenario for a lesson or see whether one is already standing.",help:{name:"sys tutorial scenario list",summary:"list the tutorial practice scenarios and their standing state",inputNote:"No input parameters.",output:[{name:"scenarios",type:"object[]",required:!0,constraint:"One row per scenario: {id, title, teaches, builds, standing}."},{name:"standing",type:"number",required:!0,constraint:"Number of scenarios currently recorded as standing."}],outputKind:"object",effects:["None. Read-only."]},run:a(async()=>{const e=x(r=>r),t=Object.entries(_).map(([r,n])=>({id:r,title:n.title,teaches:n.teaches,builds:n.builds,standing:e[r]!==void 0}));return{scenarios:t,standing:t.filter(r=>r.standing).length}},"run")}),D=f({name:"start",description:"create a tutorial practice scenario",whenToUse:"a tutorial lesson needs its disposable graph rows or inbox request before the user can practice against a real canvas surface.",help:{name:"sys tutorial scenario start",summary:"create one tutorial practice scenario",params:[{kind:"positional",name:"scenario",type:"string",required:!0,constraint:"Scenario to create: idle-nodes (three no-kickoff roots) or pending-ask (one durable inbox request)."}],output:[{name:"nodes",type:"string[]",required:!1,constraint:"Created root node ids for idle-nodes."},{name:"requests",type:"string[]",required:!1,constraint:"Created human request ids for pending-ask."},{name:"clean_with",type:"string",required:!0,constraint:"Exact command that removes this scenario."},{name:"look_at",type:"string",required:!0,constraint:"Viewer surface that shows the created state."}],outputKind:"object",effects:["idle-nodes creates three independent root rows with no kickoff turn and records their ids in ~/.crouter/tutorial-scenarios.json.","pending-ask publishes one durable human request and records its id in ~/.crouter/tutorial-scenarios.json.","Refuses a scenario already recorded as standing; clean it before starting it again."]},run:a(async e=>{const t=j(e),r=I(t);return t==="idle-nodes"?await P(r):await U(r),{...r.nodes===void 0?{}:{nodes:r.nodes},...r.requests===void 0?{}:{requests:r.requests},clean_with:`crtr sys tutorial scenario clean ${t}`,look_at:t==="idle-nodes"?"Open the graph with `Alt+C` then `g`.":"Open the inbox with `Alt+I`."}},"run")}),H=f({name:"clean",description:"remove a tutorial practice scenario",whenToUse:"the lesson has finished with a scenario and you need to remove exactly the nodes or request it recorded.",help:{name:"sys tutorial scenario clean",summary:"remove exactly the rows recorded for one tutorial practice scenario",params:[{kind:"positional",name:"scenario",type:"string",required:!0,constraint:"Scenario to remove: idle-nodes or pending-ask."}],output:[{name:"was_standing",type:"boolean",required:!0,constraint:"Whether this scenario had a ledger record to remove."},{name:"closed_nodes",type:"number",required:!1,constraint:"Number of ledger-recorded node ids closed. Present when the scenario was standing."},{name:"canceled_requests",type:"number",required:!1,constraint:"Number of ledger-recorded request ids canceled. Present when the scenario was standing."},{name:"already_settled_requests",type:"number",required:!1,constraint:"Number of ledger-recorded request ids already terminal when clean processed them. Present when the scenario was standing."}],outputKind:"object",effects:["Closes or cancels only ids recorded for this scenario in ~/.crouter/tutorial-scenarios.json, then removes that scenario record."]},run:a(async e=>{const t=j(e),r=x(o=>o[t]);if(r===void 0)return{was_standing:!1};let n=0,s=0,i=0;for(const o of r.nodes??[]){const d=await L(o);if(!(d===null||$(d.status)))try{await c().closeNode(o),n++}catch(g){m(g)||l(g)}}for(const o of r.requests??[])try{await c().cancelHumanRequest(o,{reason:"Tutorial scenario cleaned up."}),s++}catch(d){d instanceof b&&d.code==="already_settled"?i++:m(d)||l(d)}return p(o=>{delete o[t]}),{was_standing:!0,closed_nodes:n,canceled_requests:s,...i===0?{}:{already_settled_requests:i}}},"run")}),X=T({name:"scenario",description:"create and remove disposable tutorial practice state",whenToUse:"a tutorial lesson needs real but disposable canvas rows or an inbox request to inspect and practice against.",help:{name:"sys tutorial scenario",summary:"create and remove disposable canvas state for tutorial lessons",model:"Each scenario records only the ids it creates. Start refuses an already-standing scenario, and clean acts only on that recorded list."},children:[B,D,H]});export{X as scenarioBranch};
@@ -0,0 +1,2 @@
1
+ export declare const basicLeaf: import("../../../core/command.js").LeafDef;
2
+ export declare const advancedLeaf: import("../../../core/command.js").LeafDef;
@@ -0,0 +1,4 @@
1
+ var p=Object.defineProperty;var a=(e,t)=>p(e,"name",{value:t,configurable:!0});import{defineLeaf as g}from"../../../core/command.js";import{InputError as b}from"../../../core/io.js";import{ADVANCED_LESSONS as w,BASIC_LESSONS as v}from"./lessons.js";const $=["Teach this conversationally, in your own words \u2014 do not paste this lesson at the user.","Ask before you change anything on their machine, and show the exact diff you propose.","Let the user actually try each thing and confirm it worked before you move on."].join(" "),m={basic:v,advanced:w};function T(e){return e.map((t,n)=>`${n+1}. ${t.title}`).join(`
2
+ `)}a(T,"tableOfContents");async function k(){const{resolveRoot:e}=await import("../../../build-root.js"),n=(await e("sys")).subtrees.find(s=>s.name==="sys");if(n?.kind!=="branch")throw new Error("The assembled sys subtree is missing.");const r=n.children.find(s=>s.name==="tutorial");if(r?.kind!=="branch")throw new Error("The assembled sys tutorial branch is missing.");if(r.help.listing===void 0)throw new Error("The assembled sys tutorial listing is missing.");return r.help.listing}a(k,"tutorialListing");function x(e){return e.length===0?"That is the whole tutorial.":`What's next: ${e.map(({command:t,description:n})=>`${t} \u2014 ${n}`).join("; ")}`}a(x,"nextTracksLine");async function L(e,t){const n=m[e],r=n.length,s=t.step,o=s==null?1:Number(s);if(!Number.isInteger(o)||o<1||o>r)throw new b({error:"step_out_of_range",message:`--step must be between 1 and ${r} (got ${String(s)})`,field:"step",next:`Run \`crtr sys tutorial ${e} -h\` and read the contents, then pick a lesson in range.`});const d=n[o-1],u=o<r?`crtr sys tutorial ${e} --step ${o+1}`:void 0,l=u===void 0?await k():void 0;let c;if(l!==void 0){const f=l.filter(i=>i.plugin!==void 0).map(i=>({command:`crtr sys tutorial ${i.name}`,description:i.description}));if(e==="basic"){const i=l.find(y=>y.name==="advanced");if(i===void 0)throw new Error("The assembled sys tutorial advanced track is missing.");c=[{command:"crtr sys tutorial advanced",description:i.description},...f]}else c=f}return{track:e,step:o,total:r,title:d.title,body:c===void 0?d.body:`${d.body}
3
+
4
+ ${x(c)}`,...u===void 0?{}:{next:u},teaching_contract:$,...o===1?{contents:T(n)}:{},...c===void 0?{}:{next_tracks:c}}}a(L,"runTrack");function h(e,t,n){return g({name:e,description:t,whenToUse:n,help:{name:`sys tutorial ${e}`,summary:`serve one lesson from the ${e} crouter tutorial track`,params:[{kind:"flag",name:"step",type:"int",required:!1,default:1,constraint:`Lesson number, from 1 through ${m[e].length}. Default: 1.`}],output:[{name:"track",type:"string",required:!0,constraint:`The served track: ${e}.`},{name:"step",type:"number",required:!0,constraint:"The served lesson number."},{name:"total",type:"number",required:!0,constraint:"Number of lessons in this track."},{name:"title",type:"string",required:!0,constraint:"Lesson title."},{name:"body",type:"string",required:!0,constraint:"Lesson instructions for the teaching agent."},{name:"next",type:"string",required:!1,constraint:"The exact command for the next lesson. Absent on the last lesson."},{name:"teaching_contract",type:"string",required:!0,constraint:"Rules for teaching the lesson conversationally and safely."},{name:"contents",type:"string",required:!1,constraint:"Numbered titles for every lesson in this track. Present only for step 1."},{name:"next_tracks",type:"array",required:!1,constraint:"Available core and plugin tutorial tracks, each with command and description. Present only on the last lesson."}],outputKind:"object",effects:["None. Read-only. Progress is not stored."]},run:a(async r=>L(e,r),"run")})}a(h,"trackLeaf");const E=h("basic","serve a basic crouter lesson","you are learning the terminal, viewer, canvas, memory, graph, and inbox foundations before running real crouter work."),S=h("advanced","serve an advanced crouter lesson","you already know the basic operation and want the runtime model for delegation, lifecycle, and longer-lived work.");export{S as advancedLeaf,E as basicLeaf};
@@ -1 +1 @@
1
- var s=Object.defineProperty;var r=(o,m)=>s(o,"name",{value:m,configurable:!0});import{defineSysBranch as t}from"./sys/branch.js";import{configBranch as e}from"./sys/config.js";import{daemonBranch as f}from"./sys/daemon.js";import{sysDoctorLeaf as i}from"./sys/doctor.js";import{sysFeedbackLeaf as a}from"./sys/feedback.js";import{sysLogsLeaf as p}from"./sys/logs.js";import{sysWorktreesLeaf as n}from"./sys/worktrees.js";import{sysSettingsLeaf as y}from"./sys/settings.js";import{sysConnectLeaf as c}from"./sys/connect.js";import{sysSetupLeaf as L}from"./sys/setup.js";import{supportBranch as h}from"./sys/support.js";import{contextBranch as B}from"./sys/context.js";import{sysSyncBranch as g}from"./sys/sync.js";import{sysMigrateLeaf as S}from"./sys/migrate.js";import{sysUpdateLeaf as d,sysVersionLeaf as u}from"./sys/update.js";function A(){return t([y,e,c,L,i,B,a,p,n,h,d,u,S,g,f])}r(A,"registerSys");export{A as registerSys};
1
+ var t=Object.defineProperty;var r=(o,m)=>t(o,"name",{value:m,configurable:!0});import{defineSysBranch as s}from"./sys/branch.js";import{configBranch as f}from"./sys/config.js";import{daemonBranch as e}from"./sys/daemon.js";import{sysDoctorLeaf as i}from"./sys/doctor.js";import{sysFeedbackLeaf as a}from"./sys/feedback.js";import{sysLogsLeaf as p}from"./sys/logs.js";import{sysWorktreesLeaf as n}from"./sys/worktrees.js";import{sysSettingsLeaf as c}from"./sys/settings.js";import{sysConnectLeaf as y}from"./sys/connect.js";import{sysSetupLeaf as L}from"./sys/setup.js";import{tutorialBranch as h}from"./sys/tutorial/branch.js";import{supportBranch as B}from"./sys/support.js";import{contextBranch as g}from"./sys/context.js";import{sysSyncBranch as u}from"./sys/sync.js";import{sysMigrateLeaf as S}from"./sys/migrate.js";import{sysUpdateLeaf as d,sysVersionLeaf as k}from"./sys/update.js";function G(){return s([c,f,y,L,h,i,g,a,p,n,B,d,k,S,u,e])}r(G,"registerSys");export{G as registerSys};
@@ -1,9 +1,14 @@
1
+ import { cliClient } from '../../commands/api-client.js';
1
2
  export interface BootRootOpts {
2
3
  cwd: string;
3
4
  kind?: string;
4
5
  name?: string;
5
6
  /** Optional starter prompt (bare `crtr` requires none). */
6
7
  prompt?: string;
8
+ /** Whether the viewer plays the initial-whip animation for `prompt`. Defaults
9
+ * to "the prompt is non-empty"; the front door passes false for a prompt the
10
+ * runtime produced rather than the user typing it. */
11
+ initialWhip?: boolean;
7
12
  /** Explicit `--profile <id-or-name>` on the front-door invocation, resolved
8
13
  * through `resolveProfileOperand` — so a near-miss name still starts the
9
14
  * session, under the closest profile. Omit to run the MRU/create-or-root-profile
@@ -18,6 +23,8 @@ export interface BootRootOpts {
18
23
  * this a pinned default auto-picks silently (that is what "default" means). */
19
24
  pickProfile?: boolean;
20
25
  }
26
+ /** The root-create request the front door sends to crtrd. */
27
+ export declare function rootCreateRequest(opts: BootRootOpts, profileId: string): Parameters<ReturnType<typeof cliClient>['createNode']>[0];
21
28
  /** Boot the front-door root: create the resident root through crtrd (broker
22
29
  * launched + view socket ready server-side), register THIS terminal's pane as
23
30
  * its foreground viewer, then exec `crtr surface attach` inline so this terminal
@@ -1,5 +1,5 @@
1
- var l=Object.defineProperty;var s=(e,r)=>l(e,"name",{value:r,configurable:!0});import{spawn as p}from"node:child_process";import{inTmux as u,currentTmux as _,viewerSplitEnv as T}from"./placement-tmux.js";import{selectProfileForCwd as x}from"../profiles/select.js";import{cliClient as m}from"../../commands/api-client.js";import{mark as i}from"../timing.js";async function P(e){if(!u()){const t=process.stderr.isTTY?n=>`\x1B[1;33m${n}\x1B[0m`:n=>n;process.stderr.write(`
2
- ${t("crtr must be started from inside a tmux session.")}
3
- ${t("Start tmux first, e.g. tmux new -s work \u2014 then run crtr there.")}
1
+ var p=Object.defineProperty;var d=(e,o)=>p(e,"name",{value:o,configurable:!0});import{spawn as u}from"node:child_process";import{inTmux as w,currentTmux as _,viewerSplitEnv as T}from"./placement-tmux.js";import{selectProfileForCwd as x}from"../profiles/select.js";import{cliClient as f}from"../../commands/api-client.js";import{mark as a}from"../timing.js";function h(e,o){const t=e.kind;return{...t!==void 0?{kind:t}:{},mode:"base",prompt:e.prompt,profile:o,cwd:e.cwd,...e.name!==void 0?{name:e.name}:t!==void 0?{name:t}:{},root:!0,prefer_warm:!0}}d(h,"rootCreateRequest");async function P(e){if(!w()){const n=process.stderr.isTTY?i=>`\x1B[1;33m${i}\x1B[0m`:i=>i;process.stderr.write(`
2
+ ${n("crtr must be started from inside a tmux session.")}
3
+ ${n("Start tmux first, e.g. tmux new -s work \u2014 then run crtr there.")}
4
4
 
5
- `),process.exit(1)}const r=e.kind;i("boot_root.start");const f=e.selectedProfileId??await x(e.cwd,e.profile,e.pickProfile??!1);i("boot_root.profile_resolved");const d=(await m().createNode({...r!==void 0?{kind:r}:{},mode:"base",prompt:e.prompt,profile:f,cwd:e.cwd,...e.name!==void 0?{name:e.name}:r!==void 0?{name:r}:{},root:!0,prefer_warm:!0})).node_id;i("boot_root.node_created",{node:d});const o=_();if(o!==null)try{await m().registerFocus({node_id:d,pane:o.pane,session:o.session,window:o.window})}catch{}const a={...process.env,...T(),CRTR_ATTACH_WAIT_MS:"30000",...e.prompt!==void 0&&e.prompt!==""?{CRTR_ATTACH_INITIAL_WHIP:"1"}:{}};o!==null&&(a.CRTR_ATTACH_BORROWED_PANE=o.pane),i("boot_root.viewer_spawn");const c=p("crtr",["surface","attach","to",d],{stdio:"inherit",env:a}),w=await new Promise(t=>{c.once("exit",n=>t(n??0)),c.once("error",()=>t(1))});process.exit(w)}s(P,"bootRoot");export{P as bootRoot};
5
+ `),process.exit(1)}a("boot_root.start");const o=e.selectedProfileId??await x(e.cwd,e.profile,e.pickProfile??!1);a("boot_root.profile_resolved");const c=(await f().createNode(h(e,o))).node_id;a("boot_root.node_created",{node:c});const r=_();if(r!==null)try{await f().registerFocus({node_id:c,pane:r.pane,session:r.session,window:r.window})}catch{}const s={...process.env,...T(),CRTR_ATTACH_WAIT_MS:"30000",...e.initialWhip??(e.prompt!==void 0&&e.prompt!=="")?{CRTR_ATTACH_INITIAL_WHIP:"1"}:{}};r!==null&&(s.CRTR_ATTACH_BORROWED_PANE=r.pane),a("boot_root.viewer_spawn");const m=u("crtr",["surface","attach","to",c],{stdio:"inherit",env:s}),l=await new Promise(n=>{m.once("exit",i=>n(i??0)),m.once("error",()=>n(1))});process.exit(l)}d(P,"bootRoot");export{P as bootRoot,h as rootCreateRequest};
@@ -0,0 +1,11 @@
1
+ export declare const FIRST_RUN_OFFER_SENTENCE = "Want a quick tour of how this works? Say yes, or just tell me what you're working on.";
2
+ /** The prompt the front door gives a freshly booted root, and whether the
3
+ * viewer should play the initial-whip animation (only for a prompt the user
4
+ * typed). */
5
+ export declare function frontDoorBootPrompt(facts: {
6
+ setupJustRan: boolean;
7
+ userPrompt: string | undefined;
8
+ }): {
9
+ prompt: string | undefined;
10
+ initialWhip: boolean;
11
+ };
@@ -0,0 +1,5 @@
1
+ var n=Object.defineProperty;var e=(t,r)=>n(t,"name",{value:r,configurable:!0});import{formatCard as o}from"../../shared/generated-context.js";const i="Want a quick tour of how this works? Say yes, or just tell me what you're working on.",s=o("first-run",{},`Open this conversation with exactly this sentence and nothing else:
2
+
3
+ ${i}
4
+
5
+ If the user says yes, run \`crtr sys tutorial -h\`, pick the track it names, and teach it one lesson per invocation \u2014 in your own words, letting them try each thing before moving on. If the user says anything else, drop the tour entirely and do what they asked.`);function h(t){return t.userPrompt!==void 0&&t.userPrompt.trim()!==""?{prompt:t.userPrompt,initialWhip:!0}:t.setupJustRan?{prompt:s,initialWhip:!1}:{prompt:void 0,initialWhip:!1}}e(h,"frontDoorBootPrompt");export{i as FIRST_RUN_OFFER_SENTENCE,h as frontDoorBootPrompt};
@@ -1,9 +1,9 @@
1
- var E=Object.defineProperty;var p=(r,i)=>E(r,"name",{value:i,configurable:!0});import{existsSync as w,statSync as F}from"node:fs";import{homedir as v}from"node:os";import{join as I,resolve as m}from"node:path";import{SUBTREE_NAMES as _}from"../../build-root.js";import{FRONT_DOOR_ENV as N}from"./front-door-env.js";function g(r){try{return w(r)&&F(r).isDirectory()}catch{return!1}}p(g,"isDir");const A=new Set(["--name","--kind","--profile","--pick-profile","--continue","-c","--resume","-r"]),h=I(v(),".crouter","setup-completed.json");async function C(){let r=!1;const i=p(()=>{r=!0},"onSigint");process.once("SIGINT",i);try{const{runSetup:e}=await import("../../commands/sys/setup-wizard.js"),t=await e();if(r){const c=new Error("Interrupted");throw c.name="AbortError",c}const[{atomicWriteJson:a},{crtrVersion:f}]=await Promise.all([import("../fs-utils.js"),import("../../shared/crtr-version.js")]);a(h,{packageVersion:f(),completedAt:new Date().toISOString(),outcome:t})}finally{process.removeListener("SIGINT",i)}}p(C,"runFirstSetup");function L(r){let i=process.cwd(),e,t,a,f=!1,c;const l=[];for(let n=0;n<r.length;n++){const o=r[n];o==="--name"?e=r[++n]:o==="--kind"?t=r[++n]:o==="--profile"?a=r[++n]:o==="--pick-profile"?f=!0:o==="--continue"||o==="-c"?c="continue":o==="--resume"||o==="-r"?c="pick":o.startsWith("--")||l.push(o)}l.length>0&&g(m(l[0]))&&(i=m(l.shift()));const d=l.length>0?l.join(" "):void 0;return{cwd:i,prompt:d,name:e,kind:t,profile:a,pickProfile:f,resume:c}}p(L,"parseRootArgs");import{FRONT_DOOR_ENV as Y}from"./front-door-env.js";async function B(r){const i=r.slice(2),e=i[0];if(process.env[N]||e==="-h"||e==="--help"||e==="--version"||e==="-v"||e!==void 0&&_.includes(e))return!1;if(e!==void 0){const s=/\s/.test(e),u=!e.startsWith("-")&&g(m(e)),T=A.has(e);if(!s&&!u&&!T)return!1;if(u){const{buildRoot:y}=await import("../../build-root.js");if((await y()).subtrees.some(D=>D.name===e))return!1}}const t=L(i);if(t.profile!==void 0){const{resolveProfileOperand:s}=await import("../profiles/manifest.js");t.profile=s(t.profile).profileId}const a=process.stdin.isTTY&&process.stdout.isTTY;let f=!1;a&&!w(h)&&(await C(),f=!0);const{selectProfileForCwd:c,selectProfileForCwdReadOnly:l}=await import("../profiles/select.js");f&&t.profile===void 0&&process.stdout.write(`
1
+ var v=Object.defineProperty;var p=(r,i)=>v(r,"name",{value:i,configurable:!0});import{existsSync as w,statSync as I}from"node:fs";import{homedir as _}from"node:os";import{join as N,resolve as d}from"node:path";import{SUBTREE_NAMES as A}from"../../build-root.js";import{FRONT_DOOR_ENV as C}from"./front-door-env.js";import{frontDoorBootPrompt as L}from"./first-run-offer.js";function h(r){try{return w(r)&&I(r).isDirectory()}catch{return!1}}p(h,"isDir");const W=new Set(["--name","--kind","--profile","--pick-profile","--continue","-c","--resume","-r"]),g=N(_(),".crouter","setup-completed.json");async function b(){let r=!1;const i=p(()=>{r=!0},"onSigint");process.once("SIGINT",i);try{const{runSetup:t}=await import("../../commands/sys/setup-wizard.js"),e=await t();if(r){const l=new Error("Interrupted");throw l.name="AbortError",l}const[{atomicWriteJson:a},{crtrVersion:c}]=await Promise.all([import("../fs-utils.js"),import("../../shared/crtr-version.js")]);a(g,{packageVersion:c(),completedAt:new Date().toISOString(),outcome:e})}finally{process.removeListener("SIGINT",i)}}p(b,"runFirstSetup");function x(r){let i=process.cwd(),t,e,a,c=!1,l;const f=[];for(let n=0;n<r.length;n++){const o=r[n];o==="--name"?t=r[++n]:o==="--kind"?e=r[++n]:o==="--profile"?a=r[++n]:o==="--pick-profile"?c=!0:o==="--continue"||o==="-c"?l="continue":o==="--resume"||o==="-r"?l="pick":o.startsWith("--")||f.push(o)}f.length>0&&h(d(f[0]))&&(i=d(f.shift()));const m=f.length>0?f.join(" "):void 0;return{cwd:i,prompt:m,name:t,kind:e,profile:a,pickProfile:c,resume:l}}p(x,"parseRootArgs");import{FRONT_DOOR_ENV as z}from"./front-door-env.js";async function $(r){const i=r.slice(2),t=i[0];if(process.env[C]||t==="-h"||t==="--help"||t==="--version"||t==="-v"||t!==void 0&&A.includes(t))return!1;if(t!==void 0){const s=/\s/.test(t),u=!t.startsWith("-")&&h(d(t)),y=W.has(t);if(!s&&!u&&!y)return!1;if(u){const{buildRoot:E}=await import("../../build-root.js");if((await E()).subtrees.some(F=>F.name===t))return!1}}const e=x(i);if(e.profile!==void 0){const{resolveProfileOperand:s}=await import("../profiles/manifest.js");e.profile=s(e.profile).profileId}const a=process.stdin.isTTY&&process.stdout.isTTY;let c=!1;a&&!w(g)&&(await b(),c=!0);const{selectProfileForCwd:l,selectProfileForCwdReadOnly:f}=await import("../profiles/select.js");c&&e.profile===void 0&&process.stdout.write(`
2
2
  Choose a profile
3
- `);const d=t.profile??(a?await c(t.cwd,void 0,t.pickProfile):l(t.cwd)),{exitForMissingProviderCredential:n,hasConfiguredProviderCredential:o}=await import("./provider-credential-gate.js");if(!await o(d)){a||n();const{runFrontDoorLogin:s}=await import("./front-door-login.js");await s()||n()}const[{bootRoot:R},{resumeRoot:P},{rendererWarning:k,missingPackageWarning:S,piAgentDir:O}]=await Promise.all([import("./boot-root.js"),import("./resume-root.js"),import("./package-health.js")]);try{const s=S(O());s!==null&&process.stderr.write(`
3
+ `);const m=e.profile??(a?await l(e.cwd,void 0,e.pickProfile):f(e.cwd)),{exitForMissingProviderCredential:n,hasConfiguredProviderCredential:o}=await import("./provider-credential-gate.js");if(!await o(m)){a||n();const{runFrontDoorLogin:s}=await import("./front-door-login.js");await s()||n()}const[{bootRoot:P},{resumeRoot:R},{rendererWarning:k,missingPackageWarning:S,piAgentDir:O}]=await Promise.all([import("./boot-root.js"),import("./resume-root.js"),import("./package-health.js")]);try{const s=S(O());s!==null&&process.stderr.write(`
4
4
  ${s}
5
5
 
6
6
  `);const u=k();u!==null&&process.stderr.write(`
7
7
  ${u}
8
8
 
9
- `)}catch{}return t.resume!==void 0&&await P(t.cwd,t.resume==="pick")==="aborted"||await R({...t,selectedProfileId:d}),!0}p(B,"maybeBootRoot");export{Y as FRONT_DOOR_ENV,B as maybeBootRoot};
9
+ `)}catch{}if(e.resume!==void 0&&await R(e.cwd,e.resume==="pick")==="aborted")return!0;const{prompt:D,initialWhip:T}=L({setupJustRan:c,userPrompt:e.prompt});return await P({...e,prompt:D,initialWhip:T,selectedProfileId:m}),!0}p($,"maybeBootRoot");export{z as FRONT_DOOR_ENV,$ as maybeBootRoot};
@@ -140,5 +140,7 @@ export declare function spawnChild(opts: SpawnChildOpts): Promise<SpawnChildResu
140
140
  export declare function invokeBirthLifecycleHooks(meta: NodeMeta): Promise<void>;
141
141
  /** Create an independent, idle root from a node's conversation and durable
142
142
  * context. Runtime/graph state is intentionally not inherited. */
143
- export declare function forkNode(sourceId: string): Promise<SpawnChildResult>;
143
+ /** Fork `sourceId` into a fresh root. `scopes` is the fork's allow-list; omit
144
+ * for a root with no ceiling. */
145
+ export declare function forkNode(sourceId: string, scopes?: string[]): Promise<SpawnChildResult>;
144
146
  export {};
@@ -1,3 +1,3 @@
1
- var Q=Object.defineProperty;var s=(e,o)=>Q(e,"name",{value:o,configurable:!0});import{accessSync as W,constants as A,cpSync as Y,readdirSync as D,existsSync as Z,rmSync as ee}from"node:fs";import{isAbsolute as ne,resolve as oe,join as g}from"node:path";import{spawnNode as re,currentNodeContext as ie,rootOfSpine as te,newNodeId as de,preflightNodeId as ae}from"./nodes.js";import{resolveProfileOperand as se}from"../profiles/manifest.js";import{selectProfileForCwd as ce,selectProfileForCwdReadOnly as le}from"../profiles/select.js";import{buildLaunchSpecAsync as M,buildPiArgv as fe}from"./launch.js";import{createManagedWorktreeAsync as ue,rollbackManagedWorktreeAsync as me}from"../worktree-mutation-async.js";import{usage as f,brokerLaunchFailed as E}from"../errors.js";import{writeGoal as he}from"./kickoff.js";import{appendSituationalContext as we,formatSituationalProse as pe}from"./situational-context.js";import{hasRoadmap as ke,seedRoadmap as _e}from"./roadmap.js";import{buildWakeBearings as ye}from"./bearings.js";import{encodeKickoffOrigin as ve,KICKOFF_ORIGIN_ENV as ge}from"./stamp/protocol.js";import{beginExecution as be,canonicalSessionFile as I,contextDir as B,crtrHome as xe,findNodeBySessionFile as Fe,getNode as w,fullName as H,jobDir as Oe,nodeDir as Ce,recordExecutionPid as Se,abortExecution as P,reportsDir as $e,setFrozen as Ee}from"../canvas/index.js";import{boundFleet as Re,brokerThresholdsForDaemon as De}from"./fleet.js";import{emitEvent as Ie}from"../events/emit.js";import{openViewerWindow as Be,focusOf as Pe,windowOfPane as je}from"./placement.js";import{waitForBrokerViewSocket as Ne}from"./placement-tmux.js";import{transition as b}from"./lifecycle.js";import{beginBootFaultAttempt as Te}from"../canvas/recovery.js";import{piSessionsRoot as Le}from"./pi-vendored.js";import{headlessBrokerHost as j}from"./host.js";import{ensureDaemon as We}from"../../daemon/manage.js";import{readConfig as Ae}from"../config.js";import{modelRequestFromConfig as Me,unregisteredConcreteModel as He}from"../model-routes.js";import{openModelRegistry as Ke}from"./model-registry.js";import{MODEL_SPEC_NEXT as ze}from"./model-selection.js";import{initializeForkContextExposure as qe}from"../substrate/injected-store.js";import{discoverLifecycleHookRegistry as Ue}from"../command-hooks/discovery.js";import{invokeLifecycleHooks as Ve}from"../command-hooks/transport/exec-lifecycle.js";function x(e){return{detail:{schema:"crtr.node-outcome-detail/v1",message:e instanceof Error?e.message:String(e)}}}s(x,"launchFailureOutcome");function Xe(e,o,d){try{return j.launch(e,o,d)}catch(r){try{b(e,"crash",{reason:"launch_failed",outcome:x(r)})}catch{}const t=r.message;throw E(t.startsWith("[broker]")?t:`[broker] refusing to launch ${e}: ${t}`,"Inspect the cause above, fix the launch recipe or environment, then retry.")}}s(Xe,"launchBrokerOrCrash");function Ge(e,o){try{b(e,"crash",{reason:"launch_failed",outcome:x(o)})}catch{}try{j.teardown(e)}catch{}}s(Ge,"crashAndTearDown");function Je(e){const o=Le();if(!Z(o))throw f(`--fork-from '${e}': no pi sessions store at ${o} to resolve it from \u2014 pass a node id or an absolute .jsonl path.`,{next:"Pass a live node id, an absolute .jsonl session path, or drop --fork-from."});const d=[];for(const a of D(o,{withFileTypes:!0})){if(!a.isDirectory())continue;const c=g(o,a.name);for(const l of D(c)){if(!l.endsWith(".jsonl"))continue;const F=l.slice(0,-6),u=F.slice(F.lastIndexOf("_")+1);d.push({id:u,path:g(c,l)})}}const r=d.filter(a=>a.id===e),t=r.length>0?r:d.filter(a=>a.id.startsWith(e));if(t.length===0)throw f(`--fork-from '${e}': no pi session with that id found.`,{next:"Pass a live node id, a real session uuid/prefix, or an absolute .jsonl session path."});if(t.length>1)throw f(`--fork-from '${e}': ambiguous \u2014 ${t.length} sessions match that id prefix; pass the full id or the .jsonl path.`,{next:"Pass the full session uuid or an absolute .jsonl path to disambiguate."});return I(t[0].path)}s(Je,"resolveSessionUuid");function K(e){const o=e.trim();if(o==="")throw f("--fork-from requires a node id, session file, or session uuid.",{next:"Pass a live node id, an absolute .jsonl session path, or a session uuid/prefix."});if(o.includes("/")||o.endsWith(".jsonl"))return I(ne(o)?o:oe(process.cwd(),o));const d=w(o);if(d!==null){const r=d.pi_session_file;if(r==null||r==="")throw f(`node ${o} has no pi session yet \u2014 it has not captured a session FILE to fork from (the broker forks a .jsonl path, not a bare id).`,{next:"Pick a node that has captured a session, or drop --fork-from."});return I(r)}return Je(o)}s(K,"resolveForkSource");async function Qe(e,o,d,r={}){return e===null?null:e!==void 0&&e!==""?se(e).profileId:o!==null?w(o)?.profile_id??null:r.readOnlySelection===!0?le(d):ce(d)}s(Qe,"resolveProfileId");function z(e){const o=e.modelIntent===void 0?void 0:`${e.modelIntent.family}/${e.modelIntent.strength}`;return Me(e.model,o,e.modelExact===!0)}s(z,"requestFromLaunch");async function q(e,o,d){if(e===void 0||e.kind==="logical")return;const r=He(e,d??await Ke(),o);if(r!==void 0)throw f(`model '${r.request.spec}' is not registered in this daemon.`,{received:r.request.spec,next:ze})}s(q,"assertLaunchModelRegistered");function Ye(e,o,d){const r=e??o??null;if(!d&&r===null)throw new Error("spawnChild requires a calling node (CRTR_NODE_ID) or an explicit parent");return r}s(Ye,"resolveSpawner");async function U(e,o,d){try{We()}catch{}const r=ie(),t=e.root===!0,a=Ye(e.parent,r.nodeId??null,t),c=e.mode??"base",l=e.rootLifecycle??(t?"resident":"terminal"),F=t&&l==="resident",u=e.worktree===!0,p=u?e.nodeId??de():e.nodeId;e.nodeId!==void 0&&ae(e.nodeId);const k=await Qe(e.profile,a,e.cwd),N=(await M(e.kind,c,{lifecycle:l,hasManager:!t,model:e.model,modelOverride:e.model!==void 0,cwd:e.cwd,profileId:k})).launch;await q(z(N),{cwd:e.cwd,profileId:k});const O=e.forkFrom!==void 0?K(e.forkFrom):void 0,X=O===void 0?null:Fe(O)?.node_id??null;let m,C=e.cwd;try{u&&(m=await ue(e.worktreeCwd??e.cwd,p,e.worktreeBase),C=m.path);const _=u?(await M(e.kind,c,{lifecycle:l,hasManager:!t,model:e.model,modelOverride:e.model!==void 0,cwd:C,profileId:k})).launch:N;u&&await q(z(_),{cwd:C,profileId:k});const n=await re({kind:e.kind,mode:c,lifecycle:l,cwd:C,name:e.name??e.kind,description:e.description,parent:t?null:a,creator:e.creator,spawnedBy:t?a??void 0:void 0,profile_id:k,...e.scopes===void 0?{}:{scopes:e.scopes},forkFrom:e.forkFrom,forkSourceFile:O,modelOverride:e.model===void 0?void 0:_.model,...m!==void 0?{managedWorktree:m}:{},...p!==void 0?{nodeId:p}:{},...e.reviewBinding===void 0?{}:{reviewBinding:e.reviewBinding},...e.deadlineAt===void 0?{}:{deadlineAt:e.deadlineAt},...e.outcomeDelivery===void 0?{}:{outcomeDelivery:e.outcomeDelivery},launch:_});if(d?.(n.node_id),e.forkFrom!==void 0)try{qe(n.node_id,X)}catch(i){throw b(n.node_id,"crash",{reason:"launch_failed",outcome:x(i)}),i}if(he(n.node_id,e.prompt),e.situationalContext!==void 0&&we(n.node_id,pe(e.situationalContext)),o!==void 0)try{o(n.node_id)}catch(i){throw b(n.node_id,"crash",{reason:"launch_failed",outcome:x(i)}),i}const R=e.prompt!=="",G=`${R&&e.wakeOrigin!==void 0?`${ye(e.wakeOrigin)}
1
+ var Q=Object.defineProperty;var c=(e,o)=>Q(e,"name",{value:o,configurable:!0});import{accessSync as W,constants as A,cpSync as Y,readdirSync as D,existsSync as Z,rmSync as ee}from"node:fs";import{isAbsolute as ne,resolve as oe,join as g}from"node:path";import{spawnNode as re,currentNodeContext as ie,rootOfSpine as te,newNodeId as de,preflightNodeId as ae}from"./nodes.js";import{resolveProfileOperand as se}from"../profiles/manifest.js";import{selectProfileForCwd as ce,selectProfileForCwdReadOnly as le}from"../profiles/select.js";import{buildLaunchSpecAsync as M,buildPiArgv as fe}from"./launch.js";import{createManagedWorktreeAsync as ue,rollbackManagedWorktreeAsync as me}from"../worktree-mutation-async.js";import{usage as f,brokerLaunchFailed as E}from"../errors.js";import{writeGoal as he}from"./kickoff.js";import{appendSituationalContext as we,formatSituationalProse as pe}from"./situational-context.js";import{hasRoadmap as ke,seedRoadmap as _e}from"./roadmap.js";import{buildWakeBearings as ye}from"./bearings.js";import{encodeKickoffOrigin as ve,KICKOFF_ORIGIN_ENV as ge}from"./stamp/protocol.js";import{beginExecution as be,canonicalSessionFile as I,contextDir as B,crtrHome as xe,findNodeBySessionFile as Fe,getNode as w,fullName as H,jobDir as Oe,nodeDir as Ce,recordExecutionPid as Se,abortExecution as P,reportsDir as $e,setFrozen as Ee}from"../canvas/index.js";import{boundFleet as Re,brokerThresholdsForDaemon as De}from"./fleet.js";import{emitEvent as Ie}from"../events/emit.js";import{openViewerWindow as Be,focusOf as Pe,windowOfPane as je}from"./placement.js";import{waitForBrokerViewSocket as Ne}from"./placement-tmux.js";import{transition as b}from"./lifecycle.js";import{beginBootFaultAttempt as Te}from"../canvas/recovery.js";import{piSessionsRoot as Le}from"./pi-vendored.js";import{headlessBrokerHost as j}from"./host.js";import{ensureDaemon as We}from"../../daemon/manage.js";import{readConfig as Ae}from"../config.js";import{modelRequestFromConfig as Me,unregisteredConcreteModel as He}from"../model-routes.js";import{openModelRegistry as Ke}from"./model-registry.js";import{MODEL_SPEC_NEXT as ze}from"./model-selection.js";import{initializeForkContextExposure as qe}from"../substrate/injected-store.js";import{discoverLifecycleHookRegistry as Ue}from"../command-hooks/discovery.js";import{invokeLifecycleHooks as Ve}from"../command-hooks/transport/exec-lifecycle.js";function x(e){return{detail:{schema:"crtr.node-outcome-detail/v1",message:e instanceof Error?e.message:String(e)}}}c(x,"launchFailureOutcome");function Xe(e,o,r){try{return j.launch(e,o,r)}catch(i){try{b(e,"crash",{reason:"launch_failed",outcome:x(i)})}catch{}const d=i.message;throw E(d.startsWith("[broker]")?d:`[broker] refusing to launch ${e}: ${d}`,"Inspect the cause above, fix the launch recipe or environment, then retry.")}}c(Xe,"launchBrokerOrCrash");function Ge(e,o){try{b(e,"crash",{reason:"launch_failed",outcome:x(o)})}catch{}try{j.teardown(e)}catch{}}c(Ge,"crashAndTearDown");function Je(e){const o=Le();if(!Z(o))throw f(`--fork-from '${e}': no pi sessions store at ${o} to resolve it from \u2014 pass a node id or an absolute .jsonl path.`,{next:"Pass a live node id, an absolute .jsonl session path, or drop --fork-from."});const r=[];for(const a of D(o,{withFileTypes:!0})){if(!a.isDirectory())continue;const l=g(o,a.name);for(const s of D(l)){if(!s.endsWith(".jsonl"))continue;const F=s.slice(0,-6),u=F.slice(F.lastIndexOf("_")+1);r.push({id:u,path:g(l,s)})}}const i=r.filter(a=>a.id===e),d=i.length>0?i:r.filter(a=>a.id.startsWith(e));if(d.length===0)throw f(`--fork-from '${e}': no pi session with that id found.`,{next:"Pass a live node id, a real session uuid/prefix, or an absolute .jsonl session path."});if(d.length>1)throw f(`--fork-from '${e}': ambiguous \u2014 ${d.length} sessions match that id prefix; pass the full id or the .jsonl path.`,{next:"Pass the full session uuid or an absolute .jsonl path to disambiguate."});return I(d[0].path)}c(Je,"resolveSessionUuid");function K(e){const o=e.trim();if(o==="")throw f("--fork-from requires a node id, session file, or session uuid.",{next:"Pass a live node id, an absolute .jsonl session path, or a session uuid/prefix."});if(o.includes("/")||o.endsWith(".jsonl"))return I(ne(o)?o:oe(process.cwd(),o));const r=w(o);if(r!==null){const i=r.pi_session_file;if(i==null||i==="")throw f(`node ${o} has no pi session yet \u2014 it has not captured a session FILE to fork from (the broker forks a .jsonl path, not a bare id).`,{next:"Pick a node that has captured a session, or drop --fork-from."});return I(i)}return Je(o)}c(K,"resolveForkSource");async function Qe(e,o,r,i={}){return e===null?null:e!==void 0&&e!==""?se(e).profileId:o!==null?w(o)?.profile_id??null:i.readOnlySelection===!0?le(r):ce(r)}c(Qe,"resolveProfileId");function z(e){const o=e.modelIntent===void 0?void 0:`${e.modelIntent.family}/${e.modelIntent.strength}`;return Me(e.model,o,e.modelExact===!0)}c(z,"requestFromLaunch");async function q(e,o,r){if(e===void 0||e.kind==="logical")return;const i=He(e,r??await Ke(),o);if(i!==void 0)throw f(`model '${i.request.spec}' is not registered in this daemon.`,{received:i.request.spec,next:ze})}c(q,"assertLaunchModelRegistered");function Ye(e,o,r){const i=e??o??null;if(!r&&i===null)throw new Error("spawnChild requires a calling node (CRTR_NODE_ID) or an explicit parent");return i}c(Ye,"resolveSpawner");async function U(e,o,r){try{We()}catch{}const i=ie(),d=e.root===!0,a=Ye(e.parent,i.nodeId??null,d),l=e.mode??"base",s=e.rootLifecycle??(d?"resident":"terminal"),F=d&&s==="resident",u=e.worktree===!0,p=u?e.nodeId??de():e.nodeId;e.nodeId!==void 0&&ae(e.nodeId);const k=await Qe(e.profile,a,e.cwd),N=(await M(e.kind,l,{lifecycle:s,hasManager:!d,model:e.model,modelOverride:e.model!==void 0,cwd:e.cwd,profileId:k})).launch;await q(z(N),{cwd:e.cwd,profileId:k});const O=e.forkFrom!==void 0?K(e.forkFrom):void 0,X=O===void 0?null:Fe(O)?.node_id??null;let m,C=e.cwd;try{u&&(m=await ue(e.worktreeCwd??e.cwd,p,e.worktreeBase),C=m.path);const _=u?(await M(e.kind,l,{lifecycle:s,hasManager:!d,model:e.model,modelOverride:e.model!==void 0,cwd:C,profileId:k})).launch:N;u&&await q(z(_),{cwd:C,profileId:k});const n=await re({kind:e.kind,mode:l,lifecycle:s,cwd:C,name:e.name??e.kind,description:e.description,parent:d?null:a,creator:e.creator,spawnedBy:d?a??void 0:void 0,profile_id:k,...e.scopes===void 0?{}:{scopes:e.scopes},forkFrom:e.forkFrom,forkSourceFile:O,modelOverride:e.model===void 0?void 0:_.model,...m!==void 0?{managedWorktree:m}:{},...p!==void 0?{nodeId:p}:{},...e.reviewBinding===void 0?{}:{reviewBinding:e.reviewBinding},...e.deadlineAt===void 0?{}:{deadlineAt:e.deadlineAt},...e.outcomeDelivery===void 0?{}:{outcomeDelivery:e.outcomeDelivery},launch:_});if(r?.(n.node_id),e.forkFrom!==void 0)try{qe(n.node_id,X)}catch(t){throw b(n.node_id,"crash",{reason:"launch_failed",outcome:x(t)}),t}if(he(n.node_id,e.prompt),e.situationalContext!==void 0&&we(n.node_id,pe(e.situationalContext)),o!==void 0)try{o(n.node_id)}catch(t){throw b(n.node_id,"crash",{reason:"launch_failed",outcome:x(t)}),t}const R=e.prompt!=="",G=`${R&&e.wakeOrigin!==void 0?`${ye(e.wakeOrigin)}
2
2
 
3
- `:""}${e.prompt}`;c==="orchestrator"&&!ke(n.node_id)&&_e(n.node_id,{goal:e.prompt.trim()}),await Ze(n);const h=fe(n,{prompt:G,forkFrom:O});if(h.env={...h.env,CRTR_SUBTREE:te(n.node_id)},R){const i=e.wakeOrigin!==void 0?{type:"runtime",kind:"wake",wake:e.wakeOrigin}:{type:"runtime",kind:"kickoff",...n.spawned_by!=null&&n.spawned_by!==""?{spawnedBy:n.spawned_by}:{}};h.env[ge]=ve(i)}const S=Re();if(!S.reserveSlot(n.node_id))return Ee(n.node_id,new Date().toISOString()),Ie({level:"info",event:"broker.frozen",node_id:n.node_id,fields:{live_count:S.size(),hard_cap:De().automaticReviveCap}}),{node:w(n.node_id),frozen:!0,window:null,session:null};const J=Te(n.node_id),y=be(n.node_id,{daemonEpoch:S.epoch()});h.env={...h.env,CRTR_EXECUTION_ID:y};let $;try{$=Xe(n.node_id,h,{cwd:n.cwd,name:H(n),resuming:!1})}catch(i){throw P(y,{reason:"launch_failed"}),i}finally{S.releaseReservation(n.node_id)}if($.pid===null){const i=new Error(`broker host returned no pid for ${n.node_id}`);throw P(y,{reason:"launch_failed"}),b(n.node_id,"crash",{reason:"launch_failed",outcome:x(i)}),E(`failed to launch the broker engine for ${n.node_id} (${n.name}) \u2014 the node was not started.`,`Inspect canonical event diagnostics with \`crtr sys logs --node ${n.node_id}\`, then verify the pi engine can start in this environment.`)}if(Se(y,n.node_id,$.pid)==="shutdown")throw P(y,{reason:"superseded"}),j.teardown(n.node_id),E(`broker execution was superseded for ${n.node_id}`,"Retry spawning the node.");if(F&&e.readiness==="await"&&!await Ne(n.node_id,$.exited)){const i=J.explanation();throw Ge(n.node_id,new Error(i??`broker view socket never bound for ${n.node_id}`)),E(i!==null?`the root broker ${n.node_id} could not start \u2014 ${i}`:`the root broker ${n.node_id} never bound its view socket \u2014 it was not started.`,i!==null?"Resolve the cause named above, then retry.":`The broker engine exited before binding its view socket. Inspect canonical event diagnostics with \`crtr sys logs --node ${n.node_id}\`, then retry.`)}let T=null,L=null;if(!t&&a!==null&&Ae("user").auto_open_child_viewers)try{const i=Pe(a);if(i!==null&&i.pane!==null&&i.pane!==""){const v=Be(n.node_id,i.session??"",{name:H(n),cwd:e.cwd,besidePane:i.pane,waitForBroker:!0,initialWhip:R});v!==null&&v.pane!==null&&v.pane!==""&&(L=v.session,T=je(v.pane))}}catch{}return{node:w(n.node_id),frozen:!1,window:T,session:L}}catch(_){throw m!==void 0&&p!==void 0&&w(p)===null&&await me(m),_}}s(U,"spawnChildPrepared");async function Pn(e){return U(e)}s(Pn,"spawnChild");async function Ze(e){const o=Ue(e.cwd,e.profile_id).plans.get("node:start")??[];await Ve(o,{node:{id:e.node_id,name:e.name,kind:e.kind,mode:e.mode,lifecycle:e.lifecycle,cwd:e.cwd,nodeDir:Ce(e.node_id),contextDir:B(e.node_id),jobDir:Oe(e.node_id),reportsDir:$e(e.node_id)},runtime:{isBirth:!0,canvasHome:xe(),profile:e.profile_id??null}})}s(Ze,"invokeBirthLifecycleHooks");const V=new Set(["initial-prompt.md","yield-message.md","result.json"]);async function jn(e){const o=w(e);if(o===null)throw f(`unknown node: ${e}`,{received:e,next:"List nodes and pass the id of a node with a captured pi session."});const d=K(e),r=B(e);try{W(d,A.R_OK),W(r,A.R_OK)}catch(t){throw new Error(`cannot fork ${e}: its conversation or context is unreadable \u2014 ${t.message}`)}return U({kind:o.kind,mode:"base",cwd:o.cwd,prompt:"",root:!0,readiness:"await",forkFrom:e,profile:o.profile_id,model:o.model_override??void 0},t=>{const a=B(t);for(const c of D(r,{withFileTypes:!0}))V.has(c.name)||Y(g(r,c.name),g(a,c.name),{recursive:!0,dereference:!1,force:!0});for(const c of V)ee(g(a,c),{recursive:!0,force:!0})})}s(jn,"forkNode");export{q as assertLaunchModelRegistered,jn as forkNode,Ze as invokeBirthLifecycleHooks,K as resolveForkSource,Qe as resolveProfileId,Ye as resolveSpawner,Pn as spawnChild,U as spawnChildPrepared};
3
+ `:""}${e.prompt}`;l==="orchestrator"&&!ke(n.node_id)&&_e(n.node_id,{goal:e.prompt.trim()}),await Ze(n);const h=fe(n,{prompt:G,forkFrom:O});if(h.env={...h.env,CRTR_SUBTREE:te(n.node_id)},R){const t=e.wakeOrigin!==void 0?{type:"runtime",kind:"wake",wake:e.wakeOrigin}:{type:"runtime",kind:"kickoff",...n.spawned_by!=null&&n.spawned_by!==""?{spawnedBy:n.spawned_by}:{}};h.env[ge]=ve(t)}const S=Re();if(!S.reserveSlot(n.node_id))return Ee(n.node_id,new Date().toISOString()),Ie({level:"info",event:"broker.frozen",node_id:n.node_id,fields:{live_count:S.size(),hard_cap:De().automaticReviveCap}}),{node:w(n.node_id),frozen:!0,window:null,session:null};const J=Te(n.node_id),y=be(n.node_id,{daemonEpoch:S.epoch()});h.env={...h.env,CRTR_EXECUTION_ID:y};let $;try{$=Xe(n.node_id,h,{cwd:n.cwd,name:H(n),resuming:!1})}catch(t){throw P(y,{reason:"launch_failed"}),t}finally{S.releaseReservation(n.node_id)}if($.pid===null){const t=new Error(`broker host returned no pid for ${n.node_id}`);throw P(y,{reason:"launch_failed"}),b(n.node_id,"crash",{reason:"launch_failed",outcome:x(t)}),E(`failed to launch the broker engine for ${n.node_id} (${n.name}) \u2014 the node was not started.`,`Inspect canonical event diagnostics with \`crtr sys logs --node ${n.node_id}\`, then verify the pi engine can start in this environment.`)}if(Se(y,n.node_id,$.pid)==="shutdown")throw P(y,{reason:"superseded"}),j.teardown(n.node_id),E(`broker execution was superseded for ${n.node_id}`,"Retry spawning the node.");if(F&&e.readiness==="await"&&!await Ne(n.node_id,$.exited)){const t=J.explanation();throw Ge(n.node_id,new Error(t??`broker view socket never bound for ${n.node_id}`)),E(t!==null?`the root broker ${n.node_id} could not start \u2014 ${t}`:`the root broker ${n.node_id} never bound its view socket \u2014 it was not started.`,t!==null?"Resolve the cause named above, then retry.":`The broker engine exited before binding its view socket. Inspect canonical event diagnostics with \`crtr sys logs --node ${n.node_id}\`, then retry.`)}let T=null,L=null;if(!d&&a!==null&&Ae("user").auto_open_child_viewers)try{const t=Pe(a);if(t!==null&&t.pane!==null&&t.pane!==""){const v=Be(n.node_id,t.session??"",{name:H(n),cwd:e.cwd,besidePane:t.pane,waitForBroker:!0,initialWhip:R});v!==null&&v.pane!==null&&v.pane!==""&&(L=v.session,T=je(v.pane))}}catch{}return{node:w(n.node_id),frozen:!1,window:T,session:L}}catch(_){throw m!==void 0&&p!==void 0&&w(p)===null&&await me(m),_}}c(U,"spawnChildPrepared");async function Pn(e){return U(e)}c(Pn,"spawnChild");async function Ze(e){const o=Ue(e.cwd,e.profile_id).plans.get("node:start")??[];await Ve(o,{node:{id:e.node_id,name:e.name,kind:e.kind,mode:e.mode,lifecycle:e.lifecycle,cwd:e.cwd,nodeDir:Ce(e.node_id),contextDir:B(e.node_id),jobDir:Oe(e.node_id),reportsDir:$e(e.node_id)},runtime:{isBirth:!0,canvasHome:xe(),profile:e.profile_id??null}})}c(Ze,"invokeBirthLifecycleHooks");const V=new Set(["initial-prompt.md","yield-message.md","result.json"]);async function jn(e,o){const r=w(e);if(r===null)throw f(`unknown node: ${e}`,{received:e,next:"List nodes and pass the id of a node with a captured pi session."});const i=K(e),d=B(e);try{W(i,A.R_OK),W(d,A.R_OK)}catch(a){throw new Error(`cannot fork ${e}: its conversation or context is unreadable \u2014 ${a.message}`)}return U({kind:r.kind,mode:"base",cwd:r.cwd,prompt:"",root:!0,readiness:"await",forkFrom:e,profile:r.profile_id,model:r.model_override??void 0,...o===void 0?{}:{scopes:o}},a=>{const l=B(a);for(const s of D(d,{withFileTypes:!0}))V.has(s.name)||Y(g(d,s.name),g(l,s.name),{recursive:!0,dereference:!1,force:!0});for(const s of V)ee(g(l,s),{recursive:!0,force:!0})})}c(jn,"forkNode");export{q as assertLaunchModelRegistered,jn as forkNode,Ze as invokeBirthLifecycleHooks,K as resolveForkSource,Qe as resolveProfileId,Ye as resolveSpawner,Pn as spawnChild,U as spawnChildPrepared};
@@ -8,8 +8,10 @@ export declare function assertScope(meta: {
8
8
  node_id: string;
9
9
  scopes?: readonly string[] | null;
10
10
  }, scope: string): void;
11
+ /** `holder` names who lacks the scope in prose — `node <id>` for a node's own
12
+ * allow-list, `the bearer token` for a scoped API token's ceiling. */
11
13
  export declare class ScopeDeniedError extends Error {
12
- readonly nodeId: string;
14
+ readonly holder: string;
13
15
  readonly scope: string;
14
- constructor(nodeId: string, scope: string);
16
+ constructor(holder: string, scope: string);
15
17
  }
@@ -1 +1 @@
1
- var p=Object.defineProperty;var t=(r,e)=>p(r,"name",{value:e,configurable:!0});import{scopeAllowed as n}from"../shared/env.js";import{InputError as c}from"./io.js";const d=["ask","llm","act","schedule","memory:read","memory:write","net"],s=new Set(d),i=/^[a-z][a-z0-9-]*:[^\s]+$/;function h(r){if(!Array.isArray(r)||r.some(e=>typeof e!="string"||!s.has(e)&&!i.test(e))){const e=Array.isArray(r)?r.find(o=>typeof o!="string"||!s.has(o)&&!i.test(o)):r;throw new c({error:"unknown_scope",message:`unknown scope: ${String(e)}`,received:e,field:"scopes",next:"Use a runtime scope name or a parameterized scope in prefix:operand form."})}return[...r]}t(h,"validateScopes");function A(r,e){if(!n(r.scopes,e))throw new a(r.node_id,e)}t(A,"assertScope");class a extends Error{static{t(this,"ScopeDeniedError")}nodeId;scope;constructor(e,o){super(`node ${e} does not hold the ${o} scope`),this.nodeId=e,this.scope=o,this.name="ScopeDeniedError"}}export{d as SCOPE_VOCABULARY,a as ScopeDeniedError,A as assertScope,n as scopeAllowed,h as validateScopes};
1
+ var p=Object.defineProperty;var t=(r,e)=>p(r,"name",{value:e,configurable:!0});import{scopeAllowed as n}from"../shared/env.js";import{InputError as c}from"./io.js";const d=["ask","llm","act","schedule","memory:read","memory:write","net"],s=new Set(d),i=/^[a-z][a-z0-9-]*:[^\s]+$/;function E(r){if(!Array.isArray(r)||r.some(e=>typeof e!="string"||!s.has(e)&&!i.test(e))){const e=Array.isArray(r)?r.find(o=>typeof o!="string"||!s.has(o)&&!i.test(o)):r;throw new c({error:"unknown_scope",message:`unknown scope: ${String(e)}`,received:e,field:"scopes",next:"Use a runtime scope name or a parameterized scope in prefix:operand form."})}return[...r]}t(E,"validateScopes");function A(r,e){if(!n(r.scopes,e))throw new a(`node ${r.node_id}`,e)}t(A,"assertScope");class a extends Error{static{t(this,"ScopeDeniedError")}holder;scope;constructor(e,o){super(`${e} does not hold the ${o} scope`),this.holder=e,this.scope=o,this.name="ScopeDeniedError"}}export{d as SCOPE_VOCABULARY,a as ScopeDeniedError,A as assertScope,n as scopeAllowed,E as validateScopes};
@@ -9,6 +9,15 @@ import type { Scope } from '../types.js';
9
9
  export interface SecretsStore {
10
10
  relayTokens: Record<string, string>;
11
11
  apiToken?: string;
12
+ scopedApiTokens?: ScopedApiToken[];
13
+ }
14
+ /** A bearer token minted by `crtr sys connect --scopes` whose `scopes` list is
15
+ * the ceiling for every scope-gated daemon operation the holder performs. The
16
+ * owner token (`apiToken`) has no ceiling. */
17
+ export interface ScopedApiToken {
18
+ token: string;
19
+ scopes: string[];
20
+ created: string;
12
21
  }
13
22
  export declare function readSecrets(scope: Scope): SecretsStore;
14
23
  /** Writes secrets.json directly (never via `fs-utils.writeJson`, which has no
@@ -21,6 +30,8 @@ export declare function setRelayToken(ref: string, token: string, scope: Scope):
21
30
  export declare function deleteRelayToken(ref: string, scope: Scope): void;
22
31
  export declare function getApiToken(scope: Scope): string | undefined;
23
32
  export declare function setApiToken(token: string, scope: Scope): void;
33
+ export declare function listScopedApiTokens(scope: Scope): ScopedApiToken[];
34
+ export declare function addScopedApiToken(entry: ScopedApiToken, scope: Scope): void;
24
35
  /** A short, non-reversible display fingerprint of a raw token — safe to print
25
36
  * in `canvas config list`/`show` output. NEVER derive this from anything
26
37
  * that could round-trip back to the token, and never log/return the raw
@@ -1,2 +1,2 @@
1
- var a=Object.defineProperty;var r=(e,o)=>a(e,"name",{value:o,configurable:!0});import{createHash as p}from"node:crypto";import{existsSync as u,readFileSync as f,writeFileSync as T,chmodSync as k}from"node:fs";import{join as l}from"node:path";import{scopeRoot as y,requireScopeRoot as m}from"./scope.js";import{ensureDir as S}from"./fs-utils.js";const x="secrets.json";function c(e){return l(e,x)}r(c,"secretsPathFor");function i(e){const o=y(e);if(!o)return{relayTokens:{}};const n=c(o);if(!u(n))return{relayTokens:{}};try{const t=JSON.parse(f(n,"utf8"));return{relayTokens:t.relayTokens??{},...typeof t.apiToken=="string"&&t.apiToken!==""?{apiToken:t.apiToken}:{}}}catch{return{relayTokens:{}}}}r(i,"readSecrets");function s(e,o){const n=m(e);S(n);const t=c(n);T(t,JSON.stringify(o,null,2)+`
2
- `,{mode:384}),k(t,384)}r(s,"writeSecrets");function j(e,o){return i(o).relayTokens[e]}r(j,"getRelayToken");function w(e,o,n){const t=i(n);t.relayTokens[e]=o,s(n,t)}r(w,"setRelayToken");function A(e,o){const n=i(o);delete n.relayTokens[e],s(o,n)}r(A,"deleteRelayToken");function J(e){return i(e).apiToken}r(J,"getApiToken");function N(e,o){const n=i(o);n.apiToken=e,s(o,n)}r(N,"setApiToken");function O(e){return p("sha256").update(e).digest("hex").slice(0,12)}r(O,"fingerprint");export{A as deleteRelayToken,O as fingerprint,J as getApiToken,j as getRelayToken,i as readSecrets,N as setApiToken,w as setRelayToken,s as writeSecrets};
1
+ var k=Object.defineProperty;var t=(e,o)=>k(e,"name",{value:o,configurable:!0});import{createHash as T}from"node:crypto";import{existsSync as f,readFileSync as u,writeFileSync as y,chmodSync as d}from"node:fs";import{join as l}from"node:path";import{scopeRoot as A,requireScopeRoot as S}from"./scope.js";import{ensureDir as m}from"./fs-utils.js";const x="secrets.json";function a(e){return l(e,x)}t(a,"secretsPathFor");function s(e){const o=A(e);if(!o)return{relayTokens:{}};const n=a(o);if(!f(n))return{relayTokens:{}};try{const r=JSON.parse(u(n,"utf8")),c=Array.isArray(r.scopedApiTokens)?r.scopedApiTokens.filter(i=>typeof i?.token=="string"&&i.token!==""&&Array.isArray(i.scopes)&&typeof i.created=="string"):[];return{relayTokens:r.relayTokens??{},...typeof r.apiToken=="string"&&r.apiToken!==""?{apiToken:r.apiToken}:{},...c.length>0?{scopedApiTokens:c}:{}}}catch{return{relayTokens:{}}}}t(s,"readSecrets");function p(e,o){const n=S(e);m(n);const r=a(n);y(r,JSON.stringify(o,null,2)+`
2
+ `,{mode:384}),d(r,384)}t(p,"writeSecrets");function w(e,o){return s(o).relayTokens[e]}t(w,"getRelayToken");function J(e,o,n){const r=s(n);r.relayTokens[e]=o,p(n,r)}t(J,"setRelayToken");function N(e,o){const n=s(o);delete n.relayTokens[e],p(o,n)}t(N,"deleteRelayToken");function O(e){return s(e).apiToken}t(O,"getApiToken");function q(e,o){const n=s(o);n.apiToken=e,p(o,n)}t(q,"setApiToken");function C(e){return s(e).scopedApiTokens??[]}t(C,"listScopedApiTokens");function D(e,o){const n=s(o);n.scopedApiTokens=[...n.scopedApiTokens??[],e],p(o,n)}t(D,"addScopedApiToken");function H(e){return T("sha256").update(e).digest("hex").slice(0,12)}t(H,"fingerprint");export{D as addScopedApiToken,N as deleteRelayToken,H as fingerprint,O as getApiToken,w as getRelayToken,C as listScopedApiTokens,s as readSecrets,q as setApiToken,J as setRelayToken,p as writeSecrets};
@@ -37,6 +37,10 @@ export interface RemoteAttachHeartbeat {
37
37
  noteActivity: () => void;
38
38
  }
39
39
  export declare function startRemoteAttachHeartbeat(ws: WebSocket, intervalMs?: number, quietMs?: number): RemoteAttachHeartbeat;
40
+ /** Refuse an upgrade PRE-handshake: write a plain HTTP error (uniform API
41
+ * `ErrorBody`, spec §8) to the raw socket and destroy it. Never throws — a
42
+ * reset peer is swallowed. Shared with server.ts's auth and startup gates. */
43
+ export declare function refuseUpgrade(socket: Duplex, status: number, statusText: string, code: string, message: string): void;
40
44
  /** Handle the HTTP upgrade for `GET /v1/nodes/{id}/attach`. A-9 matches the
41
45
  * route, extracts `{id}`, and calls this with the raw upgrade triplet; this
42
46
  * owns the WS handshake (its own noServer `wss` above) AND the spec §5.3