@herbertgao/pi-extensions 2026.9.9 → 2026.9.11
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/README.md +1 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -2
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +427 -120
- package/node_modules/@herbertgao/pi-subagents/docs/rpc.md +184 -0
- package/node_modules/@herbertgao/pi-subagents/docs/workflows.md +466 -0
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +6 -6
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/compose.js +52 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/fan-out-audit.js +56 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/gated-fix.js +60 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/lib/count-child.js +30 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/review-panel.js +68 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/structured-findings.js +81 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +12 -10
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +52 -12
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +837 -146
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +213 -39
- package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +73 -14
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +101 -47
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +2249 -914
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +13 -0
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +215 -0
- package/node_modules/@herbertgao/pi-subagents/src/mention.ts +147 -0
- package/node_modules/@herbertgao/pi-subagents/src/model-resolver.ts +9 -1
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +40 -26
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +18 -8
- package/node_modules/@herbertgao/pi-subagents/src/prompts.ts +46 -9
- package/node_modules/@herbertgao/pi-subagents/src/schedule.ts +21 -16
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +137 -7
- package/node_modules/@herbertgao/pi-subagents/src/structured-output.ts +136 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +126 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-mention.ts +274 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +20 -5
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +10 -4
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +167 -22
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-card.ts +555 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-dialog.ts +1304 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-menu.ts +226 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +122 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/entry.ts +47 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/host.ts +463 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/journal.ts +164 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +142 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/meta.ts +401 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/progress.ts +622 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/runtime.ts +1399 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/saved.ts +230 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/task.ts +333 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/tool-description.ts +200 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/worker-source.ts +781 -0
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +97 -95
- package/node_modules/@herbertgao/pi-subagents/src/xml.ts +13 -0
- package/node_modules/@herbertgao/resume-from/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +4 -4
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +6 -6
- package/node_modules/@herbertgao/sol-pi/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/dist/index.ts +39 -89
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -87
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +8 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +40 -37
- package/node_modules/@narumitw/pi-caffeinate/README.md +21 -66
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +10 -41
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +50 -51
- package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +637 -663
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +114 -120
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +29 -29
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +108 -126
- package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +124 -128
- package/node_modules/pi-multi-account/CHANGELOG.md +1209 -0
- package/node_modules/pi-multi-account/CONTRIBUTING.md +61 -0
- package/node_modules/pi-multi-account/LICENSE +21 -0
- package/node_modules/pi-multi-account/README.md +197 -0
- package/node_modules/pi-multi-account/SECURITY.md +27 -0
- package/node_modules/pi-multi-account/auth-file-transaction.ts +56 -0
- package/node_modules/pi-multi-account/child-usability.ts +233 -0
- package/node_modules/pi-multi-account/compaction-summary.ts +32 -0
- package/node_modules/pi-multi-account/completion-route-planner.ts +224 -0
- package/node_modules/pi-multi-account/context-guard.ts +420 -0
- package/node_modules/pi-multi-account/cursor/LICENSE +21 -0
- package/node_modules/pi-multi-account/cursor/NOTICE +2 -0
- package/node_modules/pi-multi-account/cursor/auth.ts +165 -0
- package/node_modules/pi-multi-account/cursor/bridge-handle.ts +155 -0
- package/node_modules/pi-multi-account/cursor/conversation-registry.ts +104 -0
- package/node_modules/pi-multi-account/cursor/cursor-models-raw.json +611 -0
- package/node_modules/pi-multi-account/cursor/cursor-shared.ts +192 -0
- package/node_modules/pi-multi-account/cursor/h2-bridge.mjs +175 -0
- package/node_modules/pi-multi-account/cursor/index.ts +572 -0
- package/node_modules/pi-multi-account/cursor/message-parsing.ts +323 -0
- package/node_modules/pi-multi-account/cursor/prompt-usage.ts +53 -0
- package/node_modules/pi-multi-account/cursor/proto/agent_pb.ts +15294 -0
- package/node_modules/pi-multi-account/cursor/proxy.ts +2510 -0
- package/node_modules/pi-multi-account/cursor/session-lifecycle.ts +40 -0
- package/node_modules/pi-multi-account/cursor/sse-keepalive.ts +24 -0
- package/node_modules/pi-multi-account/cursor/stream-lifecycle.ts +193 -0
- package/node_modules/pi-multi-account/cursor/upstream-watchdog.ts +88 -0
- package/node_modules/pi-multi-account/cursor-bridge.ts +240 -0
- package/node_modules/pi-multi-account/cursor-model-name.ts +12 -0
- package/node_modules/pi-multi-account/index.ts +11825 -0
- package/node_modules/pi-multi-account/model-catalog.ts +354 -0
- package/node_modules/pi-multi-account/package.json +101 -0
- package/node_modules/pi-multi-account/pi-contract.ts +281 -0
- package/node_modules/pi-multi-account/provider-payload-stream.ts +44 -0
- package/node_modules/pi-multi-account/provider-priority.ts +189 -0
- package/node_modules/pi-multi-account/slot-proxy-auth.ts +167 -0
- package/node_modules/pi-multi-account/slot-proxy.ts +344 -0
- package/node_modules/pi-multi-account/state-file-transaction.ts +67 -0
- package/node_modules/pi-multi-account/usage.ts +1099 -0
- package/node_modules/pi-typesafe/README.md +6 -2
- package/node_modules/pi-typesafe/dist/client.d.ts +11 -0
- package/node_modules/pi-typesafe/dist/client.js +45 -10
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/package.json +2 -2
- package/node_modules/pi-web-access/CHANGELOG.md +36 -0
- package/node_modules/pi-web-access/README.md +75 -18
- package/node_modules/pi-web-access/anysearch.ts +4 -15
- package/node_modules/pi-web-access/bocha.ts +3 -22
- package/node_modules/pi-web-access/brave.ts +3 -21
- package/node_modules/pi-web-access/brightdata.ts +5 -32
- package/node_modules/pi-web-access/content-find.ts +168 -53
- package/node_modules/pi-web-access/curator-page.ts +4 -1
- package/node_modules/pi-web-access/curator-run.ts +2 -1
- package/node_modules/pi-web-access/curator-server.ts +1 -0
- package/node_modules/pi-web-access/dist/index.js +24620 -0
- package/node_modules/pi-web-access/domain-filter-normalization.ts +14 -0
- package/node_modules/pi-web-access/duckduckgo.ts +3 -21
- package/node_modules/pi-web-access/extract.ts +3 -1
- package/node_modules/pi-web-access/firecrawl.ts +5 -29
- package/node_modules/pi-web-access/gemini-search.ts +81 -32
- package/node_modules/pi-web-access/index.ts +149 -148
- package/node_modules/pi-web-access/jina-search.ts +4 -15
- package/node_modules/pi-web-access/kagi.ts +4 -13
- package/node_modules/pi-web-access/kimi-search.ts +5 -30
- package/node_modules/pi-web-access/mistral-search.ts +1 -15
- package/node_modules/pi-web-access/ollama.ts +2 -7
- package/node_modules/pi-web-access/openai-search.ts +174 -36
- package/node_modules/pi-web-access/opencode-session-headers.ts +24 -0
- package/node_modules/pi-web-access/package.json +10 -4
- package/node_modules/pi-web-access/page-query.ts +10 -2
- package/node_modules/pi-web-access/parallel.ts +1 -15
- package/node_modules/pi-web-access/pdf-extract.ts +3 -0
- package/node_modules/pi-web-access/querit.ts +5 -29
- package/node_modules/pi-web-access/search-answer-formatting.ts +11 -0
- package/node_modules/pi-web-access/search-result-count-normalization.ts +4 -0
- package/node_modules/pi-web-access/search1api.ts +5 -29
- package/node_modules/pi-web-access/searchinfinity.ts +5 -29
- package/node_modules/pi-web-access/searxng.ts +3 -21
- package/node_modules/pi-web-access/serpapi.ts +5 -28
- package/node_modules/pi-web-access/serpbase.ts +3 -22
- package/node_modules/pi-web-access/serpdive.ts +3 -21
- package/node_modules/pi-web-access/serper.ts +5 -28
- package/node_modules/pi-web-access/serply.ts +197 -0
- package/node_modules/pi-web-access/source-check.ts +11 -47
- package/node_modules/pi-web-access/summary-review.ts +7 -3
- package/node_modules/pi-web-access/tavily.ts +3 -21
- package/node_modules/pi-web-access/tinyfish.ts +5 -29
- package/node_modules/pi-web-access/utils.ts +9 -1
- package/node_modules/pi-web-access/valyu.ts +5 -28
- package/node_modules/pi-web-access/xai-search.ts +1 -15
- package/node_modules/pi-web-access/xcrawl.ts +5 -32
- package/package.json +17 -11
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
type SessionContext = { sessionManager: { getSessionId(): string; getLeafId?: () => string } };
|
|
4
|
+
|
|
5
|
+
export function registerSessionLifecycleHooks(
|
|
6
|
+
pi: ExtensionAPI,
|
|
7
|
+
dependencies: {
|
|
8
|
+
cleanupSessionState: (sessionId: string) => void;
|
|
9
|
+
/** Start a new Cursor-side conversation, keeping the session alive. */
|
|
10
|
+
resetConversationForSession?: (sessionId: string) => void;
|
|
11
|
+
debug?: (event: string, data: Record<string, unknown>) => void;
|
|
12
|
+
},
|
|
13
|
+
): void {
|
|
14
|
+
const cleanupCurrentSession = (_event: unknown, ctx: SessionContext) => {
|
|
15
|
+
dependencies.debug?.("session.cleanup_hook", {
|
|
16
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
17
|
+
leafId: ctx.sessionManager.getLeafId?.(),
|
|
18
|
+
});
|
|
19
|
+
dependencies.cleanupSessionState(ctx.sessionManager.getSessionId());
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
pi.on("session_before_switch", cleanupCurrentSession as any);
|
|
23
|
+
pi.on("session_before_fork", cleanupCurrentSession as any);
|
|
24
|
+
pi.on("session_before_tree", cleanupCurrentSession as any);
|
|
25
|
+
// Compaction is the one event that changes what the conversation IS without ending it.
|
|
26
|
+
// Cursor never sees Pi's compacted message list while it holds a checkpoint, so unless the
|
|
27
|
+
// conversation is restarted here, the model keeps the full pre-compaction context and keeps
|
|
28
|
+
// reporting its size — which sends Pi straight into compacting again on the next turn.
|
|
29
|
+
pi.on("session_compact", ((_event: unknown, ctx: SessionContext) => {
|
|
30
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
31
|
+
dependencies.debug?.("session.compact_hook", {
|
|
32
|
+
sessionId,
|
|
33
|
+
leafId: ctx.sessionManager.getLeafId?.(),
|
|
34
|
+
});
|
|
35
|
+
dependencies.resetConversationForSession?.(sessionId);
|
|
36
|
+
}) as any);
|
|
37
|
+
pi.on("session_shutdown", ((event: unknown, ctx: SessionContext) => {
|
|
38
|
+
cleanupCurrentSession(event, ctx);
|
|
39
|
+
}) as any);
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
|
|
3
|
+
/** Flush headers before the first model token, then send SSE comments during pauses. */
|
|
4
|
+
export const SSE_KEEPALIVE_INTERVAL_MS = 15_000;
|
|
5
|
+
|
|
6
|
+
/** Open a streaming SSE response and keep it warm. Returns the function that stops the keepalive. */
|
|
7
|
+
export function startSSEResponse(res: ServerResponse, keepaliveIntervalMs = SSE_KEEPALIVE_INTERVAL_MS): () => void {
|
|
8
|
+
res.writeHead(200, {
|
|
9
|
+
"Content-Type": "text/event-stream",
|
|
10
|
+
"Cache-Control": "no-cache",
|
|
11
|
+
"Connection": "keep-alive",
|
|
12
|
+
});
|
|
13
|
+
res.flushHeaders();
|
|
14
|
+
|
|
15
|
+
const timer = setInterval(() => {
|
|
16
|
+
if (res.writableEnded || res.destroyed) {
|
|
17
|
+
clearInterval(timer);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
res.write(": keepalive\n\n");
|
|
21
|
+
}, keepaliveIntervalMs);
|
|
22
|
+
|
|
23
|
+
return () => clearInterval(timer);
|
|
24
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor's gRPC agent is not an OpenAI chat completion. This module is the
|
|
3
|
+
* contract that makes one Cursor Run look like one Pi provider turn:
|
|
4
|
+
*
|
|
5
|
+
* - a turn ends when Cursor says so (`turnEnded`), not when the TCP stream happens
|
|
6
|
+
* to close;
|
|
7
|
+
* - only user-visible tokens (text, thinking, token deltas) and Pi-bound tool
|
|
8
|
+
* calls count as forward progress for the stall watchdog;
|
|
9
|
+
* - heartbeats, checkpoints, blob fetches, rejected native tools and mid-turn
|
|
10
|
+
* questions do not keep the spinner alive.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const NATIVE_TOOL_UNAVAILABLE =
|
|
14
|
+
"Tool not available in this environment. Use the MCP tools provided instead.";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Cursor often emits sibling `read`/`shell` execs as separate gRPC frames a few
|
|
18
|
+
* milliseconds apart. Closing the OpenAI stream on the first one leaves the rest
|
|
19
|
+
* unanswered and Pi stuck on Working. Wait this long after the last Pi-bound exec
|
|
20
|
+
* so they share one `tool_calls` pause.
|
|
21
|
+
*/
|
|
22
|
+
export const TOOL_CALL_COALESCE_MS = 75;
|
|
23
|
+
|
|
24
|
+
/** `PI_CURSOR_TOOL_COALESCE_MS` overrides the window; non-positive or garbage falls back. */
|
|
25
|
+
export function resolveToolCallCoalesceMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
26
|
+
const raw = env.PI_CURSOR_TOOL_COALESCE_MS?.trim();
|
|
27
|
+
if (raw === undefined || raw === "") return TOOL_CALL_COALESCE_MS;
|
|
28
|
+
const parsed = Number(raw);
|
|
29
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 2_147_483_647) return TOOL_CALL_COALESCE_MS;
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type CursorFrameKind =
|
|
34
|
+
| "heartbeat"
|
|
35
|
+
| "visible"
|
|
36
|
+
| "housekeeping"
|
|
37
|
+
| "turnEnded"
|
|
38
|
+
| "mcpExec"
|
|
39
|
+
| "nativeExec"
|
|
40
|
+
| "interactionQuery"
|
|
41
|
+
| "execControl"
|
|
42
|
+
| "unknown";
|
|
43
|
+
|
|
44
|
+
export type CursorFrameClass = {
|
|
45
|
+
kind: CursorFrameKind;
|
|
46
|
+
/** Reset the stall watchdog. Housekeeping must never do this. */
|
|
47
|
+
countsAsProgress: boolean;
|
|
48
|
+
/** Finish the OpenAI stream with stop, matching every other provider. */
|
|
49
|
+
completesTurn: boolean;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const VISIBLE_UPDATES = new Set(["textDelta", "thinkingDelta", "tokenDelta"]);
|
|
53
|
+
|
|
54
|
+
export function classifyCursorFrame(input: {
|
|
55
|
+
messageCase?: string;
|
|
56
|
+
updateCase?: string;
|
|
57
|
+
execCase?: string;
|
|
58
|
+
}): CursorFrameClass {
|
|
59
|
+
const messageCase = input.messageCase;
|
|
60
|
+
if (!messageCase) return { kind: "unknown", countsAsProgress: false, completesTurn: false };
|
|
61
|
+
|
|
62
|
+
if (messageCase === "interactionUpdate") {
|
|
63
|
+
const updateCase = input.updateCase;
|
|
64
|
+
if (!updateCase || updateCase === "heartbeat") {
|
|
65
|
+
return { kind: "heartbeat", countsAsProgress: false, completesTurn: false };
|
|
66
|
+
}
|
|
67
|
+
if (updateCase === "turnEnded") {
|
|
68
|
+
return { kind: "turnEnded", countsAsProgress: false, completesTurn: true };
|
|
69
|
+
}
|
|
70
|
+
if (VISIBLE_UPDATES.has(updateCase)) {
|
|
71
|
+
return { kind: "visible", countsAsProgress: true, completesTurn: false };
|
|
72
|
+
}
|
|
73
|
+
return { kind: "housekeeping", countsAsProgress: false, completesTurn: false };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (messageCase === "execServerMessage") {
|
|
77
|
+
if (input.execCase === "mcpArgs") {
|
|
78
|
+
return { kind: "mcpExec", countsAsProgress: true, completesTurn: false };
|
|
79
|
+
}
|
|
80
|
+
if (input.execCase === "requestContextArgs" || input.execCase === "diagnosticsArgs") {
|
|
81
|
+
return { kind: "housekeeping", countsAsProgress: false, completesTurn: false };
|
|
82
|
+
}
|
|
83
|
+
return { kind: "nativeExec", countsAsProgress: false, completesTurn: false };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (messageCase === "kvServerMessage" || messageCase === "conversationCheckpointUpdate") {
|
|
87
|
+
return { kind: "housekeeping", countsAsProgress: false, completesTurn: false };
|
|
88
|
+
}
|
|
89
|
+
if (messageCase === "interactionQuery") {
|
|
90
|
+
return { kind: "interactionQuery", countsAsProgress: false, completesTurn: false };
|
|
91
|
+
}
|
|
92
|
+
if (messageCase === "execServerControlMessage") {
|
|
93
|
+
return { kind: "execControl", countsAsProgress: false, completesTurn: false };
|
|
94
|
+
}
|
|
95
|
+
return { kind: "unknown", countsAsProgress: false, completesTurn: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type NativeExecForward = {
|
|
99
|
+
toolName: string;
|
|
100
|
+
args: Record<string, unknown>;
|
|
101
|
+
resultCase: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function quote(value: string): string {
|
|
105
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function asString(value: unknown): string {
|
|
109
|
+
return typeof value === "string" ? value : String(value ?? "");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Cursor's own read/shell/write tools are not a Pi sandbox. Map them onto the
|
|
114
|
+
* MCP tools Pi already exposed for this turn so the model gets real results
|
|
115
|
+
* instead of an infinite reject loop.
|
|
116
|
+
*/
|
|
117
|
+
export function mapNativeExecToPiTool(
|
|
118
|
+
execCase: string,
|
|
119
|
+
args: Record<string, unknown>,
|
|
120
|
+
availableTools: Iterable<string>,
|
|
121
|
+
): NativeExecForward | undefined {
|
|
122
|
+
const tools = availableTools instanceof Set ? availableTools : new Set(availableTools);
|
|
123
|
+
const has = (name: string) => tools.has(name);
|
|
124
|
+
const bash = (command: string, resultCase: string): NativeExecForward | undefined =>
|
|
125
|
+
has("bash") ? { toolName: "bash", args: { command }, resultCase } : undefined;
|
|
126
|
+
|
|
127
|
+
switch (execCase) {
|
|
128
|
+
case "readArgs":
|
|
129
|
+
if (has("read")) return { toolName: "read", args: { path: asString(args.path) }, resultCase: "readResult" };
|
|
130
|
+
return bash(`cat -- ${quote(asString(args.path))}`, "readResult");
|
|
131
|
+
case "writeArgs":
|
|
132
|
+
if (has("write")) {
|
|
133
|
+
return {
|
|
134
|
+
toolName: "write",
|
|
135
|
+
args: { path: asString(args.path), content: asString(args.fileText ?? args.content) },
|
|
136
|
+
resultCase: "writeResult",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
case "grepArgs":
|
|
141
|
+
if (has("grep")) {
|
|
142
|
+
const forwarded: Record<string, unknown> = { pattern: asString(args.pattern) };
|
|
143
|
+
if (args.path) forwarded.path = args.path;
|
|
144
|
+
if (args.glob) forwarded.glob = args.glob;
|
|
145
|
+
return { toolName: "grep", args: forwarded, resultCase: "grepResult" };
|
|
146
|
+
}
|
|
147
|
+
return bash(
|
|
148
|
+
`rg --line-number --color never ${quote(asString(args.pattern))}${args.path ? ` -- ${quote(asString(args.path))}` : ""}`,
|
|
149
|
+
"grepResult",
|
|
150
|
+
);
|
|
151
|
+
case "lsArgs":
|
|
152
|
+
if (has("ls")) return { toolName: "ls", args: { path: asString(args.path) }, resultCase: "lsResult" };
|
|
153
|
+
return bash(`ls -la -- ${quote(asString(args.path))}`, "lsResult");
|
|
154
|
+
case "deleteArgs":
|
|
155
|
+
return bash(`rm -f -- ${quote(asString(args.path))}`, "deleteResult");
|
|
156
|
+
case "shellArgs":
|
|
157
|
+
case "shellStreamArgs":
|
|
158
|
+
if (has("bash")) {
|
|
159
|
+
return {
|
|
160
|
+
toolName: "bash",
|
|
161
|
+
args: {
|
|
162
|
+
command: asString(args.command),
|
|
163
|
+
...(args.workingDirectory ? { cwd: asString(args.workingDirectory) } : {}),
|
|
164
|
+
},
|
|
165
|
+
resultCase: execCase === "shellStreamArgs" ? "shellStream" : "shellResult",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
default:
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function interactionQueryResultCase(queryCase: string | undefined): string | undefined {
|
|
175
|
+
switch (queryCase) {
|
|
176
|
+
case "webSearchRequestQuery":
|
|
177
|
+
return "webSearchRequestResponse";
|
|
178
|
+
case "askQuestionInteractionQuery":
|
|
179
|
+
return "askQuestionInteractionResponse";
|
|
180
|
+
case "switchModeRequestQuery":
|
|
181
|
+
return "switchModeRequestResponse";
|
|
182
|
+
case "exaSearchRequestQuery":
|
|
183
|
+
return "exaSearchRequestResponse";
|
|
184
|
+
case "exaFetchRequestQuery":
|
|
185
|
+
return "exaFetchRequestResponse";
|
|
186
|
+
case "createPlanRequestQuery":
|
|
187
|
+
return "createPlanRequestResponse";
|
|
188
|
+
case "setupVmEnvironmentArgs":
|
|
189
|
+
return "setupVmEnvironmentResult";
|
|
190
|
+
default:
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE keepalives preserve Pi's client connection even when Cursor stops making progress.
|
|
3
|
+
* Two clocks are intentionally separate:
|
|
4
|
+
* - every complete decoded Cursor frame proves the HTTP/2 transport is alive;
|
|
5
|
+
* - only user-visible tokens and Pi-bound tools prove useful model progress.
|
|
6
|
+
* A single one-minute semantic clock killed healthy long-reasoning Runs and falsely looked
|
|
7
|
+
* like exhausted Cursor quota to multi-account.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A Run producing no complete upstream frame is treated as dead after one minute. */
|
|
11
|
+
export const TRANSPORT_STALL_TIMEOUT_MS = 60_000;
|
|
12
|
+
/** A live transport may reason quietly, but housekeeping cannot keep it alive forever. */
|
|
13
|
+
export const UPSTREAM_STALL_TIMEOUT_MS = 5 * 60_000;
|
|
14
|
+
|
|
15
|
+
function resolveTimeout(raw: string | undefined, fallback: number): number {
|
|
16
|
+
const value = raw?.trim();
|
|
17
|
+
if (value === undefined || value === "") return fallback;
|
|
18
|
+
const parsed = Number(value);
|
|
19
|
+
// Node reduces delays above its signed 32-bit limit to one millisecond.
|
|
20
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 2_147_483_647) return fallback;
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `PI_CURSOR_TRANSPORT_STALL_MS` overrides the no-frame window; `0` disables it. */
|
|
25
|
+
export function resolveTransportStallTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
26
|
+
return resolveTimeout(env.PI_CURSOR_TRANSPORT_STALL_MS, TRANSPORT_STALL_TIMEOUT_MS);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `PI_CURSOR_UPSTREAM_STALL_MS` overrides the useful-output window; `0` disables it. */
|
|
30
|
+
export function resolveUpstreamStallTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
31
|
+
return resolveTimeout(env.PI_CURSOR_UPSTREAM_STALL_MS, UPSTREAM_STALL_TIMEOUT_MS);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatStallDuration(ms: number): string {
|
|
35
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
36
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
37
|
+
const seconds = totalSeconds % 60;
|
|
38
|
+
if (minutes === 0) return `${seconds}s`;
|
|
39
|
+
if (seconds === 0) return `${minutes}m`;
|
|
40
|
+
return `${minutes}m ${seconds}s`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface UpstreamWatchdog {
|
|
44
|
+
/** Record decoded upstream progress; the stall deadline moves forward. */
|
|
45
|
+
touch(): void;
|
|
46
|
+
/** Retire the watchdog. Idempotent; later touches are ignored. */
|
|
47
|
+
stop(): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Arm a stall watchdog. `onStall` runs at most once, with how long upstream had been silent.
|
|
52
|
+
* A non-positive `timeoutMs` produces an inert watchdog.
|
|
53
|
+
*/
|
|
54
|
+
export function startUpstreamWatchdog(onStall: (silentForMs: number) => void, timeoutMs: number): UpstreamWatchdog {
|
|
55
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
56
|
+
let stopped = timeoutMs <= 0;
|
|
57
|
+
let lastProgressAt = performance.now();
|
|
58
|
+
|
|
59
|
+
const arm = (delayMs = timeoutMs) => {
|
|
60
|
+
if (stopped) return;
|
|
61
|
+
if (timer) clearTimeout(timer);
|
|
62
|
+
timer = setTimeout(() => {
|
|
63
|
+
if (stopped) return;
|
|
64
|
+
const silentForMs = performance.now() - lastProgressAt;
|
|
65
|
+
// Timers may wake early. Recheck a monotonic deadline before ending the stream.
|
|
66
|
+
if (silentForMs < timeoutMs) { arm(Math.ceil(timeoutMs - silentForMs)); return; }
|
|
67
|
+
stopped = true;
|
|
68
|
+
timer = undefined;
|
|
69
|
+
onStall(silentForMs);
|
|
70
|
+
}, delayMs);
|
|
71
|
+
timer.unref?.();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
arm();
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
touch() {
|
|
78
|
+
if (stopped) return;
|
|
79
|
+
lastProgressAt = performance.now();
|
|
80
|
+
arm();
|
|
81
|
+
},
|
|
82
|
+
stop() {
|
|
83
|
+
stopped = true;
|
|
84
|
+
if (timer) clearTimeout(timer);
|
|
85
|
+
timer = undefined;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
const VENDORED_CURSOR_ROOT = join(dirname(fileURLToPath(import.meta.url)), "cursor");
|
|
7
|
+
|
|
8
|
+
export const CURSOR_BASE = "cursor";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The one bearer value the vendored proxy treats as "no usable token on this request",
|
|
12
|
+
* falling back to the base slot's stored credential (see `bearerFromRequest` in
|
|
13
|
+
* cursor/cursor-shared.ts). It is a marker, not a secret: the proxy listens on
|
|
14
|
+
* 127.0.0.1 only and supplies the real Cursor token itself.
|
|
15
|
+
*
|
|
16
|
+
* It exists so a slot published into pi's static models.json is USABLE by a bare
|
|
17
|
+
* `pi -p` child, which cannot read the OAuth token this extension holds. Locked to the
|
|
18
|
+
* vendored provider by a test — if the provider ever stops recognising this exact
|
|
19
|
+
* string, publishing it would silently send a bogus token instead.
|
|
20
|
+
*/
|
|
21
|
+
export const CURSOR_PROXY_PLACEHOLDER_KEY = "cursor-proxy";
|
|
22
|
+
|
|
23
|
+
export function isCursorProviderId(id: string): boolean {
|
|
24
|
+
return id === CURSOR_BASE || /^cursor-account-\d+$/.test(id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Where Cursor implementation is loaded from.
|
|
29
|
+
*
|
|
30
|
+
* Production always uses the copy vendored next to this file (`./cursor`).
|
|
31
|
+
* `PI_CURSOR_PROVIDER_ROOT` is a test seam only: the suite points it at a stub
|
|
32
|
+
* or an empty directory so it can exercise load failures without starting the
|
|
33
|
+
* real proxy.
|
|
34
|
+
*/
|
|
35
|
+
export function getCursorProviderRoot(): string {
|
|
36
|
+
return process.env.PI_CURSOR_PROVIDER_ROOT || VENDORED_CURSOR_ROOT;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isCursorProviderInstalled(): boolean {
|
|
40
|
+
return existsSync(join(getCursorProviderRoot(), "cursor-shared.ts"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type CursorShared = {
|
|
44
|
+
ensureCursorProxy: (
|
|
45
|
+
resolve: (providerId: string) => Promise<string>,
|
|
46
|
+
) => Promise<number>;
|
|
47
|
+
registerCursorProvider: (...args: any[]) => void;
|
|
48
|
+
FALLBACK_MODELS: unknown[];
|
|
49
|
+
discoverCursorModels?: (accessToken: string) => Promise<unknown[]>;
|
|
50
|
+
};
|
|
51
|
+
type CursorIndex = {
|
|
52
|
+
registerSessionLifecycleCleanup?: (pi: ExtensionAPI) => void;
|
|
53
|
+
cleanupControllerSession?: (sessionId: string) => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let sharedModPromise: Promise<CursorShared | undefined> | undefined;
|
|
57
|
+
let indexMod: CursorIndex | undefined;
|
|
58
|
+
let proxyPort: number | undefined;
|
|
59
|
+
let loadAttempt = 0;
|
|
60
|
+
|
|
61
|
+
function loadSpecifier(entry: string): string {
|
|
62
|
+
return loadAttempt === 0
|
|
63
|
+
? entry
|
|
64
|
+
: `${pathToFileURL(entry).href}?pi-multi-account-retry=${loadAttempt}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Cache the in-flight PROMISE, not the settled module.
|
|
69
|
+
*
|
|
70
|
+
* Discovery fires this without awaiting while `session_start` awaits its own call, so both
|
|
71
|
+
* used to reach the import at once. A second concurrent import of the same module observes it
|
|
72
|
+
* mid-initialization: hoisted functions are already callable while its `let` state is still in
|
|
73
|
+
* the temporal dead zone, which surfaced as "Cannot access 'tokenResolver' before
|
|
74
|
+
* initialization" and cost the session every Cursor account. One shared promise means the
|
|
75
|
+
* module is imported exactly once, no matter how many callers race.
|
|
76
|
+
*/
|
|
77
|
+
async function loadCursorModules(): Promise<CursorShared | undefined> {
|
|
78
|
+
sharedModPromise ??= (async () => {
|
|
79
|
+
const entry = join(getCursorProviderRoot(), "cursor-shared.ts");
|
|
80
|
+
if (!existsSync(entry)) return undefined;
|
|
81
|
+
try {
|
|
82
|
+
const shared = (await import(
|
|
83
|
+
/* @vite-ignore */ loadSpecifier(entry)
|
|
84
|
+
)) as CursorShared;
|
|
85
|
+
const indexEntry = join(getCursorProviderRoot(), "index.ts");
|
|
86
|
+
if (existsSync(indexEntry)) {
|
|
87
|
+
indexMod = (await import(
|
|
88
|
+
/* @vite-ignore */ loadSpecifier(indexEntry)
|
|
89
|
+
)) as CursorIndex;
|
|
90
|
+
}
|
|
91
|
+
return shared;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
sharedModPromise = undefined;
|
|
94
|
+
loadAttempt++;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
return sharedModPromise;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type AuthEntry = {
|
|
102
|
+
type?: string;
|
|
103
|
+
access?: string;
|
|
104
|
+
refresh?: string;
|
|
105
|
+
expires?: number;
|
|
106
|
+
key?: string;
|
|
107
|
+
accountId?: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export async function setupCursorSubscription(
|
|
111
|
+
pi: ExtensionAPI,
|
|
112
|
+
options: {
|
|
113
|
+
readAuth: () => Record<string, AuthEntry>;
|
|
114
|
+
rejectDuplicateLogin?: (slot: string, creds: AuthEntry) => AuthEntry;
|
|
115
|
+
slotIds: string[];
|
|
116
|
+
notify?: (message: string, level: "info" | "warning") => void;
|
|
117
|
+
log?: (kind: string, data: Record<string, unknown>) => void;
|
|
118
|
+
/**
|
|
119
|
+
* Host hook: provision each Cursor slot into Pi's static models.json so a
|
|
120
|
+
* bare `pi -p` child (no extensions) resolves cursor/* through the running
|
|
121
|
+
* proxy. Called after the proxy is up and again when the real catalog lands.
|
|
122
|
+
* Pass model objects (or ids); the host writes Pi-schema objects, never strings.
|
|
123
|
+
*/
|
|
124
|
+
onProvision?: (slotIds: string[], port: number, models: unknown[]) => void;
|
|
125
|
+
},
|
|
126
|
+
): Promise<number | undefined> {
|
|
127
|
+
const mod = await loadCursorModules();
|
|
128
|
+
if (!mod) {
|
|
129
|
+
options.notify?.(
|
|
130
|
+
`pi-multi-account: Cursor support is missing from this install (expected ${getCursorProviderRoot()}). Everything else keeps working; set "includeCursor": false in the config to silence this.`,
|
|
131
|
+
"warning",
|
|
132
|
+
);
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
if (indexMod?.registerSessionLifecycleCleanup) {
|
|
136
|
+
indexMod.registerSessionLifecycleCleanup(pi);
|
|
137
|
+
}
|
|
138
|
+
const resolveAccessToken = async (providerId: string) => {
|
|
139
|
+
const entry = options.readAuth()[providerId];
|
|
140
|
+
if (!entry || entry.type !== "oauth") return "";
|
|
141
|
+
return typeof entry.access === "string" ? entry.access : "";
|
|
142
|
+
};
|
|
143
|
+
proxyPort = await mod.ensureCursorProxy(resolveAccessToken);
|
|
144
|
+
const ids = [...new Set([CURSOR_BASE, ...options.slotIds])];
|
|
145
|
+
options.onProvision?.(ids, proxyPort, mod.FALLBACK_MODELS as unknown[]);
|
|
146
|
+
for (const id of ids) {
|
|
147
|
+
mod.registerCursorProvider(pi, id, proxyPort, mod.FALLBACK_MODELS, {
|
|
148
|
+
rejectDuplicateLogin: options.rejectDuplicateLogin,
|
|
149
|
+
onModelsDiscovered: (models: unknown[]) => {
|
|
150
|
+
mod.registerCursorProvider(pi, id, proxyPort!, models as any[], {
|
|
151
|
+
rejectDuplicateLogin: options.rejectDuplicateLogin,
|
|
152
|
+
});
|
|
153
|
+
options.onProvision?.(ids, proxyPort!, models);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
void (async () => {
|
|
158
|
+
if (typeof mod.discoverCursorModels !== "function") {
|
|
159
|
+
options.log?.("cursor_catalog", { outcome: "unsupported" });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
for (const id of ids) {
|
|
163
|
+
const entry = options.readAuth()[id];
|
|
164
|
+
if (!entry || entry.type !== "oauth" || !entry.access) continue;
|
|
165
|
+
try {
|
|
166
|
+
const models = await mod.discoverCursorModels(entry.access);
|
|
167
|
+
if (!models?.length) {
|
|
168
|
+
options.log?.("cursor_catalog", { outcome: "empty", provider: id });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
for (const slot of ids) {
|
|
172
|
+
mod.registerCursorProvider(pi, slot, proxyPort!, models as any[], {
|
|
173
|
+
rejectDuplicateLogin: options.rejectDuplicateLogin,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
options.onProvision?.(ids, proxyPort!, models as unknown[]);
|
|
177
|
+
options.log?.("cursor_catalog", {
|
|
178
|
+
outcome: "discovered",
|
|
179
|
+
provider: id,
|
|
180
|
+
models: models.length,
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
options.log?.("cursor_catalog", {
|
|
185
|
+
outcome: "error",
|
|
186
|
+
provider: id,
|
|
187
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
options.log?.("cursor_catalog", {
|
|
192
|
+
outcome: "unavailable",
|
|
193
|
+
reason: "no slot could read the catalog",
|
|
194
|
+
});
|
|
195
|
+
})().catch((error) => {
|
|
196
|
+
options.log?.("cursor_catalog", {
|
|
197
|
+
outcome: "crashed",
|
|
198
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
return proxyPort;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Refresh a Cursor OAuth credential through the VENDORED provider.
|
|
206
|
+
*
|
|
207
|
+
* This used to `import()` `~/.pi/agent/git/github.com/ndraiman/pi-cursor-provider/auth.ts`
|
|
208
|
+
* — a path that no longer exists for anyone (the provider is vendored into this
|
|
209
|
+
* extension, and upstream's repo never contained the file layout we expected).
|
|
210
|
+
* Every forced Cursor refresh therefore threw before it could refresh anything.
|
|
211
|
+
* Resolving through {@link getCursorProviderRoot} keeps the `PI_CURSOR_PROVIDER_ROOT`
|
|
212
|
+
* test seam working.
|
|
213
|
+
*/
|
|
214
|
+
/**
|
|
215
|
+
* Controller tasks are not Pi sessions, so the normal session_shutdown hook cannot clean their
|
|
216
|
+
* Cursor conversation state. The native controller provider calls this after each task request;
|
|
217
|
+
* the callback is deliberately a no-op when the optional Cursor provider is absent.
|
|
218
|
+
*/
|
|
219
|
+
export function cleanupCursorControllerSession(sessionId: string): void {
|
|
220
|
+
try {
|
|
221
|
+
indexMod?.cleanupControllerSession?.(sessionId);
|
|
222
|
+
} catch {
|
|
223
|
+
// Cleanup is best effort; it must not turn a settled provider result into a host error.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function refreshCursorCredentials(
|
|
228
|
+
refreshToken: string,
|
|
229
|
+
): Promise<{ access: string; refresh: string; expires: number }> {
|
|
230
|
+
const entry = join(getCursorProviderRoot(), "auth.ts");
|
|
231
|
+
if (!existsSync(entry)) {
|
|
232
|
+
throw new Error(`Cursor support is missing from this install (${entry})`);
|
|
233
|
+
}
|
|
234
|
+
const mod = (await import(/* @vite-ignore */ loadSpecifier(entry))) as {
|
|
235
|
+
refreshCursorToken: (
|
|
236
|
+
token: string,
|
|
237
|
+
) => Promise<{ access: string; refresh: string; expires: number }>;
|
|
238
|
+
};
|
|
239
|
+
return mod.refreshCursorToken(refreshToken);
|
|
240
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor catalogs name each effort variant ("Grok 4.6 Medium", "Grok 4.6 High").
|
|
3
|
+
* After we fold those variants into one model, that adjective must not appear in
|
|
4
|
+
* the picker or the powerline — thinking level is a separate Pi setting.
|
|
5
|
+
*/
|
|
6
|
+
const TRAILING_EFFORT =
|
|
7
|
+
/\s+(?:\((?:none|low|medium|high|xhigh|max|extra\s*high)\)|(?:None|Low|Medium|High|Extra High|xHigh|Max))\s*$/i;
|
|
8
|
+
|
|
9
|
+
export function cursorModelDisplayName(name: string): string {
|
|
10
|
+
const stripped = name.replace(TRAILING_EFFORT, "").trim();
|
|
11
|
+
return stripped || name;
|
|
12
|
+
}
|