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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillrepo",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Set up SkillRepo in any IDE — one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -155,6 +155,7 @@ export async function runInit(argv) {
155
155
  mcpUrl,
156
156
  apiKey,
157
157
  serverUrl: flags.url,
158
+ userId: payload.userId,
158
159
  });
159
160
  } catch (err) {
160
161
  printError(err.message);
@@ -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 { apiKey: cfg.apiKey, serverUrl: cfg.serverUrl || DEFAULT_SERVER_URL };
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
- githubUsername: sessionInfo.githubUsername || undefined,
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
- githubUsername: "",
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 { apiKey: cfg.apiKey, serverUrl: cfg.serverUrl || DEFAULT_SERVER_URL };
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
- githubUsername: sessionInfo.githubUsername || "",
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
- githubUsername,
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
@@ -3,7 +3,7 @@
3
3
  * Uses Node 18+ built-in fetch. Zero dependencies.
4
4
  */
5
5
 
6
- const VERSION = "1.0.0";
6
+ const VERSION = "1.9.0";
7
7
  const DEFAULT_URL = "https://skillrepo.dev";
8
8
 
9
9
  /**
@@ -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 serverUrl with the new values.
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
- const config = { ...preserved, apiKey, serverUrl };
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
- githubUsername: "testuser",
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.githubUsername, "testuser");
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
- githubUsername: null,
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.githubUsername, undefined);
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
- githubUsername: "testuser",
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.githubUsername, "testuser");
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
- githubUsername: null,
298
+ userId: null,
297
299
  });
298
300
 
299
301
  const event = payload.events[0];
300
302
  assert.equal(event.skillVersion, "");
301
- assert.equal(event.githubUsername, "");
303
+ assert.equal(event.userId, undefined); // null → undefined via || operator
302
304
  assert.equal(event.wasRulesDelivered, false);
303
305
  });
304
306