pi-cursor-sdk 0.2.0 → 0.3.7
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 +73 -0
- package/README.md +55 -51
- package/dist/bundled-context-windows.js +39 -0
- package/dist/context-window-cache.js +92 -0
- package/dist/context.js +353 -0
- package/dist/cursor-active-tools.js +3 -0
- package/dist/cursor-agent-message-web-tools.js +165 -0
- package/dist/cursor-agents-context-registration.js +17 -0
- package/dist/cursor-agents-context.js +127 -0
- package/dist/cursor-api-key.js +34 -0
- package/dist/cursor-bridge-contract.js +27 -0
- package/dist/cursor-cloud-lifecycle.js +650 -0
- package/dist/cursor-cloud-local-state.js +460 -0
- package/dist/cursor-cloud-options.js +145 -0
- package/dist/cursor-cloud-reporting.js +222 -0
- package/dist/cursor-compact-tool-summary.js +75 -0
- package/dist/cursor-config.js +613 -0
- package/dist/cursor-context-tools.js +6 -0
- package/dist/cursor-display-only-trace.js +11 -0
- package/dist/cursor-display-text.js +18 -0
- package/dist/cursor-durable-fs.js +53 -0
- package/dist/cursor-edit-diff.js +13 -0
- package/dist/cursor-env-boolean.js +19 -0
- package/dist/cursor-fallback-models.generated.js +7760 -0
- package/dist/cursor-fallback-warning.js +15 -0
- package/dist/cursor-http1.js +45 -0
- package/dist/cursor-incomplete-tool-visibility.js +83 -0
- package/dist/cursor-live-run-accounting.js +55 -0
- package/dist/cursor-live-run-coordinator.js +409 -0
- package/dist/cursor-mcp-timeout-override.js +116 -0
- package/dist/cursor-model-lifecycle.js +32 -0
- package/dist/cursor-model.js +5 -0
- package/dist/cursor-native-replay-routing.js +35 -0
- package/dist/cursor-native-replay-trace.js +28 -0
- package/dist/cursor-native-tool-display-registration.js +102 -0
- package/dist/cursor-native-tool-display-replay.js +428 -0
- package/dist/cursor-native-tool-display-state.js +70 -0
- package/dist/cursor-native-tool-display-tools.js +123 -0
- package/dist/cursor-native-tool-names.js +11 -0
- package/dist/cursor-partial-content-emitter.js +130 -0
- package/dist/cursor-pi-context.js +30 -0
- package/dist/cursor-pi-tool-bridge-abort.js +114 -0
- package/dist/cursor-pi-tool-bridge-constants.js +6 -0
- package/dist/cursor-pi-tool-bridge-diagnostics.js +125 -0
- package/dist/cursor-pi-tool-bridge-env.js +18 -0
- package/dist/cursor-pi-tool-bridge-mcp.js +100 -0
- package/dist/cursor-pi-tool-bridge-run.js +355 -0
- package/dist/cursor-pi-tool-bridge-server.js +163 -0
- package/dist/cursor-pi-tool-bridge-snapshot.js +63 -0
- package/dist/cursor-pi-tool-bridge-types.js +1 -0
- package/dist/cursor-pi-tool-bridge.js +123 -0
- package/dist/cursor-provider-errors.js +311 -0
- package/dist/cursor-provider-lazy.js +39 -0
- package/dist/cursor-provider-live-run-drain.js +420 -0
- package/dist/cursor-provider-overflow.js +90 -0
- package/dist/cursor-provider-run-finalizer.js +176 -0
- package/dist/cursor-provider-run-outcome.js +76 -0
- package/dist/cursor-provider-turn-coordinator.js +293 -0
- package/dist/cursor-provider-turn-display-router.js +158 -0
- package/dist/cursor-provider-turn-emit.js +41 -0
- package/dist/cursor-provider-turn-finalize.js +154 -0
- package/dist/cursor-provider-turn-lifecycle-emitter.js +126 -0
- package/dist/cursor-provider-turn-prepare.js +379 -0
- package/dist/cursor-provider-turn-runner.js +146 -0
- package/dist/cursor-provider-turn-sdk-normalizer.js +74 -0
- package/dist/cursor-provider-turn-send.js +138 -0
- package/dist/cursor-provider-turn-shell-output.js +117 -0
- package/dist/cursor-provider-turn-tool-ledger.js +99 -0
- package/dist/cursor-provider-turn-types.js +1 -0
- package/dist/cursor-provider.js +72 -0
- package/dist/cursor-question-tool.js +194 -0
- package/dist/cursor-record-utils.js +61 -0
- package/dist/cursor-replay-activity-builders.js +114 -0
- package/dist/cursor-replay-source-names.js +27 -0
- package/dist/cursor-replay-summary-args.js +105 -0
- package/dist/cursor-replay-tool-details.js +167 -0
- package/dist/cursor-ripgrep-path.js +27 -0
- package/dist/cursor-run-final-text.js +63 -0
- package/dist/cursor-runtime-state.js +380 -0
- package/dist/cursor-sdk-billed-usage.js +86 -0
- package/dist/cursor-sdk-event-debug-constants.js +33 -0
- package/dist/cursor-sdk-event-debug-session.js +107 -0
- package/dist/cursor-sdk-event-debug.js +461 -0
- package/dist/cursor-sdk-output-filter.js +1 -0
- package/dist/cursor-sdk-process-error-guard.js +221 -0
- package/dist/cursor-sdk-runtime.js +3 -0
- package/dist/cursor-sensitive-text.js +23 -0
- package/dist/cursor-session-agent-cleanup.js +311 -0
- package/dist/cursor-session-agent-lifecycle.js +32 -0
- package/dist/cursor-session-agent-lineage.js +103 -0
- package/dist/cursor-session-agent-resume.js +417 -0
- package/dist/cursor-session-agent.js +507 -0
- package/dist/cursor-session-compaction-prep.js +21 -0
- package/dist/cursor-session-scope.js +113 -0
- package/dist/cursor-session-send-policy.js +18 -0
- package/dist/cursor-session-store.js +105 -0
- package/dist/cursor-session-turn-queue.js +50 -0
- package/dist/cursor-setting-sources.js +15 -0
- package/dist/cursor-skill-tool.js +230 -0
- package/dist/cursor-state.js +519 -0
- package/dist/cursor-task-presentation.js +59 -0
- package/dist/cursor-tool-lifecycle.js +94 -0
- package/dist/cursor-tool-manifest.js +34 -0
- package/dist/cursor-tool-presentation-registry.js +305 -0
- package/dist/cursor-tool-result-display-readers.js +158 -0
- package/dist/cursor-tool-transcript.js +32 -0
- package/dist/cursor-tool-visibility.js +28 -0
- package/dist/cursor-transcript-tool-formatters.js +618 -0
- package/dist/cursor-transcript-tool-specs.js +284 -0
- package/dist/cursor-transcript-utils.js +170 -0
- package/dist/cursor-usage-accounting.js +166 -0
- package/dist/cursor-web-tool-activity.js +23 -0
- package/dist/cursor-web-tool-args.js +12 -0
- package/dist/index.js +82 -0
- package/dist/model-discovery.js +305 -0
- package/dist/model-list-cache.js +156 -0
- package/docs/cursor-dogfood-checklist.md +2 -1
- package/docs/cursor-live-smoke-checklist.md +13 -12
- package/docs/cursor-model-ux-spec.md +27 -6
- package/docs/cursor-native-tool-replay.md +13 -13
- package/docs/cursor-native-tool-visual-audit.md +6 -3
- package/docs/cursor-testing-lessons.md +14 -8
- package/docs/cursor-tool-surfaces.md +5 -5
- package/docs/platform-smoke.md +3 -3
- package/package.json +19 -7
- package/platform-smoke.config.mjs +1 -1
- package/scripts/build.mjs +126 -0
- package/scripts/cloud-runtime-smoke.mjs +3 -1
- package/scripts/debug-provider-events.mjs +3 -1
- package/scripts/debug-sdk-events.mjs +1 -1
- package/scripts/isolated-cursor-smoke.sh +5 -4
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +1 -1
- package/scripts/lib/cursor-visual-render.mjs +1 -1
- package/scripts/lib/ensure-built.mjs +16 -0
- package/scripts/lib/local-resume-smoke-harness.mjs +1 -1
- package/scripts/local-resume-smoke.mjs +3 -1
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
- package/scripts/platform-smoke/render-ansi.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +2 -2
- package/scripts/prepare.mjs +102 -0
- package/scripts/steering-rpc-smoke.mjs +4 -2
- package/scripts/tmux-live-smoke.sh +11 -11
- package/scripts/visual-tui-smoke-self-test.mjs +4 -1
- package/scripts/visual-tui-smoke.mjs +51 -7
- package/src/bundled-context-windows.ts +4 -1
- package/src/context.ts +27 -4
- package/src/cursor-agents-context-registration.ts +2 -2
- package/src/cursor-agents-context.ts +24 -10
- package/src/cursor-api-key.ts +5 -4
- package/src/cursor-cloud-reporting.ts +23 -2
- package/src/cursor-context-tools.ts +3 -1
- package/src/cursor-fallback-models.generated.ts +592 -204
- package/src/cursor-live-run-coordinator.ts +11 -6
- package/src/cursor-native-tool-display-registration.ts +10 -11
- package/src/cursor-partial-content-emitter.ts +13 -0
- package/src/cursor-pi-context.ts +44 -0
- package/src/cursor-provider-errors.ts +1 -1
- package/src/cursor-provider-lazy.ts +5 -4
- package/src/cursor-provider-live-run-drain.ts +14 -9
- package/src/cursor-provider-run-finalizer.ts +1 -0
- package/src/cursor-provider-turn-coordinator.ts +15 -0
- package/src/cursor-provider-turn-emit.ts +6 -0
- package/src/cursor-provider-turn-finalize.ts +12 -0
- package/src/cursor-provider-turn-prepare.ts +10 -5
- package/src/cursor-provider-turn-types.ts +2 -0
- package/src/cursor-sdk-billed-usage.ts +107 -0
- package/src/cursor-sdk-process-error-guard.ts +6 -4
- package/src/cursor-session-agent-lifecycle.ts +8 -9
- package/src/cursor-session-agent-resume.ts +23 -0
- package/src/cursor-session-compaction-prep.ts +4 -0
- package/src/cursor-skill-tool.ts +1 -1
- package/src/cursor-tool-manifest.ts +1 -1
- package/src/cursor-usage-accounting.ts +70 -9
- package/src/index.ts +1 -1
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +16 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { discoverModels } from "./model-discovery.js";
|
|
2
|
+
import { registerCursorRuntimeControls } from "./cursor-state.js";
|
|
3
|
+
import { registerCursorNativeToolDisplay } from "./cursor-native-tool-display-registration.js";
|
|
4
|
+
import { registerCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
|
|
5
|
+
import { registerCursorQuestionTool } from "./cursor-question-tool.js";
|
|
6
|
+
import { registerCursorSkillTool } from "./cursor-skill-tool.js";
|
|
7
|
+
import { registerCursorSessionScope } from "./cursor-session-scope.js";
|
|
8
|
+
import { registerCursorSessionAgentLifecycle } from "./cursor-session-agent-lifecycle.js";
|
|
9
|
+
import { registerCursorSessionAgentLineage } from "./cursor-session-agent-lineage.js";
|
|
10
|
+
import { registerCursorSessionAgentResume } from "./cursor-session-agent-resume.js";
|
|
11
|
+
import { streamCursorLazy } from "./cursor-provider-lazy.js";
|
|
12
|
+
import { CURSOR_API_KEY_CONFIG_VALUE, resolveCursorApiKey } from "./cursor-api-key.js";
|
|
13
|
+
import { registerCursorFallbackIssueWarning } from "./cursor-fallback-warning.js";
|
|
14
|
+
import { registerCursorAgentsContextDedup } from "./cursor-agents-context-registration.js";
|
|
15
|
+
import { registerCursorOverflowNormalization } from "./cursor-provider-overflow.js";
|
|
16
|
+
import { registerCursorSdkSessionProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
|
|
17
|
+
import { prepareCursorSessionForCompaction } from "./cursor-session-compaction-prep.js";
|
|
18
|
+
function createCursorProviderConfig(models) {
|
|
19
|
+
return {
|
|
20
|
+
name: "Cursor",
|
|
21
|
+
baseUrl: "https://cursor.com",
|
|
22
|
+
apiKey: CURSOR_API_KEY_CONFIG_VALUE,
|
|
23
|
+
api: "cursor-sdk",
|
|
24
|
+
models,
|
|
25
|
+
streamSimple: streamCursorLazy,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function registerCursorProvider(pi, models) {
|
|
29
|
+
pi.registerProvider("cursor", createCursorProviderConfig(models));
|
|
30
|
+
}
|
|
31
|
+
export default async function (pi) {
|
|
32
|
+
// Session cwd must register before other session_start listeners that depend on it.
|
|
33
|
+
registerCursorSessionScope(pi);
|
|
34
|
+
registerCursorSessionAgentLineage(pi);
|
|
35
|
+
registerCursorSessionAgentLifecycle(pi);
|
|
36
|
+
registerCursorSessionAgentResume(pi);
|
|
37
|
+
pi.on("session_before_compact", async () => {
|
|
38
|
+
await prepareCursorSessionForCompaction();
|
|
39
|
+
});
|
|
40
|
+
registerCursorRuntimeControls(pi);
|
|
41
|
+
registerCursorNativeToolDisplay(pi);
|
|
42
|
+
registerCursorQuestionTool(pi);
|
|
43
|
+
registerCursorSkillTool(pi);
|
|
44
|
+
registerCursorPiToolBridge(pi);
|
|
45
|
+
registerCursorAgentsContextDedup(pi);
|
|
46
|
+
registerCursorOverflowNormalization(pi);
|
|
47
|
+
let fallbackIssue;
|
|
48
|
+
const models = await discoverModels({
|
|
49
|
+
onFallback: (issue) => {
|
|
50
|
+
fallbackIssue = issue;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (fallbackIssue) {
|
|
54
|
+
registerCursorFallbackIssueWarning(pi, fallbackIssue);
|
|
55
|
+
}
|
|
56
|
+
pi.registerCommand("cursor-refresh-models", {
|
|
57
|
+
description: "Refresh the live Cursor model catalog without restarting pi",
|
|
58
|
+
handler: async (_args, ctx) => {
|
|
59
|
+
let refreshFallbackIssue;
|
|
60
|
+
const apiKey = resolveCursorApiKey(await ctx.modelRegistry.getApiKeyForProvider("cursor"));
|
|
61
|
+
const refreshedModels = await discoverModels({
|
|
62
|
+
apiKey,
|
|
63
|
+
forceRefresh: true,
|
|
64
|
+
onFallback: (issue) => {
|
|
65
|
+
refreshFallbackIssue = issue;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
registerCursorProvider(pi, refreshedModels);
|
|
69
|
+
if (!ctx.hasUI)
|
|
70
|
+
return;
|
|
71
|
+
if (refreshFallbackIssue) {
|
|
72
|
+
ctx.ui.notify(`Cursor model catalog refresh did not use a live catalog: ${refreshFallbackIssue.message}`, "warning");
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
ctx.ui.notify(`Cursor model catalog refreshed with ${refreshedModels.length} model${refreshedModels.length === 1 ? "" : "s"}.`, "info");
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
registerCursorProvider(pi, models);
|
|
80
|
+
// Register last so session_shutdown cleanup remains protected until other Cursor handlers finish.
|
|
81
|
+
registerCursorSdkSessionProcessErrorGuard(pi);
|
|
82
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { getCursorModelSelectionIdentities } from "../shared/cursor-model-selection-identities.mjs";
|
|
2
|
+
import { loadContextWindowCache } from "./context-window-cache.js";
|
|
3
|
+
import { loadCursorSdk } from "./cursor-sdk-runtime.js";
|
|
4
|
+
import { resolveCursorApiKey, resolveCursorRuntimeApiKey } from "./cursor-api-key.js";
|
|
5
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
6
|
+
import { fingerprintApiKey, loadAnyCachedModelCatalog, loadFreshCachedModels, saveModelListCache, } from "./model-list-cache.js";
|
|
7
|
+
const FALLBACK_CONTEXT_WINDOW = 128000;
|
|
8
|
+
const FALLBACK_MAX_TOKENS = 16384;
|
|
9
|
+
const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
10
|
+
const TEXT_AND_IMAGE_INPUT = ["text", "image"];
|
|
11
|
+
const AUTH_SETUP_HINT = "/login (Use an API key -> Cursor) or CURSOR_API_KEY; startup discovery does not parse Pi CLI arguments, and Cursor Agent CLI/Desktop login is not reused";
|
|
12
|
+
const CATALOG_REFRESH_HINT = "After adding auth to an already-started pi session, run /cursor-refresh-models to refresh the full live Cursor model catalog without restarting pi.";
|
|
13
|
+
async function getDiscoveryApiKey(apiKey) {
|
|
14
|
+
return resolveCursorApiKey(apiKey) ?? resolveCursorRuntimeApiKey();
|
|
15
|
+
}
|
|
16
|
+
const metadataByPiModelId = new Map();
|
|
17
|
+
function cloneParams(params) {
|
|
18
|
+
return params.map((param) => ({ ...param }));
|
|
19
|
+
}
|
|
20
|
+
function getParameter(item, id) {
|
|
21
|
+
return item.parameters?.find((parameter) => parameter.id === id);
|
|
22
|
+
}
|
|
23
|
+
function hasBooleanValues(parameter) {
|
|
24
|
+
const values = new Set((parameter?.values ?? []).map((value) => value.value.toLowerCase()));
|
|
25
|
+
return values.has("false") && values.has("true");
|
|
26
|
+
}
|
|
27
|
+
function getParameterValue(parameter, lowerValue) {
|
|
28
|
+
const value = parameter?.values.find((candidate) => candidate.value.toLowerCase() === lowerValue);
|
|
29
|
+
return value?.value ?? null;
|
|
30
|
+
}
|
|
31
|
+
function getPreferredParameterValue(parameter, lowerValues) {
|
|
32
|
+
for (const value of lowerValues) {
|
|
33
|
+
const candidate = getParameterValue(parameter, value);
|
|
34
|
+
if (candidate)
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function mapComparableLevel(parameter, level) {
|
|
40
|
+
if (level === "xhigh") {
|
|
41
|
+
return getPreferredParameterValue(parameter, ["xhigh", "extra-high"]);
|
|
42
|
+
}
|
|
43
|
+
return getParameterValue(parameter, level);
|
|
44
|
+
}
|
|
45
|
+
function getThinkingLevelMap(item) {
|
|
46
|
+
const reasoningParameter = getParameter(item, "reasoning");
|
|
47
|
+
const effortParameter = getParameter(item, "effort");
|
|
48
|
+
const thinkingParameter = getParameter(item, "thinking");
|
|
49
|
+
const valueParameter = effortParameter ?? reasoningParameter ?? thinkingParameter;
|
|
50
|
+
if (!valueParameter)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (valueParameter.id === "thinking" && hasBooleanValues(valueParameter)) {
|
|
53
|
+
return {
|
|
54
|
+
off: getParameterValue(valueParameter, "false"),
|
|
55
|
+
minimal: null,
|
|
56
|
+
low: null,
|
|
57
|
+
medium: null,
|
|
58
|
+
high: getParameterValue(valueParameter, "true"),
|
|
59
|
+
xhigh: null,
|
|
60
|
+
max: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
off: getParameterValue(reasoningParameter, "none") ??
|
|
65
|
+
getParameterValue(reasoningParameter, "off") ??
|
|
66
|
+
getParameterValue(thinkingParameter, "false"),
|
|
67
|
+
minimal: mapComparableLevel(valueParameter, "minimal"),
|
|
68
|
+
low: mapComparableLevel(valueParameter, "low"),
|
|
69
|
+
medium: mapComparableLevel(valueParameter, "medium"),
|
|
70
|
+
high: mapComparableLevel(valueParameter, "high"),
|
|
71
|
+
xhigh: mapComparableLevel(valueParameter, "xhigh"),
|
|
72
|
+
max: mapComparableLevel(valueParameter, "max"),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function parseContextWindow(value) {
|
|
76
|
+
const match = /^(\d+(?:\.\d+)?)([km])$/i.exec(value.trim());
|
|
77
|
+
if (!match)
|
|
78
|
+
return undefined;
|
|
79
|
+
const amount = Number(match[1]);
|
|
80
|
+
const unit = match[2]?.toLowerCase();
|
|
81
|
+
if (!Number.isFinite(amount))
|
|
82
|
+
return undefined;
|
|
83
|
+
return Math.round(amount * (unit === "m" ? 1000000 : 1000));
|
|
84
|
+
}
|
|
85
|
+
function getDefaultParams(item) {
|
|
86
|
+
if (!item.variants?.length)
|
|
87
|
+
return [];
|
|
88
|
+
const defaultVariant = item.variants.find((variant) => variant.isDefault) ?? item.variants[0];
|
|
89
|
+
return cloneParams(defaultVariant?.params ?? []);
|
|
90
|
+
}
|
|
91
|
+
function replaceParam(params, id, value) {
|
|
92
|
+
let replaced = false;
|
|
93
|
+
const next = params.map((param) => {
|
|
94
|
+
if (param.id !== id)
|
|
95
|
+
return { ...param };
|
|
96
|
+
replaced = true;
|
|
97
|
+
return { id, value };
|
|
98
|
+
});
|
|
99
|
+
if (!replaced)
|
|
100
|
+
next.push({ id, value });
|
|
101
|
+
return next;
|
|
102
|
+
}
|
|
103
|
+
function getParamValue(params, id) {
|
|
104
|
+
return params.find((param) => param.id === id)?.value;
|
|
105
|
+
}
|
|
106
|
+
function getModelName(item, context, alias, fastOverride) {
|
|
107
|
+
const displayName = item.displayName || item.id;
|
|
108
|
+
const qualifiers = [];
|
|
109
|
+
if (alias)
|
|
110
|
+
qualifiers.push(alias);
|
|
111
|
+
if (fastOverride === true)
|
|
112
|
+
qualifiers.push("fast");
|
|
113
|
+
if (fastOverride === false)
|
|
114
|
+
qualifiers.push("slow");
|
|
115
|
+
const baseName = qualifiers.length > 0 ? `${displayName} (${qualifiers.join(", ")})` : displayName;
|
|
116
|
+
return context ? `${baseName} @ ${context}` : baseName;
|
|
117
|
+
}
|
|
118
|
+
function getContextWindow(contextWindowCache, selectionKeys, context, baseModelId) {
|
|
119
|
+
for (const key of new Set(selectionKeys)) {
|
|
120
|
+
const contextWindow = contextWindowCache.get(key);
|
|
121
|
+
if (contextWindow !== undefined)
|
|
122
|
+
return contextWindow;
|
|
123
|
+
}
|
|
124
|
+
return ((context ? parseContextWindow(context) : undefined) ??
|
|
125
|
+
(baseModelId ? contextWindowCache.get(baseModelId) : undefined) ??
|
|
126
|
+
contextWindowCache.get("default") ??
|
|
127
|
+
FALLBACK_CONTEXT_WINDOW);
|
|
128
|
+
}
|
|
129
|
+
function toMetadata(item, piModelId, selectionModelId, defaultParams, context, contextWindowCache, contextWindowKeys, fastOverride) {
|
|
130
|
+
const thinkingLevelMap = getThinkingLevelMap(item);
|
|
131
|
+
const fastValue = getParamValue(defaultParams, "fast")?.toLowerCase();
|
|
132
|
+
return {
|
|
133
|
+
piModelId,
|
|
134
|
+
baseModelId: item.id,
|
|
135
|
+
selectionModelId,
|
|
136
|
+
displayName: item.displayName || item.id,
|
|
137
|
+
defaultParams: cloneParams(defaultParams),
|
|
138
|
+
...(context ? { context } : {}),
|
|
139
|
+
contextWindow: getContextWindow(contextWindowCache, contextWindowKeys, context, item.id),
|
|
140
|
+
supportsFast: getParameter(item, "fast") !== undefined,
|
|
141
|
+
defaultFast: fastValue === "true",
|
|
142
|
+
...(fastOverride !== undefined ? { fastOverride } : {}),
|
|
143
|
+
supportsReasoning: thinkingLevelMap !== undefined,
|
|
144
|
+
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
|
|
145
|
+
parameterIds: {
|
|
146
|
+
context: getParameter(item, "context") !== undefined,
|
|
147
|
+
reasoning: getParameter(item, "reasoning") !== undefined,
|
|
148
|
+
effort: getParameter(item, "effort") !== undefined,
|
|
149
|
+
thinking: getParameter(item, "thinking") !== undefined,
|
|
150
|
+
fast: getParameter(item, "fast") !== undefined,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function toModelConfig(metadata, name) {
|
|
155
|
+
return {
|
|
156
|
+
id: metadata.piModelId,
|
|
157
|
+
name,
|
|
158
|
+
reasoning: metadata.supportsReasoning,
|
|
159
|
+
...(metadata.thinkingLevelMap ? { thinkingLevelMap: metadata.thinkingLevelMap } : {}),
|
|
160
|
+
input: [...TEXT_AND_IMAGE_INPUT],
|
|
161
|
+
cost: { ...ZERO_COST },
|
|
162
|
+
contextWindow: metadata.contextWindow,
|
|
163
|
+
maxTokens: FALLBACK_MAX_TOKENS,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function registerModelItems(items) {
|
|
167
|
+
metadataByPiModelId.clear();
|
|
168
|
+
const contextWindowCache = loadContextWindowCache();
|
|
169
|
+
return getCursorModelSelectionIdentities(items).map(({ model: item, selectionModelId, context, fastOverride, piModelId, contextWindowKey, baseContextWindowKey }) => {
|
|
170
|
+
const defaultParams = getDefaultParams(item);
|
|
171
|
+
const contextParams = context ? replaceParam(defaultParams, "context", context) : defaultParams;
|
|
172
|
+
const params = fastOverride === undefined ? contextParams : replaceParam(contextParams, "fast", fastOverride ? "true" : "false");
|
|
173
|
+
const metadata = toMetadata(item, piModelId, selectionModelId, params, context, contextWindowCache, [piModelId, contextWindowKey, baseContextWindowKey], fastOverride);
|
|
174
|
+
metadataByPiModelId.set(piModelId, metadata);
|
|
175
|
+
const alias = selectionModelId === item.id ? undefined : selectionModelId;
|
|
176
|
+
return toModelConfig(metadata, getModelName(item, context, alias, fastOverride));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export function getCursorModelMetadata(modelId) {
|
|
180
|
+
return metadataByPiModelId.get(modelId);
|
|
181
|
+
}
|
|
182
|
+
export function getCursorModelMetadataEntries() {
|
|
183
|
+
return [...metadataByPiModelId.values()].map((metadata) => ({
|
|
184
|
+
...metadata,
|
|
185
|
+
defaultParams: cloneParams(metadata.defaultParams),
|
|
186
|
+
...(metadata.thinkingLevelMap ? { thinkingLevelMap: { ...metadata.thinkingLevelMap } } : {}),
|
|
187
|
+
parameterIds: { ...metadata.parameterIds },
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
function setParam(params, id, value) {
|
|
191
|
+
const existing = params.find((param) => param.id === id);
|
|
192
|
+
if (existing) {
|
|
193
|
+
existing.value = value;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
params.push({ id, value });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function deleteParam(params, id) {
|
|
200
|
+
const index = params.findIndex((param) => param.id === id);
|
|
201
|
+
if (index >= 0)
|
|
202
|
+
params.splice(index, 1);
|
|
203
|
+
}
|
|
204
|
+
function applyThinkingLevel(metadata, params, level) {
|
|
205
|
+
const mapped = metadata.thinkingLevelMap?.[level];
|
|
206
|
+
if (mapped === undefined || mapped === null)
|
|
207
|
+
return;
|
|
208
|
+
if (level === "off") {
|
|
209
|
+
if (metadata.parameterIds.thinking && mapped === "false") {
|
|
210
|
+
setParam(params, "thinking", mapped);
|
|
211
|
+
deleteParam(params, "effort");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (metadata.parameterIds.reasoning) {
|
|
215
|
+
setParam(params, "reasoning", mapped);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (metadata.parameterIds.effort) {
|
|
220
|
+
if (metadata.parameterIds.thinking)
|
|
221
|
+
setParam(params, "thinking", "true");
|
|
222
|
+
setParam(params, "effort", mapped);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (metadata.parameterIds.reasoning) {
|
|
226
|
+
setParam(params, "reasoning", mapped);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (metadata.parameterIds.thinking) {
|
|
230
|
+
setParam(params, "thinking", mapped);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export function buildCursorModelSelection(modelId, thinkingLevel, fastEnabled) {
|
|
234
|
+
const metadata = getCursorModelMetadata(modelId);
|
|
235
|
+
if (!metadata)
|
|
236
|
+
return { id: modelId };
|
|
237
|
+
const params = cloneParams(metadata.defaultParams);
|
|
238
|
+
applyThinkingLevel(metadata, params, thinkingLevel);
|
|
239
|
+
if (metadata.supportsFast && fastEnabled !== undefined) {
|
|
240
|
+
setParam(params, "fast", fastEnabled ? "true" : "false");
|
|
241
|
+
}
|
|
242
|
+
return params.length > 0 ? { id: metadata.selectionModelId, params } : { id: metadata.selectionModelId };
|
|
243
|
+
}
|
|
244
|
+
function sanitizeDiscoveryError(error, apiKey) {
|
|
245
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
246
|
+
return scrubSensitiveText(message, apiKey).trim() || undefined;
|
|
247
|
+
}
|
|
248
|
+
async function useFallbackModels(options, issue) {
|
|
249
|
+
options.onFallback?.(issue);
|
|
250
|
+
const { FALLBACK_MODEL_ITEMS } = await import("./cursor-fallback-models.generated.js");
|
|
251
|
+
return registerModelItems(FALLBACK_MODEL_ITEMS);
|
|
252
|
+
}
|
|
253
|
+
export async function discoverModels(options = {}) {
|
|
254
|
+
const apiKey = await getDiscoveryApiKey(options.apiKey);
|
|
255
|
+
if (!apiKey) {
|
|
256
|
+
return useFallbackModels(options, {
|
|
257
|
+
reason: "missing-api-key",
|
|
258
|
+
message: `Cursor model discovery needs an API key from ${AUTH_SETUP_HINT}. Using fallback Cursor models so /login and model selection still work; fallback models can run once auth exists. ${CATALOG_REFRESH_HINT}`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const keyFingerprint = fingerprintApiKey(apiKey);
|
|
262
|
+
if (!options.forceRefresh) {
|
|
263
|
+
const cachedModels = loadFreshCachedModels(keyFingerprint);
|
|
264
|
+
if (cachedModels && cachedModels.length > 0) {
|
|
265
|
+
return registerModelItems(cachedModels);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const { Cursor } = await loadCursorSdk();
|
|
270
|
+
const models = await Cursor.models.list({ apiKey });
|
|
271
|
+
if (models.length > 0) {
|
|
272
|
+
saveModelListCache(keyFingerprint, models);
|
|
273
|
+
return registerModelItems(models);
|
|
274
|
+
}
|
|
275
|
+
return useFallbackModels(options, {
|
|
276
|
+
reason: "empty-model-list",
|
|
277
|
+
message: `Cursor model discovery returned no models. Using fallback Cursor models; verify ${AUTH_SETUP_HINT}. ${CATALOG_REFRESH_HINT}`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
const errorMessage = sanitizeDiscoveryError(error, apiKey);
|
|
282
|
+
// Prefer a previously cached catalog over the generic bundled fallback when
|
|
283
|
+
// a live refresh fails (e.g. transient network/auth errors), but keep the
|
|
284
|
+
// provenance visible so refresh commands do not claim a live refresh worked.
|
|
285
|
+
const cachedCatalog = loadAnyCachedModelCatalog(keyFingerprint);
|
|
286
|
+
if (cachedCatalog && cachedCatalog.models.length > 0) {
|
|
287
|
+
options.onFallback?.({
|
|
288
|
+
reason: "cached-after-error",
|
|
289
|
+
message: `Cursor model discovery failed; using cached Cursor model catalog from ${new Date(cachedCatalog.fetchedAt).toISOString()}.${errorMessage ? ` ${errorMessage}` : ""}`,
|
|
290
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
291
|
+
});
|
|
292
|
+
return registerModelItems(cachedCatalog.models);
|
|
293
|
+
}
|
|
294
|
+
return useFallbackModels(options, {
|
|
295
|
+
reason: "discovery-failed",
|
|
296
|
+
message: `Cursor model discovery failed${errorMessage ? `: ${errorMessage}` : ""}. Using fallback Cursor models; verify ${AUTH_SETUP_HINT}. ${CATALOG_REFRESH_HINT}`,
|
|
297
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export const __testUtils = {
|
|
302
|
+
parseContextWindow,
|
|
303
|
+
registerModelItems,
|
|
304
|
+
normalizeApiKey: resolveCursorApiKey,
|
|
305
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
6
|
+
import { asRecord } from "./cursor-record-utils.js";
|
|
7
|
+
const MODEL_LIST_CACHE_FILE = "cursor-sdk-model-list.json";
|
|
8
|
+
const MODEL_LIST_CACHE_VERSION = 1;
|
|
9
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
const MAX_CACHE_CLOCK_SKEW_MS = 5 * 60 * 1000;
|
|
11
|
+
const DISABLE_ENV_VAR = "PI_CURSOR_SDK_DISABLE_MODEL_CACHE";
|
|
12
|
+
const TTL_ENV_VAR = "PI_CURSOR_SDK_MODEL_CACHE_TTL_MS";
|
|
13
|
+
function getCachePath() {
|
|
14
|
+
return join(getAgentDir(), MODEL_LIST_CACHE_FILE);
|
|
15
|
+
}
|
|
16
|
+
export function isModelCacheDisabled() {
|
|
17
|
+
return parseEnvBoolean(process.env[DISABLE_ENV_VAR], false);
|
|
18
|
+
}
|
|
19
|
+
export function getModelCacheTtlMs() {
|
|
20
|
+
const raw = process.env[TTL_ENV_VAR];
|
|
21
|
+
if (raw === undefined)
|
|
22
|
+
return DEFAULT_TTL_MS;
|
|
23
|
+
const parsed = Number.parseInt(raw, 10);
|
|
24
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
25
|
+
return DEFAULT_TTL_MS;
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
// Fingerprint the API key so a key change invalidates the cache, without ever
|
|
29
|
+
// persisting the key itself.
|
|
30
|
+
export function fingerprintApiKey(apiKey) {
|
|
31
|
+
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
32
|
+
}
|
|
33
|
+
function isStringArray(value) {
|
|
34
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
35
|
+
}
|
|
36
|
+
function isModelParameterValue(value) {
|
|
37
|
+
const record = asRecord(value);
|
|
38
|
+
return record !== undefined && typeof record.id === "string" && typeof record.value === "string";
|
|
39
|
+
}
|
|
40
|
+
function isModelParameterDefinitionValue(value) {
|
|
41
|
+
const record = asRecord(value);
|
|
42
|
+
return record !== undefined && typeof record.value === "string" && (record.displayName === undefined || typeof record.displayName === "string");
|
|
43
|
+
}
|
|
44
|
+
function isModelParameterDefinition(value) {
|
|
45
|
+
const record = asRecord(value);
|
|
46
|
+
if (!record)
|
|
47
|
+
return false;
|
|
48
|
+
return (typeof record.id === "string" &&
|
|
49
|
+
(record.displayName === undefined || typeof record.displayName === "string") &&
|
|
50
|
+
Array.isArray(record.values) &&
|
|
51
|
+
record.values.every(isModelParameterDefinitionValue));
|
|
52
|
+
}
|
|
53
|
+
function isModelVariant(value) {
|
|
54
|
+
const record = asRecord(value);
|
|
55
|
+
if (!record)
|
|
56
|
+
return false;
|
|
57
|
+
return (Array.isArray(record.params) &&
|
|
58
|
+
record.params.every(isModelParameterValue) &&
|
|
59
|
+
typeof record.displayName === "string" &&
|
|
60
|
+
(record.description === undefined || typeof record.description === "string") &&
|
|
61
|
+
(record.isDefault === undefined || typeof record.isDefault === "boolean"));
|
|
62
|
+
}
|
|
63
|
+
function isModelListItem(value) {
|
|
64
|
+
const record = asRecord(value);
|
|
65
|
+
if (!record)
|
|
66
|
+
return false;
|
|
67
|
+
return (typeof record.id === "string" &&
|
|
68
|
+
typeof record.displayName === "string" &&
|
|
69
|
+
(record.description === undefined || typeof record.description === "string") &&
|
|
70
|
+
(record.aliases === undefined || isStringArray(record.aliases)) &&
|
|
71
|
+
(record.parameters === undefined || (Array.isArray(record.parameters) && record.parameters.every(isModelParameterDefinition))) &&
|
|
72
|
+
(record.variants === undefined || (Array.isArray(record.variants) && record.variants.every(isModelVariant))));
|
|
73
|
+
}
|
|
74
|
+
function isValidFetchedAt(value) {
|
|
75
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= Date.now() + MAX_CACHE_CLOCK_SKEW_MS;
|
|
76
|
+
}
|
|
77
|
+
function parseModelListCacheFile(value) {
|
|
78
|
+
const record = asRecord(value);
|
|
79
|
+
if (!record)
|
|
80
|
+
return undefined;
|
|
81
|
+
if (record.version !== MODEL_LIST_CACHE_VERSION ||
|
|
82
|
+
!isValidFetchedAt(record.fetchedAt) ||
|
|
83
|
+
typeof record.keyFingerprint !== "string" ||
|
|
84
|
+
!Array.isArray(record.models) ||
|
|
85
|
+
!record.models.every(isModelListItem)) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
version: record.version,
|
|
90
|
+
fetchedAt: record.fetchedAt,
|
|
91
|
+
keyFingerprint: record.keyFingerprint,
|
|
92
|
+
models: record.models,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function readCacheFile() {
|
|
96
|
+
const path = getCachePath();
|
|
97
|
+
if (!existsSync(path))
|
|
98
|
+
return undefined;
|
|
99
|
+
try {
|
|
100
|
+
return parseModelListCacheFile(JSON.parse(readFileSync(path, "utf-8")));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Return cached models only when caching is enabled, the key matches, and the
|
|
107
|
+
// entry is within the TTL. Used on the hot startup path to skip the network.
|
|
108
|
+
export function loadFreshCachedModels(keyFingerprint, now = Date.now()) {
|
|
109
|
+
if (isModelCacheDisabled())
|
|
110
|
+
return undefined;
|
|
111
|
+
const ttlMs = getModelCacheTtlMs();
|
|
112
|
+
if (ttlMs <= 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
const cache = readCacheFile();
|
|
115
|
+
if (!cache || cache.keyFingerprint !== keyFingerprint)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (now - cache.fetchedAt > ttlMs)
|
|
118
|
+
return undefined;
|
|
119
|
+
return cache.models;
|
|
120
|
+
}
|
|
121
|
+
// Return cached models regardless of age, as long as the key matches. Used as a
|
|
122
|
+
// resilience fallback when a live discovery request fails.
|
|
123
|
+
export function loadAnyCachedModelCatalog(keyFingerprint) {
|
|
124
|
+
if (isModelCacheDisabled())
|
|
125
|
+
return undefined;
|
|
126
|
+
const cache = readCacheFile();
|
|
127
|
+
if (!cache || cache.keyFingerprint !== keyFingerprint)
|
|
128
|
+
return undefined;
|
|
129
|
+
return { fetchedAt: cache.fetchedAt, models: cache.models };
|
|
130
|
+
}
|
|
131
|
+
export function saveModelListCache(keyFingerprint, models) {
|
|
132
|
+
if (isModelCacheDisabled())
|
|
133
|
+
return false;
|
|
134
|
+
try {
|
|
135
|
+
const path = getCachePath();
|
|
136
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
137
|
+
const data = {
|
|
138
|
+
version: MODEL_LIST_CACHE_VERSION,
|
|
139
|
+
fetchedAt: Date.now(),
|
|
140
|
+
keyFingerprint,
|
|
141
|
+
models,
|
|
142
|
+
};
|
|
143
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
144
|
+
chmodSync(path, 0o600);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export const __testUtils = {
|
|
152
|
+
getCachePath,
|
|
153
|
+
DEFAULT_TTL_MS,
|
|
154
|
+
DISABLE_ENV_VAR,
|
|
155
|
+
TTL_ENV_VAR,
|
|
156
|
+
};
|
|
@@ -4,7 +4,8 @@ Short maintainer checklist for **minimal-surface** validation after prompt, brid
|
|
|
4
4
|
|
|
5
5
|
## Minimal environment
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- Build first after any `src/` edit: `npm run build` (the pi manifest loads compiled `dist/`)
|
|
8
|
+
- Extension only: `pi --approve -e . --cursor-no-fast --model cursor/grok-4.6`
|
|
8
9
|
- Fresh session dir: `--session-dir /tmp/pi-cursor-dogfood-<id>`
|
|
9
10
|
- Baseline surface (no ambient Cursor MCP/rules):
|
|
10
11
|
- `PI_CURSOR_SETTING_SOURCES=none`, **or**
|
|
@@ -8,8 +8,9 @@ Use this manual checklist during development and debugging of Cursor provider/ru
|
|
|
8
8
|
|
|
9
9
|
## Inner-loop rule
|
|
10
10
|
|
|
11
|
+
- Build first: `npm run build` after any `src/` edit — the pi manifest loads compiled `dist/`, so unbuilt runs validate stale code. (the cloud/steering/local-resume/provider-debug launchers rebuild automatically even when run directly with `node scripts/...`; `smoke:live`/`smoke:visual`/`smoke:isolated` build via their npm scripts; direct `pi -e .` invocations do not build.)
|
|
11
12
|
- Run from a clean working tree except for the intended branch diff.
|
|
12
|
-
- Use the local extension under test: `pi --approve -e . --cursor-no-fast --model cursor/
|
|
13
|
+
- Use the local extension under test: `pi --approve -e . --cursor-no-fast --model cursor/grok-4.6`.
|
|
13
14
|
- Use a temporary `--session-dir` for every run.
|
|
14
15
|
- Do not paste or commit Cursor API keys, raw session contents with secrets, endpoint URLs, or local private paths.
|
|
15
16
|
- If an inner-loop check fails, stop and fix or use [docs/platform-smoke.md](./platform-smoke.md) as the release-blocking source of truth. Do not treat this checklist as a narrower replacement for the platform gate.
|
|
@@ -68,8 +69,8 @@ The replay scan flags only error `toolResult` / error assistant messages with `T
|
|
|
68
69
|
Pass criteria:
|
|
69
70
|
|
|
70
71
|
- `pi --version` reports Pi 0.84.0 for this cutover baseline.
|
|
71
|
-
- `npm ls` shows `@cursor/sdk@1.0.
|
|
72
|
-
- `cursor/
|
|
72
|
+
- `npm ls` shows `@cursor/sdk@1.0.27` and local `@earendil-works/*@0.84.0` packages.
|
|
73
|
+
- `cursor/grok-4.6` appears in the model list.
|
|
73
74
|
- No Cursor key or auth token is printed.
|
|
74
75
|
- If neither `~/.pi/agent/auth.json` cursor auth nor `CURSOR_API_KEY` is available, stop and report the live smoke as blocked.
|
|
75
76
|
|
|
@@ -77,7 +78,7 @@ Pass criteria:
|
|
|
77
78
|
|
|
78
79
|
```bash
|
|
79
80
|
PI_CURSOR_SETTING_SOURCES=none \
|
|
80
|
-
pi --approve -e . --cursor-no-fast --model cursor/
|
|
81
|
+
pi --approve -e . --cursor-no-fast --model cursor/grok-4.6 \
|
|
81
82
|
--session-dir "$SMOKE_DIR/basic" \
|
|
82
83
|
--no-tools \
|
|
83
84
|
-p 'Live smoke. Reply exactly: PI_CURSOR_SMOKE_OK' \
|
|
@@ -95,7 +96,7 @@ Pass criteria:
|
|
|
95
96
|
## 2. Default setting-source startup noise check
|
|
96
97
|
|
|
97
98
|
```bash
|
|
98
|
-
pi --approve -e . --cursor-no-fast --model cursor/
|
|
99
|
+
pi --approve -e . --cursor-no-fast --model cursor/grok-4.6 \
|
|
99
100
|
--session-dir "$SMOKE_DIR/default-settings" \
|
|
100
101
|
--no-tools \
|
|
101
102
|
-p 'Default settings smoke. Include PRODUCT=42 in the final answer.' \
|
|
@@ -117,7 +118,7 @@ Run a real interactive session under tmux:
|
|
|
117
118
|
```bash
|
|
118
119
|
SESSION="pi-cursor-sdk-smoke-$(date +%s)"
|
|
119
120
|
tmux new-session -d -s "$SESSION" -x 120 -y 40 -- zsh -lc \
|
|
120
|
-
"cd '$PWD' && PI_CURSOR_SETTING_SOURCES=none pi --approve -e . --cursor-no-fast --model cursor/
|
|
121
|
+
"cd '$PWD' && PI_CURSOR_SETTING_SOURCES=none pi --approve -e . --cursor-no-fast --model cursor/grok-4.6 --session-dir '$SMOKE_DIR/tui' --session-id cursor-sdk-1016-tui --no-tools 'TUI smoke. Compute 19 + 23. Reply only with SUM=<number>.'"
|
|
121
122
|
```
|
|
122
123
|
|
|
123
124
|
Observe with `tmux capture-pane -pt "$SESSION"` or attach manually.
|
|
@@ -133,7 +134,7 @@ Pass criteria:
|
|
|
133
134
|
|
|
134
135
|
## 4. Focused visual card/color rendering check
|
|
135
136
|
|
|
136
|
-
This is the canonical inner-loop visual debug path for Cursor provider/runtime changes. It requires offscreen TUI visual inspection, not only JSONL or code review. Use Pi 0.84.0, `@cursor/sdk@1.0.
|
|
137
|
+
This is the canonical inner-loop visual debug path for Cursor provider/runtime changes. It requires offscreen TUI visual inspection, not only JSONL or code review. Use Pi 0.84.0, `@cursor/sdk@1.0.27`, a fresh temporary session dir, Cursor SDK `plan` mode, native replay enabled, and the checked-in visual runner. The runner resolves `pi` by directly walking the parent `PATH`, uses `process.execPath` for Node, and prepends that Node directory for both prereq checks and tmux launches so `#!/usr/bin/env node` shims use the validated Node. The default matrix is native replay only: native replay registration is forced on, settings sources are `none`, the pi bridge is off, overlapping built-in pi tools are not exposed, and inherited Cursor SDK event-debug artifact env is cleared. With `--event-debug`, debug capture writes to a deterministic directory under `VISUAL_DIR`.
|
|
137
138
|
|
|
138
139
|
```bash
|
|
139
140
|
VISUAL_DIR="$(mktemp -d /tmp/pi-cursor-sdk-1016-visual.XXXXXX)"
|
|
@@ -204,7 +205,7 @@ Pass criteria:
|
|
|
204
205
|
|
|
205
206
|
```bash
|
|
206
207
|
PI_CURSOR_SETTING_SOURCES=none \
|
|
207
|
-
pi --approve -e . --cursor-no-fast --cursor-mode plan --model cursor/
|
|
208
|
+
pi --approve -e . --cursor-no-fast --cursor-mode plan --model cursor/grok-4.6 \
|
|
208
209
|
--session-dir "$SMOKE_DIR/cursor-mode-plan" \
|
|
209
210
|
--session-id cursor-sdk-1016-plan \
|
|
210
211
|
--no-tools \
|
|
@@ -226,7 +227,7 @@ Pass criteria:
|
|
|
226
227
|
PI_CURSOR_SETTING_SOURCES=none \
|
|
227
228
|
PI_CURSOR_EXPOSE_BUILTIN_TOOLS=1 \
|
|
228
229
|
PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1 \
|
|
229
|
-
pi --approve -e . --cursor-no-fast --model cursor/
|
|
230
|
+
pi --approve -e . --cursor-no-fast --model cursor/grok-4.6 \
|
|
230
231
|
--session-dir "$SMOKE_DIR/bridge" \
|
|
231
232
|
-p 'Bridge smoke. Do exactly two tool calls before answering: first call pi__read on ./package.json; second call pi__read on ./definitely-missing-pi-cursor-sdk-smoke-file.txt. Then answer: OK_NAME=<package name>; MISSING_RESULT=<error or success>. Do not use shell.' \
|
|
232
233
|
> "$SMOKE_DIR/bridge.stdout.txt" \
|
|
@@ -247,7 +248,7 @@ Pass criteria:
|
|
|
247
248
|
PI_CURSOR_SETTING_SOURCES=none \
|
|
248
249
|
PI_CURSOR_PI_TOOL_BRIDGE=0 \
|
|
249
250
|
PI_CURSOR_NATIVE_TOOL_DISPLAY=1 \
|
|
250
|
-
pi --approve -e . --cursor-no-fast --model cursor/
|
|
251
|
+
pi --approve -e . --cursor-no-fast --model cursor/grok-4.6 \
|
|
251
252
|
--session-dir "$SMOKE_DIR/native-replay" \
|
|
252
253
|
-p 'Native replay smoke. Use your Cursor file-reading capability to read ./README.md, then answer README_SEEN=yes if it contains pi-cursor-sdk.' \
|
|
253
254
|
> "$SMOKE_DIR/native-replay.stdout.txt" \
|
|
@@ -321,7 +322,7 @@ Use a harmless long-running command and interrupt it after the bridge request is
|
|
|
321
322
|
PI_CURSOR_SETTING_SOURCES=none \
|
|
322
323
|
PI_CURSOR_EXPOSE_BUILTIN_TOOLS=1 \
|
|
323
324
|
PI_CURSOR_PI_TOOL_BRIDGE_DEBUG=1 \
|
|
324
|
-
pi --approve -e . --cursor-no-fast --model cursor/
|
|
325
|
+
pi --approve -e . --cursor-no-fast --model cursor/grok-4.6 \
|
|
325
326
|
--session-dir "$SMOKE_DIR/abort" \
|
|
326
327
|
-p 'Abort smoke. Call pi__bash with command: sleep 30 && echo SHOULD_NOT_PRINT. Do not answer until the tool completes.'
|
|
327
328
|
```
|
|
@@ -365,7 +366,7 @@ npm pack --dry-run
|
|
|
365
366
|
Pass criteria:
|
|
366
367
|
|
|
367
368
|
- All commands exit `0`.
|
|
368
|
-
- `npm pack --dry-run` includes all new runtime source files and excludes local smoke artifacts, sessions, package tarballs, `.env*`, `.pi/`,
|
|
369
|
+
- `npm pack --dry-run` includes all new runtime source files and the compiled `dist/` output, and excludes local smoke artifacts, sessions, package tarballs, `.env*`, `.pi/`, and `coverage/`.
|
|
369
370
|
|
|
370
371
|
## 12. Cleanup
|
|
371
372
|
|