@khotan/cli 0.6.0 → 0.9.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 +34 -1
  2. package/dist/khotan.js +921 -42
  3. package/package.json +4 -4
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-23", KHOTAN_ADAPTER_NAME = "khotan", KHOTAN_ADAPTER_VERSION = "0.6.0", SUPPORTED_CATALOG_SCHEMA_VERSIONS;
22
+ var CATALOG_SCHEMA_VERSION = 1, CATALOG_VERSION = "2026-06-28", KHOTAN_ADAPTER_NAME = "khotan", KHOTAN_ADAPTER_VERSION = "0.7.0", SUPPORTED_CATALOG_SCHEMA_VERSIONS;
23
23
  var init_version = __esm(() => {
24
24
  SUPPORTED_CATALOG_SCHEMA_VERSIONS = [
25
25
  CATALOG_SCHEMA_VERSION
@@ -322,6 +322,71 @@ function platformDomain(config) {
322
322
  };
323
323
  return [...operations, resource];
324
324
  }
325
+ function emailSubscriptionDomain(config) {
326
+ const { domain, noun, resourceParam, cap } = config;
327
+ const base = `/api/v1/${domain}/{${resourceParam}}/email-subscriptions`;
328
+ const idArg = idField(resourceParam, `The ${noun} id.`);
329
+ const subArg = idField("subscriptionId", "The email subscription id.");
330
+ const tool = (suffix) => `khotan_${domain}_email_subscriptions_${suffix}`;
331
+ return [
332
+ {
333
+ id: `${domain}.email-subscriptions.list`,
334
+ kind: "operation",
335
+ domain,
336
+ safety: "read",
337
+ auth: true,
338
+ title: `List ${noun} email subscriptions`,
339
+ description: `List the connected mailboxes this ${noun} watches.`,
340
+ input: [idArg],
341
+ http: { method: "GET", path: base, operationId: `list${cap}EmailSubscriptions` },
342
+ cli: { command: [domain, "email-subscriptions", "list"], summary: "List email subscriptions", defaultOutput: "table" },
343
+ mcp: { tool: { name: tool("list"), title: "List email subscriptions", description: `List the connected mailboxes this ${noun} watches.` } }
344
+ },
345
+ {
346
+ id: `${domain}.email-subscriptions.create`,
347
+ kind: "operation",
348
+ domain,
349
+ safety: "write",
350
+ auth: true,
351
+ title: `Subscribe ${article(noun)} ${noun} to a mailbox`,
352
+ description: `Watch a connected Gmail mailbox for this ${noun}; Google pushes notifications directly to the delivery URL.`,
353
+ input: [
354
+ idArg,
355
+ { name: "emailIntegrationId", location: "body", type: "string", required: true, description: "The connected Gmail integration id." },
356
+ { name: "deliveryUrl", location: "body", type: "string", required: true, description: "https URL on the target's primary hostname." }
357
+ ],
358
+ http: { method: "POST", path: base, operationId: `create${cap}EmailSubscription`, bodyMode: "json" },
359
+ cli: { command: [domain, "email-subscriptions", "create"], summary: "Subscribe to a mailbox", defaultOutput: "detail" },
360
+ mcp: { tool: { name: tool("create"), title: "Subscribe to a mailbox", description: `Watch a connected Gmail mailbox for this ${noun}.` } }
361
+ },
362
+ {
363
+ id: `${domain}.email-subscriptions.get`,
364
+ kind: "operation",
365
+ domain,
366
+ safety: "read",
367
+ auth: true,
368
+ title: `Get ${article(noun)} ${noun} email subscription`,
369
+ description: "Get one email subscription by id.",
370
+ input: [idArg, subArg],
371
+ http: { method: "GET", path: `${base}/{subscriptionId}`, operationId: `get${cap}EmailSubscription` },
372
+ cli: { command: [domain, "email-subscriptions", "get"], summary: "Get an email subscription", defaultOutput: "detail" },
373
+ mcp: { tool: { name: tool("get"), title: "Get an email subscription", description: "Get one email subscription by id." } }
374
+ },
375
+ {
376
+ id: `${domain}.email-subscriptions.delete`,
377
+ kind: "operation",
378
+ domain,
379
+ safety: "destructive",
380
+ auth: true,
381
+ title: `Unsubscribe ${article(noun)} ${noun} from a mailbox`,
382
+ description: "Remove an email subscription (deletes the Pub/Sub push subscription).",
383
+ input: [idArg, subArg],
384
+ http: { method: "DELETE", path: `${base}/{subscriptionId}`, operationId: `delete${cap}EmailSubscription` },
385
+ cli: { command: [domain, "email-subscriptions", "delete"], summary: "Unsubscribe from a mailbox" },
386
+ mcp: { tool: { name: tool("delete"), title: "Unsubscribe from a mailbox", description: "Remove an email subscription. Requires confirmation." } }
387
+ }
388
+ ];
389
+ }
325
390
  function getCapability(id) {
326
391
  return capabilitiesById.get(id);
327
392
  }
@@ -343,7 +408,7 @@ var idField = (name, description) => ({
343
408
  type: "string",
344
409
  required: true,
345
410
  description
346
- }), article = (noun) => /^[aeiou]/i.test(noun) ? "an" : "a", identityCapabilities, databaseCapabilities, fileCapabilities, folderCapabilities, contextCapabilities, appsCapabilities, pipelinesCapabilities, capabilities, khotanCatalog, capabilitiesById;
411
+ }), article = (noun) => /^[aeiou]/i.test(noun) ? "an" : "a", identityCapabilities, databaseCapabilities, redisCapabilities, fileCapabilities, folderCapabilities, contextCapabilities, appsCapabilities, pipelinesCapabilities, integrationsCapabilities, emailSubscriptionCapabilities, capabilities, khotanCatalog, capabilitiesById;
347
412
  var init_catalog = __esm(() => {
348
413
  init_version();
349
414
  identityCapabilities = [
@@ -582,6 +647,54 @@ var init_catalog = __esm(() => {
582
647
  cli: { command: ["databases", "branches", "rotate-credentials"], summary: "Rotate branch credentials (secret)", defaultOutput: "raw" },
583
648
  mcp: { tool: { name: "khotan_databases_branches_rotate_credentials", title: "Rotate branch credentials (secret)", description: "Rotate a branch's credentials and return the new connection. Secret-bearing." } }
584
649
  },
650
+ {
651
+ id: "databases.schema-promotions.plan",
652
+ kind: "operation",
653
+ domain: "databases",
654
+ safety: "write",
655
+ auth: true,
656
+ title: "Plan a database schema promotion",
657
+ description: "Generate and validate a schema-only promotion plan from a ready branch to the production database. Data is never copied.",
658
+ input: [
659
+ idField("databaseId", "The database id."),
660
+ idField("branchId", "The branch id.")
661
+ ],
662
+ http: { method: "POST", path: "/api/v1/databases/{databaseId}/branches/{branchId}/schema-promotions", operationId: "planDatabaseSchemaPromotion", bodyMode: "none" },
663
+ cli: { command: ["databases", "schema-promotions", "plan"], summary: "Plan a schema-only promotion", defaultOutput: "detail" },
664
+ mcp: { tool: { name: "khotan_databases_schema_promotions_plan", title: "Plan a schema promotion", description: "Generate and validate a schema-only promotion plan from a ready branch to production. Data is never copied." } }
665
+ },
666
+ {
667
+ id: "databases.schema-promotions.get",
668
+ kind: "operation",
669
+ domain: "databases",
670
+ safety: "read",
671
+ auth: true,
672
+ title: "Get a database schema promotion",
673
+ description: "Read one schema promotion record, including summary, blocked changes, failure state, and SQL.",
674
+ input: [
675
+ idField("databaseId", "The database id."),
676
+ idField("promotionId", "The schema promotion id.")
677
+ ],
678
+ http: { method: "GET", path: "/api/v1/databases/{databaseId}/schema-promotions/{promotionId}", operationId: "getDatabaseSchemaPromotion" },
679
+ cli: { command: ["databases", "schema-promotions", "get"], summary: "Get a schema promotion", defaultOutput: "detail" },
680
+ mcp: { tool: { name: "khotan_databases_schema_promotions_get", title: "Get a schema promotion", description: "Read one schema promotion record, including generated SQL." } }
681
+ },
682
+ {
683
+ id: "databases.schema-promotions.apply",
684
+ kind: "operation",
685
+ domain: "databases",
686
+ safety: "destructive",
687
+ auth: true,
688
+ title: "Apply a database schema promotion",
689
+ description: "Apply the exact reviewed schema-only promotion SQL to the production database. Fails if branch or production schema hashes changed after planning.",
690
+ input: [
691
+ idField("databaseId", "The database id."),
692
+ idField("promotionId", "The schema promotion id.")
693
+ ],
694
+ http: { method: "POST", path: "/api/v1/databases/{databaseId}/schema-promotions/{promotionId}/apply", operationId: "applyDatabaseSchemaPromotion", bodyMode: "none" },
695
+ cli: { command: ["databases", "schema-promotions", "apply"], summary: "Apply a schema promotion" },
696
+ mcp: { tool: { name: "khotan_databases_schema_promotions_apply", title: "Apply a schema promotion", description: "Apply reviewed schema-only SQL to production. Requires confirmation." } }
697
+ },
585
698
  {
586
699
  id: "databases.branches.resource",
587
700
  kind: "resource",
@@ -607,6 +720,84 @@ var init_catalog = __esm(() => {
607
720
  }
608
721
  }
609
722
  ];
723
+ redisCapabilities = [
724
+ {
725
+ id: "redis.list",
726
+ kind: "operation",
727
+ domain: "redis",
728
+ safety: "read",
729
+ auth: true,
730
+ title: "List Redis databases",
731
+ description: "List the organization's Redis (KV) databases (no credentials).",
732
+ input: [],
733
+ http: { method: "GET", path: "/api/v1/redis-databases", operationId: "listRedisDatabases" },
734
+ cli: { command: ["redis", "list"], summary: "List Redis (KV) databases", defaultOutput: "table" },
735
+ mcp: { tool: { name: "khotan_redis_list", title: "List Redis databases", description: "List the organization's Redis (KV) databases (no credentials)." } }
736
+ },
737
+ {
738
+ id: "redis.get",
739
+ kind: "operation",
740
+ domain: "redis",
741
+ safety: "read",
742
+ auth: true,
743
+ title: "Get a Redis database",
744
+ description: "Get one Redis (KV) database by id (no credentials).",
745
+ input: [idField("databaseId", "The Redis database id.")],
746
+ http: { method: "GET", path: "/api/v1/redis-databases/{databaseId}", operationId: "getRedisDatabase" },
747
+ cli: { command: ["redis", "get"], summary: "Get a Redis (KV) database", defaultOutput: "detail" },
748
+ mcp: { tool: { name: "khotan_redis_get", title: "Get a Redis database", description: "Get one Redis (KV) database by id (no credentials)." } }
749
+ },
750
+ {
751
+ id: "redis.create",
752
+ kind: "operation",
753
+ domain: "redis",
754
+ safety: "write",
755
+ auth: true,
756
+ title: "Create a Redis database",
757
+ description: "Provision a global Upstash Redis (KV) database. Not idempotent: retrying a succeeded create provisions a second database. Omit primaryRegion for the default region.",
758
+ input: [
759
+ { name: "name", location: "body", type: "string", required: true, description: "The database name." },
760
+ { name: "primaryRegion", location: "body", type: "string", description: "Optional primary region id; omit for the default region." }
761
+ ],
762
+ http: { method: "POST", path: "/api/v1/redis-databases", operationId: "createRedisDatabase", bodyMode: "json" },
763
+ cli: { command: ["redis", "create"], summary: "Create a Redis (KV) database", defaultOutput: "detail" },
764
+ mcp: { tool: { name: "khotan_redis_create", title: "Create a Redis database", description: "Provision a global Upstash Redis (KV) database. Not idempotent — retrying provisions a second database." } }
765
+ },
766
+ {
767
+ id: "redis.delete",
768
+ kind: "operation",
769
+ domain: "redis",
770
+ safety: "destructive",
771
+ auth: true,
772
+ title: "Delete a Redis database",
773
+ description: "Permanently delete a Redis (KV) database and tear down its Upstash resources. Fails if deletion protection is enabled; remove protection from the dashboard first.",
774
+ input: [idField("databaseId", "The Redis database id.")],
775
+ http: { method: "DELETE", path: "/api/v1/redis-databases/{databaseId}", operationId: "deleteRedisDatabase" },
776
+ cli: { command: ["redis", "delete"], summary: "Delete a Redis (KV) database" },
777
+ mcp: { tool: { name: "khotan_redis_delete", title: "Delete a Redis database", description: "Permanently delete a Redis (KV) database. Requires confirmation; fails if deletion protection is enabled." } }
778
+ },
779
+ {
780
+ id: "redis.resource",
781
+ kind: "resource",
782
+ domain: "redis",
783
+ safety: "read",
784
+ auth: true,
785
+ title: "Redis database summary",
786
+ description: "Credential-free Redis (KV) database summary addressed by id.",
787
+ input: [idField("databaseId", "The Redis database id.")],
788
+ readsVia: "redis.get",
789
+ http: { method: "GET", path: "/api/v1/redis-databases/{databaseId}", operationId: "getRedisDatabase" },
790
+ mcp: {
791
+ resource: {
792
+ uriTemplate: "khotan://redis/{databaseId}",
793
+ name: "redis-database-summary",
794
+ title: "Redis database summary",
795
+ description: "Credential-free Redis (KV) database summary as JSON.",
796
+ mimeType: "application/json"
797
+ }
798
+ }
799
+ }
800
+ ];
610
801
  fileCapabilities = [
611
802
  {
612
803
  id: "files.list",
@@ -715,6 +906,19 @@ var init_catalog = __esm(() => {
715
906
  cli: { command: ["files", "download-url"], summary: "Get a file download URL", defaultOutput: "detail" },
716
907
  mcp: { tool: { name: "khotan_files_download", title: "Get a file download URL", description: "Return a short-lived presigned GET URL for a ready file." } }
717
908
  },
909
+ {
910
+ id: "files.preview",
911
+ kind: "operation",
912
+ domain: "files",
913
+ safety: "read",
914
+ auth: true,
915
+ title: "Get a file preview URL",
916
+ description: "Return a short-lived presigned inline GET URL for a ready, preview-eligible file (images and PDFs).",
917
+ input: [idField("fileId", "The file id.")],
918
+ http: { method: "GET", path: "/api/v1/files/{fileId}/preview", operationId: "getFilePreviewUrl" },
919
+ cli: { command: ["files", "preview-url"], summary: "Get a file preview URL", defaultOutput: "detail" },
920
+ mcp: { tool: { name: "khotan_files_preview", title: "Get a file preview URL", description: "Return a short-lived presigned inline GET URL for a ready, preview-eligible file (images and PDFs)." } }
921
+ },
718
922
  {
719
923
  id: "files.resource",
720
924
  kind: "resource",
@@ -973,14 +1177,127 @@ var init_catalog = __esm(() => {
973
1177
  envDelete: "deletePipelineEnvVar"
974
1178
  }
975
1179
  });
1180
+ integrationsCapabilities = [
1181
+ {
1182
+ id: "integrations.list",
1183
+ kind: "operation",
1184
+ domain: "integrations",
1185
+ safety: "read",
1186
+ auth: true,
1187
+ title: "List integrations",
1188
+ description: "List the organization's connected mailbox integrations (no token material).",
1189
+ input: [],
1190
+ http: { method: "GET", path: "/api/v1/integrations", operationId: "listIntegrations" },
1191
+ cli: { command: ["integrations", "list"], summary: "List integrations", defaultOutput: "table" },
1192
+ mcp: { tool: { name: "khotan_integrations_list", title: "List integrations", description: "List the organization's connected mailbox integrations (no token material)." } }
1193
+ },
1194
+ {
1195
+ id: "integrations.get",
1196
+ kind: "operation",
1197
+ domain: "integrations",
1198
+ safety: "read",
1199
+ auth: true,
1200
+ title: "Get an integration",
1201
+ description: "Get one integration by id (no token material).",
1202
+ input: [idField("integrationId", "The integration id.")],
1203
+ http: { method: "GET", path: "/api/v1/integrations/{integrationId}", operationId: "getIntegration" },
1204
+ cli: { command: ["integrations", "get"], summary: "Get an integration", defaultOutput: "detail" },
1205
+ mcp: { tool: { name: "khotan_integrations_get", title: "Get an integration", description: "Get one integration by id (no token material)." } }
1206
+ },
1207
+ {
1208
+ id: "integrations.connect",
1209
+ kind: "operation",
1210
+ domain: "integrations",
1211
+ safety: "write",
1212
+ auth: true,
1213
+ title: "Start connecting an integration",
1214
+ description: "Begin a mailbox connection and return a URL a human opens in a browser to authorize (OAuth consent cannot be completed headlessly).",
1215
+ input: [
1216
+ { name: "provider", location: "body", type: "string", description: "The provider to connect. Defaults to google.", default: "google" }
1217
+ ],
1218
+ http: { method: "POST", path: "/api/v1/integrations", operationId: "connectIntegration", bodyMode: "json" },
1219
+ cli: { command: ["integrations", "connect"], summary: "Start connecting a mailbox", defaultOutput: "detail" },
1220
+ mcp: { tool: { name: "khotan_integrations_connect", title: "Start connecting an integration", description: "Begin a mailbox connection and return a browser URL to complete OAuth consent." } }
1221
+ },
1222
+ {
1223
+ id: "integrations.rename",
1224
+ kind: "operation",
1225
+ domain: "integrations",
1226
+ safety: "write",
1227
+ auth: true,
1228
+ title: "Rename an integration",
1229
+ description: "Set an integration's display name. Pass an empty value to clear it.",
1230
+ input: [
1231
+ idField("integrationId", "The integration id."),
1232
+ { name: "name", location: "body", type: "string", required: true, description: "The display name (empty to clear)." }
1233
+ ],
1234
+ http: { method: "PATCH", path: "/api/v1/integrations/{integrationId}", operationId: "updateIntegration", bodyMode: "json" },
1235
+ cli: { command: ["integrations", "rename"], summary: "Rename an integration", defaultOutput: "detail" },
1236
+ mcp: { tool: { name: "khotan_integrations_rename", title: "Rename an integration", description: "Set an integration's display name." } }
1237
+ },
1238
+ {
1239
+ id: "integrations.delete",
1240
+ kind: "operation",
1241
+ domain: "integrations",
1242
+ safety: "destructive",
1243
+ auth: true,
1244
+ title: "Disconnect an integration",
1245
+ description: "Permanently disconnect a mailbox integration (revokes the stored credentials).",
1246
+ input: [idField("integrationId", "The integration id.")],
1247
+ http: { method: "DELETE", path: "/api/v1/integrations/{integrationId}", operationId: "deleteIntegration" },
1248
+ cli: { command: ["integrations", "delete"], summary: "Disconnect an integration" },
1249
+ mcp: { tool: { name: "khotan_integrations_delete", title: "Disconnect an integration", description: "Permanently disconnect a mailbox integration. Requires confirmation." } }
1250
+ },
1251
+ {
1252
+ id: "integrations.token",
1253
+ kind: "operation",
1254
+ domain: "integrations",
1255
+ safety: "secret",
1256
+ auth: true,
1257
+ title: "Mint an access token (secret)",
1258
+ description: "Mint a fresh OAuth access token for the integration so a service can call the provider on the user's behalf. Secret-bearing; explicit-only.",
1259
+ input: [idField("integrationId", "The integration id.")],
1260
+ http: { method: "POST", path: "/api/v1/integrations/{integrationId}/token", operationId: "mintIntegrationToken", bodyMode: "none" },
1261
+ cli: { command: ["integrations", "token"], summary: "Mint an access token (secret)", defaultOutput: "detail" },
1262
+ mcp: { tool: { name: "khotan_integrations_token", title: "Mint an access token (secret)", description: "Mint a fresh OAuth access token for the integration. Secret-bearing — returns a credential." } }
1263
+ },
1264
+ {
1265
+ id: "integrations.resource",
1266
+ kind: "resource",
1267
+ domain: "integrations",
1268
+ safety: "read",
1269
+ auth: true,
1270
+ title: "Integration summary",
1271
+ description: "Credential-free integration summary addressed by id.",
1272
+ input: [idField("integrationId", "The integration id.")],
1273
+ readsVia: "integrations.get",
1274
+ http: { method: "GET", path: "/api/v1/integrations/{integrationId}", operationId: "getIntegration" },
1275
+ mcp: {
1276
+ resource: {
1277
+ uriTemplate: "khotan://integrations/{integrationId}",
1278
+ name: "integration-summary",
1279
+ title: "Integration summary",
1280
+ description: "Credential-free integration summary as JSON.",
1281
+ mimeType: "application/json"
1282
+ }
1283
+ }
1284
+ }
1285
+ ];
1286
+ emailSubscriptionCapabilities = [
1287
+ ...emailSubscriptionDomain({ domain: "apps", noun: "deployment", resourceParam: "appId", cap: "App" }),
1288
+ ...emailSubscriptionDomain({ domain: "pipelines", noun: "pipeline", resourceParam: "pipelineId", cap: "Pipeline" })
1289
+ ];
976
1290
  capabilities = [
977
1291
  ...identityCapabilities,
978
1292
  ...appsCapabilities,
979
1293
  ...pipelinesCapabilities,
980
1294
  ...databaseCapabilities,
1295
+ ...redisCapabilities,
981
1296
  ...fileCapabilities,
982
1297
  ...folderCapabilities,
983
- ...contextCapabilities
1298
+ ...contextCapabilities,
1299
+ ...integrationsCapabilities,
1300
+ ...emailSubscriptionCapabilities
984
1301
  ];
985
1302
  khotanCatalog = {
986
1303
  schemaVersion: CATALOG_SCHEMA_VERSION,
@@ -1341,6 +1658,66 @@ var init_profiles = __esm(() => {
1341
1658
  init_errors();
1342
1659
  });
1343
1660
 
1661
+ // ../khotan-core/src/profiles/repo-env.ts
1662
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1663
+ import { dirname as dirname2, join as join2 } from "node:path";
1664
+ function findRepoEnvFile(startDir) {
1665
+ let dir = startDir;
1666
+ for (;; ) {
1667
+ const candidate = join2(dir, REPO_ENV_FILENAME);
1668
+ if (existsSync(candidate))
1669
+ return candidate;
1670
+ if (existsSync(join2(dir, ".git")))
1671
+ return;
1672
+ const parent = dirname2(dir);
1673
+ if (parent === dir)
1674
+ return;
1675
+ dir = parent;
1676
+ }
1677
+ }
1678
+ function parseRepoEnvFile(contents) {
1679
+ const map = new Map;
1680
+ for (const rawLine of contents.split(`
1681
+ `)) {
1682
+ const line = rawLine.trim();
1683
+ if (!line || line.startsWith("#"))
1684
+ continue;
1685
+ const eq = line.indexOf("=");
1686
+ if (eq === -1)
1687
+ continue;
1688
+ const key = line.slice(0, eq).trim();
1689
+ let value = line.slice(eq + 1).trim();
1690
+ if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) {
1691
+ value = value.slice(1, -1);
1692
+ }
1693
+ map.set(key, value);
1694
+ }
1695
+ return map;
1696
+ }
1697
+ function overlayRepoEnv(env, options) {
1698
+ const filePath = findRepoEnvFile(options.cwd);
1699
+ if (!filePath)
1700
+ return { env };
1701
+ let parsed;
1702
+ try {
1703
+ parsed = parseRepoEnvFile(readFileSync2(filePath, "utf8"));
1704
+ } catch {
1705
+ return { env, filePath };
1706
+ }
1707
+ const merged = { ...env };
1708
+ for (const key of OVERLAY_KEYS) {
1709
+ const value = parsed.get(key)?.trim();
1710
+ if (value)
1711
+ merged[key] = value;
1712
+ }
1713
+ return { env: merged, filePath, orgId: merged[ENV_ORG_ID]?.trim() || undefined };
1714
+ }
1715
+ var ENV_ORG_ID = "KHOTAN_ORG_ID", REPO_ENV_FILENAME = "env.khotan.local", OVERLAY_KEYS;
1716
+ var init_repo_env = __esm(() => {
1717
+ init_profiles();
1718
+ OVERLAY_KEYS = [ENV_API_URL, ENV_API_KEY, ENV_PROFILE, ENV_ORG_ID];
1719
+ });
1720
+
1344
1721
  // ../khotan-core/src/index.ts
1345
1722
  var exports_src = {};
1346
1723
  __export(exports_src, {
@@ -1351,6 +1728,8 @@ __export(exports_src, {
1351
1728
  saveProfileStore: () => saveProfileStore,
1352
1729
  resolveProfile: () => resolveProfile,
1353
1730
  requiresConfirmation: () => requiresConfirmation,
1731
+ parseRepoEnvFile: () => parseRepoEnvFile,
1732
+ overlayRepoEnv: () => overlayRepoEnv,
1354
1733
  loadProfileStore: () => loadProfileStore,
1355
1734
  listDomains: () => listDomains,
1356
1735
  listCapabilities: () => listCapabilities,
@@ -1364,12 +1743,14 @@ __export(exports_src, {
1364
1743
  indexOpenApiOperations: () => indexOpenApiOperations,
1365
1744
  getProfileStorePath: () => getProfileStorePath,
1366
1745
  getCapability: () => getCapability,
1746
+ findRepoEnvFile: () => findRepoEnvFile,
1367
1747
  createApiClient: () => createApiClient,
1368
1748
  capabilitiesByOperationId: () => capabilitiesByOperationId,
1369
1749
  buildHttpRequest: () => buildHttpRequest,
1370
1750
  assertSupportedCatalogSchemaVersion: () => assertSupportedCatalogSchemaVersion,
1371
1751
  SUPPORTED_CATALOG_SCHEMA_VERSIONS: () => SUPPORTED_CATALOG_SCHEMA_VERSIONS,
1372
1752
  SAFETY_LEVELS: () => SAFETY_LEVELS,
1753
+ REPO_ENV_FILENAME: () => REPO_ENV_FILENAME,
1373
1754
  KhotanUnknownCapabilityError: () => KhotanUnknownCapabilityError,
1374
1755
  KhotanError: () => KhotanError,
1375
1756
  KhotanConfirmationRequiredError: () => KhotanConfirmationRequiredError,
@@ -1379,6 +1760,7 @@ __export(exports_src, {
1379
1760
  KHOTAN_ADAPTER_VERSION: () => KHOTAN_ADAPTER_VERSION,
1380
1761
  KHOTAN_ADAPTER_NAME: () => KHOTAN_ADAPTER_NAME,
1381
1762
  ENV_PROFILE: () => ENV_PROFILE,
1763
+ ENV_ORG_ID: () => ENV_ORG_ID,
1382
1764
  ENV_API_URL: () => ENV_API_URL,
1383
1765
  ENV_API_KEY: () => ENV_API_KEY,
1384
1766
  CATALOG_VERSION: () => CATALOG_VERSION,
@@ -1392,6 +1774,7 @@ var init_src = __esm(() => {
1392
1774
  init_validate();
1393
1775
  init_api_client();
1394
1776
  init_profiles();
1777
+ init_repo_env();
1395
1778
  });
1396
1779
 
1397
1780
  // src/cli/io.ts
@@ -1428,6 +1811,65 @@ function errLine(io, line = "") {
1428
1811
  }
1429
1812
  var init_io = () => {};
1430
1813
 
1814
+ // src/cli/assert-org.ts
1815
+ import { createHash } from "node:crypto";
1816
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1817
+ import { homedir as homedir2 } from "node:os";
1818
+ import { dirname as dirname4, join as join5 } from "node:path";
1819
+ function cacheDir(env) {
1820
+ const xdg = env.XDG_CACHE_HOME;
1821
+ const base = xdg && xdg.trim() ? xdg : join5(homedir2(), ".cache");
1822
+ return join5(base, "khotan");
1823
+ }
1824
+ function cacheFilePath(env, apiUrl, orgId) {
1825
+ const hash = createHash("sha256").update(`${apiUrl}
1826
+ ${orgId}`).digest("hex").slice(0, 32);
1827
+ return join5(cacheDir(env), `org-assert-${hash}.json`);
1828
+ }
1829
+ function readAssertionCache(path, expectedOrgId) {
1830
+ if (!existsSync3(path))
1831
+ return false;
1832
+ try {
1833
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
1834
+ if (parsed.organizationId !== expectedOrgId)
1835
+ return false;
1836
+ if (Date.now() - parsed.verifiedAt > CACHE_TTL_MS)
1837
+ return false;
1838
+ return true;
1839
+ } catch {
1840
+ return false;
1841
+ }
1842
+ }
1843
+ function writeAssertionCache(path, organizationId) {
1844
+ try {
1845
+ mkdirSync3(dirname4(path), { recursive: true });
1846
+ const payload = { organizationId, verifiedAt: Date.now() };
1847
+ writeFileSync4(path, `${JSON.stringify(payload)}
1848
+ `);
1849
+ } catch {}
1850
+ }
1851
+ async function assertOrg(session, expectedOrgId, options = {}) {
1852
+ const expected = expectedOrgId.trim();
1853
+ if (!expected)
1854
+ return;
1855
+ const env = options.env ?? process.env;
1856
+ const cachePath = cacheFilePath(env, session.apiUrl, expected);
1857
+ if (!options.skipCache && readAssertionCache(cachePath, expected)) {
1858
+ return;
1859
+ }
1860
+ const principal = await session.client.execute("identity.whoami");
1861
+ const actual = String(principal.organizationId ?? "");
1862
+ if (actual !== expected) {
1863
+ throw new KhotanClientError("org_mismatch", `Khotan org mismatch: expected organizationId=${expected} (KHOTAN_ORG_ID / --assert-org), ` + `but whoami returned organizationId=${actual || "(empty)"}. ` + `You may be authenticated to a different customer's org. ` + `Fix credentials in env.khotan.local, pass --assert-org, or select a per-customer profile.`);
1864
+ }
1865
+ writeAssertionCache(cachePath, actual);
1866
+ }
1867
+ var CACHE_TTL_MS;
1868
+ var init_assert_org = __esm(() => {
1869
+ init_src();
1870
+ CACHE_TTL_MS = 5 * 60 * 1000;
1871
+ });
1872
+
1431
1873
  // src/cli/session.ts
1432
1874
  function resolveSession(options) {
1433
1875
  const resolved = resolveProfile({
@@ -1447,8 +1889,17 @@ function resolveSession(options) {
1447
1889
  apiKey
1448
1890
  };
1449
1891
  }
1892
+ async function resolveSessionAsserted(options) {
1893
+ const session = resolveSession(options);
1894
+ const expectedOrgId = options.assertOrgOverride?.trim() || options.env[ENV_ORG_ID]?.trim();
1895
+ if (expectedOrgId) {
1896
+ await assertOrg(session, expectedOrgId, { env: options.env });
1897
+ }
1898
+ return session;
1899
+ }
1450
1900
  var init_session = __esm(() => {
1451
1901
  init_src();
1902
+ init_assert_org();
1452
1903
  });
1453
1904
 
1454
1905
  // src/mcp/protocol.ts
@@ -1699,13 +2150,14 @@ __export(exports_serve, {
1699
2150
  import { createInterface as createInterface2 } from "node:readline";
1700
2151
  async function serveMcp(deps) {
1701
2152
  assertSupportedCatalogSchemaVersion();
1702
- const session = resolveSession({
2153
+ const session = await resolveSessionAsserted({
1703
2154
  env: deps.env,
1704
2155
  storeOptions: deps.storeOptions,
1705
2156
  profileName: deps.profileName,
1706
2157
  apiUrlOverride: deps.apiUrlOverride,
1707
2158
  apiKeyOverride: deps.apiKeyOverride,
1708
- fetch: deps.fetch
2159
+ fetch: deps.fetch,
2160
+ assertOrgOverride: deps.assertOrgOverride
1709
2161
  });
1710
2162
  if (!session.apiKey) {
1711
2163
  throw new KhotanClientError("not_authenticated", "The MCP server needs an API key. Run `khotan auth set-key` or set KHOTAN_API_KEY.");
@@ -2079,8 +2531,8 @@ async function downloadFile(deps, options) {
2079
2531
 
2080
2532
  // src/cli/commands/init.ts
2081
2533
  init_io();
2082
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
2083
- import { dirname as dirname2, join as join2, relative } from "node:path";
2534
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
2535
+ import { dirname as dirname3, join as join3, relative } from "node:path";
2084
2536
 
2085
2537
  // src/cli/skills.ts
2086
2538
  init_src();
@@ -2187,9 +2639,11 @@ var DOMAIN_TITLES = {
2187
2639
  apps: "Apps",
2188
2640
  pipelines: "Pipelines",
2189
2641
  databases: "Databases",
2642
+ redis: "Redis (KV)",
2190
2643
  files: "Files",
2191
2644
  folders: "Folders",
2192
- context: "Context documents"
2645
+ context: "Context documents",
2646
+ integrations: "Integrations"
2193
2647
  };
2194
2648
  function commandWords(capability) {
2195
2649
  return capability.cli?.command.join(" ");
@@ -2255,6 +2709,8 @@ function printTopLevelHelp(io) {
2255
2709
  outLine(io, " files download <fileId> Download a file to --output (presigned flow)");
2256
2710
  outLine(io);
2257
2711
  outLine(io, "Workspace:");
2712
+ outLine(io, " status Show the workspace topology (apps, pipelines, databases)");
2713
+ outLine(io, " status --write Refresh a committable WORKSPACE.md from live data");
2258
2714
  outLine(io, " init Scaffold MCP config + agent guidance into this workspace");
2259
2715
  outLine(io);
2260
2716
  outLine(io, "Agent:");
@@ -2266,10 +2722,33 @@ function printTopLevelHelp(io) {
2266
2722
  outLine(io, " --profile <name> Use a specific stored profile");
2267
2723
  outLine(io, " --api-url <url> Override the API origin");
2268
2724
  outLine(io, " --api-key <key> Override the API key");
2725
+ outLine(io, " --assert-org <id> Require whoami.organizationId to equal <id> (fail closed)");
2269
2726
  outLine(io, " --help Show help for a command");
2270
2727
  outLine(io);
2271
2728
  outLine(io, `khotan v${KHOTAN_ADAPTER_VERSION}`);
2272
2729
  }
2730
+ function groupCommands(prefix) {
2731
+ if (prefix.length === 0) {
2732
+ return [];
2733
+ }
2734
+ return listCapabilities().filter((capability) => {
2735
+ if (!isOperationCapability(capability) || !capability.cli) {
2736
+ return false;
2737
+ }
2738
+ const words = capability.cli.command;
2739
+ return words.length > prefix.length && prefix.every((word, index) => words[index] === word);
2740
+ });
2741
+ }
2742
+ function printGroupHelp(io, prefix, commands) {
2743
+ outLine(io, `khotan ${prefix.join(" ")} — commands`);
2744
+ outLine(io);
2745
+ for (const capability of commands) {
2746
+ const words = commandWords(capability) ?? capability.id;
2747
+ outLine(io, ` ${words.padEnd(30)} ${capability.cli?.summary ?? capability.title}`);
2748
+ }
2749
+ outLine(io);
2750
+ outLine(io, "Run `khotan <command> --help` for details.");
2751
+ }
2273
2752
  function printCommandHelp(io, capability) {
2274
2753
  outLine(io, capability.title);
2275
2754
  outLine(io);
@@ -2302,6 +2781,7 @@ var DOMAIN_TITLES2 = {
2302
2781
  apps: "Apps",
2303
2782
  pipelines: "Pipelines",
2304
2783
  databases: "Databases",
2784
+ redis: "Redis (KV)",
2305
2785
  files: "Files",
2306
2786
  folders: "Folders",
2307
2787
  context: "Context documents"
@@ -2310,6 +2790,7 @@ var DOMAIN_LABELS = {
2310
2790
  apps: "apps",
2311
2791
  pipelines: "pipelines",
2312
2792
  databases: "databases and database branches",
2793
+ redis: "Redis (KV) databases",
2313
2794
  files: "files",
2314
2795
  folders: "folders",
2315
2796
  context: "context documents"
@@ -2484,32 +2965,54 @@ var SKILL_BASES = [".cursor", ".agents", ".claude"];
2484
2965
  function mcpServerEntry() {
2485
2966
  return {
2486
2967
  command: "khotan",
2487
- args: ["mcp", "serve"],
2488
- env: {
2489
- KHOTAN_API_URL: "<your-khotan-api-url>",
2490
- KHOTAN_API_KEY: "<your-khotan-api-key>"
2491
- }
2968
+ args: ["mcp", "serve"]
2492
2969
  };
2493
2970
  }
2494
2971
  var GUIDE_BODY = `This workspace is wired to Khotan through the \`khotan\` CLI and its MCP server.
2495
2972
  Use them to inspect and configure apps, pipelines, databases, files, folders, and
2496
2973
  context documents through the Khotan \`/api/v1\` surface.
2497
2974
 
2975
+ ## Org isolation (never cross customers)
2976
+
2977
+ Khotan credentials are machine-global unless scoped per repo. Pin this repo to
2978
+ its organization with \`KHOTAN_ORG_ID\` in \`env.khotan.local\`: the CLI and MCP
2979
+ server load that file and assert \`whoami.organizationId === KHOTAN_ORG_ID\`
2980
+ before trusting any output, failing closed on mismatch. You can also pass
2981
+ \`--assert-org <id>\` on any command.
2982
+
2983
+ If \`apps list\` / \`context list\` returns another customer's data (foreign product
2984
+ names, wrong org slug), **stop** — credentials are wrong. Do not infer
2985
+ deployment or env from CLI output until the org assertion passes.
2986
+
2498
2987
  ## Authentication (do this once — never commit secrets)
2499
2988
 
2500
- Khotan reads an organization-scoped API key from a stored profile or the
2501
- environment:
2989
+ Repo credentials live in \`env.khotan.local\` (\`KHOTAN_API_URL\`, \`KHOTAN_API_KEY\`,
2990
+ \`KHOTAN_ORG_ID\`), which the CLI auto-loads. Alternatively:
2502
2991
 
2503
- - Profile (recommended): \`khotan auth set-key --api-url <url> --api-key <key>\`
2504
- stores the key in an owner-only file outside the repository.
2505
- - Environment: export \`KHOTAN_API_URL\` and \`KHOTAN_API_KEY\` in your shell or
2506
- agent sandbox.
2992
+ - Profile: \`khotan auth set-key --api-url <url> --api-key <key>\` stores the key in
2993
+ an owner-only file outside the repository (use a per-customer profile name, not
2994
+ \`default\`).
2995
+ - Environment: export \`KHOTAN_API_URL\`, \`KHOTAN_API_KEY\`, and \`KHOTAN_ORG_ID\`.
2507
2996
 
2508
2997
  Never put an API key in a file committed to the repository. The generated MCP
2509
- config contains only placeholders.
2998
+ config carries no secrets — the server resolves credentials at startup.
2999
+
3000
+ ## Workspace layout
3001
+
3002
+ A Khotan workspace is an **organization mono-root repo**: one GitHub repo whose
3003
+ top-level subdirectories are each an independent deployable (an app or a
3004
+ pipeline). The repo root holds shared agent skills and instruction files;
3005
+ per-app guidance lives in each subdirectory's own \`AGENTS.md\` / \`CLAUDE.md\`.
3006
+
3007
+ - Run \`khotan status\` to see which subdirectory maps to which app/pipeline and
3008
+ database, and which repo it deploys from. \`khotan status --write\` refreshes a
3009
+ committable \`WORKSPACE.md\` with that map.
3010
+ - Each app lives in its own subdirectory — **do not run app scaffolders (e.g.
3011
+ \`khotan-data init\`) at the repo root.** \`cd\` into the app's subdirectory first.
2510
3012
 
2511
3013
  ## CLI surface
2512
3014
 
3015
+ - \`khotan status\` — the workspace topology (apps, pipelines, databases) at a glance
2513
3016
  - \`khotan apps list|get|create|delete|redeploy\` and \`khotan apps env …\`
2514
3017
  - \`khotan pipelines …\`, \`khotan databases …\`
2515
3018
  - \`khotan files list|upload|download|…\`, \`khotan folders …\`
@@ -2529,7 +3032,9 @@ surface.
2529
3032
  ## MCP
2530
3033
 
2531
3034
  \`khotan mcp serve\` exposes the same operations as MCP tools and durable reads as
2532
- MCP resources, reusing your stored profile or environment credentials.
3035
+ MCP resources, scoping credentials from \`env.khotan.local\` (or your stored
3036
+ profile) and asserting the org at startup. Restart the MCP server after changing
3037
+ \`env.khotan.local\`.
2533
3038
  `;
2534
3039
  var CURSOR_RULES = `---
2535
3040
  description: Use the Khotan CLI and MCP tools to configure and operate this workspace's Khotan resources
@@ -2545,31 +3050,59 @@ ${GUIDE_BODY}`;
2545
3050
  function toRelative(cwd, absolutePath) {
2546
3051
  return relative(cwd, absolutePath) || absolutePath;
2547
3052
  }
3053
+ function detectMonoRoot(cwd) {
3054
+ if (hasWorkspacesManifest(join3(cwd, "package.json")))
3055
+ return true;
3056
+ if (existsSync2(join3(cwd, "turbo.json")))
3057
+ return true;
3058
+ if (existsSync2(join3(cwd, "WORKSPACE.md")))
3059
+ return true;
3060
+ if (existsSync2(join3(cwd, "package.json")))
3061
+ return false;
3062
+ try {
3063
+ return readdirSync(cwd, { withFileTypes: true }).some((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules" && existsSync2(join3(cwd, entry.name, "package.json")));
3064
+ } catch {
3065
+ return false;
3066
+ }
3067
+ }
3068
+ function hasWorkspacesManifest(packageJsonPath) {
3069
+ if (!existsSync2(packageJsonPath))
3070
+ return false;
3071
+ try {
3072
+ const parsed = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
3073
+ if (typeof parsed !== "object" || parsed === null)
3074
+ return false;
3075
+ const workspaces = parsed.workspaces;
3076
+ return Array.isArray(workspaces) && workspaces.length > 0;
3077
+ } catch {
3078
+ return false;
3079
+ }
3080
+ }
2548
3081
  function writeTextAsset(ctx, absolutePath, content) {
2549
3082
  const path = toRelative(ctx.cwd, absolutePath);
2550
- const existed = existsSync(absolutePath);
3083
+ const existed = existsSync2(absolutePath);
2551
3084
  if (existed && !ctx.force) {
2552
3085
  ctx.results.push({ path, action: "skipped" });
2553
3086
  return;
2554
3087
  }
2555
- mkdirSync2(dirname2(absolutePath), { recursive: true });
3088
+ mkdirSync2(dirname3(absolutePath), { recursive: true });
2556
3089
  writeFileSync2(absolutePath, content);
2557
3090
  ctx.results.push({ path, action: existed ? "forced" : "created" });
2558
3091
  }
2559
3092
  function writeManagedAsset(ctx, absolutePath, content) {
2560
3093
  const path = toRelative(ctx.cwd, absolutePath);
2561
- const existed = existsSync(absolutePath);
2562
- mkdirSync2(dirname2(absolutePath), { recursive: true });
3094
+ const existed = existsSync2(absolutePath);
3095
+ mkdirSync2(dirname3(absolutePath), { recursive: true });
2563
3096
  writeFileSync2(absolutePath, content);
2564
3097
  ctx.results.push({ path, action: existed ? "updated" : "created" });
2565
3098
  }
2566
3099
  function writeMcpConfig(ctx, absolutePath) {
2567
3100
  const path = toRelative(ctx.cwd, absolutePath);
2568
3101
  let config = {};
2569
- const existed = existsSync(absolutePath);
3102
+ const existed = existsSync2(absolutePath);
2570
3103
  if (existed) {
2571
3104
  try {
2572
- config = JSON.parse(readFileSync2(absolutePath, "utf8"));
3105
+ config = JSON.parse(readFileSync3(absolutePath, "utf8"));
2573
3106
  } catch {
2574
3107
  ctx.results.push({ path, action: "skipped" });
2575
3108
  return;
@@ -2585,7 +3118,7 @@ function writeMcpConfig(ctx, absolutePath) {
2585
3118
  return;
2586
3119
  }
2587
3120
  servers[MCP_SERVER_KEY] = mcpServerEntry();
2588
- mkdirSync2(dirname2(absolutePath), { recursive: true });
3121
+ mkdirSync2(dirname3(absolutePath), { recursive: true });
2589
3122
  writeFileSync2(absolutePath, `${JSON.stringify(config, null, 2)}
2590
3123
  `);
2591
3124
  ctx.results.push({
@@ -2597,19 +3130,20 @@ function runInit(options) {
2597
3130
  const { io, cwd, client, force, json } = options;
2598
3131
  const ctx = { cwd, force, results: [] };
2599
3132
  if (client === "cursor") {
2600
- writeMcpConfig(ctx, join2(cwd, ".cursor", "mcp.json"));
2601
- writeTextAsset(ctx, join2(cwd, ".cursor", "rules", "khotan.mdc"), CURSOR_RULES);
3133
+ writeMcpConfig(ctx, join3(cwd, ".cursor", "mcp.json"));
3134
+ writeTextAsset(ctx, join3(cwd, ".cursor", "rules", "khotan.mdc"), CURSOR_RULES);
2602
3135
  } else {
2603
- writeMcpConfig(ctx, join2(cwd, "mcp.json"));
2604
- writeTextAsset(ctx, join2(cwd, "khotan-agents.md"), GENERIC_GUIDE);
3136
+ writeMcpConfig(ctx, join3(cwd, "mcp.json"));
3137
+ writeTextAsset(ctx, join3(cwd, "khotan-agents.md"), GENERIC_GUIDE);
2605
3138
  }
2606
3139
  for (const skill of generateSkills()) {
2607
3140
  for (const base of SKILL_BASES) {
2608
- writeManagedAsset(ctx, join2(cwd, base, "skills", skill.name, "SKILL.md"), skill.content);
3141
+ writeManagedAsset(ctx, join3(cwd, base, "skills", skill.name, "SKILL.md"), skill.content);
2609
3142
  }
2610
3143
  }
3144
+ const monoRoot = detectMonoRoot(cwd);
2611
3145
  if (json) {
2612
- io.out(`${JSON.stringify({ client, actions: ctx.results }, null, 2)}
3146
+ io.out(`${JSON.stringify({ client, monoRoot, actions: ctx.results }, null, 2)}
2613
3147
  `);
2614
3148
  return 0;
2615
3149
  }
@@ -2618,12 +3152,322 @@ function runInit(options) {
2618
3152
  errLine(io, ` ${result.action.padEnd(8)} ${result.path}`);
2619
3153
  }
2620
3154
  errLine(io);
3155
+ if (monoRoot) {
3156
+ errLine(io, "Detected a Khotan mono-root workspace (multiple apps under one repo).");
3157
+ errLine(io, "Each app lives in its own subdirectory — do NOT run app scaffolders");
3158
+ errLine(io, "(e.g. `khotan-data init`) at this root. `cd` into an app subdirectory");
3159
+ errLine(io, "first. Run `khotan status` to see the layout.");
3160
+ errLine(io);
3161
+ }
2621
3162
  errLine(io, "Next steps:");
2622
3163
  errLine(io, " 1. Authenticate: khotan auth set-key --api-url <url> --api-key <key>");
2623
3164
  errLine(io, " 2. Open this workspace in your agent and ask it to configure Khotan.");
2624
3165
  return 0;
2625
3166
  }
2626
3167
 
3168
+ // src/cli/commands/status.ts
3169
+ init_io();
3170
+ import { writeFileSync as writeFileSync3 } from "node:fs";
3171
+ import { join as join4, relative as relative2 } from "node:path";
3172
+ function str(value) {
3173
+ return typeof value === "string" && value.length > 0 ? value : null;
3174
+ }
3175
+ function record(value) {
3176
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3177
+ }
3178
+ function listOf(result, key) {
3179
+ const container = record(result);
3180
+ const candidate = container && Array.isArray(container[key]) && container[key] || Array.isArray(result) && result || container && soleArrayProperty(container) || [];
3181
+ return candidate.map(record).filter((r) => r !== null);
3182
+ }
3183
+ function soleArrayProperty(container) {
3184
+ const arrays = Object.keys(container).filter((k) => Array.isArray(container[k]));
3185
+ return arrays.length === 1 ? container[arrays[0]] : null;
3186
+ }
3187
+ function toResourceRow(kind, raw) {
3188
+ const repo = record(raw.repo);
3189
+ return {
3190
+ kind,
3191
+ name: str(raw.displayName) ?? str(raw.slug) ?? "(unnamed)",
3192
+ slug: str(raw.slug) ?? "",
3193
+ status: str(raw.status) ?? "unknown",
3194
+ subdirectory: repo ? str(repo.subdirectory) : null,
3195
+ repo: repo ? str(repo.fullName) : null,
3196
+ branch: repo ? str(repo.defaultBranch) : null,
3197
+ hostname: str(raw.liveUrl) ?? str(raw.primaryHostname) ?? null
3198
+ };
3199
+ }
3200
+ function toDatabaseRow(raw) {
3201
+ return {
3202
+ name: str(raw.displayName) ?? "(unnamed)",
3203
+ status: str(raw.status) ?? "unknown",
3204
+ environment: str(raw.environment),
3205
+ region: str(raw.regionId),
3206
+ host: str(raw.endpointHost)
3207
+ };
3208
+ }
3209
+ function toRedisRow(raw) {
3210
+ return {
3211
+ name: str(raw.displayName) ?? "(unnamed)",
3212
+ status: str(raw.status) ?? "unknown",
3213
+ environment: null,
3214
+ region: str(raw.primaryRegion),
3215
+ host: str(raw.endpointHost)
3216
+ };
3217
+ }
3218
+ async function readList(session, capabilityId, domain, errors, map, responseKey = domain) {
3219
+ try {
3220
+ const result = await session.client.execute(capabilityId);
3221
+ return map(listOf(result, responseKey));
3222
+ } catch (error) {
3223
+ errors.push({
3224
+ domain,
3225
+ message: error instanceof Error ? error.message : String(error)
3226
+ });
3227
+ return [];
3228
+ }
3229
+ }
3230
+ async function loadTopology(session) {
3231
+ const errors = [];
3232
+ const apps = await readList(session, "apps.list", "apps", errors, (rows) => rows.map((r) => toResourceRow("app", r)));
3233
+ const pipelines = await readList(session, "pipelines.list", "pipelines", errors, (rows) => rows.map((r) => toResourceRow("pipeline", r)));
3234
+ const databases = await readList(session, "databases.list", "databases", errors, (rows) => rows.map(toDatabaseRow));
3235
+ const redisDatabases = await readList(session, "redis.list", "redis", errors, (rows) => rows.map(toRedisRow), "databases");
3236
+ const repoBearer = [...apps, ...pipelines].find((r) => r.repo);
3237
+ return {
3238
+ repo: repoBearer?.repo ?? null,
3239
+ defaultBranch: repoBearer?.branch ?? null,
3240
+ apps,
3241
+ pipelines,
3242
+ databases,
3243
+ redisDatabases,
3244
+ errors
3245
+ };
3246
+ }
3247
+ var EM_DASH = "—";
3248
+ function table(rows) {
3249
+ if (rows.length === 0) {
3250
+ return [" (none)"];
3251
+ }
3252
+ const columns = Object.keys(rows[0]);
3253
+ const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => (row[column] ?? "").length)));
3254
+ const format = (cells) => ` ${cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" ").trimEnd()}`;
3255
+ return [
3256
+ format(columns),
3257
+ format(widths.map((width) => "-".repeat(width))),
3258
+ ...rows.map((row) => format(columns.map((column) => row[column] ?? "")))
3259
+ ];
3260
+ }
3261
+ function resourceTable(rows) {
3262
+ return rows.map((row) => ({
3263
+ subdir: row.subdirectory ?? EM_DASH,
3264
+ name: row.name,
3265
+ status: row.status,
3266
+ hostname: row.hostname ?? EM_DASH
3267
+ }));
3268
+ }
3269
+ function deployables(topology) {
3270
+ return [...topology.apps, ...topology.pipelines];
3271
+ }
3272
+ function deployableLayout(topology) {
3273
+ if (topology.errors.some((error) => error.domain === "apps" || error.domain === "pipelines")) {
3274
+ return "partial";
3275
+ }
3276
+ const rows = deployables(topology);
3277
+ if (rows.length === 0)
3278
+ return "empty";
3279
+ const withSubdir = rows.filter((row) => row.subdirectory).length;
3280
+ if (withSubdir === rows.length)
3281
+ return "subdirectory";
3282
+ if (withSubdir === 0)
3283
+ return "whole-repo";
3284
+ return "mixed";
3285
+ }
3286
+ function renderLayoutIntro(io, topology) {
3287
+ const layout = deployableLayout(topology);
3288
+ if (layout === "subdirectory") {
3289
+ outLine(io, "Every app and pipeline below lives as a subdirectory of one shared");
3290
+ outLine(io, "organization mono-root repo — that repo is the deploy source. Each");
3291
+ outLine(io, "subdirectory is an independent deployable. Databases are separate");
3292
+ outLine(io, "resources, linked to apps via env vars (`khotan apps env list`), not");
3293
+ outLine(io, "by directory.");
3294
+ return;
3295
+ }
3296
+ if (layout === "mixed") {
3297
+ outLine(io, "Some deployables below live in subdirectories of the deploy repo;");
3298
+ outLine(io, "rows marked with — use the repo root or have no subdirectory metadata.");
3299
+ } else if (layout === "whole-repo") {
3300
+ outLine(io, "Deployables below use the deploy repo root or have no subdirectory");
3301
+ outLine(io, "metadata, rather than an organization mono-root subdirectory layout.");
3302
+ } else if (layout === "partial") {
3303
+ outLine(io, "Some app or pipeline data could not be read, so the deployable");
3304
+ outLine(io, "layout below is incomplete. Treat missing deployables as unknown.");
3305
+ } else {
3306
+ outLine(io, "No app or pipeline deployables were returned yet.");
3307
+ }
3308
+ outLine(io, "Databases are separate resources, linked to apps via env vars");
3309
+ outLine(io, "(`khotan apps env list`), not by directory.");
3310
+ }
3311
+ function renderHuman(io, topology) {
3312
+ outLine(io, "Workspace topology");
3313
+ outLine(io);
3314
+ renderLayoutIntro(io, topology);
3315
+ outLine(io);
3316
+ outLine(io, `Deploy repo: ${topology.repo ?? "(none provisioned yet)"}` + (topology.defaultBranch ? ` (branch: ${topology.defaultBranch})` : ""));
3317
+ outLine(io);
3318
+ outLine(io, `Apps (${topology.apps.length})`);
3319
+ for (const line of table(resourceTable(topology.apps)))
3320
+ outLine(io, line);
3321
+ outLine(io);
3322
+ outLine(io, `Pipelines (${topology.pipelines.length})`);
3323
+ for (const line of table(resourceTable(topology.pipelines)))
3324
+ outLine(io, line);
3325
+ outLine(io);
3326
+ outLine(io, `Databases (${topology.databases.length})`);
3327
+ for (const line of table(topology.databases.map((db) => ({
3328
+ name: db.name,
3329
+ status: db.status,
3330
+ region: db.region ?? EM_DASH,
3331
+ host: db.host ?? EM_DASH
3332
+ })))) {
3333
+ outLine(io, line);
3334
+ }
3335
+ outLine(io);
3336
+ outLine(io, `Redis (KV) (${topology.redisDatabases.length})`);
3337
+ for (const line of table(topology.redisDatabases.map((db) => ({
3338
+ name: db.name,
3339
+ status: db.status,
3340
+ region: db.region ?? EM_DASH,
3341
+ host: db.host ?? EM_DASH
3342
+ })))) {
3343
+ outLine(io, line);
3344
+ }
3345
+ for (const failure of topology.errors) {
3346
+ errLine(io, `! could not read ${failure.domain}: ${failure.message}`);
3347
+ }
3348
+ }
3349
+ function markdownTable(headers, rows) {
3350
+ const lines = [
3351
+ `| ${headers.join(" | ")} |`,
3352
+ `| ${headers.map(() => "---").join(" | ")} |`,
3353
+ ...rows.map((row) => `| ${row.join(" | ")} |`)
3354
+ ];
3355
+ return lines.join(`
3356
+ `);
3357
+ }
3358
+ function mdCell(value) {
3359
+ return value ? value : EM_DASH;
3360
+ }
3361
+ function workspaceIntro(topology) {
3362
+ const layout = deployableLayout(topology);
3363
+ if (layout === "subdirectory") {
3364
+ return [
3365
+ "This directory is one **organization mono-root repo**: a single GitHub repo",
3366
+ "whose top-level subdirectories are each an independent Khotan deployable (an",
3367
+ "app or a pipeline). Cloning the repo (or a sparse subdirectory checkout) gives",
3368
+ "you the code; each subdirectory deploys on its own from this same repo.",
3369
+ "",
3370
+ "**Do not run app scaffolders (e.g. `khotan-data init`) at this root.** Each",
3371
+ "app already lives in its own subdirectory — `cd` into it first."
3372
+ ];
3373
+ }
3374
+ if (layout === "mixed") {
3375
+ return [
3376
+ "This file records Khotan deployables for this repository. Some deployables",
3377
+ "live in subdirectories; deployables marked with — use the repo root or have",
3378
+ "no subdirectory metadata.",
3379
+ "",
3380
+ "For deployables with a subdirectory, `cd` into that directory before running",
3381
+ "app scaffolders. For rows marked —, verify the resource layout before",
3382
+ "scaffolding."
3383
+ ];
3384
+ }
3385
+ if (layout === "whole-repo") {
3386
+ return [
3387
+ "This file records Khotan deployables for this repository. The returned",
3388
+ "deployables use the repo root or have no subdirectory metadata, so this is",
3389
+ "not described as an organization mono-root subdirectory layout."
3390
+ ];
3391
+ }
3392
+ if (layout === "partial") {
3393
+ return [
3394
+ "This file records Khotan deployables for this repository. Some app or",
3395
+ "pipeline data could not be read, so the deployable layout is incomplete.",
3396
+ "Use the Read Errors section before deciding where to run scaffolders."
3397
+ ];
3398
+ }
3399
+ return [
3400
+ "This file records Khotan deployables for this repository. No apps or pipelines",
3401
+ "were returned yet; re-run `khotan status --write` after provisioning resources",
3402
+ "to capture their deploy mapping."
3403
+ ];
3404
+ }
3405
+ function errorsSection(errors) {
3406
+ if (errors.length === 0)
3407
+ return [];
3408
+ return [
3409
+ "",
3410
+ "## Read Errors",
3411
+ "",
3412
+ "Some live data could not be read while generating this file. Treat affected",
3413
+ "empty sections as unknown, not as confirmed empty.",
3414
+ "",
3415
+ markdownTable(["Domain", "Error"], errors.map((error) => [error.domain, error.message]))
3416
+ ];
3417
+ }
3418
+ function buildWorkspaceManifest(topology) {
3419
+ const rows = deployables(topology);
3420
+ const sections = [];
3421
+ sections.push("# Workspace", "", "> Generated by `khotan status --write`. Re-run to refresh; safe to commit.", "", ...workspaceIntro(topology), "", `- **Deploy repo:** ${topology.repo ? `\`${topology.repo}\`` : "_(none provisioned yet)_"}` + (topology.defaultBranch ? ` (branch \`${topology.defaultBranch}\`)` : ""));
3422
+ sections.push("", "## Deployables", "", rows.length === 0 ? "_No apps or pipelines provisioned yet._" : markdownTable(["Subdirectory", "Resource", "Type", "Status", "Live URL"], rows.map((row) => [
3423
+ row.subdirectory ? `\`${row.subdirectory}\`` : EM_DASH,
3424
+ row.name,
3425
+ row.kind,
3426
+ row.status,
3427
+ row.hostname ? `https://${row.hostname.replace(/^https?:\/\//, "")}` : EM_DASH
3428
+ ])));
3429
+ sections.push("", "## Databases", "", "Databases are separate resources. An app connects to one through environment", "variables (`khotan apps env list <appId>`), not by directory.", "", topology.databases.length === 0 ? "_No databases provisioned yet._" : markdownTable(["Name", "Status", "Environment", "Region", "Host"], topology.databases.map((db) => [
3430
+ db.name,
3431
+ db.status,
3432
+ mdCell(db.environment),
3433
+ mdCell(db.region),
3434
+ mdCell(db.host)
3435
+ ])));
3436
+ sections.push("", "## Redis (KV)", "", "Redis (KV) databases are separate resources. An app connects to one through", "environment variables (`khotan apps env list <appId>`), not by directory.", "", topology.redisDatabases.length === 0 ? "_No Redis (KV) databases provisioned yet._" : markdownTable(["Name", "Status", "Region", "Host"], topology.redisDatabases.map((db) => [
3437
+ db.name,
3438
+ db.status,
3439
+ mdCell(db.region),
3440
+ mdCell(db.host)
3441
+ ])));
3442
+ sections.push(...errorsSection(topology.errors));
3443
+ sections.push("", "## Working here", "", "- `khotan status` — reprint this topology (`--json` for machine output).", "- `khotan status --write` — regenerate this `WORKSPACE.md` from live data.", "- `khotan apps list` · `khotan pipelines list` · `khotan databases list` · `khotan redis list` — per-domain detail.", "");
3444
+ return sections.join(`
3445
+ `);
3446
+ }
3447
+ async function runStatus(options) {
3448
+ const { session, io, json, writeDir } = options;
3449
+ const topology = await loadTopology(session);
3450
+ if (writeDir) {
3451
+ const target = join4(writeDir, "WORKSPACE.md");
3452
+ writeFileSync3(target, buildWorkspaceManifest(topology));
3453
+ const shown = relative2(process.cwd(), target) || target;
3454
+ if (json) {
3455
+ outLine(io, JSON.stringify({ wrote: shown, topology }, null, 2));
3456
+ } else {
3457
+ renderHuman(io, topology);
3458
+ errLine(io);
3459
+ errLine(io, `Wrote ${shown}`);
3460
+ }
3461
+ return 0;
3462
+ }
3463
+ if (json) {
3464
+ outLine(io, JSON.stringify(topology, null, 2));
3465
+ return 0;
3466
+ }
3467
+ renderHuman(io, topology);
3468
+ return 0;
3469
+ }
3470
+
2627
3471
  // src/cli/confirm.ts
2628
3472
  init_src();
2629
3473
  init_io();
@@ -2679,13 +3523,14 @@ async function runCapability(match, args, options) {
2679
3523
  printCommandHelp(options.io, capability);
2680
3524
  return 0;
2681
3525
  }
2682
- const session = resolveSession({
3526
+ const session = await resolveSessionAsserted({
2683
3527
  env: options.env,
2684
3528
  storeOptions: options.storeOptions,
2685
3529
  profileName: options.profileName,
2686
3530
  apiUrlOverride: options.apiUrlOverride,
2687
3531
  apiKeyOverride: options.apiKeyOverride,
2688
- fetch: options.fetch
3532
+ fetch: options.fetch,
3533
+ assertOrgOverride: options.assertOrgOverride
2689
3534
  });
2690
3535
  const input = buildCapabilityInput(capability, match.rest, args);
2691
3536
  const json = options.jsonFlag || session.resolved.defaultOutput === "json";
@@ -2702,7 +3547,7 @@ async function runCapability(match, args, options) {
2702
3547
  }
2703
3548
  async function run(argv, options = {}) {
2704
3549
  const io = options.io ?? createNodeIo();
2705
- const env = options.env ?? io.env;
3550
+ const env = overlayRepoEnv(options.env ?? io.env, { cwd: process.cwd() }).env;
2706
3551
  const args = parseArgs(argv);
2707
3552
  const { positionals } = args;
2708
3553
  const jsonFlag = flagBool(args, "json");
@@ -2711,6 +3556,7 @@ async function run(argv, options = {}) {
2711
3556
  const profileName = flagValue(args, "profile");
2712
3557
  const apiUrlOverride = flagValue(args, "api-url");
2713
3558
  const apiKeyOverride = flagValue(args, "api-key");
3559
+ const assertOrgOverride = flagValue(args, "assert-org");
2714
3560
  if (flagBool(args, "version")) {
2715
3561
  const { KHOTAN_ADAPTER_VERSION: KHOTAN_ADAPTER_VERSION2 } = await Promise.resolve().then(() => (init_src(), exports_src));
2716
3562
  io.out(`${KHOTAN_ADAPTER_VERSION2}
@@ -2734,7 +3580,12 @@ async function run(argv, options = {}) {
2734
3580
  if (match2 && isOperationCapability(match2.capability)) {
2735
3581
  printCommandHelp(io, match2.capability);
2736
3582
  } else {
2737
- printTopLevelHelp(io);
3583
+ const group2 = groupCommands(topic);
3584
+ if (group2.length > 0) {
3585
+ printGroupHelp(io, topic, group2);
3586
+ } else {
3587
+ printTopLevelHelp(io);
3588
+ }
2738
3589
  }
2739
3590
  return 0;
2740
3591
  }
@@ -2750,6 +3601,26 @@ async function run(argv, options = {}) {
2750
3601
  const cwd = flagValue(args, "dir") ?? process.cwd();
2751
3602
  return runInit({ io, cwd, client: clientRaw, force: flagBool(args, "force"), json: jsonFlag });
2752
3603
  }
3604
+ if (first === "status") {
3605
+ if (helpFlag) {
3606
+ errLine(io, "Usage: khotan status [--write [path]] [--dir <path>] [--json]");
3607
+ return 0;
3608
+ }
3609
+ const session = await resolveSessionAsserted({
3610
+ env,
3611
+ storeOptions: options.storeOptions,
3612
+ profileName,
3613
+ apiUrlOverride,
3614
+ apiKeyOverride,
3615
+ fetch: options.fetch,
3616
+ assertOrgOverride
3617
+ });
3618
+ const json = jsonFlag || session.resolved.defaultOutput === "json";
3619
+ const writeValue = flagValue(args, "write");
3620
+ const write = flagBool(args, "write");
3621
+ const writeDir = write ? flagValue(args, "dir") ?? (writeValue && writeValue !== "true" ? writeValue : undefined) ?? process.cwd() : undefined;
3622
+ return await runStatus({ session, io, json, writeDir });
3623
+ }
2753
3624
  if (first === "mcp" && positionals[1] === "serve") {
2754
3625
  const launcher = options.startMcpServer ?? (async (deps) => {
2755
3626
  const { serveMcp: serveMcp2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
@@ -2762,7 +3633,8 @@ async function run(argv, options = {}) {
2762
3633
  fetch: options.fetch,
2763
3634
  profileName,
2764
3635
  apiUrlOverride,
2765
- apiKeyOverride
3636
+ apiKeyOverride,
3637
+ assertOrgOverride
2766
3638
  });
2767
3639
  }
2768
3640
  if (first === "login") {
@@ -2795,13 +3667,14 @@ async function run(argv, options = {}) {
2795
3667
  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>");
2796
3668
  return 0;
2797
3669
  }
2798
- const session = resolveSession({
3670
+ const session = await resolveSessionAsserted({
2799
3671
  env,
2800
3672
  storeOptions: options.storeOptions,
2801
3673
  profileName,
2802
3674
  apiUrlOverride,
2803
3675
  apiKeyOverride,
2804
- fetch: options.fetch
3676
+ fetch: options.fetch,
3677
+ assertOrgOverride
2805
3678
  });
2806
3679
  const json = jsonFlag || session.resolved.defaultOutput === "json";
2807
3680
  if (positionals[1] === "upload") {
@@ -2834,9 +3707,15 @@ async function run(argv, options = {}) {
2834
3707
  yes,
2835
3708
  profileName,
2836
3709
  apiUrlOverride,
2837
- apiKeyOverride
3710
+ apiKeyOverride,
3711
+ assertOrgOverride
2838
3712
  });
2839
3713
  }
3714
+ const group = groupCommands(positionals);
3715
+ if (group.length > 0) {
3716
+ printGroupHelp(io, positionals, group);
3717
+ return 0;
3718
+ }
2840
3719
  errLine(io, `Unknown command "${positionals.join(" ")}". Run \`khotan help\` for usage.`);
2841
3720
  return 1;
2842
3721
  } catch (error) {