@aion0/forge 0.2.36 → 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.
- package/CLAUDE.md +9 -2
- package/README.md +9 -9
- package/app/api/claude-templates/route.ts +145 -0
- package/app/api/docs/sessions/route.ts +3 -3
- package/app/api/monitor/route.ts +2 -2
- package/app/api/pipelines/route.ts +2 -2
- package/app/api/preview/[...path]/route.ts +2 -2
- package/app/api/preview/route.ts +3 -4
- package/app/api/projects/route.ts +19 -0
- package/app/api/skills/local/route.ts +228 -0
- package/app/api/skills/route.ts +36 -11
- package/app/api/terminal-state/route.ts +2 -2
- package/app/login/page.tsx +1 -1
- package/bin/forge-server.mjs +49 -33
- package/cli/mw.ts +27 -9
- package/components/Dashboard.tsx +14 -0
- package/components/ProjectManager.tsx +581 -42
- package/components/SessionView.tsx +1 -1
- package/components/SettingsModal.tsx +18 -1
- package/components/SkillsPanel.tsx +515 -29
- package/components/WebTerminal.tsx +50 -5
- package/instrumentation.ts +2 -2
- package/lib/claude-sessions.ts +2 -2
- package/lib/claude-templates.ts +227 -0
- package/lib/cloudflared.ts +4 -3
- package/lib/crypto.ts +3 -4
- package/lib/dirs.ts +34 -0
- package/lib/flows.ts +2 -2
- package/lib/init.ts +2 -2
- package/lib/password.ts +2 -2
- package/lib/pipeline.ts +3 -3
- package/lib/session-watcher.ts +2 -2
- package/lib/settings.ts +4 -2
- package/lib/skills.ts +444 -79
- package/lib/telegram-bot.ts +12 -5
- package/lib/terminal-standalone.ts +3 -2
- package/package.json +1 -1
- package/src/config/index.ts +6 -7
- package/src/core/db/database.ts +17 -5
|
@@ -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
|
|
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:
|
|
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
|
|
package/instrumentation.ts
CHANGED
|
@@ -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 {
|
|
12
|
-
const dataDir =
|
|
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')) {
|
package/lib/claude-sessions.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
+
}
|
package/lib/cloudflared.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
19
|
-
const WORKFLOWS_DIR = join(
|
|
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');
|
package/lib/session-watcher.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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 =
|
|
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,
|