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,44 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ function getTempImageDir(ticketId) {
5
+ return path.join(os.tmpdir(), `santree-images-${ticketId}`);
6
+ }
7
+ export async function rewriteLinearImages(markdown, ticketId, accessToken) {
8
+ const imageRegex = /!\[([^\]]*)\]\((https:\/\/uploads\.linear\.app[^)]+)\)/g;
9
+ const matches = [...markdown.matchAll(imageRegex)];
10
+ if (matches.length === 0)
11
+ return markdown;
12
+ const tempDir = getTempImageDir(ticketId);
13
+ if (!fs.existsSync(tempDir)) {
14
+ fs.mkdirSync(tempDir, { recursive: true });
15
+ }
16
+ let result = markdown;
17
+ for (let i = 0; i < matches.length; i++) {
18
+ const match = matches[i];
19
+ const [fullMatch, altText, url] = match;
20
+ try {
21
+ const res = await fetch(url, {
22
+ headers: { Authorization: `Bearer ${accessToken}` },
23
+ });
24
+ if (!res.ok)
25
+ continue;
26
+ const buffer = Buffer.from(await res.arrayBuffer());
27
+ const ext = path.extname(new URL(url).pathname) || ".png";
28
+ const filename = `image-${i}${ext}`;
29
+ const filePath = path.join(tempDir, filename);
30
+ fs.writeFileSync(filePath, buffer);
31
+ result = result.replace(fullMatch, `![${altText}](${filePath})`);
32
+ }
33
+ catch {
34
+ // keep original URL on failure
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ export function cleanupLinearImages(ticketId) {
40
+ const tempDir = getTempImageDir(ticketId);
41
+ if (fs.existsSync(tempDir)) {
42
+ fs.rmSync(tempDir, { recursive: true, force: true });
43
+ }
44
+ }
@@ -0,0 +1,3 @@
1
+ import type { IssueTracker } from "../types.js";
2
+ export { getRepoLinearOrg, setRepoLinearOrg, removeRepoLinearOrg, getValidTokens, revokeTokens, startOAuthFlow, } from "./auth.js";
3
+ export declare const linearTracker: IssueTracker;
@@ -0,0 +1,100 @@
1
+ import { readLinearAuthStore } from "../auth-store.js";
2
+ import { getRepoLinearOrg, getValidTokens, removeRepoLinearOrg, revokeTokens } from "./auth.js";
3
+ import { fetchAssignedIssues, fetchIssue } from "./api.js";
4
+ import { cleanupLinearImages, rewriteLinearImages } from "./images.js";
5
+ export { getRepoLinearOrg, setRepoLinearOrg, removeRepoLinearOrg, getValidTokens, revokeTokens, startOAuthFlow, } from "./auth.js";
6
+ async function getAuthStatus(repoRoot) {
7
+ const store = readLinearAuthStore();
8
+ const orgs = Object.keys(store);
9
+ if (orgs.length === 0) {
10
+ return { authenticated: false, hint: "Run: santree linear auth" };
11
+ }
12
+ if (repoRoot) {
13
+ const repoOrg = getRepoLinearOrg(repoRoot);
14
+ if (repoOrg && store[repoOrg]) {
15
+ const tokens = store[repoOrg];
16
+ return {
17
+ authenticated: true,
18
+ accountLabel: `${tokens.org_name} (${repoOrg})`,
19
+ expiresAt: tokens.expires_at,
20
+ repoLinked: true,
21
+ };
22
+ }
23
+ }
24
+ const orgSlug = orgs[0];
25
+ const tokens = store[orgSlug];
26
+ return {
27
+ authenticated: true,
28
+ accountLabel: `${tokens.org_name} (${orgSlug})`,
29
+ expiresAt: tokens.expires_at,
30
+ repoLinked: false,
31
+ hint: "Repo not linked. Run: santree linear auth",
32
+ };
33
+ }
34
+ async function signOut(repoRoot) {
35
+ const orgSlug = getRepoLinearOrg(repoRoot);
36
+ if (orgSlug) {
37
+ await revokeTokens(orgSlug);
38
+ removeRepoLinearOrg(repoRoot);
39
+ }
40
+ }
41
+ function extractIdFromBranch(branch) {
42
+ const match = branch.match(/([a-zA-Z]+)-(\d+)/);
43
+ if (!match)
44
+ return null;
45
+ return `${match[1].toUpperCase()}-${match[2]}`;
46
+ }
47
+ async function listAssigned(repoRoot) {
48
+ const orgSlug = getRepoLinearOrg(repoRoot);
49
+ if (!orgSlug) {
50
+ return { ok: false, reason: "unauthenticated", message: "Run: santree linear auth" };
51
+ }
52
+ const tokens = await getValidTokens(orgSlug);
53
+ if (!tokens) {
54
+ return { ok: false, reason: "unauthenticated", message: "Run: santree linear auth" };
55
+ }
56
+ const issues = await fetchAssignedIssues(tokens.access_token);
57
+ if (issues === null) {
58
+ return { ok: false, reason: "network", message: "Linear API request failed" };
59
+ }
60
+ return { ok: true, value: issues };
61
+ }
62
+ async function getIssue(identifier, repoRoot) {
63
+ const orgSlug = getRepoLinearOrg(repoRoot);
64
+ if (!orgSlug) {
65
+ return { ok: false, reason: "unauthenticated", message: "Run: santree linear auth" };
66
+ }
67
+ const tokens = await getValidTokens(orgSlug);
68
+ if (!tokens) {
69
+ return { ok: false, reason: "unauthenticated", message: "Run: santree linear auth" };
70
+ }
71
+ const issue = await fetchIssue(identifier, tokens.access_token);
72
+ if (!issue) {
73
+ return { ok: false, reason: "not-found", message: `Issue ${identifier} not found` };
74
+ }
75
+ if (issue.description) {
76
+ issue.description = await rewriteLinearImages(issue.description, identifier, tokens.access_token);
77
+ }
78
+ for (const comment of issue.comments) {
79
+ if (comment.body) {
80
+ comment.body = await rewriteLinearImages(comment.body, identifier, tokens.access_token);
81
+ }
82
+ for (const child of comment.children) {
83
+ if (child.body) {
84
+ child.body = await rewriteLinearImages(child.body, identifier, tokens.access_token);
85
+ }
86
+ }
87
+ }
88
+ return { ok: true, value: issue };
89
+ }
90
+ export const linearTracker = {
91
+ kind: "linear",
92
+ displayName: "Linear",
93
+ issueNoun: "ticket",
94
+ getAuthStatus,
95
+ signOut,
96
+ extractIdFromBranch,
97
+ cleanupCache: cleanupLinearImages,
98
+ listAssigned,
99
+ getIssue,
100
+ };
@@ -0,0 +1,52 @@
1
+ export type IssueTrackerKind = "linear" | "github";
2
+ export interface Comment {
3
+ author: string;
4
+ body: string;
5
+ createdAt: string;
6
+ children: Comment[];
7
+ }
8
+ export interface State {
9
+ name: string;
10
+ type: string;
11
+ }
12
+ export interface AssignedIssue {
13
+ identifier: string;
14
+ title: string;
15
+ description: string | null;
16
+ url: string;
17
+ priority: number;
18
+ priorityLabel: string;
19
+ state: State;
20
+ labels: string[];
21
+ projectId: string | null;
22
+ projectName: string | null;
23
+ }
24
+ export interface Issue extends AssignedIssue {
25
+ comments: Comment[];
26
+ }
27
+ export interface AuthStatus {
28
+ authenticated: boolean;
29
+ accountLabel?: string;
30
+ expiresAt?: number;
31
+ repoLinked?: boolean;
32
+ hint?: string;
33
+ }
34
+ export type IssueTrackerResult<T> = {
35
+ ok: true;
36
+ value: T;
37
+ } | {
38
+ ok: false;
39
+ reason: "unauthenticated" | "not-found" | "network";
40
+ message?: string;
41
+ };
42
+ export interface IssueTracker {
43
+ readonly kind: IssueTrackerKind;
44
+ readonly displayName: string;
45
+ readonly issueNoun: string;
46
+ getAuthStatus(repoRoot: string | null): Promise<AuthStatus>;
47
+ signOut(repoRoot: string): Promise<void>;
48
+ extractIdFromBranch(branch: string): string | null;
49
+ cleanupCache(identifier: string): void;
50
+ listAssigned(repoRoot: string): Promise<IssueTrackerResult<AssignedIssue[]>>;
51
+ getIssue(identifier: string, repoRoot: string): Promise<IssueTrackerResult<Issue>>;
52
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Git worktree manager",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",
@@ -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.
@@ -1,7 +1,7 @@
1
- ## Linear Ticket: {{ identifier }}
1
+ ## {{ trackerName }} Issue: {{ identifier }}
2
2
  **{{ title }}**
3
- {% if url %}[View in Linear]({{ url }}){% endif %}
4
- {% if status %}Status: {{ status }}{% endif %}{% if priority %} | Priority: {{ priority }}{% endif %}{% if labels | length %} | Labels: {{ labels | join(", ") }}{% endif %}
3
+ {% if url %}[View in {{ trackerName }}]({{ url }}){% endif %}
4
+ {% if state and state.name %}Status: {{ state.name }}{% endif %}{% if priorityLabel %} | Priority: {{ priorityLabel }}{% endif %}{% if labels | length %} | Labels: {{ labels | join(", ") }}{% endif %}
5
5
 
6
6
  {% if description %}
7
7
  ### Description
@@ -1,2 +0,0 @@
1
- export declare const description = "Open the current Linear ticket in the browser";
2
- export default function LinearOpen(): import("react/jsx-runtime").JSX.Element;
@@ -1,83 +0,0 @@
1
- export interface LinearTokens {
2
- access_token: string;
3
- refresh_token: string;
4
- expires_at: number;
5
- org_name: string;
6
- }
7
- export interface LinearComment {
8
- author: string;
9
- body: string;
10
- createdAt: string;
11
- children: LinearComment[];
12
- }
13
- export interface LinearIssue {
14
- identifier: string;
15
- title: string;
16
- description: string | null;
17
- status: string | null;
18
- priority: string | null;
19
- labels: string[];
20
- url: string;
21
- comments: LinearComment[];
22
- }
23
- type AuthStore = Record<string, LinearTokens>;
24
- export declare function readAuthStore(): AuthStore;
25
- /**
26
- * Run the full OAuth PKCE flow:
27
- * 1. Start a temp HTTP server on an ephemeral port
28
- * 2. Open browser to Linear authorize URL
29
- * 3. Wait for callback with auth code
30
- * 4. Exchange code for tokens
31
- * 5. Fetch org info
32
- * 6. Store tokens
33
- * Returns the org slug on success, null on failure.
34
- */
35
- export declare function startOAuthFlow(): Promise<{
36
- orgSlug: string;
37
- orgName: string;
38
- } | null>;
39
- export declare function revokeTokens(orgSlug: string): Promise<boolean>;
40
- /**
41
- * Get valid tokens for an org, auto-refreshing if expired.
42
- * Returns null if no tokens found or refresh fails.
43
- */
44
- export declare function getValidTokens(orgSlug: string): Promise<LinearTokens | null>;
45
- export declare function cleanupImages(ticketId: string): void;
46
- export interface AuthStatus {
47
- authenticated: boolean;
48
- orgSlug?: string;
49
- orgName?: string;
50
- expiresAt?: number;
51
- repoLinked?: boolean;
52
- }
53
- /**
54
- * Get auth status for the current repo's Linear org (or any stored org).
55
- */
56
- export declare function getAuthStatus(repoRoot: string | null): AuthStatus;
57
- export interface LinearAssignedIssue {
58
- identifier: string;
59
- title: string;
60
- description: string | null;
61
- url: string;
62
- priority: number;
63
- priorityLabel: string;
64
- state: {
65
- name: string;
66
- type: string;
67
- };
68
- labels: string[];
69
- projectId: string | null;
70
- projectName: string | null;
71
- }
72
- /**
73
- * Fetch all active issues assigned to the current user.
74
- * Returns null if not authenticated or fetch fails.
75
- */
76
- export declare function fetchAssignedIssues(repoRoot: string): Promise<LinearAssignedIssue[] | null>;
77
- /**
78
- * Fetch full ticket content for a given ticket ID.
79
- * Looks up the repo's Linear org, gets valid tokens, fetches issue, downloads images.
80
- * Returns null if not authenticated or fetch fails.
81
- */
82
- export declare function getTicketContent(ticketId: string, repoRoot: string): Promise<LinearIssue | null>;
83
- export {};