mainflow 0.10.0 → 0.11.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 +2 -2
- package/dist/hooks.js +9 -7
- package/dist/{host-BFGfpTDq.js → host-ByGN9mGS.js} +66 -26
- package/dist/host.js +1 -1
- package/dist/{launch-D5uHpqkh.js → launch-B9QrvwZQ.js} +2 -2
- package/dist/launch.js +1 -1
- package/dist/{ui-DbEmJiwO.js → ui-BY65lR7x.js} +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { A as setSessionStatus, C as updateTask, D as readSession, F as sessionBriefing, G as planIssue, H as closeIssue, J as setFindingStatus, K as reopenIssue, M as startSession, O as setSessionArchived, P as updateAction, S as supersedeDecision, U as createFinding, V as appendOps, W as createIssue, X as HubError, Y as setIssueArchived, Z as loadHub, _ as setArchived, b as setMainlineStatus, c as migrateHub, d as addRepo, et as MAINLINE_FILE, f as createDecision, g as initHub, h as dropChecklistItem, l as acceptDecision, m as createTask, ot as findHubRoot, p as createMainline, q as setFindingArchived, rt as WriteError, tt as TASK_FILE, u as addHub, v as setChecklistItem, w as addAction, y as setCriterion } from "./host-ByGN9mGS.js";
|
|
3
3
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
4
4
|
import { COMMAND_SPECS, OPTIONS } from "./facts.js";
|
|
5
5
|
import { runHook } from "./hooks.js";
|
|
6
|
-
import { c as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-
|
|
6
|
+
import { c as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-B9QrvwZQ.js";
|
|
7
7
|
import { readFileSync, realpathSync } from "node:fs";
|
|
8
8
|
import { join, relative } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
package/dist/hooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as setSessionStatus, D as readSession, F as sessionBriefing, I as turnReminder, L as decorateReply, R as endsWithMarker, T as ensureSession, X as
|
|
1
|
+
import { A as setSessionStatus, B as mayDecorate, D as readSession, F as sessionBriefing, I as turnReminder, L as decorateReply, R as endsWithMarker, T as ensureSession, X as HubError, Z as loadHub, at as currentBranch, i as hostName, it as DATA_DIR, j as settleSession, k as setSessionInfo, ot as findHubRoot, st as worktreesOf, z as headerLine } from "./host-ByGN9mGS.js";
|
|
2
2
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
//#region src/hooks.ts
|
|
@@ -55,8 +55,9 @@ const settleOnEnd = (root, { payload }) => {
|
|
|
55
55
|
/** How a hub's interface is linked from a reply: through its /open route, which shows the address in the app. */
|
|
56
56
|
const openLink = (hub) => (path) => `https://${hostName(hub)}/open${path}`;
|
|
57
57
|
/**
|
|
58
|
-
* MessageDisplay: link the references in the lines about to be shown and expand
|
|
59
|
-
*
|
|
58
|
+
* MessageDisplay: link the references in the lines about to be shown and expand the trailing header line into
|
|
59
|
+
* the mainline › task › action its ids name, titles included, whatever state that action is in now. Display
|
|
60
|
+
* only, so the model keeps reading what it wrote. Text with nothing to change
|
|
60
61
|
* returns before the hub is loaded, because Claude Code holds every batch until the hook answers.
|
|
61
62
|
*/
|
|
62
63
|
const displayReply = (root, { payload }) => {
|
|
@@ -75,18 +76,19 @@ const displayReply = (root, { payload }) => {
|
|
|
75
76
|
} });
|
|
76
77
|
};
|
|
77
78
|
/**
|
|
78
|
-
* Stop: settle the session, then ask for the
|
|
79
|
-
* turn without one. Only the model knows which message closes a turn, so the
|
|
79
|
+
* Stop: settle the session, then ask for the header line when a session with an action in progress ended its
|
|
80
|
+
* turn without one. Only the model knows which message closes a turn, so the header is its to write; this makes
|
|
80
81
|
* a forgotten one cost a single extra message. stop_hook_active stops it from asking twice in a row.
|
|
81
82
|
*/
|
|
82
83
|
const settleAndCheckMarker = (root, context) => {
|
|
83
84
|
settleOnEnd(root, context);
|
|
84
85
|
const { payload } = context;
|
|
85
86
|
if (!payload.session_id || payload.stop_hook_active || endsWithMarker(payload.last_assistant_message ?? "")) return null;
|
|
86
|
-
|
|
87
|
+
const header = headerLine(readSession(root, claudeCodeSessionId(payload.session_id)));
|
|
88
|
+
if (!header) return null;
|
|
87
89
|
return JSON.stringify({ hookSpecificOutput: {
|
|
88
90
|
hookEventName: "Stop",
|
|
89
|
-
additionalContext: `[mainflow]
|
|
91
|
+
additionalContext: `[mainflow] 這一輪最後一則回覆的最後一行不是標頭。請只回一則內容為「${header}」的訊息,不加其他文字。`
|
|
90
92
|
} });
|
|
91
93
|
};
|
|
92
94
|
const CLAUDE_CODE_EVENTS = {
|
|
@@ -936,38 +936,76 @@ function linkMentions(text, context) {
|
|
|
936
936
|
return path ? `[${ref}](${context.link(path)})` : all;
|
|
937
937
|
})).join("");
|
|
938
938
|
}
|
|
939
|
-
/** `▸ m001 title › t001 title › a001 do
|
|
940
|
-
function
|
|
941
|
-
const action = context.session?.actions.find((a) => a.status === "doing");
|
|
942
|
-
if (!context.session || !action) return null;
|
|
939
|
+
/** `▸ m001 title › t001 title › a001 do` for the given ids, each linked; titles come from the hub and the session. */
|
|
940
|
+
function headerLineFor(ids, context) {
|
|
943
941
|
const index = refIndex(context.hub);
|
|
944
942
|
const parts = [];
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
const ref = `${served.mainline}/${served.task}`;
|
|
951
|
-
parts.push(`[${served.task}](${context.link(`/mainlines/${served.mainline}/tasks/${served.task}`)}) ${index.get(ref)?.title ?? ""}`.trimEnd());
|
|
943
|
+
if (ids.mainline) {
|
|
944
|
+
parts.push(`[${ids.mainline}](${context.link(`/mainlines/${ids.mainline}`)}) ${index.get(ids.mainline)?.title ?? ""}`.trimEnd());
|
|
945
|
+
if (ids.task) {
|
|
946
|
+
const ref = `${ids.mainline}/${ids.task}`;
|
|
947
|
+
parts.push(`[${ids.task}](${context.link(`/mainlines/${ids.mainline}/tasks/${ids.task}`)}) ${index.get(ref)?.title ?? ""}`.trimEnd());
|
|
952
948
|
}
|
|
953
949
|
}
|
|
954
|
-
|
|
950
|
+
const action = context.session?.actions.find((a) => a.id === ids.action);
|
|
951
|
+
parts.push(context.session ? `[${ids.action}](${context.link(sessionPath(context.session.id))}) ${action?.do ?? ""}`.trimEnd() : ids.action);
|
|
955
952
|
return `▸ ${parts.join(" › ")}`;
|
|
956
953
|
}
|
|
957
|
-
/**
|
|
958
|
-
|
|
954
|
+
/** The ids of the session's action in progress; null when there is none. */
|
|
955
|
+
function currentIds(session) {
|
|
956
|
+
const action = session?.actions.find((a) => a.status === "doing");
|
|
957
|
+
if (!action) return null;
|
|
958
|
+
const served = action.serves.map((s) => parseRef(s)).find((r) => r !== null);
|
|
959
|
+
if (served && "mainline" in served) return {
|
|
960
|
+
mainline: served.mainline,
|
|
961
|
+
task: "task" in served ? served.task : void 0,
|
|
962
|
+
action: action.id
|
|
963
|
+
};
|
|
964
|
+
return { action: action.id };
|
|
965
|
+
}
|
|
966
|
+
/** The current action line for a bare marker; null when the session has no action in progress. */
|
|
967
|
+
function currentActionLine(context) {
|
|
968
|
+
const ids = currentIds(context.session);
|
|
969
|
+
return ids ? headerLineFor(ids, context) : null;
|
|
970
|
+
}
|
|
971
|
+
/** The header line the model writes at the end of a turn, e.g. `▸ m001/t001 · a001`; null without an action in progress. */
|
|
972
|
+
function headerLine(session) {
|
|
973
|
+
const ids = currentIds(session);
|
|
974
|
+
if (!ids) return null;
|
|
975
|
+
const where = ids.mainline ? ids.task ? `${ids.mainline}/${ids.task}` : ids.mainline : null;
|
|
976
|
+
return `▸ ${where ? `${where} · ` : ""}${ids.action}`;
|
|
977
|
+
}
|
|
978
|
+
/** The ids in a header line the model wrote, or null for a bare marker or a line that does not parse. */
|
|
979
|
+
const WRITTEN = new RegExp(`^\\s*▸\\s+(?:(m\\d{3})(?:\\/(t\\d{3}))?\\s*·\\s*)?(a\\d{3})\\s*$`);
|
|
980
|
+
function parseHeader(line) {
|
|
981
|
+
const m = WRITTEN.exec(line);
|
|
982
|
+
return m ? {
|
|
983
|
+
mainline: m[1],
|
|
984
|
+
task: m[2],
|
|
985
|
+
action: m[3]
|
|
986
|
+
} : null;
|
|
987
|
+
}
|
|
988
|
+
/** A line that is the header: the marker alone, or the marker followed by the ids the model copied. */
|
|
989
|
+
const HEADER = new RegExp(`^[ \\t]*▸(?:[ \\t].*)?$`);
|
|
990
|
+
/** The last non-empty line, if it is the header line. */
|
|
991
|
+
const lastHeader = (text) => {
|
|
992
|
+
const last = text.trimEnd().split("\n").at(-1) ?? "";
|
|
993
|
+
return HEADER.test(last) ? last : null;
|
|
994
|
+
};
|
|
995
|
+
/** Whether a message ends with the header line, as the turn's last message should. */
|
|
996
|
+
const endsWithMarker = (text) => lastHeader(text) !== null;
|
|
959
997
|
/**
|
|
960
|
-
* The text to display for a piece of a reply: mentions linked, and a trailing
|
|
961
|
-
*
|
|
998
|
+
* The text to display for a piece of a reply: mentions linked, and a trailing header line replaced with the
|
|
999
|
+
* mainline › task › action it names, titles and links included. A bare marker names the session's action in
|
|
1000
|
+
* progress and is dropped without one. Returns the input unchanged when there is nothing to do.
|
|
962
1001
|
*/
|
|
963
1002
|
function decorateReply(text, context) {
|
|
964
|
-
|
|
965
|
-
if (
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}
|
|
970
|
-
return shown;
|
|
1003
|
+
const header = lastHeader(text);
|
|
1004
|
+
if (header === null) return linkMentions(text, context);
|
|
1005
|
+
const body = linkMentions(text.trimEnd().slice(0, -header.length).trimEnd(), context);
|
|
1006
|
+
const written = parseHeader(header);
|
|
1007
|
+
const line = written ? headerLineFor(written, context) : header.trim() === "▸" ? currentActionLine(context) : linkMentions(header.trim(), context);
|
|
1008
|
+
return line ? `${body}\n\n${line}\n` : `${body}\n`;
|
|
971
1009
|
}
|
|
972
1010
|
/** Cheap test for text that decorateReply could change, so a display hook can skip loading the hub. */
|
|
973
1011
|
const mayDecorate = (text) => /m\d{3}|[if]\d{3}|\.mainflow\/mainlines/.test(text) || endsWithMarker(text);
|
|
@@ -1039,9 +1077,11 @@ function turnReminder(root, session, options) {
|
|
|
1039
1077
|
if (session.actions.length === 0) return null;
|
|
1040
1078
|
const doing = current(session);
|
|
1041
1079
|
const open = session.actions.filter((a) => a.status === "todo" || a.status === "doing");
|
|
1080
|
+
const head = doing ? `[mainflow] 目前行動 ${doing.id} ${doing.do},服務 ${doing.serves.join("、") || "(未指定)"}。這一輪的工作若不屬於它,先調整佇列再做。` : open.length === 0 ? `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${options.cli} action add 列出下一個行動。` : `[mainflow] ${session.id} 目前沒有進行中的行動。`;
|
|
1081
|
+
const header = headerLine(session);
|
|
1042
1082
|
return [
|
|
1043
|
-
|
|
1044
|
-
...
|
|
1083
|
+
head,
|
|
1084
|
+
...header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [],
|
|
1045
1085
|
...queueWarnings(root, session, options.cli).map((w) => `- ${w}`)
|
|
1046
1086
|
].join("\n");
|
|
1047
1087
|
}
|
|
@@ -1834,4 +1874,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
|
|
|
1834
1874
|
return true;
|
|
1835
1875
|
}
|
|
1836
1876
|
//#endregion
|
|
1837
|
-
export {
|
|
1877
|
+
export { refIndex as $, setSessionStatus as A, mayDecorate as B, updateTask as C, readSession as D, loadSessions as E, sessionBriefing as F, planIssue as G, closeIssue as H, turnReminder as I, setFindingStatus as J, reopenIssue as K, decorateReply as L, startSession as M, transcriptPath as N, setSessionArchived as O, updateAction as P, linksInto as Q, endsWithMarker as R, supersedeDecision as S, ensureSession as T, createFinding as U, appendOps as V, createIssue as W, HubError as X, setIssueArchived as Y, loadHub as Z, setArchived as _, openInApp as a, currentBranch as at, setMainlineStatus as b, migrateHub as c, addRepo as d, MAINLINE_FILE as et, createDecision as f, initHub as g, dropChecklistItem as h, hostName as i, DATA_DIR as it, settleSession as j, setSessionInfo as k, acceptDecision as l, createTask as m, DEFAULT_UI_PORT as n, ConflictError as nt, siteBlock as o, findHubRoot as ot, createMainline as p, setFindingArchived as q, caddySites as r, WriteError as rt, syncHost as s, worktreesOf as st, APP_BUNDLE_ID as t, TASK_FILE as tt, addHub as u, setChecklistItem as v, addAction as w, setMainlineTitle as x, setCriterion as y, headerLine as z };
|
package/dist/host.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as openInApp, i as hostName, n as DEFAULT_UI_PORT, o as siteBlock, r as caddySites, s as syncHost, t as APP_BUNDLE_ID } from "./host-
|
|
1
|
+
import { a as openInApp, i as hostName, n as DEFAULT_UI_PORT, o as siteBlock, r as caddySites, s as syncHost, t as APP_BUNDLE_ID } from "./host-ByGN9mGS.js";
|
|
2
2
|
export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, siteBlock, syncHost };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { it as DATA_DIR, n as DEFAULT_UI_PORT, rt as WriteError, s as syncHost } from "./host-ByGN9mGS.js";
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -162,7 +162,7 @@ async function launchUi(root, hub, configured, options) {
|
|
|
162
162
|
}
|
|
163
163
|
let server;
|
|
164
164
|
if (!reuse) {
|
|
165
|
-
const { serveUi } = await import("./ui-
|
|
165
|
+
const { serveUi } = await import("./ui-BY65lR7x.js");
|
|
166
166
|
server = await serveUi(root, choice.port);
|
|
167
167
|
writeUiRecord(root, {
|
|
168
168
|
port: choice.port,
|
package/dist/launch.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as portCandidates, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-
|
|
1
|
+
import { a as portCandidates, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-B9QrvwZQ.js";
|
|
2
2
|
export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates, readUiRecord, startUiInBackground };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { C as updateTask, D as readSession, E as loadSessions,
|
|
1
|
+
import { $ as refIndex, C as updateTask, D as readSession, E as loadSessions, N as transcriptPath, O as setSessionArchived, Q as linksInto, Y as setIssueArchived, Z as loadHub, _ as setArchived, a as openInApp, b as setMainlineStatus, it as DATA_DIR, nt as ConflictError, q as setFindingArchived, rt as WriteError, v as setChecklistItem, x as setMainlineTitle, y as setCriterion } from "./host-ByGN9mGS.js";
|
|
2
2
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
3
|
-
import { c as CLI_VERSION } from "./launch-
|
|
3
|
+
import { c as CLI_VERSION } from "./launch-B9QrvwZQ.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, watch } from "node:fs";
|
|
6
6
|
import { basename, dirname, extname, join, normalize, relative, sep } from "node:path";
|