santree 0.5.3 → 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 +156 -46
- package/dist/commands/dashboard.d.ts +1 -1
- package/dist/commands/dashboard.js +22 -18
- package/dist/commands/doctor.js +97 -76
- package/dist/commands/github/auth.d.ts +2 -0
- package/dist/commands/github/auth.js +56 -0
- package/dist/commands/github/index.d.ts +1 -0
- package/dist/commands/github/index.js +1 -0
- 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/commands/helpers/template.d.ts +1 -0
- package/dist/commands/helpers/template.js +13 -10
- package/dist/commands/issue/index.d.ts +1 -0
- package/dist/commands/issue/index.js +1 -0
- package/dist/commands/issue/open.d.ts +2 -0
- package/dist/commands/{linear → issue}/open.js +13 -11
- package/dist/commands/issue/switch.d.ts +11 -0
- package/dist/commands/issue/switch.js +38 -0
- package/dist/commands/linear/auth.js +23 -10
- package/dist/commands/linear/switch.js +7 -3
- package/dist/commands/pr/create.js +7 -5
- package/dist/commands/worktree/create.js +4 -6
- package/dist/commands/worktree/work.js +1 -1
- package/dist/lib/ai.d.ts +8 -6
- package/dist/lib/ai.js +29 -15
- package/dist/lib/dashboard/DetailPanel.d.ts +5 -2
- package/dist/lib/dashboard/DetailPanel.js +6 -3
- package/dist/lib/dashboard/data.js +17 -9
- package/dist/lib/dashboard/types.d.ts +3 -16
- package/dist/lib/english-tutor.d.ts +13 -0
- package/dist/lib/english-tutor.js +125 -0
- package/dist/lib/git.d.ts +16 -33
- package/dist/lib/git.js +20 -74
- package/dist/lib/metadata.d.ts +3 -0
- package/dist/lib/metadata.js +27 -0
- package/dist/lib/multiplexer/cmux.js +1 -1
- package/dist/lib/multiplexer/index.js +5 -12
- package/dist/lib/multiplexer/types.d.ts +1 -1
- package/dist/lib/prompts.d.ts +4 -3
- package/dist/lib/prompts.js +4 -3
- package/dist/lib/session-signal.d.ts +2 -3
- package/dist/lib/session-signal.js +3 -29
- package/dist/lib/trackers/auth-store.d.ts +16 -0
- package/dist/lib/trackers/auth-store.js +57 -0
- package/dist/lib/trackers/config.d.ts +8 -0
- package/dist/lib/trackers/config.js +21 -0
- package/dist/lib/trackers/github/api.d.ts +3 -0
- package/dist/lib/trackers/github/api.js +90 -0
- package/dist/lib/trackers/github/auth.d.ts +5 -0
- package/dist/lib/trackers/github/auth.js +27 -0
- package/dist/lib/trackers/github/images.d.ts +2 -0
- package/dist/lib/trackers/github/images.js +42 -0
- package/dist/lib/trackers/github/index.d.ts +2 -0
- package/dist/lib/trackers/github/index.js +78 -0
- package/dist/lib/trackers/index.d.ts +12 -0
- package/dist/lib/trackers/index.js +34 -0
- package/dist/lib/trackers/linear/api.d.ts +4 -0
- package/dist/lib/trackers/linear/api.js +128 -0
- package/dist/lib/trackers/linear/auth.d.ts +11 -0
- package/dist/lib/trackers/linear/auth.js +206 -0
- package/dist/lib/trackers/linear/images.d.ts +2 -0
- package/dist/lib/trackers/linear/images.js +44 -0
- package/dist/lib/trackers/linear/index.d.ts +3 -0
- package/dist/lib/trackers/linear/index.js +100 -0
- package/dist/lib/trackers/types.d.ts +52 -0
- package/dist/lib/trackers/types.js +1 -0
- package/package.json +1 -1
- package/prompts/english-tutor-prompt.njk +15 -0
- package/prompts/ticket.njk +3 -3
- package/dist/commands/linear/open.d.ts +0 -2
- package/dist/lib/linear.d.ts +0 -83
- package/dist/lib/linear.js +0 -482
package/dist/commands/doctor.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as path from "path";
|
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const { version } = require("../../package.json");
|
|
12
12
|
import { findMainRepoRoot, getSantreeDir, getInitScriptPath } from "../lib/git.js";
|
|
13
|
-
import {
|
|
13
|
+
import { getIssueTracker } from "../lib/trackers/index.js";
|
|
14
14
|
import { getMultiplexer } from "../lib/multiplexer/index.js";
|
|
15
15
|
import { resolveClaudeBinary } from "../lib/ai.js";
|
|
16
16
|
import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, SANTREE_PACKAGE, getLatestVersionFor, isUpdateAvailable, detectPackageManager, getInstallCommandFor, } from "../lib/version.js";
|
|
@@ -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
|
/**
|
|
@@ -135,8 +133,9 @@ async function checkMultiplexer() {
|
|
|
135
133
|
*/
|
|
136
134
|
async function checkClaude() {
|
|
137
135
|
const resolved = resolveClaudeBinary();
|
|
136
|
+
const usingBundled = resolved?.startsWith("/Applications/cmux.app/") ?? false;
|
|
138
137
|
const inCmux = getMultiplexer().kind === "cmux";
|
|
139
|
-
const description =
|
|
138
|
+
const description = usingBundled ? "Claude Code CLI (cmux-bundled)" : "Claude Code CLI";
|
|
140
139
|
if (!resolved) {
|
|
141
140
|
return {
|
|
142
141
|
name: "claude",
|
|
@@ -197,30 +196,22 @@ async function checkGhAuth() {
|
|
|
197
196
|
};
|
|
198
197
|
}
|
|
199
198
|
/**
|
|
200
|
-
* Checks
|
|
199
|
+
* Checks the active issue tracker's auth state. The tracker (Linear, GitHub)
|
|
200
|
+
* is resolved from the repo's `_tracker` config (or env / auto-detect).
|
|
201
|
+
* Doctor is the one place that legitimately names the active tracker — it's
|
|
202
|
+
* diagnostic context, not a generic UI string.
|
|
201
203
|
*/
|
|
202
|
-
async function
|
|
204
|
+
async function checkTrackerAuth() {
|
|
203
205
|
const repoRoot = findMainRepoRoot();
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
authenticated: false,
|
|
208
|
-
hint: "Run: santree linear auth",
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
// Try to validate/refresh tokens
|
|
212
|
-
const valid = await getValidTokens(status.orgSlug);
|
|
206
|
+
const tracker = getIssueTracker(repoRoot);
|
|
207
|
+
const status = await tracker.getAuthStatus(repoRoot);
|
|
213
208
|
return {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
209
|
+
displayName: tracker.displayName,
|
|
210
|
+
authenticated: status.authenticated,
|
|
211
|
+
accountLabel: status.accountLabel,
|
|
212
|
+
expiresAt: status.expiresAt,
|
|
218
213
|
repoLinked: status.repoLinked,
|
|
219
|
-
hint:
|
|
220
|
-
? "Token expired. Run: santree linear auth"
|
|
221
|
-
: !status.repoLinked
|
|
222
|
-
? "Repo not linked. Run: santree linear auth"
|
|
223
|
-
: undefined,
|
|
214
|
+
hint: status.hint,
|
|
224
215
|
};
|
|
225
216
|
}
|
|
226
217
|
/**
|
|
@@ -334,40 +325,64 @@ function checkSessionSignalHooks() {
|
|
|
334
325
|
catch {
|
|
335
326
|
missingHooks.push(...requiredEvents);
|
|
336
327
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
328
|
+
if (missingHooks.length === 0) {
|
|
329
|
+
return { configured: true, missingHooks: [] };
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
configured: false,
|
|
333
|
+
missingHooks,
|
|
334
|
+
hint: `Missing: ${missingHooks.join(", ")}. Run: santree helpers session-signal install`,
|
|
335
|
+
};
|
|
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;
|
|
355
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);
|
|
356
365
|
}
|
|
357
|
-
|
|
358
|
-
|
|
366
|
+
const allow = settings.permissions?.allow;
|
|
367
|
+
if (Array.isArray(allow) && allow.includes(expectedPermission)) {
|
|
368
|
+
missingPermission = false;
|
|
359
369
|
}
|
|
360
370
|
}
|
|
371
|
+
else {
|
|
372
|
+
missingHooks.push(...requiredEvents);
|
|
373
|
+
}
|
|
361
374
|
}
|
|
362
|
-
|
|
363
|
-
|
|
375
|
+
catch {
|
|
376
|
+
missingHooks.push(...requiredEvents);
|
|
377
|
+
}
|
|
378
|
+
if (missingHooks.length === 0 && !missingPermission) {
|
|
379
|
+
return { configured: true, missingHooks: [], missingPermission: false };
|
|
364
380
|
}
|
|
365
381
|
return {
|
|
366
382
|
configured: false,
|
|
367
383
|
missingHooks,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
hint: `Missing: ${missingHooks.join(", ")}. Run: santree helpers session-signal install`,
|
|
384
|
+
missingPermission,
|
|
385
|
+
hint: "Run: santree helpers english-tutor install",
|
|
371
386
|
};
|
|
372
387
|
}
|
|
373
388
|
/**
|
|
@@ -451,9 +466,9 @@ function StatusIcon({ ok, required }) {
|
|
|
451
466
|
function ToolRow({ tool }) {
|
|
452
467
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: tool.installed && !tool.hint, required: tool.required }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: tool.name }), _jsxs(Text, { dimColor: true, children: [" - ", tool.description] }), !tool.required && _jsx(Text, { dimColor: true, children: " (optional)" })] }), tool.installed ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Version: ", tool.version] }), tool.latestVersion && tool.version && (_jsxs(Text, { color: isUpdateAvailable(tool.version, tool.latestVersion) ? "yellow" : undefined, dimColor: !isUpdateAvailable(tool.version, tool.latestVersion), children: ["Latest: ", tool.latestVersion, isUpdateAvailable(tool.version, tool.latestVersion) ? " ⬆ update available" : ""] })), tool.path && _jsxs(Text, { dimColor: true, children: ["Path: ", tool.path] }), tool.authStatus && _jsxs(Text, { dimColor: true, children: ["Auth: ", tool.authStatus] }), tool.updateHint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.updateHint] }), tool.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] }) }))] }));
|
|
453
468
|
}
|
|
454
|
-
function
|
|
455
|
-
const isOk =
|
|
456
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok:
|
|
469
|
+
function TrackerRow({ tracker }) {
|
|
470
|
+
const isOk = tracker.authenticated && !tracker.hint;
|
|
471
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: isOk, required: true }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: [tracker.displayName, " API"] }), _jsx(Text, { dimColor: true, children: " - Issue tracker integration" })] }), tracker.authenticated ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [tracker.accountLabel && _jsxs(Text, { dimColor: true, children: ["Account: ", tracker.accountLabel] }), tracker.repoLinked !== undefined && (_jsxs(Text, { dimColor: true, children: ["Repo linked: ", tracker.repoLinked ? "yes" : "no"] })), tracker.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tracker.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tracker.hint] }) }))] }));
|
|
457
472
|
}
|
|
458
473
|
function ShellRow({ configured, shell }) {
|
|
459
474
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: configured, required: true }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Shell Integration" }), _jsx(Text, { dimColor: true, children: " - Enables directory switching" })] }), configured ? (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["Shell: ", shell] }) })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 Add to .", shell, "rc: eval \"$(santree helpers shell-init ", shell, ")\""] }) }))] }));
|
|
@@ -465,13 +480,15 @@ function RemoteControlRow({ status }) {
|
|
|
465
480
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.enabled, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Remote Control" }), _jsx(Text, { dimColor: true, children: " - Continue sessions from any device" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Enabled: ", status.enabled ? "yes" : "no"] }), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })] }));
|
|
466
481
|
}
|
|
467
482
|
function SessionSignalRow({ status }) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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] })] })) })] }));
|
|
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] })] })) })] }));
|
|
475
492
|
}
|
|
476
493
|
function SantreeSetupRow({ status }) {
|
|
477
494
|
const isOk = status.santreeFolderExists &&
|
|
@@ -490,11 +507,12 @@ function SantreeSetupRow({ status }) {
|
|
|
490
507
|
}
|
|
491
508
|
export default function Doctor() {
|
|
492
509
|
const [tools, setTools] = useState([]);
|
|
493
|
-
const [
|
|
510
|
+
const [tracker, setTracker] = useState(null);
|
|
494
511
|
const [shellStatus, setShellStatus] = useState(null);
|
|
495
512
|
const [remoteControl, setRemoteControl] = useState(null);
|
|
496
513
|
const [statusline, setStatusline] = useState(null);
|
|
497
514
|
const [sessionSignal, setSessionSignal] = useState(null);
|
|
515
|
+
const [englishTutor, setEnglishTutor] = useState(null);
|
|
498
516
|
const [santreeSetup, setSantreeSetup] = useState(null);
|
|
499
517
|
const [loading, setLoading] = useState(true);
|
|
500
518
|
useEffect(() => {
|
|
@@ -541,12 +559,14 @@ export default function Doctor() {
|
|
|
541
559
|
}
|
|
542
560
|
}
|
|
543
561
|
}
|
|
544
|
-
// Optional: a syntax-
|
|
562
|
+
// Optional: a syntax-highlighting diff pager — used by `st worktree diff`
|
|
545
563
|
// and the dashboard `v` overlay when SANTREE_DIFF_TOOL is set. Any
|
|
546
|
-
// pager works (delta, diff-so-fancy, …); without one set,
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
|
|
564
|
+
// diff pager works (delta, diff-so-fancy, …); without one set, the
|
|
565
|
+
// dashboard renders inline with santree's own colorizer and the CLI
|
|
566
|
+
// falls back to git's default pager. Delta is the most popular
|
|
567
|
+
// choice so we check for it as a convenience, but it is never a
|
|
568
|
+
// hard dependency.
|
|
569
|
+
const deltaCheck = await checkTool("delta", "Recommended diff pager — any diff pager works", false, "delta --version | head -1", "Optional — santree's built-in colorizer handles the dashboard overlay; the CLI falls back to git's default pager. Set SANTREE_DIFF_TOOL to override. To install delta: brew install git-delta");
|
|
550
570
|
results.push(deltaCheck);
|
|
551
571
|
// Optional: a `.code-workspace`-aware editor (VSCode or Cursor).
|
|
552
572
|
// Santree itself works with any editor via $SANTREE_EDITOR — this
|
|
@@ -573,14 +593,15 @@ export default function Doctor() {
|
|
|
573
593
|
hint: "Optional — santree works with any $SANTREE_EDITOR. Only needed for the dashboard's `.code-workspace` shortcut.",
|
|
574
594
|
});
|
|
575
595
|
}
|
|
576
|
-
const
|
|
596
|
+
const trackerResult = await checkTrackerAuth();
|
|
577
597
|
const statuslineResult = await checkStatusline();
|
|
578
598
|
setTools(results);
|
|
579
|
-
|
|
599
|
+
setTracker(trackerResult);
|
|
580
600
|
setShellStatus(checkShellIntegration());
|
|
581
601
|
setRemoteControl(checkRemoteControl());
|
|
582
602
|
setStatusline(statuslineResult);
|
|
583
603
|
setSessionSignal(checkSessionSignalHooks());
|
|
604
|
+
setEnglishTutor(checkEnglishTutorHooks());
|
|
584
605
|
setSantreeSetup(checkSantreeSetup());
|
|
585
606
|
setLoading(false);
|
|
586
607
|
}
|
|
@@ -591,7 +612,7 @@ export default function Doctor() {
|
|
|
591
612
|
}
|
|
592
613
|
const requiredMissing = tools.filter((t) => t.required && (!t.installed || t.hint));
|
|
593
614
|
const optionalMissing = tools.filter((t) => !t.required && !t.installed);
|
|
594
|
-
const
|
|
595
|
-
const allRequired = requiredMissing.length === 0 &&
|
|
596
|
-
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" }) }),
|
|
615
|
+
const trackerOk = tracker?.authenticated && !tracker?.hint;
|
|
616
|
+
const allRequired = requiredMissing.length === 0 && trackerOk && shellStatus?.configured;
|
|
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"] }))] })) })] }));
|
|
597
618
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Text, Box } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import { findMainRepoRoot } from "../../lib/git.js";
|
|
8
|
+
import { getAuthenticatedUser } from "../../lib/trackers/github/auth.js";
|
|
9
|
+
import { setRepoTracker } from "../../lib/trackers/index.js";
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
export const description = "Authenticate with GitHub via the gh CLI";
|
|
12
|
+
export default function GithubAuth() {
|
|
13
|
+
const [status, setStatus] = useState("checking");
|
|
14
|
+
const [message, setMessage] = useState("");
|
|
15
|
+
const [error, setError] = useState(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
async function run() {
|
|
18
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
19
|
+
const repoRoot = findMainRepoRoot();
|
|
20
|
+
if (!repoRoot) {
|
|
21
|
+
setError("Not inside a git repository");
|
|
22
|
+
setStatus("error");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
let user = await getAuthenticatedUser();
|
|
26
|
+
if (!user) {
|
|
27
|
+
setStatus("logging-in");
|
|
28
|
+
try {
|
|
29
|
+
await execAsync("gh auth login -p https -h github.com -w", { stdio: "inherit" });
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
setError(e instanceof Error ? e.message : "gh auth login failed");
|
|
33
|
+
setStatus("error");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
user = await getAuthenticatedUser();
|
|
37
|
+
if (!user) {
|
|
38
|
+
setError("gh auth login completed but no authenticated user — try `gh auth status`");
|
|
39
|
+
setStatus("error");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
setRepoTracker(repoRoot, "github");
|
|
44
|
+
setMessage(`Authenticated as @${user.login}; this repo now uses GitHub Issues`);
|
|
45
|
+
setStatus("done");
|
|
46
|
+
}
|
|
47
|
+
run();
|
|
48
|
+
}, []);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (status === "done" || status === "error") {
|
|
51
|
+
const timer = setTimeout(() => process.exit(status === "error" ? 1 : 0), 100);
|
|
52
|
+
return () => clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}, [status]);
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "GitHub Auth" }) }), status === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Checking gh auth status..." })] })), status === "logging-in" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Running `gh auth login` \u2014 follow the browser prompt..." })] })), status === "done" && (_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] }));
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const description = "GitHub Issues tracker commands";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const description = "GitHub Issues tracker commands";
|
|
@@ -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
|
+
}
|
|
@@ -6,13 +6,13 @@ import { argument } from "pastel";
|
|
|
6
6
|
import { z } from "zod/v4";
|
|
7
7
|
import { findRepoRoot, findMainRepoRoot, getCurrentBranch, extractTicketId, } from "../../lib/git.js";
|
|
8
8
|
import { renderTicket } from "../../lib/prompts.js";
|
|
9
|
-
import {
|
|
9
|
+
import { getIssueTracker } from "../../lib/trackers/index.js";
|
|
10
10
|
import { resolveAIContext, renderAIPrompt, fetchAndRenderPR, fetchAndRenderDiff, } from "../../lib/ai.js";
|
|
11
11
|
export const description = "Render a template to stdout";
|
|
12
12
|
export const args = z.tuple([
|
|
13
|
-
z.enum(["linear", "git-changes", "pr", "fix-pr", "review"]).describe(argument({
|
|
13
|
+
z.enum(["ticket", "linear", "git-changes", "pr", "fix-pr", "review"]).describe(argument({
|
|
14
14
|
name: "type",
|
|
15
|
-
description: "Template type (
|
|
15
|
+
description: "Template type (ticket, git-changes, pr, fix-pr, or review)",
|
|
16
16
|
})),
|
|
17
17
|
]);
|
|
18
18
|
export default function Template({ args }) {
|
|
@@ -93,22 +93,24 @@ export default function Template({ args }) {
|
|
|
93
93
|
setTimeout(() => exit(), 100);
|
|
94
94
|
}
|
|
95
95
|
else {
|
|
96
|
+
const mainRoot = findMainRepoRoot() ?? repoRoot;
|
|
97
|
+
const tracker = getIssueTracker(mainRoot);
|
|
96
98
|
const ticketId = extractTicketId(branch);
|
|
97
99
|
if (!ticketId) {
|
|
98
100
|
setStatus("error");
|
|
99
|
-
setMessage(
|
|
101
|
+
setMessage(`Could not extract ${tracker.issueNoun} ID from branch name '${branch}'.`);
|
|
100
102
|
setTimeout(() => exit(), 100);
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
const result = await tracker.getIssue(ticketId, mainRoot);
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
const status = await tracker.getAuthStatus(mainRoot);
|
|
106
108
|
setStatus("error");
|
|
107
|
-
setMessage(`Could not fetch
|
|
109
|
+
setMessage(`Could not fetch ${tracker.issueNoun} ${ticketId}.${status.hint ? ` ${status.hint}` : ""}`);
|
|
108
110
|
setTimeout(() => exit(), 100);
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
|
-
process.stdout.write(renderTicket(
|
|
113
|
+
process.stdout.write(renderTicket(result.value, tracker.displayName));
|
|
112
114
|
setStatus("done");
|
|
113
115
|
setTimeout(() => exit(), 100);
|
|
114
116
|
}
|
|
@@ -118,7 +120,8 @@ export default function Template({ args }) {
|
|
|
118
120
|
if (status === "done")
|
|
119
121
|
return null;
|
|
120
122
|
const spinnerTexts = {
|
|
121
|
-
|
|
123
|
+
ticket: "Fetching issue...",
|
|
124
|
+
linear: "Fetching issue...",
|
|
122
125
|
"git-changes": "Gathering changes...",
|
|
123
126
|
pr: "Fetching PR feedback...",
|
|
124
127
|
"fix-pr": "Building fix-pr prompt...",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const description = "Issue tracker commands (Linear / GitHub)";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const description = "Issue tracker commands (Linear / GitHub)";
|