@notis_ai/cli 0.2.10 → 0.2.11
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/README.md +73 -9
- package/dist/scaffolds.json +1 -1
- package/package.json +1 -1
- package/skills/notis-apps/SKILL.md +1 -1
- package/skills/notis-apps/cli.md +1 -1
- package/skills/notis-cli/SKILL.md +56 -16
- package/skills/notis-onboarding/BRIEF.md +24 -5
- package/skills/notis-query/cli.md +1 -1
- package/src/cli.js +27 -2
- package/src/command-specs/apps.js +7 -29
- package/src/command-specs/auth.js +15 -20
- package/src/command-specs/index.js +3 -0
- package/src/command-specs/meta.js +29 -19
- package/src/command-specs/onboarding.js +122 -200
- package/src/command-specs/profile.js +358 -0
- package/src/runtime/app-platform.js +5 -5
- package/src/runtime/auth-recovery.js +100 -0
- package/src/runtime/oauth.js +111 -36
- package/src/runtime/profiles.js +395 -221
- package/src/runtime/transport.js +84 -29
- package/src/runtime/desktop-auth.js +0 -162
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { COMPOSIO_SEARCH_TOOLS, healthCheck, probeAuth } from './helpers.js';
|
|
2
2
|
import { findCommandSpec, formatDescribe } from '../runtime/help.js';
|
|
3
|
-
import { createExpiredAuthError,
|
|
3
|
+
import { createExpiredAuthError, getAuthRecovery } from '../runtime/auth-recovery.js';
|
|
4
4
|
import {
|
|
5
5
|
credentialIsExpired,
|
|
6
|
-
getJwtCanonicalUserId,
|
|
7
6
|
getProfile,
|
|
8
7
|
loadConfig,
|
|
9
8
|
} from '../runtime/profiles.js';
|
|
@@ -22,7 +21,7 @@ async function doctorHandler(ctx) {
|
|
|
22
21
|
const checks = {
|
|
23
22
|
config: 'ok',
|
|
24
23
|
auth: 'missing',
|
|
25
|
-
|
|
24
|
+
routing: 'ok',
|
|
26
25
|
health: 'unknown',
|
|
27
26
|
tool_roundtrip: 'unknown',
|
|
28
27
|
};
|
|
@@ -35,17 +34,23 @@ async function doctorHandler(ctx) {
|
|
|
35
34
|
// refresh endpoint is unavailable or rejects the stored credential.
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
|
-
let profile = getProfile(loadConfig(
|
|
37
|
+
let profile = getProfile(loadConfig(), ctx.runtime.profileName);
|
|
39
38
|
checks.auth = ctx.runtime.jwt
|
|
40
39
|
? (credentialIsExpired(ctx.runtime, profile) ? 'expired' : 'configured')
|
|
41
40
|
: 'missing';
|
|
42
|
-
|
|
41
|
+
// A worktree whose ./dev.sh has stopped leaves commands with no local
|
|
42
|
+
// backend to reach. Say so here rather than letting every later command fail
|
|
43
|
+
// as an opaque network error.
|
|
43
44
|
if (
|
|
44
|
-
|
|
45
|
-
&&
|
|
46
|
-
&& desktopUserId !== profile.oauth_user_id
|
|
45
|
+
ctx.runtime.worktreeRuntimeUnavailable
|
|
46
|
+
&& ctx.runtime.profileSource !== 'explicit'
|
|
47
47
|
) {
|
|
48
|
-
checks.
|
|
48
|
+
checks.routing = 'dev_runtime_unavailable';
|
|
49
|
+
} else if (
|
|
50
|
+
ctx.runtime.detachedWorktreeRuntime
|
|
51
|
+
|| (ctx.runtime.worktreeRuntimeUnavailable && ctx.runtime.profileSource === 'explicit')
|
|
52
|
+
) {
|
|
53
|
+
checks.routing = 'detached';
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
try {
|
|
@@ -62,7 +67,7 @@ async function doctorHandler(ctx) {
|
|
|
62
67
|
// because that legitimate roundtrip exceeds the general 30s default.
|
|
63
68
|
const payload = await probeAuth(doctorToolRoundtripRuntime(ctx.runtime));
|
|
64
69
|
checks.tool_roundtrip = Array.isArray(payload.toolkit_connection_statuses) ? 'ok' : 'error';
|
|
65
|
-
profile = getProfile(loadConfig(
|
|
70
|
+
profile = getProfile(loadConfig(), ctx.runtime.profileName);
|
|
66
71
|
checks.auth = ctx.runtime.jwt
|
|
67
72
|
? (credentialIsExpired(ctx.runtime, profile) ? 'expired' : 'configured')
|
|
68
73
|
: 'missing';
|
|
@@ -73,27 +78,31 @@ async function doctorHandler(ctx) {
|
|
|
73
78
|
|
|
74
79
|
const hints = [];
|
|
75
80
|
if (checks.auth === 'missing') {
|
|
76
|
-
hints.push(...
|
|
81
|
+
hints.push(...getAuthRecovery(ctx.runtime, { mode: 'missing' }).hints);
|
|
77
82
|
} else if (checks.auth === 'expired') {
|
|
78
83
|
hints.push(...createExpiredAuthError(ctx.runtime).hints);
|
|
79
84
|
}
|
|
85
|
+
if (checks.routing === 'dev_runtime_unavailable') {
|
|
86
|
+
hints.push(...ctx.runtime.worktreeRuntimeUnavailable.hints);
|
|
87
|
+
} else if (checks.routing === 'detached') {
|
|
88
|
+
hints.push({
|
|
89
|
+
message: ctx.runtime.detachedWorktreeRuntime?.profile
|
|
90
|
+
? `This worktree's ./dev.sh profile is "${ctx.runtime.detachedWorktreeRuntime.profile}"; profile "${ctx.runtime.profileName}" bypasses it.`
|
|
91
|
+
: `Explicit profile "${ctx.runtime.profileName}" bypasses this stopped worktree runtime.`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
80
94
|
if (checks.health === 'error' && checks.auth !== 'expired') {
|
|
81
|
-
hints.push({ command: '
|
|
95
|
+
hints.push({ command: 'notis profile show', reason: 'Check which API endpoint this profile targets' });
|
|
82
96
|
}
|
|
83
97
|
if (checks.tool_roundtrip === 'error') {
|
|
84
98
|
hints.push({ command: 'notis whoami', reason: 'Verify your account and permissions' });
|
|
85
99
|
}
|
|
86
|
-
if (checks.identity === 'error') {
|
|
87
|
-
hints.push({
|
|
88
|
-
command: 'notis logout',
|
|
89
|
-
reason: 'Desktop and OAuth credentials identify different accounts; remove the independent OAuth grant or sign Desktop back into the same account',
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
100
|
|
|
93
101
|
return ctx.output.emitSuccess({
|
|
94
102
|
command: ctx.spec.command_path.join(' '),
|
|
95
103
|
data: {
|
|
96
104
|
profile: ctx.runtime.profileName,
|
|
105
|
+
profile_source: ctx.runtime.profileSource,
|
|
97
106
|
api_base: ctx.runtime.apiBase,
|
|
98
107
|
credential_source: ctx.runtime.credentialKind || null,
|
|
99
108
|
...(ctx.runtime.credentialKind === 'oauth'
|
|
@@ -142,6 +151,7 @@ async function whoamiHandler(ctx) {
|
|
|
142
151
|
command: ctx.spec.command_path.join(' '),
|
|
143
152
|
data: {
|
|
144
153
|
profile: ctx.runtime.profileName,
|
|
154
|
+
profile_source: ctx.runtime.profileSource,
|
|
145
155
|
api_base: ctx.runtime.apiBase,
|
|
146
156
|
credential_source: ctx.runtime.credentialKind || null,
|
|
147
157
|
user_id: userId,
|
|
@@ -159,8 +169,8 @@ async function whoamiHandler(ctx) {
|
|
|
159
169
|
`Version: ${ctx.runtime.cliVersion}`,
|
|
160
170
|
].join('\n'),
|
|
161
171
|
hints: [
|
|
172
|
+
{ command: 'notis profile list', reason: 'See the other accounts this machine can switch to' },
|
|
162
173
|
{ command: 'notis tools toolkits', reason: 'List available toolkit namespaces and connection statuses' },
|
|
163
|
-
{ command: 'notis doctor', reason: 'Run a full health check' },
|
|
164
174
|
],
|
|
165
175
|
});
|
|
166
176
|
}
|
|
@@ -3,17 +3,51 @@ import { dirname, join } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
5
|
import { CliError, EXIT_CODES } from '../runtime/errors.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getAuthRecovery } from '../runtime/auth-recovery.js';
|
|
7
7
|
import {
|
|
8
8
|
credentialIsExpired,
|
|
9
9
|
getProfile,
|
|
10
10
|
loadConfig,
|
|
11
11
|
} from '../runtime/profiles.js';
|
|
12
12
|
import { ensureFreshOAuthCredential, loginWithOAuth } from '../runtime/oauth.js';
|
|
13
|
+
import { runToolCommand } from './helpers.js';
|
|
13
14
|
|
|
14
15
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
const BUNDLED_BRIEF_PATH = join(HERE, '..', '..', 'skills', 'notis-onboarding', 'BRIEF.md');
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Ask the account whether it has already been onboarded.
|
|
20
|
+
*
|
|
21
|
+
* The brief itself is served unauthenticated and is identical for everyone, so
|
|
22
|
+
* it can never answer this. Without the check `start` hands a year-old account
|
|
23
|
+
* the new-user script, and a compliant agent re-asks the user their own name
|
|
24
|
+
* and calls COMPLETE_TUTORIAL on someone who converted long ago.
|
|
25
|
+
*
|
|
26
|
+
* A failure here is not fatal: an unreachable tool bridge should not block
|
|
27
|
+
* sign-in. It resolves to null and the caller degrades to serving the brief,
|
|
28
|
+
* which is the previous behaviour.
|
|
29
|
+
*/
|
|
30
|
+
async function fetchOnboardingState(runtime) {
|
|
31
|
+
try {
|
|
32
|
+
const { payload } = await runToolCommand({
|
|
33
|
+
runtime,
|
|
34
|
+
toolName: 'LOCAL_NOTIS_GET_USER_SETTINGS',
|
|
35
|
+
arguments_: {},
|
|
36
|
+
});
|
|
37
|
+
const data = payload?.data ?? payload?.result ?? payload;
|
|
38
|
+
if (data && typeof data.onboarding_complete === 'boolean') {
|
|
39
|
+
return {
|
|
40
|
+
onboardingComplete: data.onboarding_complete,
|
|
41
|
+
settings: data.settings || {},
|
|
42
|
+
missingSettings: Array.isArray(data.missing_settings) ? data.missing_settings : [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
17
51
|
/**
|
|
18
52
|
* The brief is served rather than bundled so it tracks the deployed server. The
|
|
19
53
|
* bundled copy is a fallback for an offline or unreachable API, and the payload
|
|
@@ -47,258 +81,146 @@ async function fetchBrief(apiBase, timeoutMs) {
|
|
|
47
81
|
}
|
|
48
82
|
}
|
|
49
83
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
body: JSON.stringify({
|
|
55
|
-
email,
|
|
56
|
-
...(useCases && useCases.length ? { preferred_use_cases: useCases } : {}),
|
|
57
|
-
}),
|
|
58
|
-
});
|
|
59
|
-
const payload = await response.json().catch(() => null);
|
|
84
|
+
function isAuthenticated(runtime) {
|
|
85
|
+
return Boolean(runtime.jwt)
|
|
86
|
+
&& !credentialIsExpired(runtime, getProfile(loadConfig(), runtime.profileName));
|
|
87
|
+
}
|
|
60
88
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
/**
|
|
90
|
+
* What an authenticated `start` reports.
|
|
91
|
+
*
|
|
92
|
+
* A returning user does not need an onboarding script — they need orientation:
|
|
93
|
+
* which account, which endpoint, and confirmation that nothing is expected of
|
|
94
|
+
* them. Only an account that has genuinely not finished onboarding gets the
|
|
95
|
+
* brief, so `brief: null` is a positive signal to the agent, not a failure.
|
|
96
|
+
*/
|
|
97
|
+
async function authenticatedResult(ctx) {
|
|
98
|
+
const state = await fetchOnboardingState(ctx.runtime);
|
|
99
|
+
const onboardingComplete = state?.onboardingComplete === true;
|
|
100
|
+
const base = {
|
|
101
|
+
authenticated: true,
|
|
102
|
+
profile: ctx.runtime.profileName,
|
|
103
|
+
api_base: ctx.runtime.apiBase,
|
|
104
|
+
credential_source: ctx.runtime.credentialKind,
|
|
105
|
+
onboarding_complete: onboardingComplete,
|
|
106
|
+
...(state ? { known_settings: state.settings, missing_settings: state.missingSettings } : {}),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (onboardingComplete) {
|
|
110
|
+
const name = state?.settings?.full_name;
|
|
111
|
+
return ctx.output.emitSuccess({
|
|
112
|
+
command: 'start',
|
|
113
|
+
data: { ...base, brief: null, brief_source: null },
|
|
114
|
+
humanSummary:
|
|
115
|
+
`Profile "${ctx.runtime.profileName}" is signed in and this account is already set up.`,
|
|
116
|
+
renderHuman: () =>
|
|
117
|
+
[
|
|
118
|
+
`Signed in${name ? ` as ${name}` : ''} on profile "${ctx.runtime.profileName}".`,
|
|
119
|
+
`API: ${ctx.runtime.apiBase}`,
|
|
120
|
+
'',
|
|
121
|
+
'This account has already completed onboarding. Do not run an onboarding',
|
|
122
|
+
'flow and do not call LOCAL_NOTIS_COMPLETE_TUTORIAL.',
|
|
123
|
+
].join('\n'),
|
|
69
124
|
hints: [
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
reason: 'Requesting another link makes earlier links stop working',
|
|
73
|
-
},
|
|
125
|
+
{ command: 'notis whoami', reason: 'Show the account and its connected toolkits' },
|
|
126
|
+
{ command: 'notis tools search "<what you need>"', reason: 'Find a tool and get on with the task' },
|
|
74
127
|
],
|
|
75
128
|
});
|
|
76
129
|
}
|
|
77
130
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return payload || {};
|
|
131
|
+
const brief = await fetchBrief(ctx.runtime.apiBase, ctx.runtime.timeoutMs);
|
|
132
|
+
return ctx.output.emitSuccess({
|
|
133
|
+
command: 'start',
|
|
134
|
+
data: { ...base, brief: brief.markdown, brief_source: brief.source },
|
|
135
|
+
humanSummary: `Notis CLI is authenticated for profile "${ctx.runtime.profileName}". Onboarding is not complete.`,
|
|
136
|
+
renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
|
|
137
|
+
});
|
|
88
138
|
}
|
|
89
139
|
|
|
90
140
|
async function startHandler(ctx) {
|
|
91
141
|
const { runtime, options, output } = ctx;
|
|
92
|
-
const apiBase = runtime.apiBase;
|
|
93
142
|
if (runtime.credentialKind === 'oauth') {
|
|
94
143
|
try {
|
|
95
144
|
await ensureFreshOAuthCredential(runtime);
|
|
96
145
|
} catch {
|
|
97
146
|
// A failed refresh is equivalent to no usable session here. Interactive
|
|
98
|
-
// starts may still authorize again; brief-only
|
|
99
|
-
//
|
|
147
|
+
// starts may still authorize again; brief-only runs surface the normal
|
|
148
|
+
// authentication recovery below.
|
|
100
149
|
}
|
|
101
150
|
}
|
|
102
|
-
let authenticated = Boolean(runtime.jwt)
|
|
103
|
-
&& !credentialIsExpired(
|
|
104
|
-
runtime,
|
|
105
|
-
getProfile(loadConfig(runtime.worktreeRuntime), runtime.profileName),
|
|
106
|
-
);
|
|
107
151
|
|
|
108
|
-
// Safe to re-run: an already-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!authenticated) {
|
|
112
|
-
const recovery = getDesktopAuthRecovery(runtime, { mode: 'missing' });
|
|
113
|
-
throw new CliError({
|
|
114
|
-
code: 'auth_missing',
|
|
115
|
-
message: 'This machine is not signed in to Notis yet.',
|
|
116
|
-
exitCode: EXIT_CODES.auth,
|
|
117
|
-
hints: recovery.hints,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
const brief = await fetchBrief(apiBase, runtime.timeoutMs);
|
|
121
|
-
return output.emitSuccess({
|
|
122
|
-
command: 'start',
|
|
123
|
-
data: { authenticated: true, brief: brief.markdown, brief_source: brief.source },
|
|
124
|
-
humanSummary: 'Notis CLI is authenticated on this machine.',
|
|
125
|
-
renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
!options.email
|
|
131
|
-
&& !runtime.agentMode
|
|
132
|
-
&& !runtime.nonInteractive
|
|
133
|
-
&& options.wait !== false
|
|
134
|
-
) {
|
|
135
|
-
let authorization;
|
|
136
|
-
let authorizationError;
|
|
137
|
-
try {
|
|
138
|
-
authorization = await loginWithOAuth(runtime, { browser: true }, output);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
// Keep the original OAuth failure while checking whether another process
|
|
141
|
-
// completed authentication before this attempt failed.
|
|
142
|
-
authorizationError = error;
|
|
143
|
-
authorization = null;
|
|
144
|
-
}
|
|
145
|
-
if (authorization?.agentAuthorization) {
|
|
146
|
-
return output.emitSuccess({
|
|
147
|
-
command: 'start',
|
|
148
|
-
data: { authenticated: false, ...authorization.agentAuthorization },
|
|
149
|
-
humanSummary: 'Open the authorization URL to finish signing in.',
|
|
150
|
-
renderHuman: () => `Authorize Notis CLI: ${authorization.agentAuthorization.authorize_url}`,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
// A failed authorization leaves the stale credential that got us here in
|
|
154
|
-
// place, so re-apply the same expiry test used above rather than reporting
|
|
155
|
-
// a machine as signed in on the strength of a dead token.
|
|
156
|
-
authenticated = Boolean(runtime.jwt)
|
|
157
|
-
&& !credentialIsExpired(runtime, getProfile(loadConfig(runtime.worktreeRuntime), runtime.profileName));
|
|
158
|
-
if (authenticated) {
|
|
159
|
-
const brief = await fetchBrief(apiBase, runtime.timeoutMs);
|
|
160
|
-
return output.emitSuccess({
|
|
161
|
-
command: 'start',
|
|
162
|
-
data: {
|
|
163
|
-
authenticated: true,
|
|
164
|
-
credential_source: runtime.credentialKind,
|
|
165
|
-
brief: brief.markdown,
|
|
166
|
-
brief_source: brief.source,
|
|
167
|
-
},
|
|
168
|
-
humanSummary: 'Notis CLI is authenticated. Follow the onboarding brief below.',
|
|
169
|
-
renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
if (authorizationError) {
|
|
173
|
-
throw authorizationError;
|
|
174
|
-
}
|
|
152
|
+
// Safe to re-run: an already-authorized profile skips straight to the brief.
|
|
153
|
+
if (isAuthenticated(runtime)) {
|
|
154
|
+
return authenticatedResult(ctx);
|
|
175
155
|
}
|
|
176
156
|
|
|
177
|
-
if (
|
|
178
|
-
// The one point in the flow that has to stop and talk to the human.
|
|
157
|
+
if (options.briefOnly) {
|
|
179
158
|
throw new CliError({
|
|
180
|
-
code: '
|
|
181
|
-
message:
|
|
182
|
-
exitCode: EXIT_CODES.
|
|
183
|
-
hints:
|
|
184
|
-
{
|
|
185
|
-
command: 'Ask the user for their email address, then rerun with --email <address>',
|
|
186
|
-
reason: 'Notis sends a sign-in link there to prove they own the address',
|
|
187
|
-
},
|
|
188
|
-
],
|
|
159
|
+
code: 'auth_missing',
|
|
160
|
+
message: `Profile "${runtime.profileName}" is not signed in to Notis yet.`,
|
|
161
|
+
exitCode: EXIT_CODES.auth,
|
|
162
|
+
hints: getAuthRecovery(runtime, { mode: 'missing' }).hints,
|
|
189
163
|
});
|
|
190
164
|
}
|
|
191
165
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// Commander stores the negatable `--no-wait` flag as `options.wait === false`
|
|
198
|
-
// and never sets `options.noWait` — the same footgun already fixed for
|
|
199
|
-
// `--no-open` in apps.js. Reading `noWait` made the flag a no-op, so an agent
|
|
200
|
-
// that asked not to wait blocked for the full timeout instead.
|
|
201
|
-
if (options.wait === false) {
|
|
166
|
+
// Browser authorization is the whole signup path: it creates the account when
|
|
167
|
+
// the address is new and authorizes this machine either way. There is nothing
|
|
168
|
+
// for the CLI to collect up front, and nothing to wait on afterwards.
|
|
169
|
+
const authorization = await loginWithOAuth(runtime, { browser: true }, output);
|
|
170
|
+
if (authorization?.agentAuthorization) {
|
|
202
171
|
return output.emitSuccess({
|
|
203
172
|
command: 'start',
|
|
204
|
-
data: {
|
|
205
|
-
|
|
173
|
+
data: {
|
|
174
|
+
authenticated: false,
|
|
175
|
+
profile: runtime.profileName,
|
|
176
|
+
...authorization.agentAuthorization,
|
|
177
|
+
},
|
|
178
|
+
humanSummary: 'Open the authorization URL to create or access the Notis account.',
|
|
179
|
+
renderHuman: () => `Authorize Notis CLI: ${authorization.agentAuthorization.authorize_url}`,
|
|
206
180
|
});
|
|
207
181
|
}
|
|
208
182
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const profile = getProfile(config, profileName);
|
|
214
|
-
if (
|
|
215
|
-
profile.jwt
|
|
216
|
-
&& !credentialIsExpired(
|
|
217
|
-
{ credentialKind: 'desktop', jwt: profile.jwt },
|
|
218
|
-
profile,
|
|
219
|
-
)
|
|
220
|
-
) {
|
|
221
|
-
return profile.jwt;
|
|
222
|
-
}
|
|
223
|
-
if (
|
|
224
|
-
profile.oauth_access_token
|
|
225
|
-
&& !credentialIsExpired(
|
|
226
|
-
{ credentialKind: 'oauth', jwt: profile.oauth_access_token },
|
|
227
|
-
profile,
|
|
228
|
-
)
|
|
229
|
-
) {
|
|
230
|
-
return profile.oauth_access_token;
|
|
231
|
-
}
|
|
232
|
-
return undefined;
|
|
233
|
-
},
|
|
234
|
-
isJwtExpired: () => false,
|
|
235
|
-
profileName: runtime.profileName,
|
|
236
|
-
timeoutMs,
|
|
237
|
-
onTick: (remaining) => {
|
|
238
|
-
if (runtime.outputMode !== 'json' && remaining % 15_000 < 2_000) {
|
|
239
|
-
output.note?.(`Waiting for Notis Desktop to sign in (${Math.round(remaining / 1000)}s left)...`);
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
if (!jwt) {
|
|
183
|
+
// A failed authorization leaves whatever stale credential got us here in
|
|
184
|
+
// place, so re-apply the same expiry test rather than reporting a machine as
|
|
185
|
+
// signed in on the strength of a dead token.
|
|
186
|
+
if (!isAuthenticated(runtime)) {
|
|
245
187
|
throw new CliError({
|
|
246
|
-
code: '
|
|
247
|
-
message:
|
|
188
|
+
code: 'auth_missing',
|
|
189
|
+
message: `Authorization did not complete for profile "${runtime.profileName}".`,
|
|
248
190
|
exitCode: EXIT_CODES.auth,
|
|
249
|
-
|
|
250
|
-
hints: [
|
|
251
|
-
{
|
|
252
|
-
command: `Install Notis Desktop: ${signup.desktop_download_url || 'https://notis.ai/channels/desktop-app'}`,
|
|
253
|
-
reason: 'The desktop app is what writes the CLI credential',
|
|
254
|
-
},
|
|
255
|
-
{ command: 'notis login', reason: 'Authorize this machine directly in a browser' },
|
|
256
|
-
{ command: 'notis start --brief-only', reason: 'Resume once authentication is complete' },
|
|
257
|
-
{ command: 'notis doctor', reason: 'Re-check config, auth, and API reachability' },
|
|
258
|
-
],
|
|
191
|
+
hints: getAuthRecovery(runtime, { mode: 'missing' }).hints,
|
|
259
192
|
});
|
|
260
193
|
}
|
|
261
194
|
|
|
262
|
-
|
|
263
|
-
return output.emitSuccess({
|
|
264
|
-
command: 'start',
|
|
265
|
-
data: { authenticated: true, brief: brief.markdown, brief_source: brief.source },
|
|
266
|
-
humanSummary: 'Notis CLI is authenticated. Follow the onboarding brief below.',
|
|
267
|
-
renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
|
|
268
|
-
});
|
|
195
|
+
return authenticatedResult(ctx);
|
|
269
196
|
}
|
|
270
197
|
|
|
271
198
|
export const onboardingCommandSpecs = [
|
|
272
199
|
{
|
|
273
200
|
command_path: ['start'],
|
|
274
|
-
summary: 'Create or access a Notis account and authorize
|
|
201
|
+
summary: 'Create or access a Notis account and authorize this CLI profile.',
|
|
275
202
|
when_to_use:
|
|
276
|
-
'Run this first on a new machine, before anything that needs auth. Safe to re-run
|
|
203
|
+
'Run this first on a new machine, before anything that needs auth. Safe to re-run. An account that has already completed onboarding gets orientation instead of an onboarding brief.',
|
|
277
204
|
args_schema: {
|
|
278
205
|
arguments: [],
|
|
279
206
|
options: [
|
|
280
|
-
{ flags: '--email <email>', description: 'Email to create or sign in the Notis account with.' },
|
|
281
|
-
{ flags: '--use-case <slug>', description: 'Primary use case (repeatable).', collect: true },
|
|
282
|
-
{
|
|
283
|
-
flags: '--wait-timeout-ms <n>',
|
|
284
|
-
description: 'How long to wait for the desktop sign-in (default 300000).',
|
|
285
|
-
},
|
|
286
|
-
{ flags: '--no-wait', description: 'Send the link and exit without waiting.' },
|
|
287
207
|
{ flags: '--brief-only', description: 'Print the onboarding brief for an already-signed-in profile.' },
|
|
288
208
|
],
|
|
289
209
|
},
|
|
290
210
|
examples: [
|
|
291
|
-
'notis start
|
|
211
|
+
'notis start',
|
|
212
|
+
'notis start --profile work',
|
|
292
213
|
'notis start --brief-only',
|
|
293
|
-
'notis start --
|
|
214
|
+
'notis start --json',
|
|
294
215
|
],
|
|
295
216
|
output_schema:
|
|
296
|
-
'Returns {authenticated, brief, brief_source} once signed in
|
|
217
|
+
'Returns {authenticated, profile, api_base, onboarding_complete, known_settings, missing_settings, brief, brief_source} once signed in — brief is null when onboarding_complete is true — or {authorize_url, expires_in, redeem_command} while waiting for browser authorization.',
|
|
297
218
|
mutates: true,
|
|
298
219
|
idempotent: true,
|
|
299
220
|
require_auth: false,
|
|
300
|
-
|
|
301
|
-
|
|
221
|
+
allow_unknown_profile: true,
|
|
222
|
+
related_commands: ['notis login', 'notis profile list', 'notis doctor', 'notis tools link'],
|
|
223
|
+
backend_call: { type: 'oauth', name: 'authorization_code+pkce' },
|
|
302
224
|
handler: startHandler,
|
|
303
225
|
},
|
|
304
226
|
];
|