slidemind 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +37 -8
- package/README.md +32 -2
- package/bin/slidemind.mjs +535 -122
- package/install.ps1 +70 -0
- package/install.sh +65 -0
- package/package.json +3 -1
- package/skills/create-slides/SKILL.md +30 -6
- package/skills/search/SKILL.md +3 -1
- package/src/bridge/build-and-push.mjs +418 -38
- package/src/bridge/capture.mjs +12 -1
- package/src/bridge/client.mjs +344 -26
- package/src/bridge/config.mjs +193 -1
- package/src/bridge/graph-auth.mjs +865 -0
- package/src/bridge/http-json.mjs +15 -6
- package/src/bridge/secrets.mjs +75 -0
- package/src/bridge/setup-claim.mjs +6 -0
- package/src/builders/build.mjs +3 -1
- package/src/builders/gate-server.mjs +156 -0
- package/src/builders/gate-session.js +355 -0
- package/src/builders/plan-overlay.js +507 -225
- package/src/builders/plan.mjs +169 -98
- package/src/builders/review-overlay.js +390 -302
- package/src/builders/review.mjs +136 -50
- package/src/lib/adopt.mjs +407 -0
- package/src/lib/conversions.mjs +263 -0
- package/src/lib/overwrite-guard.mjs +91 -0
- package/src/lib/reconcile-ack.mjs +46 -0
- package/src/lib/reconcile.mjs +306 -0
- package/src/lib/slide-guids.mjs +143 -0
- package/src/pptx/fill/engine.mjs +69 -1
- package/src/pptx/hidden-ole.mjs +22 -1
- package/src/pptx/slide-swap.mjs +23 -1
- package/src/pptx/tags.mjs +676 -0
package/src/bridge/client.mjs
CHANGED
|
@@ -16,9 +16,16 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
|
|
17
17
|
import {
|
|
18
18
|
deriveEnvLabel,
|
|
19
|
+
envPhrase,
|
|
19
20
|
getSessionFor,
|
|
21
|
+
isMultiEnv,
|
|
20
22
|
readSessions,
|
|
21
23
|
} from "./config.mjs";
|
|
24
|
+
import {
|
|
25
|
+
ensureSignedIn,
|
|
26
|
+
getGraphToken,
|
|
27
|
+
getSharePointToken,
|
|
28
|
+
} from "./graph-auth.mjs";
|
|
22
29
|
import { updateCliInstruction } from "../install-channel.mjs";
|
|
23
30
|
|
|
24
31
|
// This CLI's own version, for the add-in's minCliVersion floor (see
|
|
@@ -254,7 +261,7 @@ async function bridgeFetch(pathPart, init = {}, backendURL = null) {
|
|
|
254
261
|
} catch (err) {
|
|
255
262
|
if (err && /ECONNREFUSED|ECONNRESET|ENOTFOUND|self-signed|fetch failed/i.test(String(err.message || err))) {
|
|
256
263
|
throw new Error(
|
|
257
|
-
`slidemind relay unreachable
|
|
264
|
+
`slidemind relay unreachable${envPhrase(deriveEnvLabel(session.backendURL), "on")}. Check your connection; ` +
|
|
258
265
|
"if it persists, reconnect from the add-in's Connect CLI panel.",
|
|
259
266
|
);
|
|
260
267
|
}
|
|
@@ -470,7 +477,7 @@ async function bridgeStreamToFile(pathPart, init, destPath, backendURL = null) {
|
|
|
470
477
|
)
|
|
471
478
|
) {
|
|
472
479
|
throw new Error(
|
|
473
|
-
`slidemind relay unreachable
|
|
480
|
+
`slidemind relay unreachable${envPhrase(deriveEnvLabel(session.backendURL), "on")}. Check your connection; ` +
|
|
474
481
|
"if it persists, reconnect from the add-in's Connect CLI panel.",
|
|
475
482
|
);
|
|
476
483
|
}
|
|
@@ -1258,6 +1265,144 @@ export async function generateImageForBrand(
|
|
|
1258
1265
|
throw err;
|
|
1259
1266
|
}
|
|
1260
1267
|
|
|
1268
|
+
// Deterministic PPTX→JSX conversion is a per-slide OOXML parse + a render pass
|
|
1269
|
+
// per converted slide — a whole foreign deck runs in render-grade time, not
|
|
1270
|
+
// list-grade. Budget like the render routes.
|
|
1271
|
+
function convertTimeoutMs() {
|
|
1272
|
+
return (
|
|
1273
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_CONVERT_TIMEOUT_MS || "300000", 10) ||
|
|
1274
|
+
300000
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Minimal multipart/form-data body. The route takes an uploaded PPTX, and
|
|
1279
|
+
// nodeRequest writes a Buffer verbatim — no fetch/FormData involved, so the
|
|
1280
|
+
// envelope is assembled here (fields first, file last, boundary never appearing
|
|
1281
|
+
// in the parts because it carries a random suffix).
|
|
1282
|
+
function multipartBody(fields, file) {
|
|
1283
|
+
const boundary = `----slidemind${randomUUID().replace(/-/g, "")}`;
|
|
1284
|
+
const chunks = [];
|
|
1285
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
1286
|
+
if (value === null || value === undefined) continue;
|
|
1287
|
+
chunks.push(
|
|
1288
|
+
Buffer.from(
|
|
1289
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`,
|
|
1290
|
+
),
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
// The file name reaches this from a path on disk; a quote or a newline in it
|
|
1294
|
+
// would break out of the Content-Disposition header and forge a part.
|
|
1295
|
+
const safeFileName = String(file.fileName).replace(/[\r\n"\\]/g, "_");
|
|
1296
|
+
chunks.push(
|
|
1297
|
+
Buffer.from(
|
|
1298
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="${file.name}"; ` +
|
|
1299
|
+
`filename="${safeFileName}"\r\nContent-Type: ${file.contentType}\r\n\r\n`,
|
|
1300
|
+
),
|
|
1301
|
+
Buffer.from(file.bytes),
|
|
1302
|
+
Buffer.from(`\r\n--${boundary}--\r\n`),
|
|
1303
|
+
);
|
|
1304
|
+
return {
|
|
1305
|
+
body: Buffer.concat(chunks),
|
|
1306
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Whether a backend deterministically converts foreign PPTX slides to jsx
|
|
1311
|
+
// (`POST /api/cli/brands/:brandId/convert-pptx`). Advertised only by add-ins
|
|
1312
|
+
// running with the reverse-conversion flag on; everywhere else `slidemind
|
|
1313
|
+
// adopt` folds untagged slides in as native .pptx exactly as before.
|
|
1314
|
+
export async function backendSupportsConvertPptx(backendURL) {
|
|
1315
|
+
const caps = await backendCapabilities(backendURL);
|
|
1316
|
+
return caps?.convertPptx === true;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
async function postConvertPptx(brandId, { bytes, fileName, slides }, backendURL) {
|
|
1320
|
+
const { body, contentType } = multipartBody(
|
|
1321
|
+
{
|
|
1322
|
+
// 1-based play-order slide numbers to convert. Omitted = the whole deck;
|
|
1323
|
+
// `adopt` always names the untagged ones so tagged slides are not re-parsed.
|
|
1324
|
+
slides: Array.isArray(slides) && slides.length > 0 ? slides.join(",") : null,
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
name: "file",
|
|
1328
|
+
fileName: path.basename(fileName || "deck.pptx"),
|
|
1329
|
+
contentType:
|
|
1330
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1331
|
+
bytes,
|
|
1332
|
+
},
|
|
1333
|
+
);
|
|
1334
|
+
const res = await bridgeFetch(
|
|
1335
|
+
`/api/cli/brands/${encodeURIComponent(brandId)}/convert-pptx`,
|
|
1336
|
+
{
|
|
1337
|
+
method: "POST",
|
|
1338
|
+
headers: { "content-type": contentType },
|
|
1339
|
+
body,
|
|
1340
|
+
timeoutMs: convertTimeoutMs(),
|
|
1341
|
+
},
|
|
1342
|
+
backendURL,
|
|
1343
|
+
);
|
|
1344
|
+
const data = (await res.json()) ?? {};
|
|
1345
|
+
return { ...data, backendURL };
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Convert a FILE deck's foreign slides to converted-dialect jsx over the
|
|
1349
|
+
// pane-free brand route (`slidemind adopt`). Body is multipart: the .pptx plus
|
|
1350
|
+
// the 1-based slide numbers to convert. Returns the server's
|
|
1351
|
+
// `{ slides, layouts, assets, sourceBaselines, conversionEvidence, skipped,
|
|
1352
|
+
// warnings }` (add-in `ConvertPptxResponse`) augmented with the resolved
|
|
1353
|
+
// `backendURL`. Identity and evidence are keyed by each slide's jsx `path`, not
|
|
1354
|
+
// carried on the slide — `src/lib/adopt.mjs` `convertedByNumber()` is the one
|
|
1355
|
+
// place that key shape is unfolded.
|
|
1356
|
+
//
|
|
1357
|
+
// LIVE decks are NOT convertible through this route in v1: converting an open
|
|
1358
|
+
// presentation needs a slide export + tag read over the relay, and neither
|
|
1359
|
+
// event exists (design D9) — the add-in path owns that flow.
|
|
1360
|
+
//
|
|
1361
|
+
// Fan-out and degradation mirror generateConceptImagesForBrand: throws a typed
|
|
1362
|
+
// `convert_route_unavailable` when no paired backend serves the route (older
|
|
1363
|
+
// add-ins, the reverse flag off, unknown brand, missing grant) so `adopt` falls
|
|
1364
|
+
// back to the native fold instead of failing.
|
|
1365
|
+
export async function convertPptxForBrand(
|
|
1366
|
+
brandId,
|
|
1367
|
+
{ bytes, fileName = "deck.pptx", slides = null, backendURL = null } = {},
|
|
1368
|
+
) {
|
|
1369
|
+
if (!brandId) throw new Error("convertPptxForBrand: brandId is required");
|
|
1370
|
+
if (!bytes) throw new Error("convertPptxForBrand: bytes is required");
|
|
1371
|
+
if (backendURL) {
|
|
1372
|
+
return postConvertPptx(brandId, { bytes, fileName, slides }, backendURL);
|
|
1373
|
+
}
|
|
1374
|
+
const sessions = sessionsInScope();
|
|
1375
|
+
const failures = [];
|
|
1376
|
+
for (const s of sessions) {
|
|
1377
|
+
try {
|
|
1378
|
+
if (!(await backendSupportsConvertPptx(s.backendURL))) {
|
|
1379
|
+
failures.push({
|
|
1380
|
+
env: deriveEnvLabel(s.backendURL),
|
|
1381
|
+
reason: "no convert-pptx route",
|
|
1382
|
+
});
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
return await postConvertPptx(
|
|
1386
|
+
brandId,
|
|
1387
|
+
{ bytes, fileName, slides },
|
|
1388
|
+
s.backendURL,
|
|
1389
|
+
);
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
failures.push({
|
|
1392
|
+
env: deriveEnvLabel(s.backendURL),
|
|
1393
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
const detail = failures.map((f) => `${f.env}: ${f.reason}`).join("; ");
|
|
1398
|
+
const err = new Error(
|
|
1399
|
+
`no paired backend converts foreign slides for design '${brandId}'` +
|
|
1400
|
+
`${detail ? ` (${detail})` : ""}.`,
|
|
1401
|
+
);
|
|
1402
|
+
err.code = "convert_route_unavailable";
|
|
1403
|
+
throw err;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1261
1406
|
// List the LIVE engine catalog (NOT the bundle's curated set) for the paired
|
|
1262
1407
|
// backend(s). `GET /api/cli/engines` is backend-scoped, not presId-scoped, so
|
|
1263
1408
|
// when several environments are paired this fans out and tags each engine with
|
|
@@ -1292,8 +1437,10 @@ async function listEnginesForBackend(backendURL, { type = null, subtype = null }
|
|
|
1292
1437
|
const res = await bridgeFetch(
|
|
1293
1438
|
`/api/cli/engines${qs ? `?${qs}` : ""}`,
|
|
1294
1439
|
// Pane-relayed on the add-in side — needs the engine budget, not the 8s
|
|
1295
|
-
// list timeout (which bailed before the pane could ever answer).
|
|
1296
|
-
|
|
1440
|
+
// list timeout (which bailed before the pane could ever answer). The
|
|
1441
|
+
// delegated tokens authorize the pane-free catalog (login_required
|
|
1442
|
+
// without them); older add-ins ignore the headers.
|
|
1443
|
+
{ timeoutMs: engineTimeoutMs(), headers: await graphHeaders(backendURL) },
|
|
1297
1444
|
backendURL,
|
|
1298
1445
|
);
|
|
1299
1446
|
const data = await res.json();
|
|
@@ -1503,6 +1650,16 @@ export async function backendSupportsEngineRoutes(backendURL) {
|
|
|
1503
1650
|
return caps?.engineRoutes === true;
|
|
1504
1651
|
}
|
|
1505
1652
|
|
|
1653
|
+
// Whether a backend honours a delegated Microsoft Graph token on the pane-free
|
|
1654
|
+
// engine routes — the CLI's own identity for SharePoint-backed content
|
|
1655
|
+
// (`slidemind login`, src/bridge/graph-auth.mjs). Gates BOTH the attach and the
|
|
1656
|
+
// remediation text: naming `slidemind login` on an add-in that ignores the token
|
|
1657
|
+
// would send a user through a browser flow that changes nothing.
|
|
1658
|
+
export async function backendSupportsGraphDelegated(backendURL) {
|
|
1659
|
+
const caps = await backendCapabilities(backendURL);
|
|
1660
|
+
return caps?.graphDelegated === true;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1506
1663
|
// The add-in's hard version floor: /api/cli/capabilities advertises the oldest
|
|
1507
1664
|
// CLI it still speaks (`minCliVersion`). An older CLI must refuse to proceed —
|
|
1508
1665
|
// mid-rollout skew otherwise surfaces as confusing route/contract errors deep
|
|
@@ -1698,6 +1855,9 @@ export async function renderPptx({
|
|
|
1698
1855
|
deckAssets = null,
|
|
1699
1856
|
nativeSlides = null,
|
|
1700
1857
|
etag = null,
|
|
1858
|
+
sourceGuids = null,
|
|
1859
|
+
conversions = null,
|
|
1860
|
+
reconcile = null,
|
|
1701
1861
|
backendURL = null,
|
|
1702
1862
|
onProgress = null,
|
|
1703
1863
|
}) {
|
|
@@ -1728,6 +1888,30 @@ export async function renderPptx({
|
|
|
1728
1888
|
// server splices them in by numeric-aware sorted filename (contract #7).
|
|
1729
1889
|
if (hasNative) body.nativeSlides = nativeSlides;
|
|
1730
1890
|
if (etag) body.etag = etag;
|
|
1891
|
+
// Deck-embedded source: known slide GUIDs (jsx path → sm-<uuid>) so the
|
|
1892
|
+
// server's tag bake keeps identities stable; minted ones ride back as
|
|
1893
|
+
// `sourceGuids` on the response.
|
|
1894
|
+
if (sourceGuids && Object.keys(sourceGuids).length > 0) {
|
|
1895
|
+
body.sourceGuids = sourceGuids;
|
|
1896
|
+
}
|
|
1897
|
+
// Reverse-converted slides (`slidemind adopt`): identity + fidelity evidence
|
|
1898
|
+
// per converted jsx file, so the server can gate the first replacement of a
|
|
1899
|
+
// converted slide against what it originally converted, plus the verbatim
|
|
1900
|
+
// source package whose preserved natives (chart / OLE / SmartArt) it splices
|
|
1901
|
+
// back into the rendered slide. Keyed exactly like `slides`. Additive: a deck
|
|
1902
|
+
// with no conversions sends the same body it always did.
|
|
1903
|
+
if (conversions && Object.keys(conversions).length > 0) {
|
|
1904
|
+
body.conversions = conversions;
|
|
1905
|
+
}
|
|
1906
|
+
// Manual-edit reconcile: for every slide the user edited in PowerPoint since
|
|
1907
|
+
// the last build, the baked baseline projection and the current one, keyed
|
|
1908
|
+
// exactly like `slides`. The server diffs them, folds text edits into the jsx
|
|
1909
|
+
// it renders, and reports the rest — the CLI owns no diff source. Additive: a
|
|
1910
|
+
// deck with no pending edits sends the same body it always did, and the
|
|
1911
|
+
// response's `reconcile` echo is what proves the backend understood it.
|
|
1912
|
+
if (reconcile && Object.keys(reconcile).length > 0) {
|
|
1913
|
+
body.reconcile = reconcile;
|
|
1914
|
+
}
|
|
1731
1915
|
// The brand surface has no presId to label the render with — pass the deck
|
|
1732
1916
|
// slug so job records/temp dirs stay readable (server-side sanitized).
|
|
1733
1917
|
if (brandId && deckName) body.deckName = deckName;
|
|
@@ -1962,12 +2146,12 @@ export async function resolveSearchBackend(searchId) {
|
|
|
1962
2146
|
}
|
|
1963
2147
|
}
|
|
1964
2148
|
// The two failures that are NOT the record talking: a dead session, and a
|
|
1965
|
-
//
|
|
1966
|
-
//
|
|
1967
|
-
//
|
|
1968
|
-
//
|
|
2149
|
+
// missing Microsoft 365 sign-in (the record re-read is an engine route, so
|
|
2150
|
+
// it refuses with the rest of corpus search). Reported as "that search
|
|
2151
|
+
// expired", both send the user to re-run a search that fails the same way —
|
|
2152
|
+
// so they outrank the expired-record answer.
|
|
1969
2153
|
const blocking = failures.find(
|
|
1970
|
-
(e) => isSessionInvalidError(e) || e?.code === "
|
|
2154
|
+
(e) => isSessionInvalidError(e) || e?.code === "login_required",
|
|
1971
2155
|
);
|
|
1972
2156
|
if (blocking) throw blocking;
|
|
1973
2157
|
return null;
|
|
@@ -2072,27 +2256,91 @@ async function directEngineBackend(fn, { backendURL = null, brandId = null }) {
|
|
|
2072
2256
|
throw err;
|
|
2073
2257
|
}
|
|
2074
2258
|
|
|
2075
|
-
// The
|
|
2076
|
-
//
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2080
|
-
//
|
|
2081
|
-
|
|
2082
|
-
|
|
2259
|
+
// The user's delegated tokens for a backend, or nulls. Attached ONLY to the
|
|
2260
|
+
// pane-free engine ops: they are the user's own Entra identity, and render,
|
|
2261
|
+
// bundle, whoami and the pane relay have no business carrying them (the pane
|
|
2262
|
+
// signs its own requests with a browser token that is strictly fresher).
|
|
2263
|
+
//
|
|
2264
|
+
// When nothing is stored, the sign-in RUNS HERE — browser opens, the command
|
|
2265
|
+
// blocks until the human finishes (graph-auth.ensureSignedIn, once per
|
|
2266
|
+
// process). So login is not a separate ritual: the first engine command of a
|
|
2267
|
+
// fresh machine walks the user through it, and every later one rides the
|
|
2268
|
+
// stored refresh token silently. Null only when the add-in doesn't honour the
|
|
2269
|
+
// tokens, or the sign-in was refused/abandoned — the backend then answers its
|
|
2270
|
+
// typed `login_required`, which the blocker below renders.
|
|
2271
|
+
async function engineGraphToken(backendURL) {
|
|
2272
|
+
if (!backendURL) return null;
|
|
2273
|
+
if (!(await backendSupportsGraphDelegated(backendURL))) return null;
|
|
2274
|
+
let token = await getGraphToken(backendURL);
|
|
2275
|
+
if (!token && (await ensureSignedIn(backendURL))) {
|
|
2276
|
+
token = await getGraphToken(backendURL);
|
|
2277
|
+
}
|
|
2278
|
+
return token;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
// The SharePoint-resource sibling (same sign-in, same refresh token): the
|
|
2282
|
+
// backend resolves `sg:` site-group membership from the PAIR, and on a
|
|
2283
|
+
// SharePoint-configured deployment refuses corpus search without it.
|
|
2284
|
+
async function engineSpToken(backendURL) {
|
|
2285
|
+
if (!backendURL) return null;
|
|
2286
|
+
if (!(await backendSupportsGraphDelegated(backendURL))) return null;
|
|
2287
|
+
let token = await getSharePointToken(backendURL);
|
|
2288
|
+
if (!token && (await ensureSignedIn(backendURL))) {
|
|
2289
|
+
token = await getSharePointToken(backendURL);
|
|
2290
|
+
}
|
|
2291
|
+
return token;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// Both delegated tokens as body fields, for the POST routes. Empty when there
|
|
2295
|
+
// is nothing to attach, so an older backend sees the body it always did.
|
|
2296
|
+
async function delegatedBodyFields(backendURL) {
|
|
2297
|
+
const [graphToken, spToken] = await Promise.all([
|
|
2298
|
+
engineGraphToken(backendURL),
|
|
2299
|
+
engineSpToken(backendURL),
|
|
2300
|
+
]);
|
|
2301
|
+
return {
|
|
2302
|
+
...(graphToken ? { graphApiToken: graphToken } : {}),
|
|
2303
|
+
...(spToken ? { spApiToken: spToken } : {}),
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// The byte routes are GETs, so the delegated tokens ride headers instead of
|
|
2308
|
+
// body fields. Empty (not absent-valued) when there is no token, so the request
|
|
2309
|
+
// headers are unchanged for everyone who never signed in.
|
|
2310
|
+
async function graphHeaders(backendURL) {
|
|
2311
|
+
const [graphToken, spToken] = await Promise.all([
|
|
2312
|
+
engineGraphToken(backendURL),
|
|
2313
|
+
engineSpToken(backendURL),
|
|
2314
|
+
]);
|
|
2315
|
+
return {
|
|
2316
|
+
...(graphToken ? { "x-slidemind-graph-token": graphToken } : {}),
|
|
2317
|
+
...(spToken ? { "x-slidemind-sp-token": spToken } : {}),
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
// Corpus search runs as the USER: the pane-free engine routes resolve the
|
|
2322
|
+
// caller's group membership live from the delegated tokens `slidemind login`
|
|
2323
|
+
// minted, and answer `401 { code: "login_required", action: "…" }` without
|
|
2324
|
+
// them. Shape it like the connection ladder's rungs — `{ code, actor, message }`
|
|
2325
|
+
// — so the CLI can print ONE actionable line and an agent knows it cannot clear
|
|
2326
|
+
// this one itself (the sign-in needs a browser and a human). Returns null for
|
|
2327
|
+
// any other error.
|
|
2328
|
+
export function loginRequiredBlocker(err) {
|
|
2329
|
+
if (err?.code !== "login_required") return null;
|
|
2083
2330
|
const action =
|
|
2084
2331
|
typeof err?.body?.action === "string" && err.body.action.trim()
|
|
2085
2332
|
? err.body.action.trim()
|
|
2086
|
-
: "
|
|
2333
|
+
: "run `slidemind login` to sign in to Microsoft 365";
|
|
2087
2334
|
return {
|
|
2088
|
-
code: "
|
|
2335
|
+
code: "login_required",
|
|
2089
2336
|
actor: "user",
|
|
2090
2337
|
message:
|
|
2091
|
-
`slidemind needs your
|
|
2338
|
+
`slidemind needs your Microsoft 365 sign-in before it can search the corpus — ${action}. ` +
|
|
2092
2339
|
"Everything else (render, bundle, imagery already downloaded) keeps working.",
|
|
2093
2340
|
};
|
|
2094
2341
|
}
|
|
2095
2342
|
|
|
2343
|
+
|
|
2096
2344
|
// Brand-engine search through the local bridge. New add-ins run this
|
|
2097
2345
|
// server-side with the relayed user token; older add-ins may fall back to a
|
|
2098
2346
|
// task-pane proxy. Returns a searchId + numbered results (image thumbnails, or
|
|
@@ -2136,12 +2384,18 @@ export async function engineSearch({
|
|
|
2136
2384
|
if (typeof top === "number") body.top = top;
|
|
2137
2385
|
if (direct) {
|
|
2138
2386
|
const be = await directEngineBackend("engineSearch", { backendURL, brandId });
|
|
2387
|
+
// The user's own identity: the backend resolves the caller's group
|
|
2388
|
+
// membership from these tokens per request and refuses without them
|
|
2389
|
+
// (login_required). Search also seeds the speculative insert cache
|
|
2390
|
+
// server-side, which is why it carries the Graph token as well as the
|
|
2391
|
+
// byte GETs.
|
|
2392
|
+
const delegated = await delegatedBodyFields(be);
|
|
2139
2393
|
const res = await bridgeFetch(
|
|
2140
2394
|
"/api/cli/searches",
|
|
2141
2395
|
{
|
|
2142
2396
|
method: "POST",
|
|
2143
2397
|
headers: { "content-type": "application/json" },
|
|
2144
|
-
body: JSON.stringify(body),
|
|
2398
|
+
body: JSON.stringify({ ...body, ...delegated }),
|
|
2145
2399
|
timeoutMs: timeoutMs ?? engineTimeoutMs(),
|
|
2146
2400
|
},
|
|
2147
2401
|
be,
|
|
@@ -2359,7 +2613,12 @@ export async function engineDownloadToFile({
|
|
|
2359
2613
|
if (iconColor) params.set("iconColor", iconColor);
|
|
2360
2614
|
const res = await bridgeStreamToFile(
|
|
2361
2615
|
resultRoute(searchId, selection, "image", params),
|
|
2362
|
-
{
|
|
2616
|
+
{
|
|
2617
|
+
method: "GET",
|
|
2618
|
+
headers: await graphHeaders(b),
|
|
2619
|
+
timeoutMs: downloadTimeoutMs(),
|
|
2620
|
+
onProgress,
|
|
2621
|
+
},
|
|
2363
2622
|
destPath,
|
|
2364
2623
|
b,
|
|
2365
2624
|
);
|
|
@@ -2425,7 +2684,12 @@ export async function extractSlideToFile({
|
|
|
2425
2684
|
});
|
|
2426
2685
|
const res = await bridgeStreamToFile(
|
|
2427
2686
|
resultRoute(searchId, selection, "slide"),
|
|
2428
|
-
{
|
|
2687
|
+
{
|
|
2688
|
+
method: "GET",
|
|
2689
|
+
headers: await graphHeaders(b),
|
|
2690
|
+
timeoutMs: downloadTimeoutMs(),
|
|
2691
|
+
onProgress,
|
|
2692
|
+
},
|
|
2429
2693
|
destPath,
|
|
2430
2694
|
b,
|
|
2431
2695
|
);
|
|
@@ -2657,7 +2921,11 @@ export async function viewSlide({
|
|
|
2657
2921
|
try {
|
|
2658
2922
|
const res = await bridgeStreamToFile(
|
|
2659
2923
|
resultRoute(searchId, selection, "thumbnail"),
|
|
2660
|
-
{
|
|
2924
|
+
{
|
|
2925
|
+
method: "GET",
|
|
2926
|
+
headers: await graphHeaders(b),
|
|
2927
|
+
timeoutMs: engineTimeoutMs(),
|
|
2928
|
+
},
|
|
2661
2929
|
tmp,
|
|
2662
2930
|
b,
|
|
2663
2931
|
);
|
|
@@ -2837,12 +3105,18 @@ export async function insertSlides({
|
|
|
2837
3105
|
insertAt = null,
|
|
2838
3106
|
sourceSlideIds = null,
|
|
2839
3107
|
operationId = null,
|
|
3108
|
+
sourceTagsBySlide = null,
|
|
2840
3109
|
backendURL = null,
|
|
2841
3110
|
}) {
|
|
2842
3111
|
if (!presId) throw new Error("insertSlides: presId is required");
|
|
2843
3112
|
if (!base64) throw new Error("insertSlides: base64 is required");
|
|
2844
3113
|
const be = backendURL || (await resolveBackendForPres(presId));
|
|
2845
3114
|
const body = { base64, insertAt, sourceSlideIds, operationId };
|
|
3115
|
+
// Deck-embedded source payloads for the pane's post-insert tag write,
|
|
3116
|
+
// aligned with the inserted slides in document order.
|
|
3117
|
+
if (Array.isArray(sourceTagsBySlide) && sourceTagsBySlide.some(Boolean)) {
|
|
3118
|
+
body.sourceTagsBySlide = sourceTagsBySlide;
|
|
3119
|
+
}
|
|
2846
3120
|
const res = await bridgeFetch(
|
|
2847
3121
|
`/api/cli/presentations/${encodeURIComponent(presId)}/insert`,
|
|
2848
3122
|
{
|
|
@@ -2858,21 +3132,65 @@ export async function insertSlides({
|
|
|
2858
3132
|
return ensurePaneAckOk(await res.json().catch(() => ({})), "insert");
|
|
2859
3133
|
}
|
|
2860
3134
|
|
|
3135
|
+
// Read-only sibling of the mutation ops: export the LIVE slides carrying the
|
|
3136
|
+
// given GUIDs, so a `--replace` push can see what the user changed on screen
|
|
3137
|
+
// BEFORE it wipes it (plan Phase C1). `guids` omitted ⇒ the whole deck.
|
|
3138
|
+
//
|
|
3139
|
+
// The ack's `tags` come from the Office.js tag API, NOT from the exported
|
|
3140
|
+
// bytes: exportAsBase64 strips custom tags (office-js #6104), so a slide's
|
|
3141
|
+
// drift BASELINE is recoverable only from `tags` — never from `base64`, which
|
|
3142
|
+
// carries the CURRENT state and nothing else. `deck.unmatched` lists the live
|
|
3143
|
+
// slides that are not in the requested set (what a whole-deck replace would
|
|
3144
|
+
// delete); `truncated` names GUIDs that matched but did not fit the relay
|
|
3145
|
+
// frame, to be re-requested.
|
|
3146
|
+
export async function exportSlides({
|
|
3147
|
+
presId,
|
|
3148
|
+
guids = null,
|
|
3149
|
+
presentationId = null,
|
|
3150
|
+
backendURL = null,
|
|
3151
|
+
}) {
|
|
3152
|
+
if (!presId) throw new Error("exportSlides: presId is required");
|
|
3153
|
+
const be = backendURL || (await resolveBackendForPres(presId));
|
|
3154
|
+
const body = {};
|
|
3155
|
+
if (Array.isArray(guids) && guids.length > 0) body.guids = guids;
|
|
3156
|
+
if (presentationId) body.presentationId = presentationId;
|
|
3157
|
+
const res = await bridgeFetch(
|
|
3158
|
+
`/api/cli/presentations/${encodeURIComponent(presId)}/export-slides`,
|
|
3159
|
+
{
|
|
3160
|
+
method: "POST",
|
|
3161
|
+
headers: { "content-type": "application/json" },
|
|
3162
|
+
body: JSON.stringify(body),
|
|
3163
|
+
// Same pane round trip as the mutations (a suspended pane has to wake),
|
|
3164
|
+
// and the payload is slide bytes.
|
|
3165
|
+
timeoutMs:
|
|
3166
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_MUTATION_TIMEOUT_MS || "1805000", 10) ||
|
|
3167
|
+
1805000,
|
|
3168
|
+
},
|
|
3169
|
+
be,
|
|
3170
|
+
);
|
|
3171
|
+
return ensurePaneAckOk(await res.json().catch(() => ({})), "export-slides");
|
|
3172
|
+
}
|
|
3173
|
+
|
|
2861
3174
|
export async function replaceSlides({
|
|
2862
3175
|
presId,
|
|
2863
3176
|
base64,
|
|
2864
3177
|
operationId = null,
|
|
3178
|
+
sourceTagsBySlide = null,
|
|
2865
3179
|
backendURL = null,
|
|
2866
3180
|
}) {
|
|
2867
3181
|
if (!presId) throw new Error("replaceSlides: presId is required");
|
|
2868
3182
|
if (!base64) throw new Error("replaceSlides: base64 is required");
|
|
2869
3183
|
const be = backendURL || (await resolveBackendForPres(presId));
|
|
3184
|
+
const replaceBody = { base64, operationId };
|
|
3185
|
+
if (Array.isArray(sourceTagsBySlide) && sourceTagsBySlide.some(Boolean)) {
|
|
3186
|
+
replaceBody.sourceTagsBySlide = sourceTagsBySlide;
|
|
3187
|
+
}
|
|
2870
3188
|
const res = await bridgeFetch(
|
|
2871
3189
|
`/api/cli/presentations/${encodeURIComponent(presId)}/replace`,
|
|
2872
3190
|
{
|
|
2873
3191
|
method: "POST",
|
|
2874
3192
|
headers: { "content-type": "application/json" },
|
|
2875
|
-
body: JSON.stringify(
|
|
3193
|
+
body: JSON.stringify(replaceBody),
|
|
2876
3194
|
timeoutMs:
|
|
2877
3195
|
parseInt(process.env.SLIDEMIND_BRIDGE_MUTATION_TIMEOUT_MS || "1805000", 10) ||
|
|
2878
3196
|
1805000,
|
|
@@ -3031,7 +3349,7 @@ export function pickPresentationEntry(
|
|
|
3031
3349
|
const list = registry
|
|
3032
3350
|
.map(
|
|
3033
3351
|
(p) =>
|
|
3034
|
-
` ${p.presId}\t${p.env ? `[${p.env}]\t` : ""}${p.title || p.fileName || "(untitled)"}` +
|
|
3352
|
+
` ${p.presId}\t${p.env && isMultiEnv() ? `[${p.env}]\t` : ""}${p.title || p.fileName || "(untitled)"}` +
|
|
3035
3353
|
(p.connected === false || p.suspendedAt ? "\t(suspended — PowerPoint in background)" : ""),
|
|
3036
3354
|
)
|
|
3037
3355
|
.join("\n");
|