@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.
- package/dist/build-root.d.ts +1 -16
- package/dist/build-root.js +1 -25
- package/dist/builtin-memory/insights/capture.md +66 -0
- package/dist/builtin-memory/insights/init.md +45 -0
- package/dist/builtin-memory/internal/plugins.md +12 -11
- package/dist/cli.js +2 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -1
- package/dist/clients/attach/__tests__/context-message.test.js +50 -3
- package/dist/clients/attach/__tests__/diagram.test.js +1 -1
- package/dist/clients/attach/chrome/header.d.ts +10 -0
- package/dist/clients/attach/chrome/header.js +35 -0
- package/dist/clients/attach/chrome/status-line.d.ts +3 -0
- package/dist/clients/attach/chrome/status-line.js +5 -0
- package/dist/clients/attach/chrome/widgets.js +7 -18
- package/dist/clients/attach/input/controller.d.ts +10 -0
- package/dist/clients/attach/input/controller.js +3 -0
- package/dist/clients/attach/overlays/help.d.ts +33 -0
- package/dist/clients/attach/overlays/help.js +204 -0
- package/dist/clients/attach/render/chat-view.d.ts +81 -9
- package/dist/clients/attach/render/chat-view.js +434 -90
- package/dist/clients/attach/render/context-message.js +3 -3
- package/dist/clients/attach/render/diagram.js +2 -2
- package/dist/clients/attach/render/measured-container.d.ts +94 -0
- package/dist/clients/attach/render/measured-container.js +307 -0
- package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
- package/dist/clients/attach/render/transcript-copy.js +81 -0
- package/dist/clients/attach/render/viewport.d.ts +29 -0
- package/dist/clients/attach/render/viewport.js +160 -0
- package/dist/clients/attach/session/bindings.d.ts +5 -1
- package/dist/clients/attach/session/bindings.js +16 -0
- package/dist/clients/attach/session/context.d.ts +5 -3
- package/dist/clients/attach/session/frame.d.ts +34 -0
- package/dist/clients/attach/session/frame.js +173 -0
- package/dist/clients/attach/session/frames.d.ts +2 -0
- package/dist/clients/attach/session/frames.js +3 -0
- package/dist/clients/attach/session/input-wiring.d.ts +10 -4
- package/dist/clients/attach/session/input-wiring.js +13 -13
- package/dist/clients/attach/session/keys.d.ts +4 -1
- package/dist/clients/attach/session/keys.js +9 -4
- package/dist/clients/attach/session/layout.d.ts +10 -8
- package/dist/clients/attach/session/layout.js +32 -31
- package/dist/clients/attach/session/mouse.d.ts +15 -0
- package/dist/clients/attach/session/mouse.js +61 -0
- package/dist/clients/attach/slash/dispatch.d.ts +10 -0
- package/dist/clients/attach/slash/dispatch.js +35 -2
- package/dist/clients/attach/viewer.js +605 -581
- package/dist/clients/web/dev-server.d.ts +4 -1
- package/dist/clients/web/dev-server.js +8 -19
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/human/prompts.js +16 -6
- package/dist/commands/pkg/plugin-manage.js +143 -68
- package/dist/commands/sys/__tests__/setup-core.test.js +4 -2
- package/dist/commands/sys/setup-core.js +21 -16
- package/dist/core/__tests__/human-deliver.test.js +62 -2
- package/dist/core/clipboard-text.d.ts +8 -0
- package/dist/core/clipboard-text.js +18 -0
- package/dist/core/command-manifests/registry.d.ts +1 -3
- package/dist/core/command-plugins/bundle.d.ts +29 -0
- package/dist/core/command-plugins/bundle.js +198 -0
- package/dist/core/command-plugins/discovery.d.ts +0 -8
- package/dist/core/command-plugins/discovery.js +2 -39
- package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
- package/dist/core/command-plugins/transport/http-fetch.js +17 -95
- package/dist/core/command.d.ts +9 -13
- package/dist/core/command.js +12 -17
- package/dist/core/config.js +2 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +4 -0
- package/dist/core/keybindings/attach-control.d.ts +3 -0
- package/dist/core/keybindings/attach-control.js +1 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +8 -0
- package/dist/core/runtime/broker-protocol.d.ts +13 -1
- package/dist/core/runtime/broker.js +213 -5
- package/dist/core/runtime/canvas-extensions.d.ts +3 -0
- package/dist/core/runtime/canvas-extensions.js +3 -0
- package/dist/core/runtime/front-door.js +4 -4
- package/dist/core/runtime/kickoff.d.ts +1 -5
- package/dist/core/runtime/kickoff.js +1 -5
- package/dist/core/runtime/naming.d.ts +7 -0
- package/dist/core/runtime/naming.js +5 -2
- package/dist/core/runtime/node-read.js +15 -0
- package/dist/core/runtime/package-health.d.ts +6 -21
- package/dist/core/runtime/package-health.js +12 -123
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +3 -3
- package/dist/core/runtime/tool-group-summary.d.ts +19 -0
- package/dist/core/runtime/tool-group-summary.js +117 -0
- package/dist/core/termrender/code-doc.d.ts +12 -0
- package/dist/core/termrender/code-doc.js +91 -0
- package/dist/core/termrender/display.d.ts +12 -0
- package/dist/core/termrender/display.js +19 -0
- package/dist/core/termrender/termrender.d.ts +73 -0
- package/dist/core/termrender/termrender.js +795 -0
- package/dist/core/termrender/version.d.ts +1 -0
- package/dist/core/termrender/version.js +1 -0
- package/dist/core/user-settings.d.ts +14 -0
- package/dist/core/user-settings.js +20 -0
- package/dist/daemon/crtrd.js +5 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
- package/dist/pi-extensions/canvas-stophook.js +2 -2
- package/dist/pi-extensions/canvas-structured-output.js +52 -8
- package/dist/pi-extensions/summary-tool.d.ts +33 -0
- package/dist/pi-extensions/summary-tool.js +62 -0
- package/dist/shared/generated-context.d.ts +19 -4
- package/dist/shared/generated-context.js +98 -7
- package/dist/shared/tool-groups.d.ts +23 -0
- package/dist/shared/tool-groups.js +60 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/web-client/assets/{index-U_NZ66VE.js → index-D7I3gHCL.js} +26 -19
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +3 -1
- package/runtime.lock.json +62 -3
- package/scripts/postinstall.mjs +14 -1
- package/dist/core/command-plugins/store.d.ts +0 -16
- package/dist/core/command-plugins/store.js +0 -64
- package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
- 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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
23
|
+
catch {
|
|
45
24
|
return {
|
|
46
25
|
status: 'cli_protocol_error',
|
|
47
26
|
message: `Invalid endpoint URL: ${registration.endpoint}`,
|
|
48
27
|
};
|
|
49
28
|
}
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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', (
|
|
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
|
-
}
|
package/dist/core/command.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {};
|