flightdesk 0.4.3 → 0.6.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.
Files changed (4) hide show
  1. package/README.md +48 -12
  2. package/main.js +414 -85
  3. package/main.js.map +4 -4
  4. package/package.json +1 -1
package/main.js CHANGED
@@ -958,8 +958,8 @@ var require_command = __commonJS({
958
958
  "node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js"(exports2) {
959
959
  var EventEmitter = require("node:events").EventEmitter;
960
960
  var childProcess = require("node:child_process");
961
- var path3 = require("node:path");
962
- var fs3 = require("node:fs");
961
+ var path4 = require("node:path");
962
+ var fs4 = require("node:fs");
963
963
  var process2 = require("node:process");
964
964
  var { Argument: Argument2, humanReadableArgName } = require_argument();
965
965
  var { CommanderError: CommanderError2 } = require_error();
@@ -1891,11 +1891,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1891
1891
  let launchWithNode = false;
1892
1892
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1893
1893
  function findFile(baseDir, baseName) {
1894
- const localBin = path3.resolve(baseDir, baseName);
1895
- if (fs3.existsSync(localBin)) return localBin;
1896
- if (sourceExt.includes(path3.extname(baseName))) return void 0;
1894
+ const localBin = path4.resolve(baseDir, baseName);
1895
+ if (fs4.existsSync(localBin)) return localBin;
1896
+ if (sourceExt.includes(path4.extname(baseName))) return void 0;
1897
1897
  const foundExt = sourceExt.find(
1898
- (ext) => fs3.existsSync(`${localBin}${ext}`)
1898
+ (ext) => fs4.existsSync(`${localBin}${ext}`)
1899
1899
  );
1900
1900
  if (foundExt) return `${localBin}${foundExt}`;
1901
1901
  return void 0;
@@ -1907,21 +1907,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1907
1907
  if (this._scriptPath) {
1908
1908
  let resolvedScriptPath;
1909
1909
  try {
1910
- resolvedScriptPath = fs3.realpathSync(this._scriptPath);
1910
+ resolvedScriptPath = fs4.realpathSync(this._scriptPath);
1911
1911
  } catch (err) {
1912
1912
  resolvedScriptPath = this._scriptPath;
1913
1913
  }
1914
- executableDir = path3.resolve(
1915
- path3.dirname(resolvedScriptPath),
1914
+ executableDir = path4.resolve(
1915
+ path4.dirname(resolvedScriptPath),
1916
1916
  executableDir
1917
1917
  );
1918
1918
  }
1919
1919
  if (executableDir) {
1920
1920
  let localFile = findFile(executableDir, executableFile);
1921
1921
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1922
- const legacyName = path3.basename(
1922
+ const legacyName = path4.basename(
1923
1923
  this._scriptPath,
1924
- path3.extname(this._scriptPath)
1924
+ path4.extname(this._scriptPath)
1925
1925
  );
1926
1926
  if (legacyName !== this._name) {
1927
1927
  localFile = findFile(
@@ -1932,7 +1932,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1932
1932
  }
1933
1933
  executableFile = localFile || executableFile;
1934
1934
  }
1935
- launchWithNode = sourceExt.includes(path3.extname(executableFile));
1935
+ launchWithNode = sourceExt.includes(path4.extname(executableFile));
1936
1936
  let proc;
1937
1937
  if (process2.platform !== "win32") {
1938
1938
  if (launchWithNode) {
@@ -2772,7 +2772,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2772
2772
  * @return {Command}
2773
2773
  */
2774
2774
  nameFromFilename(filename) {
2775
- this._name = path3.basename(filename, path3.extname(filename));
2775
+ this._name = path4.basename(filename, path4.extname(filename));
2776
2776
  return this;
2777
2777
  }
2778
2778
  /**
@@ -2786,9 +2786,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2786
2786
  * @param {string} [path]
2787
2787
  * @return {(string|null|Command)}
2788
2788
  */
2789
- executableDir(path4) {
2790
- if (path4 === void 0) return this._executableDir;
2791
- this._executableDir = path4;
2789
+ executableDir(path5) {
2790
+ if (path5 === void 0) return this._executableDir;
2791
+ this._executableDir = path5;
2792
2792
  return this;
2793
2793
  }
2794
2794
  /**
@@ -3018,28 +3018,45 @@ var require_commander = __commonJS({
3018
3018
  }
3019
3019
  });
3020
3020
 
3021
- // node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
3022
- var import_index = __toESM(require_commander(), 1);
3023
- var {
3024
- program,
3025
- createCommand,
3026
- createArgument,
3027
- createOption,
3028
- CommanderError,
3029
- InvalidArgumentError,
3030
- InvalidOptionArgumentError,
3031
- // deprecated old name
3032
- Command,
3033
- Argument,
3034
- Option,
3035
- Help
3036
- } = import_index.default;
3037
-
3038
3021
  // apps/cli/src/lib/config.ts
3039
3022
  var fs = __toESM(require("fs"));
3040
3023
  var path = __toESM(require("path"));
3041
3024
  var os = __toESM(require("os"));
3042
3025
  var CONFIG_FILE = path.join(os.homedir(), ".flightdeskrc");
3026
+ var FOLDER_RC = ".flightdeskrc";
3027
+ function readFolderCredential() {
3028
+ const envKey = process.env["FLIGHTDESK_API_KEY"];
3029
+ if (envKey) {
3030
+ return {
3031
+ apiKey: envKey,
3032
+ apiUrl: process.env["FLIGHTDESK_API_URL"],
3033
+ organizationId: process.env["FLIGHTDESK_ORGANIZATION_ID"],
3034
+ source: "env"
3035
+ };
3036
+ }
3037
+ let dir = process.cwd();
3038
+ const home = os.homedir();
3039
+ for (; ; ) {
3040
+ const candidate = path.join(dir, FOLDER_RC);
3041
+ if (candidate !== CONFIG_FILE && fs.existsSync(candidate)) {
3042
+ try {
3043
+ const parsed = JSON.parse(fs.readFileSync(candidate, "utf-8"));
3044
+ if (typeof parsed.apiKey === "string" && parsed.apiKey && !("organizations" in parsed))
3045
+ return { ...parsed, apiKey: parsed.apiKey, source: candidate };
3046
+ } catch {
3047
+ }
3048
+ }
3049
+ const parent = path.dirname(dir);
3050
+ if (parent === dir || dir === home) break;
3051
+ dir = parent;
3052
+ }
3053
+ return null;
3054
+ }
3055
+ var folderCredential;
3056
+ function folder() {
3057
+ if (folderCredential === void 0) folderCredential = readFolderCredential();
3058
+ return folderCredential;
3059
+ }
3043
3060
  var DEFAULT_API_URL = "https://api.flightdesk.dev";
3044
3061
  var DEV_API_URL = "http://localhost:3000";
3045
3062
  var apiUrlOverride = null;
@@ -3052,9 +3069,20 @@ function setApiUrl(url) {
3052
3069
  apiUrlOverride = url;
3053
3070
  }
3054
3071
  function getApiUrl() {
3055
- return apiUrlOverride ?? DEFAULT_API_URL;
3072
+ return apiUrlOverride ?? process.env["FLIGHTDESK_API_URL"] ?? folder()?.apiUrl ?? DEFAULT_API_URL;
3056
3073
  }
3074
+ var TOKEN_SCOPED_ORG = "token-scoped";
3057
3075
  function loadConfig() {
3076
+ const scoped = folder();
3077
+ if (scoped) {
3078
+ const id = scoped.organizationId ?? TOKEN_SCOPED_ORG;
3079
+ return {
3080
+ apiKey: scoped.apiKey,
3081
+ activeOrganization: id,
3082
+ organizations: [{ id, name: `agent (${scoped.source})` }],
3083
+ repoMapping: {}
3084
+ };
3085
+ }
3058
3086
  try {
3059
3087
  if (fs.existsSync(CONFIG_FILE)) {
3060
3088
  const content = fs.readFileSync(CONFIG_FILE, "utf-8");
@@ -3074,6 +3102,8 @@ function loadConfig() {
3074
3102
  };
3075
3103
  }
3076
3104
  function saveConfig(config) {
3105
+ if (folder())
3106
+ throw new Error("Refusing to write the home config while a folder credential is active");
3077
3107
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
3078
3108
  }
3079
3109
  function getActiveOrganization() {
@@ -3088,7 +3118,9 @@ function getActiveOrganization() {
3088
3118
  }
3089
3119
  function setActiveOrganization(orgId) {
3090
3120
  const config = loadConfig();
3091
- const org2 = config.organizations.find((o) => o.id === orgId || o.name.toLowerCase() === orgId.toLowerCase());
3121
+ const org2 = config.organizations.find(
3122
+ (o) => o.id === orgId || o.name.toLowerCase() === orgId.toLowerCase()
3123
+ );
3092
3124
  if (!org2) {
3093
3125
  throw new Error(`Organization not found: ${orgId}`);
3094
3126
  }
@@ -3137,10 +3169,10 @@ function requireActiveOrg() {
3137
3169
  return { config, org: org2 };
3138
3170
  }
3139
3171
 
3140
- // apps/cli/src/commands/init.ts
3141
- var readline = __toESM(require("readline"));
3142
-
3143
3172
  // apps/cli/src/lib/api.ts
3173
+ var fs2 = __toESM(require("node:fs"));
3174
+ var path2 = __toESM(require("node:path"));
3175
+ var import_node_crypto = require("node:crypto");
3144
3176
  var FlightDeskAPI = class _FlightDeskAPI {
3145
3177
  constructor(config, org2) {
3146
3178
  if (!config.apiKey) {
@@ -3156,6 +3188,32 @@ var FlightDeskAPI = class _FlightDeskAPI {
3156
3188
  static fromConfig(config, org2) {
3157
3189
  return new _FlightDeskAPI(config, org2);
3158
3190
  }
3191
+ /**
3192
+ * F23: transport failures (network, 5xx) are retried with backoff. GraphQL-level errors are
3193
+ * not — they are the API's answer. When `outbox` is set and every attempt fails, the call is
3194
+ * written to `<FLIGHTDESK_OUTBOX_DIR ?? cwd>/.flightdesk-outbox/<id>.json` as
3195
+ * `{ query, variables, attemptedAt }` for the runtime to flush on its next tick, and the
3196
+ * function resolves to `{ queued: true }` so an agent turn can still end.
3197
+ */
3198
+ async graphqlWithRetry(query, variables, options = {}) {
3199
+ const attempts = options.attempts ?? 3;
3200
+ let lastError;
3201
+ for (let attempt = 1; attempt <= attempts; attempt++) {
3202
+ try {
3203
+ return await this.graphql(query, variables);
3204
+ } catch (error) {
3205
+ lastError = error;
3206
+ if (!isTransportError(error)) throw error;
3207
+ if (attempt < attempts) await sleep(1e3 * 3 ** (attempt - 1));
3208
+ }
3209
+ }
3210
+ if (!options.outbox) throw lastError;
3211
+ const file = writeOutbox(query, variables);
3212
+ console.error(
3213
+ `\u26A0\uFE0F FlightDesk unreachable after ${attempts} attempts; queued to ${file} for the runtime to flush.`
3214
+ );
3215
+ return { queued: true, file };
3216
+ }
3159
3217
  async graphql(query, variables) {
3160
3218
  const verbose = process.env.FLIGHTDESK_DEBUG === "1";
3161
3219
  if (verbose) {
@@ -3167,7 +3225,7 @@ var FlightDeskAPI = class _FlightDeskAPI {
3167
3225
  method: "POST",
3168
3226
  headers: {
3169
3227
  "Content-Type": "application/json",
3170
- "Authorization": `Bearer ${this.apiKey}`
3228
+ Authorization: `Bearer ${this.apiKey}`
3171
3229
  },
3172
3230
  body: JSON.stringify({ query, variables })
3173
3231
  });
@@ -3178,7 +3236,11 @@ var FlightDeskAPI = class _FlightDeskAPI {
3178
3236
  console.log("Status:", response.status, response.statusText);
3179
3237
  console.log("Body:", body);
3180
3238
  }
3181
- throw new Error(`API request failed: ${response.status} ${response.statusText}${verbose ? "" : " (set FLIGHTDESK_DEBUG=1 for details)"}`);
3239
+ const error = new Error(
3240
+ `API request failed: ${response.status} ${response.statusText}${verbose ? "" : " (set FLIGHTDESK_DEBUG=1 for details)"}`
3241
+ );
3242
+ error.status = response.status;
3243
+ throw error;
3182
3244
  }
3183
3245
  const result = await response.json();
3184
3246
  if (verbose) {
@@ -3416,6 +3478,12 @@ var FlightDeskAPI = class _FlightDeskAPI {
3416
3478
  url
3417
3479
  primary
3418
3480
  }
3481
+ freshness
3482
+ isStale
3483
+ builtCommitSha
3484
+ builtCommitAt
3485
+ headCommitSha
3486
+ headCommitAt
3419
3487
  }
3420
3488
  }
3421
3489
  `;
@@ -3430,7 +3498,10 @@ var FlightDeskAPI = class _FlightDeskAPI {
3430
3498
  }
3431
3499
  }
3432
3500
  `;
3433
- const result = await this.graphql(query, { taskId, lines });
3501
+ const result = await this.graphql(query, {
3502
+ taskId,
3503
+ lines
3504
+ });
3434
3505
  return result.userInstanceLogs;
3435
3506
  }
3436
3507
  async restartInstance(taskId) {
@@ -3466,7 +3537,7 @@ async function fetchUserInfo(apiKey, apiUrl = "https://api.flightdesk.dev") {
3466
3537
  method: "POST",
3467
3538
  headers: {
3468
3539
  "Content-Type": "application/json",
3469
- "Authorization": `Bearer ${apiKey}`
3540
+ Authorization: `Bearer ${apiKey}`
3470
3541
  },
3471
3542
  body: JSON.stringify({
3472
3543
  query: `{
@@ -3498,8 +3569,197 @@ async function fetchUserInfo(apiKey, apiUrl = "https://api.flightdesk.dev") {
3498
3569
  }
3499
3570
  return result.data.me;
3500
3571
  }
3572
+ function isTransportError(error) {
3573
+ const status = error?.status;
3574
+ if (typeof status === "number") return status >= 500 || status === 429;
3575
+ return !(error instanceof Error && error.message.startsWith("GraphQL error"));
3576
+ }
3577
+ function sleep(ms) {
3578
+ return new Promise((resolve) => setTimeout(resolve, ms));
3579
+ }
3580
+ var lastOutboxStamp = 0;
3581
+ function outboxId() {
3582
+ lastOutboxStamp = Math.max(Date.now(), lastOutboxStamp + 1);
3583
+ const stamp = lastOutboxStamp.toString(36).padStart(10, "0");
3584
+ return `${stamp}-${(0, import_node_crypto.randomBytes)(6).toString("hex")}`;
3585
+ }
3586
+ function writeOutbox(query, variables) {
3587
+ const dir = path2.join(process.env["FLIGHTDESK_OUTBOX_DIR"] ?? process.cwd(), ".flightdesk-outbox");
3588
+ fs2.mkdirSync(dir, { recursive: true });
3589
+ const file = path2.join(dir, `${outboxId()}.json`);
3590
+ fs2.writeFileSync(
3591
+ file,
3592
+ JSON.stringify({ query, variables, attemptedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
3593
+ );
3594
+ return file;
3595
+ }
3596
+
3597
+ // apps/cli/src/commands/questions.ts
3598
+ async function execute(query, variables) {
3599
+ const { config, org: org2 } = requireActiveOrg();
3600
+ const api = FlightDeskAPI.fromConfig(config, org2);
3601
+ const outbox = query.trimStart().startsWith("mutation");
3602
+ console.log(JSON.stringify(await api.graphqlWithRetry(query, variables, { outbox }), null, 2));
3603
+ }
3604
+ function registerQuestionCommands(program3) {
3605
+ const questions = program3.command("questions").description("Ask roles, poll questions, and consume answers");
3606
+ questions.command("ask <taskId>").option("--to-role <role>", "requester, product_owner, tech_owner, client_contact, triager").option("--to-user <id>", "Address a specific human member directly").requiredOption("--key <key>", "Stable retry key").requiredOption("--body <text>", "Question").requiredOption("--context <text>", "Short answer context").option("--default <text>", "Proposed default; required for agents").option("--options <values...>", "Suggested answer choices").option("--due-at <iso>", "Optional due timestamp").option("--nonblocking", "Allow work to continue").action(
3607
+ (taskId, options) => execute("mutation($input:JSON!){userAskQuestion(input:$input)}", {
3608
+ input: {
3609
+ taskId,
3610
+ toRole: options.toRole,
3611
+ toUserId: options.toUser,
3612
+ requestKey: options.key,
3613
+ body: options.body,
3614
+ context: options.context,
3615
+ proposedDefault: options.default,
3616
+ blocking: !options.nonblocking,
3617
+ options: options.options,
3618
+ dueAt: options.dueAt
3619
+ }
3620
+ })
3621
+ );
3622
+ questions.command("list").option("--task <id>", "Task id").option("--assignee <id>", "Assignee user id").option("--status <status>", "OPEN, ANSWERED, DEFERRED, WITHDRAWN").action(
3623
+ (options) => execute("query($input:JSON!){userQuestions(input:$input)}", {
3624
+ input: { taskId: options.task, assignee: options.assignee, status: options.status }
3625
+ })
3626
+ );
3627
+ questions.command("answers <taskId>").description("Consume answers as the assigned agent; freezes answered and deferred records").action(
3628
+ (taskId) => execute("mutation($taskId:String!){userConsumeAnswers(taskId:$taskId)}", { taskId })
3629
+ );
3630
+ questions.command("withdraw <id>").action((id) => execute("mutation($id:String!){userWithdrawQuestion(id:$id)}", { id }));
3631
+ }
3632
+
3633
+ // apps/cli/src/commands/plan.ts
3634
+ var import_node_fs = require("node:fs");
3635
+ async function execute2(query, variables, attempts = 3) {
3636
+ const { config, org: org2 } = requireActiveOrg();
3637
+ const api = FlightDeskAPI.fromConfig(config, org2);
3638
+ console.log(
3639
+ JSON.stringify(
3640
+ await api.graphqlWithRetry(query, variables, { outbox: false, attempts }),
3641
+ null,
3642
+ 2
3643
+ )
3644
+ );
3645
+ }
3646
+ function registerPlanCommands(program3) {
3647
+ const plan = program3.command("plan").description("Task plans held in FlightDesk");
3648
+ plan.command("submit <taskId>").description("Submit a plan revision from a markdown file (or --body)").option("-f, --file <path>", "Markdown file holding the plan").option("--body <markdown>", "Plan body inline (prefer --file)").option("--summary <text>", "One-paragraph summary").action(async (taskId, options) => {
3649
+ const body = options.file ? (0, import_node_fs.readFileSync)(options.file, "utf8") : options.body;
3650
+ if (!body?.trim()) {
3651
+ console.error("Provide --file <path> or --body <markdown>");
3652
+ process.exit(1);
3653
+ }
3654
+ await execute2(
3655
+ "mutation($input:JSON!){userSubmitPlan(input:$input)}",
3656
+ { input: { taskId, body, summary: options.summary } },
3657
+ 1
3658
+ );
3659
+ });
3660
+ plan.command("show <taskId>").description("Print the plan revisions and approval state").action(
3661
+ (taskId) => execute2("query($taskId:String!){userTaskPlan(taskId:$taskId)}", { taskId })
3662
+ );
3663
+ plan.command("approve <taskId>").description("Approve a plan revision (latest by default); humans only").option("--revision <n>", "Revision number").action(
3664
+ (taskId, options) => execute2("mutation($input:JSON!){userApprovePlan(input:$input)}", {
3665
+ input: {
3666
+ taskId,
3667
+ revision: options.revision ? Number.parseInt(options.revision, 10) : void 0
3668
+ }
3669
+ })
3670
+ );
3671
+ }
3672
+
3673
+ // apps/cli/src/commands/dispatch.ts
3674
+ var os2 = __toESM(require("node:os"));
3675
+ async function execute3(query, variables) {
3676
+ const { config, org: org2 } = requireActiveOrg();
3677
+ const api = FlightDeskAPI.fromConfig(config, org2);
3678
+ const mutation = query.trimStart().startsWith("mutation");
3679
+ const idempotent = mutation && !query.includes("userAddTaskComment");
3680
+ const options = idempotent ? { outbox: true } : { outbox: false, attempts: mutation ? 1 : 3 };
3681
+ console.log(JSON.stringify(await api.graphqlWithRetry(query, variables, options), null, 2));
3682
+ }
3683
+ function registerDispatchCommands(program3) {
3684
+ const dispatch = program3.command("dispatch").description("Qalatra dispatch polling and reporting");
3685
+ dispatch.command("list").option("--since <iso>", "Only requests updated after this time").action(
3686
+ (options) => execute3("query($since: String) { userDispatchRequests(since: $since) }", {
3687
+ since: options.since
3688
+ })
3689
+ );
3690
+ dispatch.command("poll").description("Envelope form: { requests, serverTime, pollAfterSeconds }").option("--since <iso>", "Only requests updated after this time").action(
3691
+ (options) => execute3("query($since: String) { userDispatchPoll(since: $since) }", {
3692
+ since: options.since
3693
+ })
3694
+ );
3695
+ dispatch.command("update <id>").requiredOption("--status <status>", "ACKNOWLEDGED, RUNNING, DONE, or FAILED").option("--job <id>", "Runtime job id").option("--result-tail <text>", "Last \u22648 KB of the job output (DONE/FAILED)").option("--result-length <n>", "Full output length in characters").option("--diagnostics-kind <kind>", "error|timed_out|orphaned|launch_failed|dependency_down").option("--diagnostics-text <text>", "Sanitised diagnostics (\u22644 KB)").option("--resumable", "A session id was captured; the turn can be resumed").action(
3696
+ (id, options) => execute3("mutation($input: JSON!) { userUpdateDispatch(input: $input) }", {
3697
+ input: {
3698
+ id,
3699
+ status: options.status,
3700
+ agentJobId: options.job,
3701
+ resultTail: options.resultTail,
3702
+ resultLength: options.resultLength ? Number(options.resultLength) : void 0,
3703
+ diagnostics: options.diagnosticsKind ? { kind: options.diagnosticsKind, text: options.diagnosticsText ?? "" } : void 0,
3704
+ resumable: options.resumable
3705
+ }
3706
+ })
3707
+ );
3708
+ dispatch.command("request <taskId>").requiredOption("--agent <id>", "Agent user id").option("--kind <kind>", "PLAN, EXECUTE, ANSWER, CUSTOM", "EXECUTE").option("--prompt <text>", "Request context").action(
3709
+ (taskId, options) => execute3("mutation($input: JSON!) { userRequestDispatch(input: $input) }", {
3710
+ input: { taskId, agentUserId: options.agent, kind: options.kind, prompt: options.prompt }
3711
+ })
3712
+ );
3713
+ program3.command("comment <taskId>").requiredOption("--body <text>", "Internal comment").option("--ask-agent", "Ask the assigned agent; requires human owner/admin").action(
3714
+ (taskId, options) => execute3("mutation($input: JSON!) { userAddTaskComment(input: $input) }", {
3715
+ input: { taskId, body: options.body, askAgent: options.askAgent ?? false }
3716
+ })
3717
+ );
3718
+ program3.command("agent-bind").requiredOption("--path <path>", "Agent folder on the runtime box").option("--node <url>", "Runtime node URL (unused under pull-only; recorded for reference)").option("--box <id>", "Box identifier (defaults to the runtime hostname)").action((options) => {
3719
+ const { org: org2 } = requireActiveOrg();
3720
+ return execute3("mutation($input: JSON!) { userBindAgent(input: $input) }", {
3721
+ input: {
3722
+ // A folder credential without organizationId relies on the token's organization.
3723
+ organizationId: org2.id === TOKEN_SCOPED_ORG ? void 0 : org2.id,
3724
+ agentPath: options.path,
3725
+ agentNodeUrl: options.node,
3726
+ boxId: options.box ?? os2.hostname()
3727
+ }
3728
+ });
3729
+ });
3730
+ const turn = program3.command("turn").description("Report the outcome of an agent turn");
3731
+ turn.command("end").requiredOption("--dispatch <id>", "Dispatch id ($FLIGHTDESK_DISPATCH_ID)").requiredOption("--outcome <outcome>", "done | waiting | failed | blocked").option("--waiting-on <what>", "session | ci | review | qa | merge | deploy | dependency").option("--ref <url>", "PR URL or other reference").option("--summary <text>", "One-paragraph summary").action(
3732
+ (options) => execute3("mutation($input: JSON!) { userEndTurn(input: $input) }", {
3733
+ input: {
3734
+ dispatchId: options.dispatch,
3735
+ outcome: options.outcome,
3736
+ waitingOn: options.waitingOn,
3737
+ ref: options.ref,
3738
+ summary: options.summary
3739
+ }
3740
+ })
3741
+ );
3742
+ }
3743
+
3744
+ // node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
3745
+ var import_index = __toESM(require_commander(), 1);
3746
+ var {
3747
+ program,
3748
+ createCommand,
3749
+ createArgument,
3750
+ createOption,
3751
+ CommanderError,
3752
+ InvalidArgumentError,
3753
+ InvalidOptionArgumentError,
3754
+ // deprecated old name
3755
+ Command,
3756
+ Argument,
3757
+ Option,
3758
+ Help
3759
+ } = import_index.default;
3501
3760
 
3502
3761
  // apps/cli/src/commands/init.ts
3762
+ var readline = __toESM(require("readline"));
3503
3763
  function question(rl, prompt) {
3504
3764
  return new Promise((resolve) => {
3505
3765
  rl.question(prompt, (answer) => {
@@ -3563,8 +3823,11 @@ async function initCommand() {
3563
3823
  organizations.forEach((org2, i) => {
3564
3824
  console.log(` ${i + 1}. ${org2.name}`);
3565
3825
  });
3566
- const choice = await question(rl, `
3567
- Select active organization (1-${organizations.length}): `);
3826
+ const choice = await question(
3827
+ rl,
3828
+ `
3829
+ Select active organization (1-${organizations.length}): `
3830
+ );
3568
3831
  const choiceIndex = Number.parseInt(choice, 10) - 1;
3569
3832
  if (choiceIndex < 0 || choiceIndex >= organizations.length) {
3570
3833
  console.error("Invalid selection, using first organization");
@@ -3575,7 +3838,6 @@ Select active organization (1-${organizations.length}): `);
3575
3838
  }
3576
3839
  const config = loadConfig();
3577
3840
  config.apiKey = apiKey;
3578
- config.apiUrl = apiUrl;
3579
3841
  config.organizations = organizations;
3580
3842
  config.activeOrganization = activeOrganization;
3581
3843
  saveConfig(config);
@@ -3640,7 +3902,7 @@ function parseGitRemoteUrl(remoteUrl) {
3640
3902
 
3641
3903
  // apps/cli/src/commands/register.ts
3642
3904
  var import_node_child_process2 = require("node:child_process");
3643
- var import_node_fs = require("node:fs");
3905
+ var import_node_fs2 = require("node:fs");
3644
3906
  var import_node_os = require("node:os");
3645
3907
  var import_node_path = require("node:path");
3646
3908
  async function registerCommand(taskId, options) {
@@ -3676,19 +3938,29 @@ async function registerCommand(taskId, options) {
3676
3938
  const task2 = await api.createTask({
3677
3939
  projectId,
3678
3940
  title: options.title,
3679
- description: options.description,
3680
- subprojectId: options.subproject
3941
+ // Launch-first orchestrators do not know the FlightDesk UUID when the
3942
+ // cloud prompt is sent. Persisting that same prompt on the task lets
3943
+ // the API derive its durable Linear/Notion source identity instead.
3944
+ description: options.description ?? options.prompt,
3945
+ subprojectId: options.subproject,
3946
+ sourceSystem: options.sourceSystem,
3947
+ sourceRef: options.sourceRef,
3948
+ sourceUrl: options.sourceUrl
3681
3949
  });
3682
3950
  actualTaskId = task2.id;
3683
3951
  console.log(`\u2705 Created task: ${task2.id}`);
3684
3952
  }
3685
3953
  if (viewUrl || teleportId) {
3686
3954
  console.log("Registering session with task...");
3687
- await api.updateTask(actualTaskId, {
3688
- sessionViewUrl: viewUrl,
3689
- sessionTeleportId: teleportId,
3690
- status: "DISPATCHED"
3691
- });
3955
+ if (viewUrl && teleportId) {
3956
+ await api.registerSession(actualTaskId, { viewUrl, teleportId });
3957
+ } else {
3958
+ await api.updateTask(actualTaskId, {
3959
+ sessionViewUrl: viewUrl,
3960
+ sessionTeleportId: teleportId,
3961
+ status: "DISPATCHED"
3962
+ });
3963
+ }
3692
3964
  console.log("\u2705 Session registered");
3693
3965
  }
3694
3966
  console.log(`
@@ -3705,7 +3977,7 @@ Task ID: ${actualTaskId}`);
3705
3977
  }
3706
3978
  }
3707
3979
  function spawnClaudeRemote(prompt) {
3708
- const tmpDir = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "fd-claude-"));
3980
+ const tmpDir = (0, import_node_fs2.mkdtempSync)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "fd-claude-"));
3709
3981
  const tmpFile = (0, import_node_path.join)(tmpDir, "output.txt");
3710
3982
  const isMac = process.platform === "darwin";
3711
3983
  const escaped = prompt.replaceAll("'", `'"'"'`);
@@ -3719,14 +3991,18 @@ function spawnClaudeRemote(prompt) {
3719
3991
  try {
3720
3992
  let raw = "";
3721
3993
  try {
3722
- raw = (0, import_node_fs.readFileSync)(tmpFile, "utf8");
3994
+ raw = (0, import_node_fs2.readFileSync)(tmpFile, "utf8");
3723
3995
  } catch {
3724
3996
  }
3725
3997
  try {
3726
- (0, import_node_fs.rmSync)(tmpDir, { recursive: true });
3998
+ (0, import_node_fs2.rmSync)(tmpDir, { recursive: true });
3727
3999
  } catch {
3728
4000
  }
3729
- const output = raw.replace(/\x1b\[[0-9;]*[mGKHFJABCDr]/g, "").replace(/\r/g, "");
4001
+ const ansiEscape = new RegExp(
4002
+ String.raw`${String.fromCodePoint(27)}\[[0-9;]*[mGKHFJABCDr]`,
4003
+ "g"
4004
+ );
4005
+ const output = raw.replace(ansiEscape, "").replace(/\r/g, "");
3730
4006
  if (code !== 0) {
3731
4007
  reject(new Error(`claude exited with code ${code}
3732
4008
  Output: ${output.slice(0, 500)}`));
@@ -3794,14 +4070,20 @@ async function autoDetectProject(api, activeOrg) {
3794
4070
  if (!mappedOrg) {
3795
4071
  console.error(`\u274C Repository "${repoInfo.remote}" is not mapped to any organization.`);
3796
4072
  console.error(" Run: flightdesk sync");
3797
- console.error(" Or provide a project ID: flightdesk register --project <project-id> [task-id]");
4073
+ console.error(
4074
+ " Or provide a project ID: flightdesk register --project <project-id> [task-id]"
4075
+ );
3798
4076
  process.exit(1);
3799
4077
  }
3800
4078
  if (mappedOrg.id !== activeOrg.id) {
3801
- console.error(`\u274C Repository "${repoInfo.remote}" is mapped to organization "${mappedOrg.name}",`);
4079
+ console.error(
4080
+ `\u274C Repository "${repoInfo.remote}" is mapped to organization "${mappedOrg.name}",`
4081
+ );
3802
4082
  console.error(` but your active organization is "${activeOrg.name}".`);
3803
4083
  console.error(" Switch with: flightdesk org switch " + mappedOrg.name);
3804
- console.error(" Or provide a project ID: flightdesk register --project <project-id> [task-id]");
4084
+ console.error(
4085
+ " Or provide a project ID: flightdesk register --project <project-id> [task-id]"
4086
+ );
3805
4087
  process.exit(1);
3806
4088
  }
3807
4089
  const projects = await api.listProjects();
@@ -3949,7 +4231,10 @@ async function handleCreate(api, options) {
3949
4231
  projectId: options.project,
3950
4232
  title: options.title,
3951
4233
  description: options.description,
3952
- subprojectId: options.subproject
4234
+ subprojectId: options.subproject,
4235
+ sourceSystem: options.sourceSystem,
4236
+ sourceRef: options.sourceRef,
4237
+ sourceUrl: options.sourceUrl
3953
4238
  });
3954
4239
  console.log(`
3955
4240
  \u2705 Task created`);
@@ -3989,7 +4274,9 @@ async function handleStatus(api, options) {
3989
4274
  console.log(`ID: ${task2.id}`);
3990
4275
  console.log(`Status: ${getStatusEmoji2(task2.status)} ${task2.status}`);
3991
4276
  if (task2.project) {
3992
- console.log(`Project: ${task2.project.name}${task2.project.githubRepo ? ` (${task2.project.githubRepo})` : ""}`);
4277
+ console.log(
4278
+ `Project: ${task2.project.name}${task2.project.githubRepo ? ` (${task2.project.githubRepo})` : ""}`
4279
+ );
3993
4280
  }
3994
4281
  if (task2.description) {
3995
4282
  console.log(`
@@ -4023,18 +4310,13 @@ async function handleUpdate(api, options) {
4023
4310
  input.prUrl = options.prUrl;
4024
4311
  const prNumberMatch = options.prUrl.match(/\/pull\/(\d+)/);
4025
4312
  if (prNumberMatch) {
4026
- input.prNumber = parseInt(prNumberMatch[1], 10);
4313
+ input.prNumber = Number.parseInt(prNumberMatch[1], 10);
4027
4314
  }
4028
4315
  }
4029
4316
  if (options.prNumber) {
4030
- input.prNumber = parseInt(options.prNumber, 10);
4317
+ input.prNumber = Number.parseInt(options.prNumber, 10);
4031
4318
  }
4032
- if (options.session) {
4033
- const sessionId = parseSessionId(options.session);
4034
- input.sessionViewUrl = `https://claude.ai/code/${sessionId}`;
4035
- input.sessionTeleportId = sessionId;
4036
- }
4037
- if (Object.keys(input).length === 0) {
4319
+ if (Object.keys(input).length === 0 && !options.session) {
4038
4320
  console.error("No updates specified. Use --status, --branch, --pr-url, or --session");
4039
4321
  process.exit(1);
4040
4322
  }
@@ -4046,7 +4328,18 @@ async function handleUpdate(api, options) {
4046
4328
  }
4047
4329
  }
4048
4330
  console.log(`Updating task ${options.taskId}...`);
4049
- const task2 = await api.updateTask(options.taskId, input);
4331
+ let task2;
4332
+ if (options.session) {
4333
+ const sessionId = parseSessionId(options.session);
4334
+ task2 = await api.registerSession(options.taskId, {
4335
+ viewUrl: `https://claude.ai/code/${sessionId}`,
4336
+ teleportId: sessionId
4337
+ });
4338
+ }
4339
+ if (Object.keys(input).length > 0) {
4340
+ task2 = await api.updateTask(options.taskId, input);
4341
+ }
4342
+ if (!task2) return;
4050
4343
  console.log(`
4051
4344
  \u2705 Task updated`);
4052
4345
  console.log(` Status: ${task2.status}`);
@@ -4407,11 +4700,39 @@ async function listProjects(api) {
4407
4700
  ${projects.length} project(s)`);
4408
4701
  }
4409
4702
 
4703
+ // apps/cli/src/lib/preview-freshness.ts
4704
+ var SHORT_SHA_LENGTH = 7;
4705
+ var UNKNOWN_SHA_LABEL = "unknown";
4706
+ function short(value) {
4707
+ if (typeof value !== "string") return UNKNOWN_SHA_LABEL;
4708
+ const trimmed = value.trim();
4709
+ return trimmed ? trimmed.slice(0, SHORT_SHA_LENGTH) : UNKNOWN_SHA_LABEL;
4710
+ }
4711
+ var UNKNOWN_LINES = [
4712
+ " \u2754 Freshness unknown \u2014 this preview cannot be confirmed to contain the latest commits"
4713
+ ];
4714
+ function formatFreshnessLines(view) {
4715
+ switch (view.freshness) {
4716
+ case "FRESH":
4717
+ return [` \u2705 Up to date with ${short(view.builtCommitSha)}`];
4718
+ case "STALE":
4719
+ return [
4720
+ " \u26A0\uFE0F STALE \u2014 this preview does NOT contain the latest commits",
4721
+ ` Built from: ${short(view.builtCommitSha)}`,
4722
+ ` Branch head: ${short(view.headCommitSha)}`
4723
+ ];
4724
+ // UNKNOWN and anything unrecognised. A future server-side enum value
4725
+ // degrades to "unknown" rather than to silence.
4726
+ default:
4727
+ return UNKNOWN_LINES;
4728
+ }
4729
+ }
4730
+
4410
4731
  // apps/cli/src/commands/preview.ts
4411
4732
  var import_node_child_process3 = require("node:child_process");
4412
- var path2 = __toESM(require("node:path"));
4413
- var os2 = __toESM(require("node:os"));
4414
- var fs2 = __toESM(require("node:fs"));
4733
+ var path3 = __toESM(require("node:path"));
4734
+ var os3 = __toESM(require("node:os"));
4735
+ var fs3 = __toESM(require("node:fs"));
4415
4736
  function isValidContainerId(id) {
4416
4737
  return /^[a-fA-F0-9]+$/.test(id) && id.length >= 12 && id.length <= 64;
4417
4738
  }
@@ -4494,6 +4815,9 @@ async function handleStatus2(api, options) {
4494
4815
  } else {
4495
4816
  console.log(` Preview URL: ${instance.previewUrl}`);
4496
4817
  }
4818
+ for (const line of formatFreshnessLines(instance)) {
4819
+ console.log(line);
4820
+ }
4497
4821
  console.log(` SSH: ${instance.sshConnectionString}`);
4498
4822
  console.log(` Container: ${instance.containerId.substring(0, 12)}`);
4499
4823
  if (instance.lastActivityAt) {
@@ -4577,10 +4901,10 @@ async function handleMount(api, options) {
4577
4901
  process.exit(1);
4578
4902
  }
4579
4903
  const rawTaskIdPrefix = options.taskId.substring(0, 8);
4580
- const safeTaskId = path2.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
4581
- const mountDir = options.directory || path2.join(os2.homedir(), "flightdesk-mounts", safeTaskId);
4582
- if (!fs2.existsSync(mountDir)) {
4583
- fs2.mkdirSync(mountDir, { recursive: true });
4904
+ const safeTaskId = path3.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
4905
+ const mountDir = options.directory || path3.join(os3.homedir(), "flightdesk-mounts", safeTaskId);
4906
+ if (!fs3.existsSync(mountDir)) {
4907
+ fs3.mkdirSync(mountDir, { recursive: true });
4584
4908
  }
4585
4909
  try {
4586
4910
  const mounted = (0, import_node_child_process3.execSync)("mount", { encoding: "utf8" });
@@ -4633,9 +4957,9 @@ async function handleMount(api, options) {
4633
4957
  }
4634
4958
  async function handleUnmount(_api, options) {
4635
4959
  const rawTaskIdPrefix = options.taskId.substring(0, 8);
4636
- const safeTaskId = path2.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
4637
- const mountDir = path2.join(os2.homedir(), "flightdesk-mounts", safeTaskId);
4638
- if (!fs2.existsSync(mountDir)) {
4960
+ const safeTaskId = path3.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
4961
+ const mountDir = path3.join(os3.homedir(), "flightdesk-mounts", safeTaskId);
4962
+ if (!fs3.existsSync(mountDir)) {
4639
4963
  console.log("Mount directory does not exist");
4640
4964
  return;
4641
4965
  }
@@ -4652,7 +4976,7 @@ async function handleUnmount(_api, options) {
4652
4976
  }
4653
4977
  console.log("\u2705 Unmounted successfully");
4654
4978
  try {
4655
- fs2.rmSync(mountDir, { recursive: true, force: true });
4979
+ fs3.rmSync(mountDir, { recursive: true, force: true });
4656
4980
  } catch {
4657
4981
  }
4658
4982
  } catch (error) {
@@ -4685,7 +5009,10 @@ async function handleTeardown(api, options) {
4685
5009
 
4686
5010
  // apps/cli/src/main.ts
4687
5011
  var program2 = new Command();
4688
- program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.4.3").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
5012
+ registerDispatchCommands(program2);
5013
+ registerQuestionCommands(program2);
5014
+ registerPlanCommands(program2);
5015
+ program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.6.0").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
4689
5016
  program2.hook("preAction", () => {
4690
5017
  const opts = program2.opts();
4691
5018
  if (opts.api) {
@@ -4698,11 +5025,13 @@ program2.hook("preAction", () => {
4698
5025
  }
4699
5026
  });
4700
5027
  program2.command("init").description("Configure FlightDesk CLI with your API credentials").action(initCommand);
4701
- program2.command("register [task-id]").description("Register a Claude Code session with a FlightDesk task (auto-detects project from git repo)").option("-p, --project <id>", "Project ID (auto-detected from git repo if not provided)").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--view-url <url>", "Claude Code session view URL").option("--teleport-id <id>", "Claude Code teleport ID").option("--title <title>", "Task title (creates new task if task-id not provided)").option("--description <description>", "Task description").option("--prompt <prompt>", "Prompt to pass to claude --remote (avoids pipe detection issues)").action(registerCommand);
5028
+ program2.command("register [task-id]").description(
5029
+ "Register a Claude Code session with a FlightDesk task (auto-detects project from git repo)"
5030
+ ).option("-p, --project <id>", "Project ID (auto-detected from git repo if not provided)").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--view-url <url>", "Claude Code session view URL").option("--teleport-id <id>", "Claude Code teleport ID").option("--title <title>", "Task title (creates new task if task-id not provided)").option("--description <description>", "Task description").option("--prompt <prompt>", "Prompt to pass to claude --remote (avoids pipe detection issues)").option("--source-system <system>", "External source system (for example notion or linear)").option("--source-ref <ref>", "External source reference or page ID").option("--source-url <url>", "External source URL used for deterministic branch binding").action(registerCommand);
4702
5031
  var project = program2.command("project").description("Project management commands");
4703
5032
  project.command("list").description("List projects in the active organization").action(() => projectCommand("list", {}));
4704
5033
  var task = program2.command("task").description("Task management commands");
4705
- task.command("create").description("Create a new task").requiredOption("-p, --project <id>", "Project ID").requiredOption("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").action((options) => taskCommand("create", options));
5034
+ task.command("create").description("Create a new task").requiredOption("-p, --project <id>", "Project ID").requiredOption("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("--subproject <id>", "Subproject ID (required if project has subprojects configured)").option("--source-system <system>", "External source system (for example notion or linear)").option("--source-ref <ref>", "External source reference or page ID").option("--source-url <url>", "External source URL used for deterministic branch binding").action((options) => taskCommand("create", options));
4706
5035
  task.command("list").description("List tasks").option("-p, --project <id>", "Filter by project ID").option("--status <status>", "Filter by status").action((options) => taskCommand("list", options));
4707
5036
  task.command("status <task-id>").description("Get task status").action((taskId) => taskCommand("status", { taskId }));
4708
5037
  task.command("update <task-id>").description("Update task").option("-s, --status <status>", "New status").option("--branch <branch>", "Branch name").option("--pr-url <url>", "Pull request URL").option("--pr-number <number>", "Pull request number (use if --pr-url is not enough)").option("--session <session>", "Claude Code session (URL or session ID)").action((taskId, options) => taskCommand("update", { taskId, ...options }));