agentquad 0.3.0
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/LICENSE +21 -0
- package/README.md +318 -0
- package/dist-web/assets/index-CMaXwixo.js +1234 -0
- package/dist-web/assets/index-DBHApzV1.css +32 -0
- package/dist-web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/dist-web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/dist-web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/dist-web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
- package/dist-web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/dist-web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/dist-web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/dist-web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/dist-web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/dist-web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/dist-web/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
- package/dist-web/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/dist-web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/dist-web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/dist-web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
- package/dist-web/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
- package/dist-web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dist-web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dist-web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dist-web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dist-web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dist-web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dist-web/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dist-web/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/dist-web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/dist-web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/dist-web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
- package/dist-web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/dist-web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/dist-web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/dist-web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
- package/dist-web/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/dist-web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist-web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/dist-web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist-web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/dist-web/assets/logo-D4DDtU-r.png +0 -0
- package/dist-web/favicon.png +0 -0
- package/dist-web/index.html +14 -0
- package/package.json +88 -0
- package/src/ask-user-buttons.js +142 -0
- package/src/claude-transcript.js +203 -0
- package/src/cli.js +1040 -0
- package/src/codex-event-emitter.js +111 -0
- package/src/codex-prompt-detector.js +53 -0
- package/src/codex-sidecar.js +52 -0
- package/src/codex-transcript.js +74 -0
- package/src/config.js +692 -0
- package/src/data/claude-code-commands.json +52 -0
- package/src/db.js +1503 -0
- package/src/dispatch.js +13 -0
- package/src/export/todoMarkdown.js +246 -0
- package/src/first-run-wizard.js +82 -0
- package/src/git/gitStatus.js +139 -0
- package/src/lark-api-client.js +205 -0
- package/src/lark-bot.js +510 -0
- package/src/lark-card.js +88 -0
- package/src/lark-config-service.js +16 -0
- package/src/lark-event-client.js +107 -0
- package/src/lark-image.js +99 -0
- package/src/lark-markdown.js +51 -0
- package/src/lark-video.js +163 -0
- package/src/mcp/audit.js +34 -0
- package/src/mcp/server.js +83 -0
- package/src/mcp/tools/destructive/index.js +252 -0
- package/src/mcp/tools/openclaw/index.js +405 -0
- package/src/mcp/tools/read/index.js +269 -0
- package/src/mcp/tools/write/index.js +157 -0
- package/src/openclaw-bridge.js +566 -0
- package/src/openclaw-hook-installer.js +338 -0
- package/src/openclaw-hook.js +908 -0
- package/src/openclaw-wizard.js +2442 -0
- package/src/pending-questions.js +297 -0
- package/src/pricing.js +45 -0
- package/src/prompt-render.js +36 -0
- package/src/pty.js +992 -0
- package/src/routes/ai-terminal.js +1228 -0
- package/src/routes/git.js +89 -0
- package/src/routes/openclaw-hook.js +67 -0
- package/src/routes/openclaw-inbound.js +36 -0
- package/src/routes/recurringRules.js +80 -0
- package/src/routes/reports.js +50 -0
- package/src/routes/search.js +46 -0
- package/src/routes/stats.js +31 -0
- package/src/routes/telegram-config.js +152 -0
- package/src/routes/telegram-sync.js +221 -0
- package/src/routes/templates.js +63 -0
- package/src/routes/todos.js +649 -0
- package/src/routes/transcripts.js +75 -0
- package/src/routes/uploads.js +107 -0
- package/src/routes/wiki.js +142 -0
- package/src/search/fts.js +209 -0
- package/src/search/index.js +199 -0
- package/src/search/transcripts.js +148 -0
- package/src/server.js +1791 -0
- package/src/session-input-dispatcher.js +256 -0
- package/src/stats/markdown.js +42 -0
- package/src/stats/report.js +207 -0
- package/src/summarize.js +84 -0
- package/src/system-rules.js +52 -0
- package/src/telegram-bot.js +875 -0
- package/src/telegram-commands.js +149 -0
- package/src/telegram-config-service.js +84 -0
- package/src/telegram-image.js +95 -0
- package/src/telegram-loading-status.js +112 -0
- package/src/telegram-markdown.js +82 -0
- package/src/telegram-reaction-tracker.js +69 -0
- package/src/telegram-video.js +75 -0
- package/src/templates/claude-hooks/notify.js +103 -0
- package/src/transcript.js +305 -0
- package/src/transcripts/blocks.js +56 -0
- package/src/transcripts/index.js +222 -0
- package/src/transcripts/indexer.js +34 -0
- package/src/transcripts/matcher.js +70 -0
- package/src/transcripts/scanner.js +259 -0
- package/src/usage-footer.js +170 -0
- package/src/usage-parser.js +132 -0
- package/src/wiki/guide.js +44 -0
- package/src/wiki/index.js +232 -0
- package/src/wiki/redact.js +34 -0
- package/src/wiki/sources.js +122 -0
package/src/config.js
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { DEFAULT_PRICING } from "./pricing.js";
|
|
3
|
+
import {
|
|
4
|
+
accessSync,
|
|
5
|
+
constants,
|
|
6
|
+
cpSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { basename, join, resolve as resolvePath } from "node:path";
|
|
16
|
+
|
|
17
|
+
function canUseRootDir(rootDir) {
|
|
18
|
+
try {
|
|
19
|
+
if (!existsSync(rootDir)) mkdirSync(rootDir, { recursive: true });
|
|
20
|
+
accessSync(rootDir, constants.R_OK | constants.W_OK);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Auto-run legacy → ~/.agentquad migration once per process at import time.
|
|
28
|
+
// Must happen BEFORE DEFAULT_ROOT_DIR is computed, because canUseRootDir() has
|
|
29
|
+
// a mkdirSync side effect that would short-circuit the migration's
|
|
30
|
+
// existsSync(newDir) guard. Skipped in tests and when callers manage rootDir
|
|
31
|
+
// themselves via env vars.
|
|
32
|
+
if (
|
|
33
|
+
!process.env.AGENTQUAD_SKIP_AUTO_MIGRATE &&
|
|
34
|
+
!process.env.AGENTQUAD_ROOT_DIR &&
|
|
35
|
+
!process.env.QUADTODO_ROOT_DIR &&
|
|
36
|
+
!process.env.VITEST &&
|
|
37
|
+
process.env.NODE_ENV !== "test"
|
|
38
|
+
) {
|
|
39
|
+
const _migration = migrateLegacyHomeDirIfNeeded();
|
|
40
|
+
if (_migration.action === "abort") process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveDefaultRootDir() {
|
|
44
|
+
const envRootDir = process.env.AGENTQUAD_ROOT_DIR || process.env.QUADTODO_ROOT_DIR;
|
|
45
|
+
if (envRootDir) return resolvePath(envRootDir);
|
|
46
|
+
|
|
47
|
+
const newHomeDir = join(homedir(), ".agentquad");
|
|
48
|
+
if (canUseRootDir(newHomeDir)) return newHomeDir;
|
|
49
|
+
|
|
50
|
+
const legacyHomeDir = join(homedir(), ".quadtodo");
|
|
51
|
+
if (existsSync(legacyHomeDir) && canUseRootDir(legacyHomeDir)) return legacyHomeDir;
|
|
52
|
+
|
|
53
|
+
const newCwdDir = resolvePath(process.cwd(), ".agentquad");
|
|
54
|
+
if (canUseRootDir(newCwdDir)) return newCwdDir;
|
|
55
|
+
|
|
56
|
+
return resolvePath(process.cwd(), ".quadtodo");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_ROOT_DIR = resolveDefaultRootDir();
|
|
60
|
+
|
|
61
|
+
const TOOL_INSTALL_HINTS = {
|
|
62
|
+
claude: "npm install -g @anthropic-ai/claude-code",
|
|
63
|
+
codex: "npm install -g @openai/codex",
|
|
64
|
+
cursor: "curl https://cursor.com/install -fsSL | bash",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const SUPPORTED_TOOLS = ["claude", "codex", "cursor"];
|
|
68
|
+
|
|
69
|
+
const PERMISSION_MODES = new Set(["default", "acceptEdits", "bypass"]);
|
|
70
|
+
|
|
71
|
+
function normalizePermissionMode(value, fallback = "bypass") {
|
|
72
|
+
return PERMISSION_MODES.has(value) ? value : fallback;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const DEFAULT_WEBHOOK_CONFIG = {
|
|
76
|
+
enabled: false,
|
|
77
|
+
provider: "wecom",
|
|
78
|
+
url: "",
|
|
79
|
+
keywords: [],
|
|
80
|
+
cooldownMs: 180000,
|
|
81
|
+
notifyOnPendingConfirm: true,
|
|
82
|
+
notifyOnKeywordMatch: true,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const DEFAULT_OPENCLAW_CONFIG = {
|
|
86
|
+
enabled: false,
|
|
87
|
+
gatewayUrl: "http://127.0.0.1:18789",
|
|
88
|
+
channel: "openclaw-weixin",
|
|
89
|
+
// 微信 peer id 兜底;正常情况下每个 ai-session 启动时 OpenClaw skill 会
|
|
90
|
+
// 显式传 routeUserId(per-session 路由),这里仅在 ad-hoc 调用时用。
|
|
91
|
+
targetUserId: "",
|
|
92
|
+
askUser: {
|
|
93
|
+
defaultTimeoutMs: 600_000,
|
|
94
|
+
maxConcurrent: 8,
|
|
95
|
+
// 出站消息每分钟上限,防风控
|
|
96
|
+
rateLimitPerMin: 6,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const DEFAULT_TELEGRAM_CONFIG = {
|
|
101
|
+
enabled: false,
|
|
102
|
+
supergroupId: "",
|
|
103
|
+
longPollTimeoutSec: 30,
|
|
104
|
+
useTopics: true,
|
|
105
|
+
createTopicOnTaskStart: true,
|
|
106
|
+
closeTopicOnSessionEnd: true,
|
|
107
|
+
topicNameTemplate: "#t{shortCode} {title}",
|
|
108
|
+
topicNameDoneTemplate: "✅ {originalName}",
|
|
109
|
+
allowedChatIds: [], // 空 = 拒所有,强制白名单
|
|
110
|
+
allowedFromUserIds: [],
|
|
111
|
+
defaultPermissionMode: "bypass",
|
|
112
|
+
notificationCooldownMs: 600_000, // 同 session 内 ⚠️ idle 提醒最小间隔(默认 10 分钟,0 = 关闭去重)
|
|
113
|
+
suppressNotificationEvents: true, // 默认丢弃 Claude Code 的 idle Notification(无信息量;设 false 可恢复旧 cooldown 行为)
|
|
114
|
+
autoCreateTopic: true, // 非 wizard 起的 PTY session 自动镜像到 Telegram topic
|
|
115
|
+
pollRetryDelayMs: 5000,
|
|
116
|
+
minRenameIntervalMs: 30_000,
|
|
117
|
+
reactionEnabled: true, // 在用户触发消息上加 ✍ reaction 表示 AI 在干活;Stop hook 时清掉
|
|
118
|
+
reactionRunningEmoji: '✍', // 用哪个 Telegram 标准 emoji;群里若限制了 Available Reactions,改成允许列表里的(譬如 👀 / 🤔)
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const DEFAULT_LARK_CONFIG = {
|
|
122
|
+
enabled: false,
|
|
123
|
+
appId: "",
|
|
124
|
+
appSecret: "",
|
|
125
|
+
chatId: "",
|
|
126
|
+
requireThreadGroup: true,
|
|
127
|
+
eventSubscribeEnabled: true,
|
|
128
|
+
autoCreateTopic: true,
|
|
129
|
+
defaultPermissionMode: "bypass",
|
|
130
|
+
notificationCooldownMs: 600_000,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
function detectBinary(name) {
|
|
134
|
+
try {
|
|
135
|
+
const result = execSync(`command -v ${name}`, {
|
|
136
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
137
|
+
encoding: "utf8",
|
|
138
|
+
});
|
|
139
|
+
return result.trim() || name;
|
|
140
|
+
} catch {
|
|
141
|
+
return name;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function splitCommandLine(input = "") {
|
|
146
|
+
const tokens = [];
|
|
147
|
+
let current = "";
|
|
148
|
+
let quote = null;
|
|
149
|
+
let escaping = false;
|
|
150
|
+
|
|
151
|
+
for (const ch of String(input)) {
|
|
152
|
+
if (escaping) {
|
|
153
|
+
current += ch;
|
|
154
|
+
escaping = false;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (ch === "\\") {
|
|
158
|
+
escaping = true;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (quote) {
|
|
162
|
+
if (ch === quote) {
|
|
163
|
+
quote = null;
|
|
164
|
+
} else {
|
|
165
|
+
current += ch;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (ch === "'" || ch === '"') {
|
|
170
|
+
quote = ch;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (/\s/.test(ch)) {
|
|
174
|
+
if (current) {
|
|
175
|
+
tokens.push(current);
|
|
176
|
+
current = "";
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
current += ch;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (escaping) current += "\\";
|
|
184
|
+
if (current) tokens.push(current);
|
|
185
|
+
return tokens;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function defaultToolCommand(name) {
|
|
189
|
+
if (name === "claude") return "claude-w";
|
|
190
|
+
if (name === "cursor") return "cursor-agent";
|
|
191
|
+
return name;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeToolConfig(name, tool = {}, { applyDefaultCommand = true } = {}) {
|
|
195
|
+
const rawCommand =
|
|
196
|
+
typeof tool?.command === "string" ? tool.command.trim() : "";
|
|
197
|
+
const parsedTokens = splitCommandLine(rawCommand);
|
|
198
|
+
const parsedCommand = parsedTokens[0] || "";
|
|
199
|
+
const explicitArgs = Array.isArray(tool?.args)
|
|
200
|
+
? tool.args.map((item) => String(item))
|
|
201
|
+
: [];
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
...tool,
|
|
205
|
+
command:
|
|
206
|
+
parsedCommand ||
|
|
207
|
+
(applyDefaultCommand ? defaultToolCommand(name) : ""),
|
|
208
|
+
args: [...parsedTokens.slice(1), ...explicitArgs],
|
|
209
|
+
bin:
|
|
210
|
+
typeof tool?.bin === "string"
|
|
211
|
+
? tool.bin.trim()
|
|
212
|
+
: (tool?.bin ?? null),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function runtimeBinOverride(name) {
|
|
217
|
+
return process.env[`${name.toUpperCase()}_BIN`] || null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isStaleLegacyBin(name, configuredCommand, configuredBin, detectedBin) {
|
|
221
|
+
if (!configuredCommand || !configuredBin) return false;
|
|
222
|
+
if (configuredBin === detectedBin) return false;
|
|
223
|
+
return basename(configuredBin) === name;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getToolMetadata(name, tools = {}) {
|
|
227
|
+
const normalizedTool = normalizeToolConfig(name, tools?.[name], {
|
|
228
|
+
applyDefaultCommand: false,
|
|
229
|
+
});
|
|
230
|
+
const envBin = runtimeBinOverride(name);
|
|
231
|
+
const configuredCommand = normalizedTool.command || "";
|
|
232
|
+
const configuredBin = normalizedTool.bin || null;
|
|
233
|
+
const effectiveCommand = configuredCommand || defaultToolCommand(name);
|
|
234
|
+
const detectedBin = detectBinary(effectiveCommand);
|
|
235
|
+
const staleLegacyBin = isStaleLegacyBin(
|
|
236
|
+
name,
|
|
237
|
+
configuredCommand,
|
|
238
|
+
configuredBin,
|
|
239
|
+
detectedBin,
|
|
240
|
+
);
|
|
241
|
+
const source = envBin
|
|
242
|
+
? "env"
|
|
243
|
+
: configuredBin
|
|
244
|
+
? "config"
|
|
245
|
+
: configuredCommand
|
|
246
|
+
? "config"
|
|
247
|
+
: detectedBin !== effectiveCommand
|
|
248
|
+
? "auto-detected"
|
|
249
|
+
: "missing";
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
name,
|
|
253
|
+
configuredCommand: configuredCommand || null,
|
|
254
|
+
effectiveCommand,
|
|
255
|
+
configuredBin,
|
|
256
|
+
effectiveBin:
|
|
257
|
+
envBin || (staleLegacyBin ? detectedBin : configuredBin) || detectedBin,
|
|
258
|
+
args: normalizedTool.args,
|
|
259
|
+
source,
|
|
260
|
+
installHint: TOOL_INSTALL_HINTS[name] || null,
|
|
261
|
+
missing: source === "missing",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function resolveToolsConfig(tools = {}) {
|
|
266
|
+
const out = {};
|
|
267
|
+
for (const name of SUPPORTED_TOOLS) {
|
|
268
|
+
const normalized = normalizeToolConfig(name, tools[name]);
|
|
269
|
+
const meta = getToolMetadata(name, { ...tools, [name]: normalized });
|
|
270
|
+
out[name] = {
|
|
271
|
+
...normalized,
|
|
272
|
+
command: meta.effectiveCommand,
|
|
273
|
+
bin: meta.effectiveBin,
|
|
274
|
+
args: meta.args,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function inspectToolsConfig(tools = {}) {
|
|
281
|
+
const resolved = resolveToolsConfig(tools);
|
|
282
|
+
const out = {};
|
|
283
|
+
for (const name of SUPPORTED_TOOLS) {
|
|
284
|
+
out[name] = {
|
|
285
|
+
...getToolMetadata(name, tools),
|
|
286
|
+
command: resolved[name].command,
|
|
287
|
+
bin: resolved[name].bin,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function cloneDefaultPricing() {
|
|
294
|
+
return {
|
|
295
|
+
default: { ...DEFAULT_PRICING.default },
|
|
296
|
+
models: Object.fromEntries(
|
|
297
|
+
Object.entries(DEFAULT_PRICING.models).map(([k, v]) => [k, { ...v }]),
|
|
298
|
+
),
|
|
299
|
+
cnyRate: DEFAULT_PRICING.cnyRate,
|
|
300
|
+
// 是否在 Telegram / 飞书推送末尾附 token + 费用 footer。默认关,需要在 Settings 抽屉打开。
|
|
301
|
+
showInPush: false,
|
|
302
|
+
// footer 显示时是否同时带 ¥(CNY),仅在 showInPush=true 时有意义。
|
|
303
|
+
showCnyInPush: true,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function defaultConfig() {
|
|
308
|
+
return {
|
|
309
|
+
port: 5677,
|
|
310
|
+
// 监听地址。默认只绑定回环接口(本机安全)。
|
|
311
|
+
// 要让同网段其他设备(含 Tailscale 虚拟网段 100.x.x.x)访问,可设为 "0.0.0.0"。
|
|
312
|
+
// CLI 上也可以用 `agentquad start --expose` / `--host 0.0.0.0` 临时覆盖。
|
|
313
|
+
host: "127.0.0.1",
|
|
314
|
+
defaultTool: "claude",
|
|
315
|
+
defaultCwd: homedir(),
|
|
316
|
+
defaultPermissionMode: "default",
|
|
317
|
+
tools: resolveToolsConfig(),
|
|
318
|
+
webhook: { ...DEFAULT_WEBHOOK_CONFIG },
|
|
319
|
+
openclaw: {
|
|
320
|
+
...DEFAULT_OPENCLAW_CONFIG,
|
|
321
|
+
askUser: { ...DEFAULT_OPENCLAW_CONFIG.askUser },
|
|
322
|
+
},
|
|
323
|
+
telegram: {
|
|
324
|
+
...DEFAULT_TELEGRAM_CONFIG,
|
|
325
|
+
allowedChatIds: [...DEFAULT_TELEGRAM_CONFIG.allowedChatIds],
|
|
326
|
+
allowedFromUserIds: [...DEFAULT_TELEGRAM_CONFIG.allowedFromUserIds],
|
|
327
|
+
},
|
|
328
|
+
lark: { ...DEFAULT_LARK_CONFIG },
|
|
329
|
+
// Clone DEFAULT_PRICING so user mutations (e.g. via setConfigValue) don't
|
|
330
|
+
// leak back into the module-level constant.
|
|
331
|
+
pricing: cloneDefaultPricing(),
|
|
332
|
+
stats: { idleThresholdMs: 120_000 },
|
|
333
|
+
wiki: {
|
|
334
|
+
wikiDir: join(homedir(), ".agentquad", "wiki"),
|
|
335
|
+
maxTailTurns: 20,
|
|
336
|
+
tool: "claude",
|
|
337
|
+
timeoutMs: 600_000,
|
|
338
|
+
redact: true,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function normalizeDispatch(d = {}) {
|
|
344
|
+
const channels = ['lark', 'telegram', 'web'];
|
|
345
|
+
const out = {};
|
|
346
|
+
for (const ch of channels) {
|
|
347
|
+
const src = (d && typeof d[ch] === 'object' && d[ch] !== null) ? d[ch] : {};
|
|
348
|
+
out[ch] = { default: 'claude', ...src };
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function normalizeConfig(cfg = {}) {
|
|
354
|
+
const defaults = defaultConfig();
|
|
355
|
+
const mergedTools = {
|
|
356
|
+
...defaults.tools,
|
|
357
|
+
...(cfg.tools || {}),
|
|
358
|
+
};
|
|
359
|
+
const finalTools = {};
|
|
360
|
+
for (const name of SUPPORTED_TOOLS) {
|
|
361
|
+
mergedTools[name] = {
|
|
362
|
+
...defaults.tools[name],
|
|
363
|
+
...(cfg.tools?.[name] || {}),
|
|
364
|
+
};
|
|
365
|
+
finalTools[name] = normalizeToolConfig(name, mergedTools[name]);
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
...defaults,
|
|
369
|
+
...cfg,
|
|
370
|
+
defaultPermissionMode: normalizePermissionMode(cfg.defaultPermissionMode, "default"),
|
|
371
|
+
tools: {
|
|
372
|
+
...mergedTools,
|
|
373
|
+
...finalTools,
|
|
374
|
+
},
|
|
375
|
+
webhook: {
|
|
376
|
+
...DEFAULT_WEBHOOK_CONFIG,
|
|
377
|
+
...(cfg.webhook || {}),
|
|
378
|
+
keywords: Array.isArray(cfg.webhook?.keywords)
|
|
379
|
+
? cfg.webhook.keywords
|
|
380
|
+
.map((item) => String(item).trim())
|
|
381
|
+
.filter(Boolean)
|
|
382
|
+
: [...DEFAULT_WEBHOOK_CONFIG.keywords],
|
|
383
|
+
},
|
|
384
|
+
openclaw: {
|
|
385
|
+
...DEFAULT_OPENCLAW_CONFIG,
|
|
386
|
+
...(cfg.openclaw || {}),
|
|
387
|
+
askUser: {
|
|
388
|
+
...DEFAULT_OPENCLAW_CONFIG.askUser,
|
|
389
|
+
...(cfg.openclaw?.askUser || {}),
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
telegram: {
|
|
393
|
+
...DEFAULT_TELEGRAM_CONFIG,
|
|
394
|
+
...(cfg.telegram || {}),
|
|
395
|
+
allowedChatIds: Array.isArray(cfg.telegram?.allowedChatIds)
|
|
396
|
+
? cfg.telegram.allowedChatIds.map((x) => String(x).trim()).filter(Boolean)
|
|
397
|
+
: [...DEFAULT_TELEGRAM_CONFIG.allowedChatIds],
|
|
398
|
+
allowedFromUserIds: Array.isArray(cfg.telegram?.allowedFromUserIds)
|
|
399
|
+
? cfg.telegram.allowedFromUserIds.map((x) => String(x).trim()).filter(Boolean)
|
|
400
|
+
: [...DEFAULT_TELEGRAM_CONFIG.allowedFromUserIds],
|
|
401
|
+
defaultPermissionMode: normalizePermissionMode(cfg.telegram?.defaultPermissionMode),
|
|
402
|
+
},
|
|
403
|
+
lark: {
|
|
404
|
+
...DEFAULT_LARK_CONFIG,
|
|
405
|
+
...(cfg.lark || {}),
|
|
406
|
+
appId: typeof cfg.lark?.appId === "string"
|
|
407
|
+
? cfg.lark.appId.trim()
|
|
408
|
+
: DEFAULT_LARK_CONFIG.appId,
|
|
409
|
+
appSecret: typeof cfg.lark?.appSecret === "string"
|
|
410
|
+
? cfg.lark.appSecret.trim()
|
|
411
|
+
: DEFAULT_LARK_CONFIG.appSecret,
|
|
412
|
+
chatId: typeof cfg.lark?.chatId === "string"
|
|
413
|
+
? cfg.lark.chatId.trim()
|
|
414
|
+
: DEFAULT_LARK_CONFIG.chatId,
|
|
415
|
+
defaultPermissionMode: normalizePermissionMode(cfg.lark?.defaultPermissionMode),
|
|
416
|
+
},
|
|
417
|
+
// Note on models merge precedence: user entries with the SAME key as a
|
|
418
|
+
// default (e.g. 'claude-opus-4-*') override the default. To override
|
|
419
|
+
// pricing for a default-model pattern, the user must re-use the exact
|
|
420
|
+
// same glob key — adding a differently shaped glob will coexist with the
|
|
421
|
+
// default, and which one wins at estimateCost time depends on iteration
|
|
422
|
+
// order.
|
|
423
|
+
pricing: {
|
|
424
|
+
...defaults.pricing,
|
|
425
|
+
...(cfg.pricing || {}),
|
|
426
|
+
models: { ...defaults.pricing.models, ...(cfg.pricing?.models || {}) },
|
|
427
|
+
},
|
|
428
|
+
stats: { ...defaults.stats, ...(cfg.stats || {}) },
|
|
429
|
+
wiki: {
|
|
430
|
+
...defaults.wiki,
|
|
431
|
+
...(cfg.wiki || {}),
|
|
432
|
+
},
|
|
433
|
+
dispatch: normalizeDispatch(cfg.dispatch),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function ensureRoot(rootDir) {
|
|
438
|
+
if (!existsSync(rootDir)) mkdirSync(rootDir, { recursive: true });
|
|
439
|
+
const logsDir = join(rootDir, "logs");
|
|
440
|
+
if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function backupCorruptConfig(file) {
|
|
444
|
+
const backup = file + ".corrupt";
|
|
445
|
+
try {
|
|
446
|
+
renameSync(file, backup);
|
|
447
|
+
return;
|
|
448
|
+
} catch {
|
|
449
|
+
// Some environments disallow rename on managed files; fall back to copying.
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
writeFileSync(backup, readFileSync(file, "utf8"));
|
|
454
|
+
} catch {
|
|
455
|
+
// Ignore backup failures and continue with a fresh default config.
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function tryWriteConfig(file, cfg) {
|
|
460
|
+
try {
|
|
461
|
+
writeFileSync(file, JSON.stringify(cfg, null, 2));
|
|
462
|
+
return true;
|
|
463
|
+
} catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function loadConfig({ rootDir = DEFAULT_ROOT_DIR } = {}) {
|
|
469
|
+
ensureRoot(rootDir);
|
|
470
|
+
const file = join(rootDir, "config.json");
|
|
471
|
+
if (!existsSync(file)) {
|
|
472
|
+
const cfg = normalizeConfig();
|
|
473
|
+
tryWriteConfig(file, cfg);
|
|
474
|
+
return cfg;
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
const cfg = normalizeConfig(JSON.parse(readFileSync(file, "utf8")));
|
|
478
|
+
tryWriteConfig(file, cfg);
|
|
479
|
+
return cfg;
|
|
480
|
+
} catch {
|
|
481
|
+
backupCorruptConfig(file);
|
|
482
|
+
const cfg = normalizeConfig();
|
|
483
|
+
tryWriteConfig(file, cfg);
|
|
484
|
+
return cfg;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function saveConfig(cfg, { rootDir = DEFAULT_ROOT_DIR } = {}) {
|
|
489
|
+
ensureRoot(rootDir);
|
|
490
|
+
writeFileSync(
|
|
491
|
+
join(rootDir, "config.json"),
|
|
492
|
+
JSON.stringify(normalizeConfig(cfg), null, 2),
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function getConfigValue(path, { rootDir = DEFAULT_ROOT_DIR } = {}) {
|
|
497
|
+
const cfg = loadConfig({ rootDir });
|
|
498
|
+
return path
|
|
499
|
+
.split(".")
|
|
500
|
+
.reduce((obj, key) => (obj == null ? undefined : obj[key]), cfg);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function setConfigValue(
|
|
504
|
+
path,
|
|
505
|
+
value,
|
|
506
|
+
{ rootDir = DEFAULT_ROOT_DIR } = {},
|
|
507
|
+
) {
|
|
508
|
+
const cfg = loadConfig({ rootDir });
|
|
509
|
+
const keys = path.split(".");
|
|
510
|
+
let obj = cfg;
|
|
511
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
512
|
+
if (obj[keys[i]] == null || typeof obj[keys[i]] !== "object")
|
|
513
|
+
obj[keys[i]] = {};
|
|
514
|
+
obj = obj[keys[i]];
|
|
515
|
+
}
|
|
516
|
+
// 尝试把字符串转成合适类型(数字、布尔)
|
|
517
|
+
let v = value;
|
|
518
|
+
if (typeof value === "string") {
|
|
519
|
+
if (value === "true") v = true;
|
|
520
|
+
else if (value === "false") v = false;
|
|
521
|
+
else if (/^-?\d+(\.\d+)?$/.test(value)) v = Number(value);
|
|
522
|
+
}
|
|
523
|
+
obj[keys[keys.length - 1]] = v;
|
|
524
|
+
saveConfig(cfg, { rootDir });
|
|
525
|
+
return v;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function defaultIsPidAlive(pid) {
|
|
529
|
+
try {
|
|
530
|
+
process.kill(pid, 0);
|
|
531
|
+
return true;
|
|
532
|
+
} catch {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function rewriteConfigPaths(configPath, oldHome, newHome) {
|
|
538
|
+
if (!existsSync(configPath)) return;
|
|
539
|
+
try {
|
|
540
|
+
const raw = readFileSync(configPath, "utf8");
|
|
541
|
+
// Boundary-aware replace: only rewrite when oldHome is followed by '/' or
|
|
542
|
+
// a JSON close-quote, so /Users/u/.quadtodo-backup is not mangled.
|
|
543
|
+
const rewritten = raw
|
|
544
|
+
.split(oldHome + "/").join(newHome + "/")
|
|
545
|
+
.split(oldHome + '"').join(newHome + '"');
|
|
546
|
+
if (rewritten !== raw) writeFileSync(configPath, rewritten);
|
|
547
|
+
} catch {
|
|
548
|
+
// Non-fatal: caller will surface the abnormal config on next normalize.
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function rewriteClaudeSettings(home, oldDir, newDir) {
|
|
553
|
+
const settingsPath = join(home, ".claude", "settings.json");
|
|
554
|
+
if (!existsSync(settingsPath)) return;
|
|
555
|
+
let raw;
|
|
556
|
+
try {
|
|
557
|
+
raw = readFileSync(settingsPath, "utf8");
|
|
558
|
+
} catch {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
let settings;
|
|
562
|
+
try {
|
|
563
|
+
settings = JSON.parse(raw);
|
|
564
|
+
} catch {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
let changed = false;
|
|
568
|
+
const oldPrefix = oldDir + "/";
|
|
569
|
+
const newPrefix = newDir + "/";
|
|
570
|
+
|
|
571
|
+
// MCP entries
|
|
572
|
+
const mcp = settings.mcpServers;
|
|
573
|
+
if (mcp && typeof mcp === "object") {
|
|
574
|
+
for (const key of Object.keys(mcp)) {
|
|
575
|
+
const entry = mcp[key];
|
|
576
|
+
if (!entry || typeof entry !== "object") continue;
|
|
577
|
+
if (typeof entry.command === "string" && entry.command.includes(oldPrefix)) {
|
|
578
|
+
entry.command = entry.command.split(oldPrefix).join(newPrefix);
|
|
579
|
+
changed = true;
|
|
580
|
+
}
|
|
581
|
+
if (Array.isArray(entry.args)) {
|
|
582
|
+
for (let i = 0; i < entry.args.length; i++) {
|
|
583
|
+
if (typeof entry.args[i] === "string" && entry.args[i].includes(oldPrefix)) {
|
|
584
|
+
entry.args[i] = entry.args[i].split(oldPrefix).join(newPrefix);
|
|
585
|
+
changed = true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Hook entries (only ours, gated by _quadtodoManaged marker)
|
|
593
|
+
const hooks = settings.hooks;
|
|
594
|
+
if (hooks && typeof hooks === "object") {
|
|
595
|
+
for (const eventName of Object.keys(hooks)) {
|
|
596
|
+
const eventHooks = hooks[eventName];
|
|
597
|
+
if (!Array.isArray(eventHooks)) continue;
|
|
598
|
+
for (const matcher of eventHooks) {
|
|
599
|
+
if (!matcher || !Array.isArray(matcher.hooks)) continue;
|
|
600
|
+
for (const hook of matcher.hooks) {
|
|
601
|
+
if (!hook || hook._quadtodoManaged !== true) continue;
|
|
602
|
+
if (typeof hook.command === "string" && hook.command.includes(oldPrefix)) {
|
|
603
|
+
hook.command = hook.command.split(oldPrefix).join(newPrefix);
|
|
604
|
+
changed = true;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (changed) {
|
|
612
|
+
try {
|
|
613
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
614
|
+
} catch {
|
|
615
|
+
// Non-fatal: stderr will surface via migration result; user can re-bootstrap.
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function moveDirectory(src, dest) {
|
|
621
|
+
try {
|
|
622
|
+
renameSync(src, dest);
|
|
623
|
+
return;
|
|
624
|
+
} catch (err) {
|
|
625
|
+
if (err && err.code !== "EXDEV") throw err;
|
|
626
|
+
}
|
|
627
|
+
cpSync(src, dest, { recursive: true });
|
|
628
|
+
rmSync(src, { recursive: true, force: true });
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export function migrateLegacyHomeDirIfNeeded({
|
|
632
|
+
home = homedir(),
|
|
633
|
+
stderr = process.stderr,
|
|
634
|
+
isPidAlive = defaultIsPidAlive,
|
|
635
|
+
} = {}) {
|
|
636
|
+
const newDir = join(home, ".agentquad");
|
|
637
|
+
const oldDir = join(home, ".quadtodo");
|
|
638
|
+
|
|
639
|
+
if (existsSync(newDir)) {
|
|
640
|
+
if (existsSync(oldDir)) {
|
|
641
|
+
stderr.write(
|
|
642
|
+
`AgentQuad: found legacy ~/.quadtodo/ alongside ~/.agentquad/; ignoring. Delete it manually when ready.\n`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
return { action: "skip", reason: "new-exists" };
|
|
646
|
+
}
|
|
647
|
+
if (!existsSync(oldDir)) {
|
|
648
|
+
return { action: "skip", reason: "no-legacy" };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const legacyPidFile = join(oldDir, "quadtodo.pid");
|
|
652
|
+
if (existsSync(legacyPidFile)) {
|
|
653
|
+
const pid = Number.parseInt(
|
|
654
|
+
(readFileSync(legacyPidFile, "utf8") || "").trim(),
|
|
655
|
+
10,
|
|
656
|
+
);
|
|
657
|
+
if (Number.isFinite(pid) && pid > 0 && isPidAlive(pid)) {
|
|
658
|
+
stderr.write(
|
|
659
|
+
`AgentQuad: detected running quadtodo service (pid ${pid}).\n`,
|
|
660
|
+
);
|
|
661
|
+
stderr.write(
|
|
662
|
+
`Please run \`quadtodo stop\` (or kill ${pid}) and start AgentQuad again.\n`,
|
|
663
|
+
);
|
|
664
|
+
return { action: "abort", reason: "pid-alive", pid };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
moveDirectory(oldDir, newDir);
|
|
669
|
+
|
|
670
|
+
rewriteConfigPaths(join(newDir, "config.json"), oldDir, newDir);
|
|
671
|
+
rewriteClaudeSettings(home, oldDir, newDir);
|
|
672
|
+
|
|
673
|
+
const stalePid = join(newDir, "quadtodo.pid");
|
|
674
|
+
if (existsSync(stalePid)) rmSync(stalePid, { force: true });
|
|
675
|
+
|
|
676
|
+
const oldLog = join(newDir, "logs", "quadtodo.log");
|
|
677
|
+
const newLog = join(newDir, "logs", "agentquad.log");
|
|
678
|
+
if (existsSync(oldLog) && !existsSync(newLog)) {
|
|
679
|
+
try {
|
|
680
|
+
renameSync(oldLog, newLog);
|
|
681
|
+
} catch {
|
|
682
|
+
// Non-fatal.
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
writeFileSync(
|
|
687
|
+
join(newDir, ".migrated-from-quadtodo"),
|
|
688
|
+
new Date().toISOString(),
|
|
689
|
+
);
|
|
690
|
+
stderr.write(`AgentQuad: migrated ~/.quadtodo → ~/.agentquad\n`);
|
|
691
|
+
return { action: "migrated" };
|
|
692
|
+
}
|