claude360 0.2.2 → 0.2.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.
@@ -0,0 +1,383 @@
1
+ // 初始化配置项(PRD 6.3 / 6.4 / 6.7 / 6.8 / 8.2 / 8.3):
2
+ // AI 输出语言、系统提示词风格写入 ~/.claude/CLAUDE.md 或 ~/.codex/AGENTS.md
3
+ // 的 Claude360 标记区块(写前备份、不覆盖用户内容、可跳过);
4
+ // 默认模型与推荐环境变量/权限写入 ~/.claude/settings.json(JSON 合并,不删用户配置)。
5
+
6
+ import { copyFile, cp, mkdir, readFile, stat, writeFile } from "node:fs/promises";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+
10
+ import { createBackup } from "./backup.js";
11
+ import { upsertMarkedBlock } from "./mcp-skill.js";
12
+ import { upsertProfileKey, validateBasicToml } from "./tool-launcher.js";
13
+
14
+ const defaultFs = { copyFile, cp, mkdir, readFile, stat, writeFile };
15
+
16
+ export function resolveClaudeDir({ homedir = os.homedir } = {}) {
17
+ return path.join(homedir(), ".claude");
18
+ }
19
+
20
+ export function resolveCodexDir({ homedir = os.homedir } = {}) {
21
+ return path.join(homedir(), ".codex");
22
+ }
23
+
24
+ async function readFileIfExists(fs, filePath) {
25
+ try {
26
+ return await fs.readFile(filePath, "utf8");
27
+ } catch (error) {
28
+ if (error?.code === "ENOENT") {
29
+ return "";
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ // 写入 Markdown 标记区块:先备份目标文件到 baseDir/backup/backup_<时间戳>/,
36
+ // 再以 claude360 标记区块幂等更新,不触碰用户已有内容。
37
+ async function writeMarkedBlock({ baseDir, filePath, blockId, content, writeLine, fs, now }) {
38
+ const current = await readFileIfExists(fs, filePath);
39
+ const { backupDir } = await createBackup({ baseDir, paths: [filePath], fs, now });
40
+ if (backupDir) {
41
+ writeLine(`✓ 已创建备份:${backupDir}`);
42
+ }
43
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
44
+ await fs.writeFile(filePath, upsertMarkedBlock(current, blockId, content), "utf8");
45
+ }
46
+
47
+ // ──────────────────────────────────────────────
48
+ // AI 输出语言(PRD 6.3 / 8.2)
49
+ // ──────────────────────────────────────────────
50
+
51
+ export const OUTPUT_LANGUAGES = [
52
+ {
53
+ id: "zh-CN",
54
+ label: "简体中文 推荐中文用户",
55
+ content: [
56
+ "# Claude360 用户偏好",
57
+ "",
58
+ "请默认使用简体中文回复用户。",
59
+ "除非用户明确要求其他语言,否则保持中文输出。",
60
+ ].join("\n"),
61
+ },
62
+ {
63
+ id: "en",
64
+ label: "English",
65
+ content: [
66
+ "# Claude360 User Preference",
67
+ "",
68
+ "Reply to the user in English by default.",
69
+ "Keep English output unless the user explicitly requests another language.",
70
+ ].join("\n"),
71
+ },
72
+ ];
73
+
74
+ export const OUTPUT_LANGUAGE_BLOCK_ID = "output-language";
75
+
76
+ export async function configureOutputLanguage({
77
+ baseDir,
78
+ filePath,
79
+ promptSelect,
80
+ writeLine = console.log,
81
+ fs = defaultFs,
82
+ now,
83
+ } = {}) {
84
+ if (typeof promptSelect !== "function") {
85
+ throw new Error("缺少选择输入");
86
+ }
87
+ const selected = await promptSelect("请选择 AI 输出语言:", [
88
+ ...OUTPUT_LANGUAGES.map((language) => ({ label: language.label, value: language.id })),
89
+ { label: "跳过", value: "skip" },
90
+ ]);
91
+ if (selected === "skip") {
92
+ writeLine("已跳过 AI 输出语言配置。");
93
+ return { skipped: true };
94
+ }
95
+ const language = OUTPUT_LANGUAGES.find((item) => item.id === selected);
96
+ await writeMarkedBlock({
97
+ baseDir,
98
+ filePath,
99
+ blockId: OUTPUT_LANGUAGE_BLOCK_ID,
100
+ content: language.content,
101
+ writeLine,
102
+ fs,
103
+ now,
104
+ });
105
+ writeLine(`✓ AI 输出语言已写入:${filePath}`);
106
+ return { skipped: false, language: language.id };
107
+ }
108
+
109
+ // ──────────────────────────────────────────────
110
+ // 系统提示词风格(PRD 6.4 / 8.3):少而精,保持专业克制
111
+ // ──────────────────────────────────────────────
112
+
113
+ export const PROMPT_STYLES = [
114
+ {
115
+ id: "engineer",
116
+ label: "工程师专业版 推荐",
117
+ desc: "适合代码开发、重构、调试、架构分析,强调清晰、准确、可执行。",
118
+ content: [
119
+ "# Claude360 系统提示词风格:工程师专业版",
120
+ "",
121
+ "- 以高级软件工程师的标准回答:清晰、准确、可执行。",
122
+ "- 修改代码前先理解现有实现与约定,保持最小改动面。",
123
+ "- 给出方案时说明取舍与影响范围,重要结论给出依据。",
124
+ "- 不确定时明确说明假设,不编造 API 或行为。",
125
+ ].join("\n"),
126
+ },
127
+ {
128
+ id: "product",
129
+ label: "产品分析助手",
130
+ desc: "适合 PRD、需求拆解、用户故事、功能规划和产品设计。",
131
+ content: [
132
+ "# Claude360 系统提示词风格:产品分析助手",
133
+ "",
134
+ "- 以产品视角分析需求:目标用户、场景、价值与边界。",
135
+ "- 输出结构化结论:需求要点、功能拆解、优先级与风险。",
136
+ "- 区分「做什么」与「不做什么」,避免范围蔓延。",
137
+ ].join("\n"),
138
+ },
139
+ {
140
+ id: "concise",
141
+ label: "简洁高效模式",
142
+ desc: "减少解释,优先给出结论和操作步骤。",
143
+ content: [
144
+ "# Claude360 系统提示词风格:简洁高效模式",
145
+ "",
146
+ "- 优先给出结论和操作步骤,减少铺垫与解释。",
147
+ "- 能用列表与代码块表达的不用长段落。",
148
+ "- 仅在用户追问时展开背景与原理。",
149
+ ].join("\n"),
150
+ },
151
+ ];
152
+
153
+ export const PROMPT_STYLE_BLOCK_ID = "prompt-style";
154
+
155
+ export async function configurePromptStyle({
156
+ baseDir,
157
+ filePath,
158
+ promptSelect,
159
+ writeLine = console.log,
160
+ fs = defaultFs,
161
+ now,
162
+ } = {}) {
163
+ if (typeof promptSelect !== "function") {
164
+ throw new Error("缺少选择输入");
165
+ }
166
+ for (const style of PROMPT_STYLES) {
167
+ writeLine(`${style.label.trim()}:${style.desc}`);
168
+ }
169
+ writeLine("");
170
+ const selected = await promptSelect("请选择系统提示词风格:", [
171
+ ...PROMPT_STYLES.map((style) => ({ label: style.label, value: style.id })),
172
+ { label: "跳过", value: "skip" },
173
+ ]);
174
+ if (selected === "skip") {
175
+ writeLine("已跳过系统提示词风格配置。");
176
+ return { skipped: true };
177
+ }
178
+ const style = PROMPT_STYLES.find((item) => item.id === selected);
179
+ await writeMarkedBlock({
180
+ baseDir,
181
+ filePath,
182
+ blockId: PROMPT_STYLE_BLOCK_ID,
183
+ content: style.content,
184
+ writeLine,
185
+ fs,
186
+ now,
187
+ });
188
+ writeLine(`✓ 系统提示词风格已写入:${filePath}`);
189
+ return { skipped: false, style: style.id };
190
+ }
191
+
192
+ // ──────────────────────────────────────────────
193
+ // Claude Code 默认模型(PRD 6.7):模型名以后端返回为准,
194
+ // 后端未提供时回退到 Claude Code 官方别名(sonnet/opus/haiku),
195
+ // CLI 不硬编码具体模型 ID、倍率与计费规则。
196
+ // ──────────────────────────────────────────────
197
+
198
+ export const DEFAULT_MODEL_TIERS = [
199
+ { id: "recommended", label: "推荐模型 适合大多数代码任务", model: "sonnet" },
200
+ { id: "performance", label: "高性能模型 适合复杂架构和长上下文任务", model: "opus" },
201
+ { id: "economy", label: "经济模型 适合日常轻量任务", model: "haiku" },
202
+ ];
203
+
204
+ // 后端可选接口 GET /api/cli/models:{ tiers: [{ id, label, model }] };
205
+ // 不可用时回退内置映射。
206
+ export async function loadModelTiers(api) {
207
+ if (api && typeof api.get === "function") {
208
+ try {
209
+ const data = await api.get("/api/cli/models");
210
+ const tiers = Array.isArray(data?.tiers) ? data.tiers : [];
211
+ if (tiers.length > 0 && tiers.every((tier) => tier.id && tier.label && tier.model)) {
212
+ return { tiers, source: "backend" };
213
+ }
214
+ } catch {
215
+ // 后端暂未提供模型列表接口,使用内置映射
216
+ }
217
+ }
218
+ return { tiers: DEFAULT_MODEL_TIERS, source: "builtin" };
219
+ }
220
+
221
+ async function readJsonIfExists(fs, filePath) {
222
+ const content = await readFileIfExists(fs, filePath);
223
+ if (content.trim() === "") {
224
+ return {};
225
+ }
226
+ try {
227
+ return JSON.parse(content);
228
+ } catch {
229
+ throw new Error(`${filePath} 不是合法 JSON,已停止写入以保护现有配置。`);
230
+ }
231
+ }
232
+
233
+ async function writeJsonWithBackup({ baseDir, filePath, json, writeLine, fs, now }) {
234
+ const { backupDir } = await createBackup({ baseDir, paths: [filePath], fs, now });
235
+ if (backupDir) {
236
+ writeLine(`✓ 已创建备份:${backupDir}`);
237
+ }
238
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
239
+ await fs.writeFile(filePath, `${JSON.stringify(json, null, 2)}\n`, "utf8");
240
+ }
241
+
242
+ export async function configureClaudeDefaultModel({
243
+ api,
244
+ claudeDir = resolveClaudeDir(),
245
+ promptSelect,
246
+ writeLine = console.log,
247
+ fs = defaultFs,
248
+ now,
249
+ } = {}) {
250
+ if (typeof promptSelect !== "function") {
251
+ throw new Error("缺少选择输入");
252
+ }
253
+ const { tiers } = await loadModelTiers(api);
254
+ const selected = await promptSelect("请选择默认模型:", [
255
+ ...tiers.map((tier) => ({ label: tier.label, value: tier.id })),
256
+ { label: "跳过", value: "skip" },
257
+ ]);
258
+ if (selected === "skip") {
259
+ writeLine("已跳过默认模型配置。");
260
+ return { skipped: true };
261
+ }
262
+ const tier = tiers.find((item) => item.id === selected);
263
+ const settingsPath = path.join(claudeDir, "settings.json");
264
+ const settings = await readJsonIfExists(fs, settingsPath);
265
+ settings.model = tier.model;
266
+ await writeJsonWithBackup({ baseDir: claudeDir, filePath: settingsPath, json: settings, writeLine, fs, now });
267
+ writeLine(`✓ 默认模型已设置为 ${tier.model}(${settingsPath})`);
268
+ return { skipped: false, model: tier.model };
269
+ }
270
+
271
+ // ──────────────────────────────────────────────
272
+ // 推荐环境变量与权限(PRD 6.8):仅写 ~/.claude/settings.json,
273
+ // 不修改 shell profile、不写系统全局环境变量;权限放宽需用户确认。
274
+ // ──────────────────────────────────────────────
275
+
276
+ export const RECOMMENDED_CLAUDE_SETTINGS = {
277
+ env: {
278
+ // 隐私保护:关闭非必要遥测与错误上报流量
279
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
280
+ },
281
+ permissions: {
282
+ allow: [
283
+ // 常用只读与构建类命令,避免高频确认打断
284
+ "Bash(ls:*)",
285
+ "Bash(cat:*)",
286
+ "Bash(grep:*)",
287
+ "Bash(rg:*)",
288
+ "Bash(find:*)",
289
+ "Bash(git status)",
290
+ "Bash(git diff:*)",
291
+ "Bash(git log:*)",
292
+ "Bash(node --version)",
293
+ "Bash(npm --version)",
294
+ ],
295
+ },
296
+ };
297
+
298
+ export async function importClaudeEnvPermissions({
299
+ claudeDir = resolveClaudeDir(),
300
+ confirm,
301
+ writeLine = console.log,
302
+ fs = defaultFs,
303
+ now,
304
+ recommended = RECOMMENDED_CLAUDE_SETTINGS,
305
+ } = {}) {
306
+ if (typeof confirm !== "function") {
307
+ throw new Error("缺少确认输入");
308
+ }
309
+ const settingsPath = path.join(claudeDir, "settings.json");
310
+ writeLine([
311
+ "导入推荐环境变量和权限配置",
312
+ "",
313
+ "该操作将为 Claude Code 写入推荐配置:",
314
+ "- 隐私保护(关闭非必要遥测流量)",
315
+ "- 权限允许列表(常用只读 / git / 版本查询命令免确认)",
316
+ "",
317
+ `写入位置:${settingsPath}(不修改 shell profile,不写系统全局环境变量)`,
318
+ "注意:权限允许列表会放宽上述命令的执行确认。",
319
+ ].join("\n"));
320
+ const approved = await confirm("是否继续?");
321
+ if (!approved) {
322
+ writeLine("已跳过推荐环境变量和权限配置。");
323
+ return { skipped: true };
324
+ }
325
+ const settings = await readJsonIfExists(fs, settingsPath);
326
+ // 合并而非覆盖:用户已有的 env 值与权限条目全部保留
327
+ settings.env = { ...recommended.env, ...(settings.env || {}) };
328
+ const allow = new Set([...(settings.permissions?.allow || []), ...recommended.permissions.allow]);
329
+ settings.permissions = { ...(settings.permissions || {}), allow: [...allow] };
330
+ await writeJsonWithBackup({ baseDir: claudeDir, filePath: settingsPath, json: settings, writeLine, fs, now });
331
+ writeLine(`✓ 推荐环境变量与权限配置已写入:${settingsPath}`);
332
+ return { skipped: false, path: settingsPath };
333
+ }
334
+
335
+ // ──────────────────────────────────────────────
336
+ // Codex 默认模型(PRD 7.1 第 5 项):模型名必须来自后端;
337
+ // 后端未提供模型列表时不写入,提示在 Codex 内用 /model 选择
338
+ // (Claude Code 的内置别名不适用于 Codex)。
339
+ // ──────────────────────────────────────────────
340
+
341
+ export async function configureCodexDefaultModel({
342
+ api,
343
+ codexDir = resolveCodexDir(),
344
+ promptSelect,
345
+ writeLine = console.log,
346
+ fs = defaultFs,
347
+ now,
348
+ } = {}) {
349
+ if (typeof promptSelect !== "function") {
350
+ throw new Error("缺少选择输入");
351
+ }
352
+ const { tiers, source } = await loadModelTiers(api);
353
+ if (source !== "backend") {
354
+ writeLine("Claude360 后端暂未提供 Codex 模型列表,已跳过默认模型写入。");
355
+ writeLine("可在启动 Codex 后使用 /model 命令选择模型。");
356
+ return { skipped: true };
357
+ }
358
+ const selected = await promptSelect("请选择默认模型:", [
359
+ ...tiers.map((tier) => ({ label: tier.label, value: tier.id })),
360
+ { label: "跳过", value: "skip" },
361
+ ]);
362
+ if (selected === "skip") {
363
+ writeLine("已跳过默认模型配置。");
364
+ return { skipped: true };
365
+ }
366
+ const tier = tiers.find((item) => item.id === selected);
367
+ const configPath = path.join(codexDir, "config.toml");
368
+ const current = await readFileIfExists(fs, configPath);
369
+ const next = `${upsertProfileKey(current, "claude360", "model", tier.model).trimEnd()}\n`;
370
+ const tomlError = validateBasicToml(next);
371
+ if (tomlError) {
372
+ writeLine(`× 生成的 Codex 配置 TOML 校验失败:${tomlError}\n已放弃写入,原配置保持不变。`);
373
+ return { skipped: true };
374
+ }
375
+ const { backupDir } = await createBackup({ baseDir: codexDir, paths: [configPath], fs, now });
376
+ if (backupDir) {
377
+ writeLine(`✓ 已创建备份:${backupDir}`);
378
+ }
379
+ await fs.mkdir(codexDir, { recursive: true });
380
+ await fs.writeFile(configPath, next, "utf8");
381
+ writeLine(`✓ Codex 默认模型已设置为 ${tier.model}(${configPath})`);
382
+ return { skipped: false, model: tier.model };
383
+ }
@@ -0,0 +1,70 @@
1
+ // 完整初始化编排(PRD 5.2 / 7.2):按步骤推进,必需步骤(声明、登录、Key)
2
+ // 失败或用户返回时中止;增强步骤(工作流、MCP、模型、权限等)失败或跳过
3
+ // 不阻塞主流程(AC-05)。各步骤动作由 index.js 注入,本模块只负责编排与输出。
4
+
5
+ import { safeErrorMessage } from "./sanitize.js";
6
+
7
+ export async function runFullInit({ title, steps, writeLine = console.log } = {}) {
8
+ writeLine(`${title}\n`);
9
+ let index = 0;
10
+ for (const step of steps) {
11
+ index += 1;
12
+ writeLine(`第 ${index} 步:${step.title}`);
13
+ try {
14
+ const result = await step.run();
15
+ if (step.required && result === false) {
16
+ writeLine(`已在「${step.title}」中止初始化。`);
17
+ return { completed: false, abortedAt: step.title };
18
+ }
19
+ } catch (error) {
20
+ if (step.required) {
21
+ writeLine(`× ${step.title}失败:${safeErrorMessage(error)}`);
22
+ writeLine("初始化已中止,可稍后在菜单中重试。");
23
+ return { completed: false, abortedAt: step.title };
24
+ }
25
+ writeLine(`! ${step.title}失败(已跳过,不影响后续步骤):${safeErrorMessage(error)}`);
26
+ }
27
+ writeLine("");
28
+ }
29
+ return { completed: true };
30
+ }
31
+
32
+ // Claude Code 完整初始化步骤(PRD 5.2)
33
+ export function buildClaudeInitSteps(deps) {
34
+ return [
35
+ { title: "展示开源参考声明", required: true, run: deps.showNotice },
36
+ { title: "检查 Claude360 登录状态", required: true, run: deps.ensureLogin },
37
+ { title: "展示余额与今日用量", run: deps.showBalance },
38
+ { title: "选择或创建 Claude360 API Key", required: true, run: deps.ensureApiKey },
39
+ { title: "检查 / 安装 / 更新 Claude Code", run: deps.installTool },
40
+ { title: "配置 Claude360 API 注入", required: true, run: deps.configureApi },
41
+ { title: "选择 AI 输出语言", run: deps.configureLanguage },
42
+ { title: "选择系统提示词风格", run: deps.configureStyle },
43
+ { title: "导入推荐工作流", run: deps.installWorkflows },
44
+ { title: "配置推荐 MCP 服务", run: deps.installMcps },
45
+ { title: "配置默认模型", run: deps.configureModel },
46
+ { title: "导入推荐权限与环境变量配置", run: deps.importEnvPermissions },
47
+ { title: "测试连接", run: deps.testConnection },
48
+ { title: "询问是否立即启动 Claude Code", run: deps.askLaunch },
49
+ ];
50
+ }
51
+
52
+ // Codex 完整初始化步骤(PRD 7.2)
53
+ export function buildCodexInitSteps(deps) {
54
+ return [
55
+ { title: "展示开源参考声明", required: true, run: deps.showNotice },
56
+ { title: "检查 Claude360 登录状态", required: true, run: deps.ensureLogin },
57
+ { title: "展示余额与今日用量", run: deps.showBalance },
58
+ { title: "选择或创建 Claude360 API Key", required: true, run: deps.ensureApiKey },
59
+ { title: "检查 / 安装 / 更新 Codex", run: deps.installTool },
60
+ { title: "选择 AI 输出语言", run: deps.configureLanguage },
61
+ { title: "选择系统提示词风格", run: deps.configureStyle },
62
+ { title: "导入推荐 AGENTS 配置", run: deps.installAgents },
63
+ { title: "导入推荐 prompts / workflow", run: deps.installWorkflows },
64
+ { title: "配置 Claude360 Provider", required: true, run: deps.configureProvider },
65
+ { title: "检测 Codex 协议兼容性", required: true, run: deps.checkCompat },
66
+ { title: "配置推荐 MCP 服务", run: deps.installMcps },
67
+ { title: "测试连接", run: deps.testConnection },
68
+ { title: "询问是否立即启动 Codex", run: deps.askLaunch },
69
+ ];
70
+ }