haroo 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.
- package/README.md +58 -0
- package/dist/index.js +84883 -0
- package/package.json +73 -0
- package/src/__tests__/e2e/EventService.test.ts +211 -0
- package/src/__tests__/unit/Event.test.ts +89 -0
- package/src/__tests__/unit/Memory.test.ts +130 -0
- package/src/application/graph/builder.ts +106 -0
- package/src/application/graph/edges.ts +37 -0
- package/src/application/graph/nodes/addEvent.ts +113 -0
- package/src/application/graph/nodes/chat.ts +128 -0
- package/src/application/graph/nodes/extractMemory.ts +135 -0
- package/src/application/graph/nodes/index.ts +8 -0
- package/src/application/graph/nodes/query.ts +194 -0
- package/src/application/graph/nodes/respond.ts +26 -0
- package/src/application/graph/nodes/router.ts +82 -0
- package/src/application/graph/nodes/toolExecutor.ts +79 -0
- package/src/application/graph/nodes/types.ts +2 -0
- package/src/application/index.ts +4 -0
- package/src/application/services/DiaryService.ts +188 -0
- package/src/application/services/EventService.ts +61 -0
- package/src/application/services/index.ts +2 -0
- package/src/application/tools/calendarTool.ts +179 -0
- package/src/application/tools/diaryTool.ts +182 -0
- package/src/application/tools/index.ts +68 -0
- package/src/config/env.ts +33 -0
- package/src/config/index.ts +1 -0
- package/src/domain/entities/DiaryEntry.ts +16 -0
- package/src/domain/entities/Event.ts +13 -0
- package/src/domain/entities/Memory.ts +20 -0
- package/src/domain/index.ts +5 -0
- package/src/domain/interfaces/IDiaryRepository.ts +21 -0
- package/src/domain/interfaces/IEventsRepository.ts +12 -0
- package/src/domain/interfaces/ILanguageModel.ts +23 -0
- package/src/domain/interfaces/IMemoriesRepository.ts +15 -0
- package/src/domain/interfaces/IMemory.ts +19 -0
- package/src/domain/interfaces/index.ts +4 -0
- package/src/domain/state/AgentState.ts +30 -0
- package/src/index.ts +5 -0
- package/src/infrastructure/database/factory.ts +52 -0
- package/src/infrastructure/database/index.ts +21 -0
- package/src/infrastructure/database/sqlite-checkpointer.ts +179 -0
- package/src/infrastructure/database/sqlite-client.ts +69 -0
- package/src/infrastructure/database/sqlite-diary-repository.ts +209 -0
- package/src/infrastructure/database/sqlite-events-repository.ts +167 -0
- package/src/infrastructure/database/sqlite-memories-repository.ts +284 -0
- package/src/infrastructure/database/sqlite-schema.ts +98 -0
- package/src/infrastructure/index.ts +3 -0
- package/src/infrastructure/llm/base.ts +14 -0
- package/src/infrastructure/llm/gemini.ts +139 -0
- package/src/infrastructure/llm/index.ts +22 -0
- package/src/infrastructure/llm/ollama.ts +126 -0
- package/src/infrastructure/llm/openai.ts +148 -0
- package/src/infrastructure/memory/checkpointer.ts +19 -0
- package/src/infrastructure/memory/index.ts +2 -0
- package/src/infrastructure/settings/index.ts +96 -0
- package/src/interface/cli/calendar.ts +120 -0
- package/src/interface/cli/chat.ts +185 -0
- package/src/interface/cli/commands.ts +337 -0
- package/src/interface/cli/printer.ts +65 -0
- package/src/interface/index.ts +1 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { EventService, DiaryService } from "../../application/services";
|
|
3
|
+
import { createRepositoriesFromEnv } from "../../infrastructure/database/factory";
|
|
4
|
+
import { createLLM } from "../../infrastructure/llm";
|
|
5
|
+
import {
|
|
6
|
+
getSettings,
|
|
7
|
+
saveSettings,
|
|
8
|
+
getEffectiveProvider,
|
|
9
|
+
getEffectiveModel,
|
|
10
|
+
} from "../../infrastructure/settings";
|
|
11
|
+
import { formatTodayEvents, formatUpcomingEvents } from "./calendar";
|
|
12
|
+
import { startChat } from "./chat";
|
|
13
|
+
import { printer } from "./printer";
|
|
14
|
+
|
|
15
|
+
export type LLMProvider = "openai" | "gemini" | "ollama";
|
|
16
|
+
const VALID_PROVIDERS: LLMProvider[] = ["openai", "gemini", "ollama"];
|
|
17
|
+
|
|
18
|
+
export function createProgram(): Command {
|
|
19
|
+
const program = new Command();
|
|
20
|
+
|
|
21
|
+
program.name("haroo").description("AI-powered personal logging CLI").version("1.0.0");
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command("chat")
|
|
25
|
+
.description("Start an interactive chat session")
|
|
26
|
+
.option("-p, --provider <provider>", "LLM provider (openai, gemini, ollama)")
|
|
27
|
+
.option("-m, --model <model>", "Model name to use")
|
|
28
|
+
.option("-t, --thread <id>", "Resume a previous thread")
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
try {
|
|
31
|
+
const repositories = createRepositoriesFromEnv();
|
|
32
|
+
const provider = getEffectiveProvider(options.provider);
|
|
33
|
+
const model = getEffectiveModel(options.model);
|
|
34
|
+
|
|
35
|
+
await startChat({
|
|
36
|
+
provider,
|
|
37
|
+
model,
|
|
38
|
+
threadId: options.thread,
|
|
39
|
+
repositories,
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
43
|
+
printer.error(`Failed to start chat: ${errorMessage}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command("calendar")
|
|
50
|
+
.description("Show calendar events")
|
|
51
|
+
.option("-d, --days <number>", "Number of upcoming days to show", "7")
|
|
52
|
+
.option("--today", "Show only today's events")
|
|
53
|
+
.action(async (options) => {
|
|
54
|
+
try {
|
|
55
|
+
const repositories = createRepositoriesFromEnv();
|
|
56
|
+
const eventService = new EventService(repositories.events);
|
|
57
|
+
|
|
58
|
+
if (options.today) {
|
|
59
|
+
const events = await eventService.getToday();
|
|
60
|
+
console.log(formatTodayEvents(events));
|
|
61
|
+
} else {
|
|
62
|
+
const days = parseInt(options.days, 10);
|
|
63
|
+
const events = await eventService.getUpcoming(days);
|
|
64
|
+
console.log(formatUpcomingEvents(events, days));
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
68
|
+
printer.error(`Failed to load calendar: ${errorMessage}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command("sessions")
|
|
75
|
+
.description("List saved sessions")
|
|
76
|
+
.action(async () => {
|
|
77
|
+
printer.warn("Sessions listing not yet implemented");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Model configuration command
|
|
81
|
+
program
|
|
82
|
+
.command("model")
|
|
83
|
+
.description("View or set default LLM provider and model")
|
|
84
|
+
.argument("[provider]", "Provider to set (openai, gemini, ollama)")
|
|
85
|
+
.argument("[model]", "Model name to use")
|
|
86
|
+
.action((provider?: string, model?: string) => {
|
|
87
|
+
// If no arguments, show current settings
|
|
88
|
+
if (!provider) {
|
|
89
|
+
const settings = getSettings();
|
|
90
|
+
const effectiveProvider = getEffectiveProvider();
|
|
91
|
+
const effectiveModel = getEffectiveModel();
|
|
92
|
+
|
|
93
|
+
printer.blank();
|
|
94
|
+
printer.info("Current Model Settings");
|
|
95
|
+
printer.divider();
|
|
96
|
+
printer.system(
|
|
97
|
+
`Provider: ${effectiveProvider}${settings.defaultProvider ? "" : " (default)"}`
|
|
98
|
+
);
|
|
99
|
+
if (effectiveModel) {
|
|
100
|
+
printer.system(`Model: ${effectiveModel}${settings.defaultModel ? "" : " (default)"}`);
|
|
101
|
+
} else {
|
|
102
|
+
printer.system("Model: (provider default)");
|
|
103
|
+
}
|
|
104
|
+
printer.blank();
|
|
105
|
+
printer.system("Saved settings file: ~/.log/settings.json");
|
|
106
|
+
printer.blank();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Validate provider
|
|
111
|
+
if (!VALID_PROVIDERS.includes(provider as LLMProvider)) {
|
|
112
|
+
printer.error(`Invalid provider: ${provider}`);
|
|
113
|
+
printer.info(`Valid providers: ${VALID_PROVIDERS.join(", ")}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Save settings
|
|
118
|
+
saveSettings({
|
|
119
|
+
defaultProvider: provider as LLMProvider,
|
|
120
|
+
defaultModel: model,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (model) {
|
|
124
|
+
printer.success(`Default model set to: ${provider} / ${model}`);
|
|
125
|
+
} else {
|
|
126
|
+
printer.success(`Default provider set to: ${provider}`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Diary commands
|
|
131
|
+
const diaryCmd = program.command("diary").description("Diary/journal operations");
|
|
132
|
+
|
|
133
|
+
diaryCmd
|
|
134
|
+
.command("generate")
|
|
135
|
+
.description("Generate diary entry for a date")
|
|
136
|
+
.option("-d, --date <date>", "Date to generate (YYYY-MM-DD), defaults to yesterday")
|
|
137
|
+
.option("-p, --provider <provider>", "LLM provider (openai, gemini, ollama)")
|
|
138
|
+
.option("-m, --model <model>", "Model name to use")
|
|
139
|
+
.action(async (options) => {
|
|
140
|
+
try {
|
|
141
|
+
const repositories = createRepositoriesFromEnv();
|
|
142
|
+
const provider = getEffectiveProvider(options.provider);
|
|
143
|
+
const model = getEffectiveModel(options.model);
|
|
144
|
+
const llm = createLLM(provider, model);
|
|
145
|
+
const diaryService = new DiaryService(repositories.diary, repositories.checkpointer, llm);
|
|
146
|
+
|
|
147
|
+
let targetDate: Date;
|
|
148
|
+
if (options.date) {
|
|
149
|
+
targetDate = new Date(options.date);
|
|
150
|
+
targetDate.setHours(0, 0, 0, 0);
|
|
151
|
+
} else {
|
|
152
|
+
// Default to yesterday
|
|
153
|
+
targetDate = new Date();
|
|
154
|
+
targetDate.setDate(targetDate.getDate() - 1);
|
|
155
|
+
targetDate.setHours(0, 0, 0, 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const dateStr = targetDate.toISOString().split("T")[0];
|
|
159
|
+
const spinner = printer.spinner(`Generating diary entry for ${dateStr}...`);
|
|
160
|
+
spinner.start();
|
|
161
|
+
|
|
162
|
+
const entry = await diaryService.generateDiaryEntry(targetDate);
|
|
163
|
+
spinner.stop();
|
|
164
|
+
|
|
165
|
+
if (entry) {
|
|
166
|
+
printer.success(`Diary entry created for ${dateStr}`);
|
|
167
|
+
printer.blank();
|
|
168
|
+
printer.info(`Mood: ${entry.mood} (${entry.moodScore}/10)`);
|
|
169
|
+
printer.blank();
|
|
170
|
+
printer.system("Summary:");
|
|
171
|
+
console.log(` ${entry.summary}`);
|
|
172
|
+
printer.blank();
|
|
173
|
+
printer.system("Therapeutic Advice:");
|
|
174
|
+
console.log(` ${entry.therapeuticAdvice}`);
|
|
175
|
+
printer.blank();
|
|
176
|
+
printer.system(`Messages analyzed: ${entry.messageCount}`);
|
|
177
|
+
} else {
|
|
178
|
+
printer.warn(`No conversations found for ${dateStr}`);
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
182
|
+
printer.error(`Failed to generate diary entry: ${errorMessage}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
diaryCmd
|
|
188
|
+
.command("show")
|
|
189
|
+
.description("Show recent diary entries")
|
|
190
|
+
.option("-n, --count <number>", "Number of entries to show", "7")
|
|
191
|
+
.action(async (options) => {
|
|
192
|
+
try {
|
|
193
|
+
const repositories = createRepositoriesFromEnv();
|
|
194
|
+
const count = parseInt(options.count, 10);
|
|
195
|
+
|
|
196
|
+
const entries = await repositories.diary.getRecent(count);
|
|
197
|
+
|
|
198
|
+
if (entries.length === 0) {
|
|
199
|
+
printer.warn("No diary entries found");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
printer.blank();
|
|
204
|
+
printer.divider();
|
|
205
|
+
printer.success(`Recent Diary Entries (${entries.length})`);
|
|
206
|
+
printer.divider();
|
|
207
|
+
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const dateStr = entry.entryDate.toISOString().split("T")[0];
|
|
210
|
+
printer.blank();
|
|
211
|
+
printer.info(`${dateStr} - ${entry.mood} (${entry.moodScore}/10)`);
|
|
212
|
+
console.log(` ${entry.summary}`);
|
|
213
|
+
printer.system(` Advice: ${entry.therapeuticAdvice}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
printer.blank();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
219
|
+
printer.error(`Failed to load diary entries: ${errorMessage}`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Memory commands
|
|
225
|
+
const memoryCmd = program.command("memory").description("Manage stored memories");
|
|
226
|
+
|
|
227
|
+
memoryCmd
|
|
228
|
+
.command("list", { isDefault: true })
|
|
229
|
+
.description("View stored memories")
|
|
230
|
+
.option("-t, --type <type>", "Filter by type")
|
|
231
|
+
.option("-n, --count <number>", "Number to show", "20")
|
|
232
|
+
.action(async (options) => {
|
|
233
|
+
try {
|
|
234
|
+
const repositories = createRepositoriesFromEnv();
|
|
235
|
+
const count = parseInt(options.count, 10);
|
|
236
|
+
|
|
237
|
+
let memories;
|
|
238
|
+
if (options.type) {
|
|
239
|
+
memories = await repositories.memories.getByType(options.type);
|
|
240
|
+
} else {
|
|
241
|
+
memories = await repositories.memories.getAll(count);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (memories.length === 0) {
|
|
245
|
+
printer.warn("No memories stored yet");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
printer.blank();
|
|
250
|
+
printer.success(`Memories (${memories.length})`);
|
|
251
|
+
printer.divider();
|
|
252
|
+
|
|
253
|
+
for (const memory of memories) {
|
|
254
|
+
printer.info(`[${memory.id.slice(0, 8)}] [${memory.type}] ${memory.content}`);
|
|
255
|
+
printer.system(` Importance: ${memory.importance}/10`);
|
|
256
|
+
}
|
|
257
|
+
printer.blank();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
260
|
+
printer.error(`Failed to load memories: ${errorMessage}`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
memoryCmd
|
|
266
|
+
.command("add <content>")
|
|
267
|
+
.description("Add a new memory")
|
|
268
|
+
.option("-t, --type <type>", "Memory type", "fact")
|
|
269
|
+
.option("-i, --importance <number>", "Importance 1-10", "5")
|
|
270
|
+
.action(async (content, options) => {
|
|
271
|
+
try {
|
|
272
|
+
const repositories = createRepositoriesFromEnv();
|
|
273
|
+
|
|
274
|
+
const memory = {
|
|
275
|
+
id: crypto.randomUUID(),
|
|
276
|
+
type: options.type as
|
|
277
|
+
| "fact"
|
|
278
|
+
| "preference"
|
|
279
|
+
| "routine"
|
|
280
|
+
| "relationship"
|
|
281
|
+
| "communication_style"
|
|
282
|
+
| "interest",
|
|
283
|
+
content: content,
|
|
284
|
+
importance: parseInt(options.importance, 10),
|
|
285
|
+
createdAt: new Date(),
|
|
286
|
+
lastAccessed: new Date(),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
await repositories.memories.create(memory);
|
|
290
|
+
printer.success(`Memory added: ${memory.id.slice(0, 8)}`);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
293
|
+
printer.error(`Failed to add memory: ${errorMessage}`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
memoryCmd
|
|
299
|
+
.command("edit <id> <content>")
|
|
300
|
+
.description("Edit a memory")
|
|
301
|
+
.action(async (id, content) => {
|
|
302
|
+
try {
|
|
303
|
+
const repositories = createRepositoriesFromEnv();
|
|
304
|
+
const result = await repositories.memories.update(id, { content });
|
|
305
|
+
if (result) {
|
|
306
|
+
printer.success("Memory updated");
|
|
307
|
+
} else {
|
|
308
|
+
printer.error("Memory not found");
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
312
|
+
printer.error(`Failed to edit memory: ${errorMessage}`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
memoryCmd
|
|
318
|
+
.command("delete <id>")
|
|
319
|
+
.description("Delete a memory")
|
|
320
|
+
.action(async (id) => {
|
|
321
|
+
try {
|
|
322
|
+
const repositories = createRepositoriesFromEnv();
|
|
323
|
+
const deleted = await repositories.memories.delete(id);
|
|
324
|
+
if (deleted) {
|
|
325
|
+
printer.success("Memory deleted");
|
|
326
|
+
} else {
|
|
327
|
+
printer.error("Memory not found");
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
331
|
+
printer.error(`Failed to delete memory: ${errorMessage}`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return program;
|
|
337
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora, { type Ora } from "ora";
|
|
3
|
+
|
|
4
|
+
export interface Printer {
|
|
5
|
+
info(message: string): void;
|
|
6
|
+
success(message: string): void;
|
|
7
|
+
error(message: string): void;
|
|
8
|
+
warn(message: string): void;
|
|
9
|
+
user(message: string): void;
|
|
10
|
+
assistant(message: string): void;
|
|
11
|
+
system(message: string): void;
|
|
12
|
+
blank(): void;
|
|
13
|
+
divider(): void;
|
|
14
|
+
spinner(message: string): Ora;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createPrinter(): Printer {
|
|
18
|
+
return {
|
|
19
|
+
info(message: string): void {
|
|
20
|
+
console.log(chalk.blue("ℹ"), message);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
success(message: string): void {
|
|
24
|
+
console.log(chalk.green("✓"), message);
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
error(message: string): void {
|
|
28
|
+
console.log(chalk.red("✗"), message);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
warn(message: string): void {
|
|
32
|
+
console.log(chalk.yellow("⚠"), message);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
user(message: string): void {
|
|
36
|
+
console.log(chalk.cyan.bold("You:"), message);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
assistant(message: string): void {
|
|
40
|
+
console.log(chalk.magenta.bold("Assistant:"), message);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
system(message: string): void {
|
|
44
|
+
console.log(chalk.dim(`[${message}]`));
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
blank(): void {
|
|
48
|
+
console.log();
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
divider(): void {
|
|
52
|
+
console.log(chalk.dim("─".repeat(50)));
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
spinner(message: string): Ora {
|
|
56
|
+
return ora({
|
|
57
|
+
text: message,
|
|
58
|
+
color: "cyan",
|
|
59
|
+
discardStdin: false,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const printer = createPrinter();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createProgram } from "./cli/commands";
|