@khotan/cli 0.3.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/khotan.js +367 -148
  3. 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.3.0", SUPPORTED_CATALOG_SCHEMA_VERSIONS;
22
+ var CATALOG_SCHEMA_VERSION = 1, CATALOG_VERSION = "2026-06-23", KHOTAN_ADAPTER_NAME = "khotan", KHOTAN_ADAPTER_VERSION = "0.5.0", SUPPORTED_CATALOG_SCHEMA_VERSIONS;
23
23
  var init_version = __esm(() => {
24
24
  SUPPORTED_CATALOG_SCHEMA_VERSIONS = [
25
25
  CATALOG_SCHEMA_VERSION
@@ -187,6 +187,19 @@ function platformDomain(config) {
187
187
  cli: { command: [domain, "redeploy"], summary: `Redeploy ${a} ${noun}`, defaultOutput: "detail" },
188
188
  mcp: { tool: { name: tool("redeploy"), title: `Redeploy ${a} ${noun}`, description: `Trigger a new deployment of ${a} ${noun}.` } }
189
189
  },
190
+ {
191
+ id: `${cap}.source`,
192
+ kind: "operation",
193
+ domain,
194
+ safety: "read",
195
+ auth: true,
196
+ title: `Download ${a} ${noun}'s source`,
197
+ description: `Download the ${noun}'s source files (its subdirectory in the org mono-root repo) as a JSON listing with paths rooted at the ${noun}. Text files come back utf-8, binary files base64.`,
198
+ input: [idArg],
199
+ http: { method: "GET", path: `${base}/{${resourceParam}}/source`, operationId: operationIds.source },
200
+ cli: { command: [domain, "source"], summary: `Download ${a} ${noun}'s source`, defaultOutput: "detail" },
201
+ mcp: { tool: { name: tool("source"), title: `Download ${a} ${noun}'s source`, description: `Download the ${noun}'s source files as a JSON listing. Text files utf-8, binary base64.` } }
202
+ },
190
203
  {
191
204
  id: `${cap}.env.list`,
192
205
  kind: "operation",
@@ -907,6 +920,7 @@ var init_catalog = __esm(() => {
907
920
  create: "createApp",
908
921
  delete: "deleteApp",
909
922
  redeploy: "redeployApp",
923
+ source: "getAppSource",
910
924
  envList: "listAppEnvVars",
911
925
  envBulkSet: "bulkUpsertAppEnvVars",
912
926
  envReveal: "revealAppEnvVar",
@@ -925,6 +939,7 @@ var init_catalog = __esm(() => {
925
939
  create: "createPipeline",
926
940
  delete: "deletePipeline",
927
941
  redeploy: "redeployPipeline",
942
+ source: "getPipelineSource",
928
943
  envList: "listPipelineEnvVars",
929
944
  envBulkSet: "bulkUpsertPipelineEnvVars",
930
945
  envReveal: "revealPipelineEnvVar",
@@ -2040,155 +2055,9 @@ async function downloadFile(deps, options) {
2040
2055
  init_io();
2041
2056
  import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
2042
2057
  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
2058
 
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
-
2175
- // src/cli/confirm.ts
2059
+ // src/cli/skills.ts
2176
2060
  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
2061
 
2193
2062
  // src/cli/help.ts
2194
2063
  init_src();
@@ -2397,6 +2266,356 @@ function printCommandHelp(io, capability) {
2397
2266
  }
2398
2267
  }
2399
2268
 
2269
+ // src/cli/skills.ts
2270
+ var AUTH_LOGIN_CAPABILITY_ID = "api-keys.create-from-credentials";
2271
+ var OVERVIEW_DOMAINS = new Set([
2272
+ "identity",
2273
+ "api-keys"
2274
+ ]);
2275
+ var DOMAIN_TITLES2 = {
2276
+ apps: "Apps",
2277
+ pipelines: "Pipelines",
2278
+ databases: "Databases",
2279
+ files: "Files",
2280
+ folders: "Folders",
2281
+ context: "Context documents"
2282
+ };
2283
+ var DOMAIN_LABELS = {
2284
+ apps: "apps",
2285
+ pipelines: "pipelines",
2286
+ databases: "databases and database branches",
2287
+ files: "files",
2288
+ folders: "folders",
2289
+ context: "context documents"
2290
+ };
2291
+ function domainTitle(domain) {
2292
+ return DOMAIN_TITLES2[domain] ?? domain;
2293
+ }
2294
+ function domainLabel(domain) {
2295
+ return DOMAIN_LABELS[domain] ?? domain;
2296
+ }
2297
+ function yamlQuote(value) {
2298
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
2299
+ }
2300
+ function frontmatter(name, description) {
2301
+ return ["---", `name: ${name}`, `description: ${yamlQuote(description)}`, "---"].join(`
2302
+ `);
2303
+ }
2304
+ function stamp() {
2305
+ return `<!-- generated from khotan catalog v${CATALOG_VERSION} (adapter ${KHOTAN_ADAPTER_VERSION}); do not edit by hand -->`;
2306
+ }
2307
+ function operationsForDomain(domain) {
2308
+ 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(" ")));
2309
+ }
2310
+ function resourcesForDomain(domain) {
2311
+ return listCapabilities().filter(isResourceCapability).filter((capability) => capability.domain === domain).sort((a, b) => a.id.localeCompare(b.id));
2312
+ }
2313
+ function fieldLine(field) {
2314
+ const label = field.location === "path" ? `<${field.name}>` : `--${fieldFlag(field)}`;
2315
+ const notes = [
2316
+ field.required ? "required" : "optional",
2317
+ field.location,
2318
+ field.secret ? "secret" : null
2319
+ ].filter(Boolean).join(", ");
2320
+ return `- \`${label}\` — ${field.description ?? ""} (${notes})`;
2321
+ }
2322
+ function renderOperation(capability) {
2323
+ const command = capability.cli.command.join(" ");
2324
+ const lines = [
2325
+ `### khotan ${command}`,
2326
+ "",
2327
+ `Usage: \`${commandUsage(capability)}\``,
2328
+ "",
2329
+ capability.description
2330
+ ];
2331
+ if (capability.input.length > 0) {
2332
+ lines.push("", "Arguments & options:");
2333
+ for (const field of capability.input) {
2334
+ lines.push(fieldLine(field));
2335
+ }
2336
+ }
2337
+ if (requiresConfirmation(capability.safety)) {
2338
+ lines.push("", "Safety: destructive — prompts for confirmation in a terminal and refuses in non-interactive sessions unless you pass `--yes`.");
2339
+ } else if (isSecret(capability.safety)) {
2340
+ lines.push("", "Safety: secret — returns credential-bearing values; never shown in list/table output and never exposed as an MCP resource.");
2341
+ }
2342
+ lines.push("", `MCP tool: \`${capability.mcp.tool.name}\``);
2343
+ if (capability.cli.examples && capability.cli.examples.length > 0) {
2344
+ lines.push("", "Examples:");
2345
+ for (const example of capability.cli.examples) {
2346
+ lines.push(`- \`${example}\``);
2347
+ }
2348
+ }
2349
+ return lines.join(`
2350
+ `);
2351
+ }
2352
+ function fileTransferSection() {
2353
+ return [
2354
+ "## Local file transfer (presigned)",
2355
+ "",
2356
+ "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:",
2357
+ "",
2358
+ "- `khotan files upload <localPath> [--name <n>] [--folder-id <id>] [--folder-path <p>] [--content-type <t>]` — upload a local file.",
2359
+ "- `khotan files download <fileId> --output <path>` — download a file to a local path."
2360
+ ].join(`
2361
+ `);
2362
+ }
2363
+ function renderDomainSkill(domain) {
2364
+ const operations = operationsForDomain(domain);
2365
+ const resources = resourcesForDomain(domain);
2366
+ const label = domainLabel(domain);
2367
+ const commandTails = operations.map((capability) => capability.cli.command.slice(1).join(" ")).filter((tail) => tail.length > 0);
2368
+ 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.`;
2369
+ const sections = [
2370
+ frontmatter(`khotan-${domain}`, description),
2371
+ "",
2372
+ `# Khotan ${domainTitle(domain)}`,
2373
+ "",
2374
+ `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.`,
2375
+ "",
2376
+ "## Commands",
2377
+ "",
2378
+ operations.map(renderOperation).join(`
2379
+
2380
+ `)
2381
+ ];
2382
+ if (domain === "files") {
2383
+ sections.push("", fileTransferSection());
2384
+ }
2385
+ if (resources.length > 0) {
2386
+ sections.push("", "## Resources", "");
2387
+ for (const resource of resources) {
2388
+ sections.push(`- \`${resource.mcp.resource.uriTemplate}\` — ${resource.mcp.resource.description}`);
2389
+ }
2390
+ }
2391
+ sections.push("", stamp(), "");
2392
+ return { name: `khotan-${domain}`, content: sections.join(`
2393
+ `) };
2394
+ }
2395
+ function overviewSkill() {
2396
+ const domains = listDomains().filter((domain) => !OVERVIEW_DOMAINS.has(domain));
2397
+ 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.";
2398
+ const indexLines = domains.map((domain) => `- \`khotan-${domain}\` — ${domainLabel(domain)}.`);
2399
+ const content = [
2400
+ frontmatter("khotan", description),
2401
+ "",
2402
+ "# Khotan",
2403
+ "",
2404
+ "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.",
2405
+ "",
2406
+ "## Authentication (do this once — never commit secrets)",
2407
+ "",
2408
+ "Khotan reads an organization-scoped API key from a stored profile or the environment:",
2409
+ "",
2410
+ "- Profile (recommended): `khotan auth set-key --api-url <url> --api-key <key>` stores the key in an owner-only file outside the repository.",
2411
+ "- 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).",
2412
+ "- Switch profiles: `khotan auth use <profile>`; list them with `khotan auth list` (keys are never printed).",
2413
+ "- Environment overrides (CI/agents): `KHOTAN_API_URL`, `KHOTAN_API_KEY`, `KHOTAN_PROFILE` take precedence over stored profiles.",
2414
+ "- Verify with `khotan whoami`.",
2415
+ "",
2416
+ "Never put an API key in a file committed to the repository. Generated MCP config contains only placeholders.",
2417
+ "",
2418
+ "## Output modes",
2419
+ "",
2420
+ "- Human-readable tables/detail by default; add `--json` for API-shaped JSON.",
2421
+ "- Results go to stdout; prompts, progress, and diagnostics go to stderr, so `khotan ... --json | jq` stays clean.",
2422
+ "",
2423
+ "## Safety model",
2424
+ "",
2425
+ "Each capability has a safety class:",
2426
+ "",
2427
+ "- `read` — safe; freely listable.",
2428
+ "- `write` — mutating; summarized before running.",
2429
+ "- `destructive` — deletes/irreversible; prompts for confirmation and refuses non-interactively unless you pass `--yes`.",
2430
+ "- `secret` — returns credential-bearing values (env reveal, connection strings, credential rotation); explicit-only and never printed in lists or exposed as MCP resources.",
2431
+ "",
2432
+ "## MCP server",
2433
+ "",
2434
+ "`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.",
2435
+ "",
2436
+ "## Command skills",
2437
+ "",
2438
+ "Per-domain skills cover the full, catalog-derived command surface:",
2439
+ "",
2440
+ ...indexLines,
2441
+ "",
2442
+ "Run `khotan help` or `khotan <command> --help` for the authoritative usage.",
2443
+ "",
2444
+ stamp(),
2445
+ ""
2446
+ ].join(`
2447
+ `);
2448
+ return { name: "khotan", content };
2449
+ }
2450
+ function generateSkills() {
2451
+ const domains = listDomains().filter((domain) => !OVERVIEW_DOMAINS.has(domain));
2452
+ return [overviewSkill(), ...domains.map(renderDomainSkill)];
2453
+ }
2454
+
2455
+ // src/cli/commands/init.ts
2456
+ var MCP_SERVER_KEY = "khotan";
2457
+ var SKILL_BASES = [".cursor", ".agents", ".claude"];
2458
+ function mcpServerEntry() {
2459
+ return {
2460
+ command: "khotan",
2461
+ args: ["mcp", "serve"],
2462
+ env: {
2463
+ KHOTAN_API_URL: "<your-khotan-api-url>",
2464
+ KHOTAN_API_KEY: "<your-khotan-api-key>"
2465
+ }
2466
+ };
2467
+ }
2468
+ var GUIDE_BODY = `This workspace is wired to Khotan through the \`khotan\` CLI and its MCP server.
2469
+ Use them to inspect and configure apps, pipelines, databases, files, folders, and
2470
+ context documents through the Khotan \`/api/v1\` surface.
2471
+
2472
+ ## Authentication (do this once — never commit secrets)
2473
+
2474
+ Khotan reads an organization-scoped API key from a stored profile or the
2475
+ environment:
2476
+
2477
+ - Profile (recommended): \`khotan auth set-key --api-url <url> --api-key <key>\`
2478
+ stores the key in an owner-only file outside the repository.
2479
+ - Environment: export \`KHOTAN_API_URL\` and \`KHOTAN_API_KEY\` in your shell or
2480
+ agent sandbox.
2481
+
2482
+ Never put an API key in a file committed to the repository. The generated MCP
2483
+ config contains only placeholders.
2484
+
2485
+ ## CLI surface
2486
+
2487
+ - \`khotan apps list|get|create|delete|redeploy\` and \`khotan apps env …\`
2488
+ - \`khotan pipelines …\`, \`khotan databases …\`
2489
+ - \`khotan files list|upload|download|…\`, \`khotan folders …\`
2490
+ - \`khotan context list|get|create|update|delete\`
2491
+ - Add \`--json\` to any command for machine-readable output.
2492
+
2493
+ Run \`khotan help\` or \`khotan <command> --help\` for the full, catalog-derived
2494
+ surface.
2495
+
2496
+ ## Safety
2497
+
2498
+ - Destructive commands (deletes, env unset) require confirmation; pass \`--yes\`
2499
+ only when you intend it.
2500
+ - Secret values (env reveal, database connection strings, credential rotation)
2501
+ are only available through explicit commands and are never printed in lists.
2502
+
2503
+ ## MCP
2504
+
2505
+ \`khotan mcp serve\` exposes the same operations as MCP tools and durable reads as
2506
+ MCP resources, reusing your stored profile or environment credentials.
2507
+ `;
2508
+ var CURSOR_RULES = `---
2509
+ description: Use the Khotan CLI and MCP tools to configure and operate this workspace's Khotan resources
2510
+ alwaysApply: false
2511
+ ---
2512
+
2513
+ # Khotan
2514
+
2515
+ ${GUIDE_BODY}`;
2516
+ var GENERIC_GUIDE = `# Khotan agent guide
2517
+
2518
+ ${GUIDE_BODY}`;
2519
+ function toRelative(cwd, absolutePath) {
2520
+ return relative(cwd, absolutePath) || absolutePath;
2521
+ }
2522
+ function writeTextAsset(ctx, absolutePath, content) {
2523
+ const path = toRelative(ctx.cwd, absolutePath);
2524
+ const existed = existsSync(absolutePath);
2525
+ if (existed && !ctx.force) {
2526
+ ctx.results.push({ path, action: "skipped" });
2527
+ return;
2528
+ }
2529
+ mkdirSync2(dirname2(absolutePath), { recursive: true });
2530
+ writeFileSync2(absolutePath, content);
2531
+ ctx.results.push({ path, action: existed ? "forced" : "created" });
2532
+ }
2533
+ function writeManagedAsset(ctx, absolutePath, content) {
2534
+ const path = toRelative(ctx.cwd, absolutePath);
2535
+ const existed = existsSync(absolutePath);
2536
+ mkdirSync2(dirname2(absolutePath), { recursive: true });
2537
+ writeFileSync2(absolutePath, content);
2538
+ ctx.results.push({ path, action: existed ? "updated" : "created" });
2539
+ }
2540
+ function writeMcpConfig(ctx, absolutePath) {
2541
+ const path = toRelative(ctx.cwd, absolutePath);
2542
+ let config = {};
2543
+ const existed = existsSync(absolutePath);
2544
+ if (existed) {
2545
+ try {
2546
+ config = JSON.parse(readFileSync2(absolutePath, "utf8"));
2547
+ } catch {
2548
+ ctx.results.push({ path, action: "skipped" });
2549
+ return;
2550
+ }
2551
+ }
2552
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
2553
+ config.mcpServers = {};
2554
+ }
2555
+ const servers = config.mcpServers;
2556
+ const hasKhotan = Object.prototype.hasOwnProperty.call(servers, MCP_SERVER_KEY);
2557
+ if (hasKhotan && !ctx.force) {
2558
+ ctx.results.push({ path, action: "skipped" });
2559
+ return;
2560
+ }
2561
+ servers[MCP_SERVER_KEY] = mcpServerEntry();
2562
+ mkdirSync2(dirname2(absolutePath), { recursive: true });
2563
+ writeFileSync2(absolutePath, `${JSON.stringify(config, null, 2)}
2564
+ `);
2565
+ ctx.results.push({
2566
+ path,
2567
+ action: existed ? hasKhotan ? "forced" : "updated" : "created"
2568
+ });
2569
+ }
2570
+ function runInit(options) {
2571
+ const { io, cwd, client, force, json } = options;
2572
+ const ctx = { cwd, force, results: [] };
2573
+ if (client === "cursor") {
2574
+ writeMcpConfig(ctx, join2(cwd, ".cursor", "mcp.json"));
2575
+ writeTextAsset(ctx, join2(cwd, ".cursor", "rules", "khotan.mdc"), CURSOR_RULES);
2576
+ } else {
2577
+ writeMcpConfig(ctx, join2(cwd, "mcp.json"));
2578
+ writeTextAsset(ctx, join2(cwd, "khotan-agents.md"), GENERIC_GUIDE);
2579
+ }
2580
+ for (const skill of generateSkills()) {
2581
+ for (const base of SKILL_BASES) {
2582
+ writeManagedAsset(ctx, join2(cwd, base, "skills", skill.name, "SKILL.md"), skill.content);
2583
+ }
2584
+ }
2585
+ if (json) {
2586
+ io.out(`${JSON.stringify({ client, actions: ctx.results }, null, 2)}
2587
+ `);
2588
+ return 0;
2589
+ }
2590
+ errLine(io, `Khotan workspace integration (${client}):`);
2591
+ for (const result of ctx.results) {
2592
+ errLine(io, ` ${result.action.padEnd(8)} ${result.path}`);
2593
+ }
2594
+ errLine(io);
2595
+ errLine(io, "Next steps:");
2596
+ errLine(io, " 1. Authenticate: khotan auth set-key --api-url <url> --api-key <key>");
2597
+ errLine(io, " 2. Open this workspace in your agent and ask it to configure Khotan.");
2598
+ return 0;
2599
+ }
2600
+
2601
+ // src/cli/confirm.ts
2602
+ init_src();
2603
+ init_io();
2604
+ async function ensureConfirmed(options) {
2605
+ const { io, yes, summary } = options;
2606
+ if (yes) {
2607
+ return;
2608
+ }
2609
+ if (!io.isTty) {
2610
+ throw new KhotanConfirmationRequiredError(`${summary} This is a destructive action; re-run with --yes to confirm in a non-interactive session.`);
2611
+ }
2612
+ errLine(io, summary);
2613
+ const answer = (await io.prompt("Type 'yes' to confirm: ")).trim().toLowerCase();
2614
+ if (answer !== "yes" && answer !== "y") {
2615
+ throw new KhotanConfirmationRequiredError("Aborted; no changes were made.");
2616
+ }
2617
+ }
2618
+
2400
2619
  // src/cli/run.ts
2401
2620
  init_io();
2402
2621
  init_session();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khotan/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Khotan CLI and MCP server: human- and agent-facing adapters over the Khotan /api/v1 surface.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",