peaks-cli 2.0.0 → 2.0.2

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.
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "metadata": {
3
3
  "pluginRoot": ".",
4
- "version": "2.0.1"
4
+ "version": "2.0.2"
5
5
  },
6
6
  "plugins": [
7
7
  {
8
8
  "name": "peaks-cli",
9
9
  "description": "Cross-AI-IDE workflow-gating CLI + 11-skill family. Turns LLM improvisation into auditable engineering process. Skills cover PRD / R&D / UI / QA / change-control / context / SOP definition / orchestration. Soft-fail gates block irreversible actions mid-conversation (even under --dangerously-skip-permissions).",
10
- "version": "2.0.1",
10
+ "version": "2.0.2",
11
11
  "author": {
12
12
  "name": "SquabbyZ"
13
13
  },
package/CHANGELOG.md CHANGED
@@ -231,8 +231,81 @@ is skipped (`PEAKS_SKIP_AUTO_UPGRADE=1` or `npm i --ignore-scripts`).
231
231
 
232
232
  ---
233
233
 
234
+ ## [2.0.1] — 2026-06-12
235
+
236
+ ### Fixed
237
+
238
+ - **Bug 1 — `~/.peaks/config.json` was bloated to 9 top-level fields.**
239
+ The 2.0.0 release moved per-project fields (`language`, `model`,
240
+ `economyMode`, `swarmMode`) to `<project>/.peaks/preferences.json`
241
+ per spec §10.4, but the runtime `DEFAULT_CONFIG` still shipped
242
+ `language` / `model` / `economyMode` / `swarmMode` / `tokens` /
243
+ `providers` / `proxy` / `progress` placeholders. The slim migration
244
+ (`executeMigration`) wrote `{ version: "2.0.0" }` only, but any
245
+ code path that went through `readConfig` and re-serialised
246
+ re-bloated the file. The 2.0.1 fix:
247
+
248
+ 1. **Slim `DEFAULT_CONFIG`** to `{ version, ocr: { llm: { url, authToken, model, useAnthropic, authHeader } } }`
249
+ (placeholders for the OCR LLM endpoint only).
250
+ 2. **Slim migration write** to the same 2-key form, so a fresh
251
+ `peaks config migrate --apply` produces a discoverable
252
+ `ocr.llm` block the user can paste their endpoint into.
253
+ 3. **Tolerant loader.** Legacy 1.x files with extra fields
254
+ (`language`, `model`, `tokens`, `providers`, `proxy`, etc.)
255
+ still load without throwing; the legacy fields are exposed
256
+ via `getConfig` for backward compatibility, and
257
+ `setConfig` rejects writes to `language` / `model` /
258
+ `economyMode` / `swarmMode` with a pointer to
259
+ `<project>/.peaks/preferences.json` (do not silently migrate).
260
+
261
+ The net effect: a freshly-installed peaks-cli writes a 2-key
262
+ `~/.peaks/config.json`; legacy 1.x files migrate to the same
263
+ 2-key form; the ocr second-opinion config is now the only
264
+ discoverable surface the user needs to populate to make
265
+ `peaks code-review detect-ocr` report `state: "ready"`.
266
+
267
+ ### Verification
268
+
269
+ - 70 config tests pass (`tests/unit/config-*`).
270
+ - `pnpm tsc -p tsconfig.json --noEmit` clean (excluding pre-existing
271
+ sync-service test scaffold for Bug 2).
272
+
273
+ ---
274
+
275
+ ## [2.0.2] — 2026-06-13
276
+
277
+ ### Changed — README redesign (docs only)
278
+
279
+ The top of both `README.md` and `README-en.md` is rebuilt in the
280
+ RAG-Anything style requested from the published repo: card-grid
281
+ metadata (PROJECT / BASED ON / SKILLS.SH / STARS / VERSION / LICENSE
282
+ / TESTS / LANG / DOWNLOADS / 中文 / QUICK START / VISITORS), a
283
+ multiline `readme-typing-svg` tagline animation, a
284
+ `github-readme-streak-stats` streak band, and a `komarev` visitor
285
+ counter. Both languages are structurally identical (same card grid,
286
+ same animations, same anchor links); only the tagline and
287
+ call-to-action text differ.
288
+
289
+ - `README.md` updated to the new layout (typing animation uses the
290
+ Chinese tagline: `peaks-cli: 跨 AI IDE 的工程门禁与编排`).
291
+ - `README-en.md` synced to mirror the new layout (typing animation
292
+ uses the English tagline: `peaks-cli: cross-AI-IDE engineering
293
+ gates & orchestration`).
294
+ - Card anchors renamed to ASCII-friendly slugs on the English file
295
+ (`30-seconds-to-running`, `5-minute-onboarding`, `11-skills-in-the-family`,
296
+ `killer-feature-un-bypassable-gates`) so the README renders
297
+ consistently on GitHub's auto-generated anchor list.
298
+
299
+ No code, CLI, or schema changes. The CLI still reports
300
+ `Peaks CLI 2.0.2` after `prepublish` regenerates
301
+ `src/shared/version.ts`.
302
+
303
+ ---
304
+
234
305
  ## [1.4.2] — 2026-06-08
235
306
 
236
307
  Last 1.x release. See git history pre-2.0.0 for details.
237
308
 
309
+ [2.0.2]: https://github.com/SquabbyZ/peaks-cli/releases/tag/v2.0.2
310
+ [2.0.1]: https://github.com/SquabbyZ/peaks-cli/releases/tag/v2.0.1
238
311
  [2.0.0]: https://github.com/SquabbyZ/peaks-cli/releases/tag/v2.0.0
package/README-en.md CHANGED
@@ -2,17 +2,57 @@
2
2
 
3
3
  # ⛰️ Peaks
4
4
 
5
- **Make your AI IDE work like a disciplined engineering team**
5
+ <img src="https://readme-typing-svg.demolab.com?font=Fira+Code&weight=700&size=22&duration=3000&pause=800&color=6366F1&center=true&vCenter=true&multiline=true&width=720&height=110&lines=peaks-cli%3A%20cross-AI-IDE%20engineering%20gates%20%26%20orchestration;11%20%E2%9A%99%EF%B8%8F%20workflow%20skills%20%2B%20%E2%9A%96%EF%B8%8F%20executable%20gates;%E2%9C%89%EF%B8%8F%20%E2%9A%96%EF%B8%8F%20%E2%9C%89%EF%B8%8F%20%E2%9A%91%EF%B8%8F%20%E2%9A%96%EF%B8%8F%20%E2%9C%89%EF%B8%8F%20%E2%9A%91%EF%B8%8F%20gates%20%2B%20audit%20%2B%20cross-IDE%20adaptation" alt="peaks-cli tagline typing animation" />
6
6
 
7
7
  **English** | [简体中文](./README.md)
8
8
 
9
- [![npm version](https://img.shields.io/npm/v/peaks-cli.svg)](https://www.npmjs.com/package/peaks-cli)
10
- [![GitHub stars](https://img.shields.io/github/stars/SquabbyZ/peaks-cli?style=social)](https://github.com/SquabbyZ/peaks-cli/stargazers)
11
- [![GitHub repo](https://img.shields.io/badge/GitHub-SquabbyZ%2Fpeaks--cli-181717?logo=github)](https://github.com/SquabbyZ/peaks-cli)
12
- [![Skills.sh](https://img.shields.io/badge/discover%20on-skills.sh-181717)](https://skills.sh/SquabbyZ/peaks-cli)
13
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
14
-
15
- **One CLI + 11 workflow skills. Turn LLM improvisation into auditable engineering process.**
9
+ <table>
10
+ <tr>
11
+ <td align="center" width="180"><b>🔥 PROJECT</b></td>
12
+ <td align="center" width="180"><b>⚡ BASED ON</b></td>
13
+ <td align="center" width="180"><b>📚 SKILLS.SH</b></td>
14
+ </tr>
15
+ <tr>
16
+ <td align="center"><a href="https://github.com/SquabbyZ/peaks-cli">peaks-cli / Homepage</a></td>
17
+ <td align="center">11 Skills + Cross-IDE</td>
18
+ <td align="center"><a href="https://skills.sh/SquabbyZ/peaks-cli">Listed on skills.sh</a></td>
19
+ </tr>
20
+ <tr><td colspan="3">&nbsp;</td></tr>
21
+ <tr>
22
+ <td align="center" width="180"><b>⭐ STARS</b></td>
23
+ <td align="center" width="180"><b>📦 VERSION</b></td>
24
+ <td align="center" width="180"><b>📄 LICENSE</b></td>
25
+ </tr>
26
+ <tr>
27
+ <td align="center"><a href="https://github.com/SquabbyZ/peaks-cli/stargazers"><img src="https://img.shields.io/github/stars/SquabbyZ/peaks-cli?style=for-the-badge&logo=github&logoColor=white" alt="stars" /></a></td>
28
+ <td align="center"><a href="https://www.npmjs.com/package/peaks-cli"><img src="https://img.shields.io/npm/v/peaks-cli?style=for-the-badge&logo=npm&logoColor=white&color=CB3837" alt="npm version" /></a></td>
29
+ <td align="center"><a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=for-the-badge" alt="license" /></a></td>
30
+ </tr>
31
+ <tr><td colspan="3">&nbsp;</td></tr>
32
+ <tr>
33
+ <td align="center" width="180"><b>🧪 TESTS</b></td>
34
+ <td align="center" width="180"><b>🔧 LANG</b></td>
35
+ <td align="center" width="180"><b>📥 DOWNLOADS</b></td>
36
+ </tr>
37
+ <tr>
38
+ <td align="center"><b>2,800+</b></td>
39
+ <td align="center"><b>TypeScript</b></td>
40
+ <td align="center"><a href="https://www.npmjs.com/package/peaks-cli"><img src="https://img.shields.io/npm/dm/peaks-cli?style=for-the-badge&logo=npm&logoColor=white&color=CB3837" alt="downloads" /></a></td>
41
+ </tr>
42
+ <tr><td colspan="3">&nbsp;</td></tr>
43
+ <tr>
44
+ <td align="center" width="180"><b>🌐 中文</b></td>
45
+ <td align="center" width="180"><b>🚀 QUICK START</b></td>
46
+ <td align="center" width="180"><b>👁️ VISITORS</b></td>
47
+ </tr>
48
+ <tr>
49
+ <td align="center"><a href="./README.md">简体中文</a></td>
50
+ <td align="center"><a href="#-30-seconds-to-running">30s to running →</a></td>
51
+ <td align="center"><img src="https://komarev.com/ghpvc/?username=SquabbyZ&repo=peaks-cli&label=views&color=blue&style=for-the-badge" alt="visitors" /></td>
52
+ </tr>
53
+ </table>
54
+
55
+ <img src="https://github-readme-streak-stats.herokuapp.com?user=SquabbyZ&repo=peaks-cli&theme=radical&hide_border=true&date_format=j%20M%5B%20Y%5D" alt="GitHub Streak Stats" />
16
56
 
17
57
  [Install](#-30-seconds-to-running) · [5-min onboarding](#-5-minute-onboarding) · [Skill family](#-11-skills-in-the-family) · [Killer feature: un-bypassable gates](#-killer-feature-un-bypassable-gates)
18
58
 
@@ -221,6 +261,4 @@ See [`CHANGELOG.md`](./CHANGELOG.md) and [`docs/`](./docs/) for details.
221
261
 
222
262
  ⭐ [Star peaks-cli on GitHub](https://github.com/SquabbyZ/peaks-cli) · 🔍 [Browse on skills.sh](https://skills.sh/SquabbyZ/peaks-cli)
223
263
 
224
- Make your AI IDE work like a disciplined engineering team.
225
-
226
264
  </div>
package/README.md CHANGED
@@ -2,17 +2,57 @@
2
2
 
3
3
  # ⛰️ Peaks
4
4
 
5
- **让 AI IDE 像一支训练有素的工程团队一样工作**
5
+ <img src="https://readme-typing-svg.demolab.com?font=Fira+Code&weight=700&size=22&duration=3000&pause=800&color=6366F1&center=true&vCenter=true&multiline=true&width=720&height=110&lines=peaks-cli%3A%20跨%20AI%20IDE%20的工程门禁与编排;11%20个%E2%9A%99%EF%B8%8F%20工作流技能%20%2B%20%E2%9A%96%EF%B8%8F%20可执行门禁;%E2%9C%89%EF%B8%8F%20%E2%9A%96%EF%B8%8F%20%E2%9C%89%EF%B8%8F%20%E2%9A%91%EF%B8%8F%20%E2%9A%96%EF%B8%8F%20%E2%9C%89%EF%B8%8F%20%E2%9A%91%EF%B8%8F%20门禁%20%2B%20审计%20%2B%20跨%20IDE%20适配" alt="peaks-cli tagline typing animation" />
6
6
 
7
7
  [English](./README-en.md) | **简体中文**
8
8
 
9
- [![npm version](https://img.shields.io/npm/v/peaks-cli.svg)](https://www.npmjs.com/package/peaks-cli)
10
- [![GitHub stars](https://img.shields.io/github/stars/SquabbyZ/peaks-cli?style=social)](https://github.com/SquabbyZ/peaks-cli/stargazers)
11
- [![GitHub repo](https://img.shields.io/badge/GitHub-SquabbyZ%2Fpeaks--cli-181717?logo=github)](https://github.com/SquabbyZ/peaks-cli)
12
- [![Skills.sh](https://img.shields.io/badge/discover%20on-skills.sh-181717)](https://skills.sh/SquabbyZ/peaks-cli)
13
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
14
-
15
- **一个 CLI + 11 个工作流技能,把 LLM 的随意发挥变成可审计的工程流程。**
9
+ <table>
10
+ <tr>
11
+ <td align="center" width="180"><b>🔥 PROJECT</b></td>
12
+ <td align="center" width="180"><b>⚡ BASED ON</b></td>
13
+ <td align="center" width="180"><b>📚 SKILLS.SH</b></td>
14
+ </tr>
15
+ <tr>
16
+ <td align="center"><a href="https://github.com/SquabbyZ/peaks-cli">peaks-cli / 首页</a></td>
17
+ <td align="center">11 Skills + Cross-IDE</td>
18
+ <td align="center"><a href="https://skills.sh/SquabbyZ/peaks-cli">在 skills.sh 收录</a></td>
19
+ </tr>
20
+ <tr><td colspan="3">&nbsp;</td></tr>
21
+ <tr>
22
+ <td align="center" width="180"><b>⭐ STARS</b></td>
23
+ <td align="center" width="180"><b>📦 VERSION</b></td>
24
+ <td align="center" width="180"><b>📄 LICENSE</b></td>
25
+ </tr>
26
+ <tr>
27
+ <td align="center"><a href="https://github.com/SquabbyZ/peaks-cli/stargazers"><img src="https://img.shields.io/github/stars/SquabbyZ/peaks-cli?style=for-the-badge&logo=github&logoColor=white" alt="stars" /></a></td>
28
+ <td align="center"><a href="https://www.npmjs.com/package/peaks-cli"><img src="https://img.shields.io/npm/v/peaks-cli?style=for-the-badge&logo=npm&logoColor=white&color=CB3837" alt="npm version" /></a></td>
29
+ <td align="center"><a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=for-the-badge" alt="license" /></a></td>
30
+ </tr>
31
+ <tr><td colspan="3">&nbsp;</td></tr>
32
+ <tr>
33
+ <td align="center" width="180"><b>🧪 TESTS</b></td>
34
+ <td align="center" width="180"><b>🔧 LANG</b></td>
35
+ <td align="center" width="180"><b>📥 DOWNLOADS</b></td>
36
+ </tr>
37
+ <tr>
38
+ <td align="center"><b>2,800+</b></td>
39
+ <td align="center"><b>TypeScript</b></td>
40
+ <td align="center"><a href="https://www.npmjs.com/package/peaks-cli"><img src="https://img.shields.io/npm/dm/peaks-cli?style=for-the-badge&logo=npm&logoColor=white&color=CB3837" alt="downloads" /></a></td>
41
+ </tr>
42
+ <tr><td colspan="3">&nbsp;</td></tr>
43
+ <tr>
44
+ <td align="center" width="180"><b>🌐 中文</b></td>
45
+ <td align="center" width="180"><b>🚀 QUICK START</b></td>
46
+ <td align="center" width="180"><b>👁️ VISITORS</b></td>
47
+ </tr>
48
+ <tr>
49
+ <td align="center"><a href="./README.md">简体中文</a></td>
50
+ <td align="center"><a href="#-30-秒跑起来">30 秒跑起来 →</a></td>
51
+ <td align="center"><img src="https://komarev.com/ghpvc/?username=SquabbyZ&repo=peaks-cli&label=views&color=blue&style=for-the-badge" alt="visitors" /></td>
52
+ </tr>
53
+ </table>
54
+
55
+ <img src="https://github-readme-streak-stats.herokuapp.com?user=SquabbyZ&repo=peaks-cli&theme=radical&hide_border=true&date_format=j%20M%5B%20Y%5D" alt="GitHub Streak Stats" />
16
56
 
17
57
  [安装](#-30-秒跑起来) · [5 分钟上手](#-5-分钟上手) · [技能家族](#-11-个技能家族) · [杀手锏:不可绕过的门禁](#-杀手锏不可绕过的门禁)
18
58
 
@@ -221,6 +261,4 @@ peaks project dashboard / memories
221
261
 
222
262
  ⭐ [Star peaks-cli on GitHub](https://github.com/SquabbyZ/peaks-cli) · 🔍 [Browse on skills.sh](https://skills.sh/SquabbyZ/peaks-cli)
223
263
 
224
- 让你的 AI IDE 像一支训练有素的工程团队。
225
-
226
264
  </div>
@@ -27,10 +27,11 @@ export function runCapabilityMap(io, options) {
27
27
  }
28
28
  const config = readConfig();
29
29
  const installedCapabilityIds = getInstalledCapabilityIds(config);
30
+ const httpProxy = config.proxy?.httpProxy;
30
31
  printResult(io, ok('capabilities.map', createCapabilityMapPlan({
31
32
  source,
32
33
  installedCapabilityIds,
33
- ...(config.proxy.httpProxy === undefined ? {} : { httpProxy: config.proxy.httpProxy })
34
+ ...(httpProxy === undefined ? {} : { httpProxy })
34
35
  })), options.json);
35
36
  }
36
37
  export function getInstalledCapabilityIds(_config) {
@@ -108,7 +108,8 @@ export function registerWorkspaceCommands(program, io) {
108
108
  throw new Error(`--install-hooks must be one of: ask, auto, skip (got "${value}")`);
109
109
  }
110
110
  return value;
111
- })).action(async (options) => {
111
+ })
112
+ .option('--no-claude-hooks', 'do NOT materialize .claude/settings.local.json (slice 2.0.1-bug3 fact-forcing bypass). Default: hooks installed so tool calls inside .peaks/** are not blocked by the [Fact-Forcing Gate].')).action(async (options) => {
112
113
  try {
113
114
  // Resolve the session id. Two paths:
114
115
  // - explicit --session-id: use it as the requested binding target
@@ -168,7 +169,13 @@ export function registerWorkspaceCommands(program, io) {
168
169
  projectRoot,
169
170
  sessionId,
170
171
  allowSessionRebind: options.allowSessionRebind === true,
171
- ...(options.changeId !== undefined ? { changeId: options.changeId } : {})
172
+ ...(options.changeId !== undefined ? { changeId: options.changeId } : {}),
173
+ // Commander translates `--no-claude-hooks` into
174
+ // `options.claudeHooks = false`. The default (no flag) leaves
175
+ // `options.claudeHooks` undefined, which is not equal to
176
+ // `false`, so the default is "install hooks" (the bypass is
177
+ // on). Pass `--no-claude-hooks` to opt out.
178
+ noClaudeHooks: options.claudeHooks === false
172
179
  });
173
180
  const nextActions = [];
174
181
  if (report.previousSessionId !== null && report.bound) {
@@ -188,6 +195,28 @@ export function registerWorkspaceCommands(program, io) {
188
195
  else {
189
196
  nextActions.push('Run `peaks scan archetype --project <path> --json` next to populate rd/project-scan.md.');
190
197
  }
198
+ // Slice 2.0.1-bug3-fact-forcing-bypass: surface the consumer-
199
+ // project .claude/settings.local.json materialization outcome.
200
+ // When the bypass is in effect, the LLM knows subsequent Writes
201
+ // and Bash calls targeting .peaks/** will not be blocked by the
202
+ // [Fact-Forcing Gate]. When the user opted out, we surface a
203
+ // nextAction so the manual recovery is documented.
204
+ if (report.claudeSettings.action === 'written' || report.claudeSettings.action === 'refreshed') {
205
+ nextActions.push(`Materialized .claude/settings.local.json (action: ${report.claudeSettings.action}) — ` +
206
+ `the [Fact-Forcing Gate] is bypassed for tool calls inside .peaks/**. ` +
207
+ 'Restart Claude Code so the hooks take effect.');
208
+ }
209
+ else if (report.claudeSettings.action === 'already-current') {
210
+ // No-op: the bypass is already in effect and matches the
211
+ // current release. Do not spam the nextAction list on every
212
+ // init.
213
+ }
214
+ else if (report.claudeSettings.action === 'skipped') {
215
+ nextActions.push('Skipped .claude/settings.local.json materialization (--no-claude-hooks). ' +
216
+ 'If the [Fact-Forcing Gate] blocks subsequent Writes, run `peaks workspace init` ' +
217
+ 'again without --no-claude-hooks, or drop the contents of ' +
218
+ '`.peaks/.claude-settings-template.json` into `.claude/settings.local.json` manually.');
219
+ }
191
220
  // First-time hooks install decision. Sticky-marker at
192
221
  // .peaks/.peaks-init-hooks-decision.json records the user's answer
193
222
  // (or the auto-decision) so subsequent inits for new sessions in the
@@ -0,0 +1,20 @@
1
+ export type MessageRenderMode = 'tty' | 'plain';
2
+ export interface MessageRenderOptions {
3
+ mode: MessageRenderMode;
4
+ /**
5
+ * Optional override. When `true`, the renderer returns the input unchanged
6
+ * regardless of `mode`. Callers should set this when `NO_COLOR` is set,
7
+ * `--no-color` is passed, or `--json` is requested.
8
+ */
9
+ noColor?: boolean;
10
+ }
11
+ /**
12
+ * Render a human-readable message string for the terminal.
13
+ *
14
+ * Pure function. Returns the input unchanged when:
15
+ * - `input` is empty,
16
+ * - `input` is not a string (defensive: callers occasionally pass numbers/objects),
17
+ * - `mode === 'plain'`,
18
+ * - `noColor === true` (NO_COLOR / --no-color / --json opt-out).
19
+ */
20
+ export declare function renderMessage(input: string, options: MessageRenderOptions): string;
@@ -0,0 +1,80 @@
1
+ // Slice 2.0.1-ux-message-renderer — pure-function human-text renderer.
2
+ //
3
+ // PURE function contract:
4
+ // - No side effects (does not read process.stdout, does not call console.*).
5
+ // - Returns a string. The caller decides whether to write it to stdout.
6
+ // - Caller resolves the `mode` (TTY detection + opt-outs) and passes it in.
7
+ //
8
+ // Supported transformations (tty mode only — plain mode is a no-op pass-through):
9
+ // 1. OSC 8 hyperlink wrapping for http://, https://, and file:// URLs.
10
+ // Format: ESC]8;;URL ESC\ TEXT ESC]8;; ESC\
11
+ // 2. Markdown-lite: **bold** -> ANSI bold; `code` -> inverse-video.
12
+ // 3. Bullet markers (lines beginning with `- ` or `* `) are preserved as-is.
13
+ //
14
+ // `noColor: true` (caller's signal for `NO_COLOR` / `--no-color` / `--json`) forces
15
+ // the same pass-through behavior as `mode: 'plain'`, even if the caller somehow
16
+ // passed `mode: 'tty'`. This is a defence-in-depth opt-out: the caller should
17
+ // also pass `mode: 'plain'`, but we don't trust that either, because the JSON
18
+ // envelope contract must not leak escape sequences.
19
+ // OSC 8 escape sequence fragments. Format: ESC ] 8 ; ; URL ESC \ TEXT ESC ] 8 ; ; ESC \
20
+ // Browsers + terminals that understand OSC 8: Windows Terminal, iTerm2, WezTerm, recent GNOME Terminal.
21
+ const ESC = '';
22
+ const OSC8_OPEN = `${ESC}]8;;`;
23
+ const OSC8_SEP = `${ESC}\\`;
24
+ const OSC8_CLOSE = `${OSC8_OPEN}${OSC8_SEP}`;
25
+ // ANSI sequences used by the markdown-lite pass.
26
+ const ANSI_BOLD_OPEN = `${ESC}[1m`;
27
+ const ANSI_BOLD_CLOSE = `${ESC}[22m`;
28
+ const ANSI_INVERSE_OPEN = `${ESC}[7m`;
29
+ const ANSI_INVERSE_CLOSE = `${ESC}[27m`;
30
+ // Lightweight URL detector (deliberately not RFC-3986-perfect).
31
+ // Matches http(s):// and file:// tokens up to the first whitespace or
32
+ // common terminator. Trailing punctuation is captured as part of the URL,
33
+ // which is fine for display; the OSC 8 link still works because terminals
34
+ // re-tokenise on hover/click. To stay close to the slice spec's reference
35
+ // pattern we exclude <, >, ", ', and ` from URL characters.
36
+ const URL_PATTERN = /(https?:\/\/|file:\/\/)[^\s<>"'`]+/g;
37
+ // **bold** markers (non-greedy, multi-char safe). Allows ** at word boundaries.
38
+ const BOLD_PATTERN = /\*\*([^*\n]+?)\*\*/g;
39
+ // `inline-code` markers.
40
+ const CODE_PATTERN = /`([^`\n]+?)`/g;
41
+ /**
42
+ * Render a human-readable message string for the terminal.
43
+ *
44
+ * Pure function. Returns the input unchanged when:
45
+ * - `input` is empty,
46
+ * - `input` is not a string (defensive: callers occasionally pass numbers/objects),
47
+ * - `mode === 'plain'`,
48
+ * - `noColor === true` (NO_COLOR / --no-color / --json opt-out).
49
+ */
50
+ export function renderMessage(input, options) {
51
+ if (typeof input !== 'string' || input.length === 0) {
52
+ return input;
53
+ }
54
+ if (options.mode === 'plain' || options.noColor === true) {
55
+ return input;
56
+ }
57
+ // Markdown-lite first, then URL linking. The order matters: bold/code
58
+ // transformations can wrap parts of a URL (e.g. `\`code\`` containing a URL),
59
+ // so we link URLs on the already-formatted string. Either order would be
60
+ // acceptable; linking on the formatted string means a URL inside a code
61
+ // span still gets hyperlink-wrapped, which is the modern-terminal-friendly
62
+ // choice.
63
+ let out = applyMarkdownLite(input);
64
+ out = applyHyperlinks(out);
65
+ return out;
66
+ }
67
+ function applyMarkdownLite(input) {
68
+ let out = input.replace(BOLD_PATTERN, (_match, inner) => {
69
+ return `${ANSI_BOLD_OPEN}${inner}${ANSI_BOLD_CLOSE}`;
70
+ });
71
+ out = out.replace(CODE_PATTERN, (_match, inner) => {
72
+ return `${ANSI_INVERSE_OPEN}${inner}${ANSI_INVERSE_CLOSE}`;
73
+ });
74
+ return out;
75
+ }
76
+ function applyHyperlinks(input) {
77
+ return input.replace(URL_PATTERN, (url) => {
78
+ return `${OSC8_OPEN}${url}${OSC8_SEP}${url}${OSC8_CLOSE}`;
79
+ });
80
+ }
@@ -85,8 +85,27 @@ export function executeMigration(opts) {
85
85
  }
86
86
  savePreferences(opts.currentProjectRoot, overrides);
87
87
  }
88
- // 3. Slim config.json — only the schema version remains.
88
+ // 3. Slim config.json — schema version + discoverable ocr.llm placeholders.
89
+ // Per the 2.0.1 slim spec, the on-disk `~/.peaks/config.json` is
90
+ // `{ "version": "2.0.1", "ocr": { "llm": { ... } } }`. Legacy fields
91
+ // (language, model, economyMode, swarmMode, tokens, providers,
92
+ // proxy) live in <project>/.peaks/preferences.json. peaks-cli writes
93
+ // the `ocr.llm.*` placeholders so the user has a discoverable spot
94
+ // to paste their endpoint; the placeholders are empty strings, not
95
+ // auto-configured values, so the post-migration file MUST contain
96
+ // the `ocr.llm.*` block with empty defaults.
89
97
  mkdirSync(join(homedir(), '.peaks'), { recursive: true });
90
- writeFileSync(configPath, JSON.stringify({ version: CONFIG_SCHEMA_VERSION_V2 }, null, 2) + '\n', 'utf8');
98
+ writeFileSync(configPath, JSON.stringify({
99
+ version: CONFIG_SCHEMA_VERSION_V2,
100
+ ocr: {
101
+ llm: {
102
+ url: '',
103
+ authToken: '',
104
+ model: '',
105
+ useAnthropic: false,
106
+ authHeader: 'authorization'
107
+ }
108
+ }
109
+ }, null, 2) + '\n', 'utf8');
91
110
  return { ...plan, applied: true, backupPath: bak, newConfigPath: configPath };
92
111
  }
@@ -16,6 +16,7 @@ export { resolveProjectRootForConfig, resolveCanonicalProjectRoot } from './conf
16
16
  export declare function loadGlobalConfig(): ConfigV2 | null;
17
17
  export declare function isConfigLayer(value: string): value is ConfigLayer;
18
18
  export declare function isSensitiveConfigPath(path: string): boolean;
19
+ export declare function isLegacyConfigKey(path: string): boolean;
19
20
  export declare function containsSensitiveConfigValue(value: unknown): boolean;
20
21
  export type RedactedConfigValue = string | number | boolean | null | RedactedConfigValue[] | {
21
22
  [key: string]: RedactedConfigValue;
@@ -116,6 +116,26 @@ export function isSensitiveConfigPath(path) {
116
116
  const normalized = path.toLowerCase().replace(/[^a-z0-9]/g, '');
117
117
  return normalized.includes('apikey') || normalized.includes('accesskey') || normalized.includes('privatekey') || normalized.includes('token') || normalized.includes('secret') || normalized.includes('password') || normalized.includes('bearer') || normalized.includes('credential') || normalized.includes('auth');
118
118
  }
119
+ /**
120
+ * 2.0.1 slim-config contract: `~/.peaks/config.json` only stores
121
+ * `version` + `ocr.llm.*` placeholders. The 1.x → 2.0 migration
122
+ * moved per-project fields (`language`, `model`, `economyMode`,
123
+ * `swarmMode`) to `<project>/.peaks/preferences.json` (per spec
124
+ * §10.4). `setConfig` rejects writes to those keys and points the
125
+ * user to the preferences path; tokens / providers / proxy still
126
+ * live in `~/.peaks/config.json` (the loader is tolerant of them
127
+ * but does not synthesise defaults for them anymore).
128
+ */
129
+ const LEGACY_CONFIG_KEYS = new Set([
130
+ 'language',
131
+ 'model',
132
+ 'economyMode',
133
+ 'swarmMode'
134
+ ]);
135
+ export function isLegacyConfigKey(path) {
136
+ const topLevel = path.split(/[.[].*/, 1)[0] ?? '';
137
+ return LEGACY_CONFIG_KEYS.has(topLevel);
138
+ }
119
139
  function isProviderConfigPath(path) {
120
140
  return path === 'providers' || path.startsWith('providers.');
121
141
  }
@@ -551,6 +571,10 @@ export function setConfig(options) {
551
571
  if (!isConfigLayer(layer)) {
552
572
  throw new Error('Invalid config layer');
553
573
  }
574
+ if (isLegacyConfigKey(options.key)) {
575
+ throw new Error(`Legacy config key "${options.key}" is no longer stored in ~/.peaks/config.json. ` +
576
+ 'Set it under <project>/.peaks/preferences.json (e.g. `peaks preferences set --key <key> --value <value>`).');
577
+ }
554
578
  if (layer === 'project' && (isProviderConfigPath(options.key) || isProxyConfigPath(options.key) || isSensitiveConfigPath(options.key) || containsSensitiveConfigValue(options.value))) {
555
579
  throw new Error('Sensitive config keys must be stored in the user config layer');
556
580
  }
@@ -132,6 +132,21 @@ export type ConfigSetOptions = {
132
132
  value: unknown;
133
133
  layer?: ConfigLayer;
134
134
  };
135
+ /**
136
+ * 2.0.1 slim runtime default. The on-disk `~/.peaks/config.json`
137
+ * only carries `version` + `ocr.llm.*` placeholders. Legacy fields
138
+ * (language / model / economyMode / swarmMode / tokens / providers /
139
+ * proxy) live in `<project>/.peaks/preferences.json` (per spec
140
+ * §10.4) and are NOT synthesised here — `readConfig()` merges the
141
+ * user file over this default, and any legacy field that the user
142
+ * file still carries (1.x file) is exposed via `getConfig` for
143
+ * backward compatibility.
144
+ *
145
+ * Cast to `PeaksConfig` because the type still declares the legacy
146
+ * fields as required (they are part of the `readConfig()` contract
147
+ * for tolerant loading of pre-2.0.1 files); the runtime default
148
+ * itself does not supply them.
149
+ */
135
150
  export declare const DEFAULT_CONFIG: PeaksConfig;
136
151
  /**
137
152
  * Slim 2.0 schema for `~/.peaks/config.json`. After migration,
@@ -1,21 +1,30 @@
1
1
  import { CLI_VERSION } from '../../shared/version.js';
2
2
  import { CONFIG_SCHEMA_VERSION_V2 } from './config-migration.js';
3
+ /**
4
+ * 2.0.1 slim runtime default. The on-disk `~/.peaks/config.json`
5
+ * only carries `version` + `ocr.llm.*` placeholders. Legacy fields
6
+ * (language / model / economyMode / swarmMode / tokens / providers /
7
+ * proxy) live in `<project>/.peaks/preferences.json` (per spec
8
+ * §10.4) and are NOT synthesised here — `readConfig()` merges the
9
+ * user file over this default, and any legacy field that the user
10
+ * file still carries (1.x file) is exposed via `getConfig` for
11
+ * backward compatibility.
12
+ *
13
+ * Cast to `PeaksConfig` because the type still declares the legacy
14
+ * fields as required (they are part of the `readConfig()` contract
15
+ * for tolerant loading of pre-2.0.1 files); the runtime default
16
+ * itself does not supply them.
17
+ */
3
18
  export const DEFAULT_CONFIG = {
4
19
  version: CLI_VERSION,
5
- language: 'en',
6
- model: 'sonnet',
7
- economyMode: true,
8
- swarmMode: true,
9
- tokens: {},
10
- providers: {
11
- minimax: {
12
- model: 'minimax-2.7'
20
+ ocr: {
21
+ llm: {
22
+ url: '',
23
+ authToken: '',
24
+ model: '',
25
+ useAnthropic: false,
26
+ authHeader: 'authorization'
13
27
  }
14
- },
15
- proxy: {},
16
- progress: {
17
- enabled: true,
18
- heartbeatIntervalMs: 60000
19
28
  }
20
29
  };
21
30
  export function isConfigV2(raw) {
@@ -1,7 +1,6 @@
1
- import { DEFAULT_CONFIG } from './config-types.js';
2
1
  export const STRONGEST_MODEL_ID = 'claude-opus-4-7';
3
2
  export function getConfiguredExecutionModelId(providers) {
4
- const providerConfigs = Object.values(providers ?? DEFAULT_CONFIG.providers);
3
+ const providerConfigs = Object.values(providers ?? {});
5
4
  const configuredModel = providerConfigs
6
5
  .map((provider) => provider?.model?.trim())
7
6
  .find((model) => typeof model === 'string' && model.length > 0);
@@ -11,5 +10,8 @@ export function getConfiguredExecutionModelId(providers) {
11
10
  return configuredModel;
12
11
  }
13
12
  export function getEconomyAwareExecutionModelId(config) {
14
- return config.economyMode ? getConfiguredExecutionModelId(config.providers) : STRONGEST_MODEL_ID;
13
+ // Slice 2.0.1-bug1 round 3: economy is the project default. Treat undefined as enabled
14
+ // (matches the pre-slice implicit default from DEFAULT_CONFIG.economyMode = true). Only an
15
+ // explicit `economyMode === false` switches execution to STRONGEST_MODEL_ID.
16
+ return config.economyMode !== false ? getConfiguredExecutionModelId(config.providers) : STRONGEST_MODEL_ID;
15
17
  }
@@ -5,6 +5,22 @@ import { validateChangeIdOrThrow, buildArtifactRelativePath } from '../../shared
5
5
  import { WORKSPACE_UNAVAILABLE_NEXT_ACTIONS } from '../../shared/planner-response.js';
6
6
  import { getLocalArtifactPath, hasValidArtifactWorkspace } from '../artifacts/workspace-service.js';
7
7
  import { getConfiguredExecutionModelId, STRONGEST_MODEL_ID } from '../config/model-routing.js';
8
+ /**
9
+ * 2.0.1-bug1: the slim 2.0 `~/.peaks/config.json` no longer carries a
10
+ * `providers` block (legacy model config lives in
11
+ * `.peaks/preferences.json` per spec §10.4). `buildPlan` is a pure
12
+ * planner function and historically took its execution model from
13
+ * `DEFAULT_CONFIG.providers.minimax.model`; with the slim default
14
+ * that field is `undefined`, so `getConfiguredExecutionModelId`
15
+ * would throw. We retain the pre-2.0 default here as a literal so
16
+ * the planner remains usable when the caller has not passed an
17
+ * explicit `executionModelId` (unit tests, dry-run previews, the
18
+ * `peaks swarm plan` onboarding path). Production callers that
19
+ * have a real `ocr.llm.model` configured pass it via
20
+ * `request.executionModelId` (or via the legacy preferences.json
21
+ * bridge) and bypass this fallback.
22
+ */
23
+ const DEFAULT_EXECUTION_MODEL_ID = 'minimax-2.7';
8
24
  import { getTechStatus, TECH_REQUIRED_ARTIFACTS } from '../tech/tech-service.js';
9
25
  function normalizeGoal(goal) {
10
26
  const normalized = goal.trim();
@@ -136,6 +152,18 @@ function readArtifactFile(rootPath, artifactWorkspacePath, artifact) {
136
152
  return null;
137
153
  }
138
154
  }
155
+ function resolveExecutionModelId() {
156
+ try {
157
+ return getConfiguredExecutionModelId(undefined);
158
+ }
159
+ catch {
160
+ // 2.0.1-bug1: with the slim `~/.peaks/config.json` the legacy
161
+ // `providers` block is gone, so the configured-model lookup is
162
+ // expected to throw. Fall back to the pre-2.0 default so the
163
+ // planner remains usable in unit tests and the dry-run path.
164
+ return DEFAULT_EXECUTION_MODEL_ID;
165
+ }
166
+ }
139
167
  function getConcreteTargetAreas(request, artifactWorkspacePath, hasApprovedTechArtifacts) {
140
168
  if (!artifactWorkspacePath || !hasApprovedTechArtifacts || !hasPlannerArtifactWorkspace(request, artifactWorkspacePath)) {
141
169
  return [];
@@ -154,7 +182,7 @@ function buildPlan(request) {
154
182
  validateChangeIdOrThrow(request.changeId);
155
183
  const goal = normalizeGoal(request.goal);
156
184
  const swarmMode = request.swarmMode ?? true;
157
- const executionModelId = request.executionModelId?.trim() || getConfiguredExecutionModelId(undefined);
185
+ const executionModelId = request.executionModelId?.trim() || resolveExecutionModelId();
158
186
  const { workerTarget, blockedReasons } = resolveWorkerTarget(request.maxWorkers);
159
187
  const artifactWorkspacePath = resolveArtifactWorkspacePath(request);
160
188
  const artifactRoot = buildArtifactRelativePath(request.changeId, 'rd', 'swarm');