@cursor/july 0.1.18 → 0.1.20

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 (74) hide show
  1. package/AGENTS.md +10 -0
  2. package/dist/bin/agent-serve.js +20 -3
  3. package/dist/docs/404.html +1 -1
  4. package/dist/docs/ab.html +2 -2
  5. package/dist/docs/assets/{app.BR5EdVSq.js → app.T-5ImnM3.js} +1 -1
  6. package/dist/docs/assets/chunks/@localSearchIndexroot.CvejISXF.js +1 -0
  7. package/dist/docs/assets/chunks/{VPLocalSearchBox.CNCzfUKC.js → VPLocalSearchBox.OMGMWH6H.js} +1 -1
  8. package/dist/docs/assets/chunks/{theme.CwKvMSWv.js → theme.DLg4yB4D.js} +2 -2
  9. package/dist/docs/building-with-agents.html +2 -2
  10. package/dist/docs/concepts.html +2 -2
  11. package/dist/docs/deployment.html +2 -2
  12. package/dist/docs/evals.html +2 -2
  13. package/dist/docs/example-agents/approval-buddy.html +2 -2
  14. package/dist/docs/example-agents/benny.html +2 -2
  15. package/dist/docs/example-agents/bugbot.html +2 -2
  16. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  17. package/dist/docs/example-agents/codeowners-review.html +2 -2
  18. package/dist/docs/example-agents/concierge.html +2 -2
  19. package/dist/docs/example-agents/fsd.html +2 -2
  20. package/dist/docs/example-agents/index.html +2 -2
  21. package/dist/docs/example-agents/knowledge-base.html +2 -2
  22. package/dist/docs/example-agents/oncall.html +2 -2
  23. package/dist/docs/example-agents/security-reviewer.html +2 -2
  24. package/dist/docs/example-agents/slack-agent.html +2 -2
  25. package/dist/docs/example-agents/weather-agent.html +2 -2
  26. package/dist/docs/guides/agent-to-agent.html +2 -2
  27. package/dist/docs/guides/cloud-runtime.html +2 -2
  28. package/dist/docs/guides/github.html +2 -2
  29. package/dist/docs/guides/human-in-the-loop.html +2 -2
  30. package/dist/docs/guides/mcp-oauth.html +2 -2
  31. package/dist/docs/guides/slack.html +2 -2
  32. package/dist/docs/guides/webhooks.html +2 -2
  33. package/dist/docs/hillclimbing.html +2 -2
  34. package/dist/docs/index.html +2 -2
  35. package/dist/docs/quickstart.html +2 -2
  36. package/dist/docs/reference/agent-config.html +2 -2
  37. package/dist/docs/reference/channels.html +2 -2
  38. package/dist/docs/reference/cli.html +2 -2
  39. package/dist/docs/reference/connections.html +2 -2
  40. package/dist/docs/reference/hooks.html +2 -2
  41. package/dist/docs/reference/http-api.html +2 -2
  42. package/dist/docs/reference/instructions.html +2 -2
  43. package/dist/docs/reference/playground.html +2 -2
  44. package/dist/docs/reference/project-layout.html +2 -2
  45. package/dist/docs/reference/prompt.html +2 -2
  46. package/dist/docs/reference/schedules.html +2 -2
  47. package/dist/docs/reference/sessions.html +2 -2
  48. package/dist/docs/reference/skills.html +2 -2
  49. package/dist/docs/reference/subagents.html +2 -2
  50. package/dist/docs/reference/tools.html +2 -2
  51. package/dist/docs/scaffolding-agents.html +2 -2
  52. package/dist/docs/storage.html +2 -2
  53. package/dist/docs/troubleshooting.html +2 -2
  54. package/dist/internal/cli-skills.d.ts +19 -0
  55. package/dist/internal/cli-skills.d.ts.map +1 -0
  56. package/dist/internal/cli-skills.js +72 -0
  57. package/dist/internal/install-cursor-skills.d.ts +46 -4
  58. package/dist/internal/install-cursor-skills.d.ts.map +1 -1
  59. package/dist/internal/install-cursor-skills.js +117 -49
  60. package/dist/playground/assets/index-CidizGZv.css +1 -0
  61. package/dist/playground/assets/index-CuiAKZaP.js +85 -0
  62. package/dist/playground/index.html +2 -2
  63. package/package.json +2 -2
  64. package/skills/create-agent/SKILL.md +36 -4
  65. package/skills/evals/SKILL.md +3 -0
  66. package/skills/framework-map/SKILL.md +14 -0
  67. package/skills/github/SKILL.md +6 -1
  68. package/skills/hillclimb/SKILL.md +28 -7
  69. package/src/bin/agent-serve.ts +21 -3
  70. package/src/internal/cli-skills.ts +95 -0
  71. package/src/internal/install-cursor-skills.ts +168 -60
  72. package/dist/docs/assets/chunks/@localSearchIndexroot.BOzkeUi8.js +0 -1
  73. package/dist/playground/assets/index-DRjS5klQ.js +0 -85
  74. package/dist/playground/assets/index-TCDs-UhZ.css +0 -1
@@ -18,7 +18,7 @@ import {
18
18
  } from "node:fs/promises";
19
19
  import { homedir } from "node:os";
20
20
  import { join } from "node:path";
21
- import { packageRootDir } from "./distribution.js";
21
+ import { CLI_COMMAND_NAME, packageRootDir } from "./distribution.js";
22
22
  import { stdoutPalette } from "./terminal-style.js";
23
23
 
24
24
  /** Monorepo-only skill; excluded from the published package. */
@@ -27,6 +27,9 @@ const SKIP_SKILL_DIRS = new Set(["operate"]);
27
27
  /** Category folder under ~/.cursor/skills (Cursor discovers nested skills). */
28
28
  export const CURSOR_SKILLS_NAMESPACE = "agentkit";
29
29
 
30
+ /** How long the `init` offer waits for an answer before skipping the install. */
31
+ export const SKILL_PROMPT_TIMEOUT_MS = 45_000;
32
+
30
33
  export interface PackageSkill {
31
34
  /** Frontmatter `name` (e.g. agentkit-create-agent). */
32
35
  name: string;
@@ -115,6 +118,13 @@ export async function findMissingCursorSkills(options?: {
115
118
  return missing;
116
119
  }
117
120
 
121
+ export interface SkillInstallResult {
122
+ /** Leaf dirs written. */
123
+ installed: string[];
124
+ /** Leaf dirs deleted because the package no longer ships them. */
125
+ removed: string[];
126
+ }
127
+
118
128
  /**
119
129
  * Copy each skill directory into `skillsHome/agentkit/<leafDir>/`.
120
130
  * Always overwrites so a package upgrade refreshes local Cursor skills.
@@ -123,24 +133,13 @@ export async function findMissingCursorSkills(options?: {
123
133
  export async function installCursorSkills(
124
134
  skills: readonly PackageSkill[],
125
135
  skillsHome: string = cursorSkillsHome()
126
- ): Promise<string[]> {
136
+ ): Promise<SkillInstallResult> {
127
137
  const namespaceDir = agentkitCursorSkillsDir(skillsHome);
128
138
  await mkdir(namespaceDir, { recursive: true });
129
139
 
130
- const shippedLeaves = new Set(skills.map((skill) => skill.leafDir));
131
- let existingLeaves: string[] = [];
132
- try {
133
- const entries = await readdir(namespaceDir, { withFileTypes: true });
134
- existingLeaves = entries
135
- .filter((entry) => entry.isDirectory())
136
- .map((entry) => entry.name);
137
- } catch {
138
- existingLeaves = [];
139
- }
140
- for (const leaf of existingLeaves) {
141
- if (!shippedLeaves.has(leaf)) {
142
- await rm(join(namespaceDir, leaf), { recursive: true, force: true });
143
- }
140
+ const removed = await orphanLeaves(namespaceDir, skills);
141
+ for (const leaf of removed) {
142
+ await rm(join(namespaceDir, leaf), { recursive: true, force: true });
144
143
  }
145
144
 
146
145
  const installed: string[] = [];
@@ -155,7 +154,24 @@ export async function installCursorSkills(
155
154
  }
156
155
  installed.push(skill.leafDir);
157
156
  }
158
- return installed;
157
+ return { installed, removed };
158
+ }
159
+
160
+ /** Installed leaf dirs the package no longer ships. Installing deletes these. */
161
+ async function orphanLeaves(
162
+ namespaceDir: string,
163
+ skills: readonly PackageSkill[]
164
+ ): Promise<string[]> {
165
+ const shipped = new Set(skills.map((skill) => skill.leafDir));
166
+ let entries: Array<{ name: string; isDirectory: () => boolean }>;
167
+ try {
168
+ entries = await readdir(namespaceDir, { withFileTypes: true });
169
+ } catch {
170
+ return [];
171
+ }
172
+ return entries
173
+ .filter((entry) => entry.isDirectory() && !shipped.has(entry.name))
174
+ .map((entry) => entry.name);
159
175
  }
160
176
 
161
177
  /** Drop monorepo `paths` globs and fix package doc links for ~/.cursor installs. */
@@ -191,20 +207,72 @@ function stripPathsFromFrontmatter(frontmatter: string): string {
191
207
  return out.join("\n");
192
208
  }
193
209
 
210
+ export interface SkillInstallPlan {
211
+ shipped: PackageSkill[];
212
+ /** Resolved `~/.cursor/skills`, to install into. */
213
+ skillsHome: string;
214
+ /** Install target: `<skillsHome>/agentkit`. */
215
+ directory: string;
216
+ /** Leaf dirs the package ships. */
217
+ leaves: string[];
218
+ /** Nothing installed yet, so this is a fresh install rather than a refresh. */
219
+ firstInstall: boolean;
220
+ /** Installed leaf dirs the package dropped; installing deletes them. */
221
+ orphans: string[];
222
+ }
223
+
224
+ /** What an install would do, for the offer and for `install-skills`. */
225
+ export async function planSkillInstall(options?: {
226
+ packageRoot?: string;
227
+ skillsHome?: string;
228
+ }): Promise<SkillInstallPlan> {
229
+ const skillsHome = options?.skillsHome ?? cursorSkillsHome();
230
+ const directory = agentkitCursorSkillsDir(skillsHome);
231
+ const shipped = await listShippedPackageSkills(options?.packageRoot);
232
+ const missing = await findMissingCursorSkills({
233
+ packageRoot: options?.packageRoot,
234
+ skillsHome,
235
+ });
236
+ return {
237
+ shipped,
238
+ skillsHome,
239
+ directory,
240
+ leaves: shipped.map((skill) => skill.leafDir),
241
+ firstInstall: missing.length === shipped.length,
242
+ orphans: await orphanLeaves(directory, shipped),
243
+ };
244
+ }
245
+
246
+ /** `Installed 9 agentkit skills`, what it dropped, and where it went. */
247
+ export function formatSkillInstall(
248
+ plan: SkillInstallPlan,
249
+ result: SkillInstallResult
250
+ ): string {
251
+ const noun = result.installed.length === 1 ? "skill" : "skills";
252
+ const verb = plan.firstInstall ? "Installed" : "Updated";
253
+ const lines = [`${verb} ${result.installed.length} agentkit ${noun}`];
254
+ if (result.removed.length > 0) {
255
+ lines.push(` removed: ${result.removed.join(", ")}`);
256
+ }
257
+ lines.push(` location: ${plan.directory}`);
258
+ return `${lines.join("\n")}\n`;
259
+ }
260
+
194
261
  export interface OfferInstallCursorSkillsOptions {
195
262
  /** Skip when false (non-TTY / `--json`). */
196
263
  interactive?: boolean;
197
264
  packageRoot?: string;
198
265
  skillsHome?: string;
199
- /** Injected yes/no for tests. */
200
- confirm?: (question: string) => Promise<boolean>;
266
+ /** Injected yes/no for tests. `undefined` means the prompt went unanswered. */
267
+ confirm?: (question: string) => Promise<boolean | undefined>;
201
268
  out?: (text: string) => void;
202
269
  }
203
270
 
204
271
  /**
205
272
  * On a TTY, offer to install/update every shipped package skill into
206
273
  * ~/.cursor/skills/agentkit (always overwriting). No-op when non-interactive
207
- * or the package ships no skills.
274
+ * or the package ships no skills. An unanswered prompt skips the install
275
+ * rather than blocking the caller — see `SKILL_PROMPT_TIMEOUT_MS`.
208
276
  */
209
277
  export async function offerInstallCursorSkills(
210
278
  options: OfferInstallCursorSkillsOptions = {}
@@ -213,48 +281,60 @@ export async function offerInstallCursorSkills(
213
281
  const interactive =
214
282
  options.interactive ??
215
283
  (process.stdin.isTTY === true && process.stdout.isTTY === true);
216
- const shipped = await listShippedPackageSkills(options.packageRoot);
217
- if (shipped.length === 0) {
284
+ if (!interactive) {
218
285
  return { installed: [], skipped: true };
219
286
  }
220
287
 
221
- const home = options.skillsHome ?? cursorSkillsHome();
222
- const targetDir = agentkitCursorSkillsDir(home);
223
- const missing = await findMissingCursorSkills({
288
+ const plan = await planSkillInstall({
224
289
  packageRoot: options.packageRoot,
225
- skillsHome: home,
290
+ skillsHome: options.skillsHome,
226
291
  });
227
- const names = shipped.map((skill) => skill.leafDir).join(", ");
228
- const noun = shipped.length === 1 ? "skill" : "skills";
229
- const verb = missing.length === shipped.length ? "Install" : "Update";
230
- const question = `${verb} ${shipped.length} agentkit ${noun} in ${targetDir}?`;
231
-
232
- if (!interactive) {
292
+ if (plan.shipped.length === 0) {
233
293
  return { installed: [], skipped: true };
234
294
  }
235
295
 
296
+ const noun = plan.shipped.length === 1 ? "skill" : "skills";
297
+ const verb = plan.firstInstall ? "Install" : "Update";
236
298
  const c = stdoutPalette();
237
- out(`${question}\n`);
238
- out(c.dim(` ${names}\n`));
239
- if (missing.length < shipped.length) {
299
+ out(
300
+ `${verb} ${plan.shipped.length} agentkit ${noun} in ${plan.directory}?\n`
301
+ );
302
+ out(c.dim(` ${plan.leaves.join(", ")}\n`));
303
+ if (!plan.firstInstall) {
240
304
  out(c.dim(" (overwrites existing copies with the package version)\n"));
241
305
  }
306
+ if (plan.orphans.length > 0) {
307
+ out(c.dim(` (deletes ${plan.orphans.join(", ")})\n`));
308
+ }
242
309
 
243
310
  const confirm =
244
- options.confirm ?? ((q) => promptYesNo(q, { defaultYes: true, out }));
245
- const accepted = await confirm(
246
- missing.length === shipped.length ? "Install now?" : "Update now?"
247
- );
248
- if (!accepted) {
249
- out(c.dim("Skipped skill install.\n\n"));
311
+ options.confirm ??
312
+ ((q) =>
313
+ promptYesNo(q, {
314
+ defaultYes: true,
315
+ out,
316
+ timeoutMs: SKILL_PROMPT_TIMEOUT_MS,
317
+ }));
318
+ const accepted = await confirm(`${verb} now?`);
319
+
320
+ if (accepted !== true) {
321
+ // `undefined` is the prompt timing out or stdin closing; both mean nobody
322
+ // said yes, so do not claim a duration neither case is guaranteed to have.
323
+ const why =
324
+ accepted === undefined
325
+ ? "No answer — skipped the skill install."
326
+ : "Skipped skill install.";
327
+ out(
328
+ c.dim(
329
+ `${why}\nInstall later: ${CLI_COMMAND_NAME} install-skills (${plan.directory})\n\n`
330
+ )
331
+ );
250
332
  return { installed: [], skipped: true };
251
333
  }
252
334
 
253
- const installed = await installCursorSkills(shipped, home);
254
- out(
255
- `${verb === "Install" ? "Installed" : "Updated"} ${installed.length} ${noun} in ${targetDir}\n\n`
256
- );
257
- return { installed, skipped: false };
335
+ const result = await installCursorSkills(plan.shipped, plan.skillsHome);
336
+ out(`${formatSkillInstall(plan, result)}\n`);
337
+ return { installed: result.installed, skipped: false };
258
338
  }
259
339
 
260
340
  function parseSkillName(skillMd: string): string | undefined {
@@ -274,19 +354,34 @@ function parseSkillName(skillMd: string): string | undefined {
274
354
  return raw.replace(/^["']|["']$/g, "");
275
355
  }
276
356
 
277
- async function promptYesNo(
357
+ export interface PromptYesNoOptions {
358
+ defaultYes: boolean;
359
+ out: (text: string) => void;
360
+ /** Give up after this long; `0` (or omitted) waits forever. */
361
+ timeoutMs?: number;
362
+ /** Answer source; defaults to `process.stdin`. */
363
+ input?: NodeJS.ReadableStream;
364
+ }
365
+
366
+ /**
367
+ * Ask a yes/no question. Resolves `undefined` when the answer never comes —
368
+ * the prompt timed out, or stdin closed on an empty line.
369
+ */
370
+ export async function promptYesNo(
278
371
  question: string,
279
- options: {
280
- defaultYes: boolean;
281
- out: (text: string) => void;
282
- }
283
- ): Promise<boolean> {
372
+ options: PromptYesNoOptions
373
+ ): Promise<boolean | undefined> {
284
374
  const hint = options.defaultYes ? "Y/n" : "y/N";
375
+ const input = options.input ?? process.stdin;
285
376
  options.out(`${question} [${hint}] `);
286
- process.stdin.resume();
287
- const line = await readOneLine(process.stdin);
288
- process.stdin.pause();
289
- if (line === undefined || line.trim() === "") {
377
+ input.resume();
378
+ const line = await readOneLine(input, options.timeoutMs);
379
+ input.pause();
380
+ if (line === undefined) {
381
+ options.out("\n");
382
+ return undefined;
383
+ }
384
+ if (line.trim() === "") {
290
385
  return options.defaultYes;
291
386
  }
292
387
  const answer = line.trim().toLowerCase();
@@ -299,11 +394,14 @@ async function promptYesNo(
299
394
  return options.defaultYes;
300
395
  }
301
396
 
397
+ /** The line, or `undefined` when the stream ends or the clock runs out first. */
302
398
  function readOneLine(
303
- stream: NodeJS.ReadableStream
399
+ stream: NodeJS.ReadableStream,
400
+ timeoutMs?: number
304
401
  ): Promise<string | undefined> {
305
402
  return new Promise((resolve) => {
306
403
  let buffer = "";
404
+ let timer: NodeJS.Timeout | undefined;
307
405
  const onData = (chunk: string | Buffer): void => {
308
406
  buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
309
407
  const newline = buffer.indexOf("\n");
@@ -313,15 +411,25 @@ function readOneLine(
313
411
  cleanup();
314
412
  resolve(buffer.slice(0, newline).replace(/\r$/, ""));
315
413
  };
316
- const onEnd = (): void => {
414
+ // EOF and the timeout both land here. A tty holds a half-typed line until
415
+ // Enter, so a non-empty buffer only ever happens on piped input.
416
+ const giveUp = (): void => {
317
417
  cleanup();
318
418
  resolve(buffer === "" ? undefined : buffer.replace(/\r$/, ""));
319
419
  };
320
420
  const cleanup = (): void => {
421
+ if (timer !== undefined) {
422
+ clearTimeout(timer);
423
+ }
321
424
  stream.off("data", onData);
322
- stream.off("end", onEnd);
425
+ stream.off("end", giveUp);
323
426
  };
324
427
  stream.on("data", onData);
325
- stream.on("end", onEnd);
428
+ stream.on("end", giveUp);
429
+ if (timeoutMs !== undefined && timeoutMs > 0) {
430
+ timer = setTimeout(giveUp, timeoutMs);
431
+ // Never keep the process alive purely to wait for an answer.
432
+ timer.unref();
433
+ }
326
434
  });
327
435
  }