@optima-chat/dev-skills 0.7.35 → 0.7.36

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.
@@ -0,0 +1,700 @@
1
+ # optima-plugin CLI — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax.
4
+
5
+ **Goal:** Add `optima-plugin` (show / set-paid / set-default) to `@optima-chat/dev-skills` — the skills-side admin command the marketplace-admin-cli missed, flipping `Plugin.isPaid` / `defaultForUser` (the actual user-facing paid/free gate).
6
+
7
+ **Architecture:** New bin mirroring `optima-product`'s dispatcher + subcommand-handler structure. Hits optima-skills `PATCH /api/admin/plugins/:slug` (writes) + public `GET /api/plugins/:slug` (show). Reuses the dev-skills M2M token (same client works against skills). Refactors `billing-http.ts` into a generic `callService` core shared by `callBilling` + a new `callSkills`.
8
+
9
+ **Tech Stack:** TypeScript, Node 18+ `fetch`, existing helpers (`getServiceToken`, `fetchInfisicalSecret`, `confirmIfProd`, `validateEnv`).
10
+
11
+ **Spec:** [`docs/superpowers/specs/2026-05-25-optima-plugin-cli-design.md`](../specs/2026-05-25-optima-plugin-cli-design.md)
12
+
13
+ **Testing convention:** smoke-only, no unit tests (matches all existing dev-skills helpers). Each task has a stage smoke step. Billing regression smoke after the refactor (T2).
14
+
15
+ ---
16
+
17
+ ## File Structure
18
+
19
+ | Path | Change |
20
+ |---|---|
21
+ | `bin/helpers/billing-http.ts` | **Modify** — extract `callService(baseUrl, env, method, path, body?)` private core (token-mint + 5xx-retry + non-JSON guard move in); `callBilling` delegates (signature/behavior unchanged); add `getSkillsUrl(env)` + `callSkills`; rename `formatBillingError`→`formatServiceError` + genericize its non-JSON string; export `callSkills`. |
22
+ | `bin/helpers/plugin.ts` | **Create** — `optima-plugin` dispatcher (show/set-paid/set-default). |
23
+ | `bin/helpers/plugin/show.ts` | **Create** — `show` handler (public GET). |
24
+ | `bin/helpers/plugin/set-paid.ts` | **Create** — `set-paid` handler (PATCH isPaid). |
25
+ | `bin/helpers/plugin/set-default.ts` | **Create** — `set-default` handler (PATCH defaultForUser). |
26
+ | `package.json` | **Modify** — add `optima-plugin` bin entry; version bump. |
27
+ | `AGENTS.md` | **Modify** — add `optima-plugin` to Primary Entry Points. |
28
+
29
+ ---
30
+
31
+ ## Task 1: Infra verification (no code)
32
+
33
+ **Goal:** Confirm the one remaining open question (spec §10): prod skills URL + prod dev-skills client on prod skills allowlist. Stage is already fully verified (spec §2).
34
+
35
+ - [ ] **Step 1: Verify prod SKILLS_REGISTRY_URL + stage (sanity)**
36
+
37
+ ```bash
38
+ cd /mnt/d/work/projects/optima/optima-dev-skills
39
+ node -e "const {fetchInfisicalSecret}=require('./dist/bin/helpers/infisical-secrets'); console.log('stage:', fetchInfisicalSecret('stage','/shared-secrets/domain-urls','SKILLS_REGISTRY_URL')); console.log('prod:', fetchInfisicalSecret('prod','/shared-secrets/domain-urls','SKILLS_REGISTRY_URL'));"
40
+ ```
41
+
42
+ Expected: stage `https://skills.stage.optima.onl`; prod some `https://skills.optima.onl` (or NOT FOUND → record; prod plugin commands then unavailable until added, non-blocking since the immediate goal is stage scout).
43
+
44
+ - [ ] **Step 2: Record findings inline, commit**
45
+
46
+ ```
47
+ RESOLVED: stage SKILLS_REGISTRY_URL=<...>, prod=<... or NOT FOUND>
48
+ (prod dev-skills allowlist: skills config default 'sales-page,dev-skills' applies unless overridden — verify only if prod use is needed now.)
49
+ ```
50
+
51
+ ```bash
52
+ git add docs/superpowers/plans/2026-05-25-optima-plugin-cli-impl.md
53
+ git commit -m "plan T1: verify prod SKILLS_REGISTRY_URL"
54
+ ```
55
+
56
+ Stage is sufficient to proceed regardless of prod result (immediate goal is stage scout/skillify).
57
+
58
+ ---
59
+
60
+ ## Task 2: Refactor billing-http → callService core + callSkills
61
+
62
+ **Files:**
63
+ - Modify: `bin/helpers/billing-http.ts`
64
+
65
+ - [ ] **Step 1: Add getSkillsUrl + refactor call core**
66
+
67
+ In `bin/helpers/billing-http.ts`:
68
+
69
+ (a) Add a skills URL resolver next to `getBillingUrl` (which is private, after the cache decls):
70
+
71
+ ```typescript
72
+ function getSkillsUrl(env: string): string {
73
+ if (skillsUrlCache[env]) return skillsUrlCache[env];
74
+ const url = fetchInfisicalSecret(env, '/shared-secrets/domain-urls', 'SKILLS_REGISTRY_URL');
75
+ skillsUrlCache[env] = url;
76
+ return url;
77
+ }
78
+ ```
79
+
80
+ (b) Add the cache decl alongside the existing `billingUrlCache`:
81
+
82
+ ```typescript
83
+ const skillsUrlCache: Record<string, string> = {};
84
+ ```
85
+
86
+ (c) Rename `formatBillingError` → `formatServiceError` (declaration at `billing-http.ts:92` + its one call site, now inside `callService` per step (d)). The `"Billing returned non-JSON 2xx body"` string lives in the call core and is genericized to `"Service returned..."` in step (d)'s code block.
87
+
88
+ (d) Replace the `callBilling` function (lines ~117-162) with a generic core + two thin wrappers:
89
+
90
+ ```typescript
91
+ // ───── Public: callService / callBilling / callSkills ───────────────────────
92
+ export interface ServiceResponse<T> {
93
+ status: number;
94
+ body: T;
95
+ }
96
+
97
+ /**
98
+ * Authenticated call to an Optima service (billing or skills — same dev-skills
99
+ * M2M token works for both). Returns `{status, body}` on 2xx; throws Error with
100
+ * formatted message on non-2xx. Single retry on 5xx (no backoff — admin CLI).
101
+ */
102
+ async function callService<T>(
103
+ baseUrl: string,
104
+ env: string,
105
+ method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
106
+ path: string,
107
+ body?: object,
108
+ ): Promise<ServiceResponse<T>> {
109
+ const url = `${baseUrl}${path}`;
110
+ const token = getServiceToken(env);
111
+
112
+ const doFetch = async () => fetch(url, {
113
+ method,
114
+ headers: {
115
+ Authorization: `Bearer ${token}`,
116
+ 'Content-Type': 'application/json',
117
+ },
118
+ body: body !== undefined ? JSON.stringify(body) : undefined,
119
+ });
120
+
121
+ let res = await doFetch();
122
+ if (res.status >= 500) {
123
+ res = await doFetch();
124
+ }
125
+ const text = await res.text();
126
+ if (!res.ok) {
127
+ throw new Error(formatServiceError(res.status, res.statusText, text));
128
+ }
129
+ let parsed: T;
130
+ try {
131
+ parsed = text ? (JSON.parse(text) as T) : (undefined as unknown as T);
132
+ } catch {
133
+ throw new Error(`Service returned non-JSON 2xx body: ${text.slice(0, 200)}`);
134
+ }
135
+ return { status: res.status, body: parsed };
136
+ }
137
+
138
+ /** @deprecated name retained for the billing-side callers. */
139
+ export interface BillingResponse<T> extends ServiceResponse<T> {}
140
+
141
+ export async function callBilling<T = unknown>(
142
+ env: string,
143
+ method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
144
+ path: string,
145
+ body?: object,
146
+ ): Promise<ServiceResponse<T>> {
147
+ return callService<T>(getBillingUrl(env), env, method, path, body);
148
+ }
149
+
150
+ export async function callSkills<T = unknown>(
151
+ env: string,
152
+ method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
153
+ path: string,
154
+ body?: object,
155
+ ): Promise<ServiceResponse<T>> {
156
+ return callService<T>(getSkillsUrl(env), env, method, path, body);
157
+ }
158
+ ```
159
+
160
+ (e) Rename the function declaration `function formatBillingError(` → `function formatServiceError(`. Leave its internal envelope-handling comments as-is (they accurately describe billing's shapes; skills' nested shape is handled by the same nested branch).
161
+
162
+ - [ ] **Step 2: Build**
163
+
164
+ ```bash
165
+ npm run build
166
+ ```
167
+
168
+ Expected: no errors. (`callBilling` callers in product/* + entitlement/* unchanged — they import `callBilling` which still exists with identical signature. `BillingResponse<T>` kept as alias so any type references still resolve.)
169
+
170
+ - [ ] **Step 3: Billing regression smoke (prove the refactor didn't break billing)**
171
+
172
+ ```bash
173
+ node dist/bin/helpers/entitlement.js list --email admin@optima.chat --env stage
174
+ ```
175
+
176
+ Expected: same behavior as before (prints `(no entitlements ...)` or a table) — proves callBilling still works through callService.
177
+
178
+ - [ ] **Step 4: Skills reachability smoke (prove callSkills works)**
179
+
180
+ ```bash
181
+ node -e "
182
+ const { callSkills } = require('./dist/bin/helpers/billing-http');
183
+ callSkills('stage','GET','/api/plugins/scout')
184
+ .then(r => console.log('STATUS', r.status, 'isPaid', r.body.isPaid))
185
+ .catch(e => { console.error(e.message); process.exit(1); });
186
+ "
187
+ ```
188
+
189
+ Expected: `STATUS 200 isPaid false` (scout current state).
190
+
191
+ - [ ] **Step 5: Commit**
192
+
193
+ ```bash
194
+ git add bin/helpers/billing-http.ts
195
+ git commit -m "refactor(billing-http): extract callService core + add callSkills/getSkillsUrl"
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Task 3: optima-plugin dispatcher + show subcommand
201
+
202
+ **Files:**
203
+ - Create: `bin/helpers/plugin.ts`
204
+ - Create: `bin/helpers/plugin/show.ts`
205
+
206
+ - [ ] **Step 1: Write the dispatcher**
207
+
208
+ Create `bin/helpers/plugin.ts`:
209
+
210
+ ```typescript
211
+ #!/usr/bin/env node
212
+
213
+ import { runShow } from './plugin/show';
214
+
215
+ function printHelp() {
216
+ console.log(`Usage: optima-plugin <subcommand> [options]
217
+
218
+ Subcommands:
219
+ show Show a plugin's marketplace state (isPaid, salesUrl, ... ACTIVE plugins only)
220
+ set-paid Flip a plugin's isPaid flag (the user-facing paid/free gate)
221
+ set-default Flip a plugin's defaultForUser flag
222
+
223
+ Run 'optima-plugin <subcommand> --help' for subcommand-specific options.`);
224
+ }
225
+
226
+ async function main() {
227
+ const [, , subcommand, ...rest] = process.argv;
228
+ if (!subcommand || subcommand === '-h' || subcommand === '--help') { printHelp(); process.exit(0); }
229
+ switch (subcommand) {
230
+ case 'show': await runShow(rest); break;
231
+ case 'set-paid':
232
+ case 'set-default':
233
+ console.error(`Subcommand '${subcommand}' not yet implemented (added in a later task).`);
234
+ process.exit(1);
235
+ default:
236
+ console.error(`Unknown subcommand: ${subcommand}`);
237
+ printHelp();
238
+ process.exit(1);
239
+ }
240
+ }
241
+
242
+ main().catch((err) => { console.error(err.message); process.exit(1); });
243
+ ```
244
+
245
+ - [ ] **Step 2: Write the show handler**
246
+
247
+ Create `bin/helpers/plugin/show.ts`:
248
+
249
+ ```typescript
250
+ import { callSkills, validateEnv } from '../billing-http';
251
+
252
+ interface ShowArgs {
253
+ slug: string;
254
+ env: string;
255
+ }
256
+
257
+ function parseArgs(argv: string[]): ShowArgs {
258
+ if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
259
+ console.log(`Usage: optima-plugin show --slug <slug> [options]
260
+
261
+ Required:
262
+ --slug <slug>
263
+
264
+ Optional:
265
+ --env stage|prod (default: stage)
266
+
267
+ Note: reads the public GET /api/plugins/:slug — shows isPaid, salesUrl, and
268
+ descriptive fields, but NOT defaultForUser / status / trustLevel (public
269
+ endpoint omits them). Returns 404 for non-ACTIVE plugins.`);
270
+ process.exit(0);
271
+ }
272
+ const out: Partial<ShowArgs> = { env: 'stage' };
273
+ for (let i = 0; i < argv.length; i++) {
274
+ const a = argv[i];
275
+ const next = argv[i + 1];
276
+ switch (a) {
277
+ case '--slug': out.slug = next; i++; break;
278
+ case '--env': out.env = next; i++; break;
279
+ default: throw new Error(`Unknown arg: ${a}`);
280
+ }
281
+ }
282
+ if (!out.slug) throw new Error('--slug required');
283
+ return out as ShowArgs;
284
+ }
285
+
286
+ export async function runShow(argv: string[]): Promise<void> {
287
+ const args = parseArgs(argv);
288
+ validateEnv(args.env);
289
+ const res = await callSkills(args.env, 'GET', `/api/plugins/${encodeURIComponent(args.slug)}`);
290
+ console.log(JSON.stringify(res.body, null, 2));
291
+ }
292
+ ```
293
+
294
+ - [ ] **Step 3: Build + smoke**
295
+
296
+ ```bash
297
+ npm run build
298
+ node dist/bin/helpers/plugin.js --help
299
+ node dist/bin/helpers/plugin.js show --help
300
+ node dist/bin/helpers/plugin.js show --slug scout --env stage
301
+ ```
302
+
303
+ Expected: help texts exit 0; `show --slug scout` prints scout JSON with `isPaid: false`, `salesUrl: null`.
304
+
305
+ - [ ] **Step 4: Commit**
306
+
307
+ ```bash
308
+ git add bin/helpers/plugin.ts bin/helpers/plugin/show.ts
309
+ git commit -m "feat(plugin): add optima-plugin dispatcher + show subcommand"
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Task 4: set-paid subcommand
315
+
316
+ **Files:**
317
+ - Create: `bin/helpers/plugin/set-paid.ts`
318
+ - Modify: `bin/helpers/plugin.ts`
319
+
320
+ - [ ] **Step 1: Write the handler**
321
+
322
+ Create `bin/helpers/plugin/set-paid.ts`:
323
+
324
+ ```typescript
325
+ import { callSkills, validateEnv } from '../billing-http';
326
+ import { confirmIfProd } from '../confirm-prompt';
327
+
328
+ interface SetPaidArgs {
329
+ slug: string;
330
+ paid: boolean;
331
+ yes: boolean;
332
+ env: string;
333
+ }
334
+
335
+ function parseArgs(argv: string[]): SetPaidArgs {
336
+ if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
337
+ console.log(`Usage: optima-plugin set-paid --slug <slug> --paid true|false [options]
338
+
339
+ Required:
340
+ --slug <slug>
341
+ --paid true|false Sets Plugin.isPaid (the user-facing paid/free gate)
342
+
343
+ Optional:
344
+ --yes Skip prod confirmation prompt (no-op on stage)
345
+ --env stage|prod (default: stage)
346
+
347
+ Note: salesUrl is NOT settable here (skills PATCH is strict; salesUrl is
348
+ publish-time-only via plugin.json metadata). When isPaid=true and salesUrl is
349
+ null, the 402 falls back to sales.optima.onl.`);
350
+ process.exit(0);
351
+ }
352
+ const out: Partial<SetPaidArgs> = { env: 'stage', yes: false };
353
+ for (let i = 0; i < argv.length; i++) {
354
+ const a = argv[i];
355
+ const next = argv[i + 1];
356
+ switch (a) {
357
+ case '--slug': out.slug = next; i++; break;
358
+ case '--paid':
359
+ if (next !== 'true' && next !== 'false') throw new Error('--paid must be true or false');
360
+ out.paid = next === 'true'; i++; break;
361
+ case '--yes': out.yes = true; break;
362
+ case '--env': out.env = next; i++; break;
363
+ default: throw new Error(`Unknown arg: ${a}`);
364
+ }
365
+ }
366
+ if (!out.slug) throw new Error('--slug required');
367
+ if (out.paid === undefined) throw new Error('--paid required (true|false)');
368
+ return out as SetPaidArgs;
369
+ }
370
+
371
+ export async function runSetPaid(argv: string[]): Promise<void> {
372
+ const args = parseArgs(argv);
373
+ validateEnv(args.env);
374
+
375
+ await confirmIfProd(
376
+ args.env,
377
+ `Action: set isPaid=${args.paid} on plugin '${args.slug}' (${args.env.toUpperCase()})`,
378
+ args.yes,
379
+ );
380
+
381
+ console.log(`\n💰 Setting isPaid=${args.paid} on ${args.slug} (${args.env.toUpperCase()})...`);
382
+ const res = await callSkills(
383
+ args.env,
384
+ 'PATCH',
385
+ `/api/admin/plugins/${encodeURIComponent(args.slug)}`,
386
+ { isPaid: args.paid },
387
+ );
388
+ console.log(`✓ Updated plugin (HTTP ${res.status}):`);
389
+ console.log(JSON.stringify(res.body, null, 2));
390
+ if (args.paid) {
391
+ console.log(`\nℹ️ Reminder: ensure a billing Product + channel exists for '${args.slug}' (optima-product) or users will 402 with no purchase path. salesUrl is publish-time-only (currently shown above).`);
392
+ }
393
+ }
394
+ ```
395
+
396
+ - [ ] **Step 2: Wire into dispatcher**
397
+
398
+ In `bin/helpers/plugin.ts` add import:
399
+
400
+ ```typescript
401
+ import { runSetPaid } from './plugin/set-paid';
402
+ ```
403
+
404
+ Replace the `case 'set-paid':` line in the stub block:
405
+
406
+ ```typescript
407
+ case 'set-paid':
408
+ case 'set-default':
409
+ console.error(`Subcommand '${subcommand}' not yet implemented (added in a later task).`);
410
+ process.exit(1);
411
+ ```
412
+
413
+ with:
414
+
415
+ ```typescript
416
+ case 'set-paid': await runSetPaid(rest); break;
417
+ case 'set-default':
418
+ console.error(`Subcommand '${subcommand}' not yet implemented (added in a later task).`);
419
+ process.exit(1);
420
+ ```
421
+
422
+ - [ ] **Step 3: Build + smoke**
423
+
424
+ ```bash
425
+ npm run build
426
+ node dist/bin/helpers/plugin.js set-paid --help
427
+ # Smoke: skillify is meant to be free — set-paid false is a safe no-op-ish smoke.
428
+ node dist/bin/helpers/plugin.js set-paid --slug skillify --paid false --env stage
429
+ ```
430
+
431
+ Expected: HTTP 200, returned row `isPaid: false`. (Does NOT make scout paid yet — that's the operator's deliberate action, kept out of the build smoke.)
432
+
433
+ - [ ] **Step 4: Commit**
434
+
435
+ ```bash
436
+ git add bin/helpers/plugin.ts bin/helpers/plugin/set-paid.ts
437
+ git commit -m "feat(plugin): add set-paid subcommand"
438
+ ```
439
+
440
+ ---
441
+
442
+ ## Task 5: set-default subcommand
443
+
444
+ **Files:**
445
+ - Create: `bin/helpers/plugin/set-default.ts`
446
+ - Modify: `bin/helpers/plugin.ts`
447
+
448
+ - [ ] **Step 1: Write the handler**
449
+
450
+ Create `bin/helpers/plugin/set-default.ts`:
451
+
452
+ ```typescript
453
+ import { callSkills, validateEnv } from '../billing-http';
454
+ import { confirmIfProd } from '../confirm-prompt';
455
+
456
+ interface SetDefaultArgs {
457
+ slug: string;
458
+ default: boolean;
459
+ yes: boolean;
460
+ env: string;
461
+ }
462
+
463
+ function parseArgs(argv: string[]): SetDefaultArgs {
464
+ if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
465
+ console.log(`Usage: optima-plugin set-default --slug <slug> --default true|false [options]
466
+
467
+ Required:
468
+ --slug <slug>
469
+ --default true|false Sets Plugin.defaultForUser
470
+
471
+ Optional:
472
+ --yes Skip prod confirmation prompt (no-op on stage)
473
+ --env stage|prod (default: stage)
474
+
475
+ Note: no skill-sync broadcast — changes what NEW user syncs receive; does not
476
+ retroactively add/remove the plugin for existing users until their next sync.`);
477
+ process.exit(0);
478
+ }
479
+ const out: Partial<SetDefaultArgs> = { env: 'stage', yes: false };
480
+ for (let i = 0; i < argv.length; i++) {
481
+ const a = argv[i];
482
+ const next = argv[i + 1];
483
+ switch (a) {
484
+ case '--slug': out.slug = next; i++; break;
485
+ case '--default':
486
+ if (next !== 'true' && next !== 'false') throw new Error('--default must be true or false');
487
+ out.default = next === 'true'; i++; break;
488
+ case '--yes': out.yes = true; break;
489
+ case '--env': out.env = next; i++; break;
490
+ default: throw new Error(`Unknown arg: ${a}`);
491
+ }
492
+ }
493
+ if (!out.slug) throw new Error('--slug required');
494
+ if (out.default === undefined) throw new Error('--default required (true|false)');
495
+ return out as SetDefaultArgs;
496
+ }
497
+
498
+ export async function runSetDefault(argv: string[]): Promise<void> {
499
+ const args = parseArgs(argv);
500
+ validateEnv(args.env);
501
+
502
+ await confirmIfProd(
503
+ args.env,
504
+ `Action: set defaultForUser=${args.default} on plugin '${args.slug}' (${args.env.toUpperCase()})`,
505
+ args.yes,
506
+ );
507
+
508
+ console.log(`\n🔧 Setting defaultForUser=${args.default} on ${args.slug} (${args.env.toUpperCase()})...`);
509
+ const res = await callSkills(
510
+ args.env,
511
+ 'PATCH',
512
+ `/api/admin/plugins/${encodeURIComponent(args.slug)}`,
513
+ { defaultForUser: args.default },
514
+ );
515
+ console.log(`✓ Updated plugin (HTTP ${res.status}):`);
516
+ console.log(JSON.stringify(res.body, null, 2));
517
+ }
518
+ ```
519
+
520
+ - [ ] **Step 2: Finalize dispatcher**
521
+
522
+ In `bin/helpers/plugin.ts` add import:
523
+
524
+ ```typescript
525
+ import { runSetDefault } from './plugin/set-default';
526
+ ```
527
+
528
+ Replace the remaining stub block:
529
+
530
+ ```typescript
531
+ case 'set-default':
532
+ console.error(`Subcommand '${subcommand}' not yet implemented (added in a later task).`);
533
+ process.exit(1);
534
+ ```
535
+
536
+ with:
537
+
538
+ ```typescript
539
+ case 'set-default': await runSetDefault(rest); break;
540
+ ```
541
+
542
+ Final switch should read:
543
+
544
+ ```typescript
545
+ switch (subcommand) {
546
+ case 'show': await runShow(rest); break;
547
+ case 'set-paid': await runSetPaid(rest); break;
548
+ case 'set-default': await runSetDefault(rest); break;
549
+ default:
550
+ console.error(`Unknown subcommand: ${subcommand}`);
551
+ printHelp();
552
+ process.exit(1);
553
+ }
554
+ ```
555
+
556
+ - [ ] **Step 3: Build + smoke (read-back via returned row)**
557
+
558
+ ```bash
559
+ npm run build
560
+ node dist/bin/helpers/plugin.js set-default --help
561
+ # Read scout's current defaultForUser by setting it to its own value is risky (unknown current);
562
+ # instead smoke on skillify: set true then confirm returned row, then leave as-is.
563
+ node dist/bin/helpers/plugin.js set-default --slug skillify --default true --env stage
564
+ ```
565
+
566
+ Expected: HTTP 200, returned row shows `defaultForUser: true`. (Records skillify's prior value from the returned row first if you want to restore; skillify as a free default plugin should be defaultForUser=true anyway.)
567
+
568
+ - [ ] **Step 4: Commit**
569
+
570
+ ```bash
571
+ git add bin/helpers/plugin.ts bin/helpers/plugin/set-default.ts
572
+ git commit -m "feat(plugin): add set-default subcommand"
573
+ ```
574
+
575
+ ---
576
+
577
+ ## Task 6: Register bin + AGENTS.md + version bump
578
+
579
+ **Files:**
580
+ - Modify: `package.json`
581
+ - Modify: `AGENTS.md`
582
+
583
+ - [ ] **Step 1: Add bin entry**
584
+
585
+ In `package.json` `bin`, add (alphabetical):
586
+
587
+ ```json
588
+ "optima-plugin": "dist/bin/helpers/plugin.js",
589
+ ```
590
+
591
+ (between `optima-grant-subscription` and `optima-product`.)
592
+
593
+ - [ ] **Step 2: Version bump**
594
+
595
+ ```bash
596
+ npm version patch --no-git-tag-version
597
+ ```
598
+
599
+ (0.7.35 → 0.7.36.)
600
+
601
+ - [ ] **Step 3: Update AGENTS.md**
602
+
603
+ In the "Primary Entry Points" list add:
604
+
605
+ ```markdown
606
+ - `optima-plugin <show|set-paid|set-default> [options]` — flip a plugin's skills-side paid/free state (isPaid) + defaultForUser (the user-facing gate; pairs with optima-product for the billing side)
607
+ ```
608
+
609
+ - [ ] **Step 4: Build + commit**
610
+
611
+ ```bash
612
+ npm run build
613
+ git add package.json AGENTS.md
614
+ git commit -m "chore: register optima-plugin bin entry + AGENTS.md (v0.7.36)"
615
+ ```
616
+
617
+ ---
618
+
619
+ ## Task 7: End-to-end stage smoke (spec §8)
620
+
621
+ **Files:** none.
622
+
623
+ - [ ] **Step 1: Install locally + run the spec §8 sequence**
624
+
625
+ ```bash
626
+ npm run build
627
+ node dist/bin/helpers/plugin.js show --slug scout --env stage # isPaid=false baseline
628
+ node dist/bin/helpers/plugin.js set-paid --slug scout --paid true --env stage # → 200 isPaid=true
629
+ node dist/bin/helpers/plugin.js show --slug scout --env stage # isPaid=true reflected
630
+ node dist/bin/helpers/plugin.js set-paid --slug skillify --paid false --env stage # skillify stays free
631
+ node dist/bin/helpers/plugin.js set-default --slug nonexistent-xyz --default true --env stage # → exit 1, 404
632
+ ```
633
+
634
+ Expected: each as annotated. **Note**: step 2 deliberately makes scout paid on stage — that is the intended end state (the operator's goal), not throwaway data. Leave scout `isPaid=true` after smoke. (skillify set-paid false is a no-op — already false.)
635
+
636
+ - [ ] **Step 2: Record results / any divergence**
637
+
638
+ If any response shape, error envelope, or field differs from spec, append `~/.claude/projects/-mnt-d-work-projects-optima/memory/marketplace_admin_cli_smoke_notes.md`. Else skip (silent green).
639
+
640
+ ---
641
+
642
+ ## Task 8: PR
643
+
644
+ **Files:** none.
645
+
646
+ - [ ] **Step 1: Push + PR**
647
+
648
+ ```bash
649
+ git push -u origin spec/optima-plugin-cli
650
+ gh pr create --base main --title "feat: optima-plugin CLI (skills-side isPaid/defaultForUser)" --body "$(cat <<'EOF'
651
+ ## Summary
652
+ Adds `optima-plugin` (show / set-paid / set-default) — the skills-side admin command the marketplace-admin-cli (#11) missed. Flips `Plugin.isPaid` / `defaultForUser` (the actual user-facing paid/free gate) via skills `PATCH /api/admin/plugins/:slug`, reusing the dev-skills M2M token. Refactors billing-http into a shared `callService` core (`callBilling` unchanged + new `callSkills`).
653
+
654
+ ## Spec + plan
655
+ - Spec: `docs/superpowers/specs/2026-05-25-optima-plugin-cli-design.md` (2 review rounds)
656
+ - Plan: `docs/superpowers/plans/2026-05-25-optima-plugin-cli-impl.md`
657
+
658
+ ## Smoke (stage)
659
+ show/set-paid/set-default all green; billing regression smoke (`optima-entitlement list`) green post-refactor; scout flipped isPaid=true (intended).
660
+
661
+ ## Follow-ups (spec §7)
662
+ - skills `GET /api/admin/plugins/:slug` (admin read for defaultForUser/status/trustLevel + non-ACTIVE)
663
+ - skills patchSchema: add salesUrl (so set-paid could set a custom sales page without re-publish)
664
+
665
+ 🤖 Generated with [Claude Code](https://claude.com/claude-code)
666
+ EOF
667
+ )"
668
+ ```
669
+
670
+ - [ ] **Step 2: (branch note)** This plan lives on `spec/optima-plugin-cli` (spec + plan + impl on one branch, like the main CLI). PR base = main.
671
+
672
+ ---
673
+
674
+ ## Self-review
675
+
676
+ | Spec section | Task |
677
+ |---|---|
678
+ | §5.1 show | T3 |
679
+ | §5.2 set-paid (no --sales-url, reminder on paid=true) | T4 |
680
+ | §5.3 set-default (sync note) | T5 |
681
+ | §4 callService refactor (token-mint moves in, generic error string, callBilling byte-identical, callSkills added) | T2 |
682
+ | §6 token reuse / error handling / exit 1 | T2 (callService) + handlers |
683
+ | §8 smoke (incl negative 404) | T7 |
684
+ | §10 prod URL open Q | T1 |
685
+ | §7 follow-ups | PR body T8 |
686
+
687
+ **Type/signature consistency:**
688
+ - `callSkills(env, method, path, body?)` — used in show/set-paid/set-default identically. ✓
689
+ - `callBilling` signature unchanged → product/* + entitlement/* unaffected. ✓
690
+ - `validateEnv(env)` first line of every runX. ✓
691
+ - `confirmIfProd(env, action, yes)` in set-paid + set-default. ✓
692
+ - PATCH bodies: `{isPaid: bool}` (T4), `{defaultForUser: bool}` (T5) — both within skills `.strict()` patchSchema. ✓
693
+
694
+ **Placeholder scan:** T1 records prod URL inline (resolved at exec). No TBD/TODO. Smoke uses real slugs (scout/skillify) + a deliberate nonexistent slug for the negative case. ✓
695
+
696
+ ---
697
+
698
+ ## Execution handoff
699
+
700
+ Plan saved. Subagent-driven execution (per main-CLI precedent): fresh subagent per task + per-task review where substantive, combined review where plan-pinned. T2 (refactor) gets careful review since it touches shipped billing code.