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 +1 -1
- package/src/api-client.js +110 -96
- package/src/cli.js +4 -2
- package/src/init.js +5 -1
- package/src/mcp-server.js +8 -7
- package/src/state.js +15 -0
package/package.json
CHANGED
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 (
|
|
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
|
+
}
|