tokens-for-good 0.4.8 → 0.4.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokens-for-good",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "type": "module",
5
5
  "description": "Donate your spare AI tokens to research nonprofits for Fierce Philanthropy",
6
6
  "homepage": "https://tokensforgood.ai",
package/src/api-client.js CHANGED
@@ -1,96 +1,110 @@
1
- // HTTP client for the Fierce Philanthropy coordination API
2
-
3
- const BASE_URL = process.env.FIERCE_API_URL || 'https://tokensforgood.ai/api';
4
-
5
- export class ApiClient {
6
- constructor(apiKey) {
7
- this.apiKey = apiKey;
8
- if (!apiKey) {
9
- throw new Error('TFG_API_KEY environment variable is required. Get your key at https://tokensforgood.ai/contribute');
10
- }
11
- }
12
-
13
- async request(method, path, body = null) {
14
- const url = `${BASE_URL}${path}`;
15
- const options = {
16
- method,
17
- headers: {
18
- 'X-TFG-Api-Key': this.apiKey,
19
- 'Content-Type': 'application/json',
20
- 'Accept': 'application/json',
21
- },
22
- signal: AbortSignal.timeout(30000),
23
- };
24
-
25
- if (body) {
26
- options.body = JSON.stringify(body);
27
- }
28
-
29
- const response = await fetch(url, options);
30
- const data = await response.json();
31
-
32
- if (!response.ok) {
33
- const error = new Error(data.error || data.message || `API error ${response.status}`);
34
- error.status = response.status;
35
- error.data = data;
36
- throw error;
37
- }
38
-
39
- return data;
40
- }
41
-
42
- async claimOrg(platform = null) {
43
- return this.request('POST', '/research/claim', { platform });
44
- }
45
-
46
- async submitReport(claimId, reportMarkdown, tokenUsage = null, metrics = null, modelUsed = null, promptVersion = null) {
47
- return this.request('POST', '/research/submit', {
48
- claim_id: claimId,
49
- report_markdown: reportMarkdown,
50
- token_usage: tokenUsage,
51
- metrics: metrics,
52
- model_used: modelUsed,
53
- prompt_version: promptVersion,
54
- });
55
- }
56
-
57
- async releaseClaim(claimId) {
58
- return this.request('POST', '/research/release', { claim_id: claimId });
59
- }
60
-
61
- async getNextPeerReview() {
62
- return this.request('GET', '/research/review/next');
63
- }
64
-
65
- async submitPeerReview(claimId, score, notes = null, updatedReport = null) {
66
- return this.request('POST', '/research/review/submit', {
67
- claim_id: claimId,
68
- score,
69
- notes,
70
- updated_report: updatedReport,
71
- });
72
- }
73
-
74
- async getStatus() {
75
- const response = await fetch(`${BASE_URL}/research/status`, {
76
- headers: { 'Accept': 'application/json' },
77
- signal: AbortSignal.timeout(15000),
78
- });
79
- if (!response.ok) {
80
- throw new Error(`Status API error ${response.status}`);
81
- }
82
- return response.json();
83
- }
84
-
85
- async getImpact() {
86
- return this.request('GET', '/research/impact');
87
- }
88
-
89
- async getNextAction() {
90
- return this.request('GET', '/research/next-action');
91
- }
92
-
93
- async enableSchedule() {
94
- return this.request('POST', '/research/enable-schedule');
95
- }
96
- }
1
+ // HTTP client for the Fierce Philanthropy coordination API
2
+
3
+ const BASE_URL = process.env.FIERCE_API_URL || 'https://tokensforgood.ai/api';
4
+
5
+ export class ApiClient {
6
+ constructor(apiKey, { version = null, platform = null, installId = null } = {}) {
7
+ this.apiKey = apiKey;
8
+ this.version = version;
9
+ this.platform = platform;
10
+ this.installId = installId;
11
+ if (!apiKey) {
12
+ throw new Error('TFG_API_KEY environment variable is required. Get your key at https://tokensforgood.ai/contribute');
13
+ }
14
+ }
15
+
16
+ // Version, platform, and install_id ride along on every request so the
17
+ // server can attribute traffic by client build, editor, and per-machine
18
+ // install without inspecting the request body.
19
+ headers(extra = {}) {
20
+ const headers = {
21
+ 'Content-Type': 'application/json',
22
+ 'Accept': 'application/json',
23
+ ...extra,
24
+ };
25
+ if (this.version) headers['X-TFG-Version'] = this.version;
26
+ if (this.platform) headers['X-TFG-Platform'] = this.platform;
27
+ if (this.installId) headers['X-TFG-Install-Id'] = this.installId;
28
+ return headers;
29
+ }
30
+
31
+ async request(method, path, body = null) {
32
+ const url = `${BASE_URL}${path}`;
33
+ const options = {
34
+ method,
35
+ headers: this.headers({ 'X-TFG-Api-Key': this.apiKey }),
36
+ signal: AbortSignal.timeout(30000),
37
+ };
38
+
39
+ if (body) {
40
+ options.body = JSON.stringify(body);
41
+ }
42
+
43
+ const response = await fetch(url, options);
44
+ const data = await response.json();
45
+
46
+ if (!response.ok) {
47
+ const error = new Error(data.error || data.message || `API error ${response.status}`);
48
+ error.status = response.status;
49
+ error.data = data;
50
+ throw error;
51
+ }
52
+
53
+ return data;
54
+ }
55
+
56
+ async claimOrg(platform = null) {
57
+ return this.request('POST', '/research/claim', { platform });
58
+ }
59
+
60
+ async submitReport(claimId, reportMarkdown, tokenUsage = null, metrics = null, modelUsed = null, promptVersion = null) {
61
+ return this.request('POST', '/research/submit', {
62
+ claim_id: claimId,
63
+ report_markdown: reportMarkdown,
64
+ token_usage: tokenUsage,
65
+ metrics: metrics,
66
+ model_used: modelUsed,
67
+ prompt_version: promptVersion,
68
+ });
69
+ }
70
+
71
+ async releaseClaim(claimId) {
72
+ return this.request('POST', '/research/release', { claim_id: claimId });
73
+ }
74
+
75
+ async getNextPeerReview() {
76
+ return this.request('GET', '/research/review/next');
77
+ }
78
+
79
+ async submitPeerReview(claimId, score, notes = null, updatedReport = null) {
80
+ return this.request('POST', '/research/review/submit', {
81
+ claim_id: claimId,
82
+ score,
83
+ notes,
84
+ updated_report: updatedReport,
85
+ });
86
+ }
87
+
88
+ async getStatus() {
89
+ const response = await fetch(`${BASE_URL}/research/status`, {
90
+ headers: this.headers(),
91
+ signal: AbortSignal.timeout(15000),
92
+ });
93
+ if (!response.ok) {
94
+ throw new Error(`Status API error ${response.status}`);
95
+ }
96
+ return response.json();
97
+ }
98
+
99
+ async getImpact() {
100
+ return this.request('GET', '/research/impact');
101
+ }
102
+
103
+ async getNextAction() {
104
+ return this.request('GET', '/research/next-action');
105
+ }
106
+
107
+ async enableSchedule() {
108
+ return this.request('POST', '/research/enable-schedule');
109
+ }
110
+ }
package/src/cli.js CHANGED
@@ -46,8 +46,9 @@ if (args.includes('-v') || args.includes('--version')) {
46
46
  runSessionStartHook();
47
47
  } else if (args.includes('--status') || first === 'status') {
48
48
  const { ApiClient } = await import('./api-client.js');
49
+ const { getOrCreateInstallId } = await import('./state.js');
49
50
  try {
50
- const client = new ApiClient(process.env.TFG_API_KEY || 'public');
51
+ const client = new ApiClient(process.env.TFG_API_KEY || 'public', { version: pkg.version, installId: getOrCreateInstallId() });
51
52
  const status = await client.getStatus();
52
53
  const sys = status.system_stats || status;
53
54
  console.log('\nTokens for Good - Project Status\n');
@@ -68,8 +69,9 @@ if (args.includes('-v') || args.includes('--version')) {
68
69
  }
69
70
  } else if (args.includes('--impact') || first === 'impact') {
70
71
  const { ApiClient } = await import('./api-client.js');
72
+ const { getOrCreateInstallId } = await import('./state.js');
71
73
  try {
72
- const client = new ApiClient(process.env.TFG_API_KEY);
74
+ const client = new ApiClient(process.env.TFG_API_KEY, { version: pkg.version, installId: getOrCreateInstallId() });
73
75
  const result = await client.getImpact();
74
76
  const c = result.contributor;
75
77
  console.log(`\nYour Impact (@${c.github_handle})\n`);
package/src/init.js CHANGED
@@ -9,7 +9,7 @@ import { homedir } from 'os';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { spawnSync } from 'child_process';
11
11
  import { detectPlatform, cronForSchedule, MAX_RUNS_PER_DAY } from './platform.js';
12
- import { loadState, saveState } from './state.js';
12
+ import { loadState, saveState, getOrCreateInstallId } from './state.js';
13
13
 
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
15
  const PKG_ROOT = join(__dirname, '..');
@@ -130,6 +130,10 @@ export async function runInit() {
130
130
  console.log(`✓ ${plans[1].label}`);
131
131
  }
132
132
 
133
+ // Generate the install_id at install time so it lines up cleanly with
134
+ // `installed_at` instead of being lazily created at first MCP call.
135
+ getOrCreateInstallId();
136
+
133
137
  saveState({
134
138
  ...loadState(),
135
139
  intended_flow,
package/src/mcp-server.js CHANGED
@@ -4,7 +4,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
4
  import { z } from 'zod';
5
5
  import { ApiClient } from './api-client.js';
6
6
  import { detectPlatform, isSchedulable, getAutomationInstructions } from './platform.js';
7
- import { loadState, updateState, isSnoozed, snoozeDays, hasContributedToday, markContributed, markSetupComplete } from './state.js';
7
+ import { loadState, updateState, isSnoozed, snoozeDays, hasContributedToday, markContributed, markSetupComplete, getOrCreateInstallId } from './state.js';
8
8
  import { readFileSync, existsSync } from 'fs';
9
9
  import { join, dirname } from 'path';
10
10
  import { fileURLToPath } from 'url';
@@ -32,17 +32,18 @@ function notInitialized() {
32
32
  }
33
33
 
34
34
  const apiKey = process.env.TFG_API_KEY;
35
+ const platform = detectPlatform();
36
+ updateState({ platform });
37
+ const installId = getOrCreateInstallId();
38
+
35
39
  let client;
36
40
  try {
37
- client = new ApiClient(apiKey);
41
+ client = new ApiClient(apiKey, { version: PKG_VERSION, platform, installId });
38
42
  } catch {
39
43
  // Will fail on tool calls, but server can still start
40
44
  client = null;
41
45
  }
42
46
 
43
- const platform = detectPlatform();
44
- updateState({ platform });
45
-
46
47
  const server = new McpServer({
47
48
  name: 'tokens-for-good',
48
49
  version: PKG_VERSION,
@@ -83,7 +84,7 @@ How it works:
83
84
 
84
85
  Research pipeline (per org, all done by your AI):
85
86
  - Research the org using web search + web fetch, following the 6-prompt methodology
86
- - Score using a weighted checklist (100 pts base, 120 max with extra credit)
87
+ - Score using a weighted 6-criterion checklist (out of 120)
87
88
  - Verify citations by visiting each URL before submitting
88
89
  - Clean up writing style (no AI tells, no filler adjectives, no em dashes)
89
90
 
@@ -223,7 +224,7 @@ server.tool('submit_peer_review', 'Submit your peer review score for a report.',
223
224
 
224
225
  server.tool('research_status', 'See the overall Tokens for Good project progress and leaderboard.', {}, async () => {
225
226
  try {
226
- const clientForStatus = client || new ApiClient('dummy'); // Status is public
227
+ const clientForStatus = client || new ApiClient('dummy', { version: PKG_VERSION, platform, installId }); // Status is public
227
228
  const result = await clientForStatus.getStatus();
228
229
  const sys = result.system_stats || result;
229
230
  const queue = result.queue_status || result.queue || {};
package/src/state.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { homedir } from 'os';
5
+ import { randomUUID } from 'crypto';
5
6
 
6
7
  const STATE_DIR = join(homedir(), '.tokens-for-good');
7
8
  const STATE_FILE = join(STATE_DIR, 'state.json');
@@ -17,6 +18,7 @@ const DEFAULT_STATE = {
17
18
  runs_per_day: null, // 1-15 when intended_frequency === 'daily', else null
18
19
  first_setup_complete: false, // flipped by mark_setup_complete tool after first scheduled run or first one-off submit
19
20
  installed_at: null, // ISO timestamp when init finished
21
+ install_id: null, // stable per-machine UUID sent as X-TFG-Install-Id header
20
22
  };
21
23
 
22
24
  export function loadState() {
@@ -84,3 +86,16 @@ export function isInitialized() {
84
86
  const state = loadState();
85
87
  return state.intended_flow !== null;
86
88
  }
89
+
90
+ // Lazily generate and persist a stable per-machine install ID the first time
91
+ // it's needed. Sent to the server as X-TFG-Install-Id on every request so
92
+ // distinct installs can be counted separately from API keys (one key, N
93
+ // machines). Existing users get an ID generated on their next session;
94
+ // the server treats first-seen as install/activation.
95
+ export function getOrCreateInstallId() {
96
+ const state = loadState();
97
+ if (state.install_id) return state.install_id;
98
+ const id = randomUUID();
99
+ updateState({ install_id: id });
100
+ return id;
101
+ }