@khanhcan148/mk 0.1.18 → 0.1.19

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/README.md CHANGED
@@ -88,7 +88,7 @@ cp -r .claude ~/.claude/
88
88
 
89
89
  ```
90
90
  ├── .claude/
91
- │ ├── agents/ # 32 agents (5 primary + 27 utility: implementers, quality, docs, specialized, concerns)
91
+ │ ├── agents/ # 36 agents (5 primary + 31 utility: implementers, quality, docs, specialized, concerns, brainstorm critics)
92
92
  │ ├── skills/ # 67 skill packages (SKILL.md + scripts/references/assets)
93
93
  │ │ ├── mk-*/ # 20 workflow commands (/mk-audit, /mk-brainstorm, /mk-log-analysis, /mk-overview, /mk-wiki, etc.)
94
94
  │ │ └── ... # Domain skills (frontend, backend, testing, browser automation, etc.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanhcan148/mk",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "CLI to install and manage MyClaudeKit (.claude/) in your projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -138,6 +138,17 @@ export async function statusAction(deps = {}) {
138
138
  process.stdout.write(chalk.green('Repo access: granted\n'));
139
139
  } else {
140
140
  process.stdout.write(chalk.red('Repo access: denied\n'));
141
- process.stdout.write('Contact the repository owner for collaborator access.\n');
141
+ const hasRepoScope = access.scopes?.some((s) => s === 'repo');
142
+ if (access.status === 404 && access.scopes && !hasRepoScope) {
143
+ // 404 with no 'repo' scope usually means the token can't see the private
144
+ // KIT_REPO — re-authenticating picks up the new default scope.
145
+ const current = access.scopes.length ? access.scopes.join(', ') : 'none';
146
+ process.stdout.write(
147
+ `Token is missing the 'repo' scope (current scopes: ${current}).\n` +
148
+ "Run 'mk auth logout && mk auth login' to re-authenticate with the required scope.\n"
149
+ );
150
+ } else {
151
+ process.stdout.write('Contact the repository owner for collaborator access.\n');
152
+ }
142
153
  }
143
154
  }
@@ -32,7 +32,7 @@ import { isEmptyDir } from '../lib/fs-utils.js';
32
32
  * @param {string} str
33
33
  * @returns {string}
34
34
  */
35
- function stripTerminalEscapes(str) {
35
+ export function stripTerminalEscapes(str) {
36
36
  return str
37
37
  .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '') // CSI sequences
38
38
  .replace(/\x1b\].*?(\x07|\x1b\\)/gs, '') // OSC sequences (dotAll for multiline)
package/src/lib/auth.js CHANGED
@@ -82,8 +82,15 @@ export async function validateToken(token) {
82
82
  /**
83
83
  * Check whether the token has access to the kit repository.
84
84
  *
85
+ * Returns the HTTP status and any scopes reported via the `x-oauth-scopes`
86
+ * response header so callers can disambiguate common failure modes:
87
+ * - 404 + no 'repo' scope → token lacks scope (most common for a private KIT_REPO)
88
+ * - 404 + has 'repo' scope → authenticated account is not a collaborator
89
+ * - 403 → SSO or rate-limit block
90
+ * - 0 → network error
91
+ *
85
92
  * @param {string} token
86
- * @returns {Promise<{ accessible: boolean }>}
93
+ * @returns {Promise<{ accessible: boolean, status: number, scopes: string[] }>}
87
94
  */
88
95
  export async function checkRepoAccess(token) {
89
96
  try {
@@ -93,12 +100,17 @@ export async function checkRepoAccess(token) {
93
100
  Accept: 'application/vnd.github.v3+json'
94
101
  }
95
102
  });
96
- return { accessible: res.ok };
103
+ return { accessible: res.ok, status: res.status, scopes: parseScopes(res) };
97
104
  } catch {
98
- return { accessible: false };
105
+ return { accessible: false, status: 0, scopes: [] };
99
106
  }
100
107
  }
101
108
 
109
+ function parseScopes(res) {
110
+ const raw = res.headers?.get?.('x-oauth-scopes') || '';
111
+ return raw.split(',').map((s) => s.trim()).filter(Boolean);
112
+ }
113
+
102
114
  // ---------------------------------------------------------------------------
103
115
  // OAuth Device Flow
104
116
  // ---------------------------------------------------------------------------
@@ -127,9 +139,15 @@ export async function startDeviceFlow(opts = {}) {
127
139
  'Content-Type': 'application/x-www-form-urlencoded',
128
140
  Accept: 'application/json'
129
141
  },
130
- // Empty scope sufficient for public repos (5000 req/hr). Set MK_OAUTH_SCOPE=repo for private forks.
142
+ // KIT_REPO is private, so 'repo' scope is required to read it. Default to 'repo'.
143
+ // Override with MK_OAUTH_SCOPE for public forks (e.g. MK_OAUTH_SCOPE='' for no scope,
144
+ // or MK_OAUTH_SCOPE='public_repo' for public-only access).
145
+ // `??` (not `||`) so an explicit empty string is honored as an opt-out.
131
146
  // Use URLSearchParams to prevent parameter injection via env var containing '&' chars.
132
- body: new URLSearchParams({ client_id: GITHUB_CLIENT_ID, scope: process.env.MK_OAUTH_SCOPE || '' }).toString()
147
+ body: new URLSearchParams({
148
+ client_id: GITHUB_CLIENT_ID,
149
+ scope: process.env.MK_OAUTH_SCOPE ?? 'repo'
150
+ }).toString()
133
151
  });
134
152
 
135
153
  if (!codeRes.ok) {
package/src/lib/copy.js CHANGED
@@ -81,6 +81,14 @@ export function collectDiskFiles(targetDir) {
81
81
  return results;
82
82
  }
83
83
 
84
+ /**
85
+ * @typedef {Object} FileEntry
86
+ * @property {string} relativePath - Path relative to the target .claude/ (POSIX separators, e.g. ".claude/agents/foo.md")
87
+ * @property {string} absolutePath - Destination absolute path under targetDir (where the file is copied to)
88
+ * @property {string} sourceAbsPath - Source absolute path under sourceDir (where the file is copied from)
89
+ * @property {number} size - File size in bytes, captured at copy time
90
+ */
91
+
84
92
  /**
85
93
  * Copy kit files from sourceDir (.claude/) to targetDir (.claude/).
86
94
  * Only copies KIT_SUBDIRS (agents/, skills/, workflows/).
@@ -88,7 +96,7 @@ export function collectDiskFiles(targetDir) {
88
96
  * @param {string} sourceDir - Absolute path to source .claude/
89
97
  * @param {string} targetDir - Absolute path to target .claude/
90
98
  * @param {{ dryRun: boolean }} options
91
- * @returns {Array<{ relativePath: string, absolutePath: string, sourceAbsPath: string, size: number }>}
99
+ * @returns {FileEntry[]}
92
100
  * @remarks Naming convention: `absolutePath` is the destination (under targetDir),
93
101
  * `sourceAbsPath` is the source (under sourceDir). The asymmetry is intentional —
94
102
  * renaming would break consumers (update.js). See DEBT-016.
@@ -39,6 +39,24 @@ export function assertGitHubHostname(url) {
39
39
  } catch {
40
40
  throw new Error(`SSRF guard: invalid URL "${url}"`);
41
41
  }
42
+ // H9/H10: block non-TLS schemes. Plain http:// to github.com can be MITM-redirected,
43
+ // and the kit download path has no reason to accept anything but https.
44
+ if (parsed.protocol !== 'https:') {
45
+ // Truncate to guard against log injection via crafted long / newline-containing schemes.
46
+ const safeScheme = String(parsed.protocol).slice(0, 20);
47
+ throw new Error(
48
+ `SSRF guard: scheme "${safeScheme}" is not allowed. ` +
49
+ `Only https: is permitted for kit downloads.`
50
+ );
51
+ }
52
+ // Block userinfo-prefixed URLs (user:pass@host) — they can mask the true
53
+ // hostname in server-side url parsers or confuse logging/auditing.
54
+ if (parsed.username || parsed.password) {
55
+ throw new Error(
56
+ `SSRF guard: userinfo-prefixed URL is not allowed. ` +
57
+ `Credentials in the URL (user:pass@host) are rejected for kit downloads.`
58
+ );
59
+ }
42
60
  const { hostname } = parsed;
43
61
  if (!ALLOWED_HOSTS.has(hostname)) {
44
62
  throw new Error(