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.
- package/dist/cli.js +496 -306
- 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/
|
|
1806
|
-
import {
|
|
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 ??
|
|
1910
|
+
return process.env.RUDEL_CONFIG_DIR ?? join2(process.env.HOME ?? "~", ".rudel");
|
|
1819
1911
|
}
|
|
1820
1912
|
function getCredentialsPath() {
|
|
1821
|
-
return
|
|
1913
|
+
return join2(getConfigDir(), "credentials.json");
|
|
1822
1914
|
}
|
|
1823
1915
|
function saveCredentials(token, apiBaseUrl) {
|
|
1824
1916
|
const dir = getConfigDir();
|
|
1825
|
-
if (!
|
|
1917
|
+
if (!existsSync2(dir)) {
|
|
1826
1918
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1827
1919
|
}
|
|
1828
1920
|
const data = { token, apiBaseUrl };
|
|
1829
|
-
|
|
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 (!
|
|
1927
|
+
if (!existsSync2(path))
|
|
1836
1928
|
return null;
|
|
1837
|
-
const content =
|
|
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 (
|
|
1934
|
+
if (existsSync2(path)) {
|
|
1843
1935
|
rmSync(path);
|
|
1844
1936
|
}
|
|
1845
1937
|
}
|
|
1846
1938
|
|
|
1847
|
-
// src/commands/
|
|
1848
|
-
|
|
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
|
|
1857
|
-
if (
|
|
1858
|
-
|
|
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
|
-
|
|
1926
|
-
|
|
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
|
-
|
|
1962
|
-
write(
|
|
1955
|
+
addHook();
|
|
1956
|
+
write(`Auto-upload hook enabled in ${getClaudeSettingsPath()}`);
|
|
1963
1957
|
}
|
|
1964
|
-
var
|
|
1965
|
-
loader: async () => ({ default:
|
|
1958
|
+
var enableCommand = buildCommand({
|
|
1959
|
+
loader: async () => ({ default: runEnable }),
|
|
1966
1960
|
parameters: {},
|
|
1967
1961
|
docs: {
|
|
1968
|
-
brief: "
|
|
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/
|
|
2106
|
-
import {
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
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
|
-
|
|
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.
|
|
4115
|
+
currentVersion: "0.1.2"
|
|
3926
4116
|
},
|
|
3927
4117
|
scanner: {
|
|
3928
4118
|
caseStyle: "allow-kebab-for-camel"
|