skillfish 1.0.35 → 1.0.37

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
@@ -401,9 +401,29 @@ See [CHANGELOG.md](CHANGELOG.md) for release history.
401
401
  <details>
402
402
  <summary>Telemetry</summary>
403
403
 
404
- Anonymous, aggregate install counts only. No PII collected.
404
+ Anonymous, aggregate usage data no PII, no identifiers, no IP fingerprinting on our side.
405
405
 
406
- To opt out: `DO_NOT_TRACK=1` or `CI=true`.
406
+ **What we send**
407
+
408
+ - `command` events (one per CLI invocation): the subcommand name (`add`, `install`, `list`, etc.)
409
+ - `install` events (one per successful skill install): the skill repo (`owner/repo`), the skill name, and the platform names it was installed to (e.g. `Claude Code`, `Cursor`)
410
+
411
+ That's it. No usernames, no machine IDs, no file paths, no skill contents.
412
+
413
+ **How to opt out**
414
+
415
+ Set either env var to any non-falsy value:
416
+
417
+ ```bash
418
+ export DO_NOT_TRACK=1 # https://consoledonottrack.com/
419
+ export CI=true # already set in most CI environments
420
+ ```
421
+
422
+ Telemetry is also automatically disabled when running from source (e.g. `tsx`, `npm test`).
423
+
424
+ **How it's sent**
425
+
426
+ Each event is dispatched to a detached background process and the CLI returns immediately, so telemetry can never block your terminal. If our server is down or slow, your CLI still exits instantly.
407
427
 
408
428
  </details>
409
429
 
@@ -72,7 +72,7 @@ Examples:
72
72
  p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`v${version}`)}`);
73
73
  }
74
74
  // Track command usage (fire and forget)
75
- void trackCommand('add');
75
+ trackCommand('add');
76
76
  const force = options.force ?? false;
77
77
  const trustSource = options.yes ?? false;
78
78
  const installAll = options.all ?? false;
@@ -263,9 +263,12 @@ Examples:
263
263
  }
264
264
  totalInstalled += result.installed.length;
265
265
  totalSkipped += result.skipped.length;
266
- // Track successful installs (fire and forget)
266
+ // Track successful installs (fire and forget — dispatched to detached worker)
267
267
  if (result.installed.length > 0) {
268
- void trackInstall('add', owner, repo, skillName);
268
+ // skillPath is 'SKILL.md' for root skills, or a directory like
269
+ // 'skills/foo' for sub-skills in a monorepo.
270
+ const skillRepoPath = skillPath === SKILL_FILENAME ? undefined : skillPath;
271
+ trackInstall('add', owner, repo, skillName, result.installed.map((i) => i.agent), skillRepoPath);
269
272
  }
270
273
  }
271
274
  // Summary
@@ -78,7 +78,7 @@ Examples:
78
78
  p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`v${version}`)}`);
79
79
  }
80
80
  // Track command usage (fire and forget)
81
- void trackCommand('bundle');
81
+ trackCommand('bundle');
82
82
  // Determine scope (interactive if no flags specified)
83
83
  const { baseDir, location } = await selectBundleLocation(projectFlag, globalFlag, jsonMode);
84
84
  const manifestPath = getProjectManifestPath(location === 'global');
@@ -161,7 +161,7 @@ Examples:
161
161
  p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`v${version}`)} ${pc.dim('· Create a skill')}`);
162
162
  }
163
163
  // Track command usage (fire and forget)
164
- void trackCommand('init');
164
+ trackCommand('init');
165
165
  const skipPrompts = options.yes ?? false;
166
166
  const projectFlag = options.project ?? false;
167
167
  const globalFlag = options.global ?? false;
@@ -87,7 +87,7 @@ Examples:
87
87
  p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`v${version}`)}`);
88
88
  }
89
89
  // Track command usage (fire and forget)
90
- void trackCommand('install');
90
+ trackCommand('install');
91
91
  // Determine scope (interactive if no flags specified)
92
92
  const { location, baseDir, manifestPath } = await selectInstallLocation(projectFlag, globalFlag, jsonMode);
93
93
  jsonOutput.manifest_path = manifestPath;
@@ -473,6 +473,7 @@ Examples:
473
473
  skillName,
474
474
  success: false,
475
475
  installCount: 0,
476
+ platform: [],
476
477
  errorMsg: result.failureReason,
477
478
  };
478
479
  }
@@ -493,6 +494,7 @@ Examples:
493
494
  skillName,
494
495
  success: true,
495
496
  installCount: result.installed.length,
497
+ platform: result.installed.map((i) => i.agent),
496
498
  };
497
499
  }
498
500
  catch (err) {
@@ -518,6 +520,7 @@ Examples:
518
520
  skillName,
519
521
  success: false,
520
522
  installCount: 0,
523
+ platform: [],
521
524
  errorMsg,
522
525
  };
523
526
  }
@@ -534,8 +537,8 @@ Examples:
534
537
  const action = toInstall[i];
535
538
  if (result.success) {
536
539
  successCount++;
537
- // Track successful installs (fire and forget)
538
- void trackInstall('install', action.entry.owner, action.entry.repo, result.skillName);
540
+ // Track successful installs (fire and forget — dispatched to detached worker)
541
+ trackInstall('install', action.entry.owner, action.entry.repo, result.skillName, result.platform, action.entry.path);
539
542
  if (!jsonMode) {
540
543
  // Show which agents it was installed to if it's a partial install
541
544
  const agentCount = action.targetAgents.length;
@@ -65,7 +65,7 @@ Examples:
65
65
  });
66
66
  }
67
67
  // Track command usage (fire and forget)
68
- void trackCommand('list');
68
+ trackCommand('list');
69
69
  // Determine which locations to check
70
70
  // By default, check both global and project. Flags narrow it down.
71
71
  const checkGlobal = !projectFlag; // Check global unless --project is set
@@ -70,7 +70,7 @@ Examples:
70
70
  p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`v${version}`)}`);
71
71
  }
72
72
  // Track command usage (fire and forget)
73
- void trackCommand('remove');
73
+ trackCommand('remove');
74
74
  const skipConfirm = options.yes ?? false;
75
75
  const removeAll = options.all ?? false;
76
76
  const projectFlag = options.project ?? false;
@@ -71,7 +71,7 @@ Examples:
71
71
  p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim('Search')}`);
72
72
  }
73
73
  // Track command usage (fire and forget)
74
- void trackCommand('search');
74
+ trackCommand('search');
75
75
  // Show spinner while searching
76
76
  let response;
77
77
  if (!jsonMode) {
@@ -61,7 +61,7 @@ Examples:
61
61
  p.intro(`${pc.bgCyan(pc.black(' skillfish submit '))} ${pc.dim(`v${version}`)}`);
62
62
  }
63
63
  // Track command usage (fire and forget)
64
- void trackCommand('submit');
64
+ trackCommand('submit');
65
65
  const skipConfirm = options.yes ?? false;
66
66
  // Parse repo format - supports owner/repo or full GitHub URL
67
67
  let owner;
@@ -60,7 +60,7 @@ Examples:
60
60
  p.intro(`${pc.bgCyan(pc.black(' skillfish '))} ${pc.dim(`v${version}`)}`);
61
61
  }
62
62
  // Track command usage (fire and forget)
63
- void trackCommand('update');
63
+ trackCommand('update');
64
64
  // Detect agents (check both global and project for updates)
65
65
  const detected = getDetectedAgentsForLocation('both', process.cwd());
66
66
  if (detected.length === 0) {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Detached telemetry worker. Reads a JSON payload from stdin, POSTs it to the
3
+ * telemetry endpoint, then exits. Intended to be spawned by `telemetry.ts` so
4
+ * the parent CLI can exit immediately without waiting on the network request.
5
+ *
6
+ * Failures are swallowed silently — telemetry must never surface to the user.
7
+ */
8
+ export {};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Detached telemetry worker. Reads a JSON payload from stdin, POSTs it to the
3
+ * telemetry endpoint, then exits. Intended to be spawned by `telemetry.ts` so
4
+ * the parent CLI can exit immediately without waiting on the network request.
5
+ *
6
+ * Failures are swallowed silently — telemetry must never surface to the user.
7
+ */
8
+ const TELEMETRY_URL = 'https://mcpmarket.com/api/telemetry';
9
+ const TELEMETRY_TIMEOUT = 5000;
10
+ async function main() {
11
+ try {
12
+ const chunks = [];
13
+ for await (const chunk of process.stdin) {
14
+ chunks.push(chunk);
15
+ }
16
+ const body = Buffer.concat(chunks).toString('utf-8');
17
+ if (!body)
18
+ return;
19
+ const controller = new AbortController();
20
+ const timeoutId = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT);
21
+ try {
22
+ await fetch(TELEMETRY_URL, {
23
+ method: 'POST',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body,
26
+ signal: controller.signal,
27
+ });
28
+ }
29
+ catch {
30
+ // network/timeout — ignore
31
+ }
32
+ finally {
33
+ clearTimeout(timeoutId);
34
+ }
35
+ }
36
+ catch {
37
+ // stdin/parse error — ignore
38
+ }
39
+ }
40
+ void main().finally(() => process.exit(0));
41
+ export {};
@@ -1,17 +1,32 @@
1
1
  /**
2
- * Track a command execution. Fire and forget.
2
+ * Anonymous CLI usage telemetry.
3
+ *
4
+ * Telemetry is dispatched to a detached child process (`telemetry-worker.js`)
5
+ * that owns the HTTP request and survives the parent's exit. This means:
6
+ * - The CLI returns to the user immediately, never blocked by network I/O.
7
+ * - `process.exit()` in command code does not abort the in-flight POST.
8
+ *
9
+ * Disabled when `DO_NOT_TRACK=1` or `CI=true`. Also disabled when the module
10
+ * is loaded from TypeScript source (dev/test via tsx) since the compiled
11
+ * worker only exists in `dist/`.
12
+ */
13
+ /**
14
+ * Track a command execution. Fire-and-forget; returns immediately.
3
15
  *
4
16
  * @param command The command name (e.g., 'add', 'bundle', 'install')
5
- * @returns Promise that resolves when telemetry is sent (or times out)
6
17
  */
7
- export declare function trackCommand(command: string): Promise<void>;
18
+ export declare function trackCommand(command: string): void;
8
19
  /**
9
- * Track a skill install. Inserts into telemetry_events and increments skill download count.
20
+ * Track a skill install. Fire-and-forget; returns immediately.
21
+ * Inserts into telemetry_events and increments skill download count.
10
22
  *
11
23
  * @param command The command that triggered the install ('add' or 'install')
12
24
  * @param owner GitHub repository owner
13
25
  * @param repo GitHub repository name
14
26
  * @param skillName Name of the skill being installed
15
- * @returns Promise that resolves when telemetry is sent (or times out)
27
+ * @param platform Names of the agents the skill was installed to (e.g. ['Claude Code', 'Cursor'])
28
+ * @param path Path to the skill within the repo. Undefined for root-level
29
+ * skills (whole repo is one skill). For monorepos, this disambiguates
30
+ * sibling skills (e.g. 'skills/council' vs 'skills/marketing').
16
31
  */
17
- export declare function trackInstall(command: string, owner: string, repo: string, skillName: string): Promise<void>;
32
+ export declare function trackInstall(command: string, owner: string, repo: string, skillName: string, platform?: readonly string[], path?: string): void;
package/dist/telemetry.js CHANGED
@@ -1,61 +1,101 @@
1
- const TELEMETRY_URL = 'https://mcpmarket.com/api/telemetry';
2
- /** Timeout for telemetry requests (ms) */
3
- const TELEMETRY_TIMEOUT = 5000;
4
1
  /**
5
- * Send a telemetry payload. Returns a promise that resolves when the request
6
- * completes (or times out). Never rejects.
2
+ * Anonymous CLI usage telemetry.
3
+ *
4
+ * Telemetry is dispatched to a detached child process (`telemetry-worker.js`)
5
+ * that owns the HTTP request and survives the parent's exit. This means:
6
+ * - The CLI returns to the user immediately, never blocked by network I/O.
7
+ * - `process.exit()` in command code does not abort the in-flight POST.
8
+ *
9
+ * Disabled when `DO_NOT_TRACK=1` or `CI=true`. Also disabled when the module
10
+ * is loaded from TypeScript source (dev/test via tsx) since the compiled
11
+ * worker only exists in `dist/`.
12
+ */
13
+ import { spawn } from 'child_process';
14
+ import { fileURLToPath } from 'url';
15
+ /**
16
+ * Treat any non-empty value other than "0"/"false" as "disabled". This matches
17
+ * the de-facto behavior of the consoledonottrack.com convention used by other
18
+ * CLI tools — `DO_NOT_TRACK=true`, `DO_NOT_TRACK=yes`, etc. all disable.
7
19
  */
8
- function sendTelemetry(payload) {
20
+ function isTruthyEnv(value) {
21
+ if (!value)
22
+ return false;
23
+ const normalized = value.trim().toLowerCase();
24
+ return normalized !== '' && normalized !== '0' && normalized !== 'false';
25
+ }
26
+ function isTelemetryDisabled() {
27
+ return isTruthyEnv(process.env.DO_NOT_TRACK) || isTruthyEnv(process.env.CI);
28
+ }
29
+ function dispatch(payload) {
30
+ if (isTelemetryDisabled())
31
+ return;
32
+ // The worker only ships as a compiled .js artifact. When loaded from .ts
33
+ // source (tsx in dev/test), there is no worker to spawn — skip silently.
34
+ if (import.meta.url.endsWith('.ts'))
35
+ return;
9
36
  try {
10
- if (process.env.DO_NOT_TRACK === '1' || process.env.CI === 'true') {
11
- return Promise.resolve();
12
- }
13
- const controller = new AbortController();
14
- const timeoutId = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT);
15
- return fetch(TELEMETRY_URL, {
16
- method: 'POST',
17
- headers: { 'Content-Type': 'application/json' },
18
- body: JSON.stringify(payload),
19
- signal: controller.signal,
20
- })
21
- .then(() => { })
22
- .catch(() => { })
23
- .finally(() => clearTimeout(timeoutId));
37
+ const workerPath = fileURLToPath(new URL('./telemetry-worker.js', import.meta.url));
38
+ const child = spawn(process.execPath, [workerPath], {
39
+ detached: true,
40
+ stdio: ['pipe', 'ignore', 'ignore'],
41
+ windowsHide: true,
42
+ });
43
+ // Swallow spawn errors (ENOENT, EPERM, etc.) — telemetry must not surface.
44
+ child.on('error', () => { });
45
+ // Detach from the parent's reference count so process.exit() doesn't wait.
46
+ child.unref();
47
+ // Small JSON payloads fit comfortably in the kernel pipe buffer, so this
48
+ // write completes synchronously and the child can read it after the parent
49
+ // exits.
50
+ child.stdin?.end(JSON.stringify(payload));
24
51
  }
25
52
  catch {
26
- return Promise.resolve();
53
+ // ignore
27
54
  }
28
55
  }
29
56
  /**
30
- * Track a command execution. Fire and forget.
57
+ * Track a command execution. Fire-and-forget; returns immediately.
31
58
  *
32
59
  * @param command The command name (e.g., 'add', 'bundle', 'install')
33
- * @returns Promise that resolves when telemetry is sent (or times out)
34
60
  */
35
61
  export function trackCommand(command) {
36
62
  if (!command)
37
- return Promise.resolve();
38
- return sendTelemetry({ event_type: 'command', command });
63
+ return;
64
+ dispatch({ event_type: 'command', command });
39
65
  }
40
66
  /**
41
- * Track a skill install. Inserts into telemetry_events and increments skill download count.
67
+ * Track a skill install. Fire-and-forget; returns immediately.
68
+ * Inserts into telemetry_events and increments skill download count.
42
69
  *
43
70
  * @param command The command that triggered the install ('add' or 'install')
44
71
  * @param owner GitHub repository owner
45
72
  * @param repo GitHub repository name
46
73
  * @param skillName Name of the skill being installed
47
- * @returns Promise that resolves when telemetry is sent (or times out)
74
+ * @param platform Names of the agents the skill was installed to (e.g. ['Claude Code', 'Cursor'])
75
+ * @param path Path to the skill within the repo. Undefined for root-level
76
+ * skills (whole repo is one skill). For monorepos, this disambiguates
77
+ * sibling skills (e.g. 'skills/council' vs 'skills/marketing').
48
78
  */
49
- export function trackInstall(command, owner, repo, skillName) {
79
+ export function trackInstall(command, owner, repo, skillName, platform = [], path) {
50
80
  if (!command || !owner || !repo || !skillName)
51
- return Promise.resolve();
52
- return sendTelemetry({
81
+ return;
82
+ // Canonical skill key: 'owner/repo' for root skills, 'owner/repo/path'
83
+ // for skills nested in a monorepo. Without the path component, every
84
+ // skill in a monorepo collapses to the same key.
85
+ const normalizedPath = path?.trim().replace(/^\/+|\/+$/g, '') || undefined;
86
+ const skillKey = normalizedPath ? `${owner}/${repo}/${normalizedPath}` : `${owner}/${repo}`;
87
+ // platform is a single text column on telemetry_events. We send a
88
+ // comma-separated, de-duplicated list so one install stays one row
89
+ // (preserving download-count semantics). Agent names contain no commas.
90
+ const platformText = Array.from(new Set(platform)).join(', ');
91
+ dispatch({
53
92
  event_type: 'install',
54
93
  command,
55
- skill_key: `${owner}/${repo}`,
94
+ skill_key: skillKey,
56
95
  // Fields for skill count increment
57
96
  owner,
58
97
  repo,
59
- skillName,
98
+ skill_name: skillName,
99
+ platform: platformText,
60
100
  });
61
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillfish",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "All in one Skill manager for AI coding agents. Install, update, and sync Skills across Claude Code, Cursor, Copilot + more.",
5
5
  "type": "module",
6
6
  "bin": {