@raidiant/notifai 9.3.0 → 10.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.
- package/CHANGELOG.md +40 -0
- package/README.md +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -1
- package/dist/commands-agent-sessions.d.ts +6 -0
- package/dist/commands-agent-sessions.js +51 -0
- package/dist/commands-agent-sessions.js.map +1 -0
- package/dist/commands-auth.js +4 -4
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-config.js +11 -4
- package/dist/commands-config.js.map +1 -1
- package/dist/commands-core.d.ts +5 -0
- package/dist/commands-core.js +2 -0
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-doctor.js +121 -40
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-harness-context.d.ts +15 -2
- package/dist/commands-harness-context.js +40 -1
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hooks.d.ts +4 -3
- package/dist/commands-hooks.js +139 -16
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.js +10 -5
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +18 -1
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.js +2 -2
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-sounds.d.ts +12 -0
- package/dist/commands-sounds.js +35 -0
- package/dist/commands-sounds.js.map +1 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +2 -0
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.js +5 -2
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +3 -4
- package/dist/config.js +5 -2
- package/dist/config.js.map +1 -1
- package/dist/credentials.js +4 -1
- package/dist/credentials.js.map +1 -1
- package/dist/harnesses.d.ts +49 -7
- package/dist/harnesses.js +110 -24
- package/dist/harnesses.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +2 -2
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +10 -11
- package/dist/install-hooks.js +18 -13
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.js +20 -23
- package/dist/interactive.js.map +1 -1
- package/dist/invocation-context.d.ts +2 -2
- package/dist/openclaw-plugin.d.ts +49 -0
- package/dist/openclaw-plugin.js +350 -0
- package/dist/openclaw-plugin.js.map +1 -0
- package/dist/orca-session-title.d.ts +10 -0
- package/dist/orca-session-title.js +98 -0
- package/dist/orca-session-title.js.map +1 -0
- package/dist/program.d.ts +3 -1
- package/dist/program.js +37 -13
- package/dist/program.js.map +1 -1
- package/dist/send.js +11 -14
- package/dist/send.js.map +1 -1
- package/dist/session-activation.js +1 -1
- package/dist/session-activation.js.map +1 -1
- package/dist/session-labels.d.ts +18 -2
- package/dist/session-labels.js +79 -8
- package/dist/session-labels.js.map +1 -1
- package/dist/sound-ref.d.ts +23 -0
- package/dist/sound-ref.js +28 -0
- package/dist/sound-ref.js.map +1 -0
- package/dist/ui/help.d.ts +4 -5
- package/dist/ui/help.js +5 -6
- package/dist/ui/help.js.map +1 -1
- package/package.json +3 -3
- package/src/client.test.ts +12 -0
- package/src/client.ts +8 -0
- package/src/commands-agent-sessions.ts +72 -0
- package/src/commands-auth.ts +4 -3
- package/src/commands-config.ts +10 -4
- package/src/commands-core.ts +6 -0
- package/src/commands-doctor.ts +143 -38
- package/src/commands-harness-context.ts +56 -2
- package/src/commands-hooks.ts +180 -20
- package/src/commands-init.ts +10 -4
- package/src/commands-send-support.ts +23 -1
- package/src/commands-send.ts +2 -2
- package/src/commands-sounds.test.ts +106 -0
- package/src/commands-sounds.ts +49 -0
- package/src/commands.test.ts +529 -17
- package/src/commands.ts +5 -0
- package/src/config-schema.test.ts +1 -0
- package/src/config-schema.ts +5 -2
- package/src/config.test.ts +42 -20
- package/src/config.ts +6 -5
- package/src/credentials.test.ts +20 -0
- package/src/credentials.ts +4 -1
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +50 -0
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +57 -0
- package/src/harness-integration.test.ts +294 -0
- package/src/harnesses.test.ts +115 -8
- package/src/harnesses.ts +133 -30
- package/src/hooks.test.ts +4 -3
- package/src/hooks.ts +4 -4
- package/src/install-hooks.test.ts +213 -4
- package/src/install-hooks.ts +38 -30
- package/src/interactive.test.ts +31 -0
- package/src/interactive.ts +20 -26
- package/src/invocation-context.ts +2 -2
- package/src/openclaw-plugin.ts +409 -0
- package/src/orca-session-title.test.ts +54 -0
- package/src/orca-session-title.ts +103 -0
- package/src/program.test.ts +27 -0
- package/src/program.ts +47 -12
- package/src/send.ts +10 -19
- package/src/session-activation.ts +1 -1
- package/src/session-labels.test.ts +58 -9
- package/src/session-labels.ts +95 -12
- package/src/skill.test.ts +14 -3
- package/src/sound-ref.test.ts +31 -0
- package/src/sound-ref.ts +34 -0
- package/src/ui/config-view.test.ts +1 -0
- package/src/ui/help.test.ts +16 -0
- package/src/ui/help.ts +6 -7
package/src/commands-hooks.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
lstatSync,
|
|
13
13
|
mkdirSync,
|
|
14
14
|
readFileSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
rmdirSync,
|
|
15
17
|
rmSync,
|
|
16
18
|
} from 'node:fs'
|
|
17
19
|
import path from 'node:path'
|
|
@@ -21,7 +23,14 @@ import { ApiCallError } from './client.js'
|
|
|
21
23
|
import { codexWakeRoute } from './codex-wake.js'
|
|
22
24
|
import { loadConfig, type CliConfig } from './config.js'
|
|
23
25
|
import { withTargetFileLock } from './file-lock.js'
|
|
24
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
HARNESS_LABELS,
|
|
28
|
+
HERMES_QUESTION_ROUTING_UNAVAILABLE,
|
|
29
|
+
HOOK_INSTALLABLE_HARNESSES,
|
|
30
|
+
isHookInstallableHarness,
|
|
31
|
+
questionRoutingCapability,
|
|
32
|
+
type HookInstallableHarness,
|
|
33
|
+
} from './harnesses.js'
|
|
25
34
|
import {
|
|
26
35
|
inspectHookAdapter,
|
|
27
36
|
installHookAdapter,
|
|
@@ -51,7 +60,6 @@ import {
|
|
|
51
60
|
} from './hooks.js'
|
|
52
61
|
import {
|
|
53
62
|
NON_ROUTING_BLOCKING_STOP_TIMEOUT_SECONDS,
|
|
54
|
-
HARNESSES,
|
|
55
63
|
applyPlan,
|
|
56
64
|
buildCursorHookConfig,
|
|
57
65
|
buildHookConfig,
|
|
@@ -75,13 +83,24 @@ import {
|
|
|
75
83
|
removeHooks,
|
|
76
84
|
settingsFile,
|
|
77
85
|
withCodexLayerTransaction,
|
|
78
|
-
type Harness,
|
|
79
86
|
type Installation,
|
|
80
87
|
} from './install-hooks.js'
|
|
81
88
|
import { QUESTION_WAITER_CEILING_SECONDS } from './question-timing.js'
|
|
82
89
|
import { inferInvocationContext } from './invocation-context.js'
|
|
83
90
|
import { logConfigResolved, logSettingsFrom } from './logging.js'
|
|
84
91
|
import { isOurOpencodePlugin, opencodePluginSource } from './opencode-plugin.js'
|
|
92
|
+
import {
|
|
93
|
+
OPENCLAW_PLUGIN_MANIFEST,
|
|
94
|
+
OPENCLAW_PLUGIN_PACKAGE,
|
|
95
|
+
isOurOpenclawPlugin,
|
|
96
|
+
mergeOpenclawNotifaiEntry,
|
|
97
|
+
openclawConfigPath,
|
|
98
|
+
openclawPluginManifest,
|
|
99
|
+
openclawPluginPackage,
|
|
100
|
+
openclawPluginSource,
|
|
101
|
+
parseOpenclawConfig,
|
|
102
|
+
removeOpenclawNotifaiEntry,
|
|
103
|
+
} from './openclaw-plugin.js'
|
|
85
104
|
import { packageVersion } from './release.js'
|
|
86
105
|
import { enableProject, projectBinding, projectEnabled } from './project-enablement.js'
|
|
87
106
|
import { rejectAccidentalEscapedNewlines } from './send.js'
|
|
@@ -99,6 +118,7 @@ import {
|
|
|
99
118
|
} from './send.js'
|
|
100
119
|
import {
|
|
101
120
|
EXIT,
|
|
121
|
+
SETUP_COMMAND,
|
|
102
122
|
authedClient,
|
|
103
123
|
diagnoseIgnoredOriginOverride,
|
|
104
124
|
loadLoggedConfig,
|
|
@@ -138,8 +158,8 @@ export const HOOK_EVENTS = [
|
|
|
138
158
|
export type HookEvent = (typeof HOOK_EVENTS)[number]
|
|
139
159
|
|
|
140
160
|
/** Lifecycle handlers one installed harness must carry in this CLI build. */
|
|
141
|
-
export function requiredHookEvents(harness:
|
|
142
|
-
if (harness === 'opencode') return []
|
|
161
|
+
export function requiredHookEvents(harness: HookInstallableHarness): readonly HookEvent[] {
|
|
162
|
+
if (harness === 'opencode' || harness === 'openclaw') return []
|
|
143
163
|
return harness === 'cursor'
|
|
144
164
|
? ['session-start', 'activation-stop', 'user-prompt-submit', 'stop', 'session-end']
|
|
145
165
|
: ['session-start', 'subagent-start', 'user-prompt-submit', 'stop', 'session-end']
|
|
@@ -464,7 +484,7 @@ export async function hookRunCommand(
|
|
|
464
484
|
const credential = deps.store.load()
|
|
465
485
|
if (!credential) {
|
|
466
486
|
logger.error('hook.end', { hook: event, outcome: 'not-paired' })
|
|
467
|
-
deps.io.err(
|
|
487
|
+
deps.io.err(`notifai: hook skipped: this machine is not paired; run \`${SETUP_COMMAND}\``)
|
|
468
488
|
return EXIT.ok
|
|
469
489
|
}
|
|
470
490
|
// Pin authenticated traffic to the origin the credential was issued for. A
|
|
@@ -572,7 +592,7 @@ export async function hookRunCommand(
|
|
|
572
592
|
*/
|
|
573
593
|
function stopWakeRoute(
|
|
574
594
|
deps: CommandDeps,
|
|
575
|
-
harness:
|
|
595
|
+
harness: HookInstallableHarness | undefined,
|
|
576
596
|
sessionId: string | undefined,
|
|
577
597
|
cwd: string,
|
|
578
598
|
): EscalationDeliveryRoute | undefined {
|
|
@@ -1102,6 +1122,17 @@ export function askCommand(
|
|
|
1102
1122
|
'run the ask from a shell with one exact active harness session',
|
|
1103
1123
|
)
|
|
1104
1124
|
}
|
|
1125
|
+
if (!isHookInstallableHarness(active.harness)) {
|
|
1126
|
+
const capability = HERMES_QUESTION_ROUTING_UNAVAILABLE
|
|
1127
|
+
return askFailure(
|
|
1128
|
+
deps,
|
|
1129
|
+
flags,
|
|
1130
|
+
'question_routing_unavailable',
|
|
1131
|
+
'hook_contract',
|
|
1132
|
+
`${active.label}: ${capability.deliveryContract}`,
|
|
1133
|
+
'use a blocking `notifai send --reply` question',
|
|
1134
|
+
)
|
|
1135
|
+
}
|
|
1105
1136
|
const exactState = active.sessionId === undefined
|
|
1106
1137
|
? null
|
|
1107
1138
|
: readSessionState(active.sessionId, deps.env)
|
|
@@ -1253,7 +1284,9 @@ export function activeQuestionRouteProblems(
|
|
|
1253
1284
|
`the active ${active.label} shell does not expose an exact session id; a project-level last-writer pointer can cross-wire two sessions. Use a blocking \`notifai send --reply\` question`,
|
|
1254
1285
|
)
|
|
1255
1286
|
}
|
|
1256
|
-
const capability =
|
|
1287
|
+
const capability = isHookInstallableHarness(active.harness)
|
|
1288
|
+
? questionRoutingCapability(active.harness)
|
|
1289
|
+
: HERMES_QUESTION_ROUTING_UNAVAILABLE
|
|
1257
1290
|
if (capability.stopContinuation === 'unsupported') {
|
|
1258
1291
|
problems.push(`${active.label}: ${capability.deliveryContract}`)
|
|
1259
1292
|
}
|
|
@@ -1330,6 +1363,11 @@ export function hookActivationAdvice(installations: Installation[]): string {
|
|
|
1330
1363
|
'OpenCode: restart it, then send one prompt; plugins load at startup, but non-blocking question continuation is intentionally unsupported',
|
|
1331
1364
|
)
|
|
1332
1365
|
}
|
|
1366
|
+
if (harnesses.has('openclaw')) {
|
|
1367
|
+
advice.push(
|
|
1368
|
+
'OpenClaw: restart the Gateway, then send one prompt; plugins load at startup, but non-blocking question continuation is intentionally unsupported',
|
|
1369
|
+
)
|
|
1370
|
+
}
|
|
1333
1371
|
return `${advice.join('. ')}.`
|
|
1334
1372
|
}
|
|
1335
1373
|
|
|
@@ -1683,7 +1721,7 @@ function resolveHookAdapterTarget(deps: CommandDeps, flags: HooksInstallFlags):
|
|
|
1683
1721
|
return { execPath, scriptPath }
|
|
1684
1722
|
}
|
|
1685
1723
|
|
|
1686
|
-
function printHooksInstallClose(deps: CommandDeps, harness:
|
|
1724
|
+
function printHooksInstallClose(deps: CommandDeps, harness: HookInstallableHarness, file: string): void {
|
|
1687
1725
|
const label = HARNESS_LABELS[harness]
|
|
1688
1726
|
const activation =
|
|
1689
1727
|
harness === 'codex'
|
|
@@ -1692,6 +1730,8 @@ function printHooksInstallClose(deps: CommandDeps, harness: Harness, file: strin
|
|
|
1692
1730
|
? 'Start one fresh Cursor conversation, send one prompt, finish its first turn, then run `notifai doctor`.'
|
|
1693
1731
|
: harness === 'opencode'
|
|
1694
1732
|
? 'Restart OpenCode, start one fresh session, send one prompt, then run `notifai doctor`.'
|
|
1733
|
+
: harness === 'openclaw'
|
|
1734
|
+
? 'Restart the OpenClaw Gateway, start one fresh Agent Session, send one prompt, then run `notifai doctor`.'
|
|
1695
1735
|
: `Start one fresh ${label} session, send one prompt, then run \`notifai doctor\`.`
|
|
1696
1736
|
if (deps.io.interactive === true && deps.io.note) {
|
|
1697
1737
|
void deps.io.note(`${file}\n${activation}`, `${label} hooks installed`)
|
|
@@ -1705,7 +1745,7 @@ export function hooksInstallCommand(deps: CommandDeps, flags: HooksInstallFlags)
|
|
|
1705
1745
|
if (flags.harness === undefined) {
|
|
1706
1746
|
const detected = detectedHarnesses(deps.cwd, deps.env)
|
|
1707
1747
|
if (detected.length === 0) {
|
|
1708
|
-
deps.io.err(`Could not tell which harness you mean — pass --harness <${
|
|
1748
|
+
deps.io.err(`Could not tell which harness you mean — pass --harness <${HOOK_INSTALLABLE_HARNESSES.join('|')}>.`)
|
|
1709
1749
|
return EXIT.usage
|
|
1710
1750
|
}
|
|
1711
1751
|
let ok = true
|
|
@@ -1784,6 +1824,16 @@ export function hooksInstallCommand(deps: CommandDeps, flags: HooksInstallFlags)
|
|
|
1784
1824
|
...(flags.narrate === undefined ? {} : { narrate: flags.narrate }),
|
|
1785
1825
|
})
|
|
1786
1826
|
}
|
|
1827
|
+
if (harness === 'openclaw') {
|
|
1828
|
+
return installOpenclawPlugin(deps, settingsTarget, {
|
|
1829
|
+
adapterPath,
|
|
1830
|
+
timeoutSeconds: NON_ROUTING_BLOCKING_STOP_TIMEOUT_SECONDS,
|
|
1831
|
+
platform: hookPlatform,
|
|
1832
|
+
nodePath,
|
|
1833
|
+
global: wantGlobal,
|
|
1834
|
+
...(flags.narrate === undefined ? {} : { narrate: flags.narrate }),
|
|
1835
|
+
})
|
|
1836
|
+
}
|
|
1787
1837
|
|
|
1788
1838
|
if (harness === 'cursor') {
|
|
1789
1839
|
try {
|
|
@@ -1930,6 +1980,113 @@ function installOpencodePlugin(
|
|
|
1930
1980
|
return EXIT.ok
|
|
1931
1981
|
}
|
|
1932
1982
|
|
|
1983
|
+
function installOpenclawPlugin(
|
|
1984
|
+
deps: CommandDeps,
|
|
1985
|
+
file: string,
|
|
1986
|
+
options: {
|
|
1987
|
+
adapterPath: string
|
|
1988
|
+
timeoutSeconds: number
|
|
1989
|
+
platform?: NodeJS.Platform
|
|
1990
|
+
nodePath?: string
|
|
1991
|
+
global: boolean
|
|
1992
|
+
narrate?: boolean
|
|
1993
|
+
},
|
|
1994
|
+
): number {
|
|
1995
|
+
const pluginDir = path.dirname(file)
|
|
1996
|
+
try {
|
|
1997
|
+
withTargetFileLock(file, () => {
|
|
1998
|
+
if (existsSync(file)) {
|
|
1999
|
+
assertOwnedRegularFile(file)
|
|
2000
|
+
const existing = readFileSync(file, 'utf8')
|
|
2001
|
+
if (!isOurOpenclawPlugin(existing)) {
|
|
2002
|
+
throw new Error(`${file} exists and was not written by Notifai; move it aside first.`)
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
atomicWriteFileSync(file, openclawPluginSource(options), {
|
|
2006
|
+
mode: 0o600,
|
|
2007
|
+
preserveMode: false,
|
|
2008
|
+
requireCurrentUserOwner: true,
|
|
2009
|
+
})
|
|
2010
|
+
atomicWriteFileSync(path.join(pluginDir, OPENCLAW_PLUGIN_MANIFEST), openclawPluginManifest(), {
|
|
2011
|
+
mode: 0o600,
|
|
2012
|
+
preserveMode: false,
|
|
2013
|
+
requireCurrentUserOwner: true,
|
|
2014
|
+
})
|
|
2015
|
+
atomicWriteFileSync(path.join(pluginDir, OPENCLAW_PLUGIN_PACKAGE), openclawPluginPackage(), {
|
|
2016
|
+
mode: 0o600,
|
|
2017
|
+
preserveMode: false,
|
|
2018
|
+
requireCurrentUserOwner: true,
|
|
2019
|
+
})
|
|
2020
|
+
})
|
|
2021
|
+
writeOpenclawEnablement(deps, pluginDir, options.global)
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
deps.io.err(String(err))
|
|
2024
|
+
return EXIT.failed
|
|
2025
|
+
}
|
|
2026
|
+
if (options.narrate !== false) printHooksInstallClose(deps, 'openclaw', file)
|
|
2027
|
+
return EXIT.ok
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
function writeOpenclawEnablement(deps: CommandDeps, pluginDir: string, global: boolean): void {
|
|
2031
|
+
const configFile = openclawConfigPath(deps.env, deps.hookPlatform)
|
|
2032
|
+
withTargetFileLock(configFile, () => {
|
|
2033
|
+
let config: Record<string, unknown> = {}
|
|
2034
|
+
if (existsSync(configFile)) {
|
|
2035
|
+
assertOwnedRegularFile(configFile)
|
|
2036
|
+
config = parseOpenclawConfig(readFileSync(configFile, 'utf8'))
|
|
2037
|
+
}
|
|
2038
|
+
const merged = mergeOpenclawNotifaiEntry(config, pluginDir, global)
|
|
2039
|
+
atomicWriteFileSync(configFile, `${JSON.stringify(merged, null, 2)}\n`, {
|
|
2040
|
+
mode: 0o600,
|
|
2041
|
+
preserveMode: true,
|
|
2042
|
+
requireCurrentUserOwner: true,
|
|
2043
|
+
})
|
|
2044
|
+
})
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function uninstallOpenclawPlugin(deps: CommandDeps, file: string): number {
|
|
2048
|
+
const pluginDir = path.dirname(file)
|
|
2049
|
+
try {
|
|
2050
|
+
const removed = withTargetFileLock(file, () => {
|
|
2051
|
+
if (!existsSync(file)) return false
|
|
2052
|
+
assertOwnedRegularFile(file)
|
|
2053
|
+
if (!isOurOpenclawPlugin(readFileSync(file, 'utf8'))) {
|
|
2054
|
+
deps.io.out(`Left ${file} alone: Notifai did not write it.`)
|
|
2055
|
+
return false
|
|
2056
|
+
}
|
|
2057
|
+
rmSync(file, { force: true })
|
|
2058
|
+
rmSync(path.join(pluginDir, OPENCLAW_PLUGIN_MANIFEST), { force: true })
|
|
2059
|
+
rmSync(path.join(pluginDir, OPENCLAW_PLUGIN_PACKAGE), { force: true })
|
|
2060
|
+
if (existsSync(pluginDir) && readdirSync(pluginDir).length === 0) {
|
|
2061
|
+
rmdirSync(pluginDir)
|
|
2062
|
+
}
|
|
2063
|
+
return true
|
|
2064
|
+
})
|
|
2065
|
+
if (removed) {
|
|
2066
|
+
const configFile = openclawConfigPath(deps.env, deps.hookPlatform)
|
|
2067
|
+
if (existsSync(configFile)) {
|
|
2068
|
+
withTargetFileLock(configFile, () => {
|
|
2069
|
+
assertOwnedRegularFile(configFile)
|
|
2070
|
+
const config = parseOpenclawConfig(readFileSync(configFile, 'utf8'))
|
|
2071
|
+
const next = removeOpenclawNotifaiEntry(config, pluginDir)
|
|
2072
|
+
atomicWriteFileSync(configFile, `${JSON.stringify(next, null, 2)}\n`, {
|
|
2073
|
+
mode: 0o600,
|
|
2074
|
+
preserveMode: true,
|
|
2075
|
+
requireCurrentUserOwner: true,
|
|
2076
|
+
})
|
|
2077
|
+
})
|
|
2078
|
+
}
|
|
2079
|
+
deps.io.out(`Removed the Notifai OpenClaw plugin at ${file}`)
|
|
2080
|
+
} else if (!existsSync(file)) {
|
|
2081
|
+
deps.io.out(`Nothing to remove: ${file} does not exist.`)
|
|
2082
|
+
}
|
|
2083
|
+
return EXIT.ok
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
deps.io.err(String(err))
|
|
2086
|
+
return EXIT.failed
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
1933
2090
|
export function hooksUninstallCommand(deps: CommandDeps, flags: HooksInstallFlags): number {
|
|
1934
2091
|
const harness = resolveHarness(deps, flags.harness)
|
|
1935
2092
|
if (!harness) return EXIT.usage
|
|
@@ -1963,6 +2120,9 @@ export function hooksUninstallCommand(deps: CommandDeps, flags: HooksInstallFlag
|
|
|
1963
2120
|
return EXIT.failed
|
|
1964
2121
|
}
|
|
1965
2122
|
}
|
|
2123
|
+
if (harness === 'openclaw') {
|
|
2124
|
+
return uninstallOpenclawPlugin(deps, file)
|
|
2125
|
+
}
|
|
1966
2126
|
if (harness === 'cursor') {
|
|
1967
2127
|
let stripped: ReturnType<typeof removeCursorHooks> | null
|
|
1968
2128
|
try {
|
|
@@ -2049,17 +2209,17 @@ function assertOwnedRegularFile(file: string): void {
|
|
|
2049
2209
|
}
|
|
2050
2210
|
}
|
|
2051
2211
|
|
|
2052
|
-
function resolveHarness(deps: CommandDeps, requested: string | undefined):
|
|
2212
|
+
function resolveHarness(deps: CommandDeps, requested: string | undefined): HookInstallableHarness | null {
|
|
2053
2213
|
if (requested !== undefined) {
|
|
2054
|
-
if ((
|
|
2214
|
+
if ((HOOK_INSTALLABLE_HARNESSES as readonly string[]).includes(requested)) return requested as HookInstallableHarness
|
|
2055
2215
|
deps.io.err(
|
|
2056
|
-
`Unknown harness "${requested}". Supported: ${
|
|
2216
|
+
`Unknown harness "${requested}". Supported: ${HOOK_INSTALLABLE_HARNESSES.join(', ')}.`,
|
|
2057
2217
|
)
|
|
2058
2218
|
return null
|
|
2059
2219
|
}
|
|
2060
2220
|
const detected = detectHarness(deps.cwd, deps.env)
|
|
2061
2221
|
if (!detected) {
|
|
2062
|
-
deps.io.err(`Could not tell which harness to install for — pass --harness <${
|
|
2222
|
+
deps.io.err(`Could not tell which harness to install for — pass --harness <${HOOK_INSTALLABLE_HARNESSES.join('|')}>.`)
|
|
2063
2223
|
return null
|
|
2064
2224
|
}
|
|
2065
2225
|
return detected
|
|
@@ -2073,7 +2233,7 @@ function resolveHarness(deps: CommandDeps, requested: string | undefined): Harne
|
|
|
2073
2233
|
export async function pickHarnessesToInstall(
|
|
2074
2234
|
deps: CommandDeps,
|
|
2075
2235
|
requested?: string,
|
|
2076
|
-
): Promise<
|
|
2236
|
+
): Promise<HookInstallableHarness[] | null> {
|
|
2077
2237
|
if (requested !== undefined) {
|
|
2078
2238
|
const harness = resolveHarness(deps, requested)
|
|
2079
2239
|
return harness === null ? null : [harness]
|
|
@@ -2083,7 +2243,7 @@ export async function pickHarnessesToInstall(
|
|
|
2083
2243
|
if (deps.io.interactive === true && deps.io.multiselect) {
|
|
2084
2244
|
const picked = await deps.io.multiselect(
|
|
2085
2245
|
'Which agent harnesses should Notifai wire here?',
|
|
2086
|
-
|
|
2246
|
+
HOOK_INSTALLABLE_HARNESSES.map((name) => ({
|
|
2087
2247
|
value: name,
|
|
2088
2248
|
label: HARNESS_LABELS[name],
|
|
2089
2249
|
...(detected.includes(name) ? { hint: 'detected on this machine' } : {}),
|
|
@@ -2091,16 +2251,16 @@ export async function pickHarnessesToInstall(
|
|
|
2091
2251
|
detected,
|
|
2092
2252
|
)
|
|
2093
2253
|
if (picked === null) return null
|
|
2094
|
-
const unknown = picked.filter((name) => !(
|
|
2254
|
+
const unknown = picked.filter((name) => !(HOOK_INSTALLABLE_HARNESSES as readonly string[]).includes(name))
|
|
2095
2255
|
if (unknown.length > 0) {
|
|
2096
|
-
deps.io.err(`Unknown harness "${unknown[0]}". Supported: ${
|
|
2256
|
+
deps.io.err(`Unknown harness "${unknown[0]}". Supported: ${HOOK_INSTALLABLE_HARNESSES.join(', ')}.`)
|
|
2097
2257
|
return null
|
|
2098
2258
|
}
|
|
2099
|
-
return picked as
|
|
2259
|
+
return picked as HookInstallableHarness[]
|
|
2100
2260
|
}
|
|
2101
2261
|
if (detected.length === 0) {
|
|
2102
2262
|
deps.io.err(
|
|
2103
|
-
`Could not tell which harness to wire. Run: notifai hooks install --harness <${
|
|
2263
|
+
`Could not tell which harness to wire. Run: notifai hooks install --harness <${HOOK_INSTALLABLE_HARNESSES.join('|')}>`,
|
|
2104
2264
|
)
|
|
2105
2265
|
return null
|
|
2106
2266
|
}
|
package/src/commands-init.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { buildDraft, formatReceipt } from './send.js'
|
|
|
25
25
|
import { loginCommand } from './commands-auth.js'
|
|
26
26
|
import {
|
|
27
27
|
EXIT,
|
|
28
|
+
SETUP_COMMAND,
|
|
28
29
|
UPDATE_CLI_COMMAND,
|
|
29
30
|
authedClient,
|
|
30
31
|
loadLoggedConfig,
|
|
@@ -848,8 +849,8 @@ async function printInitClose(
|
|
|
848
849
|
await deps.io.note?.(`${blocker.title} — ${blocker.detail}\n${remedyLine(blocker)}`, 'Next')
|
|
849
850
|
deps.io.out(`Next: ${blocker.title} — ${blocker.detail}`)
|
|
850
851
|
deps.io.out(` ${remedyLine(blocker)}`)
|
|
851
|
-
if (blocker
|
|
852
|
-
deps.io.out(
|
|
852
|
+
if (shouldRestateInit(blocker)) {
|
|
853
|
+
deps.io.out(` Then re-run \`${SETUP_COMMAND}\` and it will pick up from here.`)
|
|
853
854
|
}
|
|
854
855
|
await deps.io.outro?.('One step remains (above)')
|
|
855
856
|
return
|
|
@@ -866,7 +867,12 @@ async function printInitClose(
|
|
|
866
867
|
}
|
|
867
868
|
deps.io.out(`Next: ${blocker.title} — ${blocker.detail}`)
|
|
868
869
|
deps.io.out(` ${remedyLine(blocker)}`)
|
|
869
|
-
if (blocker
|
|
870
|
-
deps.io.out(
|
|
870
|
+
if (shouldRestateInit(blocker)) {
|
|
871
|
+
deps.io.out(` Then re-run \`${SETUP_COMMAND}\` and it will pick up from here.`)
|
|
871
872
|
}
|
|
872
873
|
}
|
|
874
|
+
|
|
875
|
+
function shouldRestateInit(blocker: ReadinessState): boolean {
|
|
876
|
+
if (blocker.remedy?.by !== 'user-elsewhere' && blocker.remedy?.by !== 'user-here') return false
|
|
877
|
+
return !('command' in blocker.remedy) || blocker.remedy.command !== SETUP_COMMAND
|
|
878
|
+
}
|
|
@@ -5,16 +5,38 @@ import path from 'node:path'
|
|
|
5
5
|
import { isRetryableReplyPollError, type ApiClient } from './client.js'
|
|
6
6
|
import { fetchMediaUrl } from './url-policy.js'
|
|
7
7
|
import { buildSourceContext, inferInvocationContext } from './invocation-context.js'
|
|
8
|
+
import { readOrcaSessionTitle } from './orca-session-title.js'
|
|
8
9
|
import { type DraftInvocation, type SendFlags } from './send.js'
|
|
9
10
|
import { EXIT, reportError, type CommandDeps } from './commands-core.js'
|
|
10
11
|
import type { ActiveHarnessSession } from './commands-harness-context.js'
|
|
11
12
|
|
|
13
|
+
function managedSessionTitle(
|
|
14
|
+
deps: CommandDeps,
|
|
15
|
+
flags: Pick<SendFlags, 'sessionLabel'>,
|
|
16
|
+
active: ActiveHarnessSession | null,
|
|
17
|
+
): string | undefined {
|
|
18
|
+
if (active?.sessionLabel !== undefined) return active.sessionLabel
|
|
19
|
+
if (
|
|
20
|
+
active?.sessionId === undefined ||
|
|
21
|
+
flags.sessionLabel !== undefined ||
|
|
22
|
+
deps.env['NOTIFAI_SESSION_LABEL'] !== undefined
|
|
23
|
+
) {
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return (deps.orcaSessionTitle ?? readOrcaSessionTitle)(deps.env)
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
export function resolveDraftInvocation(
|
|
13
34
|
deps: CommandDeps,
|
|
14
35
|
flags: Pick<SendFlags, 'sessionId' | 'sessionLabel'>,
|
|
15
36
|
active: ActiveHarnessSession | null,
|
|
16
37
|
): { ok: true; invocation: DraftInvocation } | { ok: false; error: string } {
|
|
17
38
|
const inferred = inferInvocationContext(deps.cwd)
|
|
39
|
+
const sessionTitle = managedSessionTitle(deps, flags, active)
|
|
18
40
|
const source = buildSourceContext({
|
|
19
41
|
env: deps.env,
|
|
20
42
|
invocation: inferred,
|
|
@@ -26,7 +48,7 @@ export function resolveDraftInvocation(
|
|
|
26
48
|
activeHarness: {
|
|
27
49
|
harness: active.harness,
|
|
28
50
|
...(active.sessionId === undefined ? {} : { sessionId: active.sessionId }),
|
|
29
|
-
...(
|
|
51
|
+
...(sessionTitle === undefined ? {} : { sessionLabel: sessionTitle }),
|
|
30
52
|
...(active.sessionLabelPending === true ? { sessionLabelPending: true } : {}),
|
|
31
53
|
},
|
|
32
54
|
}),
|
package/src/commands-send.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
reportError,
|
|
28
28
|
type CommandDeps,
|
|
29
29
|
} from './commands-core.js'
|
|
30
|
-
import {
|
|
30
|
+
import { sourceContextHarnessSession } from './commands-harness-context.js'
|
|
31
31
|
import { resolveCommandSession } from './command-session.js'
|
|
32
32
|
import {
|
|
33
33
|
beginSendAttempt,
|
|
@@ -143,7 +143,7 @@ export async function sendCommand(
|
|
|
143
143
|
const source = resolveDraftInvocation(
|
|
144
144
|
deps,
|
|
145
145
|
flags,
|
|
146
|
-
|
|
146
|
+
sourceContextHarnessSession(deps.env, deps.cwd, (deps.now ?? Date.now)()),
|
|
147
147
|
)
|
|
148
148
|
if (!source.ok) {
|
|
149
149
|
deps.io.err(source.error)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import type { ApiClient } from './client.js'
|
|
5
|
+
import { EXIT, type CommandDeps, type CommandIo } from './commands-core.js'
|
|
6
|
+
import { soundsCommand } from './commands-sounds.js'
|
|
7
|
+
|
|
8
|
+
class CapturedIo implements CommandIo {
|
|
9
|
+
readonly outLines: string[] = []
|
|
10
|
+
readonly errLines: string[] = []
|
|
11
|
+
out(line: string): void {
|
|
12
|
+
this.outLines.push(line)
|
|
13
|
+
}
|
|
14
|
+
err(line: string): void {
|
|
15
|
+
this.errLines.push(line)
|
|
16
|
+
}
|
|
17
|
+
async confirm(): Promise<boolean> {
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
openUrl(): void {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeDeps(io: CapturedIo, client: ApiClient): CommandDeps {
|
|
24
|
+
const testRoot = path.join(os.tmpdir(), 'notifai-cli-sounds-tests')
|
|
25
|
+
return {
|
|
26
|
+
io,
|
|
27
|
+
store: {
|
|
28
|
+
load: () => ({
|
|
29
|
+
machineId: 'mac_test',
|
|
30
|
+
secret: 'test-secret',
|
|
31
|
+
baseUrl: 'https://test.notifai.invalid',
|
|
32
|
+
machineName: 'test-machine',
|
|
33
|
+
}),
|
|
34
|
+
save: () => {},
|
|
35
|
+
clear: () => {},
|
|
36
|
+
describe: () => 'test credential store',
|
|
37
|
+
},
|
|
38
|
+
env: {
|
|
39
|
+
XDG_CONFIG_HOME: testRoot,
|
|
40
|
+
XDG_STATE_HOME: path.join(os.tmpdir(), 'notifai-cli-sounds-tests-state'),
|
|
41
|
+
},
|
|
42
|
+
cwd: os.tmpdir(),
|
|
43
|
+
clientFactory: () => client,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const kitchenTimer = {
|
|
48
|
+
sound_id: 'snd_kitchen',
|
|
49
|
+
name: 'Kitchen timer',
|
|
50
|
+
duration_ms: 2400,
|
|
51
|
+
content_hash: 'a'.repeat(64),
|
|
52
|
+
url: 'https://wav.example/kitchen.wav',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('soundsCommand', () => {
|
|
56
|
+
it('lists shipped names and Account custom sounds', async () => {
|
|
57
|
+
const io = new CapturedIo()
|
|
58
|
+
const client = {
|
|
59
|
+
listSounds: async () => ({ sounds: [kitchenTimer] }),
|
|
60
|
+
} as unknown as ApiClient
|
|
61
|
+
|
|
62
|
+
expect(await soundsCommand(makeDeps(io, client), {})).toBe(EXIT.ok)
|
|
63
|
+
expect(io.outLines[0]).toBe('default Device default')
|
|
64
|
+
expect(io.outLines).toContain('done completion chime')
|
|
65
|
+
expect(io.outLines).toContain('none silent')
|
|
66
|
+
expect(io.outLines.at(-1)).toBe('snd_kitchen Kitchen timer 2.4s')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('prints the catalog as JSON when asked', async () => {
|
|
70
|
+
const io = new CapturedIo()
|
|
71
|
+
const client = {
|
|
72
|
+
listSounds: async () => ({ sounds: [kitchenTimer] }),
|
|
73
|
+
} as unknown as ApiClient
|
|
74
|
+
|
|
75
|
+
expect(await soundsCommand(makeDeps(io, client), { json: true })).toBe(EXIT.ok)
|
|
76
|
+
const parsed = JSON.parse(io.outLines.join('\n')) as {
|
|
77
|
+
shipped: { ref: string; name: string }[]
|
|
78
|
+
custom: typeof kitchenTimer[]
|
|
79
|
+
}
|
|
80
|
+
expect(parsed.shipped[0]).toEqual({ ref: 'default', name: 'Device default' })
|
|
81
|
+
expect(parsed.custom).toEqual([kitchenTimer])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('refuses to list custom sounds when this machine is not signed in', async () => {
|
|
85
|
+
const io = new CapturedIo()
|
|
86
|
+
let calls = 0
|
|
87
|
+
const deps: CommandDeps = {
|
|
88
|
+
...makeDeps(io, {
|
|
89
|
+
listSounds: async () => {
|
|
90
|
+
calls += 1
|
|
91
|
+
return { sounds: [] }
|
|
92
|
+
},
|
|
93
|
+
} as unknown as ApiClient),
|
|
94
|
+
store: {
|
|
95
|
+
load: () => null,
|
|
96
|
+
save: () => {},
|
|
97
|
+
clear: () => {},
|
|
98
|
+
describe: () => 'empty store',
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
expect(await soundsCommand(deps, {})).toBe(EXIT.auth)
|
|
103
|
+
expect(calls).toBe(0)
|
|
104
|
+
expect(io.errLines.join('\n')).toMatch(/not signed in/i)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ListSoundsResponse, SoundView } from '@raidiant/notifai-protocol'
|
|
2
|
+
import {
|
|
3
|
+
EXIT,
|
|
4
|
+
authedClient,
|
|
5
|
+
loadLoggedConfig,
|
|
6
|
+
reportError,
|
|
7
|
+
type CommandDeps,
|
|
8
|
+
} from './commands-core.js'
|
|
9
|
+
import { SHIPPED_SOUND_LISTING } from './sound-ref.js'
|
|
10
|
+
|
|
11
|
+
export interface SoundsListing {
|
|
12
|
+
shipped: readonly { ref: string; name: string }[]
|
|
13
|
+
custom: SoundView[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatSoundDuration(ms: number): string {
|
|
17
|
+
const seconds = ms / 1000
|
|
18
|
+
const text = Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(1)
|
|
19
|
+
return `${text}s`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function soundsCommand(
|
|
23
|
+
deps: CommandDeps,
|
|
24
|
+
flags: { json?: boolean },
|
|
25
|
+
): Promise<number> {
|
|
26
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
27
|
+
const authed = authedClient(deps, config)
|
|
28
|
+
if (!authed) return EXIT.auth
|
|
29
|
+
try {
|
|
30
|
+
const result: ListSoundsResponse = await authed.client.listSounds()
|
|
31
|
+
const listing: SoundsListing = {
|
|
32
|
+
shipped: SHIPPED_SOUND_LISTING,
|
|
33
|
+
custom: result.sounds,
|
|
34
|
+
}
|
|
35
|
+
if (flags.json) {
|
|
36
|
+
deps.io.out(JSON.stringify(listing, null, 2))
|
|
37
|
+
return EXIT.ok
|
|
38
|
+
}
|
|
39
|
+
for (const sound of listing.shipped) {
|
|
40
|
+
deps.io.out(`${sound.ref} ${sound.name}`)
|
|
41
|
+
}
|
|
42
|
+
for (const sound of listing.custom) {
|
|
43
|
+
deps.io.out(`${sound.sound_id} ${sound.name} ${formatSoundDuration(sound.duration_ms)}`)
|
|
44
|
+
}
|
|
45
|
+
return EXIT.ok
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return reportError(deps, err)
|
|
48
|
+
}
|
|
49
|
+
}
|