@raidiant/notifai 1.0.1 → 2.0.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 +50 -0
- package/README.md +17 -7
- package/dist/client.d.ts +5 -1
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -1
- package/dist/commands.d.ts +10 -1
- package/dist/commands.js +151 -54
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +5 -9
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +54 -3
- package/dist/credentials.js +237 -18
- package/dist/credentials.js.map +1 -1
- package/dist/file-lock.js +15 -6
- package/dist/file-lock.js.map +1 -1
- package/dist/hook-adapter.d.ts +6 -2
- package/dist/hook-adapter.js +110 -29
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +15 -1
- package/dist/hooks.js +183 -12
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +50 -21
- package/dist/install-hooks.js +141 -57
- package/dist/install-hooks.js.map +1 -1
- package/dist/logging.d.ts +1 -1
- package/dist/logging.js +4 -0
- package/dist/logging.js.map +1 -1
- package/dist/main.js +14 -3
- package/dist/main.js.map +1 -1
- package/dist/native-skills.js +5 -10
- package/dist/native-skills.js.map +1 -1
- package/dist/opencode-plugin.d.ts +7 -2
- package/dist/opencode-plugin.js +142 -4
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/platform.d.ts +73 -0
- package/dist/platform.js +143 -0
- package/dist/platform.js.map +1 -0
- package/dist/ui/config-view.js +5 -1
- package/dist/ui/config-view.js.map +1 -1
- package/package.json +11 -10
- package/src/client.test.ts +53 -0
- package/src/client.ts +24 -0
- package/src/commands.test.ts +351 -30
- package/src/commands.ts +207 -49
- package/src/config.ts +11 -9
- package/src/credentials.test.ts +291 -0
- package/src/credentials.ts +274 -17
- package/src/file-lock.ts +34 -9
- package/src/hook-adapter.test.ts +136 -0
- package/src/hook-adapter.ts +120 -28
- package/src/hooks.test.ts +175 -1
- package/src/hooks.ts +241 -12
- package/src/install-hooks.test.ts +174 -33
- package/src/install-hooks.ts +186 -54
- package/src/logging.ts +4 -0
- package/src/main.ts +17 -2
- package/src/native-skills.ts +5 -10
- package/src/opencode-plugin.ts +148 -3
- package/src/platform.test.ts +152 -0
- package/src/platform.ts +173 -0
- package/src/skill.test.ts +32 -1
- package/src/ui/config-view.ts +5 -5
package/src/commands.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto'
|
|
2
|
-
import { spawn } from 'node:child_process'
|
|
3
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
4
3
|
import os from 'node:os'
|
|
5
4
|
import path from 'node:path'
|
|
6
5
|
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml'
|
|
7
6
|
import {
|
|
7
|
+
AGENT_ACKNOWLEDGEMENT_MAX_LENGTH,
|
|
8
8
|
CAPABILITIES_V1,
|
|
9
9
|
QUESTION_TEXT_MAX_LENGTH,
|
|
10
10
|
REPLY_MAX_QUESTIONS,
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
globalConfigPath,
|
|
41
41
|
loadConfig,
|
|
42
42
|
sessionConfigPath,
|
|
43
|
+
stateDir,
|
|
43
44
|
type CliConfig,
|
|
44
45
|
type ConfigKey,
|
|
45
46
|
type FlagOverrides,
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
import type { Tone } from './ui/theme.js'
|
|
66
67
|
import {
|
|
67
68
|
WAITER_CEILING_SECONDS,
|
|
69
|
+
clearAcknowledgementObligation,
|
|
68
70
|
handleSessionEnd,
|
|
69
71
|
handleStop,
|
|
70
72
|
handleUserPromptSubmit,
|
|
@@ -154,6 +156,7 @@ import {
|
|
|
154
156
|
type SendFlags,
|
|
155
157
|
} from './send.js'
|
|
156
158
|
import type { NativeSkill, NativeSkills, SkillScope } from './native-skills.js'
|
|
159
|
+
import { openUrl } from './platform.js'
|
|
157
160
|
|
|
158
161
|
export interface CommandIo {
|
|
159
162
|
out(line: string): void
|
|
@@ -204,6 +207,8 @@ export interface CommandDeps {
|
|
|
204
207
|
hookAdapterHome?: string
|
|
205
208
|
/** Test seam; production uses this process's Node and CLI paths. */
|
|
206
209
|
hookInstallTarget?: HookAdapterTarget
|
|
210
|
+
/** Test seam; production uses process.platform for adapter format and quoting. */
|
|
211
|
+
hookPlatform?: NodeJS.Platform
|
|
207
212
|
/** Test seam; production uses fetch against base_url. */
|
|
208
213
|
clientFactory?: (baseUrl: string, bearer: string | null, options?: ClientOptions) => ApiClient
|
|
209
214
|
/** Test seam for bounded polling without wall-clock sleeps. */
|
|
@@ -682,6 +687,7 @@ export async function sendCommand(
|
|
|
682
687
|
title: flags.title,
|
|
683
688
|
overall: receipt.overall,
|
|
684
689
|
replayed: receipt.replayed,
|
|
690
|
+
agent_acknowledgement_required: receipt.agent_acknowledgement_required,
|
|
685
691
|
deliveries: receipt.deliveries.length,
|
|
686
692
|
wait_seconds: waitSeconds,
|
|
687
693
|
exit: receiptExit,
|
|
@@ -712,6 +718,9 @@ export async function sendCommand(
|
|
|
712
718
|
type: 'reply_result',
|
|
713
719
|
request_id: receipt.request_id,
|
|
714
720
|
replies: [],
|
|
721
|
+
agent_acknowledgement_required: receipt.agent_acknowledgement_required,
|
|
722
|
+
agent_acknowledgement: null,
|
|
723
|
+
acknowledgement_command: null,
|
|
715
724
|
degraded: false,
|
|
716
725
|
})
|
|
717
726
|
: JSON.stringify(receipt, null, 2),
|
|
@@ -730,15 +739,15 @@ export async function sendCommand(
|
|
|
730
739
|
recordReplies(deps, receipt.request_id, result.response.replies)
|
|
731
740
|
if (flags.json) {
|
|
732
741
|
deps.io.out(
|
|
733
|
-
JSON.stringify(
|
|
734
|
-
type: 'reply_result',
|
|
735
|
-
request_id: receipt.request_id,
|
|
736
|
-
replies: result.response.replies,
|
|
737
|
-
degraded: result.degraded,
|
|
738
|
-
}),
|
|
742
|
+
JSON.stringify(replyResultJson(result.response, result.degraded)),
|
|
739
743
|
)
|
|
740
|
-
} else if (result.response.replies.length > 0)
|
|
741
|
-
|
|
744
|
+
} else if (result.response.replies.length > 0) {
|
|
745
|
+
printReplies(deps, result.response.replies)
|
|
746
|
+
printAcknowledgementStatus(deps, result.response)
|
|
747
|
+
} else {
|
|
748
|
+
printNoReply(deps, receipt.request_id, result.response.reply_expires_at)
|
|
749
|
+
printAcknowledgementStatus(deps, result.response)
|
|
750
|
+
}
|
|
742
751
|
if (result.degraded) {
|
|
743
752
|
log(deps).error('cli.error', {
|
|
744
753
|
kind: 'network',
|
|
@@ -890,12 +899,14 @@ export async function repliesCommand(
|
|
|
890
899
|
})
|
|
891
900
|
recordReplies(deps, requestId, result.response.replies)
|
|
892
901
|
if (flags.json) {
|
|
893
|
-
jsonBodies.push(
|
|
902
|
+
jsonBodies.push(replyResultJson(result.response, result.degraded))
|
|
894
903
|
} else if (result.response.replies.length > 0) {
|
|
895
904
|
if (flags.pending === true) deps.io.out(`pending request ${requestId}`)
|
|
896
905
|
printReplies(deps, result.response.replies)
|
|
906
|
+
printAcknowledgementStatus(deps, result.response)
|
|
897
907
|
} else {
|
|
898
908
|
printNoReply(deps, requestId, result.response.reply_expires_at)
|
|
909
|
+
printAcknowledgementStatus(deps, result.response)
|
|
899
910
|
}
|
|
900
911
|
anyReplies ||= result.response.replies.length > 0
|
|
901
912
|
if (result.degraded) degradedRequestIds.push(requestId)
|
|
@@ -993,7 +1004,13 @@ export async function waitForReply(
|
|
|
993
1004
|
return {
|
|
994
1005
|
response:
|
|
995
1006
|
lastResponse ??
|
|
996
|
-
({
|
|
1007
|
+
({
|
|
1008
|
+
request_id: requestId,
|
|
1009
|
+
reply_expires_at: null,
|
|
1010
|
+
agent_acknowledgement_required: false,
|
|
1011
|
+
agent_acknowledgement: null,
|
|
1012
|
+
replies: [],
|
|
1013
|
+
} satisfies ListRepliesResponse),
|
|
997
1014
|
timedOut: true,
|
|
998
1015
|
// A poll succeeded at some point, so we do not throw — but the last thing
|
|
999
1016
|
// we know is that we could not reach the server. Reporting that as a plain
|
|
@@ -1033,6 +1050,56 @@ function isNonNegativeInteger(value: number): boolean {
|
|
|
1033
1050
|
return Number.isInteger(value) && value >= 0
|
|
1034
1051
|
}
|
|
1035
1052
|
|
|
1053
|
+
function acknowledgementCommand(
|
|
1054
|
+
requestId: string,
|
|
1055
|
+
required: boolean,
|
|
1056
|
+
acknowledgement: ListRepliesResponse['agent_acknowledgement'],
|
|
1057
|
+
hasReply = true,
|
|
1058
|
+
): string | null {
|
|
1059
|
+
return required && acknowledgement === null && hasReply
|
|
1060
|
+
? `notifai acknowledge ${requestId} --text <text>`
|
|
1061
|
+
: null
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function replyResultJson(response: ListRepliesResponse, degraded: boolean): object {
|
|
1065
|
+
return {
|
|
1066
|
+
type: 'reply_result',
|
|
1067
|
+
request_id: response.request_id,
|
|
1068
|
+
reply_expires_at: response.reply_expires_at,
|
|
1069
|
+
replies: response.replies,
|
|
1070
|
+
agent_acknowledgement_required: response.agent_acknowledgement_required,
|
|
1071
|
+
agent_acknowledgement: response.agent_acknowledgement,
|
|
1072
|
+
acknowledgement_command: acknowledgementCommand(
|
|
1073
|
+
response.request_id,
|
|
1074
|
+
response.agent_acknowledgement_required,
|
|
1075
|
+
response.agent_acknowledgement,
|
|
1076
|
+
response.replies.length > 0,
|
|
1077
|
+
),
|
|
1078
|
+
degraded,
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function printAcknowledgementStatus(deps: CommandDeps, response: ListRepliesResponse): void {
|
|
1083
|
+
if (!response.agent_acknowledgement_required) {
|
|
1084
|
+
deps.io.out('Agent Acknowledgement: not required for this request.')
|
|
1085
|
+
return
|
|
1086
|
+
}
|
|
1087
|
+
if (response.agent_acknowledgement !== null) {
|
|
1088
|
+
deps.io.out(
|
|
1089
|
+
`Agent Acknowledgement: recorded at ${response.agent_acknowledgement.created_at}: ${response.agent_acknowledgement.text}`,
|
|
1090
|
+
)
|
|
1091
|
+
return
|
|
1092
|
+
}
|
|
1093
|
+
if (response.replies.length > 0) {
|
|
1094
|
+
deps.io.out('Agent Acknowledgement required.')
|
|
1095
|
+
deps.io.out(
|
|
1096
|
+
`next: Run \`${acknowledgementCommand(response.request_id, true, null)}\` with concrete text saying what you will do because of the reply.`,
|
|
1097
|
+
)
|
|
1098
|
+
return
|
|
1099
|
+
}
|
|
1100
|
+
deps.io.out('Agent Acknowledgement: required after a user reply; no reply is recorded yet.')
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1036
1103
|
function recordReplies(deps: CommandDeps, requestId: string, replies: readonly ReplyView[]): void {
|
|
1037
1104
|
const logger = log(deps)
|
|
1038
1105
|
for (const reply of replies) {
|
|
@@ -1486,6 +1553,7 @@ function stopWakeRoute(
|
|
|
1486
1553
|
): EscalationDeliveryRoute | undefined {
|
|
1487
1554
|
if (sessionId === undefined) return undefined
|
|
1488
1555
|
if (harness === 'claude-code') {
|
|
1556
|
+
if ((deps.hookPlatform ?? process.platform) === 'win32') return undefined
|
|
1489
1557
|
return claudeWakeRoute({
|
|
1490
1558
|
sessionId,
|
|
1491
1559
|
cwd,
|
|
@@ -1709,7 +1777,7 @@ export function askCommand(deps: CommandDeps, question: string | undefined, flag
|
|
|
1709
1777
|
const { active, contested } = resolveActiveHarness(deps.env, deps.cwd, now)
|
|
1710
1778
|
let sessionId: string | undefined
|
|
1711
1779
|
if (active !== null) {
|
|
1712
|
-
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome)
|
|
1780
|
+
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
|
|
1713
1781
|
const activeInstalled = installations.some(
|
|
1714
1782
|
(installation) => installation.harness === active.harness,
|
|
1715
1783
|
)
|
|
@@ -1991,10 +2059,10 @@ function activeQuestionRouteProblems(
|
|
|
1991
2059
|
}
|
|
1992
2060
|
}
|
|
1993
2061
|
}
|
|
1994
|
-
for (const problem of inspectHookAdapter(deps.hookAdapterHome).problems) {
|
|
2062
|
+
for (const problem of inspectHookAdapter(deps.hookAdapterHome, deps.hookPlatform).problems) {
|
|
1995
2063
|
problems.push(problem)
|
|
1996
2064
|
}
|
|
1997
|
-
for (const installation of matching) problems.push(...stopShapeProblems(installation))
|
|
2065
|
+
for (const installation of matching) problems.push(...stopShapeProblems(installation, deps.hookPlatform))
|
|
1998
2066
|
problems.push(...codexTrustProblems(matching, deps.env))
|
|
1999
2067
|
return [...new Set(problems)]
|
|
2000
2068
|
}
|
|
@@ -2056,7 +2124,7 @@ function diagnoseActiveHarnessSession(
|
|
|
2056
2124
|
* checked after a prompt before assuming that a new session is required.
|
|
2057
2125
|
*/
|
|
2058
2126
|
function diagnoseMissingSession(deps: CommandDeps): string[] {
|
|
2059
|
-
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome)
|
|
2127
|
+
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
|
|
2060
2128
|
if (installations.length === 0) {
|
|
2061
2129
|
return [
|
|
2062
2130
|
'Could not tell which harness session this is: no Notifai hooks are installed for this project.',
|
|
@@ -2119,14 +2187,95 @@ function hookActivationAdvice(installations: Installation[]): string {
|
|
|
2119
2187
|
return `${advice.join('. ')}.`
|
|
2120
2188
|
}
|
|
2121
2189
|
|
|
2190
|
+
/** Record the one Agent Acknowledgement associated with a replied-to request. */
|
|
2191
|
+
export async function acknowledgeCommand(
|
|
2192
|
+
deps: CommandDeps,
|
|
2193
|
+
requestId: string,
|
|
2194
|
+
flags: { text?: string; json?: boolean },
|
|
2195
|
+
): Promise<number> {
|
|
2196
|
+
const text = flags.text?.trim() ?? ''
|
|
2197
|
+
if (text.length === 0) {
|
|
2198
|
+
deps.io.err('--text is required and must contain non-whitespace text.')
|
|
2199
|
+
return EXIT.usage
|
|
2200
|
+
}
|
|
2201
|
+
if (text.length > AGENT_ACKNOWLEDGEMENT_MAX_LENGTH) {
|
|
2202
|
+
deps.io.err(
|
|
2203
|
+
`--text must be at most ${AGENT_ACKNOWLEDGEMENT_MAX_LENGTH} characters after trimming.`,
|
|
2204
|
+
)
|
|
2205
|
+
return EXIT.usage
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
const logger = log(deps)
|
|
2209
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
2210
|
+
logger.info('acknowledgement.attempted', {
|
|
2211
|
+
request_id: requestId,
|
|
2212
|
+
text,
|
|
2213
|
+
characters: text.length,
|
|
2214
|
+
})
|
|
2215
|
+
const authed = authedClient(deps, config)
|
|
2216
|
+
if (!authed) return EXIT.auth
|
|
2217
|
+
try {
|
|
2218
|
+
const result = await authed.client.putAgentAcknowledgement(requestId, { text })
|
|
2219
|
+
const output = {
|
|
2220
|
+
request_id: requestId,
|
|
2221
|
+
outcome: result.status,
|
|
2222
|
+
acknowledgement: result.agent_acknowledgement,
|
|
2223
|
+
agent_acknowledgement_required: true,
|
|
2224
|
+
}
|
|
2225
|
+
logger.info('acknowledgement.outcome', {
|
|
2226
|
+
request_id: requestId,
|
|
2227
|
+
outcome: result.status,
|
|
2228
|
+
text: result.agent_acknowledgement.text,
|
|
2229
|
+
created_at: result.agent_acknowledgement.created_at,
|
|
2230
|
+
agent_acknowledgement_required: true,
|
|
2231
|
+
})
|
|
2232
|
+
const sessionId = readProjectSession(deps.cwd, deps.env, (deps.now ?? Date.now)())
|
|
2233
|
+
if (sessionId !== null) {
|
|
2234
|
+
clearAcknowledgementObligation(sessionId, deps.env, requestId)
|
|
2235
|
+
}
|
|
2236
|
+
if (flags.json) deps.io.out(JSON.stringify(output, null, 2))
|
|
2237
|
+
else {
|
|
2238
|
+
deps.io.out(
|
|
2239
|
+
`Agent Acknowledgement ${result.status} for ${requestId} at ${result.agent_acknowledgement.created_at}.`,
|
|
2240
|
+
)
|
|
2241
|
+
}
|
|
2242
|
+
return EXIT.ok
|
|
2243
|
+
} catch (err) {
|
|
2244
|
+
return reportError(deps, err, { operation: 'agent_acknowledgement', request_id: requestId })
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2122
2248
|
/** Retire a question so a late answer is rejected rather than silently lost. */
|
|
2123
|
-
export async function closeCommand(
|
|
2249
|
+
export async function closeCommand(
|
|
2250
|
+
deps: CommandDeps,
|
|
2251
|
+
requestId: string,
|
|
2252
|
+
flags: { json?: boolean } = {},
|
|
2253
|
+
): Promise<number> {
|
|
2124
2254
|
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
2125
2255
|
const authed = authedClient(deps, config)
|
|
2126
2256
|
if (!authed) return EXIT.auth
|
|
2127
2257
|
try {
|
|
2128
|
-
await authed.client.closeReplies(requestId)
|
|
2129
|
-
|
|
2258
|
+
const response = await authed.client.closeReplies(requestId)
|
|
2259
|
+
if (flags.json) {
|
|
2260
|
+
deps.io.out(
|
|
2261
|
+
JSON.stringify(
|
|
2262
|
+
{
|
|
2263
|
+
...response,
|
|
2264
|
+
acknowledgement_command: acknowledgementCommand(
|
|
2265
|
+
response.request_id,
|
|
2266
|
+
response.agent_acknowledgement_required,
|
|
2267
|
+
response.agent_acknowledgement,
|
|
2268
|
+
response.replies.length > 0,
|
|
2269
|
+
),
|
|
2270
|
+
},
|
|
2271
|
+
null,
|
|
2272
|
+
2,
|
|
2273
|
+
),
|
|
2274
|
+
)
|
|
2275
|
+
} else {
|
|
2276
|
+
deps.io.out(`Closed the reply window for ${requestId}.`)
|
|
2277
|
+
if (response.replies.length > 0) printAcknowledgementStatus(deps, response)
|
|
2278
|
+
}
|
|
2130
2279
|
return EXIT.ok
|
|
2131
2280
|
} catch (err) {
|
|
2132
2281
|
return reportError(deps, err)
|
|
@@ -2207,15 +2356,17 @@ export function hooksInstallCommand(deps: CommandDeps, flags: HooksInstallFlags)
|
|
|
2207
2356
|
const adapterTarget = resolveHookAdapterTarget(deps, flags)
|
|
2208
2357
|
const scriptPath =
|
|
2209
2358
|
flags.scriptPath ?? fileHookInstallTarget(adapterTarget)?.scriptPath ?? process.argv[1] ?? 'notifai'
|
|
2359
|
+
const hookPlatform = deps.hookPlatform ?? process.platform
|
|
2360
|
+
const nodePath = adapterTarget.execPath
|
|
2210
2361
|
let adapterPath: string
|
|
2211
2362
|
try {
|
|
2212
|
-
adapterPath = installHookAdapter(adapterTarget, deps.hookAdapterHome).path
|
|
2363
|
+
adapterPath = installHookAdapter(adapterTarget, deps.hookAdapterHome, hookPlatform).path
|
|
2213
2364
|
} catch (err) {
|
|
2214
2365
|
deps.io.err(`Could not prepare the stable hook adapter: ${String(err)}`)
|
|
2215
2366
|
return EXIT.failed
|
|
2216
2367
|
}
|
|
2217
2368
|
const wantGlobal = flags.global === true
|
|
2218
|
-
const existing = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome).filter(
|
|
2369
|
+
const existing = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform).filter(
|
|
2219
2370
|
(installation) => installation.harness === harness,
|
|
2220
2371
|
)
|
|
2221
2372
|
const otherScope = existing.filter((installation) => installation.global !== wantGlobal)
|
|
@@ -2231,7 +2382,7 @@ export function hooksInstallCommand(deps: CommandDeps, flags: HooksInstallFlags)
|
|
|
2231
2382
|
return EXIT.failed
|
|
2232
2383
|
}
|
|
2233
2384
|
}
|
|
2234
|
-
const file = settingsFile(harness, wantGlobal, deps.cwd, deps.env)
|
|
2385
|
+
const file = settingsFile(harness, wantGlobal, deps.cwd, deps.env, hookPlatform)
|
|
2235
2386
|
|
|
2236
2387
|
// OpenCode's adapter is a generated plugin module rather than a handler
|
|
2237
2388
|
// merged into a settings document, so it owns the whole file.
|
|
@@ -2239,6 +2390,8 @@ export function hooksInstallCommand(deps: CommandDeps, flags: HooksInstallFlags)
|
|
|
2239
2390
|
return installOpencodePlugin(deps, file, {
|
|
2240
2391
|
adapterPath,
|
|
2241
2392
|
timeoutSeconds: BLOCKING_STOP_TIMEOUT_SECONDS,
|
|
2393
|
+
platform: hookPlatform,
|
|
2394
|
+
nodePath,
|
|
2242
2395
|
})
|
|
2243
2396
|
}
|
|
2244
2397
|
|
|
@@ -2251,6 +2404,8 @@ export function hooksInstallCommand(deps: CommandDeps, flags: HooksInstallFlags)
|
|
|
2251
2404
|
buildCursorHookConfig({
|
|
2252
2405
|
adapterPath,
|
|
2253
2406
|
harness: 'cursor',
|
|
2407
|
+
platform: hookPlatform,
|
|
2408
|
+
nodePath,
|
|
2254
2409
|
}),
|
|
2255
2410
|
scriptPath,
|
|
2256
2411
|
)
|
|
@@ -2276,6 +2431,8 @@ export function hooksInstallCommand(deps: CommandDeps, flags: HooksInstallFlags)
|
|
|
2276
2431
|
buildHookConfig({
|
|
2277
2432
|
adapterPath,
|
|
2278
2433
|
harness,
|
|
2434
|
+
platform: hookPlatform,
|
|
2435
|
+
nodePath,
|
|
2279
2436
|
}),
|
|
2280
2437
|
scriptPath,
|
|
2281
2438
|
)
|
|
@@ -2331,7 +2488,12 @@ function foreignStopHandlers(document: { hooks?: Record<string, { hooks?: { comm
|
|
|
2331
2488
|
function installOpencodePlugin(
|
|
2332
2489
|
deps: CommandDeps,
|
|
2333
2490
|
file: string,
|
|
2334
|
-
options: {
|
|
2491
|
+
options: {
|
|
2492
|
+
adapterPath: string
|
|
2493
|
+
timeoutSeconds: number
|
|
2494
|
+
platform?: NodeJS.Platform
|
|
2495
|
+
nodePath?: string
|
|
2496
|
+
},
|
|
2335
2497
|
): number {
|
|
2336
2498
|
try {
|
|
2337
2499
|
withTargetFileLock(file, () => {
|
|
@@ -2360,7 +2522,13 @@ export function hooksUninstallCommand(deps: CommandDeps, flags: HooksInstallFlag
|
|
|
2360
2522
|
const harness = resolveHarness(deps, flags.harness)
|
|
2361
2523
|
if (!harness) return EXIT.usage
|
|
2362
2524
|
const scriptPath = flags.scriptPath ?? process.argv[1] ?? 'notifai'
|
|
2363
|
-
const file = settingsFile(
|
|
2525
|
+
const file = settingsFile(
|
|
2526
|
+
harness,
|
|
2527
|
+
flags.global ?? false,
|
|
2528
|
+
deps.cwd,
|
|
2529
|
+
deps.env,
|
|
2530
|
+
deps.hookPlatform,
|
|
2531
|
+
)
|
|
2364
2532
|
if (harness === 'opencode') {
|
|
2365
2533
|
try {
|
|
2366
2534
|
return withTargetFileLock(file, () => {
|
|
@@ -3193,9 +3361,7 @@ function setupProofPath(deps: CommandDeps): string {
|
|
|
3193
3361
|
// the resolved absolute path is still a stable local identity for it.
|
|
3194
3362
|
}
|
|
3195
3363
|
const digest = createHash('sha256').update(projectDir).digest('hex').slice(0, 32)
|
|
3196
|
-
|
|
3197
|
-
const base = xdg && xdg !== '' ? xdg : path.join(os.homedir(), '.local', 'state')
|
|
3198
|
-
return path.join(base, 'notifai', 'setup-proofs', `${digest}.json`)
|
|
3364
|
+
return path.join(stateDir(deps.env), 'setup-proofs', `${digest}.json`)
|
|
3199
3365
|
}
|
|
3200
3366
|
|
|
3201
3367
|
function readSetupProof(deps: CommandDeps): SetupProofRecord | null {
|
|
@@ -3579,6 +3745,9 @@ async function runSetupProof(deps: CommandDeps): Promise<GapCloseResult> {
|
|
|
3579
3745
|
const observed = observedCompanionReceipt(snapshot, proof.device_id)
|
|
3580
3746
|
if (observed) {
|
|
3581
3747
|
spinner?.stop(`Receipt observed from ${observed.delivery.device_name}`)
|
|
3748
|
+
deps.io.out(
|
|
3749
|
+
`Companion Receipt (the app's delivery confirmation) observed from ${observed.delivery.device_name}.`,
|
|
3750
|
+
)
|
|
3582
3751
|
return 'closed'
|
|
3583
3752
|
}
|
|
3584
3753
|
lastError = null
|
|
@@ -4399,7 +4568,7 @@ function remedyLine(state: ReadinessState): string {
|
|
|
4399
4568
|
* is, is each check's own call (`HookCheck`).
|
|
4400
4569
|
*/
|
|
4401
4570
|
function hookStates(deps: CommandDeps): ReadinessState[] {
|
|
4402
|
-
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome)
|
|
4571
|
+
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
|
|
4403
4572
|
const active = activeHarnessSession(deps.env, deps.cwd, (deps.now ?? Date.now)())
|
|
4404
4573
|
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
4405
4574
|
const settings: ReadinessState = {
|
|
@@ -4492,13 +4661,16 @@ function hookStates(deps: CommandDeps): ReadinessState[] {
|
|
|
4492
4661
|
* hashes into `trusted_hash`, and an untrusted handler is simply not run.
|
|
4493
4662
|
* - Everything else blocks its turn and needs a ceiling above the wait.
|
|
4494
4663
|
*/
|
|
4495
|
-
function stopShapeProblems(
|
|
4664
|
+
function stopShapeProblems(
|
|
4665
|
+
installation: Installation,
|
|
4666
|
+
platform: NodeJS.Platform = process.platform,
|
|
4667
|
+
): string[] {
|
|
4496
4668
|
if (installation.harness === 'codex') return []
|
|
4497
4669
|
const problems: string[] = []
|
|
4498
4670
|
for (const handler of installation.handlers.filter(
|
|
4499
4671
|
(entry) => handlerEvent(entry.command) === 'stop',
|
|
4500
4672
|
)) {
|
|
4501
|
-
if (installation.harness === 'claude-code') {
|
|
4673
|
+
if (installation.harness === 'claude-code' && platform !== 'win32') {
|
|
4502
4674
|
if (handler.async !== true) {
|
|
4503
4675
|
problems.push(
|
|
4504
4676
|
`${installation.file} declares a blocking Stop handler; the Claude Code wake route needs \`async: true\` so the turn ends while the waiter runs`,
|
|
@@ -4532,7 +4704,7 @@ interface HookCheck {
|
|
|
4532
4704
|
|
|
4533
4705
|
function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
4534
4706
|
const checks: HookCheck[] = []
|
|
4535
|
-
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome)
|
|
4707
|
+
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
|
|
4536
4708
|
|
|
4537
4709
|
// Not having hooks is a setup someone chose, not a fault: `send` works
|
|
4538
4710
|
// without them. A setup that cannot work is what deserves to go red.
|
|
@@ -4651,7 +4823,7 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
4651
4823
|
const adapterProblems = installations.flatMap((installation) =>
|
|
4652
4824
|
(installation.problems ?? []).map((problem) => `${installation.file}: ${problem}`),
|
|
4653
4825
|
)
|
|
4654
|
-
const sharedAdapter = inspectHookAdapter(deps.hookAdapterHome)
|
|
4826
|
+
const sharedAdapter = inspectHookAdapter(deps.hookAdapterHome, deps.hookPlatform)
|
|
4655
4827
|
adapterProblems.push(...sharedAdapter.problems)
|
|
4656
4828
|
if (adapterProblems.length > 0) {
|
|
4657
4829
|
checks.push({
|
|
@@ -4683,7 +4855,7 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
4683
4855
|
})
|
|
4684
4856
|
|
|
4685
4857
|
const shapeProblems = installations.flatMap((installation) =>
|
|
4686
|
-
stopShapeProblems(installation).map(
|
|
4858
|
+
stopShapeProblems(installation, deps.hookPlatform).map(
|
|
4687
4859
|
(problem) =>
|
|
4688
4860
|
`${problem} — run \`notifai hooks install --harness ${installation.harness}${installation.global ? ' --global' : ''}\``,
|
|
4689
4861
|
),
|
|
@@ -4896,7 +5068,7 @@ function wakeRouteCheck(
|
|
|
4896
5068
|
if (active.harness === 'claude-code') {
|
|
4897
5069
|
const readiness = inspectClaudeInbox({
|
|
4898
5070
|
pid: deps.claudeSourcePid ?? claudeSessionPid(deps.env),
|
|
4899
|
-
platform: process.platform,
|
|
5071
|
+
platform: deps.hookPlatform ?? process.platform,
|
|
4900
5072
|
readDescriptor:
|
|
4901
5073
|
deps.claudeWake?.readDescriptor ?? systemClaudeWakeAdapters(deps.env).readDescriptor,
|
|
4902
5074
|
socketExists: (socketPath) => existsSync(socketPath),
|
|
@@ -4913,7 +5085,7 @@ function wakeRouteCheck(
|
|
|
4913
5085
|
}
|
|
4914
5086
|
if (active.harness === 'codex') {
|
|
4915
5087
|
const readiness = inspectCodexResume(deps.env, {
|
|
4916
|
-
platform: process.platform,
|
|
5088
|
+
platform: deps.hookPlatform ?? process.platform,
|
|
4917
5089
|
directoryExists: (directory) => existsSync(directory),
|
|
4918
5090
|
})
|
|
4919
5091
|
return {
|
|
@@ -5069,20 +5241,6 @@ export function realIo(env: NodeJS.ProcessEnv = process.env): CommandIo {
|
|
|
5069
5241
|
return log.error(message)
|
|
5070
5242
|
}
|
|
5071
5243
|
},
|
|
5072
|
-
openUrl
|
|
5073
|
-
try {
|
|
5074
|
-
if (process.platform === 'darwin') {
|
|
5075
|
-
spawn('open', [url], { stdio: 'ignore', detached: true }).unref()
|
|
5076
|
-
} else if (process.platform === 'win32') {
|
|
5077
|
-
// `start` is a cmd builtin; spawn('start', …) cannot work on Windows.
|
|
5078
|
-
// Empty title argument keeps URLs with special characters intact.
|
|
5079
|
-
spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref()
|
|
5080
|
-
} else {
|
|
5081
|
-
spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref()
|
|
5082
|
-
}
|
|
5083
|
-
} catch {
|
|
5084
|
-
// Browser opening is best-effort; the URL is printed anyway.
|
|
5085
|
-
}
|
|
5086
|
-
},
|
|
5244
|
+
openUrl,
|
|
5087
5245
|
}
|
|
5088
5246
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
3
|
import path from 'node:path'
|
|
5
4
|
import { parse as parseToml } from 'smol-toml'
|
|
6
5
|
import type { CLI_SOUNDS, INTERRUPTION_LEVELS } from '@raidiant/notifai-protocol'
|
|
6
|
+
import { configHome, stateHome } from './platform.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Layered configuration with provenance. Most specific wins:
|
|
@@ -163,10 +163,11 @@ export function configDefaultValue(key: ConfigKey): unknown {
|
|
|
163
163
|
return DEFAULTS[key]
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
export function globalConfigDir(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
export function globalConfigDir(
|
|
167
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
168
|
+
platform: NodeJS.Platform = process.platform,
|
|
169
|
+
): string {
|
|
170
|
+
return path.join(configHome(env, platform), 'notifai')
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
export function globalConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
@@ -178,10 +179,11 @@ export function globalConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
|
178
179
|
* session config overrides. Separate from the config dir because none of it is
|
|
179
180
|
* user-authored and none of it should ever be synced or committed.
|
|
180
181
|
*/
|
|
181
|
-
export function stateDir(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
export function stateDir(
|
|
183
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
184
|
+
platform: NodeJS.Platform = process.platform,
|
|
185
|
+
): string {
|
|
186
|
+
return path.join(stateHome(env, platform), 'notifai')
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
/** Per-harness-session override file; see the ConfigSource doc comment. */
|