echoctl 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 +171 -0
- package/bin/echoctl.js +2 -0
- package/package.json +56 -0
- package/scripts/annotate.js +73 -0
- package/scripts/build-docs.js +805 -0
- package/scripts/cli/commands/capture.js +20 -0
- package/scripts/cli/commands/constants.js +70 -0
- package/scripts/cli/commands/doctor.js +10 -0
- package/scripts/cli/commands/helpers.js +27 -0
- package/scripts/cli/commands/hook.js +48 -0
- package/scripts/cli/commands/import_cmd.js +184 -0
- package/scripts/cli/commands/init.js +45 -0
- package/scripts/cli/commands/mcp.js +16 -0
- package/scripts/cli/commands/migrate.js +65 -0
- package/scripts/cli/commands/pipeline.js +26 -0
- package/scripts/cli/commands/project.js +35 -0
- package/scripts/cli/commands/refresh.js +14 -0
- package/scripts/cli/commands/search.js +28 -0
- package/scripts/cli/commands/serve.js +73 -0
- package/scripts/cli/commands/status.js +11 -0
- package/scripts/cli/commands/stop.js +136 -0
- package/scripts/cli/commands/tag.js +89 -0
- package/scripts/cli/echoctl.js +44 -0
- package/scripts/convert.js +55 -0
- package/scripts/import-sessions.js +213 -0
- package/scripts/index.js +92 -0
- package/scripts/lib/cli/names.js +33 -0
- package/scripts/lib/domain/anchor.js +78 -0
- package/scripts/lib/domain/echo-format.js +265 -0
- package/scripts/lib/domain/errors.js +8 -0
- package/scripts/lib/domain/validation.js +126 -0
- package/scripts/lib/hooks/capture.js +401 -0
- package/scripts/lib/hooks/status.js +78 -0
- package/scripts/lib/i18n/format.js +183 -0
- package/scripts/lib/i18n/messages/en.js +41 -0
- package/scripts/lib/i18n/messages/zh-CN.js +40 -0
- package/scripts/lib/import/manifest.js +87 -0
- package/scripts/lib/import/providers/claude-code.js +272 -0
- package/scripts/lib/import/scanner.js +128 -0
- package/scripts/lib/infra/config.js +36 -0
- package/scripts/lib/infra/echo-paths.js +44 -0
- package/scripts/lib/infra/markdown-store.js +161 -0
- package/scripts/lib/infra/query-log.js +27 -0
- package/scripts/lib/infra/read-stdin.js +11 -0
- package/scripts/lib/infra/workspace.js +93 -0
- package/scripts/lib/interfaces/mcp/server.js +151 -0
- package/scripts/lib/interfaces/mcp/tools.js +152 -0
- package/scripts/lib/mcp-server.js +3 -0
- package/scripts/lib/usecases/aggregate-all-projects.js +45 -0
- package/scripts/lib/usecases/convert-buffer.js +43 -0
- package/scripts/lib/usecases/discover-claude-imports.js +80 -0
- package/scripts/lib/usecases/import-claude-project.js +89 -0
- package/scripts/lib/usecases/init-workspace.js +52 -0
- package/scripts/lib/usecases/install-claude-hook.js +139 -0
- package/scripts/lib/usecases/legacy-candidates.js +134 -0
- package/scripts/lib/usecases/live-session-state.js +109 -0
- package/scripts/lib/usecases/migrate-legacy-buffer.js +209 -0
- package/scripts/lib/usecases/project-registry.js +170 -0
- package/scripts/lib/usecases/query-articles.js +380 -0
- package/scripts/lib/usecases/refresh-serve.js +77 -0
- package/scripts/lib/usecases/run-doctor.js +213 -0
- package/scripts/lib/usecases/run-pipeline.js +104 -0
- package/scripts/lib/usecases/snapshot-manifest.js +48 -0
- package/scripts/lib/usecases/status-collector.js +142 -0
- package/scripts/lib/usecases/strip-comments.js +7 -0
- package/scripts/lib/usecases/write-comment.js +122 -0
- package/scripts/resolve.js +65 -0
- package/scripts/search.js +98 -0
- package/scripts/serve.js +778 -0
- package/scripts/validate.js +79 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const { commandFor } = require("../../lib/cli/names");
|
|
2
|
+
|
|
3
|
+
function run(args) {
|
|
4
|
+
const action = args[1];
|
|
5
|
+
const { isCaptureEnabled, setCaptureEnabled } = require("../../lib/infra/config");
|
|
6
|
+
if (action === "on") {
|
|
7
|
+
const r = setCaptureEnabled(true);
|
|
8
|
+
console.log(`Capture enabled (${r.configPath})`);
|
|
9
|
+
} else if (action === "off") {
|
|
10
|
+
const r = setCaptureEnabled(false);
|
|
11
|
+
console.log(`Capture disabled (${r.configPath})`);
|
|
12
|
+
} else if (action === "status") {
|
|
13
|
+
console.log(`Capture: ${isCaptureEnabled() ? "on" : "off"}`);
|
|
14
|
+
} else {
|
|
15
|
+
console.error(`Usage: ${commandFor(["capture", "on|off|status"])}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = run;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { commandFor, cliNames } = require("../../lib/cli/names");
|
|
2
|
+
|
|
3
|
+
const USAGE = `${cliNames.canonicalName} — Echo knowledge forum CLI
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
${commandFor(["hook", "capture"])} Read hook JSON from stdin, write to session-buffer
|
|
7
|
+
${commandFor(["hook", "status"])} Generate SessionStart status output
|
|
8
|
+
${commandFor(["hook", "install", "<provider>", "[--write]"])} Print or apply hook config
|
|
9
|
+
${commandFor(["hook", "doctor"])} Check hook health
|
|
10
|
+
${commandFor(["init"])} Create workspace, write echo.json
|
|
11
|
+
${commandFor(["init", "project", "[--path <dir>]"])} Register project in ~/.echo-workspace/registry.json
|
|
12
|
+
${commandFor(["doctor"])} Check overall workspace health
|
|
13
|
+
${commandFor(["migrate", "legacy-buffer", "--project <id>|--path <dir>", "[--apply]"])} Migrate legacy session-buffer into a project
|
|
14
|
+
${commandFor(["all"])} Run full pipeline (convert -> validate -> index -> resolve)
|
|
15
|
+
${commandFor(["convert"])} Run buffer -> article conversion
|
|
16
|
+
${commandFor(["validate"])} Validate all articles and comments
|
|
17
|
+
${commandFor(["resolve"])} Resolve all annotation anchors
|
|
18
|
+
${commandFor(["search"])} Full-text search
|
|
19
|
+
${commandFor(["mcp"])} Start MCP server (stdio transport)
|
|
20
|
+
${commandFor(["capture", "on|off|status"])} Enable, disable, or check capture status
|
|
21
|
+
${commandFor(["project", "list"])} List all registered projects
|
|
22
|
+
${commandFor(["project", "find", "<projectId>"])} Show project details
|
|
23
|
+
${commandFor(["tag", "list"])} List all tags with usage counts
|
|
24
|
+
${commandFor(["tag", "add", "<article-id>", "<tag1>", "[tag2...]"])} Add one or more tags to an article
|
|
25
|
+
${commandFor(["tag", "remove", "<article-id>", "<tag1>", "[tag2...]"])} Remove one or more tags from an article
|
|
26
|
+
${commandFor(["tag", "rename", "<old-tag>", "<new-tag>"])} Rename a tag across all articles
|
|
27
|
+
${commandFor(["tag", "purge", "<tag>"])} Remove a tag from all articles
|
|
28
|
+
${commandFor(["import", "claude", "--all", "--dry-run|--apply"])} Import Claude Code sessions
|
|
29
|
+
${commandFor(["import", "claude", "--project", "<dir>", "--as-project", "<id>"])} Import single project
|
|
30
|
+
${commandFor(["serve"])} Start API + VitePress dev server in background
|
|
31
|
+
${commandFor(["serve", "--foreground"])} Start API + VitePress dev server in foreground
|
|
32
|
+
${commandFor(["refresh"])} Refresh pipeline + docs without restarting serve
|
|
33
|
+
${commandFor(["stop"])} Stop a running serve instance
|
|
34
|
+
${commandFor(["status", "[--json]", "[--lang <en|zh-CN>]"])} Show Echo status overview
|
|
35
|
+
${commandFor(["mcp", "--help"])} Show MCP setup instructions
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const MCP_HELP = `Echo MCP / Echo AI 访问接口
|
|
39
|
+
|
|
40
|
+
MCP is the bridge that lets AI assistants read and search your Echo archive.
|
|
41
|
+
MCP 是让 AI 助手读取、搜索 Echo 本地归档的桥。
|
|
42
|
+
|
|
43
|
+
What it provides / 它提供:
|
|
44
|
+
search_articles Search Echo articles / 搜索文章
|
|
45
|
+
get_article Read one article / 读取文章
|
|
46
|
+
get_article_context Read article with comments / 读取文章和评论
|
|
47
|
+
list_recent List recent articles / 最近文章
|
|
48
|
+
list_tags List tags / 标签列表
|
|
49
|
+
add_tags Add tags / 添加标签
|
|
50
|
+
remove_tags Remove tags / 移除标签
|
|
51
|
+
list_projects List registered projects / 项目列表
|
|
52
|
+
get_project Read one project / 读取项目信息
|
|
53
|
+
|
|
54
|
+
Config / 配置:
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"echo": {
|
|
59
|
+
"command": "${cliNames.canonicalName}",
|
|
60
|
+
"args": ["mcp"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Verify / 验证:
|
|
66
|
+
${cliNames.canonicalName} status Check Echo status / 查看 Echo 状态
|
|
67
|
+
${cliNames.canonicalName} doctor Diagnose setup / 诊断配置
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
module.exports = { USAGE, MCP_HELP };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const { printDoctorResults } = require("./helpers");
|
|
2
|
+
|
|
3
|
+
function run() {
|
|
4
|
+
const { runDoctor } = require("../../lib/usecases/run-doctor");
|
|
5
|
+
const results = runDoctor({ hookOnly: false });
|
|
6
|
+
console.log("Workspace health check:\n");
|
|
7
|
+
printDoctorResults(results);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = run;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function scheduleRefreshIfServeRunning(entryPath) {
|
|
5
|
+
const { getRunningServeInfo } = require("../../lib/usecases/refresh-serve");
|
|
6
|
+
if (!getRunningServeInfo()) return false;
|
|
7
|
+
const child = spawn(process.execPath, [entryPath || path.resolve(__dirname, "../echoctl.js"), "refresh", "--quiet"], {
|
|
8
|
+
detached: true,
|
|
9
|
+
stdio: "ignore",
|
|
10
|
+
env: process.env,
|
|
11
|
+
});
|
|
12
|
+
child.unref();
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function printDoctorResults(results) {
|
|
17
|
+
let hasError = false;
|
|
18
|
+
for (const r of results) {
|
|
19
|
+
const icon = r.status === "ok" ? " OK" : r.status === "warn" ? "WARN" : "ERR ";
|
|
20
|
+
console.log(` ${icon} ${r.name}: ${r.message}`);
|
|
21
|
+
if (r.status === "error") hasError = true;
|
|
22
|
+
}
|
|
23
|
+
console.log(`\n${results.length} checks.`);
|
|
24
|
+
if (hasError) process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { scheduleRefreshIfServeRunning, printDoctorResults };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { commandFor } = require("../../lib/cli/names");
|
|
2
|
+
const { USAGE } = require("./constants");
|
|
3
|
+
const { printDoctorResults } = require("./helpers");
|
|
4
|
+
|
|
5
|
+
function run(args) {
|
|
6
|
+
const sub = args[1];
|
|
7
|
+
if (sub === "capture") require("../../lib/hooks/capture");
|
|
8
|
+
else if (sub === "status") require("../../lib/hooks/status");
|
|
9
|
+
else if (sub === "install") {
|
|
10
|
+
const provider = args[2];
|
|
11
|
+
if (!provider || provider.startsWith("-")) {
|
|
12
|
+
console.error(`Error: provider required. Usage: ${commandFor(["hook", "install", "claude", "[--write]"])}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
if (provider !== "claude") {
|
|
16
|
+
console.error(`Error: unknown provider '${provider}'. Only 'claude' is supported.`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const write = args.includes("--write");
|
|
20
|
+
const { installClaudeHook } = require("../../lib/usecases/install-claude-hook");
|
|
21
|
+
const result = installClaudeHook({ write });
|
|
22
|
+
if (result.legacy.length > 0) {
|
|
23
|
+
console.log("Legacy .sh hooks detected:");
|
|
24
|
+
for (const l of result.legacy) console.log(` ${l.event}: ${l.command}`);
|
|
25
|
+
console.log("");
|
|
26
|
+
}
|
|
27
|
+
if (result.toAdd.length > 0) {
|
|
28
|
+
console.log(write ? "Installed:" : "Will install:");
|
|
29
|
+
for (const a of result.toAdd) console.log(` ${a.event}: ${a.command}`);
|
|
30
|
+
}
|
|
31
|
+
if (result.alreadyInstalled.length > 0) {
|
|
32
|
+
console.log("Already installed:");
|
|
33
|
+
for (const a of result.alreadyInstalled) console.log(` ${a.event}: ${a.command}`);
|
|
34
|
+
}
|
|
35
|
+
if (result.toAdd.length === 0) console.log("All hooks already up to date.");
|
|
36
|
+
if (!write) console.log("\nRun with --write to apply this configuration.");
|
|
37
|
+
else console.log("\nHook configuration written to ~/.claude/settings.json");
|
|
38
|
+
}
|
|
39
|
+
else if (sub === "doctor") {
|
|
40
|
+
const { runDoctor } = require("../../lib/usecases/run-doctor");
|
|
41
|
+
const results = runDoctor({ hookOnly: true });
|
|
42
|
+
console.log("Hook health check:\n");
|
|
43
|
+
printDoctorResults(results);
|
|
44
|
+
}
|
|
45
|
+
else console.log(USAGE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = run;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const { commandFor } = require("../../lib/cli/names");
|
|
2
|
+
|
|
3
|
+
function run(args) {
|
|
4
|
+
const sub = args[1];
|
|
5
|
+
if (sub !== "claude") {
|
|
6
|
+
console.error(`Error: unknown provider '${sub || "(none)"}'. Only 'claude' is supported.`);
|
|
7
|
+
console.error(`Usage: ${commandFor(["import", "claude", "--all", "--dry-run|--apply"])}`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const all = args.includes("--all");
|
|
12
|
+
const dryRun = args.includes("--dry-run");
|
|
13
|
+
const apply = args.includes("--apply");
|
|
14
|
+
const projectIdx = args.indexOf("--project");
|
|
15
|
+
const asProjectIdx = args.indexOf("--as-project");
|
|
16
|
+
const excludeIdx = args.indexOf("--exclude");
|
|
17
|
+
|
|
18
|
+
const targetProject = projectIdx !== -1 ? args[projectIdx + 1] : null;
|
|
19
|
+
const asProject = asProjectIdx !== -1 ? args[asProjectIdx + 1] : null;
|
|
20
|
+
const excludeDirs = excludeIdx !== -1 ? args[excludeIdx + 1].split(",").map((s) => s.trim()) : [];
|
|
21
|
+
|
|
22
|
+
if (!all && !targetProject) {
|
|
23
|
+
console.error("Error: need --all or --project <dir>");
|
|
24
|
+
console.error(`Usage: ${commandFor(["import", "claude", "--all", "--dry-run|--apply"])}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!dryRun && !apply) {
|
|
29
|
+
console.error("Error: need --dry-run or --apply");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const claudeProjectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(claudeProjectsDir)) {
|
|
36
|
+
console.error(`Error: ${claudeProjectsDir} not found. No Claude Code sessions to import.`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { scanClaudeProjects, buildImportPlan } = require("../../lib/import/scanner");
|
|
41
|
+
const mf = require("../../lib/import/manifest");
|
|
42
|
+
const provider = require("../../lib/import/providers/claude-code");
|
|
43
|
+
const { resolveEchoHomePath } = require("../../lib/infra/workspace");
|
|
44
|
+
const store = require("../../lib/infra/markdown-store");
|
|
45
|
+
|
|
46
|
+
const echoHome = resolveEchoHomePath();
|
|
47
|
+
const manifestPath = path.join(echoHome, "import-manifest.json");
|
|
48
|
+
const manifest = mf.loadManifest(manifestPath);
|
|
49
|
+
|
|
50
|
+
const scanOpts = {};
|
|
51
|
+
if (excludeDirs.length > 0) scanOpts.excludeDirs = excludeDirs;
|
|
52
|
+
|
|
53
|
+
let projects;
|
|
54
|
+
if (targetProject) {
|
|
55
|
+
const dirName = path.basename(targetProject);
|
|
56
|
+
const fullPath = path.join(claudeProjectsDir, dirName);
|
|
57
|
+
if (!fs.existsSync(fullPath)) {
|
|
58
|
+
console.error(`Error: project directory not found: ${fullPath}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
projects = [{
|
|
62
|
+
dirName, absPath: fullPath, jsonlFiles: [], sessionCount: 0,
|
|
63
|
+
decodedPath: targetProject, pathConfidence: "inferred",
|
|
64
|
+
}];
|
|
65
|
+
const entries = fs.readdirSync(fullPath).filter((f) => f.endsWith(".jsonl"));
|
|
66
|
+
projects[0].jsonlFiles = entries.map((f) => ({
|
|
67
|
+
sessionId: f.replace(/\.jsonl$/, ""),
|
|
68
|
+
fileName: f,
|
|
69
|
+
absPath: path.join(fullPath, f),
|
|
70
|
+
}));
|
|
71
|
+
projects[0].sessionCount = projects[0].jsonlFiles.length;
|
|
72
|
+
} else {
|
|
73
|
+
projects = scanClaudeProjects(claudeProjectsDir, scanOpts);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (projects.length === 0) {
|
|
77
|
+
console.log("No Claude Code projects found.");
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const plan = buildImportPlan(projects, manifest, {});
|
|
82
|
+
|
|
83
|
+
console.log("");
|
|
84
|
+
console.log(`Claude Code projects: ${projects.length}`);
|
|
85
|
+
console.log(`Sessions found: ${plan.summary.total}`);
|
|
86
|
+
console.log(` New: ${plan.summary.newCount}`);
|
|
87
|
+
console.log(` Updated: ${plan.summary.updatedCount}`);
|
|
88
|
+
console.log(` Skipped (same): ${plan.summary.skippedCount}`);
|
|
89
|
+
console.log("");
|
|
90
|
+
|
|
91
|
+
if (dryRun) {
|
|
92
|
+
if (plan.new.length > 0 || plan.updated.length > 0) {
|
|
93
|
+
console.log("Would import:");
|
|
94
|
+
for (const entry of plan.new.slice(0, 20)) {
|
|
95
|
+
console.log(` [NEW] ${entry.sessionId} (${entry.projectDir}, ${entry.turnCount} turns)`);
|
|
96
|
+
}
|
|
97
|
+
if (plan.new.length > 20) console.log(` ... and ${plan.new.length - 20} more`);
|
|
98
|
+
for (const entry of plan.updated.slice(0, 10)) {
|
|
99
|
+
console.log(` [UPDATED] ${entry.sessionId} (${entry.projectDir})`);
|
|
100
|
+
}
|
|
101
|
+
if (plan.updated.length > 10) console.log(` ... and ${plan.updated.length - 10} more`);
|
|
102
|
+
}
|
|
103
|
+
if (plan.skipped.length > 0) {
|
|
104
|
+
console.log("Would skip (already imported, unchanged):");
|
|
105
|
+
for (const entry of plan.skipped.slice(0, 5)) {
|
|
106
|
+
console.log(` ${entry.sessionId} -> ${entry.articleId}`);
|
|
107
|
+
}
|
|
108
|
+
if (plan.skipped.length > 5) console.log(` ... and ${plan.skipped.length - 5} more`);
|
|
109
|
+
}
|
|
110
|
+
if (plan.new.length === 0 && plan.updated.length === 0) {
|
|
111
|
+
console.log("Nothing to import. All sessions already imported and unchanged.");
|
|
112
|
+
}
|
|
113
|
+
console.log(`\nRun with --apply to execute.`);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (apply) {
|
|
118
|
+
const { resolveDataDirs } = require("../../lib/infra/echo-paths");
|
|
119
|
+
const dirs = resolveDataDirs();
|
|
120
|
+
const targetArticlesDir = asProject
|
|
121
|
+
? path.join(echoHome, "projects", asProject, "articles")
|
|
122
|
+
: dirs.articlesDir;
|
|
123
|
+
|
|
124
|
+
const opts = {
|
|
125
|
+
userSpeaker: process.env.ECHO_USER_SPEAKER || "vincent",
|
|
126
|
+
aiSpeaker: process.env.ECHO_AI_SPEAKER || "ai",
|
|
127
|
+
project: asProject || dirs.projectId || null,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
let imported = 0;
|
|
131
|
+
let skipped = 0;
|
|
132
|
+
|
|
133
|
+
const toProcess = [...plan.new, ...plan.updated];
|
|
134
|
+
for (const entry of toProcess) {
|
|
135
|
+
const articlePath = path.join(targetArticlesDir, `session-${entry.sessionId.slice(0, 8)}.md`);
|
|
136
|
+
|
|
137
|
+
if (fs.existsSync(articlePath)) {
|
|
138
|
+
console.log(` SKIP ${entry.sessionId} (article exists)`);
|
|
139
|
+
skipped++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const turns = provider.readSessionTurns(entry.filePath);
|
|
145
|
+
const classification = provider.classifySession(turns);
|
|
146
|
+
if (!classification.isMeaningful) {
|
|
147
|
+
console.log(` SKIP ${entry.sessionId} (${classification.reason})`);
|
|
148
|
+
skipped++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const metadata = provider.extractMetadata(turns);
|
|
153
|
+
const markdown = provider.toEchoArticle(turns, metadata, {
|
|
154
|
+
sessionId: entry.sessionId,
|
|
155
|
+
project: opts.project,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
fs.mkdirSync(path.dirname(articlePath), { recursive: true });
|
|
159
|
+
fs.writeFileSync(articlePath, markdown);
|
|
160
|
+
mf.recordImport(manifest, entry.sessionId, `session-${entry.sessionId.slice(0, 8)}`, entry.fileHash, { provider: "claude-code" });
|
|
161
|
+
|
|
162
|
+
console.log(` OK ${entry.sessionId} -> session-${entry.sessionId.slice(0, 8)}.md (${classification.estimatedQuality}, ${classification.turnCount} turns)`);
|
|
163
|
+
imported++;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(` FAIL ${entry.sessionId}: ${err.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
mf.saveManifest(manifest, manifestPath);
|
|
170
|
+
|
|
171
|
+
console.log(`\nImported: ${imported} Skipped: ${skipped + plan.skipped.length}`);
|
|
172
|
+
console.log(`Articles: ${targetArticlesDir}`);
|
|
173
|
+
|
|
174
|
+
const { runValidate } = require("../../validate");
|
|
175
|
+
const result = runValidate();
|
|
176
|
+
if (result.success) {
|
|
177
|
+
console.log(`Validate: OK — ${result.articleCount} articles, ${result.commentCount} comments`);
|
|
178
|
+
} else {
|
|
179
|
+
console.log(`Validate: ${result.errors.length} error(s) — see above`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = run;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { scheduleRefreshIfServeRunning } = require("./helpers");
|
|
2
|
+
|
|
3
|
+
function run(args) {
|
|
4
|
+
const sub = args[1];
|
|
5
|
+
if (sub === "project") {
|
|
6
|
+
const projectPath = args.includes("--path") ? args[args.indexOf("--path") + 1] : process.cwd();
|
|
7
|
+
if (!projectPath || projectPath.startsWith("-")) {
|
|
8
|
+
console.error("Error: --path requires a directory path");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const { registerProject } = require("../../lib/usecases/project-registry");
|
|
12
|
+
const result = registerProject(projectPath);
|
|
13
|
+
console.log(`Project: ${result.projectId}`);
|
|
14
|
+
console.log(`Root: ${result.projectRoot}`);
|
|
15
|
+
console.log(`Data: ${result.dataRoot}`);
|
|
16
|
+
if (result.created) {
|
|
17
|
+
console.log(`Registered: yes`);
|
|
18
|
+
if (result.dirsCreated.length > 0) console.log(`Created: ${result.dirsCreated.join(", ")}`);
|
|
19
|
+
if (result.dirsSkipped.length > 0) console.log(`Skipped (exists): ${result.dirsSkipped.join(", ")}`);
|
|
20
|
+
} else {
|
|
21
|
+
console.log(`Registered: no (already exists)`);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const { importClaudeProject } = require("../../lib/usecases/import-claude-project");
|
|
25
|
+
const imported = importClaudeProject(result.projectId);
|
|
26
|
+
if (imported.total > 0) {
|
|
27
|
+
console.log(`Claude transcripts: ${imported.total} found, ${imported.imported} imported, ${imported.skipped} skipped`);
|
|
28
|
+
} else {
|
|
29
|
+
console.log(`Claude transcripts: none found`);
|
|
30
|
+
}
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.log(`Claude transcripts: import skipped (${err.message})`);
|
|
33
|
+
}
|
|
34
|
+
if (scheduleRefreshIfServeRunning()) console.log(`Serve refresh: scheduled`);
|
|
35
|
+
} else {
|
|
36
|
+
const { initWorkspace } = require("../../lib/usecases/init-workspace");
|
|
37
|
+
const result = initWorkspace();
|
|
38
|
+
console.log(`Workspace: ${result.workspace}`);
|
|
39
|
+
if (result.created.length > 0) console.log(`Created: ${result.created.join(", ")}`);
|
|
40
|
+
if (result.skipped.length > 0) console.log(`Skipped (exists): ${result.skipped.join(", ")}`);
|
|
41
|
+
console.log(`echo.json: ${result.configAction}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = run;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { MCP_HELP } = require("./constants");
|
|
2
|
+
|
|
3
|
+
function run(args) {
|
|
4
|
+
if (args[1] === "--help") {
|
|
5
|
+
console.log(MCP_HELP);
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const { start } = require("../../lib/interfaces/mcp/server");
|
|
9
|
+
try {
|
|
10
|
+
start();
|
|
11
|
+
} catch (err) {
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = run;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { commandFor } = require("../../lib/cli/names");
|
|
2
|
+
const { scheduleRefreshIfServeRunning } = require("./helpers");
|
|
3
|
+
|
|
4
|
+
function run(args) {
|
|
5
|
+
const sub = args[1];
|
|
6
|
+
if (sub !== "legacy-buffer") {
|
|
7
|
+
console.error(`Usage: ${commandFor(["migrate", "legacy-buffer", "--project <id>|--path <dir>", "[--apply]"])}`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function valueAfter(flag) {
|
|
12
|
+
const idx = args.indexOf(flag);
|
|
13
|
+
if (idx === -1) return null;
|
|
14
|
+
const value = args[idx + 1];
|
|
15
|
+
return value && !value.startsWith("-") ? value : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const projectId = valueAfter("--project");
|
|
19
|
+
const projectPath = valueAfter("--path");
|
|
20
|
+
const from = valueAfter("--from");
|
|
21
|
+
const apply = args.includes("--apply");
|
|
22
|
+
const overwrite = args.includes("--overwrite");
|
|
23
|
+
const move = args.includes("--move");
|
|
24
|
+
|
|
25
|
+
if ((args.includes("--project") && !projectId) || (args.includes("--path") && !projectPath) || (args.includes("--from") && !from)) {
|
|
26
|
+
console.error("Error: --project, --path, and --from require a value.");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (projectId && projectPath) {
|
|
30
|
+
console.error("Error: use only one target: --project <id> or --path <dir>.");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const { migrateLegacyBuffer } = require("../../lib/usecases/migrate-legacy-buffer");
|
|
36
|
+
const result = migrateLegacyBuffer({ projectId, projectPath, from, apply, overwrite, move });
|
|
37
|
+
console.log(apply ? "Legacy buffer migration applied." : "Legacy buffer migration preview. Re-run with --apply to write changes.");
|
|
38
|
+
console.log(`Project: ${result.projectId}`);
|
|
39
|
+
console.log(`Source: ${result.sourceDir}`);
|
|
40
|
+
console.log(`Target: ${result.targetDir}`);
|
|
41
|
+
if (result.registered) console.log("Registered: yes");
|
|
42
|
+
console.log(`Files: copy ${result.summary.copy}, overwrite ${result.summary.overwrite}, skipped ${result.summary.skippedExisting}`);
|
|
43
|
+
console.log(`Map: update ${result.summary.mapUpdates}, conflicts ${result.summary.mapConflicts}`);
|
|
44
|
+
console.log(`Pending: ${result.summary.pending}`);
|
|
45
|
+
if (result.failuresCopied) console.log("Failures: append failures.jsonl");
|
|
46
|
+
if (result.auqCopied) console.log("AUQ: copy auq-counter.txt when target is empty");
|
|
47
|
+
if (result.mapConflicts.length > 0) {
|
|
48
|
+
console.log("\nSession map conflicts:");
|
|
49
|
+
for (const c of result.mapConflicts) {
|
|
50
|
+
console.log(` ${c.sessionId}`);
|
|
51
|
+
console.log(` existing: ${c.existing}`);
|
|
52
|
+
console.log(` legacy: ${c.next}`);
|
|
53
|
+
}
|
|
54
|
+
console.log("Use --overwrite only if these sessions should point to the migrated legacy files.");
|
|
55
|
+
}
|
|
56
|
+
if (apply && scheduleRefreshIfServeRunning()) {
|
|
57
|
+
console.log(`Serve refresh: scheduled`);
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(`Error: ${err.message}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = run;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function run(args) {
|
|
2
|
+
const { runPipeline } = require("../../lib/usecases/run-pipeline");
|
|
3
|
+
const cmd = args[0];
|
|
4
|
+
|
|
5
|
+
if (cmd === "all") {
|
|
6
|
+
runPipeline({ allProjects: true, silent: true });
|
|
7
|
+
console.log("Pipeline complete.");
|
|
8
|
+
} else if (cmd === "convert") {
|
|
9
|
+
runPipeline({ allProjects: true, silent: true, steps: ["convert"] });
|
|
10
|
+
} else if (cmd === "validate") {
|
|
11
|
+
const { resolveDataDirs } = require("../../lib/infra/echo-paths");
|
|
12
|
+
const dirs = resolveDataDirs();
|
|
13
|
+
const { validateWorkspace } = require("../../lib/domain/validation");
|
|
14
|
+
const results = validateWorkspace(dirs);
|
|
15
|
+
console.log(`Validation: ${results.errors.length} error(s), ${results.warnings.length} warning(s)`);
|
|
16
|
+
if (results.errors.length > 0) process.exit(1);
|
|
17
|
+
} else if (cmd === "resolve") {
|
|
18
|
+
const { resolveDataDirs } = require("../../lib/infra/echo-paths");
|
|
19
|
+
const dirs = resolveDataDirs();
|
|
20
|
+
const { resolveAnchors } = require("../../lib/domain/anchor");
|
|
21
|
+
resolveAnchors(dirs);
|
|
22
|
+
console.log("Anchors resolved.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = run;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { USAGE } = require("./constants");
|
|
2
|
+
|
|
3
|
+
function run(args) {
|
|
4
|
+
const sub = args[1];
|
|
5
|
+
if (sub === "list") {
|
|
6
|
+
const { listProjects } = require("../../lib/usecases/project-registry");
|
|
7
|
+
const projects = listProjects();
|
|
8
|
+
if (projects.length === 0) {
|
|
9
|
+
console.log("No registered projects.");
|
|
10
|
+
} else {
|
|
11
|
+
for (const p of projects) {
|
|
12
|
+
console.log(` ${p.projectId.padEnd(20)} ${p.root.padEnd(45)} ${(p.registeredAt || "").slice(0, 10)}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
} else if (sub === "find") {
|
|
16
|
+
const targetId = args[2];
|
|
17
|
+
if (!targetId || targetId.startsWith("-")) {
|
|
18
|
+
console.error("Error: project ID required. Usage: echoctl project find <projectId>");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const { findProjectById } = require("../../lib/usecases/project-registry");
|
|
22
|
+
const project = findProjectById(targetId);
|
|
23
|
+
if (!project) {
|
|
24
|
+
console.error(`Project "${targetId}" not found.`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
console.log(`Project: ${project.projectId}`);
|
|
28
|
+
console.log(`Root: ${project.projectRoot}`);
|
|
29
|
+
console.log(`Data: ${project.dataRoot}`);
|
|
30
|
+
} else {
|
|
31
|
+
console.log(USAGE);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = run;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function run(args) {
|
|
2
|
+
const quiet = args.includes("--quiet");
|
|
3
|
+
(async () => {
|
|
4
|
+
try {
|
|
5
|
+
const { refreshServe } = require("../../lib/usecases/refresh-serve");
|
|
6
|
+
const ok = await refreshServe();
|
|
7
|
+
if (!quiet) console.log(ok ? "Refresh OK" : "Refresh failed");
|
|
8
|
+
} catch (e) {
|
|
9
|
+
if (!quiet) console.error("Refresh error:", e.message);
|
|
10
|
+
}
|
|
11
|
+
})();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = run;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function run(args) {
|
|
2
|
+
const { runSearch } = require("../../lib/usecases/query-articles");
|
|
3
|
+
const { resolveDataDirs } = require("../../lib/infra/echo-paths");
|
|
4
|
+
|
|
5
|
+
const keywordIdx = args.indexOf("--keyword");
|
|
6
|
+
const tagIdx = args.indexOf("--tag");
|
|
7
|
+
const keyword = keywordIdx !== -1 ? args[keywordIdx + 1] : null;
|
|
8
|
+
const tag = tagIdx !== -1 ? args[tagIdx + 1] : null;
|
|
9
|
+
|
|
10
|
+
if (!keyword && !tag) {
|
|
11
|
+
console.error("Usage: echoctl search -- --keyword <keyword> [--tag <tag>]");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const dirs = resolveDataDirs();
|
|
16
|
+
const results = runSearch({ keyword, tag }, { dirs });
|
|
17
|
+
if (results.length === 0) {
|
|
18
|
+
console.log("No results found.");
|
|
19
|
+
} else {
|
|
20
|
+
for (const r of results) {
|
|
21
|
+
console.log(`${r.id} — ${r.title || "(untitled)"}`);
|
|
22
|
+
console.log(` ${r.snippet}`);
|
|
23
|
+
console.log();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = run;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
|
|
3
|
+
function run(args) {
|
|
4
|
+
if (args.includes("--foreground")) {
|
|
5
|
+
require("../../serve").start().catch((err) => {
|
|
6
|
+
console.error(`${CLI} serve failed:`, err.message);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
(async () => {
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const { spawn } = require("child_process");
|
|
13
|
+
const {
|
|
14
|
+
readServeInfo,
|
|
15
|
+
serveLogFile,
|
|
16
|
+
formatServeSummary,
|
|
17
|
+
isPidRunning,
|
|
18
|
+
} = require("../../serve");
|
|
19
|
+
const { isCaptureEnabled } = require("../../lib/infra/config");
|
|
20
|
+
|
|
21
|
+
const existing = readServeInfo();
|
|
22
|
+
if (existing && isPidRunning(existing.pid)) {
|
|
23
|
+
console.log(formatServeSummary(existing, {
|
|
24
|
+
background: true,
|
|
25
|
+
captureEnabled: isCaptureEnabled(),
|
|
26
|
+
logFile: serveLogFile(),
|
|
27
|
+
}));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const logFile = serveLogFile();
|
|
32
|
+
fs.mkdirSync(require("path").dirname(logFile), { recursive: true });
|
|
33
|
+
const logFd = fs.openSync(logFile, "a");
|
|
34
|
+
const child = spawn(process.execPath, [path.resolve(__dirname, "../echoctl.js"), "serve", "--foreground"], {
|
|
35
|
+
detached: true,
|
|
36
|
+
stdio: ["ignore", logFd, logFd],
|
|
37
|
+
env: process.env,
|
|
38
|
+
});
|
|
39
|
+
child.unref();
|
|
40
|
+
|
|
41
|
+
const startedAt = Date.now();
|
|
42
|
+
let info = null;
|
|
43
|
+
while (Date.now() - startedAt < 20000) {
|
|
44
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
45
|
+
try {
|
|
46
|
+
info = readServeInfo();
|
|
47
|
+
} catch (_) {
|
|
48
|
+
info = null;
|
|
49
|
+
}
|
|
50
|
+
if (info && info.pid === child.pid && isPidRunning(info.pid)) break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fs.closeSync(logFd);
|
|
54
|
+
|
|
55
|
+
if (!info || info.pid !== child.pid || !isPidRunning(info.pid)) {
|
|
56
|
+
console.error(`${CLI} serve failed to start in background.`);
|
|
57
|
+
console.error(`See log: ${logFile}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(formatServeSummary(info, {
|
|
62
|
+
background: true,
|
|
63
|
+
captureEnabled: isCaptureEnabled(),
|
|
64
|
+
logFile,
|
|
65
|
+
}));
|
|
66
|
+
})().catch((err) => {
|
|
67
|
+
console.error(`${CLI} serve failed:`, err.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = run;
|