@aion0/forge 0.1.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.
Files changed (80) hide show
  1. package/CLAUDE.md +4 -0
  2. package/README.md +264 -0
  3. package/app/api/auth/[...nextauth]/route.ts +3 -0
  4. package/app/api/claude/[id]/route.ts +31 -0
  5. package/app/api/claude/[id]/stream/route.ts +63 -0
  6. package/app/api/claude/route.ts +28 -0
  7. package/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
  8. package/app/api/claude-sessions/[projectName]/route.ts +37 -0
  9. package/app/api/claude-sessions/sync/route.ts +17 -0
  10. package/app/api/flows/route.ts +6 -0
  11. package/app/api/flows/run/route.ts +19 -0
  12. package/app/api/notify/test/route.ts +33 -0
  13. package/app/api/projects/route.ts +7 -0
  14. package/app/api/sessions/[id]/chat/route.ts +64 -0
  15. package/app/api/sessions/[id]/messages/route.ts +9 -0
  16. package/app/api/sessions/[id]/route.ts +17 -0
  17. package/app/api/sessions/route.ts +20 -0
  18. package/app/api/settings/route.ts +15 -0
  19. package/app/api/status/route.ts +12 -0
  20. package/app/api/tasks/[id]/route.ts +36 -0
  21. package/app/api/tasks/[id]/stream/route.ts +77 -0
  22. package/app/api/tasks/link/route.ts +37 -0
  23. package/app/api/tasks/route.ts +43 -0
  24. package/app/api/tasks/session/route.ts +14 -0
  25. package/app/api/templates/route.ts +6 -0
  26. package/app/api/tunnel/route.ts +20 -0
  27. package/app/api/watchers/route.ts +33 -0
  28. package/app/globals.css +26 -0
  29. package/app/icon.svg +26 -0
  30. package/app/layout.tsx +17 -0
  31. package/app/login/page.tsx +61 -0
  32. package/app/page.tsx +9 -0
  33. package/cli/mw.ts +377 -0
  34. package/components/ChatPanel.tsx +191 -0
  35. package/components/ClaudeTerminal.tsx +267 -0
  36. package/components/Dashboard.tsx +270 -0
  37. package/components/MarkdownContent.tsx +57 -0
  38. package/components/NewSessionModal.tsx +93 -0
  39. package/components/NewTaskModal.tsx +456 -0
  40. package/components/ProjectList.tsx +108 -0
  41. package/components/SessionList.tsx +74 -0
  42. package/components/SessionView.tsx +655 -0
  43. package/components/SettingsModal.tsx +366 -0
  44. package/components/StatusBar.tsx +99 -0
  45. package/components/TaskBoard.tsx +110 -0
  46. package/components/TaskDetail.tsx +351 -0
  47. package/components/TunnelToggle.tsx +163 -0
  48. package/components/WebTerminal.tsx +1069 -0
  49. package/docs/LOCAL-DEPLOY.md +144 -0
  50. package/docs/roadmap-multi-agent-workflow.md +330 -0
  51. package/instrumentation.ts +14 -0
  52. package/lib/auth.ts +47 -0
  53. package/lib/claude-process.ts +352 -0
  54. package/lib/claude-sessions.ts +267 -0
  55. package/lib/cloudflared.ts +218 -0
  56. package/lib/flows.ts +86 -0
  57. package/lib/init.ts +82 -0
  58. package/lib/notify.ts +75 -0
  59. package/lib/password.ts +77 -0
  60. package/lib/projects.ts +86 -0
  61. package/lib/session-manager.ts +156 -0
  62. package/lib/session-watcher.ts +345 -0
  63. package/lib/settings.ts +44 -0
  64. package/lib/task-manager.ts +668 -0
  65. package/lib/telegram-bot.ts +912 -0
  66. package/lib/terminal-server.ts +70 -0
  67. package/lib/terminal-standalone.ts +363 -0
  68. package/middleware.ts +33 -0
  69. package/next-env.d.ts +6 -0
  70. package/next.config.ts +16 -0
  71. package/package.json +66 -0
  72. package/postcss.config.mjs +7 -0
  73. package/src/config/index.ts +119 -0
  74. package/src/core/db/database.ts +133 -0
  75. package/src/core/memory/strategy.ts +32 -0
  76. package/src/core/providers/chat.ts +65 -0
  77. package/src/core/providers/registry.ts +60 -0
  78. package/src/core/session/manager.ts +190 -0
  79. package/src/types/index.ts +128 -0
  80. package/tsconfig.json +41 -0
@@ -0,0 +1,156 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { getDb } from '@/src/core/db/database';
3
+ import { getDbPath, loadTemplate } from '@/src/config';
4
+ import { chatStream, type ChatResult } from '@/src/core/providers/chat';
5
+ import { getMemoryMessages } from '@/src/core/memory/strategy';
6
+ import type { Session, SessionStatus, Message, ProviderName, MemoryConfig } from '@/src/types';
7
+
8
+ // Re-export the SessionManager but as a singleton for server-side use
9
+ class SessionManager {
10
+ private db;
11
+
12
+ constructor() {
13
+ this.db = getDb(getDbPath());
14
+ }
15
+
16
+ create(opts: { name: string; templateId: string; provider?: ProviderName; model?: string }): Session {
17
+ const template = loadTemplate(opts.templateId);
18
+ if (!template) throw new Error(`Template not found: ${opts.templateId}`);
19
+
20
+ const id = randomUUID().slice(0, 8);
21
+ const provider = opts.provider || template.provider;
22
+ const model = opts.model || template.model || '';
23
+ const memoryConfig = JSON.stringify(template.memory);
24
+
25
+ this.db.prepare(`
26
+ INSERT INTO sessions (id, name, template_id, provider, model, status, memory_config, system_prompt)
27
+ VALUES (?, ?, ?, ?, ?, 'idle', ?, ?)
28
+ `).run(id, opts.name, opts.templateId, provider, model, memoryConfig, template.systemPrompt);
29
+
30
+ return this.get(id)!;
31
+ }
32
+
33
+ get(id: string): Session | null {
34
+ const row = this.db.prepare(`
35
+ SELECT s.*, COUNT(m.id) as message_count,
36
+ (SELECT content FROM messages WHERE session_id = s.id ORDER BY created_at DESC LIMIT 1) as last_message
37
+ FROM sessions s
38
+ LEFT JOIN messages m ON m.session_id = s.id
39
+ WHERE s.id = ?
40
+ GROUP BY s.id
41
+ `).get(id) as any;
42
+
43
+ if (!row) return null;
44
+ return this.rowToSession(row);
45
+ }
46
+
47
+ getByName(name: string): Session | null {
48
+ const row = this.db.prepare(`
49
+ SELECT s.*, COUNT(m.id) as message_count,
50
+ (SELECT content FROM messages WHERE session_id = s.id ORDER BY created_at DESC LIMIT 1) as last_message
51
+ FROM sessions s
52
+ LEFT JOIN messages m ON m.session_id = s.id
53
+ WHERE s.name = ?
54
+ GROUP BY s.id
55
+ `).get(name) as any;
56
+
57
+ if (!row) return null;
58
+ return this.rowToSession(row);
59
+ }
60
+
61
+ list(status?: SessionStatus): Session[] {
62
+ let query = `
63
+ SELECT s.*, COUNT(m.id) as message_count,
64
+ (SELECT content FROM messages WHERE session_id = s.id ORDER BY created_at DESC LIMIT 1) as last_message
65
+ FROM sessions s
66
+ LEFT JOIN messages m ON m.session_id = s.id
67
+ `;
68
+ const params: string[] = [];
69
+ if (status) {
70
+ query += ' WHERE s.status = ?';
71
+ params.push(status);
72
+ }
73
+ query += ' GROUP BY s.id ORDER BY s.updated_at DESC';
74
+
75
+ const rows = this.db.prepare(query).all(...params) as any[];
76
+ return rows.map(r => this.rowToSession(r));
77
+ }
78
+
79
+ updateStatus(id: string, status: SessionStatus) {
80
+ this.db.prepare(`UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ?`).run(status, id);
81
+ }
82
+
83
+ addMessage(sessionId: string, role: string, content: string, provider: string, model: string) {
84
+ this.db.prepare(`
85
+ INSERT INTO messages (session_id, role, content, provider, model)
86
+ VALUES (?, ?, ?, ?, ?)
87
+ `).run(sessionId, role, content, provider, model);
88
+ this.db.prepare(`UPDATE sessions SET updated_at = datetime('now') WHERE id = ?`).run(sessionId);
89
+ }
90
+
91
+ getMessages(sessionId: string, limit?: number): Message[] {
92
+ let query = 'SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC';
93
+ const params: (string | number)[] = [sessionId];
94
+ if (limit) {
95
+ query = `SELECT * FROM (
96
+ SELECT * FROM messages WHERE session_id = ? ORDER BY created_at DESC LIMIT ?
97
+ ) ORDER BY created_at ASC`;
98
+ params.push(limit);
99
+ }
100
+ return this.db.prepare(query).all(...params) as Message[];
101
+ }
102
+
103
+ getMemoryMessages(sessionId: string): Message[] {
104
+ const session = this.get(sessionId);
105
+ if (!session) return [];
106
+ const allMessages = this.getMessages(sessionId);
107
+ return getMemoryMessages(allMessages, session.memory);
108
+ }
109
+
110
+ delete(id: string) {
111
+ this.db.prepare('DELETE FROM sessions WHERE id = ?').run(id);
112
+ }
113
+
114
+ recordUsage(sessionId: string, result: ChatResult) {
115
+ this.db.prepare(`
116
+ INSERT INTO usage (provider, model, session_id, input_tokens, output_tokens, cost)
117
+ VALUES (?, ?, ?, ?, ?, ?)
118
+ `).run(result.provider, result.model, sessionId, result.inputTokens, result.outputTokens, 0);
119
+ }
120
+
121
+ getUsageSummary() {
122
+ return this.db.prepare(`
123
+ SELECT provider,
124
+ SUM(input_tokens) as totalInput,
125
+ SUM(output_tokens) as totalOutput,
126
+ SUM(cost) as totalCost
127
+ FROM usage
128
+ WHERE created_at >= date('now', '-30 days')
129
+ GROUP BY provider
130
+ `).all() as any[];
131
+ }
132
+
133
+ private rowToSession(row: any): Session {
134
+ return {
135
+ id: row.id,
136
+ name: row.name,
137
+ templateId: row.template_id,
138
+ provider: row.provider,
139
+ model: row.model,
140
+ status: row.status,
141
+ memory: JSON.parse(row.memory_config),
142
+ systemPrompt: row.system_prompt,
143
+ messageCount: row.message_count || 0,
144
+ createdAt: row.created_at,
145
+ updatedAt: row.updated_at,
146
+ lastMessage: row.last_message || undefined,
147
+ };
148
+ }
149
+ }
150
+
151
+ // Singleton
152
+ let instance: SessionManager | null = null;
153
+ export function getSessionManager(): SessionManager {
154
+ if (!instance) instance = new SessionManager();
155
+ return instance;
156
+ }
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Session Watcher — monitors Claude CLI sessions and sends Telegram notifications.
3
+ *
4
+ * Watchers track entry counts in sessions. When new entries appear,
5
+ * a summary is sent to Telegram. Also detects idle/completed sessions.
6
+ */
7
+
8
+ import { randomUUID } from 'node:crypto';
9
+ import { getDb } from '@/src/core/db/database';
10
+ import { join } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+ import {
13
+ listClaudeSessions,
14
+ getSessionFilePath,
15
+ readSessionEntries,
16
+ type ClaudeSessionInfo,
17
+ type SessionEntry,
18
+ } from './claude-sessions';
19
+ import { scanProjects } from './projects';
20
+ import { loadSettings } from './settings';
21
+
22
+ const DB_PATH = join(homedir(), '.my-workflow', 'data.db');
23
+
24
+ // ─── Types ───────────────────────────────────────────────────
25
+
26
+ export interface SessionWatcher {
27
+ id: string;
28
+ projectName: string;
29
+ sessionId: string | null; // null = watch all sessions in project
30
+ label: string | null;
31
+ checkInterval: number; // seconds
32
+ lastEntryCount: number;
33
+ lastChecked: string | null;
34
+ notifyOnChange: boolean;
35
+ notifyOnIdle: boolean;
36
+ idleThreshold: number; // seconds
37
+ active: boolean;
38
+ createdAt: string;
39
+ }
40
+
41
+ // ─── Session cache sync ──────────────────────────────────────
42
+
43
+ export function syncSessionsToDb(projectName?: string) {
44
+ const db = getDb(DB_PATH);
45
+ const projects = projectName
46
+ ? [{ name: projectName }]
47
+ : scanProjects().map((p: { name: string }) => ({ name: p.name }));
48
+
49
+ const upsert = db.prepare(`
50
+ INSERT INTO cached_sessions (project_name, session_id, summary, first_prompt, message_count, created, modified, git_branch, file_size, last_synced)
51
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
52
+ ON CONFLICT(project_name, session_id) DO UPDATE SET
53
+ summary = excluded.summary,
54
+ first_prompt = excluded.first_prompt,
55
+ message_count = excluded.message_count,
56
+ created = excluded.created,
57
+ modified = excluded.modified,
58
+ git_branch = excluded.git_branch,
59
+ file_size = excluded.file_size,
60
+ last_synced = datetime('now')
61
+ `);
62
+
63
+ const updateEntryCount = db.prepare(`
64
+ UPDATE cached_sessions SET entry_count = ? WHERE project_name = ? AND session_id = ?
65
+ `);
66
+
67
+ let totalSynced = 0;
68
+
69
+ for (const proj of projects) {
70
+ const sessions = listClaudeSessions(proj.name);
71
+ for (const s of sessions) {
72
+ upsert.run(
73
+ proj.name, s.sessionId, s.summary || null, s.firstPrompt || null,
74
+ s.messageCount || 0, s.created || null, s.modified || null,
75
+ s.gitBranch || null, s.fileSize,
76
+ );
77
+
78
+ // Count entries for watcher comparison
79
+ const fp = getSessionFilePath(proj.name, s.sessionId);
80
+ if (fp) {
81
+ try {
82
+ const entries = readSessionEntries(fp);
83
+ updateEntryCount.run(entries.length, proj.name, s.sessionId);
84
+ } catch {}
85
+ }
86
+
87
+ totalSynced++;
88
+ }
89
+ }
90
+
91
+ return totalSynced;
92
+ }
93
+
94
+ export function getCachedSessions(projectName: string): ClaudeSessionInfo[] {
95
+ const db = getDb(DB_PATH);
96
+ const rows = db.prepare(`
97
+ SELECT session_id, summary, first_prompt, message_count, created, modified, git_branch, file_size
98
+ FROM cached_sessions WHERE project_name = ? ORDER BY modified DESC
99
+ `).all(projectName) as any[];
100
+
101
+ return rows.map(r => ({
102
+ sessionId: r.session_id,
103
+ summary: r.summary,
104
+ firstPrompt: r.first_prompt,
105
+ messageCount: r.message_count,
106
+ created: r.created,
107
+ modified: r.modified,
108
+ gitBranch: r.git_branch,
109
+ fileSize: r.file_size,
110
+ }));
111
+ }
112
+
113
+ export function getAllCachedSessions(): Record<string, ClaudeSessionInfo[]> {
114
+ const db = getDb(DB_PATH);
115
+ const rows = db.prepare(`
116
+ SELECT project_name, session_id, summary, first_prompt, message_count, created, modified, git_branch, file_size
117
+ FROM cached_sessions ORDER BY project_name, modified DESC
118
+ `).all() as any[];
119
+
120
+ const result: Record<string, ClaudeSessionInfo[]> = {};
121
+ for (const r of rows) {
122
+ if (!result[r.project_name]) result[r.project_name] = [];
123
+ result[r.project_name].push({
124
+ sessionId: r.session_id,
125
+ summary: r.summary,
126
+ firstPrompt: r.first_prompt,
127
+ messageCount: r.message_count,
128
+ created: r.created,
129
+ modified: r.modified,
130
+ gitBranch: r.git_branch,
131
+ fileSize: r.file_size,
132
+ });
133
+ }
134
+ return result;
135
+ }
136
+
137
+ // ─── Watcher CRUD ────────────────────────────────────────────
138
+
139
+ export function createWatcher(opts: {
140
+ projectName: string;
141
+ sessionId?: string;
142
+ label?: string;
143
+ checkInterval?: number;
144
+ }): SessionWatcher {
145
+ const db = getDb(DB_PATH);
146
+ const id = randomUUID().slice(0, 8);
147
+
148
+ db.prepare(`
149
+ INSERT INTO session_watchers (id, project_name, session_id, label, check_interval)
150
+ VALUES (?, ?, ?, ?, ?)
151
+ `).run(id, opts.projectName, opts.sessionId || null, opts.label || null, opts.checkInterval || 60);
152
+
153
+ return getWatcher(id)!;
154
+ }
155
+
156
+ export function getWatcher(id: string): SessionWatcher | null {
157
+ const db = getDb(DB_PATH);
158
+ const row = db.prepare('SELECT * FROM session_watchers WHERE id = ?').get(id) as any;
159
+ return row ? mapWatcherRow(row) : null;
160
+ }
161
+
162
+ export function listWatchers(activeOnly = false): SessionWatcher[] {
163
+ const db = getDb(DB_PATH);
164
+ const sql = activeOnly
165
+ ? 'SELECT * FROM session_watchers WHERE active = 1 ORDER BY created_at DESC'
166
+ : 'SELECT * FROM session_watchers ORDER BY created_at DESC';
167
+ return (db.prepare(sql).all() as any[]).map(mapWatcherRow);
168
+ }
169
+
170
+ export function toggleWatcher(id: string, active: boolean) {
171
+ const db = getDb(DB_PATH);
172
+ db.prepare('UPDATE session_watchers SET active = ? WHERE id = ?').run(active ? 1 : 0, id);
173
+ }
174
+
175
+ export function deleteWatcher(id: string) {
176
+ const db = getDb(DB_PATH);
177
+ db.prepare('DELETE FROM session_watchers WHERE id = ?').run(id);
178
+ }
179
+
180
+ function mapWatcherRow(row: any): SessionWatcher {
181
+ return {
182
+ id: row.id,
183
+ projectName: row.project_name,
184
+ sessionId: row.session_id,
185
+ label: row.label,
186
+ checkInterval: row.check_interval,
187
+ lastEntryCount: row.last_entry_count,
188
+ lastChecked: row.last_checked,
189
+ notifyOnChange: !!row.notify_on_change,
190
+ notifyOnIdle: !!row.notify_on_idle,
191
+ idleThreshold: row.idle_threshold,
192
+ active: !!row.active,
193
+ createdAt: row.created_at,
194
+ };
195
+ }
196
+
197
+ // ─── Watcher check loop ─────────────────────────────────────
198
+
199
+ let watcherInterval: ReturnType<typeof setInterval> | null = null;
200
+
201
+ export function startWatcherLoop() {
202
+ if (watcherInterval) return;
203
+
204
+ // Initial sync
205
+ try { syncSessionsToDb(); } catch (e) { console.error('[watcher] Initial sync error:', e); }
206
+
207
+ // Check every 30 seconds
208
+ watcherInterval = setInterval(runWatcherCheck, 30_000);
209
+ console.log('[watcher] Started session watcher loop');
210
+ }
211
+
212
+ export function stopWatcherLoop() {
213
+ if (watcherInterval) {
214
+ clearInterval(watcherInterval);
215
+ watcherInterval = null;
216
+ }
217
+ }
218
+
219
+ async function runWatcherCheck() {
220
+ const db = getDb(DB_PATH);
221
+ const watchers = listWatchers(true);
222
+ if (watchers.length === 0) return;
223
+
224
+ const now = Date.now();
225
+
226
+ for (const w of watchers) {
227
+ // Check if it's time
228
+ if (w.lastChecked) {
229
+ const elapsed = (now - new Date(w.lastChecked).getTime()) / 1000;
230
+ if (elapsed < w.checkInterval) continue;
231
+ }
232
+
233
+ try {
234
+ if (w.sessionId) {
235
+ // Watch specific session
236
+ await checkSession(db, w, w.projectName, w.sessionId);
237
+ } else {
238
+ // Watch all sessions in project
239
+ const sessions = listClaudeSessions(w.projectName);
240
+ for (const s of sessions) {
241
+ await checkSession(db, w, w.projectName, s.sessionId);
242
+ }
243
+ }
244
+
245
+ // Update last checked
246
+ db.prepare('UPDATE session_watchers SET last_checked = datetime(\'now\') WHERE id = ?').run(w.id);
247
+ } catch (e) {
248
+ console.error(`[watcher] Error checking ${w.id}:`, e);
249
+ }
250
+ }
251
+
252
+ // Periodic session sync (every check cycle)
253
+ try { syncSessionsToDb(); } catch {}
254
+ }
255
+
256
+ async function checkSession(
257
+ db: ReturnType<typeof getDb>,
258
+ watcher: SessionWatcher,
259
+ projectName: string,
260
+ sessionId: string,
261
+ ) {
262
+ const fp = getSessionFilePath(projectName, sessionId);
263
+ if (!fp) return;
264
+
265
+ const entries = readSessionEntries(fp);
266
+ const currentCount = entries.length;
267
+
268
+ // Get last known count from cached_sessions
269
+ const cached = db.prepare(
270
+ 'SELECT entry_count FROM cached_sessions WHERE project_name = ? AND session_id = ?'
271
+ ).get(projectName, sessionId) as any;
272
+
273
+ const lastCount = cached?.entry_count || 0;
274
+
275
+ if (currentCount > lastCount && watcher.notifyOnChange) {
276
+ // New entries! Summarize the changes
277
+ const newEntries = entries.slice(lastCount);
278
+ const summary = summarizeEntries(newEntries);
279
+ const label = watcher.label || `${projectName}/${sessionId.slice(0, 8)}`;
280
+
281
+ await sendWatcherNotification(
282
+ `📋 *${esc(label)}*\n\n` +
283
+ `${summary}\n\n` +
284
+ `_${currentCount} total entries (+${currentCount - lastCount} new)_`
285
+ );
286
+
287
+ // Update cached entry count
288
+ db.prepare(
289
+ 'UPDATE cached_sessions SET entry_count = ? WHERE project_name = ? AND session_id = ?'
290
+ ).run(currentCount, projectName, sessionId);
291
+ }
292
+ }
293
+
294
+ function summarizeEntries(entries: SessionEntry[]): string {
295
+ const parts: string[] = [];
296
+ let assistantText = '';
297
+ let toolNames: string[] = [];
298
+
299
+ for (const e of entries) {
300
+ if (e.type === 'user') {
301
+ parts.push(`👤 ${e.content.slice(0, 150)}`);
302
+ } else if (e.type === 'assistant_text') {
303
+ assistantText = e.content.slice(0, 300);
304
+ } else if (e.type === 'tool_use' && e.toolName) {
305
+ toolNames.push(e.toolName);
306
+ }
307
+ }
308
+
309
+ if (toolNames.length > 0) {
310
+ const unique = [...new Set(toolNames)];
311
+ parts.push(`🔧 Tools: ${unique.join(', ')}`);
312
+ }
313
+
314
+ if (assistantText) {
315
+ parts.push(`🤖 ${assistantText}`);
316
+ }
317
+
318
+ return parts.join('\n') || 'Activity detected';
319
+ }
320
+
321
+ async function sendWatcherNotification(text: string) {
322
+ const settings = loadSettings();
323
+ const { telegramBotToken, telegramChatId } = settings;
324
+ if (!telegramBotToken || !telegramChatId) return;
325
+
326
+ try {
327
+ const url = `https://api.telegram.org/bot${telegramBotToken}/sendMessage`;
328
+ await fetch(url, {
329
+ method: 'POST',
330
+ headers: { 'Content-Type': 'application/json' },
331
+ body: JSON.stringify({
332
+ chat_id: telegramChatId,
333
+ text,
334
+ parse_mode: 'Markdown',
335
+ disable_web_page_preview: true,
336
+ }),
337
+ });
338
+ } catch (err) {
339
+ console.error('[watcher] Telegram send failed:', err);
340
+ }
341
+ }
342
+
343
+ function esc(s: string): string {
344
+ return s.replace(/[_*[\]()~`>#+=|{}.!-]/g, '\\$&');
345
+ }
@@ -0,0 +1,44 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join, dirname } from 'node:path';
4
+ import YAML from 'yaml';
5
+
6
+ const SETTINGS_FILE = join(homedir(), '.my-workflow', 'settings.yaml');
7
+
8
+ export interface Settings {
9
+ projectRoots: string[]; // Multiple project directories
10
+ claudePath: string; // Path to claude binary
11
+ telegramBotToken: string; // Telegram Bot API token
12
+ telegramChatId: string; // Telegram chat ID to send notifications to
13
+ notifyOnComplete: boolean; // Notify when task completes
14
+ notifyOnFailure: boolean; // Notify when task fails
15
+ tunnelAutoStart: boolean; // Auto-start Cloudflare Tunnel on startup
16
+ telegramTunnelPassword: string; // Password for getting login password via Telegram
17
+ }
18
+
19
+ const defaults: Settings = {
20
+ projectRoots: [],
21
+ claudePath: '',
22
+ telegramBotToken: '',
23
+ telegramChatId: '',
24
+ notifyOnComplete: true,
25
+ notifyOnFailure: true,
26
+ tunnelAutoStart: false,
27
+ telegramTunnelPassword: '',
28
+ };
29
+
30
+ export function loadSettings(): Settings {
31
+ if (!existsSync(SETTINGS_FILE)) return { ...defaults };
32
+ try {
33
+ const raw = readFileSync(SETTINGS_FILE, 'utf-8');
34
+ return { ...defaults, ...YAML.parse(raw) };
35
+ } catch {
36
+ return { ...defaults };
37
+ }
38
+ }
39
+
40
+ export function saveSettings(settings: Settings) {
41
+ const dir = dirname(SETTINGS_FILE);
42
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
43
+ writeFileSync(SETTINGS_FILE, YAML.stringify(settings), 'utf-8');
44
+ }