runwork 0.6.2 → 0.8.2
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/__tests__/install-scripts.test.d.ts +1 -0
- package/dist/__tests__/install-scripts.test.js +247 -0
- package/dist/agents/__tests__/claude-code-plugin.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-code-plugin.test.js +47 -0
- package/dist/agents/__tests__/claude-code-stats.test.js +143 -78
- package/dist/agents/__tests__/claude-desktop-plugin-tree.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-plugin-tree.test.js +161 -0
- package/dist/agents/__tests__/claude-desktop-plugin.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-plugin.test.js +117 -0
- package/dist/agents/__tests__/claude-desktop-rpm.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-rpm.test.js +126 -0
- package/dist/agents/__tests__/codex-instructions.test.d.ts +1 -0
- package/dist/agents/__tests__/codex-instructions.test.js +57 -0
- package/dist/agents/__tests__/codex-stats.test.js +9 -0
- package/dist/agents/__tests__/graceful-degradation.test.js +3 -3
- package/dist/agents/__tests__/intro-skill.test.js +11 -2
- package/dist/agents/__tests__/skill-slug.test.d.ts +1 -0
- package/dist/agents/__tests__/skill-slug.test.js +77 -0
- package/dist/agents/claude-code.d.ts +3 -1
- package/dist/agents/claude-code.js +233 -58
- package/dist/agents/claude-desktop-plugin-tree.d.ts +44 -0
- package/dist/agents/claude-desktop-plugin-tree.js +92 -0
- package/dist/agents/claude-desktop.d.ts +20 -1
- package/dist/agents/claude-desktop.js +238 -61
- package/dist/agents/cline.d.ts +3 -1
- package/dist/agents/cline.js +44 -2
- package/dist/agents/codex.d.ts +3 -1
- package/dist/agents/codex.js +85 -14
- package/dist/agents/cursor.d.ts +12 -1
- package/dist/agents/cursor.js +98 -25
- package/dist/agents/gemini.d.ts +3 -1
- package/dist/agents/gemini.js +42 -4
- package/dist/agents/generic-adapter.d.ts +3 -1
- package/dist/agents/generic-adapter.js +59 -4
- package/dist/agents/intro-skill.js +77 -31
- package/dist/agents/types.d.ts +42 -2
- package/dist/agents/types.js +61 -6
- package/dist/agents/utils/instruction-hint.d.ts +10 -0
- package/dist/agents/utils/instruction-hint.js +46 -0
- package/dist/agents/utils/json-config.d.ts +6 -1
- package/dist/agents/utils/json-config.js +31 -4
- package/dist/agents/windsurf.d.ts +3 -1
- package/dist/agents/windsurf.js +37 -2
- package/dist/commands/__tests__/sync-telemetry.test.d.ts +1 -0
- package/dist/commands/__tests__/sync-telemetry.test.js +281 -0
- package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
- package/dist/commands/__tests__/upgrade.test.js +174 -0
- package/dist/commands/build-plugin.d.ts +2 -0
- package/dist/commands/build-plugin.js +135 -0
- package/dist/commands/init.d.ts +8 -2
- package/dist/commands/init.js +19 -7
- package/dist/commands/sync-telemetry.d.ts +33 -0
- package/dist/commands/sync-telemetry.js +186 -0
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +84 -118
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +145 -0
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/commands/upgrade.js +67 -16
- package/dist/generated/bundled-types.js +36 -36
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/health/__tests__/cli-distribution-checks.test.d.ts +1 -0
- package/dist/health/__tests__/cli-distribution-checks.test.js +199 -0
- package/dist/health/checks.d.ts +2 -0
- package/dist/health/checks.js +131 -18
- package/dist/health/runner.js +6 -2
- package/dist/index.js +4 -0
- package/dist/sync/executor.d.ts +2 -0
- package/dist/sync/executor.js +8 -4
- package/dist/types.d.ts +2 -0
- package/dist/ui/banner.js +15 -6
- package/dist/utils/__tests__/sqlite.test.js +84 -72
- package/dist/utils/sqlite-adapter.d.ts +48 -0
- package/dist/utils/sqlite-adapter.js +147 -0
- package/dist/utils/sqlite.d.ts +12 -5
- package/dist/utils/sqlite.js +12 -22
- package/dist/utils/zip.d.ts +6 -0
- package/dist/utils/zip.js +28 -3
- package/package.json +2 -4
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { homedir, platform } from 'os';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { homedir, platform, tmpdir } from 'os';
|
|
4
|
+
import { mergeJsonMcpServers, readJsonConfig, writeJsonConfig, removeRunworkMcpServers } from './utils/json-config.js';
|
|
5
|
+
import { createZipFromDir } from '../utils/zip.js';
|
|
6
|
+
import { writePluginMcpConfig, writePluginMetadata, writePluginSkills, writePluginTeamInstructions, writePluginTree, } from './claude-desktop-plugin-tree.js';
|
|
6
7
|
const PLUGIN_NAME = 'runwork';
|
|
7
8
|
const PLUGIN_VERSION = '1.0.0';
|
|
9
|
+
const PLUGIN_DESCRIPTION = 'Skills and tools from your Runwork workspace';
|
|
10
|
+
const PLUGIN_AUTHOR_NAME = 'Runwork';
|
|
11
|
+
function getPluginMetadata() {
|
|
12
|
+
return {
|
|
13
|
+
pluginName: PLUGIN_NAME,
|
|
14
|
+
pluginVersion: PLUGIN_VERSION,
|
|
15
|
+
description: PLUGIN_DESCRIPTION,
|
|
16
|
+
authorName: PLUGIN_AUTHOR_NAME,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
8
19
|
function getMcpConfigPath() {
|
|
9
20
|
const os = platform();
|
|
10
21
|
if (os === 'darwin') {
|
|
@@ -26,50 +37,101 @@ function getCoworkBaseDir() {
|
|
|
26
37
|
return join(homedir(), '.config', 'Claude', 'local-agent-mode-sessions');
|
|
27
38
|
}
|
|
28
39
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
40
|
+
* Walk all `local-agent-mode-sessions/<session>/<org>/` directories and
|
|
41
|
+
* invoke `visit` for each. Stops and returns the first non-null result.
|
|
31
42
|
*/
|
|
32
|
-
function
|
|
43
|
+
function walkOrgDirs(visit) {
|
|
33
44
|
const baseDir = getCoworkBaseDir();
|
|
34
45
|
if (!existsSync(baseDir))
|
|
35
46
|
return null;
|
|
36
47
|
try {
|
|
37
|
-
const
|
|
38
|
-
for (const sessionId of
|
|
48
|
+
const sessionDirs = readdirSync(baseDir).filter(d => !d.startsWith('.') && d !== 'skills-plugin');
|
|
49
|
+
for (const sessionId of sessionDirs) {
|
|
39
50
|
const sessionPath = join(baseDir, sessionId);
|
|
40
|
-
|
|
51
|
+
let orgDirs;
|
|
52
|
+
try {
|
|
53
|
+
orgDirs = readdirSync(sessionPath).filter(d => !d.startsWith('.'));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
41
58
|
for (const orgId of orgDirs) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
const orgDir = join(sessionPath, orgId);
|
|
60
|
+
const result = visit(orgDir);
|
|
61
|
+
if (result !== null)
|
|
62
|
+
return result;
|
|
46
63
|
}
|
|
47
64
|
}
|
|
48
65
|
}
|
|
49
|
-
catch {
|
|
66
|
+
catch {
|
|
67
|
+
/* ignore */
|
|
68
|
+
}
|
|
50
69
|
return null;
|
|
51
70
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Find the cowork_plugins directory by discovering the session/org ID structure.
|
|
73
|
+
* Path: local-agent-mode-sessions/<session-id>/<org-id>/cowork_plugins/
|
|
74
|
+
*/
|
|
75
|
+
function findCoworkPluginsDir() {
|
|
76
|
+
return walkOrgDirs((orgDir) => {
|
|
77
|
+
const pluginsDir = join(orgDir, 'cowork_plugins');
|
|
78
|
+
return existsSync(join(pluginsDir, 'installed_plugins.json')) ? pluginsDir : null;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Look for a plugin named `pluginName` in any `rpm/manifest.json`. These
|
|
83
|
+
* entries are created by Claude Desktop when the user uploads a plugin zip
|
|
84
|
+
* through Customize > Create plugin > Upload plugin. If found, sync should
|
|
85
|
+
* update the plugin content files inside the rpm plugin directory instead
|
|
86
|
+
* of writing to the legacy `cowork_plugins/` layout.
|
|
87
|
+
*
|
|
88
|
+
* We never modify `rpm/manifest.json` itself — Claude Desktop owns that
|
|
89
|
+
* registry and assigns the plugin id we read from it here.
|
|
90
|
+
*/
|
|
91
|
+
export function findRpmPluginByName(pluginName) {
|
|
92
|
+
return walkOrgDirs((orgDir) => {
|
|
93
|
+
const manifestPath = join(orgDir, 'rpm', 'manifest.json');
|
|
94
|
+
if (!existsSync(manifestPath))
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
98
|
+
const entry = manifest.plugins?.find((p) => p.name === pluginName);
|
|
99
|
+
if (!entry?.id)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
pluginPath: join(orgDir, 'rpm', entry.id),
|
|
103
|
+
orgDir,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
59
110
|
}
|
|
60
111
|
function getMarketplaceJson() {
|
|
61
112
|
return {
|
|
62
113
|
name: 'runwork',
|
|
63
|
-
owner: { name:
|
|
114
|
+
owner: { name: PLUGIN_AUTHOR_NAME },
|
|
64
115
|
plugins: [
|
|
65
116
|
{
|
|
66
117
|
name: PLUGIN_NAME,
|
|
67
118
|
source: `./plugins/${PLUGIN_NAME}`,
|
|
68
|
-
description:
|
|
119
|
+
description: PLUGIN_DESCRIPTION,
|
|
69
120
|
},
|
|
70
121
|
],
|
|
71
122
|
};
|
|
72
123
|
}
|
|
124
|
+
function getCoworkSettingsPath(pluginsDir) {
|
|
125
|
+
return join(dirname(pluginsDir), 'cowork_settings.json');
|
|
126
|
+
}
|
|
127
|
+
function setCoworkPluginEnabled(pluginsDir, enabled) {
|
|
128
|
+
const settingsPath = getCoworkSettingsPath(pluginsDir);
|
|
129
|
+
const settings = readJsonConfig(settingsPath);
|
|
130
|
+
const enabledPlugins = (settings.enabledPlugins ?? {});
|
|
131
|
+
enabledPlugins[`${PLUGIN_NAME}@runwork`] = enabled;
|
|
132
|
+
settings.enabledPlugins = enabledPlugins;
|
|
133
|
+
writeJsonConfig(settingsPath, settings);
|
|
134
|
+
}
|
|
73
135
|
export class ClaudeDesktopAdapter {
|
|
74
136
|
name = 'Claude Desktop (Cowork)';
|
|
75
137
|
slug = 'claude-desktop';
|
|
@@ -86,8 +148,14 @@ export class ClaudeDesktopAdapter {
|
|
|
86
148
|
return true;
|
|
87
149
|
}
|
|
88
150
|
async writeMcpServers(servers, _scope) {
|
|
89
|
-
// Claude Desktop
|
|
90
|
-
//
|
|
151
|
+
// Claude Desktop has two MCP transport paths:
|
|
152
|
+
// 1. claude_desktop_config.json with mcp-remote (stdio bridge): for chat mode
|
|
153
|
+
// 2. .mcp.json inside the plugin directory (native HTTP): for cowork mode connectors
|
|
154
|
+
//
|
|
155
|
+
// For cowork connectors, embed the auth token in the URL query string.
|
|
156
|
+
// This lets Claude Desktop connect without requiring the user to go through
|
|
157
|
+
// an OAuth flow or manually click "Connect" in the Connectors UI.
|
|
158
|
+
// Write to claude_desktop_config.json via mcp-remote (chat mode fallback)
|
|
91
159
|
const entries = {};
|
|
92
160
|
for (const s of servers) {
|
|
93
161
|
const args = ['mcp-remote', s.url];
|
|
@@ -100,49 +168,47 @@ export class ClaudeDesktopAdapter {
|
|
|
100
168
|
};
|
|
101
169
|
}
|
|
102
170
|
mergeJsonMcpServers(getMcpConfigPath(), entries, 'mcpServers');
|
|
103
|
-
//
|
|
171
|
+
// Prefer rpm/plugin_<id>/ when the user has uploaded our zip through
|
|
172
|
+
// Claude Desktop's Customize > Create plugin > Upload plugin flow.
|
|
173
|
+
const rpm = findRpmPluginByName(PLUGIN_NAME);
|
|
174
|
+
if (rpm) {
|
|
175
|
+
writePluginMcpConfig(rpm.pluginPath, servers);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Fall back to the legacy cowork_plugins layout (cache + marketplace).
|
|
104
179
|
const pluginsDir = findCoworkPluginsDir();
|
|
105
180
|
if (pluginsDir) {
|
|
106
181
|
const cacheDir = join(pluginsDir, 'cache', 'runwork', PLUGIN_NAME, PLUGIN_VERSION);
|
|
107
182
|
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
183
|
for (const dir of [cacheDir, marketDir]) {
|
|
118
|
-
|
|
119
|
-
writeJsonConfig(join(dir, '.mcp.json'), pluginEntries);
|
|
184
|
+
writePluginMcpConfig(dir, servers);
|
|
120
185
|
}
|
|
121
186
|
}
|
|
122
187
|
}
|
|
123
188
|
async writeSkills(skills, _scope) {
|
|
189
|
+
// Prefer rpm/plugin_<id>/ when the user has uploaded our zip through
|
|
190
|
+
// Claude Desktop's Customize > Create plugin > Upload plugin flow.
|
|
191
|
+
// Claude Desktop owns rpm/manifest.json (registration, enablement),
|
|
192
|
+
// so we only refresh the plugin content files.
|
|
193
|
+
const rpm = findRpmPluginByName(PLUGIN_NAME);
|
|
194
|
+
if (rpm) {
|
|
195
|
+
writePluginMetadata(rpm.pluginPath, getPluginMetadata());
|
|
196
|
+
writePluginSkills(rpm.pluginPath, skills);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
124
199
|
const pluginsDir = findCoworkPluginsDir();
|
|
125
200
|
if (!pluginsDir) {
|
|
126
201
|
console.warn(' [Claude Desktop] Cowork plugins directory not found. Open Claude Desktop at least once first.');
|
|
127
202
|
return;
|
|
128
203
|
}
|
|
129
|
-
//
|
|
204
|
+
// Fall back to legacy cowork_plugins layout: write to both cache
|
|
205
|
+
// (installed copy) and marketplaces (source).
|
|
130
206
|
const cacheDir = join(pluginsDir, 'cache', 'runwork', PLUGIN_NAME, PLUGIN_VERSION);
|
|
131
207
|
const marketRoot = join(pluginsDir, 'marketplaces', 'runwork');
|
|
132
208
|
const marketDir = join(marketRoot, 'plugins', PLUGIN_NAME);
|
|
133
|
-
// Create plugin.json in both locations
|
|
134
209
|
for (const dir of [cacheDir, marketDir]) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
writeFileSync(join(pluginJsonDir, 'plugin.json'), JSON.stringify(getPluginJson(), null, 2));
|
|
138
|
-
}
|
|
139
|
-
// Write skill files to both locations
|
|
140
|
-
for (const skill of skills) {
|
|
141
|
-
for (const dir of [cacheDir, marketDir]) {
|
|
142
|
-
const skillDir = join(dir, 'skills', skill.name);
|
|
143
|
-
mkdirSync(skillDir, { recursive: true });
|
|
144
|
-
writeFileSync(join(skillDir, 'SKILL.md'), buildSkillMd(skill));
|
|
145
|
-
}
|
|
210
|
+
writePluginMetadata(dir, getPluginMetadata());
|
|
211
|
+
writePluginSkills(dir, skills);
|
|
146
212
|
}
|
|
147
213
|
// Register in installed_plugins.json
|
|
148
214
|
const installedPath = join(pluginsDir, 'installed_plugins.json');
|
|
@@ -172,25 +238,55 @@ export class ClaudeDesktopAdapter {
|
|
|
172
238
|
lastUpdated: new Date().toISOString(),
|
|
173
239
|
};
|
|
174
240
|
writeJsonConfig(marketplacesPath, marketplaces);
|
|
241
|
+
// Sync should recover a previously disabled plugin so it shows up in Customize.
|
|
242
|
+
setCoworkPluginEnabled(pluginsDir, true);
|
|
175
243
|
}
|
|
176
244
|
async writeInstructionHint(_hint, _scope) {
|
|
177
245
|
// Claude Desktop/Cowork uses project instructions within the app UI, not files.
|
|
178
246
|
// The plugin system handles skill discovery.
|
|
179
247
|
}
|
|
248
|
+
async buildPluginZip(skills, mcpServers, teamInstructions, outputPath) {
|
|
249
|
+
// Materialise the plugin tree into a temp dir, zip it, then clean up.
|
|
250
|
+
// Uses the same writePluginTree + writePluginTeamInstructions helpers
|
|
251
|
+
// the sync path uses so the on-disk layout is byte-identical whether
|
|
252
|
+
// the user installs via upload or sync.
|
|
253
|
+
const stagingRoot = mkdtempSync(join(tmpdir(), 'runwork-plugin-'));
|
|
254
|
+
const pluginStagingDir = join(stagingRoot, PLUGIN_NAME);
|
|
255
|
+
try {
|
|
256
|
+
writePluginTree(pluginStagingDir, {
|
|
257
|
+
...getPluginMetadata(),
|
|
258
|
+
skills,
|
|
259
|
+
mcpServers,
|
|
260
|
+
});
|
|
261
|
+
if (teamInstructions) {
|
|
262
|
+
writePluginTeamInstructions(pluginStagingDir, teamInstructions);
|
|
263
|
+
}
|
|
264
|
+
createZipFromDir(pluginStagingDir, outputPath);
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
try {
|
|
268
|
+
rmSync(stagingRoot, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
/* best-effort */
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
180
275
|
async writeTeamInstructions(instructions, _scope) {
|
|
181
|
-
// Write team instructions as a skill in the
|
|
276
|
+
// Write team instructions as a skill in the Cowork plugin system.
|
|
182
277
|
// This makes them available to Claude Desktop sessions via the plugin.
|
|
278
|
+
// Prefer an rpm upload location when present, fall back to the legacy
|
|
279
|
+
// cowork_plugins cache + marketplace layout.
|
|
280
|
+
const rpm = findRpmPluginByName(PLUGIN_NAME);
|
|
281
|
+
if (rpm) {
|
|
282
|
+
writePluginTeamInstructions(rpm.pluginPath, instructions);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
183
285
|
const pluginsDir = findCoworkPluginsDir();
|
|
184
286
|
if (!pluginsDir)
|
|
185
287
|
return;
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
}
|
|
288
|
+
writePluginTeamInstructions(join(pluginsDir, 'cache', 'runwork', PLUGIN_NAME, PLUGIN_VERSION), instructions);
|
|
289
|
+
writePluginTeamInstructions(join(pluginsDir, 'marketplaces', 'runwork', 'plugins', PLUGIN_NAME), instructions);
|
|
194
290
|
}
|
|
195
291
|
// ── Agent config (limited to preferences) ──────────────────────────
|
|
196
292
|
async writeAgentConfig(config, _scope) {
|
|
@@ -224,6 +320,87 @@ export class ClaudeDesktopAdapter {
|
|
|
224
320
|
}
|
|
225
321
|
writeFileSync(configPath, JSON.stringify(desktopConfig, null, 2));
|
|
226
322
|
}
|
|
323
|
+
// ── Cleanup (uninstall) ────────────────────────────────────────────
|
|
324
|
+
async cleanup(_scope, _manifest) {
|
|
325
|
+
// 1. Remove MCP entries from claude_desktop_config.json
|
|
326
|
+
removeRunworkMcpServers(getMcpConfigPath(), 'mcpServers');
|
|
327
|
+
// 2. Remove the uploaded rpm plugin if the user installed via
|
|
328
|
+
// Customize > Create plugin > Upload plugin. We delete both the
|
|
329
|
+
// plugin directory and the manifest entry so Claude Desktop does
|
|
330
|
+
// not show a stale Runwork plugin after uninstall.
|
|
331
|
+
const rpm = findRpmPluginByName(PLUGIN_NAME);
|
|
332
|
+
if (rpm) {
|
|
333
|
+
if (existsSync(rpm.pluginPath)) {
|
|
334
|
+
try {
|
|
335
|
+
rmSync(rpm.pluginPath, { recursive: true, force: true });
|
|
336
|
+
}
|
|
337
|
+
catch { /* best-effort */ }
|
|
338
|
+
}
|
|
339
|
+
const manifestPath = join(rpm.orgDir, 'rpm', 'manifest.json');
|
|
340
|
+
if (existsSync(manifestPath)) {
|
|
341
|
+
try {
|
|
342
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
343
|
+
if (Array.isArray(manifest.plugins)) {
|
|
344
|
+
manifest.plugins = manifest.plugins.filter((p) => p.name !== PLUGIN_NAME);
|
|
345
|
+
manifest.lastUpdated = Date.now();
|
|
346
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch { /* best-effort */ }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// 3. Remove Cowork plugin directories and registration
|
|
353
|
+
const pluginsDir = findCoworkPluginsDir();
|
|
354
|
+
if (pluginsDir) {
|
|
355
|
+
// Remove cache/runwork and marketplaces/runwork directories
|
|
356
|
+
const cacheRunwork = join(pluginsDir, 'cache', 'runwork');
|
|
357
|
+
const marketRunwork = join(pluginsDir, 'marketplaces', 'runwork');
|
|
358
|
+
for (const dir of [cacheRunwork, marketRunwork]) {
|
|
359
|
+
if (existsSync(dir)) {
|
|
360
|
+
try {
|
|
361
|
+
rmSync(dir, { recursive: true, force: true });
|
|
362
|
+
}
|
|
363
|
+
catch { /* best-effort */ }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Remove from installed_plugins.json
|
|
367
|
+
const installedPath = join(pluginsDir, 'installed_plugins.json');
|
|
368
|
+
if (existsSync(installedPath)) {
|
|
369
|
+
try {
|
|
370
|
+
const installed = readJsonConfig(installedPath);
|
|
371
|
+
if (installed.plugins) {
|
|
372
|
+
delete installed.plugins[`${PLUGIN_NAME}@runwork`];
|
|
373
|
+
writeJsonConfig(installedPath, installed);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch { /* best-effort */ }
|
|
377
|
+
}
|
|
378
|
+
// Remove from known_marketplaces.json
|
|
379
|
+
const marketplacesPath = join(pluginsDir, 'known_marketplaces.json');
|
|
380
|
+
if (existsSync(marketplacesPath)) {
|
|
381
|
+
try {
|
|
382
|
+
const marketplaces = readJsonConfig(marketplacesPath);
|
|
383
|
+
delete marketplaces['runwork'];
|
|
384
|
+
writeJsonConfig(marketplacesPath, marketplaces);
|
|
385
|
+
}
|
|
386
|
+
catch { /* best-effort */ }
|
|
387
|
+
}
|
|
388
|
+
// Remove the enable flag so cleanup fully unregisters the plugin.
|
|
389
|
+
const coworkSettingsPath = getCoworkSettingsPath(pluginsDir);
|
|
390
|
+
if (existsSync(coworkSettingsPath)) {
|
|
391
|
+
try {
|
|
392
|
+
const settings = readJsonConfig(coworkSettingsPath);
|
|
393
|
+
const enabledPlugins = settings.enabledPlugins;
|
|
394
|
+
if (enabledPlugins && typeof enabledPlugins === 'object' && !Array.isArray(enabledPlugins)) {
|
|
395
|
+
delete enabledPlugins[`${PLUGIN_NAME}@runwork`];
|
|
396
|
+
settings.enabledPlugins = enabledPlugins;
|
|
397
|
+
writeJsonConfig(coworkSettingsPath, settings);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch { /* best-effort */ }
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
227
404
|
// ── Usage stats (passive, best-effort) ─────────────────────────────
|
|
228
405
|
async readUsageStats(lastSyncAt) {
|
|
229
406
|
try {
|
package/dist/agents/cline.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { AgentAdapter, AgentConfigOverride, McpServerEntry, SkillFile } from './types.js';
|
|
1
|
+
import type { AgentAdapter, AgentConfigOverride, CleanupManifest, McpServerEntry, SkillFile } from './types.js';
|
|
2
2
|
export declare class ClineAdapter implements AgentAdapter {
|
|
3
3
|
name: string;
|
|
4
4
|
slug: string;
|
|
5
|
+
mcpProvidesSkills: boolean;
|
|
5
6
|
detect(): Promise<boolean>;
|
|
6
7
|
supportsMcpScope(scope: 'project' | 'user'): boolean;
|
|
7
8
|
supportsSkills(): boolean;
|
|
@@ -10,4 +11,5 @@ export declare class ClineAdapter implements AgentAdapter {
|
|
|
10
11
|
writeInstructionHint(hint: string, scope: 'project' | 'user'): Promise<void>;
|
|
11
12
|
writeTeamInstructions(instructions: string, scope: 'project' | 'user'): Promise<void>;
|
|
12
13
|
writeAgentConfig(config: AgentConfigOverride, scope: 'project' | 'user'): Promise<void>;
|
|
14
|
+
cleanup(scope: 'project' | 'user', manifest?: CleanupManifest): Promise<void>;
|
|
13
15
|
}
|
package/dist/agents/cline.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { whichBinary } from '../utils/which.js';
|
|
5
5
|
import { buildSkillMd } from './types.js';
|
|
6
|
-
import { mergeJsonMcpServers } from './utils/json-config.js';
|
|
6
|
+
import { mergeJsonMcpServers, removeRunworkMcpServers } from './utils/json-config.js';
|
|
7
7
|
export class ClineAdapter {
|
|
8
8
|
name = 'Cline';
|
|
9
9
|
slug = 'cline';
|
|
10
|
+
mcpProvidesSkills = true;
|
|
10
11
|
async detect() {
|
|
11
12
|
return !!whichBinary('cline');
|
|
12
13
|
}
|
|
@@ -85,4 +86,45 @@ export class ClineAdapter {
|
|
|
85
86
|
mkdirSync(join(globalStatePath, '..'), { recursive: true });
|
|
86
87
|
writeFileSync(globalStatePath, JSON.stringify(state, null, 2));
|
|
87
88
|
}
|
|
89
|
+
// ── Cleanup (uninstall) ────────────────────────────────────────────
|
|
90
|
+
async cleanup(scope, manifest) {
|
|
91
|
+
// 1. Remove MCP entries (user scope only)
|
|
92
|
+
if (scope === 'user') {
|
|
93
|
+
const configPath = join(homedir(), '.cline', 'data', 'settings', 'cline_mcp_settings.json');
|
|
94
|
+
removeRunworkMcpServers(configPath, 'mcpServers');
|
|
95
|
+
}
|
|
96
|
+
// 2. Remove rule files (project scope only)
|
|
97
|
+
if (scope === 'project') {
|
|
98
|
+
const rulesDir = join(process.cwd(), '.clinerules');
|
|
99
|
+
if (existsSync(rulesDir)) {
|
|
100
|
+
for (const file of ['runwork.md', 'runwork-team.md']) {
|
|
101
|
+
const filePath = join(rulesDir, file);
|
|
102
|
+
if (existsSync(filePath)) {
|
|
103
|
+
try {
|
|
104
|
+
unlinkSync(filePath);
|
|
105
|
+
}
|
|
106
|
+
catch { /* best-effort */ }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Remove only skill .md files that we synced
|
|
110
|
+
if (manifest?.skillFilenames.length) {
|
|
111
|
+
const allowed = new Set(manifest.skillFilenames.map(f => `${f}.md`));
|
|
112
|
+
for (const entry of readdirSync(rulesDir)) {
|
|
113
|
+
if (allowed.has(entry)) {
|
|
114
|
+
try {
|
|
115
|
+
unlinkSync(join(rulesDir, entry));
|
|
116
|
+
}
|
|
117
|
+
catch { /* best-effort */ }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Remove dir if empty
|
|
122
|
+
try {
|
|
123
|
+
if (readdirSync(rulesDir).length === 0)
|
|
124
|
+
rmSync(rulesDir, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
catch { /* best-effort */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
88
130
|
}
|
package/dist/agents/codex.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { AgentAdapter, AgentConfigOverride, AgentUsageStats, McpServerEntry, SkillFile } from './types.js';
|
|
1
|
+
import type { AgentAdapter, AgentConfigOverride, AgentUsageStats, CleanupManifest, McpServerEntry, SkillFile } from './types.js';
|
|
2
2
|
export declare class CodexAdapter implements AgentAdapter {
|
|
3
3
|
name: string;
|
|
4
4
|
slug: string;
|
|
5
|
+
mcpProvidesSkills: boolean;
|
|
5
6
|
detect(): Promise<boolean>;
|
|
6
7
|
supportsMcpScope(_scope: 'project' | 'user'): boolean;
|
|
7
8
|
supportsSkills(): boolean;
|
|
@@ -10,6 +11,7 @@ export declare class CodexAdapter implements AgentAdapter {
|
|
|
10
11
|
writeInstructionHint(hint: string, scope: 'project' | 'user'): Promise<void>;
|
|
11
12
|
writeTeamInstructions(instructions: string, scope: 'project' | 'user'): Promise<void>;
|
|
12
13
|
writeAgentConfig(config: AgentConfigOverride, scope: 'project' | 'user'): Promise<void>;
|
|
14
|
+
cleanup(scope: 'project' | 'user', manifest?: CleanupManifest): Promise<void>;
|
|
13
15
|
readUsageStats(lastSyncAt: string | null): Promise<AgentUsageStats | null>;
|
|
14
16
|
readVersion(): Promise<string | null>;
|
|
15
17
|
}
|
package/dist/agents/codex.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { parse, stringify } from 'smol-toml';
|
|
5
|
-
import { buildSkillMd } from './types.js';
|
|
6
|
-
|
|
5
|
+
import { buildSkillMd, appendTokenToUrl, RUNWORK_WORKSPACE_MCP_NAME } from './types.js';
|
|
6
|
+
function isRunworkManagedCodexKey(key) {
|
|
7
|
+
return (key === RUNWORK_WORKSPACE_MCP_NAME ||
|
|
8
|
+
key.startsWith('runwork-') ||
|
|
9
|
+
key.startsWith('Runwork-'));
|
|
10
|
+
}
|
|
11
|
+
import { writeHintToFile, writeTeamInstructionsToFile, removeHintFromFile, removeTeamInstructionsFromFile } from './utils/instruction-hint.js';
|
|
7
12
|
import { querySqlite } from '../utils/sqlite.js';
|
|
8
13
|
import { whichBinary } from '../utils/which.js';
|
|
9
14
|
export class CodexAdapter {
|
|
10
15
|
name = 'Codex';
|
|
11
16
|
slug = 'codex';
|
|
17
|
+
mcpProvidesSkills = true;
|
|
12
18
|
async detect() {
|
|
13
19
|
return !!whichBinary('codex');
|
|
14
20
|
}
|
|
@@ -29,16 +35,22 @@ export class CodexAdapter {
|
|
|
29
35
|
parsed.mcp_servers = {};
|
|
30
36
|
}
|
|
31
37
|
const mcpServers = parsed.mcp_servers;
|
|
32
|
-
// Remove existing runwork
|
|
38
|
+
// Remove existing runwork-managed entries. Handles legacy lowercase
|
|
39
|
+
// ("runwork-foo"), sanitized PascalCase ("Runwork-foo"), and the bare
|
|
40
|
+
// workspace-tools name ("Runwork").
|
|
33
41
|
for (const key of Object.keys(mcpServers)) {
|
|
34
|
-
if (key
|
|
42
|
+
if (isRunworkManagedCodexKey(key)) {
|
|
35
43
|
delete mcpServers[key];
|
|
44
|
+
}
|
|
36
45
|
}
|
|
37
|
-
// Add new entries
|
|
46
|
+
// Add new entries (sanitize names to match Codex's ^[a-zA-Z0-9_-]+$ requirement)
|
|
38
47
|
for (const s of servers) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
const safeName = s.name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
49
|
+
// Embed auth token in URL so Codex doesn't need a separate env var
|
|
50
|
+
const token = s.headers?.Authorization?.replace(/^Bearer\s+/i, '');
|
|
51
|
+
const serverUrl = token ? appendTokenToUrl(s.url, token) : s.url;
|
|
52
|
+
mcpServers[safeName] = {
|
|
53
|
+
url: serverUrl,
|
|
42
54
|
};
|
|
43
55
|
}
|
|
44
56
|
mkdirSync(join(configPath, '..'), { recursive: true });
|
|
@@ -49,7 +61,16 @@ export class CodexAdapter {
|
|
|
49
61
|
? join(process.cwd(), '.codex', 'skills')
|
|
50
62
|
: join(homedir(), '.codex', 'skills');
|
|
51
63
|
for (const skill of skills) {
|
|
52
|
-
|
|
64
|
+
if (skill.name !== skill.filename) {
|
|
65
|
+
const oldDir = join(baseDir, skill.name);
|
|
66
|
+
if (existsSync(oldDir)) {
|
|
67
|
+
try {
|
|
68
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
catch { /* best-effort */ }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const skillDir = join(baseDir, skill.filename);
|
|
53
74
|
mkdirSync(skillDir, { recursive: true });
|
|
54
75
|
writeFileSync(join(skillDir, 'SKILL.md'), buildSkillMd(skill));
|
|
55
76
|
}
|
|
@@ -57,13 +78,13 @@ export class CodexAdapter {
|
|
|
57
78
|
async writeInstructionHint(hint, scope) {
|
|
58
79
|
const filePath = scope === 'project'
|
|
59
80
|
? join(process.cwd(), 'AGENTS.md')
|
|
60
|
-
: join(homedir(), '.codex', '
|
|
81
|
+
: join(homedir(), '.codex', 'AGENTS.md');
|
|
61
82
|
writeHintToFile(filePath, hint);
|
|
62
83
|
}
|
|
63
84
|
async writeTeamInstructions(instructions, scope) {
|
|
64
85
|
const filePath = scope === 'project'
|
|
65
86
|
? join(process.cwd(), 'AGENTS.md')
|
|
66
|
-
: join(homedir(), '.codex', '
|
|
87
|
+
: join(homedir(), '.codex', 'AGENTS.md');
|
|
67
88
|
writeTeamInstructionsToFile(filePath, instructions);
|
|
68
89
|
}
|
|
69
90
|
// ── Agent config (TOML) ───────────────────────────────────────────
|
|
@@ -89,6 +110,51 @@ export class CodexAdapter {
|
|
|
89
110
|
mkdirSync(join(configPath, '..'), { recursive: true });
|
|
90
111
|
writeFileSync(configPath, stringify(parsed));
|
|
91
112
|
}
|
|
113
|
+
// ── Cleanup (uninstall) ────────────────────────────────────────────
|
|
114
|
+
async cleanup(scope, manifest) {
|
|
115
|
+
// 1. Remove MCP server entries from config.toml
|
|
116
|
+
const configPath = join(homedir(), '.codex', 'config.toml');
|
|
117
|
+
if (existsSync(configPath)) {
|
|
118
|
+
try {
|
|
119
|
+
const parsed = parse(readFileSync(configPath, 'utf-8'));
|
|
120
|
+
if (parsed.mcp_servers && typeof parsed.mcp_servers === 'object') {
|
|
121
|
+
const mcpServers = parsed.mcp_servers;
|
|
122
|
+
for (const key of Object.keys(mcpServers)) {
|
|
123
|
+
// Codex TOML sanitizes names via writeMcpServers, so the JSON-style
|
|
124
|
+
// "Runwork: " prefix is never actually written here. Match the real
|
|
125
|
+
// on-disk forms: "runwork-*" (legacy), "Runwork-*" (previous), and
|
|
126
|
+
// the bare "Runwork" name used for workspace tools.
|
|
127
|
+
if (isRunworkManagedCodexKey(key)) {
|
|
128
|
+
delete mcpServers[key];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
writeFileSync(configPath, stringify(parsed));
|
|
133
|
+
}
|
|
134
|
+
catch { /* best-effort */ }
|
|
135
|
+
}
|
|
136
|
+
// 2. Remove only skill directories that we synced (listed in manifest)
|
|
137
|
+
const skillsDir = scope === 'project'
|
|
138
|
+
? join(process.cwd(), '.codex', 'skills')
|
|
139
|
+
: join(homedir(), '.codex', 'skills');
|
|
140
|
+
if (existsSync(skillsDir) && manifest?.skillFilenames.length) {
|
|
141
|
+
const allowed = new Set(manifest.skillFilenames);
|
|
142
|
+
for (const entry of readdirSync(skillsDir)) {
|
|
143
|
+
if (!allowed.has(entry))
|
|
144
|
+
continue;
|
|
145
|
+
try {
|
|
146
|
+
rmSync(join(skillsDir, entry), { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
catch { /* best-effort */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// 3. Remove instruction hints and team instructions
|
|
152
|
+
const instructionFile = scope === 'project'
|
|
153
|
+
? join(process.cwd(), 'AGENTS.md')
|
|
154
|
+
: join(homedir(), '.codex', 'AGENTS.md');
|
|
155
|
+
removeHintFromFile(instructionFile);
|
|
156
|
+
removeTeamInstructionsFromFile(instructionFile);
|
|
157
|
+
}
|
|
92
158
|
// ── Usage stats (passive, best-effort) ─────────────────────────────
|
|
93
159
|
async readUsageStats(lastSyncAt) {
|
|
94
160
|
try {
|
|
@@ -100,9 +166,14 @@ export class CodexAdapter {
|
|
|
100
166
|
const dbPath = join(codexDir, 'state_5.sqlite');
|
|
101
167
|
if (!existsSync(dbPath))
|
|
102
168
|
return null;
|
|
103
|
-
|
|
169
|
+
// Filter on updated_at, not created_at: long-lived threads can accumulate
|
|
170
|
+
// activity post-sync without ever being "newly created". tokens_used is a
|
|
171
|
+
// lifetime per-thread total — summing active threads here overcounts when
|
|
172
|
+
// a thread was already partially used before lastSyncAt, but it's a far
|
|
173
|
+
// better signal than "0 because no new threads were spawned".
|
|
174
|
+
const countResult = querySqlite(dbPath, `SELECT count(*) FROM threads WHERE updated_at > ${sinceSec}`);
|
|
104
175
|
const sessionCount = parseInt(countResult) || 0;
|
|
105
|
-
const tokensResult = querySqlite(dbPath, `SELECT coalesce(sum(tokens_used), 0) FROM threads WHERE
|
|
176
|
+
const tokensResult = querySqlite(dbPath, `SELECT coalesce(sum(tokens_used), 0) FROM threads WHERE updated_at > ${sinceSec}`);
|
|
106
177
|
const tokensUsed = parseInt(tokensResult) || 0;
|
|
107
178
|
const latestResult = querySqlite(dbPath, `SELECT max(updated_at) FROM threads`);
|
|
108
179
|
const latestSec = parseInt(latestResult) || 0;
|