@north-light/crouter 0.3.180 → 0.3.182

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 (137) hide show
  1. package/dist/api/client.d.ts +10 -1
  2. package/dist/api/client.js +13 -0
  3. package/dist/api/dto/broker.d.ts +32 -0
  4. package/dist/api/dto/crons.d.ts +17 -0
  5. package/dist/api/dto/memory.d.ts +17 -0
  6. package/dist/api/dto/memory.js +6 -0
  7. package/dist/api/dto/messages.d.ts +5 -0
  8. package/dist/api/dto/reviews.d.ts +8 -4
  9. package/dist/api/index.d.ts +1 -0
  10. package/dist/api/index.js +1 -0
  11. package/dist/api/routes.d.ts +2 -0
  12. package/dist/api/routes.js +4 -0
  13. package/dist/build-root.d.ts +7 -0
  14. package/dist/build-root.js +21 -0
  15. package/dist/builtin-memory/insights/init.md +48 -3
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/__tests__/insights-active-init.test.ts +98 -0
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +7 -50
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +16 -1
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +34 -0
  20. package/dist/cli.js +1 -2
  21. package/dist/clients/attach/__tests__/context-message.test.js +5 -2
  22. package/dist/clients/attach/assets/README.md +7 -0
  23. package/dist/clients/attach/assets/whip-06.mp3 +0 -0
  24. package/dist/clients/attach/assets/whip-crack.mp3 +0 -0
  25. package/dist/clients/attach/assets/whip-snap.mp3 +0 -0
  26. package/dist/clients/attach/chrome/canvas-panels.d.ts +7 -1
  27. package/dist/clients/attach/chrome/canvas-panels.js +20 -3
  28. package/dist/clients/attach/chrome/review-wait.d.ts +6 -0
  29. package/dist/clients/attach/chrome/review-wait.js +22 -0
  30. package/dist/clients/attach/chrome/roster.js +23 -2
  31. package/dist/clients/attach/chrome/widgets.js +1 -1
  32. package/dist/clients/attach/input/controller.js +4 -3
  33. package/dist/clients/attach/overlays/mcp.js +3 -1
  34. package/dist/clients/attach/render/chat-view.js +1 -1
  35. package/dist/clients/attach/session/whip.d.ts +1 -0
  36. package/dist/clients/attach/session/whip.js +26 -0
  37. package/dist/clients/attach/slash/dispatch.js +2 -0
  38. package/dist/clients/attach/viewer.js +578 -573
  39. package/dist/clients/inbox/review/document-surface.d.ts +1 -1
  40. package/dist/clients/inbox/review/document-surface.js +4 -4
  41. package/dist/clients/inbox/review/launch.js +16 -4
  42. package/dist/clients/inbox/review/review-client.d.ts +9 -4
  43. package/dist/clients/inbox/review/review-client.js +3 -0
  44. package/dist/commands/cron.js +30 -8
  45. package/dist/commands/human/prompts.d.ts +7 -2
  46. package/dist/commands/human/prompts.js +15 -10
  47. package/dist/commands/human.js +1 -2
  48. package/dist/commands/memory/find.js +11 -8
  49. package/dist/commands/memory/read.js +111 -11
  50. package/dist/commands/memory/write.js +1 -1
  51. package/dist/commands/memory.js +1 -1
  52. package/dist/commands/pkg/market-manage.d.ts +13 -0
  53. package/dist/commands/pkg/market-manage.js +39 -33
  54. package/dist/commands/pkg/plugin-inspect.js +4 -3
  55. package/dist/commands/pkg/plugin-manage.js +12 -11
  56. package/dist/commands/surface/node/focus.js +1 -2
  57. package/dist/commands/sys/doctor.js +4 -4
  58. package/dist/commands/sys/setup-core.d.ts +14 -7
  59. package/dist/commands/sys/setup-core.js +66 -11
  60. package/dist/commands/sys/setup-wizard.js +2 -2
  61. package/dist/commands/sys/setup.js +1 -1
  62. package/dist/core/__tests__/cron-held-settlement.test.d.ts +1 -0
  63. package/dist/core/__tests__/cron-held-settlement.test.js +222 -0
  64. package/dist/core/__tests__/helpers/harness.js +1 -2
  65. package/dist/core/__tests__/phase4-review-store.test.js +1 -0
  66. package/dist/core/__tests__/serial/command-plugins.test.js +88 -1
  67. package/dist/core/__tests__/session-model.test.js +5 -3
  68. package/dist/core/bootstrap.d.ts +0 -4
  69. package/dist/core/bootstrap.js +1 -55
  70. package/dist/core/canvas/crons.d.ts +54 -2
  71. package/dist/core/canvas/crons.js +48 -4
  72. package/dist/core/canvas/db.js +23 -0
  73. package/dist/core/command-manifests/manifest.d.ts +11 -0
  74. package/dist/core/command-manifests/manifest.js +45 -4
  75. package/dist/core/command-manifests/schema.d.ts +1 -1
  76. package/dist/core/command-plugins/bundle.d.ts +1 -0
  77. package/dist/core/command-plugins/bundle.js +3 -3
  78. package/dist/core/command-plugins/discovery.d.ts +5 -2
  79. package/dist/core/command-plugins/discovery.js +5 -5
  80. package/dist/core/command-plugins/help-addenda.d.ts +12 -0
  81. package/dist/core/command-plugins/help-addenda.js +30 -0
  82. package/dist/core/command.js +25 -2
  83. package/dist/core/config.js +0 -1
  84. package/dist/core/human/convention.d.ts +0 -1
  85. package/dist/core/human/convention.js +0 -6
  86. package/dist/core/keybindings/inbox.d.ts +6 -8
  87. package/dist/core/keybindings/inbox.js +6 -15
  88. package/dist/core/keybindings/index.d.ts +1 -1
  89. package/dist/core/keybindings/index.js +1 -1
  90. package/dist/core/memory/doc-link-grammar.js +4 -1
  91. package/dist/core/memory-resolver.d.ts +28 -4
  92. package/dist/core/memory-resolver.js +51 -39
  93. package/dist/core/review/stage.js +1 -0
  94. package/dist/core/review/store.d.ts +5 -0
  95. package/dist/core/review/store.js +10 -0
  96. package/dist/core/review/types.d.ts +4 -0
  97. package/dist/core/runtime/broker/event-projection.d.ts +8 -1
  98. package/dist/core/runtime/broker/event-projection.js +25 -1
  99. package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
  100. package/dist/core/runtime/broker/frame-dispatch.js +50 -8
  101. package/dist/core/runtime/broker/inbox.d.ts +4 -0
  102. package/dist/core/runtime/broker/inbox.js +17 -8
  103. package/dist/core/runtime/broker/message-ledger.d.ts +53 -0
  104. package/dist/core/runtime/broker/message-ledger.js +143 -0
  105. package/dist/core/runtime/broker/rebind.js +14 -0
  106. package/dist/core/runtime/broker-protocol.d.ts +46 -1
  107. package/dist/core/runtime/broker.js +11 -2
  108. package/dist/core/runtime/interactive-deliver.d.ts +5 -2
  109. package/dist/core/runtime/interactive-deliver.js +6 -3
  110. package/dist/core/runtime/shell-expansion.d.ts +32 -0
  111. package/dist/core/runtime/shell-expansion.js +102 -0
  112. package/dist/core/session-model/session-state.d.ts +9 -4
  113. package/dist/core/session-model/session-state.js +5 -1
  114. package/dist/daemon/api/handlers/broker-ops.js +8 -0
  115. package/dist/daemon/api/handlers/crons.js +14 -1
  116. package/dist/daemon/api/handlers/inbox.js +5 -0
  117. package/dist/daemon/api/handlers/memory.d.ts +2 -0
  118. package/dist/daemon/api/handlers/memory.js +48 -0
  119. package/dist/daemon/api/handlers/messages.js +7 -1
  120. package/dist/daemon/api/handlers/reviews.js +7 -5
  121. package/dist/daemon/api/map.js +3 -0
  122. package/dist/daemon/api/server.js +2 -0
  123. package/dist/daemon/cron-run.js +71 -3
  124. package/dist/daemon/crtrd.js +3 -0
  125. package/dist/daemon/reconcilers/pending-review-submit.d.ts +7 -0
  126. package/dist/daemon/reconcilers/pending-review-submit.js +35 -0
  127. package/dist/daemon/review/companion.d.ts +8 -0
  128. package/dist/daemon/review/companion.js +35 -0
  129. package/dist/daemon/review/deliver.js +2 -1
  130. package/dist/daemon/review/finish.d.ts +29 -2
  131. package/dist/daemon/review/finish.js +75 -2
  132. package/dist/pi-extensions/canvas-inbox-watcher.js +34 -1
  133. package/dist/shared/generated-context.d.ts +3 -4
  134. package/dist/shared/generated-context.js +24 -6
  135. package/dist/types.d.ts +0 -1
  136. package/package.json +1 -1
  137. package/runtime.lock.json +2 -2
@@ -9,6 +9,44 @@ import { pathExists, ensureDir, removePath, isSymlink, nowIso } from '../../core
9
9
  import { clone, pull, deriveNameFromUrl, currentSha, isGitRepo } from '../../core/git.js';
10
10
  import { readMarketplaceManifest } from '../../core/manifest.js';
11
11
  import { resolveInstallScope } from './shared.js';
12
+ /** Clone a marketplace repo into `scope` and register it in that scope's
13
+ * config. Shared by the `pkg market add` leaf and `crtr sys setup`, which
14
+ * offers the official marketplace as a checked-by-default entry. */
15
+ export function addMarketplace(opts) {
16
+ const { url, ref, scope } = opts;
17
+ const root = requireScopeRoot(scope);
18
+ ensureScopeInitialized(scope, root);
19
+ const tempName = deriveNameFromUrl(url);
20
+ const mktsDir = join(root, 'marketplaces');
21
+ ensureDir(mktsDir);
22
+ const tempDest = join(mktsDir, tempName);
23
+ if (pathExists(tempDest)) {
24
+ removePath(tempDest);
25
+ }
26
+ clone(url, tempDest, { depth: 1, ref: ref !== undefined ? ref : undefined });
27
+ const manifest = readMarketplaceManifest(tempDest);
28
+ if (!manifest) {
29
+ removePath(tempDest);
30
+ throw notFound(`marketplace manifest not found at ${tempDest}/.crouter-marketplace/marketplace.json — not a valid marketplace`);
31
+ }
32
+ const finalName = manifest.name;
33
+ const finalDest = join(mktsDir, finalName);
34
+ if (finalName !== tempName) {
35
+ if (pathExists(finalDest)) {
36
+ removePath(finalDest);
37
+ }
38
+ renameSync(tempDest, finalDest);
39
+ }
40
+ const effectiveRef = ref !== undefined ? ref : 'main';
41
+ updateConfig(scope, (cfg) => {
42
+ cfg.marketplaces[finalName] = {
43
+ url,
44
+ ref: effectiveRef,
45
+ installed_at: nowIso(),
46
+ };
47
+ });
48
+ return { name: finalName, scope, path: finalDest };
49
+ }
12
50
  export const marketAdd = defineLeaf({
13
51
  name: 'add',
14
52
  description: 'add a marketplace by git URL',
@@ -33,39 +71,7 @@ export const marketAdd = defineLeaf({
33
71
  const url = input['url'];
34
72
  const ref = input['ref'];
35
73
  const scopeInput = input['scope'];
36
- const scope = resolveInstallScope(scopeInput);
37
- const root = requireScopeRoot(scope);
38
- ensureScopeInitialized(scope, root);
39
- const tempName = deriveNameFromUrl(url);
40
- const mktsDir = join(root, 'marketplaces');
41
- ensureDir(mktsDir);
42
- const tempDest = join(mktsDir, tempName);
43
- if (pathExists(tempDest)) {
44
- removePath(tempDest);
45
- }
46
- clone(url, tempDest, { depth: 1, ref: ref !== undefined ? ref : undefined });
47
- const manifest = readMarketplaceManifest(tempDest);
48
- if (!manifest) {
49
- removePath(tempDest);
50
- throw notFound(`marketplace manifest not found at ${tempDest}/.crouter-marketplace/marketplace.json — not a valid marketplace`);
51
- }
52
- const finalName = manifest.name;
53
- const finalDest = join(mktsDir, finalName);
54
- if (finalName !== tempName) {
55
- if (pathExists(finalDest)) {
56
- removePath(finalDest);
57
- }
58
- renameSync(tempDest, finalDest);
59
- }
60
- const effectiveRef = ref !== undefined ? ref : 'main';
61
- updateConfig(scope, (cfg) => {
62
- cfg.marketplaces[finalName] = {
63
- url,
64
- ref: effectiveRef,
65
- installed_at: nowIso(),
66
- };
67
- });
68
- return { name: finalName, scope, path: finalDest };
74
+ return addMarketplace({ url, ref, scope: resolveInstallScope(scopeInput) });
69
75
  },
70
76
  });
71
77
  export const marketRemove = defineLeaf({
@@ -4,7 +4,7 @@ import { paginate } from '../../core/pagination.js';
4
4
  import { listInstalledPlugins, findPluginByName, } from '../../core/resolver.js';
5
5
  import { listPluginMemoryDocs } from '../../core/memory-resolver.js';
6
6
  import { validatePluginCommands, validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
7
- import { SUBTREE_NAMES } from '../../build-root.js';
7
+ import { SUBTREE_NAMES, coreCommandPaths } from '../../build-root.js';
8
8
  import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
9
9
  export const pluginList = defineLeaf({
10
10
  name: 'list',
@@ -120,13 +120,14 @@ export const pluginShow = defineLeaf({
120
120
  let commands;
121
121
  if (typeof found.manifest.commands === 'string' && found.manifest.commands.length > 0) {
122
122
  const reserved = new Set(SUBTREE_NAMES);
123
+ const corePaths = await coreCommandPaths();
123
124
  // Validate against the whole effective set so cross-plugin top-level
124
125
  // name collisions show up in this plugin's issues (a collided command is
125
126
  // NOT mounted). A copy outside the effective set (disabled, or shadowed
126
127
  // by a higher-precedence copy) cannot collide, so the per-plugin
127
128
  // validator is exact for it.
128
- const v = validateEffectiveCommandPlugins(reserved).find((x) => x.plugin.root === found.root) ??
129
- validatePluginCommands(found, reserved);
129
+ const v = validateEffectiveCommandPlugins(reserved, undefined, undefined, corePaths).find((x) => x.plugin.root === found.root) ??
130
+ validatePluginCommands(found, reserved, corePaths);
130
131
  commands = {
131
132
  manifestPath: v.manifestPath,
132
133
  mounts: v.contributions.map((c) => c.node.name),
@@ -13,22 +13,23 @@ import { validatePluginCommands, validateEffectiveCommandPlugins } from '../../c
13
13
  import { normalizeEndpoint, validateEndpoint, validateHttpPluginTransport } from '../../core/command-plugins/endpoint.js';
14
14
  import { validatePluginBundle } from '../../core/command-plugins/bundle.js';
15
15
  import { fetchHttpPluginBundle } from '../../core/command-plugins/transport/http-fetch.js';
16
- import { SUBTREE_NAMES } from '../../build-root.js';
16
+ import { SUBTREE_NAMES, coreCommandPaths } from '../../build-root.js';
17
17
  import { isGitUrl, setPluginEnabled, resolveInstallScope } from './shared.js';
18
18
  /** Validate a just-installed/updated plugin's command manifest, if it declares
19
19
  * one. Returns undefined for plugins without a `commands` pointer. Never
20
20
  * executes the plugin binary — validation is fully static. */
21
- function commandReport(name, scope) {
21
+ async function commandReport(name, scope) {
22
22
  const plugin = findPluginByName(name, scope);
23
23
  if (plugin === null || typeof plugin.manifest.commands !== 'string' || plugin.manifest.commands.length === 0) {
24
24
  return undefined;
25
25
  }
26
26
  const reserved = new Set(SUBTREE_NAMES);
27
+ const corePaths = await coreCommandPaths();
27
28
  // Report from the effective set so a cross-plugin top-level collision is
28
29
  // visible at install time (a collided command is NOT mounted). A copy
29
30
  // outside the effective set (disabled, or shadowed) cannot collide.
30
- const v = validateEffectiveCommandPlugins(reserved).find((x) => x.plugin.root === plugin.root) ??
31
- validatePluginCommands(plugin, reserved);
31
+ const v = validateEffectiveCommandPlugins(reserved, undefined, undefined, corePaths).find((x) => x.plugin.root === plugin.root) ??
32
+ validatePluginCommands(plugin, reserved, corePaths);
32
33
  return { mounts: v.contributions.map((c) => c.node.name), issues: v.issues };
33
34
  }
34
35
  const PLUGIN_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
@@ -65,9 +66,9 @@ function writeStagedBundle(stagingRoot, manifest, bundle) {
65
66
  writeFileSync(target, document.bytes);
66
67
  }
67
68
  }
68
- function validateStagedBundle(name, scope, root, manifest) {
69
+ async function validateStagedBundle(name, scope, root, manifest) {
69
70
  const staged = { name, scope, root, manifest, enabled: true };
70
- const validation = validatePluginCommands(staged, new Set(SUBTREE_NAMES));
71
+ const validation = validatePluginCommands(staged, new Set(SUBTREE_NAMES), await coreCommandPaths());
71
72
  if (validation.issues.length > 0) {
72
73
  throw general(`bundle staged package for "${name}" is invalid`, { issues: validation.issues });
73
74
  }
@@ -104,7 +105,7 @@ async function replaceBundlePlugin(name, bundleSource, scope, options) {
104
105
  throw network(message);
105
106
  throw general(message);
106
107
  }
107
- const validated = await validatePluginBundle(fetched.raw, { reservedCoreNames: new Set(SUBTREE_NAMES) });
108
+ const validated = await validatePluginBundle(fetched.raw, { reservedCoreNames: new Set(SUBTREE_NAMES), coreCommandPaths: await coreCommandPaths() });
108
109
  if (validated.bundle === undefined)
109
110
  throw general(`HTTP plugin bundle for "${name}" is invalid`, { issues: validated.issues });
110
111
  const version = createHash('sha256').update(fetched.raw).digest('hex').slice(0, 12);
@@ -126,7 +127,7 @@ async function replaceBundlePlugin(name, bundleSource, scope, options) {
126
127
  try {
127
128
  ensureDir(staging);
128
129
  writeStagedBundle(staging, manifest, validated.bundle);
129
- validateStagedBundle(name, scope, staging, manifest);
130
+ await validateStagedBundle(name, scope, staging, manifest);
130
131
  let movedPrevious = false;
131
132
  if (pathExists(root)) {
132
133
  renameSync(root, previous);
@@ -155,7 +156,7 @@ async function replaceBundlePlugin(name, bundleSource, scope, options) {
155
156
  state.plugins[name] = {};
156
157
  state.plugins[name].last_updated = nowIso();
157
158
  });
158
- const commands = commandReport(name, scope);
159
+ const commands = await commandReport(name, scope);
159
160
  if (commands === undefined)
160
161
  throw general(`bundle plugin "${name}" has no staged command report`);
161
162
  return { name, scope, path: root, transport: 'http', version, docs: validated.bundle.memory.length, commands };
@@ -368,7 +369,7 @@ async function updateOnePlugin(plugin, marketplaceCache, opts) {
368
369
  }
369
370
  const manifest = readPluginManifest(plugin.root);
370
371
  updatePluginVersion(plugin.scope, plugin.name, marketplaceVersion ?? manifest?.version);
371
- const commands = commandReport(plugin.name, plugin.scope);
372
+ const commands = await commandReport(plugin.name, plugin.scope);
372
373
  return {
373
374
  name: plugin.name,
374
375
  ...(plugin.manifest.transport !== undefined ? { transport: plugin.manifest.transport.kind } : {}),
@@ -435,7 +436,7 @@ export const pluginInstall = defineLeaf({
435
436
  : classified.kind === 'local' ? installFromLocal(classified.value, scope)
436
437
  : installFromGit(classified.value, gitRef, scope);
437
438
  const plugin = findPluginByName(installed.name, installed.scope);
438
- const commands = commandReport(installed.name, installed.scope);
439
+ const commands = await commandReport(installed.name, installed.scope);
439
440
  return {
440
441
  ...installed,
441
442
  ...(plugin?.manifest.transport !== undefined ? { transport: plugin.manifest.transport.kind } : {}),
@@ -1,7 +1,6 @@
1
1
  // `crtr surface node focus` — bring one viewer forward.
2
2
  import { defineLeaf } from '../../../core/command.js';
3
3
  import { InputError } from '../../../core/io.js';
4
- import { inboxHint } from '../../../core/keybindings/index.js';
5
4
  import { cliClient, rethrowAsCliError } from '../../api-client.js';
6
5
  import { currentTmux, displayPopup, focusViewer, shellQuote } from './placement.js';
7
6
  export const surfaceNodeFocusLeaf = defineLeaf({
@@ -39,7 +38,7 @@ export const surfaceNodeFocusLeaf = defineLeaf({
39
38
  throw new InputError({
40
39
  error: 'not_focusable',
41
40
  message: `node ${id} is a human-ask (kind:human), not a conversation — it has no pi session to focus.`,
42
- next: `The pending question is queued for the human (${inboxHint()} opens the inbox); see it with \`crtr human list\`, or retract it with \`crtr human cancel ${id}\`.`,
41
+ next: `The pending question is queued for the human; see it with \`crtr human list\`, or retract it with \`crtr human cancel ${id}\`.`,
43
42
  });
44
43
  }
45
44
  // Opening history is not resuming work. A terminal/canceled/crashed node has
@@ -13,7 +13,7 @@ import { readFault } from '../../core/runtime/fault.js';
13
13
  import { detectNerdFont, nerdFontInstallCommand, NERD_FONT_SELECT_HINT } from '../../core/runtime/nerd-font.js';
14
14
  import { detectPackageManager } from './setup-core.js';
15
15
  import { validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
16
- import { SUBTREE_NAMES } from '../../build-root.js';
16
+ import { SUBTREE_NAMES, coreCommandPaths } from '../../build-root.js';
17
17
  import { processGeneration, processLocatedGeneration, runtimeManifest, selectedGeneration } from './shared.js';
18
18
  function pass(scope, name, message) {
19
19
  return { scope, name, status: 'pass', message };
@@ -109,14 +109,14 @@ function faultRemediation(fault, nodeId) {
109
109
  * disable/update/remove the plugin). Only plugins whose scope is in `scopes`
110
110
  * are reported, so `--scope` narrows these checks too.
111
111
  */
112
- function runCommandPluginChecks(scopes) {
112
+ async function runCommandPluginChecks(scopes) {
113
113
  const results = [];
114
114
  const inScope = new Set(scopes);
115
115
  const reserved = new Set(SUBTREE_NAMES);
116
116
  // Validate the whole effective set together so cross-plugin top-level name
117
117
  // collisions surface here — a collided command is NOT mounted, and every
118
118
  // claimant must fail, not pass.
119
- for (const v of validateEffectiveCommandPlugins(reserved)) {
119
+ for (const v of validateEffectiveCommandPlugins(reserved, undefined, undefined, await coreCommandPaths())) {
120
120
  const plugin = v.plugin;
121
121
  if (!inScope.has(plugin.scope))
122
122
  continue;
@@ -442,7 +442,7 @@ export const sysDoctorLeaf = defineLeaf({
442
442
  }
443
443
  if (scopes.includes('user'))
444
444
  allResults.push(runNerdFontCheck());
445
- allResults.push(...runCommandPluginChecks(scopes));
445
+ allResults.push(...await runCommandPluginChecks(scopes));
446
446
  allResults.push(...await runFaultChecks());
447
447
  const processLocation = processGeneration();
448
448
  allResults.push(...await generationChecks('selected', selectedGeneration(), processLocation === null));
@@ -1,16 +1,20 @@
1
1
  import { type BindingId, type BindingResolution, type KeybindingDiagnostic, type SparseKeybindingOverrides } from '../../core/keybindings/index.js';
2
- export type SetupPackageId = 'crtr-extensions' | 'oauth-adapter' | 'capture-plugin';
2
+ export declare const OFFICIAL_MARKETPLACE_NAME = "crouter-official-marketplace";
3
+ export declare const OFFICIAL_MARKETPLACE_URL = "https://github.com/crouton-labs/crouter-official-marketplace.git";
4
+ export declare const OFFICIAL_MARKETPLACE_REF = "main";
5
+ export type SetupPackageId = 'official-marketplace' | 'crtr-extensions' | 'oauth-adapter' | 'capture-plugin';
3
6
  /** `nerd-font` is not a command on PATH like the other two — it is an installed
4
7
  * font face, detected by fontconfig/font-directory scan. The viewer renders
5
8
  * every tool and command icon with Nerd Font codepoints, so without it a fresh
6
9
  * machine shows tofu boxes. */
7
10
  export type SetupTool = 'tmux' | 'nvim' | 'nerd-font';
8
- /** `marketplace-plugin` installs a crtr plugin from a registered marketplace
9
- * (`installSource` is a `<marketplace>/<plugin>` ref) instead of running the
10
- * bundled pi CLI. Offered in setup and NOT installed by first-run bootstrap:
11
- * an install that never runs setup — a Northlight guest image, CI — should not
12
- * carry a command whose external binary it lacks. */
13
- export type SetupPackageSourceKind = 'local-path' | 'npm' | 'marketplace-plugin';
11
+ /** `marketplace-source` clones and registers a marketplace repo
12
+ * (`installSource` is its git URL); `marketplace-plugin` installs one plugin
13
+ * from a registered marketplace (`installSource` is a `<marketplace>/<plugin>`
14
+ * ref). Neither runs the bundled pi CLI, and neither happens anywhere but
15
+ * setup: an install that never runs setup — a Northlight guest image, CI —
16
+ * fetches no catalog and carries no command whose external binary it lacks. */
17
+ export type SetupPackageSourceKind = 'local-path' | 'npm' | 'marketplace-source' | 'marketplace-plugin';
14
18
  export type SetupPackageIdentity = string;
15
19
  export type PackageManager = 'brew' | 'apt-get' | 'dnf' | 'pacman' | 'manual';
16
20
  export interface SetupPackageManifestEntry {
@@ -83,6 +87,9 @@ export declare class SetupKeybindingsDraft {
83
87
  markSaved(overrides: SparseKeybindingOverrides<BindingId>, tmuxPassthrough?: readonly BindingId[]): void;
84
88
  }
85
89
  export declare function buildSetupManifest(resolveBuiltinPackageDir?: (name: string) => string): SetupPackageManifestEntry[];
90
+ /** One-line description of what installing an entry will actually run — the
91
+ * wizard's confirm step and the non-TTY dump both show it. */
92
+ export declare function packagePlanAction(entry: SetupPackageManifestEntry): string;
86
93
  export declare function normalizePackageIdentity(source: string, settingsDir: string, homeDir: string): SetupPackageIdentity;
87
94
  export declare function detectInstalledSetupPackageIds(settingsJson: unknown, manifest?: SetupPackageManifestEntry[], settingsPath?: string, homeDir?: string): Set<SetupPackageId>;
88
95
  export declare function detectCommandOnPath(command: string): boolean;
@@ -13,8 +13,12 @@ import { normalizeTmuxPassthrough } from '../../core/config.js';
13
13
  import { detectNerdFont, nerdFontInstallCommand, NERD_FONT_SELECT_HINT } from '../../core/runtime/nerd-font.js';
14
14
  import { ensureRenderer, isRendererReady } from '../../core/termrender/termrender.js';
15
15
  import { installFromMarketplace } from '../pkg/plugin-manage.js';
16
- import { findPluginByName } from '../../core/resolver.js';
16
+ import { addMarketplace } from '../pkg/market-manage.js';
17
+ import { findPluginByName, findMarketplaceByName } from '../../core/resolver.js';
17
18
  import { expandTilde } from '../../core/fs-utils.js';
19
+ export const OFFICIAL_MARKETPLACE_NAME = 'crouter-official-marketplace';
20
+ export const OFFICIAL_MARKETPLACE_URL = 'https://github.com/crouton-labs/crouter-official-marketplace.git';
21
+ export const OFFICIAL_MARKETPLACE_REF = 'main';
18
22
  function isKeybindingsObject(value) {
19
23
  return value !== null && typeof value === 'object' && !Array.isArray(value);
20
24
  }
@@ -169,6 +173,11 @@ export class SetupKeybindingsDraft {
169
173
  }
170
174
  }
171
175
  const PACKAGE_COPY = {
176
+ 'official-marketplace': {
177
+ name: OFFICIAL_MARKETPLACE_NAME,
178
+ shortDescription: 'the official crtr plugin catalog — registered as a source so its plugins are installable by ref.',
179
+ longDescription: 'Shallow-clones github.com/crouton-labs/crouter-official-marketplace into the user scope and registers it, which is what makes `crtr pkg plugin install crouter-official-marketplace/<name>` resolve and what fills `crtr pkg market browse`. Registering the source installs no plugin by itself. Uncheck it on a machine that should reach no external catalog — a plugin selected below still registers the marketplace it comes from, because its ref cannot resolve otherwise.',
180
+ },
172
181
  'crtr-extensions': {
173
182
  name: 'pi-crtr-extensions',
174
183
  shortDescription: 'crtr companion extensions — command palette, help gating, statusline, subscription/OAuth rotation, nested context.',
@@ -186,17 +195,22 @@ const PACKAGE_COPY = {
186
195
  },
187
196
  };
188
197
  const PACKAGE_SOURCES = {
198
+ 'official-marketplace': { kind: 'marketplace-source', source: OFFICIAL_MARKETPLACE_URL },
189
199
  'crtr-extensions': { kind: 'local-path', source: builtinPiPackageDir('pi-crtr-extensions') },
190
200
  'oauth-adapter': { kind: 'npm', source: 'npm:pi-claude-oauth-adapter@0.1.2' },
191
201
  'capture-plugin': { kind: 'marketplace-plugin', source: 'crouter-official-marketplace/capture' },
192
202
  };
193
203
  const LOCAL_PACKAGE_NAME_ALIASES = {
204
+ 'official-marketplace': [],
194
205
  'crtr-extensions': ['pi-crtr-extensions'],
195
206
  'oauth-adapter': [],
196
207
  'capture-plugin': [],
197
208
  };
198
209
  export function buildSetupManifest(resolveBuiltinPackageDir = builtinPiPackageDir) {
210
+ // Order matters at execution: the marketplace source registers before the
211
+ // plugin whose ref needs it.
199
212
  return [
213
+ 'official-marketplace',
200
214
  'crtr-extensions',
201
215
  'oauth-adapter',
202
216
  'capture-plugin',
@@ -215,6 +229,18 @@ export function buildSetupManifest(resolveBuiltinPackageDir = builtinPiPackageDi
215
229
  };
216
230
  });
217
231
  }
232
+ /** One-line description of what installing an entry will actually run — the
233
+ * wizard's confirm step and the non-TTY dump both show it. */
234
+ export function packagePlanAction(entry) {
235
+ switch (entry.sourceKind) {
236
+ case 'marketplace-source':
237
+ return `clone + register ${entry.installSource}`;
238
+ case 'marketplace-plugin':
239
+ return `crtr pkg plugin install ${entry.installSource}`;
240
+ default:
241
+ return `pi install ${entry.installSource}`;
242
+ }
243
+ }
218
244
  function npmPackageName(source) {
219
245
  if (!source.startsWith('npm:'))
220
246
  return null;
@@ -390,7 +416,7 @@ export function buildSetupInitialState(opts = {}) {
390
416
  const manifest = opts.manifest ?? buildSetupManifest(builtinPiPackageDir);
391
417
  const settingsJson = readJsonIfExists(settingsPath);
392
418
  const installedPackageIds = detectInstalledSetupPackageIds(settingsJson, manifest, settingsPath, homeDir);
393
- for (const id of detectInstalledMarketplacePlugins(manifest))
419
+ for (const id of detectInstalledMarketplaceEntries(manifest))
394
420
  installedPackageIds.add(id);
395
421
  const commandExists = opts.commandExists ?? detectCommandOnPath;
396
422
  const packageManager = opts.packageManager ?? detectPackageManager(process.platform, commandExists);
@@ -468,21 +494,37 @@ export function formatSetupExecutionSummary(summary) {
468
494
  summary.exaKeyWritten ? `exa key written: ${summary.exaKeyPath}` : 'exa key written: no',
469
495
  ].join('\n') + '\n';
470
496
  }
471
- /** Marketplace-plugin manifest entries already installed in any scope — the
472
- * plugin registry is their install record, not pi's settings.json. */
473
- function detectInstalledMarketplacePlugins(manifest) {
497
+ /** Marketplace manifest entries already present in any scope — the marketplace
498
+ * and plugin registries are their install record, not pi's settings.json. */
499
+ function detectInstalledMarketplaceEntries(manifest) {
474
500
  const installed = [];
475
501
  for (const entry of manifest) {
476
- if (entry.sourceKind !== 'marketplace-plugin')
477
- continue;
478
502
  try {
479
- if (findPluginByName(entry.name) !== null)
480
- installed.push(entry.id);
503
+ if (entry.sourceKind === 'marketplace-source') {
504
+ if (findMarketplaceByName(entry.name) !== null)
505
+ installed.push(entry.id);
506
+ }
507
+ else if (entry.sourceKind === 'marketplace-plugin') {
508
+ if (findPluginByName(entry.name) !== null)
509
+ installed.push(entry.id);
510
+ }
481
511
  }
482
512
  catch { /* unreadable scope config — treat as not installed */ }
483
513
  }
484
514
  return installed;
485
515
  }
516
+ /** A `<marketplace>/<plugin>` ref only resolves once its marketplace is
517
+ * registered, so selecting a plugin selects its source. Returns the
518
+ * marketplace name when this call registered it. */
519
+ function ensureMarketplaceRegistered(name) {
520
+ if (findMarketplaceByName(name) !== null)
521
+ return null;
522
+ if (name !== OFFICIAL_MARKETPLACE_NAME) {
523
+ throw new Error(`marketplace not registered: ${name}`);
524
+ }
525
+ addMarketplace({ url: OFFICIAL_MARKETPLACE_URL, ref: OFFICIAL_MARKETPLACE_REF, scope: 'user' });
526
+ return name;
527
+ }
486
528
  export async function executeSetupSubmission(submission) {
487
529
  const installedPackages = [];
488
530
  const skippedPackages = [];
@@ -493,15 +535,28 @@ export async function executeSetupSubmission(submission) {
493
535
  skippedPackages.push(`${entry.name} (already installed)`);
494
536
  continue;
495
537
  }
538
+ // A marketplace that will not clone or refresh (offline, diverged cache,
539
+ // moved repo) must not abort the rest of setup.
540
+ if (entry.sourceKind === 'marketplace-source') {
541
+ try {
542
+ addMarketplace({ url: entry.installSource, ref: OFFICIAL_MARKETPLACE_REF, scope: 'user' });
543
+ installedPackages.push(entry.name);
544
+ }
545
+ catch (e) {
546
+ skippedPackages.push(`${entry.name} (${e instanceof Error ? e.message : String(e)})`);
547
+ }
548
+ continue;
549
+ }
496
550
  if (entry.sourceKind === 'marketplace-plugin') {
497
551
  const [marketplace, plugin] = entry.installSource.split('/');
498
552
  try {
553
+ const registered = ensureMarketplaceRegistered(marketplace);
554
+ if (registered !== null)
555
+ installedPackages.push(registered);
499
556
  installFromMarketplace({ marketplace: marketplace, plugin: plugin }, 'user');
500
557
  installedPackages.push(entry.name);
501
558
  }
502
559
  catch (e) {
503
- // A marketplace that will not refresh (offline, diverged cache) must not
504
- // abort the rest of setup.
505
560
  skippedPackages.push(`${entry.name} (${e instanceof Error ? e.message : String(e)})`);
506
561
  }
507
562
  continue;
@@ -7,7 +7,7 @@
7
7
  // confirmed install, that one is a persistent editor that saves as you go.
8
8
  import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
9
9
  import { formatGesture, matchesPiTuiInput, } from "../../core/keybindings/index.js";
10
- import { buildSetupInitialState, executeSetupSubmission, formatSetupExecutionSummary, reconcileInboxFrontDoor, renderSetupStaticInstructions, resolveExaKeyPath, } from "./setup-core.js";
10
+ import { buildSetupInitialState, executeSetupSubmission, formatSetupExecutionSummary, packagePlanAction, reconcileInboxFrontDoor, renderSetupStaticInstructions, resolveExaKeyPath, } from "./setup-core.js";
11
11
  import { runSetupSettingsProcess } from "./setup-settings-runner.js";
12
12
  import { DependenciesPanel } from "./panels/dependencies-panel.js";
13
13
  import { KeysPanel } from "./panels/keys-panel.js";
@@ -239,7 +239,7 @@ export class SetupWizard {
239
239
  if (submission.packages.length) {
240
240
  out.push(truncateToWidth(theme.fg("accent", "Packages"), width));
241
241
  for (const entry of submission.packages)
242
- out.push(truncateToWidth(` \u2022 ${entry.name} ${theme.fg("dim", "\u2192")} ${this.installedPackageIds.has(entry.id) ? theme.fg("success", "skip (already installed)") : theme.fg("muted", `pi install ${entry.installSource}`)}`, width));
242
+ out.push(truncateToWidth(` \u2022 ${entry.name} ${theme.fg("dim", "\u2192")} ${this.installedPackageIds.has(entry.id) ? theme.fg("success", "skip (already installed)") : theme.fg("muted", packagePlanAction(entry))}`, width));
243
243
  }
244
244
  if (submission.systemDeps.length) {
245
245
  out.push(truncateToWidth(theme.fg("accent", "System deps"), width));
@@ -23,7 +23,7 @@ export const sysSetupLeaf = defineLeaf({
23
23
  'Opens a four-step pi-tui installer. Step one logs in to a model provider (pi\'s real OAuth/API-key flow, same as in-app /login) so a fresh install has credentials before running any node.',
24
24
  'The remaining steps collect companion package installs, system dependency installs, and an Exa API key, then confirm them all at once.',
25
25
  'Runs the bundled pi CLI to install the selected companion packages, writes EXA_API_KEY to ~/.crouter/exa.key when provided, and prints a summary.',
26
- 'The companion-package list includes crtr marketplace plugins (currently `capture`), checked by default and installed into the user scope when left checked. An unreachable marketplace is reported as skipped rather than failing setup.',
26
+ 'The companion-package list includes the official crtr marketplace and the plugins installed from it (currently `capture`), all checked by default and registered or installed into the user scope when left checked. Nothing else registers a marketplace, so an install that never runs setup fetches no catalog. An unreachable marketplace is reported as skipped rather than failing setup.',
27
27
  'On every invocation (best-effort, idempotent), refreshes crouter-owned tmux bindings and registers known nodes\' interaction roots so resolved tickets can call back into the asking node. The inbox popup toggle is a crouter-owned keybinding, editable on the keybindings tab.',
28
28
  ],
29
29
  },
@@ -0,0 +1 @@
1
+ export {};