@cocorograph/hub-agent 0.5.11 → 0.5.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/claude-md.mjs +189 -0
- package/src/tmux.mjs +77 -9
package/package.json
CHANGED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 新規 workspace ディレクトリ初期化時の CLAUDE.md 生成。
|
|
3
|
+
*
|
|
4
|
+
* ~/hub/bin/generate-claude-md (Python) と同等の仕様を Node 側に持つ。
|
|
5
|
+
* - `<dirName>/_director` slug で Hub director を引き当て、見つかれば
|
|
6
|
+
* frontmatter (client / domain / industry / service / tech_stack /
|
|
7
|
+
* google_drive) を反映した構造化 CLAUDE.md を書き出す
|
|
8
|
+
* - 認証トークン (`~/.claude/.hub_token.json`) が無い / API 失敗 / director
|
|
9
|
+
* 未登録ならプレースホルダ CLAUDE.md を書き出す
|
|
10
|
+
*
|
|
11
|
+
* CLAUDE.md が既存ならそのまま (上書きしない)。
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs/promises"
|
|
14
|
+
import os from "node:os"
|
|
15
|
+
import path from "node:path"
|
|
16
|
+
|
|
17
|
+
const DEFAULT_HUB_API = "https://api.hub.cocorograph.com"
|
|
18
|
+
const TOKEN_PATH = path.join(os.homedir(), ".claude", ".hub_token.json")
|
|
19
|
+
|
|
20
|
+
async function readJsonOrNull(p) {
|
|
21
|
+
try {
|
|
22
|
+
const text = await fs.readFile(p, "utf-8")
|
|
23
|
+
return JSON.parse(text)
|
|
24
|
+
} catch {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function loadAccessToken() {
|
|
30
|
+
const data = await readJsonOrNull(TOKEN_PATH)
|
|
31
|
+
return (data && typeof data.access === "string" && data.access) || null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function fetchDirector({ hubUrl, accessToken, dirName, fetchImpl }) {
|
|
35
|
+
const f = fetchImpl || globalThis.fetch
|
|
36
|
+
if (!f) return null
|
|
37
|
+
const slug = `${dirName}/_director`
|
|
38
|
+
const url =
|
|
39
|
+
`${hubUrl.replace(/\/+$/, "")}/api/knowledge-pages/` +
|
|
40
|
+
`?slug=${encodeURIComponent(slug)}&doc_type=director`
|
|
41
|
+
try {
|
|
42
|
+
const res = await f(url, {
|
|
43
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
44
|
+
})
|
|
45
|
+
if (!res.ok) return null
|
|
46
|
+
const payload = await res.json()
|
|
47
|
+
const items = payload?.results || []
|
|
48
|
+
return items[0] || null
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderWithDirector(dirName, director) {
|
|
55
|
+
const fm = director?.frontmatter || {}
|
|
56
|
+
const client = (fm.client || "").trim()
|
|
57
|
+
const domain = (fm.domain || "").trim()
|
|
58
|
+
const industry = (fm.industry || "").trim()
|
|
59
|
+
const service = (fm.service || "").trim()
|
|
60
|
+
const techStack = Array.isArray(fm.tech_stack) ? fm.tech_stack : []
|
|
61
|
+
const googleDrive = (fm.google_drive || "").trim()
|
|
62
|
+
|
|
63
|
+
const lines = [
|
|
64
|
+
"# CLAUDE.md",
|
|
65
|
+
"",
|
|
66
|
+
"This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.",
|
|
67
|
+
"",
|
|
68
|
+
"## プロジェクト概要",
|
|
69
|
+
"",
|
|
70
|
+
]
|
|
71
|
+
if (client) lines.push(`- **クライアント**: ${client}`)
|
|
72
|
+
if (domain) lines.push(`- **ドメイン**: ${domain}`)
|
|
73
|
+
if (industry) lines.push(`- **業種**: ${industry}`)
|
|
74
|
+
if (service) lines.push(`- **サービス**: ${service}`)
|
|
75
|
+
if (!(client || domain || industry || service)) {
|
|
76
|
+
lines.push("- (director frontmatter に該当情報なし)")
|
|
77
|
+
}
|
|
78
|
+
lines.push("")
|
|
79
|
+
|
|
80
|
+
if (techStack.length > 0) {
|
|
81
|
+
lines.push("## 技術スタック")
|
|
82
|
+
lines.push("")
|
|
83
|
+
for (const t of techStack) lines.push(`- ${t}`)
|
|
84
|
+
lines.push("")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
lines.push("## 関連")
|
|
88
|
+
lines.push("")
|
|
89
|
+
lines.push(
|
|
90
|
+
`- **Hub Director**: \`[[${dirName}/_director]]\` — 案件の進行・意思決定・直近ログ`,
|
|
91
|
+
)
|
|
92
|
+
if (googleDrive) {
|
|
93
|
+
lines.push(`- **Google Drive**: \`$GDRIVE/${googleDrive}/\` — 成果物・素材`)
|
|
94
|
+
}
|
|
95
|
+
lines.push("")
|
|
96
|
+
|
|
97
|
+
lines.push("## 初期化")
|
|
98
|
+
lines.push("")
|
|
99
|
+
lines.push(
|
|
100
|
+
"これは hub-agent が director frontmatter から自動生成した雛形です。",
|
|
101
|
+
)
|
|
102
|
+
lines.push(
|
|
103
|
+
"開発手順・SSH 接続情報・横断ナレッジへのポインタなど、より詳細な内容は",
|
|
104
|
+
)
|
|
105
|
+
lines.push(
|
|
106
|
+
"このリポジトリで `claude` を起動して `/init-claude-md` スキルを実行してください。",
|
|
107
|
+
)
|
|
108
|
+
lines.push("")
|
|
109
|
+
return lines.join("\n")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function renderPlaceholder(dirName) {
|
|
113
|
+
return [
|
|
114
|
+
"# CLAUDE.md",
|
|
115
|
+
"",
|
|
116
|
+
"This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.",
|
|
117
|
+
"",
|
|
118
|
+
"## 関連",
|
|
119
|
+
"",
|
|
120
|
+
`- **Hub Director**: \`[[${dirName}/_director]]\` — 案件の進行・意思決定・直近ログ (未作成)`,
|
|
121
|
+
"",
|
|
122
|
+
"## 初期化",
|
|
123
|
+
"",
|
|
124
|
+
"これは hub-agent の自動生成プレースホルダです。Hub director も未作成です。",
|
|
125
|
+
"詳細を生成するには、このリポジトリで `claude` を起動して `/init-claude-md` スキルを実行してください。",
|
|
126
|
+
"",
|
|
127
|
+
].join("\n")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `<targetDir>/CLAUDE.md` を必要に応じて生成する。
|
|
132
|
+
*
|
|
133
|
+
* - CLAUDE.md が既存なら何もしない (上書きしない)
|
|
134
|
+
* - Hub director が見つかれば frontmatter 連動の構造化 CLAUDE.md を生成
|
|
135
|
+
* - 見つからない / 認証なし / API 失敗ならプレースホルダを生成
|
|
136
|
+
*
|
|
137
|
+
* @param {object} args
|
|
138
|
+
* @param {string} args.targetDir 絶対パス
|
|
139
|
+
* @param {string} args.dirName director slug 解決用のディレクトリ名 (例: D00611_at-pocket.com)
|
|
140
|
+
* @param {string} [args.hubUrl] Hub API base (default: https://api.hub.cocorograph.com)
|
|
141
|
+
* @param {string} [args.accessToken] 省略時は `~/.claude/.hub_token.json` から読む
|
|
142
|
+
* @param {object} [args.logger]
|
|
143
|
+
* @param {Function} [args.fetchImpl] テスト注入用
|
|
144
|
+
* @returns {Promise<{ written: boolean, source: 'existing' | 'director' | 'placeholder' }>}
|
|
145
|
+
*/
|
|
146
|
+
export async function ensureClaudeMd({
|
|
147
|
+
targetDir,
|
|
148
|
+
dirName,
|
|
149
|
+
hubUrl,
|
|
150
|
+
accessToken,
|
|
151
|
+
logger,
|
|
152
|
+
fetchImpl,
|
|
153
|
+
} = {}) {
|
|
154
|
+
if (!targetDir) throw new Error("ensureClaudeMd requires targetDir")
|
|
155
|
+
if (!dirName) throw new Error("ensureClaudeMd requires dirName")
|
|
156
|
+
const claudeMdPath = path.join(targetDir, "CLAUDE.md")
|
|
157
|
+
try {
|
|
158
|
+
await fs.access(claudeMdPath)
|
|
159
|
+
return { written: false, source: "existing" }
|
|
160
|
+
} catch {
|
|
161
|
+
/* not found → continue */
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// accessToken は明示 null (テスト用) で「token を読まない」を選べるようにする。
|
|
165
|
+
// undefined のときだけ ~/.claude/.hub_token.json にフォールバックする。
|
|
166
|
+
const token =
|
|
167
|
+
accessToken === undefined ? await loadAccessToken() : accessToken || null
|
|
168
|
+
const url = hubUrl || DEFAULT_HUB_API
|
|
169
|
+
const director = token
|
|
170
|
+
? await fetchDirector({
|
|
171
|
+
hubUrl: url,
|
|
172
|
+
accessToken: token,
|
|
173
|
+
dirName,
|
|
174
|
+
fetchImpl,
|
|
175
|
+
})
|
|
176
|
+
: null
|
|
177
|
+
|
|
178
|
+
const body = director
|
|
179
|
+
? renderWithDirector(dirName, director)
|
|
180
|
+
: renderPlaceholder(dirName)
|
|
181
|
+
const source = director ? "director" : "placeholder"
|
|
182
|
+
|
|
183
|
+
await fs.writeFile(claudeMdPath, body, "utf-8")
|
|
184
|
+
logger?.info?.(
|
|
185
|
+
{ dirName, claudeMdPath, source },
|
|
186
|
+
"wrote CLAUDE.md for new workspace dir",
|
|
187
|
+
)
|
|
188
|
+
return { written: true, source }
|
|
189
|
+
}
|
package/src/tmux.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import os from "node:os"
|
|
|
19
19
|
import path from "node:path"
|
|
20
20
|
import { promisify } from "node:util"
|
|
21
21
|
|
|
22
|
+
import { ensureClaudeMd } from "./claude-md.mjs"
|
|
22
23
|
import { detectSessionState } from "./state.mjs"
|
|
23
24
|
import { getSessionUsages } from "./usage.mjs"
|
|
24
25
|
|
|
@@ -26,8 +27,12 @@ const execFileP = promisify(execFile)
|
|
|
26
27
|
|
|
27
28
|
const DEFAULT_TMUX_BIN = "tmux"
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// HUB_PROJECTS_BASE は実行時に解決する。テストや起動環境で
|
|
31
|
+
// `process.env.HUB_PROJECTS_BASE` を上書きできるよう、import 時に
|
|
32
|
+
// const として固定しない (tests/tmux.test.mjs が動的に差し替えるため)。
|
|
33
|
+
function hubProjectsBase() {
|
|
34
|
+
return process.env.HUB_PROJECTS_BASE || path.join(os.homedir(), "hub", "projects")
|
|
35
|
+
}
|
|
31
36
|
|
|
32
37
|
function sanitizeTmuxName(s) {
|
|
33
38
|
return s.replace(/[.:]/g, "-")
|
|
@@ -47,6 +52,24 @@ export function expandTilde(p) {
|
|
|
47
52
|
return p
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 絶対パス `p` が HUB_PROJECTS_BASE 配下かを判定する。
|
|
57
|
+
*
|
|
58
|
+
* `createSession` の cwd 自動作成判定で使う。base そのもの (`~/hub/projects`)
|
|
59
|
+
* は対象外とし、配下のサブディレクトリのみ true を返す。`path.relative` で
|
|
60
|
+
* `..` を含まないかチェックすることで `/Users/.../hub/projects-backup` のような
|
|
61
|
+
* 接頭辞一致のみの誤検知を排除する。
|
|
62
|
+
*/
|
|
63
|
+
export function isUnderHubProjectsBase(p) {
|
|
64
|
+
if (typeof p !== "string" || p.length === 0) return false
|
|
65
|
+
const abs = path.resolve(p)
|
|
66
|
+
const base = path.resolve(hubProjectsBase())
|
|
67
|
+
if (abs === base) return false
|
|
68
|
+
const rel = path.relative(base, abs)
|
|
69
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return false
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
/**
|
|
51
74
|
* git branch 名から worktree dir 名 (= tmux session 名) を導出する。
|
|
52
75
|
* 例: "feature/multi-area" -> "feature-multi-area"
|
|
@@ -87,8 +110,9 @@ export async function createWorktreeDir(parentDir, branch) {
|
|
|
87
110
|
// parentDir に `~/...` 形式が来ても受け付けられるよう先に展開する。
|
|
88
111
|
// 展開後が絶対パスなら path.resolve は HUB_PROJECTS_BASE を無視して
|
|
89
112
|
// そのまま使う (Node の path.resolve 仕様)。
|
|
90
|
-
const
|
|
91
|
-
|
|
113
|
+
const projectsBase = hubProjectsBase()
|
|
114
|
+
const repoDir = path.resolve(projectsBase, expandTilde(parentDir))
|
|
115
|
+
if (repoDir !== projectsBase && !repoDir.startsWith(projectsBase + path.sep)) {
|
|
92
116
|
throw new Error("dir outside projects base")
|
|
93
117
|
}
|
|
94
118
|
// .git の存在チェック (file or dir)
|
|
@@ -127,15 +151,16 @@ export async function createWorktreeDir(parentDir, branch) {
|
|
|
127
151
|
*/
|
|
128
152
|
async function buildWorktreeParentMap() {
|
|
129
153
|
const out = new Map()
|
|
154
|
+
const projectsBase = hubProjectsBase()
|
|
130
155
|
let projects
|
|
131
156
|
try {
|
|
132
|
-
projects = await fs.readdir(
|
|
157
|
+
projects = await fs.readdir(projectsBase, { withFileTypes: true })
|
|
133
158
|
} catch {
|
|
134
159
|
return out
|
|
135
160
|
}
|
|
136
161
|
for (const p of projects) {
|
|
137
162
|
if (!p.isDirectory() || p.name.startsWith(".")) continue
|
|
138
|
-
const wtBase = path.join(
|
|
163
|
+
const wtBase = path.join(projectsBase, p.name, ".claude", "worktrees")
|
|
139
164
|
let wts
|
|
140
165
|
try {
|
|
141
166
|
wts = await fs.readdir(wtBase, { withFileTypes: true })
|
|
@@ -348,13 +373,56 @@ export async function createSession(name, cwd, opts = {}) {
|
|
|
348
373
|
// cwd 存在チェック (旧 Cockpit 単体実装と同等の防御)
|
|
349
374
|
// 無いまま tmux に渡すと pane の current_path が HOME など別ディレクトリに
|
|
350
375
|
// fallback して、SessionStart hook が project を解決できなくなる。
|
|
376
|
+
//
|
|
377
|
+
// 例外: 解決後のパスが HUB_PROJECTS_BASE 配下 (= 案件 workspace ディレクトリ)
|
|
378
|
+
// なら、未 clone のメンバー Mac でも cockpit から起動できるよう mkdir -p で
|
|
379
|
+
// 自動生成する。任意の絶対パス (leader cwd 等) や HUB_PROJECTS_BASE 外は
|
|
380
|
+
// 引き続き `cwd not found` を返し、誤った workspace 起動を防ぐ。
|
|
351
381
|
try {
|
|
352
382
|
const st = await fs.stat(resolvedCwd)
|
|
353
383
|
if (!st.isDirectory()) throw new Error("cwd not a directory")
|
|
354
384
|
} catch (err) {
|
|
355
|
-
if (err?.code === "ENOENT")
|
|
356
|
-
|
|
357
|
-
|
|
385
|
+
if (err?.code === "ENOENT") {
|
|
386
|
+
if (isUnderHubProjectsBase(resolvedCwd)) {
|
|
387
|
+
try {
|
|
388
|
+
await fs.mkdir(resolvedCwd, { recursive: true })
|
|
389
|
+
opts.logger?.info?.(
|
|
390
|
+
{ session: name, cwd: resolvedCwd },
|
|
391
|
+
"auto-created workspace dir before tmux new-session",
|
|
392
|
+
)
|
|
393
|
+
} catch (mkdirErr) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
`cwd mkdir failed: ${mkdirErr?.message || String(mkdirErr)}`,
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
throw new Error(`cwd not found: ${resolvedCwd}`)
|
|
400
|
+
}
|
|
401
|
+
} else if (err?.message === "cwd not a directory") {
|
|
402
|
+
throw err
|
|
403
|
+
} else {
|
|
404
|
+
throw new Error(`cwd stat failed: ${err?.message || String(err)}`)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// HUB_PROJECTS_BASE 配下の workspace の場合は CLAUDE.md を必要なら生成する。
|
|
408
|
+
// Hub director があれば frontmatter 連動の構造化版、無ければ最小プレースホルダ。
|
|
409
|
+
// 既存 CLAUDE.md は触らない。失敗しても session 起動自体は止めない (warn のみ)。
|
|
410
|
+
if (isUnderHubProjectsBase(resolvedCwd)) {
|
|
411
|
+
try {
|
|
412
|
+
await ensureClaudeMd({
|
|
413
|
+
targetDir: resolvedCwd,
|
|
414
|
+
dirName: path.basename(resolvedCwd),
|
|
415
|
+
hubUrl: opts.hubUrl,
|
|
416
|
+
accessToken: opts.hubAccessToken,
|
|
417
|
+
logger: opts.logger,
|
|
418
|
+
fetchImpl: opts.fetchImpl,
|
|
419
|
+
})
|
|
420
|
+
} catch (err) {
|
|
421
|
+
opts.logger?.warn?.(
|
|
422
|
+
{ session: name, cwd: resolvedCwd, err: err?.message || String(err) },
|
|
423
|
+
"ensureClaudeMd failed (workspace dir is usable, but no CLAUDE.md)",
|
|
424
|
+
)
|
|
425
|
+
}
|
|
358
426
|
}
|
|
359
427
|
// 既存チェック
|
|
360
428
|
try {
|