deeper-cli 1.0.5 → 1.0.6
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/dist/cli/index.js +773 -266
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/chat-repl.ts +94 -85
- package/src/model/DeepSeekClient.ts +4 -2
- package/src/model/StreamHandler.ts +12 -10
- package/src/tools/tool-types.ts +1 -0
package/src/cli/chat-repl.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import readline from 'node:readline';
|
|
2
2
|
import process from 'node:process';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, Stats } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { DEEPER_HOME } from '../core/constants.js';
|
|
6
6
|
import { TOOL_SAFETY_MAP } from '../tools/tool-types.js';
|
|
@@ -12,6 +12,8 @@ import { getTodos, todoSummary } from '../tools/builtin/ai/todo_manager.js';
|
|
|
12
12
|
import { estimateTokens } from '../tools/builtin/ai/token_count.js';
|
|
13
13
|
import { SkillEngine } from '../skills/SkillEngine.js';
|
|
14
14
|
import { MCPClient } from '../mcp/MCPClient.js';
|
|
15
|
+
import { DeepSeekClient } from '../model/DeepSeekClient.js';
|
|
16
|
+
import type { ChatMessage } from '../model/types.js';
|
|
15
17
|
import { O, Oflush, A, d, b, c, g, y, r, B, G, resetTimer, thinkingAnim, thinkingAnimAt } from '../ui/ansi.js';
|
|
16
18
|
|
|
17
19
|
export interface ReplOptions {
|
|
@@ -183,10 +185,11 @@ export async function startRepl(opts: ReplOptions): Promise<void> {
|
|
|
183
185
|
drawHeader();
|
|
184
186
|
|
|
185
187
|
let resizeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
186
|
-
|
|
188
|
+
const onResize = () => {
|
|
187
189
|
if (resizeTimer) return;
|
|
188
190
|
resizeTimer = setTimeout(() => { resizeTimer = null; Oflush(); }, 200);
|
|
189
|
-
}
|
|
191
|
+
};
|
|
192
|
+
process.stdout.on('resize', onResize);
|
|
190
193
|
|
|
191
194
|
let resolveLine: ((v: string) => void) | null = null;
|
|
192
195
|
let running = true;
|
|
@@ -204,13 +207,19 @@ export async function startRepl(opts: ReplOptions): Promise<void> {
|
|
|
204
207
|
return ok;
|
|
205
208
|
};
|
|
206
209
|
|
|
207
|
-
|
|
210
|
+
const onSigint = () => {
|
|
208
211
|
if (resolveLine) { const cb = resolveLine; resolveLine = null; cb('/quit'); return; }
|
|
209
212
|
Oflush();
|
|
213
|
+
process.stdout.removeListener('resize', onResize);
|
|
214
|
+
process.removeListener('uncaughtException', onUCE);
|
|
215
|
+
process.removeListener('unhandledRejection', onUHR);
|
|
210
216
|
xmemory.save().then(() => { O('\n' + y('再见!') + '\n'); running = false; rl.close(); process.exit(0); });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
};
|
|
218
|
+
rl.on('SIGINT', onSigint);
|
|
219
|
+
const onUCE = (err: Error) => { if (!err.message?.includes('readline')) O(r(`\n ⚠ ${err.message}`) + '\n'); };
|
|
220
|
+
process.on('uncaughtException', onUCE);
|
|
221
|
+
const onUHR = (reason: unknown) => { const m = reason instanceof Error ? reason.message : String(reason); if (!m.includes('readline') && !m.includes('Abort') && !m.includes('timeout')) O(r(`\n ⚠ ${m}`) + '\n'); };
|
|
222
|
+
process.on('unhandledRejection', onUHR);
|
|
214
223
|
|
|
215
224
|
while (running) {
|
|
216
225
|
currentPrompt = c('❯ ');
|
|
@@ -356,6 +365,9 @@ export async function startRepl(opts: ReplOptions): Promise<void> {
|
|
|
356
365
|
|
|
357
366
|
rl.close();
|
|
358
367
|
Oflush();
|
|
368
|
+
process.stdout.removeListener('resize', onResize);
|
|
369
|
+
process.removeListener('uncaughtException', onUCE);
|
|
370
|
+
process.removeListener('unhandledRejection', onUHR);
|
|
359
371
|
await xmemory.save();
|
|
360
372
|
if (mcpClient) mcpClient.disconnectAll();
|
|
361
373
|
Oflush();
|
|
@@ -654,30 +666,15 @@ Rules:
|
|
|
654
666
|
|
|
655
667
|
r.push({ role: 'system', content: sysContent });
|
|
656
668
|
|
|
657
|
-
// 自动注入 deeper.md 项目上下文 + rules
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
} catch { /* skip */ }
|
|
667
|
-
try {
|
|
668
|
-
const rulesFile = join(process.cwd(), '.deeper', 'rules.md');
|
|
669
|
-
if (existsSync(rulesFile)) {
|
|
670
|
-
const rules = readFileSync(rulesFile, 'utf-8').slice(0, 4000);
|
|
671
|
-
if (rules.trim()) r.push({ role: 'system', content: `[项目规则 rules.md]\n${rules}` });
|
|
672
|
-
}
|
|
673
|
-
} catch { /* skip */ }
|
|
674
|
-
try {
|
|
675
|
-
const globalRules = join(DEEPER_HOME, 'rules.md');
|
|
676
|
-
if (existsSync(globalRules)) {
|
|
677
|
-
const rules = readFileSync(globalRules, 'utf-8').slice(0, 2000);
|
|
678
|
-
if (rules.trim()) r.push({ role: 'system', content: `[全局规则]\n${rules}` });
|
|
679
|
-
}
|
|
680
|
-
} catch { /* skip */ }
|
|
669
|
+
// 自动注入 deeper.md 项目上下文 + rules 规则(缓存读取,按 mtime 刷新)
|
|
670
|
+
const deeperCtx = cachedRead(join(process.cwd(), 'deeper.md'));
|
|
671
|
+
if (deeperCtx && deeperCtx.trim() && !deeperCtx.includes('<!-- 填写')) {
|
|
672
|
+
r.push({ role: 'system', content: `[项目上下文 deeper.md]\n${deeperCtx}` });
|
|
673
|
+
}
|
|
674
|
+
const projRules = cachedRead(join(process.cwd(), '.deeper', 'rules.md'));
|
|
675
|
+
if (projRules && projRules.trim()) r.push({ role: 'system', content: `[项目规则 rules.md]\n${projRules}` });
|
|
676
|
+
const globalRules = cachedRead(join(DEEPER_HOME, 'rules.md'), 2000);
|
|
677
|
+
if (globalRules && globalRules.trim()) r.push({ role: 'system', content: `[全局规则]\n${globalRules}` });
|
|
681
678
|
|
|
682
679
|
for (const m of history.slice(-30)) {
|
|
683
680
|
const e: Record<string, unknown> = { role: m.role };
|
|
@@ -693,7 +690,20 @@ Rules:
|
|
|
693
690
|
return r;
|
|
694
691
|
}
|
|
695
692
|
|
|
696
|
-
function trimHistory(h: Message[], max: number) { while (h.length > max) h.shift(); }
|
|
693
|
+
function trimHistory(h: Message[], max: number) { while (h.length > max) h.shift(); }
|
|
694
|
+
|
|
695
|
+
const _fileCache = new Map<string, { mtime: number; content: string }>();
|
|
696
|
+
function cachedRead(path: string, maxLen = 4000): string | null {
|
|
697
|
+
if (!existsSync(path)) return null;
|
|
698
|
+
try {
|
|
699
|
+
const st = statSync(path);
|
|
700
|
+
const cached = _fileCache.get(path);
|
|
701
|
+
if (cached && cached.mtime === st.mtimeMs) return cached.content;
|
|
702
|
+
const content = readFileSync(path, 'utf-8').slice(0, maxLen);
|
|
703
|
+
_fileCache.set(path, { mtime: st.mtimeMs, content });
|
|
704
|
+
return content;
|
|
705
|
+
} catch { return null; }
|
|
706
|
+
}
|
|
697
707
|
|
|
698
708
|
function compressHistory(h: Message[]): void {
|
|
699
709
|
if (h.length <= 6) return;
|
|
@@ -895,7 +905,7 @@ async function loadBuiltinTools(): Promise<Tool[]> {
|
|
|
895
905
|
return builtinTools as Tool[];
|
|
896
906
|
}
|
|
897
907
|
|
|
898
|
-
// ========== API ==========
|
|
908
|
+
// ========== API (uses DeepSeekClient) ==========
|
|
899
909
|
|
|
900
910
|
interface StreamChunk {
|
|
901
911
|
type: 'text'|'thinking'|'tool_call_start'|'tool_call_args'|'tool_call_end'|'done'|'error';
|
|
@@ -903,65 +913,64 @@ interface StreamChunk {
|
|
|
903
913
|
}
|
|
904
914
|
|
|
905
915
|
async function callApi(opts: ReplOptions, msgs: Array<Record<string, unknown>>, tools: ToolDef[], retry = 0, cmt = 8192): Promise<AsyncIterable<StreamChunk>> {
|
|
906
|
-
const
|
|
907
|
-
|
|
916
|
+
const client = new DeepSeekClient({
|
|
917
|
+
apiKey: opts.apiKey || '',
|
|
918
|
+
model: opts.model || 'deepseek-chat',
|
|
919
|
+
baseUrl: opts.baseUrl || 'https://api.deepseek.com',
|
|
920
|
+
temperature: opts.temperature ?? 0,
|
|
921
|
+
maxTokens: cmt,
|
|
922
|
+
think: { enabled: true, budgetTokens: cmt },
|
|
923
|
+
});
|
|
924
|
+
const chatMsgs: ChatMessage[] = msgs.map(m => ({
|
|
925
|
+
role: (m.role as ChatMessage['role']) || 'user',
|
|
926
|
+
content: m.content != null ? String(m.content) : null,
|
|
927
|
+
tool_calls: m.tool_calls as ChatMessage['tool_calls'],
|
|
928
|
+
tool_call_id: m.tool_call_id as string | undefined,
|
|
929
|
+
name: m.name as string | undefined,
|
|
930
|
+
reasoning_content: m.reasoning_content as string | undefined,
|
|
931
|
+
thinking: m.thinking as string | undefined,
|
|
932
|
+
}));
|
|
908
933
|
try {
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${opts.apiKey}` },
|
|
912
|
-
body: JSON.stringify({
|
|
913
|
-
model: opts.model,
|
|
914
|
-
messages: msgs,
|
|
915
|
-
tools: tools.length > 0 ? tools : undefined,
|
|
916
|
-
tool_choice: tools.length > 0 ? 'auto' : undefined,
|
|
917
|
-
stream: true,
|
|
918
|
-
max_tokens: cmt,
|
|
919
|
-
temperature: opts.temperature,
|
|
920
|
-
}),
|
|
921
|
-
});
|
|
922
|
-
if (!resp.ok) {
|
|
923
|
-
const et = await resp.text().catch(() => '');
|
|
924
|
-
if (resp.status === 401) throw new Error('API Key 无效: deeper config set api_key "sk-xxx"');
|
|
925
|
-
if ((resp.status === 429 || resp.status >= 500) && retry < MR) { const d = Math.min(1000*(retry+1),5000); await new Promise(r2 => setTimeout(r2, d)); return callApi(opts, msgs, tools, retry+1, cmt); }
|
|
926
|
-
throw new Error(`HTTP ${resp.status}: ${et.slice(0,200)}`);
|
|
927
|
-
}
|
|
928
|
-
if (!resp.body) throw new Error('空响应');
|
|
929
|
-
return sseIter(resp.body);
|
|
934
|
+
const stream = await client.chatStream(chatMsgs, tools.map(t => ({ name: t.function.name, description: t.function.description, category: '', parameters: t.function.parameters as unknown as import('../tools/tool-types.js').JSONSchema })));
|
|
935
|
+
return adaptStream(stream);
|
|
930
936
|
} catch (e: unknown) {
|
|
931
937
|
const m = (e instanceof Error ? e.message : String(e)).toLowerCase();
|
|
932
|
-
if ((m.includes('timeout')||m.includes('abort')||m.includes('econn')
|
|
938
|
+
if ((m.includes('timeout')||m.includes('abort')||m.includes('econn')||m.includes('429')||m.includes('500')) && retry < 2) {
|
|
939
|
+
const d = Math.min(1000*(retry+1), 5000);
|
|
940
|
+
await new Promise(r2 => setTimeout(r2, d));
|
|
941
|
+
return callApi(opts, msgs, tools, retry+1, cmt);
|
|
942
|
+
}
|
|
933
943
|
throw e;
|
|
934
|
-
}
|
|
944
|
+
}
|
|
935
945
|
}
|
|
936
946
|
|
|
937
|
-
async function*
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
const
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
if (tc.id && tc.function?.arguments) yield { type: 'tool_call_end' };
|
|
957
|
-
}
|
|
958
|
-
if (ch.finish_reason === 'tool_calls') yield { type: 'tool_call_end' };
|
|
959
|
-
} catch { /* skip */ }
|
|
947
|
+
async function* adaptStream(stream: AsyncIterable<import('../model/types.js').StreamChunk>): AsyncIterable<StreamChunk> {
|
|
948
|
+
const seen = new Set<string>();
|
|
949
|
+
let hasPending = false;
|
|
950
|
+
for await (const chunk of stream) {
|
|
951
|
+
if (chunk.type === 'text' || chunk.type === 'thinking') {
|
|
952
|
+
yield { type: chunk.type, content: chunk.content };
|
|
953
|
+
} else if (chunk.type === 'tool_call') {
|
|
954
|
+
const tc = chunk.tool_call;
|
|
955
|
+
if (tc) {
|
|
956
|
+
const key = tc.id || tc.name;
|
|
957
|
+
const sa = JSON.stringify(tc.arguments || {});
|
|
958
|
+
if (!seen.has(key)) {
|
|
959
|
+
if (hasPending) yield { type: 'tool_call_end' };
|
|
960
|
+
seen.add(key); hasPending = true;
|
|
961
|
+
yield { type: 'tool_call_start', tool_call: { id: tc.id, name: tc.name } };
|
|
962
|
+
yield { type: 'tool_call_args', content: sa };
|
|
963
|
+
} else {
|
|
964
|
+
yield { type: 'tool_call_args', content: sa };
|
|
965
|
+
}
|
|
960
966
|
}
|
|
967
|
+
} else if (chunk.type === 'done') {
|
|
968
|
+
if (hasPending) yield { type: 'tool_call_end' };
|
|
969
|
+
yield { type: 'done' };
|
|
970
|
+
return;
|
|
971
|
+
} else {
|
|
972
|
+
yield chunk as StreamChunk;
|
|
961
973
|
}
|
|
962
|
-
}
|
|
963
|
-
if (e instanceof Error && e.name === 'AbortError') { /* ok */ }
|
|
964
|
-
else yield { type: 'error', error: e instanceof Error ? e.message : String(e) };
|
|
965
|
-
} finally { try { reader.releaseLock(); } catch { /* */ } }
|
|
974
|
+
}
|
|
966
975
|
yield { type: 'done' };
|
|
967
976
|
}
|
|
@@ -145,7 +145,8 @@ export class DeepSeekClient {
|
|
|
145
145
|
const data = trimmed.slice(6);
|
|
146
146
|
const result = handler.handleEvent('message', data);
|
|
147
147
|
if (result) {
|
|
148
|
-
yield
|
|
148
|
+
if (Array.isArray(result)) { for (const r of result) yield r; }
|
|
149
|
+
else yield result;
|
|
149
150
|
}
|
|
150
151
|
} else if (trimmed === 'data: [DONE]') {
|
|
151
152
|
yield { type: 'done' };
|
|
@@ -160,7 +161,8 @@ export class DeepSeekClient {
|
|
|
160
161
|
const data = trimmed.slice(6);
|
|
161
162
|
const result = handler.handleEvent('message', data);
|
|
162
163
|
if (result) {
|
|
163
|
-
yield
|
|
164
|
+
if (Array.isArray(result)) { for (const r of result) yield r; }
|
|
165
|
+
else yield result;
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
}
|
|
@@ -27,7 +27,7 @@ export class StreamHandler {
|
|
|
27
27
|
this.finished = false;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
handleEvent(event: string, data: string): StreamChunk | null {
|
|
30
|
+
handleEvent(event: string, data: string): StreamChunk | StreamChunk[] | null {
|
|
31
31
|
if (event === '[DONE]' || data === '[DONE]') {
|
|
32
32
|
this.finished = true;
|
|
33
33
|
return { type: 'done' };
|
|
@@ -112,7 +112,8 @@ export class StreamHandler {
|
|
|
112
112
|
return this.finished;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
private handleToolCallsDelta(toolCalls: Array<Record<string, unknown>>): StreamChunk
|
|
115
|
+
private handleToolCallsDelta(toolCalls: Array<Record<string, unknown>>): StreamChunk[] {
|
|
116
|
+
const results: StreamChunk[] = [];
|
|
116
117
|
for (const tc of toolCalls) {
|
|
117
118
|
const index = tc.index as number;
|
|
118
119
|
const id = tc.id as string | undefined;
|
|
@@ -123,6 +124,7 @@ export class StreamHandler {
|
|
|
123
124
|
id: id ?? '',
|
|
124
125
|
name: fn?.name as string ?? '',
|
|
125
126
|
arguments: {},
|
|
127
|
+
index,
|
|
126
128
|
});
|
|
127
129
|
}
|
|
128
130
|
|
|
@@ -143,16 +145,16 @@ export class StreamHandler {
|
|
|
143
145
|
existing.arguments = existing.arguments || {};
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
|
-
}
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
const entry = this.toolCallBuffer.get(index);
|
|
150
|
+
if (entry) {
|
|
151
|
+
results.push({
|
|
152
|
+
type: 'tool_call',
|
|
153
|
+
tool_call: { ...entry, arguments: { ...entry.arguments } },
|
|
154
|
+
} as StreamChunk);
|
|
155
|
+
}
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
return
|
|
158
|
+
return results;
|
|
157
159
|
}
|
|
158
160
|
}
|