yam-flow 2.4.0 → 2.5.1
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/AGENTS.md +6 -4
- package/CHANGELOG.md +54 -0
- package/COMMANDS.md +13 -0
- package/README.md +53 -1
- package/ROADMAP.md +5 -1
- package/dist/bin/yam.js +104 -17
- package/dist/lib/bounded-input.d.ts +12 -0
- package/dist/lib/bounded-input.js +48 -0
- package/dist/lib/external-updates.d.ts +103 -0
- package/dist/lib/external-updates.js +1054 -0
- package/dist/lib/skill-installation.js +23 -5
- package/dist/lib/stable-order.d.ts +1 -0
- package/dist/lib/stable-order.js +5 -0
- package/dist/lib/ueye-artifacts.js +3 -2
- package/package.json +6 -3
- package/src/bin/yam.ts +109 -13
- package/src/lib/bounded-input.ts +59 -0
- package/src/lib/external-updates.ts +1220 -0
- package/src/lib/skill-installation.ts +24 -5
- package/src/lib/stable-order.ts +4 -0
- package/src/lib/ueye-artifacts.ts +3 -2
- package/yam.manifest.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { compareStableText } from './stable-order.js';
|
|
4
5
|
export const INSTALL_RECEIPT_NAME = '.yam-flow-install-receipt.json';
|
|
5
6
|
export const INSTALL_LOCK_NAME = '.yam-flow-install.lock';
|
|
6
7
|
export const INSTALL_RECEIPT_SCHEMA = 'yam.install-receipt.v1';
|
|
@@ -70,7 +71,7 @@ async function collectTreeManifest(root, prefix = '') {
|
|
|
70
71
|
const files = [];
|
|
71
72
|
async function walk(current, relative) {
|
|
72
73
|
const entries = await fsp.readdir(current, { withFileTypes: true });
|
|
73
|
-
entries.sort((left, right) => left.name
|
|
74
|
+
entries.sort((left, right) => compareStableText(left.name, right.name));
|
|
74
75
|
for (const entry of entries) {
|
|
75
76
|
const absolute = path.join(current, entry.name);
|
|
76
77
|
const nextRelative = relative ? path.join(relative, entry.name) : entry.name;
|
|
@@ -86,7 +87,7 @@ async function collectTreeManifest(root, prefix = '') {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
await walk(root, '');
|
|
89
|
-
return files.sort((left, right) => left.path
|
|
90
|
+
return files.sort((left, right) => compareStableText(left.path, right.path));
|
|
90
91
|
}
|
|
91
92
|
async function expectedInstallManifest(sourceRoot, skills) {
|
|
92
93
|
const referencesRoot = path.join(sourceRoot, 'references');
|
|
@@ -112,7 +113,7 @@ async function expectedInstallManifest(sourceRoot, skills) {
|
|
|
112
113
|
});
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
|
-
return files.sort((left, right) => left.path
|
|
116
|
+
return files.sort((left, right) => compareStableText(left.path, right.path));
|
|
116
117
|
}
|
|
117
118
|
async function copyTree(source, target) {
|
|
118
119
|
const stat = await fsp.lstat(source);
|
|
@@ -137,8 +138,11 @@ async function copyTree(source, target) {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
function manifestDigest(files) {
|
|
141
|
+
return digestManifestFiles([...files].sort((left, right) => compareStableText(left.path, right.path)));
|
|
142
|
+
}
|
|
143
|
+
function digestManifestFiles(files) {
|
|
140
144
|
const digest = createHash('sha256');
|
|
141
|
-
for (const file of
|
|
145
|
+
for (const file of files) {
|
|
142
146
|
digest.update(file.path);
|
|
143
147
|
digest.update('\0');
|
|
144
148
|
digest.update(String(file.bytes));
|
|
@@ -148,6 +152,20 @@ function manifestDigest(files) {
|
|
|
148
152
|
}
|
|
149
153
|
return digest.digest('hex');
|
|
150
154
|
}
|
|
155
|
+
function allowsLegacyManifestOrder(version) {
|
|
156
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(String(version || ''));
|
|
157
|
+
if (!match)
|
|
158
|
+
return false;
|
|
159
|
+
const major = Number(match[1]);
|
|
160
|
+
const minor = Number(match[2]);
|
|
161
|
+
const patch = Number(match[3]);
|
|
162
|
+
return major < 2 || (major === 2 && (minor < 5 || (minor === 5 && patch === 0)));
|
|
163
|
+
}
|
|
164
|
+
function receiptManifestDigestMatches(files, digest, version) {
|
|
165
|
+
if (digest === manifestDigest(files))
|
|
166
|
+
return true;
|
|
167
|
+
return allowsLegacyManifestOrder(version) && digest === digestManifestFiles(files);
|
|
168
|
+
}
|
|
151
169
|
function manifestIssues(expected, actual, label) {
|
|
152
170
|
const issues = [];
|
|
153
171
|
const expectedByPath = new Map(expected.map((file) => [file.path, file]));
|
|
@@ -236,7 +254,7 @@ function ownershipReceiptIssues(receipt, packageName, destination) {
|
|
|
236
254
|
if (value.integrity?.file_count !== files.length) {
|
|
237
255
|
issues.push('install receipt file count does not match its manifest');
|
|
238
256
|
}
|
|
239
|
-
if (value.integrity?.source_digest
|
|
257
|
+
if (!receiptManifestDigestMatches(files, value.integrity?.source_digest, value.package?.version)) {
|
|
240
258
|
issues.push('install receipt source digest does not match its manifest');
|
|
241
259
|
}
|
|
242
260
|
if (Array.isArray(value.skills)) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function compareStableText(left: string, right: string): number;
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { constants as fsConstants } from 'node:fs';
|
|
3
3
|
import fsp from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { compareStableText } from './stable-order.js';
|
|
5
6
|
export async function inspectImageFile(file) {
|
|
6
7
|
const absolute = path.resolve(file);
|
|
7
8
|
const buffer = await fsp.readFile(absolute);
|
|
@@ -46,7 +47,7 @@ export async function upsertUeyeAsset(input) {
|
|
|
46
47
|
manifest.assets[existingIndex] = entry;
|
|
47
48
|
else
|
|
48
49
|
manifest.assets.push(entry);
|
|
49
|
-
manifest.assets.sort((left, right) => left.id
|
|
50
|
+
manifest.assets.sort((left, right) => compareStableText(left.id, right.id));
|
|
50
51
|
manifest.updated_at = new Date().toISOString();
|
|
51
52
|
await writeJsonAtomic(manifestPath, manifest);
|
|
52
53
|
const warnings = assetWarnings(entry);
|
|
@@ -152,7 +153,7 @@ export async function archiveUeyeRevision(input) {
|
|
|
152
153
|
else {
|
|
153
154
|
history.revisions.push(entry);
|
|
154
155
|
}
|
|
155
|
-
history.revisions.sort((left, right) => left.round - right.round || left.artifact_id
|
|
156
|
+
history.revisions.sort((left, right) => left.round - right.round || compareStableText(left.artifact_id, right.artifact_id));
|
|
156
157
|
history.updated_at = new Date().toISOString();
|
|
157
158
|
await writeJsonAtomic(manifestPath, history);
|
|
158
159
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yam-flow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"description": "Progressive proof-first agent flow: start fast, deepen deliberately, stay honest by design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "0kim0bos",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
],
|
|
44
44
|
"scripts": {
|
|
45
45
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
46
|
-
"build": "npm run clean && tsc -p tsconfig.json",
|
|
46
|
+
"build": "npm run clean && tsc -p tsconfig.json && node -e \"require('node:fs').chmodSync('dist/bin/yam.js',0o755)\"",
|
|
47
47
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
48
|
"install:skills": "node ./dist/bin/yam.js install",
|
|
49
49
|
"uninstall:skills": "node ./dist/bin/yam.js uninstall",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"doctor": "node ./dist/bin/yam.js doctor",
|
|
53
53
|
"list": "node ./dist/bin/yam.js list",
|
|
54
54
|
"examples": "node ./dist/bin/yam.js examples",
|
|
55
|
-
"verify:self": "npm run build && node --check ./dist/bin/yam.js && node --check ./dist/lib/trust-kernel.js && node --check ./dist/lib/skill-installation.js && npm run hook-latency-smoke && node ./scripts/install-transaction-smoke.mjs && node ./dist/bin/yam.js verify && node ./dist/bin/yam.js doctor",
|
|
55
|
+
"verify:self": "npm run build && node --check ./dist/bin/yam.js && node --check ./dist/lib/trust-kernel.js && node --check ./dist/lib/skill-installation.js && node --check ./dist/lib/external-updates.js && npm run hook-latency-smoke && node ./scripts/external-updates-smoke.mjs && node ./scripts/install-transaction-smoke.mjs && npm run package-secrets:check && npm run package-secrets-smoke && node ./dist/bin/yam.js verify && node ./dist/bin/yam.js doctor",
|
|
56
56
|
"prepare": "npm run build",
|
|
57
57
|
"prepack": "npm run verify:self",
|
|
58
58
|
"prepublishOnly": "npm run verify:self",
|
|
@@ -61,9 +61,12 @@
|
|
|
61
61
|
"forbidden-names:check": "node ./scripts/check-forbidden-names.mjs",
|
|
62
62
|
"study-note:check": "node ./dist/bin/yam.js study-note check --json",
|
|
63
63
|
"package-boundary:check": "node ./scripts/check-package-boundary.mjs",
|
|
64
|
+
"package-secrets:check": "node ./scripts/check-package-secrets.mjs",
|
|
65
|
+
"package-secrets-smoke": "node ./scripts/package-secrets-smoke.mjs",
|
|
64
66
|
"dist:freshness": "node ./scripts/check-dist-freshness.mjs",
|
|
65
67
|
"registry:check": "node ./scripts/check-registry-version.mjs",
|
|
66
68
|
"cli-smoke": "node ./scripts/cli-smoke.mjs",
|
|
69
|
+
"external-updates-smoke": "npm run build && node ./scripts/external-updates-smoke.mjs",
|
|
67
70
|
"hook-latency-smoke": "node ./scripts/hook-latency-smoke.mjs",
|
|
68
71
|
"install-transaction-smoke": "npm run build && node ./scripts/install-transaction-smoke.mjs",
|
|
69
72
|
"release:check": "npm run typecheck && npm run forbidden-names:check && npm run verify:self && npm run dist:freshness && npm run package-boundary:check && npm run registry:check && npm run cli-smoke",
|
package/src/bin/yam.ts
CHANGED
|
@@ -39,6 +39,19 @@ import {
|
|
|
39
39
|
installSkillSetTransactional,
|
|
40
40
|
uninstallSkillSetSafely
|
|
41
41
|
} from '../lib/skill-installation.js';
|
|
42
|
+
import {
|
|
43
|
+
EXTERNAL_UPDATE_COMPONENTS,
|
|
44
|
+
applyExternalUpdates,
|
|
45
|
+
checkExternalUpdates
|
|
46
|
+
} from '../lib/external-updates.js';
|
|
47
|
+
import type { ExternalUpdateComponent } from '../lib/external-updates.js';
|
|
48
|
+
import {
|
|
49
|
+
BoundedInputError,
|
|
50
|
+
GENERAL_STDIN_MAX_BYTES,
|
|
51
|
+
HOOK_STDIN_MAX_BYTES,
|
|
52
|
+
readBoundedStdinJson,
|
|
53
|
+
readBoundedStdinText
|
|
54
|
+
} from '../lib/bounded-input.js';
|
|
42
55
|
|
|
43
56
|
type AnyRecord = Record<string, any>;
|
|
44
57
|
|
|
@@ -180,6 +193,9 @@ Usage:
|
|
|
180
193
|
yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
|
|
181
194
|
yam mission receipt [--thread-id id] [--role reviewer] [--lifecycle stopped] [--outcome passed] [--evidence text] [--json]
|
|
182
195
|
yam mission gate [--expected-thread id] [--receipt file] [--json]
|
|
196
|
+
yam update check [--json]
|
|
197
|
+
yam update apply --component yam|scrapling|insane-search [--json]
|
|
198
|
+
yam update apply --all [--json]
|
|
183
199
|
yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
|
|
184
200
|
yam release report [--json]
|
|
185
201
|
yam safety [text...]
|
|
@@ -2013,7 +2029,18 @@ async function hookRun(args = []) {
|
|
|
2013
2029
|
console.log(JSON.stringify({ continue: true }));
|
|
2014
2030
|
return;
|
|
2015
2031
|
}
|
|
2016
|
-
|
|
2032
|
+
let input: AnyRecord;
|
|
2033
|
+
try {
|
|
2034
|
+
input = await readStdinJson();
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
if (!(error instanceof BoundedInputError)) throw error;
|
|
2037
|
+
const code = error.code === 'invalid_json' ? 'invalid_hook_json' : error.code;
|
|
2038
|
+
console.log(JSON.stringify({
|
|
2039
|
+
continue: true,
|
|
2040
|
+
systemMessage: `yam-${profile} hook input rejected: ${code}; continuing without using or echoing the payload.`
|
|
2041
|
+
}));
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2017
2044
|
const event = input?.hook_event_name || input?.hookEventName || input?.event || 'UserPromptSubmit';
|
|
2018
2045
|
const cwd = String(input?.cwd || process.cwd());
|
|
2019
2046
|
if (profile === 'study-note' && event === 'Stop') {
|
|
@@ -2035,15 +2062,7 @@ async function hookRun(args = []) {
|
|
|
2035
2062
|
}
|
|
2036
2063
|
|
|
2037
2064
|
async function readStdinJson() {
|
|
2038
|
-
|
|
2039
|
-
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
2040
|
-
const text = Buffer.concat(chunks).toString('utf8').trim();
|
|
2041
|
-
if (!text) return {};
|
|
2042
|
-
try {
|
|
2043
|
-
return JSON.parse(text);
|
|
2044
|
-
} catch {
|
|
2045
|
-
return {};
|
|
2046
|
-
}
|
|
2065
|
+
return readBoundedStdinJson(process.stdin, HOOK_STDIN_MAX_BYTES);
|
|
2047
2066
|
}
|
|
2048
2067
|
|
|
2049
2068
|
function extractPrompt(input: AnyRecord = {}) {
|
|
@@ -2676,9 +2695,7 @@ function redactSensitiveText(text = '') {
|
|
|
2676
2695
|
|
|
2677
2696
|
async function readStdinTextIfAvailable() {
|
|
2678
2697
|
if (process.stdin.isTTY) return '';
|
|
2679
|
-
|
|
2680
|
-
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
2681
|
-
return Buffer.concat(chunks).toString('utf8').trim();
|
|
2698
|
+
return (await readBoundedStdinText(process.stdin, GENERAL_STDIN_MAX_BYTES)).trim();
|
|
2682
2699
|
}
|
|
2683
2700
|
|
|
2684
2701
|
async function loop(args = []) {
|
|
@@ -3564,6 +3581,83 @@ async function mission(args = []) {
|
|
|
3564
3581
|
return missionUsage();
|
|
3565
3582
|
}
|
|
3566
3583
|
|
|
3584
|
+
async function updateCommand(args: string[] = []) {
|
|
3585
|
+
const subcommand = args[0] || 'help';
|
|
3586
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return updateUsage();
|
|
3587
|
+
if (subcommand === 'check') {
|
|
3588
|
+
const rest = args.slice(1);
|
|
3589
|
+
const invalid = rest.filter((arg) => arg !== '--json');
|
|
3590
|
+
if (invalid.length || rest.filter((arg) => arg === '--json').length > 1) {
|
|
3591
|
+
throw new Error(`invalid update check option: ${invalid[0] || '--json repeated'}`);
|
|
3592
|
+
}
|
|
3593
|
+
const report = await checkExternalUpdates(VERSION);
|
|
3594
|
+
printJsonOrHuman(report, rest.includes('--json'), 'External update check');
|
|
3595
|
+
if (!report.success) process.exitCode = 1;
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
if (subcommand !== 'apply') {
|
|
3599
|
+
throw new Error(`unknown update command: ${subcommand}`);
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
const parsed = parseUpdateApplyArgs(args.slice(1));
|
|
3603
|
+
const report = await applyExternalUpdates(VERSION, {
|
|
3604
|
+
component: parsed.component,
|
|
3605
|
+
all: parsed.all
|
|
3606
|
+
});
|
|
3607
|
+
printJsonOrHuman(report, parsed.json, 'External update apply');
|
|
3608
|
+
if (!report.success) process.exitCode = 1;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
function parseUpdateApplyArgs(args: string[]) {
|
|
3612
|
+
let component: ExternalUpdateComponent | undefined;
|
|
3613
|
+
let all = false;
|
|
3614
|
+
let json = false;
|
|
3615
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3616
|
+
const arg = String(args[index] || '');
|
|
3617
|
+
if (arg === '--json') {
|
|
3618
|
+
if (json) throw new Error('--json may be supplied only once');
|
|
3619
|
+
json = true;
|
|
3620
|
+
continue;
|
|
3621
|
+
}
|
|
3622
|
+
if (arg === '--all') {
|
|
3623
|
+
if (all) throw new Error('--all may be supplied only once');
|
|
3624
|
+
all = true;
|
|
3625
|
+
continue;
|
|
3626
|
+
}
|
|
3627
|
+
if (arg === '--component' || arg.startsWith('--component=')) {
|
|
3628
|
+
if (component) throw new Error('--component may be supplied only once');
|
|
3629
|
+
const value = arg === '--component' ? String(args[index + 1] || '') : arg.slice('--component='.length);
|
|
3630
|
+
if (arg === '--component') index += 1;
|
|
3631
|
+
if (!EXTERNAL_UPDATE_COMPONENTS.includes(value as ExternalUpdateComponent)) {
|
|
3632
|
+
throw new Error('component must be one of: yam, scrapling, insane-search');
|
|
3633
|
+
}
|
|
3634
|
+
component = value as ExternalUpdateComponent;
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
throw new Error(`invalid update apply option: ${arg}`);
|
|
3638
|
+
}
|
|
3639
|
+
if ((all && component) || (!all && !component)) {
|
|
3640
|
+
throw new Error('choose exactly one of --component yam|scrapling|insane-search or --all');
|
|
3641
|
+
}
|
|
3642
|
+
return { component, all, json };
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
function updateUsage() {
|
|
3646
|
+
console.log(`yam update
|
|
3647
|
+
|
|
3648
|
+
Usage:
|
|
3649
|
+
yam update check [--json]
|
|
3650
|
+
yam update apply --component yam|scrapling|insane-search [--json]
|
|
3651
|
+
yam update apply --all [--json]
|
|
3652
|
+
|
|
3653
|
+
Safety:
|
|
3654
|
+
check is read-only. apply requires an explicit component or --all.
|
|
3655
|
+
--all updates Scrapling and Insane Search before yam, then stops at the first failed/manual component.
|
|
3656
|
+
Insane Search is updated only through the official Codex CLI; plugin caches are never edited directly.
|
|
3657
|
+
Every apply attempt records component checks, outcome, side effects, and rollback guidance.
|
|
3658
|
+
`);
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3567
3661
|
function missionUsage() {
|
|
3568
3662
|
console.log(`yam mission
|
|
3569
3663
|
|
|
@@ -5332,6 +5426,7 @@ function showRequestedHelp(args: string[]) {
|
|
|
5332
5426
|
media: mediaUsage,
|
|
5333
5427
|
runtime: runtimeUsage,
|
|
5334
5428
|
mission: missionUsage,
|
|
5429
|
+
update: updateUsage,
|
|
5335
5430
|
benchmark: benchmarkUsage,
|
|
5336
5431
|
memory: memoryUsage
|
|
5337
5432
|
};
|
|
@@ -5361,6 +5456,7 @@ async function main() {
|
|
|
5361
5456
|
if (command === 'media') return media(process.argv.slice(3));
|
|
5362
5457
|
if (command === 'runtime') return runtime(process.argv.slice(3));
|
|
5363
5458
|
if (command === 'mission') return mission(process.argv.slice(3));
|
|
5459
|
+
if (command === 'update') return updateCommand(process.argv.slice(3));
|
|
5364
5460
|
if (command === 'benchmark') return benchmark(process.argv.slice(3));
|
|
5365
5461
|
if (command === 'release') return release(process.argv.slice(3));
|
|
5366
5462
|
if (command === 'safety') return safety(process.argv.slice(3));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const HOOK_STDIN_MAX_BYTES = 1024 * 1024;
|
|
2
|
+
export const GENERAL_STDIN_MAX_BYTES = 4 * 1024 * 1024;
|
|
3
|
+
|
|
4
|
+
export type BoundedInputErrorCode = 'input_too_large' | 'invalid_json';
|
|
5
|
+
|
|
6
|
+
export class BoundedInputError extends Error {
|
|
7
|
+
readonly code: BoundedInputErrorCode;
|
|
8
|
+
readonly maxBytes?: number;
|
|
9
|
+
|
|
10
|
+
constructor(code: BoundedInputErrorCode, options: { maxBytes?: number } = {}) {
|
|
11
|
+
const detail = code === 'input_too_large' && options.maxBytes
|
|
12
|
+
? `stdin input exceeds the ${options.maxBytes}-byte limit`
|
|
13
|
+
: 'stdin input is not a valid JSON object';
|
|
14
|
+
super(`[${code}] ${detail}`);
|
|
15
|
+
this.name = 'BoundedInputError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.maxBytes = options.maxBytes;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function readBoundedStdinText(
|
|
22
|
+
input: NodeJS.ReadableStream & AsyncIterable<string | Buffer>,
|
|
23
|
+
maxBytes: number
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
|
|
26
|
+
throw new Error('stdin byte limit must be a positive safe integer');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const chunks: Buffer[] = [];
|
|
30
|
+
let totalBytes = 0;
|
|
31
|
+
for await (const chunk of input) {
|
|
32
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
33
|
+
totalBytes += bytes.byteLength;
|
|
34
|
+
if (totalBytes > maxBytes) {
|
|
35
|
+
throw new BoundedInputError('input_too_large', { maxBytes });
|
|
36
|
+
}
|
|
37
|
+
chunks.push(bytes);
|
|
38
|
+
}
|
|
39
|
+
return Buffer.concat(chunks, totalBytes).toString('utf8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function readBoundedStdinJson(
|
|
43
|
+
input: NodeJS.ReadableStream & AsyncIterable<string | Buffer>,
|
|
44
|
+
maxBytes: number
|
|
45
|
+
): Promise<Record<string, unknown>> {
|
|
46
|
+
const text = (await readBoundedStdinText(input, maxBytes)).trim();
|
|
47
|
+
if (!text) return {};
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const value = JSON.parse(text);
|
|
51
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
52
|
+
throw new BoundedInputError('invalid_json');
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof BoundedInputError) throw error;
|
|
57
|
+
throw new BoundedInputError('invalid_json');
|
|
58
|
+
}
|
|
59
|
+
}
|