closer-code 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.env.example +83 -0
  2. package/API_GUIDE.md +1411 -0
  3. package/AUTO_MKDIR_IMPROVEMENT.md +354 -0
  4. package/CLAUDE.md +55 -0
  5. package/CTRL_C_EXPERIMENT.md +90 -0
  6. package/PROJECT_CLEANUP_SUMMARY.md +121 -0
  7. package/README.md +686 -0
  8. package/cloco.md +51 -0
  9. package/config.example.json +116 -0
  10. package/dist/bash-runner.js +128 -0
  11. package/dist/batch-cli.js +20736 -0
  12. package/dist/closer-cli.js +21190 -0
  13. package/dist/index.js +31228 -0
  14. package/docs/EXPORT_COMMAND.md +152 -0
  15. package/docs/FILE_NAMING_IMPROVEMENT.md +168 -0
  16. package/docs/GLOBAL_CONFIG.md +128 -0
  17. package/docs/LONG_MESSAGE_DISPLAY_FIX.md +202 -0
  18. package/docs/PROJECT_HISTORY_ISOLATION.md +315 -0
  19. package/docs/QUICK_START_HISTORY.md +207 -0
  20. package/docs/TASK_PROGRESS_FEATURE.md +190 -0
  21. package/docs/THINKING_CONTENT_RESEARCH.md +267 -0
  22. package/docs/THINKING_FEATURE.md +187 -0
  23. package/docs/THINKING_IMPROVEMENT_COMPARISON.md +193 -0
  24. package/docs/THINKING_OPTIMIZATION_SUMMARY.md +242 -0
  25. package/docs/UI_IMPROVEMENTS_2025-01-18.md +256 -0
  26. package/docs/WHY_THINKING_SHORT.md +201 -0
  27. package/package.json +49 -0
  28. package/scenarios/README.md +234 -0
  29. package/scenarios/run-all-scenarios.js +342 -0
  30. package/scenarios/scenario1-batch-converter.js +247 -0
  31. package/scenarios/scenario2-code-analyzer.js +375 -0
  32. package/scenarios/scenario3-doc-generator.js +371 -0
  33. package/scenarios/scenario4-log-analyzer.js +496 -0
  34. package/scenarios/scenario5-tdd-helper.js +681 -0
  35. package/src/ai-client-legacy.js +171 -0
  36. package/src/ai-client.js +221 -0
  37. package/src/bash-runner.js +148 -0
  38. package/src/batch-cli.js +327 -0
  39. package/src/cli.jsx +166 -0
  40. package/src/closer-cli.jsx +1103 -0
  41. package/src/closer-cli.jsx.backup +948 -0
  42. package/src/commands/batch.js +62 -0
  43. package/src/commands/chat.js +10 -0
  44. package/src/commands/config.js +154 -0
  45. package/src/commands/help.js +76 -0
  46. package/src/commands/history.js +192 -0
  47. package/src/commands/setup.js +17 -0
  48. package/src/commands/upgrade.js +101 -0
  49. package/src/commands/workflow-tests.js +125 -0
  50. package/src/config.js +343 -0
  51. package/src/conversation.js +962 -0
  52. package/src/git-helper.js +349 -0
  53. package/src/index.js +88 -0
  54. package/src/logger.js +347 -0
  55. package/src/plan.js +193 -0
  56. package/src/planner.js +397 -0
  57. package/src/search.js +195 -0
  58. package/src/setup.js +147 -0
  59. package/src/shortcuts.js +269 -0
  60. package/src/snippets.js +430 -0
  61. package/src/test-modules.js +118 -0
  62. package/src/tools.js +398 -0
  63. package/src/utils/cli.js +124 -0
  64. package/src/utils/validator.js +184 -0
  65. package/src/utils/version.js +33 -0
  66. package/src/utils/workflow-test.js +271 -0
  67. package/src/utils/workflow.js +268 -0
  68. package/test/demo-file-naming.js +92 -0
  69. package/test/demo-thinking.js +124 -0
  70. package/test/final-verification-report.md +303 -0
  71. package/test/research-thinking.js +130 -0
  72. package/test/test-auto-mkdir.js +123 -0
  73. package/test/test-e2e-empty-dir.md +108 -0
  74. package/test/test-export-logic.js +119 -0
  75. package/test/test-global-cloco.js +126 -0
  76. package/test/test-history-isolation.js +291 -0
  77. package/test/test-improved-thinking.js +43 -0
  78. package/test/test-long-message.js +65 -0
  79. package/test/test-plan-functionality.js +95 -0
  80. package/test/test-real-scenario.js +216 -0
  81. package/test/test-thinking-display.js +65 -0
  82. package/test/ui-verification-test.js +203 -0
  83. package/test/verify-history-isolation.sh +71 -0
  84. package/test/verify-thinking.js +339 -0
  85. package/test/workflows/empty-dir-creation.md +51 -0
  86. package/test/workflows/inventor/ascii-teacup.js +199 -0
  87. package/test/workflows/inventor/ascii-teacup.mjs +199 -0
  88. package/test/workflows/inventor/ascii_apple.hs +84 -0
  89. package/test/workflows/inventor/ascii_apple.py +91 -0
  90. package/test/workflows/inventor/cloco.md +3 -0
  91. package/test/workflows/longtalk/cloco.md +19 -0
  92. package/test/workflows/longtalk/emoji_500.txt +63 -0
  93. package/test/workflows/longtalk/emoji_list.txt +20 -0
  94. package/test/workflows/programmer/adder.md +33 -0
  95. package/test/workflows/programmer/expect.md +2 -0
  96. package/test/workflows/programmer/prompt.md +3 -0
  97. package/test/workflows/test-empty-dir-creation.js +113 -0
  98. package/test-ctrl-c.jsx +126 -0
  99. package/test-manual-file-creation.js +151 -0
  100. package/winfix.md +3 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Workflow 测试命令
3
+ * 用于列出和运行 workflow 测试
4
+ */
5
+
6
+ import {
7
+ listWorkflows,
8
+ runWorkflowTest,
9
+ formatTestResult,
10
+ formatTestResults
11
+ } from '../utils/workflow-test.js';
12
+
13
+ export default async function workflowTestsCommand(args, options) {
14
+ try {
15
+ // 列出所有测试(无参数时)
16
+ if (!args[0] && !options.all) {
17
+ await listTests();
18
+ return;
19
+ }
20
+
21
+ // 运行所有测试
22
+ if (options.all) {
23
+ await runAllTests(options);
24
+ return;
25
+ }
26
+
27
+ // 运行单个测试
28
+ const testName = args[0];
29
+ await runSingleTest(testName, options);
30
+
31
+ } catch (error) {
32
+ console.error(`❌ 错误: ${error.message}`);
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 列出所有可用的 workflow 测试
39
+ */
40
+ async function listTests() {
41
+ const workflows = await listWorkflows();
42
+
43
+ if (workflows.length === 0) {
44
+ console.log('没有找到可用的 workflow 测试。');
45
+ console.log('测试目录: test/workflows/');
46
+ return;
47
+ }
48
+
49
+ console.log(`\n${'='.repeat(60)}`);
50
+ console.log(`可用的 Workflow 测试`);
51
+ console.log(`${'='.repeat(60)}\n`);
52
+
53
+ workflows.forEach((wf, idx) => {
54
+ console.log(`${idx + 1}. ${wf.name}`);
55
+ if (wf.description) {
56
+ console.log(` ${wf.description.split('\n').join('\n ')}`);
57
+ }
58
+ console.log('');
59
+ });
60
+
61
+ console.log(`\n使用方法:`);
62
+ console.log(` cloco workflow-tests <name> - 运行单个测试`);
63
+ console.log(` cloco workflow-tests --all - 运行所有测试`);
64
+ console.log(` cloco workflow-tests <name> --verbose - 详细输出\n`);
65
+ }
66
+
67
+ /**
68
+ * 运行单个测试
69
+ */
70
+ async function runSingleTest(testName, options) {
71
+ console.log(`\n${'='.repeat(60)}`);
72
+ console.log(`运行 Workflow 测试: ${testName}`);
73
+ console.log(`${'='.repeat(60)}\n`);
74
+
75
+ const result = await runWorkflowTest(testName, options);
76
+
77
+ console.log(formatTestResult(result, options));
78
+
79
+ // 设置退出码
80
+ if (!result.passed) {
81
+ process.exit(1);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 运行所有测试
87
+ */
88
+ async function runAllTests(options) {
89
+ const workflows = await listWorkflows();
90
+
91
+ if (workflows.length === 0) {
92
+ console.log('没有找到可用的 workflow 测试。');
93
+ return;
94
+ }
95
+
96
+ console.log(`\n${'='.repeat(60)}`);
97
+ console.log(`运行所有 Workflow 测试 (${workflows.length} 个)`);
98
+ console.log(`${'='.repeat(60)}\n`);
99
+
100
+ const results = [];
101
+
102
+ for (const wf of workflows) {
103
+ const result = await runWorkflowTest(wf.name, options);
104
+ results.push(result);
105
+
106
+ // 单个测试结果的简短输出
107
+ const statusIcon = result.passed ? '✅' : '❌';
108
+ const statusText = result.passed ? '通过' : '失败';
109
+ const durationSec = (result.duration / 1000).toFixed(2);
110
+ console.log(`${statusIcon} ${wf.name} - ${statusText} (${durationSec}s)`);
111
+
112
+ if (result.error) {
113
+ console.log(` 错误: ${result.error}`);
114
+ }
115
+ }
116
+
117
+ // 输出汇总
118
+ console.log(formatTestResults(results, options));
119
+
120
+ // 设置退出码
121
+ const hasFailure = results.some(r => !r.passed);
122
+ if (hasFailure) {
123
+ process.exit(1);
124
+ }
125
+ }
package/src/config.js ADDED
@@ -0,0 +1,343 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import crypto from 'crypto';
5
+
6
+ const CONFIG_DIR = path.join(os.homedir(), '.closer-code');
7
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
8
+ const HISTORY_DIR = path.join(CONFIG_DIR, 'history'); // 改为目录
9
+ const HISTORY_FILE = path.join(CONFIG_DIR, 'history.json'); // 保留用于兼容
10
+ const MEMORY_FILE = path.join(CONFIG_DIR, 'memory.json');
11
+
12
+ // 默认配置
13
+ const DEFAULT_CONFIG = {
14
+ // AI 提供商配置
15
+ ai: {
16
+ provider: process.env.CLOSER_AI_PROVIDER || 'anthropic', // 'anthropic' | 'openai' | 'ollama'
17
+ anthropic: {
18
+ apiKey: process.env.CLOSER_ANTHROPIC_API_KEY || '',
19
+ baseURL: process.env.CLOSER_ANTHROPIC_BASE_URL || 'https://api.anthropic.com',
20
+ model: process.env.CLOSER_ANTHROPIC_MODEL || 'claude-sonnet-4-5-20250929',
21
+ maxTokens: parseInt(process.env.CLOSER_ANTHROPIC_MAX_TOKENS || '8192')
22
+ },
23
+ openai: {
24
+ apiKey: process.env.CLOSER_OPENAI_API_KEY || '',
25
+ baseURL: process.env.CLOSER_OPENAI_BASE_URL || 'https://api.openai.com/v1',
26
+ model: process.env.CLOSER_OPENAI_MODEL || 'gpt-4o',
27
+ maxTokens: parseInt(process.env.CLOSER_OPENAI_MAX_TOKENS || '4096')
28
+ },
29
+ ollama: {
30
+ baseURL: process.env.CLOSER_OLLAMA_BASE_URL || 'http://localhost:11434',
31
+ model: process.env.CLOSER_OLLAMA_MODEL || 'llama3.1',
32
+ maxTokens: parseInt(process.env.CLOSER_OLLAMA_MAX_TOKENS || '4096')
33
+ }
34
+ },
35
+
36
+ // 行为配置
37
+ behavior: {
38
+ autoPlan: true, // 自动规划任务
39
+ autoExecute: false, // 自动执行低风险操作
40
+ confirmDestructive: true, // 危险操作需要确认
41
+ maxRetries: 3, // 失败重试次数
42
+ timeout: 30000, // 操作超时时间
43
+ workingDir: process.cwd() // 默认工作目录
44
+ },
45
+
46
+ // 工具配置
47
+ tools: {
48
+ enabled: [
49
+ 'bash', // 执行 shell 命令
50
+ 'readFile', // 读取文件
51
+ 'writeFile', // 写入文件
52
+ 'editFile', // 编辑文件
53
+ 'searchFiles', // 搜索文件
54
+ 'searchCode', // 搜索代码
55
+ 'listFiles', // 列出文件
56
+ 'analyzeError', // 分析错误
57
+ 'runTests', // 运行测试
58
+ 'planTask' // 规划任务
59
+ ]
60
+ },
61
+
62
+ // UI 配置
63
+ ui: {
64
+ theme: 'default',
65
+ showLineNumbers: true,
66
+ maxOutputLines: 100,
67
+ autoScroll: true
68
+ }
69
+ };
70
+
71
+ // 加载配置
72
+ export function loadConfig() {
73
+ try {
74
+ if (fs.existsSync(CONFIG_FILE)) {
75
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
76
+ return { ...DEFAULT_CONFIG, ...config };
77
+ }
78
+ } catch (error) {
79
+ console.warn('Failed to load config, using defaults:', error.message);
80
+ }
81
+ return DEFAULT_CONFIG;
82
+ }
83
+
84
+ // 检查配置是否存在且有效
85
+ export function hasConfig() {
86
+ try {
87
+ if (!fs.existsSync(CONFIG_FILE)) {
88
+ return false;
89
+ }
90
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
91
+
92
+ // 检查是否有有效的 API Key
93
+ const provider = config.ai?.provider || 'anthropic';
94
+ const apiKey = config.ai?.[provider]?.apiKey;
95
+
96
+ return !!apiKey;
97
+ } catch (error) {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ // 保存配置
103
+ export function saveConfig(config) {
104
+ try {
105
+ if (!fs.existsSync(CONFIG_DIR)) {
106
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
107
+ }
108
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
109
+ } catch (error) {
110
+ console.error('Failed to save config:', error.message);
111
+ throw error;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * 生成项目历史文件的路径
117
+ * @param {string} projectPath - 项目路径
118
+ * @returns {string} 历史文件路径
119
+ */
120
+ function getProjectHistoryPath(projectPath) {
121
+ // 规范化路径并生成唯一标识符
122
+ const normalizedPath = path.normalize(projectPath);
123
+ // 使用路径的哈希作为文件名前缀,避免特殊字符问题
124
+ const hash = crypto.createHash('md5').update(normalizedPath).digest('hex');
125
+ // 提取目录名作为文件名后缀,便于人类查阅
126
+ const dirName = path.basename(normalizedPath);
127
+ // 清理目录名中的特殊字符
128
+ const cleanDirName = dirName.replace(/[^a-zA-Z0-9_-]/g, '_');
129
+ return path.join(HISTORY_DIR, `${hash}-${cleanDirName}.json`);
130
+ }
131
+
132
+ /**
133
+ * 生成项目历史文件的元数据路径
134
+ * @param {string} projectPath - 项目路径
135
+ * @returns {string} 元数据文件路径
136
+ */
137
+ function getProjectMetaPath(projectPath) {
138
+ const normalizedPath = path.normalize(projectPath);
139
+ const hash = crypto.createHash('md5').update(normalizedPath).digest('hex');
140
+ const dirName = path.basename(normalizedPath);
141
+ const cleanDirName = dirName.replace(/[^a-zA-Z0-9_-]/g, '_');
142
+ return path.join(HISTORY_DIR, `${hash}-${cleanDirName}.meta.json`);
143
+ }
144
+
145
+ /**
146
+ * 加载对话历史(基于项目隔离)
147
+ * @param {string} projectPath - 项目路径(可选,默认使用当前工作目录)
148
+ * @returns {Array} 历史消息数组
149
+ */
150
+ export function loadHistory(projectPath = null) {
151
+ try {
152
+ const workingDir = projectPath || process.cwd();
153
+ const historyFile = getProjectHistoryPath(workingDir);
154
+
155
+ if (fs.existsSync(historyFile)) {
156
+ const history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
157
+ console.log(`[History] Loaded ${history.length} messages for project: ${workingDir}`);
158
+ return history;
159
+ } else {
160
+ console.log(`[History] No history found for project: ${workingDir}`);
161
+ }
162
+ } catch (error) {
163
+ console.warn('Failed to load history:', error.message);
164
+ }
165
+ return [];
166
+ }
167
+
168
+ /**
169
+ * 保存对话历史(基于项目隔离)
170
+ * @param {Array} history - 历史消息数组
171
+ * @param {string} projectPath - 项目路径(可选,默认使用当前工作目录)
172
+ */
173
+ export function saveHistory(history, projectPath = null) {
174
+ try {
175
+ const workingDir = projectPath || process.cwd();
176
+
177
+ // 确保历史目录存在
178
+ if (!fs.existsSync(HISTORY_DIR)) {
179
+ fs.mkdirSync(HISTORY_DIR, { recursive: true });
180
+ }
181
+
182
+ const historyFile = getProjectHistoryPath(workingDir);
183
+ const metaFile = getProjectMetaPath(workingDir);
184
+
185
+ // 只保留最近 100 条消息
186
+ const trimmed = history.slice(-100);
187
+
188
+ // 保存历史
189
+ fs.writeFileSync(historyFile, JSON.stringify(trimmed, null, 2));
190
+
191
+ // 保存元数据(用于调试和管理)
192
+ const meta = {
193
+ projectPath: workingDir,
194
+ messageCount: trimmed.length,
195
+ lastUpdated: new Date().toISOString(),
196
+ lastMessage: trimmed[trimmed.length - 1]?.timestamp || null
197
+ };
198
+ fs.writeFileSync(metaFile, JSON.stringify(meta, null, 2));
199
+
200
+ console.log(`[History] Saved ${trimmed.length} messages for project: ${workingDir}`);
201
+ } catch (error) {
202
+ console.error('Failed to save history:', error.message);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * 清除指定项目的历史
208
+ * @param {string} projectPath - 项目路径(可选,默认使用当前工作目录)
209
+ */
210
+ export function clearHistory(projectPath = null) {
211
+ try {
212
+ const workingDir = projectPath || process.cwd();
213
+ const historyFile = getProjectHistoryPath(workingDir);
214
+ const metaFile = getProjectMetaPath(workingDir);
215
+
216
+ if (fs.existsSync(historyFile)) {
217
+ fs.unlinkSync(historyFile);
218
+ }
219
+ if (fs.existsSync(metaFile)) {
220
+ fs.unlinkSync(metaFile);
221
+ }
222
+
223
+ console.log(`[History] Cleared history for project: ${workingDir}`);
224
+ } catch (error) {
225
+ console.error('Failed to clear history:', error.message);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * 列出所有项目的历史
231
+ * @returns {Array} 项目历史列表
232
+ */
233
+ export function listHistory() {
234
+ try {
235
+ if (!fs.existsSync(HISTORY_DIR)) {
236
+ return [];
237
+ }
238
+
239
+ const files = fs.readdirSync(HISTORY_DIR)
240
+ .filter(file => file.endsWith('.meta.json'));
241
+
242
+ const projects = files.map(file => {
243
+ const metaPath = path.join(HISTORY_DIR, file);
244
+ try {
245
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
246
+ return {
247
+ projectPath: meta.projectPath,
248
+ messageCount: meta.messageCount,
249
+ lastUpdated: meta.lastUpdated
250
+ };
251
+ } catch (error) {
252
+ return null;
253
+ }
254
+ }).filter(Boolean);
255
+
256
+ return projects;
257
+ } catch (error) {
258
+ console.error('Failed to list history:', error.message);
259
+ return [];
260
+ }
261
+ }
262
+
263
+ /**
264
+ * 迁移旧的历史文件到新的项目隔离结构
265
+ */
266
+ export function migrateHistory() {
267
+ try {
268
+ if (!fs.existsSync(HISTORY_FILE)) {
269
+ console.log('[Migration] No old history file found, skipping migration.');
270
+ return;
271
+ }
272
+
273
+ console.log('[Migration] Starting history migration...');
274
+
275
+ const oldHistory = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
276
+ console.log(`[Migration] Found ${oldHistory.length} messages in old history file.`);
277
+
278
+ // 备份旧文件
279
+ const backupFile = HISTORY_FILE + '.backup';
280
+ fs.copyFileSync(HISTORY_FILE, backupFile);
281
+ console.log(`[Migration] Backup created at: ${backupFile}`);
282
+
283
+ // 将旧历史保存到当前项目
284
+ saveHistory(oldHistory);
285
+ console.log('[Migration] Old history migrated to current project.');
286
+
287
+ // 删除旧文件(可选,这里保留备份)
288
+ // fs.unlinkSync(HISTORY_FILE);
289
+ console.log('[Migration] Migration completed. Old file preserved for safety.');
290
+ } catch (error) {
291
+ console.error('Failed to migrate history:', error.message);
292
+ }
293
+ }
294
+
295
+ // 加载记忆(项目知识)
296
+ export function loadMemory() {
297
+ try {
298
+ if (fs.existsSync(MEMORY_FILE)) {
299
+ return JSON.parse(fs.readFileSync(MEMORY_FILE, 'utf-8'));
300
+ }
301
+ } catch (error) {
302
+ console.warn('Failed to load memory:', error.message);
303
+ }
304
+ return { projects: {}, patterns: {}, lessons: [] };
305
+ }
306
+
307
+ // 保存记忆
308
+ export function saveMemory(memory) {
309
+ try {
310
+ if (!fs.existsSync(CONFIG_DIR)) {
311
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
312
+ }
313
+ fs.writeFileSync(MEMORY_FILE, JSON.stringify(memory, null, 2));
314
+ } catch (error) {
315
+ console.error('Failed to save memory:', error.message);
316
+ }
317
+ }
318
+
319
+ // 获取当前配置
320
+ export function getConfig() {
321
+ return loadConfig();
322
+ }
323
+
324
+ // 更新配置
325
+ export function updateConfig(updates) {
326
+ const config = loadConfig();
327
+ const newConfig = deepMerge(config, updates);
328
+ saveConfig(newConfig);
329
+ return newConfig;
330
+ }
331
+
332
+ // 深度合并对象
333
+ function deepMerge(target, source) {
334
+ const result = { ...target };
335
+ for (const key in source) {
336
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
337
+ result[key] = deepMerge(target[key] || {}, source[key]);
338
+ } else {
339
+ result[key] = source[key];
340
+ }
341
+ }
342
+ return result;
343
+ }