@cocorograph/hub-agent 0.6.32 → 0.6.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.6.32",
3
+ "version": "0.6.33",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/main.mjs CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  killManySessions,
37
37
  killSession as killTmuxSession,
38
38
  listSessions as listTmuxSessions,
39
+ listWorktreeNameHistory,
39
40
  listWorktreeStubs,
40
41
  removeWorktree as removeWorktreeDir,
41
42
  } from "./tmux.mjs"
@@ -1113,6 +1114,40 @@ async function dispatch(msg, ctx) {
1113
1114
  }
1114
1115
  return
1115
1116
  }
1117
+ case "worktree.list_history": {
1118
+ // body: { request_id, dir }
1119
+ // cockpit (Feature A) の worktree 作成ダイアログから呼ばれる。指定 workspace で
1120
+ // 過去に使った worktree 名 (~/.claude/projects のログ dir 由来) を返す。
1121
+ const dir = (msg.dir || "").trim()
1122
+ if (!dir) {
1123
+ ctx.client.send({
1124
+ type: "worktree.list_history.result",
1125
+ request_id: msg.request_id,
1126
+ ok: false,
1127
+ error: "dir required",
1128
+ names: [],
1129
+ })
1130
+ return
1131
+ }
1132
+ try {
1133
+ const names = await listWorktreeNameHistory(dir)
1134
+ ctx.client.send({
1135
+ type: "worktree.list_history.result",
1136
+ request_id: msg.request_id,
1137
+ ok: true,
1138
+ names,
1139
+ })
1140
+ } catch (err) {
1141
+ ctx.client.send({
1142
+ type: "worktree.list_history.result",
1143
+ request_id: msg.request_id,
1144
+ ok: false,
1145
+ error: err.message,
1146
+ names: [],
1147
+ })
1148
+ }
1149
+ return
1150
+ }
1116
1151
  case "worktree.remove": {
1117
1152
  // body: { request_id, name }
1118
1153
  // cockpit (PR 1719) のサイドバー削除ボタンから呼ばれる。
package/src/tmux.mjs CHANGED
@@ -267,6 +267,96 @@ export async function removeWorktree(name, opts = {}) {
267
267
  await execFileP("git", ["-C", parentRepo, "worktree", "remove", "--force", resolved])
268
268
  return { name: sanitized, wt_path: resolved }
269
269
  }
270
+
271
+ /**
272
+ * Claude Code がセッションログを格納する `~/.claude/projects/<encoded>` の
273
+ * ディレクトリ名エンコードを再現する。Claude Code は cwd の絶対パスに含まれる
274
+ * `/` `.` `_` をすべて `-` に置換したものを dir 名に使う。
275
+ *
276
+ * 例: `/Users/kaz/hub/projects/D00000_hub.cocorograph.com/.claude/worktrees`
277
+ * → `-Users-kaz-hub-projects-D00000-hub-cocorograph-com--claude-worktrees`
278
+ *
279
+ * 実データ (cockpit / cocomiru / orbit 等の worktree ログ dir) で prefix 一致を
280
+ * 検証済み (2026-06-01)。worktree 名はブランチ sanitize 済で `_` `.` を含まないため、
281
+ * encode 後も名前が無損失で復元できる。
282
+ */
283
+ function encodeClaudeProjectPath(p) {
284
+ return p.replace(/[/._]/g, "-")
285
+ }
286
+
287
+ /**
288
+ * 指定 workspace で過去に使った worktree 名の履歴を返す。
289
+ *
290
+ * cockpit の worktree 作成ダイアログで「過去に使った名前」をサジェストし、物理削除
291
+ * した worktree を同じ名前で簡単に復元できるようにするための情報源 (cockpit Feature A)。
292
+ *
293
+ * データ源は `buildWorktreeIndex` (現存 worktree の fs 走査) ではなく
294
+ * `~/.claude/projects/` の **セッションログ dir**。worktree dir を git worktree remove
295
+ * しても Claude Code のログ dir は残るため、「かつて使った名前」= 復元候補が得られる。
296
+ *
297
+ * - worktree の cwd は `<projectsBase>/<workspaceDir>/.claude/worktrees/<name>` なので
298
+ * encode 後は `<encodedWtBase>-<name>` という prefix を持つ。これで当該 workspace の
299
+ * 履歴だけを抽出する (他 repo の同名 worktree と混ざらない)。
300
+ * - いま現存する worktree (= buildWorktreeIndex に居る) は復元する必要がないため除外。
301
+ * - last_used (ログ dir の mtime) 降順でソートして返す。
302
+ *
303
+ * @param {string} workspaceDir 例: "D00000_hub.cocorograph.com" / "~/hub/projects/..."
304
+ * @returns {Promise<Array<{ name: string, last_used: number }>>}
305
+ */
306
+ export async function listWorktreeNameHistory(workspaceDir) {
307
+ const out = []
308
+ if (!workspaceDir || typeof workspaceDir !== "string") return out
309
+ const projectsBase = hubProjectsBase()
310
+ const repoDir = path.resolve(projectsBase, expandTilde(workspaceDir))
311
+ // path traversal 防止: projectsBase 配下のみ許可
312
+ if (repoDir !== projectsBase && !repoDir.startsWith(projectsBase + path.sep)) {
313
+ return out
314
+ }
315
+ const wtBase = path.join(repoDir, ".claude", "worktrees")
316
+ // dir 名は encode(`${wtBase}/${name}`) = `${encode(wtBase)}-${name}` (区切り `/` も `-`)
317
+ const prefix = encodeClaudeProjectPath(wtBase) + "-"
318
+
319
+ const claudeProjects = path.join(os.homedir(), ".claude", "projects")
320
+ let entries
321
+ try {
322
+ entries = await fs.readdir(claudeProjects, { withFileTypes: true })
323
+ } catch {
324
+ // ~/.claude/projects が無い環境では履歴なし
325
+ return out
326
+ }
327
+
328
+ // 現存 worktree (この workspace 直下のもの) は復元候補から除外する。
329
+ const existing = new Set()
330
+ try {
331
+ const idx = await buildWorktreeIndex()
332
+ for (const info of idx.values()) {
333
+ if (path.dirname(info.cwd) === wtBase) existing.add(path.basename(info.cwd))
334
+ }
335
+ } catch {
336
+ // index 構築失敗時は除外なしで続行 (best-effort)
337
+ }
338
+
339
+ // 同名が複数ログ dir に跨る場合は最新 mtime を採用する。
340
+ const seen = new Map()
341
+ for (const e of entries) {
342
+ if (!e.isDirectory() || !e.name.startsWith(prefix)) continue
343
+ const name = e.name.slice(prefix.length)
344
+ if (!name || existing.has(name)) continue
345
+ let mtime = 0
346
+ try {
347
+ const st = await fs.stat(path.join(claudeProjects, e.name))
348
+ mtime = st.mtimeMs
349
+ } catch {
350
+ // stat 失敗時は 0 のまま (末尾に並ぶ)
351
+ }
352
+ const prev = seen.get(name)
353
+ if (prev == null || mtime > prev) seen.set(name, mtime)
354
+ }
355
+ for (const [name, last_used] of seen) out.push({ name, last_used })
356
+ out.sort((a, b) => b.last_used - a.last_used)
357
+ return out
358
+ }
359
+
270
360
  /**
271
361
  * Claude CLI コマンドを組み立てる。
272
362
  *