@ai-devkit/agent-manager 0.30.0 → 0.32.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.
@@ -0,0 +1,614 @@
1
+ import { constants } from 'node:fs';
2
+ import { access as fsAccess, readFile as fsReadFile } from 'node:fs/promises';
3
+ import { execFile } from 'node:child_process';
4
+ import { delimiter, join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { getCodexCapacityReport } from '../capacity/index.js';
7
+ import { AGENTS } from '../utils/agents.js';
8
+ const execFileAsync = promisify(execFile);
9
+ const READINESS_AGENT_TYPES = [
10
+ 'claude',
11
+ 'codex',
12
+ 'copilot',
13
+ 'grok_cli',
14
+ 'opencode',
15
+ 'pi'
16
+ ];
17
+ const ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, 'g');
18
+ const AGENT_HOME_DIRS = {
19
+ claude: '.claude',
20
+ codex: '.codex',
21
+ copilot: '.copilot',
22
+ grok_cli: '.grok',
23
+ opencode: '.config/opencode',
24
+ pi: '.pi'
25
+ };
26
+ function runtime(options) {
27
+ return {
28
+ homeDir: options.homeDir ?? process.env.HOME ?? '',
29
+ path: options.path ?? process.env.PATH ?? '',
30
+ assetRoot: options.assetRoot ?? null,
31
+ builtInSkillNames: options.builtInSkillNames ?? [],
32
+ skillRoots: options.skillRoots ?? {},
33
+ readFile: options.readFile ?? ((target)=>fsReadFile(target, 'utf8')),
34
+ access: options.access ?? ((target, mode = constants.R_OK)=>fsAccess(target, mode)),
35
+ runCommand: options.runCommand ?? defaultRunCommand,
36
+ codexAuth: options.codexAuth ?? (async ()=>(await getCodexCapacityReport()).authenticated)
37
+ };
38
+ }
39
+ async function defaultRunCommand(command, args) {
40
+ const result = await execFileAsync(command, args, {
41
+ encoding: 'utf8',
42
+ timeout: 5000,
43
+ maxBuffer: 1024 * 1024
44
+ });
45
+ return {
46
+ stdout: result.stdout,
47
+ stderr: result.stderr
48
+ };
49
+ }
50
+ function statusRank(status) {
51
+ return status === 'fail' ? 2 : status === 'warn' ? 1 : 0;
52
+ }
53
+ export function worstReadinessStatus(statuses) {
54
+ return statuses.reduce((worst, current)=>statusRank(current) > statusRank(worst) ? current : worst, 'pass');
55
+ }
56
+ function displayHome(target, homeDir) {
57
+ return target === homeDir ? '~' : target.startsWith(`${homeDir}/`) ? `~${target.slice(homeDir.length)}` : target;
58
+ }
59
+ function record(value) {
60
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
61
+ }
62
+ function nonEmpty(value) {
63
+ return typeof value === 'string' && value.trim().length > 0;
64
+ }
65
+ async function accessible(target, rt, mode = constants.R_OK) {
66
+ try {
67
+ await rt.access(target, mode);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+ async function resolveExecutable(command, rt) {
74
+ const candidates = rt.path.split(delimiter).filter(Boolean).map((directory)=>join(directory, command));
75
+ const checks = await Promise.all(candidates.map(async (target)=>({
76
+ target,
77
+ available: await accessible(target, rt, constants.X_OK)
78
+ })));
79
+ return checks.find((check)=>check.available)?.target ?? null;
80
+ }
81
+ async function executableCheck(agent, rt) {
82
+ const command = AGENTS[agent].command;
83
+ const resolvedPath = await resolveExecutable(command, rt);
84
+ return {
85
+ command,
86
+ path: resolvedPath,
87
+ status: resolvedPath ? 'pass' : 'fail',
88
+ errors: resolvedPath ? [] : [
89
+ `${command} was not found on PATH`
90
+ ]
91
+ };
92
+ }
93
+ async function directoryCheck(agent, rt) {
94
+ const target = join(rt.homeDir, AGENT_HOME_DIRS[agent]);
95
+ const readable = await accessible(target, rt);
96
+ return {
97
+ path: displayHome(target, rt.homeDir),
98
+ present: readable,
99
+ readable,
100
+ status: readable ? 'pass' : 'fail',
101
+ errors: readable ? [] : [
102
+ 'global configuration directory is unavailable'
103
+ ]
104
+ };
105
+ }
106
+ async function builtInSkillsCheck(agent, rt) {
107
+ const relativeRoot = rt.skillRoots[agent];
108
+ if (!relativeRoot) {
109
+ return {
110
+ path: null,
111
+ required: rt.builtInSkillNames.length,
112
+ present: 0,
113
+ missing: [
114
+ ...rt.builtInSkillNames
115
+ ],
116
+ status: 'info',
117
+ errors: []
118
+ };
119
+ }
120
+ const root = join(rt.homeDir, relativeRoot);
121
+ const checks = await Promise.all(rt.builtInSkillNames.map(async (name)=>({
122
+ name,
123
+ present: await accessible(join(root, name, 'SKILL.md'), rt)
124
+ })));
125
+ const present = checks.filter((check)=>check.present).map((check)=>check.name);
126
+ const missing = rt.builtInSkillNames.filter((name)=>!present.includes(name));
127
+ return {
128
+ path: displayHome(root, rt.homeDir),
129
+ required: rt.builtInSkillNames.length,
130
+ present: present.length,
131
+ missing: [
132
+ ...missing
133
+ ],
134
+ status: 'info',
135
+ errors: []
136
+ };
137
+ }
138
+ async function scriptCheck(installed, bundled, rt) {
139
+ let installedText;
140
+ try {
141
+ installedText = await rt.readFile(installed);
142
+ } catch {
143
+ return {
144
+ path: displayHome(installed, rt.homeDir),
145
+ present: false,
146
+ readable: false,
147
+ matchesBundledAsset: false,
148
+ status: 'fail',
149
+ errors: [
150
+ 'hook script is unavailable'
151
+ ]
152
+ };
153
+ }
154
+ try {
155
+ const bundledText = await rt.readFile(bundled);
156
+ const matches = installedText === bundledText;
157
+ return {
158
+ path: displayHome(installed, rt.homeDir),
159
+ present: true,
160
+ readable: true,
161
+ matchesBundledAsset: matches,
162
+ status: matches ? 'pass' : 'fail',
163
+ errors: matches ? [] : [
164
+ 'hook script differs from the bundled AI DevKit asset'
165
+ ]
166
+ };
167
+ } catch {
168
+ return {
169
+ path: displayHome(installed, rt.homeDir),
170
+ present: true,
171
+ readable: true,
172
+ matchesBundledAsset: false,
173
+ status: 'fail',
174
+ errors: [
175
+ 'bundled hook asset is unavailable'
176
+ ]
177
+ };
178
+ }
179
+ }
180
+ function containsHook(root, event, command) {
181
+ const hooks = record(record(root)?.hooks);
182
+ const entries = hooks?.[event];
183
+ if (!Array.isArray(entries)) return false;
184
+ return entries.some((entry)=>{
185
+ const commands = record(entry)?.hooks;
186
+ return Array.isArray(commands) && commands.some((hook)=>{
187
+ const item = record(hook);
188
+ return item?.type === 'command' && item.command === command;
189
+ });
190
+ });
191
+ }
192
+ async function registrationCheck(target, event, command, rt) {
193
+ try {
194
+ const parsed = JSON.parse(await rt.readFile(target));
195
+ const valid = containsHook(parsed, event, command);
196
+ return {
197
+ path: displayHome(target, rt.homeDir),
198
+ event,
199
+ command,
200
+ present: true,
201
+ valid,
202
+ status: valid ? 'pass' : 'fail',
203
+ errors: valid ? [] : [
204
+ 'required hook registration is missing'
205
+ ]
206
+ };
207
+ } catch {
208
+ return {
209
+ path: displayHome(target, rt.homeDir),
210
+ event,
211
+ command,
212
+ present: false,
213
+ valid: false,
214
+ status: 'fail',
215
+ errors: [
216
+ 'hook configuration is missing or invalid'
217
+ ]
218
+ };
219
+ }
220
+ }
221
+ async function mappingCheck(target, rt) {
222
+ let text;
223
+ try {
224
+ text = await rt.readFile(target);
225
+ } catch {
226
+ return {
227
+ path: displayHome(target, rt.homeDir),
228
+ present: false,
229
+ valid: false,
230
+ invalidEntries: 0,
231
+ staleEntries: 0,
232
+ status: 'warn',
233
+ errors: [
234
+ 'session mapping has not been created'
235
+ ]
236
+ };
237
+ }
238
+ let parsed = null;
239
+ try {
240
+ parsed = record(JSON.parse(text));
241
+ } catch {
242
+ // Fixed safe error below.
243
+ }
244
+ if (!parsed) {
245
+ return {
246
+ path: displayHome(target, rt.homeDir),
247
+ present: true,
248
+ valid: false,
249
+ invalidEntries: 0,
250
+ staleEntries: 0,
251
+ status: 'fail',
252
+ errors: [
253
+ 'session mapping is invalid'
254
+ ]
255
+ };
256
+ }
257
+ const entries = await Promise.all(Object.entries(parsed).map(async ([pid, sessionPath])=>{
258
+ if (!/^\d+$/.test(pid) || typeof sessionPath !== 'string' || !sessionPath) {
259
+ return {
260
+ valid: false,
261
+ stale: false
262
+ };
263
+ }
264
+ return {
265
+ valid: true,
266
+ stale: !await accessible(sessionPath, rt)
267
+ };
268
+ }));
269
+ const invalidEntries = entries.filter((entry)=>!entry.valid).length;
270
+ const staleEntries = entries.filter((entry)=>entry.stale).length;
271
+ const valid = invalidEntries === 0;
272
+ return {
273
+ path: displayHome(target, rt.homeDir),
274
+ present: true,
275
+ valid,
276
+ invalidEntries,
277
+ staleEntries,
278
+ status: !valid ? 'fail' : staleEntries ? 'warn' : 'pass',
279
+ errors: !valid ? [
280
+ 'session mapping contains invalid entries'
281
+ ] : staleEntries ? [
282
+ 'session mapping contains stale entries'
283
+ ] : []
284
+ };
285
+ }
286
+ async function codexAuthCheck(rt) {
287
+ try {
288
+ const value = await rt.codexAuth();
289
+ return {
290
+ state: value === true ? 'authenticated' : value === false ? 'unauthenticated' : 'unknown',
291
+ source: displayHome(join(rt.homeDir, '.codex', 'auth.json'), rt.homeDir),
292
+ provider: null,
293
+ availableProviders: [],
294
+ status: value === true ? 'pass' : value === false ? 'fail' : 'warn',
295
+ errors: value === true ? [] : [
296
+ value === false ? 'Codex is not authenticated' : 'Codex authentication is unknown'
297
+ ]
298
+ };
299
+ } catch {
300
+ return {
301
+ state: 'unknown',
302
+ source: '~/.codex/auth.json',
303
+ provider: null,
304
+ availableProviders: [],
305
+ status: 'warn',
306
+ errors: [
307
+ 'Codex authentication probe failed'
308
+ ]
309
+ };
310
+ }
311
+ }
312
+ async function claudeAuthCheck(rt) {
313
+ try {
314
+ const result = await rt.runCommand('claude', [
315
+ 'auth',
316
+ 'status',
317
+ '--json'
318
+ ]);
319
+ const parsed = record(JSON.parse(result.stdout));
320
+ const authenticated = parsed?.loggedIn === true || parsed?.authenticated === true;
321
+ const unauthenticated = parsed?.loggedIn === false || parsed?.authenticated === false;
322
+ return {
323
+ state: authenticated ? 'authenticated' : unauthenticated ? 'unauthenticated' : 'unknown',
324
+ source: 'claude auth status --json',
325
+ provider: null,
326
+ availableProviders: [],
327
+ status: authenticated ? 'pass' : unauthenticated ? 'fail' : 'warn',
328
+ errors: authenticated ? [] : [
329
+ unauthenticated ? 'Claude is not authenticated' : 'Claude authentication is unknown'
330
+ ]
331
+ };
332
+ } catch {
333
+ return {
334
+ state: 'unknown',
335
+ source: 'claude auth status --json',
336
+ provider: null,
337
+ availableProviders: [],
338
+ status: 'warn',
339
+ errors: [
340
+ 'Claude authentication probe failed'
341
+ ]
342
+ };
343
+ }
344
+ }
345
+ function piProviderNames(parsed) {
346
+ const names = new Set();
347
+ if (nonEmpty(parsed.provider)) names.add(parsed.provider.trim());
348
+ const providers = record(parsed.providers);
349
+ if (providers) {
350
+ for (const name of Object.keys(providers)){
351
+ if (name.trim()) names.add(name);
352
+ }
353
+ }
354
+ for (const [name, value] of Object.entries(parsed)){
355
+ if (name === 'provider' || name === 'providers') continue;
356
+ if (record(value) && name.trim()) names.add(name);
357
+ }
358
+ return [
359
+ ...names
360
+ ].sort();
361
+ }
362
+ async function piAuthCheck(rt) {
363
+ const sourcePath = join(rt.homeDir, '.pi', 'agent', 'auth.json');
364
+ try {
365
+ const parsed = record(JSON.parse(await rt.readFile(sourcePath)));
366
+ if (!parsed) throw new Error('invalid');
367
+ const availableProviders = piProviderNames(parsed);
368
+ const provider = nonEmpty(parsed.provider) ? parsed.provider.trim() : null;
369
+ const authenticated = availableProviders.length > 0;
370
+ return {
371
+ state: authenticated ? 'authenticated' : 'unauthenticated',
372
+ source: displayHome(sourcePath, rt.homeDir),
373
+ provider,
374
+ availableProviders,
375
+ status: authenticated ? 'pass' : 'fail',
376
+ errors: authenticated ? [] : [
377
+ 'Pi credential file has no configured model provider'
378
+ ]
379
+ };
380
+ } catch {
381
+ return {
382
+ state: 'unauthenticated',
383
+ source: displayHome(sourcePath, rt.homeDir),
384
+ provider: null,
385
+ availableProviders: [],
386
+ status: 'fail',
387
+ errors: [
388
+ 'Pi credential file is missing or invalid'
389
+ ]
390
+ };
391
+ }
392
+ }
393
+ async function opencodeAuthCheck(rt) {
394
+ try {
395
+ const result = await rt.runCommand('opencode', [
396
+ 'auth',
397
+ 'list'
398
+ ]);
399
+ const availableProviders = opencodeAuthProviderNames(result.stdout);
400
+ const authenticated = availableProviders.length > 0;
401
+ return {
402
+ state: authenticated ? 'authenticated' : 'unauthenticated',
403
+ source: 'opencode auth list',
404
+ provider: null,
405
+ availableProviders,
406
+ status: authenticated ? 'pass' : 'fail',
407
+ errors: authenticated ? [] : [
408
+ 'OpenCode has no configured credentials'
409
+ ]
410
+ };
411
+ } catch {
412
+ return {
413
+ state: 'unknown',
414
+ source: 'opencode auth list',
415
+ provider: null,
416
+ availableProviders: [],
417
+ status: 'warn',
418
+ errors: [
419
+ 'OpenCode authentication probe failed'
420
+ ]
421
+ };
422
+ }
423
+ }
424
+ async function copilotAuthCheck(rt) {
425
+ try {
426
+ await rt.runCommand('gh', [
427
+ 'auth',
428
+ 'status',
429
+ '--hostname',
430
+ 'github.com'
431
+ ]);
432
+ return {
433
+ state: 'authenticated',
434
+ source: 'gh auth status --hostname github.com',
435
+ provider: 'github',
436
+ availableProviders: [
437
+ 'GitHub'
438
+ ],
439
+ status: 'pass',
440
+ errors: []
441
+ };
442
+ } catch {
443
+ return {
444
+ state: 'unknown',
445
+ source: 'gh auth status --hostname github.com',
446
+ provider: null,
447
+ availableProviders: [
448
+ 'GitHub'
449
+ ],
450
+ status: 'warn',
451
+ errors: [
452
+ 'Copilot authentication could not be verified through GitHub CLI'
453
+ ]
454
+ };
455
+ }
456
+ }
457
+ function opencodeAuthProviderNames(output) {
458
+ const names = new Set();
459
+ for (const line of stripAnsi(output).split(/\r?\n/)){
460
+ const match = /^\s*[●*+-]\s+(.+?)\s*$/.exec(line);
461
+ if (!match) continue;
462
+ const name = match[1].replace(/\s+(?:api|oauth|[\w-]*token|[A-Z][A-Z0-9_]+)$/i, '').trim();
463
+ if (name && !/^\d+\s+(?:credentials?|environment variables?)$/i.test(name)) names.add(name);
464
+ }
465
+ return [
466
+ ...names
467
+ ].sort();
468
+ }
469
+ function stripAnsi(value) {
470
+ return value.replace(ANSI_ESCAPE_PATTERN, '');
471
+ }
472
+ async function codexIntegrationCheck(rt) {
473
+ const script = await scriptCheck(join(rt.homeDir, '.codex', 'hooks', 'codex-session-mapping.cjs'), join(rt.assetRoot ?? '', 'codex', 'codex-session-mapping.cjs'), rt);
474
+ const registration = await registrationCheck(join(rt.homeDir, '.codex', 'hooks.json'), 'SessionStart', 'node ~/.codex/hooks/codex-session-mapping.cjs', rt);
475
+ const mappingFile = await mappingCheck(join(rt.homeDir, '.codex', 'ai-devkit', 'sessions.json'), rt);
476
+ const installed = script.status === 'pass' && registration.status === 'pass';
477
+ return {
478
+ label: 'ai-devkit hook',
479
+ installed,
480
+ status: worstReadinessStatus([
481
+ script.status,
482
+ registration.status,
483
+ mappingFile.status
484
+ ]),
485
+ errors: [
486
+ ...script.errors,
487
+ ...registration.errors,
488
+ ...mappingFile.errors
489
+ ],
490
+ details: {
491
+ sessionMappingScript: script,
492
+ registration,
493
+ mappingFile
494
+ }
495
+ };
496
+ }
497
+ async function claudeIntegrationCheck(rt) {
498
+ const script = await scriptCheck(join(rt.homeDir, '.claude', 'hooks', 'claude-prompt-hook.js'), join(rt.assetRoot ?? '', 'claude', 'claude-prompt-hook.js'), rt);
499
+ const registration = await registrationCheck(join(rt.homeDir, '.claude', 'settings.json'), 'PreToolUse', 'node ~/.claude/hooks/claude-prompt-hook.js', rt);
500
+ const installed = script.status === 'pass' && registration.status === 'pass';
501
+ return {
502
+ label: 'ai-devkit hook',
503
+ installed,
504
+ status: worstReadinessStatus([
505
+ script.status,
506
+ registration.status
507
+ ]),
508
+ errors: [
509
+ ...script.errors,
510
+ ...registration.errors
511
+ ],
512
+ details: {
513
+ promptScript: script,
514
+ registration
515
+ }
516
+ };
517
+ }
518
+ async function piIntegrationCheck(rt) {
519
+ let installed = false;
520
+ try {
521
+ const result = await rt.runCommand('pi', [
522
+ 'list'
523
+ ]);
524
+ installed = isPiSessionTrackerListed(result.stdout);
525
+ } catch {
526
+ // Safe fixed result below.
527
+ }
528
+ const mapping = await mappingCheck(join(rt.homeDir, '.pi', 'agent', 'sessions.json'), rt);
529
+ const mappingStatus = mapping.valid || !mapping.present ? 'pass' : 'fail';
530
+ const status = worstReadinessStatus([
531
+ installed ? 'pass' : 'fail',
532
+ mappingStatus
533
+ ]);
534
+ return {
535
+ label: 'ai-devkit plugin',
536
+ installed,
537
+ status,
538
+ errors: [
539
+ ...!installed ? [
540
+ 'Pi session tracker is not registered'
541
+ ] : [],
542
+ ...mappingStatus === 'fail' ? mapping.errors : []
543
+ ],
544
+ details: {
545
+ package: '@ai-devkit/pi-session-tracker',
546
+ registryPath: mapping.path,
547
+ registryValid: mapping.valid,
548
+ invalidEntries: mapping.invalidEntries,
549
+ staleEntries: mapping.staleEntries
550
+ }
551
+ };
552
+ }
553
+ function isPiSessionTrackerListed(output) {
554
+ const normalized = output.toLowerCase();
555
+ return normalized.includes('@ai-devkit/pi-session-tracker') || /\bsession[\s-]+tracker\b/.test(normalized);
556
+ }
557
+ const AUTH_CHECKS = {
558
+ claude: claudeAuthCheck,
559
+ codex: codexAuthCheck,
560
+ copilot: copilotAuthCheck,
561
+ opencode: opencodeAuthCheck,
562
+ pi: piAuthCheck
563
+ };
564
+ const INTEGRATION_CHECKS = {
565
+ claude: claudeIntegrationCheck,
566
+ codex: codexIntegrationCheck,
567
+ pi: piIntegrationCheck
568
+ };
569
+ async function authCheck(agent, rt) {
570
+ return AUTH_CHECKS[agent]?.(rt);
571
+ }
572
+ async function integrationCheck(agent, rt) {
573
+ return INTEGRATION_CHECKS[agent]?.(rt);
574
+ }
575
+ export async function getAgentReadinessReport(agent, options = {}) {
576
+ return agentReadiness(agent, runtime(options));
577
+ }
578
+ async function agentReadiness(agent, rt) {
579
+ const [executable, globalConfig, builtInSkills, auth, integration] = await Promise.all([
580
+ executableCheck(agent, rt),
581
+ directoryCheck(agent, rt),
582
+ builtInSkillsCheck(agent, rt),
583
+ authCheck(agent, rt),
584
+ integrationCheck(agent, rt)
585
+ ]);
586
+ return {
587
+ type: agent,
588
+ executable,
589
+ globalConfig,
590
+ builtInSkills,
591
+ auth,
592
+ integration,
593
+ status: worstReadinessStatus([
594
+ executable.status,
595
+ globalConfig.status,
596
+ ...auth ? [
597
+ auth.status
598
+ ] : [],
599
+ ...integration ? [
600
+ integration.status
601
+ ] : []
602
+ ])
603
+ };
604
+ }
605
+ export async function getAgentReadinessReports(options = {}) {
606
+ const rt = runtime(options);
607
+ const entries = await Promise.all(READINESS_AGENT_TYPES.map(async (agent)=>[
608
+ agent,
609
+ await agentReadiness(agent, rt)
610
+ ]));
611
+ return Object.fromEntries(entries);
612
+ }
613
+
614
+ //# sourceMappingURL=AgentReadiness.js.map