@north-light/crouter 0.3.159 → 0.3.160
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/dist/api/dto/nodes.d.ts +3 -0
- package/dist/builtin-memory/internal/INDEX.md +1 -0
- package/dist/builtin-memory/internal/workflow-codification.md +82 -0
- package/dist/clients/attach/__tests__/bash-call-width.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/bash-call-width.test.js +38 -0
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +20 -0
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +3 -1
- package/dist/clients/attach/chrome/bash-jobs.d.ts +5 -3
- package/dist/clients/attach/chrome/bash-jobs.js +15 -8
- package/dist/clients/attach/chrome/canvas-panels.js +10 -3
- package/dist/clients/attach/chrome/roster.d.ts +7 -2
- package/dist/clients/attach/chrome/roster.js +87 -3
- package/dist/clients/attach/input/controller.d.ts +3 -0
- package/dist/clients/attach/input/controller.js +1 -0
- package/dist/clients/attach/render/assistant-message.d.ts +13 -0
- package/dist/clients/attach/render/assistant-message.js +55 -0
- package/dist/clients/attach/render/chat-view.d.ts +29 -0
- package/dist/clients/attach/render/chat-view.js +233 -12
- package/dist/clients/attach/render/context-message.js +1 -1
- package/dist/clients/attach/render/edit-diff.js +20 -1
- package/dist/clients/attach/render/tool-calls.d.ts +32 -0
- package/dist/clients/attach/render/tool-calls.js +251 -0
- package/dist/clients/attach/session/input-wiring.d.ts +3 -0
- package/dist/clients/attach/session/input-wiring.js +3 -0
- package/dist/clients/attach/slash/dispatch.d.ts +4 -0
- package/dist/clients/attach/slash/dispatch.js +8 -0
- package/dist/clients/attach/viewer.js +512 -508
- package/dist/commands/cron.js +2 -2
- package/dist/commands/node.js +3 -3
- package/dist/commands/search.js +1 -1
- package/dist/commands/sys/config.js +10 -2
- package/dist/commands/sys/doctor.js +22 -1
- package/dist/commands/sys/setup-core.d.ts +5 -1
- package/dist/commands/sys/setup-core.js +16 -2
- package/dist/commands/sys/setup-wizard.d.ts +11 -0
- package/dist/commands/sys/setup-wizard.js +111 -6
- package/dist/core/__tests__/tmux-surface.test.js +5 -2
- package/dist/core/canvas/canvas.js +1 -1
- package/dist/core/canvas/labels.d.ts +1 -0
- package/dist/core/canvas/labels.js +3 -1
- package/dist/core/canvas/nav-render.d.ts +6 -0
- package/dist/core/canvas/nav-render.js +8 -0
- package/dist/core/canvas/types.d.ts +6 -0
- package/dist/core/config.js +3 -2
- package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -0
- package/dist/core/preview-registry.d.ts +5 -0
- package/dist/core/preview-registry.js +10 -0
- package/dist/core/runtime/canvas-extensions.d.ts +4 -0
- package/dist/core/runtime/canvas-extensions.js +4 -0
- package/dist/core/runtime/naming-persist.d.ts +4 -3
- package/dist/core/runtime/naming-persist.js +10 -9
- package/dist/core/runtime/naming.d.ts +28 -8
- package/dist/core/runtime/naming.js +168 -32
- package/dist/core/runtime/nerd-font.d.ts +28 -0
- package/dist/core/runtime/nerd-font.js +127 -0
- package/dist/core/runtime/tmux.js +16 -0
- package/dist/daemon/api/map.js +2 -0
- package/dist/pi-extensions/naming-tool.d.ts +24 -0
- package/dist/pi-extensions/naming-tool.js +67 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/dist/web-client/assets/index-B76ZKfT_.js +79 -0
- package/dist/web-client/assets/{index-CpEl9LTS.css → index-CqLKj8Xu.css} +1 -1
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/web-client/assets/index-CsuwzlcQ.js +0 -79
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readdirSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
/** crtr's viewer renders tool calls, command icons, node badges and the
|
|
6
|
+
* inspector with Nerd Font private-use-area codepoints (see
|
|
7
|
+
* `core/preview-registry.ts` and `clients/attach/render/tool-calls.ts`), so a
|
|
8
|
+
* terminal without a Nerd Font shows tofu boxes for every icon. Nothing about
|
|
9
|
+
* the font is auto-detectable from inside the process — the terminal emulator
|
|
10
|
+
* picks the face — so the best we can do is detect an INSTALLED Nerd Font on
|
|
11
|
+
* the system and tell the user to point their terminal profile at it. */
|
|
12
|
+
/** Font-family and filename fragments that only appear in Nerd Font builds.
|
|
13
|
+
* `symbols` alone would false-positive on system symbol faces, so the
|
|
14
|
+
* symbols-only patch is matched with its full distributed name. */
|
|
15
|
+
const NERD_FONT_PATTERN = /nerd[ _-]?font|symbolsnerdfont|symbols-only/i;
|
|
16
|
+
/** Font directories searched when fontconfig is unavailable (always the case
|
|
17
|
+
* on macOS). User-level dirs first: an install crtr suggested lands there. */
|
|
18
|
+
function fontDirs(platform, home) {
|
|
19
|
+
if (platform === 'darwin') {
|
|
20
|
+
return [join(home, 'Library', 'Fonts'), '/Library/Fonts', '/System/Library/Fonts'];
|
|
21
|
+
}
|
|
22
|
+
return [
|
|
23
|
+
join(home, '.local', 'share', 'fonts'),
|
|
24
|
+
join(home, '.fonts'),
|
|
25
|
+
'/usr/local/share/fonts',
|
|
26
|
+
'/usr/share/fonts',
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
/** Shallow recursive filename scan. Font trees nest a couple of levels
|
|
30
|
+
* (`/usr/share/fonts/truetype/<family>/`), so depth 3 reaches every real
|
|
31
|
+
* install without walking an unbounded tree. */
|
|
32
|
+
function scanForNerdFont(dir, depth) {
|
|
33
|
+
if (depth < 0)
|
|
34
|
+
return null;
|
|
35
|
+
let entries;
|
|
36
|
+
try {
|
|
37
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null; // absent or unreadable directory is simply not a match
|
|
41
|
+
}
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
const nested = scanForNerdFont(join(dir, entry.name), depth - 1);
|
|
45
|
+
if (nested !== null)
|
|
46
|
+
return nested;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!/\.(ttf|otf|ttc)$/i.test(entry.name))
|
|
50
|
+
continue;
|
|
51
|
+
if (NERD_FONT_PATTERN.test(entry.name))
|
|
52
|
+
return entry.name;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/** Ask fontconfig for installed families. Present on Linux, absent on macOS. */
|
|
57
|
+
function queryFontconfig() {
|
|
58
|
+
const probe = spawnSync('sh', ['-lc', 'command -v fc-list >/dev/null 2>&1'], { stdio: 'ignore' });
|
|
59
|
+
if (probe.status !== 0)
|
|
60
|
+
return null;
|
|
61
|
+
const res = spawnSync('fc-list', [':', 'family'], { encoding: 'utf8', timeout: 5000 });
|
|
62
|
+
if (res.status !== 0 || typeof res.stdout !== 'string')
|
|
63
|
+
return null;
|
|
64
|
+
for (const line of res.stdout.split('\n')) {
|
|
65
|
+
if (NERD_FONT_PATTERN.test(line))
|
|
66
|
+
return line.split(',')[0].trim();
|
|
67
|
+
}
|
|
68
|
+
return ''; // fontconfig answered and knows of no Nerd Font — authoritative miss
|
|
69
|
+
}
|
|
70
|
+
export function detectNerdFont(platform = process.platform, home = homedir()) {
|
|
71
|
+
if (platform !== 'darwin') {
|
|
72
|
+
const family = queryFontconfig();
|
|
73
|
+
if (family !== null && family !== '')
|
|
74
|
+
return { installed: true, evidence: family, source: 'fc-list' };
|
|
75
|
+
}
|
|
76
|
+
for (const dir of fontDirs(platform, home)) {
|
|
77
|
+
const file = scanForNerdFont(dir, 3);
|
|
78
|
+
if (file !== null)
|
|
79
|
+
return { installed: true, evidence: file, source: 'font-dir' };
|
|
80
|
+
}
|
|
81
|
+
return { installed: false, evidence: null, source: null };
|
|
82
|
+
}
|
|
83
|
+
/** Download the upstream symbols-only patch into the user's font dir. Works on
|
|
84
|
+
* every Linux regardless of package manager — Debian/Ubuntu and Fedora ship no
|
|
85
|
+
* Nerd Font package, so there is nothing native to install there. Needs no
|
|
86
|
+
* sudo: it writes to `~/.local/share/fonts`. */
|
|
87
|
+
export const NERD_FONT_MANUAL_LINUX_COMMAND = 'mkdir -p ~/.local/share/fonts && '
|
|
88
|
+
+ 'curl -fsSL -o /tmp/NerdFontsSymbolsOnly.zip '
|
|
89
|
+
+ 'https://github.com/ryanoasis/nerd-fonts/releases/latest/download/NerdFontsSymbolsOnly.zip && '
|
|
90
|
+
+ 'unzip -oq /tmp/NerdFontsSymbolsOnly.zip -d ~/.local/share/fonts && fc-cache -f';
|
|
91
|
+
/** The install command for this platform/package manager. `packageManager`
|
|
92
|
+
* matches setup's detection: only brew and pacman have a real Nerd Font
|
|
93
|
+
* package, everything else on Linux gets the upstream download. A non-brew
|
|
94
|
+
* package manager only ever exists on Linux, so it settles the platform on its
|
|
95
|
+
* own and `platform` decides only the `manual` case. */
|
|
96
|
+
export function nerdFontInstallCommand(packageManager, platform = process.platform) {
|
|
97
|
+
const onLinux = platform !== 'darwin' || packageManager === 'apt-get' || packageManager === 'dnf' || packageManager === 'pacman';
|
|
98
|
+
if (packageManager === 'brew') {
|
|
99
|
+
return {
|
|
100
|
+
command: 'brew install --cask font-symbols-only-nerd-font',
|
|
101
|
+
hint: 'Homebrew cask',
|
|
102
|
+
autoInstall: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (packageManager === 'pacman') {
|
|
106
|
+
return {
|
|
107
|
+
command: 'sudo pacman -S --noconfirm ttf-nerd-fonts-symbols',
|
|
108
|
+
hint: 'pacman install',
|
|
109
|
+
autoInstall: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (onLinux) {
|
|
113
|
+
return {
|
|
114
|
+
command: NERD_FONT_MANUAL_LINUX_COMMAND,
|
|
115
|
+
hint: 'download symbols-only patch to ~/.local/share/fonts (needs curl + unzip)',
|
|
116
|
+
autoInstall: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
command: null,
|
|
121
|
+
hint: 'install Homebrew, or download a Nerd Font from https://nerdfonts.com and add it in Font Book',
|
|
122
|
+
autoInstall: false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Every install path ends the same way: the font is on disk but the terminal
|
|
126
|
+
* emulator still has to be pointed at it, and crtr cannot do that. */
|
|
127
|
+
export const NERD_FONT_SELECT_HINT = 'then set your terminal profile\'s font to a Nerd Font (e.g. add "Symbols Nerd Font Mono" as the non-ASCII/fallback font) — installing it does not select it';
|
|
@@ -483,6 +483,8 @@ const MENU_ACTIONS = {
|
|
|
483
483
|
// M-S-r carries shift+alt and lands as the same chord the keyboard produces.
|
|
484
484
|
'crtr.tmux.menu.file-review': { description: 'review a file', action: { kind: 'raw-keys', keys: 'M-S-r' } },
|
|
485
485
|
'crtr.tmux.menu.metadata': { description: 'metadata', action: { kind: 'keys', keys: '/node-metadata' } },
|
|
486
|
+
'crtr.tmux.menu.fold-tools': { description: 'fold finished tool calls', action: { kind: 'keys', keys: '/fold-tools' } },
|
|
487
|
+
'crtr.tmux.menu.view-settings': undefined,
|
|
486
488
|
'crtr.tmux.menu.context': { description: 'context + reports', action: { kind: 'keys', keys: '/context' } },
|
|
487
489
|
'crtr.tmux.menu.providers': { description: 'settings', action: { kind: 'popup', run: 'sys setup' } },
|
|
488
490
|
'crtr.tmux.menu.graph': { description: 'graph view', action: { kind: 'keys', keys: '/graph' } },
|
|
@@ -627,6 +629,11 @@ function menuItems(bindings) {
|
|
|
627
629
|
'crtr.tmux.menu.metadata',
|
|
628
630
|
'crtr.tmux.menu.context',
|
|
629
631
|
]);
|
|
632
|
+
// How this viewer PAINTS the transcript, as opposed to what it shows. Each item
|
|
633
|
+
// is a toggle the viewer applies to its own render state (a `/`-command sent to
|
|
634
|
+
// the pane); a tmux menu is built once and cannot show live state, so the item
|
|
635
|
+
// reads as the action and the viewer answers with a notice.
|
|
636
|
+
const viewSettingsItems = boundItems(['crtr.tmux.menu.fold-tools']);
|
|
630
637
|
const exploreItems = boundItems([
|
|
631
638
|
'crtr.tmux.menu.resume',
|
|
632
639
|
'crtr.tmux.menu.graph',
|
|
@@ -641,6 +648,15 @@ function menuItems(bindings) {
|
|
|
641
648
|
});
|
|
642
649
|
}
|
|
643
650
|
}
|
|
651
|
+
if (viewSettingsItems.length > 0) {
|
|
652
|
+
for (const gesture of bindings.gestures('crtr.tmux.menu.view-settings')) {
|
|
653
|
+
exploreItems.push({
|
|
654
|
+
description: 'view settings ›',
|
|
655
|
+
selector: menuSelector(gesture),
|
|
656
|
+
action: { kind: 'submenu', title: ' view settings ', items: viewSettingsItems },
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
644
660
|
// Explore changes what is in view or opens a canvas-level browser. Current
|
|
645
661
|
// node acts on the agent in this pane without changing its lifecycle.
|
|
646
662
|
addGroup('Explore', exploreItems);
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -105,6 +105,8 @@ export function toNodeDetailDTO(meta, edges) {
|
|
|
105
105
|
};
|
|
106
106
|
if (meta.description !== undefined)
|
|
107
107
|
dto.description = meta.description;
|
|
108
|
+
if (meta.icon !== undefined)
|
|
109
|
+
dto.icon = meta.icon;
|
|
108
110
|
if (meta.cycles !== undefined)
|
|
109
111
|
dto.cycles = meta.cycles;
|
|
110
112
|
dto.context_tokens = readContextTokens(meta.node_id);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface PiLike {
|
|
2
|
+
registerTool: (definition: Record<string, unknown>) => void;
|
|
3
|
+
}
|
|
4
|
+
/** The naming schema — the ONE definition of what a generated session name is.
|
|
5
|
+
* Both fields are required: a name without an icon renders a bare label, and
|
|
6
|
+
* an icon without a name is useless, so the model is made to decide both in a
|
|
7
|
+
* single constrained emission. */
|
|
8
|
+
export declare const NAME_SESSION_SCHEMA: {
|
|
9
|
+
readonly type: "object";
|
|
10
|
+
readonly properties: {
|
|
11
|
+
readonly name: {
|
|
12
|
+
readonly type: "string";
|
|
13
|
+
readonly description: "A 3-8 word kebab-case session name: lowercase words joined by single hyphens, e.g. refactor-auth-token-flow. No punctuation, quotes, or prose.";
|
|
14
|
+
};
|
|
15
|
+
readonly icon: {
|
|
16
|
+
readonly type: "string";
|
|
17
|
+
readonly description: "The Nerd Font glyph depicting this session, as a codepoint in U+XXXX form (e.g. U+F188).";
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
readonly required: readonly ["name", "icon"];
|
|
21
|
+
readonly additionalProperties: false;
|
|
22
|
+
};
|
|
23
|
+
export declare function registerNamingTool(pi: PiLike): void;
|
|
24
|
+
export default registerNamingTool;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// naming-tool.ts — the structured-output seam for session naming.
|
|
2
|
+
//
|
|
3
|
+
// Loaded ONLY by the headless namer subprocess (`runtime/naming.ts` passes it
|
|
4
|
+
// with `-e` alongside `--no-extensions`), never by a canvas node. It registers a
|
|
5
|
+
// single `name_session` tool whose parameters ARE the naming schema, with
|
|
6
|
+
// pi's constrained sampling turned on, so the light model emits a real typed
|
|
7
|
+
// object instead of prose we have to parse out of a text reply.
|
|
8
|
+
//
|
|
9
|
+
// The result travels back to the parent over a file: the parent creates a temp
|
|
10
|
+
// path, passes it as CRTR_NAME_RESULT_FILE, and reads it after the subprocess
|
|
11
|
+
// exits. stdout is unusable for this — pi print-mode writes its own chatter
|
|
12
|
+
// there.
|
|
13
|
+
//
|
|
14
|
+
// Inert (registers nothing) when CRTR_NAME_RESULT_FILE is absent, so an
|
|
15
|
+
// accidental load in any other context is a no-op.
|
|
16
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { dirname } from 'node:path';
|
|
18
|
+
/** The naming schema — the ONE definition of what a generated session name is.
|
|
19
|
+
* Both fields are required: a name without an icon renders a bare label, and
|
|
20
|
+
* an icon without a name is useless, so the model is made to decide both in a
|
|
21
|
+
* single constrained emission. */
|
|
22
|
+
export const NAME_SESSION_SCHEMA = {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
name: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'A 3-8 word kebab-case session name: lowercase words joined by single hyphens, e.g. refactor-auth-token-flow. No punctuation, quotes, or prose.',
|
|
28
|
+
},
|
|
29
|
+
icon: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'The Nerd Font glyph depicting this session, as a codepoint in U+XXXX form (e.g. U+F188).',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['name', 'icon'],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
};
|
|
37
|
+
export function registerNamingTool(pi) {
|
|
38
|
+
const out = process.env['CRTR_NAME_RESULT_FILE'];
|
|
39
|
+
if (out === undefined || out.trim() === '')
|
|
40
|
+
return;
|
|
41
|
+
pi.registerTool({
|
|
42
|
+
name: 'name_session',
|
|
43
|
+
label: 'Name session',
|
|
44
|
+
description: 'Record the name and icon for this coding session. Call exactly once, then stop.',
|
|
45
|
+
parameters: NAME_SESSION_SCHEMA,
|
|
46
|
+
// Ask the provider to constrain sampling to the schema. `prefer` degrades
|
|
47
|
+
// silently on a model without strict tool support (`require` would fail the
|
|
48
|
+
// whole request); the parent validates and falls back to a local slug
|
|
49
|
+
// either way, so this only makes a malformed emission rarer.
|
|
50
|
+
constrainedSampling: { type: 'json_schema', strict: 'prefer' },
|
|
51
|
+
execute(_id, params) {
|
|
52
|
+
try {
|
|
53
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
54
|
+
writeFileSync(out, JSON.stringify(params ?? {}), 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// The parent falls back to a local slug when the file never lands.
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: 'text', text: 'Recorded.' }],
|
|
61
|
+
details: { recorded: true },
|
|
62
|
+
terminate: true,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export default registerNamingTool;
|
package/dist/types.d.ts
CHANGED
|
@@ -152,6 +152,11 @@ export interface ScopeConfig {
|
|
|
152
152
|
* `both` keeps both. Tool calls and results are never kept. Full detail for
|
|
153
153
|
* every cycle stays in the session file. */
|
|
154
154
|
condensed_history: CondensedHistoryMode;
|
|
155
|
+
/** Fold every SETTLED tool call in the attach viewer down to its single call
|
|
156
|
+
* line (`/fold-tools`, Alt+C → w → f). A tool still running is never folded —
|
|
157
|
+
* it folds itself once it settles. Toggling it in a viewer writes back here,
|
|
158
|
+
* so the choice carries into later chats and later viewers. Default false. */
|
|
159
|
+
fold_finished_tools: boolean;
|
|
155
160
|
keybindings: KeybindingOverrides;
|
|
156
161
|
modelLadders: ModelLaddersConfig;
|
|
157
162
|
/** The kind registry (spec §1.5): kind existence + launch knobs, keyed by
|
package/dist/types.js
CHANGED
|
@@ -38,6 +38,7 @@ export function defaultScopeConfig() {
|
|
|
38
38
|
completion_bell: true,
|
|
39
39
|
live_cycles: DEFAULT_LIVE_CYCLES,
|
|
40
40
|
condensed_history: DEFAULT_CONDENSED_HISTORY,
|
|
41
|
+
fold_finished_tools: false,
|
|
41
42
|
keybindings: {},
|
|
42
43
|
modelLadders: defaultModelLaddersConfig(),
|
|
43
44
|
kinds: defaultKindsConfig(),
|