@cursor/july 0.1.4 → 0.1.5
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/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.DF-Yx4OP.js → app.DabPG-io.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.QwK5BtEH.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DSeJSudL.js → VPLocalSearchBox.jmyr0bU0.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DZVLTXOC.js → theme.DysN9-VN.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +5 -2
- package/dist/internal/cursor-event-relay.d.ts +4 -1
- package/dist/internal/cursor-event-relay.d.ts.map +1 -1
- package/dist/internal/cursor-event-relay.js +6 -0
- package/dist/internal/cursor-relay-core.d.ts +36 -1
- package/dist/internal/cursor-relay-core.d.ts.map +1 -1
- package/dist/internal/cursor-relay-core.js +89 -8
- package/dist/internal/cursor-slack-relay.d.ts +2 -2
- package/dist/internal/cursor-slack-relay.d.ts.map +1 -1
- package/dist/internal/cursor-slack-relay.js +4 -0
- package/dist/playground/assets/{index-Ct_Usdm8.css → index-BEauYlII.css} +1 -1
- package/dist/playground/assets/{index-Dsyb-Vaj.js → index-BtM0wEGg.js} +94 -94
- package/dist/playground/index.html +2 -2
- package/package.json +1 -1
- package/src/internal/cli-deploy.ts +5 -2
- package/src/internal/cursor-event-relay.ts +10 -1
- package/src/internal/cursor-relay-core.ts +115 -9
- package/src/internal/cursor-slack-relay.ts +6 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.0XrheBv2.js +0 -1
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
/>
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10
10
|
<title>agent-serve playground</title>
|
|
11
|
-
<script type="module" crossorigin src="./assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
11
|
+
<script type="module" crossorigin src="./assets/index-BtM0wEGg.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BEauYlII.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cursor/july",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"repository": {
|
|
@@ -674,7 +674,8 @@ async function deployOne(
|
|
|
674
674
|
` generation: ${detail.generation}\n` +
|
|
675
675
|
` kind: ${detail.deploymentKind}\n` +
|
|
676
676
|
` alias: ${aliasUrl}\n` +
|
|
677
|
-
` playground: ${
|
|
677
|
+
` playground: ${aliasUrl}/playground (sign in with the alias token)\n` +
|
|
678
|
+
` or: ${CLI} playground --prod --slug ${slug} --team ${ctx.teamId}\n`
|
|
678
679
|
);
|
|
679
680
|
return 0;
|
|
680
681
|
}
|
|
@@ -795,6 +796,7 @@ export async function cmdDeploymentStatus(
|
|
|
795
796
|
` generation: ${detail.generation}`,
|
|
796
797
|
` kind: ${detail.deploymentKind}`,
|
|
797
798
|
` alias: ${aliasUrl}`,
|
|
799
|
+
` playground: ${aliasUrl}/playground`,
|
|
798
800
|
];
|
|
799
801
|
if (detail.gitRepoUrl !== null && detail.gitRepoUrl !== undefined) {
|
|
800
802
|
lines.push(` repo: ${detail.gitRepoUrl} @ ${detail.gitRef ?? ""}`);
|
|
@@ -1285,7 +1287,8 @@ function aliasTokenBlock(aliasToken: string): string {
|
|
|
1285
1287
|
return (
|
|
1286
1288
|
`\n Alias token (shown ONCE — save it now, it cannot be retrieved again):\n\n` +
|
|
1287
1289
|
` ${aliasToken}\n\n` +
|
|
1288
|
-
` Send it as the X-Agent-Alias-Token header on requests to the alias URL
|
|
1290
|
+
` Send it as the X-Agent-Alias-Token header on requests to the alias URL,\n` +
|
|
1291
|
+
` or paste it into the hosted playground (<alias>/playground) sign-in gate.\n` +
|
|
1289
1292
|
` Lost tokens: \`${CLI} rotate-token <slug>\` mints a replacement.\n\n`
|
|
1290
1293
|
);
|
|
1291
1294
|
}
|
|
@@ -68,7 +68,10 @@ export interface CursorEventRelayOptions {
|
|
|
68
68
|
repos: readonly string[];
|
|
69
69
|
events: readonly string[];
|
|
70
70
|
stateDir: string;
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Called for each matching event; failures retry a bounded number of
|
|
73
|
+
* times, then log and skip.
|
|
74
|
+
*/
|
|
72
75
|
dispatch: (event: CursorScmEvent) => Promise<void>;
|
|
73
76
|
logger: (line: string) => void;
|
|
74
77
|
/** Test seam. Defaults to global fetch. */
|
|
@@ -213,6 +216,12 @@ export class CursorEventRelay {
|
|
|
213
216
|
events: [...this.options.events],
|
|
214
217
|
})),
|
|
215
218
|
}),
|
|
219
|
+
// Empty subscriptions drop this consumer's registration on clean stop
|
|
220
|
+
// instead of leaving it to lapse by TTL.
|
|
221
|
+
buildDeregistrationBody: (consumerId) => ({
|
|
222
|
+
consumerId,
|
|
223
|
+
subscriptions: [],
|
|
224
|
+
}),
|
|
216
225
|
applyRegistrationResponse: (json) => {
|
|
217
226
|
const result = json as { repos?: SubscriptionStatus[] };
|
|
218
227
|
const statuses = result.repos ?? [];
|
|
@@ -23,6 +23,11 @@ const DEFAULT_IDLE_POLL_DELAY_MS = 250;
|
|
|
23
23
|
const MIN_BACKOFF_MS = 1_000;
|
|
24
24
|
const MAX_BACKOFF_MS = 30_000;
|
|
25
25
|
const AUTH_FAILURE_BACKOFF_MS = 60_000;
|
|
26
|
+
/** Attempts per event before the offset advances past it. */
|
|
27
|
+
const DISPATCH_ATTEMPTS = 3;
|
|
28
|
+
const DISPATCH_RETRY_DELAYS_MS = [250, 1_000];
|
|
29
|
+
/** Budget for the best-effort deregistration PUT during stop(). */
|
|
30
|
+
const DEREGISTER_TIMEOUT_MS = 1_500;
|
|
26
31
|
|
|
27
32
|
interface OffsetSnapshot {
|
|
28
33
|
version: typeof OFFSET_SNAPSHOT_VERSION;
|
|
@@ -108,6 +113,14 @@ export interface RelayAdapter<TEvent extends { offset: string }> {
|
|
|
108
113
|
readonly pollPath: string;
|
|
109
114
|
/** The registration request body for this consumer id. */
|
|
110
115
|
buildRegistrationBody(consumerId: string): unknown;
|
|
116
|
+
/**
|
|
117
|
+
* The empty-scope registration body used to deregister this consumer on a
|
|
118
|
+
* clean stop. When present, `stop()` best-effort PUTs it so the consumer's
|
|
119
|
+
* registration does not linger as a ghost until its server-side TTL (a
|
|
120
|
+
* ghost with this host's agent names makes name routing ambiguous for a
|
|
121
|
+
* redeployed sibling until it expires).
|
|
122
|
+
*/
|
|
123
|
+
buildDeregistrationBody?(consumerId: string): unknown;
|
|
111
124
|
/**
|
|
112
125
|
* Apply the (ok) registration response: update the accepted-scope filter
|
|
113
126
|
* and return the human-readable outcome lines to log. The core logs them
|
|
@@ -121,7 +134,10 @@ export interface RelayAdapter<TEvent extends { offset: string }> {
|
|
|
121
134
|
* the offset but are not dispatched.
|
|
122
135
|
*/
|
|
123
136
|
shouldDispatch(event: TEvent): boolean;
|
|
124
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Handle one matching event. Failures retry a bounded number of times
|
|
139
|
+
* ({@link DISPATCH_ATTEMPTS}), then log and skip.
|
|
140
|
+
*/
|
|
125
141
|
dispatch(event: TEvent): Promise<void>;
|
|
126
142
|
}
|
|
127
143
|
|
|
@@ -147,6 +163,14 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
|
|
|
147
163
|
private consumerId = "";
|
|
148
164
|
/** undefined until the first registration, so a clean first success logs. */
|
|
149
165
|
private lastRegistrationLog: string | undefined;
|
|
166
|
+
/** Whether any registration succeeded (gates the stop-time deregister). */
|
|
167
|
+
private registeredOnce = false;
|
|
168
|
+
/**
|
|
169
|
+
* The in-flight heartbeat re-registration, if any. `stop()` awaits it
|
|
170
|
+
* before deregistering so a just-sent heartbeat cannot land after the
|
|
171
|
+
* deregistration PUT and resurrect the registration for a full TTL.
|
|
172
|
+
*/
|
|
173
|
+
private heartbeatPromise: Promise<boolean> | undefined;
|
|
150
174
|
|
|
151
175
|
constructor(private readonly options: CursorRelayCoreOptions<TEvent>) {
|
|
152
176
|
this.offsetStore = new RelayOffsetStore(options.stateDir);
|
|
@@ -170,7 +194,7 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
|
|
|
170
194
|
const registered = await this.registerUntilSuccess();
|
|
171
195
|
if (!registered) return;
|
|
172
196
|
this.registerTimer = setInterval(() => {
|
|
173
|
-
|
|
197
|
+
this.heartbeatPromise = this.registerOnce();
|
|
174
198
|
}, DEFAULT_REGISTER_INTERVAL_MS);
|
|
175
199
|
this.registerTimer.unref?.();
|
|
176
200
|
await this.consumeLoop();
|
|
@@ -229,6 +253,56 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
|
|
|
229
253
|
timer.unref?.();
|
|
230
254
|
}),
|
|
231
255
|
]);
|
|
256
|
+
// A heartbeat re-registration may be in flight (its fetch was just
|
|
257
|
+
// aborted); let it settle so it cannot land after the deregistration.
|
|
258
|
+
await this.heartbeatPromise?.catch(() => {});
|
|
259
|
+
await this.deregister();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Best-effort empty-scope PUT on clean stop, so this consumer's
|
|
264
|
+
* registration is dropped immediately instead of lingering until its
|
|
265
|
+
* server-side TTL. A ghost registration keeps this host's agent names
|
|
266
|
+
* live and makes name routing ambiguous for a redeployed sibling with a
|
|
267
|
+
* fresh consumer id. Failures only log — the TTL still cleans up.
|
|
268
|
+
*/
|
|
269
|
+
private async deregister(): Promise<void> {
|
|
270
|
+
const adapter = this.options.adapter;
|
|
271
|
+
if (
|
|
272
|
+
adapter.buildDeregistrationBody === undefined ||
|
|
273
|
+
!this.registeredOnce ||
|
|
274
|
+
this.consumerId === ""
|
|
275
|
+
) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const url = `${this.options.baseUrl.replace(/\/+$/, "")}${adapter.subscribePath}`;
|
|
279
|
+
const controller = new AbortController();
|
|
280
|
+
const timer = setTimeout(() => controller.abort(), DEREGISTER_TIMEOUT_MS);
|
|
281
|
+
timer.unref?.();
|
|
282
|
+
try {
|
|
283
|
+
const response = await this.fetchImpl(url, {
|
|
284
|
+
method: "PUT",
|
|
285
|
+
headers: {
|
|
286
|
+
authorization: `Bearer ${this.options.apiKey}`,
|
|
287
|
+
"content-type": "application/json",
|
|
288
|
+
},
|
|
289
|
+
body: JSON.stringify(adapter.buildDeregistrationBody(this.consumerId)),
|
|
290
|
+
signal: controller.signal,
|
|
291
|
+
});
|
|
292
|
+
if (response.ok) {
|
|
293
|
+
this.log("deregistered on shutdown");
|
|
294
|
+
} else {
|
|
295
|
+
this.log(
|
|
296
|
+
`deregistration failed: ${response.status} (registration lapses by TTL)`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.log(
|
|
301
|
+
`deregistration failed: ${describeRelayError(error)} (registration lapses by TTL)`
|
|
302
|
+
);
|
|
303
|
+
} finally {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
}
|
|
232
306
|
}
|
|
233
307
|
|
|
234
308
|
private log(line: string): void {
|
|
@@ -291,6 +365,7 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
|
|
|
291
365
|
this.logRegistrationOutcome(
|
|
292
366
|
this.options.adapter.applyRegistrationResponse(json)
|
|
293
367
|
);
|
|
368
|
+
this.registeredOnce = true;
|
|
294
369
|
return true;
|
|
295
370
|
} catch (error) {
|
|
296
371
|
if (this.isAbort(error)) return false;
|
|
@@ -373,13 +448,7 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
|
|
|
373
448
|
// on replay after a crash between dispatch and save).
|
|
374
449
|
if (compareStreamOffsets(event.offset, offset) <= 0) continue;
|
|
375
450
|
if (this.options.adapter.shouldDispatch(event)) {
|
|
376
|
-
|
|
377
|
-
await this.options.adapter.dispatch(event);
|
|
378
|
-
} catch (error) {
|
|
379
|
-
this.log(
|
|
380
|
-
`dispatch failed at offset ${event.offset}: ${describeRelayError(error)}`
|
|
381
|
-
);
|
|
382
|
-
}
|
|
451
|
+
await this.dispatchWithRetry(event);
|
|
383
452
|
}
|
|
384
453
|
offset = event.offset;
|
|
385
454
|
}
|
|
@@ -403,6 +472,43 @@ export class CursorRelayCore<TEvent extends { offset: string }> {
|
|
|
403
472
|
}
|
|
404
473
|
}
|
|
405
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Dispatch one event with a small bounded retry before the offset advances
|
|
477
|
+
* past it. Transient local failures (e.g. a connection reset while POSTing
|
|
478
|
+
* to the channel) used to drop the event outright; a poison event still
|
|
479
|
+
* only costs {@link DISPATCH_ATTEMPTS} tries and is then skipped, keeping
|
|
480
|
+
* at-least-once semantics without a retry loop.
|
|
481
|
+
*/
|
|
482
|
+
private async dispatchWithRetry(event: TEvent): Promise<void> {
|
|
483
|
+
for (let attempt = 1; attempt <= DISPATCH_ATTEMPTS; attempt++) {
|
|
484
|
+
if (attempt > 1 && !this.running) {
|
|
485
|
+
this.log(
|
|
486
|
+
`dispatch abandoned at offset ${event.offset}: relay stopping`
|
|
487
|
+
);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
await this.options.adapter.dispatch(event);
|
|
492
|
+
return;
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (attempt === DISPATCH_ATTEMPTS) {
|
|
495
|
+
this.log(
|
|
496
|
+
`dispatch failed at offset ${event.offset} after ${attempt} attempts: ${describeRelayError(error)}`
|
|
497
|
+
);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this.log(
|
|
501
|
+
`dispatch attempt ${attempt} failed at offset ${event.offset}: ${describeRelayError(error)}; retrying`
|
|
502
|
+
);
|
|
503
|
+
await this.sleep(
|
|
504
|
+
DISPATCH_RETRY_DELAYS_MS[
|
|
505
|
+
Math.min(attempt - 1, DISPATCH_RETRY_DELAYS_MS.length - 1)
|
|
506
|
+
]
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
406
512
|
/** Log the failure, sleep, and return the backoff for the NEXT failure. */
|
|
407
513
|
private async backoffAfterHttpFailure(
|
|
408
514
|
response: Response,
|
|
@@ -44,8 +44,8 @@ export interface CursorSlackRelayOptions {
|
|
|
44
44
|
agents: readonly CursorSlackAgentRegistration[];
|
|
45
45
|
stateDir: string;
|
|
46
46
|
/**
|
|
47
|
-
* Called for each event addressed to this consumer's agents; failures
|
|
48
|
-
*
|
|
47
|
+
* Called for each event addressed to this consumer's agents; failures
|
|
48
|
+
* retry a bounded number of times, then log and skip.
|
|
49
49
|
*/
|
|
50
50
|
dispatch: (envelope: CursorSlackRelayEvent) => Promise<void>;
|
|
51
51
|
logger: (line: string) => void;
|
|
@@ -163,6 +163,10 @@ export class CursorSlackRelay {
|
|
|
163
163
|
})),
|
|
164
164
|
};
|
|
165
165
|
},
|
|
166
|
+
// Empty agents drop this consumer's registration on clean stop, so a
|
|
167
|
+
// redeployed sibling's identical agent names never tie with this
|
|
168
|
+
// host's ghost until the server-side TTL lapses.
|
|
169
|
+
buildDeregistrationBody: (consumerId) => ({ consumerId, agents: [] }),
|
|
166
170
|
applyRegistrationResponse: (json) => {
|
|
167
171
|
const result = json as RegistrationResponse;
|
|
168
172
|
const agentStatuses = result.agents ?? [];
|