@raidiant/notifai 9.2.2 → 10.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 +45 -0
- package/README.md +8 -5
- package/dist/claude-wake.js +11 -0
- package/dist/claude-wake.js.map +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -1
- package/dist/codex-wake.js +9 -0
- package/dist/codex-wake.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-config.js +11 -4
- package/dist/commands-config.js.map +1 -1
- package/dist/commands-core.d.ts +1 -1
- package/dist/commands-doctor.js +83 -6
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-harness-context.d.ts +15 -2
- package/dist/commands-harness-context.js +32 -1
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hook-shape.d.ts +4 -3
- package/dist/commands-hook-shape.js +19 -10
- package/dist/commands-hook-shape.js.map +1 -1
- package/dist/commands-hooks.d.ts +4 -3
- package/dist/commands-hooks.js +80 -23
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.js +12 -1
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-project.d.ts +4 -0
- package/dist/commands-project.js +41 -0
- package/dist/commands-project.js.map +1 -0
- package/dist/commands-send-support.js +0 -4
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.js +12 -6
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-skill.js +58 -0
- package/dist/commands-skill.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 +3 -0
- package/dist/commands.js +3 -0
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.js +7 -4
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +6 -7
- package/dist/config.js +9 -9
- package/dist/config.js.map +1 -1
- package/dist/credentials.js +4 -1
- package/dist/credentials.js.map +1 -1
- package/dist/guidance-content.d.ts +3 -0
- package/dist/guidance-content.js +20 -0
- package/dist/guidance-content.js.map +1 -1
- package/dist/guidance-render.js +3 -3
- package/dist/guidance-render.js.map +1 -1
- package/dist/harnesses.d.ts +49 -7
- package/dist/harnesses.js +90 -24
- package/dist/harnesses.js.map +1 -1
- package/dist/hooks.d.ts +28 -52
- package/dist/hooks.js +393 -152
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +17 -39
- package/dist/install-hooks.js +21 -41
- package/dist/install-hooks.js.map +1 -1
- package/dist/invocation-context.d.ts +2 -2
- package/dist/orca-session-title.d.ts +2 -2
- package/dist/orca-session-title.js +3 -4
- package/dist/orca-session-title.js.map +1 -1
- package/dist/program.d.ts +6 -1
- package/dist/program.js +46 -5
- package/dist/program.js.map +1 -1
- package/dist/project-enablement.d.ts +15 -0
- package/dist/project-enablement.js +87 -0
- package/dist/project-enablement.js.map +1 -0
- package/dist/question-timing.d.ts +46 -0
- package/dist/question-timing.js +48 -0
- package/dist/question-timing.js.map +1 -0
- package/dist/readiness.d.ts +7 -0
- package/dist/readiness.js.map +1 -1
- package/dist/send.d.ts +2 -0
- package/dist/send.js +17 -15
- package/dist/send.js.map +1 -1
- package/dist/session-activation.d.ts +1 -1
- package/dist/session-activation.js +3 -4
- 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/package.json +3 -3
- package/src/claude-wake.test.ts +1 -0
- package/src/claude-wake.ts +10 -0
- package/src/client.test.ts +12 -0
- package/src/client.ts +8 -0
- package/src/codex-wake.test.ts +1 -0
- package/src/codex-wake.ts +9 -0
- package/src/commands-agent-sessions.ts +72 -0
- package/src/commands-config.ts +10 -4
- package/src/commands-core.ts +1 -1
- package/src/commands-doctor.ts +96 -8
- package/src/commands-harness-context.ts +48 -2
- package/src/commands-hook-shape.ts +26 -10
- package/src/commands-hooks.ts +95 -31
- package/src/commands-init.ts +11 -1
- package/src/commands-project.ts +47 -0
- package/src/commands-send-support.ts +0 -4
- package/src/commands-send.ts +10 -5
- package/src/commands-skill.test.ts +44 -0
- package/src/commands-skill.ts +57 -0
- package/src/commands-sounds.test.ts +106 -0
- package/src/commands-sounds.ts +49 -0
- package/src/commands.test.ts +386 -23
- package/src/commands.ts +10 -0
- package/src/config-schema.test.ts +1 -0
- package/src/config-schema.ts +7 -4
- package/src/config.test.ts +44 -23
- package/src/config.ts +13 -15
- 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/guidance-content.ts +22 -0
- package/src/guidance-render.ts +3 -3
- package/src/harness-integration.test.ts +212 -0
- package/src/harnesses.test.ts +107 -8
- package/src/harnesses.ts +111 -30
- package/src/hook-adapter.test.ts +27 -8
- package/src/hooks.test.ts +812 -83
- package/src/hooks.ts +529 -185
- package/src/install-hooks.test.ts +13 -16
- package/src/install-hooks.ts +40 -58
- package/src/invocation-context.ts +2 -2
- package/src/orca-session-title.test.ts +8 -49
- package/src/orca-session-title.ts +3 -5
- package/src/program.test.ts +45 -0
- package/src/program.ts +60 -4
- package/src/project-enablement.test.ts +55 -0
- package/src/project-enablement.ts +99 -0
- package/src/question-timing.ts +56 -0
- package/src/readiness.ts +8 -1
- package/src/send.ts +18 -20
- package/src/session-activation.ts +3 -4
- 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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `--sound` and the saved `sound` key accept: shipped semantic names,
|
|
3
|
+
* the CLI silent spelling, an Account custom sound id, or a custom display name.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isCliSoundRef(value: string): boolean;
|
|
6
|
+
export declare function unknownSoundMessage(sound: string): string;
|
|
7
|
+
/** Shipped names plus the CLI silent spelling, with User-facing labels. */
|
|
8
|
+
export declare const SHIPPED_SOUND_LISTING: readonly [{
|
|
9
|
+
readonly ref: "default";
|
|
10
|
+
readonly name: "Device default";
|
|
11
|
+
}, {
|
|
12
|
+
readonly ref: "done";
|
|
13
|
+
readonly name: "completion chime";
|
|
14
|
+
}, {
|
|
15
|
+
readonly ref: "attention";
|
|
16
|
+
readonly name: "attention tone";
|
|
17
|
+
}, {
|
|
18
|
+
readonly ref: "alert";
|
|
19
|
+
readonly name: "most insistent tone";
|
|
20
|
+
}, {
|
|
21
|
+
readonly ref: "none";
|
|
22
|
+
readonly name: "silent";
|
|
23
|
+
}];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CLI_SOUNDS, SOUND_NAME_MAX_LENGTH, SOUND_REF_MAX_LENGTH, isCustomSoundId, } from '@raidiant/notifai-protocol';
|
|
2
|
+
/**
|
|
3
|
+
* What `--sound` and the saved `sound` key accept: shipped semantic names,
|
|
4
|
+
* the CLI silent spelling, an Account custom sound id, or a custom display name.
|
|
5
|
+
*/
|
|
6
|
+
export function isCliSoundRef(value) {
|
|
7
|
+
if (CLI_SOUNDS.includes(value))
|
|
8
|
+
return true;
|
|
9
|
+
if (value.length < 1 || value.length > SOUND_REF_MAX_LENGTH)
|
|
10
|
+
return false;
|
|
11
|
+
if (isCustomSoundId(value))
|
|
12
|
+
return true;
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
return trimmed.length >= 1 && trimmed.length <= SOUND_NAME_MAX_LENGTH;
|
|
15
|
+
}
|
|
16
|
+
export function unknownSoundMessage(sound) {
|
|
17
|
+
return (`Unknown sound "${sound}" — use a shipped name (${CLI_SOUNDS.join(', ')}), ` +
|
|
18
|
+
'a custom sound id, or a custom sound name. Run `notifai sounds` to list what this Account can play.');
|
|
19
|
+
}
|
|
20
|
+
/** Shipped names plus the CLI silent spelling, with User-facing labels. */
|
|
21
|
+
export const SHIPPED_SOUND_LISTING = [
|
|
22
|
+
{ ref: 'default', name: 'Device default' },
|
|
23
|
+
{ ref: 'done', name: 'completion chime' },
|
|
24
|
+
{ ref: 'attention', name: 'attention tone' },
|
|
25
|
+
{ ref: 'alert', name: 'most insistent tone' },
|
|
26
|
+
{ ref: 'none', name: 'silent' },
|
|
27
|
+
];
|
|
28
|
+
//# sourceMappingURL=sound-ref.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sound-ref.js","sourceRoot":"","sources":["../src/sound-ref.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,GAChB,MAAM,4BAA4B,CAAA;AAEnC;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAK,UAAgC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAClE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,oBAAoB;QAAE,OAAO,KAAK,CAAA;IACzE,IAAI,eAAe,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACvC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC5B,OAAO,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,OAAO,CAAC,MAAM,IAAI,qBAAqB,CAAA;AACvE,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,OAAO,CACL,kBAAkB,KAAK,2BAA2B,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;QAC5E,qGAAqG,CACtG,CAAA;AACH,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,gBAAgB,EAAE;IAC1C,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,EAAE;IACzC,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE;IAC5C,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,qBAAqB,EAAE;IAC7C,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE;CACvB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raidiant/notifai",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.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.0",
|
|
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/claude-wake.test.ts
CHANGED
package/src/claude-wake.ts
CHANGED
|
@@ -357,6 +357,7 @@ export function claudeWakeRoute(options: {
|
|
|
357
357
|
acknowledgement: 'held',
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
+
if (!event.commitDelivery()) return cancelledDelivery()
|
|
360
361
|
await adapters.resume(options.sessionId, sourceDescriptor.cwd, event.context)
|
|
361
362
|
return {
|
|
362
363
|
notes: ['cold-resumed the stopped Claude session with its accepted answer'],
|
|
@@ -376,6 +377,7 @@ export function claudeWakeRoute(options: {
|
|
|
376
377
|
acknowledgement: 'held',
|
|
377
378
|
}
|
|
378
379
|
}
|
|
380
|
+
if (!event.commitDelivery()) return cancelledDelivery()
|
|
379
381
|
await adapters.sendSocket(
|
|
380
382
|
observation.descriptor.messagingSocketPath,
|
|
381
383
|
socketLine(event.context),
|
|
@@ -400,6 +402,14 @@ export function claudeWakeRoute(options: {
|
|
|
400
402
|
}
|
|
401
403
|
}
|
|
402
404
|
|
|
405
|
+
function cancelledDelivery(): DeliveryOutcome {
|
|
406
|
+
return {
|
|
407
|
+
notes: ['the Agent Session ended before answer delivery; stopping this observer'],
|
|
408
|
+
log: { route: 'hold-for-next-turn', stage: 'queued', reason: 'session-ended' },
|
|
409
|
+
acknowledgement: 'held',
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
403
413
|
function runClaude(args: string[], options: { cwd?: string; env?: NodeJS.ProcessEnv } = {}): Promise<string> {
|
|
404
414
|
return new Promise((resolve, reject) => {
|
|
405
415
|
const child = spawn('claude', args, {
|
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',
|
package/src/codex-wake.test.ts
CHANGED
package/src/codex-wake.ts
CHANGED
|
@@ -156,6 +156,7 @@ export function codexWakeRoute(options: {
|
|
|
156
156
|
const raced = holdForNextTurn(confirmed)
|
|
157
157
|
if (raced !== null) return raced
|
|
158
158
|
|
|
159
|
+
if (!event.commitDelivery()) return cancelledDelivery()
|
|
159
160
|
await adapters.resume(options.threadId, options.cwd, event.context)
|
|
160
161
|
return {
|
|
161
162
|
notes: ['cold-resumed the stopped Codex thread with its accepted answer'],
|
|
@@ -168,6 +169,14 @@ export function codexWakeRoute(options: {
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
function cancelledDelivery(): DeliveryOutcome {
|
|
173
|
+
return {
|
|
174
|
+
notes: ['the Agent Session ended before answer delivery; stopping this observer'],
|
|
175
|
+
log: { route: 'hold-for-next-turn', stage: 'queued', reason: 'session-ended' },
|
|
176
|
+
acknowledgement: 'held',
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
171
180
|
/** The journal, with the exact reason the thread could not be resumed. */
|
|
172
181
|
function holdForNextTurn(observation: CodexWakeObservation): DeliveryOutcome | null {
|
|
173
182
|
if (observation.state === 'stopped') return null
|
|
@@ -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-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
|
@@ -82,7 +82,7 @@ export interface CommandDeps {
|
|
|
82
82
|
codexSourcePid?: number
|
|
83
83
|
/** Test seam and production adapter for the external native skills installer. */
|
|
84
84
|
nativeSkills?: NativeSkills
|
|
85
|
-
/** Test seam for Orca's exact-worktree
|
|
85
|
+
/** Test seam for Orca's exact-worktree task/session title lookup. */
|
|
86
86
|
orcaSessionTitle?: OrcaSessionTitleLookup
|
|
87
87
|
/** Test seam for registry reads; production uses fetch. */
|
|
88
88
|
fetchImpl?: typeof fetch
|
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,
|
|
@@ -13,8 +18,8 @@ import {
|
|
|
13
18
|
readSessionState,
|
|
14
19
|
} from './hooks.js'
|
|
15
20
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
NON_ROUTING_BLOCKING_STOP_TIMEOUT_SECONDS,
|
|
22
|
+
QUESTION_STOP_TIMEOUT_SECONDS,
|
|
18
23
|
codexCoexistenceNotes,
|
|
19
24
|
codexHomeNote,
|
|
20
25
|
codexLayerDir,
|
|
@@ -52,7 +57,6 @@ import {
|
|
|
52
57
|
} from './commands-core.js'
|
|
53
58
|
import { deviceInstallRemedy, readyCompanionDevices, supportPageUrl } from './commands-devices.js'
|
|
54
59
|
import {
|
|
55
|
-
activeHarnessSession,
|
|
56
60
|
claudeSessionPid,
|
|
57
61
|
resolveActiveHarness,
|
|
58
62
|
type ActiveHarnessSession,
|
|
@@ -80,6 +84,7 @@ import {
|
|
|
80
84
|
setupProofApplies,
|
|
81
85
|
} from './commands-setup-proof.js'
|
|
82
86
|
import { skillReadiness } from './commands-skill.js'
|
|
87
|
+
import { projectBinding, projectEnabled } from './project-enablement.js'
|
|
83
88
|
|
|
84
89
|
// ---------------------------------------------------------------------------
|
|
85
90
|
// doctor
|
|
@@ -229,6 +234,37 @@ function projectReadiness(deps: CommandDeps, config: CliConfig): ReadinessState
|
|
|
229
234
|
}
|
|
230
235
|
}
|
|
231
236
|
|
|
237
|
+
function projectEnablementReadiness(deps: CommandDeps, config: CliConfig): ReadinessState {
|
|
238
|
+
const binding = projectBinding(deps.cwd, deps.env, config.project.value)
|
|
239
|
+
if (binding === null) {
|
|
240
|
+
return {
|
|
241
|
+
id: 'project-enablement',
|
|
242
|
+
title: 'Project enablement',
|
|
243
|
+
status: 'optional-gap',
|
|
244
|
+
detail: 'no Project is available here; lifecycle hooks stay silent',
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (projectEnabled(binding)) {
|
|
248
|
+
return {
|
|
249
|
+
id: 'project-enablement',
|
|
250
|
+
title: 'Project enablement',
|
|
251
|
+
status: 'ready',
|
|
252
|
+
detail: `enabled by the User for Project "${binding.project}"`,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
id: 'project-enablement',
|
|
257
|
+
title: 'Project enablement',
|
|
258
|
+
status: 'optional-gap',
|
|
259
|
+
detail: `disabled for Project "${binding.project}"; installed lifecycle hooks will emit no model guidance`,
|
|
260
|
+
remedy: {
|
|
261
|
+
by: 'cli',
|
|
262
|
+
summary: 'enable Notifai lifecycle behavior for this Project',
|
|
263
|
+
command: 'notifai project enable',
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
232
268
|
function remoteStatesFrom(previous: Readiness): {
|
|
233
269
|
credential: ReadinessState
|
|
234
270
|
server: ReadinessState
|
|
@@ -303,6 +339,7 @@ export async function assessReadiness(
|
|
|
303
339
|
return {
|
|
304
340
|
states: [
|
|
305
341
|
cliBinReadiness(deps.env, deps.hookPlatform ?? process.platform),
|
|
342
|
+
projectEnablementReadiness(deps, config),
|
|
306
343
|
projectReadiness(deps, config),
|
|
307
344
|
reused.credential,
|
|
308
345
|
reused.server,
|
|
@@ -322,6 +359,7 @@ export async function assessReadiness(
|
|
|
322
359
|
let accountDevices: RoutableDevice[] | null = null
|
|
323
360
|
|
|
324
361
|
states.push(cliBinReadiness(deps.env, deps.hookPlatform ?? process.platform))
|
|
362
|
+
states.push(projectEnablementReadiness(deps, config))
|
|
325
363
|
states.push(projectReadiness(deps, config))
|
|
326
364
|
|
|
327
365
|
states.push(
|
|
@@ -727,7 +765,11 @@ export function remedyLine(state: ReadinessState): string {
|
|
|
727
765
|
*/
|
|
728
766
|
function hookStates(deps: CommandDeps): ReadinessState[] {
|
|
729
767
|
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
|
|
730
|
-
const active
|
|
768
|
+
const { active, contested } = resolveActiveHarness(
|
|
769
|
+
deps.env,
|
|
770
|
+
deps.cwd,
|
|
771
|
+
(deps.now ?? Date.now)(),
|
|
772
|
+
)
|
|
731
773
|
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
732
774
|
const settings: ReadinessState = {
|
|
733
775
|
id: 'question-routing-settings',
|
|
@@ -748,6 +790,28 @@ function hookStates(deps: CommandDeps): ReadinessState[] {
|
|
|
748
790
|
}),
|
|
749
791
|
}
|
|
750
792
|
if (installations.length === 0) {
|
|
793
|
+
if (contested.length > 1) {
|
|
794
|
+
return [
|
|
795
|
+
{
|
|
796
|
+
id: 'hooks',
|
|
797
|
+
title: 'Question routing',
|
|
798
|
+
status: 'optional-gap',
|
|
799
|
+
detail: `Several harness sessions could own this shell (${contested.map((candidate) => candidate.label).join(', ')}); routing readiness is intentionally not attributed to either one`,
|
|
800
|
+
},
|
|
801
|
+
settings,
|
|
802
|
+
]
|
|
803
|
+
}
|
|
804
|
+
if (active !== null && !isHookInstallableHarness(active.harness)) {
|
|
805
|
+
return [
|
|
806
|
+
{
|
|
807
|
+
id: 'hooks',
|
|
808
|
+
title: 'Question routing',
|
|
809
|
+
status: 'optional-gap',
|
|
810
|
+
detail: `${active.label}: ${HERMES_QUESTION_ROUTING_UNAVAILABLE.deliveryContract}`,
|
|
811
|
+
},
|
|
812
|
+
settings,
|
|
813
|
+
]
|
|
814
|
+
}
|
|
751
815
|
return [
|
|
752
816
|
{
|
|
753
817
|
id: 'hooks',
|
|
@@ -838,7 +902,11 @@ interface HookCheck {
|
|
|
838
902
|
/** Real but not in the way: worth a line, never a blocker. */
|
|
839
903
|
informational?: boolean
|
|
840
904
|
/** A remedy truer than the generic `notifai hooks install`. */
|
|
841
|
-
remedy?: {
|
|
905
|
+
remedy?: {
|
|
906
|
+
summary: string
|
|
907
|
+
command: string
|
|
908
|
+
user_action?: { code: string; harness: string; action: string; message: string }
|
|
909
|
+
}
|
|
842
910
|
}
|
|
843
911
|
|
|
844
912
|
function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
@@ -887,7 +955,21 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
887
955
|
active === null
|
|
888
956
|
? []
|
|
889
957
|
: installations.filter((installation) => installation.harness === active.harness)
|
|
890
|
-
if (
|
|
958
|
+
if (contested.length > 1) {
|
|
959
|
+
checks.push({
|
|
960
|
+
name: 'hooks (active harness)',
|
|
961
|
+
ok: false,
|
|
962
|
+
informational: true,
|
|
963
|
+
detail: `Several harness sessions could own this shell (${contested.map((candidate) => candidate.label).join(', ')}); routing readiness is intentionally not attributed to either one`,
|
|
964
|
+
})
|
|
965
|
+
} else if (active !== null && !isHookInstallableHarness(active.harness)) {
|
|
966
|
+
checks.push({
|
|
967
|
+
name: 'hooks (active harness)',
|
|
968
|
+
ok: true,
|
|
969
|
+
informational: true,
|
|
970
|
+
detail: `${active.label}: ${HERMES_QUESTION_ROUTING_UNAVAILABLE.deliveryContract}`,
|
|
971
|
+
})
|
|
972
|
+
} else if (active !== null) {
|
|
891
973
|
checks.push({
|
|
892
974
|
name: 'hooks (active harness)',
|
|
893
975
|
ok: activeInstallations.length > 0,
|
|
@@ -995,6 +1077,12 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
995
1077
|
remedy: {
|
|
996
1078
|
summary: 'open `/hooks` in Codex and approve the changed Notifai handlers',
|
|
997
1079
|
command: '/hooks',
|
|
1080
|
+
user_action: {
|
|
1081
|
+
code: 'codex_hook_approval_required',
|
|
1082
|
+
harness: 'codex',
|
|
1083
|
+
action: 'approve_or_enable_notifai_hooks',
|
|
1084
|
+
message: 'Open `/hooks` in Codex, approve or enable the Notifai handlers, then tell me when it is done. I will finish setup and verify a fresh session.',
|
|
1085
|
+
},
|
|
998
1086
|
},
|
|
999
1087
|
}),
|
|
1000
1088
|
})
|
|
@@ -1010,7 +1098,7 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
|
|
|
1010
1098
|
ok: shapeProblems.length === 0,
|
|
1011
1099
|
detail:
|
|
1012
1100
|
shapeProblems.length === 0
|
|
1013
|
-
? `every installed Stop handler declares the shape its harness needs: Claude Code async with an explicit ${
|
|
1101
|
+
? `every installed Stop handler declares the shape its harness needs: Claude Code async and Codex blocking with an explicit ${QUESTION_STOP_TIMEOUT_SECONDS}s full-window budget; non-routing blocking hosts ${NON_ROUTING_BLOCKING_STOP_TIMEOUT_SECONDS}s`
|
|
1014
1102
|
: shapeProblems.join('; '),
|
|
1015
1103
|
})
|
|
1016
1104
|
|
|
@@ -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. */
|
|
@@ -53,6 +64,16 @@ function harnessEnvCandidates(env: NodeJS.ProcessEnv): ActiveHarnessSession[] {
|
|
|
53
64
|
candidates.push({ harness: 'codex', label: 'Codex', sessionId: codexSession })
|
|
54
65
|
}
|
|
55
66
|
if ((env['CURSOR_AGENT'] ?? '') !== '') candidates.push({ harness: 'cursor', label: 'Cursor' })
|
|
67
|
+
const hermesSession = env['HERMES_SESSION_ID']
|
|
68
|
+
if (hermesSession !== undefined && hermesSession !== '') {
|
|
69
|
+
const integrationInstance = hermesClassicCliLocalInstance(env)
|
|
70
|
+
candidates.push({
|
|
71
|
+
harness: 'hermes',
|
|
72
|
+
label: 'Hermes',
|
|
73
|
+
sessionId: hermesSession,
|
|
74
|
+
...(integrationInstance === null ? {} : { integrationInstance }),
|
|
75
|
+
})
|
|
76
|
+
}
|
|
56
77
|
return candidates
|
|
57
78
|
}
|
|
58
79
|
|
|
@@ -97,6 +118,12 @@ export function resolveActiveHarness(
|
|
|
97
118
|
if (first === undefined) return { active: null, contested: [] }
|
|
98
119
|
if (candidates.length === 1) return { active: first, contested: [] }
|
|
99
120
|
|
|
121
|
+
// A send-only harness has no hook evidence, so live pointer/state of a
|
|
122
|
+
// hook-installable parent cannot prove which nested process owns this
|
|
123
|
+
// shell. Mixing Hermes with another marker therefore stays contested.
|
|
124
|
+
const mixedWithSendOnly = candidates.some((candidate) => !isHookInstallableHarness(candidate.harness))
|
|
125
|
+
if (mixedWithSendOnly) return { active: first, contested: candidates }
|
|
126
|
+
|
|
100
127
|
// Exact lifecycle state is machine-global and survives a command moving to
|
|
101
128
|
// another linked checkout. Prefer it before the checkout-local
|
|
102
129
|
// pointer. A unique newest hook event proves which nested harness owns this
|
|
@@ -133,6 +160,25 @@ export function activeHarnessSession(
|
|
|
133
160
|
return resolveActiveHarness(env, cwd, now).active
|
|
134
161
|
}
|
|
135
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Source Context attribution. Hook-installable nesting still uses declared
|
|
165
|
+
* order when nothing has fired — that existing send behavior is preserved.
|
|
166
|
+
* A contested mix that includes a send-only harness (Hermes) fails closed:
|
|
167
|
+
* the wrong Agent Session would otherwise be named as the owner.
|
|
168
|
+
*/
|
|
169
|
+
export function sourceContextHarnessSession(
|
|
170
|
+
env: NodeJS.ProcessEnv,
|
|
171
|
+
cwd: string,
|
|
172
|
+
now: number,
|
|
173
|
+
): ActiveHarnessSession | null {
|
|
174
|
+
const { active, contested } = resolveActiveHarness(env, cwd, now)
|
|
175
|
+
if (contested.some((candidate) => !isHookInstallableHarness(candidate.harness))) {
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
if (active?.harness === 'hermes' && active.integrationInstance === undefined) return null
|
|
179
|
+
return active
|
|
180
|
+
}
|
|
181
|
+
|
|
136
182
|
/**
|
|
137
183
|
* The pid of the Claude Code session this hook belongs to.
|
|
138
184
|
*
|