slidemind 0.1.4 → 0.1.5
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/bin/slidemind.mjs +12 -7
- package/package.json +1 -1
- package/src/bridge/client.mjs +101 -9
- package/src/mcp/server.mjs +12 -6
package/bin/slidemind.mjs
CHANGED
|
@@ -336,15 +336,20 @@ async function cmdSearch(rest) {
|
|
|
336
336
|
subtype = "image";
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
339
|
+
// Window-agnostic: tries the picked pane, then fails over to any other live
|
|
340
|
+
// window on the same backend if it's suspended/frozen. The answering window
|
|
341
|
+
// (res.presId) is what reuse must target — the searchId is bound to it.
|
|
342
|
+
const res = await client.engineSearchAnyPane({
|
|
343
|
+
registry,
|
|
344
|
+
explicitPresId: to,
|
|
341
345
|
engineId,
|
|
342
346
|
query,
|
|
343
347
|
subtype,
|
|
344
348
|
intent: intent ?? null,
|
|
345
349
|
top: top ?? null,
|
|
346
|
-
backendURL,
|
|
347
350
|
});
|
|
351
|
+
const ranPresId = res?.presId ?? presId;
|
|
352
|
+
const ranEntry = registry.find((p) => p.presId === ranPresId) ?? entry;
|
|
348
353
|
if (asJson) {
|
|
349
354
|
process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
350
355
|
return;
|
|
@@ -387,14 +392,14 @@ async function cmdSearch(rest) {
|
|
|
387
392
|
// add-ins that don't echo a top-level subtype.
|
|
388
393
|
const isSlideResults =
|
|
389
394
|
res.subtype === "slide" || results.some((r) => r.kind === "slide");
|
|
390
|
-
const envTag =
|
|
395
|
+
const envTag = ranEntry.env ? ` [${ranEntry.env}]` : "";
|
|
391
396
|
if (isSlideResults) {
|
|
392
397
|
// The searchId is bound to the window the search ran on — reuse must target
|
|
393
398
|
// that same window, so spell out its presId in the next-step commands.
|
|
394
399
|
process.stdout.write(
|
|
395
|
-
`searchId ${res.searchId} ran on window ${
|
|
396
|
-
` slidemind insert-slide --search ${res.searchId} --pick <n> --to ${
|
|
397
|
-
` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck> --to ${
|
|
400
|
+
`searchId ${res.searchId} ran on window ${ranPresId}${envTag} — reuse from THIS window:\n` +
|
|
401
|
+
` slidemind insert-slide --search ${res.searchId} --pick <n> --to ${ranPresId}\n` +
|
|
402
|
+
` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck> --to ${ranPresId}\n`,
|
|
398
403
|
);
|
|
399
404
|
} else {
|
|
400
405
|
process.stdout.write(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slidemind",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Connect Claude Code, Codex & opencode to the slidemind PowerPoint add-in — let your coding agent build and edit native, on-brand PowerPoint decks against your active corporate design, from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/bridge/client.mjs
CHANGED
|
@@ -28,6 +28,19 @@ function listTimeoutMs() {
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Engine ops (list / search / download / extract) are RELAYED through the task
|
|
32
|
+
// pane: the server waits up to ~25s for a suspended pane to reconnect, then up
|
|
33
|
+
// to 60s for the pane's ack. Budget above that sum so a typed pane error
|
|
34
|
+
// (pane_suspended / pane_timeout) wins over a client timeout. Note `ls engines`
|
|
35
|
+
// is pane-relayed too — using the 8s list timeout there masked the real
|
|
36
|
+
// pane_suspended as a bare "timed out after 8000ms".
|
|
37
|
+
function engineTimeoutMs() {
|
|
38
|
+
return (
|
|
39
|
+
parseInt(process.env.SLIDEMIND_BRIDGE_ENGINE_TIMEOUT_MS || "90000", 10) ||
|
|
40
|
+
90000
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
const NOT_PAIRED_MESSAGE =
|
|
32
45
|
'slidemind is not paired. In the slidemind add-in, click "Connect CLI" ' +
|
|
33
46
|
"and run the shown `slidemind pair …` command, then retry.";
|
|
@@ -483,7 +496,9 @@ async function listEnginesForBackend(backendURL, { type = null, subtype = null }
|
|
|
483
496
|
const qs = params.toString();
|
|
484
497
|
const res = await bridgeFetch(
|
|
485
498
|
`/api/cli/engines${qs ? `?${qs}` : ""}`,
|
|
486
|
-
|
|
499
|
+
// Pane-relayed on the add-in side — needs the engine budget, not the 8s
|
|
500
|
+
// list timeout (which bailed before the pane could ever answer).
|
|
501
|
+
{ timeoutMs: engineTimeoutMs() },
|
|
487
502
|
backendURL,
|
|
488
503
|
);
|
|
489
504
|
const data = await res.json();
|
|
@@ -837,6 +852,11 @@ export async function engineSearch({
|
|
|
837
852
|
intent = null,
|
|
838
853
|
top = null,
|
|
839
854
|
backendURL = null,
|
|
855
|
+
// When the chosen pane is suspended, wait CLIENT-side for THIS window to wake
|
|
856
|
+
// (the single-window default). engineSearchAnyPane sets this false for all but
|
|
857
|
+
// the last candidate so it can fail over to another live window instead.
|
|
858
|
+
waitOnSuspend = true,
|
|
859
|
+
timeoutMs = null,
|
|
840
860
|
}) {
|
|
841
861
|
if (!presId) throw new Error("engineSearch: presId is required");
|
|
842
862
|
if (!engineId) throw new Error("engineSearch: engineId is required");
|
|
@@ -846,24 +866,96 @@ export async function engineSearch({
|
|
|
846
866
|
if (subtype) body.subtype = subtype;
|
|
847
867
|
if (intent) body.intent = intent;
|
|
848
868
|
if (typeof top === "number") body.top = top;
|
|
849
|
-
|
|
869
|
+
const run = async (pid, b) => {
|
|
850
870
|
const res = await bridgeFetch(
|
|
851
871
|
`/api/cli/presentations/${encodeURIComponent(pid)}/engine/search`,
|
|
852
872
|
{
|
|
853
873
|
method: "POST",
|
|
854
874
|
headers: { "content-type": "application/json" },
|
|
855
875
|
body: JSON.stringify(body),
|
|
856
|
-
|
|
857
|
-
// a suspended pane to reconnect, then up to 60s for the pane's ack —
|
|
858
|
-
// stay above that sum so typed errors win over client timeouts.
|
|
859
|
-
timeoutMs:
|
|
860
|
-
parseInt(process.env.SLIDEMIND_BRIDGE_ENGINE_TIMEOUT_MS || "90000", 10) ||
|
|
861
|
-
90000,
|
|
876
|
+
timeoutMs: timeoutMs ?? engineTimeoutMs(),
|
|
862
877
|
},
|
|
863
878
|
b,
|
|
864
879
|
);
|
|
865
880
|
return res.json(); // { ok, searchId, engineId, subtype, intent, resultCount, gridDataUri?, results }
|
|
866
|
-
}
|
|
881
|
+
};
|
|
882
|
+
return waitOnSuspend ? engineOpWithPaneWait(presId, be, run) : run(presId, be);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// A suspended/throttled background webview must not strand a window-AGNOSTIC
|
|
886
|
+
// engine search: the catalog is per-user, so any connected pane can answer it.
|
|
887
|
+
// Try the picked pane first, then every other connected pane on the same
|
|
888
|
+
// backend (non-suspended first), failing fast past each unresponsive one; the
|
|
889
|
+
// LAST candidate keeps the normal "click the pane to wake it" wait so a genuine
|
|
890
|
+
// single-window/all-asleep case still surfaces the actionable prompt. Returns
|
|
891
|
+
// the search result tagged with the presId/backendURL that actually answered —
|
|
892
|
+
// the searchId is bound to THAT window, so callers must thread it into
|
|
893
|
+
// follow-up view/insert/add-slide. An explicit presId pins the window (no
|
|
894
|
+
// failover — the caller asked for that specific one).
|
|
895
|
+
export async function engineSearchAnyPane({
|
|
896
|
+
registry,
|
|
897
|
+
explicitPresId = null,
|
|
898
|
+
engineId,
|
|
899
|
+
query,
|
|
900
|
+
subtype = null,
|
|
901
|
+
intent = null,
|
|
902
|
+
top = null,
|
|
903
|
+
}) {
|
|
904
|
+
const primary = pickAnyPresentationEntry(registry, explicitPresId);
|
|
905
|
+
const primaryBackend = primary.backendURL ?? null;
|
|
906
|
+
let candidates;
|
|
907
|
+
if (explicitPresId) {
|
|
908
|
+
candidates = [primary];
|
|
909
|
+
} else {
|
|
910
|
+
const others = (Array.isArray(registry) ? registry : [])
|
|
911
|
+
.filter(
|
|
912
|
+
(p) =>
|
|
913
|
+
p.presId !== primary.presId &&
|
|
914
|
+
p.connected !== false &&
|
|
915
|
+
(p.backendURL ?? null) === primaryBackend,
|
|
916
|
+
)
|
|
917
|
+
.sort((a, b) => (a.suspendedAt ? 1 : 0) - (b.suspendedAt ? 1 : 0));
|
|
918
|
+
candidates = [primary, ...others];
|
|
919
|
+
}
|
|
920
|
+
const failoverTimeout = envInt(
|
|
921
|
+
"SLIDEMIND_BRIDGE_ENGINE_FAILOVER_TIMEOUT_MS",
|
|
922
|
+
25000,
|
|
923
|
+
);
|
|
924
|
+
let lastErr = null;
|
|
925
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
926
|
+
const cand = candidates[i];
|
|
927
|
+
const isLast = i === candidates.length - 1;
|
|
928
|
+
try {
|
|
929
|
+
const res = await engineSearch({
|
|
930
|
+
presId: cand.presId,
|
|
931
|
+
engineId,
|
|
932
|
+
query,
|
|
933
|
+
subtype,
|
|
934
|
+
intent,
|
|
935
|
+
top,
|
|
936
|
+
backendURL: cand.backendURL ?? primaryBackend,
|
|
937
|
+
waitOnSuspend: isLast,
|
|
938
|
+
timeoutMs: isLast ? null : failoverTimeout,
|
|
939
|
+
});
|
|
940
|
+
return {
|
|
941
|
+
...res,
|
|
942
|
+
presId: cand.presId,
|
|
943
|
+
backendURL: cand.backendURL ?? primaryBackend,
|
|
944
|
+
};
|
|
945
|
+
} catch (err) {
|
|
946
|
+
lastErr = err;
|
|
947
|
+
// A suspended/frozen/slow pane is recoverable by trying the next window;
|
|
948
|
+
// anything else (bad engineId, auth, 4xx) is real — surface it now.
|
|
949
|
+
const recoverable =
|
|
950
|
+
err?.code === "pane_suspended" ||
|
|
951
|
+
err?.code === "pane_timeout" ||
|
|
952
|
+
err?.code === "bridge_timeout" ||
|
|
953
|
+
err?.status === 503 ||
|
|
954
|
+
err?.status === 504;
|
|
955
|
+
if (isLast || !recoverable) throw err;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
throw lastErr ?? new Error("engineSearchAnyPane: no candidate panes");
|
|
867
959
|
}
|
|
868
960
|
|
|
869
961
|
// Fetch a chosen engine result's bytes through the local bridge. For image
|
package/src/mcp/server.mjs
CHANGED
|
@@ -353,15 +353,20 @@ server.registerTool(
|
|
|
353
353
|
engineId = pick.id;
|
|
354
354
|
subtype = "image";
|
|
355
355
|
}
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
// Window-agnostic: tries the picked pane, then fails over to any other
|
|
357
|
+
// live window on the same backend if it's suspended/frozen. res.presId is
|
|
358
|
+
// the window that actually answered — echo THAT back (the searchId is
|
|
359
|
+
// bound to it) so follow-up view/insert/add target the right pane.
|
|
360
|
+
const res = await client.engineSearchAnyPane({
|
|
361
|
+
registry,
|
|
362
|
+
explicitPresId: presentation_id ?? null,
|
|
358
363
|
engineId,
|
|
359
364
|
query,
|
|
360
365
|
subtype,
|
|
361
366
|
intent: intent ?? null,
|
|
362
367
|
top: top ?? null,
|
|
363
|
-
backendURL,
|
|
364
368
|
});
|
|
369
|
+
const ranPresId = res?.presId ?? presId;
|
|
365
370
|
const content = [];
|
|
366
371
|
const grid = imageContentFromDataUri(res?.gridDataUri);
|
|
367
372
|
if (grid) content.push(grid);
|
|
@@ -385,9 +390,10 @@ server.registerTool(
|
|
|
385
390
|
{
|
|
386
391
|
ok: res?.ok ?? false,
|
|
387
392
|
// The window the search ran on (auto-attached when the caller didn't
|
|
388
|
-
// pin one
|
|
389
|
-
// to
|
|
390
|
-
|
|
393
|
+
// pin one; may have failed over from the picked pane). The searchId
|
|
394
|
+
// is bound to it — pass this presentation_id to slidemind_view_slide
|
|
395
|
+
// / _insert_slide / _add_slide_to_deck.
|
|
396
|
+
presentation_id: ranPresId,
|
|
391
397
|
engineId,
|
|
392
398
|
subtype: res?.subtype ?? null,
|
|
393
399
|
intent: res?.intent ?? null,
|