@crouton-kit/crouter 0.3.29 → 0.3.30

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.
@@ -1,8 +1,16 @@
1
1
  import type { RootDef } from './core/command.js';
2
- /** Assemble the full crtr command tree. Root owns only the tagline; every
3
- * subtree declares its own root representation via its rootEntry, and every
4
- * branch assembles its child listing from the child defs (each child owns its
5
- * description/whenToUse/tier). No globals: -h is taught by each child's
6
- * whenToUse CTA and the capability-discovery rule in the root footer, so a
7
- * standalone "-h: print help" stub would be redundant. */
8
- export declare function buildRoot(): RootDef;
2
+ /** Every shipped subtree name. Cheap (no module loading) the front-door
3
+ * recursion guard and the dispatcher's first-token routing need only names. */
4
+ export declare const SUBTREE_NAMES: string[];
5
+ /** Build a root that contains only the subtree `first` dispatches into.
6
+ * Returns the FULL root when `first` is not a recognized subtree — bare `crtr`,
7
+ * `-h`/`--help`, `--version`, and any unknown leading token all need the
8
+ * complete tree (root -h lists every subtree; the unknown-path error names
9
+ * every valid child). This keeps every help/error surface byte-identical while
10
+ * the common leaf-dispatch path loads exactly one subtree. */
11
+ export declare function resolveRoot(first: string | undefined): Promise<RootDef>;
12
+ /** Assemble the full crtr command tree (all subtrees). Used for root -h, the
13
+ * unknown-path error, bare-root boot, and the listing-completeness test. Root
14
+ * owns only the tagline; every subtree declares its own root representation via
15
+ * its rootEntry. */
16
+ export declare function buildRoot(): Promise<RootDef>;
@@ -1,38 +1,50 @@
1
1
  import { defineRoot } from './core/command.js';
2
- import { registerMemory } from './commands/memory.js';
3
- import { registerPkg } from './commands/pkg.js';
4
- import { registerHuman } from './commands/human.js';
5
- import { registerSys } from './commands/sys.js';
6
- import { registerPush, registerFeed } from './commands/push.js';
7
- import { registerNode } from './commands/node.js';
8
- import { registerCanvas } from './commands/canvas.js';
9
- import { registerView } from './commands/view.js';
10
- import { registerAttach } from './clients/attach/attach-cmd.js';
11
- import { registerWorkspace } from './commands/workspace.js';
12
- import { registerWeb } from './clients/web/web-cmd.js';
13
- /** Assemble the full crtr command tree. Root owns only the tagline; every
14
- * subtree declares its own root representation via its rootEntry, and every
15
- * branch assembles its child listing from the child defs (each child owns its
16
- * description/whenToUse/tier). No globals: -h is taught by each child's
17
- * whenToUse CTA and the capability-discovery rule in the root footer, so a
18
- * standalone "-h: print help" stub would be redundant. */
19
- export function buildRoot() {
20
- return defineRoot({
21
- tagline: 'crtr: agentic planning runtime.',
22
- globals: [],
23
- subtrees: [
24
- registerMemory(),
25
- registerPkg(),
26
- registerHuman(),
27
- registerSys(),
28
- registerNode(),
29
- registerPush(),
30
- registerFeed(),
31
- registerCanvas(),
32
- registerView(),
33
- registerAttach(),
34
- registerWorkspace(),
35
- registerWeb(),
36
- ],
37
- });
2
+ const TAGLINE = 'crtr: agentic planning runtime.';
3
+ /** Lazy registry: subtree name → dynamic importer that builds its BranchDef.
4
+ * The whole point is that each `import()` only compiles that one subtree's
5
+ * module graph. The hot path (`crtr node focus …`, `crtr feed`, …) dispatches
6
+ * into a single leaf, so it must load ONE subtree — not the attach-TUI
7
+ * (babel/highlight.js), the web/vite command, and every other subtree it never
8
+ * touches. `crtr` cold-start is dominated by Node module loading; keeping the
9
+ * other 11 subtrees off the path is the biggest, lowest-risk win.
10
+ *
11
+ * Naming note: `push` and `feed` are two subtrees from one module — importing
12
+ * it once yields both registers. */
13
+ const SUBTREE_LOADERS = {
14
+ memory: async () => (await import('./commands/memory.js')).registerMemory(),
15
+ pkg: async () => (await import('./commands/pkg.js')).registerPkg(),
16
+ human: async () => (await import('./commands/human.js')).registerHuman(),
17
+ sys: async () => (await import('./commands/sys.js')).registerSys(),
18
+ node: async () => (await import('./commands/node.js')).registerNode(),
19
+ push: async () => (await import('./commands/push.js')).registerPush(),
20
+ feed: async () => (await import('./commands/push.js')).registerFeed(),
21
+ canvas: async () => (await import('./commands/canvas.js')).registerCanvas(),
22
+ view: async () => (await import('./commands/view.js')).registerView(),
23
+ attach: async () => (await import('./clients/attach/attach-cmd.js')).registerAttach(),
24
+ workspace: async () => (await import('./commands/workspace.js')).registerWorkspace(),
25
+ web: async () => (await import('./clients/web/web-cmd.js')).registerWeb(),
26
+ };
27
+ /** Every shipped subtree name. Cheap (no module loading) — the front-door
28
+ * recursion guard and the dispatcher's first-token routing need only names. */
29
+ export const SUBTREE_NAMES = Object.keys(SUBTREE_LOADERS);
30
+ /** Build a root that contains only the subtree `first` dispatches into.
31
+ * Returns the FULL root when `first` is not a recognized subtree — bare `crtr`,
32
+ * `-h`/`--help`, `--version`, and any unknown leading token all need the
33
+ * complete tree (root -h lists every subtree; the unknown-path error names
34
+ * every valid child). This keeps every help/error surface byte-identical while
35
+ * the common leaf-dispatch path loads exactly one subtree. */
36
+ export async function resolveRoot(first) {
37
+ const loader = first !== undefined ? SUBTREE_LOADERS[first] : undefined;
38
+ if (loader !== undefined) {
39
+ return defineRoot({ tagline: TAGLINE, globals: [], subtrees: [await loader()] });
40
+ }
41
+ return buildRoot();
42
+ }
43
+ /** Assemble the full crtr command tree (all subtrees). Used for root -h, the
44
+ * unknown-path error, bare-root boot, and the listing-completeness test. Root
45
+ * owns only the tagline; every subtree declares its own root representation via
46
+ * its rootEntry. */
47
+ export async function buildRoot() {
48
+ const subtrees = await Promise.all(SUBTREE_NAMES.map((n) => SUBTREE_LOADERS[n]()));
49
+ return defineRoot({ tagline: TAGLINE, globals: [], subtrees });
38
50
  }
package/dist/cli.js CHANGED
@@ -1,25 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  import { runCli } from './core/command.js';
3
- import { buildRoot } from './build-root.js';
3
+ import { resolveRoot } from './build-root.js';
4
4
  import { maybeBootRoot } from './core/runtime/front-door.js';
5
5
  import { maybeAutoUpdate } from './core/auto-update.js';
6
6
  import { ensureOfficialMarketplace, ensureProjectScope } from './core/bootstrap.js';
7
7
  import { provisionExports } from './core/skill-sync/export.js';
8
- // The full command tree is assembled in build-root.ts (shared with the
9
- // listing-completeness test). Root owns only the tagline; every subtree
10
- // declares its own representation.
11
- const root = buildRoot();
12
- // The front door: bare `crtr` (or `crtr [dir] ["prompt"]`) boots a resident
13
- // root node and execs pi in this terminal. Recognized subcommands fall through
14
- // to the normal dispatcher. Must run before anything that assumes a subcommand.
15
- if (maybeBootRoot(root, process.argv)) {
16
- // bootRoot exec'd pi inline and exited; unreachable.
8
+ async function main() {
9
+ // Lazy command tree: load only the subtree this invocation dispatches into.
10
+ // resolveRoot returns the full tree only for help/version/bare/unknown, where
11
+ // every subtree is genuinely needed (root -h, the unknown-path error). The
12
+ // hot leaf-dispatch path loads one subtree, keeping the other 11 (and their
13
+ // heavy deps the attach TUI, web/vite) off cold-start.
14
+ const root = await resolveRoot(process.argv[2]);
15
+ // The front door: bare `crtr` (or `crtr [dir] ["prompt"]`) boots a resident
16
+ // root node and execs pi in this terminal. Recognized subcommands fall through
17
+ // to the normal dispatcher. Must run before anything that assumes a subcommand.
18
+ if (maybeBootRoot(root, process.argv)) {
19
+ // bootRoot exec'd pi inline and exited; unreachable.
20
+ }
21
+ ensureOfficialMarketplace(process.argv);
22
+ provisionExports(root);
23
+ ensureProjectScope(process.argv);
24
+ maybeAutoUpdate(process.argv);
25
+ await runCli(root, process.argv);
17
26
  }
18
- ensureOfficialMarketplace(process.argv);
19
- provisionExports(root);
20
- ensureProjectScope(process.argv);
21
- maybeAutoUpdate(process.argv);
22
- runCli(root, process.argv).catch((err) => {
27
+ main().catch((err) => {
23
28
  const msg = err instanceof Error ? err.message : String(err);
24
29
  process.stderr.write(`crtr: ${msg}\n`);
25
30
  process.exit(1);
@@ -8,8 +8,7 @@
8
8
  // `crtr <argv>`. This keeps the menu static while the behaviour stays fully
9
9
  // config-driven (no per-node menu rebuild).
10
10
  //
11
- // Two special cases bypass the bind table:
12
- // • a digit key 1..9 → focus report N (the Nth live report of the pane node)
11
+ // One special case bypasses the bind table:
13
12
  // • a bind whose `run` is the sentinel `__graph__` → send-keys `/graph` into
14
13
  // the pane (toggles the in-pi GRAPH modal); the menu emits this directly so
15
14
  // the dispatcher only handles it defensively.
@@ -23,7 +22,7 @@ import { InputError } from '../core/io.js';
23
22
  import { readConfig } from '../core/config.js';
24
23
  import { sendKeysEnter } from '../core/runtime/tmux-chrome.js';
25
24
  import { nodeInPane } from './node.js';
26
- import { getNode, subscribersOf, subscriptionsOf, view, fullName, } from '../core/canvas/index.js';
25
+ import { getNode, subscribersOf, view, fullName, } from '../core/canvas/index.js';
27
26
  const pexec = promisify(execFile);
28
27
  /** Template vars available to a `run` string. Single-valued vars interpolate
29
28
  * in place (preserving spaces, e.g. a node name); `{subtree}` is multi-valued
@@ -76,7 +75,7 @@ export const chordLeaf = defineLeaf({
76
75
  name: 'key',
77
76
  type: 'string',
78
77
  required: true,
79
- constraint: 'The chord key pressed after alt+c (e.g. m, e, or a digit 1-9 for focus report N).',
78
+ constraint: 'The chord key pressed after alt+c (e.g. g or m).',
80
79
  },
81
80
  ],
82
81
  output: [
@@ -99,26 +98,6 @@ export const chordLeaf = defineLeaf({
99
98
  next: 'Run from inside an agent\'s pane, or pass --pane <pane-id>.',
100
99
  });
101
100
  }
102
- // Digit keys 1..9 → focus the Nth live report (generated, not a bind entry).
103
- if (/^[1-9]$/.test(key)) {
104
- const n = parseInt(key, 10);
105
- const reports = subscriptionsOf(selfId)
106
- .map((r) => r.node_id)
107
- .filter((id) => {
108
- const s = getNode(id)?.status;
109
- return s === 'active' || s === 'idle';
110
- });
111
- const target = reports[n - 1];
112
- if (target === undefined)
113
- return { ran: false, key, node_id: selfId, action: 'noop' };
114
- try {
115
- await pexec('crtr', ['node', 'focus', target], { timeout: 15_000 });
116
- }
117
- catch {
118
- /* best-effort */
119
- }
120
- return { ran: true, key, node_id: selfId, action: `node focus ${target}` };
121
- }
122
101
  const bind = readConfig('user').canvasNav.prefixBinds[key];
123
102
  if (bind === undefined)
124
103
  return { ran: false, key, node_id: selfId, action: 'noop' };
@@ -9,8 +9,8 @@
9
9
  import { test } from 'node:test';
10
10
  import assert from 'node:assert/strict';
11
11
  import { buildRoot } from '../../build-root.js';
12
- test('every non-hidden listing child declares description + whenToUse', () => {
13
- const root = buildRoot();
12
+ test('every non-hidden listing child declares description + whenToUse', async () => {
13
+ const root = await buildRoot();
14
14
  const missing = [];
15
15
  const walk = (branch, path) => {
16
16
  for (const child of branch.help.listing ?? []) {
@@ -23,11 +23,11 @@ export declare function listFocuses(): FocusRow[];
23
23
  * targeting it. */
24
24
  export declare function graphSurfaceTarget(nodeId: string): FocusRow | null;
25
25
  /** Synchronously wait until a broker's view.sock accepts a connection. The spawn
26
- * and focus flows are sync (the command layer calls them directly), so the
27
- * readiness probe lives in a short child Node process that can use async net
28
- * events while this process blocks in `spawnSync`. Success proves more than file
29
- * existence: it is robust to a stale leftover socket the launching broker has
30
- * not unlinked yet, because only an accepting listener exits 0. */
26
+ * and focus flows are sync (the command layer calls them directly), so the async
27
+ * net poll runs in a worker THREAD while this thread blocks on `Atomics.wait`
28
+ * in-process, no child Node cold-start. Success proves more than file existence:
29
+ * it is robust to a stale leftover socket the launching broker has not unlinked
30
+ * yet, because only an accepting listener yields a `connect`. */
31
31
  export declare function waitForBrokerViewSocket(nodeId: string): boolean;
32
32
  /** A reviver: ensure a node's broker ENGINE is alive (idempotent → reviveNode →
33
33
  * headlessBrokerHost.launch). Injected so placement.ts need not import revive.ts
@@ -23,7 +23,7 @@
23
23
  // model-over-driver so the §5.1 import-lint ("only placement.ts /
24
24
  // tmux-chrome.ts import tmux.ts") holds — every other module reaches the
25
25
  // driver verbs through here.
26
- import { spawnSync } from 'node:child_process';
26
+ import { Worker } from 'node:worker_threads';
27
27
  import { join } from 'node:path';
28
28
  import { getNode, openFocusRow, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, listFocuses as listFocusRows, view, } from '../canvas/index.js';
29
29
  import { paneExists, paneLocation, ensureSession, openNodeWindow, splitWindow, closePane, currentTmux, switchClient, selectWindow, getPaneOption, } from './tmux.js';
@@ -115,19 +115,17 @@ export function graphSurfaceTarget(nodeId) {
115
115
  // ---------------------------------------------------------------------------
116
116
  const BROKER_FOCUS_SOCKET_WAIT_MS = 30_000;
117
117
  const BROKER_FOCUS_SOCKET_RETRY_MS = 100;
118
- /** Synchronously wait until a broker's view.sock accepts a connection. The spawn
119
- * and focus flows are sync (the command layer calls them directly), so the
120
- * readiness probe lives in a short child Node process that can use async net
121
- * events while this process blocks in `spawnSync`. Success proves more than file
122
- * existence: it is robust to a stale leftover socket the launching broker has
123
- * not unlinked yet, because only an accepting listener exits 0. */
124
- export function waitForBrokerViewSocket(nodeId) {
125
- const sockPath = join(nodeDir(nodeId), 'view.sock');
126
- const probe = `
118
+ // The probe runs inside a worker thread (see waitForBrokerViewSocket). Async net
119
+ // events drive the same poll loop; the worker reports its verdict into a shared
120
+ // Int32 (0 = pending, 1 = accepted, 2 = gave up) and Atomics.notify wakes the
121
+ // blocked main thread. eval:true CommonJS, so `require` and workerData apply.
122
+ const VIEW_SOCKET_PROBE_WORKER = `
127
123
  const net = require('node:net');
128
- const sockPath = process.argv[1];
129
- const deadline = Date.now() + Number(process.argv[2]);
130
- const delay = Number(process.argv[3]);
124
+ const { workerData, parentPort } = require('node:worker_threads');
125
+ const { buffer, sockPath, waitMs, retryMs } = workerData;
126
+ const result = new Int32Array(buffer);
127
+ const deadline = Date.now() + waitMs;
128
+ const report = (code) => { Atomics.store(result, 0, code); Atomics.notify(result, 0); };
131
129
  function attempt() {
132
130
  let socket;
133
131
  let settled = false;
@@ -135,9 +133,9 @@ function attempt() {
135
133
  if (settled) return;
136
134
  settled = true;
137
135
  if (socket !== undefined) socket.destroy();
138
- if (ok) process.exit(0);
139
- if (Date.now() >= deadline) process.exit(1);
140
- setTimeout(attempt, delay);
136
+ if (ok) { report(1); return; }
137
+ if (Date.now() >= deadline) { report(2); return; }
138
+ setTimeout(attempt, retryMs);
141
139
  };
142
140
  try {
143
141
  socket = net.createConnection(sockPath);
@@ -147,18 +145,31 @@ function attempt() {
147
145
  }
148
146
  socket.once('connect', () => finish(true));
149
147
  socket.once('error', () => finish(false));
150
- socket.setTimeout(delay, () => finish(false));
148
+ socket.setTimeout(retryMs, () => finish(false));
151
149
  }
152
150
  attempt();
151
+ void parentPort;
153
152
  `;
154
- const r = spawnSync(process.execPath, ['--input-type=commonjs', '-e', probe, sockPath, String(BROKER_FOCUS_SOCKET_WAIT_MS), String(BROKER_FOCUS_SOCKET_RETRY_MS)], {
155
- stdio: 'ignore',
156
- timeout: BROKER_FOCUS_SOCKET_WAIT_MS + 1_000,
157
- // Keep the probe deterministic: NODE_OPTIONS like --input-type=module or
158
- // --inspect-brk can break/hang a tiny `node -e` readiness check.
159
- env: { ...process.env, NODE_OPTIONS: '' },
153
+ /** Synchronously wait until a broker's view.sock accepts a connection. The spawn
154
+ * and focus flows are sync (the command layer calls them directly), so the async
155
+ * net poll runs in a worker THREAD while this thread blocks on `Atomics.wait` —
156
+ * in-process, no child Node cold-start. Success proves more than file existence:
157
+ * it is robust to a stale leftover socket the launching broker has not unlinked
158
+ * yet, because only an accepting listener yields a `connect`. */
159
+ export function waitForBrokerViewSocket(nodeId) {
160
+ const sockPath = join(nodeDir(nodeId), 'view.sock');
161
+ const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
162
+ const result = new Int32Array(buffer);
163
+ const worker = new Worker(VIEW_SOCKET_PROBE_WORKER, {
164
+ eval: true,
165
+ workerData: { buffer, sockPath, waitMs: BROKER_FOCUS_SOCKET_WAIT_MS, retryMs: BROKER_FOCUS_SOCKET_RETRY_MS },
160
166
  });
161
- return r.status === 0;
167
+ worker.unref();
168
+ // Block until the worker reports (or a hard timeout matching the old
169
+ // spawnSync). 'not-equal' covers the worker finishing before we wait.
170
+ Atomics.wait(result, 0, 0, BROKER_FOCUS_SOCKET_WAIT_MS + 1_000);
171
+ void worker.terminate();
172
+ return Atomics.load(result, 0) === 1;
162
173
  }
163
174
  function newFocusId() {
164
175
  return `f-${newNodeId()}`;
@@ -178,8 +178,8 @@ export declare function switchClient(session: string): boolean;
178
178
  * empty, same limitation as the menu's `/promote` item. Best-effort. */
179
179
  export declare function sendKeysEnter(pane: string, text: string): boolean;
180
180
  /** Bind Alt+C to the crouter action menu. Best-effort; false if tmux fails.
181
- * The built-in items (promote/demote/detach/close/browse) are static; the canvas-nav
182
- * chords (graph/manager/expand/report-N + any custom prefixBind) are appended
181
+ * The built-in items (promote/resume/demote/detach/close) are static; the canvas-nav
182
+ * chords (default g→graph, m→manager + any custom prefixBind) are appended
183
183
  * from `canvasNav.prefixBinds`, each routed through `crtr canvas chord` (or, for
184
184
  * the `__graph__` sentinel, a `send-keys '/graph'`) so the menu stays static
185
185
  * while behaviour is config-driven. */
@@ -336,8 +336,8 @@ export function sendKeysEnter(pane, text) {
336
336
  * `prefixBind` may not claim these (the built-in item wins). */
337
337
  const RESERVED_MENU_KEYS = new Set(['o', 'r', 'd', 'D', 'x']);
338
338
  /** Bind Alt+C to the crouter action menu. Best-effort; false if tmux fails.
339
- * The built-in items (promote/demote/detach/close/browse) are static; the canvas-nav
340
- * chords (graph/manager/expand/report-N + any custom prefixBind) are appended
339
+ * The built-in items (promote/resume/demote/detach/close) are static; the canvas-nav
340
+ * chords (default g→graph, m→manager + any custom prefixBind) are appended
341
341
  * from `canvasNav.prefixBinds`, each routed through `crtr canvas chord` (or, for
342
342
  * the `__graph__` sentinel, a `send-keys '/graph'`) so the menu stays static
343
343
  * while behaviour is config-driven. */
@@ -363,7 +363,7 @@ export function installMenuBinding() {
363
363
  // marks them canceled); revivable. Output discarded — the keypress just acts.
364
364
  { name: 'close agent + subtree', key: 'x', cmd: `run-shell "crtr node close --pane '#{pane_id}' >/dev/null 2>&1"` },
365
365
  ];
366
- // Canvas-nav chords from config (default: g→graph, m→manager, e→expand). The
366
+ // Canvas-nav chords from config (default: g→graph, m→manager). The
367
367
  // `__graph__` sentinel toggles the in-pi GRAPH modal via send-keys; every
368
368
  // other bind shells the chord dispatcher, which resolves the pane's node and
369
369
  // interpolates the bind at popup time. Keys colliding with the built-ins are
@@ -382,26 +382,16 @@ export function installMenuBinding() {
382
382
  : `run-shell "crtr canvas chord --pane '#{pane_id}' --key ${key} >/dev/null 2>&1"`;
383
383
  items.push({ name, key, cmd });
384
384
  }
385
- // Focus report N: nine generated chord items (1..9), each resolved by the
386
- // dispatcher to subscriptionsOf(self)[N-1] at popup time.
387
- for (let n = 1; n <= 9; n++) {
388
- items.push({
389
- name: `focus report ${n}`,
390
- key: `${n}`,
391
- cmd: `run-shell "crtr canvas chord --pane '#{pane_id}' --key ${n} >/dev/null 2>&1"`,
392
- });
393
- }
394
- // Alt+C TOGGLE: re-pressing Alt+C while the menu is open should DISMISS it.
395
- // display-menu is modal, so the root `M-c` binding is shadowed while the menu
396
- // is up; tmux instead assembles the re-pressed Esc+c into the meta key `M-c`
397
- // and delivers it to the menu's own key handler. An unhandled key is ignored
398
- // (the menu just stays open) and the trailing `c` never leaks to the editor
399
- // (verified empirically in tmux 3.6b: no native ESC-cancel + c-leak occurs for
400
- // an atomic keypress). We catch that `M-c` with a menu item keyed to it whose
401
- // command is empty — selecting it runs nothing and closes the menu. The row
402
- // doubles as the menu's self-documenting dismiss hint. Placed last so it reads
403
- // as chrome below the actions; `M-c` collides with no mnemonic above.
404
- items.push({ name: 'close menu', key: 'M-c', cmd: '' });
385
+ // Dismiss hint. A tmux display-menu always closes on its native cancel keys
386
+ // (Escape / q / C-c), encoding-independent. We do NOT try to catch a re-pressed
387
+ // Alt+C: under `extended-keys on` (common, and what pi negotiates) the second
388
+ // Alt+C reaches the overlay as a CSI-u key (`\033[99;3u`) that tmux's menu does
389
+ // NOT match against an `M-c` mnemonic item, so a "close menu" row keyed M-c
390
+ // never fires and the menu just sits open (verified in tmux 3.6b: legacy Esc+c
391
+ // closes, CSI-u does not). Instead, a disabled (`-` prefix dim, unselectable)
392
+ // last row tells the user the close keys that always work. Placed last so it
393
+ // reads as chrome below the actions.
394
+ items.push({ name: '-esc / q to close', key: '', cmd: '' });
405
395
  // tmux's -x sets the menu's LEFT edge. To sit the box INSIDE the pane's
406
396
  // top-right corner, shift x left by the box width (longest line + tmux chrome:
407
397
  // borders + padding + the right-aligned mnemonic-key column) via format math.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/crouter",
3
- "version": "0.3.29",
3
+ "version": "0.3.30",
4
4
  "description": "crtr — fast access to skills, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",