@north-light/crouter 0.3.169 → 0.3.171

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 (122) hide show
  1. package/dist/build-root.d.ts +1 -16
  2. package/dist/build-root.js +1 -25
  3. package/dist/builtin-memory/insights/capture.md +66 -0
  4. package/dist/builtin-memory/insights/init.md +45 -0
  5. package/dist/builtin-memory/internal/plugins.md +12 -11
  6. package/dist/cli.js +2 -6
  7. package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -1
  8. package/dist/clients/attach/__tests__/context-message.test.js +50 -3
  9. package/dist/clients/attach/__tests__/diagram.test.js +1 -1
  10. package/dist/clients/attach/chrome/header.d.ts +10 -0
  11. package/dist/clients/attach/chrome/header.js +35 -0
  12. package/dist/clients/attach/chrome/status-line.d.ts +3 -0
  13. package/dist/clients/attach/chrome/status-line.js +5 -0
  14. package/dist/clients/attach/chrome/widgets.js +7 -18
  15. package/dist/clients/attach/input/controller.d.ts +10 -0
  16. package/dist/clients/attach/input/controller.js +3 -0
  17. package/dist/clients/attach/overlays/help.d.ts +33 -0
  18. package/dist/clients/attach/overlays/help.js +204 -0
  19. package/dist/clients/attach/render/chat-view.d.ts +81 -9
  20. package/dist/clients/attach/render/chat-view.js +434 -90
  21. package/dist/clients/attach/render/context-message.js +3 -3
  22. package/dist/clients/attach/render/diagram.js +2 -2
  23. package/dist/clients/attach/render/measured-container.d.ts +94 -0
  24. package/dist/clients/attach/render/measured-container.js +307 -0
  25. package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
  26. package/dist/clients/attach/render/transcript-copy.js +81 -0
  27. package/dist/clients/attach/render/viewport.d.ts +29 -0
  28. package/dist/clients/attach/render/viewport.js +160 -0
  29. package/dist/clients/attach/session/bindings.d.ts +5 -1
  30. package/dist/clients/attach/session/bindings.js +16 -0
  31. package/dist/clients/attach/session/context.d.ts +5 -3
  32. package/dist/clients/attach/session/frame.d.ts +34 -0
  33. package/dist/clients/attach/session/frame.js +173 -0
  34. package/dist/clients/attach/session/frames.d.ts +2 -0
  35. package/dist/clients/attach/session/frames.js +3 -0
  36. package/dist/clients/attach/session/input-wiring.d.ts +10 -4
  37. package/dist/clients/attach/session/input-wiring.js +13 -13
  38. package/dist/clients/attach/session/keys.d.ts +4 -1
  39. package/dist/clients/attach/session/keys.js +9 -4
  40. package/dist/clients/attach/session/layout.d.ts +10 -8
  41. package/dist/clients/attach/session/layout.js +32 -31
  42. package/dist/clients/attach/session/mouse.d.ts +15 -0
  43. package/dist/clients/attach/session/mouse.js +61 -0
  44. package/dist/clients/attach/slash/dispatch.d.ts +10 -0
  45. package/dist/clients/attach/slash/dispatch.js +35 -2
  46. package/dist/clients/attach/viewer.js +605 -581
  47. package/dist/clients/web/dev-server.d.ts +4 -1
  48. package/dist/clients/web/dev-server.js +8 -19
  49. package/dist/clients/web/web-cmd.js +1 -1
  50. package/dist/commands/human/prompts.js +16 -6
  51. package/dist/commands/pkg/plugin-manage.js +143 -68
  52. package/dist/commands/sys/__tests__/setup-core.test.js +4 -2
  53. package/dist/commands/sys/setup-core.js +21 -16
  54. package/dist/core/__tests__/human-deliver.test.js +62 -2
  55. package/dist/core/clipboard-text.d.ts +8 -0
  56. package/dist/core/clipboard-text.js +18 -0
  57. package/dist/core/command-manifests/registry.d.ts +1 -3
  58. package/dist/core/command-plugins/bundle.d.ts +29 -0
  59. package/dist/core/command-plugins/bundle.js +198 -0
  60. package/dist/core/command-plugins/discovery.d.ts +0 -8
  61. package/dist/core/command-plugins/discovery.js +2 -39
  62. package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
  63. package/dist/core/command-plugins/transport/http-fetch.js +17 -95
  64. package/dist/core/command.d.ts +9 -13
  65. package/dist/core/command.js +12 -17
  66. package/dist/core/config.js +2 -1
  67. package/dist/core/keybindings/__tests__/resolve.test.js +4 -0
  68. package/dist/core/keybindings/attach-control.d.ts +3 -0
  69. package/dist/core/keybindings/attach-control.js +1 -0
  70. package/dist/core/keybindings/catalog.d.ts +2 -2
  71. package/dist/core/keybindings/catalog.js +8 -0
  72. package/dist/core/runtime/broker-protocol.d.ts +13 -1
  73. package/dist/core/runtime/broker.js +213 -5
  74. package/dist/core/runtime/canvas-extensions.d.ts +3 -0
  75. package/dist/core/runtime/canvas-extensions.js +3 -0
  76. package/dist/core/runtime/front-door.js +4 -4
  77. package/dist/core/runtime/kickoff.d.ts +1 -5
  78. package/dist/core/runtime/kickoff.js +1 -5
  79. package/dist/core/runtime/naming.d.ts +7 -0
  80. package/dist/core/runtime/naming.js +5 -2
  81. package/dist/core/runtime/node-read.js +15 -0
  82. package/dist/core/runtime/package-health.d.ts +6 -21
  83. package/dist/core/runtime/package-health.js +12 -123
  84. package/dist/core/runtime/stop-guard.d.ts +1 -1
  85. package/dist/core/runtime/stop-guard.js +3 -3
  86. package/dist/core/runtime/tool-group-summary.d.ts +19 -0
  87. package/dist/core/runtime/tool-group-summary.js +117 -0
  88. package/dist/core/termrender/code-doc.d.ts +12 -0
  89. package/dist/core/termrender/code-doc.js +91 -0
  90. package/dist/core/termrender/display.d.ts +12 -0
  91. package/dist/core/termrender/display.js +19 -0
  92. package/dist/core/termrender/termrender.d.ts +73 -0
  93. package/dist/core/termrender/termrender.js +795 -0
  94. package/dist/core/termrender/version.d.ts +1 -0
  95. package/dist/core/termrender/version.js +1 -0
  96. package/dist/core/user-settings.d.ts +14 -0
  97. package/dist/core/user-settings.js +20 -0
  98. package/dist/daemon/crtrd.js +5 -6
  99. package/dist/index.d.ts +2 -0
  100. package/dist/index.js +1 -0
  101. package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
  102. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
  103. package/dist/pi-extensions/canvas-stophook.js +2 -2
  104. package/dist/pi-extensions/canvas-structured-output.js +52 -8
  105. package/dist/pi-extensions/summary-tool.d.ts +33 -0
  106. package/dist/pi-extensions/summary-tool.js +62 -0
  107. package/dist/shared/generated-context.d.ts +19 -4
  108. package/dist/shared/generated-context.js +98 -7
  109. package/dist/shared/tool-groups.d.ts +23 -0
  110. package/dist/shared/tool-groups.js +60 -0
  111. package/dist/types.d.ts +11 -0
  112. package/dist/types.js +1 -0
  113. package/dist/web-client/assets/{index-U_NZ66VE.js → index-D7I3gHCL.js} +26 -19
  114. package/dist/web-client/index.html +1 -1
  115. package/dist/web-client/sw.js +1 -1
  116. package/package.json +3 -1
  117. package/runtime.lock.json +62 -3
  118. package/scripts/postinstall.mjs +14 -1
  119. package/dist/core/command-plugins/store.d.ts +0 -16
  120. package/dist/core/command-plugins/store.js +0 -64
  121. package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
  122. package/dist/core/runtime/fault-recovery-nudge.js +0 -3
@@ -37,3 +37,21 @@ export function writeClipboardText(text) {
37
37
  }
38
38
  return false;
39
39
  }
40
+ /** Write `text` to the system clipboard AND, inside tmux, to the tmux paste
41
+ * buffer via `tmux load-buffer -w -` (which makes tmux emit OSC 52 outward on
42
+ * the app's behalf). Never emits OSC 52 from this process. Never throws.
43
+ * Returns which operations succeeded. */
44
+ export function writeClipboardTextEverywhere(text) {
45
+ const system = writeClipboardText(text);
46
+ let tmux = false;
47
+ if (process.env.TMUX !== undefined) {
48
+ const result = spawnSync('tmux', ['load-buffer', '-w', '-'], {
49
+ input: text,
50
+ timeout: SPAWN_TIMEOUT_MS,
51
+ encoding: 'utf-8',
52
+ });
53
+ // Success if no error and exit status 0.
54
+ tmux = !result.error && result.status === 0;
55
+ }
56
+ return { system, tmux };
57
+ }
@@ -25,9 +25,7 @@ export interface ContributorIssue extends CommandManifestIssue {
25
25
  contributor: CommandContributorRef;
26
26
  }
27
27
  /** Invocation-local registry snapshot after collision resolution: the accepted
28
- * contributions and every discovered issue. The amended spec has no freshness
29
- * machinery, so there is no owner-routing map — an unknown-token miss hydrates
30
- * ALL absent-store registrations once (§5.1), never a single known owner. */
28
+ * contributions and every discovered issue. */
31
29
  export interface CommandRegistrySnapshot {
32
30
  /** Accepted top-level contributions, deterministically ordered by
33
31
  * contributor name. */
@@ -0,0 +1,29 @@
1
+ import { type ValidatedCommandManifest } from '../command-manifests/manifest.js';
2
+ export interface BundleIssue {
3
+ code: string;
4
+ path?: string;
5
+ message: string;
6
+ received: string;
7
+ expected: string;
8
+ next: string;
9
+ }
10
+ export interface ValidatedBundle {
11
+ bundleVersion: 1;
12
+ commands: ValidatedCommandManifest;
13
+ commandsBytes: Uint8Array;
14
+ directories: ReadonlyArray<{
15
+ path: string;
16
+ }>;
17
+ memory: ReadonlyArray<{
18
+ path: string;
19
+ bytes: Uint8Array;
20
+ }>;
21
+ }
22
+ export interface PluginBundleValidation {
23
+ bundle?: ValidatedBundle;
24
+ issues: BundleIssue[];
25
+ }
26
+ /** Validate a Core-served plugin directory archive entirely in memory. */
27
+ export declare function validatePluginBundle(archive: Uint8Array, options: {
28
+ reservedCoreNames: ReadonlySet<string>;
29
+ }): Promise<PluginBundleValidation>;
@@ -0,0 +1,198 @@
1
+ import { Parser } from 'tar';
2
+ import { validateCommandManifest } from '../command-manifests/manifest.js';
3
+ function issue(code, message, received, expected, next, path) {
4
+ return { code, message, received, expected, next, ...(path !== undefined ? { path } : {}) };
5
+ }
6
+ function bundleInvalid(message, received, expected, next, path) {
7
+ return issue('plugin_bundle_invalid', message, received, expected, next, path);
8
+ }
9
+ function archiveEnvelopeIssue(archive) {
10
+ // tar v7's Parser always sniffs gzip, even when passed gzip: false. Reject
11
+ // its unambiguous magic bytes before parsing so archive plugins stay tar-only.
12
+ if (archive[0] === 0x1f && archive[1] === 0x8b) {
13
+ return bundleInvalid('bundle archive must be uncompressed tar data', 'gzip-compressed archive', 'an uncompressed tar archive', 'Serve the archive without compression.');
14
+ }
15
+ if (archive.length === 0 || archive.length % 512 !== 0) {
16
+ return bundleInvalid('bundle archive must be a non-empty whole number of 512-byte tar blocks', `${archive.length} bytes`, 'a non-empty tar stream with a 512-byte block length', 'Serve one complete uncompressed tar archive.');
17
+ }
18
+ if (archive.length < 1024 || !archive.subarray(-1024).every((byte) => byte === 0)) {
19
+ return bundleInvalid('bundle archive is missing the required two zero-block terminator', 'archive tail is not 1024 zero bytes', 'a complete tar stream ending in two 512-byte zero blocks', 'Serve the complete uncompressed tar archive.');
20
+ }
21
+ return undefined;
22
+ }
23
+ function validateMemberName(path, type) {
24
+ if (path.length === 0 || path.includes('\u0000') || path.includes('\\') || path.startsWith('/') || /^[A-Za-z]:($|\/)/.test(path)) {
25
+ return bundleInvalid('bundle member path must be a safe relative POSIX path', path, 'a relative path without absolute, drive-letter, backslash, or NUL forms', 'Use a safe relative POSIX member path.', path);
26
+ }
27
+ if (type === 'file' && path.endsWith('/')) {
28
+ return bundleInvalid('only directory members may have a trailing slash', path, 'a regular-file path without a trailing slash', 'Emit this member as a regular file path.', path);
29
+ }
30
+ const segments = (type === 'directory' && path.endsWith('/') ? path.slice(0, -1) : path).split('/');
31
+ if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
32
+ return bundleInvalid('bundle member path contains an unsafe segment', path, 'non-empty segments other than . or ..', 'Use a safe relative POSIX member path.', path);
33
+ }
34
+ return undefined;
35
+ }
36
+ function validateMemberShape(path, type) {
37
+ if (type === 'file') {
38
+ if (path === 'bundle.json' || path === 'commands.json' || (path.startsWith('memory/') && path.endsWith('.md')))
39
+ return undefined;
40
+ return bundleInvalid('bundle contains an unsupported file member', path, 'bundle.json, commands.json, or memory/**/*.md', 'Remove unsupported members from the archive.', path);
41
+ }
42
+ if (path === 'memory' || path.startsWith('memory/'))
43
+ return undefined;
44
+ return bundleInvalid('bundle contains an unsupported directory member', path, 'memory or a directory below memory/', 'Remove unsupported directory members from the archive.', path);
45
+ }
46
+ /** A member path as it will exist on disk: directories carry no trailing slash. */
47
+ function materializedPath(member) {
48
+ return member.type === 'directory' && member.path.endsWith('/') ? member.path.slice(0, -1) : member.path;
49
+ }
50
+ /** Reject member sets that cannot become one filesystem tree — a path claimed as
51
+ * both a file and a directory, the same path twice after directory
52
+ * normalization, or a file that another member nests beneath. */
53
+ function validateMemberTree(members) {
54
+ const claimed = new Map();
55
+ for (const member of members) {
56
+ const path = materializedPath(member);
57
+ const existing = claimed.get(path);
58
+ if (existing !== undefined) {
59
+ return bundleInvalid(existing === member.type
60
+ ? 'bundle contains a duplicate member'
61
+ : 'bundle claims one path as both a file and a directory', `${path} (${existing} and ${member.type})`, 'each materialized path exactly once, with one type', 'Emit each archive path once, as a single member type.', path);
62
+ }
63
+ claimed.set(path, member.type);
64
+ }
65
+ for (const [path, type] of claimed) {
66
+ if (type !== 'file')
67
+ continue;
68
+ for (const other of claimed.keys()) {
69
+ if (other.startsWith(`${path}/`)) {
70
+ return bundleInvalid('bundle nests a member beneath a regular-file path', `${other} beneath file ${path}`, 'members nested only beneath directory paths', 'Emit the parent path as a directory or remove the nested member.', other);
71
+ }
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+ async function parseArchive(archive) {
77
+ const members = [];
78
+ const seen = new Set();
79
+ let memberIssue;
80
+ await new Promise((resolve, reject) => {
81
+ const parser = new Parser({
82
+ strict: true,
83
+ maxMetaEntrySize: Number.MAX_SAFE_INTEGER,
84
+ gzip: false,
85
+ brotli: false,
86
+ zstd: false,
87
+ onReadEntry: (entry) => {
88
+ const path = entry.path;
89
+ const type = entry.type === 'File' || entry.type === 'OldFile'
90
+ ? 'file'
91
+ : entry.type === 'Directory'
92
+ ? 'directory'
93
+ : undefined;
94
+ if (type === undefined || entry.unsupported || entry.ignore || entry.linkpath !== undefined) {
95
+ memberIssue ??= bundleInvalid('bundle member must be a regular file or directory without a link target', `${path} (${entry.type})`, 'a regular file or directory member without link metadata', 'Remove links and unsupported member types from the archive.', path);
96
+ entry.resume();
97
+ return;
98
+ }
99
+ memberIssue ??= validateMemberName(path, type);
100
+ memberIssue ??= validateMemberShape(path, type);
101
+ if (seen.has(path)) {
102
+ memberIssue ??= bundleInvalid('bundle contains a duplicate member', path, 'each member path exactly once', 'Emit each archive member only once.', path);
103
+ }
104
+ seen.add(path);
105
+ const chunks = [];
106
+ entry.on('data', (chunk) => chunks.push(chunk));
107
+ entry.on('end', () => {
108
+ if (memberIssue === undefined)
109
+ members.push({ path, type, bytes: new Uint8Array(Buffer.concat(chunks)) });
110
+ });
111
+ },
112
+ });
113
+ parser.on('ignoredEntry', (entry) => {
114
+ const path = entry.path;
115
+ memberIssue ??= bundleInvalid('bundle contains an ignored or unsupported archive member', `${path} (${entry.type})`, 'only supported regular file or directory members', 'Remove ignored or unsupported members from the archive.', path);
116
+ entry.resume();
117
+ });
118
+ parser.once('error', reject);
119
+ parser.once('end', resolve);
120
+ try {
121
+ parser.end(Buffer.from(archive));
122
+ }
123
+ catch (error) {
124
+ reject(error);
125
+ }
126
+ }).catch((error) => {
127
+ memberIssue ??= bundleInvalid('bundle archive could not be parsed', error instanceof Error ? error.message : String(error), 'a valid complete uncompressed tar archive', 'Fix the archive format and serve it again.');
128
+ });
129
+ return memberIssue === undefined ? { members } : { issue: memberIssue };
130
+ }
131
+ function parseBundleMetadata(bytes) {
132
+ let raw;
133
+ try {
134
+ raw = JSON.parse(Buffer.from(bytes).toString('utf8'));
135
+ }
136
+ catch {
137
+ return bundleInvalid('bundle.json is not valid JSON', 'invalid JSON', '{"bundleVersion":1}', 'Write valid bundle metadata.', 'bundle.json');
138
+ }
139
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw) || Object.keys(raw).length !== 1 || raw['bundleVersion'] !== 1) {
140
+ return bundleInvalid('bundle.json must contain exactly {"bundleVersion":1}', JSON.stringify(raw), 'an object with only bundleVersion set to 1', 'Set bundle.json to {"bundleVersion":1}.', 'bundle.json');
141
+ }
142
+ return undefined;
143
+ }
144
+ function parseCommands(bytes, reservedCoreNames) {
145
+ let raw;
146
+ try {
147
+ raw = JSON.parse(Buffer.from(bytes).toString('utf8'));
148
+ }
149
+ catch {
150
+ return {
151
+ issues: [bundleInvalid('commands.json is not valid JSON', 'invalid JSON', 'a valid command-manifest JSON object', 'Regenerate commands.json.', 'commands.json')],
152
+ };
153
+ }
154
+ const validation = validateCommandManifest(raw, { transport: 'http', reservedCoreNames });
155
+ return {
156
+ ...(validation.manifest !== undefined ? { commands: validation.manifest } : {}),
157
+ issues: validation.issues,
158
+ };
159
+ }
160
+ /** Validate a Core-served plugin directory archive entirely in memory. */
161
+ export async function validatePluginBundle(archive, options) {
162
+ const envelope = archiveEnvelopeIssue(archive);
163
+ if (envelope !== undefined)
164
+ return { issues: [envelope] };
165
+ const parsed = await parseArchive(archive);
166
+ if (parsed.issue !== undefined || parsed.members === undefined)
167
+ return { issues: [parsed.issue] };
168
+ const treeIssue = validateMemberTree(parsed.members);
169
+ if (treeIssue !== undefined)
170
+ return { issues: [treeIssue] };
171
+ const bundle = parsed.members.find((member) => member.path === 'bundle.json' && member.type === 'file');
172
+ const commands = parsed.members.find((member) => member.path === 'commands.json' && member.type === 'file');
173
+ if (bundle === undefined || commands === undefined) {
174
+ return {
175
+ issues: [bundleInvalid('bundle must contain exactly one bundle.json and one commands.json regular file', `bundle.json=${bundle !== undefined}, commands.json=${commands !== undefined}`, 'one regular bundle.json and one regular commands.json member', 'Add the required metadata members to the archive.')],
176
+ };
177
+ }
178
+ const metadataIssue = parseBundleMetadata(bundle.bytes);
179
+ if (metadataIssue !== undefined)
180
+ return { issues: [metadataIssue] };
181
+ const commandValidation = parseCommands(commands.bytes, options.reservedCoreNames);
182
+ if (commandValidation.commands === undefined)
183
+ return { issues: commandValidation.issues };
184
+ return {
185
+ bundle: {
186
+ bundleVersion: 1,
187
+ commands: commandValidation.commands,
188
+ commandsBytes: commands.bytes,
189
+ directories: parsed.members
190
+ .filter((member) => member.type === 'directory')
191
+ .map((member) => ({ path: member.path })),
192
+ memory: parsed.members
193
+ .filter((member) => member.type === 'file' && member.path.startsWith('memory/'))
194
+ .map((member) => ({ path: member.path, bytes: member.bytes })),
195
+ },
196
+ issues: [],
197
+ };
198
+ }
@@ -2,7 +2,6 @@ import type { InstalledPlugin, PluginTransport } from '../../types.js';
2
2
  import { type ValidatedCommandManifest } from '../command-manifests/manifest.js';
3
3
  import type { CommandManifestIssue, DeclBranch as DeclBranchGeneric, DeclLeaf as DeclLeafGeneric } from '../command-manifests/schema.js';
4
4
  import type { CommandRegistrySnapshot } from '../command-manifests/registry.js';
5
- import type { HttpPluginManifestStore } from './store.js';
6
5
  export interface CommandDiscoveryIssue extends CommandManifestIssue {
7
6
  plugin: string;
8
7
  }
@@ -22,12 +21,6 @@ export interface PluginCommandValidation {
22
21
  transport?: PluginTransport;
23
22
  contributions: ValidatedContribution[];
24
23
  issues: CommandDiscoveryIssue[];
25
- absentStore?: boolean;
26
- httpManifestStore?: HttpPluginManifestStore;
27
- }
28
- export interface AbsentStoreHydration {
29
- hydratedAny: boolean;
30
- diagnostics: string[];
31
24
  }
32
25
  export declare function effectiveCommandPlugins(startDir?: string, profileId?: string | null): InstalledPlugin[];
33
26
  export declare function validatePluginCommands(plugin: InstalledPlugin, reservedNames?: ReadonlySet<string>): PluginCommandValidation;
@@ -38,4 +31,3 @@ export declare function discoverCommandContributions(reservedNames: ReadonlySet<
38
31
  issues: CommandDiscoveryIssue[];
39
32
  };
40
33
  export declare function buildExternalCommandSnapshot(reservedCoreNames: ReadonlySet<string>): CommandRegistrySnapshot;
41
- export declare function hydrateAbsentHttpPlugins(): Promise<AbsentStoreHydration>;
@@ -6,8 +6,6 @@ import { validateCommandManifest } from '../command-manifests/manifest.js';
6
6
  import { resolveCommandRegistry } from '../command-manifests/registry.js';
7
7
  import { adaptPluginContributions } from './compose.js';
8
8
  import { validateHttpPluginTransport } from './endpoint.js';
9
- import { readHttpPluginManifest, resolveHttpPluginManifestStore, writeHttpPluginManifest } from './store.js';
10
- import { fetchHttpPluginManifest } from './transport/http-fetch.js';
11
9
  export function effectiveCommandPlugins(startDir = process.cwd(), profileId) {
12
10
  const seen = new Set();
13
11
  const result = [];
@@ -97,10 +95,7 @@ export function validatePluginCommands(plugin, reservedNames = new Set()) {
97
95
  const transport = transportFor(plugin, issues);
98
96
  if (transport === undefined)
99
97
  return { plugin, manifestPath, contributions: [], issues };
100
- const httpManifestStore = transport.kind === 'http'
101
- ? resolveHttpPluginManifestStore({ root: plugin.root, commands }) ?? undefined
102
- : undefined;
103
- const safe = transport.kind === 'http' ? httpManifestStore?.path ?? null : safePath(plugin.root, commands);
98
+ const safe = safePath(plugin.root, commands);
104
99
  if (safe === null) {
105
100
  issues.push({ code: 'command_path_unsafe', plugin: plugin.name, message: 'commands pointer escapes the plugin root or is not a regular file', received: commands, expected: 'a relative path to a regular file inside the plugin root', next: 'Fix plugin.json.commands.', path: 'commands' });
106
101
  return { plugin, manifestPath, transport, contributions: [], issues };
@@ -108,14 +103,12 @@ export function validatePluginCommands(plugin, reservedNames = new Set()) {
108
103
  manifestPath = safe;
109
104
  let bytes;
110
105
  try {
111
- bytes = transport.kind === 'http' ? readHttpPluginManifest(httpManifestStore) : readFileSync(safe);
106
+ bytes = readFileSync(safe);
112
107
  }
113
108
  catch {
114
109
  bytes = null;
115
110
  }
116
111
  if (bytes === null) {
117
- if (transport.kind === 'http')
118
- return { plugin, manifestPath, transport, contributions: [], issues, absentStore: true, httpManifestStore };
119
112
  issues.push({ code: 'command_manifest_unreadable', plugin: plugin.name, message: 'commands.json is missing or unreadable', received: safe, expected: 'a readable JSON file', next: 'Reinstall or update the plugin.' });
120
113
  return { plugin, manifestPath, transport, contributions: [], issues };
121
114
  }
@@ -124,8 +117,6 @@ export function validatePluginCommands(plugin, reservedNames = new Set()) {
124
117
  raw = JSON.parse(Buffer.from(bytes).toString('utf8'));
125
118
  }
126
119
  catch {
127
- if (transport.kind === 'http')
128
- return { plugin, manifestPath, transport, contributions: [], issues, absentStore: true, httpManifestStore };
129
120
  issues.push({ code: 'command_manifest_invalid', plugin: plugin.name, message: 'commands.json is not valid JSON', received: safe, expected: 'a JSON object', next: 'Regenerate commands.json.' });
130
121
  return { plugin, manifestPath, transport, contributions: [], issues };
131
122
  }
@@ -171,31 +162,3 @@ export function buildExternalCommandSnapshot(reservedCoreNames) {
171
162
  const resolved = resolveCommandRegistry(candidates, reservedCoreNames);
172
163
  return { contributions: resolved.contributions, issues: [...validations.flatMap((validation) => validation.issues.map((issue) => contributorIssue(validation.plugin, issue))), ...resolved.issues] };
173
164
  }
174
- export async function hydrateAbsentHttpPlugins() {
175
- let hydratedAny = false;
176
- const diagnostics = [];
177
- for (const validation of discoverPluginCommandCandidates()) {
178
- if (validation.transport?.kind !== 'http')
179
- continue;
180
- const unsafePath = validation.issues.find((issue) => issue.code === 'command_path_unsafe');
181
- if (unsafePath !== undefined) {
182
- diagnostics.push(`HTTP plugin "${validation.plugin.name}": ${unsafePath.code}: ${unsafePath.message} — ${unsafePath.next}`);
183
- continue;
184
- }
185
- if (!validation.absentStore || validation.httpManifestStore === undefined)
186
- continue;
187
- try {
188
- const result = await fetchHttpPluginManifest({ name: validation.plugin.name, endpoint: validation.transport.endpoint, ...(validation.transport.authEnv !== undefined ? { authEnv: validation.transport.authEnv } : {}) });
189
- if (result.status === 200) {
190
- writeHttpPluginManifest(validation.httpManifestStore, result.raw);
191
- hydratedAny = true;
192
- }
193
- else
194
- diagnostics.push(`HTTP plugin "${validation.plugin.name}": ${result.message} — run \`crtr pkg plugin update --name ${validation.plugin.name}\``);
195
- }
196
- catch (error) {
197
- diagnostics.push(`HTTP plugin "${validation.plugin.name}": ${error.message} — run \`crtr pkg plugin update --name ${validation.plugin.name}\``);
198
- }
199
- }
200
- return { hydratedAny, diagnostics };
201
- }
@@ -1,42 +1,16 @@
1
1
  import type { HttpPluginFetchTarget } from '../endpoint.js';
2
- import type { HttpPluginManifestStore } from '../store.js';
3
- /** Result of a successful manifest fetch. */
2
+ /** Exact archive bytes returned by a successful authenticated bundle fetch. */
4
3
  export interface FetchSuccess {
5
4
  status: 200;
6
5
  raw: Uint8Array;
7
6
  }
8
- /** Typed failures from manifest fetch. */
9
7
  export type FetchResult = FetchSuccess | FetchFailure;
10
8
  export interface FetchFailure {
11
9
  status: 'auth_env_missing' | 'cli_unreachable' | 'cli_protocol_error';
12
10
  message: string;
13
11
  }
14
12
  /**
15
- * Fetch a HTTP-transport plugin manifest from the registered endpoint.
16
- *
17
- * Implements the core authenticated GET primitive for T0/T2/T4 fetches:
18
- * - Reads bearer token from `authEnv` env var at invocation time
19
- * - Fails with `auth_env_missing` USAGE before network if env var is named but empty
20
- * - Performs one GET request with `Accept: application/json` header
21
- * - Attaches `Authorization: Bearer <token>` header when auth var is configured
22
- * - A single 10-second socket-inactivity timeout (Node's `timeout` option) covers
23
- * the whole request — there is no separate connect clock and no 30s total cap;
24
- * that two-clock model belongs to the invoker's per-leaf HTTP calls (§6.6), not
25
- * this simpler manifest-fetch primitive.
26
- * - Returns exact response bytes on 2xx success
27
- * - Returns typed `cli_unreachable` NETWORK on transport/timeout failures
28
- * - Returns typed `cli_protocol_error` GENERAL on non-2xx without parseable error body
29
- *
30
- * No retries, no conditional request (ETag/If-None-Match), no 304 handling.
13
+ * One authenticated GET for a plugin directory archive. It has one ten-second
14
+ * inactivity timeout and never retries or conditionally revalidates.
31
15
  */
32
- export declare function fetchHttpPluginManifest(registration: HttpPluginFetchTarget): Promise<FetchResult>;
33
- /**
34
- * Explicit refresh: unconditional fetch and replace stored manifest.
35
- *
36
- * Called when user runs `crtr pkg plugin update <name>`. Always fetches
37
- * regardless of stored-manifest state, and replaces it on success.
38
- * On failure, preserves existing manifest bytes and throws CrtrError.
39
- *
40
- * Returns the raw manifest bytes on success.
41
- */
42
- export declare function refreshHttpPlugin(target: HttpPluginFetchTarget, store: HttpPluginManifestStore): Promise<Uint8Array>;
16
+ export declare function fetchHttpPluginBundle(registration: HttpPluginFetchTarget): Promise<FetchResult>;
@@ -1,31 +1,11 @@
1
1
  import { URL } from 'node:url';
2
2
  import { request as httpsRequest } from 'node:https';
3
3
  import { request as httpRequest } from 'node:http';
4
- import { writeHttpPluginManifest } from '../store.js';
5
- import { general, network, usage } from '../../errors.js';
6
- // ---------------------------------------------------------------------------
7
- // Core fetch primitive
8
- // ---------------------------------------------------------------------------
9
4
  /**
10
- * Fetch a HTTP-transport plugin manifest from the registered endpoint.
11
- *
12
- * Implements the core authenticated GET primitive for T0/T2/T4 fetches:
13
- * - Reads bearer token from `authEnv` env var at invocation time
14
- * - Fails with `auth_env_missing` USAGE before network if env var is named but empty
15
- * - Performs one GET request with `Accept: application/json` header
16
- * - Attaches `Authorization: Bearer <token>` header when auth var is configured
17
- * - A single 10-second socket-inactivity timeout (Node's `timeout` option) covers
18
- * the whole request — there is no separate connect clock and no 30s total cap;
19
- * that two-clock model belongs to the invoker's per-leaf HTTP calls (§6.6), not
20
- * this simpler manifest-fetch primitive.
21
- * - Returns exact response bytes on 2xx success
22
- * - Returns typed `cli_unreachable` NETWORK on transport/timeout failures
23
- * - Returns typed `cli_protocol_error` GENERAL on non-2xx without parseable error body
24
- *
25
- * No retries, no conditional request (ETag/If-None-Match), no 304 handling.
5
+ * One authenticated GET for a plugin directory archive. It has one ten-second
6
+ * inactivity timeout and never retries or conditionally revalidates.
26
7
  */
27
- export async function fetchHttpPluginManifest(registration) {
28
- // Check auth env var and fail before network if named but empty
8
+ export async function fetchHttpPluginBundle(registration) {
29
9
  let token;
30
10
  if (registration.authEnv) {
31
11
  token = process.env[registration.authEnv];
@@ -36,101 +16,43 @@ export async function fetchHttpPluginManifest(registration) {
36
16
  };
37
17
  }
38
18
  }
39
- // Parse endpoint URL
40
19
  let url;
41
20
  try {
42
21
  url = new URL(registration.endpoint);
43
22
  }
44
- catch (err) {
23
+ catch {
45
24
  return {
46
25
  status: 'cli_protocol_error',
47
26
  message: `Invalid endpoint URL: ${registration.endpoint}`,
48
27
  };
49
28
  }
50
- // Prepare request headers
51
- const headers = {
52
- 'Accept': 'application/json',
53
- };
54
- if (token) {
29
+ const headers = { Accept: 'application/x-tar' };
30
+ if (token)
55
31
  headers['Authorization'] = `Bearer ${token}`;
56
- }
57
- // Perform the GET request
58
32
  return new Promise((resolve) => {
59
33
  const request = url.protocol === 'https:' ? httpsRequest : httpRequest;
60
- const options = {
61
- method: 'GET',
62
- headers,
63
- timeout: 10_000, // 10 second manifest fetch timeout per spec §6.6
64
- };
65
- const req = request(url, options, (res) => {
34
+ const req = request(url, { method: 'GET', headers, timeout: 10_000 }, (res) => {
66
35
  let body = Buffer.alloc(0);
67
- res.on('data', (chunk) => {
68
- body = Buffer.concat([body, chunk]);
69
- });
36
+ res.on('data', (chunk) => { body = Buffer.concat([body, chunk]); });
70
37
  res.on('end', () => {
71
38
  if (!res.statusCode) {
72
- resolve({
73
- status: 'cli_protocol_error',
74
- message: 'No HTTP status code received.',
75
- });
76
- return;
39
+ resolve({ status: 'cli_protocol_error', message: 'No HTTP status code received.' });
77
40
  }
78
- // Success: 2xx status
79
- if (res.statusCode >= 200 && res.statusCode < 300) {
80
- resolve({
81
- status: 200,
82
- raw: new Uint8Array(body),
83
- });
84
- return;
41
+ else if (res.statusCode >= 200 && res.statusCode < 300) {
42
+ resolve({ status: 200, raw: new Uint8Array(body) });
43
+ }
44
+ else {
45
+ resolve({ status: 'cli_protocol_error', message: `HTTP ${res.statusCode} from ${registration.endpoint}` });
85
46
  }
86
- // Non-2xx failure
87
- resolve({
88
- status: 'cli_protocol_error',
89
- message: `HTTP ${res.statusCode} from ${registration.endpoint}`,
90
- });
91
47
  });
92
48
  });
93
49
  req.on('timeout', () => {
94
50
  req.destroy();
95
- resolve({
96
- status: 'cli_unreachable',
97
- message: `Request timeout (10s) fetching ${registration.endpoint}`,
98
- });
51
+ resolve({ status: 'cli_unreachable', message: `Request timeout (10s) fetching ${registration.endpoint}` });
99
52
  });
100
- req.on('error', (err) => {
101
- resolve({
102
- status: 'cli_unreachable',
103
- message: `Network error fetching ${registration.endpoint}: ${err.message}`,
104
- });
53
+ req.on('error', (error) => {
54
+ resolve({ status: 'cli_unreachable', message: `Network error fetching ${registration.endpoint}: ${error.message}` });
105
55
  });
106
56
  req.end();
107
57
  });
108
58
  }
109
- // ---------------------------------------------------------------------------
110
- // Store-integrated helpers
111
- // ---------------------------------------------------------------------------
112
- /**
113
- * Explicit refresh: unconditional fetch and replace stored manifest.
114
- *
115
- * Called when user runs `crtr pkg plugin update <name>`. Always fetches
116
- * regardless of stored-manifest state, and replaces it on success.
117
- * On failure, preserves existing manifest bytes and throws CrtrError.
118
- *
119
- * Returns the raw manifest bytes on success.
120
- */
121
- export async function refreshHttpPlugin(target, store) {
122
- // Unconditional fetch
123
- const result = await fetchHttpPluginManifest(target);
124
- if (result.status !== 200) {
125
- if (result.status === 'auth_env_missing') {
126
- throw usage(result.message);
127
- }
128
- if (result.status === 'cli_unreachable') {
129
- throw network(result.message);
130
- }
131
- throw general(result.message);
132
- }
133
- // Write to the resolved HTTP manifest store.
134
- writeHttpPluginManifest(store, result.raw);
135
- return result.raw;
136
- }
@@ -12,6 +12,11 @@ export interface LeafRunContext {
12
12
  * as a positional argument, or explicitly passed; absence means TTY with no
13
13
  * pipe (truly absent). */
14
14
  readonly providedParams: ReadonlySet<string>;
15
+ /** Absolute source paths for JSON values supplied through `--context-file`,
16
+ * keyed by their camel-cased parameter name. The parsed JSON remains the
17
+ * leaf input; this metadata preserves its source directory for values that
18
+ * reference adjacent files. */
19
+ readonly contextFilePaths: ReadonlyMap<string, string>;
15
20
  }
16
21
  /** Opt-in flag that surfaces a node as an editor slash command (a pi prompt
17
22
  * template / Claude Code command). When set, the bootstrap auto-writes a
@@ -149,6 +154,9 @@ export interface ParseArgvOptions {
149
154
  * Called once with a ReadonlySet of parameter names that were supplied by
150
155
  * the caller (distinct from defaulted or missing values). */
151
156
  onProvidedParams?: (provided: ReadonlySet<string>) => void;
157
+ /** Optional collector for absolute source paths of JSON values supplied via
158
+ * `--context-file`, keyed by their camel-cased parameter name. */
159
+ onContextFilePaths?: (paths: ReadonlyMap<string, string>) => void;
152
160
  /** The walked command path (e.g. `['push','final']`), supplied by the
153
161
  * dispatcher. Used only to name the exact leaf in a schema-class error's
154
162
  * `Next:` line, which mandates reading that leaf's `-h`. */
@@ -158,17 +166,5 @@ export interface ParseArgvOptions {
158
166
  * Returns a plain object whose keys are camelCase parameter names.
159
167
  * Optionally tracks which parameters were explicitly provided via a callback. */
160
168
  export declare function parseArgv(params: InputParam[], tokens: string[], options?: ParseArgvOptions): Promise<Record<string, unknown>>;
161
- /** Optional seams for `runCli`. */
162
- export interface RunCliOptions {
163
- /** The one unknown-first-token retry seam. Called exactly once only when an
164
- * unknown first token misses at root (walk stopped at root, `path` empty,
165
- * on a real non-help token), where absent HTTP-plugin stores may hydrate.
166
- * A deeper unknown token under a known command (`path` non-empty) never
167
- * calls it. It returns a freshly rebuilt root when hydration produced new
168
- * commands to re-walk the original argv against, otherwise undefined to
169
- * surface the plain `unknown_path` error. crtr's core fast path passes no
170
- * options, so a recognized core first token has zero HTTP-plugin I/O. */
171
- onUnknownFirstTokenMiss?: () => Promise<RootDef | undefined>;
172
- }
173
- export declare function runCli(root: RootDef, argv: string[], options?: RunCliOptions): Promise<void>;
169
+ export declare function runCli(root: RootDef, argv: string[]): Promise<void>;
174
170
  export {};