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
@@ -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
- async function syncFromState(state, statePath, credentials, dryRun) {
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
- // Fetch latest data
23
- const [allSkills, mcpServers, externalSkills] = await Promise.all([
24
- client.listWorkspaceSkills(state.workspaceId),
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
- // Build entries
29
- const wsPrefix = state.workspaceId.substring(0, 8);
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}${wsPrefix}-${s.name}`,
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}${wsPrefix}-workspace`,
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
- const baseSkillFiles = externalSkills.map(s => ({
46
- name: s.name,
47
- filename: s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
48
- content: s.content,
49
- description: s.description,
50
- }));
51
- // Fetch all app skills in parallel
52
- const allAppSkillFiles = [];
53
- const appSkillEntries = allSkills.filter(s => s.type === 'app' && s.appId);
54
- const appSkillResults = await Promise.all(appSkillEntries.map(async (s) => {
55
- try {
56
- const content = await client.getAppSkillContent(state.workspaceId, s.appId);
57
- if (content && content.trim()) {
58
- return {
59
- name: s.name,
60
- filename: s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
61
- content,
62
- description: s.description || `App skill for ${s.name}`,
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
- // Project scope: only current app's skill; User scope: all app skills
74
- let projectAppSkillFile = null;
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
- const fname = config.appName.toLowerCase().replace(/[^a-z0-9]+/g, '-');
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 userSkillFiles = [...baseSkillFiles, ...allAppSkillFiles];
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
- if (adapter.supportsSkills() && skillFiles.length > 0) {
107
- await adapter.writeSkills(skillFiles, scope);
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
- console.log(` [${adapter.name}] Updated`);
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 = userSkillFiles.map(s => s.name);
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('Refresh local agent configs with latest workspace skills and MCP servers')
124
- .option('--dry-run', 'Show what would change without writing files')
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 dryRun = !!opts.dryRun;
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, dryRun);
471
+ await syncFromState(projectState, projectStatePath, credentials, syncOpts);
139
472
  }
140
473
  if (userState) {
141
- await syncFromState(userState, userStatePath, credentials, dryRun);
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
  });
@@ -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
- appDir = await execInit(client, appName, workspace);
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
- appDir = await execClone(client, choice.value);
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.split('/').pop() || 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.5.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.5.0";
2
+ export const VERSION = "0.6.0";
@@ -0,0 +1 @@
1
+ export {};