@raidiant/notifai 0.2.1 → 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 +167 -18
- 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/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 +9 -8
- package/src/commands.test.ts +87 -8
- package/src/commands.ts +202 -17
- package/src/config-schema.test.ts +152 -0
- package/src/config-schema.ts +321 -0
- 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();
|
|
@@ -1400,39 +1429,133 @@ export function configShowCommand(deps, flags) {
|
|
|
1400
1429
|
deps.io.out(JSON.stringify(output, null, 2));
|
|
1401
1430
|
return EXIT.ok;
|
|
1402
1431
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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);
|
|
1442
|
+
}
|
|
1443
|
+
return EXIT.ok;
|
|
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;
|
|
1407
1482
|
}
|
|
1483
|
+
for (const line of renderConfigExplain(configKey, config))
|
|
1484
|
+
deps.io.out(line);
|
|
1408
1485
|
return EXIT.ok;
|
|
1409
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
|
+
}
|
|
1410
1519
|
export async function configSetCommand(deps, key, rawValue, flags) {
|
|
1411
1520
|
if (!CONFIG_KEYS.includes(key)) {
|
|
1412
|
-
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.');
|
|
1413
1527
|
return EXIT.usage;
|
|
1414
1528
|
}
|
|
1529
|
+
const configKey = key;
|
|
1530
|
+
const info = configInfo(configKey);
|
|
1415
1531
|
let value = rawValue;
|
|
1416
|
-
if (NUMERIC_CONFIG_KEYS.includes(
|
|
1532
|
+
if (NUMERIC_CONFIG_KEYS.includes(configKey)) {
|
|
1417
1533
|
const numeric = Number(rawValue);
|
|
1418
1534
|
if (!Number.isInteger(numeric)) {
|
|
1419
|
-
deps.io.err(
|
|
1535
|
+
deps.io.err(`${key} takes ${acceptedValues(configKey)}, not "${rawValue}".`);
|
|
1420
1536
|
return EXIT.usage;
|
|
1421
1537
|
}
|
|
1422
|
-
const bounds = configBounds(
|
|
1538
|
+
const bounds = configBounds(configKey);
|
|
1423
1539
|
if (bounds !== undefined && (numeric < bounds.min || numeric > bounds.max)) {
|
|
1424
1540
|
deps.io.err(`${key} must be between ${bounds.min} and ${bounds.max}.`);
|
|
1425
1541
|
return EXIT.usage;
|
|
1426
1542
|
}
|
|
1427
1543
|
value = numeric;
|
|
1428
1544
|
}
|
|
1429
|
-
if (BOOLEAN_CONFIG_KEYS.includes(
|
|
1545
|
+
if (BOOLEAN_CONFIG_KEYS.includes(configKey)) {
|
|
1430
1546
|
if (rawValue !== 'true' && rawValue !== 'false') {
|
|
1431
1547
|
deps.io.err(`${key} is a toggle — pass "true" or "false", not "${rawValue}".`);
|
|
1432
1548
|
return EXIT.usage;
|
|
1433
1549
|
}
|
|
1434
1550
|
value = rawValue === 'true';
|
|
1435
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
|
+
}
|
|
1436
1559
|
if (key === 'devices')
|
|
1437
1560
|
value = rawValue.split(',').map((s) => s.trim()).filter(Boolean);
|
|
1438
1561
|
let layer = flags.local ? 'local' : flags.project ? 'project' : 'global';
|
|
@@ -1484,7 +1607,7 @@ export async function configSetCommand(deps, key, rawValue, flags) {
|
|
|
1484
1607
|
* 1.5.x, `owner/repo@name` selects a skill; a Git ref belongs after `#`.
|
|
1485
1608
|
* Keep this immutable and public because the command is printed to users.
|
|
1486
1609
|
*/
|
|
1487
|
-
export const SKILLS_SOURCE = 'Raidiant-io/notifai#v0.
|
|
1610
|
+
export const SKILLS_SOURCE = 'Raidiant-io/notifai#v0.3.0';
|
|
1488
1611
|
function skillSourceParts() {
|
|
1489
1612
|
const match = /^([^#]+)#(.+)$/.exec(SKILLS_SOURCE);
|
|
1490
1613
|
return match === null ? null : { source: match[1], ref: match[2] };
|
|
@@ -2449,8 +2572,13 @@ export async function doctorCommand(deps, flags) {
|
|
|
2449
2572
|
// costs nothing.
|
|
2450
2573
|
if (deps.io.interactive === true && deps.io.check) {
|
|
2451
2574
|
await deps.io.intro?.('Notifai doctor');
|
|
2452
|
-
for (const s of readiness.states)
|
|
2453
|
-
|
|
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
|
+
}
|
|
2454
2582
|
await deps.io.outro?.(ok ? 'Everything looks good' : `Start with: ${remedyLine(blocker)}`);
|
|
2455
2583
|
}
|
|
2456
2584
|
else {
|
|
@@ -2469,6 +2597,19 @@ export async function doctorCommand(deps, flags) {
|
|
|
2469
2597
|
}
|
|
2470
2598
|
return ok ? EXIT.ok : EXIT.failed;
|
|
2471
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
|
+
}
|
|
2472
2613
|
/** One line telling the reader what to actually do about a state. */
|
|
2473
2614
|
function remedyLine(state) {
|
|
2474
2615
|
const remedy = state.remedy;
|
|
@@ -2853,14 +2994,22 @@ export function realIo(env = process.env) {
|
|
|
2853
2994
|
error: (next) => progress.error(next),
|
|
2854
2995
|
};
|
|
2855
2996
|
},
|
|
2856
|
-
check: async (ok, message) => {
|
|
2997
|
+
check: async (ok, message, tone) => {
|
|
2857
2998
|
if (!interactive())
|
|
2858
2999
|
return;
|
|
2859
3000
|
const { log } = await clack();
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
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
|
+
}
|
|
2864
3013
|
},
|
|
2865
3014
|
openUrl: (url) => {
|
|
2866
3015
|
try {
|