pluidr 0.4.1 → 0.5.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 (44) hide show
  1. package/README.md +115 -49
  2. package/package.json +4 -1
  3. package/src/cli/commands/doctor.js +99 -0
  4. package/src/cli/commands/init.js +14 -7
  5. package/src/cli/commands/uninstall.js +67 -0
  6. package/src/cli/commands/update.js +22 -0
  7. package/src/cli/index.js +31 -1
  8. package/src/cli/wizard/selectModelTier.js +22 -31
  9. package/src/core/agentPromptWriter.js +2 -2
  10. package/src/core/agentPromptWriter.test.js +56 -0
  11. package/src/core/backup.js +35 -5
  12. package/src/core/backup.test.js +51 -0
  13. package/src/core/configBuilder.js +13 -0
  14. package/src/core/configBuilder.test.js +47 -0
  15. package/src/core/configWriter.js +7 -4
  16. package/src/core/configWriter.test.js +26 -0
  17. package/src/core/identityHeader.test.js +15 -0
  18. package/src/core/paths.js +4 -15
  19. package/src/core/paths.test.js +25 -0
  20. package/src/core/pluginWriter.js +12 -8
  21. package/src/core/pluginWriter.test.js +41 -0
  22. package/src/core/squeezeInstaller.js +141 -0
  23. package/src/core/squeezeInstaller.test.js +77 -0
  24. package/src/plugins/README.md +29 -15
  25. package/src/plugins/{parent-session.js → pluidr-flow.js} +1 -6
  26. package/src/plugins/pluidr-squeeze.js +56 -0
  27. package/src/templates/agent-prompts/coder.txt +32 -4
  28. package/src/templates/agent-prompts/composer.txt +415 -0
  29. package/src/templates/agent-prompts/debugger.txt +55 -14
  30. package/src/templates/agent-prompts/fixer.txt +7 -0
  31. package/src/templates/agent-prompts/hierarchy.txt +11 -8
  32. package/src/templates/agent-prompts/plan-checker.txt +5 -5
  33. package/src/templates/agent-prompts/plan-writer.txt +3 -3
  34. package/src/templates/agent-prompts/reporter.txt +0 -4
  35. package/src/templates/agent-prompts/researcher.txt +3 -3
  36. package/src/templates/agent-prompts/reviewer.txt +16 -4
  37. package/src/templates/agent-prompts/tester.txt +10 -1
  38. package/src/templates/agent-prompts/writer.txt +4 -4
  39. package/src/templates/model-defaults.json +38 -2
  40. package/src/templates/opencode.config.json +93 -67
  41. package/src/templates/rtk-checksums.json +7 -0
  42. package/src/templates/agent-prompts/builder.txt +0 -107
  43. package/src/templates/agent-prompts/explorer.txt +0 -53
  44. package/src/templates/agent-prompts/planner.txt +0 -126
package/README.md CHANGED
@@ -4,75 +4,108 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/pluidr)](https://www.npmjs.com/package/pluidr)
5
5
  [![License](https://img.shields.io/npm/l/pluidr)](https://github.com/funara/pluidr/blob/main/LICENSE)
6
6
 
7
- **Plan · Build · Review · Debug** — opinionated engineering workflow installer for [OpenCode](https://opencode.ai).
7
+ **Explore · Plan · Build · Debug** — opinionated engineering workflow installer for [OpenCode](https://opencode.ai).
8
8
 
9
9
  ## How it works
10
10
 
11
- Pluidr sets up a **14-agent** pipeline in OpenCode organized under **4 primary agents**, each with a dedicated tab and exclusive subagents. Every agent has a strict role, scoped permissions, and no shared subagents.
11
+ Pluidr sets up a **12-agent** pipeline in OpenCode organized under **2 primary agents**: **Composer** (orchestrating 7 subagents across 3 phases) and **Debugger** (standalone, 3 subagents). Every agent has a strict role, scoped permissions, and no shared subagents.
12
12
 
13
13
  ### Primary Agents
14
14
 
15
- **1. Explorer tab** (optional, research-only) Brainstorms with you, scans the codebase and web for context, then produces actionable recommendations. No subagents, no file editing. Handoff to Planner when you're ready to formalize.
15
+ **1. Composer tab** — The single entry point for all feature work. Composer runs 3 strict, sequential phases:
16
16
 
17
- **2. Planner tab** Turns your request into a verified PRD. It researches technical facts via Researcher, writes the spec via Plan-Writer, and validates it for completeness via Plan-Checker. On FAIL, Planner surfaces gap remedies to you via the question tool (you pick the fix direction) Planner never silently re-routes. After your input, Planner internally delegates to Researcher or Plan-Writer. Max 3 loops, then surfaces to you for direction. Planner never edits files directly.
17
+ - **EXPLORE phase** (mandatory start) Brainstorms with you, then delegates ALL research to the Researcher subagent Composer itself has no read/glob/grep/webfetch/websearch/bash permissions. Produces actionable recommendations with certainty marking. Uses a **guardrail gate**: must ask you "Ready to write the PRD?" before proceeding to Plan.
18
18
 
19
- **3. Builder tab** — Executes a confirmed PRD. It delegates implementation to Coder, tests via Tester, checks traceability with Reviewer, and produces a completion report via Writer. Builder never edits files or runs bash directly.
19
+ - **PLAN phase** — Turns findings into a verified PRD via Plan-Writer → Plan-Checker. Plan-Writer writes the PRD to `docs/plans/`. Plan-Checker validates against your original request (PASS/FAIL + gap list only). On FAIL, surfaces gap remedies to you via the question tool (max 3 loops). On PASS, uses a **guardrail gate**: must ask you "Build from this PRD?" before proceeding to Build.
20
20
 
21
- **4. Debugger tab** (standalone, user-triggered at any time) Root-cause analysis using the **Brooks-Lint methodology** (Iron Law + 6 Decay Risks). Delegates investigation to Inspector, fixes to Fixer, and reports to Reporter. Does not depend on Builder triggered directly by you.
21
+ - **BUILD phase** Executes the confirmed PRD via Coder Tester Reviewer Writer in strict sequence. Coder implements, Tester validates (PASS/FAIL/BLOCKED), Reviewer gates against the PRD definition-of-done, Writer produces a completion report to `docs/reports/`. Coder→Coder loops without verification are forbidden. Max 3 consecutive FAILs before surfacing to you.
22
+
23
+ Composer never edits files or runs bash directly — all work is delegated to subagents.
24
+
25
+ After presenting the completion report, Composer resets to EXPLORE phase and asks what you'd like to do next: start a new feature, switch to Debugger for bugs, or iterate on the current result.
26
+
27
+ **2. Debugger tab** (standalone, user-triggered at any time) — Root-cause analysis using the **Brooks-Lint methodology** (Iron Law + 6 Decay Risks). Delegates investigation to Inspector, fixes to Fixer, and reports to Reporter. Does not depend on Composer — triggered directly by you.
28
+ Debugger never reads files, edits code, or runs bash directly — all investigation, fixes, and reports are delegated to subagents.
29
+
30
+ After presenting the investigation outcome, Debugger resets and asks what you'd like to do next: investigate a new bug, switch to Composer for feature work, or re-investigate with new information.
22
31
 
23
32
  ### Exclusive Subagents
24
33
 
25
34
  Each subagent belongs to exactly one primary agent and cannot be invoked by anyone else:
26
35
 
27
- | Primary | Subagents | Role |
28
- |---------|-----------|------|
29
- | Explorer | *(none)* | Research + recommendations only |
30
- | Planner | Researcher | Technical/codebase fact-finding (confirmed_facts/inferred_facts/unknowns/risks) |
31
- | Planner | Plan-Writer | Stateless PRD formatter missing input = TBD |
32
- | Planner | Plan-Checker | **Gate** validates PRD against original request, PASS/FAIL + gap list only |
33
- | Builder | Coder | Writes and edits implementation code |
34
- | Builder | Tester | Runs tests, reports PASS/FAIL/BLOCKED + coverage gaps |
35
- | Builder | Reviewer | **Gate** compares implementation against definition-of-done, PASS/FAIL + gap list only |
36
- | Builder | Writer | Stateless document formatter completion reports |
37
- | Debugger | Inspector | Brooks-Lint RCA (Iron Law + 6 decay risks + 4 review modes) |
38
- | Debugger | Fixer | Applies minimal, root-cause-targeted fixes |
39
- | Debugger | Reporter | Stateless diagnosis report formatter (Iron Law structure) |
36
+ | Primary | Phase | Subagents | Role |
37
+ |---------|-------|-----------|------|
38
+ | Composer | EXPLORE | Researcher | Technical/codebase fact-finding (confirmed_facts/inferred_facts/unknowns/risks) |
39
+ | Composer | PLAN | Plan-Writer | Stateless PRD formatter — writes to `docs/plans/`, missing input = TBD |
40
+ | Composer | PLAN | Plan-Checker | **Gate** — validates PRD against original request, PASS/FAIL + gap list only |
41
+ | Composer | BUILD | Coder | Writes and edits implementation code |
42
+ | Composer | BUILD | Tester | Runs tests, reports PASS/FAIL/BLOCKED + coverage gaps |
43
+ | Composer | BUILD | Reviewer | **Gate** — compares implementation against definition-of-done, PASS/FAIL + gap list only |
44
+ | Composer | BUILD | Writer | Stateless document formatter writes completion reports to `docs/reports/` |
45
+ | Debugger | DEBUG | Inspector | Brooks-Lint RCA (Iron Law + 6 decay risks + 4 review modes) |
46
+ | Debugger | DEBUG | Fixer | Applies minimal, root-cause-targeted fixes |
47
+ | Debugger | DEBUG | Reporter | Stateless diagnosis report formatter (Iron Law structure) |
40
48
 
41
49
  ### Workflow
42
50
 
43
51
  ```
44
52
  You describe a feature / idea
45
-
46
-
47
- (Optional) Explorer research + recommendations → User decides
48
-
49
-
50
- Planner → (if needed) Researcher → Plan-Writer writes PRD →
51
- Plan-Checker validatesyou confirm → handoff to Builder
52
-
53
-
54
- BuilderCoder implements Tester runs tests →
55
- Reviewer checks DoD → Writer produces completion report
53
+
54
+
55
+ Composer always starts in EXPLORE phase
56
+
57
+ ┌───────┤
58
+ │ ▼
59
+ │ [Optional] Researcherdeep fact-finding
60
+
61
+ │ Synthesize findings → Internally assess complexity
62
+ │ ├── Simple GUARDRAIL GATE 1a (question tool):
63
+ │ "Ready to build directly?"
64
+ │ │ ├── "Yes" → BUILD phase
65
+ │ │ ├── "Write a PRD first" → PLAN phase
66
+ │ │ └── "More research" → continue exploring
67
+ │ └── Complex → GUARDRAIL GATE 1b (question tool):
68
+ │ "Ready to write the PRD?"
69
+ │ ├── "Yes" → PLAN phase
70
+ │ └── "More research" → continue exploring
71
+ │ │ │
72
+ │ ▼ (PLAN) ▼ (direct BUILD)
73
+ │ Plan-Writer → docs/plans/ Coder → Tester →
74
+ │ Plan-Checker validates Reviewer → Writer → docs/reports/
75
+ │ │
76
+ │ ▼ (FAIL, max 3 loops)
77
+ │ Surface gaps → you pick remedy
78
+ │ │
79
+ │ ▼ (PASS)
80
+ │ GUARDRAIL GATE 2 (question tool):
81
+ │ "Build from this PRD?"
82
+ │ └── "Yes" → BUILD phase
83
+ │ Coder → Tester → Reviewer → Writer → docs/reports/
56
84
 
57
85
 
58
86
  You review the result
59
87
 
60
- (If bug found at any point → Debugger → Inspector → Fixer →
61
- Reporter → you verify)
88
+ (If bug found at any point → Debugger → Inspector → Fixer → Reporter → you verify)
62
89
  ```
63
90
 
64
91
  ### Flow Rules
65
92
 
66
93
  | Rule | Detail |
67
94
  |------|--------|
95
+ | **Mandatory EXPLORE start** | Composer always starts in EXPLORE phase — no skipping to Plan or Build without passing a guardrail gate |
96
+ | **Complexity assessment** | Composer internally evaluates if the feature is simple (few files, low risk, clear approach) — this is LLM judgment, not user choice |
97
+ | **Guardrail Gate 1** | Simple → question tool: "Ready to build directly?" Complex → question tool: "Ready to write the PRD?" Both paths require user confirmation |
98
+ | **Guardrail Gate 2** | PLAN→BUILD: question tool asks "Build from this PRD?" with explicit confirmation |
99
+ | **Direct build path** | Simple features can skip PLAN phase — Composer determines simplicity, user just confirms |
68
100
  | **Gate agents** | Plan-Checker and Reviewer output PASS/FAIL + gap list only — no improvement suggestions, no decisions |
69
101
  | **Researcher agents** | Researcher and Inspector output confirmed_facts/inferred_facts/unknowns/risks — no recommendations |
70
102
  | **Writer agents** | Plan-Writer, Writer, and Reporter are stateless formatters — missing input = TBD, never invent content |
71
- | **Build gate order** | Coder Tester Reviewer Writer (Builder orchestrates in sequence) |
72
- | **Planner FAIL loop** | Plan-checker FAIL Planner surfaces gap remedies to you via question tool (MC options) you pick Planner internally routes to Researcher or Plan-Writer. Max 3 loops, then surfaces to you for direction |
73
- | **Builder FAIL loop** | 3 consecutive FAILs from Tester or ReviewerBuilder surfaces to you |
74
- | **No shared subagents** | Each subagent belongs to exactly one primary no cross-primary delegation |
75
- | **Debugger independence** | Debugger is standalone does not flow through Builder, triggered directly by you |
103
+ | **Phase-scoped delegation** | EXPLORE: researcher only. PLAN: plan-writer, plan-checker only. BUILD: coder, tester, reviewer, writer only |
104
+ | **Build gate order** | CoderTesterReviewer → Writer no skipping, no Coder→Coder without verification |
105
+ | **Plan-Checker FAIL loop** | Plan-checker FAIL Composer surfaces gap remedies to you via question tool (MC options) you pick → Composer routes to Plan-Writer. Max 3 loops, then surfaces to you for direction |
106
+ | **Build FAIL loop** | 3 consecutive FAILs from Tester or Reviewer Composer surfaces to you |
107
+ | **Output directories** | Plan-Writer writes to `docs/plans/`. Writer writes to `docs/reports/`. Enforced by permissions |
108
+ | **Debugger independence** | Debugger is standalone — does not flow through Composer, triggered directly by you |
76
109
 
77
110
  ### Brooks-Lint Methodology (Debugger)
78
111
 
@@ -107,30 +140,63 @@ npx pluidr init
107
140
 
108
141
  ## Usage
109
142
 
110
- ### `pluidr init`
143
+ ### pluidr init
111
144
 
112
145
  Prompts you to select models for two agent tiers, then:
113
146
 
114
- - Builds a complete `opencode.jsonc` config with the chosen models injected into the right agents
115
- - Backs up any existing config at `~/.config/opencode/opencode.jsonc` to `opencode.jsonc.bak`
116
- - Writes the new config to `~/.config/opencode/opencode.jsonc`
117
- - Copies agent system-prompt files into `~/.config/opencode/prompts/`
118
- - Copies the bundled `parent-session` plugin into `~/.config/opencode/plugins/`
119
- - Writes a `package.json` into `~/.config/opencode/` declaring `@opencode-ai/plugin` as a dependency (OpenCode installs it automatically on first launch via its bundled Bun runtime)
147
+ - Asks whether to install the pluidr-squeeze plugin (can decline squeeze download)
148
+ - Builds a complete opencode.jsonc config with the chosen models injected into the right agents
149
+ - Backs up any existing config at ~/.config/opencode/opencode.jsonc to opencode.jsonc.bak
150
+ - Writes the new config to ~/.config/opencode/opencode.jsonc
151
+ - Copies agent system-prompt files into ~/.config/opencode/prompts/
152
+ - Copies the bundled pluidr-flow and pluidr-squeeze plugins into ~/.config/opencode/plugins/
153
+ - Writes a package.json into ~/.config/opencode/ declaring @opencode-ai/plugin as a dependency (OpenCode installs it automatically on first launch via its bundled Bun runtime)
120
154
 
121
155
  On completion, prints:
122
- ```
156
+ `
123
157
  Pluidr setup complete!
124
158
  You can update your agent model settings later in opencode.jsonc
125
- ```
126
- The filename `opencode.jsonc` (second line) is a clickable terminal hyperlink — Ctrl+click to open the config in your default editor.
159
+ `
160
+ The filename opencode.jsonc (second line) is a clickable terminal hyperlink — Ctrl+click to open the config in your default editor.
161
+
162
+ ### pluidr uninstall
163
+
164
+ Removes Pluidr artifacts and restores your previous configuration:
165
+
166
+ - Finds the latest timestamped backup (opencode.jsonc.bak.*) in ~/.config/opencode/
167
+ - Restores it to opencode.jsonc
168
+ - Removes prompts/, plugins/, and bin/ directories
169
+ - Does NOT remove opencode.jsonc itself (preserves user customizations)
170
+ - Does NOT remove package.json (may be used by other plugins)
171
+ - Prints a summary of what was removed and restored
172
+
173
+ ### pluidr update
174
+
175
+ Re-runs the setup wizard (same as pluidr init). Before writing, warns if existing config is found and asks for confirmation.
176
+
177
+ ### pluidr doctor
178
+
179
+ Checks installation health and reports PASS/FAIL for each component:
180
+
181
+ - opencode.jsonc exists
182
+ - All 13 prompt files exist
183
+ - Both plugin files exist
184
+ - package.json with @opencode-ai/plugin dependency
185
+ - squeeze binary is available
186
+ - Config is valid JSON
187
+
188
+ Prints a summary table with ✓/✗ status. Exits with code 0 if all pass, 1 if any fail.
127
189
 
128
190
  ## Bundled plugins
129
191
 
130
- Pluidr ships with one plugin: [`parent-session`](src/plugins/README.md), which gives subagents three tools for cross-session context access:
192
+ Pluidr ships with two plugins:
193
+
194
+ **`pluidr-flow`** — provides subagents with three tools for cross-session context access:
131
195
 
132
196
  - `parent_session_messages` — read the parent session's transcript
133
197
  - `session_messages(sessionId)` — read any session by ID
134
198
  - `session_messages_batch(sessionIds)` — read multiple sessions in one call
135
199
 
136
- `pluidr init` installs the plugin and its dependency declaration automatically no extra user action. On OpenCode's first launch, the bundled Bun runtime installs `@opencode-ai/plugin` from the generated `package.json`, then the plugin's tools become available to all subagents.
200
+ **`pluidr-squeeze`** hooks into tool execution to rewrite bash commands through the `squeeze` binary, filtering verbose output and saving 60-90% of tokens across all agents.
201
+
202
+ `pluidr init` installs both plugins and their dependency declaration automatically — no extra user action. `pluidr-squeeze` also downloads the engine to `~/.config/opencode/bin/`. On OpenCode's first launch, the bundled Bun runtime installs `@opencode-ai/plugin` from the generated `package.json`, then both plugins become available to all agents.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pluidr",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Opinionated Engineering Workflow for OpenCode — Plan, Build, Review.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -16,6 +16,9 @@
16
16
  ],
17
17
  "license": "MIT",
18
18
  "type": "module",
19
+ "scripts": {
20
+ "test": "node --test"
21
+ },
19
22
  "bin": {
20
23
  "pluidr": "bin/pluidr.js"
21
24
  },
@@ -0,0 +1,99 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs"
2
+ import { join, dirname } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+ import { execFileSync } from "node:child_process"
5
+ import { getConfigDir, getConfigPath, getPromptsDir } from "../../core/paths.js"
6
+ import { findRtkPath } from "../../core/squeezeInstaller.js"
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url))
9
+ const EXPECTED_PROMPT_COUNT = 13
10
+ const PLUGIN_FILES = ["pluidr-flow.js", "pluidr-squeeze.js"]
11
+
12
+ export async function runDoctor() {
13
+ const configDir = getConfigDir()
14
+ const configPath = getConfigPath()
15
+ const promptsDir = getPromptsDir()
16
+ const pluginsDir = join(configDir, "plugins")
17
+ const packageJsonPath = join(configDir, "package.json")
18
+ const binDir = join(configDir, "bin")
19
+
20
+ const checks = []
21
+
22
+ // 1. opencode.jsonc exists
23
+ const configExists = existsSync(configPath)
24
+ checks.push({ component: "opencode.jsonc", pass: configExists })
25
+
26
+ // 2. All 13 prompt files exist
27
+ let promptCount = 0
28
+ if (existsSync(promptsDir)) {
29
+ try {
30
+ const files = readdirSync(promptsDir).filter((f) => f.endsWith(".txt"))
31
+ promptCount = files.length
32
+ } catch {
33
+ promptCount = 0
34
+ }
35
+ }
36
+ checks.push({ component: "13 prompt files", pass: promptCount === EXPECTED_PROMPT_COUNT, detail: `${promptCount}/${EXPECTED_PROMPT_COUNT}` })
37
+
38
+ // 3. Both plugin files exist
39
+ let pluginsFound = 0
40
+ for (const f of PLUGIN_FILES) {
41
+ if (existsSync(join(pluginsDir, f))) pluginsFound++
42
+ }
43
+ checks.push({ component: "plugin files", pass: pluginsFound === PLUGIN_FILES.length, detail: `${pluginsFound}/${PLUGIN_FILES.length}` })
44
+
45
+ // 4. package.json with @opencode-ai/plugin dependency
46
+ let pkgValid = false
47
+ if (existsSync(packageJsonPath)) {
48
+ try {
49
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"))
50
+ pkgValid = !!(pkg.dependencies && pkg.dependencies["@opencode-ai/plugin"])
51
+ } catch {
52
+ pkgValid = false
53
+ }
54
+ }
55
+ checks.push({ component: "package.json + plugin dep", pass: pkgValid })
56
+
57
+ // 5. RTK binary exists
58
+ let rtkFound = false
59
+ const rtkOnPath = findRtkPath()
60
+ if (rtkOnPath) {
61
+ rtkFound = true
62
+ } else {
63
+ const binName = process.platform === "win32" ? "rtk.exe" : "rtk"
64
+ rtkFound = existsSync(join(binDir, binName))
65
+ }
66
+ checks.push({ component: "squeeze binary", pass: rtkFound })
67
+
68
+ // 6. Config is valid JSON/JSONC
69
+ let configValid = false
70
+ if (configExists) {
71
+ try {
72
+ const content = readFileSync(configPath, "utf-8")
73
+ const cleaned = content.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m)
74
+ JSON.parse(cleaned)
75
+ configValid = true
76
+ } catch {
77
+ configValid = false
78
+ }
79
+ }
80
+ checks.push({ component: "config JSON validity", pass: configValid })
81
+
82
+ // Print summary table
83
+ const allPass = checks.every((c) => c.pass)
84
+ console.log("\nPluidr Doctor — Installation Health Check\n")
85
+ for (const c of checks) {
86
+ const icon = c.pass ? "\u2713" : "\u2717"
87
+ const detail = c.detail ? ` (${c.detail})` : ""
88
+ console.log(` ${icon} ${c.component}${detail}`)
89
+ }
90
+ console.log("")
91
+
92
+ if (allPass) {
93
+ console.log("All checks passed.")
94
+ } else {
95
+ console.log("Some checks failed. Re-run `pluidr init` to repair.")
96
+ }
97
+
98
+ process.exit(allPass ? 0 : 1)
99
+ }
@@ -1,29 +1,32 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { resolve, dirname } from "node:path"
3
3
  import { fileURLToPath } from "node:url"
4
+ import { confirm, isCancel } from "@clack/prompts"
4
5
  import { selectModelTier } from "../wizard/selectModelTier.js"
5
6
  import { backupExistingConfig } from "../../core/backup.js"
6
7
  import { buildConfig } from "../../core/configBuilder.js"
7
8
  import { writeConfig } from "../../core/configWriter.js"
8
9
  import { writeAgentPrompts } from "../../core/agentPromptWriter.js"
9
10
  import { writePluginBundle, writePluginPackageJson } from "../../core/pluginWriter.js"
11
+ import { installSqueeze } from "../../core/squeezeInstaller.js"
10
12
  import { getConfigPath } from "../../core/paths.js"
11
13
 
12
14
  const __dirname = dirname(fileURLToPath(import.meta.url))
13
15
  const TEMPLATES_DIR = resolve(__dirname, "../../templates")
14
16
 
15
- function makeFileHyperlink(absolutePath, displayText) {
16
- const normalized = absolutePath.replace(/\\/g, "/").replace(/^\//, "")
17
- const fileUrl = `file:///${normalized}`
18
- return `\x1b]8;;${fileUrl}\x1b\\${displayText}\x1b]8;;\x1b\\`
19
- }
20
-
21
17
  export async function runInit() {
22
18
  const defaultsPath = resolve(TEMPLATES_DIR, "model-defaults.json")
23
19
  const modelDefaults = JSON.parse(readFileSync(defaultsPath, "utf-8"))
24
20
 
25
21
  const tierChoices = await selectModelTier(modelDefaults)
26
22
 
23
+ const squeezeAnswer = await confirm({
24
+ message: "Install pluidr-squeeze plugin? Reduces bash output tokens by 60-90%.",
25
+ initialValue: true,
26
+ })
27
+
28
+ const installSqueezePlugin = isCancel(squeezeAnswer) ? true : squeezeAnswer
29
+
27
30
  backupExistingConfig()
28
31
 
29
32
  const configObject = buildConfig(tierChoices, TEMPLATES_DIR)
@@ -31,6 +34,10 @@ export async function runInit() {
31
34
  writeAgentPrompts(TEMPLATES_DIR)
32
35
  writePluginBundle()
33
36
  writePluginPackageJson()
37
+ if (installSqueezePlugin) {
38
+ await installSqueeze()
39
+ }
34
40
 
35
- console.log(`Pluidr setup complete!\nYou can update your agent model settings later in ${makeFileHyperlink(getConfigPath(), "opencode.jsonc")}`)
41
+ const path = getConfigPath().replace(/\\/g, "/").replace(/^\//, "")
42
+ console.log(`Pluidr setup complete!\nYou can update your agent model settings later in \x1b]8;;file:///${path}\x1b\\opencode.jsonc\x1b]8;;\x1b\\`)
36
43
  }
@@ -0,0 +1,67 @@
1
+ import { existsSync, copyFileSync, rmSync, readdirSync } from "node:fs"
2
+ import { join, dirname } from "node:path"
3
+ import { getConfigDir, getConfigPath } from "../../core/paths.js"
4
+
5
+ function findLatestBackup() {
6
+ const dir = getConfigDir()
7
+ let backups = []
8
+ try {
9
+ backups = readdirSync(dir)
10
+ .filter((f) => f.startsWith("opencode.jsonc.bak."))
11
+ .sort()
12
+ .reverse()
13
+ } catch {
14
+ return null
15
+ }
16
+ return backups.length > 0 ? join(dir, backups[0]) : null
17
+ }
18
+
19
+ export async function runUninstall() {
20
+ const configDir = getConfigDir()
21
+ const configPath = getConfigPath()
22
+ const promptsDir = join(configDir, "prompts")
23
+ const pluginsDir = join(configDir, "plugins")
24
+ const binDir = join(configDir, "bin")
25
+
26
+ const summary = { restored: null, removed: [] }
27
+
28
+ // Restore latest backup
29
+ const latestBackup = findLatestBackup()
30
+ if (latestBackup && existsSync(configPath)) {
31
+ copyFileSync(latestBackup, configPath)
32
+ summary.restored = latestBackup
33
+ } else if (latestBackup) {
34
+ // Config doesn't exist but backup does — restore anyway
35
+ copyFileSync(latestBackup, configPath)
36
+ summary.restored = latestBackup
37
+ }
38
+
39
+ // Remove Pluidr-installed artifacts
40
+ if (existsSync(promptsDir)) {
41
+ rmSync(promptsDir, { recursive: true, force: true })
42
+ summary.removed.push("prompts/")
43
+ }
44
+ if (existsSync(pluginsDir)) {
45
+ rmSync(pluginsDir, { recursive: true, force: true })
46
+ summary.removed.push("plugins/")
47
+ }
48
+ if (existsSync(binDir)) {
49
+ rmSync(binDir, { recursive: true, force: true })
50
+ summary.removed.push("bin/")
51
+ }
52
+
53
+ // Print summary
54
+ console.log("Uninstall complete:")
55
+ if (summary.restored) {
56
+ console.log(` ✓ Restored ${summary.restored}`)
57
+ } else {
58
+ console.log(" - No backup found to restore")
59
+ }
60
+ if (summary.removed.length > 0) {
61
+ console.log(` ✓ Removed: ${summary.removed.join(", ")}`)
62
+ } else {
63
+ console.log(" - No Pluidr artifacts found to remove")
64
+ }
65
+ console.log(" - Kept opencode.jsonc (preserves user customizations)")
66
+ console.log(" - Kept package.json (may be used by other plugins)")
67
+ }
@@ -0,0 +1,22 @@
1
+ import { existsSync } from "node:fs"
2
+ import { confirm, isCancel } from "@clack/prompts"
3
+ import { getConfigPath } from "../../core/paths.js"
4
+ import { runInit } from "./init.js"
5
+
6
+ export async function runUpdate() {
7
+ const configPath = getConfigPath()
8
+
9
+ if (existsSync(configPath)) {
10
+ const answer = await confirm({
11
+ message: "Existing Pluidr config found. Overwrite? (Y/n)",
12
+ initialValue: true,
13
+ })
14
+
15
+ if (isCancel(answer) || !answer) {
16
+ console.log("Update cancelled.")
17
+ return
18
+ }
19
+ }
20
+
21
+ await runInit()
22
+ }
package/src/cli/index.js CHANGED
@@ -2,13 +2,31 @@ import { readFileSync } from "node:fs"
2
2
  import { resolve, dirname } from "node:path"
3
3
  import { fileURLToPath } from "node:url"
4
4
  import { runInit } from "./commands/init.js"
5
+ import { runUninstall } from "./commands/uninstall.js"
6
+ import { runUpdate } from "./commands/update.js"
7
+ import { runDoctor } from "./commands/doctor.js"
5
8
 
6
9
  const __dirname = dirname(fileURLToPath(import.meta.url))
7
10
  const pkg = JSON.parse(readFileSync(resolve(__dirname, "../../package.json"), "utf-8"))
8
11
 
12
+ const HELP = `Usage: pluidr <command>
13
+
14
+ Commands:
15
+ init Set up OpenCode with Pluidr's 12-agent pipeline
16
+ uninstall Remove Pluidr artifacts and restore previous config
17
+ update Re-run setup (overwrites current config)
18
+ doctor Verify installation health
19
+ --version, -v Print version number
20
+ --help, -h Show this help message`
21
+
9
22
  export function run(argv) {
10
23
  const cmd = argv[2]
11
24
 
25
+ if (cmd === "--help" || cmd === "-h") {
26
+ console.log(HELP)
27
+ return
28
+ }
29
+
12
30
  if (cmd === "--version" || cmd === "-v") {
13
31
  console.log(pkg.version)
14
32
  return
@@ -18,6 +36,18 @@ export function run(argv) {
18
36
  return runInit()
19
37
  }
20
38
 
21
- console.log("Usage: pluidr init")
39
+ if (cmd === "uninstall") {
40
+ return runUninstall()
41
+ }
42
+
43
+ if (cmd === "update") {
44
+ return runUpdate()
45
+ }
46
+
47
+ if (cmd === "doctor") {
48
+ return runDoctor()
49
+ }
50
+
51
+ console.log(HELP)
22
52
  process.exit(1)
23
53
  }
@@ -7,42 +7,33 @@ const TIER_LABELS = {
7
7
 
8
8
  export async function selectModelTier(modelDefaults) {
9
9
  const choices = {}
10
-
11
10
  for (const [tierKey, tierInfo] of Object.entries(modelDefaults)) {
12
11
  const defaultModel = `${tierInfo.provider}/${tierInfo.model}`
13
12
  const label = TIER_LABELS[tierKey] ?? tierKey
14
13
 
15
- let selectedModel = defaultModel
16
-
17
- try {
18
- const answer = await select({
19
- message: label,
20
- options: [
21
- { value: defaultModel, label: `default (${defaultModel})` },
22
- { value: "__custom__", label: "custom (provider/model)" },
23
- ],
24
- })
25
-
26
- if (isCancel(answer)) {
27
- selectedModel = defaultModel
28
- } else if (answer === "__custom__") {
29
- try {
30
- const customModel = await text({
31
- message: "Enter model:",
32
- })
33
- selectedModel = isCancel(customModel) ? defaultModel : (customModel || defaultModel)
34
- } catch {
35
- selectedModel = defaultModel
36
- }
37
- } else {
38
- selectedModel = answer
39
- }
40
- } catch {
41
- selectedModel = defaultModel
14
+ const recommendedOptions = (tierInfo.recommended || []).map((r) => ({
15
+ value: r.value,
16
+ label: r.label,
17
+ }))
18
+
19
+ const options = [
20
+ ...recommendedOptions,
21
+ { value: "__custom__", label: "custom (enter provider/model)" },
22
+ ]
23
+
24
+ const answer = await select({
25
+ message: label,
26
+ options,
27
+ })
28
+
29
+ if (isCancel(answer)) {
30
+ choices[tierKey] = defaultModel
31
+ } else if (answer === "__custom__") {
32
+ const customModel = await text({ message: "Enter model:" })
33
+ choices[tierKey] = isCancel(customModel) ? defaultModel : (customModel || defaultModel)
34
+ } else {
35
+ choices[tierKey] = answer
42
36
  }
43
-
44
- choices[tierKey] = selectedModel
45
37
  }
46
-
47
38
  return choices
48
39
  }
@@ -8,9 +8,9 @@ import IDENTITY_HEADER from "./identityHeader.js"
8
8
  // copied verbatim — no agent-identity header.
9
9
  const REFERENCE_FILES = new Set(["hierarchy"])
10
10
 
11
- export function writeAgentPrompts(templatesDir) {
11
+ export function writeAgentPrompts(templatesDir, destDir) {
12
12
  const sourceDir = resolve(templatesDir, "agent-prompts")
13
- const destDir = getPromptsDir()
13
+ if (!destDir) destDir = getPromptsDir()
14
14
 
15
15
  mkdirSync(destDir, { recursive: true })
16
16