@hippo-memo/cli 1.0.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.
@@ -0,0 +1,158 @@
1
+ import {
2
+ access,
3
+ constants,
4
+ mkdir,
5
+ readFile,
6
+ writeFile
7
+ } from "node:fs/promises";
8
+ import { EOL } from "node:os";
9
+ import { join } from "node:path";
10
+ import { createConfig, createStore } from "@hippo-memo/core";
11
+ import { MEMORY_DIR_NAME } from "@hippo-memo/shared";
12
+
13
+ const __dirname = import.meta.dirname;
14
+ const TEMPLATE_DIR = join(__dirname, "../../template/system");
15
+ const AGENTS_TEMPLATE_PATH = join(__dirname, "../../template/mcp/AGENTS.md");
16
+
17
+ const TEMPLATE_URIS = {
18
+ agent: "system://agent",
19
+ soul: "system://soul",
20
+ user: "system://user"
21
+ };
22
+
23
+ async function readTemplateContent(filename: string): Promise<string> {
24
+ return readFile(join(TEMPLATE_DIR, filename), "utf-8");
25
+ }
26
+
27
+ /**
28
+ * 从内容中提取第一行非空内容作为注入标识符
29
+ */
30
+ function getInjectionMarker(content: string): string {
31
+ const lines = content.split("\n");
32
+ const firstNonEmptyLine = lines.find((line) => line.trim() !== "");
33
+ return firstNonEmptyLine?.trim() || "";
34
+ }
35
+
36
+ /**
37
+ * 检查文件是否已注入(通过第一行判断)
38
+ */
39
+ function isAlreadyInjected(content: string, marker: string): boolean {
40
+ const lines = content.split("\n");
41
+ // 跳过开头的空行
42
+ const firstNonEmptyLine = lines.find((line) => line.trim() !== "");
43
+ return firstNonEmptyLine?.trim() === marker;
44
+ }
45
+
46
+ /**
47
+ * 安全地将内容注入到文件顶部
48
+ * @param filePath 目标文件路径
49
+ * @param contentToInject 要注入的内容
50
+ */
51
+ async function injectToTopOfFile(
52
+ filePath: string,
53
+ contentToInject: string
54
+ ): Promise<void> {
55
+ try {
56
+ let originalContent = "";
57
+
58
+ try {
59
+ originalContent = await readFile(filePath, "utf-8");
60
+ } catch {
61
+ originalContent = "";
62
+ }
63
+
64
+ // 从注入内容中提取第一行作为标识符
65
+ const injectionMarker = getInjectionMarker(contentToInject);
66
+
67
+ // 幂等性检查:通过第一行判断是否已注入
68
+ if (
69
+ originalContent &&
70
+ isAlreadyInjected(originalContent, injectionMarker)
71
+ ) {
72
+ console.log(`› Skipped: content already exists`);
73
+ return;
74
+ }
75
+
76
+ // 拼接内容,注意处理换行符
77
+ const separator = originalContent ? `${EOL}${EOL}` : "";
78
+ const updatedContent = `${contentToInject}${separator}${originalContent}`;
79
+
80
+ await writeFile(filePath, updatedContent, "utf-8");
81
+ console.log(`✔ Injected: ${filePath}`);
82
+ } catch (error) {
83
+ console.error(`✖ Failed: ${filePath}`, error);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 检测并处理 AGENTS.md 和 CLAUDE.md 文件
90
+ */
91
+ async function handleAgentsAndClaudeFiles(directory: string): Promise<void> {
92
+ const agentsPath = join(directory, "AGENTS.md");
93
+ const claudePath = join(directory, "CLAUDE.md");
94
+
95
+ let hasAgents = false;
96
+ let hasClaude = false;
97
+
98
+ try {
99
+ await access(agentsPath, constants.F_OK);
100
+ hasAgents = true;
101
+ } catch {
102
+ // AGENTS.md 不存在
103
+ }
104
+
105
+ try {
106
+ await access(claudePath, constants.F_OK);
107
+ hasClaude = true;
108
+ } catch {
109
+ // CLAUDE.md 不存在
110
+ }
111
+
112
+ // 读取 AGENTS.md 模板内容
113
+ const agentsContent = await readFile(AGENTS_TEMPLATE_PATH, "utf-8");
114
+
115
+ if (hasClaude) {
116
+ // CLAUDE.md 存在:优先注入到 CLAUDE.md 顶部
117
+ console.log("› Found: CLAUDE.md");
118
+ await injectToTopOfFile(claudePath, agentsContent);
119
+ } else if (hasAgents) {
120
+ // 只有 AGENTS.md 存在:注入到 AGENTS.md 顶部
121
+ console.log("› Found: AGENTS.md");
122
+ await injectToTopOfFile(agentsPath, agentsContent);
123
+ } else {
124
+ // 两者都不存在:创建 AGENTS.md
125
+ console.log("✔ Created: AGENTS.md");
126
+ await writeFile(agentsPath, agentsContent, "utf-8");
127
+ }
128
+ }
129
+
130
+ export async function init(directory: string): Promise<void> {
131
+ const memoryRoot = join(directory, MEMORY_DIR_NAME);
132
+
133
+ try {
134
+ await access(memoryRoot, constants.F_OK);
135
+ console.log(`Memory directory already exists at ${memoryRoot}`);
136
+ } catch {
137
+ // Directory doesn't exist, proceed with creation
138
+ await mkdir(memoryRoot, { recursive: true });
139
+
140
+ const store = createStore({
141
+ type: "filesystem",
142
+ config: createConfig(directory)
143
+ });
144
+
145
+ await Promise.all(
146
+ Object.entries(TEMPLATE_URIS).map(async ([key, uri]) => {
147
+ const content = await readTemplateContent(`${key}.md`);
148
+ await store.createMemory({ uri, content, priority: 10 });
149
+ })
150
+ );
151
+
152
+ console.log(`Initialized hippo memory at ${memoryRoot}`);
153
+ }
154
+
155
+ // 处理 AGENTS.md 和 CLAUDE.md 文件
156
+ console.log("◇ Checking AGENTS.md and CLAUDE.md...");
157
+ await handleAgentsAndClaudeFiles(directory);
158
+ }
@@ -0,0 +1,13 @@
1
+ import { createConfig, createStore } from "@hippo-memo/core";
2
+ import { startServer } from "../api/server";
3
+
4
+ export async function web(
5
+ directory: string,
6
+ port: number,
7
+ storeType: "filesystem" | "sqlite"
8
+ ): Promise<void> {
9
+ const config = createConfig(directory);
10
+ const store = createStore({ type: storeType, config });
11
+
12
+ await startServer(store, port);
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { init, web } from "./commands";
4
+
5
+ const program = new Command();
6
+
7
+ program.name("hippo").description("Hippo Memory Store CLI").version("1.0.0");
8
+
9
+ program
10
+ .command("init")
11
+ .description("Initialize a new hippo memory directory")
12
+ .argument("[directory]", "Directory to initialize", ".")
13
+ .action(async (directory) => {
14
+ await init(directory);
15
+ });
16
+
17
+ program
18
+ .command("web")
19
+ .description("Start the web server")
20
+ .argument("[directory]", "Project directory", ".")
21
+ .option("-p, --port <port>", "Port to listen on", "3000")
22
+ .option(
23
+ "-s, --store-type <type>",
24
+ "Store type (filesystem or sqlite)",
25
+ "filesystem"
26
+ )
27
+ .action(async (directory, options) => {
28
+ const port = Number.parseInt(options.port, 10);
29
+ const storeType = options.storeType;
30
+ await web(directory, port, storeType);
31
+ });
32
+
33
+ program.parse();
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true,
8
+ "resolveJsonModule": true,
9
+ "declaration": false,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "strict": true,
13
+ "skipLibCheck": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }