@moxt-ai/cli 0.3.1 → 0.3.3-moxt-run.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/index.js +2377 -125
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
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.
|
|
152
|
+
return `moxt-cli/${"0.3.3-moxt-run.0"} (${platform()}/${arch()}) node/${process.versions.node}`;
|
|
151
153
|
}
|
|
152
154
|
async function fetchApi(method, path2, body) {
|
|
153
155
|
const apiKey = getApiKey();
|
|
@@ -217,6 +219,59 @@ function printError(message) {
|
|
|
217
219
|
console.error(`${colors.red}${message}${colors.reset}`);
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
// src/utils/search-options.ts
|
|
223
|
+
var SearchOptionsError = class extends Error {
|
|
224
|
+
constructor(message) {
|
|
225
|
+
super(message);
|
|
226
|
+
this.name = "SearchOptionsError";
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
function parseSearchLimit(raw, defaultValue, max) {
|
|
230
|
+
if (raw == null || raw === "") {
|
|
231
|
+
return defaultValue;
|
|
232
|
+
}
|
|
233
|
+
const parsed = Number.parseInt(raw, 10);
|
|
234
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== raw) {
|
|
235
|
+
throw new SearchOptionsError(`Error: --limit must be a positive integer, got "${raw}".`);
|
|
236
|
+
}
|
|
237
|
+
if (parsed > max) {
|
|
238
|
+
throw new SearchOptionsError(`Error: --limit must be less than or equal to ${max}.`);
|
|
239
|
+
}
|
|
240
|
+
return parsed;
|
|
241
|
+
}
|
|
242
|
+
function parseSearchMode(raw) {
|
|
243
|
+
if (raw == null || raw === "") {
|
|
244
|
+
return "any";
|
|
245
|
+
}
|
|
246
|
+
if (raw === "all" || raw === "any") {
|
|
247
|
+
return raw;
|
|
248
|
+
}
|
|
249
|
+
throw new SearchOptionsError(`Error: --mode must be "any" or "all", got "${raw}".`);
|
|
250
|
+
}
|
|
251
|
+
function parseTeammateId(raw) {
|
|
252
|
+
if (raw == null || raw === "") {
|
|
253
|
+
return void 0;
|
|
254
|
+
}
|
|
255
|
+
const parsed = Number.parseInt(raw, 10);
|
|
256
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== raw) {
|
|
257
|
+
throw new SearchOptionsError(`Error: --teammate-id must be a positive integer, got "${raw}".`);
|
|
258
|
+
}
|
|
259
|
+
return parsed;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/utils/run-or-exit.ts
|
|
263
|
+
function runOrExit(fn) {
|
|
264
|
+
try {
|
|
265
|
+
return fn();
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (err instanceof SpaceOptionsError || err instanceof SearchOptionsError) {
|
|
268
|
+
printError(err.message);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
220
275
|
// src/utils/spinner.ts
|
|
221
276
|
var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
222
277
|
var intervalId = null;
|
|
@@ -240,22 +295,48 @@ function stopSpinner(success, message) {
|
|
|
240
295
|
}
|
|
241
296
|
|
|
242
297
|
// src/commands/file.ts
|
|
243
|
-
function runOrExit(fn) {
|
|
244
|
-
try {
|
|
245
|
-
return fn();
|
|
246
|
-
} catch (err) {
|
|
247
|
-
if (err instanceof SpaceOptionsError) {
|
|
248
|
-
printError(err.message);
|
|
249
|
-
process.exit(1);
|
|
250
|
-
}
|
|
251
|
-
throw err;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
298
|
function addSpaceOptions(cmd) {
|
|
255
299
|
return cmd.requiredOption("-w, --workspace <workspaceId>", "Workspace ID").option("-s, --space <name>", "Space name (personal or teamspace name)").option("--personal", "Use personal space (default)").option("--team-space-id <teamSpaceId>", "Team space ID").option("--teammate-id <teammateId>", "Teammate ID (teammate space)");
|
|
256
300
|
}
|
|
257
301
|
function registerFileCommand(program2) {
|
|
258
302
|
const file = program2.command("file").description("File operations");
|
|
303
|
+
addSpaceOptions(
|
|
304
|
+
file.command("search").description("Search files in accessible spaces").argument("<query>", "Search query").option("-l, --limit <limit>", "Maximum number of files to return", "30").option("--mode <mode>", "Keyword matching mode: any or all", "any")
|
|
305
|
+
).action(async (query, options) => {
|
|
306
|
+
const spaceParams = runOrExit(() => resolveSpaceSelector(options));
|
|
307
|
+
const limit = runOrExit(() => parseSearchLimit(options.limit, 30, 80));
|
|
308
|
+
const mode = runOrExit(() => parseSearchMode(options.mode));
|
|
309
|
+
startSpinner("Searching files...");
|
|
310
|
+
const response = await httpPost(
|
|
311
|
+
`/workspaces/${options.workspace}/files/search`,
|
|
312
|
+
{
|
|
313
|
+
query,
|
|
314
|
+
limit,
|
|
315
|
+
mode,
|
|
316
|
+
...spaceParams
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
stopSpinner(false, "Failed");
|
|
321
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
const { items } = response.data;
|
|
325
|
+
if (items.length === 0) {
|
|
326
|
+
stopSpinner(true, "No files found.");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
stopSpinner(true, `Found ${items.length} file(s)`);
|
|
330
|
+
for (const item of items) {
|
|
331
|
+
print(`${colors.bold}${item.rank}. ${item.filePath}${colors.reset}`);
|
|
332
|
+
print(`${colors.dim}repo=${item.repoId} file=${item.fileId}${colors.reset}`);
|
|
333
|
+
const snippet = item.contentHighlight?.text ?? item.semanticSnippets[0]?.content;
|
|
334
|
+
if (snippet) {
|
|
335
|
+
print(snippet);
|
|
336
|
+
}
|
|
337
|
+
print("");
|
|
338
|
+
}
|
|
339
|
+
});
|
|
259
340
|
addSpaceOptions(
|
|
260
341
|
file.command("get-url").description("Resolve the shareable browser URL for a file").requiredOption("-p, --path <path>", "File path")
|
|
261
342
|
).action(async (options) => {
|
|
@@ -469,6 +550,43 @@ function registerFileCommand(program2) {
|
|
|
469
550
|
);
|
|
470
551
|
}
|
|
471
552
|
|
|
553
|
+
// src/commands/memory.ts
|
|
554
|
+
function registerMemoryCommand(program2) {
|
|
555
|
+
const memory = program2.command("memory").description("Memory operations");
|
|
556
|
+
memory.command("search").description("Search archived agent memory").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").option("--teammate-id <teammateId>", "Search a managed AI teammate memory").option("-l, --limit <limit>", "Maximum number of memory chunks to return", "5").argument("<query>", "Search query").action(
|
|
557
|
+
async (query, options) => {
|
|
558
|
+
const limit = runOrExit(() => parseSearchLimit(options.limit, 5, 10));
|
|
559
|
+
const teammateId = runOrExit(() => parseTeammateId(options.teammateId));
|
|
560
|
+
startSpinner("Searching memory...");
|
|
561
|
+
const response = await httpPost(
|
|
562
|
+
`/workspaces/${options.workspace}/memory/search`,
|
|
563
|
+
{
|
|
564
|
+
query,
|
|
565
|
+
limit,
|
|
566
|
+
...teammateId !== void 0 ? { agentId: teammateId } : {}
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
if (!response.ok) {
|
|
570
|
+
stopSpinner(false, "Failed");
|
|
571
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
const { items } = response.data;
|
|
575
|
+
if (items.length === 0) {
|
|
576
|
+
stopSpinner(true, "No memory found.");
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
stopSpinner(true, `Found ${items.length} memory chunk(s)`);
|
|
580
|
+
for (const item of items) {
|
|
581
|
+
print(`${colors.bold}${item.rank}. pipeline=${item.pipelineId} chunk=${item.chunkIndex}${colors.reset}`);
|
|
582
|
+
print(`${colors.dim}chunkId=${item.chunkId}${item.contentTruncated ? " truncated=true" : ""}${colors.reset}`);
|
|
583
|
+
print(item.content);
|
|
584
|
+
print("");
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
472
590
|
// src/utils/miniapp-db.ts
|
|
473
591
|
var MiniappDbOptionsError = class extends Error {
|
|
474
592
|
constructor(message) {
|
|
@@ -626,120 +744,2246 @@ function registerMiniappCommand(program2) {
|
|
|
626
744
|
});
|
|
627
745
|
}
|
|
628
746
|
|
|
629
|
-
// src/
|
|
630
|
-
import
|
|
631
|
-
import
|
|
632
|
-
import
|
|
633
|
-
|
|
634
|
-
|
|
747
|
+
// src/run/run-agent.ts
|
|
748
|
+
import { spawn } from "child_process";
|
|
749
|
+
import { closeSync as closeSync2, fstatSync, openSync as openSync2, readSync as readSync2 } from "fs";
|
|
750
|
+
import { homedir as homedir2 } from "os";
|
|
751
|
+
|
|
752
|
+
// src/run/capture.ts
|
|
753
|
+
import { randomUUID } from "crypto";
|
|
754
|
+
import { access, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
755
|
+
import { homedir } from "os";
|
|
756
|
+
import { join, posix, win32 } from "path";
|
|
757
|
+
var DATA_FILE_TARGET_BYTES = 1024 * 1024;
|
|
758
|
+
var RUNLOG_DIRECTORY_MARKER = ".runlog";
|
|
759
|
+
var RUNLOG_DIRECTORY_MARKER_SCHEMA = "runlog.directory.v1";
|
|
760
|
+
var RUNLOG_MANAGED_OUTPUT_NAMES = /* @__PURE__ */ new Set([
|
|
761
|
+
".DS_Store",
|
|
762
|
+
RUNLOG_DIRECTORY_MARKER,
|
|
763
|
+
"events",
|
|
764
|
+
"events.jsonl",
|
|
765
|
+
"run.json",
|
|
766
|
+
"state.json",
|
|
767
|
+
"transcript",
|
|
768
|
+
"transcript.jsonl",
|
|
769
|
+
"upload.json"
|
|
770
|
+
]);
|
|
771
|
+
var RUNLOG_METADATA_SCHEMAS = {
|
|
772
|
+
"run.json": "runlog.capture.v1",
|
|
773
|
+
"state.json": "runlog.state.v1",
|
|
774
|
+
"upload.json": "runlog.upload.v1"
|
|
775
|
+
};
|
|
776
|
+
var RUNLOG_METADATA_FILE_NAMES = Object.keys(RUNLOG_METADATA_SCHEMAS);
|
|
777
|
+
var CaptureOutputPrepareError = class extends Error {
|
|
778
|
+
path;
|
|
779
|
+
constructor(path2, cause) {
|
|
780
|
+
super(errorMessage(cause));
|
|
781
|
+
this.name = "CaptureOutputPrepareError";
|
|
782
|
+
this.path = path2;
|
|
783
|
+
this.cause = cause;
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
function defaultCaptureOutputRoot(home = homedir(), env = process.env, nodePlatform = process.platform) {
|
|
787
|
+
const pathJoin = nodePlatform === "win32" ? win32.join : posix.join;
|
|
788
|
+
if (nodePlatform === "darwin") {
|
|
789
|
+
return pathJoin(home, "Library", "Application Support", "moxt", "runs");
|
|
790
|
+
}
|
|
791
|
+
if (nodePlatform === "win32") {
|
|
792
|
+
return pathJoin(env.LOCALAPPDATA ?? pathJoin(home, "AppData", "Local"), "Moxt", "runs");
|
|
793
|
+
}
|
|
794
|
+
return pathJoin(env.XDG_DATA_HOME ?? pathJoin(home, ".local", "share"), "moxt", "runs");
|
|
635
795
|
}
|
|
636
|
-
function
|
|
796
|
+
function withDefaultCaptureOutput(env, home = homedir()) {
|
|
797
|
+
const { outputDir, outputRoot } = captureEnvPaths(env);
|
|
798
|
+
if (hasValue(outputDir) || hasValue(outputRoot)) {
|
|
799
|
+
return env;
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
...env,
|
|
803
|
+
MOXT_RUN_OUTPUT_ROOT: defaultCaptureOutputRoot(home, env)
|
|
804
|
+
};
|
|
805
|
+
}
|
|
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
|
+
}
|
|
815
|
+
}
|
|
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);
|
|
821
|
+
}
|
|
822
|
+
if (hasValue(outputRoot)) {
|
|
823
|
+
dirs.add(join(outputRoot, runId));
|
|
824
|
+
}
|
|
825
|
+
return [...dirs];
|
|
826
|
+
}
|
|
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
|
+
);
|
|
834
|
+
}
|
|
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
|
+
);
|
|
843
|
+
}
|
|
844
|
+
function createCaptureRunId(startedAt, agent) {
|
|
845
|
+
const timestamp = startedAt.replaceAll(/[^A-Za-z0-9]/g, "-");
|
|
846
|
+
const suffix = randomUUID().replaceAll("-", "").slice(0, 8);
|
|
847
|
+
return agent === void 0 ? `${timestamp}-${suffix}` : `${timestamp}-${agent}-${suffix}`;
|
|
848
|
+
}
|
|
849
|
+
async function writeCaptureDirectoryPayload(payload, env, runId, capturedAt) {
|
|
850
|
+
const hasTranscript = payload.transcript_redacted !== void 0;
|
|
851
|
+
const transcript = hasTranscript ? normalizeJsonl(payload.transcript_redacted ?? "") : "";
|
|
852
|
+
const transcriptChunks = hasTranscript ? chunkJsonl(transcript, "transcript", "transcript") : [];
|
|
853
|
+
const eventChunks = hasTranscript ? chunkJsonl(serializeEventsFromTranscript(transcript, 1, payload.run.agent).text, "events", "events") : [];
|
|
854
|
+
await Promise.all(
|
|
855
|
+
captureOutputDirectories(env, runId).map(async (outputDir) => {
|
|
856
|
+
await prepareCaptureDirectory(outputDir);
|
|
857
|
+
if (hasTranscript) {
|
|
858
|
+
await writeChunks(outputDir, eventChunks);
|
|
859
|
+
await writeChunks(outputDir, transcriptChunks);
|
|
860
|
+
}
|
|
861
|
+
await writeCaptureRunFile(outputDir, payload, runId, {
|
|
862
|
+
events: eventChunks.map(withoutContent),
|
|
863
|
+
transcript: transcriptChunks.map(withoutContent)
|
|
864
|
+
});
|
|
865
|
+
await writeCaptureUploadFile(outputDir, runId);
|
|
866
|
+
await writeCaptureStateFile(outputDir, runId, "closed", capturedAt);
|
|
867
|
+
})
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
function serializeEventsFromTranscript(transcript, startSeq, agent = "claude") {
|
|
871
|
+
const events = normalizeEvents(transcript, startSeq, agent);
|
|
872
|
+
return {
|
|
873
|
+
text: serializeEvents(events),
|
|
874
|
+
nextSeq: startSeq + events.length
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function normalizeJsonl(text) {
|
|
878
|
+
if (text === "") {
|
|
879
|
+
return "";
|
|
880
|
+
}
|
|
881
|
+
return text.endsWith("\n") ? text : `${text}
|
|
882
|
+
`;
|
|
883
|
+
}
|
|
884
|
+
async function prepareCaptureDirectory(outputDir) {
|
|
885
|
+
await mkdir(outputDir, { recursive: true });
|
|
886
|
+
await assertMoxtManagedOutputDir(outputDir);
|
|
887
|
+
await Promise.all([
|
|
888
|
+
rm(join(outputDir, RUNLOG_DIRECTORY_MARKER), { force: true }),
|
|
889
|
+
rm(join(outputDir, "run.json"), { force: true }),
|
|
890
|
+
rm(join(outputDir, "state.json"), { force: true }),
|
|
891
|
+
rm(join(outputDir, "upload.json"), { force: true }),
|
|
892
|
+
rm(join(outputDir, "events.jsonl"), { force: true }),
|
|
893
|
+
rm(join(outputDir, "transcript.jsonl"), { force: true }),
|
|
894
|
+
rm(join(outputDir, "events"), { recursive: true, force: true }),
|
|
895
|
+
rm(join(outputDir, "transcript"), { recursive: true, force: true })
|
|
896
|
+
]);
|
|
897
|
+
await writeRunlogDirectoryMarker(outputDir);
|
|
898
|
+
await mkdir(join(outputDir, "events"), { recursive: true });
|
|
899
|
+
await mkdir(join(outputDir, "transcript"), { recursive: true });
|
|
900
|
+
}
|
|
901
|
+
async function assertMoxtManagedOutputDir(outputDir) {
|
|
902
|
+
const entries = await readdir(outputDir);
|
|
903
|
+
const unmanaged = entries.filter((entry) => !RUNLOG_MANAGED_OUTPUT_NAMES.has(entry));
|
|
904
|
+
if (unmanaged.length > 0) {
|
|
905
|
+
throw new Error(`refusing to use run output directory with unmanaged files: ${unmanaged.slice(0, 5).join(", ")}`);
|
|
906
|
+
}
|
|
907
|
+
const meaningfulEntries = entries.filter((entry) => entry !== ".DS_Store");
|
|
908
|
+
if (meaningfulEntries.length === 0) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (!entries.includes(RUNLOG_DIRECTORY_MARKER)) {
|
|
912
|
+
throw new Error("refusing to use run output directory without ownership marker");
|
|
913
|
+
}
|
|
914
|
+
await assertRunlogDirectoryMarker(outputDir);
|
|
915
|
+
const metadataEntries = RUNLOG_METADATA_FILE_NAMES.filter((entry) => entries.includes(entry));
|
|
916
|
+
if (metadataEntries.length === 0) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
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
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async function readRunlogMetadataRunId(outputDir, fileName) {
|
|
928
|
+
let parsed;
|
|
637
929
|
try {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
|
|
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`);
|
|
643
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;
|
|
644
945
|
}
|
|
645
|
-
function
|
|
646
|
-
|
|
647
|
-
let existing = {};
|
|
946
|
+
async function assertRunlogDirectoryMarker(outputDir) {
|
|
947
|
+
let parsed;
|
|
648
948
|
try {
|
|
649
|
-
|
|
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)}`);
|
|
952
|
+
}
|
|
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`);
|
|
956
|
+
}
|
|
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;
|
|
650
998
|
} catch {
|
|
651
|
-
existing = {};
|
|
652
999
|
}
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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");
|
|
657
1008
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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");
|
|
662
1018
|
}
|
|
663
|
-
function
|
|
664
|
-
|
|
665
|
-
|
|
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 [];
|
|
666
1037
|
}
|
|
667
|
-
|
|
668
|
-
|
|
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;
|
|
669
1077
|
}
|
|
670
|
-
|
|
1078
|
+
flush2();
|
|
1079
|
+
return chunks;
|
|
671
1080
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const state = isTelemetryDisabled() ? "disabled" : "enabled";
|
|
680
|
-
print(`telemetry: ${state}`);
|
|
681
|
-
if (envOverride) {
|
|
682
|
-
print("(disabled via environment variable)");
|
|
683
|
-
} else if (cfg.disabled) {
|
|
684
|
-
print("(disabled via config file)");
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
telemetry.command("enable").description("Enable telemetry").action(() => {
|
|
688
|
-
writeTelemetryConfig({ disabled: false });
|
|
689
|
-
print("telemetry: enabled");
|
|
690
|
-
});
|
|
691
|
-
telemetry.command("disable").description("Disable telemetry").action(() => {
|
|
692
|
-
writeTelemetryConfig({ disabled: true });
|
|
693
|
-
print("telemetry: disabled");
|
|
694
|
-
});
|
|
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
|
+
`);
|
|
695
1088
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
709
|
-
process.exit(1);
|
|
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;
|
|
710
1101
|
}
|
|
711
|
-
|
|
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;
|
|
712
1109
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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;
|
|
718
1163
|
}
|
|
719
|
-
function
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
stopSpinner(true, `Found ${workspaces.length} workspace(s)`);
|
|
731
|
-
for (const ws of workspaces) {
|
|
732
|
-
print(`${colors.bold}${ws.workspaceId}${colors.reset} ${ws.name}`);
|
|
733
|
-
}
|
|
734
|
-
} else {
|
|
735
|
-
stopSpinner(false, "Failed");
|
|
736
|
-
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
737
|
-
process.exit(1);
|
|
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;
|
|
738
1175
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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.0"} (${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
|
+
if (!options.workspace) {
|
|
2855
|
+
return void 0;
|
|
2856
|
+
}
|
|
2857
|
+
if (!process.env.MOXT_API_KEY) {
|
|
2858
|
+
printError("Error: MOXT_API_KEY is required when using moxt run upload mode.");
|
|
2859
|
+
process.exit(1);
|
|
2860
|
+
}
|
|
2861
|
+
return {
|
|
2862
|
+
workspaceId: options.workspace
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
async function runLocalAgentOrExit(agent, agentArgs, upload) {
|
|
2866
|
+
return await runLocalAgent({
|
|
2867
|
+
agent,
|
|
2868
|
+
agentArgs,
|
|
2869
|
+
...upload === void 0 ? {} : { upload }
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// src/telemetry/config.ts
|
|
2874
|
+
import * as fs2 from "fs";
|
|
2875
|
+
import * as os from "os";
|
|
2876
|
+
import * as path from "path";
|
|
2877
|
+
function getConfigPath() {
|
|
2878
|
+
return path.join(os.homedir(), ".moxt", "config.json");
|
|
2879
|
+
}
|
|
2880
|
+
function readTelemetryConfig() {
|
|
2881
|
+
try {
|
|
2882
|
+
const raw = fs2.readFileSync(getConfigPath(), "utf8");
|
|
2883
|
+
const parsed = JSON.parse(raw);
|
|
2884
|
+
return parsed.telemetry ?? {};
|
|
2885
|
+
} catch {
|
|
2886
|
+
return {};
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
function writeTelemetryConfig(update) {
|
|
2890
|
+
const configPath = getConfigPath();
|
|
2891
|
+
let existing = {};
|
|
2892
|
+
try {
|
|
2893
|
+
existing = JSON.parse(fs2.readFileSync(configPath, "utf8"));
|
|
2894
|
+
} catch {
|
|
2895
|
+
existing = {};
|
|
2896
|
+
}
|
|
2897
|
+
const currentTelemetry = existing.telemetry ?? {};
|
|
2898
|
+
const next = { ...existing, telemetry: { ...currentTelemetry, ...update } };
|
|
2899
|
+
fs2.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
2900
|
+
fs2.writeFileSync(configPath, JSON.stringify(next, null, 2));
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
// src/telemetry/opt-out.ts
|
|
2904
|
+
function isCiEnvironment() {
|
|
2905
|
+
return process.env.CI === "true" || process.env.CI === "1";
|
|
2906
|
+
}
|
|
2907
|
+
function isTelemetryDisabled() {
|
|
2908
|
+
if (process.env.MOXT_TELEMETRY_DISABLED === "1" || process.env.MOXT_TELEMETRY_DISABLED === "true") {
|
|
2909
|
+
return true;
|
|
2910
|
+
}
|
|
2911
|
+
if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true") {
|
|
2912
|
+
return true;
|
|
2913
|
+
}
|
|
2914
|
+
return readTelemetryConfig().disabled === true;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// src/commands/telemetry.ts
|
|
2918
|
+
function registerTelemetryCommand(program2) {
|
|
2919
|
+
const telemetry = program2.command("telemetry").description("Manage Moxt CLI telemetry");
|
|
2920
|
+
telemetry.command("status").description("Show telemetry status").action(() => {
|
|
2921
|
+
const cfg = readTelemetryConfig();
|
|
2922
|
+
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";
|
|
2923
|
+
const state = isTelemetryDisabled() ? "disabled" : "enabled";
|
|
2924
|
+
print(`telemetry: ${state}`);
|
|
2925
|
+
if (envOverride) {
|
|
2926
|
+
print("(disabled via environment variable)");
|
|
2927
|
+
} else if (cfg.disabled) {
|
|
2928
|
+
print("(disabled via config file)");
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
telemetry.command("enable").description("Enable telemetry").action(() => {
|
|
2932
|
+
writeTelemetryConfig({ disabled: false });
|
|
2933
|
+
print("telemetry: enabled");
|
|
2934
|
+
});
|
|
2935
|
+
telemetry.command("disable").description("Disable telemetry").action(() => {
|
|
2936
|
+
writeTelemetryConfig({ disabled: true });
|
|
2937
|
+
print("telemetry: disabled");
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
// src/commands/whoami.ts
|
|
2942
|
+
function registerWhoamiCommand(program2) {
|
|
2943
|
+
program2.command("whoami").description("Show current user information").action(async () => {
|
|
2944
|
+
startSpinner("Fetching user info...");
|
|
2945
|
+
const response = await httpGet("/users/whoami");
|
|
2946
|
+
if (response.ok) {
|
|
2947
|
+
stopSpinner(true, "Done");
|
|
2948
|
+
print(`${colors.bold}Nickname${colors.reset}: ${response.data.nickname}`);
|
|
2949
|
+
print(`${colors.bold}Email${colors.reset}: ${response.data.email}`);
|
|
2950
|
+
} else {
|
|
2951
|
+
stopSpinner(false, "Failed");
|
|
2952
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
2953
|
+
process.exit(1);
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// src/commands/workspace.ts
|
|
2959
|
+
var emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
2960
|
+
function isValidEmail(email) {
|
|
2961
|
+
return emailRegex.test(email);
|
|
2962
|
+
}
|
|
2963
|
+
function registerWorkspaceCommand(program2) {
|
|
2964
|
+
const workspace = program2.command("workspace").description("Workspace management");
|
|
2965
|
+
workspace.command("list").description("List all workspaces you belong to").action(async () => {
|
|
2966
|
+
startSpinner("Fetching workspaces...");
|
|
2967
|
+
const response = await httpGet("/workspaces");
|
|
2968
|
+
if (response.ok) {
|
|
2969
|
+
const { workspaces } = response.data;
|
|
2970
|
+
if (workspaces.length === 0) {
|
|
2971
|
+
stopSpinner(true, "No workspaces found.");
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
stopSpinner(true, `Found ${workspaces.length} workspace(s)`);
|
|
2975
|
+
for (const ws of workspaces) {
|
|
2976
|
+
print(`${colors.bold}${ws.workspaceId}${colors.reset} ${ws.name}`);
|
|
2977
|
+
}
|
|
2978
|
+
} else {
|
|
2979
|
+
stopSpinner(false, "Failed");
|
|
2980
|
+
printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
|
|
2981
|
+
process.exit(1);
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
const members = program2.command("members").description("Manage workspace members");
|
|
2985
|
+
members.command("list").description("List all members in a workspace").requiredOption("-w, --workspace <workspaceId>", "Workspace ID").action(async (options) => {
|
|
2986
|
+
const { workspace: workspaceId } = options;
|
|
743
2987
|
startSpinner("Fetching workspace members...");
|
|
744
2988
|
const response = await httpGet(`/workspaces/${workspaceId}/members`);
|
|
745
2989
|
if (!response.ok) {
|
|
@@ -823,17 +3067,33 @@ function registerWorkspaceCommand(program2) {
|
|
|
823
3067
|
});
|
|
824
3068
|
}
|
|
825
3069
|
|
|
3070
|
+
// src/program.ts
|
|
3071
|
+
function createProgram(version) {
|
|
3072
|
+
const program2 = new Command();
|
|
3073
|
+
program2.name("moxt").description("Moxt CLI - AI Workspace").version(version).action(() => {
|
|
3074
|
+
program2.help();
|
|
3075
|
+
});
|
|
3076
|
+
registerWhoamiCommand(program2);
|
|
3077
|
+
registerWorkspaceCommand(program2);
|
|
3078
|
+
registerFileCommand(program2);
|
|
3079
|
+
registerMemoryCommand(program2);
|
|
3080
|
+
registerRunCommand(program2);
|
|
3081
|
+
registerMiniappCommand(program2);
|
|
3082
|
+
registerTelemetryCommand(program2);
|
|
3083
|
+
return program2;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
826
3086
|
// src/telemetry/client.ts
|
|
827
|
-
import { arch as
|
|
3087
|
+
import { arch as arch3, platform as platform3 } from "os";
|
|
828
3088
|
var buffer = [];
|
|
829
3089
|
var FLUSH_TIMEOUT_MS = 3e3;
|
|
830
3090
|
var MAX_BATCH_SIZE = 100;
|
|
831
3091
|
function commonLabels() {
|
|
832
3092
|
return {
|
|
833
|
-
cli_version: "0.3.
|
|
3093
|
+
cli_version: "0.3.3-moxt-run.0",
|
|
834
3094
|
node_version: process.versions.node,
|
|
835
|
-
os:
|
|
836
|
-
arch:
|
|
3095
|
+
os: platform3(),
|
|
3096
|
+
arch: arch3(),
|
|
837
3097
|
is_ci: isCiEnvironment() ? "true" : "false"
|
|
838
3098
|
};
|
|
839
3099
|
}
|
|
@@ -876,7 +3136,7 @@ async function flush() {
|
|
|
876
3136
|
}
|
|
877
3137
|
const batch = buffer.splice(0, MAX_BATCH_SIZE);
|
|
878
3138
|
const payload = {
|
|
879
|
-
release_id: "0.3.
|
|
3139
|
+
release_id: "0.3.3-moxt-run.0",
|
|
880
3140
|
client_type: "cli",
|
|
881
3141
|
metric_data_list: batch.map((m) => ({
|
|
882
3142
|
profile: "cli",
|
|
@@ -1017,19 +3277,11 @@ function installExitHandler() {
|
|
|
1017
3277
|
|
|
1018
3278
|
// src/index.ts
|
|
1019
3279
|
updateNotifier({
|
|
1020
|
-
pkg: { name: "@moxt-ai/cli", version: "0.3.
|
|
3280
|
+
pkg: { name: "@moxt-ai/cli", version: "0.3.3-moxt-run.0" }
|
|
1021
3281
|
}).notify();
|
|
1022
|
-
var program =
|
|
1023
|
-
program.name("moxt").description("Moxt CLI - AI Workspace").version("0.3.1").action(() => {
|
|
1024
|
-
program.help();
|
|
1025
|
-
});
|
|
3282
|
+
var program = createProgram("0.3.3-moxt-run.0");
|
|
1026
3283
|
instrumentProgram(program);
|
|
1027
3284
|
installExitHandler();
|
|
1028
|
-
registerWhoamiCommand(program);
|
|
1029
|
-
registerWorkspaceCommand(program);
|
|
1030
|
-
registerFileCommand(program);
|
|
1031
|
-
registerMiniappCommand(program);
|
|
1032
|
-
registerTelemetryCommand(program);
|
|
1033
3285
|
program.parseAsync(process.argv).then(async () => {
|
|
1034
3286
|
await flush();
|
|
1035
3287
|
}).catch(async (err) => {
|