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.
Files changed (80) hide show
  1. package/README.md +156 -46
  2. package/dist/commands/dashboard.d.ts +1 -1
  3. package/dist/commands/dashboard.js +22 -18
  4. package/dist/commands/doctor.js +97 -76
  5. package/dist/commands/github/auth.d.ts +2 -0
  6. package/dist/commands/github/auth.js +56 -0
  7. package/dist/commands/github/index.d.ts +1 -0
  8. package/dist/commands/github/index.js +1 -0
  9. package/dist/commands/helpers/english-tutor/index.d.ts +1 -0
  10. package/dist/commands/helpers/english-tutor/index.js +1 -0
  11. package/dist/commands/helpers/english-tutor/install.d.ts +8 -0
  12. package/dist/commands/helpers/english-tutor/install.js +24 -0
  13. package/dist/commands/helpers/english-tutor/prompt.d.ts +2 -0
  14. package/dist/commands/helpers/english-tutor/prompt.js +16 -0
  15. package/dist/commands/helpers/english-tutor/session-start.d.ts +2 -0
  16. package/dist/commands/helpers/english-tutor/session-start.js +34 -0
  17. package/dist/commands/helpers/english-tutor/uninstall.d.ts +2 -0
  18. package/dist/commands/helpers/english-tutor/uninstall.js +15 -0
  19. package/dist/commands/helpers/template.d.ts +1 -0
  20. package/dist/commands/helpers/template.js +13 -10
  21. package/dist/commands/issue/index.d.ts +1 -0
  22. package/dist/commands/issue/index.js +1 -0
  23. package/dist/commands/issue/open.d.ts +2 -0
  24. package/dist/commands/{linear → issue}/open.js +13 -11
  25. package/dist/commands/issue/switch.d.ts +11 -0
  26. package/dist/commands/issue/switch.js +38 -0
  27. package/dist/commands/linear/auth.js +23 -10
  28. package/dist/commands/linear/switch.js +7 -3
  29. package/dist/commands/pr/create.js +7 -5
  30. package/dist/commands/worktree/create.js +4 -6
  31. package/dist/commands/worktree/work.js +1 -1
  32. package/dist/lib/ai.d.ts +8 -6
  33. package/dist/lib/ai.js +29 -15
  34. package/dist/lib/dashboard/DetailPanel.d.ts +5 -2
  35. package/dist/lib/dashboard/DetailPanel.js +6 -3
  36. package/dist/lib/dashboard/data.js +17 -9
  37. package/dist/lib/dashboard/types.d.ts +3 -16
  38. package/dist/lib/english-tutor.d.ts +13 -0
  39. package/dist/lib/english-tutor.js +125 -0
  40. package/dist/lib/git.d.ts +16 -33
  41. package/dist/lib/git.js +20 -74
  42. package/dist/lib/metadata.d.ts +3 -0
  43. package/dist/lib/metadata.js +27 -0
  44. package/dist/lib/multiplexer/cmux.js +1 -1
  45. package/dist/lib/multiplexer/index.js +5 -12
  46. package/dist/lib/multiplexer/types.d.ts +1 -1
  47. package/dist/lib/prompts.d.ts +4 -3
  48. package/dist/lib/prompts.js +4 -3
  49. package/dist/lib/session-signal.d.ts +2 -3
  50. package/dist/lib/session-signal.js +3 -29
  51. package/dist/lib/trackers/auth-store.d.ts +16 -0
  52. package/dist/lib/trackers/auth-store.js +57 -0
  53. package/dist/lib/trackers/config.d.ts +8 -0
  54. package/dist/lib/trackers/config.js +21 -0
  55. package/dist/lib/trackers/github/api.d.ts +3 -0
  56. package/dist/lib/trackers/github/api.js +90 -0
  57. package/dist/lib/trackers/github/auth.d.ts +5 -0
  58. package/dist/lib/trackers/github/auth.js +27 -0
  59. package/dist/lib/trackers/github/images.d.ts +2 -0
  60. package/dist/lib/trackers/github/images.js +42 -0
  61. package/dist/lib/trackers/github/index.d.ts +2 -0
  62. package/dist/lib/trackers/github/index.js +78 -0
  63. package/dist/lib/trackers/index.d.ts +12 -0
  64. package/dist/lib/trackers/index.js +34 -0
  65. package/dist/lib/trackers/linear/api.d.ts +4 -0
  66. package/dist/lib/trackers/linear/api.js +128 -0
  67. package/dist/lib/trackers/linear/auth.d.ts +11 -0
  68. package/dist/lib/trackers/linear/auth.js +206 -0
  69. package/dist/lib/trackers/linear/images.d.ts +2 -0
  70. package/dist/lib/trackers/linear/images.js +44 -0
  71. package/dist/lib/trackers/linear/index.d.ts +3 -0
  72. package/dist/lib/trackers/linear/index.js +100 -0
  73. package/dist/lib/trackers/types.d.ts +52 -0
  74. package/dist/lib/trackers/types.js +1 -0
  75. package/package.json +1 -1
  76. package/prompts/english-tutor-prompt.njk +15 -0
  77. package/prompts/ticket.njk +3 -3
  78. package/dist/commands/linear/open.d.ts +0 -2
  79. package/dist/lib/linear.d.ts +0 -83
  80. package/dist/lib/linear.js +0 -482
@@ -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
+ }
package/dist/lib/git.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
1
2
  export interface SessionState {
2
3
  state: "waiting" | "idle" | "active" | "exited";
3
4
  message: string | null;
@@ -53,10 +54,6 @@ export declare function getDefaultBranch(): string;
53
54
  * Returns an empty array on failure.
54
55
  */
55
56
  export declare function listWorktrees(): Worktree[];
56
- /**
57
- * Get the path to the .santree directory inside a repo root.
58
- */
59
- export declare function getSantreeDir(repoRoot: string): string;
60
57
  /**
61
58
  * Get the path to the .santree/worktrees directory inside a repo root.
62
59
  */
@@ -82,9 +79,14 @@ export declare function removeWorktree(branchName: string, repoRoot: string, for
82
79
  error?: string;
83
80
  }>;
84
81
  /**
85
- * Extract a ticket ID (e.g. "TEAM-123") from a branch name.
86
- * Matches the first occurrence of LETTERS-DIGITS in the string.
87
- * Returns null if no ticket ID pattern is found.
82
+ * Extract an issue identifier from a branch name. Delegates to the active
83
+ * issue tracker (Linear: `[A-Z]+-\d+`; GitHub: explicit-prefix numeric),
84
+ * resolving the tracker from the main repo's config.
85
+ *
86
+ * This is the single shim that frees every caller from having to know which
87
+ * tracker is active or how it formats IDs. Returns null when the active
88
+ * tracker doesn't recognize the branch's ID shape, or when no main repo can
89
+ * be resolved (e.g. before a worktree is created).
88
90
  */
89
91
  export declare function extractTicketId(branch: string): string | null;
90
92
  /**
@@ -93,30 +95,6 @@ export declare function extractTicketId(branch: string): string | null;
93
95
  * Returns null if no worktree is checked out on that branch.
94
96
  */
95
97
  export declare function getWorktreePath(branchName: string): string | null;
96
- /**
97
- * Read all entries from .santree/metadata.json.
98
- * Returns an empty object if the file doesn't exist or can't be parsed.
99
- */
100
- export declare function readAllMetadata(repoRoot: string): Record<string, any>;
101
- /**
102
- * Write all entries to .santree/metadata.json.
103
- */
104
- export declare function writeAllMetadata(repoRoot: string, data: Record<string, any>): void;
105
- /**
106
- * Get the Linear org slug associated with this repo.
107
- * Stored as `_linear.org` in .santree/metadata.json.
108
- */
109
- export declare function getRepoLinearOrg(repoRoot: string): string | null;
110
- /**
111
- * Associate a Linear org slug with this repo.
112
- * Stored as `_linear.org` in .santree/metadata.json.
113
- */
114
- export declare function setRepoLinearOrg(repoRoot: string, orgSlug: string): void;
115
- /**
116
- * Remove the Linear org association from this repo.
117
- * Deletes the `_linear` key from .santree/metadata.json.
118
- */
119
- export declare function removeRepoLinearOrg(repoRoot: string): void;
120
98
  /**
121
99
  * Get the stored session ID for a given ticket from .santree/metadata.json.
122
100
  * Returns null if no session ID is stored.
@@ -191,8 +169,13 @@ export declare function getCommitsAhead(baseBranch: string): number;
191
169
  */
192
170
  export declare function getCommitsAheadAsync(cwd: string, baseBranch: string): Promise<number>;
193
171
  /**
194
- * Read the SANTREE_DIFF_TOOL env var, returning the configured pager command
195
- * (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
172
+ * Read the SANTREE_DIFF_TOOL env var, returning the configured diff pager
173
+ * command (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
174
+ *
175
+ * The CLI `worktree diff` flow lets the pager do its full job (render +
176
+ * scroll) via git's `core.pager`. The dashboard's `[v]` overlay only uses
177
+ * the rendering half — it captures the pager's stdout as a string and
178
+ * handles scrolling itself in Ink.
196
179
  *
197
180
  * The value is restricted to a safe shell-token character set since it ends
198
181
  * up in arguments passed to spawn() — even though we never use shell:true,
package/dist/lib/git.js CHANGED
@@ -4,6 +4,9 @@ import * as path from "path";
4
4
  import * as fs from "fs";
5
5
  import { run, runAsync, spawnAsync } from "./exec.js";
6
6
  import { getMultiplexer } from "./multiplexer/index.js";
7
+ import { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
8
+ import { getIssueTracker } from "./trackers/index.js";
9
+ export { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
7
10
  const execAsync = promisify(exec);
8
11
  /**
9
12
  * Find the toplevel directory of the current git repository.
@@ -114,12 +117,6 @@ export function listWorktrees() {
114
117
  }
115
118
  return worktrees;
116
119
  }
117
- /**
118
- * Get the path to the .santree directory inside a repo root.
119
- */
120
- export function getSantreeDir(repoRoot) {
121
- return path.join(repoRoot, ".santree");
122
- }
123
120
  /**
124
121
  * Get the path to the .santree/worktrees directory inside a repo root.
125
122
  */
@@ -247,16 +244,18 @@ export async function removeWorktree(branchName, repoRoot, force = false) {
247
244
  }
248
245
  }
249
246
  /**
250
- * Extract a ticket ID (e.g. "TEAM-123") from a branch name.
251
- * Matches the first occurrence of LETTERS-DIGITS in the string.
252
- * Returns null if no ticket ID pattern is found.
247
+ * Extract an issue identifier from a branch name. Delegates to the active
248
+ * issue tracker (Linear: `[A-Z]+-\d+`; GitHub: explicit-prefix numeric),
249
+ * resolving the tracker from the main repo's config.
250
+ *
251
+ * This is the single shim that frees every caller from having to know which
252
+ * tracker is active or how it formats IDs. Returns null when the active
253
+ * tracker doesn't recognize the branch's ID shape, or when no main repo can
254
+ * be resolved (e.g. before a worktree is created).
253
255
  */
254
256
  export function extractTicketId(branch) {
255
- const match = branch.match(/([a-zA-Z]+)-(\d+)/);
256
- if (match) {
257
- return `${match[1].toUpperCase()}-${match[2]}`;
258
- }
259
- return null;
257
+ const repoRoot = findMainRepoRoot();
258
+ return getIssueTracker(repoRoot).extractIdFromBranch(branch);
260
259
  }
261
260
  /**
262
261
  * Get the filesystem path for a worktree by its branch name.
@@ -278,64 +277,6 @@ export function getWorktreePath(branchName) {
278
277
  }
279
278
  return null;
280
279
  }
281
- /**
282
- * Get path to centralized metadata file: .santree/metadata.json in the repo root.
283
- */
284
- function getMetadataFilePath(repoRoot) {
285
- return path.join(getSantreeDir(repoRoot), "metadata.json");
286
- }
287
- /**
288
- * Read all entries from .santree/metadata.json.
289
- * Returns an empty object if the file doesn't exist or can't be parsed.
290
- */
291
- export function readAllMetadata(repoRoot) {
292
- const filePath = getMetadataFilePath(repoRoot);
293
- if (!fs.existsSync(filePath))
294
- return {};
295
- try {
296
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
297
- }
298
- catch {
299
- return {};
300
- }
301
- }
302
- /**
303
- * Write all entries to .santree/metadata.json.
304
- */
305
- export function writeAllMetadata(repoRoot, data) {
306
- const filePath = getMetadataFilePath(repoRoot);
307
- const dir = path.dirname(filePath);
308
- if (!fs.existsSync(dir)) {
309
- fs.mkdirSync(dir, { recursive: true });
310
- }
311
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
312
- }
313
- /**
314
- * Get the Linear org slug associated with this repo.
315
- * Stored as `_linear.org` in .santree/metadata.json.
316
- */
317
- export function getRepoLinearOrg(repoRoot) {
318
- const all = readAllMetadata(repoRoot);
319
- return all._linear?.org ?? null;
320
- }
321
- /**
322
- * Associate a Linear org slug with this repo.
323
- * Stored as `_linear.org` in .santree/metadata.json.
324
- */
325
- export function setRepoLinearOrg(repoRoot, orgSlug) {
326
- const all = readAllMetadata(repoRoot);
327
- all._linear = { org: orgSlug };
328
- writeAllMetadata(repoRoot, all);
329
- }
330
- /**
331
- * Remove the Linear org association from this repo.
332
- * Deletes the `_linear` key from .santree/metadata.json.
333
- */
334
- export function removeRepoLinearOrg(repoRoot) {
335
- const all = readAllMetadata(repoRoot);
336
- delete all._linear;
337
- writeAllMetadata(repoRoot, all);
338
- }
339
280
  /**
340
281
  * Get the stored session ID for a given ticket from .santree/metadata.json.
341
282
  * Returns null if no session ID is stored.
@@ -469,8 +410,13 @@ export async function getCommitsAheadAsync(cwd, baseBranch) {
469
410
  return output ? parseInt(output, 10) || 0 : 0;
470
411
  }
471
412
  /**
472
- * Read the SANTREE_DIFF_TOOL env var, returning the configured pager command
473
- * (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
413
+ * Read the SANTREE_DIFF_TOOL env var, returning the configured diff pager
414
+ * command (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
415
+ *
416
+ * The CLI `worktree diff` flow lets the pager do its full job (render +
417
+ * scroll) via git's `core.pager`. The dashboard's `[v]` overlay only uses
418
+ * the rendering half — it captures the pager's stdout as a string and
419
+ * handles scrolling itself in Ink.
474
420
  *
475
421
  * The value is restricted to a safe shell-token character set since it ends
476
422
  * up in arguments passed to spawn() — even though we never use shell:true,
@@ -0,0 +1,3 @@
1
+ export declare function getSantreeDir(repoRoot: string): string;
2
+ export declare function readAllMetadata(repoRoot: string): Record<string, any>;
3
+ export declare function writeAllMetadata(repoRoot: string, data: Record<string, any>): void;
@@ -0,0 +1,27 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ export function getSantreeDir(repoRoot) {
4
+ return path.join(repoRoot, ".santree");
5
+ }
6
+ function getMetadataFilePath(repoRoot) {
7
+ return path.join(getSantreeDir(repoRoot), "metadata.json");
8
+ }
9
+ export function readAllMetadata(repoRoot) {
10
+ const filePath = getMetadataFilePath(repoRoot);
11
+ if (!fs.existsSync(filePath))
12
+ return {};
13
+ try {
14
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ export function writeAllMetadata(repoRoot, data) {
21
+ const filePath = getMetadataFilePath(repoRoot);
22
+ const dir = path.dirname(filePath);
23
+ if (!fs.existsSync(dir)) {
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ }
26
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
27
+ }
@@ -77,7 +77,7 @@ export const cmuxMultiplexer = {
77
77
  // follow-up sends to an existing workspace, which doesn't.
78
78
  return {
79
79
  ok: false,
80
- reason: "unsupported",
80
+ reason: "failed",
81
81
  message: "blocked by manaflow-ai/cmux#1472",
82
82
  };
83
83
  },
@@ -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
- const explicit = process.env["SANTREE_MULTIPLEXER"]?.toLowerCase();
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 SessionResult = {
3
3
  ok: true;
4
4
  } | {
5
5
  ok: false;
6
- reason: "not-active" | "unsupported" | "failed";
6
+ reason: "not-active" | "failed";
7
7
  message?: string;
8
8
  };
9
9
  export interface CreateWindowOpts {
@@ -1,4 +1,4 @@
1
- import type { LinearIssue } from "./linear.js";
1
+ import type { Issue } from "./trackers/types.js";
2
2
  import type { PRCheck, PRReview, PRReviewComment, FailedCheckDetail, PRConversationComment } from "./github.js";
3
3
  /**
4
4
  * Render a nunjucks template from the prompts/ directory.
@@ -7,9 +7,10 @@ import type { PRCheck, PRReview, PRReviewComment, FailedCheckDetail, PRConversat
7
7
  */
8
8
  export declare function renderPrompt(template: string, context: Record<string, string | undefined>): string;
9
9
  /**
10
- * Render a LinearIssue into formatted markdown using the ticket template.
10
+ * Render an issue into formatted markdown using the ticket template.
11
+ * `trackerName` is injected for header/link text ("Linear" / "GitHub").
11
12
  */
12
- export declare function renderTicket(issue: LinearIssue): string;
13
+ export declare function renderTicket(issue: Issue, trackerName: string): string;
13
14
  export interface DiffData {
14
15
  base_branch: string;
15
16
  commit_log: string | null;
@@ -33,10 +33,11 @@ export function renderPrompt(template, context) {
33
33
  return promptsEnv.render(`${template}.njk`, context);
34
34
  }
35
35
  /**
36
- * Render a LinearIssue into formatted markdown using the ticket template.
36
+ * Render an issue into formatted markdown using the ticket template.
37
+ * `trackerName` is injected for header/link text ("Linear" / "GitHub").
37
38
  */
38
- export function renderTicket(issue) {
39
- return promptsEnv.render("ticket.njk", issue);
39
+ export function renderTicket(issue, trackerName) {
40
+ return promptsEnv.render("ticket.njk", { ...issue, trackerName });
40
41
  }
41
42
  /**
42
43
  * Render diff data into formatted markdown using the diff template.
@@ -4,11 +4,10 @@ export declare function extractRepoAndTicket(cwd: string): {
4
4
  repoRoot: string;
5
5
  ticketId: string;
6
6
  } | null;
7
- export declare function renameTmuxWindow(ticketId: string, state: SessionStateValue): void;
8
- export declare function runHookScript(repoRoot: string, state: SessionStateValue, env: Record<string, string>): void;
7
+ export declare function renameSessionWindow(ticketId: string, state: SessionStateValue): void;
9
8
  /**
10
9
  * Unified helper: reads stdin, extracts repo/ticket, writes state file,
11
- * renames tmux window, runs hook script, then exits.
10
+ * renames the multiplexer window, then exits.
12
11
  */
13
12
  export declare function signalState(state: SessionStateValue): void;
14
13
  export declare function getHooksJson(): Record<string, unknown>;
@@ -1,6 +1,5 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { spawn } from "child_process";
4
3
  import { getMultiplexer } from "./multiplexer/index.js";
5
4
  export function readStdin() {
6
5
  try {
@@ -22,7 +21,7 @@ export function extractRepoAndTicket(cwd) {
22
21
  return null;
23
22
  return { repoRoot, ticketId };
24
23
  }
25
- export function renameTmuxWindow(ticketId, state) {
24
+ export function renameSessionWindow(ticketId, state) {
26
25
  const mux = getMultiplexer();
27
26
  if (!mux.isActive())
28
27
  return;
@@ -40,25 +39,9 @@ export function renameTmuxWindow(ticketId, state) {
40
39
  }
41
40
  mux.renameWindow("", name);
42
41
  }
43
- export function runHookScript(repoRoot, state, env) {
44
- const script = path.join(repoRoot, ".santree", "hooks", `on-${state}.sh`);
45
- try {
46
- fs.accessSync(script, fs.constants.X_OK);
47
- }
48
- catch {
49
- return;
50
- }
51
- const child = spawn(script, [], {
52
- cwd: env.SANTREE_WORKTREE_PATH,
53
- env: { ...process.env, ...env },
54
- stdio: "ignore",
55
- detached: true,
56
- });
57
- child.unref();
58
- }
59
42
  /**
60
43
  * Unified helper: reads stdin, extracts repo/ticket, writes state file,
61
- * renames tmux window, runs hook script, then exits.
44
+ * renames the multiplexer window, then exits.
62
45
  */
63
46
  export function signalState(state) {
64
47
  const input = readStdin();
@@ -85,16 +68,7 @@ export function signalState(state) {
85
68
  at: new Date().toISOString(),
86
69
  };
87
70
  fs.writeFileSync(stateFile, JSON.stringify(payload, null, 2) + "\n");
88
- renameTmuxWindow(ticketId, state);
89
- const worktreePath = path.join(repoRoot, ".santree", "worktrees", ticketId);
90
- runHookScript(repoRoot, state, {
91
- SANTREE_TICKET_ID: ticketId,
92
- SANTREE_SESSION_STATE: state,
93
- SANTREE_SESSION_ID: data.session_id ?? "",
94
- SANTREE_WORKTREE_PATH: worktreePath,
95
- SANTREE_REPO_ROOT: repoRoot,
96
- SANTREE_MESSAGE: payload.message ?? "",
97
- });
71
+ renameSessionWindow(ticketId, state);
98
72
  process.exit(0);
99
73
  }
100
74
  export function getHooksJson() {
@@ -0,0 +1,16 @@
1
+ export interface LinearTokens {
2
+ access_token: string;
3
+ refresh_token: string;
4
+ expires_at: number;
5
+ org_name: string;
6
+ }
7
+ export interface AuthStoreV2 {
8
+ version: 2;
9
+ linear: Record<string, LinearTokens>;
10
+ github: Record<string, never>;
11
+ }
12
+ export declare function readAuthStore(): AuthStoreV2;
13
+ export declare function writeAuthStore(store: AuthStoreV2): void;
14
+ export declare function readLinearAuthStore(): Record<string, LinearTokens>;
15
+ export declare function writeLinearTokens(orgSlug: string, tokens: LinearTokens): void;
16
+ export declare function deleteLinearTokens(orgSlug: string): void;
@@ -0,0 +1,57 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ const CONFIG_DIR = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
5
+ const AUTH_FILE_PATH = path.join(CONFIG_DIR, "santree", "auth.json");
6
+ function emptyStore() {
7
+ return { version: 2, linear: {}, github: {} };
8
+ }
9
+ export function readAuthStore() {
10
+ if (!fs.existsSync(AUTH_FILE_PATH))
11
+ return emptyStore();
12
+ let raw;
13
+ try {
14
+ raw = JSON.parse(fs.readFileSync(AUTH_FILE_PATH, "utf-8"));
15
+ }
16
+ catch {
17
+ return emptyStore();
18
+ }
19
+ if (raw && typeof raw === "object" && raw.version === 2) {
20
+ const v2 = raw;
21
+ return {
22
+ version: 2,
23
+ linear: v2.linear ?? {},
24
+ github: (v2.github ?? {}),
25
+ };
26
+ }
27
+ const migrated = emptyStore();
28
+ if (raw && typeof raw === "object") {
29
+ for (const [k, v] of Object.entries(raw)) {
30
+ if (v && typeof v === "object" && "access_token" in v) {
31
+ migrated.linear[k] = v;
32
+ }
33
+ }
34
+ }
35
+ writeAuthStore(migrated);
36
+ return migrated;
37
+ }
38
+ export function writeAuthStore(store) {
39
+ const dir = path.dirname(AUTH_FILE_PATH);
40
+ if (!fs.existsSync(dir)) {
41
+ fs.mkdirSync(dir, { recursive: true });
42
+ }
43
+ fs.writeFileSync(AUTH_FILE_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 0o600 });
44
+ }
45
+ export function readLinearAuthStore() {
46
+ return readAuthStore().linear;
47
+ }
48
+ export function writeLinearTokens(orgSlug, tokens) {
49
+ const store = readAuthStore();
50
+ store.linear[orgSlug] = tokens;
51
+ writeAuthStore(store);
52
+ }
53
+ export function deleteLinearTokens(orgSlug) {
54
+ const store = readAuthStore();
55
+ delete store.linear[orgSlug];
56
+ writeAuthStore(store);
57
+ }
@@ -0,0 +1,8 @@
1
+ import type { IssueTrackerKind } from "./types.js";
2
+ export interface TrackerConfig {
3
+ kind: IssueTrackerKind | null;
4
+ legacyLinearOrg: string | null;
5
+ }
6
+ export declare function readTrackerConfig(repoRoot: string): TrackerConfig;
7
+ export declare function setRepoTracker(repoRoot: string, kind: IssueTrackerKind): void;
8
+ export declare function removeRepoTracker(repoRoot: string): void;
@@ -0,0 +1,21 @@
1
+ import { readAllMetadata, writeAllMetadata } from "../metadata.js";
2
+ export function readTrackerConfig(repoRoot) {
3
+ const all = readAllMetadata(repoRoot);
4
+ const tracker = all._tracker;
5
+ const linear = all._linear;
6
+ let kind = null;
7
+ if (tracker?.kind === "linear" || tracker?.kind === "github") {
8
+ kind = tracker.kind;
9
+ }
10
+ return { kind, legacyLinearOrg: linear?.org ?? null };
11
+ }
12
+ export function setRepoTracker(repoRoot, kind) {
13
+ const all = readAllMetadata(repoRoot);
14
+ all._tracker = { kind };
15
+ writeAllMetadata(repoRoot, all);
16
+ }
17
+ export function removeRepoTracker(repoRoot) {
18
+ const all = readAllMetadata(repoRoot);
19
+ delete all._tracker;
20
+ writeAllMetadata(repoRoot, all);
21
+ }
@@ -0,0 +1,3 @@
1
+ import type { AssignedIssue, Issue } from "../types.js";
2
+ export declare function fetchAssignedIssues(repoNwo: string): Promise<AssignedIssue[] | null>;
3
+ export declare function fetchIssue(repoNwo: string, identifier: string): Promise<Issue | null>;