tokens-for-good 0.4.22 → 0.4.23
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 +2 -0
- package/package.json +1 -1
- package/src/api-client.js +25 -0
- package/src/api-client.test.js +35 -0
- package/src/mcp-server.js +62 -0
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ Once installed, these are available to your AI via the MCP server:
|
|
|
53
53
|
| `get_next_validation` | v3 validator: fetch both reports + cached page text to prune unsupported evidence |
|
|
54
54
|
| `submit_validation` | v3 validator: submit corrected reports (subtract/correct only, never add) |
|
|
55
55
|
| `get_next_consolidation` | v3 consolidator: fetch your assignment + both source reports to merge |
|
|
56
|
+
| `set_role_preference` | Prefer the low-fetch roles (validation/consolidation) — best for local models |
|
|
57
|
+
| `create_agent` / `list_agents` / `rotate_agent_key` / `revoke_agent` | Run several harnesses at once: each agent gets its own key + research slot |
|
|
56
58
|
| `setup_automation` | Emits `/schedule` prompt (normally called by `/tfg-schedule` skill) |
|
|
57
59
|
| `my_impact` / `research_status` / `get_badge` | Stats, leaderboard, GitHub README badge |
|
|
58
60
|
| `snooze` | Quiet the session-start prompt for N days |
|
package/package.json
CHANGED
package/src/api-client.js
CHANGED
|
@@ -95,6 +95,31 @@ export class ApiClient {
|
|
|
95
95
|
return this.request('GET', '/research/validate/next');
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
async setRolePreference(preferLowFetchRoles) {
|
|
99
|
+
return this.request('POST', '/research/agent/preferences', {
|
|
100
|
+
prefer_low_fetch_roles: !!preferLowFetchRoles,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async listAgents() {
|
|
105
|
+
return this.request('GET', '/research/agents');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async createAgent(label, preferLowFetchRoles = false) {
|
|
109
|
+
return this.request('POST', '/research/agents', {
|
|
110
|
+
label,
|
|
111
|
+
prefer_low_fetch_roles: !!preferLowFetchRoles,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async rotateAgentKey(agentId) {
|
|
116
|
+
return this.request('POST', '/research/agents/rotate', { agent_id: agentId });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async revokeAgent(agentId) {
|
|
120
|
+
return this.request('POST', '/research/agents/revoke', { agent_id: agentId });
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
async submitValidation(claimId, validatedReports, validationNotes = null, tokenUsage = null) {
|
|
99
124
|
const normalizedTokenUsage = typeof tokenUsage === 'number'
|
|
100
125
|
? { total_tokens: tokenUsage }
|
package/src/api-client.test.js
CHANGED
|
@@ -99,6 +99,41 @@ test('request() returns null on 204 No Content (consolidation queue empty)', asy
|
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
test('setRolePreference posts a real boolean to the agent preferences endpoint', async () => {
|
|
103
|
+
await withMockFetch(async (calls) => {
|
|
104
|
+
const client = new ApiClient('tfg_test_key');
|
|
105
|
+
await client.setRolePreference(true);
|
|
106
|
+
|
|
107
|
+
assert.match(calls[0].url, /\/research\/agent\/preferences$/);
|
|
108
|
+
assert.equal(JSON.parse(calls[0].opts.body).prefer_low_fetch_roles, true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('createAgent posts label + low-fetch flag', async () => {
|
|
113
|
+
await withMockFetch(async (calls) => {
|
|
114
|
+
const client = new ApiClient('tfg_test_key');
|
|
115
|
+
await client.createAgent('qwen-local', true);
|
|
116
|
+
|
|
117
|
+
assert.match(calls[0].url, /\/research\/agents$/);
|
|
118
|
+
const body = JSON.parse(calls[0].opts.body);
|
|
119
|
+
assert.equal(body.label, 'qwen-local');
|
|
120
|
+
assert.equal(body.prefer_low_fetch_roles, true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('rotateAgentKey + revokeAgent post the agent_id', async () => {
|
|
125
|
+
await withMockFetch(async (calls) => {
|
|
126
|
+
const client = new ApiClient('tfg_test_key');
|
|
127
|
+
await client.rotateAgentKey(7);
|
|
128
|
+
await client.revokeAgent(7);
|
|
129
|
+
|
|
130
|
+
assert.match(calls[0].url, /\/research\/agents\/rotate$/);
|
|
131
|
+
assert.equal(JSON.parse(calls[0].opts.body).agent_id, 7);
|
|
132
|
+
assert.match(calls[1].url, /\/research\/agents\/revoke$/);
|
|
133
|
+
assert.equal(JSON.parse(calls[1].opts.body).agent_id, 7);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
102
137
|
test('getNextValidation returns null on 204 (validation queue empty)', async () => {
|
|
103
138
|
const original = globalThis.fetch;
|
|
104
139
|
globalThis.fetch = async () => new Response(null, { status: 204 });
|
package/src/mcp-server.js
CHANGED
|
@@ -265,6 +265,68 @@ server.tool('submit_validation', 'Submit a validation: corrected ("validated") v
|
|
|
265
265
|
}
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
+
server.tool('set_role_preference', 'Set whether THIS agent prefers the low-fetch roles (validation & consolidation). Turn it on for a local/cheap model: the agent gets those no-scrape assignments first and waits instead of auto-starting an expensive research run when none is queued.', {
|
|
269
|
+
prefer_low_fetch_roles: z.boolean().describe('true = prefer validation/consolidation (best for local models); false = also do fresh research.'),
|
|
270
|
+
}, async ({ prefer_low_fetch_roles }) => {
|
|
271
|
+
if (!client) return { content: [{ type: 'text', text: 'Error: TFG_API_KEY not set.' }] };
|
|
272
|
+
try {
|
|
273
|
+
const result = await client.setRolePreference(prefer_low_fetch_roles);
|
|
274
|
+
return { content: [{ type: 'text', text: `Agent "${result.agent}" now ${result.prefers_low_fetch_roles ? 'PREFERS validation/consolidation (skips auto-research)' : 'does fresh research too'}.` }] };
|
|
275
|
+
} catch (err) {
|
|
276
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
server.tool('list_agents', 'List your agents (concurrent workers). Each has its own key + role preference; the one you call as is is_current. Also returns your agent_limit.', {}, async () => {
|
|
281
|
+
if (!client) return { content: [{ type: 'text', text: 'Error: TFG_API_KEY not set.' }] };
|
|
282
|
+
try {
|
|
283
|
+
const result = await client.listAgents();
|
|
284
|
+
const lines = (result.agents || []).map((a) =>
|
|
285
|
+
`- [${a.id}] ${a.label}${a.is_current ? ' (this one)' : ''}${a.prefers_low_fetch_roles ? ' · low-fetch' : ''}${a.last_used_at ? ` · last used ${a.last_used_at}` : ''}`
|
|
286
|
+
).join('\n');
|
|
287
|
+
return { content: [{ type: 'text', text: `Agents (limit ${result.agent_limit}):\n${lines}` }] };
|
|
288
|
+
} catch (err) {
|
|
289
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
server.tool('create_agent', 'Create a new agent (a separate concurrent worker) and get its API key — so you can run another harness at the same time (e.g. a local Qwen alongside Codex). Returns the key ONCE. Subject to your agent limit (default 2).', {
|
|
294
|
+
label: z.string().describe('A name for the new worker, e.g. "qwen-local".'),
|
|
295
|
+
prefer_low_fetch_roles: z.boolean().optional().describe('true if this agent runs a local/cheap model (prefers validation/consolidation).'),
|
|
296
|
+
}, async ({ label, prefer_low_fetch_roles }) => {
|
|
297
|
+
if (!client) return { content: [{ type: 'text', text: 'Error: TFG_API_KEY not set.' }] };
|
|
298
|
+
try {
|
|
299
|
+
const result = await client.createAgent(label, !!prefer_low_fetch_roles);
|
|
300
|
+
return { content: [{ type: 'text', text: `Created agent "${result.agent.label}" (id ${result.agent.id}).\n\nIts API key (shown once — configure the other harness with it):\n${result.api_key}` }] };
|
|
301
|
+
} catch (err) {
|
|
302
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
server.tool('rotate_agent_key', 'Rotate one of your agents\' API keys (get the id from list_agents). The old key stops working immediately; reconfigure that harness with the new key. Use if a key leaked.', {
|
|
307
|
+
agent_id: z.number().describe('The id of the agent to rotate (from list_agents).'),
|
|
308
|
+
}, async ({ agent_id }) => {
|
|
309
|
+
if (!client) return { content: [{ type: 'text', text: 'Error: TFG_API_KEY not set.' }] };
|
|
310
|
+
try {
|
|
311
|
+
const result = await client.rotateAgentKey(agent_id);
|
|
312
|
+
return { content: [{ type: 'text', text: `Rotated key for agent "${result.label}". New key (shown once):\n${result.api_key}` }] };
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
server.tool('revoke_agent', 'Permanently revoke one of your agents (get the id from list_agents). Its key stops working and the slot frees up. You cannot revoke your only agent.', {
|
|
319
|
+
agent_id: z.number().describe('The id of the agent to revoke (from list_agents).'),
|
|
320
|
+
}, async ({ agent_id }) => {
|
|
321
|
+
if (!client) return { content: [{ type: 'text', text: 'Error: TFG_API_KEY not set.' }] };
|
|
322
|
+
try {
|
|
323
|
+
await client.revokeAgent(agent_id);
|
|
324
|
+
return { content: [{ type: 'text', text: `Revoked agent ${agent_id}.` }] };
|
|
325
|
+
} catch (err) {
|
|
326
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
268
330
|
server.tool('research_status', 'See the overall Tokens for Good project progress and leaderboard.', {}, async () => {
|
|
269
331
|
try {
|
|
270
332
|
const clientForStatus = client || new ApiClient('dummy', { version: PKG_VERSION, platform, installId }); // Status is public
|