@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
package/dist/lib/skills.js
CHANGED
|
@@ -24,31 +24,6 @@ function getSkillsBase(scope) {
|
|
|
24
24
|
const base = scope === 'local' ? join(process.cwd(), '.claude') : join(homedir(), '.claude');
|
|
25
25
|
return join(base, 'skills');
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
28
|
-
* #184 defect 1 — the ownership-refusal thrown below serves two different
|
|
29
|
-
* audiences on two different channels, and one string can't correctly serve
|
|
30
|
-
* both:
|
|
31
|
-
*
|
|
32
|
-
* - A human who just typed `trawl skills install`/`update` reaches this via
|
|
33
|
-
* an uncaught throw (index.ts's generic error path prints `.message`
|
|
34
|
-
* verbatim) — that reader can decide whether to add `--force`, so the
|
|
35
|
-
* guidance belongs on this channel. `.message` (below) keeps it.
|
|
36
|
-
* - `bootstrapSkillsOnLogin` also catches this exact throw and relays its
|
|
37
|
-
* text into `skipped[].reason`, which `login.ts` prints to stderr — a
|
|
38
|
-
* channel this feature's own doc comments say must never carry a command
|
|
39
|
-
* phrased as an instruction, because this CLI is driven by AI agents and
|
|
40
|
-
* this platform can feed a CLI's own stderr back into an agent's own
|
|
41
|
-
* context. `Pass --force to overwrite it anyway` is exactly that kind of
|
|
42
|
-
* instruction: an agent "obeying" it calls `installSkill`'s own
|
|
43
|
-
* `rmSync(recursive)` on a directory the user owns and trawl did not
|
|
44
|
-
* create — the destroy-the-user's-files incident this class of bug
|
|
45
|
-
* produces.
|
|
46
|
-
*
|
|
47
|
-
* `.relayableReason` carries the identical fact — this path exists, trawl
|
|
48
|
-
* did not create it, so it was left untouched — with the imperative sentence
|
|
49
|
-
* removed, for every channel that is not a direct, synchronous reply to a
|
|
50
|
-
* human's own typed command.
|
|
51
|
-
*/
|
|
52
27
|
export class SkillOwnershipRefusalError extends Error {
|
|
53
28
|
relayableReason;
|
|
54
29
|
constructor(dest) {
|
|
@@ -58,18 +33,6 @@ export class SkillOwnershipRefusalError extends Error {
|
|
|
58
33
|
this.relayableReason = `"${dest}" already exists and was not installed by trawl (no .version marker) — left untouched.`;
|
|
59
34
|
}
|
|
60
35
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Ownership guard (#73, extended #86 finding 7): this does `rmSync(recursive)`
|
|
63
|
-
* on the target dir before reinstalling, so it must never do that to a dir
|
|
64
|
-
* the CLI didn't install. `autoUpdateInstalledSkills()` already checks this
|
|
65
|
-
* itself before ever calling here (it skips marker-less dirs outright), but
|
|
66
|
-
* the explicit `trawl skills install`/`update` commands used to call straight
|
|
67
|
-
* through with no such check — a pre-existing user-authored
|
|
68
|
-
* `.claude/skills/<name>` dir that happens to collide with a bundled skill
|
|
69
|
-
* name would get silently deleted and overwritten. A missing `.version`
|
|
70
|
-
* marker on an EXISTING dest now refuses the install/reinstall unless
|
|
71
|
-
* `force` is passed.
|
|
72
|
-
*/
|
|
73
36
|
export function installSkill(name, scope, opts = {}) {
|
|
74
37
|
const src = join(getSkillsPackageRoot(), 'skills', name);
|
|
75
38
|
if (!existsSync(src)) {
|
|
@@ -77,12 +40,6 @@ export function installSkill(name, scope, opts = {}) {
|
|
|
77
40
|
}
|
|
78
41
|
const dest = join(getSkillsBase(scope), name);
|
|
79
42
|
if (existsSync(dest)) {
|
|
80
|
-
// #91 — an unreadable `.version` (e.g. EISDIR from a directory instead of
|
|
81
|
-
// a file, a corrupted/interrupted install) is NOT proof of ownership
|
|
82
|
-
// either — treat it exactly like a missing marker (unowned) instead of
|
|
83
|
-
// letting the raw fs error crash the whole batch (`skills install`/
|
|
84
|
-
// `update` with no skill argument loops over every bundled skill; one
|
|
85
|
-
// malformed dest must not block installing the others).
|
|
86
43
|
let installedVersion;
|
|
87
44
|
try {
|
|
88
45
|
installedVersion = getInstalledVersion(name, scope);
|
|
@@ -117,20 +74,6 @@ export function getInstalledVersion(name, scope) {
|
|
|
117
74
|
export function isSkillInstalled(name, scope) {
|
|
118
75
|
return existsSync(join(getSkillsBase(scope), name));
|
|
119
76
|
}
|
|
120
|
-
/**
|
|
121
|
-
* #86 review — orphan cleanup. The re-sync loop in autoUpdateInstalledSkills
|
|
122
|
-
* iterates listBundledSkills() — the NEW package's names only. When a bundled
|
|
123
|
-
* skill is RENAMED between package versions (1.0.0 shipped `trawl`, 1.3.1
|
|
124
|
-
* renamed it `trawl-cli`), the old marker-owned dir is never visited again: a
|
|
125
|
-
* stale ghost skill teaching outdated CLI usage stays installed forever,
|
|
126
|
-
* alongside the new one. This sweeps each scope's skills base dir for
|
|
127
|
-
* installed dirs that (a) carry a `.version` marker — the same ownership
|
|
128
|
-
* proof as everywhere else; a marker-less user-authored dir is NEVER touched,
|
|
129
|
-
* whatever its name — and (b) are no longer in the bundled set, and removes
|
|
130
|
-
* them with one honest stderr line (same style as the re-sync line).
|
|
131
|
-
* Returns the removed names (for the explicit `skills update` path to
|
|
132
|
-
* summarize).
|
|
133
|
-
*/
|
|
134
77
|
export function removeOrphanedSkills(scope) {
|
|
135
78
|
const base = getSkillsBase(scope);
|
|
136
79
|
if (!existsSync(base))
|
|
@@ -144,15 +87,10 @@ export function removeOrphanedSkills(scope) {
|
|
|
144
87
|
continue;
|
|
145
88
|
}
|
|
146
89
|
catch {
|
|
147
|
-
continue;
|
|
90
|
+
continue;
|
|
148
91
|
}
|
|
149
92
|
if (bundled.has(name))
|
|
150
93
|
continue;
|
|
151
|
-
// No `.version` marker → not ours → never delete it. Read is wrapped:
|
|
152
|
-
// a weird `.version` (e.g. a directory instead of a file → EISDIR on
|
|
153
|
-
// readFileSync) must skip only THIS entry, not blow up the whole sweep —
|
|
154
|
-
// a single malformed install must never leave every other entry
|
|
155
|
-
// unswept. (#88 item 12)
|
|
156
94
|
let installedVersion;
|
|
157
95
|
try {
|
|
158
96
|
installedVersion = getInstalledVersion(name, scope);
|
|
@@ -168,22 +106,6 @@ export function removeOrphanedSkills(scope) {
|
|
|
168
106
|
}
|
|
169
107
|
return removed;
|
|
170
108
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Re-installs any CLI-owned skill whose installed version doesn't match the
|
|
173
|
-
* bundled one, and removes CLI-owned skills that are no longer bundled at all
|
|
174
|
-
* (renamed/dropped upstream — see removeOrphanedSkills). Called on CLI
|
|
175
|
-
* startup to keep skills in sync with the CLI version. Never throws —
|
|
176
|
-
* failures are silent so they don't break unrelated commands.
|
|
177
|
-
*
|
|
178
|
-
* Ownership guard (#73): `installSkill` does `rmSync(recursive)` on the target
|
|
179
|
-
* dir, so this MUST only ever touch dirs the CLI itself installed. Proof of
|
|
180
|
-
* ownership is a `.version` marker. A user-created `.claude/skills/<name>` dir
|
|
181
|
-
* that happens to collide with a bundled skill name carries no marker, so it is
|
|
182
|
-
* left untouched instead of being silently deleted + overwritten.
|
|
183
|
-
*
|
|
184
|
-
* Opt-out: `TRAWL_SKILLS_SYNC=0` disables auto-sync entirely (mirrors
|
|
185
|
-
* `TRAWL_TELEMETRY=0`), for users who manage their skills by hand.
|
|
186
|
-
*/
|
|
187
109
|
export function autoUpdateInstalledSkills() {
|
|
188
110
|
if (process.env['TRAWL_SKILLS_SYNC'] === '0')
|
|
189
111
|
return;
|
|
@@ -193,14 +115,6 @@ export function autoUpdateInstalledSkills() {
|
|
|
193
115
|
for (const scope of ['user', 'local']) {
|
|
194
116
|
if (!isSkillInstalled(name, scope))
|
|
195
117
|
continue;
|
|
196
|
-
// #91 — mirrors removeOrphanedSkills' guard below: a weird `.version`
|
|
197
|
-
// marker (e.g. a directory instead of a file, from a corrupted /
|
|
198
|
-
// interrupted install) throws EISDIR on readFileSync. Without this
|
|
199
|
-
// per-entry guard, that throw was caught by this function's OUTER
|
|
200
|
-
// try/catch (below) — which aborts the ENTIRE function, so every
|
|
201
|
-
// remaining bundled skill silently stopped syncing AND the orphan
|
|
202
|
-
// sweep (removeOrphanedSkills, called after this loop) never ran
|
|
203
|
-
// either. Must skip only THIS entry, never abort the whole sweep.
|
|
204
118
|
let installed;
|
|
205
119
|
try {
|
|
206
120
|
installed = getInstalledVersion(name, scope);
|
|
@@ -208,53 +122,22 @@ export function autoUpdateInstalledSkills() {
|
|
|
208
122
|
catch {
|
|
209
123
|
continue;
|
|
210
124
|
}
|
|
211
|
-
// No `.version` marker → not ours → never delete it.
|
|
212
125
|
if (installed === null)
|
|
213
126
|
continue;
|
|
214
127
|
if (installed === bundledVersion)
|
|
215
128
|
continue;
|
|
216
129
|
installSkill(name, scope);
|
|
217
|
-
// One honest line so a destructive-looking re-sync is never silent.
|
|
218
|
-
// stderr keeps stdout clean for --json consumers.
|
|
219
130
|
process.stderr.write(`trawl: re-synced skill "${name}" (${scope}) ${installed} → ${bundledVersion}\n`);
|
|
220
131
|
}
|
|
221
132
|
}
|
|
222
|
-
// Migration gap (#86 review): also drop marker-owned dirs whose skill
|
|
223
|
-
// was renamed/removed upstream, or they linger as stale ghosts forever.
|
|
224
133
|
for (const scope of ['user', 'local']) {
|
|
225
134
|
removeOrphanedSkills(scope);
|
|
226
135
|
}
|
|
227
136
|
}
|
|
228
137
|
catch {
|
|
229
|
-
// Silent: skill auto-update should never block the CLI
|
|
230
138
|
}
|
|
231
139
|
}
|
|
232
|
-
/**
|
|
233
|
-
* #184 — one line, reused everywhere the CLI installs a skill mid-invocation
|
|
234
|
-
* (login's bootstrap below, lib/skillsNudge.ts's two nudges): skills are
|
|
235
|
-
* discovered at Claude Code SESSION START, so a skill written to disk right
|
|
236
|
-
* now is invisible to whatever session is already running. Stated as a
|
|
237
|
-
* fact, never an imperative ("restart Claude Code") — this text can be
|
|
238
|
-
* relayed into an AI agent's own context (this CLI's whole incident was an
|
|
239
|
-
* agent driving it), and a command phrased there must never read as an
|
|
240
|
-
* instruction the agent is being told to obey.
|
|
241
|
-
*/
|
|
242
140
|
export const RESTART_CLAUDE_CODE_NOTE = 'Claude Code must be restarted to see them — skills are loaded at session start, not mid-session.';
|
|
243
|
-
/**
|
|
244
|
-
* #184 — pure gate shared by every skills-related write/print that must
|
|
245
|
-
* never fire under `--json` (a machine consumer needs pure stdout and there
|
|
246
|
-
* is no human reading a suggestion anyway), on a non-TTY invocation (a CI
|
|
247
|
-
* runner or an agent driving this CLI as a subprocess has no Claude Code
|
|
248
|
-
* session to discover a newly-installed skill in the first place), or when
|
|
249
|
-
* `TRAWL_SKILLS_SYNC=0` (the existing auto-sync opt-out, extended here: a
|
|
250
|
-
* user who manages skills by hand does not want the CLI touching that dir
|
|
251
|
-
* for ANY reason — install or nudge alike). Every signal is a parameter,
|
|
252
|
-
* exactly like lib/tips.ts's `isReferralTipDue`, so this is testable
|
|
253
|
-
* without mocking env/TTY. `isTTY`'s exact definition (stdout-only vs
|
|
254
|
-
* stdin+stdout) is the CALLER's call — see bootstrapSkillsOnLogin below vs
|
|
255
|
-
* lib/skillsNudge.ts for the two different answers this codebase already
|
|
256
|
-
* gives elsewhere (confirm.ts's `isInteractive` vs tips.ts's own check).
|
|
257
|
-
*/
|
|
258
141
|
export function isSkillsActionAllowed(opts) {
|
|
259
142
|
if (opts.json)
|
|
260
143
|
return false;
|
|
@@ -264,89 +147,6 @@ export function isSkillsActionAllowed(opts) {
|
|
|
264
147
|
return false;
|
|
265
148
|
return true;
|
|
266
149
|
}
|
|
267
|
-
/**
|
|
268
|
-
* #184 — the "install on login, and only there" half of the never-installed
|
|
269
|
-
* bootstrap (see the module doc comment on `autoUpdateInstalledSkills`
|
|
270
|
-
* above for the "elsewhere: suggest, never install" half, which lives in
|
|
271
|
-
* lib/skillsNudge.ts instead). `autoUpdateInstalledSkills` deliberately
|
|
272
|
-
* never installs (`if (!isSkillInstalled) continue`) — it only keeps an
|
|
273
|
-
* EXISTING install in sync. A user who has never run any `trawl skills`
|
|
274
|
-
* command and never logged in before this shipped has nothing installed at
|
|
275
|
-
* all, and nothing in the CLI's startup path ever puts anything there: the
|
|
276
|
-
* CLI is competent, the agent reading its skills is not — the incident this
|
|
277
|
-
* issue exists to prevent. `login` is the one intentional human setup
|
|
278
|
-
* moment (the only place a human types credentials), so it is the ONLY
|
|
279
|
-
* place this function is ever called from (see commands/login.ts) — never
|
|
280
|
-
* from the generic startup path.
|
|
281
|
-
*
|
|
282
|
-
* Gated by `isSkillsActionAllowed` exactly like every other skills-related
|
|
283
|
-
* side effect: a non-interactive `trawl login` (CI's `TRAWL_TOKEN=x trawl
|
|
284
|
-
* login --json`, or any non-TTY invocation) writes nothing — that machine
|
|
285
|
-
* has no Claude Code session to discover a skill in, and `--json`'s stdout
|
|
286
|
-
* contract has no room for a plain-text confirmation line anyway. Also
|
|
287
|
-
* skipped entirely under `TRAWL_SKILLS_SYNC=0`.
|
|
288
|
-
*
|
|
289
|
-
* Installs every bundled skill NOT YET OWNED by trawl at `scope` (default
|
|
290
|
-
* 'user' — the global location a fresh `npx @trawlme/cli login` writes to;
|
|
291
|
-
* 'local' is opt-in via the same --local convention `trawl skills install`
|
|
292
|
-
* already uses everywhere else, kept for parity/tests). "Owned" means a
|
|
293
|
-
* readable `.version` marker (#184 review MAJOR — NOT mere path existence:
|
|
294
|
-
* see below for why that distinction matters here). One skill's install
|
|
295
|
-
* throwing must never blank out the others (#91's posture, applied here):
|
|
296
|
-
* each is wrapped individually, and the function reports exactly what
|
|
297
|
-
* landed. Returns `null` (nothing written, nothing to report) ONLY when
|
|
298
|
-
* gated out, or when every bundled skill is already owned (the re-sync
|
|
299
|
-
* loop above already keeps an existing install's version current) — the
|
|
300
|
-
* genuine "nothing to do" case. Never throws — a broken bootstrap must
|
|
301
|
-
* never turn a successful login into a failed one.
|
|
302
|
-
*
|
|
303
|
-
* #184 review (BLOCK + MAJOR) — two cases used to collapse into the exact
|
|
304
|
-
* same `null`/silence as genuine "nothing to do":
|
|
305
|
-
*
|
|
306
|
-
* 1. (BLOCK) Every install attempt failing outright (an unwritable
|
|
307
|
-
* `~/.claude/skills`, e.g.) used to return `null` — indistinguishable
|
|
308
|
-
* from "already installed" or "never attempted at all" on a first-run
|
|
309
|
-
* machine, the precise false-success shape #184 exists to prevent.
|
|
310
|
-
* 2. (MAJOR) A bundled skill name colliding with a PRE-EXISTING,
|
|
311
|
-
* marker-less directory the CLI doesn't own. The action set here used
|
|
312
|
-
* to be computed from `isSkillInstalled` (mere `existsSync`), which
|
|
313
|
-
* can't tell "we already installed this" from "something else already
|
|
314
|
-
* lives at this path" — a foreign dir was silently read as "already
|
|
315
|
-
* installed, nothing to do", so `installSkill`'s ownership-refusal
|
|
316
|
-
* guard was never even reached and the collision went unreported
|
|
317
|
-
* anywhere. The action set below is computed from *ownership*
|
|
318
|
-
* (`getInstalledVersion(...) !== null`) instead, so a foreign
|
|
319
|
-
* collision is genuinely attempted — hits the same guard `installSkill`
|
|
320
|
-
* already enforces elsewhere, throws, and is captured below — rather
|
|
321
|
-
* than silently skipped as if it were a prior trawl install.
|
|
322
|
-
*
|
|
323
|
-
* Whenever there was anything to attempt, the result is now ALWAYS a
|
|
324
|
-
* non-null object reporting both what landed (`installed`) and what didn't
|
|
325
|
-
* (`skipped`, each with a RELAYABLE reason — see `SkillOwnershipRefusalError`
|
|
326
|
-
* above: the ownership-refusal case reports `.relayableReason` [fact only,
|
|
327
|
-
* no `--force` imperative], every other throw [skill-not-found, raw fs
|
|
328
|
-
* errors like EACCES] reports `.message` as before, since those were never
|
|
329
|
-
* imperative to begin with), so `login.ts` can print an honest, distinct
|
|
330
|
-
* line for a skip instead of falling through to the generic "not installed"
|
|
331
|
-
* nudge as if login had done nothing, or silently omitting a name from the
|
|
332
|
-
* success line as if it had never been requested.
|
|
333
|
-
*
|
|
334
|
-
* #184 defect 2 — `error` (present whenever the function returns non-null)
|
|
335
|
-
* distinguishes a THIRD case from both "nothing to do" (`null`) and "one or
|
|
336
|
-
* more skills failed" (`skipped`): "could not even determine what to
|
|
337
|
-
* install" — `listBundledSkills()` returning an empty list (its own
|
|
338
|
-
* contract silently swallows a missing/renamed `skills/` dir into `[]`; see
|
|
339
|
-
* its doc comment) or `getSkillsPackageRoot()` throwing outright (the
|
|
340
|
-
* `@trawlme/skills` package itself unresolvable — a broken node_modules
|
|
341
|
-
* entry). Both used to fall through to `notOwned.length === 0` or the outer
|
|
342
|
-
* catch below, landing on the exact same `null` as a fully-up-to-date
|
|
343
|
-
* install — a genuinely corrupted bundle produced ZERO signal, not even a
|
|
344
|
-
* failed-attempt line, because nothing was ever attempted. `error` is
|
|
345
|
-
* `null` on every ordinary path (including genuine "all already owned",
|
|
346
|
-
* which still short-circuits to the `null` return below) and non-null only
|
|
347
|
-
* for this diagnostic-failure case, where `installed`/`skipped` are both
|
|
348
|
-
* empty because no skill name was ever known to attempt.
|
|
349
|
-
*/
|
|
350
150
|
export function bootstrapSkillsOnLogin(opts = {}) {
|
|
351
151
|
try {
|
|
352
152
|
const optedOut = process.env['TRAWL_SKILLS_SYNC'] === '0';
|
|
@@ -359,9 +159,6 @@ export function bootstrapSkillsOnLogin(opts = {}) {
|
|
|
359
159
|
bundled = listBundledSkills();
|
|
360
160
|
}
|
|
361
161
|
catch (err) {
|
|
362
|
-
// getSkillsPackageRoot() threw (require.resolve failed — the
|
|
363
|
-
// `@trawlme/skills` package itself isn't resolvable). Report as a
|
|
364
|
-
// diagnostic failure, not the outer catch's generic `null`.
|
|
365
162
|
return {
|
|
366
163
|
installed: [],
|
|
367
164
|
skipped: [],
|
|
@@ -370,10 +167,6 @@ export function bootstrapSkillsOnLogin(opts = {}) {
|
|
|
370
167
|
};
|
|
371
168
|
}
|
|
372
169
|
if (bundled.length === 0) {
|
|
373
|
-
// The package resolved but reports zero skills — in practice this
|
|
374
|
-
// package always ships at least one, so an empty list here is
|
|
375
|
-
// corruption (e.g. its `skills/` dir renamed/deleted underneath it),
|
|
376
|
-
// not a legitimate "nothing to do".
|
|
377
170
|
return {
|
|
378
171
|
installed: [],
|
|
379
172
|
skipped: [],
|
|
@@ -386,9 +179,6 @@ export function bootstrapSkillsOnLogin(opts = {}) {
|
|
|
386
179
|
return getInstalledVersion(name, scope) === null;
|
|
387
180
|
}
|
|
388
181
|
catch {
|
|
389
|
-
// An unreadable `.version` (e.g. EISDIR) is no more proof of
|
|
390
|
-
// ownership than a missing one — treat it as actionable too, same
|
|
391
|
-
// as installSkill's own owned-check does (#91's posture).
|
|
392
182
|
return true;
|
|
393
183
|
}
|
|
394
184
|
});
|
|
@@ -402,11 +192,6 @@ export function bootstrapSkillsOnLogin(opts = {}) {
|
|
|
402
192
|
installed.push(name);
|
|
403
193
|
}
|
|
404
194
|
catch (err) {
|
|
405
|
-
// One bad skill (e.g. a marker-less foreign dir refusing overwrite,
|
|
406
|
-
// or an EACCES on an unwritable skills dir) must not blank the
|
|
407
|
-
// others — skip it, keep going, but never drop WHY silently: the
|
|
408
|
-
// caller needs this to tell "attempted and failed" apart from
|
|
409
|
-
// "nothing to do".
|
|
410
195
|
const reason = err instanceof SkillOwnershipRefusalError
|
|
411
196
|
? err.relayableReason
|
|
412
197
|
: err instanceof Error
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
export declare const SKILLS_NUDGE_TEXT = "Claude skills not installed \u2014 `trawl skills install` installs them (Claude Code must be restarted to see them \u2014 skills are loaded at session start, not mid-session.)";
|
|
2
2
|
export declare const AUTH_WALL_SKILLS_NUDGE_TEXT = "This looks like a login wall and Claude skills are not installed \u2014 `trawl skills install` installs the guidance for it too (Claude Code must be restarted to see them \u2014 skills are loaded at session start, not mid-session.)";
|
|
3
|
-
/**
|
|
4
|
-
* Pure gate — true when the throttled, generic "elsewhere" suggestion
|
|
5
|
-
* should print. Every external signal is a parameter, exactly like
|
|
6
|
-
* isReferralTipDue, so this is testable without mocking fs/env/Date.
|
|
7
|
-
*/
|
|
8
3
|
export declare function isSkillsNudgeDue(opts: {
|
|
9
4
|
json?: boolean;
|
|
10
5
|
isTTY: boolean;
|
|
@@ -13,21 +8,9 @@ export declare function isSkillsNudgeDue(opts: {
|
|
|
13
8
|
lastShownAt: number;
|
|
14
9
|
now: number;
|
|
15
10
|
}): boolean;
|
|
16
|
-
/**
|
|
17
|
-
* Best-effort generic nudge (#184 point 2) — called once per CLI invocation
|
|
18
|
-
* from index.ts's runCli, on every command. Never throws.
|
|
19
|
-
*/
|
|
20
11
|
export declare function maybeSuggestSkillsInstall(opts?: {
|
|
21
12
|
json?: boolean;
|
|
22
13
|
}): void;
|
|
23
|
-
/**
|
|
24
|
-
* Safety-net nudge (#184 point 5) — called from `doctor`/`run-info` right
|
|
25
|
-
* when the run they just showed carries a live `failureKind:'auth'` verdict
|
|
26
|
-
* (see commands/doctor.ts's `isAuthWall`). Deliberately unthrottled by the
|
|
27
|
-
* 7-day timestamp — see this module's doc comment for why — but still
|
|
28
|
-
* capped to one nudge per process via the same `nudgedThisProcess` flag the
|
|
29
|
-
* generic nudge sets. Never throws.
|
|
30
|
-
*/
|
|
31
14
|
export declare function maybeSuggestSkillsForAuthWall(opts?: {
|
|
32
15
|
json?: boolean;
|
|
33
16
|
}): void;
|
package/dist/lib/skillsNudge.js
CHANGED
|
@@ -1,61 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #184 — the "elsewhere: suggest, never install" half of the skills
|
|
3
|
-
* bootstrap story (lib/skills.ts's `bootstrapSkillsOnLogin` is the "install
|
|
4
|
-
* on login" half). Writing into `~/.claude/skills` as a side effect of an
|
|
5
|
-
* unrelated command is a filesystem mutation nobody asked for — this module
|
|
6
|
-
* only ever PRINTS a line naming the command that installs them. Never
|
|
7
|
-
* installs anything itself.
|
|
8
|
-
*
|
|
9
|
-
* Two nudges, one shared "at most once per process" guard:
|
|
10
|
-
* - `maybeSuggestSkillsInstall` — the generic, throttled nudge (once per
|
|
11
|
-
* NUDGE_THROTTLE_MS across ALL commands, mirrors lib/tips.ts's referral
|
|
12
|
-
* tip throttle exactly).
|
|
13
|
-
* - `maybeSuggestSkillsForAuthWall` — the safety-net nudge (#184 point 5):
|
|
14
|
-
* fires from `doctor`/`run-info` when the run they just showed carries a
|
|
15
|
-
* live `failureKind:'auth'` verdict and skills are absent. Deliberately
|
|
16
|
-
* UNTHROTTLED by the 7-day timestamp: sharing that throttle would mean a
|
|
17
|
-
* routine command on day 0 (stamping the generic nudge's timestamp)
|
|
18
|
-
* silently suppresses THIS nudge on day 1 when the user actually hits
|
|
19
|
-
* the auth wall the whole issue exists for — the exact failure this
|
|
20
|
-
* safety net is supposed to catch. It mirrors trawl_cli#182's own
|
|
21
|
-
* login-wall hint in `formatDoctor`, which also prints unthrottled every
|
|
22
|
-
* time the condition is true.
|
|
23
|
-
* - `nudgedThisProcess` still caps the two at "one nudge per invocation"
|
|
24
|
-
* combined: `doctor` on an auth-walled, skills-absent run can print the
|
|
25
|
-
* auth-wall nudge inside its own action, and index.ts's generic
|
|
26
|
-
* post-command nudge (which does not know `doctor` already said
|
|
27
|
-
* something) would otherwise print a second, redundant line right after
|
|
28
|
-
* it. Whichever fires first wins; reset per test via a fresh module
|
|
29
|
-
* import (see skillsNudge.test.ts's `freshImport` helper, mirroring
|
|
30
|
-
* tips.test.ts).
|
|
31
|
-
*
|
|
32
|
-
* Gate shape mirrors lib/tips.ts's isReferralTipDue/maybeShowReferralTip
|
|
33
|
-
* throughout (json/isTTY/opt-out/throttle, pure gate separated from the
|
|
34
|
-
* side-effecting caller, cheap checks before any filesystem read) — same
|
|
35
|
-
* reasoning applies verbatim.
|
|
36
|
-
*/
|
|
37
1
|
import chalk from 'chalk';
|
|
38
2
|
import config from './config.js';
|
|
39
3
|
import { listBundledSkills, isSkillInstalled, isSkillsActionAllowed, RESTART_CLAUDE_CODE_NOTE } from './skills.js';
|
|
40
|
-
/** Max once per 7 days — same window as lib/tips.ts's referral tip. */
|
|
41
4
|
const NUDGE_THROTTLE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
42
|
-
// #184 constraint A — a FACT about CLI state plus the command name, never an
|
|
43
|
-
// instruction to the reader ("install the skills"/"you should run…"). This
|
|
44
|
-
// output is not only read by a human at a terminal: this platform can relay
|
|
45
|
-
// a CLI's own stdout/stderr into an AI agent's context (the incident this
|
|
46
|
-
// issue exists to prevent was exactly an agent driving this CLI), and
|
|
47
|
-
// third-party content returned into agent context must never read as an
|
|
48
|
-
// instruction the agent is being told to obey. State the fact, name the
|
|
49
|
-
// command, stop there.
|
|
50
5
|
export const SKILLS_NUDGE_TEXT = `Claude skills not installed — \`trawl skills install\` installs them (${RESTART_CLAUDE_CODE_NOTE})`;
|
|
51
6
|
export const AUTH_WALL_SKILLS_NUDGE_TEXT = `This looks like a login wall and Claude skills are not installed — \`trawl skills install\` installs the guidance for it too (${RESTART_CLAUDE_CODE_NOTE})`;
|
|
52
|
-
/** One nudge per process, whichever of the two below fires first. */
|
|
53
7
|
let nudgedThisProcess = false;
|
|
54
|
-
/**
|
|
55
|
-
* Pure gate — true when the throttled, generic "elsewhere" suggestion
|
|
56
|
-
* should print. Every external signal is a parameter, exactly like
|
|
57
|
-
* isReferralTipDue, so this is testable without mocking fs/env/Date.
|
|
58
|
-
*/
|
|
59
8
|
export function isSkillsNudgeDue(opts) {
|
|
60
9
|
if (!isSkillsActionAllowed({ json: opts.json, isTTY: opts.isTTY, optedOut: opts.optedOut }))
|
|
61
10
|
return false;
|
|
@@ -63,14 +12,6 @@ export function isSkillsNudgeDue(opts) {
|
|
|
63
12
|
return false;
|
|
64
13
|
return opts.now - opts.lastShownAt >= NUDGE_THROTTLE_MS;
|
|
65
14
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Read-only — checks both scopes, mirrors `trawl skills list`'s own
|
|
68
|
-
* "installed anywhere" question. Failing toward `false` (not installed) on
|
|
69
|
-
* an unreadable skills dir is the safe default here: the worst case is one
|
|
70
|
-
* extra nudge line, never a missed one — the opposite failure (silently
|
|
71
|
-
* assuming "present" and staying quiet) is the exact gap this issue exists
|
|
72
|
-
* to close.
|
|
73
|
-
*/
|
|
74
15
|
function anyBundledSkillInstalled() {
|
|
75
16
|
try {
|
|
76
17
|
return listBundledSkills().some((name) => isSkillInstalled(name, 'user') || isSkillInstalled(name, 'local'));
|
|
@@ -79,15 +20,6 @@ function anyBundledSkillInstalled() {
|
|
|
79
20
|
return false;
|
|
80
21
|
}
|
|
81
22
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Shared body for both exported nudges below: the cheap json/TTY/opt-out
|
|
84
|
-
* gate, the "one nudge per process" cap, the skills-presence check, and the
|
|
85
|
-
* actual print — everything the two nudges have in common. `useThrottle`
|
|
86
|
-
* is the one real difference between them (see the module doc comment for
|
|
87
|
-
* why the safety-net nudge deliberately opts out of it), so it stays an
|
|
88
|
-
* explicit parameter here rather than two near-identical function bodies.
|
|
89
|
-
* Never throws.
|
|
90
|
-
*/
|
|
91
23
|
function tryPrintNudge(opts, text, useThrottle) {
|
|
92
24
|
try {
|
|
93
25
|
if (nudgedThisProcess)
|
|
@@ -95,8 +27,6 @@ function tryPrintNudge(opts, text, useThrottle) {
|
|
|
95
27
|
const json = Boolean(opts.json);
|
|
96
28
|
const isTTY = Boolean(process.stdout.isTTY);
|
|
97
29
|
const optedOut = process.env['TRAWL_SKILLS_SYNC'] === '0';
|
|
98
|
-
// Cheap checks first (mirrors tips.ts #153's "never pay when not due")
|
|
99
|
-
// — only touch the filesystem once json/TTY/opt-out already say "maybe".
|
|
100
30
|
if (!isSkillsActionAllowed({ json, isTTY, optedOut }))
|
|
101
31
|
return;
|
|
102
32
|
if (useThrottle) {
|
|
@@ -114,24 +44,11 @@ function tryPrintNudge(opts, text, useThrottle) {
|
|
|
114
44
|
console.error(chalk.dim(text));
|
|
115
45
|
}
|
|
116
46
|
catch {
|
|
117
|
-
// silent — a broken nudge must never break a command
|
|
118
47
|
}
|
|
119
48
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Best-effort generic nudge (#184 point 2) — called once per CLI invocation
|
|
122
|
-
* from index.ts's runCli, on every command. Never throws.
|
|
123
|
-
*/
|
|
124
49
|
export function maybeSuggestSkillsInstall(opts = {}) {
|
|
125
50
|
tryPrintNudge(opts, SKILLS_NUDGE_TEXT, true);
|
|
126
51
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Safety-net nudge (#184 point 5) — called from `doctor`/`run-info` right
|
|
129
|
-
* when the run they just showed carries a live `failureKind:'auth'` verdict
|
|
130
|
-
* (see commands/doctor.ts's `isAuthWall`). Deliberately unthrottled by the
|
|
131
|
-
* 7-day timestamp — see this module's doc comment for why — but still
|
|
132
|
-
* capped to one nudge per process via the same `nudgedThisProcess` flag the
|
|
133
|
-
* generic nudge sets. Never throws.
|
|
134
|
-
*/
|
|
135
52
|
export function maybeSuggestSkillsForAuthWall(opts = {}) {
|
|
136
53
|
tryPrintNudge(opts, AUTH_WALL_SKILLS_NUDGE_TEXT, false);
|
|
137
54
|
}
|
package/dist/lib/spinner.d.ts
CHANGED
|
@@ -5,45 +5,6 @@ type SpinOptions<T> = string | {
|
|
|
5
5
|
failText?: string | ((error: Error) => string);
|
|
6
6
|
[key: string]: unknown;
|
|
7
7
|
};
|
|
8
|
-
/**
|
|
9
|
-
* Drop-in for `oraPromise` that emits NOTHING when stderr is not a TTY.
|
|
10
|
-
*
|
|
11
|
-
* #119 — under a pipe / non-TTY, `oraPromise` still printed the start text AND
|
|
12
|
-
* a persisted `✔ …` line, so `trawl list | cat` showed the spinner caption
|
|
13
|
-
* twice. An agent/CI never wants spinner chrome; here we just run the action.
|
|
14
|
-
* When stderr IS a TTY, behaviour is identical to `oraPromise` (spinner writes
|
|
15
|
-
* to stderr, stdout stays clean either way).
|
|
16
|
-
*/
|
|
17
8
|
export declare function spin<T>(action: Action<T>, options?: SpinOptions<T>): Promise<T>;
|
|
18
|
-
/**
|
|
19
|
-
* The counterpart to `spin()`'s silence — print a plain-text confirmation on
|
|
20
|
-
* stdout when, and only when, stdout is not a TTY.
|
|
21
|
-
*
|
|
22
|
-
* #160 then #166. `spin()` above deliberately emits nothing when stderr is not
|
|
23
|
-
* a TTY, so for every command whose only non-`--json` feedback was its
|
|
24
|
-
* `successText`, a piped or redirected invocation wrote **zero bytes to both
|
|
25
|
-
* streams while exiting 0**. Our ICP is agent builders, and agents pipe stdout:
|
|
26
|
-
* a verb that writes nothing on success is unusable from a script, because the
|
|
27
|
-
* caller cannot tell success from a no-op. Two of the affected commands were
|
|
28
|
-
* deletes, where that ambiguity is at its worst.
|
|
29
|
-
*
|
|
30
|
-
* Lives here, next to the silence it compensates for, because #160 fixed
|
|
31
|
-
* `trigger` by inlining this rule and #166 then found five siblings carrying
|
|
32
|
-
* the identical defect — five more inlined copies is how the next one gets
|
|
33
|
-
* missed. Callers pass the message; the gate lives in one place.
|
|
34
|
-
*
|
|
35
|
-
* Gated on **stdout**, not stderr: stdout is the stream a script actually reads
|
|
36
|
-
* (`$(trawl …)`, `> out.txt`, `| jq`), independent of whatever `spin()` decides
|
|
37
|
-
* about stderr. So an interactive session, where the ora spinner already
|
|
38
|
-
* confirmed on stderr, never gets a duplicate line here.
|
|
39
|
-
*
|
|
40
|
-
* Never call this on a `--json` path: under `--json`, stdout must stay exactly
|
|
41
|
-
* one parseable document.
|
|
42
|
-
*
|
|
43
|
-
* Known residual, unchanged from #160: stdout attached to a real terminal while
|
|
44
|
-
* stderr is separately redirected still prints nothing on either stream. That is
|
|
45
|
-
* not the reported or common shape (full redirection, or stdout-only capture),
|
|
46
|
-
* and widening the gate would put a duplicate line in front of interactive users.
|
|
47
|
-
*/
|
|
48
9
|
export declare function confirmNonTTY(message: string): void;
|
|
49
10
|
export {};
|
package/dist/lib/spinner.js
CHANGED
|
@@ -1,50 +1,10 @@
|
|
|
1
1
|
import { oraPromise } from 'ora';
|
|
2
|
-
/**
|
|
3
|
-
* Drop-in for `oraPromise` that emits NOTHING when stderr is not a TTY.
|
|
4
|
-
*
|
|
5
|
-
* #119 — under a pipe / non-TTY, `oraPromise` still printed the start text AND
|
|
6
|
-
* a persisted `✔ …` line, so `trawl list | cat` showed the spinner caption
|
|
7
|
-
* twice. An agent/CI never wants spinner chrome; here we just run the action.
|
|
8
|
-
* When stderr IS a TTY, behaviour is identical to `oraPromise` (spinner writes
|
|
9
|
-
* to stderr, stdout stays clean either way).
|
|
10
|
-
*/
|
|
11
2
|
export function spin(action, options) {
|
|
12
3
|
if (!process.stderr.isTTY) {
|
|
13
4
|
return Promise.resolve(typeof action === 'function' ? action() : action);
|
|
14
5
|
}
|
|
15
|
-
// oraPromise's own overloads accept (action, string) and (action, options).
|
|
16
6
|
return oraPromise(action, options);
|
|
17
7
|
}
|
|
18
|
-
/**
|
|
19
|
-
* The counterpart to `spin()`'s silence — print a plain-text confirmation on
|
|
20
|
-
* stdout when, and only when, stdout is not a TTY.
|
|
21
|
-
*
|
|
22
|
-
* #160 then #166. `spin()` above deliberately emits nothing when stderr is not
|
|
23
|
-
* a TTY, so for every command whose only non-`--json` feedback was its
|
|
24
|
-
* `successText`, a piped or redirected invocation wrote **zero bytes to both
|
|
25
|
-
* streams while exiting 0**. Our ICP is agent builders, and agents pipe stdout:
|
|
26
|
-
* a verb that writes nothing on success is unusable from a script, because the
|
|
27
|
-
* caller cannot tell success from a no-op. Two of the affected commands were
|
|
28
|
-
* deletes, where that ambiguity is at its worst.
|
|
29
|
-
*
|
|
30
|
-
* Lives here, next to the silence it compensates for, because #160 fixed
|
|
31
|
-
* `trigger` by inlining this rule and #166 then found five siblings carrying
|
|
32
|
-
* the identical defect — five more inlined copies is how the next one gets
|
|
33
|
-
* missed. Callers pass the message; the gate lives in one place.
|
|
34
|
-
*
|
|
35
|
-
* Gated on **stdout**, not stderr: stdout is the stream a script actually reads
|
|
36
|
-
* (`$(trawl …)`, `> out.txt`, `| jq`), independent of whatever `spin()` decides
|
|
37
|
-
* about stderr. So an interactive session, where the ora spinner already
|
|
38
|
-
* confirmed on stderr, never gets a duplicate line here.
|
|
39
|
-
*
|
|
40
|
-
* Never call this on a `--json` path: under `--json`, stdout must stay exactly
|
|
41
|
-
* one parseable document.
|
|
42
|
-
*
|
|
43
|
-
* Known residual, unchanged from #160: stdout attached to a real terminal while
|
|
44
|
-
* stderr is separately redirected still prints nothing on either stream. That is
|
|
45
|
-
* not the reported or common shape (full redirection, or stdout-only capture),
|
|
46
|
-
* and widening the gate would put a duplicate line in front of interactive users.
|
|
47
|
-
*/
|
|
48
8
|
export function confirmNonTTY(message) {
|
|
49
9
|
if (!process.stdout.isTTY)
|
|
50
10
|
console.log(message);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface RawCdpCookie {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
domain: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
expires?: number;
|
|
7
|
+
httpOnly?: boolean;
|
|
8
|
+
secure?: boolean;
|
|
9
|
+
sameSite?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface CookieData {
|
|
13
|
+
name: string;
|
|
14
|
+
value: string;
|
|
15
|
+
domain: string;
|
|
16
|
+
path?: string;
|
|
17
|
+
secure?: boolean;
|
|
18
|
+
httpOnly?: boolean;
|
|
19
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
20
|
+
expires?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface RawOriginLocalStorage {
|
|
23
|
+
origin: string;
|
|
24
|
+
entries: Array<{
|
|
25
|
+
name: string;
|
|
26
|
+
value: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export interface OriginStorage {
|
|
30
|
+
origin: string;
|
|
31
|
+
localStorage: Array<{
|
|
32
|
+
name: string;
|
|
33
|
+
value: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export interface StorageState {
|
|
37
|
+
cookies: CookieData[];
|
|
38
|
+
origins: OriginStorage[];
|
|
39
|
+
}
|
|
40
|
+
export declare function getTargetHost(targetUrl: string): string;
|
|
41
|
+
export declare function isCookieDomainInScope(rawDomain: string, targetHost: string): boolean;
|
|
42
|
+
export declare function isOriginHostInScope(originHost: string, targetHost: string): boolean;
|
|
43
|
+
export interface MapCookiesResult {
|
|
44
|
+
cookies: CookieData[];
|
|
45
|
+
totalSeen: number;
|
|
46
|
+
droppedOutOfScope: number;
|
|
47
|
+
droppedInvalid: number;
|
|
48
|
+
}
|
|
49
|
+
export declare function mapCookies(rawCookies: RawCdpCookie[], targetUrl: string): MapCookiesResult;
|
|
50
|
+
export interface MapOriginsResult {
|
|
51
|
+
origins: OriginStorage[];
|
|
52
|
+
totalSeen: number;
|
|
53
|
+
droppedOutOfScope: number;
|
|
54
|
+
}
|
|
55
|
+
export declare function mapOrigins(rawOrigins: RawOriginLocalStorage[], targetUrl: string): MapOriginsResult;
|