@raidiant/notifai 10.1.0 → 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 +37 -0
- package/README.md +3 -0
- package/dist/cli-bin.d.ts +21 -1
- package/dist/cli-bin.js +141 -10
- package/dist/cli-bin.js.map +1 -1
- package/dist/cli-update-contract.d.ts +7 -0
- package/dist/cli-update-contract.js +10 -0
- package/dist/cli-update-contract.js.map +1 -0
- package/dist/client.d.ts +6 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/codex-wake.d.ts +2 -0
- package/dist/codex-wake.js +2 -1
- package/dist/codex-wake.js.map +1 -1
- package/dist/commands-auth.d.ts +15 -1
- package/dist/commands-auth.js +29 -11
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-core.d.ts +9 -2
- package/dist/commands-core.js +11 -4
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-devices.d.ts +10 -1
- package/dist/commands-devices.js +17 -13
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.js +93 -49
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-hooks.js +47 -10
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +16 -0
- package/dist/commands-init.js +254 -93
- 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-send.d.ts +1 -1
- package/dist/commands-send.js +121 -35
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-skill.js +72 -54
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands-update.d.ts +10 -0
- package/dist/commands-update.js +141 -0
- package/dist/commands-update.js.map +1 -0
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +2 -1
- package/dist/commands.js.map +1 -1
- package/dist/hook-adapter.d.ts +2 -0
- package/dist/hook-adapter.js +12 -1
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +43 -2
- package/dist/hooks.js +163 -21
- package/dist/hooks.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/main-run.d.ts +1 -0
- package/dist/main-run.js +42 -0
- package/dist/main-run.js.map +1 -0
- package/dist/main.js +10 -31
- package/dist/main.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 +19 -0
- package/dist/node-floor.js +39 -0
- package/dist/node-floor.js.map +1 -0
- 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/program.d.ts +2 -1
- package/dist/program.js +22 -5
- package/dist/program.js.map +1 -1
- package/dist/question-settlement-process.d.ts +15 -0
- package/dist/question-settlement-process.js +35 -0
- package/dist/question-settlement-process.js.map +1 -0
- package/dist/readiness.d.ts +25 -1
- package/dist/readiness.js +32 -2
- package/dist/readiness.js.map +1 -1
- package/dist/release.d.ts +5 -5
- package/dist/release.js +5 -5
- package/dist/setup-destinations.d.ts +23 -0
- package/dist/setup-destinations.js +44 -0
- package/dist/setup-destinations.js.map +1 -0
- 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 -38
- package/src/cli-bin.ts +0 -67
- package/src/cli-release.test.ts +0 -46
- package/src/cli-release.ts +0 -78
- package/src/client.test.ts +0 -390
- package/src/client.ts +0 -345
- package/src/codex-wake.test.ts +0 -254
- package/src/codex-wake.ts +0 -297
- package/src/command-session.ts +0 -38
- package/src/commands-agent-sessions.ts +0 -72
- package/src/commands-auth.ts +0 -205
- package/src/commands-config.ts +0 -336
- package/src/commands-core.ts +0 -298
- package/src/commands-devices.test.ts +0 -15
- package/src/commands-devices.ts +0 -173
- package/src/commands-doctor.ts +0 -1440
- 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 -2268
- package/src/commands-init.ts +0 -878
- 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 -769
- 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.test.ts +0 -7828
- package/src/commands.ts +0 -75
- 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 -410
- package/src/hook-input.test.ts +0 -39
- package/src/hook-input.ts +0 -72
- package/src/hooks.test.ts +0 -5549
- package/src/hooks.ts +0 -3759
- 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.ts +0 -36
- package/src/native-skills.test.ts +0 -115
- package/src/native-skills.ts +0 -162
- 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 -282
- package/src/program.ts +0 -818
- 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-timing.ts +0 -56
- package/src/readiness.test.ts +0 -25
- package/src/readiness.ts +0 -179
- 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/skill.test.ts +0 -415
- package/src/sound-ref.test.ts +0 -31
- package/src/sound-ref.ts +0 -34
- package/src/trust-policy.test.ts +0 -701
- package/src/ui/banner.ts +0 -95
- package/src/ui/config-view.test.ts +0 -90
- package/src/ui/config-view.ts +0 -138
- package/src/ui/help.test.ts +0 -16
- package/src/ui/help.ts +0 -128
- package/src/ui/theme.test.ts +0 -92
- package/src/ui/theme.ts +0 -265
- package/src/url-policy.ts +0 -399
- package/tsconfig.json +0 -10
package/dist/commands-init.js
CHANGED
|
@@ -7,17 +7,20 @@ import { ensurePrivateDirectory } from './atomic-file.js';
|
|
|
7
7
|
import { ApiCallError, NetworkError } from './client.js';
|
|
8
8
|
import { personalProjectConfigPath } from './config.js';
|
|
9
9
|
import { projectSlugFrom as inferredProjectSlugFrom } from './invocation-context.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import {} from './native-skills.js';
|
|
11
|
+
import { firstRequiredBlocker, isOptionalAutomation, isOptionalSetup, questionRoutingReady, readinessJson, } from './readiness.js';
|
|
12
12
|
import { buildDraft, formatReceipt } from './send.js';
|
|
13
13
|
import { loginCommand } from './commands-auth.js';
|
|
14
|
-
import { EXIT, SETUP_COMMAND,
|
|
15
|
-
import { readyCompanionDevices
|
|
14
|
+
import { EXIT, SETUP_COMMAND, authedClient, loadLoggedConfig, reportError, updateCliCommand, } from './commands-core.js';
|
|
15
|
+
import { readyCompanionDevices } from './commands-devices.js';
|
|
16
|
+
import { companionPlatformLabel, setupCompanionUrl, } from './setup-destinations.js';
|
|
16
17
|
import { assessReadiness, remedyLine } from './commands-doctor.js';
|
|
17
18
|
import { hooksInstallCommand, pickHarnessesToInstall } from './commands-hooks.js';
|
|
18
19
|
import { observedCompanionReceipt, readSetupProof, setupProofProject, setupProofApplies, setupProofIsStale, writeSetupProof, } from './commands-setup-proof.js';
|
|
19
20
|
import { listScopedNotifaiSkills, SKILLS_SOURCE } from './commands-skill.js';
|
|
20
21
|
import { enableProject, projectBinding } from './project-enablement.js';
|
|
22
|
+
import { inspectCliInstallations } from './cli-bin.js';
|
|
23
|
+
import { installHookAdapter } from './hook-adapter.js';
|
|
21
24
|
// ---------------------------------------------------------------------------
|
|
22
25
|
// init
|
|
23
26
|
// ---------------------------------------------------------------------------
|
|
@@ -36,8 +39,10 @@ function formatWaitBudget(milliseconds) {
|
|
|
36
39
|
}
|
|
37
40
|
async function closeGap(deps, state, flags) {
|
|
38
41
|
if (state.id === 'project') {
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
// Naming this checkout is local bookkeeping, not skill or hook placement,
|
|
43
|
+
// so it does not wait on the scope question. Without an explicit answer the
|
|
44
|
+
// name lands in the checkout it describes, which is the answer a User who
|
|
45
|
+
// never sees the question would have given.
|
|
41
46
|
const slug = projectSlugFrom(flags.projectId ?? path.basename(deps.cwd));
|
|
42
47
|
const configPath = flags.setupScope === 'global'
|
|
43
48
|
? personalProjectConfigPath(deps.cwd, deps.env)
|
|
@@ -73,13 +78,27 @@ async function closeGap(deps, state, flags) {
|
|
|
73
78
|
}
|
|
74
79
|
return ok ? 'closed' : 'failed';
|
|
75
80
|
}
|
|
81
|
+
if (state.id === 'hooks-adapter') {
|
|
82
|
+
const effective = inspectCliInstallations(deps.env, deps.hookPlatform ?? process.platform).effective;
|
|
83
|
+
if (effective === null || effective.artifact_path === null)
|
|
84
|
+
return 'failed';
|
|
85
|
+
try {
|
|
86
|
+
installHookAdapter({ execPath: process.execPath, scriptPath: effective.artifact_path }, deps.hookAdapterHome, deps.hookPlatform ?? process.platform);
|
|
87
|
+
return 'closed';
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
deps.io.err(`Could not retarget Question Routing: ${String(err)}`);
|
|
91
|
+
return 'failed';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
76
94
|
if (state.id === 'skill') {
|
|
77
95
|
if (deps.nativeSkills === undefined) {
|
|
78
96
|
deps.io.err('Skill installation failed — the native `npx skills` flow is unavailable.');
|
|
79
97
|
return 'failed';
|
|
80
98
|
}
|
|
81
|
-
// Refuse rather than
|
|
82
|
-
//
|
|
99
|
+
// Refuse rather than guess a release identity. The production adapter uses
|
|
100
|
+
// this only as a human label; it verifies and stages npm's bundled skill at
|
|
101
|
+
// a project-relative local path before it invokes the installer.
|
|
83
102
|
if (SKILLS_SOURCE === null) {
|
|
84
103
|
deps.io.err('Skill installation failed — this build cannot determine its own version, so there is no release tag to install from.');
|
|
85
104
|
return 'failed';
|
|
@@ -104,16 +123,18 @@ async function closeGap(deps, state, flags) {
|
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
deps.io.out(`Starting the native npx skills setup for the notifai agent skill (${installScope} scope)...`);
|
|
107
|
-
const
|
|
126
|
+
const operation = await deps.nativeSkills.add({
|
|
108
127
|
source: SKILLS_SOURCE,
|
|
109
128
|
skill: 'notifai',
|
|
110
129
|
cwd: deps.cwd,
|
|
111
130
|
env: deps.env,
|
|
112
131
|
scope: installScope,
|
|
113
132
|
});
|
|
133
|
+
const code = typeof operation === 'number' ? operation : operation.code;
|
|
114
134
|
if (code !== 0) {
|
|
115
|
-
deps.io.err(
|
|
116
|
-
|
|
135
|
+
deps.io.err(typeof operation === 'number'
|
|
136
|
+
? 'Skill installation failed — rerun `notifai init --skills` after checking the network connection.'
|
|
137
|
+
: `Skill installation refused — ${operation.error}.`);
|
|
117
138
|
}
|
|
118
139
|
return code === 0 ? 'closed' : 'failed';
|
|
119
140
|
}
|
|
@@ -133,12 +154,42 @@ function deviceBridgeMessage(devices) {
|
|
|
133
154
|
return `Waiting for ${undecided.display_name} to allow the notification prompt…`;
|
|
134
155
|
return 'Waiting for a Companion device to become ready…';
|
|
135
156
|
}
|
|
157
|
+
/** How many Companion Apps this Account has registered, on any platform. */
|
|
158
|
+
async function registeredCompanions(client) {
|
|
159
|
+
try {
|
|
160
|
+
const response = await client.listDevices();
|
|
161
|
+
return response.devices.filter((device) => device.platform === 'ios' || device.platform === 'android').length;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Unreachable here means unreachable for the wait that follows; ask, so a
|
|
165
|
+
// User with nothing installed still gets the platform-specific steps.
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Which phone the notifications should arrive on.
|
|
171
|
+
*
|
|
172
|
+
* Asked in the User's words, here, so the destination this terminal opens is
|
|
173
|
+
* already about their phone rather than an index they have to choose from
|
|
174
|
+
* again on the other screen. A run with no human gets nothing — it opens no
|
|
175
|
+
* browser and answers no question — so there is no prompt to hang on.
|
|
176
|
+
*/
|
|
177
|
+
async function askCompanionPlatform(deps) {
|
|
178
|
+
if (deps.io.interactive !== true || deps.io.select === undefined)
|
|
179
|
+
return null;
|
|
180
|
+
const selected = await deps.io.select('Where do you want to receive notifications?', [
|
|
181
|
+
{ value: 'iphone', label: 'iPhone' },
|
|
182
|
+
{ value: 'android', label: 'Android' },
|
|
183
|
+
]);
|
|
184
|
+
return selected === 'iphone' || selected === 'android' ? selected : null;
|
|
185
|
+
}
|
|
136
186
|
/**
|
|
137
187
|
* Observe the supported Device Installation path while the user finishes the
|
|
138
|
-
* app-side work. The
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* never wait on
|
|
188
|
+
* app-side work. The bridge is one focused setup destination for the platform
|
|
189
|
+
* they just named — not the omnibus help page, whose own next step is to go
|
|
190
|
+
* somewhere else. Interactive runs offer to open it so the user never types
|
|
191
|
+
* the URL; non-interactive/agent paths only print plain text and never wait on
|
|
192
|
+
* a prompt.
|
|
142
193
|
*
|
|
143
194
|
* The wait budget is stated up front. On expiry we say the *timer* expired —
|
|
144
195
|
* not the setup — and offer another budget when a human is present. Agents
|
|
@@ -156,20 +207,29 @@ async function waitForReadyDevice(deps, state) {
|
|
|
156
207
|
return 'failed';
|
|
157
208
|
}
|
|
158
209
|
const budgetLabel = formatWaitBudget(DEVICE_BRIDGE_TIMEOUT_MS);
|
|
159
|
-
|
|
210
|
+
// A phone that is registered but silent needs its permission allowed, not
|
|
211
|
+
// install steps — and asking which phone would be asking about one they are
|
|
212
|
+
// already holding. Only a User with nothing registered has a platform still
|
|
213
|
+
// to name, and that is read from the account rather than from the wording of
|
|
214
|
+
// the state that got us here.
|
|
215
|
+
const platform = (await registeredCompanions(authed.client)) === 0
|
|
216
|
+
? await askCompanionPlatform(deps)
|
|
217
|
+
: null;
|
|
218
|
+
const setupUrl = setupCompanionUrl(authed.baseUrl, platform ?? undefined);
|
|
219
|
+
const stepsLabel = platform === null ? 'Setup steps (no typing)' : `${companionPlatformLabel(platform)} steps (no typing)`;
|
|
160
220
|
await deps.io.note?.([
|
|
161
221
|
state.detail,
|
|
162
222
|
remedy.summary,
|
|
163
|
-
|
|
164
|
-
`I will wait up to ${budgetLabel} for a Companion
|
|
165
|
-
].join('\n'), 'Finish setup on your
|
|
166
|
-
// Open the
|
|
167
|
-
//
|
|
168
|
-
if (await deps.io.confirm('Open
|
|
169
|
-
deps.io.openUrl(
|
|
170
|
-
}
|
|
171
|
-
if (!(await deps.io.confirm('Wait here while you finish that on your
|
|
172
|
-
deps.io.out(`OK — finish
|
|
223
|
+
`${stepsLabel}: ${setupUrl}`,
|
|
224
|
+
`I will wait up to ${budgetLabel} for a Companion App to become ready.`,
|
|
225
|
+
].join('\n'), 'Finish setup on your phone');
|
|
226
|
+
// Open the destination so the user never has to type the URL. Decline is
|
|
227
|
+
// fine — the URL remains in the note and in the Next: line if they leave.
|
|
228
|
+
if (await deps.io.confirm('Open those steps in your browser?', true)) {
|
|
229
|
+
deps.io.openUrl(setupUrl);
|
|
230
|
+
}
|
|
231
|
+
if (!(await deps.io.confirm('Wait here while you finish that on your phone?', true))) {
|
|
232
|
+
deps.io.out(`OK — finish that when you can (steps: ${setupUrl}), then re-run \`notifai init\`.`);
|
|
173
233
|
return 'pending';
|
|
174
234
|
}
|
|
175
235
|
const now = deps.now ?? Date.now;
|
|
@@ -207,30 +267,48 @@ async function waitForReadyDevice(deps, state) {
|
|
|
207
267
|
spinner?.error(`The ${budgetLabel} wait timer expired`);
|
|
208
268
|
deps.io.err(`The ${budgetLabel} wait timer expired — setup is not finished, only this wait.`);
|
|
209
269
|
deps.io.err(deviceBridgeMessage(lastDevices).replace(/…$/, '.'));
|
|
210
|
-
deps.io.err(`Re-run \`notifai init\` later and it will pick up from here (
|
|
270
|
+
deps.io.err(`Re-run \`notifai init\` later and it will pick up from here (steps: ${setupUrl}).`);
|
|
211
271
|
const keepWaiting = await deps.io.confirm(`Keep waiting for another ${budgetLabel}?`, false);
|
|
212
272
|
if (!keepWaiting) {
|
|
213
273
|
deps.io.out('Stopping the wait. Companion setup can continue; re-run `notifai init` when ready.');
|
|
214
274
|
return 'pending';
|
|
215
275
|
}
|
|
216
|
-
spinner?.message(`Waiting another ${budgetLabel} for a Companion
|
|
276
|
+
spinner?.message(`Waiting another ${budgetLabel} for a Companion App…`);
|
|
217
277
|
deadline = now() + DEVICE_BRIDGE_TIMEOUT_MS;
|
|
218
278
|
}
|
|
219
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* The first notification a User ever receives from Notifai.
|
|
282
|
+
*
|
|
283
|
+
* It is the payoff of the whole setup, and it used to be written as an
|
|
284
|
+
* internal receipt: "This real notification completed setup verification" — a
|
|
285
|
+
* sentence about the check rather than about them — delivered silently, at
|
|
286
|
+
* `passive`, with the sound turned off. So the one arrival that proves their
|
|
287
|
+
* agents can reach them landed in Notification Center with no banner and no
|
|
288
|
+
* sound, where a first-time User has no reason to look and every reason to
|
|
289
|
+
* conclude that nothing came.
|
|
290
|
+
*
|
|
291
|
+
* It now says what is true for them and arrives the way their notifications
|
|
292
|
+
* will. `done` is the honest kind — a body of work finished successfully — and
|
|
293
|
+
* it is also what lets the server pick the sound through its own kind, Project
|
|
294
|
+
* and Account layers instead of this command stamping one and skipping them.
|
|
295
|
+
*/
|
|
220
296
|
function setupProofDraft(config, device) {
|
|
221
297
|
const project = config.project.value;
|
|
222
298
|
return buildDraft(config, {
|
|
223
|
-
title: '
|
|
299
|
+
title: 'Your agents can reach you',
|
|
224
300
|
body: project === null
|
|
225
|
-
? '
|
|
226
|
-
: `
|
|
227
|
-
kind: '
|
|
301
|
+
? 'Notifai setup is finished. Notifications from your agents will arrive like this one.'
|
|
302
|
+
: `Notifai setup is finished for ${project}. Notifications from your agents will arrive like this one.`,
|
|
303
|
+
kind: 'done',
|
|
228
304
|
platform: device.platform,
|
|
229
305
|
device: [device.device_id],
|
|
230
|
-
|
|
306
|
+
// Noticeable, not intrusive: a normal banner and sound. Deliberately not
|
|
307
|
+
// `time_sensitive` — a setup confirmation has no business breaking a Focus.
|
|
308
|
+
//
|
|
231
309
|
// Android has no caller-selected interruption level. Explicit null keeps an
|
|
232
310
|
// Apple preference in config from leaking into this Android-only proof.
|
|
233
|
-
level: device.platform === 'ios' ? '
|
|
311
|
+
level: device.platform === 'ios' ? 'active' : null,
|
|
234
312
|
collapseKey: 'notifai-setup-verification',
|
|
235
313
|
});
|
|
236
314
|
}
|
|
@@ -395,7 +473,7 @@ async function runSetupProof(deps) {
|
|
|
395
473
|
}
|
|
396
474
|
/** Closing a local gap cannot have changed the service, the keychain, or devices. */
|
|
397
475
|
function refreshAfterClose(id) {
|
|
398
|
-
return id === 'project' || id === 'project-enablement' || id === 'hooks' || id === 'skill' || id === 'question-routing-settings'
|
|
476
|
+
return id === 'project' || id === 'project-enablement' || id === 'hooks' || id.startsWith('hooks-') || id === 'skill' || id === 'question-routing-settings'
|
|
399
477
|
? ['local']
|
|
400
478
|
: undefined;
|
|
401
479
|
}
|
|
@@ -412,7 +490,12 @@ function wantsOptional(deps, state, flags) {
|
|
|
412
490
|
return Promise.resolve(true);
|
|
413
491
|
if (state.id === 'project-enablement')
|
|
414
492
|
return Promise.resolve(true);
|
|
415
|
-
|
|
493
|
+
// Every other hook sub-state is a report line about routing, not an errand
|
|
494
|
+
// with a yes/no question of its own; offering the hooks question for one of
|
|
495
|
+
// them would ask about something the answer does not change.
|
|
496
|
+
if (state.id !== 'hooks' && state.id !== 'skill')
|
|
497
|
+
return Promise.resolve(false);
|
|
498
|
+
const explicit = state.id === 'hooks' ? flags.hooks : flags.skills;
|
|
416
499
|
if (explicit !== undefined)
|
|
417
500
|
return Promise.resolve(explicit);
|
|
418
501
|
// An agent is never asked, and never assumed into a change it did not
|
|
@@ -424,26 +507,19 @@ function wantsOptional(deps, state, flags) {
|
|
|
424
507
|
: 'Install/update the agent guidance skill through the native npx skills flow?';
|
|
425
508
|
return deps.io.confirm(question, true);
|
|
426
509
|
}
|
|
427
|
-
/**
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
* everything still outstanding. That is a report, and a report is the wrong
|
|
432
|
-
* output here: someone handed five things to do does none of them, and the
|
|
433
|
-
* order was the script's rather than the dependency graph's — it offered to
|
|
434
|
-
* install hooks after a sign-in that had just failed.
|
|
435
|
-
*
|
|
436
|
-
* So this closes what it can, then surfaces exactly one thing, the first that
|
|
437
|
-
* stands in the way. Re-running advances by one. Idempotence stops being a
|
|
438
|
-
* property to preserve and becomes the mechanism: every decision is derived
|
|
439
|
-
* from observed state, so a partial run, a second project, a fresh worktree
|
|
440
|
-
* and a revoked credential are the same code path arriving at different
|
|
441
|
-
* states rather than four branches to enumerate.
|
|
442
|
-
*/
|
|
510
|
+
/** The two states whose remedy actually places files the scope question is about. */
|
|
511
|
+
function needsInstallScope(state) {
|
|
512
|
+
return state.id === 'hooks' || state.id === 'skill';
|
|
513
|
+
}
|
|
443
514
|
function isSetupScope(value) {
|
|
444
515
|
return value === 'project' || value === 'global';
|
|
445
516
|
}
|
|
446
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Scope flags are checked before anything runs, because a contradiction is a
|
|
519
|
+
* usage error and reporting it after half a setup is worse than reporting it
|
|
520
|
+
* now. Answering the question is a separate step (`scopeForLocalInstall`).
|
|
521
|
+
*/
|
|
522
|
+
function checkSetupScopeFlags(deps, flags) {
|
|
447
523
|
if (flags.setupScope !== undefined && !isSetupScope(flags.setupScope)) {
|
|
448
524
|
deps.io.err('Invalid setup scope. Choose `project` or `global`.');
|
|
449
525
|
return 'usage';
|
|
@@ -462,11 +538,35 @@ async function resolveSetupScope(deps, flags) {
|
|
|
462
538
|
deps.io.err('`--skills-scope` requires `--skills`. Use `--setup-scope` to choose project or machine for the whole setup.');
|
|
463
539
|
return 'usage';
|
|
464
540
|
}
|
|
541
|
+
const fromFlags = flags.setupScope ?? flags.skillsScope;
|
|
542
|
+
// Asking for an install nobody can be asked about is a usage error, and it
|
|
543
|
+
// is knowable from the flags alone — so it is reported before a setup runs
|
|
544
|
+
// half way rather than after.
|
|
545
|
+
if (fromFlags === undefined &&
|
|
546
|
+
deps.io.interactive !== true &&
|
|
547
|
+
(flags.skills === true || flags.hooks === true)) {
|
|
548
|
+
deps.io.err('Unattended setup changes require an explicit scope: pass `--setup-scope project` or `--setup-scope global`.');
|
|
549
|
+
return 'usage';
|
|
550
|
+
}
|
|
551
|
+
return fromFlags;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Where a skill or hook install should land, asked at the moment one is about
|
|
555
|
+
* to happen and not before.
|
|
556
|
+
*
|
|
557
|
+
* It used to be the first thing the product ever said, in front of the
|
|
558
|
+
* assessment, sign-in and every other gate — a question about skill, hook and
|
|
559
|
+
* shared-config placement put to someone who had met none of those words and
|
|
560
|
+
* might never reach the step it decides. Worse, project identity and Project
|
|
561
|
+
* Enablement were gated on the answer, so an unattended run that could not
|
|
562
|
+
* produce one advanced nothing at all.
|
|
563
|
+
*
|
|
564
|
+
* Now nothing depends on it except the two installs it actually describes.
|
|
565
|
+
*/
|
|
566
|
+
async function scopeForLocalInstall(deps, flags) {
|
|
465
567
|
const fromFlags = flags.setupScope ?? flags.skillsScope;
|
|
466
568
|
if (fromFlags !== undefined)
|
|
467
569
|
return fromFlags;
|
|
468
|
-
if (flags.skills === false && flags.hooks === false)
|
|
469
|
-
return undefined;
|
|
470
570
|
if (deps.io.interactive === true && deps.io.select) {
|
|
471
571
|
const selected = await deps.io.select('Should this Notifai setup apply to this project only, or to every project on this machine?', [
|
|
472
572
|
{
|
|
@@ -483,14 +583,27 @@ async function resolveSetupScope(deps, flags) {
|
|
|
483
583
|
if (selected === 'project' || selected === 'global')
|
|
484
584
|
return selected;
|
|
485
585
|
deps.io.err('No setup scope selected. Pass `--setup-scope project` or `--setup-scope global`.');
|
|
486
|
-
return '
|
|
487
|
-
}
|
|
488
|
-
if (flags.skills === true || flags.hooks === true) {
|
|
489
|
-
deps.io.err('Unattended setup changes require an explicit scope: pass `--setup-scope project` or `--setup-scope global`.');
|
|
490
|
-
return 'usage';
|
|
586
|
+
return 'declined';
|
|
491
587
|
}
|
|
492
|
-
|
|
588
|
+
deps.io.err('Unattended setup changes require an explicit scope: pass `--setup-scope project` or `--setup-scope global`.');
|
|
589
|
+
return 'declined';
|
|
493
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* Setup as one step at a time.
|
|
593
|
+
*
|
|
594
|
+
* The old version ran five steps in a fixed order and ended with a list of
|
|
595
|
+
* everything still outstanding. That is a report, and a report is the wrong
|
|
596
|
+
* output here: someone handed five things to do does none of them, and the
|
|
597
|
+
* order was the script's rather than the dependency graph's — it offered to
|
|
598
|
+
* install hooks after a sign-in that had just failed.
|
|
599
|
+
*
|
|
600
|
+
* So this closes what it can, then surfaces exactly one thing, the first that
|
|
601
|
+
* stands in the way. Re-running advances by one. Idempotence stops being a
|
|
602
|
+
* property to preserve and becomes the mechanism: every decision is derived
|
|
603
|
+
* from observed state, so a partial run, a second project, a fresh worktree
|
|
604
|
+
* and a revoked credential are the same code path arriving at different
|
|
605
|
+
* states rather than four branches to enumerate.
|
|
606
|
+
*/
|
|
494
607
|
export async function initCommand(deps, flags) {
|
|
495
608
|
const workingDeps = flags.json === true
|
|
496
609
|
? {
|
|
@@ -504,12 +617,14 @@ export async function initCommand(deps, flags) {
|
|
|
504
617
|
},
|
|
505
618
|
}
|
|
506
619
|
: deps;
|
|
507
|
-
const
|
|
508
|
-
if (
|
|
620
|
+
const scopeFromFlags = checkSetupScopeFlags(workingDeps, flags);
|
|
621
|
+
if (scopeFromFlags === 'usage')
|
|
509
622
|
return EXIT.usage;
|
|
510
|
-
|
|
623
|
+
let resolved = {
|
|
511
624
|
...flags,
|
|
512
|
-
...(
|
|
625
|
+
...(scopeFromFlags === undefined
|
|
626
|
+
? {}
|
|
627
|
+
: { setupScope: scopeFromFlags, skillsScope: flags.skillsScope ?? scopeFromFlags }),
|
|
513
628
|
};
|
|
514
629
|
await workingDeps.io.intro?.('Notifai setup');
|
|
515
630
|
const skillOpts = resolved.setupScope === undefined ? {} : { skillScope: resolved.setupScope };
|
|
@@ -520,6 +635,11 @@ export async function initCommand(deps, flags) {
|
|
|
520
635
|
...(refresh === undefined ? {} : { previous: readiness, refresh }),
|
|
521
636
|
});
|
|
522
637
|
let failed = false;
|
|
638
|
+
// What actually stopped a sign-in, when something did. Without it the close
|
|
639
|
+
// renders the state as it was before the attempt — "not paired … run
|
|
640
|
+
// `notifai init`" — which contradicts the correct line printed moments
|
|
641
|
+
// earlier and sends the reader back into the command that just failed.
|
|
642
|
+
let loginBlocker = null;
|
|
523
643
|
const attempted = new Set();
|
|
524
644
|
// Re-assess after every successful action. This is how a browser approval or
|
|
525
645
|
// companion registration can unlock the next state while the user is still
|
|
@@ -530,24 +650,37 @@ export async function initCommand(deps, flags) {
|
|
|
530
650
|
for (const state of readiness.states) {
|
|
531
651
|
if (state.status === 'ready')
|
|
532
652
|
continue;
|
|
653
|
+
// Question Routing is optional automation, so it is reported and never
|
|
654
|
+
// waited on. `doctor` still fails on a broken route; `init` walks past
|
|
655
|
+
// it, because a hook diagnostic standing in front of the delivery proof
|
|
656
|
+
// means a setup that can already send never proves that it can.
|
|
657
|
+
const optional = isOptionalSetup(state.id);
|
|
658
|
+
const halt = () => {
|
|
659
|
+
if (!optional)
|
|
660
|
+
stop = true;
|
|
661
|
+
return !optional;
|
|
662
|
+
};
|
|
533
663
|
if (state.status === 'unknown') {
|
|
534
|
-
|
|
535
|
-
|
|
664
|
+
if (halt())
|
|
665
|
+
break;
|
|
666
|
+
continue;
|
|
536
667
|
}
|
|
537
668
|
const remedy = state.remedy;
|
|
538
669
|
if (remedy === undefined || attempted.has(state.id)) {
|
|
539
|
-
if (state.status === 'gap')
|
|
540
|
-
stop = true;
|
|
541
|
-
if (stop)
|
|
670
|
+
if (state.status === 'gap' && halt())
|
|
542
671
|
break;
|
|
543
672
|
continue;
|
|
544
673
|
}
|
|
545
674
|
if (state.status === 'optional-gap') {
|
|
546
675
|
if (remedy.by !== 'cli' || !(await wantsOptional(workingDeps, state, resolved)))
|
|
547
676
|
continue;
|
|
548
|
-
if (
|
|
549
|
-
|
|
550
|
-
|
|
677
|
+
if (needsInstallScope(state)) {
|
|
678
|
+
const scope = await scopeForLocalInstall(workingDeps, resolved);
|
|
679
|
+
if (scope === 'declined') {
|
|
680
|
+
attempted.add(state.id);
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
resolved = { ...resolved, setupScope: scope, skillsScope: resolved.skillsScope ?? scope };
|
|
551
684
|
}
|
|
552
685
|
attempted.add(state.id);
|
|
553
686
|
const result = await closeGap(workingDeps, state, resolved);
|
|
@@ -559,25 +692,33 @@ export async function initCommand(deps, flags) {
|
|
|
559
692
|
break;
|
|
560
693
|
}
|
|
561
694
|
if (result !== 'closed') {
|
|
562
|
-
|
|
563
|
-
|
|
695
|
+
if (halt())
|
|
696
|
+
break;
|
|
697
|
+
continue;
|
|
564
698
|
}
|
|
565
699
|
readiness = await reassess(refreshAfterClose(state.id));
|
|
566
700
|
advanced = true;
|
|
567
701
|
break;
|
|
568
702
|
}
|
|
569
703
|
if (remedy.by === 'cli') {
|
|
570
|
-
if (
|
|
571
|
-
|
|
572
|
-
|
|
704
|
+
if (needsInstallScope(state)) {
|
|
705
|
+
const scope = await scopeForLocalInstall(workingDeps, resolved);
|
|
706
|
+
if (scope === 'declined') {
|
|
707
|
+
attempted.add(state.id);
|
|
708
|
+
if (halt())
|
|
709
|
+
break;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
resolved = { ...resolved, setupScope: scope, skillsScope: resolved.skillsScope ?? scope };
|
|
573
713
|
}
|
|
574
714
|
attempted.add(state.id);
|
|
575
715
|
const result = await closeGap(workingDeps, state, resolved);
|
|
576
716
|
if (result === 'failed')
|
|
577
717
|
failed = true;
|
|
578
718
|
if (result !== 'closed') {
|
|
579
|
-
|
|
580
|
-
|
|
719
|
+
if (halt())
|
|
720
|
+
break;
|
|
721
|
+
continue;
|
|
581
722
|
}
|
|
582
723
|
readiness = await reassess(refreshAfterClose(state.id));
|
|
583
724
|
advanced = true;
|
|
@@ -590,7 +731,7 @@ export async function initCommand(deps, flags) {
|
|
|
590
731
|
workingDeps.io.interactive === true) {
|
|
591
732
|
attempted.add(state.id);
|
|
592
733
|
workingDeps.io.out('Opening your browser to approve this machine — Ctrl-C to stop.');
|
|
593
|
-
if ((await loginCommand(workingDeps, {})) !== EXIT.ok) {
|
|
734
|
+
if ((await loginCommand(workingDeps, {}, (blocker) => (loginBlocker = blocker))) !== EXIT.ok) {
|
|
594
735
|
failed = true;
|
|
595
736
|
stop = true;
|
|
596
737
|
break;
|
|
@@ -615,50 +756,68 @@ export async function initCommand(deps, flags) {
|
|
|
615
756
|
break;
|
|
616
757
|
}
|
|
617
758
|
// A human-only remedy is the first blocker for an unattended agent.
|
|
618
|
-
|
|
619
|
-
|
|
759
|
+
if (halt())
|
|
760
|
+
break;
|
|
761
|
+
continue;
|
|
620
762
|
}
|
|
621
763
|
if (stop || !advanced)
|
|
622
764
|
break;
|
|
623
765
|
}
|
|
766
|
+
const blocker = loginBlocker ?? firstRequiredBlocker(readiness);
|
|
624
767
|
if (flags.json === true)
|
|
625
768
|
deps.io.out(JSON.stringify(readinessJson(readiness), null, 2));
|
|
626
769
|
else
|
|
627
|
-
await printInitClose(deps, readiness, resolved);
|
|
628
|
-
const blocker = firstBlocker(readiness);
|
|
770
|
+
await printInitClose(deps, readiness, resolved, loginBlocker);
|
|
629
771
|
if (blocker === null)
|
|
630
772
|
return failed ? EXIT.failed : EXIT.ok;
|
|
631
773
|
return failed || workingDeps.io.interactive !== true ? EXIT.failed : EXIT.ok;
|
|
632
774
|
}
|
|
633
|
-
function isHookSubstate(id) {
|
|
634
|
-
return id === 'hooks' || id.startsWith('hooks-') || id === 'question-routing-settings';
|
|
635
|
-
}
|
|
636
775
|
function leftoverOptionals(readiness, flags) {
|
|
637
776
|
return readiness.states.filter((state) => {
|
|
638
777
|
if (state.status !== 'optional-gap')
|
|
639
778
|
return false;
|
|
640
|
-
if (
|
|
779
|
+
if (isOptionalAutomation(state.id))
|
|
780
|
+
return false;
|
|
781
|
+
// Not an install anyone declined, so "Optional, not set up" would misread
|
|
782
|
+
// it; it gets its own line below.
|
|
783
|
+
if (state.id === 'cli-bin')
|
|
641
784
|
return false;
|
|
642
785
|
if (state.id === 'skill' && flags.skills === false)
|
|
643
786
|
return false;
|
|
644
787
|
return true;
|
|
645
788
|
});
|
|
646
789
|
}
|
|
790
|
+
/**
|
|
791
|
+
* A `notifai` the reader cannot type.
|
|
792
|
+
*
|
|
793
|
+
* Said on the way out of every run, successful or not, because every next step
|
|
794
|
+
* this command prints — here, in the skill, in the README — names a command
|
|
795
|
+
* that will not be found until it is fixed.
|
|
796
|
+
*/
|
|
797
|
+
function printMissingCliBin(deps, readiness) {
|
|
798
|
+
const state = readiness.states.find((candidate) => candidate.id === 'cli-bin');
|
|
799
|
+
if (state?.status !== 'optional-gap')
|
|
800
|
+
return;
|
|
801
|
+
deps.io.out(`Heads up: ${state.detail}. ${remedyLine(state)}`);
|
|
802
|
+
}
|
|
647
803
|
function printOptionalLeftovers(deps, leftovers) {
|
|
648
804
|
for (const state of leftovers) {
|
|
649
805
|
deps.io.out(`Optional, not set up — ${remedyLine(state)}`);
|
|
650
806
|
}
|
|
651
807
|
}
|
|
652
|
-
async function printInitClose(deps, readiness, flags) {
|
|
653
|
-
const blocker =
|
|
808
|
+
async function printInitClose(deps, readiness, flags, stoppedBy = null) {
|
|
809
|
+
const blocker = stoppedBy ?? firstRequiredBlocker(readiness);
|
|
654
810
|
const canSend = readiness.states.find((state) => state.id === 'devices')?.status === 'ready';
|
|
655
811
|
const questions = questionRoutingReady(readiness);
|
|
656
812
|
const leftovers = leftoverOptionals(readiness, flags).filter((state) => state.id !== 'contract');
|
|
657
813
|
if (blocker?.id === 'contract') {
|
|
658
814
|
deps.io.out(blocker.detail);
|
|
659
|
-
deps.io.out(
|
|
815
|
+
deps.io.out(blocker.remedy?.by !== 'user-elsewhere' && blocker.remedy?.command !== undefined
|
|
816
|
+
? blocker.remedy.command
|
|
817
|
+
: updateCliCommand(deps));
|
|
660
818
|
return;
|
|
661
819
|
}
|
|
820
|
+
printMissingCliBin(deps, readiness);
|
|
662
821
|
if (deps.io.interactive === true) {
|
|
663
822
|
if (blocker === null) {
|
|
664
823
|
const lines = [
|
|
@@ -675,7 +834,9 @@ async function printInitClose(deps, readiness, flags) {
|
|
|
675
834
|
await deps.io.outro?.('All set ✨');
|
|
676
835
|
return;
|
|
677
836
|
}
|
|
678
|
-
|
|
837
|
+
// No framed copy of what the next three lines already say. The "Ready"
|
|
838
|
+
// note above adds something the close does not; this one only made a
|
|
839
|
+
// stopped visit state its one step twice, box then text.
|
|
679
840
|
deps.io.out(`Next: ${blocker.title} — ${blocker.detail}`);
|
|
680
841
|
deps.io.out(` ${remedyLine(blocker)}`);
|
|
681
842
|
if (shouldRestateInit(blocker)) {
|