@north-light/crouter 0.3.164 → 0.3.166

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 (160) hide show
  1. package/dist/build-root.d.ts +6 -6
  2. package/dist/build-root.js +12 -13
  3. package/dist/builtin-memory/00-runtime-base.md +3 -4
  4. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  5. package/dist/builtin-memory/02-lifecycle/00-terminal.md +2 -0
  6. package/dist/builtin-memory/04-orchestration-kernel.md +16 -18
  7. package/dist/builtin-memory/05-kinds/advisor/00-base.md +0 -2
  8. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -9
  9. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -3
  10. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +2 -10
  11. package/dist/builtin-memory/05-kinds/explore/00-base.md +2 -2
  12. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +0 -2
  13. package/dist/builtin-memory/05-kinds/general/00-base.md +3 -5
  14. package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -4
  15. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -3
  16. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +12 -0
  17. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +0 -2
  18. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +0 -2
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +0 -2
  20. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -1
  21. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +0 -2
  22. package/dist/builtin-memory/05-kinds/review/00-base.md +0 -2
  23. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -3
  24. package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
  25. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +2 -4
  26. package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -1
  27. package/dist/builtin-memory/advisor/council.md +40 -0
  28. package/dist/builtin-memory/design.md +8 -11
  29. package/dist/builtin-memory/development.md +6 -9
  30. package/dist/builtin-memory/internal/INDEX.md +4 -5
  31. package/dist/builtin-memory/internal/agent-shaping.md +5 -7
  32. package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -3
  33. package/dist/builtin-memory/internal/marketplaces.md +13 -14
  34. package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -4
  35. package/dist/builtin-memory/internal/plugins.md +61 -45
  36. package/dist/builtin-memory/planning.md +4 -7
  37. package/dist/builtin-memory/spec.md +9 -12
  38. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +8 -22
  39. package/dist/cli.js +1 -1
  40. package/dist/clients/attach/__tests__/attach-keybindings.test.js +17 -4
  41. package/dist/clients/attach/__tests__/context-message.test.js +62 -22
  42. package/dist/clients/attach/__tests__/crtr-output.test.js +39 -30
  43. package/dist/clients/attach/render/chat-view.d.ts +20 -20
  44. package/dist/clients/attach/render/chat-view.js +40 -54
  45. package/dist/clients/attach/render/{frozen-history.d.ts → condensed-history.d.ts} +1 -18
  46. package/dist/clients/attach/render/condensed-history.js +60 -0
  47. package/dist/clients/attach/render/context-message.d.ts +1 -1
  48. package/dist/clients/attach/render/context-message.js +18 -41
  49. package/dist/clients/attach/session/bindings.d.ts +5 -3
  50. package/dist/clients/attach/session/bindings.js +7 -13
  51. package/dist/clients/attach/session/keys.d.ts +2 -0
  52. package/dist/clients/attach/session/keys.js +36 -37
  53. package/dist/clients/attach/session/profile-files.d.ts +8 -0
  54. package/dist/clients/attach/session/profile-files.js +157 -0
  55. package/dist/clients/attach/viewer.js +528 -529
  56. package/dist/commands/node-context.js +3 -2
  57. package/dist/commands/node.js +2 -2
  58. package/dist/commands/pkg/plugin-inspect.js +6 -7
  59. package/dist/commands/pkg/plugin-manage.d.ts +1 -1
  60. package/dist/commands/pkg/plugin-manage.js +131 -19
  61. package/dist/commands/pkg/plugin.js +2 -2
  62. package/dist/commands/pkg.js +6 -11
  63. package/dist/commands/profile/env.js +3 -3
  64. package/dist/commands/sys/config.js +17 -76
  65. package/dist/commands/sys/doctor.js +5 -91
  66. package/dist/commands/sys/setup-wizard.d.ts +9 -11
  67. package/dist/commands/sys/setup-wizard.js +47 -81
  68. package/dist/core/__tests__/base-worker-prompt.test.js +18 -21
  69. package/dist/core/__tests__/command-plugins-surfaces.test.js +39 -5
  70. package/dist/core/__tests__/command-plugins.test.js +75 -16
  71. package/dist/core/__tests__/review-model-floor.test.js +2 -2
  72. package/dist/core/__tests__/tmux-surface.test.js +10 -1
  73. package/dist/core/command-manifests/manifest.d.ts +24 -0
  74. package/dist/core/{configured-clis → command-manifests}/manifest.js +28 -25
  75. package/dist/core/command-manifests/registry.d.ts +1 -2
  76. package/dist/core/command-manifests/registry.js +2 -2
  77. package/dist/core/command-manifests/schema.d.ts +11 -13
  78. package/dist/core/command-manifests/schema.js +53 -192
  79. package/dist/core/command-plugins/compose.d.ts +0 -6
  80. package/dist/core/command-plugins/compose.js +32 -75
  81. package/dist/core/command-plugins/discovery.d.ts +25 -58
  82. package/dist/core/command-plugins/discovery.js +171 -261
  83. package/dist/core/command-plugins/endpoint.d.ts +24 -0
  84. package/dist/core/command-plugins/endpoint.js +48 -0
  85. package/dist/core/command-plugins/store.d.ts +16 -0
  86. package/dist/core/command-plugins/store.js +64 -0
  87. package/dist/core/command-plugins/{adapter.d.ts → transport/exec-invoke.d.ts} +3 -3
  88. package/dist/core/command-plugins/{adapter.js → transport/exec-invoke.js} +4 -4
  89. package/dist/core/{configured-clis/fetch.d.ts → command-plugins/transport/http-fetch.d.ts} +9 -18
  90. package/dist/core/{configured-clis/fetch.js → command-plugins/transport/http-fetch.js} +15 -35
  91. package/dist/core/{configured-clis/invoker.d.ts → command-plugins/transport/http-invoke.d.ts} +7 -7
  92. package/dist/core/{configured-clis/invoker.js → command-plugins/transport/http-invoke.js} +21 -23
  93. package/dist/core/command.d.ts +8 -9
  94. package/dist/core/command.js +6 -9
  95. package/dist/core/config.js +6 -10
  96. package/dist/core/env-name.d.ts +6 -0
  97. package/dist/core/env-name.js +9 -0
  98. package/dist/core/io.d.ts +1 -1
  99. package/dist/core/keybindings/__tests__/resolve.test.js +40 -3
  100. package/dist/core/keybindings/attach-control.d.ts +37 -0
  101. package/dist/core/keybindings/attach-control.js +38 -0
  102. package/dist/core/keybindings/catalog.d.ts +5 -4
  103. package/dist/core/keybindings/catalog.js +16 -8
  104. package/dist/core/keybindings/index.d.ts +1 -0
  105. package/dist/core/keybindings/index.js +1 -0
  106. package/dist/core/keybindings/types.d.ts +1 -1
  107. package/dist/core/preview-registry.js +41 -74
  108. package/dist/core/runtime/bearings.d.ts +2 -5
  109. package/dist/core/runtime/bearings.js +2 -5
  110. package/dist/core/runtime/broker.js +3 -2
  111. package/dist/core/runtime/front-door.d.ts +1 -1
  112. package/dist/core/runtime/front-door.js +2 -2
  113. package/dist/core/runtime/kickoff.d.ts +3 -3
  114. package/dist/core/runtime/kickoff.js +4 -3
  115. package/dist/core/runtime/lifecycle.js +2 -2
  116. package/dist/core/runtime/situational-context.d.ts +1 -1
  117. package/dist/core/runtime/situational-context.js +1 -1
  118. package/dist/core/runtime/spawn.js +9 -9
  119. package/dist/core/runtime/tmux.js +29 -2
  120. package/dist/core/scope.d.ts +0 -5
  121. package/dist/core/scope.js +0 -10
  122. package/dist/core/user-settings.d.ts +193 -0
  123. package/dist/core/user-settings.js +252 -0
  124. package/dist/index.d.ts +4 -0
  125. package/dist/index.js +3 -0
  126. package/dist/pi-extensions/canvas-stophook.js +3 -2
  127. package/dist/prompts/review.js +4 -2
  128. package/dist/shared/generated-context.d.ts +34 -0
  129. package/dist/shared/generated-context.js +98 -0
  130. package/dist/types.d.ts +23 -16
  131. package/dist/types.js +3 -14
  132. package/dist/web-client/assets/index-BgLGlZ3D.css +2 -0
  133. package/dist/web-client/assets/{index-NIuSCOHM.js → index-CmoNqcCv.js} +19 -19
  134. package/dist/web-client/index.html +2 -2
  135. package/dist/web-client/sw.js +1 -1
  136. package/docs/public-api.md +1 -0
  137. package/package.json +1 -1
  138. package/runtime.lock.json +2 -2
  139. package/dist/builtin-memory/05-kinds/product/00-base.md +0 -25
  140. package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +0 -15
  141. package/dist/builtin-memory/05-kinds/product/teardown.md +0 -15
  142. package/dist/builtin-memory/internal/workflow-codification.md +0 -82
  143. package/dist/builtin-memory/product.md +0 -80
  144. package/dist/clients/attach/render/frozen-history.js +0 -100
  145. package/dist/commands/pkg/cli-inspect.d.ts +0 -17
  146. package/dist/commands/pkg/cli-inspect.js +0 -190
  147. package/dist/commands/pkg/cli-manage.d.ts +0 -3
  148. package/dist/commands/pkg/cli-manage.js +0 -206
  149. package/dist/commands/pkg/cli.d.ts +0 -1
  150. package/dist/commands/pkg/cli.js +0 -14
  151. package/dist/core/configured-clis/cache.d.ts +0 -16
  152. package/dist/core/configured-clis/cache.js +0 -57
  153. package/dist/core/configured-clis/compose.d.ts +0 -14
  154. package/dist/core/configured-clis/compose.js +0 -60
  155. package/dist/core/configured-clis/discovery.d.ts +0 -47
  156. package/dist/core/configured-clis/discovery.js +0 -173
  157. package/dist/core/configured-clis/manifest.d.ts +0 -24
  158. package/dist/core/configured-clis/registration.d.ts +0 -40
  159. package/dist/core/configured-clis/registration.js +0 -201
  160. package/dist/web-client/assets/index-CqLKj8Xu.css +0 -2
@@ -1,5 +1,5 @@
1
- import type { InstalledPlugin } from '../../types.js';
2
- import type { Field } from '../help.js';
1
+ import type { InstalledPlugin } from '../../../types.js';
2
+ import type { Field } from '../../help.js';
3
3
  export interface ExternalLeafSpec {
4
4
  plugin: InstalledPlugin;
5
5
  /** Absolute, resolved executable path. */
@@ -11,7 +11,7 @@ export interface ExternalLeafSpec {
11
11
  }
12
12
  /** Validates a result object against declared output field descriptors.
13
13
  * Returns null if valid; otherwise returns a { field, receivedType } error.
14
- * Transport-neutral: used by both command-plugin and configured-CLI adapters. */
14
+ * Transport-neutral: used by exec and HTTP transport adapters. */
15
15
  export declare function validateDeclaredResult(output: Field[], result: Record<string, unknown>): {
16
16
  field: string;
17
17
  receivedType?: string;
@@ -8,9 +8,9 @@
8
8
  // environment and owns its own time budget (no crtr timeout) — this is a trust
9
9
  // boundary, not a sandbox (installed code can already read the caller's files).
10
10
  import { spawnSync } from 'node:child_process';
11
- import { CrtrError } from '../errors.js';
12
- import { diag } from '../io.js';
13
- import { ExitCode } from '../../types.js';
11
+ import { CrtrError } from '../../errors.js';
12
+ import { diag } from '../../io.js';
13
+ import { ExitCode } from '../../../types.js';
14
14
  const MAX_STDOUT = 10 * 1024 * 1024; // 10 MiB hard cap on plugin stdout.
15
15
  const RESERVED_CODES = new Set(['internal', 'unknown_path', 'command_collision', 'plugin_protocol_error']);
16
16
  // Strict snake_case: no leading/trailing underscore, no consecutive underscores.
@@ -18,7 +18,7 @@ const SNAKE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
18
18
  const NEXT = 'Run `crtr pkg plugin show` or `crtr sys doctor` to inspect the plugin.';
19
19
  /** Validates a result object against declared output field descriptors.
20
20
  * Returns null if valid; otherwise returns a { field, receivedType } error.
21
- * Transport-neutral: used by both command-plugin and configured-CLI adapters. */
21
+ * Transport-neutral: used by exec and HTTP transport adapters. */
22
22
  export function validateDeclaredResult(output, result) {
23
23
  for (const f of output) {
24
24
  const present = Object.prototype.hasOwnProperty.call(result, f.name) && result[f.name] !== undefined && result[f.name] !== null;
@@ -1,4 +1,5 @@
1
- import type { ConfiguredCliRegistration } from './registration.js';
1
+ import type { HttpPluginFetchTarget } from '../endpoint.js';
2
+ import type { HttpPluginManifestStore } from '../store.js';
2
3
  /** Result of a successful manifest fetch. */
3
4
  export interface FetchSuccess {
4
5
  status: 200;
@@ -11,10 +12,10 @@ export interface FetchFailure {
11
12
  message: string;
12
13
  }
13
14
  /**
14
- * Fetch a configured CLI manifest from the registered endpoint.
15
+ * Fetch a HTTP-transport plugin manifest from the registered endpoint.
15
16
  *
16
17
  * Implements the core authenticated GET primitive for T0/T2/T4 fetches:
17
- * - Reads bearer token from `authEnvVar` env var at invocation time
18
+ * - Reads bearer token from `authEnv` env var at invocation time
18
19
  * - Fails with `auth_env_missing` USAGE before network if env var is named but empty
19
20
  * - Performs one GET request with `Accept: application/json` header
20
21
  * - Attaches `Authorization: Bearer <token>` header when auth var is configured
@@ -28,24 +29,14 @@ export interface FetchFailure {
28
29
  *
29
30
  * No retries, no conditional request (ETag/If-None-Match), no 304 handling.
30
31
  */
31
- export declare function fetchConfiguredCliManifest(registration: ConfiguredCliRegistration): Promise<FetchResult>;
32
+ export declare function fetchHttpPluginManifest(registration: HttpPluginFetchTarget): Promise<FetchResult>;
32
33
  /**
33
34
  * Explicit refresh: unconditional fetch and replace stored manifest.
34
35
  *
35
- * Called when user runs `crtr pkg cli refresh <name>`. Always fetches
36
- * regardless of cache state, and replaces stored manifest on success.
37
- * On failure, preserves existing cache and throws CrtrError.
36
+ * Called when user runs `crtr pkg plugin update <name>`. Always fetches
37
+ * regardless of stored-manifest state, and replaces it on success.
38
+ * On failure, preserves existing manifest bytes and throws CrtrError.
38
39
  *
39
40
  * Returns the raw manifest bytes on success.
40
41
  */
41
- export declare function refreshConfiguredCli(registration: ConfiguredCliRegistration): Promise<Uint8Array>;
42
- /**
43
- * Register-time fetch: fetch and store manifest during registration.
44
- *
45
- * Called when user runs `crtr pkg cli register`. Performs one authenticated
46
- * GET and stores exact bytes. On failure, preserves any prior cached manifest
47
- * and returns the error (does not throw).
48
- *
49
- * Returns a discriminated result: success with bytes, or typed failure.
50
- */
51
- export declare function registerFetchConfiguredCli(registration: ConfiguredCliRegistration): Promise<FetchSuccess | FetchFailure>;
42
+ export declare function refreshHttpPlugin(target: HttpPluginFetchTarget, store: HttpPluginManifestStore): Promise<Uint8Array>;
@@ -1,16 +1,16 @@
1
1
  import { URL } from 'node:url';
2
2
  import { request as httpsRequest } from 'node:https';
3
3
  import { request as httpRequest } from 'node:http';
4
- import { writeConfiguredCliCacheManifest } from './cache.js';
5
- import { general, network, usage } from '../errors.js';
4
+ import { writeHttpPluginManifest } from '../store.js';
5
+ import { general, network, usage } from '../../errors.js';
6
6
  // ---------------------------------------------------------------------------
7
7
  // Core fetch primitive
8
8
  // ---------------------------------------------------------------------------
9
9
  /**
10
- * Fetch a configured CLI manifest from the registered endpoint.
10
+ * Fetch a HTTP-transport plugin manifest from the registered endpoint.
11
11
  *
12
12
  * Implements the core authenticated GET primitive for T0/T2/T4 fetches:
13
- * - Reads bearer token from `authEnvVar` env var at invocation time
13
+ * - Reads bearer token from `authEnv` env var at invocation time
14
14
  * - Fails with `auth_env_missing` USAGE before network if env var is named but empty
15
15
  * - Performs one GET request with `Accept: application/json` header
16
16
  * - Attaches `Authorization: Bearer <token>` header when auth var is configured
@@ -24,15 +24,15 @@ import { general, network, usage } from '../errors.js';
24
24
  *
25
25
  * No retries, no conditional request (ETag/If-None-Match), no 304 handling.
26
26
  */
27
- export async function fetchConfiguredCliManifest(registration) {
27
+ export async function fetchHttpPluginManifest(registration) {
28
28
  // Check auth env var and fail before network if named but empty
29
29
  let token;
30
- if (registration.authEnvVar) {
31
- token = process.env[registration.authEnvVar];
30
+ if (registration.authEnv) {
31
+ token = process.env[registration.authEnv];
32
32
  if (!token) {
33
33
  return {
34
34
  status: 'auth_env_missing',
35
- message: `Environment variable '${registration.authEnvVar}' is required but not set or empty.`,
35
+ message: `Environment variable '${registration.authEnv}' is required but not set or empty.`,
36
36
  };
37
37
  }
38
38
  }
@@ -112,15 +112,15 @@ export async function fetchConfiguredCliManifest(registration) {
112
112
  /**
113
113
  * Explicit refresh: unconditional fetch and replace stored manifest.
114
114
  *
115
- * Called when user runs `crtr pkg cli refresh <name>`. Always fetches
116
- * regardless of cache state, and replaces stored manifest on success.
117
- * On failure, preserves existing cache and throws CrtrError.
115
+ * Called when user runs `crtr pkg plugin update <name>`. Always fetches
116
+ * regardless of stored-manifest state, and replaces it on success.
117
+ * On failure, preserves existing manifest bytes and throws CrtrError.
118
118
  *
119
119
  * Returns the raw manifest bytes on success.
120
120
  */
121
- export async function refreshConfiguredCli(registration) {
121
+ export async function refreshHttpPlugin(target, store) {
122
122
  // Unconditional fetch
123
- const result = await fetchConfiguredCliManifest(registration);
123
+ const result = await fetchHttpPluginManifest(target);
124
124
  if (result.status !== 200) {
125
125
  if (result.status === 'auth_env_missing') {
126
126
  throw usage(result.message);
@@ -130,27 +130,7 @@ export async function refreshConfiguredCli(registration) {
130
130
  }
131
131
  throw general(result.message);
132
132
  }
133
- // Write to cache (replaces existing)
134
- writeConfiguredCliCacheManifest(registration, result.raw);
133
+ // Write to the resolved HTTP manifest store.
134
+ writeHttpPluginManifest(store, result.raw);
135
135
  return result.raw;
136
136
  }
137
- /**
138
- * Register-time fetch: fetch and store manifest during registration.
139
- *
140
- * Called when user runs `crtr pkg cli register`. Performs one authenticated
141
- * GET and stores exact bytes. On failure, preserves any prior cached manifest
142
- * and returns the error (does not throw).
143
- *
144
- * Returns a discriminated result: success with bytes, or typed failure.
145
- */
146
- export async function registerFetchConfiguredCli(registration) {
147
- // Perform fetch
148
- const result = await fetchConfiguredCliManifest(registration);
149
- if (result.status !== 200) {
150
- // On failure, preserve existing cache and return error
151
- return result;
152
- }
153
- // On success, write to cache
154
- writeConfiguredCliCacheManifest(registration, result.raw);
155
- return result;
156
- }
@@ -1,12 +1,12 @@
1
- import type { InputParam, Field } from '../help.js';
2
- import type { RestMapping, ManifestTimeouts } from '../command-manifests/schema.js';
3
- import type { LeafRunContext } from '../command.js';
4
- export interface ConfiguredCliInvokerSpec {
1
+ import type { InputParam, Field } from '../../help.js';
2
+ import type { RestMapping, ManifestTimeouts } from '../../command-manifests/schema.js';
3
+ import type { LeafRunContext } from '../../command.js';
4
+ export interface HttpInvokerSpec {
5
5
  /** The effective registration: local name, manifest endpoint, optional auth env var. */
6
6
  registration: {
7
7
  name: string;
8
8
  endpoint: string;
9
- authEnvVar?: string;
9
+ authEnv?: string;
10
10
  };
11
11
  /** Manifest-level REST base override (§4.2); falls back to the endpoint origin. */
12
12
  baseUrl?: string;
@@ -22,7 +22,7 @@ export interface ConfiguredCliInvokerSpec {
22
22
  commandPath: string[];
23
23
  }
24
24
  /**
25
- * Execute one configured-CLI leaf. Unary leaves return the validated response
25
+ * Execute one HTTP-transport plugin leaf. Unary leaves return the validated response
26
26
  * object (crtr's ordinary dispatcher renders it, or mirrors it under --json).
27
27
  * Streaming leaves relay NDJSON frames to stdout incrementally and return void.
28
28
  *
@@ -33,4 +33,4 @@ export interface ConfiguredCliInvokerSpec {
33
33
  * process.exitCode = NETWORK — it never throws into handle() and never writes a
34
34
  * second result/error object to stdout.
35
35
  */
36
- export declare function invokeConfiguredLeaf(spec: ConfiguredCliInvokerSpec, input: Record<string, unknown>, context?: LeafRunContext): Promise<Record<string, unknown> | void>;
36
+ export declare function invokeHttpLeaf(spec: HttpInvokerSpec, input: Record<string, unknown>, context?: LeafRunContext): Promise<Record<string, unknown> | void>;
@@ -1,10 +1,8 @@
1
- // Configured-CLI HTTP invoker — the execution seam for a validated, composed
2
- // configured-CLI leaf. On explicit leaf invocation only (never on help /
3
- // discovery), it turns a leaf's `rest` mapping + parsed input into one native
4
- // HTTP request and maps the response back onto crtr's output / error / exit
5
- // machinery (spec §6). It is the configured-CLI analogue of the command-plugin
6
- // `executeExternalLeaf` spawn — but the trust surface is definition + HTTP, so
7
- // there is nothing to spawn and nothing to sandbox.
1
+ // HTTP-transport plugin invoker — the execution seam for a validated composed
2
+ // HTTP leaf. On explicit leaf invocation only (never on help or discovery), it
3
+ // turns a leaf's `rest` mapping and parsed input into one native HTTP request
4
+ // and maps the response onto crtr's output, error, and exit machinery. Its
5
+ // trust surface is the manifest definition plus HTTP, so it spawns nothing.
8
6
  //
9
7
  // Ownership boundary: this module owns request construction, local-file
10
8
  // materialization, bearer attachment, the native HTTP lifecycle with three
@@ -17,10 +15,10 @@ import { readFileSync, statSync } from 'node:fs';
17
15
  import { resolve as resolvePath } from 'node:path';
18
16
  import { request as httpRequest } from 'node:http';
19
17
  import { request as httpsRequest } from 'node:https';
20
- import { validateDeclaredResult } from '../command-plugins/adapter.js';
21
- import { CrtrError } from '../errors.js';
22
- import { diag, writeStdout } from '../io.js';
23
- import { ExitCode } from '../../types.js';
18
+ import { validateDeclaredResult } from './exec-invoke.js';
19
+ import { CrtrError } from '../../errors.js';
20
+ import { diag, writeStdout } from '../../io.js';
21
+ import { ExitCode } from '../../../types.js';
24
22
  // ---------------------------------------------------------------------------
25
23
  // Constants
26
24
  // ---------------------------------------------------------------------------
@@ -38,7 +36,7 @@ class StreamIdleError extends Error {
38
36
  // Public entry
39
37
  // ---------------------------------------------------------------------------
40
38
  /**
41
- * Execute one configured-CLI leaf. Unary leaves return the validated response
39
+ * Execute one HTTP-transport plugin leaf. Unary leaves return the validated response
42
40
  * object (crtr's ordinary dispatcher renders it, or mirrors it under --json).
43
41
  * Streaming leaves relay NDJSON frames to stdout incrementally and return void.
44
42
  *
@@ -49,7 +47,7 @@ class StreamIdleError extends Error {
49
47
  * process.exitCode = NETWORK — it never throws into handle() and never writes a
50
48
  * second result/error object to stdout.
51
49
  */
52
- export async function invokeConfiguredLeaf(spec, input, context) {
50
+ export async function invokeHttpLeaf(spec, input, context) {
53
51
  const plan = buildRequestPlan(spec, input, context);
54
52
  if (spec.rest.streaming === true) {
55
53
  await requestStreaming(spec, plan);
@@ -93,14 +91,14 @@ function buildRequestPlan(spec, input, context) {
93
91
  // 5: auth — read the env var immediately before request creation. Missing or
94
92
  // empty fails pre-network; the value never appears in errors or evidence.
95
93
  let authorization;
96
- const authEnvVar = spec.registration.authEnvVar;
97
- if (authEnvVar !== undefined && authEnvVar !== '') {
98
- const token = process.env[authEnvVar];
94
+ const authEnv = spec.registration.authEnv;
95
+ if (authEnv !== undefined && authEnv !== '') {
96
+ const token = process.env[authEnv];
99
97
  if (token === undefined || token === '') {
100
- throw new CrtrError('auth_env_missing', `${label(spec)}: environment variable ${authEnvVar} is not set or empty`, ExitCode.USAGE, {
101
- field: authEnvVar,
102
- received: authEnvVar,
103
- next: `Export ${authEnvVar} with the bearer token, then retry.`,
98
+ throw new CrtrError('auth_env_missing', `${label(spec)}: environment variable ${authEnv} is not set or empty`, ExitCode.USAGE, {
99
+ field: authEnv,
100
+ received: authEnv,
101
+ next: `Export ${authEnv} with the bearer token, then retry.`,
104
102
  });
105
103
  }
106
104
  authorization = `Bearer ${token}`;
@@ -510,20 +508,20 @@ function deriveExit(status) {
510
508
  function protocolError(spec, received, expected) {
511
509
  return new CrtrError('cli_protocol_error', `${label(spec)}: ${received}. Expected ${expected}.`, ExitCode.GENERAL, {
512
510
  received,
513
- next: `Run \`crtr pkg cli refresh ${spec.registration.name}\` or inspect the backend response.`,
511
+ next: `Run \`crtr pkg plugin update ${spec.registration.name}\` or inspect the backend response.`,
514
512
  });
515
513
  }
516
514
  function unreachable(spec, plan, detail) {
517
515
  return new CrtrError('cli_unreachable', `${label(spec)}: ${detail}`, ExitCode.NETWORK, {
518
516
  received: plan.url.origin,
519
- next: `Check connectivity to ${plan.url.host} and retry; run \`crtr pkg cli refresh ${spec.registration.name}\` if the command surface changed.`,
517
+ next: `Check connectivity to ${plan.url.host} and retry; run \`crtr pkg plugin update ${spec.registration.name}\` if the command surface changed.`,
520
518
  });
521
519
  }
522
520
  // ---------------------------------------------------------------------------
523
521
  // Small helpers
524
522
  // ---------------------------------------------------------------------------
525
523
  function label(spec) {
526
- return `configured CLI "${spec.registration.name}" command \`${spec.commandPath.join(' ')}\``;
524
+ return `HTTP-transport plugin "${spec.registration.name}" command \`${spec.commandPath.join(' ')}\``;
527
525
  }
528
526
  function errCode(e) {
529
527
  const err = e;
@@ -160,15 +160,14 @@ export interface ParseArgvOptions {
160
160
  export declare function parseArgv(params: InputParam[], tokens: string[], options?: ParseArgvOptions): Promise<Record<string, unknown>>;
161
161
  /** Optional seams for `runCli`. */
162
162
  export interface RunCliOptions {
163
- /** The ONE unknown-first-token retry seam (amended configured-CLI spec
164
- * §5.1, §8). Called EXACTLY once, and ONLY when an unknown FIRST token
165
- * misses at root (walk stopped at root, `path` empty, on a real non-help
166
- * token) — the sole site absent-store configured-CLI hydration may fire. A
167
- * deeper unknown token under a known command (`path` non-empty) never calls
168
- * it. Returns a freshly rebuilt root when hydration produced new commands to
169
- * re-walk the original argv against, else undefined to surface the plain
170
- * `unknown_path` error. crtr's core fast path passes no options, so a
171
- * recognized core first token is dispatched with zero configured-CLI I/O. */
163
+ /** The one unknown-first-token retry seam. Called exactly once only when an
164
+ * unknown first token misses at root (walk stopped at root, `path` empty,
165
+ * on a real non-help token), where absent HTTP-plugin stores may hydrate.
166
+ * A deeper unknown token under a known command (`path` non-empty) never
167
+ * calls it. It returns a freshly rebuilt root when hydration produced new
168
+ * commands to re-walk the original argv against, otherwise undefined to
169
+ * surface the plain `unknown_path` error. crtr's core fast path passes no
170
+ * options, so a recognized core first token has zero HTTP-plugin I/O. */
172
171
  onUnknownFirstTokenMiss?: () => Promise<RootDef | undefined>;
173
172
  }
174
173
  export declare function runCli(root: RootDef, argv: string[], options?: RunCliOptions): Promise<void>;
@@ -489,7 +489,7 @@ export async function parseArgv(params, tokens, options) {
489
489
  // supply takes its value from that environment variable when it is set and
490
490
  // non-empty. It is recorded as PROVIDED — the caller's environment is a real
491
491
  // source, not a display default — so it satisfies the required check below and
492
- // ships through the configured-CLI invoker's presence rule (a static `default`
492
+ // ships through the HTTP transport's presence rule (a static `default`
493
493
  // deliberately does neither).
494
494
  for (const p of params) {
495
495
  if (p.kind !== 'flag' && p.kind !== 'positional')
@@ -564,14 +564,11 @@ export async function runCli(root, argv, options) {
564
564
  }
565
565
  let { node, path, remaining } = walk(root, tokens);
566
566
  try {
567
- // The ONE unknown-first-token retry (§5.1): an unknown FIRST token — walk
568
- // stopped at root (path empty) on a real, non-help token — is the only site
569
- // absent-store configured-CLI hydration may fire. A deeper unknown token
570
- // under a known command (path non-empty) NEVER triggers it. Invoke the seam
571
- // exactly once; if it hydrated new commands it returns a rebuilt root and we
572
- // re-walk the original argv once, then dispatch proceeds normally against the
573
- // re-resolved node. Kept inside the try so a seam throw routes through the
574
- // same error handler as everything else.
567
+ // An unknown first token that misses at root is the only site absent
568
+ // HTTP-plugin stores may hydrate. A deeper unknown token under a known
569
+ // command never triggers it. Invoke the seam once; if it hydrated commands,
570
+ // re-walk the original argv against the rebuilt root. Keeping it inside the
571
+ // try sends any seam failure through the ordinary error handler.
575
572
  if (options?.onUnknownFirstTokenMiss !== undefined &&
576
573
  node.kind === 'root' &&
577
574
  path.length === 0 &&
@@ -6,6 +6,7 @@ import { emitEvent } from './events/emit.js';
6
6
  import { atomicWriteJson, readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
7
7
  import { scopeRoot, requireScopeRoot, findProjectScopeRoots } from './scope.js';
8
8
  import { profileRoot } from './profiles/manifest.js';
9
+ import { SCOPE_CONFIG_KEYS } from './user-settings.js';
9
10
  function configPathFor(root) {
10
11
  return join(root, CONFIG_FILE);
11
12
  }
@@ -418,7 +419,6 @@ function mergeConfig(partial) {
418
419
  const schema_version = defaults.schema_version;
419
420
  const marketplaces = partial.marketplaces === undefined ? {} : partial.marketplaces;
420
421
  const plugins = partial.plugins === undefined ? {} : partial.plugins;
421
- const configuredClis = partial.configuredClis === undefined ? {} : partial.configuredClis;
422
422
  const au = partial.auto_update;
423
423
  const rawInterval = au && typeof au.interval_hours === 'number' ? au.interval_hours : undefined;
424
424
  const interval_hours = rawInterval !== undefined && Number.isFinite(rawInterval) && rawInterval >= 0
@@ -431,6 +431,9 @@ function mergeConfig(partial) {
431
431
  };
432
432
  const rawMaxPanes = partial.max_panes_per_window;
433
433
  const max_panes_per_window = normalizePositiveInteger(rawMaxPanes, defaults.max_panes_per_window);
434
+ const auto_open_child_viewers = typeof partial.auto_open_child_viewers === 'boolean'
435
+ ? partial.auto_open_child_viewers
436
+ : defaults.auto_open_child_viewers;
434
437
  const brokerThresholds = mergeBrokerThresholds(partial.brokerThresholds, defaults.brokerThresholds);
435
438
  const completion_bell = typeof partial.completion_bell === 'boolean' ? partial.completion_bell : defaults.completion_bell;
436
439
  const live_cycles = normalizePositiveInteger(partial.live_cycles, defaults.live_cycles);
@@ -445,7 +448,7 @@ function mergeConfig(partial) {
445
448
  const kinds = mergeKinds(partial.kinds);
446
449
  const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
447
450
  const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
448
- return { schema_version, marketplaces, plugins, configuredClis, auto_update, max_panes_per_window, brokerThresholds, completion_bell, live_cycles, condensed_history, fold_finished_tools, keybindings, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
451
+ return { schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, completion_bell, live_cycles, condensed_history, fold_finished_tools, keybindings, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
449
452
  }
450
453
  /** Raw (un-defaulted) partial config for one scope, or null if the scope has
451
454
  * no root or no config.json. Used by `readMergedLaunchConfig` to layer
@@ -580,13 +583,6 @@ export function subKindsAvailableTo(kind) {
580
583
  }
581
584
  return out.sort((a, b) => a.kind.localeCompare(b.kind));
582
585
  }
583
- /** Every top-level `ScopeConfig` key, diffed one-by-one by `updateConfig` so
584
- * an untouched key is never rewritten. */
585
- const CONFIG_TOP_LEVEL_KEYS = [
586
- 'schema_version', 'marketplaces', 'plugins', 'configuredClis', 'auto_update',
587
- 'max_panes_per_window', 'brokerThresholds', 'completion_bell', 'live_cycles', 'condensed_history', 'fold_finished_tools', 'keybindings', 'modelLadders', 'kinds',
588
- 'remoteCanvas', 'spawnEnv',
589
- ];
590
586
  /** The SPARSE `modelLadders` to persist after a config mutation: the raw
591
587
  * on-disk sparse ladder (absent rungs stay absent) plus ONLY the specific
592
588
  * rungs / `defaultProvider` the mutation actually changed away from the
@@ -632,7 +628,7 @@ function mutateConfigAtPath(path, mutate) {
632
628
  const cfg = mergeConfig(raw);
633
629
  mutate(cfg);
634
630
  const partial = { ...raw };
635
- for (const key of CONFIG_TOP_LEVEL_KEYS) {
631
+ for (const key of SCOPE_CONFIG_KEYS) {
636
632
  if (key === 'modelLadders')
637
633
  continue;
638
634
  if (!isDeepStrictEqual(cfg[key], baseline[key])) {
@@ -0,0 +1,6 @@
1
+ export interface EnvNameValidation {
2
+ isValid: boolean;
3
+ errors: string[];
4
+ }
5
+ /** Validate an environment variable identifier without reading its value. */
6
+ export declare function validateEnvVarName(name: unknown): EnvNameValidation;
@@ -0,0 +1,9 @@
1
+ /** Validate an environment variable identifier without reading its value. */
2
+ export function validateEnvVarName(name) {
3
+ const errors = [];
4
+ if (typeof name !== 'string')
5
+ errors.push('env var name must be a string');
6
+ else if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
7
+ errors.push('env var name must start with letter/underscore, followed by alphanumeric/underscore');
8
+ return { isValid: errors.length === 0, errors };
9
+ }
package/dist/core/io.d.ts CHANGED
@@ -12,7 +12,7 @@ export interface ErrorPayload {
12
12
  message: string;
13
13
  received?: unknown;
14
14
  field?: string;
15
- /** Transport status of a failed backend call (configured CLIs). It is kept out
15
+ /** Transport status of a failed backend call (HTTP-transport plugins). It is kept out
16
16
  * of `received` on purpose: `received` names the caller's offending VALUE, and
17
17
  * reporting an HTTP status there is a false claim about what was sent. */
18
18
  http_status?: number;
@@ -3,27 +3,43 @@ import assert from 'node:assert/strict';
3
3
  import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { BINDING_CATALOG, KeybindingConcurrentEditError, KeybindingValidationError, SAFETY_REQUIREMENTS, canonicalTerminalStroke, formatStroke, matchesTerminalInput, normalizeGesture, normalizeSparseOverrides, persistUserKeybindings, resolveKeybindings, resolveUserKeybindings, } from '../index.js';
6
+ import { ATTACH_CONTROL_BINDINGS, BINDING_CATALOG, KeybindingConcurrentEditError, KeybindingValidationError, SAFETY_REQUIREMENTS, canonicalTerminalStroke, formatStroke, matchesTerminalInput, normalizeGesture, normalizeSparseOverrides, persistUserKeybindings, resolveKeybindings, resolveUserKeybindings, } from '../index.js';
7
7
  import { readConfig } from '../../config.js';
8
8
  const typeCheckedBindingId = 'crtr.graph.down';
9
9
  // @ts-expect-error The catalog type rejects IDs that are not built in.
10
10
  const rejectedBindingId = 'crtr.graph.not-real';
11
+ // @ts-expect-error The Attach submenu opener has no tmux-global pair.
12
+ const rejectedAttachGlobalId = 'crtr.tmux.menu-global.attach';
11
13
  void typeCheckedBindingId;
12
14
  void rejectedBindingId;
15
+ void rejectedAttachGlobalId;
13
16
  function ids(prefix, actions) {
14
17
  return actions.map((action) => `${prefix}.${action}`);
15
18
  }
16
19
  function pairedMenuIds(actions) {
17
20
  return actions.flatMap((action) => [`crtr.tmux.menu.${action}`, `crtr.tmux.menu-global.${action}`]);
18
21
  }
22
+ function pairedAttachIds(pairs) {
23
+ return pairs.flatMap(([menuAction, directId]) => [`crtr.tmux.menu.attach.${menuAction}`, directId]);
24
+ }
19
25
  const REQUIRED_IDS = [
20
26
  'crtr.tmux.menu.open',
27
+ 'crtr.tmux.menu.attach',
21
28
  ...pairedMenuIds(['promote', 'resume', 'fork', 'demote', 'detach', 'close-subtree', 'background-bash', 'file-review', 'pin', 'show', 'view-settings', 'fold-tools', 'metadata', 'context', 'providers', 'graph', 'focus-manager', 'copy-id', 'reload-viewer', 'issues', 'issue.idea', 'issue.bug']),
22
29
  ...ids('crtr.tmux.node', ['next', 'previous']),
23
30
  'crtr.tmux.inbox.toggle',
24
31
  ...ids('crtr.tmux.view', ['next', 'previous']),
25
- ...ids('crtr.attach', ['detach', 'clear-or-detach', 'graph.toggle', 'model-ladder.next', 'model-ladder.previous', 'command.inspect', 'file-review']),
26
- 'crtr.mode.cycle',
32
+ ...pairedAttachIds([
33
+ ['detach', 'crtr.attach.detach'],
34
+ ['clear-or-detach', 'crtr.attach.clear-or-detach'],
35
+ ['mode-cycle', 'crtr.mode.cycle'],
36
+ ['graph.toggle', 'crtr.attach.graph.toggle'],
37
+ ['model-ladder.next', 'crtr.attach.model-ladder.next'],
38
+ ['model-ladder.previous', 'crtr.attach.model-ladder.previous'],
39
+ ['command.inspect', 'crtr.attach.command.inspect'],
40
+ ['file-review', 'crtr.attach.file-review'],
41
+ ['profile-files', 'crtr.attach.profile-files'],
42
+ ]),
27
43
  ...ids('crtr.graph', ['down', 'up', 'first', 'last', 'collapse-or-manager', 'expand-or-child', 'focus', 'revive', 'focus-manager', 'close-subtree', 'dismiss']),
28
44
  ...ids('crtr.graph.confirm', ['accept', 'cancel']),
29
45
  ...ids('crtr.view.host', ['force-quit', 'editor.backspace']),
@@ -79,6 +95,21 @@ test('catalog is closed, complete, ordered, and has valid collision-free default
79
95
  for (const gesture of definition.defaults)
80
96
  assert.equal(normalizeGesture(gesture, definition), gesture);
81
97
  }
98
+ const byId = new Map(BINDING_CATALOG.map((definition) => [definition.id, definition]));
99
+ for (const definition of BINDING_CATALOG) {
100
+ if (definition.pair === undefined)
101
+ continue;
102
+ const paired = byId.get(definition.pair.pairedId);
103
+ assert.equal(paired?.pair?.pairedId, definition.id, `${definition.id} must have a reciprocal pair`);
104
+ assert.notEqual(paired?.pair?.field, definition.pair.field, `${definition.id} pair fields must differ`);
105
+ assert.ok(definition.pair.field === 'menu' || paired?.pair?.field === 'menu', `${definition.id} must pair with a menu selector`);
106
+ }
107
+ for (const { menuId, actionId } of ATTACH_CONTROL_BINDINGS) {
108
+ assert.equal(byId.get(menuId)?.pair?.field, 'menu');
109
+ assert.equal(byId.get(menuId)?.pair?.pairedId, actionId);
110
+ assert.equal(byId.get(actionId)?.pair?.field, 'direct');
111
+ assert.equal(byId.get(actionId)?.pair?.pairedId, menuId);
112
+ }
82
113
  const resolved = resolveKeybindings();
83
114
  assert.equal(resolved.bindings.size, BINDING_CATALOG.length);
84
115
  assert.deepEqual(resolved.diagnostics, []);
@@ -148,6 +179,12 @@ test('tmux menu selectors collide only inside their own menu', () => {
148
179
  const selector = resolveKeybindings({ 'crtr.tmux.menu.promote': ['j'] });
149
180
  assert.deepEqual(selector.gestures('crtr.tmux.menu.promote'), ['j']);
150
181
  assert.deepEqual(selector.diagnostics, []);
182
+ const attachSelector = resolveKeybindings({ 'crtr.tmux.menu.attach.detach': ['o'] });
183
+ assert.deepEqual(attachSelector.gestures('crtr.tmux.menu.attach.detach'), ['o']);
184
+ assert.deepEqual(attachSelector.diagnostics, [], 'attach submenu selectors may reuse top-level menu selectors');
185
+ const attachCollision = resolveKeybindings({ 'crtr.tmux.menu.attach.detach': ['c'] });
186
+ assert.deepEqual(attachCollision.gestures('crtr.tmux.menu.attach.detach'), ['d']);
187
+ assert.ok(attachCollision.diagnostics.some((item) => item.code === 'collision'), 'selectors in the same attach submenu must remain unique');
151
188
  });
152
189
  test('reachability rejects deliberate unbinding that would trap a context', () => {
153
190
  const graph = resolveKeybindings({
@@ -0,0 +1,37 @@
1
+ export declare const ATTACH_CONTROL_BINDINGS: readonly [{
2
+ readonly menuId: "crtr.tmux.menu.attach.detach";
3
+ readonly actionId: "crtr.attach.detach";
4
+ }, {
5
+ readonly menuId: "crtr.tmux.menu.attach.clear-or-detach";
6
+ readonly actionId: "crtr.attach.clear-or-detach";
7
+ }, {
8
+ readonly menuId: "crtr.tmux.menu.attach.mode-cycle";
9
+ readonly actionId: "crtr.mode.cycle";
10
+ }, {
11
+ readonly menuId: "crtr.tmux.menu.attach.graph.toggle";
12
+ readonly actionId: "crtr.attach.graph.toggle";
13
+ }, {
14
+ readonly menuId: "crtr.tmux.menu.attach.model-ladder.next";
15
+ readonly actionId: "crtr.attach.model-ladder.next";
16
+ }, {
17
+ readonly menuId: "crtr.tmux.menu.attach.model-ladder.previous";
18
+ readonly actionId: "crtr.attach.model-ladder.previous";
19
+ }, {
20
+ readonly menuId: "crtr.tmux.menu.attach.command.inspect";
21
+ readonly actionId: "crtr.attach.command.inspect";
22
+ }, {
23
+ readonly menuId: "crtr.tmux.menu.attach.file-review";
24
+ readonly actionId: "crtr.attach.file-review";
25
+ }, {
26
+ readonly menuId: "crtr.tmux.menu.attach.profile-files";
27
+ readonly actionId: "crtr.attach.profile-files";
28
+ }];
29
+ export type AttachBindingId = typeof ATTACH_CONTROL_BINDINGS[number]['actionId'];
30
+ export type AttachMenuBindingId = typeof ATTACH_CONTROL_BINDINGS[number]['menuId'];
31
+ export interface ExtractedAttachControlInput {
32
+ readonly actionId: AttachBindingId;
33
+ readonly remaining: string;
34
+ }
35
+ export declare function decodeAttachControlInput(data: string): AttachBindingId | undefined;
36
+ export declare function encodeAttachControlInput(actionId: AttachBindingId): string;
37
+ export declare function extractAttachControlInput(data: string): ExtractedAttachControlInput | undefined;
@@ -0,0 +1,38 @@
1
+ export const ATTACH_CONTROL_BINDINGS = [
2
+ { menuId: 'crtr.tmux.menu.attach.detach', actionId: 'crtr.attach.detach' },
3
+ { menuId: 'crtr.tmux.menu.attach.clear-or-detach', actionId: 'crtr.attach.clear-or-detach' },
4
+ { menuId: 'crtr.tmux.menu.attach.mode-cycle', actionId: 'crtr.mode.cycle' },
5
+ { menuId: 'crtr.tmux.menu.attach.graph.toggle', actionId: 'crtr.attach.graph.toggle' },
6
+ { menuId: 'crtr.tmux.menu.attach.model-ladder.next', actionId: 'crtr.attach.model-ladder.next' },
7
+ { menuId: 'crtr.tmux.menu.attach.model-ladder.previous', actionId: 'crtr.attach.model-ladder.previous' },
8
+ { menuId: 'crtr.tmux.menu.attach.command.inspect', actionId: 'crtr.attach.command.inspect' },
9
+ { menuId: 'crtr.tmux.menu.attach.file-review', actionId: 'crtr.attach.file-review' },
10
+ { menuId: 'crtr.tmux.menu.attach.profile-files', actionId: 'crtr.attach.profile-files' },
11
+ ];
12
+ const ATTACH_CONTROL_PREFIX = '\x1b_crtr;';
13
+ const ATTACH_CONTROL_SUFFIX = '\x1b\\';
14
+ const ATTACH_BINDING_IDS = new Set(ATTACH_CONTROL_BINDINGS.map(({ actionId }) => actionId));
15
+ export function decodeAttachControlInput(data) {
16
+ if (!data.startsWith(ATTACH_CONTROL_PREFIX) || !data.endsWith(ATTACH_CONTROL_SUFFIX))
17
+ return undefined;
18
+ const actionId = data.slice(ATTACH_CONTROL_PREFIX.length, -ATTACH_CONTROL_SUFFIX.length);
19
+ return ATTACH_BINDING_IDS.has(actionId) ? actionId : undefined;
20
+ }
21
+ export function encodeAttachControlInput(actionId) {
22
+ return `${ATTACH_CONTROL_PREFIX}${actionId}${ATTACH_CONTROL_SUFFIX}`;
23
+ }
24
+ export function extractAttachControlInput(data) {
25
+ let match;
26
+ for (const { actionId } of ATTACH_CONTROL_BINDINGS) {
27
+ const encoded = encodeAttachControlInput(actionId);
28
+ const index = data.indexOf(encoded);
29
+ if (index >= 0 && (match === undefined || index < match.index))
30
+ match = { actionId, encoded, index };
31
+ }
32
+ if (match === undefined)
33
+ return undefined;
34
+ return {
35
+ actionId: match.actionId,
36
+ remaining: data.slice(0, match.index) + data.slice(match.index + match.encoded.length),
37
+ };
38
+ }