@chenglu.she/sandy 1.0.1 → 1.0.2

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 CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  <p align="center"><b>Sandy</b> — 飞书长连接机器人。</p>
10
10
 
11
- 通用脚手架:在任意项目目录放一份 `.env` +(可选)`.cursor/rules`,装好依赖后启动即可。
11
+ 通用脚手架:在工作目录放一份 `config.yaml` +(可选)`.cursor/rules`,装好依赖后启动即可。
12
12
 
13
13
  ## 需要什么
14
14
 
@@ -22,38 +22,40 @@
22
22
 
23
23
  `npm install -g @chenglu.she/sandy`
24
24
 
25
- ### 2. 在「工作目录」准备 `.env`
25
+ ### 2. 初始化配置
26
26
 
27
- CLI **读取你启动时的 cwd** 下的 `.env`(不是包安装目录)。例如:
27
+ 在你要运行 bot 的目录执行(例如 `~/treedome`):
28
28
 
29
29
  ```bash
30
30
  mkdir -p ~/treedome && cd ~/treedome
31
- cp "$(npm root -g)/@chenglu.she/sandy/.env.example" .env
31
+ sandy init
32
32
  ```
33
33
 
34
- 必填:
34
+ 会交互式询问飞书 / Cursor / Agent 等项,并写入当前目录的 `config.yaml`。也可参考 [config.yaml.example](./config.yaml.example) 手改。
35
35
 
36
- | 变量 | 说明 |
36
+ 必填项:
37
+
38
+ | 配置项 | 说明 |
37
39
  |------|------|
38
- | `FEISHU_APP_ID` | 飞书应用 App ID |
39
- | `FEISHU_APP_SECRET` | 飞书应用 Secret |
40
- | `CURSOR_API_KEY` | Cursor API Key |
40
+ | `feishu.appId` | 飞书应用 App ID |
41
+ | `feishu.appSecret` | 飞书应用 Secret |
42
+ | `cursor.apiKey` | Cursor API Key |
41
43
 
42
- 常用可选:
44
+ 常用可选项:
43
45
 
44
- | 变量 | 默认 | 说明 |
46
+ | 配置项 | 默认 | 说明 |
45
47
  |------|------|------|
46
- | `AGENT_NAME` | `Sandy` | Agent 显示名 |
47
- | `AGENT_CWD` | 当前目录 | Agent 工作区(会加载这里的 `.cursor/rules`) |
48
- | `AGENT_DIRS` | (空) | 额外可访问目录,逗号分隔 |
49
- | `AGENT_DIR_LINKS` | (空) | `AGENT_CWD` 下要一并放行的 symlink 名 |
50
- | `AGENT_SANDBOX` | `false` | `true` 时开 Cursor 本地沙箱 |
51
- | `CURSOR_MODEL` | `auto` | 模型 id |
52
- | `FEISHU_DOCS_FOLDER` | (空) | 创建飞书文档时的默认 Drive `folder_token` |
48
+ | `agent.name` | `Sandy` | Agent 显示名 |
49
+ | `agent.cwd` | 工作目录 | Agent 工作区(会加载这里的 `.cursor/rules`) |
50
+ | `agent.dirs` | `[]` | 额外可访问目录 |
51
+ | `agent.dirLinks` | `[]` | `agent.cwd` 下要一并放行的 symlink 名 |
52
+ | `agent.sandbox` | `false` | `true` 时开 Cursor 本地沙箱 |
53
+ | `cursor.model` | `auto` | 模型 id |
54
+ | `feishuDocsFolder` | (空) | 创建飞书文档时的默认 Drive `folder_token` |
53
55
 
54
56
  ### 3. (推荐)放项目规则
55
57
 
56
- 在 `AGENT_CWD`(默认就是 cwd)下:
58
+ 在 `agent.cwd`(默认就是运行目录)下:
57
59
 
58
60
  ```text
59
61
  .cursor/rules/your-persona.mdc
@@ -68,7 +70,7 @@ Agent 通过 `settingSources: ["project"]` 加载这些规则。人设、业务
68
70
  2. **权限**(开通后**发布新版本**才生效),至少:
69
71
  - 消息:获取与发送单聊/群消息、上传图片、上传文件、下载文件(如 `im:message`、`im:resource`)
70
72
  - 文档:读正文、创建文档、编辑文档(如 `docx:document:readonly`、`docx:document` / write)
71
- - 若要把新建文档放到指定文件夹:Drive 文件夹相关权限,并填 `FEISHU_DOCS_FOLDER`
73
+ - 若要把新建文档放到指定文件夹:Drive 文件夹相关权限,并填 `feishuDocsFolder`
72
74
  3. **版本管理**:发布;可用范围包含你自己
73
75
  4. **事件与回调** → 订阅方式选 **长连接**(先让 bot 进程在线再保存)
74
76
  5. 事件:`im.message.receive_v1`
@@ -114,7 +116,7 @@ bash scripts/sandy-ctl.sh logs
114
116
 
115
117
  ```bash
116
118
  npm run dev # tsx watch
117
- npm start # 等同 feishu-cursor-bot(仍读 cwd .env
119
+ npm start # 等同 sandy(读 cwd config.yaml
118
120
  npm run typecheck
119
121
  ```
120
122
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * CLI entry: run from any directory; loads `.env` from cwd (see src/config.ts).
3
+ * CLI entry: run from any directory; loads `config.yaml` from cwd (see src/config-io.ts).
4
4
  *
5
5
  * npx feishu-cursor-bot
6
6
  * npm i -g . && feishu-cursor-bot
@@ -11,7 +11,7 @@ import { fileURLToPath } from "node:url";
11
11
  import { createRequire } from "node:module";
12
12
 
13
13
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
- const entry = path.join(packageRoot, "src", "index.ts");
14
+ const entry = path.join(packageRoot, "src", "cli.ts");
15
15
  const require = createRequire(import.meta.url);
16
16
 
17
17
  let tsxCli;
@@ -0,0 +1,13 @@
1
+ feishu:
2
+ appId: "cli_xxx"
3
+ appSecret: "xxx"
4
+ cursor:
5
+ apiKey: "crsr_xxx"
6
+ model: "auto"
7
+ agent:
8
+ name: "Sandy"
9
+ cwd: "."
10
+ dirs: []
11
+ dirLinks: []
12
+ sandbox: false
13
+ feishuDocsFolder: ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenglu.she/sandy",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Feishu long-connection bot that drives Cursor agents via @cursor/sdk",
@@ -26,11 +26,11 @@
26
26
  "assets",
27
27
  "README.md",
28
28
  "LICENSE",
29
- ".env.example"
29
+ "config.yaml.example"
30
30
  ],
31
31
  "scripts": {
32
- "dev": "tsx watch src/index.ts",
33
- "start": "tsx src/index.ts",
32
+ "dev": "tsx watch src/cli.ts",
33
+ "start": "tsx src/cli.ts",
34
34
  "typecheck": "tsc --noEmit"
35
35
  },
36
36
  "engines": {
@@ -39,8 +39,8 @@
39
39
  "dependencies": {
40
40
  "@cursor/sdk": "^1.0.0",
41
41
  "@larksuiteoapi/node-sdk": "^1.55.0",
42
- "dotenv": "^16.6.1",
43
- "tsx": "^4.20.3"
42
+ "tsx": "^4.20.3",
43
+ "yaml": "^2.8.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^22.17.0",
package/src/cli.ts ADDED
@@ -0,0 +1,12 @@
1
+ const subcommand = process.argv[2];
2
+
3
+ if (subcommand === "init") {
4
+ const { runInit } = await import("./init.js");
5
+ await runInit(process.argv.slice(3));
6
+ } else if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
7
+ console.log(`Usage:
8
+ sandy Start the Feishu bot (reads ./config.yaml)
9
+ sandy init Interactive setup — writes config.yaml`);
10
+ } else {
11
+ await import("./index.js");
12
+ }
@@ -0,0 +1,109 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
6
+ import type { SandyConfigFile } from "./config-schema.js";
7
+
8
+ export const packageRoot = path.resolve(
9
+ path.dirname(fileURLToPath(import.meta.url)),
10
+ "..",
11
+ );
12
+
13
+ /** Workspace that owns `config.yaml` — usually where you run the CLI. */
14
+ export const workDir = (() => {
15
+ const override = process.env.SANDY_CWD?.trim() || process.env.FEISHU_CURSOR_CWD?.trim();
16
+ if (!override) return process.cwd();
17
+ return expandPath(override, process.cwd());
18
+ })();
19
+
20
+ export function configFilePath(base = workDir): string {
21
+ const override = process.env.SANDY_CONFIG?.trim();
22
+ if (override) return expandPath(override, base);
23
+ return path.join(base, "config.yaml");
24
+ }
25
+
26
+ export function expandPath(raw: string, base: string = process.cwd()): string {
27
+ const trimmed = raw.trim();
28
+ if (trimmed === "~") return os.homedir();
29
+ if (trimmed.startsWith("~/")) {
30
+ return path.join(os.homedir(), trimmed.slice(2));
31
+ }
32
+ return path.isAbsolute(trimmed) ? trimmed : path.resolve(base, trimmed);
33
+ }
34
+
35
+ export function readConfigFile(filePath: string): SandyConfigFile {
36
+ if (!fs.existsSync(filePath)) {
37
+ throw new Error(
38
+ `Config not found: ${filePath}\nRun \`sandy init\` in ${workDir} to create config.yaml.`,
39
+ );
40
+ }
41
+ const raw = fs.readFileSync(filePath, "utf8");
42
+ const parsed = parseYaml(raw) as SandyConfigFile | null;
43
+ if (!parsed || typeof parsed !== "object") {
44
+ throw new Error(`Invalid config.yaml: ${filePath}`);
45
+ }
46
+ return parsed;
47
+ }
48
+
49
+ export function writeConfigFile(filePath: string, data: SandyConfigFile): void {
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ const body = stringifyYaml(data, {
52
+ lineWidth: 0,
53
+ defaultStringType: "QUOTE_DOUBLE",
54
+ });
55
+ fs.writeFileSync(filePath, body.endsWith("\n") ? body : `${body}\n`, "utf8");
56
+ }
57
+
58
+ export function parseBool(raw: unknown, defaultValue: boolean): boolean {
59
+ if (raw === undefined || raw === null || raw === "") return defaultValue;
60
+ if (typeof raw === "boolean") return raw;
61
+ const v = String(raw).trim().toLowerCase();
62
+ if (["1", "true", "yes", "on"].includes(v)) return true;
63
+ if (["0", "false", "no", "off"].includes(v)) return false;
64
+ return defaultValue;
65
+ }
66
+
67
+ function requiredField(label: string, value: string | undefined, filePath: string): string {
68
+ const trimmed = value?.trim();
69
+ if (!trimmed) {
70
+ throw new Error(
71
+ `Missing required config: ${label} (set it in ${filePath}, or run \`sandy init\`)`,
72
+ );
73
+ }
74
+ return trimmed;
75
+ }
76
+
77
+ export function loadRuntimeConfig() {
78
+ const filePath = configFilePath();
79
+ const fileConfig = readConfigFile(filePath);
80
+ const agentSection = fileConfig.agent ?? {};
81
+ const agentCwdRaw = agentSection.cwd?.trim() || workDir;
82
+ const agentDirs = Array.isArray(agentSection.dirs)
83
+ ? [...new Set(agentSection.dirs.map((p) => expandPath(String(p), workDir)).filter(Boolean))]
84
+ : [];
85
+ const agentDirLinks = Array.isArray(agentSection.dirLinks)
86
+ ? [...new Set(agentSection.dirLinks.map((s) => String(s).trim()).filter(Boolean))]
87
+ : [];
88
+
89
+ return {
90
+ packageRoot,
91
+ workDir,
92
+ configPath: filePath,
93
+ feishuAppId: requiredField("feishu.appId", fileConfig.feishu?.appId, filePath),
94
+ feishuAppSecret: requiredField("feishu.appSecret", fileConfig.feishu?.appSecret, filePath),
95
+ cursorApiKey: requiredField("cursor.apiKey", fileConfig.cursor?.apiKey, filePath),
96
+ cursorModel: fileConfig.cursor?.model?.trim() || "auto",
97
+ agentName: agentSection.name?.trim() || "Sandy",
98
+ agentCwd: expandPath(String(agentCwdRaw), workDir),
99
+ agentDirs,
100
+ agentDirLinks,
101
+ agentSandbox: parseBool(agentSection.sandbox, false),
102
+ sessionStorePath: path.resolve(workDir, ".data", "sessions.json"),
103
+ pendingStorePath: path.resolve(workDir, ".data", "pending-questions.json"),
104
+ feishuDocsFolder: fileConfig.feishuDocsFolder?.trim() || "",
105
+ inboxDir: path.join(expandPath(String(agentCwdRaw), workDir), ".data", "feishu-inbox"),
106
+ };
107
+ }
108
+
109
+ export type RuntimeConfig = ReturnType<typeof loadRuntimeConfig>;
@@ -0,0 +1,34 @@
1
+ /** On-disk config.yaml shape. */
2
+ export type SandyConfigFile = {
3
+ feishu: {
4
+ appId: string;
5
+ appSecret: string;
6
+ };
7
+ cursor: {
8
+ apiKey: string;
9
+ model?: string;
10
+ };
11
+ agent?: {
12
+ name?: string;
13
+ cwd?: string;
14
+ dirs?: string[];
15
+ dirLinks?: string[];
16
+ sandbox?: boolean;
17
+ };
18
+ feishuDocsFolder?: string;
19
+ };
20
+
21
+ export function defaultConfigFile(workDir: string): SandyConfigFile {
22
+ return {
23
+ feishu: { appId: "", appSecret: "" },
24
+ cursor: { apiKey: "", model: "auto" },
25
+ agent: {
26
+ name: "Sandy",
27
+ cwd: workDir,
28
+ dirs: [],
29
+ dirLinks: [],
30
+ sandbox: false,
31
+ },
32
+ feishuDocsFolder: "",
33
+ };
34
+ }
package/src/config.ts CHANGED
@@ -1,93 +1,16 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import dotenv from "dotenv";
1
+ import type { RuntimeConfig } from "./config-io.js";
2
+ import { loadRuntimeConfig } from "./config-io.js";
5
3
 
6
- const packageRoot = path.resolve(
7
- path.dirname(fileURLToPath(import.meta.url)),
8
- "..",
9
- );
4
+ export { expandPath, workDir } from "./config-io.js";
5
+ export type { RuntimeConfig };
10
6
 
11
- /** Workspace that owns `.env` / agent cwd — usually where you run the CLI. */
12
- export const workDir = (() => {
13
- const override = process.env.FEISHU_CURSOR_CWD?.trim();
14
- if (!override) return process.cwd();
15
- return expandPath(override, process.cwd());
16
- })();
17
-
18
- // Prefer cwd `.env`, then package-local `.env` as fallback (no override).
19
- dotenv.config({ path: path.join(workDir, ".env") });
20
- dotenv.config({ path: path.join(packageRoot, ".env") });
21
-
22
- function required(name: string): string {
23
- const value = process.env[name]?.trim();
24
- if (!value) {
25
- throw new Error(
26
- `Missing required env: ${name} (put it in ${path.join(workDir, ".env")})`,
27
- );
28
- }
29
- return value;
30
- }
31
-
32
- function expandPath(raw: string, base: string = process.cwd()): string {
33
- const trimmed = raw.trim();
34
- if (trimmed === "~") return os.homedir();
35
- if (trimmed.startsWith("~/")) {
36
- return path.join(os.homedir(), trimmed.slice(2));
37
- }
38
- return path.isAbsolute(trimmed) ? trimmed : path.resolve(base, trimmed);
39
- }
40
-
41
- function parseBool(raw: string | undefined, defaultValue: boolean): boolean {
42
- if (raw === undefined || raw.trim() === "") return defaultValue;
43
- const v = raw.trim().toLowerCase();
44
- if (["1", "true", "yes", "on"].includes(v)) return true;
45
- if (["0", "false", "no", "off"].includes(v)) return false;
46
- return defaultValue;
47
- }
48
-
49
- const agentCwdRaw = process.env.AGENT_CWD?.trim() || workDir;
50
- const agentDirsRaw = process.env.AGENT_DIRS?.trim() || "";
51
- const agentDirs = agentDirsRaw
52
- ? [...new Set(agentDirsRaw.split(",").map((p) => expandPath(p)).filter(Boolean))]
53
- : [];
54
-
55
- /** Optional symlink names under AGENT_CWD to also allow (comma-separated). */
56
- const agentDirLinksRaw = process.env.AGENT_DIR_LINKS?.trim() || "";
57
- export const agentDirLinks = agentDirLinksRaw
58
- ? [...new Set(agentDirLinksRaw.split(",").map((s) => s.trim()).filter(Boolean))]
59
- : [];
60
-
61
- export const config = {
62
- packageRoot,
63
- workDir,
64
- feishuAppId: required("FEISHU_APP_ID"),
65
- feishuAppSecret: required("FEISHU_APP_SECRET"),
66
- cursorApiKey: required("CURSOR_API_KEY"),
67
- cursorModel: process.env.CURSOR_MODEL?.trim() || "auto",
68
- /** Display name for the Cursor agent / bot persona. */
69
- agentName: process.env.AGENT_NAME?.trim() || "Sandy",
70
- /** Primary workspace root for the agent. Defaults to cwd. */
71
- agentCwd: expandPath(String(agentCwdRaw)),
72
- /** Optional extra workspace roots (multi-root). */
73
- agentDirs,
74
- agentDirLinks,
75
- /** Enable Cursor local sandbox (FS + shell + default deny network for shell). */
76
- agentSandbox: parseBool(process.env.AGENT_SANDBOX, false),
77
- sessionStorePath: path.resolve(workDir, ".data", "sessions.json"),
78
- pendingStorePath: path.resolve(workDir, ".data", "pending-questions.json"),
79
- /** Optional Drive folder_token for feishu_doc_create default placement. */
80
- feishuDocsFolder: process.env.FEISHU_DOCS_FOLDER?.trim() || "",
81
- /** Where incoming Feishu files are saved (under AGENT_CWD so the agent can read them). */
82
- inboxDir: path.join(expandPath(String(agentCwdRaw)), ".data", "feishu-inbox"),
83
- };
7
+ export const config: RuntimeConfig = loadRuntimeConfig();
84
8
 
85
9
  export function localAgentOptions() {
86
10
  return {
87
11
  cwd: config.agentCwd,
88
12
  ...(config.agentDirs.length > 0 ? { dirs: config.agentDirs } : {}),
89
13
  ...(config.agentSandbox ? { sandboxOptions: { enabled: true as const } } : {}),
90
- // Load AGENTS.md / .cursor/rules from AGENT_CWD.
91
14
  settingSources: ["project" as const],
92
15
  };
93
16
  }
package/src/index.ts CHANGED
@@ -298,6 +298,7 @@ async function handleCardAction(data: unknown) {
298
298
 
299
299
  async function main() {
300
300
  console.log("[boot] feishu-cursor-bot starting");
301
+ console.log(`[boot] config=${config.configPath}`);
301
302
  console.log(`[boot] agent cwd=${config.agentCwd}`);
302
303
  console.log(`[boot] agent dirs=${config.agentDirs.join(", ") || "(none)"}`);
303
304
  console.log(`[boot] sandbox=${config.agentSandbox}`);
package/src/init.ts ADDED
@@ -0,0 +1,89 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ configFilePath,
5
+ expandPath,
6
+ workDir,
7
+ writeConfigFile,
8
+ } from "./config-io.js";
9
+ import { defaultConfigFile, type SandyConfigFile } from "./config-schema.js";
10
+ import { createPrompter } from "./prompt.js";
11
+
12
+ function parseArgs(argv: string[]): { targetDir: string } {
13
+ let targetDir = workDir;
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const arg = argv[i];
16
+ if (arg === "--dir" || arg === "-C") {
17
+ const next = argv[i + 1];
18
+ if (!next) throw new Error(`${arg} requires a directory path`);
19
+ targetDir = expandPath(next, process.cwd());
20
+ i++;
21
+ } else if (arg === "--help" || arg === "-h") {
22
+ console.log(`Usage: sandy init [--dir <path>]
23
+
24
+ Create config.yaml interactively in the target directory (default: current cwd).`);
25
+ process.exit(0);
26
+ } else {
27
+ throw new Error(`Unknown argument: ${arg}`);
28
+ }
29
+ }
30
+ return { targetDir };
31
+ }
32
+
33
+ function splitList(raw: string): string[] {
34
+ return raw
35
+ .split(",")
36
+ .map((s) => s.trim())
37
+ .filter(Boolean);
38
+ }
39
+
40
+ export async function runInit(argv: string[] = []): Promise<void> {
41
+ const { targetDir } = parseArgs(argv);
42
+ const outPath = configFilePath(targetDir);
43
+ const prompt = createPrompter();
44
+
45
+ try {
46
+ console.log("Sandy 初始化 — 逐步生成 config.yaml\n");
47
+ console.log(`目标目录: ${targetDir}`);
48
+ console.log(`配置文件: ${outPath}\n`);
49
+
50
+ if (fs.existsSync(outPath)) {
51
+ const overwrite = await prompt.askYesNo("已存在 config.yaml,是否覆盖?", false);
52
+ if (!overwrite) {
53
+ console.log("已取消。");
54
+ return;
55
+ }
56
+ }
57
+
58
+ const cfg: SandyConfigFile = defaultConfigFile(targetDir);
59
+
60
+ console.log("【必填】飞书与 Cursor");
61
+ cfg.feishu.appId = await prompt.askRequired("飞书 App ID (FEISHU_APP_ID)");
62
+ cfg.feishu.appSecret = await prompt.askRequired("飞书 App Secret");
63
+ cfg.cursor.apiKey = await prompt.askRequired("Cursor API Key");
64
+
65
+ console.log("\n【可选】Agent 与工作区");
66
+ cfg.agent!.name = await prompt.ask("Agent 显示名", "Sandy");
67
+ cfg.agent!.cwd = await prompt.ask("Agent 工作目录 (AGENT_CWD)", targetDir);
68
+
69
+ const dirsRaw = await prompt.ask("额外可访问目录,逗号分隔 (AGENT_DIRS,可留空)", "");
70
+ cfg.agent!.dirs = dirsRaw ? splitList(dirsRaw) : [];
71
+
72
+ const linksRaw = await prompt.ask(
73
+ "AGENT_CWD 下要放行的 symlink 名,逗号分隔 (可留空)",
74
+ "",
75
+ );
76
+ cfg.agent!.dirLinks = linksRaw ? splitList(linksRaw) : [];
77
+
78
+ cfg.agent!.sandbox = await prompt.askYesNo("开启 Cursor 本地沙箱 (AGENT_SANDBOX)?", false);
79
+ cfg.cursor.model = await prompt.ask("Cursor 模型 id", "auto");
80
+ cfg.feishuDocsFolder = await prompt.ask("飞书文档默认 folder_token (可留空)", "");
81
+
82
+ writeConfigFile(outPath, cfg);
83
+
84
+ console.log(`\n已写入 ${outPath}`);
85
+ console.log(`下一步: cd ${targetDir} && sandy`);
86
+ } finally {
87
+ prompt.close();
88
+ }
89
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,48 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+
4
+ export type Prompter = {
5
+ ask: (label: string, defaultValue?: string) => Promise<string>;
6
+ askRequired: (label: string) => Promise<string>;
7
+ askYesNo: (label: string, defaultValue?: boolean) => Promise<boolean>;
8
+ close: () => void;
9
+ };
10
+
11
+ export function createPrompter(): Prompter {
12
+ const rl = readline.createInterface({ input, output });
13
+
14
+ async function ask(label: string, defaultValue?: string): Promise<string> {
15
+ const hint =
16
+ defaultValue !== undefined && defaultValue !== ""
17
+ ? ` (${defaultValue})`
18
+ : "";
19
+ const answer = (await rl.question(`${label}${hint}: `)).trim();
20
+ if (!answer && defaultValue !== undefined) return defaultValue;
21
+ return answer;
22
+ }
23
+
24
+ async function askRequired(label: string): Promise<string> {
25
+ while (true) {
26
+ const answer = (await rl.question(`${label}: `)).trim();
27
+ if (answer) return answer;
28
+ console.log("此项必填,请重新输入。");
29
+ }
30
+ }
31
+
32
+ async function askYesNo(label: string, defaultValue = false): Promise<boolean> {
33
+ const hint = defaultValue ? "Y/n" : "y/N";
34
+ const answer = (await rl.question(`${label} [${hint}]: `)).trim().toLowerCase();
35
+ if (!answer) return defaultValue;
36
+ if (["y", "yes", "是", "1", "true"].includes(answer)) return true;
37
+ if (["n", "no", "否", "0", "false"].includes(answer)) return false;
38
+ console.log("请输入 y 或 n。");
39
+ return askYesNo(label, defaultValue);
40
+ }
41
+
42
+ return {
43
+ ask,
44
+ askRequired,
45
+ askYesNo,
46
+ close: () => rl.close(),
47
+ };
48
+ }
package/.env.example DELETED
@@ -1,24 +0,0 @@
1
- FEISHU_APP_ID=cli_xxx
2
- FEISHU_APP_SECRET=xxx
3
- CURSOR_API_KEY=crsr_xxx
4
-
5
- # Agent display name (shown in Cursor / logs)
6
- AGENT_NAME=Sandy
7
-
8
- # Workspace the agent works in (defaults to current working directory)
9
- # AGENT_CWD=.
10
-
11
- # Extra roots the agent may use, comma-separated
12
- # AGENT_DIRS=~/code/api,~/code/web
13
-
14
- # Symlink names under AGENT_CWD whose targets should also be allowlisted
15
- # AGENT_DIR_LINKS=api,web
16
-
17
- # false ≈ full local shell (SSH / git / deploy). true = Cursor sandbox.
18
- AGENT_SANDBOX=false
19
-
20
- # Model id for @cursor/sdk
21
- CURSOR_MODEL=auto
22
-
23
- # Optional Drive folder_token for feishu_doc_create default placement
24
- # FEISHU_DOCS_FOLDER=