runwork 0.5.1 → 0.6.1

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 (118) 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 +148 -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/curl-parser.d.ts +1 -1
  102. package/dist/utils/curl-parser.js +144 -26
  103. package/dist/utils/fs.js +2 -2
  104. package/dist/utils/help-json.d.ts +56 -0
  105. package/dist/utils/help-json.js +85 -0
  106. package/dist/utils/resolve.d.ts +12 -0
  107. package/dist/utils/resolve.js +52 -0
  108. package/dist/utils/setup-state.d.ts +3 -0
  109. package/dist/utils/setup-state.js +19 -0
  110. package/dist/utils/sqlite.d.ts +7 -0
  111. package/dist/utils/sqlite.js +24 -0
  112. package/dist/utils/which.d.ts +6 -0
  113. package/dist/utils/which.js +18 -0
  114. package/package.json +7 -4
  115. package/dist/agents/copilot-cli.d.ts +0 -10
  116. package/dist/agents/copilot-cli.js +0 -46
  117. package/dist/agents/copilot-vscode.d.ts +0 -10
  118. package/dist/agents/copilot-vscode.js +0 -37
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs';
1
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir, platform } from 'os';
4
4
  import { buildSkillMd } from './types.js';
@@ -57,6 +57,19 @@ function getPluginJson() {
57
57
  author: { name: 'Runwork' },
58
58
  };
59
59
  }
60
+ function getMarketplaceJson() {
61
+ return {
62
+ name: 'runwork',
63
+ owner: { name: 'Runwork' },
64
+ plugins: [
65
+ {
66
+ name: PLUGIN_NAME,
67
+ source: `./plugins/${PLUGIN_NAME}`,
68
+ description: 'Skills and tools from your Runwork workspace',
69
+ },
70
+ ],
71
+ };
72
+ }
60
73
  export class ClaudeDesktopAdapter {
61
74
  name = 'Claude Desktop (Cowork)';
62
75
  slug = 'claude-desktop';
@@ -87,6 +100,25 @@ export class ClaudeDesktopAdapter {
87
100
  };
88
101
  }
89
102
  mergeJsonMcpServers(getMcpConfigPath(), entries, 'mcpServers');
103
+ // Also write .mcp.json inside the Cowork plugin directory
104
+ const pluginsDir = findCoworkPluginsDir();
105
+ if (pluginsDir) {
106
+ const cacheDir = join(pluginsDir, 'cache', 'runwork', PLUGIN_NAME, PLUGIN_VERSION);
107
+ const marketDir = join(pluginsDir, 'marketplaces', 'runwork', 'plugins', PLUGIN_NAME);
108
+ const pluginEntries = {};
109
+ for (const s of servers) {
110
+ pluginEntries[s.name] = {
111
+ type: 'http',
112
+ url: s.url,
113
+ ...(s.headers ? { headers: s.headers } : {}),
114
+ ...(s.description ? { description: s.description } : {}),
115
+ };
116
+ }
117
+ for (const dir of [cacheDir, marketDir]) {
118
+ mkdirSync(dir, { recursive: true });
119
+ writeJsonConfig(join(dir, '.mcp.json'), pluginEntries);
120
+ }
121
+ }
90
122
  }
91
123
  async writeSkills(skills, _scope) {
92
124
  const pluginsDir = findCoworkPluginsDir();
@@ -96,7 +128,8 @@ export class ClaudeDesktopAdapter {
96
128
  }
97
129
  // Write to both cache (installed copy) and marketplaces (source)
98
130
  const cacheDir = join(pluginsDir, 'cache', 'runwork', PLUGIN_NAME, PLUGIN_VERSION);
99
- const marketDir = join(pluginsDir, 'marketplaces', 'runwork', PLUGIN_NAME);
131
+ const marketRoot = join(pluginsDir, 'marketplaces', 'runwork');
132
+ const marketDir = join(marketRoot, 'plugins', PLUGIN_NAME);
100
133
  // Create plugin.json in both locations
101
134
  for (const dir of [cacheDir, marketDir]) {
102
135
  const pluginJsonDir = join(dir, '.claude-plugin');
@@ -126,16 +159,225 @@ export class ClaudeDesktopAdapter {
126
159
  lastUpdated: new Date().toISOString(),
127
160
  }];
128
161
  writeJsonConfig(installedPath, installed);
162
+ // Write marketplace.json catalog
163
+ const marketCatalogDir = join(marketRoot, '.claude-plugin');
164
+ mkdirSync(marketCatalogDir, { recursive: true });
165
+ writeFileSync(join(marketCatalogDir, 'marketplace.json'), JSON.stringify(getMarketplaceJson(), null, 2));
129
166
  // Register in known_marketplaces.json
130
167
  const marketplacesPath = join(pluginsDir, 'known_marketplaces.json');
131
168
  const marketplaces = readJsonConfig(marketplacesPath);
132
- if (!marketplaces['runwork']) {
133
- marketplaces['runwork'] = {
134
- source: { source: 'local', path: join(pluginsDir, 'marketplaces', 'runwork') },
135
- installLocation: join(pluginsDir, 'marketplaces', 'runwork'),
136
- lastUpdated: new Date().toISOString(),
169
+ marketplaces['runwork'] = {
170
+ source: { source: 'directory', path: marketRoot },
171
+ installLocation: marketRoot,
172
+ lastUpdated: new Date().toISOString(),
173
+ };
174
+ writeJsonConfig(marketplacesPath, marketplaces);
175
+ }
176
+ async writeInstructionHint(_hint, _scope) {
177
+ // Claude Desktop/Cowork uses project instructions within the app UI, not files.
178
+ // The plugin system handles skill discovery.
179
+ }
180
+ async writeTeamInstructions(instructions, _scope) {
181
+ // Write team instructions as a skill in the CoWork plugin system.
182
+ // This makes them available to Claude Desktop sessions via the plugin.
183
+ const pluginsDir = findCoworkPluginsDir();
184
+ if (!pluginsDir)
185
+ return;
186
+ const skillContent = `---\nname: runwork-team-instructions\ndescription: Team instructions from your Runwork workspace. Always follow these guidelines.\n---\n\n${instructions}`;
187
+ // Write to both cache and marketplace locations
188
+ const cacheDir = join(pluginsDir, 'cache', 'runwork', PLUGIN_NAME, PLUGIN_VERSION, 'skills', 'runwork-team-instructions');
189
+ const marketDir = join(pluginsDir, 'marketplaces', 'runwork', 'plugins', PLUGIN_NAME, 'skills', 'runwork-team-instructions');
190
+ for (const dir of [cacheDir, marketDir]) {
191
+ mkdirSync(dir, { recursive: true });
192
+ writeFileSync(join(dir, 'SKILL.md'), skillContent);
193
+ }
194
+ }
195
+ // ── Agent config (limited to preferences) ──────────────────────────
196
+ async writeAgentConfig(config, _scope) {
197
+ const configPath = getMcpConfigPath();
198
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
199
+ let desktopConfig = {};
200
+ if (existsSync(configPath)) {
201
+ try {
202
+ desktopConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
203
+ }
204
+ catch { /* start fresh */ }
205
+ }
206
+ if (!desktopConfig.preferences)
207
+ desktopConfig.preferences = {};
208
+ const prefs = desktopConfig.preferences;
209
+ if (config.permissionRules) {
210
+ if (config.permissionRules.allow?.includes('web_search')) {
211
+ prefs.coworkWebSearchEnabled = true;
212
+ }
213
+ if (config.permissionRules.deny?.includes('web_search')) {
214
+ prefs.coworkWebSearchEnabled = false;
215
+ }
216
+ if (config.permissionRules.allow?.includes('scheduled_tasks')) {
217
+ prefs.coworkScheduledTasksEnabled = true;
218
+ prefs.ccdScheduledTasksEnabled = true;
219
+ }
220
+ if (config.permissionRules.deny?.includes('scheduled_tasks')) {
221
+ prefs.coworkScheduledTasksEnabled = false;
222
+ prefs.ccdScheduledTasksEnabled = false;
223
+ }
224
+ }
225
+ writeFileSync(configPath, JSON.stringify(desktopConfig, null, 2));
226
+ }
227
+ // ── Usage stats (passive, best-effort) ─────────────────────────────
228
+ async readUsageStats(lastSyncAt) {
229
+ try {
230
+ const os = platform();
231
+ let claudeAppDir;
232
+ if (os === 'darwin') {
233
+ claudeAppDir = join(homedir(), 'Library', 'Application Support', 'Claude');
234
+ }
235
+ else if (os === 'win32') {
236
+ claudeAppDir = join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'Claude');
237
+ }
238
+ else {
239
+ claudeAppDir = join(homedir(), '.config', 'Claude');
240
+ }
241
+ if (!existsSync(claudeAppDir))
242
+ return null;
243
+ const sinceMs = lastSyncAt ? new Date(lastSyncAt).getTime() : 0;
244
+ let sessionCount = 0;
245
+ let latestActivity = 0;
246
+ const modelsUsed = new Set();
247
+ let maxMcpTools = 0;
248
+ // Read agent mode session JSON files
249
+ // Path: local-agent-mode-sessions/{orgId}/{userId}/*.json
250
+ const agentSessionsDir = join(claudeAppDir, 'local-agent-mode-sessions');
251
+ if (existsSync(agentSessionsDir)) {
252
+ this.walkSessionDirs(agentSessionsDir, sinceMs, (session) => {
253
+ const sessionTime = session.lastActivityAt ?? session.createdAt ?? 0;
254
+ if (sessionTime > sinceMs)
255
+ sessionCount++;
256
+ if (sessionTime > latestActivity)
257
+ latestActivity = sessionTime;
258
+ if (session.model)
259
+ modelsUsed.add(session.model);
260
+ if (session.enabledMcpTools && typeof session.enabledMcpTools === 'object') {
261
+ const toolCount = Object.keys(session.enabledMcpTools).length;
262
+ if (toolCount > maxMcpTools)
263
+ maxMcpTools = toolCount;
264
+ }
265
+ });
266
+ }
267
+ // Also check code sessions launched from Desktop
268
+ const codeSessionsDir = join(claudeAppDir, 'claude-code-sessions');
269
+ if (existsSync(codeSessionsDir)) {
270
+ this.walkSessionDirs(codeSessionsDir, sinceMs, (session) => {
271
+ const sessionTime = session.lastActivityAt ?? session.createdAt ?? 0;
272
+ if (sessionTime > sinceMs)
273
+ sessionCount++;
274
+ if (sessionTime > latestActivity)
275
+ latestActivity = sessionTime;
276
+ if (session.model)
277
+ modelsUsed.add(session.model);
278
+ });
279
+ }
280
+ // Check scheduled tasks
281
+ let scheduledTaskRuns = 0;
282
+ const scheduledTasksPath = join(claudeAppDir, 'scheduled-tasks.json');
283
+ if (existsSync(scheduledTasksPath)) {
284
+ try {
285
+ const raw = readFileSync(scheduledTasksPath, 'utf-8');
286
+ const parsed = JSON.parse(raw);
287
+ const tasks = Array.isArray(parsed) ? parsed : Object.values(parsed);
288
+ for (const task of tasks) {
289
+ const t = task;
290
+ if (t.lastRunAt && new Date(t.lastRunAt).getTime() > sinceMs) {
291
+ scheduledTaskRuns++;
292
+ }
293
+ }
294
+ }
295
+ catch { /* best-effort */ }
296
+ }
297
+ if (sessionCount === 0 && latestActivity <= sinceMs && scheduledTaskRuns === 0)
298
+ return null;
299
+ return {
300
+ hasNewActivity: sessionCount > 0 || scheduledTaskRuns > 0,
301
+ sessionCount,
302
+ messageCount: 0,
303
+ toolCallCount: 0,
304
+ tokensUsed: 0,
305
+ aiLinesAdded: 0,
306
+ aiLinesRemoved: 0,
307
+ lastActiveAt: latestActivity > 0 ? new Date(latestActivity).toISOString() : null,
308
+ modelsUsed: Array.from(modelsUsed),
309
+ mcpToolCount: maxMcpTools > 0 ? maxMcpTools : undefined,
310
+ extra: scheduledTaskRuns > 0 ? { scheduledTaskRuns } : undefined,
137
311
  };
138
- writeJsonConfig(marketplacesPath, marketplaces);
139
312
  }
313
+ catch {
314
+ return null;
315
+ }
316
+ }
317
+ async readVersion() {
318
+ try {
319
+ const os = platform();
320
+ let claudeAppDir;
321
+ if (os === 'darwin') {
322
+ claudeAppDir = join(homedir(), 'Library', 'Application Support', 'Claude');
323
+ }
324
+ else if (os === 'win32') {
325
+ claudeAppDir = join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'Claude');
326
+ }
327
+ else {
328
+ claudeAppDir = join(homedir(), '.config', 'Claude');
329
+ }
330
+ const versionPath = join(claudeAppDir, 'claude-code', 'sdk-version');
331
+ if (existsSync(versionPath)) {
332
+ return readFileSync(versionPath, 'utf-8').trim();
333
+ }
334
+ return null;
335
+ }
336
+ catch {
337
+ return null;
338
+ }
339
+ }
340
+ /**
341
+ * Walk the org/user/session directory tree and invoke callback for each session JSON.
342
+ */
343
+ walkSessionDirs(baseDir, _sinceMs, onSession) {
344
+ try {
345
+ for (const orgDir of readdirSync(baseDir)) {
346
+ if (orgDir.startsWith('.') || orgDir === 'skills-plugin')
347
+ continue;
348
+ const orgPath = join(baseDir, orgDir);
349
+ try {
350
+ if (!statSync(orgPath).isDirectory())
351
+ continue;
352
+ }
353
+ catch {
354
+ continue;
355
+ }
356
+ for (const userDir of readdirSync(orgPath)) {
357
+ if (userDir.startsWith('.'))
358
+ continue;
359
+ const userPath = join(orgPath, userDir);
360
+ try {
361
+ if (!statSync(userPath).isDirectory())
362
+ continue;
363
+ }
364
+ catch {
365
+ continue;
366
+ }
367
+ for (const file of readdirSync(userPath)) {
368
+ if (!file.endsWith('.json'))
369
+ continue;
370
+ try {
371
+ const session = JSON.parse(readFileSync(join(userPath, file), 'utf-8'));
372
+ onSession(session);
373
+ }
374
+ catch {
375
+ continue;
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+ catch { /* best-effort */ }
140
382
  }
141
383
  }
@@ -0,0 +1,13 @@
1
+ import type { AgentAdapter, AgentConfigOverride, McpServerEntry, SkillFile } from './types.js';
2
+ export declare class ClineAdapter implements AgentAdapter {
3
+ name: string;
4
+ slug: string;
5
+ detect(): Promise<boolean>;
6
+ supportsMcpScope(scope: 'project' | 'user'): boolean;
7
+ supportsSkills(): boolean;
8
+ writeMcpServers(servers: McpServerEntry[], _scope: 'project' | 'user'): Promise<void>;
9
+ writeSkills(skills: SkillFile[], scope: 'project' | 'user'): Promise<void>;
10
+ writeInstructionHint(hint: string, scope: 'project' | 'user'): Promise<void>;
11
+ writeTeamInstructions(instructions: string, scope: 'project' | 'user'): Promise<void>;
12
+ writeAgentConfig(config: AgentConfigOverride, scope: 'project' | 'user'): Promise<void>;
13
+ }
@@ -0,0 +1,88 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { whichBinary } from '../utils/which.js';
5
+ import { buildSkillMd } from './types.js';
6
+ import { mergeJsonMcpServers } from './utils/json-config.js';
7
+ export class ClineAdapter {
8
+ name = 'Cline';
9
+ slug = 'cline';
10
+ async detect() {
11
+ return !!whichBinary('cline');
12
+ }
13
+ supportsMcpScope(scope) {
14
+ return scope === 'user';
15
+ }
16
+ supportsSkills() {
17
+ return true;
18
+ }
19
+ async writeMcpServers(servers, _scope) {
20
+ const configPath = join(homedir(), '.cline', 'data', 'settings', 'cline_mcp_settings.json');
21
+ const entries = {};
22
+ for (const s of servers) {
23
+ entries[s.name] = {
24
+ url: s.url,
25
+ ...(s.headers ? { headers: s.headers } : {}),
26
+ };
27
+ }
28
+ mergeJsonMcpServers(configPath, entries, 'mcpServers');
29
+ }
30
+ async writeSkills(skills, scope) {
31
+ if (scope === 'user')
32
+ return;
33
+ const rulesDir = join(process.cwd(), '.clinerules');
34
+ mkdirSync(rulesDir, { recursive: true });
35
+ for (const skill of skills) {
36
+ writeFileSync(join(rulesDir, `${skill.filename}.md`), buildSkillMd(skill));
37
+ }
38
+ }
39
+ async writeInstructionHint(hint, scope) {
40
+ if (scope === 'user')
41
+ return;
42
+ const filePath = join(process.cwd(), '.clinerules', 'runwork.md');
43
+ mkdirSync(join(filePath, '..'), { recursive: true });
44
+ writeFileSync(filePath, hint);
45
+ }
46
+ async writeTeamInstructions(instructions, scope) {
47
+ if (scope === 'user')
48
+ return;
49
+ const filePath = join(process.cwd(), '.clinerules', 'runwork-team.md');
50
+ mkdirSync(join(filePath, '..'), { recursive: true });
51
+ writeFileSync(filePath, instructions);
52
+ }
53
+ // ── Agent config (globalState.json) ────────────────────────────────
54
+ async writeAgentConfig(config, scope) {
55
+ if (scope !== 'user')
56
+ return;
57
+ const globalStatePath = join(homedir(), '.cline', 'data', 'globalState.json');
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ let state = {};
60
+ if (existsSync(globalStatePath)) {
61
+ try {
62
+ state = JSON.parse(readFileSync(globalStatePath, 'utf-8'));
63
+ }
64
+ catch { /* start fresh */ }
65
+ }
66
+ if (config.modelPreference) {
67
+ state.apiModelId = config.modelPreference;
68
+ }
69
+ if (config.permissionRules) {
70
+ if (!state.autoApprovalSettings)
71
+ state.autoApprovalSettings = {};
72
+ if (!state.autoApprovalSettings.actions)
73
+ state.autoApprovalSettings.actions = {};
74
+ if (config.permissionRules.defaultMode === 'auto') {
75
+ state.autoApprovalSettings.enabled = true;
76
+ state.autoApprovalSettings.actions.readFiles = true;
77
+ state.autoApprovalSettings.actions.editFiles = true;
78
+ state.autoApprovalSettings.actions.executeSafeCommands = true;
79
+ state.autoApprovalSettings.actions.useMcp = true;
80
+ }
81
+ else if (config.permissionRules.defaultMode === 'default') {
82
+ state.autoApprovalSettings.enabled = false;
83
+ }
84
+ }
85
+ mkdirSync(join(globalStatePath, '..'), { recursive: true });
86
+ writeFileSync(globalStatePath, JSON.stringify(state, null, 2));
87
+ }
88
+ }
@@ -1,10 +1,15 @@
1
- import type { AgentAdapter, McpServerEntry, SkillFile } from './types.js';
1
+ import type { AgentAdapter, AgentConfigOverride, AgentUsageStats, McpServerEntry, SkillFile } from './types.js';
2
2
  export declare class CodexAdapter implements AgentAdapter {
3
3
  name: string;
4
4
  slug: string;
5
5
  detect(): Promise<boolean>;
6
6
  supportsMcpScope(_scope: 'project' | 'user'): boolean;
7
7
  supportsSkills(): boolean;
8
- writeMcpServers(servers: McpServerEntry[], scope: 'project' | 'user'): Promise<void>;
8
+ writeMcpServers(servers: McpServerEntry[], _scope: 'project' | 'user'): Promise<void>;
9
9
  writeSkills(skills: SkillFile[], scope: 'project' | 'user'): Promise<void>;
10
+ writeInstructionHint(hint: string, scope: 'project' | 'user'): Promise<void>;
11
+ writeTeamInstructions(instructions: string, scope: 'project' | 'user'): Promise<void>;
12
+ writeAgentConfig(config: AgentConfigOverride, scope: 'project' | 'user'): Promise<void>;
13
+ readUsageStats(lastSyncAt: string | null): Promise<AgentUsageStats | null>;
14
+ readVersion(): Promise<string | null>;
10
15
  }
@@ -1,20 +1,16 @@
1
- import { mkdirSync, writeFileSync } from 'fs';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
- import { execFileSync } from 'child_process';
4
+ import { parse, stringify } from 'smol-toml';
5
5
  import { buildSkillMd } from './types.js';
6
- import { mergeTomlMcpServers } from './utils/toml-config.js';
6
+ import { writeHintToFile, writeTeamInstructionsToFile } from './utils/instruction-hint.js';
7
+ import { querySqlite } from '../utils/sqlite.js';
8
+ import { whichBinary } from '../utils/which.js';
7
9
  export class CodexAdapter {
8
- name = 'Codex CLI';
10
+ name = 'Codex';
9
11
  slug = 'codex';
10
12
  async detect() {
11
- try {
12
- execFileSync('which', ['codex'], { stdio: 'pipe' });
13
- return true;
14
- }
15
- catch {
16
- return false;
17
- }
13
+ return !!whichBinary('codex');
18
14
  }
19
15
  supportsMcpScope(_scope) {
20
16
  return true;
@@ -22,15 +18,31 @@ export class CodexAdapter {
22
18
  supportsSkills() {
23
19
  return true;
24
20
  }
25
- async writeMcpServers(servers, scope) {
26
- const filePath = scope === 'project'
27
- ? join(process.cwd(), '.codex', 'config.toml')
28
- : join(homedir(), '.codex', 'config.toml');
29
- const entries = {};
21
+ async writeMcpServers(servers, _scope) {
22
+ // Codex uses config.toml with [mcp_servers.*] sections
23
+ const configPath = join(homedir(), '.codex', 'config.toml');
24
+ let parsed = {};
25
+ if (existsSync(configPath)) {
26
+ parsed = parse(readFileSync(configPath, 'utf-8'));
27
+ }
28
+ if (!parsed.mcp_servers || typeof parsed.mcp_servers !== 'object') {
29
+ parsed.mcp_servers = {};
30
+ }
31
+ const mcpServers = parsed.mcp_servers;
32
+ // Remove existing runwork-* entries
33
+ for (const key of Object.keys(mcpServers)) {
34
+ if (key.startsWith('runwork-'))
35
+ delete mcpServers[key];
36
+ }
37
+ // Add new entries
30
38
  for (const s of servers) {
31
- entries[s.name] = { url: s.url, bearerTokenEnvVar: 'RUNWORK_API_KEY' };
39
+ mcpServers[s.name] = {
40
+ url: s.url,
41
+ ...(s.headers?.Authorization ? { bearer_token_env_var: 'RUNWORK_MCP_TOKEN' } : {}),
42
+ };
32
43
  }
33
- mergeTomlMcpServers(filePath, entries);
44
+ mkdirSync(join(configPath, '..'), { recursive: true });
45
+ writeFileSync(configPath, stringify(parsed));
34
46
  }
35
47
  async writeSkills(skills, scope) {
36
48
  const baseDir = scope === 'project'
@@ -42,4 +54,104 @@ export class CodexAdapter {
42
54
  writeFileSync(join(skillDir, 'SKILL.md'), buildSkillMd(skill));
43
55
  }
44
56
  }
57
+ async writeInstructionHint(hint, scope) {
58
+ const filePath = scope === 'project'
59
+ ? join(process.cwd(), 'AGENTS.md')
60
+ : join(homedir(), '.codex', 'instructions.md');
61
+ writeHintToFile(filePath, hint);
62
+ }
63
+ async writeTeamInstructions(instructions, scope) {
64
+ const filePath = scope === 'project'
65
+ ? join(process.cwd(), 'AGENTS.md')
66
+ : join(homedir(), '.codex', 'instructions.md');
67
+ writeTeamInstructionsToFile(filePath, instructions);
68
+ }
69
+ // ── Agent config (TOML) ───────────────────────────────────────────
70
+ async writeAgentConfig(config, scope) {
71
+ const configPath = scope === 'project'
72
+ ? join(process.cwd(), '.codex', 'config.toml')
73
+ : join(homedir(), '.codex', 'config.toml');
74
+ let parsed = {};
75
+ if (existsSync(configPath)) {
76
+ parsed = parse(readFileSync(configPath, 'utf-8'));
77
+ }
78
+ if (config.modelPreference) {
79
+ parsed.model = config.modelPreference;
80
+ }
81
+ if (config.permissionRules?.defaultMode) {
82
+ const modeMap = {
83
+ auto: 'never',
84
+ default: 'on-request',
85
+ plan: 'untrusted',
86
+ };
87
+ parsed.approval_policy = modeMap[config.permissionRules.defaultMode] ?? config.permissionRules.defaultMode;
88
+ }
89
+ mkdirSync(join(configPath, '..'), { recursive: true });
90
+ writeFileSync(configPath, stringify(parsed));
91
+ }
92
+ // ── Usage stats (passive, best-effort) ─────────────────────────────
93
+ async readUsageStats(lastSyncAt) {
94
+ try {
95
+ const codexDir = join(homedir(), '.codex');
96
+ if (!existsSync(codexDir))
97
+ return null;
98
+ const sinceMs = lastSyncAt ? new Date(lastSyncAt).getTime() : 0;
99
+ const sinceSec = Math.floor(sinceMs / 1000);
100
+ const dbPath = join(codexDir, 'state_5.sqlite');
101
+ if (!existsSync(dbPath))
102
+ return null;
103
+ const countResult = querySqlite(dbPath, `SELECT count(*) FROM threads WHERE created_at > ${sinceSec}`);
104
+ const sessionCount = parseInt(countResult) || 0;
105
+ const tokensResult = querySqlite(dbPath, `SELECT coalesce(sum(tokens_used), 0) FROM threads WHERE created_at > ${sinceSec}`);
106
+ const tokensUsed = parseInt(tokensResult) || 0;
107
+ const latestResult = querySqlite(dbPath, `SELECT max(updated_at) FROM threads`);
108
+ const latestSec = parseInt(latestResult) || 0;
109
+ const versionsResult = querySqlite(dbPath, `SELECT DISTINCT cli_version FROM threads WHERE cli_version IS NOT NULL ORDER BY created_at DESC LIMIT 3`);
110
+ const versions = versionsResult ? versionsResult.split(/[\r\n]+/).filter(Boolean) : [];
111
+ let messageCount = 0;
112
+ const historyPath = join(codexDir, 'history.jsonl');
113
+ if (existsSync(historyPath)) {
114
+ const content = readFileSync(historyPath, 'utf-8').trim();
115
+ if (content) {
116
+ for (const line of content.split(/[\r\n]+/)) {
117
+ try {
118
+ const entry = JSON.parse(line);
119
+ if (entry.ts && entry.ts > sinceSec)
120
+ messageCount++;
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ if (sessionCount === 0 && (latestSec * 1000) <= sinceMs)
129
+ return null;
130
+ return {
131
+ hasNewActivity: sessionCount > 0,
132
+ sessionCount,
133
+ messageCount,
134
+ toolCallCount: 0,
135
+ tokensUsed,
136
+ aiLinesAdded: 0,
137
+ aiLinesRemoved: 0,
138
+ lastActiveAt: latestSec > 0 ? new Date(latestSec * 1000).toISOString() : null,
139
+ extra: versions.length > 0 ? { cliVersions: versions } : undefined,
140
+ };
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
146
+ async readVersion() {
147
+ try {
148
+ const versionPath = join(homedir(), '.codex', 'version.json');
149
+ if (existsSync(versionPath)) {
150
+ const data = JSON.parse(readFileSync(versionPath, 'utf-8'));
151
+ return data.latest_version ?? null;
152
+ }
153
+ }
154
+ catch { /* best-effort */ }
155
+ return null;
156
+ }
45
157
  }
@@ -1,4 +1,4 @@
1
- import type { AgentAdapter, McpServerEntry, SkillFile } from './types.js';
1
+ import type { AgentAdapter, AgentConfigOverride, AgentUsageStats, McpServerEntry, SkillFile } from './types.js';
2
2
  export declare class CursorAdapter implements AgentAdapter {
3
3
  name: string;
4
4
  slug: string;
@@ -7,4 +7,9 @@ export declare class CursorAdapter implements AgentAdapter {
7
7
  supportsSkills(): boolean;
8
8
  writeMcpServers(servers: McpServerEntry[], scope: 'project' | 'user'): Promise<void>;
9
9
  writeSkills(skills: SkillFile[], scope: 'project' | 'user'): Promise<void>;
10
+ writeInstructionHint(hint: string, scope: 'project' | 'user'): Promise<void>;
11
+ writeTeamInstructions(instructions: string, scope: 'project' | 'user'): Promise<void>;
12
+ writeAgentConfig(config: AgentConfigOverride, scope: 'project' | 'user'): Promise<void>;
13
+ readUsageStats(lastSyncAt: string | null): Promise<AgentUsageStats | null>;
14
+ readVersion(): Promise<string | null>;
10
15
  }