tokens-for-good 0.4.7 → 0.4.8

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
@@ -13,7 +13,7 @@ Works with Claude Code, OpenCode, Cursor, Windsurf, Devin, and Qwen Code as an M
13
13
  npx tokens-for-good init
14
14
  ```
15
15
 
16
- init is interactive: it asks for your API key, the cadence you want (daily / weekly / hourly / one-off), and then writes everything — MCP config, SessionStart hook, `/tfg` and `/tfg-schedule` skills, and your preference — in one shot.
16
+ init is interactive: it asks for your API key, the cadence you want (daily, with a chosen number of runs per day / weekly / one-off), and then writes everything — MCP config, SessionStart hook, `/tfg` and `/tfg-schedule` skills, and your preference — in one shot.
17
17
 
18
18
  3. **Open your AI coding tool.** Your first session acts on the cadence you picked automatically:
19
19
  - Scheduled → it sets up `/schedule` via the `/tfg-schedule` skill.
@@ -23,7 +23,7 @@ To change cadence later, run `npx tokens-for-good init` again.
23
23
 
24
24
  ## What happens during research
25
25
 
26
- Each org takes ~5 minutes and ~$0.20 in tokens:
26
+ Each org takes about 5 minutes:
27
27
 
28
28
  1. **Research** — web search + 6-prompt methodology, scored checklist (100 pts)
29
29
  2. **Verify** — every citation URL checked, hallucinations flagged and corrected
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokens-for-good",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "type": "module",
5
5
  "description": "Donate your spare AI tokens to research nonprofits for Fierce Philanthropy",
6
6
  "homepage": "https://tokensforgood.ai",
@@ -1,36 +1,45 @@
1
- ---
2
- name: tfg-schedule
3
- description: Set up automated Tokens for Good research on a schedule (hourly, daily, or weekly). Use this when the user wants their AI to donate spare tokens to nonprofit research without manual prompting — it wires up a recurring /schedule agent in one shot instead of making them run /schedule, pick a frequency, and paste a prompt by hand.
4
- ---
5
-
6
- The user wants to automate Tokens for Good research on a recurring schedule.
7
-
8
- ## What to do
9
-
10
- 1. **Read the user's chosen frequency.** If the user invoked you without specifying one, default to `daily`. Otherwise use what they said (`hourly`, `daily`, `weekly`).
11
-
12
- 2. **Call the TFG MCP `setup_automation` tool** with that frequency. It returns the full `/schedule` setup text, including a research prompt scoped to that user's API key.
13
-
14
- 3. **Extract the research prompt.** The returned text contains a block delimited by `---` lines that block is the exact task description `/schedule` needs. Don't paraphrase it; pass it through verbatim.
15
-
16
- 4. **Invoke the `/schedule` skill** to create the recurring trigger:
17
- - Frequency: the value from step 1
18
- - Task description: the verbatim block from step 3
19
-
20
- 5. **Wait for `/schedule` to confirm success.** If it fails or the user cancels, stop here and tell the user do NOT call `mark_setup_complete`.
21
-
22
- 6. **On success, call the TFG MCP `mark_setup_complete` tool.** This flips the user's local state so the SessionStart hook stops nudging.
23
-
24
- 7. **Confirm to the user** in one sentence: *"Scheduled ✓ — your spare tokens will research a nonprofit every `<frequency>` from here on. You can change this anytime with /schedule."*
25
-
26
- ## If something goes wrong
27
-
28
- - **`setup_automation` returned an error about a missing API key** tell the user to run `npx tokens-for-good init` in their terminal and re-open Claude Code.
29
- - **`/schedule` skill not available** → the user's Claude Code version may be too old; direct them to update.
30
- - **User declines the /schedule confirmation** don't call `mark_setup_complete`. The hook will offer again next session.
31
-
32
- ## What not to do
33
-
34
- - Don't ask the user to confirm the frequency again if they already picked one at install — they're done making that decision.
35
- - Don't print the raw research prompt to the user; it's verbose and already flows through /schedule.
36
- - Don't assume the user knows what /schedule is. If they ask, briefly explain: "It's Anthropic's scheduled-task feature runs your prompt on their cloud on a cron schedule, no need to keep your laptop on."
1
+ ---
2
+ name: tfg-schedule
3
+ description: Set up automated Tokens for Good research on a schedule (daily — with a chosen number of runs per day — or weekly). Use this when the user wants their AI to donate spare tokens to nonprofit research without manual prompting — it wires up a recurring /schedule agent in one shot instead of making them run /schedule, pick a schedule, and paste a prompt by hand.
4
+ ---
5
+
6
+ The user wants to automate Tokens for Good research on a recurring schedule.
7
+
8
+ ## 1. Settle the cadence
9
+
10
+ Work out the cadence before doing anything else:
11
+
12
+ - If you were invoked with an explicit cadence (e.g. `frequency=daily`, `runs_per_day=3`) or the user already stated one, use it.
13
+ - Otherwise ask the user, offering: **Daily** (recommended), **Weekly** (light touch), or **Skip for now**.
14
+ - For a daily cadence with no per-day count set, ask **"How many times per day? (1–15)"**. Claude Code caps how many scheduled runs you get per day, so keep it at 15 or below. Default to 1 if the user has no preference.
15
+
16
+ Describe cadence by frequency only keep token costs and dollar amounts out of every question and confirmation.
17
+
18
+ ## 2. Wire it up
19
+
20
+ 1. **Call the TFG MCP `setup_automation` tool** with `frequency` (`daily` or `weekly`) and, for daily, `runs_per_day`. It returns the full `/schedule` setup text: a Step 2 schedule line containing a cron expression, and a research prompt scoped to the user's API key.
21
+
22
+ 2. **Extract the research prompt** the block delimited by `---` lines. Pass it through verbatim; don't paraphrase.
23
+
24
+ 3. **Invoke the `/schedule` skill** to create the recurring trigger:
25
+ - Schedule: the cron expression from `setup_automation`'s Step 2 line.
26
+ - Task description: the verbatim block from step 2.
27
+
28
+ 4. **Wait for `/schedule` to confirm success.** If it fails or the user cancels, stop here and tell the user do NOT call `mark_setup_complete`.
29
+
30
+ 5. **On success, call the TFG MCP `mark_setup_complete` tool.** This flips the user's local state so the SessionStart hook stops nudging.
31
+
32
+ 6. **Confirm to the user** in one sentence, e.g. *"Scheduled ✓ — your spare tokens will research a nonprofit on that cadence from here on. You can change it anytime with /schedule."*
33
+
34
+ ## If something goes wrong
35
+
36
+ - **`setup_automation` returned an error about a missing API key** tell the user to run `npx tokens-for-good init` in their terminal and re-open Claude Code.
37
+ - **`/schedule` skill not available** → the user's Claude Code version may be too old; direct them to update.
38
+ - **User declines the /schedule confirmation** → don't call `mark_setup_complete`. The hook will offer again next session.
39
+
40
+ ## What not to do
41
+
42
+ - Keep token costs and dollar figures out of every question and confirmation.
43
+ - If the user already picked a cadence at install, don't re-ask it — only ask for the per-day count when it's missing for a daily cadence.
44
+ - Don't print the raw research prompt to the user; it's verbose and already flows through /schedule.
45
+ - Don't assume the user knows what /schedule is. If they ask, briefly explain: "It's Anthropic's scheduled-task feature — runs your prompt on their cloud on a cron schedule, no need to keep your laptop on."
package/skills/tfg.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: tfg
3
- description: Do one Tokens for Good research task end-to-end — claim an org, research it using the provided methodology, and submit the report. Use when the user wants to contribute once (not set up automation). Takes ~5 minutes and ~$0.20 in tokens per org.
3
+ description: Do one Tokens for Good research task end-to-end — claim an org, research it using the provided methodology, and submit the report. Use when the user wants to contribute once (not set up automation). Takes about 5 minutes per org.
4
4
  ---
5
5
 
6
6
  The user wants to complete one Tokens for Good research cycle.
package/src/init.js CHANGED
@@ -8,7 +8,7 @@ import { join, dirname } from 'path';
8
8
  import { homedir } from 'os';
9
9
  import { fileURLToPath } from 'url';
10
10
  import { spawnSync } from 'child_process';
11
- import { detectPlatform } from './platform.js';
11
+ import { detectPlatform, cronForSchedule, MAX_RUNS_PER_DAY } from './platform.js';
12
12
  import { loadState, saveState } from './state.js';
13
13
 
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -16,9 +16,8 @@ const PKG_ROOT = join(__dirname, '..');
16
16
  const IS_WINDOWS = process.platform === 'win32';
17
17
 
18
18
  const FREQUENCY_CHOICES = [
19
- { title: 'Daily — recommended, sustainable cadence', value: 'daily' },
19
+ { title: 'Daily — recommended; pick how many runs per day next', value: 'daily' },
20
20
  { title: 'Weekly — light touch', value: 'weekly' },
21
- { title: 'Hourly — aggressive (many orgs per day)', value: 'hourly' },
22
21
  { title: "One-off — I'll run research manually when I feel like it", value: 'one_off' },
23
22
  ];
24
23
 
@@ -45,7 +44,7 @@ export async function runInit() {
45
44
  const { redo } = await prompts({
46
45
  type: 'confirm',
47
46
  name: 'redo',
48
- message: `Already set up (${existing.intended_flow}${existing.intended_frequency ? `, ${existing.intended_frequency}` : ''}). Reconfigure?`,
47
+ message: `Already set up (${existing.intended_flow}${existing.intended_frequency ? `, ${existing.intended_frequency}${existing.intended_frequency === 'daily' && existing.runs_per_day > 1 ? ` ${existing.runs_per_day}×/day` : ''}` : ''}). Reconfigure?`,
49
48
  initial: false,
50
49
  }, { onCancel });
51
50
  if (!redo) {
@@ -82,6 +81,21 @@ export async function runInit() {
82
81
  initial: 0,
83
82
  }, { onCancel });
84
83
 
84
+ // Step 3b: Daily cadence — how many runs per day. Claude Code caps the
85
+ // number of scheduled runs per day, so we ask for a count (1-15).
86
+ let runs_per_day = null;
87
+ if (choice === 'daily') {
88
+ const { count } = await prompts({
89
+ type: 'number',
90
+ name: 'count',
91
+ message: `How many times per day? (1-${MAX_RUNS_PER_DAY})`,
92
+ initial: 1,
93
+ min: 1,
94
+ max: MAX_RUNS_PER_DAY,
95
+ }, { onCancel });
96
+ runs_per_day = count || 1;
97
+ }
98
+
85
99
  const intended_flow = choice === 'one_off' ? 'one_off' : 'scheduled';
86
100
  const intended_frequency = choice === 'one_off' ? null : choice;
87
101
 
@@ -120,6 +134,7 @@ export async function runInit() {
120
134
  ...loadState(),
121
135
  intended_flow,
122
136
  intended_frequency,
137
+ runs_per_day,
123
138
  first_setup_complete: false,
124
139
  platform,
125
140
  installed_at: new Date().toISOString(),
@@ -130,7 +145,7 @@ export async function runInit() {
130
145
 
131
146
  // Closing message
132
147
  console.log('\n✓ Setup complete.\n');
133
- printClosingGuidance(platform, intended_flow, intended_frequency);
148
+ printClosingGuidance(platform, intended_flow, intended_frequency, runs_per_day);
134
149
  }
135
150
 
136
151
  // --- Planning (for preview + execute) ---
@@ -300,10 +315,11 @@ function writeQwenCommand(name) {
300
315
 
301
316
  // --- Closing guidance ---
302
317
 
303
- function printClosingGuidance(platform, flow, freq) {
318
+ function printClosingGuidance(platform, flow, freq, runsPerDay = 1) {
319
+ const cadence = freq === 'daily' && runsPerDay > 1 ? `daily (${runsPerDay}× per day)` : freq;
304
320
  if (platform === 'claude-code') {
305
321
  if (flow === 'scheduled') {
306
- console.log(`Open Claude Code — your first session will set up /schedule at ${freq} cadence automatically.`);
322
+ console.log(`Open Claude Code — your first session will set up /schedule at ${cadence} cadence automatically.`);
307
323
  } else {
308
324
  console.log('Open Claude Code — your first session will kick off a one-off research task.');
309
325
  }
@@ -314,7 +330,7 @@ function printClosingGuidance(platform, flow, freq) {
314
330
  if (flow === 'scheduled') {
315
331
  console.log(`MCP config written to ${mcpConfigPath('opencode')}.\n`);
316
332
  console.log('To run on a schedule, add this to your crontab (crontab -e):');
317
- console.log(` ${cronExpression(freq)} cd /path/to/workspace && opencode run "Research a nonprofit org for Fierce Philanthropy using the tokens-for-good MCP tools."\n`);
333
+ console.log(` ${cronForSchedule(freq, runsPerDay)} cd /path/to/workspace && opencode run "Research a nonprofit org for Fierce Philanthropy using the tokens-for-good MCP tools."\n`);
318
334
  } else {
319
335
  console.log('MCP config written. In OpenCode run: "Research a nonprofit org for Fierce Philanthropy."\n');
320
336
  }
@@ -328,7 +344,7 @@ function printClosingGuidance(platform, flow, freq) {
328
344
  console.log('\nFor recurring runs, either:');
329
345
  console.log(' • Enable Qwen Code\'s experimental cron (set QWEN_CODE_ENABLE_CRON=1 and use the Cron tool / /loop), or');
330
346
  console.log(' • Add a system cron line (crontab -e):');
331
- console.log(` ${cronExpression(freq)} cd /path/to/workspace && qwen --prompt "Run /tfg"`);
347
+ console.log(` ${cronForSchedule(freq, runsPerDay)} cd /path/to/workspace && qwen --prompt "Run /tfg"`);
332
348
  }
333
349
  console.log('');
334
350
  return;
@@ -341,9 +357,3 @@ function printClosingGuidance(platform, flow, freq) {
341
357
  }
342
358
  console.log('');
343
359
  }
344
-
345
- function cronExpression(freq) {
346
- if (freq === 'hourly') return '0 * * * *';
347
- if (freq === 'weekly') return '0 2 * * 1';
348
- return '0 2 * * *';
349
- }
package/src/mcp-server.js CHANGED
@@ -21,7 +21,7 @@ Tell the user to run this in their terminal (not here in the chat), then restart
21
21
 
22
22
  npx tokens-for-good init
23
23
 
24
- The init command asks them to choose a contribution cadence (hourly / daily / weekly / one-off) and wires up everything else automatically. It takes about 30 seconds.`;
24
+ The init command asks them to choose a contribution cadence (daily / weekly / one-off) and wires up everything else automatically. It takes about 30 seconds.`;
25
25
 
26
26
  // Gate: only fires for genuinely cold installs where state.json is missing
27
27
  // entirely. Existing users — including those on the pre-0.4.0 schema — pass
@@ -57,12 +57,12 @@ const NO_KEY_INSTRUCTIONS = `The user wants to set up Tokens for Good. Tell them
57
57
  The command walks them through everything in under a minute:
58
58
  1. Create an account at https://tokensforgood.ai/contribute (GitHub OAuth, free)
59
59
  2. Copy their API key (starts with \`tfg_live_\`) and paste it into the init prompt
60
- 3. Pick a cadence: **daily** (recommended), weekly, hourly, or one-off
60
+ 3. Pick a cadence: **daily** (recommended; choose how many runs per day), weekly, or one-off
61
61
  4. Confirm
62
62
 
63
63
  init writes everything — MCP config, SessionStart hook, /tfg and /tfg-schedule skills, and their recorded preference — in one shot. The first session after init runs their chosen flow automatically.
64
64
 
65
- **What is Tokens for Good?** A way for developers to donate their spare AI subscription tokens to research nonprofit organizations for Fierce Philanthropy's social impact directory. Each org takes ~5 minutes and ~$0.20 in tokens. Contributors get credit on a public leaderboard.`;
65
+ **What is Tokens for Good?** A way for developers to donate their spare AI subscription tokens to research nonprofit organizations for Fierce Philanthropy's social impact directory. Each org takes about 5 minutes. Contributors get credit on a public leaderboard.`;
66
66
 
67
67
  // --- Resources ---
68
68
 
@@ -95,7 +95,7 @@ Contributor tiers:
95
95
 
96
96
  Automation: On Claude Code, use /schedule to auto-contribute daily. On Opencode, set up a system cron. On Cursor/Windsurf, contribute manually when prompted.
97
97
 
98
- Cost: ~$0.15-0.25 per org in tokens. Scale: 750K+ US nonprofits to research.`,
98
+ Scale: 750K+ US nonprofits to research.`,
99
99
  }],
100
100
  }));
101
101
 
@@ -245,10 +245,9 @@ server.tool('my_impact', 'See your personal contribution stats, tier, and histor
245
245
  try {
246
246
  const result = await client.getImpact();
247
247
  const c = result.contributor;
248
- const estimatedCost = (c.total_tokens / 1_000_000 * 3).toFixed(2);
249
248
 
250
249
  return {
251
- content: [{ type: 'text', text: `Your Impact (@${c.github_handle}):\n\nTier: ${c.tier}\nOrgs researched: ${c.total_orgs}\nEstimated donation: ~$${estimatedCost}\nAcceptance rate: ${c.acceptance_rate}%\nAutomation: ${c.has_schedule ? 'Active' : 'Not set up'}\n\nRecent:\n${result.claims?.slice(0, 5).map(cl => ` ${cl.organization?.name || 'Unknown'} - ${cl.status}`).join('\n') || 'None'}` }],
250
+ content: [{ type: 'text', text: `Your Impact (@${c.github_handle}):\n\nTier: ${c.tier}\nOrgs researched: ${c.total_orgs}\nAcceptance rate: ${c.acceptance_rate}%\nAutomation: ${c.has_schedule ? 'Active' : 'Not set up'}\n\nRecent:\n${result.claims?.slice(0, 5).map(cl => ` ${cl.organization?.name || 'Unknown'} - ${cl.status}`).join('\n') || 'None'}` }],
252
251
  };
253
252
  } catch (err) {
254
253
  return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
@@ -260,10 +259,11 @@ server.tool('setup_guide', 'Get setup instructions for Tokens for Good. Use this
260
259
  });
261
260
 
262
261
  server.tool('setup_automation', 'Get the scheduled-research prompt + setup instructions for the user\'s platform. Usually called by the /tfg-schedule skill (which extracts the prompt and invokes /schedule). Safe to call directly too — returns human-readable instructions.', {
263
- frequency: z.enum(['hourly', 'daily', 'weekly']).optional().describe('How often to contribute'),
264
- }, async ({ frequency }) => {
262
+ frequency: z.enum(['daily', 'weekly']).optional().describe('How often to contribute'),
263
+ runs_per_day: z.number().int().min(1).max(15).optional().describe('For daily cadence: how many research runs per day (1-15). Defaults to 1.'),
264
+ }, async ({ frequency, runs_per_day }) => {
265
265
  if (notInitialized()) return { content: [{ type: 'text', text: INIT_GUARD_MESSAGE }] };
266
- const instructions = getAutomationInstructions(platform, frequency || 'daily', apiKey);
266
+ const instructions = getAutomationInstructions(platform, frequency || 'daily', apiKey, runs_per_day || 1);
267
267
  return { content: [{ type: 'text', text: instructions }] };
268
268
  });
269
269
 
@@ -342,7 +342,7 @@ server.prompt('session_start', 'Check if you should research an org or complete
342
342
  return {
343
343
  messages: [{
344
344
  role: 'user',
345
- content: { type: 'text', text: `Tokens for Good: Would you like to research a nonprofit org today? It takes about 5 minutes and costs ~$0.20 in tokens.\n\n1. Research an org now\n2. Ask me tomorrow\n3. Ask me in a week\n\nUse claim_org for option 1.` },
345
+ content: { type: 'text', text: `Tokens for Good: Would you like to research a nonprofit org today? It takes about 5 minutes.\n\n1. Research an org now\n2. Ask me tomorrow\n3. Ask me in a week\n\nUse claim_org for option 1.` },
346
346
  }],
347
347
  };
348
348
  }
package/src/platform.js CHANGED
@@ -40,19 +40,25 @@ Auth header for all requests: X-TFG-Api-Key: ${apiKey}
40
40
  All endpoints, scoring criteria, and submission format are in the fetched instructions.`;
41
41
  }
42
42
 
43
- export function getAutomationInstructions(platform, frequency = 'daily', apiKey = null) {
43
+ // Claude Code caps how many scheduled runs a routine gets per day, so daily
44
+ // research fires a chosen number of times (1-15).
45
+ export const MAX_RUNS_PER_DAY = 15;
46
+
47
+ export function getAutomationInstructions(platform, frequency = 'daily', apiKey = null, runsPerDay = 1) {
48
+ const cron = cronForSchedule(frequency, runsPerDay);
49
+ const label = scheduleLabel(frequency, runsPerDay);
44
50
  switch (platform) {
45
51
  case 'claude-code':
46
52
  return `Set up automated contributions with Claude Code's /schedule command.
47
53
 
48
- (The /tfg-schedule skill does these 3 steps automatically. If you're reading this because Claude is orchestrating /tfg-schedule, extract the prompt between the --- markers and invoke /schedule with frequency="${frequency}" and that prompt.)
54
+ (The /tfg-schedule skill does these 3 steps automatically. If you're reading this because Claude is orchestrating /tfg-schedule, extract the prompt between the --- markers and invoke /schedule on the schedule below with that prompt.)
49
55
 
50
56
  **Step 1:** Run this in Claude Code:
51
57
  \`\`\`
52
58
  /schedule
53
59
  \`\`\`
54
60
 
55
- **Step 2:** Set frequency to "${frequency}"
61
+ **Step 2:** Set the schedule to ${label} (cron: \`${cron}\`)
56
62
 
57
63
  **Step 3:** When prompted for the task description, paste the prompt between the --- markers below:
58
64
 
@@ -66,13 +72,13 @@ This runs on Anthropic's cloud infrastructure. Your machine doesn't need to be o
66
72
  return `Set up automated contributions with a system cron job:
67
73
 
68
74
  Add this to your crontab (crontab -e):
69
- ${getCronExpression(frequency)} cd /path/to/workspace && opencode run "Research a nonprofit org for Fierce Philanthropy using the tokens-for-good MCP tools. Claim an org, research it, then submit the report."
75
+ ${cron} cd /path/to/workspace && opencode run "Research a nonprofit org for Fierce Philanthropy using the tokens-for-good MCP tools. Claim an org, research it, then submit the report."
70
76
 
71
77
  Your machine must be on for cron jobs to run.`;
72
78
 
73
79
  case 'devin':
74
80
  return `Set up a recurring Devin session to contribute automatically.
75
- Configure a ${frequency} recurring session with the prompt:
81
+ Configure a ${label} recurring session with the prompt:
76
82
  "Research a nonprofit org for Fierce Philanthropy using the tokens-for-good MCP tools."
77
83
 
78
84
  Devin runs in the cloud, fully autonomous.`;
@@ -83,20 +89,31 @@ Devin runs in the cloud, fully autonomous.`;
83
89
  Qwen Code v0.14+ has experimental built-in cron — enable it with QWEN_CODE_ENABLE_CRON=1 (or "experimental.cron": true in ~/.qwen/settings.json) and then use the Cron tool / /loop skill.
84
90
 
85
91
  For a portable option, use a system cron job (add via crontab -e):
86
- ${getCronExpression(frequency)} cd /path/to/workspace && qwen --prompt "Research a nonprofit org for Fierce Philanthropy using the tokens-for-good MCP tools. Claim an org, research it, then submit the report."
92
+ ${cron} cd /path/to/workspace && qwen --prompt "Research a nonprofit org for Fierce Philanthropy using the tokens-for-good MCP tools. Claim an org, research it, then submit the report."
87
93
 
88
94
  Your machine must stay on for system cron to run.`;
89
95
 
90
96
  default:
91
- return getAutomationInstructions('claude-code', frequency, apiKey);
97
+ return getAutomationInstructions('claude-code', frequency, apiKey, runsPerDay);
92
98
  }
93
99
  }
94
100
 
95
- function getCronExpression(frequency) {
96
- switch (frequency) {
97
- case 'hourly': return '0 * * * *';
98
- case 'daily': return '0 2 * * *';
99
- case 'weekly': return '0 2 * * 1';
100
- default: return '0 2 * * *';
101
- }
101
+ // Build a cron expression. Weekly fires Monday 02:00. Daily fires `runsPerDay`
102
+ // times (1-15), evenly spaced across the day; once-daily fires at 02:00.
103
+ export function cronForSchedule(frequency, runsPerDay = 1) {
104
+ if (frequency === 'weekly') return '0 2 * * 1';
105
+ const n = clampRunsPerDay(runsPerDay);
106
+ if (n === 1) return '0 2 * * *';
107
+ const hours = Array.from({ length: n }, (_, i) => Math.round((i * 24) / n));
108
+ return `0 ${hours.join(',')} * * *`;
109
+ }
110
+
111
+ function scheduleLabel(frequency, runsPerDay = 1) {
112
+ if (frequency === 'weekly') return 'weekly (Mondays)';
113
+ const n = clampRunsPerDay(runsPerDay);
114
+ return n === 1 ? 'daily' : `${n}× per day`;
115
+ }
116
+
117
+ function clampRunsPerDay(runsPerDay) {
118
+ return Math.min(MAX_RUNS_PER_DAY, Math.max(1, Math.floor(Number(runsPerDay)) || 1));
102
119
  }
@@ -55,12 +55,16 @@ function firstSessionMessage(state) {
55
55
  : 'recently';
56
56
 
57
57
  if (state.intended_flow === 'scheduled') {
58
- const freq = state.intended_frequency || 'daily';
58
+ // Normalize to the supported cadences (older installs may hold other values).
59
+ const freq = state.intended_frequency === 'weekly' ? 'weekly' : 'daily';
60
+ const runs = freq === 'daily' ? (state.runs_per_day || 1) : null;
61
+ const runsClause = runs ? ` and runs_per_day=${runs}` : '';
62
+ const cadence = runs && runs > 1 ? `${freq}, ${runs}× per day` : freq;
59
63
  return (
60
64
  `Tokens for Good: The user ran \`npx tokens-for-good init\` on ${installedOn} ` +
61
- `and chose scheduled research at ${freq} cadence. This is their first Claude Code ` +
65
+ `and chose scheduled research (${cadence}). This is their first Claude Code ` +
62
66
  `session since. Before any other task, invoke the /tfg-schedule skill with ` +
63
- `frequency=${freq}. After /schedule confirms, the skill will call ` +
67
+ `frequency=${freq}${runsClause}. After /schedule confirms, the skill will call ` +
64
68
  `mark_setup_complete on the TFG MCP to dismiss this prompt.`
65
69
  );
66
70
  }
package/src/state.js CHANGED
@@ -13,7 +13,8 @@ const DEFAULT_STATE = {
13
13
  platform: null,
14
14
  total_session_contributions: 0,
15
15
  intended_flow: null, // 'scheduled' | 'one_off' | null (pre-init)
16
- intended_frequency: null, // 'hourly' | 'daily' | 'weekly' | null
16
+ intended_frequency: null, // 'daily' | 'weekly' | null
17
+ runs_per_day: null, // 1-15 when intended_frequency === 'daily', else null
17
18
  first_setup_complete: false, // flipped by mark_setup_complete tool after first scheduled run or first one-off submit
18
19
  installed_at: null, // ISO timestamp when init finished
19
20
  };