slidemind 0.2.2 → 0.2.3
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +8 -1
- package/AGENTS.md +57 -11
- package/README.md +78 -34
- package/assets/app-icon.png +0 -0
- package/bin/slidemind.mjs +328 -100
- package/package.json +2 -1
- package/skills/create-slides/SKILL.md +15 -15
- package/skills/search/SKILL.md +6 -6
- package/src/bridge/client.mjs +11 -10
- package/src/bridge/config.mjs +2 -2
- package/src/bridge/{pair.mjs → http-json.mjs} +9 -24
- package/src/bridge/setup-claim.mjs +127 -0
- package/src/installers/claude.mjs +24 -0
- package/src/installers/codex.mjs +60 -4
- package/src/installers/lib.mjs +49 -19
- package/src/installers/opencode.mjs +16 -0
- package/src/installers/tui.mjs +21 -10
|
@@ -39,34 +39,34 @@ Two gate invariants you must never break — one per gate, equally binding:
|
|
|
39
39
|
|
|
40
40
|
## Bootstrap (set yourself up — don't make the user do it)
|
|
41
41
|
|
|
42
|
-
**The first step of every session is `slidemind ready`** (continuing an existing deck: `slidemind ready <deck>`). In one call it verifies
|
|
42
|
+
**The first step of every session is `slidemind ready`** (continuing an existing deck: `slidemind ready <deck>`). In one call it verifies authorization, connection, and CLI version; resolves the design the add-in has open; downloads or refreshes that design's bundle; and — given a deck arg — binds the deck to it.
|
|
43
43
|
|
|
44
44
|
- **Exit 0** → you're fully set up. Read `workflow/flow.md` (the stage spine) and `design.md` from the reported `bundleDir` and start planning.
|
|
45
45
|
- **Non-zero** → the CLI names exactly one blocker and one `nextAction`; do what it says. (The two blockers that ever need the user are handled in *First step* below.)
|
|
46
46
|
|
|
47
|
-
`ready` drives the `slidemind` CLI, which reaches PowerPoint through the add-in's **cloud relay** — no local server. Patch a missing CLI yourself; **
|
|
47
|
+
`ready` drives the `slidemind` CLI, which reaches PowerPoint through the add-in's **cloud relay** — no local server. Patch a missing CLI yourself; **connecting** needs the user once (they hold the add-in's identity):
|
|
48
48
|
|
|
49
|
-
- **CLI missing** (`slidemind` not on `PATH`): `npm install -g slidemind
|
|
50
|
-
- **Not
|
|
49
|
+
- **CLI missing** (`slidemind` not on `PATH`): `npm install -g slidemind` — that alone is the fix. Only run `slidemind init --for <claude|codex|opencode|all>` ((re)deploys this skill) when this skill did **not** come from a plugin: if the `SKILL.md` you are reading sits under a harness plugin cache (`~/.claude/plugins/…`, `~/.codex/plugins/…`), the plugin already supplies it and `init` would plant a second copy that drifts on the next plugin update. `npx slidemind <command>` runs one-off without a global install; `slidemind --version` confirms PATH; `slidemind update` upgrades (`--ref <version>` to pin) — unless the CLI itself came from a plugin, in which case update it there.
|
|
50
|
+
- **Not connected:** `ready` exits with `not_connected` — the CLI cannot start a connection by itself, because the claim that carries the deployment's address is minted in the add-in. Ask the user to open PowerPoint → slidemind → **Settings → General → Connect CLI**, press the **link button**, and copy the command shown (press **CLI already installed?** for the short `slidemind setup …` form). Have them paste it to you and run it **verbatim** — it connects, refreshes the skills, and runs `ready` itself. Add `--no-install --for none` only if their install must not be touched. `slidemind whoami` lists connected environments.
|
|
51
51
|
|
|
52
|
-
The CLI is **multi-environment**:
|
|
52
|
+
The CLI is **multi-environment**: connect once per backend (local / dev / prod) and every command fans out across all of them, tags each open window with its `env`, and routes to the right backend automatically — no manual switching. (Scope first-use authorization with `--backend <url>`; scope later calls with `--backend <url|env>`.) A normal user connects to the baked-in production backend only.
|
|
53
53
|
|
|
54
|
-
##
|
|
54
|
+
## Connecting the CLI (one-time; you guide the user)
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
Connecting is the **only** step that genuinely needs the user, and it starts in
|
|
57
|
+
PowerPoint — not here. Every organization runs its own slidemind backend, so an
|
|
58
|
+
unconnected CLI has no address to reach and nothing to discover it from; the
|
|
59
|
+
authenticated add-in knows both, and mints a one-time claim carrying its address.
|
|
57
60
|
|
|
58
|
-
**1 —
|
|
59
|
-
1. `npm install -g slidemind` — skip if the CLI is already installed.
|
|
60
|
-
2. pick the coding agent(s) → the wizard prints the exact `slidemind init --for <…>` — skip if you already ran `init`.
|
|
61
|
-
3. a **one-time** `slidemind pair <code>` command — this is the one that matters.
|
|
61
|
+
**1 — Ask for the command.** Tell the user to open the **slidemind add-in** task pane → **Settings** → **General** → **Connect CLI**, press the **link button**, and press **Copy command**. If they already have the CLI, **CLI already installed?** swaps it to the shorter `slidemind setup …` form; either works.
|
|
62
62
|
|
|
63
|
-
**2 — Run
|
|
63
|
+
**2 — Run it verbatim.** Paste it exactly as given. It redeems the claim, saves the session, installs the CLI globally if needed, deploys the skills into every coding agent on the machine, and finishes by running `ready` — so no `init` and no add-in restart belong to this flow.
|
|
64
64
|
|
|
65
|
-
**3 —
|
|
65
|
+
**3 — If it fails.** A claim works once and expires in ~10 minutes; on *already used or expired*, ask for a fresh one. The pane flips to **Connected** by itself once the claim is spent.
|
|
66
66
|
|
|
67
67
|
## First step — `slidemind ready` (before anything else, every time)
|
|
68
68
|
|
|
69
|
-
Everything about how a deck is authored, styled, and rendered is shaped by the corporate design **the add-in has open**, and every brand rule you follow is read from that design's **materialized bundle**. `slidemind ready` gets you there in one call: it walks the connection ladder (
|
|
69
|
+
Everything about how a deck is authored, styled, and rendered is shaped by the corporate design **the add-in has open**, and every brand rule you follow is read from that design's **materialized bundle**. `slidemind ready` gets you there in one call: it walks the connection ladder (a saved session → reachable backend → CLI version → an open render-ready window), resolves which open design to build against, downloads or refreshes that design's bundle (incremental — unchanged files cost nothing), and — when you pass a deck — binds the deck to it by writing `<deck>/.bundle-ref.json`.
|
|
70
70
|
|
|
71
71
|
- **New work:** `slidemind ready` — resolves + materializes the open design's bundle so you can read its `design.md` while planning.
|
|
72
72
|
- **Continuing a deck:** `slidemind ready <deck>` — same, and (re)binds the deck to whatever design is open.
|
|
@@ -80,7 +80,7 @@ Everything about how a deck is authored, styled, and rendered is shaped by the c
|
|
|
80
80
|
**On non-zero** the CLI names exactly one blocker `{code, actor, message}` and one `nextAction`. **The CLI owns the remediation text — relay `nextAction`, don't restate it.** Only two blockers ever need the user:
|
|
81
81
|
|
|
82
82
|
- **`ambiguous_design`** — several windows with **different** corporate designs are open, so `ready` can't choose. The CLI lists the candidates (each with its `env` and window `title`/`fileName`, plus a design fingerprint); if the deck was last built against one of them it's marked `suggested`. **Ask the user which design to build against, naming the windows by `title`/`fileName` (and `env`) — never by raw id** — and mention the suggested one if present ("this deck was last built against X — continue with it?"). Then re-run `slidemind ready --to <presId>` for the window they pick.
|
|
83
|
-
- **Connection blockers** (`
|
|
83
|
+
- **Connection blockers** (`not_connected`, `all_unreachable`, `cli_update_required`, `no_window`, `not_render_ready`) — relay the CLI's `nextAction` verbatim (copy the setup command / open PowerPoint / click the pane / run `slidemind update`). Connecting is walked through above. (`no_window` only ever surfaces for an **unbound** deck or deckless `ready` — a bound deck rides the pane-free rung above instead.)
|
|
84
84
|
|
|
85
85
|
**The session spine** (= the stage todos you created up front): `slidemind ready` → read `workflow/flow.md` + `design.md` from `bundleDir` → intake/plan (below) → `slidemind new <slug>` (scaffolds **and** auto-binds the deck to the open design — zero extra download, because `ready` already materialized it) → `slidemind plan <slug>` (plan sign-off gate for decks; single slides stay in chat) → `slidemind concepts <slug>` (mandatory concept-mockup pass for a deck, then rebuild toward each) → author → `view-draft` → `review` (mandatory, the moment the deck builds clean) → deliver (`push` / `build-pptx`). `slidemind new` best-effort binds when the open design is unambiguous and the bundle is already materialized; if it can't (ambiguous, or no host), it scaffolds anyway and hints to run `slidemind ready <slug>`.
|
|
86
86
|
|
package/skills/search/SKILL.md
CHANGED
|
@@ -13,20 +13,20 @@ So this file owns only what is genuinely CLI-specific: the **host precondition**
|
|
|
13
13
|
|
|
14
14
|
## Bootstrap (set yourself up — don't make the user do it)
|
|
15
15
|
|
|
16
|
-
**The first step of a search session is `slidemind ready --connect-only`** (see *Precondition gate* below). It drives the `slidemind` CLI, which reaches PowerPoint through the add-in's **cloud relay** — no local server. Patch a missing CLI yourself; **
|
|
16
|
+
**The first step of a search session is `slidemind ready --connect-only`** (see *Precondition gate* below). It drives the `slidemind` CLI, which reaches PowerPoint through the add-in's **cloud relay** — no local server. Patch a missing CLI yourself; **connecting** needs the user once (they hold the add-in's identity):
|
|
17
17
|
|
|
18
|
-
- **CLI missing** (`slidemind` not on `PATH`): `npm install -g slidemind
|
|
19
|
-
- **Not
|
|
18
|
+
- **CLI missing** (`slidemind` not on `PATH`): `npm install -g slidemind` — that alone is the fix. Add `slidemind init --for <claude|codex|opencode|all>` ((re)deploys this skill) only when this skill did **not** come from a plugin: a `SKILL.md` under a harness plugin cache (`~/.claude/plugins/…`, `~/.codex/plugins/…`) is already deployed, and `init` would just plant a second, drifting copy.
|
|
19
|
+
- **Not connected:** `ready --connect-only` exits with `not_connected` — the CLI cannot start a connection by itself, because the claim carrying the deployment's address is minted in the add-in. Ask the user to open PowerPoint → slidemind → **Settings → General → Connect CLI**, press the **link button**, and copy the command shown (**CLI already installed?** gives the short `slidemind setup …` form). Have them paste it to you and run it **verbatim**. `slidemind whoami` lists connected environments.
|
|
20
20
|
|
|
21
|
-
This is the same
|
|
21
|
+
This is the same connection as `slidemind-create-slides`, whose **Connecting the CLI** section carries the full step-by-step. If it was approved there, it is already available here.
|
|
22
22
|
|
|
23
23
|
## Precondition gate (check this first, every time)
|
|
24
24
|
|
|
25
25
|
Engine search and slide reuse are **host-required** operations — without an open PowerPoint host with the slidemind add-in loaded and an active corporate design, there are no engines to query and nowhere to insert. Check the connection in one call:
|
|
26
26
|
|
|
27
|
-
1. Run `slidemind ready --connect-only`. It walks the connection ladder (
|
|
27
|
+
1. Run `slidemind ready --connect-only`. It walks the connection ladder (a saved session → reachable backend → CLI version → an open window) but does **not** resolve a design or download a bundle — search doesn't need one.
|
|
28
28
|
- **Exit 0 → you're set.** Proceed to `slidemind ls engines` (the live catalog is authoritative — the first real-work step).
|
|
29
|
-
- **Non-zero →** the CLI names exactly one blocker and one `nextAction`; relay it (
|
|
29
|
+
- **Non-zero →** the CLI names exactly one blocker and one `nextAction`; relay it (copy the setup command / open PowerPoint / click the pane / run `slidemind update`). The CLI owns the remediation text — don't restate it.
|
|
30
30
|
2. **Never pick a window for discovery.** The engine catalog is **per-user, not bound to a window**, so search and discovery attach to **any** connected window automatically. **Never ask the user which window to search with**, and don't pass `--to` to search just to disambiguate — leave it off and let the CLI grab one. (A window only matters when you *insert a slide live*, and even then it's simply the window the search already ran on — see Reuse.)
|
|
31
31
|
|
|
32
32
|
The pane-touching ops here (`search`, `view_slide`, `insert-slide`, `download-result`) wait for the pane themselves: the CLI waits up to ~2 min, prints one *"click the slidemind pane"* line when user help is genuinely needed, and retries safely. Relay that line once and keep waiting — don't re-plan or babysit.
|
package/src/bridge/client.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Client for the slidemind cloud relay.
|
|
2
2
|
//
|
|
3
3
|
// The CLI talks to `<backendURL>/api/cli/*` with a Bearer session token — both
|
|
4
|
-
// established
|
|
4
|
+
// established by redeeming a one-time setup claim minted in the authenticated add-in.
|
|
5
|
+
// The add-in's cloud
|
|
5
6
|
// backend runs render/bundle in-process and relays insert/replace/engine to the
|
|
6
7
|
// open PowerPoint pane. No localhost, no shared loopback token, no cert.
|
|
7
8
|
|
|
@@ -121,11 +122,11 @@ function readinessTimeoutMs() {
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
const NOT_PAIRED_MESSAGE =
|
|
124
|
-
|
|
125
|
-
"
|
|
125
|
+
"slidemind is not connected. In PowerPoint open slidemind → Settings → " +
|
|
126
|
+
"General → Connect CLI, press the link button, and run the command it shows.";
|
|
126
127
|
|
|
127
128
|
// The one-command remedy every host-precondition error now points at:
|
|
128
|
-
// `slidemind ready` runs
|
|
129
|
+
// `slidemind ready` runs authorization → reachability → version → window →
|
|
129
130
|
// design ladder and materializes the bundle, so a stuck author has a single
|
|
130
131
|
// next step instead of a scavenger hunt across doctor/presentations/download.
|
|
131
132
|
// Appended (never substituted) to the existing prose so callers' assert.match
|
|
@@ -230,7 +231,7 @@ async function bridgeFetch(pathPart, init = {}, backendURL = null) {
|
|
|
230
231
|
if (err && /ECONNREFUSED|ECONNRESET|ENOTFOUND|self-signed|fetch failed/i.test(String(err.message || err))) {
|
|
231
232
|
throw new Error(
|
|
232
233
|
`slidemind relay unreachable (${deriveEnvLabel(session.backendURL)}). Check your connection; ` +
|
|
233
|
-
"if it persists,
|
|
234
|
+
"if it persists, reconnect from the add-in's Connect CLI panel.",
|
|
234
235
|
);
|
|
235
236
|
}
|
|
236
237
|
throw err;
|
|
@@ -238,8 +239,8 @@ async function bridgeFetch(pathPart, init = {}, backendURL = null) {
|
|
|
238
239
|
enforceVersionFloor(response);
|
|
239
240
|
if (response.status === 401) {
|
|
240
241
|
throw new Error(
|
|
241
|
-
"slidemind session is invalid or expired.
|
|
242
|
-
|
|
242
|
+
"slidemind session is invalid or expired. Run `slidemind logout --all`, then " +
|
|
243
|
+
"reconnect with a fresh command from the add-in's Connect CLI panel.",
|
|
243
244
|
);
|
|
244
245
|
}
|
|
245
246
|
if (response.status < 200 || response.status >= 300) {
|
|
@@ -447,7 +448,7 @@ async function bridgeStreamToFile(pathPart, init, destPath, backendURL = null) {
|
|
|
447
448
|
) {
|
|
448
449
|
throw new Error(
|
|
449
450
|
`slidemind relay unreachable (${deriveEnvLabel(session.backendURL)}). Check your connection; ` +
|
|
450
|
-
"if it persists,
|
|
451
|
+
"if it persists, reconnect from the add-in's Connect CLI panel.",
|
|
451
452
|
);
|
|
452
453
|
}
|
|
453
454
|
throw err;
|
|
@@ -455,8 +456,8 @@ async function bridgeStreamToFile(pathPart, init, destPath, backendURL = null) {
|
|
|
455
456
|
enforceVersionFloor(response);
|
|
456
457
|
if (response.status === 401) {
|
|
457
458
|
throw new Error(
|
|
458
|
-
"slidemind session is invalid or expired.
|
|
459
|
-
|
|
459
|
+
"slidemind session is invalid or expired. Run `slidemind logout --all`, then " +
|
|
460
|
+
"reconnect with a fresh command from the add-in's Connect CLI panel.",
|
|
460
461
|
);
|
|
461
462
|
}
|
|
462
463
|
if (response.status < 200 || response.status >= 300) {
|
package/src/bridge/config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// CLI session config:
|
|
2
|
-
//
|
|
1
|
+
// CLI session config: connecting stores the cloud relay endpoint(s) + the
|
|
2
|
+
// opaque session token each was issued. The CLI talks to
|
|
3
3
|
// <backendURL>/api/cli/* with `Authorization: Bearer <sessionToken>`; no
|
|
4
4
|
// localhost, no cert.
|
|
5
5
|
//
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// one-time secret for an opaque session token, stored by config.mjs. whoami/
|
|
5
|
-
// logout call the session-gated endpoints with that token.
|
|
1
|
+
// Minimal JSON-over-HTTP transport shared by setup-claim redemption and session
|
|
2
|
+
// management. Validates real TLS certificates; loopback development may use a
|
|
3
|
+
// self-signed HTTPS certificate or plain HTTP.
|
|
6
4
|
|
|
7
5
|
import http from "node:http";
|
|
8
6
|
import https from "node:https";
|
|
9
7
|
|
|
10
|
-
function isLoopbackHostname(
|
|
11
|
-
return
|
|
8
|
+
function isLoopbackHostname(hostname) {
|
|
9
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
// Minimal JSON request. Validates the cloud's real cert; skips only for a
|
|
15
|
-
// loopback dev backend (self-signed).
|
|
16
12
|
export function httpJson(method, urlStr, { body = null, token = null } = {}) {
|
|
17
13
|
const url = new URL(urlStr);
|
|
18
14
|
const transport = url.protocol === "https:" ? https : http;
|
|
@@ -30,11 +26,13 @@ export function httpJson(method, urlStr, { body = null, token = null } = {}) {
|
|
|
30
26
|
method,
|
|
31
27
|
headers,
|
|
32
28
|
rejectUnauthorized:
|
|
33
|
-
url.protocol === "https:"
|
|
29
|
+
url.protocol === "https:"
|
|
30
|
+
? !isLoopbackHostname(url.hostname)
|
|
31
|
+
: undefined,
|
|
34
32
|
},
|
|
35
33
|
(res) => {
|
|
36
34
|
const chunks = [];
|
|
37
|
-
res.on("data", (
|
|
35
|
+
res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
38
36
|
res.on("end", () => {
|
|
39
37
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
40
38
|
let json = null;
|
|
@@ -52,16 +50,3 @@ export function httpJson(method, urlStr, { body = null, token = null } = {}) {
|
|
|
52
50
|
req.end();
|
|
53
51
|
});
|
|
54
52
|
}
|
|
55
|
-
|
|
56
|
-
// Decode a `slidemind pair <blob>` code → { backendURL, secret } or null.
|
|
57
|
-
export function decodePairingCode(code) {
|
|
58
|
-
try {
|
|
59
|
-
const obj = JSON.parse(Buffer.from(code, "base64url").toString("utf8"));
|
|
60
|
-
if (obj && typeof obj.u === "string" && typeof obj.s === "string") {
|
|
61
|
-
return { backendURL: obj.u, secret: obj.s };
|
|
62
|
-
}
|
|
63
|
-
} catch {
|
|
64
|
-
/* malformed */
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// One-command setup claims — the CLI half of "click Connect CLI in PowerPoint,
|
|
2
|
+
// paste one command".
|
|
3
|
+
//
|
|
4
|
+
// Why the add-in mints this rather than the CLI: the CLI cannot discover WHICH slidemind
|
|
5
|
+
// deployment a user belongs to. Every customer runs its own backend, so an
|
|
6
|
+
// anonymous CLI has no address to start an approval at. The authenticated
|
|
7
|
+
// add-in already knows its deployment, so it mints a claim that carries the
|
|
8
|
+
// backend URL alongside a one-time secret; the user pastes it once and the CLI
|
|
9
|
+
// learns both at the same moment. This is the ONLY way a CLI gets a session.
|
|
10
|
+
//
|
|
11
|
+
// Wire shape: `smc1_<base64url({ v: 1, u: <backendURL>, s: <secret> })>`.
|
|
12
|
+
// The secret is the credential — it is one-time, short-lived, and bound
|
|
13
|
+
// server-side to the identity that minted it. Nothing here is a bearer token
|
|
14
|
+
// for anything else, and the claim is worthless once redeemed.
|
|
15
|
+
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
|
|
18
|
+
import { httpJson } from "./http-json.mjs";
|
|
19
|
+
|
|
20
|
+
export const SETUP_CLAIM_PREFIX = "smc1_";
|
|
21
|
+
|
|
22
|
+
function isLoopback(hostname) {
|
|
23
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function invalid(detail) {
|
|
27
|
+
return new Error(
|
|
28
|
+
`${detail}\n\n` +
|
|
29
|
+
"Open PowerPoint → slidemind → Settings → General → Connect CLI and copy a fresh setup command.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decode + validate a setup claim into { backendURL, secret }.
|
|
35
|
+
*
|
|
36
|
+
* Tolerates the shapes a copied terminal command actually arrives in: wrapping
|
|
37
|
+
* whitespace, surrounding quotes, and the whole `npx --yes slidemind@latest
|
|
38
|
+
* setup <claim>` line pasted as one argument.
|
|
39
|
+
*/
|
|
40
|
+
export function decodeSetupClaim(raw) {
|
|
41
|
+
let text = String(raw ?? "").trim();
|
|
42
|
+
// A user who quotes the argument, or pastes the entire command as one shell
|
|
43
|
+
// word, still gets what they meant.
|
|
44
|
+
text = text.replace(/^['"]|['"]$/g, "").trim();
|
|
45
|
+
if (text.includes(SETUP_CLAIM_PREFIX) && !text.startsWith(SETUP_CLAIM_PREFIX)) {
|
|
46
|
+
const match = text.match(/smc1_[A-Za-z0-9_-]+/);
|
|
47
|
+
if (match) text = match[0];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!text.startsWith(SETUP_CLAIM_PREFIX)) {
|
|
51
|
+
throw invalid(
|
|
52
|
+
"that does not look like a slidemind setup command (it should end with a smc1_… claim).",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const body = text.slice(SETUP_CLAIM_PREFIX.length);
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
60
|
+
} catch {
|
|
61
|
+
throw invalid("this setup command is damaged — it was probably copied only in part.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!parsed || parsed.v !== 1) {
|
|
65
|
+
throw invalid(
|
|
66
|
+
"this setup command was made by a newer slidemind add-in than this CLI understands. Run `npm install -g slidemind@latest` and try again.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const secret = typeof parsed.s === "string" ? parsed.s : "";
|
|
71
|
+
if (!secret) throw invalid("this setup command carries no connection secret.");
|
|
72
|
+
|
|
73
|
+
let url;
|
|
74
|
+
try {
|
|
75
|
+
url = new URL(String(parsed.u ?? ""));
|
|
76
|
+
} catch {
|
|
77
|
+
throw invalid("this setup command carries no valid slidemind address.");
|
|
78
|
+
}
|
|
79
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback(url.hostname))) {
|
|
80
|
+
throw invalid(
|
|
81
|
+
"this setup command points at a non-HTTPS slidemind address (HTTP is allowed only for loopback development).",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { backendURL: url.toString().replace(/\/$/, ""), secret };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Spend the one-time secret at its own deployment and receive the CLI session.
|
|
90
|
+
* The claim is consumed server-side whether or not this process survives, so a
|
|
91
|
+
* failure here always means "get a fresh command", never "retry".
|
|
92
|
+
*/
|
|
93
|
+
export async function redeemSetupClaim({ backendURL, secret, cliVersion }) {
|
|
94
|
+
let result;
|
|
95
|
+
try {
|
|
96
|
+
result = await httpJson("POST", `${backendURL}/api/cli/connect/redeem`, {
|
|
97
|
+
body: {
|
|
98
|
+
secret,
|
|
99
|
+
deviceName: os.hostname() || "CLI device",
|
|
100
|
+
platform: `${process.platform}-${process.arch}`,
|
|
101
|
+
cliVersion,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`could not reach slidemind to complete setup: ${error?.message || error}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (result.status === 200 && result.json?.sessionToken) {
|
|
111
|
+
return String(result.json.sessionToken);
|
|
112
|
+
}
|
|
113
|
+
// Discriminate on the error CODE, not the status. A deployment that predates
|
|
114
|
+
// one-command setup has no such route, so the request falls through to that
|
|
115
|
+
// router's session guard and comes back 401 `no_session` — the same status a
|
|
116
|
+
// spent claim returns. Only the body tells the two apart.
|
|
117
|
+
if (result.status === 401 && result.json?.error === "setup_claim_invalid_or_expired") {
|
|
118
|
+
throw invalid("this setup command has already been used or has expired.");
|
|
119
|
+
}
|
|
120
|
+
if (result.status === 401 || result.status === 404 || result.status === 405) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
"this slidemind add-in is too old for one-command setup — it needs an add-in update before the CLI can connect to it.",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const detail = result.json?.error || result.text || `HTTP ${result.status}`;
|
|
126
|
+
throw new Error(`slidemind could not complete setup: ${detail}`);
|
|
127
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import {
|
|
17
|
+
binaryOnPath,
|
|
17
18
|
expandHome,
|
|
18
19
|
installDir,
|
|
19
20
|
isForce,
|
|
@@ -91,6 +92,29 @@ function pluginInstallDetected() {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
// Is Claude Code on this machine, and would deploying here duplicate a plugin?
|
|
96
|
+
// `--for auto` (what `slidemind setup` uses) installs only where the harness
|
|
97
|
+
// actually exists AND its plugin has not already supplied the skills — copying
|
|
98
|
+
// over a plugin's skills plants a second set that drifts on the next plugin
|
|
99
|
+
// update, which is the one thing plugin users must not do.
|
|
100
|
+
//
|
|
101
|
+
// Two signals, because either alone misses a real install: the config dir is
|
|
102
|
+
// created the first time the agent RUNS (so a just-installed, never-launched
|
|
103
|
+
// Claude Code has none), while the binary is missing for a desktop-app-only
|
|
104
|
+
// install. Present if either says so.
|
|
105
|
+
export function detect() {
|
|
106
|
+
const root = claudeRoot();
|
|
107
|
+
const configured = fs.existsSync(root);
|
|
108
|
+
const onPath = binaryOnPath("claude");
|
|
109
|
+
return {
|
|
110
|
+
root,
|
|
111
|
+
configured,
|
|
112
|
+
onPath,
|
|
113
|
+
present: configured || onPath,
|
|
114
|
+
pluginInstalled: pluginInstallDetected(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
94
118
|
export async function install({ packageRoot, flags }) {
|
|
95
119
|
const link = isLinkMode(flags);
|
|
96
120
|
const force = isForce(flags);
|
package/src/installers/codex.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import fs from "node:fs";
|
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
27
|
import {
|
|
28
|
+
binaryOnPath,
|
|
28
29
|
expandHome,
|
|
29
30
|
installDir,
|
|
30
31
|
isForce,
|
|
@@ -81,6 +82,33 @@ function agentsFile() {
|
|
|
81
82
|
return path.join(codexRoot(), "AGENTS.md");
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
// `$CODEX_HOME/plugins/cache/<marketplace>/slidemind/<version>/` — a non-empty
|
|
86
|
+
// plugin directory means the Codex plugin is also installed, so the skills
|
|
87
|
+
// copied by `init` duplicate the ones the plugin already carries (and will
|
|
88
|
+
// drift the next time either side updates). Mirrors the Claude installer's
|
|
89
|
+
// check. A marketplace-only `cache/slidemind/` (added but never installed from)
|
|
90
|
+
// holds no `slidemind` child, so it correctly reads as "not installed".
|
|
91
|
+
export function pluginInstallDetected() {
|
|
92
|
+
const cacheRoot = path.join(codexRoot(), "plugins", "cache");
|
|
93
|
+
try {
|
|
94
|
+
return fs.readdirSync(cacheRoot).some((mp) => {
|
|
95
|
+
const dir = path.join(cacheRoot, mp, NAMESPACE);
|
|
96
|
+
try {
|
|
97
|
+
return (
|
|
98
|
+
fs.statSync(dir).isDirectory() &&
|
|
99
|
+
fs
|
|
100
|
+
.readdirSync(dir)
|
|
101
|
+
.some((entry) => !entry.startsWith("."))
|
|
102
|
+
);
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
84
112
|
function configTomlFile() {
|
|
85
113
|
return path.join(codexRoot(), "config.toml");
|
|
86
114
|
}
|
|
@@ -119,13 +147,18 @@ function writeAgentsBlock() {
|
|
|
119
147
|
const block =
|
|
120
148
|
`${AGENTS_MARK_BEGIN}\n` +
|
|
121
149
|
`## slidemind\n` +
|
|
122
|
-
`Slide-deck authoring
|
|
150
|
+
`Slide-deck authoring and slide-reuse skills installed via slidemind.\n` +
|
|
123
151
|
`\n` +
|
|
124
152
|
`When the user asks to create, plan, or edit a slide deck or presentation, load\n` +
|
|
125
153
|
`the \`slidemind-create-slides\` skill (auto-discovered from\n` +
|
|
126
|
-
`${skillTarget("create-slides")}/SKILL.md) and follow it
|
|
127
|
-
|
|
128
|
-
`
|
|
154
|
+
`${skillTarget("create-slides")}/SKILL.md) and follow it.\n` +
|
|
155
|
+
`\n` +
|
|
156
|
+
`When the user asks to find or reuse an existing slide or brand image — from the\n` +
|
|
157
|
+
`corporate design's engines or from a \`.pptx\` on disk — load the\n` +
|
|
158
|
+
`\`slidemind-search\` skill (${skillTarget("search")}/SKILL.md) instead.\n` +
|
|
159
|
+
`\n` +
|
|
160
|
+
`Both drive the slidemind PowerPoint add-in and read every brand rule — tokens,\n` +
|
|
161
|
+
`layout palette, brand imagery — from the design bundle the add-in serves.\n` +
|
|
129
162
|
`\n` +
|
|
130
163
|
`Build with: \`slidemind build <deck>\` (HTML) or \`slidemind build-pptx <deck>\` (PPTX).\n` +
|
|
131
164
|
`${AGENTS_MARK_END}\n`;
|
|
@@ -155,6 +188,20 @@ function clearAgentsBlock() {
|
|
|
155
188
|
fs.writeFileSync(file, existing);
|
|
156
189
|
}
|
|
157
190
|
|
|
191
|
+
// See claude.mjs `detect()` — same two-signal contract, Codex roots.
|
|
192
|
+
export function detect() {
|
|
193
|
+
const root = codexRoot();
|
|
194
|
+
const configured = fs.existsSync(root);
|
|
195
|
+
const onPath = binaryOnPath("codex");
|
|
196
|
+
return {
|
|
197
|
+
root,
|
|
198
|
+
configured,
|
|
199
|
+
onPath,
|
|
200
|
+
present: configured || onPath,
|
|
201
|
+
pluginInstalled: pluginInstallDetected(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
158
205
|
export async function install({ packageRoot, flags }) {
|
|
159
206
|
const link = isLinkMode(flags);
|
|
160
207
|
const force = isForce(flags);
|
|
@@ -202,6 +249,15 @@ export async function install({ packageRoot, flags }) {
|
|
|
202
249
|
ui.step({ status: "remove", label: "mcp server", value: mcpNote });
|
|
203
250
|
}
|
|
204
251
|
|
|
252
|
+
if (pluginInstallDetected()) {
|
|
253
|
+
ui.step({
|
|
254
|
+
status: "info",
|
|
255
|
+
label: "plugin",
|
|
256
|
+
value:
|
|
257
|
+
"the slidemind Codex plugin is also installed — these copied skills duplicate its skills; plugin users only need `npm install -g slidemind` and can drop them with `slidemind uninstall --for codex`",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
205
261
|
ui.note("restart Codex to pick up the skill");
|
|
206
262
|
}
|
|
207
263
|
|