@prasenjeet/shipli 1.1.0 → 1.2.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.
package/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@prasenjeet/shipli)](https://www.npmjs.com/package/@prasenjeet/shipli)
4
4
  [![license](https://img.shields.io/npm/l/@prasenjeet/shipli)](LICENSE)
5
5
 
6
+ **Website:** [https://prasenjeet-symon.github.io/shipli-ai/](https://prasenjeet-symon.github.io/shipli-ai/)
7
+
6
8
  Store rejections cost days of development time. **Shipli** is a CLI tool that audits your Flutter source code against **Apple App Store** and **Google Play** guidelines using AI.
7
9
 
8
10
  Catch missing permissions, policy violations, and compliance issues before you submit.
@@ -146,9 +148,41 @@ Use `shipli config` to update settings anytime.
146
148
 
147
149
  Shipli includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server, so AI coding assistants like Claude Code, Cursor, and Windsurf can run audits directly inside your editor.
148
150
 
149
- ### Setup
151
+ ### Step 1 — Install globally
152
+
153
+ ```bash
154
+ npm install -g @prasenjeet/shipli
155
+ ```
156
+
157
+ Verify: `shipli --version` and `which shipli-mcp`.
158
+
159
+ ### Step 2 — Add the MCP server
160
+
161
+ **Option A: Claude Code CLI (recommended)**
162
+
163
+ ```bash
164
+ # Using Claude as the AI provider
165
+ claude mcp add shipli \
166
+ --transport stdio \
167
+ --env SHIPLI_PROVIDER=claude \
168
+ --env SHIPLI_MODEL=claude-haiku-4-5 \
169
+ --env ANTHROPIC_API_KEY=your-api-key \
170
+ -- shipli-mcp
171
+ ```
172
+
173
+ ```bash
174
+ # Using Gemini as the AI provider
175
+ claude mcp add shipli \
176
+ --transport stdio \
177
+ --env SHIPLI_PROVIDER=gemini \
178
+ --env SHIPLI_MODEL=gemini-2.5-flash \
179
+ --env GEMINI_API_KEY=your-api-key \
180
+ -- shipli-mcp
181
+ ```
182
+
183
+ **Option B: Manual JSON config**
150
184
 
151
- Add to your MCP config (e.g. `~/.claude/.mcp.json`):
185
+ Add to `~/.claude/.mcp.json`:
152
186
 
153
187
  ```json
154
188
  {
@@ -165,7 +199,11 @@ Add to your MCP config (e.g. `~/.claude/.mcp.json`):
165
199
  }
166
200
  ```
167
201
 
168
- For Gemini, use `SHIPLI_PROVIDER: "gemini"` and set `GEMINI_API_KEY` instead.
202
+ For Cursor/Windsurf, add the same config to your editor's MCP settings.
203
+
204
+ ### Step 3 — Restart and verify
205
+
206
+ Restart Claude Code (or run `/mcp` in chat) to pick up the new server. You should see `shipli` listed with its two tools.
169
207
 
170
208
  ### Available Tools
171
209
 
@@ -197,6 +235,24 @@ The assistant will call the appropriate Shipli MCP tool and return the results i
197
235
 
198
236
  The CLI exits with code `1` on failure, making it easy to gate deployments.
199
237
 
238
+ ## Telemetry
239
+
240
+ Shipli collects anonymous usage metrics to help improve the tool. No project data, file contents, API keys, or personally identifiable information is ever collected.
241
+
242
+ **What's collected:** audit mode, platform, provider, model, pass/fail score, duration, token counts, OS, and CLI version.
243
+
244
+ **Opt out** using any of these methods:
245
+
246
+ ```bash
247
+ # Environment variable
248
+ export SHIPLI_TELEMETRY=off
249
+
250
+ # Or use the Do Not Track standard
251
+ export DO_NOT_TRACK=1
252
+ ```
253
+
254
+ Or add `"telemetry": false` to your `~/.shipli` config file.
255
+
200
256
  ## License
201
257
 
202
258
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prasenjeet/shipli",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "AI-powered App Store review audit for Flutter projects. Catch rejections before you submit.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,9 +28,10 @@
28
28
  ],
29
29
  "author": "",
30
30
  "license": "MIT",
31
+ "homepage": "https://prasenjeet-symon.github.io/shipli-ai/",
31
32
  "repository": {
32
33
  "type": "git",
33
- "url": ""
34
+ "url": "https://github.com/prasenjeet-symon/shipli-ai"
34
35
  },
35
36
  "dependencies": {
36
37
  "@modelcontextprotocol/sdk": "^1.27.1",
package/src/index.js CHANGED
@@ -18,6 +18,7 @@ import { audit } from './auditor.js';
18
18
  import { fetchGuidelines } from './guidelines.js';
19
19
  import { print as printReport } from './reporter.js';
20
20
  import { PROVIDER_DEFAULTS as DEFAULTS, detectPlatform } from './defaults.js';
21
+ import { trackEvent } from './telemetry.js';
21
22
 
22
23
  // Read package version
23
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -113,6 +114,7 @@ program
113
114
 
114
115
  const platformLabel = { ios: 'iOS', android: 'Android', both: 'iOS + Android' }[platform];
115
116
  let spinner = ora({ text: `Scanning Flutter project (${platformLabel})...`, color: 'cyan' }).start();
117
+ const startTime = Date.now();
116
118
 
117
119
  try {
118
120
  // 1. Read pubspec.yaml first (needed for type detection)
@@ -205,11 +207,23 @@ program
205
207
  platform,
206
208
  });
207
209
 
208
- // 9. Exit with appropriate code
209
- process.exit(result.score === 'FAIL' ? 1 : 0);
210
+ // 9. Track and exit
211
+ trackEvent('audit_completed', {
212
+ source: 'cli', mode, platform, provider, model,
213
+ project_type: projectType, score: result.score,
214
+ duration_ms: Date.now() - startTime,
215
+ tokens_input: result._tokens?.actual?.input ?? null,
216
+ tokens_output: result._tokens?.actual?.output ?? null,
217
+ });
218
+ process.exitCode = result.score === 'FAIL' ? 1 : 0;
210
219
  } catch (err) {
211
220
  spinner.fail(chalk.red(err.message));
212
- process.exit(1);
221
+ trackEvent('audit_error', {
222
+ source: 'cli', mode, platform, provider, model,
223
+ duration_ms: Date.now() - startTime,
224
+ error_message: err.message.slice(0, 200),
225
+ });
226
+ process.exitCode = 1;
213
227
  }
214
228
  });
215
229
 
package/src/mcp-server.js CHANGED
@@ -15,6 +15,7 @@ import { fetchGuidelines } from './guidelines.js';
15
15
  import { audit } from './auditor.js';
16
16
  import { loadConfig } from './config.js';
17
17
  import { PROVIDER_DEFAULTS, detectPlatform } from './defaults.js';
18
+ import { trackEvent } from './telemetry.js';
18
19
 
19
20
  // ── Read package version ──
20
21
 
@@ -66,12 +67,13 @@ const server = new McpServer({
66
67
 
67
68
  server.tool(
68
69
  'shipli_store_audit',
69
- 'Run a store compliance audit on a Flutter project against Apple App Store and/or Google Play guidelines. Returns structured findings with PASS/WARNING/FAIL scores and specific guideline citations.',
70
+ 'Run a store compliance audit on a Flutter project against Apple App Store and/or Google Play guidelines. Scans Dart source files, pubspec.yaml, Info.plist (iOS), and AndroidManifest.xml (Android). Returns structured JSON with PASS/WARNING/FAIL scores, specific guideline citations, and actionable fix suggestions. Supports both Flutter apps and packages.',
70
71
  {
71
- projectDir: z.string().describe('Absolute path to the Flutter project root directory'),
72
- platform: z.enum(['ios', 'android', 'both']).optional().describe('Target platform. Auto-detected from project structure if omitted.'),
72
+ projectDir: z.string().describe('Absolute path to the Flutter project root directory. Must contain a pubspec.yaml and a lib/ folder. Example: "/Users/you/projects/my-flutter-app"'),
73
+ platform: z.enum(['ios', 'android', 'both']).optional().describe('Target platform: "ios" (App Store only), "android" (Play Store only), or "both" (both stores). Auto-detected from ios/ and android/ directory presence if omitted.'),
73
74
  },
74
75
  async ({ projectDir, platform }) => {
76
+ const startTime = Date.now();
75
77
  try {
76
78
  const dir = validateProject(projectDir);
77
79
  const { provider, model, apiKey } = resolveConfig(dir);
@@ -124,10 +126,23 @@ server.tool(
124
126
  { apiKey, model, provider, mode: 'store', platform: resolvedPlatform },
125
127
  );
126
128
 
129
+ trackEvent('audit_completed', {
130
+ source: 'mcp', mode: 'store', platform: resolvedPlatform,
131
+ provider, model, project_type: projectType, score: result.score,
132
+ duration_ms: Date.now() - startTime,
133
+ tokens_input: result._tokens?.actual?.input ?? null,
134
+ tokens_output: result._tokens?.actual?.output ?? null,
135
+ });
136
+
127
137
  return {
128
138
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
129
139
  };
130
140
  } catch (err) {
141
+ trackEvent('audit_error', {
142
+ source: 'mcp', mode: 'store',
143
+ duration_ms: Date.now() - startTime,
144
+ error_message: err.message.slice(0, 200),
145
+ });
131
146
  return {
132
147
  content: [{ type: 'text', text: `Error: ${err.message}` }],
133
148
  isError: true,
@@ -140,11 +155,12 @@ server.tool(
140
155
 
141
156
  server.tool(
142
157
  'shipli_code_review',
143
- 'Run a code quality and security review on a Flutter project. Checks architecture, error handling, performance, dependencies, and security vulnerabilities.',
158
+ 'Run a code quality and security review on a Flutter project. Scans all Dart source files and pubspec.yaml. Checks architecture patterns, error handling, performance issues, dependency hygiene, and security vulnerabilities. Returns structured JSON with findings, severity levels, and fix suggestions. Supports both Flutter apps and packages (including example/ directories).',
144
159
  {
145
- projectDir: z.string().describe('Absolute path to the Flutter project root directory'),
160
+ projectDir: z.string().describe('Absolute path to the Flutter project root directory. Must contain a pubspec.yaml and a lib/ folder. Example: "/Users/you/projects/my-flutter-app"'),
146
161
  },
147
162
  async ({ projectDir }) => {
163
+ const startTime = Date.now();
148
164
  try {
149
165
  const dir = validateProject(projectDir);
150
166
  const { provider, model, apiKey } = resolveConfig(dir);
@@ -176,10 +192,23 @@ server.tool(
176
192
  { apiKey, model, provider, mode: 'code', platform: 'both' },
177
193
  );
178
194
 
195
+ trackEvent('audit_completed', {
196
+ source: 'mcp', mode: 'code', platform: 'both',
197
+ provider, model, project_type: projectType, score: result.score,
198
+ duration_ms: Date.now() - startTime,
199
+ tokens_input: result._tokens?.actual?.input ?? null,
200
+ tokens_output: result._tokens?.actual?.output ?? null,
201
+ });
202
+
179
203
  return {
180
204
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
181
205
  };
182
206
  } catch (err) {
207
+ trackEvent('audit_error', {
208
+ source: 'mcp', mode: 'code',
209
+ duration_ms: Date.now() - startTime,
210
+ error_message: err.message.slice(0, 200),
211
+ });
183
212
  return {
184
213
  content: [{ type: 'text', text: `Error: ${err.message}` }],
185
214
  isError: true,
@@ -0,0 +1,83 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { hostname, userInfo, platform, arch } from 'node:os';
3
+ import { readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { homedir } from 'node:os';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { dirname } from 'node:path';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
11
+
12
+ const POSTHOG_ENDPOINT = 'https://us.i.posthog.com/capture';
13
+ const POSTHOG_API_KEY = 'phc_JcSoYrxMXvJjCtlePCFI6UuhNbwq33WXNeUKmDB6Msz';
14
+
15
+ // ── Opt-out check (lazy, cached) ──
16
+
17
+ let _disabled = null;
18
+
19
+ function isDisabled() {
20
+ if (_disabled !== null) return _disabled;
21
+
22
+ const envVal = (process.env.SHIPLI_TELEMETRY || '').toLowerCase();
23
+ if (['off', 'false', '0', 'no'].includes(envVal)) {
24
+ _disabled = true;
25
+ return true;
26
+ }
27
+
28
+ if (process.env.DO_NOT_TRACK === '1') {
29
+ _disabled = true;
30
+ return true;
31
+ }
32
+
33
+ try {
34
+ const raw = readFileSync(join(homedir(), '.shipli'), 'utf-8');
35
+ const config = JSON.parse(raw);
36
+ if (config.telemetry === false) {
37
+ _disabled = true;
38
+ return true;
39
+ }
40
+ } catch {
41
+ // No config file or invalid JSON — telemetry stays on
42
+ }
43
+
44
+ _disabled = false;
45
+ return false;
46
+ }
47
+
48
+ // ── Anonymous device ID ──
49
+
50
+ function getAnonymousId() {
51
+ try {
52
+ const raw = `${hostname()}:${userInfo().username}`;
53
+ return createHash('sha256').update(raw).digest('hex').slice(0, 16);
54
+ } catch {
55
+ return createHash('sha256').update(hostname()).digest('hex').slice(0, 16);
56
+ }
57
+ }
58
+
59
+ // ── Fire-and-forget event dispatch ──
60
+
61
+ export function trackEvent(name, properties = {}) {
62
+ if (isDisabled()) return;
63
+
64
+ const payload = {
65
+ api_key: POSTHOG_API_KEY,
66
+ event: name,
67
+ distinct_id: getAnonymousId(),
68
+ properties: {
69
+ ...properties,
70
+ cli_version: pkg.version,
71
+ node_version: process.version,
72
+ os: platform(),
73
+ arch: arch(),
74
+ },
75
+ };
76
+
77
+ fetch(POSTHOG_ENDPOINT, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify(payload),
81
+ signal: AbortSignal.timeout(3000),
82
+ }).catch(() => {});
83
+ }