@runtypelabs/cli 2.44.1 → 2.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +43 -1
  2. package/dist/index.js +737 -63
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -644,6 +644,7 @@ Manage agents.
644
644
  | `agents execute <id> -m <message>` | Execute an agent with a message |
645
645
  | `agents task <agent> -g <goal>` | Run a multi-session task (see Marathon section) |
646
646
  | `agents ensure <file>` | Converge a definition from a file |
647
+ | `agents pull <name>` | Print the definition file `ensure` reads |
647
648
  | `agents delete <id>` | Delete an agent |
648
649
 
649
650
  `agents execute` and `dispatch --agent` take `--alias <name>` or `--version-id <id>`
@@ -677,7 +678,48 @@ runtype agents aliases activate agent_abc live --version-id agentver_9 --revisio
677
678
 
678
679
  `agents ensure <file> --deploy <alias>` saves and activates in one step;
679
680
  `--release publish|none` is the older spelling of the same thing, and supplying both
680
- is refused. A non-live deploy leaves live unchanged.
681
+ is refused. A non-live deploy leaves live unchanged. `--bindings-file <file>` (a JSON
682
+ object of `{ SECRET_NAME: value }`) replaces the secret bindings of the `--deploy`
683
+ alias; `{}` clears them. Keep that file out of version control.
684
+
685
+ ### Config as code: `ensure` and `pull`
686
+
687
+ Each entity the TypeScript SDK converges has an `ensure` and a `pull` subcommand.
688
+ `pull` prints the definition file that `ensure` reads (stdout, or `-o <file>`), so
689
+ `runtype flows pull "Triage" > flows/triage.json` absorbs a dashboard edit into the repo.
690
+
691
+ | Command | Definition file |
692
+ | --------------------------------------------- | ------------------------------ |
693
+ | `agents ensure` / `agents pull` | `defineAgent` input + `skills` |
694
+ | `agents skills ensure` / `agents skills pull` | `defineSkill` input |
695
+ | `flows ensure` / `flows pull` | `defineFlow` input |
696
+ | `tools ensure` / `tools pull` | `defineTool` input |
697
+ | `products ensure` / `products pull` | `defineProduct` input |
698
+ | `surfaces ensure` / `surfaces pull --product` | `defineSurface` input |
699
+ | `products ensure-fpo` / `products pull-fpo` | Full Product Object |
700
+
701
+ `agents skills` manages Runtype Agent Skills, the context bundles deployed agents
702
+ load. `runtype skills` installs skills into local coding agents; it is unrelated.
703
+
704
+ Every `ensure` takes `--dry-run`, `--expect-no-changes` (exit 1 on drift, for CI),
705
+ `--overwrite-managed` (converge over a dashboard edit) and
706
+ `--expected-remote-hash <hash>`. A dry run prints the remote hash; pass it to the
707
+ apply so the write is refused if someone changed the entity in between:
708
+
709
+ ```bash
710
+ runtype flows ensure flows/triage.json --dry-run
711
+ runtype flows ensure flows/triage.json --expected-remote-hash <remoteHash>
712
+ ```
713
+
714
+ Agents, skills and flows also take `--release none|publish` (agents: `--deploy`) and
715
+ `--label` / `--notes`, which stamp provenance such as a git SHA on the appended
716
+ version. `products ensure-fpo` also takes `--prune`, which removes product-scoped
717
+ capabilities and surfaces the FPO no longer declares, and `--hash-probe`, which
718
+ skips the upload when the live product already matches (useful for a pulled FPO).
719
+
720
+ `client-tokens ensure <file>` converges a client token by name (`--dry-run`,
721
+ `--expect-no-changes`, `--json`). It has no `pull`, and it never deletes or
722
+ regenerates a token.
681
723
 
682
724
  ##### Cleaning up a pull request's preview
683
725
 
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { Box, Text, render, useApp, useInput, useStdout } from "ink";
11
11
  import open from "open";
12
12
  import crypto$1 from "crypto";
13
13
  import express from "express";
14
- import { AgentAliasPreviewLimitError, DEFAULT_STALL_STOP_AFTER, ENSURE_RELEASE_DEPLOY_CONFLICT_MESSAGE, LEDGER_ARTIFACT_LINE_PREFIX, RuntypeApiError, RuntypeClient, activateAgentPromotion, buildLedgerOffloadReference, compileWorkflowConfig, createClient, defaultWorkflow, deployWorkflow, ensureDefaultWorkflowHooks, evaluateGeneratedRuntimeToolProposal, gameWorkflow, getDefaultPlanPath, isBuiltinToolExecuteResponse, isWorkflowHookRef, parseFinalBuffer, parseLedgerArtifactRelativePath, parseOffloadedOutputId, parseSSEChunk, prepareAgentPromotion, processStream, registerWorkflowHook, validateAgentPromotion } from "@runtypelabs/sdk";
14
+ import { AgentAliasPreviewLimitError, ClientTokensNamespace, DEFAULT_STALL_STOP_AFTER, ENSURE_RELEASE_DEPLOY_CONFLICT_MESSAGE, FlowsNamespace, LEDGER_ARTIFACT_LINE_PREFIX, ProductsNamespace, RuntypeApiError, RuntypeClient, SkillsNamespace, SurfacesNamespace, ToolsNamespace, activateAgentPromotion, buildLedgerOffloadReference, compileWorkflowConfig, createClient, defaultWorkflow, defineFlow, defineFpo, defineProduct, defineSkill, defineSurface, defineTool, deployWorkflow, ensureDefaultWorkflowHooks, evaluateGeneratedRuntimeToolProposal, gameWorkflow, getDefaultPlanPath, isBuiltinToolExecuteResponse, isWorkflowHookRef, parseFinalBuffer, parseLedgerArtifactRelativePath, parseOffloadedOutputId, parseSSEChunk, prepareAgentPromotion, processStream, registerWorkflowHook, validateAgentPromotion } from "@runtypelabs/sdk";
15
15
  import Conf from "conf";
16
16
  import os, { tmpdir } from "os";
17
17
  import path$1, { delimiter, dirname as dirname$1, join as join$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
@@ -3318,6 +3318,114 @@ function handleIntersectionResults(result, left, right) {
3318
3318
  result.value = merged.data;
3319
3319
  return result;
3320
3320
  }
3321
+ const $ZodTuple = /*@__PURE__*/ $constructor("$ZodTuple", (inst, def) => {
3322
+ $ZodType.init(inst, def);
3323
+ const items = def.items;
3324
+ const memo = globalConfig.memoizer;
3325
+ memo?.attach(inst);
3326
+ inst._zod.parse = (payload, ctx) => {
3327
+ const input = payload.value;
3328
+ if (!Array.isArray(input)) {
3329
+ payload.issues.push({
3330
+ input,
3331
+ inst,
3332
+ expected: "tuple",
3333
+ code: "invalid_type"
3334
+ });
3335
+ return payload;
3336
+ }
3337
+ payload.value = memo ? memo.alloc(inst, payload, [], ctx) : [];
3338
+ const proms = [];
3339
+ const optinStart = getTupleOptStart(items, "optin");
3340
+ const optoutStart = getTupleOptStart(items, "optout");
3341
+ if (!def.rest) {
3342
+ if (input.length < optinStart) {
3343
+ payload.issues.push({
3344
+ code: "too_small",
3345
+ minimum: optinStart,
3346
+ inclusive: true,
3347
+ input,
3348
+ inst,
3349
+ origin: "array"
3350
+ });
3351
+ return payload;
3352
+ }
3353
+ if (input.length > items.length) payload.issues.push({
3354
+ code: "too_big",
3355
+ maximum: items.length,
3356
+ inclusive: true,
3357
+ input,
3358
+ inst,
3359
+ origin: "array"
3360
+ });
3361
+ }
3362
+ const itemResults = new Array(items.length);
3363
+ const abortEarly = def.rest ? ctx?.abortEarly : void 0;
3364
+ let itemAborted = false;
3365
+ for (let i = 0; i < items.length; i++) {
3366
+ const r = items[i]._zod.run({
3367
+ value: input[i],
3368
+ issues: []
3369
+ }, ctx);
3370
+ if (r instanceof Promise) proms.push(r.then((rr) => {
3371
+ itemResults[i] = rr;
3372
+ }));
3373
+ else {
3374
+ itemResults[i] = r;
3375
+ if (abortEarly && !itemAborted && r.issues.length) itemAborted = aborted(r);
3376
+ }
3377
+ }
3378
+ if (def.rest && !itemAborted) {
3379
+ let i = items.length - 1;
3380
+ const rest = input.slice(items.length);
3381
+ let seen = payload.issues.length;
3382
+ for (const el of rest) {
3383
+ if (abortEarly && payload.issues.length !== seen) {
3384
+ if (aborted(payload, seen)) break;
3385
+ seen = payload.issues.length;
3386
+ }
3387
+ i++;
3388
+ const result = def.rest._zod.run({
3389
+ value: el,
3390
+ issues: []
3391
+ }, ctx);
3392
+ if (result instanceof Promise) proms.push(result.then((r) => handleTupleResult(r, payload, i)));
3393
+ else handleTupleResult(result, payload, i);
3394
+ }
3395
+ }
3396
+ if (proms.length) return Promise.all(proms).then(() => handleTupleResults(itemResults, payload, items, input, optoutStart));
3397
+ return handleTupleResults(itemResults, payload, items, input, optoutStart);
3398
+ };
3399
+ });
3400
+ function getTupleOptStart(items, key) {
3401
+ for (let i = items.length - 1; i >= 0; i--) if (!(key === "optin" ? items[i]._zod.optin !== void 0 : items[i]._zod.optout === "optional")) return i + 1;
3402
+ return 0;
3403
+ }
3404
+ function handleTupleResult(result, final, index) {
3405
+ if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));
3406
+ final.value[index] = result.value;
3407
+ }
3408
+ function handleTupleResults(itemResults, final, items, input, optoutStart) {
3409
+ for (let i = 0; i < items.length; i++) {
3410
+ const r = itemResults[i];
3411
+ const isPresent = i < input.length;
3412
+ if (!isPresent && i >= optoutStart && items[i]._zod.optin === "optional") {
3413
+ final.value.length = i;
3414
+ break;
3415
+ }
3416
+ if (r.issues.length) {
3417
+ if (!isPresent && i >= optoutStart) {
3418
+ final.value.length = i;
3419
+ break;
3420
+ }
3421
+ final.issues.push(...prefixIssues(i, r.issues));
3422
+ }
3423
+ final.value[i] = r.value;
3424
+ }
3425
+ for (let i = final.value.length - 1; i >= input.length; i--) if (items[i]._zod.optout === "optional" && final.value[i] === void 0) final.value.length = i;
3426
+ else break;
3427
+ return final;
3428
+ }
3321
3429
  const $ZodRecord = /*@__PURE__*/ $constructor("$ZodRecord", (inst, def) => {
3322
3430
  $ZodType.init(inst, def);
3323
3431
  const memo = globalConfig.memoizer;
@@ -5394,6 +5502,58 @@ const intersectionProcessor = (schema, ctx, json, params) => {
5394
5502
  json.allOf = allOf;
5395
5503
  ctx.intersections.push(allOf);
5396
5504
  };
5505
+ const tupleProcessor = (schema, ctx, _json, params) => {
5506
+ const json = _json;
5507
+ const def = schema._zod.def;
5508
+ json.type = "array";
5509
+ const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
5510
+ const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
5511
+ const prefixItems = def.items.map((x, i) => processSchema(x, ctx, {
5512
+ ...params,
5513
+ path: [
5514
+ ...params.path,
5515
+ prefixPath,
5516
+ i
5517
+ ]
5518
+ }));
5519
+ const rest = def.rest ? processSchema(def.rest, ctx, {
5520
+ ...params,
5521
+ path: [
5522
+ ...params.path,
5523
+ restPath,
5524
+ ...ctx.target === "openapi-3.0" ? [def.items.length] : []
5525
+ ]
5526
+ }) : null;
5527
+ let minItems = def.items.length;
5528
+ while (minItems > 0) {
5529
+ const item = def.items[minItems - 1];
5530
+ if (!(ctx.io === "input" ? inputOptin(item) !== void 0 : item._zod.optout === "optional")) break;
5531
+ minItems--;
5532
+ }
5533
+ const maxItems = def.items.length;
5534
+ const isClosed = !def.rest;
5535
+ if (ctx.target === "draft-2020-12") {
5536
+ json.prefixItems = prefixItems;
5537
+ if (isClosed) json.items = false;
5538
+ else if (rest) json.items = rest;
5539
+ if (minItems > 0) json.minItems = minItems;
5540
+ if (isClosed) json.maxItems = maxItems;
5541
+ } else if (ctx.target === "openapi-3.0") {
5542
+ json.items = { anyOf: prefixItems };
5543
+ if (rest) json.items.anyOf.push(rest);
5544
+ if (minItems > 0) json.minItems = minItems;
5545
+ if (isClosed) json.maxItems = maxItems;
5546
+ } else {
5547
+ json.items = prefixItems;
5548
+ if (isClosed) json.additionalItems = false;
5549
+ else if (rest) json.additionalItems = rest;
5550
+ if (minItems > 0) json.minItems = minItems;
5551
+ if (isClosed) json.maxItems = maxItems;
5552
+ }
5553
+ const { minimum, maximum } = aggregateChecks(schema);
5554
+ if (typeof minimum === "number") json.minItems = minimum;
5555
+ if (typeof maximum === "number") json.maxItems = maximum;
5556
+ };
5397
5557
  /** JSON object keys are always strings, so a numeric record key schema is re-expressed over the
5398
5558
  * numeric-string form the record parser matches. Deferred to `finalize`, after the flatten: a key
5399
5559
  * behind a wrapper only carries its own `type` before then, and a union key only has its branches.
@@ -6315,6 +6475,39 @@ function intersection(left, right) {
6315
6475
  right
6316
6476
  });
6317
6477
  }
6478
+ const ZodTuple = /*@__PURE__*/ $constructor("ZodTuple", (inst, def) => {
6479
+ _ensureDefaultMemoizer();
6480
+ $ZodTuple.init(inst, def);
6481
+ ZodType.init(inst, def);
6482
+ inst._zod.processJSONSchema = (ctx, json, params) => tupleProcessor(inst, ctx, json, params);
6483
+ }, {
6484
+ rest(rest) {
6485
+ return this.clone({
6486
+ ...this._zod.def,
6487
+ rest
6488
+ });
6489
+ },
6490
+ partial() {
6491
+ const def = this._zod.def;
6492
+ if (def.checks?.length) throw new Error(".partial() cannot be used on tuple schemas containing refinements");
6493
+ return this.clone({
6494
+ ...def,
6495
+ items: def.items.map((item) => new ZodOptional({
6496
+ type: "optional",
6497
+ innerType: item
6498
+ }))
6499
+ });
6500
+ }
6501
+ });
6502
+ function tuple(items, _paramsOrRest, _params) {
6503
+ const hasRest = _paramsOrRest instanceof $ZodType;
6504
+ return new ZodTuple({
6505
+ type: "tuple",
6506
+ items,
6507
+ rest: hasRest ? _paramsOrRest : null,
6508
+ ...normalizeParams(hasRest ? _params : _paramsOrRest)
6509
+ });
6510
+ }
6318
6511
  const ZodRecord = /*@__PURE__*/ $constructor("ZodRecord", (inst, def) => {
6319
6512
  _ensureDefaultMemoizer();
6320
6513
  $ZodRecord.init(inst, def);
@@ -12478,7 +12671,48 @@ const GEN_AI_ATTRIBUTES = {
12478
12671
  toolCallId: "gen_ai.tool.call.id"
12479
12672
  };
12480
12673
  //#endregion
12481
- //#region ../shared/dist/chunk-B1SxgkWd.mjs
12674
+ //#region ../shared/dist/context-manifest-wire.mjs
12675
+ const entryTupleSchema = union([tuple([
12676
+ string(),
12677
+ number$1().int(),
12678
+ number$1().int(),
12679
+ number$1().int(),
12680
+ number$1().int(),
12681
+ number$1().int(),
12682
+ number$1().int()
12683
+ ]), tuple([
12684
+ string(),
12685
+ number$1().int(),
12686
+ number$1().int(),
12687
+ number$1().int(),
12688
+ number$1().int(),
12689
+ number$1().int(),
12690
+ number$1().int(),
12691
+ string(),
12692
+ string()
12693
+ ])]);
12694
+ object({
12695
+ v: literal(1),
12696
+ s: number$1().int().nonnegative(),
12697
+ e: array(entryTupleSchema),
12698
+ b: array(number$1().int().nonnegative()),
12699
+ x: array(tuple([entryTupleSchema, number$1().int()]))
12700
+ });
12701
+ object({
12702
+ v: literal(1),
12703
+ b: array(tuple([
12704
+ string(),
12705
+ string(),
12706
+ union([literal(0), literal(1)])
12707
+ ]))
12708
+ });
12709
+ object({
12710
+ hash: string(),
12711
+ content: string(),
12712
+ truncated: boolean()
12713
+ });
12714
+ //#endregion
12715
+ //#region ../shared/dist/chunk-_ZhmfUpr.mjs
12482
12716
  /**
12483
12717
  * GenAI semconv content attributes, set by a customer's own instrumentation when it opts into content export.
12484
12718
  * Two first-party producers write them: @runtypelabs/runtime's OTLP tracer for exported agents (content on by default), and @runtypelabs/flue-otel (opt-in).
@@ -12498,7 +12732,9 @@ const GEN_AI_CONTENT_ATTRIBUTES = {
12498
12732
  /** `execute_tool` span: the arguments the tool was called with. */
12499
12733
  toolCallArguments: GEN_AI_TOOL_CALL_ARGUMENTS,
12500
12734
  /** `execute_tool` span: the result the tool returned. */
12501
- toolCallResult: GEN_AI_TOOL_CALL_RESULT
12735
+ toolCallResult: GEN_AI_TOOL_CALL_RESULT,
12736
+ /** `chat` span: the tool schemas the call offered the model. */
12737
+ toolDefinitions: "gen_ai.tool.definitions"
12502
12738
  };
12503
12739
  /**
12504
12740
  * Read-side aliases for content attributes: canonical semconv name to vendor-namespaced spellings a stock instrumentation uses.
@@ -14454,6 +14690,39 @@ function unknownCatalogToolMessage(toolId) {
14454
14690
  return `Tool "${toolId}" is not a built-in or platform catalog tool. Check the id against the tool catalog (list_tools), or point toolId at a saved tool's "tool_..." id or its "tool:<name>" reference.`;
14455
14691
  }
14456
14692
  /**
14693
+ * Name-based references to saved agents and flows (agent:<name>, flow:<name>) for portable ensure/config-as-code definitions.
14694
+ * The name is canonical: hashed and persisted verbatim so definitions converge to the same content hash across environments.
14695
+ * Grammar mirrors parseNamedToolRef: split on the first colon only, exact case-sensitive match, no trimming.
14696
+ * The flow: prefix also names a template-variable namespace, but the two grammars never collide.
14697
+ */
14698
+ /** Reserved prefix for a name-based saved-agent reference (`agent:<name>`). */
14699
+ const NAMED_AGENT_REF_PREFIX = "agent:";
14700
+ /** Reserved prefix for a name-based saved-flow reference (`flow:<name>`). */
14701
+ const NAMED_FLOW_REF_PREFIX = "flow:";
14702
+ function isNamedRef(value, prefix) {
14703
+ return value.startsWith(prefix) && value.length > prefix.length;
14704
+ }
14705
+ function parseNamedRef(value, prefix) {
14706
+ if (!isNamedRef(value, prefix)) return null;
14707
+ return { name: value.slice(prefix.length) };
14708
+ }
14709
+ /**
14710
+ * Parse an `agent:<name>` reference into its name. Split on the FIRST colon
14711
+ * only; the remainder is the agent name verbatim. Returns null when `value` is
14712
+ * not a valid named agent ref.
14713
+ */
14714
+ function parseNamedAgentRef(value) {
14715
+ return parseNamedRef(value, NAMED_AGENT_REF_PREFIX);
14716
+ }
14717
+ /**
14718
+ * Parse a `flow:<name>` reference into its name. Split on the FIRST colon only;
14719
+ * the remainder is the flow name verbatim. Returns null when `value` is not a
14720
+ * valid named flow ref.
14721
+ */
14722
+ function parseNamedFlowRef(value) {
14723
+ return parseNamedRef(value, NAMED_FLOW_REF_PREFIX);
14724
+ }
14725
+ /**
14457
14726
  * Return whether an `execute-agent` step's `config.agentId` is a template
14458
14727
  * expression rather than a fixed agent reference. No static gate can scan the
14459
14728
  * agent a `{{...}}` ref substitutes to, so the runtime lane folds it as
@@ -14499,10 +14768,12 @@ function walkSteps$1(steps, basePath, context, state) {
14499
14768
  }
14500
14769
  function walkStep(node, path, context, state) {
14501
14770
  if (!isRecord$1(node)) return;
14502
- if (node.enabled === false) return;
14771
+ if (node.enabled === false && !state.includeDisabled) return;
14503
14772
  const config = isRecord$1(node.config) ? node.config : void 0;
14504
14773
  if (!config) return;
14774
+ state.visitor.visitStep?.(node, config, path, context);
14505
14775
  if (node.type === "execute-agent") state.visitor.visitExecuteAgent(node, config, path, context);
14776
+ visitFlowFallbacks(config, path, context, state);
14506
14777
  walkToolPayloads(config.tools, `${path}.config.tools`, context, state);
14507
14778
  const deeper = {
14508
14779
  ...state,
@@ -14524,6 +14795,21 @@ function walkStep(node, path, context, state) {
14524
14795
  });
14525
14796
  for (const key of CONDITIONAL_BRANCH_STEP_KEYS$2) walkSteps$1(config[key], `${path}.config.${key}`, context, nested);
14526
14797
  }
14798
+ function visitFlowFallbacks(config, path, context, state) {
14799
+ const { visitFlowFallback, visitStepFallback } = state.visitor;
14800
+ if (!visitFlowFallback && !visitStepFallback || !isRecord$1(config.errorHandling)) return;
14801
+ const fallbacks = config.errorHandling.fallbacks;
14802
+ if (!Array.isArray(fallbacks)) return;
14803
+ fallbacks.forEach((fallback, index) => {
14804
+ if (!isRecord$1(fallback)) return;
14805
+ const fallbackPath = `${path}.config.errorHandling.fallbacks[${index}]`;
14806
+ if (fallback.type === "flow") visitFlowFallback?.(fallback, fallbackPath, context);
14807
+ else if (fallback.type === "step" && visitStepFallback && isRecord$1(fallback.stepConfig)) {
14808
+ visitStepFallback(fallback, fallback.stepConfig, fallbackPath, context);
14809
+ walkToolPayloads(fallback.stepConfig.tools, `${fallbackPath}.stepConfig.tools`, context, state);
14810
+ }
14811
+ });
14812
+ }
14527
14813
  const TOOL_BAG_KEYS = ["runtimeTools", "inlineTools"];
14528
14814
  /** Descend the inline flow-as-tool and skill payloads a step's tool bag can carry. */
14529
14815
  function walkToolPayloads(tools, path, context, state) {
@@ -14546,7 +14832,7 @@ function walkRuntimeTools(runtimeTools, basePath, context, state) {
14546
14832
  if (!config) return;
14547
14833
  const toolPath = `${basePath}[${index}].config`;
14548
14834
  if (isRecord$1(config.skill) && Array.isArray(config.skill.resolvedTools)) walkRuntimeTools(config.skill.resolvedTools, `${toolPath}.skill.resolvedTools`, context, deeper);
14549
- if (isRecord$1(config.flow) && Array.isArray(config.flow.steps)) {
14835
+ if (!state.skipInlineFlows && isRecord$1(config.flow) && Array.isArray(config.flow.steps)) {
14550
14836
  const nestedContext = state.visitor.enterNestedFlow?.(config.flow, context) ?? context;
14551
14837
  walkSteps$1(config.flow.steps, `${toolPath}.flow.steps`, nestedContext, {
14552
14838
  ...deeper,
@@ -18560,39 +18846,6 @@ function normalizeMemorySummaryConfig(config) {
18560
18846
  defaultValue: config.defaultValue ?? config.default_value
18561
18847
  };
18562
18848
  }
18563
- /**
18564
- * Name-based references to saved agents and flows (agent:<name>, flow:<name>) for portable ensure/config-as-code definitions.
18565
- * The name is canonical: hashed and persisted verbatim so definitions converge to the same content hash across environments.
18566
- * Grammar mirrors parseNamedToolRef: split on the first colon only, exact case-sensitive match, no trimming.
18567
- * The flow: prefix also names a template-variable namespace, but the two grammars never collide.
18568
- */
18569
- /** Reserved prefix for a name-based saved-agent reference (`agent:<name>`). */
18570
- const NAMED_AGENT_REF_PREFIX = "agent:";
18571
- /** Reserved prefix for a name-based saved-flow reference (`flow:<name>`). */
18572
- const NAMED_FLOW_REF_PREFIX = "flow:";
18573
- function isNamedRef(value, prefix) {
18574
- return value.startsWith(prefix) && value.length > prefix.length;
18575
- }
18576
- function parseNamedRef(value, prefix) {
18577
- if (!isNamedRef(value, prefix)) return null;
18578
- return { name: value.slice(prefix.length) };
18579
- }
18580
- /**
18581
- * Parse an `agent:<name>` reference into its name. Split on the FIRST colon
18582
- * only; the remainder is the agent name verbatim. Returns null when `value` is
18583
- * not a valid named agent ref.
18584
- */
18585
- function parseNamedAgentRef(value) {
18586
- return parseNamedRef(value, NAMED_AGENT_REF_PREFIX);
18587
- }
18588
- /**
18589
- * Parse a `flow:<name>` reference into its name. Split on the FIRST colon only;
18590
- * the remainder is the flow name verbatim. Returns null when `value` is not a
18591
- * valid named flow ref.
18592
- */
18593
- function parseNamedFlowRef(value) {
18594
- return parseNamedRef(value, NAMED_FLOW_REF_PREFIX);
18595
- }
18596
18849
  function sanitizeCodeToolIdentifier(name) {
18597
18850
  let safe = name.replace(/[^a-zA-Z0-9_$]/g, "_");
18598
18851
  if (/^[0-9]/.test(safe)) safe = "_" + safe;
@@ -22149,7 +22402,8 @@ _enum([
22149
22402
  "saved_from_run",
22150
22403
  "generated",
22151
22404
  "imported",
22152
- "fpo"
22405
+ "fpo",
22406
+ "ensure"
22153
22407
  ]);
22154
22408
  /**
22155
22409
  * Where a saved_from_run case was forked from.
@@ -23098,6 +23352,11 @@ const fpoSkillCapabilitiesSchema = object({
23098
23352
  inlineTools: array(fpoSkillInlineToolSchema).optional(),
23099
23353
  mcpServers: never({ error: "Skill capabilities cannot declare `mcpServers` — MCP server bindings are rejected at write for v1 skills. Bind MCP tools via a saved tool id or an inlineTool instead." }).optional()
23100
23354
  });
23355
+ /** A `bindTo` entry that pins the binding to one skill versionNumber instead of following published. */
23356
+ const fpoSkillBindTargetSchema = object({
23357
+ capability: string().min(1),
23358
+ version: number$1().int().positive().optional()
23359
+ }).strict();
23101
23360
  const fpoSkillSchema = object({
23102
23361
  id: string().min(1),
23103
23362
  slug: string().regex(SKILL_NAME_PATTERN, "Skill slug must start with a letter and contain only lowercase letters, digits, underscores, and hyphens (max 64 chars).").optional(),
@@ -23110,7 +23369,7 @@ const fpoSkillSchema = object({
23110
23369
  "community"
23111
23370
  ]).optional(),
23112
23371
  capabilities: fpoSkillCapabilitiesSchema.optional(),
23113
- bindTo: array(string()).optional(),
23372
+ bindTo: array(union([string(), fpoSkillBindTargetSchema])).optional(),
23114
23373
  createPolicy: createPolicySchema
23115
23374
  });
23116
23375
  const metaSchema = object({
@@ -24099,8 +24358,17 @@ function validateFPO(fpo) {
24099
24358
  if (!skill || typeof skill !== "object") continue;
24100
24359
  const s = skill;
24101
24360
  if (Array.isArray(s.bindTo)) {
24102
- for (const [bi, ref] of s.bindTo.entries()) if (!capIds.has(ref)) result.errors.push(createIssue("error", "SKILL_BIND_CAPABILITY_NOT_FOUND", `Skill "${id ?? ""}" binds to capability "${ref}" which does not exist`, `skills[${i}].bindTo[${bi}]`));
24103
- else if (!agentBackedCapIds.has(ref)) result.errors.push(createIssue("error", "SKILL_BIND_TARGET_NOT_AGENT", `Skill "${id ?? ""}" binds to capability "${ref}" which is not agent-backed; skills can only be bound to agent capabilities`, `skills[${i}].bindTo[${bi}]`));
24361
+ const boundRefs = /* @__PURE__ */ new Set();
24362
+ for (const [bi, entry] of s.bindTo.entries()) {
24363
+ const ref = entry && typeof entry === "object" ? entry.capability : entry;
24364
+ if (boundRefs.has(ref)) {
24365
+ result.errors.push(createIssue("error", "SKILL_BIND_DUPLICATE_CAPABILITY", `Skill "${id ?? ""}" binds to capability "${ref}" more than once; list each capability once with the version it should load`, `skills[${i}].bindTo[${bi}]`));
24366
+ continue;
24367
+ }
24368
+ boundRefs.add(ref);
24369
+ if (!capIds.has(ref)) result.errors.push(createIssue("error", "SKILL_BIND_CAPABILITY_NOT_FOUND", `Skill "${id ?? ""}" binds to capability "${ref}" which does not exist`, `skills[${i}].bindTo[${bi}]`));
24370
+ else if (!agentBackedCapIds.has(ref)) result.errors.push(createIssue("error", "SKILL_BIND_TARGET_NOT_AGENT", `Skill "${id ?? ""}" binds to capability "${ref}" which is not agent-backed; skills can only be bound to agent capabilities`, `skills[${i}].bindTo[${bi}]`));
24371
+ }
24104
24372
  }
24105
24373
  const capabilityRefs = s.capabilities && typeof s.capabilities === "object" ? s.capabilities.capabilityRefs : void 0;
24106
24374
  if (Array.isArray(capabilityRefs)) {
@@ -27170,18 +27438,18 @@ const VERIFY_NEXT = "Ask the user for the 6-digit code from their email, then ru
27170
27438
  function claimNext(email) {
27171
27439
  return email ? `Send the verification email with: runtype auth claim ${email}` : "Ask the user for their email address, then run: runtype auth claim <email>";
27172
27440
  }
27173
- function fail$3(payload) {
27441
+ function fail$4(payload) {
27174
27442
  console.error(JSON.stringify(payload, null, 2));
27175
27443
  process.exit(1);
27176
27444
  }
27177
27445
  function failFromError(error, fallbackNext) {
27178
- if (error instanceof AgentAuthError) fail$3({
27446
+ if (error instanceof AgentAuthError) fail$4({
27179
27447
  status: "error",
27180
27448
  code: error.code,
27181
27449
  error: error.message,
27182
27450
  ...error.hint || fallbackNext ? { next: error.hint || fallbackNext } : {}
27183
27451
  });
27184
- fail$3({
27452
+ fail$4({
27185
27453
  status: "error",
27186
27454
  error: error instanceof Error ? error.message : String(error),
27187
27455
  ...fallbackNext ? { next: fallbackNext } : {}
@@ -27214,7 +27482,7 @@ async function registerAction(options) {
27214
27482
  return;
27215
27483
  }
27216
27484
  }
27217
- if (hasCredentials && !existingClaim && !options.force) fail$3({
27485
+ if (hasCredentials && !existingClaim && !options.force) fail$4({
27218
27486
  status: "error",
27219
27487
  code: "already_authenticated",
27220
27488
  error: "Credentials are already stored for a claimed account.",
@@ -27266,7 +27534,7 @@ async function registerAction(options) {
27266
27534
  });
27267
27535
  return;
27268
27536
  } catch (error) {
27269
- if (error instanceof AgentAuthError) fail$3({
27537
+ if (error instanceof AgentAuthError) fail$4({
27270
27538
  ...base,
27271
27539
  status: "registered_claim_failed",
27272
27540
  code: error.code,
@@ -27286,7 +27554,7 @@ async function claimAction(email, options) {
27286
27554
  const apiUrl = options.apiUrl || claimState?.apiUrl || (await store.getCredentials())?.apiUrl || getApiUrl();
27287
27555
  if (!claimState) {
27288
27556
  const hasCredentials = await store.hasCredentials();
27289
- fail$3({
27557
+ fail$4({
27290
27558
  status: "error",
27291
27559
  code: "no_pending_registration",
27292
27560
  error: hasCredentials ? "Stored credentials are already claimed; there is nothing to claim." : "No registration found.",
@@ -27314,13 +27582,13 @@ async function verifyAction(code, options) {
27314
27582
  const store = new CredentialStore();
27315
27583
  const claimState = await store.getClaimState();
27316
27584
  const apiUrl = options.apiUrl || claimState?.apiUrl || (await store.getCredentials())?.apiUrl || getApiUrl();
27317
- if (!claimState) fail$3({
27585
+ if (!claimState) fail$4({
27318
27586
  status: "error",
27319
27587
  code: "no_pending_registration",
27320
27588
  error: "No claim in progress.",
27321
27589
  next: "Run: runtype auth register --email <email> first."
27322
27590
  });
27323
- if (!claimState.claimEmail) fail$3({
27591
+ if (!claimState.claimEmail) fail$4({
27324
27592
  status: "error",
27325
27593
  code: "claim_not_initiated",
27326
27594
  error: "No verification email has been sent yet, so there is no code to verify.",
@@ -27923,6 +28191,283 @@ function resolveJsonPayload(inline, fromFile, label) {
27923
28191
  return parseJsonObject$1(inline, label);
27924
28192
  }
27925
28193
  //#endregion
28194
+ //#region src/lib/ensure-output.ts
28195
+ /**
28196
+ * Emit a pulled definition as JSON: to `out` when given, otherwise to stdout so
28197
+ * `runtype <entity> pull <name> > file.json` works. Hash and warnings go to stderr.
28198
+ */
28199
+ function writePulledDefinition(data, meta) {
28200
+ const json = `${JSON.stringify(data, null, 2)}\n`;
28201
+ if (meta.out) {
28202
+ writeFileSync$1(meta.out, json);
28203
+ console.error(chalk.green(`Wrote ${meta.out}`));
28204
+ } else process.stdout.write(json);
28205
+ if (meta.contentHash) console.error(chalk.gray(`contentHash ${meta.contentHash}`));
28206
+ for (const warning of meta.warnings ?? []) console.error(chalk.yellow(`warning: ${warning}`));
28207
+ }
28208
+ //#endregion
28209
+ //#region src/commands/entity-ensure.ts
28210
+ function namespaceTransport(client) {
28211
+ const transport = {
28212
+ get: (path, params) => client.get(path, params),
28213
+ post: (path, body) => client.post(path, body)
28214
+ };
28215
+ return () => transport;
28216
+ }
28217
+ /** The SDK ensure options these flags ask for, shared by every entity. */
28218
+ function buildEntityEnsureOptions(flags, support) {
28219
+ if (flags.release !== void 0 && !["none", "publish"].includes(flags.release)) throw new Error("--release must be none or publish");
28220
+ const version = support.version && (flags.label || flags.notes) ? {
28221
+ ...flags.label ? { label: flags.label } : {},
28222
+ ...flags.notes ? { notes: flags.notes } : {}
28223
+ } : void 0;
28224
+ return {
28225
+ ...flags.dryRun ? { dryRun: true } : {},
28226
+ ...flags.expectNoChanges ? { expectNoChanges: true } : {},
28227
+ ...flags.overwriteManaged ? { onConflict: "overwrite" } : {},
28228
+ ...flags.expectedRemoteHash ? { expectedRemoteHash: flags.expectedRemoteHash } : {},
28229
+ ...support.release && flags.release ? { release: flags.release } : {},
28230
+ ...version ? { version } : {}
28231
+ };
28232
+ }
28233
+ const define = (fn, definition) => fn(definition);
28234
+ const SKILL_ENSURE_SPEC = {
28235
+ noun: "skill",
28236
+ idKey: "skillId",
28237
+ release: true,
28238
+ version: true,
28239
+ productScoped: false,
28240
+ ensure: async (t, def, options) => new SkillsNamespace(t).ensure(define(defineSkill, def), options),
28241
+ pull: (t, name) => new SkillsNamespace(t).pull(name)
28242
+ };
28243
+ const FLOW_ENSURE_SPEC = {
28244
+ noun: "flow",
28245
+ idKey: "flowId",
28246
+ release: true,
28247
+ version: true,
28248
+ productScoped: false,
28249
+ ensure: async (t, def, options) => new FlowsNamespace(t).ensure(define(defineFlow, def), options),
28250
+ pull: (t, name) => new FlowsNamespace(t).pull(name)
28251
+ };
28252
+ const TOOL_ENSURE_SPEC = {
28253
+ noun: "tool",
28254
+ idKey: "toolId",
28255
+ release: false,
28256
+ version: false,
28257
+ productScoped: false,
28258
+ ensure: async (t, def, options) => new ToolsNamespace(t).ensure(define(defineTool, def), options),
28259
+ pull: (t, name) => new ToolsNamespace(t).pull(name)
28260
+ };
28261
+ const PRODUCT_ENSURE_SPEC = {
28262
+ noun: "product",
28263
+ idKey: "productId",
28264
+ release: false,
28265
+ version: false,
28266
+ productScoped: false,
28267
+ ensure: async (t, def, options) => new ProductsNamespace(t).ensure(define(defineProduct, def), options),
28268
+ pull: (t, name) => new ProductsNamespace(t).pull(name)
28269
+ };
28270
+ const SURFACE_ENSURE_SPEC = {
28271
+ noun: "surface",
28272
+ idKey: "surfaceId",
28273
+ release: false,
28274
+ version: false,
28275
+ productScoped: true,
28276
+ ensure: async (t, def, options, productId) => new SurfacesNamespace(t).ensure(productId, define(defineSurface, def), options),
28277
+ pull: (t, name, productId) => new SurfacesNamespace(t).pull(productId, name)
28278
+ };
28279
+ function isDriftError(error) {
28280
+ return error instanceof Error && error.name.endsWith("DriftError") && "plan" in error;
28281
+ }
28282
+ function printEnsureOutcome(spec, name, result) {
28283
+ if (result.result === "plan") {
28284
+ console.log(chalk.cyan(`Plan for ${spec.noun} "${name}": ${result.changes ?? "n/a"}`));
28285
+ for (const key of result.changedKeys ?? []) console.log(` ${key}`);
28286
+ if (result.remoteHash) console.log(chalk.gray(`Apply this plan with --expected-remote-hash ${result.remoteHash}`));
28287
+ return;
28288
+ }
28289
+ const id = typeof result[spec.idKey] === "string" ? ` (${result[spec.idKey]})` : "";
28290
+ console.log(chalk.green(`${spec.noun[0].toUpperCase()}${spec.noun.slice(1)} "${name}" ${result.result}${id}`));
28291
+ if (result.versionId) console.log(chalk.gray(`Version ${result.versionId}`));
28292
+ }
28293
+ function fail$3(message, detail) {
28294
+ console.error(chalk.red(message));
28295
+ if (detail !== void 0) console.error(chalk.red(detail instanceof Error ? detail.message : String(detail)));
28296
+ process.exit(1);
28297
+ }
28298
+ /** `ensure <file>`: converge a definition file (a pull's output, or its `definition`). */
28299
+ function buildEntityEnsureCommand(spec) {
28300
+ const command = new Command("ensure").description(`Converge a ${spec.noun} definition from a JSON file (config-as-code)`).argument("<file>", `${spec.noun} definition JSON file, or - to read from stdin`);
28301
+ if (spec.productScoped) command.requiredOption("--product <productId>", "Product the surface belongs to");
28302
+ command.option("--dry-run", "Plan without writing").option("--expect-no-changes", "Plan and exit 1 on any drift (the CI gate)").option("--overwrite-managed", "Converge even when the last write came from the dashboard").option("--expected-remote-hash <hash>", "Apply only if the remote still hashes to this value (the remoteHash a dry run printed)");
28303
+ if (spec.release) command.option("--release <mode>", "none or publish");
28304
+ if (spec.version) command.option("--label <text>", "Version label recorded on the version this converge appends").option("--notes <text>", "Version notes recorded on the version this converge appends");
28305
+ command.option("--json", "Output as JSON").action(async (file, flags) => {
28306
+ let options;
28307
+ let definition;
28308
+ try {
28309
+ options = buildEntityEnsureOptions(flags, spec);
28310
+ definition = readJsonFile(file, {
28311
+ label: `${spec.noun} definition`,
28312
+ unwrapKey: "definition"
28313
+ });
28314
+ } catch (error) {
28315
+ fail$3(error instanceof Error ? error.message : String(error));
28316
+ }
28317
+ const name = typeof definition.name === "string" ? definition.name : "";
28318
+ const apiKey = await ensureAuth();
28319
+ if (!apiKey) return;
28320
+ const transport = namespaceTransport(createCliClient(apiKey));
28321
+ try {
28322
+ const result = await spec.ensure(transport, definition, options, flags.product);
28323
+ if (flags.json) printJson(result);
28324
+ else printEnsureOutcome(spec, name, result);
28325
+ } catch (error) {
28326
+ if (isDriftError(error)) {
28327
+ if (flags.json) printJson({
28328
+ drift: true,
28329
+ plan: error.plan
28330
+ });
28331
+ else console.error(chalk.yellow(error.message));
28332
+ process.exit(1);
28333
+ }
28334
+ fail$3(`Failed to converge the ${spec.noun}`, error);
28335
+ }
28336
+ });
28337
+ return command;
28338
+ }
28339
+ /** `pull <name>`: print the definition file `ensure` reads back (dashboard wins). */
28340
+ function buildEntityPullCommand(spec) {
28341
+ const command = new Command("pull").description(`Print a ${spec.noun} as the definition file \`ensure\` reads (dashboard wins)`).argument("<name>", `${spec.noun} name`);
28342
+ if (spec.productScoped) command.requiredOption("--product <productId>", "Product the surface belongs to");
28343
+ command.option("-o, --out <file>", "Write the definition file here instead of stdout").option("--json", "Print the full pull response (definition, contentHash, provenance)").action(async (name, flags) => {
28344
+ const apiKey = await ensureAuth();
28345
+ if (!apiKey) return;
28346
+ const transport = namespaceTransport(createCliClient(apiKey));
28347
+ try {
28348
+ const pulled = await spec.pull(transport, name, flags.product);
28349
+ writePulledDefinition(flags.json ? pulled : pulled.definition, {
28350
+ out: flags.out,
28351
+ contentHash: pulled.contentHash,
28352
+ warnings: pulled.warnings
28353
+ });
28354
+ } catch (error) {
28355
+ fail$3(`Failed to pull ${spec.noun} "${name}"`, error);
28356
+ }
28357
+ });
28358
+ return command;
28359
+ }
28360
+ function buildFpoEnsureOptions(flags) {
28361
+ if (flags.release !== void 0 && !["none", "publish"].includes(flags.release)) throw new Error("--release must be none or publish");
28362
+ return {
28363
+ ...flags.dryRun ? { dryRun: true } : {},
28364
+ ...flags.expectNoChanges ? { expectNoChanges: true } : {},
28365
+ ...flags.overwriteManaged ? { onConflict: "overwrite" } : {},
28366
+ ...flags.expectedRemoteHash ? { expectedRemoteHash: flags.expectedRemoteHash } : {},
28367
+ ...flags.prune ? { prune: true } : {},
28368
+ ...flags.hashProbe ? { hashProbe: true } : {},
28369
+ ...flags.release ? { release: flags.release } : {}
28370
+ };
28371
+ }
28372
+ /** `ensure-fpo <file>`: converge a whole Full Product Object. */
28373
+ const productEnsureFpoCommand = new Command("ensure-fpo").description("Converge a whole Full Product Object (product, capabilities, surfaces, ...) from a JSON file").argument("<file>", "FPO JSON file (a pull-fpo output, or its fpo), or - to read from stdin").option("--dry-run", "Plan without writing").option("--expect-no-changes", "Plan and exit 1 when any entity would change (the CI gate)").option("--overwrite-managed", "Converge over dashboard/API edits").option("--expected-remote-hash <hash>", "Apply only if the live product still hashes to this value (the remoteHash a dry run printed)").option("--prune", "Remove product-scoped capabilities and surfaces absent from the FPO").option("--release <mode>", "publish (default: converged agents and flows go live) or none (save drafts)").option("--hash-probe", "With --release none, skip the upload when the live product already matches (best for pulled FPOs)").option("--json", "Output as JSON").action(async (file, flags) => {
28374
+ let fpo;
28375
+ try {
28376
+ fpo = defineFpo(readJsonFile(file, {
28377
+ label: "FPO",
28378
+ unwrapKey: "fpo"
28379
+ }));
28380
+ } catch (error) {
28381
+ fail$3(error instanceof Error ? error.message : String(error));
28382
+ }
28383
+ const apiKey = await ensureAuth();
28384
+ if (!apiKey) return;
28385
+ const products = new ProductsNamespace(namespaceTransport(createCliClient(apiKey)));
28386
+ try {
28387
+ const result = await products.ensureFpo(fpo, buildFpoEnsureOptions(flags));
28388
+ if (flags.json) printJson(result);
28389
+ else {
28390
+ console.log(chalk.cyan(`Product FPO ${result.result}${result.productId ? ` (${result.productId})` : ""}`));
28391
+ for (const entity of result.entities) {
28392
+ const line = ` ${entity.kind}:${entity.name} ${entity.result}`;
28393
+ console.log(entity.result === "failed" ? chalk.red(`${line} — ${entity.error ?? ""}`) : line);
28394
+ }
28395
+ if (result.remoteHash) console.log(chalk.gray(`Apply this plan with --expected-remote-hash ${result.remoteHash}`));
28396
+ }
28397
+ if (result.hasFailures) process.exit(1);
28398
+ } catch (error) {
28399
+ if (isDriftError(error)) {
28400
+ if (flags.json) printJson({
28401
+ drift: true,
28402
+ plan: error.plan
28403
+ });
28404
+ else console.error(chalk.yellow(error.message));
28405
+ process.exit(1);
28406
+ }
28407
+ fail$3("Failed to converge the product FPO", error);
28408
+ }
28409
+ });
28410
+ /** Runtype Agent Skills live under `agents` because `runtype skills` installs coding-agent skills. */
28411
+ const agentSkillsCommand = new Command("skills").description("Runtype Agent Skills: context bundles deployed agents load (for coding-agent skills see `runtype skills`)");
28412
+ agentSkillsCommand.addCommand(buildEntityEnsureCommand(SKILL_ENSURE_SPEC));
28413
+ agentSkillsCommand.addCommand(buildEntityPullCommand(SKILL_ENSURE_SPEC));
28414
+ /** `pull-fpo <name>`: print the whole product as the FPO `ensure-fpo` reads. */
28415
+ const productPullFpoCommand = new Command("pull-fpo").description("Print a product graph as the FPO `ensure-fpo` reads (dashboard wins)").argument("<name>", "Product name").option("-o, --out <file>", "Write the FPO here instead of stdout").option("--json", "Print the full pull response (fpo, contentHash, warnings)").action(async (name, flags) => {
28416
+ const apiKey = await ensureAuth();
28417
+ if (!apiKey) return;
28418
+ const products = new ProductsNamespace(namespaceTransport(createCliClient(apiKey)));
28419
+ try {
28420
+ const pulled = await products.pullFpo(name);
28421
+ writePulledDefinition(flags.json ? pulled : pulled.fpo, {
28422
+ out: flags.out,
28423
+ contentHash: pulled.contentHash,
28424
+ warnings: pulled.warnings
28425
+ });
28426
+ } catch (error) {
28427
+ fail$3(`Failed to pull product "${name}"`, error);
28428
+ }
28429
+ });
28430
+ /** Converge a client token file through the SDK; it keys on `name` and never deletes or regenerates. */
28431
+ async function ensureClientTokenFromFile(client, definition, flags) {
28432
+ const getClient = (() => client);
28433
+ return new ClientTokensNamespace(getClient).ensure(definition, {
28434
+ ...flags.dryRun ? { dryRun: true } : {},
28435
+ ...flags.expectNoChanges ? { expectNoChanges: true } : {}
28436
+ });
28437
+ }
28438
+ /** `client-tokens ensure <file>`: the widget token step of the agent-plus-token workflow. */
28439
+ const clientTokenEnsureCommand = new Command("ensure").description("Converge a client token definition from a JSON file, keyed by its name").argument("<file>", "Client token definition JSON file, or - to read from stdin").option("--dry-run", "Plan without writing").option("--expect-no-changes", "Plan and exit 1 on any drift (the CI gate)").option("--json", "Output as JSON").action(async (file, flags) => {
28440
+ let definition;
28441
+ try {
28442
+ definition = readJsonFile(file, { label: "client token definition" });
28443
+ } catch (error) {
28444
+ fail$3(error instanceof Error ? error.message : String(error));
28445
+ }
28446
+ const apiKey = await ensureAuth();
28447
+ if (!apiKey) return;
28448
+ try {
28449
+ const result = await ensureClientTokenFromFile(createCliClient(apiKey), definition, flags);
28450
+ if (flags.json) printJson(result);
28451
+ else if (result.result === "plan") {
28452
+ console.log(chalk.cyan(`Plan for client token "${String(definition.name)}": ${result.changes}`));
28453
+ for (const key of result.changedKeys) console.log(` ${key}`);
28454
+ } else {
28455
+ console.log(chalk.green(`Client token "${result.clientToken.name}" ${result.result} (${result.clientToken.id})`));
28456
+ if (result.token) console.log(`Token ${result.token}`);
28457
+ }
28458
+ } catch (error) {
28459
+ if (isDriftError(error)) {
28460
+ if (flags.json) printJson({
28461
+ drift: true,
28462
+ plan: error.plan
28463
+ });
28464
+ else console.error(chalk.yellow(error.message));
28465
+ process.exit(1);
28466
+ }
28467
+ fail$3("Failed to converge the client token", error);
28468
+ }
28469
+ });
28470
+ //#endregion
27926
28471
  //#region src/lib/execution-input.ts
27927
28472
  function addExecutionInputOptions(command) {
27928
28473
  return command.option("--inputs <json>", "Typed input variables as a JSON object").option("--inputs-file <file|->", "Read input variables from a JSON object file or stdin").option("-v, --variable <key=value>", "Set a string variable (repeatable)", collectVariables, []).option("--messages-file <file|->", "Read structured messages from a JSON array file or stdin");
@@ -28248,6 +28793,8 @@ function flowsListParams(options) {
28248
28793
  };
28249
28794
  }
28250
28795
  const flowsCommand = new Command("flows").description("Manage flows");
28796
+ flowsCommand.addCommand(buildEntityEnsureCommand(FLOW_ENSURE_SPEC));
28797
+ flowsCommand.addCommand(buildEntityPullCommand(FLOW_ENSURE_SPEC));
28251
28798
  flowsCommand.command("list").description("List all flows").option("--limit <n>", "Limit results (defaults to the API page size)").option("--cursor <cursor>", "Pagination cursor for the next page").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
28252
28799
  const apiKey = await ensureAuth();
28253
28800
  if (!apiKey) return;
@@ -31461,6 +32008,8 @@ function formatToolMetadata(metadata) {
31461
32008
  return parts.join("; ");
31462
32009
  }
31463
32010
  const toolsCommand = new Command("tools").description("Manage runtime tools (saved tools)");
32011
+ toolsCommand.addCommand(buildEntityEnsureCommand(TOOL_ENSURE_SPEC));
32012
+ toolsCommand.addCommand(buildEntityPullCommand(TOOL_ENSURE_SPEC));
31464
32013
  toolsCommand.command("list").description("List your tools").option("--limit <n>", "Limit results (defaults to the API page size)").option("--cursor <cursor>", "Pagination cursor for the next page").option("--json", "Output as JSON").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
31465
32014
  const apiKey = await ensureAuth();
31466
32015
  if (!apiKey) return;
@@ -32255,6 +32804,8 @@ function parsePositiveInteger$1(value, option) {
32255
32804
  return parsed;
32256
32805
  }
32257
32806
  const surfacesCommand = new Command("surfaces").description("View product surfaces");
32807
+ surfacesCommand.addCommand(buildEntityEnsureCommand(SURFACE_ENSURE_SPEC));
32808
+ surfacesCommand.addCommand(buildEntityPullCommand(SURFACE_ENSURE_SPEC));
32258
32809
  surfacesCommand.command("list").description("List surfaces across your products").option("--json", "Output as JSON").option("--limit <n>", "Maximum number of surfaces to return").option("--cursor <cursor>", "Pagination cursor for the next page").option("--type <type>", "Filter by surface type").option("--status <status>", "Filter by surface status").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
32259
32810
  const apiKey = await ensureAuth();
32260
32811
  if (!apiKey) return;
@@ -32928,6 +33479,10 @@ async function validateUrl(url, apiUrl) {
32928
33479
  return validateQuickStartUrl(url, apiUrl);
32929
33480
  }
32930
33481
  const productsCommand = new Command("products").description("Manage products");
33482
+ productsCommand.addCommand(buildEntityEnsureCommand(PRODUCT_ENSURE_SPEC));
33483
+ productsCommand.addCommand(buildEntityPullCommand(PRODUCT_ENSURE_SPEC));
33484
+ productsCommand.addCommand(productEnsureFpoCommand);
33485
+ productsCommand.addCommand(productPullFpoCommand);
32931
33486
  productsCommand.command("init").description("Initialize a product from an external resource").option("--from <url>", "URL to validate and import").option("--name <name>", "Custom product name (defaults to agent name)").option("--open", "Open browser to dashboard instead of creating via API").option("--json", "Output as JSON").option("--api-url <url>", "Override API URL").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
32932
33487
  const isJsonMode = !!options.json;
32933
33488
  if (!isTTY(options) || isJsonMode) {
@@ -46658,28 +47213,113 @@ agentDeploymentsCommand.command("list <agentId>").description("List deployment r
46658
47213
  * The activation the flags ask for. `release` and `deploy` are two spellings of
46659
47214
  * one thing, so supplying both is refused before any request is sent.
46660
47215
  */
46661
- function buildEnsureActivation(flags) {
47216
+ function buildEnsureActivation(flags, bindings) {
46662
47217
  if (flags.release !== void 0 && flags.deploy !== void 0) throw new Error(ENSURE_RELEASE_DEPLOY_CONFLICT_MESSAGE);
46663
- if (flags.deploy !== void 0) return { deploy: { alias: flags.deploy } };
47218
+ if (bindings !== void 0 && flags.deploy === void 0) throw new Error("--bindings-file sets per-alias secret bindings, so it needs --deploy <alias>.");
47219
+ if (flags.deploy !== void 0) return { deploy: {
47220
+ alias: flags.deploy,
47221
+ ...bindings !== void 0 ? { bindings } : {}
47222
+ } };
46664
47223
  if (flags.release !== void 0) return { release: flags.release };
46665
47224
  return {};
46666
47225
  }
46667
- const agentEnsureCommand = new Command("ensure").description("Converge an agent definition from a JSON file, optionally deploying it").argument("<file>", "Agent definition JSON file, or - to read from stdin").option("--deploy <alias>", "Activate the converged version at this release alias").option("--release <mode>", "Compatibility spelling of deploy: none or publish").option("--dry-run", "Plan without writing").option("--expect-no-changes", "Plan and exit 1 on any drift (the CI gate)").option("--overwrite-managed", "Converge even when the last write came from the dashboard").option("--label <text>", "Version label recorded on the version this converge appends").option("--notes <text>", "Version notes recorded on the version this converge appends").option("--json", "Output as JSON").action(async (file, options) => {
47226
+ /** The API refuses expectedRemoteHash on a preview deploy, since it guards live-row writes only. */
47227
+ function acceptsExpectedRemoteHash(deploy) {
47228
+ return deploy === void 0 || deploy === "live";
47229
+ }
47230
+ /**
47231
+ * Parse a `--bindings-file` object of `{ SECRET_NAME: value }`. The whole object
47232
+ * replaces the alias's bindings, so `{}` clears them.
47233
+ */
47234
+ function parseAliasBindings(file) {
47235
+ const bindings = {};
47236
+ for (const [name, value] of Object.entries(file)) {
47237
+ if (typeof value !== "string") throw new Error(`Binding "${name}" must be a string secret value.`);
47238
+ bindings[name] = value;
47239
+ }
47240
+ return bindings;
47241
+ }
47242
+ /** The file `agents ensure` reads back: the definition with its skills and capabilities at the top level. */
47243
+ function agentPullToDefinitionFile(pulled) {
47244
+ return {
47245
+ ...pulled.definition,
47246
+ ...pulled.skills !== void 0 ? { skills: pulled.skills } : {},
47247
+ ...pulled.capabilities !== void 0 ? { capabilities: pulled.capabilities } : {}
47248
+ };
47249
+ }
47250
+ /**
47251
+ * Lift a definition file's top-level `skills` (the agent's complete binding set)
47252
+ * out of the definition, which ensure hashes, and into the request.
47253
+ */
47254
+ function splitDefinitionSkills(file) {
47255
+ if (!("skills" in file)) return { definition: file };
47256
+ const { skills, ...definition } = file;
47257
+ if (!Array.isArray(skills)) throw new Error("\"skills\" must be an array of { name, version?, enabled? } bindings.");
47258
+ return {
47259
+ definition,
47260
+ skills
47261
+ };
47262
+ }
47263
+ /**
47264
+ * Lift a definition file's top-level `skills` and `capabilities` (each the agent's
47265
+ * complete set) out of the definition and into the request.
47266
+ */
47267
+ function splitDefinitionAgentState(file) {
47268
+ const { definition: withCapabilities, skills } = splitDefinitionSkills(file);
47269
+ if (!("capabilities" in withCapabilities)) return {
47270
+ definition: withCapabilities,
47271
+ ...skills !== void 0 ? { skills } : {}
47272
+ };
47273
+ const { capabilities, ...definition } = withCapabilities;
47274
+ if (!Array.isArray(capabilities)) throw new Error("\"capabilities\" must be an array of { toolName, flow | agent | tool, description?, parametersSchema?, enabled? } entries.");
47275
+ return {
47276
+ definition,
47277
+ ...skills !== void 0 ? { skills } : {},
47278
+ capabilities
47279
+ };
47280
+ }
47281
+ function formatCapabilityChange(change) {
47282
+ const target = change.kind && change.source ? ` (${change.kind}:${change.source})` : "";
47283
+ return ` capability:${change.toolName} ${change.action}${target}`;
47284
+ }
47285
+ /**
47286
+ * Whether a dry-run plan drifts from the definition: content, skill bindings, capabilities, or,
47287
+ * when the flags ask for a deploy, the release pointer or its binding names (#9371).
47288
+ */
47289
+ function ensurePlanHasDrift(result, activation) {
47290
+ if (result.result !== "plan") return false;
47291
+ if (result.changes !== "none") return true;
47292
+ if ((result.skillBindings ?? []).some((b) => b.action !== "unchanged")) return true;
47293
+ if ((result.capabilityChanges ?? []).some((c) => c.action !== "unchanged")) return true;
47294
+ return (activation.deploy !== void 0 || activation.release === "publish") && (result.deployment?.changed === true || result.deployment?.bindingsChanged === true);
47295
+ }
47296
+ function formatSkillBinding(binding) {
47297
+ const pin = binding.version === null ? "published" : `v${binding.version}`;
47298
+ return ` skill:${binding.name} ${binding.action} (${pin})`;
47299
+ }
47300
+ const agentEnsureCommand = new Command("ensure").description("Converge an agent definition from a JSON file, optionally deploying it").argument("<file>", "Agent definition JSON file, or - to read from stdin").option("--deploy <alias>", "Activate the converged version at this release alias").option("--release <mode>", "Compatibility spelling of deploy: none or publish").option("--dry-run", "Plan without writing").option("--expect-no-changes", "Plan and exit 1 on any drift (the CI gate)").option("--overwrite-managed", "Converge even when the last write came from the dashboard").option("--label <text>", "Version label recorded on the version this converge appends").option("--notes <text>", "Version notes recorded on the version this converge appends").option("--expected-remote-hash <hash>", "Apply only if the remote still hashes to this value (the remoteHash a dry run printed)").option("--bindings-file <file>", "JSON object of { SECRET_NAME: value } bound to the --deploy alias (replaces the whole set)").option("--json", "Output as JSON").action(async (file, options) => {
46668
47301
  if (options.release !== void 0 && !["none", "publish"].includes(options.release)) {
46669
47302
  console.error(chalk.red("--release must be none or publish"));
46670
47303
  process.exit(1);
46671
47304
  }
46672
47305
  let activation;
46673
47306
  try {
46674
- activation = buildEnsureActivation(options);
47307
+ activation = buildEnsureActivation(options, options.bindingsFile !== void 0 ? parseAliasBindings(readJsonFile(options.bindingsFile, { label: "alias bindings" })) : void 0);
46675
47308
  } catch (error) {
46676
47309
  console.error(chalk.red(error instanceof Error ? error.message : String(error)));
46677
47310
  process.exit(1);
46678
47311
  }
46679
- const definition = readJsonFile(file, {
46680
- label: "agent definition",
46681
- unwrapKey: "agent"
46682
- });
47312
+ let parsed;
47313
+ try {
47314
+ parsed = splitDefinitionAgentState(readJsonFile(file, {
47315
+ label: "agent definition",
47316
+ unwrapKey: "agent"
47317
+ }));
47318
+ } catch (error) {
47319
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
47320
+ process.exit(1);
47321
+ }
47322
+ const { definition, skills, capabilities } = parsed;
46683
47323
  const name = definition.name;
46684
47324
  if (typeof name !== "string" || name.length === 0) {
46685
47325
  console.error(chalk.red("The agent definition needs a non-empty \"name\"."));
@@ -46700,22 +47340,35 @@ const agentEnsureCommand = new Command("ensure").description("Converge an agent
46700
47340
  ...dryRun ? { dryRun: true } : {},
46701
47341
  ...options.overwriteManaged ? { onConflict: "overwrite" } : {},
46702
47342
  ...version ? { version } : {},
47343
+ ...options.expectedRemoteHash ? { expectedRemoteHash: options.expectedRemoteHash } : {},
47344
+ ...skills !== void 0 ? { skills } : {},
47345
+ ...capabilities !== void 0 ? { capabilities } : {},
46703
47346
  ...activation
46704
47347
  });
46705
47348
  if (options.json) printJson(result);
46706
47349
  else if (result.result === "plan") {
46707
47350
  console.log(chalk.cyan(`Plan for "${name}": ${result.changes}`));
46708
47351
  for (const key of result.changedKeys ?? []) console.log(` ${key}`);
47352
+ if (result.remoteHash && acceptsExpectedRemoteHash(options.deploy)) console.log(chalk.gray(`Apply this plan with --expected-remote-hash ${result.remoteHash}`));
46709
47353
  } else {
46710
47354
  console.log(chalk.green(`Agent "${name}" ${result.result} (${result.agentId})`));
46711
47355
  if (result.versionId) console.log(chalk.gray(`Version ${result.versionId}`));
46712
47356
  }
47357
+ const skillChanges = (result.skillBindings ?? []).filter((b) => b.action !== "unchanged");
47358
+ const capabilityChanges = (result.capabilityChanges ?? []).filter((change) => change.action !== "unchanged");
47359
+ if (!options.json) {
47360
+ for (const binding of skillChanges) console.log(formatSkillBinding(binding));
47361
+ for (const change of capabilityChanges) console.log(formatCapabilityChange(change));
47362
+ }
46713
47363
  if (result.deployment && !options.json) {
46714
- const { alias, versionId, revision, changed } = result.deployment;
46715
- console.log(chalk.green(`${changed ? "Deployed" : "Already deployed"} ${versionId} at "${alias}" (revision ${revision})`));
47364
+ const { alias, versionId, revision, changed, bindingsChanged } = result.deployment;
47365
+ const planned = result.result === "plan";
47366
+ const verb = changed ? planned ? "Would deploy over" : "Deployed" : "Already deployed";
47367
+ console.log(chalk.green(`${verb} ${versionId} at "${alias}" (revision ${revision})`));
47368
+ if (planned && bindingsChanged) console.log(` deploy:${alias} (bindings)`);
46716
47369
  if (alias !== "live") console.log(chalk.gray("Live unchanged."));
46717
47370
  }
46718
- if (options.expectNoChanges && result.result === "plan" && result.changes !== "none") process.exit(1);
47371
+ if (options.expectNoChanges && ensurePlanHasDrift(result, activation)) process.exit(1);
46719
47372
  } catch (error) {
46720
47373
  const message = error instanceof Error ? error.message : String(error);
46721
47374
  console.error(chalk.red("Failed to converge the agent"));
@@ -46723,6 +47376,23 @@ const agentEnsureCommand = new Command("ensure").description("Converge an agent
46723
47376
  process.exit(1);
46724
47377
  }
46725
47378
  });
47379
+ const agentPullCommand = new Command("pull").description("Print an agent as the definition file `agents ensure` reads (dashboard wins)").argument("<name>", "Agent name").option("-o, --out <file>", "Write the definition file here instead of stdout").option("--json", "Print the full pull response (definition, contentHash, provenance)").action(async (name, options) => {
47380
+ const apiKey = await ensureAuth();
47381
+ if (!apiKey) return;
47382
+ const client = createCliClient(apiKey);
47383
+ try {
47384
+ const pulled = await client.get("/agents/pull", { name });
47385
+ writePulledDefinition(options.json ? pulled : agentPullToDefinitionFile(pulled), {
47386
+ out: options.out,
47387
+ contentHash: pulled.contentHash,
47388
+ warnings: pulled.warnings
47389
+ });
47390
+ } catch (error) {
47391
+ console.error(chalk.red(`Failed to pull agent "${name}"`));
47392
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
47393
+ process.exit(1);
47394
+ }
47395
+ });
46726
47396
  //#endregion
46727
47397
  //#region src/commands/agent-promote.ts
46728
47398
  const DEFAULT_SOURCE_KEY_ENV = "RUNTYPE_SOURCE_API_KEY";
@@ -48225,6 +48895,8 @@ agentsCommand.addCommand(taskCommand);
48225
48895
  agentsCommand.addCommand(agentAliasesCommand);
48226
48896
  agentsCommand.addCommand(agentDeploymentsCommand);
48227
48897
  agentsCommand.addCommand(agentEnsureCommand);
48898
+ agentsCommand.addCommand(agentPullCommand);
48899
+ agentsCommand.addCommand(agentSkillsCommand);
48228
48900
  agentsCommand.addCommand(agentPromoteCommand);
48229
48901
  agentsCommand.addCommand(agentExportRuntimeCommand);
48230
48902
  agentsCommand.command("list").description("List all agents").option("--json", "Output as JSON").option("--limit <n>", "Limit results", "20").option("--cursor <cursor>", "Pagination cursor for the next page").option("--tty", "Force TTY mode").option("--no-tty", "Force non-TTY mode").action(async (options) => {
@@ -49893,7 +50565,7 @@ function toJUnitSuite(outcome) {
49893
50565
  }))
49894
50566
  };
49895
50567
  }
49896
- evalCommand.command("run [idOrDirPrefix]").description("Run code-colocated eval suites (**/*.eval.ts) as a CI gate (exit 0 pass / 1 fail / 2 config)").option("--strict", "Fail the exit code on soft grader misses too (default: soft misses are reported but do not fail)").option("--virtual", "Run inline without persisting a suite/batch to the dashboard").option("--junit <path>", "Write JUnit XML results to <path>").option("--url <api>", "Override the API base URL (e.g. staging)").option("--cwd <dir>", "Directory to discover *.eval.ts under (default: current directory)").option("--alias <name>", "Run an Agent-target suite against whichever version this release alias points at").option("--version-id <id>", "Run an Agent-target suite against this exact version snapshot").action(async (idOrDirPrefix, options) => {
50568
+ evalCommand.command("run [idOrDirPrefix]").description("Run code-colocated eval suites (**/*.eval.ts) as a CI gate (exit 0 pass / 1 fail / 2 config)").option("--strict", "Fail the exit code on soft grader misses too (default: soft misses are reported but do not fail)").option("--virtual", "Run inline without persisting a suite/batch to the dashboard").option("--junit <path>", "Write JUnit XML results to <path>").option("--url <api>", "Override the API base URL (e.g. staging)").option("--cwd <dir>", "Directory to discover *.eval.ts under (default: current directory)").option("--alias <name>", "Run an Agent-target suite against whichever version this release alias points at").option("--version-id <id>", "Run an Agent-target suite against this exact version snapshot").option("--overwrite-managed", "Converge each suite even when its last write came from the dashboard or API (replaces edited ensure-owned cases)").action(async (idOrDirPrefix, options) => {
49897
50569
  const apiKey = await ensureAuth();
49898
50570
  if (!apiKey) failConfig("Not authenticated. Set RUNTYPE_API_KEY or run `runtype login`.");
49899
50571
  let agentSelector = {};
@@ -49938,7 +50610,8 @@ evalCommand.command("run [idOrDirPrefix]").description("Run code-colocated eval
49938
50610
  else {
49939
50611
  const ensured = await client.post("/eval/ensure", {
49940
50612
  name: def.name,
49941
- definition: def
50613
+ definition: def,
50614
+ ...options.overwriteManaged ? { onConflict: "overwrite" } : {}
49942
50615
  });
49943
50616
  if (!ensured.suiteId) throw new Error(`ensure did not return a suiteId (result: ${ensured.result})`);
49944
50617
  result = await client.post("/eval/run", {
@@ -51509,6 +52182,7 @@ clientTokensCommand.command("regenerate <id>").description("Regenerate a client
51509
52182
  const { waitUntilExit } = render(React.createElement(App));
51510
52183
  await waitUntilExit();
51511
52184
  });
52185
+ clientTokensCommand.addCommand(clientTokenEnsureCommand);
51512
52186
  //#endregion
51513
52187
  //#region ../../node_modules/.pnpm/@runtypelabs+persona@4.24.0/node_modules/@runtypelabs/persona/dist/codegen.js
51514
52188
  var j = "4.24.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/cli",
3
- "version": "2.44.1",
3
+ "version": "2.45.0",
4
4
  "description": "Command-line interface for Runtype AI platform",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,7 @@
11
11
  "@modelcontextprotocol/client": "2.0.0",
12
12
  "@modelcontextprotocol/core": "2.0.0",
13
13
  "@runtypelabs/ink-components": "0.3.9",
14
- "@runtypelabs/sdk": "10.8.1",
14
+ "@runtypelabs/sdk": "10.9.0",
15
15
  "@runtypelabs/terminal-animations": "0.2.4",
16
16
  "add-mcp": "2.0.0",
17
17
  "chalk": "^5.3.0",
@@ -33,7 +33,7 @@
33
33
  "devDependencies": {
34
34
  "@modelcontextprotocol/server": "2.0.0",
35
35
  "@runtypelabs/persona": "4.24.0",
36
- "@runtypelabs/shared": "3.68.2",
36
+ "@runtypelabs/shared": "3.69.0",
37
37
  "@types/express": "^5.0.6",
38
38
  "@types/micromatch": "^4.0.9",
39
39
  "@types/node": "^25.3.3",