@stackstackstack/dsh-commands 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/interaction/commands/README.md
5
+ README.md: a6c48c84a816fcd5ff8048b5d9634f93e29f8f76
6
+ README.zh.md: df19b3728458543ecabfb2f398879ae14b741564
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @stackstackstack/dsh-commands
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Plugin-owned human-command registry consumed by interactive UI adapters. The [plugin command registration Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md) owns the boundary and dispatch contract.
6
+
7
+ ## Service contract
8
+
9
+ `ctx.commands.register(definition)` registers one lowercase command name, description, optional unstructured-input hint, optional `recordInput` policy, and abortable handler. `recordInput` defaults to true; a command whose authoritative domain event owns the payload sets it to false so `command/run` omits `args` instead of duplicating the input. A registered command is available to every composed command adapter; a plugin that is incompatible with a deployment does not register there. A plain-context registration is global. A command-producing plugin mounted beneath `agent.ctx` declares its own `commands` injection and creates an exact agent-scoped definition; it shadows a global definition with the same name. This child-injection shape preserves the agent scope without making the core agent loop depend on a UI service. Duplicate names within one layer fail during registration. Every disposer is the exact Cordis effect disposer, and registration or removal notifies every `commands/change` observer so live adapters can refresh discovery; observer failures are logged and cannot veto the registry mutation or starve later observers.
10
+
11
+ `list(agent)` returns immutable, name-sorted descriptors after scoped shadowing. `find(agent, name)` returns the corresponding definition. `execute(agent, line, signal)` uses `parseCommand()` and runs only a known command, returning the settled `CommandExecution` (the normalized result plus the lifecycle pairing `commandId`) or `undefined` for invalid syntax or unknown names. A resolved command's lifecycle is logged on the receiving agent's session as the log-only pair `command/run` (before the handler, with a minted `commandId`, the parser's structured name, the issuing `CommandSource`, and `args` unless `recordInput` is false) and `command/done` (at settlement, with the outcome kind and verbatim text; a successful result may also name an earlier non-command authoritative domain event through `sourceEventSeq`; a thrown or aborted handler settles as `kind: 'error'`). Admission misses log nothing. Both are direct standalone appends on the receiving agent's session: no turn wraps them, and persistence drains them through ordinary checkpoints and teardown.
12
+
13
+ `parseCommand()` recognizes a slash at byte zero, a lowercase name containing letters, digits, `_`, or `-`, and either end-of-input or whitespace. It returns every byte after the name as `rawInput`, including separator whitespace; consumers own their command-specific grammar and may normalize only what that grammar permits.
14
+
15
+ Handlers return `success` or `error` plus optional UI text. A successful handler may also return `sourceEventSeq` when an earlier domain event owns a richer presentation; the lifecycle invariant requires that reference to be a prior non-command event in the same session. Results are rendered directly by the adapter and never enter model history. The registry never submits `rawInput` to the agent implicitly; a command producer may explicitly schedule model-visible work through the receiving `Agent`, in which case that producer owns the resulting message contract. The registry races handler completion against the supplied abort signal, but an uncooperative handler may continue its own external side effects after the caller stops awaiting it.
16
+
17
+ ## Composition
18
+
19
+ The shipped `dsh` base mounts this service and the Web client dispatches through it. UI-less demo spines and ACP automation do not provide a command adapter. Custom interactive compositions and command producers mount `@stackstackstack/dsh-commands` explicitly.
20
+
21
+ ## Model Experience
22
+
23
+ ### Direct human commands
24
+
25
+ #### What the model sees
26
+
27
+ The registry itself submits nothing. Known slash commands execute in the UI command plane, and their `CommandResult` text is not submitted as a user message. Unknown slash-command input is rejected by shipped adapters instead of becoming a model prompt. A command producer may explicitly use the receiving `Agent`; for example, [`dsh-plan-mode`](../../plan/plan-mode/README.md#model-and-human-interactions) submits the optional message in `/plan [message]` after selecting plan mode.
28
+
29
+ #### Token effect
30
+
31
+ Command discovery, execution, and UI output add no model tokens. Explicit agent work scheduled by a command producer has the same token effect as the corresponding agent input.
32
+
33
+ #### KV Cache effect
34
+
35
+ Registry metadata, command input, and direct output never enter a model request and do not affect its cache. A mutated domain owns any later cache effect.
36
+
37
+ ## Known Limitations and Deferred Work
38
+
39
+ - **Only unstructured text input** — forms, completion schemas, and typed arguments remain command-owned parsing concerns.
40
+ - **Cooperative side-effect cancellation** — dispatch stops awaiting on abort; handlers must honor the signal to stop work that has already escaped into external systems.
package/README.zh.md ADDED
@@ -0,0 +1,40 @@
1
+ # @stackstackstack/dsh-commands
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 由插件负责、供交互式 UI 适配器使用的面向用户命令注册表。[插件命令注册 Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md)定义了其边界与分发约定。
6
+
7
+ ## 服务约定
8
+
9
+ `ctx.commands.register(definition)` 注册一个小写命令名称、描述、可选的非结构化输入提示、可选的 `recordInput` 策略,以及可中止的处理器。`recordInput` 默认为 true;若载荷由命令的权威领域事件持有,该命令会将 `recordInput` 设为 false,让 `command/run` 省略 `args`,避免重复记录输入。每个已注册命令都可供所有已组合的命令适配器使用;与某项部署不兼容的插件不会在此注册。普通上下文中的注册全局生效。在 `agent.ctx` 下挂载的命令生产插件会声明自身的 `commands` 注入,并创建精确限定到该 agent(智能体)的定义;该定义会遮蔽同名的全局定义。这种子级注入形态保留了 agent 作用域,同时不会让核心 agent loop(智能体循环)依赖 UI 服务。同一层中的名称重复会在注册时失败。每个 disposer 都是 Cordis effect 返回的确切 disposer;注册或移除命令时,系统会通知每个 `commands/change` 观察者,使运行中的适配器能够刷新发现结果。观察者失败会写入日志,既不能否决注册表变更,也不能阻止后续观察者运行。
10
+
11
+ `list(agent)` 在应用作用域遮蔽后,返回按名称排序的不可变描述符。`find(agent, name)` 返回相应定义。`execute(agent, line, signal)` 使用 `parseCommand()`,且只运行已知命令,返回已结算的 `CommandExecution`(规范化结果加生命周期配对 `commandId`);语法无效或名称未知时返回 `undefined`。已解析命令的生命周期会以 log-only 事件对的形式记录在接收 agent 的会话日志中:`command/run`(进入处理器前记录,携带新生成的 `commandId`、解析器的结构化名称、发起方 `CommandSource`,以及 `args`(`recordInput` 为 false 时省略))与 `command/done`(结算时记录,携带结果类型与原样文本;成功结果还可通过 `sourceEventSeq` 指向更早的一条非命令权威领域事件;处理器抛出或被中止时以 `kind: 'error'` 结算)。未通过准入的输入不记录任何事件。两者都直接独立追加到接收 agent 的会话中:没有轮次包裹它们,持久化机制会在常规检查点和销毁期间排空这些事件。
12
+
13
+ `parseCommand()` 识别位于第 0 字节的斜杠、由小写字母、数字、`_` 或 `-` 构成的名称,以及名称后紧接输入末尾或空白的形式。它将名称后的每个字节作为 `rawInput` 返回,其中包括分隔空白;消费方负责各命令专用的语法,只能执行该语法允许的规范化。
14
+
15
+ 处理器返回 `success` 或 `error`,并可附带 UI 文本。若更丰富的呈现由一条更早的领域事件持有,成功的处理器还可返回 `sourceEventSeq`;生命周期不变量要求该引用指向同一会话中更早的一条非命令事件。适配器直接渲染结果,结果绝不进入模型历史。注册表绝不会隐式地把 `rawInput` 提交给 agent;命令生产方可以通过接收命令的 `Agent` 显式安排模型可见工作,此时该生产方负责由此产生的消息约定。注册表会同时等待处理器完成和所提供的中止信号,以先发生者为准,但不响应中止的处理器可能在调用方停止等待后继续产生自身的外部副作用。
16
+
17
+ ## 组合
18
+
19
+ 随产品交付的 `dsh` 基础组合会挂载此服务,Web 客户端通过它分派命令。无 UI 的演示主干和 ACP(Agent Client Protocol)自动化不提供命令适配器。自定义交互式组合与命令生产方会显式挂载 `@stackstackstack/dsh-commands`。
20
+
21
+ ## 模型体验
22
+
23
+ ### 直接面向用户的命令
24
+
25
+ #### 模型看到的内容
26
+
27
+ 注册表自身不会提交任何内容。已知斜杠命令在 UI 命令平面执行,其 `CommandResult` 文本不会作为用户消息提交。已交付的适配器会拒绝未知斜杠命令输入,而不是将其变成模型提示词。命令生产方可以显式使用接收命令的 `Agent`;例如,[`dsh-plan-mode`](../../plan/plan-mode/README.md#model-and-human-interactions)在选择 plan mode 后,会提交 `/plan [message]` 中的可选消息。
28
+
29
+ #### Token 影响
30
+
31
+ 命令发现、执行和 UI 输出不会增加模型 token。命令生产方显式安排的 agent 工作与相应 agent 输入具有相同的 token 影响。
32
+
33
+ #### KV Cache 影响
34
+
35
+ 注册表元数据、命令输入和直接输出绝不会进入模型请求,也不会影响其缓存。发生变更的领域负责之后产生的所有缓存影响。
36
+
37
+ ## 已知限制与暂缓事项
38
+
39
+ - **仅支持非结构化文本输入**:表单、补全 schema 和类型化参数仍由各命令自行解析。
40
+ - **副作用采用协作式取消**:中止后,分发会停止等待;处理器必须遵循信号,才能停止已经进入外部系统的工作。
package/lib/index.js ADDED
@@ -0,0 +1,360 @@
1
+ import { NamedEntries, ScopedLayers } from "@stackstackstack/dsh-scope";
2
+ import { Remote, TypertRemoteService } from "@stackstackstack/dsh-typert-protocol";
3
+ //#region lib/types/brand.js
4
+ /**
5
+ * dsh-commands' owned branded id: command lifecycle pairing across the
6
+ * session log, the wire admission response, and client-side flow pairing.
7
+ *
8
+ * The `Branded<B>` primitive lives in `@stackstackstack/dsh-brand`; this module
9
+ * is a pure type/constructor outlet (no cordis imports, no module
10
+ * augmentation) so wire and client programs can name the brand without
11
+ * loading the host plugin's Context merges — the `dsh-llm/brand` shape.
12
+ *
13
+ * @module @stackstackstack/dsh-commands/brand
14
+ */
15
+ /**
16
+ * Brand a string as a {@link CommandId}.
17
+ * @param id - the executor-minted pairing id.
18
+ * @returns the same string, branded; no validation is performed.
19
+ */
20
+ function CommandId(id) {
21
+ return id;
22
+ }
23
+ //#endregion
24
+ //#region lib/types/index.js
25
+ /**
26
+ * Plugin-owned human-command registry shared by interactive UI adapters.
27
+ * @module @stackstackstack/dsh-commands
28
+ */
29
+ var __runInitializers = function(thisArg, initializers, value) {
30
+ var useValue = arguments.length > 2;
31
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ return useValue ? value : void 0;
33
+ };
34
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
35
+ function accept(f) {
36
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
37
+ return f;
38
+ }
39
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
40
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
41
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
42
+ var _, done = false;
43
+ for (var i = decorators.length - 1; i >= 0; i--) {
44
+ var context = {};
45
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
46
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
47
+ context.addInitializer = function(f) {
48
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
49
+ extraInitializers.push(accept(f || null));
50
+ };
51
+ var result = (0, decorators[i])(kind === "accessor" ? {
52
+ get: descriptor.get,
53
+ set: descriptor.set
54
+ } : descriptor[key], context);
55
+ if (kind === "accessor") {
56
+ if (result === void 0) continue;
57
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
58
+ if (_ = accept(result.get)) descriptor.get = _;
59
+ if (_ = accept(result.set)) descriptor.set = _;
60
+ if (_ = accept(result.init)) initializers.unshift(_);
61
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
62
+ else descriptor[key] = _;
63
+ }
64
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
65
+ done = true;
66
+ };
67
+ const name = "commands";
68
+ const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u;
69
+ /** All command registrations owned by one global or scoped layer. */
70
+ var CommandLayer = class {
71
+ commands;
72
+ /**
73
+ * Create one command layer with diagnostics specific to its ownership scope.
74
+ * @param scope - the scoped owner, or `undefined` for global registrations.
75
+ */
76
+ constructor(scope) {
77
+ this.commands = new NamedEntries((name) => /* @__PURE__ */ new Error(scope === void 0 ? `command "${name}" is already registered (for a per-agent variant, mount a command-injected plugin under that agent's \`agent.ctx\`)` : `command "${name}" is already registered in this scope`));
78
+ }
79
+ /** @returns whether this layer owns no command registrations. */
80
+ isEmpty() {
81
+ return this.commands.isEmpty();
82
+ }
83
+ };
84
+ /**
85
+ * Parse an exact slash command without normalizing its trailing input.
86
+ *
87
+ * @param line - Complete candidate command line.
88
+ * @returns The parsed command, or `undefined` when the line is not a command.
89
+ */
90
+ function parseCommand(line) {
91
+ const match = /^\/([a-z][a-z0-9_-]*)(?=$|[\t\n\r ])/u.exec(line);
92
+ if (match === null) return void 0;
93
+ const name = match[1];
94
+ /* v8 ignore next -- the first capture is required whenever the regular expression matches */
95
+ if (name === void 0) return void 0;
96
+ return Object.freeze({
97
+ name,
98
+ rawInput: line.slice(match[0].length)
99
+ });
100
+ }
101
+ /** Convert arbitrary abort reasons to one stable rejected Error. */
102
+ function abortError(signal) {
103
+ if (signal.reason instanceof Error) return signal.reason;
104
+ return new Error(typeof signal.reason === "string" ? signal.reason : "command aborted");
105
+ }
106
+ /** Render arbitrary thrown values without trusting their string coercion. */
107
+ function renderThrown(value) {
108
+ try {
109
+ return String(value);
110
+ } catch {
111
+ return "<unrenderable thrown value>";
112
+ }
113
+ }
114
+ /** Stop awaiting an uncooperative handler once its owning UI request aborts. */
115
+ function withAbort(promise, signal) {
116
+ if (signal.aborted) return Promise.reject(abortError(signal));
117
+ return new Promise((resolve, reject) => {
118
+ const onAbort = () => {
119
+ signal.removeEventListener("abort", onAbort);
120
+ reject(abortError(signal));
121
+ };
122
+ signal.addEventListener("abort", onAbort, { once: true });
123
+ promise.then((value) => {
124
+ signal.removeEventListener("abort", onAbort);
125
+ resolve(value);
126
+ }, (error) => {
127
+ signal.removeEventListener("abort", onAbort);
128
+ reject(error instanceof Error ? error : new Error(`command handler rejected with a non-Error value: ${renderThrown(error)}`, { cause: error }));
129
+ });
130
+ });
131
+ }
132
+ /** Reject invalid command metadata before it can reach a UI protocol. */
133
+ function normalizeDefinition(definition) {
134
+ if (!COMMAND_NAME.test(definition.name)) throw new TypeError(`command name "${definition.name}" must match ${String(COMMAND_NAME)}`);
135
+ if (typeof definition.description !== "string") throw new TypeError(`command "${definition.name}" description must be a string`);
136
+ if (definition.description.trim().length === 0) throw new TypeError(`command "${definition.name}" description must not be empty`);
137
+ if (typeof definition.handler !== "function") throw new TypeError(`command "${definition.name}" handler must be a function`);
138
+ const rawInput = definition.input;
139
+ let input;
140
+ if (rawInput !== void 0) {
141
+ if (typeof rawInput !== "object" || rawInput === null || !("hint" in rawInput) || typeof rawInput.hint !== "string") throw new TypeError(`command "${definition.name}" input hint must be a string`);
142
+ if (rawInput.hint.trim().length === 0) throw new TypeError(`command "${definition.name}" input hint must not be empty`);
143
+ input = Object.freeze({ hint: rawInput.hint });
144
+ }
145
+ const normalized = Object.freeze({
146
+ name: definition.name,
147
+ description: definition.description,
148
+ ...input === void 0 ? {} : { input },
149
+ ...definition.recordInput === void 0 ? {} : { recordInput: definition.recordInput },
150
+ handler: definition.handler
151
+ });
152
+ return {
153
+ definition: normalized,
154
+ descriptor: Object.freeze({
155
+ name: normalized.name,
156
+ description: normalized.description,
157
+ ...normalized.input === void 0 ? {} : { input: normalized.input }
158
+ })
159
+ };
160
+ }
161
+ /** Validate and detach an untrusted handler result at the registry boundary. */
162
+ function normalizeResult(command, value) {
163
+ if (typeof value !== "object" || value === null || !("kind" in value)) throw new TypeError(`command "${command}" handler must return a CommandResult`);
164
+ const result = value;
165
+ if (result.kind === "success") {
166
+ if (result.text !== void 0 && typeof result.text !== "string") throw new TypeError(`command "${command}" success text must be a string when supplied`);
167
+ if (result.sourceEventSeq !== void 0 && (!Number.isSafeInteger(result.sourceEventSeq) || result.sourceEventSeq < 0)) throw new TypeError(`command "${command}" success sourceEventSeq must be a non-negative safe integer when supplied`);
168
+ return Object.freeze({
169
+ kind: "success",
170
+ ...result.text === void 0 ? {} : { text: result.text },
171
+ ...result.sourceEventSeq === void 0 ? {} : { sourceEventSeq: result.sourceEventSeq }
172
+ });
173
+ }
174
+ if (result.kind === "error") {
175
+ if (typeof result.text !== "string" || result.text.trim().length === 0) throw new TypeError(`command "${command}" error text must be a non-empty string`);
176
+ return Object.freeze({
177
+ kind: "error",
178
+ text: result.text
179
+ });
180
+ }
181
+ throw new TypeError(`command "${command}" returned unknown result kind "${String(result.kind)}"`);
182
+ }
183
+ /**
184
+ * Human-command registry. Plain-context definitions are global; definitions
185
+ * registered through a command-injected child of an agent context shadow
186
+ * globals for that agent.
187
+ */
188
+ let CommandRuntime = (() => {
189
+ let _classSuper = TypertRemoteService;
190
+ let _instanceExtraInitializers = [];
191
+ let _list_decorators;
192
+ let _execute_decorators;
193
+ return class CommandRuntime extends _classSuper {
194
+ static {
195
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
196
+ _list_decorators = [Remote];
197
+ _execute_decorators = [Remote];
198
+ __esDecorate(this, null, _list_decorators, {
199
+ kind: "method",
200
+ name: "list",
201
+ static: false,
202
+ private: false,
203
+ access: {
204
+ has: (obj) => "list" in obj,
205
+ get: (obj) => obj.list
206
+ },
207
+ metadata: _metadata
208
+ }, null, _instanceExtraInitializers);
209
+ __esDecorate(this, null, _execute_decorators, {
210
+ kind: "method",
211
+ name: "execute",
212
+ static: false,
213
+ private: false,
214
+ access: {
215
+ has: (obj) => "execute" in obj,
216
+ get: (obj) => obj.execute
217
+ },
218
+ metadata: _metadata
219
+ }, null, _instanceExtraInitializers);
220
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
221
+ enumerable: true,
222
+ configurable: true,
223
+ writable: true,
224
+ value: _metadata
225
+ });
226
+ }
227
+ layers = (__runInitializers(this, _instanceExtraInitializers), new ScopedLayers((scope) => new CommandLayer(scope), () => {
228
+ this.notifyChange();
229
+ }));
230
+ /** Monotonic per-instance counter behind {@link mintCommandId}. */
231
+ commandSeq = 0;
232
+ /** Instance token keeping minted ids unique across process restarts over one resumed log. */
233
+ instanceToken = crypto.randomUUID().slice(0, 8);
234
+ constructor(ctx) {
235
+ super(ctx, "commands");
236
+ }
237
+ /**
238
+ * Register a global or calling-agent-scoped command.
239
+ * @param definition - discovery metadata and direct UI handler.
240
+ * @returns the exact effect disposer that unregisters this definition.
241
+ */
242
+ register(definition) {
243
+ const registered = normalizeDefinition(definition);
244
+ return this.layers.effect(this.ctx, (layer) => layer.commands.insert(registered.definition.name, registered), { label: "commands.register()" });
245
+ }
246
+ /**
247
+ * List the effective immutable command descriptors for one agent.
248
+ * @param agent - exact receiving agent and scoped-layer key.
249
+ * @returns name-sorted descriptors after scoped shadowing.
250
+ */
251
+ list(agent) {
252
+ return Object.freeze([...this.view(agent).values()].map((command) => command.descriptor).sort((left, right) => left.name < right.name ? -1 : 1));
253
+ }
254
+ /**
255
+ * Resolve one effective command definition.
256
+ * @param agent - exact receiving agent and scoped-layer key.
257
+ * @param name - command name without a slash.
258
+ * @returns the scoped shadow or global definition.
259
+ */
260
+ find(agent, name) {
261
+ return this.view(agent).get(name)?.definition;
262
+ }
263
+ /**
264
+ * Parse and execute a known command without sending it to the model.
265
+ *
266
+ * A resolved command's lifecycle is logged: `command/run` is appended
267
+ * before the handler is invoked and `command/done` after settlement (a
268
+ * thrown or aborted handler settles as `kind: 'error'`). Both are direct
269
+ * log-only appends — no turn wraps them, and persistence drains them at
270
+ * ordinary checkpoints. Admission misses (syntax or unknown name) log
271
+ * nothing — they never entered a handler. A `command/run` append failure
272
+ * fails the execution loud; a `command/done` append failure on the
273
+ * handler-failure path is contained so the handler's own error stays the
274
+ * reported failure.
275
+ *
276
+ * @param agent - exact receiving agent.
277
+ * @param line - complete slash-command line.
278
+ * @param signal - cancellation signal owned by the UI request.
279
+ * @returns the settled execution (result + lifecycle pairing id), or
280
+ * `undefined` when syntax or name does not resolve.
281
+ */
282
+ async execute(agent, line, signal) {
283
+ const parsed = parseCommand(line);
284
+ if (parsed === void 0) return void 0;
285
+ const command = this.view(agent).get(parsed.name);
286
+ if (command === void 0) return void 0;
287
+ if (signal.aborted) throw abortError(signal);
288
+ const commandId = this.mintCommandId();
289
+ this.appendLifecycle(agent.session, "command/run", {
290
+ commandId,
291
+ name: parsed.name,
292
+ ...command.definition.recordInput === false ? {} : { args: parsed.rawInput },
293
+ source: { kind: "user" }
294
+ });
295
+ const invocation = Object.freeze({
296
+ commandId,
297
+ agent,
298
+ rawInput: parsed.rawInput,
299
+ signal
300
+ });
301
+ let result;
302
+ try {
303
+ const output = command.definition.handler(invocation);
304
+ result = normalizeResult(parsed.name, await withAbort(Promise.resolve(output), signal));
305
+ } catch (error) {
306
+ try {
307
+ this.appendLifecycle(agent.session, "command/done", {
308
+ commandId,
309
+ kind: "error",
310
+ text: error instanceof Error ? error.message : renderThrown(error)
311
+ });
312
+ } catch (appendError) {
313
+ this.ctx.logger.warn(`command "${parsed.name}": command/done append failed: ${renderThrown(appendError)}`);
314
+ }
315
+ throw error;
316
+ }
317
+ this.appendLifecycle(agent.session, "command/done", {
318
+ commandId,
319
+ kind: result.kind,
320
+ ...result.text === void 0 ? {} : { text: result.text },
321
+ ...result.kind === "success" && result.sourceEventSeq !== void 0 ? { sourceEventSeq: result.sourceEventSeq } : {}
322
+ });
323
+ return Object.freeze({
324
+ commandId,
325
+ result
326
+ });
327
+ }
328
+ /** Mint the next pairing id (monotonic; instance-token-prefixed so a resumed log never repeats one). */
329
+ mintCommandId() {
330
+ this.commandSeq += 1;
331
+ return CommandId(`cmd-${this.instanceToken}-${this.commandSeq}`);
332
+ }
333
+ /**
334
+ * Append one log-only lifecycle event directly: no turn is opened for it and
335
+ * no flush is forced — persistence observes the eager `session/event` path
336
+ * and drains at ordinary checkpoints and teardown, like every other
337
+ * standalone plugin event.
338
+ */
339
+ appendLifecycle(session, type, data) {
340
+ return session.append.bind(session)(type, data);
341
+ }
342
+ /** Resolve global definitions followed by exact scoped shadows. */
343
+ view(agent) {
344
+ return this.layers.merge(agent, (layer) => layer.commands);
345
+ }
346
+ /** Notify every registry observer without making UI refresh load-bearing. */
347
+ notifyChange() {
348
+ for (const callback of this.ctx.events.dispatch("emit", ["commands/change"])) try {
349
+ const returned = callback();
350
+ Promise.resolve(returned).catch((error) => {
351
+ this.ctx.logger.warn(`commands/change listener rejected: ${renderThrown(error)}`);
352
+ });
353
+ } catch (error) {
354
+ this.ctx.logger.warn(`commands/change listener threw: ${renderThrown(error)}`);
355
+ }
356
+ }
357
+ };
358
+ })();
359
+ //#endregion
360
+ export { CommandId, CommandRuntime, CommandRuntime as default, name, parseCommand };
@@ -0,0 +1,43 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@stackstackstack/dsh-commands`:
4
+ * command lifecycle events pair by commandId within one session log.
5
+ * @module @stackstackstack/dsh-commands/invariant
6
+ */
7
+ const PACKAGE_NAME = "@stackstackstack/dsh-commands";
8
+ /** Cordis companion plugin name. */
9
+ const name = "commands-invariant";
10
+ /** Service required before the companion can reserve package ownership. */
11
+ const inject = ["invariants"];
12
+ /** Install pairing validation over loaded logs and newly appended lifecycle events. */
13
+ const install = Object.assign((ctx, fail) => {
14
+ const runIds = /* @__PURE__ */ new WeakMap();
15
+ const validateEvent = (session, event) => {
16
+ if (event.type === "command/run") {
17
+ const ids = runIds.get(session) ?? /* @__PURE__ */ new Set();
18
+ if (ids.has(event.data.commandId)) fail(`command/run repeats commandId ${JSON.stringify(event.data.commandId)}`);
19
+ ids.add(event.data.commandId);
20
+ runIds.set(session, ids);
21
+ return;
22
+ }
23
+ if (event.type !== "command/done") return;
24
+ if (runIds.get(session)?.has(event.data.commandId) !== true) fail(`command/done ${JSON.stringify(event.data.commandId)} pairs no prior command/run in this log`);
25
+ const source = event.data.sourceEventSeq;
26
+ const sourceEvent = source === void 0 ? void 0 : session.events[source];
27
+ if (source !== void 0 && (event.data.kind !== "success" || !Number.isSafeInteger(source) || source < 0 || source >= event.seq || sourceEvent?.seq !== source || sourceEvent.type === "command/run" || sourceEvent.type === "command/done")) fail(`command/done ${JSON.stringify(event.data.commandId)} has invalid sourceEventSeq ${String(source)}`);
28
+ };
29
+ for (const session of ctx.sessions.list()) for (const event of session.events) validateEvent(session, event);
30
+ ctx.on("internal/dispatch", (_mode, eventName, args) => {
31
+ if (eventName !== "session/event") return;
32
+ const [session, event] = args;
33
+ validateEvent(session, event);
34
+ }, { global: true });
35
+ }, { inject: ["sessions"] });
36
+ /**
37
+ * Register this package's invariant companion.
38
+ * @param ctx - Cordis context carrying the invariant service.
39
+ * @returns the installed registration's disposer after setup succeeds.
40
+ */
41
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
42
+ //#endregion
43
+ export { apply, inject, name };
@@ -0,0 +1,3 @@
1
+ /* Generated by @stackstackstack/dsh-typert-generator from FaceModel — do not edit. */
2
+
3
+ export declare const TYPERT: unknown