@shiplens/cli 1.2.7

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.
@@ -0,0 +1,96 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execFileSync } = require('child_process');
5
+
6
+ const CLIENTS = ['cursor', 'codex', 'claude', 'antigravity', 'manual'];
7
+
8
+ function makeError(message, code) {
9
+ const error = new Error(message);
10
+ error.code = code;
11
+ return error;
12
+ }
13
+
14
+ function getClientPath(client, homeDir = os.homedir(), platform = process.platform) {
15
+ switch (client) {
16
+ case 'cursor':
17
+ return path.join(homeDir, '.cursor', 'mcp.json');
18
+ case 'claude':
19
+ if (platform === 'darwin') return path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
20
+ if (platform === 'win32') return path.join(homeDir, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
21
+ return path.join(homeDir, '.config', 'Claude', 'claude_desktop_config.json');
22
+ case 'antigravity':
23
+ return path.join(homeDir, '.gemini', 'config', 'mcp_config.json');
24
+ case 'codex':
25
+ return path.join(homeDir, '.codex', 'config.toml');
26
+ default:
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function getMcpServer(client, url) {
32
+ void client;
33
+ void url;
34
+ return {
35
+ command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
36
+ args: ['--yes', 'shiplens-cli', 'mcp', 'serve'],
37
+ };
38
+ }
39
+
40
+ function getMcpConfig(client, url) {
41
+ if (!CLIENTS.includes(client)) throw makeError(`Unsupported MCP client: ${client}`, 'UNSUPPORTED_MCP_CLIENT');
42
+ return { mcpServers: { shiplens: getMcpServer(client, url) } };
43
+ }
44
+
45
+ function readJsonConfig(filePath) {
46
+ if (!fs.existsSync(filePath)) return {};
47
+ try {
48
+ const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
49
+ if (!value || Array.isArray(value) || typeof value !== 'object') throw new Error('root must be object');
50
+ return value;
51
+ } catch (error) {
52
+ throw makeError(`Failed to read MCP config at ${filePath}: invalid JSON`, 'INVALID_MCP_CONFIG');
53
+ }
54
+ }
55
+
56
+ function mergeJsonMcpConfig(client, url, filePath) {
57
+ const config = readJsonConfig(filePath);
58
+ config.mcpServers = config.mcpServers || {};
59
+ const desired = getMcpServer(client, url);
60
+ const existing = config.mcpServers.shiplens;
61
+ if (existing) {
62
+ const matching = JSON.stringify(existing) === JSON.stringify(desired);
63
+ return { written: false, already_configured: matching, path: filePath, config: getMcpConfig(client, url) };
64
+ }
65
+
66
+ config.mcpServers.shiplens = desired;
67
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
68
+ fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
69
+ return { written: true, already_configured: false, path: filePath, config: getMcpConfig(client, url) };
70
+ }
71
+
72
+ function configureMcpClient(client, url, options = {}) {
73
+ if (!CLIENTS.includes(client)) throw makeError(`Unsupported MCP client: ${client}`, 'UNSUPPORTED_MCP_CLIENT');
74
+ if (!url) throw makeError('MCP URL is required', 'MCP_URL_REQUIRED');
75
+
76
+ if (client === 'manual') {
77
+ return { written: false, already_configured: false, path: null, config: getMcpConfig('manual', url) };
78
+ }
79
+
80
+ const homeDir = options.homeDir || os.homedir();
81
+ const platform = options.platform || process.platform;
82
+ const configPath = getClientPath(client, homeDir, platform);
83
+ if (client !== 'codex') return mergeJsonMcpConfig(client, url, configPath);
84
+
85
+ const exec = options.execFileSync || execFileSync;
86
+ const bundledCodex = '/Applications/ChatGPT.app/Contents/Resources/codex';
87
+ const codexCommand = options.codexCommand || process.env.SHIPLENS_CODEX_BIN || (fs.existsSync(bundledCodex) ? bundledCodex : 'codex');
88
+ try {
89
+ exec(codexCommand, ['mcp', 'add', 'shiplens', '--', process.platform === 'win32' ? 'npx.cmd' : 'npx', '--yes', 'shiplens-cli', 'mcp', 'serve'], { stdio: 'ignore' });
90
+ } catch (error) {
91
+ throw makeError('Failed to configure Codex MCP. Ensure Codex Desktop is installed and codex CLI is available in PATH.', 'CODEX_MCP_CONFIG_FAILED');
92
+ }
93
+ return { written: true, already_configured: false, path: configPath, config: getMcpConfig('codex', url) };
94
+ }
95
+
96
+ module.exports = { CLIENTS, getClientPath, getMcpConfig, configureMcpClient };
@@ -0,0 +1,354 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ let taxonomyCache = null;
5
+ let indexCache = null;
6
+
7
+ function loadTaxonomy() {
8
+ if (taxonomyCache) return taxonomyCache;
9
+ const filePath = path.join(__dirname, 'assets', 'taxonomy.json');
10
+ try {
11
+ if (fs.existsSync(filePath)) {
12
+ taxonomyCache = JSON.parse(fs.readFileSync(filePath, 'utf8'));
13
+ } else {
14
+ taxonomyCache = { genres: [] };
15
+ }
16
+ } catch (e) {
17
+ taxonomyCache = { genres: [] };
18
+ }
19
+ return taxonomyCache;
20
+ }
21
+
22
+ function buildIndex() {
23
+ if (indexCache) return indexCache;
24
+ const data = loadTaxonomy();
25
+ const genresMap = new Map();
26
+ const subgenresMap = new Map();
27
+ const tagsMap = new Map();
28
+
29
+ for (const g of data.genres || []) {
30
+ const genreObj = {
31
+ id: g.id,
32
+ name: g.name,
33
+ type: g.type || 'App',
34
+ subgenres: new Map(),
35
+ };
36
+ genresMap.set(g.id, genreObj);
37
+
38
+ for (const sg of g.subgenres || []) {
39
+ const subObj = {
40
+ id: sg.id,
41
+ name: sg.name,
42
+ genre_id: g.id,
43
+ genre_name: g.name,
44
+ genre_type: g.type || 'App',
45
+ tag_categories: sg.tag_categories || {},
46
+ };
47
+ genreObj.subgenres.set(sg.id, subObj);
48
+ subgenresMap.set(sg.id, subObj);
49
+
50
+ if (sg.tag_categories) {
51
+ for (const [catKey, tagList] of Object.entries(sg.tag_categories)) {
52
+ if (Array.isArray(tagList)) {
53
+ for (const t of tagList) {
54
+ tagsMap.set(t.tag_id, {
55
+ tag_id: t.tag_id,
56
+ name: t.name,
57
+ category: catKey,
58
+ subgenre_id: sg.id,
59
+ subgenre_name: sg.name,
60
+ genre_id: g.id,
61
+ genre_name: g.name,
62
+ genre_type: g.type || 'App',
63
+ });
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ indexCache = { genresMap, subgenresMap, tagsMap };
72
+ return indexCache;
73
+ }
74
+
75
+ const CATEGORY_DISPLAY_NAMES = {
76
+ core_features: 'Core Features',
77
+ monetization: 'Monetization',
78
+ social_engagement: 'Social & Growth',
79
+ technical_platform: 'Technical Platform',
80
+ content_theme: 'Content & Compliance',
81
+ };
82
+
83
+ /**
84
+ * Automatically infer 4-level taxonomy and feature tags from project context
85
+ */
86
+ function inferTaxonomy(projectContext = {}) {
87
+ const { genresMap, subgenresMap, tagsMap } = buildIndex();
88
+ const {
89
+ name = '',
90
+ description = '',
91
+ keywords = [],
92
+ dependencies = {},
93
+ devDependencies = {},
94
+ framework = '',
95
+ } = projectContext;
96
+
97
+ const allDeps = Object.assign({}, dependencies, devDependencies);
98
+ const depKeys = Object.keys(allDeps).map((k) => k.toLowerCase());
99
+ const combinedText = `${name} ${description} ${(keywords || []).join(' ')} ${framework}`.toLowerCase();
100
+
101
+ let matchedGenreId = 'utilities';
102
+ let matchedSubgenreId = 'developer_tools';
103
+ const matchedTags = new Set();
104
+
105
+ // 1. Game detection
106
+ const isGame =
107
+ depKeys.some((d) => ['phaser', 'pixi.js', 'three', 'kaboom', 'melonjs'].includes(d)) ||
108
+ /\b(game|arcade|rpg|puzzle|casual|roguelike|fps|chess|cards)\b/.test(combinedText);
109
+
110
+ if (isGame) {
111
+ matchedGenreId = 'game_casual_puzzle';
112
+ matchedSubgenreId = 'merge_mechanic_puzzle';
113
+ } else {
114
+ // App branch inference
115
+ if (/\b(vpn|proxy|shadowsocks|v2ray|wireguard|clash)\b/.test(combinedText)) {
116
+ matchedGenreId = 'utilities';
117
+ matchedSubgenreId = 'net_vpn_proxy';
118
+ } else if (/\b(clean|booster|battery|ram|storage|cache)\b/.test(combinedText)) {
119
+ matchedGenreId = 'utilities';
120
+ matchedSubgenreId = 'system_cleaner_optimizer';
121
+ } else if (/\b(calc|turnip|price|profit|currency|converter|calculator|finance|financial|stock|crypto|accounting|tax|invoice|budget)\b/.test(combinedText)) {
122
+ matchedGenreId = 'finance_fintech';
123
+ if (/\b(stock|trading|invest)\b/.test(combinedText)) {
124
+ matchedSubgenreId = 'stock_trading_investment';
125
+ } else if (/\b(crypto|wallet|exchange|bitcoin|eth)\b/.test(combinedText)) {
126
+ matchedSubgenreId = 'crypto_wallet_exchange';
127
+ } else {
128
+ matchedSubgenreId = 'personal_budgeting_accounting';
129
+ }
130
+ } else if (/\b(shop|store|ecommerce|cart|checkout|product|shopify|order|retail)\b/.test(combinedText)) {
131
+ matchedGenreId = 'shopping_ecommerce';
132
+ matchedSubgenreId = 'marketplace_general_retail';
133
+ } else if (/\b(chat|message|social|community|forum|feed|comment|social_media|dating)\b/.test(combinedText)) {
134
+ matchedGenreId = 'social';
135
+ matchedSubgenreId = 'interest_community_forum';
136
+ } else if (/\b(team|crm|sales|hrm|agile|collaboration|meeting|pipeline)\b/.test(combinedText)) {
137
+ matchedGenreId = 'business';
138
+ matchedSubgenreId = 'project_agile_management';
139
+ } else if (/\b(task|todo|note|docs|editor|kanban|calendar|workflow|productivity|markdown|pomodoro)\b/.test(combinedText)) {
140
+ matchedGenreId = 'productivity';
141
+ matchedSubgenreId = 'todo_task_management';
142
+ } else if (/\b(video|audio|music|media|player|streaming|podcast|stream|record|camera|photo)\b/.test(combinedText)) {
143
+ matchedGenreId = 'photo_and_video';
144
+ matchedSubgenreId = 'short_video_creation_editor';
145
+ } else {
146
+ matchedGenreId = 'utilities';
147
+ matchedSubgenreId = 'browser_web_utility';
148
+ }
149
+ }
150
+
151
+ // Ensure valid genre and subgenre
152
+ if (!genresMap.has(matchedGenreId)) {
153
+ matchedGenreId = genresMap.keys().next().value || 'utilities';
154
+ }
155
+ const currentGenreObj = genresMap.get(matchedGenreId);
156
+ if (!currentGenreObj.subgenres.has(matchedSubgenreId)) {
157
+ matchedSubgenreId = currentGenreObj.subgenres.keys().next().value || 'developer_tools';
158
+ }
159
+
160
+ const currentSubObj = currentGenreObj.subgenres.get(matchedSubgenreId) || subgenresMap.get(matchedSubgenreId);
161
+
162
+ // 2. Pick feature tags from categories
163
+ if (currentSubObj && currentSubObj.tag_categories) {
164
+ const cats = currentSubObj.tag_categories;
165
+
166
+ // A. Core Features (select 2~3)
167
+ const coreList = cats.core_features || [];
168
+ if (coreList.length > 0) {
169
+ let added = 0;
170
+ for (const tag of coreList) {
171
+ const tagWords = tag.tag_id.split('_').concat(tag.name.toLowerCase().split(/\s+/));
172
+ if (tagWords.some((w) => w.length > 3 && combinedText.includes(w))) {
173
+ matchedTags.add(tag.tag_id);
174
+ added++;
175
+ if (added >= 3) break;
176
+ }
177
+ }
178
+ if (added === 0) {
179
+ for (let i = 0; i < Math.min(2, coreList.length); i++) {
180
+ matchedTags.add(coreList[i].tag_id);
181
+ }
182
+ }
183
+ }
184
+
185
+ // B. Monetization (select 1)
186
+ const monList = cats.monetization || [];
187
+ if (monList.length > 0) {
188
+ let monTag = null;
189
+ if (depKeys.some((d) => d.includes('stripe') || d.includes('lemon') || d.includes('paddle') || d.includes('paypal'))) {
190
+ monTag = monList.find((t) => t.tag_id.includes('sub') || t.tag_id.includes('vip') || t.name.toLowerCase().includes('subscription')) || monList[0];
191
+ } else if (depKeys.some((d) => d.includes('admob') || d.includes('ads') || d.includes('google-ad'))) {
192
+ monTag = monList.find((t) => t.tag_id.includes('ad') || t.name.toLowerCase().includes('ad')) || monList[0];
193
+ } else {
194
+ monTag = monList[0];
195
+ }
196
+ if (monTag) matchedTags.add(monTag.tag_id);
197
+ }
198
+
199
+ // C. Technical Platform (select 1)
200
+ const techList = cats.technical_platform || [];
201
+ if (techList.length > 0) {
202
+ let techTag = techList[0];
203
+ for (const t of techList) {
204
+ const tWords = t.tag_id.split('_').concat(t.name.toLowerCase().split(/\s+/));
205
+ if (tWords.some((w) => w.length > 3 && combinedText.includes(w))) {
206
+ techTag = t;
207
+ break;
208
+ }
209
+ }
210
+ if (techTag) matchedTags.add(techTag.tag_id);
211
+ }
212
+
213
+ // D. Social Engagement (select 1)
214
+ const socList = cats.social_engagement || [];
215
+ if (socList.length > 0) {
216
+ matchedTags.add(socList[0].tag_id);
217
+ }
218
+ }
219
+
220
+ // Assemble tag details
221
+ const tagDetails = [];
222
+ for (const tagId of matchedTags) {
223
+ const info = tagsMap.get(tagId);
224
+ if (info) {
225
+ tagDetails.push({
226
+ tag_id: info.tag_id,
227
+ name: info.name,
228
+ category: info.category,
229
+ category_name: CATEGORY_DISPLAY_NAMES[info.category] || info.category,
230
+ });
231
+ } else {
232
+ tagDetails.push({
233
+ tag_id: tagId,
234
+ name: tagId.replace(/_/g, ' '),
235
+ category: 'core_features',
236
+ category_name: 'Core Features',
237
+ });
238
+ }
239
+ }
240
+
241
+ return {
242
+ genre: {
243
+ id: currentGenreObj?.id || matchedGenreId,
244
+ name: currentGenreObj?.name || 'Utilities',
245
+ type: currentGenreObj?.type || 'App',
246
+ },
247
+ subgenre: {
248
+ id: currentSubObj?.id || matchedSubgenreId,
249
+ name: currentSubObj?.name || 'Developer Tools',
250
+ },
251
+ feature_tags: tagDetails,
252
+ feature_tag_ids: Array.from(matchedTags),
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Format taxonomy summary into 4-Level structure
258
+ */
259
+ function formatTaxonomySummary(taxonomy) {
260
+ if (!taxonomy || !taxonomy.genre) return '';
261
+
262
+ const genreStr = `${taxonomy.genre.name} (${taxonomy.genre.type})`;
263
+ const subgenreStr = taxonomy.subgenre ? taxonomy.subgenre.name : '';
264
+
265
+ const categorized = {};
266
+ for (const t of taxonomy.feature_tags || []) {
267
+ const cat = t.category || 'core_features';
268
+ if (!categorized[cat]) categorized[cat] = [];
269
+ categorized[cat].push(`${t.tag_id} (${t.name})`);
270
+ }
271
+
272
+ const lines = [
273
+ ` • Genre (L1): ${genreStr}`,
274
+ ` • Sub-genre (L2): ${subgenreStr}`,
275
+ ` • Feature Tags:`,
276
+ ];
277
+
278
+ for (const [catKey, items] of Object.entries(categorized)) {
279
+ const catName = CATEGORY_DISPLAY_NAMES[catKey] || catKey;
280
+ lines.push(` - [${catName}]: ${items.join(', ')}`);
281
+ }
282
+
283
+ return lines.join('\n');
284
+ }
285
+
286
+ /**
287
+ * Resolve taxonomy from genreId, subgenreId, and tagIdList
288
+ */
289
+ function resolveTaxonomyFromIDs(genreId, subgenreId, tagIdList = []) {
290
+ const { genresMap, subgenresMap, tagsMap } = buildIndex();
291
+
292
+ let genre = null;
293
+ let subgenre = null;
294
+
295
+ if (genreId && genresMap.has(genreId)) {
296
+ genre = genresMap.get(genreId);
297
+ }
298
+ if (subgenreId && subgenresMap.has(subgenreId)) {
299
+ subgenre = subgenresMap.get(subgenreId);
300
+ if (!genre && subgenre.genre_id && genresMap.has(subgenre.genre_id)) {
301
+ genre = genresMap.get(subgenre.genre_id);
302
+ }
303
+ }
304
+
305
+ if (!genre) {
306
+ genre = genresMap.get('utilities') || { id: 'utilities', name: 'Utilities', type: 'App' };
307
+ }
308
+ if (!subgenre) {
309
+ subgenre = subgenresMap.get('developer_tools') || { id: 'developer_tools', name: 'Developer Tools' };
310
+ }
311
+
312
+ const feature_tags = [];
313
+ for (const tid of tagIdList) {
314
+ if (tagsMap.has(tid)) {
315
+ const t = tagsMap.get(tid);
316
+ feature_tags.push({
317
+ tag_id: t.tag_id,
318
+ name: t.name,
319
+ category: t.category,
320
+ category_name: CATEGORY_DISPLAY_NAMES[t.category] || t.category,
321
+ });
322
+ } else {
323
+ feature_tags.push({
324
+ tag_id: tid,
325
+ name: tid.replace(/_/g, ' '),
326
+ category: 'core_features',
327
+ category_name: 'Core Features',
328
+ });
329
+ }
330
+ }
331
+
332
+ return {
333
+ genre: {
334
+ id: genre.id,
335
+ name: genre.name,
336
+ type: genre.type || 'App',
337
+ },
338
+ subgenre: {
339
+ id: subgenre.id,
340
+ name: subgenre.name,
341
+ },
342
+ feature_tags,
343
+ feature_tag_ids: tagIdList,
344
+ };
345
+ }
346
+
347
+ module.exports = {
348
+ loadTaxonomy,
349
+ buildIndex,
350
+ inferTaxonomy,
351
+ formatTaxonomySummary,
352
+ resolveTaxonomyFromIDs,
353
+ CATEGORY_DISPLAY_NAMES,
354
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@shiplens/cli",
3
+ "version": "1.2.7",
4
+ "description": "Shiplens CLI (In Development) - 15-second zero-config analytics initialization and full telemetry analysis engine",
5
+ "main": "lib/index.js",
6
+ "bin": {
7
+ "shiplens-cli": "bin/shiplens.js",
8
+ "shiplens": "bin/shiplens.js"
9
+ },
10
+ "keywords": [
11
+ "shiplens",
12
+ "analytics",
13
+ "cli",
14
+ "telemetry",
15
+ "heatmap",
16
+ "dashboard",
17
+ "growth",
18
+ "agent",
19
+ "mcp",
20
+ "prompts"
21
+ ],
22
+ "author": "Shiplens Team",
23
+ "license": "Apache-2.0",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/Hyperlong/shiplens-cli.git"
27
+ },
28
+ "homepage": "https://shiplens.dev",
29
+ "scripts": {
30
+ "test": "node test/cli.test.js"
31
+ },
32
+ "files": [
33
+ "bin",
34
+ "lib",
35
+ "prompts",
36
+ "README.md"
37
+ ],
38
+ "engines": {
39
+ "node": ">=16.0.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }
@@ -0,0 +1,21 @@
1
+ # Shiplens Prompt Libraries
2
+
3
+ This directory hosts the deterministic CLI scenario-based prompt libraries for Shiplens.
4
+
5
+ ---
6
+
7
+ ## 📁 Directory Structure
8
+
9
+ ```text
10
+ prompts/
11
+ ├── README.md # Multilingual architecture overview
12
+ ├── prompts_cli_en.md # [English - Official] Deterministic CLI execution presets (42 Scenarios)
13
+ └── ... # Future language extensions (ja, ko, de, fr, es, etc.)
14
+ ```
15
+
16
+ ---
17
+
18
+ ## ⚡ Execution & Dynamic Overrides
19
+
20
+ 1. **Deterministic Execution**: Every scenario in `prompts_cli_en.md` provides explicit, reproducible CLI commands and SQL queries paired with textbook analytical foundations.
21
+ 2. **Dynamic Overrides**: Local rules defined in `.shiplens/learnings.md` override default parameters (such as `--range`, `--grain`, or target funnel routes) during AI Agent execution.