@raidiant/notifai 10.1.1 → 10.1.3

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 (168) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/commands-core.d.ts +4 -1
  3. package/dist/commands-core.js.map +1 -1
  4. package/dist/commands-doctor.js +2 -7
  5. package/dist/commands-doctor.js.map +1 -1
  6. package/dist/commands-hooks.d.ts +12 -0
  7. package/dist/commands-hooks.js +15 -1
  8. package/dist/commands-hooks.js.map +1 -1
  9. package/dist/commands-init.js +9 -6
  10. package/dist/commands-init.js.map +1 -1
  11. package/dist/commands-send-support.js +10 -11
  12. package/dist/commands-send-support.js.map +1 -1
  13. package/dist/commands-skill.js +72 -54
  14. package/dist/commands-skill.js.map +1 -1
  15. package/dist/harness-session-title.d.ts +22 -0
  16. package/dist/harness-session-title.js +74 -0
  17. package/dist/harness-session-title.js.map +1 -0
  18. package/dist/invocation-context.d.ts +1 -0
  19. package/dist/invocation-context.js +15 -1
  20. package/dist/invocation-context.js.map +1 -1
  21. package/dist/native-skills.d.ts +13 -3
  22. package/dist/native-skills.js +51 -12
  23. package/dist/native-skills.js.map +1 -1
  24. package/dist/node-floor.d.ts +2 -1
  25. package/dist/node-floor.js +9 -5
  26. package/dist/node-floor.js.map +1 -1
  27. package/dist/orca-session-title.d.ts +5 -4
  28. package/dist/orca-session-title.js +32 -19
  29. package/dist/orca-session-title.js.map +1 -1
  30. package/dist/platform.d.ts +11 -17
  31. package/dist/platform.js +20 -23
  32. package/dist/platform.js.map +1 -1
  33. package/dist/release.d.ts +5 -5
  34. package/dist/release.js +5 -5
  35. package/dist/session-labels.d.ts +2 -0
  36. package/dist/session-labels.js +20 -9
  37. package/dist/session-labels.js.map +1 -1
  38. package/dist/setup-destinations.js +1 -1
  39. package/dist/setup-destinations.js.map +1 -1
  40. package/dist/skill-integrity.d.ts +49 -0
  41. package/dist/skill-integrity.js +154 -0
  42. package/dist/skill-integrity.js.map +1 -0
  43. package/dist/skill-source/manifest.json +16 -0
  44. package/dist/skill-source/notifai/SKILL.md +390 -0
  45. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  46. package/package.json +11 -14
  47. package/src/atomic-file.ts +0 -198
  48. package/src/claude-wake.test.ts +0 -321
  49. package/src/claude-wake.ts +0 -499
  50. package/src/cli-bin.test.ts +0 -176
  51. package/src/cli-bin.ts +0 -233
  52. package/src/cli-release.test.ts +0 -46
  53. package/src/cli-release.ts +0 -78
  54. package/src/cli-update-contract.ts +0 -9
  55. package/src/cli-update.test.ts +0 -125
  56. package/src/client.test.ts +0 -390
  57. package/src/client.ts +0 -352
  58. package/src/codex-wake.test.ts +0 -254
  59. package/src/codex-wake.ts +0 -301
  60. package/src/command-session.ts +0 -38
  61. package/src/commands-agent-sessions.ts +0 -72
  62. package/src/commands-auth.ts +0 -238
  63. package/src/commands-config.ts +0 -336
  64. package/src/commands-core.ts +0 -312
  65. package/src/commands-devices.ts +0 -180
  66. package/src/commands-doctor.ts +0 -1501
  67. package/src/commands-guidance.ts +0 -181
  68. package/src/commands-harness-context.ts +0 -207
  69. package/src/commands-hook-shape.ts +0 -69
  70. package/src/commands-hooks.ts +0 -2318
  71. package/src/commands-init.ts +0 -1055
  72. package/src/commands-io.ts +0 -108
  73. package/src/commands-logs.ts +0 -243
  74. package/src/commands-project.ts +0 -47
  75. package/src/commands-send-support.test.ts +0 -156
  76. package/src/commands-send-support.ts +0 -311
  77. package/src/commands-send.ts +0 -875
  78. package/src/commands-setup-proof.test.ts +0 -72
  79. package/src/commands-setup-proof.ts +0 -124
  80. package/src/commands-skill.test.ts +0 -44
  81. package/src/commands-skill.ts +0 -204
  82. package/src/commands-sounds.test.ts +0 -106
  83. package/src/commands-sounds.ts +0 -49
  84. package/src/commands-update.ts +0 -180
  85. package/src/commands.test.ts +0 -8297
  86. package/src/commands.ts +0 -77
  87. package/src/config-schema.test.ts +0 -153
  88. package/src/config-schema.ts +0 -349
  89. package/src/config.test.ts +0 -581
  90. package/src/config.ts +0 -475
  91. package/src/credentials.test.ts +0 -403
  92. package/src/credentials.ts +0 -403
  93. package/src/file-lock-process-worker.test.ts +0 -64
  94. package/src/file-lock.test.ts +0 -320
  95. package/src/file-lock.ts +0 -393
  96. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  97. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  98. package/src/generated-session-label.ts +0 -155
  99. package/src/guidance-content.ts +0 -278
  100. package/src/guidance-render.ts +0 -56
  101. package/src/guidance.test.ts +0 -327
  102. package/src/guidance.ts +0 -233
  103. package/src/harness-integration.test.ts +0 -294
  104. package/src/harnesses.test.ts +0 -149
  105. package/src/harnesses.ts +0 -164
  106. package/src/hook-adapter.test.ts +0 -496
  107. package/src/hook-adapter.ts +0 -424
  108. package/src/hook-input.test.ts +0 -39
  109. package/src/hook-input.ts +0 -72
  110. package/src/hooks.test.ts +0 -5824
  111. package/src/hooks.ts +0 -4011
  112. package/src/install-hooks.test.ts +0 -1614
  113. package/src/install-hooks.ts +0 -1501
  114. package/src/interactive.test.ts +0 -387
  115. package/src/interactive.ts +0 -858
  116. package/src/invocation-context.test.ts +0 -352
  117. package/src/invocation-context.ts +0 -197
  118. package/src/logging-process-worker.test.ts +0 -45
  119. package/src/logging.test.ts +0 -544
  120. package/src/logging.ts +0 -698
  121. package/src/main-run.ts +0 -45
  122. package/src/main.ts +0 -12
  123. package/src/native-skills.test.ts +0 -115
  124. package/src/native-skills.ts +0 -162
  125. package/src/node-floor.test.ts +0 -27
  126. package/src/node-floor.ts +0 -37
  127. package/src/onboarding-docs.test.ts +0 -25
  128. package/src/openclaw-plugin.ts +0 -409
  129. package/src/opencode-plugin.ts +0 -298
  130. package/src/orca-session-title.test.ts +0 -54
  131. package/src/orca-session-title.ts +0 -103
  132. package/src/pack-feedback-slice.test.ts +0 -212
  133. package/src/pack-feedback-slice.ts +0 -187
  134. package/src/platform.test.ts +0 -152
  135. package/src/platform.ts +0 -173
  136. package/src/program.test.ts +0 -317
  137. package/src/program.ts +0 -842
  138. package/src/project-enablement.test.ts +0 -55
  139. package/src/project-enablement.ts +0 -99
  140. package/src/question-caps.test.ts +0 -49
  141. package/src/question-settlement-process.ts +0 -47
  142. package/src/question-timing.ts +0 -56
  143. package/src/readiness.test.ts +0 -25
  144. package/src/readiness.ts +0 -212
  145. package/src/release.ts +0 -51
  146. package/src/send-attempts.test.ts +0 -143
  147. package/src/send-attempts.ts +0 -188
  148. package/src/send.test.ts +0 -28
  149. package/src/send.ts +0 -436
  150. package/src/session-activation.ts +0 -61
  151. package/src/session-end-log-lock-worker.test.ts +0 -35
  152. package/src/session-labels.test.ts +0 -490
  153. package/src/session-labels.ts +0 -474
  154. package/src/setup-destinations.test.ts +0 -41
  155. package/src/setup-destinations.ts +0 -50
  156. package/src/skill.test.ts +0 -426
  157. package/src/sound-ref.test.ts +0 -31
  158. package/src/sound-ref.ts +0 -34
  159. package/src/trust-policy.test.ts +0 -701
  160. package/src/ui/banner.ts +0 -95
  161. package/src/ui/config-view.test.ts +0 -90
  162. package/src/ui/config-view.ts +0 -138
  163. package/src/ui/help.test.ts +0 -16
  164. package/src/ui/help.ts +0 -128
  165. package/src/ui/theme.test.ts +0 -92
  166. package/src/ui/theme.ts +0 -265
  167. package/src/url-policy.ts +0 -399
  168. package/tsconfig.json +0 -10
@@ -1,474 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
- import { copyFileSync, existsSync, readFileSync } from 'node:fs'
3
- import path from 'node:path'
4
- import { SESSION_LABEL_MAX_LENGTH } from '@raidiant/notifai-protocol'
5
- import { atomicWriteFileSync } from './atomic-file.js'
6
- import { stateDir } from './config.js'
7
- import { withFileLock } from './file-lock.js'
8
- import { generatedSessionLabel } from './generated-session-label.js'
9
- import { HARNESS_LABELS, type SourceContextHarness } from './harnesses.js'
10
-
11
- const STORE_VERSION = 1
12
- const MONTHS = [
13
- 'Jan',
14
- 'Feb',
15
- 'Mar',
16
- 'Apr',
17
- 'May',
18
- 'Jun',
19
- 'Jul',
20
- 'Aug',
21
- 'Sep',
22
- 'Oct',
23
- 'Nov',
24
- 'Dec',
25
- ] as const
26
-
27
- type SessionLabelSource = 'explicit' | 'harness' | 'fallback'
28
-
29
- interface SessionLabelCandidate {
30
- label: string
31
- source: SessionLabelSource
32
- }
33
-
34
- interface StoredSessionLabel {
35
- label: string
36
- source: SessionLabelSource
37
- first_seen_at: number
38
- harness?: SourceContextHarness
39
- }
40
-
41
- interface SessionLabelStore {
42
- version: typeof STORE_VERSION
43
- sessions: Record<string, StoredSessionLabel>
44
- }
45
-
46
- export interface SessionLabelInput {
47
- env: NodeJS.ProcessEnv
48
- sessionId: string
49
- harness?: SourceContextHarness
50
- explicitLabel?: string
51
- harnessLabel?: string
52
- harnessLabelPending?: boolean
53
- now?: number
54
- }
55
-
56
- export interface SessionLabelRenameInput {
57
- env: NodeJS.ProcessEnv
58
- sessionId: string
59
- label: string
60
- harness?: SourceContextHarness
61
- now?: number
62
- }
63
-
64
- export type SessionLabelResolution =
65
- | { ok: true; label: string; source: SessionLabelSource }
66
- | { ok: false; error: string }
67
-
68
- function storePath(env: NodeJS.ProcessEnv): string {
69
- return path.join(stateDir(env), 'session-labels.json')
70
- }
71
-
72
- /** The state key cannot reveal the harness's opaque session identifier. */
73
- function sessionKey(sessionId: string): string {
74
- return createHash('sha256').update(sessionId).digest('hex')
75
- }
76
-
77
- function emptyStore(): SessionLabelStore {
78
- return { version: STORE_VERSION, sessions: {} }
79
- }
80
-
81
- function quarantineInvalidStore(file: string, raw: string): void {
82
- const digest = createHash('sha256').update(raw).digest('hex')
83
- const backup = path.join(path.dirname(file), `session-labels.invalid-${digest}.json`)
84
- if (existsSync(backup)) {
85
- if (readFileSync(backup, 'utf8') !== raw) {
86
- throw new Error('the session-name store recovery backup does not match its content hash')
87
- }
88
- return
89
- }
90
- copyFileSync(file, backup)
91
- }
92
-
93
- function recoverInvalidStore(file: string, raw: string, store: SessionLabelStore): SessionLabelStore {
94
- quarantineInvalidStore(file, raw)
95
- writeStore(file, store)
96
- return store
97
- }
98
-
99
- function storedRecord(candidate: unknown): StoredSessionLabel | null {
100
- if (typeof candidate !== 'object' || candidate === null) return null
101
- const value = candidate as Partial<StoredSessionLabel>
102
- if (
103
- typeof value.label !== 'string' ||
104
- value.label.length === 0 ||
105
- value.label.length > SESSION_LABEL_MAX_LENGTH ||
106
- !['explicit', 'harness', 'fallback'].includes(value.source ?? '') ||
107
- typeof value.first_seen_at !== 'number' ||
108
- !Number.isFinite(value.first_seen_at)
109
- ) {
110
- return null
111
- }
112
- const harness = value.harness
113
- if (harness !== undefined && !Object.hasOwn(HARNESS_LABELS, harness)) return null
114
- return {
115
- label: value.label,
116
- source: value.source as SessionLabelSource,
117
- first_seen_at: value.first_seen_at,
118
- ...(harness === undefined ? {} : { harness }),
119
- }
120
- }
121
-
122
- function readStore(file: string): SessionLabelStore {
123
- if (!existsSync(file)) return emptyStore()
124
- let rawText: string
125
- try {
126
- rawText = readFileSync(file, 'utf8')
127
- } catch (err) {
128
- throw new Error(
129
- `the session-name store is unreadable: ${err instanceof Error ? err.message : String(err)}`,
130
- )
131
- }
132
- let parsed: unknown
133
- try {
134
- parsed = JSON.parse(rawText)
135
- } catch {
136
- return recoverInvalidStore(file, rawText, emptyStore())
137
- }
138
- if (typeof parsed !== 'object' || parsed === null) {
139
- return recoverInvalidStore(file, rawText, emptyStore())
140
- }
141
- const raw = parsed as { version?: unknown; sessions?: unknown }
142
- if (raw.version !== STORE_VERSION) {
143
- return recoverInvalidStore(file, rawText, emptyStore())
144
- }
145
- if (typeof raw.sessions !== 'object' || raw.sessions === null) {
146
- return recoverInvalidStore(file, rawText, emptyStore())
147
- }
148
- const sessions: Record<string, StoredSessionLabel> = {}
149
- let invalidRecordFound = false
150
- for (const [key, candidate] of Object.entries(raw.sessions)) {
151
- const record = storedRecord(candidate)
152
- if (!/^[a-f0-9]{64}$/.test(key) || record === null) {
153
- invalidRecordFound = true
154
- continue
155
- }
156
- sessions[key] = record
157
- }
158
- const store = { version: STORE_VERSION, sessions } satisfies SessionLabelStore
159
- return invalidRecordFound ? recoverInvalidStore(file, rawText, store) : store
160
- }
161
-
162
- function writeStore(file: string, store: SessionLabelStore): void {
163
- atomicWriteFileSync(file, `${JSON.stringify(store, null, 2)}\n`)
164
- }
165
-
166
- function normalizeWhitespace(value: string): string {
167
- return value.trim().replaceAll(/\s+/gu, ' ')
168
- }
169
-
170
- /** Match the same UTF-16 length TypeBox enforces at the wire boundary. */
171
- function truncate(value: string, maxLength: number): string {
172
- if (value.length <= maxLength) return value
173
- let prefix = value.slice(0, Math.max(0, maxLength - 1))
174
- const last = prefix.charCodeAt(prefix.length - 1)
175
- if (last >= 0xd800 && last <= 0xdbff) prefix = prefix.slice(0, -1)
176
- return `${prefix}…`
177
- }
178
-
179
- const UUID_PATTERN =
180
- /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/iu
181
- const HASH_PATTERN = /\b[0-9a-f]{12,}\b/iu
182
- const HARNESS_ID_PATTERN = /\b(?:ses|thread)_[A-Za-z0-9_-]{8,}\b/iu
183
- const FILESYSTEM_PATH_PATTERN =
184
- /(?:^|[\s("'=:])(?:~[\\/]|[A-Za-z]:[\\/]|\\\\|\/(?:Users|home|private|tmp|var|Volumes|opt|etc|usr|workspace|workspaces)(?:[\\/]|$))/u
185
- const OPENCODE_PENDING_TITLE =
186
- /^New session(?:\s*-\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)?$/iu
187
-
188
- function containsPrivatePath(value: string, env: NodeJS.ProcessEnv): boolean {
189
- if (FILESYSTEM_PATH_PATTERN.test(value)) return true
190
- const folded = value.toLocaleLowerCase('en-US')
191
- for (const key of ['HOME', 'USERPROFILE', 'TMPDIR'] as const) {
192
- const candidate = env[key]?.trim().replace(/[\\/]+$/u, '')
193
- if (
194
- candidate !== undefined &&
195
- candidate.length > 1 &&
196
- folded.includes(candidate.toLocaleLowerCase('en-US'))
197
- ) {
198
- return true
199
- }
200
- }
201
- return false
202
- }
203
-
204
- function violatesLabelPrivacy(value: string, input: SessionLabelInput): boolean {
205
- const sessionId = input.sessionId.trim()
206
- const exposesCurrentId =
207
- sessionId.length > 0 &&
208
- (value === sessionId || (sessionId.length >= 8 && value.includes(sessionId)))
209
- return (
210
- exposesCurrentId ||
211
- UUID_PATTERN.test(value) ||
212
- HASH_PATTERN.test(value) ||
213
- HARNESS_ID_PATTERN.test(value) ||
214
- containsPrivatePath(value, input.env)
215
- )
216
- }
217
-
218
- function explicitCandidate(
219
- value: string | undefined,
220
- input: SessionLabelInput,
221
- ): SessionLabelCandidate | { error: string } | null {
222
- if (value === undefined) return null
223
- const normalized = normalizeWhitespace(value)
224
- if (normalized.length === 0) {
225
- return { error: '--session-label (or NOTIFAI_SESSION_LABEL) must not be empty.' }
226
- }
227
- if (normalized.length > SESSION_LABEL_MAX_LENGTH) {
228
- return {
229
- error: `--session-label (or NOTIFAI_SESSION_LABEL) must be at most ${SESSION_LABEL_MAX_LENGTH} characters.`,
230
- }
231
- }
232
- if (violatesLabelPrivacy(normalized, input)) {
233
- return {
234
- error:
235
- '--session-label (or NOTIFAI_SESSION_LABEL) must not contain a session identifier, hash, or filesystem path.',
236
- }
237
- }
238
- return { label: normalized, source: 'explicit' }
239
- }
240
-
241
- export function normalizeExplicitSessionLabel(
242
- input: Pick<SessionLabelRenameInput, 'env' | 'sessionId' | 'label'>,
243
- ): { ok: true; label: string } | { ok: false; error: string } {
244
- const candidate = explicitCandidate(input.label, {
245
- env: input.env,
246
- sessionId: input.sessionId,
247
- })
248
- if (candidate === null) {
249
- return { ok: false, error: '--session-label must not be empty.' }
250
- }
251
- if ('error' in candidate) return { ok: false, error: candidate.error }
252
- return { ok: true, label: candidate.label }
253
- }
254
-
255
- function harnessLabelIsPending(
256
- value: string | undefined,
257
- input: SessionLabelInput,
258
- ): boolean {
259
- if (input.harnessLabelPending === true) return true
260
- if (input.harness !== 'opencode' || value === undefined) return false
261
- return OPENCODE_PENDING_TITLE.test(normalizeWhitespace(value))
262
- }
263
-
264
- function harnessCandidate(
265
- value: string | undefined,
266
- input: SessionLabelInput,
267
- ): SessionLabelCandidate | null {
268
- if (value === undefined) return null
269
- const normalized = normalizeWhitespace(value)
270
- if (normalized.length === 0 || violatesLabelPrivacy(normalized, input)) return null
271
- return {
272
- label: truncate(normalized, SESSION_LABEL_MAX_LENGTH),
273
- source: 'harness',
274
- }
275
- }
276
-
277
- function pad(value: number): string {
278
- return String(value).padStart(2, '0')
279
- }
280
-
281
- /** Stable English shape using the machine's local calendar and clock. */
282
- export function formatSessionFirstSeen(now: number): string {
283
- const date = new Date(now)
284
- return `${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}`
285
- }
286
-
287
- function fallbackCandidate(sessionId: string): SessionLabelCandidate {
288
- return {
289
- label: generatedSessionLabel(sessionId),
290
- source: 'fallback',
291
- }
292
- }
293
-
294
- function isLegacyDateFallback(record: StoredSessionLabel): boolean {
295
- if (record.source !== 'fallback') return false
296
- const owner = record.harness === undefined ? 'Agent' : HARNESS_LABELS[record.harness]
297
- return record.label.startsWith(`${owner} session · `)
298
- }
299
-
300
- function withSuffix(base: string, suffix: string): string {
301
- const room = SESSION_LABEL_MAX_LENGTH - suffix.length
302
- return `${truncate(base, room)}${suffix}`
303
- }
304
-
305
- function collisionKey(value: string): string {
306
- return value.normalize('NFKC').toLocaleLowerCase('en-US')
307
- }
308
-
309
- function uniqueLabel(
310
- base: string,
311
- source: SessionLabelSource,
312
- harness: SourceContextHarness | undefined,
313
- now: number,
314
- used: ReadonlySet<string>,
315
- ): string {
316
- if (!used.has(collisionKey(base))) return base
317
-
318
- if (source === 'fallback') {
319
- for (let ordinal = 2; ; ordinal += 1) {
320
- const candidate = withSuffix(base, ` · ${ordinal}`)
321
- if (!used.has(collisionKey(candidate))) return candidate
322
- }
323
- }
324
-
325
- const owner = harness === undefined ? 'Agent' : HARNESS_LABELS[harness]
326
- const ownerCandidate = withSuffix(base, ` · ${owner}`)
327
- if (!used.has(collisionKey(ownerCandidate))) return ownerCandidate
328
-
329
- const timed = withSuffix(base, ` · ${owner} · ${formatSessionFirstSeen(now)}`)
330
- if (!used.has(collisionKey(timed))) return timed
331
-
332
- for (let ordinal = 2; ; ordinal += 1) {
333
- const candidate = withSuffix(timed, ` · ${ordinal}`)
334
- if (!used.has(collisionKey(candidate))) return candidate
335
- }
336
- }
337
-
338
- /**
339
- * Freeze the first accepted human name for one immutable session.
340
- *
341
- * A title supplied by a trusted harness adapter wins, then explicit
342
- * agent/User input, then a generated fallback. The environment-first order is
343
- * what makes `--session-label` safe to pass unconditionally: an agent never
344
- * needs to know which environments name their own sessions, because where one
345
- * does, the explicit label is simply not used. A frozen generated fallback is
346
- * the one exception to permanence: it names nothing, so the first semantic
347
- * candidate to arrive later — a trusted harness title first, then explicit —
348
- * replaces it once. Semantic names then stay frozen even when their branch,
349
- * worktree, title candidate, or notification changes.
350
- */
351
- export function resolveSessionLabel(input: SessionLabelInput): SessionLabelResolution {
352
- const now = input.now ?? Date.now()
353
- const file = storePath(input.env)
354
- const key = sessionKey(input.sessionId)
355
-
356
- try {
357
- return withFileLock(`${file}.lock`, () => {
358
- const store = readStore(file)
359
- const existing = store.sessions[key]
360
- if (existing !== undefined) {
361
- const used = new Set(
362
- Object.entries(store.sessions)
363
- .filter(([storedKey]) => storedKey !== key)
364
- .map(([, record]) => collisionKey(record.label)),
365
- )
366
- if (existing.source === 'fallback') {
367
- const explicit = explicitCandidate(input.explicitLabel, input)
368
- if (explicit !== null && 'error' in explicit) {
369
- return { ok: false, error: explicit.error }
370
- }
371
- const native = harnessLabelIsPending(input.harnessLabel, input)
372
- ? null
373
- : harnessCandidate(input.harnessLabel, input)
374
- const upgrade = native ?? explicit
375
- if (upgrade !== null) {
376
- const label = uniqueLabel(upgrade.label, upgrade.source, input.harness, now, used)
377
- store.sessions[key] = {
378
- label,
379
- source: upgrade.source,
380
- first_seen_at: existing.first_seen_at,
381
- ...(input.harness === undefined
382
- ? existing.harness === undefined
383
- ? {}
384
- : { harness: existing.harness }
385
- : { harness: input.harness }),
386
- }
387
- writeStore(file, store)
388
- return { ok: true, label, source: upgrade.source }
389
- }
390
- }
391
- if (!isLegacyDateFallback(existing)) {
392
- return { ok: true, label: existing.label, source: existing.source }
393
- }
394
- const label = uniqueLabel(
395
- generatedSessionLabel(input.sessionId),
396
- 'fallback',
397
- input.harness ?? existing.harness,
398
- now,
399
- used,
400
- )
401
- store.sessions[key] = { ...existing, label }
402
- writeStore(file, store)
403
- return { ok: true, label, source: existing.source }
404
- }
405
-
406
- const explicit = explicitCandidate(input.explicitLabel, input)
407
- if (explicit !== null && 'error' in explicit) {
408
- return { ok: false, error: explicit.error }
409
- }
410
- const nativePending = harnessLabelIsPending(input.harnessLabel, input)
411
- const native = nativePending ? null : harnessCandidate(input.harnessLabel, input)
412
- if (explicit === null && nativePending) {
413
- return {
414
- ok: false,
415
- error:
416
- "OpenCode is still generating this session's title; retry shortly or pass --session-label.",
417
- }
418
- }
419
- const candidate: SessionLabelCandidate =
420
- native ?? explicit ?? fallbackCandidate(input.sessionId)
421
- const used = new Set(
422
- Object.values(store.sessions).map((record) => collisionKey(record.label)),
423
- )
424
- const label = uniqueLabel(candidate.label, candidate.source, input.harness, now, used)
425
- store.sessions[key] = {
426
- label,
427
- source: candidate.source,
428
- first_seen_at: now,
429
- ...(input.harness === undefined ? {} : { harness: input.harness }),
430
- }
431
- writeStore(file, store)
432
- return { ok: true, label, source: candidate.source }
433
- })
434
- } catch (err) {
435
- return {
436
- ok: false,
437
- error: `Could not persist this session's name: ${err instanceof Error ? err.message : String(err)}`,
438
- }
439
- }
440
- }
441
-
442
- /** Replace the local frozen label only after the Account-authoritative rename succeeds. */
443
- export function renameStoredSessionLabel(
444
- input: SessionLabelRenameInput,
445
- ): SessionLabelResolution {
446
- const normalized = normalizeExplicitSessionLabel(input)
447
- if (!normalized.ok) return normalized
448
- const file = storePath(input.env)
449
- const key = sessionKey(input.sessionId)
450
- const now = input.now ?? Date.now()
451
- try {
452
- return withFileLock(`${file}.lock`, () => {
453
- const store = readStore(file)
454
- const existing = store.sessions[key]
455
- store.sessions[key] = {
456
- label: normalized.label,
457
- source: 'explicit',
458
- first_seen_at: existing?.first_seen_at ?? now,
459
- ...(input.harness === undefined
460
- ? existing?.harness === undefined
461
- ? {}
462
- : { harness: existing.harness }
463
- : { harness: input.harness }),
464
- }
465
- writeStore(file, store)
466
- return { ok: true, label: normalized.label, source: 'explicit' }
467
- })
468
- } catch (err) {
469
- return {
470
- ok: false,
471
- error: `The Account label changed, but this machine could not save it locally: ${err instanceof Error ? err.message : String(err)}`,
472
- }
473
- }
474
- }
@@ -1,41 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import {
3
- companionPlatformLabel,
4
- setupAccessUrl,
5
- setupCompanionUrl,
6
- supportPageUrl,
7
- } from './setup-destinations.js'
8
-
9
- describe('focused setup destinations', () => {
10
- it('sends a hosted reader to the dashboard they can sign in to', () => {
11
- expect(setupAccessUrl('https://api.notifai.sh')).toBe('https://app.notifai.sh/setup/access')
12
- expect(setupCompanionUrl('https://notifai.fly.dev')).toBe(
13
- 'https://app.notifai.sh/setup/companion',
14
- )
15
- })
16
-
17
- it('keeps a self-host on its own origin', () => {
18
- expect(setupAccessUrl('https://notify.example.com/')).toBe(
19
- 'https://notify.example.com/setup/access',
20
- )
21
- })
22
-
23
- it('carries the platform this terminal already asked about', () => {
24
- expect(setupCompanionUrl('https://api.notifai.sh', 'iphone')).toBe(
25
- 'https://app.notifai.sh/setup/companion?platform=iphone',
26
- )
27
- expect(setupAccessUrl('https://api.notifai.sh', 'android')).toBe(
28
- 'https://app.notifai.sh/setup/access?platform=android',
29
- )
30
- })
31
-
32
- it('keeps the omnibus help page separate from any setup errand', () => {
33
- expect(supportPageUrl('https://api.notifai.sh')).toBe('https://app.notifai.sh/support')
34
- expect(setupAccessUrl('https://api.notifai.sh')).not.toContain('/support')
35
- })
36
-
37
- it('names each platform the way a reader would', () => {
38
- expect(companionPlatformLabel('iphone')).toBe('iPhone')
39
- expect(companionPlatformLabel('android')).toBe('Android')
40
- })
41
- })
@@ -1,50 +0,0 @@
1
- /**
2
- * Where a cross-surface setup errand is finished.
3
- *
4
- * Every handoff opens the destination for exactly one errand, carrying the
5
- * platform when this terminal already asked for it. The omnibus help page is
6
- * help: it covers access, two install routes, billing, refunds, deletion,
7
- * privacy and terms, and its own next step is to go somewhere else. Sending
8
- * someone there for a named gate makes them find it themselves.
9
- *
10
- * The server prints these same paths into `next_action`, which is the value
11
- * this CLI prefers whenever it has one. These builders are the fallback for
12
- * the paths where it does not, and they must stay spelled the same.
13
- */
14
-
15
- /** The two platforms that can receive a notification. Mac is not one. */
16
- export const COMPANION_PLATFORMS = ['iphone', 'android'] as const
17
- export type CompanionPlatform = (typeof COMPANION_PLATFORMS)[number]
18
-
19
- export function companionPlatformLabel(platform: CompanionPlatform): string {
20
- return platform === 'iphone' ? 'iPhone' : 'Android'
21
- }
22
-
23
- /**
24
- * The dashboard a reader can sign in to, which is not always the API origin.
25
- * Only the hosted deployment splits them; a self-host serves both from one.
26
- */
27
- function dashboardOrigin(baseUrl: string): string {
28
- const normalized = baseUrl.replace(/\/+$/, '')
29
- if (normalized === 'https://api.notifai.sh' || normalized === 'https://notifai.fly.dev') {
30
- return 'https://app.notifai.sh'
31
- }
32
- return normalized
33
- }
34
-
35
- /** Getting this Account access to Notifai, and nothing else. */
36
- export function setupAccessUrl(baseUrl: string, platform?: CompanionPlatform): string {
37
- const base = `${dashboardOrigin(baseUrl)}/setup/access`
38
- return platform === undefined ? base : `${base}?platform=${platform}`
39
- }
40
-
41
- /** Installing the Companion App on one named platform. */
42
- export function setupCompanionUrl(baseUrl: string, platform?: CompanionPlatform): string {
43
- const base = `${dashboardOrigin(baseUrl)}/setup/companion`
44
- return platform === undefined ? base : `${base}?platform=${platform}`
45
- }
46
-
47
- /** General help: billing, policies, and everything setup is not. */
48
- export function supportPageUrl(baseUrl: string): string {
49
- return `${dashboardOrigin(baseUrl)}/support`
50
- }