mainflow 0.33.0 → 0.34.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { B as parseRef, H as refExample, V as parseUntil, Y as stampMs, k as UNTIL_FORMS } from "./dist-IdbnCDqM.js";
3
- import { $ as MAX_FILE_BYTES, B as checkHubName, H as loadHub, S as readSession, V as HubError, Y as nodeStorage, _ as untilState, ct as TASK_FILE, d as retroReportFromReceiver, dt as findHubRoot, f as receiverFromEnv, g as initHub, ht as WriteError, l as migrateHub, lt as requireSlug, mt as DATA_DIR, p as RETRY_RULES, q as nowMinute, st as MAINLINE_FILE, u as retroReport, v as duplicateClaimLines, x as loadSessions, y as sessionBriefing, z as applyOperation } from "./host-CObkJQmr.js";
3
+ import { $ as MAX_FILE_BYTES, B as checkHubName, H as loadHub, S as readSession, V as HubError, Y as nodeStorage, _ as untilState, ct as TASK_FILE, d as retroReportFromReceiver, dt as findHubRoot, f as receiverFromEnv, g as initHub, ht as WriteError, l as migrateHub, lt as requireSlug, mt as DATA_DIR, p as RETRY_RULES, q as nowMinute, st as MAINLINE_FILE, u as retroReport, v as duplicateClaimLines, x as loadSessions, y as sessionBriefing, z as applyOperation } from "./host-79T-_NMu.js";
4
4
  import { t as listProgress } from "./progress-1BfPg2zN.js";
5
- import { A as syncPaths, C as ENV, D as SETTINGS_FILE, E as syncSettings, S as ServerError, T as hubName, _ as readDaemon, a as remoteMode, b as oversizedFiles, c as planAnnounce, d as toolCallOf, f as startDaemon, g as probeDaemon, h as callDaemon, i as queuedCount, l as planFetch, m as DaemonUnavailable, n as hubWriter, o as fetchDenial, p as DaemonRunning, r as writtenLines, s as hookAnswer, t as CONFIRM_MS, u as planFlush, v as silentHint, w as earlierToken, x as ServerApi, y as readRemote } from "./operation-B2pq2Q9z.js";
5
+ import { A as syncPaths, C as ENV, D as SETTINGS_FILE, E as syncSettings, S as ServerError, T as hubName, _ as readDaemon, a as remoteMode, b as oversizedFiles, c as planAnnounce, d as toolCallOf, f as startDaemon, g as probeDaemon, h as callDaemon, i as queuedCount, l as planFetch, m as DaemonUnavailable, n as hubWriter, o as fetchDenial, p as DaemonRunning, r as writtenLines, s as hookAnswer, t as CONFIRM_MS, u as planFlush, v as silentHint, w as earlierToken, x as ServerApi, y as readRemote } from "./operation--fJnUqvA.js";
6
6
  import { COMMAND_SPECS, OPTIONS } from "./facts.js";
7
7
  import { hookFallback, parsePayload, runHook } from "./hooks.js";
8
- import { c as watchHubGone, l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-BemxyV9P.js";
8
+ import { c as watchHubGone, l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-DQVyIbpf.js";
9
9
  import { randomBytes } from "node:crypto";
10
10
  import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
11
11
  import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
package/dist/facts.d.ts CHANGED
@@ -336,7 +336,7 @@ export declare const COMMAND_SPECS: {
336
336
  };
337
337
  readonly retro: {
338
338
  readonly usage: 'retro <m> [--json]';
339
- readonly summary: 'Read the Claude Code transcripts of the sessions that served a mainline, and their subagents\', into estimated list-price cost (per model, opening context, cache hit rate, hook injections) and candidates for where work went wrong (tool errors, rerun Bash commands, hook injections followed by no tool call, blocks, pauses over 5 minutes, failed completion checks); read-only. With MAINFLOW_TELEMETRY_URL and MAINFLOW_TELEMETRY_TOKEN set it also reads the measurement receiver\'s export: billed cost per request, and the run time of every hook, the silent ones too';
339
+ readonly summary: 'Read the Claude Code transcripts of the sessions that served a mainline, and their subagents\', into estimated list-price cost (per model, opening context, cache hit rate, hook injections) and candidates for where work went wrong (tool errors, rerun Bash commands, hook injections followed by no tool call, blocks, pauses over 5 minutes, failed completion checks); read-only. With MAINFLOW_TELEMETRY_URL, MAINFLOW_TELEMETRY_ACCESS_CLIENT_ID and MAINFLOW_TELEMETRY_ACCESS_CLIENT_SECRET (a Cloudflare Access service token) set it also reads the measurement receiver\'s export: billed cost per request, and the run time of every hook, the silent ones too; sessions whose transcript is not on this machine are downloaded from the receiver into .mainflow/local/transcripts/ first';
340
340
  readonly args: 1;
341
341
  readonly needsHub: true;
342
342
  };
package/dist/facts.js CHANGED
@@ -326,7 +326,7 @@ const COMMAND_SPECS = {
326
326
  },
327
327
  retro: {
328
328
  usage: "retro <m> [--json]",
329
- summary: "Read the Claude Code transcripts of the sessions that served a mainline, and their subagents', into estimated list-price cost (per model, opening context, cache hit rate, hook injections) and candidates for where work went wrong (tool errors, rerun Bash commands, hook injections followed by no tool call, blocks, pauses over 5 minutes, failed completion checks); read-only. With MAINFLOW_TELEMETRY_URL and MAINFLOW_TELEMETRY_TOKEN set it also reads the measurement receiver's export: billed cost per request, and the run time of every hook, the silent ones too",
329
+ summary: "Read the Claude Code transcripts of the sessions that served a mainline, and their subagents', into estimated list-price cost (per model, opening context, cache hit rate, hook injections) and candidates for where work went wrong (tool errors, rerun Bash commands, hook injections followed by no tool call, blocks, pauses over 5 minutes, failed completion checks); read-only. With MAINFLOW_TELEMETRY_URL, MAINFLOW_TELEMETRY_ACCESS_CLIENT_ID and MAINFLOW_TELEMETRY_ACCESS_CLIENT_SECRET (a Cloudflare Access service token) set it also reads the measurement receiver's export: billed cost per request, and the run time of every hook, the silent ones too; sessions whose transcript is not on this machine are downloaded from the receiver into .mainflow/local/transcripts/ first",
330
330
  args: 1,
331
331
  needsHub: true
332
332
  },
package/dist/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { $ as describeRequest, j as configSchema } from "./dist-IdbnCDqM.js";
2
- import { D as endsWithMarker, E as decorateReply, H as loadHub, J as readYaml, O as headerLine, S as readSession, T as matchGrants, V as HubError, b as turnReminder, dt as findHubRoot, ft as worktreesOf, i as hostName, k as mayDecorate, mt as DATA_DIR, ut as currentBranch, w as grantScope, x as loadSessions, y as sessionBriefing } from "./host-CObkJQmr.js";
3
- import { n as hubWriter, r as writtenLines } from "./operation-B2pq2Q9z.js";
2
+ import { D as endsWithMarker, E as decorateReply, H as loadHub, J as readYaml, O as headerLine, S as readSession, T as matchGrants, V as HubError, b as turnReminder, dt as findHubRoot, ft as worktreesOf, i as hostName, k as mayDecorate, mt as DATA_DIR, ut as currentBranch, w as grantScope, x as loadSessions, y as sessionBriefing } from "./host-79T-_NMu.js";
3
+ import { n as hubWriter, r as writtenLines } from "./operation--fJnUqvA.js";
4
4
  import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
5
5
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
6
6
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
@@ -7,7 +7,7 @@ import { execFileSync, spawn, spawnSync } from "node:child_process";
7
7
  import { parse as parse$1, stringify } from "yaml";
8
8
  import { homedir } from "node:os";
9
9
  import { performance } from "node:perf_hooks";
10
- import { gunzipSync } from "node:zlib";
10
+ import { gunzipSync, inflateRawSync } from "node:zlib";
11
11
  //#region ../core/dist/ops/document.js
12
12
  const DATA_DIR = ".mainflow";
13
13
  const ENTRY_DIR = /^([mtdif]\d{3})-(.+)$/;
@@ -3774,6 +3774,8 @@ async function setArchivedMany(kind, ids, archived, write) {
3774
3774
  //#endregion
3775
3775
  //#region ../core/dist/retro/util.js
3776
3776
  const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3777
+ /** A 2xx response. Requests use redirect 'manual', so a 3xx (Access sending the caller to its login page) is a failure. */
3778
+ const isOk = (response) => response.status >= 200 && response.status < 300;
3777
3779
  const str = (v) => typeof v === "string" && v ? v : void 0;
3778
3780
  const finite = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
3779
3781
  /** Appends `items` to `into` one by one. */
@@ -6334,19 +6336,246 @@ function pairInjection(injection, own, toolUseId, successes, stopSummaries) {
6334
6336
  if (tag?.reason) return { reason: tag.reason };
6335
6337
  }
6336
6338
  //#endregion
6339
+ //#region ../core/dist/retro/transcripts.js
6340
+ const PAGE_LIMIT = 1e3;
6341
+ const MAX_BUNDLE_KEYS$1 = 40;
6342
+ /** Rounds of re-asking for the keys a bundle deferred, so a receiver that never delivers cannot loop forever. */
6343
+ const MAX_DEFERRED_ROUNDS = 20;
6344
+ const TEXT_SHOWN$1 = 200;
6345
+ const SAFE_ID = /^[\w.-]+$/;
6346
+ const SAFE_PATH = /^(?:[\w.-]+\.jsonl|subagents\/[\w.-]+\.(?:jsonl|meta\.json))$/;
6347
+ /** The file path under a session and the offset a stored key holds: transcripts/{sid}/{path}/{offset}.gz */
6348
+ function parseTranscriptKey(sid, key) {
6349
+ const m = new RegExp(`^transcripts/${sid.replace(/[.]/g, "\\.")}/(.+)/(\\d{1,15})\\.gz$`).exec(key);
6350
+ return m && SAFE_PATH.test(m[1]) ? {
6351
+ path: m[1],
6352
+ offset: Number(m[2])
6353
+ } : void 0;
6354
+ }
6355
+ /** Where a stored path lands: the main transcript beside the directory, the rest under <dir>/{sid}/. */
6356
+ function transcriptFile(want, path) {
6357
+ return path.includes("/") ? join(want.dir, want.sid, path) : join(want.dir, path);
6358
+ }
6359
+ /**
6360
+ * The segments of one file in use, from offset 0 on and as far as they run without a gap; a segment overlapping the
6361
+ * ones before it (a resend) only adds what lies past them. `length` of a segment is unknown (undefined) before it
6362
+ * is downloaded unless the listing gave rawLength, in which case `end` is undefined.
6363
+ */
6364
+ function joined(segments, lengthOf) {
6365
+ const used = [];
6366
+ let end = 0;
6367
+ for (const s of [...segments].sort((a, b) => a.offset - b.offset)) {
6368
+ if (end === void 0) {
6369
+ used.push(s);
6370
+ continue;
6371
+ }
6372
+ if (s.offset > end) return {
6373
+ used,
6374
+ end,
6375
+ gapAt: s.offset
6376
+ };
6377
+ used.push(s);
6378
+ const length = lengthOf(s);
6379
+ end = length === void 0 ? void 0 : Math.max(end, s.offset + length);
6380
+ }
6381
+ return {
6382
+ used,
6383
+ end
6384
+ };
6385
+ }
6386
+ /** The size the receiver's segments add up to for each file, or undefined when a segment of it has no rawLength. */
6387
+ function expectedSizes(sid, listed) {
6388
+ const byPath = /* @__PURE__ */ new Map();
6389
+ for (const o of listed) {
6390
+ const parsed = parseTranscriptKey(sid, o.key);
6391
+ if (parsed) byPath.set(parsed.path, [...byPath.get(parsed.path) ?? [], {
6392
+ ...o,
6393
+ offset: parsed.offset
6394
+ }]);
6395
+ }
6396
+ return new Map([...byPath].map(([path, segments]) => [path, joined(segments, (s) => s.rawLength).end]));
6397
+ }
6398
+ /** Cuts the gzip members out of a bundle, one per key: a member is a header, a deflate stream and an 8-byte trailer. */
6399
+ function members(bytes, count) {
6400
+ const out = [];
6401
+ let at = 0;
6402
+ while (out.length < count && at < bytes.length) {
6403
+ if (bytes[at] !== 31 || bytes[at + 1] !== 139) throw new Error("bundle is not gzip members");
6404
+ const flags = bytes[at + 3];
6405
+ let p = at + 10;
6406
+ if (flags & 4) p += 2 + bytes.readUInt16LE(p);
6407
+ if (flags & 8) p = bytes.indexOf(0, p) + 1;
6408
+ if (flags & 16) p = bytes.indexOf(0, p) + 1;
6409
+ if (flags & 2) p += 2;
6410
+ const { buffer, engine } = inflateRawSync(bytes.subarray(p), { info: true });
6411
+ out.push(buffer);
6412
+ at = p + engine.bytesWritten + 8;
6413
+ }
6414
+ if (out.length !== count) throw new Error(`bundle holds ${out.length} objects, expected ${count}`);
6415
+ return out;
6416
+ }
6417
+ /**
6418
+ * Brings the transcripts of the wanted sessions into their directories. A session whose files are all there and as
6419
+ * long as the receiver's segments add up to (by rawLength) is not downloaded again; with no rawLength it always is.
6420
+ */
6421
+ async function fetchTranscripts(source, wanted) {
6422
+ const doFetch = source.fetch ?? globalThis.fetch;
6423
+ const result = {
6424
+ fetched: [],
6425
+ warnings: [],
6426
+ requests: 0
6427
+ };
6428
+ const scrub = (text) => source.accessClientSecret ? text.split(source.accessClientSecret).join("***") : text;
6429
+ const warn = (text) => result.warnings.push(scrub(text));
6430
+ let origin;
6431
+ try {
6432
+ const base = new URL(source.url);
6433
+ if (base.protocol !== "https:" && base.protocol !== "http:") throw new Error("not http");
6434
+ origin = base.origin;
6435
+ } catch {
6436
+ warn("接收端網址不是 http 或 https 網址,沒有下載對話紀錄");
6437
+ return result;
6438
+ }
6439
+ const headers = {
6440
+ "cf-access-client-id": source.accessClientId,
6441
+ "cf-access-client-secret": source.accessClientSecret
6442
+ };
6443
+ const failure = async (response) => {
6444
+ let text = "";
6445
+ try {
6446
+ text = (await response.text()).replace(/\s+/g, " ").trim().slice(0, TEXT_SHOWN$1);
6447
+ } catch {}
6448
+ return `HTTP ${response.status}${text ? ` ${text}` : ""}`;
6449
+ };
6450
+ const thrown = (error) => {
6451
+ const e = error;
6452
+ const cause = e.cause?.code ?? e.cause?.message;
6453
+ return `${e.message ?? String(error)}${cause ? `(${cause})` : ""}`;
6454
+ };
6455
+ const list = async (sid) => {
6456
+ const objects = [];
6457
+ let cursor;
6458
+ for (let page = 0; page < PAGE_LIMIT; page++) {
6459
+ const u = new URL(`/v1/transcripts/${encodeURIComponent(sid)}`, origin);
6460
+ if (cursor) u.searchParams.set("cursor", cursor);
6461
+ result.requests += 1;
6462
+ const response = await doFetch(u, {
6463
+ headers,
6464
+ redirect: "manual"
6465
+ });
6466
+ if (!isOk(response)) throw new Error(`物件清單讀取失敗:${await failure(response)}`);
6467
+ const body = await response.json();
6468
+ if (!isObject(body) || !Array.isArray(body.objects)) throw new Error("物件清單格式不對");
6469
+ for (const o of body.objects) {
6470
+ if (!isObject(o) || typeof o.key !== "string") continue;
6471
+ const parsed = parseTranscriptKey(sid, o.key);
6472
+ if (parsed) objects.push({
6473
+ key: o.key,
6474
+ offset: parsed.offset,
6475
+ ...typeof o.rawLength === "number" ? { rawLength: o.rawLength } : {}
6476
+ });
6477
+ }
6478
+ cursor = typeof body.next === "string" && body.next ? body.next : void 0;
6479
+ if (!cursor) return objects;
6480
+ }
6481
+ throw new Error(`物件清單超過 ${PAGE_LIMIT} 頁`);
6482
+ };
6483
+ /** The bytes of each key, asking again for the keys a bundle deferred. */
6484
+ const download = async (sid, keys) => {
6485
+ const got = /* @__PURE__ */ new Map();
6486
+ let pending = [...keys];
6487
+ for (let round = 0; pending.length && round < MAX_DEFERRED_ROUNDS; round++) {
6488
+ const deferred = [];
6489
+ for (let i = 0; i < pending.length; i += MAX_BUNDLE_KEYS$1) {
6490
+ const batch = pending.slice(i, i + MAX_BUNDLE_KEYS$1);
6491
+ result.requests += 1;
6492
+ const response = await doFetch(new URL(`/v1/transcripts/${encodeURIComponent(sid)}/bundle`, origin), {
6493
+ method: "POST",
6494
+ headers: {
6495
+ ...headers,
6496
+ "content-type": "application/json"
6497
+ },
6498
+ redirect: "manual",
6499
+ body: JSON.stringify({ keys: batch })
6500
+ });
6501
+ if (!isOk(response)) throw new Error(`物件下載失敗:${await failure(response)}`);
6502
+ const split = (name) => (response.headers.get(name) ?? "").split(",").map((k) => k.trim()).filter(Boolean);
6503
+ const absent = new Set(split("x-objects-missing"));
6504
+ const later = new Set(split("x-objects-deferred"));
6505
+ if (absent.size) warn(`會話 ${sid} 的 ${absent.size} 個物件接收端找不到,略過`);
6506
+ const present = batch.filter((k) => !absent.has(k) && !later.has(k));
6507
+ const parts = members(Buffer.from(await response.arrayBuffer()), present.length);
6508
+ present.forEach((k, j) => got.set(k, parts[j]));
6509
+ deferred.push(...batch.filter((k) => later.has(k)));
6510
+ }
6511
+ pending = deferred;
6512
+ }
6513
+ if (pending.length) throw new Error(`${pending.length} 個物件一直沒有取回`);
6514
+ return got;
6515
+ };
6516
+ for (const want of wanted) {
6517
+ const { sid } = want;
6518
+ if (!SAFE_ID.test(sid)) continue;
6519
+ try {
6520
+ const listed = await list(sid);
6521
+ if (listed.length === 0) continue;
6522
+ if ([...expectedSizes(sid, listed)].every(([path, size]) => {
6523
+ const file = transcriptFile(want, path);
6524
+ return size !== void 0 && existsSync(file) && statSync(file).size === size;
6525
+ })) continue;
6526
+ const byPath = /* @__PURE__ */ new Map();
6527
+ for (const o of listed) {
6528
+ const { path } = parseTranscriptKey(sid, o.key);
6529
+ byPath.set(path, [...byPath.get(path) ?? [], {
6530
+ key: o.key,
6531
+ offset: o.offset
6532
+ }]);
6533
+ }
6534
+ const bytes = await download(sid, listed.map((o) => o.key));
6535
+ const files = /* @__PURE__ */ new Map();
6536
+ for (const [path, segments] of byPath) {
6537
+ const { used, gapAt } = joined(segments.filter((s) => bytes.has(s.key)).map((s) => ({
6538
+ ...s,
6539
+ data: bytes.get(s.key)
6540
+ })), (s) => s.data.length);
6541
+ if (gapAt !== void 0) warn(`會話 ${sid} 的 ${path} 在位置 ${gapAt} 之前的分段不連續,只用前面已有的部分`);
6542
+ const parts = [];
6543
+ let end = 0;
6544
+ for (const s of used) {
6545
+ if (s.offset + s.data.length > end) parts.push(s.data.subarray(end - s.offset));
6546
+ end = Math.max(end, s.offset + s.data.length);
6547
+ }
6548
+ if (parts.length) files.set(path, Buffer.concat(parts));
6549
+ }
6550
+ for (const [path, data] of files) {
6551
+ const file = transcriptFile(want, path);
6552
+ mkdirSync(dirname(file), { recursive: true });
6553
+ writeFileSync(file, data);
6554
+ }
6555
+ if (files.size) result.fetched.push(sid);
6556
+ } catch (error) {
6557
+ warn(`會話 ${sid} 的對話紀錄下載失敗:${thrown(error)}`);
6558
+ }
6559
+ }
6560
+ return result;
6561
+ }
6562
+ //#endregion
6337
6563
  //#region ../core/dist/retro/receiver.js
6338
- /** The environment variables a command reads the receiver from. Neither set: the retro does not read the receiver. */
6564
+ /** The environment variables a command reads the receiver from. None set: the retro does not read the receiver. */
6339
6565
  const RECEIVER_ENV = {
6340
6566
  url: "MAINFLOW_TELEMETRY_URL",
6341
- token: "MAINFLOW_TELEMETRY_TOKEN"
6567
+ accessClientId: "MAINFLOW_TELEMETRY_ACCESS_CLIENT_ID",
6568
+ accessClientSecret: "MAINFLOW_TELEMETRY_ACCESS_CLIENT_SECRET"
6342
6569
  };
6343
- /** The receiver named by the environment, or null when neither variable is set. One without the other is passed on, so the report says which is missing. */
6570
+ /** The receiver named by the environment, or null when none of the three variables is set. A partial set is passed on, so the report says which is missing. */
6344
6571
  function receiverFromEnv(env) {
6345
6572
  const url = env[RECEIVER_ENV.url]?.trim() ?? "";
6346
- const token = env[RECEIVER_ENV.token]?.trim() ?? "";
6347
- return url || token ? {
6573
+ const accessClientId = env[RECEIVER_ENV.accessClientId]?.trim() ?? "";
6574
+ const accessClientSecret = env[RECEIVER_ENV.accessClientSecret]?.trim() ?? "";
6575
+ return url || accessClientId || accessClientSecret ? {
6348
6576
  url,
6349
- token
6577
+ accessClientId,
6578
+ accessClientSecret
6350
6579
  } : null;
6351
6580
  }
6352
6581
  /** Longest window one export covers. */
@@ -6393,7 +6622,7 @@ function windows(scope) {
6393
6622
  }
6394
6623
  async function readReceiver(config, scopes) {
6395
6624
  const doFetch = config.fetch ?? globalThis.fetch;
6396
- const scrub = (text) => config.token ? text.split(config.token).join("***") : text;
6625
+ const scrub = (text) => config.accessClientSecret ? text.split(config.accessClientSecret).join("***") : text;
6397
6626
  const problems = [];
6398
6627
  const problem = (text) => problems.push(scrub(text));
6399
6628
  const unread = (origin) => ({
@@ -6406,8 +6635,13 @@ async function readReceiver(config, scopes) {
6406
6635
  requests: 0,
6407
6636
  problems
6408
6637
  });
6409
- if (!config.url || !config.token) {
6410
- problems.push(`${!config.url ? RECEIVER_ENV.url : RECEIVER_ENV.token} 沒有設定,沒有讀取接收端`);
6638
+ const absent = [
6639
+ ...config.url ? [] : [RECEIVER_ENV.url],
6640
+ ...config.accessClientId ? [] : [RECEIVER_ENV.accessClientId],
6641
+ ...config.accessClientSecret ? [] : [RECEIVER_ENV.accessClientSecret]
6642
+ ];
6643
+ if (absent.length) {
6644
+ problems.push(`${absent.join("、")} 沒有設定,沒有讀取接收端`);
6411
6645
  return unread("");
6412
6646
  }
6413
6647
  let base;
@@ -6419,7 +6653,10 @@ async function readReceiver(config, scopes) {
6419
6653
  return unread("");
6420
6654
  }
6421
6655
  const origin = base.origin;
6422
- const auth = { authorization: `Bearer ${config.token}` };
6656
+ const auth = {
6657
+ "cf-access-client-id": config.accessClientId,
6658
+ "cf-access-client-secret": config.accessClientSecret
6659
+ };
6423
6660
  let requests = 0;
6424
6661
  /** The reason a request failed: its status and the start of its text, or the network error. */
6425
6662
  const failure = async (response) => {
@@ -6447,8 +6684,11 @@ async function readReceiver(config, scopes) {
6447
6684
  u.searchParams.set("sid", scope.sessionId);
6448
6685
  if (cursor) u.searchParams.set("cursor", cursor);
6449
6686
  requests += 1;
6450
- const response = await doFetch(u, { headers: auth });
6451
- if (!response.ok) {
6687
+ const response = await doFetch(u, {
6688
+ headers: auth,
6689
+ redirect: "manual"
6690
+ });
6691
+ if (!isOk(response)) {
6452
6692
  problem(`${where}的物件清單讀取失敗:${await failure(response)},這段的資料缺`);
6453
6693
  break;
6454
6694
  }
@@ -6484,9 +6724,10 @@ async function readReceiver(config, scopes) {
6484
6724
  ...auth,
6485
6725
  "content-type": "application/json"
6486
6726
  },
6727
+ redirect: "manual",
6487
6728
  body: JSON.stringify({ keys: batch })
6488
6729
  });
6489
- if (!response.ok) {
6730
+ if (!isOk(response)) {
6490
6731
  problem(`${batch.length} 個物件下載失敗(${keysText(batch)}):${await failure(response)},這些列缺`);
6491
6732
  append(missing, batch);
6492
6733
  return;
@@ -6756,6 +6997,29 @@ const secondText = (ms) => {
6756
6997
  const d = new Date(ms);
6757
6998
  return `${d.toLocaleDateString("sv-SE")}T${d.toTimeString().slice(0, 8)}`;
6758
6999
  };
7000
+ /** Where transcripts fetched from the receiver are kept: in the hub's local directory, which the hub ignores. */
7001
+ const transcriptCache = (root) => join(root, ".mainflow/local/transcripts");
7002
+ /** The transcript to read: the recorded path when the file is there, else the copy fetched from the receiver, else the recorded path. */
7003
+ function transcriptOf(root, transcript) {
7004
+ const recorded = transcriptPath(root, transcript);
7005
+ if (existsSync(recorded)) return recorded;
7006
+ const cached = join(transcriptCache(root), basename(recorded));
7007
+ return existsSync(cached) ? cached : recorded;
7008
+ }
7009
+ /** Sessions serving the mainline whose recorded transcript is not on this machine, as the receiver knows them. */
7010
+ function missingTranscripts(root, mainlineId) {
7011
+ const wanted = /* @__PURE__ */ new Map();
7012
+ for (const s of loadSessions(root).sessions) {
7013
+ if (!s.transcript || !s.actions.some((a) => servesMainline(a, mainlineId))) continue;
7014
+ if (existsSync(transcriptPath(root, s.transcript))) continue;
7015
+ const sid = basename(s.transcript, ".jsonl");
7016
+ wanted.set(sid, {
7017
+ sid,
7018
+ dir: transcriptCache(root)
7019
+ });
7020
+ }
7021
+ return [...wanted.values()];
7022
+ }
6759
7023
  /** Subagent transcripts Claude Code keeps beside a session's: <dir>/<session>/subagents/*.jsonl. */
6760
7024
  function subagentFiles(transcript) {
6761
7025
  const dir = join(transcript.replace(/\.jsonl$/, ""), "subagents");
@@ -6888,7 +7152,7 @@ function readGroups(root, mainlineId, windowOf) {
6888
7152
  };
6889
7153
  const ordered = [...serving].sort((a, b) => Number(Boolean(a.delegatedFrom)) - Number(Boolean(b.delegatedFrom)));
6890
7154
  for (const s of ordered) {
6891
- const file = s.transcript ? transcriptPath(root, s.transcript) : void 0;
7155
+ const file = s.transcript ? transcriptOf(root, s.transcript) : void 0;
6892
7156
  if (s.delegatedFrom) {
6893
7157
  if (!file || seen.has(file)) continue;
6894
7158
  const parent = groups.find((g) => g.session === s.delegatedFrom);
@@ -6949,6 +7213,18 @@ function retroReport(root, mainlineId, options = {}) {
6949
7213
  * `measurements.problems`.
6950
7214
  */
6951
7215
  async function retroReportFromReceiver(root, mainlineId, receiver, options = {}) {
7216
+ if (receiver.url && receiver.accessClientId && receiver.accessClientSecret) {
7217
+ const wanted = missingTranscripts(root, mainlineId);
7218
+ if (wanted.length) {
7219
+ const { warnings } = await fetchTranscripts({
7220
+ url: receiver.url,
7221
+ accessClientId: receiver.accessClientId,
7222
+ accessClientSecret: receiver.accessClientSecret,
7223
+ ...receiver.fetch ? { fetch: receiver.fetch } : {}
7224
+ }, wanted);
7225
+ for (const line of warnings) (receiver.warn ?? ((l) => process.stderr.write(`${l}\n`)))(line);
7226
+ }
7227
+ }
6952
7228
  const read = readRetro(root, mainlineId, options);
6953
7229
  const t = performance.now();
6954
7230
  const exported = await readReceiver(receiver, receiverScopes(read.groups));
package/dist/host.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as openInApp, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, r as caddySites, s as siteBlock, t as APP_BUNDLE_ID } from "./host-CObkJQmr.js";
1
+ import { a as openInApp, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, r as caddySites, s as siteBlock, t as APP_BUNDLE_ID } from "./host-79T-_NMu.js";
2
2
  export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, removeHostLink, siteBlock, syncHost };
@@ -1,4 +1,4 @@
1
- import { c as syncHost, ht as WriteError, i as hostName, mt as DATA_DIR, n as DEFAULT_UI_PORT, o as removeHostLink } from "./host-CObkJQmr.js";
1
+ import { c as syncHost, ht as WriteError, i as hostName, mt as DATA_DIR, n as DEFAULT_UI_PORT, o as removeHostLink } from "./host-79T-_NMu.js";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { spawn, spawnSync } from "node:child_process";
@@ -176,7 +176,7 @@ async function launchUi(root, hub, configured, options) {
176
176
  }
177
177
  let server;
178
178
  if (!reuse) {
179
- const { serveUi } = await import("./ui-CFLjhYJ9.js");
179
+ const { serveUi } = await import("./ui-BcXPk-0P.js");
180
180
  server = await serveUi(root, choice.port);
181
181
  writeUiRecord(root, {
182
182
  port: choice.port,
package/dist/launch.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as portCandidates, c as watchHubGone, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-BemxyV9P.js";
1
+ import { a as portCandidates, c as watchHubGone, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-DQVyIbpf.js";
2
2
  export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates, readUiRecord, startUiInBackground, watchHubGone };
@@ -1,4 +1,4 @@
1
- import { $ as MAX_FILE_BYTES, F as renumberFiles, I as renumberPath, L as renumberText, N as applyOffline, P as renumberRequest, Q as MAX_BLOB_BYTES, R as scopeOfPath, X as ACCESS_HEADERS, Y as nodeStorage, Z as BLOB_HASH, _t as splitFrontMatter, at as requestTime, et as ROUTES, gt as contentHash, ht as WriteError, it as readsAsText, mt as DATA_DIR, nt as comparePaths, ot as textBytes, pt as ConflictError, q as nowMinute, rt as exceedsFileLimit, tt as SOCKET_MAX_AGE_CLOSE, z as applyOperation } from "./host-CObkJQmr.js";
1
+ import { $ as MAX_FILE_BYTES, F as renumberFiles, I as renumberPath, L as renumberText, N as applyOffline, P as renumberRequest, Q as MAX_BLOB_BYTES, R as scopeOfPath, X as ACCESS_HEADERS, Y as nodeStorage, Z as BLOB_HASH, _t as splitFrontMatter, at as requestTime, et as ROUTES, gt as contentHash, ht as WriteError, it as readsAsText, mt as DATA_DIR, nt as comparePaths, ot as textBytes, pt as ConflictError, q as nowMinute, rt as exceedsFileLimit, tt as SOCKET_MAX_AGE_CLOSE, z as applyOperation } from "./host-79T-_NMu.js";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, createReadStream, createWriteStream, existsSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -1,8 +1,8 @@
1
1
  import { j as configSchema } from "./dist-IdbnCDqM.js";
2
- import { A as attributeTrace, C as transcriptPath, G as linksInto, H as loadHub, J as readYaml, K as refIndex, M as transcriptTitle, S as readSession, U as dependencyGraph, W as mainlineEdges, a as openInApp, h as setArchivedMany, ht as WriteError, j as traceTranscript, m as ARCHIVE_KINDS, mt as DATA_DIR, pt as ConflictError, x as loadSessions } from "./host-CObkJQmr.js";
2
+ import { A as attributeTrace, C as transcriptPath, G as linksInto, H as loadHub, J as readYaml, K as refIndex, M as transcriptTitle, S as readSession, U as dependencyGraph, W as mainlineEdges, a as openInApp, h as setArchivedMany, ht as WriteError, j as traceTranscript, m as ARCHIVE_KINDS, mt as DATA_DIR, pt as ConflictError, x as loadSessions } from "./host-79T-_NMu.js";
3
3
  import { t as listProgress } from "./progress-1BfPg2zN.js";
4
- import { A as syncPaths, O as SYNC_DIR, _ as readDaemon, a as remoteMode, h as callDaemon, k as isSynced, m as DaemonUnavailable, n as hubWriter, r as writtenLines, y as readRemote } from "./operation-B2pq2Q9z.js";
5
- import { l as CLI_VERSION } from "./launch-BemxyV9P.js";
4
+ import { A as syncPaths, O as SYNC_DIR, _ as readDaemon, a as remoteMode, h as callDaemon, k as isSynced, m as DaemonUnavailable, n as hubWriter, r as writtenLines, y as readRemote } from "./operation--fJnUqvA.js";
5
+ import { l as CLI_VERSION } from "./launch-DQVyIbpf.js";
6
6
  import { createRequire } from "node:module";
7
7
  import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, realpathSync, statSync, watch } from "node:fs";
8
8
  import { basename, dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mainflow",
3
- "version": "0.33.0",
3
+ "version": "0.34.0-alpha.1",
4
4
  "description": "本機多 AI 協作的主脈絡任務管理:CLI 與介面",
5
5
  "license": "MIT",
6
6
  "type": "module",