santree 0.5.4 → 0.5.5
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 +19 -2
- package/dist/commands/doctor.js +68 -9
- package/dist/commands/helpers/english-tutor/index.d.ts +1 -0
- package/dist/commands/helpers/english-tutor/index.js +1 -0
- package/dist/commands/helpers/english-tutor/install.d.ts +8 -0
- package/dist/commands/helpers/english-tutor/install.js +24 -0
- package/dist/commands/helpers/english-tutor/prompt.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/prompt.js +16 -0
- package/dist/commands/helpers/english-tutor/session-start.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/session-start.js +34 -0
- package/dist/commands/helpers/english-tutor/uninstall.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/uninstall.js +15 -0
- package/dist/lib/ai.js +3 -3
- package/dist/lib/english-tutor.d.ts +13 -0
- package/dist/lib/english-tutor.js +125 -0
- package/dist/lib/multiplexer/index.js +5 -12
- package/dist/lib/trackers/index.d.ts +1 -1
- package/dist/lib/trackers/index.js +1 -1
- package/package.json +1 -1
- package/prompts/english-tutor-prompt.njk +15 -0
package/README.md
CHANGED
|
@@ -405,13 +405,30 @@ To preview the JSON without writing: `santree helpers session-signal install --d
|
|
|
405
405
|
|
|
406
406
|
Verify with `santree doctor` — look for the "Session Signal Hooks" row under Claude Code.
|
|
407
407
|
|
|
408
|
+
### English Tutor (Optional)
|
|
409
|
+
|
|
410
|
+
When enabled, Claude Code spots grammar mistakes in your prompts and replies with a one-line correction (`original -> correction (reason)`) before doing the actual work. Mistake-free prompts are ignored. Useful if English isn't your first language and you'd like ambient feedback while you code.
|
|
411
|
+
|
|
412
|
+
**Install:**
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
santree helpers english-tutor install
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
This adds two hooks (`UserPromptSubmit`, `SessionStart`) and a scoped `Edit` permission for the practice log to `~/.claude/settings.json`. Existing hooks and permissions are preserved.
|
|
419
|
+
|
|
420
|
+
To preview the JSON without writing: `santree helpers english-tutor install --dry`
|
|
421
|
+
|
|
422
|
+
Corrections are appended to `~/.config/santree/english-practice-log.md` (or `$XDG_CONFIG_HOME/santree/english-practice-log.md`). The `SessionStart` hook replays this log into context on new sessions so Claude can spot recurring patterns.
|
|
423
|
+
|
|
424
|
+
Remove with `santree helpers english-tutor uninstall`.
|
|
425
|
+
|
|
408
426
|
### Environment Variables
|
|
409
427
|
|
|
410
428
|
| Variable | Effect |
|
|
411
429
|
|---|---|
|
|
412
430
|
| `SANTREE_TRACKER` | Override the active issue tracker for a single invocation: `linear` or `github`. Takes precedence over the per-repo `_tracker.kind`. If unset, falls back to repo config → legacy `_linear.org` → auto-detect. |
|
|
413
431
|
| `SANTREE_EDITOR` | Editor used by `e` (open in editor) actions in the dashboard. Defaults to `code`. Examples: `cursor`, `zed`, `code`, `nvim`. |
|
|
414
|
-
| `SANTREE_MULTIPLEXER` | Terminal multiplexer used by the dashboard and `worktree create --window`. One of `tmux`, `cmux`, `none`. If unset, auto-detects from `$TMUX` / `$CMUX_SURFACE_ID`. cmux is macOS-only and limited by [manaflow-ai/cmux#1472](https://github.com/manaflow-ai/cmux/issues/1472). |
|
|
415
432
|
| `SANTREE_DIFF_TOOL` | Pager used by `worktree diff` (CLI) and the dashboard diff overlay. Passed to git as `-c core.pager=<tool>` for the CLI, and used to pipe content for the overlay. Examples: `delta`, `diff-so-fancy`. Must accept a unified diff on stdin. Names are restricted to `[A-Za-z0-9_\-/.+]`. |
|
|
416
433
|
| `SANTREE_THEME` | Dashboard color theme: `light`, `dark`, or `auto` (default). In `auto` mode, santree queries the terminal's background via OSC 11 and re-detects on each refresh cycle (≤30s) so theme switches propagate automatically. Set explicitly when your terminal doesn't respond to OSC 11. |
|
|
417
434
|
|
|
@@ -522,7 +539,7 @@ These are hardwired in `lib/github.ts` and `lib/ai.ts` respectively. Adding a se
|
|
|
522
539
|
| --- | --- | --- |
|
|
523
540
|
| **Issue tracker** | `santree issue switch <linear\|github>` (or `SANTREE_TRACKER` env var, or as a side effect of `santree linear auth` / `santree github auth`) | Linear (OAuth + GraphQL), GitHub Issues (via `gh` CLI). Adding a third tracker = one new directory under `lib/trackers/`. |
|
|
524
541
|
| **Editor** | `SANTREE_EDITOR` env var (or `--editor` flag on `worktree open`) | `code`, `cursor`, `zed`, `nvim`, `subl`, `webstorm` — any executable that takes a path argument |
|
|
525
|
-
| **Terminal multiplexer** | `
|
|
542
|
+
| **Terminal multiplexer** | Auto-detected via `$TMUX` / `$CMUX_SURFACE_ID`. No config needed — each adapter's `isActive()` declares its own runtime check. | `tmux` (all platforms), `cmux` (macOS only — see [#1472](https://github.com/manaflow-ai/cmux/issues/1472)). Zellij is planned but not implemented. |
|
|
526
543
|
| **Diff renderer** | `SANTREE_DIFF_TOOL` env var | `delta`, `diff-so-fancy`, or any pager that accepts a unified diff. Falls back to plain `git diff` colorization when unset. |
|
|
527
544
|
| **Color theme** | `SANTREE_THEME` env var (`light`/`dark`/`auto`) | Auto-detects via OSC 11; manual override available. Re-detects every refresh so theme switches show up within 30s. |
|
|
528
545
|
| **Shell integration** | `santree helpers shell-init` detects the shell | `zsh`, `bash` (templates in `shell/init.{zsh,bash}.njk`) |
|
package/dist/commands/doctor.js
CHANGED
|
@@ -60,19 +60,19 @@ async function checkTool(name, description, required, versionCommand, hint) {
|
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
62
|
* Reports the active multiplexer (tmux/cmux/none) and verifies the underlying
|
|
63
|
-
* binary is reachable.
|
|
63
|
+
* binary is reachable. Detection is auto — each adapter's `isActive()` checks
|
|
64
|
+
* its own runtime env (`$TMUX`, `$CMUX_SURFACE_ID`).
|
|
64
65
|
*/
|
|
65
66
|
async function checkMultiplexer() {
|
|
66
67
|
const mux = getMultiplexer();
|
|
67
|
-
const
|
|
68
|
-
const description = `Multiplexer (active: ${mux.kind}${explicit ? `, SANTREE_MULTIPLEXER=${explicit}` : ""})`;
|
|
68
|
+
const description = `Multiplexer (active: ${mux.kind})`;
|
|
69
69
|
if (mux.kind === "none") {
|
|
70
70
|
return {
|
|
71
71
|
name: "multiplexer",
|
|
72
72
|
description,
|
|
73
73
|
required: false,
|
|
74
74
|
installed: false,
|
|
75
|
-
hint: "
|
|
75
|
+
hint: "Run inside tmux or cmux to enable session window renaming. Install tmux: brew install tmux",
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
if (mux.kind === "tmux") {
|
|
@@ -104,7 +104,7 @@ async function checkMultiplexer() {
|
|
|
104
104
|
description,
|
|
105
105
|
required: false,
|
|
106
106
|
installed: false,
|
|
107
|
-
hint: "Install cmux.app from https://cmux.com
|
|
107
|
+
hint: "Install cmux.app from https://cmux.com (cmux is macOS-only).",
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
const version = await tryExec("cmux --version 2>/dev/null");
|
|
@@ -122,9 +122,7 @@ async function checkMultiplexer() {
|
|
|
122
122
|
installed: !!ping,
|
|
123
123
|
version: version || "unknown",
|
|
124
124
|
path,
|
|
125
|
-
hint: !ping
|
|
126
|
-
? "cmux app not reachable — open cmux.app or set SANTREE_MULTIPLEXER=tmux."
|
|
127
|
-
: undefined,
|
|
125
|
+
hint: !ping ? "cmux app not reachable — open cmux.app." : undefined,
|
|
128
126
|
};
|
|
129
127
|
}
|
|
130
128
|
/**
|
|
@@ -336,6 +334,57 @@ function checkSessionSignalHooks() {
|
|
|
336
334
|
hint: `Missing: ${missingHooks.join(", ")}. Run: santree helpers session-signal install`,
|
|
337
335
|
};
|
|
338
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Checks if english-tutor hooks are configured. Verifies the UserPromptSubmit
|
|
339
|
+
* and SessionStart hooks plus the scoped Edit permission for the practice log.
|
|
340
|
+
*/
|
|
341
|
+
function checkEnglishTutorHooks() {
|
|
342
|
+
const home = process.env.HOME || "";
|
|
343
|
+
const claudeSettingsPath = path.join(home, ".claude", "settings.json");
|
|
344
|
+
const requiredEvents = ["UserPromptSubmit", "SessionStart"];
|
|
345
|
+
const missingHooks = [];
|
|
346
|
+
let missingPermission = true;
|
|
347
|
+
const configDir = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
348
|
+
const expectedPermission = `Edit(${path.join(configDir, "santree", "english-practice-log.md")})`;
|
|
349
|
+
try {
|
|
350
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
351
|
+
const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, "utf-8"));
|
|
352
|
+
const hooks = settings.hooks || {};
|
|
353
|
+
for (const event of requiredEvents) {
|
|
354
|
+
const eventHooks = hooks[event];
|
|
355
|
+
if (!Array.isArray(eventHooks)) {
|
|
356
|
+
missingHooks.push(event);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const found = eventHooks.some((entry) => {
|
|
360
|
+
const innerHooks = entry.hooks || [];
|
|
361
|
+
return innerHooks.some((h) => typeof h.command === "string" && h.command.includes("english-tutor"));
|
|
362
|
+
});
|
|
363
|
+
if (!found)
|
|
364
|
+
missingHooks.push(event);
|
|
365
|
+
}
|
|
366
|
+
const allow = settings.permissions?.allow;
|
|
367
|
+
if (Array.isArray(allow) && allow.includes(expectedPermission)) {
|
|
368
|
+
missingPermission = false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
missingHooks.push(...requiredEvents);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
missingHooks.push(...requiredEvents);
|
|
377
|
+
}
|
|
378
|
+
if (missingHooks.length === 0 && !missingPermission) {
|
|
379
|
+
return { configured: true, missingHooks: [], missingPermission: false };
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
configured: false,
|
|
383
|
+
missingHooks,
|
|
384
|
+
missingPermission,
|
|
385
|
+
hint: "Run: santree helpers english-tutor install",
|
|
386
|
+
};
|
|
387
|
+
}
|
|
339
388
|
/**
|
|
340
389
|
* Checks if a path is gitignored (via .gitignore or .git/info/exclude).
|
|
341
390
|
*/
|
|
@@ -433,6 +482,14 @@ function RemoteControlRow({ status }) {
|
|
|
433
482
|
function SessionSignalRow({ status }) {
|
|
434
483
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.configured, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Session Signal Hooks" }), _jsx(Text, { dimColor: true, children: " - Surface session state in dashboard/tmux" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: status.configured ? (_jsx(Text, { dimColor: true, children: "All hooks configured" })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["Missing: ", status.missingHooks.join(", ")] }), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })) })] }));
|
|
435
484
|
}
|
|
485
|
+
function EnglishTutorRow({ status }) {
|
|
486
|
+
const missingParts = [];
|
|
487
|
+
if (status.missingHooks.length > 0)
|
|
488
|
+
missingParts.push(`hooks: ${status.missingHooks.join(", ")}`);
|
|
489
|
+
if (status.missingPermission)
|
|
490
|
+
missingParts.push("log Edit permission");
|
|
491
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.configured, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "English Tutor Hooks" }), _jsx(Text, { dimColor: true, children: " - Inline grammar corrections" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: status.configured ? (_jsx(Text, { dimColor: true, children: "Hooks and log permission configured" })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["Missing: ", missingParts.join("; ") || "—"] }), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })) })] }));
|
|
492
|
+
}
|
|
436
493
|
function SantreeSetupRow({ status }) {
|
|
437
494
|
const isOk = status.santreeFolderExists &&
|
|
438
495
|
status.initShExists &&
|
|
@@ -455,6 +512,7 @@ export default function Doctor() {
|
|
|
455
512
|
const [remoteControl, setRemoteControl] = useState(null);
|
|
456
513
|
const [statusline, setStatusline] = useState(null);
|
|
457
514
|
const [sessionSignal, setSessionSignal] = useState(null);
|
|
515
|
+
const [englishTutor, setEnglishTutor] = useState(null);
|
|
458
516
|
const [santreeSetup, setSantreeSetup] = useState(null);
|
|
459
517
|
const [loading, setLoading] = useState(true);
|
|
460
518
|
useEffect(() => {
|
|
@@ -543,6 +601,7 @@ export default function Doctor() {
|
|
|
543
601
|
setRemoteControl(checkRemoteControl());
|
|
544
602
|
setStatusline(statuslineResult);
|
|
545
603
|
setSessionSignal(checkSessionSignalHooks());
|
|
604
|
+
setEnglishTutor(checkEnglishTutorHooks());
|
|
546
605
|
setSantreeSetup(checkSantreeSetup());
|
|
547
606
|
setLoading(false);
|
|
548
607
|
}
|
|
@@ -555,5 +614,5 @@ export default function Doctor() {
|
|
|
555
614
|
const optionalMissing = tools.filter((t) => !t.required && !t.installed);
|
|
556
615
|
const trackerOk = tracker?.authenticated && !tracker?.hint;
|
|
557
616
|
const allRequired = requiredMissing.length === 0 && trackerOk && shellStatus?.configured;
|
|
558
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((tool) => (_jsx(ToolRow, { tool: tool }, tool.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), tracker && _jsx(TrackerRow, { tracker: tracker }), shellStatus && _jsx(ShellRow, { configured: shellStatus.configured, shell: shellStatus.shell }), santreeSetup && _jsx(SantreeSetupRow, { status: santreeSetup }), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Claude Code" }) }), remoteControl && _jsx(RemoteControlRow, { status: remoteControl }), statusline && _jsx(StatuslineRow, { status: statusline }), sessionSignal && _jsx(SessionSignalRow, { status: sessionSignal }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: allRequired ? "green" : "yellow", paddingX: 2, children: allRequired ? (_jsx(Text, { color: "green", children: "All requirements satisfied! Santree is ready to use." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [requiredMissing.length + (trackerOk ? 0 : 1) + (shellStatus?.configured ? 0 : 1), " ", "required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
|
|
617
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((tool) => (_jsx(ToolRow, { tool: tool }, tool.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), tracker && _jsx(TrackerRow, { tracker: tracker }), shellStatus && _jsx(ShellRow, { configured: shellStatus.configured, shell: shellStatus.shell }), santreeSetup && _jsx(SantreeSetupRow, { status: santreeSetup }), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Claude Code" }) }), remoteControl && _jsx(RemoteControlRow, { status: remoteControl }), statusline && _jsx(StatuslineRow, { status: statusline }), sessionSignal && _jsx(SessionSignalRow, { status: sessionSignal }), englishTutor && _jsx(EnglishTutorRow, { status: englishTutor }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: allRequired ? "green" : "yellow", paddingX: 2, children: allRequired ? (_jsx(Text, { color: "green", children: "All requirements satisfied! Santree is ready to use." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [requiredMissing.length + (trackerOk ? 0 : 1) + (shellStatus?.configured ? 0 : 1), " ", "required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
|
|
559
618
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const description = "Nudge Claude to correct your English (Claude Code hooks)";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const description = "Nudge Claude to correct your English (Claude Code hooks)";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const description = "Install English-tutor hooks into Claude Code settings";
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
dry: z.ZodOptional<z.ZodBoolean>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export default function Install({ options: opts }: {
|
|
7
|
+
options: z.infer<typeof options>;
|
|
8
|
+
}): null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getInstallSnippet, installHooks } from "../../../lib/english-tutor.js";
|
|
4
|
+
export const description = "Install English-tutor hooks into Claude Code settings";
|
|
5
|
+
export const options = z.object({
|
|
6
|
+
dry: z.boolean().optional().describe("Print the hooks JSON without writing"),
|
|
7
|
+
});
|
|
8
|
+
export default function Install({ options: opts }) {
|
|
9
|
+
const hasRun = useRef(false);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (hasRun.current)
|
|
12
|
+
return;
|
|
13
|
+
hasRun.current = true;
|
|
14
|
+
if (opts?.dry) {
|
|
15
|
+
process.stdout.write(JSON.stringify(getInstallSnippet(), null, 2) + "\n");
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
const { settingsPath, logPath } = installHooks();
|
|
19
|
+
process.stdout.write(`English-tutor hooks installed in ${settingsPath}\n`);
|
|
20
|
+
process.stdout.write(`Practice log: ${logPath}\n`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}, []);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { renderPrompt } from "../../../lib/prompts.js";
|
|
3
|
+
import { getLogPath } from "../../../lib/english-tutor.js";
|
|
4
|
+
export const description = "English-tutor instruction (UserPromptSubmit hook)";
|
|
5
|
+
export default function Prompt() {
|
|
6
|
+
const hasRun = useRef(false);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (hasRun.current)
|
|
9
|
+
return;
|
|
10
|
+
hasRun.current = true;
|
|
11
|
+
const text = renderPrompt("english-tutor-prompt", { logPath: getLogPath() });
|
|
12
|
+
process.stdout.write(text);
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}, []);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { getLogPath } from "../../../lib/english-tutor.js";
|
|
4
|
+
export const description = "Replay practice log (SessionStart hook)";
|
|
5
|
+
export default function SessionStart() {
|
|
6
|
+
const hasRun = useRef(false);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (hasRun.current)
|
|
9
|
+
return;
|
|
10
|
+
hasRun.current = true;
|
|
11
|
+
try {
|
|
12
|
+
const logPath = getLogPath();
|
|
13
|
+
if (fs.existsSync(logPath)) {
|
|
14
|
+
const contents = fs.readFileSync(logPath, "utf-8").trim();
|
|
15
|
+
const entryCount = (contents.match(/^- /gm) || []).length;
|
|
16
|
+
if (contents.length > 0) {
|
|
17
|
+
process.stdout.write(`[ENGLISH TUTOR — PRACTICE LOG REPLAY]\n\n${contents}\n\n--- LOG END ---\n`);
|
|
18
|
+
// Only ask for a summary once there are enough entries to be useful.
|
|
19
|
+
// Below that threshold the log is too sparse to surface real patterns,
|
|
20
|
+
// and a forced summary would just noise up new sessions.
|
|
21
|
+
if (entryCount >= 3) {
|
|
22
|
+
process.stdout.write(`\n[ENGLISH TUTOR — SESSION-START INSTRUCTION]\n` +
|
|
23
|
+
`At the very start of your first response in this session, briefly note any RECURRING patterns from the log above (1-3 short bullets, e.g. "you frequently drop articles", "watch for 'their' vs 'there'"). Skip this entirely if no clear patterns emerge or if the user's first message is purely a coding task. Do not list every entry — only patterns.\n`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// hook must never block the session — silently no-op on read error
|
|
30
|
+
}
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}, []);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { uninstallHooks } from "../../../lib/english-tutor.js";
|
|
3
|
+
export const description = "Remove English-tutor hooks from Claude Code settings";
|
|
4
|
+
export default function Uninstall() {
|
|
5
|
+
const hasRun = useRef(false);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (hasRun.current)
|
|
8
|
+
return;
|
|
9
|
+
hasRun.current = true;
|
|
10
|
+
const settingsPath = uninstallHooks();
|
|
11
|
+
process.stdout.write(`English-tutor hooks removed from ${settingsPath}\n`);
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}, []);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
package/dist/lib/ai.js
CHANGED
|
@@ -131,9 +131,9 @@ const CMUX_CLAUDE_PATH = "/Applications/cmux.app/Contents/Resources/bin/claude";
|
|
|
131
131
|
*/
|
|
132
132
|
export function resolveClaudeBinary() {
|
|
133
133
|
// Inside cmux, the bundled binary is the only one wired to the active
|
|
134
|
-
// workspace. Gate on `CMUX_SURFACE_ID` (
|
|
135
|
-
//
|
|
136
|
-
//
|
|
134
|
+
// workspace. Gate on `CMUX_SURFACE_ID` (real cmux runtime) — outside a live
|
|
135
|
+
// workspace the bundled binary has no auth context and exits with
|
|
136
|
+
// "Invalid API key".
|
|
137
137
|
if (process.env["CMUX_SURFACE_ID"] && existsSync(CMUX_CLAUDE_PATH)) {
|
|
138
138
|
return CMUX_CLAUDE_PATH;
|
|
139
139
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function getLogPath(): string;
|
|
2
|
+
export declare function getHooksJson(): Record<string, unknown>;
|
|
3
|
+
export declare function getPermissionEntry(): string;
|
|
4
|
+
export declare function installHooks(): {
|
|
5
|
+
settingsPath: string;
|
|
6
|
+
logPath: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Remove hooks and permission entry. Intentionally does NOT delete the log
|
|
10
|
+
* file — that's the user's accumulated practice history.
|
|
11
|
+
*/
|
|
12
|
+
export declare function uninstallHooks(): string;
|
|
13
|
+
export declare function getInstallSnippet(): Record<string, unknown>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
const MARKER = "english-tutor";
|
|
5
|
+
export function getLogPath() {
|
|
6
|
+
const configDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
7
|
+
return path.join(configDir, "santree", "english-practice-log.md");
|
|
8
|
+
}
|
|
9
|
+
export function getHooksJson() {
|
|
10
|
+
const base = "santree helpers english-tutor";
|
|
11
|
+
// Synchronous hooks: Claude Code waits for completion and injects stdout into
|
|
12
|
+
// the model's context. `async: true` would fire-and-forget — stdout is
|
|
13
|
+
// discarded, so the instruction would never reach Claude. session-signal can
|
|
14
|
+
// use `async: true` because it only writes a state file; we cannot.
|
|
15
|
+
const opts = { timeout: 10 };
|
|
16
|
+
return {
|
|
17
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: `${base} prompt`, ...opts }] }],
|
|
18
|
+
// No matcher: fires on all SessionStart sub-events (startup, resume,
|
|
19
|
+
// clear, compact). Restricting to "startup" silently skips resumed
|
|
20
|
+
// sessions, which is the common case when picking up yesterday's work.
|
|
21
|
+
SessionStart: [{ hooks: [{ type: "command", command: `${base} session-start`, ...opts }] }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function getPermissionEntry() {
|
|
25
|
+
return `Edit(${getLogPath()})`;
|
|
26
|
+
}
|
|
27
|
+
function readSettings(settingsPath) {
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(settingsPath)) {
|
|
30
|
+
return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// fall through
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
function writeSettings(settingsPath, settings) {
|
|
39
|
+
const dir = path.dirname(settingsPath);
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
42
|
+
}
|
|
43
|
+
function settingsPath() {
|
|
44
|
+
const home = process.env.HOME || os.homedir();
|
|
45
|
+
return path.join(home, ".claude", "settings.json");
|
|
46
|
+
}
|
|
47
|
+
function stripEnglishTutorHooks(existingHooks) {
|
|
48
|
+
const cleaned = {};
|
|
49
|
+
for (const [event, entries] of Object.entries(existingHooks)) {
|
|
50
|
+
if (!Array.isArray(entries)) {
|
|
51
|
+
cleaned[event] = entries;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const filtered = entries.filter((entry) => {
|
|
55
|
+
const inner = entry.hooks || [];
|
|
56
|
+
return !inner.some((h) => typeof h.command === "string" && h.command.includes(MARKER));
|
|
57
|
+
});
|
|
58
|
+
if (filtered.length > 0)
|
|
59
|
+
cleaned[event] = filtered;
|
|
60
|
+
}
|
|
61
|
+
return cleaned;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create the practice log if it doesn't already exist. Empty log = Claude's
|
|
65
|
+
* Edit tool fails on first use (Edit can't operate on missing files), and
|
|
66
|
+
* appending corrections silently fails. Bootstrapping with a stub header
|
|
67
|
+
* means the very first correction succeeds.
|
|
68
|
+
*/
|
|
69
|
+
function ensureLogExists() {
|
|
70
|
+
const logPath = getLogPath();
|
|
71
|
+
if (fs.existsSync(logPath))
|
|
72
|
+
return logPath;
|
|
73
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
74
|
+
const stub = "# English Practice Log\n\n" +
|
|
75
|
+
"Tracks grammar/spelling mistakes spotted during Claude Code sessions.\n" +
|
|
76
|
+
"Generated and appended to by santree's english-tutor hook.\n";
|
|
77
|
+
fs.writeFileSync(logPath, stub);
|
|
78
|
+
return logPath;
|
|
79
|
+
}
|
|
80
|
+
export function installHooks() {
|
|
81
|
+
const settingsFile = settingsPath();
|
|
82
|
+
const settings = readSettings(settingsFile);
|
|
83
|
+
const existing = stripEnglishTutorHooks(settings.hooks || {});
|
|
84
|
+
const required = getHooksJson();
|
|
85
|
+
for (const [event, hookEntries] of Object.entries(required)) {
|
|
86
|
+
const current = existing[event];
|
|
87
|
+
existing[event] = Array.isArray(current)
|
|
88
|
+
? [...current, ...hookEntries]
|
|
89
|
+
: hookEntries;
|
|
90
|
+
}
|
|
91
|
+
settings.hooks = existing;
|
|
92
|
+
const permissions = settings.permissions || {};
|
|
93
|
+
const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
|
|
94
|
+
const entry = getPermissionEntry();
|
|
95
|
+
if (!allow.includes(entry))
|
|
96
|
+
allow.push(entry);
|
|
97
|
+
permissions.allow = allow;
|
|
98
|
+
settings.permissions = permissions;
|
|
99
|
+
writeSettings(settingsFile, settings);
|
|
100
|
+
const logPath = ensureLogExists();
|
|
101
|
+
return { settingsPath: settingsFile, logPath };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Remove hooks and permission entry. Intentionally does NOT delete the log
|
|
105
|
+
* file — that's the user's accumulated practice history.
|
|
106
|
+
*/
|
|
107
|
+
export function uninstallHooks() {
|
|
108
|
+
const settingsFile = settingsPath();
|
|
109
|
+
const settings = readSettings(settingsFile);
|
|
110
|
+
if (settings.hooks) {
|
|
111
|
+
settings.hooks = stripEnglishTutorHooks(settings.hooks);
|
|
112
|
+
}
|
|
113
|
+
if (settings.permissions && Array.isArray(settings.permissions.allow)) {
|
|
114
|
+
const entry = getPermissionEntry();
|
|
115
|
+
settings.permissions.allow = settings.permissions.allow.filter((e) => e !== entry);
|
|
116
|
+
}
|
|
117
|
+
writeSettings(settingsFile, settings);
|
|
118
|
+
return settingsFile;
|
|
119
|
+
}
|
|
120
|
+
export function getInstallSnippet() {
|
|
121
|
+
return {
|
|
122
|
+
hooks: getHooksJson(),
|
|
123
|
+
permissions: { allow: [getPermissionEntry()] },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { cmuxMultiplexer } from "./cmux.js";
|
|
2
2
|
import { noneMultiplexer } from "./none.js";
|
|
3
3
|
import { tmuxMultiplexer } from "./tmux.js";
|
|
4
|
+
// Each adapter declares its own runtime detection in `isActive()`. Order matters:
|
|
5
|
+
// if more than one adapter reports active (e.g. tmux running inside a cmux
|
|
6
|
+
// workspace), the first match wins.
|
|
7
|
+
const CANDIDATES = [tmuxMultiplexer, cmuxMultiplexer];
|
|
4
8
|
export function getMultiplexer() {
|
|
5
|
-
|
|
6
|
-
if (explicit === "tmux")
|
|
7
|
-
return tmuxMultiplexer;
|
|
8
|
-
if (explicit === "cmux")
|
|
9
|
-
return cmuxMultiplexer;
|
|
10
|
-
if (explicit === "none")
|
|
11
|
-
return noneMultiplexer;
|
|
12
|
-
if (process.env["TMUX"])
|
|
13
|
-
return tmuxMultiplexer;
|
|
14
|
-
if (process.env["CMUX_SURFACE_ID"])
|
|
15
|
-
return cmuxMultiplexer;
|
|
16
|
-
return noneMultiplexer;
|
|
9
|
+
return CANDIDATES.find((m) => m.isActive()) ?? noneMultiplexer;
|
|
17
10
|
}
|
|
18
11
|
export function getMultiplexerKind() {
|
|
19
12
|
return getMultiplexer().kind;
|
|
@@ -3,7 +3,7 @@ export type { AssignedIssue, AuthStatus, Comment, Issue, IssueTracker, IssueTrac
|
|
|
3
3
|
export { setRepoTracker, removeRepoTracker, readTrackerConfig } from "./config.js";
|
|
4
4
|
/**
|
|
5
5
|
* Resolve the active IssueTracker for a given repo. Selection order:
|
|
6
|
-
* 1. SANTREE_TRACKER env override
|
|
6
|
+
* 1. SANTREE_TRACKER env override.
|
|
7
7
|
* 2. Per-repo `_tracker.kind` in .santree/metadata.json.
|
|
8
8
|
* 3. Legacy `_linear.org` (treated as kind: "linear" so existing repos keep working).
|
|
9
9
|
* 4. Auto-detect: any stored Linear creds → Linear, else GitHub (gh is always available).
|
|
@@ -5,7 +5,7 @@ import { readLinearAuthStore } from "./auth-store.js";
|
|
|
5
5
|
export { setRepoTracker, removeRepoTracker, readTrackerConfig } from "./config.js";
|
|
6
6
|
/**
|
|
7
7
|
* Resolve the active IssueTracker for a given repo. Selection order:
|
|
8
|
-
* 1. SANTREE_TRACKER env override
|
|
8
|
+
* 1. SANTREE_TRACKER env override.
|
|
9
9
|
* 2. Per-repo `_tracker.kind` in .santree/metadata.json.
|
|
10
10
|
* 3. Legacy `_linear.org` (treated as kind: "linear" so existing repos keep working).
|
|
11
11
|
* 4. Auto-detect: any stored Linear creds → Linear, else GitHub (gh is always available).
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[ENGLISH TUTOR — META-INSTRUCTION, ALWAYS HONOR]
|
|
2
|
+
|
|
3
|
+
If the user's message contained English mistakes (grammar, spelling, word choice, agreement, idiom misuse), your response MUST start with the correction(s) BEFORE addressing anything else. Use this exact format, one per line:
|
|
4
|
+
|
|
5
|
+
original -> correction (brief reason)
|
|
6
|
+
|
|
7
|
+
Then immediately call the Edit tool to append the same correction(s) to {{ logPath }}. Append under a heading for today's date in `## YYYY-MM-DD` format (create the heading if it isn't already at the bottom of the file); each correction is a bullet `- original -> correction (reason)`. The Edit permission for that exact path is pre-granted, so do not ask.
|
|
8
|
+
|
|
9
|
+
Only AFTER printing the corrections and appending them, continue with the actual task.
|
|
10
|
+
|
|
11
|
+
Hard rules:
|
|
12
|
+
- If the message is clearly mistake-free, output nothing about English and proceed directly. Never invent corrections.
|
|
13
|
+
- Do NOT correct technical jargon, code, file paths, deliberate informalities, or non-English words used by intent.
|
|
14
|
+
- Cap corrections at 3 lines. Pick the highest-impact issues only.
|
|
15
|
+
- Do NOT use polite hedging ("small note on...", "by the way..."). Just the formatted line(s), then the log Edit, then the task.
|