@alpic-ai/tracks 0.4.0 → 0.4.2

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 (2) hide show
  1. package/dist/main.js +138 -68
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -7,17 +7,16 @@ import { isIPv4 } from "node:net";
7
7
  import { Argument, Command, Flag } from "effect/cli";
8
8
  import { createServer } from "node:http";
9
9
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
10
- //#region ../contract/src/authorization.ts
11
- const ORGANIZATION_HEADER = "x-organization";
12
- Context.Service()("@tracks/contract/CurrentOrganization");
13
- var Unauthorized = class extends Schema.TaggedError()("Unauthorized", { message: Schema.String }, {
10
+ Context.Service()("@tracks/contract/RequestOrganization");
11
+ Context.Service()("@tracks/contract/CurrentApiKey");
12
+ var ApiKeyRefused = class extends Schema.TaggedError()("ApiKeyRefused", { message: Schema.String }, {
14
13
  httpApiStatus: 401,
15
- description: `The Agent Runner key is missing or invalid, or no \`${ORGANIZATION_HEADER}\` header names the Organization.`
14
+ description: "The API key is missing, malformed or revoked, or its organization is closed."
16
15
  }) {};
17
- var Authorization = class extends HttpApiMiddleware.Service()("@tracks/contract/Authorization", {
16
+ var ApiKeyAuthorization = class extends HttpApiMiddleware.Service()("@tracks/contract/ApiKeyAuthorization", {
18
17
  requiredForClient: true,
19
- security: { bearer: HttpApiSecurity.http({ scheme: "bearer" }).pipe(HttpApiSecurity.annotate(OpenApi.Description, `The Agent Runner key, sent as \`Authorization: Bearer <key>\`, with the Organization the request is made for in the \`${ORGANIZATION_HEADER}\` header.`)) },
20
- error: Unauthorized
18
+ security: { bearer: HttpApiSecurity.http({ scheme: "bearer" }).pipe(HttpApiSecurity.annotate(OpenApi.Description, "An API key of your organization, starting with `trk_`, sent as `Authorization: Bearer <key>`.")) },
19
+ error: ApiKeyRefused
21
20
  }) {};
22
21
  const OptionalQueryParam = (schema, description) => Schema.optionalKey(schema).annotateKey({ description }).pipe(Schema.decodeTo(Schema.Option(Schema.toType(schema)), SchemaTransformation.optionFromOptionalKey()));
23
22
  const ListQuery = {
@@ -88,18 +87,18 @@ const Connection = Schema.Union([Schema.Struct({
88
87
  }).annotate({
89
88
  identifier: "FailedConnection",
90
89
  description: "A connection whose consent did not complete."
91
- })]).annotate({ description: "Access to an MCP server as one of its users, kept by Agent Runner." });
90
+ })]).annotate({ description: "Access to an MCP server as one of its users, kept by us." });
92
91
  const CreatedConnection = Schema.Struct({
93
92
  connection: Connection,
94
93
  consent_url: Schema.String.annotate({ description: "Send the user's browser there to consent on the MCP server. Valid for 10 minutes, once. The browser then comes back to your `return_url`, or shows the confirmation code without one." })
95
94
  }).annotate({ identifier: "CreatedConnection" });
96
95
  var ConnectionNotFound = class extends Schema.TaggedError()("ConnectionNotFound", { message: Schema.String }, {
97
96
  httpApiStatus: 404,
98
- description: "No connection with this id exists for this Organization."
97
+ description: "No connection with this id exists in your organization."
99
98
  }) {};
100
99
  var ConnectionRefused = class extends Schema.TaggedError()("ConnectionRefused", { message: Schema.String }, {
101
100
  httpApiStatus: 422,
102
- description: "The connection cannot be started: the return URL is not allowed, or the MCP server cannot be authorized in a way Agent Runner supports."
101
+ description: "The connection cannot be started: the return URL is not allowed, or the MCP server cannot be authorized in a way we support."
103
102
  }) {};
104
103
  var ServerUnreachable = class extends Schema.TaggedError()("ServerUnreachable", { message: Schema.String }, {
105
104
  httpApiStatus: 502,
@@ -141,10 +140,10 @@ var ConnectionsApi = class extends HttpApiGroup.make("connections").add(HttpApiE
141
140
  }).annotateMerge(OpenApi.annotations({
142
141
  summary: "Delete a connection",
143
142
  description: "Deletes a connection and the access it holds."
144
- }))).middleware(Authorization).middleware(RequestValidation).annotateMerge(OpenApi.annotations({
143
+ }))).annotateMerge(OpenApi.annotations({
145
144
  title: "Connections",
146
145
  description: "Let agents use MCP servers as one of their users, without their credentials entering the sandbox."
147
- })) {};
146
+ })).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
148
147
  //#endregion
149
148
  //#region ../contract/src/runs.ts
150
149
  const RunId = Schema.String.pipe(Schema.brand("RunId")).annotate({
@@ -154,14 +153,15 @@ const RunId = Schema.String.pipe(Schema.brand("RunId")).annotate({
154
153
  const Harness = Schema.Literals([
155
154
  "claude-code",
156
155
  "codex",
157
- "codex-app"
156
+ "codex-app",
157
+ "cursor"
158
158
  ]).annotate({
159
159
  identifier: "Harness",
160
- description: "The coding agent to run: `claude-code` (Claude Code), `codex` (OpenAI's Codex CLI) or `codex-app` (the Codex mode of OpenAI's ChatGPT desktop app, with its browser, computer-use and document tools)."
160
+ description: "The coding agent to run: `claude-code` (Claude Code), `codex` (OpenAI's Codex CLI), `codex-app` (the Codex mode of OpenAI's ChatGPT desktop app, with its browser, computer-use and document tools) or `cursor` (Cursor's CLI)."
161
161
  });
162
162
  const Agent = Schema.Struct({
163
163
  harness: Harness,
164
- model: Schema.NonEmptyString.annotate({ description: "Model the agent uses, such as `claude-sonnet-5-5`." })
164
+ model: Schema.NonEmptyString.annotate({ description: "Model the agent uses, such as `claude-sonnet-5-5`, or a Cursor model id such as `gpt-5.6-luna-low`." })
165
165
  }).annotate({
166
166
  identifier: "Agent",
167
167
  description: "The coding agent and the model it uses."
@@ -169,8 +169,8 @@ const Agent = Schema.Struct({
169
169
  const repository = Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "URL of a Git repository to clone before the agent starts." }));
170
170
  const MAX_SCRIPT_LENGTH = 32768;
171
171
  const script = (description) => Schema.OptionFromOptionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(MAX_SCRIPT_LENGTH)).annotate({ description: `${description} At most ${MAX_SCRIPT_LENGTH} characters.` }));
172
- const setup = script("Shell script run with bash in the repository directory after checkout and before the first prompt, with the run's variables set, while the run is `preparing`. A non-zero exit fails the run. It may run again if Agent Runner restarts mid-way: make it idempotent.");
173
- const teardown = script("Shell script run with bash in the repository directory after the run ends and before its sandbox is destroyed, with the run's variables set. Its exit code doesn't change how the run ended. It may run again if Agent Runner restarts mid-way: make it idempotent.");
172
+ const setup = script("Shell script run with bash in the repository directory after checkout and before the first prompt, with the run's variables set, while the run is `preparing`. A non-zero exit fails the run. It may run again after a restart on our side: make it idempotent.");
173
+ const teardown = script("Shell script run with bash in the repository directory after the run ends and before its sandbox is destroyed, with the run's variables set. Its exit code doesn't change how the run ended. It may run again after a restart on our side: make it idempotent.");
174
174
  const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
175
175
  const RESERVED_VARIABLES = /* @__PURE__ */ new Set(["PATH", "HOME"]);
176
176
  const RESERVED_VARIABLE_PREFIXES = [
@@ -214,7 +214,7 @@ const McpServerInput = Schema.Union([Schema.Struct({
214
214
  description: "An MCP server given by a connection to it, to pick between several accounts."
215
215
  })]).annotate({
216
216
  identifier: "McpServerInput",
217
- description: "An MCP server the agent can use, through Agent Runner: credentials never enter the sandbox. Set `url` or `connection_id`."
217
+ description: "An MCP server the agent can use, reached through us: credentials never enter the sandbox. Set `url` or `connection_id`."
218
218
  });
219
219
  const McpServersInput = Schema.Array(McpServerInput).check(Schema.isMaxLength(MAX_MCP_SERVERS), Schema.makeFilter((servers) => {
220
220
  const names = servers.map((server) => server.name);
@@ -251,6 +251,26 @@ const RunEnvironment = Schema.Struct({
251
251
  identifier: "Environment",
252
252
  description: "What the agent works on."
253
253
  });
254
+ const LABEL_KEY_PATTERN = "[A-Za-z0-9_.-]{1,64}";
255
+ const LABEL_KEY = new RegExp(`^${LABEL_KEY_PATTERN}$`);
256
+ const MAX_LABELS = 20;
257
+ const MAX_LABEL_VALUE_LENGTH = 256;
258
+ const Labels = Schema.Record(Schema.String, Schema.String.check(Schema.isMaxLength(MAX_LABEL_VALUE_LENGTH))).annotate({ description: `Labels you attach to the run to find it again, as keys and values, such as \`{ "ticket": "ENG-42" }\`. We never read them. Keys match \`${LABEL_KEY.source}\`; at most ${MAX_LABELS} labels with values of up to ${MAX_LABEL_VALUE_LENGTH} characters.` }).check(Schema.isMaxProperties(MAX_LABELS), Schema.makeFilter((labels) => Object.keys(labels).flatMap((key) => LABEL_KEY.test(key) ? [] : [{
259
+ path: [key],
260
+ issue: `Expected a key matching ${LABEL_KEY.source}`
261
+ }])));
262
+ const LABEL_FILTER = new RegExp(`^${LABEL_KEY_PATTERN}:`);
263
+ const keyAndValueOf = (filter) => {
264
+ const separator = filter.indexOf(":");
265
+ return [filter.slice(0, separator), filter.slice(separator + 1)];
266
+ };
267
+ const LabelFilters = Schema.Array(Schema.String.check(Schema.isPattern(LABEL_FILTER))).check(Schema.makeFilter((filters) => {
268
+ const keys = filters.map((filter) => keyAndValueOf(filter)[0]);
269
+ return new Set(keys).size === keys.length || "Expected each label key once";
270
+ })).pipe(Schema.decodeTo(Labels, SchemaTransformation.transform({
271
+ decode: (filters) => Object.fromEntries(filters.map(keyAndValueOf)),
272
+ encode: (labels) => Object.entries(labels).map(([key, value]) => `${key}:${value}`)
273
+ })));
254
274
  const Budget = Schema.Finite.check(Schema.isBetween({
255
275
  minimum: .01,
256
276
  maximum: 1e4
@@ -263,6 +283,7 @@ const CreateRunInput = Schema.Struct({
263
283
  agent: Agent,
264
284
  prompt: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do first." }),
265
285
  environment: EnvironmentInput,
286
+ labels: Schema.optionalKey(Labels),
266
287
  budget_usd: Schema.OptionFromOptionalKey(Budget),
267
288
  one_shot: Schema.OptionFromOptionalKey(Schema.Boolean.annotate({ description: "Whether the run ends as soon as its first prompt finishes. Defaults to `true`. Set it to `false` to send follow-up prompts while the run is `idle`." })),
268
289
  idle_timeout_seconds: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
@@ -274,10 +295,10 @@ const CreateRunInput = Schema.Struct({
274
295
  maximum: MAX_TIME_LIMIT_SECONDS
275
296
  })).annotate({ description: `Longest the run may last, in seconds from when it starts, from ${MIN_TIME_LIMIT_SECONDS} to ${MAX_TIME_LIMIT_SECONDS}. Defaults to 7200. Once reached, the run ends with \`time_limit\`, even in the middle of a prompt.` }))
276
297
  }).check(Schema.makeFilter((input) => Option.contains(input.one_shot, false) || Option.isNone(input.idle_timeout_seconds) || "idle_timeout_seconds is only allowed when one_shot is false")).annotate({ identifier: "CreateRunInput" });
277
- const SubmitPromptInput = Schema.Struct({
298
+ const SendPromptInput = Schema.Struct({
278
299
  text: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do next." }),
279
300
  model: Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "Model for this prompt only. Defaults to the run's model." }))
280
- }).annotate({ identifier: "SubmitPromptInput" });
301
+ }).annotate({ identifier: "SendPromptInput" });
281
302
  const PromptIndex = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).annotate({ description: "Position of the prompt in its run, starting at 1." });
282
303
  const promptFields = {
283
304
  run_id: RunId,
@@ -329,6 +350,7 @@ const FailureKind = Schema.Literals([
329
350
  });
330
351
  const runFields = {
331
352
  id: RunId,
353
+ labels: Labels.annotate({ description: "Labels attached to the run when it was created." }),
332
354
  agent: Agent,
333
355
  environment: RunEnvironment,
334
356
  budget_usd: Schema.Number.annotate({ description: "Maximum the run may spend, in US dollars." }),
@@ -380,7 +402,7 @@ const Run = Schema.Union([
380
402
  ]).annotate({ description: "A coding agent working on your prompt in an isolated sandbox." });
381
403
  var RunNotFound = class extends Schema.TaggedError()("RunNotFound", { message: Schema.String }, {
382
404
  httpApiStatus: 404,
383
- description: "No run with this id exists for this Organization."
405
+ description: "No run with this id exists in your organization."
384
406
  }) {};
385
407
  var ConnectionUnusable = class extends Schema.TaggedError()("ConnectionUnusable", { message: Schema.String }, {
386
408
  httpApiStatus: 422,
@@ -390,6 +412,21 @@ var ModelNotOffered = class extends Schema.TaggedError()("ModelNotOffered", { me
390
412
  httpApiStatus: 422,
391
413
  description: "The agent cannot use this model. The message lists the models it can use."
392
414
  }) {};
415
+ var ModelProviderUnavailable = class extends Schema.TaggedError()("ModelProviderUnavailable", { message: Schema.String }, {
416
+ httpApiStatus: 503,
417
+ description: "The model provider could not be reached to check the model. Retry later."
418
+ }) {};
419
+ var InsufficientBalance = class extends Schema.TaggedError()("InsufficientBalance", {
420
+ message: Schema.String,
421
+ top_up_url: Schema.String.annotate({ description: "Where to add funds to your organization's balance." })
422
+ }, {
423
+ httpApiStatus: 402,
424
+ description: "Your organization's balance is $0 or below: add funds to start runs again."
425
+ }) {};
426
+ var OrganizationBlocked = class extends Schema.TaggedError()("OrganizationBlocked", { message: Schema.String }, {
427
+ httpApiStatus: 403,
428
+ description: "Your organization cannot start runs after a dispute on one of its payments: contact us."
429
+ }) {};
393
430
  var RunNotIdle = class extends Schema.TaggedError()("RunNotIdle", { message: Schema.String }, {
394
431
  httpApiStatus: 409,
395
432
  description: "The run accepts no prompt now: it is one-shot, has ended, or is still working on a prompt."
@@ -433,7 +470,7 @@ const eventFields = {
433
470
  prompt: PromptIndex.annotate({ description: "Index of the prompt the event belongs to." }),
434
471
  captured_at: Schema.DateTimeUtc.annotate({ description: "When the output the event comes from was captured." }),
435
472
  subagent_id: Schema.OptionFromOptionalKey(ToolCallId.annotate({ description: "The subagent that did it: the `tool_call_id` of the `tool_started` event that launched it. Absent when the run's agent did it." })),
436
- frame_seqs: Schema.Array(Schema.Int).annotate({ description: "The `seq` of each output line the event was read from. Empty for events Agent Runner records itself." })
473
+ frame_seqs: Schema.Array(Schema.Int).annotate({ description: "The `seq` of each output line the event was read from. Empty for events we record ourselves." })
437
474
  };
438
475
  const Event = Schema.Union([
439
476
  Schema.Struct({
@@ -529,14 +566,35 @@ const EventStreamItem = Schema.Union([Schema.Struct({
529
566
  event: Schema.Literal("heartbeat"),
530
567
  data: Schema.Literal("")
531
568
  })]);
569
+ const STARTED_BY_LABEL = "started_by";
570
+ const CreateRunInputBesidesStartedBy = CreateRunInput.check(Schema.makeFilter((input) => Object.keys(input.labels ?? {}).filter((key) => key !== STARTED_BY_LABEL).length < MAX_LABELS || {
571
+ path: ["labels"],
572
+ issue: `Expected at most 19 labels besides ${STARTED_BY_LABEL}, which is set to the API key`
573
+ }));
532
574
  var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("create", "/runs", {
533
575
  headers: { "idempotency-key": Schema.OptionFromOptionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(255)).annotate({ description: "A unique value you choose, up to 255 characters, to retry safely: a request with a key already used in your organization returns the run the first request created, without starting another, whatever its body." })) },
534
- payload: CreateRunInput,
576
+ payload: CreateRunInputBesidesStartedBy,
535
577
  success: Run.pipe(HttpApiSchema.status(201)),
536
- error: [ModelNotOffered, ConnectionUnusable]
578
+ error: [
579
+ ModelNotOffered,
580
+ ModelProviderUnavailable,
581
+ ConnectionUnusable,
582
+ InsufficientBalance,
583
+ OrganizationBlocked
584
+ ]
537
585
  }).annotateMerge(OpenApi.annotations({
538
586
  summary: "Create a run",
539
- description: "Starts a coding agent on your prompt in a fresh sandbox. A one-shot run, the default, ends when the agent finishes; otherwise the run waits `idle` for follow-up prompts until you end it or it times out. Send an `Idempotency-Key` header to retry without creating a second run."
587
+ description: `Starts a coding agent on your prompt in a fresh sandbox. A one-shot run, the default, ends when the agent finishes; otherwise the run waits \`idle\` for follow-up prompts until you end it or it times out. Send an \`Idempotency-Key\` header to retry without creating a second run. The run gets the label \`${STARTED_BY_LABEL}: key:<id>\`, with the id of the API key that created it, in place of any \`${STARTED_BY_LABEL}\` you send. Runs start only while your organization's balance is above $0.`
588
+ })), HttpApiEndpoint.get("list", "/runs", {
589
+ query: {
590
+ label: OptionalQueryParam(LabelFilters, "Return only runs with this label, written `key:value`, such as `ticket:ENG-42`. Repeat it to require several labels."),
591
+ after: OptionalQueryParam(RunId, "Return only runs listed after the run with this id."),
592
+ limit: ListQuery.limit
593
+ },
594
+ success: List(Run)
595
+ }).annotateMerge(OpenApi.annotations({
596
+ summary: "List runs",
597
+ description: "Returns your runs, most recently started first. To get the next page, pass the `id` of the last run received as `after`."
540
598
  })), HttpApiEndpoint.get("get", "/runs/:id", {
541
599
  params: { id: RunId },
542
600
  success: Run,
@@ -549,7 +607,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
549
607
  success: RunUsage,
550
608
  error: RunNotFound
551
609
  }).annotateMerge(OpenApi.annotations({
552
- summary: "Get run usage",
610
+ summary: "Get a run's usage",
553
611
  description: "Returns what the run has consumed so far, per model and in sandbox time, and what it cost."
554
612
  })), HttpApiEndpoint.post("end", "/runs/:id/end", {
555
613
  params: { id: RunId },
@@ -565,14 +623,15 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
565
623
  }).annotateMerge(OpenApi.annotations({
566
624
  summary: "Cancel a run",
567
625
  description: "Stops the agent right away, even mid-prompt, ends the run with `canceled`, then destroys its sandbox. Canceling a run that has ended does nothing."
568
- })), HttpApiEndpoint.post("submitPrompt", "/runs/:id/prompts", {
626
+ })), HttpApiEndpoint.post("sendPrompt", "/runs/:id/prompts", {
569
627
  params: { id: RunId },
570
- payload: SubmitPromptInput,
628
+ payload: SendPromptInput,
571
629
  success: Prompt.pipe(HttpApiSchema.status(202)),
572
630
  error: [
573
631
  RunNotFound,
574
632
  RunNotIdle,
575
- ModelNotOffered
633
+ ModelNotOffered,
634
+ ModelProviderUnavailable
576
635
  ]
577
636
  }).annotateMerge(OpenApi.annotations({
578
637
  summary: "Send a follow-up prompt",
@@ -583,7 +642,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
583
642
  success: List(Prompt),
584
643
  error: RunNotFound
585
644
  }).annotateMerge(OpenApi.annotations({
586
- summary: "List run prompts",
645
+ summary: "List a run's prompts",
587
646
  description: "Returns the run's prompts in order. To get the next page, pass the `index` of the last prompt received as `after`."
588
647
  })), HttpApiEndpoint.get("listFrames", "/runs/:id/frames", {
589
648
  params: { id: RunId },
@@ -594,7 +653,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
594
653
  success: List(Frame),
595
654
  error: RunNotFound
596
655
  }).annotateMerge(OpenApi.annotations({
597
- summary: "List run output",
656
+ summary: "List a run's frames",
598
657
  description: "Returns the agent's output, line by line, in order. To get the next page, pass the `seq` of the last line received as `after`."
599
658
  })), HttpApiEndpoint.get("listEvents", "/runs/:id/events", {
600
659
  params: { id: RunId },
@@ -605,7 +664,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
605
664
  success: List(Event),
606
665
  error: RunNotFound
607
666
  }).annotateMerge(OpenApi.annotations({
608
- summary: "List run events",
667
+ summary: "List a run's events",
609
668
  description: "Returns what the agent did, in order: each prompt, then its messages, reasoning, tool calls and subagents, then a `stop` once the agent stopped. Events read alike whatever the harness, and are read from the run's output: `frame_seqs` points at the lines each one comes from. To get the next page, pass the `seq` of the last event received as `after`."
610
669
  })), HttpApiEndpoint.get("followFrames", "/runs/:id/frames/stream", {
611
670
  params: { id: RunId },
@@ -613,7 +672,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
613
672
  success: HttpApiSchema.StreamSse({ events: FrameStreamItem }),
614
673
  error: RunNotFound
615
674
  }).annotateMerge(OpenApi.annotations({
616
- summary: "Stream run output",
675
+ summary: "Stream a run's frames",
617
676
  description: "Sends the agent's output as server-sent events as it is produced: a `frame` event per line, its data the line as JSON, and a `heartbeat` event every 30 seconds to keep the connection open. Closes once the run has ended and every line was sent. The connection may also close earlier; to pick up where it stopped, call again with the `seq` of the last line received as `after`, or let an SSE client reconnect: the `Last-Event-ID` header it sends is used when `after` is absent."
618
677
  })), HttpApiEndpoint.get("followEvents", "/runs/:id/events/stream", {
619
678
  params: { id: RunId },
@@ -621,41 +680,42 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
621
680
  success: HttpApiSchema.StreamSse({ events: EventStreamItem }),
622
681
  error: RunNotFound
623
682
  }).annotateMerge(OpenApi.annotations({
624
- summary: "Stream run events",
683
+ summary: "Stream a run's events",
625
684
  description: "Sends what the agent did as server-sent events as it happens: an `event` event per event, its data the event as JSON, and a `heartbeat` event every 30 seconds to keep the connection open. Closes once the run has ended and every event was sent. The connection may also close earlier; to pick up where it stopped, call again with the `seq` of the last event received as `after`, or let an SSE client reconnect: the `Last-Event-ID` header it sends is used when `after` is absent."
626
- }))).middleware(Authorization).middleware(RequestValidation).annotateMerge(OpenApi.annotations({
685
+ }))).annotateMerge(OpenApi.annotations({
627
686
  title: "Runs",
628
687
  description: "Start coding agents and follow their work."
629
- })) {};
688
+ })).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
630
689
  //#endregion
631
690
  //#region ../contract/src/system.ts
632
691
  const EgressIps = Schema.Struct({ ips: Schema.Array(Schema.String).annotate({ description: "IPv4 addresses, empty when the API runs locally." }) }).annotate({ identifier: "EgressIps" });
633
692
  var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).add(HttpApiEndpoint.get("health", "/health", { success: HttpApiSchema.NoContent }).annotateMerge(OpenApi.annotations({
634
693
  summary: "Check health",
635
- description: "Returns 204 when the API is up. No Agent Runner key required."
694
+ description: "Returns 204 when the API is up. No key required."
636
695
  }))).add(HttpApiEndpoint.get("egressIps", "/egress-ips", { success: EgressIps }).annotateMerge(OpenApi.annotations({
637
696
  summary: "List egress IPs",
638
- description: "The fixed addresses Agent Runner calls MCP servers and their authorization servers from. Allow them on an MCP server that only accepts known addresses. Commands the agent runs in its sandbox, such as cloning the repository, come from other addresses. No Agent Runner key required."
697
+ description: "The fixed addresses we call MCP servers and their authorization servers from. Allow them on an MCP server that only accepts known addresses. Commands the agent runs in its sandbox, such as cloning the repository, come from other addresses. No key required."
639
698
  }))).annotateMerge(OpenApi.annotations({ title: "System" })) {};
640
699
  //#endregion
641
700
  //#region ../contract/src/api.ts
642
- var Api = class extends HttpApi.make("agent-runner").add(RunsApi).add(ConnectionsApi).prefix("/v1").add(SystemApi).annotate(HttpApi.ParseOptions, { errors: "all" }).annotateMerge(OpenApi.annotations({
643
- title: "Agent Runner API",
644
- description: "Run coding agents such as Claude Code in isolated sandboxes. Create a run with a prompt, then follow its output and what it costs."
701
+ var TracksApi = class extends HttpApi.make("tracks").add(RunsApi).add(ConnectionsApi).add(SystemApi).annotate(HttpApi.ParseOptions, { errors: "all" }).annotateMerge(OpenApi.annotations({
702
+ title: "Tracks API",
703
+ description: "Run coding agents such as Claude Code in isolated sandboxes with an API key of your organization. Create a run with a prompt, then follow its output and what it costs."
645
704
  })) {};
646
705
  //#endregion
647
706
  //#region ../contract/src/client.ts
648
707
  const CLI_USER_AGENT = "tracks";
649
708
  var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiClient") {
650
- static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(Api, { transformClient: HttpClient.mapRequest((request) => {
709
+ static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(TracksApi, { transformClient: HttpClient.mapRequest((request) => {
651
710
  const located = HttpClientRequest.prependUrl(request, options.baseUrl.href);
652
711
  return options.userAgent === void 0 ? located : HttpClientRequest.setHeader(located, "user-agent", options.userAgent);
653
- }) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(Authorization, ({ next, request }) => next(HttpClientRequest.bearerToken(request, Redacted.value(options.agentRunnerKey)).pipe(HttpClientRequest.setHeader(ORGANIZATION_HEADER, options.organization))))));
712
+ }) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(ApiKeyAuthorization, ({ next, request }) => next(HttpClientRequest.bearerToken(request, Redacted.value(options.apiKey))))));
654
713
  };
655
714
  //#endregion
656
715
  //#region ../env/src/env.ts
657
716
  const Secret = Schema.Redacted(Schema.NonEmptyString);
658
717
  const AgentRunnerKey = Schema.Redacted(Schema.NonEmptyString.check(Schema.isMinLength(32)));
718
+ const StripeKey = (prefix) => Schema.Redacted(Schema.String.check(Schema.isStartingWith(prefix)));
659
719
  const Port = Schema.Int.check(Schema.isBetween({
660
720
  minimum: 1,
661
721
  maximum: 65535
@@ -665,7 +725,7 @@ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
665
725
  "127.0.0.1",
666
726
  "[::1]"
667
727
  ]);
668
- const AGENT_RUNNER_PATH = "/agent-runner";
728
+ const TRACKS_API_PATH = "/api";
669
729
  const SecureUrl = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" || LOOPBACK_HOSTS.has(url.hostname) || "must use https"));
670
730
  const ENCRYPTION_KEY_BYTES = 32;
671
731
  const EncryptionKey = Schema.Redacted(Schema.NonEmptyString.check(Schema.makeFilter((key) => Buffer.from(key, "base64").length === ENCRYPTION_KEY_BYTES || `must be ${ENCRYPTION_KEY_BYTES} random bytes in base64: openssl rand -base64 ${ENCRYPTION_KEY_BYTES}`)));
@@ -681,19 +741,23 @@ const IpAddresses = Schema.String.pipe(Schema.decodeTo(Schema.Array(Schema.Strin
681
741
  const SentryDsn = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" && url.username !== "" || "must be a Sentry DSN"));
682
742
  const Secrets = Schema.Struct({
683
743
  AGENT_RUNNER_KEY: AgentRunnerKey,
744
+ TRACKS_API_KEY: Secret,
684
745
  DAYTONA_API_KEY: Secret,
685
746
  E2B_API_KEY: Secret,
686
747
  ANTHROPIC_API_KEY: Secret,
687
748
  OPENAI_API_KEY: Secret,
749
+ CURSOR_API_KEY: Secret,
750
+ CURSOR_ADMIN_KEY: Secret,
688
751
  TYPESAFE_API_KEY: Secret,
689
752
  DATABASE_PASSWORD: Secret,
690
753
  CONNECTION_ENCRYPTION_KEY: EncryptionKey,
754
+ STRIPE_SECRET_KEY: StripeKey("rk_"),
755
+ STRIPE_WEBHOOK_SECRET: StripeKey("whsec_"),
691
756
  SENTRY_DSN: Schema.optionalKey(Schema.Redacted(SentryDsn))
692
757
  });
693
758
  const Variables = Schema.Struct({
694
759
  PORT: Port.pipe(Schema.withDecodingDefaultKey(Effect.succeed(3e3))),
695
- AGENT_RUNNER_ORGANIZATION: Schema.NonEmptyString,
696
- AGENT_RUNNER_API_URL: Schema.URLFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed(`https://agent-runner.lab.alpic.ai${AGENT_RUNNER_PATH}`))),
760
+ TRACKS_API_URL: Schema.URLFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed(`https://agent-runner.lab.alpic.ai${TRACKS_API_PATH}`))),
697
761
  DAYTONA_API_URL: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://app.daytona.io/api"))),
698
762
  AGENT_RUNNER_URL: SecureUrl,
699
763
  CONNECTION_RETURN_ORIGINS: Origins.pipe(Schema.withDecodingDefaultKey(Effect.succeed(""))),
@@ -731,12 +795,18 @@ const DATABASE_ENV = [
731
795
  ];
732
796
  const DAYTONA_ENV = ["DAYTONA_API_KEY", "DAYTONA_API_URL"];
733
797
  const E2B_ENV = ["E2B_API_KEY"];
734
- const GATEWAY_ENV = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
798
+ const GATEWAY_ENV = [
799
+ "ANTHROPIC_API_KEY",
800
+ "OPENAI_API_KEY",
801
+ "CURSOR_API_KEY",
802
+ "CURSOR_ADMIN_KEY"
803
+ ];
735
804
  const CONNECTIONS_ENV = [
736
805
  "AGENT_RUNNER_URL",
737
806
  "CONNECTION_RETURN_ORIGINS",
738
807
  "CONNECTION_ENCRYPTION_KEY"
739
808
  ];
809
+ const PAYMENTS_ENV = ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"];
740
810
  const AGENT_RUNNER_ENV = [
741
811
  ...DATABASE_ENV,
742
812
  ...DAYTONA_ENV,
@@ -756,19 +826,16 @@ const SERVICE_ENV = [
756
826
  "EGRESS_IPS",
757
827
  ...LOG_ENV,
758
828
  ...OBSERVABILITY_ENV,
759
- ...AGENT_RUNNER_ENV
760
- ];
761
- const CLI_ENV = [
762
- "AGENT_RUNNER_KEY",
763
- "AGENT_RUNNER_ORGANIZATION",
764
- "AGENT_RUNNER_API_URL"
829
+ ...AGENT_RUNNER_ENV,
830
+ ...PAYMENTS_ENV
765
831
  ];
832
+ const CLI_ENV = ["TRACKS_API_KEY", "TRACKS_API_URL"];
766
833
  const isDatabaseVariable = (name) => DATABASE_ENV.some((databaseName) => databaseName === name);
767
834
  SERVICE_ENV.flatMap((name) => isSecret(name) && !isDatabaseVariable(name) ? [name] : []);
768
835
  const readEnv = (...names) => Config.schema(Env.mapFields(Struct.pick(names)));
769
836
  //#endregion
770
837
  //#region package.json
771
- var version = "0.4.0";
838
+ var version = "0.4.2";
772
839
  //#endregion
773
840
  //#region src/connections.ts
774
841
  const encodeConnection = Schema.encodeEffect(Schema.fromJsonString(Connection, { space: 2 }));
@@ -777,7 +844,7 @@ const connectionId = Argument.String("id").pipe(Argument.withDescription("Connec
777
844
  const LOOPBACK = "127.0.0.1";
778
845
  const CALLBACK_PATH = "/callback";
779
846
  const CONSENT_TIMEOUT = "10 minutes";
780
- const RETURNED_PAGE = "<!doctype html><title>Agent Runner</title><p>Back to your terminal: you can close this tab.</p>";
847
+ const RETURNED_PAGE = "<!doctype html><title>Tracks</title><p>Back to your terminal: you can close this tab.</p>";
781
848
  var ConsentFailed = class extends Schema.TaggedError()("ConsentFailed", { reason: Schema.String }) {
782
849
  get message() {
783
850
  return `The connection did not complete: ${this.reason}`;
@@ -833,7 +900,7 @@ const confirmationOf = (params, id) => {
833
900
  onNone: () => Effect.fail(new ConsentFailed({ reason: params.get("error") ?? "no confirmation came back" }))
834
901
  });
835
902
  };
836
- const add = Command.make("add", { server: Argument.String("server-url").pipe(Argument.withDescription("URL of the MCP server, in https")) }, Effect.fn("connections.add")(function* ({ server }) {
903
+ const create$1 = Command.make("create", { server: Argument.String("server-url").pipe(Argument.withDescription("URL of the MCP server, in https")) }, Effect.fn("connections.create")(function* ({ server }) {
837
904
  const client = yield* ApiClient;
838
905
  const callback = yield* loopbackCallback;
839
906
  const created = yield* client.connections.create({ payload: {
@@ -861,14 +928,14 @@ const get$1 = Command.make("get", { id: connectionId }, Effect.fn("connections.g
861
928
  const client = yield* ApiClient;
862
929
  yield* Console.log(yield* encodeConnection(yield* client.connections.get({ params: { id } })));
863
930
  })).pipe(Command.withDescription("Show a connection"));
864
- const remove = Command.make("remove", { id: connectionId }, Effect.fn("connections.remove")(function* ({ id }) {
931
+ const deleteConnection = Command.make("delete", { id: connectionId }, Effect.fn("connections.delete")(function* ({ id }) {
865
932
  yield* (yield* ApiClient).connections.delete({ params: { id } });
866
933
  })).pipe(Command.withDescription("Delete a connection and the access it holds"));
867
934
  const connections = Command.make("connections").pipe(Command.withDescription("Manage connections to MCP servers"), Command.withSubcommands([
868
- add,
935
+ create$1,
869
936
  list,
870
937
  get$1,
871
- remove
938
+ deleteConnection
872
939
  ]));
873
940
  //#endregion
874
941
  //#region src/runs.ts
@@ -1003,12 +1070,12 @@ const follow = Command.make("follow", {
1003
1070
  if ((yield* client.runs.get({ params: { id } })).status === "ended") return;
1004
1071
  }
1005
1072
  })).pipe(Command.withDescription("Print a run's frames, or its events with --events, as they come, one JSON object per line, until the run ends"));
1006
- const submitPrompt = Command.make("prompt", {
1073
+ const sendPrompt = Command.make("send-prompt", {
1007
1074
  id: runId,
1008
1075
  text: Argument.String("text").pipe(Argument.withDescription("Follow-up prompt sent to the agent")),
1009
1076
  model: Flag.String("model").pipe(Flag.withAlias("m"), Flag.withDescription("Model for this prompt only"), Flag.optional)
1010
- }, Effect.fn("runs.prompt")(function* ({ id, text, model }) {
1011
- const prompt = yield* (yield* ApiClient).runs.submitPrompt({
1077
+ }, Effect.fn("runs.sendPrompt")(function* ({ id, text, model }) {
1078
+ const prompt = yield* (yield* ApiClient).runs.sendPrompt({
1012
1079
  params: { id },
1013
1080
  payload: {
1014
1081
  text,
@@ -1042,7 +1109,7 @@ const cancel = Command.make("cancel", { id: runId }, Effect.fn("runs.cancel")(fu
1042
1109
  const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), Command.withSubcommands([
1043
1110
  create,
1044
1111
  get,
1045
- submitPrompt,
1112
+ sendPrompt,
1046
1113
  prompts,
1047
1114
  frames,
1048
1115
  events,
@@ -1063,12 +1130,15 @@ const cli = Command.make("tracks").pipe(Command.withDescription("Run coding agen
1063
1130
  connections
1064
1131
  ]));
1065
1132
  const ApiClientLayer = Layer.unwrap(Effect.map(readEnv(...CLI_ENV), (env) => ApiClient.layer({
1066
- baseUrl: env.AGENT_RUNNER_API_URL,
1067
- agentRunnerKey: env.AGENT_RUNNER_KEY,
1068
- organization: env.AGENT_RUNNER_ORGANIZATION,
1133
+ baseUrl: env.TRACKS_API_URL,
1134
+ apiKey: env.TRACKS_API_KEY,
1069
1135
  userAgent: `${CLI_USER_AGENT}/${version}`
1070
1136
  }))).pipe(Layer.provide(FetchHttpClient.layer));
1071
1137
  const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer);
1072
- Command.run(cli, { version }).pipe(Effect.provide(MainLayer), NodeRuntime.runMain);
1138
+ Command.run(cli, { version }).pipe(Effect.catch((error) => {
1139
+ const explainedByHelp = error._tag === "ShowHelp";
1140
+ const line = "message" in error ? `${error._tag}: ${error.message}` : error._tag;
1141
+ return Effect.andThen(explainedByHelp ? Effect.void : Console.error(line), Effect.sync(() => void (process.exitCode = 1)));
1142
+ }), Effect.provide(MainLayer), NodeRuntime.runMain);
1073
1143
  //#endregion
1074
1144
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpic-ai/tracks",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Run coding agents in sandboxes from the command line",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {