@cocorograph/hub-agent 0.5.8 → 0.5.10
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 +1 -1
- package/src/main.mjs +86 -2
- package/src/tmux.mjs +25 -1
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
|
|
12
12
|
*/
|
|
13
|
-
import { readFileSync } from "node:fs"
|
|
14
|
-
import { readFile } from "node:fs/promises"
|
|
13
|
+
import { readFileSync, watch as fsWatch } from "node:fs"
|
|
14
|
+
import { mkdir, readFile, readdir } from "node:fs/promises"
|
|
15
15
|
import os from "node:os"
|
|
16
16
|
import path from "node:path"
|
|
17
17
|
|
|
@@ -119,11 +119,16 @@ export async function startDaemon({ version, ptyModule } = {}) {
|
|
|
119
119
|
// 5s 周期で全 tmux session の状態を捕捉し、変化したものだけ session.state を push。
|
|
120
120
|
// browser がフォーカスしていない session でも常時更新するためのバックグラウンド職人。
|
|
121
121
|
const stateLoop = startStateLoop({ client, plugins, logger, intervalMs: 5_000 })
|
|
122
|
+
// bundle hook (cockpit_session_event_hook.py) が /tmp/cockpit_session_events/<name>.json
|
|
123
|
+
// に書き出す UserPromptSubmit / Stop の event を fs.watch で拾って WS push する。
|
|
124
|
+
// text マーカー判定 (detectStatusFromText) より精度が高い「ターン境界」の signal。
|
|
125
|
+
const sessionEventLoop = await startSessionEventWatcher({ client, logger })
|
|
122
126
|
|
|
123
127
|
const shutdown = async (signal) => {
|
|
124
128
|
logger.info({ signal }, "shutting down")
|
|
125
129
|
await runHookBroadcast(plugins, "onAgentStop", ctx)
|
|
126
130
|
stateLoop.stop()
|
|
131
|
+
sessionEventLoop?.stop?.()
|
|
127
132
|
ptyBridge.shutdown()
|
|
128
133
|
client.stop()
|
|
129
134
|
process.exit(0)
|
|
@@ -134,6 +139,81 @@ export async function startDaemon({ version, ptyModule } = {}) {
|
|
|
134
139
|
return { client, plugins, ptyBridge }
|
|
135
140
|
}
|
|
136
141
|
|
|
142
|
+
const SESSION_EVENTS_DIR =
|
|
143
|
+
process.env.COCKPIT_SESSION_EVENTS_DIR || "/tmp/cockpit_session_events"
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `/tmp/cockpit_session_events/<tmux_session>.json` を fs.watch して
|
|
147
|
+
* UserPromptSubmit / Stop の event を `session.event` 型で Hub に push する。
|
|
148
|
+
*
|
|
149
|
+
* 各 .json は `{ "event": "prompt_submit" | "stop", "at": <epoch_ms> }` 形式。
|
|
150
|
+
* - bundle 配信の `cockpit_session_event_hook.py` が claude code の hook 経由で
|
|
151
|
+
* ターン境界に書き換える
|
|
152
|
+
* - capture-pane の text 判定よりタイミングが正確で attach の影響を受けない
|
|
153
|
+
*
|
|
154
|
+
* ディレクトリが無ければ作成し、起動直後に既存ファイルを 1 回読んで初期 push。
|
|
155
|
+
* 以降は変更検知のたびに該当ファイルを読み直して push する。
|
|
156
|
+
*/
|
|
157
|
+
async function startSessionEventWatcher({ client, logger }) {
|
|
158
|
+
try {
|
|
159
|
+
await mkdir(SESSION_EVENTS_DIR, { recursive: true })
|
|
160
|
+
} catch (err) {
|
|
161
|
+
logger?.warn(
|
|
162
|
+
{ err: err.message, dir: SESSION_EVENTS_DIR },
|
|
163
|
+
"session event watcher: failed to mkdir, watcher disabled",
|
|
164
|
+
)
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const pushFile = async (filename) => {
|
|
169
|
+
if (!filename || !filename.endsWith(".json")) return
|
|
170
|
+
const sessionName = filename.slice(0, -5)
|
|
171
|
+
const full = path.join(SESSION_EVENTS_DIR, filename)
|
|
172
|
+
try {
|
|
173
|
+
const text = await readFile(full, "utf-8")
|
|
174
|
+
const data = JSON.parse(text)
|
|
175
|
+
if (!data || typeof data.event !== "string") return
|
|
176
|
+
client.send({
|
|
177
|
+
type: "session.event",
|
|
178
|
+
session_name: sessionName,
|
|
179
|
+
event: data.event,
|
|
180
|
+
at: typeof data.at === "number" ? data.at : Date.now(),
|
|
181
|
+
})
|
|
182
|
+
} catch {
|
|
183
|
+
// 一時欠如 / parse 失敗等は無視 (次の変更で読み直す)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 起動時に既存ファイルを 1 回読んで初期 push (再起動後に取りこぼし防止)
|
|
188
|
+
try {
|
|
189
|
+
const files = await readdir(SESSION_EVENTS_DIR)
|
|
190
|
+
for (const f of files) await pushFile(f)
|
|
191
|
+
} catch {
|
|
192
|
+
// 無視
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let watcher = null
|
|
196
|
+
try {
|
|
197
|
+
watcher = fsWatch(SESSION_EVENTS_DIR, { persistent: false }, (_event, filename) => {
|
|
198
|
+
if (!filename) return
|
|
199
|
+
// 名前文字列で来るので path.basename 不要
|
|
200
|
+
pushFile(filename).catch(() => {})
|
|
201
|
+
})
|
|
202
|
+
} catch (err) {
|
|
203
|
+
logger?.warn(
|
|
204
|
+
{ err: err.message, dir: SESSION_EVENTS_DIR },
|
|
205
|
+
"session event watcher: fs.watch failed, watcher disabled",
|
|
206
|
+
)
|
|
207
|
+
return null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
stop() {
|
|
212
|
+
try { watcher?.close() } catch {}
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
137
217
|
/**
|
|
138
218
|
* 全 tmux session の状態を定期 capture し、変化したものだけ Hub に push する。
|
|
139
219
|
*
|
|
@@ -319,6 +399,8 @@ async function dispatch(msg, ctx) {
|
|
|
319
399
|
try {
|
|
320
400
|
await createTmuxSession(name, cwd, {
|
|
321
401
|
claudeCmd: typeof msg.claude_cmd === "string" ? msg.claude_cmd : undefined,
|
|
402
|
+
initialPrompt:
|
|
403
|
+
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
322
404
|
logger: ctx.logger,
|
|
323
405
|
})
|
|
324
406
|
ctx.client.send({
|
|
@@ -364,6 +446,8 @@ async function dispatch(msg, ctx) {
|
|
|
364
446
|
// worktree dir で tmux session を作成 (claude_cmd は createSession の default)
|
|
365
447
|
await createTmuxSession(wtName, wtPath, {
|
|
366
448
|
claudeCmd: typeof msg.claude_cmd === "string" ? msg.claude_cmd : undefined,
|
|
449
|
+
initialPrompt:
|
|
450
|
+
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
367
451
|
logger: ctx.logger,
|
|
368
452
|
})
|
|
369
453
|
ctx.client.send({
|
package/src/tmux.mjs
CHANGED
|
@@ -163,6 +163,26 @@ function tmuxBin(opts = {}) {
|
|
|
163
163
|
return opts.tmuxBin || DEFAULT_TMUX_BIN
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/**
|
|
167
|
+
* claudeCmd の各 `claude ... --permission-mode auto` の直後に
|
|
168
|
+
* initialPrompt を shell-safe quoting で末尾引数として注入する。
|
|
169
|
+
*
|
|
170
|
+
* `claude --foo --permission-mode auto || claude --bar --permission-mode auto`
|
|
171
|
+
* のように `||` で連結された OR 構造の両側に同じ prompt を埋め込むため、
|
|
172
|
+
* グローバル置換を行う。マッチが無ければ無加工の claudeCmd を返す
|
|
173
|
+
* (ユーザー定義 claudeCmd が独自フォーマットで `--permission-mode auto` を
|
|
174
|
+
* 含まない場合は noop)。
|
|
175
|
+
*
|
|
176
|
+
* `JSON.stringify` は `"` `\` 制御文字を `\u00xx` 等で安全にエスケープし、
|
|
177
|
+
* 結果は double-quoted な shell リテラルとしてそのまま使える。
|
|
178
|
+
*/
|
|
179
|
+
export function injectInitialPrompt(claudeCmd, initialPrompt) {
|
|
180
|
+
if (!initialPrompt || typeof initialPrompt !== "string") return claudeCmd
|
|
181
|
+
if (typeof claudeCmd !== "string" || claudeCmd.length === 0) return claudeCmd
|
|
182
|
+
const quoted = JSON.stringify(initialPrompt)
|
|
183
|
+
return claudeCmd.replace(/--permission-mode auto/g, `--permission-mode auto ${quoted}`)
|
|
184
|
+
}
|
|
185
|
+
|
|
166
186
|
/**
|
|
167
187
|
* 汎用 tmux 実行。`tmux.exec` メッセージから呼び出される。
|
|
168
188
|
*
|
|
@@ -317,6 +337,9 @@ export async function killManySessions(names, opts = {}) {
|
|
|
317
337
|
* 新規 session を detached で作成して claude を起動する。
|
|
318
338
|
* - 同名 session が既にあれば 'duplicate session' throw
|
|
319
339
|
* - opts.claudeCmd で send-keys 内容を上書き可 (空文字なら claude 自動起動しない)
|
|
340
|
+
* - opts.initialPrompt を渡すと `claude ... --permission-mode auto` の直後に
|
|
341
|
+
* shell-safe 引数として埋め込まれ、claude TUI の初回プロンプトとして実行される
|
|
342
|
+
* (`/orchestrate` 等のスキル発火に利用)
|
|
320
343
|
*/
|
|
321
344
|
export async function createSession(name, cwd, opts = {}) {
|
|
322
345
|
// tmux は `-c` をシェル展開しないため、ここで `~` を絶対パスに展開する。
|
|
@@ -366,7 +389,8 @@ export async function createSession(name, cwd, opts = {}) {
|
|
|
366
389
|
// display-message 失敗は致命的でないので飲み込む
|
|
367
390
|
}
|
|
368
391
|
}
|
|
369
|
-
const
|
|
392
|
+
const claudeCmdBase = opts.claudeCmd ?? DEFAULT_CLAUDE_CMD
|
|
393
|
+
const claudeCmd = injectInitialPrompt(claudeCmdBase, opts.initialPrompt)
|
|
370
394
|
if (claudeCmd) {
|
|
371
395
|
await execFileP(tmuxBin(opts), ["send-keys", "-t", name, claudeCmd, "Enter"])
|
|
372
396
|
}
|