@xwss/agentbell 0.1.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +969 -0
  4. package/RELEASE.md +31 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +106 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/doctor.d.ts +4 -0
  9. package/dist/commands/doctor.js +159 -0
  10. package/dist/commands/doctor.js.map +1 -0
  11. package/dist/commands/init.d.ts +3 -0
  12. package/dist/commands/init.js +92 -0
  13. package/dist/commands/init.js.map +1 -0
  14. package/dist/commands/initHooks.d.ts +9 -0
  15. package/dist/commands/initHooks.js +455 -0
  16. package/dist/commands/initHooks.js.map +1 -0
  17. package/dist/commands/project.d.ts +14 -0
  18. package/dist/commands/project.js +62 -0
  19. package/dist/commands/project.js.map +1 -0
  20. package/dist/commands/send.d.ts +5 -0
  21. package/dist/commands/send.js +70 -0
  22. package/dist/commands/send.js.map +1 -0
  23. package/dist/commands/setup.d.ts +13 -0
  24. package/dist/commands/setup.js +270 -0
  25. package/dist/commands/setup.js.map +1 -0
  26. package/dist/commands/test.d.ts +9 -0
  27. package/dist/commands/test.js +40 -0
  28. package/dist/commands/test.js.map +1 -0
  29. package/dist/commands/verify.d.ts +78 -0
  30. package/dist/commands/verify.js +725 -0
  31. package/dist/commands/verify.js.map +1 -0
  32. package/dist/config.d.ts +123 -0
  33. package/dist/config.js +72 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/hooks/claudeCode.d.ts +5 -0
  36. package/dist/hooks/claudeCode.js +88 -0
  37. package/dist/hooks/claudeCode.js.map +1 -0
  38. package/dist/hooks/codex.d.ts +4 -0
  39. package/dist/hooks/codex.js +74 -0
  40. package/dist/hooks/codex.js.map +1 -0
  41. package/dist/hooks/vscodeAgent.d.ts +3 -0
  42. package/dist/hooks/vscodeAgent.js +45 -0
  43. package/dist/hooks/vscodeAgent.js.map +1 -0
  44. package/dist/notify/index.d.ts +3 -0
  45. package/dist/notify/index.js +8 -0
  46. package/dist/notify/index.js.map +1 -0
  47. package/dist/notify/local.d.ts +2 -0
  48. package/dist/notify/local.js +34 -0
  49. package/dist/notify/local.js.map +1 -0
  50. package/dist/notify/ntfy.d.ts +10 -0
  51. package/dist/notify/ntfy.js +57 -0
  52. package/dist/notify/ntfy.js.map +1 -0
  53. package/dist/types.d.ts +65 -0
  54. package/dist/types.js +2 -0
  55. package/dist/types.js.map +1 -0
  56. package/dist/utils/context.d.ts +92 -0
  57. package/dist/utils/context.js +198 -0
  58. package/dist/utils/context.js.map +1 -0
  59. package/dist/utils/logger.d.ts +5 -0
  60. package/dist/utils/logger.js +12 -0
  61. package/dist/utils/logger.js.map +1 -0
  62. package/dist/utils/mask.d.ts +1 -0
  63. package/dist/utils/mask.js +10 -0
  64. package/dist/utils/mask.js.map +1 -0
  65. package/dist/utils/paths.d.ts +16 -0
  66. package/dist/utils/paths.js +93 -0
  67. package/dist/utils/paths.js.map +1 -0
  68. package/dist/utils/shell.d.ts +1 -0
  69. package/dist/utils/shell.js +21 -0
  70. package/dist/utils/shell.js.map +1 -0
  71. package/dist/utils/topic.d.ts +2 -0
  72. package/dist/utils/topic.js +20 -0
  73. package/dist/utils/topic.js.map +1 -0
  74. package/package.json +57 -0
@@ -0,0 +1,725 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { loadConfig, resolveConfigPath } from '../config.js';
8
+ import { createProvider } from '../notify/index.js';
9
+ import { sendLocalNotification } from '../notify/local.js';
10
+ import { logger } from '../utils/logger.js';
11
+ import { maskSecret } from '../utils/mask.js';
12
+ import { claudeNotificationWrapperPath, claudeSettingsPath, claudeStopWrapperPath, codexHooksPath, codexStopWrapperPath, copilotStopWrapperPath, currentCliInvocation, projectRoot, vscodeAgentHookPath } from '../utils/paths.js';
13
+ const execFileAsync = promisify(execFile);
14
+ function selectedTargets(target) {
15
+ if (!target || target === 'all') {
16
+ return ['codex', 'claude-code', 'vscode-agent'];
17
+ }
18
+ return [target];
19
+ }
20
+ async function fileExists(filePath) {
21
+ try {
22
+ await access(filePath, constants.F_OK);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ async function readText(filePath) {
30
+ try {
31
+ return { exists: true, text: await readFile(filePath, 'utf8') };
32
+ }
33
+ catch (error) {
34
+ const nodeError = error;
35
+ if (nodeError.code === 'ENOENT') {
36
+ return { exists: false };
37
+ }
38
+ return { exists: true, error: error.message };
39
+ }
40
+ }
41
+ async function readJson(filePath) {
42
+ const text = await readText(filePath);
43
+ if (!text.exists || text.error || text.text === undefined) {
44
+ return text;
45
+ }
46
+ try {
47
+ return { exists: true, text: text.text, value: JSON.parse(text.text) };
48
+ }
49
+ catch (error) {
50
+ return { exists: true, text: text.text, error: error.message };
51
+ }
52
+ }
53
+ function isRecord(value) {
54
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
55
+ }
56
+ function getNested(value, pathParts) {
57
+ let current = value;
58
+ for (const part of pathParts) {
59
+ if (typeof part === 'number') {
60
+ if (!Array.isArray(current)) {
61
+ return undefined;
62
+ }
63
+ current = current[part];
64
+ }
65
+ else {
66
+ if (!isRecord(current)) {
67
+ return undefined;
68
+ }
69
+ current = current[part];
70
+ }
71
+ }
72
+ return current;
73
+ }
74
+ function addError(report, message) {
75
+ report.errors.push(message);
76
+ report.status = 'error';
77
+ }
78
+ function addWarning(report, message) {
79
+ report.warnings.push(message);
80
+ if (report.status === 'ok') {
81
+ report.status = 'warning';
82
+ }
83
+ }
84
+ const legacyProjectName = ['Hook', 'Ping'].join('');
85
+ const legacyProjectPattern = new RegExp(legacyProjectName, 'i');
86
+ function staleReferences(text) {
87
+ if (!text) {
88
+ return [];
89
+ }
90
+ return text.split(/\r?\n/).filter((line) => legacyProjectPattern.test(line));
91
+ }
92
+ function normalizeForCompare(value) {
93
+ return value.replaceAll('\\', '/').toLowerCase();
94
+ }
95
+ function toGitBashPath(filePath) {
96
+ const normalized = filePath.replaceAll('\\', '/');
97
+ const driveMatch = /^([A-Za-z]):\/(.*)$/.exec(normalized);
98
+ if (!driveMatch) {
99
+ return normalized;
100
+ }
101
+ return `/${driveMatch[1].toLowerCase()}/${driveMatch[2]}`;
102
+ }
103
+ function fromGitBashPath(filePath) {
104
+ const match = /^\/([a-zA-Z])\/(.*)$/.exec(filePath);
105
+ if (!match) {
106
+ return filePath;
107
+ }
108
+ return `${match[1].toUpperCase()}:\\${match[2].replaceAll('/', '\\')}`;
109
+ }
110
+ async function executableExists(filePath) {
111
+ try {
112
+ await access(filePath, constants.F_OK);
113
+ return true;
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ async function findOnPath(command, options = {}) {
120
+ const env = options.env ?? process.env;
121
+ const platform = options.platform ?? process.platform;
122
+ const exists = options.exists ?? executableExists;
123
+ const pathDirs = (env.PATH ?? '').split(path.delimiter).filter(Boolean);
124
+ const extensions = platform === 'win32' ? (env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';') : [''];
125
+ for (const directory of pathDirs) {
126
+ for (const extension of extensions) {
127
+ const candidate = path.join(directory, platform === 'win32' ? `${command}${extension}` : command);
128
+ if (await exists(candidate)) {
129
+ return candidate;
130
+ }
131
+ }
132
+ }
133
+ return undefined;
134
+ }
135
+ const defaultWindowsBashPaths = [
136
+ 'C:\\Program Files\\Git\\bin\\bash.exe',
137
+ 'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
138
+ 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
139
+ 'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe'
140
+ ];
141
+ export async function resolveBashPath(options = {}) {
142
+ const env = options.env ?? process.env;
143
+ const platform = options.platform ?? process.platform;
144
+ const exists = options.exists ?? executableExists;
145
+ const find = options.findOnPath ?? ((command) => findOnPath(command, { env, platform, exists }));
146
+ const commonPaths = options.commonPaths ?? defaultWindowsBashPaths;
147
+ const envPath = env.AGENTBELL_BASH_PATH?.trim();
148
+ if (envPath) {
149
+ const resolved = path.resolve(envPath);
150
+ if (await exists(resolved)) {
151
+ return { path: resolved };
152
+ }
153
+ return { error: `AGENTBELL_BASH_PATH is set but file does not exist: ${envPath}` };
154
+ }
155
+ const pathBash = await find('bash');
156
+ if (pathBash) {
157
+ return { path: pathBash };
158
+ }
159
+ if (platform === 'win32') {
160
+ for (const candidate of commonPaths) {
161
+ if (await exists(candidate)) {
162
+ return { path: candidate };
163
+ }
164
+ }
165
+ }
166
+ return { error: 'Git Bash was not found. Set AGENTBELL_BASH_PATH or install Git for Windows.' };
167
+ }
168
+ function expectedCliMatches(content, expectedCliPath) {
169
+ const normalized = normalizeForCompare(content);
170
+ const candidates = [expectedCliPath, expectedCliPath.replaceAll('\\', '/'), toGitBashPath(expectedCliPath)];
171
+ return candidates.some((candidate) => normalized.includes(normalizeForCompare(candidate)));
172
+ }
173
+ function extractCliPaths(content) {
174
+ const quoted = [...content.matchAll(/["']([^"'\r\n]+)["']/g)].map((match) => match[1]);
175
+ return quoted
176
+ .filter((value) => normalizeForCompare(value).includes('dist/cli.js'))
177
+ .map((value) => fromGitBashPath(value));
178
+ }
179
+ async function inspectWrapper(report, wrapperPath, expectedCommand, expectedCliPath) {
180
+ report.details.wrapperPath = wrapperPath;
181
+ const wrapper = await readText(wrapperPath);
182
+ report.checks.wrapper = wrapper.exists && !wrapper.error ? 'ok' : 'error';
183
+ if (!wrapper.exists) {
184
+ addError(report, `Wrapper file is missing: ${wrapperPath}`);
185
+ return undefined;
186
+ }
187
+ if (wrapper.error || wrapper.text === undefined) {
188
+ addError(report, `Wrapper file is not readable: ${wrapper.error ?? wrapperPath}`);
189
+ return undefined;
190
+ }
191
+ if (!wrapper.text.includes(expectedCommand)) {
192
+ addError(report, `Wrapper does not contain expected command: ${expectedCommand}`);
193
+ }
194
+ if (!expectedCliMatches(wrapper.text, expectedCliPath)) {
195
+ addError(report, `Wrapper does not reference current dist/cli.js: ${expectedCliPath}`);
196
+ }
197
+ const stale = staleReferences(wrapper.text);
198
+ if (stale.length > 0) {
199
+ report.details.staleReferences = stale;
200
+ addError(report, 'Wrapper contains stale legacy project references.');
201
+ report.suggestedFixes.push('Run node dist\\cli.js init-hooks --target all');
202
+ }
203
+ const cliPaths = extractCliPaths(wrapper.text);
204
+ report.details.cliPaths = cliPaths;
205
+ if (cliPaths.length === 0) {
206
+ addError(report, 'Wrapper does not contain a detectable dist/cli.js path.');
207
+ }
208
+ for (const cliPath of cliPaths) {
209
+ if (!(await fileExists(cliPath))) {
210
+ addError(report, `Wrapper points to missing cli.js: ${cliPath}`);
211
+ report.suggestedFixes.push('Run node dist\\cli.js init-hooks --target all');
212
+ }
213
+ }
214
+ return wrapper.text;
215
+ }
216
+ function logBlockMarkerPattern(target) {
217
+ if (target === 'codex') {
218
+ return /Codex Stop hook fired/i;
219
+ }
220
+ if (target === 'claude-code') {
221
+ return /Claude(?: Code)?(?: stop| notification)? hook fired|Claude/i;
222
+ }
223
+ return /(?:VS Code )?Copilot Stop hook fired/i;
224
+ }
225
+ function splitLogBlocks(text, target) {
226
+ const marker = logBlockMarkerPattern(target);
227
+ const blocks = [];
228
+ let current;
229
+ for (const line of text.split(/\r?\n/)) {
230
+ if (marker.test(line) || /hook fired/i.test(line)) {
231
+ if (current && current.length > 0) {
232
+ blocks.push(current.join('\n'));
233
+ }
234
+ current = [line];
235
+ }
236
+ else if (current) {
237
+ current.push(line);
238
+ }
239
+ }
240
+ if (current && current.length > 0) {
241
+ blocks.push(current.join('\n'));
242
+ }
243
+ return blocks.filter((block) => marker.test(block));
244
+ }
245
+ function logSignals(block) {
246
+ return {
247
+ notificationSent: /Notification sent/i.test(block),
248
+ exitCode0: /(?:AgentBell )?exit code 0/i.test(block),
249
+ cannotFindModule: /Cannot find module/i.test(block),
250
+ moduleNotFound: /MODULE_NOT_FOUND/i.test(block),
251
+ exitCode1: /(?:AgentBell )?exit code 1/i.test(block),
252
+ hookPing: legacyProjectPattern.test(block)
253
+ };
254
+ }
255
+ async function inspectLog(report, logPath) {
256
+ report.details.logPath = logPath;
257
+ const log = await readText(logPath);
258
+ if (!log.exists) {
259
+ report.checks.log = 'warning';
260
+ addWarning(report, `Log file does not exist yet: ${logPath}`);
261
+ return;
262
+ }
263
+ if (log.error || log.text === undefined) {
264
+ report.checks.log = 'warning';
265
+ addWarning(report, `Log file is not readable: ${log.error ?? logPath}`);
266
+ return;
267
+ }
268
+ const blocks = splitLogBlocks(log.text, report.target);
269
+ report.details.logBlockCount = blocks.length;
270
+ if (blocks.length === 0) {
271
+ report.checks.log = 'warning';
272
+ addWarning(report, `Log file exists but no ${report.target} hook run block was found.`);
273
+ return;
274
+ }
275
+ const latestBlock = blocks[blocks.length - 1];
276
+ const latestSignals = logSignals(latestBlock);
277
+ const historicalBlocks = blocks.slice(0, -1).join('\n');
278
+ const historicalHasErrors = new RegExp('Cannot find module|MODULE_NOT_FOUND|exit code 1|' + legacyProjectName, 'i').test(historicalBlocks);
279
+ report.details.recentLogSignals = latestSignals;
280
+ report.details.latestLogBlockStatus = latestSignals.notificationSent && latestSignals.exitCode0 ? 'ok' : 'unknown';
281
+ report.details.historicalStaleErrorsIgnored = historicalHasErrors && latestSignals.notificationSent && latestSignals.exitCode0;
282
+ report.checks.log = 'ok';
283
+ if (latestSignals.cannotFindModule || latestSignals.moduleNotFound || latestSignals.exitCode1) {
284
+ addError(report, 'Latest wrapper log block contains module/load failure or exit code 1.');
285
+ report.suggestedFixes.push('Run node dist\\cli.js init-hooks --target all');
286
+ }
287
+ if (latestSignals.hookPing) {
288
+ addError(report, 'Latest wrapper log block contains a stale legacy project path.');
289
+ report.suggestedFixes.push('Run node dist\\cli.js init-hooks --target all');
290
+ }
291
+ if (historicalHasErrors && latestSignals.notificationSent && latestSignals.exitCode0) {
292
+ addWarning(report, 'Historical stale log errors were ignored because the latest hook run block succeeded.');
293
+ }
294
+ }
295
+ async function defaultRunner(target, wrapperPath, runnerOptions = {}) {
296
+ try {
297
+ if (target === 'claude-code') {
298
+ const command = runnerOptions.bashPath ?? 'bash';
299
+ const args = [wrapperPath];
300
+ const result = await execFileAsync(command, args, { timeout: 30000, shell: false });
301
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr, command, args };
302
+ }
303
+ if (process.platform === 'win32') {
304
+ const command = 'cmd';
305
+ const args = ['/d', '/s', '/c', wrapperPath];
306
+ const result = await execFileAsync(command, args, { timeout: 30000, shell: false });
307
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr, command, args };
308
+ }
309
+ const command = 'sh';
310
+ const args = [wrapperPath];
311
+ const result = await execFileAsync(command, args, { timeout: 30000, shell: false });
312
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr, command, args };
313
+ }
314
+ catch (error) {
315
+ const execError = error;
316
+ return {
317
+ exitCode: typeof execError.code === 'number' ? execError.code : 1,
318
+ stdout: execError.stdout ?? '',
319
+ stderr: execError.stderr ?? execError.message,
320
+ command: execError.path,
321
+ args: execError.spawnargs
322
+ };
323
+ }
324
+ }
325
+ async function maybeRunWrapper(report, options, wrapperPath, runnerOptions = {}) {
326
+ if (!options.runWrapper) {
327
+ addWarning(report, 'Wrapper was not run. Use --run-wrapper for a manual wrapper execution test.');
328
+ return;
329
+ }
330
+ const runner = options.runner ?? defaultRunner;
331
+ const result = await runner(report.target, wrapperPath, runnerOptions);
332
+ report.details.runWrapper = result;
333
+ if (result.exitCode !== 0) {
334
+ report.details.runWrapperExitCode = result.exitCode;
335
+ }
336
+ }
337
+ function reconcileRunWrapperWithLog(report) {
338
+ const runWrapper = report.details.runWrapper;
339
+ if (!runWrapper) {
340
+ return;
341
+ }
342
+ const signals = report.details.recentLogSignals;
343
+ const latestSucceeded = Boolean(signals?.notificationSent && signals.exitCode0);
344
+ const logBlockCount = typeof report.details.logBlockCount === 'number' ? report.details.logBlockCount : 0;
345
+ if (runWrapper.exitCode === 0 && latestSucceeded) {
346
+ return;
347
+ }
348
+ if (runWrapper.exitCode === 0 && !latestSucceeded) {
349
+ addWarning(report, 'Wrapper returned 0 but log was not updated with a successful latest block.');
350
+ return;
351
+ }
352
+ if (runWrapper.exitCode !== 0 && latestSucceeded) {
353
+ addWarning(report, `Wrapper execution returned exit code ${runWrapper.exitCode}, but the latest log block shows Notification sent and AgentBell exit code 0.`);
354
+ report.details.executionResultMismatch = true;
355
+ return;
356
+ }
357
+ if (logBlockCount === 0) {
358
+ addError(report, `Wrapper execution failed with exit code ${runWrapper.exitCode}, and no latest log block was found.`);
359
+ return;
360
+ }
361
+ addError(report, `Wrapper execution failed with exit code ${runWrapper.exitCode}.`);
362
+ }
363
+ function createTargetReport(target) {
364
+ return {
365
+ target,
366
+ status: 'ok',
367
+ checks: {},
368
+ details: {},
369
+ warnings: [],
370
+ errors: [],
371
+ suggestedFixes: []
372
+ };
373
+ }
374
+ async function verifyCodex(expectedCliPath, options) {
375
+ const report = createTargetReport('codex');
376
+ const hooksPath = codexHooksPath();
377
+ report.details.hooksPath = hooksPath;
378
+ const hooks = await readJson(hooksPath);
379
+ report.checks.hooksJson = hooks.exists && !hooks.error ? 'ok' : 'error';
380
+ if (!hooks.exists) {
381
+ addError(report, `Codex hooks.json is missing: ${hooksPath}`);
382
+ }
383
+ else if (hooks.error) {
384
+ addError(report, `Codex hooks.json is not valid JSON: ${hooks.error}`);
385
+ }
386
+ else {
387
+ const handler = getNested(hooks.value, ['hooks', 'Stop', 0, 'hooks', 0]);
388
+ const commandWindows = isRecord(handler) ? handler.commandWindows : undefined;
389
+ if (!Array.isArray(getNested(hooks.value, ['hooks', 'Stop']))) {
390
+ addError(report, 'Codex hooks.json does not contain hooks.Stop.');
391
+ }
392
+ if (!isRecord(handler) || handler.type !== 'command') {
393
+ addError(report, 'Codex Stop handler is not type: command.');
394
+ }
395
+ if (typeof commandWindows !== 'string') {
396
+ addError(report, 'Codex Stop handler does not contain commandWindows.');
397
+ }
398
+ else if (!commandWindows.includes('codex-stop.cmd')) {
399
+ addError(report, 'Codex commandWindows does not point to codex-stop.cmd.');
400
+ }
401
+ const stale = staleReferences(hooks.text);
402
+ if (stale.length > 0) {
403
+ report.details.staleReferences = stale;
404
+ addError(report, 'Codex hooks.json contains stale legacy project references.');
405
+ report.suggestedFixes.push('Run node dist\\cli.js init-hooks --target all');
406
+ }
407
+ }
408
+ const wrapperPath = codexStopWrapperPath();
409
+ await inspectWrapper(report, wrapperPath, 'send --tool codex --event stop', expectedCliPath);
410
+ await maybeRunWrapper(report, options, wrapperPath);
411
+ await inspectLog(report, path.join(path.dirname(path.dirname(wrapperPath)), 'codex-hook-fired.log'));
412
+ reconcileRunWrapperWithLog(report);
413
+ return report;
414
+ }
415
+ async function verifyClaude(expectedCliPath, options) {
416
+ const report = createTargetReport('claude-code');
417
+ const settingsPath = claudeSettingsPath();
418
+ report.details.settingsPath = settingsPath;
419
+ const settings = await readJson(settingsPath);
420
+ report.checks.settingsJson = settings.exists && !settings.error ? 'ok' : 'error';
421
+ if (!settings.exists) {
422
+ addError(report, `Claude settings.json is missing: ${settingsPath}`);
423
+ }
424
+ else if (settings.error) {
425
+ addError(report, `Claude settings.json is not valid JSON: ${settings.error}`);
426
+ }
427
+ else {
428
+ const stopCommand = getNested(settings.value, ['hooks', 'Stop', 0, 'hooks', 0, 'command']);
429
+ const notificationHooks = getNested(settings.value, ['hooks', 'Notification']);
430
+ if (!Array.isArray(getNested(settings.value, ['hooks', 'Stop']))) {
431
+ addError(report, 'Claude settings.json does not contain hooks.Stop.');
432
+ }
433
+ if (!Array.isArray(notificationHooks)) {
434
+ addError(report, 'Claude settings.json does not contain hooks.Notification.');
435
+ }
436
+ if (typeof stopCommand !== 'string' || !stopCommand.includes('bash')) {
437
+ addError(report, 'Claude Stop command does not use bash.');
438
+ }
439
+ if (typeof stopCommand !== 'string' || !stopCommand.includes('.agentbell-hooks') || !stopCommand.includes('claude-stop.sh')) {
440
+ addError(report, 'Claude Stop command does not point to .agentbell-hooks/claude-stop.sh.');
441
+ }
442
+ const stale = staleReferences(settings.text);
443
+ if (stale.length > 0) {
444
+ report.details.staleReferences = stale;
445
+ addError(report, 'Claude settings.json contains stale legacy project references.');
446
+ report.suggestedFixes.push('Run node dist\\cli.js init-hooks --target all');
447
+ }
448
+ }
449
+ const stopWrapper = claudeStopWrapperPath();
450
+ await inspectWrapper(report, stopWrapper, 'send --tool claude-code --event stop', expectedCliPath);
451
+ await inspectWrapper(report, claudeNotificationWrapperPath(), 'send --tool claude-code --event notification', expectedCliPath);
452
+ let bashPath;
453
+ if (process.platform === 'win32') {
454
+ const bash = options.bashResolver ? await options.bashResolver() : await resolveBashPath();
455
+ report.details.bashPath = bash.path ?? 'not found';
456
+ if (bash.error) {
457
+ addError(report, bash.error);
458
+ }
459
+ else {
460
+ bashPath = bash.path;
461
+ }
462
+ }
463
+ if (options.runWrapper && process.platform === 'win32' && !bashPath) {
464
+ report.details.runWrapperSkipped = true;
465
+ }
466
+ else {
467
+ await maybeRunWrapper(report, options, stopWrapper, { bashPath });
468
+ }
469
+ await inspectLog(report, path.join(projectRoot(), '.agentbell-hooks', 'claude-hook-fired.log'));
470
+ reconcileRunWrapperWithLog(report);
471
+ return report;
472
+ }
473
+ async function verifyVscodeAgent(expectedCliPath, options) {
474
+ const report = createTargetReport('vscode-agent');
475
+ const hookPath = vscodeAgentHookPath();
476
+ report.details.hookPath = hookPath;
477
+ const hook = await readJson(hookPath);
478
+ report.checks.hookFile = hook.exists && !hook.error ? 'ok' : 'error';
479
+ if (!hook.exists) {
480
+ addError(report, `VS Code Agent hook file is missing: ${hookPath}`);
481
+ }
482
+ else if (hook.error) {
483
+ addError(report, `VS Code Agent hook file is not valid JSON: ${hook.error}`);
484
+ }
485
+ else {
486
+ const handler = getNested(hook.value, ['hooks', 'Stop', 0]);
487
+ const windows = isRecord(handler) ? handler.windows : undefined;
488
+ if (!Array.isArray(getNested(hook.value, ['hooks', 'Stop']))) {
489
+ addError(report, 'VS Code Agent hook file does not contain hooks.Stop.');
490
+ }
491
+ if (typeof windows !== 'string') {
492
+ addError(report, 'VS Code Agent Stop hook does not contain windows field.');
493
+ }
494
+ else {
495
+ if (!windows.includes('cmd /d /s /c')) {
496
+ addError(report, 'VS Code Agent windows command does not use cmd /d /s /c.');
497
+ }
498
+ if (windows.includes("'")) {
499
+ addError(report, 'VS Code Agent windows command uses single quotes.');
500
+ }
501
+ if (!windows.includes('"')) {
502
+ addError(report, 'VS Code Agent windows command does not double-quote wrapper path.');
503
+ }
504
+ if (!windows.includes('copilot-stop.cmd')) {
505
+ addError(report, 'VS Code Agent windows command does not point to copilot-stop.cmd.');
506
+ }
507
+ }
508
+ if (isRecord(handler) && 'commandWindows' in handler) {
509
+ addError(report, 'VS Code Agent hook should not use Codex commandWindows field.');
510
+ }
511
+ const stale = staleReferences(hook.text);
512
+ if (stale.length > 0) {
513
+ report.details.staleReferences = stale;
514
+ addError(report, 'VS Code Agent hook file contains stale legacy project references.');
515
+ report.suggestedFixes.push('Run node dist\\cli.js init-hooks --target all');
516
+ }
517
+ }
518
+ const wrapperPath = copilotStopWrapperPath();
519
+ await inspectWrapper(report, wrapperPath, 'send --tool vscode-agent --event stop', expectedCliPath);
520
+ await inspectLog(report, path.join(projectRoot(), '.agentbell-hooks', 'copilot-hook-fired.log'));
521
+ reconcileRunWrapperWithLog(report);
522
+ await maybeRunWrapper(report, options, wrapperPath);
523
+ return report;
524
+ }
525
+ async function canAccessServer(url) {
526
+ try {
527
+ const response = await fetch(url, { method: 'GET', signal: AbortSignal.timeout(5000) });
528
+ return response.status < 500;
529
+ }
530
+ catch {
531
+ return false;
532
+ }
533
+ }
534
+ async function sendVerifyTest(config) {
535
+ const machine = config.profile.name || os.hostname();
536
+ const inputData = {
537
+ tool: 'custom',
538
+ event: 'custom',
539
+ title: 'AgentBell verify test',
540
+ message: `${machine} verify test succeeded.`,
541
+ priority: config.ntfy.priority,
542
+ tags: config.ntfy.tags,
543
+ config
544
+ };
545
+ await createProvider(config).send(inputData);
546
+ await sendLocalNotification(inputData);
547
+ }
548
+ function mergeTargetIntoReport(report, targetReport) {
549
+ targetReport.suggestedFixes = [...new Set(targetReport.suggestedFixes)];
550
+ report.targets[targetReport.target] = targetReport;
551
+ report.warnings.push(...targetReport.warnings);
552
+ report.errors.push(...targetReport.errors);
553
+ report.suggestedFixes.push(...targetReport.suggestedFixes);
554
+ }
555
+ function finalizeReport(report) {
556
+ report.suggestedFixes = [...new Set(report.suggestedFixes)];
557
+ if (report.errors.length > 0) {
558
+ report.overallStatus = 'error';
559
+ }
560
+ else if (report.warnings.length > 0) {
561
+ report.overallStatus = 'warning';
562
+ }
563
+ else {
564
+ report.overallStatus = 'ok';
565
+ }
566
+ return report;
567
+ }
568
+ export async function buildVerifyReport(options = {}) {
569
+ const configPath = resolveConfigPath(options.config);
570
+ const expectedCliPath = path.join(projectRoot(), 'dist', 'cli.js');
571
+ const report = {
572
+ overallStatus: 'ok',
573
+ platform: process.platform,
574
+ cwd: process.cwd(),
575
+ nodePath: process.execPath,
576
+ cliInvocation: currentCliInvocation(),
577
+ expectedCliPath,
578
+ config: {
579
+ path: configPath,
580
+ exists: false,
581
+ readable: false
582
+ },
583
+ provider: {
584
+ supported: false
585
+ },
586
+ localNotification: {
587
+ windowsSkipped: process.platform === 'win32'
588
+ },
589
+ targets: {},
590
+ warnings: [],
591
+ errors: [],
592
+ suggestedFixes: []
593
+ };
594
+ let config;
595
+ try {
596
+ const loaded = await loadConfig(options.config);
597
+ config = loaded.config;
598
+ report.config.exists = true;
599
+ report.config.readable = true;
600
+ report.config.provider = config.provider;
601
+ report.config.ntfyServer = config.ntfy.server;
602
+ report.config.topicPresent = Boolean(config.ntfy.topic.trim());
603
+ report.config.topicMasked = maskSecret(config.ntfy.topic);
604
+ report.config.profileName = config.profile.name;
605
+ report.provider.supported = config.provider === 'ntfy';
606
+ if (!config.ntfy.topic.trim()) {
607
+ report.errors.push('ntfy topic is empty.');
608
+ }
609
+ if (!options.dryRun) {
610
+ report.provider.ntfyServerReachable = await canAccessServer(config.ntfy.server);
611
+ if (!report.provider.ntfyServerReachable) {
612
+ report.warnings.push('ntfy server was not reachable.');
613
+ }
614
+ }
615
+ if (options.sendTest) {
616
+ await sendVerifyTest(config);
617
+ }
618
+ }
619
+ catch (error) {
620
+ report.errors.push(`Config is missing or invalid: ${error.message}`);
621
+ }
622
+ if (process.platform === 'win32') {
623
+ report.warnings.push('Windows local desktop notification is skipped in this MVP; phone push is unaffected.');
624
+ }
625
+ for (const target of selectedTargets(options.target)) {
626
+ const targetReport = target === 'codex'
627
+ ? await verifyCodex(expectedCliPath, options)
628
+ : target === 'claude-code'
629
+ ? await verifyClaude(expectedCliPath, options)
630
+ : await verifyVscodeAgent(expectedCliPath, options);
631
+ mergeTargetIntoReport(report, targetReport);
632
+ }
633
+ return finalizeReport(report);
634
+ }
635
+ function formatTargetName(target) {
636
+ if (target === 'codex') {
637
+ return 'Codex';
638
+ }
639
+ if (target === 'claude-code') {
640
+ return 'Claude Code';
641
+ }
642
+ return 'VS Code Copilot Agent';
643
+ }
644
+ function formatTextReport(report) {
645
+ const lines = ['AgentBell verify', '================'];
646
+ lines.push(`Config: ${report.config.readable ? 'ok' : 'error'}`);
647
+ lines.push(`Provider: ${report.provider.supported ? `${report.config.provider} ok` : 'error'}`);
648
+ lines.push(`Topic: ${report.config.topicMasked ?? '<missing>'}`);
649
+ lines.push(`Machine: ${report.config.profileName ?? os.hostname()}`);
650
+ lines.push(`CWD: ${report.cwd}`);
651
+ lines.push(`Node: ${report.nodePath}`);
652
+ lines.push(`CLI: ${report.cliInvocation}`);
653
+ lines.push('');
654
+ lines.push('Targets:');
655
+ for (const target of selectedTargets(undefined)) {
656
+ const targetReport = report.targets[target];
657
+ if (!targetReport) {
658
+ continue;
659
+ }
660
+ lines.push(`[${formatTargetName(target)}]`);
661
+ for (const [name, status] of Object.entries(targetReport.checks)) {
662
+ lines.push(` ${name}: ${status}`);
663
+ }
664
+ if (targetReport.details.bashPath) {
665
+ lines.push(` bash: ${targetReport.details.bashPath}`);
666
+ }
667
+ if (targetReport.details.staleReferences) {
668
+ lines.push(' stale legacy project references: found');
669
+ for (const stale of targetReport.details.staleReferences) {
670
+ lines.push(` stale path: ${stale.trim()}`);
671
+ }
672
+ }
673
+ const logSignals = targetReport.details.recentLogSignals;
674
+ if (logSignals) {
675
+ const active = Object.entries(logSignals)
676
+ .filter(([, value]) => value)
677
+ .map(([key]) => key)
678
+ .join(', ');
679
+ lines.push(` last log: ${active || 'no known signals'}`);
680
+ }
681
+ lines.push(` status: ${targetReport.status}`);
682
+ for (const error of targetReport.errors) {
683
+ lines.push(` error: ${error}`);
684
+ }
685
+ for (const warning of targetReport.warnings) {
686
+ lines.push(` warning: ${warning}`);
687
+ }
688
+ for (const fix of targetReport.suggestedFixes) {
689
+ lines.push(` fix: ${fix}`);
690
+ }
691
+ lines.push('');
692
+ }
693
+ if (report.warnings.length > 0) {
694
+ lines.push('Warnings:');
695
+ for (const warning of report.warnings) {
696
+ lines.push(`- ${warning}`);
697
+ }
698
+ lines.push('');
699
+ }
700
+ if (report.errors.length > 0) {
701
+ lines.push('Errors:');
702
+ for (const error of report.errors) {
703
+ lines.push(`- ${error}`);
704
+ }
705
+ lines.push('');
706
+ }
707
+ if (report.suggestedFixes.length > 0) {
708
+ lines.push('Suggested fixes:');
709
+ for (const fix of report.suggestedFixes) {
710
+ lines.push(`- ${fix}`);
711
+ }
712
+ lines.push('');
713
+ }
714
+ lines.push(`Overall: ${report.overallStatus}`);
715
+ return lines.join('\n');
716
+ }
717
+ export async function runVerify(options) {
718
+ const allowed = ['codex', 'claude-code', 'vscode-agent', 'all', undefined];
719
+ if (!allowed.includes(options.target)) {
720
+ throw new Error(`Invalid verify target: ${options.target}. Expected codex, claude-code, vscode-agent, or all.`);
721
+ }
722
+ const report = await buildVerifyReport(options);
723
+ logger.info(options.json ? JSON.stringify(report, null, 2) : formatTextReport(report));
724
+ }
725
+ //# sourceMappingURL=verify.js.map