@optima-chat/dev-skills 0.7.35 → 0.7.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.claude/skills/discount-codes/SKILL.md +61 -0
  2. package/AGENTS.md +1 -0
  3. package/README.md +2 -0
  4. package/bin/helpers/billing-http.ts +41 -12
  5. package/bin/helpers/discount/create.ts +79 -0
  6. package/bin/helpers/discount/disable.ts +44 -0
  7. package/bin/helpers/discount/generate.ts +84 -0
  8. package/bin/helpers/discount/list.ts +46 -0
  9. package/bin/helpers/discount.ts +41 -0
  10. package/bin/helpers/plugin/set-default.ts +65 -0
  11. package/bin/helpers/plugin/set-paid.ts +69 -0
  12. package/bin/helpers/plugin/show.ts +42 -0
  13. package/bin/helpers/plugin.ts +32 -0
  14. package/dist/bin/helpers/billing-http.js +23 -9
  15. package/dist/bin/helpers/discount/create.js +101 -0
  16. package/dist/bin/helpers/discount/disable.js +48 -0
  17. package/dist/bin/helpers/discount/generate.js +135 -0
  18. package/dist/bin/helpers/discount/list.js +59 -0
  19. package/dist/bin/helpers/discount.js +47 -0
  20. package/dist/bin/helpers/entitlement.js +0 -0
  21. package/dist/bin/helpers/generate-test-token.js +0 -0
  22. package/dist/bin/helpers/grant-balance.js +0 -0
  23. package/dist/bin/helpers/grant-subscription.js +0 -0
  24. package/dist/bin/helpers/plugin/set-default.js +61 -0
  25. package/dist/bin/helpers/plugin/set-paid.js +65 -0
  26. package/dist/bin/helpers/plugin/show.js +45 -0
  27. package/dist/bin/helpers/plugin.js +39 -0
  28. package/dist/bin/helpers/product.js +0 -0
  29. package/dist/bin/helpers/query-db.js +0 -0
  30. package/dist/bin/helpers/show-env.js +0 -0
  31. package/docs/2026-05-27-discount-codes-plan-B-dev-skills-cli.md +517 -0
  32. package/docs/superpowers/plans/2026-05-25-optima-plugin-cli-impl.md +700 -0
  33. package/docs/superpowers/specs/2026-05-25-optima-plugin-cli-design.md +156 -0
  34. package/package.json +3 -1
  35. package/dist/bin/helpers/grant-credits.js +0 -71
@@ -0,0 +1,156 @@
1
+ # optima-plugin CLI — Design Spec
2
+
3
+ **Status**: draft for review
4
+ **Date**: 2026-05-25
5
+ **Author**: Jerry (via Claude collaborative spec)
6
+ **Repo**: `@optima-chat/dev-skills`
7
+
8
+ ## 1. Purpose
9
+
10
+ Add the **skills-side admin command that the marketplace-admin-cli ([#11](https://github.com/Optima-Chat/optima-dev-skills/pull/11)) missed**.
11
+
12
+ The shipped `optima-product` / `optima-entitlement` CLIs manage only the **billing half** of a paid plugin (Product, Channel, Entitlement). But the flag that actually makes a plugin paid/free to users — `Plugin.isPaid` — lives in **optima-skills**, and it is the real access gate:
13
+
14
+ ```ts
15
+ // optima-skills src/routes/plugins.ts:132 (GET /:slug/download-url) + user-plugins.ts:96 (install)
16
+ // NOTE: this gate is on the install/download paths, NOT the plain detail read
17
+ // GET /:slug (line 72) that `show` uses — so `show` is unaffected by isPaid.
18
+ if (plugin.isPaid) {
19
+ const ok = await hasEntitlement({ userId, pluginId, isPaid: true }); // REAL billing HTTP call
20
+ if (!ok) throw 402 PAYMENT_REQUIRED // → plugin.salesUrl (fallback: sales.optima.onl)
21
+ }
22
+ ```
23
+
24
+ (`hasEntitlement` is the real implementation — `entitlement-checker.ts` calls billing's `checkEntitlement` with caching + fail-mode. The stale `// stub for Phase 0` comment at `plugins.ts:128` is outdated; Wave 1.5 wired the real call.)
25
+
26
+ Because no plugin currently has `isPaid=true` (the Wave 1/1.5 testing sessions exercised billing only, never flipped the skills flag), **every plugin including scout is effectively free regardless of its billing Product**. There is no CLI to flip this. The skills admin endpoint exists and its auth middleware comment explicitly says it is "callable by the dev-skills CLI" — but the command was never built.
27
+
28
+ `optima-plugin` closes that gap.
29
+
30
+ ## 2. Background — verified facts (2026-05-25)
31
+
32
+ - **Endpoint**: `PATCH /api/admin/plugins/:slug` (optima-skills `src/routes/admin-plugins.ts:19-31`). **`.strict()`** zod body — accepts ONLY `{ trustLevel?, status?, category?, tags?, readme?, defaultForUser?, isPaid? }`. **`salesUrl` is NOT accepted** (verified: `.strict()` → any extra key incl. `salesUrl` → ZodError 400). `salesUrl` is writable **only at publish time** from `pluginJson.metadata.salesUrl` (`plugin-publish-persister.ts:71`). Returns the full updated Plugin row.
33
+ - **Auth**: `requireAdminService` = `tryM2mAuth` + `requireAdminServiceClient` (`src/middleware/admin-service.ts`). Same model as billing: verified service JWT (`type: "service"`) + clientId on `ADMIN_SERVICE_ALLOWLIST` (default `'sales-page,dev-skills'`, prefix match). **Verified live**: minted a dev-skills M2M token via the existing `billing-http.getServiceToken('stage')` and a no-op `PATCH /api/admin/plugins/scout {}` returned 200 with scout's row. dev-skills token works against skills admin unchanged.
34
+ - **Skills base URL**: Infisical `/shared-secrets/domain-urls/SKILLS_REGISTRY_URL`. Stage = `https://skills.stage.optima.onl` (verified). Prod value to confirm in impl T1 — **note skills IS deployed to prod** (skills.optima.onl, marketplace-v2 Option A), so `--env prod` may actually be functional for plugin commands, unlike `optima-product` (billing prod is pre-Wave-1.5).
35
+ - **Read path**: public `GET /api/plugins/:slug` exposes `{slug, name, description, version, isPaid, salesUrl, category, tags, components, author, updatedAt}` — enough to verify isPaid/salesUrl, but **NOT** `defaultForUser` / `status` / `trustLevel` (no admin GET-single endpoint exists).
36
+ - **Error envelope**: skills uses the **nested** `{error: {code, message}}` shape (e.g. admin-service.ts 401/403). billing's shared `formatBillingError` already handles both nested and flat, so it is reusable as-is for skills responses.
37
+
38
+ ## 3. Non-goals
39
+
40
+ - **No new auth/token work**: reuse `billing-http.getServiceToken(env)` verbatim (same dev-skills M2M client serves both billing and skills).
41
+ - **No auto-coupling of isPaid ↔ defaultForUser**: operator sets each independently. (A typical free plugin is `isPaid=false, defaultForUser=true`, but the CLI does not enforce or auto-apply that — explicit is safer for an admin tool.)
42
+ - **No general `patch` escape hatch**: only the three scoped verbs below. The skills PATCH also accepts trustLevel/status/category/tags/readme, but those are out of scope (use a future command or psql if ever needed).
43
+ - **No write of `defaultForUser` read-back in `show`**: `show` uses the public GET (no defaultForUser field). Reading defaultForUser back is deferred to a skills follow-up (admin GET-single).
44
+ - **No marketplace re-publish / version management**: that is the existing publish flow's job.
45
+
46
+ ## 4. Architecture
47
+
48
+ New bin `optima-plugin` → dispatcher → subcommand handlers, mirroring `optima-product`'s structure.
49
+
50
+ ```
51
+ optima-plugin <subcommand> → bin/helpers/plugin.ts (dispatcher)
52
+ → bin/helpers/plugin/show.ts
53
+ → bin/helpers/plugin/set-paid.ts
54
+ → bin/helpers/plugin/set-default.ts
55
+ ```
56
+
57
+ **Shared HTTP refactor** (low-risk, on freshly-shipped code): generalize `billing-http.ts`'s call core so both billing and skills reuse the fetch + 5xx-retry + envelope-format + token logic.
58
+
59
+ - Extract the existing `callBilling` body into a private `callService(baseUrl, env, method, path, body?)` that takes a resolved base URL. **The `getServiceToken(env)` mint moves INTO `callService`** (currently inside `callBilling`); the 5xx single-retry + non-JSON-2xx guard move verbatim too.
60
+ - `callBilling(env, method, path, body?)` → `callService(getBillingUrl(env), env, ...)` (unchanged behavior + signature — billing path stays byte-identical).
61
+ - Add `getSkillsUrl(env)` (Infisical `SKILLS_REGISTRY_URL`, memoized like `getBillingUrl`) + `callSkills(env, method, path, body?)` → `callService(getSkillsUrl(env), env, ...)`.
62
+ - `getServiceToken`, `formatBillingError` (rename → `formatServiceError`, keep behavior), caches: shared. **Genericize the one hardcoded string** `"Billing returned non-JSON 2xx body"` → `"Service returned non-JSON 2xx body"` (it now also covers skills).
63
+
64
+ Reuse unchanged: `confirmIfProd` (prod prompt), `validateEnv` (stage|prod gate), `fetchInfisicalSecret`.
65
+
66
+ ## 5. Command surface
67
+
68
+ All commands: `--env stage|prod` (default `stage`), `-h`/`--help`, validated via `validateEnv` at entry.
69
+
70
+ ### 5.1 `optima-plugin show`
71
+
72
+ ```
73
+ optima-plugin show --slug <slug> [--env stage|prod]
74
+
75
+ GET /api/plugins/:slug (public; no auth needed but token injection is harmless)
76
+ ```
77
+
78
+ Output: pretty-printed `{slug, name, version, isPaid, salesUrl, category, tags, ...}`. **Does NOT include defaultForUser / status / trustLevel** — public endpoint omits them (documented gap; skills follow-up to add admin GET-single). One-line note printed when shown.
79
+
80
+ **Limitation**: `GET /api/plugins/:slug` returns 404 for non-ACTIVE plugins (`plugins.ts:78` rejects `status !== 'ACTIVE'`). `show` therefore only works for ACTIVE plugins; a BETA/DEPRECATED plugin surfaces as `404 NOT_FOUND`. Acceptable — scout/skillify are ACTIVE. (The same admin GET-single follow-up in §7 would lift this.)
81
+
82
+ ### 5.2 `optima-plugin set-paid`
83
+
84
+ ```
85
+ optima-plugin set-paid --slug <slug> --paid true|false [--env stage|prod] [--yes]
86
+
87
+ PATCH /api/admin/plugins/:slug
88
+ Body: { isPaid: <bool> }
89
+ ```
90
+
91
+ - `--paid` required (`true`|`false`); maps to `isPaid`.
92
+ - **No `--sales-url` flag** — the skills PATCH `.strict()` schema rejects `salesUrl` (§2). salesUrl is publish-time-only (`metadata.salesUrl`). When `--paid true` and the plugin's salesUrl is null, the 402 falls back to `sales.optima.onl`. Setting a custom sales page requires either a re-publish with `metadata.salesUrl`, or a skills follow-up to add `salesUrl` to patchSchema (§7). **Reminder printed** on `--paid true`: "ensure a billing Product + channel exists (optima-product) or users will 402 with no purchase path; salesUrl is publish-time-only."
93
+ - prod confirm prompt (resolved action: slug + isPaid + env), `--yes` bypass.
94
+ - Output: pretty-print the returned updated Plugin row (skills PATCH returns full row incl isPaid, salesUrl, defaultForUser).
95
+
96
+ ### 5.3 `optima-plugin set-default`
97
+
98
+ ```
99
+ optima-plugin set-default --slug <slug> --default true|false
100
+ [--env stage|prod] [--yes]
101
+
102
+ PATCH /api/admin/plugins/:slug
103
+ Body: { defaultForUser: <bool> }
104
+ ```
105
+
106
+ - `--default` required (`true`|`false`); maps to `defaultForUser`.
107
+ - prod confirm prompt, `--yes` bypass.
108
+ - Output: pretty-print returned row.
109
+ - **Note**: the PATCH is a plain `prisma.plugin.update` with no skill-sync broadcast (matches the rollback handler's documented Phase 0 behavior). Flipping `defaultForUser` changes what NEW user syncs receive; it does not retroactively push/remove the plugin for existing users until their next session/sync boundary.
110
+
111
+ ## 6. Implementation notes
112
+
113
+ - **Token**: `getServiceToken(env)` reused. Skills admin uses the same dev-skills client (stage `dev-skills-ubd3qz6n`, prod `dev-skills-hinxa0rs`) already on skills' `ADMIN_SERVICE_ALLOWLIST` (default includes `dev-skills`).
114
+ - **Error handling**: `callSkills` reuses the shared `formatServiceError` → `❌ Error [<status>] <code>: <message>`, non-envelope fallback for non-JSON. Exit 1 on any error (matches house convention).
115
+ - **404 on unknown slug**: skills returns 404 `{error:{code:'NOT_FOUND',...}}` (`Errors.notFound('Plugin')`). Surfaces via formatter.
116
+ - **`show` token**: public endpoint needs no auth; `callSkills` injects the Bearer anyway (harmless). Keeps one code path.
117
+ - **prod**: unlike `optima-product`, `--env prod` for `optima-plugin` may be functional (skills prod is live). T1 confirms prod `SKILLS_REGISTRY_URL` + prod dev-skills client on prod skills allowlist.
118
+
119
+ ## 7. Out-of-scope follow-ups
120
+
121
+ | Item | Why deferred |
122
+ |---|---|
123
+ | skills `GET /api/admin/plugins/:slug` (admin read with defaultForUser/status/trustLevel, any status) | No admin GET-single exists; public GET is ACTIVE-only + omits admin fields. File optima-skills issue. Until then `show` reads the public listing (isPaid/salesUrl, ACTIVE-only). |
124
+ | skills patchSchema: add `salesUrl` (so `set-paid --sales-url` becomes possible) | Currently salesUrl is publish-time-only; `.strict()` PATCH rejects it. If operators need to set custom sales pages without re-publishing, file an optima-skills issue to add `salesUrl: z.string().nullable().optional()` to patchSchema, then add `--sales-url` to set-paid. |
125
+ | `optima-plugin` verbs for trustLevel/status/category/tags/readme | PATCH supports them but no current need. Add when required. |
126
+ | Auto-set `defaultForUser=true` when making a plugin free | Deliberate non-goal — operator controls independently (§3). |
127
+
128
+ ## 8. Testing approach
129
+
130
+ Smoke-only (matches all existing dev-skills helpers; no unit tests).
131
+
132
+ Stage smoke (use `scout` — the real plugin we want paid, and `skillify` — should stay free):
133
+
134
+ 1. `optima-plugin show --slug scout --env stage` → isPaid=false, salesUrl=null (current state)
135
+ 2. `optima-plugin set-paid --slug scout --paid true --env stage` → 200, returned row isPaid=true (salesUrl stays whatever publish set — likely null; 402 falls back to sales.optima.onl)
136
+ 3. `optima-plugin show --slug scout --env stage` → isPaid=true reflected
137
+ 4. `optima-plugin set-default --slug scout --default false --env stage` → 200, returned row defaultForUser=false (verify via the returned PATCH body since show can't read it)
138
+ 5. `optima-plugin set-paid --slug skillify --paid false --env stage` → 200, isPaid=false (confirms idempotent / skillify stays free)
139
+ 6. Negative: `optima-plugin set-paid --slug nonexistent-xyz --paid true --env stage` → exit 1, 404 NOT_FOUND surfaced cleanly
140
+
141
+ **State note**: this smoke deliberately flips real stage plugin state (scout → paid). That is the intended end state per the operator's goal, not throwaway test data — leave scout paid after smoke (or coordinate with the separate "make scout paid" task). skillify set-paid false is a no-op (already false).
142
+
143
+ ## 9. Risks
144
+
145
+ | Risk | Mitigation |
146
+ |---|---|
147
+ | Shared HTTP refactor breaks the just-shipped billing path | `callBilling` keeps its exact signature + behavior (delegates to `callService` with billing URL). Smoke billing once post-refactor (`optima-entitlement list`) to confirm no regression. |
148
+ | skills prod allowlist missing dev-skills | T1 verifies; default includes `dev-skills` so likely fine. 403 message names the client if not. |
149
+ | `show` can't display defaultForUser | Documented; `set-default` output (returned PATCH row) is the read-back path until the admin GET-single follow-up lands. |
150
+ | Operator flips isPaid=true but no billing Product/channel exists → users hit 402 with no way to buy | Out of CLI's enforcement scope, but `set-paid --paid true` prints a reminder: "ensure a billing Product + channel exists (optima-product) or users will 402 with no purchase path." |
151
+
152
+ ## 10. Open questions
153
+
154
+ 1. **[T1]** prod `SKILLS_REGISTRY_URL` value + prod dev-skills client (`dev-skills-hinxa0rs`) on prod skills `ADMIN_SERVICE_ALLOWLIST`?
155
+
156
+ (Resolved during spec review: public `GET /api/plugins/:slug` rejects non-ACTIVE plugins with 404 — `plugins.ts:78`. `show` is ACTIVE-only; documented in §5.1. `salesUrl` is not PATCH-settable — `set-paid` drops `--sales-url`; documented in §2/§5.2/§7.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/dev-skills",
3
- "version": "0.7.35",
3
+ "version": "0.7.37",
4
4
  "description": "Claude Code Skills for Optima development team - cross-environment collaboration tools",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -9,6 +9,8 @@
9
9
  "optima-generate-test-token": "dist/bin/helpers/generate-test-token.js",
10
10
  "optima-grant-balance": "dist/bin/helpers/grant-balance.js",
11
11
  "optima-grant-subscription": "dist/bin/helpers/grant-subscription.js",
12
+ "optima-plugin": "dist/bin/helpers/plugin.js",
13
+ "optima-discount": "dist/bin/helpers/discount.js",
12
14
  "optima-product": "dist/bin/helpers/product.js",
13
15
  "optima-query-db": "dist/bin/helpers/query-db.js",
14
16
  "optima-show-env": "dist/bin/helpers/show-env.js"
@@ -1,71 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- const db_utils_1 = require("./db-utils");
5
- function parseArgs(args) {
6
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
7
- console.log(`Usage: optima-grant-credits <email> --amount <n> [options]
8
-
9
- Options:
10
- --amount <n> Credits to grant (required)
11
- --type <type> Credit type: bonus, referral (default: bonus)
12
- --description <text> Description (optional)
13
- --env <env> Environment: stage, prod (default: stage)
14
- -h, --help Show this help`);
15
- process.exit(0);
16
- }
17
- const email = args[0];
18
- let amount = 0;
19
- let type = 'bonus';
20
- let description = null;
21
- let env = 'stage';
22
- for (let i = 1; i < args.length; i++) {
23
- if (args[i] === '--amount' && args[i + 1]) {
24
- amount = parseInt(args[++i], 10);
25
- }
26
- else if (args[i] === '--type' && args[i + 1]) {
27
- type = args[++i];
28
- }
29
- else if (args[i] === '--description' && args[i + 1]) {
30
- description = args[++i];
31
- }
32
- else if (args[i] === '--env' && args[i + 1]) {
33
- env = args[++i];
34
- }
35
- }
36
- if (amount < 1) {
37
- console.error('--amount is required and must be >= 1');
38
- process.exit(1);
39
- }
40
- if (!['bonus', 'referral'].includes(type)) {
41
- console.error(`Unknown type: ${type}. Available: bonus, referral`);
42
- process.exit(1);
43
- }
44
- if (!['stage', 'prod'].includes(env)) {
45
- console.error('Env must be stage or prod (billing DB not available in CI)');
46
- process.exit(1);
47
- }
48
- return { email, amount, type, description, env };
49
- }
50
- async function main() {
51
- const { email, amount, type, description, env } = parseArgs(process.argv.slice(2));
52
- const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
53
- const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
54
- console.log(`\n🎁 Granting ${amount} ${type} credits to ${email} [${env.toUpperCase()}]\n`);
55
- const userId = await (0, db_utils_1.resolveUserId)(email, env, infisicalConfig, token);
56
- const billing = await (0, db_utils_1.connectBillingDB)(env, infisicalConfig, token);
57
- const bq = billing.query;
58
- const now = new Date().toISOString();
59
- const safeUserId = (0, db_utils_1.escapeSQL)(userId);
60
- const safeType = (0, db_utils_1.escapeSQL)(type);
61
- const safeDesc = (0, db_utils_1.escapeSQL)(description || `Admin ${type} credit grant`);
62
- console.log(`Inserting ${amount} ${type} credits...`);
63
- const ledgerId = bq(`INSERT INTO credit_ledger (id, user_id, type, description, initial_amount, remaining, created_at) VALUES (concat('crd_${safeType}_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safeType}', '${safeDesc}', ${amount}, ${amount}, '${now}') RETURNING id`);
64
- console.log(`✓ Credits granted (ledger ID: ${ledgerId})`);
65
- const balance = bq(`SELECT COALESCE(SUM(remaining), 0) FROM credit_ledger WHERE user_id='${safeUserId}' AND remaining > 0 AND (expires_at IS NULL OR expires_at > NOW())`);
66
- console.log(`\n✅ Done! ${email} now has ${balance} total credits\n`);
67
- }
68
- main().catch(error => {
69
- console.error('\n❌ Error:', error.message);
70
- process.exit(1);
71
- });