@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
@@ -1,267 +0,0 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { Text } from "@earendil-works/pi-tui";
3
- import { Type } from "typebox";
4
- import { arePiToolsDisabled } from "./cursor-active-tools.js";
5
- import { parseEnvBoolean } from "./cursor-env-boolean.js";
6
- import { isCursorModel } from "./cursor-model.js";
7
- import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
8
- import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
9
-
10
- export const CURSOR_ASK_QUESTION_TOOL_NAME = "cursor_ask_question";
11
- export const CURSOR_ASK_QUESTION_ENV = "PI_CURSOR_ASK_QUESTION";
12
-
13
- export function resolveCursorAskQuestionEnabled(env: Record<string, string | undefined> = process.env): boolean {
14
- return parseEnvBoolean(env[CURSOR_ASK_QUESTION_ENV], true);
15
- }
16
-
17
- /** Package-namespaced event while `cursor_ask_question` awaits pi UI input. */
18
- export const CURSOR_ASK_QUESTION_BLOCKED_EVENT = "pi-cursor-sdk:ask-question:blocked";
19
-
20
- export interface CursorAskQuestionBlockedEventPayload {
21
- active: boolean;
22
- }
23
-
24
- interface CursorQuestionOption {
25
- label: string;
26
- value: string;
27
- description?: string;
28
- }
29
-
30
- interface CursorQuestion {
31
- id: string;
32
- question: string;
33
- options: CursorQuestionOption[];
34
- allowCustom: boolean;
35
- }
36
-
37
- interface CursorQuestionAnswer {
38
- id: string;
39
- question: string;
40
- answer: string | null;
41
- value?: string;
42
- wasCustom: boolean;
43
- cancelled: boolean;
44
- }
45
-
46
- interface CursorQuestionDetails {
47
- questions: CursorQuestion[];
48
- answers: CursorQuestionAnswer[];
49
- uiAvailable: boolean;
50
- cancelled: boolean;
51
- }
52
-
53
- interface CursorQuestionToolExtensionApi
54
- extends Pick<ExtensionAPI, "getActiveTools" | "registerTool" | "setActiveTools" | "events">,
55
- CursorModelLifecycleExtensionApi {}
56
-
57
- type RawQuestionOption = string | { label?: string; value?: string; description?: string };
58
-
59
- type RawQuestion = {
60
- id?: string;
61
- question?: string;
62
- prompt?: string;
63
- options?: RawQuestionOption[];
64
- choices?: RawQuestionOption[];
65
- allowCustom?: boolean;
66
- };
67
-
68
- type CursorAskQuestionParams = RawQuestion & {
69
- questions?: RawQuestion[];
70
- };
71
-
72
- const QuestionOptionSchema = Type.Union([
73
- Type.String(),
74
- Type.Object({
75
- label: Type.String({ description: "User-facing option label" }),
76
- value: Type.Optional(Type.String({ description: "Optional value returned to Cursor; defaults to label" })),
77
- description: Type.Optional(Type.String({ description: "Optional helper text shown by compatible pi UIs" })),
78
- }),
79
- ]);
80
-
81
- const QuestionSchema = Type.Object({
82
- id: Type.Optional(Type.String({ description: "Stable question identifier" })),
83
- question: Type.Optional(Type.String({ description: "Question to ask the user" })),
84
- prompt: Type.Optional(Type.String({ description: "Alias for question" })),
85
- options: Type.Optional(Type.Array(QuestionOptionSchema, { description: "Choices the user can select" })),
86
- choices: Type.Optional(Type.Array(QuestionOptionSchema, { description: "Alias for options" })),
87
- allowCustom: Type.Optional(Type.Boolean({ description: "Allow a typed answer in addition to listed options; defaults to true" })),
88
- });
89
-
90
- const CursorAskQuestionParamsSchema = Type.Object({
91
- question: Type.Optional(Type.String({ description: "Question to ask the user" })),
92
- prompt: Type.Optional(Type.String({ description: "Alias for question" })),
93
- options: Type.Optional(Type.Array(QuestionOptionSchema, { description: "Choices the user can select" })),
94
- choices: Type.Optional(Type.Array(QuestionOptionSchema, { description: "Alias for options" })),
95
- allowCustom: Type.Optional(Type.Boolean({ description: "Allow a typed answer in addition to listed options; defaults to true" })),
96
- questions: Type.Optional(Type.Array(QuestionSchema, { description: "Ask multiple questions sequentially" })),
97
- });
98
-
99
- function normalizeOption(option: RawQuestionOption, index: number): CursorQuestionOption | undefined {
100
- if (typeof option === "string") {
101
- const trimmed = option.trim();
102
- return trimmed ? { label: trimmed, value: trimmed } : undefined;
103
- }
104
- const label = option.label?.trim() || option.value?.trim() || `Option ${index + 1}`;
105
- return {
106
- label,
107
- value: option.value?.trim() || label,
108
- ...(option.description?.trim() ? { description: option.description.trim() } : {}),
109
- };
110
- }
111
-
112
- function normalizeOptions(options: RawQuestionOption[] | undefined): CursorQuestionOption[] {
113
- return (options ?? []).map(normalizeOption).filter((option): option is CursorQuestionOption => option !== undefined);
114
- }
115
-
116
- function normalizeQuestion(raw: RawQuestion, index: number): CursorQuestion | undefined {
117
- const question = raw.question?.trim() || raw.prompt?.trim();
118
- if (!question) return undefined;
119
- return {
120
- id: raw.id?.trim() || `question_${index + 1}`,
121
- question,
122
- options: normalizeOptions(raw.options ?? raw.choices),
123
- allowCustom: raw.allowCustom !== false,
124
- };
125
- }
126
-
127
- function normalizeQuestions(params: CursorAskQuestionParams): CursorQuestion[] {
128
- const rawQuestions = Array.isArray(params.questions) && params.questions.length > 0 ? params.questions : [params];
129
- return rawQuestions.map(normalizeQuestion).filter((question): question is CursorQuestion => question !== undefined);
130
- }
131
-
132
- function summarizeAnswers(answers: CursorQuestionAnswer[]): string {
133
- if (answers.length === 0) return "No answer was collected.";
134
- if (answers.length === 1) {
135
- const [answer] = answers;
136
- return answer.cancelled || answer.answer === null ? "User cancelled the question." : `User answered: ${answer.answer}`;
137
- }
138
- return [
139
- "User answered:",
140
- ...answers.map((answer) => {
141
- const value = answer.cancelled || answer.answer === null ? "cancelled" : answer.answer;
142
- return `- ${answer.id}: ${value}`;
143
- }),
144
- ].join("\n");
145
- }
146
-
147
- function buildDetails(questions: CursorQuestion[], answers: CursorQuestionAnswer[], uiAvailable: boolean): CursorQuestionDetails {
148
- return {
149
- questions,
150
- answers,
151
- uiAvailable,
152
- cancelled: answers.some((answer) => answer.cancelled),
153
- };
154
- }
155
-
156
- async function askOneQuestion(question: CursorQuestion, ctx: { ui: ExtensionContext["ui"] }): Promise<CursorQuestionAnswer> {
157
- if (question.options.length > 0) {
158
- const labels = question.options.map((option) => option.description ? `${option.label} — ${option.description}` : option.label);
159
- const customLabel = "Type a custom answer";
160
- const choices = question.allowCustom ? [...labels, customLabel] : labels;
161
- const selected = await ctx.ui.select(question.question, choices);
162
- if (!selected) {
163
- return { id: question.id, question: question.question, answer: null, wasCustom: false, cancelled: true };
164
- }
165
- if (selected === customLabel) {
166
- const customAnswer = await ctx.ui.input(question.question, "Type your answer");
167
- const trimmed = customAnswer?.trim();
168
- return trimmed
169
- ? { id: question.id, question: question.question, answer: trimmed, value: trimmed, wasCustom: true, cancelled: false }
170
- : { id: question.id, question: question.question, answer: null, wasCustom: true, cancelled: true };
171
- }
172
- const selectedIndex = labels.indexOf(selected);
173
- const selectedOption = selectedIndex >= 0 ? question.options[selectedIndex] : undefined;
174
- const answer = selectedOption?.label ?? selected;
175
- return {
176
- id: question.id,
177
- question: question.question,
178
- answer,
179
- value: selectedOption?.value ?? answer,
180
- wasCustom: false,
181
- cancelled: false,
182
- };
183
- }
184
-
185
- const answer = await ctx.ui.input(question.question, "Type your answer");
186
- const trimmed = answer?.trim();
187
- return trimmed
188
- ? { id: question.id, question: question.question, answer: trimmed, value: trimmed, wasCustom: true, cancelled: false }
189
- : { id: question.id, question: question.question, answer: null, wasCustom: true, cancelled: true };
190
- }
191
-
192
- function syncCursorQuestionToolForModel(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">, model: ExtensionContext["model"]): void {
193
- const activeToolNames = new Set(pi.getActiveTools());
194
- const shouldBeActive = !arePiToolsDisabled(pi) && isCursorModel(model) && resolveCursorPiToolBridgeEnabled();
195
- const alreadyActive = activeToolNames.has(CURSOR_ASK_QUESTION_TOOL_NAME);
196
- if (shouldBeActive === alreadyActive) return;
197
- if (shouldBeActive) {
198
- activeToolNames.add(CURSOR_ASK_QUESTION_TOOL_NAME);
199
- } else {
200
- activeToolNames.delete(CURSOR_ASK_QUESTION_TOOL_NAME);
201
- }
202
- pi.setActiveTools([...activeToolNames]);
203
- }
204
-
205
- function emitCursorAskQuestionBlockedEvent(
206
- pi: Pick<ExtensionAPI, "events">,
207
- payload: CursorAskQuestionBlockedEventPayload,
208
- ): void {
209
- pi.events.emit(CURSOR_ASK_QUESTION_BLOCKED_EVENT, payload);
210
- }
211
-
212
- export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi): void {
213
- if (!resolveCursorAskQuestionEnabled()) return;
214
-
215
- pi.registerTool({
216
- name: CURSOR_ASK_QUESTION_TOOL_NAME,
217
- label: "Cursor question",
218
- description:
219
- "Ask the user a clarifying question from Cursor. Use when user preferences materially affect the next step; provide options when possible.",
220
- promptSnippet: "Ask the user a clarifying question through pi UI when material choices affect Cursor's next step",
221
- executionMode: "sequential",
222
- parameters: CursorAskQuestionParamsSchema,
223
- promptGuidelines: [
224
- "Use cursor_ask_question only when running a Cursor model and user input would materially change the plan, scope, platform, or implementation path.",
225
- "Prefer cursor_ask_question with 2-4 concrete options instead of guessing when Cursor plan mode needs user choices.",
226
- ],
227
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
228
- const questions = normalizeQuestions(params as CursorAskQuestionParams);
229
- if (questions.length === 0) {
230
- throw new Error("No valid question was provided.");
231
- }
232
- if (!ctx.hasUI) {
233
- throw new Error(
234
- "Cannot ask the user because pi UI is unavailable. Make a reasonable default choice and state the assumption before proceeding.",
235
- );
236
- }
237
-
238
- // Emit a package-namespaced blocked signal while the questionnaire
239
- // awaits input so consumers (e.g. Herdr) can map it to blocked/working.
240
- emitCursorAskQuestionBlockedEvent(pi, { active: true });
241
- try {
242
- const answers: CursorQuestionAnswer[] = [];
243
- for (const question of questions) {
244
- const answer = await askOneQuestion(question, ctx);
245
- answers.push(answer);
246
- if (answer.cancelled) break;
247
- }
248
-
249
- return {
250
- content: [{ type: "text" as const, text: summarizeAnswers(answers) }],
251
- details: buildDetails(questions, answers, true),
252
- };
253
- } finally {
254
- emitCursorAskQuestionBlockedEvent(pi, { active: false });
255
- }
256
- },
257
- renderCall(args, theme) {
258
- const questions = normalizeQuestions(args as CursorAskQuestionParams);
259
- const label = questions[0]?.question ?? "Ask the user";
260
- return new Text(theme.fg("toolTitle", theme.bold("cursor question ")) + theme.fg("muted", label), 0, 0);
261
- },
262
- });
263
-
264
- registerCursorModelLifecycle(pi, (ctx) => {
265
- syncCursorQuestionToolForModel(pi, ctx.model);
266
- });
267
- }