@moxt-ai/cli 0.3.3-moxt-run.1 → 0.3.3

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
@@ -149,7 +149,7 @@ function getApiKeyOrUndefined() {
149
149
 
150
150
  // src/utils/http.ts
151
151
  function getUserAgent() {
152
- return `moxt-cli/${"0.3.3-moxt-run.1"} (${platform()}/${arch()}) node/${process.versions.node}`;
152
+ return `moxt-cli/${"0.3.3"} (${platform()}/${arch()}) node/${process.versions.node}`;
153
153
  }
154
154
  async function fetchApi(method, path2, body) {
155
155
  const apiKey = getApiKey();
@@ -189,6 +189,9 @@ async function httpPut(path2, body) {
189
189
  async function httpPost(path2, body) {
190
190
  return request("POST", path2, body);
191
191
  }
192
+ async function httpPatch(path2, body) {
193
+ return request("PATCH", path2, body);
194
+ }
192
195
  async function httpDelete(path2, body) {
193
196
  return request("DELETE", path2, body);
194
197
  }
@@ -744,2334 +747,1147 @@ function registerMiniappCommand(program2) {
744
747
  });
745
748
  }
746
749
 
747
- // src/run/run-agent.ts
748
- import { spawn } from "child_process";
749
- import { closeSync as closeSync2, fstatSync, openSync as openSync2, readSync as readSync2 } from "fs";
750
- import { homedir as homedir2 } from "os";
751
-
752
- // src/run/capture.ts
753
- import { randomUUID } from "crypto";
754
- import { access, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
755
- import { homedir } from "os";
756
- import { join, posix, win32 } from "path";
757
- var DATA_FILE_TARGET_BYTES = 1024 * 1024;
758
- var RUNLOG_DIRECTORY_MARKER = ".runlog";
759
- var RUNLOG_DIRECTORY_MARKER_SCHEMA = "runlog.directory.v1";
760
- var RUNLOG_MANAGED_OUTPUT_NAMES = /* @__PURE__ */ new Set([
761
- ".DS_Store",
762
- RUNLOG_DIRECTORY_MARKER,
763
- "events",
764
- "events.jsonl",
765
- "run.json",
766
- "state.json",
767
- "transcript",
768
- "transcript.jsonl",
769
- "upload.json"
770
- ]);
771
- var RUNLOG_METADATA_SCHEMAS = {
772
- "run.json": "runlog.capture.v1",
773
- "state.json": "runlog.state.v1",
774
- "upload.json": "runlog.upload.v1"
775
- };
776
- var RUNLOG_METADATA_FILE_NAMES = Object.keys(RUNLOG_METADATA_SCHEMAS);
777
- var CaptureOutputPrepareError = class extends Error {
778
- path;
779
- constructor(path2, cause) {
780
- super(errorMessage(cause));
781
- this.name = "CaptureOutputPrepareError";
782
- this.path = path2;
783
- this.cause = cause;
784
- }
785
- };
786
- function defaultCaptureOutputRoot(home = homedir(), env = process.env, nodePlatform = process.platform) {
787
- const pathJoin = nodePlatform === "win32" ? win32.join : posix.join;
788
- if (nodePlatform === "darwin") {
789
- return pathJoin(home, "Library", "Application Support", "moxt", "runs");
790
- }
791
- if (nodePlatform === "win32") {
792
- return pathJoin(env.LOCALAPPDATA ?? pathJoin(home, "AppData", "Local"), "Moxt", "runs");
750
+ // src/telemetry/config.ts
751
+ import * as fs2 from "fs";
752
+ import * as os from "os";
753
+ import * as path from "path";
754
+ function getConfigPath() {
755
+ return path.join(os.homedir(), ".moxt", "config.json");
756
+ }
757
+ function readTelemetryConfig() {
758
+ try {
759
+ const raw = fs2.readFileSync(getConfigPath(), "utf8");
760
+ const parsed = JSON.parse(raw);
761
+ return parsed.telemetry ?? {};
762
+ } catch {
763
+ return {};
793
764
  }
794
- return pathJoin(env.XDG_DATA_HOME ?? pathJoin(home, ".local", "share"), "moxt", "runs");
795
765
  }
796
- function withDefaultCaptureOutput(env, home = homedir()) {
797
- const { outputDir, outputRoot } = captureEnvPaths(env);
798
- if (hasValue(outputDir) || hasValue(outputRoot)) {
799
- return env;
766
+ function writeTelemetryConfig(update) {
767
+ const configPath = getConfigPath();
768
+ let existing = {};
769
+ try {
770
+ existing = JSON.parse(fs2.readFileSync(configPath, "utf8"));
771
+ } catch {
772
+ existing = {};
800
773
  }
801
- return {
802
- ...env,
803
- MOXT_RUN_OUTPUT_ROOT: defaultCaptureOutputRoot(home, env)
804
- };
774
+ const currentTelemetry = existing.telemetry ?? {};
775
+ const next = { ...existing, telemetry: { ...currentTelemetry, ...update } };
776
+ fs2.mkdirSync(path.dirname(configPath), { recursive: true });
777
+ fs2.writeFileSync(configPath, JSON.stringify(next, null, 2));
778
+ }
779
+
780
+ // src/telemetry/opt-out.ts
781
+ function isCiEnvironment() {
782
+ return process.env.CI === "true" || process.env.CI === "1";
805
783
  }
806
- async function prepareCaptureOutputs(env, runId) {
807
- const { outputDir, outputRoot } = captureEnvPaths(env);
808
- if (hasValue(outputDir)) {
809
- await prepareOutputPath(outputDir, () => prepareCaptureDirectory(outputDir));
784
+ function isTelemetryDisabled() {
785
+ if (process.env.MOXT_TELEMETRY_DISABLED === "1" || process.env.MOXT_TELEMETRY_DISABLED === "true") {
786
+ return true;
810
787
  }
811
- if (hasValue(outputRoot)) {
812
- const runDir = join(outputRoot, runId);
813
- await prepareOutputPath(runDir, () => prepareCaptureDirectory(runDir));
788
+ if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true") {
789
+ return true;
814
790
  }
791
+ return readTelemetryConfig().disabled === true;
792
+ }
793
+
794
+ // src/commands/telemetry.ts
795
+ function registerTelemetryCommand(program2) {
796
+ const telemetry = program2.command("telemetry").description("Manage Moxt CLI telemetry");
797
+ telemetry.command("status").description("Show telemetry status").action(() => {
798
+ const cfg = readTelemetryConfig();
799
+ const envOverride = process.env.MOXT_TELEMETRY_DISABLED === "1" || process.env.MOXT_TELEMETRY_DISABLED === "true" || process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true";
800
+ const state = isTelemetryDisabled() ? "disabled" : "enabled";
801
+ print(`telemetry: ${state}`);
802
+ if (envOverride) {
803
+ print("(disabled via environment variable)");
804
+ } else if (cfg.disabled) {
805
+ print("(disabled via config file)");
806
+ }
807
+ });
808
+ telemetry.command("enable").description("Enable telemetry").action(() => {
809
+ writeTelemetryConfig({ disabled: false });
810
+ print("telemetry: enabled");
811
+ });
812
+ telemetry.command("disable").description("Disable telemetry").action(() => {
813
+ writeTelemetryConfig({ disabled: true });
814
+ print("telemetry: disabled");
815
+ });
816
+ }
817
+
818
+ // src/commands/whoami.ts
819
+ function registerWhoamiCommand(program2) {
820
+ program2.command("whoami").description("Show current user information").action(async () => {
821
+ startSpinner("Fetching user info...");
822
+ const response = await httpGet("/users/whoami");
823
+ if (response.ok) {
824
+ stopSpinner(true, "Done");
825
+ print(`${colors.bold}Nickname${colors.reset}: ${response.data.nickname}`);
826
+ print(`${colors.bold}Email${colors.reset}: ${response.data.email}`);
827
+ } else {
828
+ stopSpinner(false, "Failed");
829
+ printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
830
+ process.exit(1);
831
+ }
832
+ });
833
+ }
834
+
835
+ // src/commands/workspace.ts
836
+ var emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
837
+ function isValidEmail(email) {
838
+ return emailRegex.test(email);
839
+ }
840
+ function registerWorkspaceCommand(program2) {
841
+ const workspace = program2.command("workspace").description("Workspace management");
842
+ workspace.command("list").description("List all workspaces you belong to").action(async () => {
843
+ startSpinner("Fetching workspaces...");
844
+ const response = await httpGet("/workspaces");
845
+ if (response.ok) {
846
+ const { workspaces } = response.data;
847
+ if (workspaces.length === 0) {
848
+ stopSpinner(true, "No workspaces found.");
849
+ return;
850
+ }
851
+ stopSpinner(true, `Found ${workspaces.length} workspace(s)`);
852
+ for (const ws of workspaces) {
853
+ print(`${colors.bold}${ws.workspaceId}${colors.reset} ${ws.name}`);
854
+ }
855
+ } else {
856
+ stopSpinner(false, "Failed");
857
+ printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
858
+ process.exit(1);
859
+ }
860
+ });
861
+ const members = program2.command("members").description("Manage workspace members");
862
+ members.command("list").description("List all members in a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").action(async (options) => {
863
+ const { workspace: workspaceId } = options;
864
+ startSpinner("Fetching workspace members...");
865
+ const response = await httpGet(`/workspaces/${workspaceId}/members`);
866
+ if (!response.ok) {
867
+ stopSpinner(false, "Failed to fetch members");
868
+ printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
869
+ process.exit(1);
870
+ }
871
+ const { members: members2 } = response.data;
872
+ if (members2.length === 0) {
873
+ stopSpinner(true, "No members found.");
874
+ return;
875
+ }
876
+ stopSpinner(true, `Found ${members2.length} member(s)`);
877
+ for (const member of members2) {
878
+ const name = member.displayName || "-";
879
+ print(`${colors.bold}${member.email}${colors.reset} ${name} ${member.role}`);
880
+ }
881
+ });
882
+ members.command("remove").description("Remove members from a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").argument("<emails>", "Emails to remove (comma-separated)").action(async (emailsArg, options) => {
883
+ const { workspace: workspaceId } = options;
884
+ const emails = emailsArg.split(",").map((e) => e.trim()).filter(Boolean);
885
+ if (emails.length === 0) {
886
+ printError("No valid emails provided");
887
+ process.exit(1);
888
+ }
889
+ const invalidEmails = emails.filter((e) => !isValidEmail(e));
890
+ if (invalidEmails.length > 0) {
891
+ for (const email of invalidEmails) {
892
+ printError(`Invalid email format: ${email}`);
893
+ }
894
+ process.exit(1);
895
+ }
896
+ startSpinner("Fetching workspace members...");
897
+ const membersResponse = await httpGet(`/workspaces/${workspaceId}/members`);
898
+ if (!membersResponse.ok) {
899
+ stopSpinner(false, "Failed to fetch members");
900
+ printError(`Error [${membersResponse.status}]: ${JSON.stringify(membersResponse.data)}`);
901
+ process.exit(1);
902
+ }
903
+ stopSpinner(true, "Members fetched");
904
+ const { members: members2 } = membersResponse.data;
905
+ const emailToMember = new Map(members2.map((m) => [m.email.toLowerCase(), m]));
906
+ for (const email of emails) {
907
+ const member = emailToMember.get(email.toLowerCase());
908
+ if (!member) {
909
+ print(`${colors.red}\u2717${colors.reset} ${email} - not found in workspace`);
910
+ continue;
911
+ }
912
+ const deleteResponse = await httpDelete(`/workspaces/${workspaceId}/members/${member.userId}`);
913
+ if (deleteResponse.ok) {
914
+ print(`${colors.green}\u2713${colors.reset} ${email} - removed`);
915
+ } else {
916
+ print(`${colors.red}\u2717${colors.reset} ${email} - failed: ${JSON.stringify(deleteResponse.data)}`);
917
+ }
918
+ }
919
+ });
920
+ const space = program2.command("space").description("Manage spaces");
921
+ space.command("list").description("List accessible spaces in a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").action(async (options) => {
922
+ const { workspace: workspaceId } = options;
923
+ startSpinner("Fetching spaces...");
924
+ const response = await httpGet(`/workspaces/${workspaceId}/spaces`);
925
+ if (!response.ok) {
926
+ stopSpinner(false, "Failed to fetch spaces");
927
+ printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
928
+ process.exit(1);
929
+ }
930
+ const { spaces } = response.data;
931
+ if (spaces.length === 0) {
932
+ stopSpinner(true, "No spaces found.");
933
+ return;
934
+ }
935
+ stopSpinner(true, `Found ${spaces.length} space(s)`);
936
+ print(
937
+ `${colors.bold}TYPE${colors.reset} ${colors.bold}TEAM_SPACE_ID${colors.reset} ${colors.bold}TEAMMATE_ID${colors.reset} ${colors.bold}NAME${colors.reset}`
938
+ );
939
+ for (const s of spaces) {
940
+ const teamSpaceId = s.teamSpaceId ?? "";
941
+ const teammateId = s.agentId != null ? String(s.agentId) : "";
942
+ print(`${s.type} ${teamSpaceId} ${teammateId} ${s.name}`);
943
+ }
944
+ });
815
945
  }
816
- function captureOutputDirectories(env, runId) {
817
- const { outputDir, outputRoot } = captureEnvPaths(env);
818
- const dirs = /* @__PURE__ */ new Set();
819
- if (hasValue(outputDir)) {
820
- dirs.add(outputDir);
946
+
947
+ // src/commands/workflow.ts
948
+ import * as fs3 from "fs";
949
+ import { TextDecoder } from "util";
950
+ var MAX_DESC_LENGTH = 5e3;
951
+ var MAX_COMMENT_CONTENT_LENGTH = 5e3;
952
+ var MAX_COMMENT_CONTENT_BYTES = MAX_COMMENT_CONTENT_LENGTH * 4;
953
+ var WorkflowOptionsError = class extends Error {
954
+ constructor(message) {
955
+ super(message);
956
+ this.name = "WorkflowOptionsError";
821
957
  }
822
- if (hasValue(outputRoot)) {
823
- dirs.add(join(outputRoot, runId));
958
+ };
959
+ function runOrExit3(fn) {
960
+ try {
961
+ return fn();
962
+ } catch (err) {
963
+ if (err instanceof WorkflowOptionsError || err instanceof SpaceOptionsError || err instanceof SearchOptionsError) {
964
+ printError(err.message);
965
+ process.exit(1);
966
+ }
967
+ throw err;
824
968
  }
825
- return [...dirs];
826
969
  }
827
- async function emitOpenCaptureDirectories(env, runId, capturedAt) {
828
- await Promise.all(
829
- captureOutputDirectories(env, runId).map(async (outputDir) => {
830
- await writeCaptureUploadFile(outputDir, runId);
831
- await writeCaptureStateFile(outputDir, runId, "open", capturedAt);
832
- })
833
- );
970
+ function addWorkspaceOption(command) {
971
+ return command.requiredOption("-w, --workspace <workspaceId>", "Workspace ID");
834
972
  }
835
- async function finalizeCaptureDirectories(payload, env, runId, capturedAt, files) {
836
- await Promise.all(
837
- captureOutputDirectories(env, runId).map(async (outputDir) => {
838
- await writeCaptureRunFile(outputDir, payload, runId, files);
839
- await writeCaptureUploadFile(outputDir, runId);
840
- await writeCaptureStateFile(outputDir, runId, "closed", capturedAt);
841
- })
842
- );
973
+ function addSpaceOptions2(command) {
974
+ return addWorkspaceOption(command).option("-s, --space <name>", "Space name (personal or teamspace name)").option("--personal", "Use personal space (default)").option("--team-space-id <teamSpaceId>", "Team space ID").option("--teammate-id <teammateId>", "Teammate ID (teammate space)");
843
975
  }
844
- function createCaptureRunId(startedAt, agent) {
845
- const timestamp = startedAt.replaceAll(/[^A-Za-z0-9]/g, "-");
846
- const suffix = randomUUID().replaceAll("-", "").slice(0, 8);
847
- return agent === void 0 ? `${timestamp}-${suffix}` : `${timestamp}-${agent}-${suffix}`;
848
- }
849
- async function writeCaptureDirectoryPayload(payload, env, runId, capturedAt) {
850
- const hasTranscript = payload.transcript_redacted !== void 0;
851
- const transcript = hasTranscript ? normalizeJsonl(payload.transcript_redacted ?? "") : "";
852
- const transcriptChunks = hasTranscript ? chunkJsonl(transcript, "transcript", "transcript") : [];
853
- const eventChunks = hasTranscript ? chunkJsonl(serializeEventsFromTranscript(transcript, 1, payload.run.agent).text, "events", "events") : [];
854
- await Promise.all(
855
- captureOutputDirectories(env, runId).map(async (outputDir) => {
856
- await prepareCaptureDirectory(outputDir);
857
- if (hasTranscript) {
858
- await writeChunks(outputDir, eventChunks);
859
- await writeChunks(outputDir, transcriptChunks);
860
- }
861
- await writeCaptureRunFile(outputDir, payload, runId, {
862
- events: eventChunks.map(withoutContent),
863
- transcript: transcriptChunks.map(withoutContent)
864
- });
865
- await writeCaptureUploadFile(outputDir, runId);
866
- await writeCaptureStateFile(outputDir, runId, "closed", capturedAt);
867
- })
868
- );
976
+ function addAssigneeOptions(command) {
977
+ return command.option("--human-id <humanId>", "Human assignee ID").option("--assistant-id <assistantId>", "AI assistant assignee ID").option("--teammate-id <teammateId>", "AI teammate assignee ID");
869
978
  }
870
- function serializeEventsFromTranscript(transcript, startSeq, agent = "claude") {
871
- const events = normalizeEvents(transcript, startSeq, agent);
872
- return {
873
- text: serializeEvents(events),
874
- nextSeq: startSeq + events.length
875
- };
979
+ function addPreferredAssigneeOptions(command) {
980
+ return command.option("--preferred-human-id <humanId>", "Preferred human assignee ID").option("--preferred-assistant-id <assistantId>", "Preferred AI assistant assignee ID").option("--preferred-teammate-id <teammateId>", "Preferred AI teammate assignee ID");
876
981
  }
877
- function normalizeJsonl(text) {
878
- if (text === "") {
879
- return "";
880
- }
881
- return text.endsWith("\n") ? text : `${text}
882
- `;
883
- }
884
- async function prepareCaptureDirectory(outputDir) {
885
- await mkdir(outputDir, { recursive: true });
886
- await assertMoxtManagedOutputDir(outputDir);
887
- await Promise.all([
888
- rm(join(outputDir, RUNLOG_DIRECTORY_MARKER), { force: true }),
889
- rm(join(outputDir, "run.json"), { force: true }),
890
- rm(join(outputDir, "state.json"), { force: true }),
891
- rm(join(outputDir, "upload.json"), { force: true }),
892
- rm(join(outputDir, "events.jsonl"), { force: true }),
893
- rm(join(outputDir, "transcript.jsonl"), { force: true }),
894
- rm(join(outputDir, "events"), { recursive: true, force: true }),
895
- rm(join(outputDir, "transcript"), { recursive: true, force: true })
896
- ]);
897
- await writeRunlogDirectoryMarker(outputDir);
898
- await mkdir(join(outputDir, "events"), { recursive: true });
899
- await mkdir(join(outputDir, "transcript"), { recursive: true });
900
- }
901
- async function assertMoxtManagedOutputDir(outputDir) {
902
- const entries = await readdir(outputDir);
903
- const unmanaged = entries.filter((entry) => !RUNLOG_MANAGED_OUTPUT_NAMES.has(entry));
904
- if (unmanaged.length > 0) {
905
- throw new Error(`refusing to use run output directory with unmanaged files: ${unmanaged.slice(0, 5).join(", ")}`);
906
- }
907
- const meaningfulEntries = entries.filter((entry) => entry !== ".DS_Store");
908
- if (meaningfulEntries.length === 0) {
909
- return;
910
- }
911
- if (!entries.includes(RUNLOG_DIRECTORY_MARKER)) {
912
- throw new Error("refusing to use run output directory without ownership marker");
982
+ function parsePositiveInteger(raw, label) {
983
+ const parsed = Number.parseInt(raw, 10);
984
+ if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== raw) {
985
+ throw new WorkflowOptionsError(`Error: ${label} must be a positive integer, got "${raw}".`);
913
986
  }
914
- await assertRunlogDirectoryMarker(outputDir);
915
- const metadataEntries = RUNLOG_METADATA_FILE_NAMES.filter((entry) => entries.includes(entry));
916
- if (metadataEntries.length === 0) {
917
- return;
987
+ return parsed;
988
+ }
989
+ function parseOptionalPositiveInteger(raw, label) {
990
+ if (raw == null || raw === "") return void 0;
991
+ return parsePositiveInteger(raw, label);
992
+ }
993
+ function parseStatusType(raw) {
994
+ if (raw == null || raw === "") return void 0;
995
+ const parsed = Number.parseInt(raw, 10);
996
+ if ((parsed === 1 || parsed === 2 || parsed === 3) && String(parsed) === raw) return parsed;
997
+ throw new WorkflowOptionsError("Error: --type must be one of 1 (todo), 2 (in-progress), or 3 (done).");
998
+ }
999
+ function parseRequiredStatusType(raw) {
1000
+ const value = parseStatusType(raw);
1001
+ if (value === void 0) {
1002
+ throw new WorkflowOptionsError("Error: --type is required.");
1003
+ }
1004
+ return value;
1005
+ }
1006
+ function parsePriority(raw) {
1007
+ if (raw == null || raw === "") return void 0;
1008
+ if (raw === "urgent") return 0;
1009
+ if (raw === "high") return 1;
1010
+ if (raw === "medium") return 2;
1011
+ if (raw === "low") return 3;
1012
+ throw new WorkflowOptionsError('Error: --priority must be "urgent", "high", "medium", or "low".');
1013
+ }
1014
+ function parseRequiredPriority(raw) {
1015
+ return parsePriority(raw ?? "medium") ?? 2;
1016
+ }
1017
+ function parseWorkflowScope(raw) {
1018
+ if (raw == null || raw === "") return "all";
1019
+ if (raw === "all" || raw === "owner_is_me") return raw;
1020
+ throw new WorkflowOptionsError('Error: --scope must be "all" or "owner_is_me".');
1021
+ }
1022
+ function parseTaskScope(raw) {
1023
+ if (raw == null || raw === "") return "all";
1024
+ if (raw === "all" || raw === "assigned_to_me" || raw === "subscribed_by_me" || raw === "owner_is_me") {
1025
+ return raw;
918
1026
  }
919
- const runIds = await Promise.all(
920
- metadataEntries.map((entry) => readRunlogMetadataRunId(outputDir, entry))
1027
+ throw new WorkflowOptionsError(
1028
+ 'Error: --scope must be "all", "assigned_to_me", "subscribed_by_me", or "owner_is_me".'
921
1029
  );
922
- const uniqueRunIds = new Set(runIds);
923
- if (uniqueRunIds.size > 1) {
924
- throw new Error("refusing to use run output directory with mismatched run metadata");
1030
+ }
1031
+ function parseFileIds(raw, label = "--file-ids") {
1032
+ if (raw == null || raw === "") return void 0;
1033
+ const ids = raw.split(",").map((item) => item.trim()).filter(Boolean);
1034
+ if (ids.length === 0) {
1035
+ throw new WorkflowOptionsError(`Error: ${label} must include at least one file ID.`);
925
1036
  }
1037
+ return ids;
926
1038
  }
927
- async function readRunlogMetadataRunId(outputDir, fileName) {
928
- let parsed;
929
- try {
930
- parsed = JSON.parse(await readFile(join(outputDir, fileName), "utf8"));
931
- } catch (error) {
932
- throw new Error(`invalid ${fileName}: ${errorMessage(error)}`);
1039
+ function readDescFile(path2) {
1040
+ if (path2 == null || path2 === "") return void 0;
1041
+ if (!fs3.existsSync(path2)) {
1042
+ throw new WorkflowOptionsError(`Error: desc file not found: ${path2}`);
933
1043
  }
934
- const value = objectValue(parsed);
935
- if (value === null) {
936
- throw new Error(`invalid ${fileName}: expected object`);
1044
+ const stat = fs3.statSync(path2);
1045
+ if (!stat.isFile()) {
1046
+ throw new WorkflowOptionsError(`Error: desc file is not a file: ${path2}`);
937
1047
  }
938
- if (value.schema_version !== RUNLOG_METADATA_SCHEMAS[fileName]) {
939
- throw new Error(`invalid ${fileName}: unexpected schema_version`);
1048
+ const buffer2 = fs3.readFileSync(path2);
1049
+ if (isBinaryContent(buffer2)) {
1050
+ throw new WorkflowOptionsError("Error: desc file must be UTF-8 text, not binary content.");
940
1051
  }
941
- if (typeof value.run_id !== "string" || value.run_id.trim() === "") {
942
- throw new Error(`invalid ${fileName}: missing run_id`);
1052
+ const content = buffer2.toString("utf8");
1053
+ if (content.length > MAX_DESC_LENGTH) {
1054
+ throw new WorkflowOptionsError(`Error: desc file must be at most ${MAX_DESC_LENGTH} characters.`);
943
1055
  }
944
- return value.run_id;
1056
+ return content;
945
1057
  }
946
- async function assertRunlogDirectoryMarker(outputDir) {
947
- let parsed;
948
- try {
949
- parsed = JSON.parse(await readFile(join(outputDir, RUNLOG_DIRECTORY_MARKER), "utf8"));
950
- } catch (error) {
951
- throw new Error(`invalid ${RUNLOG_DIRECTORY_MARKER}: ${errorMessage(error)}`);
1058
+ function readCommentContentFile(path2) {
1059
+ if (!fs3.existsSync(path2)) {
1060
+ throw new WorkflowOptionsError(`Error: comment content file not found: ${path2}`);
952
1061
  }
953
- const value = objectValue(parsed);
954
- if (value?.schema_version !== RUNLOG_DIRECTORY_MARKER_SCHEMA) {
955
- throw new Error(`invalid ${RUNLOG_DIRECTORY_MARKER}: unexpected schema_version`);
1062
+ const stat = fs3.statSync(path2);
1063
+ if (!stat.isFile()) {
1064
+ throw new WorkflowOptionsError(`Error: comment content path is not a file: ${path2}`);
956
1065
  }
957
- }
958
- async function prepareOutputPath(path2, prepare = async () => {
959
- await mkdir(path2, { recursive: true });
960
- }) {
961
- try {
962
- await prepare();
963
- } catch (error) {
964
- throw new CaptureOutputPrepareError(path2, error);
965
- }
966
- }
967
- async function writeCaptureRunFile(outputDir, payload, runId, files) {
968
- const runFile = {
969
- schema_version: "runlog.capture.v1",
970
- run_id: runId,
971
- run: payload.run,
972
- recording: payload.recording,
973
- repo: payload.repo,
974
- ...payload.redaction_failed ? { redaction_failed: true } : {},
975
- files: {
976
- state: "state.json",
977
- upload: "upload.json",
978
- events: files.events,
979
- transcript: files.transcript
980
- }
981
- };
982
- await writeFile(join(outputDir, "run.json"), `${JSON.stringify(runFile, null, 2)}
983
- `, "utf8");
984
- }
985
- async function writeRunlogDirectoryMarker(outputDir) {
986
- await writeFile(
987
- join(outputDir, RUNLOG_DIRECTORY_MARKER),
988
- `${JSON.stringify({ schema_version: RUNLOG_DIRECTORY_MARKER_SCHEMA }, null, 2)}
989
- `,
990
- "utf8"
991
- );
992
- }
993
- async function writeCaptureUploadFile(outputDir, runId) {
994
- const path2 = join(outputDir, "upload.json");
1066
+ if (stat.size > MAX_COMMENT_CONTENT_BYTES) {
1067
+ throw new WorkflowOptionsError(
1068
+ `Error: comment content file must be at most ${MAX_COMMENT_CONTENT_LENGTH} characters.`
1069
+ );
1070
+ }
1071
+ const buffer2 = fs3.readFileSync(path2);
1072
+ if (buffer2.includes(0)) {
1073
+ throw new WorkflowOptionsError("Error: comment content file must not contain NUL bytes.");
1074
+ }
1075
+ if (isBinaryContent(buffer2)) {
1076
+ throw new WorkflowOptionsError("Error: comment content file must be UTF-8 text, not binary content.");
1077
+ }
1078
+ let content;
995
1079
  try {
996
- await access(path2);
997
- return;
1080
+ content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer2);
998
1081
  } catch {
1082
+ throw new WorkflowOptionsError("Error: comment content file must contain valid UTF-8 text.");
999
1083
  }
1000
- const uploadFile2 = {
1001
- schema_version: "runlog.upload.v1",
1002
- run_id: runId,
1003
- status: "pending",
1004
- files: {}
1005
- };
1006
- await writeFile(path2, `${JSON.stringify(uploadFile2, null, 2)}
1007
- `, "utf8");
1008
- }
1009
- async function writeCaptureStateFile(outputDir, runId, status, capturedAt) {
1010
- const stateFile = {
1011
- schema_version: "runlog.state.v1",
1012
- run_id: runId,
1013
- status,
1014
- updated_at: capturedAt
1015
- };
1016
- await writeFile(join(outputDir, "state.json"), `${JSON.stringify(stateFile, null, 2)}
1017
- `, "utf8");
1084
+ if (content.length === 0) {
1085
+ throw new WorkflowOptionsError("Error: comment content file must not be empty.");
1086
+ }
1087
+ if (content.length > MAX_COMMENT_CONTENT_LENGTH) {
1088
+ throw new WorkflowOptionsError(
1089
+ `Error: comment content file must be at most ${MAX_COMMENT_CONTENT_LENGTH} characters.`
1090
+ );
1091
+ }
1092
+ return content;
1018
1093
  }
1019
- async function writeChunks(outputDir, chunks) {
1020
- await Promise.all(chunks.map((chunk) => writeFile(join(outputDir, chunk.path), chunk.content, "utf8")));
1094
+ function parseAssigneeBody(options, allowClear) {
1095
+ const hasClear = options.clear === true;
1096
+ const humanId = parseOptionalPositiveInteger(options.humanId, "--human-id");
1097
+ const assistantId = parseOptionalPositiveInteger(options.assistantId, "--assistant-id");
1098
+ const teammateId = parseOptionalPositiveInteger(options.teammateId, "--teammate-id");
1099
+ const count = (humanId !== void 0 ? 1 : 0) + (assistantId !== void 0 ? 1 : 0) + (teammateId !== void 0 ? 1 : 0);
1100
+ if (hasClear && !allowClear) {
1101
+ throw new WorkflowOptionsError("Error: --clear is not supported here.");
1102
+ }
1103
+ if (hasClear && count > 0) {
1104
+ throw new WorkflowOptionsError("Error: --clear cannot be used with assignee ID options.");
1105
+ }
1106
+ if (count > 1) {
1107
+ throw new WorkflowOptionsError("Error: assignee ID options are mutually exclusive.");
1108
+ }
1109
+ if (hasClear) return {};
1110
+ if (humanId !== void 0) return { assigneeHumanId: humanId };
1111
+ if (assistantId !== void 0) return { assigneeAiAssistantId: assistantId };
1112
+ if (teammateId !== void 0) return { assigneeAiTeammateId: teammateId };
1113
+ return {};
1021
1114
  }
1022
- function withoutContent(chunk) {
1023
- return {
1024
- path: chunk.path,
1025
- bytes: chunk.bytes,
1026
- lines: chunk.lines,
1027
- oversized_line: chunk.oversized_line
1028
- };
1029
- }
1030
- function captureEnvPaths(env) {
1031
- const { MOXT_RUN_OUTPUT_DIR: outputDir, MOXT_RUN_OUTPUT_ROOT: outputRoot } = env;
1032
- return { outputDir, outputRoot };
1033
- }
1034
- function chunkJsonl(text, dir, prefix) {
1035
- if (text === "") {
1036
- return [];
1037
- }
1038
- const chunks = [];
1039
- let content = "";
1040
- let bytes = 0;
1041
- let lines = 0;
1042
- let oversizedLine = false;
1043
- const flush2 = () => {
1044
- if (content === "") {
1045
- return;
1046
- }
1047
- const index = String(chunks.length + 1).padStart(6, "0");
1048
- chunks.push({
1049
- path: `${dir}/${prefix}-${index}.jsonl`,
1050
- content,
1051
- bytes,
1052
- lines,
1053
- oversized_line: oversizedLine
1054
- });
1055
- content = "";
1056
- bytes = 0;
1057
- lines = 0;
1058
- oversizedLine = false;
1059
- };
1060
- for (const line of jsonlLines(text)) {
1061
- const lineBytes = Buffer.byteLength(line);
1062
- if (lineBytes > DATA_FILE_TARGET_BYTES) {
1063
- flush2();
1064
- content = line;
1065
- bytes = lineBytes;
1066
- lines = 1;
1067
- oversizedLine = true;
1068
- flush2();
1069
- continue;
1070
- }
1071
- if (bytes > 0 && bytes + lineBytes > DATA_FILE_TARGET_BYTES) {
1072
- flush2();
1073
- }
1074
- content += line;
1075
- bytes += lineBytes;
1076
- lines += 1;
1077
- }
1078
- flush2();
1079
- return chunks;
1080
- }
1081
- function jsonlLines(text) {
1082
- const normalized = normalizeJsonl(text);
1083
- if (normalized === "") {
1084
- return [];
1085
- }
1086
- return normalized.slice(0, -1).split("\n").map((line) => `${line}
1087
- `);
1115
+ function parsePreferredAssigneeBody(options) {
1116
+ const hasClear = options.clearPreferredAssignee === true;
1117
+ const humanId = parseOptionalPositiveInteger(options.preferredHumanId, "--preferred-human-id");
1118
+ const assistantId = parseOptionalPositiveInteger(options.preferredAssistantId, "--preferred-assistant-id");
1119
+ const teammateId = parseOptionalPositiveInteger(options.preferredTeammateId, "--preferred-teammate-id");
1120
+ const count = (humanId !== void 0 ? 1 : 0) + (assistantId !== void 0 ? 1 : 0) + (teammateId !== void 0 ? 1 : 0);
1121
+ if (hasClear && count > 0) {
1122
+ throw new WorkflowOptionsError("Error: --clear-preferred-assignee cannot be used with preferred assignee ID options.");
1123
+ }
1124
+ if (count > 1) {
1125
+ throw new WorkflowOptionsError("Error: preferred assignee ID options are mutually exclusive.");
1126
+ }
1127
+ if (hasClear) return { preferredAssigneeHumanId: null };
1128
+ if (humanId !== void 0) return { preferredAssigneeHumanId: humanId };
1129
+ if (assistantId !== void 0) return { preferredAssigneeAiAssistantId: assistantId };
1130
+ if (teammateId !== void 0) return { preferredAssigneeAiTeammateId: teammateId };
1131
+ return {};
1088
1132
  }
1089
- function serializeEvents(events) {
1090
- if (events.length === 0) {
1091
- return "";
1092
- }
1093
- return `${events.map((event) => JSON.stringify(event)).join("\n")}
1094
- `;
1095
- }
1096
- function normalizeEvents(transcript, startSeq, agent) {
1097
- const events = [];
1098
- for (const line of transcript.split("\n")) {
1099
- if (line.trim() === "") {
1100
- continue;
1101
- }
1102
- const raw = parseTranscriptLine(line);
1103
- const event = agent === "codex" ? normalizeCodexEvent(raw, startSeq + events.length) : normalizeClaudeEvent(raw, startSeq + events.length);
1104
- if (event !== null) {
1105
- events.push(event);
1106
- }
1107
- }
1108
- return events;
1109
- }
1110
- function normalizeClaudeEvent(raw, seq) {
1111
- if (raw === null) {
1112
- return null;
1113
- }
1114
- const timestamp = stringValue(raw.timestamp);
1115
- if (raw.type === "user") {
1116
- const message = messageValue(raw.message);
1117
- const text = textFromContent(message?.content);
1118
- if (text === "") {
1119
- return null;
1120
- }
1121
- return {
1122
- schema_version: "runlog.events.v1",
1123
- seq,
1124
- type: "user_message",
1125
- ...timestamp === void 0 ? {} : { timestamp },
1126
- text
1127
- };
1128
- }
1129
- if (raw.type === "assistant") {
1130
- const message = messageValue(raw.message);
1131
- const text = textFromContent(message?.content);
1132
- if (text === "") {
1133
- return null;
1134
- }
1135
- const usage = usageValue(message?.usage);
1136
- const model = stringValue(message?.model);
1137
- const inputTokens = numberValue(usage?.input_tokens);
1138
- const outputTokens = numberValue(usage?.output_tokens);
1139
- return {
1140
- schema_version: "runlog.events.v1",
1141
- seq,
1142
- type: "assistant_message",
1143
- ...timestamp === void 0 ? {} : { timestamp },
1144
- ...model === void 0 ? {} : { model },
1145
- text,
1146
- ...inputTokens === void 0 ? {} : { input_tokens: inputTokens },
1147
- ...outputTokens === void 0 ? {} : { output_tokens: outputTokens }
1148
- };
1149
- }
1150
- if (raw.type === "system" && raw.subtype === "turn_duration") {
1151
- const durationMs = numberValue(raw.durationMs);
1152
- const messageCount = numberValue(raw.messageCount);
1153
- return {
1154
- schema_version: "runlog.events.v1",
1155
- seq,
1156
- type: "turn_summary",
1157
- ...timestamp === void 0 ? {} : { timestamp },
1158
- ...durationMs === void 0 ? {} : { duration_ms: durationMs },
1159
- ...messageCount === void 0 ? {} : { message_count: messageCount }
1160
- };
1161
- }
1162
- return null;
1163
- }
1164
- function normalizeCodexEvent(raw, seq) {
1165
- if (raw === null || raw.type !== "event_msg") {
1166
- return null;
1167
- }
1168
- const timestamp = stringValue(raw.timestamp);
1169
- const payload = codexPayloadValue(raw.payload);
1170
- const payloadType = stringValue(payload?.type);
1171
- if (payloadType === "user_message") {
1172
- const text = stringValue(payload?.message) ?? "";
1173
- if (text === "") {
1174
- return null;
1175
- }
1176
- return {
1177
- schema_version: "runlog.events.v1",
1178
- seq,
1179
- type: "user_message",
1180
- ...timestamp === void 0 ? {} : { timestamp },
1181
- text
1182
- };
1183
- }
1184
- if (payloadType === "agent_message") {
1185
- const text = stringValue(payload?.message) ?? "";
1186
- if (text === "") {
1187
- return null;
1188
- }
1189
- return {
1190
- schema_version: "runlog.events.v1",
1191
- seq,
1192
- type: "assistant_message",
1193
- ...timestamp === void 0 ? {} : { timestamp },
1194
- text
1195
- };
1196
- }
1197
- if (payloadType === "task_complete") {
1198
- const durationMs = numberValue(payload?.duration_ms);
1199
- return {
1200
- schema_version: "runlog.events.v1",
1201
- seq,
1202
- type: "turn_summary",
1203
- ...timestamp === void 0 ? {} : { timestamp },
1204
- ...durationMs === void 0 ? {} : { duration_ms: durationMs }
1205
- };
1206
- }
1207
- return null;
1208
- }
1209
- function parseTranscriptLine(line) {
1210
- try {
1211
- const value = JSON.parse(line);
1212
- return isObject(value) ? value : null;
1213
- } catch {
1214
- return null;
1133
+ function assertNonEmptyBody(body) {
1134
+ if (Object.keys(body).length === 0) {
1135
+ throw new WorkflowOptionsError("Error: at least one update option must be provided.");
1215
1136
  }
1216
1137
  }
1217
- function textFromContent(content) {
1218
- if (typeof content === "string") {
1219
- return content;
1220
- }
1221
- if (!Array.isArray(content)) {
1222
- return "";
1223
- }
1224
- return content.map((part) => {
1225
- const contentPart = contentPartValue(part);
1226
- if (contentPart?.type !== "text") {
1227
- return "";
1138
+ function buildQuery(params) {
1139
+ const query = new URLSearchParams();
1140
+ for (const [key, value] of Object.entries(params)) {
1141
+ if (value !== void 0) {
1142
+ query.set(key, String(value));
1228
1143
  }
1229
- return stringValue(contentPart.text) ?? "";
1230
- }).filter((text) => text !== "").join("\n");
1231
- }
1232
- function messageValue(value) {
1233
- return isObject(value) ? value : void 0;
1234
- }
1235
- function codexPayloadValue(value) {
1236
- return isObject(value) ? value : void 0;
1237
- }
1238
- function usageValue(value) {
1239
- return isObject(value) ? value : void 0;
1240
- }
1241
- function contentPartValue(value) {
1242
- return isObject(value) ? value : void 0;
1243
- }
1244
- function stringValue(value) {
1245
- return typeof value === "string" ? value : void 0;
1246
- }
1247
- function numberValue(value) {
1248
- return typeof value === "number" ? value : void 0;
1249
- }
1250
- function isObject(value) {
1251
- return typeof value === "object" && value !== null && !Array.isArray(value);
1252
- }
1253
- function hasValue(value) {
1254
- return value !== void 0 && value !== "";
1255
- }
1256
- function errorMessage(error) {
1257
- if (error instanceof Error && error.message !== "") {
1258
- return error.message;
1259
1144
  }
1260
- return String(error);
1261
- }
1262
- function objectValue(value) {
1263
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1145
+ const text = query.toString();
1146
+ return text ? `?${text}` : "";
1264
1147
  }
1265
-
1266
- // src/run/redaction.ts
1267
- import { createHmac } from "crypto";
1268
- var SECRET_RULES = [
1269
- {
1270
- name: "private_key",
1271
- pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
1272
- replacement: (value, salt) => `<REDACTED:private_key:${tag(value, salt)}>`
1273
- },
1274
- {
1275
- name: "bearer",
1276
- pattern: /Bearer\s+[A-Za-z0-9._~+/=-]+/gi,
1277
- replacement: (value, salt) => `<REDACTED:bearer:${tag(value, salt)}>`
1278
- },
1279
- {
1280
- name: "basic",
1281
- pattern: /Basic\s+[A-Za-z0-9+/=-]+/gi,
1282
- replacement: (value, salt) => `<REDACTED:basic:${tag(value, salt)}>`
1283
- },
1284
- {
1285
- name: "jwt",
1286
- pattern: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
1287
- replacement: (value, salt) => `<REDACTED:jwt:${tag(value, salt)}>`
1288
- },
1289
- {
1290
- name: "github",
1291
- pattern: /github_pat_[A-Za-z0-9_]{20,}/g,
1292
- replacement: (value, salt) => `<REDACTED:github:${tag(value, salt)}>`
1293
- },
1294
- {
1295
- name: "github",
1296
- pattern: /github_pat_[A-Za-z0-9_]{4,}(?:\u2026|\.{3})/g,
1297
- replacement: (value, salt) => `<REDACTED:github:${tag(value, salt)}>`
1298
- },
1299
- {
1300
- name: "github",
1301
- pattern: /gh[opsu]_[A-Za-z0-9_]{8,}/g,
1302
- replacement: (value, salt) => `<REDACTED:github:${tag(value, salt)}>`
1303
- },
1304
- {
1305
- name: "github",
1306
- pattern: /gh[opsu]_[A-Za-z0-9_]{4,}(?:\u2026|\.{3})/g,
1307
- replacement: (value, salt) => `<REDACTED:github:${tag(value, salt)}>`
1308
- },
1309
- {
1310
- name: "openai",
1311
- pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g,
1312
- replacement: (value, salt) => `<REDACTED:openai:${tag(value, salt)}>`
1313
- },
1314
- {
1315
- name: "openai",
1316
- pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{4,}(?:\u2026|\.{3})/g,
1317
- replacement: (value, salt) => `<REDACTED:openai:${tag(value, salt)}>`
1318
- },
1319
- {
1320
- name: "slack",
1321
- pattern: /xox[baprs]-[A-Za-z0-9-]{10,}/g,
1322
- replacement: (value, salt) => `<REDACTED:slack:${tag(value, salt)}>`
1323
- },
1324
- {
1325
- name: "stripe",
1326
- pattern: /(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{10,}/g,
1327
- replacement: (value, salt) => `<REDACTED:stripe:${tag(value, salt)}>`
1328
- },
1329
- {
1330
- name: "aws_key",
1331
- pattern: /(?:AKIA|ASIA)[0-9A-Z]{16}/g,
1332
- replacement: (value, salt) => `<REDACTED:aws_key:${tag(value, salt)}>`
1333
- },
1334
- {
1335
- name: "aws_key",
1336
- pattern: /(?:AKIA|ASIA)[0-9A-Z]{4,}(?:\u2026|\.{3})/g,
1337
- replacement: (value, salt) => `<REDACTED:aws_key:${tag(value, salt)}>`
1338
- }
1339
- ];
1340
- var FIELD_QUOTED_PATTERN = /(["']?(?:password|passwd|pwd|secret|client_secret|token|access_token|refresh_token|authorization|cookie|set-cookie|api_key|apikey|private_key)["']?\s*[:=]\s*)(["'])(?:\\.|(?!\2)[\s\S])*?\2/gi;
1341
- var FIELD_UNQUOTED_PATTERN = /(["']?(?:password|passwd|pwd|secret|client_secret|token|access_token|refresh_token|cookie|set-cookie|api_key|apikey|private_key)["']?\s*[:=]\s*)[^"',\n}\s]+/gi;
1342
- function redactText(text, salt = "moxt-run-local-salt") {
1343
- const hitCounts = /* @__PURE__ */ new Map();
1344
- let redacted = text;
1345
- for (const rule of SECRET_RULES) {
1346
- redacted = redacted.replace(rule.pattern, (value) => {
1347
- addHit(hitCounts, rule.name);
1348
- return rule.replacement(value, salt);
1349
- });
1350
- }
1351
- redacted = redacted.replace(FIELD_QUOTED_PATTERN, (_match, prefix, quote) => {
1352
- addHit(hitCounts, "field");
1353
- return `${prefix}${quote}<REDACTED:field>${quote}`;
1354
- }).replace(FIELD_UNQUOTED_PATTERN, (_match, prefix) => {
1355
- addHit(hitCounts, "field");
1356
- return `${prefix}<REDACTED:field>`;
1357
- });
1358
- return {
1359
- text: redacted,
1360
- hits: [...hitCounts.entries()].map(([rule, count]) => ({ rule, count }))
1361
- };
1362
- }
1363
- function tag(value, salt) {
1364
- return createHmac("sha256", salt).update(value).digest("hex").slice(0, 8);
1148
+ function failResponse(response) {
1149
+ printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
1150
+ process.exit(1);
1365
1151
  }
1366
- function addHit(hitCounts, rule) {
1367
- hitCounts.set(rule, (hitCounts.get(rule) ?? 0) + 1);
1152
+ function priorityLabel(value) {
1153
+ switch (value) {
1154
+ case 0:
1155
+ return "urgent";
1156
+ case 1:
1157
+ return "high";
1158
+ case 2:
1159
+ return "medium";
1160
+ case 3:
1161
+ return "low";
1162
+ default: {
1163
+ const unsupportedValue = value;
1164
+ throw new Error(`Unsupported workflow task priority: ${String(unsupportedValue)}`);
1165
+ }
1166
+ }
1167
+ }
1168
+ function statusTypeLabel(value) {
1169
+ switch (value) {
1170
+ case 1:
1171
+ return "todo";
1172
+ case 2:
1173
+ return "in-progress";
1174
+ case 3:
1175
+ return "done";
1176
+ default: {
1177
+ const unsupportedValue = value;
1178
+ throw new Error(`Unsupported workflow status type: ${String(unsupportedValue)}`);
1179
+ }
1180
+ }
1181
+ }
1182
+ function formatAssignee(assignee) {
1183
+ if (!assignee) return "-";
1184
+ if (assignee.type === "human") return assignee.displayName ?? `human:${assignee.humanId}`;
1185
+ if (assignee.type === "assistant") return assignee.displayName ?? `assistant:${assignee.aiAssistantId}`;
1186
+ if (assignee.type === "teammate") return assignee.displayName ?? `teammate:${assignee.aiTeammateId}`;
1187
+ return "unknown";
1188
+ }
1189
+ function printWorkflow(item) {
1190
+ print(`${colors.bold}${item.fileId}${colors.reset} owner=${item.ownerHumanId ?? "-"} desc=${item.desc ?? "-"}`);
1191
+ }
1192
+ function indentation(size) {
1193
+ return " ".repeat(size);
1194
+ }
1195
+ function escapeTerminalControlCharacters(value) {
1196
+ return value.replace(/\r\n/g, "\n").replace(
1197
+ /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g,
1198
+ (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`
1199
+ );
1368
1200
  }
1369
-
1370
- // src/run/sanitize.ts
1371
- function stripRemoteCredentials(remote) {
1372
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(remote)) {
1373
- return remote;
1374
- }
1375
- try {
1376
- const url = new URL(remote);
1377
- url.username = "";
1378
- url.password = "";
1379
- return url.toString();
1380
- } catch {
1381
- return remote;
1201
+ function formatDetailScalar(value) {
1202
+ if (value === null) return "null";
1203
+ if (typeof value === "string") {
1204
+ const escaped = escapeTerminalControlCharacters(value);
1205
+ if (escaped === "") return '""';
1206
+ if (/\n/.test(escaped)) return JSON.stringify(escaped);
1207
+ return escaped;
1382
1208
  }
1209
+ return String(value);
1383
1210
  }
1384
- function sanitizeRepo(repo) {
1385
- if (repo === null) return null;
1386
- return {
1387
- ...repo,
1388
- remote: stripRemoteCredentials(repo.remote)
1389
- };
1211
+ function printDetailField(name, value, indent = 0) {
1212
+ print(`${indentation(indent)}${name}: ${formatDetailScalar(value)}`);
1390
1213
  }
1391
-
1392
- // src/run/session.ts
1393
- import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
1394
- import { join as join2 } from "path";
1395
- var SESSION_START_TOLERANCE_MS = 1e3;
1396
- function snapshotJsonl(root) {
1397
- const snapshot = /* @__PURE__ */ new Map();
1398
- if (!existsSync2(root)) {
1399
- return snapshot;
1400
- }
1401
- collectJsonl(root, snapshot);
1402
- return snapshot;
1403
- }
1404
- function matchSession(before, after, startedAtMs, options = {}) {
1405
- const candidates = [];
1406
- for (const [path2, current2] of after.entries()) {
1407
- const previous = before.get(path2);
1408
- if (previous === void 0) {
1409
- if (wasChangedAfterStart(current2, startedAtMs)) {
1410
- candidates.push({ path: path2, startOffset: 0 });
1411
- }
1412
- continue;
1413
- }
1414
- if (!options.newFilesOnly && current2.size > previous.size) {
1415
- candidates.push({ path: path2, startOffset: previous.size });
1416
- }
1417
- }
1418
- if (candidates.length === 0) {
1419
- return { match: "none", sessionFile: null, startOffset: 0 };
1420
- }
1421
- const trustedCandidates = options.isCandidate === void 0 ? candidates : candidates.filter((candidate) => options.isCandidate?.(candidate.path));
1422
- if (trustedCandidates.length === 0) {
1423
- return { match: "none", sessionFile: null, startOffset: 0 };
1424
- }
1425
- if (trustedCandidates.length === 1) {
1426
- const [candidate] = trustedCandidates;
1427
- if (candidate !== void 0) {
1428
- return {
1429
- match: "unique",
1430
- sessionFile: candidate.path,
1431
- startOffset: candidate.startOffset
1432
- };
1433
- }
1434
- }
1435
- return { match: "ambiguous", sessionFile: null, startOffset: 0 };
1436
- }
1437
- function matchKnownSessionFile(before, after, sessionFile) {
1438
- const current2 = after.get(sessionFile);
1439
- if (current2 === void 0) {
1440
- return { match: "none", sessionFile: null, startOffset: 0 };
1441
- }
1442
- const previous = before.get(sessionFile);
1443
- if (previous !== void 0 && current2.size <= previous.size) {
1444
- return { match: "none", sessionFile: null, startOffset: 0 };
1214
+ function printDetailTextField(name, value, indent = 0) {
1215
+ if (value === null || value === "") {
1216
+ printDetailField(name, value, indent);
1217
+ return;
1445
1218
  }
1446
- return {
1447
- match: "unique",
1448
- sessionFile,
1449
- startOffset: previous?.size ?? 0
1450
- };
1451
- }
1452
- function recordingFromSessionMatch(match) {
1453
- switch (match) {
1454
- case "unique":
1455
- return { found_chat: true, quality: "complete" };
1456
- case "none":
1457
- return { found_chat: false, quality: "not_found" };
1458
- case "ambiguous":
1459
- return { found_chat: false, quality: "uncertain" };
1460
- case "prep_failed":
1461
- return { found_chat: false, quality: "unavailable" };
1462
- }
1463
- }
1464
- function collectJsonl(dir, snapshot) {
1465
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1466
- const path2 = join2(dir, entry.name);
1467
- if (entry.isDirectory()) {
1468
- collectJsonl(path2, snapshot);
1469
- continue;
1470
- }
1471
- if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
1472
- continue;
1473
- }
1474
- const stat2 = statSync2(path2);
1475
- snapshot.set(path2, {
1476
- mtimeMs: stat2.mtimeMs,
1477
- size: stat2.size
1478
- });
1219
+ const escaped = escapeTerminalControlCharacters(value);
1220
+ print(`${indentation(indent)}${name}:`);
1221
+ for (const line of escaped.split("\n")) {
1222
+ print(`${indentation(indent + 2)}${line}`);
1479
1223
  }
1480
1224
  }
1481
- function wasChangedAfterStart(current2, startedAtMs) {
1482
- return current2.mtimeMs >= startedAtMs - SESSION_START_TOLERANCE_MS;
1483
- }
1484
-
1485
- // src/run/status.ts
1486
- var SIGNAL_NUMBERS = /* @__PURE__ */ new Map([
1487
- ["SIGHUP", 1],
1488
- ["SIGINT", 2],
1489
- ["SIGQUIT", 3],
1490
- ["SIGKILL", 9],
1491
- ["SIGTERM", 15]
1492
- ]);
1493
- function signalNumber(signal) {
1494
- return SIGNAL_NUMBERS.get(signal) ?? 0;
1495
- }
1496
- function classifyExit(event, observedSignals) {
1497
- if (event.errorCode !== void 0) {
1498
- return {
1499
- code: event.errorCode === "ENOENT" ? 127 : 126,
1500
- status: "unknown"
1501
- };
1502
- }
1503
- if (event.signal !== null) {
1504
- return {
1505
- code: 128 + signalNumber(event.signal),
1506
- status: "cancelled"
1507
- };
1508
- }
1509
- if (event.code !== null) {
1510
- if (event.code === 0) {
1511
- return { code: 0, status: "completed" };
1512
- }
1513
- const signal = event.code - 128;
1514
- return {
1515
- code: event.code,
1516
- status: event.code > 128 && observedSignals.has(signal) ? "cancelled" : "failed"
1517
- };
1518
- }
1519
- return { code: 1, status: "unknown" };
1520
- }
1521
-
1522
- // src/run/live-capture.ts
1523
- import { appendFile, open, writeFile as writeFile2 } from "fs/promises";
1524
- import { join as join3 } from "path";
1525
- var DATA_FILE_TARGET_BYTES2 = 1024 * 1024;
1526
- var LIVE_CAPTURE_INTERVAL_MS = 250;
1527
- function createLiveCapture(options) {
1528
- return new LiveCaptureRecorder(options);
1529
- }
1530
- var LiveCaptureRecorder = class {
1531
- enabled;
1532
- outputs;
1533
- env;
1534
- runId;
1535
- agent;
1536
- startedAt;
1537
- sessions;
1538
- before;
1539
- baselineOk;
1540
- startedAtMs;
1541
- knownSessionFile;
1542
- isSessionCandidate;
1543
- newSessionFilesOnly;
1544
- openSessionFile;
1545
- timer;
1546
- pollInFlight = null;
1547
- sessionFile = null;
1548
- readOffset = 0;
1549
- pendingText = "";
1550
- nextSeq = 1;
1551
- sessionMatch;
1552
- captureFailed = false;
1553
- constructor(options) {
1554
- const outputDirs = captureOutputDirectories(options.env, options.runId);
1555
- this.enabled = outputDirs.length > 0;
1556
- this.outputs = outputDirs.map((outputDir) => new CaptureDirectoryOutput(outputDir));
1557
- this.env = options.env;
1558
- this.runId = options.runId;
1559
- this.agent = options.agent;
1560
- this.startedAt = options.startedAt;
1561
- this.sessions = options.sessions;
1562
- this.before = options.before;
1563
- this.baselineOk = options.baselineOk;
1564
- this.startedAtMs = options.startedAtMs;
1565
- this.knownSessionFile = options.knownSessionFile;
1566
- this.isSessionCandidate = options.isSessionCandidate;
1567
- this.newSessionFilesOnly = options.newSessionFilesOnly ?? false;
1568
- this.openSessionFile = options.openSessionFile;
1569
- this.sessionMatch = options.baselineOk ? "none" : "prep_failed";
1570
- }
1571
- async start() {
1572
- if (!this.enabled) {
1573
- return;
1574
- }
1575
- await emitOpenCaptureDirectories(this.env, this.runId, this.startedAt);
1576
- if (this.baselineOk) {
1577
- this.timer = setInterval(() => {
1578
- this.queuePoll();
1579
- }, LIVE_CAPTURE_INTERVAL_MS);
1580
- }
1581
- }
1582
- async stop() {
1583
- if (!this.enabled) {
1584
- return;
1585
- }
1586
- if (this.timer !== void 0) {
1587
- clearInterval(this.timer);
1588
- this.timer = void 0;
1589
- }
1590
- if (this.pollInFlight !== null) {
1591
- await this.pollInFlight;
1592
- }
1593
- await this.poll();
1594
- await this.flushPendingText();
1595
- }
1596
- async finalize(payload, capturedAt) {
1597
- if (!this.enabled) {
1598
- return;
1599
- }
1600
- if (!isCompleteRecording(payload)) {
1601
- await Promise.all(this.outputs.map((output) => output.invalidate()));
1602
- }
1603
- await finalizeCaptureDirectories(this.directoryPayload(payload), this.env, this.runId, capturedAt, this.files());
1604
- }
1605
- queuePoll() {
1606
- if (this.pollInFlight !== null) {
1607
- return;
1608
- }
1609
- this.pollInFlight = this.poll().finally(() => {
1610
- this.pollInFlight = null;
1611
- });
1612
- }
1613
- async poll() {
1614
- if (!this.baselineOk || this.captureFailed) {
1615
- return;
1616
- }
1617
- try {
1618
- const snapshot = snapshotJsonl(this.sessions);
1619
- if (this.sessionFile === null) {
1620
- if (this.knownSessionFile !== void 0) {
1621
- const matched2 = matchKnownSessionFile(this.before, snapshot, this.knownSessionFile);
1622
- if (matched2.match === "unique" && matched2.sessionFile !== null) {
1623
- this.sessionMatch = matched2.match;
1624
- this.sessionFile = matched2.sessionFile;
1625
- this.readOffset = matched2.startOffset;
1626
- await this.captureNewText();
1627
- return;
1628
- }
1629
- }
1630
- const opened = this.openSessionFile?.() ?? { match: "none" };
1631
- if (opened.match === "ambiguous") {
1632
- this.sessionMatch = "ambiguous";
1633
- return;
1634
- }
1635
- if (opened.match === "unique") {
1636
- const matched2 = matchKnownSessionFile(this.before, snapshot, opened.sessionFile);
1637
- this.sessionMatch = matched2.match;
1638
- if (matched2.match === "unique" && matched2.sessionFile !== null) {
1639
- this.sessionFile = matched2.sessionFile;
1640
- this.readOffset = matched2.startOffset;
1641
- await this.captureNewText();
1642
- }
1643
- return;
1644
- }
1645
- const matched = matchSession(this.before, snapshot, this.startedAtMs, this.matchOptions());
1646
- this.sessionMatch = matched.match;
1647
- if (matched.match !== "unique" || matched.sessionFile === null) {
1648
- return;
1649
- }
1650
- this.sessionFile = matched.sessionFile;
1651
- this.readOffset = matched.startOffset;
1652
- }
1653
- await this.captureNewText();
1654
- } catch {
1655
- this.captureFailed = true;
1656
- }
1225
+ function printAssigneeDetail(name, assignee, indent = 0) {
1226
+ if (assignee === null) {
1227
+ printDetailField(name, null, indent);
1228
+ return;
1657
1229
  }
1658
- async captureNewText() {
1659
- if (this.sessionFile === null) {
1660
- return;
1661
- }
1662
- const next = await readFileFromOffset(this.sessionFile, this.readOffset);
1663
- if (next.bytes === 0) {
1230
+ print(`${indentation(indent)}${name}:`);
1231
+ printDetailField("type", assignee.type, indent + 2);
1232
+ switch (assignee.type) {
1233
+ case "human":
1234
+ printDetailField("humanId", assignee.humanId, indent + 2);
1235
+ printDetailField("displayName", assignee.displayName, indent + 2);
1236
+ printDetailField("avatarUrl", assignee.avatarUrl, indent + 2);
1664
1237
  return;
1665
- }
1666
- this.readOffset += next.bytes;
1667
- await this.captureCompleteLines(next.text);
1668
- }
1669
- async captureCompleteLines(text) {
1670
- const combined = `${this.pendingText}${text}`;
1671
- const lastNewline = combined.lastIndexOf("\n");
1672
- if (lastNewline === -1) {
1673
- this.pendingText = combined;
1238
+ case "assistant":
1239
+ printDetailField("aiAssistantId", assignee.aiAssistantId, indent + 2);
1240
+ printDetailField("displayName", assignee.displayName, indent + 2);
1241
+ printDetailField("avatarUrl", assignee.avatarUrl, indent + 2);
1674
1242
  return;
1675
- }
1676
- const complete = combined.slice(0, lastNewline + 1);
1677
- this.pendingText = combined.slice(lastNewline + 1);
1678
- await this.captureTranscriptText(complete);
1679
- }
1680
- async flushPendingText() {
1681
- if (this.pendingText === "") {
1243
+ case "teammate":
1244
+ printDetailField("aiTeammateId", assignee.aiTeammateId, indent + 2);
1245
+ printDetailField("displayName", assignee.displayName, indent + 2);
1246
+ printDetailField("avatarUrl", assignee.avatarUrl, indent + 2);
1682
1247
  return;
1683
- }
1684
- const text = `${this.pendingText}
1685
- `;
1686
- this.pendingText = "";
1687
- await this.captureTranscriptText(text);
1688
- }
1689
- async captureTranscriptText(text) {
1690
- const redacted = redactText(text).text;
1691
- const events = serializeEventsFromTranscript(redacted, this.nextSeq, this.agent);
1692
- this.nextSeq = events.nextSeq;
1693
- await Promise.all(
1694
- this.outputs.map(async (output) => {
1695
- await output.appendTranscript(redacted);
1696
- await output.appendEvents(events.text);
1697
- })
1698
- );
1699
- }
1700
- directoryPayload(payload) {
1701
- const recording = isCompleteRecording(payload) ? this.sessionFile === null ? payload.recording : recordingFromSessionMatch(this.sessionMatch) : payload.recording;
1702
- const keepRedactionFailure = this.captureFailed || this.files().transcript.length === 0 && payload.redaction_failed;
1703
- const { redaction_failed: _redactionFailed, ...rest } = payload;
1704
- return {
1705
- ...rest,
1706
- ...keepRedactionFailure ? { redaction_failed: true } : {},
1707
- recording
1708
- };
1709
- }
1710
- files() {
1711
- const [output] = this.outputs;
1712
- if (output === void 0) {
1713
- return { events: [], transcript: [] };
1714
- }
1715
- return output.files();
1716
- }
1717
- matchOptions() {
1718
- return {
1719
- ...this.isSessionCandidate === void 0 ? {} : { isCandidate: this.isSessionCandidate },
1720
- ...this.newSessionFilesOnly ? { newFilesOnly: true } : {}
1721
- };
1722
- }
1723
- };
1724
- var CaptureDirectoryOutput = class {
1725
- transcript;
1726
- events;
1727
- constructor(outputDir) {
1728
- this.transcript = new JsonlStreamWriter(outputDir, "transcript", "transcript");
1729
- this.events = new JsonlStreamWriter(outputDir, "events", "events");
1730
- }
1731
- async appendTranscript(text) {
1732
- await this.transcript.appendText(text);
1733
- }
1734
- async appendEvents(text) {
1735
- await this.events.appendText(text);
1736
- }
1737
- async invalidate() {
1738
- await Promise.all([this.transcript.invalidate(), this.events.invalidate()]);
1739
- }
1740
- files() {
1741
- return {
1742
- events: this.events.files(),
1743
- transcript: this.transcript.files()
1744
- };
1745
- }
1746
- };
1747
- var JsonlStreamWriter = class {
1748
- outputDir;
1749
- dir;
1750
- prefix;
1751
- writtenFiles = [];
1752
- current = null;
1753
- constructor(outputDir, dir, prefix) {
1754
- this.outputDir = outputDir;
1755
- this.dir = dir;
1756
- this.prefix = prefix;
1757
- }
1758
- async appendText(text) {
1759
- for (const line of jsonlLines2(text)) {
1760
- await this.appendLine(line);
1761
- }
1762
- }
1763
- async invalidate() {
1764
- await Promise.all(
1765
- this.writtenFiles.map(async (file) => {
1766
- file.bytes = 0;
1767
- file.lines = 0;
1768
- file.oversized_line = false;
1769
- await writeFile2(join3(this.outputDir, file.path), "", "utf8");
1770
- })
1771
- );
1772
- this.current = null;
1773
- }
1774
- files() {
1775
- return this.writtenFiles.map((file) => ({ ...file }));
1776
- }
1777
- async appendLine(line) {
1778
- const lineBytes = Buffer.byteLength(line);
1779
- if (lineBytes > DATA_FILE_TARGET_BYTES2) {
1780
- this.current = null;
1781
- const file2 = this.createFile();
1782
- file2.bytes += lineBytes;
1783
- file2.lines += 1;
1784
- file2.oversized_line = true;
1785
- await appendFile(join3(this.outputDir, file2.path), line, "utf8");
1786
- this.current = null;
1787
- return;
1788
- }
1789
- if (this.current !== null && this.current.bytes + lineBytes > DATA_FILE_TARGET_BYTES2) {
1790
- this.current = null;
1791
- }
1792
- const file = this.current ?? this.createFile();
1793
- file.bytes += lineBytes;
1794
- file.lines += 1;
1795
- await appendFile(join3(this.outputDir, file.path), line, "utf8");
1796
- }
1797
- createFile() {
1798
- const index = String(this.writtenFiles.length + 1).padStart(6, "0");
1799
- const file = {
1800
- path: `${this.dir}/${this.prefix}-${index}.jsonl`,
1801
- bytes: 0,
1802
- lines: 0,
1803
- oversized_line: false
1804
- };
1805
- this.writtenFiles.push(file);
1806
- this.current = file;
1807
- return file;
1808
- }
1809
- };
1810
- function isCompleteRecording(payload) {
1811
- return payload.recording.found_chat && payload.recording.quality === "complete";
1812
- }
1813
- function jsonlLines2(text) {
1814
- if (text === "") {
1815
- return [];
1816
- }
1817
- const normalized = text.endsWith("\n") ? text : `${text}
1818
- `;
1819
- return normalized.slice(0, -1).split("\n").map((line) => `${line}
1820
- `);
1821
- }
1822
- async function readFileFromOffset(path2, offset) {
1823
- const file = await open(path2, "r");
1824
- try {
1825
- const stat2 = await file.stat();
1826
- const length = Math.max(0, stat2.size - offset);
1827
- if (length === 0) {
1828
- return { text: "", bytes: 0 };
1829
- }
1830
- const buffer2 = Buffer.alloc(length);
1831
- const result = await file.read(buffer2, 0, length, offset);
1832
- return { text: buffer2.subarray(0, result.bytesRead).toString("utf8"), bytes: result.bytesRead };
1833
- } finally {
1834
- await file.close();
1835
- }
1836
- }
1837
-
1838
- // src/run/moxt-uploader.ts
1839
- import { createHash } from "crypto";
1840
- import { readdir as readdir2, readFile as readFile2, stat, writeFile as writeFile3 } from "fs/promises";
1841
- import { arch as arch2, platform as platform2 } from "os";
1842
- import { join as join4, relative } from "path";
1843
-
1844
- // src/utils/cf-access.ts
1845
- function cloudflareAccessHeaders(env = process.env) {
1846
- const headers = {};
1847
- const token = firstValue(env.MOXT_CF_ACCESS_TOKEN, env.CF_ACCESS_TOKEN);
1848
- if (token !== void 0) {
1849
- headers["CF-Access-Token"] = token;
1850
- }
1851
- const clientId = firstValue(env.MOXT_CF_ACCESS_CLIENT_ID, env.CF_ACCESS_CLIENT_ID);
1852
- const clientSecret = firstValue(env.MOXT_CF_ACCESS_CLIENT_SECRET, env.CF_ACCESS_CLIENT_SECRET);
1853
- if (clientId !== void 0 && clientSecret !== void 0) {
1854
- headers["CF-Access-Client-Id"] = clientId;
1855
- headers["CF-Access-Client-Secret"] = clientSecret;
1856
- }
1857
- return headers;
1858
- }
1859
- function firstValue(...values) {
1860
- return values.find((value) => value !== void 0 && value !== "");
1861
- }
1862
-
1863
- // src/run/moxt-uploader.ts
1864
- var DEFAULT_HOST2 = "api.moxt.ai";
1865
- var DEFAULT_UPLOAD_TIMEOUT_MS = 3e4;
1866
- async function uploadRunArtifacts(options) {
1867
- const outputDirs = captureOutputDirectories(options.env, options.runId);
1868
- const failures = [];
1869
- for (const outputDir of outputDirs) {
1870
- try {
1871
- await uploadDirectory(outputDir, options.runId, options.upload, options.env);
1872
- } catch (error) {
1873
- failures.push(`${outputDir}: ${errorMessage2(error)}`);
1874
- }
1875
- }
1876
- if (failures.length > 0) {
1877
- throw new Error(failures.join("; "));
1878
- }
1879
- }
1880
- async function uploadDirectory(outputDir, runId, config, env) {
1881
- const files = await discoverUploadFiles(outputDir);
1882
- const state = await readUploadState(outputDir, runId);
1883
- for (const file of files) {
1884
- const previous = state.files[file.artifactPath];
1885
- if (previous?.status === "uploaded" && previous.sha256 === file.sha256 && previous.bytes === file.bytes) {
1886
- continue;
1887
- }
1888
- const attempts = (previous?.attempts ?? 0) + 1;
1889
- state.files[file.artifactPath] = {
1890
- status: "uploading",
1891
- bytes: file.bytes,
1892
- sha256: file.sha256,
1893
- attempts
1894
- };
1895
- state.status = "uploading";
1896
- state.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1897
- await writeUploadState(outputDir, state);
1898
- try {
1899
- const response = await uploadFile(runId, file, config, env);
1900
- state.files[file.artifactPath] = {
1901
- status: "uploaded",
1902
- bytes: file.bytes,
1903
- sha256: file.sha256,
1904
- attempts,
1905
- uploaded_at: (/* @__PURE__ */ new Date()).toISOString(),
1906
- ...response.etag === void 0 ? {} : { etag: response.etag }
1907
- };
1908
- } catch (error) {
1909
- state.files[file.artifactPath] = {
1910
- status: "failed",
1911
- bytes: file.bytes,
1912
- sha256: file.sha256,
1913
- attempts,
1914
- failed_at: (/* @__PURE__ */ new Date()).toISOString(),
1915
- error: errorMessage2(error)
1916
- };
1917
- }
1918
- state.status = uploadStatus(
1919
- state.files,
1920
- files.map((candidate) => candidate.artifactPath)
1921
- );
1922
- state.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1923
- await writeUploadState(outputDir, state);
1924
- }
1925
- state.status = uploadStatus(
1926
- state.files,
1927
- files.map((file) => file.artifactPath)
1248
+ case "unknown":
1249
+ printDetailField("humanId", assignee.humanId, indent + 2);
1250
+ printDetailField("aiAssistantId", assignee.aiAssistantId, indent + 2);
1251
+ printDetailField("aiTeammateId", assignee.aiTeammateId, indent + 2);
1252
+ }
1253
+ }
1254
+ function printStatusRemainingFields(item, indent) {
1255
+ printDetailField("name", item.name, indent);
1256
+ printDetailTextField("desc", item.desc, indent);
1257
+ printDetailField("type", `${item.type} (${statusTypeLabel(item.type)})`, indent);
1258
+ printAssigneeDetail("preferredAssignee", item.preferredAssignee, indent);
1259
+ printDetailField("order", item.order, indent);
1260
+ }
1261
+ function printStatusDetail(item) {
1262
+ print(`${colors.bold}Status${colors.reset}`);
1263
+ printDetailField("scopedId", item.scopedId);
1264
+ printStatusRemainingFields(item, 0);
1265
+ }
1266
+ function printWorkflowDetail(item) {
1267
+ print(`${colors.bold}Workflow${colors.reset}`);
1268
+ printDetailField("fileId", item.fileId);
1269
+ printDetailField("ownerHumanId", item.ownerHumanId);
1270
+ printDetailTextField("desc", item.desc);
1271
+ if (item.statusList.length === 0) {
1272
+ print("statusList: []");
1273
+ } else {
1274
+ print("statusList:");
1275
+ for (const status of item.statusList) {
1276
+ print(` - scopedId: ${status.scopedId}`);
1277
+ printStatusRemainingFields(status, 4);
1278
+ }
1279
+ }
1280
+ if (item.transitions.length === 0) {
1281
+ print("transitions: []");
1282
+ } else {
1283
+ print("transitions:");
1284
+ for (const transition of item.transitions) {
1285
+ print(` - statusFromScopedId: ${transition.statusFromScopedId}`);
1286
+ printDetailField("statusToScopedId", transition.statusToScopedId, 4);
1287
+ }
1288
+ }
1289
+ }
1290
+ function printTaskSummary(item) {
1291
+ const runIds = item.agentRuns.map((run) => run.pipelineTriggerId).join(",") || "-";
1292
+ print(
1293
+ `${colors.bold}#${item.scopedId}${colors.reset} ${item.title} status=${item.statusScopedId} priority=${priorityLabel(item.priority)} assignee=${formatAssignee(item.assignee)} runs=${runIds}`
1928
1294
  );
1929
- state.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1930
- await writeUploadState(outputDir, state);
1931
- if (state.status === "failed") {
1932
- throw new Error("one or more run files failed to upload");
1933
- }
1934
- }
1935
- async function discoverUploadFiles(outputDir) {
1936
- const paths = await uploadPaths(outputDir);
1937
- const files = await Promise.all(
1938
- paths.map(async (path2) => {
1939
- const absolute = join4(outputDir, path2);
1940
- const content = await readFile2(absolute);
1941
- return {
1942
- artifactPath: path2,
1943
- bytes: content.byteLength,
1944
- sha256: createHash("sha256").update(content).digest("hex"),
1945
- contentText: content.toString("utf8")
1946
- };
1947
- })
1948
- );
1949
- return files.sort((a, b) => a.artifactPath.localeCompare(b.artifactPath));
1950
1295
  }
1951
- async function uploadPaths(outputDir) {
1952
- const paths = [];
1953
- for (const name of ["run.json", "state.json"]) {
1954
- const absolute = join4(outputDir, name);
1955
- try {
1956
- const fileStat = await stat(absolute);
1957
- if (fileStat.isFile()) {
1958
- paths.push(name);
1959
- }
1960
- } catch {
1961
- }
1962
- }
1963
- for (const dir of ["events", "transcript"]) {
1964
- await collectFiles(outputDir, join4(outputDir, dir), paths);
1965
- }
1966
- return paths.sort();
1967
- }
1968
- async function collectFiles(outputDir, dir, paths) {
1969
- let entries;
1970
- try {
1971
- entries = await readdir2(dir, { withFileTypes: true });
1972
- } catch {
1973
- return;
1974
- }
1975
- for (const entry of entries) {
1976
- const absolute = join4(dir, entry.name);
1977
- if (entry.isDirectory()) {
1978
- await collectFiles(outputDir, absolute, paths);
1979
- continue;
1980
- }
1981
- if (!entry.isFile()) {
1982
- continue;
1983
- }
1984
- const fileStat = await stat(absolute);
1985
- if (fileStat.size === 0) {
1986
- continue;
1987
- }
1988
- paths.push(relative(outputDir, absolute).replaceAll("\\", "/"));
1989
- }
1990
- }
1991
- async function uploadFile(runId, file, config, env) {
1992
- const apiKey = env.MOXT_API_KEY;
1993
- if (apiKey === void 0 || apiKey === "") {
1994
- throw new Error("MOXT_API_KEY is required for run upload");
1995
- }
1996
- const timeoutMs = config.timeoutMs ?? DEFAULT_UPLOAD_TIMEOUT_MS;
1997
- const controller = new AbortController();
1998
- const timeout = setTimeout(() => {
1999
- controller.abort();
2000
- }, timeoutMs);
2001
- try {
2002
- const response = await fetch(
2003
- `${apiBaseUrl(env)}/workspaces/${encodeURIComponent(config.workspaceId)}/local-agent-runs/${encodeURIComponent(runId)}/files`,
2004
- {
2005
- method: "PUT",
2006
- headers: {
2007
- "Content-Type": "application/json",
2008
- Authorization: `Bearer ${apiKey}`,
2009
- "User-Agent": userAgent(),
2010
- ...cloudflareAccessHeaders(env)
2011
- },
2012
- signal: controller.signal,
2013
- body: JSON.stringify({
2014
- schema_version: "local_agent_run.upload_file.v1",
2015
- artifact_path: file.artifactPath,
2016
- bytes: file.bytes,
2017
- sha256: file.sha256,
2018
- encoding: "utf8",
2019
- content_text: file.contentText
2020
- })
2021
- }
1296
+ function printTaskDetail(item) {
1297
+ print(`${colors.bold}Task${colors.reset}`);
1298
+ printDetailField("scopedId", item.scopedId);
1299
+ printDetailField("workflowFileId", item.workflowFileId);
1300
+ printDetailField("ownerHumanId", item.ownerHumanId);
1301
+ printDetailField("title", item.title);
1302
+ printDetailTextField("desc", item.desc);
1303
+ printDetailField("statusScopedId", item.statusScopedId);
1304
+ printDetailField("priority", `${item.priority} (${priorityLabel(item.priority)})`);
1305
+ printAssigneeDetail("assignee", item.assignee);
1306
+ if (item.linkedFileIds.length === 0) {
1307
+ print("linkedFileIds: []");
1308
+ } else {
1309
+ print("linkedFileIds:");
1310
+ for (const fileId of item.linkedFileIds) print(` - ${fileId}`);
1311
+ }
1312
+ printDetailField("subscribed", item.subscribed);
1313
+ if (item.agentRuns.length === 0) {
1314
+ print("agentRuns: []");
1315
+ } else {
1316
+ print("agentRuns:");
1317
+ for (const run of item.agentRuns) {
1318
+ print(` - pipelineTriggerId: ${run.pipelineTriggerId}`);
1319
+ printAssigneeDetail("executor", run.executor, 4);
1320
+ }
1321
+ }
1322
+ printDetailField("createdAt", item.createdAt);
1323
+ printDetailField("latestActAt", item.latestActAt);
1324
+ }
1325
+ function printCommentDetail(item) {
1326
+ print(`${colors.bold}Comment${colors.reset}`);
1327
+ printDetailField("id", item.id);
1328
+ printDetailField("workflowFileId", item.workflowFileId);
1329
+ printDetailField("workflowTaskScopedId", item.workflowTaskScopedId);
1330
+ printDetailTextField("content", item.content);
1331
+ printAssigneeDetail("createdBy", item.createdBy);
1332
+ printDetailField("createdAt", item.createdAt);
1333
+ printDetailField("updatedAt", item.updatedAt);
1334
+ }
1335
+ function registerWorkflowCommand(program2) {
1336
+ const workflow = program2.command("workflow").description("Workflow operations");
1337
+ addWorkspaceOption(
1338
+ workflow.command("list").description("List accessible workflows").option("--scope <scope>", "Workflow scope: all or owner_is_me", "all")
1339
+ ).action(async (options) => {
1340
+ const scope = runOrExit3(() => parseWorkflowScope(options.scope));
1341
+ startSpinner("Fetching workflows...");
1342
+ const response = await httpGet(
1343
+ `/workspaces/${options.workspace}/workflows${buildQuery({ scope })}`
2022
1344
  );
2023
1345
  if (!response.ok) {
2024
- throw new Error(`HTTP ${response.status}`);
2025
- }
2026
- const headerEtag = response.headers.get("etag") ?? void 0;
2027
- const body = await responseJson(response);
2028
- const { etag: bodyEtagValue } = isObject2(body) ? body : {};
2029
- const bodyEtag = typeof bodyEtagValue === "string" ? bodyEtagValue : void 0;
2030
- const etag = bodyEtag ?? headerEtag;
2031
- return etag === void 0 ? {} : { etag };
2032
- } catch (error) {
2033
- if (controller.signal.aborted) {
2034
- throw new Error(`upload timed out after ${timeoutMs}ms`);
2035
- }
2036
- throw error;
2037
- } finally {
2038
- clearTimeout(timeout);
2039
- }
2040
- }
2041
- async function readUploadState(outputDir, runId) {
2042
- try {
2043
- const value = JSON.parse(await readFile2(join4(outputDir, "upload.json"), "utf8"));
2044
- if (isUploadState(value, runId)) {
2045
- return value;
2046
- }
2047
- } catch {
2048
- }
2049
- return {
2050
- schema_version: "runlog.upload.v1",
2051
- run_id: runId,
2052
- status: "pending",
2053
- updated_at: (/* @__PURE__ */ new Date()).toISOString(),
2054
- files: {}
2055
- };
2056
- }
2057
- async function writeUploadState(outputDir, state) {
2058
- await writeFile3(join4(outputDir, "upload.json"), `${JSON.stringify(state, null, 2)}
2059
- `, "utf8");
2060
- }
2061
- function uploadStatus(files, expectedPaths) {
2062
- if (expectedPaths.length === 0) {
2063
- return "pending";
2064
- }
2065
- const states = expectedPaths.map((path2) => files[path2]?.status ?? "pending");
2066
- if (states.some((status) => status === "failed")) {
2067
- return "failed";
2068
- }
2069
- if (states.every((status) => status === "uploaded")) {
2070
- return "uploaded";
2071
- }
2072
- if (states.some((status) => status === "uploaded")) {
2073
- return "partial";
2074
- }
2075
- return "uploading";
2076
- }
2077
- function isUploadState(value, runId) {
2078
- if (!isObject2(value)) {
2079
- return false;
2080
- }
2081
- const { schema_version: schemaVersion, run_id: valueRunId, files } = value;
2082
- return schemaVersion === "runlog.upload.v1" && valueRunId === runId && isObject2(files);
2083
- }
2084
- async function responseJson(response) {
2085
- try {
2086
- return await response.json();
2087
- } catch {
2088
- return null;
2089
- }
2090
- }
2091
- function apiBaseUrl(env) {
2092
- return `https://${env.MOXT_HOST ?? DEFAULT_HOST2}/openapi/v1`;
2093
- }
2094
- function userAgent() {
2095
- return `moxt-cli/${"0.3.3-moxt-run.1"} (${platform2()}/${arch2()}) node/${process.versions.node}`;
2096
- }
2097
- function errorMessage2(error) {
2098
- if (error instanceof Error && error.message !== "") {
2099
- return error.message;
2100
- }
2101
- return String(error);
2102
- }
2103
- function isObject2(value) {
2104
- return typeof value === "object" && value !== null && !Array.isArray(value);
2105
- }
2106
-
2107
- // src/run/process-session.ts
2108
- import { execFileSync } from "child_process";
2109
- import { isAbsolute, relative as relative2 } from "path";
2110
- function findOpenSessionFile(options) {
2111
- if ((options.platform ?? process.platform) !== "darwin") {
2112
- return { match: "none" };
2113
- }
2114
- try {
2115
- const psOutput = execFileSync("ps", ["-axo", "pid=,ppid="], {
2116
- encoding: "utf8",
2117
- stdio: ["ignore", "pipe", "ignore"]
2118
- });
2119
- const pids = processTreePids(options.rootPid, psOutput);
2120
- const lsofOutput = execFileSync("lsof", ["-F", "pn", "-p", pids.join(",")], {
2121
- encoding: "utf8",
2122
- stdio: ["ignore", "pipe", "ignore"]
2123
- });
2124
- return openSessionFileFromLsof(lsofOutput, {
2125
- sessionRoot: options.sessionRoot,
2126
- ...options.isCandidate === void 0 ? {} : { isCandidate: options.isCandidate }
2127
- });
2128
- } catch {
2129
- return { match: "none" };
2130
- }
2131
- }
2132
- function processTreePids(rootPid, psOutput) {
2133
- const childrenByParent = /* @__PURE__ */ new Map();
2134
- for (const line of psOutput.split("\n")) {
2135
- const [pidText, ppidText] = line.trim().split(/\s+/, 2);
2136
- const pid = Number(pidText);
2137
- const ppid = Number(ppidText);
2138
- if (!Number.isInteger(pid) || !Number.isInteger(ppid) || pid <= 0) {
2139
- continue;
2140
- }
2141
- const children = childrenByParent.get(ppid) ?? [];
2142
- children.push(pid);
2143
- childrenByParent.set(ppid, children);
2144
- }
2145
- const pids = [];
2146
- const seen = /* @__PURE__ */ new Set();
2147
- const queue = [rootPid];
2148
- while (queue.length > 0) {
2149
- const pid = queue.shift();
2150
- if (seen.has(pid)) {
2151
- continue;
2152
- }
2153
- seen.add(pid);
2154
- pids.push(pid);
2155
- queue.push(...childrenByParent.get(pid) ?? []);
2156
- }
2157
- return pids;
2158
- }
2159
- function openSessionFileFromLsof(lsofOutput, options) {
2160
- const files = /* @__PURE__ */ new Set();
2161
- for (const line of lsofOutput.split("\n")) {
2162
- if (!line.startsWith("n")) {
2163
- continue;
2164
- }
2165
- const path2 = line.slice(1);
2166
- if (!path2.endsWith(".jsonl") || !isInside(options.sessionRoot, path2)) {
2167
- continue;
2168
- }
2169
- if (options.isCandidate !== void 0 && !options.isCandidate(path2)) {
2170
- continue;
2171
- }
2172
- files.add(alignPathToRoot(options.sessionRoot, path2));
2173
- }
2174
- if (files.size === 0) {
2175
- return { match: "none" };
2176
- }
2177
- if (files.size === 1) {
2178
- return { match: "unique", sessionFile: [...files].join("") };
2179
- }
2180
- return { match: "ambiguous" };
2181
- }
2182
- function isInside(root, path2) {
2183
- if (isPathInside(root, path2)) {
2184
- return true;
2185
- }
2186
- return isPathInside(normalizeDarwinVarPath(root), normalizeDarwinVarPath(path2));
2187
- }
2188
- function isPathInside(root, path2) {
2189
- const fromRoot = relative2(root, path2);
2190
- return fromRoot === "" || !fromRoot.startsWith("..") && !isAbsolute(fromRoot);
2191
- }
2192
- function normalizeDarwinVarPath(path2) {
2193
- return path2.startsWith("/private/var/") ? path2.slice("/private".length) : path2;
2194
- }
2195
- function alignPathToRoot(root, path2) {
2196
- if (root.startsWith("/var/") && path2.startsWith("/private/var/")) {
2197
- return path2.slice("/private".length);
2198
- }
2199
- if (root.startsWith("/private/var/") && path2.startsWith("/var/")) {
2200
- return `/private${path2}`;
2201
- }
2202
- return path2;
2203
- }
2204
-
2205
- // src/run/repo.ts
2206
- import { execFileSync as execFileSync2 } from "child_process";
2207
- function collectRepoFingerprint(cwd) {
2208
- const root = git(["rev-parse", "--show-toplevel"], cwd);
2209
- if (root === null) {
2210
- return null;
2211
- }
2212
- return {
2213
- root,
2214
- remote: git(["config", "--get", "remote.origin.url"], cwd) ?? "",
2215
- branch: git(["branch", "--show-current"], cwd) ?? "",
2216
- head: git(["rev-parse", "HEAD"], cwd) ?? ""
2217
- };
2218
- }
2219
- function git(args, cwd) {
2220
- try {
2221
- return execFileSync2("git", args, {
2222
- cwd,
2223
- encoding: "utf8",
2224
- stdio: ["ignore", "pipe", "ignore"]
2225
- }).trim();
2226
- } catch {
2227
- return null;
2228
- }
2229
- }
2230
-
2231
- // src/run/session-dir.ts
2232
- import { closeSync, openSync, readSync, realpathSync } from "fs";
2233
- import { join as join5, resolve } from "path";
2234
- var CODEX_METADATA_READ_BYTES = 256 * 1024;
2235
- function claudeProjectDir(cwd, home) {
2236
- return join5(home, ".claude", "projects", encodeClaudeProjectPath(cwd));
2237
- }
2238
- function encodeClaudeProjectPath(cwd) {
2239
- return cwd.replaceAll(/[^A-Za-z0-9]/g, "-");
2240
- }
2241
- function agentSessionDir(options) {
2242
- switch (options.agent) {
2243
- case "claude":
2244
- return claudeProjectDir(options.cwd, options.home);
2245
- case "codex":
2246
- return codexSessionsDir(options.home, options.env);
2247
- }
2248
- }
2249
- function agentSessionCandidateFilter(options) {
2250
- switch (options.agent) {
2251
- case "claude":
2252
- return void 0;
2253
- case "codex": {
2254
- const expectedCwd = expectedCodexCwd(options.cwd, options.agentArgs);
2255
- return (path2) => {
2256
- try {
2257
- const sessionCwd = codexSessionCwd(path2);
2258
- return sessionCwd !== null && sameCodexCwd(sessionCwd, expectedCwd);
2259
- } catch {
2260
- return false;
2261
- }
2262
- };
2263
- }
2264
- }
2265
- }
2266
- function agentKnownSessionFile(options) {
2267
- switch (options.agent) {
2268
- case "claude": {
2269
- const sessionId = claudeSessionIdArg(options.agentArgs) ?? (claudeForkSessionArg(options.agentArgs) ? void 0 : claudeResumeSessionIdArg(options.agentArgs));
2270
- return sessionId === void 0 ? void 0 : join5(claudeProjectDir(options.cwd, options.home), `${sessionId}.jsonl`);
2271
- }
2272
- case "codex":
2273
- return void 0;
2274
- }
2275
- }
2276
- function codexSessionsDir(home, env = {}) {
2277
- return join5(codexHome(home, env), "sessions");
2278
- }
2279
- function codexHome(home, env = {}) {
2280
- const { CODEX_HOME: configured } = env;
2281
- return hasValue2(configured) ? configured : join5(home, ".codex");
2282
- }
2283
- function expectedCodexCwd(cwd, agentArgs) {
2284
- const cd = codexCdArg(agentArgs);
2285
- return cd === void 0 ? resolve(cwd) : resolve(cwd, cd);
2286
- }
2287
- function claudeSessionIdArg(agentArgs) {
2288
- const optionArgs = claudeOptionArgs(agentArgs);
2289
- for (let index = 0; index < optionArgs.length; index += 1) {
2290
- const arg = optionArgs[index];
2291
- if (arg === "--session-id") {
2292
- return optionArgs[index + 1];
2293
- }
2294
- if (arg.startsWith("--session-id=")) {
2295
- return arg.slice("--session-id=".length);
2296
- }
2297
- }
2298
- return void 0;
2299
- }
2300
- function claudeResumeSessionIdArg(agentArgs) {
2301
- const optionArgs = claudeOptionArgs(agentArgs);
2302
- for (let index = 0; index < optionArgs.length; index += 1) {
2303
- const arg = optionArgs[index];
2304
- if (arg === "--resume" || arg === "-r") {
2305
- const value = optionArgs[index + 1];
2306
- return isUuid(value) ? value : void 0;
2307
- }
2308
- if (arg.startsWith("--resume=")) {
2309
- const value = arg.slice("--resume=".length);
2310
- return isUuid(value) ? value : void 0;
2311
- }
2312
- }
2313
- return void 0;
2314
- }
2315
- function codexSessionCwd(path2) {
2316
- const text = readFilePrefix(path2, CODEX_METADATA_READ_BYTES);
2317
- for (const line of text.split("\n")) {
2318
- const cwd = codexLineCwd(line);
2319
- if (cwd !== null) {
2320
- return cwd;
2321
- }
2322
- }
2323
- return null;
2324
- }
2325
- function codexCdArg(agentArgs) {
2326
- const optionArgs = codexOptionArgs(agentArgs);
2327
- for (let index = 0; index < optionArgs.length; index += 1) {
2328
- const arg = optionArgs[index];
2329
- if (arg === "--cd" || arg === "-C") {
2330
- return optionArgs[index + 1];
2331
- }
2332
- if (arg.startsWith("--cd=")) {
2333
- return arg.slice("--cd=".length);
2334
- }
2335
- }
2336
- return void 0;
2337
- }
2338
- function codexOptionArgs(agentArgs) {
2339
- const separatorIndex = agentArgs.indexOf("--");
2340
- return separatorIndex === -1 ? agentArgs : agentArgs.slice(0, separatorIndex);
2341
- }
2342
- function sameCodexCwd(left, right) {
2343
- return normalizeCodexCwd(left) === normalizeCodexCwd(right);
2344
- }
2345
- function normalizeCodexCwd(path2) {
2346
- try {
2347
- return realpathSync(path2);
2348
- } catch {
2349
- return resolve(path2);
2350
- }
2351
- }
2352
- function claudeForkSessionArg(agentArgs) {
2353
- return claudeOptionArgs(agentArgs).includes("--fork-session");
2354
- }
2355
- function claudeOptionArgs(agentArgs) {
2356
- const separatorIndex = agentArgs.indexOf("--");
2357
- return separatorIndex === -1 ? agentArgs : agentArgs.slice(0, separatorIndex);
2358
- }
2359
- function isUuid(value) {
2360
- return value !== void 0 && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{12}$/i.test(value);
2361
- }
2362
- function codexLineCwd(line) {
2363
- if (line.trim() === "") {
2364
- return null;
2365
- }
2366
- let value;
2367
- try {
2368
- value = JSON.parse(line);
2369
- } catch {
2370
- return null;
2371
- }
2372
- const entry = rawCodexSessionLine(value);
2373
- if (entry === null) {
2374
- return null;
2375
- }
2376
- const type = entry.type;
2377
- if (type !== "session_meta" && type !== "turn_context") {
2378
- return null;
2379
- }
2380
- const payload = rawCodexSessionPayload(entry.payload);
2381
- const cwd = payload?.cwd;
2382
- return typeof cwd === "string" ? cwd : null;
2383
- }
2384
- function readFilePrefix(path2, bytes) {
2385
- const fd = openSync(path2, "r");
2386
- try {
2387
- const buffer2 = Buffer.alloc(bytes);
2388
- const read = readSync(fd, buffer2, 0, bytes, 0);
2389
- return buffer2.subarray(0, read).toString("utf8");
2390
- } finally {
2391
- closeSync(fd);
2392
- }
2393
- }
2394
- function objectValue2(value) {
2395
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
2396
- }
2397
- function rawCodexSessionLine(value) {
2398
- return objectValue2(value);
2399
- }
2400
- function rawCodexSessionPayload(value) {
2401
- return objectValue2(value);
2402
- }
2403
- function hasValue2(value) {
2404
- return value !== void 0 && value !== "";
2405
- }
2406
-
2407
- // src/run/run-agent.ts
2408
- var MAX_TRANSCRIPT_CAPTURE_BYTES = 1024 * 1024;
2409
- var SESSION_SETTLE_TIMEOUT_MS = 1500;
2410
- var SESSION_SETTLE_INTERVAL_MS = 100;
2411
- var PROCESS_SESSION_POLL_INTERVAL_MS = 100;
2412
- var CODEX_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
2413
- "-a",
2414
- "--add-dir",
2415
- "--ask-for-approval",
2416
- "-c",
2417
- "-C",
2418
- "--cd",
2419
- "--color",
2420
- "--config",
2421
- "-i",
2422
- "--image",
2423
- "--local-provider",
2424
- "-m",
2425
- "--model",
2426
- "-o",
2427
- "--output-last-message",
2428
- "--output-schema",
2429
- "-p",
2430
- "--profile",
2431
- "--remote",
2432
- "--remote-auth-token-env",
2433
- "-s",
2434
- "--sandbox"
2435
- ]);
2436
- async function runLocalAgent(options) {
2437
- const cwd = options.cwd ?? process.cwd();
2438
- const env = options.env ?? process.env;
2439
- const home = options.home ?? env.HOME ?? homedir2();
2440
- const startedAtMs = Date.now();
2441
- const startedAt = new Date(startedAtMs).toISOString();
2442
- const runId = createCaptureRunId(startedAt, options.agent);
2443
- const captureEnv = withDefaultCaptureOutput(env, home);
2444
- try {
2445
- await prepareCaptureOutputs(captureEnv, runId);
2446
- } catch (error) {
2447
- printPrepareError(options.agent, error);
2448
- return 1;
2449
- }
2450
- const repo = collectRepoFingerprint(cwd);
2451
- let baselineOk = false;
2452
- let before = /* @__PURE__ */ new Map();
2453
- let sessions = "";
2454
- let isSessionCandidate;
2455
- let knownSessionFile;
2456
- let childRootPid;
2457
- let openSessionFileMatch = { match: "none" };
2458
- let processSessionTimer;
2459
- const newSessionFilesOnly = shouldUseNewSessionFilesOnly(options.agent, options.agentArgs);
2460
- try {
2461
- const sessionOptions = {
2462
- agent: options.agent,
2463
- cwd,
2464
- home,
2465
- env: captureEnv,
2466
- agentArgs: options.agentArgs
2467
- };
2468
- sessions = agentSessionDir(sessionOptions);
2469
- isSessionCandidate = agentSessionCandidateFilter(sessionOptions);
2470
- knownSessionFile = agentKnownSessionFile(sessionOptions);
2471
- before = snapshotJsonl(sessions);
2472
- baselineOk = true;
2473
- } catch {
2474
- baselineOk = false;
2475
- }
2476
- const rememberOpenSessionFile = () => {
2477
- if (!baselineOk || sessions === "" || childRootPid === void 0 || openSessionFileMatch.match !== "none") {
2478
- return openSessionFileMatch;
2479
- }
2480
- const matched = findOpenSessionFile({
2481
- rootPid: childRootPid,
2482
- sessionRoot: sessions,
2483
- ...isSessionCandidate === void 0 ? {} : { isCandidate: isSessionCandidate }
2484
- });
2485
- if (matched.match !== "none") {
2486
- openSessionFileMatch = matched;
2487
- }
2488
- return openSessionFileMatch;
2489
- };
2490
- const stopProcessSessionWatcher = () => {
2491
- if (processSessionTimer !== void 0) {
2492
- clearInterval(processSessionTimer);
2493
- processSessionTimer = void 0;
2494
- }
2495
- };
2496
- const liveCapture = createLiveCapture({
2497
- env: captureEnv,
2498
- runId,
2499
- agent: options.agent,
2500
- startedAt,
2501
- sessions,
2502
- before,
2503
- baselineOk,
2504
- startedAtMs,
2505
- ...knownSessionFile === void 0 ? {} : { knownSessionFile },
2506
- ...isSessionCandidate === void 0 ? {} : { isSessionCandidate },
2507
- ...newSessionFilesOnly ? { newSessionFilesOnly: true } : {},
2508
- openSessionFile: rememberOpenSessionFile
2509
- });
2510
- try {
2511
- await liveCapture.start();
2512
- } catch (error) {
2513
- printError(
2514
- [
2515
- "moxt: cannot prepare run output",
2516
- `reason: ${errorMessage3(error)}`,
2517
- "",
2518
- `${options.agent} was not started because this run cannot be recorded.`
2519
- ].join("\n")
2520
- );
2521
- return 1;
2522
- }
2523
- const observedSignals = /* @__PURE__ */ new Set();
2524
- let child = null;
2525
- let pendingForward = null;
2526
- const observe = (signal) => {
2527
- const number = signalNumber(signal);
2528
- if (number !== 0) observedSignals.add(number);
2529
- };
2530
- const terminalSignalsReachChild = process.stdin.isTTY === true || process.stdout.isTTY === true || process.stderr.isTTY === true;
2531
- const forwardInteractiveSignal = (signal) => {
2532
- observe(signal);
2533
- if (terminalSignalsReachChild) {
2534
- return;
1346
+ stopSpinner(false, "Failed");
1347
+ failResponse(response);
2535
1348
  }
2536
- if (child === null) {
2537
- pendingForward = signal;
1349
+ if (response.data.workflows.length === 0) {
1350
+ stopSpinner(true, "No workflows found.");
2538
1351
  return;
2539
1352
  }
2540
- safeKill(child, signal);
2541
- };
2542
- const onInt = () => forwardInteractiveSignal("SIGINT");
2543
- const onQuit = () => forwardInteractiveSignal("SIGQUIT");
2544
- const onTerm = () => {
2545
- observe("SIGTERM");
2546
- if (child === null) pendingForward = "SIGTERM";
2547
- else safeKill(child, "SIGTERM");
2548
- };
2549
- const onHup = () => {
2550
- observe("SIGHUP");
2551
- if (child === null) pendingForward = "SIGHUP";
2552
- else safeKill(child, "SIGHUP");
2553
- };
2554
- process.on("SIGINT", onInt);
2555
- process.on("SIGQUIT", onQuit);
2556
- process.on("SIGTERM", onTerm);
2557
- process.on("SIGHUP", onHup);
2558
- const spawned = spawn(options.agent, [...options.agentArgs], {
2559
- cwd,
2560
- env: captureEnv,
2561
- stdio: "inherit"
2562
- });
2563
- child = spawned;
2564
- childRootPid = spawned.pid;
2565
- if (childRootPid !== void 0 && baselineOk) {
2566
- rememberOpenSessionFile();
2567
- processSessionTimer = setInterval(() => {
2568
- rememberOpenSessionFile();
2569
- }, PROCESS_SESSION_POLL_INTERVAL_MS);
2570
- }
2571
- if (pendingForward !== null) {
2572
- safeKill(spawned, pendingForward);
2573
- }
2574
- const exit = await waitForExit(spawned, options.agent);
2575
- stopProcessSessionWatcher();
2576
- const onCaptureSignal = () => {
2577
- };
2578
- process.on("SIGINT", onCaptureSignal);
2579
- process.on("SIGQUIT", onCaptureSignal);
2580
- process.on("SIGTERM", onCaptureSignal);
2581
- process.on("SIGHUP", onCaptureSignal);
2582
- process.off("SIGINT", onInt);
2583
- process.off("SIGQUIT", onQuit);
2584
- process.off("SIGTERM", onTerm);
2585
- process.off("SIGHUP", onHup);
2586
- const classified = classifyExit(exit, observedSignals);
2587
- const endedAtMs = exit.waitAt;
2588
- const endedAt = new Date(endedAtMs).toISOString();
2589
- const durationMs = endedAtMs - startedAtMs;
2590
- let after;
2591
- if (baselineOk) {
2592
- try {
2593
- after = await waitForSessionSettle(
2594
- sessions,
2595
- before,
2596
- startedAtMs,
2597
- isSessionCandidate,
2598
- newSessionFilesOnly
2599
- );
2600
- } catch {
2601
- }
2602
- }
2603
- try {
2604
- await liveCapture.stop();
2605
- } catch {
2606
- }
2607
- const payload = buildPayload({
2608
- agent: options.agent,
2609
- cwd,
2610
- startedAt,
2611
- endedAt,
2612
- durationMs,
2613
- exitCode: classified.code,
2614
- status: classified.status,
2615
- repo: sanitizeRepo(repo),
2616
- baselineOk,
2617
- sessions,
2618
- before,
2619
- after,
2620
- startedAtMs,
2621
- knownSessionFile,
2622
- isSessionCandidate,
2623
- newSessionFilesOnly,
2624
- openSessionFileMatch
2625
- });
2626
- let finalized = false;
2627
- try {
2628
- if (liveCapture.enabled) {
2629
- await liveCapture.finalize(payload, (/* @__PURE__ */ new Date()).toISOString());
2630
- } else {
2631
- await writeCaptureDirectoryPayload(payload, captureEnv, runId, (/* @__PURE__ */ new Date()).toISOString());
2632
- }
2633
- finalized = true;
2634
- } catch (error) {
2635
- printError(`moxt: cannot finalize run artifact: ${errorMessage3(error)}`);
2636
- } finally {
2637
- process.off("SIGINT", onCaptureSignal);
2638
- process.off("SIGQUIT", onCaptureSignal);
2639
- process.off("SIGTERM", onCaptureSignal);
2640
- process.off("SIGHUP", onCaptureSignal);
2641
- }
2642
- if (finalized && options.upload !== void 0) {
2643
- try {
2644
- await uploadRunArtifacts({
2645
- env: captureEnv,
2646
- runId,
2647
- upload: options.upload
2648
- });
2649
- } catch (error) {
2650
- printError(`moxt: cannot upload run artifact: ${errorMessage3(error)}`);
2651
- }
2652
- }
2653
- return classified.code;
2654
- }
2655
- async function waitForSessionSettle(sessions, before, startedAtMs, isSessionCandidate, newSessionFilesOnly) {
2656
- const deadline = Date.now() + SESSION_SETTLE_TIMEOUT_MS;
2657
- let after = snapshotJsonl(sessions);
2658
- while (!hasSessionChange(before, after, startedAtMs, isSessionCandidate, newSessionFilesOnly) && Date.now() < deadline) {
2659
- await sleep(SESSION_SETTLE_INTERVAL_MS);
2660
- after = snapshotJsonl(sessions);
2661
- }
2662
- return after;
2663
- }
2664
- function hasSessionChange(before, after, startedAtMs, isSessionCandidate, newSessionFilesOnly) {
2665
- const matched = matchSession(before, after, startedAtMs, matchOptions(isSessionCandidate, newSessionFilesOnly));
2666
- if (matched.match !== "unique") {
2667
- return matched.match !== "none";
2668
- }
2669
- if (matched.sessionFile === null) {
2670
- return false;
2671
- }
2672
- const snapshot = after.get(matched.sessionFile);
2673
- return snapshot !== void 0 && snapshot.size > matched.startOffset;
2674
- }
2675
- function sleep(ms) {
2676
- return new Promise((resolve2) => setTimeout(resolve2, ms));
2677
- }
2678
- function safeKill(child, signal) {
2679
- try {
2680
- child.kill(signal);
2681
- } catch {
2682
- }
2683
- }
2684
- function waitForExit(child, agent) {
2685
- return new Promise((resolve2) => {
2686
- child.once("error", (error) => {
2687
- const errorCode = error.code ?? "UNKNOWN";
2688
- const reason = errorCode === "ENOENT" ? "command not found" : error.message;
2689
- printError(`moxt: cannot start ${agent}: ${reason}`);
2690
- resolve2({
2691
- code: null,
2692
- signal: null,
2693
- waitAt: Date.now(),
2694
- errorCode
2695
- });
2696
- });
2697
- child.once("exit", (code, signal) => {
2698
- resolve2({ code, signal, waitAt: Date.now() });
2699
- });
2700
- });
2701
- }
2702
- function buildPayload(input) {
2703
- let sessionMatch = input.baselineOk ? "none" : "prep_failed";
2704
- let transcript;
2705
- let redactionFailed = false;
2706
- if (input.baselineOk) {
2707
- try {
2708
- const after = input.after ?? snapshotJsonl(input.sessions);
2709
- const knownMatched = input.knownSessionFile === void 0 ? null : matchKnownSessionFile(input.before, after, input.knownSessionFile);
2710
- const matched = knownMatched?.match === "unique" ? knownMatched : input.openSessionFileMatch.match === "unique" ? matchKnownSessionFile(input.before, after, input.openSessionFileMatch.sessionFile) : input.openSessionFileMatch.match === "ambiguous" ? {
2711
- match: "ambiguous",
2712
- sessionFile: null,
2713
- startOffset: 0
2714
- } : matchSession(
2715
- input.before,
2716
- after,
2717
- input.startedAtMs,
2718
- matchOptions(input.isSessionCandidate, input.newSessionFilesOnly)
2719
- );
2720
- sessionMatch = matched.match;
2721
- if (matched.match === "unique" && matched.sessionFile !== null) {
2722
- try {
2723
- transcript = redactText(readFileFromOffset2(matched.sessionFile, matched.startOffset)).text;
2724
- } catch {
2725
- redactionFailed = true;
2726
- }
2727
- }
2728
- } catch {
2729
- sessionMatch = "prep_failed";
2730
- }
2731
- }
2732
- return {
2733
- ...transcript === void 0 ? {} : { transcript_redacted: transcript },
2734
- ...redactionFailed ? { redaction_failed: true } : {},
2735
- run: {
2736
- agent: input.agent,
2737
- cwd: input.cwd,
2738
- git_root: input.repo?.root ?? null,
2739
- started_at: input.startedAt,
2740
- ended_at: input.endedAt,
2741
- duration_ms: input.durationMs,
2742
- status: input.status,
2743
- exit_code: input.exitCode
2744
- },
2745
- recording: recordingFromSessionMatch(sessionMatch),
2746
- repo: input.repo
2747
- };
2748
- }
2749
- function matchOptions(isSessionCandidate, newSessionFilesOnly) {
2750
- return {
2751
- ...isSessionCandidate === void 0 ? {} : { isCandidate: isSessionCandidate },
2752
- ...newSessionFilesOnly ? { newFilesOnly: true } : {}
2753
- };
2754
- }
2755
- function shouldUseNewSessionFilesOnly(agent, agentArgs) {
2756
- return agent === "codex" && !isCodexResumeInvocation(agentArgs);
2757
- }
2758
- function isCodexResumeInvocation(agentArgs) {
2759
- const command = nextCodexCommandArg(agentArgs, 0);
2760
- if (command === null) {
2761
- return false;
2762
- }
2763
- if (command.value === "resume") {
2764
- return true;
2765
- }
2766
- if (command.value !== "exec" && command.value !== "e") {
2767
- return false;
2768
- }
2769
- return nextCodexCommandArg(agentArgs, command.index + 1)?.value === "resume";
2770
- }
2771
- function nextCodexCommandArg(agentArgs, startIndex) {
2772
- for (let index = startIndex; index < agentArgs.length; index += 1) {
2773
- const arg = agentArgs[index];
2774
- if (arg === void 0) {
2775
- continue;
2776
- }
2777
- if (arg === "--") {
2778
- return null;
2779
- }
2780
- if (arg.startsWith("-")) {
2781
- if (!arg.includes("=") && CODEX_OPTIONS_WITH_VALUE.has(arg)) {
2782
- index += 1;
2783
- }
2784
- continue;
2785
- }
2786
- return { value: arg, index };
2787
- }
2788
- return null;
2789
- }
2790
- function readFileFromOffset2(path2, offset) {
2791
- const fd = openSync2(path2, "r");
2792
- try {
2793
- const stat2 = fstatSync(fd);
2794
- const length = Math.max(0, stat2.size - offset);
2795
- if (length > MAX_TRANSCRIPT_CAPTURE_BYTES) {
2796
- throw new Error("transcript capture exceeds maximum size");
2797
- }
2798
- const buffer2 = Buffer.alloc(length);
2799
- if (length === 0) {
2800
- return "";
2801
- }
2802
- readSync2(fd, buffer2, 0, length, offset);
2803
- return buffer2.toString("utf8");
2804
- } finally {
2805
- closeSync2(fd);
2806
- }
2807
- }
2808
- function printPrepareError(agent, error) {
2809
- if (error instanceof CaptureOutputPrepareError) {
2810
- printError(
2811
- [
2812
- "moxt: cannot create run output directory",
2813
- `path: ${error.path}`,
2814
- `reason: ${error.message}`,
2815
- "",
2816
- `${agent} was not started because this run cannot be recorded.`
2817
- ].join("\n")
2818
- );
2819
- return;
2820
- }
2821
- printError(
2822
- [
2823
- "moxt: cannot prepare run output",
2824
- `reason: ${errorMessage3(error)}`,
2825
- "",
2826
- `${agent} was not started because this run cannot be recorded.`
2827
- ].join("\n")
2828
- );
2829
- }
2830
- function errorMessage3(error) {
2831
- if (error instanceof Error && error.message !== "") {
2832
- return error.message;
2833
- }
2834
- return String(error);
2835
- }
2836
-
2837
- // src/commands/run.ts
2838
- function registerRunCommand(program2) {
2839
- program2.command("run").description("Run a local agent and capture its work as Moxt context").option("-w, --workspace <workspaceId>", "Workspace ID for upload mode").argument("<agent>", "Local agent to run: claude or codex").argument("[agentArgs...]", "Arguments passed to the local agent after --").allowUnknownOption(true).allowExcessArguments(true).action(async (agentArg, agentArgs, options) => {
2840
- const agent = parseAgent(agentArg);
2841
- const upload = resolveUploadOptions(options);
2842
- const exitCode = await runLocalAgentOrExit(agent, agentArgs ?? [], upload);
2843
- process.exit(exitCode);
1353
+ stopSpinner(true, `Found ${response.data.workflows.length} workflow(s)`);
1354
+ for (const item of response.data.workflows) printWorkflow(item);
2844
1355
  });
2845
- }
2846
- function parseAgent(agent) {
2847
- if (agent === "claude" || agent === "codex") {
2848
- return agent;
2849
- }
2850
- printError(`Error: moxt run supports only "claude" or "codex", got "${agent}".`);
2851
- process.exit(1);
2852
- }
2853
- function resolveUploadOptions(options) {
2854
- const workspaceId = resolveWorkspaceId(options);
2855
- if (workspaceId === void 0) {
2856
- return void 0;
2857
- }
2858
- if (!process.env.MOXT_API_KEY) {
2859
- printError("Error: MOXT_API_KEY is required when using moxt run upload mode with -w/--workspace or MOXT_WORKSPACE_ID.");
2860
- process.exit(1);
2861
- }
2862
- return {
2863
- workspaceId
2864
- };
2865
- }
2866
- function resolveWorkspaceId(options) {
2867
- return normalizeWorkspaceId(options.workspace) ?? normalizeWorkspaceId(process.env.MOXT_WORKSPACE_ID);
2868
- }
2869
- function normalizeWorkspaceId(value) {
2870
- const trimmed = value?.trim();
2871
- return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
2872
- }
2873
- async function runLocalAgentOrExit(agent, agentArgs, upload) {
2874
- return await runLocalAgent({
2875
- agent,
2876
- agentArgs,
2877
- ...upload === void 0 ? {} : { upload }
2878
- });
2879
- }
2880
-
2881
- // src/telemetry/config.ts
2882
- import * as fs2 from "fs";
2883
- import * as os from "os";
2884
- import * as path from "path";
2885
- function getConfigPath() {
2886
- return path.join(os.homedir(), ".moxt", "config.json");
2887
- }
2888
- function readTelemetryConfig() {
2889
- try {
2890
- const raw = fs2.readFileSync(getConfigPath(), "utf8");
2891
- const parsed = JSON.parse(raw);
2892
- return parsed.telemetry ?? {};
2893
- } catch {
2894
- return {};
2895
- }
2896
- }
2897
- function writeTelemetryConfig(update) {
2898
- const configPath = getConfigPath();
2899
- let existing = {};
2900
- try {
2901
- existing = JSON.parse(fs2.readFileSync(configPath, "utf8"));
2902
- } catch {
2903
- existing = {};
2904
- }
2905
- const currentTelemetry = existing.telemetry ?? {};
2906
- const next = { ...existing, telemetry: { ...currentTelemetry, ...update } };
2907
- fs2.mkdirSync(path.dirname(configPath), { recursive: true });
2908
- fs2.writeFileSync(configPath, JSON.stringify(next, null, 2));
2909
- }
2910
-
2911
- // src/telemetry/opt-out.ts
2912
- function isCiEnvironment() {
2913
- return process.env.CI === "true" || process.env.CI === "1";
2914
- }
2915
- function isTelemetryDisabled() {
2916
- if (process.env.MOXT_TELEMETRY_DISABLED === "1" || process.env.MOXT_TELEMETRY_DISABLED === "true") {
2917
- return true;
2918
- }
2919
- if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true") {
2920
- return true;
2921
- }
2922
- return readTelemetryConfig().disabled === true;
2923
- }
2924
-
2925
- // src/commands/telemetry.ts
2926
- function registerTelemetryCommand(program2) {
2927
- const telemetry = program2.command("telemetry").description("Manage Moxt CLI telemetry");
2928
- telemetry.command("status").description("Show telemetry status").action(() => {
2929
- const cfg = readTelemetryConfig();
2930
- const envOverride = process.env.MOXT_TELEMETRY_DISABLED === "1" || process.env.MOXT_TELEMETRY_DISABLED === "true" || process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true";
2931
- const state = isTelemetryDisabled() ? "disabled" : "enabled";
2932
- print(`telemetry: ${state}`);
2933
- if (envOverride) {
2934
- print("(disabled via environment variable)");
2935
- } else if (cfg.disabled) {
2936
- print("(disabled via config file)");
1356
+ addSpaceOptions2(
1357
+ workflow.command("create").description("Create a blank workflow").requiredOption("-p, --path <path>", "Target .workflow path").option("--desc-file <localPath>", "Local UTF-8 text file for workflow description").option("--owner-human-id <humanId>", "Workflow owner human ID")
1358
+ ).action(async (options) => {
1359
+ const body = runOrExit3(() => {
1360
+ const next = {
1361
+ path: options.path,
1362
+ ...resolveSpaceSelector(options)
1363
+ };
1364
+ const desc = readDescFile(options.descFile);
1365
+ if (desc !== void 0) next.desc = desc;
1366
+ const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
1367
+ if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
1368
+ return next;
1369
+ });
1370
+ startSpinner("Creating workflow...");
1371
+ const response = await httpPost(`/workspaces/${options.workspace}/workflows`, body);
1372
+ if (!response.ok) {
1373
+ stopSpinner(false, "Failed");
1374
+ failResponse(response);
2937
1375
  }
1376
+ stopSpinner(true, "Workflow created");
1377
+ printWorkflow(response.data);
2938
1378
  });
2939
- telemetry.command("enable").description("Enable telemetry").action(() => {
2940
- writeTelemetryConfig({ disabled: false });
2941
- print("telemetry: enabled");
1379
+ addWorkspaceOption(workflow.command("get").description("Get a workflow").argument("<workflowFileId>")).action(
1380
+ async (workflowFileId, options) => {
1381
+ startSpinner("Fetching workflow...");
1382
+ const response = await httpGet(
1383
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}`
1384
+ );
1385
+ if (!response.ok) {
1386
+ stopSpinner(false, "Failed");
1387
+ failResponse(response);
1388
+ }
1389
+ stopSpinner(true, "Workflow fetched");
1390
+ printWorkflowDetail(response.data);
1391
+ }
1392
+ );
1393
+ addWorkspaceOption(
1394
+ workflow.command("update").description("Update workflow metadata").argument("<workflowFileId>").option("--desc-file <localPath>", "Local UTF-8 text file for workflow description").option("--owner-human-id <humanId>", "Workflow owner human ID")
1395
+ ).action(async (workflowFileId, options) => {
1396
+ const body = runOrExit3(() => {
1397
+ const next = {};
1398
+ const desc = readDescFile(options.descFile);
1399
+ if (desc !== void 0) next.desc = desc;
1400
+ const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
1401
+ if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
1402
+ assertNonEmptyBody(next);
1403
+ return next;
1404
+ });
1405
+ startSpinner("Updating workflow...");
1406
+ const response = await httpPatch(`/workspaces/${options.workspace}/workflows/${workflowFileId}`, body);
1407
+ if (!response.ok) {
1408
+ stopSpinner(false, "Failed");
1409
+ failResponse(response);
1410
+ }
1411
+ stopSpinner(true, "Workflow updated");
1412
+ printWorkflow(response.data);
2942
1413
  });
2943
- telemetry.command("disable").description("Disable telemetry").action(() => {
2944
- writeTelemetryConfig({ disabled: true });
2945
- print("telemetry: disabled");
1414
+ addWorkspaceOption(
1415
+ workflow.command("delete").description("Delete a workflow").argument("<workflowFileId>").option("--message <message>", "Commit message")
1416
+ ).action(async (workflowFileId, options) => {
1417
+ startSpinner("Deleting workflow...");
1418
+ const response = await httpDelete(
1419
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}`,
1420
+ options.message ? { message: options.message } : {}
1421
+ );
1422
+ if (!response.ok) {
1423
+ stopSpinner(false, "Failed");
1424
+ failResponse(response);
1425
+ }
1426
+ stopSpinner(true, `Deleted: ${response.data.fileId}`);
2946
1427
  });
2947
- }
2948
-
2949
- // src/commands/whoami.ts
2950
- function registerWhoamiCommand(program2) {
2951
- program2.command("whoami").description("Show current user information").action(async () => {
2952
- startSpinner("Fetching user info...");
2953
- const response = await httpGet("/users/whoami");
2954
- if (response.ok) {
2955
- stopSpinner(true, "Done");
2956
- print(`${colors.bold}Nickname${colors.reset}: ${response.data.nickname}`);
2957
- print(`${colors.bold}Email${colors.reset}: ${response.data.email}`);
2958
- } else {
1428
+ addWorkspaceOption(workflow.command("duplicate").description("Duplicate a workflow").argument("<workflowFileId>")).action(
1429
+ async (workflowFileId, options) => {
1430
+ startSpinner("Duplicating workflow...");
1431
+ const response = await httpPost(
1432
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/duplicate`,
1433
+ {}
1434
+ );
1435
+ if (!response.ok) {
1436
+ stopSpinner(false, "Failed");
1437
+ failResponse(response);
1438
+ }
1439
+ stopSpinner(true, "Workflow duplicated");
1440
+ printWorkflow(response.data);
1441
+ }
1442
+ );
1443
+ registerStatusCommands(workflow);
1444
+ registerTransitionCommands(workflow);
1445
+ registerTaskCommands(workflow);
1446
+ registerCommentCommands(workflow);
1447
+ registerRunCommands(workflow);
1448
+ }
1449
+ function registerStatusCommands(workflow) {
1450
+ const status = workflow.command("status").description("Workflow status operations");
1451
+ addPreferredAssigneeOptions(
1452
+ addWorkspaceOption(
1453
+ status.command("create").description("Create a workflow status").argument("<workflowFileId>").requiredOption("--name <name>", "Status name").requiredOption("--type <type>", "Status type value: 1=todo, 2=in-progress, 3=done").option("--desc-file <localPath>", "Local UTF-8 text file for status description")
1454
+ )
1455
+ ).action(
1456
+ async (workflowFileId, options) => {
1457
+ const body = runOrExit3(() => {
1458
+ const next = {
1459
+ name: options.name,
1460
+ type: parseRequiredStatusType(options.type)
1461
+ };
1462
+ const desc = readDescFile(options.descFile);
1463
+ if (desc !== void 0) next.desc = desc;
1464
+ Object.assign(next, parsePreferredAssigneeBody(options));
1465
+ return next;
1466
+ });
1467
+ startSpinner("Creating status...");
1468
+ const response = await httpPost(
1469
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses`,
1470
+ body
1471
+ );
1472
+ if (!response.ok) {
1473
+ stopSpinner(false, "Failed");
1474
+ failResponse(response);
1475
+ }
1476
+ stopSpinner(true, "Status created");
1477
+ printStatusDetail(response.data);
1478
+ }
1479
+ );
1480
+ addPreferredAssigneeOptions(
1481
+ addWorkspaceOption(
1482
+ status.command("update").description("Update a workflow status").argument("<workflowFileId>").argument("<statusScopedId>").option("--name <name>", "Status name").option("--type <type>", "Status type value: 1=todo, 2=in-progress, 3=done").option("--desc-file <localPath>", "Local UTF-8 text file for status description").option("--clear-desc", "Clear status description").option("--clear-preferred-assignee", "Clear preferred assignee")
1483
+ )
1484
+ ).action(
1485
+ async (workflowFileId, statusScopedId, options) => {
1486
+ const body = runOrExit3(() => {
1487
+ if (options.descFile && options.clearDesc) {
1488
+ throw new WorkflowOptionsError("Error: --desc-file cannot be used with --clear-desc.");
1489
+ }
1490
+ const next = {};
1491
+ if (options.name) next.name = options.name;
1492
+ const type = parseStatusType(options.type);
1493
+ if (type !== void 0) next.type = type;
1494
+ const desc = readDescFile(options.descFile);
1495
+ if (desc !== void 0) next.desc = desc;
1496
+ if (options.clearDesc) next.desc = null;
1497
+ Object.assign(next, parsePreferredAssigneeBody(options));
1498
+ assertNonEmptyBody(next);
1499
+ return next;
1500
+ });
1501
+ const id = runOrExit3(() => parsePositiveInteger(statusScopedId, "statusScopedId"));
1502
+ startSpinner("Updating status...");
1503
+ const response = await httpPatch(
1504
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}`,
1505
+ body
1506
+ );
1507
+ if (!response.ok) {
1508
+ stopSpinner(false, "Failed");
1509
+ failResponse(response);
1510
+ }
1511
+ stopSpinner(true, "Status updated");
1512
+ printStatusDetail(response.data);
1513
+ }
1514
+ );
1515
+ addWorkspaceOption(
1516
+ status.command("move-after").description("Move a workflow status after another status").argument("<workflowFileId>").argument("<statusScopedId>").option("--after <statusScopedId>", "Anchor status scoped ID").option("--first", "Move to first position")
1517
+ ).action(
1518
+ async (workflowFileId, statusScopedId, options) => {
1519
+ const { id, anchoredStatusScopedId } = runOrExit3(() => {
1520
+ if (options.after && options.first) {
1521
+ throw new WorkflowOptionsError("Error: --after cannot be used with --first.");
1522
+ }
1523
+ if (!options.after && !options.first) {
1524
+ throw new WorkflowOptionsError("Error: either --after or --first is required.");
1525
+ }
1526
+ return {
1527
+ id: parsePositiveInteger(statusScopedId, "statusScopedId"),
1528
+ anchoredStatusScopedId: options.first ? null : parsePositiveInteger(options.after, "--after")
1529
+ };
1530
+ });
1531
+ startSpinner("Moving status...");
1532
+ const response = await httpPost(
1533
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}/move-after`,
1534
+ { anchoredStatusScopedId }
1535
+ );
1536
+ if (!response.ok) {
1537
+ stopSpinner(false, "Failed");
1538
+ failResponse(response);
1539
+ }
1540
+ stopSpinner(true, "Status moved");
1541
+ }
1542
+ );
1543
+ addWorkspaceOption(
1544
+ status.command("delete").description("Delete a workflow status").argument("<workflowFileId>").argument("<statusScopedId>")
1545
+ ).action(async (workflowFileId, statusScopedId, options) => {
1546
+ const id = runOrExit3(() => parsePositiveInteger(statusScopedId, "statusScopedId"));
1547
+ startSpinner("Deleting status...");
1548
+ const response = await httpDelete(
1549
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/statuses/${id}`
1550
+ );
1551
+ if (!response.ok) {
2959
1552
  stopSpinner(false, "Failed");
2960
- printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
2961
- process.exit(1);
1553
+ failResponse(response);
2962
1554
  }
1555
+ stopSpinner(true, `Deleted: ${response.data.scopedId}`);
2963
1556
  });
2964
1557
  }
2965
-
2966
- // src/commands/workspace.ts
2967
- var emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
2968
- function isValidEmail(email) {
2969
- return emailRegex.test(email);
1558
+ function registerTransitionCommands(workflow) {
1559
+ const transition = workflow.command("transition").description("Workflow transition operations");
1560
+ for (const action of ["create", "delete"]) {
1561
+ addWorkspaceOption(
1562
+ transition.command(action).description(`${action === "create" ? "Create" : "Delete"} a workflow transition`).argument("<workflowFileId>").requiredOption("--from <statusScopedId>", "Source status scoped ID").requiredOption("--to <statusScopedId>", "Target status scoped ID")
1563
+ ).action(async (workflowFileId, options) => {
1564
+ const body = runOrExit3(() => ({
1565
+ statusFromScopedId: parsePositiveInteger(options.from, "--from"),
1566
+ statusToScopedId: parsePositiveInteger(options.to, "--to")
1567
+ }));
1568
+ startSpinner(`${action === "create" ? "Creating" : "Deleting"} transition...`);
1569
+ const response = action === "create" ? await httpPost(
1570
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/transitions`,
1571
+ body
1572
+ ) : await httpDelete(
1573
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/transitions`,
1574
+ body
1575
+ );
1576
+ if (!response.ok) {
1577
+ stopSpinner(false, "Failed");
1578
+ failResponse(response);
1579
+ }
1580
+ stopSpinner(true, action === "create" ? "Transition created" : "Transition deleted");
1581
+ print(`${response.data.statusFromScopedId} -> ${response.data.statusToScopedId}`);
1582
+ });
1583
+ }
2970
1584
  }
2971
- function registerWorkspaceCommand(program2) {
2972
- const workspace = program2.command("workspace").description("Workspace management");
2973
- workspace.command("list").description("List all workspaces you belong to").action(async () => {
2974
- startSpinner("Fetching workspaces...");
2975
- const response = await httpGet("/workspaces");
2976
- if (response.ok) {
2977
- const { workspaces } = response.data;
2978
- if (workspaces.length === 0) {
2979
- stopSpinner(true, "No workspaces found.");
1585
+ function registerTaskCommands(workflow) {
1586
+ const task = workflow.command("task").description("Workflow task operations");
1587
+ addWorkspaceOption(
1588
+ task.command("list").description("List workflow tasks").option("--workflow-file-id <workflowFileId>", "Filter by workflow file ID").option("--scope <scope>", "Task scope", "all").option("-l, --limit <limit>", "Maximum number of tasks to return", "20").option("--cursor <cursor>", "Pagination cursor")
1589
+ ).action(
1590
+ async (options) => {
1591
+ const query = runOrExit3(() => buildQuery({
1592
+ scope: parseTaskScope(options.scope),
1593
+ limit: parseSearchLimit(options.limit, 20, 100),
1594
+ cursor: options.cursor,
1595
+ workflowFileId: options.workflowFileId
1596
+ }));
1597
+ startSpinner("Fetching workflow tasks...");
1598
+ const response = await httpGet(`/workspaces/${options.workspace}/workflow-tasks${query}`);
1599
+ if (!response.ok) {
1600
+ stopSpinner(false, "Failed");
1601
+ failResponse(response);
1602
+ }
1603
+ if (response.data.tasks.length === 0) {
1604
+ stopSpinner(true, "No workflow tasks found.");
2980
1605
  return;
2981
1606
  }
2982
- stopSpinner(true, `Found ${workspaces.length} workspace(s)`);
2983
- for (const ws of workspaces) {
2984
- print(`${colors.bold}${ws.workspaceId}${colors.reset} ${ws.name}`);
1607
+ stopSpinner(true, `Found ${response.data.tasks.length} task(s)`);
1608
+ for (const item of response.data.tasks) printTaskSummary(item);
1609
+ if (response.data.nextCursor) print(`${colors.dim}nextCursor=${response.data.nextCursor}${colors.reset}`);
1610
+ }
1611
+ );
1612
+ addWorkspaceOption(
1613
+ task.command("get").description("Get a workflow task").argument("<workflowFileId>").argument("<taskScopedId>")
1614
+ ).action(async (workflowFileId, taskScopedId, options) => {
1615
+ const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
1616
+ startSpinner("Fetching task...");
1617
+ const response = await httpGet(
1618
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}`
1619
+ );
1620
+ if (!response.ok) {
1621
+ stopSpinner(false, "Failed");
1622
+ failResponse(response);
1623
+ }
1624
+ stopSpinner(true, "Task fetched");
1625
+ printTaskDetail(response.data);
1626
+ });
1627
+ addAssigneeOptions(
1628
+ addWorkspaceOption(
1629
+ task.command("create").description("Create a workflow task").argument("<workflowFileId>").requiredOption("--title <title>", "Task title").requiredOption("--status <statusScopedId>", "Status scoped ID").option("--priority <priority>", "Priority: urgent, high, medium, or low", "medium").option("--desc-file <localPath>", "Local UTF-8 text file for task description").option("--owner-human-id <humanId>", "Task owner human ID")
1630
+ )
1631
+ ).action(
1632
+ async (workflowFileId, options) => {
1633
+ const body = runOrExit3(() => {
1634
+ const next = {
1635
+ title: options.title,
1636
+ statusScopedId: parsePositiveInteger(options.status, "--status"),
1637
+ priority: parseRequiredPriority(options.priority)
1638
+ };
1639
+ const desc = readDescFile(options.descFile);
1640
+ if (desc !== void 0) next.desc = desc;
1641
+ const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
1642
+ if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
1643
+ Object.assign(next, parseAssigneeBody(options, false));
1644
+ return next;
1645
+ });
1646
+ startSpinner("Creating task...");
1647
+ const response = await httpPost(
1648
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks`,
1649
+ body
1650
+ );
1651
+ if (!response.ok) {
1652
+ stopSpinner(false, "Failed");
1653
+ failResponse(response);
2985
1654
  }
2986
- } else {
1655
+ stopSpinner(true, "Task created");
1656
+ printTaskDetail(response.data);
1657
+ }
1658
+ );
1659
+ addWorkspaceOption(
1660
+ task.command("update").description("Update workflow task metadata").argument("<workflowFileId>").argument("<taskScopedId>").option("--title <title>", "Task title").option("--priority <priority>", "Priority: urgent, high, medium, or low").option("--desc-file <localPath>", "Local UTF-8 text file for task description").option("--clear-desc", "Clear task description").option("--owner-human-id <humanId>", "Task owner human ID")
1661
+ ).action(
1662
+ async (workflowFileId, taskScopedId, options) => {
1663
+ const body = runOrExit3(() => {
1664
+ if (options.descFile && options.clearDesc) {
1665
+ throw new WorkflowOptionsError("Error: --desc-file cannot be used with --clear-desc.");
1666
+ }
1667
+ const next = {};
1668
+ if (options.title) next.title = options.title;
1669
+ const priority = parsePriority(options.priority);
1670
+ if (priority !== void 0) next.priority = priority;
1671
+ const desc = readDescFile(options.descFile);
1672
+ if (desc !== void 0) next.desc = desc;
1673
+ if (options.clearDesc) next.desc = null;
1674
+ const ownerHumanId = parseOptionalPositiveInteger(options.ownerHumanId, "--owner-human-id");
1675
+ if (ownerHumanId !== void 0) next.ownerHumanId = ownerHumanId;
1676
+ assertNonEmptyBody(next);
1677
+ return next;
1678
+ });
1679
+ const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
1680
+ startSpinner("Updating task...");
1681
+ const response = await httpPatch(
1682
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/metadata`,
1683
+ body
1684
+ );
1685
+ if (!response.ok) {
1686
+ stopSpinner(false, "Failed");
1687
+ failResponse(response);
1688
+ }
1689
+ stopSpinner(true, "Task updated");
1690
+ printTaskDetail(response.data);
1691
+ }
1692
+ );
1693
+ for (const action of ["add-linked-files", "remove-linked-files"]) {
1694
+ addWorkspaceOption(
1695
+ task.command(action).description(`${action === "add-linked-files" ? "Add" : "Remove"} linked files on a workflow task`).argument("<workflowFileId>").argument("<taskScopedId>").requiredOption("--file-ids <ids>", "Comma-separated file IDs")
1696
+ ).action(
1697
+ async (workflowFileId, taskScopedId, options) => {
1698
+ const { id, fileIds } = runOrExit3(() => ({
1699
+ id: parsePositiveInteger(taskScopedId, "taskScopedId"),
1700
+ fileIds: parseFileIds(options.fileIds, "--file-ids")
1701
+ }));
1702
+ startSpinner(action === "add-linked-files" ? "Adding linked files..." : "Removing linked files...");
1703
+ const path2 = `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/linked-files`;
1704
+ const response = action === "add-linked-files" ? await httpPost(path2, { fileIds }) : await httpDelete(path2, { fileIds });
1705
+ if (!response.ok) {
1706
+ stopSpinner(false, "Failed");
1707
+ failResponse(response);
1708
+ }
1709
+ stopSpinner(true, action === "add-linked-files" ? "Linked files added" : "Linked files removed");
1710
+ printTaskDetail(response.data);
1711
+ }
1712
+ );
1713
+ }
1714
+ addWorkspaceOption(
1715
+ task.command("move").description("Move a workflow task to another status").argument("<workflowFileId>").argument("<taskScopedId>").requiredOption("--status <statusScopedId>", "Target status scoped ID")
1716
+ ).action(
1717
+ async (workflowFileId, taskScopedId, options) => {
1718
+ const { id, statusScopedId } = runOrExit3(() => ({
1719
+ id: parsePositiveInteger(taskScopedId, "taskScopedId"),
1720
+ statusScopedId: parsePositiveInteger(options.status, "--status")
1721
+ }));
1722
+ startSpinner("Moving task...");
1723
+ const response = await httpPatch(
1724
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/status`,
1725
+ { statusScopedId }
1726
+ );
1727
+ if (!response.ok) {
1728
+ stopSpinner(false, "Failed");
1729
+ failResponse(response);
1730
+ }
1731
+ stopSpinner(true, "Task moved");
1732
+ printTaskDetail(response.data);
1733
+ }
1734
+ );
1735
+ addAssigneeOptions(
1736
+ addWorkspaceOption(
1737
+ task.command("assign").description("Update workflow task assignee").argument("<workflowFileId>").argument("<taskScopedId>").option("--clear", "Clear assignee")
1738
+ )
1739
+ ).action(
1740
+ async (workflowFileId, taskScopedId, options) => {
1741
+ const body = runOrExit3(() => {
1742
+ const next = parseAssigneeBody(options, true);
1743
+ if (Object.keys(next).length === 0 && options.clear !== true) assertNonEmptyBody(next);
1744
+ return next;
1745
+ });
1746
+ const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
1747
+ startSpinner("Updating task assignee...");
1748
+ const response = await httpPatch(
1749
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/assignee`,
1750
+ body
1751
+ );
1752
+ if (!response.ok) {
1753
+ stopSpinner(false, "Failed");
1754
+ failResponse(response);
1755
+ }
1756
+ stopSpinner(true, "Task assignee updated");
1757
+ printTaskDetail(response.data);
1758
+ }
1759
+ );
1760
+ addWorkspaceOption(
1761
+ task.command("delete").description("Delete a workflow task").argument("<workflowFileId>").argument("<taskScopedId>")
1762
+ ).action(async (workflowFileId, taskScopedId, options) => {
1763
+ const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
1764
+ startSpinner("Deleting task...");
1765
+ const response = await httpDelete(
1766
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}`
1767
+ );
1768
+ if (!response.ok) {
2987
1769
  stopSpinner(false, "Failed");
2988
- printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
2989
- process.exit(1);
1770
+ failResponse(response);
2990
1771
  }
1772
+ stopSpinner(true, `Deleted: ${response.data.scopedId}`);
2991
1773
  });
2992
- const members = program2.command("members").description("Manage workspace members");
2993
- members.command("list").description("List all members in a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").action(async (options) => {
2994
- const { workspace: workspaceId } = options;
2995
- startSpinner("Fetching workspace members...");
2996
- const response = await httpGet(`/workspaces/${workspaceId}/members`);
1774
+ }
1775
+ function registerCommentCommands(workflow) {
1776
+ const comment = workflow.command("comment").description("Workflow task comment operations");
1777
+ addWorkspaceOption(
1778
+ comment.command("create").description("Create a workflow task comment").argument("<workflowFileId>").argument("<taskScopedId>").requiredOption("--content-path <localPath>", "Local UTF-8 text file for comment content")
1779
+ ).action(
1780
+ async (workflowFileId, taskScopedId, options) => {
1781
+ const { id, content } = runOrExit3(() => ({
1782
+ id: parsePositiveInteger(taskScopedId, "taskScopedId"),
1783
+ content: readCommentContentFile(options.contentPath)
1784
+ }));
1785
+ startSpinner("Creating comment...");
1786
+ const response = await httpPost(
1787
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/comments`,
1788
+ { content }
1789
+ );
1790
+ if (!response.ok) {
1791
+ stopSpinner(false, "Failed");
1792
+ failResponse(response);
1793
+ }
1794
+ stopSpinner(true, "Comment created");
1795
+ printCommentDetail(response.data);
1796
+ }
1797
+ );
1798
+ addWorkspaceOption(
1799
+ comment.command("list").description("List workflow task comments").argument("<workflowFileId>").argument("<taskScopedId>")
1800
+ ).action(async (workflowFileId, taskScopedId, options) => {
1801
+ const id = runOrExit3(() => parsePositiveInteger(taskScopedId, "taskScopedId"));
1802
+ startSpinner("Fetching comments...");
1803
+ const response = await httpGet(
1804
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${id}/comments`
1805
+ );
2997
1806
  if (!response.ok) {
2998
- stopSpinner(false, "Failed to fetch members");
2999
- printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
3000
- process.exit(1);
1807
+ stopSpinner(false, "Failed");
1808
+ failResponse(response);
3001
1809
  }
3002
- const { members: members2 } = response.data;
3003
- if (members2.length === 0) {
3004
- stopSpinner(true, "No members found.");
1810
+ if (response.data.items.length === 0) {
1811
+ stopSpinner(true, "No comments found.");
3005
1812
  return;
3006
1813
  }
3007
- stopSpinner(true, `Found ${members2.length} member(s)`);
3008
- for (const member of members2) {
3009
- const name = member.displayName || "-";
3010
- print(`${colors.bold}${member.email}${colors.reset} ${name} ${member.role}`);
3011
- }
1814
+ stopSpinner(true, `Found ${response.data.items.length} comment(s)`);
1815
+ response.data.items.forEach((item, index) => {
1816
+ if (index > 0) print("");
1817
+ printCommentDetail(item);
1818
+ });
3012
1819
  });
3013
- members.command("remove").description("Remove members from a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").argument("<emails>", "Emails to remove (comma-separated)").action(async (emailsArg, options) => {
3014
- const { workspace: workspaceId } = options;
3015
- const emails = emailsArg.split(",").map((e) => e.trim()).filter(Boolean);
3016
- if (emails.length === 0) {
3017
- printError("No valid emails provided");
3018
- process.exit(1);
3019
- }
3020
- const invalidEmails = emails.filter((e) => !isValidEmail(e));
3021
- if (invalidEmails.length > 0) {
3022
- for (const email of invalidEmails) {
3023
- printError(`Invalid email format: ${email}`);
1820
+ addWorkspaceOption(
1821
+ comment.command("delete").description("Delete a workflow task comment").argument("<workflowFileId>").argument("<taskScopedId>").argument("<commentId>")
1822
+ ).action(
1823
+ async (workflowFileId, taskScopedId, commentId, options) => {
1824
+ const ids = runOrExit3(() => ({
1825
+ task: parsePositiveInteger(taskScopedId, "taskScopedId"),
1826
+ comment: parsePositiveInteger(commentId, "commentId")
1827
+ }));
1828
+ startSpinner("Deleting comment...");
1829
+ const response = await httpDelete(
1830
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/tasks/${ids.task}/comments/${ids.comment}`
1831
+ );
1832
+ if (!response.ok) {
1833
+ stopSpinner(false, "Failed");
1834
+ failResponse(response);
3024
1835
  }
3025
- process.exit(1);
1836
+ stopSpinner(true, `Deleted: ${response.data.id}`);
3026
1837
  }
3027
- startSpinner("Fetching workspace members...");
3028
- const membersResponse = await httpGet(`/workspaces/${workspaceId}/members`);
3029
- if (!membersResponse.ok) {
3030
- stopSpinner(false, "Failed to fetch members");
3031
- printError(`Error [${membersResponse.status}]: ${JSON.stringify(membersResponse.data)}`);
3032
- process.exit(1);
1838
+ );
1839
+ }
1840
+ function registerRunCommands(workflow) {
1841
+ const run = workflow.command("run").description("Workflow agent run operations");
1842
+ addWorkspaceOption(
1843
+ run.command("status").description("Get workflow agent run statuses").argument("<workflowFileId>").argument("<triggerIds...>")
1844
+ ).action(async (workflowFileId, triggerIds, options) => {
1845
+ const workflowPipelineTriggerIds = runOrExit3(
1846
+ () => triggerIds.map((triggerId) => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"))
1847
+ );
1848
+ startSpinner("Fetching run statuses...");
1849
+ const response = await httpPost(
1850
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/triggers/statuses`,
1851
+ { workflowPipelineTriggerIds }
1852
+ );
1853
+ if (!response.ok) {
1854
+ stopSpinner(false, "Failed");
1855
+ failResponse(response);
3033
1856
  }
3034
- stopSpinner(true, "Members fetched");
3035
- const { members: members2 } = membersResponse.data;
3036
- const emailToMember = new Map(members2.map((m) => [m.email.toLowerCase(), m]));
3037
- for (const email of emails) {
3038
- const member = emailToMember.get(email.toLowerCase());
3039
- if (!member) {
3040
- print(`${colors.red}\u2717${colors.reset} ${email} - not found in workspace`);
3041
- continue;
3042
- }
3043
- const deleteResponse = await httpDelete(`/workspaces/${workspaceId}/members/${member.userId}`);
3044
- if (deleteResponse.ok) {
3045
- print(`${colors.green}\u2713${colors.reset} ${email} - removed`);
3046
- } else {
3047
- print(`${colors.red}\u2717${colors.reset} ${email} - failed: ${JSON.stringify(deleteResponse.data)}`);
3048
- }
1857
+ stopSpinner(true, `Found ${response.data.length} run status item(s)`);
1858
+ for (const item of response.data) {
1859
+ print(`${colors.bold}${item.workflowPipelineTriggerId}${colors.reset} ${item.pipelineStatus}`);
3049
1860
  }
3050
1861
  });
3051
- const space = program2.command("space").description("Manage spaces");
3052
- space.command("list").description("List accessible spaces in a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").action(async (options) => {
3053
- const { workspace: workspaceId } = options;
3054
- startSpinner("Fetching spaces...");
3055
- const response = await httpGet(`/workspaces/${workspaceId}/spaces`);
1862
+ addWorkspaceOption(
1863
+ run.command("output").description("Get workflow agent run output events").argument("<workflowFileId>").argument("<triggerId>")
1864
+ ).action(async (workflowFileId, triggerId, options) => {
1865
+ const id = runOrExit3(() => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"));
1866
+ startSpinner("Fetching run output...");
1867
+ const response = await httpGet(
1868
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/trigger/${id}/output`
1869
+ );
3056
1870
  if (!response.ok) {
3057
- stopSpinner(false, "Failed to fetch spaces");
3058
- printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
3059
- process.exit(1);
3060
- }
3061
- const { spaces } = response.data;
3062
- if (spaces.length === 0) {
3063
- stopSpinner(true, "No spaces found.");
3064
- return;
1871
+ stopSpinner(false, "Failed");
1872
+ failResponse(response);
3065
1873
  }
3066
- stopSpinner(true, `Found ${spaces.length} space(s)`);
3067
- print(
3068
- `${colors.bold}TYPE${colors.reset} ${colors.bold}TEAM_SPACE_ID${colors.reset} ${colors.bold}TEAMMATE_ID${colors.reset} ${colors.bold}NAME${colors.reset}`
1874
+ stopSpinner(true, `Run ${response.data.workflowPipelineTriggerId}`);
1875
+ for (const event of response.data.pipelineEvents) print(event);
1876
+ });
1877
+ addWorkspaceOption(
1878
+ run.command("abort").description("Abort a workflow agent run").argument("<workflowFileId>").argument("<triggerId>")
1879
+ ).action(async (workflowFileId, triggerId, options) => {
1880
+ const id = runOrExit3(() => parsePositiveInteger(triggerId, "workflowPipelineTriggerId"));
1881
+ startSpinner("Aborting run...");
1882
+ const response = await httpPost(
1883
+ `/workspaces/${options.workspace}/workflows/${workflowFileId}/pipeline/trigger/${id}/abort`,
1884
+ {}
3069
1885
  );
3070
- for (const s of spaces) {
3071
- const teamSpaceId = s.teamSpaceId ?? "";
3072
- const teammateId = s.agentId != null ? String(s.agentId) : "";
3073
- print(`${s.type} ${teamSpaceId} ${teammateId} ${s.name}`);
1886
+ if (!response.ok) {
1887
+ stopSpinner(false, "Failed");
1888
+ failResponse(response);
3074
1889
  }
1890
+ stopSpinner(true, `Abort accepted: ${id}`);
3075
1891
  });
3076
1892
  }
3077
1893
 
@@ -3085,23 +1901,23 @@ function createProgram(version) {
3085
1901
  registerWorkspaceCommand(program2);
3086
1902
  registerFileCommand(program2);
3087
1903
  registerMemoryCommand(program2);
3088
- registerRunCommand(program2);
1904
+ registerWorkflowCommand(program2);
3089
1905
  registerMiniappCommand(program2);
3090
1906
  registerTelemetryCommand(program2);
3091
1907
  return program2;
3092
1908
  }
3093
1909
 
3094
1910
  // src/telemetry/client.ts
3095
- import { arch as arch3, platform as platform3 } from "os";
1911
+ import { arch as arch2, platform as platform2 } from "os";
3096
1912
  var buffer = [];
3097
1913
  var FLUSH_TIMEOUT_MS = 3e3;
3098
1914
  var MAX_BATCH_SIZE = 100;
3099
1915
  function commonLabels() {
3100
1916
  return {
3101
- cli_version: "0.3.3-moxt-run.1",
1917
+ cli_version: "0.3.3",
3102
1918
  node_version: process.versions.node,
3103
- os: platform3(),
3104
- arch: arch3(),
1919
+ os: platform2(),
1920
+ arch: arch2(),
3105
1921
  is_ci: isCiEnvironment() ? "true" : "false"
3106
1922
  };
3107
1923
  }
@@ -3144,7 +1960,7 @@ async function flush() {
3144
1960
  }
3145
1961
  const batch = buffer.splice(0, MAX_BATCH_SIZE);
3146
1962
  const payload = {
3147
- release_id: "0.3.3-moxt-run.1",
1963
+ release_id: "0.3.3",
3148
1964
  client_type: "cli",
3149
1965
  metric_data_list: batch.map((m) => ({
3150
1966
  profile: "cli",
@@ -3285,9 +2101,9 @@ function installExitHandler() {
3285
2101
 
3286
2102
  // src/index.ts
3287
2103
  updateNotifier({
3288
- pkg: { name: "@moxt-ai/cli", version: "0.3.3-moxt-run.1" }
2104
+ pkg: { name: "@moxt-ai/cli", version: "0.3.3" }
3289
2105
  }).notify();
3290
- var program = createProgram("0.3.3-moxt-run.1");
2106
+ var program = createProgram("0.3.3");
3291
2107
  instrumentProgram(program);
3292
2108
  installExitHandler();
3293
2109
  program.parseAsync(process.argv).then(async () => {