@nforma.ai/nforma 0.2.1
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/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,1371 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qgsd:mcp-setup
|
|
3
|
+
description: Configure quorum agents — first-run linear onboarding for new installs, live-status agent menu for re-runs
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Bash
|
|
6
|
+
- Read
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<objective>
|
|
10
|
+
Configure QGSD quorum agents in `~/.claude.json`. Detects whether any MCP servers are configured and routes to the appropriate flow:
|
|
11
|
+
- **First-run** (zero configured entries): linear onboarding — select agent templates, collect API keys via keytar, write batch changes with backup, restart agents
|
|
12
|
+
- **Re-run** (existing entries): live-status agent roster menu — view model/provider/key status, select agent, choose action (set key / swap provider / remove)
|
|
13
|
+
</objective>
|
|
14
|
+
|
|
15
|
+
<process>
|
|
16
|
+
|
|
17
|
+
## Step 1: Detect first-run vs re-run
|
|
18
|
+
|
|
19
|
+
Run this Bash command and store the output as SETUP_INFO:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
SETUP_INFO=$(node -e "
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
|
|
27
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
28
|
+
let claudeJson = {};
|
|
29
|
+
try {
|
|
30
|
+
claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// Missing or corrupt: treat as fresh install
|
|
33
|
+
}
|
|
34
|
+
const servers = claudeJson.mcpServers || {};
|
|
35
|
+
const configured = Object.entries(servers).filter(([k, v]) => v && v.command && v.args);
|
|
36
|
+
const isFirstRun = configured.length === 0;
|
|
37
|
+
const result = { isFirstRun, configuredCount: configured.length, agentKeys: configured.map(([k]) => k) };
|
|
38
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
39
|
+
")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Parse SETUP_INFO JSON for: `isFirstRun` (boolean), `configuredCount` (int), `agentKeys` (array).
|
|
43
|
+
|
|
44
|
+
**If `isFirstRun` is true:** Continue to Step 2 (first-run flow).
|
|
45
|
+
|
|
46
|
+
**If `isFirstRun` is false:** Skip to the Re-run Agent Menu section below.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Step 2: First-run onboarding flow
|
|
51
|
+
|
|
52
|
+
Display the welcome banner:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
56
|
+
QGSD ► MCP SETUP — FIRST RUN
|
|
57
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
58
|
+
|
|
59
|
+
No quorum agents configured. Let's set up your first agent.
|
|
60
|
+
|
|
61
|
+
Each agent is a claude-mcp-server instance connected to a
|
|
62
|
+
different LLM provider. You need at least one to use quorum.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Provider template map** (reference throughout the first-run flow):
|
|
66
|
+
|
|
67
|
+
| Agent | Provider | Base URL | Model |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| claude-1 | AkashML | https://api.akashml.com/v1 | deepseek-ai/DeepSeek-V3 |
|
|
70
|
+
| claude-2 | AkashML | https://api.akashml.com/v1 | MiniMaxAI/MiniMax-M2.5 |
|
|
71
|
+
| claude-3 | Together.xyz | https://api.together.xyz/v1 | Qwen/Qwen3-Coder-480B |
|
|
72
|
+
| claude-5 | Together.xyz | https://api.together.xyz/v1 | meta-llama/Llama-4-M |
|
|
73
|
+
| claude-4 | Fireworks | https://api.fireworks.ai/inference/v1 | kimi |
|
|
74
|
+
|
|
75
|
+
### Step 2a: Select agent template
|
|
76
|
+
|
|
77
|
+
Use AskUserQuestion:
|
|
78
|
+
- header: "Choose an agent to configure"
|
|
79
|
+
- question: "Select an agent template to set up. You can add more after."
|
|
80
|
+
- options (omit agents already configured or skipped in this session):
|
|
81
|
+
- "1 — claude-1 (AkashML, DeepSeek-V3)"
|
|
82
|
+
- "2 — claude-2 (AkashML, MiniMax-M2.5)"
|
|
83
|
+
- "3 — claude-3 (Together.xyz, Qwen3-Coder-480B)"
|
|
84
|
+
- "4 — claude-5 (Together.xyz, Llama-4-M)"
|
|
85
|
+
- "5 — claude-4 (Fireworks, kimi)"
|
|
86
|
+
- "Skip — configure later via /qgsd:mcp-setup"
|
|
87
|
+
|
|
88
|
+
If "Skip" is chosen, display:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
⚠ No agents configured. Run /qgsd:mcp-setup when ready.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Stop.
|
|
95
|
+
|
|
96
|
+
### Step 2b: Collect API key
|
|
97
|
+
|
|
98
|
+
Resolve agent name, provider name, base URL, and model from the selection using the template map above.
|
|
99
|
+
|
|
100
|
+
Use AskUserQuestion:
|
|
101
|
+
- header: "API Key — {agent-name}"
|
|
102
|
+
- question: "Enter your {provider-name} API key.\n\nThe key will be stored in your system keychain (keytar). It will not appear in any log or plain-text file."
|
|
103
|
+
- options:
|
|
104
|
+
- "Continue (I have my key ready)"
|
|
105
|
+
- "Skip this agent"
|
|
106
|
+
|
|
107
|
+
If "Skip this agent": add agent to skipped list, return to Step 2a.
|
|
108
|
+
|
|
109
|
+
If "Continue": use a second AskUserQuestion to collect the key:
|
|
110
|
+
- header: "Enter API Key"
|
|
111
|
+
- question: "Paste the API key for {agent-name} ({provider-name}):"
|
|
112
|
+
- options: (user types key and selects "Confirm")
|
|
113
|
+
- "Confirm key"
|
|
114
|
+
|
|
115
|
+
Store the key using this Bash command (substitute AGENT_KEY and API_KEY):
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
KEY_RESULT=$(node -e "
|
|
119
|
+
const { set, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
|
|
120
|
+
const agentKey = process.env.AGENT_KEY;
|
|
121
|
+
const apiKey = process.env.API_KEY;
|
|
122
|
+
(async () => {
|
|
123
|
+
try {
|
|
124
|
+
const keyName = 'ANTHROPIC_API_KEY_' + agentKey.toUpperCase().replace(/-/g,'_');
|
|
125
|
+
await set(SERVICE, keyName, apiKey);
|
|
126
|
+
process.stdout.write(JSON.stringify({ stored: true, method: 'keytar', keyName }) + '\n');
|
|
127
|
+
} catch (e) {
|
|
128
|
+
process.stdout.write(JSON.stringify({ stored: false, error: e.message }) + '\n');
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
" AGENT_KEY="{agent-name}" API_KEY="{user-key}")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Parse KEY_RESULT:
|
|
135
|
+
- `stored: true` — mark agent as `method: keytar` in pending batch
|
|
136
|
+
- `stored: false` (keytar unavailable) — handle fallback below
|
|
137
|
+
|
|
138
|
+
**Keytar unavailable fallback:**
|
|
139
|
+
|
|
140
|
+
Use AskUserQuestion:
|
|
141
|
+
- header: "Keychain Unavailable"
|
|
142
|
+
- question: "System keychain unavailable. API key will be stored unencrypted in ~/.claude.json (less secure). Confirm?\n\nLinux users: sudo apt install libsecret-1-dev gnome-keyring"
|
|
143
|
+
- options:
|
|
144
|
+
- "Store unencrypted in ~/.claude.json (less secure)"
|
|
145
|
+
- "Skip this agent"
|
|
146
|
+
|
|
147
|
+
If "Skip this agent": add to skipped list, return to Step 2a.
|
|
148
|
+
|
|
149
|
+
If "Store unencrypted": mark agent as `method: env_block` with the key value in pending batch. Write audit log:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
mkdir -p ~/.claude/debug
|
|
153
|
+
node -e "
|
|
154
|
+
const fs = require('fs');
|
|
155
|
+
const ts = new Date().toISOString();
|
|
156
|
+
const msg = ts + ' QGSD mcp-setup: keytar unavailable for ' + process.env.AGENT_KEY + ' — API key stored unencrypted in env block\n';
|
|
157
|
+
fs.appendFileSync(require('os').homedir() + '/.claude/debug/mcp-setup-audit.log', msg);
|
|
158
|
+
" AGENT_KEY="{agent-name}"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Step 2c: Add another or finish
|
|
162
|
+
|
|
163
|
+
Use AskUserQuestion:
|
|
164
|
+
- header: "Agent Added"
|
|
165
|
+
- question: "Agent {agent-name} configured. Add another or finish?"
|
|
166
|
+
- options:
|
|
167
|
+
- "Add another agent"
|
|
168
|
+
- "Finish setup"
|
|
169
|
+
|
|
170
|
+
If "Add another agent": return to Step 2a (omit already-configured agents).
|
|
171
|
+
If "Finish setup": continue to Step 3.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Step 3: Apply pending changes
|
|
176
|
+
|
|
177
|
+
If pending batch is empty (all skipped):
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
⚠ No agents configured. Run /qgsd:mcp-setup when ready.
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Stop.
|
|
184
|
+
|
|
185
|
+
Display pending summary:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
189
|
+
QGSD ► REVIEW PENDING CHANGES
|
|
190
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
191
|
+
|
|
192
|
+
Agents to add to ~/.claude.json:
|
|
193
|
+
|
|
194
|
+
◆ {agent-name} → {provider} ({base-url})
|
|
195
|
+
Key: {stored in system keychain | stored in env block (unencrypted)}
|
|
196
|
+
[repeat for each pending agent]
|
|
197
|
+
|
|
198
|
+
Skipped:
|
|
199
|
+
○ {agent-name} — run /qgsd:mcp-setup to configure later
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Use AskUserQuestion:
|
|
203
|
+
- header: "Apply Changes"
|
|
204
|
+
- question: "Apply changes to ~/.claude.json and restart configured agents?"
|
|
205
|
+
- options:
|
|
206
|
+
- "Apply and restart agents"
|
|
207
|
+
- "Cancel — discard all changes"
|
|
208
|
+
|
|
209
|
+
If "Cancel": display "Changes discarded." Stop.
|
|
210
|
+
|
|
211
|
+
If "Apply and restart agents":
|
|
212
|
+
|
|
213
|
+
### Step 3a: Backup ~/.claude.json
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Step 3b: Resolve claude-mcp-server path
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
CLAUDE_MCP_PATH=$(node -e "
|
|
223
|
+
const path = require('path');
|
|
224
|
+
const fs = require('fs');
|
|
225
|
+
const os = require('os');
|
|
226
|
+
const { spawnSync } = require('child_process');
|
|
227
|
+
|
|
228
|
+
// Strategy 1: read from existing ~/.claude.json entries
|
|
229
|
+
try {
|
|
230
|
+
const cj = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
231
|
+
for (const [, cfg] of Object.entries(cj.mcpServers || {})) {
|
|
232
|
+
if ((cfg.args || []).some(a => String(a).includes('claude-mcp-server'))) {
|
|
233
|
+
process.stdout.write(cfg.args[0]);
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {}
|
|
238
|
+
|
|
239
|
+
// Strategy 2: check global npm root
|
|
240
|
+
try {
|
|
241
|
+
const r = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
|
|
242
|
+
const npmRoot = (r.stdout || '').trim();
|
|
243
|
+
const candidate = path.join(npmRoot, 'claude-mcp-server', 'dist', 'index.js');
|
|
244
|
+
if (fs.existsSync(candidate)) { process.stdout.write(candidate); process.exit(0); }
|
|
245
|
+
} catch (e) {}
|
|
246
|
+
|
|
247
|
+
process.stdout.write('');
|
|
248
|
+
" 2>/dev/null)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Step 3c: Write entries to ~/.claude.json
|
|
252
|
+
|
|
253
|
+
If `$CLAUDE_MCP_PATH` is empty (Step 3b returned empty string), display a warning and use the fallback:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
if [ -z "$CLAUDE_MCP_PATH" ]; then
|
|
257
|
+
echo "⚠ Could not resolve claude-mcp-server path automatically. The mcpServers entry will use 'claude-mcp-server' as the args value. You may need to update the path manually after installation."
|
|
258
|
+
CLAUDE_MCP_PATH="claude-mcp-server"
|
|
259
|
+
fi
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
For each pending agent, add the mcpServers entry. Use this inline node script as a template (adapt for the actual pending agents in the session):
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
node -e "
|
|
266
|
+
const fs = require('fs');
|
|
267
|
+
const path = require('path');
|
|
268
|
+
const os = require('os');
|
|
269
|
+
|
|
270
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
271
|
+
let claudeJson = {};
|
|
272
|
+
try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch (e) {}
|
|
273
|
+
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
274
|
+
|
|
275
|
+
// pendingAgents is an array of { name, baseUrl, model, method, apiKey }
|
|
276
|
+
// where method is 'keytar' or 'env_block'
|
|
277
|
+
const pendingAgents = JSON.parse(process.env.PENDING_AGENTS_JSON);
|
|
278
|
+
const mcpPath = process.env.CLAUDE_MCP_PATH || '';
|
|
279
|
+
|
|
280
|
+
for (const agent of pendingAgents) {
|
|
281
|
+
claudeJson.mcpServers[agent.name] = {
|
|
282
|
+
command: 'node',
|
|
283
|
+
args: [mcpPath],
|
|
284
|
+
env: {
|
|
285
|
+
ANTHROPIC_API_KEY: agent.method === 'env_block' ? agent.apiKey : '',
|
|
286
|
+
ANTHROPIC_BASE_URL: agent.baseUrl,
|
|
287
|
+
CLAUDE_DEFAULT_MODEL: agent.model
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
|
|
293
|
+
process.stdout.write(JSON.stringify({ written: true, count: pendingAgents.length }) + '\n');
|
|
294
|
+
" PENDING_AGENTS_JSON='...' CLAUDE_MCP_PATH="$CLAUDE_MCP_PATH"
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Step 3d: Sync keytar secrets to env blocks
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
node -e "
|
|
301
|
+
const { syncToClaudeJson, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
|
|
302
|
+
syncToClaudeJson(SERVICE)
|
|
303
|
+
.then(() => process.stdout.write('synced\n'))
|
|
304
|
+
.catch(e => process.stderr.write('sync warning: ' + e.message + '\n'));
|
|
305
|
+
"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Step 3e: Restart each configured agent (sequential — one at a time)
|
|
309
|
+
|
|
310
|
+
For each agent in the pending batch:
|
|
311
|
+
|
|
312
|
+
Invoke `/qgsd:mcp-restart {agent-name}`.
|
|
313
|
+
|
|
314
|
+
If restart fails or times out, leave config in written state and display:
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
⚠ {agent-name}: restart failed. Config was written — agent will reload on next Claude Code restart.
|
|
318
|
+
Manual retry: /qgsd:mcp-restart {agent-name}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Step 3f: Closing summary
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
325
|
+
QGSD ► SETUP COMPLETE ✓
|
|
326
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
327
|
+
|
|
328
|
+
Changes applied and agents restarted.
|
|
329
|
+
|
|
330
|
+
✓ {agent-name} — restarted
|
|
331
|
+
[repeat for each successfully restarted agent]
|
|
332
|
+
|
|
333
|
+
○ {agent-name} — skipped (run /qgsd:mcp-setup to configure later)
|
|
334
|
+
[repeat for each skipped agent]
|
|
335
|
+
|
|
336
|
+
Run /qgsd:mcp-status to verify agent health.
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Re-run Agent Menu
|
|
342
|
+
|
|
343
|
+
Read the current agent roster from `~/.claude.json`:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
ROSTER=$(node -e "
|
|
347
|
+
const fs = require('fs');
|
|
348
|
+
const path = require('path');
|
|
349
|
+
const os = require('os');
|
|
350
|
+
|
|
351
|
+
(async () => {
|
|
352
|
+
let claudeJson = {};
|
|
353
|
+
try {
|
|
354
|
+
claudeJson = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
355
|
+
} catch (e) {}
|
|
356
|
+
|
|
357
|
+
const servers = claudeJson.mcpServers || {};
|
|
358
|
+
const agents = [];
|
|
359
|
+
|
|
360
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
361
|
+
const env = cfg.env || {};
|
|
362
|
+
const model = env.CLAUDE_DEFAULT_MODEL || '—';
|
|
363
|
+
const provider = env.ANTHROPIC_BASE_URL || '—';
|
|
364
|
+
|
|
365
|
+
let keyStatus = 'no key';
|
|
366
|
+
try {
|
|
367
|
+
const { get, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
|
|
368
|
+
const keyName = 'ANTHROPIC_API_KEY_' + name.toUpperCase().replace(/-/g,'_');
|
|
369
|
+
const stored = await get(SERVICE, keyName);
|
|
370
|
+
keyStatus = stored ? 'key stored' : (env.ANTHROPIC_API_KEY ? 'key in env' : 'no key');
|
|
371
|
+
} catch (e) {
|
|
372
|
+
keyStatus = env.ANTHROPIC_API_KEY ? 'key in env' : 'no key';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
agents.push({ name, model, provider, keyStatus });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
process.stdout.write(JSON.stringify({ agents }) + '\n');
|
|
379
|
+
})();
|
|
380
|
+
")
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Parse ROSTER for `agents` array.
|
|
384
|
+
|
|
385
|
+
Display re-run banner:
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
389
|
+
QGSD ► MCP SETUP — AGENT ROSTER
|
|
390
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Render a numbered table with columns: #, Agent, Model, Provider, Key:
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
# Agent Model Provider Key
|
|
397
|
+
── ─────────────────── ────────────────────────── ───────────────────────────────────── ──────────
|
|
398
|
+
1 claude-1 deepseek-ai/DeepSeek-V3 https://api.akashml.com/v1 key stored
|
|
399
|
+
2 claude-2 MiniMaxAI/MiniMax-M2.5 https://api.akashml.com/v1 no key
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Use AskUserQuestion:
|
|
403
|
+
- header: "Select Agent"
|
|
404
|
+
- question: "Enter the number of the agent to configure, or choose an option:"
|
|
405
|
+
- options:
|
|
406
|
+
- "1 — {agent-name}" (one per agent)
|
|
407
|
+
- "Add new agent"
|
|
408
|
+
- "Edit Quorum Composition"
|
|
409
|
+
- "Exit"
|
|
410
|
+
|
|
411
|
+
If "Exit": display "No changes made." Stop.
|
|
412
|
+
|
|
413
|
+
If "Edit Quorum Composition": route to **Composition Screen** section below.
|
|
414
|
+
|
|
415
|
+
If "Add new agent":
|
|
416
|
+
|
|
417
|
+
**Step A — Select agent template**
|
|
418
|
+
|
|
419
|
+
First, detect which servers are already configured in `~/.claude.json`:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
EXISTING_SERVERS=$(node -e "
|
|
423
|
+
const fs = require('fs'), os = require('os');
|
|
424
|
+
try {
|
|
425
|
+
const cj = JSON.parse(fs.readFileSync(os.homedir() + '/.claude.json', 'utf8'));
|
|
426
|
+
console.log(JSON.stringify(Object.keys(cj.mcpServers || {})));
|
|
427
|
+
} catch(e) { console.log('[]'); }
|
|
428
|
+
")
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Parse `EXISTING_SERVERS` as a JSON array. Use this array to filter the options below.
|
|
432
|
+
|
|
433
|
+
Then use AskUserQuestion with two sections of options:
|
|
434
|
+
|
|
435
|
+
- header: "Add Agent — Select Template"
|
|
436
|
+
- question: "Select an agent template to add:\n\n(Agents already configured are excluded)"
|
|
437
|
+
- options — build the list using these filtering rules:
|
|
438
|
+
- **Claude MCP slots** (omit if agent name already in EXISTING_SERVERS):
|
|
439
|
+
- "1 — claude-1 (AkashML, DeepSeek-V3)"
|
|
440
|
+
- "2 — claude-2 (AkashML, MiniMax-M2.5)"
|
|
441
|
+
- "3 — claude-3 (Together.xyz, Qwen3-Coder-480B)"
|
|
442
|
+
- "4 — claude-5 (Together.xyz, Llama-4-M)"
|
|
443
|
+
- "5 — claude-4 (Fireworks, kimi)"
|
|
444
|
+
- **Native CLI second slots** (omit if second slot already in EXISTING_SERVERS OR if first slot NOT in EXISTING_SERVERS):
|
|
445
|
+
- "6 — codex-cli-2 (second Codex slot — copies codex-cli-1 config)" [show only if codex-cli-1 is in EXISTING_SERVERS AND codex-cli-2 is NOT in EXISTING_SERVERS]
|
|
446
|
+
- "7 — gemini-cli-2 (second Gemini slot — copies gemini-cli-1 config)" [show only if gemini-cli-1 is in EXISTING_SERVERS AND gemini-cli-2 is NOT in EXISTING_SERVERS]
|
|
447
|
+
- "8 — opencode-2 (second OpenCode slot — copies opencode-1 config)" [show only if opencode-1 is in EXISTING_SERVERS AND opencode-2 is NOT in EXISTING_SERVERS]
|
|
448
|
+
- "9 — copilot-2 (second Copilot slot — copies copilot-1 config)" [show only if copilot-1 is in EXISTING_SERVERS AND copilot-2 is NOT in EXISTING_SERVERS]
|
|
449
|
+
- "Cancel — back to roster"
|
|
450
|
+
|
|
451
|
+
If "Cancel — back to roster": display "No changes made." Return to roster display.
|
|
452
|
+
|
|
453
|
+
**Resolver — map selection to slot details:**
|
|
454
|
+
|
|
455
|
+
Claude MCP slot resolver (options 1–5):
|
|
456
|
+
- "1 — claude-1…" → agentName=`claude-1`, provider=`AkashML`, baseUrl=`https://api.akashml.com/v1`, model=`deepseek-ai/DeepSeek-V3`
|
|
457
|
+
- "2 — claude-2…" → agentName=`claude-2`, provider=`AkashML`, baseUrl=`https://api.akashml.com/v1`, model=`MiniMaxAI/MiniMax-M2.5`
|
|
458
|
+
- "3 — claude-3…" → agentName=`claude-3`, provider=`Together.xyz`, baseUrl=`https://api.together.xyz/v1`, model=`Qwen/Qwen3-Coder-480B`
|
|
459
|
+
- "4 — claude-5…" → agentName=`claude-5`, provider=`Together.xyz`, baseUrl=`https://api.together.xyz/v1`, model=`meta-llama/Llama-4-M`
|
|
460
|
+
- "5 — claude-4…" → agentName=`claude-4`, provider=`Fireworks`, baseUrl=`https://api.fireworks.ai/inference/v1`, model=`kimi`
|
|
461
|
+
|
|
462
|
+
→ When options 1–5 selected: continue to Step B (API key collection) as before.
|
|
463
|
+
|
|
464
|
+
Native CLI second-slot resolver (options 6–9):
|
|
465
|
+
- "6 — codex-cli-2…" → newSlot=`codex-cli-2`, sourceSlot=`codex-cli-1`
|
|
466
|
+
- "7 — gemini-cli-2…" → newSlot=`gemini-cli-2`, sourceSlot=`gemini-cli-1`
|
|
467
|
+
- "8 — opencode-2…" → newSlot=`opencode-2`, sourceSlot=`opencode-1`
|
|
468
|
+
- "9 — copilot-2…" → newSlot=`copilot-2`, sourceSlot=`copilot-1`
|
|
469
|
+
|
|
470
|
+
→ When options 6–9 selected: route to **Step B-native** below (skip the API key step).
|
|
471
|
+
|
|
472
|
+
**Step B — Collect API key (claude-mcp-server slots only)**
|
|
473
|
+
|
|
474
|
+
Use AskUserQuestion:
|
|
475
|
+
- header: "API Key — {agent-name}"
|
|
476
|
+
- question: `"Enter your {provider-name} API key.\n\nThe key will be stored in your system keychain (keytar). It will not appear in any log or plain-text file."`
|
|
477
|
+
- options:
|
|
478
|
+
- "Continue (I have my key ready)"
|
|
479
|
+
- "Cancel"
|
|
480
|
+
|
|
481
|
+
If "Cancel": display "No changes made." Return to roster display.
|
|
482
|
+
|
|
483
|
+
If "Continue": second AskUserQuestion:
|
|
484
|
+
- header: "Enter API Key — {agent-name}"
|
|
485
|
+
- question: `"Paste the API key for {agent-name} ({provider-name}):"`
|
|
486
|
+
- options:
|
|
487
|
+
- "Confirm key"
|
|
488
|
+
- "Cancel"
|
|
489
|
+
|
|
490
|
+
If "Cancel": display "No changes made." Return to roster display.
|
|
491
|
+
|
|
492
|
+
Store the key using bin/secrets.cjs (agent name and key passed via env vars — never interpolated):
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
KEY_RESULT=$(node -e "
|
|
496
|
+
const { set, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
|
|
497
|
+
const agentKey = process.env.AGENT_KEY;
|
|
498
|
+
const apiKey = process.env.API_KEY;
|
|
499
|
+
(async () => {
|
|
500
|
+
try {
|
|
501
|
+
const keyName = 'ANTHROPIC_API_KEY_' + agentKey.toUpperCase().replace(/-/g,'_');
|
|
502
|
+
await set(SERVICE, keyName, apiKey);
|
|
503
|
+
process.stdout.write(JSON.stringify({ stored: true, method: 'keytar', keyName }) + '\n');
|
|
504
|
+
} catch (e) {
|
|
505
|
+
process.stdout.write(JSON.stringify({ stored: false, error: e.message }) + '\n');
|
|
506
|
+
}
|
|
507
|
+
})();
|
|
508
|
+
" AGENT_KEY="{agent-name}" API_KEY="{user-key}")
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Parse KEY_RESULT:
|
|
512
|
+
- `stored: true` — proceed to Step C
|
|
513
|
+
- `stored: false` — keytar unavailable fallback:
|
|
514
|
+
|
|
515
|
+
Use AskUserQuestion:
|
|
516
|
+
- header: "Keychain Unavailable"
|
|
517
|
+
- question: "System keychain unavailable. API key will be stored unencrypted in ~/.claude.json (less secure). Confirm?\n\nLinux users: sudo apt install libsecret-1-dev gnome-keyring"
|
|
518
|
+
- options:
|
|
519
|
+
- "Store unencrypted in ~/.claude.json (less secure)"
|
|
520
|
+
- "Cancel — back to roster"
|
|
521
|
+
|
|
522
|
+
If "Cancel — back to roster": display "No changes made." Return to roster display.
|
|
523
|
+
|
|
524
|
+
If "Store unencrypted in ~/.claude.json (less secure)": write audit log then mark agent as `method: env_block` in pending changes and proceed to Step C:
|
|
525
|
+
```bash
|
|
526
|
+
mkdir -p ~/.claude/debug
|
|
527
|
+
node -e "
|
|
528
|
+
const fs = require('fs');
|
|
529
|
+
const ts = new Date().toISOString();
|
|
530
|
+
const msg = ts + ' QGSD mcp-setup: keytar unavailable for ' + process.env.AGENT_KEY + ' — API key stored unencrypted in env block\n';
|
|
531
|
+
fs.appendFileSync(require('os').homedir() + '/.claude/debug/mcp-setup-audit.log', msg);
|
|
532
|
+
" AGENT_KEY="{agent-name}"
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Step C — Confirm + apply**
|
|
536
|
+
|
|
537
|
+
Show pending summary:
|
|
538
|
+
|
|
539
|
+
```
|
|
540
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
541
|
+
QGSD ► REVIEW PENDING CHANGES
|
|
542
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
543
|
+
|
|
544
|
+
◆ {agent-name} → {provider-name} ({base-url})
|
|
545
|
+
Key: {stored in system keychain | stored in env block (unencrypted)}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Use AskUserQuestion:
|
|
549
|
+
- header: "Add Agent"
|
|
550
|
+
- question: "Add {agent-name} to ~/.claude.json and start it?"
|
|
551
|
+
- options:
|
|
552
|
+
- "Add and start"
|
|
553
|
+
- "Cancel — discard changes"
|
|
554
|
+
|
|
555
|
+
If "Cancel — discard changes": display "Changes discarded." Return to roster display.
|
|
556
|
+
|
|
557
|
+
If "Add and start":
|
|
558
|
+
|
|
559
|
+
1. Backup ~/.claude.json:
|
|
560
|
+
```bash
|
|
561
|
+
cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
2. Resolve claude-mcp-server path (read from existing entries, then npm root fallback):
|
|
565
|
+
```bash
|
|
566
|
+
CLAUDE_MCP_PATH=$(node -e "
|
|
567
|
+
const path = require('path');
|
|
568
|
+
const fs = require('fs');
|
|
569
|
+
const os = require('os');
|
|
570
|
+
const { spawnSync } = require('child_process');
|
|
571
|
+
|
|
572
|
+
// Strategy 1: read from existing ~/.claude.json entries
|
|
573
|
+
try {
|
|
574
|
+
const cj = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
575
|
+
for (const [, cfg] of Object.entries(cj.mcpServers || {})) {
|
|
576
|
+
if ((cfg.args || []).some(a => String(a).includes('claude-mcp-server'))) {
|
|
577
|
+
process.stdout.write(cfg.args[0]);
|
|
578
|
+
process.exit(0);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} catch (e) {}
|
|
582
|
+
|
|
583
|
+
// Strategy 2: check global npm root
|
|
584
|
+
try {
|
|
585
|
+
const r = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
|
|
586
|
+
const npmRoot = (r.stdout || '').trim();
|
|
587
|
+
const candidate = path.join(npmRoot, 'claude-mcp-server', 'dist', 'index.js');
|
|
588
|
+
if (fs.existsSync(candidate)) { process.stdout.write(candidate); process.exit(0); }
|
|
589
|
+
} catch (e) {}
|
|
590
|
+
|
|
591
|
+
process.stdout.write('');
|
|
592
|
+
" 2>/dev/null)
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
If `CLAUDE_MCP_PATH` is empty: display warning and use placeholder args `["claude-mcp-server"]` (command: `"node"`).
|
|
596
|
+
|
|
597
|
+
3. Write new mcpServers entry via inline node (all values passed via env vars — never interpolated):
|
|
598
|
+
```bash
|
|
599
|
+
node -e "
|
|
600
|
+
const fs = require('fs');
|
|
601
|
+
const path = require('path');
|
|
602
|
+
const os = require('os');
|
|
603
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
604
|
+
let claudeJson = {};
|
|
605
|
+
try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch (e) {}
|
|
606
|
+
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
607
|
+
const agentName = process.env.AGENT_NAME;
|
|
608
|
+
const baseUrl = process.env.BASE_URL;
|
|
609
|
+
const model = process.env.MODEL;
|
|
610
|
+
const mcpPath = process.env.MCP_PATH;
|
|
611
|
+
const apiKey = process.env.API_KEY_ENV || '';
|
|
612
|
+
claudeJson.mcpServers[agentName] = {
|
|
613
|
+
command: 'node',
|
|
614
|
+
args: [mcpPath],
|
|
615
|
+
env: {
|
|
616
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
617
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
618
|
+
CLAUDE_DEFAULT_MODEL: model
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
|
|
622
|
+
process.stdout.write(JSON.stringify({ written: true }) + '\n');
|
|
623
|
+
" AGENT_NAME="{agent-name}" BASE_URL="{base-url}" MODEL="{model}" MCP_PATH="$CLAUDE_MCP_PATH" API_KEY_ENV="{api-key-if-env-block-method}"
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
4. Sync keytar secrets to ~/.claude.json:
|
|
627
|
+
```bash
|
|
628
|
+
node -e "
|
|
629
|
+
const { syncToClaudeJson, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
|
|
630
|
+
syncToClaudeJson(SERVICE).then(() => process.stdout.write('synced\n')).catch(e => process.stderr.write(e.message + '\n'));
|
|
631
|
+
"
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
5. Invoke `/qgsd:mcp-restart {agent-name}` to start the new agent process.
|
|
635
|
+
|
|
636
|
+
**Step D — Identity ping (AGENT-03)**
|
|
637
|
+
|
|
638
|
+
After restart, display: `"◆ Waiting for {agent-name} to start... calling identity tool"`
|
|
639
|
+
|
|
640
|
+
Invoke the `identity` tool on the newly started agent. Display the result:
|
|
641
|
+
|
|
642
|
+
If identity responds:
|
|
643
|
+
```
|
|
644
|
+
✓ Agent added and verified live.
|
|
645
|
+
|
|
646
|
+
✓ {agent-name} — added, restarted, identity confirmed
|
|
647
|
+
Name: {identity.name}
|
|
648
|
+
Version: {identity.version}
|
|
649
|
+
Model: {identity.model}
|
|
650
|
+
|
|
651
|
+
Run /qgsd:mcp-status to see full agent roster.
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
If identity times out or errors:
|
|
655
|
+
```
|
|
656
|
+
✓ Agent added and restarted.
|
|
657
|
+
|
|
658
|
+
◆ {agent-name} — added, restarted (identity ping timed out — agent may need a moment to start)
|
|
659
|
+
|
|
660
|
+
Run /qgsd:mcp-status to verify agent health.
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**Return path:** If this add-slot flow was entered from the Composition Screen (via "Add new slot"), return to Composition Screen after identity ping. Otherwise, return to roster display (re-read roster to show new agent).
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
**Step B-native — Add Native CLI Slot (options 6–9 from Step A)**
|
|
668
|
+
|
|
669
|
+
This branch is entered when the user selected a native CLI second slot (options 6–9) in Step A. At this point `newSlot` and `sourceSlot` are set from the resolver.
|
|
670
|
+
|
|
671
|
+
Show a confirmation screen:
|
|
672
|
+
|
|
673
|
+
```
|
|
674
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
675
|
+
QGSD ► REVIEW PENDING CHANGES
|
|
676
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
677
|
+
|
|
678
|
+
◆ {new-slot-name} → copied from {source-slot-name}
|
|
679
|
+
Binary: {path from source slot}
|
|
680
|
+
Auth: uses {source-slot-name}'s existing credentials
|
|
681
|
+
quorum_active: will be appended
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Use AskUserQuestion:
|
|
685
|
+
- header: "Add Native CLI Slot"
|
|
686
|
+
- question: "Add {new-slot-name} to ~/.claude.json and quorum_active?"
|
|
687
|
+
- options:
|
|
688
|
+
- "Add and start"
|
|
689
|
+
- "Cancel — discard changes"
|
|
690
|
+
|
|
691
|
+
If "Cancel — discard changes": display "Changes discarded." Return to roster display.
|
|
692
|
+
|
|
693
|
+
If "Add and start":
|
|
694
|
+
|
|
695
|
+
1. Backup `~/.claude.json`:
|
|
696
|
+
```bash
|
|
697
|
+
cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
2. Copy mcpServers entry from source slot to new slot (deep copy — command, args, env all preserved):
|
|
701
|
+
```bash
|
|
702
|
+
node -e "
|
|
703
|
+
const fs = require('fs');
|
|
704
|
+
const os = require('os');
|
|
705
|
+
const claudeJsonPath = os.homedir() + '/.claude.json';
|
|
706
|
+
let cj = {};
|
|
707
|
+
try { cj = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch(e) {}
|
|
708
|
+
const srcSlot = process.env.SOURCE_SLOT;
|
|
709
|
+
const newSlot = process.env.NEW_SLOT;
|
|
710
|
+
const src = (cj.mcpServers || {})[srcSlot];
|
|
711
|
+
if (!src) { process.stdout.write(JSON.stringify({ written: false, error: 'source slot not found' }) + '\n'); process.exit(1); }
|
|
712
|
+
cj.mcpServers = cj.mcpServers || {};
|
|
713
|
+
cj.mcpServers[newSlot] = JSON.parse(JSON.stringify(src)); // deep copy
|
|
714
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(cj, null, 2));
|
|
715
|
+
process.stdout.write(JSON.stringify({ written: true, newSlot, sourceSlot: srcSlot }) + '\n');
|
|
716
|
+
" SOURCE_SLOT="{source-slot-name}" NEW_SLOT="{new-slot-name}"
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
If `written: false`: display error and return to roster.
|
|
720
|
+
|
|
721
|
+
3. Append new slot to `quorum_active` in `~/.claude/qgsd.json`:
|
|
722
|
+
```bash
|
|
723
|
+
node -e "
|
|
724
|
+
const fs = require('fs'), os = require('os');
|
|
725
|
+
const qgsdPath = os.homedir() + '/.claude/qgsd.json';
|
|
726
|
+
let cfg = {};
|
|
727
|
+
try { cfg = JSON.parse(fs.readFileSync(qgsdPath, 'utf8')); } catch(e) {}
|
|
728
|
+
const active = Array.isArray(cfg.quorum_active) ? cfg.quorum_active : [];
|
|
729
|
+
const newSlot = process.env.NEW_SLOT;
|
|
730
|
+
if (!active.includes(newSlot)) {
|
|
731
|
+
cfg.quorum_active = [...active, newSlot];
|
|
732
|
+
fs.writeFileSync(qgsdPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
733
|
+
process.stdout.write(JSON.stringify({ added: true, slot: newSlot }) + '\n');
|
|
734
|
+
} else {
|
|
735
|
+
process.stdout.write(JSON.stringify({ added: false, slot: newSlot, reason: 'already present' }) + '\n');
|
|
736
|
+
}
|
|
737
|
+
" NEW_SLOT="{new-slot-name}"
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
4. Invoke `/qgsd:mcp-restart {new-slot-name}` to start the new agent process.
|
|
741
|
+
|
|
742
|
+
**Step D — Identity ping** (same as claude-mcp-server path):
|
|
743
|
+
|
|
744
|
+
After restart, display: `"◆ Waiting for {new-slot-name} to start... calling identity tool"`
|
|
745
|
+
|
|
746
|
+
Invoke the `identity` tool on the newly started agent. Display the result:
|
|
747
|
+
|
|
748
|
+
If identity responds:
|
|
749
|
+
```
|
|
750
|
+
✓ Agent added and verified live.
|
|
751
|
+
|
|
752
|
+
✓ {new-slot-name} — added, restarted, identity confirmed
|
|
753
|
+
Name: {identity.name}
|
|
754
|
+
Version: {identity.version}
|
|
755
|
+
Model: {identity.model}
|
|
756
|
+
|
|
757
|
+
Run /qgsd:mcp-status to see full agent roster.
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
If identity times out or errors:
|
|
761
|
+
```
|
|
762
|
+
✓ Agent added and restarted.
|
|
763
|
+
|
|
764
|
+
◆ {new-slot-name} — added, restarted (identity ping timed out — agent may need a moment to start)
|
|
765
|
+
|
|
766
|
+
Run /qgsd:mcp-status to verify agent health.
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Return path:** If this add-slot flow was entered from the Composition Screen (via "Add new slot"), return to Composition Screen after identity ping. Otherwise, return to roster display (re-read roster to show new agent).
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
If agent selected: continue to Agent Sub-Menu.
|
|
774
|
+
|
|
775
|
+
---
|
|
776
|
+
|
|
777
|
+
## Composition Screen
|
|
778
|
+
|
|
779
|
+
This screen is entered when the user selects "Edit Quorum Composition" from the re-run menu. It shows all configured slots with their current `quorum_active` status and allows toggling.
|
|
780
|
+
|
|
781
|
+
**Step CS-1: Read slots and quorum_active**
|
|
782
|
+
|
|
783
|
+
```bash
|
|
784
|
+
COMPOSITION_DATA=$(node -e "
|
|
785
|
+
const fs = require('fs'), os = require('os');
|
|
786
|
+
let slots = [];
|
|
787
|
+
let active = [];
|
|
788
|
+
try {
|
|
789
|
+
const cj = JSON.parse(fs.readFileSync(os.homedir() + '/.claude.json', 'utf8'));
|
|
790
|
+
slots = Object.keys(cj.mcpServers || {});
|
|
791
|
+
} catch(e) {}
|
|
792
|
+
try {
|
|
793
|
+
const qgsd = JSON.parse(fs.readFileSync(os.homedir() + '/.claude/qgsd.json', 'utf8'));
|
|
794
|
+
active = Array.isArray(qgsd.quorum_active) ? qgsd.quorum_active : [];
|
|
795
|
+
} catch(e) {}
|
|
796
|
+
process.stdout.write(JSON.stringify({ slots, active }) + '\n');
|
|
797
|
+
")
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
Parse `COMPOSITION_DATA` for `slots` (array of all slot names from `~/.claude.json`) and `active` (current `quorum_active` array from `~/.claude/qgsd.json`).
|
|
801
|
+
|
|
802
|
+
**Step CS-2: Display composition table**
|
|
803
|
+
|
|
804
|
+
Display banner:
|
|
805
|
+
|
|
806
|
+
```
|
|
807
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
808
|
+
QGSD ► QUORUM COMPOSITION
|
|
809
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Render a numbered table. Status rules:
|
|
813
|
+
- If `active` is **empty**: show `● ON (all)` for every slot — fail-open mode means all slots participate
|
|
814
|
+
- If `active` is **non-empty** AND slot IS in `active`: show `● ON`
|
|
815
|
+
- If `active` is **non-empty** AND slot NOT in `active`: show `○ OFF`
|
|
816
|
+
|
|
817
|
+
```
|
|
818
|
+
# Slot Status
|
|
819
|
+
── ──────────────── ──────
|
|
820
|
+
1 codex-cli-1 ● ON
|
|
821
|
+
2 gemini-cli-1 ● ON
|
|
822
|
+
3 opencode-1 ○ OFF
|
|
823
|
+
4 copilot-1 ● ON
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
If `active` is empty, display a note below the table:
|
|
827
|
+
|
|
828
|
+
```
|
|
829
|
+
ℹ quorum_active is empty — all slots participate (fail-open mode)
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
**Step CS-3: AskUserQuestion for composition actions**
|
|
833
|
+
|
|
834
|
+
Initialize `PENDING_ACTIVE` as a copy of `active` (in-memory working array).
|
|
835
|
+
|
|
836
|
+
Use AskUserQuestion:
|
|
837
|
+
- header: "Quorum Composition"
|
|
838
|
+
- question: "Enter slot number to toggle ON/OFF, or choose an option:"
|
|
839
|
+
- options:
|
|
840
|
+
- "1 — {slot-name} [{current PENDING_ACTIVE status}]" (one per slot — show ● ON or ○ OFF based on current PENDING_ACTIVE state)
|
|
841
|
+
- "Apply — save changes to qgsd.json"
|
|
842
|
+
- "Add new slot — add a slot to ~/.claude.json and quorum_active"
|
|
843
|
+
- "Cancel — discard changes"
|
|
844
|
+
|
|
845
|
+
**Toggle handler:** When user selects slot number N:
|
|
846
|
+
|
|
847
|
+
- If slot IS currently in `PENDING_ACTIVE`:
|
|
848
|
+
- If `PENDING_ACTIVE.length === 1` (removing would empty the array): use a second AskUserQuestion to warn: "Removing this slot will leave quorum_active empty — all slots will participate (fail-open). Continue?" with options "Yes — set fail-open mode" / "Cancel". If confirmed: set `PENDING_ACTIVE = []`. If cancelled: no change.
|
|
849
|
+
- Otherwise: remove slot from `PENDING_ACTIVE` → status becomes `○ OFF`
|
|
850
|
+
- If slot is NOT in `PENDING_ACTIVE` AND `PENDING_ACTIVE` is **non-empty**: add slot to `PENDING_ACTIVE` → status becomes `● ON`
|
|
851
|
+
- If slot is NOT in `PENDING_ACTIVE` AND `PENDING_ACTIVE` is **empty**: `PENDING_ACTIVE = [slot]` (switching from fail-open to explicit single-slot list — slot becomes `● ON`, others become `○ OFF`)
|
|
852
|
+
|
|
853
|
+
Re-display the AskUserQuestion with updated statuses after each toggle.
|
|
854
|
+
|
|
855
|
+
**Apply handler:** When "Apply" selected:
|
|
856
|
+
|
|
857
|
+
```bash
|
|
858
|
+
node -e "
|
|
859
|
+
const fs = require('fs'), os = require('os');
|
|
860
|
+
const qgsdPath = os.homedir() + '/.claude/qgsd.json';
|
|
861
|
+
let cfg = {};
|
|
862
|
+
try { cfg = JSON.parse(fs.readFileSync(qgsdPath, 'utf8')); } catch(e) {}
|
|
863
|
+
cfg.quorum_active = JSON.parse(process.env.PENDING_ACTIVE);
|
|
864
|
+
fs.writeFileSync(qgsdPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
865
|
+
process.stdout.write(JSON.stringify({ written: true, count: cfg.quorum_active.length }) + '\n');
|
|
866
|
+
" PENDING_ACTIVE="{JSON.stringify(PENDING_ACTIVE)}"
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
If `written: true`: display:
|
|
870
|
+
|
|
871
|
+
```
|
|
872
|
+
✓ quorum_active updated — {N} slot(s) active.
|
|
873
|
+
|
|
874
|
+
Changes take effect on next quorum call (no restart required).
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
Return to roster display.
|
|
878
|
+
|
|
879
|
+
If error: display error message and return to roster.
|
|
880
|
+
|
|
881
|
+
**Add new slot handler:** When "Add new slot" selected:
|
|
882
|
+
|
|
883
|
+
Route to **Step A** (EXISTING_SERVERS detection + template selection). This is the same flow as "Add new agent" from the roster menu. After Step D (identity ping) completes:
|
|
884
|
+
|
|
885
|
+
- Return to **Composition Screen** (re-read fresh `slots` and `active` data from disk).
|
|
886
|
+
- The newly added slot will appear in the table with ● ON status (it was added to `quorum_active` during the Step B or Step B-native apply flow).
|
|
887
|
+
- Re-display the Composition Screen AskUserQuestion.
|
|
888
|
+
|
|
889
|
+
**Cancel handler:** When "Cancel" selected:
|
|
890
|
+
|
|
891
|
+
Display "No changes made." Return to roster display.
|
|
892
|
+
|
|
893
|
+
---
|
|
894
|
+
|
|
895
|
+
## Agent Sub-Menu
|
|
896
|
+
|
|
897
|
+
Display agent detail banner:
|
|
898
|
+
|
|
899
|
+
```
|
|
900
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
901
|
+
QGSD ► {agent-name}
|
|
902
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
903
|
+
|
|
904
|
+
Model: {model}
|
|
905
|
+
Provider: {provider}
|
|
906
|
+
Key: {keyStatus}
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
Use AskUserQuestion:
|
|
910
|
+
- header: "Actions — {agent-name}"
|
|
911
|
+
- question: "Choose an action:"
|
|
912
|
+
- options:
|
|
913
|
+
- "1 — Set / update API key"
|
|
914
|
+
- "2 — Swap provider"
|
|
915
|
+
- "3 — Remove agent"
|
|
916
|
+
- "Back — return to agent list"
|
|
917
|
+
|
|
918
|
+
**Option 1 — Set / update API key:**
|
|
919
|
+
|
|
920
|
+
**Step A — Check existing key status**
|
|
921
|
+
|
|
922
|
+
Run an inline node script to check whether a key is already stored in keytar for the selected agent:
|
|
923
|
+
|
|
924
|
+
```bash
|
|
925
|
+
KEY_CHECK_RESULT=$(node -e "
|
|
926
|
+
const { get, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
|
|
927
|
+
(async () => {
|
|
928
|
+
try {
|
|
929
|
+
const agentName = process.env.AGENT_NAME;
|
|
930
|
+
const keyName = 'ANTHROPIC_API_KEY_' + agentName.toUpperCase().replace(/-/g,'_');
|
|
931
|
+
const stored = await get(SERVICE, keyName);
|
|
932
|
+
if (stored) {
|
|
933
|
+
process.stdout.write(JSON.stringify({ hasKey: true, method: 'keytar' }) + '\n');
|
|
934
|
+
} else {
|
|
935
|
+
// Check env block fallback
|
|
936
|
+
const fs = require('fs'), path = require('path'), os = require('os');
|
|
937
|
+
let envVal = '';
|
|
938
|
+
try {
|
|
939
|
+
const cj = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
940
|
+
envVal = (cj.mcpServers && cj.mcpServers[agentName] && cj.mcpServers[agentName].env && cj.mcpServers[agentName].env.ANTHROPIC_API_KEY) || '';
|
|
941
|
+
} catch (e) {}
|
|
942
|
+
if (envVal) {
|
|
943
|
+
process.stdout.write(JSON.stringify({ hasKey: true, method: 'env_block' }) + '\n');
|
|
944
|
+
} else {
|
|
945
|
+
process.stdout.write(JSON.stringify({ hasKey: false, method: 'none' }) + '\n');
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
} catch (e) {
|
|
949
|
+
process.stdout.write(JSON.stringify({ hasKey: false, method: 'none', error: e.message }) + '\n');
|
|
950
|
+
}
|
|
951
|
+
})();
|
|
952
|
+
" AGENT_NAME="{agent-name}")
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Parse KEY_CHECK_RESULT for: `hasKey` (boolean), `method` ('keytar'|'env_block'|'none').
|
|
956
|
+
|
|
957
|
+
**Step B — Prompt user with key-status hint**
|
|
958
|
+
|
|
959
|
+
Use AskUserQuestion:
|
|
960
|
+
- header: "Set API Key — {agent-name}"
|
|
961
|
+
- question: one of:
|
|
962
|
+
- If `hasKey` is true and `method` is `keytar`: `"API key already stored in system keychain (key stored). Enter a new key to overwrite it, or skip.\n\nThe key will be stored in your system keychain (keytar). It will not appear in any log or plain-text file."`
|
|
963
|
+
- If `method` is `env_block`: `"API key currently stored in ~/.claude.json env block. Enter a new key to move it to the system keychain, or skip.\n\nThe key will be stored in your system keychain (keytar). It will not appear in any log or plain-text file."`
|
|
964
|
+
- If `method` is `none`: `"No API key configured for {agent-name}. Enter a key to store it in your system keychain.\n\nThe key will be stored in your system keychain (keytar). It will not appear in any log or plain-text file."`
|
|
965
|
+
- options:
|
|
966
|
+
- "Continue (I have my key ready)"
|
|
967
|
+
- "Skip — back to agent menu"
|
|
968
|
+
|
|
969
|
+
If "Skip — back to agent menu": display "No changes made." Return to Agent Sub-Menu (re-display sub-menu for the same agent).
|
|
970
|
+
|
|
971
|
+
**Step C — Collect the key value**
|
|
972
|
+
|
|
973
|
+
Use a second AskUserQuestion to receive the actual key:
|
|
974
|
+
- header: "Enter API Key — {agent-name}"
|
|
975
|
+
- question: `"Paste your API key for {agent-name}:"`
|
|
976
|
+
- options:
|
|
977
|
+
- "Confirm key"
|
|
978
|
+
- "Cancel"
|
|
979
|
+
|
|
980
|
+
If "Cancel": display "No changes made." Return to Agent Sub-Menu.
|
|
981
|
+
|
|
982
|
+
**Step D — Store in keytar**
|
|
983
|
+
|
|
984
|
+
Run inline node script using `set()` from `bin/secrets.cjs`. Pass key via environment variable only — never interpolate into the script body:
|
|
985
|
+
|
|
986
|
+
```bash
|
|
987
|
+
KEY_STORE_RESULT=$(node -e "
|
|
988
|
+
const { set, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
|
|
989
|
+
(async () => {
|
|
990
|
+
try {
|
|
991
|
+
const agentName = process.env.AGENT_NAME;
|
|
992
|
+
const apiKey = process.env.API_KEY;
|
|
993
|
+
const keyName = 'ANTHROPIC_API_KEY_' + agentName.toUpperCase().replace(/-/g,'_');
|
|
994
|
+
await set(SERVICE, keyName, apiKey);
|
|
995
|
+
process.stdout.write(JSON.stringify({ stored: true, method: 'keytar', keyName }) + '\n');
|
|
996
|
+
} catch (e) {
|
|
997
|
+
process.stdout.write(JSON.stringify({ stored: false, error: e.message }) + '\n');
|
|
998
|
+
}
|
|
999
|
+
})();
|
|
1000
|
+
" AGENT_NAME="{agent-name}" API_KEY="{user-entered-key}")
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
Parse KEY_STORE_RESULT:
|
|
1004
|
+
- `stored: true` — continue to Step E
|
|
1005
|
+
- `stored: false` — handle keytar fallback:
|
|
1006
|
+
|
|
1007
|
+
Use AskUserQuestion:
|
|
1008
|
+
- header: "Keychain Unavailable"
|
|
1009
|
+
- question: "System keychain unavailable. API key will be stored unencrypted in ~/.claude.json (less secure). Confirm?\n\nLinux users: sudo apt install libsecret-1-dev gnome-keyring"
|
|
1010
|
+
- options:
|
|
1011
|
+
- "Store unencrypted in ~/.claude.json (less secure)"
|
|
1012
|
+
- "Skip — back to agent menu"
|
|
1013
|
+
|
|
1014
|
+
If "Skip — back to agent menu": display "No changes made." Return to Agent Sub-Menu.
|
|
1015
|
+
|
|
1016
|
+
If "Store unencrypted in ~/.claude.json (less secure)": write audit log then proceed to Step E with `method: env_block`:
|
|
1017
|
+
```bash
|
|
1018
|
+
mkdir -p ~/.claude/debug
|
|
1019
|
+
node -e "
|
|
1020
|
+
const fs = require('fs');
|
|
1021
|
+
const ts = new Date().toISOString();
|
|
1022
|
+
const msg = ts + ' QGSD mcp-setup: keytar unavailable for ' + process.env.AGENT_KEY + ' — API key stored unencrypted in env block\n';
|
|
1023
|
+
fs.appendFileSync(require('os').homedir() + '/.claude/debug/mcp-setup-audit.log', msg);
|
|
1024
|
+
" AGENT_KEY="{agent-name}"
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
**Step E — Confirm + apply**
|
|
1028
|
+
|
|
1029
|
+
Show pending summary using the existing "Confirm + Apply + Restart Flow" pattern:
|
|
1030
|
+
|
|
1031
|
+
```
|
|
1032
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1033
|
+
QGSD ► REVIEW PENDING CHANGES
|
|
1034
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1035
|
+
|
|
1036
|
+
◆ {agent-name} — API key updated (stored in system keychain)
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
Use AskUserQuestion:
|
|
1040
|
+
- header: "Apply Key Change"
|
|
1041
|
+
- question: "Apply key change to ~/.claude.json and restart {agent-name}?"
|
|
1042
|
+
- options:
|
|
1043
|
+
- "Apply and restart"
|
|
1044
|
+
- "Cancel — discard changes"
|
|
1045
|
+
|
|
1046
|
+
If "Cancel — discard changes": display "Changes discarded." Return to Agent Sub-Menu.
|
|
1047
|
+
|
|
1048
|
+
If "Apply and restart":
|
|
1049
|
+
|
|
1050
|
+
1. Backup ~/.claude.json:
|
|
1051
|
+
```bash
|
|
1052
|
+
cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
2. Patch ANTHROPIC_API_KEY in the agent's env block. Pass key via environment variable only — never interpolate the value into the script body:
|
|
1056
|
+
```bash
|
|
1057
|
+
node -e "
|
|
1058
|
+
const fs = require('fs');
|
|
1059
|
+
const path = require('path');
|
|
1060
|
+
const os = require('os');
|
|
1061
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
1062
|
+
let claudeJson = {};
|
|
1063
|
+
try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch (e) {}
|
|
1064
|
+
const agentName = process.env.AGENT_NAME;
|
|
1065
|
+
const apiKey = process.env.API_KEY;
|
|
1066
|
+
if (claudeJson.mcpServers && claudeJson.mcpServers[agentName]) {
|
|
1067
|
+
if (!claudeJson.mcpServers[agentName].env) claudeJson.mcpServers[agentName].env = {};
|
|
1068
|
+
claudeJson.mcpServers[agentName].env.ANTHROPIC_API_KEY = apiKey;
|
|
1069
|
+
}
|
|
1070
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
|
|
1071
|
+
process.stdout.write(JSON.stringify({ written: true }) + '\n');
|
|
1072
|
+
" AGENT_NAME="{agent-name}" API_KEY="{user-entered-key}"
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
3. Sync all keytar secrets back to ~/.claude.json:
|
|
1076
|
+
```bash
|
|
1077
|
+
node -e "
|
|
1078
|
+
const { syncToClaudeJson, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
|
|
1079
|
+
syncToClaudeJson(SERVICE).then(() => process.stdout.write('synced\n')).catch(e => process.stderr.write(e.message + '\n'));
|
|
1080
|
+
"
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
4. Invoke `/qgsd:mcp-restart {agent-name}` (sequential). If restart fails, leave config written and display:
|
|
1084
|
+
```
|
|
1085
|
+
⚠ {agent-name}: restart failed. Config applied — reload on next Claude Code restart.
|
|
1086
|
+
Manual retry: /qgsd:mcp-restart {agent-name}
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
5. Display:
|
|
1090
|
+
```
|
|
1091
|
+
✓ API key updated and agent restarted.
|
|
1092
|
+
|
|
1093
|
+
✓ {agent-name} — key updated, restarted
|
|
1094
|
+
|
|
1095
|
+
Run /qgsd:mcp-status to verify agent health.
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
Return to Agent Sub-Menu (user can make further changes or go Back).
|
|
1099
|
+
|
|
1100
|
+
**Option 2 — Swap provider:**
|
|
1101
|
+
|
|
1102
|
+
**Step A — Show current provider**
|
|
1103
|
+
|
|
1104
|
+
Read the agent's current `ANTHROPIC_BASE_URL` from `~/.claude.json`:
|
|
1105
|
+
|
|
1106
|
+
```bash
|
|
1107
|
+
CURRENT_PROVIDER=$(node -e "
|
|
1108
|
+
const fs = require('fs'), path = require('path'), os = require('os');
|
|
1109
|
+
let url = '—';
|
|
1110
|
+
try {
|
|
1111
|
+
const cj = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
1112
|
+
url = (cj.mcpServers && cj.mcpServers[process.env.AGENT_NAME] && cj.mcpServers[process.env.AGENT_NAME].env && cj.mcpServers[process.env.AGENT_NAME].env.ANTHROPIC_BASE_URL) || '—';
|
|
1113
|
+
} catch (e) {}
|
|
1114
|
+
process.stdout.write(url + '\n');
|
|
1115
|
+
" AGENT_NAME="{agent-name}")
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
Map the raw URL to a friendly provider name for display:
|
|
1119
|
+
- `https://api.akashml.com/v1` → `AkashML`
|
|
1120
|
+
- `https://api.together.xyz/v1` → `Together.xyz`
|
|
1121
|
+
- `https://api.fireworks.ai/inference/v1` → `Fireworks`
|
|
1122
|
+
- anything else → the raw URL value
|
|
1123
|
+
|
|
1124
|
+
**Step B — Prompt user with provider selection**
|
|
1125
|
+
|
|
1126
|
+
Use AskUserQuestion:
|
|
1127
|
+
- header: "Swap Provider — {agent-name}"
|
|
1128
|
+
- question: `"Current provider: {friendly-provider-name}\n\nSelect a new provider for {agent-name}:"`
|
|
1129
|
+
- options:
|
|
1130
|
+
- "1 — AkashML (https://api.akashml.com/v1)"
|
|
1131
|
+
- "2 — Together.xyz (https://api.together.xyz/v1)"
|
|
1132
|
+
- "3 — Fireworks (https://api.fireworks.ai/inference/v1)"
|
|
1133
|
+
- "4 — Custom URL"
|
|
1134
|
+
- "Skip — back to agent menu"
|
|
1135
|
+
|
|
1136
|
+
If "Skip — back to agent menu": display "No changes made." Return to Agent Sub-Menu.
|
|
1137
|
+
|
|
1138
|
+
**Step C — Resolve new URL**
|
|
1139
|
+
|
|
1140
|
+
For curated selections (1–3): resolve the canonical URL from the selection:
|
|
1141
|
+
- "1 — AkashML…" → `NEW_URL="https://api.akashml.com/v1"`, `NEW_PROVIDER_NAME="AkashML"`
|
|
1142
|
+
- "2 — Together.xyz…" → `NEW_URL="https://api.together.xyz/v1"`, `NEW_PROVIDER_NAME="Together.xyz"`
|
|
1143
|
+
- "3 — Fireworks…" → `NEW_URL="https://api.fireworks.ai/inference/v1"`, `NEW_PROVIDER_NAME="Fireworks"`
|
|
1144
|
+
|
|
1145
|
+
For "4 — Custom URL": use a second AskUserQuestion to collect the URL:
|
|
1146
|
+
- header: "Custom Provider URL — {agent-name}"
|
|
1147
|
+
- question: `"Enter the full base URL for the custom provider (e.g. https://openrouter.ai/api/v1):"`
|
|
1148
|
+
- options:
|
|
1149
|
+
- "Confirm URL"
|
|
1150
|
+
- "Cancel"
|
|
1151
|
+
|
|
1152
|
+
If "Cancel": display "No changes made." Return to Agent Sub-Menu.
|
|
1153
|
+
|
|
1154
|
+
Store the user-entered value as `NEW_URL` and `NEW_PROVIDER_NAME="custom"`.
|
|
1155
|
+
|
|
1156
|
+
**Step D — Confirm + apply**
|
|
1157
|
+
|
|
1158
|
+
Show pending summary:
|
|
1159
|
+
|
|
1160
|
+
```
|
|
1161
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1162
|
+
QGSD ► REVIEW PENDING CHANGES
|
|
1163
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1164
|
+
|
|
1165
|
+
◆ {agent-name} — provider changed to {NEW_PROVIDER_NAME} ({NEW_URL})
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
Use AskUserQuestion:
|
|
1169
|
+
- header: "Apply Provider Change"
|
|
1170
|
+
- question: "Apply provider change to ~/.claude.json and restart {agent-name}?"
|
|
1171
|
+
- options:
|
|
1172
|
+
- "Apply and restart"
|
|
1173
|
+
- "Cancel — discard changes"
|
|
1174
|
+
|
|
1175
|
+
If "Cancel — discard changes": display "Changes discarded." Return to Agent Sub-Menu.
|
|
1176
|
+
|
|
1177
|
+
If "Apply and restart":
|
|
1178
|
+
|
|
1179
|
+
1. Backup ~/.claude.json:
|
|
1180
|
+
```bash
|
|
1181
|
+
cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
2. Patch ANTHROPIC_BASE_URL in the agent's env block. Pass the new URL via environment variable — never interpolate into the script body:
|
|
1185
|
+
```bash
|
|
1186
|
+
node -e "
|
|
1187
|
+
const fs = require('fs');
|
|
1188
|
+
const path = require('path');
|
|
1189
|
+
const os = require('os');
|
|
1190
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
1191
|
+
let claudeJson = {};
|
|
1192
|
+
try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch (e) {}
|
|
1193
|
+
const agentName = process.env.AGENT_NAME;
|
|
1194
|
+
const newUrl = process.env.NEW_URL;
|
|
1195
|
+
if (claudeJson.mcpServers && claudeJson.mcpServers[agentName]) {
|
|
1196
|
+
if (!claudeJson.mcpServers[agentName].env) claudeJson.mcpServers[agentName].env = {};
|
|
1197
|
+
claudeJson.mcpServers[agentName].env.ANTHROPIC_BASE_URL = newUrl;
|
|
1198
|
+
}
|
|
1199
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
|
|
1200
|
+
process.stdout.write(JSON.stringify({ written: true }) + '\n');
|
|
1201
|
+
" AGENT_NAME="{agent-name}" NEW_URL="{new-url}"
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
3. Sync keytar secrets to ~/.claude.json:
|
|
1205
|
+
```bash
|
|
1206
|
+
node -e "
|
|
1207
|
+
const { syncToClaudeJson, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
|
|
1208
|
+
syncToClaudeJson(SERVICE).then(() => process.stdout.write('synced\n')).catch(e => process.stderr.write(e.message + '\n'));
|
|
1209
|
+
"
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
4. Invoke `/qgsd:mcp-restart {agent-name}` (sequential). If restart fails, leave config written and display:
|
|
1213
|
+
```
|
|
1214
|
+
⚠ {agent-name}: restart failed. Config applied — reload on next Claude Code restart.
|
|
1215
|
+
Manual retry: /qgsd:mcp-restart {agent-name}
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
5. Display:
|
|
1219
|
+
```
|
|
1220
|
+
✓ Provider updated and agent restarted.
|
|
1221
|
+
|
|
1222
|
+
✓ {agent-name} — provider changed to {NEW_PROVIDER_NAME}, restarted
|
|
1223
|
+
|
|
1224
|
+
Run /qgsd:mcp-status to verify agent health.
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
Return to Agent Sub-Menu (user can make further changes or go Back).
|
|
1228
|
+
|
|
1229
|
+
**Option 3 — Remove agent:**
|
|
1230
|
+
|
|
1231
|
+
**Step A — Confirm removal**
|
|
1232
|
+
|
|
1233
|
+
Display removal warning:
|
|
1234
|
+
|
|
1235
|
+
```
|
|
1236
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1237
|
+
QGSD ► REMOVE AGENT
|
|
1238
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1239
|
+
|
|
1240
|
+
⚠ This will permanently remove {agent-name} from
|
|
1241
|
+
~/.claude.json. The agent will be deregistered on
|
|
1242
|
+
the next Claude Code restart.
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
Use AskUserQuestion:
|
|
1246
|
+
- header: "Remove Agent — {agent-name}"
|
|
1247
|
+
- question: `"Remove {agent-name} from ~/.claude.json?\n\nThis deletes the mcpServers entry. The agent process will be deregistered on the next Claude Code restart."`
|
|
1248
|
+
- options:
|
|
1249
|
+
- "Remove agent"
|
|
1250
|
+
- "Cancel — back to agent menu"
|
|
1251
|
+
|
|
1252
|
+
If "Cancel — back to agent menu": display "No changes made." Return to Agent Sub-Menu.
|
|
1253
|
+
|
|
1254
|
+
**Step B — Delete mcpServers entry**
|
|
1255
|
+
|
|
1256
|
+
If "Remove agent":
|
|
1257
|
+
|
|
1258
|
+
1. Backup ~/.claude.json:
|
|
1259
|
+
```bash
|
|
1260
|
+
cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
2. Delete the agent's mcpServers entry via inline node (agent name passed via env var — never interpolated):
|
|
1264
|
+
```bash
|
|
1265
|
+
node -e "
|
|
1266
|
+
const fs = require('fs');
|
|
1267
|
+
const path = require('path');
|
|
1268
|
+
const os = require('os');
|
|
1269
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
1270
|
+
let claudeJson = {};
|
|
1271
|
+
try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch (e) {}
|
|
1272
|
+
const agentName = process.env.AGENT_NAME;
|
|
1273
|
+
if (claudeJson.mcpServers && claudeJson.mcpServers[agentName]) {
|
|
1274
|
+
delete claudeJson.mcpServers[agentName];
|
|
1275
|
+
}
|
|
1276
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
|
|
1277
|
+
process.stdout.write(JSON.stringify({ removed: true, agent: agentName }) + '\n');
|
|
1278
|
+
" AGENT_NAME="{agent-name}"
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
3. Display:
|
|
1282
|
+
```
|
|
1283
|
+
✓ Agent removed.
|
|
1284
|
+
|
|
1285
|
+
✓ {agent-name} — removed from ~/.claude.json
|
|
1286
|
+
The agent will be deregistered on next Claude Code restart.
|
|
1287
|
+
|
|
1288
|
+
Run /qgsd:mcp-status to verify the updated agent roster.
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
Return to roster display (re-read roster — removed agent no longer appears).
|
|
1292
|
+
|
|
1293
|
+
**Option "Back":** Return to Re-run Agent Menu.
|
|
1294
|
+
|
|
1295
|
+
---
|
|
1296
|
+
|
|
1297
|
+
## Confirm + Apply + Restart Flow
|
|
1298
|
+
|
|
1299
|
+
Used by actions that accumulate pending changes in a session.
|
|
1300
|
+
|
|
1301
|
+
Display pending summary:
|
|
1302
|
+
|
|
1303
|
+
```
|
|
1304
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1305
|
+
QGSD ► REVIEW PENDING CHANGES
|
|
1306
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1307
|
+
|
|
1308
|
+
◆ {agent-name} — {description of change}
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
Use AskUserQuestion:
|
|
1312
|
+
- header: "Apply Changes"
|
|
1313
|
+
- question: "Apply changes to ~/.claude.json and restart affected agents?"
|
|
1314
|
+
- options:
|
|
1315
|
+
- "Apply and restart"
|
|
1316
|
+
- "Cancel — discard changes"
|
|
1317
|
+
|
|
1318
|
+
If "Cancel": display "Changes discarded." Return to roster.
|
|
1319
|
+
|
|
1320
|
+
If "Apply and restart":
|
|
1321
|
+
|
|
1322
|
+
1. Backup:
|
|
1323
|
+
```bash
|
|
1324
|
+
cp ~/.claude.json ~/.claude.json.backup-$(date +%Y-%m-%d-%H%M%S) 2>/dev/null || true
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
2. Write changes to `~/.claude.json` (inline node — read current, apply patch, write with 2-space indent).
|
|
1328
|
+
|
|
1329
|
+
3. Sync keytar secrets:
|
|
1330
|
+
```bash
|
|
1331
|
+
node -e "
|
|
1332
|
+
const { syncToClaudeJson, SERVICE } = require('~/.claude/qgsd-bin/secrets.cjs');
|
|
1333
|
+
syncToClaudeJson(SERVICE).then(() => process.stdout.write('synced\n')).catch(e => process.stderr.write(e.message + '\n'));
|
|
1334
|
+
"
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
4. For each affected agent (sequential, one at a time): invoke `/qgsd:mcp-restart {agent-name}`.
|
|
1338
|
+
|
|
1339
|
+
5. Display:
|
|
1340
|
+
```
|
|
1341
|
+
✓ Changes applied and agent(s) restarted.
|
|
1342
|
+
|
|
1343
|
+
✓ {agent-name} — restarted
|
|
1344
|
+
|
|
1345
|
+
Run /qgsd:mcp-status to verify agent health.
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
If a restart fails, leave config written and display:
|
|
1349
|
+
```
|
|
1350
|
+
⚠ {agent-name}: restart failed. Config applied — reload on next Claude Code restart.
|
|
1351
|
+
Manual retry: /qgsd:mcp-restart {agent-name}
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
</process>
|
|
1355
|
+
|
|
1356
|
+
<success_criteria>
|
|
1357
|
+
- First-run (no mcpServers): welcome banner + agent template list + key collection (keytar/fallback) + batch-write + backup + restart + summary
|
|
1358
|
+
- Re-run (existing entries): numbered agent roster with model/provider/key-status columns
|
|
1359
|
+
- Sub-menu per agent: full API key set/update flow (Option 1), full provider swap flow (Option 2), full remove-agent flow (Option 3)
|
|
1360
|
+
- Add-agent flow (roster menu): template select (filtered) → key collection → mcpServers write → syncToClaudeJson → restart → identity ping
|
|
1361
|
+
- Remove-agent flow (Option 3): confirm → backup → delete mcpServers[agent] entry → write
|
|
1362
|
+
- Option 1 API key flow: key-status check → "(key stored)" hint → key input → keytar store → confirm → backup → patch ~/.claude.json → syncToClaudeJson → mcp-restart
|
|
1363
|
+
- Option 2 provider swap flow: current provider display → curated list (AkashML/Together.xyz/Fireworks) + Custom URL → confirm → backup → patch ANTHROPIC_BASE_URL → mcp-restart
|
|
1364
|
+
- Confirm+apply+restart: backup then write then sync keytar then mcp-restart per agent then confirmation
|
|
1365
|
+
- No changes applied without explicit user confirmation
|
|
1366
|
+
- Keytar failure: warning + Linux hint + confirmation before env-block fallback + audit log
|
|
1367
|
+
- Key value never appears in displayed text, log output, or shell history (passed via env var only)
|
|
1368
|
+
- Edit Quorum Composition flow (WIZ-08): re-run menu option "Edit Quorum Composition" → routes to Composition Screen
|
|
1369
|
+
- Composition toggle flow (WIZ-09): slot list with ● ON / ○ OFF indicators → toggle updates PENDING_ACTIVE → apply writes quorum_active to ~/.claude/qgsd.json → no restart required
|
|
1370
|
+
- Add slot from composition flow (WIZ-10): "Add new slot" → Step A/B/B-native → identity ping → return to Composition Screen showing new slot ● ON
|
|
1371
|
+
</success_criteria>
|