borgmcp 1.0.6 → 1.0.8
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 +5 -3
- package/dist/assimilate-cmd.js +39 -511
- package/dist/assimilate-deps.js +3 -177
- package/dist/assimilate-welcome.js +2 -24
- package/dist/auth-env.js +1 -107
- package/dist/auth.js +23 -612
- package/dist/claude.js +11 -281
- package/dist/cli-help.js +29 -50
- package/dist/cli-platform.js +4 -94
- package/dist/codex-app-server.js +4 -228
- package/dist/codex-app-wake.js +2 -122
- package/dist/codex-launch.js +1 -81
- package/dist/codex-remote.js +1 -250
- package/dist/config-utils.js +3 -385
- package/dist/config.js +1 -190
- package/dist/console-prefix.js +1 -86
- package/dist/cube-name.js +1 -65
- package/dist/cubes.js +4 -269
- package/dist/debug.js +1 -71
- package/dist/device-auth.js +1 -167
- package/dist/direct-log.js +1 -11
- package/dist/health-beat.js +1 -168
- package/dist/inbox-monitor.js +1 -129
- package/dist/index.js +26 -1378
- package/dist/lifecycle-log-guard.js +2 -93
- package/dist/list-roles-render.js +6 -39
- package/dist/log-audit.js +3 -186
- package/dist/log-stream.js +9 -848
- package/dist/name-validator.js +1 -22
- package/dist/parse-assimilate-args.js +1 -82
- package/dist/postinstall.js +8 -22
- package/dist/regen-format.js +11 -337
- package/dist/regen.js +5 -83
- package/dist/remote-client.d.ts +4 -7
- package/dist/remote-client.js +1 -695
- package/dist/role-resolver.js +1 -36
- package/dist/role-section.js +8 -208
- package/dist/roster-render.js +3 -96
- package/dist/setup.js +41 -251
- package/dist/shell-escape.js +1 -22
- package/dist/spawn.js +10 -29
- package/dist/stale-version-check.js +1 -102
- package/dist/stream-owner.js +2 -202
- package/dist/stream-status.js +3 -211
- package/dist/subscription-retry.js +1 -23
- package/dist/sync-roles-render.js +3 -118
- package/dist/sync.js +22 -286
- package/dist/templates.js +120 -626
- package/dist/terminal-title.js +1 -68
- package/dist/token-crypto.js +1 -91
- package/dist/token-store.js +1 -222
- package/dist/types.d.ts +0 -5
- package/dist/types.js +0 -5
- package/dist/version.js +2 -78
- package/dist/worktree-lifecycle.js +2 -173
- package/package.json +12 -2
- package/dist/assimilate-cmd.d.ts.map +0 -1
- package/dist/assimilate-cmd.js.map +0 -1
- package/dist/assimilate-deps.d.ts.map +0 -1
- package/dist/assimilate-deps.js.map +0 -1
- package/dist/assimilate-welcome.d.ts.map +0 -1
- package/dist/assimilate-welcome.js.map +0 -1
- package/dist/auth-env.d.ts.map +0 -1
- package/dist/auth-env.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/claude.d.ts.map +0 -1
- package/dist/claude.js.map +0 -1
- package/dist/cli-help.d.ts.map +0 -1
- package/dist/cli-help.js.map +0 -1
- package/dist/cli-platform.d.ts.map +0 -1
- package/dist/cli-platform.js.map +0 -1
- package/dist/codex-app-server.d.ts.map +0 -1
- package/dist/codex-app-server.js.map +0 -1
- package/dist/codex-app-wake.d.ts.map +0 -1
- package/dist/codex-app-wake.js.map +0 -1
- package/dist/codex-launch.d.ts.map +0 -1
- package/dist/codex-launch.js.map +0 -1
- package/dist/codex-remote.d.ts.map +0 -1
- package/dist/codex-remote.js.map +0 -1
- package/dist/config-utils.d.ts.map +0 -1
- package/dist/config-utils.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/console-prefix.d.ts.map +0 -1
- package/dist/console-prefix.js.map +0 -1
- package/dist/cube-name.d.ts.map +0 -1
- package/dist/cube-name.js.map +0 -1
- package/dist/cubes.d.ts.map +0 -1
- package/dist/cubes.js.map +0 -1
- package/dist/debug.d.ts.map +0 -1
- package/dist/debug.js.map +0 -1
- package/dist/device-auth.d.ts.map +0 -1
- package/dist/device-auth.js.map +0 -1
- package/dist/direct-log.d.ts.map +0 -1
- package/dist/direct-log.js.map +0 -1
- package/dist/health-beat.d.ts.map +0 -1
- package/dist/health-beat.js.map +0 -1
- package/dist/inbox-monitor.d.ts.map +0 -1
- package/dist/inbox-monitor.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycle-log-guard.d.ts.map +0 -1
- package/dist/lifecycle-log-guard.js.map +0 -1
- package/dist/list-roles-render.d.ts.map +0 -1
- package/dist/list-roles-render.js.map +0 -1
- package/dist/log-audit.d.ts.map +0 -1
- package/dist/log-audit.js.map +0 -1
- package/dist/log-stream.d.ts.map +0 -1
- package/dist/log-stream.js.map +0 -1
- package/dist/name-validator.d.ts.map +0 -1
- package/dist/name-validator.js.map +0 -1
- package/dist/parse-assimilate-args.d.ts.map +0 -1
- package/dist/parse-assimilate-args.js.map +0 -1
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js.map +0 -1
- package/dist/regen-format.d.ts.map +0 -1
- package/dist/regen-format.js.map +0 -1
- package/dist/regen.d.ts.map +0 -1
- package/dist/regen.js.map +0 -1
- package/dist/remote-client.d.ts.map +0 -1
- package/dist/remote-client.js.map +0 -1
- package/dist/role-resolver.d.ts.map +0 -1
- package/dist/role-resolver.js.map +0 -1
- package/dist/role-section.d.ts.map +0 -1
- package/dist/role-section.js.map +0 -1
- package/dist/roster-render.d.ts.map +0 -1
- package/dist/roster-render.js.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/shell-escape.d.ts.map +0 -1
- package/dist/shell-escape.js.map +0 -1
- package/dist/spawn.d.ts.map +0 -1
- package/dist/spawn.js.map +0 -1
- package/dist/stale-version-check.d.ts.map +0 -1
- package/dist/stale-version-check.js.map +0 -1
- package/dist/stream-owner.d.ts.map +0 -1
- package/dist/stream-owner.js.map +0 -1
- package/dist/stream-status.d.ts.map +0 -1
- package/dist/stream-status.js.map +0 -1
- package/dist/subscription-retry.d.ts.map +0 -1
- package/dist/subscription-retry.js.map +0 -1
- package/dist/sync-roles-render.d.ts.map +0 -1
- package/dist/sync-roles-render.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js.map +0 -1
- package/dist/terminal-title.d.ts.map +0 -1
- package/dist/terminal-title.js.map +0 -1
- package/dist/token-crypto.d.ts.map +0 -1
- package/dist/token-crypto.js.map +0 -1
- package/dist/token-store.d.ts.map +0 -1
- package/dist/token-store.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/dist/worktree-lifecycle.d.ts.map +0 -1
- package/dist/worktree-lifecycle.js.map +0 -1
|
@@ -1,93 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
const STATE_FILE = join(homedir(), '.config', 'borgmcp', 'lifecycle-log-state.json');
|
|
5
|
-
const ARRIVAL_DUPLICATE_WINDOW_MS = 10 * 60 * 1000;
|
|
6
|
-
export function lifecycleSignalForMessage(message) {
|
|
7
|
-
if (message.startsWith('ARRIVAL: '))
|
|
8
|
-
return 'arrival';
|
|
9
|
-
if (message.startsWith('READY: ') &&
|
|
10
|
-
message.includes('capacity clean') &&
|
|
11
|
-
message.includes('awaiting next dispatch')) {
|
|
12
|
-
return 'ready';
|
|
13
|
-
}
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
function stateKey(subject) {
|
|
17
|
-
return `${subject.cubeId}:${subject.droneId}`;
|
|
18
|
-
}
|
|
19
|
-
async function readState() {
|
|
20
|
-
try {
|
|
21
|
-
const raw = await readFile(STATE_FILE, 'utf8');
|
|
22
|
-
const parsed = JSON.parse(raw);
|
|
23
|
-
if (parsed &&
|
|
24
|
-
typeof parsed === 'object' &&
|
|
25
|
-
parsed.entries &&
|
|
26
|
-
typeof parsed.entries === 'object' &&
|
|
27
|
-
!Array.isArray(parsed.entries)) {
|
|
28
|
-
return parsed;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
if (err?.code !== 'ENOENT')
|
|
33
|
-
throw err;
|
|
34
|
-
}
|
|
35
|
-
return { entries: {} };
|
|
36
|
-
}
|
|
37
|
-
async function writeState(state) {
|
|
38
|
-
await mkdir(dirname(STATE_FILE), { recursive: true });
|
|
39
|
-
await writeFile(STATE_FILE, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
|
|
40
|
-
}
|
|
41
|
-
export function shouldSuppressLifecycleLogFromState(message, state, nowMs = Date.now()) {
|
|
42
|
-
const signal = lifecycleSignalForMessage(message);
|
|
43
|
-
if (!signal)
|
|
44
|
-
return { suppress: false, signal: null };
|
|
45
|
-
if (signal === 'arrival') {
|
|
46
|
-
const lastArrivalAt = state?.lastArrival?.at
|
|
47
|
-
? new Date(state.lastArrival.at).getTime()
|
|
48
|
-
: NaN;
|
|
49
|
-
const isRecent = Number.isFinite(lastArrivalAt) &&
|
|
50
|
-
nowMs - lastArrivalAt < ARRIVAL_DUPLICATE_WINDOW_MS;
|
|
51
|
-
return {
|
|
52
|
-
suppress: state?.lastArrival?.message === message && isRecent,
|
|
53
|
-
signal,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
suppress: state?.idleReady?.open === true && state.idleReady.message === message,
|
|
58
|
-
signal,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
export async function shouldSuppressLifecycleLog(subject, message) {
|
|
62
|
-
const state = await readState();
|
|
63
|
-
return shouldSuppressLifecycleLogFromState(message, state.entries[stateKey(subject)]);
|
|
64
|
-
}
|
|
65
|
-
export function nextLifecycleStateAfterLog(message, current, nowIso = new Date().toISOString()) {
|
|
66
|
-
const signal = lifecycleSignalForMessage(message);
|
|
67
|
-
if (signal === 'arrival') {
|
|
68
|
-
return {
|
|
69
|
-
...current,
|
|
70
|
-
lastArrival: { message, at: nowIso },
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
if (signal === 'ready') {
|
|
74
|
-
return {
|
|
75
|
-
...current,
|
|
76
|
-
idleReady: { message, open: true, at: nowIso },
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
if (current?.idleReady?.open) {
|
|
80
|
-
return {
|
|
81
|
-
...current,
|
|
82
|
-
idleReady: { ...current.idleReady, open: false, at: nowIso },
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
return current ?? {};
|
|
86
|
-
}
|
|
87
|
-
export async function recordLifecycleLog(subject, message) {
|
|
88
|
-
const state = await readState();
|
|
89
|
-
const key = stateKey(subject);
|
|
90
|
-
state.entries[key] = nextLifecycleStateAfterLog(message, state.entries[key]);
|
|
91
|
-
await writeState(state);
|
|
92
|
-
}
|
|
93
|
-
//# sourceMappingURL=lifecycle-log-guard.js.map
|
|
1
|
+
import{mkdir as u,readFile as f,writeFile as d}from"node:fs/promises";import{homedir as p}from"node:os";import{dirname as y,join as A}from"node:path";const n=A(p(),".config","borgmcp","lifecycle-log-state.json"),S=600*1e3;function o(e){return e.startsWith("ARRIVAL: ")?"arrival":e.startsWith("READY: ")&&e.includes("capacity clean")&&e.includes("awaiting next dispatch")?"ready":null}function s(e){return`${e.cubeId}:${e.droneId}`}async function l(){try{const e=await f(n,"utf8"),t=JSON.parse(e);if(t&&typeof t=="object"&&t.entries&&typeof t.entries=="object"&&!Array.isArray(t.entries))return t}catch(e){if(e?.code!=="ENOENT")throw e}return{entries:{}}}async function w(e){await u(y(n),{recursive:!0}),await d(n,JSON.stringify(e,null,2)+`
|
|
2
|
+
`,{mode:384})}function L(e,t,r=Date.now()){const i=o(e);if(!i)return{suppress:!1,signal:null};if(i==="arrival"){const a=t?.lastArrival?.at?new Date(t.lastArrival.at).getTime():NaN,c=Number.isFinite(a)&&r-a<S;return{suppress:t?.lastArrival?.message===e&&c,signal:i}}return{suppress:t?.idleReady?.open===!0&&t.idleReady.message===e,signal:i}}async function h(e,t){const r=await l();return L(t,r.entries[s(e)])}function R(e,t,r=new Date().toISOString()){const i=o(e);return i==="arrival"?{...t,lastArrival:{message:e,at:r}}:i==="ready"?{...t,idleReady:{message:e,open:!0,at:r}}:t?.idleReady?.open?{...t,idleReady:{...t.idleReady,open:!1,at:r}}:t??{}}async function N(e,t){const r=await l(),i=s(e);r.entries[i]=R(t,r.entries[i]),await w(r)}export{o as lifecycleSignalForMessage,R as nextLifecycleStateAfterLog,N as recordLifecycleLog,h as shouldSuppressLifecycleLog,L as shouldSuppressLifecycleLogFromState};
|
|
@@ -1,39 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* stack. The handler simply calls `renderRoleList(roles, cubeId)` and
|
|
8
|
-
* returns its output.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Render the role registry for a cube as a markdown list with role IDs
|
|
12
|
-
* exposed for use with `borg:reassign-drone`. Returns the empty-roles
|
|
13
|
-
* placeholder when the input array is empty.
|
|
14
|
-
*
|
|
15
|
-
* Each role line shape:
|
|
16
|
-
* - **<name>**(<tags>) `<uuid>` — <description>
|
|
17
|
-
*
|
|
18
|
-
* Tags collected in order: Queen, human-seat, default, can-broadcast,
|
|
19
|
-
* receives-all-direct. Joined with `, `; suffix omitted entirely when no tags apply.
|
|
20
|
-
*/
|
|
21
|
-
export function renderRoleList(roles, cubeId) {
|
|
22
|
-
if (!roles.length) {
|
|
23
|
-
return 'No roles in this cube yet.';
|
|
24
|
-
}
|
|
25
|
-
const lines = roles.map((r) => {
|
|
26
|
-
const tags = [
|
|
27
|
-
r.role_class === 'queen' ? 'Queen' : null,
|
|
28
|
-
r.is_human_seat ? 'human-seat' : null,
|
|
29
|
-
r.is_default ? 'default' : null,
|
|
30
|
-
r.can_broadcast ? 'can-broadcast' : null,
|
|
31
|
-
r.receives_all_direct ? 'receives-all-direct' : null,
|
|
32
|
-
].filter(Boolean).join(', ');
|
|
33
|
-
const tagSuffix = tags ? ` (${tags})` : '';
|
|
34
|
-
const desc = r.short_description || '_(no description)_';
|
|
35
|
-
return `- **${r.name}**${tagSuffix} \`${r.id}\` — ${desc}`;
|
|
36
|
-
});
|
|
37
|
-
return `Roles in cube ${cubeId} (${roles.length}):\n\n${lines.join('\n')}\n\nUse the role IDs above with \`borg:reassign-drone\` to change a drone's role.`;
|
|
38
|
-
}
|
|
39
|
-
//# sourceMappingURL=list-roles-render.js.map
|
|
1
|
+
function i(n,s){if(!n.length)return"No roles in this cube yet.";const l=n.map(e=>{const t=[e.role_class==="queen"?"Queen":null,e.is_human_seat?"human-seat":null,e.is_default?"default":null,e.can_broadcast?"can-broadcast":null,e.receives_all_direct?"receives-all-direct":null].filter(Boolean).join(", "),o=t?` (${t})`:"",a=e.short_description||"_(no description)_";return`- **${e.name}**${o} \`${e.id}\` \u2014 ${a}`});return`Roles in cube ${s} (${n.length}):
|
|
2
|
+
|
|
3
|
+
${l.join(`
|
|
4
|
+
`)}
|
|
5
|
+
|
|
6
|
+
Use the role IDs above with \`borg:reassign-drone\` to change a drone's role.`}export{i as renderRoleList};
|
package/dist/log-audit.js
CHANGED
|
@@ -1,187 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Domain-agnostic nudge: scans the Claude Code transcript and emits a
|
|
6
|
-
* one-line warning to stdout if the drone has accumulated MATERIAL_THRESHOLD
|
|
7
|
-
* or more state-changing tool calls (Edit / Write / Bash / etc.) since the
|
|
8
|
-
* last borg:log post. Wired in as a UserPromptSubmit hook so the warning
|
|
9
|
-
* becomes additional context for the next turn.
|
|
10
|
-
*
|
|
11
|
-
* Two refinements vs the v1 1-tool threshold (per drone-6's review):
|
|
12
|
-
* 1. Counts material tools across all assistant turns until either the
|
|
13
|
-
* threshold is hit OR a borg:log call is found (cooldown). One
|
|
14
|
-
* diagnostic Bash no longer triggers; substantive work always does.
|
|
15
|
-
* 2. Any borg:log in the scanback suppresses the nudge — so the drone
|
|
16
|
-
* gets a turn of breathing room after each post.
|
|
17
|
-
*
|
|
18
|
-
* Stays generic — knows nothing about git, branches, or any project's
|
|
19
|
-
* conventions. Only the Anthropic tool name `mcp__borg__borg_log` and a
|
|
20
|
-
* small set of canonical mutating tool names. If no cube is active in
|
|
21
|
-
* this project, silently exits.
|
|
22
|
-
*
|
|
23
|
-
* Hook input arrives as JSON on stdin (Claude Code's standard hook
|
|
24
|
-
* contract). The relevant field is `transcript_path`.
|
|
25
|
-
*/
|
|
26
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
27
|
-
import { getActiveCube } from './cubes.js';
|
|
28
|
-
import { handleVersionFlag } from './version.js';
|
|
29
|
-
const MATERIAL_TOOLS = new Set([
|
|
30
|
-
'Edit',
|
|
31
|
-
'Write',
|
|
32
|
-
'MultiEdit',
|
|
33
|
-
'NotebookEdit',
|
|
34
|
-
'Bash',
|
|
35
|
-
'apply_patch',
|
|
36
|
-
'exec_command',
|
|
37
|
-
'functions.exec_command',
|
|
38
|
-
'functions.apply_patch',
|
|
39
|
-
]);
|
|
40
|
-
const LOG_TOOL = 'mcp__borg__borg_log';
|
|
41
|
-
// Number of state-changing tool calls since the last borg:log that the
|
|
42
|
-
// drone is allowed before the audit nudges. 1 false-positives on
|
|
43
|
-
// diagnostic Bash (git status, ls, etc.); 3 has been comfortable in
|
|
44
|
-
// dogfooding — any substantive work crosses it within a turn or two.
|
|
45
|
-
const MATERIAL_THRESHOLD = 3;
|
|
46
|
-
// Cap on how many transcript entries we scan backwards before giving up.
|
|
47
|
-
// Sessions with thousands of turns still resolve in milliseconds at this
|
|
48
|
-
// bound, and anything truly old is no longer "the last span the drone
|
|
49
|
-
// failed to log."
|
|
50
|
-
const MAX_SCAN = 400;
|
|
51
|
-
function isUserPrompt(entry) {
|
|
52
|
-
const type = entry?.type ?? entry?.role;
|
|
53
|
-
if (type !== 'user')
|
|
54
|
-
return false;
|
|
55
|
-
const content = entry?.message?.content ?? entry?.content;
|
|
56
|
-
if (typeof content === 'string')
|
|
57
|
-
return content.trim().length > 0;
|
|
58
|
-
if (!Array.isArray(content))
|
|
59
|
-
return false;
|
|
60
|
-
// A "real" user prompt has at least one text block. A tool_result-only
|
|
61
|
-
// user message is a continuation of an assistant span, not a prompt.
|
|
62
|
-
return content.some((b) => b?.type === 'text');
|
|
63
|
-
}
|
|
64
|
-
function isAssistant(entry) {
|
|
65
|
-
const type = entry?.type ?? entry?.role;
|
|
66
|
-
return type === 'assistant' || type === 'response_item';
|
|
67
|
-
}
|
|
68
|
-
function scanAssistant(entry, state) {
|
|
69
|
-
const payload = entry?.payload;
|
|
70
|
-
const payloadToolName = payload?.type === 'function_call' || payload?.type === 'custom_tool_call'
|
|
71
|
-
? payload.name
|
|
72
|
-
: null;
|
|
73
|
-
if (typeof payloadToolName === 'string') {
|
|
74
|
-
if (payloadToolName === LOG_TOOL) {
|
|
75
|
-
state.loggedRecently = true;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (MATERIAL_TOOLS.has(payloadToolName))
|
|
79
|
-
state.material += 1;
|
|
80
|
-
}
|
|
81
|
-
const content = entry?.message?.content ?? entry?.content ?? [];
|
|
82
|
-
if (!Array.isArray(content))
|
|
83
|
-
return;
|
|
84
|
-
// Walk the blocks newest-first WITHIN the entry. The caller already
|
|
85
|
-
// visits entries newest-first. Counting forward within an entry would
|
|
86
|
-
// either miss post-log material work (if log is in the same entry as
|
|
87
|
-
// later material blocks) or inflate the count with pre-log work that
|
|
88
|
-
// the log already covered. Reversing here keeps "material since the
|
|
89
|
-
// last log" honest at block granularity.
|
|
90
|
-
for (let i = content.length - 1; i >= 0; i--) {
|
|
91
|
-
const block = content[i];
|
|
92
|
-
if (block?.type !== 'tool_use')
|
|
93
|
-
continue;
|
|
94
|
-
if (block.name === LOG_TOOL) {
|
|
95
|
-
state.loggedRecently = true;
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (MATERIAL_TOOLS.has(block.name))
|
|
99
|
-
state.material += 1;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
async function readStdin() {
|
|
103
|
-
if (process.stdin.isTTY)
|
|
104
|
-
return '';
|
|
105
|
-
const chunks = [];
|
|
106
|
-
for await (const chunk of process.stdin)
|
|
107
|
-
chunks.push(chunk);
|
|
108
|
-
return Buffer.concat(chunks).toString('utf-8');
|
|
109
|
-
}
|
|
110
|
-
async function main() {
|
|
111
|
-
handleVersionFlag();
|
|
112
|
-
const raw = await readStdin();
|
|
113
|
-
let input = {};
|
|
114
|
-
if (raw.trim()) {
|
|
115
|
-
try {
|
|
116
|
-
input = JSON.parse(raw);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// No usable input — silent exit.
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (!input.transcript_path || !existsSync(input.transcript_path))
|
|
124
|
-
return;
|
|
125
|
-
if (input.cwd && existsSync(input.cwd)) {
|
|
126
|
-
try {
|
|
127
|
-
process.chdir(input.cwd);
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
// Best-effort only; fall back to the hook process cwd.
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
// Only nudge if there's an active cube in this project. Otherwise the
|
|
134
|
-
// hook is fully inert.
|
|
135
|
-
const active = await getActiveCube();
|
|
136
|
-
if (!active)
|
|
137
|
-
return;
|
|
138
|
-
const lines = readFileSync(input.transcript_path, 'utf-8').split('\n').filter(Boolean);
|
|
139
|
-
if (lines.length === 0)
|
|
140
|
-
return;
|
|
141
|
-
// Walk the transcript backwards from the end. The trailing entry MAY
|
|
142
|
-
// be the user prompt that triggered this hook; skip it if so. From
|
|
143
|
-
// there, accumulate material tool calls until either we hit a borg:log
|
|
144
|
-
// (cooldown — suppress) or we cross MATERIAL_THRESHOLD (nudge). The
|
|
145
|
-
// scan stops after MAX_SCAN entries to bound work on huge sessions.
|
|
146
|
-
let i = lines.length - 1;
|
|
147
|
-
const tail = safeParse(lines[i]);
|
|
148
|
-
if (tail && isUserPrompt(tail))
|
|
149
|
-
i--;
|
|
150
|
-
const state = { material: 0, loggedRecently: false };
|
|
151
|
-
let scanned = 0;
|
|
152
|
-
for (; i >= 0 && scanned < MAX_SCAN; i--, scanned++) {
|
|
153
|
-
const entry = safeParse(lines[i]);
|
|
154
|
-
if (!entry)
|
|
155
|
-
continue;
|
|
156
|
-
if (isAssistant(entry)) {
|
|
157
|
-
scanAssistant(entry, state);
|
|
158
|
-
// Threshold has primacy over cooldown: when both could fire on the
|
|
159
|
-
// same entry (e.g. an entry containing [log, Bash, Bash, Bash]
|
|
160
|
-
// where the reversed scan first counts 3 material blocks before
|
|
161
|
-
// hitting the log), we want the nudge — the post-log material
|
|
162
|
-
// work hasn't been logged yet.
|
|
163
|
-
if (state.material >= MATERIAL_THRESHOLD) {
|
|
164
|
-
process.stdout.write(`Heads up: ${state.material}+ state-changing tool calls since the last \`borg:log\` post. ` +
|
|
165
|
-
'If that work was a substantive unit (a change that ships, a blocker hit, a finding ' +
|
|
166
|
-
"worth sharing), post to the cube log per your role's conventions before continuing.\n");
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (state.loggedRecently)
|
|
170
|
-
return; // cooldown
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
// Reached MAX_SCAN or start of transcript without finding either a
|
|
174
|
-
// log call or enough material work. Silent.
|
|
175
|
-
}
|
|
176
|
-
function safeParse(line) {
|
|
177
|
-
try {
|
|
178
|
-
return JSON.parse(line);
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
main().catch(() => {
|
|
185
|
-
// Never fail a hook — silent on error.
|
|
186
|
-
});
|
|
187
|
-
//# sourceMappingURL=log-audit.js.map
|
|
2
|
+
import{existsSync as l,readFileSync as g}from"node:fs";import{getActiveCube as h}from"./cubes.js";import{handleVersionFlag as m}from"./version.js";const u=new Set(["Edit","Write","MultiEdit","NotebookEdit","Bash","apply_patch","exec_command","functions.exec_command","functions.apply_patch"]),f="mcp__borg__borg_log",d=3,y=400;function _(t){if((t?.type??t?.role)!=="user")return!1;const o=t?.message?.content??t?.content;return typeof o=="string"?o.trim().length>0:Array.isArray(o)?o.some(e=>e?.type==="text"):!1}function A(t){const n=t?.type??t?.role;return n==="assistant"||n==="response_item"}function w(t,n){const o=t?.payload,e=o?.type==="function_call"||o?.type==="custom_tool_call"?o.name:null;if(typeof e=="string"){if(e===f){n.loggedRecently=!0;return}u.has(e)&&(n.material+=1)}const r=t?.message?.content??t?.content??[];if(Array.isArray(r))for(let s=r.length-1;s>=0;s--){const i=r[s];if(i?.type==="tool_use"){if(i.name===f){n.loggedRecently=!0;return}u.has(i.name)&&(n.material+=1)}}}async function b(){if(process.stdin.isTTY)return"";const t=[];for await(const n of process.stdin)t.push(n);return Buffer.concat(t).toString("utf-8")}async function S(){m();const t=await b();let n={};if(t.trim())try{n=JSON.parse(t)}catch{return}if(!n.transcript_path||!l(n.transcript_path))return;if(n.cwd&&l(n.cwd))try{process.chdir(n.cwd)}catch{}if(!await h())return;const e=g(n.transcript_path,"utf-8").split(`
|
|
3
|
+
`).filter(Boolean);if(e.length===0)return;let r=e.length-1;const s=p(e[r]);s&&_(s)&&r--;const i={material:0,loggedRecently:!1};let c=0;for(;r>=0&&c<y;r--,c++){const a=p(e[r]);if(a&&A(a)){if(w(a,i),i.material>=d){process.stdout.write(`Heads up: ${i.material}+ state-changing tool calls since the last \`borg:log\` post. If that work was a substantive unit (a change that ships, a blocker hit, a finding worth sharing), post to the cube log per your role's conventions before continuing.
|
|
4
|
+
`);return}if(i.loggedRecently)return}}}function p(t){try{return JSON.parse(t)}catch{return null}}S().catch(()=>{});
|