@emmaneugene/pi-cursor-sdk 0.4.2 → 0.5.0

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +19 -39
  3. package/dist/bundled-context-windows.js +6 -13
  4. package/dist/context.js +0 -3
  5. package/dist/cursor-config.js +0 -3
  6. package/dist/cursor-live-run-coordinator.js +11 -0
  7. package/dist/cursor-pi-tool-bridge-run.js +3 -0
  8. package/dist/cursor-provider-turn-prepare.js +0 -1
  9. package/dist/cursor-state.js +3 -16
  10. package/dist/index.js +0 -2
  11. package/dist/model-discovery.js +11 -47
  12. package/docs/cursor-dogfood-checklist.md +1 -1
  13. package/docs/cursor-live-smoke-checklist.md +1 -2
  14. package/docs/cursor-model-ux-spec.md +80 -151
  15. package/docs/cursor-native-tool-replay.md +2 -3
  16. package/docs/cursor-native-tool-visual-audit.md +1 -1
  17. package/docs/cursor-testing-lessons.md +1 -1
  18. package/docs/cursor-tool-surfaces.md +3 -6
  19. package/docs/platform-smoke-implementation.md +1 -1
  20. package/docs/platform-smoke.md +1 -1
  21. package/package.json +1 -1
  22. package/scripts/isolated-cursor-smoke.sh +1 -1
  23. package/scripts/lib/local-resume-smoke-harness.mjs +2 -1
  24. package/scripts/local-resume-smoke.mjs +1 -1
  25. package/scripts/platform-smoke/card-detect.mjs +1 -1
  26. package/scripts/refresh-cursor-model-snapshots.mjs +3 -3
  27. package/shared/cursor-model-selection-identities.d.mts +1 -4
  28. package/shared/cursor-model-selection-identities.mjs +21 -74
  29. package/src/bundled-context-windows.ts +6 -13
  30. package/src/context.ts +1 -5
  31. package/src/cursor-config.ts +9 -13
  32. package/src/cursor-live-run-coordinator.ts +11 -0
  33. package/src/cursor-pi-tool-bridge-run.ts +4 -0
  34. package/src/cursor-pi-tool-bridge-types.ts +1 -0
  35. package/src/cursor-provider-turn-prepare.ts +0 -1
  36. package/src/cursor-state.ts +3 -22
  37. package/src/index.ts +0 -3
  38. package/src/model-discovery.ts +16 -62
  39. package/dist/cursor-question-tool.js +0 -194
  40. package/node_modules/cross-spawn/node_modules/which/CHANGELOG.md +0 -166
  41. package/src/cursor-question-tool.ts +0 -267
@@ -49,15 +49,11 @@ async function getDiscoveryApiKey(apiKey?: string): Promise<string | undefined>
49
49
 
50
50
  export interface CursorModelMetadata {
51
51
  piModelId: string;
52
- baseModelId: string;
53
- selectionModelId: string;
54
52
  displayName: string;
55
53
  defaultParams: ModelParameterValue[];
56
- context?: string;
57
54
  contextWindow: number;
58
55
  supportsFast: boolean;
59
56
  defaultFast: boolean;
60
- fastOverride?: boolean;
61
57
  supportsReasoning: boolean;
62
58
  thinkingLevelMap?: ThinkingLevelMap;
63
59
  parameterIds: {
@@ -158,48 +154,20 @@ function getDefaultParams(item: ModelListItem): ModelParameterValue[] {
158
154
  return cloneParams(defaultVariant?.params ?? []);
159
155
  }
160
156
 
161
- function replaceParam(
162
- params: ModelParameterValue[],
163
- id: string,
164
- value: string,
165
- ): ModelParameterValue[] {
166
- let replaced = false;
167
- const next = params.map((param) => {
168
- if (param.id !== id) return { ...param };
169
- replaced = true;
170
- return { id, value };
171
- });
172
- if (!replaced) next.push({ id, value });
173
- return next;
174
- }
175
-
176
157
  function getParamValue(params: ModelParameterValue[], id: string): string | undefined {
177
158
  return params.find((param) => param.id === id)?.value;
178
159
  }
179
160
 
180
- function getModelName(item: ModelListItem, context?: string, alias?: string, fastOverride?: boolean): string {
181
- const displayName = item.displayName || item.id;
182
- const qualifiers: string[] = [];
183
- if (alias) qualifiers.push(alias);
184
- if (fastOverride === true) qualifiers.push("fast");
185
- if (fastOverride === false) qualifiers.push("slow");
186
- const baseName = qualifiers.length > 0 ? `${displayName} (${qualifiers.join(", ")})` : displayName;
187
- return context ? `${baseName} @ ${context}` : baseName;
188
- }
189
-
190
161
  function getContextWindow(
191
162
  contextWindowCache: Map<string, number>,
192
- selectionKeys: readonly string[],
193
- context?: string,
194
- baseModelId?: string,
163
+ modelId: string,
164
+ defaultContext: string | undefined,
165
+ contextWindowKey: string,
195
166
  ): number {
196
- for (const key of new Set(selectionKeys)) {
197
- const contextWindow = contextWindowCache.get(key);
198
- if (contextWindow !== undefined) return contextWindow;
199
- }
200
167
  return (
201
- (context ? parseContextWindow(context) : undefined) ??
202
- (baseModelId ? contextWindowCache.get(baseModelId) : undefined) ??
168
+ contextWindowCache.get(modelId) ??
169
+ contextWindowCache.get(contextWindowKey) ??
170
+ (defaultContext ? parseContextWindow(defaultContext) : undefined) ??
203
171
  contextWindowCache.get("default") ??
204
172
  FALLBACK_CONTEXT_WINDOW
205
173
  );
@@ -207,27 +175,20 @@ function getContextWindow(
207
175
 
208
176
  function toMetadata(
209
177
  item: ModelListItem,
210
- piModelId: string,
211
- selectionModelId: string,
212
178
  defaultParams: ModelParameterValue[],
213
- context: string | undefined,
179
+ defaultContext: string | undefined,
214
180
  contextWindowCache: Map<string, number>,
215
- contextWindowKeys: readonly string[],
216
- fastOverride?: boolean,
181
+ contextWindowKey: string,
217
182
  ): CursorModelMetadata {
218
183
  const thinkingLevelMap = getThinkingLevelMap(item);
219
184
  const fastValue = getParamValue(defaultParams, "fast")?.toLowerCase();
220
185
  return {
221
- piModelId,
222
- baseModelId: item.id,
223
- selectionModelId,
186
+ piModelId: item.id,
224
187
  displayName: item.displayName || item.id,
225
188
  defaultParams: cloneParams(defaultParams),
226
- ...(context ? { context } : {}),
227
- contextWindow: getContextWindow(contextWindowCache, contextWindowKeys, context, item.id),
189
+ contextWindow: getContextWindow(contextWindowCache, item.id, defaultContext, contextWindowKey),
228
190
  supportsFast: getParameter(item, "fast") !== undefined,
229
191
  defaultFast: fastValue === "true",
230
- ...(fastOverride !== undefined ? { fastOverride } : {}),
231
192
  supportsReasoning: thinkingLevelMap !== undefined,
232
193
  ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
233
194
  parameterIds: {
@@ -256,23 +217,16 @@ function toModelConfig(metadata: CursorModelMetadata, name: string): ProviderMod
256
217
  function registerModelItems(items: ModelListItem[]): ProviderModelConfig[] {
257
218
  metadataByPiModelId.clear();
258
219
  const contextWindowCache = loadContextWindowCache();
259
- return getCursorModelSelectionIdentities(items).map(({ model: item, selectionModelId, context, fastOverride, piModelId, contextWindowKey, baseContextWindowKey }) => {
260
- const defaultParams = getDefaultParams(item);
261
- const contextParams = context ? replaceParam(defaultParams, "context", context) : defaultParams;
262
- const params = fastOverride === undefined ? contextParams : replaceParam(contextParams, "fast", fastOverride ? "true" : "false");
220
+ return getCursorModelSelectionIdentities(items).map(({ model: item, piModelId, defaultContext, contextWindowKey }) => {
263
221
  const metadata = toMetadata(
264
222
  item,
265
- piModelId,
266
- selectionModelId,
267
- params,
268
- context,
223
+ getDefaultParams(item),
224
+ defaultContext,
269
225
  contextWindowCache,
270
- [piModelId, contextWindowKey, baseContextWindowKey],
271
- fastOverride,
226
+ contextWindowKey,
272
227
  );
273
228
  metadataByPiModelId.set(piModelId, metadata);
274
- const alias = selectionModelId === item.id ? undefined : selectionModelId;
275
- return toModelConfig(metadata, getModelName(item, context, alias, fastOverride));
229
+ return toModelConfig(metadata, metadata.displayName);
276
230
  });
277
231
  }
278
232
 
@@ -354,7 +308,7 @@ export function buildCursorModelSelection(
354
308
  setParam(params, "fast", fastEnabled ? "true" : "false");
355
309
  }
356
310
 
357
- return params.length > 0 ? { id: metadata.selectionModelId, params } : { id: metadata.selectionModelId };
311
+ return params.length > 0 ? { id: metadata.piModelId, params } : { id: metadata.piModelId };
358
312
  }
359
313
 
360
314
  function sanitizeDiscoveryError(error: unknown, apiKey: string): string | undefined {
@@ -1,194 +0,0 @@
1
- import { Text } from "@earendil-works/pi-tui";
2
- import { Type } from "typebox";
3
- import { arePiToolsDisabled } from "./cursor-active-tools.js";
4
- import { parseEnvBoolean } from "./cursor-env-boolean.js";
5
- import { isCursorModel } from "./cursor-model.js";
6
- import { registerCursorModelLifecycle } from "./cursor-model-lifecycle.js";
7
- import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
8
- export const CURSOR_ASK_QUESTION_TOOL_NAME = "cursor_ask_question";
9
- export const CURSOR_ASK_QUESTION_ENV = "PI_CURSOR_ASK_QUESTION";
10
- export function resolveCursorAskQuestionEnabled(env = process.env) {
11
- return parseEnvBoolean(env[CURSOR_ASK_QUESTION_ENV], true);
12
- }
13
- /** Package-namespaced event while `cursor_ask_question` awaits pi UI input. */
14
- export const CURSOR_ASK_QUESTION_BLOCKED_EVENT = "pi-cursor-sdk:ask-question:blocked";
15
- const QuestionOptionSchema = Type.Union([
16
- Type.String(),
17
- Type.Object({
18
- label: Type.String({ description: "User-facing option label" }),
19
- value: Type.Optional(Type.String({ description: "Optional value returned to Cursor; defaults to label" })),
20
- description: Type.Optional(Type.String({ description: "Optional helper text shown by compatible pi UIs" })),
21
- }),
22
- ]);
23
- const QuestionSchema = Type.Object({
24
- id: Type.Optional(Type.String({ description: "Stable question identifier" })),
25
- question: Type.Optional(Type.String({ description: "Question to ask the user" })),
26
- prompt: Type.Optional(Type.String({ description: "Alias for question" })),
27
- options: Type.Optional(Type.Array(QuestionOptionSchema, { description: "Choices the user can select" })),
28
- choices: Type.Optional(Type.Array(QuestionOptionSchema, { description: "Alias for options" })),
29
- allowCustom: Type.Optional(Type.Boolean({ description: "Allow a typed answer in addition to listed options; defaults to true" })),
30
- });
31
- const CursorAskQuestionParamsSchema = Type.Object({
32
- question: Type.Optional(Type.String({ description: "Question to ask the user" })),
33
- prompt: Type.Optional(Type.String({ description: "Alias for question" })),
34
- options: Type.Optional(Type.Array(QuestionOptionSchema, { description: "Choices the user can select" })),
35
- choices: Type.Optional(Type.Array(QuestionOptionSchema, { description: "Alias for options" })),
36
- allowCustom: Type.Optional(Type.Boolean({ description: "Allow a typed answer in addition to listed options; defaults to true" })),
37
- questions: Type.Optional(Type.Array(QuestionSchema, { description: "Ask multiple questions sequentially" })),
38
- });
39
- function normalizeOption(option, index) {
40
- if (typeof option === "string") {
41
- const trimmed = option.trim();
42
- return trimmed ? { label: trimmed, value: trimmed } : undefined;
43
- }
44
- const label = option.label?.trim() || option.value?.trim() || `Option ${index + 1}`;
45
- return {
46
- label,
47
- value: option.value?.trim() || label,
48
- ...(option.description?.trim() ? { description: option.description.trim() } : {}),
49
- };
50
- }
51
- function normalizeOptions(options) {
52
- return (options ?? []).map(normalizeOption).filter((option) => option !== undefined);
53
- }
54
- function normalizeQuestion(raw, index) {
55
- const question = raw.question?.trim() || raw.prompt?.trim();
56
- if (!question)
57
- return undefined;
58
- return {
59
- id: raw.id?.trim() || `question_${index + 1}`,
60
- question,
61
- options: normalizeOptions(raw.options ?? raw.choices),
62
- allowCustom: raw.allowCustom !== false,
63
- };
64
- }
65
- function normalizeQuestions(params) {
66
- const rawQuestions = Array.isArray(params.questions) && params.questions.length > 0 ? params.questions : [params];
67
- return rawQuestions.map(normalizeQuestion).filter((question) => question !== undefined);
68
- }
69
- function summarizeAnswers(answers) {
70
- if (answers.length === 0)
71
- return "No answer was collected.";
72
- if (answers.length === 1) {
73
- const [answer] = answers;
74
- return answer.cancelled || answer.answer === null ? "User cancelled the question." : `User answered: ${answer.answer}`;
75
- }
76
- return [
77
- "User answered:",
78
- ...answers.map((answer) => {
79
- const value = answer.cancelled || answer.answer === null ? "cancelled" : answer.answer;
80
- return `- ${answer.id}: ${value}`;
81
- }),
82
- ].join("\n");
83
- }
84
- function buildDetails(questions, answers, uiAvailable) {
85
- return {
86
- questions,
87
- answers,
88
- uiAvailable,
89
- cancelled: answers.some((answer) => answer.cancelled),
90
- };
91
- }
92
- async function askOneQuestion(question, ctx) {
93
- if (question.options.length > 0) {
94
- const labels = question.options.map((option) => option.description ? `${option.label} — ${option.description}` : option.label);
95
- const customLabel = "Type a custom answer";
96
- const choices = question.allowCustom ? [...labels, customLabel] : labels;
97
- const selected = await ctx.ui.select(question.question, choices);
98
- if (!selected) {
99
- return { id: question.id, question: question.question, answer: null, wasCustom: false, cancelled: true };
100
- }
101
- if (selected === customLabel) {
102
- const customAnswer = await ctx.ui.input(question.question, "Type your answer");
103
- const trimmed = customAnswer?.trim();
104
- return trimmed
105
- ? { id: question.id, question: question.question, answer: trimmed, value: trimmed, wasCustom: true, cancelled: false }
106
- : { id: question.id, question: question.question, answer: null, wasCustom: true, cancelled: true };
107
- }
108
- const selectedIndex = labels.indexOf(selected);
109
- const selectedOption = selectedIndex >= 0 ? question.options[selectedIndex] : undefined;
110
- const answer = selectedOption?.label ?? selected;
111
- return {
112
- id: question.id,
113
- question: question.question,
114
- answer,
115
- value: selectedOption?.value ?? answer,
116
- wasCustom: false,
117
- cancelled: false,
118
- };
119
- }
120
- const answer = await ctx.ui.input(question.question, "Type your answer");
121
- const trimmed = answer?.trim();
122
- return trimmed
123
- ? { id: question.id, question: question.question, answer: trimmed, value: trimmed, wasCustom: true, cancelled: false }
124
- : { id: question.id, question: question.question, answer: null, wasCustom: true, cancelled: true };
125
- }
126
- function syncCursorQuestionToolForModel(pi, model) {
127
- const activeToolNames = new Set(pi.getActiveTools());
128
- const shouldBeActive = !arePiToolsDisabled(pi) && isCursorModel(model) && resolveCursorPiToolBridgeEnabled();
129
- const alreadyActive = activeToolNames.has(CURSOR_ASK_QUESTION_TOOL_NAME);
130
- if (shouldBeActive === alreadyActive)
131
- return;
132
- if (shouldBeActive) {
133
- activeToolNames.add(CURSOR_ASK_QUESTION_TOOL_NAME);
134
- }
135
- else {
136
- activeToolNames.delete(CURSOR_ASK_QUESTION_TOOL_NAME);
137
- }
138
- pi.setActiveTools([...activeToolNames]);
139
- }
140
- function emitCursorAskQuestionBlockedEvent(pi, payload) {
141
- pi.events.emit(CURSOR_ASK_QUESTION_BLOCKED_EVENT, payload);
142
- }
143
- export function registerCursorQuestionTool(pi) {
144
- if (!resolveCursorAskQuestionEnabled())
145
- return;
146
- pi.registerTool({
147
- name: CURSOR_ASK_QUESTION_TOOL_NAME,
148
- label: "Cursor question",
149
- description: "Ask the user a clarifying question from Cursor. Use when user preferences materially affect the next step; provide options when possible.",
150
- promptSnippet: "Ask the user a clarifying question through pi UI when material choices affect Cursor's next step",
151
- executionMode: "sequential",
152
- parameters: CursorAskQuestionParamsSchema,
153
- promptGuidelines: [
154
- "Use cursor_ask_question only when running a Cursor model and user input would materially change the plan, scope, platform, or implementation path.",
155
- "Prefer cursor_ask_question with 2-4 concrete options instead of guessing when Cursor plan mode needs user choices.",
156
- ],
157
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
158
- const questions = normalizeQuestions(params);
159
- if (questions.length === 0) {
160
- throw new Error("No valid question was provided.");
161
- }
162
- if (!ctx.hasUI) {
163
- throw new Error("Cannot ask the user because pi UI is unavailable. Make a reasonable default choice and state the assumption before proceeding.");
164
- }
165
- // Emit a package-namespaced blocked signal while the questionnaire
166
- // awaits input so consumers (e.g. Herdr) can map it to blocked/working.
167
- emitCursorAskQuestionBlockedEvent(pi, { active: true });
168
- try {
169
- const answers = [];
170
- for (const question of questions) {
171
- const answer = await askOneQuestion(question, ctx);
172
- answers.push(answer);
173
- if (answer.cancelled)
174
- break;
175
- }
176
- return {
177
- content: [{ type: "text", text: summarizeAnswers(answers) }],
178
- details: buildDetails(questions, answers, true),
179
- };
180
- }
181
- finally {
182
- emitCursorAskQuestionBlockedEvent(pi, { active: false });
183
- }
184
- },
185
- renderCall(args, theme) {
186
- const questions = normalizeQuestions(args);
187
- const label = questions[0]?.question ?? "Ask the user";
188
- return new Text(theme.fg("toolTitle", theme.bold("cursor question ")) + theme.fg("muted", label), 0, 0);
189
- },
190
- });
191
- registerCursorModelLifecycle(pi, (ctx) => {
192
- syncCursorQuestionToolForModel(pi, ctx.model);
193
- });
194
- }
@@ -1,166 +0,0 @@
1
- # Changes
2
-
3
-
4
- ## 2.0.2
5
-
6
- * Rename bin to `node-which`
7
-
8
- ## 2.0.1
9
-
10
- * generate changelog and publish on version bump
11
- * enforce 100% test coverage
12
- * Promise interface
13
-
14
- ## 2.0.0
15
-
16
- * Parallel tests, modern JavaScript, and drop support for node < 8
17
-
18
- ## 1.3.1
19
-
20
- * update deps
21
- * update travis
22
-
23
- ## v1.3.0
24
-
25
- * Add nothrow option to which.sync
26
- * update tap
27
-
28
- ## v1.2.14
29
-
30
- * appveyor: drop node 5 and 0.x
31
- * travis-ci: add node 6, drop 0.x
32
-
33
- ## v1.2.13
34
-
35
- * test: Pass missing option to pass on windows
36
- * update tap
37
- * update isexe to 2.0.0
38
- * neveragain.tech pledge request
39
-
40
- ## v1.2.12
41
-
42
- * Removed unused require
43
-
44
- ## v1.2.11
45
-
46
- * Prevent changelog script from being included in package
47
-
48
- ## v1.2.10
49
-
50
- * Use env.PATH only, not env.Path
51
-
52
- ## v1.2.9
53
-
54
- * fix for paths starting with ../
55
- * Remove unused `is-absolute` module
56
-
57
- ## v1.2.8
58
-
59
- * bullet items in changelog that contain (but don't start with) #
60
-
61
- ## v1.2.7
62
-
63
- * strip 'update changelog' changelog entries out of changelog
64
-
65
- ## v1.2.6
66
-
67
- * make the changelog bulleted
68
-
69
- ## v1.2.5
70
-
71
- * make a changelog, and keep it up to date
72
- * don't include tests in package
73
- * Properly handle relative-path executables
74
- * appveyor
75
- * Attach error code to Not Found error
76
- * Make tests pass on Windows
77
-
78
- ## v1.2.4
79
-
80
- * Fix typo
81
-
82
- ## v1.2.3
83
-
84
- * update isexe, fix regression in pathExt handling
85
-
86
- ## v1.2.2
87
-
88
- * update deps, use isexe module, test windows
89
-
90
- ## v1.2.1
91
-
92
- * Sometimes windows PATH entries are quoted
93
- * Fixed a bug in the check for group and user mode bits. This bug was introduced during refactoring for supporting strict mode.
94
- * doc cli
95
-
96
- ## v1.2.0
97
-
98
- * Add support for opt.all and -as cli flags
99
- * test the bin
100
- * update travis
101
- * Allow checking for multiple programs in bin/which
102
- * tap 2
103
-
104
- ## v1.1.2
105
-
106
- * travis
107
- * Refactored and fixed undefined error on Windows
108
- * Support strict mode
109
-
110
- ## v1.1.1
111
-
112
- * test +g exes against secondary groups, if available
113
- * Use windows exe semantics on cygwin & msys
114
- * cwd should be first in path on win32, not last
115
- * Handle lower-case 'env.Path' on Windows
116
- * Update docs
117
- * use single-quotes
118
-
119
- ## v1.1.0
120
-
121
- * Add tests, depend on is-absolute
122
-
123
- ## v1.0.9
124
-
125
- * which.js: root is allowed to execute files owned by anyone
126
-
127
- ## v1.0.8
128
-
129
- * don't use graceful-fs
130
-
131
- ## v1.0.7
132
-
133
- * add license to package.json
134
-
135
- ## v1.0.6
136
-
137
- * isc license
138
-
139
- ## 1.0.5
140
-
141
- * Awful typo
142
-
143
- ## 1.0.4
144
-
145
- * Test for path absoluteness properly
146
- * win: Allow '' as a pathext if cmd has a . in it
147
-
148
- ## 1.0.3
149
-
150
- * Remove references to execPath
151
- * Make `which.sync()` work on Windows by honoring the PATHEXT variable.
152
- * Make `isExe()` always return true on Windows.
153
- * MIT
154
-
155
- ## 1.0.2
156
-
157
- * Only files can be exes
158
-
159
- ## 1.0.1
160
-
161
- * Respect the PATHEXT env for win32 support
162
- * should 0755 the bin
163
- * binary
164
- * guts
165
- * package
166
- * 1st