@jiy/quizme 0.1.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 +48 -0
- package/dist/cli/config.js +37 -0
- package/dist/cli/index.js +64 -0
- package/dist/cli/session.js +10 -0
- package/dist/generation/dedupe.js +11 -0
- package/dist/generation/schema.js +50 -0
- package/dist/generation/validator.js +134 -0
- package/dist/platform/fs.js +17 -0
- package/dist/platform/paths.js +27 -0
- package/dist/providers/claudeAgent.js +295 -0
- package/dist/sources/claudeSession.js +112 -0
- package/dist/sources/repository.js +43 -0
- package/dist/sources/topic.js +7 -0
- package/dist/storage/index.js +18 -0
- package/dist/storage/sqlite.js +294 -0
- package/dist/types.js +1 -0
- package/dist/ui/App.js +90 -0
- package/dist/ui/components/AppHeader.js +6 -0
- package/dist/ui/components/Clawd.js +37 -0
- package/dist/ui/components/Divider.js +6 -0
- package/dist/ui/components/Feed.js +32 -0
- package/dist/ui/components/FeedColumn.js +9 -0
- package/dist/ui/components/SelectList.js +13 -0
- package/dist/ui/components/StatusBar.js +9 -0
- package/dist/ui/components/TextInput.js +23 -0
- package/dist/ui/components/WelcomeBanner.js +79 -0
- package/dist/ui/formatters.js +66 -0
- package/dist/ui/logoLayout.js +30 -0
- package/dist/ui/renderApp.js +25 -0
- package/dist/ui/screens/HomeScreen.js +57 -0
- package/dist/ui/screens/InfoScreen.js +16 -0
- package/dist/ui/screens/QuizScreen.js +344 -0
- package/dist/ui/screens/SettingsScreen.js +133 -0
- package/dist/ui/screens/SetupScreen.js +73 -0
- package/dist/ui/sound.js +101 -0
- package/dist/ui/terminal.js +37 -0
- package/dist/ui/textUtils.js +54 -0
- package/dist/ui/theme.js +28 -0
- package/dist/version.js +3 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jingyuan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# QuizMe
|
|
2
|
+
|
|
3
|
+
QuizMe 是一个本地 CLI MVP,用于将 Claude Code 会话上下文、仓库上下文或用户输入主题转化为短小的面试风格选择题。
|
|
4
|
+
|
|
5
|
+
## 文档
|
|
6
|
+
|
|
7
|
+
- [产品文档](docs/product.md)
|
|
8
|
+
- [技术文档](docs/technical.md)
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @jiy/quizme
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
无需全局安装也可直接用 `npx`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @jiy/quizme
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 前置条件
|
|
23
|
+
|
|
24
|
+
题目生成和 `why` 模式会调用本机的 **Claude Code CLI**(`claude` 命令)。请确保已安装并位于 `PATH`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
claude --version # 能输出版本号即可
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
未安装可运行 `npm install -g @anthropic-ai/claude-code`,或参考 [Claude Code 文档](https://docs.anthropic.com/claude-code)。
|
|
31
|
+
|
|
32
|
+
> 无 `claude` 时仍可离线体验:`QUIZME_PROVIDER=local`。
|
|
33
|
+
|
|
34
|
+
## 使用方式
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
quizme
|
|
38
|
+
quizme --repo .
|
|
39
|
+
quizme "React rendering and caching"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 说明
|
|
43
|
+
|
|
44
|
+
- 默认模式会从 `~/.claude/projects` 读取当前仓库最近的 Claude Code transcript。统计、档案、设置、复习等功能通过交互式主界面进入。
|
|
45
|
+
- 本地数据存储在平台 app data 目录中,并使用 `sqlite3`。
|
|
46
|
+
- 在受限环境中,存储会 fallback 到 `./.quizme`。也可以通过 `QUIZME_DATA_DIR=/path/to/data` 覆盖数据目录。
|
|
47
|
+
- 题目生成和 `why` 模式会调用本地 `claude` CLI 的 print mode(`--bare` + `--tools ""`,禁用 agent tool;上下文已写入 prompt)。
|
|
48
|
+
- 离线 demo 可使用 `QUIZME_PROVIDER=local`。如果希望优先使用 Claude、失败后 fallback 到本地 provider,可使用 `QUIZME_PROVIDER_FALLBACK=local`。
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runInkSetup } from "../ui/renderApp.js";
|
|
2
|
+
export async function ensureConfig(store) {
|
|
3
|
+
const existing = store.getConfig("user");
|
|
4
|
+
if (existing) {
|
|
5
|
+
return normalizeConfig(existing);
|
|
6
|
+
}
|
|
7
|
+
const config = await runInkSetup({
|
|
8
|
+
onComplete: (next) => {
|
|
9
|
+
store.setConfig("user", next);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
return normalizeConfig(config);
|
|
13
|
+
}
|
|
14
|
+
export function normalizeConfig(config = {}) {
|
|
15
|
+
return {
|
|
16
|
+
level: config.level || "mid",
|
|
17
|
+
language: config.language || "en",
|
|
18
|
+
dailyGoal: Number(config.dailyGoal || 5),
|
|
19
|
+
soundEnabled: config.soundEnabled === true,
|
|
20
|
+
createdAt: config.createdAt || new Date().toISOString()
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function pickLevel(value) {
|
|
24
|
+
switch (String(value).trim()) {
|
|
25
|
+
case "1": return "junior";
|
|
26
|
+
case "3": return "senior";
|
|
27
|
+
case "4": return "staff";
|
|
28
|
+
default: return "mid";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function isValidLevel(value) {
|
|
32
|
+
return value === "junior" || value === "mid" || value === "senior" || value === "staff";
|
|
33
|
+
}
|
|
34
|
+
export function isValidLanguage(value) {
|
|
35
|
+
return value === "zh-CN" || value === "en";
|
|
36
|
+
}
|
|
37
|
+
export { pickLevel };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createStore } from "../storage/index.js";
|
|
4
|
+
import { ensureConfig } from "./config.js";
|
|
5
|
+
import { runQuizSession } from "./session.js";
|
|
6
|
+
import { getLatestClaudeSummary } from "../sources/claudeSession.js";
|
|
7
|
+
import { getRepoSummary } from "../sources/repository.js";
|
|
8
|
+
import { getTopicSummary } from "../sources/topic.js";
|
|
9
|
+
import { runInkHome } from "../ui/renderApp.js";
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const args = { _: [] };
|
|
12
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
13
|
+
const token = argv[i];
|
|
14
|
+
if (token === "--repo") {
|
|
15
|
+
args.repo = argv[i + 1];
|
|
16
|
+
i += 1;
|
|
17
|
+
}
|
|
18
|
+
else if (token === "--help" || token === "-h") {
|
|
19
|
+
args.help = true;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
args._.push(token);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return args;
|
|
26
|
+
}
|
|
27
|
+
function printHelp() {
|
|
28
|
+
console.log(`
|
|
29
|
+
quizme
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
quizme
|
|
33
|
+
quizme --repo .
|
|
34
|
+
quizme "React rendering"
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
async function main() {
|
|
38
|
+
const args = parseArgs(process.argv.slice(2));
|
|
39
|
+
if (args.help) {
|
|
40
|
+
printHelp();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const store = createStore();
|
|
44
|
+
const config = await ensureConfig(store);
|
|
45
|
+
if (args.repo || args._.length > 0) {
|
|
46
|
+
const source = resolveSource(args);
|
|
47
|
+
return runQuizSession({ store, config, source });
|
|
48
|
+
}
|
|
49
|
+
await runInkHome({ store, config, resolveSource });
|
|
50
|
+
}
|
|
51
|
+
function resolveSource(args) {
|
|
52
|
+
if (args.repo) {
|
|
53
|
+
return getRepoSummary(path.resolve(args.repo));
|
|
54
|
+
}
|
|
55
|
+
if (args._.length > 0) {
|
|
56
|
+
return getTopicSummary(args._.join(" "));
|
|
57
|
+
}
|
|
58
|
+
return getLatestClaudeSummary(process.cwd());
|
|
59
|
+
}
|
|
60
|
+
main().catch((error) => {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
console.error(`QuizMe error: ${message}`);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function dedupeQuestions(questions, recentQuestions) {
|
|
2
|
+
const seen = new Set(recentQuestions.map((item) => `${item.topic}:${item.question}`));
|
|
3
|
+
return questions.filter((question) => {
|
|
4
|
+
const key = `${question.topic}:${question.question}`;
|
|
5
|
+
if (seen.has(key)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
seen.add(key);
|
|
9
|
+
return true;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const QUESTION_SCHEMA = {
|
|
2
|
+
type: "object",
|
|
3
|
+
properties: {
|
|
4
|
+
questions: {
|
|
5
|
+
type: "array",
|
|
6
|
+
minItems: 3,
|
|
7
|
+
maxItems: 5,
|
|
8
|
+
items: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
id: { type: "string" },
|
|
12
|
+
sourceMode: {
|
|
13
|
+
type: "string",
|
|
14
|
+
enum: ["contextual", "adjacent", "interview_style"]
|
|
15
|
+
},
|
|
16
|
+
topic: { type: "string" },
|
|
17
|
+
difficulty: { type: "integer", minimum: 1, maximum: 5 },
|
|
18
|
+
question: { type: "string" },
|
|
19
|
+
choices: {
|
|
20
|
+
type: "array",
|
|
21
|
+
minItems: 4,
|
|
22
|
+
maxItems: 4,
|
|
23
|
+
items: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
id: { type: "string", enum: ["A", "B", "C", "D"] },
|
|
27
|
+
text: { type: "string" }
|
|
28
|
+
},
|
|
29
|
+
required: ["id", "text"]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
answer: { type: "string", enum: ["A", "B", "C", "D"] },
|
|
33
|
+
explanation: { type: "string" },
|
|
34
|
+
whyWrong: { type: "object" },
|
|
35
|
+
tags: {
|
|
36
|
+
type: "array",
|
|
37
|
+
minItems: 1,
|
|
38
|
+
items: { type: "string" }
|
|
39
|
+
},
|
|
40
|
+
followUps: {
|
|
41
|
+
type: "array",
|
|
42
|
+
items: { type: "string" }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
required: ["id", "sourceMode", "topic", "difficulty", "question", "choices", "answer", "explanation", "whyWrong", "tags", "followUps"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
required: ["questions"]
|
|
50
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const VALID_CHOICE_IDS = ["A", "B", "C", "D"];
|
|
2
|
+
const VALID_SOURCE_MODES = [
|
|
3
|
+
"contextual",
|
|
4
|
+
"adjacent",
|
|
5
|
+
"interview_style"
|
|
6
|
+
];
|
|
7
|
+
export class QuestionValidationError extends Error {
|
|
8
|
+
issues;
|
|
9
|
+
constructor(issues) {
|
|
10
|
+
super(`Invalid question payload: ${issues.join("; ")}`);
|
|
11
|
+
this.name = "QuestionValidationError";
|
|
12
|
+
this.issues = issues;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function isChoice(value) {
|
|
19
|
+
return isRecord(value)
|
|
20
|
+
&& typeof value.id === "string"
|
|
21
|
+
&& VALID_CHOICE_IDS.includes(value.id)
|
|
22
|
+
&& typeof value.text === "string"
|
|
23
|
+
&& value.text.trim().length > 0;
|
|
24
|
+
}
|
|
25
|
+
function validateOne(raw, index, issues) {
|
|
26
|
+
const at = `questions[${index}]`;
|
|
27
|
+
if (!isRecord(raw)) {
|
|
28
|
+
issues.push(`${at} is not an object`);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const errs = [];
|
|
32
|
+
if (typeof raw.id !== "string" || !raw.id.trim()) {
|
|
33
|
+
errs.push(`${at}.id missing`);
|
|
34
|
+
}
|
|
35
|
+
if (typeof raw.topic !== "string" || !raw.topic.trim()) {
|
|
36
|
+
errs.push(`${at}.topic missing`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof raw.question !== "string" || raw.question.trim().length < 4) {
|
|
39
|
+
errs.push(`${at}.question missing or too short`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof raw.explanation !== "string" || !raw.explanation.trim()) {
|
|
42
|
+
errs.push(`${at}.explanation missing`);
|
|
43
|
+
}
|
|
44
|
+
const sourceMode = raw.sourceMode;
|
|
45
|
+
if (typeof sourceMode !== "string" || !VALID_SOURCE_MODES.includes(sourceMode)) {
|
|
46
|
+
errs.push(`${at}.sourceMode must be one of ${VALID_SOURCE_MODES.join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
const difficulty = Number(raw.difficulty);
|
|
49
|
+
if (!Number.isInteger(difficulty) || difficulty < 1 || difficulty > 5) {
|
|
50
|
+
errs.push(`${at}.difficulty must be integer 1..5`);
|
|
51
|
+
}
|
|
52
|
+
const choices = raw.choices;
|
|
53
|
+
if (!Array.isArray(choices) || choices.length !== 4) {
|
|
54
|
+
errs.push(`${at}.choices must be exactly 4 items`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const ids = new Set();
|
|
58
|
+
choices.forEach((choice, i) => {
|
|
59
|
+
if (!isChoice(choice)) {
|
|
60
|
+
errs.push(`${at}.choices[${i}] invalid`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (ids.has(choice.id)) {
|
|
64
|
+
errs.push(`${at}.choices[${i}] duplicate id ${choice.id}`);
|
|
65
|
+
}
|
|
66
|
+
ids.add(choice.id);
|
|
67
|
+
});
|
|
68
|
+
if (VALID_CHOICE_IDS.some((id) => !ids.has(id))) {
|
|
69
|
+
errs.push(`${at}.choices must include ids A, B, C, D`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const answer = raw.answer;
|
|
73
|
+
if (typeof answer !== "string" || !VALID_CHOICE_IDS.includes(answer)) {
|
|
74
|
+
errs.push(`${at}.answer must be one of A, B, C, D`);
|
|
75
|
+
}
|
|
76
|
+
const whyWrong = raw.whyWrong;
|
|
77
|
+
if (!isRecord(whyWrong)) {
|
|
78
|
+
errs.push(`${at}.whyWrong must be an object`);
|
|
79
|
+
}
|
|
80
|
+
else if (typeof answer === "string") {
|
|
81
|
+
for (const id of VALID_CHOICE_IDS) {
|
|
82
|
+
if (id === answer)
|
|
83
|
+
continue;
|
|
84
|
+
if (typeof whyWrong[id] !== "string" || !whyWrong[id].trim()) {
|
|
85
|
+
errs.push(`${at}.whyWrong.${id} missing`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const tags = raw.tags;
|
|
90
|
+
if (!Array.isArray(tags) || tags.length === 0 || tags.some((t) => typeof t !== "string" || !t.trim())) {
|
|
91
|
+
errs.push(`${at}.tags must be a non-empty array of strings`);
|
|
92
|
+
}
|
|
93
|
+
const followUps = raw.followUps;
|
|
94
|
+
if (!Array.isArray(followUps) || followUps.some((t) => typeof t !== "string")) {
|
|
95
|
+
errs.push(`${at}.followUps must be an array of strings`);
|
|
96
|
+
}
|
|
97
|
+
if (errs.length) {
|
|
98
|
+
issues.push(...errs);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
id: raw.id.trim(),
|
|
103
|
+
sourceMode: sourceMode,
|
|
104
|
+
topic: raw.topic.trim(),
|
|
105
|
+
difficulty,
|
|
106
|
+
question: raw.question.trim(),
|
|
107
|
+
choices: choices.map((c) => ({ id: c.id, text: c.text.trim() })),
|
|
108
|
+
answer: answer,
|
|
109
|
+
explanation: raw.explanation.trim(),
|
|
110
|
+
whyWrong: whyWrong,
|
|
111
|
+
tags: tags.map((t) => t.trim()),
|
|
112
|
+
followUps: followUps.map((t) => t.trim())
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function validateQuestions(payload) {
|
|
116
|
+
if (!isRecord(payload)) {
|
|
117
|
+
throw new QuestionValidationError(["payload is not an object"]);
|
|
118
|
+
}
|
|
119
|
+
const raw = payload.questions;
|
|
120
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
121
|
+
throw new QuestionValidationError(["questions must be a non-empty array"]);
|
|
122
|
+
}
|
|
123
|
+
const issues = [];
|
|
124
|
+
const valid = [];
|
|
125
|
+
raw.forEach((item, index) => {
|
|
126
|
+
const q = validateOne(item, index, issues);
|
|
127
|
+
if (q)
|
|
128
|
+
valid.push(q);
|
|
129
|
+
});
|
|
130
|
+
if (!valid.length) {
|
|
131
|
+
throw new QuestionValidationError(issues.length ? issues : ["no valid questions"]);
|
|
132
|
+
}
|
|
133
|
+
return valid;
|
|
134
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function ensureDir(dirPath) {
|
|
4
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
5
|
+
}
|
|
6
|
+
export function readJson(filePath, fallback = null) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function writeJson(filePath, value) {
|
|
15
|
+
ensureDir(path.dirname(filePath));
|
|
16
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function getAppDataDir() {
|
|
4
|
+
if (process.env.QUIZME_DATA_DIR) {
|
|
5
|
+
return process.env.QUIZME_DATA_DIR;
|
|
6
|
+
}
|
|
7
|
+
const home = os.homedir();
|
|
8
|
+
if (process.platform === "darwin") {
|
|
9
|
+
return path.join(home, "Library", "Application Support", "quizme");
|
|
10
|
+
}
|
|
11
|
+
if (process.platform === "win32") {
|
|
12
|
+
return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "quizme");
|
|
13
|
+
}
|
|
14
|
+
return path.join(process.env.XDG_DATA_HOME || path.join(home, ".local", "share"), "quizme");
|
|
15
|
+
}
|
|
16
|
+
export function slugifyProjectPath(projectPath) {
|
|
17
|
+
return projectPath.replace(/[:\\/]+/g, "-").replace(/_/g, "-");
|
|
18
|
+
}
|
|
19
|
+
export function getClaudeRoots(cwd = process.cwd()) {
|
|
20
|
+
return [
|
|
21
|
+
path.join(cwd, ".claude"),
|
|
22
|
+
path.join(os.homedir(), ".claude")
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
export function getClaudeProjectsDir() {
|
|
26
|
+
return path.join(os.homedir(), ".claude", "projects");
|
|
27
|
+
}
|