@userreason/cli 0.2.0 → 0.2.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/README.md +12 -2
- package/dist/first-survey-source.d.ts +1 -0
- package/dist/first-survey-source.js +15 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/profile.js +1 -1
- package/dist/recommendation-source.d.ts +13 -0
- package/dist/recommendation-source.js +211 -0
- package/dist/setup-diagnostics.d.ts +2 -0
- package/dist/setup-diagnostics.js +26 -0
- package/dist/setup-executor.d.ts +8 -1
- package/dist/setup-executor.js +153 -132
- package/dist/setup-heartbeat.d.ts +268 -0
- package/dist/setup-heartbeat.js +120 -0
- package/dist/setup-runner.d.ts +3 -1
- package/dist/setup-runner.js +157 -108
- package/dist/setup-source.d.ts +3 -2
- package/dist/setup-source.js +6 -6
- package/dist/setup-store.d.ts +2 -2
- package/dist/setup-store.js +16 -6
- package/dist/setup-wire.d.ts +130 -0
- package/dist/setup-wire.js +22 -7
- package/dist/setup.d.ts +211 -11
- package/dist/setup.js +10 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -324,7 +324,17 @@ until tested: reachable configuration is not browser verification. Instrumentati
|
|
|
324
324
|
requires a reviewed, hash-guarded diff and separate edit approval using the agent
|
|
325
325
|
skill; unsupported host transforms are never executed automatically.
|
|
326
326
|
|
|
327
|
-
The 0.2.
|
|
328
|
-
`@userreason/cli@0.2.
|
|
327
|
+
The 0.2.1 production installation profile selects `@userreason/js@0.5.3` and
|
|
328
|
+
`@userreason/cli@0.2.1` exactly. Browser setup remains an explicit wizard entry
|
|
329
329
|
and requires an enabled compatible server; ordinary CLI authentication and
|
|
330
330
|
management commands retain their existing authorization flow.
|
|
331
|
+
|
|
332
|
+
Local setup execution failures retain bounded, redacted diagnostic text in the
|
|
333
|
+
private execution receipt. `onOperationError` displays it on failure and resume.
|
|
334
|
+
The detailed text never enters setup API requests or telemetry; the server still
|
|
335
|
+
receives only the typed outcome code.
|
|
336
|
+
|
|
337
|
+
A new run automatically recovers an existing project lock only after verifying
|
|
338
|
+
that its recorded process no longer exists. It still acquires the lock exclusively,
|
|
339
|
+
keeps all installation receipts for reconciliation, and refuses live, unreadable
|
|
340
|
+
or malformed locks. A live-lock error identifies the process to finish or stop.
|
|
@@ -43,6 +43,7 @@ export declare function collectFirstSurveySource(input: {
|
|
|
43
43
|
snapshotId?: string;
|
|
44
44
|
signal?: AbortSignal;
|
|
45
45
|
recommendations?: boolean;
|
|
46
|
+
benchmark?: boolean;
|
|
46
47
|
}): Promise<SourcePreview>;
|
|
47
48
|
/** Recheck every approved full-file hash before upload, edits or draft mutations. */
|
|
48
49
|
export declare function verifyFirstSurveySource(root: string, sources: Pick<LocalEvidence, 'path' | 'file_hash'>[]): Promise<void>;
|
|
@@ -4,6 +4,7 @@ import { lstat, realpath, readdir, open, readFile } from 'node:fs/promises';
|
|
|
4
4
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
6
|
import ignore from 'ignore';
|
|
7
|
+
import { connectedRecommendationSource } from './recommendation-source.js';
|
|
7
8
|
export const sourceHash = (value) => createHash('sha256').update(value).digest('hex');
|
|
8
9
|
const forbidden = /(^|\/)(?:\.git|\.env(?:\..*)?|node_modules|vendor|dist|build|coverage|\.next|\.nuxt|\.cache|\.output|public|uploads?|datasets?|fixtures?|__fixtures__|backups?|credentials?|secrets?)(\/|$)|(?:\.min\.[cm]?js|\.map|\.lock|\.(?:pem|key|p12|pfx|sqlite|db|csv|parquet|log|snap))$|(^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.npmrc|\.netrc|id_rsa|id_ed25519)$/i;
|
|
9
10
|
const sourceFile = /(?:\.(?:[cm]?[jt]sx?|vue|svelte|json|md))$/i;
|
|
@@ -30,10 +31,10 @@ export async function collectFirstSurveySource(input) {
|
|
|
30
31
|
const root = await realpath(input.root);
|
|
31
32
|
if (root !== input.root || !(await lstat(root)).isDirectory())
|
|
32
33
|
throw new Error('Application root must be a canonical directory, not a symlink');
|
|
33
|
-
const maxFiles = safeLimits(input.maxFiles, 200, 200);
|
|
34
|
-
const maxBytes = safeLimits(input.maxBytes, input.recommendations ? 2 * 1024 * 1024 : 500 * 1024, input.recommendations ? 2 * 1024 * 1024 : 500 * 1024);
|
|
34
|
+
const maxFiles = safeLimits(input.maxFiles, input.benchmark ? 1200 : 200, input.benchmark ? 1200 : 200);
|
|
35
|
+
const maxBytes = safeLimits(input.maxBytes, input.benchmark ? 12 * 1024 * 1024 : input.recommendations ? 2 * 1024 * 1024 : 500 * 1024, input.benchmark ? 12 * 1024 * 1024 : input.recommendations ? 2 * 1024 * 1024 : 500 * 1024);
|
|
35
36
|
// Conservative UTF-8 byte budget: at most 12,000 tokens even for non-English source.
|
|
36
|
-
const maxPacketBytes = safeLimits(input.maxPacketBytes, 10000, 10000);
|
|
37
|
+
const maxPacketBytes = safeLimits(input.maxPacketBytes, input.benchmark ? 262144 : 10000, input.benchmark ? 262144 : 10000);
|
|
37
38
|
const snapshot_id = input.snapshotId ?? randomUUID();
|
|
38
39
|
const omitted = [], evidence = [];
|
|
39
40
|
let inspected = 0, bytes = 0, packetBytes = 0, truncated = false, entries = 0;
|
|
@@ -59,7 +60,7 @@ export async function collectFirstSurveySource(input) {
|
|
|
59
60
|
const check = () => input.signal?.throwIfAborted();
|
|
60
61
|
async function walk(directory, inherited) {
|
|
61
62
|
check();
|
|
62
|
-
if (entries >= 2000 || inspected >= maxFiles || bytes >= maxBytes) {
|
|
63
|
+
if (entries >= (input.benchmark ? 20000 : 2000) || inspected >= maxFiles || bytes >= maxBytes) {
|
|
63
64
|
truncated = true;
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
@@ -81,13 +82,15 @@ export async function collectFirstSurveySource(input) {
|
|
|
81
82
|
});
|
|
82
83
|
for (const name of names) {
|
|
83
84
|
check();
|
|
84
|
-
if (++entries > 2000 || inspected >= maxFiles || bytes >= maxBytes) {
|
|
85
|
+
if (++entries > (input.benchmark ? 20000 : 2000) || inspected >= maxFiles || bytes >= maxBytes) {
|
|
85
86
|
truncated = true;
|
|
86
87
|
break;
|
|
87
88
|
}
|
|
88
89
|
const absolute = resolve(directory, name), path = relative(root, absolute).split(sep).join('/');
|
|
89
90
|
if (forbidden.test(path))
|
|
90
91
|
continue;
|
|
92
|
+
if (input.benchmark && /(?:^|\/)(?:scripts|tools|test|tests|__tests__|e2e|docs|examples|storybook|\.storybook)(?:\/|$)/i.test(path))
|
|
93
|
+
continue;
|
|
91
94
|
if (input.recommendations && /^(?:\.agents|\.cursor|\.codex)(?:\/|$)/.test(path))
|
|
92
95
|
continue;
|
|
93
96
|
const stat = await lstat(absolute);
|
|
@@ -175,7 +178,13 @@ export async function collectFirstSurveySource(input) {
|
|
|
175
178
|
}
|
|
176
179
|
}
|
|
177
180
|
await walk(root, [{ base: root, rules: rootIgnore }]);
|
|
178
|
-
if (input.
|
|
181
|
+
if (input.benchmark) {
|
|
182
|
+
for (const file of connectedRecommendationSource(candidates, maxPacketBytes)) {
|
|
183
|
+
evidence.push({ kind: 'local_source', id: `source_${evidence.length + 1}`, path: file.path, file_hash: sourceHash(candidates.find(c => c.path === file.path).text), excerpt_hash: sourceHash(file.text), snapshot_id, start_line: file.start, end_line: file.end, excerpt: file.text });
|
|
184
|
+
}
|
|
185
|
+
truncated = candidates.length > evidence.length || evidence.some(e => e.start_line !== 1 || e.excerpt !== candidates.find(c => c.path === e.path)?.text);
|
|
186
|
+
}
|
|
187
|
+
else if (input.recommendations) {
|
|
179
188
|
// Fairly cover product areas before taking more detail. Generated guidance,
|
|
180
189
|
// manifests and boilerplate cannot spend the product-evidence budget.
|
|
181
190
|
const rank = (p) => /\/(?:pages|screens|views)\//i.test('/' + p) ? 0 : /\/(?:app|routes?)\.[^.]+$/i.test(p) ? 1 : /components\//.test(p) ? 2 : 3;
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * from "./setup.js";
|
|
|
22
22
|
export * from "./setup-store.js";
|
|
23
23
|
export * from "./setup-wire.js";
|
|
24
24
|
export * from "./setup-executor.js";
|
|
25
|
+
export * from "./setup-diagnostics.js";
|
|
25
26
|
export * from "./setup-files.js";
|
|
26
27
|
export * from "./setup-terminal.js";
|
|
27
28
|
export * from "./setup-source.js";
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ export * from "./setup.js";
|
|
|
22
22
|
export * from "./setup-store.js";
|
|
23
23
|
export * from "./setup-wire.js";
|
|
24
24
|
export * from "./setup-executor.js";
|
|
25
|
+
export * from "./setup-diagnostics.js";
|
|
25
26
|
export * from "./setup-files.js";
|
|
26
27
|
export * from "./setup-terminal.js";
|
|
27
28
|
export * from "./setup-source.js";
|
package/dist/profile.js
CHANGED
|
@@ -4,7 +4,7 @@ export const PRODUCTION_DOCS = "https://docs.userreason.com";
|
|
|
4
4
|
export const DEVELOPMENT_DOCS = "https://docs-dev.userreason.com";
|
|
5
5
|
export const productionProfile = {
|
|
6
6
|
schemaVersion: 1, kind: "production", apiEnvironment: "production",
|
|
7
|
-
docsUrl: PRODUCTION_DOCS, sdkVersion: "0.5.
|
|
7
|
+
docsUrl: PRODUCTION_DOCS, sdkVersion: "0.5.3", cliVersion: "0.2.1",
|
|
8
8
|
};
|
|
9
9
|
export function parsePackageProfile(value) {
|
|
10
10
|
const data = object(value, "package profile");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type Source = {
|
|
2
|
+
path: string;
|
|
3
|
+
text: string;
|
|
4
|
+
};
|
|
5
|
+
type Window = Source & {
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
};
|
|
9
|
+
/** Same complete-file/whole-AST-unit policy as the benchmark. An oversized
|
|
10
|
+
* statement is omitted rather than cut in the middle to fill a byte quota. */
|
|
11
|
+
export declare function recommendationSourceWindows(file: Source, requiredHandlers?: string[]): Window[];
|
|
12
|
+
export declare function connectedRecommendationSource(candidates: Source[], maxPacketBytes?: number): Window[];
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** Benchmark structural source selection. The caller reads and screens files;
|
|
2
|
+
* this pure selector never executes code or expands the approved root. */
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { posix } from 'node:path';
|
|
5
|
+
const bytes = (text) => Buffer.byteLength(text);
|
|
6
|
+
const limits = { roots: 64, total: 164000, file: 26000, span: 16000, workflow: 38000, support: 8000, shared: 5000, excerpts: 200 };
|
|
7
|
+
function analyze(file) {
|
|
8
|
+
const ast = ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, true, /\.[cm]?jsx?$/.test(file.path) ? ts.ScriptKind.JSX : /\.tsx$/.test(file.path) ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
9
|
+
const imports = [], tags = new Set(), handlers = new Set(), templates = [];
|
|
10
|
+
const walk = (node) => {
|
|
11
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
12
|
+
const names = [], bindings = node.importClause?.namedBindings;
|
|
13
|
+
if (node.importClause?.name)
|
|
14
|
+
names.push(node.importClause.name.text);
|
|
15
|
+
if (bindings && ts.isNamedImports(bindings))
|
|
16
|
+
names.push(...bindings.elements.map(element => element.name.text));
|
|
17
|
+
imports.push({ specifier: node.moduleSpecifier.text, names });
|
|
18
|
+
}
|
|
19
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier))
|
|
20
|
+
imports.push({ specifier: node.moduleSpecifier.text, names: [] });
|
|
21
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node))
|
|
22
|
+
tags.add(node.tagName.getText(ast).split('.')[0]);
|
|
23
|
+
if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.getText(ast))) {
|
|
24
|
+
const ids = (child) => { if (ts.isIdentifier(child))
|
|
25
|
+
handlers.add(child.text); ts.forEachChild(child, ids); };
|
|
26
|
+
ts.forEachChild(node, ids);
|
|
27
|
+
}
|
|
28
|
+
if (ts.isPropertyAssignment(node) && node.name.getText(ast) === 'templateUrl' && ts.isStringLiteral(node.initializer))
|
|
29
|
+
templates.push(node.initializer.text);
|
|
30
|
+
ts.forEachChild(node, walk);
|
|
31
|
+
};
|
|
32
|
+
walk(ast);
|
|
33
|
+
if (/\.(?:vue|svelte|html)$/.test(file.path))
|
|
34
|
+
for (const match of file.text.matchAll(/(?:\([\w.:-]+\)|@[\w.:-]+)\s*=\s*["']([^"']+)["']/g))
|
|
35
|
+
for (const call of match[1].matchAll(/([A-Za-z_$][\w$]*)\s*\(/g))
|
|
36
|
+
handlers.add(call[1]);
|
|
37
|
+
return { ast, imports, rendered: new Set(imports.filter(item => item.names.some(name => tags.has(name))).map(item => item.specifier)), handlers, templates };
|
|
38
|
+
}
|
|
39
|
+
/** Same complete-file/whole-AST-unit policy as the benchmark. An oversized
|
|
40
|
+
* statement is omitted rather than cut in the middle to fill a byte quota. */
|
|
41
|
+
export function recommendationSourceWindows(file, requiredHandlers = []) {
|
|
42
|
+
const lines = file.text.split('\n');
|
|
43
|
+
const make = (start, end) => ({ path: file.path, start, end, text: lines.slice(start - 1, end).join('\n') });
|
|
44
|
+
if (bytes(file.text) <= limits.file)
|
|
45
|
+
return [make(1, lines.length)];
|
|
46
|
+
if (!/\.[cm]?[jt]sx?$/.test(file.path))
|
|
47
|
+
return [];
|
|
48
|
+
const a = analyze(file), units = [];
|
|
49
|
+
for (const handler of requiredHandlers)
|
|
50
|
+
a.handlers.add(handler);
|
|
51
|
+
const add = (node, priority) => units.push({ start: a.ast.getLineAndCharacterOfPosition(node.getStart(a.ast)).line + 1, end: a.ast.getLineAndCharacterOfPosition(node.end).line + 1, priority });
|
|
52
|
+
const inspect = (node) => {
|
|
53
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
|
54
|
+
add(node, 2);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && node.body && ts.isBlock(node.body)) {
|
|
58
|
+
for (const statement of node.body.statements) {
|
|
59
|
+
const raw = statement.getText(a.ast);
|
|
60
|
+
const names = ts.isVariableStatement(statement) ? statement.declarationList.declarations.map(declaration => declaration.name.getText(a.ast)) : ts.isFunctionDeclaration(statement) && statement.name ? [statement.name.text] : [];
|
|
61
|
+
add(statement, /return\s*\(?\s*</.test(raw) || /useEffect|useState|useCallback|useMemo/.test(raw) || names.some(name => a.handlers.has(name)) ? 0 : 1);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (ts.isClassDeclaration(node)) {
|
|
66
|
+
for (const member of node.members)
|
|
67
|
+
add(member, member.name && a.handlers.has(member.name.getText(a.ast)) ? 0 : 1);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (ts.isVariableStatement(node)) {
|
|
71
|
+
let expanded = false;
|
|
72
|
+
for (const declaration of node.declarationList.declarations)
|
|
73
|
+
if (declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) {
|
|
74
|
+
inspect(declaration.initializer);
|
|
75
|
+
expanded = true;
|
|
76
|
+
}
|
|
77
|
+
if (!expanded)
|
|
78
|
+
add(node, 2);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
add(node, 2);
|
|
82
|
+
};
|
|
83
|
+
for (const node of a.ast.statements)
|
|
84
|
+
inspect(node);
|
|
85
|
+
const selected = [];
|
|
86
|
+
let retained = 0;
|
|
87
|
+
for (const unit of units.sort((a, b) => a.priority - b.priority || a.start - b.start)) {
|
|
88
|
+
if (selected.some(window => window.start <= unit.start && window.end >= unit.end))
|
|
89
|
+
continue;
|
|
90
|
+
const window = make(unit.start, unit.end), length = bytes(window.text);
|
|
91
|
+
if (length > limits.span || retained + length > limits.file)
|
|
92
|
+
continue;
|
|
93
|
+
selected.push(window);
|
|
94
|
+
retained += length;
|
|
95
|
+
}
|
|
96
|
+
return selected.sort((a, b) => a.start - b.start);
|
|
97
|
+
}
|
|
98
|
+
const shell = (path) => /^(?:app|main|router|routes|layout)\.[cm]?[jt]sx?$/i.test(posix.basename(path)) || /\/app\.component\.(?:ts|html)$/.test(path);
|
|
99
|
+
const rank = (path) => /\/(?:routes|pages|screens|views)\//.test('/' + path) ? 0 : /\.component\.html$/.test(path) ? 1 : 2;
|
|
100
|
+
const family = (path) => { const parts = path.split('/'), index = parts.findIndex(part => ['routes', 'pages', 'screens', 'views'].includes(part)); return index >= 0 ? parts.slice(0, index + 2).join('/') : posix.dirname(path); };
|
|
101
|
+
export function connectedRecommendationSource(candidates, maxPacketBytes = 262144) {
|
|
102
|
+
const files = new Map(candidates.map(file => [file.path, file])), analyses = new Map();
|
|
103
|
+
const record = (path) => { if (!analyses.has(path))
|
|
104
|
+
analyses.set(path, analyze(files.get(path))); return analyses.get(path); };
|
|
105
|
+
const resolve = (from, specifier) => {
|
|
106
|
+
const base = specifier.startsWith('.') ? posix.normalize(posix.join(posix.dirname(from), specifier)) : specifier.startsWith('@/') ? `src/${specifier.slice(2)}` : '';
|
|
107
|
+
if (!base)
|
|
108
|
+
return undefined;
|
|
109
|
+
return [base, ...['.tsx', '.ts', '.jsx', '.js', '.vue', '.svelte', '.html', '/index.tsx', '/index.ts', '/index.jsx', '/index.js'].map(ext => base + ext)].find(path => files.has(path));
|
|
110
|
+
};
|
|
111
|
+
const references = (path) => {
|
|
112
|
+
const a = record(path);
|
|
113
|
+
return [
|
|
114
|
+
...a.imports.flatMap(item => { const target = resolve(path, item.specifier); return target ? [{ target, rendered: a.rendered.has(item.specifier), template: false }] : []; }),
|
|
115
|
+
...a.templates.flatMap(template => { const target = resolve(path, template); return target ? [{ target, rendered: true, template: true }] : []; }),
|
|
116
|
+
...(/\.component\.html$/.test(path) && files.has(path.replace(/\.html$/, '.ts')) ? [{ target: path.replace(/\.html$/, '.ts'), rendered: true, template: true }] : []),
|
|
117
|
+
].sort((a, b) => Number(b.rendered) - Number(a.rendered) || a.target.localeCompare(b.target));
|
|
118
|
+
};
|
|
119
|
+
const buckets = new Map();
|
|
120
|
+
for (const file of candidates.filter(file => rank(file.path) < 2 && !shell(file.path))) {
|
|
121
|
+
const key = family(file.path);
|
|
122
|
+
buckets.set(key, [...(buckets.get(key) ?? []), file]);
|
|
123
|
+
}
|
|
124
|
+
for (const bucket of buckets.values())
|
|
125
|
+
bucket.sort((a, b) => a.path.split('/').length - b.path.split('/').length || a.path.localeCompare(b.path));
|
|
126
|
+
const queues = [...buckets].sort((a, b) => a[0].localeCompare(b[0])), seeds = [];
|
|
127
|
+
for (let i = 0; seeds.length < Math.min(limits.roots, [...buckets.values()].reduce((n, values) => n + values.length, 0)); i++)
|
|
128
|
+
for (const [, bucket] of queues)
|
|
129
|
+
if (bucket[i] && seeds.length < limits.roots)
|
|
130
|
+
seeds.push(bucket[i].path);
|
|
131
|
+
if (!seeds.length)
|
|
132
|
+
seeds.push(...candidates.filter(file => shell(file.path)).map(file => file.path).slice(0, limits.roots));
|
|
133
|
+
if (!seeds.length)
|
|
134
|
+
seeds.push(...candidates.filter(file => /\.(?:[cm]?[jt]sx?|vue|svelte|html)$/.test(file.path)).map(file => file.path).slice(0, limits.roots));
|
|
135
|
+
const rootSet = new Set(seeds), owner = new Map(seeds.map(path => [path, path]));
|
|
136
|
+
const find = (path) => { const seen = new Set(); while (owner.get(path) !== path && !seen.has(path)) {
|
|
137
|
+
seen.add(path);
|
|
138
|
+
path = owner.get(path);
|
|
139
|
+
} return path; };
|
|
140
|
+
for (const seed of seeds)
|
|
141
|
+
for (const edge of references(seed))
|
|
142
|
+
if (edge.rendered && rootSet.has(edge.target) && find(seed) !== find(edge.target))
|
|
143
|
+
owner.set(find(edge.target), find(seed));
|
|
144
|
+
const groups = new Map();
|
|
145
|
+
for (const seed of seeds) {
|
|
146
|
+
const key = find(seed);
|
|
147
|
+
groups.set(key, [...(groups.get(key) ?? []), seed]);
|
|
148
|
+
}
|
|
149
|
+
const workflows = [];
|
|
150
|
+
for (const roots of groups.values()) {
|
|
151
|
+
const windows = [], seen = new Set(), queue = roots.map(path => ({ path, depth: 0 }));
|
|
152
|
+
let retained = 0, support = 0;
|
|
153
|
+
const append = (path, supporting, handlers = []) => {
|
|
154
|
+
for (const window of recommendationSourceWindows(files.get(path), handlers)) {
|
|
155
|
+
const key = `${path}:${window.start}:${window.end}`, length = bytes(window.text);
|
|
156
|
+
if (seen.has(key) || retained + length > limits.workflow || supporting && support + length > limits.support)
|
|
157
|
+
continue;
|
|
158
|
+
seen.add(key);
|
|
159
|
+
windows.push(window);
|
|
160
|
+
retained += length;
|
|
161
|
+
if (supporting)
|
|
162
|
+
support += length;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
for (const root of roots)
|
|
166
|
+
append(root, false);
|
|
167
|
+
const visited = new Set();
|
|
168
|
+
while (queue.length) {
|
|
169
|
+
const item = queue.shift();
|
|
170
|
+
if (visited.has(item.path) || item.depth > 2)
|
|
171
|
+
continue;
|
|
172
|
+
visited.add(item.path);
|
|
173
|
+
for (const edge of references(item.path).slice(0, 12)) {
|
|
174
|
+
if (rootSet.has(edge.target) && !roots.includes(edge.target))
|
|
175
|
+
continue;
|
|
176
|
+
append(edge.target, !roots.includes(edge.target), edge.template ? [...record(item.path).handlers] : []);
|
|
177
|
+
if (edge.rendered || edge.template)
|
|
178
|
+
queue.push({ path: edge.target, depth: item.depth + 1 });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (windows.length)
|
|
182
|
+
workflows.push(windows);
|
|
183
|
+
}
|
|
184
|
+
const selected = [], seen = new Set();
|
|
185
|
+
let retained = 0, wireBytes = 512;
|
|
186
|
+
const append = (window) => {
|
|
187
|
+
const key = `${window.path}:${window.start}:${window.end}`, length = bytes(window.text);
|
|
188
|
+
if (seen.has(key) || selected.length >= limits.excerpts || retained + length > limits.total)
|
|
189
|
+
return;
|
|
190
|
+
const encoded = bytes(JSON.stringify({ kind: 'local_source', id: `source_${selected.length + 1}`, path: window.path, file_hash: '0'.repeat(64), excerpt_hash: '0'.repeat(64), snapshot_id: '00000000-0000-4000-8000-000000000000', start_line: window.start, end_line: window.end, excerpt: window.text })) + 1;
|
|
191
|
+
if (wireBytes + encoded > maxPacketBytes)
|
|
192
|
+
return;
|
|
193
|
+
seen.add(key);
|
|
194
|
+
selected.push(window);
|
|
195
|
+
retained += length;
|
|
196
|
+
wireBytes += encoded;
|
|
197
|
+
};
|
|
198
|
+
let shared = 0;
|
|
199
|
+
for (const file of candidates.filter(file => shell(file.path)).slice(0, 8))
|
|
200
|
+
for (const window of recommendationSourceWindows(file))
|
|
201
|
+
if (shared + bytes(window.text) <= limits.shared) {
|
|
202
|
+
append(window);
|
|
203
|
+
shared += bytes(window.text);
|
|
204
|
+
}
|
|
205
|
+
// Fair interleaving keeps the first large view from consuming all evidence.
|
|
206
|
+
for (let i = 0; i < Math.max(0, ...workflows.map(windows => windows.length)); i++)
|
|
207
|
+
for (const windows of workflows)
|
|
208
|
+
if (windows[i])
|
|
209
|
+
append(windows[i]);
|
|
210
|
+
return selected;
|
|
211
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Bounded, local-only diagnostics. Never include these in setup API requests. */
|
|
2
|
+
export function sanitizeSetupDiagnostic(value, secrets = []) {
|
|
3
|
+
let text = value.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
|
|
4
|
+
const environmentSecrets = Object.entries(process.env)
|
|
5
|
+
.filter(([key, value]) => /token|secret|password|api[_-]?key|authorization/i.test(key) && value && value.length >= 4)
|
|
6
|
+
.map(([, value]) => value);
|
|
7
|
+
for (const secret of [...secrets, ...environmentSecrets].filter(value => value.length >= 4).sort((a, b) => b.length - a.length))
|
|
8
|
+
text = text.split(secret).join('[redacted]');
|
|
9
|
+
text = text.replace(/\b(Bearer|Basic)\s+[^\s]+/gi, '$1 [redacted]');
|
|
10
|
+
text = text.replace(/((?:_authToken|_password|password|access_token|refresh_token|api_key|authorization)\s*[:=]\s*)[^\s]+/gi, '$1[redacted]');
|
|
11
|
+
text = text.replace(/\b(?:gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|npm_[A-Za-z0-9]+|sk-[A-Za-z0-9_-]{16,})\b/g, '[redacted]');
|
|
12
|
+
text = text.replace(/https?:\/\/[^\s<>"']+/g, value => {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
url.username = '';
|
|
16
|
+
url.password = '';
|
|
17
|
+
url.search = '';
|
|
18
|
+
url.hash = '';
|
|
19
|
+
return url.toString();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return '[invalid URL]';
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return text.trim().slice(0, 8000);
|
|
26
|
+
}
|
package/dist/setup-executor.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type BrowserSetupClient } from './setup.js';
|
|
2
2
|
import { type SetupLocalPlan, type SetupEffect, type SetupSessionView } from './setup-wire.js';
|
|
3
|
+
import { SetupHeartbeat } from './setup-heartbeat.js';
|
|
3
4
|
import { type SetupLocalFile } from './setup-files.js';
|
|
4
5
|
export interface PreparedSetupOperation {
|
|
5
6
|
effect: SetupEffect;
|
|
@@ -27,9 +28,15 @@ export interface SetupExecutorOptions {
|
|
|
27
28
|
maxPolls?: number;
|
|
28
29
|
sleep?: (ms: number) => Promise<void>;
|
|
29
30
|
onSession?: (session: SetupSessionView) => void;
|
|
31
|
+
/** Sanitized local detail, retained in the private journal, never sent to the API. */
|
|
32
|
+
onOperationError?: (failure: {
|
|
33
|
+
kind: SetupEffect['kind'];
|
|
34
|
+
message: string;
|
|
35
|
+
receiptPath: string;
|
|
36
|
+
}) => void;
|
|
30
37
|
}
|
|
31
38
|
export type SetupExecutorClient = Pick<BrowserSetupClient, 'identity' | 'poll' | 'plan' | 'claim' | 'startCommand' | 'report' | 'heartbeat' | 'reconcile'>;
|
|
32
39
|
/** Bounded local cleanup never restores authority or follows an unknown file/link. */
|
|
33
40
|
export declare function pruneSetupExecutionReceipts(now?: number): Promise<number>;
|
|
34
41
|
/** Runs only after browser approval. Durable intents precede effects and durable outcomes precede reports. */
|
|
35
|
-
export declare function runSetupExecutor(client: SetupExecutorClient, directory: string, runtime: SetupExecutorRuntime, options?: SetupExecutorOptions): Promise<SetupSessionView>;
|
|
42
|
+
export declare function runSetupExecutor(client: SetupExecutorClient, directory: string, runtime: SetupExecutorRuntime, options?: SetupExecutorOptions, sharedHeartbeat?: SetupHeartbeat): Promise<SetupSessionView>;
|