@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
package/dist/core/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { CONFIG_FILE, STATE_FILE, defaultScopeConfig, defaultScopeState, defaultModelLaddersConfig, defaultKindsConfig, defaultRemoteCanvasConfig } from '../types.js';
|
|
5
|
+
import { atomicWriteJson, readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
4
6
|
import { scopeRoot, requireScopeRoot, findProjectScopeRoots } from './scope.js';
|
|
5
7
|
import { profileRoot } from './profiles/manifest.js';
|
|
6
8
|
function configPathFor(root) {
|
|
@@ -41,10 +43,60 @@ export function readState(scope) {
|
|
|
41
43
|
activeCanvas: existing.activeCanvas ?? null,
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
|
-
|
|
46
|
+
const CONFIG_LOCK_TIMEOUT_MS = 5000;
|
|
47
|
+
const CONFIG_LOCK_POLL_MS = 25;
|
|
48
|
+
function sleepSync(ms) {
|
|
49
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
50
|
+
}
|
|
51
|
+
function withConfigPathLock(path, run) {
|
|
52
|
+
ensureDir(dirname(path));
|
|
53
|
+
const lockPath = `${path}.lock`;
|
|
54
|
+
const deadline = Date.now() + CONFIG_LOCK_TIMEOUT_MS;
|
|
55
|
+
for (;;) {
|
|
56
|
+
try {
|
|
57
|
+
mkdirSync(lockPath);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code !== 'EEXIST')
|
|
62
|
+
throw error;
|
|
63
|
+
if (Date.now() > deadline)
|
|
64
|
+
throw new Error(`timed out waiting for config lock: ${path}`);
|
|
65
|
+
sleepSync(CONFIG_LOCK_POLL_MS);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return run(path);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function withConfigLock(scope, run) {
|
|
45
76
|
const root = requireScopeRoot(scope);
|
|
46
|
-
|
|
47
|
-
|
|
77
|
+
return withConfigPathLock(configPathFor(root), run);
|
|
78
|
+
}
|
|
79
|
+
export function updateRawConfigAtomically(scope, mutate) {
|
|
80
|
+
return withConfigLock(scope, (path) => {
|
|
81
|
+
const latest = readJsonIfExists(path) ?? {};
|
|
82
|
+
const next = mutate(latest);
|
|
83
|
+
atomicWriteJson(path, next);
|
|
84
|
+
return next;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export function writeConfig(scope, config) {
|
|
88
|
+
withConfigLock(scope, (path) => atomicWriteJson(path, config));
|
|
89
|
+
}
|
|
90
|
+
/** Persist a RAW PARTIAL config to a scope's config.json exactly as given.
|
|
91
|
+
* Unlike `writeConfig`, this makes no promise that every field is present --
|
|
92
|
+
* callers pass the sparse partial that should live on disk, so an untouched
|
|
93
|
+
* key stays absent (and `readConfig`/`mergeConfig` re-derive it live from
|
|
94
|
+
* whichever build is running). This is the single central primitive every
|
|
95
|
+
* mutation path funnels through so no writer ever freezes a fully
|
|
96
|
+
* materialized `modelLadders` (concrete model ids that go stale across
|
|
97
|
+
* builds -- the live-Hearth freeze bug) onto disk. */
|
|
98
|
+
export function writeRawConfig(scope, config) {
|
|
99
|
+
withConfigLock(scope, (path) => atomicWriteJson(path, config));
|
|
48
100
|
}
|
|
49
101
|
export function writeState(scope, state) {
|
|
50
102
|
const root = requireScopeRoot(scope);
|
|
@@ -53,105 +105,39 @@ export function writeState(scope, state) {
|
|
|
53
105
|
}
|
|
54
106
|
export function ensureScopeInitialized(scope, root) {
|
|
55
107
|
ensureDir(root);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
function
|
|
79
|
-
if (raw === null || typeof raw !== 'object')
|
|
80
|
-
return
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (r.run === '__graph__')
|
|
86
|
-
return { kind: 'keys', keys: '/graph', ...desc };
|
|
87
|
-
if (r.run === '__issue__') {
|
|
88
|
-
return {
|
|
89
|
-
kind: 'menu',
|
|
90
|
-
items: [
|
|
91
|
-
{ key: 'i', desc: 'idea', action: { kind: 'popup', run: 'canvas issue new --type idea' } },
|
|
92
|
-
{ key: 'b', desc: 'bug', action: { kind: 'popup', run: 'canvas issue new --type bug' } },
|
|
93
|
-
],
|
|
94
|
-
...desc,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (r.kind === 'keys' && typeof r.keys === 'string')
|
|
99
|
-
return { kind: 'keys', keys: r.keys, ...desc };
|
|
100
|
-
if (r.kind === 'popup' && typeof r.run === 'string')
|
|
101
|
-
return { kind: 'popup', run: r.run, ...desc };
|
|
102
|
-
if (r.kind === 'menu' && Array.isArray(r.items)) {
|
|
103
|
-
const items = r.items.map(normalizePrefixMenuItem).filter((item) => item !== null);
|
|
104
|
-
if (items.length !== r.items.length)
|
|
105
|
-
return null;
|
|
106
|
-
return { kind: 'menu', items, ...desc };
|
|
107
|
-
}
|
|
108
|
-
if ((r.kind === undefined || r.kind === 'crtr') && typeof r.run === 'string') {
|
|
109
|
-
return { kind: 'crtr', run: r.run, ...confirm, ...desc };
|
|
110
|
-
}
|
|
111
|
-
if (r.kind === undefined && typeof r.keys === 'string')
|
|
112
|
-
return { kind: 'keys', keys: r.keys, ...desc };
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
function normalizeGraphBind(raw) {
|
|
116
|
-
if (raw === null || typeof raw !== 'object')
|
|
117
|
-
return null;
|
|
118
|
-
const r = raw;
|
|
119
|
-
if (typeof r.run !== 'string')
|
|
120
|
-
return null;
|
|
121
|
-
return {
|
|
122
|
-
run: r.run,
|
|
123
|
-
...(r.confirm === true ? { confirm: true } : {}),
|
|
124
|
-
...(typeof r.desc === 'string' && r.desc !== '' ? { desc: r.desc } : {}),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
/** Deep-merge one bind table over its built-in defaults: a user adding a single
|
|
128
|
-
* bind must not wipe the rest. Each user entry is normalized before it
|
|
129
|
-
* replaces/extends a key. */
|
|
130
|
-
function mergeBinds(base, over, normalize) {
|
|
131
|
-
const out = { ...base };
|
|
132
|
-
if (over !== null && typeof over === 'object') {
|
|
133
|
-
for (const [k, v] of Object.entries(over)) {
|
|
134
|
-
const normalized = normalize(v);
|
|
135
|
-
if (normalized !== null)
|
|
136
|
-
out[k] = normalized;
|
|
108
|
+
withConfigPathLock(configPathFor(root), (cfgPath) => {
|
|
109
|
+
if (readJsonIfExists(cfgPath))
|
|
110
|
+
return;
|
|
111
|
+
// Persist no materialized model ladder or keybinding defaults. `modelLadders` holds concrete model ids (e.g.
|
|
112
|
+
// "openai-codex/gpt-5.6-sol:high") that are only valid as long as they match whatever
|
|
113
|
+
// pi model registry this crouter build ships. Baking a materialized snapshot here would
|
|
114
|
+
// freeze this scope's ladder to whatever ids happened to be current the moment it was
|
|
115
|
+
// first initialized -- a later crouter/pi upgrade (or a rollback) that changes the
|
|
116
|
+
// registry can then never reach it: the stale persisted override still wins over the
|
|
117
|
+
// new build's compiled defaults in `mergeConfig`/`mergeModelLadders`, producing a
|
|
118
|
+
// fallback rung `resolveViableFallback` rejects as unregistered while the terminal
|
|
119
|
+
// error misleadingly claims the fallback provider needs authenticating (see live
|
|
120
|
+
// incident: a home's config.json was seeded with `gpt-5.6-*` ladder entries from a
|
|
121
|
+
// build newer than the one actually installed, so its own model registry never had
|
|
122
|
+
// them). Omitting the key here is safe: `readConfig`/`mergeConfig` already default-fill
|
|
123
|
+
// a missing `modelLadders` from whatever build is CURRENTLY running, and an explicit
|
|
124
|
+
// user override (`sys config set modelLadders...`) still persists and still wins.
|
|
125
|
+
// Keybindings are sparse user overrides, so an untouched scope omits that member too.
|
|
126
|
+
const { modelLadders: _omittedModelLadders, keybindings: _omittedKeybindings, ...rest } = defaultScopeConfig();
|
|
127
|
+
atomicWriteJson(cfgPath, rest);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function normalizeKeybindings(raw) {
|
|
131
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
132
|
+
return {};
|
|
133
|
+
const out = {};
|
|
134
|
+
for (const [id, gestures] of Object.entries(raw)) {
|
|
135
|
+
if (Array.isArray(gestures) && gestures.every((gesture) => typeof gesture === 'string')) {
|
|
136
|
+
out[id] = [...gestures];
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
return out;
|
|
140
140
|
}
|
|
141
|
-
/** Validate + deep-merge a user `canvasNav` block over the built-in defaults.
|
|
142
|
-
* An absent or malformed block falls back wholesale to defaults. */
|
|
143
|
-
function mergeCanvasNav(raw) {
|
|
144
|
-
const defaults = defaultCanvasNavConfig();
|
|
145
|
-
if (raw === null || typeof raw !== 'object')
|
|
146
|
-
return defaults;
|
|
147
|
-
const r = raw;
|
|
148
|
-
const prefixKey = typeof r.prefixKey === 'string' && r.prefixKey.trim() !== '' ? r.prefixKey : defaults.prefixKey;
|
|
149
|
-
return {
|
|
150
|
-
prefixKey,
|
|
151
|
-
prefixBinds: mergeBinds(defaults.prefixBinds, r.prefixBinds, normalizePrefixBind),
|
|
152
|
-
graphBinds: mergeBinds(defaults.graphBinds, r.graphBinds, normalizeGraphBind),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
141
|
function mergeModelLadders(raw, base = defaultModelLaddersConfig()) {
|
|
156
142
|
const defaults = base;
|
|
157
143
|
if (raw === null || typeof raw !== 'object')
|
|
@@ -228,18 +214,55 @@ function normalizeKindEntry(raw) {
|
|
|
228
214
|
out.availableTo = r.availableTo;
|
|
229
215
|
return out;
|
|
230
216
|
}
|
|
217
|
+
/** Validate one raw `kinds.<name>` entry as a sparse PATCH — an entry with no
|
|
218
|
+
* `whenToUse` that overrides individual launch knobs (model/orchestratorModel/
|
|
219
|
+
* tools/extensions/availableTo) of a kind some lower-precedence scope already
|
|
220
|
+
* defines. This is what lets a scope (e.g. a profile) persist ONLY
|
|
221
|
+
* `{ "model": "ultra" }` for a kind instead of freezing a full registry
|
|
222
|
+
* snapshot to disk (the same staleness trap `sparseModelLaddersToPersist`
|
|
223
|
+
* exists to avoid). Returns null when no valid field is present. */
|
|
224
|
+
function normalizeKindPatch(raw) {
|
|
225
|
+
if (raw === null || typeof raw !== 'object')
|
|
226
|
+
return null;
|
|
227
|
+
const r = raw;
|
|
228
|
+
const out = {};
|
|
229
|
+
if (typeof r.model === 'string' && r.model.trim() !== '')
|
|
230
|
+
out.model = r.model;
|
|
231
|
+
if (typeof r.orchestratorModel === 'string' && r.orchestratorModel.trim() !== '')
|
|
232
|
+
out.orchestratorModel = r.orchestratorModel;
|
|
233
|
+
const isStringArray = (v) => Array.isArray(v) && v.every((item) => typeof item === 'string');
|
|
234
|
+
if (isStringArray(r.tools))
|
|
235
|
+
out.tools = r.tools;
|
|
236
|
+
if (isStringArray(r.extensions))
|
|
237
|
+
out.extensions = r.extensions;
|
|
238
|
+
if (isStringArray(r.availableTo))
|
|
239
|
+
out.availableTo = r.availableTo;
|
|
240
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
241
|
+
}
|
|
231
242
|
/** Merge a raw `kinds` block over `base` (defaulting to the builtin registry):
|
|
232
243
|
* each valid entry adds or shadows a kind by name; invalid entries are
|
|
233
244
|
* dropped, never thrown. Mirrors `mergeModelLadders`'s layer-over-defaults
|
|
234
245
|
* shape so a user/project config.json can add or override a single kind
|
|
235
|
-
* without restating the whole registry.
|
|
246
|
+
* without restating the whole registry. Two entry shapes:
|
|
247
|
+
* - FULL (has `whenToUse`): defines or wholly replaces the kind, as before.
|
|
248
|
+
* - PATCH (no `whenToUse`, ≥1 valid launch field): field-merges over the
|
|
249
|
+
* kind the layers below already define (dropped when they don't) — the
|
|
250
|
+
* sparse-override shape `persistDefaultKindModel` writes. */
|
|
236
251
|
function mergeKinds(raw, base = defaultKindsConfig()) {
|
|
237
252
|
const out = { ...base };
|
|
238
253
|
if (raw !== null && typeof raw === 'object') {
|
|
239
254
|
for (const [kind, value] of Object.entries(raw)) {
|
|
240
255
|
const normalized = normalizeKindEntry(value);
|
|
241
|
-
if (normalized !== null)
|
|
256
|
+
if (normalized !== null) {
|
|
242
257
|
out[kind] = normalized;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const existing = out[kind];
|
|
261
|
+
if (existing === undefined)
|
|
262
|
+
continue;
|
|
263
|
+
const patch = normalizeKindPatch(value);
|
|
264
|
+
if (patch !== null)
|
|
265
|
+
out[kind] = { ...existing, ...patch };
|
|
243
266
|
}
|
|
244
267
|
}
|
|
245
268
|
return out;
|
|
@@ -307,12 +330,12 @@ function mergeConfig(partial) {
|
|
|
307
330
|
const max_panes_per_window = typeof rawMaxPanes === 'number' && Number.isFinite(rawMaxPanes) && rawMaxPanes >= 1
|
|
308
331
|
? Math.floor(rawMaxPanes)
|
|
309
332
|
: defaults.max_panes_per_window;
|
|
310
|
-
const
|
|
333
|
+
const keybindings = normalizeKeybindings(partial.keybindings);
|
|
311
334
|
const modelLadders = mergeModelLadders(partial.modelLadders);
|
|
312
335
|
const kinds = mergeKinds(partial.kinds);
|
|
313
336
|
const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
|
|
314
337
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
315
|
-
return { schema_version, marketplaces, plugins, auto_update, max_panes_per_window,
|
|
338
|
+
return { schema_version, marketplaces, plugins, auto_update, max_panes_per_window, keybindings, modelLadders, kinds, remoteCanvas, spawnEnv };
|
|
316
339
|
}
|
|
317
340
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
318
341
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -320,7 +343,7 @@ function mergeConfig(partial) {
|
|
|
320
343
|
* lower-precedence scope's real customization (see that function's comment
|
|
321
344
|
* for why `readConfig(scope)`, which already defaults-fills, is unusable
|
|
322
345
|
* for cross-scope layering). */
|
|
323
|
-
function readRawScopeConfig(scope) {
|
|
346
|
+
export function readRawScopeConfig(scope) {
|
|
324
347
|
const root = scopeRoot(scope);
|
|
325
348
|
if (!root)
|
|
326
349
|
return null;
|
|
@@ -440,11 +463,106 @@ export function subKindsAvailableTo(kind) {
|
|
|
440
463
|
}
|
|
441
464
|
return out.sort((a, b) => a.kind.localeCompare(b.kind));
|
|
442
465
|
}
|
|
466
|
+
/** Every top-level `ScopeConfig` key, diffed one-by-one by `updateConfig` so
|
|
467
|
+
* an untouched key is never rewritten. */
|
|
468
|
+
const CONFIG_TOP_LEVEL_KEYS = [
|
|
469
|
+
'schema_version', 'marketplaces', 'plugins', 'auto_update',
|
|
470
|
+
'max_panes_per_window', 'keybindings', 'modelLadders', 'kinds',
|
|
471
|
+
'remoteCanvas', 'spawnEnv',
|
|
472
|
+
];
|
|
473
|
+
/** The SPARSE `modelLadders` to persist after a config mutation: the raw
|
|
474
|
+
* on-disk sparse ladder (absent rungs stay absent) plus ONLY the specific
|
|
475
|
+
* rungs / `defaultProvider` the mutation actually changed away from the
|
|
476
|
+
* merged baseline. Untouched default rungs are never materialized -- they
|
|
477
|
+
* hold concrete model ids that go stale across builds (the live-Hearth freeze
|
|
478
|
+
* bug). Returns undefined when nothing ladder-related is on disk and nothing
|
|
479
|
+
* was touched, so the key is omitted from config.json entirely. This is what
|
|
480
|
+
* makes a direct `sys config set modelLadders.<provider>.<strength>` persist
|
|
481
|
+
* only the touched rung through the SAME central write path as every other
|
|
482
|
+
* mutation, rather than a special case. */
|
|
483
|
+
function sparseModelLaddersToPersist(rawLadders, baseline, mutated) {
|
|
484
|
+
const out = { ...(rawLadders ?? {}) };
|
|
485
|
+
if (out.anthropic !== undefined)
|
|
486
|
+
out.anthropic = { ...out.anthropic };
|
|
487
|
+
if (out.openai !== undefined)
|
|
488
|
+
out.openai = { ...out.openai };
|
|
489
|
+
if (mutated.defaultProvider !== baseline.defaultProvider && mutated.defaultProvider !== undefined) {
|
|
490
|
+
out.defaultProvider = mutated.defaultProvider;
|
|
491
|
+
}
|
|
492
|
+
for (const provider of ['anthropic', 'openai']) {
|
|
493
|
+
for (const strength of ['ultra', 'strong', 'medium', 'light']) {
|
|
494
|
+
if (mutated[provider][strength] !== baseline[provider][strength]) {
|
|
495
|
+
out[provider] = { ...(out[provider] ?? {}), [strength]: mutated[provider][strength] };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const hasContent = out.defaultProvider !== undefined || out.anthropic !== undefined || out.openai !== undefined;
|
|
500
|
+
return hasContent ? out : undefined;
|
|
501
|
+
}
|
|
502
|
+
/** Apply `mutate` to a scope's MERGED config (so callers can read + write any
|
|
503
|
+
* field), then persist only a RAW PARTIAL: every top-level key the mutation
|
|
504
|
+
* left untouched keeps its exact on-disk raw value (absent stays absent, a
|
|
505
|
+
* sparse override stays sparse), and only keys the mutation actually changed
|
|
506
|
+
* are written. `modelLadders` gets sub-field-granular sparse handling via
|
|
507
|
+
* `sparseModelLaddersToPersist`. Result: a fresh bootstrap or any unrelated
|
|
508
|
+
* `sys config set` no longer freezes a full concrete `modelLadders` to disk,
|
|
509
|
+
* and a direct ladder-rung set still persists only the touched rung -- all
|
|
510
|
+
* through one lock-protected atomic update path. */
|
|
443
511
|
export function updateConfig(scope, mutate) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
512
|
+
let result;
|
|
513
|
+
updateRawConfigAtomically(scope, (latest) => {
|
|
514
|
+
const raw = latest;
|
|
515
|
+
const baseline = mergeConfig(raw);
|
|
516
|
+
const cfg = mergeConfig(raw);
|
|
517
|
+
mutate(cfg);
|
|
518
|
+
const partial = { ...raw };
|
|
519
|
+
for (const key of CONFIG_TOP_LEVEL_KEYS) {
|
|
520
|
+
if (key === 'modelLadders')
|
|
521
|
+
continue;
|
|
522
|
+
if (!isDeepStrictEqual(cfg[key], baseline[key])) {
|
|
523
|
+
partial[key] = cfg[key];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const ladders = sparseModelLaddersToPersist(raw.modelLadders, baseline.modelLadders, cfg.modelLadders);
|
|
527
|
+
if (ladders !== undefined)
|
|
528
|
+
partial.modelLadders = ladders;
|
|
529
|
+
else
|
|
530
|
+
delete partial.modelLadders;
|
|
531
|
+
result = cfg;
|
|
532
|
+
return partial;
|
|
533
|
+
});
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
/** Persist a model as the DEFAULT for a kind — the `/model`-picker "defaults
|
|
537
|
+
* going forward" write. Writes a SPARSE `kinds.<kind>` patch (just the one
|
|
538
|
+
* model field, merged by `normalizeKindPatch`/`mergeKinds` over whatever the
|
|
539
|
+
* lower scopes define — never a frozen full kind entry) into the PROFILE's
|
|
540
|
+
* config.json when the node runs under a profile, else the user scope's.
|
|
541
|
+
* Mode-aware: an orchestrator's pick lands on `orchestratorModel` (base
|
|
542
|
+
* workers of the same kind keep their own default), a base node's on `model`.
|
|
543
|
+
* Callers pass a model string in any form `normalizeModel` accepts; portable
|
|
544
|
+
* ladder tokens (`ultra`, `anthropic/strong`) are preferred by callers since
|
|
545
|
+
* they never go stale across builds. */
|
|
546
|
+
export function persistDefaultKindModel(opts) {
|
|
547
|
+
const field = opts.orchestrator === true ? 'orchestratorModel' : 'model';
|
|
548
|
+
let root;
|
|
549
|
+
let scope;
|
|
550
|
+
if (opts.profileId !== undefined && opts.profileId !== null && opts.profileId !== '') {
|
|
551
|
+
root = profileRoot(opts.profileId);
|
|
552
|
+
scope = 'profile';
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
root = requireScopeRoot('user');
|
|
556
|
+
scope = 'user';
|
|
557
|
+
}
|
|
558
|
+
ensureDir(root);
|
|
559
|
+
const path = configPathFor(root);
|
|
560
|
+
const raw = readJsonIfExists(path) ?? {};
|
|
561
|
+
const kinds = { ...(raw.kinds ?? {}) };
|
|
562
|
+
kinds[opts.kind] = { ...(kinds[opts.kind] ?? {}), [field]: opts.model };
|
|
563
|
+
raw['kinds'] = kinds;
|
|
564
|
+
writeJson(path, raw);
|
|
565
|
+
return { scope, path, field };
|
|
448
566
|
}
|
|
449
567
|
export function updateState(scope, mutate) {
|
|
450
568
|
const s = readState(scope);
|
|
@@ -15,7 +15,7 @@ function classifyProviderText(message) {
|
|
|
15
15
|
return 'overloaded';
|
|
16
16
|
if (/invalid_grant|refresh token|unauthori[sz]ed|invalid.?api.?key|authentication failed|\b401\b|\b403\b/i.test(m))
|
|
17
17
|
return 'auth';
|
|
18
|
-
if (/connection|econnreset|etimedout|enotfound|econnrefused|network|fetch failed|socket hang|timed? out|timeout/i.test(m))
|
|
18
|
+
if (/connection|econnreset|etimedout|enotfound|econnrefused|network|fetch failed|socket hang|timed? out|timeout|\bterminated\b|other side closed|premature close/i.test(m))
|
|
19
19
|
return 'connection';
|
|
20
20
|
return 'other';
|
|
21
21
|
}
|
package/dist/core/fs-utils.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function ensureDir(dir: string): void;
|
|
2
2
|
export declare function writeJson(path: string, data: unknown): void;
|
|
3
|
+
export declare function atomicWriteJson(path: string, data: unknown): void;
|
|
3
4
|
export declare function readJson<T = unknown>(path: string): T;
|
|
4
5
|
export declare function readJsonIfExists<T = unknown>(path: string): T | null;
|
|
5
6
|
export declare function readTextIfExists(path: string): string | null;
|
package/dist/core/fs-utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, cpSync, readlinkSync, } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, cpSync, readlinkSync, renameSync, chmodSync, } from 'node:fs';
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
3
|
import { platform } from 'node:os';
|
|
4
4
|
export function ensureDir(dir) {
|
|
@@ -8,6 +8,20 @@ export function writeJson(path, data) {
|
|
|
8
8
|
ensureDir(dirname(path));
|
|
9
9
|
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
10
10
|
}
|
|
11
|
+
export function atomicWriteJson(path, data) {
|
|
12
|
+
ensureDir(dirname(path));
|
|
13
|
+
const temp = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
14
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o777 : 0o600;
|
|
15
|
+
try {
|
|
16
|
+
writeFileSync(temp, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf8', mode });
|
|
17
|
+
chmodSync(temp, mode);
|
|
18
|
+
renameSync(temp, path);
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
if (existsSync(temp))
|
|
22
|
+
rmSync(temp, { force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
11
25
|
export function readJson(path) {
|
|
12
26
|
return JSON.parse(readFileSync(path, 'utf8'));
|
|
13
27
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { resolveViewPickAction } from '../../../commands/view-pick.js';
|
|
4
|
+
import { resolveBrowseNavAction, resolveBrowseSearchAction } from '../../canvas/browse/app.js';
|
|
5
|
+
import { resolveProfileMenuAction, resolveProfileSearchAction } from '../../profiles/select.js';
|
|
6
|
+
import { resolveKeybindings } from '../index.js';
|
|
7
|
+
test('bespoke consumers resolve defaults and replacements without retaining old gestures', () => {
|
|
8
|
+
const defaults = resolveKeybindings();
|
|
9
|
+
assert.equal(resolveViewPickAction(defaults, { input: 'j' }), 'crtr.view-picker.down');
|
|
10
|
+
assert.equal(resolveBrowseNavAction(defaults, { input: 'G' }), 'crtr.browse.last');
|
|
11
|
+
// Unit 1's terminal-space canonicalization lets a literal space key resolve the
|
|
12
|
+
// catalog `space` gesture — the browse fold toggle now flows through the catalog.
|
|
13
|
+
assert.equal(resolveBrowseNavAction(defaults, { input: ' ' }), 'crtr.browse.toggle-fold');
|
|
14
|
+
assert.equal(resolveBrowseSearchAction(defaults, { return: true }), 'crtr.browse.search.commit');
|
|
15
|
+
assert.equal(resolveProfileMenuAction(defaults, { input: '3' }), 'crtr.profile.menu.slot.3');
|
|
16
|
+
assert.equal(resolveProfileSearchAction(defaults, { input: 'p', ctrl: true }), 'crtr.profile.search.up');
|
|
17
|
+
const remapped = resolveKeybindings({
|
|
18
|
+
'crtr.view-picker.down': ['z'],
|
|
19
|
+
'crtr.browse.last': ['z'],
|
|
20
|
+
'crtr.browse.search.commit': ['z'],
|
|
21
|
+
'crtr.profile.menu.slot.3': ['z'],
|
|
22
|
+
'crtr.profile.search.up': ['z'],
|
|
23
|
+
});
|
|
24
|
+
assert.equal(remapped.diagnostics.length, 0);
|
|
25
|
+
assert.equal(resolveViewPickAction(remapped, { input: 'j' }), null);
|
|
26
|
+
assert.equal(resolveViewPickAction(remapped, { input: 'z' }), 'crtr.view-picker.down');
|
|
27
|
+
assert.equal(resolveBrowseNavAction(remapped, { input: 'G' }), null);
|
|
28
|
+
assert.equal(resolveBrowseNavAction(remapped, { input: 'z' }), 'crtr.browse.last');
|
|
29
|
+
assert.equal(resolveBrowseSearchAction(remapped, { return: true }), null);
|
|
30
|
+
assert.equal(resolveBrowseSearchAction(remapped, { input: 'z' }), 'crtr.browse.search.commit');
|
|
31
|
+
assert.equal(resolveProfileMenuAction(remapped, { input: '3' }), null);
|
|
32
|
+
assert.equal(resolveProfileMenuAction(remapped, { input: 'z' }), 'crtr.profile.menu.slot.3');
|
|
33
|
+
assert.equal(resolveProfileSearchAction(remapped, { input: 'p', ctrl: true }), null);
|
|
34
|
+
assert.equal(resolveProfileSearchAction(remapped, { input: 'z' }), 'crtr.profile.search.up');
|
|
35
|
+
});
|
|
36
|
+
test('profile search leaves printable filter payloads outside catalog actions', () => {
|
|
37
|
+
const bindings = resolveKeybindings();
|
|
38
|
+
assert.equal(resolveProfileSearchAction(bindings, { input: 'w' }), null);
|
|
39
|
+
assert.equal(resolveProfileSearchAction(bindings, { input: '文' }), null);
|
|
40
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|