@skillsmith/cli 0.6.1 → 0.6.3

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 (69) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/src/commands/index.d.ts +1 -0
  4. package/dist/src/commands/index.d.ts.map +1 -1
  5. package/dist/src/commands/index.js +2 -0
  6. package/dist/src/commands/index.js.map +1 -1
  7. package/dist/src/commands/info.js +1 -1
  8. package/dist/src/commands/install.js +1 -1
  9. package/dist/src/commands/install.js.map +1 -1
  10. package/dist/src/commands/search.d.ts.map +1 -1
  11. package/dist/src/commands/search.helpers.d.ts +29 -0
  12. package/dist/src/commands/search.helpers.d.ts.map +1 -1
  13. package/dist/src/commands/search.helpers.js +39 -1
  14. package/dist/src/commands/search.helpers.js.map +1 -1
  15. package/dist/src/commands/search.js +26 -2
  16. package/dist/src/commands/search.js.map +1 -1
  17. package/dist/src/commands/search.test.d.ts +10 -0
  18. package/dist/src/commands/search.test.d.ts.map +1 -0
  19. package/dist/src/commands/search.test.js +179 -0
  20. package/dist/src/commands/search.test.js.map +1 -0
  21. package/dist/src/commands/sync.d.ts.map +1 -1
  22. package/dist/src/commands/sync.helpers.d.ts +29 -1
  23. package/dist/src/commands/sync.helpers.d.ts.map +1 -1
  24. package/dist/src/commands/sync.helpers.js +38 -0
  25. package/dist/src/commands/sync.helpers.js.map +1 -1
  26. package/dist/src/commands/sync.js +20 -1
  27. package/dist/src/commands/sync.js.map +1 -1
  28. package/dist/src/commands/telemetry.d.ts +38 -0
  29. package/dist/src/commands/telemetry.d.ts.map +1 -0
  30. package/dist/src/commands/telemetry.helpers.d.ts +58 -0
  31. package/dist/src/commands/telemetry.helpers.d.ts.map +1 -0
  32. package/dist/src/commands/telemetry.helpers.js +173 -0
  33. package/dist/src/commands/telemetry.helpers.js.map +1 -0
  34. package/dist/src/commands/telemetry.helpers.test.d.ts +15 -0
  35. package/dist/src/commands/telemetry.helpers.test.d.ts.map +1 -0
  36. package/dist/src/commands/telemetry.helpers.test.js +240 -0
  37. package/dist/src/commands/telemetry.helpers.test.js.map +1 -0
  38. package/dist/src/commands/telemetry.js +354 -0
  39. package/dist/src/commands/telemetry.js.map +1 -0
  40. package/dist/src/commands/telemetry.test.d.ts +18 -0
  41. package/dist/src/commands/telemetry.test.d.ts.map +1 -0
  42. package/dist/src/commands/telemetry.test.js +401 -0
  43. package/dist/src/commands/telemetry.test.js.map +1 -0
  44. package/dist/src/index.js +3 -1
  45. package/dist/src/index.js.map +1 -1
  46. package/dist/src/utils/manifest.d.ts +75 -0
  47. package/dist/src/utils/manifest.d.ts.map +1 -1
  48. package/dist/src/utils/manifest.js +78 -0
  49. package/dist/src/utils/manifest.js.map +1 -1
  50. package/dist/src/utils/manifest.test.d.ts +20 -0
  51. package/dist/src/utils/manifest.test.d.ts.map +1 -0
  52. package/dist/src/utils/manifest.test.js +344 -0
  53. package/dist/src/utils/manifest.test.js.map +1 -0
  54. package/dist/src/utils/open-database.d.ts.map +1 -1
  55. package/dist/src/utils/open-database.js +33 -4
  56. package/dist/src/utils/open-database.js.map +1 -1
  57. package/dist/tests/embedding-probe-stdio.test.d.ts +11 -0
  58. package/dist/tests/embedding-probe-stdio.test.d.ts.map +1 -0
  59. package/dist/tests/embedding-probe-stdio.test.js +58 -0
  60. package/dist/tests/embedding-probe-stdio.test.js.map +1 -0
  61. package/dist/tests/open-database.test.js +25 -0
  62. package/dist/tests/open-database.test.js.map +1 -1
  63. package/dist/tests/sync.auth-ux.test.d.ts +14 -0
  64. package/dist/tests/sync.auth-ux.test.d.ts.map +1 -0
  65. package/dist/tests/sync.auth-ux.test.js +148 -0
  66. package/dist/tests/sync.auth-ux.test.js.map +1 -0
  67. package/package.json +5 -4
  68. package/templates/skill-telemetry.cmd +16 -0
  69. package/templates/skill-telemetry.sh +199 -0
@@ -9,6 +9,9 @@
9
9
  * not throw on a brand-new database.
10
10
  */
11
11
  import { describe, it, expect, afterEach } from 'vitest';
12
+ import { mkdtempSync, writeFileSync, existsSync, readdirSync, rmSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
12
15
  import { SearchService, SkillRepository, closeDatabase } from '@skillsmith/core';
13
16
  import { openCliDatabase } from '../src/utils/open-database.js';
14
17
  describe('SMI-4917 Bug 1: openCliDatabase', () => {
@@ -49,4 +52,26 @@ describe('SMI-4917 Bug 1: openCliDatabase', () => {
49
52
  expect(new SkillRepository(db).count()).toBe(0);
50
53
  });
51
54
  });
55
+ describe('SMI-4484: openCliDatabase corrupt-DB self-heal', () => {
56
+ let tempDir;
57
+ afterEach(() => {
58
+ if (tempDir && existsSync(tempDir))
59
+ rmSync(tempDir, { recursive: true, force: true });
60
+ });
61
+ it('recovers from a corrupt on-disk database by backing it up and rebuilding', async () => {
62
+ tempDir = mkdtempSync(join(tmpdir(), `smi4484-cli-${Date.now()}-${Math.random().toString(36).slice(2)}-`));
63
+ const dbPath = join(tempDir, 'skills.db');
64
+ // A corrupt file the WASM driver cannot read.
65
+ writeFileSync(dbPath, Buffer.from('not a sqlite database — corrupt on-disk file'));
66
+ const db = await openCliDatabase(dbPath);
67
+ closeDatabase(db);
68
+ // The corrupt file was backed up out of the way.
69
+ const backups = readdirSync(tempDir).filter((f) => f.includes('.corrupt-'));
70
+ expect(backups.length).toBe(1);
71
+ // A fresh, schema-initialized DB was opened in its place.
72
+ const reopened = await openCliDatabase(dbPath);
73
+ expect(new SkillRepository(reopened).count()).toBe(0);
74
+ closeDatabase(reopened);
75
+ });
76
+ });
52
77
  //# sourceMappingURL=open-database.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"open-database.test.js","sourceRoot":"","sources":["../../tests/open-database.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AACxD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EAAqB,MAAM,kBAAkB,CAAA;AACnG,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAE/D,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,MAAM,MAAM,GAAmB,EAAE,CAAA;IAEjC,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,MAAM,EAAE,IAAI,MAAM;YAAE,aAAa,CAAC,EAAE,CAAC,CAAA;QAC1C,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;IACnB,CAAC,CAAC,CAAA;IAEF,KAAK,UAAU,IAAI;QACjB,MAAM,EAAE,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,CAAA;QAC5C,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACf,OAAO,EAAE,CAAA;IACX,CAAC;IAED,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,EAAE,GAAG,MAAM,IAAI,EAAE,CAAA;QACvB,gEAAgE;QAChE,MAAM,UAAU,GAAG,EAAE;aAClB,OAAO,CAAC,oEAAoE,CAAC;aAC7E,GAAG,EAAE,CAAA;QACR,MAAM,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAA;IAClC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,EAAE,GAAG,MAAM,IAAI,EAAE,CAAA;QACvB,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,CAAC,CAAA;QACpC,oEAAoE;QACpE,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;IACxE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,EAAE,GAAG,MAAM,IAAI,EAAE,CAAA;QACvB,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,EAAE,CAAC,CAAA;QACpC,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;QAC/D,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QACjC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC/B,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,EAAE,GAAG,MAAM,IAAI,EAAE,CAAA;QACvB,MAAM,CAAC,IAAI,eAAe,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"open-database.test.js","sourceRoot":"","sources":["../../tests/open-database.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * SMI-4482: CLI `sync` command — actionable auth UX.
3
+ *
4
+ * Covers the fresh-install scenario where `skillsmith sync` runs before
5
+ * `skillsmith login`: the API client reaches the anonymous IP-trial path, the
6
+ * per-IP trial limit is exhausted server-side, and the `skills-search` edge
7
+ * function returns HTTP 401 `{"error":"Authentication required"}`. Previously
8
+ * the CLI printed a bare `Authentication required` with `Σ Total: 0`; it must
9
+ * now print actionable next steps and exit non-zero.
10
+ *
11
+ * Network is fully mocked — no production API calls.
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=sync.auth-ux.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.auth-ux.test.d.ts","sourceRoot":"","sources":["../../tests/sync.auth-ux.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * SMI-4482: CLI `sync` command — actionable auth UX.
3
+ *
4
+ * Covers the fresh-install scenario where `skillsmith sync` runs before
5
+ * `skillsmith login`: the API client reaches the anonymous IP-trial path, the
6
+ * per-IP trial limit is exhausted server-side, and the `skills-search` edge
7
+ * function returns HTTP 401 `{"error":"Authentication required"}`. Previously
8
+ * the CLI printed a bare `Authentication required` with `Σ Total: 0`; it must
9
+ * now print actionable next steps and exit non-zero.
10
+ *
11
+ * Network is fully mocked — no production API calls.
12
+ */
13
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
14
+ import { isAuthFailure, formatAuthGuidance } from '../src/commands/sync.helpers.js';
15
+ // ---------------------------------------------------------------------------
16
+ // Fixtures
17
+ // ---------------------------------------------------------------------------
18
+ function makeResult(overrides = {}) {
19
+ return {
20
+ success: false,
21
+ skillsAdded: 0,
22
+ skillsUpdated: 0,
23
+ skillsUnchanged: 0,
24
+ totalProcessed: 0,
25
+ errors: [],
26
+ durationMs: 5,
27
+ dryRun: false,
28
+ ...overrides,
29
+ };
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Pure-helper tests
33
+ // ---------------------------------------------------------------------------
34
+ describe('SMI-4482: isAuthFailure', () => {
35
+ it('detects the server "Authentication required" 401 signal', () => {
36
+ expect(isAuthFailure(makeResult({ errors: ['Authentication required'] }))).toBe(true);
37
+ });
38
+ it('detects an "Unauthorized" variant', () => {
39
+ expect(isAuthFailure(makeResult({ errors: ['Fetch error at offset 0: Unauthorized'] }))).toBe(true);
40
+ });
41
+ it('is case-insensitive', () => {
42
+ expect(isAuthFailure(makeResult({ errors: ['authentication required'] }))).toBe(true);
43
+ });
44
+ it('returns false for a transient network error (no auth signal)', () => {
45
+ expect(isAuthFailure(makeResult({ errors: ['fetch failed'] }))).toBe(false);
46
+ });
47
+ it('returns false for a successful sync', () => {
48
+ expect(isAuthFailure(makeResult({ success: true, totalProcessed: 12, skillsAdded: 12 }))).toBe(false);
49
+ });
50
+ it('returns false when partial results were returned even if an auth error is present', () => {
51
+ // A creds-present run that hit a 401 on one page but still synced skills
52
+ // is NOT a "needs login" situation — the user already has access.
53
+ expect(isAuthFailure(makeResult({ totalProcessed: 5, errors: ['Authentication required'] }))).toBe(false);
54
+ });
55
+ it('returns false when there are no errors', () => {
56
+ expect(isAuthFailure(makeResult({ errors: [] }))).toBe(false);
57
+ });
58
+ });
59
+ describe('SMI-4482: formatAuthGuidance', () => {
60
+ it('includes the login command and the headless/CI hint', () => {
61
+ const text = formatAuthGuidance().join('\n');
62
+ expect(text).toContain('skillsmith login');
63
+ expect(text).toContain('Sync requires authentication');
64
+ expect(text).toContain('SKILLSMITH_API_KEY');
65
+ });
66
+ });
67
+ // ---------------------------------------------------------------------------
68
+ // Command-level tests
69
+ // ---------------------------------------------------------------------------
70
+ const mocks = vi.hoisted(() => ({
71
+ runRegistrySync: vi.fn(),
72
+ dbClose: vi.fn(),
73
+ spinner: {
74
+ start: vi.fn().mockReturnThis(),
75
+ stop: vi.fn().mockReturnThis(),
76
+ succeed: vi.fn().mockReturnThis(),
77
+ fail: vi.fn().mockReturnThis(),
78
+ warn: vi.fn().mockReturnThis(),
79
+ text: '',
80
+ },
81
+ }));
82
+ vi.mock('../src/commands/run-registry-sync.js', () => ({
83
+ runRegistrySync: (...args) => mocks.runRegistrySync(...args),
84
+ }));
85
+ vi.mock('../src/utils/open-database.js', () => ({
86
+ openCliDatabase: () => Promise.resolve({ close: mocks.dbClose }),
87
+ }));
88
+ vi.mock('ora', () => ({ default: () => mocks.spinner }));
89
+ const originalConsoleLog = console.log;
90
+ const originalConsoleError = console.error;
91
+ const mockConsoleLog = vi.fn();
92
+ const mockConsoleError = vi.fn();
93
+ describe('SMI-4482: sync command — no-credentials UX', () => {
94
+ beforeEach(() => {
95
+ vi.clearAllMocks();
96
+ console.log = mockConsoleLog;
97
+ console.error = mockConsoleError;
98
+ process.exitCode = undefined;
99
+ });
100
+ afterEach(() => {
101
+ console.log = originalConsoleLog;
102
+ console.error = originalConsoleError;
103
+ process.exitCode = undefined;
104
+ });
105
+ it('prints actionable login guidance (not a bare error) and exits non-zero', async () => {
106
+ mocks.runRegistrySync.mockResolvedValue(makeResult({ errors: ['Authentication required'] }));
107
+ const { createSyncCommand } = await import('../src/commands/sync.js');
108
+ await createSyncCommand().parseAsync(['node', 'test']);
109
+ const stderr = mockConsoleError.mock.calls.map((c) => String(c[0])).join('\n');
110
+ expect(stderr).toContain('Sync requires authentication');
111
+ expect(stderr).toContain('skillsmith login');
112
+ expect(stderr).toContain('SKILLSMITH_API_KEY');
113
+ expect(process.exitCode).toBe(1);
114
+ // The bad UX must NOT appear: no bare "Σ Total: 0" results block.
115
+ const stdout = mockConsoleLog.mock.calls.map((c) => String(c[0])).join('\n');
116
+ expect(stdout).not.toContain('Total:');
117
+ expect(mocks.dbClose).toHaveBeenCalled();
118
+ });
119
+ it('emits machine-readable JSON and exit code 1 with --json on auth failure', async () => {
120
+ mocks.runRegistrySync.mockResolvedValue(makeResult({ errors: ['Authentication required'] }));
121
+ const { createSyncCommand } = await import('../src/commands/sync.js');
122
+ await createSyncCommand().parseAsync(['node', 'test', '--json']);
123
+ const stdout = mockConsoleLog.mock.calls.map((c) => String(c[0])).join('\n');
124
+ const parsed = JSON.parse(stdout);
125
+ expect(parsed.errors).toContain('Authentication required');
126
+ expect(process.exitCode).toBe(1);
127
+ });
128
+ it('does NOT trigger the guard for a successful (creds-present) sync', async () => {
129
+ mocks.runRegistrySync.mockResolvedValue(makeResult({ success: true, totalProcessed: 8, skillsAdded: 8 }));
130
+ const { createSyncCommand } = await import('../src/commands/sync.js');
131
+ await createSyncCommand().parseAsync(['node', 'test']);
132
+ expect(mocks.spinner.succeed).toHaveBeenCalled();
133
+ const stdout = mockConsoleLog.mock.calls.map((c) => String(c[0])).join('\n');
134
+ expect(stdout).toContain('Total:');
135
+ const stderr = mockConsoleError.mock.calls.map((c) => String(c[0])).join('\n');
136
+ expect(stderr).not.toContain('Sync requires authentication');
137
+ expect(process.exitCode).toBeUndefined();
138
+ });
139
+ it('does NOT trigger the guard for a non-auth transient failure', async () => {
140
+ mocks.runRegistrySync.mockResolvedValue(makeResult({ errors: ['fetch failed'] }));
141
+ const { createSyncCommand } = await import('../src/commands/sync.js');
142
+ await createSyncCommand().parseAsync(['node', 'test']);
143
+ expect(mocks.spinner.warn).toHaveBeenCalled();
144
+ const stderr = mockConsoleError.mock.calls.map((c) => String(c[0])).join('\n');
145
+ expect(stderr).not.toContain('Sync requires authentication');
146
+ });
147
+ });
148
+ //# sourceMappingURL=sync.auth-ux.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.auth-ux.test.js","sourceRoot":"","sources":["../../tests/sync.auth-ux.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillsmith/cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "CLI tools for Skillsmith skill discovery and authentication",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,8 +17,8 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@inquirer/prompts": "7.10.1",
20
- "@skillsmith/core": "^0.6.1",
21
- "@skillsmith/mcp-server": "^0.5.1",
20
+ "@skillsmith/core": "^0.8.0",
21
+ "@skillsmith/mcp-server": "^0.5.3",
22
22
  "chalk": "5.6.2",
23
23
  "chokidar": "4.0.3",
24
24
  "cli-table3": "0.6.5",
@@ -32,7 +32,7 @@
32
32
  "@isaacs/keytar": "7.9.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@types/node": "^22.15.0",
35
+ "@types/node": "^25.9.0",
36
36
  "vitest": "4.1.6"
37
37
  },
38
38
  "peerDependencies": {
@@ -46,6 +46,7 @@
46
46
  "files": [
47
47
  "dist",
48
48
  "assets",
49
+ "templates",
49
50
  "CHANGELOG.md"
50
51
  ],
51
52
  "license": "Elastic-2.0",
@@ -0,0 +1,16 @@
1
+ @echo off
2
+ REM Skillsmith skill-invocation telemetry hook — Windows Git-Bash shim.
3
+ REM
4
+ REM SMI-5012 / SMI-5020 W3.S1. Claude Code on Windows requires Git Bash,
5
+ REM so we delegate to the POSIX-sh script via `bash`. The shim itself
6
+ REM does no parsing — all logic (including the privacy-critical
7
+ REM never-read-args invariant) lives in skill-telemetry.sh.
8
+ REM
9
+ REM Stdin is piped through unchanged. Exit code is forwarded; the .sh
10
+ REM always exits 0 so this shim always exits 0 too.
11
+ REM
12
+ REM Installed at: %USERPROFILE%\.skillsmith\hooks\skill-telemetry.cmd
13
+ REM Delegates to: %USERPROFILE%\.skillsmith\hooks\skill-telemetry.sh
14
+
15
+ bash "%USERPROFILE%\.skillsmith\hooks\skill-telemetry.sh" %*
16
+ exit /b 0
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env sh
2
+ # Skillsmith skill-invocation telemetry hook (SMI-5012 / SMI-5020 W3.S1).
3
+ #
4
+ # Invoked from ~/.claude/settings.json PreToolUse + PostToolUse matchers on
5
+ # the `Skill` tool. Captures skill-invocation latency telemetry for the
6
+ # Skillsmith /functions/v1/events endpoint.
7
+ #
8
+ # PRIVACY INVARIANTS (DO NOT WEAKEN):
9
+ # - NEVER reads `.tool_input.args` from the hook stdin payload. EVER.
10
+ # Only `.session_id` and `.tool_input.skill` are extracted.
11
+ # - NEVER captures absolute paths, cwd, env vars, or file contents.
12
+ # - NEVER blocks the user. Exits 0 in every code path. No `set -e`.
13
+ # - Curl is fire-and-forget (backgrounded + output redirected).
14
+ #
15
+ # CROSS-PLATFORM:
16
+ # - POSIX `sh` only (NOT bash). Runs on macOS /bin/sh (dash-equiv),
17
+ # Linux /bin/sh (dash/bash), and Git Bash on Windows (via .cmd shim).
18
+ # - `date +%s%3N` (GNU) falls back to `date +%s` (BSD/macOS) for second-
19
+ # precision when millisecond precision is unavailable.
20
+ #
21
+ # STATE:
22
+ # - Start files: $HOME/.skillsmith/run/skill-$SESSION-$SKILL.start
23
+ # Per M8: lives under ~/.skillsmith/run (NOT /tmp) so files survive
24
+ # reboot; orphans GC'd by next pre-call's 1h sweep.
25
+ #
26
+ # OPT-IN GATES (both must pass; checked before parsing stdin):
27
+ # 1. SKILLSMITH_TELEMETRY_DISABLE != "1" (panic switch env var)
28
+ # 2. ~/.skillsmith/config.json `.telemetry.enabled == true`
29
+ #
30
+ # VERIFICATION (1000-iteration micro-bench):
31
+ # PAYLOAD='{"session_id":"abc","tool_input":{"skill":"search"}}'
32
+ # for i in $(seq 1 1000); do
33
+ # printf '%s' "$PAYLOAD" | time \
34
+ # sh packages/cli/templates/skill-telemetry.sh pre
35
+ # done
36
+ # # Target: <25ms p95 EXCLUDING the backgrounded curl in `post`.
37
+ #
38
+ # Claude Code hook stdin contract:
39
+ # https://code.claude.com/docs/en/hooks.md
40
+
41
+ set -u
42
+
43
+ MODE="${1:-}"
44
+
45
+ # ---- Gate 1: panic switch ----
46
+ if [ "${SKILLSMITH_TELEMETRY_DISABLE:-}" = "1" ]; then
47
+ exit 0
48
+ fi
49
+
50
+ # ---- Gate 2: jq availability (defensive; required by other Skillsmith hooks) ----
51
+ command -v jq >/dev/null 2>&1 || exit 0
52
+
53
+ # ---- Gate 3: opt-in config (single jq invocation for all three reads) ----
54
+ CONFIG="${HOME}/.skillsmith/config.json"
55
+ if [ ! -f "$CONFIG" ]; then
56
+ exit 0
57
+ fi
58
+ # Output is newline-separated: enabled-bool, endpoint, anonymousId.
59
+ # `jq -e` here would require a separate call; we instead check the first
60
+ # line for "true" via `case`. One jq fork instead of three.
61
+ CFG_OUT="$(jq -r '
62
+ (.telemetry.enabled // false),
63
+ (.telemetry.endpoint // "https://vrcnzpmndtroqxxoqkzy.supabase.co/functions/v1/events"),
64
+ (.telemetry.anonymousId // "")
65
+ ' "$CONFIG" 2>/dev/null)"
66
+ # POSIX line-extraction without spawning awk/sed.
67
+ CFG_ENABLED="${CFG_OUT%%
68
+ *}"
69
+ CFG_REST="${CFG_OUT#*
70
+ }"
71
+ ENDPOINT="${CFG_REST%%
72
+ *}"
73
+ ANON_ID="${CFG_REST#*
74
+ }"
75
+ case "$CFG_ENABLED" in
76
+ true) ;;
77
+ *) exit 0 ;;
78
+ esac
79
+
80
+ # ---- Parse Claude Code hook stdin payload (single jq for both reads) ----
81
+ # IMPORTANT: only `.session_id` and `.tool_input.skill` are extracted.
82
+ # `.tool_input.args` is NEVER read. See privacy invariants above.
83
+ PAYLOAD="$(cat 2>/dev/null || printf '%s' '{}')"
84
+ STDIN_OUT="$(printf '%s' "$PAYLOAD" | jq -r '(.session_id // ""), (.tool_input.skill // "")' 2>/dev/null)"
85
+ SESSION_ID="${STDIN_OUT%%
86
+ *}"
87
+ SKILL_NAME="${STDIN_OUT#*
88
+ }"
89
+
90
+ RUN_DIR="${HOME}/.skillsmith/run"
91
+ mkdir -p "$RUN_DIR" 2>/dev/null || true
92
+
93
+ # ---- Quarantine path: unparseable payload ----
94
+ # Per risk #13 (line 803): tool_input.skill shape changes ship a
95
+ # `skill_invoke_unparsed` event. Emit ONLY from `pre` so we don't
96
+ # double-count (post has no matching start file to read anyway).
97
+ if [ -z "$SESSION_ID" ] || [ -z "$SKILL_NAME" ]; then
98
+ if [ "$MODE" = "pre" ] && [ -n "$ANON_ID" ]; then
99
+ QUARANTINE_PAYLOAD="$(jq -n \
100
+ --arg event 'skill_invoke_unparsed' \
101
+ --arg anon "$ANON_ID" \
102
+ --arg source 'claude-code-hook' \
103
+ --arg framework 'claude-code' \
104
+ --arg platform "$(uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]')" \
105
+ '{event:$event, anonymous_id:$anon, metadata:{source:$source, framework:$framework, platform:$platform, success:false}}' \
106
+ 2>/dev/null)"
107
+ if [ -n "$QUARANTINE_PAYLOAD" ]; then
108
+ nohup curl -s -m 5 -X POST \
109
+ -H 'Content-Type: application/json' \
110
+ -d "$QUARANTINE_PAYLOAD" \
111
+ "$ENDPOINT" >/dev/null 2>&1 &
112
+ fi
113
+ fi
114
+ exit 0
115
+ fi
116
+
117
+ START_FILE="$RUN_DIR/skill-${SESSION_ID}-${SKILL_NAME}.start"
118
+
119
+ case "$MODE" in
120
+ pre)
121
+ # GC orphans older than 1h (mtime), then record start time (ms epoch).
122
+ # `find -mmin` is portable across BSD (macOS) and GNU (Linux/Git Bash).
123
+ find "$RUN_DIR" -maxdepth 1 -name 'skill-*.start' -type f -mmin +60 -delete 2>/dev/null || true
124
+ NOW_MS="$(date +%s%3N 2>/dev/null)"
125
+ # BSD date doesn't support %3N — falls back to seconds with 000 suffix.
126
+ case "$NOW_MS" in
127
+ *N|'')
128
+ NOW_MS="$(date +%s 2>/dev/null)000"
129
+ ;;
130
+ esac
131
+ printf '%s' "$NOW_MS" > "$START_FILE" 2>/dev/null || true
132
+ ;;
133
+
134
+ post)
135
+ if [ ! -f "$START_FILE" ]; then
136
+ exit 0
137
+ fi
138
+ START_MS="$(cat "$START_FILE" 2>/dev/null)"
139
+ rm -f "$START_FILE" 2>/dev/null || true
140
+ if [ -z "$START_MS" ]; then
141
+ exit 0
142
+ fi
143
+
144
+ NOW_MS="$(date +%s%3N 2>/dev/null)"
145
+ case "$NOW_MS" in
146
+ *N|'')
147
+ NOW_MS="$(date +%s 2>/dev/null)000"
148
+ ;;
149
+ esac
150
+
151
+ # Pure-sh integer arithmetic; both operands are integers above.
152
+ DURATION_MS=$((NOW_MS - START_MS))
153
+ if [ "$DURATION_MS" -lt 0 ]; then
154
+ DURATION_MS=0
155
+ fi
156
+
157
+ # If no anonymous id has been provisioned yet, we have nothing to send.
158
+ if [ -z "$ANON_ID" ]; then
159
+ exit 0
160
+ fi
161
+
162
+ PLATFORM="$(uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]')"
163
+
164
+ # Build payload (see wire format at skill-invoke-telemetry.md:614-643).
165
+ # NOTE: only $SKILL_NAME and $SESSION_ID flow from stdin → payload.
166
+ # No args, no paths, no env vars.
167
+ PAYLOAD_JSON="$(jq -n \
168
+ --arg event 'skill_invoke' \
169
+ --arg anon "$ANON_ID" \
170
+ --arg skill "$SKILL_NAME" \
171
+ --arg session "$SESSION_ID" \
172
+ --argjson dur "$DURATION_MS" \
173
+ --arg source 'claude-code-hook' \
174
+ --arg framework 'claude-code' \
175
+ --arg platform "$PLATFORM" \
176
+ '{event:$event, anonymous_id:$anon, metadata:{skill_name:$skill, session_id:$session, duration_ms:$dur, source:$source, framework:$framework, platform:$platform, success:true, is_subagent:false}}' \
177
+ 2>/dev/null)"
178
+
179
+ if [ -z "$PAYLOAD_JSON" ]; then
180
+ exit 0
181
+ fi
182
+
183
+ # Fire-and-forget. `nohup` + `&` + redirected fd's mean the parent
184
+ # shell does not wait on the curl process. `-m 5` caps the curl at
185
+ # 5s in case the endpoint is unreachable (no impact on hook latency
186
+ # because the curl is already detached).
187
+ nohup curl -s -m 5 -X POST \
188
+ -H 'Content-Type: application/json' \
189
+ -d "$PAYLOAD_JSON" \
190
+ "$ENDPOINT" >/dev/null 2>&1 &
191
+ ;;
192
+
193
+ *)
194
+ # Unknown mode — silently exit. Belt-and-suspenders.
195
+ :
196
+ ;;
197
+ esac
198
+
199
+ exit 0