santree 0.5.2 → 0.5.4

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 (67) hide show
  1. package/README.md +145 -52
  2. package/dist/commands/dashboard.d.ts +1 -1
  3. package/dist/commands/dashboard.js +95 -27
  4. package/dist/commands/doctor.js +33 -71
  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/template.d.ts +1 -0
  10. package/dist/commands/helpers/template.js +13 -10
  11. package/dist/commands/issue/index.d.ts +1 -0
  12. package/dist/commands/issue/index.js +1 -0
  13. package/dist/commands/issue/open.d.ts +2 -0
  14. package/dist/commands/{linear → issue}/open.js +13 -11
  15. package/dist/commands/issue/switch.d.ts +11 -0
  16. package/dist/commands/issue/switch.js +38 -0
  17. package/dist/commands/linear/auth.js +23 -10
  18. package/dist/commands/linear/switch.js +7 -3
  19. package/dist/commands/pr/create.js +7 -5
  20. package/dist/commands/worktree/create.js +4 -6
  21. package/dist/commands/worktree/work.js +1 -1
  22. package/dist/lib/ai.d.ts +8 -6
  23. package/dist/lib/ai.js +29 -15
  24. package/dist/lib/dashboard/DetailPanel.d.ts +5 -2
  25. package/dist/lib/dashboard/DetailPanel.js +6 -3
  26. package/dist/lib/dashboard/DiffOverlay.js +8 -1
  27. package/dist/lib/dashboard/data.js +17 -9
  28. package/dist/lib/dashboard/types.d.ts +3 -16
  29. package/dist/lib/git.d.ts +16 -33
  30. package/dist/lib/git.js +20 -74
  31. package/dist/lib/metadata.d.ts +3 -0
  32. package/dist/lib/metadata.js +27 -0
  33. package/dist/lib/multiplexer/cmux.js +1 -1
  34. package/dist/lib/multiplexer/types.d.ts +1 -1
  35. package/dist/lib/prompts.d.ts +4 -3
  36. package/dist/lib/prompts.js +4 -3
  37. package/dist/lib/session-signal.d.ts +2 -3
  38. package/dist/lib/session-signal.js +3 -29
  39. package/dist/lib/trackers/auth-store.d.ts +16 -0
  40. package/dist/lib/trackers/auth-store.js +57 -0
  41. package/dist/lib/trackers/config.d.ts +8 -0
  42. package/dist/lib/trackers/config.js +21 -0
  43. package/dist/lib/trackers/github/api.d.ts +3 -0
  44. package/dist/lib/trackers/github/api.js +90 -0
  45. package/dist/lib/trackers/github/auth.d.ts +5 -0
  46. package/dist/lib/trackers/github/auth.js +27 -0
  47. package/dist/lib/trackers/github/images.d.ts +2 -0
  48. package/dist/lib/trackers/github/images.js +42 -0
  49. package/dist/lib/trackers/github/index.d.ts +2 -0
  50. package/dist/lib/trackers/github/index.js +78 -0
  51. package/dist/lib/trackers/index.d.ts +12 -0
  52. package/dist/lib/trackers/index.js +34 -0
  53. package/dist/lib/trackers/linear/api.d.ts +4 -0
  54. package/dist/lib/trackers/linear/api.js +128 -0
  55. package/dist/lib/trackers/linear/auth.d.ts +11 -0
  56. package/dist/lib/trackers/linear/auth.js +206 -0
  57. package/dist/lib/trackers/linear/images.d.ts +2 -0
  58. package/dist/lib/trackers/linear/images.js +44 -0
  59. package/dist/lib/trackers/linear/index.d.ts +3 -0
  60. package/dist/lib/trackers/linear/index.js +100 -0
  61. package/dist/lib/trackers/types.d.ts +52 -0
  62. package/dist/lib/trackers/types.js +1 -0
  63. package/package.json +1 -1
  64. package/prompts/ticket.njk +3 -3
  65. package/dist/commands/linear/open.d.ts +0 -2
  66. package/dist/lib/linear.d.ts +0 -83
  67. package/dist/lib/linear.js +0 -482
@@ -0,0 +1,206 @@
1
+ import * as http from "http";
2
+ import * as crypto from "crypto";
3
+ import { exec } from "child_process";
4
+ import { readAllMetadata, writeAllMetadata } from "../../metadata.js";
5
+ import { readLinearAuthStore, writeLinearTokens, deleteLinearTokens, } from "../auth-store.js";
6
+ const CLIENT_ID = "4be2738749371d7d3401061aabe2d11b";
7
+ const LINEAR_AUTHORIZE_URL = "https://linear.app/oauth/authorize";
8
+ const LINEAR_TOKEN_URL = "https://api.linear.app/oauth/token";
9
+ const LINEAR_REVOKE_URL = "https://api.linear.app/oauth/revoke";
10
+ const LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
11
+ const OAUTH_PORT = 8420;
12
+ const REDIRECT_URI = `http://localhost:${OAUTH_PORT}`;
13
+ function generateCodeVerifier() {
14
+ return crypto.randomBytes(32).toString("base64url");
15
+ }
16
+ function generateCodeChallenge(verifier) {
17
+ return crypto.createHash("sha256").update(verifier).digest("base64url");
18
+ }
19
+ export async function startOAuthFlow() {
20
+ const codeVerifier = generateCodeVerifier();
21
+ const codeChallenge = generateCodeChallenge(codeVerifier);
22
+ const state = crypto.randomBytes(16).toString("hex");
23
+ return new Promise((resolve) => {
24
+ let handled = false;
25
+ const server = http.createServer(async (req, res) => {
26
+ const url = new URL(req.url, `http://localhost`);
27
+ const code = url.searchParams.get("code");
28
+ const returnedState = url.searchParams.get("state");
29
+ if (!code || returnedState !== state) {
30
+ res.writeHead(404);
31
+ res.end();
32
+ return;
33
+ }
34
+ if (handled) {
35
+ res.writeHead(200);
36
+ res.end();
37
+ return;
38
+ }
39
+ handled = true;
40
+ res.writeHead(200, { "Content-Type": "text/html" });
41
+ res.end("<html><body><h2>Authentication successful!</h2><p>You can close this tab.</p></body></html>");
42
+ try {
43
+ const tokens = await exchangeCode(code, REDIRECT_URI, codeVerifier);
44
+ const orgInfo = await fetchViewerOrg(tokens.access_token);
45
+ if (!orgInfo) {
46
+ server.close();
47
+ resolve(null);
48
+ return;
49
+ }
50
+ writeLinearTokens(orgInfo.urlKey, {
51
+ access_token: tokens.access_token,
52
+ refresh_token: tokens.refresh_token,
53
+ expires_at: tokens.expires_at,
54
+ org_name: orgInfo.name,
55
+ });
56
+ server.close();
57
+ resolve({ orgSlug: orgInfo.urlKey, orgName: orgInfo.name });
58
+ }
59
+ catch {
60
+ server.close();
61
+ resolve(null);
62
+ }
63
+ });
64
+ server.listen(OAUTH_PORT, () => {
65
+ const params = new URLSearchParams({
66
+ client_id: CLIENT_ID,
67
+ redirect_uri: REDIRECT_URI,
68
+ response_type: "code",
69
+ scope: "read",
70
+ state,
71
+ code_challenge: codeChallenge,
72
+ code_challenge_method: "S256",
73
+ });
74
+ const authUrl = `${LINEAR_AUTHORIZE_URL}?${params.toString()}`;
75
+ const openCmd = process.platform === "darwin"
76
+ ? "open"
77
+ : process.platform === "win32"
78
+ ? "start"
79
+ : "xdg-open";
80
+ exec(`${openCmd} "${authUrl}"`, (err) => {
81
+ if (err) {
82
+ console.error(`\nCouldn't open browser automatically. Open this URL manually:\n${authUrl}\n`);
83
+ }
84
+ });
85
+ });
86
+ setTimeout(() => {
87
+ server.close();
88
+ resolve(null);
89
+ }, 120_000);
90
+ });
91
+ }
92
+ async function exchangeCode(code, redirectUri, codeVerifier) {
93
+ const res = await fetch(LINEAR_TOKEN_URL, {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
96
+ body: new URLSearchParams({
97
+ grant_type: "authorization_code",
98
+ client_id: CLIENT_ID,
99
+ code,
100
+ redirect_uri: redirectUri,
101
+ code_verifier: codeVerifier,
102
+ }),
103
+ });
104
+ if (!res.ok) {
105
+ throw new Error(`Token exchange failed: ${res.status}`);
106
+ }
107
+ const data = (await res.json());
108
+ return {
109
+ access_token: data.access_token,
110
+ refresh_token: data.refresh_token,
111
+ expires_at: Date.now() + data.expires_in * 1000,
112
+ };
113
+ }
114
+ async function fetchViewerOrg(accessToken) {
115
+ const res = await fetch(LINEAR_GRAPHQL_URL, {
116
+ method: "POST",
117
+ headers: {
118
+ "Content-Type": "application/json",
119
+ Authorization: `Bearer ${accessToken}`,
120
+ },
121
+ body: JSON.stringify({
122
+ query: `query { viewer { organization { urlKey name } } }`,
123
+ }),
124
+ });
125
+ if (!res.ok)
126
+ return null;
127
+ const json = (await res.json());
128
+ const org = json.data?.viewer?.organization;
129
+ return org ?? null;
130
+ }
131
+ function isTokenExpired(tokens) {
132
+ return Date.now() >= tokens.expires_at - 5 * 60 * 1000;
133
+ }
134
+ async function refreshTokens(orgSlug, tokens) {
135
+ try {
136
+ const res = await fetch(LINEAR_TOKEN_URL, {
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
139
+ body: new URLSearchParams({
140
+ grant_type: "refresh_token",
141
+ client_id: CLIENT_ID,
142
+ refresh_token: tokens.refresh_token,
143
+ }),
144
+ });
145
+ if (!res.ok)
146
+ return null;
147
+ const data = (await res.json());
148
+ const updated = {
149
+ access_token: data.access_token,
150
+ refresh_token: data.refresh_token,
151
+ expires_at: Date.now() + data.expires_in * 1000,
152
+ org_name: tokens.org_name,
153
+ };
154
+ writeLinearTokens(orgSlug, updated);
155
+ return updated;
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ }
161
+ export async function revokeTokens(orgSlug) {
162
+ const store = readLinearAuthStore();
163
+ const tokens = store[orgSlug];
164
+ if (!tokens)
165
+ return false;
166
+ try {
167
+ await fetch(LINEAR_REVOKE_URL, {
168
+ method: "POST",
169
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
170
+ body: new URLSearchParams({
171
+ client_id: CLIENT_ID,
172
+ token: tokens.access_token,
173
+ }),
174
+ });
175
+ }
176
+ catch {
177
+ // best effort
178
+ }
179
+ deleteLinearTokens(orgSlug);
180
+ return true;
181
+ }
182
+ export async function getValidTokens(orgSlug) {
183
+ const store = readLinearAuthStore();
184
+ const tokens = store[orgSlug];
185
+ if (!tokens)
186
+ return null;
187
+ if (isTokenExpired(tokens)) {
188
+ return refreshTokens(orgSlug, tokens);
189
+ }
190
+ return tokens;
191
+ }
192
+ export function getRepoLinearOrg(repoRoot) {
193
+ const all = readAllMetadata(repoRoot);
194
+ const linear = all._linear;
195
+ return linear?.org ?? null;
196
+ }
197
+ export function setRepoLinearOrg(repoRoot, orgSlug) {
198
+ const all = readAllMetadata(repoRoot);
199
+ all._linear = { org: orgSlug };
200
+ writeAllMetadata(repoRoot, all);
201
+ }
202
+ export function removeRepoLinearOrg(repoRoot) {
203
+ const all = readAllMetadata(repoRoot);
204
+ delete all._linear;
205
+ writeAllMetadata(repoRoot, all);
206
+ }
@@ -0,0 +1,2 @@
1
+ export declare function rewriteLinearImages(markdown: string, ticketId: string, accessToken: string): Promise<string>;
2
+ export declare function cleanupLinearImages(ticketId: string): void;
@@ -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.2",
3
+ "version": "0.5.4",
4
4
  "description": "Git worktree manager",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",
@@ -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 {};