@raidiant/notifai 5.0.0 → 7.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 +51 -0
- package/README.md +2 -2
- package/dist/atomic-file.js +13 -0
- package/dist/atomic-file.js.map +1 -1
- package/dist/client.d.ts +8 -3
- package/dist/client.js +24 -3
- package/dist/client.js.map +1 -1
- package/dist/commands-auth.d.ts +14 -0
- package/dist/commands-auth.js +154 -0
- package/dist/commands-auth.js.map +1 -0
- package/dist/commands-config.d.ts +31 -0
- package/dist/commands-config.js +246 -0
- package/dist/commands-config.js.map +1 -0
- package/dist/commands-core.d.ts +102 -0
- package/dist/commands-core.js +135 -0
- package/dist/commands-core.js.map +1 -0
- package/dist/commands-devices.d.ts +31 -0
- package/dist/commands-devices.js +124 -0
- package/dist/commands-devices.js.map +1 -0
- package/dist/commands-doctor.d.ts +24 -0
- package/dist/commands-doctor.js +1011 -0
- package/dist/commands-doctor.js.map +1 -0
- package/dist/commands-harness-context.d.ts +65 -0
- package/dist/commands-harness-context.js +97 -0
- package/dist/commands-harness-context.js.map +1 -0
- package/dist/commands-hook-shape.d.ts +17 -0
- package/dist/commands-hook-shape.js +37 -0
- package/dist/commands-hook-shape.js.map +1 -0
- package/dist/commands-hooks.d.ts +115 -0
- package/dist/commands-hooks.js +1225 -0
- package/dist/commands-hooks.js.map +1 -0
- package/dist/commands-init.d.ts +35 -0
- package/dist/commands-init.js +580 -0
- package/dist/commands-init.js.map +1 -0
- package/dist/commands-io.d.ts +2 -0
- package/dist/commands-io.js +110 -0
- package/dist/commands-io.js.map +1 -0
- package/dist/commands-logs.d.ts +30 -0
- package/dist/commands-logs.js +201 -0
- package/dist/commands-logs.js.map +1 -0
- package/dist/commands-send-support.d.ts +44 -0
- package/dist/commands-send-support.js +183 -0
- package/dist/commands-send-support.js.map +1 -0
- package/dist/commands-send.d.ts +33 -0
- package/dist/commands-send.js +519 -0
- package/dist/commands-send.js.map +1 -0
- package/dist/commands-setup-proof.d.ts +15 -0
- package/dist/commands-setup-proof.js +53 -0
- package/dist/commands-setup-proof.js.map +1 -0
- package/dist/commands-skill.d.ts +10 -0
- package/dist/commands-skill.js +71 -0
- package/dist/commands-skill.js.map +1 -0
- package/dist/commands.d.ts +12 -387
- package/dist/commands.js +12 -4536
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.js +1 -1
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/config.js +6 -4
- package/dist/config.js.map +1 -1
- package/dist/credentials.js +13 -3
- package/dist/credentials.js.map +1 -1
- package/dist/generated-session-label.d.ts +2 -0
- package/dist/generated-session-label.js +152 -0
- package/dist/generated-session-label.js.map +1 -0
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +127 -99
- package/dist/hooks.js.map +1 -1
- package/dist/interactive.js +4 -4
- package/dist/interactive.js.map +1 -1
- package/dist/invocation-context.d.ts +3 -0
- package/dist/invocation-context.js +23 -6
- package/dist/invocation-context.js.map +1 -1
- package/dist/logging.js +1 -0
- package/dist/logging.js.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/opencode-plugin.js +18 -3
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/readiness.d.ts +3 -1
- package/dist/readiness.js.map +1 -1
- package/dist/session-labels.d.ts +30 -0
- package/dist/session-labels.js +278 -0
- package/dist/session-labels.js.map +1 -0
- package/package.json +2 -2
- package/src/atomic-file.ts +12 -0
- package/src/client.test.ts +90 -1
- package/src/client.ts +35 -2
- package/src/commands-auth.ts +174 -0
- package/src/commands-config.ts +307 -0
- package/src/commands-core.ts +240 -0
- package/src/commands-devices.ts +150 -0
- package/src/commands-doctor.ts +1209 -0
- package/src/commands-harness-context.ts +134 -0
- package/src/commands-hook-shape.ts +53 -0
- package/src/commands-hooks.ts +1586 -0
- package/src/commands-init.ts +721 -0
- package/src/commands-io.ts +108 -0
- package/src/commands-logs.ts +243 -0
- package/src/commands-send-support.ts +227 -0
- package/src/commands-send.ts +623 -0
- package/src/commands-setup-proof.ts +71 -0
- package/src/commands-skill.ts +84 -0
- package/src/commands.test.ts +567 -42
- package/src/commands.ts +58 -5614
- package/src/config-schema.ts +1 -1
- package/src/config.test.ts +9 -0
- package/src/config.ts +10 -5
- package/src/credentials.test.ts +25 -1
- package/src/credentials.ts +13 -3
- package/src/generated-session-label.ts +155 -0
- package/src/hooks.test.ts +355 -16
- package/src/hooks.ts +145 -97
- package/src/install-hooks.test.ts +84 -1
- package/src/interactive.test.ts +19 -1
- package/src/interactive.ts +4 -4
- package/src/invocation-context.test.ts +115 -16
- package/src/invocation-context.ts +32 -11
- package/src/logging.test.ts +15 -2
- package/src/logging.ts +1 -0
- package/src/main.ts +9 -3
- package/src/opencode-plugin.ts +18 -3
- package/src/readiness.ts +3 -1
- package/src/session-labels.test.ts +303 -0
- package/src/session-labels.ts +360 -0
- package/src/skill.test.ts +13 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { mkdtempSync } from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
1
4
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { sessionLabelFromId } from '@raidiant/notifai-protocol'
|
|
3
5
|
import {
|
|
4
6
|
buildSourceContext,
|
|
5
7
|
inferInvocationContext,
|
|
@@ -13,6 +15,13 @@ function fixtureGit(values: Record<string, string | null>): GitCommand {
|
|
|
13
15
|
return (_cwd, args) => values[args.join(' ')] ?? null
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
function stateEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
|
|
19
|
+
return {
|
|
20
|
+
XDG_STATE_HOME: mkdtempSync(path.join(os.tmpdir(), 'notifai-source-context-')),
|
|
21
|
+
...overrides,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
describe('projectSlugFrom', () => {
|
|
17
26
|
it.each([
|
|
18
27
|
['My Project', 'my-project'],
|
|
@@ -155,17 +164,19 @@ describe('truncateContext', () => {
|
|
|
155
164
|
|
|
156
165
|
describe('buildSourceContext', () => {
|
|
157
166
|
const invocation = { project: 'repo', branch: 'main', worktree: 'topic' }
|
|
167
|
+
const now = new Date(2026, 7, 20, 14, 5).getTime()
|
|
158
168
|
|
|
159
169
|
it('applies flag, environment, then inferred precedence per session field', () => {
|
|
160
170
|
const built = buildSourceContext({
|
|
161
|
-
env: {
|
|
171
|
+
env: stateEnv({
|
|
162
172
|
NOTIFAI_SESSION_ID: 'env-id',
|
|
163
173
|
NOTIFAI_SESSION_LABEL: 'Environment Label',
|
|
164
|
-
},
|
|
174
|
+
}),
|
|
165
175
|
invocation,
|
|
166
176
|
sessionId: 'flag-id',
|
|
167
177
|
sessionLabel: 'Flag Label',
|
|
168
|
-
activeHarness: { harness: 'claude-code', sessionId: '
|
|
178
|
+
activeHarness: { harness: 'claude-code', sessionId: 'flag-id' },
|
|
179
|
+
now,
|
|
169
180
|
})
|
|
170
181
|
expect(built).toEqual({
|
|
171
182
|
ok: true,
|
|
@@ -182,12 +193,13 @@ describe('buildSourceContext', () => {
|
|
|
182
193
|
it('uses environment values ahead of an inferred exact session', () => {
|
|
183
194
|
expect(
|
|
184
195
|
buildSourceContext({
|
|
185
|
-
env: {
|
|
196
|
+
env: stateEnv({
|
|
186
197
|
NOTIFAI_SESSION_ID: 'env-id',
|
|
187
198
|
NOTIFAI_SESSION_LABEL: 'Environment Label',
|
|
188
|
-
},
|
|
199
|
+
}),
|
|
189
200
|
invocation: { project: 'repo' },
|
|
190
|
-
activeHarness: { harness: 'codex', sessionId: '
|
|
201
|
+
activeHarness: { harness: 'codex', sessionId: 'env-id' },
|
|
202
|
+
now,
|
|
191
203
|
}),
|
|
192
204
|
).toEqual({
|
|
193
205
|
ok: true,
|
|
@@ -199,32 +211,117 @@ describe('buildSourceContext', () => {
|
|
|
199
211
|
})
|
|
200
212
|
})
|
|
201
213
|
|
|
202
|
-
it('
|
|
203
|
-
const
|
|
214
|
+
it('uses and freezes a trusted harness-native title', () => {
|
|
215
|
+
const env = stateEnv()
|
|
216
|
+
const sessionId = 'opencode-thread'
|
|
204
217
|
expect(
|
|
205
218
|
buildSourceContext({
|
|
206
|
-
env
|
|
219
|
+
env,
|
|
207
220
|
invocation: { project: 'repo', branch: 'main' },
|
|
208
|
-
activeHarness: {
|
|
221
|
+
activeHarness: {
|
|
222
|
+
harness: 'opencode',
|
|
223
|
+
sessionId,
|
|
224
|
+
sessionLabel: 'Semantic session names',
|
|
225
|
+
},
|
|
226
|
+
now,
|
|
209
227
|
}),
|
|
210
228
|
).toEqual({
|
|
211
229
|
ok: true,
|
|
212
230
|
source: {
|
|
213
231
|
session_id: sessionId,
|
|
214
|
-
session_label:
|
|
232
|
+
session_label: 'Semantic session names',
|
|
215
233
|
harness: 'opencode',
|
|
216
234
|
branch: 'main',
|
|
217
235
|
},
|
|
218
236
|
})
|
|
219
|
-
expect(
|
|
237
|
+
expect(
|
|
238
|
+
buildSourceContext({
|
|
239
|
+
env,
|
|
240
|
+
invocation: { project: 'repo' },
|
|
241
|
+
activeHarness: { harness: 'opencode', sessionId, sessionLabel: 'Changed title' },
|
|
242
|
+
now: now + 1_000,
|
|
243
|
+
}),
|
|
244
|
+
).toEqual({
|
|
245
|
+
ok: true,
|
|
246
|
+
source: {
|
|
247
|
+
session_id: sessionId,
|
|
248
|
+
session_label: 'Semantic session names',
|
|
249
|
+
harness: 'opencode',
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('does not freeze an OpenCode placeholder while its semantic title is pending', () => {
|
|
255
|
+
expect(
|
|
256
|
+
buildSourceContext({
|
|
257
|
+
env: stateEnv(),
|
|
258
|
+
invocation: { project: 'repo' },
|
|
259
|
+
activeHarness: {
|
|
260
|
+
harness: 'opencode',
|
|
261
|
+
sessionId: 'pending-session',
|
|
262
|
+
sessionLabelPending: true,
|
|
263
|
+
},
|
|
264
|
+
now,
|
|
265
|
+
}),
|
|
266
|
+
).toEqual({
|
|
267
|
+
ok: false,
|
|
268
|
+
error:
|
|
269
|
+
"OpenCode is still generating this session's title; retry shortly or pass --session-label.",
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('uses a stable generated fallback when no semantic name exists', () => {
|
|
274
|
+
const env = stateEnv()
|
|
275
|
+
const sessionId = 'opaque-thread-1234567890'
|
|
276
|
+
const expected = {
|
|
277
|
+
ok: true,
|
|
278
|
+
source: {
|
|
279
|
+
session_id: sessionId,
|
|
280
|
+
session_label: 'Ivory Koala',
|
|
281
|
+
harness: 'opencode' as const,
|
|
282
|
+
branch: 'main',
|
|
283
|
+
},
|
|
284
|
+
}
|
|
285
|
+
expect(
|
|
286
|
+
buildSourceContext({
|
|
287
|
+
env,
|
|
288
|
+
invocation: { project: 'repo', branch: 'main' },
|
|
289
|
+
activeHarness: { harness: 'opencode', sessionId },
|
|
290
|
+
now,
|
|
291
|
+
}),
|
|
292
|
+
).toEqual(expected)
|
|
293
|
+
expect(expected.source.session_label).not.toContain('1234567890')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('does not borrow harness identity or title for an explicit different session id', () => {
|
|
297
|
+
expect(
|
|
298
|
+
buildSourceContext({
|
|
299
|
+
env: stateEnv(),
|
|
300
|
+
invocation: { project: 'repo' },
|
|
301
|
+
sessionId: 'other-session',
|
|
302
|
+
activeHarness: {
|
|
303
|
+
harness: 'claude-code',
|
|
304
|
+
sessionId: 'current-session',
|
|
305
|
+
sessionLabel: 'Current work',
|
|
306
|
+
},
|
|
307
|
+
now,
|
|
308
|
+
}),
|
|
309
|
+
).toEqual({
|
|
310
|
+
ok: true,
|
|
311
|
+
source: {
|
|
312
|
+
session_id: 'other-session',
|
|
313
|
+
session_label: 'Golden Lynx',
|
|
314
|
+
},
|
|
315
|
+
})
|
|
220
316
|
})
|
|
221
317
|
|
|
222
318
|
it('does not emit a harness or label when no exact session id is available', () => {
|
|
223
319
|
expect(
|
|
224
320
|
buildSourceContext({
|
|
225
|
-
env:
|
|
321
|
+
env: stateEnv(),
|
|
226
322
|
invocation: { project: 'repo', branch: 'main' },
|
|
227
323
|
activeHarness: { harness: 'cursor' },
|
|
324
|
+
now,
|
|
228
325
|
}),
|
|
229
326
|
).toEqual({ ok: true, source: { branch: 'main' } })
|
|
230
327
|
})
|
|
@@ -232,8 +329,9 @@ describe('buildSourceContext', () => {
|
|
|
232
329
|
it('ignores the deleted NOTIFAI_SESSION variable', () => {
|
|
233
330
|
expect(
|
|
234
331
|
buildSourceContext({
|
|
235
|
-
env: { NOTIFAI_SESSION: 'legacy-id' },
|
|
332
|
+
env: stateEnv({ NOTIFAI_SESSION: 'legacy-id' }),
|
|
236
333
|
invocation: { project: 'repo' },
|
|
334
|
+
now,
|
|
237
335
|
}),
|
|
238
336
|
).toEqual({ ok: true })
|
|
239
337
|
})
|
|
@@ -241,9 +339,10 @@ describe('buildSourceContext', () => {
|
|
|
241
339
|
it('rejects a display label without an exact session id', () => {
|
|
242
340
|
expect(
|
|
243
341
|
buildSourceContext({
|
|
244
|
-
env:
|
|
342
|
+
env: stateEnv(),
|
|
245
343
|
invocation: { project: 'repo' },
|
|
246
344
|
sessionLabel: 'Unbound Label',
|
|
345
|
+
now,
|
|
247
346
|
}),
|
|
248
347
|
).toEqual({
|
|
249
348
|
ok: false,
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
sessionLabelFromId,
|
|
5
|
-
type SourceContextT,
|
|
6
|
-
} from '@raidiant/notifai-protocol'
|
|
3
|
+
import { type SourceContextT } from '@raidiant/notifai-protocol'
|
|
7
4
|
import type { Harness } from './harnesses.js'
|
|
5
|
+
import { resolveSessionLabel } from './session-labels.js'
|
|
8
6
|
|
|
9
7
|
export type GitCommand = (cwd: string, args: readonly string[]) => string | null
|
|
10
8
|
|
|
@@ -139,7 +137,13 @@ export interface SourceContextInput {
|
|
|
139
137
|
invocation: InvocationContext
|
|
140
138
|
sessionId?: string
|
|
141
139
|
sessionLabel?: string
|
|
142
|
-
activeHarness?: {
|
|
140
|
+
activeHarness?: {
|
|
141
|
+
harness: Harness
|
|
142
|
+
sessionId?: string
|
|
143
|
+
sessionLabel?: string
|
|
144
|
+
sessionLabelPending?: boolean
|
|
145
|
+
}
|
|
146
|
+
now?: number
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
export type SourceContextBuild =
|
|
@@ -161,14 +165,31 @@ export function buildSourceContext(input: SourceContextInput): SourceContextBuil
|
|
|
161
165
|
return { ok: false, error: '--session-id must not be empty.' }
|
|
162
166
|
}
|
|
163
167
|
|
|
164
|
-
const
|
|
165
|
-
sessionId
|
|
168
|
+
const activeOwnsSession =
|
|
169
|
+
sessionId !== undefined && input.activeHarness?.sessionId === sessionId
|
|
170
|
+
const harness = activeOwnsSession ? input.activeHarness?.harness : undefined
|
|
171
|
+
const label =
|
|
172
|
+
sessionId === undefined
|
|
173
|
+
? undefined
|
|
174
|
+
: resolveSessionLabel({
|
|
175
|
+
env: input.env,
|
|
176
|
+
sessionId,
|
|
177
|
+
...(harness === undefined ? {} : { harness }),
|
|
178
|
+
...(explicitLabel === undefined ? {} : { explicitLabel }),
|
|
179
|
+
...(activeOwnsSession && input.activeHarness?.sessionLabel !== undefined
|
|
180
|
+
? { harnessLabel: input.activeHarness.sessionLabel }
|
|
181
|
+
: {}),
|
|
182
|
+
...(activeOwnsSession && input.activeHarness?.sessionLabelPending === true
|
|
183
|
+
? { harnessLabelPending: true }
|
|
184
|
+
: {}),
|
|
185
|
+
...(input.now === undefined ? {} : { now: input.now }),
|
|
186
|
+
})
|
|
187
|
+
if (label !== undefined && !label.ok) return label
|
|
188
|
+
|
|
166
189
|
const source: SourceContextT = {
|
|
167
190
|
...(sessionId !== undefined ? { session_id: sessionId } : {}),
|
|
168
|
-
...(
|
|
169
|
-
...(
|
|
170
|
-
? { harness: input.activeHarness.harness }
|
|
171
|
-
: {}),
|
|
191
|
+
...(label !== undefined ? { session_label: label.label } : {}),
|
|
192
|
+
...(harness !== undefined ? { harness } : {}),
|
|
172
193
|
...(input.invocation.branch !== undefined ? { branch: input.invocation.branch } : {}),
|
|
173
194
|
...(input.invocation.worktree !== undefined ? { worktree: input.invocation.worktree } : {}),
|
|
174
195
|
}
|
package/src/logging.test.ts
CHANGED
|
@@ -418,14 +418,27 @@ describe('reading', () => {
|
|
|
418
418
|
expect(records.length).toBeGreaterThan(lines(env).filter((r) => r.event === 'cli.end').length)
|
|
419
419
|
})
|
|
420
420
|
|
|
421
|
-
it('skips
|
|
421
|
+
it('skips damaged and unknown-future lines without throwing away the file', () => {
|
|
422
422
|
const env = sandbox()
|
|
423
423
|
seed(env)
|
|
424
424
|
// A half-written line is what a crashed process leaves behind, and a
|
|
425
425
|
// reader that dies on it loses every good record around it.
|
|
426
426
|
appendFileSync(activeLogPath(env), '{"v":1,"ts":"2026-0\n')
|
|
427
427
|
appendFileSync(activeLogPath(env), 'not json at all\n')
|
|
428
|
-
|
|
428
|
+
appendFileSync(
|
|
429
|
+
activeLogPath(env),
|
|
430
|
+
`${JSON.stringify({
|
|
431
|
+
v: LOG_SCHEMA_VERSION + 1,
|
|
432
|
+
ts: '2026-08-18T12:00:00.000Z',
|
|
433
|
+
level: 'info',
|
|
434
|
+
event: 'future.event',
|
|
435
|
+
})}\n`,
|
|
436
|
+
)
|
|
437
|
+
expect(readLogRecords(env).records.map((record) => record.event)).toEqual([
|
|
438
|
+
'send.submitted',
|
|
439
|
+
'cli.error',
|
|
440
|
+
'ask.registered',
|
|
441
|
+
])
|
|
429
442
|
})
|
|
430
443
|
|
|
431
444
|
it('reports nothing at all rather than failing when there is no log', () => {
|
package/src/logging.ts
CHANGED
|
@@ -534,6 +534,7 @@ function isRecord(parsed: unknown): parsed is LogRecord {
|
|
|
534
534
|
return (
|
|
535
535
|
typeof parsed === 'object' &&
|
|
536
536
|
parsed !== null &&
|
|
537
|
+
(parsed as { v?: unknown }).v === LOG_SCHEMA_VERSION &&
|
|
537
538
|
typeof (parsed as { event?: unknown }).event === 'string' &&
|
|
538
539
|
typeof (parsed as { ts?: unknown }).ts === 'string'
|
|
539
540
|
)
|
package/src/main.ts
CHANGED
|
@@ -313,13 +313,16 @@ const send = program
|
|
|
313
313
|
.option('--device <id>', 'target a device id (repeatable)', (v: string, all: string[] = []) => [...all, v])
|
|
314
314
|
.option('--all', 'target all routable devices (overrides configured devices)')
|
|
315
315
|
.option('--session-id <id>', 'opaque exact-session override (env: NOTIFAI_SESSION_ID)')
|
|
316
|
-
.option(
|
|
316
|
+
.option(
|
|
317
|
+
'--session-label <text>',
|
|
318
|
+
'first human session name, frozen locally (env: NOTIFAI_SESSION_LABEL)',
|
|
319
|
+
)
|
|
317
320
|
.option('--event <event>', 'agent event name, e.g. tests_passed')
|
|
318
321
|
.optionsGroup(SEND_GROUP.presentation)
|
|
319
322
|
.option('--collapse-key <key>', 'replace earlier notifications with the same key')
|
|
320
323
|
.option('--thread-id <id>', 'group related notifications')
|
|
321
324
|
.option('--ttl <seconds>', 'delivery window in seconds', (v: string) => Number(v))
|
|
322
|
-
.option('--platform <platform>', 'limit optional fields to
|
|
325
|
+
.option('--platform <platform>', 'limit optional fields to one supported platform')
|
|
323
326
|
.optionsGroup(SEND_GROUP.reply)
|
|
324
327
|
.option('--reply', 'enable the inline reply action and block for the answer')
|
|
325
328
|
.option('--reply-timeout <seconds>', 'how long to wait for a reply (default: 900)', (v: string) => Number(v))
|
|
@@ -445,7 +448,10 @@ program
|
|
|
445
448
|
)
|
|
446
449
|
.option('--project <id>', 'project identifier override (otherwise configured or inferred)')
|
|
447
450
|
.option('--session-id <id>', 'opaque exact-session override (env: NOTIFAI_SESSION_ID)')
|
|
448
|
-
.option(
|
|
451
|
+
.option(
|
|
452
|
+
'--session-label <text>',
|
|
453
|
+
'first human session name, frozen locally (env: NOTIFAI_SESSION_LABEL)',
|
|
454
|
+
)
|
|
449
455
|
.action(async (question: string | undefined, opts: {
|
|
450
456
|
choice?: string[]
|
|
451
457
|
multi?: boolean
|
package/src/opencode-plugin.ts
CHANGED
|
@@ -31,7 +31,7 @@ export const OPENCODE_PLUGIN_MARKER = '// notifai managed opencode plugin'
|
|
|
31
31
|
export const OPENCODE_PLUGIN_FILENAME = 'notifai.js'
|
|
32
32
|
|
|
33
33
|
/** Bump when an installed generated file must be rewritten to remain functional. */
|
|
34
|
-
const OPENCODE_ADAPTER_VERSION =
|
|
34
|
+
const OPENCODE_ADAPTER_VERSION = 8
|
|
35
35
|
|
|
36
36
|
export function opencodePluginDir(
|
|
37
37
|
global: boolean,
|
|
@@ -103,6 +103,8 @@ import { spawn } from "node:child_process"
|
|
|
103
103
|
${nodeConstant}const ADAPTER = ${JSON.stringify(adapterPath)}
|
|
104
104
|
const TIMEOUT_MS = ${timeoutSeconds * 1000}
|
|
105
105
|
const ADAPTER_VERSION = ${OPENCODE_ADAPTER_VERSION}
|
|
106
|
+
const PENDING_SESSION_TITLE =
|
|
107
|
+
/^New session(?: *- *[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:[.][0-9]+)?Z)?$/i
|
|
106
108
|
|
|
107
109
|
/**
|
|
108
110
|
* Runs \`notifai hook <event>\`, feeding it the same JSON envelope the other
|
|
@@ -152,16 +154,29 @@ function runHook(event, envelope) {
|
|
|
152
154
|
})
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
export const NotifAIPlugin = async ({ directory }) => {
|
|
157
|
+
export const NotifAIPlugin = async ({ directory, client }) => {
|
|
156
158
|
const cwd = typeof directory === "string" && directory.length > 0 ? directory : process.cwd()
|
|
157
159
|
|
|
158
160
|
return {
|
|
159
|
-
/** Exact active-harness identity
|
|
161
|
+
/** Exact active-harness identity and first-party title for agent shell commands. */
|
|
160
162
|
"shell.env": async (input, output) => {
|
|
161
163
|
output.env.NOTIFAI_ACTIVE_HARNESS = "opencode"
|
|
162
164
|
const sessionID = input?.sessionID
|
|
163
165
|
if (typeof sessionID === "string" && sessionID.length > 0) {
|
|
164
166
|
output.env.NOTIFAI_ACTIVE_SESSION_ID = sessionID
|
|
167
|
+
try {
|
|
168
|
+
const response = await client.session.get({ path: { id: sessionID } })
|
|
169
|
+
const title = response?.data?.title
|
|
170
|
+
if (typeof title === "string" && title.trim().length > 0) {
|
|
171
|
+
if (PENDING_SESSION_TITLE.test(title.trim())) {
|
|
172
|
+
output.env.NOTIFAI_ACTIVE_SESSION_LABEL_PENDING = "1"
|
|
173
|
+
} else {
|
|
174
|
+
output.env.NOTIFAI_ACTIVE_SESSION_LABEL = title
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
// Identity remains useful when this OpenCode build cannot expose a title.
|
|
179
|
+
}
|
|
165
180
|
}
|
|
166
181
|
},
|
|
167
182
|
|
package/src/readiness.ts
CHANGED
|
@@ -57,8 +57,10 @@ export interface ReadinessState {
|
|
|
57
57
|
/** Human words for what this is, not the field name. */
|
|
58
58
|
title: string
|
|
59
59
|
status: StateStatus
|
|
60
|
-
/** The current condition, phrased
|
|
60
|
+
/** The current condition, phrased for the human renderer. */
|
|
61
61
|
detail: string
|
|
62
|
+
/** Structured diagnostics emitted only by --json / non-human surfaces. */
|
|
63
|
+
technical?: unknown
|
|
62
64
|
remedy?: Remedy
|
|
63
65
|
}
|
|
64
66
|
|