@raidiant/notifai 10.1.1 → 10.1.2
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 +20 -0
- package/dist/commands-core.d.ts +1 -1
- package/dist/commands-init.js +9 -6
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +3 -0
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-skill.js +72 -54
- package/dist/commands-skill.js.map +1 -1
- package/dist/invocation-context.d.ts +1 -0
- package/dist/invocation-context.js +9 -1
- package/dist/invocation-context.js.map +1 -1
- package/dist/native-skills.d.ts +13 -3
- package/dist/native-skills.js +51 -12
- package/dist/native-skills.js.map +1 -1
- package/dist/node-floor.d.ts +2 -1
- package/dist/node-floor.js +9 -5
- package/dist/node-floor.js.map +1 -1
- package/dist/orca-session-title.d.ts +5 -4
- package/dist/orca-session-title.js +32 -19
- package/dist/orca-session-title.js.map +1 -1
- package/dist/platform.d.ts +11 -17
- package/dist/platform.js +20 -23
- package/dist/platform.js.map +1 -1
- package/dist/release.d.ts +5 -5
- package/dist/release.js +5 -5
- package/dist/setup-destinations.js +1 -1
- package/dist/setup-destinations.js.map +1 -1
- package/dist/skill-integrity.d.ts +49 -0
- package/dist/skill-integrity.js +154 -0
- package/dist/skill-integrity.js.map +1 -0
- package/dist/skill-source/manifest.json +16 -0
- package/dist/skill-source/notifai/SKILL.md +390 -0
- package/dist/skill-source/notifai/references/harness-setup.md +220 -0
- package/package.json +11 -14
- package/src/atomic-file.ts +0 -198
- package/src/claude-wake.test.ts +0 -321
- package/src/claude-wake.ts +0 -499
- package/src/cli-bin.test.ts +0 -176
- package/src/cli-bin.ts +0 -233
- package/src/cli-release.test.ts +0 -46
- package/src/cli-release.ts +0 -78
- package/src/cli-update-contract.ts +0 -9
- package/src/cli-update.test.ts +0 -125
- package/src/client.test.ts +0 -390
- package/src/client.ts +0 -352
- package/src/codex-wake.test.ts +0 -254
- package/src/codex-wake.ts +0 -301
- package/src/command-session.ts +0 -38
- package/src/commands-agent-sessions.ts +0 -72
- package/src/commands-auth.ts +0 -238
- package/src/commands-config.ts +0 -336
- package/src/commands-core.ts +0 -312
- package/src/commands-devices.ts +0 -180
- package/src/commands-doctor.ts +0 -1501
- package/src/commands-guidance.ts +0 -181
- package/src/commands-harness-context.ts +0 -207
- package/src/commands-hook-shape.ts +0 -69
- package/src/commands-hooks.ts +0 -2318
- package/src/commands-init.ts +0 -1055
- package/src/commands-io.ts +0 -108
- package/src/commands-logs.ts +0 -243
- package/src/commands-project.ts +0 -47
- package/src/commands-send-support.test.ts +0 -156
- package/src/commands-send-support.ts +0 -311
- package/src/commands-send.ts +0 -875
- package/src/commands-setup-proof.test.ts +0 -72
- package/src/commands-setup-proof.ts +0 -124
- package/src/commands-skill.test.ts +0 -44
- package/src/commands-skill.ts +0 -204
- package/src/commands-sounds.test.ts +0 -106
- package/src/commands-sounds.ts +0 -49
- package/src/commands-update.ts +0 -180
- package/src/commands.test.ts +0 -8297
- package/src/commands.ts +0 -77
- package/src/config-schema.test.ts +0 -153
- package/src/config-schema.ts +0 -349
- package/src/config.test.ts +0 -581
- package/src/config.ts +0 -475
- package/src/credentials.test.ts +0 -403
- package/src/credentials.ts +0 -403
- package/src/file-lock-process-worker.test.ts +0 -64
- package/src/file-lock.test.ts +0 -320
- package/src/file-lock.ts +0 -393
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
- package/src/generated-session-label.ts +0 -155
- package/src/guidance-content.ts +0 -278
- package/src/guidance-render.ts +0 -56
- package/src/guidance.test.ts +0 -327
- package/src/guidance.ts +0 -233
- package/src/harness-integration.test.ts +0 -294
- package/src/harnesses.test.ts +0 -149
- package/src/harnesses.ts +0 -164
- package/src/hook-adapter.test.ts +0 -496
- package/src/hook-adapter.ts +0 -424
- package/src/hook-input.test.ts +0 -39
- package/src/hook-input.ts +0 -72
- package/src/hooks.test.ts +0 -5824
- package/src/hooks.ts +0 -4011
- package/src/install-hooks.test.ts +0 -1614
- package/src/install-hooks.ts +0 -1501
- package/src/interactive.test.ts +0 -387
- package/src/interactive.ts +0 -858
- package/src/invocation-context.test.ts +0 -352
- package/src/invocation-context.ts +0 -197
- package/src/logging-process-worker.test.ts +0 -45
- package/src/logging.test.ts +0 -544
- package/src/logging.ts +0 -698
- package/src/main-run.ts +0 -45
- package/src/main.ts +0 -12
- package/src/native-skills.test.ts +0 -115
- package/src/native-skills.ts +0 -162
- package/src/node-floor.test.ts +0 -27
- package/src/node-floor.ts +0 -37
- package/src/onboarding-docs.test.ts +0 -25
- package/src/openclaw-plugin.ts +0 -409
- package/src/opencode-plugin.ts +0 -298
- package/src/orca-session-title.test.ts +0 -54
- package/src/orca-session-title.ts +0 -103
- package/src/pack-feedback-slice.test.ts +0 -212
- package/src/pack-feedback-slice.ts +0 -187
- package/src/platform.test.ts +0 -152
- package/src/platform.ts +0 -173
- package/src/program.test.ts +0 -317
- package/src/program.ts +0 -842
- package/src/project-enablement.test.ts +0 -55
- package/src/project-enablement.ts +0 -99
- package/src/question-caps.test.ts +0 -49
- package/src/question-settlement-process.ts +0 -47
- package/src/question-timing.ts +0 -56
- package/src/readiness.test.ts +0 -25
- package/src/readiness.ts +0 -212
- package/src/release.ts +0 -51
- package/src/send-attempts.test.ts +0 -143
- package/src/send-attempts.ts +0 -188
- package/src/send.test.ts +0 -28
- package/src/send.ts +0 -436
- package/src/session-activation.ts +0 -61
- package/src/session-end-log-lock-worker.test.ts +0 -35
- package/src/session-labels.test.ts +0 -490
- package/src/session-labels.ts +0 -474
- package/src/setup-destinations.test.ts +0 -41
- package/src/setup-destinations.ts +0 -50
- package/src/skill.test.ts +0 -426
- package/src/sound-ref.test.ts +0 -31
- package/src/sound-ref.ts +0 -34
- package/src/trust-policy.test.ts +0 -701
- package/src/ui/banner.ts +0 -95
- package/src/ui/config-view.test.ts +0 -90
- package/src/ui/config-view.ts +0 -138
- package/src/ui/help.test.ts +0 -16
- package/src/ui/help.ts +0 -128
- package/src/ui/theme.test.ts +0 -92
- package/src/ui/theme.ts +0 -265
- package/src/url-policy.ts +0 -399
- package/tsconfig.json +0 -10
package/src/session-labels.ts
DELETED
|
@@ -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
|
-
}
|