chronicle-ai 0.0.1

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.
Files changed (155) hide show
  1. package/README.md +132 -0
  2. package/package.json +86 -0
  3. package/packages/README.md +139 -0
  4. package/packages/cli/README.md +92 -0
  5. package/packages/cli/bin/chronicle.js +2 -0
  6. package/packages/cli/dist/commands/add.d.ts +2 -0
  7. package/packages/cli/dist/commands/add.d.ts.map +1 -0
  8. package/packages/cli/dist/commands/add.js +82 -0
  9. package/packages/cli/dist/commands/add.js.map +1 -0
  10. package/packages/cli/dist/commands/diff.d.ts +11 -0
  11. package/packages/cli/dist/commands/diff.d.ts.map +1 -0
  12. package/packages/cli/dist/commands/diff.js +164 -0
  13. package/packages/cli/dist/commands/diff.js.map +1 -0
  14. package/packages/cli/dist/commands/init.d.ts +2 -0
  15. package/packages/cli/dist/commands/init.d.ts.map +1 -0
  16. package/packages/cli/dist/commands/init.js +54 -0
  17. package/packages/cli/dist/commands/init.js.map +1 -0
  18. package/packages/cli/dist/commands/list.d.ts +2 -0
  19. package/packages/cli/dist/commands/list.d.ts.map +1 -0
  20. package/packages/cli/dist/commands/list.js +62 -0
  21. package/packages/cli/dist/commands/list.js.map +1 -0
  22. package/packages/cli/dist/commands/log.d.ts +4 -0
  23. package/packages/cli/dist/commands/log.d.ts.map +1 -0
  24. package/packages/cli/dist/commands/log.js +223 -0
  25. package/packages/cli/dist/commands/log.js.map +1 -0
  26. package/packages/cli/dist/commands/pause.d.ts +3 -0
  27. package/packages/cli/dist/commands/pause.d.ts.map +1 -0
  28. package/packages/cli/dist/commands/pause.js +49 -0
  29. package/packages/cli/dist/commands/pause.js.map +1 -0
  30. package/packages/cli/dist/commands/queue.d.ts +2 -0
  31. package/packages/cli/dist/commands/queue.d.ts.map +1 -0
  32. package/packages/cli/dist/commands/queue.js +96 -0
  33. package/packages/cli/dist/commands/queue.js.map +1 -0
  34. package/packages/cli/dist/commands/remove.d.ts +2 -0
  35. package/packages/cli/dist/commands/remove.d.ts.map +1 -0
  36. package/packages/cli/dist/commands/remove.js +80 -0
  37. package/packages/cli/dist/commands/remove.js.map +1 -0
  38. package/packages/cli/dist/commands/reset.d.ts +5 -0
  39. package/packages/cli/dist/commands/reset.d.ts.map +1 -0
  40. package/packages/cli/dist/commands/reset.js +90 -0
  41. package/packages/cli/dist/commands/reset.js.map +1 -0
  42. package/packages/cli/dist/commands/restart.d.ts +2 -0
  43. package/packages/cli/dist/commands/restart.d.ts.map +1 -0
  44. package/packages/cli/dist/commands/restart.js +72 -0
  45. package/packages/cli/dist/commands/restart.js.map +1 -0
  46. package/packages/cli/dist/commands/start.d.ts +2 -0
  47. package/packages/cli/dist/commands/start.d.ts.map +1 -0
  48. package/packages/cli/dist/commands/start.js +127 -0
  49. package/packages/cli/dist/commands/start.js.map +1 -0
  50. package/packages/cli/dist/commands/status.d.ts +2 -0
  51. package/packages/cli/dist/commands/status.d.ts.map +1 -0
  52. package/packages/cli/dist/commands/status.js +181 -0
  53. package/packages/cli/dist/commands/status.js.map +1 -0
  54. package/packages/cli/dist/commands/stop.d.ts +2 -0
  55. package/packages/cli/dist/commands/stop.d.ts.map +1 -0
  56. package/packages/cli/dist/commands/stop.js +64 -0
  57. package/packages/cli/dist/commands/stop.js.map +1 -0
  58. package/packages/cli/dist/index.d.ts +2 -0
  59. package/packages/cli/dist/index.d.ts.map +1 -0
  60. package/packages/cli/dist/index.js +85 -0
  61. package/packages/cli/dist/index.js.map +1 -0
  62. package/packages/cli/dist/utils/paths.d.ts +26 -0
  63. package/packages/cli/dist/utils/paths.d.ts.map +1 -0
  64. package/packages/cli/dist/utils/paths.js +183 -0
  65. package/packages/cli/dist/utils/paths.js.map +1 -0
  66. package/packages/cli/package.json +25 -0
  67. package/packages/daemon/README.md +83 -0
  68. package/packages/daemon/dist/ai_test.d.ts +2 -0
  69. package/packages/daemon/dist/ai_test.d.ts.map +1 -0
  70. package/packages/daemon/dist/ai_test.js +4 -0
  71. package/packages/daemon/dist/ai_test.js.map +1 -0
  72. package/packages/daemon/dist/index.d.ts +2 -0
  73. package/packages/daemon/dist/index.d.ts.map +1 -0
  74. package/packages/daemon/dist/index.js +147 -0
  75. package/packages/daemon/dist/index.js.map +1 -0
  76. package/packages/daemon/dist/jobs/AIProcessor.d.ts +6 -0
  77. package/packages/daemon/dist/jobs/AIProcessor.d.ts.map +1 -0
  78. package/packages/daemon/dist/jobs/AIProcessor.js +58 -0
  79. package/packages/daemon/dist/jobs/AIProcessor.js.map +1 -0
  80. package/packages/daemon/dist/jobs/DocProcessor.d.ts +8 -0
  81. package/packages/daemon/dist/jobs/DocProcessor.d.ts.map +1 -0
  82. package/packages/daemon/dist/jobs/DocProcessor.js +336 -0
  83. package/packages/daemon/dist/jobs/DocProcessor.js.map +1 -0
  84. package/packages/daemon/dist/jobs/FileProcessor.d.ts +7 -0
  85. package/packages/daemon/dist/jobs/FileProcessor.d.ts.map +1 -0
  86. package/packages/daemon/dist/jobs/FileProcessor.js +29 -0
  87. package/packages/daemon/dist/jobs/FileProcessor.js.map +1 -0
  88. package/packages/daemon/dist/jobs/SystemProcessor.d.ts +13 -0
  89. package/packages/daemon/dist/jobs/SystemProcessor.d.ts.map +1 -0
  90. package/packages/daemon/dist/jobs/SystemProcessor.js +38 -0
  91. package/packages/daemon/dist/jobs/SystemProcessor.js.map +1 -0
  92. package/packages/daemon/dist/jobs/UpdateProcessor.d.ts +21 -0
  93. package/packages/daemon/dist/jobs/UpdateProcessor.d.ts.map +1 -0
  94. package/packages/daemon/dist/jobs/UpdateProcessor.js +222 -0
  95. package/packages/daemon/dist/jobs/UpdateProcessor.js.map +1 -0
  96. package/packages/daemon/dist/services/AIService.d.ts +90 -0
  97. package/packages/daemon/dist/services/AIService.d.ts.map +1 -0
  98. package/packages/daemon/dist/services/AIService.js +451 -0
  99. package/packages/daemon/dist/services/AIService.js.map +1 -0
  100. package/packages/daemon/dist/services/ConfigService.d.ts +30 -0
  101. package/packages/daemon/dist/services/ConfigService.d.ts.map +1 -0
  102. package/packages/daemon/dist/services/ConfigService.js +69 -0
  103. package/packages/daemon/dist/services/ConfigService.js.map +1 -0
  104. package/packages/daemon/dist/services/DatabaseService.d.ts +204 -0
  105. package/packages/daemon/dist/services/DatabaseService.d.ts.map +1 -0
  106. package/packages/daemon/dist/services/DatabaseService.js +692 -0
  107. package/packages/daemon/dist/services/DatabaseService.js.map +1 -0
  108. package/packages/daemon/dist/services/GitService.d.ts +12 -0
  109. package/packages/daemon/dist/services/GitService.d.ts.map +1 -0
  110. package/packages/daemon/dist/services/GitService.js +68 -0
  111. package/packages/daemon/dist/services/GitService.js.map +1 -0
  112. package/packages/daemon/dist/services/QueueService.d.ts +16 -0
  113. package/packages/daemon/dist/services/QueueService.d.ts.map +1 -0
  114. package/packages/daemon/dist/services/QueueService.js +87 -0
  115. package/packages/daemon/dist/services/QueueService.js.map +1 -0
  116. package/packages/daemon/dist/services/TriggerService.d.ts +37 -0
  117. package/packages/daemon/dist/services/TriggerService.d.ts.map +1 -0
  118. package/packages/daemon/dist/services/TriggerService.js +150 -0
  119. package/packages/daemon/dist/services/TriggerService.js.map +1 -0
  120. package/packages/daemon/dist/services/WatcherService.d.ts +12 -0
  121. package/packages/daemon/dist/services/WatcherService.d.ts.map +1 -0
  122. package/packages/daemon/dist/services/WatcherService.js +77 -0
  123. package/packages/daemon/dist/services/WatcherService.js.map +1 -0
  124. package/packages/daemon/dist/services/index.d.ts +2 -0
  125. package/packages/daemon/dist/services/index.d.ts.map +1 -0
  126. package/packages/daemon/dist/services/index.js +18 -0
  127. package/packages/daemon/dist/services/index.js.map +1 -0
  128. package/packages/daemon/dist/test-ignore.js +0 -0
  129. package/packages/daemon/package.json +28 -0
  130. package/packages/ui/app/actions.ts +73 -0
  131. package/packages/ui/app/api/ai-activity/route.ts +14 -0
  132. package/packages/ui/app/globals.css +98 -0
  133. package/packages/ui/app/layout.tsx +29 -0
  134. package/packages/ui/app/page.tsx +109 -0
  135. package/packages/ui/components/AutoRefresh.tsx +18 -0
  136. package/packages/ui/components/DocumentationViewer.tsx +276 -0
  137. package/packages/ui/components/FileList.tsx +69 -0
  138. package/packages/ui/components/HeaderWithThinking.tsx +102 -0
  139. package/packages/ui/components/JobQueue.tsx +194 -0
  140. package/packages/ui/components/JobQueueWrapper.tsx +31 -0
  141. package/packages/ui/components/MermaidInit.tsx +24 -0
  142. package/packages/ui/components/ProjectContentArea.tsx +186 -0
  143. package/packages/ui/components/ProjectList.tsx +136 -0
  144. package/packages/ui/components/ThinkingDrawer.tsx +377 -0
  145. package/packages/ui/components/ThinkingPanel.tsx +63 -0
  146. package/packages/ui/components/TriggerSettings.tsx +185 -0
  147. package/packages/ui/components/VersionSelector.tsx +132 -0
  148. package/packages/ui/lib/db.ts +521 -0
  149. package/packages/ui/next-env.d.ts +5 -0
  150. package/packages/ui/next.config.js +4 -0
  151. package/packages/ui/package.json +32 -0
  152. package/packages/ui/postcss.config.js +6 -0
  153. package/packages/ui/public/logo.png +0 -0
  154. package/packages/ui/tailwind.config.ts +32 -0
  155. package/packages/ui/tsconfig.json +40 -0
@@ -0,0 +1,132 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Clock, GitCommit, CheckCircle, AlertCircle, Archive } from 'lucide-react';
5
+ import { clsx } from 'clsx';
6
+
7
+ interface DocVersion {
8
+ id: number;
9
+ version_number: number;
10
+ trigger_type: string;
11
+ commit_hash: string | null;
12
+ status: 'draft' | 'pending_review' | 'active' | 'archived';
13
+ created_at: number;
14
+ }
15
+
16
+ interface Props {
17
+ versions: DocVersion[];
18
+ currentVersionId: number | null;
19
+ onVersionChange: (versionId: number) => void;
20
+ }
21
+
22
+ const statusIcons = {
23
+ draft: Clock,
24
+ pending_review: AlertCircle,
25
+ active: CheckCircle,
26
+ archived: Archive
27
+ };
28
+
29
+ const statusColors = {
30
+ draft: 'text-gray-400',
31
+ pending_review: 'text-amber-500',
32
+ active: 'text-green-500',
33
+ archived: 'text-gray-300'
34
+ };
35
+
36
+ export function VersionSelector({ versions, currentVersionId, onVersionChange }: Props) {
37
+ const [isOpen, setIsOpen] = useState(false);
38
+
39
+ const currentVersion = versions.find(v => v.id === currentVersionId) || versions[0];
40
+
41
+ if (!currentVersion) return null;
42
+
43
+ const StatusIcon = statusIcons[currentVersion.status];
44
+
45
+ return (
46
+ <div className="relative">
47
+ <button
48
+ onClick={() => setIsOpen(!isOpen)}
49
+ className={clsx(
50
+ "flex items-center gap-1.5 px-2 py-1 rounded text-[10px] font-mono",
51
+ "bg-gray-100 hover:bg-gray-200 transition-colors"
52
+ )}
53
+ >
54
+ <StatusIcon size={12} className={statusColors[currentVersion.status]} />
55
+ <span>v{currentVersion.version_number}</span>
56
+ {currentVersion.commit_hash && (
57
+ <span className="text-gray-400">
58
+ ({currentVersion.commit_hash.slice(0, 7)})
59
+ </span>
60
+ )}
61
+ </button>
62
+
63
+ {isOpen && (
64
+ <>
65
+ <div
66
+ className="fixed inset-0 z-10"
67
+ onClick={() => setIsOpen(false)}
68
+ />
69
+ <div className="absolute right-0 top-full mt-1 z-20 bg-white border border-gray-200 rounded-lg shadow-lg min-w-[200px] max-h-[300px] overflow-y-auto">
70
+ <div className="p-2 border-b border-gray-100">
71
+ <span className="text-[10px] font-bold uppercase tracking-wider text-gray-400">
72
+ Versions
73
+ </span>
74
+ </div>
75
+ <div className="py-1">
76
+ {versions.map((version) => {
77
+ const Icon = statusIcons[version.status];
78
+ const isActive = version.id === currentVersionId;
79
+ const date = new Date(version.created_at * 1000);
80
+
81
+ return (
82
+ <button
83
+ key={version.id}
84
+ onClick={() => {
85
+ onVersionChange(version.id);
86
+ setIsOpen(false);
87
+ }}
88
+ className={clsx(
89
+ "w-full px-3 py-2 flex items-center gap-2 hover:bg-gray-50 text-left",
90
+ isActive && "bg-blue-50"
91
+ )}
92
+ >
93
+ <Icon size={14} className={statusColors[version.status]} />
94
+ <div className="flex-1 min-w-0">
95
+ <div className="flex items-center gap-2">
96
+ <span className="text-xs font-medium">
97
+ v{version.version_number}
98
+ </span>
99
+ {version.status === 'active' && (
100
+ <span className="text-[9px] bg-green-100 text-green-600 px-1 rounded">
101
+ CURRENT
102
+ </span>
103
+ )}
104
+ {version.status === 'pending_review' && (
105
+ <span className="text-[9px] bg-amber-100 text-amber-600 px-1 rounded">
106
+ REVIEW
107
+ </span>
108
+ )}
109
+ </div>
110
+ <div className="text-[10px] text-gray-400 flex items-center gap-1">
111
+ <span>{version.trigger_type}</span>
112
+ <span>•</span>
113
+ <span>{date.toLocaleDateString()}</span>
114
+ {version.commit_hash && (
115
+ <>
116
+ <span>•</span>
117
+ <GitCommit size={10} />
118
+ <span>{version.commit_hash.slice(0, 7)}</span>
119
+ </>
120
+ )}
121
+ </div>
122
+ </div>
123
+ </button>
124
+ );
125
+ })}
126
+ </div>
127
+ </div>
128
+ </>
129
+ )}
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,521 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { marked } from 'marked';
5
+
6
+ // Configure marked to render mermaid blocks as divs
7
+ const renderer = new marked.Renderer();
8
+ renderer.code = ({ text, lang, escaped }: any) => {
9
+ const code = text;
10
+ const language = (lang || '').match(/\S*/)[0];
11
+
12
+ if (language === 'mermaid') {
13
+ return `<div class="mermaid">${code}</div>`;
14
+ }
15
+
16
+ const className = language ? `class="language-${language}"` : '';
17
+ return `<pre><code ${className}>${code}</code></pre>\n`;
18
+ };
19
+
20
+ marked.use({ renderer });
21
+
22
+ // Connect to the Daemon's DB
23
+ // Assumes UI and Daemon are in the same mono-repo structure
24
+ const DB_PATH = path.resolve(process.cwd(), '../daemon/.chronicle/db.sqlite');
25
+
26
+ // Lazy database connection - handle build-time scenarios where DB doesn't exist
27
+ let _db: Database.Database | null = null;
28
+
29
+ function getDb(): Database.Database | null {
30
+ if (_db) return _db;
31
+
32
+ // Check if database file exists before attempting to open
33
+ if (!require('fs').existsSync(DB_PATH)) {
34
+ console.warn('Database not found at:', DB_PATH);
35
+ return null;
36
+ }
37
+
38
+ try {
39
+ _db = new Database(DB_PATH, {
40
+ readonly: false,
41
+ fileMustExist: true
42
+ });
43
+ return _db;
44
+ } catch (error) {
45
+ console.error('Failed to connect to database:', error);
46
+ return null;
47
+ }
48
+ }
49
+
50
+ // For backward compatibility - functions will check if db is null
51
+ const db = {
52
+ prepare: (sql: string) => {
53
+ const database = getDb();
54
+ if (!database) {
55
+ throw new Error('Database not available');
56
+ }
57
+ return database.prepare(sql);
58
+ }
59
+ };
60
+
61
+ // ===== Stats =====
62
+
63
+ export interface FileRecord {
64
+ id: number;
65
+ path: string;
66
+ hash: string;
67
+ last_modified: number;
68
+ last_indexed: number;
69
+ }
70
+
71
+ export interface JobRecord {
72
+ id: number;
73
+ name: string;
74
+ data: string;
75
+ status: string;
76
+ created_at: number;
77
+ updated_at: number;
78
+ }
79
+
80
+ export function getStats() {
81
+ const fileCount = db.prepare('SELECT COUNT(*) as count FROM files').get() as { count: number };
82
+ const jobStats = db.prepare(`
83
+ SELECT status, COUNT(*) as count
84
+ FROM jobs
85
+ GROUP BY status
86
+ `).all() as { status: string; count: number }[];
87
+
88
+ return {
89
+ files: fileCount.count,
90
+ jobs: jobStats
91
+ };
92
+ }
93
+
94
+ export function getRecentFiles(limit = 50, projectPath?: string) {
95
+ let query = 'SELECT * FROM files';
96
+ const args: any[] = [];
97
+
98
+ if (projectPath) {
99
+ query += ' WHERE path LIKE ?';
100
+ args.push(`${projectPath}%`);
101
+ }
102
+
103
+ query += ' ORDER BY last_indexed DESC LIMIT ?';
104
+ args.push(limit);
105
+
106
+ return db.prepare(query).all(...args) as FileRecord[];
107
+ }
108
+
109
+ export function getProjects() {
110
+ return db.prepare('SELECT * FROM projects ORDER BY created_at DESC').all() as any[];
111
+ }
112
+
113
+ export function getProjectConfig(projectPath: string): any | null {
114
+ try {
115
+ const configPath = path.join(projectPath, '.chronicle.json');
116
+ if (!fs.existsSync(configPath)) {
117
+ return null;
118
+ }
119
+ const content = fs.readFileSync(configPath, 'utf-8');
120
+ return JSON.parse(content);
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ export function getProjectConfigRaw(projectPath: string): string | null {
127
+ try {
128
+ const configPath = path.join(projectPath, '.chronicle.json');
129
+ if (!fs.existsSync(configPath)) {
130
+ return null;
131
+ }
132
+ return fs.readFileSync(configPath, 'utf-8');
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ export function saveProjectConfig(projectPath: string, content: string): boolean {
139
+ try {
140
+ const configPath = path.join(projectPath, '.chronicle.json');
141
+ fs.writeFileSync(configPath, content, 'utf-8');
142
+ return true;
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ export function enqueueJob(name: string, data: any) {
149
+ return db.prepare(`
150
+ INSERT INTO jobs (name, data, status, created_at, updated_at)
151
+ VALUES (?, ?, 'pending', unixepoch(), unixepoch())
152
+ `).run(name, JSON.stringify(data));
153
+ }
154
+
155
+ export function clearQueue() {
156
+ // Get all doc jobs that will be cancelled to reset their stages
157
+ const docJobs = db.prepare(`
158
+ SELECT data FROM jobs
159
+ WHERE (name = 'doc:initialize' OR name = 'doc:update')
160
+ AND (status = 'pending' OR status = 'processing')
161
+ `).all() as { data: string }[];
162
+
163
+ // Cancel current job
164
+ db.prepare(`
165
+ UPDATE jobs
166
+ SET status = 'failed', error = 'Cancelled by user', updated_at = unixepoch()
167
+ WHERE status = 'processing'
168
+ `).run();
169
+
170
+ // Clear pending jobs
171
+ const result = db.prepare(`DELETE FROM jobs WHERE status = 'pending'`).run();
172
+
173
+ // Reset doc stages for affected projects
174
+ for (const job of docJobs) {
175
+ try {
176
+ const jobData = JSON.parse(job.data);
177
+ if (jobData.projectPath) {
178
+ const project = db.prepare(`
179
+ SELECT id FROM projects WHERE path = ?
180
+ `).get(jobData.projectPath) as { id: number } | undefined;
181
+ if (project) {
182
+ resetDocStage(project.id);
183
+ }
184
+ }
185
+ } catch {
186
+ // Skip invalid JSON
187
+ }
188
+ }
189
+
190
+ return result;
191
+ }
192
+
193
+ export function killCurrentJob() {
194
+ // Get the current processing job to reset its stage if it's a doc job
195
+ const currentJob = db.prepare(`
196
+ SELECT name, data FROM jobs WHERE status = 'processing'
197
+ `).get() as { name: string; data: string } | undefined;
198
+
199
+ const result = db.prepare(`
200
+ UPDATE jobs
201
+ SET status = 'failed', error = 'Killed by user', updated_at = unixepoch()
202
+ WHERE status = 'processing'
203
+ `).run();
204
+
205
+ // Reset doc stage if we killed a doc job
206
+ if (currentJob && (currentJob.name === 'doc:initialize' || currentJob.name === 'doc:update')) {
207
+ try {
208
+ const jobData = JSON.parse(currentJob.data);
209
+ if (jobData.projectPath) {
210
+ const project = db.prepare(`
211
+ SELECT id FROM projects WHERE path = ?
212
+ `).get(jobData.projectPath) as { id: number } | undefined;
213
+ if (project) {
214
+ resetDocStage(project.id);
215
+ }
216
+ }
217
+ } catch {
218
+ // Skip invalid JSON
219
+ }
220
+ }
221
+
222
+ return result;
223
+ }
224
+
225
+ export function toggleProjectPaused(path: string, paused: boolean) {
226
+ return db.prepare('UPDATE projects SET is_paused = ? WHERE path = ?').run(paused ? 1 : 0, path);
227
+ }
228
+
229
+ export function getJobs(limit = 20) {
230
+ // Get recent jobs
231
+ return db.prepare('SELECT * FROM jobs ORDER BY created_at DESC LIMIT ?').all(limit) as JobRecord[];
232
+ }
233
+
234
+ /**
235
+ * Reads documentation files from a project's filesystem.
236
+ * Recursively reads all .md files from docs/ folder and subfolders.
237
+ */
238
+ export function getProjectDocs(projectPath: string): DocFile[] {
239
+ const docs: DocFile[] = [];
240
+
241
+ // Root level docs
242
+ const rootDocs = ['README.md', 'chronicle.md'];
243
+ for (const docName of rootDocs) {
244
+ const docPath = path.join(projectPath, docName);
245
+ if (fs.existsSync(docPath)) {
246
+ const content = fs.readFileSync(docPath, 'utf-8');
247
+ docs.push({
248
+ name: docName,
249
+ path: docPath,
250
+ content: marked.parse(content) as string,
251
+ type: 'root'
252
+ });
253
+ }
254
+ }
255
+
256
+ // Recursively read docs folder
257
+ const docsDir = path.join(projectPath, 'docs');
258
+ if (fs.existsSync(docsDir) && fs.statSync(docsDir).isDirectory()) {
259
+ readDocsRecursively(docsDir, docsDir, docs);
260
+ }
261
+
262
+ return docs;
263
+ }
264
+
265
+ function readDocsRecursively(baseDir: string, currentDir: string, docs: DocFile[]): void {
266
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
267
+
268
+ for (const entry of entries) {
269
+ const fullPath = path.join(currentDir, entry.name);
270
+
271
+ if (entry.isDirectory()) {
272
+ // Skip hidden folders
273
+ if (!entry.name.startsWith('.')) {
274
+ readDocsRecursively(baseDir, fullPath, docs);
275
+ }
276
+ } else if (entry.name.endsWith('.md') && !entry.name.startsWith('.')) {
277
+ const relativePath = path.relative(baseDir, fullPath);
278
+ const content = fs.readFileSync(fullPath, 'utf-8');
279
+ docs.push({
280
+ name: entry.name,
281
+ path: fullPath,
282
+ content: marked.parse(content) as string,
283
+ type: 'docs'
284
+ });
285
+ }
286
+ }
287
+ }
288
+
289
+ export interface DocFile {
290
+ name: string;
291
+ path: string;
292
+ content: string;
293
+ type: 'root' | 'docs';
294
+ }
295
+
296
+ /**
297
+ * Checks if there's an active doc job (pending or processing) for a project.
298
+ */
299
+ function hasActiveDocJob(projectPath: string): boolean {
300
+ try {
301
+ const jobs = db.prepare(`
302
+ SELECT name, data, status
303
+ FROM jobs
304
+ WHERE (name = 'doc:initialize' OR name = 'doc:update')
305
+ AND (status = 'pending' OR status = 'processing')
306
+ `).all() as { name: string; data: string; status: string }[];
307
+
308
+ for (const job of jobs) {
309
+ try {
310
+ const jobData = JSON.parse(job.data);
311
+ if (jobData.projectPath === projectPath) {
312
+ return true;
313
+ }
314
+ } catch {
315
+ // Skip invalid JSON
316
+ }
317
+ }
318
+ return false;
319
+ } catch {
320
+ return false;
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Gets the documentation stage progress for a project.
326
+ * Only returns stage if there's an active doc job running.
327
+ */
328
+ export function getDocStage(projectId: number, projectPath?: string): { currentStage: number; totalStages: number } | null {
329
+ try {
330
+ const result = db.prepare(`
331
+ SELECT current_stage, total_stages FROM doc_stages WHERE project_id = ?
332
+ `).get(projectId) as { current_stage: number; total_stages: number } | undefined;
333
+
334
+ if (!result) return null;
335
+
336
+ // Only show stage if there's an active doc job
337
+ if (projectPath && !hasActiveDocJob(projectPath)) {
338
+ return null;
339
+ }
340
+
341
+ return {
342
+ currentStage: result.current_stage,
343
+ totalStages: result.total_stages
344
+ };
345
+ } catch {
346
+ return null;
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Resets the doc stage for a project (sets current_stage to 0).
352
+ */
353
+ export function resetDocStage(projectId: number): void {
354
+ try {
355
+ db.prepare(`
356
+ UPDATE doc_stages
357
+ SET current_stage = 0, last_updated = unixepoch()
358
+ WHERE project_id = ?
359
+ `).run(projectId);
360
+ } catch {
361
+ // Ignore errors
362
+ }
363
+ }
364
+
365
+ // ===== Doc Version Functions =====
366
+
367
+ export interface DocVersion {
368
+ id: number;
369
+ version_number: number;
370
+ trigger_type: string;
371
+ commit_hash: string | null;
372
+ status: 'draft' | 'pending_review' | 'active' | 'archived';
373
+ created_at: number;
374
+ }
375
+
376
+ export function getDocVersions(projectId: number): DocVersion[] {
377
+ try {
378
+ return db.prepare(`
379
+ SELECT id, version_number, trigger_type, commit_hash, status, created_at
380
+ FROM doc_versions
381
+ WHERE project_id = ?
382
+ ORDER BY version_number DESC
383
+ `).all(projectId) as DocVersion[];
384
+ } catch {
385
+ return [];
386
+ }
387
+ }
388
+
389
+ export function getActiveDocVersion(projectId: number): DocVersion | null {
390
+ try {
391
+ return db.prepare(`
392
+ SELECT id, version_number, trigger_type, commit_hash, status, created_at
393
+ FROM doc_versions
394
+ WHERE project_id = ? AND status = 'active'
395
+ ORDER BY version_number DESC
396
+ LIMIT 1
397
+ `).get(projectId) as DocVersion | null;
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+
403
+ export function getThoughtLogsForVersion(versionId: number): { doc_path: string; thinking_summary: string }[] {
404
+ try {
405
+ return db.prepare(`
406
+ SELECT doc_path, thinking_summary
407
+ FROM thought_logs
408
+ WHERE version_id = ?
409
+ ORDER BY created_at ASC
410
+ `).all(versionId) as { doc_path: string; thinking_summary: string }[];
411
+ } catch {
412
+ return [];
413
+ }
414
+ }
415
+
416
+ export function getThoughtLogForDoc(versionId: number, docPath: string): string | null {
417
+ try {
418
+ const result = db.prepare(`
419
+ SELECT thinking_summary
420
+ FROM thought_logs
421
+ WHERE version_id = ? AND doc_path = ?
422
+ LIMIT 1
423
+ `).get(versionId, docPath) as { thinking_summary: string } | undefined;
424
+ return result?.thinking_summary || null;
425
+ } catch {
426
+ return null;
427
+ }
428
+ }
429
+
430
+ // ===== AI Activity (Phase 1.6) =====
431
+
432
+ export interface AIActivity {
433
+ id: number;
434
+ project_id: number | null;
435
+ version_id: number | null;
436
+ activity_type: 'analyze' | 'generate' | 'plan' | 'embed' | 'action' | 'other';
437
+ prompt_summary: string | null;
438
+ response_preview: string | null;
439
+ thinking_summary: string | null;
440
+ tokens_used: number | null;
441
+ duration_ms: number | null;
442
+ status: 'running' | 'completed' | 'error';
443
+ created_at: number; // milliseconds
444
+ project_path?: string;
445
+ }
446
+
447
+ /**
448
+ * Gets recent AI activity for the thinking drawer.
449
+ */
450
+ export function getRecentAIActivity(limit: number = 20): AIActivity[] {
451
+ try {
452
+ return db.prepare(`
453
+ SELECT
454
+ a.*,
455
+ p.path as project_path
456
+ FROM ai_activity a
457
+ LEFT JOIN projects p ON a.project_id = p.id
458
+ ORDER BY a.created_at DESC
459
+ LIMIT ?
460
+ `).all(limit) as AIActivity[];
461
+ } catch {
462
+ return [];
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Gets AI activity for a specific version.
468
+ */
469
+ export function getActivityForVersion(versionId: number): AIActivity[] {
470
+ try {
471
+ return db.prepare(`
472
+ SELECT * FROM ai_activity
473
+ WHERE version_id = ?
474
+ ORDER BY created_at ASC
475
+ `).all(versionId) as AIActivity[];
476
+ } catch {
477
+ return [];
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Kills/cancels a running AI activity by marking it as error.
483
+ */
484
+ export function killAIActivity(activityId: number): boolean {
485
+ const database = getDb();
486
+ if (!database) return false;
487
+
488
+ try {
489
+ const result = database.prepare(`
490
+ UPDATE ai_activity
491
+ SET status = 'error',
492
+ response_preview = 'Cancelled by user',
493
+ thinking_summary = 'Activity was cancelled.'
494
+ WHERE id = ? AND status = 'running'
495
+ `).run(activityId);
496
+ return result.changes > 0;
497
+ } catch {
498
+ return false;
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Kills all running AI activities.
504
+ */
505
+ export function killAllRunningAIActivities(): number {
506
+ const database = getDb();
507
+ if (!database) return 0;
508
+
509
+ try {
510
+ const result = database.prepare(`
511
+ UPDATE ai_activity
512
+ SET status = 'error',
513
+ response_preview = 'Cancelled by user',
514
+ thinking_summary = 'Activity was cancelled.'
515
+ WHERE status = 'running'
516
+ `).run();
517
+ return result.changes;
518
+ } catch {
519
+ return 0;
520
+ }
521
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
@@ -0,0 +1,4 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {}
3
+
4
+ module.exports = nextConfig
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@chronicle/ui",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@types/marked": "^5.0.2",
13
+ "better-sqlite3": "^11.10.0",
14
+ "clsx": "^2.1.1",
15
+ "date-fns": "^2.30.0",
16
+ "lucide-react": "^0.562.0",
17
+ "marked": "^17.0.1",
18
+ "next": "14.0.3",
19
+ "react": "^18",
20
+ "react-dom": "^18",
21
+ "tailwind-merge": "^3.4.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20",
25
+ "@types/react": "^18",
26
+ "@types/react-dom": "^18",
27
+ "autoprefixer": "^10.0.1",
28
+ "postcss": "^8",
29
+ "tailwindcss": "^3.3.0",
30
+ "typescript": "^5"
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
Binary file