@notis_ai/cli 0.2.0-beta.16.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 +335 -0
- package/bin/notis.js +2 -0
- package/package.json +38 -0
- package/src/cli.js +147 -0
- package/src/command-specs/apps.js +496 -0
- package/src/command-specs/auth.js +178 -0
- package/src/command-specs/db.js +163 -0
- package/src/command-specs/helpers.js +193 -0
- package/src/command-specs/index.js +20 -0
- package/src/command-specs/meta.js +154 -0
- package/src/command-specs/tools.js +391 -0
- package/src/runtime/app-platform.js +624 -0
- package/src/runtime/app-preview-server.js +312 -0
- package/src/runtime/errors.js +55 -0
- package/src/runtime/help.js +60 -0
- package/src/runtime/output.js +180 -0
- package/src/runtime/profiles.js +202 -0
- package/src/runtime/transport.js +198 -0
- package/template/app/globals.css +3 -0
- package/template/app/layout.tsx +7 -0
- package/template/app/page.tsx +55 -0
- package/template/components/ui/badge.tsx +28 -0
- package/template/components/ui/button.tsx +53 -0
- package/template/components/ui/card.tsx +56 -0
- package/template/components.json +20 -0
- package/template/lib/utils.ts +6 -0
- package/template/notis.config.ts +18 -0
- package/template/package.json +32 -0
- package/template/packages/notis-sdk/package.json +26 -0
- package/template/packages/notis-sdk/src/config.ts +48 -0
- package/template/packages/notis-sdk/src/helpers.ts +131 -0
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +50 -0
- package/template/packages/notis-sdk/src/hooks/useBackend.ts +41 -0
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +58 -0
- package/template/packages/notis-sdk/src/hooks/useDatabase.ts +87 -0
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +61 -0
- package/template/packages/notis-sdk/src/hooks/useNotis.ts +31 -0
- package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/template/packages/notis-sdk/src/hooks/useTool.ts +49 -0
- package/template/packages/notis-sdk/src/hooks/useTools.ts +56 -0
- package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +57 -0
- package/template/packages/notis-sdk/src/index.ts +47 -0
- package/template/packages/notis-sdk/src/provider.tsx +44 -0
- package/template/packages/notis-sdk/src/runtime.ts +159 -0
- package/template/packages/notis-sdk/src/styles.css +123 -0
- package/template/packages/notis-sdk/src/ui.ts +15 -0
- package/template/packages/notis-sdk/src/vite.ts +54 -0
- package/template/packages/notis-sdk/tsconfig.json +15 -0
- package/template/postcss.config.mjs +8 -0
- package/template/tailwind.config.ts +58 -0
- package/template/tsconfig.json +22 -0
- package/template/vite.config.ts +10 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { formatTable } from '../runtime/output.js';
|
|
2
|
+
import { nextIdempotencyKey, parseMaybeJson, runToolCommand } from './helpers.js';
|
|
3
|
+
|
|
4
|
+
async function dbListHandler(ctx) {
|
|
5
|
+
const result = await runToolCommand({
|
|
6
|
+
runtime: ctx.runtime,
|
|
7
|
+
toolName: 'notis_list_databases',
|
|
8
|
+
});
|
|
9
|
+
const databases = result.payload.databases || [];
|
|
10
|
+
const firstSlug = databases[0]?.slug;
|
|
11
|
+
return ctx.output.emitSuccess({
|
|
12
|
+
command: ctx.spec.command_path.join(' '),
|
|
13
|
+
data: { databases },
|
|
14
|
+
humanSummary: databases.length ? `Found ${databases.length} databases` : 'No databases found.',
|
|
15
|
+
hints: firstSlug
|
|
16
|
+
? [{ command: `notis db query ${firstSlug}`, reason: 'Query this database' }]
|
|
17
|
+
: [{ command: 'notis db upsert --operation create --title "My DB"', reason: 'Create a new database' }],
|
|
18
|
+
renderHuman: () =>
|
|
19
|
+
databases.length
|
|
20
|
+
? formatTable(databases, [
|
|
21
|
+
{ label: 'ID', value: (database) => database.id || '' },
|
|
22
|
+
{ label: 'Name', value: (database) => database.name || 'Untitled' },
|
|
23
|
+
{ label: 'Slug', value: (database) => database.slug || '-' },
|
|
24
|
+
])
|
|
25
|
+
: 'No databases found.',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function dbUpsertHandler(ctx) {
|
|
30
|
+
const idempotencyKey = nextIdempotencyKey(ctx.globalOptions);
|
|
31
|
+
const payload = {
|
|
32
|
+
operation: ctx.options.operation,
|
|
33
|
+
};
|
|
34
|
+
if (ctx.options.databaseId) {
|
|
35
|
+
payload.database_id = ctx.options.databaseId;
|
|
36
|
+
}
|
|
37
|
+
if (ctx.options.title) {
|
|
38
|
+
payload.title = ctx.options.title;
|
|
39
|
+
}
|
|
40
|
+
if (ctx.options.description) {
|
|
41
|
+
payload.description = ctx.options.description;
|
|
42
|
+
}
|
|
43
|
+
if (ctx.options.icon) {
|
|
44
|
+
const v = ctx.options.icon;
|
|
45
|
+
payload.icon = v.startsWith('lucide:') ? v : `lucide:${v}`;
|
|
46
|
+
}
|
|
47
|
+
if (ctx.options.properties) {
|
|
48
|
+
payload.properties = parseMaybeJson(ctx.options.properties, 'properties');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = await runToolCommand({
|
|
52
|
+
runtime: ctx.runtime,
|
|
53
|
+
toolName: 'notis_upsert_database',
|
|
54
|
+
arguments_: payload,
|
|
55
|
+
mutating: true,
|
|
56
|
+
idempotencyKey,
|
|
57
|
+
});
|
|
58
|
+
return ctx.output.emitSuccess({
|
|
59
|
+
command: ctx.spec.command_path.join(' '),
|
|
60
|
+
data: result.payload,
|
|
61
|
+
humanSummary: `Database ${ctx.options.operation} completed`,
|
|
62
|
+
meta: { mutating: true, idempotency_key: idempotencyKey || null },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function dbQueryHandler(ctx) {
|
|
67
|
+
const query = {};
|
|
68
|
+
if (ctx.options.filter) {
|
|
69
|
+
query.filter = parseMaybeJson(ctx.options.filter, 'filter');
|
|
70
|
+
}
|
|
71
|
+
if (ctx.options.sort) {
|
|
72
|
+
const parsedSorts = parseMaybeJson(ctx.options.sort, 'sort');
|
|
73
|
+
query.sorts = Array.isArray(parsedSorts) ? parsedSorts : [parsedSorts];
|
|
74
|
+
}
|
|
75
|
+
if (ctx.options.pageSize) {
|
|
76
|
+
query.page_size = Number.parseInt(ctx.options.pageSize, 10);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const payload = {
|
|
80
|
+
database_slug: ctx.args.databaseSlug,
|
|
81
|
+
query,
|
|
82
|
+
};
|
|
83
|
+
if (ctx.options.offset) {
|
|
84
|
+
payload.offset = Number.parseInt(ctx.options.offset, 10);
|
|
85
|
+
}
|
|
86
|
+
if (ctx.options.cursor) {
|
|
87
|
+
payload.start_cursor = ctx.options.cursor;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = await runToolCommand({
|
|
91
|
+
runtime: ctx.runtime,
|
|
92
|
+
toolName: 'notis_query',
|
|
93
|
+
arguments_: payload,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return ctx.output.emitSuccess({
|
|
97
|
+
command: ctx.spec.command_path.join(' '),
|
|
98
|
+
data: result.payload,
|
|
99
|
+
humanSummary: `Queried database ${ctx.args.databaseSlug}`,
|
|
100
|
+
renderHuman: () => JSON.stringify(result.payload, null, 2),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const dbCommandSpecs = [
|
|
105
|
+
{
|
|
106
|
+
command_path: ['db', 'list'],
|
|
107
|
+
summary: 'List native Notis databases.',
|
|
108
|
+
when_to_use: 'Use this to find database ids and slugs before querying or updating schemas.',
|
|
109
|
+
args_schema: { arguments: [], options: [] },
|
|
110
|
+
examples: ['notis db list', 'notis db list --json'],
|
|
111
|
+
output_schema: 'Returns an array of native database records.',
|
|
112
|
+
mutates: false,
|
|
113
|
+
idempotent: true,
|
|
114
|
+
related_commands: ['notis db query <database-slug>', 'notis db upsert'],
|
|
115
|
+
backend_call: { type: 'tool', name: 'notis_list_databases' },
|
|
116
|
+
handler: dbListHandler,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
command_path: ['db', 'upsert'],
|
|
120
|
+
summary: 'Create or update a native database schema.',
|
|
121
|
+
when_to_use: 'Use this when you need to provision a new database or adjust an existing schema.',
|
|
122
|
+
args_schema: {
|
|
123
|
+
arguments: [],
|
|
124
|
+
options: [
|
|
125
|
+
{ flags: '--operation <create|update>', description: 'Create a new database or update an existing one.' },
|
|
126
|
+
{ flags: '--database-id <id>', description: 'Database id for update operations.' },
|
|
127
|
+
{ flags: '--title <text>', description: 'Database title.' },
|
|
128
|
+
{ flags: '--description <text>', description: 'Database description.' },
|
|
129
|
+
{ flags: '--icon <lucide-icon-name>', description: 'Database icon (Lucide icon name, e.g. database).' },
|
|
130
|
+
{ flags: '--properties <json>', description: 'JSON array of property definitions.' },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
examples: ['notis db upsert --operation create --title "Tasks"', 'notis db upsert --operation update --database-id db_123 --title "Tasks V2"'],
|
|
134
|
+
output_schema: 'Returns the backend database upsert payload.',
|
|
135
|
+
mutates: true,
|
|
136
|
+
idempotent: true,
|
|
137
|
+
related_commands: ['notis db list', 'notis db query <database-slug>'],
|
|
138
|
+
backend_call: { type: 'tool', name: 'notis_upsert_database' },
|
|
139
|
+
handler: dbUpsertHandler,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
command_path: ['db', 'query'],
|
|
143
|
+
summary: 'Run a structured query against a native Notis database.',
|
|
144
|
+
when_to_use: 'Use this when the database slug is known and you need direct filters, sorts, or pagination.',
|
|
145
|
+
args_schema: {
|
|
146
|
+
arguments: [{ token: '<database-slug>', description: 'Slug of the native database to query.' }],
|
|
147
|
+
options: [
|
|
148
|
+
{ flags: '--filter <json>', description: 'Structured query filter JSON.' },
|
|
149
|
+
{ flags: '--sort <json>', description: 'Sort JSON object or array.' },
|
|
150
|
+
{ flags: '--page-size <n>', description: 'Page size between 1 and 100.' },
|
|
151
|
+
{ flags: '--offset <n>', description: 'Zero-based offset.' },
|
|
152
|
+
{ flags: '--cursor <value>', description: 'Pagination cursor alias for next_offset.' },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
examples: ['notis db query tasks --page-size 50', 'notis db query tasks --filter \'{"property":"Status"}\''],
|
|
156
|
+
output_schema: 'Returns the native database query payload from `notis_query`.',
|
|
157
|
+
mutates: false,
|
|
158
|
+
idempotent: true,
|
|
159
|
+
related_commands: ['notis db list', 'notis db upsert'],
|
|
160
|
+
backend_call: { type: 'tool', name: 'notis_query' },
|
|
161
|
+
handler: dbQueryHandler,
|
|
162
|
+
},
|
|
163
|
+
];
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { CliError, EXIT_CODES, usageError } from '../runtime/errors.js';
|
|
4
|
+
import { callTool, httpRequest } from '../runtime/transport.js';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_PROFILE,
|
|
7
|
+
ensureProfile,
|
|
8
|
+
loadConfig,
|
|
9
|
+
saveConfig,
|
|
10
|
+
} from '../runtime/profiles.js';
|
|
11
|
+
|
|
12
|
+
export function parseJson(value, label) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(value);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw usageError(`${label} must be valid JSON`, { value });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseMaybeJson(value, label) {
|
|
21
|
+
if (!value) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return parseJson(value, label);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function normalizeToolkits(value) {
|
|
28
|
+
if (!value) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
if (value.startsWith('[')) {
|
|
32
|
+
const parsed = parseJson(value, 'toolkits');
|
|
33
|
+
if (!Array.isArray(parsed)) {
|
|
34
|
+
throw usageError('toolkits JSON must be an array of toolkit strings');
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
return value
|
|
39
|
+
.split(',')
|
|
40
|
+
.map((entry) => entry.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function promptForJwt() {
|
|
45
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
46
|
+
try {
|
|
47
|
+
const jwt = await rl.question('Paste your Notis JWT: ');
|
|
48
|
+
return jwt.trim();
|
|
49
|
+
} finally {
|
|
50
|
+
rl.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function nextIdempotencyKey(globalOptions) {
|
|
55
|
+
return globalOptions.idempotencyKey || randomUUID();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function runToolCommand({
|
|
59
|
+
runtime,
|
|
60
|
+
toolName,
|
|
61
|
+
arguments_ = {},
|
|
62
|
+
mutating = false,
|
|
63
|
+
idempotencyKey,
|
|
64
|
+
}) {
|
|
65
|
+
const result = await callTool({
|
|
66
|
+
runtime: { ...runtime, mutating },
|
|
67
|
+
toolName,
|
|
68
|
+
arguments_,
|
|
69
|
+
idempotencyKey: mutating ? idempotencyKey : null,
|
|
70
|
+
});
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function fetchToolkits(runtime) {
|
|
75
|
+
const result = await runToolCommand({
|
|
76
|
+
runtime,
|
|
77
|
+
toolName: 'notis_find_toolkits',
|
|
78
|
+
});
|
|
79
|
+
return result.payload.toolkits || [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function resolveSearchToolkits(runtime, rawToolkits) {
|
|
83
|
+
const toolkits = normalizeToolkits(rawToolkits);
|
|
84
|
+
if (toolkits.length) {
|
|
85
|
+
return toolkits;
|
|
86
|
+
}
|
|
87
|
+
const availableToolkits = await fetchToolkits(runtime);
|
|
88
|
+
return availableToolkits.map((entry) => entry.id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function probeAuth(runtime) {
|
|
92
|
+
const result = await runToolCommand({
|
|
93
|
+
runtime,
|
|
94
|
+
toolName: 'notis_find_toolkits',
|
|
95
|
+
});
|
|
96
|
+
return result.payload;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function healthCheck(runtime) {
|
|
100
|
+
return httpRequest({
|
|
101
|
+
runtime,
|
|
102
|
+
method: 'GET',
|
|
103
|
+
path: '/health',
|
|
104
|
+
requireAuth: false,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function updateStoredProfile({ profileName, jwt, apiBase, setCurrent = true }) {
|
|
109
|
+
let config = ensureProfile(loadConfig(), profileName || DEFAULT_PROFILE);
|
|
110
|
+
const resolvedName = profileName || DEFAULT_PROFILE;
|
|
111
|
+
config.profiles[resolvedName] = {
|
|
112
|
+
...config.profiles[resolvedName],
|
|
113
|
+
...(jwt ? { jwt } : {}),
|
|
114
|
+
...(apiBase ? { api_base: apiBase } : {}),
|
|
115
|
+
};
|
|
116
|
+
if (setCurrent) {
|
|
117
|
+
config.current_profile = resolvedName;
|
|
118
|
+
}
|
|
119
|
+
saveConfig(config);
|
|
120
|
+
return config;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function clearStoredJwt(profileName) {
|
|
124
|
+
const config = ensureProfile(loadConfig(), profileName || DEFAULT_PROFILE);
|
|
125
|
+
delete config.profiles[profileName || DEFAULT_PROFILE].jwt;
|
|
126
|
+
saveConfig(config);
|
|
127
|
+
return config;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function fetchToolSchema(runtime, toolName) {
|
|
131
|
+
const result = await runToolCommand({
|
|
132
|
+
runtime,
|
|
133
|
+
toolName: 'notis_find_tools',
|
|
134
|
+
arguments_: { query: toolName },
|
|
135
|
+
});
|
|
136
|
+
const tools = result.payload.tools || [];
|
|
137
|
+
const match = tools.find((t) => t.name === toolName);
|
|
138
|
+
if (!match) {
|
|
139
|
+
throw usageError(`Tool "${toolName}" not found.`);
|
|
140
|
+
}
|
|
141
|
+
return match;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function validateArguments(schema, args) {
|
|
145
|
+
const errors = [];
|
|
146
|
+
if (!schema || schema.type !== 'object') return errors;
|
|
147
|
+
|
|
148
|
+
const properties = schema.properties || {};
|
|
149
|
+
const required = schema.required || [];
|
|
150
|
+
|
|
151
|
+
for (const field of required) {
|
|
152
|
+
if (!(field in args)) {
|
|
153
|
+
errors.push(`Missing required field: "${field}"`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (schema.additionalProperties === false) {
|
|
158
|
+
for (const key of Object.keys(args)) {
|
|
159
|
+
if (!(key in properties)) {
|
|
160
|
+
errors.push(`Unknown field: "${key}"`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const [key, value] of Object.entries(args)) {
|
|
166
|
+
const prop = properties[key];
|
|
167
|
+
if (!prop) continue;
|
|
168
|
+
if (prop.type === 'string' && typeof value !== 'string') {
|
|
169
|
+
errors.push(`Field "${key}" should be a string, got ${typeof value}`);
|
|
170
|
+
}
|
|
171
|
+
if (prop.type === 'integer' && !Number.isInteger(value)) {
|
|
172
|
+
errors.push(`Field "${key}" should be an integer, got ${typeof value}`);
|
|
173
|
+
}
|
|
174
|
+
if (prop.type === 'array' && !Array.isArray(value)) {
|
|
175
|
+
errors.push(`Field "${key}" should be an array, got ${typeof value}`);
|
|
176
|
+
}
|
|
177
|
+
if (prop.type === 'object' && (typeof value !== 'object' || value === null || Array.isArray(value))) {
|
|
178
|
+
errors.push(`Field "${key}" should be an object, got ${typeof value}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return errors;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function toolConflictToError(payload, defaultMessage) {
|
|
186
|
+
return new CliError({
|
|
187
|
+
code: 'conflict',
|
|
188
|
+
message: payload?.error?.message || payload?.message || defaultMessage,
|
|
189
|
+
exitCode: EXIT_CODES.conflict,
|
|
190
|
+
details: payload || {},
|
|
191
|
+
hints: payload?.hints || [],
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { authCommandSpecs } from './auth.js';
|
|
2
|
+
import { appsCommandSpecs } from './apps.js';
|
|
3
|
+
import { dbCommandSpecs } from './db.js';
|
|
4
|
+
import { toolsCommandSpecs } from './tools.js';
|
|
5
|
+
import { metaCommandSpecs } from './meta.js';
|
|
6
|
+
|
|
7
|
+
export const GROUP_SUMMARIES = {
|
|
8
|
+
auth: 'Authentication and profile management.',
|
|
9
|
+
apps: 'Build, preview, and deploy Notis Apps.',
|
|
10
|
+
db: 'List, query, and update native Notis Databases.',
|
|
11
|
+
tools: 'Discover and execute generic tools exposed through Notis.',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const COMMAND_SPECS = [
|
|
15
|
+
...authCommandSpecs,
|
|
16
|
+
...appsCommandSpecs,
|
|
17
|
+
...dbCommandSpecs,
|
|
18
|
+
...toolsCommandSpecs,
|
|
19
|
+
...metaCommandSpecs,
|
|
20
|
+
];
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { healthCheck, probeAuth } from './helpers.js';
|
|
2
|
+
import { findCommandSpec, formatDescribe } from '../runtime/help.js';
|
|
3
|
+
|
|
4
|
+
async function doctorHandler(ctx) {
|
|
5
|
+
const checks = {
|
|
6
|
+
config: 'ok',
|
|
7
|
+
auth: 'missing',
|
|
8
|
+
health: 'unknown',
|
|
9
|
+
tool_roundtrip: 'unknown',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
checks.auth = ctx.runtime.jwt ? 'configured' : 'missing';
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await healthCheck(ctx.runtime);
|
|
16
|
+
checks.health = 'ok';
|
|
17
|
+
} catch (error) {
|
|
18
|
+
checks.health = 'error';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (ctx.runtime.jwt) {
|
|
22
|
+
try {
|
|
23
|
+
const payload = await probeAuth(ctx.runtime);
|
|
24
|
+
checks.tool_roundtrip = Array.isArray(payload.toolkits) ? 'ok' : 'error';
|
|
25
|
+
} catch {
|
|
26
|
+
checks.tool_roundtrip = 'error';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const hints = [];
|
|
31
|
+
if (checks.auth === 'missing') {
|
|
32
|
+
hints.push({ command: 'notis auth login --jwt <token>', reason: 'Configure credentials' });
|
|
33
|
+
}
|
|
34
|
+
if (checks.health === 'error') {
|
|
35
|
+
hints.push({ command: 'notis auth status', reason: 'Check API base URL configuration' });
|
|
36
|
+
}
|
|
37
|
+
if (checks.tool_roundtrip === 'error') {
|
|
38
|
+
hints.push({ command: 'notis whoami', reason: 'Verify your account and permissions' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return ctx.output.emitSuccess({
|
|
42
|
+
command: ctx.spec.command_path.join(' '),
|
|
43
|
+
data: {
|
|
44
|
+
profile: ctx.runtime.profileName,
|
|
45
|
+
api_base: ctx.runtime.apiBase,
|
|
46
|
+
checks,
|
|
47
|
+
},
|
|
48
|
+
humanSummary: `Doctor checks completed for profile ${ctx.runtime.profileName}`,
|
|
49
|
+
hints,
|
|
50
|
+
renderHuman: () =>
|
|
51
|
+
Object.entries(checks)
|
|
52
|
+
.map(([name, status]) => `${name.padEnd(14)} ${status}`)
|
|
53
|
+
.join('\n'),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function decodeJwtUserId(jwt) {
|
|
58
|
+
if (!jwt) return null;
|
|
59
|
+
try {
|
|
60
|
+
const parts = jwt.split('.');
|
|
61
|
+
if (parts.length !== 3) return null;
|
|
62
|
+
const decoded = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
63
|
+
return decoded.sub || decoded.user_id || null;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function whoamiHandler(ctx) {
|
|
70
|
+
const payload = await probeAuth(ctx.runtime);
|
|
71
|
+
const toolkits = payload.toolkits || [];
|
|
72
|
+
const userId = decodeJwtUserId(ctx.runtime.jwt);
|
|
73
|
+
|
|
74
|
+
return ctx.output.emitSuccess({
|
|
75
|
+
command: ctx.spec.command_path.join(' '),
|
|
76
|
+
data: {
|
|
77
|
+
profile: ctx.runtime.profileName,
|
|
78
|
+
api_base: ctx.runtime.apiBase,
|
|
79
|
+
user_id: userId,
|
|
80
|
+
toolkit_count: toolkits.length,
|
|
81
|
+
toolkits: toolkits.map((t) => t.id),
|
|
82
|
+
cli_version: ctx.runtime.cliVersion,
|
|
83
|
+
},
|
|
84
|
+
humanSummary: `Logged in as ${userId || 'unknown'} via profile "${ctx.runtime.profileName}"`,
|
|
85
|
+
renderHuman: () =>
|
|
86
|
+
[
|
|
87
|
+
`Profile: ${ctx.runtime.profileName}`,
|
|
88
|
+
`API: ${ctx.runtime.apiBase}`,
|
|
89
|
+
`User: ${userId || 'unknown'}`,
|
|
90
|
+
`Toolkits: ${toolkits.length}`,
|
|
91
|
+
`Version: ${ctx.runtime.cliVersion}`,
|
|
92
|
+
].join('\n'),
|
|
93
|
+
hints: [
|
|
94
|
+
{ command: 'notis tools toolkits', reason: 'List available toolkit namespaces' },
|
|
95
|
+
{ command: 'notis doctor', reason: 'Run a full health check' },
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function describeHandler(ctx) {
|
|
101
|
+
const spec = findCommandSpec(ctx.registrySpecs, ctx.args.commandPath);
|
|
102
|
+
return ctx.output.emitSuccess({
|
|
103
|
+
command: ctx.spec.command_path.join(' '),
|
|
104
|
+
data: { spec },
|
|
105
|
+
humanSummary: `Described ${spec.command_path.join(' ')}`,
|
|
106
|
+
renderHuman: () => formatDescribe(spec),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const metaCommandSpecs = [
|
|
111
|
+
{
|
|
112
|
+
command_path: ['whoami'],
|
|
113
|
+
summary: 'Display the active profile, user, and available toolkits.',
|
|
114
|
+
when_to_use: 'Use this to quickly confirm which account and environment a command will target.',
|
|
115
|
+
args_schema: { arguments: [], options: [] },
|
|
116
|
+
examples: ['notis whoami', 'notis whoami --json'],
|
|
117
|
+
output_schema: 'Returns profile, api_base, user_id, toolkit count, and CLI version.',
|
|
118
|
+
mutates: false,
|
|
119
|
+
idempotent: true,
|
|
120
|
+
related_commands: ['notis auth status --verify', 'notis doctor'],
|
|
121
|
+
backend_call: { type: 'tool', name: 'notis_find_toolkits' },
|
|
122
|
+
handler: whoamiHandler,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
command_path: ['doctor'],
|
|
126
|
+
summary: 'Run a quick CLI health check for config, auth, and API reachability.',
|
|
127
|
+
when_to_use: 'Use this before relying on the CLI in automation or after changing environments.',
|
|
128
|
+
args_schema: { arguments: [], options: [] },
|
|
129
|
+
examples: ['notis doctor', 'notis doctor --json'],
|
|
130
|
+
output_schema: 'Returns config, auth, health, and roundtrip check statuses.',
|
|
131
|
+
mutates: false,
|
|
132
|
+
idempotent: true,
|
|
133
|
+
related_commands: ['notis auth status --verify', 'notis tools toolkits'],
|
|
134
|
+
backend_call: { type: 'health+tool_roundtrip' },
|
|
135
|
+
handler: doctorHandler,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
command_path: ['describe'],
|
|
139
|
+
summary: 'Describe a first-class CLI command in detail.',
|
|
140
|
+
when_to_use: 'Use this when an agent or human needs the exact shape, examples, and semantics of a command.',
|
|
141
|
+
args_schema: {
|
|
142
|
+
arguments: [{ token: '<command...>', key: 'commandPath', description: 'Command path to describe, such as "apps push".' }],
|
|
143
|
+
options: [],
|
|
144
|
+
},
|
|
145
|
+
examples: ['notis describe apps push', 'notis describe db query'],
|
|
146
|
+
output_schema: 'Returns the command spec metadata for the requested command.',
|
|
147
|
+
mutates: false,
|
|
148
|
+
idempotent: true,
|
|
149
|
+
require_auth: false,
|
|
150
|
+
related_commands: ['notis --help', 'notis tools describe <tool-name>'],
|
|
151
|
+
backend_call: { type: 'local_registry' },
|
|
152
|
+
handler: describeHandler,
|
|
153
|
+
},
|
|
154
|
+
];
|