Skip to content

Commit 968d5a3

Browse files
authored
fix: deliver CLI and MCP analytics (#130)
1 parent 8317d5c commit 968d5a3

15 files changed

Lines changed: 100 additions & 25 deletions

File tree

.github/workflows/publish-packages.yml

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,14 @@ jobs:
7777

7878
- name: Publish @graspful/shared
7979
if: github.event.inputs.dry_run != 'true'
80-
run: cd packages/shared && npm publish --access public || echo "shared already published at this version"
80+
run: |
81+
cd packages/shared
82+
VERSION=$(node -p "require('./package.json').version")
83+
if npm view "@graspful/shared@$VERSION" version >/dev/null 2>&1; then
84+
echo "@graspful/shared@$VERSION is already published"
85+
else
86+
npm publish --access public
87+
fi
8188
env:
8289
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
8390

@@ -91,13 +98,27 @@ jobs:
9198

9299
- name: Publish @graspful/cli
93100
if: (steps.target.outputs.target == 'all' || steps.target.outputs.target == 'cli') && github.event.inputs.dry_run != 'true'
94-
run: cd packages/cli && npm publish --access public || echo "cli already published at this version"
101+
run: |
102+
cd packages/cli
103+
VERSION=$(node -p "require('./package.json').version")
104+
if npm view "@graspful/cli@$VERSION" version >/dev/null 2>&1; then
105+
echo "@graspful/cli@$VERSION is already published"
106+
else
107+
npm publish --access public
108+
fi
95109
env:
96110
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
97111

98112
- name: Publish @graspful/mcp
99113
if: (steps.target.outputs.target == 'all' || steps.target.outputs.target == 'mcp') && github.event.inputs.dry_run != 'true'
100-
run: cd packages/mcp && npm publish --access public || echo "mcp already published at this version"
114+
run: |
115+
cd packages/mcp
116+
VERSION=$(node -p "require('./package.json').version")
117+
if npm view "@graspful/mcp@$VERSION" version >/dev/null 2>&1; then
118+
echo "@graspful/mcp@$VERSION is already published"
119+
else
120+
npm publish --access public
121+
fi
101122
env:
102123
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
103124

bun.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,10 @@ graspful import course.yaml --org my-org
329329
|----------|-------------|
330330
| `GRASPFUL_API_KEY` | API key for authenticated commands (`import`, `publish`) |
331331
| `GRASPFUL_API_URL` | API base URL (default: `https://api.graspful.ai`) |
332+
| `GRASPFUL_USER_ID` | Optional Graspful user ID for analytics identity continuity |
333+
| `GRASPFUL_TELEMETRY_DISABLED` | Set to `1` to disable anonymous product analytics |
334+
335+
The CLI sends command usage and outcome metadata to help improve Graspful. It never sends API keys or YAML course bodies. Set `GRASPFUL_TELEMETRY_DISABLED=1` to disable this data collection.
332336

333337
## Links
334338

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@graspful/cli",
3-
"version": "0.2.6",
3+
"version": "0.2.7",
44
"description": "Create adaptive learning courses from YAML. CLI and MCP server for AI agents.",
55
"keywords": ["course", "learning", "adaptive", "mcp", "mcp-server", "ai", "ai-agent", "agent", "cli", "education", "edtech", "knowledge-graph", "spaced-repetition", "course-creation", "yaml", "adaptive-learning", "lms"],
66
"repository": {

packages/cli/src/commands/__tests__/login.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ describe('graspful login', () => {
116116
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
117117
const savedContent = JSON.parse(writeFileSyncSpy.mock.calls[0][1] as string);
118118
expect(savedContent.apiKey).toBe('gsk_browser_flow_key');
119+
expect(savedContent.userId).toBe('user-123');
119120
expect(savedContent.baseUrl).toBe('http://localhost:3000');
120121
});
121122
});

packages/cli/src/commands/init.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function detectEditors(): Editor[] {
4545
return editors;
4646
}
4747

48-
function writeMcpConfig(configPath: string, apiKey: string): void {
48+
function writeMcpConfig(configPath: string, apiKey: string, userId?: string): void {
4949
const dir = path.dirname(configPath);
5050
if (!fs.existsSync(dir)) {
5151
fs.mkdirSync(dir, { recursive: true });
@@ -64,7 +64,10 @@ function writeMcpConfig(configPath: string, apiKey: string): void {
6464
mcpServers['graspful'] = {
6565
command: 'npx',
6666
args: ['-y', '@graspful/mcp'],
67-
env: { GRASPFUL_API_KEY: apiKey },
67+
env: {
68+
GRASPFUL_API_KEY: apiKey,
69+
...(userId ? { GRASPFUL_USER_ID: userId } : {}),
70+
},
6871
};
6972
existing.mcpServers = mcpServers;
7073

@@ -91,7 +94,7 @@ export function registerInitCommand(program: Command) {
9194

9295
// Still configure MCP if requested
9396
if (opts.mcp) {
94-
configureMcp(existingCreds.apiKey);
97+
configureMcp(existingCreds.apiKey, existingCreds.userId);
9598
}
9699

97100
output(
@@ -120,7 +123,7 @@ export function registerInitCommand(program: Command) {
120123

121124
// ── Configure MCP ───────────────────────────────────────────────
122125
if (opts.mcp) {
123-
configureMcp(data.apiKey);
126+
configureMcp(data.apiKey, data.userId);
124127
}
125128

126129
output(
@@ -147,7 +150,7 @@ export function registerInitCommand(program: Command) {
147150
});
148151
}
149152

150-
function configureMcp(apiKey: string): void {
153+
function configureMcp(apiKey: string, userId?: string): void {
151154
const editors = detectEditors();
152155

153156
if (editors.length === 0) {
@@ -157,15 +160,18 @@ function configureMcp(apiKey: string): void {
157160
graspful: {
158161
command: 'npx',
159162
args: ['-y', '@graspful/mcp'],
160-
env: { GRASPFUL_API_KEY: apiKey },
163+
env: {
164+
GRASPFUL_API_KEY: apiKey,
165+
...(userId ? { GRASPFUL_USER_ID: userId } : {}),
166+
},
161167
},
162168
},
163169
}, null, 2));
164170
return;
165171
}
166172

167173
for (const editor of editors) {
168-
writeMcpConfig(editor.configPath, apiKey);
174+
writeMcpConfig(editor.configPath, apiKey, userId);
169175
cliCapture('cli initialized', { editor: editor.name });
170176
console.log(`\nMCP configured for ${editor.name}: ${editor.configPath}`);
171177
}

packages/cli/src/commands/login.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function registerLoginCommand(program: Command) {
5353
userId: string;
5454
};
5555

56-
saveApiKeyCredentials(apiKey, baseUrl);
56+
saveApiKeyCredentials(apiKey, baseUrl, userId);
5757
cliCapture('cli logged in', { method: 'email-password' });
5858
output(
5959
{ authenticated: true, baseUrl, tokenType: 'apiKey', orgSlug, userId },
@@ -70,6 +70,7 @@ export function registerLoginCommand(program: Command) {
7070
noBrowser: opts.browser === false,
7171
});
7272

73+
cliCapture('cli logged in', { method: 'browser-auth' });
7374
output(
7475
{
7576
authenticated: true,

packages/cli/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const program = new Command();
2020
program
2121
.name('graspful')
2222
.description('Create adaptive learning courses from YAML')
23-
.version('0.1.0')
23+
.version('0.2.7')
2424
.option('--format <format>', 'Output format: human or json', 'human')
2525
.hook('preAction', (thisCommand) => {
2626
const opts = thisCommand.opts();

packages/cli/src/lib/__tests__/analytics.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ describe('cliDistinctId', () => {
4848
);
4949
});
5050

51+
test('uses the stored user ID before hashing stored credentials', () => {
52+
delete process.env.GRASPFUL_USER_ID;
53+
delete process.env.GRASPFUL_API_KEY;
54+
55+
expect(cliDistinctId({ apiKey: 'gsk_stored', userId: 'user-stored' })).toBe(
56+
'user-stored',
57+
);
58+
});
59+
5160
test('uses a stable identifier for the current anonymous process', () => {
5261
delete process.env.GRASPFUL_USER_ID;
5362
delete process.env.GRASPFUL_API_KEY;

packages/cli/src/lib/analytics.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ import * as path from 'node:path';
66
import type { Credentials } from './auth';
77
import { resolveCredentials } from './auth';
88

9-
const posthogKey = process.env.POSTHOG_API_KEY || process.env.NEXT_PUBLIC_POSTHOG_KEY;
9+
const DEFAULT_POSTHOG_KEY = 'phc_ahQLCJsOBzeuro1yDeurs1a3xx07pIreJWeXG9T4d4';
10+
const telemetryDisabled =
11+
process.env.GRASPFUL_TELEMETRY_DISABLED === '1' ||
12+
process.env.NODE_ENV === 'test';
13+
const posthogKey = telemetryDisabled
14+
? null
15+
: process.env.POSTHOG_API_KEY ||
16+
process.env.NEXT_PUBLIC_POSTHOG_KEY ||
17+
DEFAULT_POSTHOG_KEY;
1018

1119
let client: PostHog | null = null;
1220
let anonymousDistinctId: string | null = null;
@@ -53,14 +61,18 @@ function getOrCreateAnonymousDistinctId(): string {
5361
}
5462

5563
export function cliDistinctId(
56-
credentials?: Pick<Credentials, 'apiKey' | 'jwt'>,
64+
credentials?: Pick<Credentials, 'apiKey' | 'jwt' | 'userId'>,
5765
fallbackAnonymousId?: string,
5866
): string {
5967
if (process.env.GRASPFUL_USER_ID) {
6068
return process.env.GRASPFUL_USER_ID;
6169
}
6270

6371
const resolvedCredentials = credentials ?? resolveCredentials();
72+
if (resolvedCredentials.userId) {
73+
return resolvedCredentials.userId;
74+
}
75+
6476
const credential =
6577
process.env.GRASPFUL_API_KEY ??
6678
resolvedCredentials.apiKey ??

0 commit comments

Comments
 (0)