@khotan/cli 0.3.0 → 0.4.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 +11 -0
- package/dist/khotan.js +352 -148
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,17 @@ It writes an MCP config wired to `khotan mcp serve` (credentials referenced by
|
|
|
43
43
|
MCP config merges into an existing one, preserving any other servers. Authenticate
|
|
44
44
|
separately with `khotan auth set-key` or environment variables — never commit a key.
|
|
45
45
|
|
|
46
|
+
`init` also installs catalog-derived agent **skills** for using the CLI into
|
|
47
|
+
`.cursor/skills/`, `.agents/skills/`, and `.claude/skills/` (regardless of
|
|
48
|
+
`--client`): an overview `khotan` skill plus one per command domain
|
|
49
|
+
(`khotan-apps`, `khotan-databases`, …). The skills are generated from the same
|
|
50
|
+
capability catalog that drives the commands, MCP tools, and `khotan help`, so
|
|
51
|
+
they can never drift. They are **managed assets**: every `khotan init` run
|
|
52
|
+
regenerates them in place (no `--force` needed), so re-running `init` is how a
|
|
53
|
+
workspace picks up new CLI commands — like the `databases branches` group. Your
|
|
54
|
+
hand-authored rule/guide and any MCP edits stay non-destructive (preserved
|
|
55
|
+
unless `--force`).
|
|
56
|
+
|
|
46
57
|
## Authentication & profiles
|
|
47
58
|
|
|
48
59
|
The CLI authenticates with an organization-scoped **API key**. Credentials are
|
package/dist/khotan.js
CHANGED
|
@@ -19,7 +19,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
19
19
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
20
|
|
|
21
21
|
// ../khotan-core/src/version.ts
|
|
22
|
-
var CATALOG_SCHEMA_VERSION = 1, CATALOG_VERSION = "2026-06-18", KHOTAN_ADAPTER_NAME = "khotan", KHOTAN_ADAPTER_VERSION = "0.
|
|
22
|
+
var CATALOG_SCHEMA_VERSION = 1, CATALOG_VERSION = "2026-06-18", KHOTAN_ADAPTER_NAME = "khotan", KHOTAN_ADAPTER_VERSION = "0.4.0", SUPPORTED_CATALOG_SCHEMA_VERSIONS;
|
|
23
23
|
var init_version = __esm(() => {
|
|
24
24
|
SUPPORTED_CATALOG_SCHEMA_VERSIONS = [
|
|
25
25
|
CATALOG_SCHEMA_VERSION
|
|
@@ -2040,155 +2040,9 @@ async function downloadFile(deps, options) {
|
|
|
2040
2040
|
init_io();
|
|
2041
2041
|
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2042
2042
|
import { dirname as dirname2, join as join2, relative } from "node:path";
|
|
2043
|
-
var MCP_SERVER_KEY = "khotan";
|
|
2044
|
-
function mcpServerEntry() {
|
|
2045
|
-
return {
|
|
2046
|
-
command: "khotan",
|
|
2047
|
-
args: ["mcp", "serve"],
|
|
2048
|
-
env: {
|
|
2049
|
-
KHOTAN_API_URL: "<your-khotan-api-url>",
|
|
2050
|
-
KHOTAN_API_KEY: "<your-khotan-api-key>"
|
|
2051
|
-
}
|
|
2052
|
-
};
|
|
2053
|
-
}
|
|
2054
|
-
var GUIDE_BODY = `This workspace is wired to Khotan through the \`khotan\` CLI and its MCP server.
|
|
2055
|
-
Use them to inspect and configure apps, pipelines, databases, files, folders, and
|
|
2056
|
-
context documents through the Khotan \`/api/v1\` surface.
|
|
2057
|
-
|
|
2058
|
-
## Authentication (do this once — never commit secrets)
|
|
2059
|
-
|
|
2060
|
-
Khotan reads an organization-scoped API key from a stored profile or the
|
|
2061
|
-
environment:
|
|
2062
|
-
|
|
2063
|
-
- Profile (recommended): \`khotan auth set-key --api-url <url> --api-key <key>\`
|
|
2064
|
-
stores the key in an owner-only file outside the repository.
|
|
2065
|
-
- Environment: export \`KHOTAN_API_URL\` and \`KHOTAN_API_KEY\` in your shell or
|
|
2066
|
-
agent sandbox.
|
|
2067
|
-
|
|
2068
|
-
Never put an API key in a file committed to the repository. The generated MCP
|
|
2069
|
-
config contains only placeholders.
|
|
2070
|
-
|
|
2071
|
-
## CLI surface
|
|
2072
|
-
|
|
2073
|
-
- \`khotan apps list|get|create|delete|redeploy\` and \`khotan apps env …\`
|
|
2074
|
-
- \`khotan pipelines …\`, \`khotan databases …\`
|
|
2075
|
-
- \`khotan files list|upload|download|…\`, \`khotan folders …\`
|
|
2076
|
-
- \`khotan context list|get|create|update|delete\`
|
|
2077
|
-
- Add \`--json\` to any command for machine-readable output.
|
|
2078
|
-
|
|
2079
|
-
Run \`khotan help\` or \`khotan <command> --help\` for the full, catalog-derived
|
|
2080
|
-
surface.
|
|
2081
|
-
|
|
2082
|
-
## Safety
|
|
2083
|
-
|
|
2084
|
-
- Destructive commands (deletes, env unset) require confirmation; pass \`--yes\`
|
|
2085
|
-
only when you intend it.
|
|
2086
|
-
- Secret values (env reveal, database connection strings, credential rotation)
|
|
2087
|
-
are only available through explicit commands and are never printed in lists.
|
|
2088
|
-
|
|
2089
|
-
## MCP
|
|
2090
|
-
|
|
2091
|
-
\`khotan mcp serve\` exposes the same operations as MCP tools and durable reads as
|
|
2092
|
-
MCP resources, reusing your stored profile or environment credentials.
|
|
2093
|
-
`;
|
|
2094
|
-
var CURSOR_RULES = `---
|
|
2095
|
-
description: Use the Khotan CLI and MCP tools to configure and operate this workspace's Khotan resources
|
|
2096
|
-
alwaysApply: false
|
|
2097
|
-
---
|
|
2098
|
-
|
|
2099
|
-
# Khotan
|
|
2100
|
-
|
|
2101
|
-
${GUIDE_BODY}`;
|
|
2102
|
-
var GENERIC_GUIDE = `# Khotan agent guide
|
|
2103
|
-
|
|
2104
|
-
${GUIDE_BODY}`;
|
|
2105
|
-
function toRelative(cwd, absolutePath) {
|
|
2106
|
-
return relative(cwd, absolutePath) || absolutePath;
|
|
2107
|
-
}
|
|
2108
|
-
function writeTextAsset(ctx, absolutePath, content) {
|
|
2109
|
-
const path = toRelative(ctx.cwd, absolutePath);
|
|
2110
|
-
const existed = existsSync(absolutePath);
|
|
2111
|
-
if (existed && !ctx.force) {
|
|
2112
|
-
ctx.results.push({ path, action: "skipped" });
|
|
2113
|
-
return;
|
|
2114
|
-
}
|
|
2115
|
-
mkdirSync2(dirname2(absolutePath), { recursive: true });
|
|
2116
|
-
writeFileSync2(absolutePath, content);
|
|
2117
|
-
ctx.results.push({ path, action: existed ? "forced" : "created" });
|
|
2118
|
-
}
|
|
2119
|
-
function writeMcpConfig(ctx, absolutePath) {
|
|
2120
|
-
const path = toRelative(ctx.cwd, absolutePath);
|
|
2121
|
-
let config = {};
|
|
2122
|
-
const existed = existsSync(absolutePath);
|
|
2123
|
-
if (existed) {
|
|
2124
|
-
try {
|
|
2125
|
-
config = JSON.parse(readFileSync2(absolutePath, "utf8"));
|
|
2126
|
-
} catch {
|
|
2127
|
-
ctx.results.push({ path, action: "skipped" });
|
|
2128
|
-
return;
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
2132
|
-
config.mcpServers = {};
|
|
2133
|
-
}
|
|
2134
|
-
const servers = config.mcpServers;
|
|
2135
|
-
const hasKhotan = Object.prototype.hasOwnProperty.call(servers, MCP_SERVER_KEY);
|
|
2136
|
-
if (hasKhotan && !ctx.force) {
|
|
2137
|
-
ctx.results.push({ path, action: "skipped" });
|
|
2138
|
-
return;
|
|
2139
|
-
}
|
|
2140
|
-
servers[MCP_SERVER_KEY] = mcpServerEntry();
|
|
2141
|
-
mkdirSync2(dirname2(absolutePath), { recursive: true });
|
|
2142
|
-
writeFileSync2(absolutePath, `${JSON.stringify(config, null, 2)}
|
|
2143
|
-
`);
|
|
2144
|
-
ctx.results.push({
|
|
2145
|
-
path,
|
|
2146
|
-
action: existed ? hasKhotan ? "forced" : "updated" : "created"
|
|
2147
|
-
});
|
|
2148
|
-
}
|
|
2149
|
-
function runInit(options) {
|
|
2150
|
-
const { io, cwd, client, force, json } = options;
|
|
2151
|
-
const ctx = { cwd, force, results: [] };
|
|
2152
|
-
if (client === "cursor") {
|
|
2153
|
-
writeMcpConfig(ctx, join2(cwd, ".cursor", "mcp.json"));
|
|
2154
|
-
writeTextAsset(ctx, join2(cwd, ".cursor", "rules", "khotan.mdc"), CURSOR_RULES);
|
|
2155
|
-
} else {
|
|
2156
|
-
writeMcpConfig(ctx, join2(cwd, "mcp.json"));
|
|
2157
|
-
writeTextAsset(ctx, join2(cwd, "khotan-agents.md"), GENERIC_GUIDE);
|
|
2158
|
-
}
|
|
2159
|
-
if (json) {
|
|
2160
|
-
io.out(`${JSON.stringify({ client, actions: ctx.results }, null, 2)}
|
|
2161
|
-
`);
|
|
2162
|
-
return 0;
|
|
2163
|
-
}
|
|
2164
|
-
errLine(io, `Khotan workspace integration (${client}):`);
|
|
2165
|
-
for (const result of ctx.results) {
|
|
2166
|
-
errLine(io, ` ${result.action.padEnd(8)} ${result.path}`);
|
|
2167
|
-
}
|
|
2168
|
-
errLine(io);
|
|
2169
|
-
errLine(io, "Next steps:");
|
|
2170
|
-
errLine(io, " 1. Authenticate: khotan auth set-key --api-url <url> --api-key <key>");
|
|
2171
|
-
errLine(io, " 2. Open this workspace in your agent and ask it to configure Khotan.");
|
|
2172
|
-
return 0;
|
|
2173
|
-
}
|
|
2174
2043
|
|
|
2175
|
-
// src/cli/
|
|
2044
|
+
// src/cli/skills.ts
|
|
2176
2045
|
init_src();
|
|
2177
|
-
init_io();
|
|
2178
|
-
async function ensureConfirmed(options) {
|
|
2179
|
-
const { io, yes, summary } = options;
|
|
2180
|
-
if (yes) {
|
|
2181
|
-
return;
|
|
2182
|
-
}
|
|
2183
|
-
if (!io.isTty) {
|
|
2184
|
-
throw new KhotanConfirmationRequiredError(`${summary} This is a destructive action; re-run with --yes to confirm in a non-interactive session.`);
|
|
2185
|
-
}
|
|
2186
|
-
errLine(io, summary);
|
|
2187
|
-
const answer = (await io.prompt("Type 'yes' to confirm: ")).trim().toLowerCase();
|
|
2188
|
-
if (answer !== "yes" && answer !== "y") {
|
|
2189
|
-
throw new KhotanConfirmationRequiredError("Aborted; no changes were made.");
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
2046
|
|
|
2193
2047
|
// src/cli/help.ts
|
|
2194
2048
|
init_src();
|
|
@@ -2397,6 +2251,356 @@ function printCommandHelp(io, capability) {
|
|
|
2397
2251
|
}
|
|
2398
2252
|
}
|
|
2399
2253
|
|
|
2254
|
+
// src/cli/skills.ts
|
|
2255
|
+
var AUTH_LOGIN_CAPABILITY_ID = "api-keys.create-from-credentials";
|
|
2256
|
+
var OVERVIEW_DOMAINS = new Set([
|
|
2257
|
+
"identity",
|
|
2258
|
+
"api-keys"
|
|
2259
|
+
]);
|
|
2260
|
+
var DOMAIN_TITLES2 = {
|
|
2261
|
+
apps: "Apps",
|
|
2262
|
+
pipelines: "Pipelines",
|
|
2263
|
+
databases: "Databases",
|
|
2264
|
+
files: "Files",
|
|
2265
|
+
folders: "Folders",
|
|
2266
|
+
context: "Context documents"
|
|
2267
|
+
};
|
|
2268
|
+
var DOMAIN_LABELS = {
|
|
2269
|
+
apps: "apps",
|
|
2270
|
+
pipelines: "pipelines",
|
|
2271
|
+
databases: "databases and database branches",
|
|
2272
|
+
files: "files",
|
|
2273
|
+
folders: "folders",
|
|
2274
|
+
context: "context documents"
|
|
2275
|
+
};
|
|
2276
|
+
function domainTitle(domain) {
|
|
2277
|
+
return DOMAIN_TITLES2[domain] ?? domain;
|
|
2278
|
+
}
|
|
2279
|
+
function domainLabel(domain) {
|
|
2280
|
+
return DOMAIN_LABELS[domain] ?? domain;
|
|
2281
|
+
}
|
|
2282
|
+
function yamlQuote(value) {
|
|
2283
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
2284
|
+
}
|
|
2285
|
+
function frontmatter(name, description) {
|
|
2286
|
+
return ["---", `name: ${name}`, `description: ${yamlQuote(description)}`, "---"].join(`
|
|
2287
|
+
`);
|
|
2288
|
+
}
|
|
2289
|
+
function stamp() {
|
|
2290
|
+
return `<!-- generated from khotan catalog v${CATALOG_VERSION} (adapter ${KHOTAN_ADAPTER_VERSION}); do not edit by hand -->`;
|
|
2291
|
+
}
|
|
2292
|
+
function operationsForDomain(domain) {
|
|
2293
|
+
return listCapabilities().filter(isOperationCapability).filter((capability) => capability.domain === domain).filter((capability) => capability.id !== AUTH_LOGIN_CAPABILITY_ID).sort((a, b) => a.cli.command.join(" ").localeCompare(b.cli.command.join(" ")));
|
|
2294
|
+
}
|
|
2295
|
+
function resourcesForDomain(domain) {
|
|
2296
|
+
return listCapabilities().filter(isResourceCapability).filter((capability) => capability.domain === domain).sort((a, b) => a.id.localeCompare(b.id));
|
|
2297
|
+
}
|
|
2298
|
+
function fieldLine(field) {
|
|
2299
|
+
const label = field.location === "path" ? `<${field.name}>` : `--${fieldFlag(field)}`;
|
|
2300
|
+
const notes = [
|
|
2301
|
+
field.required ? "required" : "optional",
|
|
2302
|
+
field.location,
|
|
2303
|
+
field.secret ? "secret" : null
|
|
2304
|
+
].filter(Boolean).join(", ");
|
|
2305
|
+
return `- \`${label}\` — ${field.description ?? ""} (${notes})`;
|
|
2306
|
+
}
|
|
2307
|
+
function renderOperation(capability) {
|
|
2308
|
+
const command = capability.cli.command.join(" ");
|
|
2309
|
+
const lines = [
|
|
2310
|
+
`### khotan ${command}`,
|
|
2311
|
+
"",
|
|
2312
|
+
`Usage: \`${commandUsage(capability)}\``,
|
|
2313
|
+
"",
|
|
2314
|
+
capability.description
|
|
2315
|
+
];
|
|
2316
|
+
if (capability.input.length > 0) {
|
|
2317
|
+
lines.push("", "Arguments & options:");
|
|
2318
|
+
for (const field of capability.input) {
|
|
2319
|
+
lines.push(fieldLine(field));
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
if (requiresConfirmation(capability.safety)) {
|
|
2323
|
+
lines.push("", "Safety: destructive — prompts for confirmation in a terminal and refuses in non-interactive sessions unless you pass `--yes`.");
|
|
2324
|
+
} else if (isSecret(capability.safety)) {
|
|
2325
|
+
lines.push("", "Safety: secret — returns credential-bearing values; never shown in list/table output and never exposed as an MCP resource.");
|
|
2326
|
+
}
|
|
2327
|
+
lines.push("", `MCP tool: \`${capability.mcp.tool.name}\``);
|
|
2328
|
+
if (capability.cli.examples && capability.cli.examples.length > 0) {
|
|
2329
|
+
lines.push("", "Examples:");
|
|
2330
|
+
for (const example of capability.cli.examples) {
|
|
2331
|
+
lines.push(`- \`${example}\``);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
return lines.join(`
|
|
2335
|
+
`);
|
|
2336
|
+
}
|
|
2337
|
+
function fileTransferSection() {
|
|
2338
|
+
return [
|
|
2339
|
+
"## Local file transfer (presigned)",
|
|
2340
|
+
"",
|
|
2341
|
+
"These commands move bytes between your machine and object storage using presigned URLs. They are CLI-only helpers (not catalog operations) that wrap the upload/download-url operations:",
|
|
2342
|
+
"",
|
|
2343
|
+
"- `khotan files upload <localPath> [--name <n>] [--folder-id <id>] [--folder-path <p>] [--content-type <t>]` — upload a local file.",
|
|
2344
|
+
"- `khotan files download <fileId> --output <path>` — download a file to a local path."
|
|
2345
|
+
].join(`
|
|
2346
|
+
`);
|
|
2347
|
+
}
|
|
2348
|
+
function renderDomainSkill(domain) {
|
|
2349
|
+
const operations = operationsForDomain(domain);
|
|
2350
|
+
const resources = resourcesForDomain(domain);
|
|
2351
|
+
const label = domainLabel(domain);
|
|
2352
|
+
const commandTails = operations.map((capability) => capability.cli.command.slice(1).join(" ")).filter((tail) => tail.length > 0);
|
|
2353
|
+
const description = `Use the khotan CLI and MCP server to work with Khotan ${label} — commands ${commandTails.join(", ")}. Use when a user wants to inspect or operate Khotan ${label} via the khotan CLI or its MCP tools.`;
|
|
2354
|
+
const sections = [
|
|
2355
|
+
frontmatter(`khotan-${domain}`, description),
|
|
2356
|
+
"",
|
|
2357
|
+
`# Khotan ${domainTitle(domain)}`,
|
|
2358
|
+
"",
|
|
2359
|
+
`Use the \`khotan\` CLI (and its MCP server) to operate Khotan ${label}. Every command also exists as an MCP tool. Add \`--json\` for machine-readable output; results go to stdout, diagnostics to stderr.`,
|
|
2360
|
+
"",
|
|
2361
|
+
"## Commands",
|
|
2362
|
+
"",
|
|
2363
|
+
operations.map(renderOperation).join(`
|
|
2364
|
+
|
|
2365
|
+
`)
|
|
2366
|
+
];
|
|
2367
|
+
if (domain === "files") {
|
|
2368
|
+
sections.push("", fileTransferSection());
|
|
2369
|
+
}
|
|
2370
|
+
if (resources.length > 0) {
|
|
2371
|
+
sections.push("", "## Resources", "");
|
|
2372
|
+
for (const resource of resources) {
|
|
2373
|
+
sections.push(`- \`${resource.mcp.resource.uriTemplate}\` — ${resource.mcp.resource.description}`);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
sections.push("", stamp(), "");
|
|
2377
|
+
return { name: `khotan-${domain}`, content: sections.join(`
|
|
2378
|
+
`) };
|
|
2379
|
+
}
|
|
2380
|
+
function overviewSkill() {
|
|
2381
|
+
const domains = listDomains().filter((domain) => !OVERVIEW_DOMAINS.has(domain));
|
|
2382
|
+
const description = "Use the khotan CLI and MCP server to operate this workspace's Khotan resources — authenticate, choose profiles, control output, and run apps, pipelines, databases, files, folders, and context commands. Use when setting up khotan auth or deciding which khotan command or domain skill to use.";
|
|
2383
|
+
const indexLines = domains.map((domain) => `- \`khotan-${domain}\` — ${domainLabel(domain)}.`);
|
|
2384
|
+
const content = [
|
|
2385
|
+
frontmatter("khotan", description),
|
|
2386
|
+
"",
|
|
2387
|
+
"# Khotan",
|
|
2388
|
+
"",
|
|
2389
|
+
"This workspace is wired to Khotan through the `khotan` CLI and its MCP server. Both are thin adapters over the Khotan `/api/v1` surface and expose the same operations: the CLI for humans, MCP tools for agents.",
|
|
2390
|
+
"",
|
|
2391
|
+
"## Authentication (do this once — never commit secrets)",
|
|
2392
|
+
"",
|
|
2393
|
+
"Khotan reads an organization-scoped API key from a stored profile or the environment:",
|
|
2394
|
+
"",
|
|
2395
|
+
"- Profile (recommended): `khotan auth set-key --api-url <url> --api-key <key>` stores the key in an owner-only file outside the repository.",
|
|
2396
|
+
"- Exchange credentials: `khotan login --api-url <url> --email <email> --password <pw> --organization-id <org>` mints and stores a key (CLI-only; never exposed as an MCP tool).",
|
|
2397
|
+
"- Switch profiles: `khotan auth use <profile>`; list them with `khotan auth list` (keys are never printed).",
|
|
2398
|
+
"- Environment overrides (CI/agents): `KHOTAN_API_URL`, `KHOTAN_API_KEY`, `KHOTAN_PROFILE` take precedence over stored profiles.",
|
|
2399
|
+
"- Verify with `khotan whoami`.",
|
|
2400
|
+
"",
|
|
2401
|
+
"Never put an API key in a file committed to the repository. Generated MCP config contains only placeholders.",
|
|
2402
|
+
"",
|
|
2403
|
+
"## Output modes",
|
|
2404
|
+
"",
|
|
2405
|
+
"- Human-readable tables/detail by default; add `--json` for API-shaped JSON.",
|
|
2406
|
+
"- Results go to stdout; prompts, progress, and diagnostics go to stderr, so `khotan ... --json | jq` stays clean.",
|
|
2407
|
+
"",
|
|
2408
|
+
"## Safety model",
|
|
2409
|
+
"",
|
|
2410
|
+
"Each capability has a safety class:",
|
|
2411
|
+
"",
|
|
2412
|
+
"- `read` — safe; freely listable.",
|
|
2413
|
+
"- `write` — mutating; summarized before running.",
|
|
2414
|
+
"- `destructive` — deletes/irreversible; prompts for confirmation and refuses non-interactively unless you pass `--yes`.",
|
|
2415
|
+
"- `secret` — returns credential-bearing values (env reveal, connection strings, credential rotation); explicit-only and never printed in lists or exposed as MCP resources.",
|
|
2416
|
+
"",
|
|
2417
|
+
"## MCP server",
|
|
2418
|
+
"",
|
|
2419
|
+
"`khotan mcp serve` starts a stdio MCP server reusing your stored profile or environment credentials. Operations are exposed as tools (e.g. `khotan_apps_create`); durable read data is exposed as URI-addressed resources. stdout carries JSON-RPC only; logs go to stderr.",
|
|
2420
|
+
"",
|
|
2421
|
+
"## Command skills",
|
|
2422
|
+
"",
|
|
2423
|
+
"Per-domain skills cover the full, catalog-derived command surface:",
|
|
2424
|
+
"",
|
|
2425
|
+
...indexLines,
|
|
2426
|
+
"",
|
|
2427
|
+
"Run `khotan help` or `khotan <command> --help` for the authoritative usage.",
|
|
2428
|
+
"",
|
|
2429
|
+
stamp(),
|
|
2430
|
+
""
|
|
2431
|
+
].join(`
|
|
2432
|
+
`);
|
|
2433
|
+
return { name: "khotan", content };
|
|
2434
|
+
}
|
|
2435
|
+
function generateSkills() {
|
|
2436
|
+
const domains = listDomains().filter((domain) => !OVERVIEW_DOMAINS.has(domain));
|
|
2437
|
+
return [overviewSkill(), ...domains.map(renderDomainSkill)];
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/cli/commands/init.ts
|
|
2441
|
+
var MCP_SERVER_KEY = "khotan";
|
|
2442
|
+
var SKILL_BASES = [".cursor", ".agents", ".claude"];
|
|
2443
|
+
function mcpServerEntry() {
|
|
2444
|
+
return {
|
|
2445
|
+
command: "khotan",
|
|
2446
|
+
args: ["mcp", "serve"],
|
|
2447
|
+
env: {
|
|
2448
|
+
KHOTAN_API_URL: "<your-khotan-api-url>",
|
|
2449
|
+
KHOTAN_API_KEY: "<your-khotan-api-key>"
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
var GUIDE_BODY = `This workspace is wired to Khotan through the \`khotan\` CLI and its MCP server.
|
|
2454
|
+
Use them to inspect and configure apps, pipelines, databases, files, folders, and
|
|
2455
|
+
context documents through the Khotan \`/api/v1\` surface.
|
|
2456
|
+
|
|
2457
|
+
## Authentication (do this once — never commit secrets)
|
|
2458
|
+
|
|
2459
|
+
Khotan reads an organization-scoped API key from a stored profile or the
|
|
2460
|
+
environment:
|
|
2461
|
+
|
|
2462
|
+
- Profile (recommended): \`khotan auth set-key --api-url <url> --api-key <key>\`
|
|
2463
|
+
stores the key in an owner-only file outside the repository.
|
|
2464
|
+
- Environment: export \`KHOTAN_API_URL\` and \`KHOTAN_API_KEY\` in your shell or
|
|
2465
|
+
agent sandbox.
|
|
2466
|
+
|
|
2467
|
+
Never put an API key in a file committed to the repository. The generated MCP
|
|
2468
|
+
config contains only placeholders.
|
|
2469
|
+
|
|
2470
|
+
## CLI surface
|
|
2471
|
+
|
|
2472
|
+
- \`khotan apps list|get|create|delete|redeploy\` and \`khotan apps env …\`
|
|
2473
|
+
- \`khotan pipelines …\`, \`khotan databases …\`
|
|
2474
|
+
- \`khotan files list|upload|download|…\`, \`khotan folders …\`
|
|
2475
|
+
- \`khotan context list|get|create|update|delete\`
|
|
2476
|
+
- Add \`--json\` to any command for machine-readable output.
|
|
2477
|
+
|
|
2478
|
+
Run \`khotan help\` or \`khotan <command> --help\` for the full, catalog-derived
|
|
2479
|
+
surface.
|
|
2480
|
+
|
|
2481
|
+
## Safety
|
|
2482
|
+
|
|
2483
|
+
- Destructive commands (deletes, env unset) require confirmation; pass \`--yes\`
|
|
2484
|
+
only when you intend it.
|
|
2485
|
+
- Secret values (env reveal, database connection strings, credential rotation)
|
|
2486
|
+
are only available through explicit commands and are never printed in lists.
|
|
2487
|
+
|
|
2488
|
+
## MCP
|
|
2489
|
+
|
|
2490
|
+
\`khotan mcp serve\` exposes the same operations as MCP tools and durable reads as
|
|
2491
|
+
MCP resources, reusing your stored profile or environment credentials.
|
|
2492
|
+
`;
|
|
2493
|
+
var CURSOR_RULES = `---
|
|
2494
|
+
description: Use the Khotan CLI and MCP tools to configure and operate this workspace's Khotan resources
|
|
2495
|
+
alwaysApply: false
|
|
2496
|
+
---
|
|
2497
|
+
|
|
2498
|
+
# Khotan
|
|
2499
|
+
|
|
2500
|
+
${GUIDE_BODY}`;
|
|
2501
|
+
var GENERIC_GUIDE = `# Khotan agent guide
|
|
2502
|
+
|
|
2503
|
+
${GUIDE_BODY}`;
|
|
2504
|
+
function toRelative(cwd, absolutePath) {
|
|
2505
|
+
return relative(cwd, absolutePath) || absolutePath;
|
|
2506
|
+
}
|
|
2507
|
+
function writeTextAsset(ctx, absolutePath, content) {
|
|
2508
|
+
const path = toRelative(ctx.cwd, absolutePath);
|
|
2509
|
+
const existed = existsSync(absolutePath);
|
|
2510
|
+
if (existed && !ctx.force) {
|
|
2511
|
+
ctx.results.push({ path, action: "skipped" });
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
mkdirSync2(dirname2(absolutePath), { recursive: true });
|
|
2515
|
+
writeFileSync2(absolutePath, content);
|
|
2516
|
+
ctx.results.push({ path, action: existed ? "forced" : "created" });
|
|
2517
|
+
}
|
|
2518
|
+
function writeManagedAsset(ctx, absolutePath, content) {
|
|
2519
|
+
const path = toRelative(ctx.cwd, absolutePath);
|
|
2520
|
+
const existed = existsSync(absolutePath);
|
|
2521
|
+
mkdirSync2(dirname2(absolutePath), { recursive: true });
|
|
2522
|
+
writeFileSync2(absolutePath, content);
|
|
2523
|
+
ctx.results.push({ path, action: existed ? "updated" : "created" });
|
|
2524
|
+
}
|
|
2525
|
+
function writeMcpConfig(ctx, absolutePath) {
|
|
2526
|
+
const path = toRelative(ctx.cwd, absolutePath);
|
|
2527
|
+
let config = {};
|
|
2528
|
+
const existed = existsSync(absolutePath);
|
|
2529
|
+
if (existed) {
|
|
2530
|
+
try {
|
|
2531
|
+
config = JSON.parse(readFileSync2(absolutePath, "utf8"));
|
|
2532
|
+
} catch {
|
|
2533
|
+
ctx.results.push({ path, action: "skipped" });
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
2538
|
+
config.mcpServers = {};
|
|
2539
|
+
}
|
|
2540
|
+
const servers = config.mcpServers;
|
|
2541
|
+
const hasKhotan = Object.prototype.hasOwnProperty.call(servers, MCP_SERVER_KEY);
|
|
2542
|
+
if (hasKhotan && !ctx.force) {
|
|
2543
|
+
ctx.results.push({ path, action: "skipped" });
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
servers[MCP_SERVER_KEY] = mcpServerEntry();
|
|
2547
|
+
mkdirSync2(dirname2(absolutePath), { recursive: true });
|
|
2548
|
+
writeFileSync2(absolutePath, `${JSON.stringify(config, null, 2)}
|
|
2549
|
+
`);
|
|
2550
|
+
ctx.results.push({
|
|
2551
|
+
path,
|
|
2552
|
+
action: existed ? hasKhotan ? "forced" : "updated" : "created"
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
function runInit(options) {
|
|
2556
|
+
const { io, cwd, client, force, json } = options;
|
|
2557
|
+
const ctx = { cwd, force, results: [] };
|
|
2558
|
+
if (client === "cursor") {
|
|
2559
|
+
writeMcpConfig(ctx, join2(cwd, ".cursor", "mcp.json"));
|
|
2560
|
+
writeTextAsset(ctx, join2(cwd, ".cursor", "rules", "khotan.mdc"), CURSOR_RULES);
|
|
2561
|
+
} else {
|
|
2562
|
+
writeMcpConfig(ctx, join2(cwd, "mcp.json"));
|
|
2563
|
+
writeTextAsset(ctx, join2(cwd, "khotan-agents.md"), GENERIC_GUIDE);
|
|
2564
|
+
}
|
|
2565
|
+
for (const skill of generateSkills()) {
|
|
2566
|
+
for (const base of SKILL_BASES) {
|
|
2567
|
+
writeManagedAsset(ctx, join2(cwd, base, "skills", skill.name, "SKILL.md"), skill.content);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
if (json) {
|
|
2571
|
+
io.out(`${JSON.stringify({ client, actions: ctx.results }, null, 2)}
|
|
2572
|
+
`);
|
|
2573
|
+
return 0;
|
|
2574
|
+
}
|
|
2575
|
+
errLine(io, `Khotan workspace integration (${client}):`);
|
|
2576
|
+
for (const result of ctx.results) {
|
|
2577
|
+
errLine(io, ` ${result.action.padEnd(8)} ${result.path}`);
|
|
2578
|
+
}
|
|
2579
|
+
errLine(io);
|
|
2580
|
+
errLine(io, "Next steps:");
|
|
2581
|
+
errLine(io, " 1. Authenticate: khotan auth set-key --api-url <url> --api-key <key>");
|
|
2582
|
+
errLine(io, " 2. Open this workspace in your agent and ask it to configure Khotan.");
|
|
2583
|
+
return 0;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// src/cli/confirm.ts
|
|
2587
|
+
init_src();
|
|
2588
|
+
init_io();
|
|
2589
|
+
async function ensureConfirmed(options) {
|
|
2590
|
+
const { io, yes, summary } = options;
|
|
2591
|
+
if (yes) {
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
if (!io.isTty) {
|
|
2595
|
+
throw new KhotanConfirmationRequiredError(`${summary} This is a destructive action; re-run with --yes to confirm in a non-interactive session.`);
|
|
2596
|
+
}
|
|
2597
|
+
errLine(io, summary);
|
|
2598
|
+
const answer = (await io.prompt("Type 'yes' to confirm: ")).trim().toLowerCase();
|
|
2599
|
+
if (answer !== "yes" && answer !== "y") {
|
|
2600
|
+
throw new KhotanConfirmationRequiredError("Aborted; no changes were made.");
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2400
2604
|
// src/cli/run.ts
|
|
2401
2605
|
init_io();
|
|
2402
2606
|
init_session();
|
package/package.json
CHANGED