@raidiant/notifai 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +117 -42
- package/dist/commands.js.map +1 -1
- package/dist/hooks.d.ts +31 -19
- package/dist/hooks.js +277 -189
- package/dist/hooks.js.map +1 -1
- package/package.json +8 -8
- package/src/commands.test.ts +70 -7
- package/src/commands.ts +127 -44
- package/src/hooks.test.ts +214 -81
- package/src/hooks.ts +335 -224
package/dist/commands.d.ts
CHANGED
|
@@ -209,7 +209,7 @@ export declare function configSetCommand(deps: CommandDeps, key: string, rawValu
|
|
|
209
209
|
* 1.5.x, `owner/repo@name` selects a skill; a Git ref belongs after `#`.
|
|
210
210
|
* Keep this immutable and public because the command is printed to users.
|
|
211
211
|
*/
|
|
212
|
-
export declare const SKILLS_SOURCE = "Raidiant-io/notifai#v0.2.
|
|
212
|
+
export declare const SKILLS_SOURCE = "Raidiant-io/notifai#v0.2.1";
|
|
213
213
|
/** Derive a contract-valid project slug from a directory name. */
|
|
214
214
|
export declare function projectSlugFrom(name: string): string;
|
|
215
215
|
export interface InitFlags {
|
package/dist/commands.js
CHANGED
|
@@ -433,20 +433,25 @@ export async function repliesCommand(deps, requestedId, flags) {
|
|
|
433
433
|
deps.io.err('Pass a request id or --pending, not both.');
|
|
434
434
|
return EXIT.usage;
|
|
435
435
|
}
|
|
436
|
-
let
|
|
436
|
+
let requestIds = requestedId === undefined ? [] : [requestedId];
|
|
437
437
|
if (flags.pending === true) {
|
|
438
438
|
const sessionId = readProjectSession(deps.cwd, deps.env, (deps.now ?? Date.now)());
|
|
439
439
|
if (sessionId === null) {
|
|
440
440
|
deps.io.err('No active session pointer is available in this directory.');
|
|
441
441
|
return EXIT.noReply;
|
|
442
442
|
}
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
// Every delivered question in the session's queue, in registration order —
|
|
444
|
+
// an agent may have several outstanding at once.
|
|
445
|
+
const state = readSessionState(sessionId, deps.env);
|
|
446
|
+
requestIds = (Array.isArray(state.pending) ? state.pending : [])
|
|
447
|
+
.map((entry) => entry.request_id)
|
|
448
|
+
.filter((id) => id !== undefined);
|
|
449
|
+
if (requestIds.length === 0) {
|
|
445
450
|
deps.io.err(`Session ${sessionId} has no pushed question pending.`);
|
|
446
451
|
return EXIT.noReply;
|
|
447
452
|
}
|
|
448
453
|
}
|
|
449
|
-
if (
|
|
454
|
+
if (requestIds.length === 0) {
|
|
450
455
|
deps.io.err('Pass a request id or --pending.');
|
|
451
456
|
return EXIT.usage;
|
|
452
457
|
}
|
|
@@ -455,27 +460,44 @@ export async function repliesCommand(deps, requestedId, flags) {
|
|
|
455
460
|
if (!authed)
|
|
456
461
|
return EXIT.auth;
|
|
457
462
|
try {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
463
|
+
let anyReplies = false;
|
|
464
|
+
let anyDegraded = false;
|
|
465
|
+
let allTimedOut = true;
|
|
466
|
+
const jsonBodies = [];
|
|
467
|
+
for (const requestId of requestIds) {
|
|
468
|
+
const result = await waitForReply(authed.client, requestId, {
|
|
469
|
+
timeoutSeconds: waitSeconds,
|
|
470
|
+
afterSeq,
|
|
471
|
+
now: deps.now,
|
|
472
|
+
sleep: deps.sleep,
|
|
473
|
+
});
|
|
474
|
+
if (flags.json) {
|
|
475
|
+
jsonBodies.push({ ...result.response, degraded: result.degraded });
|
|
476
|
+
}
|
|
477
|
+
else if (result.response.replies.length > 0) {
|
|
478
|
+
if (flags.pending === true)
|
|
479
|
+
deps.io.out(`pending request ${requestId}`);
|
|
480
|
+
printReplies(deps, result.response.replies);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
printNoReply(deps, requestId, result.response.reply_expires_at);
|
|
484
|
+
}
|
|
485
|
+
anyReplies ||= result.response.replies.length > 0;
|
|
486
|
+
anyDegraded ||= result.degraded;
|
|
487
|
+
allTimedOut &&= result.timedOut;
|
|
466
488
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
489
|
+
if (flags.json) {
|
|
490
|
+
// One request keeps the response shape agents already parse; several
|
|
491
|
+
// (only possible via --pending) arrive as an array in queue order.
|
|
492
|
+
deps.io.out(JSON.stringify(jsonBodies.length === 1 ? jsonBodies[0] : jsonBodies, null, 2));
|
|
471
493
|
}
|
|
472
|
-
|
|
473
|
-
printNoReply(deps, requestId, result.response.reply_expires_at);
|
|
474
|
-
if (result.degraded) {
|
|
494
|
+
if (anyDegraded) {
|
|
475
495
|
deps.io.err(DEGRADED_WAIT_WARNING);
|
|
476
496
|
return EXIT.network;
|
|
477
497
|
}
|
|
478
|
-
|
|
498
|
+
if (anyReplies)
|
|
499
|
+
return EXIT.ok;
|
|
500
|
+
return allTimedOut ? EXIT.noReply : EXIT.ok;
|
|
479
501
|
}
|
|
480
502
|
catch (err) {
|
|
481
503
|
return reportError(deps, err);
|
|
@@ -1462,7 +1484,7 @@ export async function configSetCommand(deps, key, rawValue, flags) {
|
|
|
1462
1484
|
* 1.5.x, `owner/repo@name` selects a skill; a Git ref belongs after `#`.
|
|
1463
1485
|
* Keep this immutable and public because the command is printed to users.
|
|
1464
1486
|
*/
|
|
1465
|
-
export const SKILLS_SOURCE = 'Raidiant-io/notifai#v0.2.
|
|
1487
|
+
export const SKILLS_SOURCE = 'Raidiant-io/notifai#v0.2.1';
|
|
1466
1488
|
function skillSourceParts() {
|
|
1467
1489
|
const match = /^([^#]+)#(.+)$/.exec(SKILLS_SOURCE);
|
|
1468
1490
|
return match === null ? null : { source: match[1], ref: match[2] };
|
|
@@ -2474,12 +2496,16 @@ function remedyLine(state) {
|
|
|
2474
2496
|
* way and is not worth re-deriving. The judgment added here is which failures
|
|
2475
2497
|
* actually stand in the way.
|
|
2476
2498
|
*
|
|
2477
|
-
*
|
|
2478
|
-
*
|
|
2479
|
-
*
|
|
2480
|
-
*
|
|
2481
|
-
*
|
|
2482
|
-
*
|
|
2499
|
+
* Not everything failed is in the way. A pointer that has never been
|
|
2500
|
+
* published is the normal condition of an install thirty seconds old — the
|
|
2501
|
+
* next prompt fixes it and no command can — and OpenCode's inability to
|
|
2502
|
+
* resume an idle turn is a property of that harness rather than a fault in
|
|
2503
|
+
* this setup. Treating those as blocking would mean `init` could never
|
|
2504
|
+
* finish for an OpenCode user, or could only finish after a session had
|
|
2505
|
+
* already run — so they report as things worth knowing rather than things to
|
|
2506
|
+
* fix, and `init` walks on to the states it can actually close, delivery
|
|
2507
|
+
* proof included. Which failures are informational, and what the true remedy
|
|
2508
|
+
* is, is each check's own call (`HookCheck`).
|
|
2483
2509
|
*/
|
|
2484
2510
|
function hookStates(deps) {
|
|
2485
2511
|
const installations = findInstallations(deps.cwd, deps.env);
|
|
@@ -2519,23 +2545,30 @@ function hookStates(deps) {
|
|
|
2519
2545
|
}
|
|
2520
2546
|
/** Real but not in the way; see the note above. */
|
|
2521
2547
|
const informational = new Set(['hooks (opencode continuation)']);
|
|
2522
|
-
if (active === null)
|
|
2523
|
-
informational.add('hooks (fired)');
|
|
2524
2548
|
return [
|
|
2525
2549
|
...hookChecks(deps).map((check) => ({
|
|
2526
2550
|
id: check.name.replace(/[ ()]+/g, '-').replace(/-$/, ''),
|
|
2527
2551
|
title: check.name === 'hooks' || check.name.startsWith('hooks (active')
|
|
2528
2552
|
? 'Question routing'
|
|
2529
2553
|
: check.name,
|
|
2530
|
-
status: check.ok
|
|
2554
|
+
status: check.ok
|
|
2555
|
+
? 'ready'
|
|
2556
|
+
: check.informational === true || informational.has(check.name)
|
|
2557
|
+
? 'optional-gap'
|
|
2558
|
+
: 'gap',
|
|
2531
2559
|
detail: check.detail,
|
|
2532
2560
|
...(check.ok
|
|
2533
2561
|
? {}
|
|
2534
2562
|
: {
|
|
2535
2563
|
remedy: {
|
|
2536
2564
|
by: 'user-here',
|
|
2537
|
-
|
|
2538
|
-
|
|
2565
|
+
// The check's own remedy when it has one; the generic reinstall
|
|
2566
|
+
// line was wrong exactly where it mattered (an unfired pointer
|
|
2567
|
+
// needs a prompt, not `hooks install`).
|
|
2568
|
+
...(check.remedy ?? {
|
|
2569
|
+
summary: 'the detail above names what to change',
|
|
2570
|
+
command: 'notifai hooks install',
|
|
2571
|
+
}),
|
|
2539
2572
|
},
|
|
2540
2573
|
}),
|
|
2541
2574
|
})),
|
|
@@ -2573,21 +2606,52 @@ function hookChecks(deps) {
|
|
|
2573
2606
|
detail: activeInstallations.length > 0
|
|
2574
2607
|
? `active ${active.label} session has a matching hook installation`
|
|
2575
2608
|
: `active ${active.label} session has no matching hook installation — run \`notifai hooks install --harness ${active.harness}\``,
|
|
2609
|
+
...(activeInstallations.length > 0
|
|
2610
|
+
? {}
|
|
2611
|
+
: {
|
|
2612
|
+
remedy: {
|
|
2613
|
+
summary: `install hooks for the active ${active.label} session`,
|
|
2614
|
+
command: `notifai hooks install --harness ${active.harness}`,
|
|
2615
|
+
},
|
|
2616
|
+
}),
|
|
2576
2617
|
});
|
|
2577
2618
|
if (activeInstallations.length > 0) {
|
|
2578
2619
|
const pointer = readProjectSessionPointer(deps.cwd, deps.env, (deps.now ?? Date.now)());
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
:
|
|
2620
|
+
if (pointer === null) {
|
|
2621
|
+
// The normal condition of hooks installed moments ago: the pointer
|
|
2622
|
+
// appears when the harness next fires a hook, and no command can
|
|
2623
|
+
// force that. Informational, so `init` walks on to the states it can
|
|
2624
|
+
// actually prove instead of exiting over evidence only time produces.
|
|
2625
|
+
checks.push({
|
|
2626
|
+
name: 'hooks (active session)',
|
|
2627
|
+
ok: false,
|
|
2628
|
+
informational: true,
|
|
2629
|
+
detail: `active ${active.label} session has not published a live pointer — send one ${active.label} prompt, then check again`,
|
|
2630
|
+
remedy: {
|
|
2631
|
+
summary: `send one ${active.label} prompt — its hook publishes the routing pointer`,
|
|
2632
|
+
command: 'notifai doctor',
|
|
2633
|
+
},
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
else {
|
|
2637
|
+
const matches = pointer.harness === active.harness &&
|
|
2638
|
+
(active.sessionId === undefined || pointer.sessionId === active.sessionId);
|
|
2639
|
+
checks.push({
|
|
2640
|
+
name: 'hooks (active session)',
|
|
2641
|
+
ok: matches,
|
|
2642
|
+
detail: matches
|
|
2588
2643
|
? `the project pointer belongs to the active ${active.label} session`
|
|
2589
2644
|
: `the project pointer belongs to another ${active.label} session or harness; refusing cross-session routing`,
|
|
2590
|
-
|
|
2645
|
+
...(matches
|
|
2646
|
+
? {}
|
|
2647
|
+
: {
|
|
2648
|
+
remedy: {
|
|
2649
|
+
summary: `send one ${active.label} prompt in this session so the pointer belongs to it`,
|
|
2650
|
+
command: 'notifai doctor',
|
|
2651
|
+
},
|
|
2652
|
+
}),
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2591
2655
|
}
|
|
2592
2656
|
}
|
|
2593
2657
|
// A handler naming an event this build dropped exits 2 every time the harness
|
|
@@ -2655,9 +2719,20 @@ function hookChecks(deps) {
|
|
|
2655
2719
|
checks.push({
|
|
2656
2720
|
name: 'hooks (fired)',
|
|
2657
2721
|
ok: fired,
|
|
2722
|
+
// Never-fired is evidence about the past, not a fault in the setup, and
|
|
2723
|
+
// the cure is a prompt no command can send.
|
|
2724
|
+
informational: true,
|
|
2658
2725
|
detail: fired
|
|
2659
2726
|
? 'a session in this directory has run them'
|
|
2660
2727
|
: `no session pointer from the last 24 hours — ${hookActivationAdvice(activationInstallations)}`,
|
|
2728
|
+
...(fired
|
|
2729
|
+
? {}
|
|
2730
|
+
: {
|
|
2731
|
+
remedy: {
|
|
2732
|
+
summary: 'send one prompt in a session here, then re-check',
|
|
2733
|
+
command: 'notifai doctor',
|
|
2734
|
+
},
|
|
2735
|
+
}),
|
|
2661
2736
|
});
|
|
2662
2737
|
if (installations.some((installation) => installation.harness === 'opencode')) {
|
|
2663
2738
|
checks.push({
|