mainflow 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hooks.js CHANGED
@@ -1,5 +1,6 @@
1
- import { $ as appendOps, F as ensureSession, H as setSessionStatus, J as matchGrants, L as loadSessions, M as sessionBriefing, N as turnReminder, Q as mayDecorate, R as readSession, St as worktreesOf, U as settleSession, V as setSessionInfo, X as endsWithMarker, Y as decorateReply, Z as headerLine, bt as currentBranch, ct as HubError, i as hostName, lt as loadHub, q as grantScope, vt as readYaml, xt as findHubRoot, yt as DATA_DIR, z as setReminderDigest } from "./host-CeN3dvXg.js";
1
+ import { D as endsWithMarker, E as decorateReply, G as loadHub, O as headerLine, S as readSession, T as matchGrants, W as HubError, b as turnReminder, et as readYaml, i as hostName, it as worktreesOf, k as mayDecorate, nt as currentBranch, ot as DATA_DIR, rt as findHubRoot, w as grantScope, x as loadSessions, y as sessionBriefing } from "./host-BJnrv8xF.js";
2
2
  import { Z as describeRequest, j as configSchema } from "./dist-BbamrIPW.js";
3
+ import { n as hubWriter, r as writtenLines } from "./operation-Bp4U2bx3.js";
3
4
  import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
4
5
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
5
6
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
@@ -74,24 +75,27 @@ const claudeCodeSessionId = (raw) => `claude-code-${shortId(raw)}`;
74
75
  * bring an ended or archived one back, since the viewer is working in it again. Its environment facts are
75
76
  * written when it is created and whenever `refresh` asks, such as on SessionStart after a resume.
76
77
  */
77
- function recordSession(root, payload, id, refresh) {
78
+ async function recordSession(write, root, payload, id, refresh) {
78
79
  const cwd = payload.cwd ?? root;
79
- const { session, created } = ensureSession(root, {
80
+ const { session, created } = (await write("session.ensure", {
80
81
  id,
81
82
  agent: "claude-code",
82
83
  cwd
83
- });
84
+ })).value;
84
85
  if (!created && !refresh) return session;
85
- return setSessionInfo(root, id, {
86
- transcript: payload.transcript_path,
87
- worktree: worktreesOf(cwd)?.current,
88
- branch: currentBranch(cwd) ?? void 0,
89
- effort: payload.effort?.level
90
- });
86
+ return (await write("session.info", {
87
+ id,
88
+ info: {
89
+ transcript: payload.transcript_path,
90
+ worktree: worktreesOf(cwd)?.current,
91
+ branch: currentBranch(cwd) ?? void 0,
92
+ effort: payload.effort?.level
93
+ }
94
+ })).value;
91
95
  }
92
96
  /** Settle the main session by the rules settleSession states; prints nothing. */
93
- const settleOnEnd = (root, { payload }) => {
94
- if (payload.session_id) settleSession(root, claudeCodeSessionId(payload.session_id));
97
+ const settleOnEnd = async (_root, { payload, write }) => {
98
+ if (payload.session_id) await write("session.settle", { id: claudeCodeSessionId(payload.session_id) });
95
99
  return null;
96
100
  };
97
101
  /** How a hub's interface is linked from a reply: through its /open route, which shows the address in the app. */
@@ -193,21 +197,24 @@ function missingHeaderRequest(root, { payload, cli }) {
193
197
  * stops it from asking twice in a row. A session asked to list an action is not settled yet: settling removes a
194
198
  * session with no actions, and the action it is asked to add needs the session to exist.
195
199
  */
196
- const settleAndCheckMarker = (root, context) => {
200
+ const settleAndCheckMarker = async (root, context) => {
197
201
  const request = missingHeaderRequest(root, context);
198
202
  const session = context.payload.session_id ? readSession(root, claudeCodeSessionId(context.payload.session_id)) : null;
199
- if (!request || session && session.actions.length > 0) settleOnEnd(root, context);
203
+ if (!request || session && session.actions.length > 0) await settleOnEnd(root, context);
200
204
  return request ? JSON.stringify({ hookSpecificOutput: {
201
205
  hookEventName: "Stop",
202
206
  additionalContext: request
203
207
  } }) : null;
204
208
  };
205
209
  const CLAUDE_CODE_EVENTS = {
206
- "session-start": (root, { payload, envFile, cli, startUi }) => {
210
+ "session-start": async (root, { payload, envFile, cli, startUi, write }) => {
207
211
  if (!payload.session_id) return null;
208
212
  const id = claudeCodeSessionId(payload.session_id);
209
- const session = recordSession(root, payload, id, true);
210
- setReminderDigest(root, id, null);
213
+ const session = await recordSession(write, root, payload, id, true);
214
+ await write("session.reminder", {
215
+ id,
216
+ digest: null
217
+ });
211
218
  if (envFile) appendFileSync(envFile, `export MAINFLOW_SESSION=${id}\n`);
212
219
  startUi?.(root);
213
220
  return sessionBriefing(root, session, {
@@ -215,16 +222,26 @@ const CLAUDE_CODE_EVENTS = {
215
222
  envNote: envFile ? "已設定環境變數 MAINFLOW_SESSION,指令不必再帶 --session" : `執行指令時帶 --session ${id}`
216
223
  });
217
224
  },
218
- "user-prompt-submit": (root, { payload, cli }) => {
225
+ "user-prompt-submit": async (root, { payload, cli, write }) => {
219
226
  if (!payload.session_id) return null;
220
- return turnReminder(root, recordSession(root, payload, claudeCodeSessionId(payload.session_id), false), { cli });
227
+ const session = await recordSession(write, root, payload, claudeCodeSessionId(payload.session_id), false);
228
+ const saved = [];
229
+ const text = turnReminder(root, session, {
230
+ cli,
231
+ saveDigest: (digest) => saved.push(write("session.reminder", {
232
+ id: session.id,
233
+ digest
234
+ }).catch(() => void 0))
235
+ });
236
+ await Promise.all(saved);
237
+ return text;
221
238
  },
222
239
  stop: settleAndCheckMarker,
223
240
  "session-end": settleOnEnd,
224
- "subagent-start": (root, { payload }) => {
241
+ "subagent-start": async (root, { payload, write }) => {
225
242
  if (!payload.agent_id) return null;
226
243
  const parent = payload.session_id ? claudeCodeSessionId(payload.session_id) : null;
227
- ensureSession(root, {
244
+ await write("session.ensure", {
228
245
  id: claudeCodeSessionId(payload.agent_id),
229
246
  agent: payload.agent_type ? `claude-code/${payload.agent_type}` : "claude-code",
230
247
  cwd: payload.cwd ?? root,
@@ -232,18 +249,24 @@ const CLAUDE_CODE_EVENTS = {
232
249
  });
233
250
  return null;
234
251
  },
235
- "subagent-stop": (root, { payload }) => {
252
+ "subagent-stop": async (root, { payload, write }) => {
236
253
  const id = payload.agent_id ? claudeCodeSessionId(payload.agent_id) : null;
237
254
  if (!id || !readSession(root, id)) return null;
238
255
  const transcript = payload.transcript_path ? subagentTranscript(payload.transcript_path, payload.agent_id) : void 0;
239
256
  const meta = transcript ? readMeta(transcript) : {};
240
- setSessionInfo(root, id, {
241
- transcript: transcript && existsSync(transcript) ? transcript : void 0,
242
- description: meta.description,
243
- depth: meta.spawnDepth,
244
- summary: brief(payload.last_assistant_message) || void 0
257
+ await write("session.info", {
258
+ id,
259
+ info: {
260
+ transcript: transcript && existsSync(transcript) ? transcript : void 0,
261
+ description: meta.description,
262
+ depth: meta.spawnDepth,
263
+ summary: brief(payload.last_assistant_message) || void 0
264
+ }
265
+ });
266
+ await write("session.set", {
267
+ id,
268
+ status: "ended"
245
269
  });
246
- setSessionStatus(root, id, "ended");
247
270
  return null;
248
271
  },
249
272
  "pre-tool-use": decideByGrants,
@@ -289,9 +312,9 @@ function askWhenGrantable({ payload }, failure) {
289
312
  /** The ops log's fields cannot hold its separator, so notes that contain one are written with a broken bar. */
290
313
  const opsText = (text) => text.replace(/\s\|\s/g, " ¦ ").replace(/\s+/g, " ").slice(0, 200);
291
314
  /** Log the allowed operation with the grant that covered it; a failure only warns, the call stays allowed. */
292
- function logGranted(root, scope, call, request, grant, warn) {
315
+ async function logGranted(write, scope, call, request, grant, warn) {
293
316
  try {
294
- appendOps(root, {
317
+ await write("ops.add", {
295
318
  target: request.action === "publish" ? `npm ${request.name}` : `github release ${request.repos.join("/")}`,
296
319
  operation: opsText(call.text),
297
320
  result: `授權放行 ${grant.ref}(${describeRequest(request)})`,
@@ -309,7 +332,7 @@ function logGranted(root, scope, call, request, grant, warn) {
309
332
  * print nothing, so other hooks and the usual permission rules decide them. Any failure asks: the caller turns a
310
333
  * throw into the fallback above.
311
334
  */
312
- function decideByGrants(root, { payload, warn }) {
335
+ async function decideByGrants(root, { payload, warn, write }) {
313
336
  const scan = grantableCall(payload);
314
337
  if (!scan) return null;
315
338
  if (scan.kind === "refused") return askBecause(scan.reason);
@@ -329,7 +352,7 @@ function decideByGrants(root, { payload, warn }) {
329
352
  const match = matchGrants(scoped.scope, [resolved.request]);
330
353
  if (!match.ok) return askBecause(match.reason);
331
354
  const { grant } = match.matches[0];
332
- logGranted(root, scoped.scope, scan.call, resolved.request, grant, warn);
355
+ await logGranted(write, scoped.scope, scan.call, resolved.request, grant, warn);
333
356
  return permission("allow", `主脈絡授權 ${grant.ref} 涵蓋 ${describeRequest(resolved.request)}`);
334
357
  }
335
358
  /** How many problems outside the written file the reminder lists before it only gives the count. */
@@ -417,7 +440,7 @@ function hookFallback(platform, event, ctx, failure) {
417
440
  * failure is reported on stderr while the caller still exits 0. Events listed in the fallbacks print their
418
441
  * fallback instead of staying silent.
419
442
  */
420
- function runHook(platform, event, ctx) {
443
+ async function runHook(platform, event, ctx) {
421
444
  const handler = HOOK_PLATFORMS[platform]?.[event];
422
445
  if (!handler) return {
423
446
  stdout: null,
@@ -431,10 +454,18 @@ function runHook(platform, event, ctx) {
431
454
  stdout: hookFallback(platform, event, ctx, { kind: "outside-hub" }),
432
455
  error: null
433
456
  };
457
+ const warn = (message) => warnings.push(message);
458
+ const writer = ctx.write ?? hubWriter(root);
459
+ const write = async (op, input) => {
460
+ const written = await writer(op, input);
461
+ for (const line of writtenLines(written)) if (line.startsWith("!")) warn(`mainflow hook ${platform} ${event}: ${line.slice(2)}`);
462
+ return written;
463
+ };
434
464
  return {
435
- stdout: handler(root, {
465
+ stdout: await handler(root, {
436
466
  ...ctx,
437
- warn: (message) => warnings.push(message)
467
+ warn,
468
+ write
438
469
  }),
439
470
  error: errorText()
440
471
  };