@flowmind/cli 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +69 -43
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -294,7 +294,7 @@ var isHttpEvidenceUrl = (value) => {
294
294
 
295
295
  // src/agent-session.ts
296
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";
297
+ import { appendFile, mkdir as mkdir5, readdir as readdir7, readFile as readFile6, rename as rename5, rm as rm4, rmdir, stat as stat3, utimes, writeFile as writeFile5 } from "node:fs/promises";
298
298
  import { join as join8 } from "node:path";
299
299
 
300
300
  // src/awake-clock.ts
@@ -566,7 +566,8 @@ function accountDirectory() {
566
566
  }
567
567
 
568
568
  // src/project-link.ts
569
- import { readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
569
+ import { randomUUID as randomUUID2 } from "node:crypto";
570
+ import { chmod, readFile as readFile2, rename, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
570
571
  import { dirname as dirname2, join as join4, resolve } from "node:path";
571
572
  var LINK_FILE = ".flowmind.json";
572
573
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -625,22 +626,40 @@ async function readLink(start) {
625
626
  async function writeLink(start, project) {
626
627
  const root = await findRepoRoot(start);
627
628
  const path = join4(root, LINK_FILE);
628
- await writeFile2(path, `${JSON.stringify({ projectId: project.id, name: project.name }, null, 2)}
629
- `, "utf8");
629
+ await replaceLinkFile(path, { projectId: project.id, name: project.name });
630
630
  return { kind: "linked", root, path, projectId: project.id, name: project.name };
631
631
  }
632
632
  async function writeIgnore(start, reason) {
633
633
  const root = await findRepoRoot(start);
634
634
  const path = join4(root, LINK_FILE);
635
635
  const trimmed = reason?.trim() || null;
636
- await writeFile2(path, `${JSON.stringify({ ignore: true, ...trimmed ? { reason: trimmed } : {} }, null, 2)}
637
- `, "utf8");
636
+ await replaceLinkFile(path, { ignore: true, ...trimmed ? { reason: trimmed } : {} });
638
637
  return { kind: "ignored", root, path, reason: trimmed };
639
638
  }
639
+ async function replaceLinkFile(path, content) {
640
+ const temporary = `${path}.${process.pid}.${randomUUID2()}.tmp`;
641
+ const mode = await permissionBits(path);
642
+ try {
643
+ await writeFile2(temporary, `${JSON.stringify(content, null, 2)}
644
+ `, { encoding: "utf8", flag: "wx", ...mode === null ? {} : { mode } });
645
+ if (mode !== null) await chmod(temporary, mode);
646
+ await rename(temporary, path);
647
+ } finally {
648
+ await rm(temporary, { force: true });
649
+ }
650
+ }
651
+ async function permissionBits(path) {
652
+ try {
653
+ return (await stat(path)).mode & 4095;
654
+ } catch (error) {
655
+ if (error.code === "ENOENT") return null;
656
+ throw error;
657
+ }
658
+ }
640
659
 
641
660
  // src/manual-save.ts
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";
661
+ import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
662
+ import { mkdir as mkdir4, readdir as readdir6, readFile as readFile5, rename as rename4, unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
644
663
  import { join as join7 } from "node:path";
645
664
 
646
665
  // src/account.ts
@@ -975,13 +994,13 @@ async function apiRequest(endpoint, args, options = {}) {
975
994
  }
976
995
 
977
996
  // 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";
997
+ import { mkdir as mkdir3, readdir as readdir4, readFile as readFile4, rename as rename3, rm as rm3, stat as stat2 } from "node:fs/promises";
979
998
  import { join as join6 } from "node:path";
980
999
 
981
1000
  // src/pending-stages.ts
982
1001
  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";
1002
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
1003
+ import { mkdir as mkdir2, readdir as readdir3, readFile as readFile3, rename as rename2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
985
1004
  import { basename, join as join5 } from "node:path";
986
1005
 
987
1006
  // src/stored-stage-input.ts
@@ -1065,9 +1084,9 @@ function readStage(value, fileName, legacy = false) {
1065
1084
  }
1066
1085
  var writePendingStage = async (stage) => {
1067
1086
  await mkdir2(outboxDir(), { recursive: true });
1068
- const temporary = fileOf(stage.id) + "." + process.pid + "." + randomUUID2() + ".tmp";
1087
+ const temporary = fileOf(stage.id) + "." + process.pid + "." + randomUUID3() + ".tmp";
1069
1088
  await writeFile3(temporary, JSON.stringify(stage, null, 2) + "\n", { encoding: "utf8", mode: 384 });
1070
- await rename(temporary, fileOf(stage.id));
1089
+ await rename2(temporary, fileOf(stage.id));
1071
1090
  };
1072
1091
  async function saveStageBeforeSending(input2) {
1073
1092
  const at2 = input2.now.toISOString();
@@ -1111,7 +1130,7 @@ async function recordStageFailure(stage, failure, now) {
1111
1130
  return next;
1112
1131
  }
1113
1132
  async function forgetStage(id) {
1114
- await rm(fileOf(id), { force: true });
1133
+ await rm2(fileOf(id), { force: true });
1115
1134
  }
1116
1135
  async function listPendingStages() {
1117
1136
  let names;
@@ -1309,7 +1328,7 @@ async function moveIntoAccount(stage) {
1309
1328
  }
1310
1329
  );
1311
1330
  if (present) return false;
1312
- await rename2(join6(legacyOutboxDir(), stage.id + ".json"), target);
1331
+ await rename3(join6(legacyOutboxDir(), stage.id + ".json"), target);
1313
1332
  await writePendingStage({ ...stage, credentialFingerprint: accountBinding().credential });
1314
1333
  return true;
1315
1334
  }
@@ -1342,7 +1361,7 @@ async function adoptUnownedProof(id) {
1342
1361
  }
1343
1362
  async function discardUnownedProof(id) {
1344
1363
  await findUnowned(id);
1345
- await rm2(join6(legacyOutboxDir(), id + ".json"));
1364
+ await rm3(join6(legacyOutboxDir(), id + ".json"));
1346
1365
  }
1347
1366
 
1348
1367
  // src/account.ts
@@ -1450,10 +1469,10 @@ async function manualSave(identity, prepare, send) {
1450
1469
  if (error.code !== "ENOENT") throw error;
1451
1470
  const body = await prepare();
1452
1471
  if (body === null) return null;
1453
- saved = { id, body, key: randomUUID3(), savedAt: Date.now() };
1454
- const temp = fileOf2(id) + "." + randomUUID3() + ".tmp";
1472
+ saved = { id, body, key: randomUUID4(), savedAt: Date.now() };
1473
+ const temp = fileOf2(id) + "." + randomUUID4() + ".tmp";
1455
1474
  await writeFile4(temp, JSON.stringify(saved), { mode: 384, flag: "wx" });
1456
- await rename3(temp, fileOf2(id));
1475
+ await rename4(temp, fileOf2(id));
1457
1476
  }
1458
1477
  if (Date.now() - saved.savedAt >= RECEIPT_MS) {
1459
1478
  throw new Error(
@@ -4232,7 +4251,7 @@ async function adoptLegacyJournals(fromDir, extendStart, now = Date.now()) {
4232
4251
  try {
4233
4252
  events = text2.split("\n").filter((line) => line.trim() !== "").map((line) => currentFormOf(line, journal.from));
4234
4253
  } catch {
4235
- await rename4(journal.from, journal.from + ".unreadable");
4254
+ await rename5(journal.from, journal.from + ".unreadable");
4236
4255
  continue;
4237
4256
  }
4238
4257
  for (const event of events) earliest = Math.min(earliest, Date.parse(event.at));
@@ -4244,17 +4263,17 @@ async function adoptLegacyJournals(fromDir, extendStart, now = Date.now()) {
4244
4263
  const target = join8(sessionsDir(), journal.name + ADOPTED);
4245
4264
  await writeFile5(target + ".partial", copy, { mode: 384 });
4246
4265
  await utimes(target + ".partial", journal.modified, journal.modified);
4247
- await rename4(target + ".partial", target);
4266
+ await rename5(target + ".partial", target);
4248
4267
  sources[journal.name] = { size: journal.size, mtimeMs: journal.mtimeMs };
4249
4268
  }
4250
4269
  for (const journal of await legacyJournals(fromDir, now)) {
4251
4270
  if (!journal.expired || !unchanged(journal, sources)) continue;
4252
- await rm3(journal.from, { force: true });
4271
+ await rm4(journal.from, { force: true });
4253
4272
  delete sources[journal.name];
4254
4273
  }
4255
4274
  const manifest = join8(sessionsDir(), SOURCES);
4256
4275
  await writeFile5(manifest + ".partial", JSON.stringify(sources), { mode: 384 });
4257
- await rename4(manifest + ".partial", manifest);
4276
+ await rename5(manifest + ".partial", manifest);
4258
4277
  try {
4259
4278
  await rmdir(fromDir);
4260
4279
  } catch (error) {
@@ -4368,9 +4387,9 @@ async function pruneOldSessions(now) {
4368
4387
  if (!retiring && now.getTime() - await lastWritten(path) <= RETENTION_MS) continue;
4369
4388
  if (!retiring && await lastWritten(join8(legacySessionsDir(), name)) !== -Infinity) continue;
4370
4389
  if (!retiring) await writeFile5(path + RETIRED, "", { mode: 384 });
4371
- await rm3(path, { force: true });
4372
- await rm3(path + ADOPTED, { force: true });
4373
- await rm3(path + RETIRED, { force: true });
4390
+ await rm4(path, { force: true });
4391
+ await rm4(path + ADOPTED, { force: true });
4392
+ await rm4(path + RETIRED, { force: true });
4374
4393
  }
4375
4394
  }
4376
4395
  async function handleHook(agent, raw, prune, now = currentMoment()) {
@@ -4525,7 +4544,7 @@ function ownedParts(start, end, cut, periods) {
4525
4544
  return part.ms > 0 ? [part] : [];
4526
4545
  });
4527
4546
  }
4528
- function measureRecordedTime(events, now, periods) {
4547
+ function measureRecordedTime(events, now, periods, observedTurnId) {
4529
4548
  const first = events.find((event) => event.event === "session" || event.event === "start");
4530
4549
  if (!first || first.event !== "session" && first.event !== "start") return null;
4531
4550
  const nowPoint = { wall: now.at.getTime(), up: now.up };
@@ -4572,7 +4591,7 @@ function measureRecordedTime(events, now, periods) {
4572
4591
  break;
4573
4592
  }
4574
4593
  }
4575
- if (open2) turns.push({ start: open2.start, end: open2.last });
4594
+ if (open2) turns.push({ start: open2.start, end: observedTurnId !== void 0 && observedTurnId === open2.turnId ? nowPoint : open2.last });
4576
4595
  const stagedCut = lastStage ? cutOf(lastStage) : null;
4577
4596
  const counted = turns.flatMap(
4578
4597
  ({ start, end }) => ownedParts(start, end.wall > nowPoint.wall ? nowPoint : end, stagedCut, periods)
@@ -4653,17 +4672,23 @@ function missedTurnEvents(events, turns, owner, now, lastWake) {
4653
4672
  }
4654
4673
  return added;
4655
4674
  }
4656
- async function stageTime(sessionId, now) {
4675
+ async function stageTime(sessionId, now, observation) {
4657
4676
  const events = await readEventsAt(sessionFile(sessionId));
4658
4677
  if (!events) return null;
4659
4678
  const owner = events.find(
4660
4679
  (event) => event.event === "session" || event.event === "start"
4661
4680
  );
4662
- if (owner?.agent !== "codex") return measureRecordedTime(events, now, signedInPeriods());
4681
+ if (owner?.agent !== "codex") {
4682
+ const latestStart = events.filter((event) => event.event === "start").at(-1);
4683
+ const observedTurn2 = observation === "active-turn" && latestStart?.event === "start" ? latestStart.turnId : void 0;
4684
+ return measureRecordedTime(events, now, signedInPeriods(), observedTurn2);
4685
+ }
4663
4686
  const file = await codexTurnsOf(sessionId, transcriptPathOf(events));
4664
4687
  if (file === null) return measureRecordedTime(events, now, signedInPeriods());
4665
4688
  const added = missedTurnEvents(events, file.turns, owner, now, lastWakeMs());
4666
- return measureRecordedTime(byTime([...events, ...added]), now, signedInPeriods());
4689
+ const latestTurn = file.turns.at(-1);
4690
+ const observedTurn = observation === "codex-env" && latestTurn?.endedAt === null ? latestTurn.turnId : void 0;
4691
+ return measureRecordedTime(byTime([...events, ...added]), now, signedInPeriods(), observedTurn);
4667
4692
  }
4668
4693
  async function promptReminder(sessionId, cwd, now) {
4669
4694
  const link2 = await readLink(cwd);
@@ -4969,7 +4994,8 @@ async function resolveStageOptions(options, cwd, now) {
4969
4994
  let { minutes, startedAt } = options;
4970
4995
  let recorded = null;
4971
4996
  if (minutes === void 0 && startedAt === void 0) {
4972
- recorded = session.sessionId ? await stageTime(session.sessionId, now) : null;
4997
+ const observation = session.source === "codex-env" || session.source === "flag" && session.sessionId === process.env.CODEX_THREAD_ID ? "codex-env" : session.source === "active-turn" ? "active-turn" : void 0;
4998
+ recorded = session.sessionId ? await stageTime(session.sessionId, now, observation) : null;
4973
4999
  if (!recorded) {
4974
5000
  throw new Error(
4975
5001
  "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."
@@ -5683,9 +5709,9 @@ var logCommand = new Command3("log").description("Log time with natural language
5683
5709
  });
5684
5710
 
5685
5711
  // src/commands/hooks.ts
5686
- import { createHash as createHash6, randomUUID as randomUUID4 } from "node:crypto";
5712
+ import { createHash as createHash6, randomUUID as randomUUID5 } from "node:crypto";
5687
5713
  import { realpathSync } from "node:fs";
5688
- import { readFile as readFile7, link, lstat, mkdir as mkdir6, rename as rename5, rm as rm4, writeFile as writeFile6 } from "node:fs/promises";
5714
+ import { readFile as readFile7, link, lstat, mkdir as mkdir6, rename as rename6, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
5689
5715
  import { homedir as homedir2 } from "node:os";
5690
5716
  import { dirname as dirname3, join as join9 } from "node:path";
5691
5717
  import { fileURLToPath } from "node:url";
@@ -5887,7 +5913,7 @@ async function updateHookFile(agent, action, base) {
5887
5913
  if (info && !info.isFile()) throw new Error("Unsupported hook settings file: " + path);
5888
5914
  const changed = () => new Error(path + " changed while Flowmind was updating it. Nothing was written; run the command again.");
5889
5915
  const removeFile = agent === "codex" && action === "uninstall" && Object.keys(next).length === 0;
5890
- const temporary = path + "." + randomUUID4() + ".tmp";
5916
+ const temporary = path + "." + randomUUID5() + ".tmp";
5891
5917
  if (!removeFile) await writeFile6(temporary, serialize(next), { mode: info ? info.mode & 511 : 384, flag: "wx" });
5892
5918
  try {
5893
5919
  let onDisk;
@@ -5898,7 +5924,7 @@ async function updateHookFile(agent, action, base) {
5898
5924
  onDisk = null;
5899
5925
  }
5900
5926
  if (onDisk !== text2) throw changed();
5901
- if (removeFile) await rm4(path);
5927
+ if (removeFile) await rm5(path);
5902
5928
  else if (text2 === null) {
5903
5929
  try {
5904
5930
  await link(temporary, path);
@@ -5906,9 +5932,9 @@ async function updateHookFile(agent, action, base) {
5906
5932
  if (error.code === "EEXIST") throw changed();
5907
5933
  throw error;
5908
5934
  }
5909
- } else await rename5(temporary, path);
5935
+ } else await rename6(temporary, path);
5910
5936
  } finally {
5911
- await rm4(temporary, { force: true });
5937
+ await rm5(temporary, { force: true });
5912
5938
  }
5913
5939
  }
5914
5940
  async function bundledSkill() {
@@ -5962,8 +5988,8 @@ hooksCommand.command("install").description(
5962
5988
  if (options.dryRun) {
5963
5989
  console.log(terminalLine`Would remove ${copy.path} and ${copy.markerPath}: ${reason}`);
5964
5990
  } else {
5965
- await rm4(copy.path);
5966
- await rm4(copy.markerPath);
5991
+ await rm5(copy.path);
5992
+ await rm5(copy.markerPath);
5967
5993
  console.log(terminalLine`Removed ${copy.path} and ${copy.markerPath}: ${reason}`);
5968
5994
  }
5969
5995
  } else if (copy.state === "absent") {
@@ -6034,9 +6060,9 @@ hooksCommand.command("uninstall").description("Remove unmodified Flowmind-manage
6034
6060
  console.log(terminalLine`Left ${target.state} ${target.path}`);
6035
6061
  continue;
6036
6062
  }
6037
- await rm4(target.path);
6063
+ await rm5(target.path);
6038
6064
  console.log(terminalLine`Removed ${target.path}`);
6039
- await rm4(target.markerPath);
6065
+ await rm5(target.markerPath);
6040
6066
  console.log(terminalLine`Removed ${target.markerPath}`);
6041
6067
  }
6042
6068
  for (const agent of agents) {
@@ -8017,7 +8043,7 @@ for (const [name, description, done] of [
8017
8043
 
8018
8044
  // src/index.ts
8019
8045
  var program = new Command12();
8020
- program.name("flowmind").description("Flowmind CLI (also installed as fm)").version("0.3.10").enablePositionalOptions();
8046
+ program.name("flowmind").description("Flowmind CLI (also installed as fm)").version("0.3.12").enablePositionalOptions();
8021
8047
  program.addCommand(agentCommand);
8022
8048
  program.addCommand(authCommand);
8023
8049
  program.addCommand(logCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowmind/cli",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Flowmind CLI: log what you and your coding agents ship, with the outcome and the link that proves it. Works with Codex and Claude Code.",
5
5
  "license": "MIT",
6
6
  "keywords": [