@raidiant/notifai 8.0.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +20 -13
- package/dist/atomic-file.d.ts +14 -0
- package/dist/atomic-file.js +27 -2
- package/dist/atomic-file.js.map +1 -1
- package/dist/cli-bin.d.ts +4 -0
- package/dist/cli-bin.js +62 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli-release.d.ts +10 -0
- package/dist/cli-release.js +75 -0
- package/dist/cli-release.js.map +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +13 -3
- package/dist/client.js.map +1 -1
- package/dist/command-session.d.ts +14 -0
- package/dist/command-session.js +30 -0
- package/dist/command-session.js.map +1 -0
- package/dist/commands-auth.d.ts +6 -0
- package/dist/commands-auth.js +28 -3
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-config.js +22 -3
- package/dist/commands-config.js.map +1 -1
- package/dist/commands-core.d.ts +12 -0
- package/dist/commands-core.js +27 -3
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-doctor.d.ts +1 -0
- package/dist/commands-doctor.js +87 -29
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.d.ts +7 -3
- package/dist/commands-guidance.js +40 -31
- package/dist/commands-guidance.js.map +1 -1
- package/dist/commands-harness-context.js +21 -1
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hooks.d.ts +20 -5
- package/dist/commands-hooks.js +509 -155
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +12 -18
- package/dist/commands-init.js +149 -47
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.d.ts +17 -2
- package/dist/commands-send-support.js +61 -5
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.d.ts +1 -0
- package/dist/commands-send.js +114 -17
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-setup-proof.d.ts +12 -3
- package/dist/commands-setup-proof.js +60 -16
- package/dist/commands-setup-proof.js.map +1 -1
- package/dist/commands-skill.d.ts +11 -1
- package/dist/commands-skill.js +61 -11
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +25 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +24 -3
- package/dist/config.js +37 -5
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +10 -1
- package/dist/credentials.js +25 -12
- package/dist/credentials.js.map +1 -1
- package/dist/guidance-content.d.ts +20 -0
- package/dist/guidance-content.js +73 -8
- package/dist/guidance-content.js.map +1 -1
- package/dist/guidance-render.d.ts +20 -0
- package/dist/guidance-render.js +42 -0
- package/dist/guidance-render.js.map +1 -0
- package/dist/guidance.d.ts +21 -0
- package/dist/guidance.js +73 -18
- package/dist/guidance.js.map +1 -1
- package/dist/hook-adapter.js +37 -3
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +83 -7
- package/dist/hooks.js +288 -90
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +10 -18
- package/dist/install-hooks.js +110 -23
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +1 -1
- package/dist/interactive.js.map +1 -1
- package/dist/logging.d.ts +8 -1
- package/dist/logging.js +43 -2
- package/dist/logging.js.map +1 -1
- package/dist/main.js +2 -13
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +19 -0
- package/dist/native-skills.js +30 -8
- package/dist/native-skills.js.map +1 -1
- package/dist/opencode-plugin.js +25 -1
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.js +34 -20
- package/dist/program.js.map +1 -1
- package/dist/readiness.d.ts +6 -0
- package/dist/readiness.js +37 -0
- package/dist/readiness.js.map +1 -1
- package/dist/send-attempts.d.ts +26 -0
- package/dist/send-attempts.js +139 -0
- package/dist/send-attempts.js.map +1 -0
- package/dist/send.d.ts +8 -1
- package/dist/send.js +17 -1
- package/dist/send.js.map +1 -1
- package/dist/session-activation.d.ts +11 -0
- package/dist/session-activation.js +46 -0
- package/dist/session-activation.js.map +1 -0
- package/dist/url-policy.d.ts +85 -0
- package/dist/url-policy.js +369 -0
- package/dist/url-policy.js.map +1 -0
- package/package.json +3 -3
- package/src/atomic-file.ts +36 -2
- package/src/cli-bin.test.ts +38 -0
- package/src/cli-bin.ts +67 -0
- package/src/cli-release.test.ts +46 -0
- package/src/cli-release.ts +78 -0
- package/src/client.test.ts +95 -0
- package/src/client.ts +16 -3
- package/src/command-session.ts +38 -0
- package/src/commands-auth.ts +32 -3
- package/src/commands-config.ts +25 -2
- package/src/commands-core.ts +38 -3
- package/src/commands-doctor.ts +117 -41
- package/src/commands-guidance.ts +43 -32
- package/src/commands-harness-context.ts +24 -1
- package/src/commands-hooks.ts +704 -188
- package/src/commands-init.ts +180 -57
- package/src/commands-send-support.test.ts +156 -0
- package/src/commands-send-support.ts +67 -5
- package/src/commands-send.ts +135 -16
- package/src/commands-setup-proof.test.ts +72 -0
- package/src/commands-setup-proof.ts +69 -16
- package/src/commands-skill.ts +78 -15
- package/src/commands.test.ts +1281 -157
- package/src/commands.ts +2 -0
- package/src/config-schema.ts +28 -1
- package/src/config.test.ts +1 -1
- package/src/config.ts +49 -5
- package/src/credentials.test.ts +71 -3
- package/src/credentials.ts +29 -15
- package/src/guidance-content.ts +74 -8
- package/src/guidance-render.ts +56 -0
- package/src/guidance.test.ts +70 -11
- package/src/guidance.ts +100 -18
- package/src/hook-adapter.test.ts +113 -1
- package/src/hook-adapter.ts +37 -3
- package/src/hooks.test.ts +778 -166
- package/src/hooks.ts +368 -129
- package/src/install-hooks.test.ts +135 -16
- package/src/install-hooks.ts +113 -25
- package/src/interactive.ts +1 -1
- package/src/logging.test.ts +66 -3
- package/src/logging.ts +45 -2
- package/src/main.ts +2 -14
- package/src/native-skills.test.ts +41 -1
- package/src/native-skills.ts +40 -6
- package/src/opencode-plugin.ts +25 -1
- package/src/program.test.ts +37 -2
- package/src/program.ts +62 -24
- package/src/readiness.ts +44 -0
- package/src/send-attempts.test.ts +143 -0
- package/src/send-attempts.ts +188 -0
- package/src/send.test.ts +28 -0
- package/src/send.ts +28 -2
- package/src/session-activation.ts +62 -0
- package/src/skill.test.ts +125 -11
- package/src/trust-policy.test.ts +701 -0
- package/src/url-policy.ts +399 -0
package/src/main.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { realIo, type CommandDeps } from './commands.js'
|
|
3
3
|
import { defaultCredentialStore } from './credentials.js'
|
|
4
4
|
import { nativeSkills } from './native-skills.js'
|
|
5
|
-
import { bootstrapLogger } from './logging.js'
|
|
5
|
+
import { argvFlagNames, bootstrapLogger } from './logging.js'
|
|
6
6
|
import { buildProgram } from './program.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -24,24 +24,12 @@ const deps: CommandDeps = {
|
|
|
24
24
|
logger,
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* Which flags were passed, without their values.
|
|
29
|
-
*
|
|
30
|
-
* The flag names answer nearly every question worth asking of an invocation —
|
|
31
|
-
* was `--reply` set, was `--all` — while the values are notification content
|
|
32
|
-
* and user text that has no business being recorded merely because a command
|
|
33
|
-
* ran. Values are available under `log_level = debug`, which is a deliberate act.
|
|
34
|
-
*/
|
|
35
|
-
function flagNames(argv: readonly string[]): string[] {
|
|
36
|
-
return argv.filter((token) => token.startsWith('--'))
|
|
37
|
-
}
|
|
38
|
-
|
|
39
27
|
const startedAt = Date.now()
|
|
40
28
|
process.on('exit', (code) => {
|
|
41
29
|
logger.info('cli.end', {
|
|
42
30
|
exit: code,
|
|
43
31
|
duration_ms: Date.now() - startedAt,
|
|
44
|
-
flags:
|
|
32
|
+
flags: argvFlagNames(process.argv.slice(2)),
|
|
45
33
|
})
|
|
46
34
|
})
|
|
47
35
|
|
|
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { describe, expect, it } from 'vitest'
|
|
5
|
-
import { nativeSkills } from './native-skills.js'
|
|
5
|
+
import { SKILLS_INSTALLER_SPEC, nativeSkills, skillsAddArgv, skillsRemoveArgv } from './native-skills.js'
|
|
6
6
|
|
|
7
7
|
function writeLock(file: string, ref: string): void {
|
|
8
8
|
mkdirSync(path.dirname(file), { recursive: true })
|
|
@@ -22,6 +22,46 @@ function writeLock(file: string, ref: string): void {
|
|
|
22
22
|
)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
describe('skillsAddArgv', () => {
|
|
26
|
+
it('pins the installer package to an exact reviewed version', () => {
|
|
27
|
+
const argv = skillsAddArgv({
|
|
28
|
+
source: 'Raidiant-io/notifai#v8.0.0',
|
|
29
|
+
skill: 'notifai',
|
|
30
|
+
cwd: '/tmp',
|
|
31
|
+
env: {},
|
|
32
|
+
})
|
|
33
|
+
expect(SKILLS_INSTALLER_SPEC).toMatch(/^skills@\d+\.\d+\.\d+$/)
|
|
34
|
+
expect(argv).toEqual([
|
|
35
|
+
'-y',
|
|
36
|
+
SKILLS_INSTALLER_SPEC,
|
|
37
|
+
'add',
|
|
38
|
+
'Raidiant-io/notifai#v8.0.0',
|
|
39
|
+
'--skill',
|
|
40
|
+
'notifai',
|
|
41
|
+
])
|
|
42
|
+
expect(argv).not.toContain('skills')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('uninstalls one skill in the named scope without a prompt', () => {
|
|
46
|
+
expect(
|
|
47
|
+
skillsRemoveArgv({
|
|
48
|
+
skill: 'notifai',
|
|
49
|
+
scope: 'project',
|
|
50
|
+
cwd: '/tmp',
|
|
51
|
+
env: {},
|
|
52
|
+
}),
|
|
53
|
+
).toEqual(['-y', SKILLS_INSTALLER_SPEC, 'remove', 'notifai', '--yes'])
|
|
54
|
+
expect(
|
|
55
|
+
skillsRemoveArgv({
|
|
56
|
+
skill: 'notifai',
|
|
57
|
+
scope: 'global',
|
|
58
|
+
cwd: '/tmp',
|
|
59
|
+
env: {},
|
|
60
|
+
}),
|
|
61
|
+
).toEqual(['-y', SKILLS_INSTALLER_SPEC, 'remove', 'notifai', '--global', '--yes'])
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
25
65
|
describe('nativeSkills.list', () => {
|
|
26
66
|
it('reads the project lock file instead of spawning npx', async () => {
|
|
27
67
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-skills-project-'))
|
package/src/native-skills.ts
CHANGED
|
@@ -3,6 +3,14 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { accountHome, npxLaunch } from './platform.js'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Exact reviewed version of the external `skills` installer.
|
|
8
|
+
*
|
|
9
|
+
* The GitHub skill source stays the immutable CLI version tag. The installer
|
|
10
|
+
* program that fetches it must not float on `latest`.
|
|
11
|
+
*/
|
|
12
|
+
export const SKILLS_INSTALLER_SPEC = 'skills@1.5.23'
|
|
13
|
+
|
|
6
14
|
/** The two scopes offered by the skills installer. */
|
|
7
15
|
export type SkillScope = 'project' | 'global'
|
|
8
16
|
|
|
@@ -30,9 +38,18 @@ export interface SkillsAddOptions {
|
|
|
30
38
|
env: NodeJS.ProcessEnv
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
export interface SkillsRemoveOptions {
|
|
42
|
+
skill: string
|
|
43
|
+
scope: SkillScope
|
|
44
|
+
cwd: string
|
|
45
|
+
env: NodeJS.ProcessEnv
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
export interface NativeSkills {
|
|
34
49
|
/** Launch the native interactive `npx skills add` flow. */
|
|
35
50
|
add(options: SkillsAddOptions): Promise<number>
|
|
51
|
+
/** Uninstall one installer-managed skill in one scope. */
|
|
52
|
+
remove(options: SkillsRemoveOptions): Promise<number>
|
|
36
53
|
/** Read installer-managed inventory from lock files. Does not spawn npx. */
|
|
37
54
|
list(scope: SkillScope, cwd: string, env: NodeJS.ProcessEnv): Promise<SkillsListResult>
|
|
38
55
|
}
|
|
@@ -108,15 +125,32 @@ function run(args: string[], options: { cwd: string; env: NodeJS.ProcessEnv }):
|
|
|
108
125
|
})
|
|
109
126
|
}
|
|
110
127
|
|
|
128
|
+
/** argv for `npx`, including the pinned installer spec. */
|
|
129
|
+
export function skillsAddArgv(options: SkillsAddOptions): string[] {
|
|
130
|
+
const args = ['-y', SKILLS_INSTALLER_SPEC, 'add', options.source, '--skill', options.skill]
|
|
131
|
+
if (options.scope === 'global') args.push('--global')
|
|
132
|
+
// An explicit scope is the unattended contract. Native `--yes` keeps all
|
|
133
|
+
// remaining installer prompts non-interactive after the scope is chosen.
|
|
134
|
+
if (options.scope !== undefined) args.push('--yes')
|
|
135
|
+
return args
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** argv for uninstalling one skill in one installer scope. */
|
|
139
|
+
export function skillsRemoveArgv(options: SkillsRemoveOptions): string[] {
|
|
140
|
+
const args = ['-y', SKILLS_INSTALLER_SPEC, 'remove', options.skill]
|
|
141
|
+
if (options.scope === 'global') args.push('--global')
|
|
142
|
+
args.push('--yes')
|
|
143
|
+
return args
|
|
144
|
+
}
|
|
145
|
+
|
|
111
146
|
/** The only process/filesystem adapter Notifai needs for the external installer. */
|
|
112
147
|
export const nativeSkills: NativeSkills = {
|
|
113
148
|
async add(options) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return run(args, { cwd: options.cwd, env: options.env })
|
|
149
|
+
return run(skillsAddArgv(options), { cwd: options.cwd, env: options.env })
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async remove(options) {
|
|
153
|
+
return run(skillsRemoveArgv(options), { cwd: options.cwd, env: options.env })
|
|
120
154
|
},
|
|
121
155
|
|
|
122
156
|
async list(scope, cwd, env) {
|
package/src/opencode-plugin.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { hookHostPlatform, type HookHostPlatform } from './hook-adapter.js'
|
|
3
3
|
import { configHome } from './install-hooks.js'
|
|
4
|
+
import { MISSING_LIFECYCLE_GUIDANCE_CONTEXT } from './session-activation.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The OpenCode adapter.
|
|
@@ -31,7 +32,7 @@ export const OPENCODE_PLUGIN_MARKER = '// notifai managed opencode plugin'
|
|
|
31
32
|
export const OPENCODE_PLUGIN_FILENAME = 'notifai.js'
|
|
32
33
|
|
|
33
34
|
/** Bump when an installed generated file must be rewritten to remain functional. */
|
|
34
|
-
const OPENCODE_ADAPTER_VERSION =
|
|
35
|
+
const OPENCODE_ADAPTER_VERSION = 11
|
|
35
36
|
|
|
36
37
|
export function opencodePluginDir(
|
|
37
38
|
global: boolean,
|
|
@@ -103,6 +104,7 @@ import { spawn } from "node:child_process"
|
|
|
103
104
|
${nodeConstant}const ADAPTER = ${JSON.stringify(adapterPath)}
|
|
104
105
|
const TIMEOUT_MS = ${timeoutSeconds * 1000}
|
|
105
106
|
const ADAPTER_VERSION = ${OPENCODE_ADAPTER_VERSION}
|
|
107
|
+
const MISSING_LIFECYCLE_GUIDANCE_CONTEXT = ${JSON.stringify(MISSING_LIFECYCLE_GUIDANCE_CONTEXT)}
|
|
106
108
|
const PENDING_SESSION_TITLE =
|
|
107
109
|
/^New session(?: *- *[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:[.][0-9]+)?Z)?$/i
|
|
108
110
|
|
|
@@ -156,8 +158,30 @@ function runHook(event, envelope) {
|
|
|
156
158
|
|
|
157
159
|
export const NotifAIPlugin = async ({ directory, client }) => {
|
|
158
160
|
const cwd = typeof directory === "string" && directory.length > 0 ? directory : process.cwd()
|
|
161
|
+
const activated = new Set()
|
|
159
162
|
|
|
160
163
|
return {
|
|
164
|
+
/** Model-visible activation, independent of CLI setup and delivery state. */
|
|
165
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
166
|
+
const sessionID = input?.sessionID
|
|
167
|
+
if (typeof sessionID !== "string" || sessionID.length === 0) return
|
|
168
|
+
if (activated.has(sessionID)) return
|
|
169
|
+
const resolved = await runHook("session-start", {
|
|
170
|
+
session_id: sessionID,
|
|
171
|
+
cwd,
|
|
172
|
+
hook_event_name: "SessionStart",
|
|
173
|
+
})
|
|
174
|
+
const context = typeof resolved === "string" && resolved.trim().length > 0
|
|
175
|
+
? resolved.trim()
|
|
176
|
+
: MISSING_LIFECYCLE_GUIDANCE_CONTEXT
|
|
177
|
+
if (output.system.length === 0) {
|
|
178
|
+
output.system.push(context)
|
|
179
|
+
} else {
|
|
180
|
+
output.system[0] = output.system[0] + "\\n\\n" + context
|
|
181
|
+
}
|
|
182
|
+
activated.add(sessionID)
|
|
183
|
+
},
|
|
184
|
+
|
|
161
185
|
/** Exact active-harness identity and first-party title for agent shell commands. */
|
|
162
186
|
"shell.env": async (input, output) => {
|
|
163
187
|
output.env.NOTIFAI_ACTIVE_HARNESS = "opencode"
|
package/src/program.test.ts
CHANGED
|
@@ -82,7 +82,6 @@ describe('program argv parsing', () => {
|
|
|
82
82
|
'--multi',
|
|
83
83
|
'--reply',
|
|
84
84
|
'--reply-timeout', '30',
|
|
85
|
-
'--event', 'tests_passed',
|
|
86
85
|
],
|
|
87
86
|
{ send: (async (_deps, flags) => ((seen = flags as Record<string, unknown>), 0)) as ProgramRunners['send'] },
|
|
88
87
|
)
|
|
@@ -95,7 +94,6 @@ describe('program argv parsing', () => {
|
|
|
95
94
|
multi: true,
|
|
96
95
|
reply: true,
|
|
97
96
|
replyTimeout: 30,
|
|
98
|
-
event: 'tests_passed',
|
|
99
97
|
noWait: false,
|
|
100
98
|
})
|
|
101
99
|
})
|
|
@@ -113,6 +111,16 @@ describe('program argv parsing', () => {
|
|
|
113
111
|
expect(seen).not.toHaveProperty('wait')
|
|
114
112
|
})
|
|
115
113
|
|
|
114
|
+
it('maps init --setup-scope onto the command flags', async () => {
|
|
115
|
+
let seen: Record<string, unknown> | undefined
|
|
116
|
+
const { exitCode } = await parse(
|
|
117
|
+
['init', '--skills', '--setup-scope', 'global', '--no-hooks'],
|
|
118
|
+
{ init: (async (_deps, flags) => ((seen = flags as Record<string, unknown>), 0)) as ProgramRunners['init'] },
|
|
119
|
+
)
|
|
120
|
+
expect(exitCode).toBe(0)
|
|
121
|
+
expect(seen).toMatchObject({ skills: true, setupScope: 'global', hooks: false })
|
|
122
|
+
})
|
|
123
|
+
|
|
116
124
|
it('disentangles --no-wait and --wait from their shared commander flag', async () => {
|
|
117
125
|
const capture = (sink: Record<string, unknown>[]): ProgramRunners['send'] =>
|
|
118
126
|
(async (_deps, flags) => (sink.push(flags as Record<string, unknown>), 0)) as ProgramRunners['send']
|
|
@@ -191,6 +199,15 @@ describe('program argv parsing', () => {
|
|
|
191
199
|
expect(deps.errLines.join('\n')).toContain('send')
|
|
192
200
|
})
|
|
193
201
|
|
|
202
|
+
it('rejects send --event now that the public event field is gone', async () => {
|
|
203
|
+
const { exitCode, deps } = await parse(
|
|
204
|
+
['send', '--kind', 'done', '--title', 'T', '--body', 'B', '--event', 'tests_passed'],
|
|
205
|
+
{ send: (async () => 0) as ProgramRunners['send'] },
|
|
206
|
+
)
|
|
207
|
+
expect(exitCode).toBe(2)
|
|
208
|
+
expect(deps.errLines).toHaveLength(0)
|
|
209
|
+
})
|
|
210
|
+
|
|
194
211
|
it('drops the empty --event collector for logs', async () => {
|
|
195
212
|
let seen: Record<string, unknown> | undefined
|
|
196
213
|
const logs = ((_deps, flags) => ((seen = flags as Record<string, unknown>), 0)) as ProgramRunners['logs']
|
|
@@ -199,4 +216,22 @@ describe('program argv parsing', () => {
|
|
|
199
216
|
await parse(['logs', '--event', 'send.submitted'], { logs })
|
|
200
217
|
expect(seen).toMatchObject({ event: ['send.submitted'] })
|
|
201
218
|
})
|
|
219
|
+
|
|
220
|
+
it('passes close --pending without inventing a request id', async () => {
|
|
221
|
+
let seenId: string | undefined
|
|
222
|
+
let seenFlags: Record<string, unknown> | undefined
|
|
223
|
+
const close = (async (_deps, requestId, flags) => {
|
|
224
|
+
seenId = requestId
|
|
225
|
+
seenFlags = flags as Record<string, unknown>
|
|
226
|
+
return 0
|
|
227
|
+
}) as ProgramRunners['close']
|
|
228
|
+
|
|
229
|
+
await parse(['close', '--pending', '--json'], { close })
|
|
230
|
+
expect(seenId).toBeUndefined()
|
|
231
|
+
expect(seenFlags).toEqual({ pending: true, json: true })
|
|
232
|
+
|
|
233
|
+
await parse(['close', 'req_abc'], { close })
|
|
234
|
+
expect(seenId).toBe('req_abc')
|
|
235
|
+
expect(seenFlags).toEqual({})
|
|
236
|
+
})
|
|
202
237
|
})
|
package/src/program.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
logoutCommand,
|
|
26
26
|
logsCommand,
|
|
27
27
|
repliesCommand,
|
|
28
|
+
reportAskFailure,
|
|
28
29
|
sendCommand,
|
|
29
30
|
statusCommand,
|
|
30
31
|
type CommandDeps,
|
|
@@ -35,6 +36,7 @@ import type { Platform } from '@raidiant/notifai-protocol'
|
|
|
35
36
|
import type { SkillScope } from './native-skills.js'
|
|
36
37
|
import { GROUP, SEND_GROUP, helpConfiguration, rootHelpFooter } from './ui/help.js'
|
|
37
38
|
import { readStdinWithTimeout } from './hook-input.js'
|
|
39
|
+
import { argvFlagNames } from './logging.js'
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* One source of truth for the version: the manifest npm actually published.
|
|
@@ -149,7 +151,7 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
149
151
|
if (!deferDiagnostics) {
|
|
150
152
|
logger?.debug('cli.start', {
|
|
151
153
|
version: version(),
|
|
152
|
-
|
|
154
|
+
flags: argvFlagNames(process.argv.slice(2)),
|
|
153
155
|
cwd: process.cwd(),
|
|
154
156
|
})
|
|
155
157
|
}
|
|
@@ -215,14 +217,28 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
215
217
|
'Interactive at a human terminal; never prompts otherwise (agents: pass flags)',
|
|
216
218
|
)
|
|
217
219
|
.option('--project-id <id>', 'project identifier slug (default: derived from the directory name)')
|
|
220
|
+
.option('--json', 'machine-readable final readiness; never prompts')
|
|
218
221
|
.option('--skills', 'install/update the agent skill from its pinned public release')
|
|
219
222
|
.option('--no-skills', 'suppress the optional agent-skill status line')
|
|
220
|
-
.option(
|
|
223
|
+
.option(
|
|
224
|
+
'--setup-scope <scope>',
|
|
225
|
+
'unattended setup scope: project or global (skill, hooks, and config). The CLI binary is always a global install',
|
|
226
|
+
)
|
|
227
|
+
.option('--skills-scope <scope>', 'unattended skill scope; same values as --setup-scope')
|
|
221
228
|
.option('--hooks', 'install harness hooks for registered-question routing')
|
|
222
229
|
.option('--no-hooks', 'skip the hooks without being asked')
|
|
223
|
-
.action(
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
.action(
|
|
231
|
+
async (opts: {
|
|
232
|
+
projectId?: string
|
|
233
|
+
json?: boolean
|
|
234
|
+
skills?: boolean
|
|
235
|
+
setupScope?: SkillScope
|
|
236
|
+
skillsScope?: SkillScope
|
|
237
|
+
hooks?: boolean
|
|
238
|
+
}) => {
|
|
239
|
+
exit(await runners.init(deps, opts))
|
|
240
|
+
},
|
|
241
|
+
)
|
|
226
242
|
|
|
227
243
|
program
|
|
228
244
|
.command('doctor')
|
|
@@ -240,7 +256,7 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
240
256
|
.summary('Sign in and pair this machine')
|
|
241
257
|
.description('Pair this machine with your Notifai account via browser approval')
|
|
242
258
|
.option('--name <name>', 'machine name shown in the dashboard (default: hostname)')
|
|
243
|
-
.option('--base-url <url>', '
|
|
259
|
+
.option('--base-url <url>', 'pairing override for the service origin (also NOTIFAI_BASE_URL)')
|
|
244
260
|
.option('--no-open', 'do not open the approval page in a browser')
|
|
245
261
|
.action(async (opts: { name?: string; baseUrl?: string; open?: boolean }) => {
|
|
246
262
|
exit(await runners.login(deps, opts))
|
|
@@ -306,6 +322,10 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
306
322
|
.requiredOption('--title <title>', 'brief title whose substance is immediately understandable')
|
|
307
323
|
.option('--body <markdown>', 'canonical Markdown body')
|
|
308
324
|
.option('--body-file <path>', 'read the canonical Markdown body from a file (use - for stdin)')
|
|
325
|
+
.option(
|
|
326
|
+
'--literal-backslash-n',
|
|
327
|
+
'allow the two-character sequence \\n in --body instead of treating it as a mistaken escaped newline',
|
|
328
|
+
)
|
|
309
329
|
.option('--subtitle <subtitle>', 'a plain-text line between the title and body')
|
|
310
330
|
.option(
|
|
311
331
|
'--image <path|url|media_id>',
|
|
@@ -318,16 +338,15 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
318
338
|
(v: string, all: string[] = []) => [...all, v],
|
|
319
339
|
)
|
|
320
340
|
.optionsGroup(SEND_GROUP.routing)
|
|
321
|
-
.option('--kind <kind>', 'what this is (required): update | done | failed | blocked — question is set by --reply')
|
|
341
|
+
.option('--kind <kind>', 'what this is (required): update | done | failed | blocked (no User reply would resume it) — question is set by --reply')
|
|
322
342
|
.option('--project <id>', 'project identifier override (otherwise configured or inferred)')
|
|
323
343
|
.option('--device <id>', 'target a device id (repeatable)', (v: string, all: string[] = []) => [...all, v])
|
|
324
344
|
.option('--all', 'target all routable devices (overrides configured devices)')
|
|
325
|
-
.option('--session-id <id>', 'opaque exact
|
|
345
|
+
.option('--session-id <id>', 'low-level automation override for an opaque exact session (env: NOTIFAI_SESSION_ID)')
|
|
326
346
|
.option(
|
|
327
347
|
'--session-label <text>',
|
|
328
|
-
'
|
|
348
|
+
'human session name; safe to repeat, with the first accepted name frozen (env: NOTIFAI_SESSION_LABEL)',
|
|
329
349
|
)
|
|
330
|
-
.option('--event <event>', 'agent event name, e.g. tests_passed')
|
|
331
350
|
.optionsGroup(SEND_GROUP.presentation)
|
|
332
351
|
.option('--collapse-key <key>', 'replace earlier notifications with the same key')
|
|
333
352
|
.option('--thread-id <id>', 'group related notifications')
|
|
@@ -357,7 +376,11 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
357
376
|
.option('--no-wait', 'return immediately after acceptance')
|
|
358
377
|
.option('--data <key=value>', 'custom data (repeatable)', (v: string, all: string[] = []) => [...all, v])
|
|
359
378
|
.option('--idempotency-key <key>', 'safe-retry key (default: random)')
|
|
360
|
-
.option('--
|
|
379
|
+
.option('--retry', 'explicitly retry one unresolved exact semantic send using its opaque saved attempt')
|
|
380
|
+
.option(
|
|
381
|
+
'--base-url <url>',
|
|
382
|
+
'pairing override for the service origin; ignored when this machine is already paired (also NOTIFAI_BASE_URL)',
|
|
383
|
+
)
|
|
361
384
|
.option('--json', 'print the full submission receipt as JSON')
|
|
362
385
|
.action(async (opts: Record<string, unknown>) => {
|
|
363
386
|
// commander maps --no-wait onto the same "wait" flag; disentangle.
|
|
@@ -443,6 +466,10 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
443
466
|
.option('--json', 'machine-readable output')
|
|
444
467
|
.option('--body <markdown>', 'optional Markdown context appended after the question block')
|
|
445
468
|
.option('--body-file <path>', 'read optional Markdown context from a file (use - for stdin)')
|
|
469
|
+
.option(
|
|
470
|
+
'--literal-backslash-n',
|
|
471
|
+
'allow the two-character sequence \\n in --body instead of treating it as a mistaken escaped newline',
|
|
472
|
+
)
|
|
446
473
|
.option('--form <path>', 'ask several questions as one form; JSON file (use - for stdin)')
|
|
447
474
|
.option(
|
|
448
475
|
'--image <path|url|media_id>',
|
|
@@ -455,10 +482,9 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
455
482
|
(v: string, all: string[] = []) => [...all, v],
|
|
456
483
|
)
|
|
457
484
|
.option('--project <id>', 'project identifier override (otherwise configured or inferred)')
|
|
458
|
-
.option('--session-id <id>', 'opaque exact-session override (env: NOTIFAI_SESSION_ID)')
|
|
459
485
|
.option(
|
|
460
486
|
'--session-label <text>',
|
|
461
|
-
'
|
|
487
|
+
'human session name; safe to repeat, with the first accepted name frozen (env: NOTIFAI_SESSION_LABEL)',
|
|
462
488
|
)
|
|
463
489
|
.action(async (question: string | undefined, opts: {
|
|
464
490
|
choice?: string[]
|
|
@@ -466,24 +492,30 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
466
492
|
json?: boolean
|
|
467
493
|
body?: string
|
|
468
494
|
bodyFile?: string
|
|
495
|
+
literalBackslashN?: boolean
|
|
469
496
|
form?: string
|
|
470
497
|
image?: string[]
|
|
471
498
|
imageAlt?: string[]
|
|
472
499
|
project?: string
|
|
473
|
-
sessionId?: string
|
|
474
500
|
sessionLabel?: string
|
|
475
501
|
}) => {
|
|
476
502
|
let body = opts.body
|
|
477
503
|
if (typeof opts.bodyFile === 'string') {
|
|
478
504
|
if (body !== undefined) {
|
|
479
|
-
deps
|
|
480
|
-
|
|
505
|
+
return exit(reportAskFailure(deps, opts, {
|
|
506
|
+
code: 'invalid_input', check_id: 'body', exit_code: 2,
|
|
507
|
+
remedy: 'pass either --body or --body-file, then retry',
|
|
508
|
+
message: 'Pass either --body or --body-file, not both.',
|
|
509
|
+
}))
|
|
481
510
|
}
|
|
482
511
|
try {
|
|
483
512
|
body = readFileSync(opts.bodyFile === '-' ? 0 : opts.bodyFile, 'utf8')
|
|
484
513
|
} catch (err) {
|
|
485
|
-
deps
|
|
486
|
-
|
|
514
|
+
return exit(reportAskFailure(deps, opts, {
|
|
515
|
+
code: 'input_unreadable', check_id: 'body_file', exit_code: 2,
|
|
516
|
+
remedy: 'fix the body-file path or permissions, then retry',
|
|
517
|
+
message: `Could not read ${opts.bodyFile}: ${String(err)}`,
|
|
518
|
+
}))
|
|
487
519
|
}
|
|
488
520
|
}
|
|
489
521
|
let form: string | undefined
|
|
@@ -491,8 +523,11 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
491
523
|
try {
|
|
492
524
|
form = readFileSync(opts.form === '-' ? 0 : opts.form, 'utf8')
|
|
493
525
|
} catch (err) {
|
|
494
|
-
deps
|
|
495
|
-
|
|
526
|
+
return exit(reportAskFailure(deps, opts, {
|
|
527
|
+
code: 'input_unreadable', check_id: 'form_file', exit_code: 2,
|
|
528
|
+
remedy: 'fix the form-file path or permissions, then retry',
|
|
529
|
+
message: `Could not read ${opts.form}: ${String(err)}`,
|
|
530
|
+
}))
|
|
496
531
|
}
|
|
497
532
|
}
|
|
498
533
|
// Commander collectors default to []; an empty list means "not passed".
|
|
@@ -504,20 +539,23 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
504
539
|
...(opts.image?.length ? { image: opts.image } : {}),
|
|
505
540
|
...(opts.imageAlt?.length ? { imageAlt: opts.imageAlt } : {}),
|
|
506
541
|
...(opts.project !== undefined ? { project: opts.project } : {}),
|
|
507
|
-
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
508
542
|
...(opts.sessionLabel !== undefined ? { sessionLabel: opts.sessionLabel } : {}),
|
|
509
543
|
...(opts.json === true ? { json: true } : {}),
|
|
544
|
+
...(opts.literalBackslashN === true ? { literalBackslashN: true } : {}),
|
|
510
545
|
}
|
|
511
546
|
exit(await runners.ask(deps, question, flags))
|
|
512
547
|
})
|
|
513
548
|
|
|
514
549
|
program
|
|
515
|
-
.command('close
|
|
550
|
+
.command('close [request_id]')
|
|
516
551
|
.helpGroup(GROUP.agent)
|
|
517
552
|
.summary('Retire a question so late answers are rejected')
|
|
518
|
-
.description(
|
|
553
|
+
.description(
|
|
554
|
+
'Retire a question so late answers are rejected rather than lost. Pass --pending to withdraw this session\'s outstanding registrations, including ones the turn-end hook has not pushed yet.',
|
|
555
|
+
)
|
|
556
|
+
.option('--pending', 'retire this session\'s outstanding questions, including ones not yet pushed')
|
|
519
557
|
.option('--json', 'machine-readable output')
|
|
520
|
-
.action(async (requestId: string, opts: { json?: boolean }) => {
|
|
558
|
+
.action(async (requestId: string | undefined, opts: { json?: boolean; pending?: boolean }) => {
|
|
521
559
|
exit(await runners.close(deps, requestId, opts))
|
|
522
560
|
})
|
|
523
561
|
|
package/src/readiness.ts
CHANGED
|
@@ -84,6 +84,50 @@ export function isReady(readiness: Readiness): boolean {
|
|
|
84
84
|
return firstBlocker(readiness) === null
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
const SEND_PREREQUISITES = new Set([
|
|
88
|
+
'cli-bin',
|
|
89
|
+
'credential',
|
|
90
|
+
'server',
|
|
91
|
+
'contract',
|
|
92
|
+
'auth',
|
|
93
|
+
'devices',
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
/** Baseline Notification Requests can be submitted in the final assessed state. */
|
|
97
|
+
export function canSend(readiness: Readiness): boolean {
|
|
98
|
+
return [...SEND_PREREQUISITES].every((id) => {
|
|
99
|
+
const state = readiness.states.find((candidate) => candidate.id === id)
|
|
100
|
+
return state?.status === 'ready' || state?.status === 'optional-gap'
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** This exact invocation has a fully evidenced asynchronous question route. */
|
|
105
|
+
export function questionRoutingReady(readiness: Readiness): boolean {
|
|
106
|
+
const relevant = readiness.states.filter(
|
|
107
|
+
(state) =>
|
|
108
|
+
state.id === 'question-routing-settings' ||
|
|
109
|
+
(state.id === 'hooks' || state.id.startsWith('hooks-')) && state.id !== 'hooks-detected',
|
|
110
|
+
)
|
|
111
|
+
return relevant.length > 0 && relevant.every((state) => state.status === 'ready')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Stable machine renderer shared by init and doctor. */
|
|
115
|
+
export function readinessJson(readiness: Readiness): object {
|
|
116
|
+
return {
|
|
117
|
+
ready: isReady(readiness),
|
|
118
|
+
can_send: canSend(readiness),
|
|
119
|
+
question_routing_ready: questionRoutingReady(readiness),
|
|
120
|
+
states: readiness.states.map((state) => ({
|
|
121
|
+
id: state.id,
|
|
122
|
+
title: state.title,
|
|
123
|
+
status: state.status,
|
|
124
|
+
detail: state.detail,
|
|
125
|
+
technical: state.technical ?? null,
|
|
126
|
+
remedy: state.remedy ?? null,
|
|
127
|
+
})),
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
87
131
|
/** Everything that could still be done, blocking or not, in dependency order. */
|
|
88
132
|
export function openItems(readiness: Readiness): ReadinessState[] {
|
|
89
133
|
return readiness.states.filter((s) => s.status === 'gap' || s.status === 'optional-gap')
|