skillrepo 1.8.0 → 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/package.json +1 -1
- package/src/commands/init.mjs +1 -0
- package/src/hooks/skillrepo-pretool-activation.mjs +10 -49
- package/src/hooks/skillrepo-prompt-match.mjs +10 -47
- package/src/lib/http.mjs +1 -1
- package/src/lib/write-configs.mjs +10 -5
- package/src/test/hooks/skillrepo-pretool-activation.test.mjs +7 -5
- package/src/test/hooks/skillrepo-prompt-match.test.mjs +7 -5
package/package.json
CHANGED
package/src/commands/init.mjs
CHANGED
|
@@ -15,7 +15,6 @@ import { join } from "node:path";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { homedir, tmpdir } from "node:os";
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
|
-
import { execSync } from "node:child_process";
|
|
19
18
|
|
|
20
19
|
// ---------------------------------------------------------------------------
|
|
21
20
|
// Constants
|
|
@@ -30,20 +29,24 @@ const MAX_EVENTS_PER_BATCH = 50;
|
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
31
|
* Read config from ~/.claude/skillrepo/config.json with fallbacks.
|
|
33
|
-
* Returns { apiKey, serverUrl } or null.
|
|
32
|
+
* Returns { apiKey, serverUrl, userId } or null.
|
|
34
33
|
*/
|
|
35
34
|
export function readConfig() {
|
|
36
35
|
const configPath = join(homedir(), ".claude", "skillrepo", "config.json");
|
|
37
36
|
try {
|
|
38
37
|
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
39
38
|
if (cfg.apiKey) {
|
|
40
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
apiKey: cfg.apiKey,
|
|
41
|
+
serverUrl: cfg.serverUrl || DEFAULT_SERVER_URL,
|
|
42
|
+
userId: cfg.userId || null,
|
|
43
|
+
};
|
|
41
44
|
}
|
|
42
45
|
} catch { /* not found */ }
|
|
43
46
|
|
|
44
47
|
const envKey = process.env.SKILLREPO_ACCESS_KEY;
|
|
45
48
|
if (envKey) {
|
|
46
|
-
return { apiKey: envKey, serverUrl: process.env.SKILLREPO_SERVER_URL || DEFAULT_SERVER_URL };
|
|
49
|
+
return { apiKey: envKey, serverUrl: process.env.SKILLREPO_SERVER_URL || DEFAULT_SERVER_URL, userId: null };
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
// .env.local fallback
|
|
@@ -60,7 +63,7 @@ export function readConfig() {
|
|
|
60
63
|
val = val.slice(1, -1);
|
|
61
64
|
}
|
|
62
65
|
val = val.replace(/\s+#.*$/, "");
|
|
63
|
-
if (val) return { apiKey: val, serverUrl: DEFAULT_SERVER_URL };
|
|
66
|
+
if (val) return { apiKey: val, serverUrl: DEFAULT_SERVER_URL, userId: null };
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
} catch { /* file doesn't exist */ }
|
|
@@ -170,45 +173,6 @@ export function updateActivationState(statePath, state, reportedMatches) {
|
|
|
170
173
|
catch { /* non-critical */ }
|
|
171
174
|
}
|
|
172
175
|
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
// GitHub username resolution
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
|
|
177
|
-
const GITHUB_USERNAME_CACHE_PATH = join(tmpdir(), "skillrepo-gh-user.json");
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Resolve GitHub username via `gh api user`. Cached per session.
|
|
181
|
-
*/
|
|
182
|
-
export function resolveGithubUsername() {
|
|
183
|
-
// Check cache first
|
|
184
|
-
try {
|
|
185
|
-
const cached = JSON.parse(readFileSync(GITHUB_USERNAME_CACHE_PATH, "utf-8"));
|
|
186
|
-
if (cached.username && cached.expiresAt > Date.now()) return cached.username;
|
|
187
|
-
} catch { /* no cache */ }
|
|
188
|
-
|
|
189
|
-
// Resolve via gh CLI
|
|
190
|
-
try {
|
|
191
|
-
const result = execSync("gh api user --jq .login", {
|
|
192
|
-
encoding: "utf-8",
|
|
193
|
-
timeout: 3_000,
|
|
194
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
195
|
-
}).trim();
|
|
196
|
-
|
|
197
|
-
if (result) {
|
|
198
|
-
// Cache for 1 hour
|
|
199
|
-
try {
|
|
200
|
-
writeFileSync(GITHUB_USERNAME_CACHE_PATH, JSON.stringify({
|
|
201
|
-
username: result,
|
|
202
|
-
expiresAt: Date.now() + 3_600_000,
|
|
203
|
-
}), "utf-8");
|
|
204
|
-
} catch { /* non-critical */ }
|
|
205
|
-
return result;
|
|
206
|
-
}
|
|
207
|
-
} catch { /* gh not installed or not authed */ }
|
|
208
|
-
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
176
|
// ---------------------------------------------------------------------------
|
|
213
177
|
// Telemetry payload
|
|
214
178
|
// ---------------------------------------------------------------------------
|
|
@@ -224,7 +188,7 @@ export function buildTelemetryPayload(matches, sessionInfo) {
|
|
|
224
188
|
activatedAt: new Date().toISOString(),
|
|
225
189
|
ide: sessionInfo.ide || "claude-code",
|
|
226
190
|
sessionHash: sessionInfo.sessionHash,
|
|
227
|
-
|
|
191
|
+
userId: sessionInfo.userId || undefined,
|
|
228
192
|
source: "pretool_hook",
|
|
229
193
|
toolPattern: m.pattern,
|
|
230
194
|
}));
|
|
@@ -306,13 +270,10 @@ export async function main(input) {
|
|
|
306
270
|
// -- Build and send telemetry --
|
|
307
271
|
const sessionHash = createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
|
|
308
272
|
|
|
309
|
-
// Skip GitHub username resolution — it can block up to 3s on cold cache via
|
|
310
|
-
// `gh api user`. The prompt-match hook already identifies the session, and
|
|
311
|
-
// the server can correlate by API key if needed.
|
|
312
273
|
const payload = buildTelemetryPayload(newMatches, {
|
|
313
274
|
ide: "claude-code",
|
|
314
275
|
sessionHash,
|
|
315
|
-
|
|
276
|
+
userId: config.userId,
|
|
316
277
|
});
|
|
317
278
|
|
|
318
279
|
sendTelemetry(config, payload); // fire-and-forget -- do NOT await
|
|
@@ -14,7 +14,6 @@ import { join } from "node:path";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { homedir, tmpdir } from "node:os";
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
|
-
import { execSync } from "node:child_process";
|
|
18
17
|
|
|
19
18
|
// ---------------------------------------------------------------------------
|
|
20
19
|
// Constants
|
|
@@ -29,20 +28,24 @@ const MAX_EVENTS_PER_BATCH = 50;
|
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* Read config from ~/.claude/skillrepo/config.json with fallbacks.
|
|
32
|
-
* Returns { apiKey, serverUrl } or null.
|
|
31
|
+
* Returns { apiKey, serverUrl, userId } or null.
|
|
33
32
|
*/
|
|
34
33
|
export function readConfig() {
|
|
35
34
|
const configPath = join(homedir(), ".claude", "skillrepo", "config.json");
|
|
36
35
|
try {
|
|
37
36
|
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
38
37
|
if (cfg.apiKey) {
|
|
39
|
-
return {
|
|
38
|
+
return {
|
|
39
|
+
apiKey: cfg.apiKey,
|
|
40
|
+
serverUrl: cfg.serverUrl || DEFAULT_SERVER_URL,
|
|
41
|
+
userId: cfg.userId || null,
|
|
42
|
+
};
|
|
40
43
|
}
|
|
41
44
|
} catch { /* not found */ }
|
|
42
45
|
|
|
43
46
|
const envKey = process.env.SKILLREPO_ACCESS_KEY;
|
|
44
47
|
if (envKey) {
|
|
45
|
-
return { apiKey: envKey, serverUrl: process.env.SKILLREPO_SERVER_URL || DEFAULT_SERVER_URL };
|
|
48
|
+
return { apiKey: envKey, serverUrl: process.env.SKILLREPO_SERVER_URL || DEFAULT_SERVER_URL, userId: null };
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
// .env.local fallback
|
|
@@ -59,7 +62,7 @@ export function readConfig() {
|
|
|
59
62
|
val = val.slice(1, -1);
|
|
60
63
|
}
|
|
61
64
|
val = val.replace(/\s+#.*$/, "");
|
|
62
|
-
if (val) return { apiKey: val, serverUrl: DEFAULT_SERVER_URL };
|
|
65
|
+
if (val) return { apiKey: val, serverUrl: DEFAULT_SERVER_URL, userId: null };
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
} catch { /* file doesn't exist */ }
|
|
@@ -175,45 +178,6 @@ export function updateSessionState(statePath, state, reportedMatches) {
|
|
|
175
178
|
catch { /* non-critical */ }
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
// ---------------------------------------------------------------------------
|
|
179
|
-
// GitHub username resolution
|
|
180
|
-
// ---------------------------------------------------------------------------
|
|
181
|
-
|
|
182
|
-
const GITHUB_USERNAME_CACHE_PATH = join(tmpdir(), "skillrepo-gh-user.json");
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Resolve GitHub username via `gh api user`. Cached per session.
|
|
186
|
-
*/
|
|
187
|
-
export function resolveGithubUsername() {
|
|
188
|
-
// Check cache first
|
|
189
|
-
try {
|
|
190
|
-
const cached = JSON.parse(readFileSync(GITHUB_USERNAME_CACHE_PATH, "utf-8"));
|
|
191
|
-
if (cached.username && cached.expiresAt > Date.now()) return cached.username;
|
|
192
|
-
} catch { /* no cache */ }
|
|
193
|
-
|
|
194
|
-
// Resolve via gh CLI
|
|
195
|
-
try {
|
|
196
|
-
const result = execSync("gh api user --jq .login", {
|
|
197
|
-
encoding: "utf-8",
|
|
198
|
-
timeout: 3_000,
|
|
199
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
200
|
-
}).trim();
|
|
201
|
-
|
|
202
|
-
if (result) {
|
|
203
|
-
// Cache for 1 hour
|
|
204
|
-
try {
|
|
205
|
-
writeFileSync(GITHUB_USERNAME_CACHE_PATH, JSON.stringify({
|
|
206
|
-
username: result,
|
|
207
|
-
expiresAt: Date.now() + 3_600_000,
|
|
208
|
-
}), "utf-8");
|
|
209
|
-
} catch { /* non-critical */ }
|
|
210
|
-
return result;
|
|
211
|
-
}
|
|
212
|
-
} catch { /* gh not installed or not authed */ }
|
|
213
|
-
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
181
|
// ---------------------------------------------------------------------------
|
|
218
182
|
// Telemetry payload
|
|
219
183
|
// ---------------------------------------------------------------------------
|
|
@@ -229,7 +193,7 @@ export function buildTelemetryPayload(matches, sessionInfo) {
|
|
|
229
193
|
matchedAt: new Date().toISOString(),
|
|
230
194
|
ide: sessionInfo.ide || "claude-code",
|
|
231
195
|
sessionHash: sessionInfo.sessionHash,
|
|
232
|
-
|
|
196
|
+
userId: sessionInfo.userId || undefined,
|
|
233
197
|
wasRulesDelivered: m.skill.isRulesDelivered ?? false,
|
|
234
198
|
}));
|
|
235
199
|
|
|
@@ -308,12 +272,11 @@ export async function main(input) {
|
|
|
308
272
|
|
|
309
273
|
// ── Build and send telemetry ────────────────────────────────
|
|
310
274
|
const sessionHash = createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
|
|
311
|
-
const githubUsername = resolveGithubUsername();
|
|
312
275
|
|
|
313
276
|
const payload = buildTelemetryPayload(newMatches, {
|
|
314
277
|
ide: "claude-code",
|
|
315
278
|
sessionHash,
|
|
316
|
-
|
|
279
|
+
userId: config.userId,
|
|
317
280
|
});
|
|
318
281
|
|
|
319
282
|
sendTelemetry(config, payload); // fire-and-forget — do NOT await
|
package/src/lib/http.mjs
CHANGED
|
@@ -28,13 +28,14 @@ import { mergeGitignore } from "./mergers/gitignore.mjs";
|
|
|
28
28
|
* @param {string} options.mcpUrl - The MCP endpoint URL
|
|
29
29
|
* @param {string} options.apiKey - The access key
|
|
30
30
|
* @param {string} options.serverUrl - The SkillRepo server URL (e.g. https://skillrepo.dev)
|
|
31
|
+
* @param {string} [options.userId] - The authenticated user's SkillRepo ID
|
|
31
32
|
* @returns {{ path: string; action: string }[]}
|
|
32
33
|
*/
|
|
33
|
-
export function writeAllConfigs({ ides, mcpUrl, apiKey, serverUrl }) {
|
|
34
|
+
export function writeAllConfigs({ ides, mcpUrl, apiKey, serverUrl, userId }) {
|
|
34
35
|
const results = [];
|
|
35
36
|
|
|
36
37
|
// ── Global config (shared across all projects) ────────────────────────
|
|
37
|
-
const globalConfigAction = writeGlobalConfig(apiKey, serverUrl);
|
|
38
|
+
const globalConfigAction = writeGlobalConfig(apiKey, serverUrl, userId);
|
|
38
39
|
results.push({ path: "~/.claude/skillrepo/config.json", action: globalConfigAction });
|
|
39
40
|
|
|
40
41
|
// Claude Code
|
|
@@ -93,7 +94,7 @@ export function writeAllConfigs({ ides, mcpUrl, apiKey, serverUrl }) {
|
|
|
93
94
|
* This is the primary config source for standalone hooks.
|
|
94
95
|
* @returns {"created" | "updated"} The action taken.
|
|
95
96
|
*/
|
|
96
|
-
function writeGlobalConfig(apiKey, serverUrl) {
|
|
97
|
+
function writeGlobalConfig(apiKey, serverUrl, userId) {
|
|
97
98
|
const configDir = globalSkillrepoDir();
|
|
98
99
|
if (!existsSync(configDir)) {
|
|
99
100
|
mkdirSync(configDir, { recursive: true });
|
|
@@ -103,13 +104,17 @@ function writeGlobalConfig(apiKey, serverUrl) {
|
|
|
103
104
|
const existingRaw = readFileSafe(configPath);
|
|
104
105
|
|
|
105
106
|
// Preserve existing config fields (e.g. maxRulesFiles, maxRulesBudgetBytes)
|
|
106
|
-
// while always overwriting apiKey and
|
|
107
|
+
// while always overwriting apiKey, serverUrl, and userId with the new values.
|
|
107
108
|
let preserved = {};
|
|
108
109
|
if (existingRaw !== null) {
|
|
109
110
|
try { preserved = JSON.parse(existingRaw); }
|
|
110
111
|
catch { /* corrupt config — start fresh */ }
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
+
// Always overwrite userId — even if the new value is undefined/null — to
|
|
114
|
+
// prevent a stale userId from a previous account persisting across re-inits.
|
|
115
|
+
const { userId: _prevUserId, ...preservedWithoutUserId } = preserved;
|
|
116
|
+
const config = { ...preservedWithoutUserId, apiKey, serverUrl };
|
|
117
|
+
if (userId) config.userId = userId;
|
|
113
118
|
|
|
114
119
|
writeFileSafe(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
115
120
|
return existingRaw !== null ? "updated" : "created";
|
|
@@ -256,14 +256,14 @@ describe("session deduplication", () => {
|
|
|
256
256
|
// ---------------------------------------------------------------------------
|
|
257
257
|
|
|
258
258
|
describe("buildTelemetryPayload", () => {
|
|
259
|
-
it("produces correct payload shape", () => {
|
|
259
|
+
it("produces correct payload shape with userId", () => {
|
|
260
260
|
const matches = [
|
|
261
261
|
{ skill: makeSkill({ owner: "alice", name: "my-skill", version: "2.0.0" }), pattern: "gh issue" },
|
|
262
262
|
];
|
|
263
263
|
const sessionInfo = {
|
|
264
264
|
ide: "claude-code",
|
|
265
265
|
sessionHash: "abc123",
|
|
266
|
-
|
|
266
|
+
userId: "user-42",
|
|
267
267
|
};
|
|
268
268
|
|
|
269
269
|
const payload = buildTelemetryPayload(matches, sessionInfo);
|
|
@@ -275,10 +275,12 @@ describe("buildTelemetryPayload", () => {
|
|
|
275
275
|
assert.equal(event.skillVersion, "2.0.0");
|
|
276
276
|
assert.equal(event.ide, "claude-code");
|
|
277
277
|
assert.equal(event.sessionHash, "abc123");
|
|
278
|
-
assert.equal(event.
|
|
278
|
+
assert.equal(event.userId, "user-42");
|
|
279
279
|
assert.equal(event.source, "pretool_hook");
|
|
280
280
|
assert.equal(event.toolPattern, "gh issue");
|
|
281
281
|
assert.ok(event.activatedAt); // ISO 8601 string
|
|
282
|
+
// githubUsername should NOT be in the payload
|
|
283
|
+
assert.equal(event.githubUsername, undefined);
|
|
282
284
|
});
|
|
283
285
|
|
|
284
286
|
it("caps events at 50 per batch", () => {
|
|
@@ -297,12 +299,12 @@ describe("buildTelemetryPayload", () => {
|
|
|
297
299
|
const payload = buildTelemetryPayload(matches, {
|
|
298
300
|
ide: "claude-code",
|
|
299
301
|
sessionHash: "hash",
|
|
300
|
-
|
|
302
|
+
userId: null,
|
|
301
303
|
});
|
|
302
304
|
|
|
303
305
|
const event = payload.events[0];
|
|
304
306
|
assert.equal(event.skillVersion, undefined);
|
|
305
|
-
assert.equal(event.
|
|
307
|
+
assert.equal(event.userId, undefined); // null → undefined via || operator
|
|
306
308
|
assert.equal(event.source, "pretool_hook");
|
|
307
309
|
});
|
|
308
310
|
});
|
|
@@ -253,14 +253,14 @@ describe("session deduplication", () => {
|
|
|
253
253
|
// ---------------------------------------------------------------------------
|
|
254
254
|
|
|
255
255
|
describe("buildTelemetryPayload", () => {
|
|
256
|
-
it("produces correct payload shape", () => {
|
|
256
|
+
it("produces correct payload shape with userId", () => {
|
|
257
257
|
const matches = [
|
|
258
258
|
{ skill: makeSkill({ owner: "alice", name: "my-skill", version: "2.0.0", isRulesDelivered: true }), score: 5 },
|
|
259
259
|
];
|
|
260
260
|
const sessionInfo = {
|
|
261
261
|
ide: "claude-code",
|
|
262
262
|
sessionHash: "abc123",
|
|
263
|
-
|
|
263
|
+
userId: "user-42",
|
|
264
264
|
};
|
|
265
265
|
|
|
266
266
|
const payload = buildTelemetryPayload(matches, sessionInfo);
|
|
@@ -272,9 +272,11 @@ describe("buildTelemetryPayload", () => {
|
|
|
272
272
|
assert.equal(event.skillVersion, "2.0.0");
|
|
273
273
|
assert.equal(event.ide, "claude-code");
|
|
274
274
|
assert.equal(event.sessionHash, "abc123");
|
|
275
|
-
assert.equal(event.
|
|
275
|
+
assert.equal(event.userId, "user-42");
|
|
276
276
|
assert.equal(event.wasRulesDelivered, true);
|
|
277
277
|
assert.ok(event.matchedAt); // ISO 8601 string
|
|
278
|
+
// githubUsername should NOT be in the payload
|
|
279
|
+
assert.equal(event.githubUsername, undefined);
|
|
278
280
|
});
|
|
279
281
|
|
|
280
282
|
it("caps events at 50 per batch", () => {
|
|
@@ -293,12 +295,12 @@ describe("buildTelemetryPayload", () => {
|
|
|
293
295
|
const payload = buildTelemetryPayload(matches, {
|
|
294
296
|
ide: "claude-code",
|
|
295
297
|
sessionHash: "hash",
|
|
296
|
-
|
|
298
|
+
userId: null,
|
|
297
299
|
});
|
|
298
300
|
|
|
299
301
|
const event = payload.events[0];
|
|
300
302
|
assert.equal(event.skillVersion, "");
|
|
301
|
-
assert.equal(event.
|
|
303
|
+
assert.equal(event.userId, undefined); // null → undefined via || operator
|
|
302
304
|
assert.equal(event.wasRulesDelivered, false);
|
|
303
305
|
});
|
|
304
306
|
|