@chenglu.she/sandy 1.0.3 → 1.0.5
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/README.md +18 -10
- package/package.json +3 -2
- package/src/authorize.ts +185 -0
- package/src/cli.ts +13 -2
- package/src/index.ts +5 -0
- package/src/init-guides.ts +9 -24
- package/src/init.ts +13 -12
- package/src/write-hook-policy.ts +0 -1
- package/templates/sandy.mdc +22 -0
package/README.md
CHANGED
|
@@ -31,7 +31,13 @@ mkdir -p ~/treedome && cd ~/treedome
|
|
|
31
31
|
sandy init
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
会交互式询问飞书 / Cursor / Agent
|
|
34
|
+
会交互式询问飞书 / Cursor / Agent 等项,写入 `config.yaml`;在 macOS 上接着触发磁盘授权弹窗(桌面 / 文稿 / 下载等)。人设模板见 [templates/sandy.mdc](./templates/sandy.mdc),可复制到 `.cursor/rules/`。
|
|
35
|
+
|
|
36
|
+
新机器务必在**电脑屏幕前**跑 init(不要 SSH):弹出的「node 想访问某某文件夹」全部点「允许」,并把提示里的 Node 路径加到「完全磁盘访问权限」。以后单独补授权:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
sandy authorize # 别名:sandy diskauth
|
|
40
|
+
```
|
|
35
41
|
|
|
36
42
|
必填项:
|
|
37
43
|
|
|
@@ -68,14 +74,16 @@ Agent 通过 `settingSources: ["project"]` 加载这些规则。人设、业务
|
|
|
68
74
|
|
|
69
75
|
1. **应用能力** → 开通 **机器人**
|
|
70
76
|
2. **权限**(开通后**发布新版本**才生效),至少:
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
- **单聊**:`im:message.p2p_msg:readonly`(只有群聊 @ 能回、私聊没反应,通常是缺这个)
|
|
78
|
+
- **群聊 @**:`im:message.group_at_msg:readonly`
|
|
79
|
+
- **发消息**:`im:message`
|
|
80
|
+
- 文件:上传/下载(`im:resource` 等)
|
|
81
|
+
- 文档:读/写 docx(按需)
|
|
82
|
+
3. **版本管理**:发布;**可用范围**包含你自己
|
|
83
|
+
4. **事件与回调** → 长连接(先 `sandy` 在线再保存)
|
|
84
|
+
5. 事件 `im.message.receive_v1`:点进去确认 **单聊消息**、**群聊 @ 机器人** 子开关都已开通
|
|
85
|
+
6. 回调:`card.action.trigger`
|
|
86
|
+
7. **读写已有文档**:把机器人加为文档协作者
|
|
79
87
|
|
|
80
88
|
### 5. 启动
|
|
81
89
|
|
|
@@ -84,7 +92,7 @@ cd ~/treedome
|
|
|
84
92
|
sandy
|
|
85
93
|
```
|
|
86
94
|
|
|
87
|
-
看到 `ws client ready`
|
|
95
|
+
看到 `ws client ready` 后,飞书里私聊机器人即可。远程任务卡住、回家才看到 node 访问目录的授权框,再跑一次 `sandy authorize`。
|
|
88
96
|
|
|
89
97
|
#### macOS 常驻(可选)
|
|
90
98
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chenglu.she/sandy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Feishu long-connection bot that drives Cursor agents via @cursor/sdk",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"assets",
|
|
27
27
|
"README.md",
|
|
28
28
|
"LICENSE",
|
|
29
|
-
"config.yaml.example"
|
|
29
|
+
"config.yaml.example",
|
|
30
|
+
"templates"
|
|
30
31
|
],
|
|
31
32
|
"scripts": {
|
|
32
33
|
"dev": "tsx watch src/cli.ts",
|
package/src/authorize.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
configFilePath,
|
|
7
|
+
expandPath,
|
|
8
|
+
readConfigFile,
|
|
9
|
+
workDir,
|
|
10
|
+
} from "./config-io.js";
|
|
11
|
+
|
|
12
|
+
export type AuthorizeOptions = {
|
|
13
|
+
extraDirs?: string[];
|
|
14
|
+
/** Open System Settings → Full Disk Access after probing. Default true. */
|
|
15
|
+
openSettings?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ProbeResult = {
|
|
19
|
+
dir: string;
|
|
20
|
+
status: "ok" | "missing" | "denied";
|
|
21
|
+
detail?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function nodeBinary(): string {
|
|
25
|
+
try {
|
|
26
|
+
return fs.realpathSync(process.execPath);
|
|
27
|
+
} catch {
|
|
28
|
+
return process.execPath;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isRemoteSession(): boolean {
|
|
33
|
+
return Boolean(process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extraDirsFromConfig(): string[] {
|
|
37
|
+
try {
|
|
38
|
+
const filePath = configFilePath();
|
|
39
|
+
if (!fs.existsSync(filePath)) return [workDir];
|
|
40
|
+
const file = readConfigFile(filePath);
|
|
41
|
+
const cwdRaw = file.agent?.cwd?.trim() || workDir;
|
|
42
|
+
const dirs = Array.isArray(file.agent?.dirs) ? file.agent.dirs : [];
|
|
43
|
+
return [
|
|
44
|
+
expandPath(String(cwdRaw), workDir),
|
|
45
|
+
...dirs.map((d) => expandPath(String(d), workDir)),
|
|
46
|
+
];
|
|
47
|
+
} catch {
|
|
48
|
+
return [workDir];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function listSubdirs(root: string): string[] {
|
|
53
|
+
try {
|
|
54
|
+
return fs
|
|
55
|
+
.readdirSync(root, { withFileTypes: true })
|
|
56
|
+
.filter((d) => d.isDirectory() || d.isSymbolicLink())
|
|
57
|
+
.map((d) => path.resolve(root, d.name));
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function standardTargets(home: string): string[] {
|
|
64
|
+
return [
|
|
65
|
+
home,
|
|
66
|
+
path.join(home, "Desktop"),
|
|
67
|
+
path.join(home, "Documents"),
|
|
68
|
+
path.join(home, "Downloads"),
|
|
69
|
+
path.join(home, "Pictures"),
|
|
70
|
+
path.join(home, "Movies"),
|
|
71
|
+
path.join(home, "Music"),
|
|
72
|
+
path.join(home, "Library"),
|
|
73
|
+
path.join(home, "Library", "CloudStorage"),
|
|
74
|
+
path.join(home, "Library", "Mobile Documents"),
|
|
75
|
+
path.join(home, "Library", "Mobile Documents", "com~apple~CloudDocs"),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function uniqueExistingOrder(dirs: string[]): string[] {
|
|
80
|
+
const seen = new Set<string>();
|
|
81
|
+
const out: string[] = [];
|
|
82
|
+
for (const raw of dirs) {
|
|
83
|
+
const resolved = path.resolve(raw);
|
|
84
|
+
if (seen.has(resolved)) continue;
|
|
85
|
+
seen.add(resolved);
|
|
86
|
+
out.push(resolved);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function probe(dir: string): ProbeResult {
|
|
92
|
+
try {
|
|
93
|
+
fs.accessSync(dir, fs.constants.R_OK);
|
|
94
|
+
const st = fs.statSync(dir);
|
|
95
|
+
if (st.isDirectory()) {
|
|
96
|
+
const dh = fs.opendirSync(dir);
|
|
97
|
+
dh.closeSync();
|
|
98
|
+
}
|
|
99
|
+
return { dir, status: "ok" };
|
|
100
|
+
} catch (e) {
|
|
101
|
+
const err = e as NodeJS.ErrnoException;
|
|
102
|
+
if (err.code === "ENOENT") return { dir, status: "missing" };
|
|
103
|
+
return { dir, status: "denied", detail: err.code || err.message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function displayPath(dir: string, home: string): string {
|
|
108
|
+
if (dir === home) return "~";
|
|
109
|
+
if (dir.startsWith(home + path.sep)) return "~" + dir.slice(home.length);
|
|
110
|
+
return dir;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function openFullDiskAccessSettings(): void {
|
|
114
|
+
spawn(
|
|
115
|
+
"open",
|
|
116
|
+
[
|
|
117
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles",
|
|
118
|
+
],
|
|
119
|
+
{ detached: true, stdio: "ignore" },
|
|
120
|
+
).unref();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Touch macOS TCC-protected folders so this Node binary gets Files and Folders
|
|
125
|
+
* prompts. Must be run at the Mac (GUI session); SSH will hang on unread dialogs.
|
|
126
|
+
*/
|
|
127
|
+
export function runAuthorize(options: AuthorizeOptions = {}): void {
|
|
128
|
+
if (process.platform !== "darwin") {
|
|
129
|
+
console.log("非 macOS,无需磁盘授权。");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const home = os.homedir();
|
|
134
|
+
const nodePath = nodeBinary();
|
|
135
|
+
const extra = options.extraDirs?.length ? options.extraDirs : extraDirsFromConfig();
|
|
136
|
+
const cloudRoot = path.resolve(path.join(home, "Library", "CloudStorage"));
|
|
137
|
+
const targets = uniqueExistingOrder([...standardTargets(home), ...extra]);
|
|
138
|
+
const openSettings = options.openSettings !== false;
|
|
139
|
+
|
|
140
|
+
console.log("macOS 磁盘授权");
|
|
141
|
+
console.log("会逐个访问受保护目录;弹出「node 想访问…」请全部点「允许」。");
|
|
142
|
+
console.log(`当前 Node: ${nodePath}`);
|
|
143
|
+
if (isRemoteSession()) {
|
|
144
|
+
console.log("检测到 SSH:弹窗会出现在这台 Mac 的屏幕上,没人点就会卡住。请在电脑前执行。");
|
|
145
|
+
}
|
|
146
|
+
console.log("");
|
|
147
|
+
|
|
148
|
+
const results: ProbeResult[] = [];
|
|
149
|
+
const seen = new Set(targets);
|
|
150
|
+
for (let i = 0; i < targets.length; i++) {
|
|
151
|
+
const dir = targets[i]!;
|
|
152
|
+
const label = displayPath(dir, home);
|
|
153
|
+
process.stdout.write(` ${label} … `);
|
|
154
|
+
const result = probe(dir);
|
|
155
|
+
results.push(result);
|
|
156
|
+
if (result.status === "ok") console.log("ok");
|
|
157
|
+
else if (result.status === "missing") console.log("(目录不存在,跳过)");
|
|
158
|
+
else console.log(`拒绝 (${result.detail})`);
|
|
159
|
+
|
|
160
|
+
if (result.status === "ok" && dir === cloudRoot) {
|
|
161
|
+
for (const child of listSubdirs(dir)) {
|
|
162
|
+
if (seen.has(child)) continue;
|
|
163
|
+
seen.add(child);
|
|
164
|
+
targets.push(child);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const denied = results.filter((r) => r.status === "denied");
|
|
170
|
+
const ok = results.filter((r) => r.status === "ok");
|
|
171
|
+
console.log("");
|
|
172
|
+
console.log(`完成:允许 ${ok.length},拒绝 ${denied.length},其余目录不存在。`);
|
|
173
|
+
if (denied.length) {
|
|
174
|
+
console.log("被拒绝的目录远程访问仍会卡住。再跑一次 sandy authorize,或到「文件和文件夹」里打开开关。");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log("");
|
|
178
|
+
console.log("建议再把这份 Node 加到「完全磁盘访问权限」(升级 Node 后要重新加):");
|
|
179
|
+
console.log(` ${nodePath}`);
|
|
180
|
+
|
|
181
|
+
if (openSettings) {
|
|
182
|
+
openFullDiskAccessSettings();
|
|
183
|
+
console.log("已打开系统设置 → 隐私与安全性 → 完全磁盘访问权限。");
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -3,10 +3,21 @@ const subcommand = process.argv[2];
|
|
|
3
3
|
if (subcommand === "init") {
|
|
4
4
|
const { runInit } = await import("./init.js");
|
|
5
5
|
await runInit(process.argv.slice(3));
|
|
6
|
+
} else if (subcommand === "authorize" || subcommand === "diskauth") {
|
|
7
|
+
const { runAuthorize } = await import("./authorize.js");
|
|
8
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
9
|
+
console.log(`Usage: sandy authorize
|
|
10
|
+
|
|
11
|
+
Touch macOS-protected folders so this Node binary gets Files and Folders prompts.
|
|
12
|
+
Run at the Mac (not over SSH). Alias: sandy diskauth`);
|
|
13
|
+
} else {
|
|
14
|
+
runAuthorize({ openSettings: true });
|
|
15
|
+
}
|
|
6
16
|
} else if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
7
17
|
console.log(`Usage:
|
|
8
|
-
sandy
|
|
9
|
-
sandy init
|
|
18
|
+
sandy Start the Feishu bot (reads ./config.yaml)
|
|
19
|
+
sandy init Interactive setup — writes config.yaml, then macOS disk auth
|
|
20
|
+
sandy authorize Trigger macOS folder-access prompts (alias: diskauth)`);
|
|
10
21
|
} else {
|
|
11
22
|
await import("./index.js");
|
|
12
23
|
}
|
package/src/index.ts
CHANGED
|
@@ -218,9 +218,14 @@ async function handleMessage(raw: Parameters<typeof parseIncomingMessage>[0]) {
|
|
|
218
218
|
const { text, mentionedBot } = extractText(msg, botOpenId ?? "");
|
|
219
219
|
|
|
220
220
|
if (!shouldHandleMessage(msg, mentionedBot)) {
|
|
221
|
+
console.log(
|
|
222
|
+
`[handle] ignored chat_type=${msg.chatType} message_type=${msg.messageType} mentionedBot=${mentionedBot}`,
|
|
223
|
+
);
|
|
221
224
|
return;
|
|
222
225
|
}
|
|
223
226
|
|
|
227
|
+
console.log(`[handle] chat_type=${msg.chatType} message_id=${msg.messageId}`);
|
|
228
|
+
|
|
224
229
|
const sessionKey = msg.chatId;
|
|
225
230
|
|
|
226
231
|
let attachmentPrompt: string | undefined;
|
package/src/init-guides.ts
CHANGED
|
@@ -36,10 +36,15 @@ export const INIT_FIELD_GUIDES = {
|
|
|
36
36
|
title: "飞书后台还需完成(init 不会替你操作)",
|
|
37
37
|
lines: [
|
|
38
38
|
"1. 应用能力 → 开通「机器人」",
|
|
39
|
-
"2.
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
39
|
+
"2. 权限管理(缺一不可,改完必须发布新版本):",
|
|
40
|
+
" · 读取用户发给机器人的单聊消息 im:message.p2p_msg:readonly",
|
|
41
|
+
" · 获取用户在群组中@机器人的消息 im:message.group_at_msg:readonly",
|
|
42
|
+
" · 获取与发送单聊、群组消息 im:message",
|
|
43
|
+
" · 文件 / 文档权限按需开通",
|
|
44
|
+
"3. 事件与回调 → 长连接;订阅 im.message.receive_v1",
|
|
45
|
+
" 点进该事件,确认「单聊消息」「群聊 @ 机器人」等子项均为已开通",
|
|
46
|
+
"4. 可用范围包含测试账号;先运行 sandy(ws client ready)再保存长连接",
|
|
47
|
+
"5. 群聊能回、私聊不回 → 几乎总是缺 p2p 权限或事件子开关未开",
|
|
43
48
|
],
|
|
44
49
|
links: [
|
|
45
50
|
{ label: "事件订阅(长连接)", url: "https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/event-subscription-guide/long-connection-mode" },
|
|
@@ -86,14 +91,6 @@ export const INIT_FIELD_GUIDES = {
|
|
|
86
91
|
],
|
|
87
92
|
} satisfies FieldGuide,
|
|
88
93
|
|
|
89
|
-
agentDirLinks: {
|
|
90
|
-
title: "工作目录下的 symlink(可选)",
|
|
91
|
-
lines: [
|
|
92
|
-
"若 agent.cwd 里有 symlink(如 api → ~/code/api),填链接名以便一并放行。",
|
|
93
|
-
"多个名称用英文逗号分隔;留空可跳过。",
|
|
94
|
-
],
|
|
95
|
-
} satisfies FieldGuide,
|
|
96
|
-
|
|
97
94
|
agentSandbox: {
|
|
98
95
|
title: "Cursor 本地沙箱",
|
|
99
96
|
lines: [
|
|
@@ -116,17 +113,5 @@ export const INIT_FIELD_GUIDES = {
|
|
|
116
113
|
links: [{ label: "Cursor 模型与 SDK", url: "https://cursor.com/docs/sdk/typescript" }],
|
|
117
114
|
} satisfies FieldGuide,
|
|
118
115
|
|
|
119
|
-
feishuDocsFolder: {
|
|
120
|
-
title: "飞书文档默认文件夹(可选)",
|
|
121
|
-
lines: [
|
|
122
|
-
"feishu_doc_create 创建文档时默认放到哪个云空间文件夹。",
|
|
123
|
-
"在飞书云文档打开目标文件夹,从 URL 或文件夹属性里复制 folder_token。",
|
|
124
|
-
"不需要自动归档文档可留空。",
|
|
125
|
-
],
|
|
126
|
-
links: [
|
|
127
|
-
{ label: "创建文档 API", url: "https://open.feishu.cn/document/server-docs/docs/docs/docx-v1/document/create" },
|
|
128
|
-
],
|
|
129
|
-
} satisfies FieldGuide,
|
|
130
|
-
|
|
131
116
|
feishuAppHome: FEISHU_APP_HOME,
|
|
132
117
|
} as const;
|
package/src/init.ts
CHANGED
|
@@ -100,13 +100,6 @@ export async function runInit(argv: string[] = []): Promise<void> {
|
|
|
100
100
|
);
|
|
101
101
|
cfg.agent!.dirs = dirsRaw ? splitList(dirsRaw) : [];
|
|
102
102
|
|
|
103
|
-
const linksRaw = await prompt.askWithGuide(
|
|
104
|
-
INIT_FIELD_GUIDES.agentDirLinks,
|
|
105
|
-
"symlink 名(逗号分隔,可留空)",
|
|
106
|
-
{ defaultValue: "" },
|
|
107
|
-
);
|
|
108
|
-
cfg.agent!.dirLinks = linksRaw ? splitList(linksRaw) : [];
|
|
109
|
-
|
|
110
103
|
cfg.agent!.sandbox = await prompt.askYesNoWithGuide(
|
|
111
104
|
INIT_FIELD_GUIDES.agentSandbox,
|
|
112
105
|
"开启本地沙箱?",
|
|
@@ -117,15 +110,23 @@ export async function runInit(argv: string[] = []): Promise<void> {
|
|
|
117
110
|
"模型 id",
|
|
118
111
|
{ defaultValue: "auto" },
|
|
119
112
|
);
|
|
120
|
-
cfg.feishuDocsFolder = await prompt.askWithGuide(
|
|
121
|
-
INIT_FIELD_GUIDES.feishuDocsFolder,
|
|
122
|
-
"folder_token(可留空)",
|
|
123
|
-
{ defaultValue: "" },
|
|
124
|
-
);
|
|
125
113
|
|
|
126
114
|
writeConfigFile(outPath, cfg);
|
|
127
115
|
|
|
128
116
|
console.log("\n✓ 已写入 " + outPath);
|
|
117
|
+
|
|
118
|
+
if (process.platform === "darwin") {
|
|
119
|
+
const { runAuthorize } = await import("./authorize.js");
|
|
120
|
+
prompt.section("四、macOS 磁盘授权");
|
|
121
|
+
console.log(" 弹出「node 想访问…」请全部点「允许」。远程 SSH 时弹窗在本机屏幕上。\n");
|
|
122
|
+
const extraDirs = [
|
|
123
|
+
expandPath(cfg.agent!.cwd || targetDir, targetDir),
|
|
124
|
+
...(cfg.agent!.dirs ?? []).map((d) => expandPath(d, targetDir)),
|
|
125
|
+
targetDir,
|
|
126
|
+
];
|
|
127
|
+
runAuthorize({ extraDirs, openSettings: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
129
130
|
console.log("\n下一步:");
|
|
130
131
|
console.log(` cd ${targetDir}`);
|
|
131
132
|
console.log(" sandy");
|
package/src/write-hook-policy.ts
CHANGED
|
@@ -29,7 +29,6 @@ export function writeHookPolicy(): string {
|
|
|
29
29
|
const payload = {
|
|
30
30
|
updatedAt: new Date().toISOString(),
|
|
31
31
|
allowedRoots: [...new Set(roots)],
|
|
32
|
-
protectedWriteGlobs: [".cursor/rules", ".rules"],
|
|
33
32
|
};
|
|
34
33
|
fs.writeFileSync(policyPath, JSON.stringify(payload, null, 2), "utf8");
|
|
35
34
|
return policyPath;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Sandy 松鼠 — 通用飞书 Cursor Agent 人设模板
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sandy 松鼠
|
|
7
|
+
|
|
8
|
+
你是 **Sandy 松鼠**,开发与运维助手。
|
|
9
|
+
|
|
10
|
+
## 职责
|
|
11
|
+
|
|
12
|
+
- 协助写代码、排障、运维与部署
|
|
13
|
+
- 在飞书里回答简洁、可执行;需要选择时用选择题
|
|
14
|
+
- 可在 `AGENT_CWD/.cursor/rules/` 下创建或更新规则,沉淀领域约定与协作习惯
|
|
15
|
+
- 文件修改范围以 `config.yaml` 里的 `agent.cwd` / `agent.dirs` 白名单为准
|
|
16
|
+
- 可使用本机 shell(SSH / git / 项目脚本),除非 `agent.sandbox=true`
|
|
17
|
+
|
|
18
|
+
## 风格
|
|
19
|
+
|
|
20
|
+
- 自称:Sandy 松鼠
|
|
21
|
+
- 直接、靠谱、少废话;先给结论再展开
|
|
22
|
+
- 领域约定写在同目录其它 rules 里,按需维护
|