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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command } from "commander";
5
4
  import updateNotifier from "update-notifier";
6
5
 
6
+ // src/program.ts
7
+ import { Command } from "commander";
8
+
7
9
  // src/commands/file.ts
8
10
  import * as fs from "fs";
9
11
 
@@ -147,7 +149,7 @@ function getApiKeyOrUndefined() {
147
149
 
148
150
  // src/utils/http.ts
149
151
  function getUserAgent() {
150
- return `moxt-cli/${"0.3.2"} (${platform()}/${arch()}) node/${process.versions.node}`;
152
+ return `moxt-cli/${"0.3.3-moxt-run.1"} (${platform()}/${arch()}) node/${process.versions.node}`;
151
153
  }
152
154
  async function fetchApi(method, path2, body) {
153
155
  const apiKey = getApiKey();
@@ -742,257 +744,2407 @@ function registerMiniappCommand(program2) {
742
744
  });
743
745
  }
744
746
 
745
- // src/telemetry/config.ts
746
- import * as fs2 from "fs";
747
- import * as os from "os";
748
- import * as path from "path";
749
- function getConfigPath() {
750
- return path.join(os.homedir(), ".moxt", "config.json");
751
- }
752
- function readTelemetryConfig() {
753
- try {
754
- const raw = fs2.readFileSync(getConfigPath(), "utf8");
755
- const parsed = JSON.parse(raw);
756
- return parsed.telemetry ?? {};
757
- } catch {
758
- return {};
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;
759
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");
793
+ }
794
+ return pathJoin(env.XDG_DATA_HOME ?? pathJoin(home, ".local", "share"), "moxt", "runs");
760
795
  }
761
- function writeTelemetryConfig(update) {
762
- const configPath = getConfigPath();
763
- let existing = {};
764
- try {
765
- existing = JSON.parse(fs2.readFileSync(configPath, "utf8"));
766
- } catch {
767
- existing = {};
796
+ function withDefaultCaptureOutput(env, home = homedir()) {
797
+ const { outputDir, outputRoot } = captureEnvPaths(env);
798
+ if (hasValue(outputDir) || hasValue(outputRoot)) {
799
+ return env;
768
800
  }
769
- const currentTelemetry = existing.telemetry ?? {};
770
- const next = { ...existing, telemetry: { ...currentTelemetry, ...update } };
771
- fs2.mkdirSync(path.dirname(configPath), { recursive: true });
772
- fs2.writeFileSync(configPath, JSON.stringify(next, null, 2));
801
+ return {
802
+ ...env,
803
+ MOXT_RUN_OUTPUT_ROOT: defaultCaptureOutputRoot(home, env)
804
+ };
773
805
  }
774
-
775
- // src/telemetry/opt-out.ts
776
- function isCiEnvironment() {
777
- return process.env.CI === "true" || process.env.CI === "1";
806
+ async function prepareCaptureOutputs(env, runId) {
807
+ const { outputDir, outputRoot } = captureEnvPaths(env);
808
+ if (hasValue(outputDir)) {
809
+ await prepareOutputPath(outputDir, () => prepareCaptureDirectory(outputDir));
810
+ }
811
+ if (hasValue(outputRoot)) {
812
+ const runDir = join(outputRoot, runId);
813
+ await prepareOutputPath(runDir, () => prepareCaptureDirectory(runDir));
814
+ }
778
815
  }
779
- function isTelemetryDisabled() {
780
- if (process.env.MOXT_TELEMETRY_DISABLED === "1" || process.env.MOXT_TELEMETRY_DISABLED === "true") {
781
- return true;
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);
782
821
  }
783
- if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true") {
784
- return true;
822
+ if (hasValue(outputRoot)) {
823
+ dirs.add(join(outputRoot, runId));
785
824
  }
786
- return readTelemetryConfig().disabled === true;
825
+ return [...dirs];
787
826
  }
788
-
789
- // src/commands/telemetry.ts
790
- function registerTelemetryCommand(program2) {
791
- const telemetry = program2.command("telemetry").description("Manage Moxt CLI telemetry");
792
- telemetry.command("status").description("Show telemetry status").action(() => {
793
- const cfg = readTelemetryConfig();
794
- 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";
795
- const state = isTelemetryDisabled() ? "disabled" : "enabled";
796
- print(`telemetry: ${state}`);
797
- if (envOverride) {
798
- print("(disabled via environment variable)");
799
- } else if (cfg.disabled) {
800
- print("(disabled via config file)");
801
- }
802
- });
803
- telemetry.command("enable").description("Enable telemetry").action(() => {
804
- writeTelemetryConfig({ disabled: false });
805
- print("telemetry: enabled");
806
- });
807
- telemetry.command("disable").description("Disable telemetry").action(() => {
808
- writeTelemetryConfig({ disabled: true });
809
- print("telemetry: disabled");
810
- });
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
+ );
811
834
  }
812
-
813
- // src/commands/whoami.ts
814
- function registerWhoamiCommand(program2) {
815
- program2.command("whoami").description("Show current user information").action(async () => {
816
- startSpinner("Fetching user info...");
817
- const response = await httpGet("/users/whoami");
818
- if (response.ok) {
819
- stopSpinner(true, "Done");
820
- print(`${colors.bold}Nickname${colors.reset}: ${response.data.nickname}`);
821
- print(`${colors.bold}Email${colors.reset}: ${response.data.email}`);
822
- } else {
823
- stopSpinner(false, "Failed");
824
- printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
825
- process.exit(1);
826
- }
827
- });
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
+ );
828
843
  }
829
-
830
- // src/commands/workspace.ts
831
- var emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
832
- function isValidEmail(email) {
833
- return emailRegex.test(email);
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}`;
834
848
  }
835
- function registerWorkspaceCommand(program2) {
836
- const workspace = program2.command("workspace").description("Workspace management");
837
- workspace.command("list").description("List all workspaces you belong to").action(async () => {
838
- startSpinner("Fetching workspaces...");
839
- const response = await httpGet("/workspaces");
840
- if (response.ok) {
841
- const { workspaces } = response.data;
842
- if (workspaces.length === 0) {
843
- stopSpinner(true, "No workspaces found.");
844
- return;
845
- }
846
- stopSpinner(true, `Found ${workspaces.length} workspace(s)`);
847
- for (const ws of workspaces) {
848
- print(`${colors.bold}${ws.workspaceId}${colors.reset} ${ws.name}`);
849
- }
850
- } else {
851
- stopSpinner(false, "Failed");
852
- printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
853
- process.exit(1);
854
- }
855
- });
856
- const members = program2.command("members").description("Manage workspace members");
857
- members.command("list").description("List all members in a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").action(async (options) => {
858
- const { workspace: workspaceId } = options;
859
- startSpinner("Fetching workspace members...");
860
- const response = await httpGet(`/workspaces/${workspaceId}/members`);
861
- if (!response.ok) {
862
- stopSpinner(false, "Failed to fetch members");
863
- printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
864
- process.exit(1);
865
- }
866
- const { members: members2 } = response.data;
867
- if (members2.length === 0) {
868
- stopSpinner(true, "No members found.");
869
- return;
870
- }
871
- stopSpinner(true, `Found ${members2.length} member(s)`);
872
- for (const member of members2) {
873
- const name = member.displayName || "-";
874
- print(`${colors.bold}${member.email}${colors.reset} ${name} ${member.role}`);
875
- }
876
- });
877
- 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) => {
878
- const { workspace: workspaceId } = options;
879
- const emails = emailsArg.split(",").map((e) => e.trim()).filter(Boolean);
880
- if (emails.length === 0) {
881
- printError("No valid emails provided");
882
- process.exit(1);
883
- }
884
- const invalidEmails = emails.filter((e) => !isValidEmail(e));
885
- if (invalidEmails.length > 0) {
886
- for (const email of invalidEmails) {
887
- printError(`Invalid email format: ${email}`);
888
- }
889
- process.exit(1);
890
- }
891
- startSpinner("Fetching workspace members...");
892
- const membersResponse = await httpGet(`/workspaces/${workspaceId}/members`);
893
- if (!membersResponse.ok) {
894
- stopSpinner(false, "Failed to fetch members");
895
- printError(`Error [${membersResponse.status}]: ${JSON.stringify(membersResponse.data)}`);
896
- process.exit(1);
897
- }
898
- stopSpinner(true, "Members fetched");
899
- const { members: members2 } = membersResponse.data;
900
- const emailToMember = new Map(members2.map((m) => [m.email.toLowerCase(), m]));
901
- for (const email of emails) {
902
- const member = emailToMember.get(email.toLowerCase());
903
- if (!member) {
904
- print(`${colors.red}\u2717${colors.reset} ${email} - not found in workspace`);
905
- continue;
906
- }
907
- const deleteResponse = await httpDelete(`/workspaces/${workspaceId}/members/${member.userId}`);
908
- if (deleteResponse.ok) {
909
- print(`${colors.green}\u2713${colors.reset} ${email} - removed`);
910
- } else {
911
- print(`${colors.red}\u2717${colors.reset} ${email} - failed: ${JSON.stringify(deleteResponse.data)}`);
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);
912
860
  }
913
- }
914
- });
915
- const space = program2.command("space").description("Manage spaces");
916
- space.command("list").description("List accessible spaces in a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").action(async (options) => {
917
- const { workspace: workspaceId } = options;
918
- startSpinner("Fetching spaces...");
919
- const response = await httpGet(`/workspaces/${workspaceId}/spaces`);
920
- if (!response.ok) {
921
- stopSpinner(false, "Failed to fetch spaces");
922
- printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
923
- process.exit(1);
924
- }
925
- const { spaces } = response.data;
926
- if (spaces.length === 0) {
927
- stopSpinner(true, "No spaces found.");
928
- return;
929
- }
930
- stopSpinner(true, `Found ${spaces.length} space(s)`);
931
- print(
932
- `${colors.bold}TYPE${colors.reset} ${colors.bold}TEAM_SPACE_ID${colors.reset} ${colors.bold}TEAMMATE_ID${colors.reset} ${colors.bold}NAME${colors.reset}`
933
- );
934
- for (const s of spaces) {
935
- const teamSpaceId = s.teamSpaceId ?? "";
936
- const teammateId = s.agentId != null ? String(s.agentId) : "";
937
- print(`${s.type} ${teamSpaceId} ${teammateId} ${s.name}`);
938
- }
939
- });
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
+ );
940
869
  }
941
-
942
- // src/telemetry/client.ts
943
- import { arch as arch2, platform as platform2 } from "os";
944
- var buffer = [];
945
- var FLUSH_TIMEOUT_MS = 3e3;
946
- var MAX_BATCH_SIZE = 100;
947
- function commonLabels() {
870
+ function serializeEventsFromTranscript(transcript, startSeq, agent = "claude") {
871
+ const events = normalizeEvents(transcript, startSeq, agent);
948
872
  return {
949
- cli_version: "0.3.2",
950
- node_version: process.versions.node,
951
- os: platform2(),
952
- arch: arch2(),
953
- is_ci: isCiEnvironment() ? "true" : "false"
873
+ text: serializeEvents(events),
874
+ nextSeq: startSeq + events.length
954
875
  };
955
876
  }
956
- function record(metric) {
957
- if (isTelemetryDisabled()) {
958
- debugLog(`record: disabled, dropping ${metric.metric_id}`);
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) {
959
909
  return;
960
910
  }
961
- if (!getApiKeyOrUndefined()) {
962
- debugLog(`record: no API key, dropping ${metric.metric_id}`);
911
+ if (!entries.includes(RUNLOG_DIRECTORY_MARKER)) {
912
+ throw new Error("refusing to use run output directory without ownership marker");
913
+ }
914
+ await assertRunlogDirectoryMarker(outputDir);
915
+ const metadataEntries = RUNLOG_METADATA_FILE_NAMES.filter((entry) => entries.includes(entry));
916
+ if (metadataEntries.length === 0) {
963
917
  return;
964
918
  }
965
- buffer.push({
966
- ...metric,
967
- extra_label_name_2_value: {
968
- ...commonLabels(),
969
- ...metric.extra_label_name_2_value ?? {}
970
- }
971
- });
972
- debugLog(`record: buffered ${metric.metric_id} (buffer=${buffer.length})`);
973
- }
974
- function isDebug() {
975
- return process.env.MOXT_TELEMETRY_DEBUG === "1" || process.env.MOXT_TELEMETRY_DEBUG === "true";
919
+ const runIds = await Promise.all(
920
+ metadataEntries.map((entry) => readRunlogMetadataRunId(outputDir, entry))
921
+ );
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");
925
+ }
976
926
  }
977
- function debugLog(msg, detail) {
978
- if (!isDebug()) return;
979
- process.stderr.write(`[telemetry] ${msg}${detail !== void 0 ? ` ${JSON.stringify(detail)}` : ""}
980
- `);
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)}`);
933
+ }
934
+ const value = objectValue(parsed);
935
+ if (value === null) {
936
+ throw new Error(`invalid ${fileName}: expected object`);
937
+ }
938
+ if (value.schema_version !== RUNLOG_METADATA_SCHEMAS[fileName]) {
939
+ throw new Error(`invalid ${fileName}: unexpected schema_version`);
940
+ }
941
+ if (typeof value.run_id !== "string" || value.run_id.trim() === "") {
942
+ throw new Error(`invalid ${fileName}: missing run_id`);
943
+ }
944
+ return value.run_id;
981
945
  }
982
- async function flush() {
983
- if (buffer.length === 0) {
984
- debugLog("flush: buffer empty, skipping");
985
- return;
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)}`);
986
952
  }
987
- const apiKey = getApiKeyOrUndefined();
988
- if (!apiKey) {
989
- debugLog("flush: no API key, dropping buffer");
990
- buffer.length = 0;
991
- return;
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`);
992
956
  }
993
- const batch = buffer.splice(0, MAX_BATCH_SIZE);
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");
995
+ try {
996
+ await access(path2);
997
+ return;
998
+ } catch {
999
+ }
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");
1018
+ }
1019
+ async function writeChunks(outputDir, chunks) {
1020
+ await Promise.all(chunks.map((chunk) => writeFile(join(outputDir, chunk.path), chunk.content, "utf8")));
1021
+ }
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
+ `);
1088
+ }
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;
1215
+ }
1216
+ }
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 "";
1228
+ }
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
+ }
1260
+ return String(error);
1261
+ }
1262
+ function objectValue(value) {
1263
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1264
+ }
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);
1365
+ }
1366
+ function addHit(hitCounts, rule) {
1367
+ hitCounts.set(rule, (hitCounts.get(rule) ?? 0) + 1);
1368
+ }
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;
1382
+ }
1383
+ }
1384
+ function sanitizeRepo(repo) {
1385
+ if (repo === null) return null;
1386
+ return {
1387
+ ...repo,
1388
+ remote: stripRemoteCredentials(repo.remote)
1389
+ };
1390
+ }
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 };
1445
+ }
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
+ });
1479
+ }
1480
+ }
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
+ }
1657
+ }
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) {
1664
+ 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;
1674
+ 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 === "") {
1682
+ 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)
1928
+ );
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
+ }
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
+ }
2022
+ );
2023
+ 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;
2535
+ }
2536
+ if (child === null) {
2537
+ pendingForward = signal;
2538
+ return;
2539
+ }
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);
2844
+ });
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)");
2937
+ }
2938
+ });
2939
+ telemetry.command("enable").description("Enable telemetry").action(() => {
2940
+ writeTelemetryConfig({ disabled: false });
2941
+ print("telemetry: enabled");
2942
+ });
2943
+ telemetry.command("disable").description("Disable telemetry").action(() => {
2944
+ writeTelemetryConfig({ disabled: true });
2945
+ print("telemetry: disabled");
2946
+ });
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 {
2959
+ stopSpinner(false, "Failed");
2960
+ printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
2961
+ process.exit(1);
2962
+ }
2963
+ });
2964
+ }
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);
2970
+ }
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.");
2980
+ return;
2981
+ }
2982
+ stopSpinner(true, `Found ${workspaces.length} workspace(s)`);
2983
+ for (const ws of workspaces) {
2984
+ print(`${colors.bold}${ws.workspaceId}${colors.reset} ${ws.name}`);
2985
+ }
2986
+ } else {
2987
+ stopSpinner(false, "Failed");
2988
+ printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
2989
+ process.exit(1);
2990
+ }
2991
+ });
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`);
2997
+ if (!response.ok) {
2998
+ stopSpinner(false, "Failed to fetch members");
2999
+ printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
3000
+ process.exit(1);
3001
+ }
3002
+ const { members: members2 } = response.data;
3003
+ if (members2.length === 0) {
3004
+ stopSpinner(true, "No members found.");
3005
+ return;
3006
+ }
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
+ }
3012
+ });
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}`);
3024
+ }
3025
+ process.exit(1);
3026
+ }
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);
3033
+ }
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
+ }
3049
+ }
3050
+ });
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`);
3056
+ 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;
3065
+ }
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}`
3069
+ );
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}`);
3074
+ }
3075
+ });
3076
+ }
3077
+
3078
+ // src/program.ts
3079
+ function createProgram(version) {
3080
+ const program2 = new Command();
3081
+ program2.name("moxt").description("Moxt CLI - AI Workspace").version(version).action(() => {
3082
+ program2.help();
3083
+ });
3084
+ registerWhoamiCommand(program2);
3085
+ registerWorkspaceCommand(program2);
3086
+ registerFileCommand(program2);
3087
+ registerMemoryCommand(program2);
3088
+ registerRunCommand(program2);
3089
+ registerMiniappCommand(program2);
3090
+ registerTelemetryCommand(program2);
3091
+ return program2;
3092
+ }
3093
+
3094
+ // src/telemetry/client.ts
3095
+ import { arch as arch3, platform as platform3 } from "os";
3096
+ var buffer = [];
3097
+ var FLUSH_TIMEOUT_MS = 3e3;
3098
+ var MAX_BATCH_SIZE = 100;
3099
+ function commonLabels() {
3100
+ return {
3101
+ cli_version: "0.3.3-moxt-run.1",
3102
+ node_version: process.versions.node,
3103
+ os: platform3(),
3104
+ arch: arch3(),
3105
+ is_ci: isCiEnvironment() ? "true" : "false"
3106
+ };
3107
+ }
3108
+ function record(metric) {
3109
+ if (isTelemetryDisabled()) {
3110
+ debugLog(`record: disabled, dropping ${metric.metric_id}`);
3111
+ return;
3112
+ }
3113
+ if (!getApiKeyOrUndefined()) {
3114
+ debugLog(`record: no API key, dropping ${metric.metric_id}`);
3115
+ return;
3116
+ }
3117
+ buffer.push({
3118
+ ...metric,
3119
+ extra_label_name_2_value: {
3120
+ ...commonLabels(),
3121
+ ...metric.extra_label_name_2_value ?? {}
3122
+ }
3123
+ });
3124
+ debugLog(`record: buffered ${metric.metric_id} (buffer=${buffer.length})`);
3125
+ }
3126
+ function isDebug() {
3127
+ return process.env.MOXT_TELEMETRY_DEBUG === "1" || process.env.MOXT_TELEMETRY_DEBUG === "true";
3128
+ }
3129
+ function debugLog(msg, detail) {
3130
+ if (!isDebug()) return;
3131
+ process.stderr.write(`[telemetry] ${msg}${detail !== void 0 ? ` ${JSON.stringify(detail)}` : ""}
3132
+ `);
3133
+ }
3134
+ async function flush() {
3135
+ if (buffer.length === 0) {
3136
+ debugLog("flush: buffer empty, skipping");
3137
+ return;
3138
+ }
3139
+ const apiKey = getApiKeyOrUndefined();
3140
+ if (!apiKey) {
3141
+ debugLog("flush: no API key, dropping buffer");
3142
+ buffer.length = 0;
3143
+ return;
3144
+ }
3145
+ const batch = buffer.splice(0, MAX_BATCH_SIZE);
994
3146
  const payload = {
995
- release_id: "0.3.2",
3147
+ release_id: "0.3.3-moxt-run.1",
996
3148
  client_type: "cli",
997
3149
  metric_data_list: batch.map((m) => ({
998
3150
  profile: "cli",
@@ -1133,20 +3285,11 @@ function installExitHandler() {
1133
3285
 
1134
3286
  // src/index.ts
1135
3287
  updateNotifier({
1136
- pkg: { name: "@moxt-ai/cli", version: "0.3.2" }
3288
+ pkg: { name: "@moxt-ai/cli", version: "0.3.3-moxt-run.1" }
1137
3289
  }).notify();
1138
- var program = new Command();
1139
- program.name("moxt").description("Moxt CLI - AI Workspace").version("0.3.2").action(() => {
1140
- program.help();
1141
- });
3290
+ var program = createProgram("0.3.3-moxt-run.1");
1142
3291
  instrumentProgram(program);
1143
3292
  installExitHandler();
1144
- registerWhoamiCommand(program);
1145
- registerWorkspaceCommand(program);
1146
- registerFileCommand(program);
1147
- registerMemoryCommand(program);
1148
- registerMiniappCommand(program);
1149
- registerTelemetryCommand(program);
1150
3293
  program.parseAsync(process.argv).then(async () => {
1151
3294
  await flush();
1152
3295
  }).catch(async (err) => {