@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.
- package/README.md +433 -133
- package/config/notis_app_boundary_rules.json +50 -0
- package/config/notis_app_design_rules.json +135 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +18672 -0
- package/dist/base-skills/notis-apps/SKILL.md +70 -0
- package/dist/base-skills/notis-apps/references/architecture.md +164 -0
- package/dist/base-skills/notis-apps/references/context.md +81 -0
- package/dist/base-skills/notis-apps/references/design.md +165 -0
- package/dist/base-skills/notis-apps/references/reading.md +89 -0
- package/dist/base-skills/notis-apps/references/release.md +99 -0
- package/dist/base-skills/notis-apps/references/sdk.md +62 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
- package/dist/base-skills/notis-cli/SKILL.md +140 -0
- package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
- package/dist/base-skills/notis-query/SKILL.md +67 -0
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
- package/dist/base-skills/notis-query/references/documents.md +50 -0
- package/dist/base-skills/notis-query/references/query.md +543 -0
- package/dist/skill-sync/index.js +1626 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/dist/skill-sync-worker.mjs +2990 -0
- package/package.json +16 -6
- package/skills/notis-apps/cli.md +313 -0
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +129 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/agent-hook-entry.js +5 -0
- package/src/cli.js +294 -25
- package/src/command-specs/agents.js +392 -0
- package/src/command-specs/apps.js +1470 -202
- package/src/command-specs/auth.js +114 -137
- package/src/command-specs/diagnostics.js +716 -0
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/helpers.js +84 -82
- package/src/command-specs/index.js +25 -6
- package/src/command-specs/meta.js +150 -18
- package/src/command-specs/onboarding.js +290 -0
- package/src/command-specs/profile.js +358 -0
- package/src/command-specs/reports.js +86 -0
- package/src/command-specs/skills.js +75 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +455 -139
- package/src/runtime/agent-browser.js +632 -0
- package/src/runtime/agent-memory-state.js +126 -0
- package/src/runtime/agent-setup.js +383 -0
- package/src/runtime/app-boundary-validator.js +404 -0
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-platform.js +2633 -210
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/app-test-server.js +292 -0
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/auth-recovery.js +110 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/channel.js +133 -0
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/errors.js +1 -0
- package/src/runtime/git.js +233 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/oauth.js +2622 -0
- package/src/runtime/output.js +37 -5
- package/src/runtime/ports.js +31 -0
- package/src/runtime/profiles.js +906 -55
- package/src/runtime/skill-sync/cloud-client.ts +99 -0
- package/src/runtime/skill-sync/index.ts +697 -0
- package/src/runtime/skill-sync/local-scanner.ts +1046 -0
- package/src/runtime/skill-sync/symlink-manager.ts +433 -0
- package/src/runtime/skill-sync/sync-plan.ts +22 -0
- package/src/runtime/skill-sync/types.ts +110 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/store-screenshot.js +143 -0
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +231 -0
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +324 -45
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +50 -0
- package/template/.harness/index.html.tmpl +430 -0
- package/template/CHANGELOG.md +5 -0
- package/template/app/layout.tsx +5 -2
- package/template/app/page.tsx +49 -42
- package/template/components/page-heading.tsx +23 -0
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/card.tsx +24 -11
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +24 -6
- package/template/package-lock.json +4137 -0
- package/template/package.json +5 -5
- package/template/packages/{notis-sdk → sdk}/package.json +13 -3
- package/template/packages/sdk/src/agentContext.ts +36 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +103 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +285 -0
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
- package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
- package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
- package/template/packages/sdk/src/config.ts +257 -0
- package/template/packages/sdk/src/documents.ts +256 -0
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +64 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +836 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +49 -0
- package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +43 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +84 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
- package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +95 -0
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +10 -4
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +11 -8
- package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
- package/template/packages/sdk/src/hooks/useTool.ts +65 -0
- package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +81 -0
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/template/packages/sdk/src/index.ts +161 -0
- package/template/packages/sdk/src/interactions/actions.ts +59 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +694 -0
- package/template/packages/sdk/src/interactions/visibility.ts +13 -0
- package/template/packages/sdk/src/interactions.ts +45 -0
- package/template/packages/sdk/src/provider.tsx +44 -0
- package/template/packages/sdk/src/queryCache.ts +170 -0
- package/template/packages/sdk/src/runtime.ts +451 -0
- package/template/packages/sdk/src/styles.css +213 -0
- package/template/packages/sdk/src/tailwind.ts +56 -0
- package/template/packages/{notis-sdk → sdk}/src/vite.ts +5 -1
- package/template/tailwind.config.ts +1 -0
- package/src/command-specs/db.js +0 -163
- package/src/runtime/app-preview-server.js +0 -312
- package/template/packages/notis-sdk/src/config.ts +0 -48
- package/template/packages/notis-sdk/src/helpers.ts +0 -131
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
- package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
- package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
- package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
- package/template/packages/notis-sdk/src/index.ts +0 -47
- package/template/packages/notis-sdk/src/provider.tsx +0 -44
- package/template/packages/notis-sdk/src/runtime.ts +0 -159
- package/template/packages/notis-sdk/src/styles.css +0 -123
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
|
@@ -1,15 +1,91 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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({
|
|
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
|
|
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.
|
|
72
|
-
const userId =
|
|
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.
|
|
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
|
|
95
|
-
{ command: 'notis
|
|
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
|
|
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
|
|
121
|
-
backend_call: { type: 'tool', name:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
];
|