@aion0/forge 0.3.0 → 0.3.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.
@@ -9,6 +9,7 @@ import '@xterm/xterm/css/xterm.css';
9
9
 
10
10
  export interface WebTerminalHandle {
11
11
  openSessionInTerminal: (sessionId: string, projectPath: string) => void;
12
+ openProjectTerminal: (projectPath: string, projectName: string) => void;
12
13
  }
13
14
 
14
15
  export interface WebTerminalProps {
@@ -269,21 +270,65 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
269
270
 
270
271
  useImperativeHandle(ref, () => ({
271
272
  openSessionInTerminal(sessionId: string, projectPath: string) {
272
- const tree = makeTerminal();
273
+ const tree = makeTerminal(undefined, projectPath);
273
274
  const paneId = firstTerminalId(tree);
274
- const cmd = `cd ${projectPath} && claude --resume ${sessionId}\n`;
275
+ const sf = skipPermissions ? ' --dangerously-skip-permissions' : '';
276
+ const cmd = `cd "${projectPath}" && claude --resume ${sessionId}${sf}\n`;
275
277
  pendingCommands.set(paneId, cmd);
278
+ const projectName = projectPath.split('/').pop() || 'Terminal';
276
279
  const newTab: TabState = {
277
280
  id: nextId++,
278
- label: `claude ${sessionId.slice(0, 8)}`,
281
+ label: projectName,
279
282
  tree,
280
283
  ratios: {},
281
284
  activeId: paneId,
285
+ projectPath,
282
286
  };
283
287
  setTabs(prev => [...prev, newTab]);
284
- setActiveTabId(newTab.id);
288
+ setTimeout(() => setActiveTabId(newTab.id), 0);
285
289
  },
286
- }));
290
+ async openProjectTerminal(projectPath: string, projectName: string) {
291
+ // Check for existing sessions to use -c
292
+ let hasSession = false;
293
+ try {
294
+ const sRes = await fetch(`/api/claude-sessions/${encodeURIComponent(projectName)}`);
295
+ const sData = await sRes.json();
296
+ hasSession = Array.isArray(sData) ? sData.length > 0 : false;
297
+ } catch {}
298
+ const sf = skipPermissions ? ' --dangerously-skip-permissions' : '';
299
+ const resumeFlag = hasSession ? ' -c' : '';
300
+
301
+ // Use a ref-stable ID so we can set active after state update
302
+ let targetTabId: number | null = null;
303
+
304
+ setTabs(prev => {
305
+ // Check if there's already a tab for this project
306
+ const existing = prev.find(t => t.projectPath === projectPath);
307
+ if (existing) {
308
+ targetTabId = existing.id;
309
+ return prev;
310
+ }
311
+ const tree = makeTerminal(undefined, projectPath);
312
+ const paneId = firstTerminalId(tree);
313
+ pendingCommands.set(paneId, `cd "${projectPath}" && claude${resumeFlag}${sf}\n`);
314
+ const newTab: TabState = {
315
+ id: nextId++,
316
+ label: projectName,
317
+ tree,
318
+ ratios: {},
319
+ activeId: paneId,
320
+ projectPath,
321
+ };
322
+ targetTabId = newTab.id;
323
+ return [...prev, newTab];
324
+ });
325
+
326
+ // Set active tab after React processes the state update
327
+ setTimeout(() => {
328
+ if (targetTabId !== null) setActiveTabId(targetTabId);
329
+ }, 0);
330
+ },
331
+ }), [skipPermissions]);
287
332
 
288
333
  // ─── Tab operations ───────────────────────────────────
289
334
 
@@ -8,8 +8,8 @@ export async function register() {
8
8
  // Load ~/.forge/.env.local if it exists (works for both pnpm dev and forge-server)
9
9
  const { existsSync, readFileSync } = await import('node:fs');
10
10
  const { join } = await import('node:path');
11
- const { homedir } = await import('node:os');
12
- const dataDir = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
11
+ const { getDataDir } = await import('./lib/dirs');
12
+ const dataDir = getDataDir();
13
13
  const envFile = join(dataDir, '.env.local');
14
14
  if (existsSync(envFile)) {
15
15
  for (const line of readFileSync(envFile, 'utf-8').split('\n')) {
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { existsSync, readFileSync, statSync, readdirSync, watch, openSync, readSync, closeSync, unlinkSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
- import { homedir } from 'node:os';
8
+ import { getClaudeDir } from './dirs';
9
9
  import { getProjectInfo } from './projects';
10
10
 
11
11
  export interface ClaudeSessionInfo {
@@ -33,7 +33,7 @@ export interface SessionEntry {
33
33
  */
34
34
  export function projectPathToClaudeDir(projectPath: string): string {
35
35
  const hash = projectPath.replace(/\//g, '-');
36
- return join(homedir(), '.claude', 'projects', hash);
36
+ return join(getClaudeDir(), 'projects', hash);
37
37
  }
38
38
 
39
39
  /**
@@ -0,0 +1,227 @@
1
+ /**
2
+ * CLAUDE.md template management.
3
+ *
4
+ * Templates are reusable markdown snippets that can be appended to project CLAUDE.md files.
5
+ * Stored in <dataDir>/claude-templates/*.md with frontmatter metadata.
6
+ * Injection is idempotent — marked with <!-- forge:template:<id> --> comments.
7
+ */
8
+
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from 'node:fs';
10
+ import { join, basename } from 'node:path';
11
+ import { getDataDir } from './dirs';
12
+ import YAML from 'yaml';
13
+
14
+ const TEMPLATES_DIR = join(getDataDir(), 'claude-templates');
15
+
16
+ export interface ClaudeTemplate {
17
+ id: string; // filename without .md
18
+ name: string;
19
+ description: string;
20
+ tags: string[];
21
+ builtin: boolean;
22
+ isDefault: boolean; // auto-inject into new projects
23
+ content: string; // markdown body (without frontmatter)
24
+ }
25
+
26
+ function ensureDir() {
27
+ if (!existsSync(TEMPLATES_DIR)) mkdirSync(TEMPLATES_DIR, { recursive: true });
28
+ }
29
+
30
+ // ─── Built-in templates ──────────────────────────────────────
31
+
32
+ const BUILTINS: Record<string, { name: string; description: string; tags: string[]; content: string }> = {
33
+ 'typescript-rules': {
34
+ name: 'TypeScript Rules',
35
+ description: 'TypeScript coding conventions and best practices',
36
+ tags: ['typescript', 'code-style'],
37
+ content: `## TypeScript Rules
38
+ - Use \`const\` by default, \`let\` only when needed
39
+ - Prefer explicit return types on exported functions
40
+ - Use \`interface\` for object shapes, \`type\` for unions/intersections
41
+ - Avoid \`any\` — use \`unknown\` + type guards
42
+ - Prefer early returns over nested if/else`,
43
+ },
44
+ 'git-workflow': {
45
+ name: 'Git Workflow',
46
+ description: 'Git commit and branch conventions',
47
+ tags: ['git', 'workflow'],
48
+ content: `## Git Workflow
49
+ - Commit messages: imperative mood, concise (e.g. "add feature X", "fix bug in Y")
50
+ - Branch naming: \`feature/<name>\`, \`fix/<name>\`, \`chore/<name>\`
51
+ - Always create a new branch for changes, never commit directly to main
52
+ - Run tests before committing`,
53
+ },
54
+ 'obsidian-vault': {
55
+ name: 'Obsidian Vault',
56
+ description: 'Obsidian vault integration for note search and management',
57
+ tags: ['obsidian', 'docs'],
58
+ content: `## Obsidian Vault
59
+ When I ask about my notes, use bash to search and read files from the vault directory.
60
+ Example: find <vault_path> -name "*.md" | head -20`,
61
+ },
62
+ 'security': {
63
+ name: 'Security Rules',
64
+ description: 'Security best practices for code generation',
65
+ tags: ['security'],
66
+ content: `## Security Rules
67
+ - Never hardcode secrets, API keys, or passwords
68
+ - Validate all user inputs at system boundaries
69
+ - Use parameterized queries for database operations
70
+ - Sanitize outputs to prevent XSS
71
+ - Follow OWASP top 10 guidelines`,
72
+ },
73
+ };
74
+
75
+ /** Ensure built-in templates exist on disk */
76
+ export function ensureBuiltins() {
77
+ ensureDir();
78
+ for (const [id, tmpl] of Object.entries(BUILTINS)) {
79
+ const file = join(TEMPLATES_DIR, `${id}.md`);
80
+ if (!existsSync(file)) {
81
+ const frontmatter = YAML.stringify({ name: tmpl.name, description: tmpl.description, tags: tmpl.tags, builtin: true });
82
+ writeFileSync(file, `---\n${frontmatter}---\n\n${tmpl.content}\n`, 'utf-8');
83
+ }
84
+ }
85
+ }
86
+
87
+ // ─── CRUD ────────────────────────────────────────────────────
88
+
89
+ function parseTemplate(filePath: string): ClaudeTemplate | null {
90
+ try {
91
+ const raw = readFileSync(filePath, 'utf-8');
92
+ const id = basename(filePath, '.md');
93
+ // Parse frontmatter
94
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
95
+ if (!fmMatch) return { id, name: id, description: '', tags: [], builtin: false, isDefault: false, content: raw.trim() };
96
+ const meta = YAML.parse(fmMatch[1]) || {};
97
+ return {
98
+ id,
99
+ name: meta.name || id,
100
+ description: meta.description || '',
101
+ tags: meta.tags || [],
102
+ builtin: !!meta.builtin,
103
+ isDefault: !!meta.isDefault,
104
+ content: fmMatch[2].trim(),
105
+ };
106
+ } catch { return null; }
107
+ }
108
+
109
+ export function listTemplates(): ClaudeTemplate[] {
110
+ ensureDir();
111
+ ensureBuiltins();
112
+ const files = readdirSync(TEMPLATES_DIR).filter(f => f.endsWith('.md')).sort();
113
+ return files.map(f => parseTemplate(join(TEMPLATES_DIR, f))).filter(Boolean) as ClaudeTemplate[];
114
+ }
115
+
116
+ export function getTemplate(id: string): ClaudeTemplate | null {
117
+ const file = join(TEMPLATES_DIR, `${id}.md`);
118
+ if (!existsSync(file)) return null;
119
+ return parseTemplate(file);
120
+ }
121
+
122
+ export function saveTemplate(id: string, name: string, description: string, tags: string[], content: string, isDefault?: boolean): void {
123
+ ensureDir();
124
+ // Preserve builtin flag if editing an existing built-in template
125
+ const existing = getTemplate(id);
126
+ const builtin = existing?.builtin || false;
127
+ const frontmatter = YAML.stringify({ name, description, tags, builtin, isDefault: !!isDefault });
128
+ writeFileSync(join(TEMPLATES_DIR, `${id}.md`), `---\n${frontmatter}---\n\n${content}\n`, 'utf-8');
129
+ }
130
+
131
+ /** Toggle default flag on a template */
132
+ export function setTemplateDefault(id: string, isDefault: boolean): boolean {
133
+ const tmpl = getTemplate(id);
134
+ if (!tmpl) return false;
135
+ const file = join(TEMPLATES_DIR, `${id}.md`);
136
+ const raw = readFileSync(file, 'utf-8');
137
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
138
+ if (!fmMatch) return false;
139
+ const meta = YAML.parse(fmMatch[1]) || {};
140
+ meta.isDefault = isDefault;
141
+ writeFileSync(file, `---\n${YAML.stringify(meta)}---\n${fmMatch[2]}`, 'utf-8');
142
+ return true;
143
+ }
144
+
145
+ /** Auto-inject all default templates into a project if not already present */
146
+ export function applyDefaultTemplates(projectPath: string): string[] {
147
+ const claudeMdPath = join(projectPath, 'CLAUDE.md');
148
+ const templates = listTemplates();
149
+ const injected: string[] = [];
150
+ for (const tmpl of templates) {
151
+ if (tmpl.isDefault && !isInjected(claudeMdPath, tmpl.id)) {
152
+ if (injectTemplate(claudeMdPath, tmpl.id)) {
153
+ injected.push(tmpl.id);
154
+ }
155
+ }
156
+ }
157
+ return injected;
158
+ }
159
+
160
+ export function deleteTemplate(id: string): boolean {
161
+ const tmpl = getTemplate(id);
162
+ if (!tmpl || tmpl.builtin) return false; // can't delete builtins
163
+ const file = join(TEMPLATES_DIR, `${id}.md`);
164
+ try { unlinkSync(file); return true; } catch { return false; }
165
+ }
166
+
167
+ // ─── Injection ───────────────────────────────────────────────
168
+
169
+ const MARKER_START = (id: string) => `<!-- forge:template:${id} -->`;
170
+ const MARKER_END = (id: string) => `<!-- /forge:template:${id} -->`;
171
+
172
+ /** Check if a template is already injected in a CLAUDE.md */
173
+ export function isInjected(claudeMdPath: string, templateId: string): boolean {
174
+ if (!existsSync(claudeMdPath)) return false;
175
+ const content = readFileSync(claudeMdPath, 'utf-8');
176
+ return content.includes(MARKER_START(templateId));
177
+ }
178
+
179
+ /** Get list of template IDs injected in a CLAUDE.md */
180
+ export function getInjectedTemplates(claudeMdPath: string): string[] {
181
+ if (!existsSync(claudeMdPath)) return [];
182
+ const content = readFileSync(claudeMdPath, 'utf-8');
183
+ const ids: string[] = [];
184
+ const regex = /<!-- forge:template:(\S+) -->/g;
185
+ let match;
186
+ while ((match = regex.exec(content)) !== null) {
187
+ ids.push(match[1]);
188
+ }
189
+ return ids;
190
+ }
191
+
192
+ /** Append a template to a CLAUDE.md file. Returns false if already injected. */
193
+ export function injectTemplate(claudeMdPath: string, templateId: string): boolean {
194
+ const tmpl = getTemplate(templateId);
195
+ if (!tmpl) return false;
196
+ if (isInjected(claudeMdPath, templateId)) return false;
197
+
198
+ const block = `\n${MARKER_START(templateId)}\n${tmpl.content}\n${MARKER_END(templateId)}\n`;
199
+
200
+ if (existsSync(claudeMdPath)) {
201
+ const existing = readFileSync(claudeMdPath, 'utf-8');
202
+ writeFileSync(claudeMdPath, existing.trimEnd() + '\n' + block, 'utf-8');
203
+ } else {
204
+ // Create new CLAUDE.md
205
+ const dir = join(claudeMdPath, '..');
206
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
207
+ writeFileSync(claudeMdPath, block.trimStart(), 'utf-8');
208
+ }
209
+ return true;
210
+ }
211
+
212
+ /** Remove a template from a CLAUDE.md file */
213
+ export function removeTemplate(claudeMdPath: string, templateId: string): boolean {
214
+ if (!existsSync(claudeMdPath)) return false;
215
+ const content = readFileSync(claudeMdPath, 'utf-8');
216
+ const start = MARKER_START(templateId);
217
+ const end = MARKER_END(templateId);
218
+ const startIdx = content.indexOf(start);
219
+ const endIdx = content.indexOf(end);
220
+ if (startIdx === -1 || endIdx === -1) return false;
221
+
222
+ const before = content.slice(0, startIdx).trimEnd();
223
+ const after = content.slice(endIdx + end.length).trimStart();
224
+ const newContent = before + (after ? '\n\n' + after : '') + '\n';
225
+ writeFileSync(claudeMdPath, newContent, 'utf-8');
226
+ return true;
227
+ }
@@ -5,12 +5,13 @@
5
5
 
6
6
  import { spawn, execSync, type ChildProcess } from 'node:child_process';
7
7
  import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, writeFileSync, readFileSync } from 'node:fs';
8
- import { homedir, platform, arch } from 'node:os';
8
+ import { platform, arch } from 'node:os';
9
9
  import { join } from 'node:path';
10
10
  import https from 'node:https';
11
11
  import http from 'node:http';
12
+ import { getConfigDir, getDataDir } from './dirs';
12
13
 
13
- const BIN_DIR = join(homedir(), '.forge', 'bin');
14
+ const BIN_DIR = join(getConfigDir(), 'bin');
14
15
  const BIN_NAME = platform() === 'win32' ? 'cloudflared.exe' : 'cloudflared';
15
16
  const BIN_PATH = join(BIN_DIR, BIN_NAME);
16
17
 
@@ -105,7 +106,7 @@ if (!gAny[stateKey]) {
105
106
  const state: TunnelState = gAny[stateKey];
106
107
 
107
108
  const MAX_LOG_LINES = 100;
108
- const TUNNEL_STATE_FILE = join(homedir(), '.forge', 'tunnel-state.json');
109
+ const TUNNEL_STATE_FILE = join(getDataDir(), 'tunnel-state.json');
109
110
 
110
111
  function saveTunnelState() {
111
112
  try {
package/lib/crypto.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * Encryption utilities for storing secrets in settings.yaml
3
- * Uses AES-256-GCM with a persistent key stored in ~/.forge/.encrypt-key
3
+ * Uses AES-256-GCM with a persistent key stored in <dataDir>/.encrypt-key
4
4
  */
5
5
 
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
- import { homedir } from 'node:os';
8
7
  import { join, dirname } from 'node:path';
9
8
  import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'node:crypto';
9
+ import { getDataDir } from './dirs';
10
10
 
11
- const DATA_DIR = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
12
- const KEY_FILE = join(DATA_DIR, '.encrypt-key');
11
+ const KEY_FILE = join(getDataDir(), '.encrypt-key');
13
12
  const PREFIX = 'enc:';
14
13
 
15
14
  function getEncryptionKey(): Buffer {
package/lib/dirs.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Centralized directory paths for Forge.
3
+ *
4
+ * Shared (configDir): ~/.forge/ — only bin/ (cloudflared)
5
+ * Instance (dataDir): ~/.forge/data/ — settings, db, state, flows, etc.
6
+ * or --dir / FORGE_DATA_DIR
7
+ * Claude (claudeDir): ~/.claude/ — or configured in settings
8
+ */
9
+
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+
13
+ /** Shared config directory — only binaries, fixed at ~/.forge/ */
14
+ export function getConfigDir(): string {
15
+ return join(homedir(), '.forge');
16
+ }
17
+
18
+ /** Instance data directory — all instance-specific data */
19
+ export function getDataDir(): string {
20
+ return process.env.FORGE_DATA_DIR || join(getConfigDir(), 'data');
21
+ }
22
+
23
+ /** Claude Code home directory — skills, commands, sessions */
24
+ export function getClaudeDir(): string {
25
+ // Env var takes precedence
26
+ if (process.env.CLAUDE_HOME) return process.env.CLAUDE_HOME;
27
+ // Try to read from settings (lazy require to avoid circular dependency)
28
+ try {
29
+ const { loadSettings } = require('./settings');
30
+ const settings = loadSettings();
31
+ if (settings.claudeHome) return settings.claudeHome.replace(/^~/, homedir());
32
+ } catch {}
33
+ return join(homedir(), '.claude');
34
+ }
package/lib/flows.ts CHANGED
@@ -6,13 +6,13 @@
6
6
 
7
7
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
- import { homedir } from 'node:os';
10
9
  import YAML from 'yaml';
11
10
  import { createTask } from './task-manager';
12
11
  import { getProjectInfo } from './projects';
13
12
  import type { Task } from '@/src/types';
13
+ import { getDataDir } from './dirs';
14
14
 
15
- const FLOWS_DIR = join(homedir(), '.forge', 'flows');
15
+ const FLOWS_DIR = join(getDataDir(), 'flows');
16
16
 
17
17
  export interface FlowStep {
18
18
  project: string;
package/lib/init.ts CHANGED
@@ -21,9 +21,9 @@ const gInit = globalThis as any;
21
21
  function migrateSecrets() {
22
22
  try {
23
23
  const { existsSync, readFileSync } = require('node:fs');
24
- const { homedir } = require('node:os');
25
24
  const YAML = require('yaml');
26
- const dataDir = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
25
+ const { getDataDir: _gdd } = require('./dirs');
26
+ const dataDir = _gdd();
27
27
  const file = join(dataDir, 'settings.yaml');
28
28
  if (!existsSync(file)) return;
29
29
  const raw = YAML.parse(readFileSync(file, 'utf-8')) || {};
package/lib/password.ts CHANGED
@@ -11,11 +11,11 @@
11
11
  */
12
12
 
13
13
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
14
- import { homedir } from 'node:os';
15
14
  import { join, dirname } from 'node:path';
16
15
  import { randomInt } from 'node:crypto';
16
+ import { getDataDir } from './dirs';
17
17
 
18
- const DATA_DIR = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
18
+ const DATA_DIR = getDataDir();
19
19
  const SESSION_CODE_FILE = join(DATA_DIR, 'session-code.json');
20
20
 
21
21
  /** Generate a random 8-digit numeric code */
package/lib/pipeline.ts CHANGED
@@ -8,15 +8,15 @@
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
- import { homedir } from 'node:os';
12
11
  import YAML from 'yaml';
13
12
  import { createTask, getTask, onTaskEvent, taskModelOverrides } from './task-manager';
14
13
  import { getProjectInfo } from './projects';
15
14
  import { loadSettings } from './settings';
16
15
  import type { Task } from '@/src/types';
16
+ import { getDataDir } from './dirs';
17
17
 
18
- const PIPELINES_DIR = join(homedir(), '.forge', 'pipelines');
19
- const WORKFLOWS_DIR = join(homedir(), '.forge', 'flows');
18
+ const PIPELINES_DIR = join(getDataDir(), 'pipelines');
19
+ const WORKFLOWS_DIR = join(getDataDir(), 'flows');
20
20
 
21
21
  // Track pipeline task IDs so terminal notifications can skip them (persists across hot-reloads)
22
22
  const pipelineTaskKey = Symbol.for('mw-pipeline-task-ids');
@@ -8,7 +8,7 @@
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { getDb } from '@/src/core/db/database';
10
10
  import { join } from 'node:path';
11
- import { homedir } from 'node:os';
11
+ import { getDataDir } from './dirs';
12
12
  import {
13
13
  listClaudeSessions,
14
14
  getSessionFilePath,
@@ -19,7 +19,7 @@ import {
19
19
  import { scanProjects } from './projects';
20
20
  import { loadSettings } from './settings';
21
21
 
22
- const DB_PATH = join(homedir(), '.forge', 'data.db');
22
+ const DB_PATH = join(getDataDir(), 'workflow.db');
23
23
 
24
24
  // ─── Types ───────────────────────────────────────────────────
25
25
 
package/lib/settings.ts CHANGED
@@ -1,16 +1,17 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
2
  import { join, dirname } from 'node:path';
4
3
  import YAML from 'yaml';
5
4
  import { encryptSecret, decryptSecret, isEncrypted, SECRET_FIELDS } from './crypto';
5
+ import { getDataDir } from './dirs';
6
6
 
7
- const DATA_DIR = process.env.FORGE_DATA_DIR || join(homedir(), '.forge');
7
+ const DATA_DIR = getDataDir();
8
8
  const SETTINGS_FILE = join(DATA_DIR, 'settings.yaml');
9
9
 
10
10
  export interface Settings {
11
11
  projectRoots: string[]; // Multiple project directories
12
12
  docRoots: string[]; // Markdown document directories (e.g. Obsidian vaults)
13
13
  claudePath: string; // Path to claude binary
14
+ claudeHome: string; // Claude Code home directory (default: ~/.claude)
14
15
  telegramBotToken: string; // Telegram Bot API token
15
16
  telegramChatId: string; // Telegram chat ID to send notifications to
16
17
  notifyOnComplete: boolean; // Notify when task completes
@@ -31,6 +32,7 @@ const defaults: Settings = {
31
32
  projectRoots: [],
32
33
  docRoots: [],
33
34
  claudePath: '',
35
+ claudeHome: '',
34
36
  telegramBotToken: '',
35
37
  telegramChatId: '',
36
38
  notifyOnComplete: true,