@itookit/dsht 0.3.8 → 0.5.2
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.i18n.yaml +2 -2
- package/README.md +33 -12
- package/README.zh.md +35 -14
- package/dist/catalog/controller.d.ts +26 -6
- package/dist/catalog/controller.js +73 -45
- package/dist/catalog/index.d.ts +1 -0
- package/dist/cli/dsht.js +206 -18
- package/dist/cli/startup.d.ts +40 -0
- package/dist/cli/startup.js +314 -0
- package/dist/cli/trace-summary.d.ts +78 -0
- package/dist/cli/trace-summary.js +241 -0
- package/dist/cli/verifier.d.ts +64 -0
- package/dist/cli/verifier.js +265 -0
- package/dist/contracts.d.ts +359 -0
- package/dist/contracts.js +1 -0
- package/dist/controller/commands.d.ts +47 -0
- package/dist/controller/commands.js +322 -0
- package/dist/controller/connection-streams.d.ts +22 -0
- package/dist/controller/connection-streams.js +105 -0
- package/dist/controller/connection.d.ts +24 -31
- package/dist/controller/connection.js +48 -111
- package/dist/controller/controller.d.ts +412 -178
- package/dist/controller/controller.js +713 -167
- package/dist/controller/foreground.d.ts +44 -0
- package/dist/controller/foreground.js +79 -0
- package/dist/controller/index.d.ts +8 -1
- package/dist/controller/index.js +5 -0
- package/dist/controller/loop-contract.d.ts +136 -0
- package/dist/controller/loop-contract.js +308 -0
- package/dist/controller/loop-coordinator.d.ts +48 -0
- package/dist/controller/loop-coordinator.js +647 -0
- package/dist/controller/loop-prompts-schema.d.ts +56 -0
- package/dist/controller/loop-prompts-schema.js +144 -0
- package/dist/controller/loop-prompts.d.ts +55 -0
- package/dist/controller/loop-prompts.generated.d.ts +104 -0
- package/dist/controller/loop-prompts.generated.js +185 -0
- package/dist/controller/loop-prompts.js +104 -0
- package/dist/controller/loop-protocols.d.ts +39 -0
- package/dist/controller/loop-protocols.js +115 -0
- package/dist/controller/loop.d.ts +275 -0
- package/dist/controller/loop.js +378 -0
- package/dist/controller/prompts.d.ts +54 -0
- package/dist/controller/prompts.js +162 -0
- package/dist/controller/trace-log.d.ts +45 -0
- package/dist/controller/trace-log.js +144 -0
- package/dist/controller/verifier.d.ts +130 -0
- package/dist/controller/verifier.js +75 -0
- package/dist/cost/controller.d.ts +1 -1
- package/dist/cost/controller.js +12 -5
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/ledger.d.ts +0 -1
- package/dist/cost/ledger.js +0 -1
- package/dist/cost/scanner.js +1 -0
- package/dist/json.d.ts +18 -0
- package/dist/json.js +19 -0
- package/dist/references.d.ts +25 -0
- package/dist/references.js +26 -0
- package/dist/session/connection-view.d.ts +2 -11
- package/dist/session/controller.d.ts +94 -105
- package/dist/session/controller.js +262 -536
- package/dist/session/history-reader.d.ts +32 -0
- package/dist/session/history-reader.js +170 -0
- package/dist/session/history.d.ts +6 -18
- package/dist/session/history.js +1 -24
- package/dist/session/index.d.ts +9 -4
- package/dist/session/index.js +7 -3
- package/dist/session/info.d.ts +20 -82
- package/dist/session/info.js +52 -25
- package/dist/session/interactions.d.ts +26 -0
- package/dist/session/interactions.js +75 -0
- package/dist/session/markdown.js +1 -1
- package/dist/session/math.js +1 -1
- package/dist/session/mutation-gate.d.ts +51 -0
- package/dist/session/mutation-gate.js +73 -0
- package/dist/session/navigation.d.ts +2 -89
- package/dist/session/navigation.js +2 -129
- package/dist/session/navigator.d.ts +47 -0
- package/dist/session/navigator.js +158 -0
- package/dist/session/peek.d.ts +38 -0
- package/dist/session/peek.js +103 -0
- package/dist/session/prompt-backfill.d.ts +23 -0
- package/dist/session/prompt-backfill.js +88 -0
- package/dist/session/references.d.ts +2 -20
- package/dist/session/references.js +1 -26
- package/dist/session/runtime.d.ts +26 -0
- package/dist/session/runtime.js +28 -0
- package/dist/session/state.d.ts +20 -0
- package/dist/session/state.js +1 -0
- package/dist/session/telemetry.d.ts +25 -17
- package/dist/session/telemetry.js +66 -60
- package/dist/session/transcript.d.ts +5 -7
- package/dist/session/transcript.js +2 -15
- package/dist/session/types.d.ts +25 -0
- package/dist/session/types.js +0 -1
- package/dist/session-title.d.ts +9 -0
- package/dist/session-title.js +21 -0
- package/dist/shell/controller.d.ts +31 -1
- package/dist/shell/controller.js +34 -2
- package/dist/shell/index.d.ts +3 -3
- package/dist/shell/index.js +2 -2
- package/dist/shell/runner.d.ts +10 -0
- package/dist/shell/runner.js +48 -9
- package/dist/slash/index.d.ts +10 -0
- package/dist/slash/index.js +7 -0
- package/dist/slash/parse.d.ts +42 -0
- package/dist/slash/parse.js +259 -0
- package/dist/slash/pipeline.d.ts +140 -0
- package/dist/slash/pipeline.js +115 -0
- package/dist/slash/registry.d.ts +88 -0
- package/dist/slash/registry.js +177 -0
- package/dist/slash/types.d.ts +126 -0
- package/dist/slash/types.js +1 -0
- package/dist/state.d.ts +16 -18
- package/dist/state.js +4 -3
- package/dist/text.d.ts +28 -0
- package/dist/text.js +55 -0
- package/dist/transport/client.d.ts +4 -3
- package/dist/transport/client.js +71 -25
- package/dist/transport/events.d.ts +104 -0
- package/dist/transport/events.js +149 -0
- package/dist/transport/wire.d.ts +9 -17
- package/dist/transport/wire.js +2 -27
- package/dist/ui/app.js +750 -550
- package/dist/ui/chat/header.js +1 -1
- package/dist/ui/chat/history-view.d.ts +1 -1
- package/dist/ui/chat/loop-status.d.ts +11 -0
- package/dist/ui/chat/loop-status.js +28 -0
- package/dist/ui/chat/navigation-model.d.ts +86 -0
- package/dist/ui/chat/navigation-model.js +107 -0
- package/dist/ui/chat/shell-view.d.ts +17 -2
- package/dist/ui/chat/shell-view.js +45 -3
- package/dist/ui/chat/status.d.ts +47 -3
- package/dist/ui/chat/status.js +65 -50
- package/dist/ui/chat/use-history-view.d.ts +69 -0
- package/dist/ui/chat/use-history-view.js +123 -0
- package/dist/ui/chat/viewport.d.ts +1 -1
- package/dist/ui/dialogs/cost.d.ts +21 -4
- package/dist/ui/dialogs/cost.js +7 -12
- package/dist/ui/dialogs/index.d.ts +22 -5
- package/dist/ui/dialogs/index.js +19 -3
- package/dist/ui/dialogs/loop.d.ts +43 -0
- package/dist/ui/dialogs/loop.js +224 -0
- package/dist/ui/dialogs/peek.d.ts +25 -0
- package/dist/ui/dialogs/peek.js +35 -0
- package/dist/ui/dialogs/picker.d.ts +2 -0
- package/dist/ui/dialogs/picker.js +4 -2
- package/dist/ui/dialogs/use-panels.d.ts +53 -0
- package/dist/ui/dialogs/use-panels.js +51 -0
- package/dist/ui/input/mouse.d.ts +12 -2
- package/dist/ui/input/mouse.js +20 -7
- package/dist/ui/input/references.d.ts +1 -1
- package/dist/ui/input/use-composer.d.ts +35 -0
- package/dist/ui/input/use-composer.js +109 -0
- package/dist/ui/input/use-deferred-lines.d.ts +16 -0
- package/dist/ui/input/use-deferred-lines.js +54 -0
- package/dist/ui/input/use-history-recall.d.ts +20 -0
- package/dist/ui/input/use-history-recall.js +47 -0
- package/dist/ui/status/model.d.ts +7 -0
- package/dist/ui/status/model.js +5 -0
- package/dist/ui/theme/index.d.ts +1 -1
- package/package.json +6 -4
- package/dist/ui/commands/parse.d.ts +0 -104
- package/dist/ui/commands/parse.js +0 -135
- package/dist/ui/commands/registry.d.ts +0 -33
- package/dist/ui/commands/registry.js +0 -73
|
@@ -1,88 +1,116 @@
|
|
|
1
1
|
import { array, errorText, object, string } from "../transport/wire.js";
|
|
2
2
|
/** Owns model-catalog and preset loads for the selected session. */
|
|
3
3
|
export class CatalogController {
|
|
4
|
-
store;
|
|
5
4
|
host;
|
|
6
5
|
presetClient;
|
|
7
6
|
revision = 0;
|
|
7
|
+
generation = new AbortController();
|
|
8
|
+
refreshAbort;
|
|
8
9
|
tasks = new Set();
|
|
9
|
-
constructor(
|
|
10
|
-
this.store = store;
|
|
10
|
+
constructor(host) {
|
|
11
11
|
this.host = host;
|
|
12
12
|
}
|
|
13
13
|
/** Drop generation-scoped catalog state at the start of a connection generation. */
|
|
14
14
|
reset() {
|
|
15
|
-
this.
|
|
15
|
+
this.close();
|
|
16
|
+
this.generation = new AbortController();
|
|
17
|
+
this.presetClient = undefined;
|
|
18
|
+
this.host.publish({ modelError: undefined, defaultModel: undefined, presets: undefined, presetError: undefined });
|
|
16
19
|
}
|
|
20
|
+
/** Stop catalog requests before waiting for transport shutdown. Reset opens the next generation. */
|
|
21
|
+
close() { this.revision++; this.generation.abort(); }
|
|
17
22
|
/** Wait for every in-flight catalog task, so shutdown leaves no pending request. */
|
|
18
|
-
async settle() { await Promise.
|
|
23
|
+
async settle() { await Promise.allSettled(this.tasks); }
|
|
24
|
+
track(task) {
|
|
25
|
+
this.tasks.add(task);
|
|
26
|
+
void task.then(() => this.tasks.delete(task), () => this.tasks.delete(task));
|
|
27
|
+
return task;
|
|
28
|
+
}
|
|
29
|
+
signal(caller = this.host.signal()) {
|
|
30
|
+
return AbortSignal.any([caller, this.host.signal(), this.generation.signal]);
|
|
31
|
+
}
|
|
19
32
|
/** Load the optional preset roster once per connection, only when a session names a preset. */
|
|
20
33
|
loadPresetNames() {
|
|
21
34
|
const client = this.host.client();
|
|
22
|
-
|
|
35
|
+
const signal = this.signal();
|
|
36
|
+
if (!client || !this.host.online() || this.presetClient === client || signal.aborted)
|
|
23
37
|
return;
|
|
24
38
|
this.presetClient = client;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
const current = () => !signal.aborted && client === this.host.client();
|
|
40
|
+
const task = client.call('agentPresets/list', {}, signal).then(value => {
|
|
41
|
+
if (current())
|
|
42
|
+
this.host.publish({ presets: array(object(value).presets).map(object), presetError: undefined });
|
|
28
43
|
}).catch(error => {
|
|
29
|
-
if (
|
|
30
|
-
this.
|
|
44
|
+
if (current())
|
|
45
|
+
this.host.publish({ presets: [], presetError: errorText(error) });
|
|
31
46
|
});
|
|
32
|
-
this.
|
|
33
|
-
void task.finally(() => this.tasks.delete(task));
|
|
47
|
+
this.track(task);
|
|
34
48
|
}
|
|
35
49
|
/** Fetch current model routes and adapter-owned reasoning choices for the selected session.
|
|
36
50
|
* @returns Host catalog; provider failures remain available to the selector.
|
|
37
51
|
*/
|
|
38
|
-
async modelCatalog() {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
async modelCatalog(caller) {
|
|
53
|
+
const selection = this.selected();
|
|
54
|
+
const signal = this.signal(caller);
|
|
55
|
+
signal.throwIfAborted();
|
|
56
|
+
return this.track((async () => {
|
|
57
|
+
const value = object(await this.host.require().call('session/modelCatalog', {}, signal));
|
|
58
|
+
signal.throwIfAborted();
|
|
59
|
+
if (!this.isSelected(selection))
|
|
60
|
+
throw new Error('Session changed while loading models');
|
|
61
|
+
return value;
|
|
62
|
+
})());
|
|
45
63
|
}
|
|
46
64
|
/** Select the next request's model; the host also attempts to save its deployment default.
|
|
47
65
|
* @param provider - Host provider route ID.
|
|
48
66
|
* @param model - Exact model ID.
|
|
49
67
|
* @param reasoningEffort - Optional adapter-owned effort ID; omission uses its default.
|
|
50
68
|
*/
|
|
51
|
-
async selectModel(provider, model, reasoningEffort) {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
async selectModel(provider, model, reasoningEffort, caller) {
|
|
70
|
+
const selection = this.selected();
|
|
71
|
+
const signal = this.signal(caller);
|
|
72
|
+
signal.throwIfAborted();
|
|
73
|
+
await this.track((async () => {
|
|
74
|
+
const selected = object(object(await this.host.require().call('session/selectModel', { request: {
|
|
75
|
+
sessionId: selection.sessionId, provider, model, ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
76
|
+
} }, signal)).selected);
|
|
77
|
+
signal.throwIfAborted();
|
|
78
|
+
if (!this.isSelected(selection))
|
|
79
|
+
return;
|
|
80
|
+
this.host.publish({ status: `Next request: ${string(selected.provider)} / ${string(selected.model)}${selected.reasoningEffort ? ` · ${string(selected.reasoningEffort)}` : ''}` });
|
|
81
|
+
this.refresh();
|
|
82
|
+
})());
|
|
61
83
|
}
|
|
62
84
|
/** Reload the default route and provider failures without touching session state. */
|
|
63
85
|
refresh() {
|
|
64
86
|
const client = this.host.client();
|
|
65
|
-
if (!client)
|
|
87
|
+
if (!client || !this.host.online())
|
|
88
|
+
return;
|
|
89
|
+
this.refreshAbort?.abort();
|
|
90
|
+
this.refreshAbort = new AbortController();
|
|
91
|
+
const signal = this.signal(this.refreshAbort.signal);
|
|
92
|
+
if (signal.aborted)
|
|
66
93
|
return;
|
|
67
94
|
const revision = ++this.revision;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (client === this.host.client() && revision === this.revision)
|
|
73
|
-
this.store.update({ defaultModel: undefined, modelError: errorText(error) });
|
|
95
|
+
const current = () => !signal.aborted && client === this.host.client() && revision === this.revision;
|
|
96
|
+
const task = client.call('session/modelCatalog', {}, signal).then(value => {
|
|
97
|
+
if (current())
|
|
98
|
+
this.host.publish({ defaultModel: object(object(value).default), modelError: undefined });
|
|
74
99
|
}).catch(error => {
|
|
75
|
-
if (
|
|
76
|
-
this.
|
|
100
|
+
if (current())
|
|
101
|
+
this.host.publish({ defaultModel: undefined, modelError: errorText(error) });
|
|
77
102
|
});
|
|
78
|
-
this.
|
|
79
|
-
void task.finally(() => this.tasks.delete(task));
|
|
103
|
+
this.track(task);
|
|
80
104
|
}
|
|
81
105
|
/** @returns The selected session identity, or a `Select a session first` failure. */
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
106
|
+
selected() {
|
|
107
|
+
const selected = this.host.selection();
|
|
108
|
+
if (!selected.sessionId)
|
|
85
109
|
throw new Error('Select a session first');
|
|
86
|
-
return
|
|
110
|
+
return { ...selected, sessionId: selected.sessionId };
|
|
111
|
+
}
|
|
112
|
+
isSelected(selection) {
|
|
113
|
+
const current = this.host.selection();
|
|
114
|
+
return current.revision === selection.revision && current.sessionId === selection.sessionId;
|
|
87
115
|
}
|
|
88
116
|
}
|
package/dist/catalog/index.d.ts
CHANGED
package/dist/cli/dsht.js
CHANGED
|
@@ -6,62 +6,123 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { CostLedger, loadPrices } from "../cost/index.js";
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
8
|
import { mount } from "../ui/mount.js";
|
|
9
|
-
import { ensureDirectory } from "../storage/index.js";
|
|
10
|
-
import {
|
|
9
|
+
import { ensureDirectory, readText } from "../storage/index.js";
|
|
10
|
+
import { runStartup } from "./startup.js";
|
|
11
|
+
import { sessionLabel } from "../session-title.js";
|
|
11
12
|
import { CookieStore, login } from "../transport/auth.js";
|
|
12
13
|
import { Client } from "../transport/client.js";
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
13
15
|
import { historyLimits } from "../session/memory.js";
|
|
16
|
+
import { ProcessVerifier } from "./verifier.js";
|
|
14
17
|
import { Controller } from "../controller/controller.js";
|
|
15
18
|
import { endpoint } from "../transport/endpoint.js";
|
|
16
|
-
import { errorText,
|
|
17
|
-
|
|
19
|
+
import { errorText, object, string } from "../transport/wire.js";
|
|
20
|
+
import { formatTraceSummary, summarizeTrace } from "./trace-summary.js";
|
|
21
|
+
import { safeText } from "../text.js";
|
|
22
|
+
const HELP = `Usage: dsht [options] [list workspaces|list sessions|trace]
|
|
18
23
|
|
|
19
24
|
With no command, choose a workspace and session interactively.
|
|
20
25
|
|
|
21
26
|
--url <url> Host URL, or the dsh web URL with ?token= (DSH_URL)
|
|
22
27
|
--workspace <id> Filter list sessions by workspace
|
|
23
|
-
--session <id>
|
|
28
|
+
--session <id|new> Open a session directly, or create one
|
|
29
|
+
--ws <id|name|path> Select this workspace at startup (default: this directory)
|
|
30
|
+
--command <line> Run this slash command once the session is ready (repeatable)
|
|
31
|
+
--prompt <text> Send this plain prompt once the session is ready
|
|
32
|
+
--wait With --headless, exit when the sent prompt's turn has finished
|
|
33
|
+
--verdict <path> With --prompt/--wait, write the reply's verdict to this file
|
|
34
|
+
--verdict-identity <id> <runId>/<kind>/<step>/<attempt>/<seq> the verdict must declare
|
|
35
|
+
--headless Run --command without the terminal interface, then exit
|
|
36
|
+
--deadline <minutes> Stop the whole loop after this many minutes (DSHT_LOOP_DEADLINE)
|
|
24
37
|
--auth-dir <path> Private cookie directory (or DSHT_AUTH_DIR)
|
|
25
38
|
--history-records <n> Soft history record limit (default 2000)
|
|
26
39
|
--history-mb <n> Soft history payload budget in MiB (default 16)
|
|
27
40
|
--memory-log <path> Append runtime memory samples; a failing log stops itself
|
|
28
41
|
--no-memory-log Disable the runtime memory log (default: enabled)
|
|
42
|
+
--trace <path> Append connection/screen/selection events (default: <state>/trace.log)
|
|
43
|
+
With the trace command, read that file instead of appending to it
|
|
44
|
+
--no-trace Disable the transition trace
|
|
45
|
+
--trace-verbose Quote sanitized child output in verifier failure reasons
|
|
29
46
|
--no-shell Disable ! local commands (DSHT_NO_SHELL=1)
|
|
30
|
-
--json Print machine-readable list output
|
|
47
|
+
--json Print machine-readable list or trace output
|
|
48
|
+
--version Print the package version and exit
|
|
31
49
|
--help Show this help
|
|
32
50
|
|
|
33
51
|
The default host is http://127.0.0.1:3080.
|
|
34
52
|
First login: export DSH_TOKEN, or export DSH_URL as the URL printed by dsh web.
|
|
35
53
|
Cookies are saved per server origin and reused on later starts. Tokens are never saved.
|
|
36
54
|
/cost shows the session and today CNY estimates.
|
|
55
|
+
/prompt lists saved shortcut prompts; /prompt TEXT saves one in <state>/prompts.json.
|
|
37
56
|
!command runs on this machine, not on the host, and prints its output in the transcript.
|
|
38
57
|
DSHT_CONFIG_DIR overrides the prices.json directory; DSHT_STATE_DIR overrides usage storage.
|
|
39
58
|
The memory log defaults to <state>/memory.log; DSHT_MEMORY_LOG sets another path or 'off'.
|
|
59
|
+
The transition trace defaults to <state>/trace.log; DSHT_TRACE sets another path or 'off'.
|
|
40
60
|
prices.json overrides the shipped rates and is seeded on first use; every scan re-decides the
|
|
41
61
|
history with the table loaded then, so an edited table reaches past requests on the next scan.
|
|
42
62
|
Examples:
|
|
43
63
|
npx @itookit/dsht
|
|
44
64
|
dsht list workspaces --json
|
|
45
65
|
dsht list sessions --workspace <id> --json
|
|
66
|
+
dsht trace --json
|
|
46
67
|
`;
|
|
68
|
+
/** State root this client reads and writes logs under, honouring the same overrides as the client. */
|
|
69
|
+
function stateRoot() {
|
|
70
|
+
return process.env.DSHT_STATE_DIR ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'dsht');
|
|
71
|
+
}
|
|
72
|
+
/** Read one trace file back as a few lines of facts.
|
|
73
|
+
*
|
|
74
|
+
* Needs no host and no credentials: the trace is the client's own record of what it did, and reading
|
|
75
|
+
* it is the whole point of having written it.
|
|
76
|
+
* @param requested - `--trace` value, when given.
|
|
77
|
+
* @param json - Print the summary as JSON instead of lines.
|
|
78
|
+
*/
|
|
79
|
+
async function printTrace(requested, json) {
|
|
80
|
+
const path = requested ?? process.env.DSHT_TRACE ?? join(stateRoot(), 'trace.log');
|
|
81
|
+
const text = await readText(path);
|
|
82
|
+
if (text === undefined) {
|
|
83
|
+
process.stdout.write(`No trace at ${path}\n`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const summary = summarizeTrace(text.split('\n').filter(line => line !== ''), path);
|
|
87
|
+
process.stdout.write(json ? `${JSON.stringify(summary, null, 2)}\n` : `${formatTraceSummary(summary).join('\n')}\n`);
|
|
88
|
+
}
|
|
47
89
|
async function main() {
|
|
48
90
|
const { values, positionals } = parseArgs({ allowPositionals: true, options: {
|
|
49
91
|
url: { type: 'string', default: process.env.DSH_URL ?? 'http://127.0.0.1:3080' },
|
|
50
92
|
'history-records': { type: 'string' }, 'history-mb': { type: 'string' },
|
|
51
|
-
workspace: { type: 'string' }, session: { type: 'string' }, 'auth-dir': { type: 'string' }, json: { type: 'boolean' }, help: { type: 'boolean' },
|
|
93
|
+
workspace: { type: 'string' }, ws: { type: 'string' }, session: { type: 'string' }, 'auth-dir': { type: 'string' }, json: { type: 'boolean' }, help: { type: 'boolean' }, version: { type: 'boolean' },
|
|
94
|
+
command: { type: 'string', multiple: true }, prompt: { type: 'string' }, wait: { type: 'boolean' },
|
|
95
|
+
verdict: { type: 'string' }, 'verdict-identity': { type: 'string' }, headless: { type: 'boolean' },
|
|
96
|
+
deadline: { type: 'string' },
|
|
52
97
|
'memory-log': { type: 'string' }, 'no-memory-log': { type: 'boolean' }, 'no-shell': { type: 'boolean' },
|
|
98
|
+
trace: { type: 'string' }, 'no-trace': { type: 'boolean' }, 'trace-verbose': { type: 'boolean' },
|
|
53
99
|
} });
|
|
54
100
|
if (values.help) {
|
|
55
101
|
process.stdout.write(HELP);
|
|
56
102
|
return;
|
|
57
103
|
}
|
|
104
|
+
// The version is read from the manifest rather than repeated here, so a release never has to edit
|
|
105
|
+
// a string in this file; like `--help` it needs no host, no credentials and no terminal.
|
|
106
|
+
if (values.version) {
|
|
107
|
+
process.stdout.write(`${await packageVersion()}\n`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
58
110
|
const list = positionals[0] === 'list' && ['workspaces', 'sessions'].includes(positionals[1] ?? '') && positionals.length === 2;
|
|
59
|
-
|
|
111
|
+
const trace = positionals[0] === 'trace' && positionals.length === 1;
|
|
112
|
+
if (positionals.length && !list && !trace)
|
|
60
113
|
throw new Error('Unknown command. Use --help.');
|
|
61
|
-
if (!list && (values.json || values.workspace))
|
|
62
|
-
throw new Error('--json and --workspace apply to list commands');
|
|
63
|
-
if (list && values.session)
|
|
114
|
+
if (!list && !trace && (values.json || values.workspace))
|
|
115
|
+
throw new Error('--json and --workspace apply to list or trace commands');
|
|
116
|
+
if ((list || trace) && values.session)
|
|
64
117
|
throw new Error('--session applies to interactive mode');
|
|
118
|
+
if ((list || trace) && (values.ws || values.command?.length || values.prompt !== undefined || values.wait || values.headless || values.deadline)) {
|
|
119
|
+
throw new Error('--ws, --command, --prompt, --wait, --deadline and --headless apply to interactive mode');
|
|
120
|
+
}
|
|
121
|
+
// Reading a trace needs no host, no credentials and no terminal, so it runs before any of them.
|
|
122
|
+
if (trace) {
|
|
123
|
+
await printTrace(values.trace, values.json === true);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
65
126
|
const limits = historyLimits(values['history-records'], values['history-mb']);
|
|
66
127
|
const { url, token } = endpoint(values.url, process.env.DSH_TOKEN);
|
|
67
128
|
const store = new CookieStore(values['auth-dir']);
|
|
@@ -93,14 +154,71 @@ async function main() {
|
|
|
93
154
|
const costDirectory = join(stateRoot, 'cost', createHash('sha256').update(new URL(url).origin).digest('hex'));
|
|
94
155
|
const costs = new CostLedger(prices, costDirectory, custom);
|
|
95
156
|
await costs.load();
|
|
96
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
97
|
-
throw new Error('Interactive mode requires a terminal. Use
|
|
157
|
+
if (!values.headless && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
158
|
+
throw new Error('Interactive mode requires a terminal. Use --headless or list workspaces/list sessions for scripts.');
|
|
159
|
+
}
|
|
98
160
|
const shellEnabled = !values['no-shell'] && process.env.DSHT_NO_SHELL !== '1';
|
|
99
|
-
const
|
|
161
|
+
const localDirectory = process.cwd();
|
|
162
|
+
// A scored review can delegate each round's verdict to a child client, which needs no shared state
|
|
163
|
+
// with this one: it is handed a session and a prompt, and answers through a file.
|
|
164
|
+
const verifier = process.env.DSHT_NO_VERIFY === '1' ? undefined : new ProcessVerifier({
|
|
165
|
+
command: [process.execPath, ...process.execArgv, process.argv[1] ?? fileURLToPath(import.meta.url)],
|
|
166
|
+
url: values.url,
|
|
167
|
+
...(values['auth-dir'] === undefined ? {} : { authDir: values['auth-dir'] }),
|
|
168
|
+
cwd: localDirectory, env: process.env,
|
|
169
|
+
timeoutMs: verifyTimeoutMs(process.env.DSHT_VERIFY_TIMEOUT_MS),
|
|
170
|
+
createSession: (title) => controller.actions.createVerifierSession(title),
|
|
171
|
+
cancelSession: async (sessionId) => {
|
|
172
|
+
if (!await controller.actions.cancelVerifierSession(sessionId))
|
|
173
|
+
throw new Error('Verifier cancellation was not accepted');
|
|
174
|
+
},
|
|
175
|
+
onLine: line => { if (values.headless)
|
|
176
|
+
log(line); },
|
|
177
|
+
// Off by default: a verifier reason reaches the progress line and the trace, and that log may be
|
|
178
|
+
// pasted into a report, so the child's own words are quoted only when the operator asks.
|
|
179
|
+
verbose: values['trace-verbose'] === true || process.env.DSHT_TRACE_VERBOSE === '1',
|
|
180
|
+
});
|
|
181
|
+
const controller = new Controller({
|
|
182
|
+
base: url, token, initialSession: values.session === 'new' ? undefined : values.session,
|
|
183
|
+
authenticate: client => login(client, token, store),
|
|
184
|
+
localDirectory, verifier,
|
|
185
|
+
// Verdicts belong to this client rather than to the reviewed tree, and the client's own
|
|
186
|
+
// directory is the one place it is always allowed to write; DSHT_VERDICT_ROOT points them
|
|
187
|
+
// elsewhere when the review targets a workspace this machine cannot write.
|
|
188
|
+
verdictRoot: process.env.DSHT_VERDICT_ROOT ?? localDirectory,
|
|
189
|
+
costs, historyLimits: limits, shellEnabled,
|
|
190
|
+
deadlineMs: loopDeadlineMs(values.deadline ?? process.env.DSHT_LOOP_DEADLINE),
|
|
191
|
+
memoryLogPath: memoryLogPath(stateRoot, values['memory-log'], values['no-memory-log']),
|
|
192
|
+
tracePath: tracePath(stateRoot, values.trace, values['no-trace']),
|
|
193
|
+
promptsPath: join(stateRoot, 'prompts.json'),
|
|
194
|
+
});
|
|
195
|
+
const plan = {
|
|
196
|
+
...(values.ws === undefined ? {} : { workspace: values.ws }),
|
|
197
|
+
...(values.session === undefined ? {} : { session: values.session }),
|
|
198
|
+
commands: values.command ?? [],
|
|
199
|
+
...(values.prompt === undefined ? {} : { prompt: values.prompt }),
|
|
200
|
+
...(values.wait === undefined ? {} : { wait: values.wait }),
|
|
201
|
+
...(values.verdict === undefined ? {} : { verdict: { file: values.verdict, identity: requireIdentity(values['verdict-identity']) } }),
|
|
202
|
+
timeoutSeconds: 3600,
|
|
203
|
+
};
|
|
204
|
+
const log = (line) => process.stderr.write(`${line}\n`);
|
|
205
|
+
controller.start();
|
|
206
|
+
if (values.headless) {
|
|
207
|
+
// No renderer: run the plan, follow a started loop to its verdict, and report it as the exit code.
|
|
208
|
+
try {
|
|
209
|
+
const outcome = await runStartup(controller, plan, log);
|
|
210
|
+
// 0 passed, 1 failed, 3 waiting for a person: a script can tell the three apart.
|
|
211
|
+
process.exitCode = outcome === 'failed' ? 1 : outcome === 'needs-human' ? 3 : 0;
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
await controller.shutdown();
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
100
218
|
const app = mount(controller);
|
|
101
219
|
const terminate = () => app.unmount();
|
|
102
220
|
process.once('SIGTERM', terminate);
|
|
103
|
-
controller.
|
|
221
|
+
void runStartup(controller, plan, log).catch(error => process.stderr.write(`${errorText(error)}\n`));
|
|
104
222
|
try {
|
|
105
223
|
await app.waitUntilExit();
|
|
106
224
|
}
|
|
@@ -109,6 +227,55 @@ async function main() {
|
|
|
109
227
|
await controller.shutdown();
|
|
110
228
|
}
|
|
111
229
|
}
|
|
230
|
+
/** Read the published version from the manifest beside this entry point.
|
|
231
|
+
*
|
|
232
|
+
* The path is relative to this module, so it resolves both in the source tree (`src/cli/`) and in
|
|
233
|
+
* the published build (`dist/cli/`). The version is never repeated as a literal, which is what lets
|
|
234
|
+
* a release touch only `package.json` and the lockfile.
|
|
235
|
+
* @returns The `version` field of `package.json`.
|
|
236
|
+
*/
|
|
237
|
+
async function packageVersion() {
|
|
238
|
+
const manifest = await readText(fileURLToPath(new URL('../../package.json', import.meta.url)));
|
|
239
|
+
if (manifest === undefined)
|
|
240
|
+
throw new Error('package.json is missing beside the client entry point');
|
|
241
|
+
const version = string(object(JSON.parse(manifest)).version);
|
|
242
|
+
if (version === '')
|
|
243
|
+
throw new Error('package.json has no version');
|
|
244
|
+
return version;
|
|
245
|
+
}
|
|
246
|
+
/** The identity a written verdict must declare, refused when the flag that carries it is missing.
|
|
247
|
+
* @param value - `--verdict-identity` value.
|
|
248
|
+
* @returns The identity.
|
|
249
|
+
*/
|
|
250
|
+
function requireIdentity(value) {
|
|
251
|
+
if (value === undefined || value.trim() === '')
|
|
252
|
+
throw new Error('--verdict requires --verdict-identity');
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
/** Whole-run budget from `--deadline`/`DSHT_LOOP_DEADLINE`, in minutes.
|
|
256
|
+
*
|
|
257
|
+
* Absent means no budget, which keeps the old behaviour for callers that never set one. A value that
|
|
258
|
+
* is not a positive number is refused rather than silently ignored, because a run that was supposed
|
|
259
|
+
* to be bounded and is not is worse than a startup error.
|
|
260
|
+
* @param value - Flag or environment value.
|
|
261
|
+
* @returns The budget in milliseconds, or undefined when unbounded.
|
|
262
|
+
*/
|
|
263
|
+
function loopDeadlineMs(value) {
|
|
264
|
+
if (value === undefined || value.trim() === '')
|
|
265
|
+
return undefined;
|
|
266
|
+
const minutes = Number(value);
|
|
267
|
+
if (!Number.isFinite(minutes) || minutes <= 0)
|
|
268
|
+
throw new Error(`--deadline must be a positive number of minutes: ${value}`);
|
|
269
|
+
return Math.round(minutes * 60_000);
|
|
270
|
+
}
|
|
271
|
+
/** How long one forked verification may run, from a minute count in the environment.
|
|
272
|
+
* @param value - `DSHT_VERIFY_TIMEOUT_MS` when set.
|
|
273
|
+
* @returns The timeout in milliseconds, defaulting to twenty minutes.
|
|
274
|
+
*/
|
|
275
|
+
function verifyTimeoutMs(value) {
|
|
276
|
+
const parsed = Number(value);
|
|
277
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 20 * 60_000;
|
|
278
|
+
}
|
|
112
279
|
/** Resolve the runtime memory log path: an explicit flag wins, then the environment, then the default.
|
|
113
280
|
* @param stateRoot - Application state root used for the default path.
|
|
114
281
|
* @param requested - `--memory-log` value, when given.
|
|
@@ -116,13 +283,34 @@ async function main() {
|
|
|
116
283
|
* @returns Absolute log path, or undefined when the log is disabled.
|
|
117
284
|
*/
|
|
118
285
|
function memoryLogPath(stateRoot, requested, disabled) {
|
|
286
|
+
return diagnosticLogPath('memory-log', 'memory.log', stateRoot, requested, disabled, process.env.DSHT_MEMORY_LOG);
|
|
287
|
+
}
|
|
288
|
+
/** Resolve the transition trace path: an explicit flag wins, then the environment, then the default.
|
|
289
|
+
* @param stateRoot - Application state root used for the default path.
|
|
290
|
+
* @param requested - `--trace` value, when given.
|
|
291
|
+
* @param disabled - `--no-trace` flag.
|
|
292
|
+
* @returns Absolute trace path, or undefined when the trace is disabled.
|
|
293
|
+
*/
|
|
294
|
+
function tracePath(stateRoot, requested, disabled) {
|
|
295
|
+
return diagnosticLogPath('trace', 'trace.log', stateRoot, requested, disabled, process.env.DSHT_TRACE);
|
|
296
|
+
}
|
|
297
|
+
/** Resolve one diagnostic log path shared by the memory log and the transition trace.
|
|
298
|
+
* @param flag - Long option name, used in the error for an empty value.
|
|
299
|
+
* @param file - Default filename under the state root.
|
|
300
|
+
* @param stateRoot - Application state root.
|
|
301
|
+
* @param requested - Flag value, when given; an empty string is a mistyped flag, not a default.
|
|
302
|
+
* @param disabled - `--no-<flag>` flag.
|
|
303
|
+
* @param environment - Environment override, where `off` disables the log.
|
|
304
|
+
* @returns Absolute log path, or undefined when the log is disabled.
|
|
305
|
+
*/
|
|
306
|
+
function diagnosticLogPath(flag, file, stateRoot, requested, disabled, environment) {
|
|
119
307
|
if (requested !== undefined && requested.trim() === '')
|
|
120
|
-
throw new Error(
|
|
308
|
+
throw new Error(`--${flag} requires a path`);
|
|
121
309
|
if (disabled)
|
|
122
310
|
return undefined;
|
|
123
|
-
const chosen = (requested ??
|
|
311
|
+
const chosen = (requested ?? environment)?.trim();
|
|
124
312
|
if (chosen === undefined || chosen === '')
|
|
125
|
-
return join(stateRoot,
|
|
313
|
+
return join(stateRoot, file);
|
|
126
314
|
return chosen === 'off' ? undefined : chosen;
|
|
127
315
|
}
|
|
128
316
|
main().catch(error => { process.stderr.write(`${errorText(error)}\n`); process.exitCode = 1; });
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Startup automation: pick a workspace and a session, then run the requested slash lines.
|
|
2
|
+
*
|
|
3
|
+
* This is the scriptable half of the client — `dsht --ws X --session new --command "…"` — so a
|
|
4
|
+
* review can be launched without typing. It drives the same controller the UI drives, and reads only
|
|
5
|
+
* the outcome of each line: the presentational effects of a command belong to the UI.
|
|
6
|
+
*/
|
|
7
|
+
import { type Controller } from '../controller/index.ts';
|
|
8
|
+
/** What the operator asked the client to do before/while taking over. */
|
|
9
|
+
export interface StartupPlan {
|
|
10
|
+
/** Workspace ID, name or path; absent adopts the directory this client runs in. */
|
|
11
|
+
workspace?: string;
|
|
12
|
+
/** Session to open, or `new`; absent creates one when commands were given. */
|
|
13
|
+
session?: string;
|
|
14
|
+
/** Slash lines to run once the session is ready, in order. */
|
|
15
|
+
commands: readonly string[];
|
|
16
|
+
/** Plain prompt to send after the commands; a forked verifier drives a session this way. */
|
|
17
|
+
prompt?: string;
|
|
18
|
+
/** Wait for that prompt's turn to finish before returning, so the child exits on its own. */
|
|
19
|
+
wait?: boolean;
|
|
20
|
+
/** Where to persist the verdict this session produced, and the identity it must declare. */
|
|
21
|
+
verdict?: VerdictTarget;
|
|
22
|
+
/** Seconds a started loop may run before it is stopped. */
|
|
23
|
+
timeoutSeconds: number;
|
|
24
|
+
}
|
|
25
|
+
/** The verdict file a forked verifier session must leave behind. */
|
|
26
|
+
export interface VerdictTarget {
|
|
27
|
+
/** Absolute path to write. */
|
|
28
|
+
file: string;
|
|
29
|
+
/** `<runId>/<kind>/<step>/<attempt>`, checked against the reply before anything is written. */
|
|
30
|
+
identity: string;
|
|
31
|
+
}
|
|
32
|
+
/** How the startup run ended. */
|
|
33
|
+
export type StartupOutcome = 'passed' | 'idle' | 'failed' | 'needs-human';
|
|
34
|
+
/** Run the plan against a started controller.
|
|
35
|
+
* @param controller - Connected application facade.
|
|
36
|
+
* @param plan - Workspace, session and lines to run.
|
|
37
|
+
* @param log - Progress sink, usually stderr in headless mode.
|
|
38
|
+
* @returns Whether a started loop passed, nothing ran, or the run failed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function runStartup(controller: Controller, plan: StartupPlan, log: (line: string) => void): Promise<StartupOutcome>;
|