codsh-cli 0.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/LICENSE +21 -0
- package/README.md +65 -0
- package/README.zh.md +65 -0
- package/agent-presets/code-cli/agent.cordis.yml +322 -0
- package/agent-presets/code-cli/preset.yml +3 -0
- package/bin/codsh.mjs +61 -0
- package/cordis.patch.yml +165 -0
- package/lib/index.js +4008 -0
- package/lib/invariant.js +23 -0
- package/lib/startup.js +64 -0
- package/lib/types/approval.d.ts +57 -0
- package/lib/types/banner.d.ts +30 -0
- package/lib/types/completion.d.ts +45 -0
- package/lib/types/console.d.ts +152 -0
- package/lib/types/custom-commands.d.ts +54 -0
- package/lib/types/editor.d.ts +186 -0
- package/lib/types/index.d.ts +48 -0
- package/lib/types/inputbox.d.ts +45 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/keys.d.ts +103 -0
- package/lib/types/markdown.d.ts +73 -0
- package/lib/types/preset-install.d.ts +34 -0
- package/lib/types/prompt.d.ts +135 -0
- package/lib/types/questions.d.ts +65 -0
- package/lib/types/selector.d.ts +99 -0
- package/lib/types/spinner.d.ts +57 -0
- package/lib/types/startup.d.ts +31 -0
- package/lib/types/status.d.ts +94 -0
- package/lib/types/streaming.d.ts +68 -0
- package/lib/types/theme.d.ts +74 -0
- package/lib/types/transcript.d.ts +129 -0
- package/package.json +149 -0
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/invariant.ts
|
|
2
|
+
const PACKAGE_NAME = "codsh-cli";
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
const name = "coding-cli-invariant";
|
|
5
|
+
/** Service required before the companion can register. */
|
|
6
|
+
const inject = ["invariants"];
|
|
7
|
+
/**
|
|
8
|
+
* No runtime invariant: the surface is a renderer and input loop over the
|
|
9
|
+
* session log whose observable contract (rendered transcript, approval
|
|
10
|
+
* decisions, exit code) is process-level and owned by the launcher e2e. It
|
|
11
|
+
* holds no durable relation of its own — every fact it shows is derived from
|
|
12
|
+
* `session/event`, whose relations `dsh-session` already audits.
|
|
13
|
+
*/
|
|
14
|
+
const install = () => {};
|
|
15
|
+
/**
|
|
16
|
+
* Register this package's invariant companion.
|
|
17
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
18
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
19
|
+
*/
|
|
20
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
package/lib/startup.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { parseCmdline } from "@deepseek-ai/dsh-cmdline";
|
|
3
|
+
|
|
4
|
+
//#region src/startup.ts
|
|
5
|
+
/** Stable Cordis plugin name. */
|
|
6
|
+
const name = "coding-cli-startup";
|
|
7
|
+
/** Services required before the invocation can be resolved. */
|
|
8
|
+
const inject = ["cmdlineArgs"];
|
|
9
|
+
/** Service provided by this plugin and injected by the terminal runner. */
|
|
10
|
+
const CODING_CLI_STARTUP_SERVICE = "codingCliStartup";
|
|
11
|
+
/**
|
|
12
|
+
* This app's command: the optional task positional, its flags, and its help text.
|
|
13
|
+
* @returns a fresh program, so one process can parse more than once (tests).
|
|
14
|
+
*/
|
|
15
|
+
function codingCliCommand() {
|
|
16
|
+
return new Command().name("dsh code").description("Work through coding tasks in an interactive terminal session.").helpOption("-h, --help", "show this help").argument("[task...]", "opening task; multiple words are joined by spaces. Omit it to start at the prompt").option("--resume <session>", "reopen a session by id and continue its conversation").option("--continue", "reopen the most recent session in this working directory").option("--preset <id>", "compose the agent from this preset instead of the roster default").option("-p, --print", "render the answer to the opening task and exit without entering the prompt").addHelpText("after", `
|
|
17
|
+
Examples:
|
|
18
|
+
dsh code start an interactive session
|
|
19
|
+
dsh code "add a slugify helper" start with an opening task
|
|
20
|
+
dsh code -p "what does this repo do?" answer once and exit
|
|
21
|
+
dsh code --continue reopen the latest session here
|
|
22
|
+
dsh code --resume session-1234 reopen one session by id
|
|
23
|
+
dsh code --preset standard compose from a different preset
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the session to reopen from the two mutually exclusive continuation flags.
|
|
28
|
+
* @param program - the parsed command, used to report a usage error.
|
|
29
|
+
* @param options - the flags commander collected.
|
|
30
|
+
* @returns the session id, `'latest'`, or the empty string.
|
|
31
|
+
*/
|
|
32
|
+
function resolveResume(program, options) {
|
|
33
|
+
if (options.continue === true && options.resume !== void 0) program.error("error: --continue and --resume are mutually exclusive");
|
|
34
|
+
if (options.continue === true) return "latest";
|
|
35
|
+
if (options.resume === void 0) return "";
|
|
36
|
+
if (options.resume.trim() === "") program.error("error: --resume needs a session id");
|
|
37
|
+
return options.resume;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse and provide this invocation as an ordinary Cordis service. On `--help`
|
|
41
|
+
* and on a usage error nothing is provided, so the runner never mounts.
|
|
42
|
+
* @param ctx - plugin context carrying the command line.
|
|
43
|
+
*/
|
|
44
|
+
function apply(ctx) {
|
|
45
|
+
const program = codingCliCommand();
|
|
46
|
+
program.action(() => {
|
|
47
|
+
const options = program.opts();
|
|
48
|
+
const task = program.args.join(" ").trim();
|
|
49
|
+
const print = options.print === true;
|
|
50
|
+
if (print && task === "") program.error("error: --print needs a task, for example: dsh code -p \"run the tests\"");
|
|
51
|
+
const preset = options.preset ?? "";
|
|
52
|
+
if (options.preset !== void 0 && preset.trim() === "") program.error("error: --preset needs a preset id");
|
|
53
|
+
ctx.provide(CODING_CLI_STARTUP_SERVICE, {
|
|
54
|
+
task,
|
|
55
|
+
resume: resolveResume(program, options),
|
|
56
|
+
preset,
|
|
57
|
+
print
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
parseCmdline(ctx, program);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { CODING_CLI_STARTUP_SERVICE, apply, inject, name };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal's approval answerer: it puts one pending tool call to the
|
|
3
|
+
* keyboard and returns the decision.
|
|
4
|
+
*
|
|
5
|
+
* `ApprovalOutcome` has no remembered grant — the vocabulary is `allowed-once`,
|
|
6
|
+
* `rejected`, `cancelled`, and `unavailable` — so "allow every call to this
|
|
7
|
+
* tool" is this surface's own state, kept here and answered as `allowed-once`
|
|
8
|
+
* without asking again.
|
|
9
|
+
* @module codsh-cli/src/approval
|
|
10
|
+
*/
|
|
11
|
+
import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval';
|
|
12
|
+
import type { Theme } from './theme.ts';
|
|
13
|
+
/** One keystroke the approval prompt accepts. */
|
|
14
|
+
export type ApprovalAnswer = 'once' | 'always' | 'reject';
|
|
15
|
+
/** Reads one approval keystroke from the terminal. */
|
|
16
|
+
export interface ApprovalPrompt {
|
|
17
|
+
/**
|
|
18
|
+
* Ask the person about one pending call.
|
|
19
|
+
* @param toolName - the tool awaiting a decision.
|
|
20
|
+
* @param reason - the asker's explanation, when it supplied one.
|
|
21
|
+
* @param signal - aborts the prompt when the call is cancelled.
|
|
22
|
+
* @returns the chosen answer, or undefined when the prompt was aborted.
|
|
23
|
+
*/
|
|
24
|
+
ask(toolName: string, reason: string | undefined, signal: AbortSignal | undefined): Promise<ApprovalAnswer | undefined>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Approval state for one terminal session.
|
|
28
|
+
*
|
|
29
|
+
* The remembered set is per-process and never written to disk. An
|
|
30
|
+
* {@link ApprovalRequest} carries the tool name, reason, and call id but NOT
|
|
31
|
+
* the call arguments, so a grant cannot be narrowed to "this command" — it
|
|
32
|
+
* covers every later call to that tool. Persisting a grant that broad across
|
|
33
|
+
* runs would outlive the intent that produced it.
|
|
34
|
+
*/
|
|
35
|
+
export declare class TerminalApproval {
|
|
36
|
+
private readonly prompt;
|
|
37
|
+
private readonly theme;
|
|
38
|
+
private readonly write;
|
|
39
|
+
private readonly allowed;
|
|
40
|
+
constructor(prompt: ApprovalPrompt, theme: Theme, write: (line: string) => void);
|
|
41
|
+
/** Tool names granted for the rest of this process, in grant order. */
|
|
42
|
+
get remembered(): readonly string[];
|
|
43
|
+
/** Forget every remembered grant, so the next call of each tool asks again. */
|
|
44
|
+
clear(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Decide one request, asking the keyboard unless the tool is already granted.
|
|
47
|
+
* @param req - the pending decision.
|
|
48
|
+
* @returns the outcome for this request.
|
|
49
|
+
*/
|
|
50
|
+
decide(req: ApprovalRequest): Promise<ApprovalOutcome>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Map one keystroke to an approval answer.
|
|
54
|
+
* @param key - the character typed at the prompt.
|
|
55
|
+
* @returns the answer, or undefined when the key means nothing here.
|
|
56
|
+
*/
|
|
57
|
+
export declare function answerForKey(key: string): ApprovalAnswer | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The opening banner: what answered, where, and which keys matter.
|
|
3
|
+
* @module codsh-cli/src/banner
|
|
4
|
+
*/
|
|
5
|
+
import type { Theme } from './theme.ts';
|
|
6
|
+
/** What the banner reports about the composed session. */
|
|
7
|
+
export interface BannerFacts {
|
|
8
|
+
/** Model route answering this session. */
|
|
9
|
+
model: string;
|
|
10
|
+
/** Composed preset, absent when the deployment composes no roster. */
|
|
11
|
+
preset?: string | undefined;
|
|
12
|
+
/** Session workspace. */
|
|
13
|
+
cwd: string;
|
|
14
|
+
/** Checked-out branch, absent outside a repository. */
|
|
15
|
+
branch?: string | undefined;
|
|
16
|
+
/** Session identity, so a person can resume this exact conversation later. */
|
|
17
|
+
session: string;
|
|
18
|
+
/** Whether Escape can reach the surface; decides which interrupt is named. */
|
|
19
|
+
readsKeys: boolean;
|
|
20
|
+
/** Whether the transcript replayed a resumed conversation above the banner. */
|
|
21
|
+
resumed: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Render the opening banner.
|
|
25
|
+
* @param facts - what to report.
|
|
26
|
+
* @param theme - styling for the frame and the detail lines.
|
|
27
|
+
* @param columns - display columns available; a narrow terminal loses the frame.
|
|
28
|
+
* @returns the lines to print, ending with a blank separator.
|
|
29
|
+
*/
|
|
30
|
+
export declare function bannerLines(facts: BannerFacts, theme: Theme, columns: number): string[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completion sources for the prompt: slash commands, and workspace files behind
|
|
3
|
+
* an `@` mention.
|
|
4
|
+
*
|
|
5
|
+
* Bare prose never completes. A sentence headed for the model would be rewritten
|
|
6
|
+
* by a completer guessing at it, so a path has to be asked for — `@` is that
|
|
7
|
+
* request, and it is also what makes the mention legible to the model, which
|
|
8
|
+
* reads the file with its own tools.
|
|
9
|
+
*
|
|
10
|
+
* Matching is fuzzy, not prefix: the person knows a fragment of the name, not
|
|
11
|
+
* where in the path it starts. A prefix match still ranks first, so the fuzzy
|
|
12
|
+
* fallback never steals an exact intention.
|
|
13
|
+
* @module codsh-cli/src/completion
|
|
14
|
+
*/
|
|
15
|
+
/** One completable command: its name without the slash, and what it does. */
|
|
16
|
+
export interface CompletableCommand {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
}
|
|
20
|
+
/** `readline`'s completer result: the candidates, and the substring they replace. */
|
|
21
|
+
export type CompletionResult = [completions: string[], substring: string];
|
|
22
|
+
/**
|
|
23
|
+
* Score `needle` against `hay` as a fuzzy subsequence.
|
|
24
|
+
*
|
|
25
|
+
* Consecutive hits and hits on a boundary (start, or after a separator) score
|
|
26
|
+
* higher, which is what ranks `src/idx` matches the way a person expects. A
|
|
27
|
+
* needle that is not a subsequence scores nothing at all.
|
|
28
|
+
* @param needle - what was typed, matched case-insensitively.
|
|
29
|
+
* @param hay - the candidate.
|
|
30
|
+
* @returns the score, or undefined when it does not match.
|
|
31
|
+
*/
|
|
32
|
+
export declare function fuzzyScore(needle: string, hay: string): number | undefined;
|
|
33
|
+
/** Drop the cached workspace walk, so a test controls what the next Tab sees. */
|
|
34
|
+
export declare function resetFileIndex(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Build the completer for one session.
|
|
37
|
+
*
|
|
38
|
+
* The command list is read on each use rather than captured, because a command
|
|
39
|
+
* registry is scoped and changes with the session's mode — plan mode alone adds
|
|
40
|
+
* and removes one.
|
|
41
|
+
* @param commands - reads the currently registered commands.
|
|
42
|
+
* @param cwd - the workspace `@` mentions resolve against.
|
|
43
|
+
* @returns a completer over the word under the cursor.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createCompleter(commands: () => readonly CompletableCommand[], cwd: string): (line: string) => CompletionResult;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process-facing terminal, in two shapes.
|
|
3
|
+
*
|
|
4
|
+
* On a terminal this surface owns the keyboard: raw mode, its own key decoding,
|
|
5
|
+
* and a managed region of rows at the bottom holding the input box. That is what
|
|
6
|
+
* an inline completion menu and a multi-line prompt require — `readline` reports
|
|
7
|
+
* no lone Escape, draws no menu, and decides for itself what Enter means.
|
|
8
|
+
*
|
|
9
|
+
* Off a terminal it is a line reader over `readline`, because a pipe has no
|
|
10
|
+
* cursor to manage and every line in a script is a separate instruction. Both
|
|
11
|
+
* shapes answer the same reads, which is what keeps piped runs and tests on the
|
|
12
|
+
* same code path as a person typing.
|
|
13
|
+
* @module codsh-cli/src/console
|
|
14
|
+
*/
|
|
15
|
+
import type { Key } from './keys.ts';
|
|
16
|
+
/** The output stream this surface writes to. */
|
|
17
|
+
export interface OutputStream extends NodeJS.WritableStream {
|
|
18
|
+
readonly columns?: number;
|
|
19
|
+
readonly isTTY?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** The input stream this surface reads from. */
|
|
22
|
+
export interface InputStream extends NodeJS.ReadableStream {
|
|
23
|
+
readonly isTTY?: boolean;
|
|
24
|
+
setRawMode?(mode: boolean): unknown;
|
|
25
|
+
}
|
|
26
|
+
/** Where the cursor belongs among a region's rows. */
|
|
27
|
+
export interface RegionCursor {
|
|
28
|
+
row: number;
|
|
29
|
+
column: number;
|
|
30
|
+
}
|
|
31
|
+
/** Line input and output over one pair of process streams. */
|
|
32
|
+
export declare class TerminalConsole {
|
|
33
|
+
private readonly input;
|
|
34
|
+
private readonly output;
|
|
35
|
+
private readonly rl;
|
|
36
|
+
private readonly decoder;
|
|
37
|
+
private readonly pending;
|
|
38
|
+
private readonly waiters;
|
|
39
|
+
private keyHandler;
|
|
40
|
+
/** Keys decoded before any handler registered — type-ahead is never dropped. */
|
|
41
|
+
private readonly earlyKeys;
|
|
42
|
+
private escapeTimer;
|
|
43
|
+
private ended;
|
|
44
|
+
/** Rows currently drawn in the bottom region. */
|
|
45
|
+
private regionRows;
|
|
46
|
+
/** Where among those rows the cursor was left. */
|
|
47
|
+
private regionCursor;
|
|
48
|
+
/** Whether the region currently holds input focus, which shows the cursor. */
|
|
49
|
+
private regionFocus;
|
|
50
|
+
constructor(input: InputStream, output: OutputStream);
|
|
51
|
+
/** Display columns available for one line, never below {@link MIN_COLUMNS}. */
|
|
52
|
+
get columns(): number;
|
|
53
|
+
/** Whether the output stream is a terminal. */
|
|
54
|
+
get isTty(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Whether this surface owns the keyboard.
|
|
57
|
+
*
|
|
58
|
+
* That needs both streams on a terminal: raw mode is what delivers a key
|
|
59
|
+
* before its line, and there is no point managing rows on a stream with no
|
|
60
|
+
* cursor.
|
|
61
|
+
*/
|
|
62
|
+
get readsKeys(): boolean;
|
|
63
|
+
/** Whether input has finished. */
|
|
64
|
+
get finished(): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Register a handler for terminal window changes.
|
|
67
|
+
* @param handler - called after each resize while registered.
|
|
68
|
+
* @returns a disposer that removes it.
|
|
69
|
+
*/
|
|
70
|
+
onResize(handler: () => void): () => void;
|
|
71
|
+
/**
|
|
72
|
+
* Clear the visible screen.
|
|
73
|
+
*
|
|
74
|
+
* The scrollback survives — this wipes the viewport the way a shell's clear
|
|
75
|
+
* does. The managed region is forgotten with it, so the caller redraws.
|
|
76
|
+
*/
|
|
77
|
+
clearScreen(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Route decoded keys to a handler.
|
|
80
|
+
* @param handler - receives every key while registered.
|
|
81
|
+
* @returns a disposer that removes it.
|
|
82
|
+
*/
|
|
83
|
+
onKey(handler: (key: Key) => void): () => void;
|
|
84
|
+
/**
|
|
85
|
+
* Decode one read and dispatch its keys.
|
|
86
|
+
* @param chunk - the bytes the terminal delivered.
|
|
87
|
+
*/
|
|
88
|
+
private onBytes;
|
|
89
|
+
/** Hand one key to the handler, or hold it until one registers. */
|
|
90
|
+
private deliver;
|
|
91
|
+
/** Wait briefly for the rest of a held sequence, then resolve it as Escape. */
|
|
92
|
+
private armEscapeFlush;
|
|
93
|
+
/**
|
|
94
|
+
* Hand one input line to the longest-waiting read, or queue it.
|
|
95
|
+
* @param line - the line the reader produced.
|
|
96
|
+
*/
|
|
97
|
+
private offer;
|
|
98
|
+
/** Mark input finished and release every waiting read. */
|
|
99
|
+
private end;
|
|
100
|
+
/**
|
|
101
|
+
* Write one finished line above the managed region.
|
|
102
|
+
*
|
|
103
|
+
* The region is erased first and redrawn after, so the transcript stays
|
|
104
|
+
* append-only while the input box keeps its place at the bottom.
|
|
105
|
+
* @param line - the line, without its terminator.
|
|
106
|
+
*/
|
|
107
|
+
write(line: string): void;
|
|
108
|
+
/**
|
|
109
|
+
* Replace the managed region at the bottom of the screen.
|
|
110
|
+
*
|
|
111
|
+
* This is the whole live area: an input box, a completion menu, a working
|
|
112
|
+
* indicator. Everything the transcript keeps goes through {@link write}
|
|
113
|
+
* instead, because a terminal cannot revise a row that has scrolled. Off a
|
|
114
|
+
* terminal the call is ignored — a redirected transcript must not collect
|
|
115
|
+
* frames of a box nobody can see.
|
|
116
|
+
* @param rows - the rows to display, top to bottom.
|
|
117
|
+
* @param cursor - where to leave the terminal cursor among them.
|
|
118
|
+
* @param focus - whether the region holds input focus. Without it the cursor
|
|
119
|
+
* stays hidden: a block cursor parked on a display row (the status line,
|
|
120
|
+
* a streaming line) reads as content colliding with it.
|
|
121
|
+
*/
|
|
122
|
+
setRegion(rows: readonly string[], cursor: RegionCursor, focus?: boolean): void;
|
|
123
|
+
/** Remove the region, leaving the cursor where the next write will land. */
|
|
124
|
+
clearRegion(): void;
|
|
125
|
+
/** Move to the region's first row and erase everything from there down. */
|
|
126
|
+
private eraseRegion;
|
|
127
|
+
/**
|
|
128
|
+
* Draw rows from the cursor down and place the cursor among them.
|
|
129
|
+
* @param rows - the rows to draw.
|
|
130
|
+
* @param cursor - the target position.
|
|
131
|
+
* @param focus - whether to show the cursor at that position afterwards.
|
|
132
|
+
*/
|
|
133
|
+
private drawRegion;
|
|
134
|
+
/** Ring the terminal bell; a pipe gets nothing to beep with. */
|
|
135
|
+
bell(): void;
|
|
136
|
+
/**
|
|
137
|
+
* Set the terminal window title.
|
|
138
|
+
* @param title - the title text; control bytes are the terminal's to reject.
|
|
139
|
+
*/
|
|
140
|
+
setTitle(title: string): void;
|
|
141
|
+
/**
|
|
142
|
+
* Read one line from a piped stream.
|
|
143
|
+
*
|
|
144
|
+
* Only the non-terminal shape reads this way; with the keyboard owned, input
|
|
145
|
+
* arrives as keys and the caller drives an editor instead.
|
|
146
|
+
* @param signal - aborts the pending read.
|
|
147
|
+
* @returns the line, or undefined when input ended or the read aborted.
|
|
148
|
+
*/
|
|
149
|
+
readLine(signal?: AbortSignal): Promise<string | undefined>;
|
|
150
|
+
/** Restore the terminal and stop reading. */
|
|
151
|
+
close(): void;
|
|
152
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom slash commands from Markdown files.
|
|
3
|
+
*
|
|
4
|
+
* A file named `review.md` under a commands root becomes `/review`: its
|
|
5
|
+
* frontmatter `description` labels it in the completion menu, and its body is
|
|
6
|
+
* the prompt submitted when the command runs — with `$ARGUMENTS` replaced by
|
|
7
|
+
* whatever followed the name. They are canned prompts, not handlers: execution
|
|
8
|
+
* goes through the same submission path as a typed message.
|
|
9
|
+
* @module codsh-cli/src/custom-commands
|
|
10
|
+
*/
|
|
11
|
+
/** One loaded command. */
|
|
12
|
+
export interface CustomCommand {
|
|
13
|
+
/** The name typed after the slash. */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Menu label, from frontmatter or a fallback naming the file. */
|
|
16
|
+
description: string;
|
|
17
|
+
/** The prompt body, with `$ARGUMENTS` placeholders intact. */
|
|
18
|
+
template: string;
|
|
19
|
+
}
|
|
20
|
+
/** What loading produced: the commands, and what was skipped and why. */
|
|
21
|
+
export interface CustomCommandLoad {
|
|
22
|
+
commands: CustomCommand[];
|
|
23
|
+
/** One line per skipped file, for the surface to show once at startup. */
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse one command file: optional `---` frontmatter with a `description`
|
|
28
|
+
* line, then the prompt body.
|
|
29
|
+
* @param source - the file content.
|
|
30
|
+
* @returns the description (empty when absent) and the body.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseCommandFile(source: string): {
|
|
33
|
+
description: string;
|
|
34
|
+
body: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Fill a template with the typed arguments.
|
|
38
|
+
* @param template - the prompt body.
|
|
39
|
+
* @param typed - what followed the command name, possibly empty.
|
|
40
|
+
* @returns the prompt to submit: placeholders replaced, or the arguments
|
|
41
|
+
* appended when the template never asked for them.
|
|
42
|
+
*/
|
|
43
|
+
export declare function expandTemplate(template: string, typed: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Load every command under the given roots, later roots shadowing earlier.
|
|
46
|
+
*
|
|
47
|
+
* A missing root is normal (most setups define no custom commands); an
|
|
48
|
+
* unreadable or misnamed file is a warning, never a failed startup — a broken
|
|
49
|
+
* canned prompt should cost that prompt, not the session.
|
|
50
|
+
* @param roots - directories to scan, lowest precedence first.
|
|
51
|
+
* @param taken - names already registered, which a file cannot shadow.
|
|
52
|
+
* @returns the loaded commands and the warnings to show once.
|
|
53
|
+
*/
|
|
54
|
+
export declare function loadCustomCommands(roots: readonly string[], taken: ReadonlySet<string>): Promise<CustomCommandLoad>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt's editing model: a multi-line buffer, a cursor, history, and the
|
|
3
|
+
* completion menu.
|
|
4
|
+
*
|
|
5
|
+
* Pure state. It takes keys and answers with what changed, so every behaviour
|
|
6
|
+
* here is testable without a terminal — the rendering and the raw-mode plumbing
|
|
7
|
+
* are somebody else's job.
|
|
8
|
+
* @module codsh-cli/src/editor
|
|
9
|
+
*/
|
|
10
|
+
import type { CompletableCommand, CompletionResult } from './completion.ts';
|
|
11
|
+
import type { Key } from './keys.ts';
|
|
12
|
+
/** One entry offered in the completion menu. */
|
|
13
|
+
export interface Candidate {
|
|
14
|
+
/** The text that replaces the token when accepted. */
|
|
15
|
+
value: string;
|
|
16
|
+
/** What it does, shown beside it. Empty for a path, which explains itself. */
|
|
17
|
+
detail: string;
|
|
18
|
+
}
|
|
19
|
+
/** What the editor is showing right now. */
|
|
20
|
+
export interface EditorView {
|
|
21
|
+
/** Buffer lines; always at least one, possibly empty. */
|
|
22
|
+
lines: readonly string[];
|
|
23
|
+
/** Cursor line index. */
|
|
24
|
+
row: number;
|
|
25
|
+
/** Cursor column within that line, in code points. */
|
|
26
|
+
column: number;
|
|
27
|
+
/** Candidates to offer, empty when the menu is closed. */
|
|
28
|
+
candidates: readonly Candidate[];
|
|
29
|
+
/** Which candidate is selected, meaningless when there are none. */
|
|
30
|
+
selected: number;
|
|
31
|
+
/** The token under the cursor, which is what the candidates matched. */
|
|
32
|
+
token: string;
|
|
33
|
+
}
|
|
34
|
+
/** What the caller must do after a key. */
|
|
35
|
+
export type EditorAction = {
|
|
36
|
+
kind: 'none';
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'submit';
|
|
39
|
+
text: string;
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'interrupt';
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'eof';
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'escape';
|
|
46
|
+
};
|
|
47
|
+
/** How the editor finds candidates for the token under the cursor. */
|
|
48
|
+
export interface EditorSources {
|
|
49
|
+
/** The commands currently registered. */
|
|
50
|
+
commands(): readonly CompletableCommand[];
|
|
51
|
+
/** Path candidates for an `@` mention, as the completer produces them. */
|
|
52
|
+
paths(token: string): CompletionResult;
|
|
53
|
+
/**
|
|
54
|
+
* Argument candidates for one command's first argument.
|
|
55
|
+
*
|
|
56
|
+
* `/plan off`, `/permission workspace-write`, `/model <id>` — the values a
|
|
57
|
+
* command takes are the command's own knowledge, so the editor asks rather
|
|
58
|
+
* than guessing. Absent or empty means the argument is free-form.
|
|
59
|
+
* @param command - the command name without its slash.
|
|
60
|
+
* @param typed - what has been typed of the argument so far.
|
|
61
|
+
* @returns candidates to offer, best first.
|
|
62
|
+
*/
|
|
63
|
+
commandArguments?(command: string, typed: string): readonly Candidate[];
|
|
64
|
+
}
|
|
65
|
+
/** A multi-line prompt editor. */
|
|
66
|
+
export declare class Editor {
|
|
67
|
+
private readonly sources;
|
|
68
|
+
private lines;
|
|
69
|
+
private row;
|
|
70
|
+
private column;
|
|
71
|
+
private candidates;
|
|
72
|
+
private selected;
|
|
73
|
+
private readonly history;
|
|
74
|
+
/** Where the caller is in history; equals `history.length` when not browsing. */
|
|
75
|
+
private browsing;
|
|
76
|
+
/** The buffer set aside while history is being browsed. */
|
|
77
|
+
private stashed;
|
|
78
|
+
constructor(sources: EditorSources);
|
|
79
|
+
/** What to render. */
|
|
80
|
+
get view(): EditorView;
|
|
81
|
+
/** The buffer as one string. */
|
|
82
|
+
get text(): string;
|
|
83
|
+
/** Whether nothing has been typed. */
|
|
84
|
+
get empty(): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Replace the buffer with earlier text, cursor at its end.
|
|
87
|
+
*
|
|
88
|
+
* This is recall-for-editing: the second Escape puts the previous submission
|
|
89
|
+
* back so it can be corrected and resent.
|
|
90
|
+
* @param text - the text to edit, possibly multi-line.
|
|
91
|
+
*/
|
|
92
|
+
prefill(text: string): void;
|
|
93
|
+
/** Submissions this session recorded, oldest first, for persistence. */
|
|
94
|
+
get pastSubmissions(): readonly string[];
|
|
95
|
+
/**
|
|
96
|
+
* Preload history from an earlier session.
|
|
97
|
+
*
|
|
98
|
+
* Applied before any live submission, so recall starts where the last
|
|
99
|
+
* session ended rather than empty.
|
|
100
|
+
* @param entries - past submissions, oldest first.
|
|
101
|
+
*/
|
|
102
|
+
seedHistory(entries: readonly string[]): void;
|
|
103
|
+
/**
|
|
104
|
+
* Apply one key.
|
|
105
|
+
* @param key - the decoded keystroke.
|
|
106
|
+
* @returns what the caller must do about it.
|
|
107
|
+
*/
|
|
108
|
+
handle(key: Key): EditorAction;
|
|
109
|
+
/** The line the cursor is on. */
|
|
110
|
+
private line;
|
|
111
|
+
/** Replace the cursor's line. */
|
|
112
|
+
private setLine;
|
|
113
|
+
/**
|
|
114
|
+
* Insert text at the cursor, splitting lines on newlines.
|
|
115
|
+
* @param text - the text to insert.
|
|
116
|
+
* @returns always `none`; insertion never completes a read.
|
|
117
|
+
*/
|
|
118
|
+
private insert;
|
|
119
|
+
/**
|
|
120
|
+
* Submit, or accept the highlighted candidate when the menu is open.
|
|
121
|
+
* @returns the submission, or `none` when a candidate was taken instead.
|
|
122
|
+
*/
|
|
123
|
+
private accept;
|
|
124
|
+
/** Record a submission for history, collapsing an immediate repeat. */
|
|
125
|
+
private remember;
|
|
126
|
+
/**
|
|
127
|
+
* Open the menu, or move through it when it is already open.
|
|
128
|
+
* @returns always `none`.
|
|
129
|
+
*/
|
|
130
|
+
private complete;
|
|
131
|
+
/**
|
|
132
|
+
* Replace the token under the cursor with the selected candidate.
|
|
133
|
+
* @returns always `none`.
|
|
134
|
+
*/
|
|
135
|
+
private take;
|
|
136
|
+
/** Where the token under the cursor begins, in code points. */
|
|
137
|
+
private tokenStart;
|
|
138
|
+
/** The token under the cursor. */
|
|
139
|
+
private token;
|
|
140
|
+
/**
|
|
141
|
+
* Recompute the candidate list for the token under the cursor.
|
|
142
|
+
*
|
|
143
|
+
* Recomputed on every edit rather than only on Tab, which is what makes the
|
|
144
|
+
* menu appear as a command is typed instead of after a key that asks for it.
|
|
145
|
+
*/
|
|
146
|
+
private refresh;
|
|
147
|
+
/** Remove the character before the cursor, joining lines at a boundary. */
|
|
148
|
+
private backspace;
|
|
149
|
+
/** Remove the character after the cursor, joining lines at a boundary. */
|
|
150
|
+
private forwardDelete;
|
|
151
|
+
/** Move up a line, or back through history from the first line. */
|
|
152
|
+
private moveUp;
|
|
153
|
+
/** Move down a line, or forward through history from the last line. */
|
|
154
|
+
private moveDown;
|
|
155
|
+
/**
|
|
156
|
+
* Step through history.
|
|
157
|
+
* @param delta - -1 for older, 1 for newer.
|
|
158
|
+
* @returns always `none`.
|
|
159
|
+
*/
|
|
160
|
+
private recall;
|
|
161
|
+
/** Move the cursor one position left, wrapping to the previous line. */
|
|
162
|
+
private moveLeft;
|
|
163
|
+
/** Move the cursor one position right, wrapping to the next line. */
|
|
164
|
+
private moveRight;
|
|
165
|
+
/**
|
|
166
|
+
* Put the cursor at a column on the current line.
|
|
167
|
+
* @param column - the target column.
|
|
168
|
+
* @returns always `none`.
|
|
169
|
+
*/
|
|
170
|
+
private jump;
|
|
171
|
+
/** Drop everything after the cursor on this line. */
|
|
172
|
+
private killLine;
|
|
173
|
+
/** Drop everything before the cursor on this line. */
|
|
174
|
+
private killInput;
|
|
175
|
+
/** Move the cursor to the start of the word before it. */
|
|
176
|
+
private wordLeft;
|
|
177
|
+
/** Move the cursor past the end of the word after it. */
|
|
178
|
+
private wordRight;
|
|
179
|
+
/** Drop the word before the cursor. */
|
|
180
|
+
private killWord;
|
|
181
|
+
/**
|
|
182
|
+
* Close the menu, or report Escape when there is none to close.
|
|
183
|
+
* @returns `none` when a menu was dismissed, otherwise `escape`.
|
|
184
|
+
*/
|
|
185
|
+
private cancel;
|
|
186
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `codsh` — the interactive terminal surface. The bundle
|
|
3
|
+
* patch rides over dsh-base without Host, HTTP, or browser plugins; this runner
|
|
4
|
+
* composes one Agent from the roster's preset, renders its session log to the
|
|
5
|
+
* terminal, answers approvals and questions from the keyboard, and drives the
|
|
6
|
+
* conversation until the person leaves.
|
|
7
|
+
*
|
|
8
|
+
* The surface shares the process with the Agent, so it reads `ctx.agents`
|
|
9
|
+
* directly. The API gateway exists to carry out-of-process clients and would
|
|
10
|
+
* add a serialization hop with no reader on the other side.
|
|
11
|
+
*
|
|
12
|
+
* @module codsh
|
|
13
|
+
*/
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import z from '@deepseek-ai/schemastery';
|
|
16
|
+
/** Stable Cordis plugin name. */
|
|
17
|
+
export declare const name = "coding-cli-runner";
|
|
18
|
+
/** Core services required before the surface can compose an agent. */
|
|
19
|
+
export declare const inject: string[];
|
|
20
|
+
/** Plugin config: the invocation resolved from this app's injected provider service. */
|
|
21
|
+
export interface Config {
|
|
22
|
+
/** Opening task, or the empty string to start at the prompt. */
|
|
23
|
+
task: string;
|
|
24
|
+
/** Session to reopen: an id, `'latest'`, or the empty string for a new session. */
|
|
25
|
+
resume: string;
|
|
26
|
+
/** Preset id overriding the roster default, or the empty string to accept it. */
|
|
27
|
+
preset: string;
|
|
28
|
+
/** Render the answer to `task` and exit rather than entering the prompt. */
|
|
29
|
+
print: boolean;
|
|
30
|
+
/** Ring the terminal bell when a decision waits or a long turn ends. */
|
|
31
|
+
bell: boolean;
|
|
32
|
+
/** How long a `!` passthrough command may run before it is killed. */
|
|
33
|
+
bangTimeoutMs: number;
|
|
34
|
+
/** Output lines a `!` passthrough keeps before summarizing the rest. */
|
|
35
|
+
bangOutputLines: number;
|
|
36
|
+
}
|
|
37
|
+
export declare const Config: z<Config>;
|
|
38
|
+
/** The process streams the surface binds to; tests substitute captures. */
|
|
39
|
+
export declare const internals: {
|
|
40
|
+
input: NodeJS.ReadableStream;
|
|
41
|
+
output: NodeJS.WriteStream;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Mount the interactive terminal surface.
|
|
45
|
+
* @param ctx - plugin context carrying core services and the launcher-provided exit request.
|
|
46
|
+
* @param config - validated invocation config.
|
|
47
|
+
*/
|
|
48
|
+
export declare function apply(ctx: Context, config: Config): void;
|