@trawlme/cli 3.12.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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/create.d.ts +0 -28
  3. package/dist/commands/create.js +0 -89
  4. package/dist/commands/doctor.d.ts +0 -79
  5. package/dist/commands/doctor.js +1 -187
  6. package/dist/commands/login.js +0 -67
  7. package/dist/commands/ping.d.ts +0 -15
  8. package/dist/commands/ping.js +0 -15
  9. package/dist/commands/scraps.d.ts +0 -120
  10. package/dist/commands/scraps.js +10 -724
  11. package/dist/commands/skills.js +0 -22
  12. package/dist/commands/spec.d.ts +0 -85
  13. package/dist/commands/spec.js +0 -67
  14. package/dist/commands/telemetry.js +0 -4
  15. package/dist/commands/token.js +0 -28
  16. package/dist/commands/upgrade.js +0 -22
  17. package/dist/commands/whoami.d.ts +0 -12
  18. package/dist/commands/whoami.js +0 -6
  19. package/dist/index.d.ts +0 -188
  20. package/dist/index.js +0 -349
  21. package/dist/lib/api.d.ts +0 -78
  22. package/dist/lib/api.js +1 -320
  23. package/dist/lib/cdp-pipe.d.ts +0 -72
  24. package/dist/lib/cdp-pipe.js +1 -81
  25. package/dist/lib/chrome-discovery.d.ts +0 -11
  26. package/dist/lib/chrome-discovery.js +0 -19
  27. package/dist/lib/chrome-launch.d.ts +0 -40
  28. package/dist/lib/chrome-launch.js +0 -69
  29. package/dist/lib/config.d.ts +0 -53
  30. package/dist/lib/config.js +0 -55
  31. package/dist/lib/confirm.d.ts +0 -55
  32. package/dist/lib/confirm.js +0 -47
  33. package/dist/lib/docs.d.ts +0 -123
  34. package/dist/lib/docs.js +0 -169
  35. package/dist/lib/errors.d.ts +0 -134
  36. package/dist/lib/errors.js +0 -151
  37. package/dist/lib/format.d.ts +0 -6
  38. package/dist/lib/format.js +0 -6
  39. package/dist/lib/json.d.ts +0 -35
  40. package/dist/lib/json.js +0 -48
  41. package/dist/lib/jwt.d.ts +0 -7
  42. package/dist/lib/jwt.js +0 -7
  43. package/dist/lib/pinch.d.ts +0 -53
  44. package/dist/lib/pinch.js +6 -112
  45. package/dist/lib/pinchAnimation.d.ts +0 -16
  46. package/dist/lib/pinchAnimation.js +8 -29
  47. package/dist/lib/posthog.d.ts +0 -9
  48. package/dist/lib/posthog.js +0 -23
  49. package/dist/lib/prompt.js +1 -20
  50. package/dist/lib/secure-transport.d.ts +0 -7
  51. package/dist/lib/secure-transport.js +0 -24
  52. package/dist/lib/session-capture-guard.d.ts +0 -15
  53. package/dist/lib/session-capture-guard.js +0 -5
  54. package/dist/lib/session-capture.d.ts +0 -125
  55. package/dist/lib/session-capture.js +0 -281
  56. package/dist/lib/skills.d.ts +0 -175
  57. package/dist/lib/skills.js +1 -216
  58. package/dist/lib/skillsNudge.d.ts +0 -17
  59. package/dist/lib/skillsNudge.js +0 -83
  60. package/dist/lib/spinner.d.ts +0 -39
  61. package/dist/lib/spinner.js +0 -40
  62. package/dist/lib/storage-state.d.ts +0 -112
  63. package/dist/lib/storage-state.js +0 -131
  64. package/dist/lib/tips.d.ts +0 -38
  65. package/dist/lib/tips.js +0 -77
  66. package/dist/lib/updateCheckWorker.js +0 -14
  67. package/dist/lib/updateNotifier.d.ts +0 -17
  68. package/dist/lib/updateNotifier.js +0 -53
  69. package/dist/lib/validate.d.ts +0 -8
  70. package/dist/lib/validate.js +0 -8
  71. package/dist/lib/version.d.ts +0 -12
  72. package/dist/lib/version.js +1 -13
  73. package/package.json +2 -2
@@ -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;
@@ -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; // raced away / unreadable — nothing to clean
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;