@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
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process'
|
|
2
|
-
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { describe, expect, it } from 'vitest'
|
|
6
|
-
import {
|
|
7
|
-
disableProject,
|
|
8
|
-
enableProject,
|
|
9
|
-
projectBinding,
|
|
10
|
-
projectEnabled,
|
|
11
|
-
} from './project-enablement.js'
|
|
12
|
-
|
|
13
|
-
function git(cwd: string, ...args: string[]): void {
|
|
14
|
-
execFileSync('git', ['-C', cwd, ...args], { stdio: 'ignore' })
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('Project enablement', () => {
|
|
18
|
-
it('is disabled by default and changes only through its User-owned marker', () => {
|
|
19
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-enablement-'))
|
|
20
|
-
const env = { XDG_CONFIG_HOME: path.join(root, 'config') }
|
|
21
|
-
const binding = projectBinding(root, env)
|
|
22
|
-
expect(binding).not.toBeNull()
|
|
23
|
-
expect(projectEnabled(binding)).toBe(false)
|
|
24
|
-
|
|
25
|
-
enableProject(binding!, new Date('2026-08-27T00:00:00.000Z'))
|
|
26
|
-
expect(projectEnabled(binding)).toBe(true)
|
|
27
|
-
disableProject(binding!)
|
|
28
|
-
expect(projectEnabled(binding)).toBe(false)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('shares one decision across linked worktrees but not unrelated checkouts with the same name', () => {
|
|
32
|
-
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-enablement-git-'))
|
|
33
|
-
const repo = path.join(root, 'same-name')
|
|
34
|
-
const linked = path.join(root, 'linked')
|
|
35
|
-
const unrelatedParent = path.join(root, 'other')
|
|
36
|
-
const unrelated = path.join(unrelatedParent, 'same-name')
|
|
37
|
-
mkdirSync(repo, { recursive: true })
|
|
38
|
-
mkdirSync(unrelated, { recursive: true })
|
|
39
|
-
for (const cwd of [repo, unrelated]) {
|
|
40
|
-
git(cwd, 'init')
|
|
41
|
-
git(cwd, 'config', 'user.email', 'test@example.invalid')
|
|
42
|
-
git(cwd, 'config', 'user.name', 'Test')
|
|
43
|
-
writeFileSync(path.join(cwd, 'file'), 'x')
|
|
44
|
-
git(cwd, 'add', 'file')
|
|
45
|
-
git(cwd, 'commit', '-m', 'fixture')
|
|
46
|
-
}
|
|
47
|
-
git(repo, 'worktree', 'add', linked, '-b', 'linked')
|
|
48
|
-
const env = { XDG_CONFIG_HOME: path.join(root, 'config') }
|
|
49
|
-
const mainBinding = projectBinding(repo, env)
|
|
50
|
-
const linkedBinding = projectBinding(linked, env)
|
|
51
|
-
const unrelatedBinding = projectBinding(unrelated, env)
|
|
52
|
-
expect(linkedBinding?.markerPath).toBe(mainBinding?.markerPath)
|
|
53
|
-
expect(unrelatedBinding?.markerPath).not.toBe(mainBinding?.markerPath)
|
|
54
|
-
})
|
|
55
|
-
})
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process'
|
|
2
|
-
import { createHash } from 'node:crypto'
|
|
3
|
-
import { existsSync, readFileSync, realpathSync, unlinkSync } from 'node:fs'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { atomicWriteFileSync } from './atomic-file.js'
|
|
6
|
-
import { globalConfigDir } from './config.js'
|
|
7
|
-
import { inferInvocationContext } from './invocation-context.js'
|
|
8
|
-
|
|
9
|
-
interface ProjectEnablementMarker {
|
|
10
|
-
version: 1
|
|
11
|
-
project: string
|
|
12
|
-
enabled: true
|
|
13
|
-
updated_at: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ProjectBinding {
|
|
17
|
-
project: string
|
|
18
|
-
markerPath: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function gitCommonDirectory(cwd: string): string | null {
|
|
22
|
-
try {
|
|
23
|
-
const raw = execFileSync('git', ['-C', cwd, 'rev-parse', '--git-common-dir'], {
|
|
24
|
-
encoding: 'utf8',
|
|
25
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
26
|
-
timeout: 2_000,
|
|
27
|
-
windowsHide: true,
|
|
28
|
-
}).trim()
|
|
29
|
-
if (raw === '') return null
|
|
30
|
-
const resolved = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw)
|
|
31
|
-
try {
|
|
32
|
-
return realpathSync(resolved)
|
|
33
|
-
} catch {
|
|
34
|
-
return path.resolve(resolved)
|
|
35
|
-
}
|
|
36
|
-
} catch {
|
|
37
|
-
return null
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Resolve the User-owned enablement record for this exact local Project.
|
|
43
|
-
*
|
|
44
|
-
* The opaque key binds the semantic Project id to Git's shared common
|
|
45
|
-
* directory. Linked worktrees therefore share one decision, while another
|
|
46
|
-
* checkout cannot borrow it merely by committing the same Project id.
|
|
47
|
-
*/
|
|
48
|
-
export function projectBinding(
|
|
49
|
-
cwd: string,
|
|
50
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
51
|
-
explicitProject?: string | null,
|
|
52
|
-
): ProjectBinding | null {
|
|
53
|
-
const project = explicitProject ?? inferInvocationContext(cwd).project
|
|
54
|
-
if (project === null) return null
|
|
55
|
-
const repository = gitCommonDirectory(cwd) ?? path.resolve(cwd)
|
|
56
|
-
const key = createHash('sha256')
|
|
57
|
-
.update('notifai-project-enablement-v1\0')
|
|
58
|
-
.update(repository)
|
|
59
|
-
.update('\0')
|
|
60
|
-
.update(project)
|
|
61
|
-
.digest('hex')
|
|
62
|
-
return {
|
|
63
|
-
project,
|
|
64
|
-
markerPath: path.join(globalConfigDir(env), 'project-enablement', `${key}.json`),
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function projectEnabled(binding: ProjectBinding | null): boolean {
|
|
69
|
-
if (binding === null || !existsSync(binding.markerPath)) return false
|
|
70
|
-
try {
|
|
71
|
-
const marker = JSON.parse(readFileSync(binding.markerPath, 'utf8')) as Partial<ProjectEnablementMarker>
|
|
72
|
-
return marker.version === 1 && marker.enabled === true && marker.project === binding.project
|
|
73
|
-
} catch {
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function enableProject(binding: ProjectBinding, now: Date = new Date()): void {
|
|
79
|
-
const marker: ProjectEnablementMarker = {
|
|
80
|
-
version: 1,
|
|
81
|
-
project: binding.project,
|
|
82
|
-
enabled: true,
|
|
83
|
-
updated_at: now.toISOString(),
|
|
84
|
-
}
|
|
85
|
-
atomicWriteFileSync(binding.markerPath, `${JSON.stringify(marker, null, 2)}\n`, {
|
|
86
|
-
mode: 0o600,
|
|
87
|
-
preserveMode: false,
|
|
88
|
-
requireCurrentUserOwner: true,
|
|
89
|
-
directoryMode: 0o700,
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function disableProject(binding: ProjectBinding): void {
|
|
94
|
-
try {
|
|
95
|
-
unlinkSync(binding.markerPath)
|
|
96
|
-
} catch (err) {
|
|
97
|
-
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
|
|
98
|
-
}
|
|
99
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync } from 'node:fs'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { describe, expect, it } from 'vitest'
|
|
5
|
-
import { MAX_LIVE_QUESTIONS, MAX_PENDING_QUESTIONS, registerQuestion } from './hooks.js'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* `registerQuestion` decides how many questions one session may hold, and these
|
|
9
|
-
* exercise it directly against a bare state directory — no harness, no server,
|
|
10
|
-
* no clock. They live apart from `hooks.test.ts` because nothing here needs
|
|
11
|
-
* what that file spends its setup on.
|
|
12
|
-
*/
|
|
13
|
-
const state = (): NodeJS.ProcessEnv => {
|
|
14
|
-
const dir = mkdtempSync(path.join(os.tmpdir(), 'notifai-caps-'))
|
|
15
|
-
return { HOME: dir, XDG_STATE_HOME: path.join(dir, 'state') } as NodeJS.ProcessEnv
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('how many questions one session may hold', () => {
|
|
19
|
-
it('stops an agent registering a fifth question nobody has asked yet', () => {
|
|
20
|
-
const env = state()
|
|
21
|
-
for (let i = 0; i < MAX_PENDING_QUESTIONS; i += 1) {
|
|
22
|
-
registerQuestion('loop', env, { question: `Q${i}?` })
|
|
23
|
-
}
|
|
24
|
-
expect(() => registerQuestion('loop', env, { question: 'One more?' })).toThrow(
|
|
25
|
-
/already waiting to be asked/,
|
|
26
|
-
)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('does not count a question the user simply has not answered yet', () => {
|
|
30
|
-
// An answer stays accepted for a day. Counting questions already on the
|
|
31
|
-
// user's devices against the loop guard would let a patient user stop the
|
|
32
|
-
// agent from asking anything at all.
|
|
33
|
-
const env = state()
|
|
34
|
-
for (let i = 0; i < MAX_PENDING_QUESTIONS; i += 1) {
|
|
35
|
-
registerQuestion('live', env, { question: `Asked ${i}?`, request_id: `req_${i}` })
|
|
36
|
-
}
|
|
37
|
-
expect(() => registerQuestion('live', env, { question: 'A new one?' })).not.toThrow()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('still bounds what one session can put on the lock screen', () => {
|
|
41
|
-
const env = state()
|
|
42
|
-
for (let i = 0; i < MAX_LIVE_QUESTIONS; i += 1) {
|
|
43
|
-
registerQuestion('flood', env, { question: `Asked ${i}?`, request_id: `req_${i}` })
|
|
44
|
-
}
|
|
45
|
-
expect(() => registerQuestion('flood', env, { question: 'One too many?' })).toThrow(
|
|
46
|
-
/already open/,
|
|
47
|
-
)
|
|
48
|
-
})
|
|
49
|
-
})
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process'
|
|
2
|
-
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import type { HookInstallableHarness } from './harnesses.js'
|
|
4
|
-
import type { HookEnvelope } from './hooks.js'
|
|
5
|
-
|
|
6
|
-
/** Private handoff from the short prompt hook to its detached question owner. */
|
|
7
|
-
export const QUESTION_SETTLEMENT_INPUT_ENV = 'NOTIFAI_INTERNAL_QUESTION_SETTLEMENT_INPUT'
|
|
8
|
-
|
|
9
|
-
export interface QuestionSettlementLaunch {
|
|
10
|
-
envelope: Pick<HookEnvelope, 'session_id' | 'cwd'>
|
|
11
|
-
harness: HookInstallableHarness
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Launch the exact installed CLI build as a detached owner.
|
|
16
|
-
*
|
|
17
|
-
* UserPromptSubmit sits in front of the User's new turn, so it may only pay the
|
|
18
|
-
* process-spawn cost. The child owns submission and the complete answer window.
|
|
19
|
-
*/
|
|
20
|
-
export function spawnQuestionSettlement(launch: QuestionSettlementLaunch): void {
|
|
21
|
-
const child = spawn(
|
|
22
|
-
process.execPath,
|
|
23
|
-
[
|
|
24
|
-
fileURLToPath(new URL('./main.js', import.meta.url)),
|
|
25
|
-
'hook',
|
|
26
|
-
'question-settlement',
|
|
27
|
-
'--owner',
|
|
28
|
-
'notifai',
|
|
29
|
-
'--harness',
|
|
30
|
-
launch.harness,
|
|
31
|
-
],
|
|
32
|
-
{
|
|
33
|
-
cwd: launch.envelope.cwd ?? process.cwd(),
|
|
34
|
-
detached: true,
|
|
35
|
-
env: {
|
|
36
|
-
...process.env,
|
|
37
|
-
[QUESTION_SETTLEMENT_INPUT_ENV]: JSON.stringify(launch.envelope),
|
|
38
|
-
},
|
|
39
|
-
stdio: 'ignore',
|
|
40
|
-
windowsHide: true,
|
|
41
|
-
},
|
|
42
|
-
)
|
|
43
|
-
// Spawn failures are diagnosed by the next ordinary lifecycle hook, which
|
|
44
|
-
// still owns the durable registration. Never turn one into a prompt delay.
|
|
45
|
-
child.once('error', () => undefined)
|
|
46
|
-
child.unref()
|
|
47
|
-
}
|
package/src/question-timing.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { REPLY_MAX_WINDOW_SECONDS } from '@raidiant/notifai-protocol'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Time before reply-window admission that the Stop owner may spend reading its
|
|
5
|
-
* input, serving terminal-first grace, resolving devices, and submitting.
|
|
6
|
-
*
|
|
7
|
-
* Grace is capped at six minutes and every network operation is independently
|
|
8
|
-
* bounded. Twenty minutes lets setup spend up to ten while preserving the
|
|
9
|
-
* separate completion margin below; neither may borrow from the answer window
|
|
10
|
-
* the service actually commits.
|
|
11
|
-
*/
|
|
12
|
-
export const QUESTION_WAITER_STARTUP_HEADROOM_SECONDS = 20 * 60
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Minimum lifetime still available when a question is admitted.
|
|
16
|
-
*
|
|
17
|
-
* The server window starts during submission, not when the hook process
|
|
18
|
-
* started. Keeping ten minutes beyond the requested window covers the bounded
|
|
19
|
-
* submit, clock skew, close fence, and route handoff. If setup used more of the
|
|
20
|
-
* allowance, the frozen question remains local for a successor owner instead
|
|
21
|
-
* of publishing a window this process cannot observe completely.
|
|
22
|
-
*/
|
|
23
|
-
export const QUESTION_SUBMISSION_COMPLETION_HEADROOM_SECONDS = 10 * 60
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The longest one Stop owner may live from process start.
|
|
27
|
-
*
|
|
28
|
-
* The service can accept an answer for up to 72 hours. Starting this budget
|
|
29
|
-
* before submission is deliberate: the startup allowance above ensures that
|
|
30
|
-
* even the maximum reply window still ends before the owner does.
|
|
31
|
-
*/
|
|
32
|
-
export const QUESTION_WAITER_CEILING_SECONDS =
|
|
33
|
-
REPLY_MAX_WINDOW_SECONDS + QUESTION_WAITER_STARTUP_HEADROOM_SECONDS
|
|
34
|
-
|
|
35
|
-
/** Time for final fencing, diagnostics, stdout handoff, and clean process exit. */
|
|
36
|
-
export const QUESTION_STOP_TEARDOWN_HEADROOM_SECONDS = 60
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Declared harness timeout for every Stop handler that owns Question Routing.
|
|
40
|
-
* It must outlive the runtime owner; otherwise the harness silently kills the
|
|
41
|
-
* only process capable of continuing the exact Agent Session.
|
|
42
|
-
*/
|
|
43
|
-
export const QUESTION_STOP_TIMEOUT_SECONDS =
|
|
44
|
-
QUESTION_WAITER_CEILING_SECONDS + QUESTION_STOP_TEARDOWN_HEADROOM_SECONDS
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Short Stop budget for harnesses that cannot own asynchronous Question
|
|
48
|
-
* Routing. They retire/refuse promptly and never wait through an answer window.
|
|
49
|
-
*/
|
|
50
|
-
export const NON_ROUTING_STOP_TIMEOUT_SECONDS = 540
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Pre-PID claim files came only from the former eight-minute owner. Retain that
|
|
54
|
-
* recovery bound instead of making a corrupt legacy file block for 72 hours.
|
|
55
|
-
*/
|
|
56
|
-
export const LEGACY_QUESTION_CLAIM_TTL_SECONDS = 480
|
package/src/readiness.test.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { refreshAfterMenuAction } from './readiness.js'
|
|
3
|
-
|
|
4
|
-
describe('refreshAfterMenuAction', () => {
|
|
5
|
-
it('does not re-assess after doctor, a test send, or a device list', () => {
|
|
6
|
-
expect(refreshAfterMenuAction('doctor', true)).toBeNull()
|
|
7
|
-
expect(refreshAfterMenuAction('test', true)).toBeNull()
|
|
8
|
-
expect(refreshAfterMenuAction('devices', true)).toBeNull()
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
it('does not re-assess when the action changed nothing', () => {
|
|
12
|
-
expect(refreshAfterMenuAction('account', false)).toBeNull()
|
|
13
|
-
expect(refreshAfterMenuAction('settings', false)).toBeNull()
|
|
14
|
-
expect(refreshAfterMenuAction('routing', false)).toBeNull()
|
|
15
|
-
expect(refreshAfterMenuAction('setup', false)).toBeNull()
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('re-probes the service only after an action that can change it', () => {
|
|
19
|
-
expect(refreshAfterMenuAction('setup', true)).toEqual(['local', 'remote'])
|
|
20
|
-
expect(refreshAfterMenuAction('account', true)).toEqual(['local', 'remote'])
|
|
21
|
-
expect(refreshAfterMenuAction('settings', true)).toEqual(['local'])
|
|
22
|
-
expect(refreshAfterMenuAction('routing', true)).toEqual(['local'])
|
|
23
|
-
expect(refreshAfterMenuAction('settings', true, { remote: true })).toEqual(['local', 'remote'])
|
|
24
|
-
})
|
|
25
|
-
})
|
package/src/readiness.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What "set up" means, in one place.
|
|
3
|
-
*
|
|
4
|
-
* `init` and `doctor` used to each carry their own idea of this — a fixed
|
|
5
|
-
* five-step script in one, a flat list of checks in the other — which is two
|
|
6
|
-
* copies of the same knowledge that can disagree, and did: doctor called
|
|
7
|
-
* missing hooks fine while init offered to install them, and only doctor knew
|
|
8
|
-
* what a revoked credential looked like.
|
|
9
|
-
*
|
|
10
|
-
* So the model lives here and both commands render it. `doctor` shows every
|
|
11
|
-
* state at once, because a report is what it is for. `init` walks the same
|
|
12
|
-
* list in order and stops at the first thing standing in the way, because
|
|
13
|
-
* someone handed five tasks does none of them. An agent gets the same states
|
|
14
|
-
* through a third renderer that never prompts.
|
|
15
|
-
*
|
|
16
|
-
* The ordering is a dependency chain, not a preference: there is no point
|
|
17
|
-
* asking the server about devices before there is a credential to ask with,
|
|
18
|
-
* and no point reporting a stale contract to someone who cannot reach the
|
|
19
|
-
* server at all. Assessment stops descending when a prerequisite is missing
|
|
20
|
-
* and marks what follows `unknown` rather than guessing.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/** Whether the gap can be closed from here, and by whom. */
|
|
24
|
-
export type Remedy =
|
|
25
|
-
/**
|
|
26
|
-
* The CLI can do it unattended — no prompt, no human. `command` is how a
|
|
27
|
-
* reader would ask for it later, for the case where it was declined or
|
|
28
|
-
* skipped; it is never needed to perform the action.
|
|
29
|
-
*/
|
|
30
|
-
| { by: 'cli'; summary: string; command?: string }
|
|
31
|
-
/**
|
|
32
|
-
* A human at this terminal. `interactive` marks the ones the CLI can drive
|
|
33
|
-
* on their behalf when someone is actually watching — a browser sign-in is
|
|
34
|
-
* the CLI's to launch but the human's to complete, so an agent must still
|
|
35
|
-
* treat it as out of reach.
|
|
36
|
-
*/
|
|
37
|
-
| {
|
|
38
|
-
by: 'user-here'
|
|
39
|
-
summary: string
|
|
40
|
-
command: string
|
|
41
|
-
interactive?: boolean
|
|
42
|
-
/** Stable agent-facing description of an unavoidable harness-owned action. */
|
|
43
|
-
user_action?: { code: string; harness: string; action: string; message: string }
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* A human somewhere else entirely — phone in hand, app store, permission
|
|
47
|
-
* dialog. The class the old init could only print a sentence about, and the
|
|
48
|
-
* one most likely to end an onboarding.
|
|
49
|
-
*/
|
|
50
|
-
| { by: 'user-elsewhere'; summary: string }
|
|
51
|
-
|
|
52
|
-
export type StateStatus =
|
|
53
|
-
/** Satisfied. */
|
|
54
|
-
| 'ready'
|
|
55
|
-
/** Not satisfied, and it stands in the way. */
|
|
56
|
-
| 'gap'
|
|
57
|
-
/** Not satisfied, and that is a legitimate choice. */
|
|
58
|
-
| 'optional-gap'
|
|
59
|
-
/** A prerequisite is missing, so this was never evaluated. */
|
|
60
|
-
| 'unknown'
|
|
61
|
-
|
|
62
|
-
export interface ReadinessState {
|
|
63
|
-
id: string
|
|
64
|
-
/** Human words for what this is, not the field name. */
|
|
65
|
-
title: string
|
|
66
|
-
status: StateStatus
|
|
67
|
-
/** The current condition, phrased for the human renderer. */
|
|
68
|
-
detail: string
|
|
69
|
-
/** Structured diagnostics emitted only by --json / non-human surfaces. */
|
|
70
|
-
technical?: unknown
|
|
71
|
-
remedy?: Remedy
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface Readiness {
|
|
75
|
-
states: ReadinessState[]
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** The first thing standing in the way, or null when nothing is. */
|
|
79
|
-
export function firstBlocker(readiness: Readiness): ReadinessState | null {
|
|
80
|
-
return readiness.states.find((s) => s.status === 'gap') ?? null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Question Routing: what lets a question reach a phone when nobody is at the
|
|
85
|
-
* terminal. Hooks, their sub-states, and the setting that admits them.
|
|
86
|
-
*
|
|
87
|
-
* Separate from `isOptionalSetup` because these are also the states a human
|
|
88
|
-
* should not be handed as leftovers — "Optional, not set up" is a fair line
|
|
89
|
-
* about the agent skill and a confusing one about a session pointer.
|
|
90
|
-
*/
|
|
91
|
-
export function isOptionalAutomation(id: string): boolean {
|
|
92
|
-
return id === 'hooks' || id.startsWith('hooks-') || id === 'question-routing-settings'
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Automation layered on top of a product that already sends: Question Routing
|
|
97
|
-
* and the agent guidance skill.
|
|
98
|
-
*
|
|
99
|
-
* Real, and reported. `doctor` is the report and judges it strictly. Anything
|
|
100
|
-
* whose job is to get a first notification delivered treats it as a line to
|
|
101
|
-
* print, never a gate — a hook diagnostic or a duplicate skill install in
|
|
102
|
-
* front of the delivery proof means a setup that can already send never proves
|
|
103
|
-
* that it can. The one that fires most often, a turn that has not ended yet,
|
|
104
|
-
* cannot be closed by the agent standing inside that turn at all.
|
|
105
|
-
*/
|
|
106
|
-
export function isOptionalSetup(id: string): boolean {
|
|
107
|
-
return id === 'skill' || isOptionalAutomation(id)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** The first thing in the way that is not optional automation. */
|
|
111
|
-
export function firstRequiredBlocker(readiness: Readiness): ReadinessState | null {
|
|
112
|
-
return readiness.states.find((s) => s.status === 'gap' && !isOptionalSetup(s.id)) ?? null
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Ready enough to be useful, which is not the same as every box ticked.
|
|
117
|
-
*
|
|
118
|
-
* Hooks and the agent skill are genuinely optional — `send` works without
|
|
119
|
-
* them — so an install that declined both is finished, not half-done. Saying
|
|
120
|
-
* otherwise trains people to ignore the summary line, and it is the summary
|
|
121
|
-
* line an agent branches on.
|
|
122
|
-
*/
|
|
123
|
-
export function isReady(readiness: Readiness): boolean {
|
|
124
|
-
return firstRequiredBlocker(readiness) === null
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const SEND_PREREQUISITES = new Set([
|
|
128
|
-
'cli-bin',
|
|
129
|
-
'credential',
|
|
130
|
-
'server',
|
|
131
|
-
'contract',
|
|
132
|
-
'auth',
|
|
133
|
-
'devices',
|
|
134
|
-
])
|
|
135
|
-
|
|
136
|
-
/** Baseline Notification Requests can be submitted in the final assessed state. */
|
|
137
|
-
export function canSend(readiness: Readiness): boolean {
|
|
138
|
-
return [...SEND_PREREQUISITES].every((id) => {
|
|
139
|
-
const state = readiness.states.find((candidate) => candidate.id === id)
|
|
140
|
-
return state?.status === 'ready' || state?.status === 'optional-gap'
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** This exact invocation has a fully evidenced asynchronous question route. */
|
|
145
|
-
export function questionRoutingReady(readiness: Readiness): boolean {
|
|
146
|
-
const relevant = readiness.states.filter(
|
|
147
|
-
(state) =>
|
|
148
|
-
state.id === 'question-routing-settings' ||
|
|
149
|
-
(state.id === 'hooks' || state.id.startsWith('hooks-')) && state.id !== 'hooks-detected',
|
|
150
|
-
)
|
|
151
|
-
return relevant.length > 0 && relevant.every((state) => state.status === 'ready')
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Stable machine renderer shared by init and doctor. */
|
|
155
|
-
export function readinessJson(readiness: Readiness): object {
|
|
156
|
-
return {
|
|
157
|
-
ready: isReady(readiness),
|
|
158
|
-
can_send: canSend(readiness),
|
|
159
|
-
question_routing_ready: questionRoutingReady(readiness),
|
|
160
|
-
states: readiness.states.map((state) => ({
|
|
161
|
-
id: state.id,
|
|
162
|
-
title: state.title,
|
|
163
|
-
status: state.status,
|
|
164
|
-
detail: state.detail,
|
|
165
|
-
technical: state.technical ?? null,
|
|
166
|
-
remedy: state.remedy ?? null,
|
|
167
|
-
})),
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Everything that could still be done, blocking or not, in dependency order. */
|
|
172
|
-
export function openItems(readiness: Readiness): ReadinessState[] {
|
|
173
|
-
return readiness.states.filter((s) => s.status === 'gap' || s.status === 'optional-gap')
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Work an assessment can do.
|
|
178
|
-
*
|
|
179
|
-
* `local` is this machine's files: project config, hook installs, skill lock
|
|
180
|
-
* files. `remote` is everything that leaves the process — the keychain, the
|
|
181
|
-
* service, the account, devices, and delivery evidence. The split exists so a
|
|
182
|
-
* redraw after a settings change does not pay for a health check.
|
|
183
|
-
*/
|
|
184
|
-
export type ReadinessRefresh = 'local' | 'remote'
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* After a menu action, which readiness work is worth doing.
|
|
188
|
-
*
|
|
189
|
-
* `null` means keep the assessment already in hand. Doctor is the report, not
|
|
190
|
-
* a mutation: the check it just ran is the redraw. A test send and a device
|
|
191
|
-
* list cannot change setup. Settings and hook wiring stay on this machine
|
|
192
|
-
* unless the caller says the service URL itself moved.
|
|
193
|
-
*/
|
|
194
|
-
export function refreshAfterMenuAction(
|
|
195
|
-
action: 'setup' | 'account' | 'test' | 'devices' | 'settings' | 'routing' | 'doctor',
|
|
196
|
-
changed: boolean,
|
|
197
|
-
options: { remote?: boolean } = {},
|
|
198
|
-
): readonly ReadinessRefresh[] | null {
|
|
199
|
-
switch (action) {
|
|
200
|
-
case 'doctor':
|
|
201
|
-
case 'test':
|
|
202
|
-
case 'devices':
|
|
203
|
-
return null
|
|
204
|
-
case 'setup':
|
|
205
|
-
case 'account':
|
|
206
|
-
return changed ? ['local', 'remote'] : null
|
|
207
|
-
case 'settings':
|
|
208
|
-
case 'routing':
|
|
209
|
-
if (!changed) return null
|
|
210
|
-
return options.remote === true ? ['local', 'remote'] : ['local']
|
|
211
|
-
}
|
|
212
|
-
}
|
package/src/release.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* What this build is, according to the manifest npm actually shipped.
|
|
7
|
-
*
|
|
8
|
-
* Read from disk rather than compiled in, because a constant baked into
|
|
9
|
-
* `dist/` is only ever as fresh as the last build. A stale `dist/` sitting
|
|
10
|
-
* next to a newer `package.json` is a real failure mode — it produces a CLI
|
|
11
|
-
* that reports the new version perfectly while running the old code — and the
|
|
12
|
-
* only defence that survives it is refusing to carry a second copy of the
|
|
13
|
-
* number at all.
|
|
14
|
-
*
|
|
15
|
-
* The manifest sits one level above both `src/` and `dist/`, so a single
|
|
16
|
-
* relative path resolves in a source checkout and in a published install.
|
|
17
|
-
*
|
|
18
|
-
* Returns null rather than guessing: a build that cannot establish its own
|
|
19
|
-
* identity has to say so, so callers can degrade honestly instead of
|
|
20
|
-
* asserting something they do not know.
|
|
21
|
-
*/
|
|
22
|
-
export function packageVersion(): string | null {
|
|
23
|
-
try {
|
|
24
|
-
const manifest = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
|
|
25
|
-
const parsed: unknown = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
26
|
-
if (typeof parsed === 'object' && parsed !== null) {
|
|
27
|
-
const { version } = parsed as { version?: unknown }
|
|
28
|
-
if (typeof version === 'string' && version.length > 0) return version
|
|
29
|
-
}
|
|
30
|
-
} catch {
|
|
31
|
-
// Fall through: an unreadable manifest must not stop the CLI running.
|
|
32
|
-
}
|
|
33
|
-
return null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Where `npx skills add` fetches the agent skill from, as printed to users.
|
|
38
|
-
*
|
|
39
|
-
* Always an immutable tag naming this exact build. The skill and the CLI ship
|
|
40
|
-
* from one repository at one version, and the skill describes how to drive the
|
|
41
|
-
* CLI — so a moving ref would hand someone guidance for a tool they do not
|
|
42
|
-
* have. Deriving it from the version is what keeps the two from disagreeing;
|
|
43
|
-
* it used to be maintained by hand in three places, and the copies drifted.
|
|
44
|
-
*
|
|
45
|
-
* In the skills CLI grammar `owner/repo#ref` selects a Git ref, while
|
|
46
|
-
* `owner/repo@name` selects a skill — so the `#` here is load-bearing.
|
|
47
|
-
*/
|
|
48
|
-
export function skillsSource(): string | null {
|
|
49
|
-
const version = packageVersion()
|
|
50
|
-
return version === null ? null : `Raidiant-io/notifai#v${version}`
|
|
51
|
-
}
|