@khotan/cli 0.1.0
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 +215 -0
- package/dist/khotan.js +2491 -0
- package/package.json +43 -0
package/dist/khotan.js
ADDED
|
@@ -0,0 +1,2491 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __returnValue = (v) => v;
|
|
6
|
+
function __exportSetter(name, newValue) {
|
|
7
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
8
|
+
}
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, {
|
|
12
|
+
get: all[name],
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
set: __exportSetter.bind(all, name)
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
|
+
|
|
21
|
+
// ../khotan-core/src/version.ts
|
|
22
|
+
var CATALOG_SCHEMA_VERSION = 1, CATALOG_VERSION = "2026-06-13", KHOTAN_ADAPTER_NAME = "khotan", KHOTAN_ADAPTER_VERSION = "0.1.0", SUPPORTED_CATALOG_SCHEMA_VERSIONS;
|
|
23
|
+
var init_version = __esm(() => {
|
|
24
|
+
SUPPORTED_CATALOG_SCHEMA_VERSIONS = [
|
|
25
|
+
CATALOG_SCHEMA_VERSION
|
|
26
|
+
];
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ../khotan-core/src/errors.ts
|
|
30
|
+
var KhotanError, KhotanApiError, KhotanClientError, KhotanUnknownCapabilityError, KhotanCatalogVersionError, KhotanConfirmationRequiredError;
|
|
31
|
+
var init_errors = __esm(() => {
|
|
32
|
+
KhotanError = class KhotanError extends Error {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "KhotanError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
KhotanApiError = class KhotanApiError extends KhotanError {
|
|
39
|
+
status;
|
|
40
|
+
code;
|
|
41
|
+
details;
|
|
42
|
+
constructor(params) {
|
|
43
|
+
super(params.message);
|
|
44
|
+
this.name = "KhotanApiError";
|
|
45
|
+
this.status = params.status;
|
|
46
|
+
this.code = params.code;
|
|
47
|
+
this.details = params.details;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
KhotanClientError = class KhotanClientError extends KhotanError {
|
|
51
|
+
code;
|
|
52
|
+
constructor(code, message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "KhotanClientError";
|
|
55
|
+
this.code = code;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
KhotanUnknownCapabilityError = class KhotanUnknownCapabilityError extends KhotanClientError {
|
|
59
|
+
constructor(capabilityId) {
|
|
60
|
+
super("unknown_capability", `No Khotan capability is registered for id "${capabilityId}".`);
|
|
61
|
+
this.name = "KhotanUnknownCapabilityError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
KhotanCatalogVersionError = class KhotanCatalogVersionError extends KhotanClientError {
|
|
65
|
+
catalogSchemaVersion;
|
|
66
|
+
supportedSchemaVersions;
|
|
67
|
+
constructor(params) {
|
|
68
|
+
super("unsupported_catalog_version", `Khotan catalog schema version ${params.catalogSchemaVersion} is not supported by adapter ${params.adapterVersion} (supports: ${params.supportedSchemaVersions.join(", ")}). Upgrade the Khotan CLI.`);
|
|
69
|
+
this.name = "KhotanCatalogVersionError";
|
|
70
|
+
this.catalogSchemaVersion = params.catalogSchemaVersion;
|
|
71
|
+
this.supportedSchemaVersions = params.supportedSchemaVersions;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
KhotanConfirmationRequiredError = class KhotanConfirmationRequiredError extends KhotanClientError {
|
|
75
|
+
constructor(message) {
|
|
76
|
+
super("confirmation_required", message);
|
|
77
|
+
this.name = "KhotanConfirmationRequiredError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ../khotan-core/src/safety.ts
|
|
83
|
+
function requiresConfirmation(safety) {
|
|
84
|
+
return safety === "destructive";
|
|
85
|
+
}
|
|
86
|
+
function isSecret(safety) {
|
|
87
|
+
return safety === "secret";
|
|
88
|
+
}
|
|
89
|
+
function isDestructive(safety) {
|
|
90
|
+
return safety === "destructive";
|
|
91
|
+
}
|
|
92
|
+
function isReadOnly(safety) {
|
|
93
|
+
return safety === "read";
|
|
94
|
+
}
|
|
95
|
+
function isResourceEligible(safety) {
|
|
96
|
+
return safety === "read";
|
|
97
|
+
}
|
|
98
|
+
var SAFETY_LEVELS;
|
|
99
|
+
var init_safety = __esm(() => {
|
|
100
|
+
SAFETY_LEVELS = ["read", "write", "destructive", "secret"];
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ../khotan-core/src/catalog/types.ts
|
|
104
|
+
function isOperationCapability(capability) {
|
|
105
|
+
return capability.kind === "operation";
|
|
106
|
+
}
|
|
107
|
+
function isResourceCapability(capability) {
|
|
108
|
+
return capability.kind === "resource";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ../khotan-core/src/catalog/catalog.ts
|
|
112
|
+
function platformDomain(config) {
|
|
113
|
+
const { domain, noun, resourceParam, resourceField, operationIds } = config;
|
|
114
|
+
const base = `/api/v1/${domain}`;
|
|
115
|
+
const idArg = idField(resourceParam, `The ${noun} id.`);
|
|
116
|
+
const cap = domain;
|
|
117
|
+
const a = article(noun);
|
|
118
|
+
const tool = (suffix) => `khotan_${domain}_${suffix}`;
|
|
119
|
+
const operations = [
|
|
120
|
+
{
|
|
121
|
+
id: `${cap}.list`,
|
|
122
|
+
kind: "operation",
|
|
123
|
+
domain,
|
|
124
|
+
safety: "read",
|
|
125
|
+
auth: true,
|
|
126
|
+
title: `List ${noun}s`,
|
|
127
|
+
description: `List the organization's ${noun}s.`,
|
|
128
|
+
input: [],
|
|
129
|
+
http: { method: "GET", path: base, operationId: operationIds.list },
|
|
130
|
+
cli: { command: [domain, "list"], summary: `List ${noun}s`, defaultOutput: "table" },
|
|
131
|
+
mcp: { tool: { name: tool("list"), title: `List ${noun}s`, description: `List the organization's ${noun}s.` } }
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: `${cap}.get`,
|
|
135
|
+
kind: "operation",
|
|
136
|
+
domain,
|
|
137
|
+
safety: "read",
|
|
138
|
+
auth: true,
|
|
139
|
+
title: `Get ${a} ${noun}`,
|
|
140
|
+
description: `Get one ${noun} by id, including its current deployment.`,
|
|
141
|
+
input: [idArg],
|
|
142
|
+
http: { method: "GET", path: `${base}/{${resourceParam}}`, operationId: operationIds.get },
|
|
143
|
+
cli: { command: [domain, "get"], summary: `Get ${a} ${noun}`, defaultOutput: "detail" },
|
|
144
|
+
mcp: { tool: { name: tool("get"), title: `Get ${a} ${noun}`, description: `Get one ${noun} by id.` } }
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: `${cap}.create`,
|
|
148
|
+
kind: "operation",
|
|
149
|
+
domain,
|
|
150
|
+
safety: "write",
|
|
151
|
+
auth: true,
|
|
152
|
+
title: `Create ${a} ${noun}`,
|
|
153
|
+
description: `Provision a new ${noun}.`,
|
|
154
|
+
input: [
|
|
155
|
+
{ name: "name", location: "body", type: "string", required: true, description: `The ${noun} name.` },
|
|
156
|
+
{ name: "regionPreference", location: "body", type: "string", description: "Optional region id; omit for the default region." },
|
|
157
|
+
{ name: "environmentVariables", location: "body", type: "json", description: "Optional initial env vars: array of { key, value }." },
|
|
158
|
+
{ name: "idempotencyKey", location: "body", type: "string", description: "Optional retry token; repeating returns the existing resource." }
|
|
159
|
+
],
|
|
160
|
+
http: { method: "POST", path: base, operationId: operationIds.create, bodyMode: "json" },
|
|
161
|
+
cli: { command: [domain, "create"], summary: `Create ${a} ${noun}`, defaultOutput: "detail" },
|
|
162
|
+
mcp: { tool: { name: tool("create"), title: `Create ${a} ${noun}`, description: `Provision a new ${noun}.` } }
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: `${cap}.delete`,
|
|
166
|
+
kind: "operation",
|
|
167
|
+
domain,
|
|
168
|
+
safety: "destructive",
|
|
169
|
+
auth: true,
|
|
170
|
+
title: `Delete ${a} ${noun}`,
|
|
171
|
+
description: `Permanently delete ${a} ${noun} and tear down its provider resources.`,
|
|
172
|
+
input: [idArg],
|
|
173
|
+
http: { method: "DELETE", path: `${base}/{${resourceParam}}`, operationId: operationIds.delete },
|
|
174
|
+
cli: { command: [domain, "delete"], summary: `Delete ${a} ${noun}` },
|
|
175
|
+
mcp: { tool: { name: tool("delete"), title: `Delete ${a} ${noun}`, description: `Permanently delete ${a} ${noun}. Requires confirmation.` } }
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: `${cap}.redeploy`,
|
|
179
|
+
kind: "operation",
|
|
180
|
+
domain,
|
|
181
|
+
safety: "write",
|
|
182
|
+
auth: true,
|
|
183
|
+
title: `Redeploy ${a} ${noun}`,
|
|
184
|
+
description: `Trigger a new deployment of ${a} ${noun}.`,
|
|
185
|
+
input: [idArg],
|
|
186
|
+
http: { method: "POST", path: `${base}/{${resourceParam}}/redeploy`, operationId: operationIds.redeploy, bodyMode: "none" },
|
|
187
|
+
cli: { command: [domain, "redeploy"], summary: `Redeploy ${a} ${noun}`, defaultOutput: "detail" },
|
|
188
|
+
mcp: { tool: { name: tool("redeploy"), title: `Redeploy ${a} ${noun}`, description: `Trigger a new deployment of ${a} ${noun}.` } }
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: `${cap}.env.list`,
|
|
192
|
+
kind: "operation",
|
|
193
|
+
domain,
|
|
194
|
+
safety: "read",
|
|
195
|
+
auth: true,
|
|
196
|
+
title: `List ${noun} environment variables`,
|
|
197
|
+
description: `List env var metadata (never values) for a ${noun}.`,
|
|
198
|
+
input: [idArg],
|
|
199
|
+
http: { method: "GET", path: `${base}/{${resourceParam}}/env`, operationId: operationIds.envList },
|
|
200
|
+
cli: { command: [domain, "env", "list"], summary: "List env var metadata", defaultOutput: "table" },
|
|
201
|
+
mcp: { tool: { name: tool("env_list"), title: "List env var metadata", description: "List env var metadata (keys and update times only; never values)." } }
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: `${cap}.env.bulk-set`,
|
|
205
|
+
kind: "operation",
|
|
206
|
+
domain,
|
|
207
|
+
safety: "write",
|
|
208
|
+
auth: true,
|
|
209
|
+
title: `Bulk upsert ${noun} environment variables`,
|
|
210
|
+
description: `Create or update many env vars in one request.`,
|
|
211
|
+
input: [
|
|
212
|
+
idArg,
|
|
213
|
+
{ name: "variables", location: "body", type: "json", required: true, description: "Array of { key, value } to upsert." }
|
|
214
|
+
],
|
|
215
|
+
http: { method: "POST", path: `${base}/{${resourceParam}}/env`, operationId: operationIds.envBulkSet, bodyMode: "json" },
|
|
216
|
+
cli: { command: [domain, "env", "bulk-set"], summary: "Bulk upsert env vars" },
|
|
217
|
+
mcp: { tool: { name: tool("env_bulk_set"), title: "Bulk upsert env vars", description: "Create or update many env vars in one request." } }
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: `${cap}.env.reveal`,
|
|
221
|
+
kind: "operation",
|
|
222
|
+
domain,
|
|
223
|
+
safety: "secret",
|
|
224
|
+
auth: true,
|
|
225
|
+
title: `Reveal one ${noun} environment variable value`,
|
|
226
|
+
description: `Reveal a single decrypted env var value. Secret-bearing; explicit-only.`,
|
|
227
|
+
input: [idArg, idField("key", "The env var key to reveal.")],
|
|
228
|
+
http: { method: "GET", path: `${base}/{${resourceParam}}/env/{key}`, operationId: operationIds.envReveal },
|
|
229
|
+
cli: { command: [domain, "env", "reveal"], summary: "Reveal one env var value (secret)", defaultOutput: "raw" },
|
|
230
|
+
mcp: { tool: { name: tool("env_reveal"), title: "Reveal one env var value (secret)", description: "Reveal one decrypted env var value. Secret-bearing — returns a credential, not a list." } }
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: `${cap}.env.set`,
|
|
234
|
+
kind: "operation",
|
|
235
|
+
domain,
|
|
236
|
+
safety: "write",
|
|
237
|
+
auth: true,
|
|
238
|
+
title: `Set one ${noun} environment variable`,
|
|
239
|
+
description: `Create or update one env var across all targets.`,
|
|
240
|
+
input: [
|
|
241
|
+
idArg,
|
|
242
|
+
idField("key", "The env var key."),
|
|
243
|
+
{ name: "value", location: "body", type: "string", required: true, secret: true, description: "The env var value." }
|
|
244
|
+
],
|
|
245
|
+
http: { method: "PUT", path: `${base}/{${resourceParam}}/env/{key}`, operationId: operationIds.envSet, bodyMode: "json" },
|
|
246
|
+
cli: { command: [domain, "env", "set"], summary: "Set one env var" },
|
|
247
|
+
mcp: { tool: { name: tool("env_set"), title: "Set one env var", description: "Create or update one env var across all targets." } }
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: `${cap}.env.unset`,
|
|
251
|
+
kind: "operation",
|
|
252
|
+
domain,
|
|
253
|
+
safety: "destructive",
|
|
254
|
+
auth: true,
|
|
255
|
+
title: `Unset one ${noun} environment variable`,
|
|
256
|
+
description: `Delete one env var across all targets.`,
|
|
257
|
+
input: [idArg, idField("key", "The env var key to delete.")],
|
|
258
|
+
http: { method: "DELETE", path: `${base}/{${resourceParam}}/env/{key}`, operationId: operationIds.envDelete },
|
|
259
|
+
cli: { command: [domain, "env", "unset"], summary: "Unset one env var" },
|
|
260
|
+
mcp: { tool: { name: tool("env_unset"), title: "Unset one env var", description: "Delete one env var across all targets. Requires confirmation." } }
|
|
261
|
+
}
|
|
262
|
+
];
|
|
263
|
+
const resource = {
|
|
264
|
+
id: `${cap}.resource`,
|
|
265
|
+
kind: "resource",
|
|
266
|
+
domain,
|
|
267
|
+
safety: "read",
|
|
268
|
+
auth: true,
|
|
269
|
+
title: `${noun[0]?.toUpperCase()}${noun.slice(1)} summary`,
|
|
270
|
+
description: `Credential-free ${noun} summary addressed by id.`,
|
|
271
|
+
input: [idArg],
|
|
272
|
+
readsVia: `${cap}.get`,
|
|
273
|
+
http: { method: "GET", path: `${base}/{${resourceParam}}`, operationId: operationIds.get },
|
|
274
|
+
mcp: {
|
|
275
|
+
resource: {
|
|
276
|
+
uriTemplate: `khotan://${domain}/{${resourceField}}`,
|
|
277
|
+
name: `${domain}-summary`,
|
|
278
|
+
title: `${noun[0]?.toUpperCase()}${noun.slice(1)} summary`,
|
|
279
|
+
description: `Credential-free ${noun} summary as JSON.`,
|
|
280
|
+
mimeType: "application/json"
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
return [...operations, resource];
|
|
285
|
+
}
|
|
286
|
+
function getCapability(id) {
|
|
287
|
+
return capabilitiesById.get(id);
|
|
288
|
+
}
|
|
289
|
+
function listCapabilities() {
|
|
290
|
+
return capabilities;
|
|
291
|
+
}
|
|
292
|
+
function listDomains() {
|
|
293
|
+
const seen = [];
|
|
294
|
+
for (const capability of capabilities) {
|
|
295
|
+
if (!seen.includes(capability.domain)) {
|
|
296
|
+
seen.push(capability.domain);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return seen;
|
|
300
|
+
}
|
|
301
|
+
var idField = (name, description) => ({
|
|
302
|
+
name,
|
|
303
|
+
location: "path",
|
|
304
|
+
type: "string",
|
|
305
|
+
required: true,
|
|
306
|
+
description
|
|
307
|
+
}), article = (noun) => /^[aeiou]/i.test(noun) ? "an" : "a", identityCapabilities, databaseCapabilities, fileCapabilities, folderCapabilities, contextCapabilities, appsCapabilities, pipelinesCapabilities, capabilities, khotanCatalog, capabilitiesById;
|
|
308
|
+
var init_catalog = __esm(() => {
|
|
309
|
+
init_version();
|
|
310
|
+
identityCapabilities = [
|
|
311
|
+
{
|
|
312
|
+
id: "identity.whoami",
|
|
313
|
+
kind: "operation",
|
|
314
|
+
domain: "identity",
|
|
315
|
+
safety: "read",
|
|
316
|
+
auth: true,
|
|
317
|
+
title: "Identify the caller",
|
|
318
|
+
description: "Resolve the organization, role, and key id from the API key.",
|
|
319
|
+
input: [],
|
|
320
|
+
http: { method: "GET", path: "/api/v1/me", operationId: "getCurrentApiPrincipal" },
|
|
321
|
+
cli: { command: ["whoami"], summary: "Show the authenticated principal", defaultOutput: "detail" },
|
|
322
|
+
mcp: { tool: { name: "khotan_whoami", title: "Identify the caller", description: "Resolve the organization, role, and key id from the API key." } }
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: "api-keys.create-from-credentials",
|
|
326
|
+
kind: "operation",
|
|
327
|
+
domain: "api-keys",
|
|
328
|
+
safety: "secret",
|
|
329
|
+
auth: false,
|
|
330
|
+
title: "Exchange credentials for an API key",
|
|
331
|
+
description: "Authenticate with email + password and mint an organization-scoped API key. Returns the key once. Used by `khotan login`; not exposed as an MCP tool because it accepts a password.",
|
|
332
|
+
input: [
|
|
333
|
+
{ name: "email", location: "body", type: "string", required: true, description: "Account email." },
|
|
334
|
+
{ name: "password", location: "body", type: "string", required: true, secret: true, description: "Account password." },
|
|
335
|
+
{ name: "organizationId", location: "body", type: "string", required: true, description: "Organization the key is scoped to." },
|
|
336
|
+
{ name: "name", location: "body", type: "string", description: "Optional key name." },
|
|
337
|
+
{ name: "role", location: "body", type: "string", description: "Optional key role (cannot exceed the caller's role)." },
|
|
338
|
+
{ name: "expiresInDays", location: "body", type: "number", description: "Optional expiry in days (max 365)." }
|
|
339
|
+
],
|
|
340
|
+
http: { method: "POST", path: "/api/v1/api-keys", operationId: "createApiKeyFromCredentials", bodyMode: "json" },
|
|
341
|
+
cli: { command: ["auth", "login"], summary: "Exchange credentials for an API key" },
|
|
342
|
+
mcp: { tool: { name: "khotan_auth_login", title: "Exchange credentials for an API key", description: "Password-bearing credential exchange. CLI-only." } }
|
|
343
|
+
}
|
|
344
|
+
];
|
|
345
|
+
databaseCapabilities = [
|
|
346
|
+
{
|
|
347
|
+
id: "databases.list",
|
|
348
|
+
kind: "operation",
|
|
349
|
+
domain: "databases",
|
|
350
|
+
safety: "read",
|
|
351
|
+
auth: true,
|
|
352
|
+
title: "List databases",
|
|
353
|
+
description: "List the organization's production databases (no credentials).",
|
|
354
|
+
input: [],
|
|
355
|
+
http: { method: "GET", path: "/api/v1/databases", operationId: "listDatabases" },
|
|
356
|
+
cli: { command: ["databases", "list"], summary: "List databases", defaultOutput: "table" },
|
|
357
|
+
mcp: { tool: { name: "khotan_databases_list", title: "List databases", description: "List the organization's databases (no credentials)." } }
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: "databases.get",
|
|
361
|
+
kind: "operation",
|
|
362
|
+
domain: "databases",
|
|
363
|
+
safety: "read",
|
|
364
|
+
auth: true,
|
|
365
|
+
title: "Get a database",
|
|
366
|
+
description: "Get one database by id (no credentials).",
|
|
367
|
+
input: [idField("databaseId", "The database id.")],
|
|
368
|
+
http: { method: "GET", path: "/api/v1/databases/{databaseId}", operationId: "getDatabase" },
|
|
369
|
+
cli: { command: ["databases", "get"], summary: "Get a database", defaultOutput: "detail" },
|
|
370
|
+
mcp: { tool: { name: "khotan_databases_get", title: "Get a database", description: "Get one database by id (no credentials)." } }
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
id: "databases.create",
|
|
374
|
+
kind: "operation",
|
|
375
|
+
domain: "databases",
|
|
376
|
+
safety: "write",
|
|
377
|
+
auth: true,
|
|
378
|
+
title: "Create a database",
|
|
379
|
+
description: "Provision a production Postgres database.",
|
|
380
|
+
input: [
|
|
381
|
+
{ name: "name", location: "body", type: "string", required: true, description: "The database name." },
|
|
382
|
+
{ name: "regionId", location: "body", type: "string", description: "Optional region id; omit for the default region." }
|
|
383
|
+
],
|
|
384
|
+
http: { method: "POST", path: "/api/v1/databases", operationId: "createDatabase", bodyMode: "json" },
|
|
385
|
+
cli: { command: ["databases", "create"], summary: "Create a database", defaultOutput: "detail" },
|
|
386
|
+
mcp: { tool: { name: "khotan_databases_create", title: "Create a database", description: "Provision a production Postgres database." } }
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: "databases.delete",
|
|
390
|
+
kind: "operation",
|
|
391
|
+
domain: "databases",
|
|
392
|
+
safety: "destructive",
|
|
393
|
+
auth: true,
|
|
394
|
+
title: "Delete a database",
|
|
395
|
+
description: "Permanently delete a database.",
|
|
396
|
+
input: [idField("databaseId", "The database id.")],
|
|
397
|
+
http: { method: "DELETE", path: "/api/v1/databases/{databaseId}", operationId: "deleteDatabase" },
|
|
398
|
+
cli: { command: ["databases", "delete"], summary: "Delete a database" },
|
|
399
|
+
mcp: { tool: { name: "khotan_databases_delete", title: "Delete a database", description: "Permanently delete a database. Requires confirmation." } }
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "databases.connection",
|
|
403
|
+
kind: "operation",
|
|
404
|
+
domain: "databases",
|
|
405
|
+
safety: "secret",
|
|
406
|
+
auth: true,
|
|
407
|
+
title: "Get a database connection (secret)",
|
|
408
|
+
description: "Retrieve a database connection URI. Secret-bearing; explicit-only.",
|
|
409
|
+
input: [idField("databaseId", "The database id.")],
|
|
410
|
+
http: { method: "GET", path: "/api/v1/databases/{databaseId}/connection", operationId: "getDatabaseConnection" },
|
|
411
|
+
cli: { command: ["databases", "connection"], summary: "Get a connection URI (secret)", defaultOutput: "raw" },
|
|
412
|
+
mcp: { tool: { name: "khotan_databases_connection", title: "Get a connection URI (secret)", description: "Retrieve a database connection URI. Secret-bearing — returns a credential." } }
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
id: "databases.rotate-credentials",
|
|
416
|
+
kind: "operation",
|
|
417
|
+
domain: "databases",
|
|
418
|
+
safety: "secret",
|
|
419
|
+
auth: true,
|
|
420
|
+
title: "Rotate database credentials (secret)",
|
|
421
|
+
description: "Rotate a database's credentials and return the new connection. Secret-bearing.",
|
|
422
|
+
input: [idField("databaseId", "The database id.")],
|
|
423
|
+
http: { method: "POST", path: "/api/v1/databases/{databaseId}/rotate-credentials", operationId: "rotateDatabaseCredentials", bodyMode: "none" },
|
|
424
|
+
cli: { command: ["databases", "rotate-credentials"], summary: "Rotate credentials (secret)", defaultOutput: "raw" },
|
|
425
|
+
mcp: { tool: { name: "khotan_databases_rotate_credentials", title: "Rotate credentials (secret)", description: "Rotate a database's credentials and return the new connection. Secret-bearing." } }
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
id: "databases.resource",
|
|
429
|
+
kind: "resource",
|
|
430
|
+
domain: "databases",
|
|
431
|
+
safety: "read",
|
|
432
|
+
auth: true,
|
|
433
|
+
title: "Database summary",
|
|
434
|
+
description: "Credential-free database summary addressed by id.",
|
|
435
|
+
input: [idField("databaseId", "The database id.")],
|
|
436
|
+
readsVia: "databases.get",
|
|
437
|
+
http: { method: "GET", path: "/api/v1/databases/{databaseId}", operationId: "getDatabase" },
|
|
438
|
+
mcp: {
|
|
439
|
+
resource: {
|
|
440
|
+
uriTemplate: "khotan://databases/{databaseId}",
|
|
441
|
+
name: "database-summary",
|
|
442
|
+
title: "Database summary",
|
|
443
|
+
description: "Credential-free database summary as JSON.",
|
|
444
|
+
mimeType: "application/json"
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
];
|
|
449
|
+
fileCapabilities = [
|
|
450
|
+
{
|
|
451
|
+
id: "files.list",
|
|
452
|
+
kind: "operation",
|
|
453
|
+
domain: "files",
|
|
454
|
+
safety: "read",
|
|
455
|
+
auth: true,
|
|
456
|
+
title: "List files",
|
|
457
|
+
description: "List files, optionally scoped to a folder.",
|
|
458
|
+
input: [
|
|
459
|
+
{ name: "folderId", location: "query", type: "string", description: "Scope to a folder id." },
|
|
460
|
+
{ name: "path", location: "query", type: "string", description: "Scope to a folder path." },
|
|
461
|
+
{ name: "limit", location: "query", type: "number", default: 50, description: "Page size (1-200)." },
|
|
462
|
+
{ name: "cursor", location: "query", type: "string", description: "Opaque pagination cursor." }
|
|
463
|
+
],
|
|
464
|
+
http: { method: "GET", path: "/api/v1/files", operationId: "listFiles" },
|
|
465
|
+
cli: { command: ["files", "list"], summary: "List files", defaultOutput: "table" },
|
|
466
|
+
mcp: { tool: { name: "khotan_files_list", title: "List files", description: "List files, optionally scoped to a folder." } }
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: "files.get",
|
|
470
|
+
kind: "operation",
|
|
471
|
+
domain: "files",
|
|
472
|
+
safety: "read",
|
|
473
|
+
auth: true,
|
|
474
|
+
title: "Get a file",
|
|
475
|
+
description: "Get one file's metadata by id.",
|
|
476
|
+
input: [idField("fileId", "The file id.")],
|
|
477
|
+
http: { method: "GET", path: "/api/v1/files/{fileId}", operationId: "getFile" },
|
|
478
|
+
cli: { command: ["files", "get"], summary: "Get file metadata", defaultOutput: "detail" },
|
|
479
|
+
mcp: { tool: { name: "khotan_files_get", title: "Get a file", description: "Get one file's metadata by id." } }
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: "files.update",
|
|
483
|
+
kind: "operation",
|
|
484
|
+
domain: "files",
|
|
485
|
+
safety: "write",
|
|
486
|
+
auth: true,
|
|
487
|
+
title: "Rename or move a file",
|
|
488
|
+
description: "Update a file's name and/or containing folder.",
|
|
489
|
+
input: [
|
|
490
|
+
idField("fileId", "The file id."),
|
|
491
|
+
{ name: "name", location: "body", type: "string", description: "New file name." },
|
|
492
|
+
{ name: "folderId", location: "body", type: "string", description: "Destination folder id (null moves to root)." },
|
|
493
|
+
{ name: "folderPath", location: "body", type: "string", description: "Destination folder path." }
|
|
494
|
+
],
|
|
495
|
+
http: { method: "PATCH", path: "/api/v1/files/{fileId}", operationId: "updateFile", bodyMode: "json" },
|
|
496
|
+
cli: { command: ["files", "update"], summary: "Rename or move a file", defaultOutput: "detail" },
|
|
497
|
+
mcp: { tool: { name: "khotan_files_update", title: "Rename or move a file", description: "Update a file's name and/or containing folder." } }
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: "files.delete",
|
|
501
|
+
kind: "operation",
|
|
502
|
+
domain: "files",
|
|
503
|
+
safety: "destructive",
|
|
504
|
+
auth: true,
|
|
505
|
+
title: "Delete a file",
|
|
506
|
+
description: "Permanently delete a file.",
|
|
507
|
+
input: [idField("fileId", "The file id.")],
|
|
508
|
+
http: { method: "DELETE", path: "/api/v1/files/{fileId}", operationId: "deleteFile" },
|
|
509
|
+
cli: { command: ["files", "delete"], summary: "Delete a file" },
|
|
510
|
+
mcp: { tool: { name: "khotan_files_delete", title: "Delete a file", description: "Permanently delete a file. Requires confirmation." } }
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
id: "files.uploads.prepare",
|
|
514
|
+
kind: "operation",
|
|
515
|
+
domain: "files",
|
|
516
|
+
safety: "write",
|
|
517
|
+
auth: true,
|
|
518
|
+
title: "Prepare file uploads",
|
|
519
|
+
description: "Reserve pending file records and return presigned PUT URLs.",
|
|
520
|
+
input: [
|
|
521
|
+
{ name: "files", location: "body", type: "json", required: true, description: "Array of { name, size, type? } descriptors." },
|
|
522
|
+
{ name: "folderId", location: "body", type: "string", description: "Target folder id." },
|
|
523
|
+
{ name: "folderPath", location: "body", type: "string", description: "Target folder path (created if missing)." }
|
|
524
|
+
],
|
|
525
|
+
http: { method: "POST", path: "/api/v1/files/uploads", operationId: "prepareFileUploads", bodyMode: "json" },
|
|
526
|
+
cli: { command: ["files", "uploads", "prepare"], summary: "Prepare presigned uploads" },
|
|
527
|
+
mcp: { tool: { name: "khotan_files_uploads_prepare", title: "Prepare file uploads", description: "Reserve pending file records and return presigned PUT URLs." } }
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
id: "files.uploads.complete",
|
|
531
|
+
kind: "operation",
|
|
532
|
+
domain: "files",
|
|
533
|
+
safety: "write",
|
|
534
|
+
auth: true,
|
|
535
|
+
title: "Complete file uploads",
|
|
536
|
+
description: "Finalize uploads after bytes are PUT to the presigned URLs.",
|
|
537
|
+
input: [
|
|
538
|
+
{ name: "files", location: "body", type: "json", required: true, description: "Array of { fileId } to finalize." }
|
|
539
|
+
],
|
|
540
|
+
http: { method: "POST", path: "/api/v1/files/uploads/complete", operationId: "completeFileUploads", bodyMode: "json" },
|
|
541
|
+
cli: { command: ["files", "uploads", "complete"], summary: "Complete presigned uploads" },
|
|
542
|
+
mcp: { tool: { name: "khotan_files_uploads_complete", title: "Complete file uploads", description: "Finalize uploads after bytes are PUT to the presigned URLs." } }
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: "files.download",
|
|
546
|
+
kind: "operation",
|
|
547
|
+
domain: "files",
|
|
548
|
+
safety: "read",
|
|
549
|
+
auth: true,
|
|
550
|
+
title: "Get a file download URL",
|
|
551
|
+
description: "Return a short-lived presigned GET URL for a ready file.",
|
|
552
|
+
input: [idField("fileId", "The file id.")],
|
|
553
|
+
http: { method: "GET", path: "/api/v1/files/{fileId}/download", operationId: "getFileDownloadUrl" },
|
|
554
|
+
cli: { command: ["files", "download-url"], summary: "Get a file download URL", defaultOutput: "detail" },
|
|
555
|
+
mcp: { tool: { name: "khotan_files_download", title: "Get a file download URL", description: "Return a short-lived presigned GET URL for a ready file." } }
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: "files.resource",
|
|
559
|
+
kind: "resource",
|
|
560
|
+
domain: "files",
|
|
561
|
+
safety: "read",
|
|
562
|
+
auth: true,
|
|
563
|
+
title: "File metadata",
|
|
564
|
+
description: "File metadata addressed by id (no raw bytes).",
|
|
565
|
+
input: [idField("fileId", "The file id.")],
|
|
566
|
+
readsVia: "files.get",
|
|
567
|
+
http: { method: "GET", path: "/api/v1/files/{fileId}", operationId: "getFile" },
|
|
568
|
+
mcp: {
|
|
569
|
+
resource: {
|
|
570
|
+
uriTemplate: "khotan://files/{fileId}",
|
|
571
|
+
name: "file-metadata",
|
|
572
|
+
title: "File metadata",
|
|
573
|
+
description: "File metadata as JSON (no raw bytes).",
|
|
574
|
+
mimeType: "application/json"
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
];
|
|
579
|
+
folderCapabilities = [
|
|
580
|
+
{
|
|
581
|
+
id: "folders.list",
|
|
582
|
+
kind: "operation",
|
|
583
|
+
domain: "folders",
|
|
584
|
+
safety: "read",
|
|
585
|
+
auth: true,
|
|
586
|
+
title: "List folders",
|
|
587
|
+
description: "List all active folders as a flat list.",
|
|
588
|
+
input: [],
|
|
589
|
+
http: { method: "GET", path: "/api/v1/folders", operationId: "listFolders" },
|
|
590
|
+
cli: { command: ["folders", "list"], summary: "List folders", defaultOutput: "table" },
|
|
591
|
+
mcp: { tool: { name: "khotan_folders_list", title: "List folders", description: "List all active folders as a flat list." } }
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
id: "folders.create",
|
|
595
|
+
kind: "operation",
|
|
596
|
+
domain: "folders",
|
|
597
|
+
safety: "write",
|
|
598
|
+
auth: true,
|
|
599
|
+
title: "Create a folder",
|
|
600
|
+
description: "Create a folder under the root, a parent id, or a parent path.",
|
|
601
|
+
input: [
|
|
602
|
+
{ name: "name", location: "body", type: "string", required: true, description: "The folder name." },
|
|
603
|
+
{ name: "parentId", location: "body", type: "string", description: "Parent folder id." },
|
|
604
|
+
{ name: "parentPath", location: "body", type: "string", description: "Parent folder path." }
|
|
605
|
+
],
|
|
606
|
+
http: { method: "POST", path: "/api/v1/folders", operationId: "createFolder", bodyMode: "json" },
|
|
607
|
+
cli: { command: ["folders", "create"], summary: "Create a folder", defaultOutput: "detail" },
|
|
608
|
+
mcp: { tool: { name: "khotan_folders_create", title: "Create a folder", description: "Create a folder under the root, a parent id, or a parent path." } }
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
id: "folders.rename",
|
|
612
|
+
kind: "operation",
|
|
613
|
+
domain: "folders",
|
|
614
|
+
safety: "write",
|
|
615
|
+
auth: true,
|
|
616
|
+
title: "Rename a folder",
|
|
617
|
+
description: "Rename a folder.",
|
|
618
|
+
input: [
|
|
619
|
+
idField("folderId", "The folder id."),
|
|
620
|
+
{ name: "name", location: "body", type: "string", required: true, description: "The new folder name." }
|
|
621
|
+
],
|
|
622
|
+
http: { method: "PATCH", path: "/api/v1/folders/{folderId}", operationId: "renameFolder", bodyMode: "json" },
|
|
623
|
+
cli: { command: ["folders", "rename"], summary: "Rename a folder", defaultOutput: "detail" },
|
|
624
|
+
mcp: { tool: { name: "khotan_folders_rename", title: "Rename a folder", description: "Rename a folder." } }
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
id: "folders.delete",
|
|
628
|
+
kind: "operation",
|
|
629
|
+
domain: "folders",
|
|
630
|
+
safety: "destructive",
|
|
631
|
+
auth: true,
|
|
632
|
+
title: "Delete a folder",
|
|
633
|
+
description: "Delete a folder and its contents.",
|
|
634
|
+
input: [idField("folderId", "The folder id.")],
|
|
635
|
+
http: { method: "DELETE", path: "/api/v1/folders/{folderId}", operationId: "deleteFolder" },
|
|
636
|
+
cli: { command: ["folders", "delete"], summary: "Delete a folder" },
|
|
637
|
+
mcp: { tool: { name: "khotan_folders_delete", title: "Delete a folder", description: "Delete a folder and its contents. Requires confirmation." } }
|
|
638
|
+
}
|
|
639
|
+
];
|
|
640
|
+
contextCapabilities = [
|
|
641
|
+
{
|
|
642
|
+
id: "context.list",
|
|
643
|
+
kind: "operation",
|
|
644
|
+
domain: "context",
|
|
645
|
+
safety: "read",
|
|
646
|
+
auth: true,
|
|
647
|
+
title: "List context documents",
|
|
648
|
+
description: "List the context document manifest (metadata only, never content).",
|
|
649
|
+
input: [
|
|
650
|
+
{ name: "kind", location: "query", type: "string", description: "Filter by kind: instructions, knowledge, or record." },
|
|
651
|
+
{ name: "q", location: "query", type: "string", description: "Full-text search query." }
|
|
652
|
+
],
|
|
653
|
+
http: { method: "GET", path: "/api/v1/context", operationId: "listContextDocuments" },
|
|
654
|
+
cli: { command: ["context", "list"], summary: "List context documents", defaultOutput: "table" },
|
|
655
|
+
mcp: { tool: { name: "khotan_context_list", title: "List context documents", description: "List the context document manifest (metadata only)." } }
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
id: "context.get",
|
|
659
|
+
kind: "operation",
|
|
660
|
+
domain: "context",
|
|
661
|
+
safety: "read",
|
|
662
|
+
auth: true,
|
|
663
|
+
title: "Get a context document",
|
|
664
|
+
description: "Get a context document including its markdown content, in a JSON envelope.",
|
|
665
|
+
input: [idField("slug", "The document slug.")],
|
|
666
|
+
http: { method: "GET", path: "/api/v1/context/{slug}", operationId: "getContextDocument" },
|
|
667
|
+
cli: { command: ["context", "get"], summary: "Get a context document", defaultOutput: "detail" },
|
|
668
|
+
mcp: { tool: { name: "khotan_context_get", title: "Get a context document", description: "Get a context document including its markdown content." } }
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
id: "context.raw",
|
|
672
|
+
kind: "operation",
|
|
673
|
+
domain: "context",
|
|
674
|
+
safety: "read",
|
|
675
|
+
auth: true,
|
|
676
|
+
title: "Get a context document's raw markdown",
|
|
677
|
+
description: "Get exactly the document's markdown text, with no JSON envelope.",
|
|
678
|
+
input: [idField("slug", "The document slug.")],
|
|
679
|
+
http: { method: "GET", path: "/api/v1/context/{slug}/raw", operationId: "getContextDocumentRaw", responseType: "markdown" },
|
|
680
|
+
cli: { command: ["context", "raw"], summary: "Print raw markdown", defaultOutput: "raw" },
|
|
681
|
+
mcp: { tool: { name: "khotan_context_raw", title: "Get raw markdown", description: "Get exactly the document's markdown text." } }
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
id: "context.create",
|
|
685
|
+
kind: "operation",
|
|
686
|
+
domain: "context",
|
|
687
|
+
safety: "write",
|
|
688
|
+
auth: true,
|
|
689
|
+
title: "Create a context document",
|
|
690
|
+
description: "Create a markdown context document.",
|
|
691
|
+
input: [
|
|
692
|
+
{ name: "title", location: "body", type: "string", required: true, description: "Document title." },
|
|
693
|
+
{ name: "kind", location: "body", type: "string", required: true, description: "instructions, knowledge, or record." },
|
|
694
|
+
{ name: "content", location: "body", type: "string", required: true, description: "Canonical markdown content." },
|
|
695
|
+
{ name: "slug", location: "body", type: "string", description: "Optional slug; derived from the title when omitted." },
|
|
696
|
+
{ name: "description", location: "body", type: "string", description: "One-line purpose statement." },
|
|
697
|
+
{ name: "tags", location: "body", type: "string[]", description: "Flat tag list." }
|
|
698
|
+
],
|
|
699
|
+
http: { method: "POST", path: "/api/v1/context", operationId: "createContextDocument", bodyMode: "json" },
|
|
700
|
+
cli: { command: ["context", "create"], summary: "Create a context document", defaultOutput: "detail" },
|
|
701
|
+
mcp: { tool: { name: "khotan_context_create", title: "Create a context document", description: "Create a markdown context document." } }
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
id: "context.update",
|
|
705
|
+
kind: "operation",
|
|
706
|
+
domain: "context",
|
|
707
|
+
safety: "write",
|
|
708
|
+
auth: true,
|
|
709
|
+
title: "Update a context document",
|
|
710
|
+
description: "Update a context document with compare-and-swap on expectedRevision.",
|
|
711
|
+
input: [
|
|
712
|
+
idField("slug", "The document slug."),
|
|
713
|
+
{ name: "expectedRevision", location: "body", type: "number", required: true, description: "Revision this edit is based on; a mismatch returns 409." },
|
|
714
|
+
{ name: "title", location: "body", type: "string", description: "New title." },
|
|
715
|
+
{ name: "kind", location: "body", type: "string", description: "New kind." },
|
|
716
|
+
{ name: "content", location: "body", type: "string", description: "New markdown content." },
|
|
717
|
+
{ name: "description", location: "body", type: "string", description: "New description (null clears it)." },
|
|
718
|
+
{ name: "tags", location: "body", type: "string[]", description: "New tag list." },
|
|
719
|
+
{ name: "changeSummary", location: "body", type: "string", description: "Optional revision-history note." }
|
|
720
|
+
],
|
|
721
|
+
http: { method: "PATCH", path: "/api/v1/context/{slug}", operationId: "updateContextDocument", bodyMode: "json" },
|
|
722
|
+
cli: { command: ["context", "update"], summary: "Update a context document", defaultOutput: "detail" },
|
|
723
|
+
mcp: { tool: { name: "khotan_context_update", title: "Update a context document", description: "Update a context document; pass expectedRevision for compare-and-swap." } }
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
id: "context.delete",
|
|
727
|
+
kind: "operation",
|
|
728
|
+
domain: "context",
|
|
729
|
+
safety: "destructive",
|
|
730
|
+
auth: true,
|
|
731
|
+
title: "Delete a context document",
|
|
732
|
+
description: "Soft-delete a context document.",
|
|
733
|
+
input: [idField("slug", "The document slug.")],
|
|
734
|
+
http: { method: "DELETE", path: "/api/v1/context/{slug}", operationId: "deleteContextDocument" },
|
|
735
|
+
cli: { command: ["context", "delete"], summary: "Delete a context document" },
|
|
736
|
+
mcp: { tool: { name: "khotan_context_delete", title: "Delete a context document", description: "Soft-delete a context document. Requires confirmation." } }
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
id: "context.resource",
|
|
740
|
+
kind: "resource",
|
|
741
|
+
domain: "context",
|
|
742
|
+
safety: "read",
|
|
743
|
+
auth: true,
|
|
744
|
+
title: "Context document markdown",
|
|
745
|
+
description: "Context document markdown addressed by slug.",
|
|
746
|
+
input: [idField("slug", "The document slug.")],
|
|
747
|
+
readsVia: "context.raw",
|
|
748
|
+
http: { method: "GET", path: "/api/v1/context/{slug}/raw", operationId: "getContextDocumentRaw", responseType: "markdown" },
|
|
749
|
+
mcp: {
|
|
750
|
+
resource: {
|
|
751
|
+
uriTemplate: "khotan://context/{slug}",
|
|
752
|
+
name: "context-document",
|
|
753
|
+
title: "Context document markdown",
|
|
754
|
+
description: "The document's canonical markdown.",
|
|
755
|
+
mimeType: "text/markdown"
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
];
|
|
760
|
+
appsCapabilities = platformDomain({
|
|
761
|
+
domain: "apps",
|
|
762
|
+
noun: "app",
|
|
763
|
+
resourceParam: "appId",
|
|
764
|
+
resourceField: "appId",
|
|
765
|
+
operationIds: {
|
|
766
|
+
list: "listApps",
|
|
767
|
+
get: "getApp",
|
|
768
|
+
create: "createApp",
|
|
769
|
+
delete: "deleteApp",
|
|
770
|
+
redeploy: "redeployApp",
|
|
771
|
+
envList: "listAppEnvVars",
|
|
772
|
+
envBulkSet: "bulkUpsertAppEnvVars",
|
|
773
|
+
envReveal: "revealAppEnvVar",
|
|
774
|
+
envSet: "upsertAppEnvVar",
|
|
775
|
+
envDelete: "deleteAppEnvVar"
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
pipelinesCapabilities = platformDomain({
|
|
779
|
+
domain: "pipelines",
|
|
780
|
+
noun: "pipeline",
|
|
781
|
+
resourceParam: "pipelineId",
|
|
782
|
+
resourceField: "pipelineId",
|
|
783
|
+
operationIds: {
|
|
784
|
+
list: "listPipelines",
|
|
785
|
+
get: "getPipeline",
|
|
786
|
+
create: "createPipeline",
|
|
787
|
+
delete: "deletePipeline",
|
|
788
|
+
redeploy: "redeployPipeline",
|
|
789
|
+
envList: "listPipelineEnvVars",
|
|
790
|
+
envBulkSet: "bulkUpsertPipelineEnvVars",
|
|
791
|
+
envReveal: "revealPipelineEnvVar",
|
|
792
|
+
envSet: "upsertPipelineEnvVar",
|
|
793
|
+
envDelete: "deletePipelineEnvVar"
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
capabilities = [
|
|
797
|
+
...identityCapabilities,
|
|
798
|
+
...appsCapabilities,
|
|
799
|
+
...pipelinesCapabilities,
|
|
800
|
+
...databaseCapabilities,
|
|
801
|
+
...fileCapabilities,
|
|
802
|
+
...folderCapabilities,
|
|
803
|
+
...contextCapabilities
|
|
804
|
+
];
|
|
805
|
+
khotanCatalog = {
|
|
806
|
+
schemaVersion: CATALOG_SCHEMA_VERSION,
|
|
807
|
+
catalogVersion: CATALOG_VERSION,
|
|
808
|
+
capabilities
|
|
809
|
+
};
|
|
810
|
+
capabilitiesById = new Map(capabilities.map((capability) => [capability.id, capability]));
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
// ../khotan-core/src/catalog/validate.ts
|
|
814
|
+
function assertSupportedCatalogSchemaVersion(catalog = khotanCatalog, options = {}) {
|
|
815
|
+
const supported = options.supportedSchemaVersions ?? SUPPORTED_CATALOG_SCHEMA_VERSIONS;
|
|
816
|
+
if (!supported.includes(catalog.schemaVersion)) {
|
|
817
|
+
throw new KhotanCatalogVersionError({
|
|
818
|
+
catalogSchemaVersion: catalog.schemaVersion,
|
|
819
|
+
supportedSchemaVersions: supported,
|
|
820
|
+
adapterVersion: options.adapterVersion ?? KHOTAN_ADAPTER_VERSION
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function pathPlaceholders(path) {
|
|
825
|
+
return [...path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
|
|
826
|
+
}
|
|
827
|
+
function validateCatalogStructure(catalog = khotanCatalog) {
|
|
828
|
+
const errors = [];
|
|
829
|
+
const seenIds = new Set;
|
|
830
|
+
const seenToolNames = new Map;
|
|
831
|
+
const seenResourceUris = new Map;
|
|
832
|
+
for (const capability of catalog.capabilities) {
|
|
833
|
+
if (seenIds.has(capability.id)) {
|
|
834
|
+
errors.push({ capabilityId: capability.id, message: "Duplicate capability id." });
|
|
835
|
+
}
|
|
836
|
+
seenIds.add(capability.id);
|
|
837
|
+
const placeholders = pathPlaceholders(capability.http.path);
|
|
838
|
+
const pathInputs = capability.input.filter((field) => field.location === "path").map((field) => field.name);
|
|
839
|
+
for (const placeholder of placeholders) {
|
|
840
|
+
if (!pathInputs.includes(placeholder)) {
|
|
841
|
+
errors.push({
|
|
842
|
+
capabilityId: capability.id,
|
|
843
|
+
message: `Path placeholder {${placeholder}} has no matching path input.`
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
for (const pathInput of pathInputs) {
|
|
848
|
+
if (!placeholders.includes(pathInput)) {
|
|
849
|
+
errors.push({
|
|
850
|
+
capabilityId: capability.id,
|
|
851
|
+
message: `Path input "${pathInput}" is not present in path template ${capability.http.path}.`
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (isResourceCapability(capability)) {
|
|
856
|
+
if (!isResourceEligible(capability.safety)) {
|
|
857
|
+
errors.push({
|
|
858
|
+
capabilityId: capability.id,
|
|
859
|
+
message: `Resource capability must be read-safe; got "${capability.safety}".`
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
if (isSecret(capability.safety)) {
|
|
863
|
+
errors.push({
|
|
864
|
+
capabilityId: capability.id,
|
|
865
|
+
message: "Resource capability must not expose secret material."
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
const uri = capability.mcp.resource.uriTemplate;
|
|
869
|
+
const owner = seenResourceUris.get(uri);
|
|
870
|
+
if (owner) {
|
|
871
|
+
errors.push({
|
|
872
|
+
capabilityId: capability.id,
|
|
873
|
+
message: `Resource URI template ${uri} is already used by ${owner}.`
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
seenResourceUris.set(uri, capability.id);
|
|
877
|
+
const target = catalog.capabilities.find((c) => c.id === capability.readsVia);
|
|
878
|
+
if (!target) {
|
|
879
|
+
errors.push({
|
|
880
|
+
capabilityId: capability.id,
|
|
881
|
+
message: `readsVia references unknown capability "${capability.readsVia}".`
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
const toolName = capability.mcp.tool.name;
|
|
886
|
+
const owner = seenToolNames.get(toolName);
|
|
887
|
+
if (owner) {
|
|
888
|
+
errors.push({
|
|
889
|
+
capabilityId: capability.id,
|
|
890
|
+
message: `Tool name ${toolName} is already used by ${owner}.`
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
seenToolNames.set(toolName, capability.id);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return { ok: errors.length === 0, errors };
|
|
897
|
+
}
|
|
898
|
+
function indexOpenApiOperations(doc) {
|
|
899
|
+
const index = new Map;
|
|
900
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
901
|
+
if (!item) {
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
for (const method of HTTP_METHODS) {
|
|
905
|
+
const operation = item[method.toLowerCase()];
|
|
906
|
+
if (operation) {
|
|
907
|
+
index.set(`${method} ${path}`, operation.operationId);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return index;
|
|
912
|
+
}
|
|
913
|
+
function validateCatalogAgainstOpenApi(doc, catalog = khotanCatalog) {
|
|
914
|
+
const index = indexOpenApiOperations(doc);
|
|
915
|
+
const errors = [];
|
|
916
|
+
for (const capability of catalog.capabilities) {
|
|
917
|
+
const { method, path, operationId } = capability.http;
|
|
918
|
+
const key = `${method} ${path}`;
|
|
919
|
+
if (!index.has(key)) {
|
|
920
|
+
errors.push({
|
|
921
|
+
capabilityId: capability.id,
|
|
922
|
+
message: `No OpenAPI operation for ${key}.`
|
|
923
|
+
});
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const docOperationId = index.get(key);
|
|
927
|
+
if (docOperationId !== operationId) {
|
|
928
|
+
errors.push({
|
|
929
|
+
capabilityId: capability.id,
|
|
930
|
+
message: `OpenAPI operationId mismatch for ${key}: catalog="${operationId}", openapi="${docOperationId ?? "(none)"}".`
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return { ok: errors.length === 0, errors };
|
|
935
|
+
}
|
|
936
|
+
function capabilitiesByOperationId(catalog = khotanCatalog) {
|
|
937
|
+
const byOperation = new Map;
|
|
938
|
+
for (const capability of catalog.capabilities) {
|
|
939
|
+
const list = byOperation.get(capability.http.operationId) ?? [];
|
|
940
|
+
list.push(capability);
|
|
941
|
+
byOperation.set(capability.http.operationId, list);
|
|
942
|
+
}
|
|
943
|
+
return byOperation;
|
|
944
|
+
}
|
|
945
|
+
var HTTP_METHODS;
|
|
946
|
+
var init_validate = __esm(() => {
|
|
947
|
+
init_errors();
|
|
948
|
+
init_safety();
|
|
949
|
+
init_version();
|
|
950
|
+
init_catalog();
|
|
951
|
+
HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// ../khotan-core/src/client/api-client.ts
|
|
955
|
+
function resolveCapability(capabilityOrId) {
|
|
956
|
+
if (typeof capabilityOrId !== "string") {
|
|
957
|
+
return capabilityOrId;
|
|
958
|
+
}
|
|
959
|
+
const capability = getCapability(capabilityOrId);
|
|
960
|
+
if (!capability) {
|
|
961
|
+
throw new KhotanUnknownCapabilityError(capabilityOrId);
|
|
962
|
+
}
|
|
963
|
+
return capability;
|
|
964
|
+
}
|
|
965
|
+
function normalizeApiUrl(apiUrl) {
|
|
966
|
+
return apiUrl.replace(/\/+$/, "");
|
|
967
|
+
}
|
|
968
|
+
function buildHttpRequest(capabilityOrId, input, context) {
|
|
969
|
+
const capability = resolveCapability(capabilityOrId);
|
|
970
|
+
const { http } = capability;
|
|
971
|
+
let path = http.path;
|
|
972
|
+
const query = new URLSearchParams;
|
|
973
|
+
const body = {};
|
|
974
|
+
for (const field of capability.input) {
|
|
975
|
+
const value = input[field.name];
|
|
976
|
+
const provided = value !== undefined && value !== null;
|
|
977
|
+
if (field.location === "path") {
|
|
978
|
+
if (!provided) {
|
|
979
|
+
throw new KhotanClientError("missing_argument", `Missing required path argument "${field.name}" for ${capability.id}.`);
|
|
980
|
+
}
|
|
981
|
+
path = path.replace(`{${field.name}}`, encodeURIComponent(String(value)));
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (field.location === "query") {
|
|
985
|
+
const effective = provided ? value : field.default;
|
|
986
|
+
if (effective !== undefined && effective !== null) {
|
|
987
|
+
query.set(field.name, String(effective));
|
|
988
|
+
}
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (provided) {
|
|
992
|
+
body[field.name] = value;
|
|
993
|
+
} else if (field.required) {
|
|
994
|
+
throw new KhotanClientError("missing_argument", `Missing required argument "${field.name}" for ${capability.id}.`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const queryString = query.toString();
|
|
998
|
+
const url = `${normalizeApiUrl(context.apiUrl)}${path}${queryString ? `?${queryString}` : ""}`;
|
|
999
|
+
const headers = { accept: "application/json" };
|
|
1000
|
+
if (capability.auth) {
|
|
1001
|
+
if (!context.apiKey) {
|
|
1002
|
+
throw new KhotanClientError("not_authenticated", `Capability ${capability.id} requires authentication but no API key is configured.`);
|
|
1003
|
+
}
|
|
1004
|
+
headers["x-api-key"] = context.apiKey;
|
|
1005
|
+
}
|
|
1006
|
+
const request = { url, method: http.method, headers };
|
|
1007
|
+
if (http.bodyMode === "json" && Object.keys(body).length > 0) {
|
|
1008
|
+
headers["content-type"] = "application/json";
|
|
1009
|
+
request.body = JSON.stringify(body);
|
|
1010
|
+
}
|
|
1011
|
+
return request;
|
|
1012
|
+
}
|
|
1013
|
+
function toApiError(status, rawBody) {
|
|
1014
|
+
try {
|
|
1015
|
+
const parsed = JSON.parse(rawBody);
|
|
1016
|
+
if (parsed?.error?.code && parsed.error.message) {
|
|
1017
|
+
return new KhotanApiError({
|
|
1018
|
+
status,
|
|
1019
|
+
code: parsed.error.code,
|
|
1020
|
+
message: parsed.error.message,
|
|
1021
|
+
details: parsed.error.details
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
} catch {}
|
|
1025
|
+
return new KhotanApiError({
|
|
1026
|
+
status,
|
|
1027
|
+
code: "http_error",
|
|
1028
|
+
message: rawBody.trim() || `Request failed with status ${status}.`
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
function createApiClient(context) {
|
|
1032
|
+
const doFetch = context.fetch ?? fetch;
|
|
1033
|
+
return {
|
|
1034
|
+
context,
|
|
1035
|
+
buildRequest(capabilityOrId, input = {}) {
|
|
1036
|
+
return buildHttpRequest(capabilityOrId, input, context);
|
|
1037
|
+
},
|
|
1038
|
+
async execute(capabilityOrId, input = {}) {
|
|
1039
|
+
const capability = resolveCapability(capabilityOrId);
|
|
1040
|
+
const request = buildHttpRequest(capability, input, context);
|
|
1041
|
+
let response;
|
|
1042
|
+
try {
|
|
1043
|
+
response = await doFetch(request.url, {
|
|
1044
|
+
method: request.method,
|
|
1045
|
+
headers: request.headers,
|
|
1046
|
+
body: request.body
|
|
1047
|
+
});
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
throw new KhotanClientError("network_error", `Could not reach the Khotan API at ${context.apiUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1050
|
+
}
|
|
1051
|
+
const rawBody = await response.text();
|
|
1052
|
+
if (!response.ok) {
|
|
1053
|
+
throw toApiError(response.status, rawBody);
|
|
1054
|
+
}
|
|
1055
|
+
if (capability.http.responseType === "markdown") {
|
|
1056
|
+
return rawBody;
|
|
1057
|
+
}
|
|
1058
|
+
if (!rawBody) {
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
return JSON.parse(rawBody);
|
|
1063
|
+
} catch {
|
|
1064
|
+
throw new KhotanClientError("invalid_response", `The Khotan API returned a non-JSON success response for ${capability.id}.`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
var init_api_client = __esm(() => {
|
|
1070
|
+
init_catalog();
|
|
1071
|
+
init_errors();
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
// ../khotan-core/src/profiles/profiles.ts
|
|
1075
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1076
|
+
import { homedir } from "node:os";
|
|
1077
|
+
import { dirname, join } from "node:path";
|
|
1078
|
+
function emptyStore() {
|
|
1079
|
+
return { version: 1, current: "default", profiles: {} };
|
|
1080
|
+
}
|
|
1081
|
+
function getProfileStorePath(options = {}) {
|
|
1082
|
+
if (options.storePath) {
|
|
1083
|
+
return options.storePath;
|
|
1084
|
+
}
|
|
1085
|
+
const env = options.env ?? process.env;
|
|
1086
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
1087
|
+
const base = xdg && xdg.trim() ? xdg : join(homedir(), ".config");
|
|
1088
|
+
return join(base, "khotan", "profiles.json");
|
|
1089
|
+
}
|
|
1090
|
+
function loadProfileStore(options = {}) {
|
|
1091
|
+
const path = getProfileStorePath(options);
|
|
1092
|
+
let raw;
|
|
1093
|
+
try {
|
|
1094
|
+
raw = readFileSync(path, "utf8");
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
if (error.code === "ENOENT") {
|
|
1097
|
+
return emptyStore();
|
|
1098
|
+
}
|
|
1099
|
+
throw new KhotanClientError("profile_read_failed", `Could not read Khotan profiles at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
const parsed = JSON.parse(raw);
|
|
1103
|
+
return {
|
|
1104
|
+
version: 1,
|
|
1105
|
+
current: parsed.current ?? "default",
|
|
1106
|
+
profiles: parsed.profiles ?? {}
|
|
1107
|
+
};
|
|
1108
|
+
} catch {
|
|
1109
|
+
throw new KhotanClientError("profile_parse_failed", `Khotan profiles at ${path} are not valid JSON.`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function saveProfileStore(store, options = {}) {
|
|
1113
|
+
const path = getProfileStorePath(options);
|
|
1114
|
+
mkdirSync(dirname(path), { recursive: true, mode: DIR_MODE });
|
|
1115
|
+
writeFileSync(path, `${JSON.stringify(store, null, 2)}
|
|
1116
|
+
`, { mode: FILE_MODE });
|
|
1117
|
+
try {
|
|
1118
|
+
chmodSync(path, FILE_MODE);
|
|
1119
|
+
} catch {}
|
|
1120
|
+
}
|
|
1121
|
+
function setProfile(name, profile, options = {}) {
|
|
1122
|
+
const store = loadProfileStore(options);
|
|
1123
|
+
store.profiles[name] = profile;
|
|
1124
|
+
if (!store.profiles[store.current]) {
|
|
1125
|
+
store.current = name;
|
|
1126
|
+
}
|
|
1127
|
+
saveProfileStore(store, options);
|
|
1128
|
+
return store;
|
|
1129
|
+
}
|
|
1130
|
+
function selectProfile(name, options = {}) {
|
|
1131
|
+
const store = loadProfileStore(options);
|
|
1132
|
+
if (!store.profiles[name]) {
|
|
1133
|
+
throw new KhotanClientError("unknown_profile", `No Khotan profile named "${name}". Run \`khotan auth set-key\` to create one.`);
|
|
1134
|
+
}
|
|
1135
|
+
store.current = name;
|
|
1136
|
+
saveProfileStore(store, options);
|
|
1137
|
+
return store;
|
|
1138
|
+
}
|
|
1139
|
+
function resolveProfile(options = {}) {
|
|
1140
|
+
const env = options.env ?? process.env;
|
|
1141
|
+
const store = loadProfileStore(options);
|
|
1142
|
+
const name = options.profileName ?? env[ENV_PROFILE] ?? store.current;
|
|
1143
|
+
const stored = store.profiles[name];
|
|
1144
|
+
const envApiUrl = env[ENV_API_URL]?.trim() || undefined;
|
|
1145
|
+
const envApiKey = env[ENV_API_KEY]?.trim() || undefined;
|
|
1146
|
+
const apiUrl = envApiUrl ?? stored?.apiUrl;
|
|
1147
|
+
const apiKey = envApiKey ?? stored?.apiKey;
|
|
1148
|
+
return {
|
|
1149
|
+
name,
|
|
1150
|
+
apiUrl,
|
|
1151
|
+
apiKey,
|
|
1152
|
+
defaultOutput: stored?.defaultOutput ?? "human",
|
|
1153
|
+
source: {
|
|
1154
|
+
apiUrl: envApiUrl ? "env" : stored?.apiUrl ? "profile" : "none",
|
|
1155
|
+
apiKey: envApiKey ? "env" : stored?.apiKey ? "profile" : "none"
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
var ENV_API_KEY = "KHOTAN_API_KEY", ENV_API_URL = "KHOTAN_API_URL", ENV_PROFILE = "KHOTAN_PROFILE", FILE_MODE = 384, DIR_MODE = 448;
|
|
1160
|
+
var init_profiles = __esm(() => {
|
|
1161
|
+
init_errors();
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
// ../khotan-core/src/index.ts
|
|
1165
|
+
var exports_src = {};
|
|
1166
|
+
__export(exports_src, {
|
|
1167
|
+
validateCatalogStructure: () => validateCatalogStructure,
|
|
1168
|
+
validateCatalogAgainstOpenApi: () => validateCatalogAgainstOpenApi,
|
|
1169
|
+
setProfile: () => setProfile,
|
|
1170
|
+
selectProfile: () => selectProfile,
|
|
1171
|
+
saveProfileStore: () => saveProfileStore,
|
|
1172
|
+
resolveProfile: () => resolveProfile,
|
|
1173
|
+
requiresConfirmation: () => requiresConfirmation,
|
|
1174
|
+
loadProfileStore: () => loadProfileStore,
|
|
1175
|
+
listDomains: () => listDomains,
|
|
1176
|
+
listCapabilities: () => listCapabilities,
|
|
1177
|
+
khotanCatalog: () => khotanCatalog,
|
|
1178
|
+
isSecret: () => isSecret,
|
|
1179
|
+
isResourceEligible: () => isResourceEligible,
|
|
1180
|
+
isResourceCapability: () => isResourceCapability,
|
|
1181
|
+
isReadOnly: () => isReadOnly,
|
|
1182
|
+
isOperationCapability: () => isOperationCapability,
|
|
1183
|
+
isDestructive: () => isDestructive,
|
|
1184
|
+
indexOpenApiOperations: () => indexOpenApiOperations,
|
|
1185
|
+
getProfileStorePath: () => getProfileStorePath,
|
|
1186
|
+
getCapability: () => getCapability,
|
|
1187
|
+
createApiClient: () => createApiClient,
|
|
1188
|
+
capabilitiesByOperationId: () => capabilitiesByOperationId,
|
|
1189
|
+
buildHttpRequest: () => buildHttpRequest,
|
|
1190
|
+
assertSupportedCatalogSchemaVersion: () => assertSupportedCatalogSchemaVersion,
|
|
1191
|
+
SUPPORTED_CATALOG_SCHEMA_VERSIONS: () => SUPPORTED_CATALOG_SCHEMA_VERSIONS,
|
|
1192
|
+
SAFETY_LEVELS: () => SAFETY_LEVELS,
|
|
1193
|
+
KhotanUnknownCapabilityError: () => KhotanUnknownCapabilityError,
|
|
1194
|
+
KhotanError: () => KhotanError,
|
|
1195
|
+
KhotanConfirmationRequiredError: () => KhotanConfirmationRequiredError,
|
|
1196
|
+
KhotanClientError: () => KhotanClientError,
|
|
1197
|
+
KhotanCatalogVersionError: () => KhotanCatalogVersionError,
|
|
1198
|
+
KhotanApiError: () => KhotanApiError,
|
|
1199
|
+
KHOTAN_ADAPTER_VERSION: () => KHOTAN_ADAPTER_VERSION,
|
|
1200
|
+
KHOTAN_ADAPTER_NAME: () => KHOTAN_ADAPTER_NAME,
|
|
1201
|
+
ENV_PROFILE: () => ENV_PROFILE,
|
|
1202
|
+
ENV_API_URL: () => ENV_API_URL,
|
|
1203
|
+
ENV_API_KEY: () => ENV_API_KEY,
|
|
1204
|
+
CATALOG_VERSION: () => CATALOG_VERSION,
|
|
1205
|
+
CATALOG_SCHEMA_VERSION: () => CATALOG_SCHEMA_VERSION
|
|
1206
|
+
});
|
|
1207
|
+
var init_src = __esm(() => {
|
|
1208
|
+
init_version();
|
|
1209
|
+
init_errors();
|
|
1210
|
+
init_safety();
|
|
1211
|
+
init_catalog();
|
|
1212
|
+
init_validate();
|
|
1213
|
+
init_api_client();
|
|
1214
|
+
init_profiles();
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// src/cli/io.ts
|
|
1218
|
+
import { createInterface } from "node:readline";
|
|
1219
|
+
function createNodeIo() {
|
|
1220
|
+
return {
|
|
1221
|
+
out(text) {
|
|
1222
|
+
process.stdout.write(text);
|
|
1223
|
+
},
|
|
1224
|
+
err(text) {
|
|
1225
|
+
process.stderr.write(text);
|
|
1226
|
+
},
|
|
1227
|
+
isTty: Boolean(process.stdout.isTTY && process.stdin.isTTY),
|
|
1228
|
+
env: process.env,
|
|
1229
|
+
async prompt(question) {
|
|
1230
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1231
|
+
try {
|
|
1232
|
+
return await new Promise((resolve) => {
|
|
1233
|
+
rl.question(question, (answer) => resolve(answer));
|
|
1234
|
+
});
|
|
1235
|
+
} finally {
|
|
1236
|
+
rl.close();
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
function outLine(io, line = "") {
|
|
1242
|
+
io.out(`${line}
|
|
1243
|
+
`);
|
|
1244
|
+
}
|
|
1245
|
+
function errLine(io, line = "") {
|
|
1246
|
+
io.err(`${line}
|
|
1247
|
+
`);
|
|
1248
|
+
}
|
|
1249
|
+
var init_io = () => {};
|
|
1250
|
+
|
|
1251
|
+
// src/cli/session.ts
|
|
1252
|
+
function resolveSession(options) {
|
|
1253
|
+
const resolved = resolveProfile({
|
|
1254
|
+
env: options.env,
|
|
1255
|
+
profileName: options.profileName,
|
|
1256
|
+
...options.storeOptions
|
|
1257
|
+
});
|
|
1258
|
+
const apiUrl = options.apiUrlOverride ?? resolved.apiUrl;
|
|
1259
|
+
const apiKey = options.apiKeyOverride ?? resolved.apiKey;
|
|
1260
|
+
if (!apiUrl) {
|
|
1261
|
+
throw new KhotanClientError("no_api_url", "No Khotan API URL configured. Set one with `khotan auth set-key --api-url <url> --api-key <key>`, or export KHOTAN_API_URL.");
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
client: createApiClient({ apiUrl, apiKey, fetch: options.fetch }),
|
|
1265
|
+
resolved,
|
|
1266
|
+
apiUrl,
|
|
1267
|
+
apiKey
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
var init_session = __esm(() => {
|
|
1271
|
+
init_src();
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
// src/mcp/protocol.ts
|
|
1275
|
+
function fieldSchema(field) {
|
|
1276
|
+
const base = field.description ? { description: field.description } : {};
|
|
1277
|
+
switch (field.type) {
|
|
1278
|
+
case "number":
|
|
1279
|
+
return { ...base, type: "number" };
|
|
1280
|
+
case "boolean":
|
|
1281
|
+
return { ...base, type: "boolean" };
|
|
1282
|
+
case "string[]":
|
|
1283
|
+
return { ...base, type: "array", items: { type: "string" } };
|
|
1284
|
+
case "json":
|
|
1285
|
+
return { ...base, type: "array" };
|
|
1286
|
+
default:
|
|
1287
|
+
return { ...base, type: "string" };
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function toolInputSchema(capability) {
|
|
1291
|
+
const properties = {};
|
|
1292
|
+
const required = [];
|
|
1293
|
+
for (const field of capability.input) {
|
|
1294
|
+
properties[field.name] = fieldSchema(field);
|
|
1295
|
+
if (field.required) {
|
|
1296
|
+
required.push(field.name);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (isDestructive(capability.safety)) {
|
|
1300
|
+
properties.confirm = {
|
|
1301
|
+
type: "boolean",
|
|
1302
|
+
description: "Must be true to perform this destructive action."
|
|
1303
|
+
};
|
|
1304
|
+
required.push("confirm");
|
|
1305
|
+
}
|
|
1306
|
+
return { type: "object", properties, required };
|
|
1307
|
+
}
|
|
1308
|
+
function describeForAgent(capability) {
|
|
1309
|
+
const prefix = isSecret(capability.safety) ? "[secret] " : isDestructive(capability.safety) ? "[destructive] " : "";
|
|
1310
|
+
return `${prefix}${capability.mcp.tool.description}`;
|
|
1311
|
+
}
|
|
1312
|
+
function listMcpTools() {
|
|
1313
|
+
const tools = [];
|
|
1314
|
+
for (const capability of listCapabilities()) {
|
|
1315
|
+
if (!isOperationCapability(capability) || TOOL_DENYLIST.has(capability.id)) {
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
tools.push({
|
|
1319
|
+
name: capability.mcp.tool.name,
|
|
1320
|
+
title: capability.mcp.tool.title,
|
|
1321
|
+
description: describeForAgent(capability),
|
|
1322
|
+
inputSchema: toolInputSchema(capability)
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
return tools;
|
|
1326
|
+
}
|
|
1327
|
+
function findToolCapability(name) {
|
|
1328
|
+
for (const capability of listCapabilities()) {
|
|
1329
|
+
if (isOperationCapability(capability) && !TOOL_DENYLIST.has(capability.id) && capability.mcp.tool.name === name) {
|
|
1330
|
+
return capability;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
function listMcpResourceTemplates() {
|
|
1336
|
+
return listCapabilities().filter(isResourceCapability).map((capability) => ({
|
|
1337
|
+
uriTemplate: capability.mcp.resource.uriTemplate,
|
|
1338
|
+
name: capability.mcp.resource.name,
|
|
1339
|
+
title: capability.mcp.resource.title,
|
|
1340
|
+
description: capability.mcp.resource.description,
|
|
1341
|
+
mimeType: capability.mcp.resource.mimeType
|
|
1342
|
+
}));
|
|
1343
|
+
}
|
|
1344
|
+
function matchResourceUri(uri) {
|
|
1345
|
+
for (const capability of listCapabilities()) {
|
|
1346
|
+
if (!isResourceCapability(capability)) {
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
const template = capability.mcp.resource.uriTemplate;
|
|
1350
|
+
const paramNames = [];
|
|
1351
|
+
let source = "^";
|
|
1352
|
+
for (const segment of template.match(/\{[^}]+\}|[^{]+/g) ?? []) {
|
|
1353
|
+
if (segment.startsWith("{")) {
|
|
1354
|
+
paramNames.push(segment.slice(1, -1));
|
|
1355
|
+
source += "([^/]+)";
|
|
1356
|
+
} else {
|
|
1357
|
+
source += segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
source += "$";
|
|
1361
|
+
const exec = new RegExp(source).exec(uri);
|
|
1362
|
+
if (exec) {
|
|
1363
|
+
const input = {};
|
|
1364
|
+
paramNames.forEach((name, index) => {
|
|
1365
|
+
input[name] = decodeURIComponent(exec[index + 1]);
|
|
1366
|
+
});
|
|
1367
|
+
return { capability, input };
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
var MCP_PROTOCOL_VERSION = "2025-06-18", TOOL_DENYLIST;
|
|
1373
|
+
var init_protocol = __esm(() => {
|
|
1374
|
+
init_src();
|
|
1375
|
+
TOOL_DENYLIST = new Set(["api-keys.create-from-credentials"]);
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
// src/mcp/server.ts
|
|
1379
|
+
function ok(id, result) {
|
|
1380
|
+
return { jsonrpc: "2.0", id: id ?? null, result };
|
|
1381
|
+
}
|
|
1382
|
+
function fail(id, code, message, data) {
|
|
1383
|
+
return { jsonrpc: "2.0", id: id ?? null, error: { code, message, data } };
|
|
1384
|
+
}
|
|
1385
|
+
function textContent(value) {
|
|
1386
|
+
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
1387
|
+
return { content: [{ type: "text", text }] };
|
|
1388
|
+
}
|
|
1389
|
+
function toolError(message) {
|
|
1390
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
1391
|
+
}
|
|
1392
|
+
async function callTool(deps, params) {
|
|
1393
|
+
const name = params.name;
|
|
1394
|
+
if (typeof name !== "string") {
|
|
1395
|
+
return toolError("tools/call requires a string `name`.");
|
|
1396
|
+
}
|
|
1397
|
+
const capability = findToolCapability(name);
|
|
1398
|
+
if (!capability) {
|
|
1399
|
+
return toolError(`Unknown tool "${name}".`);
|
|
1400
|
+
}
|
|
1401
|
+
const args = { ...params.arguments ?? {} };
|
|
1402
|
+
if (isDestructive(capability.safety)) {
|
|
1403
|
+
if (args.confirm !== true) {
|
|
1404
|
+
return toolError(`Tool "${name}" is destructive and requires { "confirm": true } to proceed.`);
|
|
1405
|
+
}
|
|
1406
|
+
delete args.confirm;
|
|
1407
|
+
}
|
|
1408
|
+
try {
|
|
1409
|
+
const result = await deps.client.execute(capability, args);
|
|
1410
|
+
const content = textContent(result);
|
|
1411
|
+
return {
|
|
1412
|
+
...content,
|
|
1413
|
+
structuredContent: typeof result === "object" && result !== null ? result : { value: result }
|
|
1414
|
+
};
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
if (error instanceof KhotanApiError) {
|
|
1417
|
+
return toolError(`[${error.code}] ${error.message}`);
|
|
1418
|
+
}
|
|
1419
|
+
return toolError(error instanceof Error ? error.message : String(error));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
function isResourceError(value) {
|
|
1423
|
+
return typeof value === "object" && value !== null && "__error" in value;
|
|
1424
|
+
}
|
|
1425
|
+
async function readResource(deps, params) {
|
|
1426
|
+
const uri = params.uri;
|
|
1427
|
+
if (typeof uri !== "string") {
|
|
1428
|
+
return { __error: "resources/read requires a string `uri`.", code: JSON_RPC_INVALID_PARAMS };
|
|
1429
|
+
}
|
|
1430
|
+
const match = matchResourceUri(uri);
|
|
1431
|
+
if (!match) {
|
|
1432
|
+
return { __error: `No Khotan resource matches "${uri}".`, code: JSON_RPC_INVALID_PARAMS };
|
|
1433
|
+
}
|
|
1434
|
+
if (isSecret(match.capability.safety)) {
|
|
1435
|
+
return { __error: "Secret-bearing data is not exposed as a resource.", code: JSON_RPC_INVALID_PARAMS };
|
|
1436
|
+
}
|
|
1437
|
+
let value;
|
|
1438
|
+
try {
|
|
1439
|
+
value = await deps.client.execute(match.capability, match.input);
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
const message = error instanceof KhotanApiError ? `[${error.code}] ${error.message}` : error instanceof Error ? error.message : String(error);
|
|
1442
|
+
return { __error: message, code: JSON_RPC_INTERNAL_ERROR };
|
|
1443
|
+
}
|
|
1444
|
+
const mimeType = match.capability.mcp.resource.mimeType ?? "application/json";
|
|
1445
|
+
const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
1446
|
+
return { contents: [{ uri, mimeType, text }] };
|
|
1447
|
+
}
|
|
1448
|
+
async function handleRpc(message, deps) {
|
|
1449
|
+
const isNotification = message.id === undefined || message.id === null;
|
|
1450
|
+
const params = message.params ?? {};
|
|
1451
|
+
switch (message.method) {
|
|
1452
|
+
case "initialize":
|
|
1453
|
+
return ok(message.id, {
|
|
1454
|
+
protocolVersion: typeof params.protocolVersion === "string" ? params.protocolVersion : MCP_PROTOCOL_VERSION,
|
|
1455
|
+
capabilities: { tools: {}, resources: {} },
|
|
1456
|
+
serverInfo: { name: KHOTAN_ADAPTER_NAME, version: KHOTAN_ADAPTER_VERSION }
|
|
1457
|
+
});
|
|
1458
|
+
case "notifications/initialized":
|
|
1459
|
+
return null;
|
|
1460
|
+
case "ping":
|
|
1461
|
+
return ok(message.id, {});
|
|
1462
|
+
case "tools/list":
|
|
1463
|
+
return ok(message.id, { tools: listMcpTools() });
|
|
1464
|
+
case "tools/call":
|
|
1465
|
+
return ok(message.id, await callTool(deps, params));
|
|
1466
|
+
case "resources/list":
|
|
1467
|
+
return ok(message.id, { resources: [] });
|
|
1468
|
+
case "resources/templates/list":
|
|
1469
|
+
return ok(message.id, { resourceTemplates: listMcpResourceTemplates() });
|
|
1470
|
+
case "resources/read": {
|
|
1471
|
+
const result = await readResource(deps, params);
|
|
1472
|
+
if (isResourceError(result)) {
|
|
1473
|
+
return fail(message.id, result.code, result.__error);
|
|
1474
|
+
}
|
|
1475
|
+
return ok(message.id, result);
|
|
1476
|
+
}
|
|
1477
|
+
default:
|
|
1478
|
+
if (isNotification) {
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
return fail(message.id, JSON_RPC_METHOD_NOT_FOUND, `Unknown method "${message.method}".`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
var JSON_RPC_METHOD_NOT_FOUND = -32601, JSON_RPC_INVALID_PARAMS = -32602, JSON_RPC_INTERNAL_ERROR = -32603;
|
|
1485
|
+
var init_server = __esm(() => {
|
|
1486
|
+
init_src();
|
|
1487
|
+
init_protocol();
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
// src/mcp/transport.ts
|
|
1491
|
+
async function dispatchLine(line, deps) {
|
|
1492
|
+
const trimmed = line.trim();
|
|
1493
|
+
if (!trimmed) {
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
let message;
|
|
1497
|
+
try {
|
|
1498
|
+
message = JSON.parse(trimmed);
|
|
1499
|
+
} catch {
|
|
1500
|
+
return JSON.stringify({
|
|
1501
|
+
jsonrpc: "2.0",
|
|
1502
|
+
id: null,
|
|
1503
|
+
error: { code: JSON_RPC_PARSE_ERROR, message: "Parse error: invalid JSON." }
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
const response = await handleRpc(message, deps);
|
|
1507
|
+
return response ? JSON.stringify(response) : null;
|
|
1508
|
+
}
|
|
1509
|
+
var JSON_RPC_PARSE_ERROR = -32700;
|
|
1510
|
+
var init_transport = __esm(() => {
|
|
1511
|
+
init_server();
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// src/mcp/serve.ts
|
|
1515
|
+
var exports_serve = {};
|
|
1516
|
+
__export(exports_serve, {
|
|
1517
|
+
serveMcp: () => serveMcp
|
|
1518
|
+
});
|
|
1519
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
1520
|
+
async function serveMcp(deps) {
|
|
1521
|
+
assertSupportedCatalogSchemaVersion();
|
|
1522
|
+
const session = resolveSession({
|
|
1523
|
+
env: deps.env,
|
|
1524
|
+
storeOptions: deps.storeOptions,
|
|
1525
|
+
profileName: deps.profileName,
|
|
1526
|
+
apiUrlOverride: deps.apiUrlOverride,
|
|
1527
|
+
apiKeyOverride: deps.apiKeyOverride,
|
|
1528
|
+
fetch: deps.fetch
|
|
1529
|
+
});
|
|
1530
|
+
if (!session.apiKey) {
|
|
1531
|
+
throw new KhotanClientError("not_authenticated", "The MCP server needs an API key. Run `khotan auth set-key` or set KHOTAN_API_KEY.");
|
|
1532
|
+
}
|
|
1533
|
+
const serverDeps = { client: session.client };
|
|
1534
|
+
errLine(deps.io, `khotan MCP server ready on stdio (origin: ${session.apiUrl}).`);
|
|
1535
|
+
const input = deps.input ?? process.stdin;
|
|
1536
|
+
const rl = createInterface2({ input });
|
|
1537
|
+
for await (const line of rl) {
|
|
1538
|
+
let response;
|
|
1539
|
+
try {
|
|
1540
|
+
response = await dispatchLine(line, serverDeps);
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
errLine(deps.io, `MCP handler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (response !== null) {
|
|
1546
|
+
deps.io.out(`${response}
|
|
1547
|
+
`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return 0;
|
|
1551
|
+
}
|
|
1552
|
+
var init_serve = __esm(() => {
|
|
1553
|
+
init_src();
|
|
1554
|
+
init_io();
|
|
1555
|
+
init_session();
|
|
1556
|
+
init_transport();
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
// src/cli/run.ts
|
|
1560
|
+
init_src();
|
|
1561
|
+
|
|
1562
|
+
// src/cli/args.ts
|
|
1563
|
+
var KNOWN_BOOLEANS = new Set([
|
|
1564
|
+
"json",
|
|
1565
|
+
"yes",
|
|
1566
|
+
"help",
|
|
1567
|
+
"h",
|
|
1568
|
+
"version",
|
|
1569
|
+
"no-input",
|
|
1570
|
+
"force"
|
|
1571
|
+
]);
|
|
1572
|
+
function parseArgs(argv) {
|
|
1573
|
+
const positionals = [];
|
|
1574
|
+
const flags = new Map;
|
|
1575
|
+
const addFlag = (name, value) => {
|
|
1576
|
+
const existing = flags.get(name);
|
|
1577
|
+
if (existing) {
|
|
1578
|
+
existing.push(value);
|
|
1579
|
+
} else {
|
|
1580
|
+
flags.set(name, [value]);
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
for (let i = 0;i < argv.length; i++) {
|
|
1584
|
+
const token = argv[i];
|
|
1585
|
+
if (token === "--") {
|
|
1586
|
+
positionals.push(...argv.slice(i + 1));
|
|
1587
|
+
break;
|
|
1588
|
+
}
|
|
1589
|
+
if (token.startsWith("--")) {
|
|
1590
|
+
const body = token.slice(2);
|
|
1591
|
+
const eq = body.indexOf("=");
|
|
1592
|
+
if (eq >= 0) {
|
|
1593
|
+
addFlag(body.slice(0, eq), body.slice(eq + 1));
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
if (KNOWN_BOOLEANS.has(body)) {
|
|
1597
|
+
addFlag(body, "true");
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
const next = argv[i + 1];
|
|
1601
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
1602
|
+
addFlag(body, next);
|
|
1603
|
+
i++;
|
|
1604
|
+
} else {
|
|
1605
|
+
addFlag(body, "true");
|
|
1606
|
+
}
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
1610
|
+
const body = token.slice(1);
|
|
1611
|
+
addFlag(body === "h" ? "help" : body, "true");
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
positionals.push(token);
|
|
1615
|
+
}
|
|
1616
|
+
return { positionals, flags };
|
|
1617
|
+
}
|
|
1618
|
+
function flagValue(args, name) {
|
|
1619
|
+
return args.flags.get(name)?.[0];
|
|
1620
|
+
}
|
|
1621
|
+
function flagValues(args, name) {
|
|
1622
|
+
return args.flags.get(name) ?? [];
|
|
1623
|
+
}
|
|
1624
|
+
function flagBool(args, name) {
|
|
1625
|
+
const values = args.flags.get(name);
|
|
1626
|
+
if (!values || values.length === 0) {
|
|
1627
|
+
return false;
|
|
1628
|
+
}
|
|
1629
|
+
const last = values[values.length - 1];
|
|
1630
|
+
return last !== "false" && last !== "0";
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// src/cli/commands/auth.ts
|
|
1634
|
+
init_src();
|
|
1635
|
+
init_io();
|
|
1636
|
+
|
|
1637
|
+
// src/cli/render.ts
|
|
1638
|
+
init_io();
|
|
1639
|
+
function isScalar(value) {
|
|
1640
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
1641
|
+
}
|
|
1642
|
+
function scalar(value) {
|
|
1643
|
+
if (value === null || value === undefined) {
|
|
1644
|
+
return "";
|
|
1645
|
+
}
|
|
1646
|
+
if (isScalar(value)) {
|
|
1647
|
+
return String(value);
|
|
1648
|
+
}
|
|
1649
|
+
return JSON.stringify(value);
|
|
1650
|
+
}
|
|
1651
|
+
function renderTable(io, rows) {
|
|
1652
|
+
if (rows.length === 0) {
|
|
1653
|
+
outLine(io, "(none)");
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
const objectRows = rows.filter((row) => typeof row === "object" && row !== null && !Array.isArray(row));
|
|
1657
|
+
if (objectRows.length !== rows.length) {
|
|
1658
|
+
for (const row of rows) {
|
|
1659
|
+
outLine(io, scalar(row));
|
|
1660
|
+
}
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const columns = [];
|
|
1664
|
+
for (const row of objectRows) {
|
|
1665
|
+
for (const key of Object.keys(row)) {
|
|
1666
|
+
if (!columns.includes(key) && isScalar(row[key])) {
|
|
1667
|
+
columns.push(key);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
const widths = columns.map((column) => Math.max(column.length, ...objectRows.map((row) => scalar(row[column]).length)));
|
|
1672
|
+
const formatRow = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
|
|
1673
|
+
outLine(io, formatRow(columns));
|
|
1674
|
+
outLine(io, formatRow(widths.map((width) => "-".repeat(width))));
|
|
1675
|
+
for (const row of objectRows) {
|
|
1676
|
+
outLine(io, formatRow(columns.map((column) => scalar(row[column]))));
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
function renderDetail(io, record) {
|
|
1680
|
+
const keys = Object.keys(record);
|
|
1681
|
+
if (keys.length === 0) {
|
|
1682
|
+
outLine(io, "(empty)");
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const width = Math.max(...keys.map((key) => key.length));
|
|
1686
|
+
for (const key of keys) {
|
|
1687
|
+
const value = record[key];
|
|
1688
|
+
const rendered = isScalar(value) || value === null || value === undefined ? scalar(value) : JSON.stringify(value, null, 2);
|
|
1689
|
+
outLine(io, `${key.padEnd(width)} ${rendered}`);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
function singleArrayProperty(record) {
|
|
1693
|
+
const arrayKeys = Object.keys(record).filter((key) => Array.isArray(record[key]));
|
|
1694
|
+
if (arrayKeys.length === 1) {
|
|
1695
|
+
return record[arrayKeys[0]];
|
|
1696
|
+
}
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
function renderResult(io, result, options) {
|
|
1700
|
+
if (options.json) {
|
|
1701
|
+
outLine(io, JSON.stringify(result, null, 2));
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (typeof result === "string") {
|
|
1705
|
+
io.out(result.endsWith(`
|
|
1706
|
+
`) ? result : `${result}
|
|
1707
|
+
`);
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
if (result === null || result === undefined) {
|
|
1711
|
+
outLine(io, "(no content)");
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
if (Array.isArray(result)) {
|
|
1715
|
+
renderTable(io, result);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
if (typeof result === "object") {
|
|
1719
|
+
const record = result;
|
|
1720
|
+
const list = singleArrayProperty(record);
|
|
1721
|
+
if (list) {
|
|
1722
|
+
renderTable(io, list);
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
renderDetail(io, record);
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
outLine(io, scalar(result));
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/cli/commands/auth.ts
|
|
1732
|
+
async function promptIfMissing(io, value, question) {
|
|
1733
|
+
if (value !== undefined) {
|
|
1734
|
+
return value;
|
|
1735
|
+
}
|
|
1736
|
+
if (!io.isTty) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
const answer = (await io.prompt(question)).trim();
|
|
1740
|
+
return answer.length > 0 ? answer : undefined;
|
|
1741
|
+
}
|
|
1742
|
+
async function authSetKey(ctx) {
|
|
1743
|
+
const { io, args, env, storeOptions, json } = ctx;
|
|
1744
|
+
const profileName = flagValue(args, "profile") ?? "default";
|
|
1745
|
+
const apiUrl = flagValue(args, "api-url") ?? env.KHOTAN_API_URL ?? await promptIfMissing(io, undefined, "Khotan API URL: ");
|
|
1746
|
+
if (!apiUrl) {
|
|
1747
|
+
throw new KhotanClientError("missing_argument", "Provide --api-url (the Khotan API origin).");
|
|
1748
|
+
}
|
|
1749
|
+
const apiKey = flagValue(args, "api-key") ?? env.KHOTAN_API_KEY ?? await promptIfMissing(io, undefined, "Khotan API key: ");
|
|
1750
|
+
if (!apiKey) {
|
|
1751
|
+
throw new KhotanClientError("missing_argument", "Provide --api-key (an organization API key).");
|
|
1752
|
+
}
|
|
1753
|
+
const client = createApiClient({ apiUrl, apiKey, fetch: ctx.fetch });
|
|
1754
|
+
const principal = await client.execute("identity.whoami");
|
|
1755
|
+
setProfile(profileName, { apiUrl, apiKey, defaultOutput: json ? "json" : "human" }, storeOptions);
|
|
1756
|
+
selectProfile(profileName, storeOptions);
|
|
1757
|
+
errLine(io, `Saved API key to profile "${profileName}" and verified it.`);
|
|
1758
|
+
renderResult(io, principal, { json });
|
|
1759
|
+
return 0;
|
|
1760
|
+
}
|
|
1761
|
+
async function authLogin(ctx) {
|
|
1762
|
+
const { io, args, env, storeOptions, json } = ctx;
|
|
1763
|
+
const profileName = flagValue(args, "profile") ?? "default";
|
|
1764
|
+
const apiUrl = flagValue(args, "api-url") ?? env.KHOTAN_API_URL;
|
|
1765
|
+
if (!apiUrl) {
|
|
1766
|
+
throw new KhotanClientError("missing_argument", "Provide --api-url (the Khotan API origin).");
|
|
1767
|
+
}
|
|
1768
|
+
const email = await promptIfMissing(io, flagValue(args, "email"), "Email: ");
|
|
1769
|
+
const password = await promptIfMissing(io, flagValue(args, "password"), "Password: ");
|
|
1770
|
+
const organizationId = await promptIfMissing(io, flagValue(args, "organization-id"), "Organization id: ");
|
|
1771
|
+
if (!email || !password || !organizationId) {
|
|
1772
|
+
throw new KhotanClientError("missing_argument", "login requires --email, --password, and --organization-id (or an interactive terminal to prompt).");
|
|
1773
|
+
}
|
|
1774
|
+
const expiresInRaw = flagValue(args, "expires-in-days");
|
|
1775
|
+
const input = { email, password, organizationId };
|
|
1776
|
+
const name = flagValue(args, "name");
|
|
1777
|
+
const role = flagValue(args, "role");
|
|
1778
|
+
if (name)
|
|
1779
|
+
input.name = name;
|
|
1780
|
+
if (role)
|
|
1781
|
+
input.role = role;
|
|
1782
|
+
if (expiresInRaw)
|
|
1783
|
+
input.expiresInDays = Number(expiresInRaw);
|
|
1784
|
+
const bootstrapClient = createApiClient({ apiUrl, fetch: ctx.fetch });
|
|
1785
|
+
const minted = await bootstrapClient.execute("api-keys.create-from-credentials", input);
|
|
1786
|
+
setProfile(profileName, { apiUrl, apiKey: minted.key, defaultOutput: json ? "json" : "human" }, storeOptions);
|
|
1787
|
+
selectProfile(profileName, storeOptions);
|
|
1788
|
+
const client = createApiClient({ apiUrl, apiKey: minted.key, fetch: ctx.fetch });
|
|
1789
|
+
const principal = await client.execute("identity.whoami");
|
|
1790
|
+
errLine(io, `Minted an API key (id ${minted.id}) and saved it to profile "${profileName}".`);
|
|
1791
|
+
renderResult(io, principal, { json });
|
|
1792
|
+
return 0;
|
|
1793
|
+
}
|
|
1794
|
+
function authUse(ctx, profileName) {
|
|
1795
|
+
selectProfile(profileName, ctx.storeOptions);
|
|
1796
|
+
errLine(ctx.io, `Active profile is now "${profileName}".`);
|
|
1797
|
+
return 0;
|
|
1798
|
+
}
|
|
1799
|
+
function authList(ctx) {
|
|
1800
|
+
const store = loadProfileStore(ctx.storeOptions);
|
|
1801
|
+
const rows = Object.entries(store.profiles).map(([name, profile]) => ({
|
|
1802
|
+
profile: name,
|
|
1803
|
+
current: name === store.current,
|
|
1804
|
+
apiUrl: profile.apiUrl,
|
|
1805
|
+
hasKey: Boolean(profile.apiKey)
|
|
1806
|
+
}));
|
|
1807
|
+
renderResult(ctx.io, { profiles: rows }, { json: ctx.json });
|
|
1808
|
+
return 0;
|
|
1809
|
+
}
|
|
1810
|
+
async function whoami(ctx) {
|
|
1811
|
+
const resolved = resolveProfile({
|
|
1812
|
+
env: ctx.env,
|
|
1813
|
+
profileName: flagValue(ctx.args, "profile"),
|
|
1814
|
+
...ctx.storeOptions
|
|
1815
|
+
});
|
|
1816
|
+
const apiUrl = flagValue(ctx.args, "api-url") ?? resolved.apiUrl;
|
|
1817
|
+
const apiKey = flagValue(ctx.args, "api-key") ?? resolved.apiKey;
|
|
1818
|
+
if (!apiUrl || !apiKey) {
|
|
1819
|
+
throw new KhotanClientError("not_authenticated", "No credentials configured. Run `khotan auth set-key` or set KHOTAN_API_URL and KHOTAN_API_KEY.");
|
|
1820
|
+
}
|
|
1821
|
+
const client = createApiClient({ apiUrl, apiKey, fetch: ctx.fetch });
|
|
1822
|
+
const principal = await client.execute("identity.whoami");
|
|
1823
|
+
renderResult(ctx.io, principal, { json: ctx.json });
|
|
1824
|
+
return 0;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/cli/commands/files-transfer.ts
|
|
1828
|
+
init_src();
|
|
1829
|
+
init_io();
|
|
1830
|
+
import { basename } from "node:path";
|
|
1831
|
+
async function defaultReadFile(path) {
|
|
1832
|
+
const { readFile } = await import("node:fs/promises");
|
|
1833
|
+
return readFile(path);
|
|
1834
|
+
}
|
|
1835
|
+
async function defaultWriteFile(path, data) {
|
|
1836
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1837
|
+
await writeFile(path, data);
|
|
1838
|
+
}
|
|
1839
|
+
async function uploadFile(deps, options) {
|
|
1840
|
+
const { session, io, json } = deps;
|
|
1841
|
+
const httpFetch = deps.httpFetch ?? fetch;
|
|
1842
|
+
const readFile = deps.readFile ?? defaultReadFile;
|
|
1843
|
+
const bytes = await readFile(options.localPath);
|
|
1844
|
+
const name = options.name ?? basename(options.localPath);
|
|
1845
|
+
const prepareInput = {
|
|
1846
|
+
files: [
|
|
1847
|
+
{
|
|
1848
|
+
name,
|
|
1849
|
+
size: bytes.byteLength,
|
|
1850
|
+
...options.contentType ? { type: options.contentType } : {}
|
|
1851
|
+
}
|
|
1852
|
+
]
|
|
1853
|
+
};
|
|
1854
|
+
if (options.folderId)
|
|
1855
|
+
prepareInput.folderId = options.folderId;
|
|
1856
|
+
if (options.folderPath)
|
|
1857
|
+
prepareInput.folderPath = options.folderPath;
|
|
1858
|
+
const prepared = await session.client.execute("files.uploads.prepare", prepareInput);
|
|
1859
|
+
const upload = prepared.uploads[0];
|
|
1860
|
+
if (!upload) {
|
|
1861
|
+
throw new KhotanClientError("upload_failed", "The API returned no presigned upload.");
|
|
1862
|
+
}
|
|
1863
|
+
errLine(io, `Uploading ${name} (${bytes.byteLength} bytes)…`);
|
|
1864
|
+
const putResponse = await httpFetch(upload.uploadUrl, {
|
|
1865
|
+
method: "PUT",
|
|
1866
|
+
headers: upload.headers,
|
|
1867
|
+
body: bytes
|
|
1868
|
+
});
|
|
1869
|
+
if (!putResponse.ok) {
|
|
1870
|
+
throw new KhotanClientError("upload_failed", `Object storage rejected the upload (status ${putResponse.status}).`);
|
|
1871
|
+
}
|
|
1872
|
+
const completed = await session.client.execute("files.uploads.complete", {
|
|
1873
|
+
files: [{ fileId: upload.fileId }]
|
|
1874
|
+
});
|
|
1875
|
+
const result = completed.results[0];
|
|
1876
|
+
if (result && result.status === "failed") {
|
|
1877
|
+
throw new KhotanClientError("upload_failed", result.error);
|
|
1878
|
+
}
|
|
1879
|
+
renderResult(io, result?.status === "ready" ? result.file : completed, { json });
|
|
1880
|
+
return 0;
|
|
1881
|
+
}
|
|
1882
|
+
async function downloadFile(deps, options) {
|
|
1883
|
+
const { session, io, json } = deps;
|
|
1884
|
+
const httpFetch = deps.httpFetch ?? fetch;
|
|
1885
|
+
const writeFile = deps.writeFile ?? defaultWriteFile;
|
|
1886
|
+
const signed = await session.client.execute("files.download", {
|
|
1887
|
+
fileId: options.fileId
|
|
1888
|
+
});
|
|
1889
|
+
const response = await httpFetch(signed.url, { method: "GET" });
|
|
1890
|
+
if (!response.ok) {
|
|
1891
|
+
throw new KhotanClientError("download_failed", `Object storage rejected the download (status ${response.status}).`);
|
|
1892
|
+
}
|
|
1893
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
1894
|
+
await writeFile(options.outputPath, data);
|
|
1895
|
+
errLine(io, `Wrote ${data.byteLength} bytes to ${options.outputPath}.`);
|
|
1896
|
+
renderResult(io, { fileId: options.fileId, output: options.outputPath, bytes: data.byteLength }, { json });
|
|
1897
|
+
return 0;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/cli/commands/init.ts
|
|
1901
|
+
init_io();
|
|
1902
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1903
|
+
import { dirname as dirname2, join as join2, relative } from "node:path";
|
|
1904
|
+
var MCP_SERVER_KEY = "khotan";
|
|
1905
|
+
function mcpServerEntry() {
|
|
1906
|
+
return {
|
|
1907
|
+
command: "khotan",
|
|
1908
|
+
args: ["mcp", "serve"],
|
|
1909
|
+
env: {
|
|
1910
|
+
KHOTAN_API_URL: "<your-khotan-api-url>",
|
|
1911
|
+
KHOTAN_API_KEY: "<your-khotan-api-key>"
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
var GUIDE_BODY = `This workspace is wired to Khotan through the \`khotan\` CLI and its MCP server.
|
|
1916
|
+
Use them to inspect and configure apps, pipelines, databases, files, folders, and
|
|
1917
|
+
context documents through the Khotan \`/api/v1\` surface.
|
|
1918
|
+
|
|
1919
|
+
## Authentication (do this once — never commit secrets)
|
|
1920
|
+
|
|
1921
|
+
Khotan reads an organization-scoped API key from a stored profile or the
|
|
1922
|
+
environment:
|
|
1923
|
+
|
|
1924
|
+
- Profile (recommended): \`khotan auth set-key --api-url <url> --api-key <key>\`
|
|
1925
|
+
stores the key in an owner-only file outside the repository.
|
|
1926
|
+
- Environment: export \`KHOTAN_API_URL\` and \`KHOTAN_API_KEY\` in your shell or
|
|
1927
|
+
agent sandbox.
|
|
1928
|
+
|
|
1929
|
+
Never put an API key in a file committed to the repository. The generated MCP
|
|
1930
|
+
config contains only placeholders.
|
|
1931
|
+
|
|
1932
|
+
## CLI surface
|
|
1933
|
+
|
|
1934
|
+
- \`khotan apps list|get|create|delete|redeploy\` and \`khotan apps env …\`
|
|
1935
|
+
- \`khotan pipelines …\`, \`khotan databases …\`
|
|
1936
|
+
- \`khotan files list|upload|download|…\`, \`khotan folders …\`
|
|
1937
|
+
- \`khotan context list|get|create|update|delete\`
|
|
1938
|
+
- Add \`--json\` to any command for machine-readable output.
|
|
1939
|
+
|
|
1940
|
+
Run \`khotan help\` or \`khotan <command> --help\` for the full, catalog-derived
|
|
1941
|
+
surface.
|
|
1942
|
+
|
|
1943
|
+
## Safety
|
|
1944
|
+
|
|
1945
|
+
- Destructive commands (deletes, env unset) require confirmation; pass \`--yes\`
|
|
1946
|
+
only when you intend it.
|
|
1947
|
+
- Secret values (env reveal, database connection strings, credential rotation)
|
|
1948
|
+
are only available through explicit commands and are never printed in lists.
|
|
1949
|
+
|
|
1950
|
+
## MCP
|
|
1951
|
+
|
|
1952
|
+
\`khotan mcp serve\` exposes the same operations as MCP tools and durable reads as
|
|
1953
|
+
MCP resources, reusing your stored profile or environment credentials.
|
|
1954
|
+
`;
|
|
1955
|
+
var CURSOR_RULES = `---
|
|
1956
|
+
description: Use the Khotan CLI and MCP tools to configure and operate this workspace's Khotan resources
|
|
1957
|
+
alwaysApply: false
|
|
1958
|
+
---
|
|
1959
|
+
|
|
1960
|
+
# Khotan
|
|
1961
|
+
|
|
1962
|
+
${GUIDE_BODY}`;
|
|
1963
|
+
var GENERIC_GUIDE = `# Khotan agent guide
|
|
1964
|
+
|
|
1965
|
+
${GUIDE_BODY}`;
|
|
1966
|
+
function toRelative(cwd, absolutePath) {
|
|
1967
|
+
return relative(cwd, absolutePath) || absolutePath;
|
|
1968
|
+
}
|
|
1969
|
+
function writeTextAsset(ctx, absolutePath, content) {
|
|
1970
|
+
const path = toRelative(ctx.cwd, absolutePath);
|
|
1971
|
+
const existed = existsSync(absolutePath);
|
|
1972
|
+
if (existed && !ctx.force) {
|
|
1973
|
+
ctx.results.push({ path, action: "skipped" });
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
mkdirSync2(dirname2(absolutePath), { recursive: true });
|
|
1977
|
+
writeFileSync2(absolutePath, content);
|
|
1978
|
+
ctx.results.push({ path, action: existed ? "forced" : "created" });
|
|
1979
|
+
}
|
|
1980
|
+
function writeMcpConfig(ctx, absolutePath) {
|
|
1981
|
+
const path = toRelative(ctx.cwd, absolutePath);
|
|
1982
|
+
let config = {};
|
|
1983
|
+
const existed = existsSync(absolutePath);
|
|
1984
|
+
if (existed) {
|
|
1985
|
+
try {
|
|
1986
|
+
config = JSON.parse(readFileSync2(absolutePath, "utf8"));
|
|
1987
|
+
} catch {
|
|
1988
|
+
ctx.results.push({ path, action: "skipped" });
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
1993
|
+
config.mcpServers = {};
|
|
1994
|
+
}
|
|
1995
|
+
const servers = config.mcpServers;
|
|
1996
|
+
const hasKhotan = Object.prototype.hasOwnProperty.call(servers, MCP_SERVER_KEY);
|
|
1997
|
+
if (hasKhotan && !ctx.force) {
|
|
1998
|
+
ctx.results.push({ path, action: "skipped" });
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
servers[MCP_SERVER_KEY] = mcpServerEntry();
|
|
2002
|
+
mkdirSync2(dirname2(absolutePath), { recursive: true });
|
|
2003
|
+
writeFileSync2(absolutePath, `${JSON.stringify(config, null, 2)}
|
|
2004
|
+
`);
|
|
2005
|
+
ctx.results.push({
|
|
2006
|
+
path,
|
|
2007
|
+
action: existed ? hasKhotan ? "forced" : "updated" : "created"
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
function runInit(options) {
|
|
2011
|
+
const { io, cwd, client, force, json } = options;
|
|
2012
|
+
const ctx = { cwd, force, results: [] };
|
|
2013
|
+
if (client === "cursor") {
|
|
2014
|
+
writeMcpConfig(ctx, join2(cwd, ".cursor", "mcp.json"));
|
|
2015
|
+
writeTextAsset(ctx, join2(cwd, ".cursor", "rules", "khotan.mdc"), CURSOR_RULES);
|
|
2016
|
+
} else {
|
|
2017
|
+
writeMcpConfig(ctx, join2(cwd, "mcp.json"));
|
|
2018
|
+
writeTextAsset(ctx, join2(cwd, "khotan-agents.md"), GENERIC_GUIDE);
|
|
2019
|
+
}
|
|
2020
|
+
if (json) {
|
|
2021
|
+
io.out(`${JSON.stringify({ client, actions: ctx.results }, null, 2)}
|
|
2022
|
+
`);
|
|
2023
|
+
return 0;
|
|
2024
|
+
}
|
|
2025
|
+
errLine(io, `Khotan workspace integration (${client}):`);
|
|
2026
|
+
for (const result of ctx.results) {
|
|
2027
|
+
errLine(io, ` ${result.action.padEnd(8)} ${result.path}`);
|
|
2028
|
+
}
|
|
2029
|
+
errLine(io);
|
|
2030
|
+
errLine(io, "Next steps:");
|
|
2031
|
+
errLine(io, " 1. Authenticate: khotan auth set-key --api-url <url> --api-key <key>");
|
|
2032
|
+
errLine(io, " 2. Open this workspace in your agent and ask it to configure Khotan.");
|
|
2033
|
+
return 0;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// src/cli/confirm.ts
|
|
2037
|
+
init_src();
|
|
2038
|
+
init_io();
|
|
2039
|
+
async function ensureConfirmed(options) {
|
|
2040
|
+
const { io, yes, summary } = options;
|
|
2041
|
+
if (yes) {
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
if (!io.isTty) {
|
|
2045
|
+
throw new KhotanConfirmationRequiredError(`${summary} This is a destructive action; re-run with --yes to confirm in a non-interactive session.`);
|
|
2046
|
+
}
|
|
2047
|
+
errLine(io, summary);
|
|
2048
|
+
const answer = (await io.prompt("Type 'yes' to confirm: ")).trim().toLowerCase();
|
|
2049
|
+
if (answer !== "yes" && answer !== "y") {
|
|
2050
|
+
throw new KhotanConfirmationRequiredError("Aborted; no changes were made.");
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/cli/help.ts
|
|
2055
|
+
init_src();
|
|
2056
|
+
|
|
2057
|
+
// src/cli/input.ts
|
|
2058
|
+
init_src();
|
|
2059
|
+
function camelToKebab(name) {
|
|
2060
|
+
return name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
2061
|
+
}
|
|
2062
|
+
function fieldFlag(field) {
|
|
2063
|
+
return camelToKebab(field.name);
|
|
2064
|
+
}
|
|
2065
|
+
function parseKeyValueList(pairs) {
|
|
2066
|
+
return pairs.map((pair) => {
|
|
2067
|
+
const eq = pair.indexOf("=");
|
|
2068
|
+
if (eq < 0) {
|
|
2069
|
+
throw new KhotanClientError("invalid_argument", `Expected KEY=VALUE but received "${pair}".`);
|
|
2070
|
+
}
|
|
2071
|
+
return { key: pair.slice(0, eq), value: pair.slice(eq + 1) };
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
function coerceScalar(field, raw) {
|
|
2075
|
+
if (field.type === "number") {
|
|
2076
|
+
const value = Number(raw);
|
|
2077
|
+
if (Number.isNaN(value)) {
|
|
2078
|
+
throw new KhotanClientError("invalid_argument", `--${fieldFlag(field)} expects a number, received "${raw}".`);
|
|
2079
|
+
}
|
|
2080
|
+
return value;
|
|
2081
|
+
}
|
|
2082
|
+
if (field.type === "boolean") {
|
|
2083
|
+
return raw !== "false" && raw !== "0";
|
|
2084
|
+
}
|
|
2085
|
+
return raw;
|
|
2086
|
+
}
|
|
2087
|
+
function buildCapabilityInput(capability, positionals, args) {
|
|
2088
|
+
const input = {};
|
|
2089
|
+
const pathFields = capability.input.filter((field) => field.location === "path");
|
|
2090
|
+
pathFields.forEach((field, index) => {
|
|
2091
|
+
const value = positionals[index];
|
|
2092
|
+
if (value !== undefined) {
|
|
2093
|
+
input[field.name] = value;
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
for (const field of capability.input) {
|
|
2097
|
+
if (field.location === "path") {
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2100
|
+
if (field.name === "environmentVariables") {
|
|
2101
|
+
const pairs = flagValues(args, "env");
|
|
2102
|
+
if (pairs.length > 0) {
|
|
2103
|
+
input[field.name] = parseKeyValueList(pairs);
|
|
2104
|
+
}
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
if (field.name === "variables") {
|
|
2108
|
+
const pairs = flagValues(args, "var");
|
|
2109
|
+
if (pairs.length > 0) {
|
|
2110
|
+
input[field.name] = parseKeyValueList(pairs);
|
|
2111
|
+
}
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
if (field.type === "json") {
|
|
2115
|
+
const raw2 = flagValue(args, fieldFlag(field));
|
|
2116
|
+
if (raw2 !== undefined) {
|
|
2117
|
+
try {
|
|
2118
|
+
input[field.name] = JSON.parse(raw2);
|
|
2119
|
+
} catch {
|
|
2120
|
+
throw new KhotanClientError("invalid_argument", `--${fieldFlag(field)} expects JSON, received "${raw2}".`);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
if (field.type === "string[]") {
|
|
2126
|
+
const repeated = flagValues(args, fieldFlag(field));
|
|
2127
|
+
if (repeated.length === 1) {
|
|
2128
|
+
input[field.name] = repeated[0].split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
2129
|
+
} else if (repeated.length > 1) {
|
|
2130
|
+
input[field.name] = repeated;
|
|
2131
|
+
}
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
if (field.type === "boolean") {
|
|
2135
|
+
if (args.flags.has(fieldFlag(field))) {
|
|
2136
|
+
input[field.name] = flagBool(args, fieldFlag(field));
|
|
2137
|
+
}
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
const raw = flagValue(args, fieldFlag(field));
|
|
2141
|
+
if (raw !== undefined) {
|
|
2142
|
+
input[field.name] = coerceScalar(field, raw);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return input;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/cli/help.ts
|
|
2149
|
+
init_io();
|
|
2150
|
+
var DOMAIN_TITLES = {
|
|
2151
|
+
identity: "Identity",
|
|
2152
|
+
"api-keys": "API keys",
|
|
2153
|
+
apps: "Apps",
|
|
2154
|
+
pipelines: "Pipelines",
|
|
2155
|
+
databases: "Databases",
|
|
2156
|
+
files: "Files",
|
|
2157
|
+
folders: "Folders",
|
|
2158
|
+
context: "Context documents"
|
|
2159
|
+
};
|
|
2160
|
+
function commandWords(capability) {
|
|
2161
|
+
return capability.cli?.command.join(" ");
|
|
2162
|
+
}
|
|
2163
|
+
function argUsage(field) {
|
|
2164
|
+
return field.required ? `<${field.name}>` : `[${field.name}]`;
|
|
2165
|
+
}
|
|
2166
|
+
function flagUsage(field) {
|
|
2167
|
+
if (field.name === "environmentVariables" || field.name === "variables") {
|
|
2168
|
+
return field.name === "variables" ? "[--var KEY=VALUE]..." : "[--env KEY=VALUE]...";
|
|
2169
|
+
}
|
|
2170
|
+
const placeholder = field.type === "boolean" ? "" : ` <${field.type === "string[]" ? "a,b" : field.type}>`;
|
|
2171
|
+
const flag = `--${fieldFlag(field)}${placeholder}`;
|
|
2172
|
+
return field.required ? flag : `[${flag}]`;
|
|
2173
|
+
}
|
|
2174
|
+
function commandUsage(capability) {
|
|
2175
|
+
const words = commandWords(capability) ?? capability.id;
|
|
2176
|
+
const pathArgs = capability.input.filter((field) => field.location === "path").map(argUsage);
|
|
2177
|
+
const flags = capability.input.filter((field) => field.location !== "path").map(flagUsage);
|
|
2178
|
+
return ["khotan", words, ...pathArgs, ...flags].join(" ");
|
|
2179
|
+
}
|
|
2180
|
+
function printTopLevelHelp(io) {
|
|
2181
|
+
outLine(io, "khotan — first-party CLI and MCP server for the Khotan API");
|
|
2182
|
+
outLine(io);
|
|
2183
|
+
outLine(io, "Usage: khotan <command> [arguments] [options]");
|
|
2184
|
+
outLine(io);
|
|
2185
|
+
outLine(io, "Authentication & profiles:");
|
|
2186
|
+
outLine(io, " auth login Exchange email/password for an API key");
|
|
2187
|
+
outLine(io, " auth set-key Store an existing API key in a profile");
|
|
2188
|
+
outLine(io, " auth use <profile> Switch the active profile");
|
|
2189
|
+
outLine(io, " auth list List stored profiles");
|
|
2190
|
+
outLine(io, " whoami Show the authenticated principal");
|
|
2191
|
+
outLine(io);
|
|
2192
|
+
const byDomain = new Map;
|
|
2193
|
+
for (const capability of listCapabilities()) {
|
|
2194
|
+
if (!isOperationCapability(capability)) {
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
if (capability.id === "api-keys.create-from-credentials") {
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
const list = byDomain.get(capability.domain) ?? [];
|
|
2201
|
+
list.push(capability);
|
|
2202
|
+
byDomain.set(capability.domain, list);
|
|
2203
|
+
}
|
|
2204
|
+
for (const domain of listDomains()) {
|
|
2205
|
+
const capabilities2 = byDomain.get(domain);
|
|
2206
|
+
if (!capabilities2 || capabilities2.length === 0) {
|
|
2207
|
+
continue;
|
|
2208
|
+
}
|
|
2209
|
+
if (domain === "identity") {
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
outLine(io, `${DOMAIN_TITLES[domain] ?? domain}:`);
|
|
2213
|
+
for (const capability of capabilities2) {
|
|
2214
|
+
const words = commandWords(capability) ?? capability.id;
|
|
2215
|
+
outLine(io, ` ${words.padEnd(30)} ${capability.cli?.summary ?? capability.title}`);
|
|
2216
|
+
}
|
|
2217
|
+
outLine(io);
|
|
2218
|
+
}
|
|
2219
|
+
outLine(io, "File transfer:");
|
|
2220
|
+
outLine(io, " files upload <path> Upload a local file (presigned flow)");
|
|
2221
|
+
outLine(io, " files download <fileId> Download a file to --output (presigned flow)");
|
|
2222
|
+
outLine(io);
|
|
2223
|
+
outLine(io, "Workspace:");
|
|
2224
|
+
outLine(io, " init Scaffold MCP config + agent guidance into this workspace");
|
|
2225
|
+
outLine(io);
|
|
2226
|
+
outLine(io, "Agent:");
|
|
2227
|
+
outLine(io, " mcp serve Start the Khotan MCP server over stdio");
|
|
2228
|
+
outLine(io);
|
|
2229
|
+
outLine(io, "Global options:");
|
|
2230
|
+
outLine(io, " --json Emit machine-readable JSON to stdout");
|
|
2231
|
+
outLine(io, " --yes Confirm destructive actions non-interactively");
|
|
2232
|
+
outLine(io, " --profile <name> Use a specific stored profile");
|
|
2233
|
+
outLine(io, " --api-url <url> Override the API origin");
|
|
2234
|
+
outLine(io, " --api-key <key> Override the API key");
|
|
2235
|
+
outLine(io, " --help Show help for a command");
|
|
2236
|
+
outLine(io);
|
|
2237
|
+
outLine(io, `khotan v${KHOTAN_ADAPTER_VERSION}`);
|
|
2238
|
+
}
|
|
2239
|
+
function printCommandHelp(io, capability) {
|
|
2240
|
+
outLine(io, capability.title);
|
|
2241
|
+
outLine(io);
|
|
2242
|
+
outLine(io, `Usage: ${commandUsage(capability)}`);
|
|
2243
|
+
outLine(io);
|
|
2244
|
+
outLine(io, capability.description);
|
|
2245
|
+
const args = capability.input;
|
|
2246
|
+
if (args.length > 0) {
|
|
2247
|
+
outLine(io);
|
|
2248
|
+
outLine(io, "Arguments & options:");
|
|
2249
|
+
for (const field of args) {
|
|
2250
|
+
const label = field.location === "path" ? `<${field.name}>` : `--${fieldFlag(field)}`;
|
|
2251
|
+
const flags = [
|
|
2252
|
+
field.required ? "required" : "optional",
|
|
2253
|
+
field.location,
|
|
2254
|
+
field.secret ? "secret" : null
|
|
2255
|
+
].filter(Boolean).join(", ");
|
|
2256
|
+
outLine(io, ` ${label.padEnd(24)}${field.description ?? ""} (${flags})`);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// src/cli/run.ts
|
|
2262
|
+
init_io();
|
|
2263
|
+
init_session();
|
|
2264
|
+
function buildCommandIndex() {
|
|
2265
|
+
const index = new Map;
|
|
2266
|
+
for (const capability of listCapabilities()) {
|
|
2267
|
+
if (capability.cli) {
|
|
2268
|
+
index.set(capability.cli.command.join(" "), capability);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
return index;
|
|
2272
|
+
}
|
|
2273
|
+
var commandIndex = buildCommandIndex();
|
|
2274
|
+
function matchCommand(positionals) {
|
|
2275
|
+
for (let n = Math.min(3, positionals.length);n >= 1; n--) {
|
|
2276
|
+
const key = positionals.slice(0, n).join(" ");
|
|
2277
|
+
const capability = commandIndex.get(key);
|
|
2278
|
+
if (capability) {
|
|
2279
|
+
return { capability, rest: positionals.slice(n) };
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
function describeTarget(capability, input) {
|
|
2285
|
+
const pathField = capability.input.find((field) => field.location === "path");
|
|
2286
|
+
const id = pathField ? input[pathField.name] : undefined;
|
|
2287
|
+
return id ? `${capability.title}: ${String(id)}` : capability.title;
|
|
2288
|
+
}
|
|
2289
|
+
async function runCapability(match, args, options) {
|
|
2290
|
+
const { capability } = match;
|
|
2291
|
+
if (!isOperationCapability(capability)) {
|
|
2292
|
+
throw new KhotanClientError("not_a_command", `${capability.id} is not an executable command.`);
|
|
2293
|
+
}
|
|
2294
|
+
if (flagBool(args, "help")) {
|
|
2295
|
+
printCommandHelp(options.io, capability);
|
|
2296
|
+
return 0;
|
|
2297
|
+
}
|
|
2298
|
+
const session = resolveSession({
|
|
2299
|
+
env: options.env,
|
|
2300
|
+
storeOptions: options.storeOptions,
|
|
2301
|
+
profileName: options.profileName,
|
|
2302
|
+
apiUrlOverride: options.apiUrlOverride,
|
|
2303
|
+
apiKeyOverride: options.apiKeyOverride,
|
|
2304
|
+
fetch: options.fetch
|
|
2305
|
+
});
|
|
2306
|
+
const input = buildCapabilityInput(capability, match.rest, args);
|
|
2307
|
+
const json = options.jsonFlag || session.resolved.defaultOutput === "json";
|
|
2308
|
+
if (isDestructive(capability.safety)) {
|
|
2309
|
+
await ensureConfirmed({
|
|
2310
|
+
io: options.io,
|
|
2311
|
+
yes: options.yes,
|
|
2312
|
+
summary: `About to ${capability.title.toLowerCase()} — ${describeTarget(capability, input)}.`
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
const result = await session.client.execute(capability, input);
|
|
2316
|
+
renderResult(options.io, result, { json, capability });
|
|
2317
|
+
return 0;
|
|
2318
|
+
}
|
|
2319
|
+
async function run(argv, options = {}) {
|
|
2320
|
+
const io = options.io ?? createNodeIo();
|
|
2321
|
+
const env = options.env ?? io.env;
|
|
2322
|
+
const args = parseArgs(argv);
|
|
2323
|
+
const { positionals } = args;
|
|
2324
|
+
const jsonFlag = flagBool(args, "json");
|
|
2325
|
+
const yes = flagBool(args, "yes");
|
|
2326
|
+
const helpFlag = flagBool(args, "help");
|
|
2327
|
+
const profileName = flagValue(args, "profile");
|
|
2328
|
+
const apiUrlOverride = flagValue(args, "api-url");
|
|
2329
|
+
const apiKeyOverride = flagValue(args, "api-key");
|
|
2330
|
+
if (flagBool(args, "version")) {
|
|
2331
|
+
const { KHOTAN_ADAPTER_VERSION: KHOTAN_ADAPTER_VERSION2 } = await Promise.resolve().then(() => (init_src(), exports_src));
|
|
2332
|
+
io.out(`${KHOTAN_ADAPTER_VERSION2}
|
|
2333
|
+
`);
|
|
2334
|
+
return 0;
|
|
2335
|
+
}
|
|
2336
|
+
const authCtx = {
|
|
2337
|
+
io,
|
|
2338
|
+
args,
|
|
2339
|
+
env,
|
|
2340
|
+
storeOptions: options.storeOptions,
|
|
2341
|
+
json: jsonFlag,
|
|
2342
|
+
fetch: options.fetch
|
|
2343
|
+
};
|
|
2344
|
+
try {
|
|
2345
|
+
assertSupportedCatalogSchemaVersion();
|
|
2346
|
+
const first = positionals[0];
|
|
2347
|
+
if (!first || first === "help") {
|
|
2348
|
+
const topic = positionals.slice(first === "help" ? 1 : 0);
|
|
2349
|
+
const match2 = topic.length > 0 ? matchCommand(topic) : undefined;
|
|
2350
|
+
if (match2 && isOperationCapability(match2.capability)) {
|
|
2351
|
+
printCommandHelp(io, match2.capability);
|
|
2352
|
+
} else {
|
|
2353
|
+
printTopLevelHelp(io);
|
|
2354
|
+
}
|
|
2355
|
+
return 0;
|
|
2356
|
+
}
|
|
2357
|
+
if (first === "init") {
|
|
2358
|
+
if (helpFlag) {
|
|
2359
|
+
errLine(io, "Usage: khotan init [--client cursor|generic] [--dir <path>] [--force] [--json]");
|
|
2360
|
+
return 0;
|
|
2361
|
+
}
|
|
2362
|
+
const clientRaw = flagValue(args, "client") ?? "cursor";
|
|
2363
|
+
if (clientRaw !== "cursor" && clientRaw !== "generic") {
|
|
2364
|
+
throw new KhotanClientError("invalid_argument", `Unknown --client "${clientRaw}". Use "cursor" or "generic".`);
|
|
2365
|
+
}
|
|
2366
|
+
const cwd = flagValue(args, "dir") ?? process.cwd();
|
|
2367
|
+
return runInit({ io, cwd, client: clientRaw, force: flagBool(args, "force"), json: jsonFlag });
|
|
2368
|
+
}
|
|
2369
|
+
if (first === "mcp" && positionals[1] === "serve") {
|
|
2370
|
+
const launcher = options.startMcpServer ?? (async (deps) => {
|
|
2371
|
+
const { serveMcp: serveMcp2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
2372
|
+
return serveMcp2(deps);
|
|
2373
|
+
});
|
|
2374
|
+
return await launcher({
|
|
2375
|
+
io,
|
|
2376
|
+
env,
|
|
2377
|
+
storeOptions: options.storeOptions,
|
|
2378
|
+
fetch: options.fetch,
|
|
2379
|
+
profileName,
|
|
2380
|
+
apiUrlOverride,
|
|
2381
|
+
apiKeyOverride
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
if (first === "login") {
|
|
2385
|
+
return await authLogin(authCtx);
|
|
2386
|
+
}
|
|
2387
|
+
if (first === "whoami" && !matchCommand(positionals)) {
|
|
2388
|
+
return await whoami(authCtx);
|
|
2389
|
+
}
|
|
2390
|
+
if (first === "auth") {
|
|
2391
|
+
const sub = positionals[1];
|
|
2392
|
+
if (sub === "set-key")
|
|
2393
|
+
return await authSetKey(authCtx);
|
|
2394
|
+
if (sub === "login")
|
|
2395
|
+
return await authLogin(authCtx);
|
|
2396
|
+
if (sub === "use") {
|
|
2397
|
+
const name = positionals[2];
|
|
2398
|
+
if (!name)
|
|
2399
|
+
throw new KhotanClientError("missing_argument", "Usage: khotan auth use <profile>.");
|
|
2400
|
+
return authUse(authCtx, name);
|
|
2401
|
+
}
|
|
2402
|
+
if (sub === "list")
|
|
2403
|
+
return authList(authCtx);
|
|
2404
|
+
if (sub === "whoami")
|
|
2405
|
+
return await whoami(authCtx);
|
|
2406
|
+
errLine(io, `Unknown auth command "${sub ?? ""}". Try: set-key, login, use, list.`);
|
|
2407
|
+
return 1;
|
|
2408
|
+
}
|
|
2409
|
+
if (first === "files" && (positionals[1] === "upload" || positionals[1] === "download")) {
|
|
2410
|
+
if (helpFlag) {
|
|
2411
|
+
errLine(io, positionals[1] === "upload" ? "Usage: khotan files upload <localPath> [--name <n>] [--folder-id <id>] [--folder-path <p>] [--content-type <t>]" : "Usage: khotan files download <fileId> --output <path>");
|
|
2412
|
+
return 0;
|
|
2413
|
+
}
|
|
2414
|
+
const session = resolveSession({
|
|
2415
|
+
env,
|
|
2416
|
+
storeOptions: options.storeOptions,
|
|
2417
|
+
profileName,
|
|
2418
|
+
apiUrlOverride,
|
|
2419
|
+
apiKeyOverride,
|
|
2420
|
+
fetch: options.fetch
|
|
2421
|
+
});
|
|
2422
|
+
const json = jsonFlag || session.resolved.defaultOutput === "json";
|
|
2423
|
+
if (positionals[1] === "upload") {
|
|
2424
|
+
const localPath = positionals[2];
|
|
2425
|
+
if (!localPath)
|
|
2426
|
+
throw new KhotanClientError("missing_argument", "Usage: khotan files upload <localPath>.");
|
|
2427
|
+
return await uploadFile({ session, io, json, httpFetch: options.fetch }, {
|
|
2428
|
+
localPath,
|
|
2429
|
+
name: flagValue(args, "name"),
|
|
2430
|
+
folderId: flagValue(args, "folder-id"),
|
|
2431
|
+
folderPath: flagValue(args, "folder-path"),
|
|
2432
|
+
contentType: flagValue(args, "content-type")
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
const fileId = positionals[2];
|
|
2436
|
+
const output = flagValue(args, "output");
|
|
2437
|
+
if (!fileId || !output) {
|
|
2438
|
+
throw new KhotanClientError("missing_argument", "Usage: khotan files download <fileId> --output <path>.");
|
|
2439
|
+
}
|
|
2440
|
+
return await downloadFile({ session, io, json, httpFetch: options.fetch }, { fileId, outputPath: output });
|
|
2441
|
+
}
|
|
2442
|
+
const match = matchCommand(positionals);
|
|
2443
|
+
if (match) {
|
|
2444
|
+
return await runCapability(match, args, {
|
|
2445
|
+
io,
|
|
2446
|
+
env,
|
|
2447
|
+
storeOptions: options.storeOptions,
|
|
2448
|
+
fetch: options.fetch,
|
|
2449
|
+
jsonFlag,
|
|
2450
|
+
yes,
|
|
2451
|
+
profileName,
|
|
2452
|
+
apiUrlOverride,
|
|
2453
|
+
apiKeyOverride
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
errLine(io, `Unknown command "${positionals.join(" ")}". Run \`khotan help\` for usage.`);
|
|
2457
|
+
return 1;
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
return handleError(io, error, jsonFlag);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
function handleError(io, error, json) {
|
|
2463
|
+
if (error instanceof KhotanApiError) {
|
|
2464
|
+
if (json) {
|
|
2465
|
+
io.out(`${JSON.stringify({ error: { code: error.code, message: error.message, details: error.details } }, null, 2)}
|
|
2466
|
+
`);
|
|
2467
|
+
} else {
|
|
2468
|
+
errLine(io, `Error [${error.code}]: ${error.message}`);
|
|
2469
|
+
}
|
|
2470
|
+
return 1;
|
|
2471
|
+
}
|
|
2472
|
+
if (error instanceof KhotanConfirmationRequiredError) {
|
|
2473
|
+
errLine(io, error.message);
|
|
2474
|
+
return 2;
|
|
2475
|
+
}
|
|
2476
|
+
if (error instanceof KhotanClientError) {
|
|
2477
|
+
errLine(io, `Error [${error.code}]: ${error.message}`);
|
|
2478
|
+
return 1;
|
|
2479
|
+
}
|
|
2480
|
+
errLine(io, `Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
2481
|
+
return 1;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// src/bin/khotan.ts
|
|
2485
|
+
run(process.argv.slice(2)).then((code) => {
|
|
2486
|
+
process.exitCode = code;
|
|
2487
|
+
}).catch((error) => {
|
|
2488
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
|
|
2489
|
+
`);
|
|
2490
|
+
process.exitCode = 1;
|
|
2491
|
+
});
|