slidemind 0.3.20 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
package/src/cli/search.mjs
CHANGED
|
@@ -5,6 +5,24 @@ import { fail, resolveDeckArg, SRC_DIR } from "./common.mjs";
|
|
|
5
5
|
import { readDesignName, readThemeColors, designRows } from "./designs.mjs";
|
|
6
6
|
import { searchFailureHint, writeEmptySearchDiagnostic, writeSearchGrid, resolveEngineRouting, deckBrandId, failOnEngineBlocker, paneRelayForLoginRequired } from "./engine-helpers.mjs";
|
|
7
7
|
|
|
8
|
+
// The live engine catalog, or null when the caller falls back to the bundle:
|
|
9
|
+
// this add-in has no catalog route (a 404). A catalog that wants a Microsoft
|
|
10
|
+
// 365 sign-in while a pane is open falls back too, naming that pane — it
|
|
11
|
+
// refuses a token-less CLI even then, and the search itself relays through the
|
|
12
|
+
// pane (paneRelayForLoginRequired). Anything else — a 429, a timeout, an
|
|
13
|
+
// outage — is not "no engines": it surfaces as itself (x71/x72: a rate-limited
|
|
14
|
+
// read printed "no image engines" and the agent fabricated icons).
|
|
15
|
+
async function liveCatalog(client, opts, to) {
|
|
16
|
+
try {
|
|
17
|
+
return await client.listEngines(opts);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
if (err?.status === 404) return null;
|
|
20
|
+
const relay = await paneRelayForLoginRequired(client, err, { to });
|
|
21
|
+
if (relay) return { engines: [], pane: relay.entry };
|
|
22
|
+
return failOnEngineBlocker(client, err, { to });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
export async function cmdSearch(rest) {
|
|
9
27
|
let asJson = false;
|
|
10
28
|
let to = null;
|
|
@@ -111,7 +129,7 @@ export async function cmdSearch(rest) {
|
|
|
111
129
|
!bundle.isSearchScopeId(engineId) &&
|
|
112
130
|
!/^[0-9a-f-]{8,}$/i.test(engineId)
|
|
113
131
|
) {
|
|
114
|
-
const live = await client
|
|
132
|
+
const live = await liveCatalog(client, { backendURL }, to);
|
|
115
133
|
const wanted = engineId.toLowerCase();
|
|
116
134
|
const ids = new Set(
|
|
117
135
|
(live?.engines ?? [])
|
|
@@ -140,26 +158,27 @@ export async function cmdSearch(rest) {
|
|
|
140
158
|
// Imagery: resolve a real image engine (a scope is not a brand image
|
|
141
159
|
// source). Ask the LIVE catalog first — the same one the add-in's own
|
|
142
160
|
// design agent reads — and fall back to the materialized bundle only when
|
|
143
|
-
// the
|
|
144
|
-
// retired server-side, and a dead id surfaces as "Engine not
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
.catch(() => ({ engines: [] }));
|
|
161
|
+
// the add-in has no catalog route. Bundle-first would be stale: engine
|
|
162
|
+
// ids can be retired server-side, and a dead id surfaces as "Engine not
|
|
163
|
+
// found".
|
|
164
|
+
const live = (await liveCatalog(client, { subtype: "image", backendURL }, to)) ?? { engines: [] };
|
|
148
165
|
let pick = bundle.pickImageEngine(live.engines);
|
|
149
166
|
if (!pick) {
|
|
150
167
|
// Best-effort: a bundle that can't be materialized (e.g. an incomplete
|
|
151
168
|
// one) must not replace the actionable "wire an image engine" message
|
|
152
169
|
// below with a bundle error — the live catalog is the real answer here.
|
|
153
|
-
// Pane path: fetch by the window's presId. Direct path:
|
|
154
|
-
//
|
|
155
|
-
// there is nothing to fall back to and
|
|
170
|
+
// Pane path: fetch by the window's presId. Direct path: a bound deck
|
|
171
|
+
// (--deck) names the design, else the open pane a sign-in-less search
|
|
172
|
+
// relays through; without either there is nothing to fall back to and
|
|
173
|
+
// the live catalog's answer stands.
|
|
156
174
|
const brandId = presId
|
|
157
175
|
? null
|
|
158
176
|
: await deckBrandId(deck ? resolveDeckArg(deck) : null);
|
|
177
|
+
const windowId = presId ?? (brandId ? null : (live.pane?.presId ?? null));
|
|
159
178
|
const mat =
|
|
160
|
-
|
|
179
|
+
windowId || brandId
|
|
161
180
|
? await bundle
|
|
162
|
-
.materializeBundle(
|
|
181
|
+
.materializeBundle(windowId, { backendURL, brandId })
|
|
163
182
|
.catch(() => null)
|
|
164
183
|
: null;
|
|
165
184
|
if (mat) pick = bundle.pickImageEngine(bundle.readEnginesFromDir(mat.dir));
|
package/src/cli/session.mjs
CHANGED
|
@@ -65,7 +65,11 @@ export async function cmdLogin(rest) {
|
|
|
65
65
|
`Signed in to Microsoft 365 as ${res.username ?? "your account"}${envPhrase(target.label, "for")}.\n` +
|
|
66
66
|
"SharePoint-backed slides and images now download with no PowerPoint window open.\n",
|
|
67
67
|
);
|
|
68
|
-
|
|
68
|
+
// Only the environments that actually still lack a Graph session — listing a
|
|
69
|
+
// backend the user signed in to earlier reads as "your sign-in was lost".
|
|
70
|
+
const others = sessions.filter(
|
|
71
|
+
(s) => s.backendURL !== target.backendURL && !graph.hasGraphSession(s.backendURL),
|
|
72
|
+
);
|
|
69
73
|
if (others.length > 0) {
|
|
70
74
|
process.stdout.write(
|
|
71
75
|
`Still signed out: ${others.map((s) => s.label).join(", ")} ` +
|
|
@@ -74,19 +78,29 @@ export async function cmdLogin(rest) {
|
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
export async function cmdWhoami() {
|
|
81
|
+
export async function cmdWhoami(rest = []) {
|
|
82
|
+
const asJson = rest.includes("--json");
|
|
83
|
+
const stray = rest.find((a) => a.startsWith("-") && a !== "--json");
|
|
84
|
+
if (stray) fail(`unexpected argument: ${stray}\nusage: slidemind whoami [--json]`);
|
|
78
85
|
const cfg = await loadBridgeModule("config");
|
|
79
86
|
const sessions = cfg.readSessions();
|
|
80
87
|
if (sessions.length === 0) {
|
|
88
|
+
// An agent asked a question and must get an answer it can parse — "no
|
|
89
|
+
// environments" is one, a bare stderr line is not.
|
|
90
|
+
if (asJson) {
|
|
91
|
+
process.stdout.write(`${JSON.stringify({ environments: [] }, null, 2)}\n`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
81
94
|
process.stderr.write(`${NOT_CONNECTED_LINE}\n`);
|
|
82
95
|
return;
|
|
83
96
|
}
|
|
84
97
|
const { httpJson } = await loadBridgeModule("http-json");
|
|
85
98
|
const graph = await loadBridgeModule("graph-auth");
|
|
86
99
|
const def = cfg.getDefaultBackend();
|
|
87
|
-
const
|
|
100
|
+
const envs = await Promise.all(
|
|
88
101
|
sessions.map(async (s) => {
|
|
89
|
-
|
|
102
|
+
let oid = null;
|
|
103
|
+
let tid = null;
|
|
90
104
|
let detail;
|
|
91
105
|
try {
|
|
92
106
|
const res = await httpJson(
|
|
@@ -94,21 +108,50 @@ export async function cmdWhoami() {
|
|
|
94
108
|
`${s.backendURL}/api/cli/whoami`,
|
|
95
109
|
{ token: s.sessionToken },
|
|
96
110
|
);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
111
|
+
if (res.status === 200 && res.json) {
|
|
112
|
+
oid = res.json.oid ?? null;
|
|
113
|
+
tid = res.json.tid ?? null;
|
|
114
|
+
detail = `user ${oid} · tenant ${tid ?? "(n/a)"}`;
|
|
115
|
+
} else {
|
|
116
|
+
detail = `session invalid (${res.status}) — authorize again`;
|
|
117
|
+
}
|
|
101
118
|
} catch {
|
|
102
119
|
detail = "unreachable — check your connection or authorize again";
|
|
103
120
|
}
|
|
104
121
|
// Storage-only — no probe. The Graph sign-in is a SECOND identity, so it
|
|
105
|
-
// is reported next to the session rather than folded into it
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
// is reported next to the session rather than folded into it, and the
|
|
123
|
+
// account name is whatever `login` wrote down at sign-in (records written
|
|
124
|
+
// before that field existed are signed in with no name to show).
|
|
125
|
+
return {
|
|
126
|
+
label: s.label,
|
|
127
|
+
default: s.backendURL === def,
|
|
128
|
+
oid,
|
|
129
|
+
tid,
|
|
130
|
+
microsoft365: {
|
|
131
|
+
signedIn: graph.hasGraphSession(s.backendURL),
|
|
132
|
+
username: s.msGraphUsername ?? null,
|
|
133
|
+
},
|
|
134
|
+
detail,
|
|
135
|
+
};
|
|
110
136
|
}),
|
|
111
137
|
);
|
|
138
|
+
if (asJson) {
|
|
139
|
+
process.stdout.write(
|
|
140
|
+
`${JSON.stringify(
|
|
141
|
+
{ environments: envs.map(({ detail, ...env }) => env) },
|
|
142
|
+
null,
|
|
143
|
+
2,
|
|
144
|
+
)}\n`,
|
|
145
|
+
);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const lines = envs.map((env) => {
|
|
149
|
+
const marker = env.default ? "*" : " ";
|
|
150
|
+
const graphLine = env.microsoft365.signedIn
|
|
151
|
+
? `Microsoft 365: signed in${env.microsoft365.username ? ` as ${env.microsoft365.username}` : ""}`
|
|
152
|
+
: "Microsoft 365: not signed in — needed only for window-free SharePoint downloads (`slidemind login`)";
|
|
153
|
+
return `${marker} ${env.label}\n ${env.detail}\n ${graphLine}`;
|
|
154
|
+
});
|
|
112
155
|
process.stdout.write(
|
|
113
156
|
`Connected environments (${sessions.length}):\n${lines.join("\n")}\n`,
|
|
114
157
|
);
|
package/src/cli/setup.mjs
CHANGED
|
@@ -174,7 +174,16 @@ export async function cmdSetup(rest) {
|
|
|
174
174
|
bundleMod.clearBundleCache();
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
// --- 4.
|
|
177
|
+
// --- 4. the workspace: running now, and at every login -------------------
|
|
178
|
+
// Where the user answers the agent's questions (doc 21). It must be running
|
|
179
|
+
// BEFORE an agent asks: inside Codex's sandbox an agent can neither start it
|
|
180
|
+
// nor keep a detached process, so this unsandboxed moment — the user pasting
|
|
181
|
+
// the setup line — is when it starts, and the login item keeps it there.
|
|
182
|
+
// Best-effort: a failure here never fails setup (any unsandboxed CLI run
|
|
183
|
+
// starts it again). SLIDEMIND_NO_AUTOSTART=1 leaves it alone (tests, CI).
|
|
184
|
+
const workspace = await setUpWorkspace({ ui, globalScript: installed });
|
|
185
|
+
|
|
186
|
+
// --- 5. Microsoft 365 sign-in (best-effort) -----------------------------
|
|
178
187
|
// The pasted-claim moment is the one time the user is reliably at the
|
|
179
188
|
// keyboard in consent headspace, so offer the browser sign-in NOW and no
|
|
180
189
|
// later command has to interrupt a task for it. Strictly best-effort and
|
|
@@ -238,6 +247,7 @@ export async function cmdSetup(rest) {
|
|
|
238
247
|
// Present-but-plugin-served harnesses, so an agent can tell "nothing
|
|
239
248
|
// deployed because nothing is installed" from "nothing needed".
|
|
240
249
|
skippedPlugins,
|
|
250
|
+
workspace,
|
|
241
251
|
nextAction: "slidemind ready",
|
|
242
252
|
// Only on the unhappy path: the session IS saved, so `ok` stays true,
|
|
243
253
|
// but an agent needs to know the binary did not land.
|
|
@@ -271,7 +281,7 @@ export async function cmdSetup(rest) {
|
|
|
271
281
|
return;
|
|
272
282
|
}
|
|
273
283
|
|
|
274
|
-
// ---
|
|
284
|
+
// --- 6. finish the session the user actually came for -------------------
|
|
275
285
|
// `ready` exits non-zero for every legitimate not-yet blocker (PowerPoint
|
|
276
286
|
// closed, no design activated). Setup already succeeded at that point, so its
|
|
277
287
|
// blocker is information, not a failure — run it for the message and the
|
|
@@ -280,3 +290,37 @@ export async function cmdSetup(rest) {
|
|
|
280
290
|
await spawnStatus(process.execPath, [BIN_PATH, "ready"]);
|
|
281
291
|
}
|
|
282
292
|
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Start the workspace and register its login item. Resolves `{ running, url,
|
|
296
|
+
* atLogin }` for the --json payload; never throws.
|
|
297
|
+
*/
|
|
298
|
+
async function setUpWorkspace({ ui, globalScript }) {
|
|
299
|
+
if (process.env.SLIDEMIND_NO_AUTOSTART === "1") return { running: null, url: null, atLogin: "skipped" };
|
|
300
|
+
await ui.section("workspace", "where you answer your agent");
|
|
301
|
+
const { ensureRunning } = await import("../workspace/lifecycle.mjs");
|
|
302
|
+
const auto = await import("../workspace/autostart.mjs");
|
|
303
|
+
const out = { running: false, url: null, atLogin: "unsupported" };
|
|
304
|
+
// the login item needs paths that outlive npx's cache and a node upgrade
|
|
305
|
+
const scriptPath = auto.stableScriptPath({ running: globalScript ? null : BIN_PATH });
|
|
306
|
+
if (auto.autostartPath()) {
|
|
307
|
+
if (!scriptPath) {
|
|
308
|
+
out.atLogin = "skipped";
|
|
309
|
+
ui.step({ status: "warn", label: "at login", value: "skipped", note: "no permanent slidemind install to start it from" });
|
|
310
|
+
} else {
|
|
311
|
+
const env = { PATH: process.env.PATH, SLIDEMIND_NO_UPDATE_CHECK: "1", ...(process.env.XDG_CONFIG_HOME ? { XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME } : {}) };
|
|
312
|
+
const r = auto.installAutostart({ nodePath: auto.stableNodePath(), scriptPath, env });
|
|
313
|
+
out.atLogin = r.installed ? "registered" : "failed";
|
|
314
|
+
ui.step(r.installed ? { label: "at login", value: "starts by itself" } : { status: "warn", label: "at login", value: "not registered", note: r.reason ?? "" });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const run = await ensureRunning({ note: () => {} });
|
|
319
|
+
out.running = true;
|
|
320
|
+
out.url = run.url;
|
|
321
|
+
ui.step({ label: "workspace", value: "running" });
|
|
322
|
+
} catch (err) {
|
|
323
|
+
ui.step({ status: "warn", label: "workspace", value: "not started", note: err?.message ?? String(err) });
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
package/src/cli/status.mjs
CHANGED
|
@@ -3,21 +3,51 @@
|
|
|
3
3
|
// stand and what is the next command. `ready <deck>` prints it (the skill
|
|
4
4
|
// already names `ready` as the first command after any continuation), so the
|
|
5
5
|
// stage → re-read table that used to live in the skill's prose is a line of
|
|
6
|
-
// output instead. Pure disk: no bridge call
|
|
6
|
+
// output instead. Pure disk: no bridge call (the materialized bundle's layout
|
|
7
|
+
// jobs are read only to tell frame slides from body slides).
|
|
7
8
|
import fs from "node:fs";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import { pathToFileURL } from "node:url";
|
|
10
11
|
import { BUILDERS_DIR } from "./common.mjs";
|
|
12
|
+
import { deckRoot, deckSlug } from "../lib/deck-paths.mjs";
|
|
13
|
+
import { revisionStatus } from "../lib/revision.mjs";
|
|
14
|
+
import { remoteHumanLine, remoteReadiness, savedDestination } from "../lib/remote.mjs";
|
|
15
|
+
import { listTasks } from "../workspace/tasks.mjs";
|
|
16
|
+
import { receiveCommand } from "../workspace/handback.mjs";
|
|
17
|
+
|
|
18
|
+
const GATE_COMMAND = { "plan-approval": "plan", "preview-review": "review" };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The workspace's side of the two gates, read from the deck's task store: an
|
|
22
|
+
* answer the human gave that no agent has received yet (it wins — the gate
|
|
23
|
+
* command hands it over), else a question still waiting for the human. Null
|
|
24
|
+
* when the deck has neither, or no readable store.
|
|
25
|
+
*/
|
|
26
|
+
export function workspaceGate(deckDir) {
|
|
27
|
+
let tasks;
|
|
28
|
+
try {
|
|
29
|
+
tasks = listTasks(deckRoot(deckDir));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const gates = tasks.filter((t) => GATE_COMMAND[t.kind]);
|
|
34
|
+
const unreceived = gates
|
|
35
|
+
.filter((t) => t.status === "completed" && !t.pickedUpAt)
|
|
36
|
+
.sort((a, b) => String(b.response?.at ?? "").localeCompare(String(a.response?.at ?? "")))[0];
|
|
37
|
+
if (unreceived) return { kind: unreceived.kind, state: "answered", action: unreceived.response?.action ?? null, at: unreceived.response?.at ?? null, taskId: unreceived.id, adapterId: unreceived.adapterId ?? null };
|
|
38
|
+
const pending = gates.find((t) => t.status === "pending");
|
|
39
|
+
return pending ? { kind: pending.kind, state: "asked", action: null, at: pending.createdAt, taskId: pending.id, adapterId: pending.adapterId ?? null } : null;
|
|
40
|
+
}
|
|
11
41
|
|
|
12
42
|
const load = (name) => import(pathToFileURL(path.join(BUILDERS_DIR, name)).href);
|
|
13
43
|
|
|
14
44
|
export async function deckStage(deckDir) {
|
|
15
|
-
const slug =
|
|
16
|
-
const [plan, review, check,
|
|
45
|
+
const slug = deckSlug(deckDir);
|
|
46
|
+
const [plan, review, check, conceptWaves] = await Promise.all([
|
|
17
47
|
load("plan.mjs"),
|
|
18
48
|
load("review.mjs"),
|
|
19
49
|
load("draft-check.mjs"),
|
|
20
|
-
load("
|
|
50
|
+
load("concept-waves.mjs"),
|
|
21
51
|
]);
|
|
22
52
|
const slidesDir = path.join(deckDir, "slides");
|
|
23
53
|
const files = fs.existsSync(slidesDir) ? fs.readdirSync(slidesDir).sort() : [];
|
|
@@ -29,7 +59,7 @@ export async function deckStage(deckDir) {
|
|
|
29
59
|
const planPath = path.join(deckDir, "plan.md");
|
|
30
60
|
const planExists = fs.existsSync(planPath);
|
|
31
61
|
const planMarkdown = planExists ? fs.readFileSync(planPath, "utf8") : "";
|
|
32
|
-
const planned = planExists ? [...
|
|
62
|
+
const planned = planExists ? [...conceptWaves.plannedSlideNumbers(planMarkdown)].sort((a, b) => a - b) : [];
|
|
33
63
|
const planState = plan.loadPlanState(deckDir);
|
|
34
64
|
const planApproved = planExists && plan.requirePlanApproved(deckDir) === null;
|
|
35
65
|
|
|
@@ -37,82 +67,161 @@ export async function deckStage(deckDir) {
|
|
|
37
67
|
const concepts = fs.existsSync(conceptsDir)
|
|
38
68
|
? fs.readdirSync(conceptsDir).filter((f) => /^concept-slide-\d+\.png$/.test(f)).length
|
|
39
69
|
: 0;
|
|
40
|
-
const assets = check.assetPlanSummary(deckDir);
|
|
41
70
|
|
|
42
71
|
const record = check.loadDraftCheck(deckDir);
|
|
43
72
|
const draftFresh =
|
|
44
73
|
Boolean(record?.slides) &&
|
|
45
74
|
jsx.every((f) => record.slides[f] && record.slides[f].hash === check.hashFile(path.join(slidesDir, f)));
|
|
46
75
|
const standing = check.draftCheckBlocker(deckDir) ? Object.values(record?.slides ?? {}).reduce((n, e) => n + (e.defects?.length ?? 0), 0) : 0;
|
|
47
|
-
const selfCheck = check.verdictBlocker(deckDir) ? "incomplete" : "ok";
|
|
48
76
|
|
|
49
77
|
const reviewState = review.loadReviewState(deckDir);
|
|
78
|
+
// Passed once is passed: a deck edited after its approval goes to deliver,
|
|
79
|
+
// never back to the review gate.
|
|
50
80
|
const reviewCurrent = review.requireReviewApproved(deckDir) === null;
|
|
51
81
|
|
|
52
|
-
|
|
82
|
+
// An ADOPTED deck already exists. It has no storyboard to approve, no preview
|
|
83
|
+
// to approve, nothing to concept and nothing missing to author — its route is
|
|
84
|
+
// edit → draft check → deliver, and its own state is the accepted base, not plan.md.
|
|
85
|
+
const revision = revisionStatus(deckDir);
|
|
86
|
+
const remote = remoteReadiness(deckDir, revisionStatus);
|
|
87
|
+
// A folder the user connected in the workspace before any publish: the next
|
|
88
|
+
// build creates the file there (build-pptx publishes a connected deck itself).
|
|
89
|
+
const folder = remote ? null : savedDestination(deckDir);
|
|
90
|
+
const intoFolder = folder ? `creates ${folder.name} in the SharePoint folder the user connected` : null;
|
|
91
|
+
const editRoute = plan.isEditRouteDeck(deckDir);
|
|
92
|
+
|
|
93
|
+
const missing = editRoute ? [] : planned.filter((n) => !authored.has(n));
|
|
94
|
+
// Body slides owing their content draft, before any concept or slide exists.
|
|
95
|
+
const owed = !editRoute && planApproved && concepts === 0 && authored.size === 0 ? plan.slidesWithoutContent(deckDir, planMarkdown) : [];
|
|
53
96
|
let name;
|
|
54
97
|
let next;
|
|
55
98
|
// No plan.md and no slides: nothing exists yet. No plan.md but slides: an
|
|
56
99
|
// adopted or hand-made deck on the edit route — no plan, no concepts.
|
|
57
|
-
if (
|
|
100
|
+
if (editRoute && revision.conflicts > 0) {
|
|
101
|
+
name = "reconcile";
|
|
102
|
+
next = `\`slidemind rebase ${slug} --dry-run\` to see the ${revision.conflicts} open conflict(s), then resolve them`;
|
|
103
|
+
} else if (editRoute && (!draftFresh || standing > 0)) {
|
|
104
|
+
name = "draft check";
|
|
105
|
+
next = revision.localDirty
|
|
106
|
+
? `\`slidemind view-draft ${slug}\` (${revision.dirtySlides.length} slide(s) changed since the accepted base)`
|
|
107
|
+
: `\`slidemind view-draft ${slug}\``;
|
|
108
|
+
} else if (editRoute) {
|
|
109
|
+
name = "deliver";
|
|
110
|
+
// On a remote-bound deck `revision accept` REFUSES (only a confirmed publish
|
|
111
|
+
// receipt may move that base), so naming it here would send the agent into a
|
|
112
|
+
// guaranteed error. Publishing is the acceptance door there, and a build
|
|
113
|
+
// walks through it itself; a candidate still standing is one whose publish
|
|
114
|
+
// did not save (or a `--no-publish` build).
|
|
115
|
+
next = remote
|
|
116
|
+
? revision.candidate && !revision.candidate.stale
|
|
117
|
+
? `\`slidemind publish ${slug}\` — it reconciles first and replaces ${remote.name ?? "the bound file"} only if nobody saved meanwhile`
|
|
118
|
+
: `\`slidemind build-pptx ${slug}\` — it publishes to ${remote.name ?? "the bound file"} too`
|
|
119
|
+
: folder
|
|
120
|
+
? revision.candidate && !revision.candidate.stale
|
|
121
|
+
? `\`slidemind publish ${slug}\` — it ${intoFolder}`
|
|
122
|
+
: `\`slidemind build-pptx ${slug}\` — it publishes too: ${intoFolder}`
|
|
123
|
+
: revision.candidate && !revision.candidate.stale
|
|
124
|
+
? `\`slidemind revision accept ${slug} --candidate ${revision.candidate.candidateId}\` to make that build the deck's base, or \`slidemind push ${slug}\``
|
|
125
|
+
: `\`slidemind build-pptx ${slug}\` (writes a candidate; accepting it is a separate, deliberate step)`;
|
|
126
|
+
} else if (authored.size === 0 && (!planExists || planned.length === 0)) {
|
|
58
127
|
name = "intake";
|
|
59
128
|
next = `write ${slug}/plan.md (## Red thread + ## Per-slide storyboards), then \`slidemind plan ${slug}\``;
|
|
60
129
|
} else if (planExists && !planApproved) {
|
|
61
130
|
name = "plan gate";
|
|
62
131
|
next = planState.decision === "comments"
|
|
63
|
-
? `apply the comments in ${slug}/plan-review.json to plan.md, then \`slidemind plan ${slug}\``
|
|
132
|
+
? `apply the comments in ${slug}/.slidemind/state/plan-review.json to plan.md, then \`slidemind plan ${slug}\``
|
|
64
133
|
: `\`slidemind plan ${slug}\` (wait for DECISION: approved)`;
|
|
134
|
+
} else if (owed.length > 0) {
|
|
135
|
+
// A deck continued from an approved plan never saw the approval's "Next:"
|
|
136
|
+
// line; without this stage it was sent straight into the concepts refusal.
|
|
137
|
+
name = "content draft";
|
|
138
|
+
next = `write ${plan.CONTENT_DRAFT} in plan.md (slide${owed.length === 1 ? "" : "s"} ${owed.join(", ")}; no new sign-off needed), then \`slidemind concepts ${slug}\``;
|
|
65
139
|
} else if (planExists && concepts === 0 && authored.size === 0) {
|
|
66
140
|
name = "concepts";
|
|
67
|
-
next = `\`slidemind concepts ${slug}\`, then
|
|
68
|
-
} else if (assets && (assets.unfilled > 0 || assets.iconsResolved < assets.icons) && missing.length > 0) {
|
|
69
|
-
name = "sourcing";
|
|
70
|
-
next = assets.unfilled > 0
|
|
71
|
-
? `fill the regions/organisms/connectors/icons rows in ${slug}/asset-plan.md from the concept PNGs (${assets.unfilled} slide section${assets.unfilled === 1 ? "" : "s"} still empty)`
|
|
72
|
-
: `search + download the ${assets.icons - assets.iconsResolved} unresolved icon row${assets.icons - assets.iconsResolved === 1 ? "" : "s"} in ${slug}/asset-plan.md (\`slidemind search\` → \`download-result\`), append each file to its row`;
|
|
141
|
+
next = `\`slidemind concepts ${slug}\`, then \`slidemind transcribe ${slug}\` and compare concept/draft pairs`;
|
|
73
142
|
} else if (missing.length > 0) {
|
|
74
143
|
name = "authoring";
|
|
75
|
-
next =
|
|
76
|
-
} else if (!draftFresh || standing > 0
|
|
144
|
+
next = `\`slidemind transcribe ${slug}\`, complete frame or missing-spec slides from the plan, then \`slidemind view-draft ${slug}\` and compare concept/draft pairs`;
|
|
145
|
+
} else if (!draftFresh || standing > 0) {
|
|
77
146
|
name = "draft check";
|
|
78
147
|
next = !draftFresh
|
|
79
148
|
? `\`slidemind view-draft ${slug}\` (slides changed since the last check${record ? "" : ", or never checked"})`
|
|
80
|
-
: standing
|
|
81
|
-
? `fix the ${standing} measured defect${standing === 1 ? "" : "s"} in the JSX, then \`slidemind view-draft ${slug}\``
|
|
82
|
-
: `tick every organism under \`draft-check:\` in ${slug}/asset-plan.md (concept vs .draft-preview pair), then \`slidemind review ${slug}\``;
|
|
149
|
+
: `fix the ${standing} measured defect${standing === 1 ? "" : "s"} in the JSX, then \`slidemind view-draft ${slug}\``;
|
|
83
150
|
} else if (!reviewCurrent) {
|
|
84
151
|
name = "review";
|
|
85
152
|
next = reviewState.decision === "comments"
|
|
86
|
-
? `edit the files cited in ${slug}/review.json, \`slidemind view-draft ${slug}\`, then \`slidemind review ${slug}\``
|
|
153
|
+
? `edit the files cited in ${slug}/.slidemind/state/review.json, \`slidemind view-draft ${slug}\`, then \`slidemind review ${slug}\``
|
|
87
154
|
: `\`slidemind review ${slug}\` (wait for DECISION: approved)`;
|
|
88
155
|
} else {
|
|
89
156
|
name = "deliver";
|
|
90
|
-
|
|
157
|
+
// A deck bound to a SharePoint item has ONE delivery: the file it came from.
|
|
158
|
+
// Naming push/build-pptx there would send the agent to produce a local .pptx
|
|
159
|
+
// nobody is waiting for while the colleague's copy stays untouched.
|
|
160
|
+
next = remote
|
|
161
|
+
? `\`slidemind build-pptx ${slug}\` — it publishes too: pulls, reconciles and replaces ${remote.name ?? "the bound file"} only if nobody saved meanwhile; then stop`
|
|
162
|
+
: folder
|
|
163
|
+
? `\`slidemind build-pptx ${slug}\` — it publishes too: ${intoFolder}; then stop`
|
|
164
|
+
: `\`slidemind push ${slug}\` (into PowerPoint) or \`slidemind build-pptx ${slug}\` (to a file) — whichever target was agreed at intake; then stop`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The human answered in the workspace: whatever the stage, the next step is the
|
|
168
|
+
// command that receives their answer (the one their pasted message names). A
|
|
169
|
+
// question still open keeps its stage: the agent's turn ends with its SAY line.
|
|
170
|
+
const gate = workspaceGate(deckDir);
|
|
171
|
+
if (gate?.state === "answered") {
|
|
172
|
+
const what = gate.action === "approve" ? "approved" : "sent comments";
|
|
173
|
+
const receive = receiveCommand({ kind: gate.kind, adapterId: gate.adapterId, deckPath: deckRoot(deckDir), taskId: gate.taskId });
|
|
174
|
+
next = `\`${receive}\` — the user ${what} on the ${gate.kind === "plan-approval" ? "plan" : "preview"} in the workspace; it prints their answer`;
|
|
175
|
+
} else if (gate?.state === "asked" && ((gate.kind === "plan-approval" && name === "plan gate") || (gate.kind === "preview-review" && name === "review"))) {
|
|
176
|
+
next = `your question is waiting for the user in the workspace — \`slidemind ${GATE_COMMAND[gate.kind]} ${slug}\` prints its link and SAY line again; end your turn with that line, do not wait`;
|
|
91
177
|
}
|
|
92
178
|
|
|
93
179
|
return {
|
|
94
180
|
name,
|
|
95
181
|
next,
|
|
182
|
+
...(gate ? { gate } : {}),
|
|
96
183
|
plan: { exists: planExists, approved: planApproved, decision: planState.decision ?? null, planned: planned.length },
|
|
97
184
|
concepts,
|
|
98
|
-
assets,
|
|
99
185
|
slides: { jsx: jsx.length, native: native.length, missing },
|
|
100
|
-
draftCheck: { ran: Boolean(record), fresh: draftFresh, standingDefects: standing
|
|
101
|
-
|
|
186
|
+
draftCheck: { ran: Boolean(record), fresh: draftFresh, standingDefects: standing },
|
|
187
|
+
// current: delivery may proceed; approved: a human actually approved (an
|
|
188
|
+
// adopted deck is current without ever being approved — it needs no review)
|
|
189
|
+
review: { decision: reviewState.decision ?? null, current: reviewCurrent, approved: Boolean(reviewState.approval) },
|
|
190
|
+
route: editRoute ? "edit" : "author",
|
|
191
|
+
// The accepted base, what has changed against it, the candidate a build
|
|
192
|
+
// proposed and anything staged or conflicted — the whole local revision
|
|
193
|
+
// picture, in the one payload an agent already reads after a summarization.
|
|
194
|
+
revision,
|
|
195
|
+
// The remote half, read from disk. Null for a deck that publishes nowhere.
|
|
196
|
+
// No network call: `ready` must not become a SharePoint round trip, so this
|
|
197
|
+
// reports the last SUCCESSFUL observation and when it happened rather than
|
|
198
|
+
// claiming the deck is current.
|
|
199
|
+
...(remote ? { remote } : {}),
|
|
102
200
|
};
|
|
103
201
|
}
|
|
104
202
|
|
|
105
203
|
export function stageHumanLines(stage) {
|
|
106
|
-
const a = stage.assets;
|
|
107
204
|
const parts = [
|
|
108
205
|
`plan: ${!stage.plan.exists ? "none" : stage.plan.approved ? "approved" : stage.plan.decision ?? "not gated"}`,
|
|
109
206
|
`concepts: ${stage.concepts}/${stage.plan.planned}`,
|
|
110
|
-
a
|
|
111
|
-
? `asset plan: ${a.unfilled > 0 ? `${a.unfilled} section${a.unfilled === 1 ? "" : "s"} empty` : `${a.iconsResolved}/${a.icons} icon rows resolved`}`
|
|
112
|
-
: "asset plan: none",
|
|
113
207
|
`slides: ${stage.slides.jsx + stage.slides.native}/${stage.plan.planned}${stage.slides.native ? ` (${stage.slides.native} native)` : ""}`,
|
|
114
|
-
`draft check: ${!stage.draftCheck.ran ? "not run" : !stage.draftCheck.fresh ? "stale" : stage.draftCheck.standingDefects > 0 ? `${stage.draftCheck.standingDefects} defect${stage.draftCheck.standingDefects === 1 ? "" : "s"} standing` :
|
|
115
|
-
`review: ${stage.review.
|
|
208
|
+
`draft check: ${!stage.draftCheck.ran ? "not run" : !stage.draftCheck.fresh ? "stale" : stage.draftCheck.standingDefects > 0 ? `${stage.draftCheck.standingDefects} defect${stage.draftCheck.standingDefects === 1 ? "" : "s"} standing` : "clean"}`,
|
|
209
|
+
`review: ${stage.review.approved ? "approved" : stage.review.current ? "not needed" : stage.review.decision ?? "none"}`,
|
|
116
210
|
];
|
|
117
|
-
|
|
211
|
+
// An adopted deck's plan/concepts lines say nothing — it has none.
|
|
212
|
+
// What matters there is what changed since the file it was adopted from.
|
|
213
|
+
const r = stage.revision;
|
|
214
|
+
const lines = stage.route === "edit"
|
|
215
|
+
? [
|
|
216
|
+
`base: ${r.base ? `${r.base.sha256.slice(0, 8)} (${r.base.slideCount} slides, gen ${r.generation})` : "none"}`,
|
|
217
|
+
`local: ${r.localDirty ? `${r.dirtySlides.length} slide(s) changed` : "clean"}${r.designMoved ? " · design moved (build-pptx --refresh-design)" : ""}`,
|
|
218
|
+
`slides: ${stage.slides.jsx} jsx${stage.slides.native ? ` + ${stage.slides.native} native` : ""}`,
|
|
219
|
+
`candidate: ${r.candidate ? `${r.candidate.candidateId}${r.candidate.stale ? " (stale)" : ""}` : "none"}`,
|
|
220
|
+
`review: ${stage.review.approved ? "approved" : stage.review.current ? "not needed" : stage.review.decision ?? "none"}`,
|
|
221
|
+
...(r.staged.length ? [`staged: ${r.staged.length}`] : []),
|
|
222
|
+
...(r.conflicts ? [`conflicts: ${r.conflicts}`] : []),
|
|
223
|
+
]
|
|
224
|
+
: parts;
|
|
225
|
+
const remote = stage.remote ? ` ${remoteHumanLine(stage.remote).trimStart()}` : "";
|
|
226
|
+
return ` stage: ${stage.name} — ${lines.join(" · ")}\n${remote} next: ${stage.next}\n`;
|
|
118
227
|
}
|