Skip to content

Commit e6ace48

Browse files
committed
refactor!: migrate task and journal tools to TypeScript with YAML storage
1 parent 74f7406 commit e6ace48

9 files changed

Lines changed: 423 additions & 822 deletions

File tree

.opencode/AGENTS.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,20 @@ This is a personal starter project using OpenCode for AI-assisted development.
2828
## Workflow Conventions
2929

3030
### Task Management
31-
- Use the `task` tool to manage TASKS.md
31+
- Use the `task` tool to manage tasks in `tasks.yaml`
3232
- All task modifications must go through the tool
33-
- Task format: `- [status] **ID** Label: Description (Complexity: X) [Deps: Y] (See plan: Z)`
33+
- Storage: `tasks.yaml` (YAML format)
34+
- Actions: `add`, `start`, `cancel`, `archive`, `attach-plan`, `list`
3435

3536
### Journaling
3637
- Use the `journal` tool to add daily entries
37-
- Format: `[YYYY-MM-DDTHH:MM:SS] - description`
38-
- Journal entries must be newer than recent changes for commits
38+
- Storage: `journal/YYYY-MM-DD.yaml` (YAML format)
39+
- Actions: `add`, `list`
3940

4041
### Planning
4142
- Plans are saved to `plans/` directory
4243
- Filenames: kebab-case (e.g., `plans/implement-auth.md`)
43-
- Link plans to tasks via `task` tool
44+
- Link plans to tasks via `task attach-plan` action
4445

4546
### Pre-commit Validation
4647
- Run `git_precommit` tool before committing
@@ -51,11 +52,11 @@ This is a personal starter project using OpenCode for AI-assisted development.
5152
.opencode/
5253
├── agents/ # Agent definitions
5354
├── commands/ # High-level commands
54-
├── tools/ # Custom tools
55+
├── tools/ # Custom tools (TypeScript)
5556
└── style-guide.md # Prose style rules
5657
5758
plans/ # Saved plans
58-
journal/ # Daily journal entries
59+
journal/ # Daily journal entries (YAML)
5960
research/ # Research assets
60-
TASKS.md # Project roadmap
61+
tasks.yaml # Project roadmap (YAML)
6162
```

.opencode/tools/journal.py

Lines changed: 0 additions & 27 deletions
This file was deleted.

.opencode/tools/journal.ts

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,109 @@
1-
import { tool } from "@opencode-ai/plugin"
2-
import { $ } from "bun"
1+
import { tool } from "@opencode-ai/plugin";
2+
3+
interface JournalEntry {
4+
timestamp: string;
5+
message: string;
6+
}
7+
8+
interface JournalData {
9+
date: string;
10+
entries: JournalEntry[];
11+
}
12+
13+
const JOURNAL_DIR = "journal";
14+
15+
function getToday(): string {
16+
return new Date().toISOString().split("T")[0];
17+
}
18+
19+
function getJournalPath(date: string): string {
20+
return `${JOURNAL_DIR}/${date}.yaml`;
21+
}
22+
23+
async function loadJournal(date: string): Promise<JournalData> {
24+
const path = getJournalPath(date);
25+
const file = Bun.file(path);
26+
if (!await file.exists()) {
27+
return { date, entries: [] };
28+
}
29+
const content = await file.text();
30+
if (!content.trim()) {
31+
return { date, entries: [] };
32+
}
33+
return parseYaml(content, date);
34+
}
35+
36+
function parseYaml(content: string, date: string): JournalData {
37+
const lines = content.split("\n");
38+
const data: JournalData = { date, entries: [] };
39+
let inEntries = false;
40+
41+
for (const line of lines) {
42+
if (line.startsWith("date:")) {
43+
const match = line.match(/date: "(.+)"/);
44+
if (match) data.date = match[1];
45+
} else if (line.startsWith("entries:")) {
46+
inEntries = true;
47+
} else if (inEntries && line.match(/^ - timestamp:/)) {
48+
const match = line.match(/timestamp: "(.+)"/);
49+
if (match) {
50+
data.entries.push({ timestamp: match[1], message: "" });
51+
}
52+
} else if (inEntries && line.match(/^ message:/) && data.entries.length > 0) {
53+
const match = line.match(/message: "(.+)"/);
54+
if (match) {
55+
data.entries[data.entries.length - 1].message = match[1];
56+
}
57+
}
58+
}
59+
60+
return data;
61+
}
62+
63+
function toYaml(data: JournalData): string {
64+
const lines: string[] = [];
65+
lines.push(`date: "${data.date}"`);
66+
lines.push("entries:");
67+
for (const entry of data.entries) {
68+
lines.push(` - timestamp: "${entry.timestamp}"`);
69+
lines.push(` message: "${entry.message}"`);
70+
}
71+
return lines.join("\n") + "\n";
72+
}
373

474
export default tool({
5-
description: "Add a journal entry with timestamp for today's date",
75+
description: "Manage journal entries. Actions: add (add entry for today), list (show today's entries)",
676
args: {
7-
description: tool.schema.string().describe("Journal entry description"),
77+
action: tool.schema.string().describe("Action: add or list"),
78+
message: tool.schema.string().optional().describe("Journal entry message (for add)"),
879
},
980
async execute(args) {
10-
const script = ".opencode/tools/journal.py"
11-
const result = await $`python3 ${script} ${args.description}`.text()
12-
return result
81+
const action = args.action || "add";
82+
const now = new Date();
83+
const timestamp = now.toISOString();
84+
const today = now.toISOString().split("T")[0];
85+
86+
if (action === "list") {
87+
const data = await loadJournal(today);
88+
if (data.entries.length === 0) {
89+
return `No journal entries for ${today}`;
90+
}
91+
let output = `# Journal - ${today}\n\n`;
92+
for (const entry of data.entries) {
93+
const time = entry.timestamp.split("T")[1].split(".")[0];
94+
output += `- [${time}] ${entry.message}\n`;
95+
}
96+
return output;
97+
}
98+
99+
const message = args.message;
100+
if (!message) {
101+
return "Error: --message is required for add action";
102+
}
103+
104+
const data = await loadJournal(today);
105+
data.entries.push({ timestamp, message });
106+
await Bun.write(getJournalPath(today), toYaml(data));
107+
return `Added journal entry: [${timestamp}] - ${message}`;
13108
},
14-
})
109+
});

.opencode/tools/pre-commit.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import subprocess
44
import sys
5+
import re
56
from datetime import date, datetime
67

78
def run_command(command):
@@ -10,7 +11,7 @@ def run_command(command):
1011

1112
def main():
1213
today = date.today().strftime("%Y-%m-%d")
13-
journal_path = f"journal/{today}.md"
14+
journal_path = f"journal/{today}.yaml"
1415

1516
res = run_command("git status --porcelain")
1617
changed_files = [line[3:] for line in res.stdout.strip().splitlines() if line]
@@ -27,7 +28,7 @@ def main():
2728
print(res.stderr)
2829
return res.returncode
2930

30-
meaningful_changes = [f for f in changed_files if not f.startswith(".gemini") and not f.startswith(".opencode") and f != journal_path]
31+
meaningful_changes = [f for f in changed_files if not f.startswith(".gemini") and not f.startswith(".opencode") and f != journal_path and not f.endswith(".yaml")]
3132

3233
if not meaningful_changes:
3334
return 0
@@ -41,36 +42,32 @@ def main():
4142

4243
if not os.path.exists(journal_path):
4344
print(f"Error: Updated journal required. Please read the latest entries in journal/ to understand context,")
44-
print(f"then use: journal <one line summary of changes>")
45+
print(f"then use: journal add \"one line summary of changes\"")
4546
return 1
4647

4748
with open(journal_path, "r") as f:
48-
lines = [l.strip() for l in f.readlines() if l.strip()]
49-
50-
if not lines:
51-
print(f"Error: Journal {journal_path} is empty.")
52-
print(f"Please use: journal <one line summary of changes>")
53-
return 1
54-
55-
last_line = lines[-1]
56-
if not (last_line.startswith("[") and "] - " in last_line):
57-
print(f"Error: Invalid journal entry format in {journal_path}: '{last_line}'")
58-
print(f"Expected format: '[YYYY-MM-DDTHH:MM:SS] - description'")
59-
print(f"Please use: journal <one line summary of changes>")
49+
content = f.read()
50+
51+
# Parse YAML to find last timestamp
52+
# Look for pattern: timestamp: "YYYY-MM-DDTHH:MM:SS"
53+
timestamps = re.findall(r'timestamp: "(.+)"', content)
54+
55+
if not timestamps:
56+
print(f"Error: No journal entries found in {journal_path}")
57+
print(f"Please use: journal add \"one line summary of changes\"")
6058
return 1
6159

6260
try:
63-
ts_str = last_line[1:last_line.index("]")]
64-
last_entry_time = datetime.fromisoformat(ts_str).timestamp()
61+
last_entry_time = datetime.fromisoformat(timestamps[-1]).timestamp()
6562
except (ValueError, IndexError):
66-
print(f"Error: Could not parse timestamp from journal entry: '{last_line}'")
67-
print(f"Please use: journal <one line summary of changes>")
63+
print(f"Error: Could not parse timestamp from journal entry: '{timestamps[-1]}'")
64+
print(f"Please use: journal add \"one line summary of changes\"")
6865
return 1
6966

7067
if last_entry_time < max_mtime:
7168
print(f"Error: Last journal entry is older than recent changes.")
7269
print(f"Please read {journal_path} to catch up, then add a summary of your latest work using:")
73-
print(f"journal <one line summary of changes>")
70+
print(f"journal add \"one line summary of changes\"")
7471
return 1
7572

7673
return 0

0 commit comments

Comments
 (0)