@cursor/july 0.1.17 → 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.
- package/dist/bin/agent-serve.js +20 -3
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +1 -1
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +4 -1
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +6 -1
- package/dist/channels/slack/index.d.ts +1 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +1 -0
- package/dist/channels/slack/init.d.ts.map +1 -1
- package/dist/channels/slack/init.js +4 -1
- package/dist/channels/slack/interactive.d.ts.map +1 -1
- package/dist/channels/slack/interactive.js +1 -0
- package/dist/channels/slack/manifest.d.ts +10 -0
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +33 -14
- package/dist/channels/slack/message-body.d.ts +13 -0
- package/dist/channels/slack/message-body.d.ts.map +1 -0
- package/dist/channels/slack/message-body.js +82 -0
- package/dist/channels/slack/thread-context.d.ts +0 -2
- package/dist/channels/slack/thread-context.d.ts.map +1 -1
- package/dist/channels/slack/thread-context.js +1 -65
- package/dist/channels/slack/types.d.ts +16 -1
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.BbEXr2KO.js → app.CrsWMchO.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BnHRjfoe.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DBEi4HDp.js → VPLocalSearchBox.D1JqzSh8.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DGfptcHw.js → theme.DaBvZYwl.js} +2 -2
- package/dist/docs/assets/{example-agents_security-reviewer.md.BZSNycMf.js → example-agents_security-reviewer.md.Dhj_m7_B.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +5 -5
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/internal/builtin-tools/index.d.ts +10 -0
- package/dist/internal/builtin-tools/index.d.ts.map +1 -0
- package/dist/internal/builtin-tools/index.js +27 -0
- package/dist/internal/builtin-tools/reminders.d.ts +9 -0
- package/dist/internal/builtin-tools/reminders.d.ts.map +1 -0
- package/dist/internal/builtin-tools/reminders.js +138 -0
- package/dist/internal/cli-skills.d.ts +19 -0
- package/dist/internal/cli-skills.d.ts.map +1 -0
- package/dist/internal/cli-skills.js +72 -0
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +36 -15
- package/dist/internal/cursor-event-relay.d.ts +30 -0
- package/dist/internal/cursor-event-relay.d.ts.map +1 -1
- package/dist/internal/cursor-event-relay.js +37 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +41 -0
- package/dist/internal/install-cursor-skills.d.ts +46 -4
- package/dist/internal/install-cursor-skills.d.ts.map +1 -1
- package/dist/internal/install-cursor-skills.js +117 -49
- package/dist/internal/reminder-runner.d.ts +7 -0
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +21 -0
- package/dist/internal/sdk-runner.d.ts +15 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +113 -13
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +1 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +9 -0
- package/dist/internal/slack-provision-client.d.ts +6 -3
- package/dist/internal/slack-provision-client.d.ts.map +1 -1
- package/dist/internal/slack-provision-client.js +5 -3
- package/dist/playground/assets/{index-DNqirsrK.js → index-C2SU2xV5.js} +43 -43
- package/dist/playground/index.html +1 -1
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/example-agents/security-reviewer.md +1 -1
- package/package.json +1 -1
- package/skills/setup-slack/SKILL.md +4 -0
- package/src/bin/agent-serve.ts +21 -3
- package/src/channels/slack/cursor-account.ts +1 -0
- package/src/channels/slack/dispatch.ts +3 -1
- package/src/channels/slack/inbound.ts +6 -1
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/init.ts +3 -0
- package/src/channels/slack/interactive.ts +1 -0
- package/src/channels/slack/manifest.ts +34 -6
- package/src/channels/slack/message-body.ts +90 -0
- package/src/channels/slack/thread-context.ts +1 -69
- package/src/channels/slack/types.ts +16 -1
- package/src/internal/builtin-tools/index.ts +40 -0
- package/src/internal/builtin-tools/reminders.ts +158 -0
- package/src/internal/cli-skills.ts +95 -0
- package/src/internal/cli-slack.ts +38 -14
- package/src/internal/cursor-event-relay.ts +67 -0
- package/src/internal/discovery.ts +52 -0
- package/src/internal/install-cursor-skills.ts +168 -60
- package/src/internal/reminder-runner.ts +30 -0
- package/src/internal/sdk-runner.ts +131 -13
- package/src/internal/server.ts +2 -0
- package/src/internal/session-engine.ts +14 -0
- package/src/internal/slack-provision-client.ts +7 -3
- package/src/types.ts +33 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.DCBsrfwP.js +0 -1
- /package/dist/docs/assets/{example-agents_security-reviewer.md.BZSNycMf.lean.js → example-agents_security-reviewer.md.Dhj_m7_B.lean.js} +0 -0
|
@@ -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<
|
|
136
|
+
): Promise<SkillInstallResult> {
|
|
127
137
|
const namespaceDir = agentkitCursorSkillsDir(skillsHome);
|
|
128
138
|
await mkdir(namespaceDir, { recursive: true });
|
|
129
139
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
217
|
-
if (shipped.length === 0) {
|
|
284
|
+
if (!interactive) {
|
|
218
285
|
return { installed: [], skipped: true };
|
|
219
286
|
}
|
|
220
287
|
|
|
221
|
-
const
|
|
222
|
-
const targetDir = agentkitCursorSkillsDir(home);
|
|
223
|
-
const missing = await findMissingCursorSkills({
|
|
288
|
+
const plan = await planSkillInstall({
|
|
224
289
|
packageRoot: options.packageRoot,
|
|
225
|
-
skillsHome:
|
|
290
|
+
skillsHome: options.skillsHome,
|
|
226
291
|
});
|
|
227
|
-
|
|
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(
|
|
238
|
-
|
|
239
|
-
|
|
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 ??
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
254
|
-
out(
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
const line = await readOneLine(
|
|
288
|
-
|
|
289
|
-
if (line === undefined
|
|
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
|
-
|
|
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",
|
|
425
|
+
stream.off("end", giveUp);
|
|
323
426
|
};
|
|
324
427
|
stream.on("data", onData);
|
|
325
|
-
stream.on("end",
|
|
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
|
}
|
|
@@ -336,6 +336,36 @@ export class ReminderRunner {
|
|
|
336
336
|
return n;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Re-key armed reminders when a channel rotates a session's continuation
|
|
341
|
+
* token (the built-in HTTP channel rotates after every follow-up). Without
|
|
342
|
+
* this, a reminder armed before the rotation misses its session at fire
|
|
343
|
+
* time and disarms as `session_gone`.
|
|
344
|
+
*/
|
|
345
|
+
async rekeyContinuation(
|
|
346
|
+
channelId: string,
|
|
347
|
+
oldToken: string,
|
|
348
|
+
newToken: string
|
|
349
|
+
): Promise<number> {
|
|
350
|
+
const armed = await this.store.listArmed();
|
|
351
|
+
let n = 0;
|
|
352
|
+
for (const record of armed) {
|
|
353
|
+
if (
|
|
354
|
+
record.channelId !== channelId ||
|
|
355
|
+
record.continuationToken !== oldToken
|
|
356
|
+
) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
await this.persist({
|
|
360
|
+
...record,
|
|
361
|
+
continuationToken: newToken,
|
|
362
|
+
updatedAt: new Date().toISOString(),
|
|
363
|
+
});
|
|
364
|
+
n += 1;
|
|
365
|
+
}
|
|
366
|
+
return n;
|
|
367
|
+
}
|
|
368
|
+
|
|
339
369
|
/** Fire one reminder immediately (dev / hillclimb). */
|
|
340
370
|
async dispatch(reminderId: string): Promise<{
|
|
341
371
|
reminderId: string;
|
|
@@ -86,16 +86,90 @@ export interface CursorSdkRunnerOptions {
|
|
|
86
86
|
logger?: (line: string) => void;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* How long one SDK agent handle (and the Connect transport inside it) may be
|
|
91
|
+
* reused before the runner retires it.
|
|
92
|
+
*
|
|
93
|
+
* The transport exchanges the account API key for a short-lived access token
|
|
94
|
+
* (`SessionType.API_KEY_TOKEN`, 60 minutes) once per handle, and has no
|
|
95
|
+
* reliable refresh: an Unauthenticated rejection on the agent's streaming RPC
|
|
96
|
+
* surfaces while the stream is being read — outside the transport's
|
|
97
|
+
* request-time 401 retry — so an expired token gets replayed on every
|
|
98
|
+
* subsequent turn until the handle is rebuilt. Long-lived sessions (watch
|
|
99
|
+
* wakes, crons) hit this at the ~60 minute mark and then fail forever.
|
|
100
|
+
* Retiring the handle comfortably before the token can expire keeps them
|
|
101
|
+
* healthy: `Agent.resume` rehydrates the same conversation from the runner
|
|
102
|
+
* store on a fresh transport, so recycling costs only transport warmth.
|
|
103
|
+
*/
|
|
104
|
+
const MAX_AGENT_HANDLE_AGE_MS = 45 * 60 * 1000;
|
|
105
|
+
|
|
106
|
+
interface CachedAgentHandle {
|
|
107
|
+
agent: Promise<SDKAgent>;
|
|
108
|
+
openedAtMs: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The backend's `NotLoggedIn` copy ("Authentication error" title, "try
|
|
113
|
+
* logging out and back in." detail) as it surfaces through SDK RunResults.
|
|
114
|
+
* Deliberately narrow: this gates a one-shot in-place retry, and matching
|
|
115
|
+
* broader error text would retry turns that a fresh transport cannot fix.
|
|
116
|
+
*/
|
|
117
|
+
export function isAuthErrorMessage(message: string | undefined): boolean {
|
|
118
|
+
if (message === undefined) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return /authentication error|try logging out and back in/i.test(message);
|
|
122
|
+
}
|
|
123
|
+
|
|
89
124
|
export class CursorSdkRunner implements AgentRunner {
|
|
90
|
-
private readonly agents = new Map<string,
|
|
125
|
+
private readonly agents = new Map<string, CachedAgentHandle>();
|
|
91
126
|
private readonly stores = new Map<string, Promise<LocalAgentStore>>();
|
|
92
127
|
private readonly prewarmReleases: Array<() => Promise<void>> = [];
|
|
93
128
|
|
|
94
129
|
constructor(private readonly options: CursorSdkRunnerOptions = {}) {}
|
|
95
130
|
|
|
96
131
|
async runTurn(request: RunnerTurnRequest): Promise<TurnOutcome> {
|
|
132
|
+
let sawUpdate = false;
|
|
133
|
+
const tracked: RunnerTurnRequest = {
|
|
134
|
+
...request,
|
|
135
|
+
onUpdate: (update) => {
|
|
136
|
+
sawUpdate = true;
|
|
137
|
+
return request.onUpdate(update);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const outcome = await this.runTurnAttempt(tracked);
|
|
141
|
+
if (
|
|
142
|
+
outcome.status !== "error" ||
|
|
143
|
+
!isAuthErrorMessage(outcome.errorMessage) ||
|
|
144
|
+
sawUpdate ||
|
|
145
|
+
request.isFirstTurn ||
|
|
146
|
+
request.signal?.aborted === true
|
|
147
|
+
) {
|
|
148
|
+
return outcome;
|
|
149
|
+
}
|
|
150
|
+
// The turn died on an authentication failure before producing any output
|
|
151
|
+
// — the signature of a handle whose exchanged access token expired (the
|
|
152
|
+
// failed attempt already evicted it). Retrying once on a fresh handle is
|
|
153
|
+
// safe here because nothing streamed and no tool ran. First turns are
|
|
154
|
+
// excluded: their token was exchanged moments ago, so the credential
|
|
155
|
+
// itself is the problem and a retry cannot succeed.
|
|
156
|
+
this.log(
|
|
157
|
+
`[agentkit] session ${request.sessionId}: retrying the turn on a fresh SDK agent handle after an authentication failure`
|
|
158
|
+
);
|
|
159
|
+
try {
|
|
160
|
+
return await this.runTurnAttempt(tracked);
|
|
161
|
+
} catch {
|
|
162
|
+
// The retry was best effort; report the original failure.
|
|
163
|
+
return outcome;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async runTurnAttempt(
|
|
168
|
+
request: RunnerTurnRequest
|
|
169
|
+
): Promise<TurnOutcome> {
|
|
170
|
+
const agentPromise = this.getAgent(request);
|
|
97
171
|
try {
|
|
98
|
-
const agent = await
|
|
172
|
+
const agent = await agentPromise;
|
|
99
173
|
const run = await agent.send(request.prompt, {
|
|
100
174
|
onDelta: async ({ update }) => {
|
|
101
175
|
try {
|
|
@@ -141,14 +215,24 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
141
215
|
}
|
|
142
216
|
try {
|
|
143
217
|
const result = await run.wait();
|
|
144
|
-
|
|
218
|
+
const outcome = toTurnOutcome(result);
|
|
219
|
+
if (outcome.status === "error") {
|
|
220
|
+
// An error result usually means the handle's transport is poisoned
|
|
221
|
+
// — most often a cached access token that aged past its 60-minute
|
|
222
|
+
// lifetime and gets replayed on every request (stream failures
|
|
223
|
+
// bypass the transport's 401 retry). Retire the handle so the next
|
|
224
|
+
// attempt resumes the conversation from the store on a fresh
|
|
225
|
+
// transport instead of failing every turn until a restart.
|
|
226
|
+
this.evictAgent(request.sessionId, agentPromise);
|
|
227
|
+
}
|
|
228
|
+
return outcome;
|
|
145
229
|
} finally {
|
|
146
230
|
signal?.removeEventListener("abort", onAbort);
|
|
147
231
|
}
|
|
148
232
|
} catch (error) {
|
|
149
233
|
// Drop the cached agent so a dead Connect/HTTP2 session cannot poison
|
|
150
234
|
// the next turn (terminal/shell overflow often kills the transport).
|
|
151
|
-
this.
|
|
235
|
+
this.evictAgent(request.sessionId, agentPromise);
|
|
152
236
|
throw error;
|
|
153
237
|
}
|
|
154
238
|
}
|
|
@@ -178,7 +262,9 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
178
262
|
async dispose(): Promise<void> {
|
|
179
263
|
const releases = this.prewarmReleases.splice(0);
|
|
180
264
|
await Promise.allSettled(releases.map((release) => release()));
|
|
181
|
-
const agents = await Promise.allSettled(
|
|
265
|
+
const agents = await Promise.allSettled(
|
|
266
|
+
[...this.agents.values()].map((handle) => handle.agent)
|
|
267
|
+
);
|
|
182
268
|
this.agents.clear();
|
|
183
269
|
for (const settled of agents) {
|
|
184
270
|
if (settled.status === "fulfilled") {
|
|
@@ -188,17 +274,49 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
188
274
|
}
|
|
189
275
|
|
|
190
276
|
private getAgent(request: RunnerTurnRequest): Promise<SDKAgent> {
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
this
|
|
277
|
+
const cached = this.agents.get(request.sessionId);
|
|
278
|
+
if (cached !== undefined) {
|
|
279
|
+
const ageMs = Date.now() - cached.openedAtMs;
|
|
280
|
+
if (ageMs < MAX_AGENT_HANDLE_AGE_MS) {
|
|
281
|
+
return cached.agent;
|
|
282
|
+
}
|
|
283
|
+
// Past this age the handle's exchanged access token could expire
|
|
284
|
+
// mid-turn. Retire it and reopen below on a fresh transport.
|
|
285
|
+
this.log(
|
|
286
|
+
`[agentkit] session ${request.sessionId}: recycling the SDK agent handle after ${Math.round(ageMs / 60_000)} min so the turn runs on a fresh access token`
|
|
287
|
+
);
|
|
288
|
+
this.evictAgent(request.sessionId, cached.agent);
|
|
198
289
|
}
|
|
290
|
+
const agentPromise = this.openAgent(request).catch((error) => {
|
|
291
|
+
// Nothing opened, so there is no handle to close — just free the slot.
|
|
292
|
+
this.agents.delete(request.sessionId);
|
|
293
|
+
throw error;
|
|
294
|
+
});
|
|
295
|
+
this.agents.set(request.sessionId, {
|
|
296
|
+
agent: agentPromise,
|
|
297
|
+
openedAtMs: Date.now(),
|
|
298
|
+
});
|
|
199
299
|
return agentPromise;
|
|
200
300
|
}
|
|
201
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Drop a cached handle and close it best-effort to release its transport
|
|
304
|
+
* and executor lease. Identity-checked so a turn that lost a race with a
|
|
305
|
+
* newer open cannot evict the replacement handle.
|
|
306
|
+
*/
|
|
307
|
+
private evictAgent(sessionId: string, agent: Promise<SDKAgent>): void {
|
|
308
|
+
const cached = this.agents.get(sessionId);
|
|
309
|
+
if (cached === undefined || cached.agent !== agent) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
this.agents.delete(sessionId);
|
|
313
|
+
void cached.agent.then((sdkAgent) => sdkAgent.close()).catch(() => {});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private log(line: string): void {
|
|
317
|
+
(this.options.logger ?? (() => {}))(line);
|
|
318
|
+
}
|
|
319
|
+
|
|
202
320
|
private requireApiKey(): string {
|
|
203
321
|
const apiKey = this.options.apiKey ?? process.env.CURSOR_API_KEY;
|
|
204
322
|
if (apiKey === undefined || apiKey.trim() === "") {
|
|
@@ -274,7 +392,7 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
274
392
|
// conversation under the same id beats failing this thread's turns
|
|
275
393
|
// forever ("Agent … not found" on every follow-up). The channel's
|
|
276
394
|
// warm-turn prompt context re-hydrates the thread history.
|
|
277
|
-
|
|
395
|
+
this.log(
|
|
278
396
|
`[agentkit] no stored conversation for ${agentId} on this host; starting a fresh one`
|
|
279
397
|
);
|
|
280
398
|
return Agent.create({ ...options, agentId });
|
package/src/internal/server.ts
CHANGED
|
@@ -421,6 +421,8 @@ export async function startServer(
|
|
|
421
421
|
cancel: (id, reason) => reminderRunner.cancel(id, reason),
|
|
422
422
|
cancelForContinuation: (token, reason) =>
|
|
423
423
|
reminderRunner.cancelForContinuation(token, reason),
|
|
424
|
+
rekeyContinuation: (channelId, oldToken, newToken) =>
|
|
425
|
+
reminderRunner.rekeyContinuation(channelId, oldToken, newToken),
|
|
424
426
|
dispatch: (id) => reminderRunner.dispatch(id),
|
|
425
427
|
});
|
|
426
428
|
engine.setEvals({
|
|
@@ -2148,10 +2148,24 @@ export class SessionEngine {
|
|
|
2148
2148
|
sessionId: string,
|
|
2149
2149
|
token: string | null
|
|
2150
2150
|
): Promise<void> {
|
|
2151
|
+
const previous = await this.sessions.get(sessionId);
|
|
2151
2152
|
await this.updateRecord(sessionId, (current) => ({
|
|
2152
2153
|
...current,
|
|
2153
2154
|
continuationKey: token,
|
|
2154
2155
|
}));
|
|
2156
|
+
// Keep armed reminders addressed at the conversation across rotation
|
|
2157
|
+
// (the built-in HTTP channel rotates after every follow-up).
|
|
2158
|
+
if (
|
|
2159
|
+
previous?.continuationKey != null &&
|
|
2160
|
+
token !== null &&
|
|
2161
|
+
token !== previous.continuationKey
|
|
2162
|
+
) {
|
|
2163
|
+
await this.reminderApi?.rekeyContinuation(
|
|
2164
|
+
previous.channelId,
|
|
2165
|
+
previous.continuationKey,
|
|
2166
|
+
token
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2155
2169
|
}
|
|
2156
2170
|
|
|
2157
2171
|
isTurnRunning(sessionId: string): boolean {
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* The endpoints are gated on the `agent_serve_slack_provisioning` Statsig
|
|
10
10
|
* gate and 404 when it is off — the error mapping says so instead of
|
|
11
11
|
* "not found". Expected provisioning failures carry a stable JSON `code`
|
|
12
|
-
* (`not_connected`, `insufficient_scopes`, `
|
|
13
|
-
* `app_cap_reached`, `ratelimited`, `invalid_app_id`, …)
|
|
14
|
-
* through on {@link SlackProvisionApiError.code} so the CLI
|
|
12
|
+
* (`not_connected`, `insufficient_scopes`, `manager_reauth_required`,
|
|
13
|
+
* `app_conflict`, `app_cap_reached`, `ratelimited`, `invalid_app_id`, …)
|
|
14
|
+
* which flows through on {@link SlackProvisionApiError.code} so the CLI
|
|
15
|
+
* can branch.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import { z } from "zod";
|
|
@@ -21,6 +22,8 @@ export type SlackAppEnv = "dev" | "prod";
|
|
|
21
22
|
/** One manager-connected Slack workspace from `GET /status`. */
|
|
22
23
|
export interface SlackConnectedWorkspace {
|
|
23
24
|
slackTeamId: string;
|
|
25
|
+
/** Workspace name; absent on older backends, null on pre-existing rows. */
|
|
26
|
+
slackTeamName?: string | null;
|
|
24
27
|
slackUserId: string;
|
|
25
28
|
connectedAt: string;
|
|
26
29
|
}
|
|
@@ -113,6 +116,7 @@ const statusResponseSchema = z
|
|
|
113
116
|
z
|
|
114
117
|
.object({
|
|
115
118
|
slackTeamId: z.string(),
|
|
119
|
+
slackTeamName: z.string().nullish(),
|
|
116
120
|
slackUserId: z.string(),
|
|
117
121
|
connectedAt: z.string(),
|
|
118
122
|
})
|
package/src/types.ts
CHANGED
|
@@ -198,6 +198,29 @@ export interface AgentConfig {
|
|
|
198
198
|
* Engine-wide turn admission limits. See {@link AgentConcurrencyOptions}.
|
|
199
199
|
*/
|
|
200
200
|
concurrency?: AgentConcurrencyOptions;
|
|
201
|
+
/**
|
|
202
|
+
* Framework-provided model-facing tools, opted in per capability. See
|
|
203
|
+
* {@link BuiltinToolsConfig}.
|
|
204
|
+
*/
|
|
205
|
+
builtinTools?: BuiltinToolsConfig;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Built-in tools on {@link AgentConfig.builtinTools}. Each enabled
|
|
210
|
+
* capability materializes as ordinary server tools at discovery time, so
|
|
211
|
+
* turns, direct calls, `info`, and the playground treat them like authored
|
|
212
|
+
* tools. Authored tools with the same name win (with a warning), and none
|
|
213
|
+
* of them run on cloud turns (server tools are local-runtime only).
|
|
214
|
+
*/
|
|
215
|
+
export interface BuiltinToolsConfig {
|
|
216
|
+
/**
|
|
217
|
+
* Reminder tools bound to the current conversation, over
|
|
218
|
+
* {@link HostContext.reminders}: `reminders_create`, `reminders_list`,
|
|
219
|
+
* and `reminders_cancel` (the name the framework's reminder fire prompt
|
|
220
|
+
* tells the model to call when a stop condition is met). Sessions
|
|
221
|
+
* without a continuation key cannot arm reminders.
|
|
222
|
+
*/
|
|
223
|
+
reminders?: boolean;
|
|
201
224
|
}
|
|
202
225
|
|
|
203
226
|
/**
|
|
@@ -1722,6 +1745,16 @@ export interface ReminderHostApi {
|
|
|
1722
1745
|
continuationToken: string,
|
|
1723
1746
|
reason: string
|
|
1724
1747
|
): Promise<number>;
|
|
1748
|
+
/**
|
|
1749
|
+
* Re-key armed reminders after a channel rotates a session's continuation
|
|
1750
|
+
* token, so earlier reminders keep waking the conversation. The engine
|
|
1751
|
+
* calls this from `setContinuationToken`; channels rarely need to.
|
|
1752
|
+
*/
|
|
1753
|
+
rekeyContinuation(
|
|
1754
|
+
channelId: string,
|
|
1755
|
+
oldToken: string,
|
|
1756
|
+
newToken: string
|
|
1757
|
+
): Promise<number>;
|
|
1725
1758
|
/**
|
|
1726
1759
|
* Fire immediately (dev / hillclimb).
|
|
1727
1760
|
* `outcome` is `"noop"` when already firing or not armed.
|