@prasenjeet/shipli 1.2.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.
@@ -233,6 +235,24 @@ The assistant will call the appropriate Shipli MCP tool and return the results i
233
235
 
234
236
  The CLI exits with code `1` on failure, making it easy to gate deployments.
235
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
+
236
256
  ## License
237
257
 
238
258
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prasenjeet/shipli",
3
- "version": "1.2.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
 
@@ -72,6 +73,7 @@ server.tool(
72
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,
@@ -145,6 +160,7 @@ server.tool(
145
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
+ }