runwork 0.6.2 → 0.8.3

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 (80) hide show
  1. package/dist/__tests__/install-scripts.test.d.ts +1 -0
  2. package/dist/__tests__/install-scripts.test.js +247 -0
  3. package/dist/agents/__tests__/claude-code-plugin.test.d.ts +1 -0
  4. package/dist/agents/__tests__/claude-code-plugin.test.js +47 -0
  5. package/dist/agents/__tests__/claude-code-stats.test.js +143 -78
  6. package/dist/agents/__tests__/claude-desktop-plugin-tree.test.d.ts +1 -0
  7. package/dist/agents/__tests__/claude-desktop-plugin-tree.test.js +161 -0
  8. package/dist/agents/__tests__/claude-desktop-plugin.test.d.ts +1 -0
  9. package/dist/agents/__tests__/claude-desktop-plugin.test.js +117 -0
  10. package/dist/agents/__tests__/claude-desktop-rpm.test.d.ts +1 -0
  11. package/dist/agents/__tests__/claude-desktop-rpm.test.js +126 -0
  12. package/dist/agents/__tests__/codex-instructions.test.d.ts +1 -0
  13. package/dist/agents/__tests__/codex-instructions.test.js +57 -0
  14. package/dist/agents/__tests__/codex-stats.test.js +9 -0
  15. package/dist/agents/__tests__/graceful-degradation.test.js +3 -3
  16. package/dist/agents/__tests__/intro-skill.test.js +11 -2
  17. package/dist/agents/__tests__/skill-slug.test.d.ts +1 -0
  18. package/dist/agents/__tests__/skill-slug.test.js +77 -0
  19. package/dist/agents/claude-code.d.ts +3 -1
  20. package/dist/agents/claude-code.js +233 -58
  21. package/dist/agents/claude-desktop-plugin-tree.d.ts +44 -0
  22. package/dist/agents/claude-desktop-plugin-tree.js +92 -0
  23. package/dist/agents/claude-desktop.d.ts +20 -1
  24. package/dist/agents/claude-desktop.js +238 -61
  25. package/dist/agents/cline.d.ts +3 -1
  26. package/dist/agents/cline.js +44 -2
  27. package/dist/agents/codex.d.ts +3 -1
  28. package/dist/agents/codex.js +85 -14
  29. package/dist/agents/cursor.d.ts +12 -1
  30. package/dist/agents/cursor.js +98 -25
  31. package/dist/agents/gemini.d.ts +3 -1
  32. package/dist/agents/gemini.js +42 -4
  33. package/dist/agents/generic-adapter.d.ts +3 -1
  34. package/dist/agents/generic-adapter.js +59 -4
  35. package/dist/agents/intro-skill.js +77 -31
  36. package/dist/agents/types.d.ts +42 -2
  37. package/dist/agents/types.js +61 -6
  38. package/dist/agents/utils/instruction-hint.d.ts +10 -0
  39. package/dist/agents/utils/instruction-hint.js +46 -0
  40. package/dist/agents/utils/json-config.d.ts +6 -1
  41. package/dist/agents/utils/json-config.js +31 -4
  42. package/dist/agents/windsurf.d.ts +3 -1
  43. package/dist/agents/windsurf.js +37 -2
  44. package/dist/commands/__tests__/sync-telemetry.test.d.ts +1 -0
  45. package/dist/commands/__tests__/sync-telemetry.test.js +281 -0
  46. package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
  47. package/dist/commands/__tests__/upgrade.test.js +174 -0
  48. package/dist/commands/build-plugin.d.ts +2 -0
  49. package/dist/commands/build-plugin.js +135 -0
  50. package/dist/commands/init.d.ts +8 -2
  51. package/dist/commands/init.js +19 -7
  52. package/dist/commands/sync-telemetry.d.ts +33 -0
  53. package/dist/commands/sync-telemetry.js +186 -0
  54. package/dist/commands/sync.d.ts +1 -0
  55. package/dist/commands/sync.js +84 -118
  56. package/dist/commands/uninstall.d.ts +2 -0
  57. package/dist/commands/uninstall.js +145 -0
  58. package/dist/commands/upgrade.d.ts +12 -0
  59. package/dist/commands/upgrade.js +67 -16
  60. package/dist/generated/bundled-types.js +36 -36
  61. package/dist/generated/version.d.ts +1 -1
  62. package/dist/generated/version.js +1 -1
  63. package/dist/health/__tests__/cli-distribution-checks.test.d.ts +1 -0
  64. package/dist/health/__tests__/cli-distribution-checks.test.js +199 -0
  65. package/dist/health/checks.d.ts +2 -0
  66. package/dist/health/checks.js +131 -18
  67. package/dist/health/runner.js +6 -2
  68. package/dist/index.js +4 -0
  69. package/dist/sync/executor.d.ts +2 -0
  70. package/dist/sync/executor.js +8 -4
  71. package/dist/types.d.ts +2 -0
  72. package/dist/ui/banner.js +15 -6
  73. package/dist/utils/__tests__/sqlite.test.js +84 -72
  74. package/dist/utils/sqlite-adapter.d.ts +48 -0
  75. package/dist/utils/sqlite-adapter.js +147 -0
  76. package/dist/utils/sqlite.d.ts +12 -5
  77. package/dist/utils/sqlite.js +12 -22
  78. package/dist/utils/zip.d.ts +6 -0
  79. package/dist/utils/zip.js +28 -3
  80. package/package.json +2 -4
@@ -1,11 +1,11 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir, platform } from 'os';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { whichBinary } from '../utils/which.js';
6
6
  import { buildSkillMd } from './types.js';
7
- import { mergeJsonMcpServers, readJsonConfig, writeJsonConfig } from './utils/json-config.js';
8
- import { writeHintToFile, writeTeamInstructionsToFile } from './utils/instruction-hint.js';
7
+ import { mergeJsonMcpServers, readJsonConfig, writeJsonConfig, removeRunworkMcpServers } from './utils/json-config.js';
8
+ import { writeHintToFile, writeTeamInstructionsToFile, removeHintFromFile, removeTeamInstructionsFromFile } from './utils/instruction-hint.js';
9
9
  const PLUGIN_NAME = 'runwork';
10
10
  const PLUGIN_VERSION = '1.0.0';
11
11
  function getPluginJson() {
@@ -33,6 +33,7 @@ function getMarketplaceJson() {
33
33
  export class ClaudeCodeAdapter {
34
34
  name = 'Claude Code';
35
35
  slug = 'claude-code';
36
+ mcpProvidesSkills = true;
36
37
  async detect() {
37
38
  if (whichBinary('claude'))
38
39
  return true;
@@ -79,6 +80,9 @@ export class ClaudeCodeAdapter {
79
80
  const pluginDir = this.getPluginDir();
80
81
  mkdirSync(pluginDir, { recursive: true });
81
82
  const pluginMcpPath = join(pluginDir, '.mcp.json');
83
+ const marketDir = this.getMarketplaceDir();
84
+ mkdirSync(marketDir, { recursive: true });
85
+ const marketplaceMcpPath = join(marketDir, '.mcp.json');
82
86
  const pluginEntries = {};
83
87
  for (const s of servers) {
84
88
  pluginEntries[s.name] = {
@@ -89,6 +93,7 @@ export class ClaudeCodeAdapter {
89
93
  };
90
94
  }
91
95
  writeJsonConfig(pluginMcpPath, pluginEntries);
96
+ writeJsonConfig(marketplaceMcpPath, pluginEntries);
92
97
  }
93
98
  }
94
99
  async writeSkills(skills, scope) {
@@ -97,7 +102,8 @@ export class ClaudeCodeAdapter {
97
102
  ? join(process.cwd(), '.claude', 'skills')
98
103
  : join(homedir(), '.claude', 'skills');
99
104
  for (const skill of skills) {
100
- const skillDir = join(baseDir, skill.name);
105
+ const skillDir = join(baseDir, skill.filename);
106
+ cleanupOldSkillDir(baseDir, skill);
101
107
  mkdirSync(skillDir, { recursive: true });
102
108
  writeFileSync(join(skillDir, 'SKILL.md'), buildSkillMd(skill));
103
109
  }
@@ -110,7 +116,8 @@ export class ClaudeCodeAdapter {
110
116
  writeFileSync(join(pluginJsonDir, 'plugin.json'), JSON.stringify(getPluginJson(), null, 2));
111
117
  // Write skill files inside the plugin
112
118
  for (const skill of skills) {
113
- const skillDir = join(pluginDir, 'skills', skill.name);
119
+ const skillDir = join(pluginDir, 'skills', skill.filename);
120
+ cleanupOldSkillDir(join(pluginDir, 'skills'), skill);
114
121
  mkdirSync(skillDir, { recursive: true });
115
122
  writeFileSync(join(skillDir, 'SKILL.md'), buildSkillMd(skill));
116
123
  }
@@ -120,7 +127,8 @@ export class ClaudeCodeAdapter {
120
127
  mkdirSync(marketPluginJsonDir, { recursive: true });
121
128
  writeFileSync(join(marketPluginJsonDir, 'plugin.json'), JSON.stringify(getPluginJson(), null, 2));
122
129
  for (const skill of skills) {
123
- const skillDir = join(marketDir, 'skills', skill.name);
130
+ const skillDir = join(marketDir, 'skills', skill.filename);
131
+ cleanupOldSkillDir(join(marketDir, 'skills'), skill);
124
132
  mkdirSync(skillDir, { recursive: true });
125
133
  writeFileSync(join(skillDir, 'SKILL.md'), buildSkillMd(skill));
126
134
  }
@@ -195,72 +203,224 @@ export class ClaudeCodeAdapter {
195
203
  const idx = arr.indexOf(marker);
196
204
  return idx >= 0 ? arr.slice(0, idx) : [...arr];
197
205
  }
206
+ // ── Cleanup (uninstall) ────────────────────────────────────────────
207
+ async cleanup(scope, manifest) {
208
+ // 1. Remove MCP server entries
209
+ if (scope === 'project') {
210
+ removeRunworkMcpServers(join(process.cwd(), '.mcp.json'), 'mcpServers');
211
+ }
212
+ else {
213
+ removeRunworkMcpServers(join(homedir(), '.claude', 'settings.json'), 'mcpServers');
214
+ }
215
+ // 2. Remove only skill directories that we synced (listed in manifest)
216
+ const skillsDir = scope === 'project'
217
+ ? join(process.cwd(), '.claude', 'skills')
218
+ : join(homedir(), '.claude', 'skills');
219
+ if (existsSync(skillsDir) && manifest?.skillFilenames.length) {
220
+ const allowed = new Set(manifest.skillFilenames);
221
+ for (const entry of readdirSync(skillsDir)) {
222
+ if (!allowed.has(entry))
223
+ continue;
224
+ try {
225
+ rmSync(join(skillsDir, entry), { recursive: true, force: true });
226
+ }
227
+ catch { /* best-effort */ }
228
+ }
229
+ }
230
+ // 3. Remove instruction hints and team instructions
231
+ const instructionFile = scope === 'project'
232
+ ? join(process.cwd(), '.claude', 'CLAUDE.md')
233
+ : join(homedir(), '.claude', 'CLAUDE.md');
234
+ removeHintFromFile(instructionFile);
235
+ removeTeamInstructionsFromFile(instructionFile);
236
+ // 4. Remove agent config overrides (managed permission entries, model, enabledPlugins)
237
+ if (scope === 'user') {
238
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
239
+ if (existsSync(settingsPath)) {
240
+ try {
241
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
242
+ // Remove managed permission entries
243
+ if (settings.permissions) {
244
+ const MANAGED_MARKER = '# runwork-managed';
245
+ if (settings.permissions.allow) {
246
+ const idx = settings.permissions.allow.indexOf(MANAGED_MARKER);
247
+ if (idx >= 0)
248
+ settings.permissions.allow = settings.permissions.allow.slice(0, idx);
249
+ }
250
+ if (settings.permissions.deny) {
251
+ const idx = settings.permissions.deny.indexOf(MANAGED_MARKER);
252
+ if (idx >= 0)
253
+ settings.permissions.deny = settings.permissions.deny.slice(0, idx);
254
+ }
255
+ }
256
+ // Remove plugin enablement
257
+ if (settings.enabledPlugins) {
258
+ delete settings.enabledPlugins[`${PLUGIN_NAME}@runwork`];
259
+ }
260
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
261
+ }
262
+ catch { /* best-effort */ }
263
+ }
264
+ // 5. Remove plugin directories
265
+ const pluginDir = this.getPluginDir();
266
+ const marketRoot = this.getMarketplaceRoot();
267
+ for (const dir of [pluginDir, marketRoot]) {
268
+ if (existsSync(dir)) {
269
+ try {
270
+ rmSync(dir, { recursive: true, force: true });
271
+ }
272
+ catch { /* best-effort */ }
273
+ }
274
+ }
275
+ // 6. Remove from installed_plugins.json
276
+ const pluginsBase = this.getPluginsBaseDir();
277
+ const installedPath = join(pluginsBase, 'installed_plugins.json');
278
+ if (existsSync(installedPath)) {
279
+ try {
280
+ const installed = readJsonConfig(installedPath);
281
+ if (installed.plugins) {
282
+ delete installed.plugins[`${PLUGIN_NAME}@runwork`];
283
+ writeJsonConfig(installedPath, installed);
284
+ }
285
+ }
286
+ catch { /* best-effort */ }
287
+ }
288
+ // 7. Remove from known_marketplaces.json
289
+ const marketplacesPath = join(pluginsBase, 'known_marketplaces.json');
290
+ if (existsSync(marketplacesPath)) {
291
+ try {
292
+ const marketplaces = readJsonConfig(marketplacesPath);
293
+ delete marketplaces['runwork'];
294
+ writeJsonConfig(marketplacesPath, marketplaces);
295
+ }
296
+ catch { /* best-effort */ }
297
+ }
298
+ }
299
+ }
198
300
  // ── Usage stats (passive, best-effort) ─────────────────────────────
199
301
  async readUsageStats(lastSyncAt) {
302
+ // Source of truth: ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl
303
+ // Each line is one user/assistant/system entry. The file grows continuously
304
+ // during a live session, so mtime + per-entry timestamp give an accurate
305
+ // post-sync activity picture. (stats-cache.json was abandoned upstream and
306
+ // session-env/ only records session-start hooks.)
200
307
  try {
201
308
  const claudeDir = join(homedir(), '.claude');
202
309
  if (!existsSync(claudeDir))
203
310
  return null;
204
- // Primary: stats-cache.json has pre-computed daily activity
205
- // Format: { version, lastComputedDate, dailyActivity: [{ date, messageCount, sessionCount, toolCallCount }] }
206
- const statsCachePath = join(claudeDir, 'stats-cache.json');
207
- if (existsSync(statsCachePath)) {
208
- const statsCache = JSON.parse(readFileSync(statsCachePath, 'utf-8'));
209
- if (statsCache.dailyActivity && Array.isArray(statsCache.dailyActivity)) {
210
- const sinceDate = lastSyncAt ? lastSyncAt.slice(0, 10) : '1970-01-01';
211
- const recentDays = statsCache.dailyActivity.filter((d) => d.date > sinceDate);
212
- const totalMessages = recentDays.reduce((sum, d) => sum + (d.messageCount ?? 0), 0);
213
- const totalSessions = recentDays.reduce((sum, d) => sum + (d.sessionCount ?? 0), 0);
214
- const totalToolCalls = recentDays.reduce((sum, d) => sum + (d.toolCallCount ?? 0), 0);
215
- if (totalMessages > 0 || totalSessions > 0) {
216
- return {
217
- hasNewActivity: true,
218
- sessionCount: totalSessions,
219
- messageCount: totalMessages,
220
- toolCallCount: totalToolCalls,
221
- tokensUsed: 0,
222
- aiLinesAdded: 0,
223
- aiLinesRemoved: 0,
224
- lastActiveAt: recentDays.length > 0
225
- ? new Date(recentDays[recentDays.length - 1].date).toISOString()
226
- : null,
227
- };
228
- }
229
- }
311
+ const projectsDir = join(claudeDir, 'projects');
312
+ if (!existsSync(projectsDir))
313
+ return null;
314
+ const sinceMs = lastSyncAt ? new Date(lastSyncAt).getTime() : 0;
315
+ let sessionCount = 0;
316
+ let messageCount = 0;
317
+ let toolCallCount = 0;
318
+ let inputTokens = 0;
319
+ let outputTokens = 0;
320
+ let cacheReadTokens = 0;
321
+ let latestActivityMs = 0;
322
+ let cwdEntries;
323
+ try {
324
+ cwdEntries = readdirSync(projectsDir);
325
+ }
326
+ catch {
327
+ return null;
230
328
  }
231
- // Fallback: count session directories modified since last sync
232
- const sessionEnvDir = join(claudeDir, 'session-env');
233
- if (existsSync(sessionEnvDir)) {
234
- const entries = readdirSync(sessionEnvDir);
235
- const sinceMs = lastSyncAt ? new Date(lastSyncAt).getTime() : 0;
236
- let recentCount = 0;
237
- let latestMtime = 0;
238
- for (const entry of entries) {
329
+ for (const cwd of cwdEntries) {
330
+ const cwdPath = join(projectsDir, cwd);
331
+ let files;
332
+ try {
333
+ files = readdirSync(cwdPath);
334
+ }
335
+ catch {
336
+ continue;
337
+ }
338
+ for (const file of files) {
339
+ if (!file.endsWith('.jsonl'))
340
+ continue;
341
+ const filePath = join(cwdPath, file);
342
+ let stat;
239
343
  try {
240
- const stat = statSync(join(sessionEnvDir, entry));
241
- if (stat.mtimeMs > sinceMs)
242
- recentCount++;
243
- if (stat.mtimeMs > latestMtime)
244
- latestMtime = stat.mtimeMs;
344
+ stat = statSync(filePath);
245
345
  }
246
346
  catch {
247
347
  continue;
248
348
  }
249
- }
250
- if (recentCount > 0) {
251
- return {
252
- hasNewActivity: true,
253
- sessionCount: recentCount,
254
- messageCount: 0,
255
- toolCallCount: 0,
256
- tokensUsed: 0,
257
- aiLinesAdded: 0,
258
- aiLinesRemoved: 0,
259
- lastActiveAt: new Date(latestMtime).toISOString(),
260
- };
349
+ if (stat.mtimeMs <= sinceMs)
350
+ continue;
351
+ let content;
352
+ try {
353
+ content = readFileSync(filePath, 'utf-8');
354
+ }
355
+ catch {
356
+ continue;
357
+ }
358
+ let sessionHadActivity = false;
359
+ for (const line of content.split('\n')) {
360
+ if (!line)
361
+ continue;
362
+ let entry;
363
+ try {
364
+ entry = JSON.parse(line);
365
+ }
366
+ catch {
367
+ continue;
368
+ }
369
+ const tsRaw = entry.timestamp;
370
+ if (typeof tsRaw !== 'string')
371
+ continue;
372
+ const ts = Date.parse(tsRaw);
373
+ if (!Number.isFinite(ts) || ts <= sinceMs)
374
+ continue;
375
+ sessionHadActivity = true;
376
+ if (ts > latestActivityMs)
377
+ latestActivityMs = ts;
378
+ const type = entry.type;
379
+ if (type === 'user' || type === 'assistant') {
380
+ messageCount++;
381
+ }
382
+ if (type === 'assistant') {
383
+ const msg = entry.message;
384
+ if (msg && typeof msg === 'object') {
385
+ const msgRecord = msg;
386
+ const blocks = Array.isArray(msgRecord.content) ? msgRecord.content : [];
387
+ for (const block of blocks) {
388
+ if (block && typeof block === 'object'
389
+ && block.type === 'tool_use') {
390
+ toolCallCount++;
391
+ }
392
+ }
393
+ const usage = msgRecord.usage;
394
+ if (usage && typeof usage === 'object') {
395
+ const u = usage;
396
+ if (typeof u.input_tokens === 'number')
397
+ inputTokens += u.input_tokens;
398
+ if (typeof u.output_tokens === 'number')
399
+ outputTokens += u.output_tokens;
400
+ if (typeof u.cache_read_input_tokens === 'number') {
401
+ cacheReadTokens += u.cache_read_input_tokens;
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
407
+ if (sessionHadActivity)
408
+ sessionCount++;
261
409
  }
262
410
  }
263
- return null;
411
+ if (sessionCount === 0)
412
+ return null;
413
+ return {
414
+ hasNewActivity: true,
415
+ sessionCount,
416
+ messageCount,
417
+ toolCallCount,
418
+ tokensUsed: inputTokens + outputTokens,
419
+ aiLinesAdded: 0,
420
+ aiLinesRemoved: 0,
421
+ lastActiveAt: latestActivityMs > 0 ? new Date(latestActivityMs).toISOString() : null,
422
+ extra: { inputTokens, outputTokens, cacheReadTokens },
423
+ };
264
424
  }
265
425
  catch {
266
426
  return null;
@@ -371,3 +531,18 @@ export class ClaudeCodeAdapter {
371
531
  }
372
532
  }
373
533
  }
534
+ /**
535
+ * Remove old skill directory that used the display name (with spaces) if it
536
+ * differs from the new slug-based directory name.
537
+ */
538
+ function cleanupOldSkillDir(baseDir, skill) {
539
+ if (skill.name === skill.filename)
540
+ return;
541
+ const oldDir = join(baseDir, skill.name);
542
+ if (existsSync(oldDir)) {
543
+ try {
544
+ rmSync(oldDir, { recursive: true, force: true });
545
+ }
546
+ catch { /* best-effort */ }
547
+ }
548
+ }
@@ -0,0 +1,44 @@
1
+ import type { McpServerEntry, SkillFile } from './types.js';
2
+ export interface PluginMetadata {
3
+ pluginName: string;
4
+ pluginVersion: string;
5
+ description: string;
6
+ authorName: string;
7
+ }
8
+ export interface PluginTreeInput extends PluginMetadata {
9
+ skills: SkillFile[];
10
+ mcpServers: McpServerEntry[];
11
+ }
12
+ /**
13
+ * Write <destDir>/.claude-plugin/plugin.json.
14
+ */
15
+ export declare function writePluginMetadata(destDir: string, metadata: PluginMetadata): void;
16
+ /**
17
+ * Write <destDir>/.mcp.json with the { mcpServers: { ... } } wrapper
18
+ * Claude Desktop expects. Token from Authorization header is appended to
19
+ * the URL so the connector auto-authenticates without a manual OAuth click.
20
+ */
21
+ export declare function writePluginMcpConfig(destDir: string, mcpServers: McpServerEntry[]): void;
22
+ /**
23
+ * Write <destDir>/skills/runwork-team-instructions/SKILL.md containing the
24
+ * workspace team instructions (plus any per-agent extra instructions that
25
+ * the caller has already merged in). Called from both the sync path and the
26
+ * plugin zip build path so both produce the same on-disk layout.
27
+ */
28
+ export declare function writePluginTeamInstructions(destDir: string, instructions: string): void;
29
+ /**
30
+ * Write <destDir>/skills/<filename>/SKILL.md for each skill. Cleans up
31
+ * legacy directories that used display name with spaces (e.g. "Write Cold
32
+ * Email" instead of kebab-cased "write-cold-email").
33
+ */
34
+ export declare function writePluginSkills(destDir: string, skills: SkillFile[]): void;
35
+ /**
36
+ * Convenience wrapper that writes the entire plugin tree in one shot.
37
+ * Used by the build-plugin command to materialise a zip-ready directory.
38
+ *
39
+ * Layout:
40
+ * <destDir>/.claude-plugin/plugin.json
41
+ * <destDir>/.mcp.json
42
+ * <destDir>/skills/<filename>/SKILL.md
43
+ */
44
+ export declare function writePluginTree(destDir: string, input: PluginTreeInput): void;
@@ -0,0 +1,92 @@
1
+ import { mkdirSync, rmSync, existsSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { appendTokenToUrl, buildSkillMd } from './types.js';
4
+ /**
5
+ * Write <destDir>/.claude-plugin/plugin.json.
6
+ */
7
+ export function writePluginMetadata(destDir, metadata) {
8
+ const pluginJsonDir = join(destDir, '.claude-plugin');
9
+ mkdirSync(pluginJsonDir, { recursive: true });
10
+ writeFileSync(join(pluginJsonDir, 'plugin.json'), JSON.stringify({
11
+ name: metadata.pluginName,
12
+ version: metadata.pluginVersion,
13
+ description: metadata.description,
14
+ author: { name: metadata.authorName },
15
+ }, null, 2));
16
+ }
17
+ /**
18
+ * Write <destDir>/.mcp.json with the { mcpServers: { ... } } wrapper
19
+ * Claude Desktop expects. Token from Authorization header is appended to
20
+ * the URL so the connector auto-authenticates without a manual OAuth click.
21
+ */
22
+ export function writePluginMcpConfig(destDir, mcpServers) {
23
+ mkdirSync(destDir, { recursive: true });
24
+ const mcpEntries = {};
25
+ for (const server of mcpServers) {
26
+ const token = server.headers?.Authorization?.replace(/^Bearer\s+/i, '');
27
+ const connectorUrl = token ? appendTokenToUrl(server.url, token) : server.url;
28
+ mcpEntries[server.name] = {
29
+ type: 'http',
30
+ url: connectorUrl,
31
+ ...(server.description ? { description: server.description } : {}),
32
+ };
33
+ }
34
+ writeFileSync(join(destDir, '.mcp.json'), JSON.stringify({ mcpServers: mcpEntries }, null, 2));
35
+ }
36
+ /**
37
+ * Write <destDir>/skills/runwork-team-instructions/SKILL.md containing the
38
+ * workspace team instructions (plus any per-agent extra instructions that
39
+ * the caller has already merged in). Called from both the sync path and the
40
+ * plugin zip build path so both produce the same on-disk layout.
41
+ */
42
+ export function writePluginTeamInstructions(destDir, instructions) {
43
+ const skillDir = join(destDir, 'skills', 'runwork-team-instructions');
44
+ mkdirSync(skillDir, { recursive: true });
45
+ const skillContent = `---\nname: runwork-team-instructions\ndescription: Team instructions from your Runwork workspace. Always follow these guidelines.\n---\n\n${instructions}`;
46
+ writeFileSync(join(skillDir, 'SKILL.md'), skillContent);
47
+ }
48
+ /**
49
+ * Write <destDir>/skills/<filename>/SKILL.md for each skill. Cleans up
50
+ * legacy directories that used display name with spaces (e.g. "Write Cold
51
+ * Email" instead of kebab-cased "write-cold-email").
52
+ */
53
+ export function writePluginSkills(destDir, skills) {
54
+ const skillsRoot = join(destDir, 'skills');
55
+ mkdirSync(skillsRoot, { recursive: true });
56
+ for (const skill of skills) {
57
+ if (skill.name !== skill.filename) {
58
+ const legacyDir = join(skillsRoot, skill.name);
59
+ if (existsSync(legacyDir)) {
60
+ try {
61
+ rmSync(legacyDir, { recursive: true, force: true });
62
+ }
63
+ catch {
64
+ /* best-effort */
65
+ }
66
+ }
67
+ }
68
+ const skillDir = join(skillsRoot, skill.filename);
69
+ mkdirSync(skillDir, { recursive: true });
70
+ writeFileSync(join(skillDir, 'SKILL.md'), buildSkillMd(skill));
71
+ }
72
+ }
73
+ /**
74
+ * Convenience wrapper that writes the entire plugin tree in one shot.
75
+ * Used by the build-plugin command to materialise a zip-ready directory.
76
+ *
77
+ * Layout:
78
+ * <destDir>/.claude-plugin/plugin.json
79
+ * <destDir>/.mcp.json
80
+ * <destDir>/skills/<filename>/SKILL.md
81
+ */
82
+ export function writePluginTree(destDir, input) {
83
+ mkdirSync(destDir, { recursive: true });
84
+ writePluginMetadata(destDir, {
85
+ pluginName: input.pluginName,
86
+ pluginVersion: input.pluginVersion,
87
+ description: input.description,
88
+ authorName: input.authorName,
89
+ });
90
+ writePluginMcpConfig(destDir, input.mcpServers);
91
+ writePluginSkills(destDir, input.skills);
92
+ }
@@ -1,4 +1,21 @@
1
- import type { AgentAdapter, AgentConfigOverride, AgentUsageStats, McpServerEntry, SkillFile } from './types.js';
1
+ import type { AgentAdapter, AgentConfigOverride, AgentUsageStats, CleanupManifest, McpServerEntry, SkillFile } from './types.js';
2
+ export interface RpmPluginLocation {
3
+ /** Full path to the rpm plugin directory, e.g. `<org>/rpm/plugin_01YBqw...` */
4
+ pluginPath: string;
5
+ /** The org-level directory that contains `rpm/` */
6
+ orgDir: string;
7
+ }
8
+ /**
9
+ * Look for a plugin named `pluginName` in any `rpm/manifest.json`. These
10
+ * entries are created by Claude Desktop when the user uploads a plugin zip
11
+ * through Customize > Create plugin > Upload plugin. If found, sync should
12
+ * update the plugin content files inside the rpm plugin directory instead
13
+ * of writing to the legacy `cowork_plugins/` layout.
14
+ *
15
+ * We never modify `rpm/manifest.json` itself — Claude Desktop owns that
16
+ * registry and assigns the plugin id we read from it here.
17
+ */
18
+ export declare function findRpmPluginByName(pluginName: string): RpmPluginLocation | null;
2
19
  export declare class ClaudeDesktopAdapter implements AgentAdapter {
3
20
  name: string;
4
21
  slug: string;
@@ -8,8 +25,10 @@ export declare class ClaudeDesktopAdapter implements AgentAdapter {
8
25
  writeMcpServers(servers: McpServerEntry[], _scope: 'project' | 'user'): Promise<void>;
9
26
  writeSkills(skills: SkillFile[], _scope: 'project' | 'user'): Promise<void>;
10
27
  writeInstructionHint(_hint: string, _scope: 'project' | 'user'): Promise<void>;
28
+ buildPluginZip(skills: SkillFile[], mcpServers: McpServerEntry[], teamInstructions: string | null, outputPath: string): Promise<void>;
11
29
  writeTeamInstructions(instructions: string, _scope: 'project' | 'user'): Promise<void>;
12
30
  writeAgentConfig(config: AgentConfigOverride, _scope: 'project' | 'user'): Promise<void>;
31
+ cleanup(_scope: 'project' | 'user', _manifest?: CleanupManifest): Promise<void>;
13
32
  readUsageStats(lastSyncAt: string | null): Promise<AgentUsageStats | null>;
14
33
  readVersion(): Promise<string | null>;
15
34
  /**