runwork 0.5.0 → 0.6.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.
Files changed (116) hide show
  1. package/dist/agents/__tests__/claude-code-stats.test.d.ts +1 -0
  2. package/dist/agents/__tests__/claude-code-stats.test.js +153 -0
  3. package/dist/agents/__tests__/claude-desktop-stats.test.d.ts +1 -0
  4. package/dist/agents/__tests__/claude-desktop-stats.test.js +280 -0
  5. package/dist/agents/__tests__/codex-stats.test.d.ts +1 -0
  6. package/dist/agents/__tests__/codex-stats.test.js +262 -0
  7. package/dist/agents/__tests__/cursor-stats.test.d.ts +1 -0
  8. package/dist/agents/__tests__/cursor-stats.test.js +183 -0
  9. package/dist/agents/__tests__/graceful-degradation.test.d.ts +1 -0
  10. package/dist/agents/__tests__/graceful-degradation.test.js +123 -0
  11. package/dist/agents/__tests__/instruction-hint.test.d.ts +1 -0
  12. package/dist/agents/__tests__/instruction-hint.test.js +69 -0
  13. package/dist/agents/__tests__/intro-skill.test.d.ts +1 -0
  14. package/dist/agents/__tests__/intro-skill.test.js +135 -0
  15. package/dist/agents/__tests__/schema-validation.test.d.ts +1 -0
  16. package/dist/agents/__tests__/schema-validation.test.js +212 -0
  17. package/dist/agents/claude-code.d.ts +16 -1
  18. package/dist/agents/claude-code.js +344 -21
  19. package/dist/agents/claude-desktop.d.ts +10 -1
  20. package/dist/agents/claude-desktop.js +250 -8
  21. package/dist/agents/cline.d.ts +13 -0
  22. package/dist/agents/cline.js +88 -0
  23. package/dist/agents/codex.d.ts +7 -2
  24. package/dist/agents/codex.js +130 -18
  25. package/dist/agents/cursor.d.ts +6 -1
  26. package/dist/agents/cursor.js +164 -7
  27. package/dist/agents/detect.js +30 -7
  28. package/dist/agents/gemini.d.ts +6 -3
  29. package/dist/agents/gemini.js +57 -20
  30. package/dist/agents/generic-adapter.d.ts +23 -0
  31. package/dist/agents/generic-adapter.js +106 -0
  32. package/dist/agents/intro-skill.d.ts +36 -0
  33. package/dist/agents/intro-skill.js +358 -0
  34. package/dist/agents/registry.d.ts +53 -0
  35. package/dist/agents/registry.js +219 -0
  36. package/dist/agents/types.d.ts +44 -1
  37. package/dist/agents/types.js +11 -4
  38. package/dist/agents/utils/instruction-hint.d.ts +11 -0
  39. package/dist/agents/utils/instruction-hint.js +62 -0
  40. package/dist/agents/utils/json-config.js +3 -3
  41. package/dist/agents/windsurf.d.ts +2 -0
  42. package/dist/agents/windsurf.js +26 -9
  43. package/dist/api/client.d.ts +85 -2
  44. package/dist/api/client.js +40 -10
  45. package/dist/auth/__tests__/login-flow.test.d.ts +1 -0
  46. package/dist/auth/__tests__/login-flow.test.js +125 -0
  47. package/dist/auth/login-flow.d.ts +16 -0
  48. package/dist/auth/login-flow.js +101 -1
  49. package/dist/commands/clone.d.ts +9 -1
  50. package/dist/commands/clone.js +40 -12
  51. package/dist/commands/deploy.js +27 -4
  52. package/dist/commands/dev.js +35 -1
  53. package/dist/commands/doctor.d.ts +2 -0
  54. package/dist/commands/doctor.js +82 -0
  55. package/dist/commands/info.js +6 -2
  56. package/dist/commands/init.d.ts +10 -2
  57. package/dist/commands/init.js +58 -15
  58. package/dist/commands/integrations.js +47 -10
  59. package/dist/commands/login.js +31 -4
  60. package/dist/commands/mcp.js +143 -15
  61. package/dist/commands/setup.js +47 -122
  62. package/dist/commands/skills.js +230 -12
  63. package/dist/commands/sync.d.ts +8 -0
  64. package/dist/commands/sync.js +398 -65
  65. package/dist/commands/welcome.js +6 -3
  66. package/dist/generated/version.d.ts +1 -1
  67. package/dist/generated/version.js +1 -1
  68. package/dist/health/__tests__/checks.test.d.ts +1 -0
  69. package/dist/health/__tests__/checks.test.js +223 -0
  70. package/dist/health/checks.d.ts +20 -0
  71. package/dist/health/checks.js +392 -0
  72. package/dist/health/runner.d.ts +2 -0
  73. package/dist/health/runner.js +26 -0
  74. package/dist/health/types.d.ts +12 -0
  75. package/dist/health/types.js +1 -0
  76. package/dist/index.js +32 -0
  77. package/dist/sync/__tests__/change-detect.test.d.ts +1 -0
  78. package/dist/sync/__tests__/change-detect.test.js +123 -0
  79. package/dist/sync/__tests__/hash.test.d.ts +1 -0
  80. package/dist/sync/__tests__/hash.test.js +18 -0
  81. package/dist/sync/change-detect.d.ts +19 -0
  82. package/dist/sync/change-detect.js +136 -0
  83. package/dist/sync/conflict-ui.d.ts +5 -0
  84. package/dist/sync/conflict-ui.js +78 -0
  85. package/dist/sync/executor.d.ts +21 -0
  86. package/dist/sync/executor.js +98 -0
  87. package/dist/sync/hash.d.ts +1 -0
  88. package/dist/sync/hash.js +4 -0
  89. package/dist/sync/types.d.ts +24 -0
  90. package/dist/sync/types.js +1 -0
  91. package/dist/types.d.ts +12 -0
  92. package/dist/ui/__tests__/banner.test.js +17 -1
  93. package/dist/ui/banner.d.ts +1 -1
  94. package/dist/ui/banner.js +27 -2
  95. package/dist/utils/__tests__/resolve.test.d.ts +1 -0
  96. package/dist/utils/__tests__/resolve.test.js +95 -0
  97. package/dist/utils/__tests__/sqlite.test.d.ts +1 -0
  98. package/dist/utils/__tests__/sqlite.test.js +77 -0
  99. package/dist/utils/agent-guidance.d.ts +32 -0
  100. package/dist/utils/agent-guidance.js +106 -0
  101. package/dist/utils/fs.js +2 -2
  102. package/dist/utils/help-json.d.ts +56 -0
  103. package/dist/utils/help-json.js +85 -0
  104. package/dist/utils/resolve.d.ts +12 -0
  105. package/dist/utils/resolve.js +52 -0
  106. package/dist/utils/setup-state.d.ts +3 -0
  107. package/dist/utils/setup-state.js +19 -0
  108. package/dist/utils/sqlite.d.ts +7 -0
  109. package/dist/utils/sqlite.js +24 -0
  110. package/dist/utils/which.d.ts +6 -0
  111. package/dist/utils/which.js +18 -0
  112. package/package.json +7 -3
  113. package/dist/agents/copilot-cli.d.ts +0 -10
  114. package/dist/agents/copilot-cli.js +0 -46
  115. package/dist/agents/copilot-vscode.d.ts +0 -10
  116. package/dist/agents/copilot-vscode.js +0 -37
@@ -9,13 +9,23 @@ const searchCommand = new Command('search')
9
9
  .description('Search available integrations from the platform catalog')
10
10
  .argument('<query>', 'Search query (e.g., "google drive", "hubspot", "slack")')
11
11
  .option('--limit <n>', 'Maximum results to show', '20')
12
+ .option('--workspace <id>', 'Workspace ID (enables team usage info)')
12
13
  .action(async (query, opts, command) => {
13
14
  const useJson = shouldOutputJson(command.optsWithGlobals().json);
14
15
  const credentials = requireAuth();
15
16
  const client = new ApiClient(credentials);
16
17
  const limit = parseInt(opts.limit, 10);
17
18
  try {
18
- const { results } = await client.searchIntegrations(query, limit);
19
+ // Resolve workspace for enriched search results (connectedByUser, usedByTeammates)
20
+ let workspaceId;
21
+ try {
22
+ const resolved = await resolveWorkspace(client, opts);
23
+ workspaceId = resolved.workspaceId;
24
+ }
25
+ catch {
26
+ // Workspace not available, search without enrichment
27
+ }
28
+ const { results } = await client.searchIntegrations(query, limit, workspaceId);
19
29
  if (useJson) {
20
30
  const items = results.map(({ provider: _p, ...rest }) => rest);
21
31
  jsonOut({ results: items });
@@ -28,7 +38,13 @@ const searchCommand = new Command('search')
28
38
  }
29
39
  console.log(`Found ${results.length} integration(s) for "${query}":\n`);
30
40
  for (const r of results) {
31
- console.log(` ${r.id} - ${r.name} (${r.provider})`);
41
+ const tags = [];
42
+ if (r.connectedByUser)
43
+ tags.push('[connected]');
44
+ else if (r.usedByTeammates)
45
+ tags.push('[used by teammates]');
46
+ const tagStr = tags.length > 0 ? ` ${tags.join(' ')}` : '';
47
+ console.log(` ${r.id} - ${r.name} (${r.provider})${tagStr}`);
32
48
  if (r.docs) {
33
49
  console.log(` Base URL: ${r.docs.baseUrl}`);
34
50
  console.log(` Path format: ${r.docs.pathFormat}`);
@@ -66,26 +82,47 @@ const listCommand = new Command('list')
66
82
  const { workspaceId } = await resolveWorkspace(client, opts);
67
83
  try {
68
84
  const integrations = await client.listConnectedIntegrations(workspaceId);
85
+ const teamUsage = await client.getTeamIntegrationUsage(workspaceId);
69
86
  if (useJson) {
70
87
  const items = integrations.map(i => ({
71
88
  id: i.canonicalId || i.integrationId,
72
89
  status: i.status || 'connected',
73
90
  createdAt: i.createdAt || null,
74
91
  }));
75
- jsonOut({ items });
92
+ const teamItems = teamUsage
93
+ .filter(t => !t.connectedByMe && t.userCount > 0)
94
+ .map(t => ({
95
+ canonicalId: t.canonicalId,
96
+ userCount: t.userCount,
97
+ users: t.users.map(u => u.name),
98
+ }));
99
+ jsonOut({ items, teamUsage: teamItems });
76
100
  return;
77
101
  }
78
102
  if (integrations.length === 0) {
79
103
  console.log('No integrations connected in this workspace.');
80
104
  console.log('Connect integrations at https://runwork.ai/workspace-settings');
81
- return;
82
105
  }
83
- console.log(`Connected integrations (${integrations.length}):\n`);
84
- for (const i of integrations) {
85
- const id = i.canonicalId || i.integrationId;
86
- const status = i.status || 'connected';
87
- const date = i.createdAt ? ` (since ${new Date(i.createdAt).toLocaleDateString()})` : '';
88
- console.log(` ${id} - ${i.provider}${date} [${status}]`);
106
+ else {
107
+ console.log(`Connected integrations (${integrations.length}):\n`);
108
+ for (const i of integrations) {
109
+ const id = i.canonicalId || i.integrationId;
110
+ const status = i.status || 'connected';
111
+ const date = i.createdAt ? ` (since ${new Date(i.createdAt).toLocaleDateString()})` : '';
112
+ console.log(` ${id} - ${i.provider}${date} [${status}]`);
113
+ }
114
+ }
115
+ const teamOnly = teamUsage.filter(t => !t.connectedByMe && t.userCount > 0);
116
+ if (teamOnly.length > 0) {
117
+ console.log(`\nUsed by your team (${teamOnly.length} more):\n`);
118
+ for (const t of teamOnly) {
119
+ const names = t.users.map(u => u.name);
120
+ const display = names.length <= 3
121
+ ? names.join(', ')
122
+ : `${names.slice(0, 2).join(', ')}, and ${names.length - 2} other${names.length - 2 !== 1 ? 's' : ''}`;
123
+ console.log(` ${t.canonicalId} - Used by ${display}`);
124
+ }
125
+ console.log(`\n Connect at https://runwork.ai/workspace-settings?section=integrations`);
89
126
  }
90
127
  }
91
128
  catch (err) {
@@ -1,12 +1,39 @@
1
1
  import { Command } from 'commander';
2
- import { performLogin } from '../auth/login-flow.js';
2
+ import { performLogin, performLoginNoOpen, performLoginFromUrl, performLoginWithApiKey, } from '../auth/login-flow.js';
3
3
  import { bold, cyan } from '../ui/colors.js';
4
4
  export const loginCommand = new Command('login')
5
5
  .description('Authenticate with Runwork platform')
6
+ .argument('[url]', 'Login URL from a previous --no-open session')
6
7
  .option('--base-url <url>', 'Platform URL', 'https://runwork.ai')
7
- .action(async (options) => {
8
- console.log('Opening browser for authentication...');
8
+ .option('--no-open', 'Print login URL without opening browser (polls for completion)')
9
+ .option('--print-only', 'With --no-open: print URL and exit without polling')
10
+ .option('--api-key <key>', 'Authenticate directly with an API key (for CI)')
11
+ .action(async (url, options) => {
12
+ if (!options)
13
+ return;
14
+ // Mode 1: Direct API key auth
15
+ if (options.apiKey) {
16
+ await performLoginWithApiKey(options.apiKey, options.baseUrl);
17
+ return;
18
+ }
19
+ // Mode 2: Resume from a previously printed URL
20
+ if (url) {
21
+ await performLoginFromUrl(url);
22
+ printNextSteps();
23
+ return;
24
+ }
25
+ // Mode 3: No-open (print URL, optionally poll)
26
+ if (!options.open) {
27
+ const result = await performLoginNoOpen(options.baseUrl, options.printOnly);
28
+ if (result)
29
+ printNextSteps();
30
+ return;
31
+ }
32
+ // Mode 4: Default browser flow
9
33
  await performLogin(options.baseUrl);
34
+ printNextSteps();
35
+ });
36
+ function printNextSteps() {
10
37
  console.log('');
11
38
  console.log(bold('What next?'));
12
39
  console.log('');
@@ -14,4 +41,4 @@ export const loginCommand = new Command('login')
14
41
  console.log(` ${cyan('runwork clone')} Clone an existing app`);
15
42
  console.log(` ${cyan('runwork dev')} Start developing (inside an app directory)`);
16
43
  console.log('');
17
- });
44
+ }
@@ -13,26 +13,57 @@ const listCommand = new Command('list')
13
13
  const client = new ApiClient(credentials);
14
14
  const { workspaceId, workspaceName } = await resolveWorkspace(client, opts);
15
15
  try {
16
- const servers = await client.listMcpServers(workspaceId);
16
+ const baseUrl = credentials.baseUrl || 'https://runwork.ai';
17
+ const [servers, apps] = await Promise.all([
18
+ client.listMcpServers(workspaceId),
19
+ client.listApps(workspaceId).catch(() => []),
20
+ ]);
21
+ // Build the workspace MCP entry
22
+ const workspaceMcpUrl = `${baseUrl}/api/workspaces/${workspaceId}/mcp`;
23
+ // Build per-app MCP entries
24
+ const appMcpEntries = apps.map(app => ({
25
+ name: app.name,
26
+ url: `${baseUrl}/api/workspaces/${workspaceId}/apps/${app.id}/mcp`,
27
+ }));
17
28
  if (useJson) {
18
- jsonOut({ servers });
19
- return;
20
- }
21
- if (servers.length === 0) {
22
- console.log('No MCP servers configured in this workspace.');
23
- console.log('Add one with: runwork mcp add <name> <url>');
29
+ jsonOut({
30
+ workspace: { name: 'Workspace', url: workspaceMcpUrl },
31
+ apps: appMcpEntries,
32
+ external: servers,
33
+ });
24
34
  return;
25
35
  }
26
36
  console.log(`\nWorkspace: ${workspaceName || workspaceId}\n`);
27
- console.log(` ${'Name'.padEnd(20)} ${'URL'.padEnd(45)} Status`);
37
+ // Workspace MCP
38
+ console.log(' Workspace MCP (all tools across apps)');
28
39
  console.log(' ' + '-'.repeat(75));
29
- for (const s of servers) {
30
- const status = s.enabled
31
- ? (s.toolCount ? `connected (${s.toolCount} tools)` : 'enabled')
32
- : 'disabled';
33
- console.log(` ${s.name.padEnd(20)} ${s.url.padEnd(45)} ${status}`);
34
- }
40
+ console.log(` ${workspaceMcpUrl}`);
35
41
  console.log('');
42
+ // App MCPs
43
+ if (appMcpEntries.length > 0) {
44
+ console.log(' App MCP Servers (per-app scope)');
45
+ console.log(' ' + '-'.repeat(75));
46
+ for (const app of appMcpEntries) {
47
+ console.log(` ${app.name.padEnd(20)} ${app.url}`);
48
+ }
49
+ console.log('');
50
+ }
51
+ // External MCPs
52
+ if (servers.length > 0) {
53
+ console.log(' External MCP Servers');
54
+ console.log(' ' + '-'.repeat(75));
55
+ for (const s of servers) {
56
+ const status = s.enabled
57
+ ? (s.toolCount ? `connected (${s.toolCount} tools)` : 'enabled')
58
+ : 'disabled';
59
+ console.log(` ${s.name.padEnd(20)} ${s.url.padEnd(45)} ${status}`);
60
+ }
61
+ console.log('');
62
+ }
63
+ else {
64
+ console.log(' No external MCP servers. Add one with: runwork mcp add <name> <url>');
65
+ console.log('');
66
+ }
36
67
  }
37
68
  catch (err) {
38
69
  console.error('Failed to list MCP servers:', err instanceof Error ? err.message : err);
@@ -110,8 +141,105 @@ const removeCommand = new Command('remove')
110
141
  process.exit(1);
111
142
  }
112
143
  });
144
+ const searchCommand = new Command('search')
145
+ .description('Search community MCP servers from the MCP Registry')
146
+ .argument('<query>', 'Search query')
147
+ .option('--limit <n>', 'Max results (1-50)', '10')
148
+ .action(async (query, opts, command) => {
149
+ const useJson = shouldOutputJson(command.optsWithGlobals().json);
150
+ const credentials = requireAuth();
151
+ const client = new ApiClient(credentials);
152
+ try {
153
+ const results = await client.searchCommunityMcpServers(query, Number(opts.limit));
154
+ if (useJson) {
155
+ jsonOut(results);
156
+ return;
157
+ }
158
+ if (results.servers.length === 0) {
159
+ console.log(`No remote-capable MCP servers found for "${query}".`);
160
+ return;
161
+ }
162
+ console.log(`\n ${results.count} remote-capable server${results.count !== 1 ? 's' : ''} for "${query}"\n`);
163
+ console.log(` ${'Name'.padEnd(35)} ${'Type'.padEnd(18)} URL`);
164
+ console.log(' ' + '-'.repeat(90));
165
+ for (const s of results.servers) {
166
+ const name = s.title || s.name.split('/').pop() || s.name;
167
+ const remote = s.remotes[0];
168
+ const transport = remote ? remote.type : 'N/A';
169
+ const url = remote ? truncateUrl(remote.url, 40) : 'N/A';
170
+ console.log(` ${name.padEnd(35)} ${transport.padEnd(18)} ${url}`);
171
+ }
172
+ console.log(`\n Connect with: runwork mcp install <name>`);
173
+ console.log('');
174
+ }
175
+ catch (err) {
176
+ console.error('Search failed:', err instanceof Error ? err.message : err);
177
+ process.exit(1);
178
+ }
179
+ });
180
+ const installCommand = new Command('install')
181
+ .description('Install a community MCP server from the MCP Registry into your workspace')
182
+ .argument('<query>', 'Server name or search query')
183
+ .option('--workspace <id>', 'Workspace ID')
184
+ .action(async (query, opts, command) => {
185
+ const useJson = shouldOutputJson(command.optsWithGlobals().json);
186
+ const credentials = requireAuth();
187
+ const client = new ApiClient(credentials);
188
+ const { workspaceId } = await resolveWorkspace(client, opts);
189
+ try {
190
+ const results = await client.searchCommunityMcpServers(query, 5);
191
+ if (results.servers.length === 0) {
192
+ console.error(`No remote-capable MCP servers found for "${query}".`);
193
+ process.exit(1);
194
+ }
195
+ // Pick best match (first result)
196
+ const server = results.servers[0];
197
+ const remote = server.remotes[0];
198
+ if (!remote) {
199
+ console.error(`Server "${server.name}" has no remote endpoint.`);
200
+ process.exit(1);
201
+ }
202
+ if (remote.url.includes('{') && remote.url.includes('}')) {
203
+ console.error(`Server "${server.name}" requires configuration variables in its URL.`);
204
+ console.error(`URL template: ${remote.url}`);
205
+ console.error('Use `runwork mcp add <name> <url>` to add it manually with the correct URL.');
206
+ process.exit(1);
207
+ }
208
+ const displayName = server.title || server.name.split('/').pop() || server.name;
209
+ if (!opts.yes && !useJson) {
210
+ console.log(`\n Found: ${displayName}`);
211
+ console.log(` URL: ${remote.url}`);
212
+ console.log(` Type: ${remote.type}\n`);
213
+ const confirmed = await promptConfirm(`Connect "${displayName}" to your workspace?`);
214
+ if (!confirmed)
215
+ return;
216
+ }
217
+ const added = await client.addMcpServer(workspaceId, {
218
+ name: displayName,
219
+ url: remote.url,
220
+ transport: remote.type,
221
+ });
222
+ if (useJson) {
223
+ jsonOut({ server: added, source: server.name });
224
+ return;
225
+ }
226
+ console.log(`Connected "${displayName}" (${remote.url})`);
227
+ console.log('Run `runwork sync` to update your local agent configs.');
228
+ }
229
+ catch (err) {
230
+ console.error('Install failed:', err instanceof Error ? err.message : err);
231
+ process.exit(1);
232
+ }
233
+ });
234
+ function truncateUrl(url, max) {
235
+ if (url.length <= max)
236
+ return url;
237
+ return url.slice(0, max - 3) + '...';
238
+ }
113
239
  export const mcpCommand = new Command('mcp')
114
240
  .description('Manage workspace MCP servers')
115
241
  .addCommand(listCommand)
116
242
  .addCommand(addCommand)
117
- .addCommand(removeCommand);
243
+ .addCommand(removeCommand)
244
+ .addCommand(searchCommand)
245
+ .addCommand(installCommand);
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { writeFileSync, mkdirSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { homedir } from 'os';
5
5
  import { requireAuth } from '../auth/store.js';
@@ -7,12 +7,13 @@ import { ApiClient } from '../api/client.js';
7
7
  import { resolveWorkspace, hasProjectConfig } from '../workspace/resolve.js';
8
8
  import { promptSelect, promptConfirm } from '../utils/prompt.js';
9
9
  import { detectAgents, printNoAgentsMessage } from '../agents/detect.js';
10
- import { RUNWORK_MCP_PREFIX } from '../agents/types.js';
10
+ import { syncFromState } from './sync.js';
11
11
  export const setupCommand = new Command('setup')
12
12
  .description('Configure local AI agents with workspace skills and MCP servers')
13
13
  .option('--workspace <id>', 'Workspace ID')
14
14
  .option('--agent <slug>', 'Only configure a specific agent (e.g. claude-code, cursor)')
15
15
  .option('--dry-run', 'Show what would be configured without writing files')
16
+ .option('-y, --yes', 'Skip all prompts, configure all detected agents with user scope')
16
17
  .action(async (opts) => {
17
18
  const credentials = requireAuth();
18
19
  const client = new ApiClient(credentials);
@@ -33,6 +34,14 @@ export const setupCommand = new Command('setup')
33
34
  process.exit(1);
34
35
  }
35
36
  }
37
+ else if (opts.yes) {
38
+ // Non-interactive: configure all detected agents
39
+ console.log('Detected agents:');
40
+ for (const a of agents) {
41
+ console.log(` - ${a.name}`);
42
+ }
43
+ console.log('\nConfiguring all agents (--yes).\n');
44
+ }
36
45
  else {
37
46
  // Show detected agents and confirm
38
47
  console.log('Detected agents:');
@@ -57,7 +66,7 @@ export const setupCommand = new Command('setup')
57
66
  }
58
67
  // 3. Determine scope
59
68
  let scope = 'user';
60
- if (hasProjectConfig()) {
69
+ if (hasProjectConfig() && !opts.yes) {
61
70
  const scopeChoices = [
62
71
  { label: 'This project only', value: 'project' },
63
72
  { label: 'Your user (global)', value: 'user' },
@@ -66,134 +75,50 @@ export const setupCommand = new Command('setup')
66
75
  const chosen = await promptSelect('Configure for:', scopeChoices);
67
76
  scope = chosen.value;
68
77
  }
69
- // 4. Fetch workspace data
70
- console.log('\nFetching workspace skills and MCP servers...');
71
- const [allSkills, mcpServers, externalSkills] = await Promise.all([
72
- client.listWorkspaceSkills(workspaceId),
73
- client.listMcpServers(workspaceId),
74
- client.listExternalSkills(workspaceId),
75
- ]);
76
- // 5. Build MCP server entries
77
- const wsPrefix = workspaceId.substring(0, 8);
78
- const mcpEntries = mcpServers
79
- .filter(s => s.enabled)
80
- .map(s => ({
81
- name: `${RUNWORK_MCP_PREFIX}${wsPrefix}-${s.name}`,
82
- url: s.url,
83
- transport: s.transport,
84
- headers: { Authorization: `Bearer ${credentials.apiKey}` },
85
- }));
86
- // Also add the workspace MCP endpoint itself
87
- const baseUrl = credentials.baseUrl || 'https://runwork.ai';
88
- mcpEntries.push({
89
- name: `${RUNWORK_MCP_PREFIX}${wsPrefix}-workspace`,
90
- url: `${baseUrl}/api/workspaces/${workspaceId}/mcp`,
91
- transport: 'streamable-http',
92
- headers: { Authorization: `Bearer ${credentials.apiKey}` },
93
- });
94
- // 6. Build skill files — external skills go to all scopes
95
- const baseSkillFiles = externalSkills.map(s => ({
96
- name: s.name,
97
- filename: s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
98
- content: s.content,
99
- description: s.description,
100
- }));
101
- // Fetch all app skills for user scope
102
- const allAppSkillFiles = [];
103
- const appSkillEntries = allSkills.filter(s => s.type === 'app' && s.appId);
104
- if (appSkillEntries.length > 0) {
105
- console.log(`Fetching skills for ${appSkillEntries.length} app(s)...`);
106
- const results = await Promise.all(appSkillEntries.map(async (s) => {
107
- try {
108
- const content = await client.getAppSkillContent(workspaceId, s.appId);
109
- if (content && content.trim()) {
110
- return {
111
- name: s.name,
112
- filename: s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
113
- content,
114
- description: s.description || `App skill for ${s.name}`,
115
- };
116
- }
117
- }
118
- catch { /* skip */ }
119
- return null;
120
- }));
121
- for (const r of results) {
122
- if (r)
123
- allAppSkillFiles.push(r);
124
- }
125
- }
126
- // For project scope: only the current app's skill
127
- let projectAppSkillFile = null;
128
- if (hasProjectConfig()) {
129
- try {
130
- const config = JSON.parse(readFileSync('.runwork.json', 'utf-8'));
131
- if (config.appId) {
132
- projectAppSkillFile = allAppSkillFiles.find(s => s.filename === (config.appName || '').toLowerCase().replace(/[^a-z0-9]+/g, '-')) || null;
133
- }
134
- }
135
- catch { /* ignore */ }
78
+ // 4. Get workspace slug
79
+ let workspaceSlug;
80
+ try {
81
+ const workspaces = await client.listWorkspaces();
82
+ workspaceSlug = workspaces.find(w => w.id === workspaceId)?.slug;
136
83
  }
137
- // Build scope-specific skill lists
138
- const userSkillFiles = [...baseSkillFiles, ...allAppSkillFiles];
139
- const projectSkillFiles = projectAppSkillFile
140
- ? [...baseSkillFiles, projectAppSkillFile]
141
- : [...baseSkillFiles];
142
- if (opts.dryRun) {
143
- console.log('\n[Dry run] Would configure:\n');
144
- console.log(` MCP servers: ${mcpEntries.length}`);
145
- for (const e of mcpEntries)
146
- console.log(` - ${e.name}: ${e.url}`);
147
- console.log(` Skills (user): ${userSkillFiles.length}`);
148
- for (const s of userSkillFiles)
149
- console.log(` - ${s.name}`);
150
- if (scope === 'project' || scope === 'both') {
151
- console.log(` Skills (project): ${projectSkillFiles.length}`);
152
- for (const s of projectSkillFiles)
153
- console.log(` - ${s.name}`);
154
- }
155
- console.log(`\n Agents: ${agents.map(a => a.name).join(', ')}`);
156
- console.log(` Scope: ${scope}`);
157
- return;
158
- }
159
- // 7. Write configs
160
- const scopes = scope === 'both' ? ['project', 'user'] : [scope];
161
- for (const agent of agents) {
162
- for (const s of scopes) {
163
- const skillFiles = s === 'project' ? projectSkillFiles : userSkillFiles;
164
- try {
165
- if (agent.supportsMcpScope(s) && mcpEntries.length > 0) {
166
- await agent.writeMcpServers(mcpEntries, s);
167
- console.log(` [${agent.name}] MCP servers configured (${s})`);
168
- }
169
- if (agent.supportsSkills() && skillFiles.length > 0) {
170
- await agent.writeSkills(skillFiles, s);
171
- console.log(` [${agent.name}] Skills written (${s})`);
172
- }
173
- }
174
- catch (err) {
175
- console.warn(` [${agent.name}] Failed (${s}): ${err instanceof Error ? err.message : err}`);
176
- }
177
- }
178
- }
179
- // 8. Save setup state
84
+ catch { /* best-effort */ }
85
+ // 5. Save setup state (minimal, sync populates the rest)
180
86
  const state = {
181
87
  workspaceId,
182
88
  workspaceName: workspaceName || '',
89
+ workspaceSlug: workspaceSlug || undefined,
183
90
  configuredAgents: agents.map(a => a.slug),
184
91
  scope,
185
- lastSyncAt: new Date().toISOString(),
186
- mcpServers: mcpEntries.map(e => e.name),
187
- skills: userSkillFiles.map(s => s.name),
92
+ lastSyncAt: '',
93
+ mcpServers: [],
94
+ skills: [],
95
+ skillHashes: {},
188
96
  };
189
- if (scope === 'project' || scope === 'both') {
190
- mkdirSync('.runwork', { recursive: true });
191
- writeFileSync('.runwork/setup.json', JSON.stringify(state, null, 2));
192
- }
193
- if (scope === 'user' || scope === 'both') {
194
- const dir = join(homedir(), '.runwork');
97
+ const scopes = scope === 'both' ? ['project', 'user'] : [scope];
98
+ for (const s of scopes) {
99
+ const dir = s === 'project' ? '.runwork' : join(homedir(), '.runwork');
195
100
  mkdirSync(dir, { recursive: true });
196
101
  writeFileSync(join(dir, 'setup.json'), JSON.stringify(state, null, 2));
197
102
  }
103
+ if (opts.dryRun) {
104
+ console.log('\n[Dry run] Saved setup state. Would sync:\n');
105
+ console.log(` Agents: ${agents.map(a => a.name).join(', ')}`);
106
+ console.log(` Scope: ${scope}`);
107
+ console.log('\nRe-run without --dry-run to sync workspace data.');
108
+ return;
109
+ }
110
+ // 6. Delegate all write operations to sync
111
+ console.log('\nSyncing workspace data...\n');
112
+ for (const s of scopes) {
113
+ const statePath = s === 'project'
114
+ ? join(process.cwd(), '.runwork', 'setup.json')
115
+ : join(homedir(), '.runwork', 'setup.json');
116
+ await syncFromState(state, statePath, credentials, {
117
+ dryRun: false,
118
+ pullOnly: true,
119
+ yes: true,
120
+ prefer: 'remote',
121
+ });
122
+ }
198
123
  console.log('\nSetup complete. Run `runwork sync` anytime to refresh.');
199
124
  });