@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raidiant/notifai",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Send native device notifications from agents and local programs",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@clack/prompts": "^1.7.0",
|
|
24
|
-
"@raidiant/notifai-protocol": "
|
|
24
|
+
"@raidiant/notifai-protocol": "6.0.1",
|
|
25
25
|
"commander": "^14.0.0",
|
|
26
26
|
"picocolors": "^1.1.1",
|
|
27
27
|
"smol-toml": "^1.4.0"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"vitest": "^3.2.0"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
34
|
+
"node": ">=22"
|
|
35
35
|
},
|
|
36
36
|
"license": "Apache-2.0",
|
|
37
37
|
"author": "Rafael Vidaurre",
|
package/src/client.test.ts
CHANGED
|
@@ -110,6 +110,18 @@ describe('a server that never answers', () => {
|
|
|
110
110
|
])
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
+
it('lists Account custom sounds from the catalog endpoint', async () => {
|
|
114
|
+
let seen: string | undefined
|
|
115
|
+
const baseUrl = await serving((request, response) => {
|
|
116
|
+
seen = `${request.method} ${request.url}`
|
|
117
|
+
response.setHeader('content-type', 'application/json')
|
|
118
|
+
response.end(JSON.stringify({ sounds: [] }))
|
|
119
|
+
})
|
|
120
|
+
const result = await createClient(baseUrl, 'Bearer test').listSounds()
|
|
121
|
+
expect(seen).toBe('GET /api/v1/sounds')
|
|
122
|
+
expect(result).toEqual({ sounds: [] })
|
|
123
|
+
})
|
|
124
|
+
|
|
113
125
|
it('refuses redirects on bearer-authenticated calls', async () => {
|
|
114
126
|
let followed = false
|
|
115
127
|
const baseUrl = await serving((request, response) => {
|
package/src/client.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
EvidenceSnapshot,
|
|
10
10
|
GetAgentAcknowledgementResponse,
|
|
11
11
|
ListDevicesResponse,
|
|
12
|
+
ListSoundsResponse,
|
|
12
13
|
ClientCapability,
|
|
13
14
|
CompatibilityResponse,
|
|
14
15
|
RecoveryAction,
|
|
@@ -17,6 +18,8 @@ import type {
|
|
|
17
18
|
PollPairingResponse,
|
|
18
19
|
PutAgentAcknowledgementRequestT,
|
|
19
20
|
PutAgentAcknowledgementResponse,
|
|
21
|
+
PutAgentSessionLabelRequestT,
|
|
22
|
+
AgentSessionView,
|
|
20
23
|
SubmissionReceipt,
|
|
21
24
|
SubmitNotificationRequestT,
|
|
22
25
|
} from '@raidiant/notifai-protocol'
|
|
@@ -76,6 +79,7 @@ export interface ApiClient {
|
|
|
76
79
|
pollPairing(pairingId: string, pollVerifier: string): Promise<PollPairingResponse>
|
|
77
80
|
accessStatus(): Promise<AccountAccessResponse>
|
|
78
81
|
listDevices(): Promise<ListDevicesResponse>
|
|
82
|
+
listSounds(): Promise<ListSoundsResponse>
|
|
79
83
|
capabilities(
|
|
80
84
|
platform?: Platform,
|
|
81
85
|
appVersion?: string,
|
|
@@ -94,6 +98,7 @@ export interface ApiClient {
|
|
|
94
98
|
requestId: string,
|
|
95
99
|
body: PutAgentAcknowledgementRequestT,
|
|
96
100
|
): Promise<PutAgentAcknowledgementResponse>
|
|
101
|
+
putAgentSessionLabel(body: PutAgentSessionLabelRequestT): Promise<AgentSessionView>
|
|
97
102
|
agentAcknowledgement(
|
|
98
103
|
requestId: string,
|
|
99
104
|
options: { waitSeconds: number },
|
|
@@ -250,6 +255,7 @@ export function createClient(
|
|
|
250
255
|
}),
|
|
251
256
|
accessStatus: () => call('GET', '/api/v1/account/access'),
|
|
252
257
|
listDevices: () => call('GET', '/api/v1/devices'),
|
|
258
|
+
listSounds: () => call('GET', '/api/v1/sounds'),
|
|
253
259
|
capabilities: (platform = 'ios', appVersion, appBuild) => {
|
|
254
260
|
const query = new URLSearchParams()
|
|
255
261
|
if (appVersion !== undefined) query.set('app_version', appVersion)
|
|
@@ -279,6 +285,8 @@ export function createClient(
|
|
|
279
285
|
`/api/v1/notifications/${encodeURIComponent(requestId)}/agent-acknowledgement`,
|
|
280
286
|
body,
|
|
281
287
|
),
|
|
288
|
+
putAgentSessionLabel: (body) =>
|
|
289
|
+
call<AgentSessionView>('PUT', '/api/v1/agent-sessions/label', body),
|
|
282
290
|
agentAcknowledgement: (requestId, { waitSeconds }) =>
|
|
283
291
|
call<GetAgentAcknowledgementResponse>(
|
|
284
292
|
'GET',
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { resolveActiveHarness } from './commands-harness-context.js'
|
|
2
|
+
import {
|
|
3
|
+
authedClient,
|
|
4
|
+
EXIT,
|
|
5
|
+
loadLoggedConfig,
|
|
6
|
+
reportError,
|
|
7
|
+
type CommandDeps,
|
|
8
|
+
} from './commands-core.js'
|
|
9
|
+
import {
|
|
10
|
+
normalizeExplicitSessionLabel,
|
|
11
|
+
renameStoredSessionLabel,
|
|
12
|
+
} from './session-labels.js'
|
|
13
|
+
|
|
14
|
+
export interface AgentSessionRenameFlags {
|
|
15
|
+
json?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Rename only the exact harness-owned Agent Session running this command. */
|
|
19
|
+
export async function agentSessionRenameCommand(
|
|
20
|
+
deps: CommandDeps,
|
|
21
|
+
label: string,
|
|
22
|
+
flags: AgentSessionRenameFlags = {},
|
|
23
|
+
): Promise<number> {
|
|
24
|
+
const now = deps.now?.() ?? Date.now()
|
|
25
|
+
const resolution = resolveActiveHarness(deps.env, deps.cwd, now)
|
|
26
|
+
const active = resolution.active
|
|
27
|
+
if (resolution.contested.length > 0 || active?.sessionId === undefined) {
|
|
28
|
+
deps.io.err(
|
|
29
|
+
'No exact active Agent Session can be proven here. Run this command inside the Agent Session you intend to rename.',
|
|
30
|
+
)
|
|
31
|
+
return EXIT.usage
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalized = normalizeExplicitSessionLabel({
|
|
35
|
+
env: deps.env,
|
|
36
|
+
sessionId: active.sessionId,
|
|
37
|
+
label,
|
|
38
|
+
})
|
|
39
|
+
if (!normalized.ok) {
|
|
40
|
+
deps.io.err(normalized.error)
|
|
41
|
+
return EXIT.usage
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
45
|
+
const auth = authedClient(deps, config)
|
|
46
|
+
if (!auth) return EXIT.auth
|
|
47
|
+
try {
|
|
48
|
+
const renamed = await auth.client.putAgentSessionLabel({
|
|
49
|
+
session_id: active.sessionId,
|
|
50
|
+
label: normalized.label,
|
|
51
|
+
})
|
|
52
|
+
const local = renameStoredSessionLabel({
|
|
53
|
+
env: deps.env,
|
|
54
|
+
sessionId: active.sessionId,
|
|
55
|
+
label: renamed.label,
|
|
56
|
+
harness: active.harness,
|
|
57
|
+
now,
|
|
58
|
+
})
|
|
59
|
+
if (!local.ok) {
|
|
60
|
+
deps.io.err(local.error)
|
|
61
|
+
return EXIT.failed
|
|
62
|
+
}
|
|
63
|
+
deps.io.out(
|
|
64
|
+
flags.json
|
|
65
|
+
? JSON.stringify(renamed)
|
|
66
|
+
: `Agent Session renamed to "${renamed.label}" across your Notifai account.`,
|
|
67
|
+
)
|
|
68
|
+
return EXIT.ok
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return reportError(deps, err, { operation: 'agent_session_rename' })
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/commands-auth.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type FlagOverrides } from './config.js'
|
|
|
7
7
|
import { checkApproveUrl } from './url-policy.js'
|
|
8
8
|
import {
|
|
9
9
|
EXIT,
|
|
10
|
+
SETUP_COMMAND,
|
|
10
11
|
authedClient,
|
|
11
12
|
loadLoggedConfig,
|
|
12
13
|
makeClient,
|
|
@@ -128,14 +129,14 @@ export async function loginCommand(
|
|
|
128
129
|
spinner?.error('Account has no Alpha access')
|
|
129
130
|
deps.io.err('This account has no active plan or temporary Alpha access.')
|
|
130
131
|
deps.io.err(`next: ${next}`)
|
|
131
|
-
deps.io.err(
|
|
132
|
+
deps.io.err(`After access is granted, run \`${SETUP_COMMAND}\` again.`)
|
|
132
133
|
return EXIT.auth
|
|
133
134
|
}
|
|
134
135
|
if (poll.status === 'expired') break
|
|
135
136
|
spinner?.message(approvalWaitMessage())
|
|
136
137
|
}
|
|
137
138
|
spinner?.error('Pairing expired')
|
|
138
|
-
deps.io.err(
|
|
139
|
+
deps.io.err(`Pairing expired before it was approved. Run \`${SETUP_COMMAND}\` again.`)
|
|
139
140
|
return EXIT.auth
|
|
140
141
|
}
|
|
141
142
|
|
|
@@ -166,7 +167,7 @@ export function authStatusCommand(deps: CommandDeps, flags: { json?: boolean }):
|
|
|
166
167
|
return credential ? EXIT.ok : EXIT.auth
|
|
167
168
|
}
|
|
168
169
|
if (!credential) {
|
|
169
|
-
deps.io.err(
|
|
170
|
+
deps.io.err(`Not signed in. Run \`${SETUP_COMMAND}\`; it will coordinate machine login and device setup.`)
|
|
170
171
|
return EXIT.auth
|
|
171
172
|
}
|
|
172
173
|
deps.io.out(`Signed in as machine "${credential.machineName}" (${credential.machineId})`)
|
package/src/commands-config.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { normalizeOrigin } from './url-policy.js'
|
|
27
27
|
import { renderConfigExplain, renderConfigList, renderConfigPlain } from './ui/config-view.js'
|
|
28
28
|
import { EXIT, loadLoggedConfig, type CommandDeps } from './commands-core.js'
|
|
29
|
+
import { isCliSoundRef } from './sound-ref.js'
|
|
29
30
|
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
31
32
|
// config show / set / unset
|
|
@@ -186,10 +187,15 @@ export async function configSetCommand(
|
|
|
186
187
|
}
|
|
187
188
|
value = rawValue === 'true'
|
|
188
189
|
}
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
if (
|
|
190
|
+
// Sound is listed as an enum of shipped names so the interactive picker has
|
|
191
|
+
// choices, but it also accepts an Account custom name or id. Other enum keys
|
|
192
|
+
// stay closed: an unrecognised value used to be written and only fail at send.
|
|
193
|
+
if (configKey === 'sound') {
|
|
194
|
+
if (!isCliSoundRef(rawValue)) {
|
|
195
|
+
deps.io.err(`${key} takes ${acceptedValues(configKey)} — not "${rawValue}".`)
|
|
196
|
+
return EXIT.usage
|
|
197
|
+
}
|
|
198
|
+
} else if (info.kind === 'enum' && info.choices !== undefined && !info.choices.includes(rawValue)) {
|
|
193
199
|
deps.io.err(`${key} takes one of: ${info.choices.join(', ')} — not "${rawValue}".`)
|
|
194
200
|
return EXIT.usage
|
|
195
201
|
}
|
package/src/commands-core.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { CredentialStore, MachineCredential } from './credentials.js'
|
|
|
13
13
|
import { type HookAdapterTarget } from './hook-adapter.js'
|
|
14
14
|
import { logConfigResolved, logSettingsFrom, nullLogger, type Logger } from './logging.js'
|
|
15
15
|
import type { NativeSkills } from './native-skills.js'
|
|
16
|
+
import type { OrcaSessionTitleLookup } from './orca-session-title.js'
|
|
16
17
|
import { packageVersion } from './release.js'
|
|
17
18
|
import type { Tone } from './ui/theme.js'
|
|
18
19
|
|
|
@@ -81,6 +82,8 @@ export interface CommandDeps {
|
|
|
81
82
|
codexSourcePid?: number
|
|
82
83
|
/** Test seam and production adapter for the external native skills installer. */
|
|
83
84
|
nativeSkills?: NativeSkills
|
|
85
|
+
/** Test seam for Orca's exact-worktree task/session title lookup. */
|
|
86
|
+
orcaSessionTitle?: OrcaSessionTitleLookup
|
|
84
87
|
/** Test seam for registry reads; production uses fetch. */
|
|
85
88
|
fetchImpl?: typeof fetch
|
|
86
89
|
/**
|
|
@@ -183,6 +186,9 @@ export function authedClient(deps: CommandDeps, config: CliConfig): { client: Ap
|
|
|
183
186
|
|
|
184
187
|
export const UPDATE_CLI_COMMAND = 'npm install -g @raidiant/notifai'
|
|
185
188
|
|
|
189
|
+
/** The one first-run command an unsigned machine is told to run. */
|
|
190
|
+
export const SETUP_COMMAND = 'notifai init'
|
|
191
|
+
|
|
186
192
|
function affectedDeviceNames(details: unknown): string[] {
|
|
187
193
|
if (typeof details !== 'object' || details === null || !('device_names' in details)) return []
|
|
188
194
|
const names = (details as { device_names?: unknown }).device_names
|
package/src/commands-doctor.ts
CHANGED
|
@@ -5,7 +5,12 @@ import { inspectClaudeInbox, systemClaudeWakeAdapters } from './claude-wake.js'
|
|
|
5
5
|
import { ApiCallError, type ApiClient } from './client.js'
|
|
6
6
|
import { inspectCodexResume } from './codex-wake.js'
|
|
7
7
|
import { type CliConfig } from './config.js'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
HARNESS_CAPABILITIES,
|
|
10
|
+
HARNESS_LABELS,
|
|
11
|
+
HERMES_QUESTION_ROUTING_UNAVAILABLE,
|
|
12
|
+
isHookInstallableHarness,
|
|
13
|
+
} from './harnesses.js'
|
|
9
14
|
import { inspectHookAdapter } from './hook-adapter.js'
|
|
10
15
|
import {
|
|
11
16
|
readLiveProjectSessionPointers,
|
|
@@ -43,6 +48,7 @@ import type { Tone } from './ui/theme.js'
|
|
|
43
48
|
import type { MachineCredential } from './credentials.js'
|
|
44
49
|
import {
|
|
45
50
|
EXIT,
|
|
51
|
+
SETUP_COMMAND,
|
|
46
52
|
UPDATE_CLI_COMMAND,
|
|
47
53
|
diagnoseIgnoredOriginOverride,
|
|
48
54
|
loadLoggedConfig,
|
|
@@ -52,7 +58,6 @@ import {
|
|
|
52
58
|
} from './commands-core.js'
|
|
53
59
|
import { deviceInstallRemedy, readyCompanionDevices, supportPageUrl } from './commands-devices.js'
|
|
54
60
|
import {
|
|
55
|
-
activeHarnessSession,
|
|
56
61
|
claudeSessionPid,
|
|
57
62
|
resolveActiveHarness,
|
|
58
63
|
type ActiveHarnessSession,
|
|
@@ -299,6 +304,94 @@ function remoteInvalidatedByConfig(
|
|
|
299
304
|
return server === undefined || !server.detail.includes(origin)
|
|
300
305
|
}
|
|
301
306
|
|
|
307
|
+
function isNoActivePlanError(err: unknown): err is ApiCallError {
|
|
308
|
+
return err instanceof ApiCallError && err.code === 'no_active_plan'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Device listing is grant-gated; access status is not. A paired Account whose
|
|
313
|
+
* plan lapsed must not be diagnosed as a revoked machine.
|
|
314
|
+
*/
|
|
315
|
+
async function probeAccount(
|
|
316
|
+
client: ApiClient,
|
|
317
|
+
credential: MachineCredential,
|
|
318
|
+
baseUrl: string,
|
|
319
|
+
): Promise<{
|
|
320
|
+
email: string | null
|
|
321
|
+
devices: RoutableDevice[] | null
|
|
322
|
+
lookupFailed: boolean
|
|
323
|
+
auth: ReadinessState
|
|
324
|
+
}> {
|
|
325
|
+
const [devicesOutcome, accessOutcome] = await Promise.allSettled([
|
|
326
|
+
Promise.resolve().then(() => client.listDevices()),
|
|
327
|
+
Promise.resolve().then(() => client.accessStatus()),
|
|
328
|
+
])
|
|
329
|
+
const access = accessOutcome.status === 'fulfilled' ? accessOutcome.value : null
|
|
330
|
+
const devices = devicesOutcome.status === 'fulfilled' ? devicesOutcome.value.devices : null
|
|
331
|
+
const email = access?.email ?? null
|
|
332
|
+
const devicesErr = devicesOutcome.status === 'rejected' ? devicesOutcome.reason : null
|
|
333
|
+
const noActivePlan =
|
|
334
|
+
access?.status === 'no_active_plan' || isNoActivePlanError(devicesErr)
|
|
335
|
+
|
|
336
|
+
if (noActivePlan) {
|
|
337
|
+
const next =
|
|
338
|
+
(isNoActivePlanError(devicesErr) ? devicesErr.nextAction : null) ??
|
|
339
|
+
`Open ${supportPageUrl(baseUrl)} to request Alpha access, then retry.`
|
|
340
|
+
return {
|
|
341
|
+
email,
|
|
342
|
+
devices: null,
|
|
343
|
+
lookupFailed: true,
|
|
344
|
+
auth: {
|
|
345
|
+
id: 'auth',
|
|
346
|
+
title: 'Account',
|
|
347
|
+
status: 'gap',
|
|
348
|
+
detail: email
|
|
349
|
+
? `no active plan or temporary Alpha access (${email})`
|
|
350
|
+
: 'no active plan or temporary Alpha access',
|
|
351
|
+
remedy: {
|
|
352
|
+
by: 'user-elsewhere',
|
|
353
|
+
summary: next,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (devices !== null) {
|
|
360
|
+
return {
|
|
361
|
+
email,
|
|
362
|
+
devices,
|
|
363
|
+
lookupFailed: false,
|
|
364
|
+
auth: {
|
|
365
|
+
id: 'auth',
|
|
366
|
+
title: 'Account',
|
|
367
|
+
status: 'ready',
|
|
368
|
+
detail: email
|
|
369
|
+
? `machine ${credential.machineId} accepted (${email})`
|
|
370
|
+
: `machine ${credential.machineId} accepted`,
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const err = devicesErr ?? (accessOutcome.status === 'rejected' ? accessOutcome.reason : null)
|
|
376
|
+
return {
|
|
377
|
+
email: null,
|
|
378
|
+
devices: null,
|
|
379
|
+
lookupFailed: true,
|
|
380
|
+
auth: {
|
|
381
|
+
id: 'auth',
|
|
382
|
+
title: 'Account',
|
|
383
|
+
status: 'gap',
|
|
384
|
+
detail: err instanceof ApiCallError ? `${err.code}: ${err.message}` : String(err),
|
|
385
|
+
remedy: {
|
|
386
|
+
by: 'user-here',
|
|
387
|
+
summary: 'this machine is no longer recognised; pair it again',
|
|
388
|
+
command: SETUP_COMMAND,
|
|
389
|
+
interactive: true,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
302
395
|
/**
|
|
303
396
|
* Read the whole setup once, in dependency order.
|
|
304
397
|
*
|
|
@@ -374,7 +467,7 @@ export async function assessReadiness(
|
|
|
374
467
|
remedy: {
|
|
375
468
|
by: 'user-here',
|
|
376
469
|
summary: 'sign in — this opens your browser to approve the machine',
|
|
377
|
-
command:
|
|
470
|
+
command: SETUP_COMMAND,
|
|
378
471
|
interactive: true,
|
|
379
472
|
},
|
|
380
473
|
},
|
|
@@ -433,39 +526,11 @@ export async function assessReadiness(
|
|
|
433
526
|
} else {
|
|
434
527
|
const client = makeClient(deps, baseUrl, `Bearer nfm_${credential.machineId}.${credential.secret}`)
|
|
435
528
|
accountClient = client
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
.catch(() => null as string | null),
|
|
442
|
-
])
|
|
443
|
-
accountDevices = devices
|
|
444
|
-
accountEmail = email
|
|
445
|
-
states.push({
|
|
446
|
-
id: 'auth',
|
|
447
|
-
title: 'Account',
|
|
448
|
-
status: 'ready',
|
|
449
|
-
detail: accountEmail
|
|
450
|
-
? `machine ${credential.machineId} accepted (${accountEmail})`
|
|
451
|
-
: `machine ${credential.machineId} accepted`,
|
|
452
|
-
})
|
|
453
|
-
} catch (err) {
|
|
454
|
-
// A credential the server rejects is revocation, not absence, and the
|
|
455
|
-
// remedy is the same sign-in either way.
|
|
456
|
-
accountLookupFailed = true
|
|
457
|
-
states.push({
|
|
458
|
-
id: 'auth',
|
|
459
|
-
title: 'Account',
|
|
460
|
-
status: 'gap',
|
|
461
|
-
detail: err instanceof ApiCallError ? `${err.code}: ${err.message}` : String(err),
|
|
462
|
-
remedy: {
|
|
463
|
-
by: 'user-here',
|
|
464
|
-
summary: 'this machine is no longer recognised; pair it again',
|
|
465
|
-
command: 'notifai login',
|
|
466
|
-
},
|
|
467
|
-
})
|
|
468
|
-
}
|
|
529
|
+
const probed = await probeAccount(client, credential, baseUrl)
|
|
530
|
+
accountDevices = probed.devices
|
|
531
|
+
accountEmail = probed.email
|
|
532
|
+
accountLookupFailed = probed.lookupFailed
|
|
533
|
+
states.push(probed.auth)
|
|
469
534
|
}
|
|
470
535
|
|
|
471
536
|
// Optional setup that works without a companion device must appear before the
|
|
@@ -761,7 +826,11 @@ export function remedyLine(state: ReadinessState): string {
|
|
|
761
826
|
*/
|
|
762
827
|
function hookStates(deps: CommandDeps): ReadinessState[] {
|
|
763
828
|
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
|
|
764
|
-
const active
|
|
829
|
+
const { active, contested } = resolveActiveHarness(
|
|
830
|
+
deps.env,
|
|
831
|
+
deps.cwd,
|
|
832
|
+
(deps.now ?? Date.now)(),
|
|
833
|
+
)
|
|
765
834
|
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
766
835
|
const settings: ReadinessState = {
|
|
767
836
|
id: 'question-routing-settings',
|
|
@@ -782,6 +851,28 @@ function hookStates(deps: CommandDeps): ReadinessState[] {
|
|
|
782
851
|
}),
|
|
783
852
|
}
|
|
784
853
|
if (installations.length === 0) {
|
|
854
|
+
if (contested.length > 1) {
|
|
855
|
+
return [
|
|
856
|
+
{
|
|
857
|
+
id: 'hooks',
|
|
858
|
+
title: 'Question routing',
|
|
859
|
+
status: 'optional-gap',
|
|
860
|
+
detail: `Several harness sessions could own this shell (${contested.map((candidate) => candidate.label).join(', ')}); routing readiness is intentionally not attributed to either one`,
|
|
861
|
+
},
|
|
862
|
+
settings,
|
|
863
|
+
]
|
|
864
|
+
}
|
|
865
|
+
if (active !== null && !isHookInstallableHarness(active.harness)) {
|
|
866
|
+
return [
|
|
867
|
+
{
|
|
868
|
+
id: 'hooks',
|
|
869
|
+
title: 'Question routing',
|
|
870
|
+
status: 'optional-gap',
|
|
871
|
+
detail: `${active.label}: ${HERMES_QUESTION_ROUTING_UNAVAILABLE.deliveryContract}`,
|
|
872
|
+
},
|
|
873
|
+
settings,
|
|
874
|
+
]
|
|
875
|
+
}
|
|
785
876
|
return [
|
|
786
877
|
{
|
|
787
878
|
id: 'hooks',
|
|
@@ -925,7 +1016,21 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
925
1016
|
active === null
|
|
926
1017
|
? []
|
|
927
1018
|
: installations.filter((installation) => installation.harness === active.harness)
|
|
928
|
-
if (
|
|
1019
|
+
if (contested.length > 1) {
|
|
1020
|
+
checks.push({
|
|
1021
|
+
name: 'hooks (active harness)',
|
|
1022
|
+
ok: false,
|
|
1023
|
+
informational: true,
|
|
1024
|
+
detail: `Several harness sessions could own this shell (${contested.map((candidate) => candidate.label).join(', ')}); routing readiness is intentionally not attributed to either one`,
|
|
1025
|
+
})
|
|
1026
|
+
} else if (active !== null && !isHookInstallableHarness(active.harness)) {
|
|
1027
|
+
checks.push({
|
|
1028
|
+
name: 'hooks (active harness)',
|
|
1029
|
+
ok: true,
|
|
1030
|
+
informational: true,
|
|
1031
|
+
detail: `${active.label}: ${HERMES_QUESTION_ROUTING_UNAVAILABLE.deliveryContract}`,
|
|
1032
|
+
})
|
|
1033
|
+
} else if (active !== null) {
|
|
929
1034
|
checks.push({
|
|
930
1035
|
name: 'hooks (active harness)',
|
|
931
1036
|
ok: activeInstallations.length > 0,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hermesClassicCliLocalInstance,
|
|
3
|
+
isHookInstallableHarness,
|
|
4
|
+
type HermesClassicCliLocalInstance,
|
|
5
|
+
type SourceContextHarness,
|
|
6
|
+
} from './harnesses.js'
|
|
1
7
|
import { readLiveProjectSessionPointers, readSessionState } from './hooks.js'
|
|
2
|
-
import { type Harness } from './install-hooks.js'
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Exact evidence that this shell command is running inside one supported
|
|
@@ -7,11 +12,17 @@ import { type Harness } from './install-hooks.js'
|
|
|
7
12
|
* describe where a tool stores files, not which tool owns the current shell.
|
|
8
13
|
* OpenCode's generated plugin supplies the Notifai-owned marker because its
|
|
9
14
|
* plugin API exposes Agent Session identity but the ordinary environment does not.
|
|
15
|
+
*
|
|
16
|
+
* Hermes contributes `HERMES_SESSION_ID` only — the durable Agent Session id
|
|
17
|
+
* bridged into tool subprocesses. `HERMES_SESSION_KEY` is a gateway routing
|
|
18
|
+
* key and must never be treated as Agent Session identity.
|
|
10
19
|
*/
|
|
11
20
|
export interface ActiveHarnessSession {
|
|
12
|
-
harness:
|
|
21
|
+
harness: SourceContextHarness
|
|
13
22
|
label: string
|
|
14
23
|
sessionId?: string
|
|
24
|
+
/** Exact surface/backend cell proven for this marker envelope. */
|
|
25
|
+
integrationInstance?: HermesClassicCliLocalInstance
|
|
15
26
|
/** Trusted human title published by this harness's managed adapter. */
|
|
16
27
|
sessionLabel?: string
|
|
17
28
|
/** The harness has only a temporary placeholder title so far. */
|
|
@@ -40,6 +51,14 @@ function harnessEnvCandidates(env: NodeJS.ProcessEnv): ActiveHarnessSession[] {
|
|
|
40
51
|
...(sessionLabelPending ? { sessionLabelPending: true } : {}),
|
|
41
52
|
})
|
|
42
53
|
}
|
|
54
|
+
if (env['NOTIFAI_ACTIVE_HARNESS'] === 'openclaw') {
|
|
55
|
+
const sessionId = env['NOTIFAI_ACTIVE_SESSION_ID']
|
|
56
|
+
candidates.push({
|
|
57
|
+
harness: 'openclaw',
|
|
58
|
+
label: 'OpenClaw',
|
|
59
|
+
...(sessionId === undefined || sessionId === '' ? {} : { sessionId }),
|
|
60
|
+
})
|
|
61
|
+
}
|
|
43
62
|
if (env['CLAUDECODE'] === '1') {
|
|
44
63
|
const sessionId = env['CLAUDE_CODE_SESSION_ID']
|
|
45
64
|
candidates.push({
|
|
@@ -53,6 +72,16 @@ function harnessEnvCandidates(env: NodeJS.ProcessEnv): ActiveHarnessSession[] {
|
|
|
53
72
|
candidates.push({ harness: 'codex', label: 'Codex', sessionId: codexSession })
|
|
54
73
|
}
|
|
55
74
|
if ((env['CURSOR_AGENT'] ?? '') !== '') candidates.push({ harness: 'cursor', label: 'Cursor' })
|
|
75
|
+
const hermesSession = env['HERMES_SESSION_ID']
|
|
76
|
+
if (hermesSession !== undefined && hermesSession !== '') {
|
|
77
|
+
const integrationInstance = hermesClassicCliLocalInstance(env)
|
|
78
|
+
candidates.push({
|
|
79
|
+
harness: 'hermes',
|
|
80
|
+
label: 'Hermes',
|
|
81
|
+
sessionId: hermesSession,
|
|
82
|
+
...(integrationInstance === null ? {} : { integrationInstance }),
|
|
83
|
+
})
|
|
84
|
+
}
|
|
56
85
|
return candidates
|
|
57
86
|
}
|
|
58
87
|
|
|
@@ -97,6 +126,12 @@ export function resolveActiveHarness(
|
|
|
97
126
|
if (first === undefined) return { active: null, contested: [] }
|
|
98
127
|
if (candidates.length === 1) return { active: first, contested: [] }
|
|
99
128
|
|
|
129
|
+
// A send-only harness has no hook evidence, so live pointer/state of a
|
|
130
|
+
// hook-installable parent cannot prove which nested process owns this
|
|
131
|
+
// shell. Mixing Hermes with another marker therefore stays contested.
|
|
132
|
+
const mixedWithSendOnly = candidates.some((candidate) => !isHookInstallableHarness(candidate.harness))
|
|
133
|
+
if (mixedWithSendOnly) return { active: first, contested: candidates }
|
|
134
|
+
|
|
100
135
|
// Exact lifecycle state is machine-global and survives a command moving to
|
|
101
136
|
// another linked checkout. Prefer it before the checkout-local
|
|
102
137
|
// pointer. A unique newest hook event proves which nested harness owns this
|
|
@@ -133,6 +168,25 @@ export function activeHarnessSession(
|
|
|
133
168
|
return resolveActiveHarness(env, cwd, now).active
|
|
134
169
|
}
|
|
135
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Source Context attribution. Hook-installable nesting still uses declared
|
|
173
|
+
* order when nothing has fired — that existing send behavior is preserved.
|
|
174
|
+
* A contested mix that includes a send-only harness (Hermes) fails closed:
|
|
175
|
+
* the wrong Agent Session would otherwise be named as the owner.
|
|
176
|
+
*/
|
|
177
|
+
export function sourceContextHarnessSession(
|
|
178
|
+
env: NodeJS.ProcessEnv,
|
|
179
|
+
cwd: string,
|
|
180
|
+
now: number,
|
|
181
|
+
): ActiveHarnessSession | null {
|
|
182
|
+
const { active, contested } = resolveActiveHarness(env, cwd, now)
|
|
183
|
+
if (contested.some((candidate) => !isHookInstallableHarness(candidate.harness))) {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
if (active?.harness === 'hermes' && active.integrationInstance === undefined) return null
|
|
187
|
+
return active
|
|
188
|
+
}
|
|
189
|
+
|
|
136
190
|
/**
|
|
137
191
|
* The pid of the Claude Code session this hook belongs to.
|
|
138
192
|
*
|