@geekbeer/minion 4.4.0 → 4.7.0

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.
@@ -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
  }
@@ -251,6 +251,37 @@ Response:
251
251
  }
252
252
  ```
253
253
 
254
+ ### Chat (Detached Execution)
255
+
256
+ チャットメッセージは **デタッチ実行** される (v4.6.0〜)。`POST /api/chat` で起動した LLM プロセスは
257
+ HTTP リクエストではなく内部の **run manager** が所有するため、SSE 接続が切れても (タブを閉じる・
258
+ リロード・プロキシのアイドルタイムアウト・回線断) 処理は中断されない。接続は単なる「覗き窓」であり、
259
+ 切断時はサブスクライブを解除するだけでプロセスは kill されない。LLM を実際に停止するのは
260
+ `POST /api/chat/abort` のみ。
261
+
262
+ | Method | Endpoint | Description |
263
+ |--------|----------|-------------|
264
+ | POST | `/api/chat` | メッセージ送信 → 内部で run を起動し、その run のイベントを SSE で tail。SSE の最初のイベントは `{ "type": "run", "run_id": "..." }`(再接続用) |
265
+ | GET | `/api/chat/stream` | 進行中(または直近完了)の run に **再接続**し、`cursor` 以降のイベントから tail を再開。`run_id` または `workspace_id`(アクティブ run 自動解決) と任意の `cursor`(=最後に受け取った `seq`) を指定 |
266
+ | POST | `/api/chat/abort` | アクティブ run を明示的に kill (SIGTERM → 2秒後 SIGKILL)。Body に `workspace_id` / `run_id`(任意) |
267
+ | GET | `/api/chat/session` | アクティブセッションに加え、進行中 run があれば `active_run: { run_id, status, last_seq }` を返す(クライアントはこれを見て `/api/chat/stream` にアタッチする) |
268
+
269
+ 各 SSE イベントには単調増加の `seq` が付与され、これが再接続時の `cursor` になる。終端は必ず
270
+ `{ "type": "done", "session_id, turn_count }`(失敗時は直前に `{ "type": "error" }`)。run の
271
+ イベントログは `$DATA_DIR/chat-runs/{run_id}.ndjson` に永続化され、完了から一定時間(既定10分)後に
272
+ メモリから evict、24時間でログ/索引を prune する。
273
+
274
+ > **再起動の挙動 (Phase 1):** run manager はミニオンプロセス内で動作するため、ミニオン自体の再起動は
275
+ > 生存しない。起動時に `running` のまま残った run は `interrupted` に掃き出される (クライアントは待ち続けない)。
276
+ > 接続断への耐性が主目的であり、再起動生存は将来の拡張 (tmux バックエンド) で対応予定。
277
+
278
+ `GET /api/chat/stream` 例:
279
+ ```bash
280
+ # 直近のアクティブ run に seq 12 以降から再接続
281
+ curl -N -H "Authorization: Bearer $API_TOKEN" \
282
+ "http://localhost:8080/api/chat/stream?workspace_id=ws_abc123&cursor=12"
283
+ ```
284
+
254
285
  ### Self-Reflection Schedule (自己反省時間)
255
286
 
256
287
  The minion has a built-in daily scheduler that automatically runs end-of-day processing
@@ -714,10 +745,32 @@ Web ページの読み取り・要約・情報抽出をミニオン内のサブ
714
745
  ```json
715
746
  {
716
747
  "url": "https://example.com/article/123",
717
- "hint": "本文と著者を抽出してほしい (任意, 抽出フィールドのヒント)"
748
+ "hint": "本文と著者を抽出してほしい (任意, 抽出フィールドのヒント)",
749
+ "scroll": {
750
+ "strategy": "count",
751
+ "targetItems": 50,
752
+ "itemSelector": ".feed-item",
753
+ "maxScrolls": 20,
754
+ "maxMs": 15000,
755
+ "settleMs": 600
756
+ }
718
757
  }
719
758
  ```
720
759
 
760
+ **`scroll` (任意, v4.7.0〜):** 無限スクロール / 遅延ロードのページで「どこまでコンテンツを読み込むか」を**呼び出し側が宣言**するためのオプション。省略時はスクロールしない (従来動作)。
761
+
762
+ | フィールド | 説明 |
763
+ |-----------|------|
764
+ | `strategy` | `"count"` (件数到達まで) / `"untilStable"` (件数=増加が止まるまで) / `"fixed"` (回数固定)。未指定/不正ならスクロールしない |
765
+ | `targetItems` | `count` の目標件数。`itemSelector` が解決できる場合のみ有効 |
766
+ | `itemSelector` | 件数を数える CSS セレクタ。省略時はレシピ内の最初の `multiple: true` セレクタを流用 |
767
+ | `maxScrolls` | スクロール回数の上限 (default 10、サーバー上限 50) |
768
+ | `maxMs` | スクロールに使う最大時間 (default 15000、サーバー上限 45000) |
769
+ | `settleMs` | 1スクロールごとの描画待ち静止時間 (default 600) |
770
+ | `times` | `fixed` のスクロール回数 (default 10) |
771
+
772
+ > 値はサーバー側で上限にクランプされる。スクロール有効時はリクエスト全体のタイムアウトが 60s→120s に拡張される。
773
+
721
774
  **レスポンス (success):**
722
775
  ```json
723
776
  {
@@ -732,15 +785,24 @@ Web ページの読み取り・要約・情報抽出をミニオン内のサブ
732
785
  "title": "...",
733
786
  "content": "Markdown 本文...",
734
787
  "structured": { "title": "...", "author": "...", "publishedAt": "..." },
735
- "selectors": { "title": { "selector": "h1" }, "author": { "selector": "a[rel=author]" } }
788
+ "selectors": { "title": { "selector": "h1" }, "author": { "selector": "a[rel=author]" } },
789
+ "scrollInfo": { "scrolls": 12, "items": 50, "reachedTarget": true, "stoppedReason": "reachedTarget" }
736
790
  }
737
791
  ```
738
792
 
793
+ - `scrollInfo` は `scroll` 指定時のみ含まれる。目標未達で上限打ち切りの場合は `reachedTarget: false` と `warning` が返るので、`maxScrolls` / `maxMs` を上げて再試行できる (サイレントに打ち切らない)。
794
+
739
795
  **動作:**
740
- - 初回アクセス (cold): Playwright でレンダリング → Readability で本文抽出 → Anthropic Haiku でセレクタ生成 → SQLite (`page_recipes`) に保存 → セレクタで再抽出して返却
741
- - 2回目以降 (hot): URL 正規化・テンプレート化 → DOM フィンガープリントで保存済みレシピを照合 → セレクタで抽出のみ (LLM 呼び出しなし)
796
+ - 初回アクセス (cold): Playwright でレンダリング (**DOM が静止するまで待機**) → Readability で本文抽出 → Anthropic Haiku でセレクタ + `ready_selector` (描画完了の合図となる要素) を生成 → SQLite (`page_recipes`) に保存 → セレクタで再抽出して返却
797
+ - 2回目以降 (hot): URL 正規化・テンプレート化 → DOM フィンガープリントで保存済みレシピを照合 → **`ready_selector` の出現を待機**してからセレクタで抽出 (LLM 呼び出しなし)
742
798
  - セルフヒール: hot 実行で空結果が返ったら `fail_count++`、3回失敗で破棄して次回 cold 再生成
743
799
 
800
+ **SPA (クライアントレンダリング) への対応 (v4.7.0〜):**
801
+ - `page.goto` は `domcontentloaded` で解決するが、SPA はその時点では中身が空のシェルなので、ナビゲーション後に追加で描画完了を待つ:
802
+ 1. レシピに `ready_selector` があればその要素の出現を待つ
803
+ 2. 無ければ **DOM が `settleMs` の間ミューテーションしなくなるまで待つ** (MutationObserver、コンテンツ量に依存せず自己校正)
804
+ - これにより「SPA の描画が始まる前に空 DOM を掴んでタイムアウト/空結果になる」問題を回避する
805
+
744
806
  **URL 正規化ルール:**
745
807
  - `utm_*` `fbclid` `gclid` `ref` 等のトラッキングクエリは除去
746
808
  - `page` `p` `offset` 等のページネーション値は `:n` プレースホルダ化
@@ -932,6 +994,20 @@ Response:
932
994
 
933
995
  同名キーがある場合はワークスペース別が優先される。`/api/secrets/*` エンドポイントは `?workspace_id=<uuid>` クエリパラメータでスコープを指定する。省略または空文字でミニオン全体を操作する。シークレット値はHQ DBには保存されず、HQ APIはpass-throughとして中継するのみ。
934
996
 
997
+ #### スコープ間の移動
998
+
999
+ 誤ったワークスペーススコープに登録してしまったシークレット/変数は、削除・再登録せずに別スコープへ**移動**できる。移動はミニオン内部で1トランザクションとして実行され、シークレット値はミニオンの外に出ない。
1000
+
1001
+ | Method | Endpoint | Description |
1002
+ |--------|----------|-------------|
1003
+ | POST | `/api/variables/:key/move` | 変数を別スコープへ移動。Body: `{from_workspace_id, to_workspace_id}` |
1004
+ | POST | `/api/secrets/:key/move` | シークレットを別スコープへ移動。Body: `{from_workspace_id, to_workspace_id}` |
1005
+
1006
+ - `from_workspace_id` / `to_workspace_id` は省略または空文字 `""` でミニオン全体スコープを指す。
1007
+ - 移動先に同名キーが既に存在する場合、**上書きせず `409` を返す**(値の消失を防ぐため)。先に移動先の既存キーを削除すること。
1008
+ - 移動元にキーが無い場合は `404`、移動元と移動先が同一スコープの場合は `400`。
1009
+ - シークレットを移動した場合、ミニオン全体スコープから外れると `process.env` から除去され、ミニオン全体スコープに入ると `process.env` に反映される。
1010
+
935
1011
  ### Project Tasks
936
1012
 
937
1013
  タスクは5段階Kanban (`backlog`/`todo`/`doing`/`review`/`done`)で管理される。親子関係は2階層まで(孫タスク禁止)。担当は **ミニオンか人間の二択**(両方は不可、後勝ち null)。
@@ -2160,6 +2236,8 @@ POST `/api/minion/workspaces/:id/notes` body:
2160
2236
 
2161
2237
  PATCH body は workspace 版と共通: `{title?, content?, change_summary?}` (status は workspace 経由のみ受理)。
2162
2238
 
2239
+ > **ロックされたノート (423 Locked):** ユーザーが「ロック」したノート(パスワード等の保護用)は、ミニオンからの更新がすべて拒否され、`HTTP 423` (`{"code":"note_locked"}`) が返る。**ロックの解除は人間のみが HQ ダッシュボードで行える。ミニオンは解除できない。** 423 を受け取ったら、上書きを試み続けず、必要なら threads でユーザーにロック解除を依頼すること。ノート詳細(GET)では `is_locked` / `is_masked` フィールドで状態を確認できる(`is_masked` は HQ UI 上の表示マスクで、API レスポンスの本文には影響しない)。
2240
+
2163
2241
  #### hq CLI ラッパー
2164
2242
 
2165
2243
  ```bash
@@ -20,16 +20,34 @@ curl -X POST http://localhost:8080/api/web/extract \
20
20
 
21
21
  このAPIは内部で Playwright + Readability を回して **メインセッションには結果 JSON だけ返す** ため、Playwright MCP を使うときに起きていたチャットコンテキストのトークン肥大化が回避できる。
22
22
 
23
+ ### SPA / 無限スクロールのページ (v4.7.0〜)
24
+
25
+ - **SPA (React/Vue 等でクライアント描画するページ)** もそのまま `/api/web/extract` でよい。内部で DOM が静止するまで待ってから抽出するため、空シェルを掴む問題は解消済み。
26
+ - **無限スクロール / 「もっと見る」で件数が増えるページ**で十分な件数を確保したい場合は `scroll` オプションを付ける。**どれだけ集めるかは呼び出し側が決める**:
27
+
28
+ ```bash
29
+ curl -X POST http://localhost:8080/api/web/extract \
30
+ -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
31
+ -d '{
32
+ "url": "対象URL",
33
+ "scroll": { "strategy": "count", "targetItems": 50, "maxScrolls": 20, "maxMs": 15000 }
34
+ }' | jq
35
+ ```
36
+
37
+ - `count`: 目標件数 (`targetItems`) に達するまでスクロール。`untilStable`: 件数が増えなくなるまで。`fixed`: 回数固定。
38
+ - レスポンスの `scrollInfo.reachedTarget` が `false` なら上限で打ち切られている → `maxScrolls` / `maxMs` を上げて再試行する。
39
+ - スクロール上限はサーバー側でクランプされる (maxScrolls≤50 / maxMs≤45s)。それ以上の網羅が要るなら**ページネーションURLをループ呼び出し**する方が確実。
40
+
23
41
  ### Playwright MCP を使うべき場面
24
42
 
25
43
  `/api/web/extract` で対応できないのは以下のケース。このときだけ `mcp__playwright__*` を使う:
26
44
 
27
45
  - ログイン必須ページ (Cookie/2FA 等の認証必要)
28
46
  - フォーム入力・複数ページ遷移を伴う操作
29
- - ボタンクリック→動的に追加されるコンテンツの取得
47
+ - 「もっと見る」**ボタンのクリック**で追加ロードするページ (スクロールでは増えないもの。`scroll` はスクロール式のみ対応)
30
48
  - Lancers コンペ応募など、明らかに対話的操作が必要なフロー
31
49
 
32
- **単純な閲覧・抽出用途では MCP を使わない。**
50
+ **単純な閲覧・抽出 (SPA・無限スクロール含む) では MCP を使わない。**
33
51
 
34
52
  ### よくあるパターン
35
53