Files
ppc/packages/marts/c2vi/deno.ts
2026-02-24 14:15:17 +01:00

432 lines
11 KiB
TypeScript

import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
import { eq } from "drizzle-orm";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { spawnSync } from "node:child_process";
export async function init(ppc: PPC) {
ppc.c2vi = new C2vi(ppc);
ppc.cli
.command("bed", "command to run when c2vi goes to bed")
.action(async () => {
await save_habitica_dailies(ppc);
await save_habitica_logs(ppc);
});
ppc.cli
.command("clearTodos", "command to run when c2vi goes to bed")
.alias("clt")
.action(async () => {
const todos = await ppc.habitica.get_tasks();
for (const todo of todos) {
await ppc.habitica.delete_task(todo.id);
}
});
ppc.cli
.command("listTodos", "command to run when c2vi goes to bed")
.alias("dut")
.action(async () => {
const todos = await ppc.habitica.get_tasks();
for (const todo of todos) {
console.log(todo.text);
}
});
ppc.cli
.command("buyHealthPotion", "command to run when c2vi goes to bed")
.alias("buyh")
.action(async () => {
await ppc.habitica.api_request("POST", "/user/buy-health-potion");
});
ppc.cli
.command("addActionItems", "command to run when c2vi goes to bed")
.alias("aal")
.action(async () => {
await add_action_items(ppc);
});
ppc.cli
.command("test", "a command to run some test code")
.alias("t")
.action(async () => {});
ppc.cli
.command("skip [num]", "command to skip a habitica todo")
.alias("sk")
.action((num = 1) => {
skip_habitica_todo(ppc, parseInt(num));
});
ppc.cli.command("dumpLog", "dump the complete habitica log").action(() => {
dump_habitica_logs(ppc);
});
ppc.cli
.command("listDailies", "list all dailies")
.action(async (folder, options) => {
const allDailies = await ppc.c2vi.db.select().from(dailiesTable).all();
for (const task of allDailies) {
console.log(task.id + ": " + task.text);
}
});
ppc.cli
.command("printTask <id>", "print a specific todo")
.action(async (id) => {
const data = await ppc.habitica.api_request("GET", `/tasks/${id}`, {});
console.log(data.history);
});
}
class C2vi {
[key: string]: any;
constructor(ppc: PPC) {
this.ppc = ppc;
this.db = drizzle(
createClient({
url: "file://" + ppc.config.local_storage_path + "/data.db",
}),
);
}
}
function formatDate(d) {
const pad = (n) => n.toString().padStart(2, "0");
// Custom YYYY-MM-DD format
const formatted = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
return formatted;
}
// ============= SCHEMA DEFINITION =============
const dailiesTable = sqliteTable("habitica_dailies", {
id: text("id").primaryKey(),
text: text("text").notNull(),
notes: text("notes"),
priority: text("priority"), // 0.1, 1, 1.5, 2
frequency: text("frequency"), // 'daily', 'weekly', etc.
});
const habiticaLogTable = sqliteTable("habitica_log", {
date: text("date").primaryKey(),
data: text("data").notNull(),
});
async function save_habitica_dailies(ppc) {
console.log("######### cli bed cmd #########");
await ppc.c2vi.db.run(
"CREATE TABLE IF NOT EXISTS habitica_dailies (id TEXT PRIMARY KEY, text TEXT NOT NULL, notes TEXT, priority TEXT, frequency TEXT)",
);
await ppc.c2vi.db.run(
"CREATE TABLE IF NOT EXISTS habitica_log (date TEXT PRIMARY KEY, data TEXT NOT NULL)",
);
// ============= DATABASE INITIALIZATION =============
if (!ppc.config.habitica.user_id || !ppc.config.habitica.api_token) {
console.error(
"Error: HABITICA_USER_ID and HABITICA_API_TOKEN env vars required",
);
console.error(
"Get these from Habitica Settings > API and configure them in the ppc.config",
);
process.exit(1);
}
const dailies = await ppc.habitica.get_tasks("dailys");
await writeDailiesToDatabase(dailies, ppc);
}
async function dump_habitica_logs(ppc) {
const entries = await ppc.c2vi.db.select().from(habiticaLogTable).all();
for (const entry of entries) {
console.log(entry.date + ": ", JSON.parse(entry.data));
}
}
async function skip_habitica_todo(ppc, num) {
await ppc.c2vi.db.run(
"CREATE TABLE IF NOT EXISTS habitica_dailies (id TEXT PRIMARY KEY, text TEXT NOT NULL, notes TEXT, priority TEXT, frequency TEXT)",
);
await ppc.c2vi.db.run(
"CREATE TABLE IF NOT EXISTS habitica_log (date TEXT PRIMARY KEY, data TEXT NOT NULL)",
);
const todos = await ppc.habitica.get_tasks("todos");
const date = new Date();
const save_for_yesterday = date.getHours() < 12;
if (save_for_yesterday) {
console.log(
"Saving logs for yesterday!!!! because it is " +
date.getHours() +
"hours of the day",
);
date.setDate(date.getDate() - 1);
}
const result = await ppc.c2vi.db
.select()
.from(habiticaLogTable)
.where(eq(habiticaLogTable.date, formatDate(date)));
const data = result[0]
? JSON.parse(result[0].data)
: {
dailies_done: [],
dailies_skipped: [],
todos_done: [],
todos_skipped: [],
};
for (let i = 0; i < num; i++) {
const todo = todos[i];
if (!todo) continue;
await ppc.habitica.delete_task(todo.id);
await incrementHabit(ppc);
data.todos_skipped.push({
id: todo.id,
text: todo.text,
notes: todo.notes || "",
checklist: todo.checklist || [],
tags: todo.tags || [],
});
}
// saving to db
await ppc.c2vi.db
.insert(habiticaLogTable)
.values({ date: formatDate(date), data: JSON.stringify(data) })
.onConflictDoUpdate({
target: habiticaLogTable.date,
set: {
data: JSON.stringify(data),
},
});
}
async function save_habitica_logs(ppc) {
const date = new Date();
const save_for_yesterday = date.getHours() < 12;
if (save_for_yesterday) {
console.log(
"Saving logs for yesterday!!!! because it is " +
date.getHours() +
"hours of the day",
);
date.setDate(date.getDate() - 1);
}
const result = await ppc.c2vi.db
.select()
.from(habiticaLogTable)
.where(eq(habiticaLogTable.date, formatDate(date)));
const data = result[0]
? JSON.parse(result[0].data)
: {
dailies_done: [],
dailies_skipped: [],
todos_done: [],
todos_skipped: [],
};
// dailies
const dailies = await ppc.habitica.get_tasks("dailys");
for (const daily of dailies) {
let completed = daily.completed;
let isDue = daily.isDue;
if (save_for_yesterday) {
const yesterday = daily.history.reduce((prev, current) => {
return prev.date > current.date ? prev : current;
});
completed = yesterday.completed;
isDue = yesterday.isDue;
}
if (!isDue) {
continue;
}
if (completed) {
data.dailies_done.push(daily.id);
console.log("daily-done: " + daily.text);
} else {
data.dailies_skipped.push(daily.id);
console.log("daily-skipped: " + daily.text);
}
}
// todos done
const todos = await ppc.habitica.get_tasks("completedTodos");
for (const todo of todos) {
const dateCompleted = new Date(todo.dateCompleted);
// check if this todo is already saved in yesterday's log entry
if (save_for_yesterday) {
// get yesterday log entry
const result = await ppc.c2vi.db
.select()
.from(habiticaLogTable)
.where(eq(habiticaLogTable.date, formatDate(date)));
const data = result[0]
? JSON.parse(result[0].data)
: {
dailies_done: [],
dailies_skipped: [],
todos_done: [],
todos_skipped: [],
};
// check if this todo is already saved in yesterday's log entry
const todoIndex = data.todos_done.findIndex((t) => t.id === todo.id);
if (todoIndex !== -1) {
continue; // skipp saving this todo
}
}
if (dateCompleted.toDateString() == date.toDateString()) {
console.log("todo-done:", todo.text);
data.todos_done.push({
id: todo.id,
text: todo.text,
notes: todo.notes || "",
checklist: todo.checklist || [],
tags: todo.tags || [],
});
}
// we save_for_yesterday but the todo was completed today... also save that for this day
if (
save_for_yesterday &&
dateCompleted.toDateString() == new Date().toDateString()
) {
console.log("todo-done:", todo.text);
data.todos_done.push({
id: todo.id,
text: todo.text,
notes: todo.notes || "",
checklist: todo.checklist || [],
tags: todo.tags || [],
});
}
}
// saving to db
await ppc.c2vi.db
.insert(habiticaLogTable)
.values({ date: formatDate(date), data: JSON.stringify(data) })
.onConflictDoUpdate({
target: habiticaLogTable.date,
set: {
data: JSON.stringify(data),
},
});
}
async function writeDailiesToDatabase(dailies, ppc) {
console.log(`Syncing ${dailies.length} dailies to database...`);
const records = dailies.map((daily) => ({
id: daily.id,
text: daily.text,
notes: daily.notes || "",
priority: daily.priority?.toString() || null,
frequency: daily.frequency || "daily",
}));
async function upsertMany(records) {
for (const record of records) {
await ppc.c2vi.db
.insert(dailiesTable)
.values(record)
.onConflictDoUpdate({
target: dailiesTable.id,
set: {
text: record.text,
notes: record.notes,
priority: record.priority,
frequency: record.frequency,
},
});
}
}
await upsertMany(records);
}
async function incrementHabit(ppc) {
const habits = await ppc.habitica.get_tasks("habits");
let id = "";
for (const habit of habits) {
if ((habit.text = "skipped a planned task")) {
id = habit.id;
}
}
await ppc.habitica.api_request("POST", `tasks/${id}/score/down`);
}
async function add_action_items(ppc: PPC) {
let task_ids = [];
const tmpFile = path.join(os.tmpdir(), `habitica-tasks-${Date.now()}.md`);
const initialContent = `
gu 10min TIMER
uw 15min FROG
b coffee/tee/kakau
uw FROG
b breakfast, brush teeth, lüften
uw FROG
lunch
uw 15min TIMER of next days FROG
fw
dt 30min TIMER
fun 20:00 (smth that can be stopped easily)
se 21:40
read
bed 22:10
`;
fs.writeFileSync(tmpFile, initialContent.trim());
spawnSync("nvim", [tmpFile], { stdio: "inherit" });
// read file
const listStr = fs.readFileSync(tmpFile, "utf8");
// delete file
fs.unlinkSync(tmpFile);
// Simple parser: filter out empty lines
const tasks = listStr
.split("\n")
.map((t) => t.trim())
.filter((t) => t.length > 0);
// 5. Add tasks to Habitica
for (const todoText of tasks) {
const data = await ppc.habitica.api_request(
"POST",
"tasks/user",
{},
{ text: todoText, type: "todo" },
);
task_ids.push(data.id);
console.log(`Added to-do: '${todoText}' with id '${data.id}'`);
}
// 6. Move tasks to bottom
for (const taskId of task_ids) {
await ppc.habitica.api_request("POST", `tasks/${taskId}/move/to/-1`);
}
}