@moda-ai/cli 1.36.0 → 1.38.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.
@@ -0,0 +1,128 @@
1
+ import {
2
+ SKILL_SOURCE_INTEGRATION_BUNDLED,
3
+ claudeSkillToCursorRule,
4
+ detectAgents,
5
+ packageAssetPath,
6
+ readSkillsManifest,
7
+ upsertSkillManifestRecord
8
+ } from "./cli-ssfq84k5.js";
9
+
10
+ // src/skills-bundled.ts
11
+ import { createHash } from "node:crypto";
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { dirname, join, relative } from "node:path";
14
+ var BUNDLED_SKILLS_DIR = packageAssetPath("skills", "integration");
15
+ function bundledSkillsDir() {
16
+ return process.env.MODA_BUNDLED_SKILLS_DIR || BUNDLED_SKILLS_DIR;
17
+ }
18
+ function readBundledIndex() {
19
+ const indexPath = join(bundledSkillsDir(), "index.json");
20
+ if (!existsSync(indexPath))
21
+ return null;
22
+ try {
23
+ const parsed = JSON.parse(readFileSync(indexPath, "utf8"));
24
+ if (!Array.isArray(parsed.skills))
25
+ return null;
26
+ return parsed;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ function readBundledSkillMd(skillId) {
32
+ const path = join(bundledSkillsDir(), skillId, "SKILL.md");
33
+ if (!existsSync(path))
34
+ return null;
35
+ try {
36
+ return readFileSync(path, "utf8");
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ function installIntegrationSkill(skillId, cwd = process.cwd()) {
42
+ if (!/^[a-z0-9-]+$/.test(skillId)) {
43
+ throw new Error(`Invalid integration skill id: ${JSON.stringify(skillId).slice(0, 80)}`);
44
+ }
45
+ const sourceDir = join(bundledSkillsDir(), skillId);
46
+ if (!existsSync(join(sourceDir, "SKILL.md"))) {
47
+ const bundled = readBundledIndex()?.skills.map((skill) => skill.id) ?? [];
48
+ throw new Error(`Integration skill '${skillId}' is not bundled with this CLI.${bundled.length > 0 ? ` Bundled skills: ${bundled.join(", ")}` : ""}`);
49
+ }
50
+ const files = listFilesRecursive(sourceDir).sort();
51
+ const plannedWrites = [];
52
+ for (const file of files) {
53
+ const relPath = relative(sourceDir, file).replace(/\\/g, "/");
54
+ plannedWrites.push({
55
+ targetRelPath: `.claude/skills/${skillId}/${relPath}`,
56
+ targetPath: join(cwd, ".claude", "skills", skillId, relPath),
57
+ content: readFileSync(file, "utf8")
58
+ });
59
+ }
60
+ const writeCursor = detectAgents().includes("Cursor") || existsSync(join(cwd, ".cursor"));
61
+ if (writeCursor) {
62
+ const skillMd = readFileSync(join(sourceDir, "SKILL.md"), "utf8");
63
+ plannedWrites.push({
64
+ targetRelPath: `.cursor/rules/${skillId}.mdc`,
65
+ targetPath: join(cwd, ".cursor", "rules", `${skillId}.mdc`),
66
+ content: claudeSkillToCursorRule(skillMd)
67
+ });
68
+ }
69
+ const ownedPaths = new Set((readSkillsManifest(cwd)?.records ?? []).flatMap((record) => record.paths));
70
+ const collisions = plannedWrites.filter((write) => existsSync(write.targetPath) && !ownedPaths.has(write.targetRelPath));
71
+ if (collisions.length > 0) {
72
+ throw new Error(`Refusing to overwrite ${collisions.length} existing file(s) not managed by Moda: ` + `${collisions.map((write) => write.targetRelPath).join(", ")}. ` + "Move or remove them, then re-run to install the bundled integration skill.");
73
+ }
74
+ const paths = [];
75
+ let wroteAny = false;
76
+ for (const write of plannedWrites) {
77
+ if (writeIfChanged(write.targetPath, write.content))
78
+ wroteAny = true;
79
+ paths.push(write.targetRelPath);
80
+ }
81
+ upsertSkillManifestRecord(cwd, {
82
+ skillId,
83
+ contentHash: hashSkillDir(sourceDir, files),
84
+ source: SKILL_SOURCE_INTEGRATION_BUNDLED,
85
+ paths
86
+ });
87
+ return { skillId, status: wroteAny ? "installed" : "unchanged", paths };
88
+ }
89
+ function writeIfChanged(path, content) {
90
+ if (existsSync(path)) {
91
+ try {
92
+ if (readFileSync(path, "utf8") === content)
93
+ return false;
94
+ } catch {}
95
+ }
96
+ mkdirSync(dirname(path), { recursive: true });
97
+ writeFileSync(path, content);
98
+ return true;
99
+ }
100
+ function listFilesRecursive(dir) {
101
+ const files = [];
102
+ const stack = [dir];
103
+ while (stack.length > 0) {
104
+ const current = stack.pop();
105
+ if (!current)
106
+ continue;
107
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
108
+ const path = join(current, entry.name);
109
+ if (entry.isDirectory())
110
+ stack.push(path);
111
+ else if (entry.isFile())
112
+ files.push(path);
113
+ }
114
+ }
115
+ return files;
116
+ }
117
+ function hashSkillDir(sourceDir, files) {
118
+ const hash = createHash("sha256");
119
+ for (const file of files) {
120
+ hash.update(relative(sourceDir, file).replace(/\\/g, "/"));
121
+ hash.update("\x00");
122
+ hash.update(readFileSync(file));
123
+ hash.update("\x00");
124
+ }
125
+ return hash.digest("hex");
126
+ }
127
+
128
+ export { bundledSkillsDir, readBundledIndex, readBundledSkillMd, installIntegrationSkill };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  authFetch,
3
3
  getCachedLoginContext
4
- } from "./cli-yz99t7n8.js";
4
+ } from "./cli-kegd748v.js";
5
5
  import {
6
6
  CliAccessError,
7
7
  CliInputError,
@@ -15,7 +15,7 @@ import {
15
15
  resolveTenantIdFromActiveProfile,
16
16
  saveCliConfig,
17
17
  writeSecretJsonFile
18
- } from "./cli-drabjmaj.js";
18
+ } from "./cli-q79sq80a.js";
19
19
 
20
20
  // src/init/tenant.ts
21
21
  import { randomUUID } from "node:crypto";
@@ -1,14 +1,16 @@
1
1
  import {
2
2
  CliAuthError,
3
3
  CliInputError,
4
- __require,
5
4
  getModaPaths,
6
5
  loadProfileConfig,
7
6
  resolveModaBaseUrl,
8
7
  saveProfileConfig,
9
8
  secretFilePathFromRef,
10
9
  writeSecretJsonFile
11
- } from "./cli-drabjmaj.js";
10
+ } from "./cli-q79sq80a.js";
11
+ import {
12
+ __require
13
+ } from "./cli-0v6na3yp.js";
12
14
 
13
15
  // src/auth-storage.ts
14
16
  import { existsSync, readFileSync, rmSync } from "node:fs";
@@ -326,6 +328,71 @@ function getCachedLoginContext(options = {}) {
326
328
  return null;
327
329
  return cachedLoginContext.context;
328
330
  }
331
+ var MISMATCH_MESSAGE = "The sign-in code is for a different workspace than --tenant-id.";
332
+ var CONNECT_CODE_RE = /^mcc_[A-Za-z0-9_-]{43}$/;
333
+
334
+ class ConnectCodeError extends Error {
335
+ reason;
336
+ constructor(message, reason) {
337
+ super(message);
338
+ this.reason = reason;
339
+ this.name = "ConnectCodeError";
340
+ }
341
+ }
342
+ async function connectWithCode(code, options = {}) {
343
+ if (!CONNECT_CODE_RE.test(code)) {
344
+ throw new ConnectCodeError("That is not a Moda sign-in code. Copy the command from the Moda onboarding page again.", "rejected");
345
+ }
346
+ let response;
347
+ try {
348
+ response = await fetch(`${resolveModaBaseUrl(options)}/api/cli/auth/connect`, {
349
+ method: "POST",
350
+ headers: { "Content-Type": "application/json" },
351
+ body: JSON.stringify({ code, label: hostname(), ...options.tenantId ? { tenant_id: options.tenantId } : {} }),
352
+ redirect: "manual",
353
+ signal: AbortSignal.timeout(EXCHANGE_TIMEOUT_MS)
354
+ });
355
+ } catch {
356
+ throw new ConnectCodeError("Could not reach Moda to use the sign-in code.", "unreachable");
357
+ }
358
+ if (response.status === 400) {
359
+ const body = await response.json().catch(() => ({}));
360
+ if (body.error === "tenant_mismatch") {
361
+ throw new ConnectCodeError(MISMATCH_MESSAGE, "workspace_mismatch");
362
+ }
363
+ }
364
+ if (response.status === 401 || response.status === 400) {
365
+ throw new ConnectCodeError("The sign-in code was rejected: it expired (10 minutes) or was already used.", "rejected");
366
+ }
367
+ if (!response.ok || response.status >= 300 && response.status < 400) {
368
+ throw new ConnectCodeError(`Moda could not use the sign-in code (HTTP ${response.status}).`, "failed");
369
+ }
370
+ const payload = await response.json().catch(() => ({}));
371
+ const cliToken = typeof payload.cli_token === "string" ? payload.cli_token : undefined;
372
+ const expiresAtMs = typeof payload.expires_at === "string" ? Date.parse(payload.expires_at) : NaN;
373
+ const tenantId = typeof payload.tenant_id === "string" ? payload.tenant_id : undefined;
374
+ if (!cliToken || !Number.isFinite(expiresAtMs) || !tenantId) {
375
+ throw new ConnectCodeError("Moda returned an incomplete response for the sign-in code.", "failed");
376
+ }
377
+ if (options.tenantId && tenantId !== options.tenantId) {
378
+ throw new ConnectCodeError(MISMATCH_MESSAGE, "workspace_mismatch");
379
+ }
380
+ const userEmail = typeof payload.user?.email === "string" ? payload.user.email : undefined;
381
+ persistAuthSession({
382
+ access_token: cliToken,
383
+ expires_at: Math.floor(expiresAtMs / 1000),
384
+ profile: resolveStorageProfile(options),
385
+ token_type: "bearer",
386
+ token_kind: "moda_ct",
387
+ ...userEmail ? { user_email: userEmail } : {}
388
+ }, options);
389
+ const context = {
390
+ user: { email: userEmail ?? "", name: typeof payload.user?.name === "string" ? payload.user.name : null },
391
+ tenants: Array.isArray(payload.tenants) ? payload.tenants : []
392
+ };
393
+ cachedLoginContext = { profile: resolveStorageProfile(options), context };
394
+ return { ...context, tenantId };
395
+ }
329
396
  var SUCCESS_HTML = `<!DOCTYPE html>
330
397
  <html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#fafafa">
331
398
  <div style="text-align:center">
@@ -759,4 +826,4 @@ function safeResolveUrl(location, base) {
759
826
  }
760
827
  }
761
828
 
762
- export { isAuthSessionValid, loadToken, loadAuthSession, clearAuthSession, revokeCliSessionToken, getCachedLoginContext, login, authFetch };
829
+ export { isAuthSessionValid, loadToken, loadAuthSession, clearAuthSession, revokeCliSessionToken, getCachedLoginContext, ConnectCodeError, connectWithCode, login, authFetch };
@@ -1,37 +1,3 @@
1
- import { createRequire } from "node:module";
2
- var __create = Object.create;
3
- var __getProtoOf = Object.getPrototypeOf;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- function __accessProp(key) {
8
- return this[key];
9
- }
10
- var __toESMCache_node;
11
- var __toESMCache_esm;
12
- var __toESM = (mod, isNodeMode, target) => {
13
- var canCache = mod != null && typeof mod === "object";
14
- if (canCache) {
15
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
- var cached = cache.get(mod);
17
- if (cached)
18
- return cached;
19
- }
20
- target = mod != null ? __create(__getProtoOf(mod)) : {};
21
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
- for (let key of __getOwnPropNames(mod))
23
- if (!__hasOwnProp.call(to, key))
24
- __defProp(to, key, {
25
- get: __accessProp.bind(mod, key),
26
- enumerable: true
27
- });
28
- if (canCache)
29
- cache.set(mod, to);
30
- return to;
31
- };
32
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
34
-
35
1
  // src/profiles.ts
36
2
  import {
37
3
  chmodSync,
@@ -1600,4 +1566,4 @@ function createCommandContext(input, options = {}) {
1600
1566
  };
1601
1567
  }
1602
1568
 
1603
- export { __toESM, __commonJS, __require, PROD_BASE_URL, DEFAULT_INGEST_URL, getModaPaths, getModaDir, isLoopbackBaseUrl, assertValidProfileName, loadProfileConfig, saveProfileConfig, getActiveProfile, setActiveProfile, upsertProfile, clearStoredApiKeyCredentials, secretFilePathFromRef, resolveSecretReference, resolveApiKeyFromActiveProfile, resolveTenantIdFromActiveProfile, ensureSecretFilePermissions, writeSecretJsonFile, CliInputError, CliAccessError, CliInputRequiredError, CliAuthError, CliCancellationError, isNetworkLikeError, isApiLikeError, VERSION, MODA_BASE_URL, loadCliConfig, saveCliConfig, resolveApiKey, embeddedTenantIdFromApiKey, resolveIngestUrl2 as resolveIngestUrl, resolveTenantId, resolveModaBaseUrl, validateConfig, MAX_SCANNED_FILE_BYTES, DEFAULT_MAX_INVENTORY_FILES, DEFAULT_MAX_INVENTORY_TEXT_BYTES, MAX_RECORDED_SKIPS, SKIPPED_DIRS, isSkippedDir, isSkippedPathPrefix, isEvalLikePath, shouldInventoryFile, isTextFile, isSecretishPath, SECRET_KEYS, redactDeep, redactString, packSnapshot, EXIT_OK, EXIT_ERROR, EXIT_DEGRADED, EXIT_AUTH_REQUIRED, EXIT_INPUT_REQUIRED, EXIT_CANCELLED, AGENT_ERROR_CODES, wasOutputCancelled, safeWriteStdout, safeWriteStderr, resolveOutputMode, createCliOutput, stringOption, createCommandContext };
1569
+ export { PROD_BASE_URL, DEFAULT_INGEST_URL, getModaPaths, getModaDir, isLoopbackBaseUrl, assertValidProfileName, loadProfileConfig, saveProfileConfig, getActiveProfile, setActiveProfile, upsertProfile, clearStoredApiKeyCredentials, secretFilePathFromRef, resolveSecretReference, resolveApiKeyFromActiveProfile, resolveTenantIdFromActiveProfile, ensureSecretFilePermissions, writeSecretJsonFile, CliInputError, CliAccessError, CliInputRequiredError, CliAuthError, CliCancellationError, isNetworkLikeError, isApiLikeError, VERSION, MODA_BASE_URL, loadCliConfig, saveCliConfig, resolveApiKey, embeddedTenantIdFromApiKey, resolveIngestUrl2 as resolveIngestUrl, resolveTenantId, resolveModaBaseUrl, validateConfig, MAX_SCANNED_FILE_BYTES, DEFAULT_MAX_INVENTORY_FILES, DEFAULT_MAX_INVENTORY_TEXT_BYTES, MAX_RECORDED_SKIPS, SKIPPED_DIRS, isSkippedDir, isSkippedPathPrefix, isEvalLikePath, shouldInventoryFile, isTextFile, isSecretishPath, SECRET_KEYS, redactDeep, redactString, packSnapshot, EXIT_OK, EXIT_ERROR, EXIT_DEGRADED, EXIT_AUTH_REQUIRED, EXIT_INPUT_REQUIRED, EXIT_CANCELLED, AGENT_ERROR_CODES, wasOutputCancelled, safeWriteStdout, safeWriteStderr, resolveOutputMode, createCliOutput, stringOption, createCommandContext };
@@ -0,0 +1,393 @@
1
+ // src/package-assets.ts
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ function packageAssetPath(...parts) {
5
+ return join(dirname(fileURLToPath(import.meta.url)), "..", ...parts);
6
+ }
7
+
8
+ // src/init/rules.ts
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { join as join2 } from "node:path";
11
+ import { confirm } from "@inquirer/prompts";
12
+ var SKILLS_SOURCE_DIR = packageAssetPath("skills");
13
+ var SKILLS = [
14
+ {
15
+ id: "moda-cli",
16
+ label: "moda-cli — let coding agents query Moda analytics via the CLI"
17
+ }
18
+ ];
19
+ var ADAPTERS = {
20
+ "Claude Code": {
21
+ name: "Claude Code",
22
+ targetFor: (skillId) => ({
23
+ dir: join2(".claude", "skills", skillId),
24
+ filename: "SKILL.md"
25
+ }),
26
+ transform: (content) => content
27
+ },
28
+ Cursor: {
29
+ name: "Cursor",
30
+ targetFor: (skillId) => ({
31
+ dir: join2(".cursor", "rules"),
32
+ filename: `${skillId}.mdc`
33
+ }),
34
+ transform: claudeSkillToCursorRule
35
+ }
36
+ };
37
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
38
+ var DESCRIPTION_RE = /^description:\s*(.+?)\s*$/m;
39
+ function claudeSkillToCursorRule(skillContent) {
40
+ const match = FRONTMATTER_RE.exec(skillContent);
41
+ if (!match) {
42
+ return skillContent;
43
+ }
44
+ const [, frontmatter, body] = match;
45
+ const descMatch = DESCRIPTION_RE.exec(frontmatter);
46
+ const description = descMatch?.[1].trim() ?? "";
47
+ const cursorFrontmatter = `---
48
+ ` + `description: ${JSON.stringify(description)}
49
+ ` + `alwaysApply: false
50
+ ` + `---
51
+ `;
52
+ return `${cursorFrontmatter}
53
+ ${body.replace(/^\s+/, "")}`;
54
+ }
55
+ function loadSkillSource(skillId) {
56
+ const path = join2(SKILLS_SOURCE_DIR, skillId, "SKILL.md");
57
+ if (!existsSync(path)) {
58
+ throw new Error(`Skill source missing: ${path}`);
59
+ }
60
+ return readFileSync(path, "utf8");
61
+ }
62
+ function installSkillForAgent(skill, adapter, content) {
63
+ const { dir, filename } = adapter.targetFor(skill.id);
64
+ const dirPath = join2(process.cwd(), dir);
65
+ const filePath = join2(dirPath, filename);
66
+ const relativePath = join2(dir, filename);
67
+ try {
68
+ mkdirSync(dirPath, { recursive: true });
69
+ if (existsSync(filePath)) {
70
+ return { agent: adapter.name, relativePath, status: "exists" };
71
+ }
72
+ writeFileSync(filePath, adapter.transform(content));
73
+ return { agent: adapter.name, relativePath, status: "wrote" };
74
+ } catch (err) {
75
+ return {
76
+ agent: adapter.name,
77
+ relativePath,
78
+ status: "failed",
79
+ error: err instanceof Error ? err.message : String(err)
80
+ };
81
+ }
82
+ }
83
+ async function installRules(detectedAgents, options = {}) {
84
+ const adapters = detectedAgents.map((a) => ADAPTERS[a]).filter((a) => a !== undefined);
85
+ if (adapters.length === 0) {
86
+ if (!options.silent)
87
+ console.log(" No supported coding agents detected");
88
+ return;
89
+ }
90
+ const agentList = adapters.map((a) => a.name).join(", ");
91
+ const skillList = SKILLS.map((s) => `
92
+ • ${s.label}`).join("");
93
+ const shouldInstall = options.yes ? true : await confirm({
94
+ message: `Install Moda skills for ${agentList}?${skillList}
95
+ Continue?`,
96
+ default: true
97
+ });
98
+ if (!shouldInstall) {
99
+ if (!options.silent)
100
+ console.log(" Skipped");
101
+ return;
102
+ }
103
+ let wrote = 0;
104
+ let skipped = 0;
105
+ let failed = 0;
106
+ for (const skill of SKILLS) {
107
+ let content;
108
+ try {
109
+ content = loadSkillSource(skill.id);
110
+ } catch (err) {
111
+ const message = err instanceof Error ? err.message : String(err);
112
+ console.error(` Warning: ${message}`);
113
+ failed += adapters.length;
114
+ continue;
115
+ }
116
+ for (const adapter of adapters) {
117
+ const outcome = installSkillForAgent(skill, adapter, content);
118
+ switch (outcome.status) {
119
+ case "wrote":
120
+ if (!options.silent)
121
+ console.log(` ${outcome.agent}: wrote ${outcome.relativePath}`);
122
+ wrote++;
123
+ break;
124
+ case "exists":
125
+ if (!options.silent) {
126
+ console.log(` ${outcome.agent}: ${outcome.relativePath} already exists — skipping`);
127
+ }
128
+ skipped++;
129
+ break;
130
+ case "failed":
131
+ if (!options.silent) {
132
+ console.error(` ${outcome.agent}: failed to write ${outcome.relativePath} — ${outcome.error}`);
133
+ }
134
+ failed++;
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ const summary = [
140
+ wrote && `${wrote} written`,
141
+ skipped && `${skipped} already present`,
142
+ failed && `${failed} failed`
143
+ ].filter(Boolean).join(", ");
144
+ if (summary) {
145
+ if (!options.silent)
146
+ console.log(` Done — ${summary}`);
147
+ }
148
+ }
149
+
150
+ // src/init/mcp.ts
151
+ import { existsSync as existsSync2 } from "node:fs";
152
+ import { join as join3 } from "node:path";
153
+ import { execSync } from "node:child_process";
154
+ function isOnPath(cmd) {
155
+ try {
156
+ execSync(`which ${cmd}`, { stdio: "pipe" });
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+ function detectAgents() {
163
+ const cwd = process.cwd();
164
+ const agents = [];
165
+ if (existsSync2(join3(cwd, ".claude")) || isOnPath("claude")) {
166
+ agents.push("Claude Code");
167
+ }
168
+ if (existsSync2(join3(cwd, ".cursor"))) {
169
+ agents.push("Cursor");
170
+ }
171
+ return agents;
172
+ }
173
+
174
+ // src/skills-manifest.ts
175
+ import {
176
+ existsSync as existsSync3,
177
+ mkdirSync as mkdirSync2,
178
+ readdirSync,
179
+ readFileSync as readFileSync2,
180
+ rmdirSync,
181
+ unlinkSync,
182
+ writeFileSync as writeFileSync2
183
+ } from "node:fs";
184
+ import { dirname as dirname2, isAbsolute, join as join4, normalize, relative, resolve } from "node:path";
185
+ var SKILLS_MANIFEST_PATH = join4(".moda", "skills.yml");
186
+ var SKILL_SOURCE_HARNESS = "moda";
187
+ var SKILL_SOURCE_INTEGRATION_BUNDLED = "integration-bundled";
188
+ function readSkillsManifest(cwd) {
189
+ const manifestPath = join4(cwd, SKILLS_MANIFEST_PATH);
190
+ if (!existsSync3(manifestPath))
191
+ return null;
192
+ let text = "";
193
+ try {
194
+ text = readFileSync2(manifestPath, "utf8");
195
+ } catch {
196
+ return null;
197
+ }
198
+ return {
199
+ tenantId: readYamlScalar(text, "tenant_id"),
200
+ generationRunId: readYamlScalar(text, "generation_run_id"),
201
+ generationVersion: Number(readYamlScalar(text, "generation_version") || 0),
202
+ releaseKey: readYamlScalar(text, "release_key"),
203
+ statusFilter: readYamlScalar(text, "status_filter"),
204
+ releasedAt: readYamlScalar(text, "released_at"),
205
+ managedPaths: readManagedPaths(text),
206
+ records: readSkillRecords(text)
207
+ };
208
+ }
209
+ function manifestHeaderFrom(manifest) {
210
+ return {
211
+ tenantId: manifest?.tenantId ?? "",
212
+ releaseKey: manifest?.releaseKey ?? "",
213
+ statusFilter: manifest?.statusFilter ?? "",
214
+ generationRunId: manifest?.generationRunId ?? "",
215
+ generationVersion: manifest?.generationVersion ?? 0,
216
+ releasedAt: manifest?.releasedAt ?? ""
217
+ };
218
+ }
219
+ function writeSkillsManifest(cwd, header, records) {
220
+ const manifestPath = join4(cwd, SKILLS_MANIFEST_PATH);
221
+ const managedPaths = [...new Set(records.flatMap((record) => record.paths))].sort();
222
+ const lines = [
223
+ "version: 1",
224
+ "source: moda",
225
+ `tenant_id: ${yamlQuote(header.tenantId)}`,
226
+ `release_key: ${yamlQuote(header.releaseKey)}`,
227
+ `status_filter: ${yamlQuote(header.statusFilter)}`,
228
+ `generation_run_id: ${yamlQuote(header.generationRunId)}`,
229
+ `generation_version: ${header.generationVersion}`,
230
+ `released_at: ${yamlQuote(header.releasedAt)}`,
231
+ `installed_at: ${yamlQuote(new Date().toISOString())}`,
232
+ "managed_paths:",
233
+ ...managedPaths.map((path) => ` - ${yamlQuote(path)}`),
234
+ "skills:",
235
+ ...records.flatMap((record) => [
236
+ ` - skill_id: ${yamlQuote(record.skillId)}`,
237
+ ` content_hash: ${yamlQuote(record.contentHash)}`,
238
+ ` source: ${yamlQuote(record.source)}`,
239
+ " paths:",
240
+ ...record.paths.map((path) => ` - ${yamlQuote(path)}`)
241
+ ]),
242
+ ""
243
+ ];
244
+ mkdirSync2(dirname2(manifestPath), { recursive: true });
245
+ writeFileSync2(manifestPath, lines.join(`
246
+ `));
247
+ }
248
+ function upsertSkillManifestRecord(cwd, record) {
249
+ const manifest = readSkillsManifest(cwd);
250
+ const records = (manifest?.records ?? []).filter((existing) => !(existing.skillId === record.skillId && existing.source === record.source));
251
+ records.push(record);
252
+ writeSkillsManifest(cwd, manifestHeaderFrom(manifest), records);
253
+ }
254
+ function pruneManagedSkillPaths(cwd, managedPaths, log) {
255
+ let pruned = 0;
256
+ for (const rawPath of [...new Set(managedPaths)]) {
257
+ const safePath = safeManagedRelativePath(rawPath);
258
+ if (!safePath)
259
+ continue;
260
+ const absolutePath = absoluteManagedPath(cwd, safePath);
261
+ if (!absolutePath || !existsSync3(absolutePath))
262
+ continue;
263
+ try {
264
+ unlinkSync(absolutePath);
265
+ pruned++;
266
+ log(` pruned ${safePath}`);
267
+ pruneEmptyClaudeSkillDir(cwd, safePath);
268
+ } catch {}
269
+ }
270
+ return pruned;
271
+ }
272
+ function pruneEmptyClaudeSkillDir(cwd, relativePath) {
273
+ if (!relativePath.startsWith(".claude/skills/") || !relativePath.endsWith("/SKILL.md"))
274
+ return;
275
+ const dir = dirname2(join4(cwd, relativePath));
276
+ try {
277
+ if (existsSync3(dir) && readdirSync(dir).length === 0) {
278
+ rmdirSync(dir);
279
+ }
280
+ } catch {}
281
+ }
282
+ function absoluteManagedPath(cwd, relativePath) {
283
+ const abs = resolve(cwd, relativePath);
284
+ const root = resolve(cwd);
285
+ const rel = relative(root, abs);
286
+ if (rel.startsWith("..") || isAbsolute(rel))
287
+ return null;
288
+ return abs;
289
+ }
290
+ function safeManagedRelativePath(rawPath) {
291
+ if (!rawPath || rawPath.includes("\x00") || isAbsolute(rawPath))
292
+ return null;
293
+ const normalized = normalize(rawPath).replace(/\\/g, "/");
294
+ if (normalized.startsWith("../") || normalized === ".." || normalized.includes("/../"))
295
+ return null;
296
+ const isClaudeSkill = normalized.startsWith(".claude/skills/") && normalized.endsWith("/SKILL.md");
297
+ const isCursorRule = normalized.startsWith(".cursor/rules/") && normalized.endsWith(".mdc");
298
+ return isClaudeSkill || isCursorRule ? normalized : null;
299
+ }
300
+ function readYamlScalar(text, key) {
301
+ const match = text.match(new RegExp(`^${key}:\\s*(.*?)\\s*$`, "m"));
302
+ return match ? parseYamlScalar(match[1]) : "";
303
+ }
304
+ function readSkillRecords(text) {
305
+ const records = [];
306
+ let inSkills = false;
307
+ let current = null;
308
+ let inPaths = false;
309
+ for (const line of text.split(/\r?\n/)) {
310
+ if (line.trim() === "skills:") {
311
+ inSkills = true;
312
+ continue;
313
+ }
314
+ if (!inSkills)
315
+ continue;
316
+ if (line.length > 0 && !line.startsWith(" "))
317
+ break;
318
+ const skillIdMatch = line.match(/^ {2}- skill_id:\s*(.*?)\s*$/);
319
+ if (skillIdMatch) {
320
+ if (current)
321
+ records.push(current);
322
+ current = {
323
+ skillId: parseYamlScalar(skillIdMatch[1]),
324
+ contentHash: "",
325
+ source: SKILL_SOURCE_HARNESS,
326
+ paths: []
327
+ };
328
+ inPaths = false;
329
+ continue;
330
+ }
331
+ if (!current)
332
+ continue;
333
+ const hashMatch = line.match(/^ {4}content_hash:\s*(.*?)\s*$/);
334
+ if (hashMatch) {
335
+ current.contentHash = parseYamlScalar(hashMatch[1]);
336
+ continue;
337
+ }
338
+ const sourceMatch = line.match(/^ {4}source:\s*(.*?)\s*$/);
339
+ if (sourceMatch) {
340
+ current.source = parseYamlScalar(sourceMatch[1]) || SKILL_SOURCE_HARNESS;
341
+ continue;
342
+ }
343
+ if (/^ {4}paths:\s*$/.test(line)) {
344
+ inPaths = true;
345
+ continue;
346
+ }
347
+ const pathMatch = inPaths ? line.match(/^ {6}-\s*(.*?)\s*$/) : null;
348
+ if (pathMatch) {
349
+ const value = parseYamlScalar(pathMatch[1]);
350
+ if (value)
351
+ current.paths.push(value);
352
+ }
353
+ }
354
+ if (current)
355
+ records.push(current);
356
+ return records;
357
+ }
358
+ function readManagedPaths(text) {
359
+ const paths = [];
360
+ let inManagedPaths = false;
361
+ for (const line of text.split(/\r?\n/)) {
362
+ if (line.trim() === "managed_paths:") {
363
+ inManagedPaths = true;
364
+ continue;
365
+ }
366
+ if (inManagedPaths && line.length > 0 && !line.startsWith(" "))
367
+ break;
368
+ if (!inManagedPaths)
369
+ continue;
370
+ const match = line.match(/^\s*-\s*(.*?)\s*$/);
371
+ if (match)
372
+ paths.push(parseYamlScalar(match[1]));
373
+ }
374
+ return paths.filter(Boolean);
375
+ }
376
+ function parseYamlScalar(raw) {
377
+ const value = raw.trim();
378
+ if (!value)
379
+ return "";
380
+ if (value.startsWith('"')) {
381
+ try {
382
+ return String(JSON.parse(value));
383
+ } catch {
384
+ return value.replace(/^"|"$/g, "");
385
+ }
386
+ }
387
+ return value.replace(/^'|'$/g, "");
388
+ }
389
+ function yamlQuote(value) {
390
+ return JSON.stringify(value);
391
+ }
392
+
393
+ export { packageAssetPath, claudeSkillToCursorRule, installRules, detectAgents, SKILL_SOURCE_HARNESS, SKILL_SOURCE_INTEGRATION_BUNDLED, readSkillsManifest, writeSkillsManifest, upsertSkillManifestRecord, pruneManagedSkillPaths };