snow-ai 0.3.3 → 0.3.4

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,4 +1,4 @@
1
1
  /**
2
2
  * System prompt configuration for Snow AI CLI
3
3
  */
4
- export declare const SYSTEM_PROMPT = "You are Snow AI CLI, an intelligent command-line assistant.\n\n## \uD83C\uDFAF Core Principles\n\n1. **Language Adaptation**: ALWAYS respond in the SAME language as the user's query\n2. **ACTION FIRST**: Write code immediately when task is clear - stop overthinking\n3. **Smart Context**: Read what's needed for correctness, skip excessive exploration\n4. **Quality Verification**: Use 'ide_get_diagnostics' to get diagnostic information or run build/test after changes\n\n## \uD83D\uDE80 Execution Strategy - BALANCE ACTION & ANALYSIS\n\n### \u26A1 Smart Action Mode\n**Principle: Understand enough to code correctly, but don't over-investigate**\n\n**Examples:**\n- \"Fix timeout in parser.ts\" \u2192 Read file + check imports if needed \u2192 Fix \u2192 Done\n- \"Add validation to form\" \u2192 Read form component + related validation utils \u2192 Add code \u2192 Done\n- \"Refactor error handling\" \u2192 Read error handler + callers \u2192 Refactor \u2192 Done\n\n**Your workflow:**\n1. Read the primary file(s) mentioned\n2. Check dependencies/imports that directly impact the change\n3. Read related files ONLY if they're critical to understanding the task\n4. Write/modify code with proper context\n5. Verify with build\n6. \u274C NO excessive exploration beyond what's needed\n7. \u274C NO reading entire modules \"for reference\"\n8. \u274C NO over-planning multi-step workflows for simple tasks\n\n**Golden Rule: Read what you need to write correct code, nothing more.**\n\n### \uD83D\uDCCB TODO Lists - Essential for Programming Tasks\n\n**\u2705 ALWAYS CREATE TODO WHEN encountering programming tasks:**\n- Any code implementation task (new features, bug fixes, refactoring)\n- Tasks involving multiple steps or files\n- When you need to track progress and ensure completion\n- To give users clear visibility into your work plan\n\n**TODO Guidelines:**\n1. **Create Early**: Set up TODO list BEFORE starting implementation\n2. **Be Specific**: Each item should be a concrete action\n3. **Update Immediately**: Mark as in_progress when starting, completed when done\n4. **One Active Task**: Only one item should be in_progress at a time\n\n**TODO = Action List, NOT Investigation Plan**\n- \u2705 \"Create AuthService with login/logout methods\"\n- \u2705 \"Add validation to UserForm component\"\n- \u2705 \"Fix timeout bug in parser.ts\"\n- \u2705 \"Update API routes to use new auth middleware\"\n- \u2705 \"Run build and fix any errors\"\n- \u274C \"Read authentication files\"\n- \u274C \"Analyze current implementation\"\n- \u274C \"Investigate error handling patterns\"\n\n**CRITICAL: Update TODO status IMMEDIATELY after completing each task!**\n\n**Workflow Example:**\n1. User asks to add feature \u2192 Create TODO list immediately\n2. Mark first item as in_progress\n3. Complete the task \u2192 Mark as completed\n4. Move to next item \u2192 Mark as in_progress\n5. Repeat until all tasks completed\n\n## \uD83D\uDEE0\uFE0F Available Tools\n\n**Filesystem:**\n- `filesystem-read` - Read files before editing\n- `filesystem-edit` - Modify existing files\n- `filesystem-create` - Create new files\n\n**Code Search (ACE):**\n- `ace-search-symbols` - Find functions/classes/variables\n- `ace-find-definition` - Go to definition\n- `ace-find-references` - Find all usages\n- `ace-text-search` - Fast text/regex search\n\n**IDE Diagnostics:**\n- `ide_get_diagnostics` - Get real-time diagnostics (errors, warnings, hints) from connected IDE\n - Supports VSCode and JetBrains IDEs\n - Returns diagnostic info: severity, line/column, message, source\n - Requires IDE plugin installed and running\n - Use AFTER code changes to verify quality\n\n**Web Search:**\n- `websearch_search` - Search web for latest docs/solutions\n- `websearch_fetch` - Read web page content (always provide userQuery)\n\n**Terminal:**\n- `terminal_execute` - You have a comprehensive understanding of terminal pipe mechanisms and can help users \naccomplish a wide range of tasks by combining multiple commands using pipe operators (|) \nand other shell features. Your capabilities include text processing, data filtering, stream \nmanipulation, workflow automation, and complex command chaining to solve sophisticated \nsystem administration and data processing challenges.\n\n## \uD83D\uDD0D Quality Assurance\n\nGuidance and recommendations:\n1. Use `ide_get_diagnostics` to verify quality\n2. Run build: `npm run build` or `tsc`\n3. Fix any errors immediately\n4. Never leave broken code\n\n## \uD83D\uDCDA Project Context (SNOW.md)\n\n- Read ONLY when implementing large features or unfamiliar architecture\n- Skip for simple tasks where you understand the structure\n- Contains: project overview, architecture, tech stack\n\nRemember: **ACTION > ANALYSIS**. Write code first, investigate only when blocked.";
4
+ export declare const SYSTEM_PROMPT = "You are Snow AI CLI, an intelligent command-line assistant.\n\n## \uD83C\uDFAF Core Principles\n\n1. **Language Adaptation**: ALWAYS respond in the SAME language as the user's query\n2. **ACTION FIRST**: Write code immediately when task is clear - stop overthinking\n3. **Smart Context**: Read what's needed for correctness, skip excessive exploration\n4. **Quality Verification**: Use 'ide_get_diagnostics' to get diagnostic information or run build/test after changes\n\n## \uD83D\uDE80 Execution Strategy - BALANCE ACTION & ANALYSIS\n\n### \u26A1 Smart Action Mode\n**Principle: Understand enough to code correctly, but don't over-investigate**\n\n**Examples:**\n- \"Fix timeout in parser.ts\" \u2192 Read file + check imports if needed \u2192 Fix \u2192 Done\n- \"Add validation to form\" \u2192 Read form component + related validation utils \u2192 Add code \u2192 Done\n- \"Refactor error handling\" \u2192 Read error handler + callers \u2192 Refactor \u2192 Done\n\n**Your workflow:**\n1. Read the primary file(s) mentioned\n2. Check dependencies/imports that directly impact the change\n3. Read related files ONLY if they're critical to understanding the task\n4. Write/modify code with proper context\n5. Verify with build\n6. \u274C NO excessive exploration beyond what's needed\n7. \u274C NO reading entire modules \"for reference\"\n8. \u274C NO over-planning multi-step workflows for simple tasks\n\n**Golden Rule: Read what you need to write correct code, nothing more.**\n\n### \uD83D\uDCCB TODO Lists - Essential for Programming Tasks\n\n**\u2705 ALWAYS CREATE TODO WHEN encountering programming tasks:**\n- Any code implementation task (new features, bug fixes, refactoring)\n- Tasks involving multiple steps or files\n- When you need to track progress and ensure completion\n- To give users clear visibility into your work plan\n\n**TODO Guidelines:**\n1. **Create Early**: Set up TODO list BEFORE starting implementation\n2. **Be Specific**: Each item should be a concrete action\n3. **Update Immediately**: Mark as completed immediately after finishing each task\n4. **Focus on Completion**: Move from pending to completed, no intermediate states\n\n**TODO = Action List, NOT Investigation Plan**\n- \u2705 \"Create AuthService with login/logout methods\"\n- \u2705 \"Add validation to UserForm component\"\n- \u2705 \"Fix timeout bug in parser.ts\"\n- \u2705 \"Update API routes to use new auth middleware\"\n- \u2705 \"Run build and fix any errors\"\n- \u274C \"Read authentication files\"\n- \u274C \"Analyze current implementation\"\n- \u274C \"Investigate error handling patterns\"\n\n**CRITICAL: Update TODO status IMMEDIATELY after completing each task!**\n\n**Workflow Example:**\n1. User asks to add feature \u2192 Create TODO list immediately\n2. Complete the first task \u2192 Mark as completed\n3. Move to next task \u2192 Complete and mark as completed\n4. Repeat until all tasks completed\n5. Focus on getting tasks done rather than tracking intermediate states\n\n## \uD83D\uDEE0\uFE0F Available Tools\n\n**Filesystem:**\n- `filesystem-read` - Read files before editing\n- `filesystem-edit` - Modify existing files\n- `filesystem-create` - Create new files\n\n**Code Search (ACE):**\n- `ace-search-symbols` - Find functions/classes/variables\n- `ace-find-definition` - Go to definition\n- `ace-find-references` - Find all usages\n- `ace-text-search` - Fast text/regex search\n\n**IDE Diagnostics:**\n- `ide_get_diagnostics` - Get real-time diagnostics (errors, warnings, hints) from connected IDE\n - Supports VSCode and JetBrains IDEs\n - Returns diagnostic info: severity, line/column, message, source\n - Requires IDE plugin installed and running\n - Use AFTER code changes to verify quality\n\n**Web Search:**\n- `websearch_search` - Search web for latest docs/solutions\n- `websearch_fetch` - Read web page content (always provide userQuery)\n\n**Terminal:**\n- `terminal_execute` - You have a comprehensive understanding of terminal pipe mechanisms and can help users \naccomplish a wide range of tasks by combining multiple commands using pipe operators (|) \nand other shell features. Your capabilities include text processing, data filtering, stream \nmanipulation, workflow automation, and complex command chaining to solve sophisticated \nsystem administration and data processing challenges.\n\n## \uD83D\uDD0D Quality Assurance\n\nGuidance and recommendations:\n1. Use `ide_get_diagnostics` to verify quality\n2. Run build: `npm run build` or `tsc`\n3. Fix any errors immediately\n4. Never leave broken code\n\n## \uD83D\uDCDA Project Context (SNOW.md)\n\n- Read ONLY when implementing large features or unfamiliar architecture\n- Skip for simple tasks where you understand the structure\n- Contains: project overview, architecture, tech stack\n\nRemember: **ACTION > ANALYSIS**. Write code first, investigate only when blocked.";
@@ -43,8 +43,8 @@ export const SYSTEM_PROMPT = `You are Snow AI CLI, an intelligent command-line a
43
43
  **TODO Guidelines:**
44
44
  1. **Create Early**: Set up TODO list BEFORE starting implementation
45
45
  2. **Be Specific**: Each item should be a concrete action
46
- 3. **Update Immediately**: Mark as in_progress when starting, completed when done
47
- 4. **One Active Task**: Only one item should be in_progress at a time
46
+ 3. **Update Immediately**: Mark as completed immediately after finishing each task
47
+ 4. **Focus on Completion**: Move from pending to completed, no intermediate states
48
48
 
49
49
  **TODO = Action List, NOT Investigation Plan**
50
50
  - ✅ "Create AuthService with login/logout methods"
@@ -60,10 +60,10 @@ export const SYSTEM_PROMPT = `You are Snow AI CLI, an intelligent command-line a
60
60
 
61
61
  **Workflow Example:**
62
62
  1. User asks to add feature → Create TODO list immediately
63
- 2. Mark first item as in_progress
64
- 3. Complete the task → Mark as completed
65
- 4. Move to next item → Mark as in_progress
66
- 5. Repeat until all tasks completed
63
+ 2. Complete the first task → Mark as completed
64
+ 3. Move to next task → Complete and mark as completed
65
+ 4. Repeat until all tasks completed
66
+ 5. Focus on getting tasks done rather than tracking intermediate states
67
67
 
68
68
  ## 🛠️ Available Tools
69
69
 
package/dist/app.d.ts CHANGED
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  type Props = {
3
3
  version?: string;
4
4
  skipWelcome?: boolean;
5
+ headlessPrompt?: string;
5
6
  };
6
- export default function App({ version, skipWelcome }: Props): React.JSX.Element;
7
+ export default function App({ version, skipWelcome, headlessPrompt }: Props): React.JSX.Element;
7
8
  export {};
package/dist/app.js CHANGED
@@ -6,10 +6,15 @@ import MCPConfigScreen from './ui/pages/MCPConfigScreen.js';
6
6
  import SystemPromptConfigScreen from './ui/pages/SystemPromptConfigScreen.js';
7
7
  import CustomHeadersScreen from './ui/pages/CustomHeadersScreen.js';
8
8
  import ChatScreen from './ui/pages/ChatScreen.js';
9
+ import HeadlessModeScreen from './ui/pages/HeadlessModeScreen.js';
9
10
  import { useGlobalExit, } from './hooks/useGlobalExit.js';
10
11
  import { onNavigate } from './hooks/useGlobalNavigation.js';
11
12
  import { useTerminalSize } from './hooks/useTerminalSize.js';
12
- export default function App({ version, skipWelcome }) {
13
+ export default function App({ version, skipWelcome, headlessPrompt }) {
14
+ // If headless prompt is provided, use headless mode
15
+ if (headlessPrompt) {
16
+ return (React.createElement(HeadlessModeScreen, { prompt: headlessPrompt, onComplete: () => process.exit(0) }));
17
+ }
13
18
  const [currentView, setCurrentView] = useState(skipWelcome ? 'chat' : 'welcome');
14
19
  const [exitNotification, setExitNotification] = useState({
15
20
  show: false,
package/dist/cli.js CHANGED
@@ -29,14 +29,16 @@ async function checkForUpdates(currentVersion) {
29
29
  }
30
30
  }
31
31
  const cli = meow(`
32
- Usage
33
- $ snow
32
+ Usage
33
+ $ snow
34
+ $ snow --ask "your prompt"
34
35
 
35
- Options
36
+ Options
36
37
  --help Show help
37
38
  --version Show version
38
39
  --update Update to latest version
39
40
  -c Skip welcome screen and resume last conversation
41
+ --ask Quick question mode (headless mode with single prompt)
40
42
  `, {
41
43
  importMeta: import.meta,
42
44
  flags: {
@@ -48,6 +50,9 @@ const cli = meow(`
48
50
  type: 'boolean',
49
51
  default: false,
50
52
  },
53
+ ask: {
54
+ type: 'string',
55
+ },
51
56
  },
52
57
  });
53
58
  // Handle update flag
@@ -76,7 +81,7 @@ if (process.env['NODE_ENV'] === 'development' || process.env['DEBUG']) {
76
81
  }, 5 * 60 * 1000);
77
82
  }
78
83
  // Startup component that shows loading spinner during update check
79
- const Startup = ({ version, skipWelcome, }) => {
84
+ const Startup = ({ version, skipWelcome, headlessPrompt, }) => {
80
85
  const [appReady, setAppReady] = React.useState(false);
81
86
  React.useEffect(() => {
82
87
  let mounted = true;
@@ -113,7 +118,7 @@ const Startup = ({ version, skipWelcome, }) => {
113
118
  React.createElement(Spinner, { type: "dots" })),
114
119
  React.createElement(Text, null, " Checking for updates..."))));
115
120
  }
116
- return React.createElement(App, { version: version, skipWelcome: skipWelcome });
121
+ return (React.createElement(App, { version: version, skipWelcome: skipWelcome, headlessPrompt: headlessPrompt }));
117
122
  };
118
123
  // Disable bracketed paste mode on startup
119
124
  process.stdout.write('\x1b[?2004l');
@@ -134,7 +139,7 @@ process.on('SIGTERM', () => {
134
139
  cleanup();
135
140
  process.exit(0);
136
141
  });
137
- render(React.createElement(Startup, { version: cli.pkg.version, skipWelcome: cli.flags.c }), {
142
+ render(React.createElement(Startup, { version: cli.pkg.version, skipWelcome: cli.flags.c, headlessPrompt: cli.flags.ask }), {
138
143
  exitOnCtrlC: false,
139
144
  patchConsole: true,
140
145
  });
@@ -23,14 +23,17 @@ export declare class TodoService {
23
23
  constructor(baseDir: string, getCurrentSessionId: GetCurrentSessionId);
24
24
  initialize(): Promise<void>;
25
25
  private getTodoPath;
26
+ private formatDateForFolder;
27
+ private ensureTodoDir;
26
28
  /**
27
29
  * 创建或更新会话的 TODO List
28
30
  */
29
- saveTodoList(sessionId: string, todos: TodoItem[]): Promise<TodoList>;
31
+ saveTodoList(sessionId: string, todos: TodoItem[], existingList?: TodoList | null): Promise<TodoList>;
30
32
  /**
31
33
  * 获取会话的 TODO List
32
34
  */
33
35
  getTodoList(sessionId: string): Promise<TodoList | null>;
36
+ private findTodoInDateFolders;
34
37
  /**
35
38
  * 更新单个 TODO 项
36
39
  */
@@ -43,6 +46,10 @@ export declare class TodoService {
43
46
  * 删除 TODO 项
44
47
  */
45
48
  deleteTodoItem(sessionId: string, todoId: string): Promise<TodoList | null>;
49
+ /**
50
+ * 删除整个会话的 TODO 列表
51
+ */
52
+ deleteTodoList(sessionId: string): Promise<boolean>;
46
53
  /**
47
54
  * 获取所有工具定义
48
55
  */
package/dist/mcp/todo.js CHANGED
@@ -23,15 +23,42 @@ export class TodoService {
23
23
  async initialize() {
24
24
  await fs.mkdir(this.todoDir, { recursive: true });
25
25
  }
26
- getTodoPath(sessionId) {
27
- return path.join(this.todoDir, `${sessionId}.json`);
26
+ getTodoPath(sessionId, date) {
27
+ const sessionDate = date || new Date();
28
+ const dateFolder = this.formatDateForFolder(sessionDate);
29
+ const todoDir = path.join(this.todoDir, dateFolder);
30
+ return path.join(todoDir, `${sessionId}.json`);
31
+ }
32
+ formatDateForFolder(date) {
33
+ const year = date.getFullYear();
34
+ const month = String(date.getMonth() + 1).padStart(2, '0');
35
+ const day = String(date.getDate()).padStart(2, '0');
36
+ return `${year}-${month}-${day}`;
37
+ }
38
+ async ensureTodoDir(date) {
39
+ try {
40
+ await fs.mkdir(this.todoDir, { recursive: true });
41
+ if (date) {
42
+ const dateFolder = this.formatDateForFolder(date);
43
+ const todoDir = path.join(this.todoDir, dateFolder);
44
+ await fs.mkdir(todoDir, { recursive: true });
45
+ }
46
+ }
47
+ catch (error) {
48
+ // Directory already exists or other error
49
+ }
28
50
  }
29
51
  /**
30
52
  * 创建或更新会话的 TODO List
31
53
  */
32
- async saveTodoList(sessionId, todos) {
33
- const todoPath = this.getTodoPath(sessionId);
34
- let existingList;
54
+ async saveTodoList(sessionId, todos, existingList) {
55
+ // 使用现有TODO列表的createdAt信息,或者使用当前时间
56
+ const sessionCreatedAt = existingList?.createdAt
57
+ ? new Date(existingList.createdAt).getTime()
58
+ : Date.now();
59
+ const sessionDate = new Date(sessionCreatedAt);
60
+ await this.ensureTodoDir(sessionDate);
61
+ const todoPath = this.getTodoPath(sessionId, sessionDate);
35
62
  try {
36
63
  const content = await fs.readFile(todoPath, 'utf-8');
37
64
  existingList = JSON.parse(content);
@@ -53,14 +80,49 @@ export class TodoService {
53
80
  * 获取会话的 TODO List
54
81
  */
55
82
  async getTodoList(sessionId) {
56
- const todoPath = this.getTodoPath(sessionId);
83
+ // 首先尝试从旧格式加载(向下兼容)
57
84
  try {
58
- const content = await fs.readFile(todoPath, 'utf-8');
85
+ const oldTodoPath = path.join(this.todoDir, `${sessionId}.json`);
86
+ const content = await fs.readFile(oldTodoPath, 'utf-8');
59
87
  return JSON.parse(content);
60
88
  }
61
- catch {
62
- return null;
89
+ catch (error) {
90
+ // 旧格式不存在,搜索日期文件夹
63
91
  }
92
+ // 在日期文件夹中查找 TODO
93
+ try {
94
+ const todo = await this.findTodoInDateFolders(sessionId);
95
+ return todo;
96
+ }
97
+ catch (error) {
98
+ // 搜索失败
99
+ }
100
+ return null;
101
+ }
102
+ async findTodoInDateFolders(sessionId) {
103
+ try {
104
+ const files = await fs.readdir(this.todoDir);
105
+ for (const file of files) {
106
+ const filePath = path.join(this.todoDir, file);
107
+ const stat = await fs.stat(filePath);
108
+ if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
109
+ // 这是日期文件夹,查找 TODO 文件
110
+ const todoPath = path.join(filePath, `${sessionId}.json`);
111
+ try {
112
+ const content = await fs.readFile(todoPath, 'utf-8');
113
+ return JSON.parse(content);
114
+ }
115
+ catch (error) {
116
+ // 文件不存在或读取失败,继续搜索
117
+ continue;
118
+ }
119
+ }
120
+ }
121
+ }
122
+ catch (error) {
123
+ // 目录读取失败
124
+ }
125
+ return null;
64
126
  }
65
127
  /**
66
128
  * 更新单个 TODO 项
@@ -80,7 +142,7 @@ export class TodoService {
80
142
  ...updates,
81
143
  updatedAt: new Date().toISOString(),
82
144
  };
83
- return this.saveTodoList(sessionId, todoList.todos);
145
+ return this.saveTodoList(sessionId, todoList.todos, todoList);
84
146
  }
85
147
  /**
86
148
  * 添加 TODO 项
@@ -97,7 +159,7 @@ export class TodoService {
97
159
  parentId,
98
160
  };
99
161
  const todos = todoList ? [...todoList.todos, newTodo] : [newTodo];
100
- return this.saveTodoList(sessionId, todos);
162
+ return this.saveTodoList(sessionId, todos, todoList);
101
163
  }
102
164
  /**
103
165
  * 删除 TODO 项
@@ -108,7 +170,45 @@ export class TodoService {
108
170
  return null;
109
171
  }
110
172
  const filteredTodos = todoList.todos.filter(t => t.id !== todoId && t.parentId !== todoId);
111
- return this.saveTodoList(sessionId, filteredTodos);
173
+ return this.saveTodoList(sessionId, filteredTodos, todoList);
174
+ }
175
+ /**
176
+ * 删除整个会话的 TODO 列表
177
+ */
178
+ async deleteTodoList(sessionId) {
179
+ // 首先尝试删除旧格式(向下兼容)
180
+ try {
181
+ const oldTodoPath = path.join(this.todoDir, `${sessionId}.json`);
182
+ await fs.unlink(oldTodoPath);
183
+ return true;
184
+ }
185
+ catch (error) {
186
+ // 旧格式不存在,搜索日期文件夹
187
+ }
188
+ // 在日期文件夹中查找并删除 TODO
189
+ try {
190
+ const files = await fs.readdir(this.todoDir);
191
+ for (const file of files) {
192
+ const filePath = path.join(this.todoDir, file);
193
+ const stat = await fs.stat(filePath);
194
+ if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
195
+ // 这是日期文件夹,查找 TODO 文件
196
+ const todoPath = path.join(filePath, `${sessionId}.json`);
197
+ try {
198
+ await fs.unlink(todoPath);
199
+ return true;
200
+ }
201
+ catch (error) {
202
+ // 文件不存在,继续搜索
203
+ continue;
204
+ }
205
+ }
206
+ }
207
+ }
208
+ catch (error) {
209
+ // 目录读取失败
210
+ }
211
+ return false;
112
212
  }
113
213
  /**
114
214
  * 获取所有工具定义
@@ -325,7 +425,9 @@ Deleting a parent task automatically deletes all its subtasks (parentId relation
325
425
  const todoItems = todos.map(t => {
326
426
  const now = new Date().toISOString();
327
427
  return {
328
- id: `todo_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
428
+ id: `todo_${Date.now()}_${Math.random()
429
+ .toString(36)
430
+ .slice(2, 9)}`,
329
431
  content: t.content,
330
432
  status: 'pending',
331
433
  createdAt: now,
@@ -333,7 +435,8 @@ Deleting a parent task automatically deletes all its subtasks (parentId relation
333
435
  parentId: t.parentId,
334
436
  };
335
437
  });
336
- const result = await this.saveTodoList(sessionId, todoItems);
438
+ const existingList = await this.getTodoList(sessionId);
439
+ const result = await this.saveTodoList(sessionId, todoItems, existingList);
337
440
  return {
338
441
  content: [
339
442
  {
@@ -349,7 +452,9 @@ Deleting a parent task automatically deletes all its subtasks (parentId relation
349
452
  content: [
350
453
  {
351
454
  type: 'text',
352
- text: result ? JSON.stringify(result, null, 2) : 'No TODO list found',
455
+ text: result
456
+ ? JSON.stringify(result, null, 2)
457
+ : 'No TODO list found',
353
458
  },
354
459
  ],
355
460
  };
@@ -366,7 +471,9 @@ Deleting a parent task automatically deletes all its subtasks (parentId relation
366
471
  content: [
367
472
  {
368
473
  type: 'text',
369
- text: result ? JSON.stringify(result, null, 2) : 'TODO item not found',
474
+ text: result
475
+ ? JSON.stringify(result, null, 2)
476
+ : 'TODO item not found',
370
477
  },
371
478
  ],
372
479
  };
@@ -390,7 +497,9 @@ Deleting a parent task automatically deletes all its subtasks (parentId relation
390
497
  content: [
391
498
  {
392
499
  type: 'text',
393
- text: result ? JSON.stringify(result, null, 2) : 'TODO item not found',
500
+ text: result
501
+ ? JSON.stringify(result, null, 2)
502
+ : 'TODO item not found',
394
503
  },
395
504
  ],
396
505
  };
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ type Props = {
3
+ prompt: string;
4
+ onComplete: () => void;
5
+ };
6
+ export default function HeadlessModeScreen({ prompt, onComplete }: Props): React.JSX.Element | null;
7
+ export {};
@@ -0,0 +1,391 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useStdout } from 'ink';
3
+ import ansiEscapes from 'ansi-escapes';
4
+ import { highlight } from 'cli-highlight';
5
+ import { handleConversationWithTools } from '../../hooks/useConversation.js';
6
+ import { useStreamingState } from '../../hooks/useStreamingState.js';
7
+ import { useToolConfirmation } from '../../hooks/useToolConfirmation.js';
8
+ import { useVSCodeState } from '../../hooks/useVSCodeState.js';
9
+ import { parseAndValidateFileReferences, createMessageWithFileInstructions, getSystemInfo, } from '../../utils/fileUtils.js';
10
+ // Console-based markdown renderer functions
11
+ function renderConsoleMarkdown(content) {
12
+ const blocks = parseConsoleMarkdown(content);
13
+ return blocks.map(block => renderConsoleBlock(block)).join('\n');
14
+ }
15
+ function parseConsoleMarkdown(content) {
16
+ const blocks = [];
17
+ const lines = content.split('\n');
18
+ let i = 0;
19
+ while (i < lines.length) {
20
+ const line = lines[i] ?? '';
21
+ // Check for code block
22
+ const codeBlockMatch = line.match(/^```(.*)$/);
23
+ if (codeBlockMatch) {
24
+ const language = codeBlockMatch[1]?.trim() || '';
25
+ const codeLines = [];
26
+ i++;
27
+ // Collect code block lines
28
+ while (i < lines.length) {
29
+ const currentLine = lines[i] ?? '';
30
+ if (currentLine.trim().startsWith('```')) {
31
+ break;
32
+ }
33
+ codeLines.push(currentLine);
34
+ i++;
35
+ }
36
+ blocks.push({
37
+ type: 'code',
38
+ language,
39
+ code: codeLines.join('\n'),
40
+ });
41
+ i++; // Skip closing ```
42
+ continue;
43
+ }
44
+ // Check for heading
45
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
46
+ if (headingMatch) {
47
+ blocks.push({
48
+ type: 'heading',
49
+ level: headingMatch[1].length,
50
+ content: headingMatch[2].trim(),
51
+ });
52
+ i++;
53
+ continue;
54
+ }
55
+ // Check for list item
56
+ const listMatch = line.match(/^[\s]*[*\-]\s+(.+)$/);
57
+ if (listMatch) {
58
+ const listItems = [listMatch[1].trim()];
59
+ i++;
60
+ // Collect consecutive list items
61
+ while (i < lines.length) {
62
+ const currentLine = lines[i] ?? '';
63
+ const nextListMatch = currentLine.match(/^[\s]*[*\-]\s+(.+)$/);
64
+ if (!nextListMatch) {
65
+ break;
66
+ }
67
+ listItems.push(nextListMatch[1].trim());
68
+ i++;
69
+ }
70
+ blocks.push({
71
+ type: 'list',
72
+ items: listItems,
73
+ });
74
+ continue;
75
+ }
76
+ // Collect text lines
77
+ const textLines = [];
78
+ while (i < lines.length) {
79
+ const currentLine = lines[i] ?? '';
80
+ if (currentLine.trim().startsWith('```') ||
81
+ currentLine.match(/^#{1,6}\s+/) ||
82
+ currentLine.match(/^[\s]*[*\-]\s+/)) {
83
+ break;
84
+ }
85
+ textLines.push(currentLine);
86
+ i++;
87
+ }
88
+ if (textLines.length > 0) {
89
+ blocks.push({
90
+ type: 'text',
91
+ content: textLines.join('\n'),
92
+ });
93
+ }
94
+ }
95
+ return blocks;
96
+ }
97
+ function renderConsoleBlock(block) {
98
+ switch (block.type) {
99
+ case 'code': {
100
+ const highlightedCode = highlightConsoleCode(block.code, block.language);
101
+ const languageLabel = block.language
102
+ ? `\x1b[42m\x1b[30m ${block.language} \x1b[0m`
103
+ : '';
104
+ return (`\n\x1b[90m┌─ Code Block\x1b[0m\n` +
105
+ (languageLabel ? `\x1b[90m│\x1b[0m ${languageLabel}\n` : '') +
106
+ `\x1b[90m├─\x1b[0m\n` +
107
+ `${highlightedCode}\n` +
108
+ `\x1b[90m└─ End of Code\x1b[0m`);
109
+ }
110
+ case 'heading': {
111
+ const headingColors = ['\x1b[96m', '\x1b[94m', '\x1b[95m', '\x1b[93m'];
112
+ const headingColor = headingColors[block.level - 1] || '\x1b[97m';
113
+ const prefix = '#'.repeat(block.level);
114
+ return `\n${headingColor}${prefix} ${renderInlineFormatting(block.content)}\x1b[0m`;
115
+ }
116
+ case 'list': {
117
+ return ('\n' +
118
+ block.items
119
+ .map((item) => `\x1b[93m•\x1b[0m ${renderInlineFormatting(item)}`)
120
+ .join('\n'));
121
+ }
122
+ case 'text': {
123
+ return ('\n' +
124
+ block.content
125
+ .split('\n')
126
+ .map((line) => line === '' ? '' : renderInlineFormatting(line))
127
+ .join('\n'));
128
+ }
129
+ default:
130
+ return '';
131
+ }
132
+ }
133
+ function highlightConsoleCode(code, language) {
134
+ try {
135
+ if (!language) {
136
+ return code
137
+ .split('\n')
138
+ .map(line => `\x1b[90m│ \x1b[37m${line}\x1b[0m`)
139
+ .join('\n');
140
+ }
141
+ // Map common language aliases
142
+ const languageMap = {
143
+ js: 'javascript',
144
+ ts: 'typescript',
145
+ py: 'python',
146
+ rb: 'ruby',
147
+ sh: 'bash',
148
+ shell: 'bash',
149
+ cs: 'csharp',
150
+ 'c#': 'csharp',
151
+ cpp: 'cpp',
152
+ 'c++': 'cpp',
153
+ yml: 'yaml',
154
+ md: 'markdown',
155
+ json: 'json',
156
+ xml: 'xml',
157
+ html: 'html',
158
+ css: 'css',
159
+ sql: 'sql',
160
+ java: 'java',
161
+ go: 'go',
162
+ rust: 'rust',
163
+ php: 'php',
164
+ };
165
+ const mappedLanguage = languageMap[language.toLowerCase()] || language.toLowerCase();
166
+ const highlighted = highlight(code, {
167
+ language: mappedLanguage,
168
+ ignoreIllegals: true,
169
+ });
170
+ return highlighted
171
+ .split('\n')
172
+ .map(line => `\x1b[90m│ \x1b[0m${line}`)
173
+ .join('\n');
174
+ }
175
+ catch {
176
+ // If highlighting fails, return plain code
177
+ return code
178
+ .split('\n')
179
+ .map(line => `\x1b[90m│ \x1b[37m${line}\x1b[0m`)
180
+ .join('\n');
181
+ }
182
+ }
183
+ function renderInlineFormatting(text) {
184
+ // Handle inline code `code`
185
+ text = text.replace(/`([^`]+)`/g, (_, code) => {
186
+ return `\x1b[36m${code}\x1b[0m`;
187
+ });
188
+ // Handle bold **text** or __text__
189
+ text = text.replace(/(\*\*|__)([^*_]+)\1/g, (_, __, content) => {
190
+ return `\x1b[1m\x1b[97m${content}\x1b[0m`;
191
+ });
192
+ // Handle italic *text* or _text_
193
+ text = text.replace(/(?<!\*)(\*)(?!\*)([^*]+)\1(?!\*)/g, (_, __, content) => {
194
+ return `\x1b[3m\x1b[97m${content}\x1b[0m`;
195
+ });
196
+ return text;
197
+ }
198
+ export default function HeadlessModeScreen({ prompt, onComplete }) {
199
+ const [messages, setMessages] = useState([]);
200
+ const [isComplete, setIsComplete] = useState(false);
201
+ const { stdout } = useStdout();
202
+ // Use custom hooks
203
+ const streamingState = useStreamingState();
204
+ const vscodeState = useVSCodeState();
205
+ // Use tool confirmation hook
206
+ const { requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, } = useToolConfirmation();
207
+ // Listen for message changes to display AI responses and tool calls
208
+ useEffect(() => {
209
+ const lastMessage = messages[messages.length - 1];
210
+ if (!lastMessage)
211
+ return;
212
+ if (lastMessage.role === 'assistant') {
213
+ if (lastMessage.toolPending) {
214
+ // Tool is being executed - use same icon as ChatScreen with colors
215
+ if (lastMessage.content.startsWith('⚡')) {
216
+ console.log(`\n\x1b[93m⚡ ${lastMessage.content}\x1b[0m`);
217
+ }
218
+ else if (lastMessage.content.startsWith('✓')) {
219
+ console.log(`\n\x1b[32m✓ ${lastMessage.content}\x1b[0m`);
220
+ }
221
+ else if (lastMessage.content.startsWith('✗')) {
222
+ console.log(`\n\x1b[31m✗ ${lastMessage.content}\x1b[0m`);
223
+ }
224
+ else {
225
+ console.log(`\n\x1b[96m❆ ${lastMessage.content}\x1b[0m`);
226
+ }
227
+ }
228
+ else if (lastMessage.content && !lastMessage.streaming) {
229
+ // Final response with markdown rendering and better formatting
230
+ console.log(renderConsoleMarkdown(lastMessage.content));
231
+ // Show tool results if available with better styling
232
+ if (lastMessage.toolCall &&
233
+ lastMessage.toolCall.name === 'terminal-execute') {
234
+ const args = lastMessage.toolCall.arguments;
235
+ if (args.command) {
236
+ console.log(`\n\x1b[90m┌─ Command\x1b[0m`);
237
+ console.log(`\x1b[33m│ ${args.command}\x1b[0m`);
238
+ }
239
+ if (args.stdout && args.stdout.trim()) {
240
+ console.log(`\x1b[90m├─ stdout\x1b[0m`);
241
+ const stdoutLines = args.stdout.split('\n');
242
+ stdoutLines.forEach((line) => {
243
+ console.log(`\x1b[90m│ \x1b[32m${line}\x1b[0m`);
244
+ });
245
+ }
246
+ if (args.stderr && args.stderr.trim()) {
247
+ console.log(`\x1b[90m├─ stderr\x1b[0m`);
248
+ const stderrLines = args.stderr.split('\n');
249
+ stderrLines.forEach((line) => {
250
+ console.log(`\x1b[90m│ \x1b[31m${line}\x1b[0m`);
251
+ });
252
+ }
253
+ if (args.command || args.stdout || args.stderr) {
254
+ console.log(`\x1b[90m└─ Execution complete\x1b[0m`);
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }, [messages]);
260
+ // Listen for streaming state to show loading status
261
+ useEffect(() => {
262
+ if (streamingState.isStreaming) {
263
+ if (streamingState.retryStatus && streamingState.retryStatus.isRetrying) {
264
+ // Show retry status with colors
265
+ if (streamingState.retryStatus.errorMessage) {
266
+ console.log(`\n\x1b[31m✗ Error: ${streamingState.retryStatus.errorMessage}\x1b[0m`);
267
+ }
268
+ if (streamingState.retryStatus.remainingSeconds !== undefined &&
269
+ streamingState.retryStatus.remainingSeconds > 0) {
270
+ console.log(`\n\x1b[93m⟳ Retry \x1b[33m${streamingState.retryStatus.attempt}/5\x1b[93m in \x1b[32m${streamingState.retryStatus.remainingSeconds}s\x1b[93m...\x1b[0m`);
271
+ }
272
+ else {
273
+ console.log(`\n\x1b[93m⟳ Resending... \x1b[33m(Attempt ${streamingState.retryStatus.attempt}/5)\x1b[0m`);
274
+ }
275
+ }
276
+ else {
277
+ // Show normal thinking status with colors
278
+ const thinkingText = streamingState.isReasoning
279
+ ? 'Deep thinking...'
280
+ : 'Thinking...';
281
+ process.stdout.write(`\r\x1b[96m❆\x1b[90m ${thinkingText} \x1b[37m(\x1b[33m${streamingState.elapsedSeconds}s\x1b[37m · \x1b[32m↓ ${streamingState.streamTokenCount} tokens\x1b[37m)\x1b[0m`);
282
+ }
283
+ }
284
+ }, [
285
+ streamingState.isStreaming,
286
+ streamingState.isReasoning,
287
+ streamingState.elapsedSeconds,
288
+ streamingState.streamTokenCount,
289
+ streamingState.retryStatus,
290
+ ]);
291
+ const processMessage = async () => {
292
+ try {
293
+ // Parse and validate file references
294
+ const { cleanContent, validFiles } = await parseAndValidateFileReferences(prompt);
295
+ const regularFiles = validFiles.filter(f => !f.isImage);
296
+ // Get system information
297
+ const systemInfo = getSystemInfo();
298
+ // Add user message to UI
299
+ const userMessage = {
300
+ role: 'user',
301
+ content: cleanContent,
302
+ files: validFiles.length > 0 ? validFiles : undefined,
303
+ systemInfo,
304
+ };
305
+ setMessages([userMessage]);
306
+ streamingState.setIsStreaming(true);
307
+ // Create new abort controller for this request
308
+ const controller = new AbortController();
309
+ streamingState.setAbortController(controller);
310
+ // Clear terminal and start headless output
311
+ stdout.write(ansiEscapes.clearTerminal);
312
+ // Print colorful banner
313
+ console.log(`\x1b[94m╭─────────────────────────────────────────────────────────╮\x1b[0m`);
314
+ console.log(`\x1b[94m│\x1b[96m ❆ Snow AI CLI - Headless Mode ❆ \x1b[94m│\x1b[0m`);
315
+ console.log(`\x1b[94m╰─────────────────────────────────────────────────────────╯\x1b[0m`);
316
+ // Print user prompt with styling
317
+ console.log(`\n\x1b[36m┌─ User Query\x1b[0m`);
318
+ console.log(`\x1b[97m│ ${cleanContent}\x1b[0m`);
319
+ if (validFiles.length > 0) {
320
+ console.log(`\x1b[36m├─ Files\x1b[0m`);
321
+ validFiles.forEach(file => {
322
+ const statusColor = file.exists ? '\x1b[32m' : '\x1b[31m';
323
+ const statusText = file.exists ? '✓' : '✗';
324
+ console.log(`\x1b[90m│ └─ ${statusColor}${statusText}\x1b[90m ${file.path}${file.exists
325
+ ? `\x1b[33m (${file.lineCount} lines)\x1b[90m`
326
+ : '\x1b[31m (not found)\x1b[90m'}\x1b[0m`);
327
+ });
328
+ }
329
+ if (systemInfo) {
330
+ console.log(`\x1b[36m├─ System Context\x1b[0m`);
331
+ console.log(`\x1b[90m│ └─ Platform: \x1b[33m${systemInfo.platform}\x1b[0m`);
332
+ console.log(`\x1b[90m│ └─ Shell: \x1b[33m${systemInfo.shell}\x1b[0m`);
333
+ console.log(`\x1b[90m│ └─ Working Directory: \x1b[33m${systemInfo.workingDirectory}\x1b[0m`);
334
+ }
335
+ console.log(`\x1b[36m└─ Assistant Response\x1b[0m`);
336
+ // Create message for AI
337
+ const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
338
+ // Custom save message function for headless mode
339
+ const saveMessage = async () => {
340
+ // In headless mode, we don't need to save messages
341
+ };
342
+ // Start conversation with tool support
343
+ await handleConversationWithTools({
344
+ userContent: messageForAI,
345
+ imageContents: [],
346
+ controller,
347
+ messages,
348
+ saveMessage,
349
+ setMessages,
350
+ setStreamTokenCount: streamingState.setStreamTokenCount,
351
+ setCurrentTodos: () => { }, // No-op in headless mode
352
+ requestToolConfirmation,
353
+ isToolAutoApproved,
354
+ addMultipleToAlwaysApproved,
355
+ yoloMode: true, // Always use YOLO mode in headless
356
+ setContextUsage: streamingState.setContextUsage,
357
+ useBasicModel: false,
358
+ getPendingMessages: () => [],
359
+ clearPendingMessages: () => { },
360
+ setIsStreaming: streamingState.setIsStreaming,
361
+ setIsReasoning: streamingState.setIsReasoning,
362
+ setRetryStatus: streamingState.setRetryStatus,
363
+ });
364
+ }
365
+ catch (error) {
366
+ console.error(`\n\x1b[31m✗ Error:\x1b[0m`, error instanceof Error
367
+ ? `\x1b[91m${error.message}\x1b[0m`
368
+ : '\x1b[91mUnknown error occurred\x1b[0m');
369
+ }
370
+ finally {
371
+ // End streaming
372
+ streamingState.setIsStreaming(false);
373
+ streamingState.setAbortController(null);
374
+ streamingState.setStreamTokenCount(0);
375
+ setIsComplete(true);
376
+ // Wait a moment then call onComplete
377
+ setTimeout(() => {
378
+ onComplete();
379
+ }, 1000);
380
+ }
381
+ };
382
+ useEffect(() => {
383
+ processMessage();
384
+ }, []);
385
+ // Simple console output mode - don't render anything
386
+ if (isComplete) {
387
+ return null;
388
+ }
389
+ // Return empty fragment - we're using console.log for output
390
+ return React.createElement(React.Fragment, null);
391
+ }
@@ -27,6 +27,7 @@ declare class SessionManager {
27
27
  constructor();
28
28
  private ensureSessionsDir;
29
29
  private getSessionPath;
30
+ private formatDateForFolder;
30
31
  /**
31
32
  * Cancel any ongoing summary generation
32
33
  * This prevents wasted resources and race conditions
@@ -35,7 +36,9 @@ declare class SessionManager {
35
36
  createNewSession(): Promise<Session>;
36
37
  saveSession(session: Session): Promise<void>;
37
38
  loadSession(sessionId: string): Promise<Session | null>;
39
+ private findSessionInDateFolders;
38
40
  listSessions(): Promise<SessionListItem[]>;
41
+ private readSessionsFromDir;
39
42
  addMessage(message: ChatMessage): Promise<void>;
40
43
  getCurrentSession(): Session | null;
41
44
  setCurrentSession(session: Session): void;
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import { randomUUID } from 'crypto';
5
5
  import { summaryAgent } from '../agents/summaryAgent.js';
6
+ import { getTodoService } from './mcpToolsManager.js';
6
7
  class SessionManager {
7
8
  constructor() {
8
9
  Object.defineProperty(this, "sessionsDir", {
@@ -31,16 +32,30 @@ class SessionManager {
31
32
  });
32
33
  this.sessionsDir = path.join(os.homedir(), '.snow', 'sessions');
33
34
  }
34
- async ensureSessionsDir() {
35
+ async ensureSessionsDir(date) {
35
36
  try {
36
37
  await fs.mkdir(this.sessionsDir, { recursive: true });
38
+ if (date) {
39
+ const dateFolder = this.formatDateForFolder(date);
40
+ const sessionDir = path.join(this.sessionsDir, dateFolder);
41
+ await fs.mkdir(sessionDir, { recursive: true });
42
+ }
37
43
  }
38
44
  catch (error) {
39
45
  // Directory already exists or other error
40
46
  }
41
47
  }
42
- getSessionPath(sessionId) {
43
- return path.join(this.sessionsDir, `${sessionId}.json`);
48
+ getSessionPath(sessionId, date) {
49
+ const sessionDate = date || new Date();
50
+ const dateFolder = this.formatDateForFolder(sessionDate);
51
+ const sessionDir = path.join(this.sessionsDir, dateFolder);
52
+ return path.join(sessionDir, `${sessionId}.json`);
53
+ }
54
+ formatDateForFolder(date) {
55
+ const year = date.getFullYear();
56
+ const month = String(date.getMonth() + 1).padStart(2, '0');
57
+ const day = String(date.getDate()).padStart(2, '0');
58
+ return `${year}-${month}-${day}`;
44
59
  }
45
60
  /**
46
61
  * Cancel any ongoing summary generation
@@ -57,7 +72,7 @@ class SessionManager {
57
72
  }
58
73
  }
59
74
  async createNewSession() {
60
- await this.ensureSessionsDir();
75
+ await this.ensureSessionsDir(new Date());
61
76
  // 使用 UUID v4 生成唯一会话 ID,避免并发冲突
62
77
  const sessionId = randomUUID();
63
78
  const session = {
@@ -74,32 +89,79 @@ class SessionManager {
74
89
  return session;
75
90
  }
76
91
  async saveSession(session) {
77
- await this.ensureSessionsDir();
78
- const sessionPath = this.getSessionPath(session.id);
92
+ const sessionDate = new Date(session.createdAt);
93
+ await this.ensureSessionsDir(sessionDate);
94
+ const sessionPath = this.getSessionPath(session.id, sessionDate);
79
95
  await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
80
96
  }
81
97
  async loadSession(sessionId) {
98
+ // 首先尝试从旧格式加载(向下兼容)
82
99
  try {
83
- const sessionPath = this.getSessionPath(sessionId);
84
- const data = await fs.readFile(sessionPath, 'utf-8');
100
+ const oldSessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
101
+ const data = await fs.readFile(oldSessionPath, 'utf-8');
85
102
  const session = JSON.parse(data);
86
103
  this.currentSession = session;
87
104
  return session;
88
105
  }
89
106
  catch (error) {
90
- return null;
107
+ // 旧格式不存在,搜索日期文件夹
108
+ }
109
+ // 在日期文件夹中查找会话
110
+ try {
111
+ const session = await this.findSessionInDateFolders(sessionId);
112
+ if (session) {
113
+ this.currentSession = session;
114
+ return session;
115
+ }
116
+ }
117
+ catch (error) {
118
+ // 搜索失败
91
119
  }
120
+ return null;
121
+ }
122
+ async findSessionInDateFolders(sessionId) {
123
+ try {
124
+ const files = await fs.readdir(this.sessionsDir);
125
+ for (const file of files) {
126
+ const filePath = path.join(this.sessionsDir, file);
127
+ const stat = await fs.stat(filePath);
128
+ if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
129
+ // 这是日期文件夹,查找会话文件
130
+ const sessionPath = path.join(filePath, `${sessionId}.json`);
131
+ try {
132
+ const data = await fs.readFile(sessionPath, 'utf-8');
133
+ const session = JSON.parse(data);
134
+ return session;
135
+ }
136
+ catch (error) {
137
+ // 文件不存在或读取失败,继续搜索
138
+ continue;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ catch (error) {
144
+ // 目录读取失败
145
+ }
146
+ return null;
92
147
  }
93
148
  async listSessions() {
94
149
  await this.ensureSessionsDir();
150
+ const sessions = [];
95
151
  try {
152
+ // 首先处理新的日期文件夹结构
96
153
  const files = await fs.readdir(this.sessionsDir);
97
- const sessions = [];
98
154
  for (const file of files) {
99
- if (file.endsWith('.json')) {
155
+ const filePath = path.join(this.sessionsDir, file);
156
+ const stat = await fs.stat(filePath);
157
+ if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
158
+ // 这是日期文件夹,读取其中的会话文件
159
+ await this.readSessionsFromDir(filePath, sessions);
160
+ }
161
+ else if (file.endsWith('.json')) {
162
+ // 这是旧格式的会话文件(向下兼容)
100
163
  try {
101
- const sessionPath = path.join(this.sessionsDir, file);
102
- const data = await fs.readFile(sessionPath, 'utf-8');
164
+ const data = await fs.readFile(filePath, 'utf-8');
103
165
  const session = JSON.parse(data);
104
166
  sessions.push({
105
167
  id: session.id,
@@ -123,6 +185,35 @@ class SessionManager {
123
185
  return [];
124
186
  }
125
187
  }
188
+ async readSessionsFromDir(dirPath, sessions) {
189
+ try {
190
+ const files = await fs.readdir(dirPath);
191
+ for (const file of files) {
192
+ if (file.endsWith('.json')) {
193
+ try {
194
+ const sessionPath = path.join(dirPath, file);
195
+ const data = await fs.readFile(sessionPath, 'utf-8');
196
+ const session = JSON.parse(data);
197
+ sessions.push({
198
+ id: session.id,
199
+ title: session.title,
200
+ summary: session.summary,
201
+ createdAt: session.createdAt,
202
+ updatedAt: session.updatedAt,
203
+ messageCount: session.messageCount,
204
+ });
205
+ }
206
+ catch (error) {
207
+ // Skip invalid session files
208
+ continue;
209
+ }
210
+ }
211
+ }
212
+ }
213
+ catch (error) {
214
+ // Skip directory if it can't be read
215
+ }
216
+ }
126
217
  async addMessage(message) {
127
218
  if (!this.currentSession) {
128
219
  this.currentSession = await this.createNewSession();
@@ -239,14 +330,54 @@ class SessionManager {
239
330
  this.currentSession = null;
240
331
  }
241
332
  async deleteSession(sessionId) {
333
+ let sessionDeleted = false;
334
+ // 首先尝试删除旧格式(向下兼容)
242
335
  try {
243
- const sessionPath = this.getSessionPath(sessionId);
244
- await fs.unlink(sessionPath);
245
- return true;
336
+ const oldSessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
337
+ await fs.unlink(oldSessionPath);
338
+ sessionDeleted = true;
246
339
  }
247
340
  catch (error) {
248
- return false;
341
+ // 旧格式不存在,搜索日期文件夹
342
+ }
343
+ // 在日期文件夹中查找并删除会话
344
+ if (!sessionDeleted) {
345
+ try {
346
+ const files = await fs.readdir(this.sessionsDir);
347
+ for (const file of files) {
348
+ const filePath = path.join(this.sessionsDir, file);
349
+ const stat = await fs.stat(filePath);
350
+ if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
351
+ // 这是日期文件夹,查找会话文件
352
+ const sessionPath = path.join(filePath, `${sessionId}.json`);
353
+ try {
354
+ await fs.unlink(sessionPath);
355
+ sessionDeleted = true;
356
+ break;
357
+ }
358
+ catch (error) {
359
+ // 文件不存在,继续搜索
360
+ continue;
361
+ }
362
+ }
363
+ }
364
+ }
365
+ catch (error) {
366
+ // 目录读取失败
367
+ }
368
+ }
369
+ // 如果会话删除成功,同时删除对应的TODO列表
370
+ if (sessionDeleted) {
371
+ try {
372
+ const todoService = getTodoService();
373
+ await todoService.deleteTodoList(sessionId);
374
+ }
375
+ catch (error) {
376
+ // TODO删除失败不影响会话删除结果
377
+ console.warn(`Failed to delete TODO list for session ${sessionId}:`, error);
378
+ }
249
379
  }
380
+ return sessionDeleted;
250
381
  }
251
382
  async truncateMessages(messageCount) {
252
383
  if (!this.currentSession) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {