@raidiant/notifai 6.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 +24 -0
- package/dist/atomic-file.js +13 -0
- package/dist/atomic-file.js.map +1 -1
- package/dist/commands-harness-context.d.ts +4 -0
- package/dist/commands-harness-context.js +4 -0
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-send-support.js +3 -0
- package/dist/commands-send-support.js.map +1 -1
- package/dist/config.d.ts +3 -3
- package/dist/config.js +3 -3
- 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/invocation-context.d.ts +3 -0
- package/dist/invocation-context.js +23 -6
- package/dist/invocation-context.js.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/opencode-plugin.js +18 -3
- package/dist/opencode-plugin.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/commands-harness-context.ts +8 -0
- package/src/commands-send-support.ts +3 -0
- package/src/commands.test.ts +74 -3
- package/src/config.ts +3 -3
- package/src/generated-session-label.ts +155 -0
- package/src/hooks.test.ts +4 -5
- package/src/install-hooks.test.ts +84 -1
- package/src/invocation-context.test.ts +115 -16
- package/src/invocation-context.ts +32 -11
- package/src/main.ts +8 -2
- package/src/opencode-plugin.ts +18 -3
- package/src/session-labels.test.ts +303 -0
- package/src/session-labels.ts +360 -0
- package/src/skill.test.ts +13 -0
package/src/hooks.test.ts
CHANGED
|
@@ -15,7 +15,6 @@ import os from 'node:os'
|
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { PassThrough } from 'node:stream'
|
|
17
17
|
import {
|
|
18
|
-
sessionLabelFromId,
|
|
19
18
|
type ListRepliesResponse,
|
|
20
19
|
type ReplyView,
|
|
21
20
|
type SubmissionReceipt,
|
|
@@ -3139,7 +3138,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
3139
3138
|
question: 'Ship it?',
|
|
3140
3139
|
source: {
|
|
3141
3140
|
session_id: 'sess-abc',
|
|
3142
|
-
session_label:
|
|
3141
|
+
session_label: 'Semantic session',
|
|
3143
3142
|
harness: 'claude-code',
|
|
3144
3143
|
},
|
|
3145
3144
|
},
|
|
@@ -3150,7 +3149,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
3150
3149
|
|
|
3151
3150
|
expect(h.recorder.submitted[0]?.draft.source).toEqual({
|
|
3152
3151
|
session_id: 'sess-abc',
|
|
3153
|
-
session_label:
|
|
3152
|
+
session_label: 'Semantic session',
|
|
3154
3153
|
harness: 'claude-code',
|
|
3155
3154
|
})
|
|
3156
3155
|
})
|
|
@@ -3164,7 +3163,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
3164
3163
|
question: 'Ship it?',
|
|
3165
3164
|
source: {
|
|
3166
3165
|
session_id: 'sess-abc',
|
|
3167
|
-
session_label:
|
|
3166
|
+
session_label: 'Semantic session',
|
|
3168
3167
|
harness: 'claude-code',
|
|
3169
3168
|
},
|
|
3170
3169
|
},
|
|
@@ -3183,7 +3182,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
3183
3182
|
const retirement = h.recorder.submitted.find((s) => s.draft.event === 'question_retired')
|
|
3184
3183
|
expect(retirement?.draft.source).toEqual({
|
|
3185
3184
|
session_id: 'sess-abc',
|
|
3186
|
-
session_label:
|
|
3185
|
+
session_label: 'Semantic session',
|
|
3187
3186
|
harness: 'claude-code',
|
|
3188
3187
|
})
|
|
3189
3188
|
})
|
|
@@ -838,11 +838,94 @@ describe('the OpenCode adapter', () => {
|
|
|
838
838
|
expect(source).not.toContain('synthetic: true')
|
|
839
839
|
})
|
|
840
840
|
|
|
841
|
-
it('injects
|
|
841
|
+
it('injects exact identity and the first-party OpenCode title into agent shells', () => {
|
|
842
842
|
expect(source).toContain('"shell.env"')
|
|
843
843
|
expect(source).toContain('NOTIFAI_ACTIVE_HARNESS')
|
|
844
844
|
expect(source).toContain('NOTIFAI_ACTIVE_SESSION_ID')
|
|
845
|
+
expect(source).toContain('NOTIFAI_ACTIVE_SESSION_LABEL')
|
|
846
|
+
expect(source).toContain('NOTIFAI_ACTIVE_SESSION_LABEL_PENDING')
|
|
845
847
|
expect(source).toContain('input?.sessionID')
|
|
848
|
+
expect(source).toContain('client.session.get({ path: { id: sessionID } })')
|
|
849
|
+
expect(source).toContain('response?.data?.title')
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
it('executes the generated adapter and publishes the SDK session title', async () => {
|
|
853
|
+
let requested = ''
|
|
854
|
+
const generated = (await import(
|
|
855
|
+
`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`
|
|
856
|
+
)) as {
|
|
857
|
+
NotifAIPlugin(input: {
|
|
858
|
+
directory: string
|
|
859
|
+
client: {
|
|
860
|
+
session: {
|
|
861
|
+
get(input: { path: { id: string } }): Promise<{ data: { title: string } }>
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}): Promise<{
|
|
865
|
+
'shell.env'?: (
|
|
866
|
+
input: { sessionID?: string },
|
|
867
|
+
output: { env: Record<string, string> },
|
|
868
|
+
) => Promise<void>
|
|
869
|
+
}>
|
|
870
|
+
}
|
|
871
|
+
const plugin = await generated.NotifAIPlugin({
|
|
872
|
+
directory: '/repo',
|
|
873
|
+
client: {
|
|
874
|
+
session: {
|
|
875
|
+
get: async ({ path: request }) => {
|
|
876
|
+
requested = request.id
|
|
877
|
+
return { data: { title: 'Semantic session names' } }
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
},
|
|
881
|
+
})
|
|
882
|
+
const output = { env: {} as Record<string, string> }
|
|
883
|
+
await plugin['shell.env']?.({ sessionID: 'opencode-session' }, output)
|
|
884
|
+
|
|
885
|
+
expect(requested).toBe('opencode-session')
|
|
886
|
+
expect(output.env).toMatchObject({
|
|
887
|
+
NOTIFAI_ACTIVE_HARNESS: 'opencode',
|
|
888
|
+
NOTIFAI_ACTIVE_SESSION_ID: 'opencode-session',
|
|
889
|
+
NOTIFAI_ACTIVE_SESSION_LABEL: 'Semantic session names',
|
|
890
|
+
})
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
it('marks OpenCode placeholder titles as pending instead of publishing them', async () => {
|
|
894
|
+
const generated = (await import(
|
|
895
|
+
`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`
|
|
896
|
+
)) as {
|
|
897
|
+
NotifAIPlugin(input: {
|
|
898
|
+
directory: string
|
|
899
|
+
client: {
|
|
900
|
+
session: {
|
|
901
|
+
get(input: { path: { id: string } }): Promise<{ data: { title: string } }>
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}): Promise<{
|
|
905
|
+
'shell.env'?: (
|
|
906
|
+
input: { sessionID?: string },
|
|
907
|
+
output: { env: Record<string, string> },
|
|
908
|
+
) => Promise<void>
|
|
909
|
+
}>
|
|
910
|
+
}
|
|
911
|
+
const plugin = await generated.NotifAIPlugin({
|
|
912
|
+
directory: '/repo',
|
|
913
|
+
client: {
|
|
914
|
+
session: {
|
|
915
|
+
get: async () => ({
|
|
916
|
+
data: { title: 'New session - 2026-08-20T13:05:00.000Z' },
|
|
917
|
+
}),
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
})
|
|
921
|
+
const output = { env: {} as Record<string, string> }
|
|
922
|
+
await plugin['shell.env']?.({ sessionID: 'opencode-session' }, output)
|
|
923
|
+
|
|
924
|
+
expect(output.env).toEqual({
|
|
925
|
+
NOTIFAI_ACTIVE_HARNESS: 'opencode',
|
|
926
|
+
NOTIFAI_ACTIVE_SESSION_ID: 'opencode-session',
|
|
927
|
+
NOTIFAI_ACTIVE_SESSION_LABEL_PENDING: '1',
|
|
928
|
+
})
|
|
846
929
|
})
|
|
847
930
|
|
|
848
931
|
it('carries the ownership marker so a second checkout replaces it', () => {
|
|
@@ -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/main.ts
CHANGED
|
@@ -313,7 +313,10 @@ 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')
|
|
@@ -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
|
|