@dolusoft/command-console-core 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/dist/command-console-core.js +517 -28
- package/dist/command-console-core.js.map +1 -1
- package/dist/types/core/console/console.types.d.ts +28 -2
- package/dist/types/core/dispatch/commandError.d.ts +24 -0
- package/dist/types/core/dispatch/dispatch.types.d.ts +28 -1
- package/dist/types/core/dispatch/dispatch.utils.d.ts +4 -3
- package/dist/types/core/dispatch/operation.d.ts +23 -0
- package/dist/types/helpers/agentTools.d.ts +34 -0
- package/dist/types/index.d.ts +6 -2
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { quoteArgument, tokenize } from "@dolusoft/command-protocol";
|
|
1
|
+
import { checkInputSchema, envelopeBytes, envelopeMaxBytes, formatCommandLine, parseEnvelope, parseOptions, quoteArgument, tokenize, validateInput } from "@dolusoft/command-protocol";
|
|
2
2
|
// src/shared/registry.ts
|
|
3
3
|
var createHooks = () => ({
|
|
4
4
|
lineSubmitted: [],
|
|
@@ -316,8 +316,9 @@ var checkName = (name, where) => {
|
|
|
316
316
|
/**
|
|
317
317
|
* Validates the commands (core README: Options, C-56): names and aliases are
|
|
318
318
|
* non-empty, without whitespace or quotes, unique across every name and alias;
|
|
319
|
-
* `fromArg` is a non-negative integer; `run` is a function
|
|
320
|
-
*
|
|
319
|
+
* `fromArg` is a non-negative integer; `run` is a function; `input` is an
|
|
320
|
+
* input schema, `danger` a known value and `normalize` a function (C-76).
|
|
321
|
+
* Throws on the first violation.
|
|
321
322
|
*/
|
|
322
323
|
var validateCommands = (commands) => {
|
|
323
324
|
if (!Array.isArray(commands)) throw new Error("commands must be an array of command definitions.");
|
|
@@ -332,8 +333,24 @@ var validateCommands = (commands) => {
|
|
|
332
333
|
const { sensitive } = command;
|
|
333
334
|
if (sensitive !== void 0 && typeof sensitive !== "boolean" && !(typeof sensitive === "object" && sensitive !== null && Number.isInteger(sensitive.fromArg) && sensitive.fromArg >= 0)) throw new Error(`command ${command.name}: sensitive must be a boolean or { fromArg } with a non-negative integer.`);
|
|
334
335
|
if (typeof command.run !== "function") throw new Error(`command ${command.name}: run must be a function.`);
|
|
336
|
+
checkDescription(command);
|
|
335
337
|
}
|
|
336
338
|
};
|
|
339
|
+
var dangers = [
|
|
340
|
+
"read",
|
|
341
|
+
"change",
|
|
342
|
+
"irreversible"
|
|
343
|
+
];
|
|
344
|
+
/** The checks of C-76: `input`, `danger` and `normalize`. */
|
|
345
|
+
var checkDescription = (command) => {
|
|
346
|
+
if (command.input !== void 0) try {
|
|
347
|
+
checkInputSchema(command.input);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
throw new TypeError(`command ${command.name}: ${error.message}`, { cause: error });
|
|
350
|
+
}
|
|
351
|
+
if (command.danger !== void 0 && !dangers.includes(command.danger)) throw new Error(`command ${command.name}: danger must be 'read', 'change' or 'irreversible'.`);
|
|
352
|
+
if (command.normalize !== void 0 && typeof command.normalize !== "function") throw new Error(`command ${command.name}: normalize must be a function.`);
|
|
353
|
+
};
|
|
337
354
|
/** The JSON spec of a command, without its functions (`getCommands`). */
|
|
338
355
|
var specOf = (command) => {
|
|
339
356
|
const spec = {
|
|
@@ -343,6 +360,8 @@ var specOf = (command) => {
|
|
|
343
360
|
if (command.summary !== void 0) spec.summary = command.summary;
|
|
344
361
|
if (command.usage !== void 0) spec.usage = command.usage;
|
|
345
362
|
if (command.sensitive !== void 0) spec.sensitive = typeof command.sensitive === "object" ? { fromArg: command.sensitive.fromArg } : command.sensitive;
|
|
363
|
+
if (command.input !== void 0) spec.input = JSON.parse(JSON.stringify(command.input));
|
|
364
|
+
if (command.danger !== void 0) spec.danger = command.danger;
|
|
346
365
|
return spec;
|
|
347
366
|
};
|
|
348
367
|
/** A command's return as a response (README: Running a line, End action). */
|
|
@@ -361,6 +380,73 @@ var blocksOf = (output) => {
|
|
|
361
380
|
};
|
|
362
381
|
/** Whether `value` is a promise or another thenable. */
|
|
363
382
|
var isThenable = (value) => (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
383
|
+
// src/core/dispatch/operation.ts
|
|
384
|
+
/** How many closed envelope ids a console remembers (C-81). */
|
|
385
|
+
var closedLimit = 500;
|
|
386
|
+
/** The longest `summary` an outcome carries, in code points (C-82). */
|
|
387
|
+
var summaryLimit = 200;
|
|
388
|
+
/** Whether two JSON values are equal, objects compared by their keys. */
|
|
389
|
+
var sameJson = (a, b) => {
|
|
390
|
+
if (a === b) return true;
|
|
391
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null || Array.isArray(a) !== Array.isArray(b)) return false;
|
|
392
|
+
const left = Object.keys(a);
|
|
393
|
+
const right = b;
|
|
394
|
+
return left.length === Object.keys(right).length && left.every((key) => Object.hasOwn(right, key) && sameJson(a[key], right[key]));
|
|
395
|
+
};
|
|
396
|
+
/** Remembers `id` among the newest `closedLimit` closed ids. */
|
|
397
|
+
var rememberClosed = (closed, id) => {
|
|
398
|
+
closed.delete(id);
|
|
399
|
+
closed.add(id);
|
|
400
|
+
if (closed.size > closedLimit) closed.delete(closed.values().next().value);
|
|
401
|
+
};
|
|
402
|
+
/** `summary` cut to `summaryLimit` code points; `undefined` unless a string. */
|
|
403
|
+
var cutSummary = (summary) => typeof summary === "string" ? Array.from(summary).slice(0, summaryLimit).join("") : void 0;
|
|
404
|
+
/** An RFC 4122 version 4 UUID from 16 random bytes. */
|
|
405
|
+
var uuidV4 = (bytes) => {
|
|
406
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
407
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
408
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
|
|
409
|
+
return [
|
|
410
|
+
hex.slice(0, 4),
|
|
411
|
+
hex.slice(4, 6),
|
|
412
|
+
hex.slice(6, 8),
|
|
413
|
+
hex.slice(8, 10),
|
|
414
|
+
hex.slice(10, 16)
|
|
415
|
+
].map((part) => part.join("")).join("-");
|
|
416
|
+
};
|
|
417
|
+
/**
|
|
418
|
+
* The page's UUID function: `crypto.randomUUID`, else a v4 UUID from
|
|
419
|
+
* `crypto.getRandomValues` (a page on an insecure origin has only the
|
|
420
|
+
* latter), else none.
|
|
421
|
+
*/
|
|
422
|
+
var randomUUID = () => {
|
|
423
|
+
const crypto = globalThis.crypto;
|
|
424
|
+
if (typeof crypto?.randomUUID === "function") return () => crypto.randomUUID();
|
|
425
|
+
if (typeof crypto?.getRandomValues === "function") return () => uuidV4(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)));
|
|
426
|
+
};
|
|
427
|
+
/**
|
|
428
|
+
* What makes envelope ids: `createEnvelopeId`, else the page's UUID function.
|
|
429
|
+
* Throws when a gate is given and neither exists (C-78).
|
|
430
|
+
*/
|
|
431
|
+
var envelopeIds = (options) => {
|
|
432
|
+
const create = options.createEnvelopeId ?? randomUUID();
|
|
433
|
+
if (options.gate !== void 0 && create === void 0) throw new Error("A console with a gate needs crypto.randomUUID, crypto.getRandomValues or a createEnvelopeId option.");
|
|
434
|
+
return create;
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* Checks the gate options at creation and on every `setOptions` (C-78):
|
|
438
|
+
* envelope ids can be made, and `echoTimeoutMs` is a finite number above 0.
|
|
439
|
+
*/
|
|
440
|
+
var checkGateOptions = (options) => {
|
|
441
|
+
envelopeIds(options);
|
|
442
|
+
const ms = options.echoTimeoutMs;
|
|
443
|
+
if (ms !== void 0 && !(typeof ms === "number" && Number.isFinite(ms) && ms > 0)) throw new TypeError(`echoTimeoutMs must be a finite number above 0 (got ${typeof ms === "number" ? ms : typeof ms}).`);
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Whether a command can travel in an envelope: it has `input` and `danger`
|
|
447
|
+
* and no `sensitive` (C-79).
|
|
448
|
+
*/
|
|
449
|
+
var describedCommand = (command) => command.input !== void 0 && command.danger !== void 0 && (command.sensitive === void 0 || command.sensitive === false);
|
|
364
450
|
// src/core/console/consoleFeature.ts
|
|
365
451
|
var controllable$1 = [
|
|
366
452
|
"entries",
|
|
@@ -420,7 +506,9 @@ var consoleFeature = {
|
|
|
420
506
|
const previous = record.options;
|
|
421
507
|
const given = resolveUpdater(updater, previous);
|
|
422
508
|
if (!sameFeatures(previous.features, given.features)) throw new Error("setOptions cannot change the feature set.");
|
|
509
|
+
if (given.gate !== previous.gate) throw new Error("setOptions cannot change the gate.");
|
|
423
510
|
validateCommands(given.commands);
|
|
511
|
+
checkGateOptions(given);
|
|
424
512
|
store.action(() => {
|
|
425
513
|
const before = { ...store.read() };
|
|
426
514
|
record.options = mergeOptions(featureDefaults.get(console), given);
|
|
@@ -489,6 +577,37 @@ var consoleFeature = {
|
|
|
489
577
|
});
|
|
490
578
|
}
|
|
491
579
|
};
|
|
580
|
+
// src/core/dispatch/commandError.ts
|
|
581
|
+
var commandCodes = [
|
|
582
|
+
"target-not-found",
|
|
583
|
+
"invalid-args",
|
|
584
|
+
"failed"
|
|
585
|
+
];
|
|
586
|
+
/**
|
|
587
|
+
* Thrown (or rejected) by a command's `run` to end the run with `code`: the
|
|
588
|
+
* run fails as with any error (`onError`, the error entry), and the outcome a
|
|
589
|
+
* `request()` resolves is `{ status: 'error', code }`.
|
|
590
|
+
*/
|
|
591
|
+
var CommandError = class extends Error {
|
|
592
|
+
/** The code the run's outcome carries. */
|
|
593
|
+
code;
|
|
594
|
+
constructor(code, message, options) {
|
|
595
|
+
super(message ?? code, options);
|
|
596
|
+
this.name = "CommandError";
|
|
597
|
+
this.code = code;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
/**
|
|
601
|
+
* The outcome code of an error a command's `run` threw: a `CommandError`'s
|
|
602
|
+
* code when a command may give it, else `failed` (C-86). The error is known
|
|
603
|
+
* by its `name`, not by `instanceof`: a command built against a second copy
|
|
604
|
+
* of the core throws a `CommandError` of another class.
|
|
605
|
+
*/
|
|
606
|
+
var commandErrorCode = (error) => {
|
|
607
|
+
if (!(error instanceof Error) || error.name !== "CommandError") return "failed";
|
|
608
|
+
const { code } = error;
|
|
609
|
+
return commandCodes.includes(code) ? code : "failed";
|
|
610
|
+
};
|
|
492
611
|
// src/core/dispatch/sensitivity.ts
|
|
493
612
|
/**
|
|
494
613
|
* The first secret argument position of `command`: `0` for
|
|
@@ -726,14 +845,31 @@ var output = (tone, blocks, inputId, quiet) => quiet === true ? {
|
|
|
726
845
|
blocks,
|
|
727
846
|
inputId
|
|
728
847
|
};
|
|
729
|
-
|
|
848
|
+
var failure = (code) => ({
|
|
849
|
+
status: "error",
|
|
850
|
+
code
|
|
851
|
+
});
|
|
852
|
+
/**
|
|
853
|
+
* Clears `running` when it still names `run`, cancels the echo timer,
|
|
854
|
+
* records the envelope id as closed and resolves the run's promises.
|
|
855
|
+
*/
|
|
730
856
|
var cleanup = (console, run) => {
|
|
731
857
|
if (run.cleaned) return;
|
|
732
858
|
run.cleaned = true;
|
|
859
|
+
run.waiting = false;
|
|
733
860
|
const state = stateOf(console);
|
|
734
861
|
if (state.run === run) state.run = null;
|
|
862
|
+
if (run.envelope) rememberClosed(state.closed, run.envelope.id);
|
|
863
|
+
const cancelTimer = run.cancelTimer;
|
|
864
|
+
run.cancelTimer = null;
|
|
865
|
+
if (cancelTimer) try {
|
|
866
|
+
cancelTimer();
|
|
867
|
+
} catch (error) {
|
|
868
|
+
reportCallbackError(console, error);
|
|
869
|
+
}
|
|
735
870
|
recordOf(console)?.store.update("running", (running) => running?.runId === run.id ? null : running);
|
|
736
871
|
run.resolve(true);
|
|
872
|
+
run.onOutcome?.(run.outcome);
|
|
737
873
|
};
|
|
738
874
|
/** Effects in order, then the output (README: Running a line, B-29). */
|
|
739
875
|
var apply = (console, run, response, defaultTone) => {
|
|
@@ -742,16 +878,28 @@ var apply = (console, run, response, defaultTone) => {
|
|
|
742
878
|
const blocks = blocksOf(response.output);
|
|
743
879
|
if (blocks.length > 0) addEntry(console, output(response.tone ?? defaultTone, blocks, run.inputId, response.quiet));
|
|
744
880
|
};
|
|
881
|
+
/** Ends `run` with what it returned; an `error` tone is `failed` (C-87). */
|
|
745
882
|
var fulfil = (console, run, value) => {
|
|
746
883
|
run.current = false;
|
|
747
884
|
try {
|
|
748
|
-
|
|
885
|
+
const response = normalizeReturn(value);
|
|
886
|
+
const summary = cutSummary(response.summary);
|
|
887
|
+
run.outcome = response.tone === "error" ? failure("failed") : summary === void 0 ? { status: "ok" } : {
|
|
888
|
+
status: "ok",
|
|
889
|
+
summary
|
|
890
|
+
};
|
|
891
|
+
apply(console, run, response, "default");
|
|
749
892
|
} finally {
|
|
750
893
|
cleanup(console, run);
|
|
751
894
|
}
|
|
752
895
|
};
|
|
753
|
-
|
|
896
|
+
/**
|
|
897
|
+
* Ends `run` as failed (C-18); `code` is the outcome's code, `failed` unless
|
|
898
|
+
* the command's own `run` threw a `CommandError` (C-86).
|
|
899
|
+
*/
|
|
900
|
+
var fail = (console, run, error, code = "failed") => {
|
|
754
901
|
run.current = false;
|
|
902
|
+
run.outcome = failure(code);
|
|
755
903
|
try {
|
|
756
904
|
const { store, options } = liveRecord(console);
|
|
757
905
|
store.afterCommit(() => reportError(recordOf(console)?.options.onError, error, run.info));
|
|
@@ -768,11 +916,27 @@ var fail = (console, run, error) => {
|
|
|
768
916
|
}
|
|
769
917
|
};
|
|
770
918
|
/** Ends `run` without output and without an entry (`false`, reset, dispose). */
|
|
771
|
-
var stop = (console, run, abort) => {
|
|
919
|
+
var stop = (console, run, abort, code) => {
|
|
772
920
|
run.current = false;
|
|
921
|
+
run.outcome = failure(code);
|
|
773
922
|
if (abort) run.controller.abort();
|
|
774
923
|
cleanup(console, run);
|
|
775
924
|
};
|
|
925
|
+
/**
|
|
926
|
+
* Ends an operation that did not run its command with an `operation-failed`
|
|
927
|
+
* entry (C-79, C-80); `error` goes to `onError` with the run's info.
|
|
928
|
+
*/
|
|
929
|
+
var close$1 = (console, run, code, error) => {
|
|
930
|
+
run.current = false;
|
|
931
|
+
run.outcome = failure(code);
|
|
932
|
+
run.controller.abort();
|
|
933
|
+
try {
|
|
934
|
+
if (error) liveRecord(console).store.afterCommit(() => reportError(recordOf(console)?.options.onError, error, run.info));
|
|
935
|
+
addEntry(console, output("error", [message("operation-failed", { code })], run.inputId));
|
|
936
|
+
} finally {
|
|
937
|
+
cleanup(console, run);
|
|
938
|
+
}
|
|
939
|
+
};
|
|
776
940
|
/** Runs `fn` as its own action if `run` is still current (a settle). */
|
|
777
941
|
var settle = (console, run, fn) => {
|
|
778
942
|
if (!run.current || isDisposed(console)) return;
|
|
@@ -781,16 +945,21 @@ var settle = (console, run, fn) => {
|
|
|
781
945
|
if (run.current) fn();
|
|
782
946
|
});
|
|
783
947
|
};
|
|
784
|
-
/**
|
|
785
|
-
|
|
786
|
-
|
|
948
|
+
/**
|
|
949
|
+
* Handles a return that may be a promise: now, or in its own action. A
|
|
950
|
+
* rejection fails the run with the code `codeOf` gives it.
|
|
951
|
+
*/
|
|
952
|
+
var whenSettled = (console, run, value, onValue, codeOf = () => "failed") => {
|
|
953
|
+
if (isThenable(value)) Promise.resolve(value).then((result) => settle(console, run, () => onValue(result)), (error) => settle(console, run, () => fail(console, run, error, codeOf(error))));
|
|
787
954
|
else onValue(value);
|
|
788
955
|
};
|
|
956
|
+
var toRunning = (console, run) => liveRecord(console).store.update("running", (running) => running?.runId === run.id ? {
|
|
957
|
+
...running,
|
|
958
|
+
phase: "running"
|
|
959
|
+
} : running);
|
|
960
|
+
/** Calls the command's `run`. */
|
|
789
961
|
var go = (console, run) => {
|
|
790
|
-
|
|
791
|
-
...running,
|
|
792
|
-
phase: "running"
|
|
793
|
-
} : running);
|
|
962
|
+
toRunning(console, run);
|
|
794
963
|
const context = {
|
|
795
964
|
runId: run.id,
|
|
796
965
|
inputId: run.inputId,
|
|
@@ -801,10 +970,10 @@ var go = (console, run) => {
|
|
|
801
970
|
try {
|
|
802
971
|
result = run.command.run(run.request, context);
|
|
803
972
|
} catch (error) {
|
|
804
|
-
fail(console, run, error);
|
|
973
|
+
fail(console, run, error, commandErrorCode(error));
|
|
805
974
|
return;
|
|
806
975
|
}
|
|
807
|
-
whenSettled(console, run, result, (value) => fulfil(console, run, value));
|
|
976
|
+
whenSettled(console, run, result, (value) => fulfil(console, run, value), commandErrorCode);
|
|
808
977
|
};
|
|
809
978
|
var write$1 = (console, run, out, tone) => {
|
|
810
979
|
settle(console, run, () => {
|
|
@@ -812,7 +981,106 @@ var write$1 = (console, run, out, tone) => {
|
|
|
812
981
|
if (blocks.length > 0) addEntry(console, output(tone ?? "default", blocks, run.inputId));
|
|
813
982
|
});
|
|
814
983
|
};
|
|
815
|
-
|
|
984
|
+
/** What the gate delivered: the echo of the pending envelope, or nothing. */
|
|
985
|
+
var receive = (console, raw) => {
|
|
986
|
+
const record = recordOf(console);
|
|
987
|
+
const gate = record?.options.gate;
|
|
988
|
+
if (!record || !gate) return;
|
|
989
|
+
const parsed = parseEnvelope(raw);
|
|
990
|
+
if (!parsed.ok) return;
|
|
991
|
+
const echo = parsed.envelope;
|
|
992
|
+
const state = stateOf(console);
|
|
993
|
+
const run = state.run;
|
|
994
|
+
if (echo.target.tab !== gate.tab || echo.from.tab !== echo.target.tab || state.closed.has(echo.id) || !run?.waiting || run.envelope?.id !== echo.id) return;
|
|
995
|
+
settle(console, run, () => {
|
|
996
|
+
if (!run.waiting) return;
|
|
997
|
+
const local = run.envelope;
|
|
998
|
+
run.waiting = false;
|
|
999
|
+
if (echo.command === local.command && sameJson(echo.args, local.args) && sameJson(echo.target, local.target)) go(console, run);
|
|
1000
|
+
else close$1(console, run, "id-conflict", /* @__PURE__ */ new Error(`The echo of operation ${local.id} differs from its record.`));
|
|
1001
|
+
});
|
|
1002
|
+
};
|
|
1003
|
+
/**
|
|
1004
|
+
* Sends the run's envelope and waits for its echo (C-79): the timer and the
|
|
1005
|
+
* send are the gate's calls, made after the action's commit.
|
|
1006
|
+
*/
|
|
1007
|
+
var send = (console, run) => {
|
|
1008
|
+
const { store, options } = liveRecord(console);
|
|
1009
|
+
const gate = options.gate;
|
|
1010
|
+
toRunning(console, run);
|
|
1011
|
+
let envelope;
|
|
1012
|
+
try {
|
|
1013
|
+
const id = envelopeIds(options)();
|
|
1014
|
+
if (typeof id !== "string" || id === "" || stateOf(console).closed.has(id)) throw new Error(`createEnvelopeId returned ${id}, an id already used.`);
|
|
1015
|
+
const { view } = run.request;
|
|
1016
|
+
envelope = {
|
|
1017
|
+
v: 1,
|
|
1018
|
+
id,
|
|
1019
|
+
kind: "request",
|
|
1020
|
+
actor: run.actor,
|
|
1021
|
+
from: { tab: gate.tab },
|
|
1022
|
+
target: view === void 0 ? { tab: gate.tab } : {
|
|
1023
|
+
tab: gate.tab,
|
|
1024
|
+
view
|
|
1025
|
+
},
|
|
1026
|
+
command: run.command.name,
|
|
1027
|
+
args: run.request.input
|
|
1028
|
+
};
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
fail(console, run, error);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (envelopeBytes(envelope) > envelopeMaxBytes) {
|
|
1034
|
+
close$1(console, run, "too-large");
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
run.envelope = envelope;
|
|
1038
|
+
run.waiting = true;
|
|
1039
|
+
const ended = (code) => () => settle(console, run, () => {
|
|
1040
|
+
if (run.waiting) close$1(console, run, code);
|
|
1041
|
+
});
|
|
1042
|
+
store.afterCommit(() => {
|
|
1043
|
+
const state = stateOf(console);
|
|
1044
|
+
if (!run.waiting) return;
|
|
1045
|
+
try {
|
|
1046
|
+
if (state.unsubscribe === null) state.unsubscribe = gate.subscribe((raw) => receive(console, raw));
|
|
1047
|
+
run.cancelTimer = gate.schedule(options.echoTimeoutMs ?? 5e3, ended("timeout"));
|
|
1048
|
+
const copy = JSON.parse(JSON.stringify(envelope));
|
|
1049
|
+
Promise.resolve(gate.send(copy)).then(void 0, ended("connection-lost"));
|
|
1050
|
+
} catch {
|
|
1051
|
+
ended("connection-lost")();
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
};
|
|
1055
|
+
/** After `beforeRun` lets the run go: the gate, or the command at once. */
|
|
1056
|
+
var proceed = (console, run) => {
|
|
1057
|
+
if (run.gated) send(console, run);
|
|
1058
|
+
else go(console, run);
|
|
1059
|
+
};
|
|
1060
|
+
/**
|
|
1061
|
+
* Applies the plan of a command with `input` (C-77): normalizes and checks
|
|
1062
|
+
* the value, and completes the request from its canonical line. Throws on a
|
|
1063
|
+
* `normalize` or `activeView` that throws, or a value the schema refuses.
|
|
1064
|
+
*/
|
|
1065
|
+
var prepare = (run, plan) => {
|
|
1066
|
+
const { command } = run;
|
|
1067
|
+
const schema = command.input;
|
|
1068
|
+
const view = plan.view();
|
|
1069
|
+
const mapped = command.normalize ? command.normalize(plan.value) : plan.value;
|
|
1070
|
+
const checked = validateInput(schema, mapped);
|
|
1071
|
+
if (!checked.ok) throw new TypeError(`command ${command.name}: normalize returned an input the schema refuses.`);
|
|
1072
|
+
const line = formatCommandLine(command.name, checked.value, schema);
|
|
1073
|
+
const tokens = tokenize(line).tokens;
|
|
1074
|
+
run.request = {
|
|
1075
|
+
...run.request,
|
|
1076
|
+
args: tokens.slice(1).map((token) => token.value),
|
|
1077
|
+
rest: tokens.length > 1 ? line.slice(tokens[1].start) : "",
|
|
1078
|
+
line,
|
|
1079
|
+
input: checked.value
|
|
1080
|
+
};
|
|
1081
|
+
if (typeof view === "string" && view !== "") run.request.view = view;
|
|
1082
|
+
};
|
|
1083
|
+
var start = (console, command, request, inputId, sensitive, plan, onOutcome) => {
|
|
816
1084
|
const { store, options } = liveRecord(console);
|
|
817
1085
|
const provider = contextOf(console);
|
|
818
1086
|
const frames = provider?.snapshot() ?? [];
|
|
@@ -839,9 +1107,16 @@ var start = (console, command, request, inputId, sensitive) => {
|
|
|
839
1107
|
command,
|
|
840
1108
|
controller: new AbortController(),
|
|
841
1109
|
contextRevision: provider?.revision() ?? null,
|
|
1110
|
+
gated: plan !== null && options.gate !== void 0 && !sensitive && describedCommand(command),
|
|
1111
|
+
actor: plan?.actor ?? "user",
|
|
842
1112
|
current: true,
|
|
843
1113
|
cleaned: false,
|
|
844
|
-
|
|
1114
|
+
envelope: null,
|
|
1115
|
+
waiting: false,
|
|
1116
|
+
cancelTimer: null,
|
|
1117
|
+
outcome: failure("cancelled"),
|
|
1118
|
+
resolve,
|
|
1119
|
+
onOutcome
|
|
845
1120
|
};
|
|
846
1121
|
stateOf(console).run = run;
|
|
847
1122
|
const { beforeRun } = options;
|
|
@@ -851,8 +1126,14 @@ var start = (console, command, request, inputId, sensitive) => {
|
|
|
851
1126
|
inputId,
|
|
852
1127
|
phase: beforeRun ? "authorizing" : "running"
|
|
853
1128
|
}));
|
|
1129
|
+
if (plan) try {
|
|
1130
|
+
prepare(run, plan);
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
fail(console, run, error);
|
|
1133
|
+
return done;
|
|
1134
|
+
}
|
|
854
1135
|
if (!beforeRun) {
|
|
855
|
-
|
|
1136
|
+
proceed(console, run);
|
|
856
1137
|
return done;
|
|
857
1138
|
}
|
|
858
1139
|
let answer;
|
|
@@ -863,8 +1144,8 @@ var start = (console, command, request, inputId, sensitive) => {
|
|
|
863
1144
|
return done;
|
|
864
1145
|
}
|
|
865
1146
|
whenSettled(console, run, answer, (value) => {
|
|
866
|
-
if (value === void 0)
|
|
867
|
-
else if (value === false) stop(console, run, false);
|
|
1147
|
+
if (value === void 0) proceed(console, run);
|
|
1148
|
+
else if (value === false) stop(console, run, false, "not-allowed");
|
|
868
1149
|
else fulfil(console, run, value);
|
|
869
1150
|
});
|
|
870
1151
|
return done;
|
|
@@ -912,13 +1193,87 @@ var submitLine = (console) => {
|
|
|
912
1193
|
addEntry(console, output("error", [message("command-not-found", { name: sensitive ? null : first.value })], inputId));
|
|
913
1194
|
return Promise.resolve(true);
|
|
914
1195
|
}
|
|
1196
|
+
if (command.danger === "irreversible") {
|
|
1197
|
+
addEntry(console, output("error", [message("operation-failed", { code: "not-allowed" })], inputId));
|
|
1198
|
+
return Promise.resolve(true);
|
|
1199
|
+
}
|
|
1200
|
+
const args = parsed.tokens.slice(1).map((token) => token.value);
|
|
1201
|
+
let plan = null;
|
|
1202
|
+
if (command.input) {
|
|
1203
|
+
const read = parseOptions(args, command.input);
|
|
1204
|
+
if (!read.ok) {
|
|
1205
|
+
const [error] = read.errors;
|
|
1206
|
+
addEntry(console, output("error", [message("invalid-args", {
|
|
1207
|
+
path: sensitive ? null : error.path,
|
|
1208
|
+
reason: error.code
|
|
1209
|
+
})], inputId));
|
|
1210
|
+
return Promise.resolve(true);
|
|
1211
|
+
}
|
|
1212
|
+
plan = {
|
|
1213
|
+
value: read.value,
|
|
1214
|
+
view: () => liveRecord(console).options.activeView?.(),
|
|
1215
|
+
actor: "user"
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
915
1218
|
return start(console, command, {
|
|
916
1219
|
name: first.value,
|
|
917
1220
|
command: command.name,
|
|
918
|
-
args
|
|
1221
|
+
args,
|
|
919
1222
|
rest: second ? trimmed.slice(second.start) : "",
|
|
920
1223
|
line: trimmed
|
|
921
|
-
}, inputId, sensitive);
|
|
1224
|
+
}, inputId, sensitive, plan);
|
|
1225
|
+
};
|
|
1226
|
+
/**
|
|
1227
|
+
* `request()` in the open action (C-82): every refusal before the line is
|
|
1228
|
+
* accepted leaves no entry.
|
|
1229
|
+
*/
|
|
1230
|
+
var requestLine = (console, input) => {
|
|
1231
|
+
const refuse = (code) => Promise.resolve(failure(code));
|
|
1232
|
+
const { store } = liveRecord(console);
|
|
1233
|
+
if (store.read().running !== null) return refuse("busy");
|
|
1234
|
+
const lookup = lookupOf(console);
|
|
1235
|
+
let command;
|
|
1236
|
+
let tokens = null;
|
|
1237
|
+
let view;
|
|
1238
|
+
if ("line" in input) {
|
|
1239
|
+
const parsed = tokenize(String(input.line).trim());
|
|
1240
|
+
if (!parsed.ok || parsed.tokens.length === 0) return refuse("invalid-args");
|
|
1241
|
+
command = lookup.find(parsed.tokens[0].value);
|
|
1242
|
+
tokens = parsed.tokens.slice(1).map((token) => token.value);
|
|
1243
|
+
view = () => liveRecord(console).options.activeView?.();
|
|
1244
|
+
} else {
|
|
1245
|
+
command = typeof input.command === "string" ? lookup.find(input.command) : void 0;
|
|
1246
|
+
const given = typeof input.view === "string" ? input.view : void 0;
|
|
1247
|
+
view = () => given;
|
|
1248
|
+
}
|
|
1249
|
+
if (!command || command.hidden === true) return refuse("unknown-command");
|
|
1250
|
+
if (!describedCommand(command) || command.danger !== "read" || input.actor !== "external-agent") return refuse("not-allowed");
|
|
1251
|
+
const schema = command.input;
|
|
1252
|
+
const read = tokens === null ? validateInput(schema, input.args) : parseOptions(tokens, schema);
|
|
1253
|
+
if (!read.ok) return refuse("invalid-args");
|
|
1254
|
+
const line = formatCommandLine(command.name, read.value, schema);
|
|
1255
|
+
if (lookup.isSensitive(line)) return refuse("not-allowed");
|
|
1256
|
+
const inputId = addEntry(console, {
|
|
1257
|
+
kind: "input",
|
|
1258
|
+
prompt: currentPrompt(console),
|
|
1259
|
+
text: line,
|
|
1260
|
+
redacted: false,
|
|
1261
|
+
status: "submitted",
|
|
1262
|
+
actor: input.actor
|
|
1263
|
+
});
|
|
1264
|
+
return new Promise((resolve) => {
|
|
1265
|
+
start(console, command, {
|
|
1266
|
+
name: command.name,
|
|
1267
|
+
command: command.name,
|
|
1268
|
+
args: [],
|
|
1269
|
+
rest: "",
|
|
1270
|
+
line
|
|
1271
|
+
}, inputId, false, {
|
|
1272
|
+
value: read.value,
|
|
1273
|
+
view,
|
|
1274
|
+
actor: input.actor
|
|
1275
|
+
}, resolve);
|
|
1276
|
+
});
|
|
922
1277
|
};
|
|
923
1278
|
/**
|
|
924
1279
|
* Ctrl+C without a run: the line ends as cancelled (README: Cancelling and
|
|
@@ -940,6 +1295,7 @@ var cancelLine = (console) => {
|
|
|
940
1295
|
/** Ctrl+C or `cancel()` during a run: invalidate, abort, `interrupted`. */
|
|
941
1296
|
var interrupt = (console, run) => {
|
|
942
1297
|
run.current = false;
|
|
1298
|
+
run.outcome = failure("cancelled");
|
|
943
1299
|
run.controller.abort();
|
|
944
1300
|
addEntry(console, output("notice", [message("interrupted")], run.inputId));
|
|
945
1301
|
cleanup(console, run);
|
|
@@ -960,7 +1316,9 @@ var coreFeatures = [
|
|
|
960
1316
|
constructConsoleAPIs(console) {
|
|
961
1317
|
states.set(console, {
|
|
962
1318
|
run: null,
|
|
963
|
-
lookup: null
|
|
1319
|
+
lookup: null,
|
|
1320
|
+
unsubscribe: null,
|
|
1321
|
+
closed: /* @__PURE__ */ new Set()
|
|
964
1322
|
});
|
|
965
1323
|
const locked = (fn, otherwise) => {
|
|
966
1324
|
const { store } = liveRecord(console);
|
|
@@ -975,11 +1333,20 @@ var coreFeatures = [
|
|
|
975
1333
|
cancel() {
|
|
976
1334
|
liveRecord(console).store.action(() => cancel(console));
|
|
977
1335
|
},
|
|
1336
|
+
request(input) {
|
|
1337
|
+
try {
|
|
1338
|
+
return liveRecord(console).store.action(() => requestLine(console, input));
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
reportCallbackError(console, error);
|
|
1341
|
+
return Promise.resolve(failure("failed"));
|
|
1342
|
+
}
|
|
1343
|
+
},
|
|
978
1344
|
isSensitive: (line) => lookupOf(console).isSensitive(line),
|
|
979
1345
|
getCommands: () => lookupOf(console).visible().map(specOf)
|
|
980
1346
|
}, {
|
|
981
1347
|
submit: Promise.resolve(false),
|
|
982
1348
|
run: Promise.resolve(false),
|
|
1349
|
+
request: Promise.resolve(failure("cancelled")),
|
|
983
1350
|
isSensitive: false,
|
|
984
1351
|
getCommands: []
|
|
985
1352
|
});
|
|
@@ -1002,17 +1369,21 @@ var coreFeatures = [
|
|
|
1002
1369
|
});
|
|
1003
1370
|
onContextChange(console, () => {
|
|
1004
1371
|
const run = stateOf(console).run;
|
|
1005
|
-
if (run?.current &&
|
|
1372
|
+
if (run?.current && run.waiting) interrupt(console, run);
|
|
1373
|
+
else if (run?.current && !run.controller.signal.aborted) run.controller.abort();
|
|
1006
1374
|
});
|
|
1007
1375
|
onReset(console, () => {
|
|
1008
1376
|
const run = stateOf(console).run;
|
|
1009
|
-
if (run) stop(console, run, true);
|
|
1377
|
+
if (run) stop(console, run, true, "cancelled");
|
|
1010
1378
|
});
|
|
1011
1379
|
onDispose(console, () => {
|
|
1012
1380
|
const state = stateOf(console);
|
|
1013
1381
|
const run = state.run;
|
|
1382
|
+
const unsubscribe = state.unsubscribe;
|
|
1014
1383
|
state.run = null;
|
|
1015
|
-
|
|
1384
|
+
state.unsubscribe = null;
|
|
1385
|
+
if (run) stop(console, run, true, "cancelled");
|
|
1386
|
+
unsubscribe?.();
|
|
1016
1387
|
});
|
|
1017
1388
|
}
|
|
1018
1389
|
}
|
|
@@ -1159,6 +1530,7 @@ function constructConsole(options, core) {
|
|
|
1159
1530
|
const defaults = Object.assign({}, ...features.map((feature) => feature.getDefaultOptions?.() ?? {}));
|
|
1160
1531
|
const merged = mergeOptions(defaults, options);
|
|
1161
1532
|
validateCommands(merged.commands);
|
|
1533
|
+
checkGateOptions(merged);
|
|
1162
1534
|
let initial = emptyState();
|
|
1163
1535
|
for (const slice of controllable) {
|
|
1164
1536
|
const value = merged.initialState?.[slice];
|
|
@@ -1208,6 +1580,123 @@ function consoleFeatures(features) {
|
|
|
1208
1580
|
function createConsole(options) {
|
|
1209
1581
|
return constructConsole(options, coreFeatures);
|
|
1210
1582
|
}
|
|
1583
|
+
// src/helpers/agentTools.ts
|
|
1584
|
+
/**
|
|
1585
|
+
* A command an agent can see: one with `input` and `danger` whose definition
|
|
1586
|
+
* is not `sensitive` (what `request` could run, C-82).
|
|
1587
|
+
*/
|
|
1588
|
+
var described = (spec) => spec.input !== void 0 && spec.danger !== void 0 && (spec.sensitive === void 0 || spec.sensitive === false);
|
|
1589
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1590
|
+
/** `value` without its `undefined` fields, as JSON. */
|
|
1591
|
+
var json = (value) => JSON.parse(JSON.stringify(value));
|
|
1592
|
+
var error = (code) => ({
|
|
1593
|
+
status: "error",
|
|
1594
|
+
code
|
|
1595
|
+
});
|
|
1596
|
+
var failed = {
|
|
1597
|
+
status: "error",
|
|
1598
|
+
code: "failed"
|
|
1599
|
+
};
|
|
1600
|
+
/**
|
|
1601
|
+
* The `context`, `help` and `run` tools of `console` (C-83). `run` calls
|
|
1602
|
+
* `console.request` as `external-agent`, so only `read` commands run.
|
|
1603
|
+
*/
|
|
1604
|
+
function agentTools(console, options) {
|
|
1605
|
+
const commands = () => console.getCommands().filter(described);
|
|
1606
|
+
const help = (input) => {
|
|
1607
|
+
const name = isRecord(input) ? input.command : void 0;
|
|
1608
|
+
if (name === void 0) return commands().map((spec) => json({
|
|
1609
|
+
name: spec.name,
|
|
1610
|
+
summary: spec.summary,
|
|
1611
|
+
danger: spec.danger
|
|
1612
|
+
}));
|
|
1613
|
+
const found = commands().find((spec) => spec.name === name || spec.aliases?.includes(name));
|
|
1614
|
+
if (!found) return error("unknown-command");
|
|
1615
|
+
return json({
|
|
1616
|
+
name: found.name,
|
|
1617
|
+
summary: found.summary,
|
|
1618
|
+
usage: found.usage,
|
|
1619
|
+
danger: found.danger,
|
|
1620
|
+
input: found.input
|
|
1621
|
+
});
|
|
1622
|
+
};
|
|
1623
|
+
return [
|
|
1624
|
+
{
|
|
1625
|
+
name: "context",
|
|
1626
|
+
description: "The open tab, its views and the active view. Call it before run to pick a view; call help for the commands.",
|
|
1627
|
+
inputSchema: {
|
|
1628
|
+
type: "object",
|
|
1629
|
+
properties: {}
|
|
1630
|
+
},
|
|
1631
|
+
annotations: {
|
|
1632
|
+
readOnlyHint: true,
|
|
1633
|
+
untrustedContentHint: true
|
|
1634
|
+
},
|
|
1635
|
+
execute: async () => {
|
|
1636
|
+
try {
|
|
1637
|
+
return await options.context();
|
|
1638
|
+
} catch {
|
|
1639
|
+
return failed;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
name: "help",
|
|
1645
|
+
description: "Without input: the commands you can run (name, summary, danger). With a command name: its usage and input schema. Call it before run.",
|
|
1646
|
+
inputSchema: {
|
|
1647
|
+
type: "object",
|
|
1648
|
+
properties: { command: {
|
|
1649
|
+
type: "string",
|
|
1650
|
+
description: "A command name."
|
|
1651
|
+
} }
|
|
1652
|
+
},
|
|
1653
|
+
annotations: { readOnlyHint: true },
|
|
1654
|
+
execute: (input) => Promise.resolve(help(input))
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
name: "run",
|
|
1658
|
+
description: "Runs a command: { line } as typed (name --field value), or { command, args, view? } with args matching its input schema. Returns { status, summary } or { status: \"error\", code }. Call help first.",
|
|
1659
|
+
inputSchema: {
|
|
1660
|
+
type: "object",
|
|
1661
|
+
properties: {
|
|
1662
|
+
line: {
|
|
1663
|
+
type: "string",
|
|
1664
|
+
description: "A whole command line."
|
|
1665
|
+
},
|
|
1666
|
+
command: {
|
|
1667
|
+
type: "string",
|
|
1668
|
+
description: "A command name."
|
|
1669
|
+
},
|
|
1670
|
+
args: {
|
|
1671
|
+
type: "object",
|
|
1672
|
+
description: "The command input."
|
|
1673
|
+
},
|
|
1674
|
+
view: {
|
|
1675
|
+
type: "string",
|
|
1676
|
+
description: "A view id from context."
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
annotations: {
|
|
1681
|
+
readOnlyHint: true,
|
|
1682
|
+
untrustedContentHint: true
|
|
1683
|
+
},
|
|
1684
|
+
execute: async (input) => {
|
|
1685
|
+
if (isRecord(input) && typeof input.line === "string") return console.request({
|
|
1686
|
+
line: input.line,
|
|
1687
|
+
actor: "external-agent"
|
|
1688
|
+
});
|
|
1689
|
+
if (isRecord(input) && typeof input.command === "string" && isRecord(input.args) && (input.view === void 0 || typeof input.view === "string")) return console.request({
|
|
1690
|
+
command: input.command,
|
|
1691
|
+
args: input.args,
|
|
1692
|
+
...input.view === void 0 ? {} : { view: input.view },
|
|
1693
|
+
actor: "external-agent"
|
|
1694
|
+
});
|
|
1695
|
+
return error("invalid-args");
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
];
|
|
1699
|
+
}
|
|
1211
1700
|
// src/features/context/context.utils.ts
|
|
1212
1701
|
/**
|
|
1213
1702
|
* The id sequence of `frames`, as one comparable string. Two stacks with the
|
|
@@ -1727,6 +2216,6 @@ var completionFeature = {
|
|
|
1727
2216
|
onLineCleared(console, () => setSession(console, null));
|
|
1728
2217
|
}
|
|
1729
2218
|
};
|
|
1730
|
-
export { completionFeature, consoleFeatures, contextFeature, createConsole, recallFeature };
|
|
2219
|
+
export { CommandError, agentTools, completionFeature, consoleFeatures, contextFeature, createConsole, recallFeature };
|
|
1731
2220
|
|
|
1732
2221
|
//# sourceMappingURL=command-console-core.js.map
|