@berthdev/berth 0.3.6 → 0.4.3
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 +7 -1
- package/dist/src/agents/capabilities.d.ts +13 -0
- package/dist/src/agents/capabilities.js +52 -0
- package/dist/src/agents/capabilities.js.map +1 -0
- package/dist/src/agents/codex-binding.d.ts +10 -0
- package/dist/src/agents/codex-binding.js +105 -0
- package/dist/src/agents/codex-binding.js.map +1 -0
- package/dist/src/agents/commands.d.ts +4 -2
- package/dist/src/agents/commands.js +41 -9
- package/dist/src/agents/commands.js.map +1 -1
- package/dist/src/agents/copilot-state.d.ts +3 -0
- package/dist/src/agents/copilot-state.js +94 -0
- package/dist/src/agents/copilot-state.js.map +1 -0
- package/dist/src/agents/gemini-state.d.ts +3 -0
- package/dist/src/agents/gemini-state.js +41 -0
- package/dist/src/agents/gemini-state.js.map +1 -0
- package/dist/src/agents/hook-files.d.ts +1 -3
- package/dist/src/agents/hook-files.js +69 -53
- package/dist/src/agents/hook-files.js.map +1 -1
- package/dist/src/agents/hook-health.d.ts +4 -0
- package/dist/src/agents/hook-health.js +95 -0
- package/dist/src/agents/hook-health.js.map +1 -0
- package/dist/src/agents/hooks.d.ts +4 -1
- package/dist/src/agents/hooks.js +153 -21
- package/dist/src/agents/hooks.js.map +1 -1
- package/dist/src/agents/jsonl-titles.d.ts +8 -0
- package/dist/src/agents/jsonl-titles.js +124 -0
- package/dist/src/agents/jsonl-titles.js.map +1 -0
- package/dist/src/agents/late-binding.d.ts +2 -0
- package/dist/src/agents/late-binding.js +25 -0
- package/dist/src/agents/late-binding.js.map +1 -0
- package/dist/src/agents/profile.d.ts +14 -0
- package/dist/src/agents/profile.js +75 -0
- package/dist/src/agents/profile.js.map +1 -0
- package/dist/src/agents/registry.d.ts +12 -3
- package/dist/src/agents/registry.js +52 -19
- package/dist/src/agents/registry.js.map +1 -1
- package/dist/src/agents/roots.d.ts +2 -0
- package/dist/src/agents/roots.js +21 -0
- package/dist/src/agents/roots.js.map +1 -0
- package/dist/src/agents/titles.d.ts +14 -0
- package/dist/src/agents/titles.js +82 -0
- package/dist/src/agents/titles.js.map +1 -0
- package/dist/src/agents/transcript-cursor.d.ts +9 -0
- package/dist/src/agents/transcript-cursor.js +86 -0
- package/dist/src/agents/transcript-cursor.js.map +1 -0
- package/dist/src/cli/main.js +40 -7
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/config/index.d.ts +6 -0
- package/dist/src/config/index.js +37 -6
- package/dist/src/config/index.js.map +1 -1
- package/dist/src/config/write.d.ts +2 -0
- package/dist/src/config/write.js +41 -0
- package/dist/src/config/write.js.map +1 -0
- package/dist/src/context.js +1 -1
- package/dist/src/context.js.map +1 -1
- package/dist/src/index.d.ts +11 -5
- package/dist/src/index.js +10 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/iterm2/install.d.ts +2 -0
- package/dist/src/iterm2/install.js +37 -12
- package/dist/src/iterm2/install.js.map +1 -1
- package/dist/src/present/attach.js +17 -5
- package/dist/src/present/attach.js.map +1 -1
- package/dist/src/present/state.d.ts +3 -0
- package/dist/src/present/state.js +12 -8
- package/dist/src/present/state.js.map +1 -1
- package/dist/src/process-arguments.d.ts +2 -0
- package/dist/src/process-arguments.js +59 -0
- package/dist/src/process-arguments.js.map +1 -0
- package/dist/src/process-directory.d.ts +2 -0
- package/dist/src/process-directory.js +37 -0
- package/dist/src/process-directory.js.map +1 -0
- package/dist/src/process-owner.d.ts +7 -0
- package/dist/src/process-owner.js +69 -0
- package/dist/src/process-owner.js.map +1 -0
- package/dist/src/registry/directory.d.ts +2 -0
- package/dist/src/registry/directory.js +19 -0
- package/dist/src/registry/directory.js.map +1 -0
- package/dist/src/registry/discover.d.ts +15 -5
- package/dist/src/registry/discover.js +142 -38
- package/dist/src/registry/discover.js.map +1 -1
- package/dist/src/registry/doctor.d.ts +5 -1
- package/dist/src/registry/doctor.js +53 -12
- package/dist/src/registry/doctor.js.map +1 -1
- package/dist/src/registry/hosts.d.ts +28 -0
- package/dist/src/registry/hosts.js +73 -0
- package/dist/src/registry/hosts.js.map +1 -0
- package/dist/src/registry/launch.d.ts +3 -0
- package/dist/src/registry/launch.js +29 -0
- package/dist/src/registry/launch.js.map +1 -0
- package/dist/src/registry/native-state.d.ts +19 -0
- package/dist/src/registry/native-state.js +135 -0
- package/dist/src/registry/native-state.js.map +1 -0
- package/dist/src/registry/sessions.d.ts +21 -3
- package/dist/src/registry/sessions.js +179 -38
- package/dist/src/registry/sessions.js.map +1 -1
- package/dist/src/restore/ensure.d.ts +12 -2
- package/dist/src/restore/ensure.js +171 -37
- package/dist/src/restore/ensure.js.map +1 -1
- package/dist/src/restore/lock.d.ts +3 -5
- package/dist/src/restore/lock.js +82 -52
- package/dist/src/restore/lock.js.map +1 -1
- package/dist/src/restore/status.d.ts +9 -0
- package/dist/src/restore/status.js +16 -0
- package/dist/src/restore/status.js.map +1 -0
- package/dist/src/snapshot/identity.d.ts +7 -0
- package/dist/src/snapshot/identity.js +20 -0
- package/dist/src/snapshot/identity.js.map +1 -0
- package/dist/src/snapshot/lifecycle.d.ts +9 -0
- package/dist/src/snapshot/lifecycle.js +37 -0
- package/dist/src/snapshot/lifecycle.js.map +1 -0
- package/dist/src/snapshot/store.d.ts +11 -1
- package/dist/src/snapshot/store.js +28 -9
- package/dist/src/snapshot/store.js.map +1 -1
- package/dist/src/snapshot/take.d.ts +2 -1
- package/dist/src/snapshot/take.js +29 -25
- package/dist/src/snapshot/take.js.map +1 -1
- package/dist/src/tmux/client.d.ts +6 -2
- package/dist/src/tmux/client.js +49 -25
- package/dist/src/tmux/client.js.map +1 -1
- package/dist/src/tmux/connections.d.ts +24 -0
- package/dist/src/tmux/connections.js +86 -0
- package/dist/src/tmux/connections.js.map +1 -0
- package/dist/src/tmux/fields.d.ts +7 -0
- package/dist/src/tmux/fields.js +23 -0
- package/dist/src/tmux/fields.js.map +1 -0
- package/dist/src/tmux/install.js +31 -18
- package/dist/src/tmux/install.js.map +1 -1
- package/dist/src/tools.d.ts +3 -1
- package/dist/src/tools.js +23 -11
- package/dist/src/tools.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/agents/name-key.d.ts +0 -1
- package/dist/src/agents/name-key.js +0 -66
- package/dist/src/agents/name-key.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { agentRoot } from './roots.js';
|
|
2
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { writeConfigFile } from '../config/write.js';
|
|
4
|
+
import { join } from 'node:path';
|
|
4
5
|
import { BerthError, ExitCode } from '../errors.js';
|
|
5
6
|
/**
|
|
6
7
|
* Where and how each agent reads its hooks (spec §8). Every format marks
|
|
@@ -8,7 +9,6 @@ import { BerthError, ExitCode } from '../errors.js';
|
|
|
8
9
|
* only Berth's own entries.
|
|
9
10
|
*/
|
|
10
11
|
export const MARKER = 'BERTH_HOOK=1';
|
|
11
|
-
const home = (env) => env.HOME || homedir();
|
|
12
12
|
function readJson(file) {
|
|
13
13
|
if (!existsSync(file))
|
|
14
14
|
return {};
|
|
@@ -19,11 +19,7 @@ function readJson(file) {
|
|
|
19
19
|
throw new BerthError(`cannot parse ${file}: ${error.message}`);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
24
|
-
writeFileSync(file, text);
|
|
25
|
-
}
|
|
26
|
-
const writeJson = (file, data) => writeText(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
22
|
+
const writeJson = (file, data) => writeConfigFile(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
27
23
|
function withoutBerthNested(groups = []) {
|
|
28
24
|
return groups
|
|
29
25
|
.map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !h.command?.includes(MARKER)) }))
|
|
@@ -98,58 +94,83 @@ function stripTomlBlock(text) {
|
|
|
98
94
|
*/
|
|
99
95
|
const tomlConfig = {
|
|
100
96
|
file(env) {
|
|
101
|
-
|
|
102
|
-
return join(env.KIMI_CODE_HOME, 'config.toml');
|
|
103
|
-
const legacy = join(home(env), '.kimi');
|
|
104
|
-
const current = join(home(env), '.kimi-code');
|
|
105
|
-
return existsSync(legacy) && !existsSync(current) ? join(legacy, 'config.toml') : join(current, 'config.toml');
|
|
97
|
+
return join(agentRoot('kimi', env), 'config.toml');
|
|
106
98
|
},
|
|
107
99
|
install(path, events, commandFor) {
|
|
108
100
|
const base = stripTomlBlock(existsSync(path) ? readFileSync(path, 'utf8') : '');
|
|
109
101
|
const tables = events.map((event) => `[[hooks]]\nevent = ${JSON.stringify(event)}\ncommand = ${JSON.stringify(commandFor(event))}\n`);
|
|
110
102
|
const sep = base && !base.endsWith('\n') ? '\n\n' : base ? '\n' : '';
|
|
111
|
-
|
|
103
|
+
writeConfigFile(path, `${base}${sep}${TOML_BEGIN}\n${tables.join('\n')}${TOML_END}\n`);
|
|
112
104
|
},
|
|
113
105
|
uninstall(path) {
|
|
114
106
|
if (existsSync(path))
|
|
115
|
-
|
|
107
|
+
writeConfigFile(path, stripTomlBlock(readFileSync(path, 'utf8')));
|
|
116
108
|
},
|
|
117
109
|
};
|
|
118
110
|
// ---- OpenCode: a plugin module ------------------------------------------
|
|
119
111
|
/** OpenCode loads every module in its plugins folder; this one forwards session events to `berth hook`. */
|
|
120
|
-
export function opencodePlugin(node, cli, events) {
|
|
112
|
+
export function opencodePlugin(node, cli, events, electron = process.env.ELECTRON_RUN_AS_NODE === '1') {
|
|
121
113
|
return `// ${MARKER}: written by \`berth hooks install opencode\`; remove with \`berth hooks uninstall opencode\`.
|
|
122
114
|
import { spawn } from 'node:child_process';
|
|
123
115
|
|
|
124
116
|
const EVENTS = new Set(${JSON.stringify(events)});
|
|
125
|
-
const
|
|
117
|
+
const roots = new Set();
|
|
118
|
+
const failed = new Set();
|
|
119
|
+
let pending = Promise.resolve();
|
|
126
120
|
|
|
127
|
-
function send(event, id) {
|
|
128
|
-
if (!process.env.TMUX_PANE) return;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
121
|
+
function send(event, id, payload = {}) {
|
|
122
|
+
if (!process.env.TMUX_PANE) return Promise.resolve();
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
try {
|
|
125
|
+
const child = spawn(${JSON.stringify(node)}, [${JSON.stringify(cli)}, 'hook', 'opencode', event], {
|
|
126
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
127
|
+
env: { ...process.env, BERTH_HOOK: '1', ...${JSON.stringify(electron ? { ELECTRON_RUN_AS_NODE: '1' } : {})} },
|
|
128
|
+
});
|
|
129
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 10_000);
|
|
130
|
+
const finish = () => { clearTimeout(timer); resolve(); };
|
|
131
|
+
child.on('error', finish);
|
|
132
|
+
child.on('close', finish);
|
|
133
|
+
child.stdin.on('error', () => {});
|
|
134
|
+
child.stdin.end(JSON.stringify({ ...payload, session_id: id }));
|
|
135
|
+
} catch {
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
141
|
+
async function forward(event) {
|
|
142
|
+
if (!EVENTS.has(event.type)) return;
|
|
143
|
+
const props = event.properties ?? event.data ?? {};
|
|
144
|
+
const identity = event.type === 'session.created' || event.type === 'session.updated';
|
|
145
|
+
const id = props.sessionID ?? (identity ? props.info?.id : undefined);
|
|
146
|
+
if (typeof id !== 'string' || !id) return;
|
|
147
|
+
if (identity) {
|
|
148
|
+
if (!props.info || props.info.id !== id || props.info.parentID) {
|
|
149
|
+
roots.delete(id);
|
|
149
150
|
return;
|
|
150
151
|
}
|
|
151
|
-
|
|
152
|
-
send(event.type, id);
|
|
152
|
+
roots.add(id);
|
|
153
|
+
return send(event.type, id);
|
|
154
|
+
}
|
|
155
|
+
if (!roots.has(id)) return;
|
|
156
|
+
if (event.type === 'session.status') {
|
|
157
|
+
if (!['busy', 'retry'].includes(props.status?.type)) return;
|
|
158
|
+
failed.delete(id);
|
|
159
|
+
}
|
|
160
|
+
const rejected = event.type === 'permission.replied' && props.reply === 'reject';
|
|
161
|
+
if (event.type === 'permission.replied' && !['once', 'always', 'reject'].includes(props.reply)) return;
|
|
162
|
+
if (event.type === 'session.error' || rejected) failed.add(id);
|
|
163
|
+
const type = rejected || (event.type === 'session.idle' && failed.has(id)) ? 'session.error' : event.type;
|
|
164
|
+
const payload = event.type === 'permission.asked' && typeof props.permission === 'string'
|
|
165
|
+
? { message: 'Approval requested: ' + props.permission } : {};
|
|
166
|
+
return send(type, id, payload);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const BerthPlugin = async () => ({
|
|
170
|
+
event: ({ event }) => {
|
|
171
|
+
// Native publishers need not await plugin callbacks; keep child processes ordered.
|
|
172
|
+
pending = pending.then(() => forward(event)).catch(() => {});
|
|
173
|
+
return pending;
|
|
153
174
|
},
|
|
154
175
|
});
|
|
155
176
|
`;
|
|
@@ -157,11 +178,10 @@ export const BerthPlugin = async () => ({
|
|
|
157
178
|
function opencodeFormat(node, cli) {
|
|
158
179
|
return {
|
|
159
180
|
file(env) {
|
|
160
|
-
|
|
161
|
-
return join(config, 'opencode', 'plugins', 'berth.js');
|
|
181
|
+
return join(agentRoot('opencode', env), 'plugins', 'berth.js');
|
|
162
182
|
},
|
|
163
183
|
install(path, events) {
|
|
164
|
-
|
|
184
|
+
writeConfigFile(path, opencodePlugin(node, cli, events));
|
|
165
185
|
},
|
|
166
186
|
uninstall(path) {
|
|
167
187
|
if (existsSync(path) && readFileSync(path, 'utf8').includes(MARKER))
|
|
@@ -173,17 +193,17 @@ function opencodeFormat(node, cli) {
|
|
|
173
193
|
export function hookFormat(target, cli = '') {
|
|
174
194
|
switch (target) {
|
|
175
195
|
case 'claude-settings':
|
|
176
|
-
return nestedJson((env) => join(
|
|
196
|
+
return nestedJson((env) => join(agentRoot('claude', env), 'settings.json'));
|
|
177
197
|
case 'codex-hooks':
|
|
178
|
-
return nestedJson((env) => join(
|
|
198
|
+
return nestedJson((env) => join(agentRoot('codex', env), 'hooks.json'));
|
|
179
199
|
case 'gemini-settings':
|
|
180
|
-
return nestedJson((env) => join(
|
|
200
|
+
return nestedJson((env) => join(agentRoot('gemini', env), 'settings.json'));
|
|
181
201
|
case 'qoder-settings':
|
|
182
|
-
return nestedJson((env) => join(
|
|
202
|
+
return nestedJson((env) => join(agentRoot('qoder', env), 'settings.json'));
|
|
183
203
|
case 'cursor-hooks':
|
|
184
|
-
return flatJson((env) => join(
|
|
204
|
+
return flatJson((env) => join(agentRoot('cursor', env), 'hooks.json'), false);
|
|
185
205
|
case 'copilot-hooks':
|
|
186
|
-
return flatJson((env) => join(
|
|
206
|
+
return flatJson((env) => join(agentRoot('copilot', env), 'hooks', 'berth.json'), true);
|
|
187
207
|
case 'kimi-config':
|
|
188
208
|
return tomlConfig;
|
|
189
209
|
case 'opencode-plugin':
|
|
@@ -192,8 +212,4 @@ export function hookFormat(target, cli = '') {
|
|
|
192
212
|
throw new BerthError(`unknown hook target ${String(target)}`, ExitCode.usage);
|
|
193
213
|
}
|
|
194
214
|
}
|
|
195
|
-
/** Whether the target file holds Berth's entries. */
|
|
196
|
-
export function containsBerth(file) {
|
|
197
|
-
return existsSync(file) && readFileSync(file, 'utf8').includes(MARKER);
|
|
198
|
-
}
|
|
199
215
|
//# sourceMappingURL=hook-files.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hook-files.js","sourceRoot":"","sources":["../../../src/agents/hook-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"hook-files.js","sourceRoot":"","sources":["../../../src/agents/hook-files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Adapter } from './registry.js';
|
|
2
|
+
export type FileHookHealth = 'installed' | 'missing' | 'broken';
|
|
3
|
+
/** Configuration health only; agent version and runtime delivery need separate evidence. */
|
|
4
|
+
export declare function inspectHooks(adapter: Adapter, env: NodeJS.ProcessEnv): FileHookHealth;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { parse as parseToml } from 'smol-toml';
|
|
4
|
+
import { shellQuote, shellSplit } from '../shell.js';
|
|
5
|
+
import { hookFormat, MARKER, opencodePlugin } from './hook-files.js';
|
|
6
|
+
const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
7
|
+
function accessible(file, mode) {
|
|
8
|
+
try {
|
|
9
|
+
if (!isAbsolute(file) || !statSync(file).isFile())
|
|
10
|
+
return false;
|
|
11
|
+
accessSync(file, mode);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function targetsExist(runtime, cli) {
|
|
19
|
+
return accessible(runtime, constants.X_OK) && accessible(cli, constants.R_OK);
|
|
20
|
+
}
|
|
21
|
+
/** Accept only the shell command shape Berth writes; never execute it to inspect it. */
|
|
22
|
+
function validCommand(value, agent, event) {
|
|
23
|
+
if (typeof value !== 'string' || !value.startsWith(`${MARKER} `))
|
|
24
|
+
return false;
|
|
25
|
+
const words = shellSplit(value);
|
|
26
|
+
if (words[0] !== MARKER)
|
|
27
|
+
return false;
|
|
28
|
+
const electron = words[1] === 'ELECTRON_RUN_AS_NODE=1';
|
|
29
|
+
const offset = electron ? 2 : 1;
|
|
30
|
+
const args = words.slice(offset);
|
|
31
|
+
if (args.length !== 5 || args[2] !== 'hook' || args[3] !== agent || args[4] !== event)
|
|
32
|
+
return false;
|
|
33
|
+
const canonical = `${MARKER} ${electron ? 'ELECTRON_RUN_AS_NODE=1 ' : ''}${shellQuote(args[0])} ${shellQuote(args[1])} hook ${agent} ${shellQuote(event)}`;
|
|
34
|
+
return value === canonical && targetsExist(args[0], args[1]);
|
|
35
|
+
}
|
|
36
|
+
function jsonComplete(text, adapter, events) {
|
|
37
|
+
const data = record(JSON.parse(text));
|
|
38
|
+
if (data.disableAllHooks === true)
|
|
39
|
+
return false;
|
|
40
|
+
const flat = adapter.hookTarget === 'cursor-hooks' || adapter.hookTarget === 'copilot-hooks';
|
|
41
|
+
if (flat && data.version !== 1)
|
|
42
|
+
return false;
|
|
43
|
+
const hooks = record(data.hooks);
|
|
44
|
+
return events.every((event) => {
|
|
45
|
+
const groups = hooks[event];
|
|
46
|
+
if (!Array.isArray(groups))
|
|
47
|
+
return false;
|
|
48
|
+
const commands = flat ? groups : groups.flatMap((value) => {
|
|
49
|
+
const group = record(value);
|
|
50
|
+
if (group.matcher && group.matcher !== '*')
|
|
51
|
+
return [];
|
|
52
|
+
return Array.isArray(group.hooks) ? group.hooks : [];
|
|
53
|
+
});
|
|
54
|
+
return commands.some((value) => {
|
|
55
|
+
const entry = record(value);
|
|
56
|
+
return entry.type === 'command' && validCommand(entry.command, adapter.id, event);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function tomlComplete(text, adapter, events) {
|
|
61
|
+
const hooks = parseToml(text).hooks;
|
|
62
|
+
return Array.isArray(hooks) && events.every((event) => hooks.some((value) => {
|
|
63
|
+
const entry = record(value);
|
|
64
|
+
return entry.event === event && validCommand(entry.command, adapter.id, event);
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
function pluginComplete(text, events) {
|
|
68
|
+
const match = /const child = spawn\(("(?:[^"\\]|\\.)*"), \[("(?:[^"\\]|\\.)*"), 'hook', 'opencode', event\]/.exec(text);
|
|
69
|
+
if (!match)
|
|
70
|
+
return false;
|
|
71
|
+
const runtime = JSON.parse(match[1]), cli = JSON.parse(match[2]);
|
|
72
|
+
return targetsExist(runtime, cli) && [false, true].some((electron) => text === opencodePlugin(runtime, cli, events, electron));
|
|
73
|
+
}
|
|
74
|
+
/** Configuration health only; agent version and runtime delivery need separate evidence. */
|
|
75
|
+
export function inspectHooks(adapter, env) {
|
|
76
|
+
if (!adapter.hookTarget)
|
|
77
|
+
return 'missing';
|
|
78
|
+
const file = hookFormat(adapter.hookTarget).file(env);
|
|
79
|
+
try {
|
|
80
|
+
if (!existsSync(file))
|
|
81
|
+
return 'missing';
|
|
82
|
+
const text = readFileSync(file, 'utf8');
|
|
83
|
+
if (!text.includes(MARKER))
|
|
84
|
+
return 'missing';
|
|
85
|
+
const events = [...new Set(Object.values(adapter.events).flat())];
|
|
86
|
+
const complete = adapter.hookTarget === 'opencode-plugin' ? pluginComplete(text, events)
|
|
87
|
+
: adapter.hookTarget === 'kimi-config' ? tomlComplete(text, adapter, events)
|
|
88
|
+
: jsonComplete(text, adapter, events);
|
|
89
|
+
return complete ? 'installed' : 'broken';
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return 'broken';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=hook-health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-health.js","sourceRoot":"","sources":["../../../src/agents/hook-health.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,9 +4,12 @@ export declare function hookCommand(cliPath: string, agent: string, event: strin
|
|
|
4
4
|
export declare function installHooks(agentId: string, cliPath: string, env?: NodeJS.ProcessEnv): string;
|
|
5
5
|
export declare function uninstallHooks(agentId: string, env?: NodeJS.ProcessEnv): string;
|
|
6
6
|
export declare function hooksInstalled(agentId: string, env?: NodeJS.ProcessEnv): boolean;
|
|
7
|
-
export type HookStatus = 'installed' | 'missing' | 'none';
|
|
7
|
+
export type HookStatus = 'installed' | 'missing' | 'broken' | 'none' | 'unknown';
|
|
8
8
|
/** Whether an agent's hooks are installed; `none` when Berth has no hooks for it. */
|
|
9
9
|
export declare function hookStatus(agentId: string, env?: NodeJS.ProcessEnv): HookStatus;
|
|
10
|
+
/** Agent-owned activation that writing a hook file cannot complete. */
|
|
11
|
+
export declare function hookSetupGuidance(agentId: string): string | undefined;
|
|
12
|
+
export declare function hookInstallNotice(agentId: string, file: string): string;
|
|
10
13
|
/** What a user is told when an agent in use has no hooks (spec §4.2). */
|
|
11
14
|
export declare function hookHint(agentId: string): string;
|
|
12
15
|
/** What a hook event changes on the session, or nothing when the event does not matter. */
|
package/dist/src/agents/hooks.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import { copilotCursor } from './copilot-state.js';
|
|
2
|
+
import { lateBindingId } from './late-binding.js';
|
|
3
|
+
import { geminiCursor } from './gemini-state.js';
|
|
4
|
+
import { processLifetime } from '../process-owner.js';
|
|
5
|
+
import { captureProfile, profileKey, readProfile } from './profile.js';
|
|
6
|
+
import { withLock } from '../restore/lock.js';
|
|
1
7
|
import { BerthError, ExitCode } from '../errors.js';
|
|
2
8
|
import { shellQuote } from '../shell.js';
|
|
3
9
|
import { Tmux } from '../tmux/client.js';
|
|
4
10
|
import { presentState } from '../present/state.js';
|
|
5
11
|
import { createContext } from '../context.js';
|
|
12
|
+
import { discover, processes, sessionProducers, PRODUCER_CHANGED, BINDING_UNVERIFIED } from '../registry/discover.js';
|
|
6
13
|
import { snapshotQuietly } from '../snapshot/take.js';
|
|
7
14
|
import { requireAdapter } from './commands.js';
|
|
8
15
|
import { getAdapter, stateForEvent } from './registry.js';
|
|
9
|
-
import {
|
|
16
|
+
import { inspectHooks } from './hook-health.js';
|
|
17
|
+
import { hookFormat, MARKER } from './hook-files.js';
|
|
10
18
|
function hookEvents(adapter) {
|
|
11
|
-
|
|
12
|
-
return [...new Set([...start, ...working, ...waiting, ...done])];
|
|
19
|
+
return [...new Set(Object.values(adapter.events).flat())];
|
|
13
20
|
}
|
|
14
21
|
function targetOf(adapter) {
|
|
15
22
|
if (!adapter.hookTarget)
|
|
@@ -18,13 +25,16 @@ function targetOf(adapter) {
|
|
|
18
25
|
}
|
|
19
26
|
/** Command an agent runs for an event: `BERTH_HOOK=1 <node> <cli> hook <agent> <event>`. */
|
|
20
27
|
export function hookCommand(cliPath, agent, event) {
|
|
21
|
-
|
|
28
|
+
const runtimeMode = process.env.ELECTRON_RUN_AS_NODE === '1' ? 'ELECTRON_RUN_AS_NODE=1 ' : '';
|
|
29
|
+
return `${MARKER} ${runtimeMode}${shellQuote(process.execPath)} ${shellQuote(cliPath)} hook ${agent} ${shellQuote(event)}`;
|
|
22
30
|
}
|
|
23
31
|
export function installHooks(agentId, cliPath, env = process.env) {
|
|
24
32
|
const adapter = requireAdapter(agentId);
|
|
25
33
|
const format = hookFormat(targetOf(adapter), cliPath);
|
|
26
34
|
const file = format.file(env);
|
|
27
35
|
format.install(file, hookEvents(adapter), (event) => hookCommand(cliPath, adapter.id, event), adapter);
|
|
36
|
+
if (inspectHooks(adapter, env) !== 'installed')
|
|
37
|
+
throw new BerthError(`Hooks were written to ${file}, but configuration remains incomplete, disabled, or its runtime is unavailable. Inspect this file before retrying.`);
|
|
28
38
|
return file;
|
|
29
39
|
}
|
|
30
40
|
export function uninstallHooks(agentId, env = process.env) {
|
|
@@ -37,7 +47,7 @@ export function hooksInstalled(agentId, env = process.env) {
|
|
|
37
47
|
const adapter = requireAdapter(agentId);
|
|
38
48
|
if (!adapter.hookTarget)
|
|
39
49
|
return false;
|
|
40
|
-
return
|
|
50
|
+
return inspectHooks(adapter, env) === 'installed';
|
|
41
51
|
}
|
|
42
52
|
/** Whether an agent's hooks are installed; `none` when Berth has no hooks for it. */
|
|
43
53
|
export function hookStatus(agentId, env = process.env) {
|
|
@@ -45,16 +55,26 @@ export function hookStatus(agentId, env = process.env) {
|
|
|
45
55
|
if (!adapter?.hookTarget)
|
|
46
56
|
return 'none';
|
|
47
57
|
try {
|
|
48
|
-
return
|
|
58
|
+
return inspectHooks(adapter, env);
|
|
49
59
|
}
|
|
50
60
|
catch {
|
|
51
61
|
return 'missing';
|
|
52
62
|
}
|
|
53
63
|
}
|
|
64
|
+
/** Agent-owned activation that writing a hook file cannot complete. */
|
|
65
|
+
export function hookSetupGuidance(agentId) {
|
|
66
|
+
return agentId === 'codex'
|
|
67
|
+
? 'Codex manages hook trust in /hooks. Review new or changed Berth hooks there before they can run.'
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
export function hookInstallNotice(agentId, file) {
|
|
71
|
+
const name = getAdapter(agentId)?.displayName ?? agentId;
|
|
72
|
+
return `Berth: ${name} hook configuration installed (${file}).${hookSetupGuidance(agentId) ? ` ${hookSetupGuidance(agentId)}` : ''}`;
|
|
73
|
+
}
|
|
54
74
|
/** What a user is told when an agent in use has no hooks (spec §4.2). */
|
|
55
75
|
export function hookHint(agentId) {
|
|
56
76
|
const name = getAdapter(agentId)?.displayName ?? agentId;
|
|
57
|
-
return `Berth: ${name} hooks are not installed
|
|
77
|
+
return `Berth: ${name} hooks are not installed. State updates and future conversation binding may be unavailable. Run: berth hooks install ${agentId}`;
|
|
58
78
|
}
|
|
59
79
|
/** What a hook event changes on the session, or nothing when the event does not matter. */
|
|
60
80
|
export function metaForEvent(adapter, event, payload) {
|
|
@@ -64,14 +84,33 @@ export function metaForEvent(adapter, event, payload) {
|
|
|
64
84
|
meta.conversation = id;
|
|
65
85
|
let state = stateForEvent(adapter, event);
|
|
66
86
|
// Some agents also notify for idle reminders; only a permission request means waiting.
|
|
67
|
-
if (state === 'waiting' && adapter.waitingWhen && !adapter.waitingWhen(payload))
|
|
87
|
+
if (state === 'waiting' && adapter.waitingWhen && !adapter.waitingWhen(payload, event))
|
|
68
88
|
state = undefined;
|
|
69
|
-
if (state)
|
|
89
|
+
if (state !== undefined && adapter.idleWhen?.(event, payload))
|
|
90
|
+
state = '';
|
|
91
|
+
if (state !== undefined)
|
|
70
92
|
meta.state = state;
|
|
71
93
|
// Latest notification text, shown in session lists (spec §6). A new state
|
|
72
94
|
// replaces it, so a finished session never shows an old permission request.
|
|
73
|
-
if (state)
|
|
74
|
-
|
|
95
|
+
if (state !== undefined) {
|
|
96
|
+
const input = payload.tool_input;
|
|
97
|
+
const description = input && typeof input === 'object' && 'description' in input ? input.description : undefined;
|
|
98
|
+
const message = typeof payload.message === 'string' && payload.message.trim() ? payload.message
|
|
99
|
+
: state === 'waiting' && typeof description === 'string' ? description
|
|
100
|
+
: state === 'waiting' && typeof payload.tool_name === 'string' ? `Approval requested: ${payload.tool_name}` : '';
|
|
101
|
+
meta.note = state === '' ? '' : message.trim().slice(0, 200);
|
|
102
|
+
}
|
|
103
|
+
// The session's subtitle: the first line of the prompt the user just sent (spec §4.2).
|
|
104
|
+
// Text the agent injects itself (`<task-notification>`, `<command-name>`…) is not the user's.
|
|
105
|
+
if (state === 'working' && typeof payload.prompt === 'string' && !/^\s*<[a-z][\w-]*[\s>]/i.test(payload.prompt)) {
|
|
106
|
+
// Attachment placeholders such as `[Image #3]` say nothing about the topic.
|
|
107
|
+
const first = payload.prompt
|
|
108
|
+
.split('\n')
|
|
109
|
+
.map((l) => l.replace(/\[(Image|Pasted text) #\d+[^\]]*\]/g, '').replace(/\s+/g, ' ').trim())
|
|
110
|
+
.find(Boolean);
|
|
111
|
+
if (first)
|
|
112
|
+
meta.topic = first.slice(0, 120);
|
|
113
|
+
}
|
|
75
114
|
return meta;
|
|
76
115
|
}
|
|
77
116
|
/**
|
|
@@ -92,22 +131,115 @@ export function handleHook(agentId, event, stdin, env = process.env) {
|
|
|
92
131
|
catch {
|
|
93
132
|
payload = {};
|
|
94
133
|
}
|
|
95
|
-
|
|
134
|
+
let meta = metaForEvent(adapter, event, payload);
|
|
135
|
+
const lateId = lateBindingId(agentId, event, payload);
|
|
96
136
|
if (Object.keys(meta).length === 0)
|
|
97
137
|
return {};
|
|
98
138
|
const tmux = new Tmux({ bin: env.BERTH_TMUX || 'tmux', socketPath });
|
|
99
139
|
const session = tmux.sessionOfPane(pane);
|
|
100
140
|
if (!session)
|
|
101
141
|
return {};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
142
|
+
const ctx = createContext({ ...env, BERTH_TMUX_SOCKET_PATH: socketPath });
|
|
143
|
+
return withLock(ctx.config.stateDir, () => {
|
|
144
|
+
let live = tmux.listSessions().find((s) => s.name === session);
|
|
145
|
+
const table = processes();
|
|
146
|
+
const producers = sessionProducers(ctx, session, table);
|
|
147
|
+
const ancestors = [];
|
|
148
|
+
for (let pid = process.pid; pid > 1 && ancestors.length < 64 && !ancestors.includes(pid);) {
|
|
149
|
+
ancestors.push(pid);
|
|
150
|
+
pid = table.find((p) => p.pid === pid)?.ppid ?? 0;
|
|
151
|
+
}
|
|
152
|
+
if (!live?.meta.agent || live.meta.agent === 'shell') {
|
|
153
|
+
const producer = producers[0];
|
|
154
|
+
if (!ctx.config.autoAdopt || (!adapter.events.start.includes(event) && !lateId) || producers.length !== 1
|
|
155
|
+
|| producer.agent !== agentId || producer.pane !== pane || !ancestors.includes(producer.pid))
|
|
156
|
+
return {};
|
|
157
|
+
discover(ctx);
|
|
158
|
+
live = tmux.listSessions().find((s) => s.name === session);
|
|
159
|
+
}
|
|
160
|
+
if (!live?.meta.agent)
|
|
161
|
+
return {};
|
|
162
|
+
if (agentId === 'gemini' && event === 'BeforeModel' && live.meta.state !== 'waiting')
|
|
163
|
+
return {};
|
|
164
|
+
if (live.meta.agent !== agentId) {
|
|
165
|
+
tmux.setMeta(session, { bindingIssue: 'Agent producer changed. Verify the conversation before restoring.' });
|
|
166
|
+
snapshotQuietly(ctx);
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
const otherPane = live.meta.producerPane && live.meta.producerPane !== pane;
|
|
170
|
+
if (producers.length > 1 || otherPane) {
|
|
171
|
+
tmux.setMeta(session, { bindingIssue: 'Multiple independent agents. Move each agent to its own tmux session before restoring.' });
|
|
172
|
+
snapshotQuietly(ctx);
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
const producer = producers[0];
|
|
176
|
+
if (producer) {
|
|
177
|
+
if (producer.agent !== agentId || producer.pane !== pane || !ancestors.includes(producer.pid))
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
if (!producer && (!live.meta.recovery || live.meta.recovery === 'confirmed') && meta.conversation && live.meta.conversation && meta.conversation !== live.meta.conversation) {
|
|
181
|
+
tmux.setMeta(session, { bindingIssue: 'Cannot verify the producer of a different conversation. Inspect the agent and re-adopt before restoring.' });
|
|
182
|
+
snapshotQuietly(ctx);
|
|
183
|
+
return {};
|
|
184
|
+
}
|
|
185
|
+
const payloadId = payload.session_id ?? payload.sessionId ?? payload.sessionID ?? payload.conversation_id;
|
|
186
|
+
if (!adapter.events.start.includes(event) && typeof payloadId === 'string' && live.meta.conversation && payloadId !== live.meta.conversation)
|
|
187
|
+
return {};
|
|
188
|
+
if (producer && lateId && !live.meta.conversation && (!live.meta.recovery || live.meta.recovery === 'confirmed')
|
|
189
|
+
&& (!live.meta.bindingIssue || [PRODUCER_CHANGED, BINDING_UNVERIFIED].includes(live.meta.bindingIssue))) {
|
|
190
|
+
const existingProfile = readProfile(agentId, live.meta.profile);
|
|
191
|
+
const observedProfile = captureProfile(agentId, env);
|
|
192
|
+
if (live.meta.profile && (!existingProfile || profileKey(agentId, existingProfile) !== profileKey(agentId, observedProfile)))
|
|
193
|
+
return {};
|
|
194
|
+
meta = { ...meta, conversation: lateId };
|
|
195
|
+
}
|
|
196
|
+
// Claim once in tmux, so simultaneous first hooks cannot both own the row.
|
|
197
|
+
tmux.run(['set-option', '-o', '-t', `=${session}:`, '@berth_producerPane', pane], true);
|
|
198
|
+
const owner = tmux.run(['display-message', '-p', '-t', `=${session}:`, '#{@berth_producerPane}'], true).stdout.trim();
|
|
199
|
+
if (owner !== pane) {
|
|
200
|
+
tmux.setMeta(session, { bindingIssue: 'Another pane owns this conversation. Separate agents before restoring.' });
|
|
201
|
+
snapshotQuietly(ctx);
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
const confirmsExisting = !!producer && typeof payloadId === 'string' && !!live.meta.conversation && payloadId === live.meta.conversation;
|
|
205
|
+
const profile = meta.conversation || (confirmsExisting && !live.meta.profile) ? captureProfile(agentId, env) : undefined;
|
|
206
|
+
if (live.meta.recovery && live.meta.recovery !== 'confirmed' && meta.conversation) {
|
|
207
|
+
const expectedProfile = readProfile(agentId, live.meta.profile);
|
|
208
|
+
if (profile && expectedProfile && profileKey(agentId, profile) !== profileKey(agentId, expectedProfile)) {
|
|
209
|
+
tmux.setMeta(session, { recovery: 'failed', recoveryDetail: 'Agent opened a different profile. The expected conversation and profile were preserved.' });
|
|
210
|
+
snapshotQuietly(ctx);
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
if (meta.conversation !== live.meta.conversation) {
|
|
214
|
+
tmux.setMeta(session, { recovery: 'failed', recoveryDetail: 'Agent opened a different conversation. Inspect the terminal; the expected ID was preserved.' });
|
|
215
|
+
snapshotQuietly(ctx);
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
218
|
+
const dead = tmux.run(['display-message', '-p', '-t', pane, '#{pane_dead}'], true).stdout.trim();
|
|
219
|
+
if (dead === '1')
|
|
220
|
+
return {};
|
|
221
|
+
tmux.setMeta(session, { recovery: 'confirmed', recoveryDetail: '' });
|
|
222
|
+
tmux.run(['set-option', '-w', '-t', `=${session}:`, 'remain-on-exit', 'off']);
|
|
223
|
+
}
|
|
224
|
+
tmux.setMeta(session, {
|
|
225
|
+
...meta, producerPane: pane,
|
|
226
|
+
...(agentId === 'gemini' && event === 'BeforeAgent' ? {
|
|
227
|
+
nativeTranscript: producer ? geminiCursor(payload.transcript_path, meta.conversation || live.meta.conversation || '', env) : '',
|
|
228
|
+
} : agentId === 'copilot' && event === 'userPromptSubmitted' ? {
|
|
229
|
+
nativeTranscript: producer ? copilotCursor(meta.conversation || live.meta.conversation || '', env) : '',
|
|
230
|
+
} : ['gemini', 'copilot'].includes(agentId) && adapter.events.start.includes(event) ? { nativeTranscript: '' } : {}),
|
|
231
|
+
...(producer ? { producerLifetime: processLifetime(producer.pid)?.key } : {}),
|
|
232
|
+
...(producer && (meta.conversation || (typeof payloadId === 'string' && payloadId === live.meta.conversation)) && (live.meta.bindingIssue === PRODUCER_CHANGED || live.meta.bindingIssue === BINDING_UNVERIFIED) ? { bindingIssue: '' } : {}),
|
|
233
|
+
...(profile ? { profile: JSON.stringify(profile) } : {}),
|
|
234
|
+
});
|
|
235
|
+
if (meta.state !== undefined)
|
|
236
|
+
presentState(tmux, session, meta.state, { note: meta.note });
|
|
237
|
+
// A new conversation id must reach the snapshot before any reboot.
|
|
238
|
+
if (meta.conversation || (confirmsExisting && (profile || live.meta.bindingIssue === PRODUCER_CHANGED || live.meta.bindingIssue === BINDING_UNVERIFIED))) {
|
|
239
|
+
snapshotQuietly(ctx);
|
|
240
|
+
}
|
|
241
|
+
return meta;
|
|
242
|
+
});
|
|
111
243
|
}
|
|
112
244
|
catch {
|
|
113
245
|
return {};
|