exponential-cli 1.18.1 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/program.ts
4
- import { Command as Command25 } from "commander";
4
+ import { Command as Command26 } from "commander";
5
5
 
6
6
  // src/commands/auth.ts
7
7
  import { Command } from "commander";
@@ -1187,6 +1187,41 @@ Tickets (${tickets.length} total)`));
1187
1187
  }
1188
1188
  console.log();
1189
1189
  }
1190
+ function transformTicketSearchHit(hit) {
1191
+ return {
1192
+ id: hit.id,
1193
+ number: hit.number,
1194
+ shortId: hit.shortId,
1195
+ title: hit.title,
1196
+ status: hit.status,
1197
+ priority: hit.priority,
1198
+ assignee: hit.assignee ?? null
1199
+ };
1200
+ }
1201
+ function outputTicketSearchJson(hits, query) {
1202
+ console.log(JSON.stringify({
1203
+ query,
1204
+ tickets: hits.map(transformTicketSearchHit),
1205
+ total: hits.length
1206
+ }, null, 2));
1207
+ }
1208
+ function outputTicketSearchPretty(hits, query) {
1209
+ if (hits.length === 0) {
1210
+ console.log(chalk3.gray(`No tickets match "${query}".`));
1211
+ return;
1212
+ }
1213
+ console.log(chalk3.bold(`
1214
+ Tickets matching "${query}" (${hits.length})`));
1215
+ console.log(chalk3.gray("\u2500".repeat(50)));
1216
+ for (const t of hits) {
1217
+ const id = t.shortId ?? (t.number != null ? `#${t.number}` : "?");
1218
+ const statusBadge2 = chalk3[getTicketStatusColor(t.status)](`[${t.status}]`);
1219
+ const assignee = t.assignee ? chalk3.gray(` \u2014 ${t.assignee.name ?? t.assignee.id}`) : "";
1220
+ console.log(` ${statusBadge2} ${chalk3.bold(id)} ${t.title}${assignee}`);
1221
+ console.log(chalk3.gray(` ID: ${t.id}`));
1222
+ }
1223
+ console.log();
1224
+ }
1190
1225
  function transformTicket(ticket) {
1191
1226
  const detail = ticket;
1192
1227
  return {
@@ -3959,6 +3994,23 @@ function createEpicsCommand() {
3959
3994
  }
3960
3995
  }
3961
3996
  );
3997
+ epics.command("delete").description(
3998
+ "Delete an epic. Its tickets and actions are unlinked from it (epicId is cleared), not deleted. Only workspace owners/admins and the epic owner can delete."
3999
+ ).requiredOption("--id <id>", "Epic CUID").action(async (options, cmd) => {
4000
+ const globalOpts = cmd.optsWithGlobals();
4001
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4002
+ try {
4003
+ const client = getClient();
4004
+ await client.epics.delete(options.id);
4005
+ if (useJson) {
4006
+ console.log(JSON.stringify({ deleted: true, id: options.id }, null, 2));
4007
+ } else {
4008
+ console.log("\n\u2713 Epic deleted");
4009
+ }
4010
+ } catch (error) {
4011
+ handleError(error, useJson);
4012
+ }
4013
+ });
3962
4014
  return epics;
3963
4015
  }
3964
4016
 
@@ -4069,6 +4121,42 @@ function createStoriesCommand() {
4069
4121
  }
4070
4122
  }
4071
4123
  );
4124
+ stories.command("update").description("Update a user story. Only the fields you pass are written.").requiredOption("--id <id>", "User story CUID").option("--as-a <text>", '"As a ..." actor').option("--i-want <text>", '"I want ..." capability').option("--so-that <text>", '"So that ..." outcome').option("--acceptance <text>", "Acceptance criteria").option("--scope <id>", 'Feature scope CUID to group the story under (or "null" to ungroup)').action(
4125
+ async (options, cmd) => {
4126
+ const globalOpts = cmd.optsWithGlobals();
4127
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4128
+ try {
4129
+ const client = getClient();
4130
+ const story = await client.userStories.update({
4131
+ id: options.id,
4132
+ asA: options.asA,
4133
+ iWant: options.iWant,
4134
+ soThat: options.soThat,
4135
+ acceptanceCriteria: options.acceptance,
4136
+ scopeId: options.scope === "null" ? null : options.scope
4137
+ });
4138
+ if (useJson) outputUserStoryJson(story);
4139
+ else {
4140
+ console.log("\n\u2713 User story updated");
4141
+ outputUserStoryPretty(story);
4142
+ }
4143
+ } catch (error) {
4144
+ handleError(error, useJson);
4145
+ }
4146
+ }
4147
+ );
4148
+ stories.command("rm").description("Delete a user story").requiredOption("--id <id>", "User story CUID").action(async (options, cmd) => {
4149
+ const globalOpts = cmd.optsWithGlobals();
4150
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
4151
+ try {
4152
+ const client = getClient();
4153
+ const result = await client.userStories.delete(options.id);
4154
+ if (useJson) console.log(JSON.stringify(result, null, 2));
4155
+ else console.log("\n\u2713 User story deleted");
4156
+ } catch (error) {
4157
+ handleError(error, useJson);
4158
+ }
4159
+ });
4072
4160
  return stories;
4073
4161
  }
4074
4162
 
@@ -5635,6 +5723,84 @@ function createProductsCommand() {
5635
5723
  }
5636
5724
  }
5637
5725
  );
5726
+ products.command("update <slugOrId>").description("Update a product. Only the fields you pass are written.").option("-n, --name <name>", "New name").option("-d, --description <text>", "New description").option("--icon <icon>", "Icon shortcode").option("--color <color>", "Color").option("--fun-ticket-ids", "Use fun ticket ids (e.g. prime.toucan) for new tickets").option("--no-fun-ticket-ids", "Use numbered ticket ids for new tickets").option("--workspace <slug|id>", "Workspace (required when using a slug)").action(
5727
+ async (slugOrId, options, cmd) => {
5728
+ const globalOpts = cmd.optsWithGlobals();
5729
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5730
+ try {
5731
+ const client = getClient();
5732
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
5733
+ const existing = await client.products.resolve(workspaceId, slugOrId);
5734
+ const product = await client.products.update({
5735
+ id: existing.id,
5736
+ name: options.name,
5737
+ description: options.description,
5738
+ icon: options.icon,
5739
+ color: options.color,
5740
+ funTicketIds: options.funTicketIds
5741
+ });
5742
+ if (useJson) outputProductJson(product);
5743
+ else {
5744
+ console.log("\n\u2713 Product updated");
5745
+ outputProductPretty(product);
5746
+ }
5747
+ } catch (error) {
5748
+ handleError(error, useJson);
5749
+ }
5750
+ }
5751
+ );
5752
+ products.command("delete <slugOrId>").description(
5753
+ "Delete a product. Deleting a product deletes everything under it \u2014 its features, tickets, epics, research and retrospectives cascade \u2014 so this refuses while it still has any of them, unless --force."
5754
+ ).option("--force", "Delete anyway, taking everything under it with it (destructive)").option("--workspace <slug|id>", "Workspace (required when using a slug)").action(
5755
+ async (slugOrId, options, cmd) => {
5756
+ const globalOpts = cmd.optsWithGlobals();
5757
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
5758
+ try {
5759
+ const client = getClient();
5760
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
5761
+ const product = await client.products.resolve(workspaceId, slugOrId);
5762
+ const counts = product._count ?? {};
5763
+ const blockers = [
5764
+ counts.features ? `${counts.features} feature(s)` : null,
5765
+ counts.tickets ? `${counts.tickets} ticket(s)` : null,
5766
+ counts.researches ? `${counts.researches} research item(s)` : null,
5767
+ counts.retrospectives ? `${counts.retrospectives} retrospective(s)` : null
5768
+ ].filter(Boolean);
5769
+ if (blockers.length > 0 && !options.force) {
5770
+ const payload2 = {
5771
+ deleted: false,
5772
+ id: product.id,
5773
+ slug: product.slug,
5774
+ reason: `has ${blockers.join(", ")}`,
5775
+ counts
5776
+ };
5777
+ if (useJson) console.log(JSON.stringify(payload2, null, 2));
5778
+ else {
5779
+ console.error(
5780
+ `Product "${product.name}" has ${blockers.join(", ")}. Deleting it deletes them too.
5781
+ Re-run with --force to delete it anyway.`
5782
+ );
5783
+ }
5784
+ process.exitCode = 1;
5785
+ return;
5786
+ }
5787
+ await client.products.delete(product.id);
5788
+ const payload = {
5789
+ deleted: true,
5790
+ id: product.id,
5791
+ slug: product.slug,
5792
+ counts
5793
+ };
5794
+ if (useJson) console.log(JSON.stringify(payload, null, 2));
5795
+ else {
5796
+ console.log("\n\u2713 Product deleted");
5797
+ if (blockers.length) console.log(` ${blockers.join(", ")} went with it`);
5798
+ }
5799
+ } catch (error) {
5800
+ handleError(error, useJson);
5801
+ }
5802
+ }
5803
+ );
5638
5804
  return products;
5639
5805
  }
5640
5806
 
@@ -5784,8 +5950,571 @@ Re-run with --force to delete it anyway.`
5784
5950
  return projects;
5785
5951
  }
5786
5952
 
5787
- // src/commands/search.ts
5953
+ // src/commands/runner.ts
5788
5954
  import { Command as Command20 } from "commander";
5955
+ import { hostname, homedir as homedir2, tmpdir as tmpdir2 } from "os";
5956
+ import { execFileSync } from "child_process";
5957
+ import { mkdirSync as mkdirSync3 } from "fs";
5958
+ import { fileURLToPath } from "url";
5959
+ import { resolve } from "path";
5960
+
5961
+ // src/runner/config.ts
5962
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
5963
+ import { dirname, join } from "path";
5964
+ function runnerConfigPath() {
5965
+ return join(dirname(getConfigPath()), "runner.json");
5966
+ }
5967
+ function loadRunnerConfig(path = runnerConfigPath()) {
5968
+ if (!existsSync(path)) return {};
5969
+ try {
5970
+ return JSON.parse(readFileSync2(path, "utf-8"));
5971
+ } catch {
5972
+ throw new Error(`Could not parse ${path}; fix or delete it`);
5973
+ }
5974
+ }
5975
+ function saveRunnerConfig(values, path = runnerConfigPath()) {
5976
+ const next = { ...loadRunnerConfig(path), ...values };
5977
+ mkdirSync(dirname(path), { recursive: true });
5978
+ writeFileSync(path, JSON.stringify(next, null, 2) + "\n");
5979
+ return next;
5980
+ }
5981
+
5982
+ // src/runner/stream.ts
5983
+ var MAX_PAYLOAD_TEXT = 2e3;
5984
+ function clip(value, max = MAX_PAYLOAD_TEXT) {
5985
+ const text = typeof value === "string" ? value : JSON.stringify(value ?? null);
5986
+ return text.length > max ? `${text.slice(0, max)}\u2026` : text;
5987
+ }
5988
+ var StreamParser = class {
5989
+ nextSeq;
5990
+ events = [];
5991
+ resultText = null;
5992
+ isError = false;
5993
+ errorMessage = null;
5994
+ usage = null;
5995
+ sessionId = null;
5996
+ /** The session called ask_owner: the run is parked by the app; the runner must not finish it. */
5997
+ askedOwner = false;
5998
+ /** The session called finish_run itself: the runner's final finish is a no-op. */
5999
+ finishedViaTool = false;
6000
+ constructor(firstSeq = 1) {
6001
+ this.nextSeq = firstSeq;
6002
+ }
6003
+ push(kind, payload) {
6004
+ const event = { seq: this.nextSeq++, kind, payload };
6005
+ this.events.push(event);
6006
+ return event;
6007
+ }
6008
+ /** Feed one line; returns the events it produced (possibly none). */
6009
+ feed(line) {
6010
+ const trimmed = line.trim();
6011
+ if (!trimmed) return [];
6012
+ let parsed;
6013
+ try {
6014
+ parsed = JSON.parse(trimmed);
6015
+ } catch {
6016
+ return [];
6017
+ }
6018
+ const before = this.events.length;
6019
+ switch (parsed.type) {
6020
+ case "system":
6021
+ if (parsed.subtype === "init" && typeof parsed.session_id === "string") this.sessionId = parsed.session_id;
6022
+ break;
6023
+ case "assistant": {
6024
+ const content = parsed.message?.content;
6025
+ if (typeof content === "string") {
6026
+ if (content.trim()) this.push("text", { text: clip(content) });
6027
+ } else if (Array.isArray(content)) {
6028
+ for (const block of content) {
6029
+ if (block.type === "tool_use") {
6030
+ const name = block.name ?? "tool";
6031
+ const input = block.input ?? {};
6032
+ if (name.endsWith("__report_progress") && typeof input.text === "string") {
6033
+ this.push("text", { text: clip(input.text, 500) });
6034
+ } else if (name.endsWith("__ask_owner")) {
6035
+ this.askedOwner = true;
6036
+ this.push("tool_call", { tool: "ask-owner", snippet: clip(input.question, 200) });
6037
+ } else if (name.endsWith("__finish_run")) {
6038
+ this.finishedViaTool = true;
6039
+ this.push("tool_call", { tool: "finish-run", readyToClose: input.readyToClose === true });
6040
+ } else {
6041
+ this.push("tool_call", { tool: name, input: clip(block.input, 500) });
6042
+ }
6043
+ } else if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
6044
+ this.push("text", { text: clip(block.text) });
6045
+ }
6046
+ }
6047
+ }
6048
+ break;
6049
+ }
6050
+ case "user": {
6051
+ const content = parsed.message?.content;
6052
+ if (Array.isArray(content)) {
6053
+ for (const block of content) {
6054
+ if (block.type === "tool_result") {
6055
+ this.push(block.is_error ? "error" : "tool_result", {
6056
+ toolUseId: block.tool_use_id ?? null,
6057
+ output: clip(block.content, 500)
6058
+ });
6059
+ }
6060
+ }
6061
+ }
6062
+ break;
6063
+ }
6064
+ case "result":
6065
+ this.resultText = typeof parsed.result === "string" ? parsed.result : null;
6066
+ this.isError = parsed.is_error === true || (parsed.subtype ?? "").startsWith("error");
6067
+ this.errorMessage = this.isError ? parsed.error ?? parsed.result ?? parsed.subtype ?? "error" : null;
6068
+ this.usage = parsed.usage ?? null;
6069
+ if (typeof parsed.total_cost_usd === "number") {
6070
+ this.usage = { ...this.usage ?? {}, totalCostUsd: parsed.total_cost_usd };
6071
+ }
6072
+ break;
6073
+ default:
6074
+ break;
6075
+ }
6076
+ return this.events.slice(before);
6077
+ }
6078
+ /** Events not yet handed out by a previous `drain`. */
6079
+ drained = 0;
6080
+ drain() {
6081
+ const batch = this.events.slice(this.drained);
6082
+ this.drained = this.events.length;
6083
+ return batch;
6084
+ }
6085
+ snapshot() {
6086
+ return {
6087
+ events: [...this.events],
6088
+ resultText: this.resultText,
6089
+ isError: this.isError,
6090
+ errorMessage: this.errorMessage,
6091
+ usage: this.usage,
6092
+ sessionId: this.sessionId,
6093
+ askedOwner: this.askedOwner,
6094
+ finishedViaTool: this.finishedViaTool
6095
+ };
6096
+ }
6097
+ };
6098
+
6099
+ // src/runner/mcp.ts
6100
+ import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
6101
+ import { tmpdir } from "os";
6102
+ import { join as join2 } from "path";
6103
+ var MCP_SERVER_NAME = "exponential";
6104
+ var RUN_TOOL_NAMES = ["report_progress", "ask_owner", "finish_run"];
6105
+ function buildMcpConfig(input) {
6106
+ const [command, ...args] = input.command ?? ["npx", "-y", "exponential-mcp"];
6107
+ return {
6108
+ mcpServers: {
6109
+ [MCP_SERVER_NAME]: {
6110
+ command,
6111
+ args,
6112
+ env: {
6113
+ EXPONENTIAL_API_KEY: input.token,
6114
+ EXPONENTIAL_API_URL: input.apiUrl,
6115
+ EXPONENTIAL_RUN_ID: input.runId,
6116
+ EXPONENTIAL_RUNNER_ID: input.runnerId
6117
+ }
6118
+ }
6119
+ }
6120
+ };
6121
+ }
6122
+ function allowedRunTools(extra = []) {
6123
+ return [...RUN_TOOL_NAMES.map((t) => `mcp__${MCP_SERVER_NAME}__${t}`), ...extra];
6124
+ }
6125
+ function writeMcpSession(input, extraAllowed = []) {
6126
+ const dir = mkdtempSync(join2(tmpdir(), "exponential-runner-"));
6127
+ const file = join2(dir, "mcp.json");
6128
+ writeFileSync2(file, JSON.stringify(buildMcpConfig(input), null, 2), { mode: 384 });
6129
+ return {
6130
+ args: ["--mcp-config", file, "--allowedTools", allowedRunTools(extraAllowed).join(",")],
6131
+ cleanup: () => rmSync(dir, { recursive: true, force: true })
6132
+ };
6133
+ }
6134
+
6135
+ // src/runner/retry.ts
6136
+ var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
6137
+ function isRetryableError(error) {
6138
+ const code = error?.data?.code;
6139
+ if (!code) return true;
6140
+ return !["FORBIDDEN", "UNAUTHORIZED", "NOT_FOUND", "PRECONDITION_FAILED", "BAD_REQUEST"].includes(code);
6141
+ }
6142
+ async function withRetry(fn, options = {}) {
6143
+ const attempts = options.attempts ?? 5;
6144
+ const baseMs = options.baseMs ?? 1e3;
6145
+ const maxMs = options.maxMs ?? 3e4;
6146
+ const retryable = options.retryable ?? isRetryableError;
6147
+ const sleep = options.sleep ?? defaultSleep;
6148
+ const jitter = options.jitter ?? (() => 0.5 + Math.random());
6149
+ let lastError;
6150
+ for (let attempt = 1; attempt <= attempts; attempt++) {
6151
+ try {
6152
+ return await fn();
6153
+ } catch (error) {
6154
+ lastError = error;
6155
+ if (attempt === attempts || !retryable(error)) throw error;
6156
+ const delay = Math.min(maxMs, baseMs * 2 ** (attempt - 1)) * jitter();
6157
+ options.log?.(`retry ${attempt}/${attempts - 1} in ${Math.round(delay)}ms: ${error instanceof Error ? error.message : String(error)}`);
6158
+ await sleep(delay);
6159
+ }
6160
+ }
6161
+ throw lastError;
6162
+ }
6163
+ var Backoff = class {
6164
+ constructor(baseMs, maxMs = 5 * 6e4) {
6165
+ this.baseMs = baseMs;
6166
+ this.maxMs = maxMs;
6167
+ }
6168
+ failures = 0;
6169
+ /** Current wait, before recording an outcome. */
6170
+ get delayMs() {
6171
+ return Math.min(this.maxMs, this.baseMs * 2 ** this.failures);
6172
+ }
6173
+ success() {
6174
+ this.failures = 0;
6175
+ }
6176
+ failure() {
6177
+ this.failures = Math.min(this.failures + 1, 16);
6178
+ }
6179
+ };
6180
+
6181
+ // src/runner/loop.ts
6182
+ var DEFAULT_HEARTBEAT_MS = 6e4;
6183
+ var DEFAULT_FLUSH_MS = 5e3;
6184
+ var MAX_BATCH = 200;
6185
+ function promptsFor(run) {
6186
+ const system = run.messages.filter((m) => m.role === "system").map((m) => m.content).join("\n\n");
6187
+ const user = run.messages.filter((m) => m.role === "user").map((m) => m.content).join("\n\n");
6188
+ return { systemPrompt: system, prompt: user || `You have been assigned the action "${run.action.name}".` };
6189
+ }
6190
+ async function runOnce(options) {
6191
+ const { client, runnerId, cwd, spawn: spawn2 } = options;
6192
+ const log = options.log ?? (() => void 0);
6193
+ const run = await client.agentRuns.claim(runnerId);
6194
+ if (!run) return null;
6195
+ log(`claimed run ${run.id} for action "${run.action.name}"`);
6196
+ const parser = new StreamParser(1);
6197
+ let eventsSent = 0;
6198
+ let cancelled = false;
6199
+ const retry = { log, sleep: options.sleep, attempts: options.retryAttempts };
6200
+ let pending = [];
6201
+ const flush = async () => {
6202
+ pending.push(...parser.drain());
6203
+ while (pending.length > 0) {
6204
+ const slice = pending.slice(0, MAX_BATCH);
6205
+ await withRetry(() => client.agentRuns.appendEvents(run.id, slice, runnerId), retry);
6206
+ pending = pending.slice(slice.length);
6207
+ eventsSent += slice.length;
6208
+ }
6209
+ };
6210
+ const { systemPrompt, prompt } = promptsFor(run);
6211
+ let mcp = null;
6212
+ if (options.mcp) {
6213
+ mcp = writeMcpSession(
6214
+ { token: options.mcp.token, apiUrl: options.mcp.apiUrl, runId: run.id, runnerId, command: options.mcp.command },
6215
+ options.mcp.allowedTools
6216
+ );
6217
+ }
6218
+ const session = spawn2({ prompt, systemPrompt, cwd, extraArgs: [...options.extraArgs ?? [], ...mcp?.args ?? []] });
6219
+ const heartbeat = setInterval(() => {
6220
+ void client.agentRuns.heartbeat(run.id, runnerId).then((h) => {
6221
+ if (!h.ok) {
6222
+ cancelled = true;
6223
+ log(`run ${run.id} left RUNNING (cancelled) \u2014 stopping the session`);
6224
+ session.kill();
6225
+ }
6226
+ }).catch((err) => log(`heartbeat failed: ${err instanceof Error ? err.message : String(err)}`));
6227
+ }, options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
6228
+ const flusher = setInterval(() => {
6229
+ void flush().catch((err) => log(`append failed: ${err instanceof Error ? err.message : String(err)}`));
6230
+ }, options.flushMs ?? DEFAULT_FLUSH_MS);
6231
+ let exitCode = 1;
6232
+ let spawnError = null;
6233
+ try {
6234
+ for await (const line of session.lines) {
6235
+ parser.feed(line);
6236
+ }
6237
+ exitCode = await session.exit;
6238
+ } catch (err) {
6239
+ spawnError = err instanceof Error ? err.message : String(err);
6240
+ } finally {
6241
+ clearInterval(heartbeat);
6242
+ clearInterval(flusher);
6243
+ mcp?.cleanup();
6244
+ }
6245
+ await flush().catch((err) => log(`final append failed: ${err instanceof Error ? err.message : String(err)}`));
6246
+ if (cancelled) return { runId: run.id, status: "FAILED", eventsSent };
6247
+ const snap = parser.snapshot();
6248
+ if (snap.askedOwner) {
6249
+ log(`run ${run.id} is waiting on the owner (${eventsSent} events)`);
6250
+ return { runId: run.id, status: "WAITING_ON_OWNER", eventsSent };
6251
+ }
6252
+ if (snap.finishedViaTool) {
6253
+ log(`run ${run.id} finished by the session (${eventsSent} events)`);
6254
+ return { runId: run.id, status: "SUCCEEDED", eventsSent };
6255
+ }
6256
+ let outcome;
6257
+ if (spawnError) {
6258
+ outcome = { status: "FAILED", error: spawnError };
6259
+ } else if (snap.isError || exitCode !== 0) {
6260
+ outcome = {
6261
+ status: "FAILED",
6262
+ error: snap.errorMessage ?? `claude exited with code ${exitCode}`,
6263
+ summary: snap.resultText ?? void 0,
6264
+ usage: snap.usage ?? void 0
6265
+ };
6266
+ } else {
6267
+ outcome = {
6268
+ status: "SUCCEEDED",
6269
+ summary: snap.resultText ?? void 0,
6270
+ usage: snap.usage ?? void 0
6271
+ };
6272
+ }
6273
+ await withRetry(() => client.agentRuns.finish(run.id, outcome, runnerId), retry);
6274
+ log(`run ${run.id} ${outcome.status.toLowerCase()} (${eventsSent} events)`);
6275
+ return { runId: run.id, status: outcome.status, eventsSent };
6276
+ }
6277
+ async function runForever(options) {
6278
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
6279
+ const log = options.log ?? (() => void 0);
6280
+ const backoff = new Backoff(options.intervalMs);
6281
+ while (!options.shouldStop()) {
6282
+ let outcome = null;
6283
+ let failed = false;
6284
+ try {
6285
+ outcome = await runOnce(options);
6286
+ backoff.success();
6287
+ } catch (err) {
6288
+ failed = true;
6289
+ backoff.failure();
6290
+ log(`runner error: ${err instanceof Error ? err.message : String(err)} \u2014 next poll in ${Math.round(backoff.delayMs / 1e3)}s`);
6291
+ }
6292
+ if ((!outcome || failed) && !options.shouldStop()) await sleep(backoff.delayMs);
6293
+ }
6294
+ }
6295
+
6296
+ // src/runner/spawn.ts
6297
+ import { spawn } from "child_process";
6298
+ import { createInterface } from "readline";
6299
+ var spawnClaude = ({ prompt, systemPrompt, cwd, extraArgs = [] }) => {
6300
+ const child = spawn(
6301
+ "claude",
6302
+ ["-p", prompt, "--output-format", "stream-json", "--verbose", "--append-system-prompt", systemPrompt, ...extraArgs],
6303
+ { cwd, stdio: ["ignore", "pipe", "pipe"], env: process.env }
6304
+ );
6305
+ const stderr = [];
6306
+ child.stderr?.on("data", (chunk) => stderr.push(chunk.toString()));
6307
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
6308
+ const exit = new Promise((resolve2, reject) => {
6309
+ child.on("error", (err) => reject(new Error(`could not start claude: ${err.message}`)));
6310
+ child.on("close", (code) => {
6311
+ if (code !== 0 && stderr.length) {
6312
+ process.stderr.write(stderr.join("").slice(-2e3));
6313
+ }
6314
+ resolve2(code ?? 1);
6315
+ });
6316
+ });
6317
+ return { lines, exit, kill: () => child.kill("SIGTERM") };
6318
+ };
6319
+
6320
+ // src/runner/install.ts
6321
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, existsSync as existsSync2, rmSync as rmSync2 } from "fs";
6322
+ import { homedir, platform } from "os";
6323
+ import { join as join3 } from "path";
6324
+ var SERVICE_LABEL = "im.exponential.runner";
6325
+ function xmlEscape(value) {
6326
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6327
+ }
6328
+ function launchdPlist(input) {
6329
+ const args = [input.nodeBin, input.exponentialBin, "runner", "start", "--runner-id", input.runnerId, "--cwd", input.cwd];
6330
+ return `<?xml version="1.0" encoding="UTF-8"?>
6331
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
6332
+ <plist version="1.0">
6333
+ <dict>
6334
+ <key>Label</key><string>${SERVICE_LABEL}</string>
6335
+ <key>ProgramArguments</key>
6336
+ <array>
6337
+ ${args.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n")}
6338
+ </array>
6339
+ <key>EnvironmentVariables</key>
6340
+ <dict>
6341
+ <key>PATH</key><string>${xmlEscape(input.path)}</string>
6342
+ <key>HOME</key><string>${xmlEscape(input.home)}</string>
6343
+ </dict>
6344
+ <key>WorkingDirectory</key><string>${xmlEscape(input.cwd)}</string>
6345
+ <key>RunAtLoad</key><true/>
6346
+ <key>KeepAlive</key><true/>
6347
+ <key>StandardOutPath</key><string>${xmlEscape(join3(input.logDir, "runner.log"))}</string>
6348
+ <key>StandardErrorPath</key><string>${xmlEscape(join3(input.logDir, "runner.log"))}</string>
6349
+ </dict>
6350
+ </plist>
6351
+ `;
6352
+ }
6353
+ function systemdUnit(input) {
6354
+ const q = (s) => `"${s.replace(/"/g, '\\"')}"`;
6355
+ return `[Unit]
6356
+ Description=Exponential Assistant runner
6357
+ After=network-online.target
6358
+
6359
+ [Service]
6360
+ ExecStart=${q(input.nodeBin)} ${q(input.exponentialBin)} runner start --runner-id ${q(input.runnerId)} --cwd ${q(input.cwd)}
6361
+ WorkingDirectory=${input.cwd}
6362
+ Environment=PATH=${input.path}
6363
+ Environment=HOME=${input.home}
6364
+ Restart=always
6365
+ RestartSec=10
6366
+
6367
+ [Install]
6368
+ WantedBy=default.target
6369
+ `;
6370
+ }
6371
+ function planService(input, os = platform(), home = homedir()) {
6372
+ if (os === "darwin") {
6373
+ const file = join3(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
6374
+ return {
6375
+ kind: "launchd",
6376
+ file,
6377
+ contents: launchdPlist(input),
6378
+ load: ["launchctl", "bootstrap", `gui/${process.getuid?.() ?? 501}`, file],
6379
+ unload: ["launchctl", "bootout", `gui/${process.getuid?.() ?? 501}/${SERVICE_LABEL}`]
6380
+ };
6381
+ }
6382
+ if (os === "linux") {
6383
+ const file = join3(home, ".config", "systemd", "user", "exponential-runner.service");
6384
+ return {
6385
+ kind: "systemd",
6386
+ file,
6387
+ contents: systemdUnit(input),
6388
+ load: ["systemctl", "--user", "enable", "--now", "exponential-runner.service"],
6389
+ unload: ["systemctl", "--user", "disable", "--now", "exponential-runner.service"]
6390
+ };
6391
+ }
6392
+ throw new Error(`runner install is not supported on ${os}; run "exponential runner start" in a terminal instead`);
6393
+ }
6394
+ function writeServiceFile(plan) {
6395
+ mkdirSync2(join3(plan.file, ".."), { recursive: true });
6396
+ writeFileSync3(plan.file, plan.contents, { mode: 420 });
6397
+ }
6398
+ function removeServiceFile(plan) {
6399
+ if (!existsSync2(plan.file)) return false;
6400
+ rmSync2(plan.file);
6401
+ return true;
6402
+ }
6403
+
6404
+ // src/commands/runner.ts
6405
+ function createRunnerCommand() {
6406
+ const cmd = new Command20("runner").description(
6407
+ "Run your Assistant locally: claim its queued runs and work them with Claude Code on this machine"
6408
+ );
6409
+ cmd.command("start").description("Poll for queued runs and work them until stopped (Ctrl-C)").option("--runner-id <id>", "Name of this runner, recorded on each run", hostname()).option("--cwd <dir>", "Working directory for the spawned CLI (overrides runner config; never from the server)").option("--interval <seconds>", "Seconds between polls when nothing is queued", "15").option("--once", "Claim at most one run, work it, and exit").option("--no-mcp", "Spawn without the Exponential MCP run tools (progress still streams; the session cannot ask the owner)").action(async (opts) => {
6410
+ try {
6411
+ const client = getClient();
6412
+ const settings = loadRunnerConfig();
6413
+ const cwd = resolve(opts.cwd ?? settings.cwd ?? process.cwd());
6414
+ const auth = getConfig();
6415
+ const log = (line) => console.error(`[runner ${opts.runnerId}] ${line}`);
6416
+ const base = {
6417
+ client,
6418
+ runnerId: opts.runnerId,
6419
+ cwd,
6420
+ spawn: spawnClaude,
6421
+ log,
6422
+ mcp: opts.mcp ? { token: auth.token, apiUrl: auth.apiUrl, command: settings.mcpCommand, allowedTools: settings.allowedTools } : void 0
6423
+ };
6424
+ if (opts.once) {
6425
+ const outcome = await runOnce(base);
6426
+ console.log(JSON.stringify(outcome ?? { runId: null, status: "IDLE", eventsSent: 0 }));
6427
+ return;
6428
+ }
6429
+ let stop = false;
6430
+ const onSignal = () => {
6431
+ stop = true;
6432
+ log("stopping after the current run");
6433
+ };
6434
+ process.on("SIGINT", onSignal);
6435
+ process.on("SIGTERM", onSignal);
6436
+ log(`polling every ${opts.interval}s in ${cwd}`);
6437
+ await runForever({ ...base, intervalMs: Number(opts.interval) * 1e3, shouldStop: () => stop });
6438
+ } catch (error) {
6439
+ handleError(error);
6440
+ }
6441
+ });
6442
+ cmd.command("install").description("Keep the runner running as a user service (launchd on macOS, systemd --user on Linux)").option("--runner-id <id>", "Name of this runner", hostname()).option("--cwd <dir>", "Working directory for the spawned CLI (defaults to runner config, then the current directory)").option("--dry-run", "Print the service file and commands without installing").action((opts) => {
6443
+ try {
6444
+ getClient();
6445
+ const settings = loadRunnerConfig();
6446
+ const cwd = resolve(opts.cwd ?? settings.cwd ?? process.cwd());
6447
+ const logDir = resolve(homedir2(), "Library", "Logs", "exponential-runner");
6448
+ const plan = planService({
6449
+ exponentialBin: resolve(fileURLToPath(import.meta.url), "..", "..", "bin", "exponential.js"),
6450
+ nodeBin: process.execPath,
6451
+ runnerId: opts.runnerId,
6452
+ cwd,
6453
+ logDir: process.platform === "darwin" ? logDir : resolve(tmpdir2(), "exponential-runner"),
6454
+ path: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
6455
+ home: homedir2()
6456
+ });
6457
+ if (opts.dryRun) {
6458
+ console.log(`# ${plan.file}
6459
+ ${plan.contents}
6460
+ # load: ${plan.load.join(" ")}
6461
+ # unload: ${plan.unload.join(" ")}`);
6462
+ return;
6463
+ }
6464
+ if (plan.kind === "launchd") mkdirSync3(logDir, { recursive: true });
6465
+ saveRunnerConfig({ cwd });
6466
+ writeServiceFile(plan);
6467
+ try {
6468
+ execFileSync(plan.unload[0], plan.unload.slice(1), { stdio: "ignore" });
6469
+ } catch {
6470
+ }
6471
+ execFileSync(plan.load[0], plan.load.slice(1), { stdio: "inherit" });
6472
+ console.error(`Installed ${plan.kind} service ${plan.file}; the runner works in ${cwd}.`);
6473
+ } catch (error) {
6474
+ handleError(error);
6475
+ }
6476
+ });
6477
+ cmd.command("uninstall").description("Stop and remove the runner user service").action(() => {
6478
+ try {
6479
+ const plan = planService({
6480
+ exponentialBin: "",
6481
+ nodeBin: "",
6482
+ runnerId: "",
6483
+ cwd: "",
6484
+ logDir: "",
6485
+ path: "",
6486
+ home: homedir2()
6487
+ });
6488
+ try {
6489
+ execFileSync(plan.unload[0], plan.unload.slice(1), { stdio: "ignore" });
6490
+ } catch {
6491
+ }
6492
+ console.error(removeServiceFile(plan) ? `Removed ${plan.file}` : `No service file at ${plan.file}`);
6493
+ } catch (error) {
6494
+ handleError(error);
6495
+ }
6496
+ });
6497
+ const config = cmd.command("config").description(`Runner settings on this machine (${runnerConfigPath()})`);
6498
+ config.command("show").description("Print the runner settings").action(() => {
6499
+ console.log(JSON.stringify({ path: runnerConfigPath(), ...loadRunnerConfig() }, null, 2));
6500
+ });
6501
+ config.command("set").description("Set a runner setting: cwd <dir> | allowed-tools <a,b,c> | mcp-command <cmd ...>").argument("<key>", "cwd | allowed-tools | mcp-command").argument("<value...>", "The value").action((key, value) => {
6502
+ try {
6503
+ let next;
6504
+ if (key === "cwd") next = saveRunnerConfig({ cwd: resolve(value.join(" ")) });
6505
+ else if (key === "allowed-tools") next = saveRunnerConfig({ allowedTools: value.join(" ").split(",").map((t) => t.trim()).filter(Boolean) });
6506
+ else if (key === "mcp-command") next = saveRunnerConfig({ mcpCommand: value });
6507
+ else throw new Error(`Unknown setting "${key}". Use cwd, allowed-tools or mcp-command.`);
6508
+ console.log(JSON.stringify(next, null, 2));
6509
+ } catch (error) {
6510
+ handleError(error);
6511
+ }
6512
+ });
6513
+ return cmd;
6514
+ }
6515
+
6516
+ // src/commands/search.ts
6517
+ import { Command as Command21 } from "commander";
5789
6518
  import chalk8 from "chalk";
5790
6519
  var TYPE_LABELS = {
5791
6520
  workspace: "Workspaces",
@@ -5804,7 +6533,7 @@ var TYPE_LABELS = {
5804
6533
  product: "Products"
5805
6534
  };
5806
6535
  function createSearchCommand() {
5807
- const search = new Command20("search").description(
6536
+ const search = new Command21("search").description(
5808
6537
  "Global search across projects, actions, goals, workspaces and more \u2014 same coverage as the app's Cmd+K palette"
5809
6538
  ).argument("<query>", "Text to search for").option("-w, --workspace <slug>", "Restrict results to one workspace (slug or ID)").option("-l, --limit <n>", "Max results per entity type (1-25, default 10)").action(async (query, options, cmd) => {
5810
6539
  const globalOpts = cmd.optsWithGlobals();
@@ -5863,7 +6592,7 @@ ${TYPE_LABELS[type] ?? type}`));
5863
6592
  }
5864
6593
 
5865
6594
  // src/commands/tickets.ts
5866
- import { Command as Command21 } from "commander";
6595
+ import { Command as Command22 } from "commander";
5867
6596
  function collectRepeatable(value, previous) {
5868
6597
  return [...previous, value];
5869
6598
  }
@@ -5906,7 +6635,7 @@ function validateTicketType(value) {
5906
6635
  return value;
5907
6636
  }
5908
6637
  function createTicketsCommand() {
5909
- const tickets = new Command21("tickets").description(
6638
+ const tickets = new Command22("tickets").description(
5910
6639
  "Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
5911
6640
  );
5912
6641
  tickets.command("list").description(
@@ -5995,6 +6724,49 @@ function createTicketsCommand() {
5995
6724
  }
5996
6725
  }
5997
6726
  );
6727
+ tickets.command("search <query>").description(
6728
+ [
6729
+ "Find tickets in a product by free text \u2014 the same picker search the app uses",
6730
+ "when you link a dependency. Matches title (case-insensitive substring), shortId",
6731
+ '(word-order-insensitive, so "toucan.prime" finds prime.toucan) or an exact',
6732
+ "ticket number. Most recently updated first. Returns slim rows (id, number,",
6733
+ "shortId, title, status, priority, assignee); use `tickets get` for the rest."
6734
+ ].join("\n")
6735
+ ).requiredOption("--product <slug|id>", "Product slug or CUID").option("--workspace <slug|id>", "Workspace (required when --product is a slug)").option("--limit <n>", "Max results, 1-50 (default 20)").option("--exclude <id>", "Ticket CUID to leave out of the results").action(
6736
+ async (query, options, cmd) => {
6737
+ const globalOpts = cmd.optsWithGlobals();
6738
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
6739
+ try {
6740
+ const trimmed = query.trim();
6741
+ if (!trimmed) {
6742
+ throw new Error("Search query is empty. Use `tickets list` to list a product's tickets.");
6743
+ }
6744
+ if (trimmed.length > 200) {
6745
+ throw new Error("Search query is too long (max 200 characters).");
6746
+ }
6747
+ let limit;
6748
+ if (options.limit !== void 0) {
6749
+ limit = Number(options.limit);
6750
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
6751
+ throw new Error(`Invalid --limit "${options.limit}". Use a whole number from 1 to 50.`);
6752
+ }
6753
+ }
6754
+ const client = getClient();
6755
+ const workspaceId = await resolveWorkspaceId(client, options.workspace);
6756
+ const productId = await resolveProductId(client, workspaceId, options.product);
6757
+ const hits = await client.tickets.search({
6758
+ productId,
6759
+ query: trimmed,
6760
+ excludeTicketId: options.exclude,
6761
+ limit
6762
+ });
6763
+ if (useJson) outputTicketSearchJson(hits, trimmed);
6764
+ else outputTicketSearchPretty(hits, trimmed);
6765
+ } catch (error) {
6766
+ handleError(error, useJson);
6767
+ }
6768
+ }
6769
+ );
5998
6770
  tickets.command("get <id>").description("Show a ticket with dependencies, actions, and comments").action(async (id, _options, cmd) => {
5999
6771
  const globalOpts = cmd.optsWithGlobals();
6000
6772
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -6232,7 +7004,7 @@ function createTicketsCommand() {
6232
7004
  handleError(error, useJson);
6233
7005
  }
6234
7006
  });
6235
- const comment = new Command21("comment").description("Manage comments on a ticket");
7007
+ const comment = new Command22("comment").description("Manage comments on a ticket");
6236
7008
  comment.command("list").description("List comments on a ticket").requiredOption("--id <id>", "Ticket CUID").action(async (options, cmd) => {
6237
7009
  const globalOpts = cmd.optsWithGlobals();
6238
7010
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -6324,7 +7096,7 @@ function createTicketsCommand() {
6324
7096
  }
6325
7097
 
6326
7098
  // src/commands/time.ts
6327
- import { Command as Command22 } from "commander";
7099
+ import { Command as Command23 } from "commander";
6328
7100
  import { segmentConversation } from "exponential-sdk";
6329
7101
  var SOURCES2 = ["manual", "claude-desktop", "agent-run"];
6330
7102
  var STATUSES2 = ["PROPOSED", "CONFIRMED"];
@@ -6414,7 +7186,7 @@ function batchItemToInput(item, index, defaults) {
6414
7186
  };
6415
7187
  }
6416
7188
  function createTimeCommand() {
6417
- const time = new Command22("time").description(
7189
+ const time = new Command23("time").description(
6418
7190
  `Time entries \u2014 log the day's work and list it. ${OWNERSHIP_NOTE}`
6419
7191
  );
6420
7192
  time.command("log").description(
@@ -6584,10 +7356,10 @@ function parseMessagesFile(raw) {
6584
7356
  }
6585
7357
 
6586
7358
  // src/commands/workspaces.ts
6587
- import { Command as Command23 } from "commander";
7359
+ import { Command as Command24 } from "commander";
6588
7360
  import chalk9 from "chalk";
6589
7361
  function createWorkspacesCommand() {
6590
- const workspaces = new Command23("workspaces").description("Manage workspaces");
7362
+ const workspaces = new Command24("workspaces").description("Manage workspaces");
6591
7363
  workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
6592
7364
  const globalOpts = cmd.optsWithGlobals();
6593
7365
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -6677,7 +7449,7 @@ Members (${members.length})
6677
7449
  }
6678
7450
 
6679
7451
  // src/commands/goals.ts
6680
- import { Command as Command24 } from "commander";
7452
+ import { Command as Command25 } from "commander";
6681
7453
  var GOAL_STATUSES = [
6682
7454
  "planned",
6683
7455
  "active",
@@ -6789,7 +7561,7 @@ function validateKeyResultUnit(value) {
6789
7561
  return value;
6790
7562
  }
6791
7563
  function createKeyResultsCommand() {
6792
- const kr = new Command24("kr").description(
7564
+ const kr = new Command25("kr").description(
6793
7565
  "Key results on an objective. Ids are CUIDs; --goal takes the objective's integer id."
6794
7566
  );
6795
7567
  kr.command("list").description("List key results").option("--goal <id>", "Objective ID (a number)").option("-w, --workspace <slug|id>", "Workspace (workspace-wide: every member's KRs)").option("--period <period>", "Period, e.g. Q3-2026").option("--status <status>", `Filter by status: ${KEY_RESULT_STATUSES.join(", ")}`).option("--mine", "Narrow a workspace list to key results you own").action(
@@ -6992,7 +7764,7 @@ function createKeyResultsCommand() {
6992
7764
  return kr;
6993
7765
  }
6994
7766
  function createGoalCommentsCommand() {
6995
- const comment = new Command24("comment").description(
7767
+ const comment = new Command25("comment").description(
6996
7768
  "Read and post comments on a goal. Mention teammates with --mention."
6997
7769
  );
6998
7770
  comment.command("list").description("List comments on a goal").requiredOption("--goal <id>", "Goal ID (a number)").action(async (options, cmd) => {
@@ -7087,18 +7859,39 @@ function createGoalCommentsCommand() {
7087
7859
  return comment;
7088
7860
  }
7089
7861
  function createGoalsCommand() {
7090
- const goals = new Command24("goals").description(
7862
+ const goals = new Command25("goals").description(
7091
7863
  "Manage goals (objectives) and their key results. Objectives have integer ids; key results are CUIDs."
7092
7864
  );
7093
7865
  goals.command("list").description(
7094
7866
  "List objectives, each with the projects linked to it \u2014 the join that ties an action (which carries a projectId) back to a goal."
7095
- ).option("-w, --workspace <slug|id>", "Workspace slug or ID (defaults to your default workspace)").option("--all-workspaces", "List across every workspace you belong to").option("--period <period>", "Filter by period, e.g. Q3-2026").option("--status <status>", `Filter by status: ${GOAL_STATUSES.join(", ")}`).option("--tree", "Render the parent/child cascade instead of a flat list").action(
7867
+ ).option("-w, --workspace <slug|id>", "Workspace slug or ID (defaults to your default workspace)").option("--all-workspaces", "List across every workspace you belong to").option("--period <period>", "Filter by period, e.g. Q3-2026").option("--status <status>", `Filter by status: ${GOAL_STATUSES.join(", ")}`).option("--tree", "Render the parent/child cascade instead of a flat list").option(
7868
+ "--project <cuid>",
7869
+ "Only objectives linked to this project (yours only \u2014 the server scopes this lookup to objectives you created). Not combinable with --workspace, --all-workspaces or --tree."
7870
+ ).action(
7096
7871
  async (options, cmd) => {
7097
7872
  const globalOpts = cmd.optsWithGlobals();
7098
7873
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
7099
7874
  try {
7100
7875
  const status = validateGoalStatus(options.status);
7101
7876
  const client = getClient();
7877
+ if (options.project) {
7878
+ const conflicting = [
7879
+ options.workspace ? "--workspace" : null,
7880
+ options.allWorkspaces ? "--all-workspaces" : null,
7881
+ options.tree ? "--tree" : null
7882
+ ].filter(Boolean);
7883
+ if (conflicting.length > 0) {
7884
+ throw new Error(
7885
+ `--project can't be combined with ${conflicting.join(", ")}.`
7886
+ );
7887
+ }
7888
+ const list2 = (await client.goals.listByProject(options.project)).filter(
7889
+ (g) => (!options.period || g.period === options.period) && (!status || g.status === status)
7890
+ );
7891
+ if (useJson) outputGoalsJson(list2);
7892
+ else outputGoalsPretty(list2);
7893
+ return;
7894
+ }
7102
7895
  const workspaces = await targetWorkspaces(client, options);
7103
7896
  if (options.tree) {
7104
7897
  const trees = await fanOutByWorkspace(
@@ -7353,7 +8146,7 @@ Re-run with --with-key-results to go ahead, or move them with \`goals kr update
7353
8146
  return goals;
7354
8147
  }
7355
8148
  function createOkrsCommand() {
7356
- const okrs = new Command24("okrs").description(
8149
+ const okrs = new Command25("okrs").description(
7357
8150
  "OKRs \u2014 objectives with their key results nested. Manage the parts with `goals` and `goals kr`."
7358
8151
  );
7359
8152
  okrs.command("list").description(
@@ -7431,18 +8224,18 @@ function createOkrsCommand() {
7431
8224
  }
7432
8225
 
7433
8226
  // src/program.ts
7434
- import { readFileSync as readFileSync2 } from "fs";
7435
- import { dirname, join } from "path";
7436
- import { fileURLToPath } from "url";
8227
+ import { readFileSync as readFileSync3 } from "fs";
8228
+ import { dirname as dirname2, join as join4 } from "path";
8229
+ import { fileURLToPath as fileURLToPath2 } from "url";
7437
8230
  var PKG_VERSION = JSON.parse(
7438
- readFileSync2(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8")
8231
+ readFileSync3(join4(dirname2(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf-8")
7439
8232
  ).version;
7440
8233
  var CLI_VERSION_FLAGS = "-V, --cli-version";
7441
8234
  function isBareVersionRequest(args) {
7442
8235
  return args.includes("--version") && args.every((arg) => arg.startsWith("-"));
7443
8236
  }
7444
8237
  function buildProgram() {
7445
- const program = new Command25();
8238
+ const program = new Command26();
7446
8239
  program.name("exponential").description(
7447
8240
  [
7448
8241
  "CLI to interact with Exponential productivity app.",
@@ -7465,6 +8258,7 @@ function buildProgram() {
7465
8258
  program.addCommand(createPagesCommand());
7466
8259
  program.addCommand(createProductsCommand());
7467
8260
  program.addCommand(createProjectsCommand());
8261
+ program.addCommand(createRunnerCommand());
7468
8262
  program.addCommand(createSearchCommand());
7469
8263
  program.addCommand(createTicketsCommand());
7470
8264
  program.addCommand(createTimeCommand());