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
package/dist/commands/sync.js
CHANGED
|
@@ -6,6 +6,11 @@ import { requireAuth } from '../auth/store.js';
|
|
|
6
6
|
import { ApiClient } from '../api/client.js';
|
|
7
7
|
import { getAdapterBySlug } from '../agents/detect.js';
|
|
8
8
|
import { RUNWORK_MCP_PREFIX } from '../agents/types.js';
|
|
9
|
+
import { generateIntroSkill, generateInstructionHint, buildAppSkillDescription } from '../agents/intro-skill.js';
|
|
10
|
+
import { computeSyncPlan } from '../sync/change-detect.js';
|
|
11
|
+
import { printSyncSummary, resolveConflict, resolveConflictNonInteractive } from '../sync/conflict-ui.js';
|
|
12
|
+
import { executeSyncPlan } from '../sync/executor.js';
|
|
13
|
+
import { getRegistryAgent, resolveToAbsolute } from '../agents/registry.js';
|
|
9
14
|
function loadSetupState(filePath) {
|
|
10
15
|
if (!existsSync(filePath))
|
|
11
16
|
return null;
|
|
@@ -16,115 +21,444 @@ function loadSetupState(filePath) {
|
|
|
16
21
|
return null;
|
|
17
22
|
}
|
|
18
23
|
}
|
|
19
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Read local skill content from the first configured agent's skill directory.
|
|
26
|
+
*/
|
|
27
|
+
function readLocalSkills(state) {
|
|
28
|
+
const results = [];
|
|
29
|
+
if (!state.skills || state.skills.length === 0)
|
|
30
|
+
return results;
|
|
31
|
+
for (const slug of state.configuredAgents) {
|
|
32
|
+
const def = getRegistryAgent(slug);
|
|
33
|
+
if (!def?.skillsPaths)
|
|
34
|
+
continue;
|
|
35
|
+
const useScope = state.scope === 'both' ? 'user' : state.scope;
|
|
36
|
+
const pathTemplate = useScope === 'project' ? def.skillsPaths.project : def.skillsPaths.global;
|
|
37
|
+
if (!pathTemplate)
|
|
38
|
+
continue;
|
|
39
|
+
const baseDir = resolveToAbsolute(pathTemplate, useScope === 'project' ? 'project' : 'global');
|
|
40
|
+
if (!baseDir)
|
|
41
|
+
continue;
|
|
42
|
+
for (const skillName of state.skills) {
|
|
43
|
+
// Try SKILL.md in subdirectory
|
|
44
|
+
const skillMdPath = join(baseDir, skillName, 'SKILL.md');
|
|
45
|
+
if (existsSync(skillMdPath)) {
|
|
46
|
+
results.push({ name: skillName, content: readFileSync(skillMdPath, 'utf-8') });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Try flat file
|
|
50
|
+
const filename = skillName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
51
|
+
const flatPath = join(baseDir, `${filename}.md`);
|
|
52
|
+
if (existsSync(flatPath)) {
|
|
53
|
+
results.push({ name: skillName, content: readFileSync(flatPath, 'utf-8') });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (results.length > 0)
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
export async function syncFromState(state, statePath, credentials, opts) {
|
|
20
62
|
const client = new ApiClient(credentials);
|
|
63
|
+
// Refresh workspace name/slug if missing or stale (e.g. setup ran before name was set)
|
|
64
|
+
if (!state.workspaceName || state.workspaceName === state.workspaceId || !state.workspaceSlug) {
|
|
65
|
+
try {
|
|
66
|
+
const workspaces = await client.listWorkspaces();
|
|
67
|
+
const ws = workspaces.find(w => w.id === state.workspaceId);
|
|
68
|
+
if (ws?.name)
|
|
69
|
+
state.workspaceName = ws.name;
|
|
70
|
+
if (ws?.slug)
|
|
71
|
+
state.workspaceSlug = ws.slug;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Best-effort, keep existing values
|
|
75
|
+
}
|
|
76
|
+
}
|
|
21
77
|
console.log(`Syncing workspace: ${state.workspaceName || state.workspaceId}`);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
78
|
+
console.log(' Fetching workspace data...');
|
|
79
|
+
// Fetch latest data (includeContent=true gets all skill content in one request)
|
|
80
|
+
const [allSkills, mcpServers, externalSkills, registries, connectedIntegrations] = await Promise.all([
|
|
81
|
+
client.listWorkspaceSkills(state.workspaceId, true),
|
|
25
82
|
client.listMcpServers(state.workspaceId),
|
|
26
83
|
client.listExternalSkills(state.workspaceId),
|
|
84
|
+
client.getWorkspaceAll(state.workspaceId).catch(() => null),
|
|
85
|
+
client.listConnectedIntegrations(state.workspaceId)
|
|
86
|
+
.then(list => list.map(i => i.canonicalId ?? i.integrationId))
|
|
87
|
+
.catch(() => []),
|
|
27
88
|
]);
|
|
28
|
-
|
|
29
|
-
const
|
|
89
|
+
const appCount = allSkills.filter(s => s.type === 'app').length;
|
|
90
|
+
const parts = [];
|
|
91
|
+
if (appCount > 0)
|
|
92
|
+
parts.push(`${appCount} app${appCount > 1 ? 's' : ''}`);
|
|
93
|
+
if (externalSkills.length > 0)
|
|
94
|
+
parts.push(`${externalSkills.length} skill${externalSkills.length > 1 ? 's' : ''}`);
|
|
95
|
+
if (mcpServers.length > 0)
|
|
96
|
+
parts.push(`${mcpServers.filter(s => s.enabled).length} MCP server${mcpServers.filter(s => s.enabled).length > 1 ? 's' : ''}`);
|
|
97
|
+
if (connectedIntegrations.length > 0)
|
|
98
|
+
parts.push(`${connectedIntegrations.length} integration${connectedIntegrations.length > 1 ? 's' : ''}`);
|
|
99
|
+
if (registries) {
|
|
100
|
+
if (registries.entities.length > 0)
|
|
101
|
+
parts.push(`${registries.entities.length} entit${registries.entities.length > 1 ? 'ies' : 'y'}`);
|
|
102
|
+
if (registries.workflows.length > 0)
|
|
103
|
+
parts.push(`${registries.workflows.length} workflow${registries.workflows.length > 1 ? 's' : ''}`);
|
|
104
|
+
if (registries.agents.length > 0)
|
|
105
|
+
parts.push(`${registries.agents.length} agent${registries.agents.length > 1 ? 's' : ''}`);
|
|
106
|
+
}
|
|
107
|
+
if (parts.length > 0) {
|
|
108
|
+
console.log(` Found: ${parts.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
// Build MCP entries (pull-only)
|
|
30
111
|
const baseUrl = credentials.baseUrl || 'https://runwork.ai';
|
|
31
112
|
const mcpEntries = mcpServers
|
|
32
113
|
.filter(s => s.enabled)
|
|
33
114
|
.map(s => ({
|
|
34
|
-
name: `${RUNWORK_MCP_PREFIX}${
|
|
115
|
+
name: `${RUNWORK_MCP_PREFIX}${s.name}`,
|
|
35
116
|
url: s.url,
|
|
36
117
|
transport: s.transport,
|
|
37
118
|
headers: { Authorization: `Bearer ${credentials.apiKey}` },
|
|
119
|
+
description: `Runwork MCP server: ${s.name}. Provides tools for interacting with this workspace resource.`,
|
|
38
120
|
}));
|
|
39
121
|
mcpEntries.push({
|
|
40
|
-
name: `${RUNWORK_MCP_PREFIX}
|
|
122
|
+
name: `${RUNWORK_MCP_PREFIX}Workspace Tools`,
|
|
41
123
|
url: `${baseUrl}/api/workspaces/${state.workspaceId}/mcp`,
|
|
42
124
|
transport: 'streamable-http',
|
|
43
125
|
headers: { Authorization: `Bearer ${credentials.apiKey}` },
|
|
126
|
+
description: 'Runwork workspace tools: query entities, trigger workflows, manage integrations, run agents, and access shared data across your team\'s apps.',
|
|
44
127
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
content: s.content,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
128
|
+
// Build remote skills list
|
|
129
|
+
const remoteSkills = [];
|
|
130
|
+
for (const s of externalSkills) {
|
|
131
|
+
remoteSkills.push({ name: s.name, content: s.content, source: 'external', remoteId: s.id });
|
|
132
|
+
}
|
|
133
|
+
for (const s of allSkills) {
|
|
134
|
+
if (s.type === 'app' && s.appId && s.content?.trim()) {
|
|
135
|
+
remoteSkills.push({ name: s.name, content: s.content, source: 'app', appId: s.appId });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Read local skills for change detection
|
|
139
|
+
const localSkills = readLocalSkills(state);
|
|
140
|
+
// Compute sync plan
|
|
141
|
+
const plan = computeSyncPlan({
|
|
142
|
+
storedHashes: state.skillHashes || {},
|
|
143
|
+
localSkills,
|
|
144
|
+
remoteSkills,
|
|
145
|
+
});
|
|
146
|
+
// In pull-only mode, move pushes and conflicts to skips
|
|
147
|
+
if (opts.pullOnly) {
|
|
148
|
+
plan.skips.push(...plan.pushes.map(p => ({ ...p, action: 'skip' })));
|
|
149
|
+
plan.pushes = [];
|
|
150
|
+
plan.skips.push(...plan.conflicts.map(c => ({ ...c, action: 'skip' })));
|
|
151
|
+
plan.conflicts = [];
|
|
152
|
+
}
|
|
153
|
+
// Display summary
|
|
154
|
+
const hasChanges = plan.pulls.length > 0 || plan.pushes.length > 0 || plan.conflicts.length > 0 || plan.deletions.length > 0;
|
|
155
|
+
if (hasChanges) {
|
|
156
|
+
printSyncSummary(plan);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.log(' All skills up to date.');
|
|
160
|
+
}
|
|
161
|
+
if (opts.dryRun)
|
|
162
|
+
return;
|
|
163
|
+
// Resolve conflicts
|
|
164
|
+
const resolvedConflicts = new Map();
|
|
165
|
+
for (const conflict of plan.conflicts) {
|
|
166
|
+
if (opts.yes || !process.stdout.isTTY) {
|
|
167
|
+
const nonInteractiveResolution = resolveConflictNonInteractive(opts.prefer || 'remote');
|
|
168
|
+
if (nonInteractiveResolution !== 'skip') {
|
|
169
|
+
resolvedConflicts.set(conflict.name, nonInteractiveResolution);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const resolution = await resolveConflict(conflict);
|
|
174
|
+
if (resolution !== 'skip') {
|
|
175
|
+
resolvedConflicts.set(conflict.name, resolution);
|
|
64
176
|
}
|
|
65
177
|
}
|
|
66
|
-
catch { /* skip */ }
|
|
67
|
-
return null;
|
|
68
|
-
}));
|
|
69
|
-
for (const r of appSkillResults) {
|
|
70
|
-
if (r)
|
|
71
|
-
allAppSkillFiles.push(r);
|
|
72
178
|
}
|
|
73
|
-
//
|
|
74
|
-
|
|
179
|
+
// Build adapters
|
|
180
|
+
const adapters = [];
|
|
181
|
+
for (const slug of state.configuredAgents) {
|
|
182
|
+
const adapter = getAdapterBySlug(slug);
|
|
183
|
+
if (adapter)
|
|
184
|
+
adapters.push(adapter);
|
|
185
|
+
}
|
|
186
|
+
const scopes = state.scope === 'both' ? ['project', 'user'] : [state.scope];
|
|
187
|
+
if (adapters.length > 0) {
|
|
188
|
+
console.log(` Syncing to: ${adapters.map(a => a.name).join(', ')}`);
|
|
189
|
+
}
|
|
190
|
+
// Execute skill sync plan
|
|
191
|
+
const newHashes = await executeSyncPlan(plan, resolvedConflicts, {
|
|
192
|
+
client,
|
|
193
|
+
workspaceId: state.workspaceId,
|
|
194
|
+
adapters,
|
|
195
|
+
scopes,
|
|
196
|
+
});
|
|
197
|
+
// Write intro skill, MCP configs, instruction hints (pull-only)
|
|
198
|
+
const introSkill = generateIntroSkill({
|
|
199
|
+
workspaceName: state.workspaceName || state.workspaceId,
|
|
200
|
+
workspaceId: state.workspaceId,
|
|
201
|
+
workspaceSlug: state.workspaceSlug,
|
|
202
|
+
registries,
|
|
203
|
+
connectedIntegrations,
|
|
204
|
+
mcpServerCount: mcpEntries.length,
|
|
205
|
+
skillCount: remoteSkills.length,
|
|
206
|
+
mcpServerNames: mcpEntries.map(e => e.name.replace(RUNWORK_MCP_PREFIX, '')),
|
|
207
|
+
skillNames: remoteSkills.map(s => s.name),
|
|
208
|
+
});
|
|
209
|
+
const instructionHint = generateInstructionHint({
|
|
210
|
+
workspaceName: state.workspaceName || state.workspaceId,
|
|
211
|
+
workspaceId: state.workspaceId,
|
|
212
|
+
workspaceSlug: state.workspaceSlug,
|
|
213
|
+
registries,
|
|
214
|
+
connectedIntegrations,
|
|
215
|
+
appCount: allSkills.filter(s => s.type === 'app').length,
|
|
216
|
+
skillCount: remoteSkills.length,
|
|
217
|
+
mcpServerCount: mcpEntries.length,
|
|
218
|
+
});
|
|
219
|
+
// For project scope: determine which app skill to include (only current app's skill)
|
|
220
|
+
let projectAppSkillFilter = null;
|
|
75
221
|
if (existsSync('.runwork.json')) {
|
|
76
222
|
try {
|
|
77
223
|
const config = JSON.parse(readFileSync('.runwork.json', 'utf-8'));
|
|
78
224
|
if (config.appName) {
|
|
79
|
-
|
|
80
|
-
projectAppSkillFile = allAppSkillFiles.find(s => s.filename === fname) || null;
|
|
225
|
+
projectAppSkillFilter = config.appName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
81
226
|
}
|
|
82
227
|
}
|
|
83
228
|
catch { /* ignore */ }
|
|
84
229
|
}
|
|
85
|
-
const
|
|
86
|
-
const projectSkillFiles = projectAppSkillFile
|
|
87
|
-
? [...baseSkillFiles, projectAppSkillFile]
|
|
88
|
-
: [...baseSkillFiles];
|
|
89
|
-
if (dryRun) {
|
|
90
|
-
console.log(` Would update: ${mcpEntries.length} MCP servers, ${userSkillFiles.length} skills (user), ${projectSkillFiles.length} skills (project)`);
|
|
91
|
-
console.log(` Agents: ${state.configuredAgents.join(', ')}`);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
// Write to configured agents
|
|
95
|
-
const scopes = state.scope === 'both' ? ['project', 'user'] : [state.scope];
|
|
96
|
-
for (const slug of state.configuredAgents) {
|
|
97
|
-
const adapter = getAdapterBySlug(slug);
|
|
98
|
-
if (!adapter)
|
|
99
|
-
continue;
|
|
230
|
+
for (const adapter of adapters) {
|
|
100
231
|
for (const scope of scopes) {
|
|
101
|
-
const skillFiles = scope === 'project' ? projectSkillFiles : userSkillFiles;
|
|
102
232
|
try {
|
|
233
|
+
// Write skills: for project scope, filter to only the current app's skill
|
|
234
|
+
if (adapter.supportsSkills()) {
|
|
235
|
+
const scopeSkills = remoteSkills.filter(s => {
|
|
236
|
+
if (scope === 'project' && s.source === 'app' && projectAppSkillFilter) {
|
|
237
|
+
const filename = s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
238
|
+
return filename === projectAppSkillFilter;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
});
|
|
242
|
+
const allSkillFiles = [introSkill, ...scopeSkills.map(s => ({
|
|
243
|
+
name: s.name,
|
|
244
|
+
filename: s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
245
|
+
content: s.content,
|
|
246
|
+
description: buildAppSkillDescription(s.name, registries) || `${s.name} skill`,
|
|
247
|
+
}))];
|
|
248
|
+
await adapter.writeSkills(allSkillFiles, scope);
|
|
249
|
+
}
|
|
250
|
+
// Write MCP configs
|
|
103
251
|
if (adapter.supportsMcpScope(scope) && mcpEntries.length > 0) {
|
|
104
252
|
await adapter.writeMcpServers(mcpEntries, scope);
|
|
253
|
+
console.log(` [${adapter.name}] Updated ${mcpEntries.length} MCP server${mcpEntries.length > 1 ? 's' : ''} (${scope})`);
|
|
105
254
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
255
|
+
// Write instruction hint
|
|
256
|
+
await adapter.writeInstructionHint(instructionHint, scope);
|
|
257
|
+
console.log(` [${adapter.name}] Updated instruction hints (${scope})`);
|
|
109
258
|
}
|
|
110
259
|
catch (err) {
|
|
111
260
|
console.warn(` [${adapter.name}] Failed (${scope}): ${err instanceof Error ? err.message : err}`);
|
|
112
261
|
}
|
|
113
262
|
}
|
|
114
|
-
|
|
263
|
+
}
|
|
264
|
+
// Pull and apply team instructions from onboarding config (non-fatal)
|
|
265
|
+
let teamInstructionsApplied = false;
|
|
266
|
+
let agentConfigsApplied = 0;
|
|
267
|
+
try {
|
|
268
|
+
const onboardingConfig = await client.getOnboardingConfig(state.workspaceId);
|
|
269
|
+
const teamInstructions = onboardingConfig?.config?.teamInstructions;
|
|
270
|
+
const agentConfigs = onboardingConfig?.config?.agentConfigs;
|
|
271
|
+
// Write team instructions and/or per-agent extra instructions.
|
|
272
|
+
// Either or both may be present independently.
|
|
273
|
+
for (const adapter of adapters) {
|
|
274
|
+
if (!adapter.writeTeamInstructions)
|
|
275
|
+
continue;
|
|
276
|
+
const agentExtra = agentConfigs?.[adapter.slug]?.extraInstructions;
|
|
277
|
+
const parts = [teamInstructions, agentExtra].filter(Boolean);
|
|
278
|
+
if (parts.length === 0)
|
|
279
|
+
continue;
|
|
280
|
+
const fullInstructions = parts.join('\n\n');
|
|
281
|
+
for (const scope of scopes) {
|
|
282
|
+
try {
|
|
283
|
+
await adapter.writeTeamInstructions(fullInstructions, scope);
|
|
284
|
+
teamInstructionsApplied = true;
|
|
285
|
+
console.log(` [${adapter.name}] Updated team instructions (${scope})`);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// Best-effort per adapter/scope
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Apply per-agent config overrides (model preferences, permissions)
|
|
293
|
+
if (agentConfigs) {
|
|
294
|
+
for (const adapter of adapters) {
|
|
295
|
+
const agentConfig = agentConfigs[adapter.slug];
|
|
296
|
+
if (!agentConfig || !adapter.writeAgentConfig)
|
|
297
|
+
continue;
|
|
298
|
+
// Only apply model/permission config (extraInstructions already handled above)
|
|
299
|
+
const { extraInstructions: _, ...configWithoutInstructions } = agentConfig;
|
|
300
|
+
if (Object.keys(configWithoutInstructions).length === 0)
|
|
301
|
+
continue;
|
|
302
|
+
for (const scope of scopes) {
|
|
303
|
+
try {
|
|
304
|
+
await adapter.writeAgentConfig(configWithoutInstructions, scope);
|
|
305
|
+
agentConfigsApplied++;
|
|
306
|
+
const configKeys = Object.keys(configWithoutInstructions).join(', ');
|
|
307
|
+
console.log(` [${adapter.name}] Updated agent config: ${configKeys} (${scope})`);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Best-effort per adapter/scope
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Team config pull is non-fatal
|
|
115
318
|
}
|
|
116
319
|
// Update state
|
|
117
320
|
state.lastSyncAt = new Date().toISOString();
|
|
118
321
|
state.mcpServers = mcpEntries.map(e => e.name);
|
|
119
|
-
state.skills =
|
|
322
|
+
state.skills = remoteSkills.map(s => s.name);
|
|
323
|
+
// Merge hashes: keep existing for skips, update for everything else
|
|
324
|
+
const mergedHashes = { ...(state.skillHashes || {}) };
|
|
325
|
+
for (const [name, hash] of Object.entries(newHashes)) {
|
|
326
|
+
mergedHashes[name] = hash;
|
|
327
|
+
}
|
|
328
|
+
// Remove deleted skills
|
|
329
|
+
for (const del of plan.deletions) {
|
|
330
|
+
delete mergedHashes[del.name];
|
|
331
|
+
}
|
|
332
|
+
state.skillHashes = mergedHashes;
|
|
120
333
|
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
334
|
+
// Report telemetry (non-fatal)
|
|
335
|
+
try {
|
|
336
|
+
const now = new Date().toISOString();
|
|
337
|
+
const previouslyReported = new Set(state.reportedAgentSlugs ?? []);
|
|
338
|
+
const telemetryEvents = [];
|
|
339
|
+
// Only report newly detected agents (not already reported in previous syncs)
|
|
340
|
+
const newAgents = [];
|
|
341
|
+
for (const slug of state.configuredAgents) {
|
|
342
|
+
if (!previouslyReported.has(slug)) {
|
|
343
|
+
telemetryEvents.push({
|
|
344
|
+
eventType: 'local_agent.detected',
|
|
345
|
+
agentSlug: slug,
|
|
346
|
+
metadata: { installed: true },
|
|
347
|
+
timestamp: now,
|
|
348
|
+
});
|
|
349
|
+
newAgents.push(slug);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Collect passive usage stats from each agent
|
|
353
|
+
for (const adapter of adapters) {
|
|
354
|
+
try {
|
|
355
|
+
const stats = await adapter.readUsageStats?.(state.lastSyncAt ?? null);
|
|
356
|
+
if (stats && stats.hasNewActivity) {
|
|
357
|
+
telemetryEvents.push({
|
|
358
|
+
eventType: 'local_agent.usage',
|
|
359
|
+
agentSlug: adapter.slug,
|
|
360
|
+
metadata: {
|
|
361
|
+
sessionCount: stats.sessionCount,
|
|
362
|
+
messageCount: stats.messageCount,
|
|
363
|
+
toolCallCount: stats.toolCallCount,
|
|
364
|
+
tokensUsed: stats.tokensUsed,
|
|
365
|
+
aiLinesAdded: stats.aiLinesAdded,
|
|
366
|
+
aiLinesRemoved: stats.aiLinesRemoved,
|
|
367
|
+
lastActiveAt: stats.lastActiveAt,
|
|
368
|
+
modelsUsed: stats.modelsUsed,
|
|
369
|
+
mcpToolCount: stats.mcpToolCount,
|
|
370
|
+
...stats.extra,
|
|
371
|
+
},
|
|
372
|
+
timestamp: now,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// Usage stats collection is best-effort
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Collect skill usage stats
|
|
381
|
+
for (const adapter of adapters) {
|
|
382
|
+
try {
|
|
383
|
+
const skillUsage = await adapter.readSkillUsage?.(state.lastSyncAt ?? null);
|
|
384
|
+
if (skillUsage && skillUsage.length > 0) {
|
|
385
|
+
for (const entry of skillUsage) {
|
|
386
|
+
telemetryEvents.push({
|
|
387
|
+
eventType: 'local_agent.skill_used',
|
|
388
|
+
agentSlug: adapter.slug,
|
|
389
|
+
metadata: {
|
|
390
|
+
skillName: entry.skillName,
|
|
391
|
+
count: entry.count,
|
|
392
|
+
lastUsedAt: entry.lastUsedAt,
|
|
393
|
+
},
|
|
394
|
+
timestamp: now,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Skill usage collection is best-effort
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Report agent health once per day (version, platform)
|
|
404
|
+
const lastHealthReport = state.lastHealthReportAt ? new Date(state.lastHealthReportAt) : null;
|
|
405
|
+
const shouldReportHealth = !lastHealthReport ||
|
|
406
|
+
(Date.now() - lastHealthReport.getTime()) > 24 * 60 * 60 * 1000;
|
|
407
|
+
if (shouldReportHealth) {
|
|
408
|
+
for (const adapter of adapters) {
|
|
409
|
+
try {
|
|
410
|
+
const version = await adapter.readVersion?.();
|
|
411
|
+
telemetryEvents.push({
|
|
412
|
+
eventType: 'local_agent.health',
|
|
413
|
+
agentSlug: adapter.slug,
|
|
414
|
+
metadata: {
|
|
415
|
+
version,
|
|
416
|
+
installed: true,
|
|
417
|
+
platform: process.platform,
|
|
418
|
+
arch: process.arch,
|
|
419
|
+
},
|
|
420
|
+
timestamp: now,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
// Health reporting is best-effort
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
state.lastHealthReportAt = now;
|
|
428
|
+
}
|
|
429
|
+
telemetryEvents.push({
|
|
430
|
+
eventType: 'local_agent.sync_completed',
|
|
431
|
+
metadata: {
|
|
432
|
+
syncedSkills: remoteSkills.length,
|
|
433
|
+
syncedMcpServers: mcpEntries.length,
|
|
434
|
+
teamInstructionsApplied,
|
|
435
|
+
agentConfigsApplied,
|
|
436
|
+
},
|
|
437
|
+
timestamp: now,
|
|
438
|
+
});
|
|
439
|
+
await client.reportTelemetry(state.workspaceId, telemetryEvents);
|
|
440
|
+
// Track which agents have been reported so we don't spam on future syncs
|
|
441
|
+
state.reportedAgentSlugs = [...new Set([...previouslyReported, ...newAgents])];
|
|
442
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
// Telemetry failures are non-fatal
|
|
446
|
+
}
|
|
121
447
|
}
|
|
122
448
|
export const syncCommand = new Command('sync')
|
|
123
|
-
.description('
|
|
124
|
-
.option('--dry-run', 'Show what would change without
|
|
449
|
+
.description('Sync skills bidirectionally and refresh MCP configs from workspace')
|
|
450
|
+
.option('--dry-run', 'Show what would change without doing anything')
|
|
451
|
+
.option('--pull-only', 'Only pull remote changes, never push local edits')
|
|
452
|
+
.option('--prefer <side>', 'Auto-resolve conflicts: "local" or "remote" (implies --yes)')
|
|
453
|
+
.option('-y, --yes', 'Non-interactive mode (default: prefer remote for conflicts)')
|
|
125
454
|
.action(async (opts) => {
|
|
126
455
|
const credentials = requireAuth();
|
|
127
|
-
const
|
|
456
|
+
const syncOpts = {
|
|
457
|
+
dryRun: !!opts.dryRun,
|
|
458
|
+
pullOnly: !!opts.pullOnly,
|
|
459
|
+
yes: !!opts.yes || !!opts.prefer,
|
|
460
|
+
prefer: opts.prefer,
|
|
461
|
+
};
|
|
128
462
|
const projectStatePath = join(process.cwd(), '.runwork', 'setup.json');
|
|
129
463
|
const userStatePath = join(homedir(), '.runwork', 'setup.json');
|
|
130
464
|
const projectState = loadSetupState(projectStatePath);
|
|
@@ -133,14 +467,13 @@ export const syncCommand = new Command('sync')
|
|
|
133
467
|
console.error('No setup state found. Run `runwork setup` first.');
|
|
134
468
|
process.exit(1);
|
|
135
469
|
}
|
|
136
|
-
// Sync both states independently — even if same workspace, they may target different scopes/agents
|
|
137
470
|
if (projectState) {
|
|
138
|
-
await syncFromState(projectState, projectStatePath, credentials,
|
|
471
|
+
await syncFromState(projectState, projectStatePath, credentials, syncOpts);
|
|
139
472
|
}
|
|
140
473
|
if (userState) {
|
|
141
|
-
await syncFromState(userState, userStatePath, credentials,
|
|
474
|
+
await syncFromState(userState, userStatePath, credentials, syncOpts);
|
|
142
475
|
}
|
|
143
|
-
if (!dryRun) {
|
|
476
|
+
if (!syncOpts.dryRun) {
|
|
144
477
|
console.log('\nSync complete.');
|
|
145
478
|
}
|
|
146
479
|
});
|
package/dist/commands/welcome.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
1
2
|
import { getCredentials } from '../auth/store.js';
|
|
2
3
|
import { performLogin } from '../auth/login-flow.js';
|
|
3
4
|
import { ApiClient } from '../api/client.js';
|
|
@@ -50,7 +51,8 @@ export async function runWelcomeWizard() {
|
|
|
50
51
|
console.log('');
|
|
51
52
|
console.log(`Creating ${cyan(appName)} in workspace ${cyan(workspace.name)}...`);
|
|
52
53
|
const { execInit } = await import('./init.js');
|
|
53
|
-
|
|
54
|
+
const initResult = await execInit(client, appName, workspace);
|
|
55
|
+
appDir = initResult.directory;
|
|
54
56
|
}
|
|
55
57
|
else {
|
|
56
58
|
const apps = await client.listApps();
|
|
@@ -60,7 +62,8 @@ export async function runWelcomeWizard() {
|
|
|
60
62
|
}
|
|
61
63
|
const choice = await promptSelect('Select app to clone:', apps.map(a => ({ label: `${a.name} (${a.workspaceName})`, value: a })));
|
|
62
64
|
const { execClone } = await import('./clone.js');
|
|
63
|
-
|
|
65
|
+
const cloneResult = await execClone(client, choice.value);
|
|
66
|
+
appDir = cloneResult.directory;
|
|
64
67
|
}
|
|
65
68
|
// Step 4: Agent guidance
|
|
66
69
|
await runAgentWizard(appDir);
|
|
@@ -76,7 +79,7 @@ export async function runWelcomeWizard() {
|
|
|
76
79
|
await execDev();
|
|
77
80
|
}
|
|
78
81
|
else {
|
|
79
|
-
const slug = appDir
|
|
82
|
+
const slug = basename(appDir) || appDir;
|
|
80
83
|
console.log('');
|
|
81
84
|
console.log(`Next: ${cyan(`cd ${slug} && runwork dev`)}`);
|
|
82
85
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.6.0";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-types.ts -- do not edit
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.6.0";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|