@north-light/crouter 0.3.184 → 0.3.185

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/build-root.js +10 -3
  2. package/dist/builtin-memory/internal/plugins.md +1 -1
  3. package/dist/clients/attach/input/capabilities.js +3 -0
  4. package/dist/clients/attach/slash/dispatch.js +12 -0
  5. package/dist/clients/attach/viewer.js +191 -191
  6. package/dist/commands/pkg/browse/actions.d.ts +7 -0
  7. package/dist/commands/pkg/browse/actions.js +199 -0
  8. package/dist/commands/pkg/browse/catalog.d.ts +9 -0
  9. package/dist/commands/pkg/browse/catalog.js +466 -0
  10. package/dist/commands/pkg/browse/command-view.d.ts +3 -0
  11. package/dist/commands/pkg/browse/command-view.js +191 -0
  12. package/dist/commands/pkg/browse/detail-panel.d.ts +66 -0
  13. package/dist/commands/pkg/browse/detail-panel.js +402 -0
  14. package/dist/commands/pkg/browse/doc-view.d.ts +3 -0
  15. package/dist/commands/pkg/browse/doc-view.js +81 -0
  16. package/dist/commands/pkg/browse/list-panel.d.ts +28 -0
  17. package/dist/commands/pkg/browse/list-panel.js +168 -0
  18. package/dist/commands/pkg/browse/model.d.ts +230 -0
  19. package/dist/commands/pkg/browse/model.js +36 -0
  20. package/dist/commands/pkg/browse/shell.d.ts +93 -0
  21. package/dist/commands/pkg/browse/shell.js +640 -0
  22. package/dist/commands/pkg/browse.d.ts +2 -0
  23. package/dist/commands/pkg/browse.js +27 -0
  24. package/dist/commands/pkg/market-manage.js +13 -4
  25. package/dist/commands/pkg/plugin-manage.js +8 -3
  26. package/dist/commands/pkg.js +2 -1
  27. package/dist/commands/sys/doctor.js +6 -4
  28. package/dist/commands/sys/panels/broker-limits-panel.d.ts +1 -1
  29. package/dist/commands/sys/panels/broker-limits-panel.js +1 -1
  30. package/dist/commands/sys/panels/dependencies-panel.d.ts +1 -1
  31. package/dist/commands/sys/panels/dependencies-panel.js +1 -1
  32. package/dist/commands/sys/panels/keybindings-panel.js +1 -1
  33. package/dist/commands/sys/panels/models-panel.d.ts +1 -1
  34. package/dist/commands/sys/panels/models-panel.js +1 -1
  35. package/dist/commands/sys/panels/plugins-panel.d.ts +1 -1
  36. package/dist/commands/sys/panels/plugins-panel.js +1 -1
  37. package/dist/commands/sys/panels/prefs-panel.d.ts +1 -1
  38. package/dist/commands/sys/panels/prefs-panel.js +1 -1
  39. package/dist/commands/sys/panels/provider-panel.d.ts +1 -1
  40. package/dist/commands/sys/panels/provider-panel.js +1 -1
  41. package/dist/commands/sys/settings-shell.js +1 -1
  42. package/dist/commands/sys/setup-settings-runner.d.ts +7 -18
  43. package/dist/commands/sys/setup-settings-runner.js +3 -52
  44. package/dist/commands/sys/setup-wizard.js +1 -1
  45. package/dist/core/__tests__/serial/command-plugins.test.js +21 -40
  46. package/dist/core/command-manifests/registry.d.ts +10 -3
  47. package/dist/core/command-manifests/registry.js +36 -22
  48. package/dist/core/command-plugins/compose.js +1 -1
  49. package/dist/core/command-plugins/discovery.js +15 -7
  50. package/dist/core/keybindings/catalog.d.ts +1 -1
  51. package/dist/core/keybindings/catalog.js +1 -0
  52. package/dist/core/resolver.d.ts +2 -2
  53. package/dist/core/runtime/tmux-bindings.js +2 -0
  54. package/dist/core/tui/page-host.d.ts +25 -0
  55. package/dist/core/tui/page-host.js +65 -0
  56. package/dist/pi-extensions/canvas-bash-valve.js +45 -1
  57. package/dist/types.d.ts +4 -1
  58. package/package.json +1 -1
  59. package/runtime.lock.json +2 -2
  60. /package/dist/{commands/sys/panels → core/tui}/panel.d.ts +0 -0
  61. /package/dist/{commands/sys/panels → core/tui}/panel.js +0 -0
@@ -3,7 +3,7 @@ import { resolve, sep } from 'node:path';
3
3
  import { listInstalledPlugins, listInstalledPluginsInRoot } from '../resolver.js';
4
4
  import { projectScopeRoots } from '../scope.js';
5
5
  import { validateCommandManifest } from '../command-manifests/manifest.js';
6
- import { resolveCommandRegistry } from '../command-manifests/registry.js';
6
+ import { qualifiedTopLevelCommandName, resolveCommandRegistry } from '../command-manifests/registry.js';
7
7
  import { adaptPluginContributions } from './compose.js';
8
8
  import { validateHttpPluginTransport } from './endpoint.js';
9
9
  export function effectiveCommandPlugins(startDir = process.cwd(), profileId) {
@@ -139,18 +139,26 @@ export function validateEffectiveCommandPlugins(reservedNames, startDir = proces
139
139
  const validations = effectiveCommandPlugins(startDir, profileId).map((plugin) => validatePluginCommands(plugin, reservedNames, coreCommandPaths));
140
140
  const claims = new Map();
141
141
  for (const validation of validations)
142
- for (const contribution of validation.contributions)
143
- (claims.get(contribution.node.name) ?? (claims.set(contribution.node.name, []), claims.get(contribution.node.name))).push(validation);
142
+ for (const contribution of validation.contributions) {
143
+ const claimants = claims.get(contribution.node.name);
144
+ if (claimants === undefined)
145
+ claims.set(contribution.node.name, [contribution]);
146
+ else
147
+ claimants.push(contribution);
148
+ }
144
149
  for (const [name, claimants] of claims)
145
150
  if (claimants.length > 1)
146
- for (const validation of claimants) {
147
- validation.contributions = validation.contributions.filter((contribution) => contribution.node.name !== name);
148
- validation.issues.push({ code: 'command_collision', plugin: validation.plugin.name, path: name, message: `top-level command "${name}" is claimed by multiple plugins`, received: claimants.map((claimant) => claimant.plugin.name).join(', '), expected: 'exactly one plugin per top-level command name', next: 'Disable or remove all but one claimant.' });
151
+ for (const claimant of claimants) {
152
+ const { plugin } = claimant;
153
+ const qualifiedName = qualifiedTopLevelCommandName({ name: plugin.name, sourceMarketplace: plugin.sourceMarketplace }, name);
154
+ const validation = validations.find((candidate) => candidate.plugin.root === plugin.root);
155
+ validation.contributions = validation.contributions.map((contribution) => contribution === claimant ? { ...contribution, node: { ...contribution.node, name: qualifiedName } } : contribution);
156
+ validation.issues.push({ code: 'command_collision', plugin: plugin.name, path: name, message: `top-level command "${name}" is claimed by ${claimants.map((entry) => entry.plugin.name).join(', ')} — mounted as "${qualifiedName}"`, received: claimants.map((entry) => entry.plugin.name).join(', '), expected: 'an origin-qualified top-level command for each claimant', next: `Invoke "${qualifiedName}" to use this plugin command.` });
149
157
  }
150
158
  return validations;
151
159
  }
152
160
  function contributorIssue(plugin, issue) {
153
- return { ...issue, contributor: { name: plugin.name, scope: plugin.scope } };
161
+ return { ...issue, contributor: { name: plugin.name, scope: plugin.scope, sourceMarketplace: plugin.sourceMarketplace } };
154
162
  }
155
163
  export function discoverCommandContributions(reservedNames, startDir = process.cwd(), profileId) {
156
164
  const validations = validateEffectiveCommandPlugins(reservedNames, startDir, profileId);
@@ -1,6 +1,6 @@
1
1
  import type { BindingDefinition, SafetyRequirement } from './types.js';
2
2
  import type { AttachBindingId, AttachMenuBindingId } from './attach-control.js';
3
- type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'resume' | 'fork' | 'demote' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'providers' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
3
+ type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'resume' | 'fork' | 'demote' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
4
4
  type TmuxMenuEntryAction = Exclude<TmuxMenuAction, 'open' | 'attach'> | `issue.${'idea' | 'bug'}`;
5
5
  type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'revive' | 'focus-manager' | 'close-subtree' | 'dismiss';
6
6
  type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'search' | 'revive-now' | 'focus' | 'focus-background';
@@ -53,6 +53,7 @@ export const BINDING_CATALOG = Object.freeze([
53
53
  ...menuEntry('metadata', 'Show metadata', 'Show the focused node’s metadata dossier.', ['m'], ['tmux.menu.show']),
54
54
  ...menuEntry('context', 'Browse context', 'Browse the focused node context.', ['c'], ['tmux.menu.show']),
55
55
  ...menuEntry('providers', 'Open settings', 'Open the crouter settings panel.', [','], ['tmux.menu']),
56
+ ...menuEntry('plugins', 'Browse plugins', 'Open the crouter plugin browser.', ['k'], ['tmux.menu']),
56
57
  ...menuEntry('graph', 'Canvas graph', 'Open the canvas graph.', ['g'], ['tmux.menu']),
57
58
  ...menuEntry('focus-manager', 'Go to manager', 'Focus this node’s manager.', ['h'], ['tmux.menu']),
58
59
  ...menuEntry('copy-id', 'Copy node id', 'Copy the focused node id to the system clipboard.', ['y'], ['tmux.menu']),
@@ -8,7 +8,7 @@ export interface ParsedSkillQualifier {
8
8
  segments: string[];
9
9
  }
10
10
  export declare function parseSkillQualifier(raw: string): ParsedSkillQualifier;
11
- export declare function listInstalledMarketplaces(scope: Scope): InstalledMarketplace[];
11
+ export declare function listInstalledMarketplaces(scope: Exclude<Scope, 'builtin'>): InstalledMarketplace[];
12
12
  export declare function listAllMarketplaces(): InstalledMarketplace[];
13
- export declare function findMarketplaceByName(name: string, scope?: Scope): InstalledMarketplace | null;
13
+ export declare function findMarketplaceByName(name: string, scope?: Exclude<Scope, 'builtin'>): InstalledMarketplace | null;
14
14
  export declare function scopeRootsLabel(): string;
@@ -33,6 +33,7 @@ const LEAF_MENU_ACTIONS = {
33
33
  // The settings page lays out a grouped rail beside a content panel; the
34
34
  // default dialog-sized popup clipped both the rail and the models matrix.
35
35
  'crtr.tmux.menu.providers': { description: 'settings', action: { kind: 'popup', run: 'sys settings', size: { w: '90%', h: '85%' } } },
36
+ 'crtr.tmux.menu.plugins': { description: 'plugins', action: { kind: 'popup', run: 'pkg browse', size: { w: '90%', h: '85%' } } },
36
37
  'crtr.tmux.menu.graph': { description: 'canvas graph', action: { kind: 'keys', keys: '/graph' } },
37
38
  'crtr.tmux.menu.focus-manager': { description: 'go to manager', action: { kind: 'command', run: 'surface node focus {manager}' } },
38
39
  'crtr.tmux.menu.copy-id': { description: 'copy node id', action: { kind: 'command', run: 'surface node id --pane {pane}' } },
@@ -301,6 +302,7 @@ function menuItems(bindings, entries) {
301
302
  ]));
302
303
  addGroup('System', boundItems([
303
304
  'crtr.tmux.menu.providers',
305
+ 'crtr.tmux.menu.plugins',
304
306
  'crtr.tmux.menu.issues',
305
307
  ]));
306
308
  const prefixGestures = bindings.gestures('crtr.tmux.menu.open');
@@ -0,0 +1,25 @@
1
+ import { ProcessTerminal, TUI, type Component, type Focusable } from "@earendil-works/pi-tui";
2
+ import { readRawScopeConfig } from "../config.js";
3
+ import { type BindingId, type BindingResolution } from "../keybindings/index.js";
4
+ /** A full-screen page: a focusable TUI component that cleans up after itself. */
5
+ export type TuiPageSurface = Component & Focusable & {
6
+ dispose(): void;
7
+ };
8
+ export interface TuiPageContext {
9
+ tui: TUI;
10
+ terminal: ProcessTerminal;
11
+ bindings: BindingResolution<BindingId>;
12
+ rawUserConfig: ReturnType<typeof readRawScopeConfig>;
13
+ }
14
+ export interface TuiPageControl<Result> {
15
+ /** Close the page with a result. */
16
+ submit(result: Result): void;
17
+ /** Close the page with no result. */
18
+ cancel(): void;
19
+ }
20
+ /** Mount one page as the TUI's only child and resolve when it closes.
21
+ *
22
+ * `interceptCancel` is a process-level escape hatch for surfaces that must be
23
+ * abandonable while a nested component holds the keyboard; it runs before the
24
+ * page sees the input and closes the page when it returns true. */
25
+ export declare function runTuiPage<Result>(createSurface: (context: TuiPageContext, control: TuiPageControl<Result>) => TuiPageSurface, interceptCancel?: (data: string, bindings: BindingResolution<BindingId>) => boolean): Promise<Result | undefined>;
@@ -0,0 +1,65 @@
1
+ // The process-level terminal host for crouter's full-screen TUI pages —
2
+ // `crtr sys settings`, the setup wizard, and `crtr pkg browse`.
3
+ //
4
+ // It owns exactly the concerns every page shares: theme, resolved keybindings,
5
+ // the raw user config those pages edit, the ProcessTerminal/TUI pair, and a
6
+ // restore path that survives an exception, a signal, and process exit. Anything
7
+ // a single page needs beyond that — pi's model runtime, a canvas client — the
8
+ // page's own runner builds and closes over before calling in here.
9
+ import { ProcessTerminal, TUI, } from "@earendil-works/pi-tui";
10
+ import { applyTheme } from "../../clients/attach/config.js";
11
+ import { readRawScopeConfig } from "../config.js";
12
+ import { resolveKeybindings, } from "../keybindings/index.js";
13
+ /** Mount one page as the TUI's only child and resolve when it closes.
14
+ *
15
+ * `interceptCancel` is a process-level escape hatch for surfaces that must be
16
+ * abandonable while a nested component holds the keyboard; it runs before the
17
+ * page sees the input and closes the page when it returns true. */
18
+ export async function runTuiPage(createSurface, interceptCancel) {
19
+ applyTheme({ cwd: process.cwd() });
20
+ const rawUserConfig = readRawScopeConfig("user");
21
+ const bindings = resolveKeybindings(rawUserConfig?.keybindings);
22
+ const terminal = new ProcessTerminal();
23
+ const tui = new TUI(terminal, true);
24
+ return new Promise((resolve, reject) => {
25
+ let surface;
26
+ let restored = false;
27
+ const restore = () => {
28
+ if (restored)
29
+ return;
30
+ restored = true;
31
+ surface?.dispose();
32
+ try {
33
+ tui.stop();
34
+ }
35
+ catch {
36
+ /* terminal restore */
37
+ }
38
+ };
39
+ const finish = (result) => {
40
+ restore();
41
+ resolve(result);
42
+ };
43
+ try {
44
+ surface = createSurface({ tui, terminal, bindings, rawUserConfig }, { submit: finish, cancel: () => finish(undefined) });
45
+ tui.addChild(surface);
46
+ tui.setFocus(surface);
47
+ process.once("exit", restore);
48
+ for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"])
49
+ process.once(signal, () => finish(undefined));
50
+ if (interceptCancel !== undefined) {
51
+ tui.addInputListener((data) => {
52
+ if (!interceptCancel(data, bindings))
53
+ return undefined;
54
+ finish(undefined);
55
+ return { consume: true };
56
+ });
57
+ }
58
+ tui.start();
59
+ }
60
+ catch (error) {
61
+ restore();
62
+ reject(error);
63
+ }
64
+ });
65
+ }
@@ -28,6 +28,7 @@ import { spawn } from 'node:child_process';
28
28
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync, } from 'node:fs';
29
29
  import { homedir } from 'node:os';
30
30
  import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId } from '../core/bash-jobs.js';
31
+ import { Type } from 'typebox';
31
32
  import { createBashToolDefinition } from '@earendil-works/pi-coding-agent';
32
33
  // ---------------------------------------------------------------------------
33
34
  // Deadline — 5 minutes by default, overridable via CRTR_BASH_VALVE_MS
@@ -342,6 +343,49 @@ export function createValveOperations(nodeId, contextDir) {
342
343
  // ---------------------------------------------------------------------------
343
344
  // Extension
344
345
  // ---------------------------------------------------------------------------
346
+ // ---------------------------------------------------------------------------
347
+ // `purpose` — the one line a person reads while a command runs
348
+ //
349
+ // A deliberate deviation from taking pi's definition verbatim, and the only one
350
+ // here. Everywhere crtr's output reaches a non-technical audience, the host has
351
+ // to render SOMETHING per step, and without this field its only material is the
352
+ // command — which yields "Running command" for an entire turn. The model is the
353
+ // only party that knows why it is running this, so it says so, in one clause,
354
+ // before the command streams.
355
+ //
356
+ // Declared FIRST so it arrives ahead of `command` in the argument stream: a host
357
+ // parsing partial JSON can show the label while the command is still being
358
+ // written, rather than after the call is complete and about to finish.
359
+ //
360
+ // The execute path ignores it entirely — this is a labelling channel, not an
361
+ // input. Optional, so a model that omits it behaves exactly as before.
362
+ //
363
+ // Upstream ask: pi's own bash tool should carry this (Claude Code's bash tool
364
+ // has shipped an equivalent `description` field for years). Until it does, the
365
+ // field is re-declared here on top of pi's schema rather than forked into it.
366
+ // ---------------------------------------------------------------------------
367
+ const PURPOSE_DESCRIPTION = [
368
+ 'One short clause naming what this step is FOR, in the words of the person you',
369
+ 'are working for — "Reading the Acme thread", not "Fetching emails" and never',
370
+ '"Running GMAIL_FETCH_EMAILS". A person reads this while the command runs; it',
371
+ 'is the only thing they see of this step. Under about eight words. Omit it for',
372
+ 'steps that only touch your own scratch work.',
373
+ ].join(' ');
374
+ /**
375
+ * Prepend an optional `purpose` to pi's bash schema, carrying pi's own property
376
+ * definitions through verbatim — so a future upstream change to the `command`
377
+ * or `timeout` wording still reaches the model, and this file owns exactly one
378
+ * field.
379
+ */
380
+ function withPurpose(definition) {
381
+ return {
382
+ ...definition,
383
+ parameters: Type.Object({
384
+ purpose: Type.Optional(Type.String({ description: PURPOSE_DESCRIPTION })),
385
+ ...definition.parameters.properties,
386
+ }),
387
+ };
388
+ }
345
389
  export default function (pi) {
346
390
  const nodeId = process.env['CRTR_NODE_ID'];
347
391
  if (nodeId === undefined || nodeId.trim() === '')
@@ -356,6 +400,6 @@ export default function (pi) {
356
400
  // every session_start is idempotent (last registration wins).
357
401
  pi.on('session_start', (_event, ctx) => {
358
402
  const operations = createValveOperations(nodeId, contextDir);
359
- pi.registerTool(createBashToolDefinition(ctx.cwd, { operations }));
403
+ pi.registerTool(withPurpose(createBashToolDefinition(ctx.cwd, { operations })));
360
404
  });
361
405
  }
package/dist/types.d.ts CHANGED
@@ -301,7 +301,10 @@ export interface InstalledPlugin {
301
301
  }
302
302
  export interface InstalledMarketplace {
303
303
  name: string;
304
- scope: Scope;
304
+ /** Never `builtin`: marketplaces are registered by the user into the user or
305
+ * project scope, and `listInstalledMarketplaces` has no builtin directory to
306
+ * read. `builtin` describes plugins that ship with crtr, not their source. */
307
+ scope: Exclude<Scope, 'builtin'>;
305
308
  root: string;
306
309
  manifest: MarketplaceManifest;
307
310
  url: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.184",
3
+ "version": "0.3.185",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.184",
3
+ "version": "0.3.185",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.184",
9
+ "version": "0.3.185",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
File without changes