echoctl 0.1.0 → 0.1.3

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.
Files changed (25) hide show
  1. package/docs/.vitepress/config.mts +38 -0
  2. package/docs/.vitepress/echo-sidebar.mts +52 -0
  3. package/docs/.vitepress/theme/Layout.vue +28 -0
  4. package/docs/.vitepress/theme/components/EchoArticleActions.vue +235 -0
  5. package/docs/.vitepress/theme/components/EchoChatBubbles.vue +153 -0
  6. package/docs/.vitepress/theme/components/EchoClaudeImportBanner.vue +335 -0
  7. package/docs/.vitepress/theme/components/EchoCommentChain.vue +79 -0
  8. package/docs/.vitepress/theme/components/EchoCommentNode.vue +126 -0
  9. package/docs/.vitepress/theme/components/EchoCommentReplies.vue +95 -0
  10. package/docs/.vitepress/theme/components/EchoGlobalControls.vue +91 -0
  11. package/docs/.vitepress/theme/components/EchoLegacyRecovery.vue +178 -0
  12. package/docs/.vitepress/theme/components/EchoLiveSession.vue +82 -0
  13. package/docs/.vitepress/theme/components/EchoProjectTabs.vue +132 -0
  14. package/docs/.vitepress/theme/components/EchoSearchLanding.vue +215 -0
  15. package/docs/.vitepress/theme/components/EchoSelectionComment.vue +129 -0
  16. package/docs/.vitepress/theme/components/EchoTagsPage.vue +301 -0
  17. package/docs/.vitepress/theme/custom.css +964 -0
  18. package/docs/.vitepress/theme/index.ts +36 -0
  19. package/docs/.vitepress/theme/lib/echo-api.ts +298 -0
  20. package/docs/.vitepress/theme/lib/echo-heartbeat.ts +34 -0
  21. package/docs/.vitepress/theme/lib/useEchoStatus.ts +59 -0
  22. package/docs/.vitepress/theme/lib/useProjectFilter.ts +42 -0
  23. package/docs/index.md +89 -0
  24. package/package.json +13 -4
  25. package/scripts/build-docs.js +1 -1
@@ -0,0 +1,36 @@
1
+ import DefaultTheme from 'vitepress/theme'
2
+ import Layout from './Layout.vue'
3
+ import EchoArticleActions from './components/EchoArticleActions.vue'
4
+ import EchoSelectionComment from './components/EchoSelectionComment.vue'
5
+ import EchoSearchLanding from './components/EchoSearchLanding.vue'
6
+ import EchoTagsPage from './components/EchoTagsPage.vue'
7
+ import EchoChatBubbles from './components/EchoChatBubbles.vue'
8
+ import EchoProjectTabs from './components/EchoProjectTabs.vue'
9
+ import EchoCommentChain from './components/EchoCommentChain.vue'
10
+ import EchoCommentNode from './components/EchoCommentNode.vue'
11
+ // [LIVE_SESSION_DISABLED] 后期恢复时取消注释
12
+ // import EchoLiveSession from './components/EchoLiveSession.vue'
13
+ import EchoLegacyRecovery from './components/EchoLegacyRecovery.vue'
14
+ import EchoGlobalControls from './components/EchoGlobalControls.vue'
15
+ import EchoClaudeImportBanner from './components/EchoClaudeImportBanner.vue'
16
+ import './custom.css'
17
+
18
+ export default {
19
+ extends: DefaultTheme,
20
+ Layout,
21
+ enhanceApp({ app }: any) {
22
+ app.component('EchoArticleActions', EchoArticleActions)
23
+ app.component('EchoSelectionComment', EchoSelectionComment)
24
+ app.component('EchoSearchLanding', EchoSearchLanding)
25
+ app.component('EchoTagsPage', EchoTagsPage)
26
+ app.component('EchoChatBubbles', EchoChatBubbles)
27
+ app.component('EchoProjectTabs', EchoProjectTabs)
28
+ app.component('EchoCommentChain', EchoCommentChain)
29
+ app.component('EchoCommentNode', EchoCommentNode)
30
+ // [LIVE_SESSION_DISABLED] 后期恢复时取消注释
31
+ // app.component('EchoLiveSession', EchoLiveSession)
32
+ app.component('EchoLegacyRecovery', EchoLegacyRecovery)
33
+ app.component('EchoGlobalControls', EchoGlobalControls)
34
+ app.component('EchoClaudeImportBanner', EchoClaudeImportBanner)
35
+ },
36
+ }
@@ -0,0 +1,298 @@
1
+ const CONFIGURED_API_BASE = (typeof import.meta !== 'undefined' && import.meta.env?.VITE_ECHO_API_BASE) || ''
2
+ const DEFAULT_API_BASE = 'http://127.0.0.1:8787'
3
+ const API_CANDIDATES = [CONFIGURED_API_BASE, DEFAULT_API_BASE].filter(Boolean)
4
+
5
+ interface EchoStatus {
6
+ ok: boolean
7
+ captureEnabled: boolean
8
+ projectId: string | null
9
+ version: string
10
+ author: string
11
+ }
12
+
13
+ interface CommentPayload {
14
+ articleId: string
15
+ comment: string
16
+ quote?: string
17
+ prefix?: string
18
+ suffix?: string
19
+ occurrence?: number
20
+ author?: string
21
+ scope?: string
22
+ evolutionOf?: string[]
23
+ evolutionKind?: string
24
+ projectId?: string | null
25
+ }
26
+
27
+ interface TagPayload {
28
+ articleId: string
29
+ tag: string
30
+ projectId?: string | null
31
+ }
32
+
33
+ interface PublishPayload {
34
+ sessionId: string
35
+ projectId?: string | null
36
+ }
37
+
38
+ interface PublishResponse {
39
+ ok: boolean
40
+ id: string
41
+ slug: string
42
+ turnCount: number
43
+ version: number
44
+ latest: boolean
45
+ }
46
+
47
+ interface LiveSessionState {
48
+ ok: boolean
49
+ exists: boolean
50
+ projectId: string | null
51
+ sessionId: string
52
+ turnCount: number
53
+ hash: string | null
54
+ updatedAt: string | null
55
+ }
56
+
57
+ interface LegacyCandidate {
58
+ sessionId: string
59
+ fileName: string
60
+ sourcePath: string
61
+ turnCount: number
62
+ mtime: string
63
+ confidence: string
64
+ evidence: { kind: string; projectRoot: string } | null
65
+ }
66
+
67
+ interface LegacyCandidatesResponse {
68
+ projectId: string
69
+ sourceDir: string
70
+ candidates: LegacyCandidate[]
71
+ }
72
+
73
+ interface ClaudeImportCandidate {
74
+ sessionId: string
75
+ filePath: string
76
+ status: string
77
+ articleId: string
78
+ turnCount: number
79
+ mtime: string
80
+ fileHash: string
81
+ }
82
+
83
+ interface ClaudeImportCandidatesResponse {
84
+ projectId: string
85
+ provider: string
86
+ projectDir: string
87
+ summary: { total: number; new: number; updated: number; skipped: number }
88
+ candidates: ClaudeImportCandidate[]
89
+ }
90
+
91
+ interface ClaudeImportResponse {
92
+ ok: boolean
93
+ imported: number
94
+ skipped: number
95
+ articlesDir: string | null
96
+ refreshScheduled: boolean
97
+ }
98
+
99
+ interface LegacyMigrateResponse {
100
+ ok: boolean
101
+ migrated: number
102
+ skipped: number
103
+ targetDir: string
104
+ refreshScheduled: boolean
105
+ }
106
+
107
+ class EchoApiError extends Error {
108
+ status: number
109
+ constructor(message: string, status: number) {
110
+ super(message)
111
+ this.name = 'EchoApiError'
112
+ this.status = status
113
+ }
114
+ }
115
+
116
+ async function request<T>(path: string, options?: RequestInit): Promise<T> {
117
+ if (API_CANDIDATES.length === 0) {
118
+ throw new EchoApiError('API not available', 0)
119
+ }
120
+
121
+ let lastError: EchoApiError | null = null
122
+ for (const base of API_CANDIDATES) {
123
+ try {
124
+ return await requestFrom<T>(base, path, options)
125
+ } catch (err) {
126
+ lastError = err instanceof EchoApiError ? err : new EchoApiError((err as Error).message || 'Network error', 0)
127
+ }
128
+ }
129
+
130
+ throw lastError || new EchoApiError('Network error', 0)
131
+ }
132
+
133
+ async function requestFrom<T>(base: string, path: string, options?: RequestInit): Promise<T> {
134
+ const controller = new AbortController()
135
+ const timeout = setTimeout(() => controller.abort(), 5000)
136
+ try {
137
+ const res = await fetch(`${base}${path}`, {
138
+ ...options,
139
+ signal: controller.signal,
140
+ headers: { 'Content-Type': 'application/json', ...options?.headers },
141
+ })
142
+ if (!res.ok) {
143
+ const body = await res.json().catch(() => ({}))
144
+ throw new EchoApiError((body as any).error || res.statusText, res.status)
145
+ }
146
+ return res.json() as Promise<T>
147
+ } catch (err) {
148
+ if (err instanceof EchoApiError) throw err
149
+ if ((err as Error).name === 'AbortError') {
150
+ throw new EchoApiError('Request timed out', 0)
151
+ }
152
+ throw new EchoApiError((err as Error).message || 'Network error', 0)
153
+ } finally {
154
+ clearTimeout(timeout)
155
+ }
156
+ }
157
+
158
+ export function getStatus(): Promise<EchoStatus> {
159
+ return request<EchoStatus>('/api/status')
160
+ }
161
+
162
+ export function getCaptureStatus(): Promise<{ enabled: boolean }> {
163
+ return request<{ enabled: boolean }>('/api/capture')
164
+ }
165
+
166
+ export function setCapture(enabled: boolean): Promise<{ enabled: boolean }> {
167
+ return request<{ enabled: boolean }>('/api/capture', {
168
+ method: 'POST',
169
+ body: JSON.stringify({ enabled }),
170
+ })
171
+ }
172
+
173
+ export function getMcpConfig(): Promise<{ canonical: { command: string; args: string[] }; legacy: any[]; serverInfo: any }> {
174
+ return request('/api/mcp-config')
175
+ }
176
+
177
+ export function postComment(payload: CommentPayload): Promise<any> {
178
+ return request('/api/comments', {
179
+ method: 'POST',
180
+ body: JSON.stringify(payload),
181
+ })
182
+ }
183
+
184
+ export function postTag(payload: TagPayload): Promise<any> {
185
+ return request('/api/tags', {
186
+ method: 'POST',
187
+ body: JSON.stringify(payload),
188
+ })
189
+ }
190
+
191
+ interface RemoveTagPayload {
192
+ articleId: string
193
+ tags: string[]
194
+ projectId?: string | null
195
+ }
196
+
197
+ export function removeTags(payload: RemoveTagPayload): Promise<any> {
198
+ return request('/api/tags/remove', {
199
+ method: 'POST',
200
+ body: JSON.stringify(payload),
201
+ })
202
+ }
203
+
204
+ interface RenameTagPayload {
205
+ oldTag: string
206
+ newTag: string
207
+ projectId?: string | null
208
+ }
209
+
210
+ interface RenameTagResponse {
211
+ oldTag: string
212
+ newTag: string
213
+ renamed: number
214
+ }
215
+
216
+ export function renameTag(payload: RenameTagPayload): Promise<RenameTagResponse> {
217
+ return request<RenameTagResponse>('/api/tags/rename', {
218
+ method: 'POST',
219
+ body: JSON.stringify(payload),
220
+ })
221
+ }
222
+
223
+ interface PurgeTagPayload {
224
+ tag: string
225
+ projectId?: string | null
226
+ }
227
+
228
+ interface PurgeTagResponse {
229
+ tag: string
230
+ purged: number
231
+ }
232
+
233
+ export function purgeTag(payload: PurgeTagPayload): Promise<PurgeTagResponse> {
234
+ return request<PurgeTagResponse>('/api/tags/purge', {
235
+ method: 'POST',
236
+ body: JSON.stringify(payload),
237
+ })
238
+ }
239
+
240
+ interface SummaryPayload {
241
+ articleId: string
242
+ summary: string
243
+ projectId?: string | null
244
+ }
245
+
246
+ export function updateSummary(payload: SummaryPayload): Promise<any> {
247
+ return request('/api/summary', {
248
+ method: 'POST',
249
+ body: JSON.stringify(payload),
250
+ })
251
+ }
252
+
253
+ export function postPublish(payload: PublishPayload): Promise<PublishResponse> {
254
+ return request<PublishResponse>('/api/publish', {
255
+ method: 'POST',
256
+ body: JSON.stringify(payload),
257
+ })
258
+ }
259
+
260
+ export function getLiveSessionState(projectId: string | null, sessionId: string): Promise<LiveSessionState> {
261
+ const params = new URLSearchParams({ sessionId })
262
+ if (projectId) params.set('projectId', projectId)
263
+ return request<LiveSessionState>(`/api/live-session-state?${params.toString()}`)
264
+ }
265
+
266
+ export function getLegacyCandidates(projectId: string): Promise<LegacyCandidatesResponse> {
267
+ return request<LegacyCandidatesResponse>(`/api/legacy-candidates?projectId=${encodeURIComponent(projectId)}`)
268
+ }
269
+
270
+ export function migrateLegacyCandidates(projectId: string, candidateIds?: string[]): Promise<LegacyMigrateResponse> {
271
+ return request<LegacyMigrateResponse>('/api/legacy-candidates/migrate', {
272
+ method: 'POST',
273
+ body: JSON.stringify({ projectId, candidateIds }),
274
+ })
275
+ }
276
+
277
+ interface ProjectsResponse {
278
+ projects: { id: string; name: string; root: string; dataRoot: string }[]
279
+ currentId: string | null
280
+ }
281
+
282
+ export function getProjects(): Promise<ProjectsResponse> {
283
+ return request<ProjectsResponse>('/api/projects')
284
+ }
285
+
286
+ export function getClaudeImportCandidates(projectId: string): Promise<ClaudeImportCandidatesResponse> {
287
+ return request<ClaudeImportCandidatesResponse>(`/api/import/claude-candidates?projectId=${encodeURIComponent(projectId)}`)
288
+ }
289
+
290
+ export function importClaudeSessions(projectId: string, sessionIds: string[]): Promise<ClaudeImportResponse> {
291
+ return request<ClaudeImportResponse>('/api/import/claude', {
292
+ method: 'POST',
293
+ body: JSON.stringify({ projectId, sessionIds }),
294
+ })
295
+ }
296
+
297
+ export { EchoApiError, CONFIGURED_API_BASE, DEFAULT_API_BASE }
298
+ export type { EchoStatus, CommentPayload, TagPayload, RemoveTagPayload, RenameTagPayload, RenameTagResponse, PurgeTagPayload, PurgeTagResponse, SummaryPayload, PublishPayload, PublishResponse, LiveSessionState, LegacyCandidate, LegacyCandidatesResponse, LegacyMigrateResponse, ClaudeImportCandidate, ClaudeImportCandidatesResponse, ClaudeImportResponse }
@@ -0,0 +1,34 @@
1
+ type Subscriber = () => void | Promise<void>
2
+
3
+ const subscribers = new Set<Subscriber>()
4
+ let timer: ReturnType<typeof window.setInterval> | null = null
5
+ const intervalMs = 10000
6
+
7
+ function tick() {
8
+ if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return
9
+ for (const subscriber of Array.from(subscribers)) {
10
+ void subscriber()
11
+ }
12
+ }
13
+
14
+ function start() {
15
+ if (typeof window === 'undefined' || timer) return
16
+ timer = window.setInterval(tick, intervalMs)
17
+ }
18
+
19
+ function stop() {
20
+ if (!timer || subscribers.size > 0) return
21
+ window.clearInterval(timer)
22
+ timer = null
23
+ }
24
+
25
+ export function subscribeEchoHeartbeat(subscriber: Subscriber): () => void {
26
+ subscribers.add(subscriber)
27
+ start()
28
+ return () => {
29
+ subscribers.delete(subscriber)
30
+ stop()
31
+ }
32
+ }
33
+
34
+ export { intervalMs as echoHeartbeatIntervalMs }
@@ -0,0 +1,59 @@
1
+ import { ref, watch, onMounted, onUnmounted, type Ref } from 'vue'
2
+ import { getStatus, EchoApiError, type EchoStatus } from './echo-api'
3
+
4
+ type StatusState = 'loading' | 'ready' | 'unavailable'
5
+
6
+ export function useEchoStatus(articleId: Ref<string | undefined>) {
7
+ const state = ref<StatusState>('loading')
8
+ const status = ref<EchoStatus | null>(null)
9
+ const error = ref<string>('')
10
+ let controller: AbortController | null = null
11
+ let retryTimer: ReturnType<typeof setTimeout> | null = null
12
+
13
+ async function check() {
14
+ controller?.abort()
15
+ controller = new AbortController()
16
+ state.value = 'loading'
17
+ error.value = ''
18
+
19
+ try {
20
+ const result = await getStatus()
21
+ status.value = result
22
+ state.value = 'ready'
23
+ } catch (err) {
24
+ if ((err as Error).name === 'AbortError' || (err as Error).message?.includes('abort')) return
25
+ state.value = 'unavailable'
26
+ error.value = err instanceof EchoApiError ? err.message : '无法连接 Echo 服务'
27
+ scheduleRetry()
28
+ }
29
+ }
30
+
31
+ function scheduleRetry() {
32
+ if (retryTimer) clearTimeout(retryTimer)
33
+ retryTimer = setTimeout(check, 30000)
34
+ }
35
+
36
+ function cancel() {
37
+ controller?.abort()
38
+ controller = null
39
+ if (retryTimer) {
40
+ clearTimeout(retryTimer)
41
+ retryTimer = null
42
+ }
43
+ }
44
+
45
+ onMounted(() => {
46
+ check()
47
+ })
48
+
49
+ watch(articleId, () => {
50
+ cancel()
51
+ check()
52
+ })
53
+
54
+ onUnmounted(() => {
55
+ cancel()
56
+ })
57
+
58
+ return { state, status, error, retry: check }
59
+ }
@@ -0,0 +1,42 @@
1
+ import { ref, computed } from 'vue'
2
+ import { getProjects } from './echo-api'
3
+
4
+ interface Project {
5
+ id: string
6
+ name: string
7
+ root: string
8
+ dataRoot: string
9
+ }
10
+
11
+ // Module-level state for cross-component sharing
12
+ const selected = ref<string>('__all__')
13
+ const projects = ref<Project[]>([])
14
+ const loaded = ref(false)
15
+
16
+ export function useProjectFilter() {
17
+ const selectedProject = computed(() => selected.value)
18
+ const allProjects = computed(() => projects.value)
19
+
20
+ async function load() {
21
+ if (loaded.value) return
22
+ try {
23
+ const result = await getProjects()
24
+ projects.value = result.projects || []
25
+ loaded.value = true
26
+ } catch (_) {}
27
+ }
28
+
29
+ function select(projectId: string) {
30
+ selected.value = projectId
31
+ try { localStorage.setItem('echo-project-filter', projectId) } catch (_) {}
32
+ }
33
+
34
+ function restore() {
35
+ try {
36
+ const saved = localStorage.getItem('echo-project-filter')
37
+ if (saved) selected.value = saved
38
+ } catch (_) {}
39
+ }
40
+
41
+ return { selectedProject, allProjects, load, select, restore }
42
+ }
package/docs/index.md ADDED
@@ -0,0 +1,89 @@
1
+ ---
2
+ layout: home
3
+
4
+ hero:
5
+ name: "Echo"
6
+ text: "本地优先的 AI 对话知识论坛"
7
+ tagline: 把 AI 编程会话捕获为不可变的 Markdown 文章,支持浏览、搜索、标签、评论、MCP 访问。
8
+ actions:
9
+ - theme: brand
10
+ text: 快速开始
11
+ link: /../GETTING_STARTED.md
12
+ - theme: alt
13
+ text: 完整文档
14
+ link: /../README.md
15
+
16
+ features:
17
+ - icon: 📝
18
+ title: 不可变归档
19
+ details: 文章正文一旦创建即不可修改。AI 对话作为源记录永久保存,后续整理通过标签、评论、标注完成。
20
+ - icon: 🔴
21
+ title: Live Session 实时预览
22
+ details: 正在聊天的内容通过 live session 页面实时查看,前端心跳自动检测更新,无需手动刷新。
23
+ - icon: 🤖
24
+ title: MCP AI 接口
25
+ details: 9 个 MCP 工具让 AI 助手直接读取、搜索 Echo 本地归档,成为 AI 的长期记忆。
26
+ - icon: 📁
27
+ title: 多项目支持
28
+ details: 每个项目独立管理,会话自动归入对应项目。空目录需显式注册,未注册目录降级写入 legacy buffer。
29
+ - icon: 🔍
30
+ title: 全文搜索
31
+ details: 本地搜索索引,通过 CLI 或网页快速找到历史对话中的关键信息。
32
+ - icon: 🏷️
33
+ title: 标签管理
34
+ details: 为文章打标签、分类整理,支持添加、移除、重命名、删除标签,构建个人知识体系。
35
+ ---
36
+
37
+ ## 一分钟了解 Echo
38
+
39
+ Echo 在你和 AI 聊天时自动捕获会话,转成 Markdown 文章,并在本地网页上展示。你不需要手动整理——hook 会静默工作。
40
+
41
+ **安装只需三步:**
42
+
43
+ ```bash
44
+ cd echo-prototype && npm install && npm link # 安装 CLI
45
+ echoctl init && echoctl init project # 注册项目
46
+ echoctl hook install claude --write # 安装捕获 hook
47
+ ```
48
+
49
+ 然后启动网页:
50
+
51
+ ```bash
52
+ echoctl serve
53
+ ```
54
+
55
+ 浏览器打开显示的地址,就能看到你的 AI 对话文章了。
56
+
57
+ **核心命令速览:**
58
+
59
+ | 命令 | 用途 |
60
+ |---|---|
61
+ | `echoctl serve` | 后台启动本地网页和 API |
62
+ | `echoctl stop` | 停止后台服务 |
63
+ | `echoctl status` | 查看 Echo 全面状态 |
64
+ | `echoctl all` | 手动跑完整管线(convert → validate → index) |
65
+ | `echoctl refresh` | 不重启 serve 刷新页面 |
66
+ | `echoctl search -- --keyword "关键词"` | 全文搜索文章 |
67
+ | `echoctl tag add <id> <tag>` | 给文章加标签 |
68
+ | `echoctl project list` | 查看已注册项目 |
69
+
70
+ **MCP 接入(让 AI 读你的归档):**
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "echo": {
76
+ "command": "echoctl",
77
+ "args": ["mcp"]
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ 配置后 AI 助手即可通过 `search_articles`、`get_article`、`list_recent` 等工具访问你的 Echo 归档。
84
+
85
+ ---
86
+
87
+ > **状态:开发中,未发布 npm。** 当前通过 `npm link` 使用开发版。
88
+ >
89
+ > 更多文档:[上手指南](/../GETTING_STARTED.md) · [完整 README](https://github.com/daxiguaguagua-hash/echo/blob/main/README.md) · [使用指南 V3](/../USAGE_GUIDE_V3.md) · [工程边界](/../ENGINEERING_BOUNDARIES.md) · [项目进度](/../ECHO_STATUS.md)
package/package.json CHANGED
@@ -1,9 +1,19 @@
1
1
  {
2
2
  "name": "echoctl",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
+ "license": "MIT",
5
6
  "description": "Local-first AI conversation knowledge forum — capture, search, and annotate Claude Code sessions as Markdown articles with MCP server support",
6
- "keywords": ["echo", "knowledge-forum", "ai-conversations", "markdown", "local-first", "mcp", "claude-code", "cli"],
7
+ "keywords": [
8
+ "echo",
9
+ "knowledge-forum",
10
+ "ai-conversations",
11
+ "markdown",
12
+ "local-first",
13
+ "mcp",
14
+ "claude-code",
15
+ "cli"
16
+ ],
7
17
  "engines": {
8
18
  "node": ">=18.0.0"
9
19
  },
@@ -51,6 +61,5 @@
51
61
  "gray-matter": "^4.0.3",
52
62
  "vitepress": "^1.6.4"
53
63
  },
54
- "devDependencies": {
55
- }
64
+ "devDependencies": {}
56
65
  }
@@ -7,7 +7,7 @@ const store = require("./lib/infra/markdown-store");
7
7
  const { stripCommentSections } = require("./lib/usecases/strip-comments");
8
8
  const { TURN_MARKER_REGEX } = require("./lib/domain/echo-format");
9
9
 
10
- const PACKAGE_DOCS_ROOT = path.resolve(__dirname, "../../docs");
10
+ const PACKAGE_DOCS_ROOT = path.resolve(__dirname, "../docs");
11
11
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
12
12
 
13
13
  function defaultDocsRoot() {