@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +2 -2
  3. package/dist/atomic-file.js +13 -0
  4. package/dist/atomic-file.js.map +1 -1
  5. package/dist/client.d.ts +8 -3
  6. package/dist/client.js +24 -3
  7. package/dist/client.js.map +1 -1
  8. package/dist/commands-auth.d.ts +14 -0
  9. package/dist/commands-auth.js +154 -0
  10. package/dist/commands-auth.js.map +1 -0
  11. package/dist/commands-config.d.ts +31 -0
  12. package/dist/commands-config.js +246 -0
  13. package/dist/commands-config.js.map +1 -0
  14. package/dist/commands-core.d.ts +102 -0
  15. package/dist/commands-core.js +135 -0
  16. package/dist/commands-core.js.map +1 -0
  17. package/dist/commands-devices.d.ts +31 -0
  18. package/dist/commands-devices.js +124 -0
  19. package/dist/commands-devices.js.map +1 -0
  20. package/dist/commands-doctor.d.ts +24 -0
  21. package/dist/commands-doctor.js +1011 -0
  22. package/dist/commands-doctor.js.map +1 -0
  23. package/dist/commands-harness-context.d.ts +65 -0
  24. package/dist/commands-harness-context.js +97 -0
  25. package/dist/commands-harness-context.js.map +1 -0
  26. package/dist/commands-hook-shape.d.ts +17 -0
  27. package/dist/commands-hook-shape.js +37 -0
  28. package/dist/commands-hook-shape.js.map +1 -0
  29. package/dist/commands-hooks.d.ts +115 -0
  30. package/dist/commands-hooks.js +1225 -0
  31. package/dist/commands-hooks.js.map +1 -0
  32. package/dist/commands-init.d.ts +35 -0
  33. package/dist/commands-init.js +580 -0
  34. package/dist/commands-init.js.map +1 -0
  35. package/dist/commands-io.d.ts +2 -0
  36. package/dist/commands-io.js +110 -0
  37. package/dist/commands-io.js.map +1 -0
  38. package/dist/commands-logs.d.ts +30 -0
  39. package/dist/commands-logs.js +201 -0
  40. package/dist/commands-logs.js.map +1 -0
  41. package/dist/commands-send-support.d.ts +44 -0
  42. package/dist/commands-send-support.js +183 -0
  43. package/dist/commands-send-support.js.map +1 -0
  44. package/dist/commands-send.d.ts +33 -0
  45. package/dist/commands-send.js +519 -0
  46. package/dist/commands-send.js.map +1 -0
  47. package/dist/commands-setup-proof.d.ts +15 -0
  48. package/dist/commands-setup-proof.js +53 -0
  49. package/dist/commands-setup-proof.js.map +1 -0
  50. package/dist/commands-skill.d.ts +10 -0
  51. package/dist/commands-skill.js +71 -0
  52. package/dist/commands-skill.js.map +1 -0
  53. package/dist/commands.d.ts +12 -387
  54. package/dist/commands.js +12 -4536
  55. package/dist/commands.js.map +1 -1
  56. package/dist/config-schema.js +1 -1
  57. package/dist/config-schema.js.map +1 -1
  58. package/dist/config.d.ts +4 -4
  59. package/dist/config.js +6 -4
  60. package/dist/config.js.map +1 -1
  61. package/dist/credentials.js +13 -3
  62. package/dist/credentials.js.map +1 -1
  63. package/dist/generated-session-label.d.ts +2 -0
  64. package/dist/generated-session-label.js +152 -0
  65. package/dist/generated-session-label.js.map +1 -0
  66. package/dist/hooks.d.ts +2 -2
  67. package/dist/hooks.js +127 -99
  68. package/dist/hooks.js.map +1 -1
  69. package/dist/interactive.js +4 -4
  70. package/dist/interactive.js.map +1 -1
  71. package/dist/invocation-context.d.ts +3 -0
  72. package/dist/invocation-context.js +23 -6
  73. package/dist/invocation-context.js.map +1 -1
  74. package/dist/logging.js +1 -0
  75. package/dist/logging.js.map +1 -1
  76. package/dist/main.js +3 -3
  77. package/dist/main.js.map +1 -1
  78. package/dist/opencode-plugin.js +18 -3
  79. package/dist/opencode-plugin.js.map +1 -1
  80. package/dist/readiness.d.ts +3 -1
  81. package/dist/readiness.js.map +1 -1
  82. package/dist/session-labels.d.ts +30 -0
  83. package/dist/session-labels.js +278 -0
  84. package/dist/session-labels.js.map +1 -0
  85. package/package.json +2 -2
  86. package/src/atomic-file.ts +12 -0
  87. package/src/client.test.ts +90 -1
  88. package/src/client.ts +35 -2
  89. package/src/commands-auth.ts +174 -0
  90. package/src/commands-config.ts +307 -0
  91. package/src/commands-core.ts +240 -0
  92. package/src/commands-devices.ts +150 -0
  93. package/src/commands-doctor.ts +1209 -0
  94. package/src/commands-harness-context.ts +134 -0
  95. package/src/commands-hook-shape.ts +53 -0
  96. package/src/commands-hooks.ts +1586 -0
  97. package/src/commands-init.ts +721 -0
  98. package/src/commands-io.ts +108 -0
  99. package/src/commands-logs.ts +243 -0
  100. package/src/commands-send-support.ts +227 -0
  101. package/src/commands-send.ts +623 -0
  102. package/src/commands-setup-proof.ts +71 -0
  103. package/src/commands-skill.ts +84 -0
  104. package/src/commands.test.ts +567 -42
  105. package/src/commands.ts +58 -5614
  106. package/src/config-schema.ts +1 -1
  107. package/src/config.test.ts +9 -0
  108. package/src/config.ts +10 -5
  109. package/src/credentials.test.ts +25 -1
  110. package/src/credentials.ts +13 -3
  111. package/src/generated-session-label.ts +155 -0
  112. package/src/hooks.test.ts +355 -16
  113. package/src/hooks.ts +145 -97
  114. package/src/install-hooks.test.ts +84 -1
  115. package/src/interactive.test.ts +19 -1
  116. package/src/interactive.ts +4 -4
  117. package/src/invocation-context.test.ts +115 -16
  118. package/src/invocation-context.ts +32 -11
  119. package/src/logging.test.ts +15 -2
  120. package/src/logging.ts +1 -0
  121. package/src/main.ts +9 -3
  122. package/src/opencode-plugin.ts +18 -3
  123. package/src/readiness.ts +3 -1
  124. package/src/session-labels.test.ts +303 -0
  125. package/src/session-labels.ts +360 -0
  126. package/src/skill.test.ts +13 -0
@@ -83,7 +83,7 @@ const INFO: Record<ConfigKey, Omit<ConfigKeyInfo, 'key'>> = {
83
83
  label: 'Send questions to my devices',
84
84
  group: 'questions',
85
85
  kind: 'boolean',
86
- summary: 'Whether a registered question may reach your phone and Mac at all',
86
+ summary: 'Whether a registered question may reach your iPhone at all',
87
87
  detail:
88
88
  'The master switch for question routing. When this is off, a question an agent registers with `notifai ask` stays in the terminal and never leaves this machine, whatever else is configured. Turn it off to stop being reached for a while without uninstalling the harness hooks.',
89
89
  example: 'true',
@@ -177,6 +177,15 @@ describe('config precedence', () => {
177
177
  expect(config.wait_seconds.value).toBe(2)
178
178
  expect(config.wait_seconds.source).toBe('flag')
179
179
  })
180
+
181
+ it('uses the prior default when a newer CLI wrote an unknown enum value', () => {
182
+ const { env, cwd } = setup({
183
+ globalToml: 'sound = "future-sound"\ninterruption_level = "future-level"\n',
184
+ })
185
+ const config = loadConfig({ cwd, env })
186
+ expect(config.sound).toEqual({ value: null, source: 'default' })
187
+ expect(config.interruption_level).toEqual({ value: null, source: 'default' })
188
+ })
180
189
  })
181
190
 
182
191
  describe('draft building', () => {
package/src/config.ts CHANGED
@@ -1,9 +1,12 @@
1
- import { REPLY_MAX_WINDOW_SECONDS } from '@raidiant/notifai-protocol'
1
+ import {
2
+ CLI_SOUNDS,
3
+ INTERRUPTION_LEVELS,
4
+ REPLY_MAX_WINDOW_SECONDS,
5
+ } from '@raidiant/notifai-protocol'
2
6
  import { createHash } from 'node:crypto'
3
7
  import { existsSync, readFileSync, realpathSync } from 'node:fs'
4
8
  import path from 'node:path'
5
9
  import { parse as parseToml } from 'smol-toml'
6
- import type { CLI_SOUNDS, INTERRUPTION_LEVELS } from '@raidiant/notifai-protocol'
7
10
  import { configHome, stateHome } from './platform.js'
8
11
 
9
12
  /**
@@ -133,6 +136,8 @@ export const NUMERIC_CONFIG_KEYS: readonly ConfigKey[] = [
133
136
  * is the same discipline the numeric bounds follow.
134
137
  */
135
138
  export const ENUM_CONFIG_VALUES: Partial<Record<ConfigKey, readonly string[]>> = {
139
+ sound: CLI_SOUNDS,
140
+ interruption_level: INTERRUPTION_LEVELS,
136
141
  log_level: LOG_LEVELS,
137
142
  }
138
143
 
@@ -183,9 +188,9 @@ export function globalConfigPath(env: NodeJS.ProcessEnv = process.env): string {
183
188
  }
184
189
 
185
190
  /**
186
- * Machine-local mutable state: presence markers, pending questions, and
187
- * session config overrides. Separate from the config dir because none of it is
188
- * user-authored and none of it should ever be synced or committed.
191
+ * Machine-local mutable state: presence markers, pending questions, frozen
192
+ * session names, and session config overrides. Separate from the config dir
193
+ * because none of it should ever be synced or committed.
189
194
  */
190
195
  export function stateDir(
191
196
  env: NodeJS.ProcessEnv = process.env,
@@ -168,7 +168,10 @@ describe('FileStore', () => {
168
168
  if (process.platform !== 'win32') {
169
169
  expect(statSync(file).mode & 0o777).toBe(0o600)
170
170
  }
171
- expect(JSON.parse(readFileSync(file, 'utf8'))).toEqual(SAMPLE)
171
+ expect(JSON.parse(readFileSync(file, 'utf8'))).toEqual({
172
+ format: 'notifai.machine-credential.v1',
173
+ ...SAMPLE,
174
+ })
172
175
  expect(store.load()).toEqual(SAMPLE)
173
176
  store.save(OTHER)
174
177
  expect(store.load()).toEqual(OTHER)
@@ -190,6 +193,27 @@ describe('FileStore', () => {
190
193
  writeRaw(file, `${JSON.stringify({ machineId: 'mach_fixture' })}\n`)
191
194
  expect(new FileStore(env).load()).toBeNull()
192
195
  })
196
+
197
+ it('reads the unversioned v1 shape while making every new save explicit', () => {
198
+ const { env } = sandbox('file-unversioned-v1')
199
+ const file = path.join(env.XDG_CONFIG_HOME!, 'notifai', 'credentials.json')
200
+ writeRaw(file, `${JSON.stringify(SAMPLE)}\n`)
201
+ const store = new FileStore(env)
202
+ expect(store.load()).toEqual(SAMPLE)
203
+ store.save(SAMPLE)
204
+ expect(JSON.parse(readFileSync(file, 'utf8'))).toMatchObject({
205
+ format: 'notifai.machine-credential.v1',
206
+ })
207
+ })
208
+
209
+ it('does not interpret or erase a credential from a future format epoch', () => {
210
+ const { env } = sandbox('file-future-format')
211
+ const file = path.join(env.XDG_CONFIG_HOME!, 'notifai', 'credentials.json')
212
+ const future = `${JSON.stringify({ format: 'notifai.machine-credential.v2', ...SAMPLE })}\n`
213
+ writeRaw(file, future)
214
+ expect(new FileStore(env).load()).toBeNull()
215
+ expect(readFileSync(file, 'utf8')).toBe(future)
216
+ })
193
217
  })
194
218
 
195
219
  describe('WindowsDpapiStore', () => {
@@ -44,6 +44,7 @@ export interface CredentialStoreOptions {
44
44
  }
45
45
 
46
46
  const SERVICE = 'io.notifai.cli'
47
+ const CREDENTIAL_FORMAT = 'notifai.machine-credential.v1'
47
48
  const DPAPI_FORMAT = 'notifai.dpapi.current-user.v1'
48
49
  const DPAPI_ENTROPY = 'io.notifai.cli'
49
50
  const DPAPI_TIMEOUT_MS = 15_000
@@ -136,7 +137,7 @@ export class KeychainStore implements CredentialStore {
136
137
  '-a',
137
138
  'machine',
138
139
  '-w',
139
- JSON.stringify(credential),
140
+ JSON.stringify(serializeCredential(credential)),
140
141
  ],
141
142
  { stdio: 'ignore' },
142
143
  )
@@ -189,7 +190,7 @@ export class FileStore implements CredentialStore {
189
190
  }
190
191
 
191
192
  save(credential: MachineCredential): void {
192
- atomicWriteFileSync(this.filePath(), `${JSON.stringify(credential, null, 2)}\n`, {
193
+ atomicWriteFileSync(this.filePath(), `${JSON.stringify(serializeCredential(credential), null, 2)}\n`, {
193
194
  mode: 0o600,
194
195
  preserveMode: false,
195
196
  requireCurrentUserOwner: true,
@@ -251,7 +252,7 @@ export class WindowsDpapiStore implements CredentialStore {
251
252
  if (file === null) {
252
253
  throw new Error('Windows credential store path is not configured (LOCALAPPDATA is missing)')
253
254
  }
254
- const blob = this.protect(Buffer.from(JSON.stringify(credential), 'utf8'))
255
+ const blob = this.protect(Buffer.from(JSON.stringify(serializeCredential(credential)), 'utf8'))
255
256
  atomicWriteFileSync(file, `${JSON.stringify({ format: DPAPI_FORMAT, data: blob })}\n`, {
256
257
  mode: 0o600,
257
258
  preserveMode: false,
@@ -302,6 +303,10 @@ export class WindowsDpapiStore implements CredentialStore {
302
303
  }
303
304
  }
304
305
 
306
+ function serializeCredential(credential: MachineCredential): Record<string, string> {
307
+ return { format: CREDENTIAL_FORMAT, ...credential }
308
+ }
309
+
305
310
  function parseCredentialJson(raw: string): MachineCredential | null {
306
311
  try {
307
312
  return parseCredential(JSON.parse(raw) as unknown)
@@ -313,6 +318,11 @@ function parseCredentialJson(raw: string): MachineCredential | null {
313
318
  function parseCredential(raw: unknown): MachineCredential | null {
314
319
  if (raw === null || typeof raw !== 'object') return null
315
320
  const value = raw as Record<string, unknown>
321
+ const format = value['format']
322
+ // Unversioned credentials are the pre-epoch v1 representation. A future
323
+ // format is left untouched on disk/keychain and treated as unavailable;
324
+ // interpreting it as v1 could silently change authentication meaning.
325
+ if (format !== undefined && format !== CREDENTIAL_FORMAT) return null
316
326
  const machineId = value['machineId']
317
327
  const secret = value['secret']
318
328
  const baseUrl = value['baseUrl']
@@ -0,0 +1,155 @@
1
+ const FNV1A_64_OFFSET = 0xcbf29ce484222325n
2
+ const FNV1A_64_PRIME = 0x100000001b3n
3
+ const WORD_COUNT = 64n
4
+
5
+ /** Frozen word lists keep generated fallbacks stable for a given opaque id. */
6
+ const ADJECTIVES = [
7
+ 'Amber',
8
+ 'Arctic',
9
+ 'Ash',
10
+ 'Azure',
11
+ 'Blue',
12
+ 'Bold',
13
+ 'Bright',
14
+ 'Bronze',
15
+ 'Calm',
16
+ 'Cedar',
17
+ 'Clear',
18
+ 'Copper',
19
+ 'Coral',
20
+ 'Crisp',
21
+ 'Dawn',
22
+ 'Deep',
23
+ 'Ember',
24
+ 'Fern',
25
+ 'Frost',
26
+ 'Gentle',
27
+ 'Golden',
28
+ 'Green',
29
+ 'Harbor',
30
+ 'Hazel',
31
+ 'Indigo',
32
+ 'Ivory',
33
+ 'Jade',
34
+ 'Keen',
35
+ 'Lively',
36
+ 'Lunar',
37
+ 'Maple',
38
+ 'Meadow',
39
+ 'Mellow',
40
+ 'Misty',
41
+ 'Noble',
42
+ 'Ocean',
43
+ 'Olive',
44
+ 'Onyx',
45
+ 'Quiet',
46
+ 'Rapid',
47
+ 'Red',
48
+ 'River',
49
+ 'Royal',
50
+ 'Sage',
51
+ 'Silver',
52
+ 'Solar',
53
+ 'Steady',
54
+ 'Stone',
55
+ 'Sunny',
56
+ 'Swift',
57
+ 'Teal',
58
+ 'Timber',
59
+ 'True',
60
+ 'Velvet',
61
+ 'Verdant',
62
+ 'Violet',
63
+ 'Warm',
64
+ 'Wild',
65
+ 'Winter',
66
+ 'Wise',
67
+ 'Witty',
68
+ 'Yellow',
69
+ 'Young',
70
+ 'Zealous',
71
+ ] as const
72
+
73
+ const NOUNS = [
74
+ 'Alpaca',
75
+ 'Antelope',
76
+ 'Badger',
77
+ 'Bear',
78
+ 'Beaver',
79
+ 'Bison',
80
+ 'Bobcat',
81
+ 'Caribou',
82
+ 'Cougar',
83
+ 'Coyote',
84
+ 'Crane',
85
+ 'Deer',
86
+ 'Dolphin',
87
+ 'Dragonfly',
88
+ 'Eagle',
89
+ 'Elk',
90
+ 'Falcon',
91
+ 'Finch',
92
+ 'Firefly',
93
+ 'Fox',
94
+ 'Gazelle',
95
+ 'Gecko',
96
+ 'Hare',
97
+ 'Heron',
98
+ 'Ibex',
99
+ 'Jay',
100
+ 'Kestrel',
101
+ 'Kingfisher',
102
+ 'Koala',
103
+ 'Lark',
104
+ 'Leopard',
105
+ 'Lynx',
106
+ 'Magpie',
107
+ 'Mantis',
108
+ 'Marten',
109
+ 'Moose',
110
+ 'Osprey',
111
+ 'Otter',
112
+ 'Owl',
113
+ 'Panda',
114
+ 'Panther',
115
+ 'Pelican',
116
+ 'Penguin',
117
+ 'Puma',
118
+ 'Quail',
119
+ 'Rabbit',
120
+ 'Raven',
121
+ 'Robin',
122
+ 'Sable',
123
+ 'Salmon',
124
+ 'Seal',
125
+ 'Shark',
126
+ 'Sparrow',
127
+ 'Stag',
128
+ 'Swan',
129
+ 'Tiger',
130
+ 'Tortoise',
131
+ 'Trout',
132
+ 'Turtle',
133
+ 'Viper',
134
+ 'Whale',
135
+ 'Wolf',
136
+ 'Wren',
137
+ 'Yak',
138
+ ] as const
139
+
140
+ function fnv1a64(value: string): bigint {
141
+ let hash = FNV1A_64_OFFSET
142
+ for (const byte of new TextEncoder().encode(value)) {
143
+ hash ^= BigInt(byte)
144
+ hash = BigInt.asUintN(64, hash * FNV1A_64_PRIME)
145
+ }
146
+ return hash
147
+ }
148
+
149
+ /** Deterministic fallback for a session whose work has no semantic title. */
150
+ export function generatedSessionLabel(sessionId: string): string {
151
+ const hash = fnv1a64(sessionId)
152
+ const adjective = ADJECTIVES[Number(hash % WORD_COUNT)]
153
+ const noun = NOUNS[Number((hash >> 6n) % WORD_COUNT)]
154
+ return `${adjective} ${noun}`
155
+ }