@raidiant/notifai 8.0.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -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/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 +71 -15
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.d.ts +7 -3
- package/dist/commands-guidance.js +63 -28
- package/dist/commands-guidance.js.map +1 -1
- package/dist/commands-hooks.d.ts +9 -4
- package/dist/commands-hooks.js +430 -32
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +10 -18
- package/dist/commands-init.js +114 -32
- 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.js +60 -11
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-setup-proof.d.ts +8 -0
- package/dist/commands-setup-proof.js +31 -2
- 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 +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.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 +68 -7
- package/dist/hooks.js +231 -85
- 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 +14 -1
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.js +13 -8
- package/dist/program.js.map +1 -1
- 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 +15 -0
- package/dist/session-activation.js +35 -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/commands-auth.ts +32 -3
- package/src/commands-config.ts +25 -2
- package/src/commands-core.ts +38 -3
- package/src/commands-doctor.ts +99 -13
- package/src/commands-guidance.ts +75 -29
- package/src/commands-hooks.ts +503 -34
- package/src/commands-init.ts +139 -38
- package/src/commands-send-support.test.ts +156 -0
- package/src/commands-send-support.ts +67 -5
- package/src/commands-send.ts +73 -10
- package/src/commands-setup-proof.ts +36 -2
- package/src/commands-skill.ts +78 -15
- package/src/commands.test.ts +1083 -101
- package/src/commands.ts +1 -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.test.ts +42 -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 +746 -165
- package/src/hooks.ts +299 -124
- 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 +14 -1
- package/src/program.test.ts +37 -2
- package/src/program.ts +40 -12
- package/src/send.test.ts +28 -0
- package/src/send.ts +28 -2
- package/src/session-activation.ts +45 -0
- package/src/skill.test.ts +120 -4
- 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 { SESSION_ACTIVATION_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 = 10
|
|
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 SESSION_ACTIVATION_CONTEXT = ${JSON.stringify(SESSION_ACTIVATION_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
|
|
|
@@ -158,6 +160,17 @@ export const NotifAIPlugin = async ({ directory, client }) => {
|
|
|
158
160
|
const cwd = typeof directory === "string" && directory.length > 0 ? directory : process.cwd()
|
|
159
161
|
|
|
160
162
|
return {
|
|
163
|
+
/** Model-visible activation, independent of CLI setup and delivery state. */
|
|
164
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
165
|
+
const sessionID = input?.sessionID
|
|
166
|
+
if (typeof sessionID !== "string" || sessionID.length === 0) return
|
|
167
|
+
if (output.system.length === 0) {
|
|
168
|
+
output.system.push(SESSION_ACTIVATION_CONTEXT)
|
|
169
|
+
} else {
|
|
170
|
+
output.system[0] = output.system[0] + "\\n\\n" + SESSION_ACTIVATION_CONTEXT
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
|
|
161
174
|
/** Exact active-harness identity and first-party title for agent shell commands. */
|
|
162
175
|
"shell.env": async (input, output) => {
|
|
163
176
|
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
|
@@ -35,6 +35,7 @@ import type { Platform } from '@raidiant/notifai-protocol'
|
|
|
35
35
|
import type { SkillScope } from './native-skills.js'
|
|
36
36
|
import { GROUP, SEND_GROUP, helpConfiguration, rootHelpFooter } from './ui/help.js'
|
|
37
37
|
import { readStdinWithTimeout } from './hook-input.js'
|
|
38
|
+
import { argvFlagNames } from './logging.js'
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* One source of truth for the version: the manifest npm actually published.
|
|
@@ -149,7 +150,7 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
149
150
|
if (!deferDiagnostics) {
|
|
150
151
|
logger?.debug('cli.start', {
|
|
151
152
|
version: version(),
|
|
152
|
-
|
|
153
|
+
flags: argvFlagNames(process.argv.slice(2)),
|
|
153
154
|
cwd: process.cwd(),
|
|
154
155
|
})
|
|
155
156
|
}
|
|
@@ -217,12 +218,24 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
217
218
|
.option('--project-id <id>', 'project identifier slug (default: derived from the directory name)')
|
|
218
219
|
.option('--skills', 'install/update the agent skill from its pinned public release')
|
|
219
220
|
.option('--no-skills', 'suppress the optional agent-skill status line')
|
|
220
|
-
.option(
|
|
221
|
+
.option(
|
|
222
|
+
'--setup-scope <scope>',
|
|
223
|
+
'unattended setup scope: project or global (skill, hooks, and config). The CLI binary is always a global install',
|
|
224
|
+
)
|
|
225
|
+
.option('--skills-scope <scope>', 'unattended skill scope; same values as --setup-scope')
|
|
221
226
|
.option('--hooks', 'install harness hooks for registered-question routing')
|
|
222
227
|
.option('--no-hooks', 'skip the hooks without being asked')
|
|
223
|
-
.action(
|
|
224
|
-
|
|
225
|
-
|
|
228
|
+
.action(
|
|
229
|
+
async (opts: {
|
|
230
|
+
projectId?: string
|
|
231
|
+
skills?: boolean
|
|
232
|
+
setupScope?: SkillScope
|
|
233
|
+
skillsScope?: SkillScope
|
|
234
|
+
hooks?: boolean
|
|
235
|
+
}) => {
|
|
236
|
+
exit(await runners.init(deps, opts))
|
|
237
|
+
},
|
|
238
|
+
)
|
|
226
239
|
|
|
227
240
|
program
|
|
228
241
|
.command('doctor')
|
|
@@ -240,7 +253,7 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
240
253
|
.summary('Sign in and pair this machine')
|
|
241
254
|
.description('Pair this machine with your Notifai account via browser approval')
|
|
242
255
|
.option('--name <name>', 'machine name shown in the dashboard (default: hostname)')
|
|
243
|
-
.option('--base-url <url>', '
|
|
256
|
+
.option('--base-url <url>', 'pairing override for the service origin (also NOTIFAI_BASE_URL)')
|
|
244
257
|
.option('--no-open', 'do not open the approval page in a browser')
|
|
245
258
|
.action(async (opts: { name?: string; baseUrl?: string; open?: boolean }) => {
|
|
246
259
|
exit(await runners.login(deps, opts))
|
|
@@ -306,6 +319,10 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
306
319
|
.requiredOption('--title <title>', 'brief title whose substance is immediately understandable')
|
|
307
320
|
.option('--body <markdown>', 'canonical Markdown body')
|
|
308
321
|
.option('--body-file <path>', 'read the canonical Markdown body from a file (use - for stdin)')
|
|
322
|
+
.option(
|
|
323
|
+
'--literal-backslash-n',
|
|
324
|
+
'allow the two-character sequence \\n in --body instead of treating it as a mistaken escaped newline',
|
|
325
|
+
)
|
|
309
326
|
.option('--subtitle <subtitle>', 'a plain-text line between the title and body')
|
|
310
327
|
.option(
|
|
311
328
|
'--image <path|url|media_id>',
|
|
@@ -318,7 +335,7 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
318
335
|
(v: string, all: string[] = []) => [...all, v],
|
|
319
336
|
)
|
|
320
337
|
.optionsGroup(SEND_GROUP.routing)
|
|
321
|
-
.option('--kind <kind>', 'what this is (required): update | done | failed | blocked — question is set by --reply')
|
|
338
|
+
.option('--kind <kind>', 'what this is (required): update | done | failed | blocked (no User reply would resume it) — question is set by --reply')
|
|
322
339
|
.option('--project <id>', 'project identifier override (otherwise configured or inferred)')
|
|
323
340
|
.option('--device <id>', 'target a device id (repeatable)', (v: string, all: string[] = []) => [...all, v])
|
|
324
341
|
.option('--all', 'target all routable devices (overrides configured devices)')
|
|
@@ -327,7 +344,6 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
327
344
|
'--session-label <text>',
|
|
328
345
|
'first human session name, frozen locally (env: NOTIFAI_SESSION_LABEL)',
|
|
329
346
|
)
|
|
330
|
-
.option('--event <event>', 'agent event name, e.g. tests_passed')
|
|
331
347
|
.optionsGroup(SEND_GROUP.presentation)
|
|
332
348
|
.option('--collapse-key <key>', 'replace earlier notifications with the same key')
|
|
333
349
|
.option('--thread-id <id>', 'group related notifications')
|
|
@@ -357,7 +373,10 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
357
373
|
.option('--no-wait', 'return immediately after acceptance')
|
|
358
374
|
.option('--data <key=value>', 'custom data (repeatable)', (v: string, all: string[] = []) => [...all, v])
|
|
359
375
|
.option('--idempotency-key <key>', 'safe-retry key (default: random)')
|
|
360
|
-
.option(
|
|
376
|
+
.option(
|
|
377
|
+
'--base-url <url>',
|
|
378
|
+
'pairing override for the service origin; ignored when this machine is already paired (also NOTIFAI_BASE_URL)',
|
|
379
|
+
)
|
|
361
380
|
.option('--json', 'print the full submission receipt as JSON')
|
|
362
381
|
.action(async (opts: Record<string, unknown>) => {
|
|
363
382
|
// commander maps --no-wait onto the same "wait" flag; disentangle.
|
|
@@ -443,6 +462,10 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
443
462
|
.option('--json', 'machine-readable output')
|
|
444
463
|
.option('--body <markdown>', 'optional Markdown context appended after the question block')
|
|
445
464
|
.option('--body-file <path>', 'read optional Markdown context from a file (use - for stdin)')
|
|
465
|
+
.option(
|
|
466
|
+
'--literal-backslash-n',
|
|
467
|
+
'allow the two-character sequence \\n in --body instead of treating it as a mistaken escaped newline',
|
|
468
|
+
)
|
|
446
469
|
.option('--form <path>', 'ask several questions as one form; JSON file (use - for stdin)')
|
|
447
470
|
.option(
|
|
448
471
|
'--image <path|url|media_id>',
|
|
@@ -466,6 +489,7 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
466
489
|
json?: boolean
|
|
467
490
|
body?: string
|
|
468
491
|
bodyFile?: string
|
|
492
|
+
literalBackslashN?: boolean
|
|
469
493
|
form?: string
|
|
470
494
|
image?: string[]
|
|
471
495
|
imageAlt?: string[]
|
|
@@ -507,17 +531,21 @@ export function buildProgram(deps: CommandDeps, options: BuildProgramOptions = {
|
|
|
507
531
|
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
508
532
|
...(opts.sessionLabel !== undefined ? { sessionLabel: opts.sessionLabel } : {}),
|
|
509
533
|
...(opts.json === true ? { json: true } : {}),
|
|
534
|
+
...(opts.literalBackslashN === true ? { literalBackslashN: true } : {}),
|
|
510
535
|
}
|
|
511
536
|
exit(await runners.ask(deps, question, flags))
|
|
512
537
|
})
|
|
513
538
|
|
|
514
539
|
program
|
|
515
|
-
.command('close
|
|
540
|
+
.command('close [request_id]')
|
|
516
541
|
.helpGroup(GROUP.agent)
|
|
517
542
|
.summary('Retire a question so late answers are rejected')
|
|
518
|
-
.description(
|
|
543
|
+
.description(
|
|
544
|
+
'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.',
|
|
545
|
+
)
|
|
546
|
+
.option('--pending', 'retire this session\'s outstanding questions, including ones not yet pushed')
|
|
519
547
|
.option('--json', 'machine-readable output')
|
|
520
|
-
.action(async (requestId: string, opts: { json?: boolean }) => {
|
|
548
|
+
.action(async (requestId: string | undefined, opts: { json?: boolean; pending?: boolean }) => {
|
|
521
549
|
exit(await runners.close(deps, requestId, opts))
|
|
522
550
|
})
|
|
523
551
|
|
package/src/send.test.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
accidentalEscapedNewlineMessage,
|
|
4
|
+
hasAccidentalEscapedNewlines,
|
|
5
|
+
rejectAccidentalEscapedNewlines,
|
|
6
|
+
} from './send.js'
|
|
7
|
+
|
|
8
|
+
describe('accidental escaped newlines', () => {
|
|
9
|
+
it('catches a shell-quoted backslash-n body', () => {
|
|
10
|
+
expect(hasAccidentalEscapedNewlines('line1\\nline2')).toBe(true)
|
|
11
|
+
expect(rejectAccidentalEscapedNewlines('line1\\nline2', false)).toBe(
|
|
12
|
+
accidentalEscapedNewlineMessage(),
|
|
13
|
+
)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('preserves real multiline Markdown', () => {
|
|
17
|
+
expect(hasAccidentalEscapedNewlines('line1\nline2')).toBe(false)
|
|
18
|
+
expect(rejectAccidentalEscapedNewlines('line1\nline2', false)).toBeNull()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('lets an explicit flag keep visible backslash-n text', () => {
|
|
22
|
+
expect(rejectAccidentalEscapedNewlines('use \\n in the sample', true)).toBeNull()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('does not treat an escaped backslash plus n as accidental', () => {
|
|
26
|
+
expect(hasAccidentalEscapedNewlines('escape as \\\\n')).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
})
|
package/src/send.ts
CHANGED
|
@@ -31,7 +31,6 @@ export interface SendFlags {
|
|
|
31
31
|
/** The one canonical Markdown body. */
|
|
32
32
|
body: string
|
|
33
33
|
subtitle?: string
|
|
34
|
-
event?: string
|
|
35
34
|
/** What this notification is; absent means update. */
|
|
36
35
|
kind?: string
|
|
37
36
|
project?: string
|
|
@@ -80,6 +79,34 @@ export interface SendFlags {
|
|
|
80
79
|
* questions and their retirements ride the wire as what they are.
|
|
81
80
|
*/
|
|
82
81
|
lifecycle?: LifecycleT
|
|
82
|
+
/**
|
|
83
|
+
* Allow the two-character sequence backslash-n to be sent as visible text.
|
|
84
|
+
* Without this, a likely accidental escaped newline is refused.
|
|
85
|
+
*/
|
|
86
|
+
literalBackslashN?: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A backslash-n pair that was not itself escaped as a literal backslash. */
|
|
90
|
+
const ACCIDENTAL_ESCAPED_NEWLINE = /(?:^|[^\\])\\n/
|
|
91
|
+
|
|
92
|
+
export function hasAccidentalEscapedNewlines(body: string): boolean {
|
|
93
|
+
return ACCIDENTAL_ESCAPED_NEWLINE.test(body)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function accidentalEscapedNewlineMessage(): string {
|
|
97
|
+
return [
|
|
98
|
+
'This body contains the two-character sequence \\n, which is sent literally and shows as \\n on the device.',
|
|
99
|
+
'To send a real line break, pass a multiline --body or --body-file -.',
|
|
100
|
+
'If you meant the visible characters \\n, pass --literal-backslash-n.',
|
|
101
|
+
].join('\n')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function rejectAccidentalEscapedNewlines(
|
|
105
|
+
body: string | undefined,
|
|
106
|
+
allowLiteral: boolean | undefined,
|
|
107
|
+
): string | null {
|
|
108
|
+
if (allowLiteral === true || body === undefined) return null
|
|
109
|
+
return hasAccidentalEscapedNewlines(body) ? accidentalEscapedNewlineMessage() : null
|
|
83
110
|
}
|
|
84
111
|
|
|
85
112
|
export type DraftBuild =
|
|
@@ -309,7 +336,6 @@ export function buildDraft(
|
|
|
309
336
|
|
|
310
337
|
const draft: NotificationDraftT = {
|
|
311
338
|
schema_version: NOTIFICATION_SCHEMA_VERSION,
|
|
312
|
-
...(flags.event !== undefined ? { event: flags.event } : {}),
|
|
313
339
|
...(flags.kind !== undefined
|
|
314
340
|
? { kind: flags.kind as (typeof NOTIFICATION_KINDS)[number] }
|
|
315
341
|
: {}),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { HookHarness } from './hooks.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Model-visible session bootstrap. It names the installed capability and its
|
|
5
|
+
* authoritative sources without restating any notification policy that the
|
|
6
|
+
* user may have overridden.
|
|
7
|
+
*/
|
|
8
|
+
export const SESSION_ACTIVATION_CONTEXT =
|
|
9
|
+
'Notifai is active for this session, even when the user did not mention it. ' +
|
|
10
|
+
'Before beginning task work, use the Notifai skill and run `notifai guidance`; its resolved guidance governs notification decisions for Agent Events. ' +
|
|
11
|
+
'Missing skill or CLI readiness is handled through `notifai init` and its diagnosis. ' +
|
|
12
|
+
'For delegated work, the parent owns User-visible Notification Requests unless it explicitly delegates that ownership; workers report Agent Events to the parent and do not send independently.'
|
|
13
|
+
|
|
14
|
+
/** One lifecycle meaning, encoded for each harness output contract. */
|
|
15
|
+
export function sessionActivationOutput(
|
|
16
|
+
harness: HookHarness | undefined,
|
|
17
|
+
hookEventName: 'SessionStart' | 'SubagentStart' | 'UserPromptSubmit',
|
|
18
|
+
): string | undefined {
|
|
19
|
+
if (harness === 'cursor') {
|
|
20
|
+
return JSON.stringify({ additional_context: SESSION_ACTIVATION_CONTEXT })
|
|
21
|
+
}
|
|
22
|
+
if (harness === 'claude-code' || harness === 'codex') {
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
hookSpecificOutput: {
|
|
25
|
+
hookEventName,
|
|
26
|
+
additionalContext: SESSION_ACTIVATION_CONTEXT,
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cursor currently accepts sessionStart context without reliably delivering it
|
|
35
|
+
* to the model. Its native post-Stop follow-up is the narrow fallback and is
|
|
36
|
+
* claimed once per conversation by the hook-state layer.
|
|
37
|
+
*/
|
|
38
|
+
export function cursorStopActivationOutput(): string {
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
followup_message: SESSION_ACTIVATION_CONTEXT.replace(
|
|
41
|
+
'Before beginning task work,',
|
|
42
|
+
'Before finalizing the Agent Event from the turn that just ended,',
|
|
43
|
+
),
|
|
44
|
+
})
|
|
45
|
+
}
|