@cursor/july 0.1.18 → 0.1.19

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 (67) hide show
  1. package/dist/bin/agent-serve.js +20 -3
  2. package/dist/docs/404.html +1 -1
  3. package/dist/docs/ab.html +2 -2
  4. package/dist/docs/assets/{app.BR5EdVSq.js → app.CrsWMchO.js} +1 -1
  5. package/dist/docs/assets/chunks/@localSearchIndexroot.BnHRjfoe.js +1 -0
  6. package/dist/docs/assets/chunks/{VPLocalSearchBox.CNCzfUKC.js → VPLocalSearchBox.D1JqzSh8.js} +1 -1
  7. package/dist/docs/assets/chunks/{theme.CwKvMSWv.js → theme.DaBvZYwl.js} +2 -2
  8. package/dist/docs/building-with-agents.html +2 -2
  9. package/dist/docs/concepts.html +2 -2
  10. package/dist/docs/deployment.html +2 -2
  11. package/dist/docs/evals.html +2 -2
  12. package/dist/docs/example-agents/approval-buddy.html +2 -2
  13. package/dist/docs/example-agents/benny.html +2 -2
  14. package/dist/docs/example-agents/bugbot.html +2 -2
  15. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  16. package/dist/docs/example-agents/codeowners-review.html +2 -2
  17. package/dist/docs/example-agents/concierge.html +2 -2
  18. package/dist/docs/example-agents/fsd.html +2 -2
  19. package/dist/docs/example-agents/index.html +2 -2
  20. package/dist/docs/example-agents/knowledge-base.html +2 -2
  21. package/dist/docs/example-agents/oncall.html +2 -2
  22. package/dist/docs/example-agents/security-reviewer.html +2 -2
  23. package/dist/docs/example-agents/slack-agent.html +2 -2
  24. package/dist/docs/example-agents/weather-agent.html +2 -2
  25. package/dist/docs/guides/agent-to-agent.html +2 -2
  26. package/dist/docs/guides/cloud-runtime.html +2 -2
  27. package/dist/docs/guides/github.html +2 -2
  28. package/dist/docs/guides/human-in-the-loop.html +2 -2
  29. package/dist/docs/guides/mcp-oauth.html +2 -2
  30. package/dist/docs/guides/slack.html +2 -2
  31. package/dist/docs/guides/webhooks.html +2 -2
  32. package/dist/docs/hillclimbing.html +2 -2
  33. package/dist/docs/index.html +2 -2
  34. package/dist/docs/quickstart.html +2 -2
  35. package/dist/docs/reference/agent-config.html +2 -2
  36. package/dist/docs/reference/channels.html +2 -2
  37. package/dist/docs/reference/cli.html +2 -2
  38. package/dist/docs/reference/connections.html +2 -2
  39. package/dist/docs/reference/hooks.html +2 -2
  40. package/dist/docs/reference/http-api.html +2 -2
  41. package/dist/docs/reference/instructions.html +2 -2
  42. package/dist/docs/reference/playground.html +2 -2
  43. package/dist/docs/reference/project-layout.html +2 -2
  44. package/dist/docs/reference/prompt.html +2 -2
  45. package/dist/docs/reference/schedules.html +2 -2
  46. package/dist/docs/reference/sessions.html +2 -2
  47. package/dist/docs/reference/skills.html +2 -2
  48. package/dist/docs/reference/subagents.html +2 -2
  49. package/dist/docs/reference/tools.html +2 -2
  50. package/dist/docs/scaffolding-agents.html +2 -2
  51. package/dist/docs/storage.html +2 -2
  52. package/dist/docs/troubleshooting.html +2 -2
  53. package/dist/internal/cli-skills.d.ts +19 -0
  54. package/dist/internal/cli-skills.d.ts.map +1 -0
  55. package/dist/internal/cli-skills.js +72 -0
  56. package/dist/internal/install-cursor-skills.d.ts +46 -4
  57. package/dist/internal/install-cursor-skills.d.ts.map +1 -1
  58. package/dist/internal/install-cursor-skills.js +117 -49
  59. package/dist/playground/assets/{index-DRjS5klQ.js → index-C2SU2xV5.js} +23 -23
  60. package/dist/playground/assets/index-CidizGZv.css +1 -0
  61. package/dist/playground/index.html +2 -2
  62. package/package.json +1 -1
  63. package/src/bin/agent-serve.ts +21 -3
  64. package/src/internal/cli-skills.ts +95 -0
  65. package/src/internal/install-cursor-skills.ts +168 -60
  66. package/dist/docs/assets/chunks/@localSearchIndexroot.BOzkeUi8.js +0 -1
  67. package/dist/playground/assets/index-TCDs-UhZ.css +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"install-cursor-skills.d.ts","sourceRoot":"","sources":["../../src/internal/install-cursor-skills.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmBH,+EAA+E;AAC/E,eAAO,MAAM,uBAAuB,aAAa,CAAC;AAElD,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,GAAE,MAAsC,GAC3C,MAAM,CAGR;AAED,wEAAwE;AACxE,wBAAgB,uBAAuB,CACrC,UAAU,GAAE,MAA2B,GACtC,MAAM,CAER;AAED,mEAAmE;AACnE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,EACpC,UAAU,GAAE,MAA2B,GACtC,MAAM,CAER;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,WAAW,GAAE,MAAyB,GACrC,OAAO,CAAC,YAAY,EAAE,CAAC,CA2BzB;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,CAAC,EAAE;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAY1B;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,SAAS,YAAY,EAAE,EAC/B,UAAU,GAAE,MAA2B,GACtC,OAAO,CAAC,MAAM,EAAE,CAAC,CAiCnB;AAED,oFAAoF;AACpF,wBAAgB,oCAAoC,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAY5E;AAoBD,MAAM,WAAW,+BAA+B;IAC9C,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,GAAE,+BAAoC,GAC5C,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CA+CpD"}
1
+ {"version":3,"file":"install-cursor-skills.d.ts","sourceRoot":"","sources":["../../src/internal/install-cursor-skills.ts"],"names":[],"mappings":"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"}
@@ -18,12 +18,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
18
18
  import { access, cp, mkdir, readdir, readFile, rm, writeFile, } 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
  /** Monorepo-only skill; excluded from the published package. */
24
24
  const SKIP_SKILL_DIRS = new Set(["operate"]);
25
25
  /** Category folder under ~/.cursor/skills (Cursor discovers nested skills). */
26
26
  export const CURSOR_SKILLS_NAMESPACE = "agentkit";
27
+ /** How long the `init` offer waits for an answer before skipping the install. */
28
+ export const SKILL_PROMPT_TIMEOUT_MS = 45000;
27
29
  export function cursorSkillsHome(home) {
28
30
  var _a;
29
31
  if (home === void 0) { home = (_a = process.env.HOME) !== null && _a !== void 0 ? _a : homedir(); }
@@ -105,21 +107,9 @@ export function installCursorSkills(skills_1) {
105
107
  return __awaiter(this, arguments, void 0, function* (skills, skillsHome = cursorSkillsHome()) {
106
108
  const namespaceDir = agentkitCursorSkillsDir(skillsHome);
107
109
  yield mkdir(namespaceDir, { recursive: true });
108
- const shippedLeaves = new Set(skills.map((skill) => skill.leafDir));
109
- let existingLeaves = [];
110
- try {
111
- const entries = yield readdir(namespaceDir, { withFileTypes: true });
112
- existingLeaves = entries
113
- .filter((entry) => entry.isDirectory())
114
- .map((entry) => entry.name);
115
- }
116
- catch (_a) {
117
- existingLeaves = [];
118
- }
119
- for (const leaf of existingLeaves) {
120
- if (!shippedLeaves.has(leaf)) {
121
- yield rm(join(namespaceDir, leaf), { recursive: true, force: true });
122
- }
110
+ const removed = yield orphanLeaves(namespaceDir, skills);
111
+ for (const leaf of removed) {
112
+ yield rm(join(namespaceDir, leaf), { recursive: true, force: true });
123
113
  }
124
114
  const installed = [];
125
115
  for (const skill of skills) {
@@ -133,7 +123,23 @@ export function installCursorSkills(skills_1) {
133
123
  }
134
124
  installed.push(skill.leafDir);
135
125
  }
136
- return installed;
126
+ return { installed, removed };
127
+ });
128
+ }
129
+ /** Installed leaf dirs the package no longer ships. Installing deletes these. */
130
+ function orphanLeaves(namespaceDir, skills) {
131
+ return __awaiter(this, void 0, void 0, function* () {
132
+ const shipped = new Set(skills.map((skill) => skill.leafDir));
133
+ let entries;
134
+ try {
135
+ entries = yield readdir(namespaceDir, { withFileTypes: true });
136
+ }
137
+ catch (_a) {
138
+ return [];
139
+ }
140
+ return entries
141
+ .filter((entry) => entry.isDirectory() && !shipped.has(entry.name))
142
+ .map((entry) => entry.name);
137
143
  });
138
144
  }
139
145
  /** Drop monorepo `paths` globs and fix package doc links for ~/.cursor installs. */
@@ -166,48 +172,88 @@ function stripPathsFromFrontmatter(frontmatter) {
166
172
  }
167
173
  return out.join("\n");
168
174
  }
175
+ /** What an install would do, for the offer and for `install-skills`. */
176
+ export function planSkillInstall(options) {
177
+ return __awaiter(this, void 0, void 0, function* () {
178
+ var _a;
179
+ const skillsHome = (_a = options === null || options === void 0 ? void 0 : options.skillsHome) !== null && _a !== void 0 ? _a : cursorSkillsHome();
180
+ const directory = agentkitCursorSkillsDir(skillsHome);
181
+ const shipped = yield listShippedPackageSkills(options === null || options === void 0 ? void 0 : options.packageRoot);
182
+ const missing = yield findMissingCursorSkills({
183
+ packageRoot: options === null || options === void 0 ? void 0 : options.packageRoot,
184
+ skillsHome,
185
+ });
186
+ return {
187
+ shipped,
188
+ skillsHome,
189
+ directory,
190
+ leaves: shipped.map((skill) => skill.leafDir),
191
+ firstInstall: missing.length === shipped.length,
192
+ orphans: yield orphanLeaves(directory, shipped),
193
+ };
194
+ });
195
+ }
196
+ /** `Installed 9 agentkit skills`, what it dropped, and where it went. */
197
+ export function formatSkillInstall(plan, result) {
198
+ const noun = result.installed.length === 1 ? "skill" : "skills";
199
+ const verb = plan.firstInstall ? "Installed" : "Updated";
200
+ const lines = [`${verb} ${result.installed.length} agentkit ${noun}`];
201
+ if (result.removed.length > 0) {
202
+ lines.push(` removed: ${result.removed.join(", ")}`);
203
+ }
204
+ lines.push(` location: ${plan.directory}`);
205
+ return `${lines.join("\n")}\n`;
206
+ }
169
207
  /**
170
208
  * On a TTY, offer to install/update every shipped package skill into
171
209
  * ~/.cursor/skills/agentkit (always overwriting). No-op when non-interactive
172
- * or the package ships no skills.
210
+ * or the package ships no skills. An unanswered prompt skips the install
211
+ * rather than blocking the caller — see `SKILL_PROMPT_TIMEOUT_MS`.
173
212
  */
174
213
  export function offerInstallCursorSkills() {
175
214
  return __awaiter(this, arguments, void 0, function* (options = {}) {
176
- var _a, _b, _c, _d;
215
+ var _a, _b, _c;
177
216
  const out = (_a = options.out) !== null && _a !== void 0 ? _a : ((text) => process.stdout.write(text));
178
217
  const interactive = (_b = options.interactive) !== null && _b !== void 0 ? _b : (process.stdin.isTTY === true && process.stdout.isTTY === true);
179
- const shipped = yield listShippedPackageSkills(options.packageRoot);
180
- if (shipped.length === 0) {
218
+ if (!interactive) {
181
219
  return { installed: [], skipped: true };
182
220
  }
183
- const home = (_c = options.skillsHome) !== null && _c !== void 0 ? _c : cursorSkillsHome();
184
- const targetDir = agentkitCursorSkillsDir(home);
185
- const missing = yield findMissingCursorSkills({
221
+ const plan = yield planSkillInstall({
186
222
  packageRoot: options.packageRoot,
187
- skillsHome: home,
223
+ skillsHome: options.skillsHome,
188
224
  });
189
- const names = shipped.map((skill) => skill.leafDir).join(", ");
190
- const noun = shipped.length === 1 ? "skill" : "skills";
191
- const verb = missing.length === shipped.length ? "Install" : "Update";
192
- const question = `${verb} ${shipped.length} agentkit ${noun} in ${targetDir}?`;
193
- if (!interactive) {
225
+ if (plan.shipped.length === 0) {
194
226
  return { installed: [], skipped: true };
195
227
  }
228
+ const noun = plan.shipped.length === 1 ? "skill" : "skills";
229
+ const verb = plan.firstInstall ? "Install" : "Update";
196
230
  const c = stdoutPalette();
197
- out(`${question}\n`);
198
- out(c.dim(` ${names}\n`));
199
- if (missing.length < shipped.length) {
231
+ out(`${verb} ${plan.shipped.length} agentkit ${noun} in ${plan.directory}?\n`);
232
+ out(c.dim(` ${plan.leaves.join(", ")}\n`));
233
+ if (!plan.firstInstall) {
200
234
  out(c.dim(" (overwrites existing copies with the package version)\n"));
201
235
  }
202
- const confirm = (_d = options.confirm) !== null && _d !== void 0 ? _d : ((q) => promptYesNo(q, { defaultYes: true, out }));
203
- const accepted = yield confirm(missing.length === shipped.length ? "Install now?" : "Update now?");
204
- if (!accepted) {
205
- out(c.dim("Skipped skill install.\n\n"));
236
+ if (plan.orphans.length > 0) {
237
+ out(c.dim(` (deletes ${plan.orphans.join(", ")})\n`));
238
+ }
239
+ const confirm = (_c = options.confirm) !== null && _c !== void 0 ? _c : ((q) => promptYesNo(q, {
240
+ defaultYes: true,
241
+ out,
242
+ timeoutMs: SKILL_PROMPT_TIMEOUT_MS,
243
+ }));
244
+ const accepted = yield confirm(`${verb} now?`);
245
+ if (accepted !== true) {
246
+ // `undefined` is the prompt timing out or stdin closing; both mean nobody
247
+ // said yes, so do not claim a duration neither case is guaranteed to have.
248
+ const why = accepted === undefined
249
+ ? "No answer — skipped the skill install."
250
+ : "Skipped skill install.";
251
+ out(c.dim(`${why}\nInstall later: ${CLI_COMMAND_NAME} install-skills (${plan.directory})\n\n`));
206
252
  return { installed: [], skipped: true };
207
253
  }
208
- const installed = yield installCursorSkills(shipped, home);
209
- out(`${verb === "Install" ? "Installed" : "Updated"} ${installed.length} ${noun} in ${targetDir}\n\n`);
210
- return { installed, skipped: false };
254
+ const result = yield installCursorSkills(plan.shipped, plan.skillsHome);
255
+ out(`${formatSkillInstall(plan, result)}\n`);
256
+ return { installed: result.installed, skipped: false };
211
257
  });
212
258
  }
213
259
  function parseSkillName(skillMd) {
@@ -228,14 +274,24 @@ function parseSkillName(skillMd) {
228
274
  // Strip optional quotes around the YAML scalar.
229
275
  return raw.replace(/^["']|["']$/g, "");
230
276
  }
231
- function promptYesNo(question, options) {
277
+ /**
278
+ * Ask a yes/no question. Resolves `undefined` when the answer never comes —
279
+ * the prompt timed out, or stdin closed on an empty line.
280
+ */
281
+ export function promptYesNo(question, options) {
232
282
  return __awaiter(this, void 0, void 0, function* () {
283
+ var _a;
233
284
  const hint = options.defaultYes ? "Y/n" : "y/N";
285
+ const input = (_a = options.input) !== null && _a !== void 0 ? _a : process.stdin;
234
286
  options.out(`${question} [${hint}] `);
235
- process.stdin.resume();
236
- const line = yield readOneLine(process.stdin);
237
- process.stdin.pause();
238
- if (line === undefined || line.trim() === "") {
287
+ input.resume();
288
+ const line = yield readOneLine(input, options.timeoutMs);
289
+ input.pause();
290
+ if (line === undefined) {
291
+ options.out("\n");
292
+ return undefined;
293
+ }
294
+ if (line.trim() === "") {
239
295
  return options.defaultYes;
240
296
  }
241
297
  const answer = line.trim().toLowerCase();
@@ -248,9 +304,11 @@ function promptYesNo(question, options) {
248
304
  return options.defaultYes;
249
305
  });
250
306
  }
251
- function readOneLine(stream) {
307
+ /** The line, or `undefined` when the stream ends or the clock runs out first. */
308
+ function readOneLine(stream, timeoutMs) {
252
309
  return new Promise((resolve) => {
253
310
  let buffer = "";
311
+ let timer;
254
312
  const onData = (chunk) => {
255
313
  buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
256
314
  const newline = buffer.indexOf("\n");
@@ -260,15 +318,25 @@ function readOneLine(stream) {
260
318
  cleanup();
261
319
  resolve(buffer.slice(0, newline).replace(/\r$/, ""));
262
320
  };
263
- const onEnd = () => {
321
+ // EOF and the timeout both land here. A tty holds a half-typed line until
322
+ // Enter, so a non-empty buffer only ever happens on piped input.
323
+ const giveUp = () => {
264
324
  cleanup();
265
325
  resolve(buffer === "" ? undefined : buffer.replace(/\r$/, ""));
266
326
  };
267
327
  const cleanup = () => {
328
+ if (timer !== undefined) {
329
+ clearTimeout(timer);
330
+ }
268
331
  stream.off("data", onData);
269
- stream.off("end", onEnd);
332
+ stream.off("end", giveUp);
270
333
  };
271
334
  stream.on("data", onData);
272
- stream.on("end", onEnd);
335
+ stream.on("end", giveUp);
336
+ if (timeoutMs !== undefined && timeoutMs > 0) {
337
+ timer = setTimeout(giveUp, timeoutMs);
338
+ // Never keep the process alive purely to wait for an answer.
339
+ timer.unref();
340
+ }
273
341
  });
274
342
  }