crewx 0.8.7-rc.2 → 0.8.7-rc.21

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.
Files changed (94) hide show
  1. package/dist/assets/MarketPage-aY16UW1_.js +51 -0
  2. package/dist/assets/{PromptTab-DPxKgALd.js → PromptTab-C3aTJS6u.js} +7 -7
  3. package/dist/assets/_baseUniq-DxEFlwNt.js +1 -0
  4. package/dist/assets/{arc-Wd7DG8CX.js → arc-BCQAokB1.js} +1 -1
  5. package/dist/assets/architectureDiagram-UYN6MBPD-Bc87ujv2.js +36 -0
  6. package/dist/assets/blockDiagram-ZHA2E4KO-wC9uH8Lb.js +121 -0
  7. package/dist/assets/{c4Diagram-6F5ED5ID-CcfPHYky.js → c4Diagram-6F5ED5ID-CqB7lqni.js} +1 -1
  8. package/dist/assets/channel-DNLHkiNp.js +1 -0
  9. package/dist/assets/{chunk-5HRBRIJM-TGnRph8o.js → chunk-5HRBRIJM-Cc1L17Eh.js} +1 -1
  10. package/dist/assets/{chunk-7U56Z5CX-D24Jb5U0.js → chunk-7U56Z5CX-Df3nu_7G.js} +1 -1
  11. package/dist/assets/{chunk-ASOPGD6M-zBk7x36Z.js → chunk-ASOPGD6M-DIXslLG-.js} +1 -1
  12. package/dist/assets/{chunk-KFBOBJHC-U1tpWuJd.js → chunk-KFBOBJHC-FytVljyP.js} +1 -1
  13. package/dist/assets/{chunk-T2TOU4HS-P78s0i18.js → chunk-T2TOU4HS-CU5z-cXq.js} +1 -1
  14. package/dist/assets/{chunk-TMUBEWPD-_9HBqU-T.js → chunk-TMUBEWPD-CWo1AFyn.js} +1 -1
  15. package/dist/assets/classDiagram-LNE6IOMH-B430Onym.js +1 -0
  16. package/dist/assets/classDiagram-v2-MQ7JQ4JX-B430Onym.js +1 -0
  17. package/dist/assets/clone-B9O3E3P1.js +1 -0
  18. package/dist/assets/dagre-4EVJKHTY-CgZW5tg4.js +4 -0
  19. package/dist/assets/diagram-QW4FP2JN-CJAng8RC.js +24 -0
  20. package/dist/assets/{erDiagram-6RL3IURR-CR3kiDSu.js → erDiagram-6RL3IURR-C1uHztHX.js} +2 -2
  21. package/dist/assets/{flowDiagram-7ASYPVHJ-B0gVEfBk.js → flowDiagram-7ASYPVHJ-Yiu2odO-.js} +1 -1
  22. package/dist/assets/{ganttDiagram-NTVNEXSI-CIEk1X3_.js → ganttDiagram-NTVNEXSI-tutc0dJP.js} +3 -3
  23. package/dist/assets/gitGraph-YCYPL57B-C3jM6T4G.js +133 -0
  24. package/dist/assets/gitGraphDiagram-NRZ2UAAF-0aqHEKzz.js +65 -0
  25. package/dist/assets/graph-DC224ILV.js +1 -0
  26. package/dist/assets/infoDiagram-A4XQUW5V-Do7Mpzkx.js +2 -0
  27. package/dist/assets/{journeyDiagram-G5WM74LC-U9w5k3my.js → journeyDiagram-G5WM74LC-DAruEJc7.js} +1 -1
  28. package/dist/assets/{kanban-definition-QRCXZQQD-CO8Lwd1_.js → kanban-definition-QRCXZQQD-DB63vLR0.js} +1 -1
  29. package/dist/assets/layout-BiYH5x6Q.js +1 -0
  30. package/dist/assets/{linear-CHGWcXFV.js → linear-CLyGW2s0.js} +1 -1
  31. package/dist/assets/main-CEKkUqZb.js +1094 -0
  32. package/dist/assets/main-D3YeRndF.css +10 -0
  33. package/dist/assets/min-ChJMFU2N.js +1 -0
  34. package/dist/assets/{mindmap-definition-GWI6TPTV-FRHfidFi.js → mindmap-definition-GWI6TPTV-CtADqQgd.js} +1 -1
  35. package/dist/assets/pieDiagram-YF2LJOPJ-BSOfzo3O.js +30 -0
  36. package/dist/assets/{quadrantDiagram-OS5C2QUG-N0MJL1hU.js → quadrantDiagram-OS5C2QUG-BLBQNFVb.js} +1 -1
  37. package/dist/assets/{requirementDiagram-MIRIMTAZ-n0yYIEdg.js → requirementDiagram-MIRIMTAZ-DqorvuS6.js} +2 -2
  38. package/dist/assets/{sankeyDiagram-Y46BX6SQ-DzRt1UUn.js → sankeyDiagram-Y46BX6SQ-4NDcrLkB.js} +1 -1
  39. package/dist/assets/{sequenceDiagram-G6AWOVSC-Bw73EL9L.js → sequenceDiagram-G6AWOVSC-yha6BxK2.js} +1 -1
  40. package/dist/assets/stateDiagram-MAYHULR4-XJ_I41ez.js +1 -0
  41. package/dist/assets/stateDiagram-v2-4JROLMXI-CxcyPJO2.js +1 -0
  42. package/dist/assets/{timeline-definition-U7ZMHBDA-BEgtmEeC.js → timeline-definition-U7ZMHBDA-DCNrmvE7.js} +1 -1
  43. package/dist/assets/{xychartDiagram-6QU3TZC5-bWuCFYmh.js → xychartDiagram-6QU3TZC5-9N-Ohhdp.js} +2 -2
  44. package/dist/index.html +2 -2
  45. package/dist-server/app.module.js +6 -0
  46. package/dist-server/common/analytics.client.js +180 -0
  47. package/dist-server/common/analytics.module.js +21 -0
  48. package/dist-server/common/device-id.js +33 -0
  49. package/dist-server/domain/cli/cli.service.js +66 -4
  50. package/dist-server/domain/document/document.service.js +6 -3
  51. package/dist-server/domain/goal/dto/goal.dto.js +49 -0
  52. package/dist-server/domain/goal/goal.controller.js +86 -0
  53. package/dist-server/domain/goal/goal.module.js +22 -0
  54. package/dist-server/domain/goal/goal.service.js +197 -0
  55. package/dist-server/domain/goal/parser/goal-md-parser.js +166 -0
  56. package/dist-server/domain/goal/parser/goal-md-serializer.js +110 -0
  57. package/dist-server/domain/market/market.controller.js +25 -1
  58. package/dist-server/domain/market/market.service.js +414 -18
  59. package/dist-server/domain/planner/dto/planner.dto.js +7 -44
  60. package/dist-server/domain/planner/planner.controller.js +2 -2
  61. package/dist-server/domain/planner/planner.service.js +24 -0
  62. package/dist-server/domain/settings/settings.controller.js +52 -0
  63. package/dist-server/domain/settings/settings.module.js +22 -0
  64. package/dist-server/domain/settings/settings.service.js +105 -0
  65. package/dist-server/domain/task/task.service.js +20 -0
  66. package/dist-server/domain/thread/thread.controller.js +28 -0
  67. package/dist-server/domain/thread/thread.service.js +20 -0
  68. package/dist-server/main.js +1 -0
  69. package/dist-server/repository/task.repository.js +4 -0
  70. package/dist-server/repository/thread.repository.js +22 -0
  71. package/package.json +13 -8
  72. package/packages/cli/dist/builtin.js +1 -0
  73. package/packages/cli/dist/commands/init.js +251 -8
  74. package/packages/cli/package.json +2 -1
  75. package/server.js +6 -3
  76. package/dist/assets/MarketPage-CjJYSssq.js +0 -56
  77. package/dist/assets/architectureDiagram-UYN6MBPD-CMavUkL9.js +0 -36
  78. package/dist/assets/blockDiagram-ZHA2E4KO-D1rNF19s.js +0 -121
  79. package/dist/assets/channel-CyMi7jVG.js +0 -1
  80. package/dist/assets/classDiagram-LNE6IOMH-DVWuJ-Df.js +0 -1
  81. package/dist/assets/classDiagram-v2-MQ7JQ4JX-DVWuJ-Df.js +0 -1
  82. package/dist/assets/dagre-4EVJKHTY-CcSwRLTq.js +0 -4
  83. package/dist/assets/diagram-QW4FP2JN-N887K_Ef.js +0 -24
  84. package/dist/assets/gitGraph-YCYPL57B-Zl60oDrh.js +0 -133
  85. package/dist/assets/gitGraphDiagram-NRZ2UAAF-CmIpkGbx.js +0 -65
  86. package/dist/assets/graph-BlwPajkw.js +0 -1
  87. package/dist/assets/infoDiagram-A4XQUW5V-D3UxyUCV.js +0 -2
  88. package/dist/assets/layout-Bvwu4dCi.js +0 -1
  89. package/dist/assets/main-Dnmo7KzM.js +0 -1064
  90. package/dist/assets/main-Fz5tpoGZ.css +0 -10
  91. package/dist/assets/pieDiagram-YF2LJOPJ-Eo_N9Dkd.js +0 -30
  92. package/dist/assets/stateDiagram-MAYHULR4-hqIdsHFt.js +0 -1
  93. package/dist/assets/stateDiagram-v2-4JROLMXI-BoqygJ0s.js +0 -1
  94. /package/dist/assets/{gemini-logo.svg → google-logo.svg} +0 -0
@@ -38,6 +38,9 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  return result;
39
39
  };
40
40
  })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
41
44
  var MarketService_1;
42
45
  Object.defineProperty(exports, "__esModule", { value: true });
43
46
  exports.MarketService = void 0;
@@ -55,6 +58,7 @@ const yaml = __importStar(require("js-yaml"));
55
58
  const zod_1 = require("zod");
56
59
  const skill_1 = require("@crewx/skill");
57
60
  const workspace_context_store_js_1 = require("../../common/workspace-context.store.js");
61
+ const analytics_client_js_1 = require("../../common/analytics.client.js");
58
62
  // --- Zod schemas ---
59
63
  const MarketPackageTypeSchema = zod_1.z.enum(['skill', 'template', 'workflow', 'plugin']);
60
64
  const TemplateEntrySchema = zod_1.z.object({
@@ -69,6 +73,24 @@ const TemplateEntrySchema = zod_1.z.object({
69
73
  features: zod_1.z.array(zod_1.z.string()).optional(),
70
74
  crewxVersion: zod_1.z.string().optional(),
71
75
  });
76
+ const MarketplaceRegistryEntrySchema = zod_1.z.object({
77
+ id: zod_1.z.string(),
78
+ name: zod_1.z.string().optional(),
79
+ url: zod_1.z.string(),
80
+ type: zod_1.z.string().optional(),
81
+ trust: zod_1.z.enum(['trusted', 'verified', 'unverified']).optional(),
82
+ verified: zod_1.z.boolean().optional(),
83
+ default: zod_1.z.boolean().optional(),
84
+ tokenEnv: zod_1.z.string().optional(),
85
+ allow: zod_1.z.array(zod_1.z.string()).optional(),
86
+ });
87
+ const MarketplaceConfigSchema = zod_1.z.object({
88
+ version: zod_1.z.string().optional(),
89
+ forceDisabled: zod_1.z.array(zod_1.z.string()).optional(),
90
+ forceEnabled: zod_1.z.array(zod_1.z.string()).optional(),
91
+ deny: zod_1.z.array(zod_1.z.string()).optional(),
92
+ registries: zod_1.z.array(MarketplaceRegistryEntrySchema),
93
+ });
72
94
  const VALID_REGISTRY_SOURCE_TYPES = new Set(['crewx', 'codex', 'claude']);
73
95
  const TRUST_RANK = { trusted: 2, verified: 1, unverified: 0 };
74
96
  function boolToTrustLevel(isVerified, isDefault, source) {
@@ -87,12 +109,31 @@ function trustLevelToIsVerified(level) {
87
109
  // --- Constants ---
88
110
  const DEFAULT_REGISTRY_URL = 'https://github.com/sowonlabs/crewx-templates';
89
111
  const DEFAULT_REGISTRY_NAME = 'crewx-templates';
112
+ const MARKETPLACE_REMOTE_URL = 'https://raw.githubusercontent.com/sowonlabs/crewx-templates/main/marketplace.json';
113
+ const MARKETPLACE_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
114
+ function loadBundledMarketplace() {
115
+ try {
116
+ const bundlePath = (0, path_1.join)(__dirname, 'default-marketplace.json');
117
+ const raw = JSON.parse((0, fs_1.readFileSync)(bundlePath, 'utf-8'));
118
+ const result = MarketplaceConfigSchema.safeParse(raw);
119
+ return result.success ? result.data : null;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
90
125
  // Cache TTL: 15 minutes (§4.3 rate limit)
91
126
  const CACHE_TTL_MS = 15 * 60 * 1000;
92
127
  let MarketService = MarketService_1 = class MarketService {
93
128
  logger = new common_1.Logger(MarketService_1.name);
129
+ analytics;
130
+ constructor(analytics) {
131
+ this.analytics = analytics;
132
+ }
94
133
  // In-memory cache for templates.json fetches
95
134
  templateCache = new Map();
135
+ marketplaceMemoryCache = null;
136
+ marketplaceRefreshPending = false;
96
137
  get skillsDir() {
97
138
  return (0, path_1.join)((0, workspace_context_store_js_1.getWorkspacePath)(), 'skills');
98
139
  }
@@ -108,6 +149,9 @@ let MarketService = MarketService_1 = class MarketService {
108
149
  get marketplacePath() {
109
150
  return (0, path_1.join)((0, os_1.homedir)(), '.crewx', 'marketplace.json');
110
151
  }
152
+ get marketplaceCachePath() {
153
+ return (0, path_1.join)((0, os_1.homedir)(), '.crewx', 'marketplace.cache.json');
154
+ }
111
155
  get workspaceMarketplacePath() {
112
156
  return (0, path_1.join)((0, workspace_context_store_js_1.getWorkspacePath)(), '.crewx', 'marketplace.json');
113
157
  }
@@ -142,6 +186,23 @@ let MarketService = MarketService_1 = class MarketService {
142
186
  if (tokenEnv !== undefined) {
143
187
  this.validateTokenEnv(tokenEnv);
144
188
  }
189
+ const mpConfig = this.loadMarketplaceJson();
190
+ if (mpConfig?.forceDisabled?.length) {
191
+ const disabledSet = new Set(mpConfig.forceDisabled);
192
+ const parsedInput = this.parseGitHubUrl(url);
193
+ const blockedByPolicy = mpConfig.registries.some((r) => {
194
+ if (!disabledSet.has(r.id))
195
+ return false;
196
+ const parsedBlocked = this.parseGitHubUrl(r.url);
197
+ if (!parsedInput || !parsedBlocked)
198
+ return false;
199
+ return (parsedInput.owner.toLowerCase() === parsedBlocked.owner.toLowerCase() &&
200
+ parsedInput.repo.toLowerCase() === parsedBlocked.repo.toLowerCase());
201
+ });
202
+ if (blockedByPolicy) {
203
+ throw new common_1.BadRequestException('This registry is blocked by marketplace policy (forceDisabled)');
204
+ }
205
+ }
145
206
  const entries = this.loadRegistries();
146
207
  // Check duplicate URL
147
208
  const normalized = this.normalizeUrl(url);
@@ -157,6 +218,7 @@ let MarketService = MarketService_1 = class MarketService {
157
218
  trustLevel: 'unverified',
158
219
  isDefault: false,
159
220
  addedAt: new Date().toISOString(),
221
+ source: 'workspace',
160
222
  ...(tokenEnv ? { tokenEnv } : {}),
161
223
  };
162
224
  entries.push(entry);
@@ -172,6 +234,9 @@ let MarketService = MarketService_1 = class MarketService {
172
234
  if (entries[idx].isDefault) {
173
235
  throw new common_1.BadRequestException('Cannot delete the default registry');
174
236
  }
237
+ if (entries[idx].source === 'global') {
238
+ throw new common_1.BadRequestException('Cannot delete official registry from marketplace policy');
239
+ }
175
240
  entries.splice(idx, 1);
176
241
  this.saveRegistries(entries);
177
242
  this.templateCache.delete(id);
@@ -184,7 +249,11 @@ let MarketService = MarketService_1 = class MarketService {
184
249
  throw new common_1.NotFoundException(`Registry '${registryId}' not found`);
185
250
  }
186
251
  const templates = await this.fetchTemplates(registry);
187
- const installedNames = await this.getInstalledItemNames();
252
+ const [installedNames, installStats] = await Promise.all([
253
+ this.getInstalledItemNames(),
254
+ this.analytics.fetchInstallStats(templates.map((t) => t.name)),
255
+ ]);
256
+ const installedSlugs = new Set([...installedNames].map((n) => n.toLowerCase().replace(/[\s_]+/g, '-')));
188
257
  const items = templates.map((t) => ({
189
258
  name: t.name,
190
259
  displayName: t.displayName || t.name,
@@ -195,12 +264,48 @@ let MarketService = MarketService_1 = class MarketService {
195
264
  type: t.type || 'template',
196
265
  trustLevel: registry.trustLevel,
197
266
  isVerified: trustLevelToIsVerified(registry.trustLevel),
198
- isInstalled: installedNames.has(t.name),
267
+ isInstalled: installedNames.has(t.name) ||
268
+ installedSlugs.has(t.name.toLowerCase().replace(/[\s_]+/g, '-')),
199
269
  registryId: registry.id,
200
270
  registryName: registry.name,
271
+ installCount: installStats[t.name]?.unique ?? 0,
201
272
  }));
202
273
  return { items };
203
274
  }
275
+ async listAllMarketItems() {
276
+ const registries = this.loadRegistries();
277
+ const [installedNames, ...templateResults] = await Promise.all([
278
+ this.getInstalledItemNames(),
279
+ ...registries.map((registry) => this.fetchTemplates(registry)
280
+ .then((templates) => ({ registry, templates }))
281
+ .catch(() => ({ registry, templates: [] }))),
282
+ ]);
283
+ const installedSlugs = new Set([...installedNames].map((n) => n.toLowerCase().replace(/[\s_]+/g, '-')));
284
+ const allNames = templateResults.flatMap(({ templates }) => templates.map((t) => t.name));
285
+ const installStats = await this.analytics.fetchInstallStats(allNames);
286
+ const items = [];
287
+ for (const { registry, templates } of templateResults) {
288
+ for (const t of templates) {
289
+ items.push({
290
+ name: t.name,
291
+ displayName: t.displayName || t.name,
292
+ description: t.description || '',
293
+ author: t.author || '',
294
+ version: t.version || '0.0.0',
295
+ tags: t.tags || [],
296
+ type: t.type || 'template',
297
+ trustLevel: registry.trustLevel,
298
+ isVerified: trustLevelToIsVerified(registry.trustLevel),
299
+ isInstalled: installedNames.has(t.name) ||
300
+ installedSlugs.has(t.name.toLowerCase().replace(/[\s_]+/g, '-')),
301
+ registryId: registry.id,
302
+ registryName: registry.name,
303
+ installCount: installStats[t.name]?.unique ?? 0,
304
+ });
305
+ }
306
+ }
307
+ return { items };
308
+ }
204
309
  // --- Install / Uninstall ---
205
310
  async installMarketItem(registryId, marketName) {
206
311
  this.validateItemName(marketName);
@@ -209,6 +314,14 @@ let MarketService = MarketService_1 = class MarketService {
209
314
  if (!registry) {
210
315
  throw new common_1.NotFoundException(`Registry '${registryId}' not found`);
211
316
  }
317
+ // Deny list: block packages listed in marketplace.json deny[]
318
+ const mpConfig = this.loadMarketplaceJson();
319
+ if (mpConfig?.deny?.length) {
320
+ const denySet = new Set(mpConfig.deny.map((d) => d.toLowerCase()));
321
+ if (denySet.has(marketName.toLowerCase())) {
322
+ throw new common_1.BadRequestException(`Package '${marketName}' is blocked by marketplace policy (deny list).`);
323
+ }
324
+ }
212
325
  const templates = await this.fetchTemplates(registry);
213
326
  const template = templates.find((t) => t.name === marketName);
214
327
  if (!template) {
@@ -271,7 +384,7 @@ let MarketService = MarketService_1 = class MarketService {
271
384
  const relPath = file.path.slice(skillPrefix.length);
272
385
  // Path traversal guard
273
386
  const destPath = (0, path_1.resolve)((0, path_1.join)(tempDir, relPath));
274
- if (!destPath.startsWith(resolvedTempDir + '/') && destPath !== resolvedTempDir) {
387
+ if (!destPath.startsWith(resolvedTempDir + path_1.sep) && destPath !== resolvedTempDir) {
275
388
  throw new Error(`Path traversal detected: ${relPath}`);
276
389
  }
277
390
  const destFileDir = (0, path_1.dirname)(destPath);
@@ -371,6 +484,12 @@ let MarketService = MarketService_1 = class MarketService {
371
484
  this.logger.warn(`Failed to cache GitHub metadata for '${marketName}': ${err}`);
372
485
  });
373
486
  this.logger.log(`Installed skill '${marketName}' to ${targetDir}`);
487
+ this.analytics.track('market.install', {
488
+ item_name: marketName,
489
+ item_type: template.type || 'template',
490
+ item_version: template.version || '0.0.0',
491
+ registry_id: registryId,
492
+ });
374
493
  return { installed: true, path: targetDir };
375
494
  }
376
495
  catch (err) {
@@ -450,23 +569,26 @@ let MarketService = MarketService_1 = class MarketService {
450
569
  uninstallMarketItem(name) {
451
570
  this.validateItemName(name);
452
571
  const targetDir = (0, path_1.join)(this.skillsDir, name);
453
- if (!(0, fs_1.existsSync)(targetDir)) {
454
- throw new common_1.NotFoundException(`Installed skill '${name}' not found`);
572
+ if ((0, fs_1.existsSync)(targetDir)) {
573
+ (0, fs_1.rmSync)(targetDir, { recursive: true, force: true });
455
574
  }
456
- (0, fs_1.rmSync)(targetDir, { recursive: true, force: true });
457
575
  const lock = this.loadLockFile();
458
576
  delete lock.skills[name];
459
577
  this.saveLockFile(lock);
460
578
  this.logger.log(`Uninstalled skill '${name}'`);
579
+ this.analytics.track('market.uninstall', {
580
+ item_name: name,
581
+ item_type: 'skill',
582
+ });
461
583
  }
462
584
  // --- Installed items ---
463
585
  resolveItemSource(dir, projectRoot) {
464
- const nmDir = (0, path_1.join)(projectRoot, 'node_modules') + '/';
586
+ const nmDir = (0, path_1.join)(projectRoot, 'node_modules') + path_1.sep;
465
587
  if (!dir.startsWith(nmDir) && dir !== (0, path_1.join)(projectRoot, 'node_modules')) {
466
588
  return 'local';
467
589
  }
468
590
  const relFromNm = dir.slice(nmDir.length);
469
- return relFromNm.startsWith('@crewx/') ? 'builtin' : 'node_modules';
591
+ return relFromNm.startsWith(`@crewx${path_1.sep}`) ? 'builtin' : 'node_modules';
470
592
  }
471
593
  async listInstalledMarkets() {
472
594
  const projectRoot = (0, path_1.dirname)(this.skillsDir);
@@ -658,6 +780,37 @@ let MarketService = MarketService_1 = class MarketService {
658
780
  }
659
781
  return { frontmatter: result, body: match[2] || '' };
660
782
  }
783
+ async getRemoteSkillDetail(registryId, skillName) {
784
+ this.validateItemName(skillName);
785
+ const entries = this.loadRegistries();
786
+ const registry = entries.find((r) => r.id === registryId);
787
+ if (!registry) {
788
+ throw new common_1.NotFoundException(`Registry '${registryId}' not found`);
789
+ }
790
+ const parsed = this.parseGitHubUrl(registry.url);
791
+ if (!parsed) {
792
+ return { body: '' };
793
+ }
794
+ const templates = await this.fetchTemplates(registry);
795
+ const template = templates.find((t) => t.name === skillName);
796
+ const skillPath = template?.path || skillName;
797
+ const candidates = registry.type === 'claude'
798
+ ? [`${skillPath}/SKILL.md`, `${skillPath}/README.md`]
799
+ : [`skills/${skillPath}/SKILL.md`, `${skillPath}/SKILL.md`, `skills/${skillPath}/README.md`, `${skillPath}/README.md`];
800
+ const headers = this.getGitHubHeaders(registry);
801
+ for (const candidate of candidates) {
802
+ const url = `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/${candidate}`;
803
+ try {
804
+ const content = await this.fetchUrl(url, headers);
805
+ const { body } = this.parseFrontmatter(content);
806
+ return { body: body.trim() ? body : content };
807
+ }
808
+ catch {
809
+ continue;
810
+ }
811
+ }
812
+ return { body: '' };
813
+ }
661
814
  detectInstallSource(itemName) {
662
815
  const builtinDir = (0, path_1.join)(process.cwd(), 'packages', 'built-in', 'skill', 'skills', itemName);
663
816
  if ((0, fs_1.existsSync)(builtinDir)) {
@@ -835,21 +988,133 @@ let MarketService = MarketService_1 = class MarketService {
835
988
  };
836
989
  }
837
990
  loadMarketplaceJson() {
838
- if (!(0, fs_1.existsSync)(this.marketplacePath))
991
+ // 1. Memory cache (process lifetime)
992
+ if (this.marketplaceMemoryCache) {
993
+ const age = Date.now() - this.marketplaceMemoryCache.fetchedAt;
994
+ if (age >= MARKETPLACE_CACHE_TTL_MS && !this.marketplaceRefreshPending) {
995
+ this.marketplaceRefreshPending = true;
996
+ this.refreshMarketplaceRemote();
997
+ }
998
+ return this.marketplaceMemoryCache.config;
999
+ }
1000
+ // 2. User-managed override (~/.crewx/marketplace.json)
1001
+ if ((0, fs_1.existsSync)(this.marketplacePath)) {
1002
+ try {
1003
+ const raw = JSON.parse((0, fs_1.readFileSync)(this.marketplacePath, 'utf-8'));
1004
+ const result = MarketplaceConfigSchema.safeParse(raw);
1005
+ if (result.success) {
1006
+ const config = result.data;
1007
+ this.marketplaceMemoryCache = { config, fetchedAt: Date.now() };
1008
+ return config;
1009
+ }
1010
+ this.logger.warn(`Invalid marketplace.json schema: ${result.error.message}`);
1011
+ }
1012
+ catch {
1013
+ this.logger.warn('Failed to parse marketplace.json');
1014
+ }
1015
+ }
1016
+ // 3. Disk cache (~/.crewx/marketplace.cache.json)
1017
+ const diskCache = this.loadMarketplaceDiskCache();
1018
+ if (diskCache) {
1019
+ this.marketplaceMemoryCache = diskCache;
1020
+ if (Date.now() - diskCache.fetchedAt >= MARKETPLACE_CACHE_TTL_MS && !this.marketplaceRefreshPending) {
1021
+ this.marketplaceRefreshPending = true;
1022
+ this.refreshMarketplaceRemote();
1023
+ }
1024
+ return diskCache.config;
1025
+ }
1026
+ // 4. No local data — return null, let loadRegistries() handle fallback.
1027
+ return null;
1028
+ }
1029
+ loadMarketplaceDiskCache() {
1030
+ if (!(0, fs_1.existsSync)(this.marketplaceCachePath))
839
1031
  return null;
840
1032
  try {
841
- return JSON.parse((0, fs_1.readFileSync)(this.marketplacePath, 'utf-8'));
1033
+ const raw = JSON.parse((0, fs_1.readFileSync)(this.marketplaceCachePath, 'utf-8'));
1034
+ if (!raw || typeof raw.fetchedAt !== 'number' || !raw.config)
1035
+ return null;
1036
+ const result = MarketplaceConfigSchema.safeParse(raw.config);
1037
+ if (!result.success)
1038
+ return null;
1039
+ return {
1040
+ config: result.data,
1041
+ etag: raw.etag,
1042
+ lastModified: raw.lastModified,
1043
+ fetchedAt: raw.fetchedAt,
1044
+ };
842
1045
  }
843
1046
  catch {
844
- this.logger.warn('Failed to parse marketplace.json');
845
1047
  return null;
846
1048
  }
847
1049
  }
1050
+ saveMarketplaceDiskCache(cache) {
1051
+ try {
1052
+ const dir = (0, path_1.dirname)(this.marketplaceCachePath);
1053
+ if (!(0, fs_1.existsSync)(dir))
1054
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
1055
+ (0, fs_1.writeFileSync)(this.marketplaceCachePath, JSON.stringify(cache, null, 2), 'utf-8');
1056
+ }
1057
+ catch {
1058
+ this.logger.warn('Failed to save marketplace disk cache');
1059
+ }
1060
+ }
1061
+ refreshMarketplaceRemote() {
1062
+ const existing = this.marketplaceMemoryCache;
1063
+ const headers = {};
1064
+ if (existing?.etag)
1065
+ headers['If-None-Match'] = existing.etag;
1066
+ if (existing?.lastModified)
1067
+ headers['If-Modified-Since'] = existing.lastModified;
1068
+ fetch(MARKETPLACE_REMOTE_URL, { headers, signal: AbortSignal.timeout(10_000) })
1069
+ .then(async (res) => {
1070
+ if (res.status === 304) {
1071
+ if (existing) {
1072
+ existing.fetchedAt = Date.now();
1073
+ this.saveMarketplaceDiskCache(existing);
1074
+ }
1075
+ return;
1076
+ }
1077
+ if (!res.ok)
1078
+ return;
1079
+ const text = await res.text();
1080
+ const parsed = MarketplaceConfigSchema.safeParse(JSON.parse(text));
1081
+ if (!parsed.success)
1082
+ return;
1083
+ const cache = {
1084
+ config: parsed.data,
1085
+ etag: res.headers.get('etag') ?? undefined,
1086
+ lastModified: res.headers.get('last-modified') ?? undefined,
1087
+ fetchedAt: Date.now(),
1088
+ };
1089
+ this.marketplaceMemoryCache = cache;
1090
+ this.saveMarketplaceDiskCache(cache);
1091
+ })
1092
+ .catch(() => {
1093
+ // Network failure — seed disk cache from bundled default if nothing cached yet
1094
+ if (!this.marketplaceMemoryCache && !(0, fs_1.existsSync)(this.marketplaceCachePath)) {
1095
+ const bundled = loadBundledMarketplace();
1096
+ if (bundled) {
1097
+ const cache = { config: bundled, fetchedAt: Date.now() };
1098
+ this.marketplaceMemoryCache = cache;
1099
+ this.saveMarketplaceDiskCache(cache);
1100
+ }
1101
+ }
1102
+ })
1103
+ .finally(() => {
1104
+ this.marketplaceRefreshPending = false;
1105
+ });
1106
+ }
848
1107
  loadWorkspaceMarketplaceJson() {
849
1108
  if (!(0, fs_1.existsSync)(this.workspaceMarketplacePath))
850
1109
  return null;
851
1110
  try {
852
- return JSON.parse((0, fs_1.readFileSync)(this.workspaceMarketplacePath, 'utf-8'));
1111
+ const raw = JSON.parse((0, fs_1.readFileSync)(this.workspaceMarketplacePath, 'utf-8'));
1112
+ const result = MarketplaceConfigSchema.safeParse(raw);
1113
+ if (!result.success) {
1114
+ this.logger.warn(`Invalid workspace marketplace.json schema: ${result.error.message}`);
1115
+ return null;
1116
+ }
1117
+ return result.data;
853
1118
  }
854
1119
  catch {
855
1120
  this.logger.warn('Failed to parse workspace marketplace.json');
@@ -869,18 +1134,25 @@ let MarketService = MarketService_1 = class MarketService {
869
1134
  ...(item.tokenEnv ? { tokenEnv: item.tokenEnv } : {}),
870
1135
  };
871
1136
  }
1137
+ isBuiltinExempt(reg) {
1138
+ return (reg.name.startsWith('@crewx/') ||
1139
+ this.normalizeUrl(reg.url) === this.normalizeUrl(DEFAULT_REGISTRY_URL));
1140
+ }
872
1141
  mergeRegistries(global, workspace, forceDisabled) {
873
1142
  const disabledSet = new Set(forceDisabled);
874
1143
  const merged = new Map();
875
1144
  for (const reg of global) {
876
- if (!disabledSet.has(reg.id)) {
877
- merged.set(reg.name, { ...reg, source: 'global' });
1145
+ if (!disabledSet.has(reg.id) || this.isBuiltinExempt(reg)) {
1146
+ merged.set(this.normalizeUrl(reg.url), { ...reg, source: 'global' });
878
1147
  }
879
1148
  }
880
1149
  for (const reg of workspace) {
881
- if (disabledSet.has(reg.id))
1150
+ if (disabledSet.has(reg.id) && !this.isBuiltinExempt(reg))
1151
+ continue;
1152
+ const key = this.normalizeUrl(reg.url);
1153
+ if (merged.has(key))
882
1154
  continue;
883
- merged.set(reg.name, { ...reg, source: 'workspace', trustLevel: 'unverified' });
1155
+ merged.set(key, { ...reg, source: 'workspace', trustLevel: 'unverified' });
884
1156
  }
885
1157
  return Array.from(merged.values());
886
1158
  }
@@ -890,6 +1162,11 @@ let MarketService = MarketService_1 = class MarketService {
890
1162
  const config = this.loadConfig();
891
1163
  const registriesYaml = config?.skills?.registries;
892
1164
  if (!mpConfig && !wsMpConfig) {
1165
+ // No marketplace.json anywhere — seed disk cache from remote for next restart
1166
+ if (!this.marketplaceRefreshPending) {
1167
+ this.marketplaceRefreshPending = true;
1168
+ this.refreshMarketplaceRemote();
1169
+ }
893
1170
  if (!registriesYaml || registriesYaml.length === 0) {
894
1171
  const defaults = this.getDefaultRegistries();
895
1172
  this.saveRegistries(defaults);
@@ -930,7 +1207,8 @@ let MarketService = MarketService_1 = class MarketService {
930
1207
  if (!this.isPlainObject(config.skills)) {
931
1208
  config.skills = {};
932
1209
  }
933
- config.skills.registries = entries.map((e) => this.registryEntryToYaml(e));
1210
+ const persistable = entries.filter((e) => e.source !== 'global');
1211
+ config.skills.registries = persistable.map((e) => this.registryEntryToYaml(e));
934
1212
  this.saveConfig(config);
935
1213
  }
936
1214
  getDefaultRegistries() {
@@ -965,6 +1243,9 @@ let MarketService = MarketService_1 = class MarketService {
965
1243
  if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
966
1244
  return cached.data;
967
1245
  }
1246
+ if (registry.type === 'claude') {
1247
+ return this.fetchClaudeTemplates(registry);
1248
+ }
968
1249
  const parsed = this.parseGitHubUrl(registry.url);
969
1250
  if (!parsed) {
970
1251
  this.logger.warn(`Invalid registry URL: ${registry.url}`);
@@ -996,6 +1277,120 @@ let MarketService = MarketService_1 = class MarketService {
996
1277
  return [];
997
1278
  }
998
1279
  }
1280
+ async fetchClaudeTemplates(registry) {
1281
+ const parsed = this.parseGitHubUrl(registry.url);
1282
+ if (!parsed) {
1283
+ this.logger.warn(`Invalid registry URL: ${registry.url}`);
1284
+ return [];
1285
+ }
1286
+ const url = `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/.claude-plugin/marketplace.json`;
1287
+ try {
1288
+ const content = await this.fetchUrl(url, this.getGitHubHeaders(registry));
1289
+ const json = JSON.parse(content);
1290
+ const author = json.owner?.name ?? parsed.owner;
1291
+ const version = json.metadata?.version ?? '1.0.0';
1292
+ const templates = [];
1293
+ for (const plugin of json.plugins ?? []) {
1294
+ if (plugin.skills && plugin.skills.length > 0) {
1295
+ for (const skillRef of plugin.skills) {
1296
+ const skillPath = skillRef.replace(/^\.\//, '');
1297
+ const skillName = skillPath.split('/').pop() ?? skillPath;
1298
+ const displayName = skillName
1299
+ .split('-')
1300
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1301
+ .join(' ');
1302
+ templates.push({
1303
+ name: skillName,
1304
+ displayName,
1305
+ description: plugin.description,
1306
+ path: skillPath,
1307
+ type: 'skill',
1308
+ author,
1309
+ version,
1310
+ });
1311
+ }
1312
+ }
1313
+ else if (plugin.source != null) {
1314
+ const discovered = await this.discoverSkillsViaSource(plugin.source, parsed, registry);
1315
+ for (const skillName of discovered) {
1316
+ const displayName = skillName
1317
+ .split('-')
1318
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1319
+ .join(' ');
1320
+ templates.push({
1321
+ name: skillName,
1322
+ displayName,
1323
+ description: plugin.description,
1324
+ path: `skills/${skillName}`,
1325
+ type: 'skill',
1326
+ author,
1327
+ version,
1328
+ });
1329
+ }
1330
+ }
1331
+ }
1332
+ this.templateCache.set(registry.id, {
1333
+ data: templates,
1334
+ fetchedAt: Date.now(),
1335
+ });
1336
+ return templates;
1337
+ }
1338
+ catch (err) {
1339
+ this.logger.warn(`Failed to fetch .claude-plugin/marketplace.json from ${registry.url}: ${err}`);
1340
+ return [];
1341
+ }
1342
+ }
1343
+ async discoverSkillsViaSource(source, registryParsed, registry) {
1344
+ let owner;
1345
+ let repo;
1346
+ let ref = 'main';
1347
+ let basePath = '';
1348
+ if (typeof source === 'string') {
1349
+ owner = registryParsed.owner;
1350
+ repo = registryParsed.repo;
1351
+ basePath = source.replace(/^\.\//, '').replace(/\/+$/, '');
1352
+ }
1353
+ else {
1354
+ if (source.url) {
1355
+ const srcParsed = this.parseGitHubUrl(source.url);
1356
+ if (!srcParsed)
1357
+ return [];
1358
+ owner = srcParsed.owner;
1359
+ repo = srcParsed.repo;
1360
+ }
1361
+ else {
1362
+ owner = registryParsed.owner;
1363
+ repo = registryParsed.repo;
1364
+ }
1365
+ if (source.ref)
1366
+ ref = source.ref;
1367
+ basePath = (source.path ?? '').replace(/^\.\//, '').replace(/\/+$/, '');
1368
+ }
1369
+ try {
1370
+ const treesUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${ref}?recursive=1`;
1371
+ const treesRes = await fetch(treesUrl, { headers: this.getGitHubHeaders(registry) });
1372
+ if (!treesRes.ok)
1373
+ return [];
1374
+ const treesData = await treesRes.json();
1375
+ const skillsPrefix = basePath ? `${basePath}/skills/` : 'skills/';
1376
+ const skillNames = new Set();
1377
+ for (const item of treesData.tree) {
1378
+ if (item.type !== 'blob')
1379
+ continue;
1380
+ if (!item.path.startsWith(skillsPrefix))
1381
+ continue;
1382
+ const rel = item.path.slice(skillsPrefix.length);
1383
+ const parts = rel.split('/');
1384
+ if (parts.length === 2 && parts[1] === 'SKILL.md') {
1385
+ skillNames.add(parts[0]);
1386
+ }
1387
+ }
1388
+ return [...skillNames];
1389
+ }
1390
+ catch {
1391
+ return [];
1392
+ }
1393
+ }
999
1394
  async fetchUrl(url, headers) {
1000
1395
  const response = await fetch(url, headers ? { headers } : undefined);
1001
1396
  if (!response.ok) {
@@ -1047,5 +1442,6 @@ let MarketService = MarketService_1 = class MarketService {
1047
1442
  };
1048
1443
  exports.MarketService = MarketService;
1049
1444
  exports.MarketService = MarketService = MarketService_1 = __decorate([
1050
- (0, common_1.Injectable)()
1445
+ (0, common_1.Injectable)(),
1446
+ __metadata("design:paramtypes", [analytics_client_js_1.AnalyticsService])
1051
1447
  ], MarketService);