@crouton-kit/crouter 0.3.70 → 0.3.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/build-root.d.ts +12 -4
- package/dist/build-root.js +25 -6
- package/dist/builtin-memory/00-runtime-base.md +3 -10
- package/dist/builtin-memory/04-base-worker.md +18 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +82 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +1115 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +234 -71
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -516
- package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
- package/dist/builtin-views/canvas/tui.mjs +8 -9
- package/dist/builtin-views/chat/tui.mjs +12 -12
- package/dist/builtin-views/git-pr/tui.mjs +7 -8
- package/dist/builtin-views/inbox/tui.mjs +27 -41
- package/dist/builtin-views/linkedin/tui.mjs +23 -37
- package/dist/builtin-views/prompt-review/tui.mjs +11 -11
- package/dist/builtin-views/settings/tui.mjs +11 -11
- package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
- package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/mermaid-render.test.js +28 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
- package/dist/clients/attach/attach-cmd.d.ts +9 -1
- package/dist/clients/attach/attach-cmd.js +841 -792
- package/dist/clients/attach/auth-pickers.d.ts +0 -12
- package/dist/clients/attach/auth-pickers.js +64 -15
- package/dist/clients/attach/canvas-panels.js +2 -3
- package/dist/clients/attach/chat-view.d.ts +7 -8
- package/dist/clients/attach/chat-view.js +119 -79
- package/dist/clients/attach/edit-diff-render.d.ts +6 -0
- package/dist/clients/attach/edit-diff-render.js +263 -0
- package/dist/clients/attach/graph-overlay.d.ts +12 -2
- package/dist/clients/attach/graph-overlay.js +83 -33
- package/dist/clients/attach/input-controller.d.ts +28 -2
- package/dist/clients/attach/input-controller.js +38 -5
- package/dist/clients/attach/mermaid-render.js +5 -2
- package/dist/clients/attach/pickers.d.ts +8 -7
- package/dist/clients/attach/pickers.js +11 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +127 -7
- package/dist/clients/attach/titled-editor.d.ts +12 -1
- package/dist/clients/attach/titled-editor.js +103 -8
- package/dist/commands/canvas-browse.js +2 -2
- package/dist/commands/human/queue.js +3 -4
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +9 -2
- package/dist/commands/pkg/plugin-inspect.js +22 -1
- package/dist/commands/pkg/plugin-manage.js +31 -9
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
- package/dist/commands/sys/__tests__/setup-core.test.js +158 -1
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/doctor.js +42 -4
- package/dist/commands/sys/setup-core.d.ts +49 -1
- package/dist/commands/sys/setup-core.js +161 -4
- package/dist/commands/sys/setup.d.ts +88 -0
- package/dist/commands/sys/setup.js +943 -169
- package/dist/commands/view-pick.d.ts +4 -0
- package/dist/commands/view-pick.js +17 -7
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +34 -9
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
- package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins.test.js +444 -0
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
- package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
- package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
- package/dist/core/__tests__/stream-watchdog.test.js +70 -0
- package/dist/core/__tests__/tmux-surface.test.js +72 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.d.ts +6 -0
- package/dist/core/canvas/browse/app.js +230 -41
- package/dist/core/canvas/browse/model.d.ts +10 -6
- package/dist/core/canvas/browse/model.js +20 -12
- package/dist/core/canvas/browse/pins.d.ts +4 -0
- package/dist/core/canvas/browse/pins.js +29 -0
- package/dist/core/canvas/browse/render.d.ts +13 -1
- package/dist/core/canvas/browse/render.js +72 -15
- package/dist/core/canvas/nav-model.js +5 -11
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +7 -0
- package/dist/core/command-plugins/adapter.d.ts +15 -0
- package/dist/core/command-plugins/adapter.js +145 -0
- package/dist/core/command-plugins/compose.d.ts +5 -0
- package/dist/core/command-plugins/compose.js +56 -0
- package/dist/core/command-plugins/discovery.d.ts +104 -0
- package/dist/core/command-plugins/discovery.js +565 -0
- package/dist/core/config.d.ts +48 -0
- package/dist/core/config.js +228 -110
- package/dist/core/fault-classifier.js +1 -1
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +15 -1
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
- package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
- package/dist/core/keybindings/catalog.d.ts +14 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/index.d.ts +5 -0
- package/dist/core/keybindings/index.js +4 -0
- package/dist/core/keybindings/match.d.ts +29 -0
- package/dist/core/keybindings/match.js +99 -0
- package/dist/core/keybindings/persistence.d.ts +14 -0
- package/dist/core/keybindings/persistence.js +31 -0
- package/dist/core/keybindings/resolve.d.ts +14 -0
- package/dist/core/keybindings/resolve.js +305 -0
- package/dist/core/keybindings/types.d.ts +37 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/memory-resolver.d.ts +1 -1
- package/dist/core/memory-resolver.js +10 -3
- package/dist/core/predicate.d.ts +5 -3
- package/dist/core/predicate.js +5 -3
- package/dist/core/profiles/select.d.ts +6 -0
- package/dist/core/profiles/select.js +86 -52
- package/dist/core/provider-management.d.ts +12 -0
- package/dist/core/provider-management.js +24 -0
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +164 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +78 -20
- package/dist/core/runtime/naming.js +3 -3
- package/dist/core/runtime/pi-cli.d.ts +6 -0
- package/dist/core/runtime/pi-cli.js +16 -2
- package/dist/core/runtime/pi-vendored.d.ts +8 -0
- package/dist/core/runtime/pi-vendored.js +14 -0
- package/dist/core/runtime/placement.d.ts +2 -2
- package/dist/core/runtime/placement.js +4 -1
- package/dist/core/runtime/promote.js +4 -0
- package/dist/core/runtime/recap.d.ts +3 -3
- package/dist/core/runtime/recap.js +75 -47
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/session-list-cache.d.ts +33 -0
- package/dist/core/runtime/session-list-cache.js +338 -0
- package/dist/core/runtime/spawn.js +52 -45
- package/dist/core/runtime/stream-watchdog.d.ts +26 -0
- package/dist/core/runtime/stream-watchdog.js +75 -0
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +2 -2
- package/dist/core/runtime/tmux.d.ts +18 -15
- package/dist/core/runtime/tmux.js +217 -110
- package/dist/core/scope.js +27 -4
- package/dist/core/subscription-state.d.ts +90 -0
- package/dist/core/subscription-state.js +762 -0
- package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
- package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
- package/dist/core/tui/host.d.ts +30 -1
- package/dist/core/tui/host.js +148 -34
- package/dist/core/view/contract.d.ts +17 -3
- package/dist/daemon/crtrd.js +253 -12
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
- package/dist/pi-extensions/canvas-recap.d.ts +4 -0
- package/dist/pi-extensions/canvas-recap.js +73 -46
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/types.d.ts +9 -26
- package/dist/types.js +1 -28
- package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +8 -4
- package/dist/pi-extensions/canvas-nav.d.ts +0 -43
- package/dist/pi-extensions/canvas-nav.js +0 -640
- package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
- package/dist/pi-extensions/widget-order-bus.js +0 -34
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
// root) or switch-client + select-window (across roots). done/dead nodes close
|
|
9
9
|
// their window; reviving opens a fresh one.
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import {
|
|
12
|
-
import { defaultCanvasNavConfig } from '../../types.js';
|
|
11
|
+
import { resolveUserKeybindings, } from '../keybindings/index.js';
|
|
13
12
|
import { bundledPiCommandForShell } from './pi-cli.js';
|
|
14
13
|
import { surfaceTmuxStyleArgs } from './surface-bg.js';
|
|
15
14
|
// ---------------------------------------------------------------------------
|
|
@@ -72,13 +71,22 @@ function ensureBracketedPaste() {
|
|
|
72
71
|
// broken `Enbp=E[?2004h` (no ESC) and silently disables bracketed paste.
|
|
73
72
|
tmux(['set-option', '-sga', 'terminal-overrides', '*:Enbp=\\E[?2004h:Dsbp=\\E[?2004l']);
|
|
74
73
|
}
|
|
74
|
+
/** Server-level tmux prerequisites for crtr's viewer surfaces, applied once a
|
|
75
|
+
* server exists (callers invoke via ensureSession). Currently: bracketed paste
|
|
76
|
+
* capability (above) and `focus-events on`, which forwards terminal focus
|
|
77
|
+
* in/out (DECSET 1004) to the program in each pane — the attach viewer uses it
|
|
78
|
+
* to hollow out the editor cursor in blurred panes. */
|
|
79
|
+
function ensureServerOptions() {
|
|
80
|
+
ensureBracketedPaste();
|
|
81
|
+
tmux(['set-option', '-s', 'focus-events', 'on']);
|
|
82
|
+
}
|
|
75
83
|
/** Create a detached session rooted at `cwd` if it doesn't exist. The session
|
|
76
84
|
* name is a root's tmux home; every node under that root is a window in it. */
|
|
77
85
|
export function ensureSession(name, cwd) {
|
|
78
86
|
if (!sessionExists(name)) {
|
|
79
87
|
tmux(['new-session', '-d', '-s', name, '-c', cwd]);
|
|
80
88
|
}
|
|
81
|
-
|
|
89
|
+
ensureServerOptions();
|
|
82
90
|
}
|
|
83
91
|
function envFlags(env) {
|
|
84
92
|
const out = [];
|
|
@@ -400,135 +408,234 @@ export function sendKeysEnter(pane, text) {
|
|
|
400
408
|
return tmux(['send-keys', '-t', pane, text, 'Enter']).ok;
|
|
401
409
|
}
|
|
402
410
|
// ---------------------------------------------------------------------------
|
|
403
|
-
//
|
|
404
|
-
// actions. Installed on the running server at root boot; idempotent (a re-bind
|
|
405
|
-
// overwrites the previous one). Menu entries are data-driven: tmux renders the
|
|
406
|
-
// action tree, and every `crtr` / `popup` action routes through `crtr canvas
|
|
407
|
-
// chord` so node-relative vars resolve in one safe place.
|
|
411
|
+
// Crouter-owned tmux bindings
|
|
408
412
|
// ---------------------------------------------------------------------------
|
|
413
|
+
const TMUX_BINDING_MANIFEST = '@crtr_keybindings_manifest';
|
|
414
|
+
const ROOT_TABLE = 'root';
|
|
415
|
+
const MENU_ACTIONS = {
|
|
416
|
+
'crtr.tmux.menu.promote': { description: 'promote to orchestrator', action: { kind: 'keys', keys: '/promote' } },
|
|
417
|
+
'crtr.tmux.menu.resume': { description: 'resume node', action: { kind: 'keys', keys: '/resume-node' } },
|
|
418
|
+
'crtr.tmux.menu.demote': { description: 'demote to terminal', action: { kind: 'command', run: 'node lifecycle demote --pane {pane}' } },
|
|
419
|
+
'crtr.tmux.menu.detach': { description: 'detach to background', action: { kind: 'command', run: 'node lifecycle demote --pane {pane} --detach' } },
|
|
420
|
+
'crtr.tmux.menu.close-subtree': { description: 'close agent + subtree', action: { kind: 'command', run: 'node lifecycle close --pane {pane}' } },
|
|
421
|
+
'crtr.tmux.menu.context': { description: 'browse context dirs', action: { kind: 'keys', keys: '/context' } },
|
|
422
|
+
'crtr.tmux.menu.providers': { description: 'manage providers', action: { kind: 'popup', run: 'sys setup' } },
|
|
423
|
+
'crtr.tmux.menu.graph': { description: 'graph view', action: { kind: 'keys', keys: '/graph' } },
|
|
424
|
+
'crtr.tmux.menu.focus-manager': { description: 'focus manager', action: { kind: 'command', run: 'node focus {manager}' } },
|
|
425
|
+
'crtr.tmux.menu.open': undefined,
|
|
426
|
+
'crtr.tmux.menu.issues': undefined,
|
|
427
|
+
'crtr.tmux.menu.issue.idea': undefined,
|
|
428
|
+
'crtr.tmux.menu.issue.bug': undefined,
|
|
429
|
+
'crtr.tmux.node.next': undefined,
|
|
430
|
+
'crtr.tmux.node.previous': undefined,
|
|
431
|
+
'crtr.tmux.view.next': undefined,
|
|
432
|
+
'crtr.tmux.view.previous': undefined,
|
|
433
|
+
};
|
|
409
434
|
function escapeTmuxFormats(value) {
|
|
410
435
|
return value.replace(/#/g, '##');
|
|
411
436
|
}
|
|
412
|
-
function
|
|
413
|
-
return `run-shell ${shellQuote(shellCmd)}`;
|
|
414
|
-
}
|
|
415
|
-
function tmuxCmd(args) {
|
|
437
|
+
function tmuxCommand(args) {
|
|
416
438
|
return args.map(shellQuote).join(' ');
|
|
417
439
|
}
|
|
418
|
-
function
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
function
|
|
425
|
-
|
|
440
|
+
function chordCommand(run, interactive = false) {
|
|
441
|
+
const command = `crtr canvas chord --pane '#{pane_id}' --run ${shellQuote(escapeTmuxFormats(run))}${interactive ? ' --interactive' : ''}`;
|
|
442
|
+
return interactive
|
|
443
|
+
? `display-popup -E -w 60% -h 50% -d "#{pane_current_path}" ${shellQuote(command)}`
|
|
444
|
+
: `run-shell ${shellQuote(`${command} >/dev/null 2>&1`)}`;
|
|
445
|
+
}
|
|
446
|
+
function renderMenuAction(action) {
|
|
447
|
+
switch (action.kind) {
|
|
448
|
+
case 'keys': return `send-keys -t '#{pane_id}' ${shellQuote(escapeTmuxFormats(action.keys))} Enter`;
|
|
449
|
+
case 'command': return chordCommand(action.run);
|
|
450
|
+
case 'popup': return chordCommand(action.run, true);
|
|
451
|
+
case 'submenu': return tmuxCommand(buildMenuArgs(action.title, action.items));
|
|
452
|
+
}
|
|
426
453
|
}
|
|
427
454
|
function buildMenuArgs(title, items) {
|
|
428
455
|
const safeTitle = escapeTmuxFormats(title);
|
|
456
|
+
const widestItem = items.reduce((width, item) => Math.max(width, escapeTmuxFormats(item.description).length), 0);
|
|
429
457
|
const args = ['display-menu', '-T', `#[align=centre]${safeTitle}`, ...surfaceTmuxStyleArgs()];
|
|
430
|
-
|
|
431
|
-
const nudgeX = 1;
|
|
432
|
-
const nudgeY = 3;
|
|
433
|
-
args.push('-x', `#{e|-:#{pane_right},${boxW + nudgeX}}`, '-y', `#{e|+:#{pane_top},${nudgeY}}`);
|
|
458
|
+
args.push('-x', `#{e|-:#{pane_right},${Math.max(safeTitle.length, widestItem) + 7}}`, '-y', '#{e|+:#{pane_top},3}');
|
|
434
459
|
for (const item of items) {
|
|
435
|
-
args.push(escapeTmuxFormats(item.
|
|
460
|
+
args.push(escapeTmuxFormats(item.description), item.selector, renderMenuAction(item.action));
|
|
436
461
|
}
|
|
437
462
|
args.push('-esc / q to close', '', '');
|
|
438
463
|
return args;
|
|
439
464
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
465
|
+
const TMUX_NAMED_KEYS = {
|
|
466
|
+
enter: 'Enter', escape: 'Escape', tab: 'Tab', backspace: 'BSpace', delete: 'DC',
|
|
467
|
+
space: 'Space', up: 'Up', down: 'Down', left: 'Left', right: 'Right',
|
|
468
|
+
pageup: 'PPage', pagedown: 'NPage', home: 'Home', end: 'End',
|
|
469
|
+
};
|
|
470
|
+
function tmuxKey(stroke) {
|
|
471
|
+
const parts = stroke.split('+');
|
|
472
|
+
let key = parts.pop();
|
|
473
|
+
const modifiers = new Set(parts);
|
|
474
|
+
if (modifiers.has('shift') && key.length === 1 && /[a-z]/.test(key)) {
|
|
475
|
+
key = key.toUpperCase();
|
|
476
|
+
modifiers.delete('shift');
|
|
477
|
+
}
|
|
478
|
+
key = TMUX_NAMED_KEYS[key] ?? key;
|
|
479
|
+
const prefix = [
|
|
480
|
+
modifiers.has('ctrl') ? 'C-' : '',
|
|
481
|
+
modifiers.has('alt') ? 'M-' : '',
|
|
482
|
+
modifiers.has('shift') ? 'S-' : '',
|
|
483
|
+
].join('');
|
|
484
|
+
return `${prefix}${key}`;
|
|
485
|
+
}
|
|
486
|
+
function menuSelector(stroke) {
|
|
487
|
+
const parts = stroke.split('+');
|
|
488
|
+
const key = parts.at(-1);
|
|
489
|
+
return parts.includes('shift') && /[a-z]/.test(key) ? key.toUpperCase() : key === 'space' ? ' ' : key;
|
|
490
|
+
}
|
|
491
|
+
function menuItems(bindings) {
|
|
492
|
+
const issueItems = [];
|
|
493
|
+
for (const [id, description, type] of [
|
|
494
|
+
['crtr.tmux.menu.issue.idea', 'idea', 'idea'],
|
|
495
|
+
['crtr.tmux.menu.issue.bug', 'bug', 'bug'],
|
|
496
|
+
]) {
|
|
497
|
+
for (const gesture of bindings.gestures(id)) {
|
|
498
|
+
issueItems.push({ description, selector: menuSelector(gesture), action: { kind: 'popup', run: `canvas issue new --type ${type}` } });
|
|
499
|
+
}
|
|
453
500
|
}
|
|
501
|
+
const items = [];
|
|
502
|
+
for (const id of [
|
|
503
|
+
'crtr.tmux.menu.promote', 'crtr.tmux.menu.resume', 'crtr.tmux.menu.demote',
|
|
504
|
+
'crtr.tmux.menu.detach', 'crtr.tmux.menu.close-subtree', 'crtr.tmux.menu.context',
|
|
505
|
+
'crtr.tmux.menu.providers', 'crtr.tmux.menu.graph', 'crtr.tmux.menu.focus-manager', 'crtr.tmux.menu.issues',
|
|
506
|
+
]) {
|
|
507
|
+
const entry = id === 'crtr.tmux.menu.issues'
|
|
508
|
+
? { description: 'new issue / todo', action: { kind: 'submenu', title: 'new issue / todo', items: issueItems } }
|
|
509
|
+
: MENU_ACTIONS[id];
|
|
510
|
+
if (id === 'crtr.tmux.menu.issues' && issueItems.length === 0)
|
|
511
|
+
continue;
|
|
512
|
+
for (const gesture of bindings.gestures(id)) {
|
|
513
|
+
items.push({ description: entry.description, selector: menuSelector(gesture), action: entry.action });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return items;
|
|
517
|
+
}
|
|
518
|
+
function bindingAt(table, key) {
|
|
519
|
+
const result = tmux(['list-keys', '-T', table]);
|
|
520
|
+
if (!result.ok)
|
|
521
|
+
return null;
|
|
522
|
+
return result.stdout.split('\n').find((line) => {
|
|
523
|
+
const match = line.match(/^bind-key\s+(?:-r\s+)?-T\s+\S+\s+(\S+)\s+/);
|
|
524
|
+
return match?.[1] === key;
|
|
525
|
+
}) ?? null;
|
|
454
526
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
527
|
+
function sequenceTable(prefix) {
|
|
528
|
+
return `crtr-${Buffer.from(prefix).toString('hex')}`;
|
|
529
|
+
}
|
|
530
|
+
function readManifest() {
|
|
531
|
+
const option = tmux(['show-options', '-s', '-q', TMUX_BINDING_MANIFEST]);
|
|
532
|
+
if (!option.ok)
|
|
533
|
+
return null;
|
|
534
|
+
if (option.stdout === '')
|
|
535
|
+
return { pairs: [], existed: false };
|
|
536
|
+
const result = tmux(['show-options', '-s', '-qv', TMUX_BINDING_MANIFEST]);
|
|
537
|
+
if (!result.ok)
|
|
538
|
+
return null;
|
|
458
539
|
try {
|
|
459
|
-
|
|
540
|
+
const value = JSON.parse(result.stdout);
|
|
541
|
+
if (!Array.isArray(value) || value.some((pair) => typeof pair !== 'object' || pair === null || typeof pair.table !== 'string' || typeof pair.key !== 'string'))
|
|
542
|
+
return null;
|
|
543
|
+
return { pairs: value, existed: true };
|
|
460
544
|
}
|
|
461
545
|
catch {
|
|
462
|
-
|
|
546
|
+
return null;
|
|
463
547
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
548
|
+
}
|
|
549
|
+
const LEGACY_ROOT_BINDINGS = [
|
|
550
|
+
{ key: 'M-c', markers: ['display-menu', 'promote to orchestrator', 'crtr canvas chord', 'new issue / todo'] },
|
|
551
|
+
{ key: 'M-]', markers: ['run-shell', 'crtr node cycle --dir next', "--pane '#{pane_id}'"] },
|
|
552
|
+
{ key: 'M-[', markers: ['run-shell', 'crtr node cycle --dir prev', "--pane '#{pane_id}'"] },
|
|
553
|
+
{ key: 'M-v', markers: ['switch-client', '-T crtr-view'] },
|
|
554
|
+
];
|
|
555
|
+
function adoptLegacyBindings() {
|
|
556
|
+
for (const legacy of LEGACY_ROOT_BINDINGS) {
|
|
557
|
+
const existing = bindingAt(ROOT_TABLE, legacy.key);
|
|
558
|
+
if (existing !== null && legacy.markers.every((marker) => existing.includes(marker))) {
|
|
559
|
+
tmux(['unbind-key', '-T', ROOT_TABLE, legacy.key]);
|
|
560
|
+
}
|
|
470
561
|
}
|
|
471
|
-
return tmux(['bind-key', '-n', 'M-c', ...buildMenuArgs(' crtr ', items)]).ok;
|
|
472
562
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
* false if either bind fails. NOTE: Alt+[ is only delivered cleanly when the
|
|
482
|
-
* terminal/tmux disambiguate it from a raw CSI introducer (`extended-keys on`).
|
|
483
|
-
*/
|
|
484
|
-
export function installNavBindings() {
|
|
485
|
-
const next = tmux([
|
|
486
|
-
'bind-key', '-n', 'M-]', 'run-shell',
|
|
487
|
-
`crtr node cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`,
|
|
488
|
-
]).ok;
|
|
489
|
-
const prev = tmux([
|
|
490
|
-
'bind-key', '-n', 'M-[', 'run-shell',
|
|
491
|
-
`crtr node cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`,
|
|
492
|
-
]).ok;
|
|
493
|
-
return next && prev;
|
|
563
|
+
function actionCommand(id) {
|
|
564
|
+
switch (id) {
|
|
565
|
+
case 'crtr.tmux.node.next': return ['run-shell', `crtr node cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`];
|
|
566
|
+
case 'crtr.tmux.node.previous': return ['run-shell', `crtr node cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`];
|
|
567
|
+
case 'crtr.tmux.view.next': return ['run-shell', `crtr surface view cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`];
|
|
568
|
+
case 'crtr.tmux.view.previous': return ['run-shell', `crtr surface view cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`];
|
|
569
|
+
default: throw new Error(`no tmux command for ${id}`);
|
|
570
|
+
}
|
|
494
571
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
])
|
|
533
|
-
|
|
572
|
+
/** Reconcile the tmux server with one immutable crouter binding snapshot. */
|
|
573
|
+
export function installTmuxBindings(bindings = resolveUserKeybindings()) {
|
|
574
|
+
const diagnostics = [];
|
|
575
|
+
const installed = [];
|
|
576
|
+
const manifest = readManifest();
|
|
577
|
+
if (manifest === null) {
|
|
578
|
+
diagnostics.push({ code: 'tmux-error', message: `tmux is unavailable or ${TMUX_BINDING_MANIFEST} is malformed` });
|
|
579
|
+
return { ok: false, installed, diagnostics };
|
|
580
|
+
}
|
|
581
|
+
if (!manifest.existed)
|
|
582
|
+
adoptLegacyBindings();
|
|
583
|
+
for (const pair of manifest.pairs) {
|
|
584
|
+
const result = tmux(['unbind-key', '-T', pair.table, pair.key]);
|
|
585
|
+
if (!result.ok && bindingAt(pair.table, pair.key) !== null) {
|
|
586
|
+
diagnostics.push({ code: 'tmux-error', message: `could not remove crouter binding ${pair.table}:${pair.key}: ${result.stderr}` });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (diagnostics.some((diagnostic) => diagnostic.code === 'tmux-error')) {
|
|
590
|
+
return { ok: false, installed, diagnostics };
|
|
591
|
+
}
|
|
592
|
+
const install = (table, key, command, id, gesture) => {
|
|
593
|
+
if (bindingAt(table, key) !== null) {
|
|
594
|
+
diagnostics.push({ code: 'occupied', bindingId: id, gesture, message: `${table}:${key} is already bound by tmux; ${id} was not installed` });
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
const result = tmux(['bind-key', '-T', table, key, ...command]);
|
|
598
|
+
if (!result.ok) {
|
|
599
|
+
diagnostics.push({ code: 'tmux-error', bindingId: id, gesture, message: `could not install ${id}: ${result.stderr}` });
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
installed.push({ table, key });
|
|
603
|
+
return true;
|
|
604
|
+
};
|
|
605
|
+
const menuCommand = buildMenuArgs(' crtr ', menuItems(bindings));
|
|
606
|
+
for (const gesture of bindings.gestures('crtr.tmux.menu.open')) {
|
|
607
|
+
install(ROOT_TABLE, tmuxKey(gesture), menuCommand, 'crtr.tmux.menu.open', gesture);
|
|
608
|
+
}
|
|
609
|
+
for (const id of ['crtr.tmux.node.next', 'crtr.tmux.node.previous']) {
|
|
610
|
+
for (const gesture of bindings.gestures(id))
|
|
611
|
+
install(ROOT_TABLE, tmuxKey(gesture), actionCommand(id), id, gesture);
|
|
612
|
+
}
|
|
613
|
+
const sequences = new Map();
|
|
614
|
+
for (const id of ['crtr.tmux.view.next', 'crtr.tmux.view.previous']) {
|
|
615
|
+
for (const gesture of bindings.gestures(id)) {
|
|
616
|
+
const [first, second] = gesture.split(' > ');
|
|
617
|
+
const prefix = tmuxKey(first);
|
|
618
|
+
const entries = sequences.get(prefix) ?? [];
|
|
619
|
+
entries.push({ id, gesture, second: tmuxKey(second) });
|
|
620
|
+
sequences.set(prefix, entries);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
for (const [prefix, entries] of sequences) {
|
|
624
|
+
if (bindingAt(ROOT_TABLE, prefix) !== null) {
|
|
625
|
+
for (const entry of entries)
|
|
626
|
+
diagnostics.push({ code: 'occupied', bindingId: entry.id, gesture: entry.gesture, message: `${ROOT_TABLE}:${prefix} is already bound by tmux; ${entry.id} was not installed` });
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const table = sequenceTable(prefix);
|
|
630
|
+
let children = 0;
|
|
631
|
+
for (const entry of entries)
|
|
632
|
+
if (install(table, entry.second, actionCommand(entry.id), entry.id, entry.gesture))
|
|
633
|
+
children++;
|
|
634
|
+
if (children > 0)
|
|
635
|
+
install(ROOT_TABLE, prefix, ['switch-client', '-T', table], entries[0].id, entries[0].gesture);
|
|
636
|
+
}
|
|
637
|
+
const save = tmux(['set-option', '-s', TMUX_BINDING_MANIFEST, JSON.stringify(installed)]);
|
|
638
|
+
if (!save.ok)
|
|
639
|
+
diagnostics.push({ code: 'tmux-error', message: `could not save crouter tmux ownership manifest: ${save.stderr}` });
|
|
640
|
+
return { ok: diagnostics.every((diagnostic) => diagnostic.code !== 'tmux-error'), installed, diagnostics };
|
|
534
641
|
}
|
package/dist/core/scope.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
-
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { existsSync, statSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { join, resolve, dirname } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { CRTR_DIR_NAME } from '../types.js';
|
|
@@ -48,6 +48,14 @@ export function builtinPiPackageDir(name) {
|
|
|
48
48
|
export function userScopeRoot() {
|
|
49
49
|
return join(homedir(), CRTR_DIR_NAME);
|
|
50
50
|
}
|
|
51
|
+
function realpathOrSelf(p) {
|
|
52
|
+
try {
|
|
53
|
+
return realpathSync(p);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return p;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
51
59
|
function isProjectScopeDir(candidate, userRoot) {
|
|
52
60
|
if (candidate === userRoot || !existsSync(candidate))
|
|
53
61
|
return false;
|
|
@@ -61,16 +69,31 @@ function isProjectScopeDir(candidate, userRoot) {
|
|
|
61
69
|
export function findProjectScopeRoot(startDir = process.cwd()) {
|
|
62
70
|
return findProjectScopeRoots(startDir)[0] ?? null;
|
|
63
71
|
}
|
|
64
|
-
/** Walk `startDir` upward
|
|
65
|
-
*
|
|
66
|
-
*
|
|
72
|
+
/** Walk `startDir` upward collecting every ancestor `.crouter/` dir (nearest
|
|
73
|
+
* first). The user-global `~/.crouter/` is excluded because it is the separate
|
|
74
|
+
* user scope, not a project ancestor.
|
|
75
|
+
*
|
|
76
|
+
* The walk is FENCED at `homedir()`: it must never climb at or above HOME.
|
|
77
|
+
* Home's own `.crouter/` is the user scope (already excluded), and nothing at
|
|
78
|
+
* or above HOME is a project ancestor. Without this fence, a relocated HOME
|
|
79
|
+
* nested under a directory tree that itself contains a `.crouter/` (e.g. a
|
|
80
|
+
* mock-VM home under the real host home) would sweep the host's `~/.crouter`
|
|
81
|
+
* (and any other above-HOME `.crouter`) in as a bogus project ancestor,
|
|
82
|
+
* leaking one machine's user-global memory into a nested-HOME node. Mirrors
|
|
83
|
+
* the on-read positional-walk fence (src/core/substrate/on-read.ts). A read
|
|
84
|
+
* whose start dir is outside HOME (e.g. a production VM with HOME=/root and no
|
|
85
|
+
* ancestor `.crouter`) never reaches home and walks to the filesystem root, as
|
|
86
|
+
* before. */
|
|
67
87
|
function collectAncestorScopeRoots(startDir, userRoot) {
|
|
68
88
|
const roots = [];
|
|
89
|
+
const home = realpathOrSelf(homedir());
|
|
69
90
|
let dir = resolve(startDir);
|
|
70
91
|
while (true) {
|
|
71
92
|
const candidate = join(dir, CRTR_DIR_NAME);
|
|
72
93
|
if (isProjectScopeDir(candidate, userRoot))
|
|
73
94
|
roots.push(candidate);
|
|
95
|
+
if (realpathOrSelf(dir) === home)
|
|
96
|
+
return roots;
|
|
74
97
|
const parent = dirname(dir);
|
|
75
98
|
if (parent === dir)
|
|
76
99
|
return roots;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
export declare const ANTHROPIC_PROVIDER_ID = "anthropic";
|
|
4
|
+
export declare const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
|
5
|
+
export type ManagedProviderId = typeof ANTHROPIC_PROVIDER_ID | typeof OPENAI_CODEX_PROVIDER_ID;
|
|
6
|
+
export type LadderProviderId = "anthropic" | "openai";
|
|
7
|
+
export type ModelStrength = "ultra" | "strong" | "medium" | "light";
|
|
8
|
+
export type SubscriptionCredential = {
|
|
9
|
+
label: string;
|
|
10
|
+
refresh?: string;
|
|
11
|
+
access?: string;
|
|
12
|
+
expires?: number;
|
|
13
|
+
rateLimitedUntil: number;
|
|
14
|
+
lastAttemptAt: number;
|
|
15
|
+
lastRateLimitedAt: number;
|
|
16
|
+
accountId?: string;
|
|
17
|
+
};
|
|
18
|
+
export type ModelRef = {
|
|
19
|
+
providerId: ManagedProviderId;
|
|
20
|
+
modelId: string;
|
|
21
|
+
};
|
|
22
|
+
export type RotationConfig = {
|
|
23
|
+
defaultFallbackStrength?: ModelStrength;
|
|
24
|
+
revertWhenAvailable?: boolean;
|
|
25
|
+
preferredModel?: ModelRef;
|
|
26
|
+
};
|
|
27
|
+
type RotationConfigUpdate = {
|
|
28
|
+
defaultFallbackStrength?: ModelStrength;
|
|
29
|
+
revertWhenAvailable?: boolean;
|
|
30
|
+
preferredModel?: ModelRef | null;
|
|
31
|
+
};
|
|
32
|
+
export type FallbackTarget = {
|
|
33
|
+
providerId: ManagedProviderId;
|
|
34
|
+
modelId: string;
|
|
35
|
+
label: string;
|
|
36
|
+
strength: ModelStrength;
|
|
37
|
+
thinkingLevel?: ThinkingLevel;
|
|
38
|
+
};
|
|
39
|
+
export declare function isManagedProvider(providerId: string | undefined): providerId is ManagedProviderId;
|
|
40
|
+
/** The `modelLadders` CONFIG key for a runtime provider id -- `openai-codex`
|
|
41
|
+
* (the runtime/auth id) maps to the `openai` ladder key used in
|
|
42
|
+
* ~/.crouter/config.json. Callers building a repair command that a user can
|
|
43
|
+
* paste into `crtr sys config set modelLadders.<key>...` must use THIS key,
|
|
44
|
+
* not the runtime provider id. */
|
|
45
|
+
export declare function getLadderProviderId(providerId: ManagedProviderId): LadderProviderId;
|
|
46
|
+
export declare function getProviderLabel(providerId: ManagedProviderId): string;
|
|
47
|
+
export declare function mutateSubscriptionPool(providerId: ManagedProviderId, fn: (pool: SubscriptionCredential[]) => SubscriptionCredential[] | undefined): SubscriptionCredential[];
|
|
48
|
+
export declare function readSubscriptionPool(providerId: ManagedProviderId): SubscriptionCredential[];
|
|
49
|
+
export declare function writeSubscriptionPool(providerId: ManagedProviderId, next: SubscriptionCredential[]): SubscriptionCredential[];
|
|
50
|
+
export declare function upsertSubscription(providerId: ManagedProviderId, next: SubscriptionCredential): SubscriptionCredential[];
|
|
51
|
+
export declare class DuplicateSubscriptionError extends Error {
|
|
52
|
+
constructor(message: string);
|
|
53
|
+
}
|
|
54
|
+
export declare function addSubscription(providerId: ManagedProviderId, entry: SubscriptionCredential): SubscriptionCredential[];
|
|
55
|
+
export declare function upsertSubscriptionWithUniqueAccount(providerId: ManagedProviderId, next: SubscriptionCredential): SubscriptionCredential[];
|
|
56
|
+
export type DefaultSlotLogin = {
|
|
57
|
+
refresh: string;
|
|
58
|
+
access: string;
|
|
59
|
+
expires: number;
|
|
60
|
+
accountId?: string;
|
|
61
|
+
lastRateLimitedAt: number;
|
|
62
|
+
};
|
|
63
|
+
export declare function commitDefaultIdentity(providerId: ManagedProviderId, login: DefaultSlotLogin): SubscriptionCredential[];
|
|
64
|
+
export declare function renameSubscription(providerId: ManagedProviderId, ref: string, nextLabel: string): SubscriptionCredential[];
|
|
65
|
+
export declare function removeManagedAccount(providerId: ManagedProviderId, ref: string): SubscriptionCredential[];
|
|
66
|
+
export declare function commitManagedLogin(providerId: ManagedProviderId, label: string, credential: {
|
|
67
|
+
refresh: string;
|
|
68
|
+
access: string;
|
|
69
|
+
expires: number;
|
|
70
|
+
accountId?: string;
|
|
71
|
+
defaultLastRateLimitedAt: number;
|
|
72
|
+
}): SubscriptionCredential[];
|
|
73
|
+
export declare function removeManagedProvider(providerId: ManagedProviderId, label?: string): SubscriptionCredential[];
|
|
74
|
+
export declare function removeSubscription(providerId: ManagedProviderId, ref: string): SubscriptionCredential[];
|
|
75
|
+
export declare function promoteSubscription(providerId: ManagedProviderId, ref: string): SubscriptionCredential[];
|
|
76
|
+
export declare function markSubscriptionAttempt(providerId: ManagedProviderId, ref: string, attemptAt?: number): SubscriptionCredential[];
|
|
77
|
+
export declare function markSubscriptionRateLimited(providerId: ManagedProviderId, ref: string, retryAfterMs: number, attemptAt?: number, rateLimitedAt?: number): SubscriptionCredential[];
|
|
78
|
+
export declare function markSubscriptionSuccess(providerId: ManagedProviderId, ref: string, attemptAt?: number): SubscriptionCredential[];
|
|
79
|
+
export declare function findAvailableSubscription(providerId: ManagedProviderId, now?: number): SubscriptionCredential | undefined;
|
|
80
|
+
export declare function parseRetryAfterHeader(value: string | undefined, now?: number): number | undefined;
|
|
81
|
+
export declare function readRotationConfig(): RotationConfig;
|
|
82
|
+
export declare function writeRotationConfig(next: RotationConfigUpdate): RotationConfig;
|
|
83
|
+
export declare function clearPreferredModel(): RotationConfig;
|
|
84
|
+
export declare function rememberPreferredModel(model: ModelRef): RotationConfig;
|
|
85
|
+
export declare function resolveFallbackTarget(currentProviderId: ManagedProviderId, currentModelId?: string, config?: RotationConfig, currentThinkingLevel?: string): FallbackTarget | undefined;
|
|
86
|
+
export declare function shouldRestorePreferredModel(config?: RotationConfig, now?: number): boolean;
|
|
87
|
+
export declare function restorePreferredModelIfPossible(piApi: ExtensionAPI, ctx: Pick<ExtensionContext, "model" | "modelRegistry">): Promise<boolean>;
|
|
88
|
+
export declare function switchToFallbackIfPossible(piApi: ExtensionAPI, ctx: Pick<ExtensionContext, "model" | "modelRegistry">, preferredModel?: ModelRef | undefined): Promise<FallbackTarget | undefined>;
|
|
89
|
+
export declare function formatStatusLine(providerId: ManagedProviderId, now?: number): string;
|
|
90
|
+
export {};
|