@raidiant/notifai 0.2.0 → 0.3.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.
- package/dist/commands.d.ts +33 -3
- package/dist/commands.js +283 -59
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +62 -0
- package/dist/config-schema.js +274 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/hooks.d.ts +31 -19
- package/dist/hooks.js +277 -189
- package/dist/hooks.js.map +1 -1
- package/dist/interactive.d.ts +4 -0
- package/dist/interactive.js +604 -0
- package/dist/interactive.js.map +1 -0
- package/dist/main.js +106 -17
- package/dist/main.js.map +1 -1
- package/dist/ui/banner.d.ts +22 -0
- package/dist/ui/banner.js +87 -0
- package/dist/ui/banner.js.map +1 -0
- package/dist/ui/config-view.d.ts +32 -0
- package/dist/ui/config-view.js +105 -0
- package/dist/ui/config-view.js.map +1 -0
- package/dist/ui/help.d.ts +46 -0
- package/dist/ui/help.js +105 -0
- package/dist/ui/help.js.map +1 -0
- package/dist/ui/theme.d.ts +108 -0
- package/dist/ui/theme.js +261 -0
- package/dist/ui/theme.js.map +1 -0
- package/package.json +2 -1
- package/src/commands.test.ts +153 -11
- package/src/commands.ts +328 -60
- package/src/config-schema.test.ts +152 -0
- package/src/config-schema.ts +321 -0
- package/src/hooks.test.ts +214 -81
- package/src/hooks.ts +335 -224
- package/src/interactive.ts +692 -0
- package/src/main.ts +107 -16
- package/src/ui/banner.ts +95 -0
- package/src/ui/config-view.test.ts +86 -0
- package/src/ui/config-view.ts +135 -0
- package/src/ui/help.ts +124 -0
- package/src/ui/theme.test.ts +92 -0
- package/src/ui/theme.ts +279 -0
package/dist/commands.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type ListRepliesResponse, type Platform, type QuestionT, type ReplyView } from '@raidiant/notifai-protocol';
|
|
1
|
+
import { type ListRepliesResponse, type Platform, type QuestionT, type ReplyView, type RoutableDevice } from '@raidiant/notifai-protocol';
|
|
2
2
|
import { type ApiClient, type ClientOptions } from './client.js';
|
|
3
3
|
import type { CredentialStore } from './credentials.js';
|
|
4
4
|
import { type Readiness } from './readiness.js';
|
|
5
|
+
import type { Tone } from './ui/theme.js';
|
|
5
6
|
import { type HookHarness } from './hooks.js';
|
|
6
7
|
import { type SendFlags } from './send.js';
|
|
7
8
|
import type { NativeSkills, SkillScope } from './native-skills.js';
|
|
@@ -28,7 +29,13 @@ export interface CommandIo {
|
|
|
28
29
|
outro?(message: string): Promise<void>;
|
|
29
30
|
note?(message: string, title?: string): Promise<void>;
|
|
30
31
|
spinner?(message: string): Promise<CommandSpinner | null>;
|
|
31
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A report line. `tone` distinguishes the two states that are neither pass
|
|
34
|
+
* nor fail — something optional the user declined, and something that was
|
|
35
|
+
* never evaluated — which a boolean has to round to one or the other.
|
|
36
|
+
* Optional so existing implementations keep type-checking.
|
|
37
|
+
*/
|
|
38
|
+
check?(ok: boolean, message: string, tone?: Tone): Promise<void>;
|
|
32
39
|
}
|
|
33
40
|
export interface CommandSpinner {
|
|
34
41
|
message(message: string): void;
|
|
@@ -75,6 +82,18 @@ export declare function accessStatusCommand(deps: CommandDeps, flags: {
|
|
|
75
82
|
export declare function devicesCommand(deps: CommandDeps, flags: {
|
|
76
83
|
json?: boolean;
|
|
77
84
|
}): Promise<number>;
|
|
85
|
+
/**
|
|
86
|
+
* Devices as data, for surfaces that render them themselves.
|
|
87
|
+
*
|
|
88
|
+
* `devicesCommand` prints and returns an exit code, which is the right shape
|
|
89
|
+
* for a command and the wrong one for a menu that wants to show names beside
|
|
90
|
+
* checkboxes. Silent on every failure by design: the interactive app already
|
|
91
|
+
* shows the credential and connectivity state on its status card, so a second
|
|
92
|
+
* error line here would be reporting the same fault twice.
|
|
93
|
+
*/
|
|
94
|
+
export declare function deviceInventory(deps: CommandDeps): Promise<RoutableDevice[] | null>;
|
|
95
|
+
/** Whether a device is in a state where a notification would actually arrive. */
|
|
96
|
+
export declare function canDeviceReceive(device: RoutableDevice): boolean;
|
|
78
97
|
export declare function capabilitiesCommand(deps: CommandDeps, flags: {
|
|
79
98
|
json?: boolean;
|
|
80
99
|
platform?: Platform;
|
|
@@ -197,6 +216,17 @@ export declare function hooksUninstallCommand(deps: CommandDeps, flags: HooksIns
|
|
|
197
216
|
export declare function configShowCommand(deps: CommandDeps, flags: {
|
|
198
217
|
json?: boolean;
|
|
199
218
|
explain?: boolean;
|
|
219
|
+
plain?: boolean;
|
|
220
|
+
}): number;
|
|
221
|
+
/**
|
|
222
|
+
* One setting, explained in full.
|
|
223
|
+
*
|
|
224
|
+
* The gap this closes: `config show` prints `require_idle = true` and there was
|
|
225
|
+
* nowhere to go from there. Every key already had a careful explanation — in a
|
|
226
|
+
* TypeScript comment, read by everyone except the person who needed it.
|
|
227
|
+
*/
|
|
228
|
+
export declare function configExplainCommand(deps: CommandDeps, key: string, flags?: {
|
|
229
|
+
json?: boolean;
|
|
200
230
|
}): number;
|
|
201
231
|
export declare function configSetCommand(deps: CommandDeps, key: string, rawValue: string, flags: {
|
|
202
232
|
project?: boolean;
|
|
@@ -209,7 +239,7 @@ export declare function configSetCommand(deps: CommandDeps, key: string, rawValu
|
|
|
209
239
|
* 1.5.x, `owner/repo@name` selects a skill; a Git ref belongs after `#`.
|
|
210
240
|
* Keep this immutable and public because the command is printed to users.
|
|
211
241
|
*/
|
|
212
|
-
export declare const SKILLS_SOURCE = "Raidiant-io/notifai#v0.
|
|
242
|
+
export declare const SKILLS_SOURCE = "Raidiant-io/notifai#v0.3.0";
|
|
213
243
|
/** Derive a contract-valid project slug from a directory name. */
|
|
214
244
|
export declare function projectSlugFrom(name: string): string;
|
|
215
245
|
export interface InitFlags {
|
package/dist/commands.js
CHANGED
|
@@ -8,7 +8,9 @@ import { CAPABILITIES_V1, QUESTION_TEXT_MAX_LENGTH, REPLY_MAX_QUESTIONS, REPLY_M
|
|
|
8
8
|
import { sha256Hex } from '@raidiant/notifai-protocol/node';
|
|
9
9
|
import { ApiCallError, NetworkError, createClient, } from './client.js';
|
|
10
10
|
import { BOOLEAN_CONFIG_KEYS, CONFIG_KEYS, NUMERIC_CONFIG_KEYS, configBounds, findProjectConfigPath, findProjectLocalConfigPath, globalConfigPath, loadConfig, sessionConfigPath, } from './config.js';
|
|
11
|
-
import {
|
|
11
|
+
import { acceptedValues, configInfo } from './config-schema.js';
|
|
12
|
+
import { renderConfigExplain, renderConfigList, renderConfigPlain, } from './ui/config-view.js';
|
|
13
|
+
import { firstBlocker, openItems, } from './readiness.js';
|
|
12
14
|
import { handleSessionEnd, handleStop, handleUserPromptSubmit, parseHookInput, pruneAbandonedSessions, readProjectSession, readProjectSessionPointer, readSessionState, registerQuestion, } from './hooks.js';
|
|
13
15
|
import { readIdleSeconds } from './idle.js';
|
|
14
16
|
import { HARNESSES, applyPlan, blockingHookTimeoutSeconds, buildCursorHookConfig, buildHookConfig, codexLayerDir, codexProjectRoot, detectHarness, findInstallations, handlerEvent, loadCursorSettings, loadSettings, mergeCursorHooks, mergeHooks, removeCursorHooks, removeHooks, settingsFile, } from './install-hooks.js';
|
|
@@ -237,6 +239,33 @@ export async function devicesCommand(deps, flags) {
|
|
|
237
239
|
return reportError(deps, err);
|
|
238
240
|
}
|
|
239
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Devices as data, for surfaces that render them themselves.
|
|
244
|
+
*
|
|
245
|
+
* `devicesCommand` prints and returns an exit code, which is the right shape
|
|
246
|
+
* for a command and the wrong one for a menu that wants to show names beside
|
|
247
|
+
* checkboxes. Silent on every failure by design: the interactive app already
|
|
248
|
+
* shows the credential and connectivity state on its status card, so a second
|
|
249
|
+
* error line here would be reporting the same fault twice.
|
|
250
|
+
*/
|
|
251
|
+
export async function deviceInventory(deps) {
|
|
252
|
+
if (!deps.store.load())
|
|
253
|
+
return null;
|
|
254
|
+
const config = loadConfig({ cwd: deps.cwd, env: deps.env });
|
|
255
|
+
const authed = authedClient(deps, config);
|
|
256
|
+
if (!authed)
|
|
257
|
+
return null;
|
|
258
|
+
try {
|
|
259
|
+
return (await authed.client.listDevices()).devices;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** Whether a device is in a state where a notification would actually arrive. */
|
|
266
|
+
export function canDeviceReceive(device) {
|
|
267
|
+
return deviceCanReceive(device);
|
|
268
|
+
}
|
|
240
269
|
export async function capabilitiesCommand(deps, flags) {
|
|
241
270
|
const config = loadConfig({ cwd: deps.cwd, env: deps.env });
|
|
242
271
|
const credential = deps.store.load();
|
|
@@ -433,20 +462,25 @@ export async function repliesCommand(deps, requestedId, flags) {
|
|
|
433
462
|
deps.io.err('Pass a request id or --pending, not both.');
|
|
434
463
|
return EXIT.usage;
|
|
435
464
|
}
|
|
436
|
-
let
|
|
465
|
+
let requestIds = requestedId === undefined ? [] : [requestedId];
|
|
437
466
|
if (flags.pending === true) {
|
|
438
467
|
const sessionId = readProjectSession(deps.cwd, deps.env, (deps.now ?? Date.now)());
|
|
439
468
|
if (sessionId === null) {
|
|
440
469
|
deps.io.err('No active session pointer is available in this directory.');
|
|
441
470
|
return EXIT.noReply;
|
|
442
471
|
}
|
|
443
|
-
|
|
444
|
-
|
|
472
|
+
// Every delivered question in the session's queue, in registration order —
|
|
473
|
+
// an agent may have several outstanding at once.
|
|
474
|
+
const state = readSessionState(sessionId, deps.env);
|
|
475
|
+
requestIds = (Array.isArray(state.pending) ? state.pending : [])
|
|
476
|
+
.map((entry) => entry.request_id)
|
|
477
|
+
.filter((id) => id !== undefined);
|
|
478
|
+
if (requestIds.length === 0) {
|
|
445
479
|
deps.io.err(`Session ${sessionId} has no pushed question pending.`);
|
|
446
480
|
return EXIT.noReply;
|
|
447
481
|
}
|
|
448
482
|
}
|
|
449
|
-
if (
|
|
483
|
+
if (requestIds.length === 0) {
|
|
450
484
|
deps.io.err('Pass a request id or --pending.');
|
|
451
485
|
return EXIT.usage;
|
|
452
486
|
}
|
|
@@ -455,27 +489,44 @@ export async function repliesCommand(deps, requestedId, flags) {
|
|
|
455
489
|
if (!authed)
|
|
456
490
|
return EXIT.auth;
|
|
457
491
|
try {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
492
|
+
let anyReplies = false;
|
|
493
|
+
let anyDegraded = false;
|
|
494
|
+
let allTimedOut = true;
|
|
495
|
+
const jsonBodies = [];
|
|
496
|
+
for (const requestId of requestIds) {
|
|
497
|
+
const result = await waitForReply(authed.client, requestId, {
|
|
498
|
+
timeoutSeconds: waitSeconds,
|
|
499
|
+
afterSeq,
|
|
500
|
+
now: deps.now,
|
|
501
|
+
sleep: deps.sleep,
|
|
502
|
+
});
|
|
503
|
+
if (flags.json) {
|
|
504
|
+
jsonBodies.push({ ...result.response, degraded: result.degraded });
|
|
505
|
+
}
|
|
506
|
+
else if (result.response.replies.length > 0) {
|
|
507
|
+
if (flags.pending === true)
|
|
508
|
+
deps.io.out(`pending request ${requestId}`);
|
|
509
|
+
printReplies(deps, result.response.replies);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
printNoReply(deps, requestId, result.response.reply_expires_at);
|
|
513
|
+
}
|
|
514
|
+
anyReplies ||= result.response.replies.length > 0;
|
|
515
|
+
anyDegraded ||= result.degraded;
|
|
516
|
+
allTimedOut &&= result.timedOut;
|
|
466
517
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
518
|
+
if (flags.json) {
|
|
519
|
+
// One request keeps the response shape agents already parse; several
|
|
520
|
+
// (only possible via --pending) arrive as an array in queue order.
|
|
521
|
+
deps.io.out(JSON.stringify(jsonBodies.length === 1 ? jsonBodies[0] : jsonBodies, null, 2));
|
|
471
522
|
}
|
|
472
|
-
|
|
473
|
-
printNoReply(deps, requestId, result.response.reply_expires_at);
|
|
474
|
-
if (result.degraded) {
|
|
523
|
+
if (anyDegraded) {
|
|
475
524
|
deps.io.err(DEGRADED_WAIT_WARNING);
|
|
476
525
|
return EXIT.network;
|
|
477
526
|
}
|
|
478
|
-
|
|
527
|
+
if (anyReplies)
|
|
528
|
+
return EXIT.ok;
|
|
529
|
+
return allTimedOut ? EXIT.noReply : EXIT.ok;
|
|
479
530
|
}
|
|
480
531
|
catch (err) {
|
|
481
532
|
return reportError(deps, err);
|
|
@@ -1378,39 +1429,133 @@ export function configShowCommand(deps, flags) {
|
|
|
1378
1429
|
deps.io.out(JSON.stringify(output, null, 2));
|
|
1379
1430
|
return EXIT.ok;
|
|
1380
1431
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1432
|
+
// Anything that is not a person at a terminal keeps the flat `key = value`
|
|
1433
|
+
// form it has always had. Scripts parse this, and a prettier layout for an
|
|
1434
|
+
// audience that cannot see it would only be a breaking change.
|
|
1435
|
+
if (deps.io.interactive !== true || flags.plain === true) {
|
|
1436
|
+
for (const line of renderConfigPlain(config, flags.explain === true))
|
|
1437
|
+
deps.io.out(line);
|
|
1438
|
+
return EXIT.ok;
|
|
1439
|
+
}
|
|
1440
|
+
for (const line of renderConfigList(config, { showAdvanced: flags.explain === true })) {
|
|
1441
|
+
deps.io.out(line);
|
|
1385
1442
|
}
|
|
1386
1443
|
return EXIT.ok;
|
|
1387
1444
|
}
|
|
1445
|
+
/**
|
|
1446
|
+
* One setting, explained in full.
|
|
1447
|
+
*
|
|
1448
|
+
* The gap this closes: `config show` prints `require_idle = true` and there was
|
|
1449
|
+
* nowhere to go from there. Every key already had a careful explanation — in a
|
|
1450
|
+
* TypeScript comment, read by everyone except the person who needed it.
|
|
1451
|
+
*/
|
|
1452
|
+
export function configExplainCommand(deps, key, flags = {}) {
|
|
1453
|
+
if (!CONFIG_KEYS.includes(key)) {
|
|
1454
|
+
deps.io.err(`Unknown setting "${key}".`);
|
|
1455
|
+
deps.io.err(`Valid settings: ${CONFIG_KEYS.join(', ')}`);
|
|
1456
|
+
return EXIT.usage;
|
|
1457
|
+
}
|
|
1458
|
+
const configKey = key;
|
|
1459
|
+
const config = loadConfig({ cwd: deps.cwd, env: deps.env });
|
|
1460
|
+
const info = configInfo(configKey);
|
|
1461
|
+
const entry = config[configKey];
|
|
1462
|
+
if (flags.json) {
|
|
1463
|
+
deps.io.out(JSON.stringify({
|
|
1464
|
+
key: configKey,
|
|
1465
|
+
label: info.label,
|
|
1466
|
+
group: info.group,
|
|
1467
|
+
kind: info.kind,
|
|
1468
|
+
summary: info.summary,
|
|
1469
|
+
detail: info.detail,
|
|
1470
|
+
accepts: acceptedValues(configKey),
|
|
1471
|
+
...(info.choices !== undefined ? { choices: info.choices } : {}),
|
|
1472
|
+
value: entry.value,
|
|
1473
|
+
source: entry.source,
|
|
1474
|
+
}, null, 2));
|
|
1475
|
+
return EXIT.ok;
|
|
1476
|
+
}
|
|
1477
|
+
if (deps.io.interactive !== true) {
|
|
1478
|
+
deps.io.out(`${configKey} = ${JSON.stringify(entry.value)} [${entry.source}]`);
|
|
1479
|
+
deps.io.out(info.detail.replace(/\n\n/g, '\n'));
|
|
1480
|
+
deps.io.out(`accepts: ${acceptedValues(configKey)}`);
|
|
1481
|
+
return EXIT.ok;
|
|
1482
|
+
}
|
|
1483
|
+
for (const line of renderConfigExplain(configKey, config))
|
|
1484
|
+
deps.io.out(line);
|
|
1485
|
+
return EXIT.ok;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Closest config key by edit distance, or null when nothing is close.
|
|
1489
|
+
*
|
|
1490
|
+
* The threshold matters more than the algorithm: suggesting a key that shares
|
|
1491
|
+
* three letters with the typo sends the reader to the wrong setting with
|
|
1492
|
+
* confidence, which is worse than listing all fourteen and letting them look.
|
|
1493
|
+
*/
|
|
1494
|
+
function nearestKey(input) {
|
|
1495
|
+
let best = null;
|
|
1496
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1497
|
+
for (const candidate of CONFIG_KEYS) {
|
|
1498
|
+
const distance = editDistance(input.toLowerCase(), candidate);
|
|
1499
|
+
if (distance < bestDistance) {
|
|
1500
|
+
bestDistance = distance;
|
|
1501
|
+
best = candidate;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return best !== null && bestDistance <= Math.max(2, Math.floor(best.length / 3)) ? best : null;
|
|
1505
|
+
}
|
|
1506
|
+
function editDistance(a, b) {
|
|
1507
|
+
const previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1508
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
1509
|
+
let diagonal = previous[0];
|
|
1510
|
+
previous[0] = i;
|
|
1511
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
1512
|
+
const above = previous[j];
|
|
1513
|
+
previous[j] = Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1514
|
+
diagonal = above;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return previous[b.length];
|
|
1518
|
+
}
|
|
1388
1519
|
export async function configSetCommand(deps, key, rawValue, flags) {
|
|
1389
1520
|
if (!CONFIG_KEYS.includes(key)) {
|
|
1390
|
-
deps.io.err(`Unknown
|
|
1521
|
+
deps.io.err(`Unknown setting "${key}".`);
|
|
1522
|
+
const near = nearestKey(key);
|
|
1523
|
+
if (near !== null)
|
|
1524
|
+
deps.io.err(`Did you mean "${near}"?`);
|
|
1525
|
+
deps.io.err(`Valid settings: ${CONFIG_KEYS.join(', ')}`);
|
|
1526
|
+
deps.io.err('Run `notifai config explain <key>` to see what one of them does.');
|
|
1391
1527
|
return EXIT.usage;
|
|
1392
1528
|
}
|
|
1529
|
+
const configKey = key;
|
|
1530
|
+
const info = configInfo(configKey);
|
|
1393
1531
|
let value = rawValue;
|
|
1394
|
-
if (NUMERIC_CONFIG_KEYS.includes(
|
|
1532
|
+
if (NUMERIC_CONFIG_KEYS.includes(configKey)) {
|
|
1395
1533
|
const numeric = Number(rawValue);
|
|
1396
1534
|
if (!Number.isInteger(numeric)) {
|
|
1397
|
-
deps.io.err(
|
|
1535
|
+
deps.io.err(`${key} takes ${acceptedValues(configKey)}, not "${rawValue}".`);
|
|
1398
1536
|
return EXIT.usage;
|
|
1399
1537
|
}
|
|
1400
|
-
const bounds = configBounds(
|
|
1538
|
+
const bounds = configBounds(configKey);
|
|
1401
1539
|
if (bounds !== undefined && (numeric < bounds.min || numeric > bounds.max)) {
|
|
1402
1540
|
deps.io.err(`${key} must be between ${bounds.min} and ${bounds.max}.`);
|
|
1403
1541
|
return EXIT.usage;
|
|
1404
1542
|
}
|
|
1405
1543
|
value = numeric;
|
|
1406
1544
|
}
|
|
1407
|
-
if (BOOLEAN_CONFIG_KEYS.includes(
|
|
1545
|
+
if (BOOLEAN_CONFIG_KEYS.includes(configKey)) {
|
|
1408
1546
|
if (rawValue !== 'true' && rawValue !== 'false') {
|
|
1409
1547
|
deps.io.err(`${key} is a toggle — pass "true" or "false", not "${rawValue}".`);
|
|
1410
1548
|
return EXIT.usage;
|
|
1411
1549
|
}
|
|
1412
1550
|
value = rawValue === 'true';
|
|
1413
1551
|
}
|
|
1552
|
+
// Enum keys were accepted unchecked, so `config set sound whatever` wrote a
|
|
1553
|
+
// value the sender would later reject — a typo that only surfaces at the
|
|
1554
|
+
// moment a notification fails to carry the sound you asked for.
|
|
1555
|
+
if (info.kind === 'enum' && info.choices !== undefined && !info.choices.includes(rawValue)) {
|
|
1556
|
+
deps.io.err(`${key} takes one of: ${info.choices.join(', ')} — not "${rawValue}".`);
|
|
1557
|
+
return EXIT.usage;
|
|
1558
|
+
}
|
|
1414
1559
|
if (key === 'devices')
|
|
1415
1560
|
value = rawValue.split(',').map((s) => s.trim()).filter(Boolean);
|
|
1416
1561
|
let layer = flags.local ? 'local' : flags.project ? 'project' : 'global';
|
|
@@ -1462,7 +1607,7 @@ export async function configSetCommand(deps, key, rawValue, flags) {
|
|
|
1462
1607
|
* 1.5.x, `owner/repo@name` selects a skill; a Git ref belongs after `#`.
|
|
1463
1608
|
* Keep this immutable and public because the command is printed to users.
|
|
1464
1609
|
*/
|
|
1465
|
-
export const SKILLS_SOURCE = 'Raidiant-io/notifai#v0.
|
|
1610
|
+
export const SKILLS_SOURCE = 'Raidiant-io/notifai#v0.3.0';
|
|
1466
1611
|
function skillSourceParts() {
|
|
1467
1612
|
const match = /^([^#]+)#(.+)$/.exec(SKILLS_SOURCE);
|
|
1468
1613
|
return match === null ? null : { source: match[1], ref: match[2] };
|
|
@@ -2427,8 +2572,13 @@ export async function doctorCommand(deps, flags) {
|
|
|
2427
2572
|
// costs nothing.
|
|
2428
2573
|
if (deps.io.interactive === true && deps.io.check) {
|
|
2429
2574
|
await deps.io.intro?.('Notifai doctor');
|
|
2430
|
-
for (const s of readiness.states)
|
|
2431
|
-
|
|
2575
|
+
for (const s of readiness.states) {
|
|
2576
|
+
// Four states, not two. Rendering `optional-gap` and `unknown` as
|
|
2577
|
+
// successes made a green report that had never checked half of what it
|
|
2578
|
+
// listed, and put a tick beside things the reader had deliberately
|
|
2579
|
+
// declined.
|
|
2580
|
+
await deps.io.check(s.status !== 'gap', line(s), doctorTone(s.status));
|
|
2581
|
+
}
|
|
2432
2582
|
await deps.io.outro?.(ok ? 'Everything looks good' : `Start with: ${remedyLine(blocker)}`);
|
|
2433
2583
|
}
|
|
2434
2584
|
else {
|
|
@@ -2447,6 +2597,19 @@ export async function doctorCommand(deps, flags) {
|
|
|
2447
2597
|
}
|
|
2448
2598
|
return ok ? EXIT.ok : EXIT.failed;
|
|
2449
2599
|
}
|
|
2600
|
+
/** Readiness status as a report tone. */
|
|
2601
|
+
function doctorTone(status) {
|
|
2602
|
+
switch (status) {
|
|
2603
|
+
case 'ready':
|
|
2604
|
+
return 'ok';
|
|
2605
|
+
case 'gap':
|
|
2606
|
+
return 'bad';
|
|
2607
|
+
case 'optional-gap':
|
|
2608
|
+
return 'warn';
|
|
2609
|
+
case 'unknown':
|
|
2610
|
+
return 'pending';
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2450
2613
|
/** One line telling the reader what to actually do about a state. */
|
|
2451
2614
|
function remedyLine(state) {
|
|
2452
2615
|
const remedy = state.remedy;
|
|
@@ -2474,12 +2637,16 @@ function remedyLine(state) {
|
|
|
2474
2637
|
* way and is not worth re-deriving. The judgment added here is which failures
|
|
2475
2638
|
* actually stand in the way.
|
|
2476
2639
|
*
|
|
2477
|
-
*
|
|
2478
|
-
*
|
|
2479
|
-
*
|
|
2480
|
-
*
|
|
2481
|
-
*
|
|
2482
|
-
*
|
|
2640
|
+
* Not everything failed is in the way. A pointer that has never been
|
|
2641
|
+
* published is the normal condition of an install thirty seconds old — the
|
|
2642
|
+
* next prompt fixes it and no command can — and OpenCode's inability to
|
|
2643
|
+
* resume an idle turn is a property of that harness rather than a fault in
|
|
2644
|
+
* this setup. Treating those as blocking would mean `init` could never
|
|
2645
|
+
* finish for an OpenCode user, or could only finish after a session had
|
|
2646
|
+
* already run — so they report as things worth knowing rather than things to
|
|
2647
|
+
* fix, and `init` walks on to the states it can actually close, delivery
|
|
2648
|
+
* proof included. Which failures are informational, and what the true remedy
|
|
2649
|
+
* is, is each check's own call (`HookCheck`).
|
|
2483
2650
|
*/
|
|
2484
2651
|
function hookStates(deps) {
|
|
2485
2652
|
const installations = findInstallations(deps.cwd, deps.env);
|
|
@@ -2519,23 +2686,30 @@ function hookStates(deps) {
|
|
|
2519
2686
|
}
|
|
2520
2687
|
/** Real but not in the way; see the note above. */
|
|
2521
2688
|
const informational = new Set(['hooks (opencode continuation)']);
|
|
2522
|
-
if (active === null)
|
|
2523
|
-
informational.add('hooks (fired)');
|
|
2524
2689
|
return [
|
|
2525
2690
|
...hookChecks(deps).map((check) => ({
|
|
2526
2691
|
id: check.name.replace(/[ ()]+/g, '-').replace(/-$/, ''),
|
|
2527
2692
|
title: check.name === 'hooks' || check.name.startsWith('hooks (active')
|
|
2528
2693
|
? 'Question routing'
|
|
2529
2694
|
: check.name,
|
|
2530
|
-
status: check.ok
|
|
2695
|
+
status: check.ok
|
|
2696
|
+
? 'ready'
|
|
2697
|
+
: check.informational === true || informational.has(check.name)
|
|
2698
|
+
? 'optional-gap'
|
|
2699
|
+
: 'gap',
|
|
2531
2700
|
detail: check.detail,
|
|
2532
2701
|
...(check.ok
|
|
2533
2702
|
? {}
|
|
2534
2703
|
: {
|
|
2535
2704
|
remedy: {
|
|
2536
2705
|
by: 'user-here',
|
|
2537
|
-
|
|
2538
|
-
|
|
2706
|
+
// The check's own remedy when it has one; the generic reinstall
|
|
2707
|
+
// line was wrong exactly where it mattered (an unfired pointer
|
|
2708
|
+
// needs a prompt, not `hooks install`).
|
|
2709
|
+
...(check.remedy ?? {
|
|
2710
|
+
summary: 'the detail above names what to change',
|
|
2711
|
+
command: 'notifai hooks install',
|
|
2712
|
+
}),
|
|
2539
2713
|
},
|
|
2540
2714
|
}),
|
|
2541
2715
|
})),
|
|
@@ -2573,21 +2747,52 @@ function hookChecks(deps) {
|
|
|
2573
2747
|
detail: activeInstallations.length > 0
|
|
2574
2748
|
? `active ${active.label} session has a matching hook installation`
|
|
2575
2749
|
: `active ${active.label} session has no matching hook installation — run \`notifai hooks install --harness ${active.harness}\``,
|
|
2750
|
+
...(activeInstallations.length > 0
|
|
2751
|
+
? {}
|
|
2752
|
+
: {
|
|
2753
|
+
remedy: {
|
|
2754
|
+
summary: `install hooks for the active ${active.label} session`,
|
|
2755
|
+
command: `notifai hooks install --harness ${active.harness}`,
|
|
2756
|
+
},
|
|
2757
|
+
}),
|
|
2576
2758
|
});
|
|
2577
2759
|
if (activeInstallations.length > 0) {
|
|
2578
2760
|
const pointer = readProjectSessionPointer(deps.cwd, deps.env, (deps.now ?? Date.now)());
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
:
|
|
2761
|
+
if (pointer === null) {
|
|
2762
|
+
// The normal condition of hooks installed moments ago: the pointer
|
|
2763
|
+
// appears when the harness next fires a hook, and no command can
|
|
2764
|
+
// force that. Informational, so `init` walks on to the states it can
|
|
2765
|
+
// actually prove instead of exiting over evidence only time produces.
|
|
2766
|
+
checks.push({
|
|
2767
|
+
name: 'hooks (active session)',
|
|
2768
|
+
ok: false,
|
|
2769
|
+
informational: true,
|
|
2770
|
+
detail: `active ${active.label} session has not published a live pointer — send one ${active.label} prompt, then check again`,
|
|
2771
|
+
remedy: {
|
|
2772
|
+
summary: `send one ${active.label} prompt — its hook publishes the routing pointer`,
|
|
2773
|
+
command: 'notifai doctor',
|
|
2774
|
+
},
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
else {
|
|
2778
|
+
const matches = pointer.harness === active.harness &&
|
|
2779
|
+
(active.sessionId === undefined || pointer.sessionId === active.sessionId);
|
|
2780
|
+
checks.push({
|
|
2781
|
+
name: 'hooks (active session)',
|
|
2782
|
+
ok: matches,
|
|
2783
|
+
detail: matches
|
|
2588
2784
|
? `the project pointer belongs to the active ${active.label} session`
|
|
2589
2785
|
: `the project pointer belongs to another ${active.label} session or harness; refusing cross-session routing`,
|
|
2590
|
-
|
|
2786
|
+
...(matches
|
|
2787
|
+
? {}
|
|
2788
|
+
: {
|
|
2789
|
+
remedy: {
|
|
2790
|
+
summary: `send one ${active.label} prompt in this session so the pointer belongs to it`,
|
|
2791
|
+
command: 'notifai doctor',
|
|
2792
|
+
},
|
|
2793
|
+
}),
|
|
2794
|
+
});
|
|
2795
|
+
}
|
|
2591
2796
|
}
|
|
2592
2797
|
}
|
|
2593
2798
|
// A handler naming an event this build dropped exits 2 every time the harness
|
|
@@ -2655,9 +2860,20 @@ function hookChecks(deps) {
|
|
|
2655
2860
|
checks.push({
|
|
2656
2861
|
name: 'hooks (fired)',
|
|
2657
2862
|
ok: fired,
|
|
2863
|
+
// Never-fired is evidence about the past, not a fault in the setup, and
|
|
2864
|
+
// the cure is a prompt no command can send.
|
|
2865
|
+
informational: true,
|
|
2658
2866
|
detail: fired
|
|
2659
2867
|
? 'a session in this directory has run them'
|
|
2660
2868
|
: `no session pointer from the last 24 hours — ${hookActivationAdvice(activationInstallations)}`,
|
|
2869
|
+
...(fired
|
|
2870
|
+
? {}
|
|
2871
|
+
: {
|
|
2872
|
+
remedy: {
|
|
2873
|
+
summary: 'send one prompt in a session here, then re-check',
|
|
2874
|
+
command: 'notifai doctor',
|
|
2875
|
+
},
|
|
2876
|
+
}),
|
|
2661
2877
|
});
|
|
2662
2878
|
if (installations.some((installation) => installation.harness === 'opencode')) {
|
|
2663
2879
|
checks.push({
|
|
@@ -2778,14 +2994,22 @@ export function realIo(env = process.env) {
|
|
|
2778
2994
|
error: (next) => progress.error(next),
|
|
2779
2995
|
};
|
|
2780
2996
|
},
|
|
2781
|
-
check: async (ok, message) => {
|
|
2997
|
+
check: async (ok, message, tone) => {
|
|
2782
2998
|
if (!interactive())
|
|
2783
2999
|
return;
|
|
2784
3000
|
const { log } = await clack();
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
3001
|
+
switch (tone ?? (ok ? 'ok' : 'bad')) {
|
|
3002
|
+
case 'ok':
|
|
3003
|
+
return log.success(message);
|
|
3004
|
+
case 'warn':
|
|
3005
|
+
return log.warn(message);
|
|
3006
|
+
case 'pending':
|
|
3007
|
+
// Never checked. `log.info` reads as neutral, which is the honest
|
|
3008
|
+
// rendering for a state no evidence was gathered about.
|
|
3009
|
+
return log.info(message);
|
|
3010
|
+
case 'bad':
|
|
3011
|
+
return log.error(message);
|
|
3012
|
+
}
|
|
2789
3013
|
},
|
|
2790
3014
|
openUrl: (url) => {
|
|
2791
3015
|
try {
|