cc-viewer 1.9.15 → 1.9.17
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/cli.js +1 -0
- package/concepts/ar/UltraPlan.md +9 -9
- package/concepts/da/UltraPlan.md +9 -9
- package/concepts/de/UltraPlan.md +9 -9
- package/concepts/en/UltraPlan.md +9 -9
- package/concepts/es/UltraPlan.md +9 -9
- package/concepts/fr/UltraPlan.md +9 -9
- package/concepts/it/UltraPlan.md +9 -9
- package/concepts/ja/UltraPlan.md +9 -9
- package/concepts/ko/UltraPlan.md +9 -9
- package/concepts/no/UltraPlan.md +9 -9
- package/concepts/pl/UltraPlan.md +9 -9
- package/concepts/pt-BR/UltraPlan.md +9 -9
- package/concepts/ru/UltraPlan.md +9 -9
- package/concepts/th/UltraPlan.md +9 -9
- package/concepts/tr/UltraPlan.md +9 -9
- package/concepts/uk/UltraPlan.md +9 -9
- package/concepts/zh/UltraPlan.md +9 -9
- package/concepts/zh-TW/UltraPlan.md +9 -9
- package/dist/assets/App-BlNbziAQ.js +2 -0
- package/dist/assets/App-BvVdj099.css +1 -0
- package/dist/assets/{MdxEditorPanel-DRLFmdU5.js → MdxEditorPanel-C9cr-WBH.js} +1 -1
- package/dist/assets/Mobile-CDQPYYQo.js +1 -0
- package/dist/assets/{ProxyStatsModal-Ba1E1lu4.js → ProxyStatsModal-BEqrTR-F.js} +1 -1
- package/dist/assets/index-B4J-uwYG.js +2 -0
- package/dist/assets/seqResourceLoaders-B-5xGs9q.css +41 -0
- package/dist/assets/seqResourceLoaders-DaZ8fZXu.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/lib/ask/perm-bridge.js +4 -1
- package/server/lib/auto-approve.js +160 -0
- package/server/lib/file-access-policy.js +132 -5
- package/server/lib/os-open.js +66 -0
- package/server/lib/sdk-manager.js +96 -9
- package/server/lib/target-scope.js +174 -0
- package/server/routes/ask-perm.js +54 -3
- package/server/routes/files-content.js +174 -8
- package/server/routes/files-fs.js +8 -54
- package/server/routes/skills.js +19 -8
- package/server/routes/target-files-fs.js +332 -0
- package/server/server.js +89 -14
- package/server/system-prompt-templates/presets/kimi-k3.md +1 -0
- package/ultraAgents/code-expert.json +1 -1
- package/dist/assets/App-BCDnm2Lh.js +0 -2
- package/dist/assets/App-CCnPkf2_.css +0 -1
- package/dist/assets/Mobile-C8VJqBwa.js +0 -1
- package/dist/assets/index-Sp3JYYKo.js +0 -2
- package/dist/assets/seqResourceLoaders-BUfjTaCV.css +0 -41
- package/dist/assets/seqResourceLoaders-CHmtPDFO.js +0 -2
package/server/routes/skills.js
CHANGED
|
@@ -9,13 +9,20 @@ import { loadWorkspaces } from '../workspace-registry.js';
|
|
|
9
9
|
import { listLivePtys } from '../pty-manager.js';
|
|
10
10
|
|
|
11
11
|
// Multi-project (2026-10): the skills LIST resolves the viewed project via
|
|
12
|
-
// the shared viewRootOrReply (?project=). toggle/delete
|
|
13
|
-
// bound root
|
|
14
|
-
|
|
12
|
+
// the shared viewRootOrReply (?project=). toggle/delete used to keep the
|
|
13
|
+
// bound root — that was a deliberate safety choice, but it breaks multi-view
|
|
14
|
+
// semantics: the list shows skills for project B while a click on toggle or
|
|
15
|
+
// delete silently rewrote project A's same-named skill. Resolve mutations
|
|
16
|
+
// through the same viewRootOrReply (with body-level project/instance) so the
|
|
17
|
+
// action targets the project the user is actually looking at; the helper
|
|
18
|
+
// still constrains the resolved root to bound/registered/live projects.
|
|
19
|
+
function _viewRootOrReply(req, res, parsedUrl, bodyProject, bodyInstance) {
|
|
15
20
|
return viewRootOrReply(req, res, parsedUrl, {
|
|
16
21
|
boundCwd: process.env.CCV_PROJECT_DIR || process.cwd(),
|
|
17
22
|
loadWorkspaces,
|
|
18
23
|
listLivePtys,
|
|
24
|
+
bodyProject,
|
|
25
|
+
bodyInstance,
|
|
19
26
|
});
|
|
20
27
|
}
|
|
21
28
|
|
|
@@ -34,18 +41,20 @@ async function skillsList(req, res, parsedUrl) {
|
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
// Skill 动态装卸 —— 切换单个 skill(在 skills/ 和 skills-skip/ 之间 move)
|
|
37
|
-
function skillsToggle(req, res) {
|
|
44
|
+
function skillsToggle(req, res, parsedUrl) {
|
|
38
45
|
let body = '';
|
|
39
46
|
req.on('data', chunk => { body += chunk; if (body.length > 4096) req.destroy(); });
|
|
40
47
|
req.on('end', async () => {
|
|
41
48
|
try {
|
|
42
|
-
const { source, name, enable } = JSON.parse(body);
|
|
49
|
+
const { source, name, enable, project, instance } = JSON.parse(body);
|
|
50
|
+
const projectDir = _viewRootOrReply(req, res, parsedUrl, project, instance);
|
|
51
|
+
if (!projectDir) return;
|
|
43
52
|
// 不加进程锁:moveSkill 里 existsSync 前置 + renameSync 原子性已能让并发
|
|
44
53
|
// toggle 落到合理分支(一个成功、另一个拿 SOURCE_MISSING 或 DUPLICATE);
|
|
45
54
|
// 前端 toggling:Set 也已防同 tab 连点,两者叠加足够安全
|
|
46
55
|
moveSkill({
|
|
47
56
|
source, name, enable: !!enable,
|
|
48
|
-
projectDir
|
|
57
|
+
projectDir,
|
|
49
58
|
});
|
|
50
59
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
51
60
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -75,10 +84,12 @@ function skillsDelete(req, res, parsedUrl, isLocal) {
|
|
|
75
84
|
req.on('data', chunk => { body += chunk; if (body.length > 4096) req.destroy(); });
|
|
76
85
|
req.on('end', async () => {
|
|
77
86
|
try {
|
|
78
|
-
const { source, name, enabled } = JSON.parse(body);
|
|
87
|
+
const { source, name, enabled, project, instance } = JSON.parse(body);
|
|
88
|
+
const projectDir = _viewRootOrReply(req, res, parsedUrl, project, instance);
|
|
89
|
+
if (!projectDir) return;
|
|
79
90
|
deleteSkill({
|
|
80
91
|
source, name, enabled: !!enabled,
|
|
81
|
-
projectDir
|
|
92
|
+
projectDir,
|
|
82
93
|
});
|
|
83
94
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
84
95
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Target-mode filesystem mutation + OS-open routes for the file browser modal
|
|
2
|
+
// (CLAUDE.md / skill folder targets). These mirror the project-scoped routes in
|
|
3
|
+
// files-fs.js, but resolve the writable root from the TARGET identity (kind+id /
|
|
4
|
+
// source+targetPath) via lib/target-scope.js instead of the viewed project —
|
|
5
|
+
// because a target frequently lives OUTSIDE any project root (~/.claude, $HOME),
|
|
6
|
+
// where no ?project= value could resolve it.
|
|
7
|
+
//
|
|
8
|
+
// Path contract: `path` / `dirPath` / `oldPath` in the request body are relative
|
|
9
|
+
// to the target's NAVIGATION ROOT (the same root the read route /api/target-files
|
|
10
|
+
// returns entries against — that is what the browser displays in its breadcrumb).
|
|
11
|
+
// This differs from files-fs.js, where paths are relative to the project root.
|
|
12
|
+
// Do NOT "fix" callers to send project-relative paths.
|
|
13
|
+
//
|
|
14
|
+
// SECURITY / INTENTIONAL DESIGN (do NOT weaken in review without product sign-off):
|
|
15
|
+
// - The writable surface is the WHOLE navigation root (see
|
|
16
|
+
// lib/target-scope.js resolveTargetWriteScope), a deliberate product decision
|
|
17
|
+
// so the context menu keeps working after the user navigates to ancestors.
|
|
18
|
+
// - Sensitive files/dirs inside that wider surface are the backstop of
|
|
19
|
+
// isWriteAllowed (lib/file-access-policy.js): ~/.claude credentials/settings/
|
|
20
|
+
// history, ~/.ssh, the LOG_DIR tree, the config-backup tree, private keys, etc.
|
|
21
|
+
// - rename validates BOTH source and destination with isWriteAllowed (renaming a
|
|
22
|
+
// denylisted name to a benign one must not make it readable); recursive delete
|
|
23
|
+
// scans the subtree and refuses if any descendant is write-denied.
|
|
24
|
+
// - Every handler is gated by isAdminReq + isSameOriginBrowserRequest BEFORE the
|
|
25
|
+
// body is read (same bar as the read route), because these endpoints can reach
|
|
26
|
+
// the user's home tree.
|
|
27
|
+
// - Symlinks are rejected as mutation targets (lstat on the spelled path);
|
|
28
|
+
// isWriteAllowed additionally refuses a dangling-symlink leaf so create/save
|
|
29
|
+
// cannot be redirected outside the root. Delete re-realpaths before rmdir
|
|
30
|
+
// (TOCTOU defense).
|
|
31
|
+
import {
|
|
32
|
+
existsSync, statSync, lstatSync, mkdirSync, writeFileSync,
|
|
33
|
+
realpathSync, rmSync, unlinkSync, readdirSync,
|
|
34
|
+
} from 'node:fs';
|
|
35
|
+
import { join, dirname, basename } from 'node:path';
|
|
36
|
+
import { isWriteAllowed, isReadAllowed } from '../lib/file-access-policy.js';
|
|
37
|
+
import { renameSyncWithRetry } from '../lib/file-api.js';
|
|
38
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
39
|
+
import { revealInOs, openTerminalAt } from '../lib/os-open.js';
|
|
40
|
+
import { viewRootOrReply } from '../lib/view-root.js';
|
|
41
|
+
import { isAdminReq } from '../lib/is-admin.js';
|
|
42
|
+
import { isSameOriginBrowserRequest } from '../lib/same-origin.js';
|
|
43
|
+
import { loadWorkspaces } from '../workspace-registry.js';
|
|
44
|
+
import { listLivePtys } from '../pty-manager.js';
|
|
45
|
+
import {
|
|
46
|
+
targetError, errnoToStatus, insidePath, requiredTargetRelPath, resolveTargetWriteScope,
|
|
47
|
+
} from '../lib/target-scope.js';
|
|
48
|
+
|
|
49
|
+
function forbidden(res) {
|
|
50
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
51
|
+
res.end(JSON.stringify({ error: 'forbidden' }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function send(res, status, obj) {
|
|
55
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
56
|
+
res.end(JSON.stringify(obj));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function replyTargetError(res, err) {
|
|
60
|
+
const status = err?.status || 500;
|
|
61
|
+
if (status === 500) console.error('[api/target-*]', err);
|
|
62
|
+
send(res, status, { error: status === 500 ? 'Internal error' : err.message });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Resolve the viewed project cwd (query project/instance win over POST body).
|
|
66
|
+
function cwdFor(req, res, parsedUrl, body) {
|
|
67
|
+
return viewRootOrReply(req, res, parsedUrl, {
|
|
68
|
+
boundCwd: process.env.CCV_PROJECT_DIR || process.cwd(),
|
|
69
|
+
loadWorkspaces,
|
|
70
|
+
listLivePtys,
|
|
71
|
+
bodyProject: body?.project,
|
|
72
|
+
bodyInstance: body?.instance,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Read + JSON-parse the POST body, then run `run(body, scope)`. The admin +
|
|
77
|
+
// same-origin gate runs BEFORE the body is consumed so a cross-site / non-admin
|
|
78
|
+
// caller cannot even reach the parser. `scope` is re-derived server-side from
|
|
79
|
+
// the target identity in the query string (never from a client-sent root).
|
|
80
|
+
function withBody(req, res, parsedUrl, isLocal, deps, run) {
|
|
81
|
+
if (!isAdminReq(req, isLocal) || !isSameOriginBrowserRequest(req, parsedUrl)) {
|
|
82
|
+
return forbidden(res);
|
|
83
|
+
}
|
|
84
|
+
let body = '';
|
|
85
|
+
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
86
|
+
req.on('end', () => {
|
|
87
|
+
let parsed;
|
|
88
|
+
try { parsed = JSON.parse(body); } catch { return send(res, 400, { error: 'Invalid request body' }); }
|
|
89
|
+
try {
|
|
90
|
+
const cwd = cwdFor(req, res, parsedUrl, parsed);
|
|
91
|
+
if (!cwd) return;
|
|
92
|
+
const scope = resolveTargetWriteScope(parsedUrl, cwd);
|
|
93
|
+
run(parsed, scope);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
replyTargetError(res, err);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Reject symlink mutation targets: an attacker could swap a symlink between
|
|
101
|
+
// validation and the mutation so the operation lands outside the navigation
|
|
102
|
+
// root. lstat does not follow the link, so an isSymbolicLink() hit refuses.
|
|
103
|
+
function lstatNoSymlink(fullPath) {
|
|
104
|
+
let st;
|
|
105
|
+
try { st = lstatSync(fullPath); } catch (err) { throw targetError(errnoToStatus(err), 'Path not found'); }
|
|
106
|
+
if (st.isSymbolicLink()) throw targetError(400, 'Cannot operate on symbolic links via this endpoint');
|
|
107
|
+
return st;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Walk a directory subtree and throw 403 if any descendant fails the write
|
|
111
|
+
// policy. Used before a recursive rmdir so deleting a directory cannot bypass
|
|
112
|
+
// the per-file denylist (credentials, settings, transcripts). Symlinks are
|
|
113
|
+
// skipped (their targets are evaluated by the caller's own containment checks).
|
|
114
|
+
function assertSubtreeWritable(dirReal) {
|
|
115
|
+
let dirents;
|
|
116
|
+
try { dirents = readdirSync(dirReal, { withFileTypes: true }); } catch { return; }
|
|
117
|
+
for (const entry of dirents) {
|
|
118
|
+
const child = join(dirReal, entry.name);
|
|
119
|
+
let lst;
|
|
120
|
+
try { lst = lstatSync(child); } catch { continue; }
|
|
121
|
+
if (lst.isSymbolicLink()) continue;
|
|
122
|
+
const policy = isWriteAllowed(child);
|
|
123
|
+
if (!policy.ok) throw targetError(403, `Refusing to delete: contains a protected path (${entry.name})`);
|
|
124
|
+
if (lst.isDirectory()) assertSubtreeWritable(child);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateName(name, kind) {
|
|
129
|
+
if (!name) throw targetError(400, 'Missing name');
|
|
130
|
+
if (name.includes('/') || name.includes('\\') || name.includes('..') || /[\x00-\x1f]/.test(name)) {
|
|
131
|
+
throw targetError(400, `Invalid ${kind} name`);
|
|
132
|
+
}
|
|
133
|
+
// A `.git` segment must never be created/renamed into place from the browser.
|
|
134
|
+
if (name.toLowerCase() === '.git') throw targetError(400, `Invalid ${kind} name`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Resolve a navigation-root-relative path to its real absolute path, enforcing
|
|
138
|
+
// containment in the navigation root. Returns the REAL path (symlinks resolved).
|
|
139
|
+
function resolveContained(scope, segments) {
|
|
140
|
+
const full = segments.length ? join(scope.rootReal, ...segments) : scope.rootReal;
|
|
141
|
+
let real;
|
|
142
|
+
try { real = realpathSync(full); } catch (err) { throw targetError(errnoToStatus(err), 'Path not found'); }
|
|
143
|
+
if (!insidePath(real, scope.rootReal)) throw targetError(403, 'Path outside navigation root');
|
|
144
|
+
return { full, real };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── reveal in OS file manager ──────────────────────────────────────────────
|
|
148
|
+
function targetRevealFile(req, res, parsedUrl, isLocal, deps) {
|
|
149
|
+
withBody(req, res, parsedUrl, isLocal, deps, (parsed, scope) => {
|
|
150
|
+
// '' resolves to the navigation root itself (the browser's container menu).
|
|
151
|
+
const segments = requiredTargetRelPath(parsed.path ?? '');
|
|
152
|
+
const { full, real } = resolveContained(scope, segments);
|
|
153
|
+
// Reveal is a READ-side action (open the OS file manager), so gate it with the
|
|
154
|
+
// read policy, not the write policy — a read-only-but-unwritable entry (e.g.
|
|
155
|
+
// inside LOG_DIR) must still be revealable.
|
|
156
|
+
if (parsed.path) {
|
|
157
|
+
const policy = isReadAllowed(real);
|
|
158
|
+
if (!policy.ok) throw targetError(403, 'Path not allowed');
|
|
159
|
+
}
|
|
160
|
+
// A directory reveals itself; a file reveals its parent (Linux xdg-open on a
|
|
161
|
+
// file would launch the default app instead of showing it).
|
|
162
|
+
revealInOs(full, { isDir: statSync(real).isDirectory() });
|
|
163
|
+
send(res, 200, { ok: true, fullPath: full });
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── open in OS terminal ────────────────────────────────────────────────────
|
|
168
|
+
function targetOpenTerminal(req, res, parsedUrl, isLocal, deps) {
|
|
169
|
+
withBody(req, res, parsedUrl, isLocal, deps, (parsed, scope) => {
|
|
170
|
+
const segments = requiredTargetRelPath(parsed.path ?? '');
|
|
171
|
+
const { real } = resolveContained(scope, segments);
|
|
172
|
+
const st = statSync(real);
|
|
173
|
+
// Opening a terminal on a FILE opens it in the file's directory instead.
|
|
174
|
+
const dir = st.isDirectory() ? real : dirname(real);
|
|
175
|
+
openTerminalAt(dir);
|
|
176
|
+
send(res, 200, { ok: true });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── resolve to absolute path (for copy-path / attach-to-chat) ──────────────
|
|
181
|
+
function targetResolvePath(req, res, parsedUrl, isLocal, deps) {
|
|
182
|
+
withBody(req, res, parsedUrl, isLocal, deps, (parsed, scope) => {
|
|
183
|
+
const segments = parsed.path ? requiredTargetRelPath(parsed.path) : [];
|
|
184
|
+
const full = segments.length ? join(scope.rootReal, ...segments) : scope.rootReal;
|
|
185
|
+
send(res, 200, { ok: true, fullPath: full });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── create file ────────────────────────────────────────────────────────────
|
|
190
|
+
function targetCreateFile(req, res, parsedUrl, isLocal, deps) {
|
|
191
|
+
withBody(req, res, parsedUrl, isLocal, deps, (parsed, scope) => {
|
|
192
|
+
validateName(parsed.name, 'file');
|
|
193
|
+
const dirSegs = parsed.dirPath ? requiredTargetRelPath(parsed.dirPath) : [];
|
|
194
|
+
const { real: dirReal } = resolveContained(scope, dirSegs);
|
|
195
|
+
if (!statSync(dirReal).isDirectory()) throw targetError(400, 'Directory not found');
|
|
196
|
+
const full = join(dirReal, parsed.name);
|
|
197
|
+
if (existsSync(full)) throw targetError(409, 'File already exists');
|
|
198
|
+
const policy = isWriteAllowed(full);
|
|
199
|
+
if (!policy.ok) throw targetError(403, 'Path not allowed');
|
|
200
|
+
writeFileSync(policy.real, '');
|
|
201
|
+
const relPath = parsed.dirPath ? `${parsed.dirPath}/${parsed.name}` : parsed.name;
|
|
202
|
+
send(res, 200, { ok: true, path: relPath });
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── create directory ───────────────────────────────────────────────────────
|
|
207
|
+
function targetCreateDir(req, res, parsedUrl, isLocal, deps) {
|
|
208
|
+
withBody(req, res, parsedUrl, isLocal, deps, (parsed, scope) => {
|
|
209
|
+
validateName(parsed.name, 'folder');
|
|
210
|
+
const dirSegs = parsed.dirPath ? requiredTargetRelPath(parsed.dirPath) : [];
|
|
211
|
+
const { real: dirReal } = resolveContained(scope, dirSegs);
|
|
212
|
+
if (!statSync(dirReal).isDirectory()) throw targetError(400, 'Directory not found');
|
|
213
|
+
const full = join(dirReal, parsed.name);
|
|
214
|
+
if (existsSync(full)) throw targetError(409, 'Already exists');
|
|
215
|
+
const policy = isWriteAllowed(full);
|
|
216
|
+
if (!policy.ok) throw targetError(403, 'Path not allowed');
|
|
217
|
+
mkdirSync(policy.real);
|
|
218
|
+
const relPath = parsed.dirPath ? `${parsed.dirPath}/${parsed.name}` : parsed.name;
|
|
219
|
+
send(res, 200, { ok: true, path: relPath });
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── rename (basename only; never moves across directories) ────────────────
|
|
224
|
+
function targetRenameFile(req, res, parsedUrl, isLocal, deps) {
|
|
225
|
+
withBody(req, res, parsedUrl, isLocal, deps, (parsed, scope) => {
|
|
226
|
+
const { oldPath, newName } = parsed;
|
|
227
|
+
if (!oldPath || !newName) throw targetError(400, 'Missing oldPath or newName');
|
|
228
|
+
validateName(newName, 'file');
|
|
229
|
+
const segs = requiredTargetRelPath(oldPath);
|
|
230
|
+
// Renaming the navigation root itself would break the browser's own anchor.
|
|
231
|
+
if (segs.length === 0) throw targetError(403, 'Cannot rename the navigation root');
|
|
232
|
+
const { full: oldFull, real: oldReal } = resolveContained(scope, segs);
|
|
233
|
+
// Symlink check must run on the SPELLED path (oldFull), not the realpath'd
|
|
234
|
+
// one — lstat on oldReal can never see a symlink, so checking it would
|
|
235
|
+
// silently rename the link's target instead of refusing the link.
|
|
236
|
+
lstatNoSymlink(oldFull);
|
|
237
|
+
// SECURITY: validate the SOURCE with the write policy too, not just the
|
|
238
|
+
// destination. Renaming a denylisted name (settings.json / credentials.json /
|
|
239
|
+
// master.key) or subtree (projects/ todos/ shell-snapshots/ …) to a benign
|
|
240
|
+
// name would otherwise make it readable via /api/file-content — a denylist
|
|
241
|
+
// bypass. Do NOT drop this check in review.
|
|
242
|
+
const srcPolicy = isWriteAllowed(oldReal);
|
|
243
|
+
if (!srcPolicy.ok) throw targetError(403, 'Path not allowed');
|
|
244
|
+
// SECURITY: renaming a DIRECTORY moves its whole subtree, so apply the same
|
|
245
|
+
// per-descendant write scan as delete. Without this, renaming ~/.claude (or a
|
|
246
|
+
// sensitive-prefix dir like ~/.ssh) to a benign name would sidestep every
|
|
247
|
+
// path-based denylist (the read policy keys off the live config-dir path) and
|
|
248
|
+
// expose credentials/transcripts. Do NOT drop this scan in review.
|
|
249
|
+
if (statSync(oldReal).isDirectory()) assertSubtreeWritable(oldReal);
|
|
250
|
+
// Parent must also stay inside the navigation root (it does, but the rename
|
|
251
|
+
// destination is derived from it, so assert before composing).
|
|
252
|
+
const parentDir = dirname(oldReal);
|
|
253
|
+
if (!insidePath(parentDir, scope.rootReal)) throw targetError(403, 'Path outside navigation root');
|
|
254
|
+
const newFull = join(parentDir, newName);
|
|
255
|
+
const policy = isWriteAllowed(newFull);
|
|
256
|
+
if (!policy.ok) throw targetError(403, 'Path not allowed');
|
|
257
|
+
const parentRel = segs.slice(0, -1).join('/');
|
|
258
|
+
const newRelPath = parentRel ? `${parentRel}/${newName}` : newName;
|
|
259
|
+
|
|
260
|
+
// Case-only rename on a case-insensitive FS needs a temp hop (same dance as
|
|
261
|
+
// files-fs.js renameFile) so NTFS/APFS do not see the destination as the source.
|
|
262
|
+
const oldName = basename(oldReal);
|
|
263
|
+
if (oldName !== newName && oldName.toLowerCase() === newName.toLowerCase()) {
|
|
264
|
+
try {
|
|
265
|
+
if (realpathSync.native(newFull) === realpathSync.native(oldReal)) {
|
|
266
|
+
const tempPath = join(parentDir, `.ccv-rename-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
267
|
+
renameSyncWithRetry(oldReal, tempPath);
|
|
268
|
+
try { renameSyncWithRetry(tempPath, newFull); }
|
|
269
|
+
catch (err) {
|
|
270
|
+
try { renameSyncWithRetry(tempPath, oldReal); }
|
|
271
|
+
catch (rollbackErr) { reportSwallowed('targetFs.renameRollback', rollbackErr, { tempPath }); }
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
return send(res, 200, { ok: true, newPath: newRelPath });
|
|
275
|
+
}
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (err.code !== 'ENOENT') throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (existsSync(newFull)) throw targetError(409, 'Target already exists');
|
|
281
|
+
renameSyncWithRetry(oldReal, newFull);
|
|
282
|
+
send(res, 200, { ok: true, newPath: newRelPath });
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── delete ─────────────────────────────────────────────────────────────────
|
|
287
|
+
function targetDeleteFile(req, res, parsedUrl, isLocal, deps) {
|
|
288
|
+
withBody(req, res, parsedUrl, isLocal, deps, (parsed, scope) => {
|
|
289
|
+
const filePath = parsed.path;
|
|
290
|
+
if (!filePath) throw targetError(400, 'Missing path');
|
|
291
|
+
const segs = requiredTargetRelPath(filePath);
|
|
292
|
+
if (segs.length === 0) throw targetError(403, 'Cannot delete the navigation root');
|
|
293
|
+
const { full, real } = resolveContained(scope, segs);
|
|
294
|
+
const policy = isWriteAllowed(real);
|
|
295
|
+
if (!policy.ok) throw targetError(403, 'Path not allowed');
|
|
296
|
+
const st = lstatNoSymlink(full);
|
|
297
|
+
if (st.isDirectory()) {
|
|
298
|
+
// protectedDirs guard must defeat Win backslash + NTFS case-insensitive
|
|
299
|
+
// spellings, so normalize both separators and case before the check.
|
|
300
|
+
const protectedDirs = new Set(['node_modules', '.git', '.svn', '.hg']);
|
|
301
|
+
const normalizedSegs = filePath.split(/[\\/]/).map(s => s.toLowerCase());
|
|
302
|
+
if (normalizedSegs.some(part => protectedDirs.has(part))) {
|
|
303
|
+
throw targetError(400, 'Cannot delete protected directory');
|
|
304
|
+
}
|
|
305
|
+
// Defense-in-depth: re-realpath in the window between lstat and rmdir.
|
|
306
|
+
const real2 = realpathSync(full);
|
|
307
|
+
if (!insidePath(real2, scope.rootReal)) throw targetError(403, 'Path escaped navigation root');
|
|
308
|
+
// SECURITY: a recursive rmdir must not bypass the per-file write policy.
|
|
309
|
+
// Scan the subtree and refuse the whole delete if ANY descendant is
|
|
310
|
+
// write-denied (settings.json / credentials / projects transcripts / …),
|
|
311
|
+
// otherwise deleting ~/.claude would wipe the very files the policy
|
|
312
|
+
// protects one-by-one. Do NOT remove this scan to "speed up" deletes.
|
|
313
|
+
assertSubtreeWritable(real2);
|
|
314
|
+
rmSync(full, { recursive: true, force: true });
|
|
315
|
+
} else if (st.isFile()) {
|
|
316
|
+
unlinkSync(full);
|
|
317
|
+
} else {
|
|
318
|
+
throw targetError(400, 'Unsupported path type');
|
|
319
|
+
}
|
|
320
|
+
send(res, 200, { ok: true });
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const targetFilesFsRoutes = [
|
|
325
|
+
{ method: 'POST', match: 'exact', path: '/api/target-reveal-file', handler: targetRevealFile },
|
|
326
|
+
{ method: 'POST', match: 'exact', path: '/api/target-open-terminal', handler: targetOpenTerminal },
|
|
327
|
+
{ method: 'POST', match: 'exact', path: '/api/target-resolve-path', handler: targetResolvePath },
|
|
328
|
+
{ method: 'POST', match: 'exact', path: '/api/target-create-file', handler: targetCreateFile },
|
|
329
|
+
{ method: 'POST', match: 'exact', path: '/api/target-create-dir', handler: targetCreateDir },
|
|
330
|
+
{ method: 'POST', match: 'exact', path: '/api/target-rename-file', handler: targetRenameFile },
|
|
331
|
+
{ method: 'POST', match: 'exact', path: '/api/target-delete-file', handler: targetDeleteFile },
|
|
332
|
+
];
|
package/server/server.js
CHANGED
|
@@ -32,6 +32,7 @@ import { ultraAgentsRoutes } from './routes/ultra-agents.js';
|
|
|
32
32
|
import { filesContentRoutes } from './routes/files-content.js';
|
|
33
33
|
import { workflowJournalRoutes } from './routes/workflow-journal.js';
|
|
34
34
|
import { filesFsRoutes } from './routes/files-fs.js';
|
|
35
|
+
import { targetFilesFsRoutes } from './routes/target-files-fs.js';
|
|
35
36
|
import { searchRoutes } from './routes/search.js';
|
|
36
37
|
import { workspacesRoutes } from './routes/workspaces.js';
|
|
37
38
|
import { expertRoutes } from './routes/expert.js';
|
|
@@ -51,6 +52,7 @@ import * as imCore from './lib/im/im-bridge-core.js';
|
|
|
51
52
|
import * as imProcMgr from './lib/im/im-process-manager.js';
|
|
52
53
|
import * as chatQueue from './lib/chat-queue.js';
|
|
53
54
|
import { reportSwallowed } from './lib/error-report.js';
|
|
55
|
+
import { cancelAutoApproveState } from './lib/auto-approve.js';
|
|
54
56
|
import { initScheduledTaskScheduler, startScheduledTaskScheduler, stopScheduledTaskScheduler } from './lib/scheduled-task-scheduler.js';
|
|
55
57
|
import { executeScheduledTask } from './lib/scheduled-task-executor.js';
|
|
56
58
|
import './lib/adapters/dingtalk-adapter.js'; // side-effect: registers the DingTalk adapter
|
|
@@ -293,9 +295,10 @@ const gitRestoreLocks = new Map(); // Map<absLockKey, Promise<void>>
|
|
|
293
295
|
|
|
294
296
|
// Notify the parent process (Electron main, when forked under tab-worker) about pending state changes.
|
|
295
297
|
// No-op outside Electron (process.send is undefined when run as a standalone Node server).
|
|
296
|
-
// Only ask-hook-* / sdk-ask-* are translated. Permission
|
|
297
|
-
// drive global modal / flashFrame / Notification (per UX direction).
|
|
298
|
-
// renderer and reported via window.tabBridge directly
|
|
298
|
+
// Only ask-hook-* / sdk-ask-* are translated. Permission stays inline-only and does not
|
|
299
|
+
// drive global modal / flashFrame / Notification (per UX direction). Plan approvals — PTY and
|
|
300
|
+
// SDK — are parsed in the renderer and reported via window.tabBridge directly (SDK ExitPlanMode
|
|
301
|
+
// reuses the PTY plan card/modal channel), not through this server-side hook.
|
|
299
302
|
function _notifyParentPending(msg) {
|
|
300
303
|
if (!process.send || !msg || typeof msg !== 'object' || !msg.type) return;
|
|
301
304
|
let event = null;
|
|
@@ -636,6 +639,15 @@ function getAllLocalIps() {
|
|
|
636
639
|
return ips;
|
|
637
640
|
}
|
|
638
641
|
|
|
642
|
+
// Approval/cancel message id validator — the single source for the ≤256-char +
|
|
643
|
+
// [a-zA-Z0-9_-] whitelist shared by ask-cancel / sdk-plan-auto-cancel /
|
|
644
|
+
// perm-hook-auto-cancel (keeps the three branches consistent).
|
|
645
|
+
function validateApprovalId(raw) {
|
|
646
|
+
if (raw == null) return null;
|
|
647
|
+
const id = String(raw);
|
|
648
|
+
return id.length > 0 && id.length <= 256 && /^[a-zA-Z0-9_-]+$/.test(id) ? id : null;
|
|
649
|
+
}
|
|
650
|
+
|
|
639
651
|
// ─── Route dependency bag ──────────────────────────────────────────
|
|
640
652
|
// Single dependency object handed to every per-domain route handler (server/routes/*).
|
|
641
653
|
// Built once; route bodies were moved out of handleRequest's if-chain verbatim, with
|
|
@@ -857,6 +869,7 @@ const _routes = [
|
|
|
857
869
|
...filesContentRoutes,
|
|
858
870
|
...workflowJournalRoutes,
|
|
859
871
|
...filesFsRoutes,
|
|
872
|
+
...targetFilesFsRoutes,
|
|
860
873
|
...searchRoutes,
|
|
861
874
|
...workspacesRoutes,
|
|
862
875
|
...expertRoutes,
|
|
@@ -1614,6 +1627,7 @@ function _buildPluginInteractions() {
|
|
|
1614
1627
|
const entry = pendingPermHooks.get(id);
|
|
1615
1628
|
if (!entry) return false;
|
|
1616
1629
|
clearTimeout(entry.timer);
|
|
1630
|
+
if (entry.auto?.timer) clearTimeout(entry.auto.timer);
|
|
1617
1631
|
pendingPermHooks.delete(id);
|
|
1618
1632
|
try {
|
|
1619
1633
|
if (!entry.res.headersSent) {
|
|
@@ -2161,7 +2175,14 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
2161
2175
|
// 直到超时 silent deny)。连接时重发,前端 perm 分支按 id 去重,天然幂等。
|
|
2162
2176
|
for (const [id, entry] of pendingPermHooks) {
|
|
2163
2177
|
try {
|
|
2164
|
-
ws.send(JSON.stringify({
|
|
2178
|
+
ws.send(JSON.stringify({
|
|
2179
|
+
type: 'perm-hook-pending', id, toolName: entry.toolName, input: entry.input,
|
|
2180
|
+
autoApproveAt: entry.autoCancelled ? null : (entry.auto?.autoApproveAt ?? null),
|
|
2181
|
+
// 0 = server says OFF (fork-off / npm publish floor): never let a reconnecting
|
|
2182
|
+
// client fall back to its local pref and re-arm a countdown the server disabled.
|
|
2183
|
+
autoApproveSeconds: entry.auto?.seconds ?? 0,
|
|
2184
|
+
autoCancelled: entry.autoCancelled === true,
|
|
2185
|
+
}));
|
|
2165
2186
|
} catch {}
|
|
2166
2187
|
}
|
|
2167
2188
|
|
|
@@ -2208,6 +2229,13 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
2208
2229
|
if (p.kind === 'ask') {
|
|
2209
2230
|
payload.startedAt = Date.now();
|
|
2210
2231
|
payload.timeoutMs = p.remainingMs;
|
|
2232
|
+
} else {
|
|
2233
|
+
// plan/perm: carry the server-side auto-approve deadline so a reconnected
|
|
2234
|
+
// client renders the REMAINING time (and a cancelled one never re-arms)
|
|
2235
|
+
// instead of restarting a full-length countdown.
|
|
2236
|
+
payload.autoApproveAt = p.autoApproveAt ?? null;
|
|
2237
|
+
payload.autoCancelled = p.autoCancelled === true;
|
|
2238
|
+
if (p.autoApproveSeconds != null) payload.autoApproveSeconds = p.autoApproveSeconds;
|
|
2211
2239
|
}
|
|
2212
2240
|
try { ws.send(JSON.stringify(payload)); } catch (err) { reportSwallowed('sdk.approval-replay', err); }
|
|
2213
2241
|
}
|
|
@@ -2670,6 +2698,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
2670
2698
|
if (hookEntry) {
|
|
2671
2699
|
const { res: hookRes, timer } = hookEntry;
|
|
2672
2700
|
clearTimeout(timer);
|
|
2701
|
+
if (hookEntry.auto?.timer) clearTimeout(hookEntry.auto.timer);
|
|
2673
2702
|
pendingPermHooks.delete(msg.id);
|
|
2674
2703
|
permAnswered = true;
|
|
2675
2704
|
try {
|
|
@@ -2706,7 +2735,7 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
2706
2735
|
// cancelId/Reason 校验:与 toolUseId 同套白名单(≤256 字符 + [a-zA-Z0-9_-])+ reason ≤500
|
|
2707
2736
|
// 防恶意/buggy client 塞超长 key 撑大 _pendingApprovals 或塞 1MB reason 打爆 broadcast。
|
|
2708
2737
|
const rawId = msg.id != null ? String(msg.id) : null;
|
|
2709
|
-
const cancelId =
|
|
2738
|
+
const cancelId = validateApprovalId(msg.id);
|
|
2710
2739
|
if (rawId && !cancelId) {
|
|
2711
2740
|
console.warn('[server] ask-cancel rejected: invalid id format');
|
|
2712
2741
|
return;
|
|
@@ -2782,20 +2811,63 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
2782
2811
|
}
|
|
2783
2812
|
}
|
|
2784
2813
|
} else if (msg.type === 'sdk-plan-auto-cancel') {
|
|
2785
|
-
// Cancellation stops the automatic timers on
|
|
2786
|
-
// not an approval/denial: the plan
|
|
2787
|
-
|
|
2788
|
-
|
|
2814
|
+
// Cancellation stops the automatic timers on every viewer AND the
|
|
2815
|
+
// server-side fallback timer. It is not an approval/denial: the plan
|
|
2816
|
+
// remains pending for manual review; the cancelled state is replayed
|
|
2817
|
+
// to fresh clients so they never re-arm the countdown.
|
|
2818
|
+
const rawPlanCancelId = msg.id != null ? String(msg.id) : null;
|
|
2819
|
+
const planCancelId = validateApprovalId(msg.id);
|
|
2820
|
+
if (rawPlanCancelId && !planCancelId) {
|
|
2821
|
+
console.warn('[server] sdk-plan-auto-cancel rejected: invalid id format');
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
let handled = false;
|
|
2825
|
+
if (planCancelId && _sdkAutoCancelApproval) {
|
|
2826
|
+
try { handled = _sdkAutoCancelApproval(planCancelId, 'plan') === true; } catch (err) { reportSwallowed('server.plan-auto-cancel', err); }
|
|
2827
|
+
}
|
|
2828
|
+
// Legacy relay fallback: no SDK pending matched (PTY-mode senders or a
|
|
2829
|
+
// stale id) — keep the old behavior so other clients still stop counting.
|
|
2830
|
+
if (!handled && planCancelId) broadcastWsMessage({
|
|
2831
|
+
type: 'sdk-plan-auto-cancel', id: planCancelId,
|
|
2789
2832
|
cwd: msg.cwd || null, project: msg.project || null,
|
|
2790
2833
|
instanceKey: msg.instanceKey || null,
|
|
2791
2834
|
});
|
|
2835
|
+
} else if (msg.type === 'perm-hook-auto-cancel') {
|
|
2836
|
+
// Symmetric to sdk-plan-auto-cancel for permission approvals: destroys the
|
|
2837
|
+
// server-side fallback timer (SDK pending or PTY-hook long-poll), keeps the
|
|
2838
|
+
// approval pending for a manual decision, and notifies all clients.
|
|
2839
|
+
const rawPermCancelId = msg.id != null ? String(msg.id) : null;
|
|
2840
|
+
const permCancelId = validateApprovalId(msg.id);
|
|
2841
|
+
if (rawPermCancelId && !permCancelId) {
|
|
2842
|
+
console.warn('[server] perm-hook-auto-cancel rejected: invalid id format');
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
let permCancelHandled = false;
|
|
2846
|
+
if (permCancelId && _sdkAutoCancelApproval) {
|
|
2847
|
+
try { permCancelHandled = _sdkAutoCancelApproval(permCancelId, 'perm') === true; } catch (err) { reportSwallowed('server.perm-auto-cancel', err); }
|
|
2848
|
+
}
|
|
2849
|
+
if (!permCancelHandled && permCancelId) {
|
|
2850
|
+
const hookEntry = pendingPermHooks.get(permCancelId);
|
|
2851
|
+
if (hookEntry) {
|
|
2852
|
+
cancelAutoApproveState(hookEntry);
|
|
2853
|
+
permCancelHandled = true;
|
|
2854
|
+
if (terminalWss) {
|
|
2855
|
+
const cmsg = JSON.stringify({ type: 'perm-hook-auto-cancel', id: permCancelId });
|
|
2856
|
+
terminalWss.clients.forEach((c) => {
|
|
2857
|
+
if (c.readyState === 1) try { c.send(cmsg); } catch {}
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2792
2862
|
} else if (msg.type === 'sdk-plan-answer') {
|
|
2793
2863
|
// Plan approval in SDK mode
|
|
2794
|
-
|
|
2795
|
-
|
|
2864
|
+
let planAnswered = false;
|
|
2865
|
+
if (_sdkResolveApproval && msg.id) {
|
|
2866
|
+
planAnswered = _sdkResolveApproval(msg.id, { approve: msg.approve !== false, feedback: msg.feedback || '' }) === true;
|
|
2796
2867
|
}
|
|
2797
|
-
// Broadcast resolved to other clients
|
|
2798
|
-
|
|
2868
|
+
// Broadcast resolved to other clients only when an answer was actually
|
|
2869
|
+
// processed — a stale/unknown id must not close other viewers' plan modal.
|
|
2870
|
+
if (planAnswered && terminalWss) {
|
|
2799
2871
|
const rmsg = JSON.stringify({ type: 'sdk-plan-resolved', id: msg.id });
|
|
2800
2872
|
terminalWss.clients.forEach((c) => {
|
|
2801
2873
|
if (c !== ws && c.readyState === 1) try { c.send(rmsg); } catch {}
|
|
@@ -3655,7 +3727,8 @@ export function broadcastWsMessage(msg) { if (terminalWss) {
|
|
|
3655
3727
|
try { c.send(str); } catch {}
|
|
3656
3728
|
});
|
|
3657
3729
|
}
|
|
3658
|
-
// 仅对 ask 类型转译给主进程;perm-hook-*
|
|
3730
|
+
// 仅对 ask 类型转译给主进程;perm-hook-* 维持 inline-only(红线)。sdk-plan-* 不经此 server
|
|
3731
|
+
// 侧钩子——SDK plan 在渲染端复用 PTY plan 卡片/modal 通道,由 window.tabBridge 上报。
|
|
3659
3732
|
// 显式调用 _notifyParentPending 的分支(ask-hook-resolved 等)走 ws.send 不进这里,无重复触发。
|
|
3660
3733
|
if (msg && typeof msg === 'object' && typeof msg.type === 'string'
|
|
3661
3734
|
&& (msg.type === 'sdk-ask-pending' || msg.type === 'sdk-ask-resolved' || msg.type === 'sdk-ask-timeout'
|
|
@@ -3715,6 +3788,8 @@ function _broadcastApprovalCloses(cancelled, reason) {
|
|
|
3715
3788
|
/** Reference to sdk-manager's resolveApproval (set by cli.js after import). */
|
|
3716
3789
|
let _sdkResolveApproval = null;
|
|
3717
3790
|
export function setSdkResolveApproval(fn) { _sdkResolveApproval = fn; }
|
|
3791
|
+
let _sdkAutoCancelApproval = null;
|
|
3792
|
+
export function setSdkAutoCancelApproval(fn) { _sdkAutoCancelApproval = fn; }
|
|
3718
3793
|
|
|
3719
3794
|
/** Reference to sdk-manager's cancelApproval (set by cli.js after import). */
|
|
3720
3795
|
// 与 _sdkResolveApproval 平行——但语义不同:cancelApproval 让 _waitForApproval resolve
|
|
@@ -22,6 +22,7 @@ IMPORTANT: Never generate or guess URLs unless you are confident they help the u
|
|
|
22
22
|
- Convergence budget: once you have enough evidence to answer, stop searching and answer; a partial report beats endless exploration.
|
|
23
23
|
- When a request could be read either as a question or as a change to make, treat it as a task and carry it out. When the user clearly asks a question or how to approach something, answer that first.
|
|
24
24
|
- Deliver exactly what was asked and nothing more: no unrequested CLI wrappers, configuration options, logging, progress output, or abstractions. This is very important to your performance.
|
|
25
|
+
- Do not schedule real-environment verification (deploy checks, live E2E, manual smoke runs) unless the user explicitly asks — it burns tokens for little signal.
|
|
25
26
|
- Never assume a library or framework is available — check the project's manifest or neighboring files before using it.
|
|
26
27
|
- Do not introduce security vulnerabilities (injection, XSS, path traversal, and the rest of the OWASP top 10); fix insecure code you write immediately.
|
|
27
28
|
- Iterate to green: run the relevant tests or code path, read the failure, fix the cause, and run again.
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"tr": "Kıdemli mühendis: çoklu ajan keşfi, plan incelemesi ve yüksek kaliteli uygulama — kod incelemesi, yeniden düzenleme, performans, sürdürülebilirlik.",
|
|
42
42
|
"uk": "Senior-інженер: багатоагентне дослідження, рев'ю плану та якісна реалізація — рев'ю коду, рефакторинг, продуктивність, супровід."
|
|
43
43
|
},
|
|
44
|
-
"content": "<system-reminder>\n[SCOPED INSTRUCTION] The following instructions apply only to the next 1–3 interactions. Once the task is complete, these instructions should gradually decrease in priority and no longer affect subsequent interactions. You should be adept at utilizing tools such as `AskUserQuestion`, `EnterPlanMode`, and `TeamCreate`, rather than relying solely on plain text processing. Before execution, you must ensure that the `EnterPlanMode`, `ExitPlanMode`, `SendMessage`, `TaskCreate`, `TaskUpdate`, `TaskStop`, `TaskGet`, `TaskOutput` and `TaskList` tools are loaded.\n\nPre-requisite: Use `AskUserQuestion` to clarify user intent whenever the request is ambiguous (target element, interaction style, scope of platforms, etc.). Skip only if the intent is unambiguous.\n\nLeverage a multi-agent exploration mechanism to formulate a highly detailed implementation plan.\n\nInstructions:\n1. Use the `Agent` tool to spawn parallel agents that simultaneously explore different aspects of the codebase:\n- If necessary, assign a preliminary researcher to use the `webSearch` tool to first investigate cutting-edge solutions in the relevant industry domain;\n- One agent responsible for understanding the relevant existing code and architecture;\n- One agent responsible for identifying all files that need to be modified;\n- One agent responsible for identifying potential risks, edge cases, and dependencies;\n- You may add other roles or deploy additional agents beyond the three listed above; the maximum number of concurrently dispatched agents is 5.\n\n2. Synthesize the findings from all agents into a detailed, step-by-step implementation plan.\n\n3. Use the `Agent` tool to spawn 2-3 review agents that examine the plan from different perspectives, checking for missing steps, potential risks, or corresponding mitigation strategies.\n\n4. Integrate the feedback gathered during the review process, then call `ExitPlanMode` to submit your final plan.\n\n5. Once `ExitPlanMode` returns a result:\n- If approved: proceed to execute the plan within this session.\n- If rejected: revise the plan based on the feedback provided and call `ExitPlanMode` again.\n- If an error occurs (including receiving a \"Not in Plan Mode\" message): do **not** follow the suggestions provided in the error message; instead, prompt the user for further instructions.\n\nYour final plan must include the following elements:\n- A clear summary of the implementation strategy;\n- An ordered list of files to be created or modified, with precise details of the required changes for each file;\n- A step-by-step execution sequence;\n- Testing and validation procedures;\n- Potential risks and their corresponding mitigation strategies;\n\n6. After the final plan has been successfully executed:\nFirst run `git diff --quiet && git diff --cached --quiet` (or equivalent) to detect whether the working tree actually has non-trivial changes; if there are no real changes (or only whitespace/comment-only edits), skip the UltraReview step.\nOtherwise, if the project is managed with Git:\
|
|
44
|
+
"content": "<system-reminder>\n[SCOPED INSTRUCTION] The following instructions apply only to the next 1–3 interactions. Once the task is complete, these instructions should gradually decrease in priority and no longer affect subsequent interactions. You should be adept at utilizing tools such as `AskUserQuestion`, `EnterPlanMode`, and `TeamCreate`, rather than relying solely on plain text processing. Before execution, you must ensure that the `EnterPlanMode`, `ExitPlanMode`, `SendMessage`, `TaskCreate`, `TaskUpdate`, `TaskStop`, `TaskGet`, `TaskOutput` and `TaskList` tools are loaded.\n\nPre-requisite: Use `AskUserQuestion` to clarify user intent whenever the request is ambiguous (target element, interaction style, scope of platforms, etc.). Skip only if the intent is unambiguous.\n\nLeverage a multi-agent exploration mechanism to formulate a highly detailed implementation plan.\n\nInstructions:\n1. Use the `Agent` tool to spawn parallel agents that simultaneously explore different aspects of the codebase:\n- If necessary, assign a preliminary researcher to use the `webSearch` tool to first investigate cutting-edge solutions in the relevant industry domain;\n- One agent responsible for understanding the relevant existing code and architecture;\n- One agent responsible for identifying all files that need to be modified;\n- One agent responsible for identifying potential risks, edge cases, and dependencies;\n- You may add other roles or deploy additional agents beyond the three listed above; the maximum number of concurrently dispatched agents is 5.\n\n2. Synthesize the findings from all agents into a detailed, step-by-step implementation plan.\n\n3. Use the `Agent` tool to spawn 2-3 review agents that examine the plan from different perspectives, checking for missing steps, potential risks, or corresponding mitigation strategies.\n\n4. Integrate the feedback gathered during the review process, then call `ExitPlanMode` to submit your final plan.\n\n5. Once `ExitPlanMode` returns a result:\n- If approved: proceed to execute the plan within this session.\n- If rejected: revise the plan based on the feedback provided and call `ExitPlanMode` again.\n- If an error occurs (including receiving a \"Not in Plan Mode\" message): do **not** follow the suggestions provided in the error message; instead, prompt the user for further instructions.\n\nYour final plan must include the following elements:\n- A clear summary of the implementation strategy;\n- An ordered list of files to be created or modified, with precise details of the required changes for each file;\n- A step-by-step execution sequence;\n- Testing and validation procedures;\n- Potential risks and their corresponding mitigation strategies;\n\n6. After the final plan has been successfully executed:\nFirst run `git diff --quiet && git diff --cached --quiet` (or equivalent) to detect whether the working tree actually has non-trivial changes; if there are no real changes (or only whitespace/comment-only edits), skip the UltraReview step.\nOtherwise, if the project is managed with Git:\nUse the `Agent` tool to build a team, dynamically allocating the number of teammates based on task complexity (5+ is recommended);\nNote: teammates are named (pass `name`), not plain subagents (which have no name);\nTask: Conduct a Code Review of the current git changes from multiple roles and perspectives;\nPre-requisites:\n- The git repository may be located in a subdirectory of the current directory; recursively locate the .git directory first (prefer `git rev-parse --show-toplevel`) before proceeding;\n- In the case of multiple repositories, review tasks may be executed separately per repository;\nReview dimensions (a reference framework — each role covers them as its perspective requires, aiming to be as comprehensive as possible):\n- Whether requirements/objectives have been met and functionality is complete;\n- Whether newly added code introduces side effects, breaks existing functionality, or poses potential risks;\n- Code quality: naming, readability, complexity, technical debt, maintainability;\n- Testing and documentation: whether there is adequate test coverage, and whether critical logic has necessary comments or documentation;\n- Dependencies and compatibility: whether new dependencies or version compatibility issues have been introduced;\n- Any other dimensions you see fit;\nWorkflow:\n- Each teammate, according to their own role, covers the review dimensions one by one and independently outputs a report;\n- After consolidating the reports, perform a cross-review to identify conflicts or shared concerns;\n- Distill specific, actionable modification suggestions and annotate them with priority levels (P0/P1/P2/P3);\n- Upon completion, fix all P0 and P1 items; evaluate P2 items dialectically (use `AskUserQuestion` to consult the user when any item needs a special decision);\n- After execution is complete, close the team: notify each teammate to exit individually;\n</system-reminder>"
|
|
45
45
|
}
|