opencode-ultra 0.5.1 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +74 -15
  2. package/dist/index.js +458 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,27 +1,28 @@
1
1
  # opencode-ultra
2
2
 
3
3
  [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) をベースに、micode / opencode-skillful の良い部分を取り込んだ OpenCode 1.2.x プラグイン。
4
- マルチエージェントオーケストレーション・キーワード駆動モード切替・ルール注入・セッション継続・AST検索を軽量な単一プラグインで実現する。
4
+ マルチエージェントオーケストレーション・キーワード駆動モード切替・ルール注入・セッション継続・AST検索・自己改善 (evolve) を軽量な単一プラグインで実現する。
5
5
 
6
6
  ## 機能一覧
7
7
 
8
- ### ツール (7)
8
+ ### ツール (8)
9
9
 
10
10
  | ツール | 説明 |
11
11
  |--------|------|
12
- | `spawn_agent` | 並列エージェント実行 (ConcurrencyPool 制御付き) |
13
- | `ralph_loop` | 自律ループ実行 (`<promise>DONE</promise>` で完了検知) |
12
+ | `spawn_agent` | 並列エージェント実行 (ConcurrencyPool + spawn limit + timeout + sanitizer) |
13
+ | `ralph_loop` | 自律ループ実行 (`<promise>DONE</promise>` で完了検知, per-iteration timeout) |
14
14
  | `cancel_ralph` | 実行中の Ralph Loop をキャンセル |
15
15
  | `batch_read` | 複数ファイル並列読み込み (最大20) |
16
16
  | `ledger_save` | Continuity Ledger の保存 (.opencode/ledgers/) |
17
17
  | `ledger_load` | Continuity Ledger の読み込み (名前指定 or 最新) |
18
18
  | `ast_search` | AST-aware コード検索 (ast-grep/sg バイナリ使用、未インストール時は自動スキップ) |
19
+ | `evolve_apply` | プラグイン推薦の信頼度スコア評価 + OpenCode 設定への自動適用 (dry-run/backup 対応) |
19
20
 
20
21
  ### フック (9)
21
22
 
22
23
  | フック | hook ポイント | 説明 |
23
24
  |--------|-------------|------|
24
- | `keyword-detector` | chat.message | ultrawork/search/analyze/think キーワード検知 |
25
+ | `keyword-detector` | chat.message | ultrawork/search/analyze/think/evolve キーワード検知 |
25
26
  | `rules-injector` | system.transform | .opencode/rules.md を system prompt に注入 |
26
27
  | `context-injector` | system.transform | ARCHITECTURE.md / CODE_STYLE.md を自動注入 |
27
28
  | `fragment-injector` | system.transform | エージェント毎のカスタムプロンプト断片注入 |
@@ -31,6 +32,45 @@
31
32
  | `todo-enforcer` | event (session.idle) | 未完了 TODO を検知して強制継続 |
32
33
  | `session-compaction` | experimental.session.compacting | セッション圧縮時に structured summary 生成 |
33
34
 
35
+ ### Safety
36
+
37
+ | 機能 | 説明 |
38
+ |------|------|
39
+ | **Prompt Injection Sanitizer** | エージェント出力から注入パターンを検知・無害化 (17パターン, 6カテゴリ) |
40
+ | **Spawn Limit** | 同時実行セッション上限 (default: 15) — エージェント爆発防止 |
41
+ | **Agent Timeout** | per-agent タイムアウト (default: 180s) — ハング防止 |
42
+ | **Trust Score** | npm パッケージの信頼度スコア (0-100) — evolve 推薦の品質ゲート |
43
+
44
+ ### Evolve (自己改善)
45
+
46
+ | キーワード | 効果 |
47
+ |-----------|------|
48
+ | `evolve` / `self-improve` / `自己改善` / `進化` | evolve モード起動 |
49
+
50
+ evolve モードでは:
51
+ 1. **scout** エージェントが npm/GitHub を検索し、プラグインのメタデータを収集
52
+ 2. **explore** エージェントが opencode-ultra 自身の機能をカタログ化
53
+ 3. 発見されたプラグインに **Trust Score** (0-100) を自動計算
54
+ 4. ギャップ分析と改善提案を生成
55
+ 5. ユーザー承認後、`evolve_apply` で設定に自動適用 (バックアップ付き)
56
+
57
+ Trust Score 評価基準:
58
+
59
+ | カテゴリ | 配点 | チェック項目 |
60
+ |----------|------|-------------|
61
+ | Recency | 25 | 最終 publish からの日数 |
62
+ | Popularity | 25 | 週間 DL 数 + GitHub stars |
63
+ | Quality | 20 | ライセンス, README, メンテナ数, description |
64
+ | Repository | 15 | GitHub/GitLab リポジトリの存在 |
65
+ | Safety | 15 | typosquat 検知, 依存数, 既知スコープ |
66
+
67
+ | スコア | レベル | 自動インストール |
68
+ |--------|--------|----------------|
69
+ | 90-100 | HIGH | 可 |
70
+ | 70-89 | MEDIUM | レビュー推奨 |
71
+ | 40-69 | LOW | 要注意 |
72
+ | 0-39 | RISKY | ブロック |
73
+
34
74
  ### その他
35
75
 
36
76
  | 機能 | 説明 |
@@ -46,7 +86,7 @@
46
86
  ```
47
87
  User
48
88
 
49
- │ "ultrawork" / "ulw" / "think hard" キーワード
89
+ │ "ultrawork" / "ulw" / "evolve" / "think hard" キーワード
50
90
 
51
91
 
52
92
  ┌──────────────────────────────────────────────┐
@@ -54,7 +94,7 @@ User
54
94
  │ │
55
95
  │ chat.message hook │
56
96
  │ ├─ keyword-detector │
57
- │ │ └─ ultrawork / search / analyze / think
97
+ │ │ └─ ultrawork/search/analyze/think/evolve
58
98
  │ └─ ultrawork → variant: "max" │
59
99
  │ │
60
100
  │ system.transform hook │
@@ -80,11 +120,20 @@ User
80
120
  │ └─ demote built-in agents to subagent │
81
121
  │ │
82
122
  │ tools │
83
- │ ├─ spawn_agent (並列実行 + 並列制御) │
123
+ │ ├─ spawn_agent (並列実行 + 安全制御) │
124
+ │ │ ├─ ConcurrencyPool │
125
+ │ │ ├─ Spawn limit (max 15) │
126
+ │ │ ├─ Agent timeout (180s) │
127
+ │ │ └─ Prompt injection sanitizer │
84
128
  │ ├─ ralph_loop / cancel_ralph │
85
129
  │ ├─ batch_read (複数ファイル並列読み込み) │
86
130
  │ ├─ ledger_save / ledger_load (文脈継続) │
87
- └─ ast_search (構文木検索, optional) │
131
+ ├─ ast_search (構文木検索, optional) │
132
+ │ └─ evolve_apply (信頼度スコア + 設定適用) │
133
+ │ │
134
+ │ safety │
135
+ │ ├─ sanitizer (17 injection patterns) │
136
+ │ └─ trust-score (5-factor, 0-100) │
88
137
  │ │
89
138
  │ MCP registration │
90
139
  │ └─ context7 (自動登録) │
@@ -119,6 +168,7 @@ Sisyphus (オーケストレーター) が直接コードを読み、実装は
119
168
  | **momus** | コードレビュー・品質チェック | anthropic/claude-sonnet-4-5 | subagent |
120
169
  | **atlas** | タスク管理・進捗追跡 | anthropic/claude-sonnet-4-5 | subagent |
121
170
  | **multimodal-looker** | 画像・スクリーンショット解析 | anthropic/claude-sonnet-4-5 | subagent |
171
+ | **scout** | プラグインエコシステム調査・信頼度メタデータ収集 | anthropic/claude-sonnet-4-5 | subagent |
122
172
 
123
173
  全てのモデルは `oh-my-opencode.json` でオーバーライド可能。
124
174
 
@@ -190,6 +240,7 @@ ledger_load({})
190
240
  | `search` / `find` / `探して` 等 | search | 網羅的検索 |
191
241
  | `analyze` / `調査` / `debug` 等 | analyze | コンテキスト収集 |
192
242
  | `think hard` / `じっくり` / `熟考` 等 | think | Extended thinking 有効化 |
243
+ | `evolve` / `self-improve` / `自己改善` 等 | evolve | 自己改善サイクル起動 |
193
244
 
194
245
  日本語・中国語キーワードにも対応。
195
246
 
@@ -231,8 +282,8 @@ ledger_load({})
231
282
  "disabled_hooks": [], // keyword-detector, rules-injector, context-injector,
232
283
  // fragment-injector, prompt-renderer, comment-checker,
233
284
  // token-truncation, todo-enforcer, session-compaction
234
- "disabled_tools": [], // spawn_agent, ralph_loop, cancel_ralph,
235
- // batch_read, ledger_save, ledger_load, ast_search
285
+ "disabled_tools": [], // spawn_agent, ralph_loop, cancel_ralph, batch_read,
286
+ // ledger_save, ledger_load, ast_search, evolve_apply
236
287
  "disabled_mcps": [], // context7
237
288
 
238
289
  // Built-in Agent Demotion (default: true)
@@ -245,6 +296,12 @@ ledger_load({})
245
296
  "modelConcurrency": { "openai/gpt-5.2": 2 }
246
297
  },
247
298
 
299
+ // Safety
300
+ "safety": {
301
+ "maxTotalSpawned": 15, // 同時実行セッション上限
302
+ "agentTimeoutMs": 180000 // per-agent タイムアウト (ms)
303
+ },
304
+
248
305
  // Token Truncation
249
306
  "token_truncation": { "maxChars": 30000 },
250
307
 
@@ -270,7 +327,7 @@ opencode-ultra/
270
327
  │ │ ├── types.ts # AgentDef 型定義
271
328
  │ │ └── index.ts # ビルトインエージェント + 動的プロンプト生成
272
329
  │ ├── hooks/
273
- │ │ ├── keyword-detector.ts # ultrawork/search/analyze/think 検知
330
+ │ │ ├── keyword-detector.ts # ultrawork/search/analyze/think/evolve 検知
274
331
  │ │ ├── rules-injector.ts # rules.md + ARCHITECTURE.md + CODE_STYLE.md 注入
275
332
  │ │ ├── fragment-injector.ts # エージェント毎の断片注入
276
333
  │ │ ├── prompt-renderer.ts # モデル別フォーマット変換
@@ -279,16 +336,18 @@ opencode-ultra/
279
336
  │ │ ├── todo-enforcer.ts # 未完了 TODO 強制継続
280
337
  │ │ └── session-compaction.ts # セッション圧縮サマリ生成
281
338
  │ ├── tools/
282
- │ │ ├── spawn-agent.ts # 並列エージェント実行
339
+ │ │ ├── spawn-agent.ts # 並列エージェント実行 + 安全制御
283
340
  │ │ ├── ralph-loop.ts # 自律ループ実行
284
341
  │ │ ├── batch-read.ts # 複数ファイル並列読み込み
285
342
  │ │ ├── continuity-ledger.ts # セッション間文脈継続
286
- │ │ └── ast-search.ts # AST-aware コード検索
343
+ │ │ ├── ast-search.ts # AST-aware コード検索
344
+ │ │ └── evolve-apply.ts # 信頼度スコア評価 + 設定適用
287
345
  │ ├── concurrency/ # Semaphore + ConcurrencyPool
288
346
  │ ├── categories/ # ビルトインカテゴリ
347
+ │ ├── safety/ # sanitizer + trust-score
289
348
  │ ├── mcp/ # MCP サーバー登録
290
349
  │ └── shared/ # ユーティリティ
291
- ├── __test__/ # Bun テスト (92 tests, 20 files)
350
+ ├── __test__/ # Bun テスト
292
351
  ├── package.json
293
352
  ├── tsconfig.json
294
353
  └── .gitignore
package/dist/index.js CHANGED
@@ -13552,6 +13552,9 @@ import { join } from "path";
13552
13552
  function getConfigDir() {
13553
13553
  return process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, "opencode") : join(homedir(), ".config", "opencode");
13554
13554
  }
13555
+ function getCacheDir() {
13556
+ return process.env.XDG_CACHE_HOME ? join(process.env.XDG_CACHE_HOME, "opencode") : join(homedir(), ".cache", "opencode");
13557
+ }
13555
13558
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
13556
13559
  function createScanner(text, ignoreTrivia = false) {
13557
13560
  const len = text.length;
@@ -14640,12 +14643,12 @@ var BUILTIN_AGENTS = {
14640
14643
  },
14641
14644
  scout: {
14642
14645
  model: "anthropic/claude-sonnet-4-5",
14643
- description: "Plugin ecosystem researcher \u2014 finds, analyzes, and compares OpenCode plugins for self-improvement",
14646
+ description: "Plugin ecosystem researcher \u2014 finds, analyzes, and compares OpenCode plugins with trust scoring",
14644
14647
  prompt: `You are Scout, an OpenCode plugin ecosystem researcher.
14645
14648
 
14646
14649
  ## YOUR MISSION
14647
14650
  Search the web (npm, GitHub, OpenCode community) for OpenCode plugins and extensions.
14648
- Analyze their features, architecture, and quality. Compare with opencode-ultra.
14651
+ Collect STRUCTURED METADATA for trust scoring. Compare with opencode-ultra.
14649
14652
 
14650
14653
  ## SEARCH STRATEGY
14651
14654
  1. Search npm for "opencode-plugin", "opencode-ai", "@opencode" packages
@@ -14653,17 +14656,48 @@ Analyze their features, architecture, and quality. Compare with opencode-ultra.
14653
14656
  3. Look at package.json dependencies on @opencode-ai/plugin or @opencode-ai/sdk
14654
14657
  4. Read README files and source code of discovered plugins
14655
14658
 
14659
+ ## CRITICAL: METADATA COLLECTION
14660
+ For EACH plugin found, you MUST collect these fields (used for trust scoring):
14661
+ - **name**: exact npm package name
14662
+ - **version**: latest version string
14663
+ - **lastPublished**: ISO date of last npm publish (e.g. "2026-01-15")
14664
+ - **weeklyDownloads**: weekly npm download count (number)
14665
+ - **stars**: GitHub stars (number, 0 if unknown)
14666
+ - **repository**: GitHub/GitLab repo URL
14667
+ - **license**: SPDX license identifier (e.g. "MIT", "ISC")
14668
+ - **hasReadme**: true/false
14669
+ - **maintainerCount**: number of npm maintainers
14670
+ - **dependencyCount**: number of production dependencies
14671
+ - **description**: one-line description
14672
+ - **features**: bullet list of capabilities
14673
+ - **uniqueIdeas**: features that opencode-ultra does NOT have
14674
+
14656
14675
  ## OUTPUT FORMAT
14657
- For each plugin found, report:
14658
- - **Name**: package name + repo URL
14659
- - **Version**: latest version
14660
- - **Features**: bullet list of capabilities
14661
- - **Architecture**: hook types used, tool count, agent count
14662
- - **Quality signals**: test count, TypeScript, last updated, download count
14663
- - **Unique ideas**: features that opencode-ultra does NOT have
14676
+ Return a JSON array of plugin objects with the fields above. Example:
14677
+ \`\`\`json
14678
+ [
14679
+ {
14680
+ "name": "opencode-supermemory",
14681
+ "version": "1.2.0",
14682
+ "lastPublished": "2026-02-01",
14683
+ "weeklyDownloads": 500,
14684
+ "stars": 45,
14685
+ "repository": "https://github.com/supermemoryai/opencode-supermemory",
14686
+ "license": "MIT",
14687
+ "hasReadme": true,
14688
+ "maintainerCount": 2,
14689
+ "dependencyCount": 5,
14690
+ "description": "Persistent memory across OpenCode sessions",
14691
+ "features": ["Session memory", "Cross-project recall"],
14692
+ "uniqueIdeas": ["Long-term memory that opencode-ultra lacks"]
14693
+ }
14694
+ ]
14695
+ \`\`\`
14696
+
14697
+ Also include a text summary with gap analysis after the JSON block.
14664
14698
 
14665
14699
  ## COMPARISON
14666
- After listing plugins, generate a structured gap analysis:
14700
+ After listing plugins, generate:
14667
14701
  - Features others have that opencode-ultra lacks
14668
14702
  - Features opencode-ultra has that others lack (competitive advantages)
14669
14703
  - Improvement priority list (high/medium/low impact)
@@ -14906,30 +14940,58 @@ var THINK_MESSAGE = `Extended thinking enabled. Take your time to reason thoroug
14906
14940
  var EVOLVE_MESSAGE = `[evolve-mode] SELF-IMPROVEMENT CYCLE ACTIVATED.
14907
14941
 
14908
14942
  ## MISSION
14909
- Search the OpenCode plugin ecosystem, find what others have built, and identify gaps in opencode-ultra.
14943
+ Search the OpenCode plugin ecosystem, evaluate trust/quality, identify gaps, and optionally apply improvements.
14944
+
14945
+ ## TOOLS AVAILABLE
14946
+ - **spawn_agent** \u2014 run scout + explore agents in parallel for data gathering
14947
+ - **evolve_apply** \u2014 apply plugin recommendations to OpenCode config (with trust scoring and backup)
14910
14948
 
14911
14949
  ## STEPS
14912
- 1. **SCOUT** \u2014 Spawn the **scout** agent to search npm/GitHub for OpenCode plugins
14913
- 2. **READ SELF** \u2014 Read opencode-ultra's own README.md and key source files to know current capabilities
14914
- 3. **COMPARE** \u2014 Generate a structured gap analysis (what we have vs what we're missing)
14915
- 4. **PRIORITIZE** \u2014 Rank missing features by impact (high/medium/low)
14916
- 5. **PROPOSE** \u2014 Present improvement plan to the user
14917
- 6. **SAVE** \u2014 Save findings to a continuity ledger via ledger_save for future reference
14950
+ 1. **SCOUT** \u2014 Spawn the **scout** agent to search npm/GitHub for OpenCode plugins. Scout MUST return package metadata (lastPublished, weeklyDownloads, stars, repository, license, maintainerCount, dependencyCount) for each plugin found.
14951
+ 2. **READ SELF** \u2014 Read opencode-ultra's own capabilities via explore agent
14952
+ 3. **SCORE** \u2014 For each discovered plugin, the evolve_apply tool computes a trust score (0-100):
14953
+ - 90-100: HIGH trust (safe to auto-install)
14954
+ - 70-89: MEDIUM trust (review recommended)
14955
+ - 40-69: LOW trust (caution)
14956
+ - 0-39: RISKY (blocked from install)
14957
+ 4. **COMPARE** \u2014 Generate gap analysis with trust scores
14958
+ 5. **PROPOSE** \u2014 Present scored recommendations to the user
14959
+ 6. **APPLY** \u2014 If user approves, call evolve_apply to update config:
14960
+
14961
+ \`\`\`
14962
+ evolve_apply({
14963
+ plugins: [
14964
+ {
14965
+ name: "opencode-supermemory",
14966
+ version: "latest",
14967
+ reason: "Persistent memory across sessions",
14968
+ metadata: {name: "opencode-supermemory", lastPublished: "2026-02-01", weeklyDownloads: 500, stars: 45, repository: "https://github.com/...", license: "MIT", maintainerCount: 2}
14969
+ }
14970
+ ],
14971
+ dryRun: true
14972
+ })
14973
+ \`\`\`
14974
+
14975
+ Use dryRun: true FIRST to preview, then dryRun: false after user approval.
14976
+
14977
+ 7. **SAVE** \u2014 Save findings to ledger: ledger_save({name: "evolve-scan-YYYY-MM-DD", content: "..."})
14918
14978
 
14919
14979
  ## EXECUTION
14920
14980
  \`\`\`
14921
14981
  spawn_agent({
14922
14982
  agents: [
14923
- {agent: "scout", prompt: "Search npm and GitHub for OpenCode 1.2.x plugins. Find all published plugins, analyze features, compare with opencode-ultra.", description: "Plugin ecosystem scan"},
14983
+ {agent: "scout", prompt: "Search npm and GitHub for OpenCode 1.2.x plugins. For EACH plugin found, collect: name, version, lastPublished date, weeklyDownloads, GitHub stars, repository URL, license, maintainerCount, dependencyCount. Return structured data.", description: "Plugin ecosystem scan + metadata"},
14924
14984
  {agent: "explore", prompt: "Read opencode-ultra's README.md, src/index.ts, src/agents/index.ts to catalog current features", description: "Self-analysis"}
14925
14985
  ]
14926
14986
  })
14927
14987
  \`\`\`
14928
14988
 
14929
- After gathering results, synthesize into a gap analysis and present to the user.
14930
- Save the analysis via: ledger_save({name: "evolve-scan-YYYY-MM-DD", content: "..."})
14989
+ After gathering results:
14990
+ 1. Call evolve_apply with dryRun: true to preview trust scores
14991
+ 2. Present results to user with recommendation
14992
+ 3. If approved, call evolve_apply with dryRun: false
14931
14993
 
14932
- **This is how opencode-ultra gets better \u2014 by knowing what it doesn't know.**`;
14994
+ **Trust scoring prevents bad plugins from entering your system. Always dry-run first.**`;
14933
14995
 
14934
14996
  // src/hooks/rules-injector.ts
14935
14997
  import * as fs2 from "fs";
@@ -27489,6 +27551,182 @@ function sanitizeSpawnResult(result) {
27489
27551
  return banner + `
27490
27552
  ` + text;
27491
27553
  }
27554
+ // src/safety/trust-score.ts
27555
+ var KNOWN_SCOPES = [
27556
+ "@opencode-ai/",
27557
+ "opencode-",
27558
+ "@tarquinen/",
27559
+ "@azumag/",
27560
+ "@hoangp8/",
27561
+ "@gitlab/"
27562
+ ];
27563
+ var TYPOSQUAT_PATTERNS = [
27564
+ /^0pencode/i,
27565
+ /^opencodd/i,
27566
+ /^openc0de/i,
27567
+ /^opencodee/i,
27568
+ /^opeencode/i,
27569
+ /^opemcode/i,
27570
+ /^opencode-.{1,3}$/i
27571
+ ];
27572
+ function computeTrustScore(meta3) {
27573
+ const factors = [];
27574
+ factors.push(scoreRecency(meta3));
27575
+ factors.push(scorePopularity(meta3));
27576
+ factors.push(scoreQuality(meta3));
27577
+ factors.push(scoreRepository(meta3));
27578
+ factors.push(scoreSafety(meta3));
27579
+ const totalScore = Math.min(100, factors.reduce((sum, f) => sum + f.score, 0));
27580
+ const level = classifyLevel(totalScore);
27581
+ const summary = buildSummary(meta3.name, totalScore, level, factors);
27582
+ log("Trust score computed", { package: meta3.name, score: totalScore, level });
27583
+ return { score: totalScore, level, factors, summary };
27584
+ }
27585
+ function scoreRecency(meta3) {
27586
+ const maxScore = 25;
27587
+ if (!meta3.lastPublished) {
27588
+ return { name: "recency", score: 0, maxScore, detail: "No publish date available" };
27589
+ }
27590
+ const daysAgo = daysSince(meta3.lastPublished);
27591
+ if (daysAgo < 30)
27592
+ return { name: "recency", score: 25, maxScore, detail: `Updated ${daysAgo}d ago` };
27593
+ if (daysAgo < 90)
27594
+ return { name: "recency", score: 20, maxScore, detail: `Updated ${daysAgo}d ago` };
27595
+ if (daysAgo < 180)
27596
+ return { name: "recency", score: 15, maxScore, detail: `Updated ${daysAgo}d ago` };
27597
+ if (daysAgo < 365)
27598
+ return { name: "recency", score: 10, maxScore, detail: `Updated ${daysAgo}d ago \u2014 getting stale` };
27599
+ return { name: "recency", score: 3, maxScore, detail: `Updated ${daysAgo}d ago \u2014 likely abandoned` };
27600
+ }
27601
+ function scorePopularity(meta3) {
27602
+ const maxScore = 25;
27603
+ let score = 0;
27604
+ const details = [];
27605
+ const dl = meta3.weeklyDownloads ?? 0;
27606
+ if (dl > 1e4) {
27607
+ score += 15;
27608
+ details.push(`${dl.toLocaleString()} weekly DL`);
27609
+ } else if (dl > 1000) {
27610
+ score += 12;
27611
+ details.push(`${dl.toLocaleString()} weekly DL`);
27612
+ } else if (dl > 100) {
27613
+ score += 8;
27614
+ details.push(`${dl} weekly DL`);
27615
+ } else if (dl > 10) {
27616
+ score += 4;
27617
+ details.push(`${dl} weekly DL \u2014 low`);
27618
+ } else {
27619
+ score += 0;
27620
+ details.push(`${dl} weekly DL \u2014 very low`);
27621
+ }
27622
+ const stars = meta3.stars ?? 0;
27623
+ if (stars > 100) {
27624
+ score += 10;
27625
+ details.push(`${stars} stars`);
27626
+ } else if (stars > 20) {
27627
+ score += 7;
27628
+ details.push(`${stars} stars`);
27629
+ } else if (stars > 5) {
27630
+ score += 4;
27631
+ details.push(`${stars} stars`);
27632
+ } else {
27633
+ score += 0;
27634
+ details.push(`${stars} stars`);
27635
+ }
27636
+ return { name: "popularity", score: Math.min(maxScore, score), maxScore, detail: details.join(", ") };
27637
+ }
27638
+ function scoreQuality(meta3) {
27639
+ const maxScore = 20;
27640
+ let score = 0;
27641
+ const details = [];
27642
+ if (meta3.license && meta3.license !== "NONE" && meta3.license !== "UNLICENSED") {
27643
+ score += 7;
27644
+ details.push(`License: ${meta3.license}`);
27645
+ } else {
27646
+ details.push("No license");
27647
+ }
27648
+ if (meta3.hasReadme !== false) {
27649
+ score += 5;
27650
+ details.push("Has README");
27651
+ } else {
27652
+ details.push("No README");
27653
+ }
27654
+ const maintainers = meta3.maintainerCount ?? 0;
27655
+ if (maintainers >= 2) {
27656
+ score += 5;
27657
+ details.push(`${maintainers} maintainers`);
27658
+ } else if (maintainers === 1) {
27659
+ score += 3;
27660
+ details.push("1 maintainer");
27661
+ } else {
27662
+ details.push("No maintainer info");
27663
+ }
27664
+ if (meta3.description && meta3.description.length > 20) {
27665
+ score += 3;
27666
+ } else {
27667
+ details.push("Weak description");
27668
+ }
27669
+ return { name: "quality", score: Math.min(maxScore, score), maxScore, detail: details.join(", ") };
27670
+ }
27671
+ function scoreRepository(meta3) {
27672
+ const maxScore = 15;
27673
+ if (!meta3.repository) {
27674
+ return { name: "repository", score: 0, maxScore, detail: "No repository link \u2014 suspicious" };
27675
+ }
27676
+ if (meta3.repository.includes("github.com")) {
27677
+ return { name: "repository", score: 15, maxScore, detail: `Repo: ${meta3.repository}` };
27678
+ }
27679
+ if (meta3.repository.includes("gitlab.com") || meta3.repository.includes("bitbucket.org")) {
27680
+ return { name: "repository", score: 12, maxScore, detail: `Repo: ${meta3.repository}` };
27681
+ }
27682
+ return { name: "repository", score: 5, maxScore, detail: `Repo: ${meta3.repository} (unknown host)` };
27683
+ }
27684
+ function scoreSafety(meta3) {
27685
+ const maxScore = 15;
27686
+ let score = 15;
27687
+ const details = [];
27688
+ if (isTyposquatSuspect(meta3.name)) {
27689
+ score -= 15;
27690
+ details.push("TYPOSQUAT SUSPECT");
27691
+ } else {
27692
+ details.push("Name OK");
27693
+ }
27694
+ if (KNOWN_SCOPES.some((s) => meta3.name.startsWith(s))) {
27695
+ score = Math.min(maxScore, score + 3);
27696
+ details.push("Known scope");
27697
+ }
27698
+ const deps = meta3.dependencyCount ?? 0;
27699
+ if (deps > 50) {
27700
+ score = Math.max(0, score - 5);
27701
+ details.push(`${deps} deps \u2014 heavy`);
27702
+ } else if (deps > 20) {
27703
+ score = Math.max(0, score - 2);
27704
+ details.push(`${deps} deps`);
27705
+ }
27706
+ return { name: "safety", score: Math.max(0, score), maxScore, detail: details.join(", ") };
27707
+ }
27708
+ function isTyposquatSuspect(name) {
27709
+ return TYPOSQUAT_PATTERNS.some((p) => p.test(name));
27710
+ }
27711
+ function classifyLevel(score) {
27712
+ if (score >= 90)
27713
+ return "high";
27714
+ if (score >= 70)
27715
+ return "medium";
27716
+ if (score >= 40)
27717
+ return "low";
27718
+ return "risky";
27719
+ }
27720
+ function daysSince(isoDate) {
27721
+ const then = new Date(isoDate).getTime();
27722
+ const now = Date.now();
27723
+ return Math.floor((now - then) / (1000 * 60 * 60 * 24));
27724
+ }
27725
+ function buildSummary(name, score, level, factors) {
27726
+ const icon = level === "high" ? "V" : level === "medium" ? "~" : level === "low" ? "!" : "X";
27727
+ const weakest = [...factors].sort((a, b) => a.score / a.maxScore - b.score / b.maxScore)[0];
27728
+ return `[${icon}] ${name}: ${score}/100 (${level}) \u2014 weakest: ${weakest.name} (${weakest.score}/${weakest.maxScore}: ${weakest.detail})`;
27729
+ }
27492
27730
  // src/tools/spawn-agent.ts
27493
27731
  var DEFAULT_MAX_TOTAL_SPAWNED = 15;
27494
27732
  var DEFAULT_AGENT_TIMEOUT_MS = 180000;
@@ -28086,6 +28324,199 @@ function createAstSearchTool(ctx, binaryPath) {
28086
28324
  });
28087
28325
  }
28088
28326
 
28327
+ // src/tools/evolve-apply.ts
28328
+ import * as fs7 from "fs";
28329
+ import * as path7 from "path";
28330
+ function readJson(filePath) {
28331
+ try {
28332
+ return JSON.parse(fs7.readFileSync(filePath, "utf-8"));
28333
+ } catch {
28334
+ return null;
28335
+ }
28336
+ }
28337
+ function writeJson(filePath, data) {
28338
+ fs7.writeFileSync(filePath, JSON.stringify(data, null, 2) + `
28339
+ `, "utf-8");
28340
+ }
28341
+ function createBackup(configDir) {
28342
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
28343
+ const backupDir = path7.join(configDir, "backups", `pre-evolve-${ts}`);
28344
+ fs7.mkdirSync(backupDir, { recursive: true });
28345
+ const configPath = path7.join(configDir, "opencode.json");
28346
+ const pkgPath = path7.join(configDir, "package.json");
28347
+ const cachePkgPath = path7.join(getCacheDir(), "package.json");
28348
+ if (fs7.existsSync(configPath)) {
28349
+ fs7.copyFileSync(configPath, path7.join(backupDir, "opencode.json"));
28350
+ }
28351
+ if (fs7.existsSync(pkgPath)) {
28352
+ fs7.copyFileSync(pkgPath, path7.join(backupDir, "package.json"));
28353
+ }
28354
+ if (fs7.existsSync(cachePkgPath)) {
28355
+ fs7.copyFileSync(cachePkgPath, path7.join(backupDir, "cache-package.json"));
28356
+ }
28357
+ log("Evolve backup created", { path: backupDir });
28358
+ return backupDir;
28359
+ }
28360
+ function createEvolveApplyTool(ctx) {
28361
+ return tool({
28362
+ description: `Apply evolve scan recommendations to OpenCode configuration.
28363
+ Takes a list of plugin recommendations (from evolve scan), computes trust scores,
28364
+ creates a backup, and adds approved plugins to opencode.json and package.json.
28365
+
28366
+ IMPORTANT: Only plugins with trust score >= 40 (low or above) are added.
28367
+ Risky plugins (score < 40) are skipped with a warning.
28368
+
28369
+ Use dryRun: true to preview changes without applying them.
28370
+
28371
+ Example:
28372
+ evolve_apply({
28373
+ plugins: [
28374
+ {name: "opencode-supermemory", version: "latest", reason: "Persistent memory across sessions"},
28375
+ {name: "@azumag/opencode-rate-limit-fallback", version: "^1.0.0", reason: "Rate limit handling"}
28376
+ ],
28377
+ dryRun: false
28378
+ })`,
28379
+ args: {
28380
+ plugins: tool.schema.array(tool.schema.object({
28381
+ name: tool.schema.string().describe("npm package name"),
28382
+ version: tool.schema.string().optional().describe("Version spec (default: latest)"),
28383
+ reason: tool.schema.string().describe("Why this plugin is recommended"),
28384
+ metadata: tool.schema.object({
28385
+ name: tool.schema.string(),
28386
+ lastPublished: tool.schema.string().optional(),
28387
+ weeklyDownloads: tool.schema.number().optional(),
28388
+ stars: tool.schema.number().optional(),
28389
+ repository: tool.schema.string().optional(),
28390
+ license: tool.schema.string().optional(),
28391
+ hasReadme: tool.schema.boolean().optional(),
28392
+ maintainerCount: tool.schema.number().optional(),
28393
+ dependencyCount: tool.schema.number().optional()
28394
+ }).optional().describe("Package metadata for trust scoring (if available)")
28395
+ })).describe("Plugins to evaluate and potentially install"),
28396
+ dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: false)"),
28397
+ minTrustScore: tool.schema.number().optional().describe("Minimum trust score to auto-install (default: 40)")
28398
+ },
28399
+ execute: async (args) => {
28400
+ const { plugins } = args;
28401
+ const dryRun = args.dryRun ?? false;
28402
+ const minScore = args.minTrustScore ?? 40;
28403
+ if (!plugins || plugins.length === 0) {
28404
+ return "No plugins specified.";
28405
+ }
28406
+ const configDir = getConfigDir();
28407
+ const configPath = path7.join(configDir, "opencode.json");
28408
+ const cachePkgPath = path7.join(getCacheDir(), "package.json");
28409
+ const config3 = readJson(configPath);
28410
+ if (!config3) {
28411
+ return `Error: Cannot read ${configPath}`;
28412
+ }
28413
+ const currentPlugins = config3.plugin ?? [];
28414
+ const cachePkg = readJson(cachePkgPath);
28415
+ const currentDeps = cachePkg?.dependencies ?? {};
28416
+ const added = [];
28417
+ const skipped = [];
28418
+ const trustLines = [];
28419
+ for (const rec of plugins) {
28420
+ const alreadyInConfig = currentPlugins.some((p) => p === rec.name || p.startsWith(rec.name + "@"));
28421
+ const alreadyInDeps = rec.name in currentDeps;
28422
+ if (alreadyInConfig || alreadyInDeps) {
28423
+ skipped.push({ name: rec.name, reason: "Already installed" });
28424
+ trustLines.push(` SKIP ${rec.name} \u2014 already installed`);
28425
+ continue;
28426
+ }
28427
+ const meta3 = rec.metadata ?? { name: rec.name };
28428
+ const trustResult = computeTrustScore(meta3);
28429
+ const scoreTag = `[${trustResult.score}/100 ${trustResult.level}]`;
28430
+ trustLines.push(` ${scoreTag} ${rec.name} \u2014 ${rec.reason}`);
28431
+ for (const f of trustResult.factors) {
28432
+ trustLines.push(` ${f.name}: ${f.score}/${f.maxScore} (${f.detail})`);
28433
+ }
28434
+ if (trustResult.score < minScore) {
28435
+ skipped.push({
28436
+ name: rec.name,
28437
+ reason: `Trust score ${trustResult.score} < ${minScore} (${trustResult.level})`
28438
+ });
28439
+ trustLines.push(` -> REJECTED (below threshold ${minScore})`);
28440
+ continue;
28441
+ }
28442
+ added.push(rec.name);
28443
+ trustLines.push(` -> APPROVED`);
28444
+ }
28445
+ const trustReport = trustLines.join(`
28446
+ `);
28447
+ if (added.length === 0) {
28448
+ log("Evolve apply: nothing to add", { skipped: skipped.length });
28449
+ return formatResult({
28450
+ added: [],
28451
+ skipped,
28452
+ dryRun,
28453
+ trustReport
28454
+ });
28455
+ }
28456
+ if (dryRun) {
28457
+ log("Evolve apply: dry run", { wouldAdd: added });
28458
+ return formatResult({ added, skipped, dryRun: true, trustReport });
28459
+ }
28460
+ const backupDir = createBackup(configDir);
28461
+ const pluginEntry = (name, version3) => version3 && version3 !== "latest" ? `${name}@${version3}` : name;
28462
+ for (const name of added) {
28463
+ const rec = plugins.find((p) => p.name === name);
28464
+ const entry = pluginEntry(name, rec.version);
28465
+ if (!currentPlugins.includes(entry)) {
28466
+ currentPlugins.push(entry);
28467
+ }
28468
+ }
28469
+ config3.plugin = currentPlugins;
28470
+ writeJson(configPath, config3);
28471
+ log("Evolve apply: updated opencode.json", { plugins: currentPlugins });
28472
+ if (cachePkg) {
28473
+ const deps = cachePkg.dependencies ?? {};
28474
+ for (const name of added) {
28475
+ const rec = plugins.find((p) => p.name === name);
28476
+ deps[name] = rec.version ?? "latest";
28477
+ }
28478
+ cachePkg.dependencies = deps;
28479
+ writeJson(cachePkgPath, cachePkg);
28480
+ log("Evolve apply: updated cache/package.json");
28481
+ }
28482
+ return formatResult({ added, skipped, backup: backupDir, dryRun: false, trustReport });
28483
+ }
28484
+ });
28485
+ }
28486
+ function formatResult(result) {
28487
+ const lines = [];
28488
+ lines.push(result.dryRun ? "## Evolve Apply (DRY RUN)" : "## Evolve Apply Results");
28489
+ lines.push("");
28490
+ if (result.backup) {
28491
+ lines.push(`**Backup**: \`${result.backup}\``);
28492
+ lines.push("");
28493
+ }
28494
+ lines.push("### Trust Score Report");
28495
+ lines.push("```");
28496
+ lines.push(result.trustReport);
28497
+ lines.push("```");
28498
+ lines.push("");
28499
+ if (result.added.length > 0) {
28500
+ lines.push(result.dryRun ? "### Would Add" : "### Added");
28501
+ for (const name of result.added) {
28502
+ lines.push(`- ${name}`);
28503
+ }
28504
+ lines.push("");
28505
+ }
28506
+ if (result.skipped.length > 0) {
28507
+ lines.push("### Skipped");
28508
+ for (const { name, reason } of result.skipped) {
28509
+ lines.push(`- ${name} \u2014 ${reason}`);
28510
+ }
28511
+ lines.push("");
28512
+ }
28513
+ if (!result.dryRun && result.added.length > 0) {
28514
+ lines.push("> **Note**: Run `bun install` in `~/.cache/opencode/` to install the new packages, then restart OpenCode.");
28515
+ }
28516
+ return lines.join(`
28517
+ `);
28518
+ }
28519
+
28089
28520
  // src/hooks/todo-enforcer.ts
28090
28521
  var DEFAULT_MAX_ENFORCEMENTS = 5;
28091
28522
  var sessionState = new Map;
@@ -28168,7 +28599,7 @@ ${unfinished.map((t) => `- [ ] ${t.text}`).join(`
28168
28599
  }
28169
28600
 
28170
28601
  // src/hooks/comment-checker.ts
28171
- import * as fs7 from "fs";
28602
+ import * as fs8 from "fs";
28172
28603
  var AI_SLOP_PATTERNS = [
28173
28604
  /\/\/ .{80,}/,
28174
28605
  /\/\/ (This|The|We|Here|Note:)/i,
@@ -28247,7 +28678,7 @@ function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshol
28247
28678
  if (!filePath || !isCodeFile(filePath))
28248
28679
  return;
28249
28680
  try {
28250
- const content = fs7.readFileSync(filePath, "utf-8");
28681
+ const content = fs8.readFileSync(filePath, "utf-8");
28251
28682
  const result = checkComments(content, filePath, maxRatio, slopThreshold);
28252
28683
  if (result.shouldWarn) {
28253
28684
  output.output += `
@@ -28555,6 +28986,7 @@ var OpenCodeUltra = async (ctx) => {
28555
28986
  const ledgerLoad = createLedgerLoadTool(ctx);
28556
28987
  const astGrepBin = findAstGrepBinary();
28557
28988
  const astSearch = astGrepBin ? createAstSearchTool(ctx, astGrepBin) : null;
28989
+ const evolveApply = createEvolveApplyTool(ctx);
28558
28990
  const todoEnforcer = createTodoEnforcer(ctx, internalSessions, pluginConfig.todo_enforcer?.maxEnforcements);
28559
28991
  const commentCheckerHook = createCommentCheckerHook(internalSessions, pluginConfig.comment_checker?.maxRatio, pluginConfig.comment_checker?.slopThreshold);
28560
28992
  const tokenTruncationHook = createTokenTruncationHook(internalSessions, pluginConfig.token_truncation?.maxChars);
@@ -28593,6 +29025,9 @@ var OpenCodeUltra = async (ctx) => {
28593
29025
  if (!disabledTools.has("ast_search") && astSearch) {
28594
29026
  toolRegistry.ast_search = astSearch;
28595
29027
  }
29028
+ if (!disabledTools.has("evolve_apply")) {
29029
+ toolRegistry.evolve_apply = evolveApply;
29030
+ }
28596
29031
  return {
28597
29032
  tool: toolRegistry,
28598
29033
  config: async (config3) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ultra",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Lightweight OpenCode 1.2.x plugin — ultrawork mode, multi-agent orchestration, rules injection",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",