clairo 1.1.0 → 1.2.0

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 (3) hide show
  1. package/README.md +13 -13
  2. package/dist/cli.js +282 -105
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,19 +1,27 @@
1
1
  # clairo
2
2
 
3
- Terminal dashboard for GitHub PRs and Jira tickets.
3
+ dashboard tui for github PRs, jira tickets, and daily logs.
4
4
 
5
- ## Requirements
5
+ ## features
6
+
7
+ - branch aware github dashboard: see open PR details, create new PRs
8
+ - claude code integration (requires claude code to be set up) for generating standup notes
9
+ - link jira tickets and change ticket status from the terminal
10
+ - auto jira ticket detection based on branch name
11
+ - daily logs that update automatically with tui actions that can be used for generateStandupNotes
12
+
13
+ ## requirements
6
14
 
7
15
  - Node.js 18+
8
16
  - [GitHub CLI](https://cli.github.com/) (`gh`) installed and authenticated
9
17
 
10
- ## Usage
18
+ ## usage
11
19
 
12
20
  ```bash
13
21
  npx clairo
14
22
  ```
15
23
 
16
- ### Options
24
+ ### options
17
25
 
18
26
  ```
19
27
  --cwd <path>, -C Run in a different directory
@@ -21,7 +29,7 @@ npx clairo
21
29
  --help Show help
22
30
  ```
23
31
 
24
- ### Examples
32
+ ### examples
25
33
 
26
34
  ```bash
27
35
  # Run in current directory
@@ -30,11 +38,3 @@ npx clairo
30
38
  # Run in a different repo
31
39
  npx clairo --cwd ~/projects/other-repo
32
40
  ```
33
-
34
- ## Keyboard
35
-
36
- - `1-6` - Switch between boxes
37
- - `j/k` - Navigate lists
38
- - `Enter` - Select
39
- - `o` - Open in browser
40
- - `Ctrl+C` - Quit
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { Box as Box16, useApp, useInput as useInput13 } from "ink";
11
11
  import { useCallback as useCallback8, useEffect as useEffect8, useRef as useRef5, useState as useState11 } from "react";
12
12
  import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
13
13
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
14
+ import { ScrollView as ScrollView4 } from "ink-scroll-view";
14
15
 
15
16
  // src/hooks/github/useGitRepo.ts
16
17
  import { useCallback, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
@@ -170,7 +171,7 @@ function findRemoteWithBranch(branch) {
170
171
  }
171
172
 
172
173
  // src/lib/github/index.ts
173
- import { exec } from "child_process";
174
+ import { exec, execFile } from "child_process";
174
175
  import { promisify } from "util";
175
176
  var execAsync = promisify(exec);
176
177
  function resolveCheckStatus(check) {
@@ -251,12 +252,10 @@ async function listPRsForBranch(branch, repo) {
251
252
  }
252
253
  try {
253
254
  const { stdout } = await execAsync(
254
- `gh pr list --state open --json ${fields},headRefName --repo "${repo}" 2>/dev/null`
255
+ `gh pr list --state open --head "${branch}" --json ${fields} --repo "${repo}" 2>/dev/null`
255
256
  );
256
- const allPrs = JSON.parse(stdout);
257
- const prs = allPrs.filter((pr) => pr.headRefName === branch || pr.headRefName.endsWith(`:${branch}`));
258
- const result = prs.map(({ headRefName: _headRefName, ...rest }) => rest);
259
- return { success: true, data: result };
257
+ const prs = JSON.parse(stdout);
258
+ return { success: true, data: prs };
260
259
  } catch {
261
260
  return { success: false, error: "Failed to fetch PRs", errorType: "api_error" };
262
261
  }
@@ -313,6 +312,34 @@ function openPRCreationPage(owner, branch, onComplete) {
313
312
  onComplete == null ? void 0 : onComplete(error);
314
313
  });
315
314
  }
315
+ function openPRCreationPageWithContent(owner, branch, title, body, onComplete) {
316
+ const headFlag = `${owner}:${branch}`;
317
+ const args = ["pr", "create", "--web", "--head", headFlag, "--title", title, "--body", body];
318
+ execFile("gh", args, (error) => {
319
+ process.stdout.emit("resize");
320
+ onComplete == null ? void 0 : onComplete(error);
321
+ });
322
+ }
323
+ function editPRDescription(prNumber, repo, title, body, onComplete) {
324
+ const args = [
325
+ "api",
326
+ `repos/${repo}/pulls/${prNumber}`,
327
+ "--method",
328
+ "PATCH",
329
+ "--field",
330
+ `title=${title}`,
331
+ "--field",
332
+ `body=${body}`
333
+ ];
334
+ execFile("gh", args, (error, _stdout, stderr) => {
335
+ if (error) {
336
+ const message = (stderr == null ? void 0 : stderr.trim()) || error.message;
337
+ onComplete == null ? void 0 : onComplete(new Error(message));
338
+ } else {
339
+ onComplete == null ? void 0 : onComplete(null);
340
+ }
341
+ });
342
+ }
316
343
 
317
344
  // src/hooks/useTerminalFocus.ts
318
345
  import { useEffect, useState } from "react";
@@ -598,6 +625,99 @@ function usePullRequests() {
598
625
  };
599
626
  }
600
627
 
628
+ // src/lib/claude/api.ts
629
+ import { exec as exec2 } from "child_process";
630
+ function runClaudePrompt(prompt) {
631
+ let childProcess = null;
632
+ let cancelled = false;
633
+ const promise = new Promise((resolve) => {
634
+ const escapedPrompt = prompt.replace(/'/g, "'\\''").replace(/\n/g, "\\n");
635
+ const command = `claude -p $'${escapedPrompt}' --output-format json < /dev/null`;
636
+ childProcess = exec2(command, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
637
+ if (cancelled) {
638
+ resolve({
639
+ success: false,
640
+ error: "Cancelled",
641
+ errorType: "execution_error"
642
+ });
643
+ return;
644
+ }
645
+ if (error) {
646
+ if (error.message.includes("command not found") || error.message.includes("ENOENT") || error.code === "ENOENT") {
647
+ resolve({
648
+ success: false,
649
+ error: "Claude CLI not installed. Run: npm install -g @anthropic-ai/claude-code",
650
+ errorType: "not_installed"
651
+ });
652
+ return;
653
+ }
654
+ resolve({
655
+ success: false,
656
+ error: (stderr == null ? void 0 : stderr.trim()) || error.message,
657
+ errorType: "execution_error"
658
+ });
659
+ return;
660
+ }
661
+ if (!(stdout == null ? void 0 : stdout.trim())) {
662
+ resolve({
663
+ success: false,
664
+ error: (stderr == null ? void 0 : stderr.trim()) || "Claude returned empty response",
665
+ errorType: "execution_error"
666
+ });
667
+ return;
668
+ }
669
+ try {
670
+ const json = JSON.parse(stdout.trim());
671
+ if (json.is_error) {
672
+ resolve({
673
+ success: false,
674
+ error: json.result || "Claude returned an error",
675
+ errorType: "execution_error"
676
+ });
677
+ return;
678
+ }
679
+ resolve({
680
+ success: true,
681
+ data: json.result || stdout.trim()
682
+ });
683
+ } catch {
684
+ resolve({
685
+ success: true,
686
+ data: stdout.trim()
687
+ });
688
+ }
689
+ });
690
+ });
691
+ const cancel = () => {
692
+ cancelled = true;
693
+ if (childProcess) {
694
+ childProcess.kill("SIGTERM");
695
+ }
696
+ };
697
+ return { promise, cancel };
698
+ }
699
+ function generatePRContent() {
700
+ const prompt = `Your output will be used directly to pre-fill a GitHub pull request creation form. Analyze the current branch's changes and return ONLY a raw JSON object (no markdown fences, no explanation) with "title" and "body" fields.
701
+
702
+ Rules:
703
+ - "title": a concise PR title in imperative mood, under 72 characters
704
+ - "body": a markdown PR description summarizing what changed and why. Use bullet points for multiple changes.
705
+ - Do not include any text outside the JSON object
706
+ - Do not add any "Co-Authored-By" or attribution lines`;
707
+ return runClaudePrompt(prompt);
708
+ }
709
+ function generateStandupNotes(logContent) {
710
+ const prompt = `You are helping a developer prepare standup notes. Based on the following log entries, generate concise standup notes that summarize what was accomplished.
711
+
712
+ Format the output as bullet points grouped by category (e.g., Features, Bug Fixes, Refactoring, etc.). Keep it brief and professional.
713
+
714
+ Log entries:
715
+ ${logContent}
716
+
717
+ Generate the standup notes:`;
718
+ return runClaudePrompt(prompt);
719
+ }
720
+
601
721
  // src/lib/jira/parser.ts
602
722
  var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
603
723
  function isValidTicketKeyFormat(key) {
@@ -1624,12 +1744,12 @@ function useRubberDuck() {
1624
1744
  }
1625
1745
 
1626
1746
  // src/lib/clipboard.ts
1627
- import { exec as exec2 } from "child_process";
1747
+ import { exec as exec3 } from "child_process";
1628
1748
  async function copyToClipboard(text) {
1629
1749
  var _a, _b;
1630
1750
  const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
1631
1751
  try {
1632
- const child = exec2(command);
1752
+ const child = exec3(command);
1633
1753
  (_a = child.stdin) == null ? void 0 : _a.write(text);
1634
1754
  (_b = child.stdin) == null ? void 0 : _b.end();
1635
1755
  await new Promise((resolve, reject) => {
@@ -1655,7 +1775,8 @@ function PullRequestsBox({
1655
1775
  error,
1656
1776
  branch,
1657
1777
  repoSlug,
1658
- isActive
1778
+ isActive,
1779
+ isGeneratingPR
1659
1780
  }) {
1660
1781
  const [copied, setCopied] = useState10(false);
1661
1782
  const selectedIndex = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
@@ -1704,6 +1825,7 @@ function PullRequestsBox({
1704
1825
  children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
1705
1826
  loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
1706
1827
  error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
1828
+ isGeneratingPR && /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "Generating PR with Claude... (Esc to cancel)" }),
1707
1829
  !loading && !error && /* @__PURE__ */ jsxs3(ScrollView2, { ref: scrollRef, children: [
1708
1830
  prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }, "empty"),
1709
1831
  prs.map((pr, idx) => {
@@ -1783,6 +1905,10 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
1783
1905
  const repo = useGitRepo();
1784
1906
  const pullRequests = usePullRequests();
1785
1907
  const [focusedBox, setFocusedBox] = useState11("remotes");
1908
+ const [isGeneratingPR, setIsGeneratingPR] = useState11(false);
1909
+ const claudeProcessRef = useRef5(null);
1910
+ const previewScrollRef = useRef5(null);
1911
+ const [prPreview, setPrPreview] = useState11(null);
1786
1912
  useEffect8(() => {
1787
1913
  if (repo.loading || !repo.currentBranch || !repo.currentRepoSlug) return;
1788
1914
  pullRequests.fetchPRsAndDetails(repo.currentBranch, repo.currentRepoSlug);
@@ -1809,6 +1935,94 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
1809
1935
  );
1810
1936
  const onLogUpdatedRef = useRef5(onLogUpdated);
1811
1937
  onLogUpdatedRef.current = onLogUpdated;
1938
+ const prsRef = useRef5(pullRequests.prs);
1939
+ prsRef.current = pullRequests.prs;
1940
+ useEffect8(() => {
1941
+ return () => {
1942
+ var _a;
1943
+ (_a = claudeProcessRef.current) == null ? void 0 : _a.cancel();
1944
+ };
1945
+ }, []);
1946
+ const handleAICreatePR = useCallback8(() => {
1947
+ if (!repo.currentBranch || !repo.currentRepoSlug) {
1948
+ pullRequests.setError("prs", "No branch or repo detected");
1949
+ duckEvents.emit("error");
1950
+ return;
1951
+ }
1952
+ const remoteResult = findRemoteWithBranch(repo.currentBranch);
1953
+ if (!remoteResult.success) {
1954
+ pullRequests.setError("prs", "Push your branch to a remote first");
1955
+ duckEvents.emit("error");
1956
+ return;
1957
+ }
1958
+ setIsGeneratingPR(true);
1959
+ const repoSlug = repo.currentRepoSlug;
1960
+ const branch = repo.currentBranch;
1961
+ const repoPath = repo.repoPath;
1962
+ const owner = remoteResult.data.owner;
1963
+ const process2 = generatePRContent();
1964
+ claudeProcessRef.current = process2;
1965
+ process2.promise.then((result) => {
1966
+ claudeProcessRef.current = null;
1967
+ setIsGeneratingPR(false);
1968
+ if (!result.success) {
1969
+ if (result.error !== "Cancelled") {
1970
+ pullRequests.setError("prs", result.error);
1971
+ duckEvents.emit("error");
1972
+ }
1973
+ return;
1974
+ }
1975
+ let title;
1976
+ let body;
1977
+ try {
1978
+ const cleaned = result.data.replace(/^```(?:json)?\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
1979
+ const parsed = JSON.parse(cleaned);
1980
+ title = parsed.title;
1981
+ body = parsed.body;
1982
+ } catch {
1983
+ pullRequests.setError("prs", "Failed to parse AI response");
1984
+ duckEvents.emit("error");
1985
+ return;
1986
+ }
1987
+ const tickets = repoPath && branch ? getLinkedTickets(repoPath, branch) : [];
1988
+ if (tickets.length > 0) {
1989
+ const siteUrl = repoPath ? getJiraSiteUrl(repoPath) : null;
1990
+ const prefix = tickets.map((t) => t.key).join(" ");
1991
+ title = `${prefix} ${title}`;
1992
+ if (siteUrl) {
1993
+ const baseUrl = siteUrl.replace(/\/$/, "");
1994
+ const links = tickets.map((t) => `[${t.key}](${baseUrl}/browse/${t.key})`).join(", ");
1995
+ const label = tickets.length === 1 ? "Ticket" : "Tickets";
1996
+ body = `${label}: ${links}
1997
+
1998
+ ${body}`;
1999
+ }
2000
+ }
2001
+ const currentPrs = prsRef.current;
2002
+ const existingPR = currentPrs.length > 0 ? currentPrs[0] : null;
2003
+ if (existingPR) {
2004
+ setPrPreview({ title, body, prNumber: existingPR.number, repoSlug, branch });
2005
+ } else {
2006
+ openPRCreationPageWithContent(owner, branch, title, body, (error) => {
2007
+ if (error) {
2008
+ pullRequests.setError("prs", `Failed to create PR: ${error.message}`);
2009
+ duckEvents.emit("error");
2010
+ }
2011
+ });
2012
+ pullRequests.pollForNewPR({
2013
+ branch,
2014
+ repoSlug,
2015
+ onNewPR: (newPR) => {
2016
+ var _a;
2017
+ const ticketKeys = repoPath && branch ? getLinkedTickets(repoPath, branch).map((t) => t.key) : [];
2018
+ logPRCreated(newPR.number, newPR.title, ticketKeys);
2019
+ duckEvents.emit("pr:opened");
2020
+ (_a = onLogUpdatedRef.current) == null ? void 0 : _a.call(onLogUpdatedRef);
2021
+ }
2022
+ });
2023
+ }
2024
+ });
2025
+ }, [repo.currentBranch, repo.currentRepoSlug, repo.repoPath, pullRequests.setError, pullRequests.pollForNewPR]);
1812
2026
  const handleCreatePR = useCallback8(() => {
1813
2027
  if (!repo.currentBranch) {
1814
2028
  pullRequests.setError("prs", "No branch detected");
@@ -1844,7 +2058,42 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
1844
2058
  });
1845
2059
  }, [repo.currentBranch, repo.currentRepoSlug, repo.repoPath, pullRequests.setError, pullRequests.pollForNewPR]);
1846
2060
  useInput4(
1847
- (input) => {
2061
+ (input, key) => {
2062
+ var _a, _b, _c;
2063
+ if (key.escape && isGeneratingPR) {
2064
+ (_a = claudeProcessRef.current) == null ? void 0 : _a.cancel();
2065
+ claudeProcessRef.current = null;
2066
+ setIsGeneratingPR(false);
2067
+ return;
2068
+ }
2069
+ if (prPreview) {
2070
+ if (key.escape) {
2071
+ setPrPreview(null);
2072
+ return;
2073
+ }
2074
+ if (key.upArrow || input === "k") {
2075
+ (_b = previewScrollRef.current) == null ? void 0 : _b.scrollBy(-1);
2076
+ return;
2077
+ }
2078
+ if (key.downArrow || input === "j") {
2079
+ (_c = previewScrollRef.current) == null ? void 0 : _c.scrollBy(1);
2080
+ return;
2081
+ }
2082
+ if (key.return) {
2083
+ const { prNumber, repoSlug, branch, title, body } = prPreview;
2084
+ setPrPreview(null);
2085
+ editPRDescription(prNumber, repoSlug, title, body, (error) => {
2086
+ if (error) {
2087
+ pullRequests.setError("prs", `Failed to update PR: ${error.message}`);
2088
+ duckEvents.emit("error");
2089
+ } else {
2090
+ pullRequests.fetchPRsAndDetails(branch, repoSlug);
2091
+ }
2092
+ });
2093
+ return;
2094
+ }
2095
+ return;
2096
+ }
1848
2097
  if (input === "1") setFocusedBox("remotes");
1849
2098
  if (input === "2") setFocusedBox("prs");
1850
2099
  if (input === "3") setFocusedBox("details");
@@ -1857,6 +2106,9 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
1857
2106
  pullRequests.refreshDetails(pullRequests.selectedPR, repo.currentRepoSlug);
1858
2107
  }
1859
2108
  }
2109
+ if (input === "c" && focusedBox === "prs" && !isGeneratingPR) {
2110
+ handleAICreatePR();
2111
+ }
1860
2112
  if (input === "n" && focusedBox === "prs") {
1861
2113
  handleCreatePR();
1862
2114
  }
@@ -1875,7 +2127,7 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
1875
2127
  onSelect: handleRemoteSelect,
1876
2128
  loading: repo.loading,
1877
2129
  error: repo.error,
1878
- isActive: isActive && focusedBox === "remotes"
2130
+ isActive: isActive && !prPreview && focusedBox === "remotes"
1879
2131
  }
1880
2132
  ),
1881
2133
  /* @__PURE__ */ jsx5(
@@ -1889,7 +2141,8 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
1889
2141
  error: pullRequests.errors.prs,
1890
2142
  branch: repo.currentBranch,
1891
2143
  repoSlug: repo.currentRepoSlug,
1892
- isActive: isActive && focusedBox === "prs"
2144
+ isActive: isActive && !prPreview && focusedBox === "prs",
2145
+ isGeneratingPR
1893
2146
  }
1894
2147
  ),
1895
2148
  /* @__PURE__ */ jsx5(
@@ -1898,9 +2151,17 @@ function GitHubView({ isActive, onFocusedBoxChange, onLogUpdated }) {
1898
2151
  pr: pullRequests.prDetails,
1899
2152
  loading: pullRequests.loading.details,
1900
2153
  error: pullRequests.errors.details,
1901
- isActive: isActive && focusedBox === "details"
2154
+ isActive: isActive && !prPreview && focusedBox === "details"
1902
2155
  }
1903
- )
2156
+ ),
2157
+ prPreview && /* @__PURE__ */ jsx5(TitledBox3, { borderStyle: "round", titles: ["PR Preview"], borderColor: "yellow", flexGrow: 1, flexBasis: 0, children: /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, flexGrow: 1, flexBasis: 0, overflow: "hidden", children: [
2158
+ /* @__PURE__ */ jsx5(Box5, { flexGrow: 1, flexBasis: 0, overflow: "hidden", children: /* @__PURE__ */ jsx5(ScrollView4, { ref: previewScrollRef, children: /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2159
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: prPreview.title }),
2160
+ /* @__PURE__ */ jsx5(Text5, { children: "" }),
2161
+ /* @__PURE__ */ jsx5(Text5, { children: prPreview.body })
2162
+ ] }) }) }),
2163
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Enter to apply, Esc to dismiss, j/k to scroll" })
2164
+ ] }) })
1904
2165
  ] });
1905
2166
  }
1906
2167
 
@@ -2066,7 +2327,7 @@ function ChangeStatusModal({ repoPath, ticketKey, currentStatus, onComplete, onC
2066
2327
  // src/components/jira/ConfigureJiraSiteModal.tsx
2067
2328
  import { useState as useState14 } from "react";
2068
2329
  import { Box as Box9, Text as Text9, useInput as useInput8 } from "ink";
2069
- import { ScrollView as ScrollView4 } from "ink-scroll-view";
2330
+ import { ScrollView as ScrollView5 } from "ink-scroll-view";
2070
2331
 
2071
2332
  // src/lib/editor.ts
2072
2333
  import { spawnSync as spawnSync2 } from "child_process";
@@ -2207,7 +2468,7 @@ function ConfigureJiraSiteModal({
2207
2468
  /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Select an existing configuration or enter new credentials" }),
2208
2469
  /* @__PURE__ */ jsx9(Box9, { marginTop: 1 }),
2209
2470
  error && /* @__PURE__ */ jsx9(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "red", children: error }) }),
2210
- /* @__PURE__ */ jsx9(Box9, { height: listHeight, overflow: "hidden", children: /* @__PURE__ */ jsxs9(ScrollView4, { ref: scrollRef, children: [
2471
+ /* @__PURE__ */ jsx9(Box9, { height: listHeight, overflow: "hidden", children: /* @__PURE__ */ jsxs9(ScrollView5, { ref: scrollRef, children: [
2211
2472
  existingConfigs.map((config, idx) => {
2212
2473
  const isSelected = selectedExisting === idx;
2213
2474
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
@@ -2451,93 +2712,8 @@ import { Box as Box14, useInput as useInput12 } from "ink";
2451
2712
  import { useEffect as useEffect11, useRef as useRef7, useState as useState15 } from "react";
2452
2713
  import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
2453
2714
  import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
2454
- import { ScrollView as ScrollView5 } from "ink-scroll-view";
2715
+ import { ScrollView as ScrollView6 } from "ink-scroll-view";
2455
2716
  import TextInput2 from "ink-text-input";
2456
-
2457
- // src/lib/claude/api.ts
2458
- import { exec as exec3 } from "child_process";
2459
- function runClaudePrompt(prompt) {
2460
- let childProcess = null;
2461
- let cancelled = false;
2462
- const promise = new Promise((resolve) => {
2463
- const escapedPrompt = prompt.replace(/'/g, "'\\''").replace(/\n/g, "\\n");
2464
- const command = `claude -p $'${escapedPrompt}' --output-format json < /dev/null`;
2465
- childProcess = exec3(command, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
2466
- if (cancelled) {
2467
- resolve({
2468
- success: false,
2469
- error: "Cancelled",
2470
- errorType: "execution_error"
2471
- });
2472
- return;
2473
- }
2474
- if (error) {
2475
- if (error.message.includes("command not found") || error.message.includes("ENOENT") || error.code === "ENOENT") {
2476
- resolve({
2477
- success: false,
2478
- error: "Claude CLI not installed. Run: npm install -g @anthropic-ai/claude-code",
2479
- errorType: "not_installed"
2480
- });
2481
- return;
2482
- }
2483
- resolve({
2484
- success: false,
2485
- error: (stderr == null ? void 0 : stderr.trim()) || error.message,
2486
- errorType: "execution_error"
2487
- });
2488
- return;
2489
- }
2490
- if (!(stdout == null ? void 0 : stdout.trim())) {
2491
- resolve({
2492
- success: false,
2493
- error: (stderr == null ? void 0 : stderr.trim()) || "Claude returned empty response",
2494
- errorType: "execution_error"
2495
- });
2496
- return;
2497
- }
2498
- try {
2499
- const json = JSON.parse(stdout.trim());
2500
- if (json.is_error) {
2501
- resolve({
2502
- success: false,
2503
- error: json.result || "Claude returned an error",
2504
- errorType: "execution_error"
2505
- });
2506
- return;
2507
- }
2508
- resolve({
2509
- success: true,
2510
- data: json.result || stdout.trim()
2511
- });
2512
- } catch {
2513
- resolve({
2514
- success: true,
2515
- data: stdout.trim()
2516
- });
2517
- }
2518
- });
2519
- });
2520
- const cancel = () => {
2521
- cancelled = true;
2522
- if (childProcess) {
2523
- childProcess.kill("SIGTERM");
2524
- }
2525
- };
2526
- return { promise, cancel };
2527
- }
2528
- function generateStandupNotes(logContent) {
2529
- const prompt = `You are helping a developer prepare standup notes. Based on the following log entries, generate concise standup notes that summarize what was accomplished.
2530
-
2531
- Format the output as bullet points grouped by category (e.g., Features, Bug Fixes, Refactoring, etc.). Keep it brief and professional.
2532
-
2533
- Log entries:
2534
- ${logContent}
2535
-
2536
- Generate the standup notes:`;
2537
- return runClaudePrompt(prompt);
2538
- }
2539
-
2540
- // src/components/logs/LogViewerBox.tsx
2541
2717
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2542
2718
  function LogViewerBox({ date, content, isActive, onRefresh, onLogCreated }) {
2543
2719
  const scrollRef = useRef7(null);
@@ -2635,7 +2811,7 @@ ${value.trim()}
2635
2811
  onRefresh();
2636
2812
  };
2637
2813
  return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", flexGrow: 1, children: [
2638
- /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(ScrollView5, { ref: scrollRef, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
2814
+ /* @__PURE__ */ jsx12(TitledBox5, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx12(ScrollView6, { ref: scrollRef, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
2639
2815
  !date && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Select a log file to view" }),
2640
2816
  date && content === null && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Log file not found" }),
2641
2817
  date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Empty log file" }),
@@ -2671,7 +2847,7 @@ ${value.trim()}
2671
2847
  // src/components/logs/LogsHistoryBox.tsx
2672
2848
  import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
2673
2849
  import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
2674
- import { ScrollView as ScrollView6 } from "ink-scroll-view";
2850
+ import { ScrollView as ScrollView7 } from "ink-scroll-view";
2675
2851
  import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2676
2852
  function LogsHistoryBox({
2677
2853
  logFiles,
@@ -2704,7 +2880,7 @@ function LogsHistoryBox({
2704
2880
  );
2705
2881
  return /* @__PURE__ */ jsx13(TitledBox6, { borderStyle: "round", titles: [title], borderColor, height: 5, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, flexGrow: 1, overflow: "hidden", children: [
2706
2882
  logFiles.length === 0 && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "No logs yet" }),
2707
- logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView6, { ref: scrollRef, children: logFiles.map((file, idx) => {
2883
+ logFiles.length > 0 && /* @__PURE__ */ jsx13(ScrollView7, { ref: scrollRef, children: logFiles.map((file, idx) => {
2708
2884
  const isHighlighted = idx === highlightedIndex;
2709
2885
  const isSelected = file.date === selectedDate;
2710
2886
  const cursor = isHighlighted ? ">" : " ";
@@ -2792,6 +2968,7 @@ var GITHUB_KEYBINDINGS = {
2792
2968
  remotes: [{ key: "Space", label: "Select Remote" }],
2793
2969
  prs: [
2794
2970
  { key: "Space", label: "Select" },
2971
+ { key: "c", label: "Generate PR", color: "green" },
2795
2972
  { key: "n", label: "New PR", color: "green" },
2796
2973
  { key: "r", label: "Refresh" },
2797
2974
  { key: "o", label: "Open", color: "green" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clairo",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",