@work-shape/catalyst-cli 0.6.3 → 0.7.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/dist/index.js CHANGED
@@ -5,11 +5,6 @@ import { realpathSync } from "fs";
5
5
  import { parseArgs as parseArgs2 } from "util";
6
6
  import { pathToFileURL } from "url";
7
7
 
8
- // src/asset-commands.ts
9
- import { readFile as readFile5 } from "fs/promises";
10
- import { resolve as resolve5 } from "path";
11
- import { parseArgs } from "util";
12
-
13
8
  // ../asset-schemas/src/asset-types.ts
14
9
  var REASONING_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"];
15
10
  var SKILL_STARTERS_MAX = 2;
@@ -77,7 +72,7 @@ var PROVIDER_CHECK_ERROR_CLASSES = [
77
72
  ];
78
73
  var CONNECTION_NAME_PATTERN = /^[a-z][a-z0-9]*([_-][a-z0-9]+)*$/u;
79
74
  var CONNECTION_NAME_MAX_LENGTH = 64;
80
- var CONNECTION_EXPOSURES = ["agent", "bridge", "workflow"];
75
+ var CONNECTION_EXPOSURES = ["agent", "bridge", "workflow", "outside"];
81
76
  var CONNECTION_HEALTH_STATES = [
82
77
  "unknown",
83
78
  "ok",
@@ -282,6 +277,9 @@ var agentConfigSchema = z.object({
282
277
  maxSteps: z.number().int().positive().optional(),
283
278
  toolNames: z.array(z.string().min(1)).default([]),
284
279
  skillNames: z.array(skillNameSchema).default([]),
280
+ knowledgeBases: z.array(z.string().min(1)).refine((names) => new Set(names).size === names.length, {
281
+ message: "A Knowledge Base is listed once"
282
+ }).optional(),
285
283
  initialPrompts: z.array(
286
284
  z.object({
287
285
  title: localizedStringSchema,
@@ -939,6 +937,69 @@ function buildPath(pathTemplate, queryParamNames, options = {}) {
939
937
  return queryString ? `${path}?${queryString}` : path;
940
938
  }
941
939
 
940
+ // ../api-contract/src/openapi.ts
941
+ import { z as z37 } from "zod";
942
+
943
+ // ../api-contract/package.json
944
+ var package_default = {
945
+ name: "@vivd-catalyst/api-contract",
946
+ version: "0.7.0",
947
+ license: "MIT",
948
+ repository: {
949
+ type: "git",
950
+ url: "git+https://github.com/vivd-solutions/vivd-catalyst.git",
951
+ directory: "packages/api-contract"
952
+ },
953
+ type: "module",
954
+ sideEffects: false,
955
+ engines: {
956
+ node: ">=22"
957
+ },
958
+ files: [
959
+ "dist",
960
+ "openapi.json"
961
+ ],
962
+ main: "./dist/index.js",
963
+ types: "./dist/index.d.ts",
964
+ exports: {
965
+ ".": {
966
+ types: "./dist/index.d.ts",
967
+ development: "./src/index.ts",
968
+ import: "./dist/index.js"
969
+ },
970
+ "./openapi.json": "./openapi.json"
971
+ },
972
+ publishConfig: {
973
+ access: "restricted",
974
+ registry: "https://registry.npmjs.org/",
975
+ exports: {
976
+ ".": {
977
+ types: "./dist/index.d.ts",
978
+ import: "./dist/index.js"
979
+ },
980
+ "./openapi.json": "./openapi.json"
981
+ }
982
+ },
983
+ scripts: {
984
+ build: "tsup src/index.ts --format esm --dts",
985
+ "check:contract": "tsx scripts/check-contract.ts",
986
+ "check:openapi": "tsx scripts/check-openapi.ts",
987
+ "generate:openapi": "tsx scripts/write-openapi.ts",
988
+ typecheck: "tsc -p tsconfig.json --noEmit"
989
+ },
990
+ dependencies: {
991
+ "@vivd-catalyst/asset-schemas": "workspace:*",
992
+ zod: "^4.4.3"
993
+ },
994
+ devDependencies: {
995
+ "@redocly/openapi-core": "^2.62.1",
996
+ "@types/node": "^24.13.1",
997
+ tsup: "^8.5.1",
998
+ tsx: "^4.22.4",
999
+ typescript: "^6.0.3"
1000
+ }
1001
+ };
1002
+
942
1003
  // ../api-contract/src/errors.ts
943
1004
  import { z as z6 } from "zod";
944
1005
  var appErrorCodeSchema = z6.enum([
@@ -1498,6 +1559,7 @@ var agentEditableFieldSchema = z9.enum([
1498
1559
  "maxSteps",
1499
1560
  "toolNames",
1500
1561
  "skillNames",
1562
+ "knowledgeBases",
1501
1563
  "initialPrompts"
1502
1564
  ]);
1503
1565
  var editableAgentFieldsSchema = z9.preprocess((value) => {
@@ -1731,6 +1793,28 @@ var configAssetRevisionSchema = z9.object({
1731
1793
  globalVersion: z9.number().int().positive(),
1732
1794
  createdAt: timestampSchema
1733
1795
  });
1796
+ var toolGroupSchema = z9.enum([
1797
+ "documents",
1798
+ "web",
1799
+ "files",
1800
+ "results",
1801
+ "skills",
1802
+ "platform",
1803
+ "instance"
1804
+ ]);
1805
+ var toolTextSchema = z9.object({ en: z9.string(), de: z9.string() });
1806
+ var agentToolReferenceSchema = z9.object({
1807
+ name: z9.string(),
1808
+ title: toolTextSchema.optional(),
1809
+ summary: toolTextSchema.optional(),
1810
+ group: toolGroupSchema.optional(),
1811
+ /** A key of the icon registry of the UI library. */
1812
+ icon: z9.string().optional(),
1813
+ /** Missing for a tool the instance enables and no code registers. */
1814
+ effect: z9.enum(["reading", "changing"]).optional(),
1815
+ /** The module whose switch governs the tool. A core tool has none. */
1816
+ module: z9.string().optional()
1817
+ });
1734
1818
  var configAssetReferencesSchema = z9.object({
1735
1819
  modelProviderIds: z9.array(z9.string()),
1736
1820
  modelBindingIds: z9.array(z9.string()),
@@ -1745,6 +1829,8 @@ var configAssetReferencesSchema = z9.object({
1745
1829
  fastModeModelBindingIds: z9.array(z9.string()),
1746
1830
  reasoningEfforts: z9.array(reasoningEffortSchema),
1747
1831
  enabledToolNames: z9.array(z9.string()),
1832
+ /** The tools of `enabledToolNames`, in their order, with what each tells people. */
1833
+ tools: z9.array(agentToolReferenceSchema).optional(),
1748
1834
  /**
1749
1835
  * Tools the instance enables whose module is off. No agent can use one. An agent that
1750
1836
  * still names one shows it as unavailable, so it can be removed.
@@ -2866,6 +2952,14 @@ var syncAssetsRequestSchema = z14.object({
2866
2952
  * instance.
2867
2953
  */
2868
2954
  namespace: nameText(z14.string().min(1)).optional(),
2955
+ /**
2956
+ * The workspace that owns every asset the batch names, as a CLI root bound with
2957
+ * `--workspace` sends it. Left out, the batch names the instance's own assets. A batch
2958
+ * names a Namespace or a workspace, never both, and one that names a workspace is no
2959
+ * release: its agents start hidden and its writer holds the rights on what the workspace
2960
+ * owns.
2961
+ */
2962
+ workspaceId: workspaceIdSchema,
2869
2963
  /**
2870
2964
  * At most `ASSET_SYNC_MAX_ITEMS` items, or `ASSET_RELEASE_SYNC_MAX_ITEMS` for a holder of
2871
2965
  * the release right. A batch without items names the default agent.
@@ -2964,7 +3058,7 @@ var assetOperations = {
2964
3058
  id: "assets.sync",
2965
3059
  method: "POST",
2966
3060
  path: "/api/v1/assets/sync",
2967
- summary: "Apply puts and deletes in one Namespace, all of them or none",
3061
+ summary: "Apply puts and deletes in one Namespace or workspace, all of them or none",
2968
3062
  tag: "Assets",
2969
3063
  auth: "principal",
2970
3064
  scope: "config_assets:write",
@@ -5732,8 +5826,99 @@ var jobOperations = {
5732
5826
  })
5733
5827
  };
5734
5828
 
5735
- // ../api-contract/src/operations/operations.ts
5829
+ // ../api-contract/src/operations/knowledge.ts
5736
5830
  import { z as z30 } from "zod";
5831
+ var SCALAR_TYPES2 = ["text", "integer", "number", "boolean", "date", "timestamp"];
5832
+ var scalarSchema2 = z30.union([z30.string(), z30.number(), z30.boolean(), z30.null()]);
5833
+ var valueSchema2 = z30.union([scalarSchema2, z30.array(scalarSchema2)]);
5834
+ var knowledgeQueryRequestSchema = z30.strictObject({
5835
+ /** One read-only SELECT or WITH statement with positional placeholders. */
5836
+ sql: z30.string().min(1),
5837
+ /** A bare value, or a value with the type it is bound as. Never part of the statement. */
5838
+ parameters: z30.array(
5839
+ z30.union([
5840
+ valueSchema2,
5841
+ z30.strictObject({
5842
+ type: z30.enum(SCALAR_TYPES2.flatMap((type) => [type, `${type}[]`])),
5843
+ value: valueSchema2
5844
+ })
5845
+ ])
5846
+ ).optional()
5847
+ });
5848
+ var knowledgeQueryResponseSchema = z30.object({
5849
+ /** `type` is the product's name of the type, `nativeType` the database's. */
5850
+ columns: z30.array(z30.object({ name: z30.string(), type: z30.string(), nativeType: z30.string() })),
5851
+ rows: z30.array(z30.record(z30.string(), z30.unknown())),
5852
+ /** Whether a cap ended the result before its last row. */
5853
+ truncated: z30.boolean(),
5854
+ /** What a citation names: the Knowledge Base and the tables it describes. */
5855
+ source: z30.object({
5856
+ knowledgeBase: z30.string(),
5857
+ title: z30.string(),
5858
+ tables: z30.array(z30.string())
5859
+ })
5860
+ });
5861
+ var knowledgeStateResponseSchema = z30.object({
5862
+ /** The id of the Knowledge Base's asset. */
5863
+ id: z30.string(),
5864
+ /** The id operations and routes name the Knowledge Base by. */
5865
+ name: z30.string(),
5866
+ /** `unchecked` until the published revision was first checked. */
5867
+ status: z30.enum(["unchecked", "ok", "needs_attention"]),
5868
+ /** The revision the last check was of. */
5869
+ revision: z30.number().int().optional(),
5870
+ checkedAt: z30.string().optional(),
5871
+ /** Each described table, and whether the last check could read it. */
5872
+ tables: z30.array(z30.object({ name: z30.string(), readable: z30.boolean() })),
5873
+ /** Why the backend could not be asked at all, as a sentence. */
5874
+ problem: z30.string().optional()
5875
+ });
5876
+ var BASE = {
5877
+ tag: "Knowledge Bases",
5878
+ auth: "principal",
5879
+ requires: [],
5880
+ errors: ["NOT_FOUND", "CONFLICT", "TIMEOUT"]
5881
+ };
5882
+ var knowledgeOperations = {
5883
+ "knowledge_bases.get": defineRegisteredOperation({
5884
+ ...BASE,
5885
+ id: "knowledge_bases.get",
5886
+ method: "GET",
5887
+ path: "/api/v1/knowledge-bases/:knowledgeBase",
5888
+ summary: "Read the state of one published Knowledge Base: whether its last check could read every table it describes",
5889
+ scope: "config_assets:read",
5890
+ effect: "reading",
5891
+ response: json(knowledgeStateResponseSchema),
5892
+ errors: ["NOT_FOUND"],
5893
+ rateClass: "read"
5894
+ }),
5895
+ "knowledge_bases.check": defineRegisteredOperation({
5896
+ ...BASE,
5897
+ id: "knowledge_bases.check",
5898
+ method: "POST",
5899
+ path: "/api/v1/knowledge-bases/:knowledgeBase/check",
5900
+ summary: "Check one published Knowledge Base now: whether every table it describes can be read. It reads no row",
5901
+ scope: "config_assets:write",
5902
+ effect: "changing",
5903
+ response: json(knowledgeStateResponseSchema),
5904
+ rateClass: "write"
5905
+ }),
5906
+ "knowledge_bases.query": defineRegisteredOperation({
5907
+ ...BASE,
5908
+ id: "knowledge_bases.query",
5909
+ method: "POST",
5910
+ path: "/api/v1/knowledge-bases/:knowledgeBase/query",
5911
+ summary: "Read rows of one published Knowledge Base with one read-only statement and bound parameters",
5912
+ scope: "data_store_rows:read",
5913
+ effect: "reading",
5914
+ body: knowledgeQueryRequestSchema,
5915
+ response: json(knowledgeQueryResponseSchema),
5916
+ rateClass: "read"
5917
+ })
5918
+ };
5919
+
5920
+ // ../api-contract/src/operations/operations.ts
5921
+ import { z as z31 } from "zod";
5737
5922
  var operationRunOperations = {
5738
5923
  "operations.get_run": defineOperation({
5739
5924
  id: "operations.get_run",
@@ -5762,14 +5947,14 @@ var operationRunOperations = {
5762
5947
  requires: ["audit.view"],
5763
5948
  effect: "reading",
5764
5949
  query: listQuerySchema.extend({
5765
- operation: z30.string().min(1).optional(),
5950
+ operation: z31.string().min(1).optional(),
5766
5951
  /** Only reading calls or only changing ones. Without it the list holds both. */
5767
- effect: z30.enum(["reading", "changing"]).optional(),
5952
+ effect: z31.enum(["reading", "changing"]).optional(),
5768
5953
  status: operationRunStatusSchema.optional(),
5769
5954
  /** The id of the user or service principal that called. */
5770
- actor: z30.string().min(1).optional(),
5771
- origin: z30.enum(["agent", "user", "cli", "mcp", "app", "workflow", "schedule"]).optional(),
5772
- workspaceId: z30.string().min(1).optional(),
5955
+ actor: z31.string().min(1).optional(),
5956
+ origin: z31.enum(["agent", "user", "cli", "mcp", "app", "workflow", "schedule"]).optional(),
5957
+ workspaceId: z31.string().min(1).optional(),
5773
5958
  /** Runs created at or after this moment. */
5774
5959
  since: timestampSchema.optional()
5775
5960
  }),
@@ -5841,74 +6026,74 @@ var pageOperations = {
5841
6026
  };
5842
6027
 
5843
6028
  // ../api-contract/src/operations/personal-api-keys.ts
5844
- import { z as z32 } from "zod";
6029
+ import { z as z33 } from "zod";
5845
6030
 
5846
6031
  // ../api-contract/src/personal-api-keys.ts
5847
- import { z as z31 } from "zod";
5848
- var personalApiKeyScopeSchema = z31.string().min(1).max(200);
5849
- var personalApiKeySchema = z31.object({
5850
- id: z31.string(),
5851
- ownerUserId: z31.string(),
6032
+ import { z as z32 } from "zod";
6033
+ var personalApiKeyScopeSchema = z32.string().min(1).max(200);
6034
+ var personalApiKeySchema = z32.object({
6035
+ id: z32.string(),
6036
+ ownerUserId: z32.string(),
5852
6037
  /** The owner's name, in the instance's list of every person's keys. */
5853
- ownerDisplayLabel: z31.string().optional(),
5854
- name: z31.string(),
6038
+ ownerDisplayLabel: z32.string().optional(),
6039
+ name: z32.string(),
5855
6040
  /** The start of the key, which a person compares with the key they hold. Never the key. */
5856
- keyPrefix: z31.string(),
5857
- scopes: z31.array(personalApiKeyScopeSchema),
6041
+ keyPrefix: z32.string(),
6042
+ scopes: z32.array(personalApiKeyScopeSchema),
5858
6043
  createdAt: timestampSchema,
5859
6044
  expiresAt: timestampSchema,
5860
6045
  revokedAt: timestampSchema.optional(),
5861
6046
  lastUsedAt: timestampSchema.optional()
5862
6047
  });
5863
- var keyFieldsSchema = z31.object({
5864
- name: z31.string().trim().min(1).max(100),
5865
- scopes: z31.array(personalApiKeyScopeSchema).min(1).max(64),
6048
+ var keyFieldsSchema = z32.object({
6049
+ name: z32.string().trim().min(1).max(100),
6050
+ scopes: z32.array(personalApiKeyScopeSchema).min(1).max(64),
5866
6051
  /** At most a year ahead. */
5867
6052
  expiresAt: timestampSchema
5868
6053
  });
5869
6054
  var createPersonalApiKeyRequestSchema = keyFieldsSchema;
5870
- var createPersonalApiKeyResponseSchema = z31.object({
6055
+ var createPersonalApiKeyResponseSchema = z32.object({
5871
6056
  key: personalApiKeySchema,
5872
6057
  /** The key, shown this once. Only its hash is stored. */
5873
- secret: z31.string()
6058
+ secret: z32.string()
5874
6059
  });
5875
- var startConnectCodeRequestSchema = z31.object({
6060
+ var startConnectCodeRequestSchema = z32.object({
5876
6061
  /** What the CLI calls itself, shown to the person who confirms. Attribution, never authority. */
5877
- clientLabel: z31.string().trim().min(1).max(100).optional()
6062
+ clientLabel: z32.string().trim().min(1).max(100).optional()
5878
6063
  });
5879
- var startConnectCodeResponseSchema = z31.object({
6064
+ var startConnectCodeResponseSchema = z32.object({
5880
6065
  /** The code the person confirms in Settings, as `ABCD-EFGH`. */
5881
- userCode: z31.string(),
6066
+ userCode: z32.string(),
5882
6067
  /** The CLI's own code to collect the key with. It never leaves the CLI. */
5883
- deviceCode: z31.string(),
6068
+ deviceCode: z32.string(),
5884
6069
  expiresAt: timestampSchema,
5885
6070
  /** How long the CLI waits between two collection tries. */
5886
- intervalSeconds: z31.number().int().positive(),
6071
+ intervalSeconds: z32.number().int().positive(),
5887
6072
  /** Where the person confirms the code, relative to the interface's address. */
5888
- confirmPath: z31.string()
6073
+ confirmPath: z32.string()
5889
6074
  });
5890
- var redeemConnectCodeRequestSchema = z31.object({
5891
- deviceCode: z31.string().min(1).max(200)
6075
+ var redeemConnectCodeRequestSchema = z32.object({
6076
+ deviceCode: z32.string().min(1).max(200)
5892
6077
  });
5893
- var redeemConnectCodeResponseSchema = z31.discriminatedUnion("status", [
6078
+ var redeemConnectCodeResponseSchema = z32.discriminatedUnion("status", [
5894
6079
  /** Not yet confirmed: try again after the interval. */
5895
- z31.object({ status: z31.literal("pending") }),
5896
- z31.object({
5897
- status: z31.literal("issued"),
6080
+ z32.object({ status: z32.literal("pending") }),
6081
+ z32.object({
6082
+ status: z32.literal("issued"),
5898
6083
  key: personalApiKeySchema,
5899
6084
  /** The key, answered this once. Only its hash is stored. */
5900
- secret: z31.string()
6085
+ secret: z32.string()
5901
6086
  })
5902
6087
  ]);
5903
- var connectCodeSchema = z31.object({
5904
- clientLabel: z31.string().optional(),
6088
+ var connectCodeSchema = z32.object({
6089
+ clientLabel: z32.string().optional(),
5905
6090
  createdAt: timestampSchema,
5906
6091
  expiresAt: timestampSchema
5907
6092
  });
5908
6093
  var confirmConnectCodeRequestSchema = keyFieldsSchema;
5909
6094
 
5910
6095
  // ../api-contract/src/operations/personal-api-keys.ts
5911
- var confirmedConnectCodeSchema = z32.object({ confirmed: z32.literal(true) });
6096
+ var confirmedConnectCodeSchema = z33.object({ confirmed: z33.literal(true) });
5912
6097
  var personalApiKeyOperations = {
5913
6098
  "me.api_keys.list": defineOperation({
5914
6099
  id: "me.api_keys.list",
@@ -6044,73 +6229,99 @@ var personalApiKeyOperations = {
6044
6229
  };
6045
6230
 
6046
6231
  // ../api-contract/src/platform.ts
6047
- import { z as z33 } from "zod";
6048
- var platformAssetKindSchema = z33.object({
6049
- kind: z33.string(),
6050
- plural: z33.string(),
6232
+ import { z as z34 } from "zod";
6233
+ var platformAssetKindSchema = z34.object({
6234
+ kind: z34.string(),
6235
+ plural: z34.string(),
6051
6236
  /**
6052
6237
  * `active`: the newest write is what the kind's users are served. `draft_published`: a write
6053
6238
  * moves a draft, and `assets.publish` moves what its users are served.
6054
6239
  */
6055
- lifecycle: z33.enum(["active", "draft_published"]),
6240
+ lifecycle: z34.enum(["active", "draft_published"]),
6056
6241
  /** `config`: a revision is its config. `files`: a revision holds a set of files beside it. */
6057
- representation: z33.enum(["config", "files"]),
6058
- actions: z33.object({
6059
- read: z33.string(),
6060
- write: z33.string(),
6061
- delete: z33.string(),
6242
+ representation: z34.enum(["config", "files"]),
6243
+ /**
6244
+ * Who may own an asset of the kind: the instance, a workspace, or either. An asset a
6245
+ * workspace owns is read and written by naming that workspace.
6246
+ */
6247
+ owners: z34.array(z34.enum(["instance", "workspace"])),
6248
+ actions: z34.object({
6249
+ read: z34.string(),
6250
+ write: z34.string(),
6251
+ delete: z34.string(),
6062
6252
  /** Named by a kind with the lifecycle `draft_published`. */
6063
- publish: z33.string().optional(),
6253
+ publish: z34.string().optional(),
6064
6254
  /**
6065
6255
  * Only for a kind whose assets are used through a right of their own: a Connection, or a
6066
6256
  * kind whose published revision has users of its own.
6067
6257
  */
6068
- use: z33.string().optional()
6258
+ use: z34.string().optional()
6069
6259
  }),
6070
6260
  /**
6071
6261
  * The rights of the kind a service principal can be granted, each on one named asset. Left
6072
6262
  * out for a kind that has none. `permissions.grant` hands out these and no other.
6073
6263
  */
6074
- principalActions: z33.array(z33.string()).optional(),
6264
+ principalActions: z34.array(z34.string()).optional(),
6075
6265
  /** The file of a `files` kind that holds its manifest. */
6076
- manifestFile: z33.string().optional(),
6266
+ manifestFile: z34.string().optional(),
6077
6267
  /** What a write of a `files` kind is held to, by the names of the limits. */
6078
- limits: z33.object({
6079
- FILE_SET_MAX_FILES: z33.number().int().positive(),
6080
- FILE_SET_MAX_FILE_BYTES: z33.number().int().positive(),
6081
- FILE_SET_MAX_TOTAL_BYTES: z33.number().int().positive(),
6082
- ASSET_PUT_FILES_MAX_BYTES: z33.number().int().positive(),
6083
- ASSET_GET_FILES_MAX_BYTES: z33.number().int().positive()
6268
+ limits: z34.object({
6269
+ FILE_SET_MAX_FILES: z34.number().int().positive(),
6270
+ FILE_SET_MAX_FILE_BYTES: z34.number().int().positive(),
6271
+ FILE_SET_MAX_TOTAL_BYTES: z34.number().int().positive(),
6272
+ ASSET_PUT_FILES_MAX_BYTES: z34.number().int().positive(),
6273
+ ASSET_GET_FILES_MAX_BYTES: z34.number().int().positive()
6084
6274
  }).optional()
6085
6275
  });
6086
- var platformContextNamespaceSchema = z33.object({
6087
- prefix: z33.string(),
6088
- displayName: z33.string(),
6089
- actions: z33.array(z33.string())
6276
+ var platformContextNamespaceSchema = z34.object({
6277
+ prefix: z34.string(),
6278
+ displayName: z34.string(),
6279
+ actions: z34.array(z34.string())
6090
6280
  });
6091
- var platformContextSchema = z33.object({
6092
- instance: z33.object({ id: z33.string(), name: z33.string() }),
6093
- release: z33.object({ version: z33.string() }),
6094
- kinds: z33.array(platformAssetKindSchema),
6281
+ var platformContextWorkspaceSchema = z34.object({
6282
+ id: z34.string(),
6283
+ name: z34.string(),
6284
+ kind: z34.enum(["personal", "shared"]),
6285
+ /** The caller is a member: a new agent it makes there starts offered there. */
6286
+ member: z34.boolean(),
6287
+ actions: z34.array(z34.string())
6288
+ });
6289
+ var platformContextSchema = z34.object({
6290
+ instance: z34.object({ id: z34.string(), name: z34.string() }),
6291
+ release: z34.object({
6292
+ version: z34.string(),
6293
+ /**
6294
+ * The version of `@work-shape/catalyst-cli` on npm that connects a local agent to this
6295
+ * instance. Absent while none is published for it.
6296
+ */
6297
+ cliVersion: z34.string().optional()
6298
+ }),
6299
+ kinds: z34.array(platformAssetKindSchema),
6095
6300
  /**
6096
6301
  * The actions of the listed kinds the caller can use on the instance's own assets as a
6097
6302
  * whole, whatever their names. A right in one Namespace or on one asset is not among them.
6098
6303
  */
6099
- actions: z33.array(z33.string()),
6304
+ actions: z34.array(z34.string()),
6100
6305
  /**
6101
6306
  * The read actions of the listed kinds a grant allows the caller on one asset or on what one
6102
6307
  * workspace owns. Which assets those are, the kind's list says: a deny or a deleted asset can
6103
6308
  * leave it empty.
6104
6309
  */
6105
- assetActions: z33.array(z33.string()),
6106
- namespaces: z33.array(platformContextNamespaceSchema),
6310
+ assetActions: z34.array(z34.string()),
6311
+ namespaces: z34.array(platformContextNamespaceSchema),
6312
+ /**
6313
+ * The workspaces whose own assets the caller can use an action of a listed kind on, each
6314
+ * with those actions: through a membership, a grant on the workspace or a right on the
6315
+ * whole instance. A workspace where the caller can use none is left out.
6316
+ */
6317
+ workspaces: z34.array(platformContextWorkspaceSchema),
6107
6318
  /**
6108
6319
  * The section rows of the interface's sidebar that are there for the caller, of those that
6109
6320
  * belong to no workspace. `workflows`: the module is on, and the caller is listed at least
6110
6321
  * one Workflow by `workflows.list` or holds `workflow.write` on the instance, in a Namespace
6111
6322
  * or on one Workflow. A member is never shown an empty row; whoever may build one is.
6112
6323
  */
6113
- visibleSections: z33.array(z33.enum(["workflows"]))
6324
+ visibleSections: z34.array(z34.enum(["workflows"]))
6114
6325
  });
6115
6326
 
6116
6327
  // ../api-contract/src/operations/platform.ts
@@ -6184,32 +6395,32 @@ var referenceOperations = {
6184
6395
  };
6185
6396
 
6186
6397
  // ../api-contract/src/system.ts
6187
- import { z as z34 } from "zod";
6188
- var healthSchema = z34.object({
6189
- status: z34.literal("ok"),
6190
- clientInstanceId: z34.string(),
6398
+ import { z as z35 } from "zod";
6399
+ var healthSchema = z35.object({
6400
+ status: z35.literal("ok"),
6401
+ clientInstanceId: z35.string(),
6191
6402
  time: timestampSchema
6192
6403
  });
6193
- var readySchema = z34.object({
6194
- status: z34.literal("ready"),
6404
+ var readySchema = z35.object({
6405
+ status: z35.literal("ready"),
6195
6406
  /** The newest migration of this release. */
6196
- migration: z34.string()
6407
+ migration: z35.string()
6197
6408
  });
6198
- var notReadySchema = z34.object({
6199
- status: z34.literal("not_ready"),
6200
- reason: z34.enum(["database_unreachable", "database_behind"]),
6201
- missing: z34.array(z34.string()).optional()
6409
+ var notReadySchema = z35.object({
6410
+ status: z35.literal("not_ready"),
6411
+ reason: z35.enum(["database_unreachable", "database_behind"]),
6412
+ missing: z35.array(z35.string()).optional()
6202
6413
  });
6203
- var readinessSchema = z34.discriminatedUnion("status", [readySchema, notReadySchema]);
6204
- var capturedMailSchema = z34.object({
6205
- id: z34.string(),
6206
- to: z34.object({
6207
- email: z34.string(),
6208
- displayLabel: z34.string().optional()
6414
+ var readinessSchema = z35.discriminatedUnion("status", [readySchema, notReadySchema]);
6415
+ var capturedMailSchema = z35.object({
6416
+ id: z35.string(),
6417
+ to: z35.object({
6418
+ email: z35.string(),
6419
+ displayLabel: z35.string().optional()
6209
6420
  }),
6210
- subject: z34.string(),
6211
- text: z34.string(),
6212
- html: z34.string(),
6421
+ subject: z35.string(),
6422
+ text: z35.string(),
6423
+ html: z35.string(),
6213
6424
  sentAt: timestampSchema
6214
6425
  });
6215
6426
 
@@ -6442,9 +6653,9 @@ var userOperations = {
6442
6653
  };
6443
6654
 
6444
6655
  // ../api-contract/src/operations/workflows.ts
6445
- import { z as z35 } from "zod";
6656
+ import { z as z36 } from "zod";
6446
6657
  var WORKFLOW_RUN_OUTPUT_PREVIEW_MAX_CHARS = 2e3;
6447
- var runStatusSchema = z35.enum([
6658
+ var runStatusSchema = z36.enum([
6448
6659
  "queued",
6449
6660
  "running",
6450
6661
  "waiting",
@@ -6453,8 +6664,8 @@ var runStatusSchema = z35.enum([
6453
6664
  "cancelled",
6454
6665
  "limit_reached"
6455
6666
  ]);
6456
- var stepRunStatusSchema = z35.enum(["queued", "running", "waiting", "done", "failed", "cancelled"]);
6457
- var stepTypeSchema = z35.enum([
6667
+ var stepRunStatusSchema = z36.enum(["queued", "running", "waiting", "done", "failed", "cancelled"]);
6668
+ var stepTypeSchema = z36.enum([
6458
6669
  "trigger",
6459
6670
  "operation",
6460
6671
  "loop",
@@ -6466,50 +6677,50 @@ var stepTypeSchema = z35.enum([
6466
6677
  "condition",
6467
6678
  "parallel"
6468
6679
  ]);
6469
- var countsSchema = z35.object({
6470
- total: z35.number().int().min(0),
6471
- done: z35.number().int().min(0),
6472
- failed: z35.number().int().min(0),
6473
- running: z35.number().int().min(0)
6680
+ var countsSchema = z36.object({
6681
+ total: z36.number().int().min(0),
6682
+ done: z36.number().int().min(0),
6683
+ failed: z36.number().int().min(0),
6684
+ running: z36.number().int().min(0)
6474
6685
  });
6475
- var workflowRunErrorSchema = z35.object({
6686
+ var workflowRunErrorSchema = z36.object({
6476
6687
  /** A code of the engine, or the code of the refusal or failure of the step's operation. */
6477
- code: z35.string(),
6688
+ code: z36.string(),
6478
6689
  /** A sentence of the product. Never a value of the run and never text of an outside system. */
6479
- message: z35.string(),
6690
+ message: z36.string(),
6480
6691
  /** Where the run's principal was refused for a right it does not hold: that right. */
6481
- action: z35.string().optional(),
6692
+ action: z36.string().optional(),
6482
6693
  /** On a run: the step run it ended at. */
6483
- stepRunId: z35.string().optional()
6694
+ stepRunId: z36.string().optional()
6484
6695
  });
6485
- var workflowOutputPreviewSchema = z35.object({
6696
+ var workflowOutputPreviewSchema = z36.object({
6486
6697
  /** The start of the value as JSON. */
6487
- text: z35.string().max(WORKFLOW_RUN_OUTPUT_PREVIEW_MAX_CHARS),
6488
- truncated: z35.boolean()
6698
+ text: z36.string().max(WORKFLOW_RUN_OUTPUT_PREVIEW_MAX_CHARS),
6699
+ truncated: z36.boolean()
6489
6700
  });
6490
- var workflowRunSchema = z35.object({
6491
- id: z35.string(),
6492
- workflow: z35.string(),
6701
+ var workflowRunSchema = z36.object({
6702
+ id: z36.string(),
6703
+ workflow: z36.string(),
6493
6704
  /** The revision of the definition the run started on and keeps to its end. */
6494
- definitionRevisionId: z35.string(),
6495
- mode: z35.enum(["live", "test"]),
6496
- trigger: z35.enum(["manual", "schedule", "parent"]),
6705
+ definitionRevisionId: z36.string(),
6706
+ mode: z36.enum(["live", "test"]),
6707
+ trigger: z36.enum(["manual", "schedule", "parent"]),
6497
6708
  status: runStatusSchema,
6498
6709
  /**
6499
6710
  * The service principal the run acts as. Its rights are the rights of every step. A test run
6500
6711
  * (`mode: test`) acts as the person who started it, bounded by this principal.
6501
6712
  */
6502
- principal: z35.object({ id: z35.string(), name: z35.string(), displayLabel: z35.string() }),
6503
- startedBy: z35.object({
6504
- kind: z35.enum(["user", "service_principal"]),
6505
- id: z35.string(),
6506
- label: z35.string()
6713
+ principal: z36.object({ id: z36.string(), name: z36.string(), displayLabel: z36.string() }),
6714
+ startedBy: z36.object({
6715
+ kind: z36.enum(["user", "service_principal"]),
6716
+ id: z36.string(),
6717
+ label: z36.string()
6507
6718
  }),
6508
6719
  /** The due time of the schedule that started the run. Missing on every other run. */
6509
6720
  scheduledFor: timestampSchema.optional(),
6510
6721
  /** The Operation Run of the call that started the run. */
6511
- operationRunId: z35.string().optional(),
6512
- stepRunCount: z35.number().int().min(0),
6722
+ operationRunId: z36.string().optional(),
6723
+ stepRunCount: z36.number().int().min(0),
6513
6724
  /** After this moment no further step starts and the run ends as `limit_reached`. */
6514
6725
  deadlineAt: timestampSchema,
6515
6726
  error: workflowRunErrorSchema.optional(),
@@ -6518,7 +6729,7 @@ var workflowRunSchema = z35.object({
6518
6729
  startedAt: timestampSchema.optional(),
6519
6730
  finishedAt: timestampSchema.optional(),
6520
6731
  /** The person who cancelled the run. Missing where a service principal did, and on a run nobody cancelled. */
6521
- cancelledBy: z35.string().optional(),
6732
+ cancelledBy: z36.string().optional(),
6522
6733
  cancelledAt: timestampSchema.optional(),
6523
6734
  /**
6524
6735
  * When the values of the run were removed: its input and result, and the inputs and outputs
@@ -6526,98 +6737,98 @@ var workflowRunSchema = z35.object({
6526
6737
  */
6527
6738
  dataPrunedAt: timestampSchema.optional()
6528
6739
  });
6529
- var workflowStepRunSchema = z35.object({
6530
- id: z35.string(),
6531
- runId: z35.string(),
6532
- stepId: z35.string(),
6740
+ var workflowStepRunSchema = z36.object({
6741
+ id: z36.string(),
6742
+ runId: z36.string(),
6743
+ stepId: z36.string(),
6533
6744
  stepType: stepTypeSchema,
6534
6745
  /** The step's path in the run, such as `each_client[3].bookings`. */
6535
- key: z35.string(),
6746
+ key: z36.string(),
6536
6747
  /** The step run of the block that holds the step: today a loop. */
6537
- containerStepRunId: z35.string().optional(),
6748
+ containerStepRunId: z36.string().optional(),
6538
6749
  /** The branch of that block the step stands in. No block with branches runs yet. */
6539
- branch: z35.string().optional(),
6750
+ branch: z36.string().optional(),
6540
6751
  /** Which item of that loop, from 0, and what the loop's `itemLabel` field holds for it. */
6541
- iteration: z35.object({ index: z35.number().int().min(0), label: z35.string().optional() }).optional(),
6752
+ iteration: z36.object({ index: z36.number().int().min(0), label: z36.string().optional() }).optional(),
6542
6753
  /** On the step run of a loop: how many items it has and how far they are. */
6543
6754
  loop: countsSchema.optional(),
6544
6755
  /** On the step run of a loop: how the step runs of each step directly inside it stand, by step id. */
6545
- bodyCounts: z35.record(z35.string(), countsSchema).optional(),
6756
+ bodyCounts: z36.record(z36.string(), countsSchema).optional(),
6546
6757
  /**
6547
6758
  * On the step run of a loop: how many of its items run at once. `asked` is the loop's
6548
6759
  * `concurrency`, an upper bound; `ranWith` is what one process runs of it, which the step
6549
6760
  * slots of a process bound.
6550
6761
  */
6551
- concurrency: z35.object({ asked: z35.number().int().min(1), ranWith: z35.number().int().min(1) }).optional(),
6762
+ concurrency: z36.object({ asked: z36.number().int().min(1), ranWith: z36.number().int().min(1) }).optional(),
6552
6763
  /** On the step run of an operation step: what its operation does and what it belongs to. */
6553
- effect: z35.enum(["reading", "changing"]).optional(),
6554
- source: z35.string().optional(),
6764
+ effect: z36.enum(["reading", "changing"]).optional(),
6765
+ source: z36.string().optional(),
6555
6766
  /**
6556
6767
  * On the step run of an operation step: where its call went (ADR-0031). `live` on every step
6557
6768
  * of a run of the published revision; in a test run `dry_run` for a changing step that was
6558
6769
  * simulated, `test` for a call to a test target, `live` for a read and for "Run live once".
6559
6770
  */
6560
- target: z35.enum(["live", "test", "dry_run"]).optional(),
6771
+ target: z36.enum(["live", "test", "dry_run"]).optional(),
6561
6772
  /**
6562
6773
  * On a step run of a test run: the tester's start satisfied the step's `confirm`, and a run
6563
6774
  * of the published revision asks for an approval here.
6564
6775
  */
6565
- publishedWouldAsk: z35.enum(["approval"]).optional(),
6776
+ publishedWouldAsk: z36.enum(["approval"]).optional(),
6566
6777
  /**
6567
6778
  * On the step run of a code step that ran: what its script wrote to its output and error
6568
6779
  * streams beside its result, each cut to 16 KiB.
6569
6780
  */
6570
- log: z35.object({ stdout: z35.string(), stderr: z35.string(), truncated: z35.boolean() }).optional(),
6781
+ log: z36.object({ stdout: z36.string(), stderr: z36.string(), truncated: z36.boolean() }).optional(),
6571
6782
  status: stepRunStatusSchema,
6572
- attempt: z35.number().int().min(0),
6783
+ attempt: z36.number().int().min(0),
6573
6784
  /** The Operation Run of the call an operation step made. */
6574
- operationRunId: z35.string().optional(),
6785
+ operationRunId: z36.string().optional(),
6575
6786
  error: workflowRunErrorSchema.optional(),
6576
6787
  outputPreview: workflowOutputPreviewSchema.optional(),
6577
6788
  createdAt: timestampSchema,
6578
6789
  startedAt: timestampSchema.optional(),
6579
6790
  finishedAt: timestampSchema.optional()
6580
6791
  });
6581
- var startWorkflowRunRequestSchema = z35.object({
6792
+ var startWorkflowRunRequestSchema = z36.object({
6582
6793
  /** The input of the run, checked against the `inputs` of the Workflow's definition. */
6583
- input: z35.record(z35.string(), z35.unknown()).optional()
6794
+ input: z36.record(z36.string(), z36.unknown()).optional()
6584
6795
  });
6585
6796
  var listWorkflowStepRunsQuerySchema = listQuerySchema.extend({
6586
- topLevel: z35.enum(["true", "false"]).optional(),
6587
- containerStepRunId: z35.string().min(1).optional(),
6797
+ topLevel: z36.enum(["true", "false"]).optional(),
6798
+ containerStepRunId: z36.string().min(1).optional(),
6588
6799
  status: stepRunStatusSchema.optional(),
6589
- iteration: z35.coerce.number().int().min(0).optional()
6800
+ iteration: z36.coerce.number().int().min(0).optional()
6590
6801
  });
6591
- var workflowStartIssueSchema = z35.object({
6592
- code: z35.string(),
6802
+ var workflowStartIssueSchema = z36.object({
6803
+ code: z36.string(),
6593
6804
  /** A sentence of the product. Never a value of a run. */
6594
- message: z35.string(),
6805
+ message: z36.string(),
6595
6806
  /** Where in the definition it is, such as `steps.check.type`. */
6596
- path: z35.string().optional()
6807
+ path: z36.string().optional()
6597
6808
  });
6598
- var workflowSummarySchema = z35.object({
6599
- id: z35.string(),
6600
- name: z35.string(),
6601
- description: z35.string().optional(),
6809
+ var workflowSummarySchema = z36.object({
6810
+ id: z36.string(),
6811
+ name: z36.string(),
6812
+ description: z36.string().optional(),
6602
6813
  /** The published revision: what a run started now has as its `definitionRevisionId`. */
6603
- definitionRevisionId: z35.string(),
6814
+ definitionRevisionId: z36.string(),
6604
6815
  /** Whether a start by hand is accepted now, as far as that is known before its input. */
6605
- runnable: z35.boolean(),
6816
+ runnable: z36.boolean(),
6606
6817
  /** Why not. Empty exactly where `runnable` is true. */
6607
- reasons: z35.array(workflowStartIssueSchema),
6818
+ reasons: z36.array(workflowStartIssueSchema),
6608
6819
  /**
6609
6820
  * The published revision's `inputs`: the JSON Schema a start's `input` is checked against,
6610
6821
  * from which a page builds its start form. Missing where the definition cannot be read.
6611
6822
  */
6612
- inputs: z35.record(z35.string(), z35.unknown()).optional(),
6823
+ inputs: z36.record(z36.string(), z36.unknown()).optional(),
6613
6824
  /**
6614
6825
  * What the caller may do with this Workflow, of `workflow.read` (its runs) and `workflow.use`
6615
6826
  * (a start). Never empty: a Workflow the caller holds neither on is not listed.
6616
6827
  */
6617
- actions: z35.array(z35.string()),
6828
+ actions: z36.array(z36.string()),
6618
6829
  /** The latest run of the Workflow. Missing on one that never ran. */
6619
- lastRun: z35.object({
6620
- id: z35.string(),
6830
+ lastRun: z36.object({
6831
+ id: z36.string(),
6621
6832
  status: runStatusSchema,
6622
6833
  startedAt: timestampSchema.optional(),
6623
6834
  endedAt: timestampSchema.optional()
@@ -6634,7 +6845,7 @@ var workflowOperations = {
6634
6845
  scope: "config_assets:read",
6635
6846
  requires: [],
6636
6847
  effect: "reading",
6637
- response: json(z35.object({ items: z35.array(workflowSummarySchema) })),
6848
+ response: json(z36.object({ items: z36.array(workflowSummarySchema) })),
6638
6849
  errors: [],
6639
6850
  rateClass: "read"
6640
6851
  }),
@@ -6747,6 +6958,7 @@ var apiOperations = {
6747
6958
  ...guardrailOperations,
6748
6959
  ...workflowOperations,
6749
6960
  ...dataStoreOperations,
6961
+ ...knowledgeOperations,
6750
6962
  ...userOperations,
6751
6963
  ...apiAccessOperations,
6752
6964
  ...personalApiKeyOperations,
@@ -6757,11 +6969,20 @@ var apiOperations = {
6757
6969
  ...systemOperations
6758
6970
  };
6759
6971
 
6972
+ // ../api-contract/src/openapi.ts
6973
+ var RELEASE_VERSION = package_default.version;
6974
+ var API_ERROR_CODE_ORDER = appErrorCodeSchema.options;
6975
+
6976
+ // src/asset-commands.ts
6977
+ import { readFile as readFile5 } from "fs/promises";
6978
+ import { resolve as resolve5 } from "path";
6979
+ import { parseArgs } from "util";
6980
+
6760
6981
  // src/api.ts
6761
6982
  import { randomUUID } from "crypto";
6762
6983
 
6763
6984
  // ../api-client/src/errors.ts
6764
- import { z as z36 } from "zod";
6985
+ import { z as z38 } from "zod";
6765
6986
  var ApiError = class extends Error {
6766
6987
  status;
6767
6988
  code;
@@ -6828,7 +7049,7 @@ function readCorrelationId(payload) {
6828
7049
  return error && "correlationId" in error && typeof error.correlationId === "string" ? error.correlationId : void 0;
6829
7050
  }
6830
7051
  function readRetryAfterSeconds(payload) {
6831
- const parsed = z36.object({ error: z36.object({ details: rateLimitedDetailsSchema }) }).safeParse(payload);
7052
+ const parsed = z38.object({ error: z38.object({ details: rateLimitedDetailsSchema }) }).safeParse(payload);
6832
7053
  return parsed.success ? parsed.data.error.details.retryAfterSeconds : void 0;
6833
7054
  }
6834
7055
 
@@ -7091,6 +7312,7 @@ async function createConfigApi(options) {
7091
7312
  fetchImpl
7092
7313
  });
7093
7314
  const sleep = options.sleep ?? defaultSleep;
7315
+ const scope = options.workspaceId === void 0 ? {} : { workspaceId: options.workspaceId };
7094
7316
  const change = async (request, unanswered) => {
7095
7317
  const idempotencyKey2 = randomUUID();
7096
7318
  for (let attempt = 1; ; attempt += 1) {
@@ -7132,7 +7354,11 @@ async function createConfigApi(options) {
7132
7354
  sleep,
7133
7355
  () => client.assets.list({
7134
7356
  params: { kind },
7135
- query: { limit: MAX_LIST_LIMIT, ...cursor === void 0 ? {} : { cursor } }
7357
+ query: {
7358
+ limit: MAX_LIST_LIMIT,
7359
+ ...scope,
7360
+ ...cursor === void 0 ? {} : { cursor }
7361
+ }
7136
7362
  })
7137
7363
  );
7138
7364
  assets.push(...page2.items);
@@ -7143,7 +7369,10 @@ async function createConfigApi(options) {
7143
7369
  /** One asset with its content, or nothing where the instance holds none of that name. */
7144
7370
  getAsset: async (kind, name) => {
7145
7371
  try {
7146
- return await read(sleep, () => client.assets.get({ params: { kind, name } }));
7372
+ return await read(
7373
+ sleep,
7374
+ () => client.assets.get({ params: { kind, name }, query: { ...scope } })
7375
+ );
7147
7376
  } catch (error) {
7148
7377
  if (isMissingAsset(error)) return void 0;
7149
7378
  throw error;
@@ -7153,7 +7382,7 @@ async function createConfigApi(options) {
7153
7382
  getLatestRevision: async (kind, name) => {
7154
7383
  const page2 = await read(
7155
7384
  sleep,
7156
- () => client.assets.revisions.list({ params: { kind, name }, query: { limit: 1 } })
7385
+ () => client.assets.revisions.list({ params: { kind, name }, query: { limit: 1, ...scope } })
7157
7386
  );
7158
7387
  return page2.items[0];
7159
7388
  },
@@ -7222,8 +7451,8 @@ function withoutRedirects(fetchImpl) {
7222
7451
  function instanceFetch(fetchImpl) {
7223
7452
  return withoutRedirects(fetchImpl ?? fetch);
7224
7453
  }
7225
- function isLoopbackHostname(hostname2) {
7226
- return hostname2 === "localhost" || hostname2 === "::1" || hostname2 === "[::1]" || /^127(?:\.\d{1,3}){3}$/u.test(hostname2);
7454
+ function isLoopbackHostname(hostname3) {
7455
+ return hostname3 === "localhost" || hostname3 === "::1" || hostname3 === "[::1]" || /^127(?:\.\d{1,3}){3}$/u.test(hostname3);
7227
7456
  }
7228
7457
  async function exchangeApiKey(fetchImpl, baseUrl, apiKey) {
7229
7458
  try {
@@ -7278,8 +7507,8 @@ function redactSecret(message, secret) {
7278
7507
  }
7279
7508
 
7280
7509
  // src/asset-sources.ts
7281
- import { readFile as readFile3, stat as stat2 } from "fs/promises";
7282
- import { basename, dirname as dirname2, extname, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
7510
+ import { readFile as readFile4, stat as stat3 } from "fs/promises";
7511
+ import { basename, dirname as dirname3, extname, join as join3, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
7283
7512
 
7284
7513
  // src/guardrail-files.ts
7285
7514
  import { readFile, stat } from "fs/promises";
@@ -7353,9 +7582,90 @@ function isRecord2(value) {
7353
7582
  }
7354
7583
 
7355
7584
  // src/working-copy.ts
7356
- import { mkdir, readFile as readFile2, readdir, rename, rm, rmdir, writeFile } from "fs/promises";
7357
- import { dirname, isAbsolute, relative, resolve, sep } from "path";
7585
+ import { mkdir as mkdir2, readFile as readFile3, readdir, rename as rename2, rm, rmdir, writeFile as writeFile2 } from "fs/promises";
7586
+ import { dirname as dirname2, isAbsolute, relative, resolve as resolve2, sep } from "path";
7358
7587
  import yaml3 from "js-yaml";
7588
+
7589
+ // src/credentials.ts
7590
+ import { chmod, mkdir, readFile as readFile2, rename, stat as stat2, writeFile } from "fs/promises";
7591
+ import { homedir } from "os";
7592
+ import { dirname, join as join2, resolve } from "path";
7593
+ var CREDENTIALS_FILENAME = "credentials.json";
7594
+ function credentialsPath(env) {
7595
+ const folder = env.CATALYST_CONFIG_HOME ?? join2(env.XDG_CONFIG_HOME ?? join2(homedir(), ".config"), "catalyst");
7596
+ return resolve(folder, CREDENTIALS_FILENAME);
7597
+ }
7598
+ function credentialOrigin(url) {
7599
+ return new URL(url).origin;
7600
+ }
7601
+ async function readStoredCredential(env, url) {
7602
+ return (await readCredentialFile(credentialsPath(env))).instances[credentialOrigin(url)];
7603
+ }
7604
+ async function savedInstanceOrigins(env) {
7605
+ return Object.keys((await readCredentialFile(credentialsPath(env))).instances);
7606
+ }
7607
+ async function saveCredential(env, url, credential) {
7608
+ const path = credentialsPath(env);
7609
+ const file = await readCredentialFile(path);
7610
+ file.instances[credentialOrigin(url)] = credential;
7611
+ await writeCredentialFile(path, file);
7612
+ return path;
7613
+ }
7614
+ async function removeCredential(env, url) {
7615
+ const path = credentialsPath(env);
7616
+ const file = await readCredentialFile(path);
7617
+ const origin = credentialOrigin(url);
7618
+ if (file.instances[origin] === void 0) return false;
7619
+ delete file.instances[origin];
7620
+ await writeCredentialFile(path, file);
7621
+ return true;
7622
+ }
7623
+ async function readCredentialFile(path) {
7624
+ let contents;
7625
+ try {
7626
+ contents = await readFile2(path, "utf8");
7627
+ } catch (error) {
7628
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
7629
+ return { version: 1, instances: {} };
7630
+ }
7631
+ throw error;
7632
+ }
7633
+ if (process.platform !== "win32" && ((await stat2(path)).mode & 63) !== 0) {
7634
+ throw new Error(
7635
+ `${path} can be read by other users of this machine. Run 'chmod 600 ${path}', or remove it and run 'catalyst login' again.`
7636
+ );
7637
+ }
7638
+ const parsed = JSON.parse(contents);
7639
+ if (!isCredentialFile(parsed)) {
7640
+ throw new Error(`${path} is not a credentials file of this CLI. Remove it and log in again.`);
7641
+ }
7642
+ return parsed;
7643
+ }
7644
+ async function writeCredentialFile(path, file) {
7645
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
7646
+ const temporary = `${path}.${process.pid}.tmp`;
7647
+ await writeFile(temporary, `${JSON.stringify(file, null, 2)}
7648
+ `, { mode: 384 });
7649
+ await chmod(temporary, 384);
7650
+ await rename(temporary, path);
7651
+ }
7652
+ function isCredentialFile(value) {
7653
+ if (typeof value !== "object" || value === null) return false;
7654
+ if (!("version" in value) || value.version !== 1) return false;
7655
+ if (!("instances" in value) || typeof value.instances !== "object" || value.instances === null) {
7656
+ return false;
7657
+ }
7658
+ return Object.values(value.instances).every(
7659
+ (entry) => typeof entry === "object" && entry !== null && "apiKey" in entry && typeof entry.apiKey === "string" && "keyId" in entry && typeof entry.keyId === "string" && "name" in entry && typeof entry.name === "string" && "expiresAt" in entry && typeof entry.expiresAt === "string" && "savedAt" in entry && typeof entry.savedAt === "string"
7660
+ );
7661
+ }
7662
+
7663
+ // src/env.ts
7664
+ function readConfigCliEnv(env) {
7665
+ return env ?? process.env;
7666
+ }
7667
+
7668
+ // src/working-copy.ts
7359
7669
  var MANIFEST_FILENAME = "catalyst.yaml";
7360
7670
  var STATE_FILENAME = ".catalyst-state.json";
7361
7671
  var WorkingCopyValidationError = class extends Error {
@@ -7383,12 +7693,19 @@ function parseManifest(contents, source = MANIFEST_FILENAME) {
7383
7693
  const namespace = readOptionalString(value, "namespace", `${source} instance '${name}'`);
7384
7694
  if (namespace !== void 0 && !NAMESPACE_PREFIX_PATTERN.test(namespace)) {
7385
7695
  throw new Error(
7386
- `${source} instance '${name}' names no Namespace prefix: '${namespace}'. A prefix is lowercase words joined by '-' and ends in '-', such as 'kai-'.`
7696
+ `${source} instance '${name}' names no Namespace prefix: '${namespace}'. A prefix is lowercase words joined by '-' and ends in '-', such as 'tax-'.`
7697
+ );
7698
+ }
7699
+ const workspace = readOptionalString(value, "workspace", `${source} instance '${name}'`);
7700
+ if (namespace !== void 0 && workspace !== void 0) {
7701
+ throw new Error(
7702
+ `${source} instance '${name}' names a Namespace and a workspace. A working copy belongs to one of them.`
7387
7703
  );
7388
7704
  }
7389
7705
  instances[name] = {
7390
7706
  url: normalizeUrl(value.url, `${source} instance '${name}'`),
7391
- ...namespace === void 0 ? {} : { namespace }
7707
+ ...namespace === void 0 ? {} : { namespace },
7708
+ ...workspace === void 0 ? {} : { workspace }
7392
7709
  };
7393
7710
  }
7394
7711
  const defaultInstance = readOptionalString(input, "defaultInstance", source);
@@ -7403,8 +7720,8 @@ function parseManifest(contents, source = MANIFEST_FILENAME) {
7403
7720
  };
7404
7721
  }
7405
7722
  async function readManifest(workingDir) {
7406
- const path = resolve(workingDir, MANIFEST_FILENAME);
7407
- return parseManifest(await readFile2(path, "utf8"), path);
7723
+ const path = resolve2(workingDir, MANIFEST_FILENAME);
7724
+ return parseManifest(await readFile3(path, "utf8"), path);
7408
7725
  }
7409
7726
  async function readManifestIfPresent(workingDir) {
7410
7727
  try {
@@ -7414,24 +7731,38 @@ async function readManifestIfPresent(workingDir) {
7414
7731
  throw error;
7415
7732
  }
7416
7733
  }
7417
- function resolveInstance(manifest, requestedInstance) {
7418
- const key = requestedInstance ?? manifest.defaultInstance;
7419
- if (!key) {
7734
+ async function resolveInstance(manifest, options = {}) {
7735
+ const key = options.instance ?? manifest?.defaultInstance;
7736
+ if (key !== void 0 && key !== "") {
7737
+ const configured = manifest?.instances[key];
7738
+ if (configured) return { key, ...configured };
7739
+ return { key, url: normalizeUrl(key, `Instance '${key}'`) };
7740
+ }
7741
+ const refusal = `No instance selected; pass --instance or set defaultInstance in ${MANIFEST_FILENAME}.`;
7742
+ if (manifest !== void 0) {
7743
+ const names = Object.keys(manifest.instances);
7420
7744
  throw new Error(
7421
- `No instance selected; pass --instance or set defaultInstance in ${MANIFEST_FILENAME}`
7745
+ names.length === 0 ? refusal : `${refusal} ${MANIFEST_FILENAME} names ${names.join(", ")}.`
7422
7746
  );
7423
7747
  }
7424
- const configured = manifest.instances[key];
7425
- if (configured) {
7426
- return { key, ...configured };
7748
+ if (readConfigCliEnv(options.env).CATALYST_API_KEY) {
7749
+ throw new Error(`${refusal} CATALYST_API_KEY is set: name the instance it belongs to.`);
7750
+ }
7751
+ const saved = await savedInstanceOrigins(readConfigCliEnv(options.env));
7752
+ const [origin, ...more] = saved;
7753
+ if (origin === void 0) {
7754
+ throw new Error(
7755
+ `${refusal} No key is saved; 'catalyst login --instance <https-url>' saves one.`
7756
+ );
7427
7757
  }
7428
- return { key, url: normalizeUrl(key, `Instance '${key}'`) };
7758
+ if (more.length > 0) throw new Error(`${refusal} Keys are saved for ${saved.join(", ")}.`);
7759
+ return { key: origin, url: origin };
7429
7760
  }
7430
7761
  var ASSET_KEY_PATTERN = /^(agent|skill|guardrail):.+$/u;
7431
7762
  async function readStateFile(path) {
7432
7763
  let contents;
7433
7764
  try {
7434
- contents = await readFile2(path, "utf8");
7765
+ contents = await readFile3(path, "utf8");
7435
7766
  } catch (error) {
7436
7767
  if (isNodeError(error) && error.code === "ENOENT") {
7437
7768
  return { instances: {} };
@@ -7491,11 +7822,11 @@ async function readStateFile(path) {
7491
7822
  return { instances };
7492
7823
  }
7493
7824
  async function writeStateFile(path, state) {
7494
- await mkdir(dirname(path), { recursive: true });
7825
+ await mkdir2(dirname2(path), { recursive: true });
7495
7826
  const temporaryPath = `${path}.${process.pid}.tmp`;
7496
- await writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}
7827
+ await writeFile2(temporaryPath, `${JSON.stringify(state, null, 2)}
7497
7828
  `, "utf8");
7498
- await rename(temporaryPath, path);
7829
+ await rename2(temporaryPath, path);
7499
7830
  }
7500
7831
  async function readWorkingCopy(workingDir, manifest) {
7501
7832
  const [agentFiles, skillFiles, guardrailFiles] = await Promise.all([
@@ -7510,7 +7841,7 @@ async function readWorkingCopy(workingDir, manifest) {
7510
7841
  for (const path of agentFiles) {
7511
7842
  const file = toRelativePath(workingDir, path);
7512
7843
  try {
7513
- const input = loadYamlData(await readFile2(path, "utf8"), file);
7844
+ const input = loadYamlData(await readFile3(path, "utf8"), file);
7514
7845
  const parsed = agentConfigSchema.safeParse(input);
7515
7846
  if (parsed.success) {
7516
7847
  agents.push(parsed.data);
@@ -7524,7 +7855,7 @@ async function readWorkingCopy(workingDir, manifest) {
7524
7855
  for (const path of skillFiles) {
7525
7856
  const file = toRelativePath(workingDir, path);
7526
7857
  try {
7527
- const contents = await readFile2(path, "utf8");
7858
+ const contents = await readFile3(path, "utf8");
7528
7859
  const parsedMarkdown = parseSkillMarkdown(contents, file, toPosixPath(path));
7529
7860
  const resources = await readSkillResources(path);
7530
7861
  const parsed = skillConfigSchema.safeParse(
@@ -7543,7 +7874,7 @@ async function readWorkingCopy(workingDir, manifest) {
7543
7874
  for (const path of guardrailFiles) {
7544
7875
  const file = toRelativePath(workingDir, path);
7545
7876
  try {
7546
- guardrails.push(await readGuardrailFolder(dirname(path)));
7877
+ guardrails.push(await readGuardrailFolder(dirname2(path)));
7547
7878
  } catch (error) {
7548
7879
  const extracted = extractErrorIssues(file, error);
7549
7880
  issues.push(...extracted.length > 0 ? extracted : [{ file, message: errorMessage(error) }]);
@@ -7583,18 +7914,18 @@ function matchesManifestPath(root, path, patterns) {
7583
7914
  }
7584
7915
  function agentAssetPath(workingDir, name) {
7585
7916
  assertSafeAssetName(name);
7586
- return resolve(workingDir, "agents", `${name}.agent.yaml`);
7917
+ return resolve2(workingDir, "agents", `${name}.agent.yaml`);
7587
7918
  }
7588
7919
  function skillAssetPath(workingDir, name) {
7589
7920
  assertSafeAssetName(name);
7590
- return resolve(workingDir, "skills", name, "SKILL.md");
7921
+ return resolve2(workingDir, "skills", name, "SKILL.md");
7591
7922
  }
7592
7923
  function guardrailAssetPath(workingDir, name) {
7593
7924
  assertSafeAssetName(name);
7594
- return resolve(workingDir, "guardrails", name, GUARDRAIL_FILE);
7925
+ return resolve2(workingDir, "guardrails", name, GUARDRAIL_FILE);
7595
7926
  }
7596
7927
  async function readSkillResources(skillFilePath) {
7597
- const skillDirectory = dirname(skillFilePath);
7928
+ const skillDirectory = dirname2(skillFilePath);
7598
7929
  const files = [];
7599
7930
  await walkFiles(skillDirectory, skillDirectory, files);
7600
7931
  const resources = [];
@@ -7609,7 +7940,7 @@ async function readSkillResources(skillFilePath) {
7609
7940
  `Unsupported skill resource '${resourcePath}'; supported extensions are .md, .txt, .json, .yaml, and .yml`
7610
7941
  );
7611
7942
  }
7612
- const bytes = await readFile2(path);
7943
+ const bytes = await readFile3(path);
7613
7944
  let content;
7614
7945
  try {
7615
7946
  content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
@@ -7634,11 +7965,11 @@ async function removeStaleManifestAssets(workingDir, manifest, desiredPaths) {
7634
7965
  continue;
7635
7966
  }
7636
7967
  if (path.endsWith("/SKILL.md") || path.endsWith(`/${GUARDRAIL_FILE}`)) {
7637
- await rm(dirname(path), { recursive: true, force: true });
7968
+ await rm(dirname2(path), { recursive: true, force: true });
7638
7969
  continue;
7639
7970
  }
7640
7971
  await rm(path);
7641
- const parent = dirname(path);
7972
+ const parent = dirname2(path);
7642
7973
  if (parent !== workingDir) {
7643
7974
  try {
7644
7975
  await rmdir(parent);
@@ -7651,7 +7982,7 @@ async function removeStaleManifestAssets(workingDir, manifest, desiredPaths) {
7651
7982
  }
7652
7983
  }
7653
7984
  async function updateManifestDefaultAgent(path, defaultAgentName) {
7654
- const contents = await readFile2(path, "utf8");
7985
+ const contents = await readFile3(path, "utf8");
7655
7986
  const linePattern = /^defaultAgentName:[^\r\n]*(?:\r?\n|$)/mu;
7656
7987
  let updated;
7657
7988
  if (defaultAgentName === void 0) {
@@ -7669,7 +8000,37 @@ ${line}
7669
8000
  `;
7670
8001
  }
7671
8002
  }
7672
- await writeFile(path, updated, "utf8");
8003
+ await writeFile2(path, updated, "utf8");
8004
+ }
8005
+ async function bindManifestWorkspace(path, instanceKey, workspaceId) {
8006
+ const contents = await readFile3(path, "utf8");
8007
+ const lines = contents.split("\n");
8008
+ const keyLine = lines.findIndex((line) => {
8009
+ const match = /^(\s+)(["']?)([^"':]+)\2:\s*(?:#.*)?$/u.exec(line.trimEnd());
8010
+ return match?.[3] === instanceKey;
8011
+ });
8012
+ const keyIndent = keyLine < 0 ? -1 : indentOf(lines[keyLine] ?? "");
8013
+ let urlLine = -1;
8014
+ for (let index = keyLine + 1; keyLine >= 0 && index < lines.length; index += 1) {
8015
+ const line = lines[index] ?? "";
8016
+ if (line.trim() === "" || line.trim().startsWith("#")) continue;
8017
+ if (indentOf(line) <= keyIndent) break;
8018
+ if (/^\s+url:/u.test(line)) {
8019
+ urlLine = index;
8020
+ break;
8021
+ }
8022
+ }
8023
+ if (urlLine < 0) {
8024
+ throw new Error(
8025
+ `Cannot bind instance '${instanceKey}' in ${MANIFEST_FILENAME}: add 'workspace: ${workspaceId}' to that instance yourself, beside its url.`
8026
+ );
8027
+ }
8028
+ const entry = yaml3.dump({ workspace: workspaceId }, { lineWidth: -1 }).trimEnd();
8029
+ lines.splice(urlLine + 1, 0, `${" ".repeat(indentOf(lines[urlLine] ?? ""))}${entry}`);
8030
+ await writeFile2(path, lines.join("\n"), "utf8");
8031
+ }
8032
+ function indentOf(line) {
8033
+ return line.length - line.trimStart().length;
7673
8034
  }
7674
8035
  async function walkFiles(root, directory, output) {
7675
8036
  let entries;
@@ -7682,7 +8043,7 @@ async function walkFiles(root, directory, output) {
7682
8043
  throw error;
7683
8044
  }
7684
8045
  for (const entry of entries) {
7685
- const path = resolve(directory, entry.name);
8046
+ const path = resolve2(directory, entry.name);
7686
8047
  if (entry.isDirectory()) {
7687
8048
  if (directory === root && [".git", "node_modules"].includes(entry.name)) {
7688
8049
  continue;
@@ -7697,7 +8058,7 @@ function globSearchRoot(root, pattern) {
7697
8058
  const wildcardIndex = pattern.search(/[?*]/u);
7698
8059
  const fixedPart = wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
7699
8060
  const slashIndex = fixedPart.lastIndexOf("/");
7700
- return resolve(root, slashIndex === -1 ? "." : fixedPart.slice(0, slashIndex));
8061
+ return resolve2(root, slashIndex === -1 ? "." : fixedPart.slice(0, slashIndex));
7701
8062
  }
7702
8063
  function globPatternToRegExp(pattern) {
7703
8064
  let source = "^";
@@ -7799,9 +8160,9 @@ async function readAssetSource(path, representation) {
7799
8160
  return representation === "files" ? { files: await readFileSet(path) } : { config: await readDefinition(path) };
7800
8161
  }
7801
8162
  async function readDefinition(path) {
7802
- const folder = (await stat2(path)).isDirectory();
7803
- const guardrailFolder = folder ? path : basename(path) === GUARDRAIL_FILE ? dirname2(path) : void 0;
7804
- if (guardrailFolder !== void 0 && await stat2(join2(guardrailFolder, GUARDRAIL_FILE)).then(
8163
+ const folder = (await stat3(path)).isDirectory();
8164
+ const guardrailFolder = folder ? path : basename(path) === GUARDRAIL_FILE ? dirname3(path) : void 0;
8165
+ if (guardrailFolder !== void 0 && await stat3(join3(guardrailFolder, GUARDRAIL_FILE)).then(
7805
8166
  (found) => found.isFile(),
7806
8167
  () => false
7807
8168
  )) {
@@ -7811,8 +8172,8 @@ async function readDefinition(path) {
7811
8172
  );
7812
8173
  });
7813
8174
  }
7814
- const file = folder ? join2(path, SKILL_FILE) : path;
7815
- const { size } = await stat2(file).catch(() => {
8175
+ const file = folder ? join3(path, SKILL_FILE) : path;
8176
+ const { size } = await stat3(file).catch(() => {
7816
8177
  throw new Error(
7817
8178
  `'${path}' is a folder without a ${SKILL_FILE}. Name the YAML or JSON file of the definition.`
7818
8179
  );
@@ -7822,7 +8183,7 @@ async function readDefinition(path) {
7822
8183
  `'${file}' has ${size} bytes, and a definition holds at most ${ASSET_SYNC_ITEM_MAX_BYTES}.`
7823
8184
  );
7824
8185
  }
7825
- const contents = await readFile3(file, "utf8");
8186
+ const contents = await readFile4(file, "utf8");
7826
8187
  const definition = await parseDefinition(file, contents).catch((error) => {
7827
8188
  throw new Error(
7828
8189
  `'${file}' cannot be read: ${error instanceof Error ? error.message : String(error)}`
@@ -7843,7 +8204,7 @@ function definitionOfShown(value) {
7843
8204
  async function parseDefinition(file, contents) {
7844
8205
  if (basename(file) === SKILL_FILE) {
7845
8206
  const skill = parseSkillMarkdown(contents, file, file.split(sep2).join("/"));
7846
- const resources = await readSkillResources(resolve2(file));
8207
+ const resources = await readSkillResources(resolve3(file));
7847
8208
  return resources.length === 0 || typeof skill !== "object" ? skill : { ...skill, resources };
7848
8209
  }
7849
8210
  switch (extname(file).toLowerCase()) {
@@ -7859,7 +8220,7 @@ async function parseDefinition(file, contents) {
7859
8220
  }
7860
8221
  }
7861
8222
  async function readFileSet(directory) {
7862
- if (!(await stat2(directory)).isDirectory()) {
8223
+ if (!(await stat3(directory)).isDirectory()) {
7863
8224
  throw new Error(
7864
8225
  `'${directory}' is not a folder. An asset of files is written from its folder.`
7865
8226
  );
@@ -7875,7 +8236,7 @@ async function readFileSet(directory) {
7875
8236
  );
7876
8237
  }
7877
8238
  let total = 0;
7878
- for (const path of paths) total += (await stat2(path)).size;
8239
+ for (const path of paths) total += (await stat3(path)).size;
7879
8240
  if (total > ASSET_PUT_FILES_MAX_BYTES) {
7880
8241
  throw new Error(
7881
8242
  `'${directory}' holds ${total} bytes, and one write carries at most ${ASSET_PUT_FILES_MAX_BYTES}.`
@@ -7883,7 +8244,7 @@ async function readFileSet(directory) {
7883
8244
  }
7884
8245
  const files = [];
7885
8246
  for (const path of paths.sort()) {
7886
- const bytes = await readFile3(path);
8247
+ const bytes = await readFile4(path);
7887
8248
  const relativePath = relative2(directory, path).split(sep2).join("/");
7888
8249
  const text = utf8Text(bytes);
7889
8250
  files.push(
@@ -7913,6 +8274,10 @@ function formatContext(context) {
7913
8274
  ...context.namespaces.map(
7914
8275
  (namespace) => ` ${namespace.prefix} ${namespace.displayName}: ${list(namespace.actions)}`
7915
8276
  ),
8277
+ context.workspaces.length === 0 ? "Workspaces: none" : "Workspaces:",
8278
+ ...context.workspaces.map(
8279
+ (workspace) => ` ${workspace.id} ${workspace.name}: ${list(workspace.actions)}`
8280
+ ),
7916
8281
  `Kinds: ${context.kinds.map((kind) => kind.kind).join(", ")}`
7917
8282
  ].join("\n");
7918
8283
  }
@@ -8052,82 +8417,6 @@ function isRecord4(value) {
8052
8417
  return typeof value === "object" && value !== null && !Array.isArray(value);
8053
8418
  }
8054
8419
 
8055
- // src/credentials.ts
8056
- import { chmod, mkdir as mkdir2, readFile as readFile4, rename as rename2, stat as stat3, writeFile as writeFile2 } from "fs/promises";
8057
- import { homedir } from "os";
8058
- import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
8059
- var CREDENTIALS_FILENAME = "credentials.json";
8060
- function credentialsPath(env) {
8061
- const folder = env.CATALYST_CONFIG_HOME ?? join3(env.XDG_CONFIG_HOME ?? join3(homedir(), ".config"), "catalyst");
8062
- return resolve3(folder, CREDENTIALS_FILENAME);
8063
- }
8064
- function credentialOrigin(url) {
8065
- return new URL(url).origin;
8066
- }
8067
- async function readStoredCredential(env, url) {
8068
- return (await readCredentialFile(credentialsPath(env))).instances[credentialOrigin(url)];
8069
- }
8070
- async function saveCredential(env, url, credential) {
8071
- const path = credentialsPath(env);
8072
- const file = await readCredentialFile(path);
8073
- file.instances[credentialOrigin(url)] = credential;
8074
- await writeCredentialFile(path, file);
8075
- return path;
8076
- }
8077
- async function removeCredential(env, url) {
8078
- const path = credentialsPath(env);
8079
- const file = await readCredentialFile(path);
8080
- const origin = credentialOrigin(url);
8081
- if (file.instances[origin] === void 0) return false;
8082
- delete file.instances[origin];
8083
- await writeCredentialFile(path, file);
8084
- return true;
8085
- }
8086
- async function readCredentialFile(path) {
8087
- let contents;
8088
- try {
8089
- contents = await readFile4(path, "utf8");
8090
- } catch (error) {
8091
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
8092
- return { version: 1, instances: {} };
8093
- }
8094
- throw error;
8095
- }
8096
- if (process.platform !== "win32" && ((await stat3(path)).mode & 63) !== 0) {
8097
- throw new Error(
8098
- `${path} can be read by other users of this machine. Run 'chmod 600 ${path}', or remove it and run 'catalyst login' again.`
8099
- );
8100
- }
8101
- const parsed = JSON.parse(contents);
8102
- if (!isCredentialFile(parsed)) {
8103
- throw new Error(`${path} is not a credentials file of this CLI. Remove it and log in again.`);
8104
- }
8105
- return parsed;
8106
- }
8107
- async function writeCredentialFile(path, file) {
8108
- await mkdir2(dirname3(path), { recursive: true, mode: 448 });
8109
- const temporary = `${path}.${process.pid}.tmp`;
8110
- await writeFile2(temporary, `${JSON.stringify(file, null, 2)}
8111
- `, { mode: 384 });
8112
- await chmod(temporary, 384);
8113
- await rename2(temporary, path);
8114
- }
8115
- function isCredentialFile(value) {
8116
- if (typeof value !== "object" || value === null) return false;
8117
- if (!("version" in value) || value.version !== 1) return false;
8118
- if (!("instances" in value) || typeof value.instances !== "object" || value.instances === null) {
8119
- return false;
8120
- }
8121
- return Object.values(value.instances).every(
8122
- (entry) => typeof entry === "object" && entry !== null && "apiKey" in entry && typeof entry.apiKey === "string" && "keyId" in entry && typeof entry.keyId === "string" && "name" in entry && typeof entry.name === "string" && "expiresAt" in entry && typeof entry.expiresAt === "string" && "savedAt" in entry && typeof entry.savedAt === "string"
8123
- );
8124
- }
8125
-
8126
- // src/env.ts
8127
- function readConfigCliEnv(env) {
8128
- return env ?? process.env;
8129
- }
8130
-
8131
8420
  // src/command-support.ts
8132
8421
  import { createHash } from "crypto";
8133
8422
  import { mkdir as mkdir3, mkdtemp, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
@@ -8264,7 +8553,16 @@ function workingCopyKinds(manifest) {
8264
8553
  }
8265
8554
  var PULL_CONCURRENCY = 8;
8266
8555
  async function readInstanceOverview(api, instance) {
8267
- return instance.namespace === void 0 ? api.getOverview() : {};
8556
+ return holdsWholeInstance(instance) ? api.getOverview() : {};
8557
+ }
8558
+ function batchScope(instance) {
8559
+ return {
8560
+ ...instance.namespace === void 0 ? {} : { namespace: instance.namespace },
8561
+ ...instance.workspace === void 0 ? {} : { workspaceId: instance.workspace }
8562
+ };
8563
+ }
8564
+ function holdsWholeInstance(instance) {
8565
+ return instance.namespace === void 0 && instance.workspace === void 0;
8268
8566
  }
8269
8567
  async function listRemoteAssets(api, kinds) {
8270
8568
  const assets = /* @__PURE__ */ new Map();
@@ -8519,7 +8817,7 @@ function isNodeError2(error) {
8519
8817
  function commandEnv(options) {
8520
8818
  return readConfigCliEnv(options.env);
8521
8819
  }
8522
- async function connectApi(url, options) {
8820
+ async function connectApi(url, options, bound = {}) {
8523
8821
  const apiKey = commandEnv(options).CATALYST_API_KEY || (await readStoredCredential(commandEnv(options), url))?.apiKey;
8524
8822
  if (!apiKey) {
8525
8823
  throw new Error(
@@ -8530,7 +8828,8 @@ async function connectApi(url, options) {
8530
8828
  baseUrl: url,
8531
8829
  apiKey,
8532
8830
  ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
8533
- ...options.sleep === void 0 ? {} : { sleep: options.sleep }
8831
+ ...options.sleep === void 0 ? {} : { sleep: options.sleep },
8832
+ ...bound.workspace === void 0 ? {} : { workspaceId: bound.workspace }
8534
8833
  });
8535
8834
  }
8536
8835
  function assetHash(contents) {
@@ -8724,18 +9023,19 @@ var VERBS = {
8724
9023
  }
8725
9024
  },
8726
9025
  validate: {
8727
- usage: "validate --from <file>",
8728
- summary: "Check a definition against the instance without writing it (assets.validate)",
9026
+ usage: "validate --from <file-or-folder>",
9027
+ summary: "Check a definition against the instance without writing it (assets.validate); a folder of files is checked against the limits of one write",
8729
9028
  named: false,
8730
9029
  flags: ["from"],
8731
9030
  prepare: (values, _name, cwd) => {
8732
- const from = fromPath(values, cwd, "--from <file>");
9031
+ const from = fromPath(values, cwd, "--from <file-or-folder>");
8733
9032
  return async (session2) => {
8734
- const config = await readDefinition(from);
9033
+ const source = await readAssetSource(from, session2.kind.representation);
9034
+ if ("files" in source) return filesRead(session2, from, source.files.length);
8735
9035
  const result = await session2.api.read(
8736
9036
  (client) => client.assets.validate({
8737
9037
  params: { kind: session2.kind.kind },
8738
- body: { config, ...session2.scope }
9038
+ body: { config: source.config, ...session2.scope }
8739
9039
  })
8740
9040
  );
8741
9041
  writeOut(
@@ -9103,13 +9403,14 @@ ${assetHelpText}`);
9103
9403
  throw new UsageError(`Unexpected argument '${extra[0] ?? name}'.`);
9104
9404
  }
9105
9405
  const call2 = verb.prepare(values, name, options.cwd);
9106
- const api = await openInstance(options);
9406
+ const { api, workspace } = await openInstance(options);
9107
9407
  const context = await api.read((client) => client.platform.context.get());
9408
+ const workspaceId = values.workspace ?? workspace;
9108
9409
  return await call2({
9109
9410
  api,
9110
9411
  kind: findKind(context.kinds, noun),
9111
9412
  values,
9112
- scope: values.workspace === void 0 ? {} : { workspaceId: values.workspace },
9413
+ scope: workspaceId === void 0 ? {} : { workspaceId },
9113
9414
  options
9114
9415
  });
9115
9416
  } catch (error) {
@@ -9125,7 +9426,7 @@ ${assetHelpText}`);
9125
9426
  }
9126
9427
  }
9127
9428
  async function runInstanceCommand(command, values, options) {
9128
- const api = await openInstance(options);
9429
+ const { api } = await openInstance(options);
9129
9430
  const json2 = values.json;
9130
9431
  const out = (value, text) => writeOut(options, json2 ? JSON.stringify(value, null, 2) : text());
9131
9432
  if (command === "references") {
@@ -9140,11 +9441,8 @@ async function runInstanceCommand(command, values, options) {
9140
9441
  }
9141
9442
  async function openInstance(options) {
9142
9443
  const manifest = await readManifestIfPresent(resolveWorkingDir(options));
9143
- const instance = resolveInstance(
9144
- manifest ?? { instances: {}, agents: [], skills: [] },
9145
- options.instance
9146
- );
9147
- return connectApi(instance.url, options);
9444
+ const instance = await resolveInstance(manifest, options);
9445
+ return { api: await connectApi(instance.url, options), workspace: instance.workspace };
9148
9446
  }
9149
9447
  function findKind(kinds, noun) {
9150
9448
  const wanted = noun.replaceAll("-", "_");
@@ -9156,6 +9454,18 @@ function findKind(kinds, noun) {
9156
9454
  }
9157
9455
  return kind;
9158
9456
  }
9457
+ function filesRead(session2, from, files) {
9458
+ const noun = kindNoun(session2.kind);
9459
+ const draft = session2.kind.lifecycle === "draft_published" ? " as a draft" : "";
9460
+ return print(
9461
+ session2,
9462
+ { files, checkedByInstance: false },
9463
+ () => [
9464
+ `Read ${files} files from '${from}', within what one write carries.`,
9465
+ `The instance checks the files of a ${session2.kind.kind} as it writes them: 'catalyst ${noun} put <name> --from ${from} --if-revision <n|new>' writes them${draft}.`
9466
+ ].join("\n")
9467
+ );
9468
+ }
9159
9469
  function settle(session2, outcome, lines) {
9160
9470
  if (outcome.status === "pending_approval") {
9161
9471
  const text = `Waits for an approval on the instance; nothing has changed yet (Operation Run ${outcome.run.id}).`;
@@ -9221,9 +9531,12 @@ ${Object.values(VERBS).map((verb) => ` catalyst <kind> ${verb.usage}
9221
9531
  ${verb.summary}`).join("\n")}
9222
9532
 
9223
9533
  Options:
9224
- --instance <name-or-url> An instance of catalyst.yaml, or its URL
9534
+ --instance <name-or-url> An instance of catalyst.yaml, or its URL. Left out: the
9535
+ defaultInstance of catalyst.yaml; outside a working copy,
9536
+ the one instance a key is saved for
9225
9537
  --dir <working-copy-dir> Directory containing catalyst.yaml (default: .)
9226
- --workspace <id> Address the assets one workspace owns
9538
+ --workspace <id> Address the assets one workspace owns. Left out in a working
9539
+ copy bound to a workspace: that workspace
9227
9540
  --json Print the instance's answer as JSON
9228
9541
  --help Show this help
9229
9542
 
@@ -9338,7 +9651,7 @@ import { resolve as resolve6 } from "path";
9338
9651
  async function pushConfig(options) {
9339
9652
  const workingDir = resolveWorkingDir(options);
9340
9653
  const manifest = await readManifest(workingDir);
9341
- const instance = resolveInstance(manifest, options.instance);
9654
+ const instance = await resolveInstance(manifest, options);
9342
9655
  const local = await readWorkingCopy(workingDir, manifest);
9343
9656
  const selectors = parseOnlySelectors(options.only);
9344
9657
  if (options.prune && selectors.length > 0) {
@@ -9360,7 +9673,7 @@ async function pushConfig(options) {
9360
9673
  );
9361
9674
  }
9362
9675
  const baselineAssets = baseline.assets;
9363
- const api = await connectApi(instance.url, options);
9676
+ const api = await connectApi(instance.url, options, instance);
9364
9677
  const listing = await listRemoteAssets(api, workingCopyKinds(manifest));
9365
9678
  const touched = assetEntries(bundle).filter(
9366
9679
  (asset) => baselineAssets[asset.key]?.hash !== assetHash(asset.contents)
@@ -9368,7 +9681,7 @@ async function pushConfig(options) {
9368
9681
  const touchedKeys = new Set(touched.map((asset) => asset.key));
9369
9682
  const localKeys = new Set(assetEntries(local).map((asset) => asset.key));
9370
9683
  const deleteAssets = options.prune ? [...listing.values()].filter((asset) => !localKeys.has(asset.key)) : [];
9371
- const changesDefault = instance.namespace === void 0 && selectors.length === 0 && (bundle.defaultAgentName ?? null) !== baseline.defaultAgentName;
9684
+ const changesDefault = holdsWholeInstance(instance) && selectors.length === 0 && (bundle.defaultAgentName ?? null) !== baseline.defaultAgentName;
9372
9685
  if (changesDefault && baseline.defaultAgentName === void 0) {
9373
9686
  throw new Error(
9374
9687
  "No default-agent baseline is recorded. Run 'catalyst config pull' before changing the default agent."
@@ -9447,7 +9760,7 @@ async function pushConfig(options) {
9447
9760
  }
9448
9761
  if (items.length > 0 || changesDefault) {
9449
9762
  const batch = {
9450
- ...instance.namespace === void 0 ? {} : { namespace: instance.namespace },
9763
+ ...batchScope(instance),
9451
9764
  items,
9452
9765
  ...changesDefault ? {
9453
9766
  defaultAgentName: bundle.defaultAgentName ?? null,
@@ -9499,6 +9812,7 @@ async function pushConfig(options) {
9499
9812
  baseOf,
9500
9813
  listing,
9501
9814
  afterSync: items.length > 0 || changesDefault,
9815
+ ...instance.workspace === void 0 ? {} : { workspaceId: instance.workspace },
9502
9816
  options
9503
9817
  });
9504
9818
  }
@@ -9538,6 +9852,7 @@ async function syncBatch(api, batch, options) {
9538
9852
  }
9539
9853
  async function writeGuardrails(api, input) {
9540
9854
  const { options } = input;
9855
+ const owner = input.workspaceId === void 0 ? {} : { workspaceId: input.workspaceId };
9541
9856
  let written = input.afterSync;
9542
9857
  const record = (asset, revisions, tombstones = {}) => recordBaseline(
9543
9858
  input.workingDir,
@@ -9555,7 +9870,11 @@ async function writeGuardrails(api, input) {
9555
9870
  const outcome = await api.change(
9556
9871
  (client, idempotencyKey2) => client.assets.put({
9557
9872
  params: { kind: asset.kind, name: asset.name },
9558
- body: { config: { ...asset.config }, expectedRevision: base?.revision ?? null },
9873
+ body: {
9874
+ config: { ...asset.config },
9875
+ expectedRevision: base?.revision ?? null,
9876
+ ...owner
9877
+ },
9559
9878
  idempotencyKey: idempotencyKey2
9560
9879
  }),
9561
9880
  "It may have been written: run 'catalyst config diff' before pushing again."
@@ -9580,7 +9899,7 @@ async function writeGuardrails(api, input) {
9580
9899
  const outcome = await api.change(
9581
9900
  (client, idempotencyKey2) => client.assets.delete({
9582
9901
  params: { kind: asset.kind, name: asset.name },
9583
- body: { expectedRevision: base.revision },
9902
+ body: { expectedRevision: base.revision, ...owner },
9584
9903
  idempotencyKey: idempotencyKey2
9585
9904
  }),
9586
9905
  "It may have been applied: run 'catalyst config diff' before pushing again."
@@ -9819,9 +10138,8 @@ async function runConfigCommand(command, options) {
9819
10138
  }
9820
10139
  async function pullConfig(options) {
9821
10140
  const workingDir = resolveWorkingDir(options);
9822
- const manifest = await readManifest(workingDir);
9823
- const instance = resolveInstance(manifest, options.instance);
9824
- const api = await connectApi(instance.url, options);
10141
+ const { manifest, instance } = await bindWorkspace(workingDir, options);
10142
+ const api = await connectApi(instance.url, options, instance);
9825
10143
  const overview = await readInstanceOverview(api, instance);
9826
10144
  const listing = await listRemoteAssets(api, workingCopyKinds(manifest));
9827
10145
  const selectors = parseOnlySelectors(options.only);
@@ -9894,7 +10212,7 @@ async function pullConfig(options) {
9894
10212
  force: true
9895
10213
  });
9896
10214
  }
9897
- const fullInstancePull = selectors.length === 0 && instance.namespace === void 0;
10215
+ const fullInstancePull = selectors.length === 0 && holdsWholeInstance(instance);
9898
10216
  if (selectors.length === 0) {
9899
10217
  await removeStaleManifestAssets(workingDir, manifest, desiredPaths);
9900
10218
  }
@@ -9921,6 +10239,43 @@ async function pullConfig(options) {
9921
10239
  );
9922
10240
  return 0;
9923
10241
  }
10242
+ async function bindWorkspace(workingDir, options) {
10243
+ const manifest = await readManifest(workingDir);
10244
+ const instance = await resolveInstance(manifest, options);
10245
+ const wanted = options.workspace;
10246
+ if (wanted === void 0 || instance.workspace === wanted) return { manifest, instance };
10247
+ if (instance.workspace !== void 0) {
10248
+ throw new Error(
10249
+ `This working copy is bound to workspace '${instance.workspace}'. Pull another workspace into a directory of its own.`
10250
+ );
10251
+ }
10252
+ if (instance.namespace !== void 0) {
10253
+ throw new Error(
10254
+ `This working copy belongs to Namespace '${instance.namespace}'. Pull a workspace into a directory of its own.`
10255
+ );
10256
+ }
10257
+ const baseline = (await readStateFile(resolve7(workingDir, STATE_FILENAME))).instances[instance.key];
10258
+ if (baseline !== void 0) {
10259
+ throw new Error(
10260
+ `This working copy holds the instance's own assets of '${instance.key}'. Pull a workspace into a directory of its own.`
10261
+ );
10262
+ }
10263
+ if (manifest.instances[instance.key] === void 0) {
10264
+ throw new Error(
10265
+ `Name the instance in ${MANIFEST_FILENAME} to bind it to a workspace: '${instance.key}' is not one of its instances.`
10266
+ );
10267
+ }
10268
+ await bindManifestWorkspace(resolve7(workingDir, MANIFEST_FILENAME), instance.key, wanted);
10269
+ const bound = await readManifest(workingDir);
10270
+ const boundInstance = await resolveInstance(bound, options);
10271
+ if (boundInstance.workspace !== wanted) {
10272
+ throw new Error(
10273
+ `Could not bind instance '${instance.key}' in ${MANIFEST_FILENAME}: add 'workspace: ${wanted}' to that instance yourself, beside its url.`
10274
+ );
10275
+ }
10276
+ writeOutput(options, `Bound this working copy to workspace '${wanted}'.`);
10277
+ return { manifest: bound, instance: boundInstance };
10278
+ }
9924
10279
  function assetPath(workingDir, kind, name) {
9925
10280
  switch (kind) {
9926
10281
  case "agent":
@@ -9954,9 +10309,9 @@ function assertPullTargetsMatchManifest(workingDir, manifest, targets) {
9954
10309
  async function diffConfig(options) {
9955
10310
  const workingDir = resolveWorkingDir(options);
9956
10311
  const manifest = await readManifest(workingDir);
9957
- const instance = resolveInstance(manifest, options.instance);
10312
+ const instance = await resolveInstance(manifest, options);
9958
10313
  const local = await readWorkingCopy(workingDir, manifest);
9959
- const api = await connectApi(instance.url, options);
10314
+ const api = await connectApi(instance.url, options, instance);
9960
10315
  const overview = await readInstanceOverview(api, instance);
9961
10316
  const remoteAssets = await listRemoteAssets(api, workingCopyKinds(manifest));
9962
10317
  const baseline = (await readStateFile(resolve7(workingDir, STATE_FILENAME))).instances[instance.key];
@@ -10015,9 +10370,9 @@ async function diffConfig(options) {
10015
10370
  async function validateConfig(options) {
10016
10371
  const workingDir = resolveWorkingDir(options);
10017
10372
  const manifest = await readManifest(workingDir);
10018
- const instance = resolveInstance(manifest, options.instance);
10373
+ const instance = await resolveInstance(manifest, options);
10019
10374
  const bundle = await readWorkingCopy(workingDir, manifest);
10020
- const api = await connectApi(instance.url, options);
10375
+ const api = await connectApi(instance.url, options, instance);
10021
10376
  const listing = await listRemoteAssets(api, workingCopyKinds(manifest));
10022
10377
  const deleted = (await readStateFile(resolve7(workingDir, STATE_FILENAME))).instances[instance.key]?.deleted;
10023
10378
  const items = assetEntries(bundle).filter((asset) => asset.kind !== "guardrail").map((asset) => ({
@@ -10031,6 +10386,7 @@ async function validateConfig(options) {
10031
10386
  try {
10032
10387
  if (items.length > 0 || bundle.defaultAgentName !== void 0) {
10033
10388
  await checkSyncOnThisRelease(api, {
10389
+ ...batchScope(instance),
10034
10390
  items,
10035
10391
  ...bundle.defaultAgentName === void 0 ? {} : { defaultAgentName: bundle.defaultAgentName }
10036
10392
  });
@@ -10049,9 +10405,9 @@ async function validateConfig(options) {
10049
10405
  async function listConfig(options) {
10050
10406
  const workingDir = resolveWorkingDir(options);
10051
10407
  const manifest = await readManifest(workingDir);
10052
- const instance = resolveInstance(manifest, options.instance);
10408
+ const instance = await resolveInstance(manifest, options);
10053
10409
  const local = await readWorkingCopy(workingDir, manifest);
10054
- const api = await connectApi(instance.url, options);
10410
+ const api = await connectApi(instance.url, options, instance);
10055
10411
  const version = String((await readInstanceOverview(api, instance)).version ?? "-");
10056
10412
  const remote = await listRemoteAssets(api, workingCopyKinds(manifest));
10057
10413
  const localKeys = new Set(assetEntries(local).map((asset) => asset.key));
@@ -10078,8 +10434,8 @@ async function showConfig(options) {
10078
10434
  }
10079
10435
  const workingDir = resolveWorkingDir(options);
10080
10436
  const manifest = await readManifest(workingDir);
10081
- const instance = resolveInstance(manifest, options.instance);
10082
- const api = await connectApi(instance.url, options);
10437
+ const instance = await resolveInstance(manifest, options);
10438
+ const api = await connectApi(instance.url, options, instance);
10083
10439
  const remote = await fetchRemoteAsset(api, options.assetKind, options.assetName);
10084
10440
  if (!remote) {
10085
10441
  throw new Error(`Remote ${options.assetKind} '${options.assetName}' was not found.`);
@@ -10109,61 +10465,75 @@ import { hostname } from "os";
10109
10465
  async function loginCommand(options) {
10110
10466
  try {
10111
10467
  const url = await resolveInstanceUrl(options);
10112
- assertSafeApiKeyExchangeUrl(url, "receive an API key");
10113
- const sleep = options.sleep ?? defaultSleep2;
10114
- const client = createApiClient({
10115
- baseUrl: url,
10116
- fetchImpl: instanceFetch(options.fetchImpl)
10117
- });
10118
- const started = await call(
10119
- () => client.connect_codes.start({ body: { clientLabel: `catalyst CLI on ${hostname()}` } })
10120
- );
10468
+ const key = await loginByConnectCode(options, url, `catalyst CLI on ${hostname()}`);
10469
+ if (key === void 0) {
10470
+ writeError(options, "The code expired before it was confirmed. Run 'catalyst login' again.");
10471
+ return 1;
10472
+ }
10121
10473
  writeOutput(
10122
10474
  options,
10123
10475
  [
10124
- `Open ${url}${started.confirmPath}`,
10125
- `and confirm the code ${started.userCode} with the scopes this key may use.`,
10126
- `Waiting for the confirmation until ${formatTime(started.expiresAt)}\u2026`
10476
+ `Logged in to ${credentialOrigin(url)} with the key '${key.name}' (${key.keyPrefix}\u2026).`,
10477
+ `It may: ${key.scopes.join(", ")}, as far as your rights reach, until ${formatTime(key.expiresAt)}.`,
10478
+ `Saved in ${key.path}. See and revoke it at ${apiKeysAddress(url)}.`
10127
10479
  ].join("\n")
10128
10480
  );
10129
- const deadline = Date.parse(started.expiresAt);
10130
- while (Date.now() < deadline) {
10131
- await sleep(started.intervalSeconds * 1e3);
10132
- const answer = await call(
10133
- () => client.connect_codes.redeem({ body: { deviceCode: started.deviceCode } })
10134
- );
10135
- if (answer.status === "pending") continue;
10136
- const path = await saveCredential(readConfigCliEnv(options.env), url, {
10137
- apiKey: answer.secret,
10138
- keyId: answer.key.id,
10139
- name: answer.key.name,
10140
- expiresAt: answer.key.expiresAt,
10141
- savedAt: (/* @__PURE__ */ new Date()).toISOString()
10142
- });
10143
- writeOutput(
10144
- options,
10145
- [
10146
- `Logged in to ${credentialOrigin(url)} with the key '${answer.key.name}' (${answer.key.keyPrefix}\u2026).`,
10147
- `It may: ${answer.key.scopes.join(", ")}, as far as your rights reach, until ${formatTime(answer.key.expiresAt)}.`,
10148
- `Saved in ${path}. Revoke it in Settings, API keys.`
10149
- ].join("\n")
10150
- );
10151
- return 0;
10152
- }
10153
- writeError(options, "The code expired before it was confirmed. Run 'catalyst login' again.");
10154
- return 1;
10481
+ return 0;
10155
10482
  } catch (error) {
10156
10483
  writeError(options, formatCommandError(error));
10157
10484
  return 1;
10158
10485
  }
10159
10486
  }
10487
+ async function loginByConnectCode(options, url, clientLabel, openUrl) {
10488
+ assertSafeApiKeyExchangeUrl(url, "receive an API key");
10489
+ const sleep = options.sleep ?? defaultSleep2;
10490
+ const client = createApiClient({
10491
+ baseUrl: url,
10492
+ fetchImpl: instanceFetch(options.fetchImpl)
10493
+ });
10494
+ const started = await call(() => client.connect_codes.start({ body: { clientLabel } }));
10495
+ const address = `${url}${started.confirmPath}`;
10496
+ writeOutput(
10497
+ options,
10498
+ [
10499
+ `Open ${address}`,
10500
+ `and confirm the code ${started.userCode} with the scopes this key may use.`,
10501
+ `Waiting for the confirmation until ${formatTime(started.expiresAt)}\u2026`
10502
+ ].join("\n")
10503
+ );
10504
+ openUrl?.(address);
10505
+ const deadline = Date.parse(started.expiresAt);
10506
+ while (Date.now() < deadline) {
10507
+ await sleep(started.intervalSeconds * 1e3);
10508
+ const answer = await call(
10509
+ () => client.connect_codes.redeem({ body: { deviceCode: started.deviceCode } })
10510
+ );
10511
+ if (answer.status === "pending") continue;
10512
+ const path = await saveCredential(readConfigCliEnv(options.env), url, {
10513
+ apiKey: answer.secret,
10514
+ keyId: answer.key.id,
10515
+ name: answer.key.name,
10516
+ scopes: answer.key.scopes,
10517
+ expiresAt: answer.key.expiresAt,
10518
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
10519
+ });
10520
+ return {
10521
+ name: answer.key.name,
10522
+ keyPrefix: answer.key.keyPrefix,
10523
+ scopes: answer.key.scopes,
10524
+ expiresAt: answer.key.expiresAt,
10525
+ path
10526
+ };
10527
+ }
10528
+ return void 0;
10529
+ }
10160
10530
  async function logoutCommand(options) {
10161
10531
  try {
10162
10532
  const url = await resolveInstanceUrl(options);
10163
10533
  const removed = await removeCredential(readConfigCliEnv(options.env), url);
10164
10534
  writeOutput(
10165
10535
  options,
10166
- removed ? `Removed the saved key of ${credentialOrigin(url)}. It stays valid until you revoke it in Settings, API keys.` : `No key is saved for ${credentialOrigin(url)}.`
10536
+ removed ? `Removed the saved key of ${credentialOrigin(url)}. It stays valid until you revoke it at ${apiKeysAddress(url)}.` : `No key is saved for ${credentialOrigin(url)}.`
10167
10537
  );
10168
10538
  return 0;
10169
10539
  } catch (error) {
@@ -10176,16 +10546,11 @@ async function resolveInstanceUrl(options) {
10176
10546
  if (requested !== void 0 && /^https?:\/\//u.test(requested)) {
10177
10547
  return requested.replace(/\/+$/u, "");
10178
10548
  }
10179
- try {
10180
- return resolveInstance(await readManifest(resolveWorkingDir(options)), requested).url;
10181
- } catch (error) {
10182
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
10183
- throw new Error(
10184
- `No ${MANIFEST_FILENAME} here to name the instance. Pass --instance <https-url>.`
10185
- );
10186
- }
10187
- throw error;
10188
- }
10549
+ const manifest = await readManifestIfPresent(resolveWorkingDir(options));
10550
+ return (await resolveInstance(manifest, options)).url;
10551
+ }
10552
+ function apiKeysAddress(url) {
10553
+ return `${url}/settings/you/api-keys`;
10189
10554
  }
10190
10555
  async function call(request) {
10191
10556
  try {
@@ -10201,17 +10566,185 @@ function defaultSleep2(milliseconds) {
10201
10566
  return new Promise((resolveSleep) => setTimeout(resolveSleep, milliseconds));
10202
10567
  }
10203
10568
 
10569
+ // src/setup.ts
10570
+ import { spawn } from "child_process";
10571
+ import { createHash as createHash2 } from "crypto";
10572
+ import { lstat, mkdir as mkdir5, readFile as readFile6, rename as rename4, rm as rm4, writeFile as writeFile5 } from "fs/promises";
10573
+ import { homedir as homedir2, hostname as hostname2 } from "os";
10574
+ import { dirname as dirname6, join as join4 } from "path";
10575
+ var LOCAL_AGENT_TOOLS = {
10576
+ "claude-code": { label: "Claude Code", skillsFolder: [".claude", "skills"] },
10577
+ // Codex reads `~/.agents/skills`, the shared place for tools other than Claude Code.
10578
+ codex: { label: "Codex", skillsFolder: [".agents", "skills"] },
10579
+ other: { label: "Local agent", skillsFolder: [".agents", "skills"] }
10580
+ };
10581
+ function isLocalAgentTool(value) {
10582
+ return value !== void 0 && Object.hasOwn(LOCAL_AGENT_TOOLS, value);
10583
+ }
10584
+ var SETUP_USAGE = "Usage: catalyst setup <claude-code|codex|other> --instance <url> [--force]";
10585
+ var SHIPPED_SKILL = new URL("../skills/catalyst/SKILL.md", import.meta.url);
10586
+ var MARK = /\n<!-- Written by catalyst setup \(@work-shape\/catalyst-cli [^,]+, sha256 ([0-9a-f]{64})\)\. -->\n$/u;
10587
+ function sha256(text) {
10588
+ return createHash2("sha256").update(text).digest("hex");
10589
+ }
10590
+ async function renderCatalystSkill() {
10591
+ const body = await readFile6(SHIPPED_SKILL, "utf8");
10592
+ return `${body}
10593
+ <!-- Written by catalyst setup (@work-shape/catalyst-cli ${RELEASE_VERSION}, sha256 ${sha256(body)}). -->
10594
+ `;
10595
+ }
10596
+ function writtenBySetup(contents) {
10597
+ const mark2 = MARK.exec(contents);
10598
+ return mark2 !== null && sha256(contents.slice(0, mark2.index)) === mark2[1];
10599
+ }
10600
+ function catalystSkillPath(tool, env) {
10601
+ const folder = tool === "claude-code" && env.CLAUDE_CONFIG_DIR ? join4(env.CLAUDE_CONFIG_DIR, "skills") : join4(env.HOME || homedir2(), ...LOCAL_AGENT_TOOLS[tool].skillsFolder);
10602
+ return join4(folder, "catalyst", "SKILL.md");
10603
+ }
10604
+ async function setupCommand(options) {
10605
+ try {
10606
+ const url = await resolveInstanceUrl(options);
10607
+ const tool = LOCAL_AGENT_TOOLS[options.tool];
10608
+ const saved = await workingSavedKey(options, url);
10609
+ let keyLine;
10610
+ if (saved) {
10611
+ keyLine = `Kept the saved key '${saved.name}'${scopesOf(saved.scopes)}, valid until ${formatTime(saved.expiresAt)}.`;
10612
+ } else {
10613
+ const key = await loginByConnectCode(
10614
+ options,
10615
+ url,
10616
+ `${tool.label} on ${hostname2()}`,
10617
+ options.openUrl ?? openInBrowser(url)
10618
+ );
10619
+ if (key === void 0) {
10620
+ writeError(
10621
+ options,
10622
+ `The code expired before it was confirmed. Run 'catalyst setup ${options.tool} --instance ${url}' again.`
10623
+ );
10624
+ return 1;
10625
+ }
10626
+ keyLine = `Logged in with the key '${key.name}'${scopesOf(key.scopes)}, valid until ${formatTime(key.expiresAt)}.`;
10627
+ }
10628
+ const path = catalystSkillPath(options.tool, readConfigCliEnv(options.env));
10629
+ const skill = await renderCatalystSkill();
10630
+ const existing = await readSkillFile(path);
10631
+ const lines = [`Instance: ${credentialOrigin(url)}`, keyLine];
10632
+ if (existing === skill) {
10633
+ lines.push(`The Catalyst skill at ${path} is current.`);
10634
+ } else if (existing === void 0 || existing !== NOT_A_FILE && writtenBySetup(existing)) {
10635
+ await replaceFile(path, skill);
10636
+ lines.push(`Wrote the Catalyst skill to ${path}.`);
10637
+ } else if (options.force) {
10638
+ const backup = await freeBackupPath(path);
10639
+ await rename4(path, backup);
10640
+ await replaceFile(path, skill);
10641
+ lines.push(`Wrote the Catalyst skill to ${path}; the file that was there is ${backup}.`);
10642
+ } else {
10643
+ writeOutput(options, lines.join("\n"));
10644
+ writeError(
10645
+ options,
10646
+ `Kept ${path}: it is not the skill this CLI writes. Run the command again with --force to replace it; the file that is there is then kept as ${await freeBackupPath(path)}.`
10647
+ );
10648
+ return 1;
10649
+ }
10650
+ if (options.tool === "other") {
10651
+ lines.push("Point your tool at that file, or copy it to where your tool reads skills.");
10652
+ }
10653
+ lines.push(`See and revoke the key at ${apiKeysAddress(url)}.`);
10654
+ writeOutput(options, lines.join("\n"));
10655
+ return 0;
10656
+ } catch (error) {
10657
+ writeError(options, formatCommandError(error));
10658
+ return 1;
10659
+ }
10660
+ }
10661
+ var NOT_A_FILE = /* @__PURE__ */ Symbol("not a file");
10662
+ async function readSkillFile(path) {
10663
+ const entry = await lstat(path).catch((error) => {
10664
+ if (isMissing(error)) return void 0;
10665
+ throw error;
10666
+ });
10667
+ if (entry === void 0) return void 0;
10668
+ return entry.isFile() ? readFile6(path, "utf8") : NOT_A_FILE;
10669
+ }
10670
+ async function replaceFile(path, contents) {
10671
+ await mkdir5(dirname6(path), { recursive: true });
10672
+ const temporary = `${path}.${process.pid}.tmp`;
10673
+ await writeFile5(temporary, contents, { flag: "wx" });
10674
+ try {
10675
+ await rename4(temporary, path);
10676
+ } catch (error) {
10677
+ await rm4(temporary, { force: true });
10678
+ throw error;
10679
+ }
10680
+ }
10681
+ async function freeBackupPath(path) {
10682
+ for (let number = 0; ; number += 1) {
10683
+ const candidate = number === 0 ? `${path}.bak` : `${path}.bak.${number}`;
10684
+ const taken = await lstat(candidate).then(
10685
+ () => true,
10686
+ (error) => {
10687
+ if (isMissing(error)) return false;
10688
+ throw error;
10689
+ }
10690
+ );
10691
+ if (!taken) return candidate;
10692
+ }
10693
+ }
10694
+ function isMissing(error) {
10695
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
10696
+ }
10697
+ async function workingSavedKey(options, url) {
10698
+ const saved = await readStoredCredential(readConfigCliEnv(options.env), url);
10699
+ if (saved === void 0 || Date.parse(saved.expiresAt) <= Date.now()) return void 0;
10700
+ try {
10701
+ const api = await createConfigApi({
10702
+ baseUrl: url,
10703
+ apiKey: saved.apiKey,
10704
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
10705
+ ...options.sleep === void 0 ? {} : { sleep: options.sleep }
10706
+ });
10707
+ await api.read((client) => client.platform.context.get());
10708
+ return saved;
10709
+ } catch (error) {
10710
+ if (error instanceof ApiKeyExchangeError && (error.status === 401 || error.status === 403)) {
10711
+ return void 0;
10712
+ }
10713
+ throw error;
10714
+ }
10715
+ }
10716
+ function scopesOf(scopes) {
10717
+ return scopes === void 0 ? "" : ` (${scopes.join(", ")})`;
10718
+ }
10719
+ function openInBrowser(url) {
10720
+ return (address) => {
10721
+ if (new URL(address).origin !== credentialOrigin(url)) return;
10722
+ const [command, args] = process.platform === "darwin" ? ["open", [address]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", address]] : ["xdg-open", [address]];
10723
+ try {
10724
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
10725
+ child.on("error", () => void 0);
10726
+ child.unref();
10727
+ } catch {
10728
+ }
10729
+ };
10730
+ }
10731
+
10204
10732
  // src/index.ts
10205
10733
  var helpText = `Usage: catalyst config <command> [options]
10206
10734
  catalyst login --instance <name-or-url>
10207
10735
  catalyst logout --instance <name-or-url>
10736
+ catalyst setup <claude-code|codex|other> --instance <url> [--force]
10208
10737
 
10209
10738
  Sync local config assets with a live Catalyst instance.
10210
10739
 
10211
10740
  Login:
10212
- login Show a connect code, wait until you confirm it in Settings, API keys, and
10213
- save the personal API key it issues, readable by you alone
10214
- logout Forget the saved key of an instance; revoke it in Settings
10741
+ login Show a connect code, wait until you confirm it on the instance at
10742
+ /settings/you/api-keys, and save the personal API key it issues, readable by
10743
+ you alone
10744
+ logout Forget the saved key of an instance; revoke it at /settings/you/api-keys
10745
+ setup Connect a local agent: log in as 'login' does unless a working key is saved,
10746
+ then write the Catalyst skill for Claude Code, Codex or another tool. --force
10747
+ replaces a different skill file there and keeps it as SKILL.md.bak
10215
10748
 
10216
10749
  Commands:
10217
10750
  pull Sync remote config assets into the local working copy
@@ -10222,13 +10755,18 @@ Commands:
10222
10755
  show Print a remote asset: show <agent|skill|guardrail> <name>
10223
10756
 
10224
10757
  Options:
10225
- --instance <name-or-url> Manifest instance name or direct URL
10758
+ --instance <name-or-url> Manifest instance name or direct URL. Left out: the
10759
+ manifest's defaultInstance; outside a working copy, the
10760
+ one instance a key is saved for
10226
10761
  --dir <working-copy-dir> Directory containing catalyst.yaml (default: .)
10227
10762
  --prune Mirror on push, deleting the remote-only assets the last pull
10228
10763
  saw
10229
10764
  --only <agent:name|skill:name|guardrail:name>
10230
10765
  Sync selected assets; repeatable on push/pull
10766
+ --workspace <id> On pull: bind the working copy to the workspace and hold
10767
+ what it owns
10231
10768
  --help Show this help
10769
+ --version Print the version of this CLI
10232
10770
 
10233
10771
  Environment:
10234
10772
  CATALYST_API_KEY API key exchanged for a short-lived access token. It wins over
@@ -10236,10 +10774,16 @@ Environment:
10236
10774
  CATALYST_CONFIG_HOME Folder of the saved keys (default: ~/.config/catalyst)
10237
10775
 
10238
10776
  A working copy that belongs to one Namespace names it beside the instance's url in
10239
- catalyst.yaml, as 'namespace: kai-'. Its push names that Namespace, as the instance asks
10777
+ catalyst.yaml, as 'namespace: tax-'. Its push names that Namespace, as the instance asks
10240
10778
  of everyone but the holder of the release right; its pull, diff and list read only the
10241
10779
  assets the key may read, never the instance's version or default agent.
10242
10780
 
10781
+ A working copy bound to a workspace names it the same way, as 'workspace: <id>', which the
10782
+ first 'catalyst config pull --workspace <id>' writes. It holds what that workspace owns, and
10783
+ everything its push creates is owned by that workspace, guardrails included. Its push is never
10784
+ a release: a new agent starts hidden until an instance admin offers it. A working copy without
10785
+ either holds the instance's own assets.
10786
+
10243
10787
  Guardrails join the working copy where catalyst.yaml lists their globs under 'guardrails',
10244
10788
  such as 'guardrails/*/guardrail.yaml'. Each is a folder with its guardrail.yaml and, for a
10245
10789
  model_judge check, its instruction in check.md. Guardrails are no part of a release: a push
@@ -10254,7 +10798,12 @@ async function runCli(argv, runtime = {}) {
10254
10798
  ${helpText}`);
10255
10799
  return 0;
10256
10800
  }
10257
- if (argv[0] === "login" || argv[0] === "logout") {
10801
+ if (argv[0] === "--version") {
10802
+ stdout(`${RELEASE_VERSION}
10803
+ `);
10804
+ return 0;
10805
+ }
10806
+ if (argv[0] === "login" || argv[0] === "logout" || argv[0] === "setup") {
10258
10807
  return runLoginCommand(argv[0], argv.slice(1), runtime, stdout, stderr);
10259
10808
  }
10260
10809
  if (argv[0] !== "config") {
@@ -10292,6 +10841,7 @@ ${helpText}`);
10292
10841
  force: { type: "boolean", default: false },
10293
10842
  prune: { type: "boolean", default: false },
10294
10843
  only: { type: "string", multiple: true },
10844
+ workspace: { type: "string" },
10295
10845
  help: { type: "boolean", short: "h", default: false }
10296
10846
  },
10297
10847
  allowPositionals: true,
@@ -10313,6 +10863,12 @@ ${helpText}`);
10313
10863
  stderr("--only is only valid with 'catalyst config push' or 'catalyst config pull'.\n");
10314
10864
  return 2;
10315
10865
  }
10866
+ if (parsed.values.workspace !== void 0 && command !== "pull") {
10867
+ stderr(
10868
+ "--workspace is only valid with 'catalyst config pull', which binds the working copy.\n"
10869
+ );
10870
+ return 2;
10871
+ }
10316
10872
  if (parsed.values.prune && parsed.values.only) {
10317
10873
  stderr("--prune cannot be combined with --only.\n");
10318
10874
  return 2;
@@ -10334,6 +10890,7 @@ ${helpText}`);
10334
10890
  ...parsed.values.force ? { force: true } : {},
10335
10891
  ...parsed.values.prune ? { prune: true } : {},
10336
10892
  ...parsed.values.only === void 0 ? {} : { only: parsed.values.only },
10893
+ ...parsed.values.workspace === void 0 ? {} : { workspace: parsed.values.workspace },
10337
10894
  ...command === "show" && isShownKind(parsed.positionals[0]) ? { assetKind: parsed.positionals[0], assetName: parsed.positionals[1] } : {},
10338
10895
  ...runtime.fetchImpl === void 0 ? {} : { fetchImpl: runtime.fetchImpl },
10339
10896
  ...runtime.env === void 0 ? {} : { env: runtime.env },
@@ -10355,8 +10912,10 @@ async function runLoginCommand(command, args, runtime, stdout, stderr) {
10355
10912
  options: {
10356
10913
  instance: { type: "string" },
10357
10914
  dir: { type: "string" },
10915
+ force: { type: "boolean", default: false },
10358
10916
  help: { type: "boolean", short: "h", default: false }
10359
10917
  },
10918
+ allowPositionals: command === "setup",
10360
10919
  strict: true
10361
10920
  });
10362
10921
  } catch (error) {
@@ -10368,6 +10927,11 @@ async function runLoginCommand(command, args, runtime, stdout, stderr) {
10368
10927
  stdout(helpText);
10369
10928
  return 0;
10370
10929
  }
10930
+ if (parsed.values.force && command !== "setup") {
10931
+ stderr(`--force is not an option of 'catalyst ${command}'.
10932
+ `);
10933
+ return 2;
10934
+ }
10371
10935
  const options = {
10372
10936
  cwd: runtime.cwd ?? process.cwd(),
10373
10937
  ...parsed.values.dir === void 0 ? {} : { dir: parsed.values.dir },
@@ -10378,6 +10942,20 @@ async function runLoginCommand(command, args, runtime, stdout, stderr) {
10378
10942
  stdout,
10379
10943
  stderr
10380
10944
  };
10945
+ if (command === "setup") {
10946
+ const [tool, ...extra] = parsed.positionals;
10947
+ if (!isLocalAgentTool(tool) || extra.length > 0 || parsed.values.instance === void 0) {
10948
+ stderr(`${SETUP_USAGE}
10949
+ `);
10950
+ return 2;
10951
+ }
10952
+ return setupCommand({
10953
+ ...options,
10954
+ tool,
10955
+ ...parsed.values.force ? { force: true } : {},
10956
+ ...runtime.openUrl === void 0 ? {} : { openUrl: runtime.openUrl }
10957
+ });
10958
+ }
10381
10959
  return command === "login" ? loginCommand(options) : logoutCommand(options);
10382
10960
  }
10383
10961
  function isShownKind(value) {
@@ -10398,6 +10976,7 @@ export {
10398
10976
  WorkingCopyValidationError,
10399
10977
  agentAssetPath,
10400
10978
  assertSafeApiKeyExchangeUrl,
10979
+ bindManifestWorkspace,
10401
10980
  canonicalBundleFiles,
10402
10981
  canonicalizeAgentConfig,
10403
10982
  canonicalizeSkillConfig,
@@ -10420,6 +10999,7 @@ export {
10420
10999
  readStateFile,
10421
11000
  readWorkingCopy,
10422
11001
  removeStaleManifestAssets,
11002
+ renderCatalystSkill,
10423
11003
  resolveInstance,
10424
11004
  resolveSkillResourceTarget,
10425
11005
  runCli,