@notis_ai/cli 0.2.9 → 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.
@@ -56,12 +56,18 @@ export async function runToolCommand({
56
56
  mutating = false,
57
57
  idempotencyKey,
58
58
  fileBindings = [],
59
+ sendIdempotencyKeyWhenReading = false,
59
60
  }) {
61
+ // The server owns effect classification and requires a key whenever *its*
62
+ // metadata says write or unknown — a client-side `mutating: false` hint does
63
+ // not exempt the call. Callers that knowingly dispatch through an
64
+ // unknown-classified wrapper (e.g. COMPOSIO_MULTI_EXECUTE_TOOL) opt in so the
65
+ // request carries a key instead of being rejected as idempotency_key_required.
60
66
  const result = await callTool({
61
67
  runtime: { ...runtime, mutating },
62
68
  toolName,
63
69
  arguments_,
64
- idempotencyKey: mutating ? idempotencyKey : null,
70
+ idempotencyKey: mutating || sendIdempotencyKeyWhenReading ? idempotencyKey : null,
65
71
  fileBindings,
66
72
  });
67
73
  return result;
@@ -5,16 +5,19 @@ import { onboardingCommandSpecs } from './onboarding.js';
5
5
  import { diagnosticCommandSpecs } from './diagnostics.js';
6
6
  import { smokeCommandSpecs } from './smoke.js';
7
7
  import { authCommandSpecs } from './auth.js';
8
+ import { profileCommandSpecs } from './profile.js';
8
9
 
9
10
  export const GROUP_SUMMARIES = {
10
11
  apps: 'Develop, deploy, and submit Notis Apps.',
11
12
  tools: 'Discover and execute generic tools exposed through Notis.',
13
+ profile: 'Switch between signed-in accounts and their API endpoints.',
12
14
  debug: 'Inspect effective runtime context, worker identity, and trace costs.',
13
15
  smoke: 'Run deterministic connected-service smoke tests with guaranteed cleanup.',
14
16
  };
15
17
 
16
18
  export const COMMAND_SPECS = [
17
19
  ...authCommandSpecs,
20
+ ...profileCommandSpecs,
18
21
  ...onboardingCommandSpecs,
19
22
  ...appsCommandSpecs,
20
23
  ...toolsCommandSpecs,
@@ -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, getDesktopAuthRecovery } from '../runtime/desktop-auth.js';
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
- identity: 'ok',
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(ctx.runtime.worktreeRuntime), ctx.runtime.profileName);
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
- const desktopUserId = getJwtCanonicalUserId(profile.jwt);
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
- desktopUserId
45
- && profile.oauth_user_id
46
- && desktopUserId !== profile.oauth_user_id
45
+ ctx.runtime.worktreeRuntimeUnavailable
46
+ && ctx.runtime.profileSource !== 'explicit'
47
47
  ) {
48
- checks.identity = 'error';
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(ctx.runtime.worktreeRuntime), ctx.runtime.profileName);
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(...getDesktopAuthRecovery(ctx.runtime, { mode: 'missing' }).hints);
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: 'Open the Notis desktop app and sign in again', reason: 'Refresh local CLI configuration' });
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 { getDesktopAuthRecovery, waitForDesktopAuth } from '../runtime/desktop-auth.js';
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
- async function requestSignupLink(apiBase, { email, useCases }) {
51
- const response = await fetch(`${apiBase.replace(/\/$/, '')}/signup/agent`, {
52
- method: 'POST',
53
- headers: { 'Content-Type': 'application/json' },
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
- if (response.status === 429) {
62
- // Requesting another link invalidates the previous one, so retrying is
63
- // actively harmful. Surface the cooldown and stop.
64
- throw new CliError({
65
- code: 'signup_throttled',
66
- message: payload?.message || 'A sign-in link was just sent to this address.',
67
- exitCode: EXIT_CODES.usage,
68
- details: payload || {},
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
- command: 'Open the newest Notis email on this machine',
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
- if (!response.ok) {
79
- throw new CliError({
80
- code: 'signup_failed',
81
- message: payload?.message || `Signup failed with status ${response.status}`,
82
- exitCode: EXIT_CODES.backend,
83
- details: payload || {},
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 and agent runs surface
99
- // the normal authentication recovery below.
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-authenticated machine skips straight to the brief.
109
- // Agents retry commands, and a second signup would invalidate the first email.
110
- if (authenticated || options.briefOnly) {
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 (!options.email) {
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: 'signup_email_required',
181
- message: 'An email address is required to create or access a Notis account.',
182
- exitCode: EXIT_CODES.usage,
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
- const signup = await requestSignupLink(apiBase, {
193
- email: options.email,
194
- useCases: options.useCase,
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: { authenticated: false, next_action: 'open_email_link', ...signup },
205
- humanSummary: `A sign-in link is on its way to ${options.email}.`,
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
- const timeoutMs = Number(options.waitTimeoutMs) > 0 ? Number(options.waitTimeoutMs) : 300_000;
210
- const jwt = await waitForDesktopAuth({
211
- loadConfig,
212
- getJwt: (config, profileName) => {
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: 'auth_timeout',
247
- message: 'Timed out waiting for Notis Desktop to authenticate the CLI.',
188
+ code: 'auth_missing',
189
+ message: `Authorization did not complete for profile "${runtime.profileName}".`,
248
190
  exitCode: EXIT_CODES.auth,
249
- details: { desktop_download_url: signup.desktop_download_url || null },
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
- const brief = await fetchBrief(apiBase, runtime.timeoutMs);
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 the CLI.',
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: an already-signed-in machine just reprints the onboarding brief.',
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 --email you@example.com',
211
+ 'notis start',
212
+ 'notis start --profile work',
292
213
  'notis start --brief-only',
293
- 'notis start --email you@example.com --json',
214
+ 'notis start --json',
294
215
  ],
295
216
  output_schema:
296
- 'Returns {authenticated, brief, brief_source} once signed in, or {next_action, cooldown_seconds, desktop_download_url} while waiting.',
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
- related_commands: ['notis doctor', 'notis tools link'],
301
- backend_call: { type: 'http', name: 'POST /signup/agent' },
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
  ];