@alpic-ai/tracks 0.4.1 → 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 +116 -63
  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({
@@ -170,8 +169,8 @@ const Agent = Schema.Struct({
170
169
  const repository = Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "URL of a Git repository to clone before the agent starts." }));
171
170
  const MAX_SCRIPT_LENGTH = 32768;
172
171
  const script = (description) => Schema.OptionFromOptionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(MAX_SCRIPT_LENGTH)).annotate({ description: `${description} At most ${MAX_SCRIPT_LENGTH} characters.` }));
173
- 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.");
174
- 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.");
175
174
  const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
176
175
  const RESERVED_VARIABLES = /* @__PURE__ */ new Set(["PATH", "HOME"]);
177
176
  const RESERVED_VARIABLE_PREFIXES = [
@@ -215,7 +214,7 @@ const McpServerInput = Schema.Union([Schema.Struct({
215
214
  description: "An MCP server given by a connection to it, to pick between several accounts."
216
215
  })]).annotate({
217
216
  identifier: "McpServerInput",
218
- 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`."
219
218
  });
220
219
  const McpServersInput = Schema.Array(McpServerInput).check(Schema.isMaxLength(MAX_MCP_SERVERS), Schema.makeFilter((servers) => {
221
220
  const names = servers.map((server) => server.name);
@@ -252,6 +251,26 @@ const RunEnvironment = Schema.Struct({
252
251
  identifier: "Environment",
253
252
  description: "What the agent works on."
254
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
+ })));
255
274
  const Budget = Schema.Finite.check(Schema.isBetween({
256
275
  minimum: .01,
257
276
  maximum: 1e4
@@ -264,6 +283,7 @@ const CreateRunInput = Schema.Struct({
264
283
  agent: Agent,
265
284
  prompt: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do first." }),
266
285
  environment: EnvironmentInput,
286
+ labels: Schema.optionalKey(Labels),
267
287
  budget_usd: Schema.OptionFromOptionalKey(Budget),
268
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`." })),
269
289
  idle_timeout_seconds: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
@@ -275,10 +295,10 @@ const CreateRunInput = Schema.Struct({
275
295
  maximum: MAX_TIME_LIMIT_SECONDS
276
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.` }))
277
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" });
278
- const SubmitPromptInput = Schema.Struct({
298
+ const SendPromptInput = Schema.Struct({
279
299
  text: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do next." }),
280
300
  model: Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "Model for this prompt only. Defaults to the run's model." }))
281
- }).annotate({ identifier: "SubmitPromptInput" });
301
+ }).annotate({ identifier: "SendPromptInput" });
282
302
  const PromptIndex = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).annotate({ description: "Position of the prompt in its run, starting at 1." });
283
303
  const promptFields = {
284
304
  run_id: RunId,
@@ -330,6 +350,7 @@ const FailureKind = Schema.Literals([
330
350
  });
331
351
  const runFields = {
332
352
  id: RunId,
353
+ labels: Labels.annotate({ description: "Labels attached to the run when it was created." }),
333
354
  agent: Agent,
334
355
  environment: RunEnvironment,
335
356
  budget_usd: Schema.Number.annotate({ description: "Maximum the run may spend, in US dollars." }),
@@ -381,7 +402,7 @@ const Run = Schema.Union([
381
402
  ]).annotate({ description: "A coding agent working on your prompt in an isolated sandbox." });
382
403
  var RunNotFound = class extends Schema.TaggedError()("RunNotFound", { message: Schema.String }, {
383
404
  httpApiStatus: 404,
384
- description: "No run with this id exists for this Organization."
405
+ description: "No run with this id exists in your organization."
385
406
  }) {};
386
407
  var ConnectionUnusable = class extends Schema.TaggedError()("ConnectionUnusable", { message: Schema.String }, {
387
408
  httpApiStatus: 422,
@@ -395,6 +416,17 @@ var ModelProviderUnavailable = class extends Schema.TaggedError()("ModelProvider
395
416
  httpApiStatus: 503,
396
417
  description: "The model provider could not be reached to check the model. Retry later."
397
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
+ }) {};
398
430
  var RunNotIdle = class extends Schema.TaggedError()("RunNotIdle", { message: Schema.String }, {
399
431
  httpApiStatus: 409,
400
432
  description: "The run accepts no prompt now: it is one-shot, has ended, or is still working on a prompt."
@@ -438,7 +470,7 @@ const eventFields = {
438
470
  prompt: PromptIndex.annotate({ description: "Index of the prompt the event belongs to." }),
439
471
  captured_at: Schema.DateTimeUtc.annotate({ description: "When the output the event comes from was captured." }),
440
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." })),
441
- 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." })
442
474
  };
443
475
  const Event = Schema.Union([
444
476
  Schema.Struct({
@@ -534,18 +566,35 @@ const EventStreamItem = Schema.Union([Schema.Struct({
534
566
  event: Schema.Literal("heartbeat"),
535
567
  data: Schema.Literal("")
536
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
+ }));
537
574
  var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("create", "/runs", {
538
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." })) },
539
- payload: CreateRunInput,
576
+ payload: CreateRunInputBesidesStartedBy,
540
577
  success: Run.pipe(HttpApiSchema.status(201)),
541
578
  error: [
542
579
  ModelNotOffered,
543
580
  ModelProviderUnavailable,
544
- ConnectionUnusable
581
+ ConnectionUnusable,
582
+ InsufficientBalance,
583
+ OrganizationBlocked
545
584
  ]
546
585
  }).annotateMerge(OpenApi.annotations({
547
586
  summary: "Create a run",
548
- 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`."
549
598
  })), HttpApiEndpoint.get("get", "/runs/:id", {
550
599
  params: { id: RunId },
551
600
  success: Run,
@@ -558,7 +607,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
558
607
  success: RunUsage,
559
608
  error: RunNotFound
560
609
  }).annotateMerge(OpenApi.annotations({
561
- summary: "Get run usage",
610
+ summary: "Get a run's usage",
562
611
  description: "Returns what the run has consumed so far, per model and in sandbox time, and what it cost."
563
612
  })), HttpApiEndpoint.post("end", "/runs/:id/end", {
564
613
  params: { id: RunId },
@@ -574,9 +623,9 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
574
623
  }).annotateMerge(OpenApi.annotations({
575
624
  summary: "Cancel a run",
576
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."
577
- })), HttpApiEndpoint.post("submitPrompt", "/runs/:id/prompts", {
626
+ })), HttpApiEndpoint.post("sendPrompt", "/runs/:id/prompts", {
578
627
  params: { id: RunId },
579
- payload: SubmitPromptInput,
628
+ payload: SendPromptInput,
580
629
  success: Prompt.pipe(HttpApiSchema.status(202)),
581
630
  error: [
582
631
  RunNotFound,
@@ -593,7 +642,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
593
642
  success: List(Prompt),
594
643
  error: RunNotFound
595
644
  }).annotateMerge(OpenApi.annotations({
596
- summary: "List run prompts",
645
+ summary: "List a run's prompts",
597
646
  description: "Returns the run's prompts in order. To get the next page, pass the `index` of the last prompt received as `after`."
598
647
  })), HttpApiEndpoint.get("listFrames", "/runs/:id/frames", {
599
648
  params: { id: RunId },
@@ -604,7 +653,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
604
653
  success: List(Frame),
605
654
  error: RunNotFound
606
655
  }).annotateMerge(OpenApi.annotations({
607
- summary: "List run output",
656
+ summary: "List a run's frames",
608
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`."
609
658
  })), HttpApiEndpoint.get("listEvents", "/runs/:id/events", {
610
659
  params: { id: RunId },
@@ -615,7 +664,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
615
664
  success: List(Event),
616
665
  error: RunNotFound
617
666
  }).annotateMerge(OpenApi.annotations({
618
- summary: "List run events",
667
+ summary: "List a run's events",
619
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`."
620
669
  })), HttpApiEndpoint.get("followFrames", "/runs/:id/frames/stream", {
621
670
  params: { id: RunId },
@@ -623,7 +672,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
623
672
  success: HttpApiSchema.StreamSse({ events: FrameStreamItem }),
624
673
  error: RunNotFound
625
674
  }).annotateMerge(OpenApi.annotations({
626
- summary: "Stream run output",
675
+ summary: "Stream a run's frames",
627
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."
628
677
  })), HttpApiEndpoint.get("followEvents", "/runs/:id/events/stream", {
629
678
  params: { id: RunId },
@@ -631,41 +680,42 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
631
680
  success: HttpApiSchema.StreamSse({ events: EventStreamItem }),
632
681
  error: RunNotFound
633
682
  }).annotateMerge(OpenApi.annotations({
634
- summary: "Stream run events",
683
+ summary: "Stream a run's events",
635
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."
636
- }))).middleware(Authorization).middleware(RequestValidation).annotateMerge(OpenApi.annotations({
685
+ }))).annotateMerge(OpenApi.annotations({
637
686
  title: "Runs",
638
687
  description: "Start coding agents and follow their work."
639
- })) {};
688
+ })).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
640
689
  //#endregion
641
690
  //#region ../contract/src/system.ts
642
691
  const EgressIps = Schema.Struct({ ips: Schema.Array(Schema.String).annotate({ description: "IPv4 addresses, empty when the API runs locally." }) }).annotate({ identifier: "EgressIps" });
643
692
  var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).add(HttpApiEndpoint.get("health", "/health", { success: HttpApiSchema.NoContent }).annotateMerge(OpenApi.annotations({
644
693
  summary: "Check health",
645
- 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."
646
695
  }))).add(HttpApiEndpoint.get("egressIps", "/egress-ips", { success: EgressIps }).annotateMerge(OpenApi.annotations({
647
696
  summary: "List egress IPs",
648
- 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."
649
698
  }))).annotateMerge(OpenApi.annotations({ title: "System" })) {};
650
699
  //#endregion
651
700
  //#region ../contract/src/api.ts
652
- var Api = class extends HttpApi.make("agent-runner").add(RunsApi).add(ConnectionsApi).prefix("/v1").add(SystemApi).annotate(HttpApi.ParseOptions, { errors: "all" }).annotateMerge(OpenApi.annotations({
653
- title: "Agent Runner API",
654
- 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."
655
704
  })) {};
656
705
  //#endregion
657
706
  //#region ../contract/src/client.ts
658
707
  const CLI_USER_AGENT = "tracks";
659
708
  var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiClient") {
660
- 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) => {
661
710
  const located = HttpClientRequest.prependUrl(request, options.baseUrl.href);
662
711
  return options.userAgent === void 0 ? located : HttpClientRequest.setHeader(located, "user-agent", options.userAgent);
663
- }) })).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))))));
664
713
  };
665
714
  //#endregion
666
715
  //#region ../env/src/env.ts
667
716
  const Secret = Schema.Redacted(Schema.NonEmptyString);
668
717
  const AgentRunnerKey = Schema.Redacted(Schema.NonEmptyString.check(Schema.isMinLength(32)));
718
+ const StripeKey = (prefix) => Schema.Redacted(Schema.String.check(Schema.isStartingWith(prefix)));
669
719
  const Port = Schema.Int.check(Schema.isBetween({
670
720
  minimum: 1,
671
721
  maximum: 65535
@@ -675,7 +725,7 @@ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
675
725
  "127.0.0.1",
676
726
  "[::1]"
677
727
  ]);
678
- const AGENT_RUNNER_PATH = "/agent-runner";
728
+ const TRACKS_API_PATH = "/api";
679
729
  const SecureUrl = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" || LOOPBACK_HOSTS.has(url.hostname) || "must use https"));
680
730
  const ENCRYPTION_KEY_BYTES = 32;
681
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}`)));
@@ -691,6 +741,7 @@ const IpAddresses = Schema.String.pipe(Schema.decodeTo(Schema.Array(Schema.Strin
691
741
  const SentryDsn = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" && url.username !== "" || "must be a Sentry DSN"));
692
742
  const Secrets = Schema.Struct({
693
743
  AGENT_RUNNER_KEY: AgentRunnerKey,
744
+ TRACKS_API_KEY: Secret,
694
745
  DAYTONA_API_KEY: Secret,
695
746
  E2B_API_KEY: Secret,
696
747
  ANTHROPIC_API_KEY: Secret,
@@ -700,12 +751,13 @@ const Secrets = Schema.Struct({
700
751
  TYPESAFE_API_KEY: Secret,
701
752
  DATABASE_PASSWORD: Secret,
702
753
  CONNECTION_ENCRYPTION_KEY: EncryptionKey,
754
+ STRIPE_SECRET_KEY: StripeKey("rk_"),
755
+ STRIPE_WEBHOOK_SECRET: StripeKey("whsec_"),
703
756
  SENTRY_DSN: Schema.optionalKey(Schema.Redacted(SentryDsn))
704
757
  });
705
758
  const Variables = Schema.Struct({
706
759
  PORT: Port.pipe(Schema.withDecodingDefaultKey(Effect.succeed(3e3))),
707
- AGENT_RUNNER_ORGANIZATION: Schema.NonEmptyString,
708
- 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}`))),
709
761
  DAYTONA_API_URL: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://app.daytona.io/api"))),
710
762
  AGENT_RUNNER_URL: SecureUrl,
711
763
  CONNECTION_RETURN_ORIGINS: Origins.pipe(Schema.withDecodingDefaultKey(Effect.succeed(""))),
@@ -754,6 +806,7 @@ const CONNECTIONS_ENV = [
754
806
  "CONNECTION_RETURN_ORIGINS",
755
807
  "CONNECTION_ENCRYPTION_KEY"
756
808
  ];
809
+ const PAYMENTS_ENV = ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"];
757
810
  const AGENT_RUNNER_ENV = [
758
811
  ...DATABASE_ENV,
759
812
  ...DAYTONA_ENV,
@@ -773,19 +826,16 @@ const SERVICE_ENV = [
773
826
  "EGRESS_IPS",
774
827
  ...LOG_ENV,
775
828
  ...OBSERVABILITY_ENV,
776
- ...AGENT_RUNNER_ENV
777
- ];
778
- const CLI_ENV = [
779
- "AGENT_RUNNER_KEY",
780
- "AGENT_RUNNER_ORGANIZATION",
781
- "AGENT_RUNNER_API_URL"
829
+ ...AGENT_RUNNER_ENV,
830
+ ...PAYMENTS_ENV
782
831
  ];
832
+ const CLI_ENV = ["TRACKS_API_KEY", "TRACKS_API_URL"];
783
833
  const isDatabaseVariable = (name) => DATABASE_ENV.some((databaseName) => databaseName === name);
784
834
  SERVICE_ENV.flatMap((name) => isSecret(name) && !isDatabaseVariable(name) ? [name] : []);
785
835
  const readEnv = (...names) => Config.schema(Env.mapFields(Struct.pick(names)));
786
836
  //#endregion
787
837
  //#region package.json
788
- var version = "0.4.1";
838
+ var version = "0.4.2";
789
839
  //#endregion
790
840
  //#region src/connections.ts
791
841
  const encodeConnection = Schema.encodeEffect(Schema.fromJsonString(Connection, { space: 2 }));
@@ -794,7 +844,7 @@ const connectionId = Argument.String("id").pipe(Argument.withDescription("Connec
794
844
  const LOOPBACK = "127.0.0.1";
795
845
  const CALLBACK_PATH = "/callback";
796
846
  const CONSENT_TIMEOUT = "10 minutes";
797
- 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>";
798
848
  var ConsentFailed = class extends Schema.TaggedError()("ConsentFailed", { reason: Schema.String }) {
799
849
  get message() {
800
850
  return `The connection did not complete: ${this.reason}`;
@@ -850,7 +900,7 @@ const confirmationOf = (params, id) => {
850
900
  onNone: () => Effect.fail(new ConsentFailed({ reason: params.get("error") ?? "no confirmation came back" }))
851
901
  });
852
902
  };
853
- 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 }) {
854
904
  const client = yield* ApiClient;
855
905
  const callback = yield* loopbackCallback;
856
906
  const created = yield* client.connections.create({ payload: {
@@ -878,14 +928,14 @@ const get$1 = Command.make("get", { id: connectionId }, Effect.fn("connections.g
878
928
  const client = yield* ApiClient;
879
929
  yield* Console.log(yield* encodeConnection(yield* client.connections.get({ params: { id } })));
880
930
  })).pipe(Command.withDescription("Show a connection"));
881
- 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 }) {
882
932
  yield* (yield* ApiClient).connections.delete({ params: { id } });
883
933
  })).pipe(Command.withDescription("Delete a connection and the access it holds"));
884
934
  const connections = Command.make("connections").pipe(Command.withDescription("Manage connections to MCP servers"), Command.withSubcommands([
885
- add,
935
+ create$1,
886
936
  list,
887
937
  get$1,
888
- remove
938
+ deleteConnection
889
939
  ]));
890
940
  //#endregion
891
941
  //#region src/runs.ts
@@ -1020,12 +1070,12 @@ const follow = Command.make("follow", {
1020
1070
  if ((yield* client.runs.get({ params: { id } })).status === "ended") return;
1021
1071
  }
1022
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"));
1023
- const submitPrompt = Command.make("prompt", {
1073
+ const sendPrompt = Command.make("send-prompt", {
1024
1074
  id: runId,
1025
1075
  text: Argument.String("text").pipe(Argument.withDescription("Follow-up prompt sent to the agent")),
1026
1076
  model: Flag.String("model").pipe(Flag.withAlias("m"), Flag.withDescription("Model for this prompt only"), Flag.optional)
1027
- }, Effect.fn("runs.prompt")(function* ({ id, text, model }) {
1028
- const prompt = yield* (yield* ApiClient).runs.submitPrompt({
1077
+ }, Effect.fn("runs.sendPrompt")(function* ({ id, text, model }) {
1078
+ const prompt = yield* (yield* ApiClient).runs.sendPrompt({
1029
1079
  params: { id },
1030
1080
  payload: {
1031
1081
  text,
@@ -1059,7 +1109,7 @@ const cancel = Command.make("cancel", { id: runId }, Effect.fn("runs.cancel")(fu
1059
1109
  const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), Command.withSubcommands([
1060
1110
  create,
1061
1111
  get,
1062
- submitPrompt,
1112
+ sendPrompt,
1063
1113
  prompts,
1064
1114
  frames,
1065
1115
  events,
@@ -1080,12 +1130,15 @@ const cli = Command.make("tracks").pipe(Command.withDescription("Run coding agen
1080
1130
  connections
1081
1131
  ]));
1082
1132
  const ApiClientLayer = Layer.unwrap(Effect.map(readEnv(...CLI_ENV), (env) => ApiClient.layer({
1083
- baseUrl: env.AGENT_RUNNER_API_URL,
1084
- agentRunnerKey: env.AGENT_RUNNER_KEY,
1085
- organization: env.AGENT_RUNNER_ORGANIZATION,
1133
+ baseUrl: env.TRACKS_API_URL,
1134
+ apiKey: env.TRACKS_API_KEY,
1086
1135
  userAgent: `${CLI_USER_AGENT}/${version}`
1087
1136
  }))).pipe(Layer.provide(FetchHttpClient.layer));
1088
1137
  const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer);
1089
- 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);
1090
1143
  //#endregion
1091
1144
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpic-ai/tracks",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Run coding agents in sandboxes from the command line",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {