@geekbeer/minion 4.3.4 → 4.5.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/core/lib/claude-settings-sync.js +106 -0
- package/core/routes/variables.js +47 -5
- package/core/stores/variable-store.js +63 -0
- package/docs/api-reference.md +16 -0
- package/docs/task-guides.md +11 -0
- package/linux/routes/chat.js +32 -5
- package/linux/server.js +4 -26
- package/package.json +1 -1
- package/rules/core.md +17 -1
- package/settings/hooks/block-hq-navigation.js +114 -0
- package/win/routes/chat.js +29 -3
- package/win/server.js +4 -19
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sync bundled Claude Code settings (permissions + hooks) into
|
|
5
|
+
* <homeDir>/.claude/settings.json. Shared by the Linux and Windows servers so
|
|
6
|
+
* both platforms get identical baseline permissions and the PreToolUse hooks.
|
|
7
|
+
*
|
|
8
|
+
* User overrides live in settings.local.json which Claude merges on top of the
|
|
9
|
+
* settings.json we write here, so re-syncing on every boot is safe.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs')
|
|
13
|
+
const path = require('path')
|
|
14
|
+
|
|
15
|
+
// packages/minion/core/lib/claude-settings-sync.js -> packages/minion
|
|
16
|
+
const PACKAGE_ROOT = path.join(__dirname, '..', '..')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Copy bundled hook scripts to <claudeDir>/hooks/ and return the PreToolUse
|
|
20
|
+
* hook config block to embed in settings.json. Returns null when no hooks are
|
|
21
|
+
* bundled.
|
|
22
|
+
*
|
|
23
|
+
* Currently wires block-hq-navigation.js, which denies Playwright navigation to
|
|
24
|
+
* HQ (*.minion-agent.com) so the minion uses the API instead of scraping the
|
|
25
|
+
* dashboard.
|
|
26
|
+
*/
|
|
27
|
+
function syncHookScripts(claudeDir, log) {
|
|
28
|
+
try {
|
|
29
|
+
const bundledHooksDir = path.join(PACKAGE_ROOT, 'settings', 'hooks')
|
|
30
|
+
if (!fs.existsSync(bundledHooksDir)) return null
|
|
31
|
+
|
|
32
|
+
const targetHooksDir = path.join(claudeDir, 'hooks')
|
|
33
|
+
if (!fs.existsSync(targetHooksDir)) {
|
|
34
|
+
fs.mkdirSync(targetHooksDir, { recursive: true })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const files = fs.readdirSync(bundledHooksDir).filter((f) => f.endsWith('.js'))
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
fs.copyFileSync(
|
|
40
|
+
path.join(bundledHooksDir, file),
|
|
41
|
+
path.join(targetHooksDir, file),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const navHook = path.join(targetHooksDir, 'block-hq-navigation.js')
|
|
46
|
+
if (!fs.existsSync(navHook)) return null
|
|
47
|
+
|
|
48
|
+
log(`[Hooks] Synced ${files.length} hook script(s) to ~/.claude/hooks/`)
|
|
49
|
+
return {
|
|
50
|
+
PreToolUse: [
|
|
51
|
+
{
|
|
52
|
+
matcher: 'mcp__playwright__browser_navigate',
|
|
53
|
+
hooks: [{ type: 'command', command: `node ${navHook}` }],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
log(`[Hooks] Failed to sync hook scripts: ${err.message}`)
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sync bundled permissions + hooks to <homeDir>/.claude/settings.json.
|
|
65
|
+
* @param {string} homeDir - the minion HOME (config.HOME_DIR)
|
|
66
|
+
* @param {(msg: string) => void} [log] - logger (defaults to console.log)
|
|
67
|
+
*/
|
|
68
|
+
function syncClaudeSettings(homeDir, log = console.log) {
|
|
69
|
+
const bundledPath = path.join(PACKAGE_ROOT, 'settings', 'permissions.json')
|
|
70
|
+
const settingsDir = path.join(homeDir, '.claude')
|
|
71
|
+
const settingsPath = path.join(settingsDir, 'settings.json')
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(bundledPath)) return
|
|
75
|
+
|
|
76
|
+
const bundled = JSON.parse(fs.readFileSync(bundledPath, 'utf-8'))
|
|
77
|
+
|
|
78
|
+
let settings = {}
|
|
79
|
+
if (fs.existsSync(settingsPath)) {
|
|
80
|
+
try {
|
|
81
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
|
|
82
|
+
} catch {
|
|
83
|
+
log('[Permissions] existing settings.json invalid, overwriting')
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
settings.permissions = {
|
|
88
|
+
allow: bundled.allow || [],
|
|
89
|
+
deny: bundled.deny || [],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fs.mkdirSync(settingsDir, { recursive: true })
|
|
93
|
+
|
|
94
|
+
const hooks = syncHookScripts(settingsDir, log)
|
|
95
|
+
if (hooks) {
|
|
96
|
+
settings.hooks = hooks
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8')
|
|
100
|
+
log(`[Permissions] Synced: allow=${(bundled.allow || []).length}, deny=${(bundled.deny || []).length}`)
|
|
101
|
+
} catch (err) {
|
|
102
|
+
log(`[Permissions] Failed to sync permissions: ${err.message}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { syncClaudeSettings }
|
package/core/routes/variables.js
CHANGED
|
@@ -108,6 +108,29 @@ function variableRoutes(fastify, _opts, done) {
|
|
|
108
108
|
return { success: true, scopes: variableStore.listVariableScopes() }
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
+
// Move a variable from one workspace scope to another. Body:
|
|
112
|
+
// { from_workspace_id, to_workspace_id } — omit/empty string targets the
|
|
113
|
+
// minion-wide bucket. The destination is never overwritten on conflict.
|
|
114
|
+
fastify.post('/api/variables/:key/move', async (request, reply) => {
|
|
115
|
+
if (!verifyToken(request)) {
|
|
116
|
+
return reply.code(401).send({ error: 'Unauthorized' })
|
|
117
|
+
}
|
|
118
|
+
const { key } = request.params
|
|
119
|
+
const from = typeof request.body?.from_workspace_id === 'string' ? request.body.from_workspace_id : ''
|
|
120
|
+
const to = typeof request.body?.to_workspace_id === 'string' ? request.body.to_workspace_id : ''
|
|
121
|
+
const result = variableStore.moveVariable(from, to, key)
|
|
122
|
+
if (result.status === 'same_scope') {
|
|
123
|
+
return reply.code(400).send({ error: 'Source and destination scopes are the same.' })
|
|
124
|
+
}
|
|
125
|
+
if (result.status === 'not_found') {
|
|
126
|
+
return reply.code(404).send({ error: `Variable not found: ${key}` })
|
|
127
|
+
}
|
|
128
|
+
if (result.status === 'conflict') {
|
|
129
|
+
return reply.code(409).send({ error: `A variable with the same key already exists in the destination scope: ${key}` })
|
|
130
|
+
}
|
|
131
|
+
return { success: true, key, from_workspace_id: result.from, to_workspace_id: result.to }
|
|
132
|
+
})
|
|
133
|
+
|
|
111
134
|
// ─── Secrets (sensitive, workspace-scoped) ────────────────────────────
|
|
112
135
|
//
|
|
113
136
|
// Secrets are scoped per workspace. Pass ?workspace_id=<uuid> to target a
|
|
@@ -118,11 +141,6 @@ function variableRoutes(fastify, _opts, done) {
|
|
|
118
141
|
// Values are never returned via the API by design — only key names. Secrets
|
|
119
142
|
// never leave the minion: the HQ proxy is a pure pass-through.
|
|
120
143
|
|
|
121
|
-
function readWorkspaceId(request) {
|
|
122
|
-
const rawWs = request.query?.workspace_id
|
|
123
|
-
return (typeof rawWs === 'string') ? rawWs : ''
|
|
124
|
-
}
|
|
125
|
-
|
|
126
144
|
fastify.get('/api/secrets', async (request, reply) => {
|
|
127
145
|
if (!verifyToken(request)) {
|
|
128
146
|
return reply.code(401).send({ error: 'Unauthorized' })
|
|
@@ -175,6 +193,30 @@ function variableRoutes(fastify, _opts, done) {
|
|
|
175
193
|
return { success: true, scopes: variableStore.listSecretScopes() }
|
|
176
194
|
})
|
|
177
195
|
|
|
196
|
+
// Move a secret from one workspace scope to another. Body:
|
|
197
|
+
// { from_workspace_id, to_workspace_id } — omit/empty string targets the
|
|
198
|
+
// minion-wide bucket. The value is moved within the minion (it never leaves);
|
|
199
|
+
// the destination is never overwritten on conflict.
|
|
200
|
+
fastify.post('/api/secrets/:key/move', async (request, reply) => {
|
|
201
|
+
if (!verifyToken(request)) {
|
|
202
|
+
return reply.code(401).send({ error: 'Unauthorized' })
|
|
203
|
+
}
|
|
204
|
+
const { key } = request.params
|
|
205
|
+
const from = typeof request.body?.from_workspace_id === 'string' ? request.body.from_workspace_id : ''
|
|
206
|
+
const to = typeof request.body?.to_workspace_id === 'string' ? request.body.to_workspace_id : ''
|
|
207
|
+
const result = variableStore.moveSecret(from, to, key)
|
|
208
|
+
if (result.status === 'same_scope') {
|
|
209
|
+
return reply.code(400).send({ error: 'Source and destination scopes are the same.' })
|
|
210
|
+
}
|
|
211
|
+
if (result.status === 'not_found') {
|
|
212
|
+
return reply.code(404).send({ error: `Secret not found: ${key}` })
|
|
213
|
+
}
|
|
214
|
+
if (result.status === 'conflict') {
|
|
215
|
+
return reply.code(409).send({ error: `A secret with the same key already exists in the destination scope: ${key}` })
|
|
216
|
+
}
|
|
217
|
+
return { success: true, key, from_workspace_id: result.from, to_workspace_id: result.to }
|
|
218
|
+
})
|
|
219
|
+
|
|
178
220
|
done()
|
|
179
221
|
}
|
|
180
222
|
|
|
@@ -177,6 +177,36 @@ function deleteEntry(table, workspaceId, key) {
|
|
|
177
177
|
return { removed: info.changes > 0, wsId }
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Move a single entry from one workspace scope to another, atomically.
|
|
182
|
+
*
|
|
183
|
+
* Used to fix entries that were registered under the wrong workspace scope.
|
|
184
|
+
* The destination is NEVER overwritten: if the key already exists at the
|
|
185
|
+
* destination, the move is refused (`status: 'conflict'`) so no value is lost.
|
|
186
|
+
*
|
|
187
|
+
* @returns {{status: 'moved'|'not_found'|'conflict'|'same_scope', value?: string, from?: string, to?: string}}
|
|
188
|
+
*/
|
|
189
|
+
function moveEntry(table, fromWorkspaceId, toWorkspaceId, key) {
|
|
190
|
+
const from = normalizeWorkspaceId(fromWorkspaceId)
|
|
191
|
+
const to = normalizeWorkspaceId(toWorkspaceId)
|
|
192
|
+
if (from === to) return { status: 'same_scope' }
|
|
193
|
+
|
|
194
|
+
const db = getDb()
|
|
195
|
+
const src = db.prepare(`SELECT value FROM ${table} WHERE workspace_id = ? AND key = ?`).get(from, key)
|
|
196
|
+
if (!src) return { status: 'not_found' }
|
|
197
|
+
|
|
198
|
+
const dst = db.prepare(`SELECT 1 FROM ${table} WHERE workspace_id = ? AND key = ?`).get(to, key)
|
|
199
|
+
if (dst) return { status: 'conflict' }
|
|
200
|
+
|
|
201
|
+
const tx = db.transaction(() => {
|
|
202
|
+
db.prepare(`INSERT INTO ${table} (workspace_id, key, value, updated_at) VALUES (?, ?, ?, ?)`)
|
|
203
|
+
.run(to, key, src.value, Date.now())
|
|
204
|
+
db.prepare(`DELETE FROM ${table} WHERE workspace_id = ? AND key = ?`).run(from, key)
|
|
205
|
+
})
|
|
206
|
+
tx()
|
|
207
|
+
return { status: 'moved', value: src.value, from, to }
|
|
208
|
+
}
|
|
209
|
+
|
|
180
210
|
function listScopes(table) {
|
|
181
211
|
const db = getDb()
|
|
182
212
|
const rows = db.prepare(`SELECT DISTINCT workspace_id FROM ${table}`).all()
|
|
@@ -219,6 +249,19 @@ function removeVariable(workspaceId, key) {
|
|
|
219
249
|
return removed
|
|
220
250
|
}
|
|
221
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Move a variable from one workspace scope to another. The destination is not
|
|
254
|
+
* overwritten on conflict — see `moveEntry`.
|
|
255
|
+
*/
|
|
256
|
+
function moveVariable(fromWorkspaceId, toWorkspaceId, key) {
|
|
257
|
+
migrateLegacyVariablesFile()
|
|
258
|
+
const r = moveEntry('variables', fromWorkspaceId, toWorkspaceId, key)
|
|
259
|
+
if (r.status === 'moved') {
|
|
260
|
+
console.log(`[VariableStore] Moved variable ${key}: ${r.from || 'minion-wide'} -> ${r.to || 'minion-wide'}`)
|
|
261
|
+
}
|
|
262
|
+
return r
|
|
263
|
+
}
|
|
264
|
+
|
|
222
265
|
function listVariableKeys(workspaceId) {
|
|
223
266
|
migrateLegacyVariablesFile()
|
|
224
267
|
const rows = getRowsForScope('variables', workspaceId)
|
|
@@ -280,6 +323,24 @@ function removeSecret(workspaceId, key) {
|
|
|
280
323
|
return removed
|
|
281
324
|
}
|
|
282
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Move a secret from one workspace scope to another. The destination is not
|
|
328
|
+
* overwritten on conflict — see `moveEntry`.
|
|
329
|
+
*
|
|
330
|
+
* Keeps `process.env` in sync: leaving the minion-wide bucket removes the env
|
|
331
|
+
* var; entering it sets the env var. WS-scoped secrets never touch process.env.
|
|
332
|
+
*/
|
|
333
|
+
function moveSecret(fromWorkspaceId, toWorkspaceId, key) {
|
|
334
|
+
migrateLegacySecretsFile()
|
|
335
|
+
const r = moveEntry('secrets', fromWorkspaceId, toWorkspaceId, key)
|
|
336
|
+
if (r.status === 'moved') {
|
|
337
|
+
if (r.from === '') delete process.env[key]
|
|
338
|
+
if (r.to === '') process.env[key] = String(r.value ?? '')
|
|
339
|
+
console.log(`[VariableStore] Moved secret ${key}: ${r.from || 'minion-wide'} -> ${r.to || 'minion-wide'}`)
|
|
340
|
+
}
|
|
341
|
+
return r
|
|
342
|
+
}
|
|
343
|
+
|
|
283
344
|
function listSecretScopes() {
|
|
284
345
|
migrateLegacySecretsFile()
|
|
285
346
|
return listScopes('secrets')
|
|
@@ -345,6 +406,7 @@ module.exports = {
|
|
|
345
406
|
getVariable,
|
|
346
407
|
setVariable,
|
|
347
408
|
removeVariable,
|
|
409
|
+
moveVariable,
|
|
348
410
|
listVariableKeys,
|
|
349
411
|
listVariableScopes,
|
|
350
412
|
migrateLegacyVariablesFile,
|
|
@@ -355,6 +417,7 @@ module.exports = {
|
|
|
355
417
|
listSecretKeys,
|
|
356
418
|
setSecret,
|
|
357
419
|
removeSecret,
|
|
420
|
+
moveSecret,
|
|
358
421
|
listSecretScopes,
|
|
359
422
|
migrateLegacySecretsFile,
|
|
360
423
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -932,6 +932,20 @@ Response:
|
|
|
932
932
|
|
|
933
933
|
同名キーがある場合はワークスペース別が優先される。`/api/secrets/*` エンドポイントは `?workspace_id=<uuid>` クエリパラメータでスコープを指定する。省略または空文字でミニオン全体を操作する。シークレット値はHQ DBには保存されず、HQ APIはpass-throughとして中継するのみ。
|
|
934
934
|
|
|
935
|
+
#### スコープ間の移動
|
|
936
|
+
|
|
937
|
+
誤ったワークスペーススコープに登録してしまったシークレット/変数は、削除・再登録せずに別スコープへ**移動**できる。移動はミニオン内部で1トランザクションとして実行され、シークレット値はミニオンの外に出ない。
|
|
938
|
+
|
|
939
|
+
| Method | Endpoint | Description |
|
|
940
|
+
|--------|----------|-------------|
|
|
941
|
+
| POST | `/api/variables/:key/move` | 変数を別スコープへ移動。Body: `{from_workspace_id, to_workspace_id}` |
|
|
942
|
+
| POST | `/api/secrets/:key/move` | シークレットを別スコープへ移動。Body: `{from_workspace_id, to_workspace_id}` |
|
|
943
|
+
|
|
944
|
+
- `from_workspace_id` / `to_workspace_id` は省略または空文字 `""` でミニオン全体スコープを指す。
|
|
945
|
+
- 移動先に同名キーが既に存在する場合、**上書きせず `409` を返す**(値の消失を防ぐため)。先に移動先の既存キーを削除すること。
|
|
946
|
+
- 移動元にキーが無い場合は `404`、移動元と移動先が同一スコープの場合は `400`。
|
|
947
|
+
- シークレットを移動した場合、ミニオン全体スコープから外れると `process.env` から除去され、ミニオン全体スコープに入ると `process.env` に反映される。
|
|
948
|
+
|
|
935
949
|
### Project Tasks
|
|
936
950
|
|
|
937
951
|
タスクは5段階Kanban (`backlog`/`todo`/`doing`/`review`/`done`)で管理される。親子関係は2階層まで(孫タスク禁止)。担当は **ミニオンか人間の二択**(両方は不可、後勝ち null)。
|
|
@@ -2160,6 +2174,8 @@ POST `/api/minion/workspaces/:id/notes` body:
|
|
|
2160
2174
|
|
|
2161
2175
|
PATCH body は workspace 版と共通: `{title?, content?, change_summary?}` (status は workspace 経由のみ受理)。
|
|
2162
2176
|
|
|
2177
|
+
> **ロックされたノート (423 Locked):** ユーザーが「ロック」したノート(パスワード等の保護用)は、ミニオンからの更新がすべて拒否され、`HTTP 423` (`{"code":"note_locked"}`) が返る。**ロックの解除は人間のみが HQ ダッシュボードで行える。ミニオンは解除できない。** 423 を受け取ったら、上書きを試み続けず、必要なら threads でユーザーにロック解除を依頼すること。ノート詳細(GET)では `is_locked` / `is_masked` フィールドで状態を確認できる(`is_masked` は HQ UI 上の表示マスクで、API レスポンスの本文には影響しない)。
|
|
2178
|
+
|
|
2163
2179
|
#### hq CLI ラッパー
|
|
2164
2180
|
|
|
2165
2181
|
```bash
|
package/docs/task-guides.md
CHANGED
|
@@ -41,6 +41,17 @@ curl -X POST http://localhost:8080/api/web/extract \
|
|
|
41
41
|
| 「ログインしてダッシュボード操作」 | Playwright MCP |
|
|
42
42
|
| 「フォームを送信」 | Playwright MCP |
|
|
43
43
|
| 「複数ページ巡回して全件取得」 | `/api/web/extract` をループ呼び出し (各ページに対して) |
|
|
44
|
+
| **HQ (`*.minion-agent.com`) のノート/タスク/プロジェクト** | **HQ API(Playwright禁止)** |
|
|
45
|
+
|
|
46
|
+
### HQ (`*.minion-agent.com`) のリンクは API で取得する
|
|
47
|
+
|
|
48
|
+
HQダッシュボードのページURLを Playwright MCP で開いてはならない(PreToolUse フック `block-hq-navigation.js` で拒否される)。受け取ったURL/タグは以下に変換する。チャットで参照された場合、本文はプロンプト先頭の「参照ノート」「参照チケット」ブロックに既に注入済みなので、まずそれを読む。
|
|
49
|
+
|
|
50
|
+
| 受け取った形 | 取得方法 |
|
|
51
|
+
|------|---------|
|
|
52
|
+
| `[note:UUID]` / `…/notes/:id` | `GET $HQ_URL/api/minion/workspaces/:wsId/notes/:id`(または `…/projects/:pid/notes/:id`) |
|
|
53
|
+
| `[task:UUID]` / `…/tasks/:id` | `GET $HQ_URL/api/minion/projects/:pid/tasks/:id` |
|
|
54
|
+
| `…/projects/:id` | `GET $HQ_URL/api/minion/projects/:id` |
|
|
44
55
|
|
|
45
56
|
### キャッシュの確認・破棄 (debug)
|
|
46
57
|
|
package/linux/routes/chat.js
CHANGED
|
@@ -44,7 +44,7 @@ async function chatRoutes(fastify) {
|
|
|
44
44
|
return { success: false, error: 'Unauthorized' }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const { message, session_id, context, workspace_id, referenced_tasks } = request.body || {}
|
|
47
|
+
const { message, session_id, context, workspace_id, referenced_tasks, referenced_notes } = request.body || {}
|
|
48
48
|
|
|
49
49
|
if (!message || typeof message !== 'string') {
|
|
50
50
|
reply.code(400)
|
|
@@ -54,9 +54,10 @@ async function chatRoutes(fastify) {
|
|
|
54
54
|
const workspaceId = workspace_id || null
|
|
55
55
|
|
|
56
56
|
// Build prompt — add memory context on new sessions + page context + workspace
|
|
57
|
-
// referenced_tasks
|
|
58
|
-
// so the user's chat log keeps just the [task:UUID]
|
|
59
|
-
|
|
57
|
+
// referenced_tasks / referenced_notes are injected into the prompt only (not
|
|
58
|
+
// stored in history) so the user's chat log keeps just the [task:UUID] /
|
|
59
|
+
// [note:UUID] tag (or pasted URL), not a noisy dump.
|
|
60
|
+
const prompt = await buildContextPrefix(message, context, session_id, workspaceId, referenced_tasks, referenced_notes)
|
|
60
61
|
|
|
61
62
|
// Persist the user message BEFORE invoking the LLM so that crashes,
|
|
62
63
|
// timeouts, or unparseable CLI output can't lose it. For new sessions we
|
|
@@ -267,7 +268,7 @@ ${indexed}`
|
|
|
267
268
|
* On new sessions (no session_id), injects minion memory + recent daily logs.
|
|
268
269
|
* No conversation history injection — Claude CLI handles that via --resume.
|
|
269
270
|
*/
|
|
270
|
-
async function buildContextPrefix(message, context, sessionId, workspaceId, referencedTasks) {
|
|
271
|
+
async function buildContextPrefix(message, context, sessionId, workspaceId, referencedTasks, referencedNotes) {
|
|
271
272
|
const parts = []
|
|
272
273
|
|
|
273
274
|
// Resolved [task:UUID] tags from HQ — surface ticket details so Claude can
|
|
@@ -288,6 +289,29 @@ async function buildContextPrefix(message, context, sessionId, workspaceId, refe
|
|
|
288
289
|
parts.push('')
|
|
289
290
|
}
|
|
290
291
|
|
|
292
|
+
// Resolved [note:UUID] tags / pasted note URLs from HQ — surface the note body
|
|
293
|
+
// inline so Claude answers WITHOUT opening the HQ page with Playwright. The
|
|
294
|
+
// body is a snippet; the full note is one API call away.
|
|
295
|
+
if (Array.isArray(referencedNotes) && referencedNotes.length > 0) {
|
|
296
|
+
parts.push('[参照ノート — ユーザーがメッセージ内で `[note:UUID]` 形式またはURLで参照しているHQノート。Playwrightで開かず、全文が必要なら下記APIを使うこと]')
|
|
297
|
+
const NOTE_LIMIT = 2000
|
|
298
|
+
for (const n of referencedNotes) {
|
|
299
|
+
if (!n || !n.id) continue
|
|
300
|
+
const body = String(n.content || '').trim()
|
|
301
|
+
const truncated = body.length > NOTE_LIMIT
|
|
302
|
+
const snippet = truncated ? body.slice(0, NOTE_LIMIT) : body
|
|
303
|
+
parts.push(`- [note:${n.id}] ${n.title || '(無題)'} (status: ${n.status || '?'})`)
|
|
304
|
+
if (snippet) {
|
|
305
|
+
parts.push(' ```', ...snippet.split('\n').map((l) => ` ${l}`), ' ```')
|
|
306
|
+
}
|
|
307
|
+
parts.push(
|
|
308
|
+
` 全文/更新: GET|PATCH $HQ_URL/api/minion/workspaces/${n.workspace_id}/notes/${n.id}` +
|
|
309
|
+
(truncated ? ' (本文は上記抜粋、全文はAPIで取得)' : ''),
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
parts.push('')
|
|
313
|
+
}
|
|
314
|
+
|
|
291
315
|
// Inject workspace context so Claude Code knows which workspace it's operating in
|
|
292
316
|
if (workspaceId) {
|
|
293
317
|
const workspaceStore = require('../../core/stores/workspace-store')
|
|
@@ -388,6 +412,8 @@ async function buildContextPrefix(message, context, sessionId, workspaceId, refe
|
|
|
388
412
|
'',
|
|
389
413
|
'Playwright MCP (`mcp__playwright__*`) は **ログイン・フォーム入力・複数画面の対話操作**が必要な場合のみ使用する。',
|
|
390
414
|
'単純な閲覧・要約・一覧取得用途ではMCPを使わない。',
|
|
415
|
+
'',
|
|
416
|
+
'HQ (`*.minion-agent.com`) のページURLは **Playwrightで開かずAPIで取得する**。ノートは `GET $HQ_URL/api/minion/workspaces/:wsId/notes/:id`、タスクは `GET $HQ_URL/api/minion/projects/:pid/tasks/:id`。チャットで参照されたノート/タスクの本文は上記「参照ノート」「参照チケット」ブロックに既に注入済み。',
|
|
391
417
|
''
|
|
392
418
|
)
|
|
393
419
|
}
|
|
@@ -399,6 +425,7 @@ async function buildContextPrefix(message, context, sessionId, workspaceId, refe
|
|
|
399
425
|
'成果物の保存先は以下のルールに従うこと。`/home/minion/` 直下にファイルを保存しないこと。',
|
|
400
426
|
'',
|
|
401
427
|
'- **テキスト成果物**(レポート、調査結果、要約等)→ ノートに保存: `hq note create <project_id> --title "タイトル" --content "本文"` (プロジェクト紐づけなしは `hq note create --workspace <ws_id> ...`)',
|
|
428
|
+
' - ロックされたノートの更新は `HTTP 423`(`note_locked`)で拒否される。解除は人間のみ可能なので、上書きを繰り返さずユーザーに依頼すること。',
|
|
402
429
|
'- **バイナリファイル**(PDF、画像、ZIP等)→ `~/files/` に配置(ユーザーがHQからダウンロード可能)',
|
|
403
430
|
'- **一時ファイル** → `/tmp/` に配置(作業後に削除)',
|
|
404
431
|
'',
|
package/linux/server.js
CHANGED
|
@@ -38,6 +38,7 @@ const PACKAGE_ROOT = path.join(__dirname, '..')
|
|
|
38
38
|
// Core shared modules
|
|
39
39
|
const { config, validate, isHqConfigured } = require('../core/config')
|
|
40
40
|
const { sendHeartbeat } = require('../core/api')
|
|
41
|
+
const { syncClaudeSettings } = require('../core/lib/claude-settings-sync')
|
|
41
42
|
const { version } = require('../package.json')
|
|
42
43
|
|
|
43
44
|
const workflowStore = require('../core/stores/workflow-store')
|
|
@@ -159,34 +160,11 @@ process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
|
159
160
|
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
|
-
* Sync bundled permissions into ~/.claude/settings.json.
|
|
163
|
+
* Sync bundled permissions + PreToolUse hooks into ~/.claude/settings.json.
|
|
164
|
+
* Delegates to the shared core module so Linux and Windows stay in sync.
|
|
163
165
|
*/
|
|
164
166
|
function syncPermissions() {
|
|
165
|
-
|
|
166
|
-
const settingsDir = path.join(config.HOME_DIR, '.claude')
|
|
167
|
-
const settingsPath = path.join(settingsDir, 'settings.json')
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
if (!fs.existsSync(bundledPath)) return
|
|
171
|
-
|
|
172
|
-
const bundled = JSON.parse(fs.readFileSync(bundledPath, 'utf-8'))
|
|
173
|
-
|
|
174
|
-
let settings = {}
|
|
175
|
-
if (fs.existsSync(settingsPath)) {
|
|
176
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
settings.permissions = {
|
|
180
|
-
allow: bundled.allow || [],
|
|
181
|
-
deny: bundled.deny || [],
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
fs.mkdirSync(settingsDir, { recursive: true })
|
|
185
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8')
|
|
186
|
-
console.log(`[Permissions] Synced: allow=${bundled.allow.length}, deny=${bundled.deny.length}`)
|
|
187
|
-
} catch (err) {
|
|
188
|
-
console.error(`[Permissions] Failed to sync permissions: ${err.message}`)
|
|
189
|
-
}
|
|
167
|
+
syncClaudeSettings(config.HOME_DIR, (msg) => console.log(msg))
|
|
190
168
|
}
|
|
191
169
|
|
|
192
170
|
/**
|
package/package.json
CHANGED
package/rules/core.md
CHANGED
|
@@ -47,6 +47,20 @@ Minion
|
|
|
47
47
|
- **Workspace**: ミニオンは複数のワークスペースに所属でき、スキルやプロジェクトはワークスペース単位でスコープされる。チャットセッションもワークスペース別に分離される。所属ワークスペースはハートビートで自動同期され、`hq list workspaces` で確認できる。
|
|
48
48
|
- ミニオンは複数プロジェクトに `pm`、`engineer`、`accountant` として参加できる。
|
|
49
49
|
|
|
50
|
+
## HQ リソースへのリンクは API で取得する
|
|
51
|
+
|
|
52
|
+
`*.minion-agent.com`(HQダッシュボード)のページURLを **Playwright MCP で開いてはならない**。ほぼ全リソースがAPIで取得できる。受け取ったURL/タグは以下のAPIに変換して使うこと。
|
|
53
|
+
|
|
54
|
+
| 受け取った形 | 取得方法 |
|
|
55
|
+
|------|---------|
|
|
56
|
+
| `[note:UUID]` / `…/notes/:id` | `GET $HQ_URL/api/minion/workspaces/:wsId/notes/:id`(プロジェクト紐づきなら `…/projects/:pid/notes/:id` も可) |
|
|
57
|
+
| `[task:UUID]` / `…/tasks/:id` | `GET $HQ_URL/api/minion/projects/:pid/tasks/:id` |
|
|
58
|
+
| `…/projects/:id` | `GET $HQ_URL/api/minion/projects/:id` |
|
|
59
|
+
|
|
60
|
+
- チャットでユーザーが `[note:UUID]` / `[task:UUID]` やノートURLを貼った場合、**本文はプロンプト先頭の「参照ノート」「参照チケット」ブロックに既に注入済み**。まずそれを読むこと。全文・最新版が要るときだけ上記APIを叩く。
|
|
61
|
+
- 認証なしのPlaywright閲覧はログイン画面や不完全なHTMLを掴むため結果が壊れる。HQリンクは必ずAPIで取得する。
|
|
62
|
+
- このルールは PreToolUse フックでも強制されており、`mcp__playwright__browser_navigate` で `*.minion-agent.com` を開こうとすると拒否される。
|
|
63
|
+
|
|
50
64
|
## Email
|
|
51
65
|
|
|
52
66
|
各ミニオンには専用メールアドレス `m-{MINION_ID}@minion-agent.com` が割り当てられている。
|
|
@@ -224,7 +238,7 @@ Routine 実行中は以下もtmuxセッション環境で利用可能:
|
|
|
224
238
|
- `MINION_ROUTINE_NAME` — ルーティン名
|
|
225
239
|
- `MINION_ROUTINE_WORKSPACE_ID` — ルーティンが特定ワークスペースにバインドされている場合のワークスペースUUID(未バインドなら空文字)
|
|
226
240
|
|
|
227
|
-
**変数**(ワークスペース変数・プロジェクト変数・ワークフロー変数・ミニオン変数)はスキル本文の `{{VAR_NAME}}` テンプレートとして実行時に展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述すること。展開優先順位: ワークスペース < プロジェクト < ワークフロー < ミニオン(後者が優先)。ミニオンが最終オーバーライドとなる設計で、ミニオン運用者がHQ側のデフォルトを差し替えられる経路を保証する。ミニオン変数はさらにミニオン全体スコープとワークスペース別スコープに分かれ、当該ワークスペースのコンテキストで動く実行時はWS別がミニオン全体を上書きする。`/api/variables/*` は `?workspace_id=<uuid>`
|
|
241
|
+
**変数**(ワークスペース変数・プロジェクト変数・ワークフロー変数・ミニオン変数)はスキル本文の `{{VAR_NAME}}` テンプレートとして実行時に展開される。スキル作成時にパラメータ化したい値は `{{変数名}}` で記述すること。展開優先順位: ワークスペース < プロジェクト < ワークフロー < ミニオン(後者が優先)。ミニオンが最終オーバーライドとなる設計で、ミニオン運用者がHQ側のデフォルトを差し替えられる経路を保証する。ミニオン変数はさらにミニオン全体スコープとワークスペース別スコープに分かれ、当該ワークスペースのコンテキストで動く実行時はWS別がミニオン全体を上書きする。`/api/variables/*` は `?workspace_id=<uuid>` でスコープ指定(省略時はミニオン全体)。誤ったスコープに登録してしまった変数・シークレットは `POST /api/variables/:key/move`・`POST /api/secrets/:key/move`(body: `{from_workspace_id, to_workspace_id}`)で削除・再登録せずに別スコープへ移動できる。移動先に同名キーがある場合は上書きせず `409` を返す。
|
|
228
242
|
|
|
229
243
|
DAGワークフローでは、HQ側スコープ(workspace/project/dag_workflow)の変数は **`POST /api/dag/minion/claim-node` のレスポンス `template_vars`** に乗って降ってきて、ミニオン側で minion-local 変数とマージしたうえで `POST /api/skills/fetch/:name?vars=...` の base64 JSON に詰めて HQ に渡し、HQ 側で SKILL.md の `{{VAR}}` を置換して返す。つまり HQ-scope 変数の更新は **次のノード claim から反映される**(既存のミニオン上 SKILL.md は次回 fetch で上書きされる)。DAGワークフロー単位の変数は `dag_workflow_variables` テーブルに格納され、DAGエディタの "Variables" ボタンから編集可能。
|
|
230
244
|
|
|
@@ -471,6 +485,8 @@ hq note search --workspace <workspace_id> "キーワード"
|
|
|
471
485
|
|
|
472
486
|
**削除はミニオンから実行できない**(事故防止のため人間操作限定)。不要なノートは `hq note update ... --status archived` (または PATCH の `status: "archived"`) でアーカイブすること。
|
|
473
487
|
|
|
488
|
+
**ロックされたノートは更新できない。** ユーザーがパスワード等を保護するために「ロック」したノートは、`hq note update` / PATCH が `HTTP 423`(`{"code":"note_locked"}`)で拒否される。**ロック解除は人間のみ**が HQ ダッシュボードで行えるため、ミニオンは解除できない。423 を受けたら上書きを繰り返さず、更新が必要な場合は threads でユーザーにロック解除を依頼すること。
|
|
489
|
+
|
|
474
490
|
### ノート内のユーザーメンション
|
|
475
491
|
|
|
476
492
|
人間ユーザーがノート編集UIで `Ctrl+I` を押すと、自分の発言を示すアバター付きチップが行頭に挿入される。マークダウン上は次の形式で永続化される:
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse hook: block Playwright navigation to HQ (*.minion-agent.com).
|
|
4
|
+
*
|
|
5
|
+
* The minion must read HQ resources (notes, tasks, projects) via the Agent/HQ
|
|
6
|
+
* API, not by scraping the dashboard with Playwright — unauthenticated browser
|
|
7
|
+
* navigation just lands on a login page or partial HTML and corrupts results.
|
|
8
|
+
* This hook is the enforcement layer behind the "HQ リソースへのリンクは API で
|
|
9
|
+
* 取得する" rule in core.md.
|
|
10
|
+
*
|
|
11
|
+
* Wired into ~/.claude/settings.json as:
|
|
12
|
+
* hooks.PreToolUse[] -> matcher "mcp__playwright__browser_navigate"
|
|
13
|
+
*
|
|
14
|
+
* Receives the tool call as JSON on stdin; denies via hookSpecificOutput when
|
|
15
|
+
* the target host matches HQ, allows everything else. Blocked attempts are
|
|
16
|
+
* appended to ~/.minion/logs/hq-nav-blocks.log for effectiveness measurement.
|
|
17
|
+
*/
|
|
18
|
+
const fs = require('fs')
|
|
19
|
+
const os = require('os')
|
|
20
|
+
const path = require('path')
|
|
21
|
+
|
|
22
|
+
function readStdin() {
|
|
23
|
+
try {
|
|
24
|
+
return fs.readFileSync(0, 'utf8')
|
|
25
|
+
} catch {
|
|
26
|
+
return ''
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Hosts considered "HQ". Overridable via HQ_NAV_BLOCK_HOSTS (comma-separated). */
|
|
31
|
+
function blockedHostSuffixes() {
|
|
32
|
+
const fromEnv = (process.env.HQ_NAV_BLOCK_HOSTS || '')
|
|
33
|
+
.split(',')
|
|
34
|
+
.map((s) => s.trim().toLowerCase())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
const suffixes = new Set(['minion-agent.com', ...fromEnv])
|
|
37
|
+
// Also include the configured HQ host, in case it lives on a custom domain.
|
|
38
|
+
if (process.env.HQ_URL) {
|
|
39
|
+
try {
|
|
40
|
+
suffixes.add(new URL(process.env.HQ_URL).hostname.toLowerCase())
|
|
41
|
+
} catch {
|
|
42
|
+
/* ignore malformed HQ_URL */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...suffixes]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isBlocked(rawUrl, suffixes) {
|
|
49
|
+
let host
|
|
50
|
+
try {
|
|
51
|
+
host = new URL(rawUrl).hostname.toLowerCase()
|
|
52
|
+
} catch {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
return suffixes.some((s) => host === s || host.endsWith(`.${s}`))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function logBlock(url) {
|
|
59
|
+
try {
|
|
60
|
+
const dir = path.join(os.homedir(), '.minion', 'logs')
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
62
|
+
fs.appendFileSync(
|
|
63
|
+
path.join(dir, 'hq-nav-blocks.log'),
|
|
64
|
+
`${new Date().toISOString()}\t${url}\n`,
|
|
65
|
+
)
|
|
66
|
+
} catch {
|
|
67
|
+
/* best-effort logging only */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function allow() {
|
|
72
|
+
process.exit(0)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function deny(url) {
|
|
76
|
+
logBlock(url)
|
|
77
|
+
const reason =
|
|
78
|
+
`Playwright で ${url} を開くことは禁止されています。HQ (*.minion-agent.com) の` +
|
|
79
|
+
'リソースはAPIで取得してください。例: ノートは `GET $HQ_URL/api/minion/workspaces/:wsId/notes/:id`、' +
|
|
80
|
+
'タスクは `GET $HQ_URL/api/minion/projects/:pid/tasks/:id`。' +
|
|
81
|
+
'チャットで参照されたノート/タスクの本文はプロンプト先頭の「参照ノート」「参照チケット」ブロックに既に注入されています。'
|
|
82
|
+
process.stdout.write(
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
hookSpecificOutput: {
|
|
85
|
+
hookEventName: 'PreToolUse',
|
|
86
|
+
permissionDecision: 'deny',
|
|
87
|
+
permissionDecisionReason: reason,
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
process.exit(0)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function main() {
|
|
95
|
+
let payload
|
|
96
|
+
try {
|
|
97
|
+
payload = JSON.parse(readStdin() || '{}')
|
|
98
|
+
} catch {
|
|
99
|
+
allow()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
const url = payload?.tool_input?.url
|
|
103
|
+
if (typeof url !== 'string' || !url) {
|
|
104
|
+
allow()
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (isBlocked(url, blockedHostSuffixes())) {
|
|
108
|
+
deny(url)
|
|
109
|
+
} else {
|
|
110
|
+
allow()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
main()
|
package/win/routes/chat.js
CHANGED
|
@@ -119,7 +119,7 @@ async function chatRoutes(fastify) {
|
|
|
119
119
|
return { success: false, error: 'Unauthorized' }
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
const { message, session_id, context, wsl_mode, workspace_id, referenced_tasks } = request.body || {}
|
|
122
|
+
const { message, session_id, context, wsl_mode, workspace_id, referenced_tasks, referenced_notes } = request.body || {}
|
|
123
123
|
if (!message || typeof message !== 'string') {
|
|
124
124
|
reply.code(400)
|
|
125
125
|
return { success: false, error: 'message is required' }
|
|
@@ -128,7 +128,7 @@ async function chatRoutes(fastify) {
|
|
|
128
128
|
const workspaceId = workspace_id || null
|
|
129
129
|
// referenced_tasks is injected into the prompt only (not stored in history)
|
|
130
130
|
// so the user's chat log keeps just the [task:UUID] tag, not a noisy dump.
|
|
131
|
-
const prompt = await buildContextPrefix(message, context, session_id, workspaceId, referenced_tasks)
|
|
131
|
+
const prompt = await buildContextPrefix(message, context, session_id, workspaceId, referenced_tasks, referenced_notes)
|
|
132
132
|
const currentSessionId = session_id || null
|
|
133
133
|
|
|
134
134
|
// Persist the user message BEFORE invoking the LLM so that crashes,
|
|
@@ -336,7 +336,7 @@ ${indexed}`
|
|
|
336
336
|
})
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
async function buildContextPrefix(message, context, sessionId, workspaceId, referencedTasks) {
|
|
339
|
+
async function buildContextPrefix(message, context, sessionId, workspaceId, referencedTasks, referencedNotes) {
|
|
340
340
|
const parts = []
|
|
341
341
|
|
|
342
342
|
// Resolved [task:UUID] tags from HQ — surface ticket details so Claude can
|
|
@@ -355,6 +355,29 @@ async function buildContextPrefix(message, context, sessionId, workspaceId, refe
|
|
|
355
355
|
parts.push('')
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// Resolved [note:UUID] tags / pasted note URLs from HQ — surface the note body
|
|
359
|
+
// inline so Claude answers WITHOUT opening the HQ page with Playwright. The
|
|
360
|
+
// body is a snippet; the full note is one API call away.
|
|
361
|
+
if (Array.isArray(referencedNotes) && referencedNotes.length > 0) {
|
|
362
|
+
parts.push('[参照ノート — ユーザーがメッセージ内で `[note:UUID]` 形式またはURLで参照しているHQノート。Playwrightで開かず、全文が必要なら下記APIを使うこと]')
|
|
363
|
+
const NOTE_LIMIT = 2000
|
|
364
|
+
for (const n of referencedNotes) {
|
|
365
|
+
if (!n || !n.id) continue
|
|
366
|
+
const body = String(n.content || '').trim()
|
|
367
|
+
const truncated = body.length > NOTE_LIMIT
|
|
368
|
+
const snippet = truncated ? body.slice(0, NOTE_LIMIT) : body
|
|
369
|
+
parts.push(`- [note:${n.id}] ${n.title || '(無題)'} (status: ${n.status || '?'})`)
|
|
370
|
+
if (snippet) {
|
|
371
|
+
parts.push(' ```', ...snippet.split('\n').map((l) => ` ${l}`), ' ```')
|
|
372
|
+
}
|
|
373
|
+
parts.push(
|
|
374
|
+
` 全文/更新: GET|PATCH $HQ_URL/api/minion/workspaces/${n.workspace_id}/notes/${n.id}` +
|
|
375
|
+
(truncated ? ' (本文は上記抜粋、全文はAPIで取得)' : ''),
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
parts.push('')
|
|
379
|
+
}
|
|
380
|
+
|
|
358
381
|
// Inject workspace context so Claude Code knows which workspace it's operating in
|
|
359
382
|
if (workspaceId) {
|
|
360
383
|
const workspaceStore = require('../../core/stores/workspace-store')
|
|
@@ -454,6 +477,8 @@ async function buildContextPrefix(message, context, sessionId, workspaceId, refe
|
|
|
454
477
|
'',
|
|
455
478
|
'Playwright MCP (`mcp__playwright__*`) は **ログイン・フォーム入力・複数画面の対話操作**が必要な場合のみ使用する。',
|
|
456
479
|
'単純な閲覧・要約・一覧取得用途ではMCPを使わない。',
|
|
480
|
+
'',
|
|
481
|
+
'HQ (`*.minion-agent.com`) のページURLは **Playwrightで開かずAPIで取得する**。ノートは `GET $HQ_URL/api/minion/workspaces/:wsId/notes/:id`、タスクは `GET $HQ_URL/api/minion/projects/:pid/tasks/:id`。チャットで参照されたノート/タスクの本文は上記「参照ノート」「参照チケット」ブロックに既に注入済み。',
|
|
457
482
|
''
|
|
458
483
|
)
|
|
459
484
|
}
|
|
@@ -465,6 +490,7 @@ async function buildContextPrefix(message, context, sessionId, workspaceId, refe
|
|
|
465
490
|
'成果物の保存先は以下のルールに従うこと。`/home/minion/` 直下にファイルを保存しないこと。',
|
|
466
491
|
'',
|
|
467
492
|
'- **テキスト成果物**(レポート、調査結果、要約等)→ ノートに保存: `hq note create <project_id> --title "タイトル" --content "本文"` (プロジェクト紐づけなしは `hq note create --workspace <ws_id> ...`)',
|
|
493
|
+
' - ロックされたノートの更新は `HTTP 423`(`note_locked`)で拒否される。解除は人間のみ可能なので、上書きを繰り返さずユーザーに依頼すること。',
|
|
468
494
|
'- **バイナリファイル**(PDF、画像、ZIP等)→ `~/files/` に配置(ユーザーがHQからダウンロード可能)',
|
|
469
495
|
'- **一時ファイル** → `/tmp/` に配置(作業後に削除)',
|
|
470
496
|
'',
|
package/win/server.js
CHANGED
|
@@ -35,6 +35,7 @@ const { getConfigWarnings } = require('../core/lib/config-warnings')
|
|
|
35
35
|
|
|
36
36
|
// Bundled skill deployment (version-gated, see core/lib/bundled-skills.js)
|
|
37
37
|
const { syncBundledSkills } = require('../core/lib/bundled-skills')
|
|
38
|
+
const { syncClaudeSettings } = require('../core/lib/claude-settings-sync')
|
|
38
39
|
const { getActiveSkillDirs } = require('../core/llm-plugins/lib/skill-dirs')
|
|
39
40
|
|
|
40
41
|
// Pull-model daemons (from core/)
|
|
@@ -126,27 +127,11 @@ process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
|
126
127
|
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
|
-
* Sync bundled permissions into ~/.claude/settings.json.
|
|
130
|
+
* Sync bundled permissions + PreToolUse hooks into ~/.claude/settings.json.
|
|
131
|
+
* Delegates to the shared core module so Linux and Windows stay in sync.
|
|
130
132
|
*/
|
|
131
133
|
function syncPermissions() {
|
|
132
|
-
|
|
133
|
-
const settingsDir = path.join(config.HOME_DIR, '.claude')
|
|
134
|
-
const settingsPath = path.join(settingsDir, 'settings.json')
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
if (!fs.existsSync(bundledPath)) return
|
|
138
|
-
const bundled = JSON.parse(fs.readFileSync(bundledPath, 'utf-8'))
|
|
139
|
-
let settings = {}
|
|
140
|
-
if (fs.existsSync(settingsPath)) {
|
|
141
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
|
|
142
|
-
}
|
|
143
|
-
settings.permissions = { allow: bundled.allow || [], deny: bundled.deny || [] }
|
|
144
|
-
fs.mkdirSync(settingsDir, { recursive: true })
|
|
145
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8')
|
|
146
|
-
console.log(`[Permissions] Synced: allow=${bundled.allow.length}, deny=${bundled.deny.length}`)
|
|
147
|
-
} catch (err) {
|
|
148
|
-
console.error(`[Permissions] Failed to sync permissions: ${err.message}`)
|
|
149
|
-
}
|
|
134
|
+
syncClaudeSettings(config.HOME_DIR, (msg) => console.log(msg))
|
|
150
135
|
}
|
|
151
136
|
|
|
152
137
|
/**
|