@flowmind/cli 0.3.8 → 0.3.9
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 +10 -8
- package/dist/flowmind/SKILL.md +1 -1
- package/dist/index.js +1592 -1380
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -293,9 +293,9 @@ var isHttpEvidenceUrl = (value) => {
|
|
|
293
293
|
};
|
|
294
294
|
|
|
295
295
|
// src/agent-session.ts
|
|
296
|
-
import { createHash as
|
|
297
|
-
import { appendFile, mkdir as
|
|
298
|
-
import { join as
|
|
296
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
297
|
+
import { appendFile, mkdir as mkdir5, readdir as readdir7, readFile as readFile6, rename as rename4, rm as rm3, rmdir, stat as stat3, utimes, writeFile as writeFile5 } from "node:fs/promises";
|
|
298
|
+
import { join as join8 } from "node:path";
|
|
299
299
|
|
|
300
300
|
// src/awake-clock.ts
|
|
301
301
|
import { execFileSync } from "node:child_process";
|
|
@@ -513,6 +513,33 @@ function bindAccount(apiKey, account, since, url = API_URL) {
|
|
|
513
513
|
if (url === API_URL) Object.assign(snapshot, updated);
|
|
514
514
|
}
|
|
515
515
|
var accountBinding = () => checkedBinding(snapshot, API_URL);
|
|
516
|
+
var legacySessionsDir = () => join3(dirname(config.path), "agent-sessions");
|
|
517
|
+
function soleAccountProfile() {
|
|
518
|
+
const current = config.store;
|
|
519
|
+
const periods = current.signedOutPeriods;
|
|
520
|
+
return checkedBinding(current, API_URL) !== null && (periods === void 0 || Object.keys(periods).length === 0);
|
|
521
|
+
}
|
|
522
|
+
function extendSignInStart(since) {
|
|
523
|
+
const current = config.store;
|
|
524
|
+
const binding = checkedBinding(current, API_URL);
|
|
525
|
+
const pinned = accountBinding();
|
|
526
|
+
if (!binding || !pinned || binding.credential !== pinned.credential || binding.account !== pinned.account) {
|
|
527
|
+
throw new Error("The sign-in changed while Flowmind adopted older local records. Run the command again.");
|
|
528
|
+
}
|
|
529
|
+
if (!soleAccountProfile() || !validPoint(since) || Date.parse(since.at) >= Date.parse(binding.since.at)) return;
|
|
530
|
+
const updated = { ...current, binding: { ...binding, since } };
|
|
531
|
+
config.store = updated;
|
|
532
|
+
Object.assign(snapshot, updated);
|
|
533
|
+
}
|
|
534
|
+
function refreshSignInStart() {
|
|
535
|
+
const pinned = accountBinding();
|
|
536
|
+
if (!pinned) return;
|
|
537
|
+
const stored = checkedBinding(config.store, API_URL);
|
|
538
|
+
if (!stored || stored.account !== pinned.account || stored.credential !== pinned.credential || (stored.origin ?? API_URL) !== (pinned.origin ?? API_URL)) {
|
|
539
|
+
throw new Error("The sign-in changed while this command ran. Run it again.");
|
|
540
|
+
}
|
|
541
|
+
if (Date.parse(stored.since.at) < Date.parse(pinned.since.at)) snapshot.binding = { ...pinned, since: stored.since };
|
|
542
|
+
}
|
|
516
543
|
function ownershipPeriods() {
|
|
517
544
|
const binding = accountBinding();
|
|
518
545
|
if (!binding) throw new Error("Sign in to measure agent time.");
|
|
@@ -612,9 +639,12 @@ async function writeIgnore(start, reason) {
|
|
|
612
639
|
}
|
|
613
640
|
|
|
614
641
|
// src/manual-save.ts
|
|
615
|
-
import { createHash as
|
|
616
|
-
import { mkdir as
|
|
617
|
-
import { join as
|
|
642
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
|
|
643
|
+
import { mkdir as mkdir4, readdir as readdir6, readFile as readFile5, rename as rename3, unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
|
|
644
|
+
import { join as join7 } from "node:path";
|
|
645
|
+
|
|
646
|
+
// src/account.ts
|
|
647
|
+
import { readdir as readdir5 } from "node:fs/promises";
|
|
618
648
|
|
|
619
649
|
// ../../packages/contracts/src/generated/revision.ts
|
|
620
650
|
var contractRevision = "3620e18f995dfc34dee05d8a132148178ce03b4a0223aafb99778e00e5585ef4";
|
|
@@ -944,292 +974,694 @@ async function apiRequest(endpoint, args, options = {}) {
|
|
|
944
974
|
}
|
|
945
975
|
}
|
|
946
976
|
|
|
947
|
-
// src/
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
977
|
+
// src/legacy-proofs.ts
|
|
978
|
+
import { mkdir as mkdir3, readdir as readdir4, readFile as readFile4, rename as rename2, rm as rm2, stat as stat2 } from "node:fs/promises";
|
|
979
|
+
import { join as join6 } from "node:path";
|
|
980
|
+
|
|
981
|
+
// src/pending-stages.ts
|
|
982
|
+
import { spawn } from "node:child_process";
|
|
983
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
984
|
+
import { mkdir as mkdir2, readdir as readdir3, readFile as readFile3, rename, rm, writeFile as writeFile3 } from "node:fs/promises";
|
|
985
|
+
import { basename, join as join5 } from "node:path";
|
|
986
|
+
|
|
987
|
+
// src/stored-stage-input.ts
|
|
988
|
+
var SOURCE_KINDS = [
|
|
989
|
+
"calendar",
|
|
990
|
+
"claude_code",
|
|
991
|
+
"cli",
|
|
992
|
+
"codex",
|
|
993
|
+
"github",
|
|
994
|
+
"linear",
|
|
995
|
+
"manual",
|
|
996
|
+
"session_log",
|
|
997
|
+
"other"
|
|
998
|
+
];
|
|
999
|
+
var evidenceKinds = ["calendar_event", "commit", "issue", "linear_issue", "pr", "session_log", "other"];
|
|
1000
|
+
var shape = (v6, keys2) => v6 !== null && typeof v6 === "object" && !Array.isArray(v6) && Object.keys(v6).every((key) => keys2.includes(key));
|
|
1001
|
+
var text = (v6, max) => typeof v6 === "string" && v6.trim().length > 0 && [...v6].length <= max;
|
|
1002
|
+
var optionalText = (v6, max) => v6 === void 0 || v6 === null || text(v6, max);
|
|
1003
|
+
var member = (v6, kinds) => typeof v6 === "string" && kinds.includes(v6);
|
|
1004
|
+
var isoDate = (v6) => typeof v6 === "string" && Number.isFinite(Date.parse(v6)) && new Date(v6).toISOString() === v6;
|
|
1005
|
+
var keys = [
|
|
1006
|
+
"targetKind",
|
|
1007
|
+
"projectId",
|
|
1008
|
+
"startedAt",
|
|
1009
|
+
"durationMinutes",
|
|
1010
|
+
"description",
|
|
1011
|
+
"note",
|
|
1012
|
+
"details",
|
|
1013
|
+
"artifactUrl",
|
|
1014
|
+
"source",
|
|
1015
|
+
"evidence"
|
|
1016
|
+
];
|
|
1017
|
+
function isStoredStageInput(v6) {
|
|
1018
|
+
if (!shape(v6, keys) || v6.targetKind !== "project" || typeof v6.projectId !== "string" || !isProjectId(v6.projectId) || !isoDate(v6.startedAt) || typeof v6.durationMinutes !== "number" || !Number.isSafeInteger(v6.durationMinutes) || v6.durationMinutes < 1 || !text(v6.description, 280) || !text(v6.note, AGENT_OUTCOME_MAX_LENGTH) || /[\r\n]/.test(v6.note) || v6.artifactUrl !== void 0 && (!text(v6.artifactUrl, 2048) || !isHttpEvidenceUrl(v6.artifactUrl)))
|
|
1019
|
+
return false;
|
|
1020
|
+
const details = v6.details === void 0 ? [] : v6.details;
|
|
1021
|
+
if (!Array.isArray(details) || !details.every((d5) => text(d5, AGENT_DETAIL_MAX_LENGTH) && !/[\r\n]/.test(d5)) || agentProofIssues({ outcome: v6.note, details }).length)
|
|
1022
|
+
return false;
|
|
1023
|
+
const source = v6.source;
|
|
1024
|
+
return shape(source, ["kind", "externalId", "label"]) && member(source.kind, SOURCE_KINDS) && optionalText(source.externalId, 200) && optionalText(source.label, 160) && Array.isArray(v6.evidence) && v6.evidence.length <= 20 && v6.evidence.every(
|
|
1025
|
+
(e4) => shape(e4, ["kind", "label", "externalId", "url"]) && member(e4.kind, evidenceKinds) && text(e4.label, 160) && optionalText(e4.externalId, 200) && (e4.url === null || e4.url === void 0 || text(e4.url, 500) && isHttpEvidenceUrl(e4.url))
|
|
1026
|
+
);
|
|
987
1027
|
}
|
|
988
1028
|
|
|
989
|
-
// src/
|
|
990
|
-
var
|
|
991
|
-
var
|
|
992
|
-
var
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1029
|
+
// src/pending-stages.ts
|
|
1030
|
+
var STAGE_KEY_IN_USE = "This stage key is already in use";
|
|
1031
|
+
var MAX_BACKOFF_MINUTES = 60;
|
|
1032
|
+
var outboxDir = () => join5(accountDirectory(), "pending-stages");
|
|
1033
|
+
var takeStageLock = () => tryLock(join5(accountDirectory(), "stage-writers"));
|
|
1034
|
+
var fileOf = (id) => join5(outboxDir(), id + ".json");
|
|
1035
|
+
var nextAttempt = (attempts, now) => new Date(now.getTime() + Math.min(MAX_BACKOFF_MINUTES, 2 ** Math.max(0, attempts - 1)) * 6e4).toISOString();
|
|
1036
|
+
var errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
1037
|
+
var classifyStageFailure = (error) => {
|
|
1038
|
+
if (!(error instanceof BridgeError)) return { kind: "unexpected", reason: errorText(error) };
|
|
1039
|
+
const { kind, status } = error.context;
|
|
1040
|
+
if (kind === "transport") return { kind: "retry", reason: "The Flowmind API could not be reached in time" };
|
|
1041
|
+
if (kind === "contract")
|
|
1042
|
+
return {
|
|
1043
|
+
kind: "retry",
|
|
1044
|
+
reason: "Flowmind's answer could not be read" + (status === null ? "" : " (HTTP " + status + ")")
|
|
1045
|
+
};
|
|
1046
|
+
const message = error instanceof ApiRejectionError ? error.rejectionMessage : "The Flowmind API answered HTTP " + status;
|
|
1047
|
+
if (status === 401 || status === 403) return { kind: "login", reason: message };
|
|
1048
|
+
if (status === 409 && message === STAGE_KEY_IN_USE) return { kind: "conflict", reason: message };
|
|
1049
|
+
if (status !== null && status >= 400 && status < 500 && status !== 408 && status !== 429)
|
|
1050
|
+
return { kind: "reject", reason: message };
|
|
1051
|
+
return { kind: "retry", reason: message };
|
|
1052
|
+
};
|
|
1053
|
+
var stageIdOf = (idempotencyKey, input2) => createHash2("sha256").update(idempotencyKey + "\n" + JSON.stringify(input2)).digest("hex").slice(0, 16);
|
|
1054
|
+
var cutDigestOf = (id, sessionId, cut) => createHash2("sha256").update(JSON.stringify([id, sessionId, cut === null ? null : [cut.at, cut.up]])).digest("hex");
|
|
1055
|
+
function readStage(value, fileName, legacy = false) {
|
|
1056
|
+
const stage = value;
|
|
1057
|
+
if (typeof stage !== "object" || stage === null) return null;
|
|
1058
|
+
const input2 = stage.input;
|
|
1059
|
+
const { id, sessionId, cut } = stage;
|
|
1060
|
+
const valid = typeof id === "string" && /^[a-f0-9]{16}$/.test(id) && fileName === id + ".json" && typeof stage.idempotencyKey === "string" && stage.idempotencyKey.trim() === stage.idempotencyKey && /^[\x20-\x7e]+$/.test(stage.idempotencyKey) && typeof stage.apiUrl === "string" && isStoredStageInput(input2) && (legacy && stage.credentialFingerprint === void 0 || typeof stage.credentialFingerprint === "string" && /^[a-f0-9]{64}$/.test(stage.credentialFingerprint)) && stageIdOf(stage.idempotencyKey, input2) === id && isoDate(stage.savedAt) && isoDate(stage.nextAttemptAt) && (stage.lastAttemptAt === null || isoDate(stage.lastAttemptAt)) && (sessionId === null || typeof sessionId === "string" && sessionId.length > 0) && (cut === null || typeof cut === "object" && isoDate(cut.at) && Date.parse(cut.at) <= Date.parse(stage.savedAt) && (cut.up === null || Number.isFinite(cut.up))) && sessionId === null === (cut === null) && (stage.state === "waiting" || stage.state === "rejected" || stage.state === "conflict") && Number.isSafeInteger(stage.attempts) && stage.attempts >= 0 && (typeof stage.lastError === "string" || stage.state === "waiting" && stage.lastError === null);
|
|
1061
|
+
if (!valid) return null;
|
|
1062
|
+
const cutDigest = cutDigestOf(id, sessionId, cut);
|
|
1063
|
+
if (stage.cutDigest !== cutDigest && !(legacy && stage.cutDigest === void 0)) return null;
|
|
1064
|
+
return { ...stage, credentialFingerprint: stage.credentialFingerprint ?? null, cutDigest };
|
|
1002
1065
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1066
|
+
var writePendingStage = async (stage) => {
|
|
1067
|
+
await mkdir2(outboxDir(), { recursive: true });
|
|
1068
|
+
const temporary = fileOf(stage.id) + "." + process.pid + "." + randomUUID2() + ".tmp";
|
|
1069
|
+
await writeFile3(temporary, JSON.stringify(stage, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
1070
|
+
await rename(temporary, fileOf(stage.id));
|
|
1071
|
+
};
|
|
1072
|
+
async function saveStageBeforeSending(input2) {
|
|
1073
|
+
const at2 = input2.now.toISOString();
|
|
1074
|
+
const id = stageIdOf(input2.idempotencyKey, input2.stage);
|
|
1075
|
+
const pending = {
|
|
1076
|
+
id,
|
|
1077
|
+
savedAt: at2,
|
|
1078
|
+
apiUrl: API_URL,
|
|
1079
|
+
idempotencyKey: input2.idempotencyKey,
|
|
1080
|
+
credentialFingerprint: accountBinding().credential,
|
|
1081
|
+
input: input2.stage,
|
|
1082
|
+
sessionId: input2.sessionId,
|
|
1083
|
+
cut: input2.cut,
|
|
1084
|
+
cutDigest: cutDigestOf(id, input2.sessionId, input2.cut),
|
|
1085
|
+
state: "waiting",
|
|
1086
|
+
attempts: 0,
|
|
1087
|
+
lastAttemptAt: null,
|
|
1088
|
+
nextAttemptAt: at2,
|
|
1089
|
+
lastError: null
|
|
1090
|
+
};
|
|
1091
|
+
if (!readStage(pending, pending.id + ".json"))
|
|
1092
|
+
throw new Error("The stage request does not match the Flowmind proof contract. Check its fields before retrying.");
|
|
1093
|
+
const { stages, unreadable } = await listPendingStages();
|
|
1094
|
+
if (unreadable.includes(fileOf(pending.id)) || stages.some((stage) => stage.id === pending.id)) {
|
|
1095
|
+
throw new Error("This proof (" + pending.id + ") is already saved. See: flowmind agent pending");
|
|
1007
1096
|
}
|
|
1008
|
-
|
|
1097
|
+
await writePendingStage(pending);
|
|
1098
|
+
return pending;
|
|
1009
1099
|
}
|
|
1010
|
-
async function
|
|
1011
|
-
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
const temp = fileOf(id) + "." + randomUUID2() + ".tmp";
|
|
1023
|
-
await writeFile3(temp, JSON.stringify(saved), { mode: 384, flag: "wx" });
|
|
1024
|
-
await rename(temp, fileOf(id));
|
|
1025
|
-
}
|
|
1026
|
-
if (Date.now() - saved.savedAt >= RECEIPT_MS) {
|
|
1027
|
-
throw new Error(
|
|
1028
|
-
`This log (${id}) was not confirmed within 24 hours. Check Activity, then run flowmind pending-logs done ${id} if it is there, or flowmind pending-logs discard ${id} to log it again.`
|
|
1029
|
-
);
|
|
1030
|
-
}
|
|
1031
|
-
let result;
|
|
1032
|
-
try {
|
|
1033
|
-
result = await send(saved.body, saved.key);
|
|
1034
|
-
} catch (error) {
|
|
1035
|
-
const status = error instanceof BridgeError && error.context.kind === "api" ? error.context.status : null;
|
|
1036
|
-
if (status !== null && status >= 400 && status < 500 && ![401, 403, 408, 409, 429].includes(status))
|
|
1037
|
-
await unlink2(fileOf(id));
|
|
1038
|
-
else console.error(`This log (${id}) was not confirmed. Run the same command to retry the saved request.`);
|
|
1039
|
-
throw error;
|
|
1040
|
-
}
|
|
1041
|
-
await unlink2(fileOf(id));
|
|
1042
|
-
return result;
|
|
1043
|
-
});
|
|
1100
|
+
async function recordStageFailure(stage, failure, now) {
|
|
1101
|
+
const attempts = stage.attempts + 1;
|
|
1102
|
+
const next = {
|
|
1103
|
+
...stage,
|
|
1104
|
+
state: failure.kind === "conflict" || stage.state === "conflict" ? "conflict" : failure.kind === "reject" ? "rejected" : "waiting",
|
|
1105
|
+
attempts,
|
|
1106
|
+
lastAttemptAt: now.toISOString(),
|
|
1107
|
+
nextAttemptAt: nextAttempt(attempts, now),
|
|
1108
|
+
lastError: failure.kind === "conflict" || stage.state === "conflict" ? conflictAdvice(stage.id) : failure.reason
|
|
1109
|
+
};
|
|
1110
|
+
await writePendingStage(next);
|
|
1111
|
+
return next;
|
|
1044
1112
|
}
|
|
1045
|
-
async function
|
|
1046
|
-
await
|
|
1113
|
+
async function forgetStage(id) {
|
|
1114
|
+
await rm(fileOf(id), { force: true });
|
|
1115
|
+
}
|
|
1116
|
+
async function listPendingStages() {
|
|
1047
1117
|
let names;
|
|
1048
1118
|
try {
|
|
1049
|
-
names = await readdir3(
|
|
1119
|
+
names = await readdir3(outboxDir());
|
|
1050
1120
|
} catch (error) {
|
|
1051
|
-
if (error.code === "ENOENT") return [];
|
|
1121
|
+
if (error.code === "ENOENT") return { stages: [], unreadable: [] };
|
|
1052
1122
|
throw error;
|
|
1053
1123
|
}
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
const text2 = (value) => typeof value === "string";
|
|
1060
|
-
logs.push({
|
|
1061
|
-
id,
|
|
1062
|
-
savedAt: new Date(saved.savedAt).toISOString(),
|
|
1063
|
-
expired: Date.now() - saved.savedAt >= RECEIPT_MS,
|
|
1064
|
-
projectId: field("projectId", text2),
|
|
1065
|
-
durationMinutes: field("durationMinutes", Number.isSafeInteger),
|
|
1066
|
-
startedAt: field("startedAt", text2),
|
|
1067
|
-
date: field("date", text2)
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
return logs.sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt));
|
|
1071
|
-
}
|
|
1072
|
-
async function resolveSavedLog(id, resolution) {
|
|
1073
|
-
await ensureAccount();
|
|
1074
|
-
if (!/^[a-f0-9]{16}$/.test(id)) throw new Error("No saved log " + id + ". See: flowmind pending-logs");
|
|
1075
|
-
await withWriters(async () => {
|
|
1076
|
-
let saved;
|
|
1124
|
+
const stages = [];
|
|
1125
|
+
const unreadable = [];
|
|
1126
|
+
for (const name of names.filter((entry) => entry.endsWith(".json"))) {
|
|
1127
|
+
const path = join5(outboxDir(), name);
|
|
1128
|
+
let text2;
|
|
1077
1129
|
try {
|
|
1078
|
-
|
|
1130
|
+
text2 = await readFile3(path, "utf8");
|
|
1079
1131
|
} catch (error) {
|
|
1080
|
-
if (error.code === "ENOENT")
|
|
1081
|
-
|
|
1082
|
-
|
|
1132
|
+
if (error.code === "ENOENT") continue;
|
|
1133
|
+
unreadable.push(path);
|
|
1134
|
+
continue;
|
|
1083
1135
|
}
|
|
1084
|
-
|
|
1085
|
-
await mkdir2(join5(savesDir(), "resolved"), { recursive: true, mode: 448 });
|
|
1086
|
-
await writeFile3(
|
|
1087
|
-
join5(savesDir(), "resolved", id + "-" + saved.savedAt + ".json"),
|
|
1088
|
-
JSON.stringify({ ...saved, resolution: "checked-in-activity", resolvedAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
1089
|
-
{ mode: 384, flag: "wx" }
|
|
1090
|
-
);
|
|
1091
|
-
await unlink2(fileOf(id));
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// ../../packages/contracts/src/generated/validators/shared/OUWE7ZSG.js
|
|
1096
|
-
var U = c((r2) => {
|
|
1097
|
-
"use strict";
|
|
1098
|
-
Object.defineProperty(r2, "__esModule", { value: true });
|
|
1099
|
-
r2.formatNames = r2.fastFormats = r2.fullFormats = void 0;
|
|
1100
|
-
function n7(a5, d5) {
|
|
1101
|
-
return { validate: a5, compare: d5 };
|
|
1102
|
-
}
|
|
1103
|
-
r2.fullFormats = { date: n7(b4, m6), time: n7(c5(true), $2), "date-time": n7(v6(true), N2), "iso-time": n7(c5(), A2), "iso-date-time": n7(v6(), g), duration: /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/, uri: R2, "uri-reference": /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i, "uri-template": /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i, url: /^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu, email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, hostname: /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i, ipv4: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/, ipv6: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i, regex: P4, uuid: /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i, "json-pointer": /^(?:\/(?:[^~/]|~0|~1)*)*$/, "json-pointer-uri-fragment": /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i, "relative-json-pointer": /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/, byte: w3, int32: { type: "number", validate: Z }, int64: { type: "number", validate: H2 }, float: { type: "number", validate: D2 }, double: { type: "number", validate: D2 }, password: true, binary: true };
|
|
1104
|
-
r2.fastFormats = { ...r2.fullFormats, date: n7(/^\d\d\d\d-[0-1]\d-[0-3]\d$/, m6), time: n7(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, $2), "date-time": n7(/^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, N2), "iso-time": n7(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, A2), "iso-date-time": n7(/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, g), uri: /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i, "uri-reference": /^(?:(?:[a-z][a-z0-9+\-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i };
|
|
1105
|
-
r2.formatNames = Object.keys(r2.fullFormats);
|
|
1106
|
-
function y3(a5) {
|
|
1107
|
-
return a5 % 4 === 0 && (a5 % 100 !== 0 || a5 % 400 === 0);
|
|
1108
|
-
}
|
|
1109
|
-
var O2 = /^(\d\d\d\d)-(\d\d)-(\d\d)$/, h10 = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
1110
|
-
function b4(a5) {
|
|
1111
|
-
let d5 = O2.exec(a5);
|
|
1112
|
-
if (!d5) return false;
|
|
1113
|
-
let t8 = +d5[1], e4 = +d5[2], f3 = +d5[3];
|
|
1114
|
-
return e4 >= 1 && e4 <= 12 && f3 >= 1 && f3 <= (e4 === 2 && y3(t8) ? 29 : h10[e4]);
|
|
1115
|
-
}
|
|
1116
|
-
function m6(a5, d5) {
|
|
1117
|
-
if (a5 && d5) return a5 > d5 ? 1 : a5 < d5 ? -1 : 0;
|
|
1118
|
-
}
|
|
1119
|
-
var s7 = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
1120
|
-
function c5(a5) {
|
|
1121
|
-
return function(t8) {
|
|
1122
|
-
let e4 = s7.exec(t8);
|
|
1123
|
-
if (!e4) return false;
|
|
1124
|
-
let f3 = +e4[1], u4 = +e4[2], i5 = +e4[3], M = e4[4], l4 = e4[5] === "-" ? -1 : 1, _ = +(e4[6] || 0), p6 = +(e4[7] || 0);
|
|
1125
|
-
if (_ > 23 || p6 > 59 || a5 && !M) return false;
|
|
1126
|
-
if (f3 <= 23 && u4 <= 59 && i5 < 60) return true;
|
|
1127
|
-
let o11 = u4 - p6 * l4, T2 = f3 - _ * l4 - (o11 < 0 ? 1 : 0);
|
|
1128
|
-
return (T2 === 23 || T2 === -1) && (o11 === 59 || o11 === -1) && i5 < 61;
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
function $2(a5, d5) {
|
|
1132
|
-
if (!(a5 && d5)) return;
|
|
1133
|
-
let t8 = (/* @__PURE__ */ new Date("2020-01-01T" + a5)).valueOf(), e4 = (/* @__PURE__ */ new Date("2020-01-01T" + d5)).valueOf();
|
|
1134
|
-
if (t8 && e4) return t8 - e4;
|
|
1135
|
-
}
|
|
1136
|
-
function A2(a5, d5) {
|
|
1137
|
-
if (!(a5 && d5)) return;
|
|
1138
|
-
let t8 = s7.exec(a5), e4 = s7.exec(d5);
|
|
1139
|
-
if (t8 && e4) return a5 = t8[1] + t8[2] + t8[3], d5 = e4[1] + e4[2] + e4[3], a5 > d5 ? 1 : a5 < d5 ? -1 : 0;
|
|
1140
|
-
}
|
|
1141
|
-
var z = /t|\s/i;
|
|
1142
|
-
function v6(a5) {
|
|
1143
|
-
let d5 = c5(a5);
|
|
1144
|
-
return function(e4) {
|
|
1145
|
-
let f3 = e4.split(z);
|
|
1146
|
-
return f3.length === 2 && b4(f3[0]) && d5(f3[1]);
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
function N2(a5, d5) {
|
|
1150
|
-
if (!(a5 && d5)) return;
|
|
1151
|
-
let t8 = new Date(a5).valueOf(), e4 = new Date(d5).valueOf();
|
|
1152
|
-
if (t8 && e4) return t8 - e4;
|
|
1153
|
-
}
|
|
1154
|
-
function g(a5, d5) {
|
|
1155
|
-
if (!(a5 && d5)) return;
|
|
1156
|
-
let [t8, e4] = a5.split(z), [f3, u4] = d5.split(z), i5 = m6(t8, f3);
|
|
1157
|
-
if (i5 !== void 0) return i5 || $2(e4, u4);
|
|
1158
|
-
}
|
|
1159
|
-
var E = /\/|:/, F2 = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
|
|
1160
|
-
function R2(a5) {
|
|
1161
|
-
return E.test(a5) && F2.test(a5);
|
|
1162
|
-
}
|
|
1163
|
-
var I2 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
|
|
1164
|
-
function w3(a5) {
|
|
1165
|
-
return I2.lastIndex = 0, I2.test(a5);
|
|
1166
|
-
}
|
|
1167
|
-
var S3 = -(2 ** 31), j2 = 2 ** 31 - 1;
|
|
1168
|
-
function Z(a5) {
|
|
1169
|
-
return Number.isInteger(a5) && a5 <= j2 && a5 >= S3;
|
|
1170
|
-
}
|
|
1171
|
-
function H2(a5) {
|
|
1172
|
-
return Number.isInteger(a5);
|
|
1173
|
-
}
|
|
1174
|
-
function D2() {
|
|
1175
|
-
return true;
|
|
1176
|
-
}
|
|
1177
|
-
var Y = /[^\\]\\Z/;
|
|
1178
|
-
function P4(a5) {
|
|
1179
|
-
if (Y.test(a5)) return false;
|
|
1136
|
+
let value;
|
|
1180
1137
|
try {
|
|
1181
|
-
|
|
1138
|
+
value = JSON.parse(text2);
|
|
1182
1139
|
} catch {
|
|
1183
|
-
|
|
1140
|
+
value = null;
|
|
1184
1141
|
}
|
|
1142
|
+
const stage = readStage(value, name);
|
|
1143
|
+
if (stage && stage.apiUrl === API_URL) stages.push(stage);
|
|
1144
|
+
else unreadable.push(path);
|
|
1185
1145
|
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
if (
|
|
1197
|
-
{
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1146
|
+
return { stages: stages.sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt)), unreadable };
|
|
1147
|
+
}
|
|
1148
|
+
async function restoreSavedStageMarks(measuring = false) {
|
|
1149
|
+
const { stages, unreadable } = await listPendingStages();
|
|
1150
|
+
if (measuring && unreadable.length) {
|
|
1151
|
+
throw new Error(
|
|
1152
|
+
"Cannot measure new work: unreadable reservation " + unreadable[0] + ". Check Activity. Restore the original file and run flowmind agent retry, or discard the reservation with flowmind agent discard " + basename(unreadable[0], ".json") + ". An unowned original needs flowmind agent adopt <id>."
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
for (const stage of stages) {
|
|
1156
|
+
if (stage.sessionId === null || stage.cut === null) continue;
|
|
1157
|
+
const mark = { at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id };
|
|
1158
|
+
if (await restoreStageMark(stage.sessionId, mark) === "accepted") {
|
|
1159
|
+
await forgetStage(stage.id);
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
if ((stage.state === "rejected" || stage.state === "conflict") && stage.lastError !== null) {
|
|
1163
|
+
await settleStageMark(stage.sessionId, mark, { state: stage.state, reason: stage.lastError });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
var isDue = (stage, now) => stage.state === "waiting" && stage.apiUrl === API_URL && Date.parse(stage.nextAttemptAt) <= now.getTime();
|
|
1168
|
+
async function sendPendingStages(options) {
|
|
1169
|
+
const clock = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1170
|
+
const count = async () => {
|
|
1171
|
+
const { stages, unreadable } = await listPendingStages();
|
|
1172
|
+
return { waiting: stages.filter((stage) => stage.state === "waiting").length, unreadable };
|
|
1173
|
+
};
|
|
1174
|
+
const result = {
|
|
1175
|
+
busy: false,
|
|
1176
|
+
sent: [],
|
|
1177
|
+
rejected: [],
|
|
1178
|
+
conflicts: [],
|
|
1179
|
+
stoppedBy: null,
|
|
1180
|
+
waiting: 0,
|
|
1181
|
+
unreadable: []
|
|
1182
|
+
};
|
|
1183
|
+
const lock = await takeStageLock();
|
|
1184
|
+
if (!lock) return { ...result, busy: true, ...await count() };
|
|
1185
|
+
try {
|
|
1186
|
+
await restoreSavedStageMarks();
|
|
1187
|
+
const candidates = (await listPendingStages()).stages.filter((stage) => {
|
|
1188
|
+
const now = clock();
|
|
1189
|
+
if (stage.state === "conflict") {
|
|
1190
|
+
if (options.manual && stage.credentialFingerprint === accountBinding().credential) return true;
|
|
1191
|
+
if (options.manual) result.conflicts.push({ id: stage.id, reason: conflictAdvice(stage.id) });
|
|
1192
|
+
return false;
|
|
1202
1193
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
if (
|
|
1214
|
-
|
|
1215
|
-
|
|
1194
|
+
return options.due ? isDue(stage, now) : stage.state === "waiting" && stage.apiUrl === API_URL;
|
|
1195
|
+
});
|
|
1196
|
+
for (const stage of candidates) {
|
|
1197
|
+
let proposal;
|
|
1198
|
+
try {
|
|
1199
|
+
proposal = await createAgentLogProposal(stage.input, stage.idempotencyKey);
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
const failure = classifyStageFailure(error);
|
|
1202
|
+
const saved = await recordStageFailure(stage, failure, clock());
|
|
1203
|
+
if (saved.state === "conflict") {
|
|
1204
|
+
if (stage.sessionId && stage.cut)
|
|
1205
|
+
await settleStageMark(
|
|
1206
|
+
stage.sessionId,
|
|
1207
|
+
{ at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
|
|
1208
|
+
{ state: "conflict", reason: conflictAdvice(stage.id) }
|
|
1209
|
+
);
|
|
1210
|
+
result.conflicts.push({ id: stage.id, reason: conflictAdvice(stage.id) });
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (failure.kind === "reject") {
|
|
1214
|
+
if (stage.sessionId && stage.cut)
|
|
1215
|
+
await settleStageMark(
|
|
1216
|
+
stage.sessionId,
|
|
1217
|
+
{ at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
|
|
1218
|
+
{ state: "rejected", reason: failure.reason }
|
|
1219
|
+
);
|
|
1220
|
+
result.rejected.push({ id: stage.id, reason: failure.reason });
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
result.stoppedBy = failure.reason;
|
|
1224
|
+
break;
|
|
1216
1225
|
}
|
|
1226
|
+
if (stage.sessionId && stage.cut)
|
|
1227
|
+
await settleStageMark(
|
|
1228
|
+
stage.sessionId,
|
|
1229
|
+
{ at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
|
|
1230
|
+
{ state: "accepted", proposalId: proposal.id }
|
|
1231
|
+
);
|
|
1232
|
+
await forgetStage(stage.id);
|
|
1233
|
+
result.sent.push({ id: stage.id, proposal });
|
|
1217
1234
|
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1235
|
+
} finally {
|
|
1236
|
+
await lock.release();
|
|
1237
|
+
}
|
|
1238
|
+
return { ...result, ...await count() };
|
|
1239
|
+
}
|
|
1240
|
+
var conflictAdvice = (id) => "Stage key conflict (" + id + "). Delivery is unknown. Check Activity, then retry with the original credential or run flowmind agent discard " + id + ".";
|
|
1241
|
+
async function discardReservation(id) {
|
|
1242
|
+
if (!id || basename(id) !== id || id === "." || id === "..") return false;
|
|
1243
|
+
const { stages, unreadable } = await listPendingStages();
|
|
1244
|
+
const stage = stages.find((item) => item.id === id);
|
|
1245
|
+
if (!stage && !unreadable.includes(fileOf(id))) return false;
|
|
1246
|
+
if (stage && stage.state !== "conflict")
|
|
1247
|
+
throw new Error("This account already has a saved proof " + id + ". See: flowmind agent pending");
|
|
1248
|
+
if (stage?.sessionId && stage.cut)
|
|
1249
|
+
await settleStageMark(
|
|
1250
|
+
stage.sessionId,
|
|
1251
|
+
{ at: stage.savedAt, cut: stage.cut, pendingStageId: id },
|
|
1252
|
+
{ state: "discarded", reason: "Discarded after checking Activity." }
|
|
1253
|
+
);
|
|
1254
|
+
await forgetStage(id);
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
async function startDueRetry(now) {
|
|
1258
|
+
const { stages } = await listPendingStages();
|
|
1259
|
+
if (!stages.some((stage) => isDue(stage, now))) return false;
|
|
1260
|
+
const script = process.argv[1];
|
|
1261
|
+
if (!script) throw new Error("Cannot find the Flowmind CLI script to send saved stages");
|
|
1262
|
+
const child = spawn(process.execPath, [script, "agent", "retry", "--due"], {
|
|
1263
|
+
detached: true,
|
|
1264
|
+
stdio: "ignore",
|
|
1265
|
+
windowsHide: true
|
|
1266
|
+
});
|
|
1267
|
+
child.on("error", (error) => {
|
|
1268
|
+
console.error("Flowmind could not start sending saved stages: " + error.message);
|
|
1269
|
+
process.exitCode = 1;
|
|
1270
|
+
});
|
|
1271
|
+
child.unref();
|
|
1272
|
+
return true;
|
|
1220
1273
|
}
|
|
1221
1274
|
|
|
1222
|
-
//
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1275
|
+
// src/legacy-proofs.ts
|
|
1276
|
+
async function legacyStages() {
|
|
1277
|
+
let names;
|
|
1278
|
+
try {
|
|
1279
|
+
names = await readdir4(legacyOutboxDir());
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
if (error.code === "ENOENT") return { stages: [], unreadable: [] };
|
|
1282
|
+
throw error;
|
|
1283
|
+
}
|
|
1284
|
+
const stages = [];
|
|
1285
|
+
const unreadable = [];
|
|
1286
|
+
for (const name of names.filter((entry) => entry.endsWith(".json"))) {
|
|
1287
|
+
const path = join6(legacyOutboxDir(), name);
|
|
1288
|
+
let value = null;
|
|
1289
|
+
try {
|
|
1290
|
+
value = JSON.parse(await readFile4(path, "utf8"));
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
if (error.code === "ENOENT") continue;
|
|
1293
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
1294
|
+
}
|
|
1295
|
+
const stage = readStage(value, name, true);
|
|
1296
|
+
if (stage) stages.push(stage);
|
|
1297
|
+
else unreadable.push(path);
|
|
1298
|
+
}
|
|
1299
|
+
return { stages, unreadable };
|
|
1300
|
+
}
|
|
1301
|
+
async function moveIntoAccount(stage) {
|
|
1302
|
+
await mkdir3(outboxDir(), { recursive: true, mode: 448 });
|
|
1303
|
+
const target = join6(outboxDir(), stage.id + ".json");
|
|
1304
|
+
const present = await stat2(target).then(
|
|
1305
|
+
() => true,
|
|
1306
|
+
(error) => {
|
|
1307
|
+
if (error.code === "ENOENT") return false;
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
);
|
|
1311
|
+
if (present) return false;
|
|
1312
|
+
await rename2(join6(legacyOutboxDir(), stage.id + ".json"), target);
|
|
1313
|
+
await writePendingStage({ ...stage, credentialFingerprint: accountBinding().credential });
|
|
1314
|
+
return true;
|
|
1315
|
+
}
|
|
1316
|
+
async function adoptLegacyProofs() {
|
|
1317
|
+
for (const stage of (await legacyStages()).stages) if (stage.apiUrl === API_URL) await moveIntoAccount(stage);
|
|
1318
|
+
}
|
|
1319
|
+
async function listUnownedProofs() {
|
|
1320
|
+
const { stages, unreadable } = await legacyStages();
|
|
1321
|
+
const unowned = stages.map((stage) => ({
|
|
1322
|
+
id: stage.id,
|
|
1323
|
+
projectId: stage.input.projectId,
|
|
1324
|
+
durationMinutes: stage.input.durationMinutes,
|
|
1325
|
+
savedAt: stage.savedAt,
|
|
1326
|
+
apiUrl: stage.apiUrl
|
|
1327
|
+
})).sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt));
|
|
1328
|
+
return { unowned, unreadable };
|
|
1329
|
+
}
|
|
1330
|
+
async function findUnowned(id) {
|
|
1331
|
+
if (!/^[a-f0-9]{16}$/.test(id)) throw new Error("No unowned saved proof " + id + ". See: flowmind agent pending");
|
|
1332
|
+
const stage = (await legacyStages()).stages.find((candidate) => candidate.id === id);
|
|
1333
|
+
if (!stage) throw new Error("No unowned saved proof " + id + ". See: flowmind agent pending");
|
|
1334
|
+
return stage;
|
|
1335
|
+
}
|
|
1336
|
+
async function adoptUnownedProof(id) {
|
|
1337
|
+
const stage = await findUnowned(id);
|
|
1338
|
+
if (stage.apiUrl !== API_URL)
|
|
1339
|
+
throw new Error("Saved proof " + id + " belongs to " + stage.apiUrl + ", not " + API_URL + ".");
|
|
1340
|
+
if (!await moveIntoAccount(stage))
|
|
1341
|
+
throw new Error("This account already has a reservation for " + id + ". Check Activity before adopting it.");
|
|
1342
|
+
}
|
|
1343
|
+
async function discardUnownedProof(id) {
|
|
1344
|
+
await findUnowned(id);
|
|
1345
|
+
await rm2(join6(legacyOutboxDir(), id + ".json"));
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/account.ts
|
|
1349
|
+
var bindingStartNow = () => ({ at: (/* @__PURE__ */ new Date()).toISOString(), up: awakeMs() });
|
|
1350
|
+
async function withConfigWriters(change) {
|
|
1351
|
+
let lock = await configWriters();
|
|
1352
|
+
for (let waited = 0; !lock && waited < 15e3; waited += 250) {
|
|
1353
|
+
await new Promise((resolve3) => setTimeout(resolve3, 250));
|
|
1354
|
+
lock = await configWriters();
|
|
1355
|
+
}
|
|
1356
|
+
if (!lock) throw new Error("Another Flowmind process is changing the sign-in. Try again when it finishes.");
|
|
1357
|
+
try {
|
|
1358
|
+
return await change();
|
|
1359
|
+
} finally {
|
|
1360
|
+
await lock.release();
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async function ensureAccount() {
|
|
1364
|
+
if (!accountBinding()) await learnAccount();
|
|
1365
|
+
if (!await legacyStateWaits()) return;
|
|
1366
|
+
const lock = await takeStageLock();
|
|
1367
|
+
if (!lock) return;
|
|
1368
|
+
try {
|
|
1369
|
+
await adoptLegacyStateHeld();
|
|
1370
|
+
} finally {
|
|
1371
|
+
await lock.release();
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
async function learnAccount() {
|
|
1375
|
+
const key = requireApiKey();
|
|
1376
|
+
await withConfigWriters(async () => {
|
|
1377
|
+
const before = config.store;
|
|
1378
|
+
if (before.binding !== void 0 || before.apiKey !== key || (before.apiUrl ?? "https://api.flowmind.pro") !== API_URL) {
|
|
1379
|
+
throw new Error("The sign-in changed while Flowmind checked it. Run the command again.");
|
|
1380
|
+
}
|
|
1381
|
+
let account;
|
|
1382
|
+
try {
|
|
1383
|
+
account = (await apiRequest(getV1Me, {}, { signal: AbortSignal.timeout(1e4) })).userId;
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
if (error instanceof BridgeError && error.context.kind === "api" && error.context.status === 401) {
|
|
1386
|
+
throw new Error("Flowmind refused the saved sign-in. Run: flowmind auth login");
|
|
1387
|
+
}
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
"Could not verify the signed-in account: " + (error instanceof Error ? error.message : String(error))
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
const current = config.store;
|
|
1393
|
+
if (current.apiKey !== key || (current.apiUrl ?? "https://api.flowmind.pro") !== API_URL) {
|
|
1394
|
+
throw new Error("The sign-in changed while Flowmind checked it. Run the command again.");
|
|
1395
|
+
}
|
|
1396
|
+
bindAccount(key, account, bindingStartNow());
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
async function holdsFiles(directory, suffix) {
|
|
1400
|
+
try {
|
|
1401
|
+
return (await readdir5(directory)).some((name) => name.endsWith(suffix));
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
if (error.code === "ENOENT") return false;
|
|
1404
|
+
throw error;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
async function legacyStateWaits() {
|
|
1408
|
+
return soleAccountProfile() && (await legacyJournalsWaiting(legacySessionsDir()) || await holdsFiles(legacyOutboxDir(), ".json"));
|
|
1409
|
+
}
|
|
1410
|
+
async function adoptLegacyStateHeld() {
|
|
1411
|
+
if (await legacyStateWaits()) {
|
|
1412
|
+
await adoptLegacyJournals(
|
|
1413
|
+
legacySessionsDir(),
|
|
1414
|
+
(earliest) => withConfigWriters(() => extendSignInStart({ at: new Date(earliest).toISOString(), up: null }))
|
|
1415
|
+
);
|
|
1416
|
+
await adoptLegacyProofs();
|
|
1417
|
+
}
|
|
1418
|
+
refreshSignInStart();
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/manual-save.ts
|
|
1422
|
+
var RECEIPT_MS = 24 * 60 * 6e4;
|
|
1423
|
+
var savesDir = () => join7(accountDirectory(), "manual-saves");
|
|
1424
|
+
var fileOf2 = (id) => join7(savesDir(), id + ".json");
|
|
1425
|
+
async function withWriters(work) {
|
|
1426
|
+
await mkdir4(savesDir(), { recursive: true, mode: 448 });
|
|
1427
|
+
const lock = await tryLock(join7(savesDir(), "writers"));
|
|
1428
|
+
if (!lock) throw new Error("Another Flowmind process is saving a log. Try again when it finishes.");
|
|
1429
|
+
try {
|
|
1430
|
+
return await work();
|
|
1431
|
+
} finally {
|
|
1432
|
+
await lock.release();
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function readSaved(text2, id) {
|
|
1436
|
+
const saved = JSON.parse(text2);
|
|
1437
|
+
if (!saved || saved.id !== id || !Number.isFinite(saved.savedAt) || typeof saved.key !== "string" || typeof saved.body !== "object" || saved.body === null) {
|
|
1438
|
+
throw new Error("Unreadable saved log " + id + " in " + savesDir());
|
|
1439
|
+
}
|
|
1440
|
+
return saved;
|
|
1441
|
+
}
|
|
1442
|
+
async function manualSave(identity, prepare, send) {
|
|
1443
|
+
await ensureAccount();
|
|
1444
|
+
const id = createHash3("sha256").update(JSON.stringify(identity)).digest("hex").slice(0, 16);
|
|
1445
|
+
return withWriters(async () => {
|
|
1446
|
+
let saved;
|
|
1447
|
+
try {
|
|
1448
|
+
saved = readSaved(await readFile5(fileOf2(id), "utf8"), id);
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
if (error.code !== "ENOENT") throw error;
|
|
1451
|
+
const body = await prepare();
|
|
1452
|
+
if (body === null) return null;
|
|
1453
|
+
saved = { id, body, key: randomUUID3(), savedAt: Date.now() };
|
|
1454
|
+
const temp = fileOf2(id) + "." + randomUUID3() + ".tmp";
|
|
1455
|
+
await writeFile4(temp, JSON.stringify(saved), { mode: 384, flag: "wx" });
|
|
1456
|
+
await rename3(temp, fileOf2(id));
|
|
1457
|
+
}
|
|
1458
|
+
if (Date.now() - saved.savedAt >= RECEIPT_MS) {
|
|
1459
|
+
throw new Error(
|
|
1460
|
+
`This log (${id}) was not confirmed within 24 hours. Check Activity, then run flowmind pending-logs done ${id} if it is there, or flowmind pending-logs discard ${id} to log it again.`
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
let result;
|
|
1464
|
+
try {
|
|
1465
|
+
result = await send(saved.body, saved.key);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
const status = error instanceof BridgeError && error.context.kind === "api" ? error.context.status : null;
|
|
1468
|
+
if (status !== null && status >= 400 && status < 500 && ![401, 403, 408, 409, 429].includes(status))
|
|
1469
|
+
await unlink2(fileOf2(id));
|
|
1470
|
+
else console.error(`This log (${id}) was not confirmed. Run the same command to retry the saved request.`);
|
|
1471
|
+
throw error;
|
|
1472
|
+
}
|
|
1473
|
+
await unlink2(fileOf2(id));
|
|
1474
|
+
return result;
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
async function listSavedLogs() {
|
|
1478
|
+
await ensureAccount();
|
|
1479
|
+
let names;
|
|
1480
|
+
try {
|
|
1481
|
+
names = await readdir6(savesDir());
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
if (error.code === "ENOENT") return [];
|
|
1484
|
+
throw error;
|
|
1485
|
+
}
|
|
1486
|
+
const logs = [];
|
|
1487
|
+
for (const name of names.filter((entry) => /^[a-f0-9]{16}\.json$/.test(entry))) {
|
|
1488
|
+
const id = name.slice(0, 16);
|
|
1489
|
+
const saved = readSaved(await readFile5(fileOf2(id), "utf8"), id);
|
|
1490
|
+
const field = (key, test) => test(saved.body[key]) ? saved.body[key] : null;
|
|
1491
|
+
const text2 = (value) => typeof value === "string";
|
|
1492
|
+
logs.push({
|
|
1493
|
+
id,
|
|
1494
|
+
savedAt: new Date(saved.savedAt).toISOString(),
|
|
1495
|
+
expired: Date.now() - saved.savedAt >= RECEIPT_MS,
|
|
1496
|
+
projectId: field("projectId", text2),
|
|
1497
|
+
durationMinutes: field("durationMinutes", Number.isSafeInteger),
|
|
1498
|
+
startedAt: field("startedAt", text2),
|
|
1499
|
+
date: field("date", text2)
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
return logs.sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt));
|
|
1503
|
+
}
|
|
1504
|
+
async function resolveSavedLog(id, resolution) {
|
|
1505
|
+
await ensureAccount();
|
|
1506
|
+
if (!/^[a-f0-9]{16}$/.test(id)) throw new Error("No saved log " + id + ". See: flowmind pending-logs");
|
|
1507
|
+
await withWriters(async () => {
|
|
1508
|
+
let saved;
|
|
1509
|
+
try {
|
|
1510
|
+
saved = readSaved(await readFile5(fileOf2(id), "utf8"), id);
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
if (error.code === "ENOENT")
|
|
1513
|
+
throw new Error("No saved log " + id + ". See: flowmind pending-logs");
|
|
1514
|
+
throw error;
|
|
1515
|
+
}
|
|
1516
|
+
if (resolution === "discard") return unlink2(fileOf2(id));
|
|
1517
|
+
await mkdir4(join7(savesDir(), "resolved"), { recursive: true, mode: 448 });
|
|
1518
|
+
await writeFile4(
|
|
1519
|
+
join7(savesDir(), "resolved", id + "-" + saved.savedAt + ".json"),
|
|
1520
|
+
JSON.stringify({ ...saved, resolution: "checked-in-activity", resolvedAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
1521
|
+
{ mode: 384, flag: "wx" }
|
|
1522
|
+
);
|
|
1523
|
+
await unlink2(fileOf2(id));
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// ../../packages/contracts/src/generated/validators/shared/OUWE7ZSG.js
|
|
1528
|
+
var U = c((r2) => {
|
|
1529
|
+
"use strict";
|
|
1530
|
+
Object.defineProperty(r2, "__esModule", { value: true });
|
|
1531
|
+
r2.formatNames = r2.fastFormats = r2.fullFormats = void 0;
|
|
1532
|
+
function n7(a5, d5) {
|
|
1533
|
+
return { validate: a5, compare: d5 };
|
|
1534
|
+
}
|
|
1535
|
+
r2.fullFormats = { date: n7(b4, m6), time: n7(c5(true), $2), "date-time": n7(v6(true), N2), "iso-time": n7(c5(), A2), "iso-date-time": n7(v6(), g), duration: /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/, uri: R2, "uri-reference": /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i, "uri-template": /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i, url: /^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu, email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, hostname: /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i, ipv4: /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/, ipv6: /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i, regex: P4, uuid: /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i, "json-pointer": /^(?:\/(?:[^~/]|~0|~1)*)*$/, "json-pointer-uri-fragment": /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i, "relative-json-pointer": /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/, byte: w3, int32: { type: "number", validate: Z }, int64: { type: "number", validate: H2 }, float: { type: "number", validate: D2 }, double: { type: "number", validate: D2 }, password: true, binary: true };
|
|
1536
|
+
r2.fastFormats = { ...r2.fullFormats, date: n7(/^\d\d\d\d-[0-1]\d-[0-3]\d$/, m6), time: n7(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, $2), "date-time": n7(/^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, N2), "iso-time": n7(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, A2), "iso-date-time": n7(/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, g), uri: /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i, "uri-reference": /^(?:(?:[a-z][a-z0-9+\-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i };
|
|
1537
|
+
r2.formatNames = Object.keys(r2.fullFormats);
|
|
1538
|
+
function y3(a5) {
|
|
1539
|
+
return a5 % 4 === 0 && (a5 % 100 !== 0 || a5 % 400 === 0);
|
|
1540
|
+
}
|
|
1541
|
+
var O2 = /^(\d\d\d\d)-(\d\d)-(\d\d)$/, h10 = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
1542
|
+
function b4(a5) {
|
|
1543
|
+
let d5 = O2.exec(a5);
|
|
1544
|
+
if (!d5) return false;
|
|
1545
|
+
let t8 = +d5[1], e4 = +d5[2], f3 = +d5[3];
|
|
1546
|
+
return e4 >= 1 && e4 <= 12 && f3 >= 1 && f3 <= (e4 === 2 && y3(t8) ? 29 : h10[e4]);
|
|
1547
|
+
}
|
|
1548
|
+
function m6(a5, d5) {
|
|
1549
|
+
if (a5 && d5) return a5 > d5 ? 1 : a5 < d5 ? -1 : 0;
|
|
1550
|
+
}
|
|
1551
|
+
var s7 = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
1552
|
+
function c5(a5) {
|
|
1553
|
+
return function(t8) {
|
|
1554
|
+
let e4 = s7.exec(t8);
|
|
1555
|
+
if (!e4) return false;
|
|
1556
|
+
let f3 = +e4[1], u4 = +e4[2], i5 = +e4[3], M = e4[4], l4 = e4[5] === "-" ? -1 : 1, _ = +(e4[6] || 0), p6 = +(e4[7] || 0);
|
|
1557
|
+
if (_ > 23 || p6 > 59 || a5 && !M) return false;
|
|
1558
|
+
if (f3 <= 23 && u4 <= 59 && i5 < 60) return true;
|
|
1559
|
+
let o11 = u4 - p6 * l4, T2 = f3 - _ * l4 - (o11 < 0 ? 1 : 0);
|
|
1560
|
+
return (T2 === 23 || T2 === -1) && (o11 === 59 || o11 === -1) && i5 < 61;
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
function $2(a5, d5) {
|
|
1564
|
+
if (!(a5 && d5)) return;
|
|
1565
|
+
let t8 = (/* @__PURE__ */ new Date("2020-01-01T" + a5)).valueOf(), e4 = (/* @__PURE__ */ new Date("2020-01-01T" + d5)).valueOf();
|
|
1566
|
+
if (t8 && e4) return t8 - e4;
|
|
1567
|
+
}
|
|
1568
|
+
function A2(a5, d5) {
|
|
1569
|
+
if (!(a5 && d5)) return;
|
|
1570
|
+
let t8 = s7.exec(a5), e4 = s7.exec(d5);
|
|
1571
|
+
if (t8 && e4) return a5 = t8[1] + t8[2] + t8[3], d5 = e4[1] + e4[2] + e4[3], a5 > d5 ? 1 : a5 < d5 ? -1 : 0;
|
|
1572
|
+
}
|
|
1573
|
+
var z = /t|\s/i;
|
|
1574
|
+
function v6(a5) {
|
|
1575
|
+
let d5 = c5(a5);
|
|
1576
|
+
return function(e4) {
|
|
1577
|
+
let f3 = e4.split(z);
|
|
1578
|
+
return f3.length === 2 && b4(f3[0]) && d5(f3[1]);
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
function N2(a5, d5) {
|
|
1582
|
+
if (!(a5 && d5)) return;
|
|
1583
|
+
let t8 = new Date(a5).valueOf(), e4 = new Date(d5).valueOf();
|
|
1584
|
+
if (t8 && e4) return t8 - e4;
|
|
1585
|
+
}
|
|
1586
|
+
function g(a5, d5) {
|
|
1587
|
+
if (!(a5 && d5)) return;
|
|
1588
|
+
let [t8, e4] = a5.split(z), [f3, u4] = d5.split(z), i5 = m6(t8, f3);
|
|
1589
|
+
if (i5 !== void 0) return i5 || $2(e4, u4);
|
|
1590
|
+
}
|
|
1591
|
+
var E = /\/|:/, F2 = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
|
|
1592
|
+
function R2(a5) {
|
|
1593
|
+
return E.test(a5) && F2.test(a5);
|
|
1594
|
+
}
|
|
1595
|
+
var I2 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
|
|
1596
|
+
function w3(a5) {
|
|
1597
|
+
return I2.lastIndex = 0, I2.test(a5);
|
|
1598
|
+
}
|
|
1599
|
+
var S3 = -(2 ** 31), j2 = 2 ** 31 - 1;
|
|
1600
|
+
function Z(a5) {
|
|
1601
|
+
return Number.isInteger(a5) && a5 <= j2 && a5 >= S3;
|
|
1602
|
+
}
|
|
1603
|
+
function H2(a5) {
|
|
1604
|
+
return Number.isInteger(a5);
|
|
1605
|
+
}
|
|
1606
|
+
function D2() {
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1609
|
+
var Y = /[^\\]\\Z/;
|
|
1610
|
+
function P4(a5) {
|
|
1611
|
+
if (Y.test(a5)) return false;
|
|
1612
|
+
try {
|
|
1613
|
+
return new RegExp(a5), true;
|
|
1614
|
+
} catch {
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
// ../../packages/contracts/src/generated/validators/shared/J43X2ZUC.js
|
|
1621
|
+
var t3 = U().fullFormats.date;
|
|
1622
|
+
|
|
1623
|
+
// ../../packages/contracts/src/generated/validators/shared/HNC2DQA2.js
|
|
1624
|
+
function s(e4, { instancePath: a5 = "", parentData: n7, parentDataProperty: u4, rootData: l4 = e4 } = {}) {
|
|
1625
|
+
let m6 = null, r2 = 0;
|
|
1626
|
+
if (r2 === 0) if (e4 && typeof e4 == "object" && !Array.isArray(e4)) {
|
|
1627
|
+
let i5;
|
|
1628
|
+
if (e4.text === void 0 && (i5 = "text") || e4.sourceDay === void 0 && (i5 = "sourceDay")) return s.errors = [{ instancePath: a5, schemaPath: "#/required", keyword: "required", params: { missingProperty: i5 }, message: "must have required property '" + i5 + "'" }], false;
|
|
1629
|
+
{
|
|
1630
|
+
let y3 = r2;
|
|
1631
|
+
for (let t8 in e4) if (!(t8 === "sourceDay" || t8 === "text")) {
|
|
1632
|
+
return s.errors = [{ instancePath: a5, schemaPath: "#/additionalProperties", keyword: "additionalProperties", params: { additionalProperty: t8 }, message: "must NOT have additional properties" }], false;
|
|
1633
|
+
break;
|
|
1634
|
+
}
|
|
1635
|
+
if (y3 === r2) {
|
|
1636
|
+
if (e4.sourceDay !== void 0) {
|
|
1637
|
+
let t8 = e4.sourceDay, p6 = r2;
|
|
1638
|
+
if (r2 === p6 && r2 === p6) if (typeof t8 == "string") {
|
|
1639
|
+
if (!t3.validate(t8)) return s.errors = [{ instancePath: a5 + "/sourceDay", schemaPath: "#/properties/sourceDay/format", keyword: "format", params: { format: "date" }, message: 'must match format "date"' }], false;
|
|
1640
|
+
} else return s.errors = [{ instancePath: a5 + "/sourceDay", schemaPath: "#/properties/sourceDay/type", keyword: "type", params: { type: "string" }, message: "must be string" }], false;
|
|
1641
|
+
var o11 = p6 === r2;
|
|
1642
|
+
} else var o11 = true;
|
|
1643
|
+
if (o11) if (e4.text !== void 0) {
|
|
1644
|
+
let t8 = r2;
|
|
1645
|
+
if (typeof e4.text != "string") return s.errors = [{ instancePath: a5 + "/text", schemaPath: "#/properties/text/type", keyword: "type", params: { type: "string" }, message: "must be string" }], false;
|
|
1646
|
+
var o11 = t8 === r2;
|
|
1647
|
+
} else var o11 = true;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
} else return s.errors = [{ instancePath: a5, schemaPath: "#/type", keyword: "type", params: { type: "object" }, message: "must be object" }], false;
|
|
1651
|
+
return s.errors = m6, r2 === 0;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// ../../packages/contracts/src/generated/validators/shared/VSRNQHIL.js
|
|
1655
|
+
var e = U().fullFormats["date-time"];
|
|
1656
|
+
|
|
1657
|
+
// ../../packages/contracts/src/generated/validators/shared/CA7CC2WJ.js
|
|
1658
|
+
var a2 = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
|
|
1659
|
+
|
|
1660
|
+
// ../../packages/contracts/src/generated/validators/getV1Projects200.js
|
|
1661
|
+
var D = { description: "A listed project with its time totals. totalSeconds counts every non-voided entry, including approved agent work; agentSeconds is the agent share of that total.", properties: { agentSeconds: { minimum: 0, type: "integer" }, archivedAt: { format: "date-time", nullable: true, type: "string" }, category: { type: "string" }, color: { type: "string" }, completedAt: { format: "date-time", nullable: true, type: "string" }, createdAt: { format: "date-time", type: "string" }, description: { nullable: true, type: "string" }, details: { description: "Private details for lastOutcome, or an empty array when no outcome exists.", items: { type: "string" }, type: "array" }, estimatedHours: { nullable: true, type: "integer" }, icon: { type: "string" }, id: { format: "uuid", type: "string" }, lastActiveDay: { format: "date", nullable: true, type: "string" }, lastOutcome: { nullable: true, type: "string" }, launchDescription: { nullable: true, type: "string" }, launchUrl: { nullable: true, type: "string" }, launchedAt: { format: "date-time", nullable: true, type: "string" }, name: { type: "string" }, nextStep: { anyOf: [{ $ref: "urn:flowmind:schema:ProjectNextStep" }, { enum: [null], nullable: true, type: "object" }] }, totalSeconds: { minimum: 0, type: "integer" }, updatedAt: { format: "date-time", type: "string" } }, required: ["id", "name", "description", "color", "icon", "category", "estimatedHours", "launchedAt", "completedAt", "launchDescription", "launchUrl", "createdAt", "updatedAt", "archivedAt", "nextStep", "lastOutcome", "details", "lastActiveDay", "totalSeconds", "agentSeconds"], type: "object" };
|
|
1662
|
+
function n(t8, { instancePath: i5 = "", parentData: w3, parentDataProperty: k3, rootData: d5 = t8 } = {}) {
|
|
1663
|
+
let o11 = null, r2 = 0;
|
|
1664
|
+
if (r2 === 0) if (t8 && typeof t8 == "object" && !Array.isArray(t8)) {
|
|
1233
1665
|
let l4;
|
|
1234
1666
|
if (t8.id === void 0 && (l4 = "id") || t8.name === void 0 && (l4 = "name") || t8.description === void 0 && (l4 = "description") || t8.color === void 0 && (l4 = "color") || t8.icon === void 0 && (l4 = "icon") || t8.category === void 0 && (l4 = "category") || t8.estimatedHours === void 0 && (l4 = "estimatedHours") || t8.launchedAt === void 0 && (l4 = "launchedAt") || t8.completedAt === void 0 && (l4 = "completedAt") || t8.launchDescription === void 0 && (l4 = "launchDescription") || t8.launchUrl === void 0 && (l4 = "launchUrl") || t8.createdAt === void 0 && (l4 = "createdAt") || t8.updatedAt === void 0 && (l4 = "updatedAt") || t8.archivedAt === void 0 && (l4 = "archivedAt") || t8.nextStep === void 0 && (l4 = "nextStep") || t8.lastOutcome === void 0 && (l4 = "lastOutcome") || t8.details === void 0 && (l4 = "details") || t8.lastActiveDay === void 0 && (l4 = "lastActiveDay") || t8.totalSeconds === void 0 && (l4 = "totalSeconds") || t8.agentSeconds === void 0 && (l4 = "agentSeconds")) return n.errors = [{ instancePath: i5, schemaPath: "#/required", keyword: "required", params: { missingProperty: l4 }, message: "must have required property '" + l4 + "'" }], false;
|
|
1235
1667
|
if (t8.agentSeconds !== void 0) {
|
|
@@ -3268,1165 +3700,925 @@ function h7(r2, { instancePath: s7 = "", parentData: P4, parentDataProperty: w3,
|
|
|
3268
3700
|
let p6 = null, e4 = 0, a5 = e4;
|
|
3269
3701
|
i(r2, { instancePath: s7, parentData: P4, parentDataProperty: w3, rootData: g }) || (p6 = p6 === null ? i.errors : p6.concat(i.errors), e4 = p6.length);
|
|
3270
3702
|
var f3 = a5 === e4;
|
|
3271
|
-
if (f3) {
|
|
3272
|
-
let l4 = e4;
|
|
3273
|
-
if (e4 === l4) if (r2 && typeof r2 == "object" && !Array.isArray(r2)) {
|
|
3274
|
-
let m6;
|
|
3275
|
-
if (r2.data === void 0 && (m6 = "data")) return h7.errors = [{ instancePath: s7, schemaPath: "#/allOf/1/required", keyword: "required", params: { missingProperty: m6 }, message: "must have required property '" + m6 + "'" }], false;
|
|
3276
|
-
r2.data !== void 0 && (u3(r2.data, { instancePath: s7 + "/data", parentData: r2, parentDataProperty: "data", rootData: g }) || (p6 = p6 === null ? u3.errors : p6.concat(u3.errors), e4 = p6.length));
|
|
3277
|
-
} else return h7.errors = [{ instancePath: s7, schemaPath: "#/allOf/1/type", keyword: "type", params: { type: "object" }, message: "must be object" }], false;
|
|
3278
|
-
var f3 = l4 === e4;
|
|
3279
|
-
}
|
|
3280
|
-
return h7.errors = p6, e4 === 0;
|
|
3281
|
-
}
|
|
3282
|
-
var C = h7;
|
|
3283
|
-
|
|
3284
|
-
// ../../packages/contracts/src/generated/endpoints/getV1HourBank.ts
|
|
3285
|
-
var getV1HourBank = {
|
|
3286
|
-
method: "GET",
|
|
3287
|
-
path: "/v1/hour-bank/",
|
|
3288
|
-
operation: "GET /v1/hour-bank/",
|
|
3289
|
-
revision: contractRevision,
|
|
3290
|
-
responses: {
|
|
3291
|
-
200: { mediaType: "application/json", schemaId: "urn:flowmind:getV1HourBank:200", validate: C }
|
|
3292
|
-
},
|
|
3293
|
-
error: { mediaType: "application/json", schemaId: "ApiErrorEnvelope", validate: N }
|
|
3294
|
-
};
|
|
3295
|
-
|
|
3296
|
-
// src/contracts.ts
|
|
3297
|
-
var STAGE_TIMEOUT_MS = 3e4;
|
|
3298
|
-
var projectStatus = (project) => {
|
|
3299
|
-
if (project.archivedAt) return "archived";
|
|
3300
|
-
if (project.completedAt) return "completed";
|
|
3301
|
-
if (project.launchedAt) return "launched";
|
|
3302
|
-
return "active";
|
|
3303
|
-
};
|
|
3304
|
-
var listProjects = async (request = apiRequest) => {
|
|
3305
|
-
const data = await request(getV1Projects, {});
|
|
3306
|
-
return data.projects;
|
|
3307
|
-
};
|
|
3308
|
-
var getAgentCapabilities = (request = apiRequest) => request(getV1AgentCapabilities, {});
|
|
3309
|
-
var listAgentProjects = async (request = apiRequest) => {
|
|
3310
|
-
const data = await request(getV1AgentProjects, {});
|
|
3311
|
-
return data.projects;
|
|
3312
|
-
};
|
|
3313
|
-
var listAgentRoutines = async (request = apiRequest) => {
|
|
3314
|
-
const data = await request(getV1AgentRoutines, {});
|
|
3315
|
-
return data.routines;
|
|
3316
|
-
};
|
|
3317
|
-
var withIdempotencyKey = (idempotencyKey) => ({
|
|
3318
|
-
headers: { "Idempotency-Key": idempotencyKey }
|
|
3319
|
-
});
|
|
3320
|
-
var createAgentLogProposal = async (input2, idempotencyKey, request = apiRequest) => {
|
|
3321
|
-
const data = await request(
|
|
3322
|
-
postV1AgentLogProposals,
|
|
3323
|
-
{ body: input2 },
|
|
3324
|
-
{ ...withIdempotencyKey(idempotencyKey), preserveRejectionMessage: true, signal: AbortSignal.timeout(STAGE_TIMEOUT_MS) }
|
|
3325
|
-
);
|
|
3326
|
-
return data.proposal;
|
|
3327
|
-
};
|
|
3328
|
-
var listAgentLogProposals = async (status, request = apiRequest) => {
|
|
3329
|
-
const data = await request(getV1AgentLogProposals, { query: { status } });
|
|
3330
|
-
return data.proposals;
|
|
3331
|
-
};
|
|
3332
|
-
var cancelAgentLogProposal = async (proposalId, idempotencyKey, request = apiRequest) => {
|
|
3333
|
-
const data = await request(
|
|
3334
|
-
postV1AgentLogProposalsByProposalIdCancel,
|
|
3335
|
-
{ params: { proposalId } },
|
|
3336
|
-
withIdempotencyKey(idempotencyKey)
|
|
3337
|
-
);
|
|
3338
|
-
return data.proposal;
|
|
3339
|
-
};
|
|
3340
|
-
var parseNaturalLog = (message, request = apiRequest) => request(postV1AiLog, { body: { message } });
|
|
3341
|
-
var confirmNaturalLog = async (input2, idempotencyKey, request = apiRequest) => {
|
|
3342
|
-
return request(postV1AiLogConfirm, { body: input2 }, withIdempotencyKey(idempotencyKey));
|
|
3343
|
-
};
|
|
3344
|
-
var durationMinutesFromHours = (hours) => {
|
|
3345
|
-
const minutes = hours * 60;
|
|
3346
|
-
if (!Number.isSafeInteger(Math.round(minutes)) || Math.round(minutes) / 60 !== hours) {
|
|
3347
|
-
throw new Error("Hours must resolve to whole minutes.");
|
|
3348
|
-
}
|
|
3349
|
-
return Math.round(minutes);
|
|
3350
|
-
};
|
|
3351
|
-
var resolveActiveProjectByName = (projects, projectName) => {
|
|
3352
|
-
const matches = projects.filter((project) => project.name === projectName && project.archivedAt === null);
|
|
3353
|
-
if (matches.length === 0) {
|
|
3354
|
-
throw new Error(`No active project named "${projectName}"`);
|
|
3355
|
-
}
|
|
3356
|
-
if (matches.length > 1) {
|
|
3357
|
-
throw new Error(`Multiple active projects named "${projectName}". Use Flowmind web to disambiguate before logging.`);
|
|
3358
|
-
}
|
|
3359
|
-
return matches[0];
|
|
3360
|
-
};
|
|
3361
|
-
var createStructuredTimeEntry = async (input2, request = apiRequest) => {
|
|
3362
|
-
const durationMinutes = durationMinutesFromHours(input2.hours);
|
|
3363
|
-
const now = input2.now ?? /* @__PURE__ */ new Date();
|
|
3364
|
-
let start = new Date(now.getTime() - durationMinutes * 6e4);
|
|
3365
|
-
if (input2.startedAt !== void 0) {
|
|
3366
|
-
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(input2.startedAt)) {
|
|
3367
|
-
throw new Error("started-at must be a valid ISO date-time with a timezone.");
|
|
3368
|
-
}
|
|
3369
|
-
const calendarDay = /* @__PURE__ */ new Date(`${input2.startedAt.slice(0, 10)}T00:00:00.000Z`);
|
|
3370
|
-
if (Number.isNaN(calendarDay.getTime()) || calendarDay.toISOString().slice(0, 10) !== input2.startedAt.slice(0, 10)) {
|
|
3371
|
-
throw new Error("started-at must be a valid ISO date-time.");
|
|
3372
|
-
}
|
|
3373
|
-
start = new Date(input2.startedAt);
|
|
3374
|
-
if (Number.isNaN(start.getTime())) throw new Error("started-at must be a valid ISO date-time.");
|
|
3375
|
-
}
|
|
3376
|
-
if (start.getTime() + durationMinutes * 6e4 > now.getTime()) {
|
|
3377
|
-
throw new Error("Logged work cannot end in the future");
|
|
3378
|
-
}
|
|
3379
|
-
const prepare = async () => {
|
|
3380
|
-
const project = resolveActiveProjectByName(await listProjects(request), input2.projectName);
|
|
3381
|
-
return {
|
|
3382
|
-
projectId: project.id,
|
|
3383
|
-
startedAt: input2.startedAt === void 0 ? new Date((input2.now ?? /* @__PURE__ */ new Date()).getTime() - durationMinutes * 6e4).toISOString() : start.toISOString(),
|
|
3384
|
-
durationMinutes,
|
|
3385
|
-
...input2.nextStep !== void 0 ? { nextStep: input2.nextStep } : {},
|
|
3386
|
-
...input2.description !== void 0 ? { description: input2.description } : {},
|
|
3387
|
-
...input2.note !== void 0 ? { note: input2.note } : {},
|
|
3388
|
-
...input2.artifactUrl !== void 0 ? { artifactUrl: input2.artifactUrl } : {}
|
|
3389
|
-
};
|
|
3390
|
-
};
|
|
3391
|
-
return manualSave(["structured", input2], prepare, (body, key) => request(postV1TimeEntries, { body }, { headers: { [IDEMPOTENCY_KEY_HEADER]: key } }));
|
|
3392
|
-
};
|
|
3393
|
-
var getStatusSnapshot = async (request = apiRequest) => {
|
|
3394
|
-
const [publicStats, streak, xp] = await Promise.all([
|
|
3395
|
-
request(getV1MePublicStats, {}),
|
|
3396
|
-
request(getV1MeStreak, {}),
|
|
3397
|
-
request(getV1XpSummary, {})
|
|
3398
|
-
]);
|
|
3399
|
-
return { publicStats, streak, xp };
|
|
3400
|
-
};
|
|
3401
|
-
var getDetailedStats = async (request = apiRequest) => {
|
|
3402
|
-
const [status, badges, hourBank] = await Promise.all([
|
|
3403
|
-
getStatusSnapshot(request),
|
|
3404
|
-
request(getV1MeBadges, {}),
|
|
3405
|
-
request(getV1HourBank, {})
|
|
3406
|
-
]);
|
|
3407
|
-
return { ...status, badges, hourBank };
|
|
3408
|
-
};
|
|
3409
|
-
|
|
3410
|
-
// src/project-ref.ts
|
|
3411
|
-
async function resolveAgentProject(reference) {
|
|
3412
|
-
const wanted = reference.trim();
|
|
3413
|
-
const projects = await listAgentProjects();
|
|
3414
|
-
const matches = isProjectId(wanted) ? projects.filter((project) => project.id.toLowerCase() === wanted.toLowerCase()) : projects.filter((project) => project.name.toLowerCase() === wanted.toLowerCase());
|
|
3415
|
-
if (matches.length === 1) return matches[0];
|
|
3416
|
-
const list = (matches.length > 1 ? matches : projects).map((project) => ` ${terminalText(project.name)} (${project.id})`).join("\n");
|
|
3417
|
-
if (matches.length > 1) {
|
|
3418
|
-
throw new Error(`Several projects are named "${terminalText(wanted)}". Use a project id:
|
|
3419
|
-
${list}`);
|
|
3420
|
-
}
|
|
3421
|
-
throw new Error(
|
|
3422
|
-
`No active project matches "${terminalText(wanted)}".` + (list ? `
|
|
3423
|
-
Active projects:
|
|
3424
|
-
${list}` : " No active projects exist.")
|
|
3425
|
-
);
|
|
3426
|
-
}
|
|
3427
|
-
|
|
3428
|
-
// src/repository-status.ts
|
|
3429
|
-
import { execFile } from "node:child_process";
|
|
3430
|
-
var GIT_TIMEOUT_MS = 3e3;
|
|
3431
|
-
function git(root, args, absentCode) {
|
|
3432
|
-
return new Promise((resolve3, reject) => {
|
|
3433
|
-
execFile(
|
|
3434
|
-
"git",
|
|
3435
|
-
args,
|
|
3436
|
-
{
|
|
3437
|
-
cwd: root,
|
|
3438
|
-
encoding: "utf8",
|
|
3439
|
-
timeout: GIT_TIMEOUT_MS,
|
|
3440
|
-
windowsHide: true,
|
|
3441
|
-
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
|
|
3442
|
-
},
|
|
3443
|
-
(error, stdout) => {
|
|
3444
|
-
if (!error) resolve3(stdout);
|
|
3445
|
-
else if (!error.killed && absentCode !== void 0 && error.code === absentCode) resolve3(null);
|
|
3446
|
-
else reject(new Error("Git lookup failed (" + args[0] + "): " + (error.killed ? "timed out" : error.code)));
|
|
3447
|
-
}
|
|
3448
|
-
);
|
|
3449
|
-
});
|
|
3450
|
-
}
|
|
3451
|
-
async function readRemoteLink(root) {
|
|
3452
|
-
const branch = (await git(root, ["symbolic-ref", "--quiet", "HEAD"], 1))?.trim();
|
|
3453
|
-
const upstream = branch ? (await git(root, ["for-each-ref", "--format=%(upstream)", branch]))?.trim() : null;
|
|
3454
|
-
const refs = [
|
|
3455
|
-
...upstream?.startsWith("refs/remotes/") ? [upstream] : [],
|
|
3456
|
-
"refs/remotes/origin/HEAD",
|
|
3457
|
-
"refs/remotes/origin/main",
|
|
3458
|
-
"refs/remotes/origin/master"
|
|
3459
|
-
];
|
|
3460
|
-
for (const ref of new Set(refs)) {
|
|
3461
|
-
if (await git(root, ["rev-parse", "--verify", "--quiet", ref], 1) === null) continue;
|
|
3462
|
-
if (!(await git(root, ["ls-tree", "--name-only", ref, "--", LINK_FILE]))?.trim()) continue;
|
|
3463
|
-
const text2 = await git(root, ["show", `${ref}:${LINK_FILE}`]);
|
|
3464
|
-
return { ref, link: parseLink(text2, `${ref}:${LINK_FILE}`, root) };
|
|
3465
|
-
}
|
|
3466
|
-
return null;
|
|
3467
|
-
}
|
|
3468
|
-
async function remoteStatus(root) {
|
|
3469
|
-
const remote = await readRemoteLink(root);
|
|
3470
|
-
if (remote?.link.kind === "linked") return { status: "remote-linked", root, link: remote.link, ref: remote.ref };
|
|
3471
|
-
if (remote?.link.kind === "ignored") return { status: "ignored", root, link: remote.link };
|
|
3472
|
-
return { status: "unlinked", root };
|
|
3473
|
-
}
|
|
3474
|
-
async function repositoryStatus(cwd) {
|
|
3475
|
-
const local = await readLink(cwd);
|
|
3476
|
-
if (local?.kind === "linked") return { status: "linked", root: local.root, link: local };
|
|
3477
|
-
if (local?.kind === "ignored") return { status: "ignored", root: local.root, link: local };
|
|
3478
|
-
const root = await findGitRoot(cwd);
|
|
3479
|
-
return root === null ? { status: "outside-git" } : remoteStatus(root);
|
|
3480
|
-
}
|
|
3481
|
-
async function originUrl(root) {
|
|
3482
|
-
const url = (await git(root, ["config", "--get", "remote.origin.url"], 1))?.trim();
|
|
3483
|
-
if (!url) return null;
|
|
3484
|
-
let parsed;
|
|
3485
|
-
try {
|
|
3486
|
-
parsed = new URL(url);
|
|
3487
|
-
} catch {
|
|
3488
|
-
return url;
|
|
3489
|
-
}
|
|
3490
|
-
if (!parsed.username && !parsed.password) return url;
|
|
3491
|
-
parsed.username = "";
|
|
3492
|
-
parsed.password = "";
|
|
3493
|
-
return parsed.toString();
|
|
3494
|
-
}
|
|
3495
|
-
|
|
3496
|
-
// src/agent-session.ts
|
|
3497
|
-
var RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
3498
|
-
var ACTIVE_WINDOW_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
3499
|
-
var REMINDER_MINUTES = 20;
|
|
3500
|
-
var OPEN_TURN_LIMIT_MS = 12 * 60 * 60 * 1e3;
|
|
3501
|
-
var MISSED_START_MS = 15e3;
|
|
3502
|
-
var CLOCK_DRIFT_MS = 1e3;
|
|
3503
|
-
var EVENTS = /* @__PURE__ */ new Set(["session", "start", "stop", "subagent", "staged", "unlinked-reminder"]);
|
|
3504
|
-
var sessionsDir = () => join6(accountDirectory(), "agent-sessions");
|
|
3505
|
-
var periodPoint = (p6) => ({ wall: Date.parse(p6.at), up: p6.up });
|
|
3506
|
-
var signedInPeriods = () => ownershipPeriods().map(({ start, end }) => ({
|
|
3507
|
-
start: periodPoint(start),
|
|
3508
|
-
end: end && periodPoint(end)
|
|
3509
|
-
}));
|
|
3510
|
-
function sessionFile(sessionId) {
|
|
3511
|
-
const name = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$/.test(sessionId) ? sessionId : createHash3("sha256").update(sessionId).digest("hex");
|
|
3512
|
-
return join6(sessionsDir(), `${name}.jsonl`);
|
|
3513
|
-
}
|
|
3514
|
-
async function append(sessionId, event) {
|
|
3515
|
-
await mkdir3(sessionsDir(), { recursive: true });
|
|
3516
|
-
await appendFile(sessionFile(sessionId), `${JSON.stringify(event)}
|
|
3517
|
-
`, { encoding: "utf8", mode: 384 });
|
|
3518
|
-
}
|
|
3519
|
-
var byTime = (events) => events.sort((a5, b4) => Date.parse(a5.at) - Date.parse(b4.at));
|
|
3520
|
-
function parseEvent(line, path) {
|
|
3521
|
-
const value = JSON.parse(line);
|
|
3522
|
-
if (typeof value === "object" && value !== null) {
|
|
3523
|
-
const { event, at: at2, up, cutAt, cutUp } = value;
|
|
3524
|
-
if (typeof event === "string" && EVENTS.has(event) && typeof at2 === "string" && !Number.isNaN(Date.parse(at2)) && (up === void 0 || Number.isFinite(up)) && (cutAt === void 0 || typeof cutAt === "string" && !Number.isNaN(Date.parse(cutAt))) && (cutUp === void 0 || Number.isFinite(cutUp))) {
|
|
3525
|
-
if (event === "staged") {
|
|
3526
|
-
const mark = value;
|
|
3527
|
-
if (typeof cutAt !== "string" || typeof mark.pendingStageId !== "string" || !(mark.state === "waiting" || mark.state === "accepted" && typeof mark.proposalId === "string" || (mark.state === "rejected" || mark.state === "conflict" || mark.state === "discarded") && typeof mark.reason === "string")) throw new Error("Unreadable stage mark in " + path);
|
|
3528
|
-
}
|
|
3529
|
-
return value;
|
|
3530
|
-
}
|
|
3703
|
+
if (f3) {
|
|
3704
|
+
let l4 = e4;
|
|
3705
|
+
if (e4 === l4) if (r2 && typeof r2 == "object" && !Array.isArray(r2)) {
|
|
3706
|
+
let m6;
|
|
3707
|
+
if (r2.data === void 0 && (m6 = "data")) return h7.errors = [{ instancePath: s7, schemaPath: "#/allOf/1/required", keyword: "required", params: { missingProperty: m6 }, message: "must have required property '" + m6 + "'" }], false;
|
|
3708
|
+
r2.data !== void 0 && (u3(r2.data, { instancePath: s7 + "/data", parentData: r2, parentDataProperty: "data", rootData: g }) || (p6 = p6 === null ? u3.errors : p6.concat(u3.errors), e4 = p6.length));
|
|
3709
|
+
} else return h7.errors = [{ instancePath: s7, schemaPath: "#/allOf/1/type", keyword: "type", params: { type: "object" }, message: "must be object" }], false;
|
|
3710
|
+
var f3 = l4 === e4;
|
|
3531
3711
|
}
|
|
3532
|
-
|
|
3712
|
+
return h7.errors = p6, e4 === 0;
|
|
3533
3713
|
}
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3714
|
+
var C = h7;
|
|
3715
|
+
|
|
3716
|
+
// ../../packages/contracts/src/generated/endpoints/getV1HourBank.ts
|
|
3717
|
+
var getV1HourBank = {
|
|
3718
|
+
method: "GET",
|
|
3719
|
+
path: "/v1/hour-bank/",
|
|
3720
|
+
operation: "GET /v1/hour-bank/",
|
|
3721
|
+
revision: contractRevision,
|
|
3722
|
+
responses: {
|
|
3723
|
+
200: { mediaType: "application/json", schemaId: "urn:flowmind:getV1HourBank:200", validate: C }
|
|
3724
|
+
},
|
|
3725
|
+
error: { mediaType: "application/json", schemaId: "ApiErrorEnvelope", validate: N }
|
|
3726
|
+
};
|
|
3727
|
+
|
|
3728
|
+
// src/contracts.ts
|
|
3729
|
+
var STAGE_TIMEOUT_MS = 3e4;
|
|
3730
|
+
var projectStatus = (project) => {
|
|
3731
|
+
if (project.archivedAt) return "archived";
|
|
3732
|
+
if (project.completedAt) return "completed";
|
|
3733
|
+
if (project.launchedAt) return "launched";
|
|
3734
|
+
return "active";
|
|
3735
|
+
};
|
|
3736
|
+
var listProjects = async (request = apiRequest) => {
|
|
3737
|
+
const data = await request(getV1Projects, {});
|
|
3738
|
+
return data.projects;
|
|
3739
|
+
};
|
|
3740
|
+
var getAgentCapabilities = (request = apiRequest) => request(getV1AgentCapabilities, {});
|
|
3741
|
+
var listAgentProjects = async (request = apiRequest) => {
|
|
3742
|
+
const data = await request(getV1AgentProjects, {});
|
|
3743
|
+
return data.projects;
|
|
3744
|
+
};
|
|
3745
|
+
var listAgentRoutines = async (request = apiRequest) => {
|
|
3746
|
+
const data = await request(getV1AgentRoutines, {});
|
|
3747
|
+
return data.routines;
|
|
3748
|
+
};
|
|
3749
|
+
var withIdempotencyKey = (idempotencyKey) => ({
|
|
3750
|
+
headers: { "Idempotency-Key": idempotencyKey }
|
|
3751
|
+
});
|
|
3752
|
+
var createAgentLogProposal = async (input2, idempotencyKey, request = apiRequest) => {
|
|
3753
|
+
const data = await request(
|
|
3754
|
+
postV1AgentLogProposals,
|
|
3755
|
+
{ body: input2 },
|
|
3756
|
+
{ ...withIdempotencyKey(idempotencyKey), preserveRejectionMessage: true, signal: AbortSignal.timeout(STAGE_TIMEOUT_MS) }
|
|
3544
3757
|
);
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
if (
|
|
3566
|
-
throw new Error("
|
|
3567
|
-
}
|
|
3568
|
-
if (transcriptPath !== void 0 && transcriptPath !== null && typeof transcriptPath !== "string") {
|
|
3569
|
-
throw new Error("Hook input transcript_path must be a string when present.");
|
|
3758
|
+
return data.proposal;
|
|
3759
|
+
};
|
|
3760
|
+
var listAgentLogProposals = async (status, request = apiRequest) => {
|
|
3761
|
+
const data = await request(getV1AgentLogProposals, { query: { status } });
|
|
3762
|
+
return data.proposals;
|
|
3763
|
+
};
|
|
3764
|
+
var cancelAgentLogProposal = async (proposalId, idempotencyKey, request = apiRequest) => {
|
|
3765
|
+
const data = await request(
|
|
3766
|
+
postV1AgentLogProposalsByProposalIdCancel,
|
|
3767
|
+
{ params: { proposalId } },
|
|
3768
|
+
withIdempotencyKey(idempotencyKey)
|
|
3769
|
+
);
|
|
3770
|
+
return data.proposal;
|
|
3771
|
+
};
|
|
3772
|
+
var parseNaturalLog = (message, request = apiRequest) => request(postV1AiLog, { body: { message } });
|
|
3773
|
+
var confirmNaturalLog = async (input2, idempotencyKey, request = apiRequest) => {
|
|
3774
|
+
return request(postV1AiLogConfirm, { body: input2 }, withIdempotencyKey(idempotencyKey));
|
|
3775
|
+
};
|
|
3776
|
+
var durationMinutesFromHours = (hours) => {
|
|
3777
|
+
const minutes = hours * 60;
|
|
3778
|
+
if (!Number.isSafeInteger(Math.round(minutes)) || Math.round(minutes) / 60 !== hours) {
|
|
3779
|
+
throw new Error("Hours must resolve to whole minutes.");
|
|
3570
3780
|
}
|
|
3571
|
-
return
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
transcriptPath: typeof transcriptPath === "string" && transcriptPath !== "" ? transcriptPath : null
|
|
3578
|
-
};
|
|
3579
|
-
}
|
|
3580
|
-
var proofRule = `Write the outcome as one line of at most ${AGENT_OUTCOME_MAX_LENGTH} characters: what changed. Add up to ${AGENT_DETAILS_MAX_COUNT} optional details for proof or open items, each one line of at most ${AGENT_DETAIL_MAX_LENGTH} characters.`;
|
|
3581
|
-
function sessionContext(projectName) {
|
|
3582
|
-
return [
|
|
3583
|
-
`Flowmind: this repository is linked to the Flowmind project "${terminalText(projectName)}".`,
|
|
3584
|
-
"When you finish a material unit of work here, stage it before you start the next one, with",
|
|
3585
|
-
'`flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`,',
|
|
3586
|
-
"adding `--artifact-url <commit, PR, or document URL>` when one exists.",
|
|
3587
|
-
proofRule,
|
|
3588
|
-
"When several units finish together, stage them as one proof.",
|
|
3589
|
-
"The CLI fills in the project and the recorded session time, including the time of your subagents.",
|
|
3590
|
-
"Subagents do not stage. Share the returned review URL with the user.",
|
|
3591
|
-
"Do not use `flowmind log` for agent work."
|
|
3592
|
-
].join(" ");
|
|
3593
|
-
}
|
|
3594
|
-
function unstagedReminder(projectName, minutes) {
|
|
3595
|
-
return [
|
|
3596
|
-
`Flowmind: ${minutes} minutes of agent work in this session are not staged yet for "${terminalText(projectName)}".`,
|
|
3597
|
-
"When the current unit of work is complete, stage it with",
|
|
3598
|
-
'`flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`',
|
|
3599
|
-
"and share the review URL.",
|
|
3600
|
-
proofRule
|
|
3601
|
-
].join(" ");
|
|
3602
|
-
}
|
|
3603
|
-
var projectLabel = (link2) => `"${terminalText(link2.name)}" (${link2.projectId})`;
|
|
3604
|
-
function remoteLinkedContext(link2) {
|
|
3605
|
-
return [
|
|
3606
|
-
`Flowmind: the remote already links this repository to the Flowmind project ${projectLabel(link2)},`,
|
|
3607
|
-
"but this checkout has no .flowmind.json yet.",
|
|
3608
|
-
"Run `git pull` to get .flowmind.json. Never create a new Flowmind project for this repository.",
|
|
3609
|
-
'After the pull, stage finished work with `flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`.',
|
|
3610
|
-
proofRule
|
|
3611
|
-
].join(" ");
|
|
3612
|
-
}
|
|
3613
|
-
function unlinkedContext() {
|
|
3614
|
-
return [
|
|
3615
|
-
"Flowmind: this git repository is not linked to a Flowmind project.",
|
|
3616
|
-
"If it is a project the user works on, link it before you finish:",
|
|
3617
|
-
"1. Run `git pull` first. Another machine may have linked it already.",
|
|
3618
|
-
"2. Look for the existing project with `flowmind projects --json`. One product is one project on every machine; match by product, repository and name.",
|
|
3619
|
-
'3. Only for a new product, create one with `flowmind projects add "<name>" --description "<one line>" --json`.',
|
|
3620
|
-
"4. Run `flowmind link <project-id>` and commit .flowmind.json.",
|
|
3621
|
-
"For a third-party clone or a scratch folder, run `flowmind link --ignore`. Ask the user once when you are unsure."
|
|
3622
|
-
].join("\n");
|
|
3623
|
-
}
|
|
3624
|
-
function remoteLinkedReminder(link2, minutes) {
|
|
3625
|
-
return [
|
|
3626
|
-
`Flowmind: ${minutes} minutes of agent work, and this checkout still has no .flowmind.json.`,
|
|
3627
|
-
`The remote links this repository to ${projectLabel(link2)}. Run \`git pull\`; never create a new project for it.`
|
|
3628
|
-
].join(" ");
|
|
3629
|
-
}
|
|
3630
|
-
function unlinkedReminder(minutes) {
|
|
3631
|
-
return [
|
|
3632
|
-
`Flowmind: ${minutes} minutes of agent work, and this repository is still not linked to a Flowmind project.`,
|
|
3633
|
-
"Before you finish: run `git pull`, find the project with `flowmind projects --json`",
|
|
3634
|
-
"(create one with `flowmind projects add` only for a new product), run `flowmind link <project-id>`, and commit .flowmind.json.",
|
|
3635
|
-
"For a third-party clone or a scratch folder, run `flowmind link --ignore`."
|
|
3636
|
-
].join(" ");
|
|
3637
|
-
}
|
|
3638
|
-
async function pruneOldSessions(now) {
|
|
3639
|
-
let names;
|
|
3640
|
-
try {
|
|
3641
|
-
names = await readdir4(sessionsDir());
|
|
3642
|
-
} catch (error) {
|
|
3643
|
-
if (error.code === "ENOENT") return;
|
|
3644
|
-
throw error;
|
|
3781
|
+
return Math.round(minutes);
|
|
3782
|
+
};
|
|
3783
|
+
var resolveActiveProjectByName = (projects, projectName) => {
|
|
3784
|
+
const matches = projects.filter((project) => project.name === projectName && project.archivedAt === null);
|
|
3785
|
+
if (matches.length === 0) {
|
|
3786
|
+
throw new Error(`No active project named "${projectName}"`);
|
|
3645
3787
|
}
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
if (now.getTime() - (await stat2(path)).mtimeMs > RETENTION_MS) await rm(path, { force: true });
|
|
3788
|
+
if (matches.length > 1) {
|
|
3789
|
+
throw new Error(`Multiple active projects named "${projectName}". Use Flowmind web to disambiguate before logging.`);
|
|
3649
3790
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
const
|
|
3654
|
-
const
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
await append(input2.sessionId, {
|
|
3660
|
-
event: "session",
|
|
3661
|
-
at: at2,
|
|
3662
|
-
up,
|
|
3663
|
-
sessionId: input2.sessionId,
|
|
3664
|
-
agent,
|
|
3665
|
-
cwd: input2.cwd,
|
|
3666
|
-
...input2.transcriptPath === null ? {} : { transcriptPath: input2.transcriptPath }
|
|
3667
|
-
});
|
|
3668
|
-
await pruneOldSessions(now.at);
|
|
3669
|
-
const repository = await repositoryStatus(input2.cwd);
|
|
3670
|
-
switch (repository.status) {
|
|
3671
|
-
case "linked":
|
|
3672
|
-
return sessionContext(repository.link.name);
|
|
3673
|
-
case "remote-linked":
|
|
3674
|
-
return remoteLinkedContext(repository.link);
|
|
3675
|
-
case "unlinked":
|
|
3676
|
-
return unlinkedContext();
|
|
3677
|
-
case "ignored":
|
|
3678
|
-
case "outside-git":
|
|
3679
|
-
return "";
|
|
3680
|
-
}
|
|
3791
|
+
return matches[0];
|
|
3792
|
+
};
|
|
3793
|
+
var createStructuredTimeEntry = async (input2, request = apiRequest) => {
|
|
3794
|
+
const durationMinutes = durationMinutesFromHours(input2.hours);
|
|
3795
|
+
const now = input2.now ?? /* @__PURE__ */ new Date();
|
|
3796
|
+
let start = new Date(now.getTime() - durationMinutes * 6e4);
|
|
3797
|
+
if (input2.startedAt !== void 0) {
|
|
3798
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(input2.startedAt)) {
|
|
3799
|
+
throw new Error("started-at must be a valid ISO date-time with a timezone.");
|
|
3681
3800
|
}
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
event: "start",
|
|
3689
|
-
at: at2,
|
|
3690
|
-
up,
|
|
3691
|
-
sessionId: input2.sessionId,
|
|
3692
|
-
agent,
|
|
3693
|
-
cwd: input2.cwd,
|
|
3694
|
-
turnId: input2.turnId
|
|
3695
|
-
});
|
|
3696
|
-
return promptReminder(input2.sessionId, input2.cwd, now);
|
|
3697
|
-
case "Stop":
|
|
3698
|
-
case "Interrupt":
|
|
3699
|
-
if (subagent !== null) return "";
|
|
3700
|
-
await append(input2.sessionId, { event: "stop", at: at2, up, turnId: input2.turnId });
|
|
3701
|
-
if (agent === "codex" && input2.turnId !== null) await addMissedTurnStart(input2, now);
|
|
3702
|
-
return "";
|
|
3703
|
-
default:
|
|
3704
|
-
throw new Error(`Unsupported hook event: ${input2.event}`);
|
|
3801
|
+
const calendarDay = /* @__PURE__ */ new Date(`${input2.startedAt.slice(0, 10)}T00:00:00.000Z`);
|
|
3802
|
+
if (Number.isNaN(calendarDay.getTime()) || calendarDay.toISOString().slice(0, 10) !== input2.startedAt.slice(0, 10)) {
|
|
3803
|
+
throw new Error("started-at must be a valid ISO date-time.");
|
|
3804
|
+
}
|
|
3805
|
+
start = new Date(input2.startedAt);
|
|
3806
|
+
if (Number.isNaN(start.getTime())) throw new Error("started-at must be a valid ISO date-time.");
|
|
3705
3807
|
}
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3808
|
+
if (start.getTime() + durationMinutes * 6e4 > now.getTime()) {
|
|
3809
|
+
throw new Error("Logged work cannot end in the future");
|
|
3810
|
+
}
|
|
3811
|
+
const prepare = async () => {
|
|
3812
|
+
const project = resolveActiveProjectByName(await listProjects(request), input2.projectName);
|
|
3813
|
+
return {
|
|
3814
|
+
projectId: project.id,
|
|
3815
|
+
startedAt: input2.startedAt === void 0 ? new Date((input2.now ?? /* @__PURE__ */ new Date()).getTime() - durationMinutes * 6e4).toISOString() : start.toISOString(),
|
|
3816
|
+
durationMinutes,
|
|
3817
|
+
...input2.nextStep !== void 0 ? { nextStep: input2.nextStep } : {},
|
|
3818
|
+
...input2.description !== void 0 ? { description: input2.description } : {},
|
|
3819
|
+
...input2.note !== void 0 ? { note: input2.note } : {},
|
|
3820
|
+
...input2.artifactUrl !== void 0 ? { artifactUrl: input2.artifactUrl } : {}
|
|
3821
|
+
};
|
|
3822
|
+
};
|
|
3823
|
+
return manualSave(["structured", input2], prepare, (body, key) => request(postV1TimeEntries, { body }, { headers: { [IDEMPOTENCY_KEY_HEADER]: key } }));
|
|
3715
3824
|
};
|
|
3716
|
-
|
|
3717
|
-
const
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
}
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
const
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
}
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3825
|
+
var getStatusSnapshot = async (request = apiRequest) => {
|
|
3826
|
+
const [publicStats, streak, xp] = await Promise.all([
|
|
3827
|
+
request(getV1MePublicStats, {}),
|
|
3828
|
+
request(getV1MeStreak, {}),
|
|
3829
|
+
request(getV1XpSummary, {})
|
|
3830
|
+
]);
|
|
3831
|
+
return { publicStats, streak, xp };
|
|
3832
|
+
};
|
|
3833
|
+
var getDetailedStats = async (request = apiRequest) => {
|
|
3834
|
+
const [status, badges, hourBank] = await Promise.all([
|
|
3835
|
+
getStatusSnapshot(request),
|
|
3836
|
+
request(getV1MeBadges, {}),
|
|
3837
|
+
request(getV1HourBank, {})
|
|
3838
|
+
]);
|
|
3839
|
+
return { ...status, badges, hourBank };
|
|
3840
|
+
};
|
|
3841
|
+
|
|
3842
|
+
// src/project-ref.ts
|
|
3843
|
+
async function resolveAgentProject(reference) {
|
|
3844
|
+
const wanted = reference.trim();
|
|
3845
|
+
const projects = await listAgentProjects();
|
|
3846
|
+
const matches = isProjectId(wanted) ? projects.filter((project) => project.id.toLowerCase() === wanted.toLowerCase()) : projects.filter((project) => project.name.toLowerCase() === wanted.toLowerCase());
|
|
3847
|
+
if (matches.length === 1) return matches[0];
|
|
3848
|
+
const list = (matches.length > 1 ? matches : projects).map((project) => ` ${terminalText(project.name)} (${project.id})`).join("\n");
|
|
3849
|
+
if (matches.length > 1) {
|
|
3850
|
+
throw new Error(`Several projects are named "${terminalText(wanted)}". Use a project id:
|
|
3851
|
+
${list}`);
|
|
3852
|
+
}
|
|
3853
|
+
throw new Error(
|
|
3854
|
+
`No active project matches "${terminalText(wanted)}".` + (list ? `
|
|
3855
|
+
Active projects:
|
|
3856
|
+
${list}` : " No active projects exist.")
|
|
3857
|
+
);
|
|
3736
3858
|
}
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3859
|
+
|
|
3860
|
+
// src/repository-status.ts
|
|
3861
|
+
import { execFile } from "node:child_process";
|
|
3862
|
+
var GIT_TIMEOUT_MS = 3e3;
|
|
3863
|
+
function git(root, args, absentCode) {
|
|
3864
|
+
return new Promise((resolve3, reject) => {
|
|
3865
|
+
execFile(
|
|
3866
|
+
"git",
|
|
3867
|
+
args,
|
|
3868
|
+
{
|
|
3869
|
+
cwd: root,
|
|
3870
|
+
encoding: "utf8",
|
|
3871
|
+
timeout: GIT_TIMEOUT_MS,
|
|
3872
|
+
windowsHide: true,
|
|
3873
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
|
|
3874
|
+
},
|
|
3875
|
+
(error, stdout) => {
|
|
3876
|
+
if (!error) resolve3(stdout);
|
|
3877
|
+
else if (!error.killed && absentCode !== void 0 && error.code === absentCode) resolve3(null);
|
|
3878
|
+
else reject(new Error("Git lookup failed (" + args[0] + "): " + (error.killed ? "timed out" : error.code)));
|
|
3879
|
+
}
|
|
3880
|
+
);
|
|
3881
|
+
});
|
|
3744
3882
|
}
|
|
3745
|
-
function
|
|
3746
|
-
const
|
|
3747
|
-
|
|
3748
|
-
const
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
}
|
|
3883
|
+
async function readRemoteLink(root) {
|
|
3884
|
+
const branch = (await git(root, ["symbolic-ref", "--quiet", "HEAD"], 1))?.trim();
|
|
3885
|
+
const upstream = branch ? (await git(root, ["for-each-ref", "--format=%(upstream)", branch]))?.trim() : null;
|
|
3886
|
+
const refs = [
|
|
3887
|
+
...upstream?.startsWith("refs/remotes/") ? [upstream] : [],
|
|
3888
|
+
"refs/remotes/origin/HEAD",
|
|
3889
|
+
"refs/remotes/origin/main",
|
|
3890
|
+
"refs/remotes/origin/master"
|
|
3891
|
+
];
|
|
3892
|
+
for (const ref of new Set(refs)) {
|
|
3893
|
+
if (await git(root, ["rev-parse", "--verify", "--quiet", ref], 1) === null) continue;
|
|
3894
|
+
if (!(await git(root, ["ls-tree", "--name-only", ref, "--", LINK_FILE]))?.trim()) continue;
|
|
3895
|
+
const text2 = await git(root, ["show", `${ref}:${LINK_FILE}`]);
|
|
3896
|
+
return { ref, link: parseLink(text2, `${ref}:${LINK_FILE}`, root) };
|
|
3897
|
+
}
|
|
3898
|
+
return null;
|
|
3760
3899
|
}
|
|
3761
|
-
async function
|
|
3762
|
-
const
|
|
3763
|
-
|
|
3764
|
-
if (
|
|
3765
|
-
|
|
3766
|
-
if (file === null) return;
|
|
3767
|
-
const turn = file.turns.find((candidate) => candidate.turnId === turnId);
|
|
3768
|
-
if (!turn) throw new Error(`The Codex session file ${file.path} has no start for turn ${turnId}.`);
|
|
3769
|
-
const start = missedStartEvent(events, turn, input2, now, lastWakeMs());
|
|
3770
|
-
if (start) await append(input2.sessionId, start);
|
|
3900
|
+
async function remoteStatus(root) {
|
|
3901
|
+
const remote = await readRemoteLink(root);
|
|
3902
|
+
if (remote?.link.kind === "linked") return { status: "remote-linked", root, link: remote.link, ref: remote.ref };
|
|
3903
|
+
if (remote?.link.kind === "ignored") return { status: "ignored", root, link: remote.link };
|
|
3904
|
+
return { status: "unlinked", root };
|
|
3771
3905
|
}
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
return { from, to, ms: ms > OPEN_TURN_LIMIT_MS ? 0 : ms, awake: from.up !== null && to.up !== null };
|
|
3906
|
+
async function repositoryStatus(cwd) {
|
|
3907
|
+
const local = await readLink(cwd);
|
|
3908
|
+
if (local?.kind === "linked") return { status: "linked", root: local.root, link: local };
|
|
3909
|
+
if (local?.kind === "ignored") return { status: "ignored", root: local.root, link: local };
|
|
3910
|
+
const root = await findGitRoot(cwd);
|
|
3911
|
+
return root === null ? { status: "outside-git" } : remoteStatus(root);
|
|
3779
3912
|
}
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3913
|
+
async function originUrl(root) {
|
|
3914
|
+
const url = (await git(root, ["config", "--get", "remote.origin.url"], 1))?.trim();
|
|
3915
|
+
if (!url) return null;
|
|
3916
|
+
let parsed;
|
|
3917
|
+
try {
|
|
3918
|
+
parsed = new URL(url);
|
|
3919
|
+
} catch {
|
|
3920
|
+
return url;
|
|
3921
|
+
}
|
|
3922
|
+
if (!parsed.username && !parsed.password) return url;
|
|
3923
|
+
parsed.username = "";
|
|
3924
|
+
parsed.password = "";
|
|
3925
|
+
return parsed.toString();
|
|
3784
3926
|
}
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
});
|
|
3927
|
+
|
|
3928
|
+
// src/agent-session.ts
|
|
3929
|
+
var RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
3930
|
+
var ACTIVE_WINDOW_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
3931
|
+
var REMINDER_MINUTES = 20;
|
|
3932
|
+
var OPEN_TURN_LIMIT_MS = 12 * 60 * 60 * 1e3;
|
|
3933
|
+
var MISSED_START_MS = 15e3;
|
|
3934
|
+
var CLOCK_DRIFT_MS = 1e3;
|
|
3935
|
+
var EVENTS = /* @__PURE__ */ new Set(["session", "start", "stop", "subagent", "staged", "unlinked-reminder"]);
|
|
3936
|
+
var sessionsDir = () => join8(accountDirectory(), "agent-sessions");
|
|
3937
|
+
var periodPoint = (p6) => ({ wall: Date.parse(p6.at), up: p6.up });
|
|
3938
|
+
var signedInPeriods = () => ownershipPeriods().map(({ start, end }) => ({
|
|
3939
|
+
start: periodPoint(start),
|
|
3940
|
+
end: end && periodPoint(end)
|
|
3941
|
+
}));
|
|
3942
|
+
function sessionFile(sessionId) {
|
|
3943
|
+
const name = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$/.test(sessionId) ? sessionId : createHash4("sha256").update(sessionId).digest("hex");
|
|
3944
|
+
return join8(sessionsDir(), `${name}.jsonl`);
|
|
3802
3945
|
}
|
|
3803
|
-
function
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
if (
|
|
3817
|
-
unknownStarts = unknownStarts ? unknownStarts + 1 : 2;
|
|
3818
|
-
open2 = null;
|
|
3819
|
-
break;
|
|
3820
|
-
}
|
|
3821
|
-
if (open2 && open2.turnId !== null && open2.turnId === event.turnId) {
|
|
3822
|
-
if (sameEpoch(open2.start, point)) open2.last = point;
|
|
3823
|
-
break;
|
|
3824
|
-
}
|
|
3825
|
-
if (open2) turns.push({ start: open2.start, end: open2.last });
|
|
3826
|
-
open2 = { start: point, last: point, turnId: event.turnId };
|
|
3827
|
-
break;
|
|
3828
|
-
case "stop": {
|
|
3829
|
-
if (event.turnId === null && unknownStarts > 0) {
|
|
3830
|
-
unknownStarts--;
|
|
3831
|
-
break;
|
|
3832
|
-
}
|
|
3833
|
-
const matches = open2 !== null && event.turnId === open2.turnId;
|
|
3834
|
-
if (open2 && matches) {
|
|
3835
|
-
turns.push({ start: open2.start, end: sameEpoch(open2.start, point) ? point : open2.last });
|
|
3836
|
-
open2 = null;
|
|
3837
|
-
}
|
|
3838
|
-
break;
|
|
3946
|
+
async function append(sessionId, event) {
|
|
3947
|
+
await mkdir5(sessionsDir(), { recursive: true });
|
|
3948
|
+
await appendFile(sessionFile(sessionId), `${JSON.stringify(event)}
|
|
3949
|
+
`, { encoding: "utf8", mode: 384 });
|
|
3950
|
+
}
|
|
3951
|
+
var byTime = (events) => events.sort((a5, b4) => Date.parse(a5.at) - Date.parse(b4.at));
|
|
3952
|
+
function parseEvent(line, path) {
|
|
3953
|
+
const value = JSON.parse(line);
|
|
3954
|
+
if (typeof value === "object" && value !== null) {
|
|
3955
|
+
const { event, at: at2, up, cutAt, cutUp } = value;
|
|
3956
|
+
if (typeof event === "string" && EVENTS.has(event) && typeof at2 === "string" && !Number.isNaN(Date.parse(at2)) && (up === void 0 || Number.isFinite(up)) && (cutAt === void 0 || typeof cutAt === "string" && !Number.isNaN(Date.parse(cutAt))) && (cutUp === void 0 || Number.isFinite(cutUp))) {
|
|
3957
|
+
if (event === "staged") {
|
|
3958
|
+
const mark = value;
|
|
3959
|
+
if (typeof cutAt !== "string" || typeof mark.pendingStageId !== "string" || !(mark.state === "waiting" || mark.state === "accepted" && typeof mark.proposalId === "string" || (mark.state === "rejected" || mark.state === "conflict" || mark.state === "discarded") && typeof mark.reason === "string")) throw new Error("Unreadable stage mark in " + path);
|
|
3839
3960
|
}
|
|
3840
|
-
|
|
3841
|
-
if (!lastStage || cutOf(event).wall >= cutOf(lastStage).wall) lastStage = event;
|
|
3842
|
-
break;
|
|
3843
|
-
case "subagent":
|
|
3844
|
-
if (open2 && open2.turnId === event.turnId && sameEpoch(open2.start, point)) open2.last = point;
|
|
3845
|
-
break;
|
|
3961
|
+
return value;
|
|
3846
3962
|
}
|
|
3847
3963
|
}
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3964
|
+
throw new Error(`Unreadable agent session record in ${path}`);
|
|
3965
|
+
}
|
|
3966
|
+
async function readEventsAt(path) {
|
|
3967
|
+
if (await readIfPresent(path + RETIRED) !== null) return null;
|
|
3968
|
+
const sources = await Promise.all([path + ADOPTED, path].map(async (file) => ({ file, text: await readIfPresent(file) })));
|
|
3969
|
+
if (sources.every((source) => source.text === null)) return null;
|
|
3970
|
+
return byTime(
|
|
3971
|
+
sources.flatMap(
|
|
3972
|
+
({ file, text: text2 }) => (text2 ?? "").split("\n").filter((line) => line.trim() !== "").map((line) => parseEvent(line, file))
|
|
3973
|
+
)
|
|
3852
3974
|
);
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3975
|
+
}
|
|
3976
|
+
var ADOPTED = ".adopted";
|
|
3977
|
+
var RETIRED = ".retired";
|
|
3978
|
+
async function readIfPresent(path) {
|
|
3979
|
+
try {
|
|
3980
|
+
return await readFile6(path, "utf8");
|
|
3981
|
+
} catch (error) {
|
|
3982
|
+
if (error.code === "ENOENT") return null;
|
|
3983
|
+
throw error;
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
var journalNames = (names) => [
|
|
3987
|
+
...new Set(
|
|
3988
|
+
names.flatMap((name) => {
|
|
3989
|
+
if (name.endsWith(".jsonl")) return [name];
|
|
3990
|
+
for (const suffix of [ADOPTED, RETIRED]) if (name.endsWith(".jsonl" + suffix)) return [name.slice(0, -suffix.length)];
|
|
3991
|
+
return [];
|
|
3992
|
+
})
|
|
3993
|
+
)
|
|
3994
|
+
];
|
|
3995
|
+
async function lastWritten(path) {
|
|
3996
|
+
let newest = -Infinity;
|
|
3997
|
+
for (const file of [path, path + ADOPTED]) {
|
|
3998
|
+
try {
|
|
3999
|
+
newest = Math.max(newest, (await stat3(file)).mtimeMs);
|
|
4000
|
+
} catch (error) {
|
|
4001
|
+
if (error.code !== "ENOENT") throw error;
|
|
3861
4002
|
}
|
|
3862
|
-
cut = {
|
|
3863
|
-
at: new Date(part.from.wall + remaining).toISOString(),
|
|
3864
|
-
up: part.awake && part.from.up !== null ? part.from.up + remaining : null
|
|
3865
|
-
};
|
|
3866
|
-
break;
|
|
3867
4003
|
}
|
|
3868
|
-
return
|
|
3869
|
-
agent: first.agent,
|
|
3870
|
-
unstagedMs,
|
|
3871
|
-
minutes,
|
|
3872
|
-
startedAt: counted.length > 0 ? new Date(counted[0].from.wall).toISOString() : null,
|
|
3873
|
-
cut,
|
|
3874
|
-
lastProposalId: lastStage?.state === "accepted" ? lastStage.proposalId : null,
|
|
3875
|
-
lastPendingStageId: lastStage && lastStage.state !== "accepted" ? lastStage.pendingStageId : null,
|
|
3876
|
-
lastHold: lastStage?.state === "conflict" || lastStage?.state === "discarded" ? lastStage.reason : null,
|
|
3877
|
-
lastRejection: lastStage?.state === "rejected" ? lastStage.reason : null
|
|
3878
|
-
};
|
|
4004
|
+
return newest;
|
|
3879
4005
|
}
|
|
3880
|
-
|
|
3881
|
-
const
|
|
3882
|
-
|
|
4006
|
+
function currentFormOf(line, path) {
|
|
4007
|
+
const value = JSON.parse(line);
|
|
4008
|
+
if (typeof value !== "object" || value === null || value.event !== "staged" || "state" in value) {
|
|
4009
|
+
return parseEvent(line, path);
|
|
4010
|
+
}
|
|
4011
|
+
const { proposalId, pendingStageId, cutAt, cutUp, at: at2, up, ...rest } = value;
|
|
4012
|
+
const cut = cutAt === void 0 ? { cutAt: at2, ...up === void 0 ? {} : { cutUp: up } } : { cutAt, ...cutUp === void 0 ? {} : { cutUp } };
|
|
4013
|
+
const mark = typeof proposalId === "string" ? { pendingStageId: "legacy-" + proposalId, state: "accepted", proposalId } : { pendingStageId, state: "waiting" };
|
|
4014
|
+
return parseEvent(JSON.stringify({ ...rest, at: at2, ...up === void 0 ? {} : { up }, ...cut, ...mark }), path);
|
|
4015
|
+
}
|
|
4016
|
+
var SOURCES = "0.3.7-sources.json";
|
|
4017
|
+
async function adoptedSources() {
|
|
4018
|
+
const text2 = await readIfPresent(join8(sessionsDir(), SOURCES));
|
|
4019
|
+
if (text2 === null) return {};
|
|
4020
|
+
try {
|
|
4021
|
+
const value = JSON.parse(text2);
|
|
4022
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) return value;
|
|
4023
|
+
} catch (error) {
|
|
4024
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
4025
|
+
}
|
|
4026
|
+
return {};
|
|
3883
4027
|
}
|
|
3884
|
-
function
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
if (
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
4028
|
+
async function legacyJournals(fromDir, now) {
|
|
4029
|
+
let names;
|
|
4030
|
+
try {
|
|
4031
|
+
names = await readdir7(fromDir);
|
|
4032
|
+
} catch (error) {
|
|
4033
|
+
if (error.code === "ENOENT") return [];
|
|
4034
|
+
throw error;
|
|
4035
|
+
}
|
|
4036
|
+
const journals = [];
|
|
4037
|
+
for (const name of names.filter((entry) => entry.endsWith(".jsonl"))) {
|
|
4038
|
+
const from = join8(fromDir, name);
|
|
4039
|
+
try {
|
|
4040
|
+
const info = await stat3(from);
|
|
4041
|
+
journals.push({ name, from, size: info.size, mtimeMs: info.mtimeMs, modified: info.mtime, expired: now - info.mtimeMs > RETENTION_MS });
|
|
4042
|
+
} catch (error) {
|
|
4043
|
+
if (error.code !== "ENOENT") throw error;
|
|
3893
4044
|
}
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
4045
|
+
}
|
|
4046
|
+
return journals;
|
|
4047
|
+
}
|
|
4048
|
+
var unchanged = (journal, sources) => sources[journal.name]?.size === journal.size && sources[journal.name]?.mtimeMs === journal.mtimeMs;
|
|
4049
|
+
async function legacyJournalsWaiting(fromDir, now = Date.now()) {
|
|
4050
|
+
const journals = await legacyJournals(fromDir, now);
|
|
4051
|
+
if (journals.length === 0) return false;
|
|
4052
|
+
const sources = await adoptedSources();
|
|
4053
|
+
return journals.some((journal) => journal.expired || !unchanged(journal, sources));
|
|
4054
|
+
}
|
|
4055
|
+
async function adoptLegacyJournals(fromDir, extendStart, now = Date.now()) {
|
|
4056
|
+
const sources = await adoptedSources();
|
|
4057
|
+
const copies = [];
|
|
4058
|
+
let earliest = Infinity;
|
|
4059
|
+
for (const journal of await legacyJournals(fromDir, now)) {
|
|
4060
|
+
if (unchanged(journal, sources)) continue;
|
|
4061
|
+
const text2 = await readIfPresent(journal.from);
|
|
4062
|
+
if (text2 === null) continue;
|
|
4063
|
+
let events;
|
|
4064
|
+
try {
|
|
4065
|
+
events = text2.split("\n").filter((line) => line.trim() !== "").map((line) => currentFormOf(line, journal.from));
|
|
4066
|
+
} catch {
|
|
4067
|
+
await rename4(journal.from, journal.from + ".unreadable");
|
|
4068
|
+
continue;
|
|
3914
4069
|
}
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
}
|
|
4070
|
+
for (const event of events) earliest = Math.min(earliest, Date.parse(event.at));
|
|
4071
|
+
copies.push({ journal, copy: events.map((event) => JSON.stringify(event) + "\n").join("") });
|
|
4072
|
+
}
|
|
4073
|
+
if (earliest !== Infinity) await extendStart(earliest);
|
|
4074
|
+
await mkdir5(sessionsDir(), { recursive: true });
|
|
4075
|
+
for (const { journal, copy } of copies) {
|
|
4076
|
+
const target = join8(sessionsDir(), journal.name + ADOPTED);
|
|
4077
|
+
await writeFile5(target + ".partial", copy, { mode: 384 });
|
|
4078
|
+
await utimes(target + ".partial", journal.modified, journal.modified);
|
|
4079
|
+
await rename4(target + ".partial", target);
|
|
4080
|
+
sources[journal.name] = { size: journal.size, mtimeMs: journal.mtimeMs };
|
|
4081
|
+
}
|
|
4082
|
+
for (const journal of await legacyJournals(fromDir, now)) {
|
|
4083
|
+
if (!journal.expired || !unchanged(journal, sources)) continue;
|
|
4084
|
+
await rm3(journal.from, { force: true });
|
|
4085
|
+
delete sources[journal.name];
|
|
4086
|
+
}
|
|
4087
|
+
const manifest = join8(sessionsDir(), SOURCES);
|
|
4088
|
+
await writeFile5(manifest + ".partial", JSON.stringify(sources), { mode: 384 });
|
|
4089
|
+
await rename4(manifest + ".partial", manifest);
|
|
4090
|
+
try {
|
|
4091
|
+
await rmdir(fromDir);
|
|
4092
|
+
} catch (error) {
|
|
4093
|
+
const code = error.code;
|
|
4094
|
+
if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "ENOENT") throw error;
|
|
3926
4095
|
}
|
|
3927
|
-
return added;
|
|
3928
|
-
}
|
|
3929
|
-
async function stageTime(sessionId, now) {
|
|
3930
|
-
const events = await readEventsAt(sessionFile(sessionId));
|
|
3931
|
-
if (!events) return null;
|
|
3932
|
-
const owner = events.find(
|
|
3933
|
-
(event) => event.event === "session" || event.event === "start"
|
|
3934
|
-
);
|
|
3935
|
-
if (owner?.agent !== "codex") return measureRecordedTime(events, now, signedInPeriods());
|
|
3936
|
-
const file = await codexTurnsOf(sessionId, transcriptPathOf(events));
|
|
3937
|
-
if (file === null) return measureRecordedTime(events, now, signedInPeriods());
|
|
3938
|
-
const added = missedTurnEvents(events, file.turns, owner, now, lastWakeMs());
|
|
3939
|
-
return measureRecordedTime(byTime([...events, ...added]), now, signedInPeriods());
|
|
3940
4096
|
}
|
|
3941
|
-
|
|
3942
|
-
const
|
|
3943
|
-
if (
|
|
3944
|
-
|
|
3945
|
-
const recorded2 = await recordedTime(sessionId, now);
|
|
3946
|
-
return recorded2 && recorded2.minutes >= REMINDER_MINUTES ? unstagedReminder(link2.name, recorded2.minutes) : "";
|
|
4097
|
+
function parseHookInput(raw) {
|
|
4098
|
+
const value = JSON.parse(raw);
|
|
4099
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
4100
|
+
throw new Error("Hook input must be a JSON object.");
|
|
3947
4101
|
}
|
|
3948
|
-
const
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
4102
|
+
const {
|
|
4103
|
+
session_id: sessionId,
|
|
4104
|
+
hook_event_name: event,
|
|
4105
|
+
cwd,
|
|
4106
|
+
turn_id: turnId,
|
|
4107
|
+
agent_id: agentId,
|
|
4108
|
+
transcript_path: transcriptPath
|
|
4109
|
+
} = value;
|
|
4110
|
+
if (typeof sessionId !== "string" || sessionId === "") throw new Error("Hook input needs a session_id string.");
|
|
4111
|
+
if (typeof event !== "string") throw new Error("Hook input needs a hook_event_name string.");
|
|
4112
|
+
if (typeof cwd !== "string") throw new Error("Hook input needs a cwd string.");
|
|
4113
|
+
if (turnId !== void 0 && turnId !== null && typeof turnId !== "string") {
|
|
4114
|
+
throw new Error("Hook input turn_id must be a string when present.");
|
|
4115
|
+
}
|
|
4116
|
+
if (agentId !== void 0 && agentId !== null && typeof agentId !== "string") {
|
|
4117
|
+
throw new Error("Hook input agent_id must be a string when present.");
|
|
4118
|
+
}
|
|
4119
|
+
if (transcriptPath !== void 0 && transcriptPath !== null && typeof transcriptPath !== "string") {
|
|
4120
|
+
throw new Error("Hook input transcript_path must be a string when present.");
|
|
4121
|
+
}
|
|
4122
|
+
return {
|
|
4123
|
+
sessionId,
|
|
4124
|
+
event,
|
|
4125
|
+
cwd,
|
|
4126
|
+
turnId: typeof turnId === "string" ? turnId : null,
|
|
4127
|
+
agentId: typeof agentId === "string" && agentId !== "" ? agentId : null,
|
|
4128
|
+
transcriptPath: typeof transcriptPath === "string" && transcriptPath !== "" ? transcriptPath : null
|
|
4129
|
+
};
|
|
3958
4130
|
}
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
return "pending";
|
|
4131
|
+
var proofRule = `Write the outcome as one line of at most ${AGENT_OUTCOME_MAX_LENGTH} characters: what changed. Add up to ${AGENT_DETAILS_MAX_COUNT} optional details for proof or open items, each one line of at most ${AGENT_DETAIL_MAX_LENGTH} characters.`;
|
|
4132
|
+
function sessionContext(projectName) {
|
|
4133
|
+
return [
|
|
4134
|
+
`Flowmind: this repository is linked to the Flowmind project "${terminalText(projectName)}".`,
|
|
4135
|
+
"When you finish a material unit of work here, stage it before you start the next one, with",
|
|
4136
|
+
'`flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`,',
|
|
4137
|
+
"adding `--artifact-url <commit, PR, or document URL>` when one exists.",
|
|
4138
|
+
proofRule,
|
|
4139
|
+
"When several units finish together, stage them as one proof.",
|
|
4140
|
+
"The CLI fills in the project and the recorded session time, including the time of your subagents.",
|
|
4141
|
+
"Subagents do not stage. Share the returned review URL with the user.",
|
|
4142
|
+
"Do not use `flowmind log` for agent work."
|
|
4143
|
+
].join(" ");
|
|
3973
4144
|
}
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
4145
|
+
function unstagedReminder(projectName, minutes) {
|
|
4146
|
+
return [
|
|
4147
|
+
`Flowmind: ${minutes} minutes of agent work in this session are not staged yet for "${terminalText(projectName)}".`,
|
|
4148
|
+
"When the current unit of work is complete, stage it with",
|
|
4149
|
+
'`flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`',
|
|
4150
|
+
"and share the review URL.",
|
|
4151
|
+
proofRule
|
|
4152
|
+
].join(" ");
|
|
4153
|
+
}
|
|
4154
|
+
var projectLabel = (link2) => `"${terminalText(link2.name)}" (${link2.projectId})`;
|
|
4155
|
+
function remoteLinkedContext(link2) {
|
|
4156
|
+
return [
|
|
4157
|
+
`Flowmind: the remote already links this repository to the Flowmind project ${projectLabel(link2)},`,
|
|
4158
|
+
"but this checkout has no .flowmind.json yet.",
|
|
4159
|
+
"Run `git pull` to get .flowmind.json. Never create a new Flowmind project for this repository.",
|
|
4160
|
+
'After the pull, stage finished work with `flowmind agent stage --intention "<goal>" --outcome "<what changed>" --detail "<proof or open item>" --json`.',
|
|
4161
|
+
proofRule
|
|
4162
|
+
].join(" ");
|
|
4163
|
+
}
|
|
4164
|
+
function unlinkedContext() {
|
|
4165
|
+
return [
|
|
4166
|
+
"Flowmind: this git repository is not linked to a Flowmind project.",
|
|
4167
|
+
"If it is a project the user works on, link it before you finish:",
|
|
4168
|
+
"1. Run `git pull` first. Another machine may have linked it already.",
|
|
4169
|
+
"2. Look for the existing project with `flowmind projects --json`. One product is one project on every machine; match by product, repository and name.",
|
|
4170
|
+
'3. Only for a new product, create one with `flowmind projects add "<name>" --description "<one line>" --json`.',
|
|
4171
|
+
"4. Run `flowmind link <project-id>` and commit .flowmind.json.",
|
|
4172
|
+
"For a third-party clone or a scratch folder, run `flowmind link --ignore`. Ask the user once when you are unsure."
|
|
4173
|
+
].join("\n");
|
|
4174
|
+
}
|
|
4175
|
+
function remoteLinkedReminder(link2, minutes) {
|
|
4176
|
+
return [
|
|
4177
|
+
`Flowmind: ${minutes} minutes of agent work, and this checkout still has no .flowmind.json.`,
|
|
4178
|
+
`The remote links this repository to ${projectLabel(link2)}. Run \`git pull\`; never create a new project for it.`
|
|
4179
|
+
].join(" ");
|
|
3985
4180
|
}
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
4181
|
+
function unlinkedReminder(minutes) {
|
|
4182
|
+
return [
|
|
4183
|
+
`Flowmind: ${minutes} minutes of agent work, and this repository is still not linked to a Flowmind project.`,
|
|
4184
|
+
"Before you finish: run `git pull`, find the project with `flowmind projects --json`",
|
|
4185
|
+
"(create one with `flowmind projects add` only for a new product), run `flowmind link <project-id>`, and commit .flowmind.json.",
|
|
4186
|
+
"For a third-party clone or a scratch folder, run `flowmind link --ignore`."
|
|
4187
|
+
].join(" ");
|
|
4188
|
+
}
|
|
4189
|
+
async function pruneOldSessions(now) {
|
|
4190
|
+
let names;
|
|
3993
4191
|
try {
|
|
3994
|
-
names = await
|
|
4192
|
+
names = await readdir7(sessionsDir());
|
|
3995
4193
|
} catch (error) {
|
|
3996
|
-
if (error.code
|
|
3997
|
-
|
|
3998
|
-
for (const name of names.filter((entry) => entry.endsWith(".jsonl"))) {
|
|
3999
|
-
const path = join6(sessionsDir(), name);
|
|
4000
|
-
if (now.getTime() - (await stat2(path)).mtimeMs > ACTIVE_WINDOW_MS) continue;
|
|
4001
|
-
const events = await readEventsAt(path) ?? [];
|
|
4002
|
-
if (codexThread && events.some((event) => event.event === "subagent" && event.agentId === codexThread)) {
|
|
4003
|
-
const main = events.find(
|
|
4004
|
-
(event) => event.event === "session" || event.event === "start"
|
|
4005
|
-
);
|
|
4006
|
-
if (main) return { sessionId: main.sessionId, source: "subagent" };
|
|
4007
|
-
}
|
|
4008
|
-
let last = null;
|
|
4009
|
-
for (const event of events) {
|
|
4010
|
-
if (event.event === "start") last = event;
|
|
4011
|
-
if (event.event === "stop" && event.turnId === last?.turnId) last = null;
|
|
4012
|
-
}
|
|
4013
|
-
if (last?.event === "start" && now.getTime() - Date.parse(last.at) < OPEN_TURN_LIMIT_MS && await findRepoRoot(last.cwd) === root) {
|
|
4014
|
-
active.add(last.sessionId);
|
|
4015
|
-
}
|
|
4194
|
+
if (error.code === "ENOENT") return;
|
|
4195
|
+
throw error;
|
|
4016
4196
|
}
|
|
4017
|
-
|
|
4018
|
-
|
|
4197
|
+
for (const name of journalNames(names)) {
|
|
4198
|
+
const path = join8(sessionsDir(), name);
|
|
4199
|
+
const retiring = await readIfPresent(path + RETIRED) !== null;
|
|
4200
|
+
if (!retiring && now.getTime() - await lastWritten(path) <= RETENTION_MS) continue;
|
|
4201
|
+
if (!retiring && await lastWritten(join8(legacySessionsDir(), name)) !== -Infinity) continue;
|
|
4202
|
+
if (!retiring) await writeFile5(path + RETIRED, "", { mode: 384 });
|
|
4203
|
+
await rm3(path, { force: true });
|
|
4204
|
+
await rm3(path + ADOPTED, { force: true });
|
|
4205
|
+
await rm3(path + RETIRED, { force: true });
|
|
4019
4206
|
}
|
|
4020
|
-
const [only] = active;
|
|
4021
|
-
if (only) return { sessionId: only, source: "active-turn" };
|
|
4022
|
-
return codexThread ? { sessionId: codexThread, source: "codex-env" } : { sessionId: null, source: null };
|
|
4023
4207
|
}
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4208
|
+
async function handleHook(agent, raw, prune, now = currentMoment()) {
|
|
4209
|
+
const input2 = parseHookInput(raw);
|
|
4210
|
+
const at2 = now.at.toISOString();
|
|
4211
|
+
const { up } = now;
|
|
4212
|
+
const subagent = input2.agentId;
|
|
4213
|
+
switch (input2.event) {
|
|
4214
|
+
case "SessionStart": {
|
|
4215
|
+
if (subagent !== null) return "";
|
|
4216
|
+
await append(input2.sessionId, {
|
|
4217
|
+
event: "session",
|
|
4218
|
+
at: at2,
|
|
4219
|
+
up,
|
|
4220
|
+
sessionId: input2.sessionId,
|
|
4221
|
+
agent,
|
|
4222
|
+
cwd: input2.cwd,
|
|
4223
|
+
...input2.transcriptPath === null ? {} : { transcriptPath: input2.transcriptPath }
|
|
4224
|
+
});
|
|
4225
|
+
await prune(now.at);
|
|
4226
|
+
const repository = await repositoryStatus(input2.cwd);
|
|
4227
|
+
switch (repository.status) {
|
|
4228
|
+
case "linked":
|
|
4229
|
+
return sessionContext(repository.link.name);
|
|
4230
|
+
case "remote-linked":
|
|
4231
|
+
return remoteLinkedContext(repository.link);
|
|
4232
|
+
case "unlinked":
|
|
4233
|
+
return unlinkedContext();
|
|
4234
|
+
case "ignored":
|
|
4235
|
+
case "outside-git":
|
|
4236
|
+
return "";
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
case "UserPromptSubmit":
|
|
4240
|
+
if (subagent !== null) {
|
|
4241
|
+
await append(input2.sessionId, { event: "subagent", at: at2, up, agentId: subagent, turnId: input2.turnId });
|
|
4242
|
+
return "";
|
|
4243
|
+
}
|
|
4244
|
+
await append(input2.sessionId, {
|
|
4245
|
+
event: "start",
|
|
4246
|
+
at: at2,
|
|
4247
|
+
up,
|
|
4248
|
+
sessionId: input2.sessionId,
|
|
4249
|
+
agent,
|
|
4250
|
+
cwd: input2.cwd,
|
|
4251
|
+
turnId: input2.turnId
|
|
4252
|
+
});
|
|
4253
|
+
return promptReminder(input2.sessionId, input2.cwd, now);
|
|
4254
|
+
case "Stop":
|
|
4255
|
+
case "Interrupt":
|
|
4256
|
+
if (subagent !== null) return "";
|
|
4257
|
+
await append(input2.sessionId, { event: "stop", at: at2, up, turnId: input2.turnId });
|
|
4258
|
+
if (agent === "codex" && input2.turnId !== null) await addMissedTurnStart(input2, now);
|
|
4259
|
+
return "";
|
|
4260
|
+
default:
|
|
4261
|
+
throw new Error(`Unsupported hook event: ${input2.event}`);
|
|
4059
4262
|
}
|
|
4060
|
-
process.exitCode = 1;
|
|
4061
|
-
};
|
|
4062
|
-
|
|
4063
|
-
// src/legacy-proofs.ts
|
|
4064
|
-
import { mkdir as mkdir5, readdir as readdir6, readFile as readFile6, rename as rename3, rm as rm3, stat as stat3 } from "node:fs/promises";
|
|
4065
|
-
import { join as join8 } from "node:path";
|
|
4066
|
-
|
|
4067
|
-
// src/pending-stages.ts
|
|
4068
|
-
import { spawn } from "node:child_process";
|
|
4069
|
-
import { createHash as createHash4, randomUUID as randomUUID3 } from "node:crypto";
|
|
4070
|
-
import { mkdir as mkdir4, readdir as readdir5, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
4071
|
-
import { basename, join as join7 } from "node:path";
|
|
4072
|
-
|
|
4073
|
-
// src/stored-stage-input.ts
|
|
4074
|
-
var SOURCE_KINDS = [
|
|
4075
|
-
"calendar",
|
|
4076
|
-
"claude_code",
|
|
4077
|
-
"cli",
|
|
4078
|
-
"codex",
|
|
4079
|
-
"github",
|
|
4080
|
-
"linear",
|
|
4081
|
-
"manual",
|
|
4082
|
-
"session_log",
|
|
4083
|
-
"other"
|
|
4084
|
-
];
|
|
4085
|
-
var evidenceKinds = ["calendar_event", "commit", "issue", "linear_issue", "pr", "session_log", "other"];
|
|
4086
|
-
var shape = (v6, keys2) => v6 !== null && typeof v6 === "object" && !Array.isArray(v6) && Object.keys(v6).every((key) => keys2.includes(key));
|
|
4087
|
-
var text = (v6, max) => typeof v6 === "string" && v6.trim().length > 0 && [...v6].length <= max;
|
|
4088
|
-
var optionalText = (v6, max) => v6 === void 0 || v6 === null || text(v6, max);
|
|
4089
|
-
var member = (v6, kinds) => typeof v6 === "string" && kinds.includes(v6);
|
|
4090
|
-
var isoDate = (v6) => typeof v6 === "string" && Number.isFinite(Date.parse(v6)) && new Date(v6).toISOString() === v6;
|
|
4091
|
-
var keys = [
|
|
4092
|
-
"targetKind",
|
|
4093
|
-
"projectId",
|
|
4094
|
-
"startedAt",
|
|
4095
|
-
"durationMinutes",
|
|
4096
|
-
"description",
|
|
4097
|
-
"note",
|
|
4098
|
-
"details",
|
|
4099
|
-
"artifactUrl",
|
|
4100
|
-
"source",
|
|
4101
|
-
"evidence"
|
|
4102
|
-
];
|
|
4103
|
-
function isStoredStageInput(v6) {
|
|
4104
|
-
if (!shape(v6, keys) || v6.targetKind !== "project" || typeof v6.projectId !== "string" || !isProjectId(v6.projectId) || !isoDate(v6.startedAt) || typeof v6.durationMinutes !== "number" || !Number.isSafeInteger(v6.durationMinutes) || v6.durationMinutes < 1 || !text(v6.description, 280) || !text(v6.note, AGENT_OUTCOME_MAX_LENGTH) || /[\r\n]/.test(v6.note) || v6.artifactUrl !== void 0 && (!text(v6.artifactUrl, 2048) || !isHttpEvidenceUrl(v6.artifactUrl)))
|
|
4105
|
-
return false;
|
|
4106
|
-
const details = v6.details === void 0 ? [] : v6.details;
|
|
4107
|
-
if (!Array.isArray(details) || !details.every((d5) => text(d5, AGENT_DETAIL_MAX_LENGTH) && !/[\r\n]/.test(d5)) || agentProofIssues({ outcome: v6.note, details }).length)
|
|
4108
|
-
return false;
|
|
4109
|
-
const source = v6.source;
|
|
4110
|
-
return shape(source, ["kind", "externalId", "label"]) && member(source.kind, SOURCE_KINDS) && optionalText(source.externalId, 200) && optionalText(source.label, 160) && Array.isArray(v6.evidence) && v6.evidence.length <= 20 && v6.evidence.every(
|
|
4111
|
-
(e4) => shape(e4, ["kind", "label", "externalId", "url"]) && member(e4.kind, evidenceKinds) && text(e4.label, 160) && optionalText(e4.externalId, 200) && (e4.url === null || e4.url === void 0 || text(e4.url, 500) && isHttpEvidenceUrl(e4.url))
|
|
4112
|
-
);
|
|
4113
4263
|
}
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
var outboxDir = () => join7(accountDirectory(), "pending-stages");
|
|
4119
|
-
var takeStageLock = () => tryLock(join7(accountDirectory(), "stage-writers"));
|
|
4120
|
-
var fileOf2 = (id) => join7(outboxDir(), id + ".json");
|
|
4121
|
-
var nextAttempt = (attempts, now) => new Date(now.getTime() + Math.min(MAX_BACKOFF_MINUTES, 2 ** Math.max(0, attempts - 1)) * 6e4).toISOString();
|
|
4122
|
-
var errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
4123
|
-
var classifyStageFailure = (error) => {
|
|
4124
|
-
if (!(error instanceof BridgeError)) return { kind: "unexpected", reason: errorText(error) };
|
|
4125
|
-
const { kind, status } = error.context;
|
|
4126
|
-
if (kind === "transport") return { kind: "retry", reason: "The Flowmind API could not be reached in time" };
|
|
4127
|
-
if (kind === "contract")
|
|
4128
|
-
return {
|
|
4129
|
-
kind: "retry",
|
|
4130
|
-
reason: "Flowmind's answer could not be read" + (status === null ? "" : " (HTTP " + status + ")")
|
|
4131
|
-
};
|
|
4132
|
-
const message = error instanceof ApiRejectionError ? error.rejectionMessage : "The Flowmind API answered HTTP " + status;
|
|
4133
|
-
if (status === 401 || status === 403) return { kind: "login", reason: message };
|
|
4134
|
-
if (status === 409 && message === STAGE_KEY_IN_USE) return { kind: "conflict", reason: message };
|
|
4135
|
-
if (status !== null && status >= 400 && status < 500 && status !== 408 && status !== 429)
|
|
4136
|
-
return { kind: "reject", reason: message };
|
|
4137
|
-
return { kind: "retry", reason: message };
|
|
4138
|
-
};
|
|
4139
|
-
var stageIdOf = (idempotencyKey, input2) => createHash4("sha256").update(idempotencyKey + "\n" + JSON.stringify(input2)).digest("hex").slice(0, 16);
|
|
4140
|
-
var cutDigestOf = (id, sessionId, cut) => createHash4("sha256").update(JSON.stringify([id, sessionId, cut === null ? null : [cut.at, cut.up]])).digest("hex");
|
|
4141
|
-
function readStage(value, fileName, legacy = false) {
|
|
4142
|
-
const stage = value;
|
|
4143
|
-
if (typeof stage !== "object" || stage === null) return null;
|
|
4144
|
-
const input2 = stage.input;
|
|
4145
|
-
const { id, sessionId, cut } = stage;
|
|
4146
|
-
const valid = typeof id === "string" && /^[a-f0-9]{16}$/.test(id) && fileName === id + ".json" && typeof stage.idempotencyKey === "string" && stage.idempotencyKey.trim() === stage.idempotencyKey && /^[\x20-\x7e]+$/.test(stage.idempotencyKey) && typeof stage.apiUrl === "string" && isStoredStageInput(input2) && (legacy && stage.credentialFingerprint === void 0 || typeof stage.credentialFingerprint === "string" && /^[a-f0-9]{64}$/.test(stage.credentialFingerprint)) && stageIdOf(stage.idempotencyKey, input2) === id && isoDate(stage.savedAt) && isoDate(stage.nextAttemptAt) && (stage.lastAttemptAt === null || isoDate(stage.lastAttemptAt)) && (sessionId === null || typeof sessionId === "string" && sessionId.length > 0) && (cut === null || typeof cut === "object" && isoDate(cut.at) && Date.parse(cut.at) <= Date.parse(stage.savedAt) && (cut.up === null || Number.isFinite(cut.up))) && sessionId === null === (cut === null) && (stage.state === "waiting" || stage.state === "rejected" || stage.state === "conflict") && Number.isSafeInteger(stage.attempts) && stage.attempts >= 0 && (typeof stage.lastError === "string" || stage.state === "waiting" && stage.lastError === null);
|
|
4147
|
-
if (!valid) return null;
|
|
4148
|
-
const cutDigest = cutDigestOf(id, sessionId, cut);
|
|
4149
|
-
if (stage.cutDigest !== cutDigest && !(legacy && stage.cutDigest === void 0)) return null;
|
|
4150
|
-
return { ...stage, credentialFingerprint: stage.credentialFingerprint ?? null, cutDigest };
|
|
4264
|
+
function turnIdTime(turnId) {
|
|
4265
|
+
const hex = turnId.replaceAll("-", "");
|
|
4266
|
+
if (!/^[0-9a-f]{32}$/i.test(hex) || hex[12] !== "7") return null;
|
|
4267
|
+
return Number.parseInt(hex.slice(0, 12), 16);
|
|
4151
4268
|
}
|
|
4152
|
-
var
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
await writeFile4(temporary, JSON.stringify(stage, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
4156
|
-
await rename2(temporary, fileOf2(stage.id));
|
|
4269
|
+
var firstRecordedStart = (events, turnId) => {
|
|
4270
|
+
const first = events.find((event) => event.event === "start" && event.turnId === turnId);
|
|
4271
|
+
return first ? Date.parse(first.at) : null;
|
|
4157
4272
|
};
|
|
4158
|
-
|
|
4159
|
-
const
|
|
4160
|
-
const
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
cutDigest: cutDigestOf(id, input2.sessionId, input2.cut),
|
|
4171
|
-
state: "waiting",
|
|
4172
|
-
attempts: 0,
|
|
4173
|
-
lastAttemptAt: null,
|
|
4174
|
-
nextAttemptAt: at2,
|
|
4175
|
-
lastError: null
|
|
4176
|
-
};
|
|
4177
|
-
if (!readStage(pending, pending.id + ".json"))
|
|
4178
|
-
throw new Error("The stage request does not match the Flowmind proof contract. Check its fields before retrying.");
|
|
4179
|
-
const { stages, unreadable } = await listPendingStages();
|
|
4180
|
-
if (unreadable.includes(fileOf2(pending.id)) || stages.some((stage) => stage.id === pending.id)) {
|
|
4181
|
-
throw new Error("This proof (" + pending.id + ") is already saved. See: flowmind agent pending");
|
|
4182
|
-
}
|
|
4183
|
-
await writePendingStage(pending);
|
|
4184
|
-
return pending;
|
|
4273
|
+
function hooksSawStart(events, turnId) {
|
|
4274
|
+
const first = firstRecordedStart(events, turnId);
|
|
4275
|
+
const created = turnIdTime(turnId);
|
|
4276
|
+
return first !== null && created !== null && first - created <= MISSED_START_MS;
|
|
4277
|
+
}
|
|
4278
|
+
var transcriptPathOf = (events) => events.filter((event) => event.event === "session").filter((event) => event.transcriptPath !== void 0).at(-1)?.transcriptPath ?? null;
|
|
4279
|
+
async function codexTurnsOf(sessionId, knownPath) {
|
|
4280
|
+
const known = knownPath === null ? null : await readCodexTurns(knownPath);
|
|
4281
|
+
if (known !== null) return { path: knownPath, turns: known };
|
|
4282
|
+
const found = await findCodexSessionFile(sessionId);
|
|
4283
|
+
const turns = found === null ? null : await readCodexTurns(found);
|
|
4284
|
+
return turns === null ? null : { path: found, turns };
|
|
4185
4285
|
}
|
|
4186
|
-
|
|
4187
|
-
const
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4286
|
+
function countBack(wall, events, now, lastWake) {
|
|
4287
|
+
const nowWall = now.at.getTime();
|
|
4288
|
+
if (wall > nowWall) return null;
|
|
4289
|
+
const back = { wall, up: now.up - (nowWall - wall) };
|
|
4290
|
+
if (lastWake !== null && lastWake <= nowWall) return lastWake <= wall ? back : null;
|
|
4291
|
+
const before = awakeSamples(events).filter((point) => point.wall <= wall).at(-1);
|
|
4292
|
+
return before && Math.abs(nowWall - now.up - (before.wall - before.up)) <= CLOCK_DRIFT_MS ? back : null;
|
|
4293
|
+
}
|
|
4294
|
+
var awakeSamples = (events) => events.flatMap((event) => event.event !== "unlinked-reminder" && event.up !== void 0 ? [pointOf(event.at, event.up)] : []);
|
|
4295
|
+
function awakePointAt(wall, events, now, lastWake) {
|
|
4296
|
+
const exact = countBack(wall, events, now, lastWake);
|
|
4297
|
+
if (exact) return exact;
|
|
4298
|
+
const nowWall = now.at.getTime();
|
|
4299
|
+
if (lastWake !== null && lastWake <= nowWall) return countBack(lastWake, events, now, lastWake);
|
|
4300
|
+
return awakeSamples(events).find((point) => point.wall > wall && point.wall < nowWall) ?? null;
|
|
4301
|
+
}
|
|
4302
|
+
function missedStartEvent(events, turn, owner, now, lastWake) {
|
|
4303
|
+
const first = firstRecordedStart(events, turn.turnId);
|
|
4304
|
+
if (first !== null && first - turn.startedAt <= MISSED_START_MS) return null;
|
|
4305
|
+
const point = awakePointAt(turn.startedAt, events, now, lastWake);
|
|
4306
|
+
if (point === null) return null;
|
|
4307
|
+
return {
|
|
4308
|
+
event: "start",
|
|
4309
|
+
at: new Date(point.wall).toISOString(),
|
|
4310
|
+
...point.up === null ? {} : { up: point.up },
|
|
4311
|
+
sessionId: owner.sessionId,
|
|
4312
|
+
agent: "codex",
|
|
4313
|
+
cwd: owner.cwd,
|
|
4314
|
+
turnId: turn.turnId,
|
|
4315
|
+
source: "session-file"
|
|
4195
4316
|
};
|
|
4196
|
-
await writePendingStage(next);
|
|
4197
|
-
return next;
|
|
4198
4317
|
}
|
|
4199
|
-
async function
|
|
4200
|
-
|
|
4318
|
+
async function addMissedTurnStart(input2, now) {
|
|
4319
|
+
const turnId = input2.turnId;
|
|
4320
|
+
const events = await readEventsAt(sessionFile(input2.sessionId)) ?? [];
|
|
4321
|
+
if (hooksSawStart(events, turnId)) return;
|
|
4322
|
+
const file = await codexTurnsOf(input2.sessionId, input2.transcriptPath ?? transcriptPathOf(events));
|
|
4323
|
+
if (file === null) return;
|
|
4324
|
+
const turn = file.turns.find((candidate) => candidate.turnId === turnId);
|
|
4325
|
+
if (!turn) throw new Error(`The Codex session file ${file.path} has no start for turn ${turnId}.`);
|
|
4326
|
+
const start = missedStartEvent(events, turn, input2, now, lastWakeMs());
|
|
4327
|
+
if (start) await append(input2.sessionId, start);
|
|
4201
4328
|
}
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4329
|
+
var pointOf = (at2, up) => ({ wall: Date.parse(at2), up: up ?? null });
|
|
4330
|
+
var cutOf = (event) => pointOf(event.cutAt, event.cutUp);
|
|
4331
|
+
function span(from, to) {
|
|
4332
|
+
const wall = to.wall - from.wall;
|
|
4333
|
+
if (wall <= 0 || from.up !== null && to.up !== null && to.up < from.up) return { from, to, ms: 0, awake: false };
|
|
4334
|
+
const ms = from.up === null || to.up === null ? wall : Math.min(to.up - from.up, wall);
|
|
4335
|
+
return { from, to, ms: ms > OPEN_TURN_LIMIT_MS ? 0 : ms, awake: from.up !== null && to.up !== null };
|
|
4336
|
+
}
|
|
4337
|
+
var sameEpoch = (from, to) => from.up === null || to.up === null || to.up >= from.up;
|
|
4338
|
+
function supportedEdge(start, edge, end) {
|
|
4339
|
+
const follows = (a5, b4) => a5.up !== null && b4.up !== null && b4.up >= a5.up && b4.up - a5.up <= b4.wall - a5.wall + CLOCK_DRIFT_MS;
|
|
4340
|
+
return follows(start, edge) && follows(edge, end);
|
|
4341
|
+
}
|
|
4342
|
+
function ownedParts(start, end, cut, periods) {
|
|
4343
|
+
if (end.wall <= start.wall || end.wall - start.wall > OPEN_TURN_LIMIT_MS || !sameEpoch(start, end)) return [];
|
|
4344
|
+
return periods.flatMap((period) => {
|
|
4345
|
+
if (period.start.wall >= end.wall || period.end && period.end.wall <= start.wall) return [];
|
|
4346
|
+
let from = start, to = end;
|
|
4347
|
+
if (period.start.wall > start.wall) {
|
|
4348
|
+
if (!supportedEdge(start, period.start, end)) return [];
|
|
4349
|
+
from = period.start;
|
|
4221
4350
|
}
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
} catch {
|
|
4226
|
-
value = null;
|
|
4351
|
+
if (period.end && period.end.wall < end.wall) {
|
|
4352
|
+
if (!supportedEdge(start, period.end, end)) return [];
|
|
4353
|
+
to = period.end;
|
|
4227
4354
|
}
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
}
|
|
4232
|
-
return { stages: stages.sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt)), unreadable };
|
|
4355
|
+
if (cut && cut.wall > from.wall) from = cut;
|
|
4356
|
+
const part = span(from, to);
|
|
4357
|
+
return part.ms > 0 ? [part] : [];
|
|
4358
|
+
});
|
|
4233
4359
|
}
|
|
4234
|
-
|
|
4235
|
-
const
|
|
4236
|
-
if (
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4360
|
+
function measureRecordedTime(events, now, periods) {
|
|
4361
|
+
const first = events.find((event) => event.event === "session" || event.event === "start");
|
|
4362
|
+
if (!first || first.event !== "session" && first.event !== "start") return null;
|
|
4363
|
+
const nowPoint = { wall: now.at.getTime(), up: now.up };
|
|
4364
|
+
const turns = [];
|
|
4365
|
+
let open2 = null;
|
|
4366
|
+
let unknownStarts = 0;
|
|
4367
|
+
let lastStage = null;
|
|
4368
|
+
const settled = new Set(events.flatMap((event) => event.event === "staged" && event.state !== "waiting" ? [event.pendingStageId] : []));
|
|
4369
|
+
for (const event of events) {
|
|
4370
|
+
if (event.event === "session" || event.event === "unlinked-reminder") continue;
|
|
4371
|
+
const point = pointOf(event.at, event.up);
|
|
4372
|
+
switch (event.event) {
|
|
4373
|
+
case "start":
|
|
4374
|
+
if (event.turnId === null && (unknownStarts > 0 || open2 && open2.turnId === null)) {
|
|
4375
|
+
unknownStarts = unknownStarts ? unknownStarts + 1 : 2;
|
|
4376
|
+
open2 = null;
|
|
4377
|
+
break;
|
|
4378
|
+
}
|
|
4379
|
+
if (open2 && open2.turnId !== null && open2.turnId === event.turnId) {
|
|
4380
|
+
if (sameEpoch(open2.start, point)) open2.last = point;
|
|
4381
|
+
break;
|
|
4382
|
+
}
|
|
4383
|
+
if (open2) turns.push({ start: open2.start, end: open2.last });
|
|
4384
|
+
open2 = { start: point, last: point, turnId: event.turnId };
|
|
4385
|
+
break;
|
|
4386
|
+
case "stop": {
|
|
4387
|
+
if (event.turnId === null && unknownStarts > 0) {
|
|
4388
|
+
unknownStarts--;
|
|
4389
|
+
break;
|
|
4390
|
+
}
|
|
4391
|
+
const matches = open2 !== null && event.turnId === open2.turnId;
|
|
4392
|
+
if (open2 && matches) {
|
|
4393
|
+
turns.push({ start: open2.start, end: sameEpoch(open2.start, point) ? point : open2.last });
|
|
4394
|
+
open2 = null;
|
|
4395
|
+
}
|
|
4396
|
+
break;
|
|
4397
|
+
}
|
|
4398
|
+
case "staged":
|
|
4399
|
+
if (event.state === "waiting" && settled.has(event.pendingStageId)) break;
|
|
4400
|
+
if (!lastStage || cutOf(event).wall >= cutOf(lastStage).wall) lastStage = event;
|
|
4401
|
+
break;
|
|
4402
|
+
case "subagent":
|
|
4403
|
+
if (open2 && open2.turnId === event.turnId && sameEpoch(open2.start, point)) open2.last = point;
|
|
4404
|
+
break;
|
|
4405
|
+
}
|
|
4240
4406
|
}
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4407
|
+
if (open2) turns.push({ start: open2.start, end: open2.last });
|
|
4408
|
+
const stagedCut = lastStage ? cutOf(lastStage) : null;
|
|
4409
|
+
const counted = turns.flatMap(
|
|
4410
|
+
({ start, end }) => ownedParts(start, end.wall > nowPoint.wall ? nowPoint : end, stagedCut, periods)
|
|
4411
|
+
);
|
|
4412
|
+
const unstagedMs = counted.reduce((sum, part) => sum + part.ms, 0);
|
|
4413
|
+
const minutes = Math.floor(unstagedMs / 6e4);
|
|
4414
|
+
let cut = { at: now.at.toISOString(), up: now.up };
|
|
4415
|
+
let remaining = minutes * 6e4;
|
|
4416
|
+
for (const part of counted) {
|
|
4417
|
+
if (remaining > part.ms) {
|
|
4418
|
+
remaining -= part.ms;
|
|
4246
4419
|
continue;
|
|
4247
4420
|
}
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4421
|
+
cut = {
|
|
4422
|
+
at: new Date(part.from.wall + remaining).toISOString(),
|
|
4423
|
+
up: part.awake && part.from.up !== null ? part.from.up + remaining : null
|
|
4424
|
+
};
|
|
4425
|
+
break;
|
|
4251
4426
|
}
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
sent: [],
|
|
4263
|
-
rejected: [],
|
|
4264
|
-
conflicts: [],
|
|
4265
|
-
stoppedBy: null,
|
|
4266
|
-
waiting: 0,
|
|
4267
|
-
unreadable: []
|
|
4427
|
+
return {
|
|
4428
|
+
agent: first.agent,
|
|
4429
|
+
unstagedMs,
|
|
4430
|
+
minutes,
|
|
4431
|
+
startedAt: counted.length > 0 ? new Date(counted[0].from.wall).toISOString() : null,
|
|
4432
|
+
cut,
|
|
4433
|
+
lastProposalId: lastStage?.state === "accepted" ? lastStage.proposalId : null,
|
|
4434
|
+
lastPendingStageId: lastStage && lastStage.state !== "accepted" ? lastStage.pendingStageId : null,
|
|
4435
|
+
lastHold: lastStage?.state === "conflict" || lastStage?.state === "discarded" ? lastStage.reason : null,
|
|
4436
|
+
lastRejection: lastStage?.state === "rejected" ? lastStage.reason : null
|
|
4268
4437
|
};
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
await settleStageMark(
|
|
4302
|
-
stage.sessionId,
|
|
4303
|
-
{ at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
|
|
4304
|
-
{ state: "rejected", reason: failure.reason }
|
|
4305
|
-
);
|
|
4306
|
-
result.rejected.push({ id: stage.id, reason: failure.reason });
|
|
4307
|
-
continue;
|
|
4438
|
+
}
|
|
4439
|
+
async function recordedTime(sessionId, now) {
|
|
4440
|
+
const events = await readEventsAt(sessionFile(sessionId));
|
|
4441
|
+
return events ? measureRecordedTime(events, now, signedInPeriods()) : null;
|
|
4442
|
+
}
|
|
4443
|
+
function missedTurnEvents(events, turns, owner, now, lastWake) {
|
|
4444
|
+
const added = [];
|
|
4445
|
+
for (const turn of turns) {
|
|
4446
|
+
const first = firstRecordedStart(events, turn.turnId);
|
|
4447
|
+
if (first !== null && first - turn.startedAt <= MISSED_START_MS) continue;
|
|
4448
|
+
if (turn.endedAt === null) {
|
|
4449
|
+
const start = missedStartEvent(events, turn, owner, now, lastWake);
|
|
4450
|
+
if (start) added.push(start);
|
|
4451
|
+
continue;
|
|
4452
|
+
}
|
|
4453
|
+
if (turn.durationMs === null) continue;
|
|
4454
|
+
const stop = events.find(
|
|
4455
|
+
(event) => event.event === "stop" && event.turnId === turn.turnId
|
|
4456
|
+
);
|
|
4457
|
+
let end;
|
|
4458
|
+
let awakeToEnd;
|
|
4459
|
+
if (stop?.up !== void 0) {
|
|
4460
|
+
end = pointOf(stop.at, stop.up);
|
|
4461
|
+
awakeToEnd = turn.durationMs - Math.max(0, turn.endedAt - end.wall);
|
|
4462
|
+
} else {
|
|
4463
|
+
end = countBack(turn.endedAt, events, now, lastWake);
|
|
4464
|
+
awakeToEnd = turn.durationMs;
|
|
4465
|
+
if (end === null) {
|
|
4466
|
+
const sample = awakeSamples(events).filter((point) => point.wall > turn.startedAt && point.wall <= turn.endedAt).at(-1);
|
|
4467
|
+
if (sample) {
|
|
4468
|
+
end = sample;
|
|
4469
|
+
awakeToEnd = turn.durationMs - (turn.endedAt - sample.wall);
|
|
4308
4470
|
}
|
|
4309
|
-
result.stoppedBy = failure.reason;
|
|
4310
|
-
break;
|
|
4311
4471
|
}
|
|
4312
|
-
if (
|
|
4313
|
-
await settleStageMark(
|
|
4314
|
-
stage.sessionId,
|
|
4315
|
-
{ at: stage.savedAt, cut: stage.cut, pendingStageId: stage.id },
|
|
4316
|
-
{ state: "accepted", proposalId: proposal.id }
|
|
4317
|
-
);
|
|
4318
|
-
await forgetStage(stage.id);
|
|
4319
|
-
result.sent.push({ id: stage.id, proposal });
|
|
4472
|
+
if (end) added.push({ event: "stop", at: new Date(end.wall).toISOString(), up: end.up, turnId: turn.turnId });
|
|
4320
4473
|
}
|
|
4321
|
-
|
|
4322
|
-
|
|
4474
|
+
if (end === null || awakeToEnd <= 0) continue;
|
|
4475
|
+
added.push({
|
|
4476
|
+
event: "start",
|
|
4477
|
+
at: new Date(turn.startedAt).toISOString(),
|
|
4478
|
+
up: end.up - awakeToEnd,
|
|
4479
|
+
sessionId: owner.sessionId,
|
|
4480
|
+
agent: "codex",
|
|
4481
|
+
cwd: owner.cwd,
|
|
4482
|
+
turnId: turn.turnId,
|
|
4483
|
+
source: "session-file"
|
|
4484
|
+
});
|
|
4323
4485
|
}
|
|
4324
|
-
return
|
|
4486
|
+
return added;
|
|
4325
4487
|
}
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
if (!
|
|
4329
|
-
const
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
if (
|
|
4333
|
-
|
|
4334
|
-
if (
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
{ at: stage.savedAt, cut: stage.cut, pendingStageId: id },
|
|
4338
|
-
{ state: "discarded", reason: "Discarded after checking Activity." }
|
|
4339
|
-
);
|
|
4340
|
-
await forgetStage(id);
|
|
4341
|
-
return true;
|
|
4488
|
+
async function stageTime(sessionId, now) {
|
|
4489
|
+
const events = await readEventsAt(sessionFile(sessionId));
|
|
4490
|
+
if (!events) return null;
|
|
4491
|
+
const owner = events.find(
|
|
4492
|
+
(event) => event.event === "session" || event.event === "start"
|
|
4493
|
+
);
|
|
4494
|
+
if (owner?.agent !== "codex") return measureRecordedTime(events, now, signedInPeriods());
|
|
4495
|
+
const file = await codexTurnsOf(sessionId, transcriptPathOf(events));
|
|
4496
|
+
if (file === null) return measureRecordedTime(events, now, signedInPeriods());
|
|
4497
|
+
const added = missedTurnEvents(events, file.turns, owner, now, lastWakeMs());
|
|
4498
|
+
return measureRecordedTime(byTime([...events, ...added]), now, signedInPeriods());
|
|
4342
4499
|
}
|
|
4343
|
-
async function
|
|
4344
|
-
const
|
|
4345
|
-
if (
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4500
|
+
async function promptReminder(sessionId, cwd, now) {
|
|
4501
|
+
const link2 = await readLink(cwd);
|
|
4502
|
+
if (link2?.kind === "ignored") return "";
|
|
4503
|
+
if (link2?.kind === "linked") {
|
|
4504
|
+
const recorded2 = await recordedTime(sessionId, now);
|
|
4505
|
+
return recorded2 && recorded2.minutes >= REMINDER_MINUTES ? unstagedReminder(link2.name, recorded2.minutes) : "";
|
|
4506
|
+
}
|
|
4507
|
+
const root = await findGitRoot(cwd);
|
|
4508
|
+
if (root === null) return "";
|
|
4509
|
+
const events = await readEventsAt(sessionFile(sessionId)) ?? [];
|
|
4510
|
+
if (events.some((event) => event.event === "unlinked-reminder")) return "";
|
|
4511
|
+
const recorded = measureRecordedTime(events, now, signedInPeriods());
|
|
4512
|
+
if (!recorded || recorded.minutes < REMINDER_MINUTES) return "";
|
|
4513
|
+
const repository = await remoteStatus(root);
|
|
4514
|
+
if (repository.status !== "remote-linked" && repository.status !== "unlinked") return "";
|
|
4515
|
+
await append(sessionId, { event: "unlinked-reminder", at: now.at.toISOString() });
|
|
4516
|
+
return repository.status === "remote-linked" ? remoteLinkedReminder(repository.link, recorded.minutes) : unlinkedReminder(recorded.minutes);
|
|
4517
|
+
}
|
|
4518
|
+
async function restoreStageMark(sessionId, mark) {
|
|
4519
|
+
const events = await readEventsAt(sessionFile(sessionId));
|
|
4520
|
+
const existing = events?.filter((event) => event.event === "staged" && event.pendingStageId === mark.pendingStageId);
|
|
4521
|
+
if (existing?.some((event) => event.event === "staged" && event.state === "accepted")) return "accepted";
|
|
4522
|
+
if (existing?.length) return "pending";
|
|
4523
|
+
await append(sessionId, {
|
|
4524
|
+
event: "staged",
|
|
4525
|
+
state: "waiting",
|
|
4526
|
+
at: mark.at,
|
|
4527
|
+
pendingStageId: mark.pendingStageId,
|
|
4528
|
+
cutAt: mark.cut.at,
|
|
4529
|
+
...mark.cut.up === null ? {} : { cutUp: mark.cut.up }
|
|
4352
4530
|
});
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4531
|
+
return "pending";
|
|
4532
|
+
}
|
|
4533
|
+
async function settleStageMark(sessionId, mark, outcome) {
|
|
4534
|
+
const events = await readEventsAt(sessionFile(sessionId));
|
|
4535
|
+
if (events?.some((e4) => e4.event === "staged" && e4.pendingStageId === mark.pendingStageId && (outcome.state === "accepted" ? e4.state === "accepted" && e4.proposalId === outcome.proposalId : e4.state === outcome.state && e4.reason === outcome.reason))) return;
|
|
4536
|
+
await append(sessionId, {
|
|
4537
|
+
event: "staged",
|
|
4538
|
+
at: mark.at,
|
|
4539
|
+
pendingStageId: mark.pendingStageId,
|
|
4540
|
+
...outcome,
|
|
4541
|
+
cutAt: mark.cut.at,
|
|
4542
|
+
...mark.cut.up === null ? {} : { cutUp: mark.cut.up }
|
|
4356
4543
|
});
|
|
4357
|
-
child.unref();
|
|
4358
|
-
return true;
|
|
4359
4544
|
}
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4545
|
+
async function resolveSession(explicit, cwd, now = /* @__PURE__ */ new Date()) {
|
|
4546
|
+
if (explicit) return { sessionId: explicit, source: "flag" };
|
|
4547
|
+
const codexThread = process.env.CODEX_THREAD_ID || void 0;
|
|
4548
|
+
if (codexThread && await readEventsAt(sessionFile(codexThread))) return { sessionId: codexThread, source: "codex-env" };
|
|
4549
|
+
const root = await findRepoRoot(cwd);
|
|
4550
|
+
const active = /* @__PURE__ */ new Set();
|
|
4551
|
+
let names = [];
|
|
4364
4552
|
try {
|
|
4365
|
-
names = await
|
|
4553
|
+
names = await readdir7(sessionsDir());
|
|
4366
4554
|
} catch (error) {
|
|
4367
|
-
if (error.code
|
|
4368
|
-
throw error;
|
|
4555
|
+
if (error.code !== "ENOENT") throw error;
|
|
4369
4556
|
}
|
|
4370
|
-
const
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
const
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
if (
|
|
4379
|
-
if (!(error instanceof SyntaxError)) throw error;
|
|
4557
|
+
for (const name of journalNames(names)) {
|
|
4558
|
+
const path = join8(sessionsDir(), name);
|
|
4559
|
+
if (now.getTime() - await lastWritten(path) > ACTIVE_WINDOW_MS) continue;
|
|
4560
|
+
const events = await readEventsAt(path) ?? [];
|
|
4561
|
+
if (codexThread && events.some((event) => event.event === "subagent" && event.agentId === codexThread)) {
|
|
4562
|
+
const main = events.find(
|
|
4563
|
+
(event) => event.event === "session" || event.event === "start"
|
|
4564
|
+
);
|
|
4565
|
+
if (main) return { sessionId: main.sessionId, source: "subagent" };
|
|
4380
4566
|
}
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
return { stages, unreadable };
|
|
4386
|
-
}
|
|
4387
|
-
async function moveIntoAccount(stage) {
|
|
4388
|
-
await mkdir5(outboxDir(), { recursive: true, mode: 448 });
|
|
4389
|
-
const target = join8(outboxDir(), stage.id + ".json");
|
|
4390
|
-
const present = await stat3(target).then(
|
|
4391
|
-
() => true,
|
|
4392
|
-
(error) => {
|
|
4393
|
-
if (error.code === "ENOENT") return false;
|
|
4394
|
-
throw error;
|
|
4567
|
+
let last = null;
|
|
4568
|
+
for (const event of events) {
|
|
4569
|
+
if (event.event === "start") last = event;
|
|
4570
|
+
if (event.event === "stop" && event.turnId === last?.turnId) last = null;
|
|
4395
4571
|
}
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
const
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
projectId: stage.input.projectId,
|
|
4407
|
-
durationMinutes: stage.input.durationMinutes,
|
|
4408
|
-
savedAt: stage.savedAt,
|
|
4409
|
-
apiUrl: stage.apiUrl
|
|
4410
|
-
})).sort((a5, b4) => Date.parse(a5.savedAt) - Date.parse(b4.savedAt));
|
|
4411
|
-
return { unowned, unreadable };
|
|
4412
|
-
}
|
|
4413
|
-
async function findUnowned(id) {
|
|
4414
|
-
if (!/^[a-f0-9]{16}$/.test(id)) throw new Error("No unowned saved proof " + id + ". See: flowmind agent pending");
|
|
4415
|
-
const stage = (await legacyStages()).stages.find((candidate) => candidate.id === id);
|
|
4416
|
-
if (!stage) throw new Error("No unowned saved proof " + id + ". See: flowmind agent pending");
|
|
4417
|
-
return stage;
|
|
4418
|
-
}
|
|
4419
|
-
async function adoptUnownedProof(id) {
|
|
4420
|
-
const stage = await findUnowned(id);
|
|
4421
|
-
if (stage.apiUrl !== API_URL)
|
|
4422
|
-
throw new Error("Saved proof " + id + " belongs to " + stage.apiUrl + ", not " + API_URL + ".");
|
|
4423
|
-
await moveIntoAccount(stage);
|
|
4424
|
-
}
|
|
4425
|
-
async function discardUnownedProof(id) {
|
|
4426
|
-
await findUnowned(id);
|
|
4427
|
-
await rm3(join8(legacyOutboxDir(), id + ".json"));
|
|
4572
|
+
if (last?.event === "start" && now.getTime() - Date.parse(last.at) < OPEN_TURN_LIMIT_MS && await findRepoRoot(last.cwd) === root) {
|
|
4573
|
+
active.add(last.sessionId);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
if (active.size > 1) {
|
|
4577
|
+
throw new Error("Several agent sessions have a turn in progress in this repository. Pass --session-id.");
|
|
4578
|
+
}
|
|
4579
|
+
const [only] = active;
|
|
4580
|
+
if (only) return { sessionId: only, source: "active-turn" };
|
|
4581
|
+
return codexThread ? { sessionId: codexThread, source: "codex-env" } : { sessionId: null, source: null };
|
|
4428
4582
|
}
|
|
4429
4583
|
|
|
4584
|
+
// src/command-error.ts
|
|
4585
|
+
var LIMIT_WORDS = {
|
|
4586
|
+
"projects.active": (limit) => `${limit} active projects`,
|
|
4587
|
+
"logs.personal.month": (limit) => `${limit} personal logs this month`,
|
|
4588
|
+
"logs.agent.month": (limit) => `${limit} agent logs this month`
|
|
4589
|
+
};
|
|
4590
|
+
var describePlanLimit = ({ limit, limitKey, resetAt }) => {
|
|
4591
|
+
const what = (LIMIT_WORDS[limitKey] ?? ((count) => `${count} of ${limitKey}`))(limit);
|
|
4592
|
+
const frees = resetAt === null ? "Archiving a finished project frees a slot (archiving is final), or upgrade to Pro." : `It resets on ${new Date(resetAt).toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" })}, or upgrade to Pro.`;
|
|
4593
|
+
return `Your plan allows ${what}. ${frees} Everything you already logged stays. Upgrade in the Flowmind web app under Settings > Plan.`;
|
|
4594
|
+
};
|
|
4595
|
+
var printCommandError = (action, error, json = false, extra) => {
|
|
4596
|
+
let diagnostic;
|
|
4597
|
+
if (error instanceof PlanLimitError) {
|
|
4598
|
+
const message = describePlanLimit(error.details);
|
|
4599
|
+
console.error(json ? JSON.stringify({ error: message, code: error.code, ...error.details, ...extra }) : `${terminalText(message)}${extra ? `
|
|
4600
|
+
${terminalText(extra.message)}` : ""}`);
|
|
4601
|
+
process.exitCode = 1;
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
if (error instanceof BridgeError) {
|
|
4605
|
+
const context2 = error.context;
|
|
4606
|
+
const schema = `${context2.schemaId ?? "unavailable"}${context2.schemaPath ?? ""}`;
|
|
4607
|
+
diagnostic = `${action} failed: ${error.code} (${context2.kind}); operation=${context2.operation ?? "unavailable"}; stage=${context2.stage ?? "request"}; schema=${schema}; requestId=${context2.requestId ?? "unavailable"}`;
|
|
4608
|
+
} else {
|
|
4609
|
+
diagnostic = `${action} failed: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
4610
|
+
}
|
|
4611
|
+
if (error instanceof ApiRejectionError) {
|
|
4612
|
+
console.error(json ? JSON.stringify({ error: error.rejectionMessage, diagnostic, ...extra }) : `${terminalText(error.rejectionMessage)}
|
|
4613
|
+
Error: ${terminalText(diagnostic)}${extra ? `
|
|
4614
|
+
${terminalText(extra.message)}` : ""}`);
|
|
4615
|
+
} else {
|
|
4616
|
+
console.error(json ? JSON.stringify({ error: diagnostic, ...extra }) : `Error: ${terminalText(diagnostic)}${extra ? `
|
|
4617
|
+
${terminalText(extra.message)}` : ""}`);
|
|
4618
|
+
}
|
|
4619
|
+
process.exitCode = 1;
|
|
4620
|
+
};
|
|
4621
|
+
|
|
4430
4622
|
// src/commands/agent-stage.ts
|
|
4431
4623
|
import { createHash as createHash5 } from "node:crypto";
|
|
4432
4624
|
var trimRequired = (value, field) => {
|
|
@@ -4612,7 +4804,7 @@ async function resolveStageOptions(options, cwd, now) {
|
|
|
4612
4804
|
recorded = session.sessionId ? await stageTime(session.sessionId, now) : null;
|
|
4613
4805
|
if (!recorded) {
|
|
4614
4806
|
throw new Error(
|
|
4615
|
-
"No recorded agent time for this session.
|
|
4807
|
+
"No recorded agent time for this session. If the session hooks are not installed, run `flowmind hooks install`; for this session, pass --minutes and --started-at."
|
|
4616
4808
|
);
|
|
4617
4809
|
}
|
|
4618
4810
|
if (recorded.minutes < 1 || recorded.startedAt === null) {
|
|
@@ -4785,6 +4977,7 @@ agentCommand.command("stage").description("Stage completed agent work as a proje
|
|
|
4785
4977
|
await ensureAccount();
|
|
4786
4978
|
lock = await takeStageLock();
|
|
4787
4979
|
if (!lock) throw new Error("Another Flowmind process is staging or sending proofs. Try again when it finishes.");
|
|
4980
|
+
await adoptLegacyStateHeld();
|
|
4788
4981
|
await restoreSavedStageMarks(options.minutes === void 0 && options.startedAt === void 0);
|
|
4789
4982
|
const resolved = await resolveStageOptions({ ...stageOptions, detail }, process.cwd(), now);
|
|
4790
4983
|
if (resolved.kind === "skip") {
|
|
@@ -4961,7 +5154,16 @@ agentCommand.command("hook").description('Record agent session time from Codex o
|
|
|
4961
5154
|
try {
|
|
4962
5155
|
if (!getApiKey()) return;
|
|
4963
5156
|
await ensureAccount();
|
|
4964
|
-
const
|
|
5157
|
+
const pruneWhenFree = async (at2) => {
|
|
5158
|
+
const pruning = await takeStageLock();
|
|
5159
|
+
if (!pruning) return;
|
|
5160
|
+
try {
|
|
5161
|
+
await pruneOldSessions(at2);
|
|
5162
|
+
} finally {
|
|
5163
|
+
await pruning.release();
|
|
5164
|
+
}
|
|
5165
|
+
};
|
|
5166
|
+
const context2 = await handleHook(options.agent, await readStdin(), pruneWhenFree);
|
|
4965
5167
|
if (context2) process.stdout.write(`${context2}
|
|
4966
5168
|
`);
|
|
4967
5169
|
await startDueRetry(/* @__PURE__ */ new Date());
|
|
@@ -5219,9 +5421,28 @@ var reviewNaturalLog = async (preview) => {
|
|
|
5219
5421
|
|
|
5220
5422
|
// src/commands/log.ts
|
|
5221
5423
|
var FLOW_SESSION_MIN_HOURS = 1.5;
|
|
5424
|
+
var HOUR_UNIT = "(?:h|hr|hrs|hour|hours)";
|
|
5425
|
+
var MINUTE_UNIT = "(?:m|min|mins|minute|minutes)";
|
|
5426
|
+
var HOURS = new RegExp("^(\\d+(?:[.,]\\d+)?|[.,]\\d+)(?:\\s*" + HOUR_UNIT + ")?$");
|
|
5427
|
+
var MINUTES = new RegExp("^(\\d+)\\s*" + MINUTE_UNIT + "$");
|
|
5428
|
+
var HOURS_AND_MINUTES = new RegExp("^(\\d+)\\s*" + HOUR_UNIT + "\\s*(\\d+)(?:\\s*" + MINUTE_UNIT + ")?$");
|
|
5429
|
+
function parseHours(value) {
|
|
5430
|
+
const text2 = value.trim().toLowerCase();
|
|
5431
|
+
let match;
|
|
5432
|
+
let minutes = Number.NaN;
|
|
5433
|
+
if (match = HOURS.exec(text2)) minutes = Number(match[1].replace(",", ".")) * 60;
|
|
5434
|
+
else if (match = MINUTES.exec(text2)) minutes = Number(match[1]);
|
|
5435
|
+
else if (match = HOURS_AND_MINUTES.exec(text2)) minutes = Number(match[1]) * 60 + Number(match[2]);
|
|
5436
|
+
const whole = Math.round(minutes);
|
|
5437
|
+
if (!Number.isSafeInteger(whole) || whole < 1) {
|
|
5438
|
+
throw new InvalidArgumentError2("Give the time as hours or minutes, for example 1.5, 1,5, 2h, 90m or 1h30m.");
|
|
5439
|
+
}
|
|
5440
|
+
return whole / 60;
|
|
5441
|
+
}
|
|
5442
|
+
var duration = (minutes) => [minutes >= 60 ? Math.floor(minutes / 60) + "h" : "", minutes % 60 ? minutes % 60 + "m" : ""].filter(Boolean).join(" ");
|
|
5222
5443
|
function printCreated(result) {
|
|
5223
5444
|
console.log(chalk3.green(`
|
|
5224
|
-
\u2713 Logged ${result.hours}
|
|
5445
|
+
\u2713 Logged ${duration(Math.round(result.hours * 60))} successfully.`));
|
|
5225
5446
|
if (result.xpAwarded !== null) {
|
|
5226
5447
|
console.log(chalk3.green(` +${result.xpAwarded} XP earned`));
|
|
5227
5448
|
}
|
|
@@ -5236,16 +5457,7 @@ function printCreatedTimeEntry(result) {
|
|
|
5236
5457
|
isFlowSession: result.isFlowSession
|
|
5237
5458
|
});
|
|
5238
5459
|
}
|
|
5239
|
-
var logCommand = new Command3("log").description("Log time with natural language or explicit project/hours").helpOption("-H, --help", "display help for command").argument("[text]", 'Natural language input, e.g. "worked 2.5h on API"').option("-p, --project <project>", "Project name").option("-h, --hours <
|
|
5240
|
-
if (!/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(value) || !Number.isFinite(Number(value)) || Number(value) <= 0) {
|
|
5241
|
-
throw new InvalidArgumentError2("Hours must be a positive decimal number, for example 1.5.");
|
|
5242
|
-
}
|
|
5243
|
-
const fraction = (value.split(".")[1] ?? "").replace(/0+$/, "");
|
|
5244
|
-
if (fraction.length > 2 || Number(fraction.padEnd(2, "0")) % 5 !== 0) {
|
|
5245
|
-
throw new InvalidArgumentError2("Hours must resolve to whole minutes.");
|
|
5246
|
-
}
|
|
5247
|
-
return Number(value);
|
|
5248
|
-
}).option("--started-at <ISO 8601 date-time>", "Session start with timezone; defaults to save time minus duration").option("-i, --intention <text>", "What the session intended to complete").option("-o, --outcome <text>", "What changed, shipped, or became clear").option("--next <text>", "Private next step saved with this session").option("-a, --artifact-url <url>", "Commit, document, design, or published artifact URL").action(async (text2, options) => {
|
|
5460
|
+
var logCommand = new Command3("log").description("Log time with natural language or explicit project/hours").helpOption("-H, --help", "display help for command").argument("[text]", 'Natural language input, e.g. "worked 2.5h on API"').option("-p, --project <project>", "Project name").option("-h, --hours <time>", "Time worked: 1.5, 1,5, 2h, 90m or 1h30m", parseHours).option("--started-at <ISO 8601 date-time>", "Session start with timezone; defaults to save time minus duration").option("-i, --intention <text>", "What the session intended to complete").option("-o, --outcome <text>", "What changed, shipped, or became clear").option("--next <text>", "Private next step saved with this session").option("-a, --artifact-url <url>", "Commit, document, design, or published artifact URL").action(async (text2, options) => {
|
|
5249
5461
|
const usesStructuredOptions = options.next !== void 0 || options.project !== void 0 || options.hours !== void 0 || options.intention !== void 0 || options.outcome !== void 0 || options.artifactUrl !== void 0 || options.startedAt !== void 0;
|
|
5250
5462
|
if (text2 && usesStructuredOptions) {
|
|
5251
5463
|
console.error(chalk3.red("Use either natural language input OR -p/-h options, not both in the same command."));
|
|
@@ -5305,7 +5517,7 @@ var logCommand = new Command3("log").description("Log time with natural language
|
|
|
5305
5517
|
// src/commands/hooks.ts
|
|
5306
5518
|
import { createHash as createHash6, randomUUID as randomUUID4 } from "node:crypto";
|
|
5307
5519
|
import { realpathSync } from "node:fs";
|
|
5308
|
-
import { readFile as readFile7, link, lstat, mkdir as mkdir6, rename as
|
|
5520
|
+
import { readFile as readFile7, link, lstat, mkdir as mkdir6, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
|
|
5309
5521
|
import { homedir as homedir2 } from "node:os";
|
|
5310
5522
|
import { dirname as dirname3, join as join9 } from "node:path";
|
|
5311
5523
|
import { fileURLToPath } from "node:url";
|
|
@@ -5508,7 +5720,7 @@ async function updateHookFile(agent, action, base) {
|
|
|
5508
5720
|
const changed = () => new Error(path + " changed while Flowmind was updating it. Nothing was written; run the command again.");
|
|
5509
5721
|
const removeFile = agent === "codex" && action === "uninstall" && Object.keys(next).length === 0;
|
|
5510
5722
|
const temporary = path + "." + randomUUID4() + ".tmp";
|
|
5511
|
-
if (!removeFile) await
|
|
5723
|
+
if (!removeFile) await writeFile6(temporary, serialize(next), { mode: info ? info.mode & 511 : 384, flag: "wx" });
|
|
5512
5724
|
try {
|
|
5513
5725
|
let onDisk;
|
|
5514
5726
|
try {
|
|
@@ -5526,7 +5738,7 @@ async function updateHookFile(agent, action, base) {
|
|
|
5526
5738
|
if (error.code === "EEXIST") throw changed();
|
|
5527
5739
|
throw error;
|
|
5528
5740
|
}
|
|
5529
|
-
} else await
|
|
5741
|
+
} else await rename5(temporary, path);
|
|
5530
5742
|
} finally {
|
|
5531
5743
|
await rm4(temporary, { force: true });
|
|
5532
5744
|
}
|
|
@@ -5568,10 +5780,10 @@ hooksCommand.command("install").description(
|
|
|
5568
5780
|
}
|
|
5569
5781
|
await mkdir6(dirname3(target.path), { recursive: true });
|
|
5570
5782
|
if (writeSkill) {
|
|
5571
|
-
await
|
|
5783
|
+
await writeFile6(target.path, content, { flag: "w", mode: 420 });
|
|
5572
5784
|
console.log(terminalLine`Wrote ${target.path}`);
|
|
5573
5785
|
}
|
|
5574
|
-
await
|
|
5786
|
+
await writeFile6(target.markerPath, `${nextHash}
|
|
5575
5787
|
`, { flag: "w", mode: 420 });
|
|
5576
5788
|
console.log(terminalLine`Wrote ${target.markerPath}`);
|
|
5577
5789
|
}
|
|
@@ -5678,7 +5890,7 @@ hooksCommand.command("uninstall").description("Remove unmodified Flowmind-manage
|
|
|
5678
5890
|
});
|
|
5679
5891
|
|
|
5680
5892
|
// src/commands/link.ts
|
|
5681
|
-
import { readdir as
|
|
5893
|
+
import { readdir as readdir8 } from "node:fs/promises";
|
|
5682
5894
|
import { join as join10, resolve as resolve2 } from "node:path";
|
|
5683
5895
|
import { Command as Command5, InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
5684
5896
|
import chalk4 from "chalk";
|
|
@@ -5696,7 +5908,7 @@ async function findRepositories(dir, depth, found) {
|
|
|
5696
5908
|
if (depth === 0) return;
|
|
5697
5909
|
let entries;
|
|
5698
5910
|
try {
|
|
5699
|
-
entries = await
|
|
5911
|
+
entries = await readdir8(dir, { withFileTypes: true });
|
|
5700
5912
|
} catch (error) {
|
|
5701
5913
|
const code = error.code;
|
|
5702
5914
|
if (code === "EACCES" || code === "EPERM") return;
|
|
@@ -7637,7 +7849,7 @@ for (const [name, description, done] of [
|
|
|
7637
7849
|
|
|
7638
7850
|
// src/index.ts
|
|
7639
7851
|
var program = new Command12();
|
|
7640
|
-
program.name("flowmind").description("Flowmind CLI (also installed as fm)").version("0.3.
|
|
7852
|
+
program.name("flowmind").description("Flowmind CLI (also installed as fm)").version("0.3.9").enablePositionalOptions();
|
|
7641
7853
|
program.addCommand(agentCommand);
|
|
7642
7854
|
program.addCommand(authCommand);
|
|
7643
7855
|
program.addCommand(logCommand);
|