tokenmaxxing 0.19.0 → 0.21.0

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.
Files changed (59) hide show
  1. package/DESIGN.md +34 -23
  2. package/README.md +4 -4
  3. package/package.json +1 -1
  4. package/src/cli/add.ts +1 -0
  5. package/src/cli/auth.ts +25 -14
  6. package/src/cli/check.ts +3 -2
  7. package/src/cli/codexadd.ts +44 -40
  8. package/src/cli/codexinit.ts +59 -12
  9. package/src/cli/codexswitch.ts +15 -1
  10. package/src/cli/config.ts +10 -1
  11. package/src/cli/doctor.ts +3 -3
  12. package/src/cli/init.ts +54 -32
  13. package/src/cli/onboard.ts +62 -45
  14. package/src/cli/render.ts +0 -16
  15. package/src/cli/rm.ts +40 -2
  16. package/src/cli/serve.ts +650 -78
  17. package/src/cli/status.ts +69 -23
  18. package/src/cli/switch.ts +54 -19
  19. package/src/entries/codexstophook.ts +123 -4
  20. package/src/entries/codexsupervisor.ts +87 -13
  21. package/src/entries/sessionstart.ts +1 -1
  22. package/src/entries/statusline.ts +56 -20
  23. package/src/entries/stophook.ts +23 -9
  24. package/src/entries/supervisor.ts +134 -18
  25. package/src/lib/atomic.ts +28 -6
  26. package/src/lib/claudebin.ts +2 -2
  27. package/src/lib/claudejson.ts +5 -5
  28. package/src/lib/claudelock.ts +112 -37
  29. package/src/lib/codexauth.ts +10 -2
  30. package/src/lib/codexbin.ts +1 -1
  31. package/src/lib/codexdecide.ts +149 -19
  32. package/src/lib/codexpick.ts +17 -6
  33. package/src/lib/codexpresence.ts +59 -21
  34. package/src/lib/codexsample.ts +17 -8
  35. package/src/lib/codexswap.ts +10 -1
  36. package/src/lib/credstore.ts +6 -2
  37. package/src/lib/decide.ts +114 -42
  38. package/src/lib/install.ts +125 -17
  39. package/src/lib/keychain.ts +41 -15
  40. package/src/lib/lock.ts +57 -35
  41. package/src/lib/log.ts +36 -7
  42. package/src/lib/oauth.ts +18 -11
  43. package/src/lib/paths.ts +17 -11
  44. package/src/lib/picker.ts +11 -3
  45. package/src/lib/proc.ts +37 -0
  46. package/src/lib/sample.ts +91 -31
  47. package/src/lib/sessions.ts +23 -1
  48. package/src/lib/settings.ts +59 -18
  49. package/src/lib/slackbridge.ts +583 -76
  50. package/src/lib/slackstate.ts +159 -12
  51. package/src/lib/slackstream.ts +127 -21
  52. package/src/lib/state.ts +131 -35
  53. package/src/lib/swap.ts +109 -47
  54. package/src/lib/types.ts +79 -37
  55. package/src/lib/usage.ts +114 -16
  56. package/src/main.ts +61 -7
  57. package/src/serve-plugin/.claude-plugin/plugin.json +4 -0
  58. package/src/serve-plugin/skills/ask-the-user/SKILL.md +36 -0
  59. package/src/serve-plugin/skills/serve-session/SKILL.md +50 -0
@@ -4,7 +4,7 @@
4
4
  // are ours outright - tokenmaxxing renders them natively, so any other
5
5
  // statusLine command is replaced.
6
6
 
7
- import { existsSync, readFileSync } from "node:fs";
7
+ import { existsSync, readFileSync, statSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { z } from "zod";
10
10
  import { paths } from "./paths.ts";
@@ -39,11 +39,15 @@ function readSettings(): Settings {
39
39
  }
40
40
 
41
41
  function writeSettings(s: Settings): void {
42
- writeFileAtomic(paths.claudeSettings, JSON.stringify(s, null, 2) + "\n", 0o644);
42
+ // Preserve the user's mode: settings.json can carry an env block with
43
+ // credentials, and the atomic rename would otherwise widen a 0600 file to
44
+ // world-readable. A brand-new file starts at the conservative 0600.
45
+ const mode = existsSync(paths.claudeSettings) ? statSync(paths.claudeSettings).mode & 0o777 : 0o600;
46
+ writeFileAtomic(paths.claudeSettings, JSON.stringify(s, null, 2) + "\n", mode);
43
47
  }
44
48
 
45
49
  /** True if a hook/statusline command string is one tokenmaxxing installed. */
46
- export function isOurCommand(cmd: string | undefined): boolean {
50
+ function isOurCommand(cmd: string | undefined): boolean {
47
51
  if (!cmd) return false;
48
52
  return (
49
53
  cmd.includes(SUBCMD.statusline) ||
@@ -55,36 +59,69 @@ export function isOurCommand(cmd: string | undefined): boolean {
55
59
  );
56
60
  }
57
61
 
62
+ /** The exact command string installSettings writes for a subcommand; doctor
63
+ * compares against it verbatim, so a green check proves the canonical entry. */
64
+ function ourCommand(sub: string): string {
65
+ return `${JSON.stringify(installedBin())} ${sub}`;
66
+ }
67
+
58
68
  function ourHookGroup(sub: string): HookGroup {
59
- return { hooks: [{ type: "command", command: `${JSON.stringify(installedBin())} ${sub}` }] };
69
+ return { hooks: [{ type: "command", command: ourCommand(sub) }] };
60
70
  }
61
71
 
62
72
  function appendHook(s: Settings, event: string, sub: string): void {
63
73
  s.hooks ??= {};
64
74
  s.hooks[event] ??= [];
65
75
  const arr = s.hooks[event]!;
66
- const present = arr.some((g) => g.hooks?.some((h) => h.command?.includes(sub)));
76
+ const present = arr.some((g) => g.hooks.some((h) => h.command === ourCommand(sub)));
67
77
  if (!present) arr.push(ourHookGroup(sub));
68
78
  }
69
79
 
80
+ /** True only for a command tokenmaxxing itself wrote - the exact historical
81
+ * shape `"<...>/tokenmaxxing" <sub>` at ANY install path (so stale
82
+ * pre-relocation entries match too). A foreign command that merely mentions
83
+ * the subcommand or the path as text is NOT ours and must survive removal. */
84
+ /** Structural ownership: exactly `"<path>/tokenmaxxing" <sub>`. Exported for
85
+ * the codex hooks.json installer, whose old includes()-based match deleted
86
+ * foreign hooks sharing a group and misclassified commands merely mentioning
87
+ * the subcommand (closing-review catch; the same class settings.ts's own
88
+ * removeHook was fixed for in PR #31). */
89
+ export function isOurHookCommand(cmd: string, sub: string): boolean {
90
+ if (!cmd.endsWith(` ${sub}`)) return false;
91
+ const quotedPath = cmd.slice(0, cmd.length - (sub.length + 1));
92
+ if (!quotedPath.startsWith('"') || !quotedPath.endsWith('"')) return false;
93
+ let path: unknown;
94
+ try {
95
+ path = JSON.parse(quotedPath);
96
+ } catch {
97
+ return false;
98
+ }
99
+ const parsed = z.string().safeParse(path);
100
+ return parsed.success && parsed.data.endsWith("/tokenmaxxing");
101
+ }
102
+
70
103
  function removeHook(s: Settings, event: string, sub: string): void {
71
104
  const arr = s.hooks?.[event];
72
105
  if (!arr) return;
73
- s.hooks![event] = arr.filter((g) => !g.hooks?.some((h) => h.command?.includes(sub)));
106
+ // Strip only VERIFIED tokenmaxxing-owned entries from WITHIN each group:
107
+ // foreign hooks sharing a group - or merely mentioning our strings - survive
108
+ // (review catches, PR #31), and a group is dropped only once it is empty.
109
+ for (const g of arr) g.hooks = g.hooks.filter((h) => !isOurHookCommand(h.command, sub));
110
+ s.hooks![event] = arr.filter((g) => g.hooks.length > 0);
74
111
  if (s.hooks![event]!.length === 0) delete s.hooks![event];
75
112
  }
76
113
 
77
- /** Install the entries: take both statusLine slots, append our hooks. */
114
+ /** Install the entries: take both statusLine slots, append our hooks. Stale
115
+ * same-subcommand hooks from an OLD install path are dropped first, so a
116
+ * TOKENMAXXING_HOME relocation rewrites the entries instead of leaving dead
117
+ * paths that read as installed (relocation residue is how the supervisor
118
+ * recursion incident started). Foreign hooks are untouched. */
78
119
  export function installSettings(): void {
79
120
  const s = readSettings();
80
- s.statusLine = {
81
- type: "command",
82
- command: `${JSON.stringify(installedBin())} ${SUBCMD.statusline}`,
83
- };
84
- s.subagentStatusLine = {
85
- type: "command",
86
- command: `${JSON.stringify(installedBin())} ${SUBCMD.subagentStatusline}`,
87
- };
121
+ s.statusLine = { type: "command", command: ourCommand(SUBCMD.statusline) };
122
+ s.subagentStatusLine = { type: "command", command: ourCommand(SUBCMD.subagentStatusline) };
123
+ removeHook(s, "Stop", SUBCMD.stop);
124
+ removeHook(s, "SessionStart", SUBCMD.sessionStart);
88
125
  appendHook(s, "Stop", SUBCMD.stop);
89
126
  appendHook(s, "SessionStart", SUBCMD.sessionStart);
90
127
  writeSettings(s);
@@ -110,11 +147,15 @@ export type SettingsCheck = z.infer<typeof SettingsCheckSchema>;
110
147
 
111
148
  export function checkSettings(): SettingsCheck {
112
149
  const s = readSettings();
150
+ // Exact-command identity (review catch, PR #31): only the verbatim string
151
+ // installSettings writes counts as installed. A hook pointing at an OLD
152
+ // install path reads as broken, and a foreign command that merely mentions
153
+ // the path or subcommand as text never green-lights.
113
154
  const has = (event: string, sub: string) =>
114
- !!s.hooks?.[event]?.some((g) => g.hooks?.some((h) => h.command?.includes(sub)));
155
+ !!s.hooks?.[event]?.some((g) => g.hooks.some((h) => h.command === ourCommand(sub)));
115
156
  return {
116
- statusLineOk: isOurCommand(s.statusLine?.command),
117
- subagentStatusLineOk: isOurCommand(s.subagentStatusLine?.command),
157
+ statusLineOk: s.statusLine?.command === ourCommand(SUBCMD.statusline),
158
+ subagentStatusLineOk: s.subagentStatusLine?.command === ourCommand(SUBCMD.subagentStatusline),
118
159
  stopOk: has("Stop", SUBCMD.stop),
119
160
  sessionStartOk: has("SessionStart", SUBCMD.sessionStart),
120
161
  };