@halooojustin/cch 0.1.1 → 0.2.1
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/README.md +4 -4
- package/README.zh-CN.md +2 -2
- package/dist/cli.js +91 -40
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -43,9 +43,9 @@ Then you can just tell Claude Code things like "find my iOS debugging conversati
|
|
|
43
43
|
Just describe what you remember. AI finds the session.
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
ch
|
|
47
|
-
ch the one where I was deploying
|
|
48
|
-
ch
|
|
46
|
+
ch the iOS debugging session
|
|
47
|
+
ch the one where I was deploying openclaw
|
|
48
|
+
ch the wallet refactor last week
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
`ch` pipes your query + session list to `claude -p` (tries Haiku first for speed, falls back to default model), which returns the best matches. Pick one and it resumes in your multiplexer.
|
|
@@ -121,7 +121,7 @@ Descriptions you pass to `ch new` are used in multiple places:
|
|
|
121
121
|
|
|
122
122
|
- **Zellij tab name** — visible in the tab bar when inside the session (supports Chinese)
|
|
123
123
|
- **`ch ls` output** — shown next to the session name
|
|
124
|
-
- **Session name** — English descriptions are included in the session name (e.g. `ch-
|
|
124
|
+
- **Session name** — English descriptions are included in the session name (e.g. `ch-myproject-fix-login-bug`), Chinese descriptions use a hash fallback (e.g. `ch-myproject-a1b2c3`) since Zellij session names don't support CJK
|
|
125
125
|
|
|
126
126
|
## Configuration
|
|
127
127
|
|
package/README.zh-CN.md
CHANGED
|
@@ -45,7 +45,7 @@ cp -r $(npm root -g)/cch/skill ~/.claude/skills/cch
|
|
|
45
45
|
```bash
|
|
46
46
|
ch 上次帮我调试 iOS 的那个对话
|
|
47
47
|
ch 帮我部署虾的那几个
|
|
48
|
-
ch
|
|
48
|
+
ch demo 钱包重构的那个
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
`ch` 会把你的描述和会话列表一起发给 `claude -p`(优先使用 Haiku 模型加速,失败自动回退到默认模型),返回最匹配的结果。选中后直接在终端复用器里恢复。
|
|
@@ -121,7 +121,7 @@ ch kill ch-myproject-fix-auth
|
|
|
121
121
|
|
|
122
122
|
- **Zellij tab 名** — 进入会话后在 tab 栏可见(支持中文)
|
|
123
123
|
- **`ch ls` 输出** — 显示在会话名旁边
|
|
124
|
-
- **会话名** — 英文描述直接拼入会话名(如 `ch-
|
|
124
|
+
- **会话名** — 英文描述直接拼入会话名(如 `ch-myproject-fix-login-bug`),中文描述使用哈希缩写(如 `ch-myproject-a1b2c3`),因为 Zellij 会话名不支持 CJK 字符
|
|
125
125
|
|
|
126
126
|
## 配置
|
|
127
127
|
|
package/dist/cli.js
CHANGED
|
@@ -3547,6 +3547,10 @@ default_layout "${escapeKdl(layoutPath)}"
|
|
|
3547
3547
|
try {
|
|
3548
3548
|
execFileSync("zellij", ["kill-session", name], { stdio: "pipe" });
|
|
3549
3549
|
} catch {
|
|
3550
|
+
try {
|
|
3551
|
+
execFileSync("zellij", ["delete-session", name], { stdio: "pipe" });
|
|
3552
|
+
} catch {
|
|
3553
|
+
}
|
|
3550
3554
|
}
|
|
3551
3555
|
}
|
|
3552
3556
|
};
|
|
@@ -3751,7 +3755,7 @@ var CACHE_FILE = join2(CONFIG_DIR, "cache.json");
|
|
|
3751
3755
|
var DEFAULT_CONFIG = {
|
|
3752
3756
|
backend: "auto",
|
|
3753
3757
|
claudeCommand: "claude",
|
|
3754
|
-
claudeArgs: [],
|
|
3758
|
+
claudeArgs: ["--dangerously-skip-permissions"],
|
|
3755
3759
|
historyLimit: 100
|
|
3756
3760
|
};
|
|
3757
3761
|
function ensureDir() {
|
|
@@ -3951,13 +3955,13 @@ async function killSession(name) {
|
|
|
3951
3955
|
backend.killSession(name);
|
|
3952
3956
|
removeSessionMeta(name);
|
|
3953
3957
|
}
|
|
3954
|
-
async function resumeInSession(sessionId, cwd) {
|
|
3958
|
+
async function resumeInSession(sessionId, cwd, description) {
|
|
3955
3959
|
const backend = await getBackend();
|
|
3956
3960
|
const config = getConfig();
|
|
3957
3961
|
const dirName = basename2(cwd);
|
|
3958
3962
|
const name = `ch-${dirName}-${sessionId.slice(0, 8)}`;
|
|
3959
3963
|
setSessionMeta(name, {
|
|
3960
|
-
description:
|
|
3964
|
+
description: description || sessionId.slice(0, 8),
|
|
3961
3965
|
cwd,
|
|
3962
3966
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3963
3967
|
});
|
|
@@ -3992,10 +3996,10 @@ function truncate(str, maxWidth) {
|
|
|
3992
3996
|
}
|
|
3993
3997
|
return str.slice(0, i);
|
|
3994
3998
|
}
|
|
3995
|
-
function interactiveSelect(items, hint = "Up/Down to navigate, Enter to select, Esc to cancel") {
|
|
3996
|
-
if (!process.stdin.isTTY) return Promise.resolve(-1);
|
|
3999
|
+
function interactiveSelect(items, hint = "Up/Down to navigate, Enter to select, Esc to cancel", options) {
|
|
4000
|
+
if (!process.stdin.isTTY) return Promise.resolve({ value: -1, action: "cancel" });
|
|
3997
4001
|
return new Promise((resolve) => {
|
|
3998
|
-
let cursor = 0;
|
|
4002
|
+
let cursor = Math.min(options?.initialCursor ?? 0, items.length - 1);
|
|
3999
4003
|
const cols = process.stdout.columns || 80;
|
|
4000
4004
|
const rows = process.stdout.rows || 24;
|
|
4001
4005
|
const pageSize = Math.min(items.length, rows - 4);
|
|
@@ -4009,8 +4013,8 @@ function interactiveSelect(items, hint = "Up/Down to navigate, Enter to select,
|
|
|
4009
4013
|
function renderLines() {
|
|
4010
4014
|
const { start, end } = getWindow();
|
|
4011
4015
|
const lines = [];
|
|
4012
|
-
const
|
|
4013
|
-
lines.push(truncate(`${DIM}${hint}${RESET}${
|
|
4016
|
+
const statusDisplay = pendingDelete ? ` ${ESC}[31md \u2014 press d again to kill${RESET}` : inputBuf ? ` > ${inputBuf}_` : "";
|
|
4017
|
+
lines.push(truncate(`${DIM}${hint}${RESET}${statusDisplay}`, cols));
|
|
4014
4018
|
for (let i = start; i < end; i++) {
|
|
4015
4019
|
const raw = items[i].label;
|
|
4016
4020
|
if (i === cursor) {
|
|
@@ -4047,20 +4051,37 @@ function interactiveSelect(items, hint = "Up/Down to navigate, Enter to select,
|
|
|
4047
4051
|
drawnLines = lines.length;
|
|
4048
4052
|
}
|
|
4049
4053
|
let inputBuf = "";
|
|
4050
|
-
|
|
4054
|
+
let pendingDelete = false;
|
|
4055
|
+
let deleteTimer = null;
|
|
4056
|
+
function cleanup(clearOutput = false) {
|
|
4057
|
+
if (deleteTimer) clearTimeout(deleteTimer);
|
|
4051
4058
|
process.stdin.setRawMode(false);
|
|
4052
4059
|
process.stdin.removeListener("data", onData);
|
|
4053
4060
|
process.stdin.pause();
|
|
4061
|
+
if (clearOutput && drawnLines > 0) {
|
|
4062
|
+
for (let i = 0; i < drawnLines; i++) {
|
|
4063
|
+
process.stdout.write(`${ESC}[A`);
|
|
4064
|
+
}
|
|
4065
|
+
for (let i = 0; i < drawnLines; i++) {
|
|
4066
|
+
process.stdout.write(`${CLEAR_LINE}
|
|
4067
|
+
`);
|
|
4068
|
+
}
|
|
4069
|
+
for (let i = 0; i < drawnLines; i++) {
|
|
4070
|
+
process.stdout.write(`${ESC}[A`);
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4054
4073
|
process.stdout.write(CURSOR_SHOW);
|
|
4055
4074
|
}
|
|
4056
4075
|
function onData(buf) {
|
|
4057
4076
|
const key = buf.toString();
|
|
4058
4077
|
if (key === `${ESC}[A` || key === "k") {
|
|
4059
4078
|
inputBuf = "";
|
|
4079
|
+
pendingDelete = false;
|
|
4060
4080
|
if (cursor > 0) cursor--;
|
|
4061
4081
|
draw();
|
|
4062
4082
|
} else if (key === `${ESC}[B` || key === "j") {
|
|
4063
4083
|
inputBuf = "";
|
|
4084
|
+
pendingDelete = false;
|
|
4064
4085
|
if (cursor < items.length - 1) cursor++;
|
|
4065
4086
|
draw();
|
|
4066
4087
|
} else if (key === "\r" || key === "\n") {
|
|
@@ -4068,15 +4089,30 @@ function interactiveSelect(items, hint = "Up/Down to navigate, Enter to select,
|
|
|
4068
4089
|
const num = parseInt(inputBuf, 10);
|
|
4069
4090
|
if (num >= 1 && num <= items.length) {
|
|
4070
4091
|
cleanup();
|
|
4071
|
-
resolve(items[num - 1].value);
|
|
4092
|
+
resolve({ value: items[num - 1].value, action: "select" });
|
|
4072
4093
|
return;
|
|
4073
4094
|
}
|
|
4074
4095
|
}
|
|
4075
4096
|
cleanup();
|
|
4076
|
-
resolve(items[cursor].value);
|
|
4097
|
+
resolve({ value: items[cursor].value, action: "select" });
|
|
4098
|
+
} else if (key === "d" && options?.deleteKey && !inputBuf) {
|
|
4099
|
+
if (pendingDelete) {
|
|
4100
|
+
pendingDelete = false;
|
|
4101
|
+
if (deleteTimer) clearTimeout(deleteTimer);
|
|
4102
|
+
cleanup(true);
|
|
4103
|
+
resolve({ value: items[cursor].value, action: "delete" });
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
pendingDelete = true;
|
|
4107
|
+
draw();
|
|
4108
|
+
deleteTimer = setTimeout(() => {
|
|
4109
|
+
pendingDelete = false;
|
|
4110
|
+
draw();
|
|
4111
|
+
}, 1e3);
|
|
4112
|
+
return;
|
|
4077
4113
|
} else if (key === ESC || key === "q" || key === "") {
|
|
4078
4114
|
cleanup();
|
|
4079
|
-
resolve(-1);
|
|
4115
|
+
resolve({ value: -1, action: "cancel" });
|
|
4080
4116
|
} else if (key === "\x7F" || key === "\b") {
|
|
4081
4117
|
if (inputBuf.length > 0) {
|
|
4082
4118
|
inputBuf = inputBuf.slice(0, -1);
|
|
@@ -4109,10 +4145,10 @@ async function lsCommand(n) {
|
|
|
4109
4145
|
const ts = s.timestamp.slice(5, 16).replace("T", " ");
|
|
4110
4146
|
return { label: `${num} ${project.padEnd(20)} ${ts} ${msg}`, value: i };
|
|
4111
4147
|
});
|
|
4112
|
-
const
|
|
4113
|
-
if (
|
|
4114
|
-
const s = sessions[
|
|
4115
|
-
await resumeInSession(s.sessionId, s.cwd);
|
|
4148
|
+
const result = await interactiveSelect(items);
|
|
4149
|
+
if (result.action === "select" && result.value >= 0) {
|
|
4150
|
+
const s = sessions[result.value];
|
|
4151
|
+
await resumeInSession(s.sessionId, s.cwd, s.firstMsg.replace(/\n/g, " ").slice(0, 50));
|
|
4116
4152
|
}
|
|
4117
4153
|
}
|
|
4118
4154
|
|
|
@@ -4144,7 +4180,7 @@ Found ${matches.length} sessions:
|
|
|
4144
4180
|
const idx = parseInt(answer, 10);
|
|
4145
4181
|
if (idx >= 1 && idx <= matches.length) {
|
|
4146
4182
|
const s = matches[idx - 1];
|
|
4147
|
-
await resumeInSession(s.sessionId, s.cwd);
|
|
4183
|
+
await resumeInSession(s.sessionId, s.cwd, s.firstMsg.replace(/\n/g, " ").slice(0, 50));
|
|
4148
4184
|
}
|
|
4149
4185
|
}
|
|
4150
4186
|
}
|
|
@@ -4239,7 +4275,7 @@ async function defaultCommand(query) {
|
|
|
4239
4275
|
rl.close();
|
|
4240
4276
|
if (answer.trim().toLowerCase() !== "n") {
|
|
4241
4277
|
const s = sessions[indices[0] - 1];
|
|
4242
|
-
await resumeInSession(s.sessionId, s.cwd);
|
|
4278
|
+
await resumeInSession(s.sessionId, s.cwd, s.firstMsg.replace(/\n/g, " ").slice(0, 50));
|
|
4243
4279
|
}
|
|
4244
4280
|
} else {
|
|
4245
4281
|
const answer = await new Promise((resolve) => {
|
|
@@ -4249,7 +4285,7 @@ async function defaultCommand(query) {
|
|
|
4249
4285
|
const pick = parseInt(answer, 10);
|
|
4250
4286
|
if (pick >= 1 && pick <= indices.length) {
|
|
4251
4287
|
const s = sessions[indices[pick - 1] - 1];
|
|
4252
|
-
await resumeInSession(s.sessionId, s.cwd);
|
|
4288
|
+
await resumeInSession(s.sessionId, s.cwd, s.firstMsg.replace(/\n/g, " ").slice(0, 50));
|
|
4253
4289
|
}
|
|
4254
4290
|
}
|
|
4255
4291
|
}
|
|
@@ -4266,26 +4302,41 @@ async function newCommand(description, force) {
|
|
|
4266
4302
|
|
|
4267
4303
|
// src/commands/ps.ts
|
|
4268
4304
|
async function psCommand() {
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
const
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
const
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
await
|
|
4305
|
+
let cursorPos = 0;
|
|
4306
|
+
while (true) {
|
|
4307
|
+
const sessions = await listActiveSessions();
|
|
4308
|
+
if (!sessions.length) {
|
|
4309
|
+
console.log("No active multiplexer sessions.");
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
const meta = getSessionsMeta();
|
|
4313
|
+
sessions.sort((a, b) => {
|
|
4314
|
+
const timeA = meta[a.name]?.createdAt || "";
|
|
4315
|
+
const timeB = meta[b.name]?.createdAt || "";
|
|
4316
|
+
return timeB.localeCompare(timeA);
|
|
4317
|
+
});
|
|
4318
|
+
const items = sessions.map((s, i) => {
|
|
4319
|
+
const num = String(i + 1).padStart(3);
|
|
4320
|
+
const desc = meta[s.name]?.description || "";
|
|
4321
|
+
const label = desc ? `${num} ${s.name.padEnd(28)} ${s.created.padEnd(12)} ${desc}` : `${num} ${s.name.padEnd(28)} ${s.created}`;
|
|
4322
|
+
return { label, value: i };
|
|
4323
|
+
});
|
|
4324
|
+
const result = await interactiveSelect(
|
|
4325
|
+
items,
|
|
4326
|
+
"Up/Down navigate, Enter attach, dd kill, Esc cancel",
|
|
4327
|
+
{ deleteKey: true, initialCursor: cursorPos }
|
|
4328
|
+
);
|
|
4329
|
+
if (result.action === "cancel") return;
|
|
4330
|
+
if (result.action === "delete") {
|
|
4331
|
+
const s = sessions[result.value];
|
|
4332
|
+
cursorPos = result.value;
|
|
4333
|
+
await killSession(s.name);
|
|
4334
|
+
continue;
|
|
4335
|
+
}
|
|
4336
|
+
if (result.action === "select") {
|
|
4337
|
+
await attachToSession(sessions[result.value].name);
|
|
4338
|
+
return;
|
|
4339
|
+
}
|
|
4289
4340
|
}
|
|
4290
4341
|
}
|
|
4291
4342
|
|
|
@@ -4305,7 +4356,7 @@ async function resumeCommand(sessionId) {
|
|
|
4305
4356
|
const sessions = loadSessions();
|
|
4306
4357
|
const match = sessions.find((s) => s.sessionId === sessionId);
|
|
4307
4358
|
if (match) {
|
|
4308
|
-
await resumeInSession(match.sessionId, match.cwd);
|
|
4359
|
+
await resumeInSession(match.sessionId, match.cwd, match.firstMsg.replace(/\n/g, " ").slice(0, 50));
|
|
4309
4360
|
} else {
|
|
4310
4361
|
console.error(`Session not found: ${sessionId}`);
|
|
4311
4362
|
console.error("Try `ch list` to see available sessions.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@halooojustin/cch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Claude Code History — AI-powered conversation history management with Zellij/tmux session support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
"cli"
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/halooojustin/cch"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/halooojustin/cch#readme",
|
|
26
31
|
"files": [
|
|
27
32
|
"dist/",
|
|
28
33
|
"bin/",
|