@code-yeongyu/senpi 2026.10.10-4 → 2026.10.10-6
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/CHANGELOG.md +46 -0
- package/dist/bundle/chunks/{anthropic-messages-OE3QBX4P.js → anthropic-messages-DUULMQBF.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-N3L2LNP7.js → app-server-command-GJ5NXQSY.js} +1 -1
- package/dist/bundle/chunks/{chunk-3A3C5VVO.js → chunk-3OGEJMZ7.js} +1 -1
- package/dist/bundle/chunks/{chunk-J5MUMVWB.js → chunk-64ZVBCTL.js} +8 -7
- package/dist/bundle/chunks/{chunk-MP2DMTI7.js → chunk-C7LEZMWS.js} +124 -124
- package/dist/bundle/chunks/{chunk-ZSYRWQWG.js → chunk-JMI5OCIJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-ODYQK5XJ.js → chunk-TALS4X7S.js} +1 -1
- package/dist/bundle/chunks/{chunk-7GVD54RM.js → chunk-U3IL5N76.js} +1 -1
- package/dist/bundle/chunks/{chunk-H67EVDWG.js → chunk-URQSNRHZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-H4UIUFU5.js → chunk-VGYIPYOK.js} +1 -1
- package/dist/bundle/chunks/{chunk-M2IHGL3M.js → chunk-VW74BGIC.js} +1 -1
- package/dist/bundle/chunks/{chunk-4TK7VCZZ.js → chunk-WKMUAG23.js} +1 -1
- package/dist/bundle/chunks/chunk-YG5UO3E4.js +7 -0
- package/dist/bundle/chunks/chunk-YV2XQFXG.js +14 -0
- package/dist/bundle/chunks/{chunk-Z7MMKKIY.js → chunk-ZYHIRFGT.js} +1 -1
- package/dist/bundle/chunks/{cli-main-CHDR33FZ.js → cli-main-SZ54SQLA.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-EHRZEBRO.js → help-fast-path-W5W3FYYM.js} +1 -1
- package/dist/bundle/chunks/{host-command-VATE7HRG.js → host-command-YG2FIDPN.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-GC3GIR2N.js → host-lifecycle-5AV4JXDQ.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-RVEM62WX.js → interactive-mode-SCNDQ4R6.js} +1 -1
- package/dist/bundle/chunks/{list-tips-IIRFXBTO.js → list-tips-BXAA4FG5.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-CFND7CXH.js → multi-session-host-ZDZMPNP5.js} +7 -6
- package/dist/bundle/chunks/{package-manager-cli-NWNECBF7.js → package-manager-cli-4LMYHHKH.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-C5BM7TMM.js → rotation-stream-E3XKSST7.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-JQFUDLNB.js → rpc-mode-ZFNFREN5.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-EQ4PQCRP.js → schedule-command-K6CTAB4T.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-HMB5T5VD.js → session-control-endpoint-MVSYBGM3.js} +1 -1
- package/dist/bundle/chunks/{session-picker-O7OESJWU.js → session-picker-TNXWY5QY.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +224 -222
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session-runtime.d.ts +6 -0
- package/dist/core/agent-session-runtime.js +18 -0
- package/dist/core/agent-session.js +5 -2
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +3 -2
- package/dist/core/extensions/builtin/anthropic-subscription/options.js +11 -2
- package/dist/core/extensions/builtin/anthropic-subscription/session-reattach.js +4 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-sync.js +3 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +5 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.js +11 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream.d.ts +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream.js +17 -1
- package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +20 -12
- package/dist/core/extensions/builtin/compaction/lane-policy.js +39 -34
- package/dist/core/extensions/builtin/html-render/bootstrap.d.ts +15 -0
- package/dist/core/extensions/builtin/html-render/bootstrap.js +123 -0
- package/dist/core/extensions/builtin/html-render/images.d.ts +16 -0
- package/dist/core/extensions/builtin/html-render/images.js +155 -0
- package/dist/core/extensions/builtin/html-render/index.d.ts +3 -0
- package/dist/core/extensions/builtin/html-render/index.js +5 -0
- package/dist/core/extensions/builtin/html-render/tool.d.ts +19 -0
- package/dist/core/extensions/builtin/html-render/tool.js +74 -0
- package/dist/core/extensions/builtin/index.js +4 -0
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +41 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-lock.d.ts +3 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-lock.js +39 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-migration.d.ts +14 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-migration.js +91 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +1 -1
- package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/auth/token-store.js +19 -90
- package/dist/core/extensions/builtin/mcp/catalog-cache.d.ts +7 -3
- package/dist/core/extensions/builtin/mcp/catalog-cache.js +61 -12
- package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
- package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/expose/call.d.ts +12 -0
- package/dist/core/extensions/builtin/mcp/expose/call.js +60 -0
- package/dist/core/extensions/builtin/mcp/expose/proxy.js +16 -3
- package/dist/core/extensions/builtin/mcp/expose/register.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/expose/register.js +41 -50
- package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/expose/session.js +5 -1
- package/dist/core/extensions/builtin/mcp/expose/tier-b.js +15 -4
- package/dist/core/extensions/builtin/mcp/index.js +5 -26
- package/dist/core/extensions/builtin/mcp/invocation.d.ts +42 -0
- package/dist/core/extensions/builtin/mcp/invocation.js +118 -0
- package/dist/core/extensions/builtin/mcp/service-connection.d.ts +3 -0
- package/dist/core/extensions/builtin/mcp/service-connection.js +6 -0
- package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/service-register.js +45 -1
- package/dist/core/extensions/builtin/mcp/service-tools-changed.js +26 -3
- package/dist/core/extensions/builtin/mcp/service-types.d.ts +6 -0
- package/dist/core/extensions/builtin/mcp/service.js +83 -49
- package/dist/core/extensions/builtin/mcp/shared-connection.js +17 -4
- package/dist/core/extensions/builtin/mcp/sharing-policy.js +1 -0
- package/dist/core/extensions/builtin/mcp/startup-race.js +47 -9
- package/dist/core/extensions/builtin/permission-system/dispatch-metadata.d.ts +8 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-metadata.js +23 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-policy.d.ts +14 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-policy.js +53 -0
- package/dist/core/extensions/builtin/permission-system/dispatch.d.ts +33 -0
- package/dist/core/extensions/builtin/permission-system/dispatch.js +106 -0
- package/dist/core/extensions/builtin/permission-system/index.js +64 -17
- package/dist/core/extensions/builtin/permission-system/prompt.d.ts +1 -1
- package/dist/core/extensions/builtin/permission-system/prompt.js +3 -1
- package/dist/core/extensions/builtin/permission-system/service.d.ts +11 -1
- package/dist/core/extensions/builtin/permission-system/service.js +14 -4
- package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.d.ts +1 -0
- package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.js +24 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/modes/interactive/interactive-mode.js +21 -4
- package/dist/modes/rpc/host-reservations.d.ts +1 -1
- package/dist/modes/rpc/host-reservations.js +9 -3
- package/dist/modes/rpc/rpc-client.js +14 -2
- package/dist/modes/rpc/rpc-request-deadline.d.ts +8 -0
- package/dist/modes/rpc/rpc-request-deadline.js +10 -0
- package/dist/modes/rpc/rpc-types.d.ts +7 -4
- package/dist/modes/rpc/session-binding.js +15 -1
- package/dist/modes/rpc/session-command-router.d.ts +1 -0
- package/dist/modes/rpc/session-command-router.js +16 -0
- package/dist/modes/rpc/session-registry-attach.js +8 -2
- package/dist/modes/rpc/session-registry.js +5 -7
- package/dist/modes/rpc/session-teardown.d.ts +6 -1
- package/dist/modes/rpc/session-teardown.js +26 -7
- package/dist/modes/rpc/session-worker-client.d.ts +2 -0
- package/dist/modes/rpc/session-worker-client.js +6 -0
- package/dist/modes/rpc/session-worker-protocol.d.ts +4 -0
- package/dist/modes/rpc/session-worker.js +6 -0
- package/dist/modes/rpc/worker-session-registry.js +6 -1
- package/docs/providers.md +3 -2
- package/docs/rpc.md +26 -10
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-RADHRQ4K.js +0 -7
- package/dist/bundle/chunks/chunk-USRAHAW4.js +0 -14
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Provider-scoped compaction
|
|
2
|
+
* Provider-scoped compaction ownership for the `anthropic-subscription` main lane.
|
|
3
3
|
*
|
|
4
|
-
* That lane keeps one resident SDK session per senpi session
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* That lane keeps one resident SDK session per senpi session. By default senpi
|
|
5
|
+
* owns its compaction (speculative, idle, restoration, degradation recovery): an
|
|
6
|
+
* accepted compaction invalidates the resident binding and the next turn
|
|
7
|
+
* cold-seeds the compacted branch (`tainted_compaction` always flattens), while
|
|
8
|
+
* the SDK's native auto-compact is pinned off so exactly one side compacts.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* `compactionOwner: "sdk"` opts out: the Claude Agent SDK runs its own native
|
|
11
|
+
* auto-compaction over the resident transcript and senpi stands down for the
|
|
12
|
+
* lane (no auto-compaction triggers, no context reduction). The stand-down lifts
|
|
13
|
+
* with the `resumeMode: "off"` escape hatch, where senpi flattens its own
|
|
14
|
+
* history into every request. Every other provider is untouched.
|
|
15
|
+
*
|
|
16
|
+
* The owner is read per call, from the same settings snapshot the query options
|
|
17
|
+
* use for the next turn, so a mid-session change can never leave the two sides
|
|
18
|
+
* disagreeing about who compacts.
|
|
13
19
|
*
|
|
14
20
|
* This module also owns the shape of the mirrored `compact_boundary` ledger
|
|
15
21
|
* entry, so the SDK's native compactions stay visible in senpi history.
|
|
16
22
|
*/
|
|
17
23
|
import { ANTHROPIC_SUBSCRIPTION_PROVIDER_ID } from "../anthropic-subscription/account-management.js";
|
|
18
24
|
import { isColdSeedOverflowMessage } from "../anthropic-subscription/cold-seed-budget.js";
|
|
19
|
-
import { loadAnthropicSubscriptionProviderSettingsFromDisk } from "../anthropic-subscription/settings.js";
|
|
25
|
+
import { loadAnthropicSubscriptionProviderSettingsFromDisk, resolveCompactionOwner, } from "../anthropic-subscription/settings.js";
|
|
20
26
|
/** Custom session entry type carrying a mirrored SDK compaction boundary. */
|
|
21
27
|
export const ANTHROPIC_SUBSCRIPTION_COMPACT_ENTRY_TYPE = "claude-sdk-oauth-compact";
|
|
22
28
|
/** Assistant-message diagnostic the lane uses to transport a received boundary. */
|
|
@@ -55,33 +61,27 @@ function isRecord(value) {
|
|
|
55
61
|
export function isSdkNativeCompactionLane(input) {
|
|
56
62
|
if (input.model?.provider !== ANTHROPIC_SUBSCRIPTION_PROVIDER_ID)
|
|
57
63
|
return false;
|
|
58
|
-
return input.resumeMode !== "off";
|
|
64
|
+
return input.compactionOwner === "sdk" && input.resumeMode !== "off";
|
|
59
65
|
}
|
|
60
66
|
/**
|
|
61
|
-
* Per-extension-instance policy
|
|
62
|
-
*
|
|
63
|
-
*
|
|
67
|
+
* Per-extension-instance lane policy. Settings are only read when the active
|
|
68
|
+
* model actually belongs to the lane, so other providers never pay for the
|
|
69
|
+
* lookup; the loader itself is cached by settings-file revision.
|
|
64
70
|
*/
|
|
65
71
|
export function createCompactionLanePolicy(options = {}) {
|
|
66
72
|
const load = options.loadProviderSettings ?? loadAnthropicSubscriptionProviderSettingsFromDisk;
|
|
67
|
-
|
|
68
|
-
let
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
const resolveResumeMode = (cwd) => {
|
|
74
|
-
if (cachedCwd === cwd)
|
|
75
|
-
return true;
|
|
73
|
+
// Read on every call, never memoized here: the query options re-read the same
|
|
74
|
+
// settings for every turn, and a cached owner would let a mid-session change
|
|
75
|
+
// leave senpi and the resident process disagreeing about who compacts. Returns
|
|
76
|
+
// undefined when the settings cannot be read, so callers fail closed to
|
|
77
|
+
// senpi's full behavior.
|
|
78
|
+
const readProviderSettings = (cwd) => {
|
|
76
79
|
try {
|
|
77
|
-
|
|
80
|
+
return load(cwd);
|
|
78
81
|
}
|
|
79
82
|
catch {
|
|
80
|
-
|
|
81
|
-
return false;
|
|
83
|
+
return undefined;
|
|
82
84
|
}
|
|
83
|
-
cachedCwd = cwd;
|
|
84
|
-
return true;
|
|
85
85
|
};
|
|
86
86
|
// Declared as a local so `ownsCompaction` never depends on `this`: the policy object
|
|
87
87
|
// is routinely destructured at call sites, which would otherwise unbind the receiver.
|
|
@@ -93,20 +93,25 @@ export function createCompactionLanePolicy(options = {}) {
|
|
|
93
93
|
// the escape hatch for lanes whose SDK never fires native compaction.
|
|
94
94
|
if (context.getCompactionSettings?.().model)
|
|
95
95
|
return false;
|
|
96
|
-
// A settings read failure must never silently disable senpi compaction
|
|
97
|
-
|
|
98
|
-
if (!
|
|
96
|
+
// A settings read failure must never silently disable senpi compaction.
|
|
97
|
+
const providerSettings = readProviderSettings(context.cwd);
|
|
98
|
+
if (!providerSettings)
|
|
99
99
|
return false;
|
|
100
|
-
return isSdkNativeCompactionLane({
|
|
100
|
+
return isSdkNativeCompactionLane({
|
|
101
|
+
model: context.model,
|
|
102
|
+
resumeMode: providerSettings.resumeMode,
|
|
103
|
+
compactionOwner: resolveCompactionOwner(providerSettings),
|
|
104
|
+
});
|
|
101
105
|
};
|
|
102
106
|
return {
|
|
103
107
|
disablesSenpiCompaction,
|
|
104
108
|
hasAppendOnlyTranscript(context) {
|
|
105
109
|
if (context.model?.provider !== ANTHROPIC_SUBSCRIPTION_PROVIDER_ID)
|
|
106
110
|
return false;
|
|
107
|
-
|
|
111
|
+
const providerSettings = readProviderSettings(context.cwd);
|
|
112
|
+
if (!providerSettings)
|
|
108
113
|
return false;
|
|
109
|
-
return
|
|
114
|
+
return providerSettings.resumeMode !== "off";
|
|
110
115
|
},
|
|
111
116
|
ownsCompaction(context, reason) {
|
|
112
117
|
// Manual is senpi-owned everywhere: it is the user's explicit recovery path,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A page is a static snapshot: everything it shows is in the document (inline,
|
|
3
|
+
* data: or blob:), and it reaches no network. Its scripts run whenever it
|
|
4
|
+
* opens, but fetch, XHR, WebSocket, EventSource, remote scripts, styles,
|
|
5
|
+
* images and fonts, frames, form posts and <base> are refused. The policy is
|
|
6
|
+
* the document's first element, so nothing the page writes loads before it.
|
|
7
|
+
* Mirrors the desktop's packages/shared/src/htmlRenderBootstrap.ts.
|
|
8
|
+
*/
|
|
9
|
+
export declare const HTML_RENDER_CONTENT_SECURITY_POLICY: string;
|
|
10
|
+
/**
|
|
11
|
+
* Inserts the theme bootstrap at the start of the document head, and the
|
|
12
|
+
* snapshot policy ahead of everything the page wrote.
|
|
13
|
+
*/
|
|
14
|
+
export declare function injectHtmlRenderBootstrap(html: string): string;
|
|
15
|
+
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// The theme bootstrap injection and base stylesheet, mirroring the desktop's
|
|
2
|
+
// packages/shared/src/htmlRender.ts (upstream t3code #15968). Kept small and
|
|
3
|
+
// dependency-light: pure string functions, no Effect, no server. A page the
|
|
4
|
+
// agent writes here is the same shape the desktop stores on publish.
|
|
5
|
+
const BASE_CSS = "html{background:var(--background);color:var(--foreground);font-family:var(--font-sans);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;scrollbar-width:none}" +
|
|
6
|
+
"html::-webkit-scrollbar{display:none}body{margin:0}code,kbd,pre,samp{font-family:var(--font-mono)}";
|
|
7
|
+
const DARK_VARS = {
|
|
8
|
+
"--background": "#0a0a0a",
|
|
9
|
+
"--foreground": "#f5f5f5",
|
|
10
|
+
"--muted-foreground": "#818181",
|
|
11
|
+
"--chart-1": "#60a5fa",
|
|
12
|
+
"--font-sans": '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
|
|
13
|
+
"--font-mono": '"SF Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace',
|
|
14
|
+
"--radius": "0.625rem",
|
|
15
|
+
};
|
|
16
|
+
const LIGHT_VARS = {
|
|
17
|
+
...DARK_VARS,
|
|
18
|
+
"--background": "#ffffff",
|
|
19
|
+
"--foreground": "#0a0a0a",
|
|
20
|
+
"--muted-foreground": "#525252",
|
|
21
|
+
"--chart-1": "#1d4ed8",
|
|
22
|
+
};
|
|
23
|
+
function rootRule(appearance, variables) {
|
|
24
|
+
const declarations = Object.entries(variables)
|
|
25
|
+
.map(([name, value]) => `${name}:${value};`)
|
|
26
|
+
.join("");
|
|
27
|
+
return `:root{color-scheme:${appearance};${declarations}}`;
|
|
28
|
+
}
|
|
29
|
+
const BOOTSTRAP_SCRIPT = `(function(){var s=document.getElementById("t3-theme");if(!s)return;var b=${JSON.stringify(BASE_CSS)};try{var m=window.matchMedia("(prefers-color-scheme: light)");var a=function(){s.textContent=m.matches?${JSON.stringify(rootRule("light", LIGHT_VARS))}:${JSON.stringify(rootRule("dark", DARK_VARS))}+b;};a();if(m.addEventListener)m.addEventListener("change",a);}catch(e){}})();`;
|
|
30
|
+
function bootstrapMarkup(markup) {
|
|
31
|
+
const defaultCss = `${rootRule("dark", DARK_VARS)}@media (prefers-color-scheme: light){${rootRule("light", LIGHT_VARS)}}${BASE_CSS}`;
|
|
32
|
+
return [
|
|
33
|
+
/<meta\s[^>]*charset/i.test(markup.slice(0, 4096)) ? "" : '<meta charset="utf-8">',
|
|
34
|
+
/<meta\s[^>]*name\s*=\s*["']?viewport/i.test(markup)
|
|
35
|
+
? ""
|
|
36
|
+
: '<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
37
|
+
`<style id="t3-theme">${defaultCss}</style>`,
|
|
38
|
+
`<script>${BOOTSTRAP_SCRIPT}</script>`,
|
|
39
|
+
].join("");
|
|
40
|
+
}
|
|
41
|
+
const blankNonMarkup = (html) => {
|
|
42
|
+
const scan = html.replace(/<!--[\s\S]*?(?:-->|$)|<(script|style|textarea|title|xmp|iframe|noembed|noframes|noscript)\b[\s\S]*?(?:<\/\1\s*>|$)|<plaintext\b[\s\S]*$/gi, (match) => " ".repeat(match.length));
|
|
43
|
+
const parts = [];
|
|
44
|
+
let depth = 0;
|
|
45
|
+
let start = 0;
|
|
46
|
+
let at = 0;
|
|
47
|
+
for (const match of scan.matchAll(/<(\/?)template(?:\s[^>]*)?\/?>/gi)) {
|
|
48
|
+
if (!match[1]) {
|
|
49
|
+
if (depth++ === 0)
|
|
50
|
+
start = match.index;
|
|
51
|
+
}
|
|
52
|
+
else if (depth > 0 && --depth === 0) {
|
|
53
|
+
const end = match.index + match[0].length;
|
|
54
|
+
parts.push(scan.slice(at, start), " ".repeat(end - start));
|
|
55
|
+
at = end;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (depth > 0) {
|
|
59
|
+
parts.push(scan.slice(at, start), " ".repeat(scan.length - start));
|
|
60
|
+
at = scan.length;
|
|
61
|
+
}
|
|
62
|
+
parts.push(scan.slice(at));
|
|
63
|
+
return parts.join("");
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* A page is a static snapshot: everything it shows is in the document (inline,
|
|
67
|
+
* data: or blob:), and it reaches no network. Its scripts run whenever it
|
|
68
|
+
* opens, but fetch, XHR, WebSocket, EventSource, remote scripts, styles,
|
|
69
|
+
* images and fonts, frames, form posts and <base> are refused. The policy is
|
|
70
|
+
* the document's first element, so nothing the page writes loads before it.
|
|
71
|
+
* Mirrors the desktop's packages/shared/src/htmlRenderBootstrap.ts.
|
|
72
|
+
*/
|
|
73
|
+
export const HTML_RENDER_CONTENT_SECURITY_POLICY = [
|
|
74
|
+
"default-src 'none'",
|
|
75
|
+
"script-src 'unsafe-inline' 'unsafe-eval' data: blob:",
|
|
76
|
+
"style-src 'unsafe-inline' data:",
|
|
77
|
+
"img-src data: blob:",
|
|
78
|
+
"font-src data:",
|
|
79
|
+
"media-src data: blob:",
|
|
80
|
+
"worker-src blob:",
|
|
81
|
+
"connect-src 'none'",
|
|
82
|
+
"frame-src 'none'",
|
|
83
|
+
"form-action 'none'",
|
|
84
|
+
"base-uri 'none'",
|
|
85
|
+
].join("; ");
|
|
86
|
+
const POLICY_META = `<meta http-equiv="Content-Security-Policy" content="${HTML_RENDER_CONTENT_SECURITY_POLICY}">`;
|
|
87
|
+
// Every written page starts with a UTF-8 byte order mark, a standards-mode
|
|
88
|
+
// doctype and the policy, in that order and in plain ASCII. The mark makes the
|
|
89
|
+
// browser decode the file as UTF-8 whatever charset the page declares, so no
|
|
90
|
+
// declared encoding can turn the policy into text, and nothing the page wrote
|
|
91
|
+
// comes before it. A leading doctype of the page's own is dropped only when it is
|
|
92
|
+
// printable ASCII; anything else stays where it is, after the policy, where the
|
|
93
|
+
// parser ignores a doctype.
|
|
94
|
+
const PREAMBLE = `\uFEFF<!doctype html>${POLICY_META}`;
|
|
95
|
+
const LEADING_ASCII_DOCTYPE = /^\uFEFF?[\t\n\f\r ]*<!doctype[ -=?-~]*>/i;
|
|
96
|
+
/**
|
|
97
|
+
* Inserts the theme bootstrap at the start of the document head, and the
|
|
98
|
+
* snapshot policy ahead of everything the page wrote.
|
|
99
|
+
*/
|
|
100
|
+
export function injectHtmlRenderBootstrap(html) {
|
|
101
|
+
return PREAMBLE + injectThemeBootstrap(html).replace(LEADING_ASCII_DOCTYPE, "");
|
|
102
|
+
}
|
|
103
|
+
function injectThemeBootstrap(html) {
|
|
104
|
+
const scan = blankNonMarkup(html);
|
|
105
|
+
const markup = bootstrapMarkup(scan);
|
|
106
|
+
const headOpen = /<head(?:\s[^>]*)?>/i.exec(scan);
|
|
107
|
+
if (headOpen) {
|
|
108
|
+
const at = headOpen.index + headOpen[0].length;
|
|
109
|
+
return html.slice(0, at) + markup + html.slice(at);
|
|
110
|
+
}
|
|
111
|
+
const htmlOpen = /<html(?:\s[^>]*)?>/i.exec(scan);
|
|
112
|
+
if (htmlOpen) {
|
|
113
|
+
const at = htmlOpen.index + htmlOpen[0].length;
|
|
114
|
+
return `${html.slice(0, at)}<head>${markup}</head>${html.slice(at)}`;
|
|
115
|
+
}
|
|
116
|
+
const doctype = /^\s*<!doctype[^>]*>/i.exec(html);
|
|
117
|
+
if (doctype) {
|
|
118
|
+
const at = doctype[0].length;
|
|
119
|
+
return `${html.slice(0, at)}<head>${markup}</head>${html.slice(at)}`;
|
|
120
|
+
}
|
|
121
|
+
return `<!doctype html><head>${markup}</head>${html}`;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class HtmlRenderImagesNotFoundError extends Error {
|
|
2
|
+
readonly paths: readonly string[];
|
|
3
|
+
constructor(paths: readonly string[]);
|
|
4
|
+
}
|
|
5
|
+
export declare class HtmlRenderImageTooLargeError extends Error {
|
|
6
|
+
constructor(path: string, sizeBytes: number);
|
|
7
|
+
}
|
|
8
|
+
export declare class HtmlRenderPageTooLargeError extends Error {
|
|
9
|
+
constructor(sizeBytes: number);
|
|
10
|
+
}
|
|
11
|
+
/** Replaces every local image reference with a data URI; unreadable paths throw. */
|
|
12
|
+
export declare function inlineLocalImages(html: string): Promise<{
|
|
13
|
+
html: string;
|
|
14
|
+
missing: string[];
|
|
15
|
+
}>;
|
|
16
|
+
//# sourceMappingURL=images.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Local-image preparation, mirroring the desktop's apps/server/src/htmlRender/
|
|
2
|
+
// HtmlRender.ts (upstream t3code #15968): absolute-path images are inlined as
|
|
3
|
+
// data URIs only after a magic-byte/SVG-root check, so a renamed secret is
|
|
4
|
+
// refused, and the page stays within the size caps. Pure byte/string work.
|
|
5
|
+
import { readFile, stat } from "node:fs/promises";
|
|
6
|
+
const MIB = 1024 * 1024;
|
|
7
|
+
const MAX_IMAGE_BYTES = 10 * MIB;
|
|
8
|
+
const MAX_PAGE_BYTES = 25 * MIB;
|
|
9
|
+
export class HtmlRenderImagesNotFoundError extends Error {
|
|
10
|
+
constructor(paths) {
|
|
11
|
+
super(`These local images could not be read: ${paths.join(", ")}. Use absolute paths to existing image files, or remove them.`);
|
|
12
|
+
this.name = "HtmlRenderImagesNotFoundError";
|
|
13
|
+
this.paths = paths;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class HtmlRenderImageTooLargeError extends Error {
|
|
17
|
+
constructor(path, sizeBytes) {
|
|
18
|
+
super(`${path} is ${(sizeBytes / MIB).toFixed(1)} MiB; each local image must be at most 10 MiB.`);
|
|
19
|
+
this.name = "HtmlRenderImageTooLargeError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class HtmlRenderPageTooLargeError extends Error {
|
|
23
|
+
constructor(sizeBytes) {
|
|
24
|
+
super(`With its images inlined the page is ${(sizeBytes / MIB).toFixed(1)} MiB; the limit is 25 MiB. Use smaller images.`);
|
|
25
|
+
this.name = "HtmlRenderPageTooLargeError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const IMAGE_MIME_TYPES = {
|
|
29
|
+
png: "image/png",
|
|
30
|
+
jpg: "image/jpeg",
|
|
31
|
+
jpeg: "image/jpeg",
|
|
32
|
+
gif: "image/gif",
|
|
33
|
+
webp: "image/webp",
|
|
34
|
+
avif: "image/avif",
|
|
35
|
+
svg: "image/svg+xml",
|
|
36
|
+
bmp: "image/bmp",
|
|
37
|
+
ico: "image/x-icon",
|
|
38
|
+
};
|
|
39
|
+
const IMAGE_EXTENSIONS = Object.keys(IMAGE_MIME_TYPES).join("|");
|
|
40
|
+
const ABSOLUTE_PATH = String.raw `(?:/(?!/)|[a-z]:[\\/])`;
|
|
41
|
+
const LOCAL_IMAGE_PATTERN = new RegExp(String.raw `(["'\x60])(${ABSOLUTE_PATH}(?:(?!\1)[^\r\n]){0,2048}?\.(?:${IMAGE_EXTENSIONS}))\1` +
|
|
42
|
+
String.raw `|url\(\s*(${ABSOLUTE_PATH}[^\s"'\x60()]{0,2048}?\.(?:${IMAGE_EXTENSIONS}))\s*\)`, "gid");
|
|
43
|
+
const findLocalImages = (html) => Array.from(html.matchAll(LOCAL_IMAGE_PATTERN)).flatMap((match) => {
|
|
44
|
+
const span = match.indices?.[2] ?? match.indices?.[3];
|
|
45
|
+
return span ? [{ start: span[0], end: span[1], path: html.slice(span[0], span[1]) }] : [];
|
|
46
|
+
});
|
|
47
|
+
const filePathFor = (reference) => /^[a-z]:/i.test(reference) ? reference.replaceAll("\\\\", "\\") : reference;
|
|
48
|
+
const dataUriPrefix = (path) => `data:${IMAGE_MIME_TYPES[path.slice(path.lastIndexOf(".") + 1).toLowerCase()] ?? "application/octet-stream"};base64,`;
|
|
49
|
+
const latin1 = (bytes, start, end) => String.fromCharCode(...bytes.subarray(start, end));
|
|
50
|
+
const after = (text, token, from) => {
|
|
51
|
+
const at = text.indexOf(token, from);
|
|
52
|
+
return at === -1 ? -1 : at + token.length;
|
|
53
|
+
};
|
|
54
|
+
const afterDoctype = (text, from) => {
|
|
55
|
+
let inSubset = false;
|
|
56
|
+
let at = from;
|
|
57
|
+
while (at !== -1 && at < text.length) {
|
|
58
|
+
const char = text[at];
|
|
59
|
+
if (char === '"' || char === "'")
|
|
60
|
+
at = after(text, char, at + 1);
|
|
61
|
+
else if (inSubset && text.startsWith("<!--", at))
|
|
62
|
+
at = after(text, "-->", at + 4);
|
|
63
|
+
else if (inSubset && text.startsWith("<?", at))
|
|
64
|
+
at = after(text, "?>", at + 2);
|
|
65
|
+
else if (char === ">" && !inSubset)
|
|
66
|
+
return at + 1;
|
|
67
|
+
else {
|
|
68
|
+
if (char === "[")
|
|
69
|
+
inSubset = true;
|
|
70
|
+
else if (char === "]")
|
|
71
|
+
inSubset = false;
|
|
72
|
+
at += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return -1;
|
|
76
|
+
};
|
|
77
|
+
const hasSvgRoot = (text) => {
|
|
78
|
+
let at = 0;
|
|
79
|
+
while (at !== -1) {
|
|
80
|
+
while (/\s/.test(text.charAt(at)))
|
|
81
|
+
at += 1;
|
|
82
|
+
if (text.startsWith("<?", at))
|
|
83
|
+
at = after(text, "?>", at + 2);
|
|
84
|
+
else if (text.startsWith("<!--", at))
|
|
85
|
+
at = after(text, "-->", at + 4);
|
|
86
|
+
else if (text.slice(at, at + 9).toLowerCase() === "<!doctype")
|
|
87
|
+
at = afterDoctype(text, at + 9);
|
|
88
|
+
else
|
|
89
|
+
return /^<svg[ \t\r\n/>]/.test(text.slice(at, at + 5));
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
};
|
|
93
|
+
/** Whether file bytes are an image, whatever the file is named. */
|
|
94
|
+
const isImageBytes = (bytes) => {
|
|
95
|
+
const head = latin1(bytes, 0, 12);
|
|
96
|
+
if (head.startsWith("\x89PNG") ||
|
|
97
|
+
head.startsWith("\xff\xd8\xff") ||
|
|
98
|
+
head.startsWith("GIF8") ||
|
|
99
|
+
head.startsWith("\0\0\x01\0") ||
|
|
100
|
+
(head.startsWith("BM") && head.slice(6, 10) === "\0\0\0\0") ||
|
|
101
|
+
(head.startsWith("RIFF") && head.slice(8, 12) === "WEBP") ||
|
|
102
|
+
/^ftyp(?:avif|avis|mif1)$/.test(head.slice(4, 12))) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return hasSvgRoot(new TextDecoder().decode(bytes.subarray(0, 4096)));
|
|
106
|
+
};
|
|
107
|
+
/** Replaces every local image reference with a data URI; unreadable paths throw. */
|
|
108
|
+
export async function inlineLocalImages(html) {
|
|
109
|
+
const references = findLocalImages(html);
|
|
110
|
+
const uniquePaths = [...new Set(references.map((reference) => reference.path))];
|
|
111
|
+
const files = await Promise.all(uniquePaths.map(async (path) => {
|
|
112
|
+
try {
|
|
113
|
+
const info = await stat(filePathFor(path));
|
|
114
|
+
return { path, size: info.isFile() ? Number(info.size) : undefined };
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return { path, size: undefined };
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
120
|
+
const oversized = files.find((file) => file.size !== undefined && file.size > MAX_IMAGE_BYTES);
|
|
121
|
+
if (oversized?.size !== undefined) {
|
|
122
|
+
throw new HtmlRenderImageTooLargeError(oversized.path, oversized.size);
|
|
123
|
+
}
|
|
124
|
+
const images = new Map();
|
|
125
|
+
let readBytes = 0;
|
|
126
|
+
for (const file of files.filter((entry) => entry.size !== undefined)) {
|
|
127
|
+
const bytes = new Uint8Array(await readFile(filePathFor(file.path)));
|
|
128
|
+
if (!isImageBytes(bytes))
|
|
129
|
+
continue;
|
|
130
|
+
if (bytes.byteLength > MAX_IMAGE_BYTES) {
|
|
131
|
+
throw new HtmlRenderImageTooLargeError(file.path, bytes.byteLength);
|
|
132
|
+
}
|
|
133
|
+
readBytes += Math.ceil(bytes.byteLength / 3) * 4;
|
|
134
|
+
if (readBytes > MAX_PAGE_BYTES)
|
|
135
|
+
throw new HtmlRenderPageTooLargeError(readBytes);
|
|
136
|
+
images.set(file.path, bytes);
|
|
137
|
+
}
|
|
138
|
+
const parts = [];
|
|
139
|
+
let cursor = 0;
|
|
140
|
+
for (const reference of references) {
|
|
141
|
+
const bytes = images.get(reference.path);
|
|
142
|
+
if (bytes === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
parts.push(html.slice(cursor, reference.start), dataUriPrefix(reference.path) + Buffer.from(bytes).toString("base64"));
|
|
145
|
+
cursor = reference.end;
|
|
146
|
+
}
|
|
147
|
+
parts.push(html.slice(cursor));
|
|
148
|
+
const inlined = parts.join("");
|
|
149
|
+
if (Buffer.byteLength(inlined) > MAX_PAGE_BYTES) {
|
|
150
|
+
throw new HtmlRenderPageTooLargeError(Buffer.byteLength(inlined));
|
|
151
|
+
}
|
|
152
|
+
const missing = files.filter((file) => !images.has(file.path)).map((file) => file.path);
|
|
153
|
+
return { html: inlined, missing };
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=images.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
export interface ShowHtmlPageDetails {
|
|
3
|
+
path: string;
|
|
4
|
+
title: string;
|
|
5
|
+
height: number | undefined;
|
|
6
|
+
prepared: boolean;
|
|
7
|
+
missingImages?: string[];
|
|
8
|
+
/**
|
|
9
|
+
* The page as the agent wrote it, for a host that publishes it itself (the
|
|
10
|
+
* desktop renders it in the thread). Never part of the model-visible content.
|
|
11
|
+
*/
|
|
12
|
+
html: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const showHtmlPageTool: import("../../types.ts").ToolDefinition<Type.TObject<{
|
|
15
|
+
html: Type.TString;
|
|
16
|
+
title: Type.TString;
|
|
17
|
+
height: Type.TOptional<Type.TInteger>;
|
|
18
|
+
}>, ShowHtmlPageDetails, any> & import("../../types.ts").ToolDefinition<any, any, any>;
|
|
19
|
+
//# sourceMappingURL=tool.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { defineTool } from "../../types.js";
|
|
5
|
+
import { injectHtmlRenderBootstrap } from "./bootstrap.js";
|
|
6
|
+
import { inlineLocalImages } from "./images.js";
|
|
7
|
+
// The same input limit as the desktop's html_render. Images belong as absolute
|
|
8
|
+
// file paths, which are inlined after the check, so the document itself stays
|
|
9
|
+
// small: it travels in the tool call and again in the result details.
|
|
10
|
+
const MAX_HTML_CHARACTERS = 512_000;
|
|
11
|
+
const Params = Type.Object({
|
|
12
|
+
html: Type.String({
|
|
13
|
+
description: "A complete, self-contained HTML document.",
|
|
14
|
+
minLength: 1,
|
|
15
|
+
maxLength: MAX_HTML_CHARACTERS,
|
|
16
|
+
}),
|
|
17
|
+
title: Type.String({ description: "Short name for the page.", minLength: 1, maxLength: 200 }),
|
|
18
|
+
height: Type.Optional(Type.Integer({
|
|
19
|
+
description: "Frame height hint in CSS pixels, 80-2000. Defaults to the page's natural height.",
|
|
20
|
+
})),
|
|
21
|
+
});
|
|
22
|
+
const OUTPUT_DIR = ".senpi/html-pages";
|
|
23
|
+
const clampHeight = (height) => height === undefined ? undefined : Math.min(2000, Math.max(80, Math.round(height)));
|
|
24
|
+
export const showHtmlPageTool = defineTool({
|
|
25
|
+
name: "show_html_page",
|
|
26
|
+
label: "Show HTML Page",
|
|
27
|
+
description: "Show a finished self-contained HTML page (chart, table, diagram, mockup) to the reader. " +
|
|
28
|
+
"No network: inline every script, style and image (data: URIs). A <script src>, stylesheet link, font, image " +
|
|
29
|
+
"or fetch that points at a URL (a CDN included) is refused, and the page comes out blank or broken. " +
|
|
30
|
+
"Local images written as absolute file paths " +
|
|
31
|
+
"are inlined automatically after a byte check. In the desktop thread this renders inline above your " +
|
|
32
|
+
"reply; elsewhere it is written to a file you can open in the desktop.",
|
|
33
|
+
promptSnippet: "show_html_page: show a self-contained HTML page (chart/table/diagram) to the reader",
|
|
34
|
+
promptGuidelines: [
|
|
35
|
+
"No network: inline every script, style and image (data: URIs); a CDN <script src> or remote font is refused and leaves the page blank.",
|
|
36
|
+
"Absolute-path local images are inlined after a magic-byte check; a renamed non-image is refused.",
|
|
37
|
+
],
|
|
38
|
+
parameters: Params,
|
|
39
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
40
|
+
const inlined = await inlineLocalImages(params.html);
|
|
41
|
+
const prepared = injectHtmlRenderBootstrap(inlined.html);
|
|
42
|
+
const height = clampHeight(params.height);
|
|
43
|
+
const dir = join(ctx.cwd, OUTPUT_DIR);
|
|
44
|
+
await mkdir(dir, { recursive: true });
|
|
45
|
+
const safeName = params.title
|
|
46
|
+
.replace(/[\\/:*?"<>|\p{Cc}]+/gu, " ")
|
|
47
|
+
.replace(/\s+/g, " ")
|
|
48
|
+
.trim()
|
|
49
|
+
.slice(0, 60) || "page";
|
|
50
|
+
const path = join(dir, `${safeName}-${Date.now().toString(36)}.html`);
|
|
51
|
+
await writeFile(path, prepared, "utf8");
|
|
52
|
+
const missing = inlined.missing.length === 0 ? undefined : inlined.missing;
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: `Wrote the page to ${path}. Open it in the desktop to see it rendered with your theme. ` +
|
|
58
|
+
(missing === undefined
|
|
59
|
+
? "Local images were inlined."
|
|
60
|
+
: `Some local images could not be read and were left as written: ${missing.join(", ")}.`),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
details: {
|
|
64
|
+
path,
|
|
65
|
+
title: params.title,
|
|
66
|
+
height,
|
|
67
|
+
prepared: true,
|
|
68
|
+
...(missing ? { missingImages: missing } : {}),
|
|
69
|
+
html: params.html,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=tool.js.map
|
|
@@ -19,6 +19,7 @@ import helpExtension from "./help/index.js";
|
|
|
19
19
|
import herdrExtension from "./herdr/index.js";
|
|
20
20
|
import historySearchExtension from "./history-search/index.js";
|
|
21
21
|
import hooksExtension from "./hooks/index.js";
|
|
22
|
+
import htmlRenderExtension from "./html-render/index.js";
|
|
22
23
|
import imageGenExtension from "./imagegen/index.js";
|
|
23
24
|
import importReproExtension from "./import-repro.js";
|
|
24
25
|
import lookAtExtension from "./look-at/index.js";
|
|
@@ -103,6 +104,9 @@ export const builtinExtensions = [
|
|
|
103
104
|
{ id: "cache-keepalive", factory: cacheKeepAliveExtension },
|
|
104
105
|
{ id: "ttsr", factory: ttsrExtension },
|
|
105
106
|
{ id: "btw", factory: btwExtension },
|
|
107
|
+
// HTML page rendering for standalone senpi (TUI/local); the desktop thread reaches the
|
|
108
|
+
// same capability through the desktop's MCP server instead.
|
|
109
|
+
{ id: "html-render", factory: htmlRenderExtension },
|
|
106
110
|
// Provider-neutral account listing; sits before the provider lanes so their
|
|
107
111
|
// dedicated commands (claude-account, cursor accounts) keep their own names.
|
|
108
112
|
{ id: "account", factory: accountExtension },
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServerConfig } from "../config-schema.ts";
|
|
2
|
+
/** Local snapshot only: never migrates credentials, acquires a lock, or refreshes authentication. */
|
|
3
|
+
export declare function mcpCredentialIdentity(config: McpServerConfig, serverName: string, agentDir: string | undefined, env: Record<string, string | undefined> | undefined): string | undefined;
|
|
4
|
+
//# sourceMappingURL=catalog-identity.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolveAuthMode } from "./context.js";
|
|
4
|
+
import { McpTokenStore } from "./token-store.js";
|
|
5
|
+
/** Local snapshot only: never migrates credentials, acquires a lock, or refreshes authentication. */
|
|
6
|
+
export function mcpCredentialIdentity(config, serverName, agentDir, env) {
|
|
7
|
+
const mode = resolveAuthMode(config);
|
|
8
|
+
if (mode === "none")
|
|
9
|
+
return "none";
|
|
10
|
+
if (mode === "bearer") {
|
|
11
|
+
const name = config.bearerTokenEnv;
|
|
12
|
+
if (name === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
const token = env?.[name] ?? process.env[name];
|
|
15
|
+
if (token === undefined || token.length === 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
return createHash("sha256")
|
|
18
|
+
.update(JSON.stringify(["mcp-catalog-bearer", token]))
|
|
19
|
+
.digest("hex");
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const store = new McpTokenStore({ serverName, serverUrl: config.url ?? "", agentDir });
|
|
23
|
+
const value = JSON.parse(readFileSync(store.tokensPath, "utf8"));
|
|
24
|
+
if (typeof value !== "object" || value === null || !("accessToken" in value))
|
|
25
|
+
return undefined;
|
|
26
|
+
if (typeof value.accessToken !== "string" || value.accessToken.length === 0)
|
|
27
|
+
return undefined;
|
|
28
|
+
const refreshToken = "refreshToken" in value ? value.refreshToken : undefined;
|
|
29
|
+
if (refreshToken !== undefined && typeof refreshToken !== "string")
|
|
30
|
+
return undefined;
|
|
31
|
+
return createHash("sha256")
|
|
32
|
+
.update(JSON.stringify(["mcp-catalog-oauth", value.accessToken, refreshToken]))
|
|
33
|
+
.digest("hex");
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof Error)
|
|
37
|
+
return undefined;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=catalog-identity.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { open, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { safeDelay } from "../wrap.js";
|
|
3
|
+
/** Shares the existing O_EXCL file with synchronous migrators without blocking the event loop. */
|
|
4
|
+
export async function acquireLegacyLock(path, stale) {
|
|
5
|
+
const deadline = Date.now() + stale;
|
|
6
|
+
for (;;) {
|
|
7
|
+
try {
|
|
8
|
+
const file = await open(path, "wx", 0o600);
|
|
9
|
+
return async () => {
|
|
10
|
+
try {
|
|
11
|
+
await file.close();
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
await rm(path, { force: true });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST")
|
|
20
|
+
throw error;
|
|
21
|
+
try {
|
|
22
|
+
const age = Date.now() - (await stat(path)).mtimeMs;
|
|
23
|
+
if (age >= stale) {
|
|
24
|
+
await rm(path, { force: true });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
30
|
+
continue;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
if (Date.now() >= deadline)
|
|
34
|
+
throw new Error("Legacy OAuth migration lock acquisition timed out");
|
|
35
|
+
await safeDelay(20);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=legacy-lock.js.map
|