@qilin-agent/commands 0.0.0-stage → 3.1.3
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 +21 -0
- package/README.i18n.yaml +76 -0
- package/README.md +148 -2
- package/README.zh.md +149 -0
- package/lib/index.js +478 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +821 -0
- package/lib/typert.remote-client.d.ts +29 -0
- package/lib/typert.remote-client.js +131 -0
- package/lib/types/brand.d.ts +32 -0
- package/lib/types/brand.js +27 -0
- package/lib/types/index.d.ts +160 -0
- package/lib/types/index.js +462 -0
- package/lib/types/types.d.ts +121 -0
- package/lib/types/types.js +10 -0
- package/package.json +74 -3
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-owned human-command registry shared by interactive UI adapters.
|
|
3
|
+
* @module @qilin-agent/commands
|
|
4
|
+
*/
|
|
5
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
6
|
+
var useValue = arguments.length > 2;
|
|
7
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
8
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
9
|
+
}
|
|
10
|
+
return useValue ? value : void 0;
|
|
11
|
+
};
|
|
12
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
13
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
14
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
15
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
16
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
17
|
+
var _, done = false;
|
|
18
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
19
|
+
var context = {};
|
|
20
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
21
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
22
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
23
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
24
|
+
if (kind === "accessor") {
|
|
25
|
+
if (result === void 0) continue;
|
|
26
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
27
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
28
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
29
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
30
|
+
}
|
|
31
|
+
else if (_ = accept(result)) {
|
|
32
|
+
if (kind === "field") initializers.unshift(_);
|
|
33
|
+
else descriptor[key] = _;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
37
|
+
done = true;
|
|
38
|
+
};
|
|
39
|
+
import { randomUUID } from '@qilin-agent/util-crypto';
|
|
40
|
+
import { AttachmentError, admitEncodedImages } from '@qilin-agent/attachment';
|
|
41
|
+
import { NamedEntries, ScopedLayers } from '@qilin-agent/scope';
|
|
42
|
+
import { SessionSeq } from '@qilin-agent/session';
|
|
43
|
+
import { TypertRemoteService, Remote } from '@qilin-agent/typert-protocol';
|
|
44
|
+
import { CommandId } from "./brand.js";
|
|
45
|
+
export { CommandDefinitionId, CommandId } from "./brand.js";
|
|
46
|
+
export const name = 'commands';
|
|
47
|
+
const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u;
|
|
48
|
+
/** Shared frozen attachments value for attachment-free invocations. */
|
|
49
|
+
const NO_ATTACHMENTS = Object.freeze([]);
|
|
50
|
+
/** All command registrations owned by one global or scoped layer. */
|
|
51
|
+
class CommandLayer {
|
|
52
|
+
commands;
|
|
53
|
+
/**
|
|
54
|
+
* Create one command layer with diagnostics specific to its ownership scope.
|
|
55
|
+
* @param scope - the scoped owner, or `undefined` for global registrations.
|
|
56
|
+
*/
|
|
57
|
+
constructor(scope) {
|
|
58
|
+
this.commands = new NamedEntries(name => new Error(scope === undefined
|
|
59
|
+
? `command "${name}" is already registered (for a per-agent variant, mount a command-injected plugin under that agent's \`agent.ctx\`)`
|
|
60
|
+
: `command "${name}" is already registered in this scope`));
|
|
61
|
+
}
|
|
62
|
+
/** @returns whether this layer owns no command registrations. */
|
|
63
|
+
isEmpty() {
|
|
64
|
+
return this.commands.isEmpty();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parse an exact slash command without normalizing its trailing input.
|
|
69
|
+
*
|
|
70
|
+
* @param line - Complete candidate command line.
|
|
71
|
+
* @returns The parsed command, or `undefined` when the line is not a command.
|
|
72
|
+
*/
|
|
73
|
+
export function parseCommand(line) {
|
|
74
|
+
const match = /^\/([a-z][a-z0-9_-]*)(?=$|[\t\n\r ])/u.exec(line);
|
|
75
|
+
if (match === null)
|
|
76
|
+
return undefined;
|
|
77
|
+
const name = match[1];
|
|
78
|
+
/* v8 ignore next -- the first capture is required whenever the regular expression matches */
|
|
79
|
+
if (name === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
return Object.freeze({ name, rawInput: line.slice(match[0].length) });
|
|
82
|
+
}
|
|
83
|
+
/** Convert arbitrary abort reasons to one stable rejected Error. */
|
|
84
|
+
function abortError(signal) {
|
|
85
|
+
if (signal.reason instanceof Error)
|
|
86
|
+
return signal.reason;
|
|
87
|
+
return new Error(typeof signal.reason === 'string' ? signal.reason : 'command aborted');
|
|
88
|
+
}
|
|
89
|
+
/** The signal's normalized abort error when it is already aborted. */
|
|
90
|
+
function cancellationOf(signal) {
|
|
91
|
+
return signal.aborted ? abortError(signal) : undefined;
|
|
92
|
+
}
|
|
93
|
+
/** Render arbitrary thrown values without trusting their string coercion. */
|
|
94
|
+
function renderThrown(value) {
|
|
95
|
+
try {
|
|
96
|
+
return String(value);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return '<unrenderable thrown value>';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Stop awaiting an uncooperative handler once its owning UI request aborts. */
|
|
103
|
+
function withAbort(promise, signal) {
|
|
104
|
+
if (signal.aborted)
|
|
105
|
+
return Promise.reject(abortError(signal));
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const onAbort = () => {
|
|
108
|
+
signal.removeEventListener('abort', onAbort);
|
|
109
|
+
reject(abortError(signal));
|
|
110
|
+
};
|
|
111
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
112
|
+
promise.then((value) => {
|
|
113
|
+
signal.removeEventListener('abort', onAbort);
|
|
114
|
+
resolve(value);
|
|
115
|
+
}, (error) => {
|
|
116
|
+
signal.removeEventListener('abort', onAbort);
|
|
117
|
+
reject(error instanceof Error
|
|
118
|
+
? error
|
|
119
|
+
: new Error(`command handler rejected with a non-Error value: ${renderThrown(error)}`, { cause: error }));
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Reject invalid command metadata before it can reach a UI protocol. */
|
|
124
|
+
function normalizeDefinition(definition) {
|
|
125
|
+
if (!COMMAND_NAME.test(definition.name)) {
|
|
126
|
+
throw new TypeError(`command name "${definition.name}" must match ${String(COMMAND_NAME)}`);
|
|
127
|
+
}
|
|
128
|
+
if (typeof definition.description !== 'string') {
|
|
129
|
+
throw new TypeError(`command "${definition.name}" description must be a string`);
|
|
130
|
+
}
|
|
131
|
+
if (definition.description.trim().length === 0) {
|
|
132
|
+
throw new TypeError(`command "${definition.name}" description must not be empty`);
|
|
133
|
+
}
|
|
134
|
+
if (typeof definition.handler !== 'function') {
|
|
135
|
+
throw new TypeError(`command "${definition.name}" handler must be a function`);
|
|
136
|
+
}
|
|
137
|
+
const rawInput = definition.input;
|
|
138
|
+
let input;
|
|
139
|
+
if (rawInput !== undefined) {
|
|
140
|
+
if (typeof rawInput !== 'object' || rawInput === null || !('hint' in rawInput)
|
|
141
|
+
|| typeof rawInput.hint !== 'string') {
|
|
142
|
+
throw new TypeError(`command "${definition.name}" input hint must be a string`);
|
|
143
|
+
}
|
|
144
|
+
if (rawInput.hint.trim().length === 0) {
|
|
145
|
+
throw new TypeError(`command "${definition.name}" input hint must not be empty`);
|
|
146
|
+
}
|
|
147
|
+
if ('attachments' in rawInput && rawInput.attachments !== undefined && typeof rawInput.attachments !== 'boolean') {
|
|
148
|
+
throw new TypeError(`command "${definition.name}" input attachments flag must be a boolean`);
|
|
149
|
+
}
|
|
150
|
+
input = Object.freeze({
|
|
151
|
+
hint: rawInput.hint,
|
|
152
|
+
...('attachments' in rawInput && rawInput.attachments === true) ? { attachments: true } : {},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const normalized = Object.freeze({
|
|
156
|
+
...definition.definitionId === undefined ? {} : { definitionId: definition.definitionId },
|
|
157
|
+
name: definition.name,
|
|
158
|
+
description: definition.description,
|
|
159
|
+
...input === undefined ? {} : { input },
|
|
160
|
+
...definition.recordInput === undefined ? {} : { recordInput: definition.recordInput },
|
|
161
|
+
handler: definition.handler,
|
|
162
|
+
});
|
|
163
|
+
const descriptor = Object.freeze({
|
|
164
|
+
...normalized.definitionId === undefined ? {} : { definitionId: normalized.definitionId },
|
|
165
|
+
name: normalized.name,
|
|
166
|
+
description: normalized.description,
|
|
167
|
+
...normalized.input === undefined ? {} : { input: normalized.input },
|
|
168
|
+
});
|
|
169
|
+
return { definition: normalized, descriptor };
|
|
170
|
+
}
|
|
171
|
+
/** Validate and detach an untrusted handler result at the registry boundary. */
|
|
172
|
+
function normalizeResult(command, value) {
|
|
173
|
+
if (typeof value !== 'object' || value === null || !('kind' in value)) {
|
|
174
|
+
throw new TypeError(`command "${command}" handler must return a CommandResult`);
|
|
175
|
+
}
|
|
176
|
+
const result = value;
|
|
177
|
+
if (result.kind === 'success') {
|
|
178
|
+
if (result.text !== undefined && typeof result.text !== 'string') {
|
|
179
|
+
throw new TypeError(`command "${command}" success text must be a string when supplied`);
|
|
180
|
+
}
|
|
181
|
+
if (result.sourceEventSeq !== undefined
|
|
182
|
+
&& (!Number.isSafeInteger(result.sourceEventSeq)
|
|
183
|
+
|| result.sourceEventSeq < 0
|
|
184
|
+
|| Object.is(result.sourceEventSeq, -0))) {
|
|
185
|
+
throw new TypeError(`command "${command}" success sourceEventSeq must be a non-negative safe integer when supplied`);
|
|
186
|
+
}
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
kind: 'success',
|
|
189
|
+
...result.text === undefined ? {} : { text: result.text },
|
|
190
|
+
...result.sourceEventSeq === undefined ? {} : { sourceEventSeq: SessionSeq(result.sourceEventSeq) },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (result.kind === 'error') {
|
|
194
|
+
if (typeof result.text !== 'string' || result.text.trim().length === 0) {
|
|
195
|
+
throw new TypeError(`command "${command}" error text must be a non-empty string`);
|
|
196
|
+
}
|
|
197
|
+
return Object.freeze({ kind: 'error', text: result.text });
|
|
198
|
+
}
|
|
199
|
+
throw new TypeError(`command "${command}" returned unknown result kind "${String(result.kind)}"`);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Human-command registry. Plain-context definitions are global; definitions
|
|
203
|
+
* registered through a command-injected child of an agent context shadow
|
|
204
|
+
* globals for that agent.
|
|
205
|
+
*/
|
|
206
|
+
let CommandRuntime = (() => {
|
|
207
|
+
let _classSuper = TypertRemoteService;
|
|
208
|
+
let _instanceExtraInitializers = [];
|
|
209
|
+
let _list_decorators;
|
|
210
|
+
let _execute_decorators;
|
|
211
|
+
return class CommandRuntime extends _classSuper {
|
|
212
|
+
static {
|
|
213
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
214
|
+
_list_decorators = [Remote];
|
|
215
|
+
_execute_decorators = [Remote];
|
|
216
|
+
__esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
217
|
+
__esDecorate(this, null, _execute_decorators, { kind: "method", name: "execute", static: false, private: false, access: { has: obj => "execute" in obj, get: obj => obj.execute }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
218
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
219
|
+
}
|
|
220
|
+
layers = (__runInitializers(this, _instanceExtraInitializers), new ScopedLayers(scope => new CommandLayer(scope), () => { this.notifyChange(); }));
|
|
221
|
+
/** Monotonic per-instance counter behind {@link mintCommandId}. */
|
|
222
|
+
commandSeq = 0;
|
|
223
|
+
/** Instance token keeping minted ids unique across process restarts over one resumed log. */
|
|
224
|
+
instanceToken = randomUUID().slice(0, 8);
|
|
225
|
+
/** Optional provider installed by the Session upload owner. */
|
|
226
|
+
fileReceipts = { resolver: undefined };
|
|
227
|
+
constructor(ctx) {
|
|
228
|
+
super(ctx, 'commands');
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Register a global or calling-agent-scoped command.
|
|
232
|
+
* @param definition - discovery metadata and direct UI handler.
|
|
233
|
+
* @returns the exact effect disposer that unregisters this definition.
|
|
234
|
+
*/
|
|
235
|
+
register(definition) {
|
|
236
|
+
const registered = normalizeDefinition(definition);
|
|
237
|
+
return this.layers.effect(this.ctx, layer => layer.commands.insert(registered.definition.name, registered), { label: 'commands.register()' });
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Register the sole authority that resolves staged file receipts for command submissions.
|
|
241
|
+
* @param resolver - Session-aware receipt resolver.
|
|
242
|
+
* @returns disposer that removes this exact resolver.
|
|
243
|
+
*/
|
|
244
|
+
registerFileReceiptResolver(resolver) {
|
|
245
|
+
if (this.fileReceipts.resolver !== undefined) {
|
|
246
|
+
throw new Error('commands: a file receipt resolver is already registered');
|
|
247
|
+
}
|
|
248
|
+
this.fileReceipts.resolver = resolver;
|
|
249
|
+
return () => {
|
|
250
|
+
if (this.fileReceipts.resolver === resolver)
|
|
251
|
+
this.fileReceipts.resolver = undefined;
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* List the effective immutable command descriptors for one agent.
|
|
256
|
+
* @param agent - exact receiving agent and scoped-layer key.
|
|
257
|
+
* @returns name-sorted descriptors after scoped shadowing.
|
|
258
|
+
*/
|
|
259
|
+
list(agent) {
|
|
260
|
+
return Object.freeze([...this.view(agent).values()]
|
|
261
|
+
.map(command => command.descriptor)
|
|
262
|
+
// Names are unique in the effective view, so equality is impossible.
|
|
263
|
+
.sort((left, right) => left.name < right.name ? -1 : 1));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Resolve one effective command definition.
|
|
267
|
+
* @param agent - exact receiving agent and scoped-layer key.
|
|
268
|
+
* @param name - command name without a slash.
|
|
269
|
+
* @returns the scoped shadow or global definition.
|
|
270
|
+
*/
|
|
271
|
+
find(agent, name) {
|
|
272
|
+
return this.view(agent).get(name)?.definition;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Parse and execute a known command without sending it to the model.
|
|
276
|
+
*
|
|
277
|
+
* A resolved command's lifecycle is logged: `command/run` is appended
|
|
278
|
+
* before the handler is invoked and `command/done` after settlement (a
|
|
279
|
+
* thrown or aborted handler settles as `kind: 'error'`). Both are direct
|
|
280
|
+
* log-only appends — no turn wraps them, and persistence drains them at
|
|
281
|
+
* ordinary checkpoints. Admission misses (syntax or unknown name) log
|
|
282
|
+
* nothing — they never entered a handler. A `command/run` append failure
|
|
283
|
+
* fails the execution loud; a `command/done` append failure on the
|
|
284
|
+
* handler-failure path is contained so the handler's own error stays the
|
|
285
|
+
* reported failure.
|
|
286
|
+
*
|
|
287
|
+
* Attachment admission is enforced here, not in the composer: attachments sent to a
|
|
288
|
+
* command that does not declare `input.attachments`, an absent attachment store,
|
|
289
|
+
* and an exceeded image limit each settle as an error result before
|
|
290
|
+
* the handler runs. Validation rejection starts no attachment writes;
|
|
291
|
+
* a storage failure can leave only unreachable content-addressed objects
|
|
292
|
+
* for deferred collection.
|
|
293
|
+
*
|
|
294
|
+
* @param agent - exact receiving agent.
|
|
295
|
+
* @param line - complete slash-command line.
|
|
296
|
+
* @param submittedAttachments - encoded images and staged file receipts accompanying the line,
|
|
297
|
+
* in submission order; empty for a plain invocation.
|
|
298
|
+
* @param signal - cancellation signal owned by the UI request.
|
|
299
|
+
* @returns the settled execution (result + lifecycle pairing id), or
|
|
300
|
+
* `undefined` when syntax or name does not resolve.
|
|
301
|
+
*/
|
|
302
|
+
async execute(agent, line, submittedAttachments, signal) {
|
|
303
|
+
const parsed = parseCommand(line);
|
|
304
|
+
if (parsed === undefined)
|
|
305
|
+
return undefined;
|
|
306
|
+
const command = this.view(agent).get(parsed.name);
|
|
307
|
+
if (command === undefined)
|
|
308
|
+
return undefined;
|
|
309
|
+
if (signal.aborted)
|
|
310
|
+
throw abortError(signal);
|
|
311
|
+
const commandId = this.mintCommandId();
|
|
312
|
+
this.appendLifecycle(agent.session, 'command/run', {
|
|
313
|
+
commandId,
|
|
314
|
+
name: parsed.name,
|
|
315
|
+
...command.definition.recordInput === false ? {} : { args: parsed.rawInput },
|
|
316
|
+
source: { kind: 'user' },
|
|
317
|
+
});
|
|
318
|
+
const settle = (result) => {
|
|
319
|
+
this.appendLifecycle(agent.session, 'command/done', {
|
|
320
|
+
commandId, kind: result.kind,
|
|
321
|
+
...result.text === undefined ? {} : { text: result.text },
|
|
322
|
+
...result.kind === 'success' && result.sourceEventSeq !== undefined
|
|
323
|
+
? { sourceEventSeq: result.sourceEventSeq }
|
|
324
|
+
: {},
|
|
325
|
+
});
|
|
326
|
+
return Object.freeze({ commandId, result: Object.freeze(result) });
|
|
327
|
+
};
|
|
328
|
+
let attachments = NO_ATTACHMENTS;
|
|
329
|
+
if (submittedAttachments.length > 0) {
|
|
330
|
+
if (command.definition.input?.attachments !== true) {
|
|
331
|
+
return settle({ kind: 'error', text: `/${parsed.name} does not accept attachments` });
|
|
332
|
+
}
|
|
333
|
+
const store = this.ctx.get('attachments');
|
|
334
|
+
if (store === undefined) {
|
|
335
|
+
return settle({ kind: 'error', text: `/${parsed.name}: attachments are unavailable because no attachment store is composed` });
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
attachments = await admitCommandAttachments(store, submittedAttachments, receiptId => this.fileReceipts.resolver?.(agent, receiptId));
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (error instanceof AttachmentError) {
|
|
342
|
+
return settle({ kind: 'error', text: error.message });
|
|
343
|
+
}
|
|
344
|
+
this.settleThrown(agent.session, parsed.name, commandId, error);
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
// Cancellation must be honored BEFORE the handler runs: admission may
|
|
348
|
+
// await slow storage, and a handler entered after the caller cancelled
|
|
349
|
+
// would mutate state the retrying caller then duplicates. (The committed
|
|
350
|
+
// image objects stay unreferenced and are deferred-GC territory.)
|
|
351
|
+
const cancelledDuringAdmission = cancellationOf(signal);
|
|
352
|
+
if (cancelledDuringAdmission !== undefined) {
|
|
353
|
+
this.settleThrown(agent.session, parsed.name, commandId, cancelledDuringAdmission);
|
|
354
|
+
throw cancelledDuringAdmission;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const invocation = Object.freeze({ commandId, agent, rawInput: parsed.rawInput, attachments, signal });
|
|
358
|
+
let result;
|
|
359
|
+
try {
|
|
360
|
+
const output = command.definition.handler(invocation);
|
|
361
|
+
result = normalizeResult(parsed.name, await withAbort(Promise.resolve(output), signal));
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
this.settleThrown(agent.session, parsed.name, commandId, error);
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
return settle(result);
|
|
368
|
+
}
|
|
369
|
+
/** Contained `command/done` error append for a thrown handler or admission failure. */
|
|
370
|
+
settleThrown(session, command, commandId, error) {
|
|
371
|
+
try {
|
|
372
|
+
this.appendLifecycle(session, 'command/done', {
|
|
373
|
+
commandId, kind: 'error',
|
|
374
|
+
text: error instanceof Error ? error.message : renderThrown(error),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
catch (appendError) {
|
|
378
|
+
this.ctx.logger.warn(`command "${command}": command/done append failed: ${renderThrown(appendError)}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/** Mint the next pairing id (monotonic; instance-token-prefixed so a resumed log never repeats one). */
|
|
382
|
+
mintCommandId() {
|
|
383
|
+
this.commandSeq += 1;
|
|
384
|
+
return CommandId(`cmd-${this.instanceToken}-${this.commandSeq}`);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Append one log-only lifecycle event directly: no turn is opened for it and
|
|
388
|
+
* no flush is forced — persistence observes the eager `session/event` path
|
|
389
|
+
* and drains at ordinary checkpoints and teardown, like every other
|
|
390
|
+
* standalone plugin event.
|
|
391
|
+
*/
|
|
392
|
+
appendLifecycle(session, type, data) {
|
|
393
|
+
// Both admitted types are log-only (non-surface), but TypeScript does not
|
|
394
|
+
// reduce Session.append's conditional rest parameter through a generic
|
|
395
|
+
// type parameter. Preserve the proven two-argument call shape.
|
|
396
|
+
const appendLogOnly = session.append.bind(session);
|
|
397
|
+
return appendLogOnly(type, data);
|
|
398
|
+
}
|
|
399
|
+
/** Resolve global definitions followed by exact scoped shadows. */
|
|
400
|
+
view(agent) {
|
|
401
|
+
return this.layers.merge(agent, layer => layer.commands);
|
|
402
|
+
}
|
|
403
|
+
/** Notify every registry observer without making UI refresh load-bearing. */
|
|
404
|
+
notifyChange() {
|
|
405
|
+
// Cordis emit uses Array.map: one synchronous throw starves later listeners,
|
|
406
|
+
// and returned promises are discarded. Registry notifications are
|
|
407
|
+
// non-vetoing, so contain each callback independently.
|
|
408
|
+
for (const callback of this.ctx.events.dispatch('emit', ['commands/change'])) {
|
|
409
|
+
try {
|
|
410
|
+
const returned = callback();
|
|
411
|
+
void Promise.resolve(returned).catch((error) => {
|
|
412
|
+
this.ctx.logger.warn(`commands/change listener rejected: ${renderThrown(error)}`);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
this.ctx.logger.warn(`commands/change listener threw: ${renderThrown(error)}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
})();
|
|
422
|
+
export { CommandRuntime };
|
|
423
|
+
/** Admit a mixed command batch and restore its original image/file order. */
|
|
424
|
+
async function admitCommandAttachments(store, attachments, resolveFileReceipt) {
|
|
425
|
+
const files = new Map();
|
|
426
|
+
for (const attachment of attachments) {
|
|
427
|
+
if (attachment.type !== 'file' || files.has(attachment.receiptId))
|
|
428
|
+
continue;
|
|
429
|
+
const file = resolveFileReceipt(attachment.receiptId);
|
|
430
|
+
if (file === undefined) {
|
|
431
|
+
throw new AttachmentError('File upload receipt is unknown for this session.', 'ATTACHMENT_NOT_FOUND');
|
|
432
|
+
}
|
|
433
|
+
files.set(attachment.receiptId, file);
|
|
434
|
+
}
|
|
435
|
+
const images = [];
|
|
436
|
+
for (const attachment of attachments) {
|
|
437
|
+
if (attachment.type !== 'image')
|
|
438
|
+
continue;
|
|
439
|
+
images.push({
|
|
440
|
+
mediaType: attachment.mediaType,
|
|
441
|
+
data: attachment.data,
|
|
442
|
+
...(attachment.name === undefined ? {} : { name: attachment.name }),
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const imageRefs = images.length === 0 ? [] : await admitEncodedImages(store, images);
|
|
446
|
+
let imageIndex = 0;
|
|
447
|
+
const blocks = [];
|
|
448
|
+
for (const attachment of attachments) {
|
|
449
|
+
if (attachment.type === 'image') {
|
|
450
|
+
const ref = imageRefs[imageIndex];
|
|
451
|
+
imageIndex += 1;
|
|
452
|
+
blocks.push(Object.freeze({ type: 'image', attachment: ref }));
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
blocks.push(Object.freeze({
|
|
456
|
+
type: 'file', attachment: files.get(attachment.receiptId),
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
return Object.freeze(blocks);
|
|
460
|
+
}
|
|
461
|
+
export default CommandRuntime;
|
|
462
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable command event vocabulary and the registry's Cordis event
|
|
3
|
+
* declaration, shared with type-only consumers. Client-safe: nothing here
|
|
4
|
+
* reaches a Host-only symbol, so a Client compilation face reads the same
|
|
5
|
+
* `commands/change` signature the Host emits.
|
|
6
|
+
*
|
|
7
|
+
* @module @qilin-agent/commands/types
|
|
8
|
+
*/
|
|
9
|
+
import type { SessionSeq } from '@qilin-agent/session/types';
|
|
10
|
+
import type { CommandDefinitionId, CommandId } from './brand.ts';
|
|
11
|
+
import type { EncodedImageAttachment } from '@qilin-agent/attachment/types';
|
|
12
|
+
/** One browser-submitted command attachment: encoded image input or a staged file receipt. */
|
|
13
|
+
export type CommandSubmitAttachment = ({
|
|
14
|
+
readonly type: 'image';
|
|
15
|
+
} & EncodedImageAttachment) | {
|
|
16
|
+
readonly type: 'file';
|
|
17
|
+
readonly receiptId: string;
|
|
18
|
+
};
|
|
19
|
+
/** Immutable metadata for a command's optional unstructured input. */
|
|
20
|
+
export interface CommandInputDescriptor {
|
|
21
|
+
/** Placeholder shown before the user supplies free-form input. */
|
|
22
|
+
readonly hint: string;
|
|
23
|
+
/**
|
|
24
|
+
* Whether composer attachments may accompany an invocation. Absent or
|
|
25
|
+
* false = the executor rejects an invocation carrying attachments and capable
|
|
26
|
+
* composers refuse the submission before dispatch. A declaring command's
|
|
27
|
+
* handler receives the admitted durable blocks and owns every further
|
|
28
|
+
* grammar decision, including rejecting sub-commands that cannot use them.
|
|
29
|
+
*/
|
|
30
|
+
readonly attachments?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Expected command outcome rendered directly by the dispatching UI. */
|
|
33
|
+
export type CommandResult = {
|
|
34
|
+
readonly kind: 'success';
|
|
35
|
+
readonly text?: string;
|
|
36
|
+
/** Earlier authoritative domain event that owns a richer presentation. */
|
|
37
|
+
readonly sourceEventSeq?: SessionSeq;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: 'error';
|
|
40
|
+
readonly text: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* One settled command execution: the handler's normalized result plus the
|
|
44
|
+
* lifecycle pairing id minted for its `command/run`/`command/done` records,
|
|
45
|
+
* so a dispatching surface can correlate the Remote acknowledgment with the
|
|
46
|
+
* flow node those events produce.
|
|
47
|
+
*/
|
|
48
|
+
export interface CommandExecution {
|
|
49
|
+
/** Pairing id carried by this execution's lifecycle events. */
|
|
50
|
+
readonly commandId: CommandId;
|
|
51
|
+
/** The handler's normalized outcome. */
|
|
52
|
+
readonly result: CommandResult;
|
|
53
|
+
}
|
|
54
|
+
/** Handler-free immutable command view returned to UI adapters. */
|
|
55
|
+
export interface CommandDescriptor {
|
|
56
|
+
/** Stable plugin-owned identity; absent for definitions without identity-based client behavior. */
|
|
57
|
+
readonly definitionId?: CommandDefinitionId;
|
|
58
|
+
/** Lowercase command name without the leading slash. */
|
|
59
|
+
readonly name: string;
|
|
60
|
+
/** Human-readable summary used in discovery UI. */
|
|
61
|
+
readonly description: string;
|
|
62
|
+
/** Optional free-form input hint advertised to capable clients. */
|
|
63
|
+
readonly input?: CommandInputDescriptor;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Producer record for one command invocation (the `command/run` event's
|
|
67
|
+
* source slot). Merge-extensible sum type mirroring `MessageSourceMap`'s
|
|
68
|
+
* shape; minimal because every executor caller is a human-facing UI
|
|
69
|
+
* surface dispatching a human-typed line, so the sole variant is `user`.
|
|
70
|
+
*/
|
|
71
|
+
export interface CommandSourceMap {
|
|
72
|
+
user: {
|
|
73
|
+
kind: 'user';
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** The union over {@link CommandSourceMap} — who issued a command line. */
|
|
77
|
+
export type CommandSource = CommandSourceMap[keyof CommandSourceMap];
|
|
78
|
+
declare module '@qilin-agent/kylin' {
|
|
79
|
+
interface Events {
|
|
80
|
+
/**
|
|
81
|
+
* A command was registered or unregistered. This is an unfiltered registry
|
|
82
|
+
* notification because a global or scoped change may affect any UI view.
|
|
83
|
+
* Observer failures are contained and cannot veto the registry mutation.
|
|
84
|
+
* @mode emit
|
|
85
|
+
*/
|
|
86
|
+
'commands/change'(): void;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
declare module '@qilin-agent/session/types' {
|
|
90
|
+
interface SessionEventMap {
|
|
91
|
+
/**
|
|
92
|
+
* A resolved slash command entered its handler. Log-only (never model
|
|
93
|
+
* surface); paired with `command/done` by `commandId`, mirroring the
|
|
94
|
+
* `tool/call`↔`tool/result` pairing. The payload is structured — `name`
|
|
95
|
+
* and `args` are `parseCommand`'s own split (name and verbatim rawInput,
|
|
96
|
+
* separator whitespace included), so a consumer (a projection unit
|
|
97
|
+
* folding its own command records, a rich command card) never re-parses
|
|
98
|
+
* a line. `args` is absent when the definition sets `recordInput: false`
|
|
99
|
+
* because an authoritative domain event owns the input payload.
|
|
100
|
+
*/
|
|
101
|
+
'command/run': {
|
|
102
|
+
commandId: CommandId;
|
|
103
|
+
name: string;
|
|
104
|
+
args?: string;
|
|
105
|
+
source: CommandSource;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The paired command settled. `kind`/`text` carry the handler's verbatim
|
|
109
|
+
* outcome (a thrown/aborted handler settles as `kind: 'error'` with the
|
|
110
|
+
* rendered failure). A successful command may identify the earlier
|
|
111
|
+
* authoritative domain event for a richer client-computed presentation.
|
|
112
|
+
*/
|
|
113
|
+
'command/done': {
|
|
114
|
+
commandId: CommandId;
|
|
115
|
+
kind: 'success' | 'error';
|
|
116
|
+
text?: string;
|
|
117
|
+
sourceEventSeq?: import('@qilin-agent/session/types').SessionSeq;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable command event vocabulary and the registry's Cordis event
|
|
3
|
+
* declaration, shared with type-only consumers. Client-safe: nothing here
|
|
4
|
+
* reaches a Host-only symbol, so a Client compilation face reads the same
|
|
5
|
+
* `commands/change` signature the Host emits.
|
|
6
|
+
*
|
|
7
|
+
* @module @qilin-agent/commands/types
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/commands",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Plugin-owned human command registry for QiLin UIs",
|
|
4
|
+
"version": "3.1.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kkutysllb/QiLin.git",
|
|
11
|
+
"directory": "packages/interaction/commands"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./types": {
|
|
25
|
+
"types": "./lib/types/types.d.ts",
|
|
26
|
+
"default": "./lib/types/types.js"
|
|
27
|
+
},
|
|
28
|
+
"./brand": {
|
|
29
|
+
"types": "./lib/types/brand.d.ts",
|
|
30
|
+
"default": "./lib/types/brand.js"
|
|
31
|
+
},
|
|
32
|
+
"./typert": {
|
|
33
|
+
"types": "./lib/typert.host.d.ts",
|
|
34
|
+
"default": "./lib/typert.host.js"
|
|
35
|
+
},
|
|
36
|
+
"./remote": {
|
|
37
|
+
"types": "./lib/typert.remote-client.d.ts",
|
|
38
|
+
"default": "./lib/typert.remote-client.js"
|
|
39
|
+
},
|
|
40
|
+
"./src/*": "./src/*",
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"lib/index.js",
|
|
45
|
+
"lib/types/**/*.js",
|
|
46
|
+
"lib/types/**/*.d.ts",
|
|
47
|
+
"lib/typert.host.js",
|
|
48
|
+
"lib/typert.host.d.ts",
|
|
49
|
+
"lib/typert.remote-client.js",
|
|
50
|
+
"lib/typert.remote-client.d.ts"
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@qilin-agent/attachment": "3.1.3",
|
|
55
|
+
"@qilin-agent/brand": "3.1.3",
|
|
56
|
+
"@qilin-agent/llm": "3.1.3",
|
|
57
|
+
"@qilin-agent/agent": "3.1.3",
|
|
58
|
+
"@qilin-agent/scope": "3.1.3",
|
|
59
|
+
"@qilin-agent/session": "3.1.3",
|
|
60
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
61
|
+
"@qilin-agent/typert-protocol": "3.1.3"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"zod": "^4.4.3",
|
|
65
|
+
"@qilin-agent/util-crypto": "3.1.3"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@qilin-agent/agent": "3.1.3",
|
|
69
|
+
"@qilin-agent/brand": "3.1.3",
|
|
70
|
+
"@qilin-agent/attachment": "3.1.3",
|
|
71
|
+
"@qilin-agent/llm": "3.1.3",
|
|
72
|
+
"@qilin-agent/scope": "3.1.3",
|
|
73
|
+
"@qilin-agent/session": "3.1.3",
|
|
74
|
+
"@qilin-agent/typert-protocol": "3.1.3",
|
|
75
|
+
"@qilin-agent/kylin": "~4.0.3"
|
|
76
|
+
}
|
|
6
77
|
}
|