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.
@@ -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
- process.stdout.on('resize', () => {
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
- rl.on('SIGINT', () => {
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
- process.on('uncaughtException', (err) => { if (!err.message?.includes('readline')) O(r(`\n ⚠ ${err.message}`) + '\n'); });
213
- process.on('unhandledRejection', (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'); });
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
- try {
659
- const deeperMd = join(process.cwd(), 'deeper.md');
660
- if (existsSync(deeperMd)) {
661
- const ctx = readFileSync(deeperMd, 'utf-8').slice(0, 4000);
662
- if (ctx.trim() && !ctx.includes('<!-- 填写')) {
663
- r.push({ role: 'system', content: `[项目上下文 deeper.md]\n${ctx}` });
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 MR = 2, TO = 90_000;
907
- const ac = new AbortController(); const t = setTimeout(() => ac.abort(), TO);
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 resp = await fetch(`${opts.baseUrl}/v1/chat/completions`, {
910
- method: 'POST', signal: ac.signal,
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')) && 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); }
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
- } finally { clearTimeout(t); }
944
+ }
935
945
  }
936
946
 
937
- async function* sseIter(body: ReadableStream<Uint8Array>): AsyncIterable<StreamChunk> {
938
- const reader = body.getReader(); const dec = new TextDecoder(); let buf = '';
939
- try {
940
- while (true) {
941
- const { done, value } = await reader.read(); if (done) break;
942
- buf += dec.decode(value, { stream: true });
943
- if (buf.length > 65536) buf = buf.slice(-32768);
944
- const lines = buf.split('\n'); buf = lines.pop() || '';
945
- for (const line of lines) {
946
- const tr = line.trim(); if (!tr || tr.startsWith(':')) continue; if (!tr.startsWith('data: ')) continue;
947
- const d = tr.slice(6).trim(); if (d === '[DONE]') { yield { type: 'done' }; return; }
948
- try {
949
- const p = JSON.parse(d); const ch = p.choices?.[0]; if (!ch) continue;
950
- const dl = ch.delta;
951
- if (dl?.content) yield { type: 'text', content: dl.content };
952
- if (dl?.reasoning_content) yield { type: 'thinking', content: dl.reasoning_content };
953
- if (dl?.tool_calls) for (const tc of dl.tool_calls) {
954
- if (tc.id) yield { type: 'tool_call_start', tool_call: { id: tc.id, name: tc.function?.name || '' } };
955
- if (tc.function?.arguments) yield { type: 'tool_call_args', content: tc.function.arguments };
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
- } catch (e: unknown) {
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 result;
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 result;
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 | null {
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
- const currentCall = this.toolCallBuffer.get(toolCalls[0].index as number);
149
- if (currentCall) {
150
- return {
151
- type: 'tool_call',
152
- tool_call: { ...currentCall, arguments: { ...currentCall.arguments } },
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 null;
158
+ return results;
157
159
  }
158
160
  }
@@ -43,6 +43,7 @@ export interface ToolCall {
43
43
  id: string;
44
44
  name: string;
45
45
  arguments: Record<string, unknown>;
46
+ index?: number;
46
47
  }
47
48
 
48
49
  export interface ToolCallResult {