@cocorograph/hub-agent 0.6.2 → 0.6.3

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.2",
3
+ "version": "0.6.3",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -127,12 +127,55 @@ export async function fetchSessionHistory({ cwd, session_id, maxLines, projectsR
127
127
  }
128
128
 
129
129
  /**
130
- * cwd 配下の全 jsonl ファイルから session 一覧を返す (Phase 2 用、現状未使用)。
131
- * 各ファイルから session_id / 最終更新時刻 / 最初の user message の冒頭を抽出。
130
+ * jsonl ファイルから最初の user メッセージ本文 (preview 用) を抽出する。
131
+ * 大きいファイルでも先頭付近で見つかるので、先頭 64KB だけ読んで探す。
132
132
  *
133
- * @param {{cwd: string, projectsRoot?: string, logger?: import('pino').Logger}} args
133
+ * @param {string} filePath
134
+ * @returns {Promise<string>} 先頭 user メッセージの冒頭 (最大 80 文字)、無ければ ""
134
135
  */
135
- export async function listSessions({ cwd, projectsRoot, logger }) {
136
+ async function extractPreview(filePath) {
137
+ let text
138
+ try {
139
+ const buf = await readFile(filePath, "utf-8")
140
+ // 先頭 64KB だけ見る (preview には十分)
141
+ text = buf.length > 65536 ? buf.slice(0, 65536) : buf
142
+ } catch {
143
+ return ""
144
+ }
145
+ for (const line of text.split("\n")) {
146
+ if (!line) continue
147
+ let obj
148
+ try {
149
+ obj = JSON.parse(line)
150
+ } catch {
151
+ continue
152
+ }
153
+ if (obj?.type === "user" && obj.message) {
154
+ const content = obj.message.content
155
+ let str = ""
156
+ if (typeof content === "string") {
157
+ str = content
158
+ } else if (Array.isArray(content)) {
159
+ const textBlock = content.find((b) => b?.type === "text" && typeof b.text === "string")
160
+ if (textBlock) str = textBlock.text
161
+ }
162
+ str = str.trim().replace(/\s+/g, " ")
163
+ if (str) return str.slice(0, 80)
164
+ }
165
+ }
166
+ return ""
167
+ }
168
+
169
+ /**
170
+ * cwd 配下の全 jsonl ファイルから session 一覧を返す。
171
+ * 各ファイルから session_id / 最終更新時刻 / サイズ / 最初の user message preview を抽出。
172
+ *
173
+ * Cockpit ChatView の「過去セッション一覧」ドロップダウン用 (tmux で作業した
174
+ * セッションも同じ cwd の project dir に並ぶため、Chat 側から読み返せる)。
175
+ *
176
+ * @param {{cwd: string, projectsRoot?: string, limit?: number, logger?: import('pino').Logger}} args
177
+ */
178
+ export async function listSessions({ cwd, projectsRoot, limit = 30, logger }) {
136
179
  if (!cwd) return { sessions: [] }
137
180
  const dir = path.join(
138
181
  projectsRoot || path.join(os.homedir(), ".claude", "projects"),
@@ -146,24 +189,25 @@ export async function listSessions({ cwd, projectsRoot, logger }) {
146
189
  logger?.warn({ err: err.message, dir }, "claude history list failed")
147
190
  return { sessions: [], error: err.message }
148
191
  }
149
- const sessions = []
192
+ const stats = []
150
193
  for (const f of files) {
151
194
  if (!f.endsWith(".jsonl")) continue
152
195
  const session_id = f.slice(0, -".jsonl".length)
153
196
  const filePath = path.join(dir, f)
154
197
  try {
155
198
  const st = await stat(filePath)
156
- sessions.push({
157
- session_id,
158
- file_path: filePath,
159
- mtime: st.mtimeMs,
160
- size_bytes: st.size,
161
- })
199
+ stats.push({ session_id, file_path: filePath, mtime: st.mtimeMs, size_bytes: st.size })
162
200
  } catch {
163
201
  // ignore individual file stat errors
164
202
  }
165
203
  }
166
- // 最新順
167
- sessions.sort((a, b) => b.mtime - a.mtime)
204
+ // 最新順 + limit 件に絞ってから preview 抽出 (全ファイル読みを避ける)
205
+ stats.sort((a, b) => b.mtime - a.mtime)
206
+ const top = stats.slice(0, limit)
207
+ const sessions = []
208
+ for (const s of top) {
209
+ const preview = await extractPreview(s.file_path)
210
+ sessions.push({ ...s, preview })
211
+ }
168
212
  return { sessions }
169
213
  }
package/src/main.mjs CHANGED
@@ -22,7 +22,7 @@ import { loadPlugins, runHookBroadcast, runHookChain } from "./plugin-loader.mjs
22
22
  import { WsClient } from "./ws-client.mjs"
23
23
  import { PtyBridge } from "./pty-bridge.mjs"
24
24
  import { ClaudeStreamBridge } from "./claude-stream-bridge.mjs"
25
- import { fetchSessionHistory } from "./claude-history.mjs"
25
+ import { fetchSessionHistory, listSessions } from "./claude-history.mjs"
26
26
  import { listAgents } from "./agents.mjs"
27
27
  import { listSkills } from "./skills.mjs"
28
28
  import { listSessionStates } from "./state.mjs"
@@ -686,6 +686,23 @@ async function dispatch(msg, ctx) {
686
686
  })
687
687
  return
688
688
  }
689
+ case "claude.sessions.request": {
690
+ // Sprint G 0.6.3: cwd 配下の全 jsonl セッション一覧を返す。
691
+ // tmux で作業したセッションも同じ project dir に並ぶため、Chat 側から
692
+ // 「過去セッション」として読み返せる。
693
+ const stream_id = msg.stream_id
694
+ const cwd = msg.cwd || ""
695
+ const limit = typeof msg.limit === "number" ? msg.limit : undefined
696
+ const result = await listSessions({ cwd, limit, logger: ctx.logger })
697
+ ctx.client.send({
698
+ type: "claude.sessions.response",
699
+ stream_id,
700
+ cwd,
701
+ sessions: result.sessions || [],
702
+ error: result.error,
703
+ })
704
+ return
705
+ }
689
706
  case "tmux.exec": {
690
707
  const args = Array.isArray(msg.args) ? msg.args : []
691
708
  const hookResult = await runHookChain(ctx.plugins, "interceptTmuxExec", {