@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 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 上次帮我调试 iOS 的那个对话
47
- ch the one where I was deploying shrimp
48
- ch that donut wallet refactor
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-infist-fix-login-bug`), Chinese descriptions use a hash fallback (e.g. `ch-infist-a1b2c3`) since Zellij session names don't support CJK
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 donut 钱包重构的那个
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-infist-fix-login-bug`),中文描述使用哈希缩写(如 `ch-infist-a1b2c3`),因为 Zellij 会话名不支持 CJK 字符
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: `resumed: ${sessionId.slice(0, 8)}`,
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 inputDisplay = inputBuf ? ` > ${inputBuf}_` : "";
4013
- lines.push(truncate(`${DIM}${hint}${RESET}${inputDisplay}`, cols));
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
- function cleanup() {
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 selected = await interactiveSelect(items);
4113
- if (selected >= 0) {
4114
- const s = sessions[selected];
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
- const sessions = await listActiveSessions();
4270
- if (!sessions.length) {
4271
- console.log("No active multiplexer sessions.");
4272
- return;
4273
- }
4274
- const meta = getSessionsMeta();
4275
- sessions.sort((a, b) => {
4276
- const timeA = meta[a.name]?.createdAt || "";
4277
- const timeB = meta[b.name]?.createdAt || "";
4278
- return timeB.localeCompare(timeA);
4279
- });
4280
- const items = sessions.map((s, i) => {
4281
- const num = String(i + 1).padStart(3);
4282
- const desc = meta[s.name]?.description || "";
4283
- const label = desc ? `${num} ${s.name.padEnd(28)} ${s.created.padEnd(12)} ${desc}` : `${num} ${s.name.padEnd(28)} ${s.created}`;
4284
- return { label, value: i };
4285
- });
4286
- const selected = await interactiveSelect(items);
4287
- if (selected >= 0) {
4288
- await attachToSession(sessions[selected].name);
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.1.1",
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/",