@antst/dashi 0.1.0-alpha.10
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 +25 -0
- package/lib/application.d.ts +43 -0
- package/lib/application.js +273 -0
- package/lib/at-trigger.d.ts +8 -0
- package/lib/at-trigger.js +13 -0
- package/lib/catalogs.d.ts +25 -0
- package/lib/catalogs.js +235 -0
- package/lib/clipboard-image.d.ts +3 -0
- package/lib/clipboard-image.js +74 -0
- package/lib/completion-trigger.d.ts +2 -0
- package/lib/completion-trigger.js +7 -0
- package/lib/diff-view.d.ts +5 -0
- package/lib/diff-view.js +24 -0
- package/lib/external-editor.d.ts +3 -0
- package/lib/external-editor.js +39 -0
- package/lib/help.d.ts +2 -0
- package/lib/help.js +6 -0
- package/lib/history-records.d.ts +4 -0
- package/lib/history-records.js +14 -0
- package/lib/image-input.d.ts +8 -0
- package/lib/image-input.js +37 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +301 -0
- package/lib/input.d.ts +10 -0
- package/lib/input.js +222 -0
- package/lib/memory.d.ts +3 -0
- package/lib/memory.js +27 -0
- package/lib/mouse-input.d.ts +1 -0
- package/lib/mouse-input.js +6 -0
- package/lib/presentation.d.ts +9 -0
- package/lib/presentation.js +54 -0
- package/lib/renderer.d.ts +20 -0
- package/lib/renderer.js +435 -0
- package/lib/rewind.d.ts +18 -0
- package/lib/rewind.js +84 -0
- package/lib/session-export.d.ts +3 -0
- package/lib/session-export.js +27 -0
- package/lib/session-list.d.ts +15 -0
- package/lib/session-list.js +65 -0
- package/lib/session-runtime.d.ts +46 -0
- package/lib/session-runtime.js +1293 -0
- package/lib/shell-command.d.ts +10 -0
- package/lib/shell-command.js +65 -0
- package/lib/state.d.ts +508 -0
- package/lib/state.js +667 -0
- package/lib/terminal-guard.d.ts +14 -0
- package/lib/terminal-guard.js +76 -0
- package/lib/tool-presentation.d.ts +14 -0
- package/lib/tool-presentation.js +156 -0
- package/lib/transcript.d.ts +12 -0
- package/lib/transcript.js +323 -0
- package/lib/tui-root.d.ts +20 -0
- package/lib/tui-root.js +25 -0
- package/package.json +73 -0
- package/validated-dsh-versions.json +3 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { draftImage } from './image-input.js';
|
|
3
|
+
const APPLE_SCRIPT = `
|
|
4
|
+
ObjC.import('AppKit')
|
|
5
|
+
const pasteboard = $.NSPasteboard.generalPasteboard
|
|
6
|
+
let data = pasteboard.dataForType('public.png')
|
|
7
|
+
if (!data) {
|
|
8
|
+
const tiff = pasteboard.dataForType('public.tiff')
|
|
9
|
+
if (tiff) {
|
|
10
|
+
const image = $.NSBitmapImageRep.imageRepWithData(tiff)
|
|
11
|
+
if (image) data = image.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $())
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (!data) $.exit(2)
|
|
15
|
+
console.log(ObjC.unwrap(data.base64EncodedStringWithOptions(0)))
|
|
16
|
+
`.trim();
|
|
17
|
+
function capture(command, args, base64, path) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const child = spawn(command, args, {
|
|
20
|
+
...(path === undefined ? {} : { env: { ...process.env, PATH: path } }),
|
|
21
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
22
|
+
});
|
|
23
|
+
const chunks = [];
|
|
24
|
+
child.stdout.on('data', (chunk) => { chunks.push(chunk); });
|
|
25
|
+
child.once('error', error => {
|
|
26
|
+
if (error.code === 'ENOENT')
|
|
27
|
+
resolve({ found: false });
|
|
28
|
+
else
|
|
29
|
+
reject(error);
|
|
30
|
+
});
|
|
31
|
+
child.once('close', code => {
|
|
32
|
+
if (code !== 0)
|
|
33
|
+
return resolve({ found: true });
|
|
34
|
+
const output = Buffer.concat(chunks);
|
|
35
|
+
if (output.length === 0)
|
|
36
|
+
return resolve({ found: true });
|
|
37
|
+
if (!base64)
|
|
38
|
+
return resolve({ found: true, output: new Uint8Array(output) });
|
|
39
|
+
try {
|
|
40
|
+
const decoded = Buffer.from(output.toString('utf8').trim(), 'base64');
|
|
41
|
+
resolve({ found: true, ...(decoded.length === 0 ? {} : { output: new Uint8Array(decoded) }) });
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
reject(error);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** Read a PNG from an installed platform clipboard helper. */
|
|
50
|
+
export async function readClipboardImage(platform = process.platform, path) {
|
|
51
|
+
const attempts = platform === 'darwin'
|
|
52
|
+
? [
|
|
53
|
+
['pngpaste', ['-'], false],
|
|
54
|
+
['osascript', ['-l', 'JavaScript', '-e', APPLE_SCRIPT], true],
|
|
55
|
+
]
|
|
56
|
+
: platform === 'linux'
|
|
57
|
+
? [
|
|
58
|
+
['wl-paste', ['--no-newline', '--type', 'image/png'], false],
|
|
59
|
+
['xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o'], false],
|
|
60
|
+
]
|
|
61
|
+
: [];
|
|
62
|
+
let helperFound = false;
|
|
63
|
+
for (const [command, args, base64] of attempts) {
|
|
64
|
+
const result = await capture(command, args, base64, path);
|
|
65
|
+
helperFound ||= result.found;
|
|
66
|
+
if (result.output !== undefined)
|
|
67
|
+
return draftImage(result.output, 'image/png', 'clipboard.png', 'clipboard');
|
|
68
|
+
}
|
|
69
|
+
if (attempts.length === 0)
|
|
70
|
+
throw new Error('Clipboard image paste is supported on Linux and macOS.');
|
|
71
|
+
throw new Error(helperFound
|
|
72
|
+
? 'The clipboard does not contain a PNG image.'
|
|
73
|
+
: `Clipboard image paste needs ${platform === 'darwin' ? 'pngpaste or osascript' : 'wl-paste or xclip'}.`);
|
|
74
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { detectAtTrigger } from './at-trigger.js';
|
|
2
|
+
export function slashCompletionQuery(input) {
|
|
3
|
+
return /^\/([a-z0-9_-]*)$/u.exec(input)?.[1];
|
|
4
|
+
}
|
|
5
|
+
export function hasCompletionTrigger(input, caret) {
|
|
6
|
+
return slashCompletionQuery(input) !== undefined || detectAtTrigger(input, caret) !== undefined;
|
|
7
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
3
|
+
import type { Overlay } from './state.js';
|
|
4
|
+
import { type ToolPresenter } from './tool-presentation.js';
|
|
5
|
+
export declare function diffOverlay(ctx: Context, agent: Agent, presenter: ToolPresenter, mode: '' | 'turn', signal: AbortSignal): Promise<Overlay>;
|
package/lib/diff-view.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HUMAN_SHELL_STREAM_BYTES } from './shell-command.js';
|
|
2
|
+
import { boundedBody } from './tool-presentation.js';
|
|
3
|
+
import { foldCells } from './transcript.js';
|
|
4
|
+
export async function diffOverlay(ctx, agent, presenter, mode, signal) {
|
|
5
|
+
if (mode === 'turn') {
|
|
6
|
+
const events = agent.session.snapshotEvents();
|
|
7
|
+
const start = events.map(event => event.type).lastIndexOf('turn/start');
|
|
8
|
+
const cells = (start < 0 ? [] : foldCells(events.slice(start), presenter)).filter(cell => cell.pending !== true && cell.tool?.card === 'diff' && (cell.tool.diffs?.length ?? 0) > 0);
|
|
9
|
+
return { cells, kind: 'info', lines: cells.length === 0 ? ['No write/edit changes in the last turn.'] : [], title: 'Last turn diff' };
|
|
10
|
+
}
|
|
11
|
+
const command = 'git diff --no-ext-diff --no-color HEAD --';
|
|
12
|
+
const result = await ctx.shell.run(ctx.shell.resolve({
|
|
13
|
+
command, sandboxPolicy: ctx.sandboxPolicy.resolve({ session: agent.session }), signal,
|
|
14
|
+
stdoutMaxBytes: HUMAN_SHELL_STREAM_BYTES, timeoutMs: 30_000, workdir: agent.session.header.cwd,
|
|
15
|
+
}));
|
|
16
|
+
const output = [result.stdout.text, result.stderr.text].filter(Boolean).join('\n');
|
|
17
|
+
const cell = {
|
|
18
|
+
key: 'diff:working', kind: result.exitCode === 0 ? 'tool' : 'error', text: 'git diff',
|
|
19
|
+
tool: { card: 'terminal', title: 'git diff', status: result.exitCode === 0 ? output === '' ? 'clean' : 'changed'
|
|
20
|
+
: result.exitCode === null ? result.signal ?? 'failed' : `exit ${String(result.exitCode)}`,
|
|
21
|
+
...(output === '' ? {} : { body: boundedBody(output) }) },
|
|
22
|
+
};
|
|
23
|
+
return { cells: [cell], kind: 'info', lines: [], title: 'Working tree diff' };
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, readFile, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
export function runEditor(command, file, cwd) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn('/bin/sh', ['-c', `exec ${command} "$1"`, 'dashi-editor', file], {
|
|
8
|
+
cwd, stdio: 'inherit',
|
|
9
|
+
});
|
|
10
|
+
child.once('error', reject);
|
|
11
|
+
child.once('exit', (code, signal) => {
|
|
12
|
+
if (code === 0)
|
|
13
|
+
resolve();
|
|
14
|
+
else
|
|
15
|
+
reject(new Error(`editor exited with ${code === null ? signal ?? 'unknown status' : `code ${String(code)}`}`));
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** Edit one draft through the user's terminal editor and always remove its file. */
|
|
20
|
+
export async function editDraftExternally(text, cwd) {
|
|
21
|
+
const command = process.env.VISUAL?.trim() || process.env.EDITOR?.trim();
|
|
22
|
+
if (command === undefined || command === '')
|
|
23
|
+
throw new Error('set $VISUAL or $EDITOR to use Ctrl+G');
|
|
24
|
+
const directory = await mkdtemp(join(tmpdir(), 'dashi-editor-'));
|
|
25
|
+
const file = join(directory, 'draft.md');
|
|
26
|
+
try {
|
|
27
|
+
await writeFile(file, text, { encoding: 'utf8', mode: 0o600 });
|
|
28
|
+
await runEditor(command, file, cwd);
|
|
29
|
+
return await readFile(file, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
try {
|
|
33
|
+
await rm(file, { force: true });
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await rmdir(directory);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/lib/help.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const VERSION_LINE: string;
|
|
2
|
+
export declare const FLAG_HELP: readonly ["Launch flags: --inline | --fullscreen · --accessible · -r, --resume [NAME|UUID] · -c, --continue · --fork-session · --all", " -n, --name TITLE · --agent PRESET · --session-id UUID · --model ID [--provider ID] · --effort LEVEL", " --permission PRESET · --tools NAMES · --disallowedTools NAMES · --yolo | --dangerously-skip-permissions · --image PATH · -h, --help"];
|
package/lib/help.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import dashi from '../package.json' with { type: 'json' };
|
|
2
|
+
import dsh from '@deepseek-ai/dsh-cmdline/package.json' with { type: 'json' };
|
|
3
|
+
export const VERSION_LINE = `dashi ${dashi.version} on DSH ${dsh.version}`;
|
|
4
|
+
export const FLAG_HELP = [
|
|
5
|
+
'Launch flags: --inline | --fullscreen · --accessible · -r, --resume [NAME|UUID] · -c, --continue · --fork-session · --all', ' -n, --name TITLE · --agent PRESET · --session-id UUID · --model ID [--provider ID] · --effort LEVEL', ' --permission PRESET · --tools NAMES · --disallowedTools NAMES · --yolo | --dangerously-skip-permissions · --image PATH · -h, --help'
|
|
6
|
+
];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SessionHistoryRecord } from '@deepseek-ai/dsh-api-session-controller';
|
|
2
|
+
import { type SessionEvent } from '@deepseek-ai/dsh-session';
|
|
3
|
+
/** Expand the Controller's compact history wire records to canonical Session events. */
|
|
4
|
+
export declare function eventsFromRecords(records: readonly SessionHistoryRecord[]): SessionEvent[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { decodeStorageRecord } from '@deepseek-ai/dsh-session';
|
|
2
|
+
/** Expand the Controller's compact history wire records to canonical Session events. */
|
|
3
|
+
export function eventsFromRecords(records) {
|
|
4
|
+
return records.flatMap((record) => {
|
|
5
|
+
if (record.type === 'event')
|
|
6
|
+
return [record.event];
|
|
7
|
+
const encoded = record.event.type === 'chunkrow/text-chunks'
|
|
8
|
+
? { type: 'text-chunks', seq0: record.event.seq, time0: record.event.time, data: record.event.data }
|
|
9
|
+
: record.event.type === 'chunkrow/reasoning-chunks'
|
|
10
|
+
? { type: 'reasoning-chunks', seq0: record.event.seq, time0: record.event.time, data: record.event.data }
|
|
11
|
+
: { type: 'tool-call-chunks', seq0: record.event.seq, time0: record.event.time, data: record.event.data };
|
|
12
|
+
return decodeStorageRecord(encoded);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AttachmentStore, EncodedImageAttachment, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment';
|
|
2
|
+
import type { DraftAttachment } from './state.js';
|
|
3
|
+
export declare function isImagePath(path: string): boolean;
|
|
4
|
+
export declare function draftImage(data: Uint8Array, mediaType: ImageMediaType, name: string, path: string): DraftAttachment;
|
|
5
|
+
/** Snapshot explicit user paths into one disposable composer draft. */
|
|
6
|
+
export declare function readDraftImages(cwd: string, paths: readonly string[]): Promise<DraftAttachment[]>;
|
|
7
|
+
export declare function encodeDraftImages(images: readonly DraftAttachment[]): EncodedImageAttachment[];
|
|
8
|
+
export declare function admitDraftImages(attachments: AttachmentStore, images: readonly DraftAttachment[]): Promise<readonly ImageAttachmentRef[]>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname, resolve } from 'node:path';
|
|
3
|
+
const MEDIA_TYPES = {
|
|
4
|
+
'.gif': 'image/gif',
|
|
5
|
+
'.jpeg': 'image/jpeg',
|
|
6
|
+
'.jpg': 'image/jpeg',
|
|
7
|
+
'.png': 'image/png',
|
|
8
|
+
'.webp': 'image/webp',
|
|
9
|
+
};
|
|
10
|
+
export function isImagePath(path) {
|
|
11
|
+
return MEDIA_TYPES[extname(path).toLocaleLowerCase()] !== undefined;
|
|
12
|
+
}
|
|
13
|
+
export function draftImage(data, mediaType, name, path) {
|
|
14
|
+
return { bytes: data.byteLength, data, mediaType, name, path };
|
|
15
|
+
}
|
|
16
|
+
/** Snapshot explicit user paths into one disposable composer draft. */
|
|
17
|
+
export async function readDraftImages(cwd, paths) {
|
|
18
|
+
return Promise.all(paths.map(async (path) => {
|
|
19
|
+
const absolute = resolve(cwd, path);
|
|
20
|
+
const mediaType = MEDIA_TYPES[extname(absolute).toLocaleLowerCase()];
|
|
21
|
+
if (mediaType === undefined) {
|
|
22
|
+
throw new Error(`unsupported image extension for ${path}; accepted: .png, .jpg, .jpeg, .webp, .gif`);
|
|
23
|
+
}
|
|
24
|
+
const data = new Uint8Array(await readFile(absolute));
|
|
25
|
+
return draftImage(data, mediaType, basename(absolute), absolute);
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
export function encodeDraftImages(images) {
|
|
29
|
+
return images.map(image => ({
|
|
30
|
+
data: Buffer.from(image.data).toString('base64'), mediaType: image.mediaType, name: image.name,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
export function admitDraftImages(attachments, images) {
|
|
34
|
+
return attachments.saveImages(images.map(image => ({
|
|
35
|
+
data: image.data, mediaType: image.mediaType, name: image.name,
|
|
36
|
+
})));
|
|
37
|
+
}
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { exitOnStdinEnd } from '@deepseek-ai/dsh-cmdline';
|
|
5
|
+
import { createTerminalShell } from './application.js';
|
|
6
|
+
import { sessionOverlay } from './catalogs.js';
|
|
7
|
+
import { readClipboardImage } from './clipboard-image.js';
|
|
8
|
+
import { editDraftExternally } from './external-editor.js';
|
|
9
|
+
import { FLAG_HELP, VERSION_LINE } from './help.js';
|
|
10
|
+
import { formatSessionList, isSessionId, parseSessionListArgs, sessionMatches, sessionNotFound, } from './session-list.js';
|
|
11
|
+
import { createSessionRuntime, LaunchArgumentError, } from './session-runtime.js';
|
|
12
|
+
import { TuiRoot } from './tui-root.js';
|
|
13
|
+
export const name = 'dashi';
|
|
14
|
+
export const inject = [
|
|
15
|
+
'agentPresets', 'agents', 'attachments', 'authorization', 'cmdlineArgs', 'commands', 'credentials', 'fs', 'jobs', 'loader', 'permissionPresets', 'subagents',
|
|
16
|
+
'pluginInventory', 'sessionController', 'sandboxPolicy', 'sessionProjections', 'sessionQuery', 'sessions', 'settings', 'shell', 'skills', 'tools',
|
|
17
|
+
];
|
|
18
|
+
const validatedVersions = JSON.parse(readFileSync(new URL('../validated-dsh-versions.json', import.meta.url), 'utf8'));
|
|
19
|
+
function parseArgs(args) {
|
|
20
|
+
let accessible = false;
|
|
21
|
+
let agentPreset, sessionId;
|
|
22
|
+
let forkSession = false;
|
|
23
|
+
let inline = true;
|
|
24
|
+
let name;
|
|
25
|
+
let effort;
|
|
26
|
+
let model;
|
|
27
|
+
let permission;
|
|
28
|
+
let provider;
|
|
29
|
+
let tools;
|
|
30
|
+
let disallowedTools;
|
|
31
|
+
let resume;
|
|
32
|
+
let resumeAll = false;
|
|
33
|
+
let resumePicker = false;
|
|
34
|
+
let useContinue = false;
|
|
35
|
+
const images = [];
|
|
36
|
+
const prompt = [];
|
|
37
|
+
for (let index = 0; index < args.length; index++) {
|
|
38
|
+
const argument = args[index];
|
|
39
|
+
if (argument === '--accessible')
|
|
40
|
+
accessible = true;
|
|
41
|
+
else if (argument === '--inline')
|
|
42
|
+
inline = true;
|
|
43
|
+
else if (argument === '--fullscreen')
|
|
44
|
+
inline = false;
|
|
45
|
+
else if (['--name', '-n', '--agent', '--image', '--effort', '--model', '--permission', '--provider', '--tools', '--disallowedTools',
|
|
46
|
+
'--session-id'].includes(argument ?? '')) {
|
|
47
|
+
const value = args[++index];
|
|
48
|
+
if (value === undefined || value === '')
|
|
49
|
+
return undefined;
|
|
50
|
+
if (argument === '--name' || argument === '-n')
|
|
51
|
+
name = value;
|
|
52
|
+
else if (argument === '--agent')
|
|
53
|
+
agentPreset = value;
|
|
54
|
+
else if (argument === '--image')
|
|
55
|
+
images.push(value);
|
|
56
|
+
else if (argument === '--effort')
|
|
57
|
+
effort = value;
|
|
58
|
+
else if (argument === '--model')
|
|
59
|
+
model = value;
|
|
60
|
+
else if (argument === '--permission')
|
|
61
|
+
permission = value;
|
|
62
|
+
else if (argument === '--session-id')
|
|
63
|
+
sessionId = value;
|
|
64
|
+
else if (argument === '--tools' || argument === '--disallowedTools') {
|
|
65
|
+
const names = value.split(',').map(name => name.trim());
|
|
66
|
+
if (names.includes(''))
|
|
67
|
+
return undefined;
|
|
68
|
+
if (argument === '--tools')
|
|
69
|
+
tools = names;
|
|
70
|
+
else
|
|
71
|
+
disallowedTools = names;
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
provider = value;
|
|
75
|
+
}
|
|
76
|
+
else if (argument === '--resume' || argument === '-r') {
|
|
77
|
+
const value = args[index + 1];
|
|
78
|
+
if (value === '')
|
|
79
|
+
return undefined;
|
|
80
|
+
if (value === undefined || value.startsWith('-'))
|
|
81
|
+
resumePicker = true;
|
|
82
|
+
else
|
|
83
|
+
resume = args[++index];
|
|
84
|
+
}
|
|
85
|
+
else if (argument === '--all')
|
|
86
|
+
resumeAll = true;
|
|
87
|
+
else if (argument === '--continue' || argument === '-c')
|
|
88
|
+
useContinue = true;
|
|
89
|
+
else if (argument === '--fork-session')
|
|
90
|
+
forkSession = true;
|
|
91
|
+
else if (argument === '--yolo' || argument === '--dangerously-skip-permissions') {
|
|
92
|
+
permission = 'danger-full-access';
|
|
93
|
+
}
|
|
94
|
+
else if (argument?.startsWith('-') === true)
|
|
95
|
+
return undefined;
|
|
96
|
+
else if (argument !== undefined)
|
|
97
|
+
prompt.push(argument);
|
|
98
|
+
}
|
|
99
|
+
if (resume !== undefined && (resumePicker || useContinue) || resumePicker && useContinue
|
|
100
|
+
|| name !== undefined && (resume !== undefined || resumePicker || useContinue)
|
|
101
|
+
|| resumeAll && resume === undefined && !resumePicker || resumeAll && useContinue
|
|
102
|
+
|| provider !== undefined && model === undefined
|
|
103
|
+
|| sessionId !== undefined && (!isSessionId(sessionId) || resume !== undefined || resumePicker || useContinue)
|
|
104
|
+
|| agentPreset !== undefined && (resume !== undefined || resumePicker || useContinue)
|
|
105
|
+
|| forkSession && resume === undefined && !resumePicker && !useContinue)
|
|
106
|
+
return undefined;
|
|
107
|
+
return {
|
|
108
|
+
accessible,
|
|
109
|
+
continue: useContinue,
|
|
110
|
+
...(agentPreset === undefined ? {} : { agentPreset }), forkSession, inline,
|
|
111
|
+
resumeAll,
|
|
112
|
+
resumePicker,
|
|
113
|
+
...(images.length === 0 ? {} : { images }),
|
|
114
|
+
...(effort === undefined ? {} : { effort }),
|
|
115
|
+
...(model === undefined ? {} : { model }),
|
|
116
|
+
...(name === undefined ? {} : { name }),
|
|
117
|
+
...(permission === undefined ? {} : { permission }),
|
|
118
|
+
...(prompt.length === 0 ? {} : { prompt: prompt.join(' ') }),
|
|
119
|
+
...(provider === undefined ? {} : { provider }),
|
|
120
|
+
...(tools === undefined ? {} : { tools }),
|
|
121
|
+
...(disallowedTools === undefined ? {} : { disallowedTools }),
|
|
122
|
+
...(resume === undefined ? {} : { resume }), ...(sessionId === undefined ? {} : { sessionId }),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function packageVersion(path) {
|
|
126
|
+
const manifest = JSON.parse(readFileSync(path, 'utf8'));
|
|
127
|
+
return typeof manifest.version === 'string' ? manifest.version : 'unknown';
|
|
128
|
+
}
|
|
129
|
+
function runningDshVersions() {
|
|
130
|
+
try {
|
|
131
|
+
const entry = realpathSync(process.argv[1] ?? '');
|
|
132
|
+
const manifest = resolve(dirname(entry), '../package.json');
|
|
133
|
+
const version = packageVersion(manifest);
|
|
134
|
+
try {
|
|
135
|
+
return [version, packageVersion(createRequire(manifest).resolve('@deepseek-ai/dsh-base/package.json'))];
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return [version, 'unknown'];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return ['unknown', 'unknown'];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function warnDshVersion() {
|
|
146
|
+
const [version, baseVersion] = runningDshVersions();
|
|
147
|
+
if (baseVersion !== version) {
|
|
148
|
+
process.stderr.write(`dashi: warning: DSH ${version} loads @deepseek-ai/dsh-base ${baseVersion}; versions must match\n`);
|
|
149
|
+
}
|
|
150
|
+
if (!validatedVersions.includes(version)) {
|
|
151
|
+
process.stderr.write(`dashi: warning: DSH ${version} is not validated; validated: ${validatedVersions.join(', ')}\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function installSessionList(ctx, options, ready, exit) {
|
|
155
|
+
const abort = new AbortController();
|
|
156
|
+
let listing;
|
|
157
|
+
const announce = ready.onReady(() => {
|
|
158
|
+
warnDshVersion();
|
|
159
|
+
listing = ctx.sessionController.list({}, abort.signal).then(({ items }) => {
|
|
160
|
+
const cwd = options.all ? undefined : resolve(options.cwd ?? process.cwd());
|
|
161
|
+
process.stdout.write(`${formatSessionList(items, cwd, options.json)}\n`);
|
|
162
|
+
exit(0);
|
|
163
|
+
}).catch((error) => {
|
|
164
|
+
process.stderr.write(`dashi: sessions list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
165
|
+
exit(1);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
ctx.effect(() => async () => {
|
|
169
|
+
announce();
|
|
170
|
+
abort.abort();
|
|
171
|
+
await listing;
|
|
172
|
+
}, 'dashi.sessions-list');
|
|
173
|
+
}
|
|
174
|
+
export function apply(ctx) {
|
|
175
|
+
const exit = ctx.get('appExit');
|
|
176
|
+
const ready = ctx.get('appReady');
|
|
177
|
+
const cmdline = ctx.get('cmdlineArgs');
|
|
178
|
+
if (exit === undefined || ready === undefined || cmdline === undefined) {
|
|
179
|
+
throw new Error('dashi: the launcher must provide ctx.appExit, ctx.appReady, and ctx.cmdlineArgs');
|
|
180
|
+
}
|
|
181
|
+
const args = cmdline.get();
|
|
182
|
+
if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
|
|
183
|
+
process.stdout.write(`${VERSION_LINE}\n\nUsage: dashi [options] [prompt]\n\n${FLAG_HELP.join('\n')}\n`);
|
|
184
|
+
exit(0);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (args[0] === 'sessions') {
|
|
188
|
+
const options = parseSessionListArgs(args);
|
|
189
|
+
if (options === undefined) {
|
|
190
|
+
process.stderr.write(`dashi: unsupported arguments: ${args.join(' ')}\n`);
|
|
191
|
+
exit(2);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
installSessionList(ctx, options, ready, exit);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const parsed = parseArgs(args);
|
|
198
|
+
if (parsed === undefined) {
|
|
199
|
+
process.stderr.write(`dashi: unsupported arguments: ${args.join(' ')}\n`);
|
|
200
|
+
exit(2);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const tuiRoot = new TuiRoot(ctx);
|
|
204
|
+
let shell;
|
|
205
|
+
let runtime;
|
|
206
|
+
let boot;
|
|
207
|
+
const stdoutError = (error) => { void shell?.fail(error); };
|
|
208
|
+
process.stdout.on('error', stdoutError);
|
|
209
|
+
const changed = ctx.on('tui/root-changed', (_previous, current) => {
|
|
210
|
+
if (current === undefined)
|
|
211
|
+
shell?.dispatch({ type: 'root-cleared' });
|
|
212
|
+
});
|
|
213
|
+
const announce = ready.onReady(() => {
|
|
214
|
+
warnDshVersion();
|
|
215
|
+
boot = (async () => {
|
|
216
|
+
const cwd = process.cwd();
|
|
217
|
+
const makeRuntime = (options) => createSessionRuntime(ctx, options, tuiRoot, action => { shell?.dispatch(action); }, {
|
|
218
|
+
cancel: id => { shell?.cancelDecision(id); },
|
|
219
|
+
request: decision => shell?.requestDecision(decision)
|
|
220
|
+
?? Promise.reject(new Error('dashi terminal is not ready')),
|
|
221
|
+
});
|
|
222
|
+
const sendPrompt = () => {
|
|
223
|
+
if (parsed.prompt !== undefined)
|
|
224
|
+
shell?.dispatch({ type: 'composer-changed', text: parsed.prompt });
|
|
225
|
+
if (parsed.prompt !== undefined)
|
|
226
|
+
shell?.dispatch({ type: 'submit' });
|
|
227
|
+
};
|
|
228
|
+
let pickerItems = parsed.resumePicker
|
|
229
|
+
? (await ctx.sessionController.list({}, new AbortController().signal)).items : undefined;
|
|
230
|
+
if (parsed.resume !== undefined && !isSessionId(parsed.resume)) {
|
|
231
|
+
const matches = sessionMatches((await ctx.sessionController.list({}, new AbortController().signal)).items, parsed.resume, parsed.resumeAll ? undefined : cwd);
|
|
232
|
+
const [match] = matches;
|
|
233
|
+
if (match === undefined)
|
|
234
|
+
throw sessionNotFound(parsed.resume);
|
|
235
|
+
if (matches.length > 1)
|
|
236
|
+
pickerItems = matches;
|
|
237
|
+
else
|
|
238
|
+
runtime = await makeRuntime({ ...parsed, cwd, resume: String(match.sessionId) });
|
|
239
|
+
}
|
|
240
|
+
else if (!parsed.resumePicker)
|
|
241
|
+
runtime = await makeRuntime({ ...parsed, cwd });
|
|
242
|
+
const initial = runtime;
|
|
243
|
+
shell = createTerminalShell({
|
|
244
|
+
accessible: parsed.accessible,
|
|
245
|
+
activateOverlay: async (value) => {
|
|
246
|
+
if (runtime !== undefined)
|
|
247
|
+
return runtime.activate(value);
|
|
248
|
+
if (value.kind !== 'resume')
|
|
249
|
+
return;
|
|
250
|
+
runtime = await makeRuntime({ ...parsed, continue: false, cwd, resume: value.sessionId });
|
|
251
|
+
shell?.dispatch({ type: 'root-bound', cells: runtime.initialCells, hasMore: runtime.initialHasMore,
|
|
252
|
+
prompts: runtime.initialPrompts, root: runtime.root });
|
|
253
|
+
for (const attachment of runtime.initialAttachments)
|
|
254
|
+
shell?.dispatch({ type: 'attachment-added', attachment });
|
|
255
|
+
runtime.start();
|
|
256
|
+
sendPrompt();
|
|
257
|
+
},
|
|
258
|
+
attach: path => runtime?.attach(path) ?? Promise.resolve(undefined),
|
|
259
|
+
beforeExit: () => runtime?.shutdown() ?? Promise.resolve(),
|
|
260
|
+
complete: (query, caret) => runtime?.complete(query, caret) ?? Promise.resolve([]),
|
|
261
|
+
cwd,
|
|
262
|
+
cyclePermission: () => runtime?.cyclePermission() ?? Promise.resolve(),
|
|
263
|
+
exit,
|
|
264
|
+
externalEdit: editDraftExternally,
|
|
265
|
+
...(initial === undefined ? {} : {
|
|
266
|
+
initialAttachments: initial.initialAttachments, initialCells: initial.initialCells, initialHasMore: initial.initialHasMore,
|
|
267
|
+
initialPrompts: initial.initialPrompts, initialRoot: initial.root,
|
|
268
|
+
}),
|
|
269
|
+
inline: parsed.inline || parsed.accessible,
|
|
270
|
+
interrupt: () => { runtime?.interrupt(); },
|
|
271
|
+
loadHistory: rootId => runtime?.loadHistory(rootId) ?? Promise.resolve(),
|
|
272
|
+
onReleased: () => { process.stdout.write(`${runtime?.summary ?? ''}\n`); },
|
|
273
|
+
pasteImage: readClipboardImage,
|
|
274
|
+
search: (query, rootId) => runtime?.search(query, rootId) ?? Promise.resolve(),
|
|
275
|
+
submit: (text, mode, attachments) => runtime?.submit(text, mode, attachments),
|
|
276
|
+
});
|
|
277
|
+
shell.start();
|
|
278
|
+
if (pickerItems !== undefined) {
|
|
279
|
+
shell.dispatch({ type: 'open-overlay', overlay: sessionOverlay(pickerItems, '', parsed.resumeAll ? undefined : cwd) });
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
runtime?.start();
|
|
283
|
+
sendPrompt();
|
|
284
|
+
}
|
|
285
|
+
})().catch(async (error) => {
|
|
286
|
+
process.stderr.write(`dashi: startup failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
287
|
+
await runtime?.shutdown();
|
|
288
|
+
await shell?.dispose();
|
|
289
|
+
exit(error instanceof LaunchArgumentError ? 2 : 1);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
ctx.effect(() => async () => {
|
|
293
|
+
announce();
|
|
294
|
+
changed();
|
|
295
|
+
await boot;
|
|
296
|
+
await runtime?.shutdown();
|
|
297
|
+
await shell?.dispose();
|
|
298
|
+
process.stdout.off('error', stdoutError);
|
|
299
|
+
}, 'dashi.profile-boot');
|
|
300
|
+
exitOnStdinEnd(ctx, 'dashi.stdin');
|
|
301
|
+
}
|
package/lib/input.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type TUI } from '@earendil-works/pi-tui';
|
|
2
|
+
import type { ComposerCursor, UiAction, ViewState } from './state.js';
|
|
3
|
+
export interface InputBindings {
|
|
4
|
+
readonly dispatch: (action: UiAction) => void;
|
|
5
|
+
readonly insertNewline?: () => void;
|
|
6
|
+
readonly readComposerCursor?: () => ComposerCursor;
|
|
7
|
+
readonly readState: () => ViewState;
|
|
8
|
+
}
|
|
9
|
+
export declare function caretOffset(text: string, cursor: ComposerCursor): number;
|
|
10
|
+
export declare function installInput(tui: TUI, bindings: InputBindings): () => void;
|