@trawlme/cli 3.11.0 → 3.12.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 +5 -2
- package/dist/commands/create.d.ts +0 -28
- package/dist/commands/create.js +0 -89
- package/dist/commands/doctor.d.ts +0 -79
- package/dist/commands/doctor.js +1 -187
- package/dist/commands/login.js +0 -67
- package/dist/commands/ping.d.ts +0 -15
- package/dist/commands/ping.js +0 -15
- package/dist/commands/scraps.d.ts +0 -120
- package/dist/commands/scraps.js +142 -656
- package/dist/commands/skills.js +0 -22
- package/dist/commands/spec.d.ts +0 -85
- package/dist/commands/spec.js +0 -67
- package/dist/commands/telemetry.js +0 -4
- package/dist/commands/token.js +0 -28
- package/dist/commands/upgrade.js +0 -22
- package/dist/commands/whoami.d.ts +0 -12
- package/dist/commands/whoami.js +0 -6
- package/dist/index.d.ts +0 -188
- package/dist/index.js +0 -349
- package/dist/lib/api.d.ts +0 -78
- package/dist/lib/api.js +1 -320
- package/dist/lib/cdp-pipe.d.ts +31 -0
- package/dist/lib/cdp-pipe.js +141 -0
- package/dist/lib/chrome-discovery.d.ts +1 -0
- package/dist/lib/chrome-discovery.js +30 -0
- package/dist/lib/chrome-launch.d.ts +8 -0
- package/dist/lib/chrome-launch.js +53 -0
- package/dist/lib/config.d.ts +0 -53
- package/dist/lib/config.js +0 -55
- package/dist/lib/confirm.d.ts +0 -55
- package/dist/lib/confirm.js +0 -47
- package/dist/lib/docs.d.ts +0 -123
- package/dist/lib/docs.js +0 -169
- package/dist/lib/errors.d.ts +0 -134
- package/dist/lib/errors.js +0 -151
- package/dist/lib/format.d.ts +0 -6
- package/dist/lib/format.js +0 -6
- package/dist/lib/json.d.ts +0 -35
- package/dist/lib/json.js +0 -48
- package/dist/lib/jwt.d.ts +0 -7
- package/dist/lib/jwt.js +0 -7
- package/dist/lib/pinch.d.ts +0 -53
- package/dist/lib/pinch.js +6 -112
- package/dist/lib/pinchAnimation.d.ts +0 -16
- package/dist/lib/pinchAnimation.js +8 -29
- package/dist/lib/posthog.d.ts +0 -9
- package/dist/lib/posthog.js +0 -23
- package/dist/lib/prompt.js +1 -20
- package/dist/lib/secure-transport.d.ts +1 -0
- package/dist/lib/secure-transport.js +15 -0
- package/dist/lib/session-capture-guard.d.ts +6 -0
- package/dist/lib/session-capture-guard.js +9 -0
- package/dist/lib/session-capture.d.ts +55 -0
- package/dist/lib/session-capture.js +319 -0
- package/dist/lib/skills.d.ts +0 -175
- package/dist/lib/skills.js +1 -216
- package/dist/lib/skillsNudge.d.ts +0 -17
- package/dist/lib/skillsNudge.js +0 -83
- package/dist/lib/spinner.d.ts +0 -39
- package/dist/lib/spinner.js +0 -40
- package/dist/lib/storage-state.d.ts +55 -0
- package/dist/lib/storage-state.js +96 -0
- package/dist/lib/tips.d.ts +0 -38
- package/dist/lib/tips.js +0 -77
- package/dist/lib/updateCheckWorker.js +0 -14
- package/dist/lib/updateNotifier.d.ts +0 -17
- package/dist/lib/updateNotifier.js +0 -53
- package/dist/lib/validate.d.ts +0 -8
- package/dist/lib/validate.js +0 -8
- package/dist/lib/version.d.ts +0 -12
- package/dist/lib/version.js +1 -13
- package/package.json +2 -2
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function detectNonInteractive({ stdinIsTTY, platform, env }) {
|
|
2
|
+
if (!stdinIsTTY) {
|
|
3
|
+
return 'No interactive terminal attached (stdin is not a TTY) — this command opens a visible Chrome window for a human to log into and needs a terminal to confirm completion. It does not work headless, in CI, or piped.';
|
|
4
|
+
}
|
|
5
|
+
if (platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
|
|
6
|
+
return 'No display detected (DISPLAY/WAYLAND_DISPLAY are both unset) — this command opens a visible Chrome window and cannot run over a plain SSH session, in a container, or headless.';
|
|
7
|
+
}
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import type { CdpPipe } from './cdp-pipe.js';
|
|
3
|
+
import { type RawCdpCookie, type RawOriginLocalStorage, type StorageState } from './storage-state.js';
|
|
4
|
+
export interface CaptureCounts {
|
|
5
|
+
cookiesCaptured: number;
|
|
6
|
+
cookiesDroppedOutOfScope: number;
|
|
7
|
+
cookiesDroppedInvalid: number;
|
|
8
|
+
originsCaptured: number;
|
|
9
|
+
originsDroppedOutOfScope: number;
|
|
10
|
+
originsUnreadable: number;
|
|
11
|
+
closedEarly: boolean;
|
|
12
|
+
}
|
|
13
|
+
export type CaptureFailureReason = 'non_interactive' | 'no_chrome' | 'launch_failed' | 'capture_failed' | 'process_exited_before_capture' | 'cdp_protocol_error' | 'no_cookies_in_scope';
|
|
14
|
+
export type CaptureResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
storageState: StorageState;
|
|
17
|
+
counts: CaptureCounts;
|
|
18
|
+
targetDomain: string;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
reason: CaptureFailureReason;
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
interface RawCapture {
|
|
25
|
+
cookies: RawCdpCookie[];
|
|
26
|
+
origins: RawOriginLocalStorage[];
|
|
27
|
+
originsUnreadable: number;
|
|
28
|
+
}
|
|
29
|
+
export interface CaptureDeps {
|
|
30
|
+
detectNonInteractive(): string | null;
|
|
31
|
+
findChrome(): string | null;
|
|
32
|
+
mkdtemp(): string;
|
|
33
|
+
rmSync(dir: string): void;
|
|
34
|
+
launch(chromePath: string, targetUrl: string, userDataDir: string): Promise<{
|
|
35
|
+
proc: ChildProcess;
|
|
36
|
+
cdp: CdpPipe;
|
|
37
|
+
}>;
|
|
38
|
+
findPageTarget(cdp: CdpPipe): Promise<string>;
|
|
39
|
+
waitForDone(cdp: CdpPipe, targetId: string, cleanup: () => void): Promise<{
|
|
40
|
+
closedEarly: boolean;
|
|
41
|
+
}>;
|
|
42
|
+
readCapture(cdp: CdpPipe, targetUrl: string, closedEarly: boolean): Promise<RawCapture>;
|
|
43
|
+
onSigint(cleanup: () => void): () => void;
|
|
44
|
+
}
|
|
45
|
+
export declare function findPageTargetDefault(cdp: CdpPipe): Promise<string>;
|
|
46
|
+
export declare function waitForDoneDefault(cdp: CdpPipe, targetId: string, cleanup: () => void): Promise<{
|
|
47
|
+
closedEarly: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
export declare const LOCALSTORAGE_READ_TIMEOUT_MS = 30000;
|
|
50
|
+
export declare function readCaptureDefault(cdp: CdpPipe, targetUrl: string, closedEarly: boolean): Promise<RawCapture>;
|
|
51
|
+
export declare function rmSyncWithRetry(dir: string): void;
|
|
52
|
+
export declare function registerTerminationHandlers(cleanup: () => void): () => void;
|
|
53
|
+
export declare function checkInteractiveEnvironment(): string | null;
|
|
54
|
+
export declare function captureSession(targetUrl: string, depsOverride?: Partial<CaptureDeps>): Promise<CaptureResult>;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { findChrome } from './chrome-discovery.js';
|
|
6
|
+
import { launchChrome } from './chrome-launch.js';
|
|
7
|
+
import { detectNonInteractive } from './session-capture-guard.js';
|
|
8
|
+
import { mapCookies, mapOrigins, getTargetHost, isOriginHostInScope, } from './storage-state.js';
|
|
9
|
+
const PROCESS_EXITED_MESSAGE = 'Chrome exited before the session was read — nothing uploaded. The capture completes on Enter in the terminal; Chrome exiting first ends the run without a session.';
|
|
10
|
+
function cdpProtocolErrorMessage(protocolError) {
|
|
11
|
+
return `${protocolError.message} Nothing was uploaded.`;
|
|
12
|
+
}
|
|
13
|
+
export async function findPageTargetDefault(cdp) {
|
|
14
|
+
const deadline = Date.now() + 10_000;
|
|
15
|
+
for (;;) {
|
|
16
|
+
const { targetInfos } = await cdp.send('Target.getTargets');
|
|
17
|
+
const page = targetInfos.find((t) => t.type === 'page');
|
|
18
|
+
if (page)
|
|
19
|
+
return page.targetId;
|
|
20
|
+
if (Date.now() > deadline)
|
|
21
|
+
throw new Error('Chrome did not open a page target in time');
|
|
22
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function waitForDoneDefault(cdp, targetId, cleanup) {
|
|
26
|
+
await cdp.send('Target.setDiscoverTargets', { discover: true });
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
let settled = false;
|
|
29
|
+
const offDestroyed = cdp.on('Target.targetDestroyed', (params) => {
|
|
30
|
+
const p = params;
|
|
31
|
+
if (!settled && p.targetId === targetId) {
|
|
32
|
+
settled = true;
|
|
33
|
+
offDestroyed();
|
|
34
|
+
offClose();
|
|
35
|
+
rl.close();
|
|
36
|
+
resolve({ closedEarly: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const offClose = cdp.onClose(() => {
|
|
40
|
+
if (!settled) {
|
|
41
|
+
settled = true;
|
|
42
|
+
offDestroyed();
|
|
43
|
+
rl.close();
|
|
44
|
+
resolve({ closedEarly: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
48
|
+
rl.on('SIGINT', () => {
|
|
49
|
+
settled = true;
|
|
50
|
+
offDestroyed();
|
|
51
|
+
offClose();
|
|
52
|
+
rl.close();
|
|
53
|
+
cleanup();
|
|
54
|
+
process.exit(130);
|
|
55
|
+
});
|
|
56
|
+
rl.question('Capture completes on Enter once you are signed in, or when the Chrome window is closed: ', () => {
|
|
57
|
+
if (!settled) {
|
|
58
|
+
settled = true;
|
|
59
|
+
offDestroyed();
|
|
60
|
+
offClose();
|
|
61
|
+
rl.close();
|
|
62
|
+
resolve({ closedEarly: false });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export const LOCALSTORAGE_READ_TIMEOUT_MS = 30_000;
|
|
68
|
+
const LOCALSTORAGE_READ_PROGRESS_MS = 5_000;
|
|
69
|
+
function isLocalStorageEntriesShape(value) {
|
|
70
|
+
return (Array.isArray(value) &&
|
|
71
|
+
value.every((e) => typeof e === 'object' && e !== null && typeof e.name === 'string' && typeof e.value === 'string'));
|
|
72
|
+
}
|
|
73
|
+
async function readPageOverCdp(cdp, targetId) {
|
|
74
|
+
let sessionId;
|
|
75
|
+
try {
|
|
76
|
+
const attached = await cdp.send('Target.attachToTarget', {
|
|
77
|
+
targetId,
|
|
78
|
+
flatten: true,
|
|
79
|
+
});
|
|
80
|
+
sessionId = attached.sessionId;
|
|
81
|
+
const progressTimer = setTimeout(() => {
|
|
82
|
+
process.stderr.write(` still waiting for a page to finish its scripts (up to ${LOCALSTORAGE_READ_TIMEOUT_MS / 1000}s)…\n`);
|
|
83
|
+
}, LOCALSTORAGE_READ_PROGRESS_MS);
|
|
84
|
+
try {
|
|
85
|
+
return await cdp.send('Runtime.evaluate', {
|
|
86
|
+
expression: 'Object.entries(window.localStorage).map(([name, value]) => ({ name, value }))',
|
|
87
|
+
returnByValue: true,
|
|
88
|
+
}, sessionId, LOCALSTORAGE_READ_TIMEOUT_MS);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
clearTimeout(progressTimer);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (sessionId) {
|
|
96
|
+
try {
|
|
97
|
+
await cdp.send('Target.detachFromTarget', { sessionId });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export async function readCaptureDefault(cdp, targetUrl, closedEarly) {
|
|
105
|
+
const { cookies } = await cdp.send('Storage.getCookies', {});
|
|
106
|
+
if (closedEarly)
|
|
107
|
+
return { cookies, origins: [], originsUnreadable: 0 };
|
|
108
|
+
const targetHost = getTargetHost(targetUrl);
|
|
109
|
+
const { targetInfos } = await cdp.send('Target.getTargets');
|
|
110
|
+
const pages = targetInfos.filter((t) => t.type === 'page');
|
|
111
|
+
const origins = [];
|
|
112
|
+
let originsUnreadable = 0;
|
|
113
|
+
for (const page of pages) {
|
|
114
|
+
let origin;
|
|
115
|
+
try {
|
|
116
|
+
const u = new URL(page.url);
|
|
117
|
+
if (!isOriginHostInScope(u.hostname, targetHost))
|
|
118
|
+
continue;
|
|
119
|
+
origin = u.origin;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
let evalResult;
|
|
125
|
+
try {
|
|
126
|
+
evalResult = await readPageOverCdp(cdp, page.targetId);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
if (cdp.isClosed)
|
|
130
|
+
throw e;
|
|
131
|
+
originsUnreadable++;
|
|
132
|
+
process.stderr.write(' one in-scope page could not be read over CDP — counted, continuing…\n');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (evalResult.exceptionDetails) {
|
|
136
|
+
originsUnreadable++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const value = evalResult.result?.value;
|
|
140
|
+
if (!isLocalStorageEntriesShape(value)) {
|
|
141
|
+
originsUnreadable++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (value.length > 0) {
|
|
145
|
+
origins.push({ origin, entries: value });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { cookies, origins, originsUnreadable };
|
|
149
|
+
}
|
|
150
|
+
export function rmSyncWithRetry(dir) {
|
|
151
|
+
const MAX_ATTEMPTS = 5;
|
|
152
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
153
|
+
try {
|
|
154
|
+
rmSync(dir, { recursive: true, force: true });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
if (attempt === MAX_ATTEMPTS)
|
|
159
|
+
throw e;
|
|
160
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 40);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const TERMINATION_EXIT_CODES = {
|
|
165
|
+
SIGHUP: 129,
|
|
166
|
+
SIGINT: 130,
|
|
167
|
+
SIGTERM: 143,
|
|
168
|
+
};
|
|
169
|
+
export function registerTerminationHandlers(cleanup) {
|
|
170
|
+
const signals = Object.keys(TERMINATION_EXIT_CODES);
|
|
171
|
+
const handlers = signals.map((signal) => {
|
|
172
|
+
const handler = () => {
|
|
173
|
+
cleanup();
|
|
174
|
+
process.exit(TERMINATION_EXIT_CODES[signal]);
|
|
175
|
+
};
|
|
176
|
+
process.on(signal, handler);
|
|
177
|
+
return { signal, handler };
|
|
178
|
+
});
|
|
179
|
+
return () => {
|
|
180
|
+
for (const { signal, handler } of handlers)
|
|
181
|
+
process.off(signal, handler);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function checkInteractiveEnvironment() {
|
|
185
|
+
return detectNonInteractive({ stdinIsTTY: Boolean(process.stdin.isTTY), platform: process.platform, env: process.env });
|
|
186
|
+
}
|
|
187
|
+
function defaultDeps() {
|
|
188
|
+
return {
|
|
189
|
+
detectNonInteractive: checkInteractiveEnvironment,
|
|
190
|
+
findChrome: () => findChrome(),
|
|
191
|
+
mkdtemp: () => mkdtempSync(join(tmpdir(), 'trawl-session-')),
|
|
192
|
+
rmSync: rmSyncWithRetry,
|
|
193
|
+
launch: (chromePath, targetUrl, userDataDir) => launchChrome(chromePath, targetUrl, userDataDir),
|
|
194
|
+
findPageTarget: findPageTargetDefault,
|
|
195
|
+
waitForDone: waitForDoneDefault,
|
|
196
|
+
readCapture: readCaptureDefault,
|
|
197
|
+
onSigint: registerTerminationHandlers,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export async function captureSession(targetUrl, depsOverride = {}) {
|
|
201
|
+
const deps = { ...defaultDeps(), ...depsOverride };
|
|
202
|
+
const guardMessage = deps.detectNonInteractive();
|
|
203
|
+
if (guardMessage)
|
|
204
|
+
return { ok: false, reason: 'non_interactive', message: guardMessage };
|
|
205
|
+
const chromePath = deps.findChrome();
|
|
206
|
+
if (!chromePath) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
reason: 'no_chrome',
|
|
210
|
+
message: 'No Chrome or Chromium executable was found. Google Chrome, once installed, is auto-detected; TRAWL_CHROME_PATH names one explicitly.',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const userDataDir = deps.mkdtemp();
|
|
214
|
+
let proc;
|
|
215
|
+
let cleaned = false;
|
|
216
|
+
const cleanup = () => {
|
|
217
|
+
if (cleaned)
|
|
218
|
+
return;
|
|
219
|
+
cleaned = true;
|
|
220
|
+
try {
|
|
221
|
+
if (proc?.pid) {
|
|
222
|
+
if (process.platform === 'win32')
|
|
223
|
+
proc.kill('SIGKILL');
|
|
224
|
+
else
|
|
225
|
+
process.kill(-proc.pid, 'SIGKILL');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
deps.rmSync(userDataDir);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const unregisterSigint = deps.onSigint(cleanup);
|
|
237
|
+
let launched;
|
|
238
|
+
let stage = 'launch';
|
|
239
|
+
try {
|
|
240
|
+
launched = await deps.launch(chromePath, targetUrl, userDataDir);
|
|
241
|
+
proc = launched.proc;
|
|
242
|
+
let pipeClosed = false;
|
|
243
|
+
const offClose = launched.cdp.onClose(() => {
|
|
244
|
+
pipeClosed = true;
|
|
245
|
+
});
|
|
246
|
+
stage = 'handshake';
|
|
247
|
+
const targetId = await deps.findPageTarget(launched.cdp);
|
|
248
|
+
const { closedEarly } = await deps.waitForDone(launched.cdp, targetId, cleanup);
|
|
249
|
+
if (pipeClosed) {
|
|
250
|
+
if (launched.cdp.protocolError) {
|
|
251
|
+
return {
|
|
252
|
+
ok: false,
|
|
253
|
+
reason: 'cdp_protocol_error',
|
|
254
|
+
message: cdpProtocolErrorMessage(launched.cdp.protocolError),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
reason: 'process_exited_before_capture',
|
|
260
|
+
message: PROCESS_EXITED_MESSAGE,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
stage = 'capture';
|
|
264
|
+
const raw = await deps.readCapture(launched.cdp, targetUrl, closedEarly);
|
|
265
|
+
offClose();
|
|
266
|
+
const cookieResult = mapCookies(raw.cookies, targetUrl);
|
|
267
|
+
const originResult = mapOrigins(raw.origins, targetUrl);
|
|
268
|
+
const targetDomain = getTargetHost(targetUrl);
|
|
269
|
+
if (cookieResult.cookies.length === 0) {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
reason: 'no_cookies_in_scope',
|
|
273
|
+
message: `0 cookies in scope for ${targetDomain} — nothing uploaded. Login had not completed on that domain when the capture ran.`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
ok: true,
|
|
278
|
+
storageState: { cookies: cookieResult.cookies, origins: originResult.origins },
|
|
279
|
+
targetDomain,
|
|
280
|
+
counts: {
|
|
281
|
+
cookiesCaptured: cookieResult.cookies.length,
|
|
282
|
+
cookiesDroppedOutOfScope: cookieResult.droppedOutOfScope,
|
|
283
|
+
cookiesDroppedInvalid: cookieResult.droppedInvalid,
|
|
284
|
+
originsCaptured: originResult.origins.length,
|
|
285
|
+
originsDroppedOutOfScope: originResult.droppedOutOfScope,
|
|
286
|
+
originsUnreadable: raw.originsUnreadable,
|
|
287
|
+
closedEarly,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
if (launched?.cdp.protocolError) {
|
|
293
|
+
return {
|
|
294
|
+
ok: false,
|
|
295
|
+
reason: 'cdp_protocol_error',
|
|
296
|
+
message: cdpProtocolErrorMessage(launched.cdp.protocolError),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (launched?.cdp.isClosed) {
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
reason: 'process_exited_before_capture',
|
|
303
|
+
message: PROCESS_EXITED_MESSAGE,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (stage === 'capture') {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
reason: 'capture_failed',
|
|
310
|
+
message: 'The capture failed during the capture stage, after Chrome was already reachable over CDP and a page was already open — this is a CLI-side failure, not a page or environment problem. Nothing was uploaded.',
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return { ok: false, reason: 'launch_failed', message: `Chrome could not be driven over CDP: ${e.message}` };
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
unregisterSigint();
|
|
317
|
+
cleanup();
|
|
318
|
+
}
|
|
319
|
+
}
|
package/dist/lib/skills.d.ts
CHANGED
|
@@ -1,198 +1,23 @@
|
|
|
1
1
|
export declare function getBundledSkillsVersion(): string;
|
|
2
2
|
export declare function listBundledSkills(): string[];
|
|
3
|
-
/**
|
|
4
|
-
* #184 defect 1 — the ownership-refusal thrown below serves two different
|
|
5
|
-
* audiences on two different channels, and one string can't correctly serve
|
|
6
|
-
* both:
|
|
7
|
-
*
|
|
8
|
-
* - A human who just typed `trawl skills install`/`update` reaches this via
|
|
9
|
-
* an uncaught throw (index.ts's generic error path prints `.message`
|
|
10
|
-
* verbatim) — that reader can decide whether to add `--force`, so the
|
|
11
|
-
* guidance belongs on this channel. `.message` (below) keeps it.
|
|
12
|
-
* - `bootstrapSkillsOnLogin` also catches this exact throw and relays its
|
|
13
|
-
* text into `skipped[].reason`, which `login.ts` prints to stderr — a
|
|
14
|
-
* channel this feature's own doc comments say must never carry a command
|
|
15
|
-
* phrased as an instruction, because this CLI is driven by AI agents and
|
|
16
|
-
* this platform can feed a CLI's own stderr back into an agent's own
|
|
17
|
-
* context. `Pass --force to overwrite it anyway` is exactly that kind of
|
|
18
|
-
* instruction: an agent "obeying" it calls `installSkill`'s own
|
|
19
|
-
* `rmSync(recursive)` on a directory the user owns and trawl did not
|
|
20
|
-
* create — the destroy-the-user's-files incident this class of bug
|
|
21
|
-
* produces.
|
|
22
|
-
*
|
|
23
|
-
* `.relayableReason` carries the identical fact — this path exists, trawl
|
|
24
|
-
* did not create it, so it was left untouched — with the imperative sentence
|
|
25
|
-
* removed, for every channel that is not a direct, synchronous reply to a
|
|
26
|
-
* human's own typed command.
|
|
27
|
-
*/
|
|
28
3
|
export declare class SkillOwnershipRefusalError extends Error {
|
|
29
4
|
readonly relayableReason: string;
|
|
30
5
|
constructor(dest: string);
|
|
31
6
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Ownership guard (#73, extended #86 finding 7): this does `rmSync(recursive)`
|
|
34
|
-
* on the target dir before reinstalling, so it must never do that to a dir
|
|
35
|
-
* the CLI didn't install. `autoUpdateInstalledSkills()` already checks this
|
|
36
|
-
* itself before ever calling here (it skips marker-less dirs outright), but
|
|
37
|
-
* the explicit `trawl skills install`/`update` commands used to call straight
|
|
38
|
-
* through with no such check — a pre-existing user-authored
|
|
39
|
-
* `.claude/skills/<name>` dir that happens to collide with a bundled skill
|
|
40
|
-
* name would get silently deleted and overwritten. A missing `.version`
|
|
41
|
-
* marker on an EXISTING dest now refuses the install/reinstall unless
|
|
42
|
-
* `force` is passed.
|
|
43
|
-
*/
|
|
44
7
|
export declare function installSkill(name: string, scope: 'user' | 'local', opts?: {
|
|
45
8
|
force?: boolean;
|
|
46
9
|
}): string;
|
|
47
10
|
export declare function uninstallSkill(name: string, scope: 'user' | 'local'): string | null;
|
|
48
11
|
export declare function getInstalledVersion(name: string, scope: 'user' | 'local'): string | null;
|
|
49
12
|
export declare function isSkillInstalled(name: string, scope: 'user' | 'local'): boolean;
|
|
50
|
-
/**
|
|
51
|
-
* #86 review — orphan cleanup. The re-sync loop in autoUpdateInstalledSkills
|
|
52
|
-
* iterates listBundledSkills() — the NEW package's names only. When a bundled
|
|
53
|
-
* skill is RENAMED between package versions (1.0.0 shipped `trawl`, 1.3.1
|
|
54
|
-
* renamed it `trawl-cli`), the old marker-owned dir is never visited again: a
|
|
55
|
-
* stale ghost skill teaching outdated CLI usage stays installed forever,
|
|
56
|
-
* alongside the new one. This sweeps each scope's skills base dir for
|
|
57
|
-
* installed dirs that (a) carry a `.version` marker — the same ownership
|
|
58
|
-
* proof as everywhere else; a marker-less user-authored dir is NEVER touched,
|
|
59
|
-
* whatever its name — and (b) are no longer in the bundled set, and removes
|
|
60
|
-
* them with one honest stderr line (same style as the re-sync line).
|
|
61
|
-
* Returns the removed names (for the explicit `skills update` path to
|
|
62
|
-
* summarize).
|
|
63
|
-
*/
|
|
64
13
|
export declare function removeOrphanedSkills(scope: 'user' | 'local'): string[];
|
|
65
|
-
/**
|
|
66
|
-
* Re-installs any CLI-owned skill whose installed version doesn't match the
|
|
67
|
-
* bundled one, and removes CLI-owned skills that are no longer bundled at all
|
|
68
|
-
* (renamed/dropped upstream — see removeOrphanedSkills). Called on CLI
|
|
69
|
-
* startup to keep skills in sync with the CLI version. Never throws —
|
|
70
|
-
* failures are silent so they don't break unrelated commands.
|
|
71
|
-
*
|
|
72
|
-
* Ownership guard (#73): `installSkill` does `rmSync(recursive)` on the target
|
|
73
|
-
* dir, so this MUST only ever touch dirs the CLI itself installed. Proof of
|
|
74
|
-
* ownership is a `.version` marker. A user-created `.claude/skills/<name>` dir
|
|
75
|
-
* that happens to collide with a bundled skill name carries no marker, so it is
|
|
76
|
-
* left untouched instead of being silently deleted + overwritten.
|
|
77
|
-
*
|
|
78
|
-
* Opt-out: `TRAWL_SKILLS_SYNC=0` disables auto-sync entirely (mirrors
|
|
79
|
-
* `TRAWL_TELEMETRY=0`), for users who manage their skills by hand.
|
|
80
|
-
*/
|
|
81
14
|
export declare function autoUpdateInstalledSkills(): void;
|
|
82
|
-
/**
|
|
83
|
-
* #184 — one line, reused everywhere the CLI installs a skill mid-invocation
|
|
84
|
-
* (login's bootstrap below, lib/skillsNudge.ts's two nudges): skills are
|
|
85
|
-
* discovered at Claude Code SESSION START, so a skill written to disk right
|
|
86
|
-
* now is invisible to whatever session is already running. Stated as a
|
|
87
|
-
* fact, never an imperative ("restart Claude Code") — this text can be
|
|
88
|
-
* relayed into an AI agent's own context (this CLI's whole incident was an
|
|
89
|
-
* agent driving it), and a command phrased there must never read as an
|
|
90
|
-
* instruction the agent is being told to obey.
|
|
91
|
-
*/
|
|
92
15
|
export declare const RESTART_CLAUDE_CODE_NOTE = "Claude Code must be restarted to see them \u2014 skills are loaded at session start, not mid-session.";
|
|
93
|
-
/**
|
|
94
|
-
* #184 — pure gate shared by every skills-related write/print that must
|
|
95
|
-
* never fire under `--json` (a machine consumer needs pure stdout and there
|
|
96
|
-
* is no human reading a suggestion anyway), on a non-TTY invocation (a CI
|
|
97
|
-
* runner or an agent driving this CLI as a subprocess has no Claude Code
|
|
98
|
-
* session to discover a newly-installed skill in the first place), or when
|
|
99
|
-
* `TRAWL_SKILLS_SYNC=0` (the existing auto-sync opt-out, extended here: a
|
|
100
|
-
* user who manages skills by hand does not want the CLI touching that dir
|
|
101
|
-
* for ANY reason — install or nudge alike). Every signal is a parameter,
|
|
102
|
-
* exactly like lib/tips.ts's `isReferralTipDue`, so this is testable
|
|
103
|
-
* without mocking env/TTY. `isTTY`'s exact definition (stdout-only vs
|
|
104
|
-
* stdin+stdout) is the CALLER's call — see bootstrapSkillsOnLogin below vs
|
|
105
|
-
* lib/skillsNudge.ts for the two different answers this codebase already
|
|
106
|
-
* gives elsewhere (confirm.ts's `isInteractive` vs tips.ts's own check).
|
|
107
|
-
*/
|
|
108
16
|
export declare function isSkillsActionAllowed(opts: {
|
|
109
17
|
json?: boolean;
|
|
110
18
|
isTTY: boolean;
|
|
111
19
|
optedOut: boolean;
|
|
112
20
|
}): boolean;
|
|
113
|
-
/**
|
|
114
|
-
* #184 — the "install on login, and only there" half of the never-installed
|
|
115
|
-
* bootstrap (see the module doc comment on `autoUpdateInstalledSkills`
|
|
116
|
-
* above for the "elsewhere: suggest, never install" half, which lives in
|
|
117
|
-
* lib/skillsNudge.ts instead). `autoUpdateInstalledSkills` deliberately
|
|
118
|
-
* never installs (`if (!isSkillInstalled) continue`) — it only keeps an
|
|
119
|
-
* EXISTING install in sync. A user who has never run any `trawl skills`
|
|
120
|
-
* command and never logged in before this shipped has nothing installed at
|
|
121
|
-
* all, and nothing in the CLI's startup path ever puts anything there: the
|
|
122
|
-
* CLI is competent, the agent reading its skills is not — the incident this
|
|
123
|
-
* issue exists to prevent. `login` is the one intentional human setup
|
|
124
|
-
* moment (the only place a human types credentials), so it is the ONLY
|
|
125
|
-
* place this function is ever called from (see commands/login.ts) — never
|
|
126
|
-
* from the generic startup path.
|
|
127
|
-
*
|
|
128
|
-
* Gated by `isSkillsActionAllowed` exactly like every other skills-related
|
|
129
|
-
* side effect: a non-interactive `trawl login` (CI's `TRAWL_TOKEN=x trawl
|
|
130
|
-
* login --json`, or any non-TTY invocation) writes nothing — that machine
|
|
131
|
-
* has no Claude Code session to discover a skill in, and `--json`'s stdout
|
|
132
|
-
* contract has no room for a plain-text confirmation line anyway. Also
|
|
133
|
-
* skipped entirely under `TRAWL_SKILLS_SYNC=0`.
|
|
134
|
-
*
|
|
135
|
-
* Installs every bundled skill NOT YET OWNED by trawl at `scope` (default
|
|
136
|
-
* 'user' — the global location a fresh `npx @trawlme/cli login` writes to;
|
|
137
|
-
* 'local' is opt-in via the same --local convention `trawl skills install`
|
|
138
|
-
* already uses everywhere else, kept for parity/tests). "Owned" means a
|
|
139
|
-
* readable `.version` marker (#184 review MAJOR — NOT mere path existence:
|
|
140
|
-
* see below for why that distinction matters here). One skill's install
|
|
141
|
-
* throwing must never blank out the others (#91's posture, applied here):
|
|
142
|
-
* each is wrapped individually, and the function reports exactly what
|
|
143
|
-
* landed. Returns `null` (nothing written, nothing to report) ONLY when
|
|
144
|
-
* gated out, or when every bundled skill is already owned (the re-sync
|
|
145
|
-
* loop above already keeps an existing install's version current) — the
|
|
146
|
-
* genuine "nothing to do" case. Never throws — a broken bootstrap must
|
|
147
|
-
* never turn a successful login into a failed one.
|
|
148
|
-
*
|
|
149
|
-
* #184 review (BLOCK + MAJOR) — two cases used to collapse into the exact
|
|
150
|
-
* same `null`/silence as genuine "nothing to do":
|
|
151
|
-
*
|
|
152
|
-
* 1. (BLOCK) Every install attempt failing outright (an unwritable
|
|
153
|
-
* `~/.claude/skills`, e.g.) used to return `null` — indistinguishable
|
|
154
|
-
* from "already installed" or "never attempted at all" on a first-run
|
|
155
|
-
* machine, the precise false-success shape #184 exists to prevent.
|
|
156
|
-
* 2. (MAJOR) A bundled skill name colliding with a PRE-EXISTING,
|
|
157
|
-
* marker-less directory the CLI doesn't own. The action set here used
|
|
158
|
-
* to be computed from `isSkillInstalled` (mere `existsSync`), which
|
|
159
|
-
* can't tell "we already installed this" from "something else already
|
|
160
|
-
* lives at this path" — a foreign dir was silently read as "already
|
|
161
|
-
* installed, nothing to do", so `installSkill`'s ownership-refusal
|
|
162
|
-
* guard was never even reached and the collision went unreported
|
|
163
|
-
* anywhere. The action set below is computed from *ownership*
|
|
164
|
-
* (`getInstalledVersion(...) !== null`) instead, so a foreign
|
|
165
|
-
* collision is genuinely attempted — hits the same guard `installSkill`
|
|
166
|
-
* already enforces elsewhere, throws, and is captured below — rather
|
|
167
|
-
* than silently skipped as if it were a prior trawl install.
|
|
168
|
-
*
|
|
169
|
-
* Whenever there was anything to attempt, the result is now ALWAYS a
|
|
170
|
-
* non-null object reporting both what landed (`installed`) and what didn't
|
|
171
|
-
* (`skipped`, each with a RELAYABLE reason — see `SkillOwnershipRefusalError`
|
|
172
|
-
* above: the ownership-refusal case reports `.relayableReason` [fact only,
|
|
173
|
-
* no `--force` imperative], every other throw [skill-not-found, raw fs
|
|
174
|
-
* errors like EACCES] reports `.message` as before, since those were never
|
|
175
|
-
* imperative to begin with), so `login.ts` can print an honest, distinct
|
|
176
|
-
* line for a skip instead of falling through to the generic "not installed"
|
|
177
|
-
* nudge as if login had done nothing, or silently omitting a name from the
|
|
178
|
-
* success line as if it had never been requested.
|
|
179
|
-
*
|
|
180
|
-
* #184 defect 2 — `error` (present whenever the function returns non-null)
|
|
181
|
-
* distinguishes a THIRD case from both "nothing to do" (`null`) and "one or
|
|
182
|
-
* more skills failed" (`skipped`): "could not even determine what to
|
|
183
|
-
* install" — `listBundledSkills()` returning an empty list (its own
|
|
184
|
-
* contract silently swallows a missing/renamed `skills/` dir into `[]`; see
|
|
185
|
-
* its doc comment) or `getSkillsPackageRoot()` throwing outright (the
|
|
186
|
-
* `@trawlme/skills` package itself unresolvable — a broken node_modules
|
|
187
|
-
* entry). Both used to fall through to `notOwned.length === 0` or the outer
|
|
188
|
-
* catch below, landing on the exact same `null` as a fully-up-to-date
|
|
189
|
-
* install — a genuinely corrupted bundle produced ZERO signal, not even a
|
|
190
|
-
* failed-attempt line, because nothing was ever attempted. `error` is
|
|
191
|
-
* `null` on every ordinary path (including genuine "all already owned",
|
|
192
|
-
* which still short-circuits to the `null` return below) and non-null only
|
|
193
|
-
* for this diagnostic-failure case, where `installed`/`skipped` are both
|
|
194
|
-
* empty because no skill name was ever known to attempt.
|
|
195
|
-
*/
|
|
196
21
|
export declare function bootstrapSkillsOnLogin(opts?: {
|
|
197
22
|
json?: boolean;
|
|
198
23
|
isTTY?: boolean;
|