@chenglu.she/sandy 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/package.json +3 -2
- package/src/index.ts +5 -0
- package/src/init-guides.ts +137 -0
- package/src/init.ts +67 -20
- package/src/prompt.ts +60 -3
- package/src/write-hook-policy.ts +0 -1
- package/templates/sandy.mdc +22 -0
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ mkdir -p ~/treedome && cd ~/treedome
|
|
|
31
31
|
sandy init
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
会交互式询问飞书 / Cursor / Agent 等项,并写入当前目录的 `config.yaml
|
|
34
|
+
会交互式询问飞书 / Cursor / Agent 等项,并写入当前目录的 `config.yaml`。人设模板见 [templates/sandy.mdc](./templates/sandy.mdc),可复制到 `.cursor/rules/`。
|
|
35
35
|
|
|
36
36
|
必填项:
|
|
37
37
|
|
|
@@ -68,14 +68,16 @@ Agent 通过 `settingSources: ["project"]` 加载这些规则。人设、业务
|
|
|
68
68
|
|
|
69
69
|
1. **应用能力** → 开通 **机器人**
|
|
70
70
|
2. **权限**(开通后**发布新版本**才生效),至少:
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
- **单聊**:`im:message.p2p_msg:readonly`(只有群聊 @ 能回、私聊没反应,通常是缺这个)
|
|
72
|
+
- **群聊 @**:`im:message.group_at_msg:readonly`
|
|
73
|
+
- **发消息**:`im:message`
|
|
74
|
+
- 文件:上传/下载(`im:resource` 等)
|
|
75
|
+
- 文档:读/写 docx(按需)
|
|
76
|
+
3. **版本管理**:发布;**可用范围**包含你自己
|
|
77
|
+
4. **事件与回调** → 长连接(先 `sandy` 在线再保存)
|
|
78
|
+
5. 事件 `im.message.receive_v1`:点进去确认 **单聊消息**、**群聊 @ 机器人** 子开关都已开通
|
|
79
|
+
6. 回调:`card.action.trigger`
|
|
80
|
+
7. **读写已有文档**:把机器人加为文档协作者
|
|
79
81
|
|
|
80
82
|
### 5. 启动
|
|
81
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chenglu.she/sandy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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/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;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { FieldGuide } from "./prompt.js";
|
|
2
|
+
|
|
3
|
+
const FEISHU_APP_HOME: FieldGuide = {
|
|
4
|
+
title: "飞书开放平台",
|
|
5
|
+
lines: ["企业自建应用的入口,下面几项都在同一个应用里配置。"],
|
|
6
|
+
links: [{ label: "打开飞书开发者后台", url: "https://open.feishu.cn/app" }],
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const INIT_FIELD_GUIDES = {
|
|
10
|
+
feishuAppId: {
|
|
11
|
+
title: "飞书 App ID",
|
|
12
|
+
lines: [
|
|
13
|
+
"在飞书开发者后台创建「企业自建应用」后获取。",
|
|
14
|
+
"路径:你的应用 → 凭证与基础信息 → App ID",
|
|
15
|
+
"格式通常以 cli_ 开头。",
|
|
16
|
+
],
|
|
17
|
+
links: [
|
|
18
|
+
{ label: "飞书开发者后台", url: "https://open.feishu.cn/app" },
|
|
19
|
+
{
|
|
20
|
+
label: "创建应用说明",
|
|
21
|
+
url: "https://open.feishu.cn/document/home/introduction-to-custom-app-development/self-built-application-development-process",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
} satisfies FieldGuide,
|
|
25
|
+
|
|
26
|
+
feishuAppSecret: {
|
|
27
|
+
title: "飞书 App Secret",
|
|
28
|
+
lines: [
|
|
29
|
+
"与 App ID 在同一页:凭证与基础信息 → App Secret",
|
|
30
|
+
"点击「显示」或「重置」后复制;勿泄露或提交到 git。",
|
|
31
|
+
],
|
|
32
|
+
links: [{ label: "飞书开发者后台", url: "https://open.feishu.cn/app" }],
|
|
33
|
+
} satisfies FieldGuide,
|
|
34
|
+
|
|
35
|
+
feishuSetupHint: {
|
|
36
|
+
title: "飞书后台还需完成(init 不会替你操作)",
|
|
37
|
+
lines: [
|
|
38
|
+
"1. 应用能力 → 开通「机器人」",
|
|
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 权限或事件子开关未开",
|
|
48
|
+
],
|
|
49
|
+
links: [
|
|
50
|
+
{ label: "事件订阅(长连接)", url: "https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/event-subscription-guide/long-connection-mode" },
|
|
51
|
+
{ label: "机器人能力说明", url: "https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-enable-bot-ability" },
|
|
52
|
+
],
|
|
53
|
+
} satisfies FieldGuide,
|
|
54
|
+
|
|
55
|
+
cursorApiKey: {
|
|
56
|
+
title: "Cursor API Key",
|
|
57
|
+
lines: [
|
|
58
|
+
"在 Cursor 账号设置里创建,用于 @cursor/sdk 调用本地 Agent。",
|
|
59
|
+
"路径:Cursor Settings → Integrations / API Keys(或 Dashboard 中的 API Keys)",
|
|
60
|
+
"格式通常以 crsr_ 开头。",
|
|
61
|
+
],
|
|
62
|
+
links: [
|
|
63
|
+
{ label: "Cursor 设置", url: "https://cursor.com/settings" },
|
|
64
|
+
{ label: "Cursor SDK 文档", url: "https://cursor.com/docs/sdk/typescript" },
|
|
65
|
+
],
|
|
66
|
+
} satisfies FieldGuide,
|
|
67
|
+
|
|
68
|
+
agentName: {
|
|
69
|
+
title: "Agent 显示名",
|
|
70
|
+
lines: [
|
|
71
|
+
"仅影响日志与 Cursor 侧 Agent 标题,飞书里仍显示机器人在后台设置的名称。",
|
|
72
|
+
"直接回车使用默认 Sandy 即可。",
|
|
73
|
+
],
|
|
74
|
+
} satisfies FieldGuide,
|
|
75
|
+
|
|
76
|
+
agentCwd: {
|
|
77
|
+
title: "Agent 工作目录",
|
|
78
|
+
lines: [
|
|
79
|
+
"Agent 读写代码、加载 .cursor/rules 的根目录。",
|
|
80
|
+
"建议设为你要放 config.yaml 的目录(例如 ~/treedome)。",
|
|
81
|
+
"支持 ~ 与绝对路径;相对路径相对于运行 sandy 时的 cwd。",
|
|
82
|
+
],
|
|
83
|
+
} satisfies FieldGuide,
|
|
84
|
+
|
|
85
|
+
agentDirs: {
|
|
86
|
+
title: "额外可访问目录(可选)",
|
|
87
|
+
lines: [
|
|
88
|
+
"除工作目录外,Agent 还可以访问哪些路径(多仓库 / 多项目时用)。",
|
|
89
|
+
"多个目录用英文逗号分隔,例如:~/code/api,~/code/web",
|
|
90
|
+
"留空表示仅使用上面的工作目录。",
|
|
91
|
+
],
|
|
92
|
+
} satisfies FieldGuide,
|
|
93
|
+
|
|
94
|
+
agentDirLinks: {
|
|
95
|
+
title: "工作目录下的 symlink(可选)",
|
|
96
|
+
lines: [
|
|
97
|
+
"若 agent.cwd 里有 symlink(如 api → ~/code/api),填链接名以便一并放行。",
|
|
98
|
+
"多个名称用英文逗号分隔;留空可跳过。",
|
|
99
|
+
],
|
|
100
|
+
} satisfies FieldGuide,
|
|
101
|
+
|
|
102
|
+
agentSandbox: {
|
|
103
|
+
title: "Cursor 本地沙箱",
|
|
104
|
+
lines: [
|
|
105
|
+
"开启后 Agent 的 shell/文件操作走 Cursor 沙箱,默认禁止随意访问网络。",
|
|
106
|
+
"需要 SSH、git push、deploy 等本机能力时选「否」。",
|
|
107
|
+
"一般个人机器上建议关闭(默认)。",
|
|
108
|
+
],
|
|
109
|
+
links: [
|
|
110
|
+
{ label: "SDK 沙箱说明", url: "https://cursor.com/docs/sdk/typescript#sandbox-options" },
|
|
111
|
+
],
|
|
112
|
+
} satisfies FieldGuide,
|
|
113
|
+
|
|
114
|
+
cursorModel: {
|
|
115
|
+
title: "Cursor 模型",
|
|
116
|
+
lines: [
|
|
117
|
+
"传给 @cursor/sdk 的 model id。",
|
|
118
|
+
"auto 表示由 Cursor 自动选择;也可填具体模型名。",
|
|
119
|
+
"直接回车使用 auto。",
|
|
120
|
+
],
|
|
121
|
+
links: [{ label: "Cursor 模型与 SDK", url: "https://cursor.com/docs/sdk/typescript" }],
|
|
122
|
+
} satisfies FieldGuide,
|
|
123
|
+
|
|
124
|
+
feishuDocsFolder: {
|
|
125
|
+
title: "飞书文档默认文件夹(可选)",
|
|
126
|
+
lines: [
|
|
127
|
+
"feishu_doc_create 创建文档时默认放到哪个云空间文件夹。",
|
|
128
|
+
"在飞书云文档打开目标文件夹,从 URL 或文件夹属性里复制 folder_token。",
|
|
129
|
+
"不需要自动归档文档可留空。",
|
|
130
|
+
],
|
|
131
|
+
links: [
|
|
132
|
+
{ label: "创建文档 API", url: "https://open.feishu.cn/document/server-docs/docs/docs/docx-v1/document/create" },
|
|
133
|
+
],
|
|
134
|
+
} satisfies FieldGuide,
|
|
135
|
+
|
|
136
|
+
feishuAppHome: FEISHU_APP_HOME,
|
|
137
|
+
} as const;
|
package/src/init.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import {
|
|
4
3
|
configFilePath,
|
|
5
4
|
expandPath,
|
|
@@ -7,6 +6,7 @@ import {
|
|
|
7
6
|
writeConfigFile,
|
|
8
7
|
} from "./config-io.js";
|
|
9
8
|
import { defaultConfigFile, type SandyConfigFile } from "./config-schema.js";
|
|
9
|
+
import { INIT_FIELD_GUIDES } from "./init-guides.js";
|
|
10
10
|
import { createPrompter } from "./prompt.js";
|
|
11
11
|
|
|
12
12
|
function parseArgs(argv: string[]): { targetDir: string } {
|
|
@@ -43,12 +43,13 @@ export async function runInit(argv: string[] = []): Promise<void> {
|
|
|
43
43
|
const prompt = createPrompter();
|
|
44
44
|
|
|
45
45
|
try {
|
|
46
|
-
console.log("Sandy 初始化
|
|
46
|
+
console.log("Sandy 初始化");
|
|
47
|
+
console.log("会逐步说明每项如何获取,并写入 config.yaml。\n");
|
|
47
48
|
console.log(`目标目录: ${targetDir}`);
|
|
48
|
-
console.log(`配置文件: ${outPath}
|
|
49
|
+
console.log(`配置文件: ${outPath}`);
|
|
49
50
|
|
|
50
51
|
if (fs.existsSync(outPath)) {
|
|
51
|
-
const overwrite = await prompt.askYesNo("已存在 config.yaml,是否覆盖?", false);
|
|
52
|
+
const overwrite = await prompt.askYesNo("\n已存在 config.yaml,是否覆盖?", false);
|
|
52
53
|
if (!overwrite) {
|
|
53
54
|
console.log("已取消。");
|
|
54
55
|
return;
|
|
@@ -57,32 +58,78 @@ export async function runInit(argv: string[] = []): Promise<void> {
|
|
|
57
58
|
|
|
58
59
|
const cfg: SandyConfigFile = defaultConfigFile(targetDir);
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
cfg.feishu.appSecret = await prompt.askRequired("飞书 App Secret");
|
|
63
|
-
cfg.cursor.apiKey = await prompt.askRequired("Cursor API Key");
|
|
61
|
+
prompt.section("一、飞书应用");
|
|
62
|
+
prompt.guide(INIT_FIELD_GUIDES.feishuAppHome);
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
cfg.feishu.appId = await prompt.askWithGuide(
|
|
65
|
+
INIT_FIELD_GUIDES.feishuAppId,
|
|
66
|
+
"App ID",
|
|
67
|
+
{ required: true },
|
|
68
|
+
);
|
|
69
|
+
cfg.feishu.appSecret = await prompt.askWithGuide(
|
|
70
|
+
INIT_FIELD_GUIDES.feishuAppSecret,
|
|
71
|
+
"App Secret",
|
|
72
|
+
{ required: true },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
prompt.guide(INIT_FIELD_GUIDES.feishuSetupHint);
|
|
76
|
+
|
|
77
|
+
prompt.section("二、Cursor");
|
|
78
|
+
cfg.cursor.apiKey = await prompt.askWithGuide(
|
|
79
|
+
INIT_FIELD_GUIDES.cursorApiKey,
|
|
80
|
+
"API Key",
|
|
81
|
+
{ required: true },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
prompt.section("三、Agent 与工作区(可一路回车用默认)");
|
|
85
|
+
cfg.agent!.name = await prompt.askWithGuide(
|
|
86
|
+
INIT_FIELD_GUIDES.agentName,
|
|
87
|
+
"显示名",
|
|
88
|
+
{ defaultValue: "Sandy" },
|
|
89
|
+
);
|
|
90
|
+
cfg.agent!.cwd = await prompt.askWithGuide(
|
|
91
|
+
INIT_FIELD_GUIDES.agentCwd,
|
|
92
|
+
"工作目录",
|
|
93
|
+
{ defaultValue: targetDir },
|
|
94
|
+
);
|
|
68
95
|
|
|
69
|
-
const dirsRaw = await prompt.
|
|
96
|
+
const dirsRaw = await prompt.askWithGuide(
|
|
97
|
+
INIT_FIELD_GUIDES.agentDirs,
|
|
98
|
+
"额外目录(逗号分隔,可留空)",
|
|
99
|
+
{ defaultValue: "" },
|
|
100
|
+
);
|
|
70
101
|
cfg.agent!.dirs = dirsRaw ? splitList(dirsRaw) : [];
|
|
71
102
|
|
|
72
|
-
const linksRaw = await prompt.
|
|
73
|
-
|
|
74
|
-
"",
|
|
103
|
+
const linksRaw = await prompt.askWithGuide(
|
|
104
|
+
INIT_FIELD_GUIDES.agentDirLinks,
|
|
105
|
+
"symlink 名(逗号分隔,可留空)",
|
|
106
|
+
{ defaultValue: "" },
|
|
75
107
|
);
|
|
76
108
|
cfg.agent!.dirLinks = linksRaw ? splitList(linksRaw) : [];
|
|
77
109
|
|
|
78
|
-
cfg.agent!.sandbox = await prompt.
|
|
79
|
-
|
|
80
|
-
|
|
110
|
+
cfg.agent!.sandbox = await prompt.askYesNoWithGuide(
|
|
111
|
+
INIT_FIELD_GUIDES.agentSandbox,
|
|
112
|
+
"开启本地沙箱?",
|
|
113
|
+
false,
|
|
114
|
+
);
|
|
115
|
+
cfg.cursor.model = await prompt.askWithGuide(
|
|
116
|
+
INIT_FIELD_GUIDES.cursorModel,
|
|
117
|
+
"模型 id",
|
|
118
|
+
{ defaultValue: "auto" },
|
|
119
|
+
);
|
|
120
|
+
cfg.feishuDocsFolder = await prompt.askWithGuide(
|
|
121
|
+
INIT_FIELD_GUIDES.feishuDocsFolder,
|
|
122
|
+
"folder_token(可留空)",
|
|
123
|
+
{ defaultValue: "" },
|
|
124
|
+
);
|
|
81
125
|
|
|
82
126
|
writeConfigFile(outPath, cfg);
|
|
83
127
|
|
|
84
|
-
console.log(
|
|
85
|
-
console.log(
|
|
128
|
+
console.log("\n✓ 已写入 " + outPath);
|
|
129
|
+
console.log("\n下一步:");
|
|
130
|
+
console.log(` cd ${targetDir}`);
|
|
131
|
+
console.log(" sandy");
|
|
132
|
+
console.log("\n飞书后台若尚未配置长连接与权限,请按上文「飞书后台还需完成」逐项检查。");
|
|
86
133
|
} finally {
|
|
87
134
|
prompt.close();
|
|
88
135
|
}
|
package/src/prompt.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import readline from "node:readline/promises";
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
3
|
|
|
4
|
+
export type GuideLink = {
|
|
5
|
+
label: string;
|
|
6
|
+
url: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type FieldGuide = {
|
|
10
|
+
title: string;
|
|
11
|
+
lines: string[];
|
|
12
|
+
links?: GuideLink[];
|
|
13
|
+
};
|
|
14
|
+
|
|
4
15
|
export type Prompter = {
|
|
16
|
+
section: (title: string) => void;
|
|
17
|
+
guide: (guide: FieldGuide) => void;
|
|
5
18
|
ask: (label: string, defaultValue?: string) => Promise<string>;
|
|
6
19
|
askRequired: (label: string) => Promise<string>;
|
|
20
|
+
askWithGuide: (guide: FieldGuide, label: string, options?: { required?: boolean; defaultValue?: string }) => Promise<string>;
|
|
21
|
+
askYesNoWithGuide: (guide: FieldGuide, label: string, defaultValue?: boolean) => Promise<boolean>;
|
|
7
22
|
askYesNo: (label: string, defaultValue?: boolean) => Promise<boolean>;
|
|
8
23
|
close: () => void;
|
|
9
24
|
};
|
|
@@ -11,10 +26,27 @@ export type Prompter = {
|
|
|
11
26
|
export function createPrompter(): Prompter {
|
|
12
27
|
const rl = readline.createInterface({ input, output });
|
|
13
28
|
|
|
29
|
+
function section(title: string): void {
|
|
30
|
+
console.log(`\n${title}`);
|
|
31
|
+
console.log("─".repeat(Math.min(title.length + 4, 60)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function guide(g: FieldGuide): void {
|
|
35
|
+
console.log(`\n▸ ${g.title}`);
|
|
36
|
+
for (const line of g.lines) {
|
|
37
|
+
console.log(` ${line}`);
|
|
38
|
+
}
|
|
39
|
+
if (g.links?.length) {
|
|
40
|
+
for (const link of g.links) {
|
|
41
|
+
console.log(` → ${link.label}: ${link.url}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
14
46
|
async function ask(label: string, defaultValue?: string): Promise<string> {
|
|
15
47
|
const hint =
|
|
16
48
|
defaultValue !== undefined && defaultValue !== ""
|
|
17
|
-
? `
|
|
49
|
+
? ` [默认: ${defaultValue}]`
|
|
18
50
|
: "";
|
|
19
51
|
const answer = (await rl.question(`${label}${hint}: `)).trim();
|
|
20
52
|
if (!answer && defaultValue !== undefined) return defaultValue;
|
|
@@ -25,8 +57,20 @@ export function createPrompter(): Prompter {
|
|
|
25
57
|
while (true) {
|
|
26
58
|
const answer = (await rl.question(`${label}: `)).trim();
|
|
27
59
|
if (answer) return answer;
|
|
28
|
-
console.log("此项必填,请重新输入。");
|
|
60
|
+
console.log(" 此项必填,请重新输入。");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function askWithGuide(
|
|
65
|
+
g: FieldGuide,
|
|
66
|
+
label: string,
|
|
67
|
+
options?: { required?: boolean; defaultValue?: string },
|
|
68
|
+
): Promise<string> {
|
|
69
|
+
guide(g);
|
|
70
|
+
if (options?.required) {
|
|
71
|
+
return askRequired(label);
|
|
29
72
|
}
|
|
73
|
+
return ask(label, options?.defaultValue);
|
|
30
74
|
}
|
|
31
75
|
|
|
32
76
|
async function askYesNo(label: string, defaultValue = false): Promise<boolean> {
|
|
@@ -35,13 +79,26 @@ export function createPrompter(): Prompter {
|
|
|
35
79
|
if (!answer) return defaultValue;
|
|
36
80
|
if (["y", "yes", "是", "1", "true"].includes(answer)) return true;
|
|
37
81
|
if (["n", "no", "否", "0", "false"].includes(answer)) return false;
|
|
38
|
-
console.log("请输入 y 或 n。");
|
|
82
|
+
console.log(" 请输入 y 或 n。");
|
|
83
|
+
return askYesNo(label, defaultValue);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function askYesNoWithGuide(
|
|
87
|
+
g: FieldGuide,
|
|
88
|
+
label: string,
|
|
89
|
+
defaultValue = false,
|
|
90
|
+
): Promise<boolean> {
|
|
91
|
+
guide(g);
|
|
39
92
|
return askYesNo(label, defaultValue);
|
|
40
93
|
}
|
|
41
94
|
|
|
42
95
|
return {
|
|
96
|
+
section,
|
|
97
|
+
guide,
|
|
43
98
|
ask,
|
|
44
99
|
askRequired,
|
|
100
|
+
askWithGuide,
|
|
101
|
+
askYesNoWithGuide,
|
|
45
102
|
askYesNo,
|
|
46
103
|
close: () => rl.close(),
|
|
47
104
|
};
|
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 里,按需维护
|