skillrepo 3.1.3 → 3.1.5

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/LICENSE ADDED
@@ -0,0 +1,37 @@
1
+ SkillRepo CLI
2
+ Copyright (c) 2026 SkillRepo LLC. All rights reserved.
3
+
4
+ This software is proprietary and confidential. Its installation, use,
5
+ reproduction, modification, and distribution are governed exclusively by
6
+ the SkillRepo End User License Agreement (the "EULA") available at:
7
+
8
+ https://skillrepo.dev/eula
9
+
10
+ By installing, copying, or otherwise using this software you agree to be
11
+ bound by the EULA. If you do not agree to the EULA, do not install or
12
+ use this software.
13
+
14
+ No license or right, whether by implication, estoppel, or otherwise, is
15
+ granted except as expressly set forth in the EULA. Without limiting the
16
+ foregoing, you may not:
17
+
18
+ - reverse engineer, decompile, disassemble, or otherwise attempt to
19
+ derive the source code of this software, except to the extent such
20
+ activity is expressly permitted by applicable law notwithstanding
21
+ this limitation;
22
+ - sell, resell, rent, lease, sublicense, distribute, or otherwise
23
+ transfer this software or access to it to any third party;
24
+ - use this software, its outputs, or any data obtained through it to
25
+ build or operate a service that is substantially similar to or
26
+ competes with the SkillRepo platform; or
27
+ - remove, alter, or obscure any proprietary notices on or in this
28
+ software.
29
+
30
+ THIS SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS
31
+ OR IMPLIED. YOUR USE OF THIS SOFTWARE IS SUBJECT TO THE WARRANTY
32
+ DISCLAIMERS, LIMITATION OF LIABILITY, AND ALL OTHER PROVISIONS OF THE
33
+ EULA, WHICH ARE INCORPORATED INTO THIS NOTICE BY REFERENCE.
34
+
35
+ SkillRepo and the SkillRepo logo are trademarks of SkillRepo LLC.
36
+
37
+ Contact: hello@skillrepo.dev
package/README.md CHANGED
@@ -346,4 +346,7 @@ fully overwrites it.
346
346
 
347
347
  ## License
348
348
 
349
- MIT see [LICENSE](./LICENSE).
349
+ Proprietary. Copyright © 2026 SkillRepo LLC. All rights reserved.
350
+ Use of this CLI is governed by the SkillRepo End User License Agreement
351
+ at [https://skillrepo.dev/eula](https://skillrepo.dev/eula). See
352
+ [LICENSE](./LICENSE) for the full notice.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillrepo",
3
- "version": "3.1.3",
3
+ "version": "3.1.5",
4
4
  "description": "Pull-based CLI for agent skills — init, sync, search, add, remove your library from any IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
- "src/"
11
+ "src/",
12
+ "LICENSE"
12
13
  ],
13
14
  "repository": {
14
15
  "type": "git",
@@ -16,7 +17,8 @@
16
17
  "directory": "packages/cli"
17
18
  },
18
19
  "keywords": ["skillrepo", "cli", "mcp", "ai-skills"],
19
- "license": "MIT",
20
+ "author": "SkillRepo LLC",
21
+ "license": "SEE LICENSE IN LICENSE",
20
22
  "dependencies": {
21
23
  "cli-table3": "^0.6.5"
22
24
  }
@@ -227,7 +227,7 @@ export async function runInit(argv, io = {}, deps = {}) {
227
227
  p.step(2, 7, "Validating key");
228
228
  let accountCtx;
229
229
  try {
230
- accountCtx = await validateAccessKey(serverUrl, apiKey);
230
+ accountCtx = await validateAccessKey(serverUrl, apiKey, "init");
231
231
  } catch (err) {
232
232
  // If the existing config had a stale/revoked key, fall back to
233
233
  // prompt (unless --yes, which is meant to be non-interactive).
@@ -245,7 +245,7 @@ export async function runInit(argv, io = {}, deps = {}) {
245
245
  if (!apiKey || !apiKey.startsWith("sk_live_")) {
246
246
  throw validationError("Invalid access key format.");
247
247
  }
248
- accountCtx = await validateAccessKey(serverUrl, apiKey);
248
+ accountCtx = await validateAccessKey(serverUrl, apiKey, "init");
249
249
  } else {
250
250
  throw err;
251
251
  }
package/src/lib/http.mjs CHANGED
@@ -82,6 +82,22 @@ async function userAgent() {
82
82
  return `skillrepo-cli/${_cachedVersion}`;
83
83
  }
84
84
 
85
+ /**
86
+ * Valid values for the `X-SkillRepo-Source` header sent on auth-validate
87
+ * requests. `init` is used by `skillrepo init`; every other command
88
+ * sends `validate`. The server-side logging middleware (atxpace/skill-repo#732)
89
+ * records one of `cli.bootstrap` or `cli.validate` in `server_events`
90
+ * based on this header. Older CLI versions that predate this header
91
+ * cause the server to default to `validate` — tile counts are a floor,
92
+ * never inflated.
93
+ */
94
+ export const CLI_SOURCE_VALUES = ["init", "validate"];
95
+
96
+ /** Type guard for the discriminated union — exported for tests. */
97
+ export function isCliSource(value) {
98
+ return CLI_SOURCE_VALUES.includes(value);
99
+ }
100
+
85
101
  /**
86
102
  * Build the canonical headers for every authenticated request.
87
103
  */
@@ -368,15 +384,38 @@ async function mapErrorResponse(res, url) {
368
384
  * authenticated account context. Replaces the deprecated
369
385
  * `/api/v1/setup` endpoint for credential validation.
370
386
  *
387
+ * The `source` parameter surfaces the CLI's intent to the server-side
388
+ * logging middleware (atxpace/skill-repo#732) via the
389
+ * `X-SkillRepo-Source` header:
390
+ *
391
+ * - `"init"` → server records `cli.bootstrap` in `server_events`.
392
+ * Used by `skillrepo init` — a first-install event that drives
393
+ * the Feed "CLI bootstraps 7d/30d" tile counter.
394
+ * - `"validate"` (default) → server records `cli.validate`. Used by
395
+ * every other authenticated command that revalidates the key.
396
+ *
397
+ * An unrecognized `source` is silently coerced to `"validate"` rather
398
+ * than thrown. The coercion guards against a caller typo or a future
399
+ * value the current client-side taxonomy doesn't know yet — we prefer
400
+ * under-counting bootstraps to fabricating a category the caller
401
+ * didn't mean to use. Servers ignore unknown HTTP header values, so
402
+ * there is no wire-level risk to sending whatever a caller passes;
403
+ * the coercion is purely a safety net for CLI-side typos.
404
+ *
371
405
  * @param {string} serverUrl
372
406
  * @param {string} apiKey
407
+ * @param {string} [source] - `"init"` or `"validate"` (default).
373
408
  * @returns {Promise<AuthValidateResult>}
374
409
  */
375
- export async function validateAccessKey(serverUrl, apiKey) {
410
+ export async function validateAccessKey(serverUrl, apiKey, source = "validate") {
376
411
  const url = `${normalizeUrl(serverUrl)}/api/v1/auth/validate`;
412
+ const normalizedSource = isCliSource(source) ? source : "validate";
377
413
  const res = await safeFetch(url, {
378
414
  method: "POST",
379
- headers: await authHeaders(apiKey),
415
+ headers: {
416
+ ...(await authHeaders(apiKey)),
417
+ "X-SkillRepo-Source": normalizedSource,
418
+ },
380
419
  // POST /auth/validate is side-effect-free on the server (it
381
420
  // only reads the key and returns account context), so retrying
382
421
  // a transient 5xx is safe. 4xx auth errors are NOT retried
@@ -112,6 +112,21 @@ describe("runInit — happy path", () => {
112
112
  assert.match(stdout.text(), /SkillRepo is ready/);
113
113
  });
114
114
 
115
+ it("sends X-SkillRepo-Source: init to /api/v1/auth/validate (Epic 12 #905)", async () => {
116
+ // The init command is the ONLY call path that should report
117
+ // `init` to the server. Every other CLI command reports
118
+ // `validate` by default. This test pins the contract so a silent
119
+ // regression (e.g. removing the source arg in init.mjs, or the
120
+ // http.mjs default drifting) surfaces here.
121
+ await runInit(
122
+ ["--key", VALID_KEY, "--url", serverUrl, "--yes"],
123
+ { stdout, stderr },
124
+ );
125
+ const validateHeaders = server.getLastValidateHeaders();
126
+ assert.ok(validateHeaders, "mock server should have seen a validate call");
127
+ assert.equal(validateHeaders["x-skillrepo-source"], "init");
128
+ });
129
+
115
130
  it("--json outputs structured summary", async () => {
116
131
  await runInit(
117
132
  ["--key", VALID_KEY, "--url", serverUrl, "--yes", "--json"],
@@ -100,6 +100,13 @@ export function createMockServer(initialPayload, options = {}) {
100
100
  // practice this is fine — but a future parallel test runner
101
101
  // would need per-request capture.
102
102
  let lastPostBody = null;
103
+ /**
104
+ * Captures the headers from the most recent POST /api/v1/auth/validate
105
+ * request. Used by tests (e.g. init.test.mjs) to assert the CLI sent
106
+ * the expected `X-SkillRepo-Source` value. Reset to null on each
107
+ * validate-route hit so tests see only the headers they triggered.
108
+ */
109
+ let lastValidateHeaders = null;
103
110
 
104
111
  /**
105
112
  * Validate the Authorization header.
@@ -170,6 +177,12 @@ export function createMockServer(initialPayload, options = {}) {
170
177
 
171
178
  // ── PR2: POST /api/v1/auth/validate ─────────────────────────────
172
179
  if (url.pathname === "/api/v1/auth/validate" && req.method === "POST") {
180
+ // Snapshot request headers before the auth check so tests can
181
+ // assert on the CLI's `X-SkillRepo-Source` header even when the
182
+ // auth check fails (future-proofing — today we only call this
183
+ // after auth succeeds, but the snapshot is ordering-safe either
184
+ // way).
185
+ lastValidateHeaders = { ...req.headers };
173
186
  if (!checkAuth(req, res)) return;
174
187
  res.writeHead(200, { "Content-Type": "application/json" });
175
188
  res.end(JSON.stringify(validateResponse));
@@ -484,5 +497,15 @@ export function createMockServer(initialPayload, options = {}) {
484
497
  getLastPostBody() {
485
498
  return lastPostBody;
486
499
  },
500
+
501
+ /**
502
+ * Return the headers from the most recent POST /api/v1/auth/validate
503
+ * request, or null if none has been made yet. Used by init-command
504
+ * tests to assert the CLI sent the documented
505
+ * `X-SkillRepo-Source` value (Epic 12 #905).
506
+ */
507
+ getLastValidateHeaders() {
508
+ return lastValidateHeaders;
509
+ },
487
510
  };
488
511
  }
@@ -39,6 +39,8 @@ import {
39
39
  searchSkills,
40
40
  addSkillToLibrary,
41
41
  removeSkillFromLibrary,
42
+ isCliSource,
43
+ CLI_SOURCE_VALUES,
42
44
  } from "../../lib/http.mjs";
43
45
  import {
44
46
  CliError,
@@ -91,6 +93,32 @@ const VALID_KEY = "sk_live_test_abc123";
91
93
 
92
94
  // ── validateAccessKey ──────────────────────────────────────────────────
93
95
 
96
+ // ── CLI source header exports ──────────────────────────────────────────
97
+
98
+ describe("CLI_SOURCE_VALUES / isCliSource", () => {
99
+ it("CLI_SOURCE_VALUES declares exactly the documented values", () => {
100
+ // Any new source value requires a server-side taxonomy update
101
+ // (atxpace/skill-repo#732). Pin the declared values so a silent
102
+ // extension without the corresponding server work surfaces here.
103
+ assert.deepEqual([...CLI_SOURCE_VALUES].sort(), ["init", "validate"]);
104
+ });
105
+
106
+ it("isCliSource accepts only the documented values", () => {
107
+ assert.equal(isCliSource("init"), true);
108
+ assert.equal(isCliSource("validate"), true);
109
+ });
110
+
111
+ it("isCliSource rejects everything else", () => {
112
+ assert.equal(isCliSource(""), false);
113
+ assert.equal(isCliSource("INIT"), false);
114
+ assert.equal(isCliSource("validate "), false);
115
+ assert.equal(isCliSource("bootstrap"), false);
116
+ assert.equal(isCliSource(undefined), false);
117
+ assert.equal(isCliSource(null), false);
118
+ assert.equal(isCliSource(42), false);
119
+ });
120
+ });
121
+
94
122
  describe("validateAccessKey", () => {
95
123
  it("returns the account context on 200", async () => {
96
124
  const srv = await startServer((req, res) => {
@@ -118,6 +146,81 @@ describe("validateAccessKey", () => {
118
146
  }
119
147
  });
120
148
 
149
+ it("sends X-SkillRepo-Source: validate by default", async () => {
150
+ // Epic 12 #905: the server's CLI-logging middleware (#732)
151
+ // distinguishes `cli.bootstrap` from `cli.validate` based on this
152
+ // header. The default path MUST be `validate` so only the explicit
153
+ // init caller lands on the bootstrap bucket.
154
+ const srv = await startServer((req, res) => {
155
+ jsonRes(res, 200, {
156
+ userId: "user-1",
157
+ accountId: "acc-1",
158
+ accountSlug: "alice",
159
+ accountName: "Alice",
160
+ scopes: [],
161
+ keyId: "key-1",
162
+ tier: "free",
163
+ });
164
+ });
165
+ try {
166
+ await validateAccessKey(srv.url, VALID_KEY);
167
+ assert.equal(
168
+ srv.lastRequest.headers["x-skillrepo-source"],
169
+ "validate",
170
+ );
171
+ } finally {
172
+ await srv.close();
173
+ }
174
+ });
175
+
176
+ it("sends X-SkillRepo-Source: init when source='init' is passed", async () => {
177
+ const srv = await startServer((req, res) => {
178
+ jsonRes(res, 200, {
179
+ userId: "user-1",
180
+ accountId: "acc-1",
181
+ accountSlug: "alice",
182
+ accountName: "Alice",
183
+ scopes: [],
184
+ keyId: "key-1",
185
+ tier: "free",
186
+ });
187
+ });
188
+ try {
189
+ await validateAccessKey(srv.url, VALID_KEY, "init");
190
+ assert.equal(srv.lastRequest.headers["x-skillrepo-source"], "init");
191
+ } finally {
192
+ await srv.close();
193
+ }
194
+ });
195
+
196
+ it("coerces an unknown source to 'validate' (forward-compat safety)", async () => {
197
+ // A future CLI version may pass a source value the CURRENT server
198
+ // doesn't know about. Rather than fabricating a bogus category,
199
+ // the CLI silently falls back to `validate`. Server-side CLI-
200
+ // bootstrap counts are a floor, never inflated.
201
+ const srv = await startServer((req, res) => {
202
+ jsonRes(res, 200, {
203
+ userId: "user-1",
204
+ accountId: "acc-1",
205
+ accountSlug: "alice",
206
+ accountName: "Alice",
207
+ scopes: [],
208
+ keyId: "key-1",
209
+ tier: "free",
210
+ });
211
+ });
212
+ try {
213
+ // @ts-expect-error — intentional unknown source.
214
+ await validateAccessKey(srv.url, VALID_KEY, "experimental");
215
+ assert.equal(
216
+ srv.lastRequest.headers["x-skillrepo-source"],
217
+ "validate",
218
+ );
219
+ } finally {
220
+ await srv.close();
221
+ }
222
+ });
223
+
121
224
  it("throws authError on 401", async () => {
122
225
  const srv = await startServer((req, res) => {
123
226
  jsonRes(res, 401, { error: "Invalid access key" });