jeo-code 0.11.5 → 0.11.6
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/CHANGELOG.md +15 -0
- package/README.ja.md +49 -2
- package/README.ko.md +49 -2
- package/README.md +35 -2
- package/README.zh.md +49 -2
- package/package.json +1 -1
- package/src/ai/model-cache.ts +79 -28
- package/src/ai/model-catalog.ts +38 -9
- package/src/ai/model-discovery.ts +77 -11
- package/src/ai/providers/antigravity.ts +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,21 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.11.6] - 2026-10-16
|
|
13
|
+
_Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability._
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **Live Antigravity model discovery scoped to account** (`src/ai/model-discovery.ts`, `src/commands/launch.ts`) — OAuth account's real `agentModelSorts` models from the Cloud Code Assist endpoint are cached and rehydrated at launch, scoped to the selected account. Picker `/model provider antigravity` and `/agents <role> provider antigravity` offer only the account's live model list (no static fallback catalog, no Argon guarantee for every account). Empty or failed discovery is reported honestly.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **Model picker account scoping** — OpenAI and Antigravity pickers honor the selected OAuth account; prior-account stale models are suppressed. Cache keys include provider, account ID, and source (oauth/api_key) for isolation.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Picker empty results** — an authoritative empty Antigravity account list no longer falls back to fabricated static rows. Project lookup is best-effort; mismatches between discovered and stored project IDs are handled safely.
|
|
23
|
+
|
|
24
|
+
### Verified
|
|
25
|
+
- Live Antigravity OAuth account discovery: account scope is honored on picker refresh and cache refresh. Model picker respects discovered list with no static fallback. `bun run typecheck` clean.
|
|
26
|
+
|
|
12
27
|
## [0.11.5] - 2026-10-04
|
|
13
28
|
_Anthropic login now runs Opus 5.5 and Sonnet 5.5 as the selected models: the OAuth client version gate and incompatible fast-mode temperature are fixed, with real request and streaming responses verified._
|
|
14
29
|
|
package/README.ja.md
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
- **蓄積されるスキル** — 行き詰まったターンは、その行き止まりをまさに同じスキルのプロジェクトレベルファイル(`.jeo/skills/<name>.md`、初回書き込み時にバンドルスキルからシード、決定論的なキーワードマッチ、LLM不使用)に書き込むようになりました。これにより次のセッションの `$<skill>` 呼び出しは、バンドルされたドキュメントが永遠に静的なままでいる代わりに、蓄積された「Known Failure Modes」/「Anti-Patterns」の知識を引き継ぎます。`jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"` で手動記録、`jeo skills eval <skill>` は記録済みの各教訓がスキルの現行ガイダンスでまだカバーされているか、それとも陳腐化したかを実際の LLM 判定で確認します。
|
|
50
50
|
- **低コスト層のグレーダールーティング** — `/goal` の検証器、`critic` サブエージェントロール、固定されていない `task` ファンアウトバッチは、採点・実行対象の作業と同じフルプライスモデルに黙って乗る代わりに、デフォルトで低コストのクレデンシャル済みモデルを使用します(`resolveVerifierModel`、ブラウザの `verify` アクションについてはビジョン対応能力でフィルタされ、テキストのみの低コストモデルが添付スクリーンショットを黙って取りこぼすことがありません)。
|
|
51
51
|
- **`jeo routine init`** — スケジュール/issue/PR トリガーで jeo をヘッドレス実行する(`jeo "<prompt>" -p`) GitHub Actions ワークフローを、GitHub 自身のランナー上に生成します — ラップトップは不要で、jeo 自体の内部に新しい攻撃対象領域も一切増えません(インプロセスのスケジューラや Webhook リスナーはありません)。`--dry-run` でプレビュー、`--no-pr` でデフォルトの実行ごとの PR の代わりに直接コミット。
|
|
52
|
-
- **リモート監視・制御(Telegram, Discord, Slack)** — ボットを一度ペアリング(`jeo notify setup --provider telegram|discord|slack`)すれば、共有の `jeo daemon start` 1 つが、稼働中のすべてのセッションにわたってサブエージェントの状態遷移(開始 → 完了/失敗/キャンセル)ごとにメッセージを送り、人間の許可リスト(allowlist)からの `/subagents`、`/steer`、`/cancel`(Discord/Slack ではさらに `/sessions` と `/send`)を受け付けます。Telegram はフォーラムトピック・インラインキーボード・画像添付に対応し、Discord は REST + Gateway WebSocket、Slack は Web API + Socket Mode
|
|
52
|
+
- **リモート監視・制御(Telegram, Discord, Slack)** — ボットを一度ペアリング(`jeo notify setup --provider telegram|discord|slack`)すれば、共有の `jeo daemon start` 1 つが、稼働中のすべてのセッションにわたってサブエージェントの状態遷移(開始 → 完了/失敗/キャンセル)ごとにメッセージを送り、人間の許可リスト(allowlist)からの `/subagents`、`/steer`、`/cancel`(Discord/Slack ではさらに `/sessions` と `/send`)を受け付けます。Telegram はフォーラムトピック・インラインキーボード・画像添付に対応し、Discord は REST + Gateway WebSocket、Slack は Web API + Socket Mode を使います。プラットフォーム別詳細ガイドおよびインストール可能なエージェントスキル(`channel-notify`)を提供します。
|
|
53
53
|
- **ごまかしの効かないゲート** — `approve` は承認対象そのもののプランダイジェストを再検証し、`team` はレビュー済みダイジェストが変わったプランを拒否します。`done` 呼び出しは最新の検証証拠と照らして再チェックされ(後から失敗したチェックは先の合格を無効化し、修正の差し戻し 3 回の後は強い拒否)、`autopilot` ラチェットはロールバック失敗を `rollback_failed` として記録し、ステップを戻したと主張する代わりに非ゼロで停止します。
|
|
54
54
|
- **ターンをブロックしないセッション単位の非同期実行** — `task` ツールの実際の `tasks` 配列で独立した作業をファンアウトし、親ターンを止めません。detached サブエージェント、バックグラウンド job、行単位 monitor は後続ターンからも `subagent`/`job`/`monitor` の `list`、`inspect`、`await`、`cancel`、`tail` で制御できます。インライン TUI は各ワーカーのライブ activity を専用スロットに表示し、セッション終了または Ctrl-C ですべてのレジストリを破棄します。
|
|
55
55
|
- **独立した検証者、実際に強制** — プランはもう architect/critic ステップをスキップできません: `PlanSchema` は未検証の変更で終わるプランを拒否し(検証対象の変更より前に置かれた verifier も無効)、`ralplan` のドラフト時点と `team`/`approve` の実行時点の両方で適用されます。すべての architect/critic 評決も実際の証拠を示す必要があり、観測された `read`/`search`/`find`/`ast_grep`/`lsp` 呼び出しがゼロなら、テキストが何を主張していても評決はブロックされます。
|
|
@@ -107,6 +107,11 @@ jeo --tmux # 独立した tmux セッションで実行
|
|
|
107
107
|
> [!CAUTION]
|
|
108
108
|
> **`/model <name>` で特定モデルを手動指定すると、そのセッション中ルーティングが固定されます。** プロンプトルーティング(`/route`)はモデルが手動固定されていない間のみ毎ターン再評価されます。`/model <name>` で特定モデルを選ぶとその選択が固定され、`/model auto`(ピンを完全に解除)を実行するか、`/route on`(ピンを消さずに優先度だけ上げる — `/route off` した瞬間ピンが復活)を実行するまでルーティングは切り替わりません。`roles.*` 未設定時に `defaultModel` へ確定フォールバックするのは `standard` ティアのみで、`high`/`complex` ティアは通常フォールバック前にライブでクレデンシャル済みの最強モデルを先に探索するため、未設定でも毎ターン別モデルに着地することがあります。**例外:** Antigravity または Gemini OAuth でクレデンシャルされたセッションは一つの資格情報で Anthropic/Google/OpenAI のモデルを再公開しており、その場合 `high`/`complex` は(必ずしも最強ではなく)会社ごとに1モデルへセッション単位で安定的に分散されるため、ターンごとに変わらずそのセッション中は固定されます。
|
|
109
109
|
|
|
110
|
+
### Antigravity アカウント モデル ライブディスカバリー
|
|
111
|
+
|
|
112
|
+
**ライブ モデル ディスカバリー** — `/model provider antigravity` と `/agents <ロール> provider antigravity` は OAuth アカウントが Cloud Code Assist で実際に保有するモデルを表示します。ピッカーはアカウントが持つモデルのみを認め、すべてのアカウントが Argon を備えていることは保証しません。空のアカウント一覧は正直に報告され、静的なカタログでの代用はしません。アカウントスコープはセッションのリフレッシュ全体を通じて保持されます。
|
|
113
|
+
|
|
114
|
+
**例:** `jeo auth login antigravity` はアカウントの実際に利用可能なモデルを発見しキャッシュします。`/model antigravity/<モデル-id>` または `/agents executor provider antigravity` で選択し、セッション中に利用するか `/model save antigravity/<モデル-id>` で既定値として設定します。
|
|
110
115
|
## CLI コマンド
|
|
111
116
|
|
|
112
117
|
`jeo --help` が正式な一覧です。よく使うものは次のとおり:
|
|
@@ -205,6 +210,13 @@ jeo ultragoal
|
|
|
205
210
|
|
|
206
211
|
オプトイン: 共有デーモン 1 つがマシン上の稼働中の `jeo` セッションをすべて監視し、**サブエージェントの状態遷移**(開始 → 完了/失敗/キャンセル)ごとにメッセージを送ります。加えてセッション識別ヘッダー、ターンの開始/終了サマリー、確定したターンのテキストも送りますが、「まだ実行中」という繰り返しの ping は決して送りません。allowlist に登録された人間はチャットから一覧・送信・ステア・キャンセルができ、すべての制御はローカルセッションへ中継されて **確認応答済み(受理であって完了ではない)** または **未確認応答** として報告されます — デーモンがコマンドの実行完了を主張することはありません。
|
|
207
212
|
|
|
213
|
+
プラットフォーム別詳細ガイド:
|
|
214
|
+
- 📖 [チャネル連携概要 & アーキテクチャ](docs/integrations/README.md) — 機能マトリクスと全体構成
|
|
215
|
+
- 📱 [Telegram 連携詳細ガイド](docs/integrations/telegram.md) — 内蔵バックグラウンドデーモン、BotFather ペアリング、フォーラムトピック、`/steer` コマンド
|
|
216
|
+
- 🎮 [Discord 連携詳細ガイド](docs/integrations/discord.md) — 即時チャネル通知用 Webhook、双方向対話ボット、Embed カード形式
|
|
217
|
+
- 💬 [Slack 連携詳細ガイド](docs/integrations/slack.md) — ワークスペース通知用 Incoming Webhook、チーム共同作業用 Slack App / Socket Mode、Block Kit カード
|
|
218
|
+
- 🧩 [エージェントスキルインストールガイド](docs/integrations/agent-skills.md) — `jeo`、Claude Code、Cursor、Codex などへチャネル通知スキルを導入
|
|
219
|
+
|
|
208
220
|
```bash
|
|
209
221
|
jeo notify setup [--provider telegram|discord|slack] [--token-env NAME] [--app-token-env NAME] [--chat-id ID | --channel-id ID] [--allowed-user-ids ID,ID,...]
|
|
210
222
|
jeo notify status [--provider …] # マスク済みトークン、宛先、allowlist、デーモン状態(stopped / stale / initializing / initialized / pairing)
|
|
@@ -240,6 +252,41 @@ jeo daemon start|stop|status|reload # 共有デーモンにプロバイダ選
|
|
|
240
252
|
└─────────────────────┘ session└──────────────────┘ └───────────────────────────┘
|
|
241
253
|
```
|
|
242
254
|
|
|
255
|
+
### Discord & Slack (Webhook & ボットアプリ)
|
|
256
|
+
|
|
257
|
+
チームのワークスペース通知用に環境変数を設定します:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
# Discord Webhook
|
|
261
|
+
export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..."
|
|
262
|
+
|
|
263
|
+
# Slack Incoming Webhook
|
|
264
|
+
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
タスク実行状況、所要時間、コミットおよび PR リンクを含むリッチな Embed(Discord) や Block Kit カード(Slack) を送信できます。ペイロード形式や CI 自動化スニペットは [Discord ガイド](docs/integrations/discord.md) および [Slack ガイド](docs/integrations/slack.md) をご覧ください。
|
|
268
|
+
|
|
269
|
+
### エージェントスキルとしてのインストール
|
|
270
|
+
|
|
271
|
+
標準の [Agent Skills](https://agentskills.io) CLI を使用して、`jeo` やその他の AI コーディングエージェントに通知スキルを追加できます:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# 現在のリポジトリに channel-notify スキルをインストール:
|
|
275
|
+
npx skills add akillness/jeo-code --skill channel-notify
|
|
276
|
+
|
|
277
|
+
# または全プロジェクト共通で使えるようグローバル(-g)にインストール:
|
|
278
|
+
npx skills add akillness/jeo-code --skill channel-notify -g
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
インストール後はエージェントがタスク実行中に自動で進捗を通知します:
|
|
282
|
+
```bash
|
|
283
|
+
# jeo でのワンショット実行:
|
|
284
|
+
jeo $channel-notify "全47件のテストにパスしました。レビュー準備完了。"
|
|
285
|
+
|
|
286
|
+
# Claude Code での実行:
|
|
287
|
+
/channel-notify "ビルド成功。PR: https://github.com/..."
|
|
288
|
+
```
|
|
289
|
+
|
|
243
290
|
## ルーティン(GitHub Actions)
|
|
244
291
|
|
|
245
292
|
```bash
|
|
@@ -348,10 +395,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
348
395
|
|
|
349
396
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
350
397
|
- **[Unreleased]**
|
|
398
|
+
- **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
|
|
351
399
|
- **[0.11.5]** (2026-10-04) — Anthropic login now runs Opus 5.5 and Sonnet 5.5 as the selected models: the OAuth client version gate and incompatible fast-mode temperature are fixed, with real request and streaming responses verified.
|
|
352
400
|
- **[0.11.4]** (2026-10-01) — The release pipeline could not actually release: 13 tests failed on Linux CI only, and the stored npm token no longer authenticated. Both are fixed at the root — the suite is order-independent, and publishing no longer depends on a secret that can expire.
|
|
353
401
|
- **[0.11.3]** (2026-10-01) — Remote control grew from one Telegram bot into one shared daemon serving Telegram, Discord, and Slack — and the workflow gates that were meant to block unverified work (`approve`/`team` plan identity, `done` re-checks, the autopilot ratchet) now actually block it.
|
|
354
|
-
- **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
|
|
355
402
|
|
|
356
403
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
357
404
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
- **누적되는 스킬** — 정체된 턴은 이제 그 막다른 지점을 바로 그 스킬의 프로젝트 레벨 파일(`.jeo/skills/<name>.md`, 첫 작성 시 번들 스킬로 시드, 결정론적 키워드 매칭, LLM 미사용)에 기록하므로, 다음 세션의 `$<skill>` 호출은 번들 문서가 영원히 정적으로 남는 대신 누적된 "Known Failure Modes"/"Anti-Patterns" 지식을 함께 가져갑니다. 수동 항목은 `jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"`로; `jeo skills eval <skill>`은 기록된 각 교훈이 스킬의 현재 가이던스로 여전히 커버되는지 아니면 낡았는지를 실제 LLM 판단으로 확인합니다.
|
|
50
50
|
- **저비용 등급 채점 라우팅** — `/goal` 검증기, `critic` 서브에이전트 역할, 그리고 고정되지 않은 `task` 팬아웃 배치는 채점·실행 대상인 작업과 같은 풀프라이스 모델에 조용히 편승하는 대신 기본적으로 저비용 크레덴셜 모델을 사용합니다(`resolveVerifierModel`, 브라우저 `verify` 액션에 대해서는 비전 능력으로 필터링되어 텍스트 전용 저비용 모델이 첨부된 스크린샷을 조용히 누락시키지 않도록 합니다).
|
|
51
51
|
- **`jeo routine init`** — 스케줄/이슈/PR 트리거에서 jeo를 헤드리스로 실행하는(`jeo "<prompt>" -p`) GitHub Actions 워크플로를 생성합니다, GitHub 자체 러너 위에서 — 노트북이 필요 없고, jeo 내부에 새로운 공격 표면도 전혀 추가되지 않습니다(인프로세스 스케줄러도 웹훅 리스너도 없음). `--dry-run`으로 미리보기, `--no-pr`로 기본값인 실행당 PR 대신 직접 커밋.
|
|
52
|
-
- **원격 모니터링 & 제어(Telegram, Discord, Slack)** — 봇을 한 번 페어링(`jeo notify setup --provider telegram|discord|slack`)하면, 공유 `jeo daemon start` 하나가 모든 활성 세션에 걸쳐 서브에이전트 상태 전환(시작 → 완료/실패/취소)마다 메시지를 보내고, 사람 allowlist로부터 `/subagents`, `/steer`, `/cancel`(Discord/Slack에서는 `/sessions`, `/send` 추가)을 되받습니다. Telegram은 포럼 토픽·인라인 키보드·이미지 첨부를 더하고, Discord는 REST + Gateway WebSocket, Slack은 Web API + Socket Mode를 사용합니다.
|
|
52
|
+
- **원격 모니터링 & 제어(Telegram, Discord, Slack)** — 봇을 한 번 페어링(`jeo notify setup --provider telegram|discord|slack`)하면, 공유 `jeo daemon start` 하나가 모든 활성 세션에 걸쳐 서브에이전트 상태 전환(시작 → 완료/실패/취소)마다 메시지를 보내고, 사람 allowlist로부터 `/subagents`, `/steer`, `/cancel`(Discord/Slack에서는 `/sessions`, `/send` 추가)을 되받습니다. Telegram은 포럼 토픽·인라인 키보드·이미지 첨부를 더하고, Discord는 REST + Gateway WebSocket, Slack은 Web API + Socket Mode를 사용합니다. 플랫폼별 상세 가이드 및 설치 가능한 에이전트 스킬(`channel-notify`)을 제공합니다.
|
|
53
53
|
- **말로 넘길 수 없는 게이트** — `approve`는 승인 대상인 바로 그 플랜 다이제스트를 재검증하고, `team`은 리뷰된 다이제스트가 바뀐 플랜을 거부합니다. `done` 호출은 최신 검증 증거와 대조해 재확인되며(나중에 실패한 검사가 앞선 통과를 무효화하고, 교정 반송 3회 뒤에는 하드 거부), `autopilot` ratchet은 실패한 롤백을 `rollback_failed`로 기록하고 되돌린 스텝이라고 주장하는 대신 0이 아닌 코드로 중단합니다.
|
|
54
54
|
- **턴을 막지 않는 세션 범위 비동기 실행** — `task` 도구의 실제 `tasks` 배열로 독립 작업을 팬아웃해 부모 턴을 막지 않습니다. detached 서브에이전트, 백그라운드 job, 줄 단위 monitor는 이후 턴에서도 `subagent`/`job`/`monitor`의 `list`, `inspect`, `await`, `cancel`, `tail`로 계속 제어할 수 있습니다. 인라인 TUI는 각 워커의 실시간 activity를 별도 슬롯에 표시하고 세션 종료 또는 Ctrl-C에서 모든 레지스트리를 정리합니다.
|
|
55
55
|
- **실제로 강제되는 독립 검증자** — 플랜은 이제 architect/critic 단계를 건너뛸 수 없습니다: `PlanSchema`는 미검증 변이로 끝나는 모든 플랜을 거부하며(검증 대상 변이보다 앞에 배치된 검증자도 인정하지 않음), `ralplan` 드래프트 시점과 `team`/`approve` 실행 시점 양쪽 모두에 적용됩니다. 모든 architect/critic 평결도 실제 증거를 제시해야 하며, 관찰된 `read`/`search`/`find`/`ast_grep`/`lsp` 호출이 0건이면 텍스트가 무엇을 주장하든 평결이 차단됩니다.
|
|
@@ -107,6 +107,11 @@ jeo --tmux # 독립 tmux 세션에서 실행
|
|
|
107
107
|
|
|
108
108
|
> [!CAUTION]
|
|
109
109
|
> **`/model <name>`으로 특정 모델을 수동 지정하면 그 세션 동안 라우팅이 고정됩니다.** 프롬프트 라우팅(`/route`)은 모델이 수동으로 고정되지 않은 동안만 매 턴 재평가됩니다. `/model <name>`으로 특정 모델을 선택하면 그 선택이 그대로 고정되며, `/model auto`(핀을 완전히 해제)를 실행하거나 `/route on`(핀을 지우지 않고 그보다 우선순위를 높임 — `/route off` 하는 즉시 핀이 다시 살아남)을 실행하기 전까지는 라우팅이 다시 전환되지 않습니다. `roles.*` 항목 미설정 시 `defaultModel`로 확정 폴백되는 건 `standard` 티어뿐이며, `high`/`complex` 티어는 보통 폴백 전에 실시간으로 크레덴셜된 가장 강력한 모델을 먼저 탐색하므로, 설정이 없어도 매 턴 다른 모델로 갈 수 있습니다. **예외:** Antigravity 또는 Gemini OAuth로 크레덴셜된 세션은 하나의 크레덴셜로 Anthropic/Google/OpenAI 모델을 함께 재노출하는데, 이 경우 `high`/`complex`는 (항상 최강 모델이 아니라) 회사당 모델 1개로 세션-안정적으로 분산되어, 턴마다 바뀌지 않고 해당 세션 내내 고정됩니다.
|
|
110
|
+
### Antigravity 계정 모델 라이브 디스커버리
|
|
111
|
+
|
|
112
|
+
**실시간 모델 디스커버리** — `/model provider antigravity`와 `/agents <역할> provider antigravity`는 OAuth 계정이 Cloud Code Assist에서 실제로 보유한 모델을 보여줍니다. 피커는 계정이 가진 모델만 인정하며, 모든 계정이 Argon을 갖추었다고 보장하지 않습니다. 빈 계정 목록은 정직하게 보고되며, 정적 카탈로그로 대체하지 않습니다. 계정 스코프는 세션 리프레시 전체에서 보존됩니다.
|
|
113
|
+
|
|
114
|
+
**예시:** `jeo auth login antigravity`는 계정의 실제 이용 가능한 모델을 발견하고 캐시합니다. `/model antigravity/<모델-id>` 또는 `/agents executor provider antigravity`로 선택한 뒤 세션 동안 사용하거나 `/model save antigravity/<모델-id>`로 기본값 설정.
|
|
110
115
|
|
|
111
116
|
## CLI 명령
|
|
112
117
|
|
|
@@ -206,6 +211,13 @@ jeo ultragoal
|
|
|
206
211
|
|
|
207
212
|
옵트인: 공유 데몬 하나가 머신의 모든 활성 `jeo` 세션을 지켜보다가 **서브에이전트 상태 전환**(시작 → 완료/실패/취소)마다 메시지를 보내고, 세션 식별 헤더, 턴 시작/종료 요약, 확정된 턴 텍스트도 함께 전송합니다 — "여전히 실행 중" 반복 핑은 절대 보내지 않습니다. allowlist에 등록된 사람은 채팅에서 목록 조회·전송·조종·취소를 할 수 있고, 모든 제어는 로컬 세션으로 중계되어 **확인 응답됨(수락됨, 완료 아님)** 또는 **확인 응답 안 됨**으로 보고됩니다 — 데몬은 명령이 실행됐다고 절대 주장하지 않습니다.
|
|
208
213
|
|
|
214
|
+
플랫폼별 상세 단독 가이드:
|
|
215
|
+
- 📖 [채널 연동 개요 & 아키텍처](docs/integrations/README.md) — 기능 매트릭스 및 구조 개요
|
|
216
|
+
- 📱 [Telegram 연동 상세 가이드](docs/integrations/telegram.md) — 내장 백그라운드 데몬, BotFather 대화형 페어링, 포럼 토픽, `/steer` 원격 명령
|
|
217
|
+
- 🎮 [Discord 연동 상세 가이드](docs/integrations/discord.md) — 즉각적인 채널 알림을 위한 웹훅, 양방향 상호작용을 위한 봇 및 임베드 카드 포맷
|
|
218
|
+
- 💬 [Slack 연동 상세 가이드](docs/integrations/slack.md) — 워크스페이스 알림용 Incoming Webhook, 팀 협업용 Slack App / Socket Mode, Block Kit 카드
|
|
219
|
+
- 🧩 [에이전트 스킬 설치 가이드](docs/integrations/agent-skills.md) — `jeo`, Claude Code, Cursor, Codex 등 다양한 에이전트에 채널 알림 스킬 설치하기
|
|
220
|
+
|
|
209
221
|
```bash
|
|
210
222
|
jeo notify setup [--provider telegram|discord|slack] [--token-env NAME] [--app-token-env NAME] [--chat-id ID | --channel-id ID] [--allowed-user-ids ID,ID,...]
|
|
211
223
|
jeo notify status [--provider …] # 마스킹된 토큰, 목적지, allowlist, 데몬 상태 (stopped / stale / initializing / initialized / pairing)
|
|
@@ -242,6 +254,41 @@ jeo daemon start|stop|status|reload # 공유 데몬에는 프로바이더 선
|
|
|
242
254
|
```
|
|
243
255
|
|
|
244
256
|
|
|
257
|
+
### Discord & Slack (웹훅 & 봇 앱)
|
|
258
|
+
|
|
259
|
+
팀 워크스페이스를 위해 표준 웹훅 또는 봇 자격증명을 환경 변수로 설정합니다:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
# Discord Webhook
|
|
263
|
+
export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..."
|
|
264
|
+
|
|
265
|
+
# Slack Incoming Webhook
|
|
266
|
+
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
작업 상태, 소요 시간, 커밋 및 PR 링크가 포함된 풍부한 임베드(Discord) 및 Block Kit 카드(Slack)를 전송할 수 있습니다. 페이로드 스키마와 CI 자동화 스니펫은 [Discord 가이드](docs/integrations/discord.md) 및 [Slack 가이드](docs/integrations/slack.md)를 참고하세요.
|
|
270
|
+
|
|
271
|
+
### 에이전트 스킬로 설치
|
|
272
|
+
|
|
273
|
+
표준 [Agent Skills](https://agentskills.io) CLI를 사용하여 `jeo` 및 다른 AI 코딩 에이전트에 채널 알림 스킬을 즉시 설치할 수 있습니다:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# 현재 저장소에 channel-notify 스킬 설치:
|
|
277
|
+
npx skills add akillness/jeo-code --skill channel-notify
|
|
278
|
+
|
|
279
|
+
# 또는 모든 프로젝트에서 사용하도록 전역(-g) 설치:
|
|
280
|
+
npx skills add akillness/jeo-code --skill channel-notify -g
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
설치 후 에이전트는 작업 수행 중 자동으로 상태를 브로드캐스트할 수 있습니다:
|
|
284
|
+
```bash
|
|
285
|
+
# jeo에서 원샷 호출:
|
|
286
|
+
jeo $channel-notify "테스트 47개 전수 통과 완료. 리뷰 준비됨."
|
|
287
|
+
|
|
288
|
+
# Claude Code에서 호출:
|
|
289
|
+
/channel-notify "빌드 성공. PR: https://github.com/..."
|
|
290
|
+
```
|
|
291
|
+
|
|
245
292
|
## 루틴 (GitHub Actions)
|
|
246
293
|
|
|
247
294
|
```bash
|
|
@@ -350,10 +397,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
350
397
|
|
|
351
398
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
352
399
|
- **[Unreleased]**
|
|
400
|
+
- **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
|
|
353
401
|
- **[0.11.5]** (2026-10-04) — Anthropic login now runs Opus 5.5 and Sonnet 5.5 as the selected models: the OAuth client version gate and incompatible fast-mode temperature are fixed, with real request and streaming responses verified.
|
|
354
402
|
- **[0.11.4]** (2026-10-01) — The release pipeline could not actually release: 13 tests failed on Linux CI only, and the stored npm token no longer authenticated. Both are fixed at the root — the suite is order-independent, and publishing no longer depends on a secret that can expire.
|
|
355
403
|
- **[0.11.3]** (2026-10-01) — Remote control grew from one Telegram bot into one shared daemon serving Telegram, Discord, and Slack — and the workflow gates that were meant to block unverified work (`approve`/`team` plan identity, `done` re-checks, the autopilot ratchet) now actually block it.
|
|
356
|
-
- **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
|
|
357
404
|
|
|
358
405
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
359
406
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Run `jeo` inside a repository and it reads files, edits them, runs commands, and
|
|
|
54
54
|
- **Skills that compound** — a stalled turn now writes the dead end into the SAME skill's project-level file (`.jeo/skills/<name>.md`, seeded from the bundled skill on first write, deterministic keyword match, no LLM), so the next session's `$<skill>` invocation carries accumulated "Known Failure Modes"/"Anti-Patterns" knowledge instead of the bundled doc staying static forever. `jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"` for manual entries; `jeo skills eval <skill>` runs a real LLM judgment on whether each recorded lesson is still covered by the skill's current guidance or has gone stale.
|
|
55
55
|
- **Cheap-tier grader routing** — the `/goal` verifier, the `critic` subagent role, and unpinned `task` fan-out batches default to a cheap credentialed model instead of silently riding the same full-price model as the work they're grading/executing (`resolveVerifierModel`, vision-capability-filtered for the browser `verify` action so a text-only cheap model never silently drops an attached screenshot).
|
|
56
56
|
- **`jeo routine init`** — generates a GitHub Actions workflow that runs jeo headlessly (`jeo "<prompt>" -p`) on a schedule/issue/PR trigger, on GitHub's own runners — no laptop required, and zero new attack surface inside jeo itself (no in-process scheduler or webhook listener). `--dry-run` to preview, `--no-pr` for a direct commit instead of the default PR-per-run.
|
|
57
|
-
- **Remote monitoring & control (Telegram, Discord, Slack)** — pair a bot once (`jeo notify setup --provider telegram|discord|slack`), then one shared `jeo daemon start` pushes a message on every subagent state edge (started → done/failed/cancelled) across every live session and accepts `/subagents`, `/steer`, `/cancel` (plus `/sessions` and `/send` on Discord/Slack) back from a human allowlist. Telegram adds forum topics, inline keyboards, and image attachments; Discord uses REST + Gateway WebSocket; Slack uses Web API + Socket Mode.
|
|
57
|
+
- **Remote monitoring & control (Telegram, Discord, Slack)** — pair a bot once (`jeo notify setup --provider telegram|discord|slack`), then one shared `jeo daemon start` pushes a message on every subagent state edge (started → done/failed/cancelled) across every live session and accepts `/subagents`, `/steer`, `/cancel` (plus `/sessions` and `/send` on Discord/Slack) back from a human allowlist. Telegram adds forum topics, inline keyboards, and image attachments; Discord uses REST + Gateway WebSocket; Slack uses Web API + Socket Mode. Dedicated standalone setup guides and an installable Agent Skill (`channel-notify`) are included.
|
|
58
58
|
- **Gates that cannot be talked around** — `approve` re-validates the exact plan digest it is approving and `team` refuses a plan whose reviewed digest changed; a `done` call is re-checked against the latest verification evidence (a later failed check invalidates an earlier pass, three correction bounces then a hard rejection); the `autopilot` ratchet records a failed rollback as `rollback_failed` and halts non-zero instead of claiming a reverted step.
|
|
59
59
|
- **Session-scoped async execution** — fan out independent work through the `task` tool's real `tasks` array without blocking the parent turn; detached subagents, background jobs, and line monitors remain controllable from later turns with `subagent`/`job`/`monitor` actions (`list`, `inspect`, `await`, `cancel`, `tail`). The inline TUI keeps each worker's live activity in its own slot and tears down every registry on session exit or Ctrl-C.
|
|
60
60
|
- **Independent verifier, actually enforced** — a plan can no longer skip its architect/critic step: `PlanSchema` rejects any plan that ends with an unverified mutation (a verifier placed BEFORE the mutation it should check doesn't count either), at both `ralplan` draft time and `team`/`approve` execution time. Every architect/critic verdict must also show real evidence — zero observed `read`/`search`/`find`/`ast_grep`/`lsp` calls blocks the verdict regardless of what the text claims.
|
|
@@ -112,6 +112,11 @@ Inside the `jeo` REPL (Tab autocompletes; `/` opens the palette).
|
|
|
112
112
|
|
|
113
113
|
> [!CAUTION]
|
|
114
114
|
> **`/model <name>` locks routing for the rest of the session.** Prompt routing (`/route`) only re-evaluates per turn while no model is manually pinned. Picking a specific model via `/model <name>` freezes that choice — routing will *not* switch away from it again until you run `/model auto` (which clears the pin), or `/route on` (which *outranks* an active pin without clearing it — the pin reasserts itself the moment you run `/route off`). Missing a `roles.*` entry only guarantees a `defaultModel` fallback on the `standard` tier; the `high`/`complex` tiers otherwise scan for the strongest live-credentialed model, so they can still land on a different model each turn even when unconfigured. **Exception:** an Antigravity- or Gemini-OAuth-credentialed session re-exports Anthropic/Google/OpenAI models under one credential — `high`/`complex` there instead session-stably spread across one model per company (not necessarily the strongest), so the pick stays fixed for that session rather than varying turn to turn.
|
|
115
|
+
### Antigravity account model discovery
|
|
116
|
+
|
|
117
|
+
**Live model discovery** — `/model provider antigravity` and `/agents <role> provider antigravity` show your OAuth account's actual available models from Cloud Code Assist. The picker honors only models your account holds; no Argon is guaranteed for every account. Empty account lists are reported honestly instead of falling back to static catalogs. Account scope is preserved across session refreshes.
|
|
118
|
+
|
|
119
|
+
**Example:** `jeo auth login antigravity` discovers and caches your account's available models. Pick one with `/model antigravity/<model-id>` or `/agents executor provider antigravity`, then use it for the session or persist it as default via `/model save antigravity/<model-id>`.
|
|
115
120
|
|
|
116
121
|
## CLI commands
|
|
117
122
|
|
|
@@ -212,6 +217,13 @@ Non-zero hook output is appended to the tool result the model reads (deduped per
|
|
|
212
217
|
|
|
213
218
|
Opt-in: one shared daemon watches every live `jeo` session on the machine and pushes a message on each **subagent state edge** (started → completed/failed/cancelled), plus the session identity header, turn start/finish summaries, and the finalized turn text — never repeated "still running" pings. Allowlisted humans can list, send, steer, and cancel from the chat; every control is relayed to the local session and reported as **acknowledged (accepted, not completed)** or **not acknowledged** — the daemon never claims a command ran.
|
|
214
219
|
|
|
220
|
+
Detailed standalone guides:
|
|
221
|
+
- 📖 [Channel Integrations Overview](docs/integrations/README.md) — Feature matrix and architecture overview
|
|
222
|
+
- 📱 [Telegram Integration Guide](docs/integrations/telegram.md) — Built-in background daemon, interactive BotFather pairing, forum topics, and `/steer` commands
|
|
223
|
+
- 🎮 [Discord Integration Guide](docs/integrations/discord.md) — Webhooks for instant channel updates, Discord Bot for bidirectional interactions, and embed formats
|
|
224
|
+
- 💬 [Slack Integration Guide](docs/integrations/slack.md) — Incoming Webhooks for workspace notifications, Slack App / Socket Mode for team collaboration, and Block Kit cards
|
|
225
|
+
- 🧩 [Agent Skills Installation Guide](docs/integrations/agent-skills.md) — Install channel notification capabilities into `jeo`, Claude Code, Cursor, Codex, and other agents
|
|
226
|
+
|
|
215
227
|
```bash
|
|
216
228
|
jeo notify setup [--provider telegram|discord|slack] [--token-env NAME] [--app-token-env NAME] [--chat-id ID | --channel-id ID] [--allowed-user-ids ID,ID,...]
|
|
217
229
|
jeo notify status [--provider …] # masked token, destination, allowlist, daemon state (stopped / stale / initializing / initialized / pairing)
|
|
@@ -247,6 +259,27 @@ What the words mean, because they are not interchangeable:
|
|
|
247
259
|
└─────────────────────┘ session└──────────────────┘ └───────────────────────────┘
|
|
248
260
|
```
|
|
249
261
|
|
|
262
|
+
### Install as an Agent Skill
|
|
263
|
+
|
|
264
|
+
Install channel notification capabilities into `jeo` or any other AI coding agent using the standard [Agent Skills](https://agentskills.io) CLI:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# Install channel-notify skill into the current repository:
|
|
268
|
+
npx skills add akillness/jeo-code --skill channel-notify
|
|
269
|
+
|
|
270
|
+
# Or install globally across all your projects:
|
|
271
|
+
npx skills add akillness/jeo-code --skill channel-notify -g
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Once installed, agents can automatically broadcast status updates during tasks:
|
|
275
|
+
```bash
|
|
276
|
+
# One-shot invocation in jeo:
|
|
277
|
+
jeo $channel-notify "All 47 tests passed. Ready for review."
|
|
278
|
+
|
|
279
|
+
# In Claude Code:
|
|
280
|
+
/channel-notify "Build #108 succeeded. PR: https://github.com/..."
|
|
281
|
+
```
|
|
282
|
+
|
|
250
283
|
## Routines (GitHub Actions)
|
|
251
284
|
|
|
252
285
|
```bash
|
|
@@ -356,10 +389,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
356
389
|
|
|
357
390
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
358
391
|
- **[Unreleased]**
|
|
392
|
+
- **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
|
|
359
393
|
- **[0.11.5]** (2026-10-04) — Anthropic login now runs Opus 5.5 and Sonnet 5.5 as the selected models: the OAuth client version gate and incompatible fast-mode temperature are fixed, with real request and streaming responses verified.
|
|
360
394
|
- **[0.11.4]** (2026-10-01) — The release pipeline could not actually release: 13 tests failed on Linux CI only, and the stored npm token no longer authenticated. Both are fixed at the root — the suite is order-independent, and publishing no longer depends on a secret that can expire.
|
|
361
395
|
- **[0.11.3]** (2026-10-01) — Remote control grew from one Telegram bot into one shared daemon serving Telegram, Discord, and Slack — and the workflow gates that were meant to block unverified work (`approve`/`team` plan identity, `done` re-checks, the autopilot ratchet) now actually block it.
|
|
362
|
-
- **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
|
|
363
396
|
|
|
364
397
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
365
398
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
- **持续积累的技能** — 卡壳的一轮现在会把死胡同写入同一个技能的项目级文件(`.jeo/skills/<name>.md`,首次写入时从内置技能播种,采用确定性关键词匹配,不使用 LLM),因此下一个会话的 `$<skill>` 调用会带上累积的"Known Failure Modes"/"Anti-Patterns"知识,而不是让内置文档永远保持静态。手动记录用 `jeo skills lesson <skill> <failure|anti-pattern> "<title>" "<detail>"`;`jeo skills eval <skill>` 会运行一次真正的 LLM 判断,检查每条已记录的经验是否仍被技能当前的指引覆盖,还是已经过时。
|
|
50
50
|
- **低价位评分模型路由** — `/goal` 验证器、`critic` 子代理角色,以及未锁定的 `task` 扇出批次,现在默认使用低价位、已配置凭证的模型,而不是悄悄搭乘与它们所评分/执行的工作相同的全价模型(`resolveVerifierModel`,针对浏览器 `verify` 动作按视觉能力过滤,确保纯文本的低价模型不会悄悄丢弃附带的截图)。
|
|
51
51
|
- **`jeo routine init`** — 生成一个 GitHub Actions 工作流,在 schedule/issue/PR 触发器上以无头模式运行 jeo(`jeo "<prompt>" -p`),运行在 GitHub 自己的 runner 上 — 不需要笔记本电脑,并且不会给 jeo 自身增加任何新的攻击面(没有进程内调度器或 webhook 监听器)。`--dry-run` 可预览,`--no-pr` 则改为直接提交而非默认的每次运行一个 PR。
|
|
52
|
-
- **远程监控与控制(Telegram、Discord、Slack)** — 配对一次机器人(`jeo notify setup --provider telegram|discord|slack`)后,一个共享的 `jeo daemon start` 会在所有活跃会话中、子代理每次状态变化(启动 → 完成/失败/取消)时推送消息,并接受来自人类 allowlist 的 `/subagents`、`/steer`、`/cancel`(Discord/Slack 上还有 `/sessions` 和 `/send`)回传。Telegram 额外提供论坛主题、内联键盘和图片附件;Discord 使用 REST + Gateway WebSocket;Slack 使用 Web API + Socket Mode
|
|
52
|
+
- **远程监控与控制(Telegram、Discord、Slack)** — 配对一次机器人(`jeo notify setup --provider telegram|discord|slack`)后,一个共享的 `jeo daemon start` 会在所有活跃会话中、子代理每次状态变化(启动 → 完成/失败/取消)时推送消息,并接受来自人类 allowlist 的 `/subagents`、`/steer`、`/cancel`(Discord/Slack 上还有 `/sessions` 和 `/send`)回传。Telegram 额外提供论坛主题、内联键盘和图片附件;Discord 使用 REST + Gateway WebSocket;Slack 使用 Web API + Socket Mode。附带各平台详细配置指南及可安装的 Agent Skill(`channel-notify`)。
|
|
53
53
|
- **无法绕过的门禁** — `approve` 会重新校验它正在批准的那份计划的精确摘要,`team` 会拒绝已审阅摘要发生变化的计划;`done` 调用会对照最新的验证证据重新检查(后来的失败检查会使先前的通过失效,三次纠正往返后硬性拒绝);`autopilot` ratchet 会把失败的回滚记录为 `rollback_failed` 并以非零状态停止,而不是声称该步骤已回退。
|
|
54
54
|
- **不阻塞回合的会话级异步执行** — 通过 `task` 工具真实的 `tasks` 数组扇出独立工作,不阻塞父回合。detached 子代理、后台 job 和逐行 monitor 可在后续回合中继续使用 `subagent`/`job`/`monitor` 的 `list`、`inspect`、`await`、`cancel`、`tail` 控制。内联 TUI 为每个 worker 保留独立的实时 activity 槽位,并在会话退出或 Ctrl-C 时清理全部 registry。
|
|
55
55
|
- **独立验证器,真正强制执行** — 计划再也无法跳过 architect/critic 步骤: `PlanSchema` 会拒绝任何以未验证变更结尾的计划(把验证器放在它该检查的变更之前也不算数),在 `ralplan` 起草阶段和 `team`/`approve` 执行阶段都强制生效。每一次 architect/critic 裁决也都必须展示真实证据 — 观测到的 `read`/`search`/`find`/`ast_grep`/`lsp` 调用为零,无论文本怎么声称,裁决都会被拦下。
|
|
@@ -107,6 +107,11 @@ jeo --tmux # 在独立 tmux 会话中运行
|
|
|
107
107
|
> [!CAUTION]
|
|
108
108
|
> **通过 `/model <name>` 手动指定模型后,路由会在本会话内保持锁定。** Prompt routing(`/route`)只在没有手动锁定模型时才会逐轮重新评估。用 `/model <name>` 选定具体模型后,该选择会被锁定 — 直到你运行 `/model auto`(彻底解除锁定),或运行 `/route on`(不会清除锁定,只是优先级更高 — 一旦运行 `/route off`,锁定会立刻恢复)之前,路由都不会再切换。未配置 `roles.*` 条目时,只有 `standard` 层级会确定性地退回到 `defaultModel`;`high`/`complex` 层级通常会先实时扫描已配置凭证中最强的可用模型再决定是否回退,所以即使未配置,也可能每轮落到不同的模型上。**例外:** 通过 Antigravity 或 Gemini OAuth 授权的会话会用同一个凭证重新导出 Anthropic/Google/OpenAI 的模型 —此时 `high`/`complex` 会按公司各选一个模型、以会话为单位稳定分布(不一定是最强的那个),因此在同一会话内保持固定,而不是逐轮变化。
|
|
109
109
|
|
|
110
|
+
### Antigravity 账户 模型 实时发现
|
|
111
|
+
|
|
112
|
+
**实时模型发现** — `/model provider antigravity` 与 `/agents <角色> provider antigravity` 会显示你的 OAuth 账户在 Cloud Code Assist 实际拥有的模型。选择器仅认可账户持有的模型;并不保证每个账户都备有 Argon。空的账户列表会被诚实报告,不会被静态目录代用。账户范围在会话刷新全程得以保留。
|
|
113
|
+
|
|
114
|
+
**示例:** `jeo auth login antigravity` 会发现并缓存你账户的实际可用模型。选择: `/model antigravity/<模型-id>` 或 `/agents executor provider antigravity`,之后在本会话内使用,或通过 `/model save antigravity/<模型-id>` 设为默认值。
|
|
110
115
|
## CLI 命令
|
|
111
116
|
|
|
112
117
|
`jeo --help` 是权威列表;下面是你最常用到的:
|
|
@@ -205,6 +210,13 @@ jeo ultragoal
|
|
|
205
210
|
|
|
206
211
|
可选功能: 一个共享守护进程监视本机上所有活跃的 `jeo` 会话,并在每次**子代理状态变化**(启动 → 完成/失败/取消)时推送消息,另附会话身份头、回合开始/结束摘要以及最终的回合文本 — 绝不会重复"仍在运行"之类的通知。allowlist 中的人类可以在聊天里列出、发送、引导和取消;每一条控制都会转发给本地会话,并报告为**已确认(已接受,未必已完成)**或**未确认** — 守护进程绝不声称某条命令已执行。
|
|
207
212
|
|
|
213
|
+
各平台详细独立指南:
|
|
214
|
+
- 📖 [渠道集成概览与架构](docs/integrations/README.md) — 功能对比矩阵与架构图
|
|
215
|
+
- 📱 [Telegram 集成指南](docs/integrations/telegram.md) — 内置后台守护进程、BotFather 交互配对、论坛主题与 `/steer` 命令
|
|
216
|
+
- 🎮 [Discord 集成指南](docs/integrations/discord.md) — 即时频道通知 Webhook、双向交互机器人与 Embed 卡片规范
|
|
217
|
+
- 💬 [Slack 集成指南](docs/integrations/slack.md) — 工作区通知 Incoming Webhook、团队协作 Slack App / Socket Mode 与 Block Kit 卡片
|
|
218
|
+
- 🧩 [Agent Skill 安装指南](docs/integrations/agent-skills.md) — 将渠道通知技能一键安装至 `jeo`、Claude Code、Cursor、Codex 等环境
|
|
219
|
+
|
|
208
220
|
```bash
|
|
209
221
|
jeo notify setup [--provider telegram|discord|slack] [--token-env NAME] [--app-token-env NAME] [--chat-id ID | --channel-id ID] [--allowed-user-ids ID,ID,...]
|
|
210
222
|
jeo notify status [--provider …] # masked token, destination, allowlist, daemon state (stopped / stale / initializing / initialized / pairing)
|
|
@@ -240,6 +252,41 @@ jeo daemon start|stop|status|reload # the shared daemon has no provider select
|
|
|
240
252
|
└─────────────────────┘ session└──────────────────┘ └───────────────────────────┘
|
|
241
253
|
```
|
|
242
254
|
|
|
255
|
+
### Discord & Slack (Webhook 与 Bot App)
|
|
256
|
+
|
|
257
|
+
为团队工作区配置标准 Webhook 或 Bot 凭证环境变量:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
# Discord Webhook
|
|
261
|
+
export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..."
|
|
262
|
+
|
|
263
|
+
# Slack Incoming Webhook
|
|
264
|
+
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
支持发送包含任务状态、耗时、Git 提交与 PR 链接的富文本 Embed(Discord) 及 Block Kit 卡片(Slack)。详见 [Discord 指南](docs/integrations/discord.md) 与 [Slack 指南](docs/integrations/slack.md)。
|
|
268
|
+
|
|
269
|
+
### 安装为 Agent Skill
|
|
270
|
+
|
|
271
|
+
使用标准的 [Agent Skills](https://agentskills.io) CLI,可将渠道通知技能快速安装到 `jeo` 或其它 AI 编码代理中:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# 安装 channel-notify 技能到当前仓库:
|
|
275
|
+
npx skills add akillness/jeo-code --skill channel-notify
|
|
276
|
+
|
|
277
|
+
# 或全局(-g)安装,供所有项目使用:
|
|
278
|
+
npx skills add akillness/jeo-code --skill channel-notify -g
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
安装后,代理可在执行任务时自主发送进度通知:
|
|
282
|
+
```bash
|
|
283
|
+
# 在 jeo 中单次调用:
|
|
284
|
+
jeo $channel-notify "所有 47 个测试均已通过,等待代码审查。"
|
|
285
|
+
|
|
286
|
+
# 在 Claude Code 中调用:
|
|
287
|
+
/channel-notify "构建成功。PR 链接: https://github.com/..."
|
|
288
|
+
```
|
|
289
|
+
|
|
243
290
|
## 例行任务 (GitHub Actions)
|
|
244
291
|
|
|
245
292
|
```bash
|
|
@@ -346,10 +393,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
346
393
|
|
|
347
394
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
348
395
|
- **[Unreleased]**
|
|
396
|
+
- **[0.11.6]** (2026-10-16) — Live Antigravity account model discovery now serves the actual account's available models instead of assuming static availability.
|
|
349
397
|
- **[0.11.5]** (2026-10-04) — Anthropic login now runs Opus 5.5 and Sonnet 5.5 as the selected models: the OAuth client version gate and incompatible fast-mode temperature are fixed, with real request and streaming responses verified.
|
|
350
398
|
- **[0.11.4]** (2026-10-01) — The release pipeline could not actually release: 13 tests failed on Linux CI only, and the stored npm token no longer authenticated. Both are fixed at the root — the suite is order-independent, and publishing no longer depends on a secret that can expire.
|
|
351
399
|
- **[0.11.3]** (2026-10-01) — Remote control grew from one Telegram bot into one shared daemon serving Telegram, Discord, and Slack — and the workflow gates that were meant to block unverified work (`approve`/`team` plan identity, `done` re-checks, the autopilot ratchet) now actually block it.
|
|
352
|
-
- **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
|
|
353
400
|
|
|
354
401
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
355
402
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/ai/model-cache.ts
CHANGED
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
* corrupt, or stale file only means live discovery must refresh the catalog,
|
|
15
15
|
* never an error.
|
|
16
16
|
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
17
18
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
18
19
|
import * as os from "node:os";
|
|
19
20
|
import * as path from "node:path";
|
|
20
21
|
import { jeoEnv } from "../util/env";
|
|
21
|
-
import { recordLiveCodexModels, recordLiveProviderModels, setOpenAIOauthAccountScope } from "./model-catalog";
|
|
22
|
-
import {
|
|
22
|
+
import { recordLiveCodexModels, recordLiveProviderModels, setAntigravityAccountScope, setOpenAIOauthAccountScope } from "./model-catalog";
|
|
23
|
+
import type { Credential } from "../auth";
|
|
24
|
+
import { getStoredOAuth, snapshotProvider } from "../auth/storage";
|
|
23
25
|
import { extractChatgptAccountId } from "./providers/openai-responses";
|
|
24
26
|
import type { ProviderName } from "./types";
|
|
25
27
|
|
|
@@ -32,7 +34,7 @@ export interface CachedProviderModels {
|
|
|
32
34
|
source: ModelCacheSource;
|
|
33
35
|
/** Base URL the list came from, so an OpenAI-compatible endpoint's ids stay scoped to it. */
|
|
34
36
|
baseUrl?: string;
|
|
35
|
-
/** Stable
|
|
37
|
+
/** Stable account scope for OAuth rows; required for OpenAI and Antigravity rows. */
|
|
36
38
|
accountId?: string;
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -46,7 +48,7 @@ export interface ModelCacheFile {
|
|
|
46
48
|
const CACHE_VERSION = 3;
|
|
47
49
|
/** Refresh in the background once the cache is older than this (6h). */
|
|
48
50
|
export const MODEL_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
49
|
-
/** Bound
|
|
51
|
+
/** Bound non-authoritative provider rows; preserve complete OpenAI and Antigravity lists. */
|
|
50
52
|
const MAX_MODELS_PER_PROVIDER = 500;
|
|
51
53
|
|
|
52
54
|
function cacheDir(): string {
|
|
@@ -57,6 +59,40 @@ function cachePath(): string {
|
|
|
57
59
|
return path.join(cacheDir(), "model-catalog-cache.json");
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/** Peek at the selected OAuth account without importing or refreshing credentials. */
|
|
63
|
+
export async function antigravityAccountScope(credential?: Credential): Promise<string | undefined> {
|
|
64
|
+
try {
|
|
65
|
+
let selected = credential;
|
|
66
|
+
if (!selected) {
|
|
67
|
+
const own = await snapshotProvider("antigravity");
|
|
68
|
+
const provider = own.oauth ? "antigravity" : "gemini";
|
|
69
|
+
const snapshot = own.oauth ? own : await snapshotProvider("gemini");
|
|
70
|
+
if (!snapshot.oauth) return undefined;
|
|
71
|
+
selected = { kind: "oauth", provider, token: snapshot.oauth };
|
|
72
|
+
}
|
|
73
|
+
if (selected.kind !== "oauth" || (selected.provider !== "antigravity" && selected.provider !== "gemini")) return undefined;
|
|
74
|
+
const providerSlot = selected.provider;
|
|
75
|
+
const stored = await getStoredOAuth(providerSlot).catch(() => undefined);
|
|
76
|
+
const accessMatches = stored?.access === selected.token;
|
|
77
|
+
const email = accessMatches ? stored?.email?.trim().toLowerCase() : "";
|
|
78
|
+
const identity = accessMatches
|
|
79
|
+
? email
|
|
80
|
+
? `email:${email}`
|
|
81
|
+
: stored?.refresh
|
|
82
|
+
? `refresh:${stored.refresh}`
|
|
83
|
+
: `access:${selected.token}`
|
|
84
|
+
: `access:${selected.token}`;
|
|
85
|
+
const projectId = selected.projectId?.trim()
|
|
86
|
+
|| (accessMatches ? stored?.projectId?.trim() : undefined)
|
|
87
|
+
|| process.env.GOOGLE_CLOUD_PROJECT?.trim()
|
|
88
|
+
|| process.env.GOOGLE_CLOUD_PROJECT_ID?.trim()
|
|
89
|
+
|| "";
|
|
90
|
+
return createHash("sha256").update(`${providerSlot}\u0000${identity}\u0000${projectId}`, "utf8").digest("hex");
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
60
96
|
export function normalizeCacheEntries(raw: unknown): CachedProviderModels[] {
|
|
61
97
|
if (!Array.isArray(raw)) return [];
|
|
62
98
|
const out: CachedProviderModels[] = [];
|
|
@@ -64,19 +100,21 @@ export function normalizeCacheEntries(raw: unknown): CachedProviderModels[] {
|
|
|
64
100
|
const row = entry as Partial<CachedProviderModels>;
|
|
65
101
|
if (!row || typeof row.provider !== "string" || !Array.isArray(row.models)) continue;
|
|
66
102
|
const provider = row.provider as ProviderName;
|
|
67
|
-
const models = row.models.filter((m): m is string => typeof m === "string" && m.trim().length > 0).slice(0, provider === "openai" ? undefined : MAX_MODELS_PER_PROVIDER);
|
|
68
|
-
if (models.length === 0 && (provider !== "openai" || row.models.length !== 0)) continue;
|
|
69
103
|
const source: ModelCacheSource =
|
|
70
104
|
row.source === "oauth" || row.source === "api_key" || row.source === "keyless" || row.source === "none" ? row.source : "none";
|
|
71
105
|
const baseUrl = typeof row.baseUrl === "string" ? row.baseUrl.replace(/\/+$/, "") : undefined;
|
|
72
106
|
const accountId = typeof row.accountId === "string" ? row.accountId.trim() : "";
|
|
73
|
-
|
|
107
|
+
const isScopedOAuth = (provider === "openai" || provider === "antigravity") && source === "oauth";
|
|
108
|
+
if ((isScopedOAuth && !accountId) || (provider === "antigravity" && source !== "oauth")) continue;
|
|
109
|
+
const models = row.models.filter((m): m is string => typeof m === "string" && m.trim().length > 0)
|
|
110
|
+
.slice(0, provider === "openai" || provider === "antigravity" ? undefined : MAX_MODELS_PER_PROVIDER);
|
|
111
|
+
if (models.length === 0 && provider !== "openai" && provider !== "antigravity") continue;
|
|
74
112
|
out.push({
|
|
75
113
|
provider,
|
|
76
114
|
models,
|
|
77
115
|
source,
|
|
78
116
|
...(baseUrl ? { baseUrl } : {}),
|
|
79
|
-
...(
|
|
117
|
+
...(isScopedOAuth ? { accountId } : {}),
|
|
80
118
|
});
|
|
81
119
|
}
|
|
82
120
|
return out;
|
|
@@ -115,26 +153,29 @@ export function mergeCacheEntries(
|
|
|
115
153
|
results: readonly { provider: ProviderName; models: readonly string[]; ok: boolean; source?: ModelCacheSource; baseUrl?: string; accountId?: string; fallback?: boolean }[],
|
|
116
154
|
): CachedProviderModels[] {
|
|
117
155
|
const byKey = new Map<string, CachedProviderModels>();
|
|
118
|
-
const
|
|
119
|
-
|
|
156
|
+
const isScopedOAuth = (provider: ProviderName, source: ModelCacheSource): boolean =>
|
|
157
|
+
(provider === "openai" || provider === "antigravity") && source === "oauth";
|
|
158
|
+
const keyOf = (provider: ProviderName, baseUrl?: string, accountId?: string, source?: ModelCacheSource) =>
|
|
159
|
+
`${provider}\u0000${baseUrl?.replace(/\/+$/, "") ?? ""}\u0000${isScopedOAuth(provider, source ?? "none") ? accountId ?? "" : ""}\u0000${provider === "openai" || provider === "antigravity" ? source ?? "none" : ""}`;
|
|
120
160
|
for (const entry of previous) {
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
byKey.set(keyOf(entry.provider, entry.baseUrl,
|
|
161
|
+
const scoped = isScopedOAuth(entry.provider, entry.source);
|
|
162
|
+
if ((scoped && !entry.accountId) || (entry.provider === "antigravity" && entry.source !== "oauth")) continue;
|
|
163
|
+
byKey.set(keyOf(entry.provider, entry.baseUrl, entry.accountId, entry.source), entry);
|
|
124
164
|
}
|
|
125
165
|
for (const result of results) {
|
|
126
|
-
if (!result.ok || result.fallback || (result.models.length === 0 && result.provider !== "openai")) continue;
|
|
127
166
|
const source: ModelCacheSource = result.source ?? "none";
|
|
128
|
-
const
|
|
167
|
+
const scoped = isScopedOAuth(result.provider, source);
|
|
168
|
+
const isAntigravity = result.provider === "antigravity";
|
|
129
169
|
const baseUrl = typeof result.baseUrl === "string" ? result.baseUrl.replace(/\/+$/, "") : undefined;
|
|
130
170
|
const accountId = typeof result.accountId === "string" ? result.accountId.trim() : "";
|
|
131
|
-
if (
|
|
132
|
-
|
|
171
|
+
if ((scoped && !accountId) || (isAntigravity && source !== "oauth")) continue;
|
|
172
|
+
if (!result.ok || result.fallback || (result.models.length === 0 && result.provider !== "openai" && !isAntigravity)) continue;
|
|
173
|
+
byKey.set(keyOf(result.provider, baseUrl, accountId, source), {
|
|
133
174
|
provider: result.provider,
|
|
134
|
-
models: [...result.models].slice(0, result.provider === "openai" ? undefined : MAX_MODELS_PER_PROVIDER),
|
|
175
|
+
models: [...result.models].slice(0, result.provider === "openai" || isAntigravity ? undefined : MAX_MODELS_PER_PROVIDER),
|
|
135
176
|
source,
|
|
136
177
|
...(baseUrl ? { baseUrl } : {}),
|
|
137
|
-
...(
|
|
178
|
+
...(scoped ? { accountId } : {}),
|
|
138
179
|
});
|
|
139
180
|
}
|
|
140
181
|
return [...byKey.values()];
|
|
@@ -148,9 +189,12 @@ export async function writeModelCache(
|
|
|
148
189
|
// explicitly sandboxed JEO_CONFIG_DIR, never the developer's real ~/.jeo.
|
|
149
190
|
if (process.env.NODE_ENV === "test" && !jeoEnv("CONFIG_DIR")) return;
|
|
150
191
|
// A failed/fallback-only refresh must not make an old snapshot appear fresh.
|
|
151
|
-
const hasFreshResult = results.some(result =>
|
|
152
|
-
|
|
153
|
-
|
|
192
|
+
const hasFreshResult = results.some(result => {
|
|
193
|
+
if (!result.ok || result.fallback) return false;
|
|
194
|
+
if (result.provider === "antigravity") return result.source === "oauth" && !!result.accountId?.trim();
|
|
195
|
+
if (result.provider === "openai") return result.models.length > 0 || !(result.source === "oauth" && !result.accountId?.trim());
|
|
196
|
+
return result.models.length > 0;
|
|
197
|
+
});
|
|
154
198
|
if (!hasFreshResult) return;
|
|
155
199
|
try {
|
|
156
200
|
const existing = await readModelCache();
|
|
@@ -169,18 +213,23 @@ export async function writeModelCache(
|
|
|
169
213
|
* routing, and — critically — the OAuth Codex gate know the account's real models
|
|
170
214
|
* BEFORE the first network call of this launch. Returns the number of ids applied.
|
|
171
215
|
*/
|
|
172
|
-
export function applyCachedModels(cache: ModelCacheFile | null, openAIOauthAccountId?: string): number {
|
|
216
|
+
export function applyCachedModels(cache: ModelCacheFile | null, openAIOauthAccountId?: string, antigravityAccountId?: string): number {
|
|
217
|
+
const openAIScope = typeof openAIOauthAccountId === "string" ? openAIOauthAccountId.trim() : "";
|
|
218
|
+
const antigravityScope = typeof antigravityAccountId === "string" ? antigravityAccountId.trim() : "";
|
|
219
|
+
setAntigravityAccountScope(antigravityScope);
|
|
173
220
|
if (!cache) return 0;
|
|
174
|
-
const accountId = typeof openAIOauthAccountId === "string" ? openAIOauthAccountId.trim() : "";
|
|
175
221
|
let applied = 0;
|
|
176
222
|
for (const entry of cache.providers) {
|
|
177
223
|
const isOpenAIOAuth = entry.provider === "openai" && entry.source === "oauth";
|
|
178
|
-
|
|
224
|
+
const isAntigravityOAuth = entry.provider === "antigravity" && entry.source === "oauth";
|
|
225
|
+
if (isOpenAIOAuth && (!openAIScope || entry.accountId !== openAIScope)) continue;
|
|
226
|
+
if (isAntigravityOAuth && (!antigravityScope || entry.accountId !== antigravityScope)) continue;
|
|
227
|
+
if (entry.provider === "antigravity" && !isAntigravityOAuth) continue;
|
|
179
228
|
recordLiveProviderModels(entry.provider, entry.models, {
|
|
180
229
|
source: entry.source,
|
|
181
230
|
baseUrl: entry.baseUrl,
|
|
182
|
-
replace: entry.provider === "openai",
|
|
183
|
-
...(isOpenAIOAuth
|
|
231
|
+
replace: entry.provider === "openai" || entry.provider === "antigravity",
|
|
232
|
+
...(isOpenAIOAuth || isAntigravityOAuth ? { accountId: entry.accountId } : {}),
|
|
184
233
|
});
|
|
185
234
|
if (isOpenAIOAuth && entry.accountId && !entry.baseUrl) {
|
|
186
235
|
recordLiveCodexModels(entry.models, entry.accountId);
|
|
@@ -201,7 +250,9 @@ export async function rehydrateLiveModels(): Promise<ModelCacheFile | null> {
|
|
|
201
250
|
} catch {
|
|
202
251
|
accountId = undefined;
|
|
203
252
|
}
|
|
253
|
+
const antigravityAccountId = await antigravityAccountScope();
|
|
204
254
|
setOpenAIOauthAccountScope(accountId);
|
|
205
|
-
|
|
255
|
+
setAntigravityAccountScope(antigravityAccountId);
|
|
256
|
+
applyCachedModels(cache, accountId, antigravityAccountId);
|
|
206
257
|
return cache;
|
|
207
258
|
}
|
package/src/ai/model-catalog.ts
CHANGED
|
@@ -202,6 +202,7 @@ export const CODEX_MODELS: readonly string[] = ["gpt-5.5", "gpt-5.4", "gpt-5.4-m
|
|
|
202
202
|
const EPHEMERAL_OPENAI_OAUTH_SCOPE = "__jeo-openai-oauth-ephemeral__";
|
|
203
203
|
let activeOpenAIOauthScope = EPHEMERAL_OPENAI_OAUTH_SCOPE;
|
|
204
204
|
const liveCodexModels = new Map<string, Set<string>>();
|
|
205
|
+
let activeAntigravityAccountScope: string | undefined;
|
|
205
206
|
|
|
206
207
|
function normalizeOpenAIOauthScope(scope: string | undefined): string {
|
|
207
208
|
return typeof scope === "string" ? scope.trim() : "";
|
|
@@ -236,6 +237,18 @@ export function setOpenAIOauthAccountScope(accountId: string | undefined): void
|
|
|
236
237
|
}
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
/** Select the current Antigravity OAuth account and evict every other account's live rows. */
|
|
241
|
+
export function setAntigravityAccountScope(accountId: string | undefined): void {
|
|
242
|
+
const nextScope = normalizeOpenAIOauthScope(accountId) || undefined;
|
|
243
|
+
activeAntigravityAccountScope = nextScope;
|
|
244
|
+
for (const [recordKey, record] of liveProviderModels.entries()) {
|
|
245
|
+
if (record.row.provider === "antigravity" && record.source === "oauth"
|
|
246
|
+
&& normalizeOpenAIOauthScope(record.accountId) !== nextScope) {
|
|
247
|
+
removeLiveProviderRecord(recordKey, record);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
239
252
|
/** Record ids confirmed by the current account's Codex Responses endpoint. */
|
|
240
253
|
export function recordLiveCodexModels(ids: readonly string[], accountId?: string): void {
|
|
241
254
|
const explicitScope = normalizeOpenAIOauthScope(accountId);
|
|
@@ -338,8 +351,8 @@ function liveModelRecordKey(
|
|
|
338
351
|
accountId?: string,
|
|
339
352
|
source?: LiveProviderModelSource,
|
|
340
353
|
): string {
|
|
341
|
-
const normalizedAccountId = provider === "openai" ? normalizeOpenAIOauthScope(accountId) : "";
|
|
342
|
-
return `${provider}\u0000${normalizeBaseUrl(baseUrl) ?? ""}\u0000${normalizedAccountId}\u0000${provider === "openai" ? source ?? "none" : ""}\u0000${canonical}`;
|
|
354
|
+
const normalizedAccountId = (provider === "openai" || provider === "antigravity") ? normalizeOpenAIOauthScope(accountId) : "";
|
|
355
|
+
return `${provider}\u0000${normalizeBaseUrl(baseUrl) ?? ""}\u0000${normalizedAccountId}\u0000${provider === "openai" || provider === "antigravity" ? source ?? "none" : ""}\u0000${canonical}`;
|
|
343
356
|
}
|
|
344
357
|
|
|
345
358
|
function liveModelFallbackRow(provider: ProviderName, canonical: string, providerModel: string): CatalogModel {
|
|
@@ -358,8 +371,8 @@ function liveModelFallbackRow(provider: ProviderName, canonical: string, provide
|
|
|
358
371
|
};
|
|
359
372
|
}
|
|
360
373
|
|
|
361
|
-
/** Record live ids. Complete OpenAI refreshes
|
|
362
|
-
*
|
|
374
|
+
/** Record live ids. Complete OpenAI and Antigravity refreshes replace their matching scope;
|
|
375
|
+
* partial observations and other providers remain additive. */
|
|
363
376
|
export function recordLiveProviderModels(
|
|
364
377
|
provider: ProviderName,
|
|
365
378
|
ids: readonly string[],
|
|
@@ -367,16 +380,26 @@ export function recordLiveProviderModels(
|
|
|
367
380
|
): void {
|
|
368
381
|
const source = opts.source ?? "none";
|
|
369
382
|
const baseUrl = normalizeBaseUrl(opts.baseUrl);
|
|
370
|
-
const scopedAccountId = provider === "openai" && source === "oauth"
|
|
383
|
+
const scopedAccountId = (provider === "openai" || provider === "antigravity") && source === "oauth"
|
|
371
384
|
? normalizeOpenAIOauthScope(opts.accountId) || undefined
|
|
372
385
|
: undefined;
|
|
386
|
+
// A complete discovery refresh must not reactivate an account that was
|
|
387
|
+
// superseded while its request was in flight. Non-refresh observations may
|
|
388
|
+
// remain hidden until that account is explicitly selected.
|
|
389
|
+
if (provider === "antigravity" && (!scopedAccountId || source !== "oauth")) return;
|
|
390
|
+
if (provider === "antigravity" && opts.replace && scopedAccountId !== activeAntigravityAccountScope) return;
|
|
373
391
|
if (provider === "openai" && source === "oauth" && scopedAccountId) {
|
|
374
392
|
setOpenAIOauthAccountScope(scopedAccountId);
|
|
375
393
|
}
|
|
376
|
-
|
|
394
|
+
const shouldReplace = provider === "antigravity" || (provider === "openai" && !!opts.replace);
|
|
395
|
+
if (shouldReplace) {
|
|
377
396
|
for (const [key, record] of liveProviderModels) {
|
|
378
|
-
|
|
379
|
-
|
|
397
|
+
const sameScope = provider === "openai"
|
|
398
|
+
? oauthScope(record.accountId) === oauthScope(scopedAccountId)
|
|
399
|
+
: provider === "antigravity"
|
|
400
|
+
? normalizeOpenAIOauthScope(record.accountId) === normalizeOpenAIOauthScope(scopedAccountId)
|
|
401
|
+
: true;
|
|
402
|
+
if (record.row.provider === provider && record.source === source && record.baseUrl === baseUrl && sameScope) {
|
|
380
403
|
removeLiveProviderRecord(key, record);
|
|
381
404
|
}
|
|
382
405
|
}
|
|
@@ -405,6 +428,10 @@ function liveRecordMatchesConfig(
|
|
|
405
428
|
record: LiveProviderModelRecord,
|
|
406
429
|
config?: { openaiBaseUrl?: string; openaiOauthScope?: string },
|
|
407
430
|
): boolean {
|
|
431
|
+
if (record.row.provider === "antigravity") {
|
|
432
|
+
return record.source === "oauth" && activeAntigravityAccountScope !== undefined
|
|
433
|
+
&& normalizeOpenAIOauthScope(record.accountId) === activeAntigravityAccountScope;
|
|
434
|
+
}
|
|
408
435
|
if (record.row.provider !== "openai") return true;
|
|
409
436
|
const wanted = normalizeBaseUrl(config?.openaiBaseUrl);
|
|
410
437
|
if (wanted || record.baseUrl) {
|
|
@@ -434,7 +461,8 @@ export function liveProviderCatalogModels(config?: { openaiBaseUrl?: string; ope
|
|
|
434
461
|
export function findLiveCatalogModel(canonical: string): CatalogModel | undefined {
|
|
435
462
|
for (const record of liveProviderModels.values()) {
|
|
436
463
|
if (record.row.canonical !== canonical) continue;
|
|
437
|
-
if (record.row.provider === "openai" && record.source === "oauth" && !liveRecordMatchesConfig(record))
|
|
464
|
+
if ((record.row.provider === "openai" && record.source === "oauth" && !liveRecordMatchesConfig(record))
|
|
465
|
+
|| (record.row.provider === "antigravity" && !liveRecordMatchesConfig(record))) continue;
|
|
438
466
|
return record.row;
|
|
439
467
|
}
|
|
440
468
|
return undefined;
|
|
@@ -461,6 +489,7 @@ export function isLiveProviderModel(provider: ProviderName, model: string, confi
|
|
|
461
489
|
export function resetLiveProviderModels(): void {
|
|
462
490
|
liveProviderModels.clear();
|
|
463
491
|
liveModelIdIndex.clear();
|
|
492
|
+
activeAntigravityAccountScope = undefined;
|
|
464
493
|
}
|
|
465
494
|
|
|
466
495
|
|
|
@@ -17,10 +17,14 @@ import {
|
|
|
17
17
|
recordLiveCodexModels,
|
|
18
18
|
recordLiveProviderModels,
|
|
19
19
|
setOpenAIOauthAccountScope,
|
|
20
|
+
setAntigravityAccountScope,
|
|
20
21
|
} from "./model-catalog";
|
|
21
22
|
import { extractChatgptAccountId } from "./providers/openai-responses";
|
|
22
23
|
import { getStoredOAuth } from "../auth/storage";
|
|
23
24
|
import { openaiCompatDef } from "./providers/openai-compatible-catalog";
|
|
25
|
+
import { antigravityAccountScope } from "./model-cache";
|
|
26
|
+
import { getAntigravityUserAgent, resolveAntigravityProjectId } from "./providers/antigravity";
|
|
27
|
+
import { discoverGoogleProjectId, ANTIGRAVITY_DISCOVERY_METADATA } from "../auth/flows/google-project";
|
|
24
28
|
|
|
25
29
|
export interface ProviderModelsResult {
|
|
26
30
|
provider: ProviderName;
|
|
@@ -29,7 +33,7 @@ export interface ProviderModelsResult {
|
|
|
29
33
|
ok: boolean;
|
|
30
34
|
/** How the request authenticated (for display). */
|
|
31
35
|
source: "oauth" | "api_key" | "keyless" | "none";
|
|
32
|
-
/**
|
|
36
|
+
/** Opaque OAuth account scope for account-specific cache rows. */
|
|
33
37
|
accountId?: string;
|
|
34
38
|
/** Actual custom endpoint queried, for endpoint-scoped persistent caches. */
|
|
35
39
|
baseUrl?: string;
|
|
@@ -46,7 +50,7 @@ export interface DiscoveryOptions {
|
|
|
46
50
|
/** Per-request timeout; default 5000ms. */
|
|
47
51
|
timeoutMs?: number;
|
|
48
52
|
signal?: AbortSignal;
|
|
49
|
-
/** Cap returned ids; OpenAI
|
|
53
|
+
/** Cap returned ids; OpenAI and Antigravity default to the full account list. */
|
|
50
54
|
limit?: number;
|
|
51
55
|
/** Config snapshot used for provider base URLs. */
|
|
52
56
|
config?: Config;
|
|
@@ -147,9 +151,9 @@ export function discoveryRequest(
|
|
|
147
151
|
const token = cred?.kind === "oauth" ? cred.token : "";
|
|
148
152
|
return {
|
|
149
153
|
url: ANTIGRAVITY_MODELS_URL,
|
|
150
|
-
headers: token ? { authorization: `Bearer ${token}`, "content-type": "application/json", "User-Agent":
|
|
154
|
+
headers: token ? { authorization: `Bearer ${token}`, "content-type": "application/json", "User-Agent": getAntigravityUserAgent() } : {},
|
|
151
155
|
method: "POST",
|
|
152
|
-
body: "{}
|
|
156
|
+
body: JSON.stringify(cred?.kind === "oauth" && cred.projectId ? { project: cred.projectId } : {}),
|
|
153
157
|
};
|
|
154
158
|
}
|
|
155
159
|
case "ollama": {
|
|
@@ -267,7 +271,7 @@ export function parseModelsBody(provider: ProviderName, body: unknown, opts: { o
|
|
|
267
271
|
.filter(([id, model]) =>
|
|
268
272
|
id.length > 0 &&
|
|
269
273
|
model?.isInternal !== true &&
|
|
270
|
-
(
|
|
274
|
+
(Array.isArray(payload.agentModelSorts) ? agentIds.has(id) : !nonChat.has(id)))
|
|
271
275
|
.map(([id]) => `antigravity/${id}`);
|
|
272
276
|
}
|
|
273
277
|
if (provider === "gemini") {
|
|
@@ -342,7 +346,7 @@ export async function listProviderModels(
|
|
|
342
346
|
opts: DiscoveryOptions = {},
|
|
343
347
|
): Promise<ProviderModelsResult> {
|
|
344
348
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
345
|
-
const limit = opts.limit ?? (provider === "openai" ? Number.POSITIVE_INFINITY : DEFAULT_LIMIT);
|
|
349
|
+
const limit = opts.limit ?? (provider === "openai" || provider === "antigravity" ? Number.POSITIVE_INFINITY : DEFAULT_LIMIT);
|
|
346
350
|
let baseUrl = opts.baseUrl;
|
|
347
351
|
|
|
348
352
|
let cred: Credential | undefined;
|
|
@@ -376,6 +380,7 @@ export async function listProviderModels(
|
|
|
376
380
|
}
|
|
377
381
|
}
|
|
378
382
|
if (cred.kind !== "oauth") {
|
|
383
|
+
setAntigravityAccountScope(undefined);
|
|
379
384
|
return { provider, models: [], ok: false, source, error: "not logged in with Antigravity OAuth" };
|
|
380
385
|
}
|
|
381
386
|
}
|
|
@@ -417,11 +422,41 @@ export async function listProviderModels(
|
|
|
417
422
|
}
|
|
418
423
|
}
|
|
419
424
|
|
|
420
|
-
const { url, headers, method, body: requestBody } = discoveryRequest(provider, cred, baseUrl, oauthAccountId);
|
|
421
425
|
const scope = { ...(oauthAccountId ? { accountId: oauthAccountId } : {}), ...(baseUrl ? { baseUrl } : {}) };
|
|
422
426
|
const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
423
|
-
|
|
427
|
+
let signal = opts.signal ? AbortSignal.any([opts.signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
|
|
428
|
+
let initialAntigravityScope: string | undefined;
|
|
424
429
|
try {
|
|
430
|
+
if (provider === "antigravity" && cred?.kind === "oauth") {
|
|
431
|
+
initialAntigravityScope = await antigravityAccountScope(cred);
|
|
432
|
+
scope.accountId = initialAntigravityScope;
|
|
433
|
+
try {
|
|
434
|
+
const projectId = await resolveAntigravityProjectId(cred, {
|
|
435
|
+
signal,
|
|
436
|
+
discover: token => discoverGoogleProjectId(token, {
|
|
437
|
+
fetchImpl,
|
|
438
|
+
metadata: { ...ANTIGRAVITY_DISCOVERY_METADATA },
|
|
439
|
+
extraHeaders: { "User-Agent": getAntigravityUserAgent() },
|
|
440
|
+
alwaysOnboard: true,
|
|
441
|
+
signal,
|
|
442
|
+
requestTimeoutMs: timeout,
|
|
443
|
+
}),
|
|
444
|
+
});
|
|
445
|
+
cred = { ...cred, projectId };
|
|
446
|
+
} catch (error) {
|
|
447
|
+
// The OAuth models endpoint also accepts {}. Project lookup is optional
|
|
448
|
+
// for listing, so an onboarding failure must not hide account models.
|
|
449
|
+
if (opts.signal?.aborted) throw error;
|
|
450
|
+
}
|
|
451
|
+
signal = opts.signal ? AbortSignal.any([opts.signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
|
|
452
|
+
scope.accountId = await antigravityAccountScope(cred);
|
|
453
|
+
const currentScope = await antigravityAccountScope();
|
|
454
|
+
if (currentScope !== scope.accountId && currentScope !== initialAntigravityScope) {
|
|
455
|
+
return { provider, models: [], ok: false, source, error: "account changed during model discovery", ...scope };
|
|
456
|
+
}
|
|
457
|
+
setAntigravityAccountScope(scope.accountId);
|
|
458
|
+
}
|
|
459
|
+
const { url, headers, method, body: requestBody } = discoveryRequest(provider, cred, baseUrl, oauthAccountId);
|
|
425
460
|
const res = await fetchImpl(url, { method: method ?? "GET", headers, body: requestBody, signal });
|
|
426
461
|
if (!res.ok) {
|
|
427
462
|
const reason = res.status === 401 || res.status === 403 ? "auth rejected" : `HTTP ${res.status}`;
|
|
@@ -436,6 +471,30 @@ export async function listProviderModels(
|
|
|
436
471
|
return { provider, models: [], ok: false, source, error: "invalid models response", ...scope };
|
|
437
472
|
}
|
|
438
473
|
}
|
|
474
|
+
if (provider === "antigravity") {
|
|
475
|
+
const payload = body as ({ models?: unknown; agentModelSorts?: unknown } & Record<string, unknown>) | null;
|
|
476
|
+
const models = payload?.models;
|
|
477
|
+
const rows = models && typeof models === "object" ? Object.entries(models) : undefined;
|
|
478
|
+
const sorts = payload?.agentModelSorts;
|
|
479
|
+
const invalidSorts = sorts !== undefined && (!Array.isArray(sorts) || sorts.some(sort =>
|
|
480
|
+
!sort || typeof sort !== "object" || !Array.isArray(sort.groups) || sort.groups.some((group: { modelIds?: unknown } | null) =>
|
|
481
|
+
!group || !Array.isArray(group.modelIds) || group.modelIds.some(id => typeof id !== "string" || !id.trim()))));
|
|
482
|
+
const invalidRoles = ["tabModelIds", "imageGenerationModelIds", "audioTranscriptionModelIds", "commitMessageModelIds", "mqueryModelIds"].some(field => {
|
|
483
|
+
const ids = payload?.[field];
|
|
484
|
+
return ids !== undefined && (!Array.isArray(ids) || ids.some(id => typeof id !== "string" || !id.trim()));
|
|
485
|
+
});
|
|
486
|
+
const deprecated = payload?.deprecatedModelIds;
|
|
487
|
+
const invalidDeprecated = deprecated !== undefined && (Array.isArray(deprecated)
|
|
488
|
+
? deprecated.some(id => typeof id !== "string" || !id.trim())
|
|
489
|
+
: !deprecated || typeof deprecated !== "object");
|
|
490
|
+
if (!rows || rows.some(([id, row]) => !row || typeof row !== "object" || Array.isArray(row)
|
|
491
|
+
|| (Array.isArray(models)
|
|
492
|
+
? ![row.slug, row.id, row.name].some(value => typeof value === "string" && value.trim().length > 0)
|
|
493
|
+
: !id.trim()))
|
|
494
|
+
|| invalidSorts || invalidRoles || invalidDeprecated) {
|
|
495
|
+
return { provider, models: [], ok: false, source, error: "invalid models response", ...scope };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
439
498
|
let ids = parseModelsBody(provider, body, { openaiCompatible: provider === "openai" && !!baseUrl });
|
|
440
499
|
// Gemini paginates: follow nextPageToken (bounded) so the available list is
|
|
441
500
|
// COMPLETE — page 1 alone silently dropped the newest models (round-15).
|
|
@@ -452,14 +511,21 @@ export async function listProviderModels(
|
|
|
452
511
|
}
|
|
453
512
|
const allModels = [...new Set(ids)].sort();
|
|
454
513
|
const models = allModels.slice(0, limit);
|
|
514
|
+
if (provider === "antigravity") {
|
|
515
|
+
const currentScope = await antigravityAccountScope();
|
|
516
|
+
if (currentScope !== scope.accountId && currentScope !== initialAntigravityScope) {
|
|
517
|
+
return { provider, models: [], ok: false, source, error: "account changed during model discovery", ...scope };
|
|
518
|
+
}
|
|
519
|
+
setAntigravityAccountScope(scope.accountId);
|
|
520
|
+
}
|
|
455
521
|
// Record live ids for routing supplements. For OpenAI OAuth, keep the
|
|
456
522
|
// existing Codex allow-list widening separate: API-key/custom-base discovery
|
|
457
523
|
// must not make OAuth Codex calls accept unrelated API models.
|
|
458
524
|
recordLiveProviderModels(provider, allModels, {
|
|
459
525
|
source,
|
|
460
526
|
baseUrl,
|
|
461
|
-
replace: provider === "openai",
|
|
462
|
-
...(
|
|
527
|
+
replace: provider === "openai" || provider === "antigravity",
|
|
528
|
+
...(source === "oauth" && scope.accountId ? { accountId: scope.accountId } : {}),
|
|
463
529
|
});
|
|
464
530
|
if (provider === "openai" && source === "oauth" && !baseUrl) recordLiveCodexModels(allModels, oauthAccountId);
|
|
465
531
|
return {
|
|
@@ -470,7 +536,7 @@ export async function listProviderModels(
|
|
|
470
536
|
...scope,
|
|
471
537
|
};
|
|
472
538
|
} catch (err) {
|
|
473
|
-
const msg = (err as Error)?.name === "TimeoutError" || (err as Error)?.name === "AbortError" ? "timeout" : "unreachable";
|
|
539
|
+
const msg = signal.aborted || (err as Error)?.name === "TimeoutError" || (err as Error)?.name === "AbortError" ? "timeout" : "unreachable";
|
|
474
540
|
return { provider, models: [], ok: false, source, error: msg, ...scope };
|
|
475
541
|
}
|
|
476
542
|
}
|
|
@@ -6,6 +6,7 @@ import { providerHttpError, ProviderStreamError } from "./errors";
|
|
|
6
6
|
import { serializeToolCalls } from "../../agent/tool-schemas";
|
|
7
7
|
import { sanitizeJsonStrings } from "../../util/sanitize-json";
|
|
8
8
|
import { geminiThinkingConfig, type GeminiThinkingConfig } from "./gemini";
|
|
9
|
+
import { saveConfigPatch } from "../../agent/state";
|
|
9
10
|
|
|
10
11
|
const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
11
12
|
const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
@@ -133,10 +134,12 @@ export async function resolveAntigravityProjectId(
|
|
|
133
134
|
discoveredProjects.set(credential.token, projectId);
|
|
134
135
|
|
|
135
136
|
const persist = opts.persist ?? (async (id: string) => {
|
|
136
|
-
const { getStoredOAuth, setOauthCredential } = await import("../../auth/storage");
|
|
137
137
|
const owner = credential.provider === "antigravity" ? "antigravity" : "gemini";
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
await saveConfigPatch(raw => {
|
|
139
|
+
const stored = raw.oauth?.[owner];
|
|
140
|
+
if (!stored || typeof stored === "string" || stored.access !== credential.token || stored.projectId) return {};
|
|
141
|
+
return { oauth: { ...raw.oauth, [owner]: { ...stored, projectId: id } } };
|
|
142
|
+
});
|
|
140
143
|
});
|
|
141
144
|
try {
|
|
142
145
|
await persist(projectId);
|