@cocorograph/hub-agent 0.5.7 → 0.5.9
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 +6 -0
- package/src/tmux.mjs +81 -3
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -319,6 +319,9 @@ async function dispatch(msg, ctx) {
|
|
|
319
319
|
try {
|
|
320
320
|
await createTmuxSession(name, cwd, {
|
|
321
321
|
claudeCmd: typeof msg.claude_cmd === "string" ? msg.claude_cmd : undefined,
|
|
322
|
+
initialPrompt:
|
|
323
|
+
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
324
|
+
logger: ctx.logger,
|
|
322
325
|
})
|
|
323
326
|
ctx.client.send({
|
|
324
327
|
type: "tmux.create_session.result",
|
|
@@ -363,6 +366,9 @@ async function dispatch(msg, ctx) {
|
|
|
363
366
|
// worktree dir で tmux session を作成 (claude_cmd は createSession の default)
|
|
364
367
|
await createTmuxSession(wtName, wtPath, {
|
|
365
368
|
claudeCmd: typeof msg.claude_cmd === "string" ? msg.claude_cmd : undefined,
|
|
369
|
+
initialPrompt:
|
|
370
|
+
typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
|
|
371
|
+
logger: ctx.logger,
|
|
366
372
|
})
|
|
367
373
|
ctx.client.send({
|
|
368
374
|
type: "worktree.create.result",
|
package/src/tmux.mjs
CHANGED
|
@@ -33,6 +33,20 @@ function sanitizeTmuxName(s) {
|
|
|
33
33
|
return s.replace(/[.:]/g, "-")
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 先頭の `~` / `~/` を `os.homedir()` に展開する。
|
|
38
|
+
*
|
|
39
|
+
* tmux は `-c` に渡された cwd をシェル展開しないため、フロントから
|
|
40
|
+
* 受け取った `~/hub/projects/...` を絶対パスに変換する必要がある。
|
|
41
|
+
* 既に絶対パス / 相対パスなら noop。
|
|
42
|
+
*/
|
|
43
|
+
export function expandTilde(p) {
|
|
44
|
+
if (typeof p !== "string" || p.length === 0) return p
|
|
45
|
+
if (p === "~") return os.homedir()
|
|
46
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2))
|
|
47
|
+
return p
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
/**
|
|
37
51
|
* git branch 名から worktree dir 名 (= tmux session 名) を導出する。
|
|
38
52
|
* 例: "feature/multi-area" -> "feature-multi-area"
|
|
@@ -70,7 +84,10 @@ async function branchExists(repoDir, branch) {
|
|
|
70
84
|
* 移植元: D00000_cockpit/webapp/lib/worktrees.ts (createWorktree)
|
|
71
85
|
*/
|
|
72
86
|
export async function createWorktreeDir(parentDir, branch) {
|
|
73
|
-
|
|
87
|
+
// parentDir に `~/...` 形式が来ても受け付けられるよう先に展開する。
|
|
88
|
+
// 展開後が絶対パスなら path.resolve は HUB_PROJECTS_BASE を無視して
|
|
89
|
+
// そのまま使う (Node の path.resolve 仕様)。
|
|
90
|
+
const repoDir = path.resolve(HUB_PROJECTS_BASE, expandTilde(parentDir))
|
|
74
91
|
if (repoDir !== HUB_PROJECTS_BASE && !repoDir.startsWith(HUB_PROJECTS_BASE + path.sep)) {
|
|
75
92
|
throw new Error("dir outside projects base")
|
|
76
93
|
}
|
|
@@ -146,6 +163,26 @@ function tmuxBin(opts = {}) {
|
|
|
146
163
|
return opts.tmuxBin || DEFAULT_TMUX_BIN
|
|
147
164
|
}
|
|
148
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
|
+
|
|
149
186
|
/**
|
|
150
187
|
* 汎用 tmux 実行。`tmux.exec` メッセージから呼び出される。
|
|
151
188
|
*
|
|
@@ -300,8 +337,25 @@ export async function killManySessions(names, opts = {}) {
|
|
|
300
337
|
* 新規 session を detached で作成して claude を起動する。
|
|
301
338
|
* - 同名 session が既にあれば 'duplicate session' throw
|
|
302
339
|
* - opts.claudeCmd で send-keys 内容を上書き可 (空文字なら claude 自動起動しない)
|
|
340
|
+
* - opts.initialPrompt を渡すと `claude ... --permission-mode auto` の直後に
|
|
341
|
+
* shell-safe 引数として埋め込まれ、claude TUI の初回プロンプトとして実行される
|
|
342
|
+
* (`/orchestrate` 等のスキル発火に利用)
|
|
303
343
|
*/
|
|
304
344
|
export async function createSession(name, cwd, opts = {}) {
|
|
345
|
+
// tmux は `-c` をシェル展開しないため、ここで `~` を絶対パスに展開する。
|
|
346
|
+
// 展開後の cwd は同名引数で以降の検証 / tmux に渡す。
|
|
347
|
+
const resolvedCwd = expandTilde(cwd)
|
|
348
|
+
// cwd 存在チェック (旧 Cockpit 単体実装と同等の防御)
|
|
349
|
+
// 無いまま tmux に渡すと pane の current_path が HOME など別ディレクトリに
|
|
350
|
+
// fallback して、SessionStart hook が project を解決できなくなる。
|
|
351
|
+
try {
|
|
352
|
+
const st = await fs.stat(resolvedCwd)
|
|
353
|
+
if (!st.isDirectory()) throw new Error("cwd not a directory")
|
|
354
|
+
} catch (err) {
|
|
355
|
+
if (err?.code === "ENOENT") throw new Error(`cwd not found: ${resolvedCwd}`)
|
|
356
|
+
if (err?.message === "cwd not a directory") throw err
|
|
357
|
+
throw new Error(`cwd stat failed: ${err?.message || String(err)}`)
|
|
358
|
+
}
|
|
305
359
|
// 既存チェック
|
|
306
360
|
try {
|
|
307
361
|
await execFileP(tmuxBin(opts), ["has-session", "-t", name])
|
|
@@ -311,8 +365,32 @@ export async function createSession(name, cwd, opts = {}) {
|
|
|
311
365
|
if (msg === "duplicate session") throw err
|
|
312
366
|
// has-session が非 0 = セッション無し
|
|
313
367
|
}
|
|
314
|
-
await execFileP(tmuxBin(opts), ["new-session", "-d", "-s", name, "-c",
|
|
315
|
-
|
|
368
|
+
await execFileP(tmuxBin(opts), ["new-session", "-d", "-s", name, "-c", resolvedCwd])
|
|
369
|
+
// 起動直後 pane の current_path を確認し、想定 cwd と異なれば WARN ログを残す
|
|
370
|
+
// (Phase 3: 任意検証。tmux 側のクオート / 権限問題で fallback した場合を検知する)
|
|
371
|
+
if (opts.logger) {
|
|
372
|
+
try {
|
|
373
|
+
const { stdout } = await execFileP(tmuxBin(opts), [
|
|
374
|
+
"display-message",
|
|
375
|
+
"-p",
|
|
376
|
+
"-t",
|
|
377
|
+
`${name}:`,
|
|
378
|
+
"-F",
|
|
379
|
+
"#{pane_current_path}",
|
|
380
|
+
])
|
|
381
|
+
const actual = (stdout || "").trim()
|
|
382
|
+
if (actual && actual !== resolvedCwd) {
|
|
383
|
+
opts.logger.warn(
|
|
384
|
+
{ session: name, requested: resolvedCwd, actual },
|
|
385
|
+
"tmux pane cwd differs from requested cwd",
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
} catch {
|
|
389
|
+
// display-message 失敗は致命的でないので飲み込む
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const claudeCmdBase = opts.claudeCmd ?? DEFAULT_CLAUDE_CMD
|
|
393
|
+
const claudeCmd = injectInitialPrompt(claudeCmdBase, opts.initialPrompt)
|
|
316
394
|
if (claudeCmd) {
|
|
317
395
|
await execFileP(tmuxBin(opts), ["send-keys", "-t", name, claudeCmd, "Enter"])
|
|
318
396
|
}
|