tokens-for-good 0.4.0 → 0.4.2

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.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Donate your spare AI tokens to research nonprofits for Fierce Philanthropy",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  // CLI entry point for tokens-for-good.
4
4
  // Usage:
package/src/init.js CHANGED
@@ -7,6 +7,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
7
7
  import { join, dirname } from 'path';
8
8
  import { homedir } from 'os';
9
9
  import { fileURLToPath } from 'url';
10
+ import { spawnSync } from 'child_process';
10
11
  import { detectPlatform } from './platform.js';
11
12
  import { loadState, saveState } from './state.js';
12
13
 
@@ -238,15 +239,44 @@ function writeSessionStartHook() {
238
239
  matcher: '',
239
240
  hooks: [{
240
241
  type: 'command',
241
- command: IS_WINDOWS
242
- ? 'cmd /c npx -y tokens-for-good session-start-hook'
243
- : 'npx -y tokens-for-good session-start-hook',
242
+ command: hookCommand(),
244
243
  }],
245
244
  });
246
245
  }
247
246
  writeJson(abs, cfg);
248
247
  }
249
248
 
249
+ // Claude Code runs SessionStart hooks under Git Bash on Windows with a
250
+ // stripped PATH that typically does not include C:\Program Files\nodejs,
251
+ // so a bare `npx` lookup fails silently. Resolve the absolute npx path at
252
+ // init time (when the user's full PATH is available) and bake it into the
253
+ // hook command so it works regardless of Claude Code's hook-runner PATH.
254
+ function hookCommand() {
255
+ if (!IS_WINDOWS) return 'npx -y tokens-for-good session-start-hook';
256
+
257
+ const npxPath = resolveWindowsNpxPath();
258
+ // Bash accepts double-quoted paths with spaces; escape backslashes for JSON.
259
+ return `"${npxPath}" -y tokens-for-good session-start-hook`;
260
+ }
261
+
262
+ function resolveWindowsNpxPath() {
263
+ // First try `where npx.cmd` — most reliable when PATH is correct.
264
+ try {
265
+ const r = spawnSync('where', ['npx.cmd'], { encoding: 'utf-8' });
266
+ if (r.status === 0) {
267
+ const first = r.stdout.trim().split(/\r?\n/)[0];
268
+ if (first && existsSync(first)) return first;
269
+ }
270
+ } catch { /* fall through */ }
271
+
272
+ // Fallback: npx.cmd usually sits alongside node.exe.
273
+ const alongside = join(dirname(process.execPath), 'npx.cmd');
274
+ if (existsSync(alongside)) return alongside;
275
+
276
+ // Last-resort guess — user's hook may need manual edit if this is wrong.
277
+ return 'C:\\Program Files\\nodejs\\npx.cmd';
278
+ }
279
+
250
280
  function writeSkillFile(name) {
251
281
  const src = join(PKG_ROOT, 'skills', `${name}.md`);
252
282
  const dst = join(homedir(), '.claude', 'skills', name, 'SKILL.md');
@@ -3,9 +3,14 @@
3
3
  // ~/.claude/settings.json on every Claude Code session start.
4
4
  //
5
5
  // Reads state.json and, if the user's init choice hasn't been acted on yet,
6
- // emits instructions to stdout so Claude sees them as part of the session
7
- // context and acts on the pre-made choice immediately. Exits silently in
6
+ // emits a JSON payload on stdout in Claude Code's SessionStart-hook format
7
+ // so the message is injected into the session's context. Exits silently in
8
8
  // the normal steady state so users aren't nudged forever.
9
+ //
10
+ // Docs: https://code.claude.com/docs/en/hooks.md (SessionStart section).
11
+ // Required output shape for SessionStart hooks:
12
+ // { "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": "<text>" } }
13
+ // Plain stdout is ignored by Claude Code, which is why this needs JSON.
9
14
  import { loadState, isSnoozed } from './state.js';
10
15
 
11
16
  export function runSessionStartHook() {
@@ -16,7 +21,7 @@ export function runSessionStartHook() {
16
21
 
17
22
  // First session after init — emit the handoff instruction exactly once.
18
23
  if (!state.first_setup_complete) {
19
- emitFirstSessionInstruction(state);
24
+ emit(firstSessionMessage(state));
20
25
  return;
21
26
  }
22
27
 
@@ -27,37 +32,45 @@ export function runSessionStartHook() {
27
32
  const last = state.last_contributed ? new Date(state.last_contributed) : null;
28
33
  const daysSince = last ? Math.floor((Date.now() - last.getTime()) / 86_400_000) : Infinity;
29
34
  if (daysSince >= 7) {
30
- process.stdout.write(
35
+ emit(
31
36
  "Tokens for Good: It's been a while — say 'run /tfg' to research one org now, " +
32
- "or 'run /tfg-schedule daily' to automate it from here on.\n"
37
+ "or 'run /tfg-schedule daily' to automate it from here on."
33
38
  );
34
39
  }
35
40
  }
36
41
  }
37
42
 
38
- function emitFirstSessionInstruction(state) {
43
+ function emit(additionalContext) {
44
+ process.stdout.write(JSON.stringify({
45
+ hookSpecificOutput: {
46
+ hookEventName: 'SessionStart',
47
+ additionalContext,
48
+ },
49
+ }));
50
+ }
51
+
52
+ function firstSessionMessage(state) {
39
53
  const installedOn = state.installed_at
40
54
  ? new Date(state.installed_at).toISOString().slice(0, 10)
41
55
  : 'recently';
42
56
 
43
57
  if (state.intended_flow === 'scheduled') {
44
58
  const freq = state.intended_frequency || 'daily';
45
- process.stdout.write(
59
+ return (
46
60
  `Tokens for Good: The user ran \`npx tokens-for-good init\` on ${installedOn} ` +
47
61
  `and chose scheduled research at ${freq} cadence. This is their first Claude Code ` +
48
62
  `session since. Before any other task, invoke the /tfg-schedule skill with ` +
49
63
  `frequency=${freq}. After /schedule confirms, the skill will call ` +
50
- `mark_setup_complete on the TFG MCP to dismiss this prompt.\n`
64
+ `mark_setup_complete on the TFG MCP to dismiss this prompt.`
51
65
  );
52
- return;
53
66
  }
54
67
 
55
68
  // one_off flow
56
- process.stdout.write(
69
+ return (
57
70
  `Tokens for Good: The user ran \`npx tokens-for-good init\` on ${installedOn} ` +
58
71
  `and chose to do research manually (one-off, not scheduled). This is their first ` +
59
72
  `Claude Code session since. Offer to start a research task now by invoking the ` +
60
73
  `/tfg skill. After they submit their first report, the skill will call ` +
61
- `mark_setup_complete on the TFG MCP to dismiss this prompt.\n`
74
+ `mark_setup_complete on the TFG MCP to dismiss this prompt.`
62
75
  );
63
76
  }