rudel 0.1.1 → 0.1.2

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 (2) hide show
  1. package/dist/cli.js +496 -306
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,5 +1,22 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
+ return to;
18
+ };
19
+ var __require = import.meta.require;
3
20
 
4
21
  // ../../node_modules/.bun/@stricli+core@1.2.5/node_modules/@stricli/core/dist/index.js
5
22
  function checkEnvironmentVariable(process2, varName) {
@@ -1802,242 +1819,150 @@ async function run(app, inputs, context) {
1802
1819
  context.process.exitCode = exitCode;
1803
1820
  }
1804
1821
 
1805
- // src/commands/login.ts
1806
- import { randomBytes } from "crypto";
1822
+ // src/lib/claude-settings.ts
1823
+ import { existsSync, readFileSync, writeFileSync } from "fs";
1824
+ import { join } from "path";
1825
+ var HOOK_COMMAND = "rudel hooks claude session-end";
1826
+ function getClaudeSettingsPath() {
1827
+ return join(process.env.HOME ?? "~", ".claude", "settings.json");
1828
+ }
1829
+ function readClaudeSettings() {
1830
+ const path = getClaudeSettingsPath();
1831
+ if (!existsSync(path))
1832
+ return {};
1833
+ const content = readFileSync(path, "utf-8");
1834
+ return JSON.parse(content);
1835
+ }
1836
+ function writeClaudeSettings(settings) {
1837
+ const path = getClaudeSettingsPath();
1838
+ writeFileSync(path, `${JSON.stringify(settings, null, 2)}
1839
+ `);
1840
+ }
1841
+ function isHookEnabled() {
1842
+ const settings = readClaudeSettings();
1843
+ const entries = settings.hooks?.SessionEnd;
1844
+ if (!Array.isArray(entries))
1845
+ return false;
1846
+ return entries.some((entry) => entry.hooks?.some((h) => h.command === HOOK_COMMAND));
1847
+ }
1848
+ function addHook() {
1849
+ const settings = readClaudeSettings();
1850
+ if (!settings.hooks) {
1851
+ settings.hooks = {};
1852
+ }
1853
+ if (!Array.isArray(settings.hooks.SessionEnd)) {
1854
+ settings.hooks.SessionEnd = [];
1855
+ }
1856
+ const alreadyExists = settings.hooks.SessionEnd.some((entry) => entry.hooks?.some((h) => h.command === HOOK_COMMAND));
1857
+ if (alreadyExists)
1858
+ return;
1859
+ settings.hooks.SessionEnd.push({
1860
+ matcher: "",
1861
+ hooks: [{ type: "command", command: HOOK_COMMAND, async: true }]
1862
+ });
1863
+ writeClaudeSettings(settings);
1864
+ }
1865
+ function removeHook() {
1866
+ const settings = readClaudeSettings();
1867
+ const hooks = settings.hooks;
1868
+ const entries = hooks?.SessionEnd;
1869
+ if (!hooks || !Array.isArray(entries))
1870
+ return;
1871
+ hooks.SessionEnd = entries.filter((entry) => !entry.hooks?.some((h) => h.command === HOOK_COMMAND));
1872
+ if (hooks.SessionEnd.length === 0) {
1873
+ delete hooks.SessionEnd;
1874
+ }
1875
+ if (Object.keys(hooks).length === 0) {
1876
+ delete settings.hooks;
1877
+ }
1878
+ writeClaudeSettings(settings);
1879
+ }
1880
+
1881
+ // src/commands/disable.ts
1882
+ async function runDisable() {
1883
+ const write = (msg) => process.stdout.write(`${msg}
1884
+ `);
1885
+ if (!isHookEnabled()) {
1886
+ write("Auto-upload hook is not enabled.");
1887
+ return;
1888
+ }
1889
+ removeHook();
1890
+ write(`Auto-upload hook removed from ${getClaudeSettingsPath()}`);
1891
+ }
1892
+ var disableCommand = buildCommand({
1893
+ loader: async () => ({ default: runDisable }),
1894
+ parameters: {},
1895
+ docs: {
1896
+ brief: "Disable automatic session upload"
1897
+ }
1898
+ });
1807
1899
 
1808
1900
  // src/lib/credentials.ts
1809
1901
  import {
1810
- existsSync,
1902
+ existsSync as existsSync2,
1811
1903
  mkdirSync,
1812
- readFileSync,
1904
+ readFileSync as readFileSync2,
1813
1905
  rmSync,
1814
- writeFileSync
1906
+ writeFileSync as writeFileSync2
1815
1907
  } from "fs";
1816
- import { join } from "path";
1908
+ import { join as join2 } from "path";
1817
1909
  function getConfigDir() {
1818
- return process.env.RUDEL_CONFIG_DIR ?? join(process.env.HOME ?? "~", ".rudel");
1910
+ return process.env.RUDEL_CONFIG_DIR ?? join2(process.env.HOME ?? "~", ".rudel");
1819
1911
  }
1820
1912
  function getCredentialsPath() {
1821
- return join(getConfigDir(), "credentials.json");
1913
+ return join2(getConfigDir(), "credentials.json");
1822
1914
  }
1823
1915
  function saveCredentials(token, apiBaseUrl) {
1824
1916
  const dir = getConfigDir();
1825
- if (!existsSync(dir)) {
1917
+ if (!existsSync2(dir)) {
1826
1918
  mkdirSync(dir, { recursive: true, mode: 448 });
1827
1919
  }
1828
1920
  const data = { token, apiBaseUrl };
1829
- writeFileSync(getCredentialsPath(), JSON.stringify(data, null, 2), {
1921
+ writeFileSync2(getCredentialsPath(), JSON.stringify(data, null, 2), {
1830
1922
  mode: 384
1831
1923
  });
1832
1924
  }
1833
1925
  function loadCredentials() {
1834
1926
  const path = getCredentialsPath();
1835
- if (!existsSync(path))
1927
+ if (!existsSync2(path))
1836
1928
  return null;
1837
- const content = readFileSync(path, "utf-8");
1929
+ const content = readFileSync2(path, "utf-8");
1838
1930
  return JSON.parse(content);
1839
1931
  }
1840
1932
  function clearCredentials() {
1841
1933
  const path = getCredentialsPath();
1842
- if (existsSync(path)) {
1934
+ if (existsSync2(path)) {
1843
1935
  rmSync(path);
1844
1936
  }
1845
1937
  }
1846
1938
 
1847
- // src/commands/login.ts
1848
- var DEFAULT_API_BASE = "https://rudel.numia.workers.dev";
1849
- var DEFAULT_WEB_URL = "https://rudel.numia.workers.dev";
1850
- var CALLBACK_TIMEOUT_MS = 120000;
1851
- async function runLogin(flags) {
1939
+ // src/commands/enable.ts
1940
+ async function runEnable() {
1852
1941
  const write = (msg) => process.stdout.write(`${msg}
1853
1942
  `);
1854
1943
  const writeError = (msg) => process.stderr.write(`${msg}
1855
1944
  `);
1856
- const existing = loadCredentials();
1857
- if (existing) {
1858
- write("Already logged in. Run `rudel logout` first to switch accounts.");
1859
- return;
1860
- }
1861
- const state = randomBytes(16).toString("hex");
1862
- let resolveCallback;
1863
- let rejectCallback;
1864
- const tokenPromise = new Promise((resolve, reject) => {
1865
- resolveCallback = resolve;
1866
- rejectCallback = reject;
1867
- });
1868
- const server = Bun.serve({
1869
- port: 0,
1870
- hostname: "127.0.0.1",
1871
- fetch(request) {
1872
- const url = new URL(request.url);
1873
- if (url.pathname !== "/callback") {
1874
- return new Response("Not found", { status: 404 });
1875
- }
1876
- const receivedToken = url.searchParams.get("token");
1877
- const receivedState = url.searchParams.get("state");
1878
- if (receivedState !== state) {
1879
- rejectCallback(new Error("State mismatch \u2014 possible CSRF attack"));
1880
- return new Response("<html><body><h1>Login failed</h1><p>State mismatch. Please try again.</p></body></html>", { headers: { "Content-Type": "text/html" } });
1881
- }
1882
- if (!receivedToken) {
1883
- rejectCallback(new Error("No token received"));
1884
- return new Response("<html><body><h1>Login failed</h1><p>No token received.</p></body></html>", { headers: { "Content-Type": "text/html" } });
1885
- }
1886
- resolveCallback(receivedToken);
1887
- return new Response("<html><body><h1>Login successful!</h1><p>You can close this tab and return to the terminal.</p></body></html>", { headers: { "Content-Type": "text/html" } });
1888
- }
1889
- });
1890
- const callbackUrl = `http://127.0.0.1:${server.port}/callback`;
1891
- const loginUrl = `${flags.webUrl}?cli_callback=${encodeURIComponent(callbackUrl)}&state=${state}`;
1892
- write("Opening browser for authentication...");
1893
- write(`If the browser doesn't open, visit: ${loginUrl}`);
1894
- const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1895
- Bun.spawn([opener, loginUrl], { stdout: "ignore", stderr: "ignore" });
1896
- const timeout = setTimeout(() => {
1897
- rejectCallback(new Error("Login timed out after 120 seconds"));
1898
- }, CALLBACK_TIMEOUT_MS);
1899
- let token;
1900
- try {
1901
- token = await tokenPromise;
1902
- } catch (error) {
1903
- clearTimeout(timeout);
1904
- server.stop();
1905
- writeError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
1906
- process.exitCode = 1;
1907
- return;
1908
- }
1909
- clearTimeout(timeout);
1910
- server.stop();
1911
- write("Validating token...");
1912
- const meResponse = await fetch(`${flags.apiBase}/rpc/me`, {
1913
- method: "POST",
1914
- headers: {
1915
- "Content-Type": "application/json",
1916
- Authorization: `Bearer ${token}`
1917
- },
1918
- body: JSON.stringify({})
1919
- });
1920
- if (!meResponse.ok) {
1921
- writeError("Login failed: token validation failed");
1945
+ const credentials = loadCredentials();
1946
+ if (!credentials) {
1947
+ writeError("Error: Not authenticated. Run `rudel login` first.");
1922
1948
  process.exitCode = 1;
1923
1949
  return;
1924
1950
  }
1925
- const body = await meResponse.json();
1926
- saveCredentials(token, flags.apiBase);
1927
- write(`Logged in as ${body.json.name} (${body.json.email})`);
1928
- }
1929
- var loginCommand = buildCommand({
1930
- loader: async () => ({ default: runLogin }),
1931
- parameters: {
1932
- flags: {
1933
- apiBase: {
1934
- kind: "parsed",
1935
- parse: String,
1936
- brief: "API server base URL",
1937
- default: DEFAULT_API_BASE
1938
- },
1939
- webUrl: {
1940
- kind: "parsed",
1941
- parse: String,
1942
- brief: "Web app URL for authentication",
1943
- default: DEFAULT_WEB_URL
1944
- }
1945
- }
1946
- },
1947
- docs: {
1948
- brief: "Authenticate with the Rudel API via browser login"
1949
- }
1950
- });
1951
-
1952
- // src/commands/logout.ts
1953
- async function runLogout() {
1954
- const write = (msg) => process.stdout.write(`${msg}
1955
- `);
1956
- const credentials = loadCredentials();
1957
- if (!credentials) {
1958
- write("Not logged in.");
1951
+ if (isHookEnabled()) {
1952
+ write("Auto-upload hook is already enabled.");
1959
1953
  return;
1960
1954
  }
1961
- clearCredentials();
1962
- write("Logged out successfully.");
1955
+ addHook();
1956
+ write(`Auto-upload hook enabled in ${getClaudeSettingsPath()}`);
1963
1957
  }
1964
- var logoutCommand = buildCommand({
1965
- loader: async () => ({ default: runLogout }),
1958
+ var enableCommand = buildCommand({
1959
+ loader: async () => ({ default: runEnable }),
1966
1960
  parameters: {},
1967
1961
  docs: {
1968
- brief: "Log out and remove stored credentials"
1962
+ brief: "Enable automatic session upload via Claude Code hook"
1969
1963
  }
1970
1964
  });
1971
1965
 
1972
- // src/lib/classifier.ts
1973
- import { mkdir, unlink } from "fs/promises";
1974
- import { homedir } from "os";
1975
- import { join as join2 } from "path";
1976
-
1977
- // src/lib/types.ts
1978
- var SESSION_TAGS = [
1979
- "research",
1980
- "new_feature",
1981
- "bug_fix",
1982
- "refactoring",
1983
- "documentation",
1984
- "tests",
1985
- "other"
1986
- ];
1987
- var DEFAULT_ENDPOINT = "https://rudel.numia.workers.dev/rpc";
1988
-
1989
- // src/lib/classifier.ts
1990
- var SYSTEM_PROMPT = `You are a session classifier. Analyze the Claude Code session transcript and classify it into exactly ONE of these categories:
1991
-
1992
- - research: Exploring codebase, understanding code, answering questions about how things work
1993
- - new_feature: Implementing new functionality or features
1994
- - bug_fix: Fixing bugs, errors, or unexpected behavior
1995
- - refactoring: Restructuring existing code without changing functionality
1996
- - documentation: Writing or updating documentation, comments, READMEs
1997
- - tests: Writing, updating, or fixing tests
1998
-
1999
- CRITICAL: Respond with ONLY the tag name. Nothing else. No explanation, no punctuation, no formatting. Just ONE of: research, new_feature, bug_fix, refactoring, documentation, tests`;
2000
- async function classifySession(content) {
2001
- const truncatedContent = content.slice(0, 50000);
2002
- const tempDir = join2(homedir(), ".claude", "temp");
2003
- const tempFile = join2(tempDir, `classify-${Date.now()}.txt`);
2004
- try {
2005
- await mkdir(tempDir, { recursive: true });
2006
- await Bun.write(tempFile, `Classify this session transcript:
2007
-
2008
- ${truncatedContent}`);
2009
- const prompt = `Read and classify the session transcript in this file: ${tempFile}`;
2010
- const escapedPrompt = prompt.replace(/'/g, "'\\''");
2011
- const escapedSystemPrompt = SYSTEM_PROMPT.replace(/'/g, "'\\''");
2012
- const proc = Bun.spawn([
2013
- "sh",
2014
- "-c",
2015
- `echo '${escapedPrompt}' | claude --output-format text --print --model haiku --no-session-persistence --dangerously-skip-permissions --system-prompt '${escapedSystemPrompt}'`
2016
- ], { stdout: "pipe", stderr: "pipe" });
2017
- const exitCode = await proc.exited;
2018
- const stdout = await new Response(proc.stdout).text();
2019
- if (exitCode !== 0) {
2020
- return "other";
2021
- }
2022
- const output = stdout.trim().toLowerCase();
2023
- if (SESSION_TAGS.includes(output)) {
2024
- return output;
2025
- }
2026
- for (const tag of SESSION_TAGS) {
2027
- if (new RegExp(`\\b${tag}\\b`).test(output)) {
2028
- return tag;
2029
- }
2030
- }
2031
- return "other";
2032
- } catch {
2033
- return;
2034
- } finally {
2035
- try {
2036
- await unlink(tempFile);
2037
- } catch {}
2038
- }
2039
- }
2040
-
2041
1966
  // src/lib/git-info.ts
2042
1967
  import { join as join3 } from "path";
2043
1968
  var {$ } = globalThis.Bun;
@@ -2102,118 +2027,27 @@ async function getGitSha(cwd) {
2102
2027
  }
2103
2028
  }
2104
2029
 
2105
- // src/lib/session-resolver.ts
2106
- import { readdir, stat } from "fs/promises";
2107
- import { homedir as homedir2 } from "os";
2108
- import { basename, dirname, join as join4 } from "path";
2109
- var SESSIONS_BASE_DIR = join4(homedir2(), ".claude", "projects");
2110
- async function resolveSession(input) {
2111
- const isPath = input.includes("/") || input.endsWith(".jsonl");
2112
- if (isPath) {
2113
- return resolveFromPath(input);
2114
- }
2115
- return resolveFromId(input);
2116
- }
2117
- async function resolveFromPath(filePath) {
2118
- const filename = basename(filePath);
2119
- validateNotSubagent(filename);
2120
- const file = Bun.file(filePath);
2121
- if (!await file.exists()) {
2122
- throw new Error(`Session file not found: ${filePath}`);
2030
+ // src/lib/subagent-reader.ts
2031
+ import { join as join4 } from "path";
2032
+ async function readSubagentFiles(sessionDir, agentIds, sessionId) {
2033
+ const subagents = [];
2034
+ for (const agentId of agentIds) {
2035
+ const possiblePaths = [
2036
+ join4(sessionDir, `agent-${agentId}.jsonl`),
2037
+ ...sessionId ? [join4(sessionDir, sessionId, "subagents", `agent-${agentId}.jsonl`)] : []
2038
+ ];
2039
+ for (const agentPath of possiblePaths) {
2040
+ try {
2041
+ const file = Bun.file(agentPath);
2042
+ if (await file.exists()) {
2043
+ const content = await file.text();
2044
+ subagents.push({ agentId, content });
2045
+ break;
2046
+ }
2047
+ } catch {}
2048
+ }
2123
2049
  }
2124
- const sessionId = filename.replace(/\.jsonl$/, "");
2125
- const sessionDir = dirname(filePath);
2126
- const parentDir = basename(sessionDir);
2127
- const projectPath = await decodeProjectPath(parentDir);
2128
- return { transcriptPath: filePath, projectPath, sessionDir, sessionId };
2129
- }
2130
- async function resolveFromId(sessionId) {
2131
- validateNotSubagent(`${sessionId}.jsonl`);
2132
- const sessionFileName = `${sessionId}.jsonl`;
2133
- let projectDirs;
2134
- try {
2135
- projectDirs = await readdir(SESSIONS_BASE_DIR);
2136
- } catch {
2137
- throw new Error(`Session not found: ${sessionId}`);
2138
- }
2139
- for (const projectDir of projectDirs) {
2140
- const sessionDir = join4(SESSIONS_BASE_DIR, projectDir);
2141
- try {
2142
- const files = await readdir(sessionDir);
2143
- if (files.includes(sessionFileName)) {
2144
- const transcriptPath = join4(sessionDir, sessionFileName);
2145
- const projectPath = await decodeProjectPath(projectDir);
2146
- return {
2147
- transcriptPath,
2148
- projectPath,
2149
- sessionDir,
2150
- sessionId
2151
- };
2152
- }
2153
- } catch {}
2154
- }
2155
- throw new Error(`Session not found: ${sessionId}`);
2156
- }
2157
- function validateNotSubagent(filename) {
2158
- if (filename.startsWith("agent-") && filename.endsWith(".jsonl")) {
2159
- throw new Error("This is a subagent file, not a main session. Please provide the main session ID or path.");
2160
- }
2161
- }
2162
- async function decodeProjectPath(encodedDir) {
2163
- const parts = encodedDir.replace(/^-/, "").split("-");
2164
- async function findPath(partIndex, currentPath) {
2165
- if (partIndex >= parts.length) {
2166
- try {
2167
- await stat(currentPath);
2168
- return currentPath;
2169
- } catch {
2170
- return null;
2171
- }
2172
- }
2173
- for (let endIndex = parts.length;endIndex > partIndex; endIndex--) {
2174
- const segment = parts.slice(partIndex, endIndex).join("-");
2175
- const testPath = currentPath ? `${currentPath}/${segment}` : `/${segment}`;
2176
- try {
2177
- await stat(testPath);
2178
- if (endIndex === parts.length) {
2179
- return testPath;
2180
- }
2181
- const result2 = await findPath(endIndex, testPath);
2182
- if (result2) {
2183
- return result2;
2184
- }
2185
- } catch {}
2186
- }
2187
- return null;
2188
- }
2189
- const result = await findPath(0, "");
2190
- if (result) {
2191
- return result;
2192
- }
2193
- return `/${parts.join("/")}`;
2194
- }
2195
-
2196
- // src/lib/subagent-reader.ts
2197
- import { join as join5 } from "path";
2198
- async function readSubagentFiles(sessionDir, agentIds, sessionId) {
2199
- const subagents = [];
2200
- for (const agentId of agentIds) {
2201
- const possiblePaths = [
2202
- join5(sessionDir, `agent-${agentId}.jsonl`),
2203
- ...sessionId ? [join5(sessionDir, sessionId, "subagents", `agent-${agentId}.jsonl`)] : []
2204
- ];
2205
- for (const agentPath of possiblePaths) {
2206
- try {
2207
- const file = Bun.file(agentPath);
2208
- if (await file.exists()) {
2209
- const content = await file.text();
2210
- subagents.push({ agentId, content });
2211
- break;
2212
- }
2213
- } catch {}
2214
- }
2215
- }
2216
- return subagents;
2050
+ return subagents;
2217
2051
  }
2218
2052
 
2219
2053
  // src/lib/transcript-reader.ts
@@ -3734,6 +3568,358 @@ async function uploadSession(request, config) {
3734
3568
  }
3735
3569
  }
3736
3570
 
3571
+ // src/commands/hooks/claude/session-end.ts
3572
+ async function readStdin() {
3573
+ const chunks = [];
3574
+ for await (const chunk of process.stdin) {
3575
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString());
3576
+ }
3577
+ return chunks.join("");
3578
+ }
3579
+ async function runSessionEnd() {
3580
+ try {
3581
+ const raw = await readStdin();
3582
+ if (!raw.trim())
3583
+ return;
3584
+ const input = JSON.parse(raw);
3585
+ if (!input.session_id || !input.transcript_path)
3586
+ return;
3587
+ const credentials = loadCredentials();
3588
+ if (!credentials)
3589
+ return;
3590
+ const content = await readTranscript(input.transcript_path);
3591
+ const agentIds = extractAgentIds(content);
3592
+ const { dirname } = await import("path");
3593
+ const sessionDir = dirname(input.transcript_path);
3594
+ const subagents = agentIds.length > 0 ? await readSubagentFiles(sessionDir, agentIds, input.session_id) : [];
3595
+ const gitInfo = await getGitInfo(input.cwd);
3596
+ const request = {
3597
+ sessionId: input.session_id,
3598
+ projectPath: input.cwd,
3599
+ repository: gitInfo.repository,
3600
+ gitBranch: gitInfo.branch,
3601
+ gitSha: gitInfo.sha,
3602
+ content,
3603
+ subagents: subagents.length > 0 ? subagents : undefined
3604
+ };
3605
+ const endpoint = `${credentials.apiBaseUrl}/rpc`;
3606
+ await uploadSession(request, { endpoint, token: credentials.token });
3607
+ } catch {}
3608
+ }
3609
+ var sessionEndCommand = buildCommand({
3610
+ loader: async () => ({ default: runSessionEnd }),
3611
+ parameters: {},
3612
+ docs: {
3613
+ brief: "Handle Claude Code SessionEnd hook"
3614
+ }
3615
+ });
3616
+
3617
+ // src/commands/hooks/claude/index.ts
3618
+ var claudeRouteMap = buildRouteMap({
3619
+ routes: {
3620
+ "session-end": sessionEndCommand
3621
+ },
3622
+ docs: {
3623
+ brief: "Claude Code hook handlers"
3624
+ }
3625
+ });
3626
+
3627
+ // src/commands/hooks/index.ts
3628
+ var hooksRouteMap = buildRouteMap({
3629
+ routes: {
3630
+ claude: claudeRouteMap
3631
+ },
3632
+ docs: {
3633
+ brief: "Hook handlers"
3634
+ }
3635
+ });
3636
+
3637
+ // src/commands/login.ts
3638
+ import { randomBytes } from "crypto";
3639
+ var DEFAULT_API_BASE = "https://app.rudel.ai";
3640
+ var DEFAULT_WEB_URL = "https://app.rudel.ai";
3641
+ var CALLBACK_TIMEOUT_MS = 120000;
3642
+ async function runLogin(flags) {
3643
+ const write = (msg) => process.stdout.write(`${msg}
3644
+ `);
3645
+ const writeError = (msg) => process.stderr.write(`${msg}
3646
+ `);
3647
+ const existing = loadCredentials();
3648
+ if (existing) {
3649
+ write("Already logged in. Run `rudel logout` first to switch accounts.");
3650
+ return;
3651
+ }
3652
+ const state = randomBytes(16).toString("hex");
3653
+ let resolveCallback;
3654
+ let rejectCallback;
3655
+ const tokenPromise = new Promise((resolve, reject) => {
3656
+ resolveCallback = resolve;
3657
+ rejectCallback = reject;
3658
+ });
3659
+ const server = Bun.serve({
3660
+ port: 0,
3661
+ hostname: "127.0.0.1",
3662
+ fetch(request) {
3663
+ const url = new URL(request.url);
3664
+ if (url.pathname !== "/callback") {
3665
+ return new Response("Not found", { status: 404 });
3666
+ }
3667
+ const receivedToken = url.searchParams.get("token");
3668
+ const receivedState = url.searchParams.get("state");
3669
+ if (receivedState !== state) {
3670
+ rejectCallback(new Error("State mismatch \u2014 possible CSRF attack"));
3671
+ return new Response("<html><body><h1>Login failed</h1><p>State mismatch. Please try again.</p></body></html>", { headers: { "Content-Type": "text/html" } });
3672
+ }
3673
+ if (!receivedToken) {
3674
+ rejectCallback(new Error("No token received"));
3675
+ return new Response("<html><body><h1>Login failed</h1><p>No token received.</p></body></html>", { headers: { "Content-Type": "text/html" } });
3676
+ }
3677
+ resolveCallback(receivedToken);
3678
+ return new Response("<html><body><h1>Login successful!</h1><p>You can close this tab and return to the terminal.</p></body></html>", { headers: { "Content-Type": "text/html" } });
3679
+ }
3680
+ });
3681
+ const callbackUrl = `http://127.0.0.1:${server.port}/callback`;
3682
+ const loginUrl = `${flags.webUrl}?cli_callback=${encodeURIComponent(callbackUrl)}&state=${state}`;
3683
+ write("Opening browser for authentication...");
3684
+ write(`If the browser doesn't open, visit: ${loginUrl}`);
3685
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3686
+ Bun.spawn([opener, loginUrl], { stdout: "ignore", stderr: "ignore" });
3687
+ const timeout = setTimeout(() => {
3688
+ rejectCallback(new Error("Login timed out after 120 seconds"));
3689
+ }, CALLBACK_TIMEOUT_MS);
3690
+ let token;
3691
+ try {
3692
+ token = await tokenPromise;
3693
+ } catch (error) {
3694
+ clearTimeout(timeout);
3695
+ server.stop();
3696
+ writeError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
3697
+ process.exitCode = 1;
3698
+ return;
3699
+ }
3700
+ clearTimeout(timeout);
3701
+ server.stop();
3702
+ write("Validating token...");
3703
+ const meResponse = await fetch(`${flags.apiBase}/rpc/me`, {
3704
+ method: "POST",
3705
+ headers: {
3706
+ "Content-Type": "application/json",
3707
+ Authorization: `Bearer ${token}`
3708
+ },
3709
+ body: JSON.stringify({})
3710
+ });
3711
+ if (!meResponse.ok) {
3712
+ writeError("Login failed: token validation failed");
3713
+ process.exitCode = 1;
3714
+ return;
3715
+ }
3716
+ const body = await meResponse.json();
3717
+ saveCredentials(token, flags.apiBase);
3718
+ write(`Logged in as ${body.json.name} (${body.json.email})`);
3719
+ }
3720
+ var loginCommand = buildCommand({
3721
+ loader: async () => ({ default: runLogin }),
3722
+ parameters: {
3723
+ flags: {
3724
+ apiBase: {
3725
+ kind: "parsed",
3726
+ parse: String,
3727
+ brief: "API server base URL",
3728
+ default: DEFAULT_API_BASE
3729
+ },
3730
+ webUrl: {
3731
+ kind: "parsed",
3732
+ parse: String,
3733
+ brief: "Web app URL for authentication",
3734
+ default: DEFAULT_WEB_URL
3735
+ }
3736
+ }
3737
+ },
3738
+ docs: {
3739
+ brief: "Authenticate with the Rudel API via browser login"
3740
+ }
3741
+ });
3742
+
3743
+ // src/commands/logout.ts
3744
+ async function runLogout() {
3745
+ const write = (msg) => process.stdout.write(`${msg}
3746
+ `);
3747
+ const credentials = loadCredentials();
3748
+ if (!credentials) {
3749
+ write("Not logged in.");
3750
+ return;
3751
+ }
3752
+ clearCredentials();
3753
+ write("Logged out successfully.");
3754
+ }
3755
+ var logoutCommand = buildCommand({
3756
+ loader: async () => ({ default: runLogout }),
3757
+ parameters: {},
3758
+ docs: {
3759
+ brief: "Log out and remove stored credentials"
3760
+ }
3761
+ });
3762
+
3763
+ // src/lib/classifier.ts
3764
+ import { mkdir, unlink } from "fs/promises";
3765
+ import { homedir } from "os";
3766
+ import { join as join5 } from "path";
3767
+
3768
+ // src/lib/types.ts
3769
+ var SESSION_TAGS = [
3770
+ "research",
3771
+ "new_feature",
3772
+ "bug_fix",
3773
+ "refactoring",
3774
+ "documentation",
3775
+ "tests",
3776
+ "other"
3777
+ ];
3778
+ var DEFAULT_ENDPOINT = "https://app.rudel.ai/rpc";
3779
+
3780
+ // src/lib/classifier.ts
3781
+ var SYSTEM_PROMPT = `You are a session classifier. Analyze the Claude Code session transcript and classify it into exactly ONE of these categories:
3782
+
3783
+ - research: Exploring codebase, understanding code, answering questions about how things work
3784
+ - new_feature: Implementing new functionality or features
3785
+ - bug_fix: Fixing bugs, errors, or unexpected behavior
3786
+ - refactoring: Restructuring existing code without changing functionality
3787
+ - documentation: Writing or updating documentation, comments, READMEs
3788
+ - tests: Writing, updating, or fixing tests
3789
+
3790
+ CRITICAL: Respond with ONLY the tag name. Nothing else. No explanation, no punctuation, no formatting. Just ONE of: research, new_feature, bug_fix, refactoring, documentation, tests`;
3791
+ async function classifySession(content) {
3792
+ const truncatedContent = content.slice(0, 50000);
3793
+ const tempDir = join5(homedir(), ".claude", "temp");
3794
+ const tempFile = join5(tempDir, `classify-${Date.now()}.txt`);
3795
+ try {
3796
+ await mkdir(tempDir, { recursive: true });
3797
+ await Bun.write(tempFile, `Classify this session transcript:
3798
+
3799
+ ${truncatedContent}`);
3800
+ const prompt = `Read and classify the session transcript in this file: ${tempFile}`;
3801
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
3802
+ const escapedSystemPrompt = SYSTEM_PROMPT.replace(/'/g, "'\\''");
3803
+ const proc = Bun.spawn([
3804
+ "sh",
3805
+ "-c",
3806
+ `echo '${escapedPrompt}' | claude --output-format text --print --model haiku --no-session-persistence --dangerously-skip-permissions --system-prompt '${escapedSystemPrompt}'`
3807
+ ], { stdout: "pipe", stderr: "pipe" });
3808
+ const exitCode = await proc.exited;
3809
+ const stdout = await new Response(proc.stdout).text();
3810
+ if (exitCode !== 0) {
3811
+ return "other";
3812
+ }
3813
+ const output = stdout.trim().toLowerCase();
3814
+ if (SESSION_TAGS.includes(output)) {
3815
+ return output;
3816
+ }
3817
+ for (const tag of SESSION_TAGS) {
3818
+ if (new RegExp(`\\b${tag}\\b`).test(output)) {
3819
+ return tag;
3820
+ }
3821
+ }
3822
+ return "other";
3823
+ } catch {
3824
+ return;
3825
+ } finally {
3826
+ try {
3827
+ await unlink(tempFile);
3828
+ } catch {}
3829
+ }
3830
+ }
3831
+
3832
+ // src/lib/session-resolver.ts
3833
+ import { readdir, stat } from "fs/promises";
3834
+ import { homedir as homedir2 } from "os";
3835
+ import { basename, dirname, join as join6 } from "path";
3836
+ var SESSIONS_BASE_DIR = join6(homedir2(), ".claude", "projects");
3837
+ async function resolveSession(input) {
3838
+ const isPath = input.includes("/") || input.endsWith(".jsonl");
3839
+ if (isPath) {
3840
+ return resolveFromPath(input);
3841
+ }
3842
+ return resolveFromId(input);
3843
+ }
3844
+ async function resolveFromPath(filePath) {
3845
+ const filename = basename(filePath);
3846
+ validateNotSubagent(filename);
3847
+ const file = Bun.file(filePath);
3848
+ if (!await file.exists()) {
3849
+ throw new Error(`Session file not found: ${filePath}`);
3850
+ }
3851
+ const sessionId = filename.replace(/\.jsonl$/, "");
3852
+ const sessionDir = dirname(filePath);
3853
+ const parentDir = basename(sessionDir);
3854
+ const projectPath = await decodeProjectPath(parentDir);
3855
+ return { transcriptPath: filePath, projectPath, sessionDir, sessionId };
3856
+ }
3857
+ async function resolveFromId(sessionId) {
3858
+ validateNotSubagent(`${sessionId}.jsonl`);
3859
+ const sessionFileName = `${sessionId}.jsonl`;
3860
+ let projectDirs;
3861
+ try {
3862
+ projectDirs = await readdir(SESSIONS_BASE_DIR);
3863
+ } catch {
3864
+ throw new Error(`Session not found: ${sessionId}`);
3865
+ }
3866
+ for (const projectDir of projectDirs) {
3867
+ const sessionDir = join6(SESSIONS_BASE_DIR, projectDir);
3868
+ try {
3869
+ const files = await readdir(sessionDir);
3870
+ if (files.includes(sessionFileName)) {
3871
+ const transcriptPath = join6(sessionDir, sessionFileName);
3872
+ const projectPath = await decodeProjectPath(projectDir);
3873
+ return {
3874
+ transcriptPath,
3875
+ projectPath,
3876
+ sessionDir,
3877
+ sessionId
3878
+ };
3879
+ }
3880
+ } catch {}
3881
+ }
3882
+ throw new Error(`Session not found: ${sessionId}`);
3883
+ }
3884
+ function validateNotSubagent(filename) {
3885
+ if (filename.startsWith("agent-") && filename.endsWith(".jsonl")) {
3886
+ throw new Error("This is a subagent file, not a main session. Please provide the main session ID or path.");
3887
+ }
3888
+ }
3889
+ async function decodeProjectPath(encodedDir) {
3890
+ const parts = encodedDir.replace(/^-/, "").split("-");
3891
+ async function findPath(partIndex, currentPath) {
3892
+ if (partIndex >= parts.length) {
3893
+ try {
3894
+ await stat(currentPath);
3895
+ return currentPath;
3896
+ } catch {
3897
+ return null;
3898
+ }
3899
+ }
3900
+ for (let endIndex = parts.length;endIndex > partIndex; endIndex--) {
3901
+ const segment = parts.slice(partIndex, endIndex).join("-");
3902
+ const testPath = currentPath ? `${currentPath}/${segment}` : `/${segment}`;
3903
+ try {
3904
+ await stat(testPath);
3905
+ if (endIndex === parts.length) {
3906
+ return testPath;
3907
+ }
3908
+ const result2 = await findPath(endIndex, testPath);
3909
+ if (result2) {
3910
+ return result2;
3911
+ }
3912
+ } catch {}
3913
+ }
3914
+ return null;
3915
+ }
3916
+ const result = await findPath(0, "");
3917
+ if (result) {
3918
+ return result;
3919
+ }
3920
+ return `/${parts.join("/")}`;
3921
+ }
3922
+
3737
3923
  // src/commands/upload.ts
3738
3924
  async function runUpload(flags, session) {
3739
3925
  const write = (msg) => {
@@ -3913,16 +4099,20 @@ var routes = buildRouteMap({
3913
4099
  login: loginCommand,
3914
4100
  logout: logoutCommand,
3915
4101
  whoami: whoamiCommand,
3916
- upload: uploadCommand
4102
+ upload: uploadCommand,
4103
+ enable: enableCommand,
4104
+ disable: disableCommand,
4105
+ hooks: hooksRouteMap
3917
4106
  },
3918
4107
  docs: {
3919
- brief: "CLI tools for managing Claude Code sessions"
4108
+ brief: "CLI tools for managing Claude Code sessions",
4109
+ hideRoute: { hooks: true }
3920
4110
  }
3921
4111
  });
3922
4112
  var app = buildApplication(routes, {
3923
4113
  name: "rudel",
3924
4114
  versionInfo: {
3925
- currentVersion: "0.1.1"
4115
+ currentVersion: "0.1.2"
3926
4116
  },
3927
4117
  scanner: {
3928
4118
  caseStyle: "allow-kebab-for-camel"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rudel",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "rudel": "./dist/cli.js"