winter-super-cli 2026.5.30 → 2026.5.31
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/bin/winter.js +3 -1
- package/package.json +1 -1
- package/src/ai/prompts/system-prompt.js +2 -10
- package/src/cli/commands.js +11 -6
- package/src/cli/context-loader.js +15 -1
- package/src/cli/project-docs.js +188 -0
- package/src/cli/prompt-builder.js +7 -12
- package/src/cli/repl-commands.js +4 -0
- package/src/cli/repl.js +102 -93
- package/src/cli/runtime-env.js +130 -0
- package/src/cli/slash-commands.js +3 -1
- package/src/session/manager.js +66 -1
- package/src/tools/executor.js +7 -23
package/bin/winter.js
CHANGED
|
@@ -18,7 +18,7 @@ const version = pkg.version;
|
|
|
18
18
|
|
|
19
19
|
const COMMANDS = new Set([
|
|
20
20
|
'chat', 'call', 'benchmark', 'session', 'skill', 'plugin', 'design', 'config', 'init',
|
|
21
|
-
'help', 'project', 'code', 'review', 'mcp', 'permissions',
|
|
21
|
+
'help', 'project', 'code', 'review', 'debug', 'auto', 'mcp', 'permissions',
|
|
22
22
|
'provider', 'providers', 'model', 'models',
|
|
23
23
|
]);
|
|
24
24
|
|
|
@@ -68,6 +68,8 @@ Commands:
|
|
|
68
68
|
winter config Show config
|
|
69
69
|
winter init Initialize local state
|
|
70
70
|
winter review Code review mode
|
|
71
|
+
winter debug <error/task> Auto-debug with verification
|
|
72
|
+
winter auto <task> Auto-heal with test/build loop
|
|
71
73
|
winter code Code analysis mode
|
|
72
74
|
|
|
73
75
|
Flags:
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { isSmallModel, getModelCapabilityLabel } from '../model-capabilities.js';
|
|
8
|
+
import { formatRuntimeEnvironmentSummary, getRuntimeEnvironment } from '../../cli/runtime-env.js';
|
|
8
9
|
|
|
9
10
|
const BASE_PRINCIPLES = [
|
|
10
11
|
'Execute, don\'t describe - Do the work, don\'t write plans about doing the work',
|
|
@@ -25,18 +26,9 @@ const TOOL_CATEGORIES = {
|
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
function buildEnvironmentSummary() {
|
|
28
|
-
const os = process.platform === 'win32' ? 'Windows'
|
|
29
|
-
: process.platform === 'darwin' ? 'macOS'
|
|
30
|
-
: process.platform === 'linux' ? 'Linux' : process.platform;
|
|
31
|
-
|
|
32
|
-
const shellHint = process.platform === 'win32'
|
|
33
|
-
? 'Use shell:"powershell" for PowerShell, shell:"cmd" for cmd.exe'
|
|
34
|
-
: `Shell: ${process.env.SHELL || 'bash'}`;
|
|
35
|
-
|
|
36
29
|
return [
|
|
37
|
-
|
|
30
|
+
formatRuntimeEnvironmentSummary(getRuntimeEnvironment()),
|
|
38
31
|
`Node: ${process.version}`,
|
|
39
|
-
shellHint,
|
|
40
32
|
].join('\n');
|
|
41
33
|
}
|
|
42
34
|
|
package/src/cli/commands.js
CHANGED
|
@@ -12,6 +12,7 @@ import { PluginManager } from '../plugins/manager.js';
|
|
|
12
12
|
import { MCPClient } from '../mcp/client.js';
|
|
13
13
|
import { BenchmarkRunner } from '../ai/benchmark.js';
|
|
14
14
|
import { redactSecrets } from './secret-env.js';
|
|
15
|
+
import { formatRuntimeEnvironmentSummary, getRuntimeEnvironment } from './runtime-env.js';
|
|
15
16
|
|
|
16
17
|
export { redactSecrets } from './secret-env.js';
|
|
17
18
|
|
|
@@ -60,6 +61,8 @@ export class CommandParser {
|
|
|
60
61
|
design: this.handleDesign.bind(this),
|
|
61
62
|
code: this.handleCode.bind(this),
|
|
62
63
|
review: this.handleReview.bind(this),
|
|
64
|
+
debug: this.handleDebug.bind(this),
|
|
65
|
+
auto: this.handleDebug.bind(this),
|
|
63
66
|
config: this.handleConfig.bind(this),
|
|
64
67
|
init: this.handleInit.bind(this),
|
|
65
68
|
help: this.handleHelp.bind(this),
|
|
@@ -104,6 +107,8 @@ export class CommandParser {
|
|
|
104
107
|
'/models': () => this.showModels(),
|
|
105
108
|
'/mcp': () => this.handleMcp(args),
|
|
106
109
|
'/permissions': () => this.handlePermissions(args),
|
|
110
|
+
'/debug': () => this.handleDebug(args),
|
|
111
|
+
'/auto': () => this.handleDebug(args),
|
|
107
112
|
'/exit': () => process.exit(0),
|
|
108
113
|
};
|
|
109
114
|
|
|
@@ -115,6 +120,11 @@ export class CommandParser {
|
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
async handleDebug(args) {
|
|
124
|
+
const task = args.join(' ') || 'Find the root cause, patch it, and verify with the closest test or build command';
|
|
125
|
+
return this.handleChat([`AUTO DEBUG: ${task}`]);
|
|
126
|
+
}
|
|
127
|
+
|
|
118
128
|
async handleChat(args) {
|
|
119
129
|
const message = args.join(' ');
|
|
120
130
|
if (!message) {
|
|
@@ -638,12 +648,7 @@ export class CommandParser {
|
|
|
638
648
|
|
|
639
649
|
getWinterSystemPrompt() {
|
|
640
650
|
const environmentSummary = [
|
|
641
|
-
|
|
642
|
-
`Node platform: ${process.platform}`,
|
|
643
|
-
`Current shell hint: ${process.platform === 'win32' ? 'powershell-capable or cmd/unknown' : (process.env.SHELL || 'bash/sh')}`,
|
|
644
|
-
process.platform === 'win32'
|
|
645
|
-
? 'Shell rule: Use shell:"powershell" for PowerShell cmdlets, shell:"cmd" for cmd.exe syntax, and shell:"auto" when unsure.'
|
|
646
|
-
: 'Shell rule: Use the native POSIX shell on non-Windows hosts and leave shell unspecified unless a specific shell is required.',
|
|
651
|
+
formatRuntimeEnvironmentSummary(getRuntimeEnvironment()),
|
|
647
652
|
].join('\n');
|
|
648
653
|
|
|
649
654
|
return `You are Winter, an expert AI coding assistant.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { homedir } from 'os';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* ContextLoader — Loads project context, resource paths, and instruction files.
|
|
@@ -42,8 +46,18 @@ export class ContextLoader {
|
|
|
42
46
|
return files;
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
getResourceRoot() {
|
|
50
|
+
const projectLocalRoot = path.join(this.projectPath, 'resources', 'local');
|
|
51
|
+
const packageLocalRoot = path.join(PACKAGE_ROOT, 'resources', 'local');
|
|
52
|
+
|
|
53
|
+
if (this.projectPath === PACKAGE_ROOT) return projectLocalRoot;
|
|
54
|
+
if (existsSync(projectLocalRoot)) return projectLocalRoot;
|
|
55
|
+
|
|
56
|
+
return packageLocalRoot;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
getResourcePaths() {
|
|
46
|
-
const localRoot =
|
|
60
|
+
const localRoot = this.getResourceRoot();
|
|
47
61
|
return {
|
|
48
62
|
codex: {
|
|
49
63
|
root: path.join(localRoot, 'codex'),
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const GENERATED_MARKER = 'File này được tự động tạo bởi Winter CLI.';
|
|
5
|
+
|
|
6
|
+
const CORE_SKILLS = [
|
|
7
|
+
['coding', 'Inspect source first, make focused code changes, and verify syntax/tests.'],
|
|
8
|
+
['debug', 'Trace the concrete failing path, explain the first hard blocker, then patch it.'],
|
|
9
|
+
['design', 'Use awesome-design-md before inventing UI style, spacing, or brand language.'],
|
|
10
|
+
['refactor', 'Keep behavior stable while reducing complexity in small, reviewable steps.'],
|
|
11
|
+
['test', 'Add regression coverage near the changed behavior and run the narrow test first.'],
|
|
12
|
+
['security', 'Protect secrets, validate inputs, avoid unsafe shell/file operations.'],
|
|
13
|
+
['performance', 'Measure or reason from the hot path before optimizing.'],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
async function readJsonIfExists(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
19
|
+
return JSON.parse(raw.replace(/^\uFEFF/, ''));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function listEntries(target, { directories = true, files = false, limit = 100 } = {}) {
|
|
26
|
+
try {
|
|
27
|
+
const entries = await fs.readdir(target, { withFileTypes: true });
|
|
28
|
+
return entries
|
|
29
|
+
.filter(entry => (directories && entry.isDirectory()) || (files && entry.isFile()))
|
|
30
|
+
.map(entry => entry.name)
|
|
31
|
+
.sort((a, b) => a.localeCompare(b))
|
|
32
|
+
.slice(0, limit);
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatBytes(bytes = 0) {
|
|
39
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
|
40
|
+
const mb = bytes / 1024 / 1024;
|
|
41
|
+
if (mb >= 1) return `${mb.toFixed(1)} MB`;
|
|
42
|
+
const kb = bytes / 1024;
|
|
43
|
+
return `${kb.toFixed(1)} KB`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getResource(manifest, name) {
|
|
47
|
+
return (manifest?.localResources || []).find(resource => resource.name === name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function bulletList(items, emptyText) {
|
|
51
|
+
if (!items.length) return `- ${emptyText}`;
|
|
52
|
+
return items.map(item => `- ${item}`).join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function generatedFooter() {
|
|
56
|
+
return `---\n*${GENERATED_MARKER}*`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isWinterGeneratedProjectDoc(content = '') {
|
|
60
|
+
return /File n(?:ày|Ã y) .*Winter CLI/i.test(content)
|
|
61
|
+
|| /được tự động tạo bởi Winter CLI/i.test(content)
|
|
62
|
+
|| /được tự động tạo bởi Winter CLI/i.test(content);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function buildDesignDoc({ projectPath, resourcePaths }) {
|
|
66
|
+
const manifest = await readJsonIfExists(resourcePaths.manifest);
|
|
67
|
+
const designResource = getResource(manifest, 'awesome-design-md');
|
|
68
|
+
const brandNames = await listEntries(resourcePaths.designs, { directories: true, limit: 40 });
|
|
69
|
+
const relativeDesignPath = path.relative(projectPath, resourcePaths.designs) || resourcePaths.designs;
|
|
70
|
+
const relativeReadmePath = path.join(path.relative(projectPath, path.dirname(resourcePaths.designs)), 'README.md');
|
|
71
|
+
const corpusStatus = brandNames.length > 0
|
|
72
|
+
? `Corpus local sẵn sàng tại \`${relativeDesignPath}\`.`
|
|
73
|
+
: 'Corpus chi tiết chưa có trong working tree hiện tại. Dùng manifest để biết index, hoặc cài/copy resource pack đầy đủ trước khi làm UI/brand task.';
|
|
74
|
+
|
|
75
|
+
return `# Design Guidance
|
|
76
|
+
|
|
77
|
+
File này không phải danh bạ trang trí. Đây là checklist bắt buộc khi Winter xử lý UI, brand, landing page, dashboard, hoặc component.
|
|
78
|
+
|
|
79
|
+
## Resource Status
|
|
80
|
+
- Source: \`${relativeReadmePath}\`
|
|
81
|
+
- Corpus: \`${relativeDesignPath}\`
|
|
82
|
+
- Manifest: ${designResource ? `${designResource.files} files, ${formatBytes(designResource.bytes)}` : 'không có entry awesome-design-md'}
|
|
83
|
+
- Status: ${corpusStatus}
|
|
84
|
+
|
|
85
|
+
## How Winter Must Use This
|
|
86
|
+
1. Khi task liên quan UI/design/brand, tìm brand hoặc style gần nhất trong \`awesome-design-md/design-md\` trước khi viết code.
|
|
87
|
+
2. Nếu user nêu brand cụ thể, ưu tiên đúng brand đó; nếu không có, chọn brand gần nhất theo domain và nói rõ giả định.
|
|
88
|
+
3. Không tự bịa palette, spacing, typography, tone khi local design guide có dữ liệu phù hợp.
|
|
89
|
+
4. Với app/work tool, ưu tiên layout rõ, dense, dễ scan; không dùng hero marketing nếu user đang cần tool thật.
|
|
90
|
+
5. Sau khi sửa UI, kiểm tra responsive và text overflow nếu có thể chạy app.
|
|
91
|
+
|
|
92
|
+
## Available Brand Samples (${brandNames.length}${brandNames.length >= 40 ? '+' : ''})
|
|
93
|
+
${bulletList(brandNames, 'Chưa tìm thấy brand folder trong working tree hiện tại.')}
|
|
94
|
+
|
|
95
|
+
## Useful Commands
|
|
96
|
+
- \`/designs\` để liệt kê hoặc tìm design systems.
|
|
97
|
+
- \`/read resources/local/awesome-design-md/README.md\` để đọc overview.
|
|
98
|
+
- \`/grep <brand> resources/local/awesome-design-md/design-md\` để tìm brand/style guide.
|
|
99
|
+
|
|
100
|
+
${generatedFooter()}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function buildSkillDoc({ contextLoader }) {
|
|
104
|
+
const catalog = await contextLoader.getStartupSkillCatalog();
|
|
105
|
+
const skills = [...catalog]
|
|
106
|
+
.filter(skill => skill && !skill.startsWith('.'))
|
|
107
|
+
.sort((a, b) => a.localeCompare(b));
|
|
108
|
+
|
|
109
|
+
return `# Skill Guidance
|
|
110
|
+
|
|
111
|
+
File này định nghĩa cách Winter chọn và áp dụng skill. Không chỉ liệt kê tên skill.
|
|
112
|
+
|
|
113
|
+
## Default Rule
|
|
114
|
+
- Luôn đọc yêu cầu, repo context, và file liên quan trước khi quyết định skill.
|
|
115
|
+
- Skill là operational context: áp dụng vào hành động thật, không chỉ nhắc lại trong câu trả lời.
|
|
116
|
+
- Model nhỏ vẫn phải theo cùng tiêu chuẩn: inspect, edit bằng tool, verify, rồi mới kết luận.
|
|
117
|
+
|
|
118
|
+
## Core Skills
|
|
119
|
+
${CORE_SKILLS.map(([name, description]) => `- **${name}**: ${description}`).join('\n')}
|
|
120
|
+
|
|
121
|
+
## Available Local Skills (${skills.length})
|
|
122
|
+
${bulletList(skills, 'Chưa tìm thấy skill local ngoài core skills.')}
|
|
123
|
+
|
|
124
|
+
## When To Apply
|
|
125
|
+
- Code change: coding + test, thêm debug nếu có lỗi cụ thể.
|
|
126
|
+
- UI/page/component: design + coding + test.
|
|
127
|
+
- Bug/runtime log: debug trước, coding sau.
|
|
128
|
+
- Refactor lớn: refactor + test, giữ behavior stable.
|
|
129
|
+
- Security/config/secret: security bắt buộc.
|
|
130
|
+
- Performance/flicker/slow tool call: performance + debug.
|
|
131
|
+
|
|
132
|
+
${generatedFooter()}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function buildRuleDoc({ projectPath, resourcePaths, userResourcePaths, readProjectInstructionFiles }) {
|
|
136
|
+
const manifest = await readJsonIfExists(resourcePaths.manifest);
|
|
137
|
+
const resources = manifest?.localResources || [];
|
|
138
|
+
const instructionFiles = (await readProjectInstructionFiles())
|
|
139
|
+
.map(file => file.relativePath)
|
|
140
|
+
.filter(relativePath => relativePath !== 'rule.md');
|
|
141
|
+
const bundledRuleFiles = await listEntries(resourcePaths.codex.rules, { directories: false, files: true, limit: 30 });
|
|
142
|
+
const userRuleFiles = await listEntries(userResourcePaths?.codexRules, { directories: false, files: true, limit: 30 });
|
|
143
|
+
const karpathyPath = path.relative(projectPath, path.join(resourcePaths.karpathy, 'CLAUDE.md'));
|
|
144
|
+
const agentsPath = path.relative(projectPath, path.join(resourcePaths.agents, 'AGENTS.md'));
|
|
145
|
+
|
|
146
|
+
return `# Project Operating Rules
|
|
147
|
+
|
|
148
|
+
File này là contract vận hành cho Winter trong project này.
|
|
149
|
+
|
|
150
|
+
## Non-Negotiable Behavior
|
|
151
|
+
- Không nói đã sửa/chạy/kiểm tra nếu chưa có tool result trong lượt đó.
|
|
152
|
+
- Trước khi sửa code: đọc file liên quan, hiểu entrypoint/runtime path, rồi mới patch.
|
|
153
|
+
- Giữ thay đổi hẹp, không revert code user không yêu cầu.
|
|
154
|
+
- Sau khi sửa: chạy syntax check hoặc test gần nhất có thể.
|
|
155
|
+
- Với model nhỏ: bắt buộc chia việc thành inspect -> implement -> verify -> report.
|
|
156
|
+
|
|
157
|
+
## Project Instruction Files
|
|
158
|
+
${instructionFiles.length ? instructionFiles.map(file => `- [${file}](./${file})`).join('\n') : '- Chưa có instruction file khác.'}
|
|
159
|
+
|
|
160
|
+
## Mandatory Local Resources
|
|
161
|
+
- Karpathy tools: \`${karpathyPath}\`
|
|
162
|
+
- Agents guide: \`${agentsPath}\`
|
|
163
|
+
- Design corpus: \`${path.relative(projectPath, resourcePaths.designs)}\`
|
|
164
|
+
|
|
165
|
+
## Resource Inventory
|
|
166
|
+
${resources.length ? resources.map(resource => `- **${resource.name}**: ${resource.files} files, ${formatBytes(resource.bytes)}`).join('\n') : '- Không đọc được manifest resource.'}
|
|
167
|
+
|
|
168
|
+
## Extra Rule Files
|
|
169
|
+
- Bundled Codex rules: ${bundledRuleFiles.length ? bundledRuleFiles.join(', ') : 'none found'}
|
|
170
|
+
- User Codex rules: ${userRuleFiles.length ? userRuleFiles.join(', ') : 'none found'}
|
|
171
|
+
|
|
172
|
+
## Acceptance Checklist
|
|
173
|
+
- Đúng provider/model user chọn, không tự route sai.
|
|
174
|
+
- Tool call phải dùng đúng schema; lỗi thì retry/recover trước khi báo user.
|
|
175
|
+
- Memory/session phải giữ đúng project.
|
|
176
|
+
- UI docs/design phải dùng local resources khi task liên quan.
|
|
177
|
+
- Final answer ngắn, nêu file đã sửa và verification thật.
|
|
178
|
+
|
|
179
|
+
${generatedFooter()}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function buildProjectDocs(options) {
|
|
183
|
+
return [
|
|
184
|
+
{ filename: 'design.md', content: await buildDesignDoc(options) },
|
|
185
|
+
{ filename: 'skill.md', content: await buildSkillDoc(options) },
|
|
186
|
+
{ filename: 'rule.md', content: await buildRuleDoc(options) },
|
|
187
|
+
];
|
|
188
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { formatRuntimeEnvironmentSummary, getRuntimeEnvironment } from './runtime-env.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* PromptBuilder — Builds system prompts for Winter CLI agents.
|
|
3
5
|
* Extracted from WinterREPL to reduce repl.js size.
|
|
@@ -79,6 +81,7 @@ export class PromptBuilder {
|
|
|
79
81
|
`Use tools when they materially help. For coding tasks: inspect first, edit second, verify third.`,
|
|
80
82
|
`Prefer Read/Grep/Glob before editing. Use Write/Edit for file changes.`,
|
|
81
83
|
`When a task touches coding, agents, UI, brand, or design, inspect the relevant required local resource in depth before deciding.`,
|
|
84
|
+
`If the user asks you to modify, run, inspect, check, publish, commit, or otherwise act on the project, you MUST use tools. Do not claim completion without a tool result from this turn.`,
|
|
82
85
|
``,
|
|
83
86
|
`## Session`,
|
|
84
87
|
`Working directory: ${this.projectPath}`,
|
|
@@ -143,9 +146,7 @@ export class PromptBuilder {
|
|
|
143
146
|
|
|
144
147
|
const rolePrompt = rolePrompts[role] || 'You are a Winter coding subagent. Solve the task directly, use tools when needed, and return a concise result.';
|
|
145
148
|
|
|
146
|
-
const
|
|
147
|
-
? 'Windows; Bash auto-detects PowerShell and cmd.exe syntax. Use shell="powershell" or shell="cmd" when needed.'
|
|
148
|
-
: process.platform;
|
|
149
|
+
const runtimeSummary = this.tools?.getRuntimeEnvironmentSummary?.() || this._defaultEnvironmentSummary();
|
|
149
150
|
|
|
150
151
|
return [
|
|
151
152
|
'## CRITICAL AI RULES (MUST FOLLOW STRICTLY):',
|
|
@@ -161,8 +162,9 @@ export class PromptBuilder {
|
|
|
161
162
|
'## Tool Rules',
|
|
162
163
|
'- Canonical tools: Read, Write, Edit, Bash, Glob, Grep, TaskCreate, TaskUpdate, TaskList, BrowserDebug, WebFetch, WebSearch.',
|
|
163
164
|
'- Treat skills, memories, bundled resources, local project rules, and the tool list as operational context. Use them proactively when relevant.',
|
|
164
|
-
`-
|
|
165
|
+
`- Runtime environment:\n${runtimeSummary}`,
|
|
165
166
|
'- Prefer Write/Edit for writing files. Bash accepts both PowerShell and cmd.exe on Windows, but do not use long echo chains for code files.',
|
|
167
|
+
'- For action requests, use tools before claiming anything is done. Never claim files changed, tests ran, or checks passed unless this conversation contains the matching tool result.',
|
|
166
168
|
'- If a tool call fails because of an unknown alias, call the canonical tool name next.',
|
|
167
169
|
'- Always start with a brief plan, then refine it when new facts appear.',
|
|
168
170
|
'',
|
|
@@ -196,14 +198,7 @@ export class PromptBuilder {
|
|
|
196
198
|
|
|
197
199
|
/** @private */
|
|
198
200
|
_defaultEnvironmentSummary() {
|
|
199
|
-
return
|
|
200
|
-
`Host OS: ${process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'macOS' : process.platform === 'linux' ? 'Linux' : process.platform}`,
|
|
201
|
-
`Node platform: ${process.platform}`,
|
|
202
|
-
`Current shell hint: ${process.platform === 'win32' ? 'powershell-capable or cmd/unknown' : (process.env.SHELL || 'bash/sh')}`,
|
|
203
|
-
process.platform === 'win32'
|
|
204
|
-
? 'Shell rule: Use shell:"powershell" for PowerShell cmdlets, shell:"cmd" for cmd.exe syntax, and shell:"auto" when unsure.'
|
|
205
|
-
: 'Shell rule: Use the native POSIX shell on non-Windows hosts and leave shell unspecified unless a specific shell is required.',
|
|
206
|
-
].join('\n');
|
|
201
|
+
return formatRuntimeEnvironmentSummary(getRuntimeEnvironment());
|
|
207
202
|
}
|
|
208
203
|
|
|
209
204
|
/** @private */
|
package/src/cli/repl-commands.js
CHANGED
|
@@ -217,6 +217,10 @@ export async function handleSlashCommand(repl, input) {
|
|
|
217
217
|
const task = args.join(' ') || 'Run tests until they pass';
|
|
218
218
|
await repl.runAutoHealing(task);
|
|
219
219
|
return;
|
|
220
|
+
case '/debug':
|
|
221
|
+
const debugTask = args.join(' ') || 'Find the root cause of the current failure, patch it, and verify with the closest test or build command';
|
|
222
|
+
await repl.runAutoHealing(`AUTO DEBUG: ${debugTask}`);
|
|
223
|
+
return;
|
|
220
224
|
case '/plan:':
|
|
221
225
|
case '/plan-gen':
|
|
222
226
|
if (args.length === 0) {
|
package/src/cli/repl.js
CHANGED
|
@@ -21,6 +21,7 @@ import { formatMarkdown } from './markdown-format.js';
|
|
|
21
21
|
import { Spinner } from './spinner.js';
|
|
22
22
|
import { ContextLoader } from './context-loader.js';
|
|
23
23
|
import { PromptBuilder } from './prompt-builder.js';
|
|
24
|
+
import { buildProjectDocs, isWinterGeneratedProjectDoc } from './project-docs.js';
|
|
24
25
|
import { classifyModelTier, isSmallModel } from '../ai/model-capabilities.js';
|
|
25
26
|
import {
|
|
26
27
|
addUsage as mergeUsage,
|
|
@@ -231,104 +232,30 @@ export class WinterREPL {
|
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
// ── Tự động tạo design.md, skill.md, rule.md nếu chưa có ──────────────
|
|
234
|
-
const autoCreateDocs =
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const entries = await fsPromises.readdir(designDir, { withFileTypes: true });
|
|
242
|
-
brands = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
243
|
-
} catch {}
|
|
244
|
-
return `# Design Resources
|
|
245
|
-
|
|
246
|
-
Danh sách các design system có sẵn trong local resources:
|
|
247
|
-
|
|
248
|
-
## Available Brands (${brands.length})
|
|
249
|
-
|
|
250
|
-
${brands.length > 0 ? brands.map(b => `- ${b}`).join('\n') : '- Không tìm thấy design system nào.'}
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
*File này được tự động tạo bởi Winter CLI.*`;
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
filename: 'skill.md',
|
|
258
|
-
generate: async () => {
|
|
259
|
-
const catalog = await this.contextLoader.getStartupSkillCatalog();
|
|
260
|
-
const skills = [...catalog].sort();
|
|
261
|
-
return `# Available Skills
|
|
262
|
-
|
|
263
|
-
Danh sách các skill có sẵn trong hệ thống:
|
|
264
|
-
|
|
265
|
-
## Core Skills
|
|
266
|
-
- **coding**: Code analysis, generation, review
|
|
267
|
-
- **design**: Design system integration
|
|
268
|
-
- **debug**: Debugging assistance
|
|
269
|
-
- **refactor**: Code refactoring
|
|
270
|
-
- **test**: Test generation
|
|
271
|
-
- **security**: Security review
|
|
272
|
-
- **performance**: Performance optimization
|
|
273
|
-
|
|
274
|
-
## All Available Skills (${skills.length})
|
|
275
|
-
|
|
276
|
-
${skills.map(s => `- ${s}`).join('\n')}
|
|
277
|
-
|
|
278
|
-
---
|
|
279
|
-
*File này được tự động tạo bởi Winter CLI.*`;
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
filename: 'rule.md',
|
|
284
|
-
generate: async () => {
|
|
285
|
-
const parts = ['# Project Rules', '', '## Quy tắc dự án', ''];
|
|
286
|
-
// Load từ các instruction files đã có
|
|
287
|
-
const files = await this.readProjectInstructionFiles();
|
|
288
|
-
for (const file of files) {
|
|
289
|
-
if (file.relativePath === 'rule.md') continue; // skip self
|
|
290
|
-
parts.push(`- [${file.relativePath}](./${file.relativePath})`);
|
|
291
|
-
}
|
|
292
|
-
// Liệt kê các thư mục rules
|
|
293
|
-
const rulesDirs = [
|
|
294
|
-
this.getResourcePaths().codex.rules,
|
|
295
|
-
this.getUserResourcePaths()?.codexRules,
|
|
296
|
-
].filter(Boolean);
|
|
297
|
-
for (const dir of rulesDirs) {
|
|
298
|
-
try {
|
|
299
|
-
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
300
|
-
const ruleFiles = entries.filter(e => e.isFile() && e.name.endsWith('.md')).map(e => e.name);
|
|
301
|
-
if (ruleFiles.length > 0) {
|
|
302
|
-
parts.push('', `## Rules from ${path.basename(path.dirname(dir))}/${path.basename(dir)}`, '');
|
|
303
|
-
for (const f of ruleFiles) {
|
|
304
|
-
parts.push(`- ${f}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
} catch {}
|
|
308
|
-
}
|
|
309
|
-
// Liệt kê local resource rules
|
|
310
|
-
parts.push('', '## Local Resource Guidelines', '');
|
|
311
|
-
parts.push('- Karpathy tools guidelines available in local resources');
|
|
312
|
-
parts.push('- Agents.md project guidelines available in local resources');
|
|
313
|
-
parts.push('', '---', '*File này được tự động tạo bởi Winter CLI.*');
|
|
314
|
-
return parts.join('\n');
|
|
315
|
-
},
|
|
316
|
-
},
|
|
317
|
-
];
|
|
235
|
+
const autoCreateDocs = await buildProjectDocs({
|
|
236
|
+
projectPath: this.projectPath,
|
|
237
|
+
resourcePaths: this.getResourcePaths(),
|
|
238
|
+
userResourcePaths: this.getUserResourcePaths(),
|
|
239
|
+
contextLoader: this.contextLoader,
|
|
240
|
+
readProjectInstructionFiles: () => this.readProjectInstructionFiles(),
|
|
241
|
+
});
|
|
318
242
|
|
|
319
243
|
for (const doc of autoCreateDocs) {
|
|
320
244
|
const filePath = path.join(this.projectPath, doc.filename);
|
|
321
245
|
try {
|
|
322
|
-
await fsPromises.
|
|
323
|
-
|
|
246
|
+
const existing = await fsPromises.readFile(filePath, 'utf8');
|
|
247
|
+
if (!isWinterGeneratedProjectDoc(existing)) continue;
|
|
248
|
+
|
|
249
|
+
await fsPromises.writeFile(filePath, doc.content, 'utf8');
|
|
250
|
+
console.log(`${colors.green}✓ Đã nâng cấp file ${doc.filename} từ local resources.${colors.reset}`);
|
|
251
|
+
const memoryKey = `[Quy tắc dự án từ ${doc.filename}]`;
|
|
252
|
+
await this.session.replaceMemory(memoryKey, doc.content);
|
|
324
253
|
} catch {
|
|
325
|
-
// File chưa tồn tại, tự động tạo
|
|
326
254
|
try {
|
|
327
|
-
|
|
328
|
-
await fsPromises.writeFile(filePath, content, 'utf8');
|
|
255
|
+
await fsPromises.writeFile(filePath, doc.content, 'utf8');
|
|
329
256
|
console.log(`${colors.green}✓ Đã tự động tạo file ${doc.filename} từ local resources!${colors.reset}`);
|
|
330
257
|
const memoryKey = `[Quy tắc dự án từ ${doc.filename}]`;
|
|
331
|
-
await this.session.replaceMemory(memoryKey, content);
|
|
258
|
+
await this.session.replaceMemory(memoryKey, doc.content);
|
|
332
259
|
} catch (err) {
|
|
333
260
|
// Bỏ qua nếu không tạo được
|
|
334
261
|
}
|
|
@@ -894,7 +821,8 @@ ${colors.magenta}╭${'─'.repeat(w)}╮${colors.reset}
|
|
|
894
821
|
row(`${c.yellow}/config${c.reset} Xem cấu hình`, `${c.yellow}/exit${c.reset} Thoát`),
|
|
895
822
|
'',
|
|
896
823
|
`${c.bright}AI & Công cụ${c.reset}`,
|
|
897
|
-
row(`${c.yellow}/auto${c.reset} TDD
|
|
824
|
+
row(`${c.yellow}/auto${c.reset} TDD t? s?a l?i`, `${c.yellow}/debug${c.reset} Auto debug l?i`),
|
|
825
|
+
row(`${c.yellow}/agent${c.reset} Ch?y sub-agent`, `${c.yellow}/swe${c.reset} SWE workflow`),
|
|
898
826
|
row(`${c.yellow}/read${c.reset} Đọc file`, `${c.yellow}/write${c.reset} Ghi file`),
|
|
899
827
|
row(`${c.yellow}/bash${c.reset} Chạy lệnh terminal`, `${c.yellow}/grep${c.reset} Tìm trong file`),
|
|
900
828
|
row(`${c.yellow}/glob${c.reset} Tìm file theo pattern`, `${c.yellow}/image${c.reset} Phân tích UI`),
|
|
@@ -949,6 +877,8 @@ ${colors.white}Plans & Tasks:${colors.reset}
|
|
|
949
877
|
/task <desc> Create task
|
|
950
878
|
/tasks List tasks
|
|
951
879
|
/agent [role] <task> Run a subagent
|
|
880
|
+
/auto [task] Auto-heal with test/build loop
|
|
881
|
+
/debug [error] Auto-debug and verify a failure
|
|
952
882
|
|
|
953
883
|
${colors.white}Tools:${colors.reset}
|
|
954
884
|
/read <file> Read file
|
|
@@ -1150,6 +1080,8 @@ ${colors.reset}
|
|
|
1150
1080
|
const totalUsage = {};
|
|
1151
1081
|
const toolSignatureHistory = [];
|
|
1152
1082
|
const executionProfile = this.selectExecutionProfile(messages, { enableTools: true });
|
|
1083
|
+
const requireToolEvidence = this.actionRequiresTools(messages);
|
|
1084
|
+
let noToolActionRetries = 0;
|
|
1153
1085
|
try {
|
|
1154
1086
|
for (let i = 0; i < 8; i++) {
|
|
1155
1087
|
if (this.isCancelled) throw new Error('AbortError');
|
|
@@ -1157,6 +1089,7 @@ ${colors.reset}
|
|
|
1157
1089
|
provider: executionProfile.provider,
|
|
1158
1090
|
model: executionProfile.model,
|
|
1159
1091
|
enableTools: true,
|
|
1092
|
+
requireToolEvidence: requireToolEvidence && !usedTools,
|
|
1160
1093
|
}, startedAt, totalUsage);
|
|
1161
1094
|
|
|
1162
1095
|
const assistantMsg = turn.assistantMsg || {};
|
|
@@ -1167,6 +1100,25 @@ ${colors.reset}
|
|
|
1167
1100
|
}
|
|
1168
1101
|
|
|
1169
1102
|
if (toolCalls.length === 0) {
|
|
1103
|
+
if (turn.finishReason === 'tool_evidence_required') {
|
|
1104
|
+
noToolActionRetries++;
|
|
1105
|
+
if (noToolActionRetries > 2) {
|
|
1106
|
+
finalContent = 'Chưa thực hiện được: model trả lời mà không dùng tool nên Winter đã chặn để tránh báo xạo.';
|
|
1107
|
+
console.log(`\n${colors.yellow}${finalContent}${colors.reset}\n`);
|
|
1108
|
+
reachedToolLimit = false;
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
messages.push({
|
|
1112
|
+
role: 'assistant',
|
|
1113
|
+
content: assistantMsg.content || '',
|
|
1114
|
+
});
|
|
1115
|
+
messages.push({
|
|
1116
|
+
role: 'user',
|
|
1117
|
+
content: this.buildToolEvidenceCorrection(messages),
|
|
1118
|
+
});
|
|
1119
|
+
finalContent = '';
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1170
1122
|
if (turn.finishReason === 'length') {
|
|
1171
1123
|
console.log(`\n${colors.yellow}ℹ Phản hồi bị cắt cụt do hết token. Đang tự động tiếp tục...${colors.reset}`);
|
|
1172
1124
|
messages.push({
|
|
@@ -1304,6 +1256,51 @@ ${colors.yellow}ℹ AI tool loop detected (3 consecutive identical tool calls).
|
|
|
1304
1256
|
return { finalContent, usedTools };
|
|
1305
1257
|
}
|
|
1306
1258
|
|
|
1259
|
+
getLatestUserText(messages = []) {
|
|
1260
|
+
const list = Array.isArray(messages) ? messages : [{ role: 'user', content: String(messages || '') }];
|
|
1261
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
1262
|
+
const message = list[i];
|
|
1263
|
+
if (message?.role && message.role !== 'user') continue;
|
|
1264
|
+
const content = message?.content;
|
|
1265
|
+
if (typeof content === 'string') return content;
|
|
1266
|
+
if (Array.isArray(content)) {
|
|
1267
|
+
return content.map(part => part?.text || '').filter(Boolean).join('\n');
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return '';
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
actionRequiresTools(messages = []) {
|
|
1274
|
+
const text = this.getLatestUserText(messages).toLowerCase();
|
|
1275
|
+
if (!text.trim()) return false;
|
|
1276
|
+
|
|
1277
|
+
const actionPattern = /\b(fix|repair|bug|debug|implement|create|write|edit|modify|update|delete|remove|refactor|run|test|build|commit|push|publish|install|check|inspect|read|scan|grep|search|change|apply|patch)\b|(?:sửa|fix|làm|tạo|ghi|đọc|xóa|xoá|chạy|test|build|commit|push|publish|kiểm tra|check|cài|thêm|đổi|sửa lỗi|refactor|review|soát|quét|tìm|áp dụng)/i;
|
|
1278
|
+
const targetPattern = /\b(file|repo|project|code|src|test|build|git|npm|node|folder|directory|cli|tool|provider|model|config|readme|package\.json)\b|[A-Za-z]:[\\/]|\.js\b|\.ts\b|\.tsx\b|\.json\b|\.md\b|(?:dự án|mã|thư mục|tệp|file|cấu hình|lỗi|chức năng|tool|provider|model)/i;
|
|
1279
|
+
const pureQuestionPattern = /^(what|why|how|when|where|is|are|can|could|should|would|tại sao|vì sao|là gì|có nên|có phải)\b/i;
|
|
1280
|
+
|
|
1281
|
+
if (pureQuestionPattern.test(text) && !actionPattern.test(text)) return false;
|
|
1282
|
+
return actionPattern.test(text) && targetPattern.test(text);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
responseNeedsToolEvidence(content = '') {
|
|
1286
|
+
const text = String(content || '').toLowerCase();
|
|
1287
|
+
if (!text.trim()) return false;
|
|
1288
|
+
|
|
1289
|
+
const clarification = /(?:cần thêm|cho mình|vui lòng|please provide|which file|what file|need more|clarify|không rõ|chưa rõ|file nào|thư mục nào)/i;
|
|
1290
|
+
if (clarification.test(text)) return false;
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
buildToolEvidenceCorrection(messages = []) {
|
|
1295
|
+
const request = this.getLatestUserText(messages);
|
|
1296
|
+
return [
|
|
1297
|
+
'Runtime correction: the user requested an action that requires tool evidence.',
|
|
1298
|
+
'Your previous response did not use any tool, so it was blocked to avoid falsely claiming completion.',
|
|
1299
|
+
'Now use the available tools to inspect/edit/run/check as needed. Do not say the task is done until a tool result proves it.',
|
|
1300
|
+
`Original user request: ${request}`,
|
|
1301
|
+
].join('\n');
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1307
1304
|
async requestAssistantTurn(messages, options, startedAt, totalUsage) {
|
|
1308
1305
|
if (typeof this.ai.streamRequest === 'function') {
|
|
1309
1306
|
try {
|
|
@@ -1321,6 +1318,9 @@ ${colors.yellow}ℹ AI tool loop detected (3 consecutive identical tool calls).
|
|
|
1321
1318
|
const finishReason = response.choices?.[0]?.finish_reason;
|
|
1322
1319
|
|
|
1323
1320
|
if (assistantMsg.content && toolCalls.length === 0) {
|
|
1321
|
+
if (options?.requireToolEvidence && this.responseNeedsToolEvidence(assistantMsg.content)) {
|
|
1322
|
+
return { assistantMsg, toolCalls, finalContent: '', finishReason: 'tool_evidence_required' };
|
|
1323
|
+
}
|
|
1324
1324
|
this.printAssistantAnswer(assistantMsg.content, startedAt, totalUsage);
|
|
1325
1325
|
return { assistantMsg, toolCalls, finalContent: assistantMsg.content, finishReason };
|
|
1326
1326
|
}
|
|
@@ -1389,6 +1389,14 @@ ${colors.yellow}ℹ AI tool loop detected (3 consecutive identical tool calls).
|
|
|
1389
1389
|
const visibleContent = inlineToolExtraction.content || content;
|
|
1390
1390
|
|
|
1391
1391
|
if (bufferToolModeContent && toolCalls.length === 0 && visibleContent) {
|
|
1392
|
+
if (options?.requireToolEvidence && this.responseNeedsToolEvidence(visibleContent)) {
|
|
1393
|
+
return {
|
|
1394
|
+
assistantMsg: { content: visibleContent },
|
|
1395
|
+
toolCalls,
|
|
1396
|
+
finalContent: '',
|
|
1397
|
+
finishReason: 'tool_evidence_required',
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1392
1400
|
this.printAssistantAnswer(visibleContent, startedAt, totalUsage);
|
|
1393
1401
|
return {
|
|
1394
1402
|
assistantMsg: { content: visibleContent },
|
|
@@ -1576,8 +1584,9 @@ ${colors.yellow}ℹ AI tool loop detected (3 consecutive identical tool calls).
|
|
|
1576
1584
|
if (query === '/') {
|
|
1577
1585
|
const preferred = [
|
|
1578
1586
|
'/help', '/exit', '/pwd', '/cd',
|
|
1579
|
-
|
|
1580
|
-
|
|
1587
|
+
'/provider', '/model', '/models', '/providers',
|
|
1588
|
+
'/auto', '/debug', '/swe',
|
|
1589
|
+
'/read', '/write', '/glob', '/grep', '/bash',
|
|
1581
1590
|
'/codex', '/claude', '/karpathy', '/agents',
|
|
1582
1591
|
'/resources', '/designs', '/skills',
|
|
1583
1592
|
];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
let cachedDefaultProfile = null;
|
|
4
|
+
let cachedParentProcessName = null;
|
|
5
|
+
|
|
6
|
+
export function getHostOs(platform = process.platform) {
|
|
7
|
+
if (platform === 'win32') return 'Windows';
|
|
8
|
+
if (platform === 'darwin') return 'macOS';
|
|
9
|
+
if (platform === 'linux') return 'Linux';
|
|
10
|
+
return platform;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function detectTerminalApp(env = process.env, platform = process.platform) {
|
|
14
|
+
if (env.WT_SESSION) return 'Windows Terminal';
|
|
15
|
+
if (env.TERM_PROGRAM) return env.TERM_PROGRAM;
|
|
16
|
+
if (env.VSCODE_PID || env.TERM_PROGRAM === 'vscode') return 'VS Code integrated terminal';
|
|
17
|
+
if (env.ConEmuANSI || env.ConEmuBuild) return 'ConEmu/Cmder';
|
|
18
|
+
if (env.TERMINUS_SUBLIME || env.TERMINUS_SESSION) return 'Terminus';
|
|
19
|
+
if (env.TERM) return env.TERM;
|
|
20
|
+
if (platform === 'win32') return 'Windows console host';
|
|
21
|
+
return 'unknown';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function detectCurrentShell(env = process.env, platform = process.platform) {
|
|
25
|
+
if (platform !== 'win32') {
|
|
26
|
+
return {
|
|
27
|
+
name: env.SHELL ? env.SHELL.split(/[\\/]/).pop() : 'sh',
|
|
28
|
+
kind: env.SHELL ? env.SHELL.split(/[\\/]/).pop() : 'sh',
|
|
29
|
+
source: 'SHELL',
|
|
30
|
+
path: env.SHELL || '',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const shellPath = env.SHELL || '';
|
|
35
|
+
const lowerShell = shellPath.toLowerCase();
|
|
36
|
+
const parentName = detectParentProcessName(env, platform).toLowerCase();
|
|
37
|
+
const parentProcess = [
|
|
38
|
+
env.WINTER_PARENT_PROCESS,
|
|
39
|
+
env.npm_config_script_shell,
|
|
40
|
+
parentName,
|
|
41
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
42
|
+
|
|
43
|
+
if (lowerShell.includes('pwsh') || parentProcess.includes('pwsh')) {
|
|
44
|
+
return { name: 'pwsh', kind: 'powershell', source: 'env', path: shellPath };
|
|
45
|
+
}
|
|
46
|
+
if (lowerShell.includes('powershell') || parentProcess.includes('powershell')) {
|
|
47
|
+
return { name: 'powershell.exe', kind: 'powershell', source: 'env', path: shellPath };
|
|
48
|
+
}
|
|
49
|
+
if (lowerShell.includes('cmd.exe') || parentProcess.includes('cmd.exe')) {
|
|
50
|
+
return { name: 'cmd.exe', kind: 'cmd', source: 'parent/env', path: shellPath || env.ComSpec || '' };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
name: parentName || 'unknown Windows shell',
|
|
54
|
+
kind: 'unknown',
|
|
55
|
+
source: parentName ? 'parent' : 'unknown',
|
|
56
|
+
path: shellPath || env.ComSpec || '',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function detectParentProcessName(env = process.env, platform = process.platform) {
|
|
61
|
+
if (env.WINTER_PARENT_PROCESS) return env.WINTER_PARENT_PROCESS;
|
|
62
|
+
if (platform !== 'win32') return '';
|
|
63
|
+
if (env === process.env && cachedParentProcessName !== null) return cachedParentProcessName;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const script = `$p=(Get-CimInstance Win32_Process -Filter "ProcessId=${process.ppid}" -ErrorAction SilentlyContinue); if($p){$p.Name}`;
|
|
67
|
+
const parentName = execFileSync('powershell.exe', ['-NoProfile', '-Command', script], {
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
windowsHide: true,
|
|
70
|
+
timeout: 1500,
|
|
71
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
72
|
+
}).trim();
|
|
73
|
+
if (env === process.env) cachedParentProcessName = parentName;
|
|
74
|
+
return parentName;
|
|
75
|
+
} catch {
|
|
76
|
+
if (env === process.env) cachedParentProcessName = '';
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getRuntimeEnvironment(env = process.env, platform = process.platform) {
|
|
82
|
+
if (env === process.env && platform === process.platform && cachedDefaultProfile) {
|
|
83
|
+
return cachedDefaultProfile;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const shell = detectCurrentShell(env, platform);
|
|
87
|
+
const terminalApp = detectTerminalApp(env, platform);
|
|
88
|
+
const defaultExecutionShell = platform === 'win32'
|
|
89
|
+
? (shell.kind === 'cmd' ? 'cmd' : 'powershell')
|
|
90
|
+
: (env.SHELL || 'sh');
|
|
91
|
+
|
|
92
|
+
const profile = {
|
|
93
|
+
hostOs: getHostOs(platform),
|
|
94
|
+
platform,
|
|
95
|
+
arch: process.arch,
|
|
96
|
+
terminalApp,
|
|
97
|
+
shell,
|
|
98
|
+
defaultExecutionShell,
|
|
99
|
+
isWindows: platform === 'win32',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (env === process.env && platform === process.platform) {
|
|
103
|
+
cachedDefaultProfile = profile;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return profile;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function formatRuntimeEnvironmentSummary(profile = getRuntimeEnvironment()) {
|
|
110
|
+
const shellPath = profile.shell?.path ? ` (${profile.shell.path})` : '';
|
|
111
|
+
const shellRule = profile.isWindows
|
|
112
|
+
? [
|
|
113
|
+
'Bash tool shell rules:',
|
|
114
|
+
'- default/auto executes PowerShell unless the command clearly uses cmd.exe syntax.',
|
|
115
|
+
'- use shell:"powershell" for PowerShell cmdlets, pipes, Get-ChildItem, Select-String, npm/node commands.',
|
|
116
|
+
'- use shell:"cmd" for cmd builtins such as type, copy, del, dir /b, echo foo>file, && chains.',
|
|
117
|
+
'- do not mix PowerShell-only syntax with shell:"cmd" or cmd-only redirection with shell:"powershell".',
|
|
118
|
+
].join('\n')
|
|
119
|
+
: 'Bash tool shell rule: use the native POSIX shell; leave shell unspecified unless a specific shell is required.';
|
|
120
|
+
|
|
121
|
+
return [
|
|
122
|
+
`Host OS: ${profile.hostOs}`,
|
|
123
|
+
`Node platform: ${profile.platform}`,
|
|
124
|
+
`CPU arch: ${profile.arch}`,
|
|
125
|
+
`Terminal app: ${profile.terminalApp}`,
|
|
126
|
+
`Detected current shell: ${profile.shell?.name || 'unknown'} [${profile.shell?.kind || 'unknown'}]${shellPath}`,
|
|
127
|
+
`Bash default execution shell: ${profile.defaultExecutionShell}`,
|
|
128
|
+
shellRule,
|
|
129
|
+
].join('\n');
|
|
130
|
+
}
|
|
@@ -14,6 +14,9 @@ export const SLASH_COMMANDS = [
|
|
|
14
14
|
{ cmd: '/tasks', desc: 'List tasks' },
|
|
15
15
|
{ cmd: '/task', desc: 'Create task', usage: '/task <description>' },
|
|
16
16
|
{ cmd: '/agent', desc: 'Launch subagent', usage: '/agent <task>' },
|
|
17
|
+
{ cmd: '/auto', desc: 'Auto-heal with test/build loop', usage: '/auto [task]' },
|
|
18
|
+
{ cmd: '/debug', desc: 'Auto-debug errors with tool verification', usage: '/debug [error or task]' },
|
|
19
|
+
{ cmd: '/tdd', desc: 'Alias for auto-healing loop', usage: '/tdd [task]' },
|
|
17
20
|
{ cmd: '/read', desc: 'Read file', usage: '/read <file>' },
|
|
18
21
|
{ cmd: '/write', desc: 'Write file', usage: '/write <file> <content>' },
|
|
19
22
|
{ cmd: '/glob', desc: 'Find files', usage: '/glob <pattern>' },
|
|
@@ -47,4 +50,3 @@ export const SLASH_COMMANDS = [
|
|
|
47
50
|
{ cmd: '/exit', desc: 'Exit Winter' },
|
|
48
51
|
{ cmd: '/quit', desc: 'Exit Winter' },
|
|
49
52
|
];
|
|
50
|
-
|
package/src/session/manager.js
CHANGED
|
@@ -40,12 +40,40 @@ export class SessionManager {
|
|
|
40
40
|
console.log(`\x1b[33m⚠ Không tìm thấy session ${options.sessionId}, tạo session mới...\x1b[0m`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
//
|
|
43
|
+
// Mặc định resume session mới nhất của đúng project để /memories không bị rỗng sau restart.
|
|
44
|
+
if (options.resume !== false && !options.newSession) {
|
|
45
|
+
const success = await this.loadLatestProjectSession(options.project || process.cwd());
|
|
46
|
+
if (success) {
|
|
47
|
+
await this.rememberProject(this.currentSession?.project || options.project || process.cwd());
|
|
48
|
+
this.initialized = true;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
await this.newSession(options);
|
|
45
54
|
await this.rememberProject(options.project || this.currentSession?.project || process.cwd());
|
|
46
55
|
this.initialized = true;
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
normalizeProjectPath(projectPath) {
|
|
59
|
+
if (!projectPath) return '';
|
|
60
|
+
const normalized = path.resolve(projectPath);
|
|
61
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
hydrateSession(session) {
|
|
65
|
+
this.currentSession = session;
|
|
66
|
+
this.context = session.context || {};
|
|
67
|
+
this.plans = session.plans || [];
|
|
68
|
+
this.memory = session.memory || [];
|
|
69
|
+
this.currentSession.history = session.history || [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async writeCurrentPointer(sessionId) {
|
|
73
|
+
const currentPath = path.join(this.sessionsDir, 'active', 'current.json');
|
|
74
|
+
await fs.writeFile(currentPath, JSON.stringify({ id: sessionId }));
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
async loadSession(sessionId) {
|
|
50
78
|
const sessionPath = path.join(this.sessionsDir, 'active', `${sessionId}.json`);
|
|
51
79
|
try {
|
|
@@ -64,6 +92,43 @@ export class SessionManager {
|
|
|
64
92
|
}
|
|
65
93
|
}
|
|
66
94
|
|
|
95
|
+
async loadLatestProjectSession(projectPath) {
|
|
96
|
+
const activeDir = path.join(this.sessionsDir, 'active');
|
|
97
|
+
const targetProject = this.normalizeProjectPath(projectPath);
|
|
98
|
+
if (!targetProject) return false;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const files = await fs.readdir(activeDir);
|
|
102
|
+
const candidates = [];
|
|
103
|
+
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
if (!file.endsWith('.json') || file === 'current.json') continue;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const sessionData = await fs.readFile(path.join(activeDir, file), 'utf8');
|
|
109
|
+
const session = JSON.parse(sessionData);
|
|
110
|
+
if (this.normalizeProjectPath(session.project) !== targetProject) continue;
|
|
111
|
+
|
|
112
|
+
candidates.push({
|
|
113
|
+
session,
|
|
114
|
+
time: Date.parse(session.updatedAt || session.createdAt || 0) || 0,
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
// Skip corrupt or partially-written session files.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
candidates.sort((a, b) => b.time - a.time);
|
|
122
|
+
if (candidates.length === 0) return false;
|
|
123
|
+
|
|
124
|
+
this.hydrateSession(candidates[0].session);
|
|
125
|
+
await this.writeCurrentPointer(this.currentSession.id);
|
|
126
|
+
return true;
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
67
132
|
async newSession(options = {}) {
|
|
68
133
|
const sessionId = crypto.randomUUID();
|
|
69
134
|
const sessionPath = path.join(this.sessionsDir, 'active', `${sessionId}.json`);
|
package/src/tools/executor.js
CHANGED
|
@@ -20,6 +20,7 @@ import { AgentTool } from './agent.js';
|
|
|
20
20
|
import { InsertTextTool } from './insert-text.js';
|
|
21
21
|
import { StrReplaceAllTool } from './str-replace-all.js';
|
|
22
22
|
import { WebArchiveTool } from './web-archive.js';
|
|
23
|
+
import { formatRuntimeEnvironmentSummary, getRuntimeEnvironment } from '../cli/runtime-env.js';
|
|
23
24
|
|
|
24
25
|
const execAsync = promisify(exec);
|
|
25
26
|
const execFileAsync = promisify(execFile);
|
|
@@ -44,28 +45,7 @@ export class ToolExecutor {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
getRuntimeEnvironmentSummary() {
|
|
47
|
-
|
|
48
|
-
? 'Windows'
|
|
49
|
-
: process.platform === 'darwin'
|
|
50
|
-
? 'macOS'
|
|
51
|
-
: process.platform === 'linux'
|
|
52
|
-
? 'Linux'
|
|
53
|
-
: process.platform;
|
|
54
|
-
|
|
55
|
-
const currentShell = process.platform === 'win32'
|
|
56
|
-
? (process.env.PSModulePath ? 'powershell-capable' : 'cmd/unknown')
|
|
57
|
-
: (process.env.SHELL || 'bash/sh');
|
|
58
|
-
|
|
59
|
-
const shellGuidance = process.platform === 'win32'
|
|
60
|
-
? 'Use shell:"powershell" for PowerShell cmdlets, shell:"cmd" for cmd.exe syntax, and shell:"auto" when unsure.'
|
|
61
|
-
: 'Use the native POSIX shell on non-Windows hosts and leave shell unspecified unless a specific shell is required.';
|
|
62
|
-
|
|
63
|
-
return [
|
|
64
|
-
`Host OS: ${hostOs}`,
|
|
65
|
-
`Node platform: ${process.platform}`,
|
|
66
|
-
`Current shell hint: ${currentShell}`,
|
|
67
|
-
`Shell rule: ${shellGuidance}`,
|
|
68
|
-
].join('\n');
|
|
48
|
+
return formatRuntimeEnvironmentSummary(getRuntimeEnvironment());
|
|
69
49
|
}
|
|
70
50
|
|
|
71
51
|
getToolDefinitions() {
|
|
@@ -1057,6 +1037,9 @@ export class ToolExecutor {
|
|
|
1057
1037
|
}
|
|
1058
1038
|
|
|
1059
1039
|
try {
|
|
1040
|
+
const executedShell = process.platform === 'win32'
|
|
1041
|
+
? (requestedShell === 'auto' ? this.detectWindowsShell(command) : requestedShell)
|
|
1042
|
+
: (process.env.SHELL || 'native');
|
|
1060
1043
|
const { stdout, stderr } = process.platform === 'win32'
|
|
1061
1044
|
? await this.execWindowsCommand(command, cwd, timeout, requestedShell)
|
|
1062
1045
|
: await execAsync(command, { cwd, timeout, shell: true, maxBuffer: 10 * 1024 * 1024 });
|
|
@@ -1064,7 +1047,8 @@ export class ToolExecutor {
|
|
|
1064
1047
|
success: true,
|
|
1065
1048
|
stdout: stdout || '',
|
|
1066
1049
|
stderr: stderr || '',
|
|
1067
|
-
exitCode: 0
|
|
1050
|
+
exitCode: 0,
|
|
1051
|
+
shell: executedShell,
|
|
1068
1052
|
};
|
|
1069
1053
|
} catch (error) {
|
|
1070
1054
|
return {
|