@cursor/july 0.1.47 → 0.1.48

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 (88) hide show
  1. package/dist/bin/agent-serve.js +30 -35
  2. package/dist/docs/404.html +1 -1
  3. package/dist/docs/ab.html +2 -2
  4. package/dist/docs/assets/{app.IEXEim5m.js → app.BC1P20xI.js} +1 -1
  5. package/dist/docs/assets/chunks/@localSearchIndexroot.DV5SAHnQ.js +1 -0
  6. package/dist/docs/assets/chunks/{VPLocalSearchBox.78s6nqsH.js → VPLocalSearchBox.C_900xn2.js} +1 -1
  7. package/dist/docs/assets/chunks/{theme.DroGdz_Q.js → theme.CT135Jhh.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/opentelemetry.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/artifacts.html +2 -2
  38. package/dist/docs/reference/channels.html +2 -2
  39. package/dist/docs/reference/cli.html +2 -2
  40. package/dist/docs/reference/connections.html +2 -2
  41. package/dist/docs/reference/hooks.html +2 -2
  42. package/dist/docs/reference/http-api.html +2 -2
  43. package/dist/docs/reference/instructions.html +2 -2
  44. package/dist/docs/reference/playground.html +2 -2
  45. package/dist/docs/reference/project-layout.html +2 -2
  46. package/dist/docs/reference/prompt.html +2 -2
  47. package/dist/docs/reference/schedules.html +2 -2
  48. package/dist/docs/reference/sessions.html +2 -2
  49. package/dist/docs/reference/skills.html +2 -2
  50. package/dist/docs/reference/subagents.html +2 -2
  51. package/dist/docs/reference/tools.html +2 -2
  52. package/dist/docs/scaffolding-agents.html +2 -2
  53. package/dist/docs/storage.html +2 -2
  54. package/dist/docs/templates/triage.html +2 -2
  55. package/dist/docs/troubleshooting.html +2 -2
  56. package/dist/internal/authored-loaders.d.ts +19 -0
  57. package/dist/internal/authored-loaders.d.ts.map +1 -1
  58. package/dist/internal/authored-loaders.js +39 -0
  59. package/dist/internal/cli-deploy.d.ts +2 -1
  60. package/dist/internal/cli-deploy.d.ts.map +1 -1
  61. package/dist/internal/cli-deploy.js +97 -1
  62. package/dist/internal/deploy-client.d.ts +7 -2
  63. package/dist/internal/deploy-client.d.ts.map +1 -1
  64. package/dist/internal/deploy-client.js +12 -0
  65. package/dist/internal/discovery.d.ts.map +1 -1
  66. package/dist/internal/discovery.js +2 -8
  67. package/dist/internal/eval-runner.d.ts.map +1 -1
  68. package/dist/internal/eval-runner.js +3 -8
  69. package/dist/internal/logs-client.d.ts +1 -1
  70. package/dist/internal/logs-client.d.ts.map +1 -1
  71. package/dist/internal/logs-client.js +3 -0
  72. package/dist/internal/serve-restart.d.ts +42 -0
  73. package/dist/internal/serve-restart.d.ts.map +1 -0
  74. package/dist/internal/serve-restart.js +85 -0
  75. package/dist/playground/assets/index-CiCmH6R-.css +1 -0
  76. package/dist/playground/assets/{index-BgUHfe3L.js → index-DEVTyeOu.js} +46 -46
  77. package/dist/playground/index.html +2 -2
  78. package/package.json +1 -1
  79. package/src/bin/agent-serve.ts +39 -38
  80. package/src/internal/authored-loaders.ts +45 -0
  81. package/src/internal/cli-deploy.ts +103 -1
  82. package/src/internal/deploy-client.ts +19 -1
  83. package/src/internal/discovery.ts +5 -12
  84. package/src/internal/eval-runner.ts +3 -12
  85. package/src/internal/logs-client.ts +4 -0
  86. package/src/internal/serve-restart.ts +120 -0
  87. package/dist/docs/assets/chunks/@localSearchIndexroot.D7w61u6Y.js +0 -1
  88. package/dist/playground/assets/index-uFaeqVZS.css +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-BgUHfe3L.js"></script>
12
- <link rel="stylesheet" crossorigin href="./assets/index-uFaeqVZS.css">
11
+ <script type="module" crossorigin src="./assets/index-DEVTyeOu.js"></script>
12
+ <link rel="stylesheet" crossorigin href="./assets/index-CiCmH6R-.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.47",
3
+ "version": "0.1.48",
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": {
@@ -16,6 +16,7 @@
16
16
  * agent-serve slack <subcommand>
17
17
  */
18
18
 
19
+ import { spawn } from "node:child_process";
19
20
  import { readFile } from "node:fs/promises";
20
21
  import { resolve } from "node:path";
21
22
  import { parseArgs } from "node:util";
@@ -44,6 +45,7 @@ import {
44
45
  cmdTrajectory,
45
46
  } from "../internal/cli-ax.js";
46
47
  import {
48
+ cmdDelete,
47
49
  cmdDeploy,
48
50
  cmdDeploymentStatus,
49
51
  cmdDeployments,
@@ -61,6 +63,11 @@ import {
61
63
  packageVersion,
62
64
  } from "../internal/distribution.js";
63
65
  import { loadAgentLocalEnv } from "../internal/local-env.js";
66
+ import {
67
+ ENTER_RESTART_EXIT_CODE,
68
+ isServeRestartWorker,
69
+ runEnterRestartSupervisor,
70
+ } from "../internal/serve-restart.js";
64
71
  import { clearTerminal, stdoutPalette } from "../internal/terminal-style.js";
65
72
  import { cmdUpdate, warnIfOutdated } from "../internal/update-check.js";
66
73
  import type { AgentProject, AgentServeHandle } from "../types.js";
@@ -134,6 +141,7 @@ Usage:
134
141
  ${CLI} deployments [--team <id>] [--json]
135
142
  ${CLI} deployment <slug> [--team <id>] [--json]
136
143
  ${CLI} stop <slug> [--team <id>] [--json] [--no-wait]
144
+ ${CLI} delete <slug> [--team <id>] [--json] [--no-wait]
137
145
  ${CLI} rotate-token <slug> [--team <id>]
138
146
  ${CLI} rotate-pod-credential <slug> [--team <id>]
139
147
  ${CLI} secrets set <slug> NAME... | list <slug> | unset <slug> NAME
@@ -143,7 +151,8 @@ Usage:
143
151
 
144
152
  Commands:
145
153
  serve Serve agents under /<slug>/... (index at /). On a TTY, press
146
- Enter to restart (reloads agent code + Vite HMR child).
154
+ Enter to restart (re-executes this process so agent code, evals,
155
+ and Vite reload).
147
156
  dev Local development: same as \`${CLI} serve --dev\` (manual
148
157
  schedule/reminder dispatch, unsigned GitHub loopback, Vite HMR).
149
158
  chat Interactive REPL against a running server (follow-ups keep the
@@ -252,6 +261,8 @@ Commands:
252
261
  deployments List the team's deployments (slug, status, generation, kind).
253
262
  deployment Show one deployment in full (status, lastError, engine info).
254
263
  stop Stop a deployment and wait until it reports stopped.
264
+ delete Delete a deployment (teardown + purge the record) and wait
265
+ until GET 404s. The slug is then free to redeploy.
255
266
  rotate-token Mint a new alias token for a deployment (shown once); the old
256
267
  token stops working immediately.
257
268
  rotate-pod-credential
@@ -555,7 +566,12 @@ async function main(argv: string[]): Promise<number> {
555
566
  // Best-effort, cached, and silent on failure. `update` does its own fresh
556
567
  // lookup, --json runs stay quiet for machine consumers, and `mcp` skips it
557
568
  // so MCP client spawns stay fast (stdout there is the protocol wire).
558
- if (command !== "update" && command !== "mcp" && values.json !== true) {
569
+ if (
570
+ command !== "update" &&
571
+ command !== "mcp" &&
572
+ values.json !== true &&
573
+ !isServeRestartWorker()
574
+ ) {
559
575
  await warnIfOutdated();
560
576
  }
561
577
 
@@ -731,6 +747,10 @@ async function main(argv: string[]): Promise<number> {
731
747
  return cmdStop(positionals[0], deployOptionsFromValues(values));
732
748
  }
733
749
 
750
+ case "delete": {
751
+ return cmdDelete(positionals[0], deployOptionsFromValues(values));
752
+ }
753
+
734
754
  case "rotate-token": {
735
755
  return cmdRotateToken(positionals[0], deployOptionsFromValues(values));
736
756
  }
@@ -1227,6 +1247,16 @@ async function main(argv: string[]): Promise<number> {
1227
1247
  },
1228
1248
  };
1229
1249
  const enterToRestart = process.stdin.isTTY === true;
1250
+ if (enterToRestart && !isServeRestartWorker()) {
1251
+ return runEnterRestartSupervisor({
1252
+ spawn: (command, args, spawnOptions) =>
1253
+ spawn(command, args, spawnOptions),
1254
+ onRestart: () => {
1255
+ clearTerminal();
1256
+ },
1257
+ });
1258
+ }
1259
+
1230
1260
  const bannerOpts = {
1231
1261
  noPlayground: values["no-playground"] === true,
1232
1262
  dev,
@@ -1244,9 +1274,9 @@ async function main(argv: string[]): Promise<number> {
1244
1274
  return next;
1245
1275
  };
1246
1276
 
1247
- let handle = await start();
1277
+ const handle = await start();
1248
1278
  let closing = false;
1249
- let restarting = false;
1279
+ let restartRequested = false;
1250
1280
 
1251
1281
  await new Promise<void>((resolve) => {
1252
1282
  const shutdown = (): void => {
@@ -1268,40 +1298,17 @@ async function main(argv: string[]): Promise<number> {
1268
1298
  process.stdin.on("data", (chunk: string | Buffer) => {
1269
1299
  const text =
1270
1300
  typeof chunk === "string" ? chunk : chunk.toString("utf8");
1271
- if (!isEnterOnly(text) || closing || restarting) {
1301
+ if (!isEnterOnly(text) || closing) {
1272
1302
  return;
1273
1303
  }
1274
- restarting = true;
1275
- // Close + re-serve reloads authored agent modules and respawns Vite.
1276
- // Hold the "restarting…" frame briefly so a fast close doesn't look
1277
- // like Enter did nothing.
1304
+ closing = true;
1305
+ restartRequested = true;
1278
1306
  clearTerminal();
1279
1307
  process.stdout.write(style.dim("restarting…\n"));
1280
- void Promise.all([handle.close(), sleep(450)])
1281
- .then(() => {
1282
- clearTerminal();
1283
- return start();
1284
- })
1285
- .then((next) => {
1286
- handle = next;
1287
- })
1288
- .catch((error: unknown) => {
1289
- process.stderr.write(
1290
- `[agent-sdk] restart failed: ${
1291
- error instanceof Error
1292
- ? (error.stack ?? error.message)
1293
- : String(error)
1294
- }\n`
1295
- );
1296
- closing = true;
1297
- resolve();
1298
- })
1299
- .finally(() => {
1300
- restarting = false;
1301
- });
1308
+ void handle.close().finally(resolve);
1302
1309
  });
1303
1310
  });
1304
- return 0;
1311
+ return restartRequested ? ENTER_RESTART_EXIT_CODE : 0;
1305
1312
  }
1306
1313
 
1307
1314
  default: {
@@ -1437,12 +1444,6 @@ function isEnterOnly(text: string): boolean {
1437
1444
  return text.length > 0 && [...text].every((ch) => ch === "\n" || ch === "\r");
1438
1445
  }
1439
1446
 
1440
- function sleep(ms: number): Promise<void> {
1441
- return new Promise((resolve) => {
1442
- setTimeout(resolve, ms);
1443
- });
1444
- }
1445
-
1446
1447
  /**
1447
1448
  * `dev [path]` takes a positional project root; `serve` keeps `--dir` only.
1448
1449
  * Reject mixing a positional with an explicit `--dir`, and extra args on `dev`.
@@ -16,6 +16,7 @@
16
16
  * them.
17
17
  */
18
18
 
19
+ import { stat } from "node:fs/promises";
19
20
  import { register as registerModuleHooks } from "node:module";
20
21
  import { join } from "node:path";
21
22
  import { pathToFileURL } from "node:url";
@@ -38,6 +39,50 @@ import {
38
39
  const TSX_REGISTERED_KEY = Symbol.for("cursor.agentkit.tsxRegistered");
39
40
  const ALIASES_REGISTERED_KEY = Symbol.for("cursor.agentkit.aliasesRegistered");
40
41
 
42
+ /**
43
+ * ESM specifier for an authored agent/eval file.
44
+ *
45
+ * Node caches `import()` by URL for the life of the process, so a long-lived
46
+ * `agent-sdk dev` would otherwise keep serving the first load of eval files
47
+ * and agent modules. The mtime query string is ignored by the filesystem
48
+ * resolver and is the cache key.
49
+ *
50
+ * Truncate to an integer: vitest/esbuild treat the substring after the last
51
+ * `.` as a loader id, so `?mtime=1700000000000.4949` fails with
52
+ * `Invalid loader value: "4949"`. Named `mtime` rather than `t` so Vite does
53
+ * not treat it as its HMR timestamp and collapse the specifier to the path.
54
+ */
55
+ export function authoredModuleSpecifier(
56
+ absolutePath: string,
57
+ mtimeMs: number
58
+ ): string {
59
+ const url = pathToFileURL(absolutePath);
60
+ url.searchParams.set("mtime", String(Math.trunc(mtimeMs)));
61
+ return url.href;
62
+ }
63
+
64
+ /**
65
+ * Dynamically import an authored TypeScript/JavaScript module, re-reading it
66
+ * from disk when the file's mtime has changed since the last import.
67
+ */
68
+ export async function importAuthoredModule(
69
+ absolutePath: string
70
+ ): Promise<Record<string, unknown>> {
71
+ let mtimeMs = 0;
72
+ try {
73
+ mtimeMs = (await stat(absolutePath)).mtimeMs;
74
+ } catch {
75
+ // import() reports the real missing-file error.
76
+ }
77
+ const loaded: unknown = await import(
78
+ authoredModuleSpecifier(absolutePath, mtimeMs)
79
+ );
80
+ if (typeof loaded !== "object" || loaded === null) {
81
+ throw new Error(`authored module is not an object: ${absolutePath}`);
82
+ }
83
+ return loaded as Record<string, unknown>;
84
+ }
85
+
41
86
  let registration: Promise<void> | undefined;
42
87
 
43
88
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `agent-sdk deploy | deployments | deployment | stop | rotate-token |
2
+ * `agent-sdk deploy | deployments | deployment | stop | delete | rotate-token |
3
3
  * rotate-pod-credential | secrets set|list|unset` — Heroku-style deploys
4
4
  * onto Cursor's managed agent-serve hosting.
5
5
  *
@@ -24,6 +24,7 @@ import {
24
24
  import { authorizeCursorAccountConnectors } from "./cursor-account-mcp-auth.js";
25
25
  import {
26
26
  AgentServeDeployClient,
27
+ type DeleteAccepted,
27
28
  type DeployAccepted,
28
29
  DeployApiError,
29
30
  type Deployment,
@@ -1117,6 +1118,54 @@ export async function cmdStop(
1117
1118
  return 0;
1118
1119
  }
1119
1120
 
1121
+ // ============================================================================
1122
+ // delete <slug>
1123
+ // ============================================================================
1124
+
1125
+ export async function cmdDelete(
1126
+ slug: string | undefined,
1127
+ options: DeployCliOptions
1128
+ ): Promise<number> {
1129
+ if (slug === undefined || slug === "") {
1130
+ (options.err ?? ((t: string) => void process.stderr.write(t)))(
1131
+ `Usage: ${CLI} delete <slug> [--team <id>] [--json] [--no-wait]\n`
1132
+ );
1133
+ return 1;
1134
+ }
1135
+ const ctx = await prepare(options);
1136
+ if (typeof ctx === "number") {
1137
+ return ctx;
1138
+ }
1139
+ let accepted: DeleteAccepted;
1140
+ try {
1141
+ accepted = await ctx.client.deleteDeployment({
1142
+ teamId: ctx.teamId,
1143
+ slug,
1144
+ });
1145
+ } catch (error) {
1146
+ return renderApiError(error, ctx.err);
1147
+ }
1148
+ if (ctx.noWait) {
1149
+ if (ctx.json) {
1150
+ ctx.out(`${JSON.stringify(accepted, null, 2)}\n`);
1151
+ } else {
1152
+ ctx.out(`Delete accepted for ${slug} (status: ${accepted.status}).\n`);
1153
+ }
1154
+ return 0;
1155
+ }
1156
+ try {
1157
+ await pollUntilGone(ctx, slug);
1158
+ } catch (error) {
1159
+ return renderApiError(error, ctx.err);
1160
+ }
1161
+ if (ctx.json) {
1162
+ ctx.out(`${JSON.stringify({ slug, status: "deleted" }, null, 2)}\n`);
1163
+ } else {
1164
+ ctx.out(`Deleted ${slug} (team ${ctx.teamId}).\n`);
1165
+ }
1166
+ return 0;
1167
+ }
1168
+
1120
1169
  // ============================================================================
1121
1170
  // rotate-token <slug>
1122
1171
  // ============================================================================
@@ -1526,6 +1575,59 @@ async function pollUntil(
1526
1575
  }
1527
1576
  }
1528
1577
 
1578
+ /**
1579
+ * Wait until GET 404s (the delete reconciler purged the record). Treats
1580
+ * other errors as failures. Throws DeployApiError on timeout.
1581
+ */
1582
+ async function pollUntilGone(
1583
+ ctx: DeployCliContext,
1584
+ slug: string
1585
+ ): Promise<void> {
1586
+ const startedAt = Date.now();
1587
+ const tty = process.stderr.isTTY === true;
1588
+ let sawProgress = false;
1589
+ const report = (label: string): void => {
1590
+ if (ctx.json) {
1591
+ return;
1592
+ }
1593
+ const elapsed = Math.round((Date.now() - startedAt) / 1000);
1594
+ if (tty) {
1595
+ ctx.err(`\r\x1b[2K${slug}: ${label} (${elapsed}s)`);
1596
+ sawProgress = true;
1597
+ }
1598
+ };
1599
+ const finishProgressLine = (): void => {
1600
+ if (tty && sawProgress) {
1601
+ ctx.err("\n");
1602
+ }
1603
+ };
1604
+
1605
+ for (;;) {
1606
+ try {
1607
+ const detail = await ctx.client.getDeployment({
1608
+ teamId: ctx.teamId,
1609
+ slug,
1610
+ });
1611
+ report(deployProgressLabel(detail));
1612
+ } catch (error: unknown) {
1613
+ if (error instanceof DeployApiError && error.status === 404) {
1614
+ finishProgressLine();
1615
+ return;
1616
+ }
1617
+ finishProgressLine();
1618
+ throw error;
1619
+ }
1620
+ if (Date.now() - startedAt >= ctx.timeoutMs) {
1621
+ finishProgressLine();
1622
+ throw new DeployApiError(
1623
+ `Timed out after ${Math.round(ctx.timeoutMs / 1000)}s waiting for ${slug} to disappear. It may still be deleting — check \`${CLI} deployment ${slug} --team ${ctx.teamId}\`.`,
1624
+ 0
1625
+ );
1626
+ }
1627
+ await sleep(ctx.pollIntervalMs);
1628
+ }
1629
+ }
1630
+
1529
1631
  function aliasTokenBlock(aliasToken: string): string {
1530
1632
  return (
1531
1633
  `\n Alias token (shown ONCE — save it now, it cannot be retrieved again):\n\n` +
@@ -25,7 +25,8 @@ export type DeploymentStatus =
25
25
  | "degraded"
26
26
  | "failed"
27
27
  | "stopping"
28
- | "stopped";
28
+ | "stopped"
29
+ | "deleting";
29
30
 
30
31
  /** Customer-safe deploy progress (no hosting phase / infra names). */
31
32
  export type DeploymentProgress =
@@ -39,6 +40,7 @@ export type DeploymentProgress =
39
40
  | "failed"
40
41
  | "stopping"
41
42
  | "stopped"
43
+ | "deleting"
42
44
  | "degraded";
43
45
 
44
46
  export const TERMINAL_DEPLOY_STATUSES: ReadonlySet<DeploymentStatus> = new Set([
@@ -211,6 +213,8 @@ export interface StopAccepted {
211
213
  status: string;
212
214
  }
213
215
 
216
+ export type DeleteAccepted = StopAccepted;
217
+
214
218
  /** One secret's metadata from the list endpoint — names only, never values. */
215
219
  export interface SecretInfo {
216
220
  name: string;
@@ -276,6 +280,7 @@ const deploymentSchema = z.object({
276
280
  "failed",
277
281
  "stopping",
278
282
  "stopped",
283
+ "deleting",
279
284
  ]),
280
285
  progress: z
281
286
  .enum([
@@ -289,6 +294,7 @@ const deploymentSchema = z.object({
289
294
  "failed",
290
295
  "stopping",
291
296
  "stopped",
297
+ "deleting",
292
298
  "degraded",
293
299
  ])
294
300
  .optional(),
@@ -495,6 +501,18 @@ export class AgentServeDeployClient {
495
501
  return stopAcceptedSchema.parse(raw);
496
502
  }
497
503
 
504
+ async deleteDeployment(args: {
505
+ teamId: number;
506
+ slug: string;
507
+ }): Promise<DeleteAccepted> {
508
+ const raw = await this.request({
509
+ method: "DELETE",
510
+ path: `/internal/agent-serve/deployments/${encodeURIComponent(args.slug)}?teamId=${args.teamId}`,
511
+ context: { verb: "delete", slug: args.slug },
512
+ });
513
+ return stopAcceptedSchema.parse(raw);
514
+ }
515
+
498
516
  /** Returns the new alias token exactly once; the old one dies now. */
499
517
  async rotateAliasToken(args: {
500
518
  teamId: number;
@@ -16,7 +16,6 @@ import {
16
16
  relative,
17
17
  resolve,
18
18
  } from "node:path";
19
- import { pathToFileURL } from "node:url";
20
19
  import { type ABConfigFile, resolveABMaxPlaygroundSessions } from "../ab.js";
21
20
  import { resolveArtifactsMax } from "../artifacts.js";
22
21
  import { httpChannel } from "../channels.js";
@@ -61,7 +60,10 @@ import {
61
60
  type ToolDefinition,
62
61
  toPublicCloudInfo,
63
62
  } from "../types.js";
64
- import { registerAuthoredModuleLoaders } from "./authored-loaders.js";
63
+ import {
64
+ importAuthoredModule,
65
+ registerAuthoredModuleLoaders,
66
+ } from "./authored-loaders.js";
65
67
  import { normalizePositiveInt } from "./bounded-int.js";
66
68
  import { getDefinitionKind } from "./brand.js";
67
69
  import {
@@ -94,15 +96,6 @@ export interface LoadAgentProjectOptions {
94
96
 
95
97
  type ImportModule = (absolutePath: string) => Promise<Record<string, unknown>>;
96
98
 
97
- async function defaultImportModule(
98
- absolutePath: string
99
- ): Promise<Record<string, unknown>> {
100
- return (await import(pathToFileURL(absolutePath).href)) as Record<
101
- string,
102
- unknown
103
- >;
104
- }
105
-
106
99
  interface DiscoveryContext {
107
100
  rootDir: string;
108
101
  diagnostics: Diagnostic[];
@@ -282,7 +275,7 @@ export async function loadAgentProject(
282
275
  const ctx: DiscoveryContext = {
283
276
  rootDir,
284
277
  diagnostics,
285
- importModule: options.importModule ?? defaultImportModule,
278
+ importModule: options.importModule ?? importAuthoredModule,
286
279
  };
287
280
 
288
281
  const name = (await readPackageName(rootDir)) ?? basename(rootDir);
@@ -11,7 +11,6 @@
11
11
  import type { Dirent } from "node:fs";
12
12
  import { readdir } from "node:fs/promises";
13
13
  import { join, relative, resolve } from "node:path";
14
- import { pathToFileURL } from "node:url";
15
14
  import type {
16
15
  EvalAssertionHandle,
17
16
  EvalAssertions,
@@ -62,6 +61,7 @@ import {
62
61
  normalizeEvalMaxConcurrency,
63
62
  } from "../evals.js";
64
63
  import type { ModelSetting, SessionEvent } from "../types.js";
64
+ import { importAuthoredModule } from "./authored-loaders.js";
65
65
  import { isDefinitionOfKind } from "./brand.js";
66
66
  import {
67
67
  callJudgeModel,
@@ -131,15 +131,6 @@ export interface RunEvalsOptions extends EvalSelectionOptions {
131
131
  signal?: AbortSignal;
132
132
  }
133
133
 
134
- async function defaultImport(
135
- absolutePath: string
136
- ): Promise<Record<string, unknown>> {
137
- return (await import(pathToFileURL(absolutePath).href)) as Record<
138
- string,
139
- unknown
140
- >;
141
- }
142
-
143
134
  async function listEvalFiles(dir: string): Promise<string[]> {
144
135
  const out: string[] = [];
145
136
  async function walk(current: string): Promise<void> {
@@ -172,7 +163,7 @@ export async function discoverEvals(
172
163
  projectRoot: string,
173
164
  importModule: (
174
165
  absolutePath: string
175
- ) => Promise<Record<string, unknown>> = defaultImport
166
+ ) => Promise<Record<string, unknown>> = importAuthoredModule
176
167
  ): Promise<{
177
168
  evals: DiscoveredEval[];
178
169
  config: Partial<EvalConfigFile>;
@@ -291,7 +282,7 @@ export async function runDiscoveredEvals(
291
282
  selected = options.discovered.evals;
292
283
  config = options.discovered.config;
293
284
  } else {
294
- const importModule = options.importModule ?? defaultImport;
285
+ const importModule = options.importModule ?? importAuthoredModule;
295
286
  const discovered = await discoverEvals(options.projectRoot, importModule);
296
287
  config = discovered.config;
297
288
  selected = filterDiscoveredEvals(discovered.evals, {
@@ -43,6 +43,7 @@ export type LogsProgress =
43
43
  | "failed"
44
44
  | "stopping"
45
45
  | "stopped"
46
+ | "deleting"
46
47
  | "degraded";
47
48
 
48
49
  const PROGRESS_MESSAGES: Readonly<Record<LogsProgress, string>> = {
@@ -56,6 +57,7 @@ const PROGRESS_MESSAGES: Readonly<Record<LogsProgress, string>> = {
56
57
  failed: "Deploy failed",
57
58
  stopping: "Stopping deployment…",
58
59
  stopped: "Deployment stopped",
60
+ deleting: "Deleting deployment…",
59
61
  degraded: "Deployment degraded",
60
62
  };
61
63
 
@@ -385,6 +387,8 @@ function resolveProgress(detail: DeploymentDetail): LogsProgress {
385
387
  return "stopping";
386
388
  case "stopped":
387
389
  return "stopped";
390
+ case "deleting":
391
+ return "deleting";
388
392
  default:
389
393
  return "queued";
390
394
  }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Enter-to-restart for `serve` / `dev` on a TTY.
3
+ *
4
+ * In-process close+re-serve cannot reload every authored change: Node ESM
5
+ * caches transitive imports, and module-level state (tsx, singletons) survives.
6
+ * The parent process therefore supervises a worker and respawns it when the
7
+ * worker exits with {@link ENTER_RESTART_EXIT_CODE}.
8
+ */
9
+
10
+ export const ENTER_RESTART_EXIT_CODE = 75;
11
+
12
+ export const SERVE_RESTART_WORKER_ENV = "AGENT_SERVE_RESTART_WORKER";
13
+
14
+ export interface ServeRestartProcess {
15
+ execPath: string;
16
+ execArgv: readonly string[];
17
+ argv: readonly string[];
18
+ env: NodeJS.ProcessEnv;
19
+ }
20
+
21
+ export interface ServeRestartSpawnPlan {
22
+ command: string;
23
+ args: string[];
24
+ env: NodeJS.ProcessEnv;
25
+ }
26
+
27
+ export interface ServeRestartWorkerChild {
28
+ once(
29
+ event: "exit",
30
+ listener: (code: number | null, signal: NodeJS.Signals | null) => void
31
+ ): void;
32
+ }
33
+
34
+ export function isServeRestartWorker(
35
+ env: NodeJS.ProcessEnv = process.env
36
+ ): boolean {
37
+ return env[SERVE_RESTART_WORKER_ENV] === "1";
38
+ }
39
+
40
+ export function serveRestartSpawnPlan(
41
+ proc: ServeRestartProcess
42
+ ): ServeRestartSpawnPlan {
43
+ return {
44
+ command: proc.execPath,
45
+ args: [...proc.execArgv, ...proc.argv.slice(1)],
46
+ env: { ...proc.env, [SERVE_RESTART_WORKER_ENV]: "1" },
47
+ };
48
+ }
49
+
50
+ export function shouldRespawnServeWorker(
51
+ code: number | null,
52
+ signal: NodeJS.Signals | null
53
+ ): boolean {
54
+ return signal === null && code === ENTER_RESTART_EXIT_CODE;
55
+ }
56
+
57
+ /**
58
+ * Run the TTY Enter-to-restart supervisor: spawn this CLI as a worker, and
59
+ * respawn whenever the worker exits asking for a restart. Other exits
60
+ * (Ctrl-C drain, crash, bind failure) propagate to the caller.
61
+ */
62
+ export async function runEnterRestartSupervisor(options: {
63
+ spawn: (
64
+ command: string,
65
+ args: string[],
66
+ spawnOptions: { stdio: "inherit"; env: NodeJS.ProcessEnv }
67
+ ) => ServeRestartWorkerChild;
68
+ proc?: ServeRestartProcess;
69
+ onRestart?: () => void;
70
+ installSignalGuards?: (handler: () => void) => () => void;
71
+ }): Promise<number> {
72
+ const proc = options.proc ?? {
73
+ execPath: process.execPath,
74
+ execArgv: process.execArgv,
75
+ argv: process.argv,
76
+ env: process.env,
77
+ };
78
+ const plan = serveRestartSpawnPlan(proc);
79
+ const uninstallSignals = (
80
+ options.installSignalGuards ?? installDefaultSignalGuards
81
+ )(() => undefined);
82
+
83
+ try {
84
+ for (;;) {
85
+ const child = options.spawn(plan.command, plan.args, {
86
+ stdio: "inherit",
87
+ env: plan.env,
88
+ });
89
+ const { code, signal } = await waitForExit(child);
90
+ if (!shouldRespawnServeWorker(code, signal)) {
91
+ if (signal !== null) {
92
+ return 1;
93
+ }
94
+ return code ?? 1;
95
+ }
96
+ options.onRestart?.();
97
+ }
98
+ } finally {
99
+ uninstallSignals();
100
+ }
101
+ }
102
+
103
+ function waitForExit(
104
+ child: ServeRestartWorkerChild
105
+ ): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
106
+ return new Promise((resolve) => {
107
+ child.once("exit", (code, signal) => {
108
+ resolve({ code, signal });
109
+ });
110
+ });
111
+ }
112
+
113
+ function installDefaultSignalGuards(handler: () => void): () => void {
114
+ process.on("SIGINT", handler);
115
+ process.on("SIGTERM", handler);
116
+ return () => {
117
+ process.off("SIGINT", handler);
118
+ process.off("SIGTERM", handler);
119
+ };
120
+ }