pluidr 0.4.0 → 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.
- package/README.md +115 -49
- package/package.json +4 -1
- package/src/cli/commands/doctor.js +99 -0
- package/src/cli/commands/init.js +14 -7
- package/src/cli/commands/uninstall.js +67 -0
- package/src/cli/commands/update.js +22 -0
- package/src/cli/index.js +42 -1
- package/src/cli/wizard/selectModelTier.js +22 -31
- package/src/core/agentPromptWriter.js +2 -2
- package/src/core/agentPromptWriter.test.js +56 -0
- package/src/core/backup.js +35 -5
- package/src/core/backup.test.js +51 -0
- package/src/core/configBuilder.js +13 -0
- package/src/core/configBuilder.test.js +47 -0
- package/src/core/configWriter.js +7 -4
- package/src/core/configWriter.test.js +26 -0
- package/src/core/identityHeader.test.js +15 -0
- package/src/core/paths.js +4 -15
- package/src/core/paths.test.js +25 -0
- package/src/core/pluginWriter.js +12 -8
- package/src/core/pluginWriter.test.js +41 -0
- package/src/core/squeezeInstaller.js +141 -0
- package/src/core/squeezeInstaller.test.js +77 -0
- package/src/plugins/README.md +29 -15
- package/src/plugins/{parent-session.js → pluidr-flow.js} +1 -6
- package/src/plugins/pluidr-squeeze.js +56 -0
- package/src/templates/agent-prompts/coder.txt +32 -4
- package/src/templates/agent-prompts/composer.txt +415 -0
- package/src/templates/agent-prompts/debugger.txt +55 -14
- package/src/templates/agent-prompts/fixer.txt +7 -0
- package/src/templates/agent-prompts/hierarchy.txt +11 -8
- package/src/templates/agent-prompts/plan-checker.txt +5 -5
- package/src/templates/agent-prompts/plan-writer.txt +3 -3
- package/src/templates/agent-prompts/reporter.txt +0 -4
- package/src/templates/agent-prompts/researcher.txt +3 -3
- package/src/templates/agent-prompts/reviewer.txt +16 -4
- package/src/templates/agent-prompts/tester.txt +10 -1
- package/src/templates/agent-prompts/writer.txt +4 -4
- package/src/templates/model-defaults.json +38 -2
- package/src/templates/opencode.config.json +93 -67
- package/src/templates/rtk-checksums.json +7 -0
- package/src/templates/agent-prompts/builder.txt +0 -107
- package/src/templates/agent-prompts/explorer.txt +0 -53
- package/src/templates/agent-prompts/planner.txt +0 -126
package/README.md
CHANGED
|
@@ -4,75 +4,108 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/pluidr)
|
|
5
5
|
[](https://github.com/funara/pluidr/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
**
|
|
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 **
|
|
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.
|
|
15
|
+
**1. Composer tab** — The single entry point for all feature work. Composer runs 3 strict, sequential phases:
|
|
16
16
|
|
|
17
|
-
**
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
| Debugger |
|
|
38
|
-
| Debugger |
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
│
|
|
54
|
+
▼
|
|
55
|
+
Composer — always starts in EXPLORE phase
|
|
56
|
+
│
|
|
57
|
+
┌───────┤
|
|
58
|
+
│ ▼
|
|
59
|
+
│ [Optional] Researcher → deep 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
|
-
| **
|
|
72
|
-
| **
|
|
73
|
-
| **
|
|
74
|
-
| **
|
|
75
|
-
| **
|
|
103
|
+
| **Phase-scoped delegation** | EXPLORE: researcher only. PLAN: plan-writer, plan-checker only. BUILD: coder, tester, reviewer, writer only |
|
|
104
|
+
| **Build gate order** | Coder → Tester → Reviewer → 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
|
-
###
|
|
143
|
+
### pluidr init
|
|
111
144
|
|
|
112
145
|
Prompts you to select models for two agent tiers, then:
|
|
113
146
|
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
- Copies
|
|
119
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|
package/src/cli/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -1,12 +1,53 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { resolve, dirname } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
1
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"
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../../package.json"), "utf-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`
|
|
2
21
|
|
|
3
22
|
export function run(argv) {
|
|
4
23
|
const cmd = argv[2]
|
|
5
24
|
|
|
25
|
+
if (cmd === "--help" || cmd === "-h") {
|
|
26
|
+
console.log(HELP)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
31
|
+
console.log(pkg.version)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
6
35
|
if (cmd === "init") {
|
|
7
36
|
return runInit()
|
|
8
37
|
}
|
|
9
38
|
|
|
10
|
-
|
|
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)
|
|
11
52
|
process.exit(1)
|
|
12
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
13
|
+
if (!destDir) destDir = getPromptsDir()
|
|
14
14
|
|
|
15
15
|
mkdirSync(destDir, { recursive: true })
|
|
16
16
|
|