@raidiant/notifai 0.2.1 → 0.4.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/README.md +20 -4
- package/dist/atomic-file.d.ts +4 -0
- package/dist/atomic-file.js +60 -10
- package/dist/atomic-file.js.map +1 -1
- package/dist/client.d.ts +25 -2
- package/dist/client.js +45 -1
- package/dist/client.js.map +1 -1
- package/dist/commands.d.ts +85 -6
- package/dist/commands.js +1133 -322
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +62 -0
- package/dist/config-schema.js +312 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/config.d.ts +38 -3
- package/dist/config.js +61 -14
- package/dist/config.js.map +1 -1
- package/dist/file-lock.d.ts +26 -0
- package/dist/file-lock.js +268 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/harnesses.d.ts +28 -0
- package/dist/harnesses.js +40 -0
- package/dist/harnesses.js.map +1 -0
- package/dist/hook-adapter.d.ts +27 -0
- package/dist/hook-adapter.js +188 -0
- package/dist/hook-adapter.js.map +1 -0
- package/dist/hook-input.d.ts +10 -0
- package/dist/hook-input.js +64 -0
- package/dist/hook-input.js.map +1 -0
- package/dist/hooks.d.ts +66 -10
- package/dist/hooks.js +751 -276
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +40 -16
- package/dist/install-hooks.js +217 -73
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +4 -0
- package/dist/interactive.js +604 -0
- package/dist/interactive.js.map +1 -0
- package/dist/logging.d.ts +142 -0
- package/dist/logging.js +533 -0
- package/dist/logging.js.map +1 -0
- package/dist/main.js +201 -51
- package/dist/main.js.map +1 -1
- package/dist/opencode-plugin.d.ts +7 -7
- package/dist/opencode-plugin.js +12 -13
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/ui/banner.d.ts +22 -0
- package/dist/ui/banner.js +87 -0
- package/dist/ui/banner.js.map +1 -0
- package/dist/ui/config-view.d.ts +32 -0
- package/dist/ui/config-view.js +108 -0
- package/dist/ui/config-view.js.map +1 -0
- package/dist/ui/help.d.ts +46 -0
- package/dist/ui/help.js +105 -0
- package/dist/ui/help.js.map +1 -0
- package/dist/ui/theme.d.ts +108 -0
- package/dist/ui/theme.js +261 -0
- package/dist/ui/theme.js.map +1 -0
- package/package.json +11 -9
- package/src/atomic-file.ts +86 -7
- package/src/client.test.ts +12 -0
- package/src/client.ts +72 -4
- package/src/commands.test.ts +971 -44
- package/src/commands.ts +1323 -337
- package/src/config-schema.test.ts +152 -0
- package/src/config-schema.ts +363 -0
- package/src/config.test.ts +6 -5
- package/src/config.ts +85 -16
- package/src/file-lock-process-worker.test.ts +64 -0
- package/src/file-lock.test.ts +279 -0
- package/src/file-lock.ts +318 -0
- package/src/harnesses.test.ts +48 -0
- package/src/harnesses.ts +59 -0
- package/src/hook-adapter.test.ts +187 -0
- package/src/hook-adapter.ts +212 -0
- package/src/hook-input.test.ts +39 -0
- package/src/hook-input.ts +72 -0
- package/src/hooks.test.ts +1318 -240
- package/src/hooks.ts +1046 -296
- package/src/install-hooks.test.ts +90 -41
- package/src/install-hooks.ts +259 -74
- package/src/interactive.ts +692 -0
- package/src/logging-process-worker.test.ts +39 -0
- package/src/logging.test.ts +453 -0
- package/src/logging.ts +639 -0
- package/src/main.ts +220 -48
- package/src/opencode-plugin.ts +15 -16
- package/src/session-end-log-lock-worker.test.ts +35 -0
- package/src/skill.test.ts +11 -0
- package/src/ui/banner.ts +95 -0
- package/src/ui/config-view.test.ts +88 -0
- package/src/ui/config-view.ts +138 -0
- package/src/ui/help.ts +124 -0
- package/src/ui/theme.test.ts +92 -0
- package/src/ui/theme.ts +279 -0
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
codexLayerDir,
|
|
28
28
|
codexProjectRoot,
|
|
29
29
|
findInstallations,
|
|
30
|
+
codexHookIdentityHash,
|
|
31
|
+
codexTrustKey,
|
|
32
|
+
codexTrustProblems,
|
|
30
33
|
handlerEvent,
|
|
31
34
|
hookCommand,
|
|
32
35
|
mergeHooks,
|
|
@@ -38,9 +41,10 @@ import {
|
|
|
38
41
|
|
|
39
42
|
const SCRIPT = '/opt/notifai/dist/main.js'
|
|
40
43
|
const EXEC = '/usr/local/bin/node'
|
|
44
|
+
const ADAPTER = '/Users/test/.notifai/bin/hook-adapter'
|
|
41
45
|
|
|
42
46
|
function ours(): HookConfig {
|
|
43
|
-
return buildHookConfig({
|
|
47
|
+
return buildHookConfig({ adapterPath: ADAPTER })
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
describe('choice labels', () => {
|
|
@@ -71,21 +75,21 @@ describe('choice labels', () => {
|
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
describe('hook config', () => {
|
|
74
|
-
it('invokes
|
|
75
|
-
expect(hookCommand(
|
|
78
|
+
it('invokes one stable adapter so a sparse hook PATH still works', () => {
|
|
79
|
+
expect(hookCommand(ADAPTER, 'stop')).toBe(`'${ADAPTER}' hook stop --owner notifai`)
|
|
76
80
|
})
|
|
77
81
|
|
|
78
82
|
it('single-quotes so a path cannot become a command', () => {
|
|
79
83
|
// Double quotes still expand $() and backticks, so a checkout path with
|
|
80
84
|
// shell syntax was execution on every hook event.
|
|
81
|
-
const cmd = hookCommand('/
|
|
82
|
-
expect(cmd).toBe("'/
|
|
83
|
-
expect(hookCommand(
|
|
85
|
+
const cmd = hookCommand('/tmp/$(touch /tmp/pwned)/hook-adapter', 'stop')
|
|
86
|
+
expect(cmd).toBe("'/tmp/$(touch /tmp/pwned)/hook-adapter' hook stop --owner notifai")
|
|
87
|
+
expect(hookCommand("/it's/here", 'stop')).toContain(`'/it'\\''s/here'`)
|
|
84
88
|
})
|
|
85
89
|
|
|
86
|
-
it('
|
|
87
|
-
const config = buildHookConfig({
|
|
88
|
-
expect(config['Stop']?.[0]?.hooks[0]?.timeout).toBe(
|
|
90
|
+
it('uses one fixed blocking process budget rather than accepting mutable timing inputs', () => {
|
|
91
|
+
const config = buildHookConfig({ adapterPath: ADAPTER })
|
|
92
|
+
expect(config['Stop']?.[0]?.hooks[0]?.timeout).toBe(540)
|
|
89
93
|
// Claude Code caps UserPromptSubmit at 30s; Codex gives SessionEnd 1-3s.
|
|
90
94
|
expect(config['UserPromptSubmit']?.[0]?.hooks[0]?.timeout).toBe(15)
|
|
91
95
|
expect(config['SessionEnd']?.[0]?.hooks[0]?.timeout).toBe(3)
|
|
@@ -93,10 +97,7 @@ describe('hook config', () => {
|
|
|
93
97
|
|
|
94
98
|
it('stamps the exact harness on every generated command', () => {
|
|
95
99
|
const config = buildHookConfig({
|
|
96
|
-
|
|
97
|
-
scriptPath: SCRIPT,
|
|
98
|
-
replyTimeoutSeconds: 240,
|
|
99
|
-
graceSeconds: 0,
|
|
100
|
+
adapterPath: ADAPTER,
|
|
100
101
|
harness: 'claude-code',
|
|
101
102
|
})
|
|
102
103
|
expect(
|
|
@@ -106,6 +107,16 @@ describe('hook config', () => {
|
|
|
106
107
|
.every((handler) => handler.command.includes('--harness claude-code')),
|
|
107
108
|
).toBe(true)
|
|
108
109
|
})
|
|
110
|
+
|
|
111
|
+
it('lets Codex own Stop timeout while keeping short fixed lifecycle budgets', () => {
|
|
112
|
+
const codex = buildHookConfig({ adapterPath: ADAPTER, harness: 'codex' })
|
|
113
|
+
const claude = buildHookConfig({ adapterPath: ADAPTER, harness: 'claude-code' })
|
|
114
|
+
|
|
115
|
+
expect(codex['UserPromptSubmit']?.[0]?.hooks[0]?.timeout).toBe(15)
|
|
116
|
+
expect(codex['Stop']?.[0]?.hooks[0]?.timeout).toBeUndefined()
|
|
117
|
+
expect(codex['SessionEnd']?.[0]?.hooks[0]?.timeout).toBe(3)
|
|
118
|
+
expect(claude['Stop']?.[0]?.hooks[0]?.timeout).toBe(540)
|
|
119
|
+
})
|
|
109
120
|
})
|
|
110
121
|
|
|
111
122
|
describe('merging into existing settings', () => {
|
|
@@ -130,11 +141,11 @@ describe('merging into existing settings', () => {
|
|
|
130
141
|
const once = mergeHooks({}, ours(), SCRIPT)
|
|
131
142
|
const twice = mergeHooks(
|
|
132
143
|
once.document,
|
|
133
|
-
buildHookConfig({
|
|
144
|
+
buildHookConfig({ adapterPath: ADAPTER }),
|
|
134
145
|
SCRIPT,
|
|
135
146
|
)
|
|
136
147
|
expect(twice.document.hooks?.['Stop']).toHaveLength(1)
|
|
137
|
-
expect(twice.document.hooks?.['Stop']?.[0]?.hooks[0]?.timeout).toBe(
|
|
148
|
+
expect(twice.document.hooks?.['Stop']?.[0]?.hooks[0]?.timeout).toBe(540)
|
|
138
149
|
expect(twice.replaced).toContain('Stop')
|
|
139
150
|
})
|
|
140
151
|
|
|
@@ -292,9 +303,9 @@ describe('upgrading past a dropped event', () => {
|
|
|
292
303
|
const stale = {
|
|
293
304
|
hooks: {
|
|
294
305
|
PermissionRequest: [
|
|
295
|
-
{ hooks: [{ type: 'command' as const, command: hookCommand(
|
|
306
|
+
{ hooks: [{ type: 'command' as const, command: hookCommand(ADAPTER, 'permission-request') }] },
|
|
296
307
|
],
|
|
297
|
-
Stop: [{ hooks: [{ type: 'command' as const, command: hookCommand(
|
|
308
|
+
Stop: [{ hooks: [{ type: 'command' as const, command: hookCommand(ADAPTER, 'stop') }] }],
|
|
298
309
|
},
|
|
299
310
|
}
|
|
300
311
|
|
|
@@ -311,7 +322,7 @@ describe('upgrading past a dropped event', () => {
|
|
|
311
322
|
PermissionRequest: [
|
|
312
323
|
{
|
|
313
324
|
hooks: [
|
|
314
|
-
{ type: 'command' as const, command: hookCommand(
|
|
325
|
+
{ type: 'command' as const, command: hookCommand(ADAPTER, 'permission-request') },
|
|
315
326
|
{ type: 'command' as const, command: 'their-own-tool --check' },
|
|
316
327
|
],
|
|
317
328
|
},
|
|
@@ -393,10 +404,49 @@ describe('finding what is installed', () => {
|
|
|
393
404
|
it('reads the event a handler actually invokes, not the key it sits under', () => {
|
|
394
405
|
// These diverge exactly when an upgrade drops an event, which is the case
|
|
395
406
|
// worth detecting.
|
|
396
|
-
expect(handlerEvent(hookCommand(
|
|
407
|
+
expect(handlerEvent(hookCommand(ADAPTER, 'permission-request'))).toBe('permission-request')
|
|
397
408
|
expect(handlerEvent('unrelated --tool')).toBeNull()
|
|
398
409
|
})
|
|
399
410
|
|
|
411
|
+
it('detects when Codex still trusts an older definition of an installed hook', () => {
|
|
412
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-codex-trust-'))
|
|
413
|
+
const codexHome = path.join(cwd, 'codex-home')
|
|
414
|
+
const hookFile = path.join(cwd, '.codex', 'hooks.json')
|
|
415
|
+
mkdirSync(path.dirname(hookFile), { recursive: true })
|
|
416
|
+
applyPlan(hookFile, { hooks: ours() })
|
|
417
|
+
const installations = findInstallations(cwd, { CODEX_HOME: codexHome })
|
|
418
|
+
const codex = installations.find((installation) => installation.harness === 'codex')
|
|
419
|
+
const stop = codex?.handlers.find((handler) => handler.event === 'Stop')
|
|
420
|
+
expect(stop).toBeDefined()
|
|
421
|
+
|
|
422
|
+
mkdirSync(codexHome, { recursive: true })
|
|
423
|
+
writeFileSync(
|
|
424
|
+
path.join(codexHome, 'config.toml'),
|
|
425
|
+
`[hooks.state.${JSON.stringify(codexTrustKey(codex!, stop!))}]\ntrusted_hash = "sha256:obsolete"\n`,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
const stale = codexTrustProblems(installations, { CODEX_HOME: codexHome })
|
|
429
|
+
expect(stale).toHaveLength(3)
|
|
430
|
+
expect(stale).toEqual(expect.arrayContaining([expect.stringMatching(/Stop.*changed.*\/hooks/i)]))
|
|
431
|
+
|
|
432
|
+
writeFileSync(
|
|
433
|
+
path.join(codexHome, 'config.toml'),
|
|
434
|
+
`[hooks.state.${JSON.stringify(codexTrustKey(codex!, stop!))}]\ntrusted_hash = "${codexHookIdentityHash(stop!)}"\n`,
|
|
435
|
+
)
|
|
436
|
+
expect(codexTrustProblems(installations, { CODEX_HOME: codexHome })).toEqual([
|
|
437
|
+
expect.stringMatching(/UserPromptSubmit.*not trusted.*\/hooks/i),
|
|
438
|
+
expect.stringMatching(/SessionEnd.*not trusted.*\/hooks/i),
|
|
439
|
+
])
|
|
440
|
+
|
|
441
|
+
writeFileSync(
|
|
442
|
+
path.join(codexHome, 'config.toml'),
|
|
443
|
+
`[hooks.state.${JSON.stringify(codexTrustKey(codex!, stop!))}]\ntrusted_hash = "${codexHookIdentityHash(stop!)}"\nenabled = false\n`,
|
|
444
|
+
)
|
|
445
|
+
expect(codexTrustProblems(installations, { CODEX_HOME: codexHome })).toEqual(
|
|
446
|
+
expect.arrayContaining([expect.stringMatching(/Stop.*disabled.*\/hooks/i)]),
|
|
447
|
+
)
|
|
448
|
+
})
|
|
449
|
+
|
|
400
450
|
})
|
|
401
451
|
|
|
402
452
|
/**
|
|
@@ -405,7 +455,7 @@ describe('finding what is installed', () => {
|
|
|
405
455
|
* the harness ran both, and one question produced two notifications.
|
|
406
456
|
*/
|
|
407
457
|
describe('two checkouts', () => {
|
|
408
|
-
const OTHER = '/Users/rafael/
|
|
458
|
+
const OTHER = '/Users/rafael/notifai-public/apps/cli/dist/main.js'
|
|
409
459
|
|
|
410
460
|
function documentWith(command: string): SettingsDocument {
|
|
411
461
|
return { hooks: { Stop: [{ matcher: '*', hooks: [{ type: 'command', command, timeout: 300 }] }] } }
|
|
@@ -416,26 +466,26 @@ describe('two checkouts', () => {
|
|
|
416
466
|
}
|
|
417
467
|
|
|
418
468
|
it('replaces a handler another checkout installed instead of running beside it', () => {
|
|
419
|
-
const existing = documentWith(hookCommand(
|
|
469
|
+
const existing = documentWith(hookCommand('/other/notifai/hook-adapter', 'stop'))
|
|
420
470
|
const merged = mergeHooks(
|
|
421
471
|
existing,
|
|
422
|
-
buildHookConfig({
|
|
472
|
+
buildHookConfig({ adapterPath: ADAPTER }),
|
|
423
473
|
SCRIPT,
|
|
424
474
|
)
|
|
425
475
|
const commands = stopCommands(merged.document)
|
|
426
476
|
expect(commands.filter((c) => c.includes('hook stop'))).toHaveLength(1)
|
|
427
|
-
expect(commands[0]).toContain(
|
|
428
|
-
expect(commands.join(' ')).not.toContain(
|
|
477
|
+
expect(commands[0]).toContain(ADAPTER)
|
|
478
|
+
expect(commands.join(' ')).not.toContain('/other/notifai/hook-adapter')
|
|
429
479
|
})
|
|
430
480
|
|
|
431
481
|
it('uninstalls a handler another checkout installed', () => {
|
|
432
|
-
const removed = removeHooks(documentWith(hookCommand(
|
|
482
|
+
const removed = removeHooks(documentWith(hookCommand('/other/notifai/hook-adapter', 'stop')), SCRIPT)
|
|
433
483
|
expect(stopCommands(removed.document)).toHaveLength(0)
|
|
434
484
|
})
|
|
435
485
|
|
|
436
486
|
it('still recognises an install that predates the marker', () => {
|
|
437
|
-
//
|
|
438
|
-
const legacy = documentWith(`'${EXEC}' '${
|
|
487
|
+
// Different checkout, no marker: the form written by an older build.
|
|
488
|
+
const legacy = documentWith(`'${EXEC}' '${OTHER}' hook stop`)
|
|
439
489
|
expect(stopCommands(removeHooks(legacy, SCRIPT).document)).toHaveLength(0)
|
|
440
490
|
})
|
|
441
491
|
|
|
@@ -443,6 +493,11 @@ describe('two checkouts', () => {
|
|
|
443
493
|
const foreign = documentWith('/usr/local/bin/my-own-thing --stop')
|
|
444
494
|
expect(stopCommands(removeHooks(foreign, SCRIPT).document)).toHaveLength(1)
|
|
445
495
|
})
|
|
496
|
+
|
|
497
|
+
it("preserves a foreign project's generic dist/main.js hook", () => {
|
|
498
|
+
const foreign = documentWith("'/usr/local/bin/node' '/opt/foreign/dist/main.js' hook stop")
|
|
499
|
+
expect(stopCommands(removeHooks(foreign, SCRIPT).document)).toHaveLength(1)
|
|
500
|
+
})
|
|
446
501
|
})
|
|
447
502
|
|
|
448
503
|
/**
|
|
@@ -451,8 +506,7 @@ describe('two checkouts', () => {
|
|
|
451
506
|
*/
|
|
452
507
|
describe('the OpenCode adapter', () => {
|
|
453
508
|
const source = opencodePluginSource({
|
|
454
|
-
|
|
455
|
-
scriptPath: SCRIPT,
|
|
509
|
+
adapterPath: ADAPTER,
|
|
456
510
|
timeoutSeconds: 240,
|
|
457
511
|
})
|
|
458
512
|
|
|
@@ -460,8 +514,9 @@ describe('the OpenCode adapter', () => {
|
|
|
460
514
|
// The whole point: presence, escalation and retirement stay in the CLI.
|
|
461
515
|
expect(source).toContain('"hook", event')
|
|
462
516
|
expect(source).toContain('"--harness", "opencode"')
|
|
463
|
-
expect(source).toContain(JSON.stringify(
|
|
464
|
-
expect(source).toContain(JSON.stringify(
|
|
517
|
+
expect(source).toContain(JSON.stringify(ADAPTER))
|
|
518
|
+
expect(source).not.toContain(JSON.stringify(SCRIPT))
|
|
519
|
+
expect(source).not.toContain(JSON.stringify(EXEC))
|
|
465
520
|
})
|
|
466
521
|
|
|
467
522
|
it('wires the three question lifecycle joints and leaves permissions alone', () => {
|
|
@@ -504,27 +559,21 @@ describe('the OpenCode adapter', () => {
|
|
|
504
559
|
expect(settingsFile('opencode', false, '/repo', {})).toContain('notifai.js')
|
|
505
560
|
})
|
|
506
561
|
|
|
507
|
-
it('reports the
|
|
508
|
-
// `doctor` asks every handler which build it invokes. A plugin is a module
|
|
509
|
-
// rather than a command line, so answering with the plugin's own path made
|
|
510
|
-
// an ordinary OpenCode install look like a second checkout and turned the
|
|
511
|
-
// duplicate check red on a healthy machine.
|
|
562
|
+
it('reports the same stable adapter identity as command-hook harnesses', () => {
|
|
512
563
|
expect(opencodePluginTarget(source)).toEqual({
|
|
513
|
-
|
|
514
|
-
script: SCRIPT,
|
|
564
|
+
adapter: ADAPTER,
|
|
515
565
|
current: true,
|
|
516
566
|
timeoutSeconds: 240,
|
|
517
567
|
})
|
|
518
568
|
expect(opencodePluginTarget(source.replace(/^const ADAPTER_VERSION = .*\n/m, ''))).toEqual({
|
|
519
|
-
|
|
520
|
-
script: SCRIPT,
|
|
569
|
+
adapter: ADAPTER,
|
|
521
570
|
current: false,
|
|
522
571
|
timeoutSeconds: 240,
|
|
523
572
|
})
|
|
524
573
|
expect(opencodePluginTarget('export const SomeoneElse = () => ({})')).toBeNull()
|
|
525
574
|
// Ours, but with the constants edited beyond recognition: no answer beats
|
|
526
575
|
// a wrong one, since the caller compares these for equality.
|
|
527
|
-
expect(opencodePluginTarget(`${OPENCODE_PLUGIN_MARKER}\nconst
|
|
576
|
+
expect(opencodePluginTarget(`${OPENCODE_PLUGIN_MARKER}\nconst ADAPTER = whatever\n`)).toBeNull()
|
|
528
577
|
})
|
|
529
578
|
})
|
|
530
579
|
|