@notis_ai/cli 0.2.0-beta.16.1 → 0.2.0-beta.160.1

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.
Files changed (154) hide show
  1. package/README.md +433 -133
  2. package/config/notis_app_boundary_rules.json +50 -0
  3. package/config/notis_app_design_rules.json +135 -0
  4. package/dist/agent-hooks/notis-agent-hook.mjs +18672 -0
  5. package/dist/base-skills/notis-apps/SKILL.md +70 -0
  6. package/dist/base-skills/notis-apps/references/architecture.md +164 -0
  7. package/dist/base-skills/notis-apps/references/context.md +81 -0
  8. package/dist/base-skills/notis-apps/references/design.md +165 -0
  9. package/dist/base-skills/notis-apps/references/reading.md +89 -0
  10. package/dist/base-skills/notis-apps/references/release.md +99 -0
  11. package/dist/base-skills/notis-apps/references/sdk.md +62 -0
  12. package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +140 -0
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
  15. package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
  16. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  17. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  18. package/dist/base-skills/notis-query/SKILL.md +67 -0
  19. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  20. package/dist/base-skills/notis-query/references/documents.md +50 -0
  21. package/dist/base-skills/notis-query/references/query.md +543 -0
  22. package/dist/skill-sync/index.js +1626 -0
  23. package/dist/skill-sync/index.js.map +7 -0
  24. package/dist/skill-sync-worker.mjs +2990 -0
  25. package/package.json +16 -6
  26. package/skills/notis-apps/cli.md +313 -0
  27. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  28. package/skills/notis-onboarding/BRIEF.md +129 -0
  29. package/skills/notis-query/cli.md +39 -0
  30. package/src/agent-hook-entry.js +5 -0
  31. package/src/cli.js +294 -25
  32. package/src/command-specs/agents.js +392 -0
  33. package/src/command-specs/apps.js +1470 -202
  34. package/src/command-specs/auth.js +114 -137
  35. package/src/command-specs/diagnostics.js +716 -0
  36. package/src/command-specs/handover.js +374 -0
  37. package/src/command-specs/helpers.js +84 -82
  38. package/src/command-specs/index.js +25 -6
  39. package/src/command-specs/meta.js +150 -18
  40. package/src/command-specs/onboarding.js +290 -0
  41. package/src/command-specs/profile.js +358 -0
  42. package/src/command-specs/reports.js +86 -0
  43. package/src/command-specs/skills.js +75 -0
  44. package/src/command-specs/smoke.js +386 -0
  45. package/src/command-specs/tools.js +455 -139
  46. package/src/runtime/agent-browser.js +632 -0
  47. package/src/runtime/agent-memory-state.js +126 -0
  48. package/src/runtime/agent-setup.js +383 -0
  49. package/src/runtime/app-boundary-validator.js +404 -0
  50. package/src/runtime/app-changelog.js +79 -0
  51. package/src/runtime/app-platform.js +2633 -210
  52. package/src/runtime/app-registry-scaffolds.js +367 -0
  53. package/src/runtime/app-test-server.js +292 -0
  54. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  55. package/src/runtime/auth-recovery.js +110 -0
  56. package/src/runtime/base-skills.d.ts +20 -0
  57. package/src/runtime/base-skills.js +167 -0
  58. package/src/runtime/channel.js +133 -0
  59. package/src/runtime/delegated-context.js +68 -0
  60. package/src/runtime/errors.js +1 -0
  61. package/src/runtime/git.js +233 -0
  62. package/src/runtime/login-listener.js +15 -0
  63. package/src/runtime/oauth.js +2622 -0
  64. package/src/runtime/output.js +37 -5
  65. package/src/runtime/ports.js +31 -0
  66. package/src/runtime/profiles.js +906 -55
  67. package/src/runtime/skill-sync/cloud-client.ts +99 -0
  68. package/src/runtime/skill-sync/index.ts +697 -0
  69. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  70. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  71. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  72. package/src/runtime/skill-sync/types.ts +110 -0
  73. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  74. package/src/runtime/skill-sync-service.js +109 -0
  75. package/src/runtime/store-screenshot.js +143 -0
  76. package/src/runtime/sync-skills.d.ts +37 -0
  77. package/src/runtime/sync-skills.js +231 -0
  78. package/src/runtime/telemetry.js +92 -0
  79. package/src/runtime/transport.js +324 -45
  80. package/src/skill-sync-worker-entry.js +2 -0
  81. package/src/skill-sync-worker.js +50 -0
  82. package/template/.harness/index.html.tmpl +430 -0
  83. package/template/CHANGELOG.md +5 -0
  84. package/template/app/layout.tsx +5 -2
  85. package/template/app/page.tsx +49 -42
  86. package/template/components/page-heading.tsx +23 -0
  87. package/template/components/ui/badge.tsx +7 -4
  88. package/template/components/ui/card.tsx +24 -11
  89. package/template/components/ui/native-select.tsx +24 -0
  90. package/template/notis.config.ts +24 -6
  91. package/template/package-lock.json +4137 -0
  92. package/template/package.json +5 -5
  93. package/template/packages/{notis-sdk → sdk}/package.json +13 -3
  94. package/template/packages/sdk/src/agentContext.ts +36 -0
  95. package/template/packages/sdk/src/components/DocumentEditor.tsx +103 -0
  96. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  97. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  98. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  99. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  100. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  101. package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  102. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  103. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  104. package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
  105. package/template/packages/sdk/src/config.ts +257 -0
  106. package/template/packages/sdk/src/documents.ts +256 -0
  107. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  108. package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
  109. package/template/packages/sdk/src/hooks/useCloudComputer.ts +64 -0
  110. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +836 -0
  111. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  112. package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  113. package/template/packages/sdk/src/hooks/useDocument.ts +43 -0
  114. package/template/packages/sdk/src/hooks/useDocuments.ts +84 -0
  115. package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
  116. package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
  117. package/template/packages/sdk/src/hooks/useMultiSelect.ts +95 -0
  118. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +10 -4
  119. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +11 -8
  120. package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
  121. package/template/packages/sdk/src/hooks/useTool.ts +65 -0
  122. package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
  123. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +81 -0
  124. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  125. package/template/packages/sdk/src/index.ts +161 -0
  126. package/template/packages/sdk/src/interactions/actions.ts +59 -0
  127. package/template/packages/sdk/src/interactions/shortcuts.tsx +694 -0
  128. package/template/packages/sdk/src/interactions/visibility.ts +13 -0
  129. package/template/packages/sdk/src/interactions.ts +45 -0
  130. package/template/packages/sdk/src/provider.tsx +44 -0
  131. package/template/packages/sdk/src/queryCache.ts +170 -0
  132. package/template/packages/sdk/src/runtime.ts +451 -0
  133. package/template/packages/sdk/src/styles.css +213 -0
  134. package/template/packages/sdk/src/tailwind.ts +56 -0
  135. package/template/packages/{notis-sdk → sdk}/src/vite.ts +5 -1
  136. package/template/tailwind.config.ts +1 -0
  137. package/src/command-specs/db.js +0 -163
  138. package/src/runtime/app-preview-server.js +0 -312
  139. package/template/packages/notis-sdk/src/config.ts +0 -48
  140. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  141. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  142. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  143. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  144. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  145. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  146. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  147. package/template/packages/notis-sdk/src/index.ts +0 -47
  148. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  149. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  150. package/template/packages/notis-sdk/src/styles.css +0 -123
  151. /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
  152. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
  153. /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
  154. /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
@@ -1,15 +1,91 @@
1
- import { healthCheck, probeAuth } from './helpers.js';
1
+ import { dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ import { COMPOSIO_SEARCH_TOOLS, healthCheck, probeAuth } from './helpers.js';
2
5
  import { findCommandSpec, formatDescribe } from '../runtime/help.js';
6
+ import { createExpiredAuthError, getAuthRecovery } from '../runtime/auth-recovery.js';
7
+ import { cliCommandForChannel, resolveChannelSwitch } from '../runtime/channel.js';
8
+ import {
9
+ credentialIsExpired,
10
+ getProfile,
11
+ loadConfig,
12
+ } from '../runtime/profiles.js';
13
+ import { ensureFreshOAuthCredential } from '../runtime/oauth.js';
14
+
15
+ export const DOCTOR_TOOL_ROUNDTRIP_TIMEOUT_MS = 90_000;
16
+
17
+ export function doctorToolRoundtripRuntime(runtime) {
18
+ return {
19
+ ...runtime,
20
+ timeoutMs: Math.max(runtime.timeoutMs || 0, DOCTOR_TOOL_ROUNDTRIP_TIMEOUT_MS),
21
+ };
22
+ }
23
+
24
+ export function doctorChannelSummary(
25
+ runtime,
26
+ moduleDirectory = dirname(fileURLToPath(import.meta.url)),
27
+ ) {
28
+ const decision = resolveChannelSwitch({
29
+ runningVersion: runtime.cliVersion,
30
+ profile: {
31
+ channel: runtime.channel,
32
+ api_base: runtime.apiBase,
33
+ },
34
+ moduleDirectory,
35
+ });
36
+ const mismatch = Boolean(
37
+ decision.targetChannel
38
+ && decision.targetChannel !== decision.runningChannel,
39
+ );
40
+ const releaseChannel = runtime.worktreeRuntime ? 'dev' : runtime.channel;
41
+ return {
42
+ decision,
43
+ mismatch,
44
+ releaseChannel,
45
+ status: runtime.worktreeRuntime
46
+ ? 'dev'
47
+ : mismatch
48
+ ? `mismatch:${decision.reason}`
49
+ : decision.runningChannel,
50
+ };
51
+ }
3
52
 
4
53
  async function doctorHandler(ctx) {
5
54
  const checks = {
6
55
  config: 'ok',
7
56
  auth: 'missing',
57
+ channel: 'unknown',
58
+ routing: 'ok',
8
59
  health: 'unknown',
9
60
  tool_roundtrip: 'unknown',
10
61
  };
11
62
 
12
- checks.auth = ctx.runtime.jwt ? 'configured' : 'missing';
63
+ if (ctx.runtime.credentialKind === 'oauth') {
64
+ try {
65
+ await ensureFreshOAuthCredential(ctx.runtime);
66
+ } catch {
67
+ // Doctor still reports the remaining health and recovery checks when a
68
+ // refresh endpoint is unavailable or rejects the stored credential.
69
+ }
70
+ }
71
+ let profile = getProfile(loadConfig(), ctx.runtime.profileName);
72
+ checks.auth = ctx.runtime.jwt
73
+ ? (credentialIsExpired(ctx.runtime, profile) ? 'expired' : 'configured')
74
+ : 'missing';
75
+ // A worktree whose ./dev.sh has stopped leaves commands with no local
76
+ // backend to reach. Say so here rather than letting every later command fail
77
+ // as an opaque network error.
78
+ if (
79
+ ctx.runtime.worktreeRuntimeUnavailable
80
+ && ctx.runtime.profileSource !== 'explicit'
81
+ ) {
82
+ checks.routing = 'dev_runtime_unavailable';
83
+ } else if (
84
+ ctx.runtime.detachedWorktreeRuntime
85
+ || (ctx.runtime.worktreeRuntimeUnavailable && ctx.runtime.profileSource === 'explicit')
86
+ ) {
87
+ checks.routing = 'detached';
88
+ }
13
89
 
14
90
  try {
15
91
  await healthCheck(ctx.runtime);
@@ -20,19 +96,54 @@ async function doctorHandler(ctx) {
20
96
 
21
97
  if (ctx.runtime.jwt) {
22
98
  try {
23
- const payload = await probeAuth(ctx.runtime);
24
- checks.tool_roundtrip = Array.isArray(payload.toolkits) ? 'ok' : 'error';
99
+ // Tool discovery may have to query several connected MCP servers on a
100
+ // cold local backend. A diagnostic must not report a false failure just
101
+ // because that legitimate roundtrip exceeds the general 30s default.
102
+ const payload = await probeAuth(doctorToolRoundtripRuntime(ctx.runtime));
103
+ checks.tool_roundtrip = Array.isArray(payload.toolkit_connection_statuses) ? 'ok' : 'error';
104
+ profile = getProfile(loadConfig(), ctx.runtime.profileName);
105
+ checks.auth = ctx.runtime.jwt
106
+ ? (credentialIsExpired(ctx.runtime, profile) ? 'expired' : 'configured')
107
+ : 'missing';
25
108
  } catch {
26
109
  checks.tool_roundtrip = 'error';
27
110
  }
28
111
  }
29
112
 
113
+ // A mismatch here means the automatic hand-off could not happen: a source
114
+ // checkout, an explicit opt-out, or a switch that could not reach npm. The
115
+ // profile still routes to the right API, so this reports rather than fails.
116
+ const {
117
+ decision: channelDecision,
118
+ mismatch: channelMismatch,
119
+ releaseChannel,
120
+ status: channelStatus,
121
+ } = doctorChannelSummary(ctx.runtime);
122
+ checks.channel = channelStatus;
123
+
30
124
  const hints = [];
125
+ if (channelMismatch) {
126
+ hints.push({
127
+ command: `${cliCommandForChannel(channelDecision.targetChannel)} doctor`,
128
+ reason: `Profile "${ctx.runtime.profileName}" belongs to the ${channelDecision.targetChannel} CLI channel`,
129
+ });
130
+ }
31
131
  if (checks.auth === 'missing') {
32
- hints.push({ command: 'notis auth login --jwt <token>', reason: 'Configure credentials' });
132
+ hints.push(...getAuthRecovery(ctx.runtime, { mode: 'missing' }).hints);
133
+ } else if (checks.auth === 'expired') {
134
+ hints.push(...createExpiredAuthError(ctx.runtime).hints);
135
+ }
136
+ if (checks.routing === 'dev_runtime_unavailable') {
137
+ hints.push(...ctx.runtime.worktreeRuntimeUnavailable.hints);
138
+ } else if (checks.routing === 'detached') {
139
+ hints.push({
140
+ message: ctx.runtime.detachedWorktreeRuntime?.profile
141
+ ? `This worktree's ./dev.sh profile is "${ctx.runtime.detachedWorktreeRuntime.profile}"; profile "${ctx.runtime.profileName}" bypasses it.`
142
+ : `Explicit profile "${ctx.runtime.profileName}" bypasses this stopped worktree runtime.`,
143
+ });
33
144
  }
34
- if (checks.health === 'error') {
35
- hints.push({ command: 'notis auth status', reason: 'Check API base URL configuration' });
145
+ if (checks.health === 'error' && checks.auth !== 'expired') {
146
+ hints.push({ command: 'notis profile show', reason: 'Check which API endpoint this profile targets' });
36
147
  }
37
148
  if (checks.tool_roundtrip === 'error') {
38
149
  hints.push({ command: 'notis whoami', reason: 'Verify your account and permissions' });
@@ -42,7 +153,19 @@ async function doctorHandler(ctx) {
42
153
  command: ctx.spec.command_path.join(' '),
43
154
  data: {
44
155
  profile: ctx.runtime.profileName,
156
+ profile_source: ctx.runtime.profileSource,
45
157
  api_base: ctx.runtime.apiBase,
158
+ release_channel: releaseChannel || null,
159
+ cli_version: ctx.runtime.cliVersion || null,
160
+ credential_source: ctx.runtime.credentialKind || null,
161
+ ...(ctx.runtime.credentialKind === 'oauth'
162
+ ? {
163
+ oauth_client_id: profile.oauth_client_id || null,
164
+ oauth_scopes: profile.oauth_scopes || [],
165
+ oauth_access_expires_at: profile.oauth_access_expires_at || null,
166
+ oauth_refresh_expires_at: profile.oauth_refresh_expires_at || null,
167
+ }
168
+ : {}),
46
169
  checks,
47
170
  },
48
171
  humanSummary: `Doctor checks completed for profile ${ctx.runtime.profileName}`,
@@ -66,19 +189,27 @@ function decodeJwtUserId(jwt) {
66
189
  }
67
190
  }
68
191
 
192
+ export function activeRuntimeUserId(runtime) {
193
+ return runtime.credentialKind === 'oauth'
194
+ ? runtime.oauthUserId
195
+ : decodeJwtUserId(runtime.jwt);
196
+ }
197
+
69
198
  async function whoamiHandler(ctx) {
70
199
  const payload = await probeAuth(ctx.runtime);
71
- const toolkits = payload.toolkits || [];
72
- const userId = decodeJwtUserId(ctx.runtime.jwt);
200
+ const toolkits = payload.toolkit_connection_statuses || [];
201
+ const userId = activeRuntimeUserId(ctx.runtime);
73
202
 
74
203
  return ctx.output.emitSuccess({
75
204
  command: ctx.spec.command_path.join(' '),
76
205
  data: {
77
206
  profile: ctx.runtime.profileName,
207
+ profile_source: ctx.runtime.profileSource,
78
208
  api_base: ctx.runtime.apiBase,
209
+ credential_source: ctx.runtime.credentialKind || null,
79
210
  user_id: userId,
80
211
  toolkit_count: toolkits.length,
81
- toolkits: toolkits.map((t) => t.id),
212
+ toolkits: toolkits.map((t) => t.toolkit),
82
213
  cli_version: ctx.runtime.cliVersion,
83
214
  },
84
215
  humanSummary: `Logged in as ${userId || 'unknown'} via profile "${ctx.runtime.profileName}"`,
@@ -91,8 +222,8 @@ async function whoamiHandler(ctx) {
91
222
  `Version: ${ctx.runtime.cliVersion}`,
92
223
  ].join('\n'),
93
224
  hints: [
94
- { command: 'notis tools toolkits', reason: 'List available toolkit namespaces' },
95
- { command: 'notis doctor', reason: 'Run a full health check' },
225
+ { command: 'notis profile list', reason: 'See the other accounts this machine can switch to' },
226
+ { command: 'notis tools toolkits', reason: 'List available toolkit namespaces and connection statuses' },
96
227
  ],
97
228
  });
98
229
  }
@@ -110,15 +241,15 @@ async function describeHandler(ctx) {
110
241
  export const metaCommandSpecs = [
111
242
  {
112
243
  command_path: ['whoami'],
113
- summary: 'Display the active profile, user, and available toolkits.',
244
+ summary: 'Display the active profile, user, and available toolkit connection statuses.',
114
245
  when_to_use: 'Use this to quickly confirm which account and environment a command will target.',
115
246
  args_schema: { arguments: [], options: [] },
116
247
  examples: ['notis whoami', 'notis whoami --json'],
117
248
  output_schema: 'Returns profile, api_base, user_id, toolkit count, and CLI version.',
118
249
  mutates: false,
119
250
  idempotent: true,
120
- related_commands: ['notis auth status --verify', 'notis doctor'],
121
- backend_call: { type: 'tool', name: 'notis_find_toolkits' },
251
+ related_commands: ['notis doctor'],
252
+ backend_call: { type: 'tool', name: COMPOSIO_SEARCH_TOOLS },
122
253
  handler: whoamiHandler,
123
254
  },
124
255
  {
@@ -130,7 +261,8 @@ export const metaCommandSpecs = [
130
261
  output_schema: 'Returns config, auth, health, and roundtrip check statuses.',
131
262
  mutates: false,
132
263
  idempotent: true,
133
- related_commands: ['notis auth status --verify', 'notis tools toolkits'],
264
+ require_auth: false,
265
+ related_commands: ['notis tools toolkits'],
134
266
  backend_call: { type: 'health+tool_roundtrip' },
135
267
  handler: doctorHandler,
136
268
  },
@@ -139,10 +271,10 @@ export const metaCommandSpecs = [
139
271
  summary: 'Describe a first-class CLI command in detail.',
140
272
  when_to_use: 'Use this when an agent or human needs the exact shape, examples, and semantics of a command.',
141
273
  args_schema: {
142
- arguments: [{ token: '<command...>', key: 'commandPath', description: 'Command path to describe, such as "apps push".' }],
274
+ arguments: [{ token: '<command...>', key: 'commandPath', description: 'Command path to describe, such as "apps deploy".' }],
143
275
  options: [],
144
276
  },
145
- examples: ['notis describe apps push', 'notis describe db query'],
277
+ examples: ['notis describe apps deploy', 'notis describe tools exec'],
146
278
  output_schema: 'Returns the command spec metadata for the requested command.',
147
279
  mutates: false,
148
280
  idempotent: true,
@@ -0,0 +1,290 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { CliError, EXIT_CODES } from '../runtime/errors.js';
6
+ import { getAuthRecovery } from '../runtime/auth-recovery.js';
7
+ import {
8
+ credentialIsExpired,
9
+ getProfile,
10
+ loadConfig,
11
+ } from '../runtime/profiles.js';
12
+ import { ensureFreshOAuthCredential, loginWithOAuth } from '../runtime/oauth.js';
13
+ import { runToolCommand } from './helpers.js';
14
+ import { loginAgentAuthorizationPresentation } from './auth.js';
15
+ import { installLocalAgentContext } from './agents.js';
16
+
17
+ const HERE = dirname(fileURLToPath(import.meta.url));
18
+ const BUNDLED_BRIEF_PATH = join(HERE, '..', '..', 'skills', 'notis-onboarding', 'BRIEF.md');
19
+
20
+ /**
21
+ * Ask the account whether it has already been onboarded.
22
+ *
23
+ * The brief itself is served unauthenticated and is identical for everyone, so
24
+ * it can never answer this. Without the check `start` hands a year-old account
25
+ * the new-user script, and a compliant agent re-asks the user their own name
26
+ * and calls COMPLETE_TUTORIAL on someone who converted long ago.
27
+ *
28
+ * A failure here is not fatal: an unreachable tool bridge should not block
29
+ * sign-in. It resolves to null and the caller degrades to serving the brief,
30
+ * which is the previous behaviour.
31
+ */
32
+ async function fetchOnboardingState(runtime) {
33
+ try {
34
+ const { payload } = await runToolCommand({
35
+ runtime,
36
+ toolName: 'LOCAL_NOTIS_GET_USER_SETTINGS',
37
+ arguments_: {},
38
+ });
39
+ const data = payload?.data ?? payload?.result ?? payload;
40
+ if (data && typeof data.onboarding_complete === 'boolean') {
41
+ return {
42
+ onboardingComplete: data.onboarding_complete,
43
+ settings: data.settings || {},
44
+ missingSettings: Array.isArray(data.missing_settings) ? data.missing_settings : [],
45
+ };
46
+ }
47
+ return null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * The brief is served rather than bundled so it tracks the deployed server. The
55
+ * bundled copy is a fallback for an offline or unreachable API, and the payload
56
+ * says which one the caller got so a stale brief is diagnosable.
57
+ */
58
+ async function fetchBrief(apiBase, timeoutMs) {
59
+ try {
60
+ const controller = new AbortController();
61
+ const timer = setTimeout(() => controller.abort(), Math.min(timeoutMs || 30_000, 30_000));
62
+ try {
63
+ const response = await fetch(`${apiBase.replace(/\/$/, '')}/signup/onboarding-brief`, {
64
+ signal: controller.signal,
65
+ });
66
+ if (response.ok) {
67
+ const payload = await response.json();
68
+ if (payload?.markdown) {
69
+ return { markdown: payload.markdown, source: 'server' };
70
+ }
71
+ }
72
+ } finally {
73
+ clearTimeout(timer);
74
+ }
75
+ } catch {
76
+ // fall through to the bundled copy
77
+ }
78
+
79
+ try {
80
+ return { markdown: readFileSync(BUNDLED_BRIEF_PATH, 'utf-8'), source: 'bundled' };
81
+ } catch {
82
+ return { markdown: null, source: null };
83
+ }
84
+ }
85
+
86
+ function isAuthenticated(runtime) {
87
+ return Boolean(runtime.jwt)
88
+ && !credentialIsExpired(runtime, getProfile(loadConfig(), runtime.profileName));
89
+ }
90
+
91
+ function renderAgentSetup(results) {
92
+ const configured = (results || []).filter((result) => result?.agent && result?.instructions);
93
+ if (!configured.length) return '';
94
+ const lines = configured.map((result) => {
95
+ const label = result.agent === 'claude-code' ? 'Claude Code' : 'Codex';
96
+ const instructions = result.instructions?.status || 'skipped';
97
+ const memoryStatus = result.memory_hook?.status || 'skipped';
98
+ const memory = memoryStatus === 'preserved'
99
+ ? 'memory hooks not changed'
100
+ : `memory recall and capture ${memoryStatus}`;
101
+ return `- ${label}: instructions ${instructions}; ${memory}`;
102
+ });
103
+ if (configured.some((result) => (
104
+ result.agent === 'codex'
105
+ && ['installed', 'updated', 'unchanged'].includes(result.memory_hook?.status)
106
+ ))) {
107
+ lines.push('- Codex: open /hooks once and trust the Notis hooks before they can run.');
108
+ }
109
+ return ['## Coding-agent setup', ...lines].join('\n');
110
+ }
111
+
112
+ function agentSetupHints(results) {
113
+ const hints = [];
114
+ if ((results || []).some((result) => (
115
+ result.agent === 'codex'
116
+ && ['installed', 'updated', 'unchanged'].includes(result.memory_hook?.status)
117
+ ))) {
118
+ hints.push({ message: 'In Codex, open /hooks once and trust the Notis hooks.' });
119
+ }
120
+ if ((results || []).some((result) => result.status === 'not_detected')) {
121
+ hints.push({ command: 'notis agents install', reason: 'Configure Codex and Claude Code later if they were not detected now' });
122
+ }
123
+ if ((results || []).some((result) => result.memory_hook?.status === 'preserved')) {
124
+ hints.push({ command: 'notis agents install', reason: 'Explicitly enable Notis memory recall and completed-turn capture' });
125
+ }
126
+ return hints;
127
+ }
128
+
129
+ /**
130
+ * What an authenticated `start` reports.
131
+ *
132
+ * A returning user does not need an onboarding script — they need orientation:
133
+ * which account, which endpoint, and confirmation that nothing is expected of
134
+ * them. Only an account that has genuinely not finished onboarding gets the
135
+ * brief, so `brief: null` is a positive signal to the agent, not a failure.
136
+ */
137
+ async function authenticatedResult(ctx) {
138
+ const state = await fetchOnboardingState(ctx.runtime);
139
+ const onboardingComplete = state?.onboardingComplete === true;
140
+ const base = {
141
+ authenticated: true,
142
+ profile: ctx.runtime.profileName,
143
+ api_base: ctx.runtime.apiBase,
144
+ credential_source: ctx.runtime.credentialKind,
145
+ onboarding_complete: onboardingComplete,
146
+ ...(state ? { known_settings: state.settings, missing_settings: state.missingSettings } : {}),
147
+ };
148
+ try {
149
+ base.agent_setup = installLocalAgentContext(ctx, {
150
+ onlyExisting: true,
151
+ memoryHooks: null,
152
+ });
153
+ } catch {
154
+ // Authentication and onboarding remain usable if a local vendor config is
155
+ // malformed or read-only. `notis agents install` reports the exact path.
156
+ base.agent_setup = [];
157
+ }
158
+
159
+ if (onboardingComplete) {
160
+ const name = state?.settings?.first_name;
161
+ const setupSummary = renderAgentSetup(base.agent_setup);
162
+ return ctx.output.emitSuccess({
163
+ command: 'start',
164
+ data: { ...base, brief: null, brief_source: null },
165
+ humanSummary:
166
+ `Profile "${ctx.runtime.profileName}" is signed in and this account is already set up.`,
167
+ renderHuman: () =>
168
+ [
169
+ `Signed in${name ? ` as ${name}` : ''} on profile "${ctx.runtime.profileName}".`,
170
+ `API: ${ctx.runtime.apiBase}`,
171
+ '',
172
+ 'This account has already completed onboarding. Do not run an onboarding',
173
+ 'flow and do not call LOCAL_NOTIS_COMPLETE_TUTORIAL.',
174
+ ...(setupSummary ? ['', setupSummary] : []),
175
+ ].join('\n'),
176
+ hints: [
177
+ { command: 'notis whoami', reason: 'Show the account and its connected toolkits' },
178
+ { command: 'notis tools search "<what you need>"', reason: 'Find a tool and get on with the task' },
179
+ ...agentSetupHints(base.agent_setup),
180
+ ],
181
+ });
182
+ }
183
+
184
+ const brief = await fetchBrief(ctx.runtime.apiBase, ctx.runtime.timeoutMs);
185
+ const setupSummary = renderAgentSetup(base.agent_setup);
186
+ return ctx.output.emitSuccess({
187
+ command: 'start',
188
+ data: { ...base, brief: brief.markdown, brief_source: brief.source },
189
+ humanSummary: `Notis CLI is authenticated for profile "${ctx.runtime.profileName}". Onboarding is not complete.`,
190
+ renderHuman: () => [
191
+ brief.markdown || 'Notis CLI is authenticated.',
192
+ setupSummary,
193
+ ].filter(Boolean).join('\n\n'),
194
+ hints: agentSetupHints(base.agent_setup),
195
+ });
196
+ }
197
+
198
+ async function startHandler(ctx) {
199
+ const { runtime, options, output } = ctx;
200
+ if (runtime.credentialKind === 'oauth') {
201
+ try {
202
+ await ensureFreshOAuthCredential(runtime);
203
+ } catch {
204
+ // A failed refresh is equivalent to no usable session here. Interactive
205
+ // starts may still authorize again; brief-only runs surface the normal
206
+ // authentication recovery below.
207
+ }
208
+ }
209
+
210
+ // Safe to re-run: an already-authorized profile skips straight to the brief.
211
+ if (isAuthenticated(runtime)) {
212
+ return authenticatedResult(ctx);
213
+ }
214
+
215
+ if (options.briefOnly) {
216
+ throw new CliError({
217
+ code: 'auth_missing',
218
+ message: `Profile "${runtime.profileName}" is not signed in to Notis yet.`,
219
+ exitCode: EXIT_CODES.auth,
220
+ hints: getAuthRecovery(runtime, { mode: 'missing' }).hints,
221
+ });
222
+ }
223
+
224
+ // Browser authorization is the whole signup path: it creates the account when
225
+ // the address is new and authorizes this machine either way. There is nothing
226
+ // for the CLI to collect up front, and nothing to wait on afterwards.
227
+ const authorization = await loginWithOAuth(
228
+ runtime,
229
+ { browser: true, mode: options.mode, reusePersistedCredential: true },
230
+ output,
231
+ );
232
+ if (authorization?.agentAuthorization) {
233
+ const presentation = loginAgentAuthorizationPresentation(authorization.agentAuthorization);
234
+ return output.emitSuccess({
235
+ command: 'start',
236
+ data: {
237
+ authenticated: false,
238
+ profile: runtime.profileName,
239
+ ...authorization.agentAuthorization,
240
+ },
241
+ ...presentation,
242
+ });
243
+ }
244
+
245
+ // A failed authorization leaves whatever stale credential got us here in
246
+ // place, so re-apply the same expiry test rather than reporting a machine as
247
+ // signed in on the strength of a dead token.
248
+ if (!isAuthenticated(runtime)) {
249
+ throw new CliError({
250
+ code: 'auth_missing',
251
+ message: `Authorization did not complete for profile "${runtime.profileName}".`,
252
+ exitCode: EXIT_CODES.auth,
253
+ hints: getAuthRecovery(runtime, { mode: 'missing' }).hints,
254
+ });
255
+ }
256
+
257
+ return authenticatedResult(ctx);
258
+ }
259
+
260
+ export const onboardingCommandSpecs = [
261
+ {
262
+ command_path: ['start'],
263
+ summary: 'Create or access a Notis account and authorize this CLI profile.',
264
+ when_to_use:
265
+ '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.',
266
+ args_schema: {
267
+ arguments: [],
268
+ options: [
269
+ { flags: '--brief-only', description: 'Print the onboarding brief for an already-signed-in profile.' },
270
+ { flags: '--mode <mode>', description: 'auto (default) hands the browser callback to a background listener when this command cannot wait; browser waits in-process; code shows a one-time code to copy.' },
271
+ ],
272
+ },
273
+ examples: [
274
+ 'notis start',
275
+ 'notis start --profile work',
276
+ 'notis start --mode browser',
277
+ 'notis start --brief-only',
278
+ 'notis start --json',
279
+ ],
280
+ output_schema:
281
+ 'Returns {authenticated, profile, api_base, onboarding_complete, known_settings, missing_settings, agent_setup, brief, brief_source} once signed in — brief is null when onboarding_complete is true — or {authorize_url, expires_in, hand_off, ...} while waiting for browser authorization. hand_off is "browser_callback" when signing in finishes the login by itself (re-run confirm_command to see it land) or "code" when the user must be asked for the code the page shows and it redeemed with redeem_command.',
282
+ mutates: true,
283
+ idempotent: true,
284
+ require_auth: false,
285
+ allow_unknown_profile: true,
286
+ related_commands: ['notis login', 'notis profile list', 'notis doctor', 'notis tools link'],
287
+ backend_call: { type: 'oauth', name: 'authorization_code+pkce' },
288
+ handler: startHandler,
289
+ },
290
+ ];