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.
- package/dist/agents/__tests__/claude-code-stats.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-code-stats.test.js +153 -0
- package/dist/agents/__tests__/claude-desktop-stats.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-stats.test.js +280 -0
- package/dist/agents/__tests__/codex-stats.test.d.ts +1 -0
- package/dist/agents/__tests__/codex-stats.test.js +262 -0
- package/dist/agents/__tests__/cursor-stats.test.d.ts +1 -0
- package/dist/agents/__tests__/cursor-stats.test.js +183 -0
- package/dist/agents/__tests__/graceful-degradation.test.d.ts +1 -0
- package/dist/agents/__tests__/graceful-degradation.test.js +123 -0
- package/dist/agents/__tests__/instruction-hint.test.d.ts +1 -0
- package/dist/agents/__tests__/instruction-hint.test.js +69 -0
- package/dist/agents/__tests__/intro-skill.test.d.ts +1 -0
- package/dist/agents/__tests__/intro-skill.test.js +135 -0
- package/dist/agents/__tests__/schema-validation.test.d.ts +1 -0
- package/dist/agents/__tests__/schema-validation.test.js +212 -0
- package/dist/agents/claude-code.d.ts +16 -1
- package/dist/agents/claude-code.js +344 -21
- package/dist/agents/claude-desktop.d.ts +10 -1
- package/dist/agents/claude-desktop.js +250 -8
- package/dist/agents/cline.d.ts +13 -0
- package/dist/agents/cline.js +88 -0
- package/dist/agents/codex.d.ts +7 -2
- package/dist/agents/codex.js +130 -18
- package/dist/agents/cursor.d.ts +6 -1
- package/dist/agents/cursor.js +164 -7
- package/dist/agents/detect.js +30 -7
- package/dist/agents/gemini.d.ts +6 -3
- package/dist/agents/gemini.js +57 -20
- package/dist/agents/generic-adapter.d.ts +23 -0
- package/dist/agents/generic-adapter.js +106 -0
- package/dist/agents/intro-skill.d.ts +36 -0
- package/dist/agents/intro-skill.js +358 -0
- package/dist/agents/registry.d.ts +53 -0
- package/dist/agents/registry.js +219 -0
- package/dist/agents/types.d.ts +44 -1
- package/dist/agents/types.js +11 -4
- package/dist/agents/utils/instruction-hint.d.ts +11 -0
- package/dist/agents/utils/instruction-hint.js +62 -0
- package/dist/agents/utils/json-config.js +3 -3
- package/dist/agents/windsurf.d.ts +2 -0
- package/dist/agents/windsurf.js +26 -9
- package/dist/api/client.d.ts +85 -2
- package/dist/api/client.js +40 -10
- package/dist/auth/__tests__/login-flow.test.d.ts +1 -0
- package/dist/auth/__tests__/login-flow.test.js +125 -0
- package/dist/auth/login-flow.d.ts +16 -0
- package/dist/auth/login-flow.js +101 -1
- package/dist/commands/clone.d.ts +9 -1
- package/dist/commands/clone.js +40 -12
- package/dist/commands/deploy.js +27 -4
- package/dist/commands/dev.js +35 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +82 -0
- package/dist/commands/info.js +6 -2
- package/dist/commands/init.d.ts +10 -2
- package/dist/commands/init.js +58 -15
- package/dist/commands/integrations.js +47 -10
- package/dist/commands/login.js +31 -4
- package/dist/commands/mcp.js +143 -15
- package/dist/commands/setup.js +47 -122
- package/dist/commands/skills.js +230 -12
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.js +398 -65
- package/dist/commands/welcome.js +6 -3
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/health/__tests__/checks.test.d.ts +1 -0
- package/dist/health/__tests__/checks.test.js +223 -0
- package/dist/health/checks.d.ts +20 -0
- package/dist/health/checks.js +392 -0
- package/dist/health/runner.d.ts +2 -0
- package/dist/health/runner.js +26 -0
- package/dist/health/types.d.ts +12 -0
- package/dist/health/types.js +1 -0
- package/dist/index.js +32 -0
- package/dist/sync/__tests__/change-detect.test.d.ts +1 -0
- package/dist/sync/__tests__/change-detect.test.js +123 -0
- package/dist/sync/__tests__/hash.test.d.ts +1 -0
- package/dist/sync/__tests__/hash.test.js +18 -0
- package/dist/sync/change-detect.d.ts +19 -0
- package/dist/sync/change-detect.js +136 -0
- package/dist/sync/conflict-ui.d.ts +5 -0
- package/dist/sync/conflict-ui.js +78 -0
- package/dist/sync/executor.d.ts +21 -0
- package/dist/sync/executor.js +98 -0
- package/dist/sync/hash.d.ts +1 -0
- package/dist/sync/hash.js +4 -0
- package/dist/sync/types.d.ts +24 -0
- package/dist/sync/types.js +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/ui/__tests__/banner.test.js +17 -1
- package/dist/ui/banner.d.ts +1 -1
- package/dist/ui/banner.js +27 -2
- package/dist/utils/__tests__/resolve.test.d.ts +1 -0
- package/dist/utils/__tests__/resolve.test.js +95 -0
- package/dist/utils/__tests__/sqlite.test.d.ts +1 -0
- package/dist/utils/__tests__/sqlite.test.js +77 -0
- package/dist/utils/agent-guidance.d.ts +32 -0
- package/dist/utils/agent-guidance.js +106 -0
- package/dist/utils/fs.js +2 -2
- package/dist/utils/help-json.d.ts +56 -0
- package/dist/utils/help-json.js +85 -0
- package/dist/utils/resolve.d.ts +12 -0
- package/dist/utils/resolve.js +52 -0
- package/dist/utils/setup-state.d.ts +3 -0
- package/dist/utils/setup-state.js +19 -0
- package/dist/utils/sqlite.d.ts +7 -0
- package/dist/utils/sqlite.js +24 -0
- package/dist/utils/which.d.ts +6 -0
- package/dist/utils/which.js +18 -0
- package/package.json +7 -3
- package/dist/agents/copilot-cli.d.ts +0 -10
- package/dist/agents/copilot-cli.js +0 -46
- package/dist/agents/copilot-vscode.d.ts +0 -10
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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) {
|
package/dist/commands/login.js
CHANGED
|
@@ -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
|
-
.
|
|
8
|
-
|
|
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
|
+
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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
|
|
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({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
37
|
+
// Workspace MCP
|
|
38
|
+
console.log(' Workspace MCP (all tools across apps)');
|
|
28
39
|
console.log(' ' + '-'.repeat(75));
|
|
29
|
-
|
|
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);
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
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 {
|
|
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.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
client.
|
|
73
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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:
|
|
186
|
-
mcpServers:
|
|
187
|
-
skills:
|
|
92
|
+
lastSyncAt: '',
|
|
93
|
+
mcpServers: [],
|
|
94
|
+
skills: [],
|
|
95
|
+
skillHashes: {},
|
|
188
96
|
};
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
});
|