@trawlme/cli 1.7.2 → 1.9.0

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 CHANGED
@@ -56,6 +56,21 @@ trawl scraps account clear-session <id>
56
56
  trawl scraps account status <id> [--json]
57
57
  ```
58
58
 
59
+ ### Claude Code skills
60
+
61
+ The CLI bundles a Claude Code skill that teaches Claude how to use `trawl`. Once installed, Claude can manage scraps for you via prompts.
62
+
63
+ ```
64
+ trawl skills list List bundled skills and install status
65
+ trawl skills install [<skill>] [--local] Install all (or one). Default: ~/.claude/skills/
66
+ trawl skills uninstall [<skill>] [--local] Remove
67
+ trawl skills update [<skill>] [--local] Reinstall (force sync with CLI version)
68
+ ```
69
+
70
+ Skills auto-update silently when you upgrade the CLI — no need to re-install manually.
71
+
72
+ You can also install skills standalone (without the CLI): `npx @trawlme/skills install`.
73
+
59
74
  ## Environment variables
60
75
 
61
76
  | Variable | Description |
@@ -98,14 +98,14 @@ scraps
98
98
  .requiredOption('-t, --title <title>', 'Scrap title')
99
99
  .option('-u, --url <url>', 'Target URL')
100
100
  .option('-r, --request <request>', 'Request/query')
101
- .option('-s, --scrapper <scrapper>', 'Scrapper type', 'puppeteer')
101
+ .option('-d, --description <text>', 'Scrap description')
102
102
  .action(async (opts) => {
103
103
  const spinner = ora('Creating scrap…').start();
104
104
  const data = await api.post('/api/scraps', {
105
105
  title: opts.title,
106
106
  ...(opts.url && { url: opts.url }),
107
107
  request: opts.request || '',
108
- scrapper: opts.scrapper,
108
+ ...(opts.description !== undefined && { description: opts.description }),
109
109
  });
110
110
  spinner.succeed(`Scrap created: ${chalk.bold(data._id)}`);
111
111
  console.log(chalk.dim(` Title: ${data.title}`));
@@ -117,7 +117,7 @@ scraps
117
117
  .option('-t, --title <title>', 'New title')
118
118
  .option('-u, --url <url>', 'New target URL')
119
119
  .option('-r, --request <request>', 'New request')
120
- .option('-s, --scrapper <scrapper>', 'New scrapper type')
120
+ .option('-d, --description <text>', 'New description')
121
121
  .option('--cron <expression>', 'Cron expression (empty string to disable)')
122
122
  .option('--no-cron', 'Disable cron (set to null)')
123
123
  .option('--alert <email>', 'Failure alert email (empty string to clear)')
@@ -133,8 +133,8 @@ scraps
133
133
  body.url = opts.url;
134
134
  if (opts.request !== undefined)
135
135
  body.request = opts.request;
136
- if (opts.scrapper !== undefined)
137
- body.scrapper = opts.scrapper;
136
+ if (opts.description !== undefined)
137
+ body.description = opts.description;
138
138
  if (opts.cron === false)
139
139
  body.cron = null;
140
140
  else if (typeof opts.cron === 'string')
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const skills: Command;
@@ -0,0 +1,83 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { listBundledSkills, installSkill, uninstallSkill, getBundledSkillsVersion, getInstalledVersion, isSkillInstalled, } from '../lib/skills.js';
4
+ function pickScope(opts) {
5
+ return opts.local ? 'local' : 'user';
6
+ }
7
+ function pickSkills(arg) {
8
+ const all = listBundledSkills();
9
+ if (!arg || arg === 'all')
10
+ return all;
11
+ if (!all.includes(arg)) {
12
+ throw new Error(`Unknown skill "${arg}". Available: ${all.join(', ') || '(none)'}`);
13
+ }
14
+ return [arg];
15
+ }
16
+ export const skills = new Command('skills').description('Manage Claude Code skills bundled with the Trawl CLI');
17
+ skills
18
+ .command('list')
19
+ .description('List bundled skills and their installed status')
20
+ .action(() => {
21
+ const bundled = listBundledSkills();
22
+ if (!bundled.length) {
23
+ console.log(chalk.dim('No bundled skills.'));
24
+ return;
25
+ }
26
+ const version = getBundledSkillsVersion();
27
+ console.log(chalk.dim(`@trawlme/skills@${version}\n`));
28
+ for (const name of bundled) {
29
+ const userInstalled = isSkillInstalled(name, 'user');
30
+ const localInstalled = isSkillInstalled(name, 'local');
31
+ const installedVersion = getInstalledVersion(name, 'user') ?? getInstalledVersion(name, 'local');
32
+ const stale = installedVersion && installedVersion !== version;
33
+ const tag = userInstalled
34
+ ? localInstalled
35
+ ? '(user + local)'
36
+ : '(user)'
37
+ : localInstalled
38
+ ? '(local)'
39
+ : '(not installed)';
40
+ const staleNote = stale ? chalk.yellow(` outdated: ${installedVersion} → ${version}`) : '';
41
+ console.log(` ${name} ${chalk.dim(tag)}${staleNote}`);
42
+ }
43
+ });
44
+ skills
45
+ .command('install [skill]')
46
+ .description('Install one or all bundled skills')
47
+ .option('--local', 'Install at project level (./.claude/skills) instead of user level (~/.claude/skills)')
48
+ .action((skill, opts) => {
49
+ const scope = pickScope(opts);
50
+ const targets = pickSkills(skill);
51
+ for (const name of targets) {
52
+ const dest = installSkill(name, scope);
53
+ console.log(chalk.green(`✓ Installed "${name}"`) + chalk.dim(` at ${dest}`));
54
+ }
55
+ console.log(chalk.dim(' Restart Claude Code if it was already running.'));
56
+ });
57
+ skills
58
+ .command('uninstall [skill]')
59
+ .description('Remove one or all bundled skills')
60
+ .option('--local', 'Remove from project level (./.claude/skills)')
61
+ .action((skill, opts) => {
62
+ const scope = pickScope(opts);
63
+ const targets = pickSkills(skill);
64
+ for (const name of targets) {
65
+ const dest = uninstallSkill(name, scope);
66
+ if (dest)
67
+ console.log(chalk.green(`✓ Removed "${name}"`) + chalk.dim(` from ${dest}`));
68
+ else
69
+ console.log(chalk.dim(` No "${name}" found`));
70
+ }
71
+ });
72
+ skills
73
+ .command('update [skill]')
74
+ .description('Reinstall over the existing skill (force sync with CLI version)')
75
+ .option('--local', 'Update at project level')
76
+ .action((skill, opts) => {
77
+ const scope = pickScope(opts);
78
+ const targets = pickSkills(skill);
79
+ for (const name of targets) {
80
+ const dest = installSkill(name, scope);
81
+ console.log(chalk.green(`✓ Updated "${name}"`) + chalk.dim(` at ${dest}`));
82
+ }
83
+ });
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@ import { Command } from 'commander';
3
3
  import chalk from 'chalk';
4
4
  import { login, logout } from './commands/login.js';
5
5
  import { scraps } from './commands/scraps.js';
6
+ import { skills } from './commands/skills.js';
7
+ import { autoUpdateInstalledSkills } from './lib/skills.js';
8
+ autoUpdateInstalledSkills();
6
9
  const program = new Command()
7
10
  .name('trawl')
8
11
  .description('Trawl CLI — manage scraps from the terminal')
@@ -11,6 +14,7 @@ const program = new Command()
11
14
  program.addCommand(login);
12
15
  program.addCommand(logout);
13
16
  program.addCommand(scraps);
17
+ program.addCommand(skills);
14
18
  program.parseAsync().catch((err) => {
15
19
  const { debug } = program.opts();
16
20
  if (debug || process.env['DEBUG']) {
@@ -0,0 +1,12 @@
1
+ export declare function getBundledSkillsVersion(): string;
2
+ export declare function listBundledSkills(): string[];
3
+ export declare function installSkill(name: string, scope: 'user' | 'local'): string;
4
+ export declare function uninstallSkill(name: string, scope: 'user' | 'local'): string | null;
5
+ export declare function getInstalledVersion(name: string, scope: 'user' | 'local'): string | null;
6
+ export declare function isSkillInstalled(name: string, scope: 'user' | 'local'): boolean;
7
+ /**
8
+ * Silently re-installs any skill whose installed version doesn't match the bundled one.
9
+ * Called on CLI startup to keep skills in sync with the CLI version.
10
+ * Never throws — failures are silent so they don't break unrelated commands.
11
+ */
12
+ export declare function autoUpdateInstalledSkills(): void;
@@ -0,0 +1,77 @@
1
+ import { mkdirSync, existsSync, rmSync, readFileSync, writeFileSync, readdirSync, statSync, cpSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { createRequire } from 'node:module';
5
+ const require = createRequire(import.meta.url);
6
+ function getSkillsPackageRoot() {
7
+ const pkgJson = require.resolve('@trawlme/skills/package.json');
8
+ return dirname(pkgJson);
9
+ }
10
+ export function getBundledSkillsVersion() {
11
+ const pkg = JSON.parse(readFileSync(join(getSkillsPackageRoot(), 'package.json'), 'utf8'));
12
+ return pkg.version;
13
+ }
14
+ export function listBundledSkills() {
15
+ const src = join(getSkillsPackageRoot(), 'skills');
16
+ if (!existsSync(src))
17
+ return [];
18
+ return readdirSync(src).filter((name) => {
19
+ const dir = join(src, name);
20
+ return statSync(dir).isDirectory() && existsSync(join(dir, 'SKILL.md'));
21
+ });
22
+ }
23
+ function getSkillsBase(scope) {
24
+ const base = scope === 'local' ? join(process.cwd(), '.claude') : join(homedir(), '.claude');
25
+ return join(base, 'skills');
26
+ }
27
+ export function installSkill(name, scope) {
28
+ const src = join(getSkillsPackageRoot(), 'skills', name);
29
+ if (!existsSync(src)) {
30
+ throw new Error(`Skill "${name}" not found in @trawlme/skills`);
31
+ }
32
+ const dest = join(getSkillsBase(scope), name);
33
+ if (existsSync(dest))
34
+ rmSync(dest, { recursive: true, force: true });
35
+ mkdirSync(dest, { recursive: true });
36
+ cpSync(src, dest, { recursive: true });
37
+ writeFileSync(join(dest, '.version'), getBundledSkillsVersion(), 'utf8');
38
+ return dest;
39
+ }
40
+ export function uninstallSkill(name, scope) {
41
+ const dest = join(getSkillsBase(scope), name);
42
+ if (!existsSync(dest))
43
+ return null;
44
+ rmSync(dest, { recursive: true, force: true });
45
+ return dest;
46
+ }
47
+ export function getInstalledVersion(name, scope) {
48
+ const versionFile = join(getSkillsBase(scope), name, '.version');
49
+ if (!existsSync(versionFile))
50
+ return null;
51
+ return readFileSync(versionFile, 'utf8').trim();
52
+ }
53
+ export function isSkillInstalled(name, scope) {
54
+ return existsSync(join(getSkillsBase(scope), name));
55
+ }
56
+ /**
57
+ * Silently re-installs any skill whose installed version doesn't match the bundled one.
58
+ * Called on CLI startup to keep skills in sync with the CLI version.
59
+ * Never throws — failures are silent so they don't break unrelated commands.
60
+ */
61
+ export function autoUpdateInstalledSkills() {
62
+ try {
63
+ const bundledVersion = getBundledSkillsVersion();
64
+ for (const name of listBundledSkills()) {
65
+ for (const scope of ['user', 'local']) {
66
+ if (!isSkillInstalled(name, scope))
67
+ continue;
68
+ if (getInstalledVersion(name, scope) === bundledVersion)
69
+ continue;
70
+ installSkill(name, scope);
71
+ }
72
+ }
73
+ }
74
+ catch {
75
+ // Silent: skill auto-update should never block the CLI
76
+ }
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trawlme/cli",
3
- "version": "1.7.2",
3
+ "version": "1.9.0",
4
4
  "description": "Trawl CLI — manage scraps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,6 +40,7 @@
40
40
  "url": "https://github.com/comes-io/trawl_cli/issues"
41
41
  },
42
42
  "dependencies": {
43
+ "@trawlme/skills": "^1.0.0",
43
44
  "chalk": "^5.6.2",
44
45
  "commander": "^14.0.3",
45
46
  "conf": "^15.1.0",