@notis_ai/cli 0.2.6 → 0.2.8
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 +17 -0
- package/dist/scaffolds/notis-database/app/page.tsx +38 -41
- package/dist/scaffolds/notis-database/lib/types.ts +5 -7
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +1839 -0
- package/dist/scaffolds/notis-database/notis.config.ts +48 -1
- package/dist/scaffolds/notis-database/package-lock.json +3935 -0
- package/dist/scaffolds/notis-database/package.json +2 -1
- package/dist/scaffolds/notis-database/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +4 -1
- package/dist/scaffolds/notis-journal/CHANGELOG.md +4 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +1 -0
- package/dist/scaffolds/notis-journal/package-lock.json +4615 -0
- package/dist/scaffolds/notis-journal/package.json +2 -2
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +52 -1
- package/dist/scaffolds/notis-notes/app/page.tsx +39 -46
- package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +596 -0
- package/dist/scaffolds/notis-notes/lib/visible-properties.ts +144 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +752 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +48 -3
- package/dist/scaffolds/notis-notes/package-lock.json +4636 -0
- package/dist/scaffolds/notis-notes/package.json +7 -3
- package/dist/scaffolds/notis-notes/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-random/CHANGELOG.md +12 -2
- package/dist/scaffolds/notis-random/app/history/page.tsx +5 -4
- package/dist/scaffolds/notis-random/app/page.tsx +141 -74
- package/dist/scaffolds/notis-random/components/ui/button.tsx +50 -0
- package/dist/scaffolds/notis-random/components/ui/card.tsx +16 -0
- package/dist/scaffolds/notis-random/components/ui/input.tsx +23 -0
- package/dist/scaffolds/notis-random/components.json +20 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +26 -7
- package/dist/scaffolds/notis-random/lib/rng.ts +179 -19
- package/dist/scaffolds/notis-random/lib/roll-record.ts +103 -16
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +753 -0
- package/dist/scaffolds/notis-random/notis.config.ts +47 -3
- package/dist/scaffolds/notis-random/package-lock.json +4513 -0
- package/dist/scaffolds/notis-random/package.json +6 -2
- package/dist/scaffolds/notis-random/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +8 -1
- package/dist/scaffolds.json +8 -6
- package/package.json +2 -1
- package/skills/notis-apps/SKILL.md +449 -144
- package/skills/notis-apps/cli.md +15 -0
- package/skills/notis-cli/SKILL.md +8 -4
- package/skills/notis-onboarding/BRIEF.md +93 -0
- package/src/cli.js +52 -7
- package/src/command-specs/apps.js +154 -21
- package/src/command-specs/diagnostics.js +674 -0
- package/src/command-specs/helpers.js +4 -1
- package/src/command-specs/index.js +8 -0
- package/src/command-specs/onboarding.js +216 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +189 -5
- package/src/runtime/app-platform.js +65 -4
- package/src/runtime/desktop-auth.js +66 -17
- package/src/runtime/errors.js +1 -0
- package/src/runtime/output.js +21 -3
- package/src/runtime/profiles.js +214 -18
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +29 -1
- package/template/.harness/index.html.tmpl +30 -2
- package/template/packages/sdk/src/config.ts +38 -1
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { appsCommandSpecs } from './apps.js';
|
|
2
2
|
import { toolsCommandSpecs } from './tools.js';
|
|
3
3
|
import { metaCommandSpecs } from './meta.js';
|
|
4
|
+
import { onboardingCommandSpecs } from './onboarding.js';
|
|
5
|
+
import { diagnosticCommandSpecs } from './diagnostics.js';
|
|
6
|
+
import { smokeCommandSpecs } from './smoke.js';
|
|
4
7
|
|
|
5
8
|
export const GROUP_SUMMARIES = {
|
|
6
9
|
apps: 'Develop, deploy, and submit Notis Apps.',
|
|
7
10
|
tools: 'Discover and execute generic tools exposed through Notis.',
|
|
11
|
+
debug: 'Inspect effective runtime context, worker identity, and trace costs.',
|
|
12
|
+
smoke: 'Run deterministic connected-service smoke tests with guaranteed cleanup.',
|
|
8
13
|
};
|
|
9
14
|
|
|
10
15
|
export const COMMAND_SPECS = [
|
|
16
|
+
...onboardingCommandSpecs,
|
|
11
17
|
...appsCommandSpecs,
|
|
12
18
|
...toolsCommandSpecs,
|
|
19
|
+
...diagnosticCommandSpecs,
|
|
20
|
+
...smokeCommandSpecs,
|
|
13
21
|
...metaCommandSpecs,
|
|
14
22
|
];
|
|
@@ -0,0 +1,216 @@
|
|
|
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 { getDesktopAuthRecovery, waitForDesktopAuth } from '../runtime/desktop-auth.js';
|
|
7
|
+
import { getJwt, isJwtExpired, loadConfig } from '../runtime/profiles.js';
|
|
8
|
+
|
|
9
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const BUNDLED_BRIEF_PATH = join(HERE, '..', '..', 'skills', 'notis-onboarding', 'BRIEF.md');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The brief is served rather than bundled so it tracks the deployed server. The
|
|
14
|
+
* bundled copy is a fallback for an offline or unreachable API, and the payload
|
|
15
|
+
* says which one the caller got so a stale brief is diagnosable.
|
|
16
|
+
*/
|
|
17
|
+
async function fetchBrief(apiBase, timeoutMs) {
|
|
18
|
+
try {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), Math.min(timeoutMs || 30_000, 30_000));
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(`${apiBase.replace(/\/$/, '')}/signup/onboarding-brief`, {
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
});
|
|
25
|
+
if (response.ok) {
|
|
26
|
+
const payload = await response.json();
|
|
27
|
+
if (payload?.markdown) {
|
|
28
|
+
return { markdown: payload.markdown, source: 'server' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} finally {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// fall through to the bundled copy
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return { markdown: readFileSync(BUNDLED_BRIEF_PATH, 'utf-8'), source: 'bundled' };
|
|
40
|
+
} catch {
|
|
41
|
+
return { markdown: null, source: null };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function requestSignupLink(apiBase, { email, useCases }) {
|
|
46
|
+
const response = await fetch(`${apiBase.replace(/\/$/, '')}/signup/agent`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
email,
|
|
51
|
+
...(useCases && useCases.length ? { preferred_use_cases: useCases } : {}),
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
const payload = await response.json().catch(() => null);
|
|
55
|
+
|
|
56
|
+
if (response.status === 429) {
|
|
57
|
+
// Requesting another link invalidates the previous one, so retrying is
|
|
58
|
+
// actively harmful. Surface the cooldown and stop.
|
|
59
|
+
throw new CliError({
|
|
60
|
+
code: 'signup_throttled',
|
|
61
|
+
message: payload?.message || 'A sign-in link was just sent to this address.',
|
|
62
|
+
exitCode: EXIT_CODES.usage,
|
|
63
|
+
details: payload || {},
|
|
64
|
+
hints: [
|
|
65
|
+
{
|
|
66
|
+
command: 'Open the newest Notis email on this machine',
|
|
67
|
+
reason: 'Requesting another link makes earlier links stop working',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new CliError({
|
|
75
|
+
code: 'signup_failed',
|
|
76
|
+
message: payload?.message || `Signup failed with status ${response.status}`,
|
|
77
|
+
exitCode: EXIT_CODES.backend,
|
|
78
|
+
details: payload || {},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return payload || {};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function startHandler(ctx) {
|
|
86
|
+
const { runtime, options, output } = ctx;
|
|
87
|
+
const apiBase = runtime.apiBase;
|
|
88
|
+
const authenticated = Boolean(runtime.jwt) && !isJwtExpired(runtime.jwt);
|
|
89
|
+
|
|
90
|
+
// Safe to re-run: an already-authenticated machine skips straight to the brief.
|
|
91
|
+
// Agents retry commands, and a second signup would invalidate the first email.
|
|
92
|
+
if (authenticated || options.briefOnly) {
|
|
93
|
+
if (!authenticated) {
|
|
94
|
+
const recovery = getDesktopAuthRecovery(runtime, { mode: 'missing' });
|
|
95
|
+
throw new CliError({
|
|
96
|
+
code: 'auth_missing',
|
|
97
|
+
message: 'This machine is not signed in to Notis yet.',
|
|
98
|
+
exitCode: EXIT_CODES.auth,
|
|
99
|
+
hints: recovery.hints,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const brief = await fetchBrief(apiBase, runtime.timeoutMs);
|
|
103
|
+
return output.emitSuccess({
|
|
104
|
+
command: 'start',
|
|
105
|
+
data: { authenticated: true, brief: brief.markdown, brief_source: brief.source },
|
|
106
|
+
humanSummary: 'Notis CLI is authenticated on this machine.',
|
|
107
|
+
renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!options.email) {
|
|
112
|
+
// The one point in the flow that has to stop and talk to the human.
|
|
113
|
+
throw new CliError({
|
|
114
|
+
code: 'signup_email_required',
|
|
115
|
+
message: 'An email address is required to create or access a Notis account.',
|
|
116
|
+
exitCode: EXIT_CODES.usage,
|
|
117
|
+
hints: [
|
|
118
|
+
{
|
|
119
|
+
command: 'Ask the user for their email address, then rerun with --email <address>',
|
|
120
|
+
reason: 'Notis sends a sign-in link there to prove they own the address',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const signup = await requestSignupLink(apiBase, {
|
|
127
|
+
email: options.email,
|
|
128
|
+
useCases: options.useCase,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Commander stores the negatable `--no-wait` flag as `options.wait === false`
|
|
132
|
+
// and never sets `options.noWait` — the same footgun already fixed for
|
|
133
|
+
// `--no-open` in apps.js. Reading `noWait` made the flag a no-op, so an agent
|
|
134
|
+
// that asked not to wait blocked for the full timeout instead.
|
|
135
|
+
if (options.wait === false) {
|
|
136
|
+
return output.emitSuccess({
|
|
137
|
+
command: 'start',
|
|
138
|
+
data: { authenticated: false, next_action: 'open_email_link', ...signup },
|
|
139
|
+
humanSummary: `A sign-in link is on its way to ${options.email}.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const timeoutMs = Number(options.waitTimeoutMs) > 0 ? Number(options.waitTimeoutMs) : 300_000;
|
|
144
|
+
const jwt = await waitForDesktopAuth({
|
|
145
|
+
loadConfig,
|
|
146
|
+
getJwt,
|
|
147
|
+
isJwtExpired,
|
|
148
|
+
profileName: runtime.profileName,
|
|
149
|
+
timeoutMs,
|
|
150
|
+
onTick: (remaining) => {
|
|
151
|
+
if (runtime.outputMode !== 'json' && remaining % 15_000 < 2_000) {
|
|
152
|
+
output.note?.(`Waiting for Notis Desktop to sign in (${Math.round(remaining / 1000)}s left)...`);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!jwt) {
|
|
158
|
+
throw new CliError({
|
|
159
|
+
code: 'auth_timeout',
|
|
160
|
+
message: 'Timed out waiting for Notis Desktop to authenticate the CLI.',
|
|
161
|
+
exitCode: EXIT_CODES.auth,
|
|
162
|
+
details: { desktop_download_url: signup.desktop_download_url || null },
|
|
163
|
+
hints: [
|
|
164
|
+
{
|
|
165
|
+
command: `Install Notis Desktop: ${signup.desktop_download_url || 'https://notis.ai/channels/desktop-app'}`,
|
|
166
|
+
reason: 'The desktop app is what writes the CLI credential',
|
|
167
|
+
},
|
|
168
|
+
{ command: 'notis start --brief-only', reason: 'Resume once the desktop app is signed in' },
|
|
169
|
+
{ command: 'notis doctor', reason: 'Re-check config, auth, and API reachability' },
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const brief = await fetchBrief(apiBase, runtime.timeoutMs);
|
|
175
|
+
return output.emitSuccess({
|
|
176
|
+
command: 'start',
|
|
177
|
+
data: { authenticated: true, brief: brief.markdown, brief_source: brief.source },
|
|
178
|
+
humanSummary: 'Notis CLI is authenticated. Follow the onboarding brief below.',
|
|
179
|
+
renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const onboardingCommandSpecs = [
|
|
184
|
+
{
|
|
185
|
+
command_path: ['start'],
|
|
186
|
+
summary: 'Create or access a Notis account and wait for the desktop app to sign the CLI in.',
|
|
187
|
+
when_to_use:
|
|
188
|
+
'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.',
|
|
189
|
+
args_schema: {
|
|
190
|
+
arguments: [],
|
|
191
|
+
options: [
|
|
192
|
+
{ flags: '--email <email>', description: 'Email to create or sign in the Notis account with.' },
|
|
193
|
+
{ flags: '--use-case <slug>', description: 'Primary use case (repeatable).', collect: true },
|
|
194
|
+
{
|
|
195
|
+
flags: '--wait-timeout-ms <n>',
|
|
196
|
+
description: 'How long to wait for the desktop sign-in (default 300000).',
|
|
197
|
+
},
|
|
198
|
+
{ flags: '--no-wait', description: 'Send the link and exit without waiting.' },
|
|
199
|
+
{ flags: '--brief-only', description: 'Print the onboarding brief for an already-signed-in profile.' },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
examples: [
|
|
203
|
+
'notis start --email you@example.com',
|
|
204
|
+
'notis start --brief-only',
|
|
205
|
+
'notis start --email you@example.com --json',
|
|
206
|
+
],
|
|
207
|
+
output_schema:
|
|
208
|
+
'Returns {authenticated, brief, brief_source} once signed in, or {next_action, cooldown_seconds, desktop_download_url} while waiting.',
|
|
209
|
+
mutates: true,
|
|
210
|
+
idempotent: true,
|
|
211
|
+
require_auth: false,
|
|
212
|
+
related_commands: ['notis doctor', 'notis tools link'],
|
|
213
|
+
backend_call: { type: 'http', name: 'POST /signup/agent' },
|
|
214
|
+
handler: startHandler,
|
|
215
|
+
},
|
|
216
|
+
];
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { basename, join } from 'node:path';
|
|
10
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
11
|
+
import { usageError } from '../runtime/errors.js';
|
|
12
|
+
import {
|
|
13
|
+
COMPOSIO_MULTI_EXECUTE_TOOL,
|
|
14
|
+
fetchToolDiscovery,
|
|
15
|
+
fetchToolSchema,
|
|
16
|
+
nextIdempotencyKey,
|
|
17
|
+
runToolCommand,
|
|
18
|
+
} from './helpers.js';
|
|
19
|
+
import { mintEntitlementOverride, unwrapToolExecutionPayload } from './diagnostics.js';
|
|
20
|
+
import { hashFileSha256, parseFileBindings } from './tools.js';
|
|
21
|
+
|
|
22
|
+
function discoveryNames(payload) {
|
|
23
|
+
return [...new Set(
|
|
24
|
+
(payload.results || []).flatMap((result) => [
|
|
25
|
+
...(result.primary_tool_slugs || []),
|
|
26
|
+
...(result.related_tool_slugs || []),
|
|
27
|
+
]).filter(Boolean),
|
|
28
|
+
)];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function selectDropboxTools(payloads) {
|
|
32
|
+
const names = [...new Set(payloads.flatMap(discoveryNames))];
|
|
33
|
+
const find = (preferred, ...needles) => (
|
|
34
|
+
names.find((name) => name.toUpperCase() === preferred)
|
|
35
|
+
|| names.find((name) => {
|
|
36
|
+
const upper = name.toUpperCase();
|
|
37
|
+
return upper.includes('DROPBOX')
|
|
38
|
+
&& !upper.includes('ALPHA')
|
|
39
|
+
&& needles.every((needle) => upper.includes(needle));
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
const tools = {
|
|
43
|
+
upload: find('DROPBOX_UPLOAD_FILE', 'UPLOAD', 'FILE'),
|
|
44
|
+
metadata: find('DROPBOX_GET_METADATA', 'GET', 'METADATA'),
|
|
45
|
+
read: find('DROPBOX_READ_FILE', 'READ', 'FILE')
|
|
46
|
+
|| find('DROPBOX_DOWNLOAD_FILE', 'DOWNLOAD', 'FILE'),
|
|
47
|
+
delete: find('DROPBOX_DELETE_FILE', 'DELETE', 'FILE'),
|
|
48
|
+
};
|
|
49
|
+
const missing = Object.entries(tools).filter(([, value]) => !value).map(([key]) => key);
|
|
50
|
+
if (missing.length) {
|
|
51
|
+
throw usageError(`Dropbox smoke could not discover: ${missing.join(', ')}.`);
|
|
52
|
+
}
|
|
53
|
+
return tools;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function fetchDiscoveryWithRetry(ctx, useCase, knownFields) {
|
|
57
|
+
const maxAttempts = 3;
|
|
58
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
59
|
+
try {
|
|
60
|
+
return await fetchToolDiscovery(ctx.runtime, useCase, knownFields);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (!error?.retryable || attempt === maxAttempts) throw error;
|
|
63
|
+
ctx.output.emitProgress({
|
|
64
|
+
phase: 'discover-retry',
|
|
65
|
+
message: `Tool discovery transiently failed; retrying (${attempt}/${maxAttempts - 1})`,
|
|
66
|
+
requestId: error?.details?.request_id || null,
|
|
67
|
+
});
|
|
68
|
+
await delay(250 * (2 ** (attempt - 1)));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw usageError('Dropbox tool discovery exhausted its retries.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function targetResponse(payload) {
|
|
75
|
+
const unwrapped = unwrapToolExecutionPayload(payload);
|
|
76
|
+
if (unwrapped?.results?.[0]?.response) return unwrapped.results[0].response;
|
|
77
|
+
if (payload?.data?.results?.[0]?.response) return payload.data.results[0].response;
|
|
78
|
+
if (payload?.results?.[0]?.response) return payload.results[0].response;
|
|
79
|
+
return unwrapped;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function requireSuccessfulResponse(payload, phase) {
|
|
83
|
+
const response = targetResponse(payload);
|
|
84
|
+
if (!response || response.successful === false || payload?.successful === false) {
|
|
85
|
+
throw usageError(`Dropbox ${phase} failed.`, { phase, response });
|
|
86
|
+
}
|
|
87
|
+
return response.data ?? response;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function executeDropboxTool(ctx, toolName, args, {
|
|
91
|
+
mutating,
|
|
92
|
+
fileBindings = [],
|
|
93
|
+
phase,
|
|
94
|
+
}) {
|
|
95
|
+
ctx.output.emitProgress({ phase, message: `${phase} via ${toolName}` });
|
|
96
|
+
const baseIdempotencyKey = mutating ? nextIdempotencyKey(ctx.globalOptions) : null;
|
|
97
|
+
const result = await runToolCommand({
|
|
98
|
+
runtime: ctx.runtime,
|
|
99
|
+
toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
|
|
100
|
+
arguments_: { tools: [{ tool_slug: toolName, arguments: args }] },
|
|
101
|
+
mutating,
|
|
102
|
+
idempotencyKey: baseIdempotencyKey ? `${baseIdempotencyKey}:${phase}` : null,
|
|
103
|
+
fileBindings,
|
|
104
|
+
});
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function remotePath(folder, name) {
|
|
109
|
+
const normalizedFolder = `/${String(folder || '/Notis Tests/cli-file-upload')
|
|
110
|
+
.split('/')
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join('/')}`;
|
|
113
|
+
return `${normalizedFolder}/${name}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function smokeFileUploadHandler(ctx) {
|
|
117
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'notis-smoke-file-upload-'));
|
|
118
|
+
const marker = `${new Date().toISOString().replaceAll(/[-:.]/g, '')}-${randomUUID().slice(0, 8)}`;
|
|
119
|
+
const generatedFixture = !ctx.options.sourceFile;
|
|
120
|
+
const fixturePath = ctx.options.sourceFile || join(tempDir, `notis-file-upload-${marker}.txt`);
|
|
121
|
+
if (generatedFixture) {
|
|
122
|
+
writeFileSync(
|
|
123
|
+
fixturePath,
|
|
124
|
+
`Notis Dropbox file-upload smoke\nmarker=${marker}\nunicode=été Zürich 東京\n`,
|
|
125
|
+
'utf-8',
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const localHash = await hashFileSha256(fixturePath);
|
|
129
|
+
const localBytes = readFileSync(fixturePath);
|
|
130
|
+
const destination = remotePath(
|
|
131
|
+
ctx.options.remoteFolder,
|
|
132
|
+
`notis-file-upload-${marker}-${basename(fixturePath)}`,
|
|
133
|
+
);
|
|
134
|
+
const result = {
|
|
135
|
+
marker,
|
|
136
|
+
destination,
|
|
137
|
+
local: { bytes: localBytes.length, sha256: localHash },
|
|
138
|
+
phases: [],
|
|
139
|
+
request_ids: {},
|
|
140
|
+
cleanup: { attempted: false, deleted: false, not_found_verified: false },
|
|
141
|
+
parity: null,
|
|
142
|
+
};
|
|
143
|
+
let uploadAttempted = false;
|
|
144
|
+
let deleted = false;
|
|
145
|
+
let revision = null;
|
|
146
|
+
let tools;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
ctx.output.emitProgress({ phase: 'discover', message: 'Resolving Dropbox upload, metadata, read, and delete capabilities' });
|
|
150
|
+
// Discover sequentially. Both calls can materialize the same provider session,
|
|
151
|
+
// and racing that initialization has caused otherwise healthy smoke runs to
|
|
152
|
+
// fail with a transient "Server disconnected" before any upload occurred.
|
|
153
|
+
const discoveries = [
|
|
154
|
+
await fetchDiscoveryWithRetry(
|
|
155
|
+
ctx,
|
|
156
|
+
'Upload a local file to Dropbox, get its exact metadata, and read the same file bytes back',
|
|
157
|
+
'path, content, strict_conflict',
|
|
158
|
+
),
|
|
159
|
+
await fetchDiscoveryWithRetry(
|
|
160
|
+
ctx,
|
|
161
|
+
'Delete one Dropbox file by exact canonical path and parent revision, then verify metadata reports path not found',
|
|
162
|
+
'path, parent_rev',
|
|
163
|
+
),
|
|
164
|
+
];
|
|
165
|
+
tools = selectDropboxTools(discoveries);
|
|
166
|
+
const uploadSchema = await fetchToolSchema(ctx.runtime, tools.upload);
|
|
167
|
+
if (uploadSchema.parameters?.properties?.content?.file_uploadable !== true) {
|
|
168
|
+
throw usageError(`${tools.upload} content is not marked file_uploadable.`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (ctx.options.parityUser) {
|
|
172
|
+
const minted = await mintEntitlementOverride(ctx, ctx.options.parityUser);
|
|
173
|
+
ctx.runtime.debugEntitlementOverride = minted.override;
|
|
174
|
+
result.parity = {
|
|
175
|
+
requested: true,
|
|
176
|
+
expected_parity_hash: minted.parity_hash,
|
|
177
|
+
reference_scope_type: minted.reference_scope_type,
|
|
178
|
+
persistent: false,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const fileBindings = await parseFileBindings([`content=${fixturePath}`]);
|
|
183
|
+
uploadAttempted = true;
|
|
184
|
+
const upload = await executeDropboxTool(
|
|
185
|
+
ctx,
|
|
186
|
+
tools.upload,
|
|
187
|
+
{ path: destination, strict_conflict: true, mode: 'add', autorename: false },
|
|
188
|
+
{ mutating: true, fileBindings, phase: 'upload' },
|
|
189
|
+
);
|
|
190
|
+
result.request_ids.upload = upload.requestId;
|
|
191
|
+
const uploadData = requireSuccessfulResponse(upload.payload, 'upload');
|
|
192
|
+
const appliedParity = upload.payload?.debug_entitlement_override;
|
|
193
|
+
if (result.parity) {
|
|
194
|
+
if (!appliedParity?.applied || appliedParity.parity_hash !== result.parity.expected_parity_hash) {
|
|
195
|
+
throw usageError('Backend did not assert the requested entitlement parity before upload.');
|
|
196
|
+
}
|
|
197
|
+
result.parity.applied = true;
|
|
198
|
+
result.parity.expires_at = appliedParity.expires_at;
|
|
199
|
+
}
|
|
200
|
+
revision = uploadData.rev;
|
|
201
|
+
result.upload = {
|
|
202
|
+
path: uploadData.path_display,
|
|
203
|
+
revision,
|
|
204
|
+
bytes: uploadData.size,
|
|
205
|
+
provider_content_hash: uploadData.content_hash,
|
|
206
|
+
};
|
|
207
|
+
result.phases.push('upload');
|
|
208
|
+
|
|
209
|
+
const metadata = await executeDropboxTool(
|
|
210
|
+
ctx,
|
|
211
|
+
tools.metadata,
|
|
212
|
+
{ path: uploadData.path_display || destination },
|
|
213
|
+
{ mutating: false, phase: 'metadata' },
|
|
214
|
+
);
|
|
215
|
+
result.request_ids.metadata = metadata.requestId;
|
|
216
|
+
const metadataData = requireSuccessfulResponse(metadata.payload, 'metadata').metadata;
|
|
217
|
+
if (!metadataData || metadataData.rev !== revision || Number(metadataData.size) !== localBytes.length) {
|
|
218
|
+
throw usageError('Dropbox metadata did not match the uploaded file revision and byte count.');
|
|
219
|
+
}
|
|
220
|
+
result.metadata = {
|
|
221
|
+
path: metadataData.path_display,
|
|
222
|
+
revision: metadataData.rev,
|
|
223
|
+
bytes: metadataData.size,
|
|
224
|
+
};
|
|
225
|
+
result.phases.push('metadata');
|
|
226
|
+
|
|
227
|
+
const read = await executeDropboxTool(
|
|
228
|
+
ctx,
|
|
229
|
+
tools.read,
|
|
230
|
+
{ path: metadataData.path_display },
|
|
231
|
+
{ mutating: false, phase: 'download' },
|
|
232
|
+
);
|
|
233
|
+
result.request_ids.download = read.requestId;
|
|
234
|
+
const readData = requireSuccessfulResponse(read.payload, 'read');
|
|
235
|
+
const signedUrl = readData.content?.s3url;
|
|
236
|
+
if (!signedUrl) throw usageError('Dropbox read did not return a temporary download URL.');
|
|
237
|
+
const downloadResponse = await fetch(signedUrl);
|
|
238
|
+
if (!downloadResponse.ok) {
|
|
239
|
+
throw usageError(`Dropbox temporary download returned HTTP ${downloadResponse.status}.`);
|
|
240
|
+
}
|
|
241
|
+
const downloaded = Buffer.from(await downloadResponse.arrayBuffer());
|
|
242
|
+
const downloadedHash = createHash('sha256').update(downloaded).digest('hex');
|
|
243
|
+
if (downloadedHash !== localHash) {
|
|
244
|
+
throw usageError('Downloaded Dropbox bytes did not match the uploaded fixture hash.');
|
|
245
|
+
}
|
|
246
|
+
result.download = {
|
|
247
|
+
bytes: downloaded.length,
|
|
248
|
+
sha256: downloadedHash,
|
|
249
|
+
hash_match: true,
|
|
250
|
+
};
|
|
251
|
+
result.phases.push('download');
|
|
252
|
+
|
|
253
|
+
result.cleanup.attempted = true;
|
|
254
|
+
const deletion = await executeDropboxTool(
|
|
255
|
+
ctx,
|
|
256
|
+
tools.delete,
|
|
257
|
+
{ path: metadataData.path_display, parent_rev: revision },
|
|
258
|
+
{ mutating: true, phase: 'delete' },
|
|
259
|
+
);
|
|
260
|
+
result.request_ids.delete = deletion.requestId;
|
|
261
|
+
requireSuccessfulResponse(deletion.payload, 'delete');
|
|
262
|
+
result.cleanup.deleted = true;
|
|
263
|
+
result.phases.push('delete');
|
|
264
|
+
|
|
265
|
+
const finalMetadata = await executeDropboxTool(
|
|
266
|
+
ctx,
|
|
267
|
+
tools.metadata,
|
|
268
|
+
{ path: metadataData.path_display },
|
|
269
|
+
{ mutating: false, phase: 'verify-not-found' },
|
|
270
|
+
);
|
|
271
|
+
result.request_ids.verify_not_found = finalMetadata.requestId;
|
|
272
|
+
const finalResponse = targetResponse(finalMetadata.payload);
|
|
273
|
+
const finalText = JSON.stringify(finalResponse);
|
|
274
|
+
if (finalResponse?.successful !== false || !finalText.includes('path/not_found')) {
|
|
275
|
+
throw usageError('Dropbox cleanup verification did not return path/not_found.');
|
|
276
|
+
}
|
|
277
|
+
result.cleanup.not_found_verified = true;
|
|
278
|
+
deleted = true;
|
|
279
|
+
result.phases.push('verify-not-found');
|
|
280
|
+
ctx.output.emitProgress({ phase: 'complete', message: 'Dropbox file-upload smoke passed with verified cleanup' });
|
|
281
|
+
return ctx.output.emitSuccess({
|
|
282
|
+
command: ctx.spec.command_path.join(' '),
|
|
283
|
+
data: { ...result, tools },
|
|
284
|
+
humanSummary: `Dropbox file-upload smoke passed for ${destination}; SHA-256 matched and cleanup was verified.`,
|
|
285
|
+
requestId: result.request_ids.verify_not_found,
|
|
286
|
+
meta: { mutating: true, cleaned_up: true },
|
|
287
|
+
renderHuman: () => JSON.stringify({ ...result, tools }, null, 2),
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (error && typeof error === 'object') {
|
|
291
|
+
error.details = {
|
|
292
|
+
...(error.details || {}),
|
|
293
|
+
smoke_result: { ...result, tools },
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
} finally {
|
|
298
|
+
if (uploadAttempted && !result.cleanup.not_found_verified && tools) {
|
|
299
|
+
result.cleanup.attempted = true;
|
|
300
|
+
try {
|
|
301
|
+
let cleanupRevision = revision;
|
|
302
|
+
if (!cleanupRevision) {
|
|
303
|
+
const recoveryMetadata = await executeDropboxTool(
|
|
304
|
+
ctx,
|
|
305
|
+
tools.metadata,
|
|
306
|
+
{ path: destination },
|
|
307
|
+
{ mutating: false, phase: 'recover-cleanup-metadata' },
|
|
308
|
+
);
|
|
309
|
+
result.request_ids.recover_cleanup_metadata = recoveryMetadata.requestId;
|
|
310
|
+
const recoveryResponse = targetResponse(recoveryMetadata.payload);
|
|
311
|
+
const recoveryText = JSON.stringify(recoveryResponse);
|
|
312
|
+
if (recoveryResponse?.successful === false && recoveryText.includes('path/not_found')) {
|
|
313
|
+
result.cleanup.deleted = true;
|
|
314
|
+
result.cleanup.not_found_verified = true;
|
|
315
|
+
} else {
|
|
316
|
+
const recoveryData = requireSuccessfulResponse(recoveryMetadata.payload, 'cleanup metadata');
|
|
317
|
+
cleanupRevision = recoveryData.metadata?.rev || recoveryData.rev;
|
|
318
|
+
if (!cleanupRevision) {
|
|
319
|
+
throw usageError('Dropbox cleanup metadata did not return the uploaded file revision.');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!result.cleanup.not_found_verified) {
|
|
324
|
+
const cleanup = await executeDropboxTool(
|
|
325
|
+
ctx,
|
|
326
|
+
tools.delete,
|
|
327
|
+
{ path: destination, parent_rev: cleanupRevision },
|
|
328
|
+
{ mutating: true, phase: 'cleanup-after-failure' },
|
|
329
|
+
);
|
|
330
|
+
result.request_ids.cleanup_after_failure = cleanup.requestId;
|
|
331
|
+
const cleanupResponse = targetResponse(cleanup.payload);
|
|
332
|
+
const cleanupText = JSON.stringify(cleanupResponse);
|
|
333
|
+
result.cleanup.deleted = (
|
|
334
|
+
cleanupResponse?.successful === true
|
|
335
|
+
|| cleanupText.includes('path/not_found')
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const verification = await executeDropboxTool(
|
|
339
|
+
ctx,
|
|
340
|
+
tools.metadata,
|
|
341
|
+
{ path: destination },
|
|
342
|
+
{ mutating: false, phase: 'verify-cleanup-after-failure' },
|
|
343
|
+
);
|
|
344
|
+
result.request_ids.verify_cleanup_after_failure = verification.requestId;
|
|
345
|
+
const verificationResponse = targetResponse(verification.payload);
|
|
346
|
+
const verificationText = JSON.stringify(verificationResponse);
|
|
347
|
+
result.cleanup.not_found_verified = (
|
|
348
|
+
verificationResponse?.successful === false
|
|
349
|
+
&& verificationText.includes('path/not_found')
|
|
350
|
+
);
|
|
351
|
+
deleted = result.cleanup.not_found_verified;
|
|
352
|
+
result.cleanup.deleted = deleted;
|
|
353
|
+
} catch {
|
|
354
|
+
deleted = false;
|
|
355
|
+
result.cleanup.deleted = false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export const smokeCommandSpecs = [
|
|
363
|
+
{
|
|
364
|
+
command_path: ['smoke', 'file-upload'],
|
|
365
|
+
summary: 'Run a complete Dropbox upload/read/hash/delete smoke test.',
|
|
366
|
+
when_to_use: 'Use to prove the local-file bridge and connected Dropbox behavior under the current or parity entitlement context.',
|
|
367
|
+
args_schema: {
|
|
368
|
+
arguments: [],
|
|
369
|
+
options: [
|
|
370
|
+
{ flags: '--source-file <path>', description: 'Use an existing local fixture instead of generating one.' },
|
|
371
|
+
{ flags: '--remote-folder <path>', description: 'Unique Dropbox parent folder (default /Notis Tests/cli-file-upload).' },
|
|
372
|
+
{ flags: '--parity-user <user>', description: 'Apply this reference user’s effective plan for only the smoke requests.' },
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
examples: [
|
|
376
|
+
'notis smoke file-upload --json',
|
|
377
|
+
'notis smoke file-upload --parity-user user@example.com --json',
|
|
378
|
+
],
|
|
379
|
+
output_schema: 'Returns structured phase results, request ids, metadata, hashes, parity proof, and cleanup verification.',
|
|
380
|
+
mutates: true,
|
|
381
|
+
idempotent: true,
|
|
382
|
+
related_commands: ['notis debug user-context <user>', 'notis debug entitlement-override <reference-user>'],
|
|
383
|
+
backend_call: { type: 'tool-discovery', name: 'Dropbox upload/read/delete capabilities' },
|
|
384
|
+
handler: smokeFileUploadHandler,
|
|
385
|
+
},
|
|
386
|
+
];
|