@cocorograph/hub-agent 0.4.1
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/LICENSE +5 -0
- package/README.md +108 -0
- package/bin/hub-agent.mjs +201 -0
- package/package.json +56 -0
- package/plugins/10-tailscale-remote/README.md +71 -0
- package/plugins/10-tailscale-remote/config.example.json +6 -0
- package/plugins/10-tailscale-remote/plugin.mjs +99 -0
- package/scripts/fix-node-pty-perms.mjs +55 -0
- package/scripts/install.sh +130 -0
- package/src/config.mjs +76 -0
- package/src/enroll.mjs +75 -0
- package/src/hooks.mjs +68 -0
- package/src/main.mjs +362 -0
- package/src/plugin-install.mjs +111 -0
- package/src/plugin-loader.mjs +105 -0
- package/src/pty-bridge.mjs +176 -0
- package/src/service-install.mjs +144 -0
- package/src/skills.mjs +142 -0
- package/src/state.mjs +125 -0
- package/src/tmux.mjs +181 -0
- package/src/usage.mjs +288 -0
- package/src/ws-client.mjs +194 -0
- package/templates/co.cocorograph.hub-agent.plist +46 -0
- package/templates/hub-agent.service +18 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# hub-agent ワンライナーインストーラ (Sprint L)
|
|
4
|
+
#
|
|
5
|
+
# Hub の `GET /api/cockpit/agents/install-script` が、このスクリプトに
|
|
6
|
+
# enrollment token + hub url を埋め込んだ personalized 版を返す。
|
|
7
|
+
#
|
|
8
|
+
# 単独使用 (token なし):
|
|
9
|
+
# curl -fsSL https://raw.githubusercontent.com/cocorograph/D00000_hub-agent/main/scripts/install.sh | bash
|
|
10
|
+
#
|
|
11
|
+
# 内容:
|
|
12
|
+
# 1. macOS なら Homebrew (なければ install)
|
|
13
|
+
# 2. tmux + node (なければ install via brew / apt)
|
|
14
|
+
# 3. npm i -g @cocorograph/hub-agent
|
|
15
|
+
# 4. HUB_AGENT_TOKEN が設定されていれば hub-agent enroll を自動実行
|
|
16
|
+
# 5. hub-agent install-service で OS サービス化
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
NODE_MIN_MAJOR=20
|
|
22
|
+
PACKAGE_NAME="@cocorograph/hub-agent"
|
|
23
|
+
|
|
24
|
+
color_step() { printf "\033[1;34m==> %s\033[0m\n" "$1"; }
|
|
25
|
+
color_ok() { printf "\033[1;32m✓ %s\033[0m\n" "$1"; }
|
|
26
|
+
color_warn() { printf "\033[1;33m! %s\033[0m\n" "$1"; }
|
|
27
|
+
color_err() { printf "\033[1;31m✗ %s\033[0m\n" "$1" >&2; }
|
|
28
|
+
|
|
29
|
+
present() { command -v "$1" >/dev/null 2>&1; }
|
|
30
|
+
|
|
31
|
+
ensure_brew() {
|
|
32
|
+
if [[ "$(uname)" != "Darwin" ]]; then return 0; fi
|
|
33
|
+
if present brew; then color_ok "brew already installed"; return 0; fi
|
|
34
|
+
color_step "Homebrew をインストール"
|
|
35
|
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
36
|
+
# Apple Silicon の brew はデフォルト PATH に入らないので追加
|
|
37
|
+
if [[ -d /opt/homebrew/bin ]]; then
|
|
38
|
+
export PATH="/opt/homebrew/bin:$PATH"
|
|
39
|
+
fi
|
|
40
|
+
present brew || { color_err "brew install failed"; exit 1; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ensure_pkg() {
|
|
44
|
+
local cmd="$1"
|
|
45
|
+
local brew_pkg="$2"
|
|
46
|
+
local apt_pkg="${3:-$brew_pkg}"
|
|
47
|
+
if present "$cmd"; then color_ok "$cmd already installed"; return 0; fi
|
|
48
|
+
color_step "$cmd をインストール"
|
|
49
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
50
|
+
brew install "$brew_pkg"
|
|
51
|
+
elif present apt-get; then
|
|
52
|
+
sudo apt-get update -y
|
|
53
|
+
sudo apt-get install -y "$apt_pkg"
|
|
54
|
+
elif present dnf; then
|
|
55
|
+
sudo dnf install -y "$apt_pkg"
|
|
56
|
+
elif present pacman; then
|
|
57
|
+
sudo pacman -S --noconfirm "$apt_pkg"
|
|
58
|
+
else
|
|
59
|
+
color_err "対応するパッケージマネージャ (brew/apt/dnf/pacman) が見つかりません。$cmd を手動で install してください"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ensure_node_version() {
|
|
65
|
+
local v
|
|
66
|
+
v=$(node --version 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
|
67
|
+
if [[ -z "$v" ]]; then
|
|
68
|
+
ensure_pkg node node nodejs
|
|
69
|
+
return
|
|
70
|
+
fi
|
|
71
|
+
if (( v < NODE_MIN_MAJOR )); then
|
|
72
|
+
color_warn "node $v は古いです (>=${NODE_MIN_MAJOR} 必須)。brew で更新を試みます"
|
|
73
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
74
|
+
brew upgrade node || brew install node
|
|
75
|
+
else
|
|
76
|
+
color_err "Node ${NODE_MIN_MAJOR}+ を手動で install してください (nvm 推奨)"
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
else
|
|
80
|
+
color_ok "node $v"
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ensure_global_install() {
|
|
85
|
+
if present hub-agent; then
|
|
86
|
+
local cur
|
|
87
|
+
cur=$(hub-agent --version 2>/dev/null || echo "unknown")
|
|
88
|
+
color_step "hub-agent (現在 $cur) を最新版にアップデート"
|
|
89
|
+
npm install -g "$PACKAGE_NAME"
|
|
90
|
+
else
|
|
91
|
+
color_step "$PACKAGE_NAME を install"
|
|
92
|
+
npm install -g "$PACKAGE_NAME"
|
|
93
|
+
fi
|
|
94
|
+
color_ok "hub-agent $(hub-agent --version)"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
do_enroll() {
|
|
98
|
+
if [[ -z "${HUB_AGENT_TOKEN:-}" ]]; then
|
|
99
|
+
color_warn "HUB_AGENT_TOKEN が未設定。enroll は skip します"
|
|
100
|
+
color_warn " 手動で実行: hub-agent enroll <token> --hub-url ${HUB_AGENT_URL:-https://api.hub.cocorograph.com}"
|
|
101
|
+
return 0
|
|
102
|
+
fi
|
|
103
|
+
if [[ -f "$HOME/.hub/agent.json" ]]; then
|
|
104
|
+
color_warn "~/.hub/agent.json が既にあります。--force で上書きします"
|
|
105
|
+
hub-agent enroll "$HUB_AGENT_TOKEN" --hub-url "${HUB_AGENT_URL:-https://api.hub.cocorograph.com}" --force
|
|
106
|
+
else
|
|
107
|
+
hub-agent enroll "$HUB_AGENT_TOKEN" --hub-url "${HUB_AGENT_URL:-https://api.hub.cocorograph.com}"
|
|
108
|
+
fi
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
do_install_service() {
|
|
112
|
+
color_step "OS サービスとして自動起動を登録"
|
|
113
|
+
hub-agent install-service
|
|
114
|
+
color_ok "install-service 完了。ログ: ~/.hub/agent.log"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main() {
|
|
118
|
+
color_step "hub-agent ワンライナーセットアップを開始"
|
|
119
|
+
ensure_brew
|
|
120
|
+
ensure_pkg tmux tmux tmux
|
|
121
|
+
ensure_node_version
|
|
122
|
+
ensure_global_install
|
|
123
|
+
do_enroll
|
|
124
|
+
do_install_service
|
|
125
|
+
echo ""
|
|
126
|
+
color_ok "セットアップ完了。Hub UI で online 表示を確認してください"
|
|
127
|
+
echo " https://hub.cocorograph.com/user/cockpit/agents"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main "$@"
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `~/.hub/agent.json` の読み書き。
|
|
3
|
+
*
|
|
4
|
+
* 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
|
|
5
|
+
*
|
|
6
|
+
* パスは毎呼び出しで `os.homedir()` を解決する (テストで HOME を差し替え
|
|
7
|
+
* られるように)。`$HUB_AGENT_CONFIG_DIR` を export しておくと、その値が
|
|
8
|
+
* 優先される (mosh / 専用ユーザー用)。
|
|
9
|
+
*/
|
|
10
|
+
import { promises as fs, constants as fsConsts } from "node:fs"
|
|
11
|
+
import path from "node:path"
|
|
12
|
+
import os from "node:os"
|
|
13
|
+
|
|
14
|
+
function resolveConfigDir() {
|
|
15
|
+
if (process.env.HUB_AGENT_CONFIG_DIR) return process.env.HUB_AGENT_CONFIG_DIR
|
|
16
|
+
return path.join(os.homedir(), ".hub")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const paths = {
|
|
20
|
+
get configDir() {
|
|
21
|
+
return resolveConfigDir()
|
|
22
|
+
},
|
|
23
|
+
get configFile() {
|
|
24
|
+
return path.join(resolveConfigDir(), "agent.json")
|
|
25
|
+
},
|
|
26
|
+
get pluginsDir() {
|
|
27
|
+
return path.join(resolveConfigDir(), "plugins")
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** ~/.hub を作成する (chmod 700)。 */
|
|
32
|
+
export async function ensureConfigDir() {
|
|
33
|
+
await fs.mkdir(paths.configDir, { recursive: true, mode: 0o700 })
|
|
34
|
+
await fs.mkdir(paths.pluginsDir, { recursive: true, mode: 0o700 })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 設定が存在するかどうか。 */
|
|
38
|
+
export async function hasConfig() {
|
|
39
|
+
try {
|
|
40
|
+
await fs.access(paths.configFile, fsConsts.R_OK)
|
|
41
|
+
return true
|
|
42
|
+
} catch {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 設定を読み込む。無い場合は null を返す。 */
|
|
48
|
+
export async function readConfig() {
|
|
49
|
+
try {
|
|
50
|
+
const raw = await fs.readFile(paths.configFile, "utf-8")
|
|
51
|
+
return JSON.parse(raw)
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (err.code === "ENOENT") return null
|
|
54
|
+
throw err
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 設定を保存する (chmod 600)。
|
|
60
|
+
*
|
|
61
|
+
* @param {object} config - `{ agent_id, agent_token, hub_url }`
|
|
62
|
+
*/
|
|
63
|
+
export async function writeConfig(config) {
|
|
64
|
+
await ensureConfigDir()
|
|
65
|
+
const payload = JSON.stringify(config, null, 2)
|
|
66
|
+
await fs.writeFile(paths.configFile, payload, { mode: 0o600 })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 設定をクリアする (agent unenroll や revoke 後)。 */
|
|
70
|
+
export async function clearConfig() {
|
|
71
|
+
try {
|
|
72
|
+
await fs.unlink(paths.configFile)
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err.code !== "ENOENT") throw err
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/enroll.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hub-agent enroll <enrollment_token>` の処理。
|
|
3
|
+
*
|
|
4
|
+
* Hub の POST /api/cockpit/agents/enroll-complete/ に hostname と token を
|
|
5
|
+
* 送って agent_id + agent_token を取得し、~/.hub/agent.json に保存する。
|
|
6
|
+
*
|
|
7
|
+
* 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
|
|
8
|
+
*/
|
|
9
|
+
import os from "node:os"
|
|
10
|
+
|
|
11
|
+
import { hasConfig, writeConfig } from "./config.mjs"
|
|
12
|
+
|
|
13
|
+
const DEFAULT_HUB_URL = process.env.HUB_URL || "https://hub.cocorograph.com"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} enrollmentToken
|
|
17
|
+
* @param {{ hubUrl?: string, hostname?: string, version?: string, force?: boolean, fetchImpl?: typeof fetch }} [opts]
|
|
18
|
+
*/
|
|
19
|
+
export async function enroll(enrollmentToken, opts = {}) {
|
|
20
|
+
if (!enrollmentToken) {
|
|
21
|
+
throw new Error("enrollment_token が指定されていません。")
|
|
22
|
+
}
|
|
23
|
+
if (!opts.force && (await hasConfig())) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"既に enroll 済みです。再 enroll するには --force を付けてください (旧 agent は Hub 側で revoke してから実行)。"
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
const hubUrl = (opts.hubUrl || DEFAULT_HUB_URL).replace(/\/+$/, "")
|
|
29
|
+
const hostname = opts.hostname || os.hostname()
|
|
30
|
+
const version = opts.version || "0.1.0"
|
|
31
|
+
|
|
32
|
+
const fetchImpl = opts.fetchImpl || globalThis.fetch
|
|
33
|
+
if (typeof fetchImpl !== "function") {
|
|
34
|
+
throw new Error("global fetch がありません。Node 20+ で実行してください。")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const res = await fetchImpl(`${hubUrl}/api/cockpit/agents/enroll-complete/`, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "content-type": "application/json" },
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
enrollment_token: enrollmentToken,
|
|
42
|
+
hostname,
|
|
43
|
+
version,
|
|
44
|
+
}),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const data = await safeJson(res)
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const detail = data?.detail || JSON.stringify(data) || `HTTP ${res.status}`
|
|
50
|
+
throw new Error(`enroll failed: ${detail}`)
|
|
51
|
+
}
|
|
52
|
+
if (!data?.agent_id || !data?.agent_token) {
|
|
53
|
+
throw new Error(`enroll response 不正: ${JSON.stringify(data)}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await writeConfig({
|
|
57
|
+
agent_id: data.agent_id,
|
|
58
|
+
agent_token: data.agent_token,
|
|
59
|
+
hub_url: hubUrl,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
agent_id: data.agent_id,
|
|
64
|
+
is_primary: !!data.is_primary,
|
|
65
|
+
hub_url: hubUrl,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function safeJson(res) {
|
|
70
|
+
try {
|
|
71
|
+
return await res.json()
|
|
72
|
+
} catch {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/hooks.mjs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin hook 名と signatures の型定義 (JSDoc)。
|
|
3
|
+
*
|
|
4
|
+
* 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
|
|
5
|
+
* の「プラグイン機構 > Hook 一覧」を参照。
|
|
6
|
+
*
|
|
7
|
+
* すべての hook は async で、ハンドラ毎に optional。最初に non-null を
|
|
8
|
+
* 返した plugin が採用される (`plugin-loader.mjs` 側で chain 実行)。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} HookContext
|
|
13
|
+
* @property {import('pino').Logger} logger
|
|
14
|
+
* @property {Object} config - 全体設定 (~/.hub/agent.json)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const HOOK_NAMES = Object.freeze([
|
|
18
|
+
"onAgentStart",
|
|
19
|
+
"onAgentStop",
|
|
20
|
+
"interceptPtySpawn",
|
|
21
|
+
"interceptTmuxExec",
|
|
22
|
+
"interceptSessionCreate",
|
|
23
|
+
"interceptSessionKill",
|
|
24
|
+
"filterSessions",
|
|
25
|
+
"transformChatInput",
|
|
26
|
+
"transformStatusDetection",
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} PtySpawnArgs
|
|
31
|
+
* @property {string} sessionName
|
|
32
|
+
* @property {number} cols
|
|
33
|
+
* @property {number} rows
|
|
34
|
+
* @property {Record<string, string>} env
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} PtySpawnDecision
|
|
39
|
+
* @property {string} command
|
|
40
|
+
* @property {string[]} args
|
|
41
|
+
* @property {Record<string, string>} [env]
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} TmuxExecArgs
|
|
46
|
+
* @property {string[]} args
|
|
47
|
+
* @property {Record<string, string>} env
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Object} TmuxExecDecision
|
|
52
|
+
* @property {string} command
|
|
53
|
+
* @property {string[]} args
|
|
54
|
+
* @property {Record<string, string>} [env]
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {Object} Plugin
|
|
59
|
+
* @property {string} name
|
|
60
|
+
* @property {{
|
|
61
|
+
* onAgentStart?: (ctx: HookContext) => Promise<void>,
|
|
62
|
+
* onAgentStop?: (ctx: HookContext) => Promise<void>,
|
|
63
|
+
* interceptPtySpawn?: (ctx: HookContext & PtySpawnArgs) => Promise<PtySpawnDecision | null>,
|
|
64
|
+
* interceptTmuxExec?: (ctx: HookContext & TmuxExecArgs) => Promise<TmuxExecDecision | null>,
|
|
65
|
+
* }} hooks
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
export default { HOOK_NAMES }
|