@cocorograph/hub-agent 0.7.11 → 0.7.12
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 +12 -5
- package/src/tmux.mjs +71 -2
- package/src/tool-result-mask.mjs +38 -0
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -3022,9 +3022,12 @@ async function dispatch(msg, ctx) {
|
|
|
3022
3022
|
return
|
|
3023
3023
|
}
|
|
3024
3024
|
case "worktree.remove": {
|
|
3025
|
-
// body: { request_id, name }
|
|
3025
|
+
// body: { request_id, name, force? }
|
|
3026
3026
|
// cockpit (PR 1719) のサイドバー削除ボタンから呼ばれる。
|
|
3027
|
-
//
|
|
3027
|
+
// 2 段階削除:
|
|
3028
|
+
// - force=false (デフォルト): 未コミット/未 push を検出して dirty なら削除せず
|
|
3029
|
+
// `dirty: true, changes, unpushed_commits` を返す。フロントが「破棄して削除」確認を出す。
|
|
3030
|
+
// - force=true: 検査をスキップして `git worktree remove --force` で物理削除。
|
|
3028
3031
|
const name = (msg.name || "").trim()
|
|
3029
3032
|
if (!name) {
|
|
3030
3033
|
ctx.client.send({
|
|
@@ -3035,14 +3038,18 @@ async function dispatch(msg, ctx) {
|
|
|
3035
3038
|
})
|
|
3036
3039
|
return
|
|
3037
3040
|
}
|
|
3041
|
+
const force = msg.force === true
|
|
3038
3042
|
try {
|
|
3039
|
-
const
|
|
3043
|
+
const result = await removeWorktreeDir(name, { force })
|
|
3040
3044
|
ctx.client.send({
|
|
3041
3045
|
type: "worktree.remove.result",
|
|
3042
3046
|
request_id: msg.request_id,
|
|
3043
3047
|
ok: true,
|
|
3044
|
-
name:
|
|
3045
|
-
wt_path:
|
|
3048
|
+
name: result.name,
|
|
3049
|
+
wt_path: result.wt_path,
|
|
3050
|
+
dirty: result.dirty === true,
|
|
3051
|
+
changes: result.changes || [],
|
|
3052
|
+
unpushed_commits: result.unpushed_commits || 0,
|
|
3046
3053
|
})
|
|
3047
3054
|
} catch (err) {
|
|
3048
3055
|
ctx.client.send({
|
package/src/tmux.mjs
CHANGED
|
@@ -232,13 +232,66 @@ export async function listWorktreeStubs(liveSessionNames) {
|
|
|
232
232
|
return stubs
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* worktree 内の未コミット変更と未 push コミットを検出する(破棄前確認用)。
|
|
237
|
+
*
|
|
238
|
+
* - `git status --porcelain` で未コミット変更(追跡/未追跡/変更/削除)を列挙
|
|
239
|
+
* - `git rev-list --count @{u}..HEAD` で upstream に未 push のコミット数を取得
|
|
240
|
+
*
|
|
241
|
+
* upstream 未設定の worktree(新規ブランチで未 push)も「unpushed あり扱い」にして
|
|
242
|
+
* 誤って削除されないようにする(rev-list が失敗しても unpushed_commits=1 で警告)。
|
|
243
|
+
*
|
|
244
|
+
* @param {string} wtPath worktree の絶対パス
|
|
245
|
+
* @returns {Promise<{dirty: boolean, changes: string[], unpushed_commits: number}>}
|
|
246
|
+
*/
|
|
247
|
+
async function inspectWorktreeDirty(wtPath) {
|
|
248
|
+
/** @type {string[]} */
|
|
249
|
+
let changes = []
|
|
250
|
+
let unpushed = 0
|
|
251
|
+
try {
|
|
252
|
+
const { stdout } = await execFileP("git", ["-C", wtPath, "status", "--porcelain"])
|
|
253
|
+
changes = String(stdout || "")
|
|
254
|
+
.split("\n")
|
|
255
|
+
.map((line) => line.replace(/\r$/, ""))
|
|
256
|
+
.filter((line) => line.length > 0)
|
|
257
|
+
} catch {
|
|
258
|
+
// git status 自体が失敗するケースは滅多に無いが、保守的に「変更あり扱い」にせず空配列で続行
|
|
259
|
+
changes = []
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const { stdout } = await execFileP("git", [
|
|
263
|
+
"-C",
|
|
264
|
+
wtPath,
|
|
265
|
+
"rev-list",
|
|
266
|
+
"--count",
|
|
267
|
+
"@{u}..HEAD",
|
|
268
|
+
])
|
|
269
|
+
unpushed = parseInt(String(stdout || "0").trim(), 10) || 0
|
|
270
|
+
} catch {
|
|
271
|
+
// upstream 未設定 (新規ブランチで未 push) は rev-list が失敗する → 警告のため 1 扱い
|
|
272
|
+
unpushed = 1
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
dirty: changes.length > 0 || unpushed > 0,
|
|
276
|
+
changes,
|
|
277
|
+
unpushed_commits: unpushed,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
235
281
|
/**
|
|
236
282
|
* `git worktree remove --force <wtPath>` を実行する。事前に live tmux session が
|
|
237
283
|
* あれば kill する。cockpit `worktree.remove` ハンドラから呼ばれる。
|
|
238
284
|
*
|
|
239
285
|
* - name から path を解決するには buildWorktreeIndex で fs を走査する (path 検証も兼ねる)
|
|
240
286
|
* - HUB_PROJECTS_BASE 配下に居ない path は拒否 (path traversal 防止)
|
|
241
|
-
* -
|
|
287
|
+
* - **未コミット変更 / 未 push コミットがあれば、`opts.force=true` でない限り
|
|
288
|
+
* 削除を中止して構造化応答 `{ ok: true, dirty: true, ... }` を返す**(throw しない、
|
|
289
|
+
* 呼び出し元が「破棄して削除」確認ダイアログを出して再呼び出ししやすい設計)。
|
|
290
|
+
* - 未コミットの変更等で git remove 自体が失敗した場合はそのエラーを上位に投げる
|
|
291
|
+
*
|
|
292
|
+
* @param {string} name
|
|
293
|
+
* @param {{ force?: boolean, tmuxBin?: string }} [opts]
|
|
294
|
+
* @returns {Promise<{ name: string, wt_path: string, dirty?: boolean, changes?: string[], unpushed_commits?: number }>}
|
|
242
295
|
*/
|
|
243
296
|
export async function removeWorktree(name, opts = {}) {
|
|
244
297
|
const sanitized = sanitizeTmuxName(String(name || "").trim())
|
|
@@ -251,6 +304,22 @@ export async function removeWorktree(name, opts = {}) {
|
|
|
251
304
|
if (!resolved.startsWith(projectsBase + path.sep)) {
|
|
252
305
|
throw new Error("worktree path outside projects base")
|
|
253
306
|
}
|
|
307
|
+
|
|
308
|
+
// force=false (デフォルト): 未コミット / 未 push を検出して dirty なら削除中止
|
|
309
|
+
if (!opts.force) {
|
|
310
|
+
const inspect = await inspectWorktreeDirty(resolved)
|
|
311
|
+
if (inspect.dirty) {
|
|
312
|
+
// セッションは kill せず(誤操作からの復帰を妨げない)、構造化応答で UI に判断を返す
|
|
313
|
+
return {
|
|
314
|
+
name: sanitized,
|
|
315
|
+
wt_path: resolved,
|
|
316
|
+
dirty: true,
|
|
317
|
+
changes: inspect.changes,
|
|
318
|
+
unpushed_commits: inspect.unpushed_commits,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
254
323
|
// 走行中の session があれば kill
|
|
255
324
|
try {
|
|
256
325
|
await execFileP(tmuxBin(opts), ["kill-session", "-t", sanitized])
|
|
@@ -271,7 +340,7 @@ export async function removeWorktree(name, opts = {}) {
|
|
|
271
340
|
? resolved.slice(0, -wtSegment.length - 1)
|
|
272
341
|
: path.resolve(resolved, "..", "..", "..")
|
|
273
342
|
await execFileP("git", ["-C", parentRepo, "worktree", "remove", "--force", resolved])
|
|
274
|
-
return { name: sanitized, wt_path: resolved }
|
|
343
|
+
return { name: sanitized, wt_path: resolved, dirty: false }
|
|
275
344
|
}
|
|
276
345
|
|
|
277
346
|
/**
|
package/src/tool-result-mask.mjs
CHANGED
|
@@ -38,6 +38,18 @@ export const SECRET_PLACEHOLDER = "[REDACTED]"
|
|
|
38
38
|
export const SKILL_PROMPT_PLACEHOLDER =
|
|
39
39
|
"[get_skill_prompt の本文はマスクされています(必要なら再取得できます)]"
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* デプロイ接続情報の置換マーカー。
|
|
43
|
+
*
|
|
44
|
+
* hub_helper.deploy_creds や mcp__hub_staff__get_deploy_target が返す情報は、
|
|
45
|
+
* 「秘匿値そのものは返さない」設計(参照パス・has_* フラグのみ)になっているが、
|
|
46
|
+
* host/port/user/deploy_method 等のメタが画面・jsonl に出ると別案件サーバーへの
|
|
47
|
+
* 誤デプロイの原因になる(人間オペレーターの目視ミスを誘発)。
|
|
48
|
+
* 一律にプレースホルダで潰し、AI には「Hub から再取得できる」ことだけ知らせる方針。
|
|
49
|
+
*/
|
|
50
|
+
export const DEPLOY_CRED_PLACEHOLDER =
|
|
51
|
+
"[デプロイ接続情報はマスクされています(必要なら hub_helper.py deploy_creds で再取得できます)]"
|
|
52
|
+
|
|
41
53
|
/**
|
|
42
54
|
* 既知の秘匿情報パターン (誤検知を抑えるため、形が明確なトークンに限定する)。
|
|
43
55
|
* 各要素は global フラグ付き RegExp。replace で SECRET_PLACEHOLDER に置換する。
|
|
@@ -103,11 +115,33 @@ export function isSkillPromptTool(name) {
|
|
|
103
115
|
)
|
|
104
116
|
}
|
|
105
117
|
|
|
118
|
+
/**
|
|
119
|
+
* tool 名がデプロイ接続情報を返す MCP ツールか判定する。素名と MCP 名前空間
|
|
120
|
+
* 付き (`mcp__hub_staff__get_deploy_target` / `mcp__hub_staff__deploy_creds` 等)
|
|
121
|
+
* の両方を拾う。
|
|
122
|
+
* @param {unknown} name
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
export function isDeployCredentialTool(name) {
|
|
126
|
+
if (typeof name !== "string") return false
|
|
127
|
+
return (
|
|
128
|
+
name === "get_deploy_target" ||
|
|
129
|
+
name === "deploy_creds" ||
|
|
130
|
+
name.endsWith("__get_deploy_target") ||
|
|
131
|
+
name.endsWith("__deploy_creds")
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
106
135
|
/** tool_result の content (string | block[]) をプレースホルダ 1 本へ潰す。 */
|
|
107
136
|
function skillPlaceholderContent() {
|
|
108
137
|
return [{ type: "text", text: SKILL_PROMPT_PLACEHOLDER }]
|
|
109
138
|
}
|
|
110
139
|
|
|
140
|
+
/** tool_result の content をデプロイ接続情報プレースホルダ 1 本へ潰す。 */
|
|
141
|
+
function deployCredPlaceholderContent() {
|
|
142
|
+
return [{ type: "text", text: DEPLOY_CRED_PLACEHOLDER }]
|
|
143
|
+
}
|
|
144
|
+
|
|
111
145
|
/** content blocks 配列の text を redactSecrets で処理した新配列を返す (非破壊)。 */
|
|
112
146
|
function redactBlocks(blocks) {
|
|
113
147
|
if (!Array.isArray(blocks)) return blocks
|
|
@@ -166,6 +200,10 @@ export function maskMessageObject(obj, toolNames) {
|
|
|
166
200
|
changed = true
|
|
167
201
|
return { ...b, content: skillPlaceholderContent() }
|
|
168
202
|
}
|
|
203
|
+
if (isDeployCredentialTool(name)) {
|
|
204
|
+
changed = true
|
|
205
|
+
return { ...b, content: deployCredPlaceholderContent() }
|
|
206
|
+
}
|
|
169
207
|
// 非スキル tool_result: 中身 (string | block[]) の秘匿情報を伏せる。
|
|
170
208
|
if (typeof b.content === "string") {
|
|
171
209
|
const masked = redactSecrets(b.content)
|