@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.
- package/CHANGELOG.md +20 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/commands/index.d.ts +1 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/info.js +1 -1
- package/dist/src/commands/install.js +1 -1
- package/dist/src/commands/install.js.map +1 -1
- package/dist/src/commands/search.d.ts.map +1 -1
- package/dist/src/commands/search.helpers.d.ts +29 -0
- package/dist/src/commands/search.helpers.d.ts.map +1 -1
- package/dist/src/commands/search.helpers.js +39 -1
- package/dist/src/commands/search.helpers.js.map +1 -1
- package/dist/src/commands/search.js +26 -2
- package/dist/src/commands/search.js.map +1 -1
- package/dist/src/commands/search.test.d.ts +10 -0
- package/dist/src/commands/search.test.d.ts.map +1 -0
- package/dist/src/commands/search.test.js +179 -0
- package/dist/src/commands/search.test.js.map +1 -0
- package/dist/src/commands/sync.d.ts.map +1 -1
- package/dist/src/commands/sync.helpers.d.ts +29 -1
- package/dist/src/commands/sync.helpers.d.ts.map +1 -1
- package/dist/src/commands/sync.helpers.js +38 -0
- package/dist/src/commands/sync.helpers.js.map +1 -1
- package/dist/src/commands/sync.js +20 -1
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/telemetry.d.ts +38 -0
- package/dist/src/commands/telemetry.d.ts.map +1 -0
- package/dist/src/commands/telemetry.helpers.d.ts +58 -0
- package/dist/src/commands/telemetry.helpers.d.ts.map +1 -0
- package/dist/src/commands/telemetry.helpers.js +173 -0
- package/dist/src/commands/telemetry.helpers.js.map +1 -0
- package/dist/src/commands/telemetry.helpers.test.d.ts +15 -0
- package/dist/src/commands/telemetry.helpers.test.d.ts.map +1 -0
- package/dist/src/commands/telemetry.helpers.test.js +240 -0
- package/dist/src/commands/telemetry.helpers.test.js.map +1 -0
- package/dist/src/commands/telemetry.js +354 -0
- package/dist/src/commands/telemetry.js.map +1 -0
- package/dist/src/commands/telemetry.test.d.ts +18 -0
- package/dist/src/commands/telemetry.test.d.ts.map +1 -0
- package/dist/src/commands/telemetry.test.js +401 -0
- package/dist/src/commands/telemetry.test.js.map +1 -0
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/utils/manifest.d.ts +75 -0
- package/dist/src/utils/manifest.d.ts.map +1 -1
- package/dist/src/utils/manifest.js +78 -0
- package/dist/src/utils/manifest.js.map +1 -1
- package/dist/src/utils/manifest.test.d.ts +20 -0
- package/dist/src/utils/manifest.test.d.ts.map +1 -0
- package/dist/src/utils/manifest.test.js +344 -0
- package/dist/src/utils/manifest.test.js.map +1 -0
- package/dist/src/utils/open-database.d.ts.map +1 -1
- package/dist/src/utils/open-database.js +33 -4
- package/dist/src/utils/open-database.js.map +1 -1
- package/dist/tests/embedding-probe-stdio.test.d.ts +11 -0
- package/dist/tests/embedding-probe-stdio.test.d.ts.map +1 -0
- package/dist/tests/embedding-probe-stdio.test.js +58 -0
- package/dist/tests/embedding-probe-stdio.test.js.map +1 -0
- package/dist/tests/open-database.test.js +25 -0
- package/dist/tests/open-database.test.js.map +1 -1
- package/dist/tests/sync.auth-ux.test.d.ts +14 -0
- package/dist/tests/sync.auth-ux.test.d.ts.map +1 -0
- package/dist/tests/sync.auth-ux.test.js +148 -0
- package/dist/tests/sync.auth-ux.test.js.map +1 -0
- package/package.json +5 -4
- package/templates/skill-telemetry.cmd +16 -0
- 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.
|
|
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.
|
|
21
|
-
"@skillsmith/mcp-server": "^0.5.
|
|
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": "^
|
|
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
|