@mattstratton/install-marketing-skills 1.0.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 (3) hide show
  1. package/README.md +40 -0
  2. package/bin/install.js +369 -0
  3. package/package.json +28 -0
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @mattstratton/install-marketing-skills
2
+
3
+ One-line installer for the **tigerdata-marketing-skills** Cowork (Claude Desktop) plugin.
4
+
5
+ Workaround for [anthropics/claude-code#40600](https://github.com/anthropics/claude-code/issues/40600) — Cowork's `RemotePluginManager` can wipe marketplace-installed plugins on restart. This installer writes directly to the `LocalPluginsReader` path, which persists.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ # Install or update to the latest version
11
+ npx @mattstratton/install-marketing-skills
12
+
13
+ # Check if an update is available (doesn't install)
14
+ npx @mattstratton/install-marketing-skills --check
15
+ ```
16
+
17
+ ## What it does
18
+
19
+ 1. Fetches the latest release from [timescale/marketing-skills](https://github.com/timescale/marketing-skills/releases)
20
+ 2. Downloads the plugin zip (no git clone needed)
21
+ 3. Finds your Cowork installation(s)
22
+ 4. Installs the plugin into `cowork_plugins/cache/` where it persists across restarts
23
+ 5. Enables the plugin in `cowork_settings.json`
24
+
25
+ **Restart Claude Desktop after running** for changes to take effect.
26
+
27
+ ## Requirements
28
+
29
+ - Node.js 18+
30
+ - Claude Desktop installed (Cowork must have been opened at least once)
31
+
32
+ ## Platforms
33
+
34
+ Works on macOS, Windows, and Linux.
35
+
36
+ On macOS, you can also use the shell script (no Node.js required):
37
+
38
+ ```bash
39
+ curl -fsSL https://raw.githubusercontent.com/timescale/marketing-skills/main/scripts/install.sh | bash
40
+ ```
package/bin/install.js ADDED
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // install.js — Cross-platform installer for tigerdata-marketing-skills
6
+ //
7
+ // Downloads the latest release zip from GitHub and installs it into the
8
+ // Cowork LocalPluginsReader path, which persists across restarts.
9
+ // Workaround for https://github.com/anthropics/claude-code/issues/40600
10
+ //
11
+ // Usage:
12
+ // npx @mattstratton/install-marketing-skills # Install or update
13
+ // npx @mattstratton/install-marketing-skills --check # Check if update available
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const https = require('https');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const crypto = require('crypto');
21
+ const { execFileSync } = require('child_process');
22
+
23
+ // --- Configuration ---------------------------------------------------------
24
+ const REPO = 'timescale/marketing-skills';
25
+ const MARKETPLACE_NAME = 'tigerdata-marketing';
26
+ const PLUGIN_NAME = 'tigerdata-marketing-skills';
27
+ const PLUGIN_KEY = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
28
+ const MARKETPLACE_GITHUB = 'timescale/marketing-skills';
29
+
30
+ // --- Output helpers --------------------------------------------------------
31
+ const GREEN = '\x1b[32m';
32
+ const YELLOW = '\x1b[33m';
33
+ const RED = '\x1b[31m';
34
+ const BOLD = '\x1b[1m';
35
+ const NC = '\x1b[0m';
36
+
37
+ function info(msg) { console.log(`${GREEN}✓${NC} ${msg}`); }
38
+ function warn(msg) { console.log(`${YELLOW}!${NC} ${msg}`); }
39
+ function error(msg) { console.error(`${RED}✗${NC} ${msg}`); process.exit(1); }
40
+
41
+ // --- HTTP helpers (zero deps) ----------------------------------------------
42
+ function httpsGet(url, redirects = 0) {
43
+ if (redirects > 10) return Promise.reject(new Error('Too many redirects'));
44
+ return new Promise((resolve, reject) => {
45
+ https.get(url, { headers: { 'User-Agent': 'install-marketing-skills' } }, (res) => {
46
+ // Follow redirects
47
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
48
+ return httpsGet(res.headers.location, redirects + 1).then(resolve, reject);
49
+ }
50
+ if (res.statusCode !== 200) {
51
+ return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
52
+ }
53
+ const chunks = [];
54
+ res.on('data', (chunk) => chunks.push(chunk));
55
+ res.on('end', () => resolve(Buffer.concat(chunks)));
56
+ res.on('error', reject);
57
+ }).on('error', reject);
58
+ });
59
+ }
60
+
61
+ async function fetchJSON(url) {
62
+ const buf = await httpsGet(url);
63
+ return JSON.parse(buf.toString('utf-8'));
64
+ }
65
+
66
+ async function downloadFile(url, destPath) {
67
+ const buf = await httpsGet(url);
68
+ fs.writeFileSync(destPath, buf);
69
+ return buf.length;
70
+ }
71
+
72
+ // --- Platform detection ----------------------------------------------------
73
+ function getSessionsRoot() {
74
+ switch (os.platform()) {
75
+ case 'darwin':
76
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'local-agent-mode-sessions');
77
+ case 'win32': {
78
+ const appdata = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
79
+ return path.join(appdata, 'Claude', 'local-agent-mode-sessions');
80
+ }
81
+ default: // linux
82
+ return path.join(os.homedir(), '.config', 'Claude', 'local-agent-mode-sessions');
83
+ }
84
+ }
85
+
86
+ // --- Zip extraction (platform-native, no deps) -----------------------------
87
+ function extractZip(zipPath, destDir) {
88
+ fs.mkdirSync(destDir, { recursive: true });
89
+ if (os.platform() === 'win32') {
90
+ // PowerShell's Expand-Archive
91
+ const escapedZip = zipPath.replace(/"/g, '`"');
92
+ const escapedDest = destDir.replace(/"/g, '`"');
93
+ execFileSync('powershell', [
94
+ '-NoProfile', '-Command',
95
+ `Expand-Archive -LiteralPath "${escapedZip}" -DestinationPath "${escapedDest}" -Force`,
96
+ ], { stdio: 'pipe' });
97
+ } else {
98
+ execFileSync('unzip', ['-q', zipPath, '-d', destDir], { stdio: 'pipe' });
99
+ }
100
+ }
101
+
102
+ // --- JSON helpers ----------------------------------------------------------
103
+ function readJSON(filePath, defaultValue) {
104
+ try {
105
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
106
+ } catch {
107
+ return defaultValue !== undefined ? defaultValue : {};
108
+ }
109
+ }
110
+
111
+ function writeJSON(filePath, data) {
112
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
113
+ }
114
+
115
+ // --- File hashing ----------------------------------------------------------
116
+ function sha256File(filePath) {
117
+ const content = fs.readFileSync(filePath);
118
+ return crypto.createHash('sha256').update(content).digest('hex');
119
+ }
120
+
121
+ function collectFileHashes(dir) {
122
+ const hashes = {};
123
+ function walk(currentDir) {
124
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
125
+ const fullPath = path.join(currentDir, entry.name);
126
+ if (entry.isDirectory()) {
127
+ walk(fullPath);
128
+ } else if (!entry.name.includes('.DS_Store')) {
129
+ const rel = path.relative(dir, fullPath);
130
+ // Normalize to forward slashes for cross-platform consistency
131
+ hashes[rel.split(path.sep).join('/')] = sha256File(fullPath);
132
+ }
133
+ }
134
+ }
135
+ walk(dir);
136
+ return hashes;
137
+ }
138
+
139
+ // --- Discovery -------------------------------------------------------------
140
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
141
+
142
+ function discoverInstallations(sessionsRoot) {
143
+ const results = [];
144
+
145
+ if (!fs.existsSync(sessionsRoot)) {
146
+ error(`Sessions directory not found: ${sessionsRoot}\nIs Claude Desktop installed and has Cowork been used at least once?`);
147
+ }
148
+
149
+ for (const acct of fs.readdirSync(sessionsRoot, { withFileTypes: true })) {
150
+ if (!acct.isDirectory() || !UUID_RE.test(acct.name)) continue;
151
+ const acctPath = path.join(sessionsRoot, acct.name);
152
+
153
+ for (const org of fs.readdirSync(acctPath, { withFileTypes: true })) {
154
+ if (!org.isDirectory() || org.name.startsWith('.')) continue;
155
+ const orgPath = path.join(acctPath, org.name);
156
+ const coworkPlugins = path.join(orgPath, 'cowork_plugins');
157
+ const coworkSettings = path.join(orgPath, 'cowork_settings.json');
158
+
159
+ // Create cowork_plugins structure if org dir has content
160
+ if (!fs.existsSync(coworkPlugins)) {
161
+ const contents = fs.readdirSync(orgPath);
162
+ if (contents.length > 0) {
163
+ console.log(` Creating cowork_plugins in ${org.name.slice(0, 8)}...`);
164
+ fs.mkdirSync(path.join(coworkPlugins, 'marketplaces'), { recursive: true });
165
+ fs.mkdirSync(path.join(coworkPlugins, 'cache'), { recursive: true });
166
+ fs.mkdirSync(path.join(coworkPlugins, '.install-manifests'), { recursive: true });
167
+ writeJSON(path.join(coworkPlugins, 'installed_plugins.json'), { version: 2, plugins: {} });
168
+ writeJSON(path.join(coworkPlugins, 'known_marketplaces.json'), {});
169
+ } else {
170
+ continue;
171
+ }
172
+ }
173
+
174
+ results.push({ orgName: org.name, coworkPlugins, coworkSettings });
175
+ }
176
+ }
177
+
178
+ if (results.length === 0) {
179
+ error('No Cowork installations found.\nIs Claude Desktop installed and has Cowork been used at least once?');
180
+ }
181
+
182
+ return results;
183
+ }
184
+
185
+ // --- Installation ----------------------------------------------------------
186
+ function installTo(coworkPlugins, coworkSettings, extractDir, version, tagName) {
187
+ const installedPluginsPath = path.join(coworkPlugins, 'installed_plugins.json');
188
+ const installedData = readJSON(installedPluginsPath, { version: 2, plugins: {} });
189
+ const entries = (installedData.plugins || {})[PLUGIN_KEY] || [];
190
+ const oldVersion = entries.length > 0 ? entries[0].version : null;
191
+
192
+ if (oldVersion === version) {
193
+ console.log(` Already up to date (v${version})`);
194
+ return;
195
+ }
196
+
197
+ console.log(` Updating: v${oldVersion || 'none'} → v${version}`);
198
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, '.000Z');
199
+
200
+ // Step 1: Populate cache
201
+ const cacheDir = path.join(coworkPlugins, 'cache', MARKETPLACE_NAME, PLUGIN_NAME, version);
202
+ if (fs.existsSync(cacheDir)) {
203
+ fs.rmSync(cacheDir, { recursive: true, force: true });
204
+ }
205
+ fs.cpSync(extractDir, cacheDir, { recursive: true });
206
+
207
+ const fileHashes = collectFileHashes(cacheDir);
208
+ const fileCount = Object.keys(fileHashes).length;
209
+ console.log(` Cache: ${fileCount} files → ${cacheDir}`);
210
+
211
+ // Step 2: Install manifest
212
+ const manifestDir = path.join(coworkPlugins, '.install-manifests');
213
+ fs.mkdirSync(manifestDir, { recursive: true });
214
+ const manifestPath = path.join(manifestDir, `${PLUGIN_KEY}.json`);
215
+
216
+ let createdAt = now;
217
+ const oldManifest = readJSON(manifestPath, null);
218
+ if (oldManifest) {
219
+ createdAt = oldManifest.createdAt || now;
220
+ }
221
+
222
+ writeJSON(manifestPath, {
223
+ pluginId: PLUGIN_KEY,
224
+ createdAt,
225
+ files: fileHashes,
226
+ });
227
+ console.log(` Manifest: ${fileCount} file hashes`);
228
+
229
+ // Step 3: Update installed_plugins.json
230
+ installedData.version = installedData.version || 2;
231
+ installedData.plugins = installedData.plugins || {};
232
+ const existing = installedData.plugins[PLUGIN_KEY] || [{}];
233
+ const entry = existing[0] || {};
234
+ entry.scope = 'user';
235
+ entry.installPath = cacheDir;
236
+ entry.version = version;
237
+ entry.lastUpdated = now;
238
+ entry.gitCommitSha = tagName;
239
+ entry.installedAt = entry.installedAt || now;
240
+ installedData.plugins[PLUGIN_KEY] = [entry];
241
+ writeJSON(installedPluginsPath, installedData);
242
+ console.log(' installed_plugins.json: updated');
243
+
244
+ // Step 4: Ensure cowork_settings.json
245
+ const settings = readJSON(coworkSettings);
246
+ const enabled = settings.enabledPlugins = settings.enabledPlugins || {};
247
+ if (!enabled[PLUGIN_KEY]) {
248
+ enabled[PLUGIN_KEY] = true;
249
+ writeJSON(coworkSettings, settings);
250
+ console.log(' cowork_settings.json: enabled plugin');
251
+ } else {
252
+ console.log(' cowork_settings.json: already enabled');
253
+ }
254
+
255
+ // Step 5: Ensure known_marketplaces.json
256
+ const knownPath = path.join(coworkPlugins, 'known_marketplaces.json');
257
+ const known = readJSON(knownPath);
258
+ if (!known[MARKETPLACE_NAME]) {
259
+ known[MARKETPLACE_NAME] = {
260
+ source: { source: 'github', repo: MARKETPLACE_GITHUB },
261
+ installLocation: path.join(coworkPlugins, 'marketplaces', MARKETPLACE_NAME),
262
+ lastUpdated: now,
263
+ };
264
+ writeJSON(knownPath, known);
265
+ console.log(' known_marketplaces.json: added marketplace');
266
+ }
267
+
268
+ console.log(` Installed v${version} (${fileCount} files)`);
269
+ }
270
+
271
+ // --- Main ------------------------------------------------------------------
272
+ async function main() {
273
+ const checkOnly = process.argv.includes('--check');
274
+
275
+ console.log('');
276
+ console.log(`${BOLD}${PLUGIN_NAME} installer for Cowork${NC}`);
277
+ console.log('Workaround for github.com/anthropics/claude-code/issues/40600');
278
+ console.log('');
279
+
280
+ const sessionsRoot = getSessionsRoot();
281
+ console.log(`Platform: ${os.platform()}`);
282
+ console.log(`Sessions: ${sessionsRoot}`);
283
+ console.log('');
284
+
285
+ // Fetch latest release
286
+ console.log('Fetching latest release...');
287
+ let release;
288
+ try {
289
+ release = await fetchJSON(`https://api.github.com/repos/${REPO}/releases/latest`);
290
+ } catch (err) {
291
+ error(`Failed to fetch release info: ${err.message}`);
292
+ }
293
+
294
+ const version = release.tag_name.replace(/^v/, '');
295
+ const tagName = release.tag_name;
296
+ const zipAsset = (release.assets || []).find((a) => a.name.endsWith('.zip'));
297
+ if (!zipAsset) {
298
+ error('No zip asset found in the latest release.');
299
+ }
300
+ info(`Latest release: v${version} (${tagName})`);
301
+
302
+ // Discover installations
303
+ const installations = discoverInstallations(sessionsRoot);
304
+ console.log(`Found ${installations.length} Cowork installation(s)`);
305
+ console.log('');
306
+
307
+ // Check-only mode
308
+ if (checkOnly) {
309
+ for (const inst of installations) {
310
+ const ipPath = path.join(inst.coworkPlugins, 'installed_plugins.json');
311
+ const data = readJSON(ipPath, { version: 2, plugins: {} });
312
+ const entries = (data.plugins || {})[PLUGIN_KEY] || [];
313
+ const installed = entries.length > 0 ? entries[0].version : null;
314
+ const short = inst.orgName.slice(0, 8);
315
+
316
+ if (!installed) {
317
+ console.log(` ${short}... Not installed. Run without --check to install.`);
318
+ } else if (installed === version) {
319
+ console.log(` ${short}... Up to date (v${installed})`);
320
+ } else {
321
+ console.log(` ${short}... Update available: v${installed} → v${version}`);
322
+ }
323
+ }
324
+ console.log('');
325
+ console.log('Done!');
326
+ return;
327
+ }
328
+
329
+ // Download zip
330
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'marketing-skills-'));
331
+ const cleanup = () => { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} };
332
+ process.on('exit', cleanup);
333
+ process.on('SIGINT', () => { cleanup(); process.exit(1); });
334
+
335
+ const zipFile = path.join(tmpDir, 'plugin.zip');
336
+ console.log(`Downloading v${version}...`);
337
+ try {
338
+ const bytes = await downloadFile(zipAsset.browser_download_url, zipFile);
339
+ info(`Downloaded ${(bytes / 1024).toFixed(0)} KB zip`);
340
+ } catch (err) {
341
+ error(`Failed to download: ${err.message}`);
342
+ }
343
+
344
+ // Extract
345
+ const extractDir = path.join(tmpDir, 'extracted');
346
+ try {
347
+ extractZip(zipFile, extractDir);
348
+ info('Extracted plugin files');
349
+ } catch (err) {
350
+ error(`Failed to extract zip: ${err.message}`);
351
+ }
352
+ console.log('');
353
+
354
+ // Install to each location
355
+ let count = 0;
356
+ for (const inst of installations) {
357
+ console.log(`--- ${inst.orgName.slice(0, 8)}... ---`);
358
+ installTo(inst.coworkPlugins, inst.coworkSettings, extractDir, version, tagName);
359
+ count++;
360
+ console.log('');
361
+ }
362
+
363
+ console.log(`Done! Installed to ${count} location(s).`);
364
+ console.log('Restart Claude Desktop for changes to take effect.');
365
+ }
366
+
367
+ main().catch((err) => {
368
+ error(err.message);
369
+ });
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@mattstratton/install-marketing-skills",
3
+ "version": "1.0.0",
4
+ "description": "One-line installer for tigerdata-marketing-skills Cowork plugin. Workaround for github.com/anthropics/claude-code/issues/40600",
5
+ "bin": {
6
+ "install-marketing-skills": "./bin/install.js"
7
+ },
8
+ "files": [
9
+ "bin/"
10
+ ],
11
+ "keywords": [
12
+ "claude",
13
+ "cowork",
14
+ "plugin",
15
+ "tigerdata",
16
+ "marketing"
17
+ ],
18
+ "author": "Matt Stratton",
19
+ "license": "Apache-2.0",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/timescale/marketing-skills.git",
23
+ "directory": "packages/installer"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ }
28
+ }