@aion0/forge 0.4.15 → 0.5.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 (100) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/RELEASE_NOTES.md +170 -13
  4. package/app/api/agents/route.ts +17 -0
  5. package/app/api/delivery/[id]/route.ts +62 -0
  6. package/app/api/delivery/route.ts +40 -0
  7. package/app/api/mobile-chat/route.ts +13 -7
  8. package/app/api/monitor/route.ts +10 -6
  9. package/app/api/pipelines/[id]/route.ts +16 -3
  10. package/app/api/tasks/route.ts +2 -1
  11. package/app/api/workspace/[id]/agents/route.ts +35 -0
  12. package/app/api/workspace/[id]/memory/route.ts +23 -0
  13. package/app/api/workspace/[id]/smith/route.ts +22 -0
  14. package/app/api/workspace/[id]/stream/route.ts +28 -0
  15. package/app/api/workspace/route.ts +100 -0
  16. package/app/global-error.tsx +10 -4
  17. package/app/icon.ico +0 -0
  18. package/app/layout.tsx +2 -2
  19. package/app/login/LoginForm.tsx +96 -0
  20. package/app/login/page.tsx +7 -98
  21. package/app/page.tsx +2 -2
  22. package/bin/forge-server.mjs +23 -4
  23. package/check-forge-status.sh +9 -0
  24. package/cli/mw.ts +2 -2
  25. package/components/ConversationEditor.tsx +411 -0
  26. package/components/ConversationGraphView.tsx +347 -0
  27. package/components/ConversationTerminalView.tsx +303 -0
  28. package/components/Dashboard.tsx +36 -39
  29. package/components/DashboardWrapper.tsx +9 -0
  30. package/components/DeliveryFlowEditor.tsx +491 -0
  31. package/components/DeliveryList.tsx +230 -0
  32. package/components/DeliveryWorkspace.tsx +589 -0
  33. package/components/DocTerminal.tsx +12 -4
  34. package/components/DocsViewer.tsx +10 -2
  35. package/components/HelpTerminal.tsx +13 -8
  36. package/components/InlinePipelineView.tsx +111 -0
  37. package/components/MobileView.tsx +20 -0
  38. package/components/MonitorPanel.tsx +9 -4
  39. package/components/NewTaskModal.tsx +32 -0
  40. package/components/PipelineEditor.tsx +49 -6
  41. package/components/PipelineView.tsx +482 -64
  42. package/components/ProjectDetail.tsx +314 -56
  43. package/components/ProjectManager.tsx +49 -4
  44. package/components/SessionView.tsx +27 -13
  45. package/components/SettingsModal.tsx +790 -124
  46. package/components/SkillsPanel.tsx +34 -8
  47. package/components/TaskBoard.tsx +3 -0
  48. package/components/WebTerminal.tsx +259 -45
  49. package/components/WorkspaceTree.tsx +221 -0
  50. package/components/WorkspaceView.tsx +2224 -0
  51. package/docs/LOCAL-DEPLOY.md +15 -15
  52. package/install.sh +2 -2
  53. package/lib/agents/claude-adapter.ts +104 -0
  54. package/lib/agents/generic-adapter.ts +64 -0
  55. package/lib/agents/index.ts +242 -0
  56. package/lib/agents/types.ts +70 -0
  57. package/lib/artifacts.ts +106 -0
  58. package/lib/cloudflared.ts +1 -1
  59. package/lib/delivery.ts +787 -0
  60. package/lib/forge-skills/forge-inbox.md +37 -0
  61. package/lib/forge-skills/forge-send.md +40 -0
  62. package/lib/forge-skills/forge-status.md +32 -0
  63. package/lib/forge-skills/forge-workspace-sync.md +37 -0
  64. package/lib/help-docs/00-overview.md +8 -2
  65. package/lib/help-docs/01-settings.md +159 -2
  66. package/lib/help-docs/05-pipelines.md +95 -6
  67. package/lib/help-docs/07-projects.md +35 -1
  68. package/lib/help-docs/11-workspace.md +204 -0
  69. package/lib/help-docs/CLAUDE.md +5 -2
  70. package/lib/init.ts +62 -12
  71. package/lib/pipeline.ts +537 -1
  72. package/lib/settings.ts +115 -22
  73. package/lib/skills.ts +249 -372
  74. package/lib/task-manager.ts +113 -33
  75. package/lib/telegram-bot.ts +33 -1
  76. package/lib/telegram-standalone.ts +1 -1
  77. package/lib/terminal-server.ts +2 -2
  78. package/lib/terminal-standalone.ts +1 -1
  79. package/lib/workspace/__tests__/state-machine.test.ts +388 -0
  80. package/lib/workspace/__tests__/workspace.test.ts +311 -0
  81. package/lib/workspace/agent-bus.ts +416 -0
  82. package/lib/workspace/agent-worker.ts +667 -0
  83. package/lib/workspace/backends/api-backend.ts +262 -0
  84. package/lib/workspace/backends/cli-backend.ts +479 -0
  85. package/lib/workspace/index.ts +82 -0
  86. package/lib/workspace/manager.ts +136 -0
  87. package/lib/workspace/orchestrator.ts +1804 -0
  88. package/lib/workspace/persistence.ts +310 -0
  89. package/lib/workspace/presets.ts +170 -0
  90. package/lib/workspace/skill-installer.ts +188 -0
  91. package/lib/workspace/smith-memory.ts +498 -0
  92. package/lib/workspace/types.ts +231 -0
  93. package/lib/workspace/watch-manager.ts +288 -0
  94. package/lib/workspace-standalone.ts +790 -0
  95. package/middleware.ts +1 -0
  96. package/next-env.d.ts +1 -1
  97. package/package.json +5 -2
  98. package/src/config/index.ts +13 -2
  99. package/src/core/db/database.ts +1 -0
  100. package/start.sh +10 -0
package/lib/settings.ts CHANGED
@@ -7,26 +7,56 @@ import { getDataDir } from './dirs';
7
7
  const DATA_DIR = getDataDir();
8
8
  const SETTINGS_FILE = join(DATA_DIR, 'settings.yaml');
9
9
 
10
+ export interface AgentEntry {
11
+ // Base agent fields (for detected agents like claude, codex, aider)
12
+ path?: string; name?: string; enabled?: boolean;
13
+ flags?: string[]; taskFlags?: string; interactiveCmd?: string; resumeFlag?: string; outputFormat?: string;
14
+ models?: { terminal?: string; task?: string; telegram?: string; help?: string; mobile?: string };
15
+ skipPermissionsFlag?: string;
16
+ requiresTTY?: boolean;
17
+ // Profile fields (for profiles that extend a base agent)
18
+ base?: string; // base agent ID (e.g., 'claude') — makes this a profile
19
+ // API profile fields
20
+ type?: 'cli' | 'api'; // 'api' = API mode, default = 'cli'
21
+ provider?: string; // API provider (e.g., 'anthropic', 'google')
22
+ model?: string; // model override (for both CLI and API profiles)
23
+ apiKey?: string; // per-profile API key (encrypted)
24
+ env?: Record<string, string>; // environment variables injected when spawning CLI
25
+ cliType?: 'claude-code' | 'codex' | 'aider' | 'generic'; // CLI tool type — determines session support, resume flags, etc.
26
+ profile?: string; // linked profile ID — overrides model, env, etc. when launching
27
+ }
28
+
29
+ export interface ProviderEntry {
30
+ apiKey?: string; // encrypted, fallback to env var
31
+ defaultModel?: string;
32
+ enabled?: boolean;
33
+ }
34
+
10
35
  export interface Settings {
11
- projectRoots: string[]; // Multiple project directories
12
- docRoots: string[]; // Markdown document directories (e.g. Obsidian vaults)
13
- claudePath: string; // Path to claude binary
14
- claudeHome: string; // Claude Code home directory (default: ~/.claude)
15
- telegramBotToken: string; // Telegram Bot API token
16
- telegramChatId: string; // Telegram chat ID to send notifications to
17
- notifyOnComplete: boolean; // Notify when task completes
18
- notifyOnFailure: boolean; // Notify when task fails
19
- tunnelAutoStart: boolean; // Auto-start Cloudflare Tunnel on startup
20
- telegramTunnelPassword: string; // Admin password (encrypted) — for login, tunnel, secrets, Telegram
21
- taskModel: string; // Model for tasks (default: sonnet)
22
- pipelineModel: string; // Model for pipelines (default: sonnet)
23
- telegramModel: string; // Model for Telegram AI features (default: sonnet)
24
- skipPermissions: boolean; // Add --dangerously-skip-permissions to all claude invocations
25
- notificationRetentionDays: number; // Auto-cleanup notifications older than N days
26
- skillsRepoUrl: string; // GitHub raw URL for skills registry
27
- displayName: string; // User display name (shown in header)
28
- displayEmail: string; // User email (for session/future integrations)
29
- favoriteProjects: string[]; // Favorite project paths (shown at top of sidebar)
36
+ projectRoots: string[];
37
+ docRoots: string[];
38
+ claudePath: string;
39
+ claudeHome: string;
40
+ telegramBotToken: string;
41
+ telegramChatId: string;
42
+ notifyOnComplete: boolean;
43
+ notifyOnFailure: boolean;
44
+ tunnelAutoStart: boolean;
45
+ telegramTunnelPassword: string;
46
+ taskModel: string;
47
+ pipelineModel: string;
48
+ telegramModel: string;
49
+ skipPermissions: boolean;
50
+ notificationRetentionDays: number;
51
+ skillsRepoUrl: string;
52
+ displayName: string;
53
+ displayEmail: string;
54
+ favoriteProjects: string[];
55
+ defaultAgent: string;
56
+ telegramAgent: string;
57
+ docsAgent: string;
58
+ agents: Record<string, AgentEntry>;
59
+ providers: Record<string, ProviderEntry>; // API provider configs
30
60
  }
31
61
 
32
62
  const defaults: Settings = {
@@ -49,20 +79,65 @@ const defaults: Settings = {
49
79
  displayName: 'Forge',
50
80
  displayEmail: '',
51
81
  favoriteProjects: [],
82
+ defaultAgent: 'claude',
83
+ telegramAgent: '',
84
+ docsAgent: '',
85
+ agents: {},
86
+ providers: {},
52
87
  };
53
88
 
89
+ /** Decrypt nested apiKey fields in agents and providers */
90
+ function decryptNestedSecrets(settings: Settings): void {
91
+ // Decrypt provider apiKeys
92
+ if (settings.providers) {
93
+ for (const p of Object.values(settings.providers)) {
94
+ if (p.apiKey && isEncrypted(p.apiKey)) {
95
+ p.apiKey = decryptSecret(p.apiKey);
96
+ }
97
+ }
98
+ }
99
+ // Decrypt agent profile apiKeys
100
+ if (settings.agents) {
101
+ for (const a of Object.values(settings.agents)) {
102
+ if (a.apiKey && isEncrypted(a.apiKey)) {
103
+ a.apiKey = decryptSecret(a.apiKey);
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ /** Encrypt nested apiKey fields in agents and providers */
110
+ function encryptNestedSecrets(settings: Settings): void {
111
+ if (settings.providers) {
112
+ for (const p of Object.values(settings.providers)) {
113
+ if (p.apiKey && !isEncrypted(p.apiKey)) {
114
+ p.apiKey = encryptSecret(p.apiKey);
115
+ }
116
+ }
117
+ }
118
+ if (settings.agents) {
119
+ for (const a of Object.values(settings.agents)) {
120
+ if (a.apiKey && !isEncrypted(a.apiKey)) {
121
+ a.apiKey = encryptSecret(a.apiKey);
122
+ }
123
+ }
124
+ }
125
+ }
126
+
54
127
  /** Load settings with secrets decrypted (for internal use) */
55
128
  export function loadSettings(): Settings {
56
129
  if (!existsSync(SETTINGS_FILE)) return { ...defaults };
57
130
  try {
58
131
  const raw = readFileSync(SETTINGS_FILE, 'utf-8');
59
132
  const parsed = { ...defaults, ...YAML.parse(raw) };
60
- // Decrypt secret fields
133
+ // Decrypt top-level secret fields
61
134
  for (const field of SECRET_FIELDS) {
62
135
  if (parsed[field] && isEncrypted(parsed[field])) {
63
136
  parsed[field] = decryptSecret(parsed[field]);
64
137
  }
65
138
  }
139
+ // Decrypt nested apiKeys
140
+ decryptNestedSecrets(parsed);
66
141
  return parsed;
67
142
  } catch {
68
143
  return { ...defaults };
@@ -77,6 +152,21 @@ export function loadSettingsMasked(): Settings & { _secretStatus: Record<string,
77
152
  status[field] = !!settings[field];
78
153
  settings[field] = settings[field] ? '••••••••' : '';
79
154
  }
155
+ // Mask nested apiKeys
156
+ if (settings.providers) {
157
+ for (const [name, p] of Object.entries(settings.providers)) {
158
+ status[`providers.${name}.apiKey`] = !!p.apiKey;
159
+ p.apiKey = p.apiKey ? '••••••••' : '';
160
+ }
161
+ }
162
+ if (settings.agents) {
163
+ for (const [name, a] of Object.entries(settings.agents)) {
164
+ if (a.apiKey) {
165
+ status[`agents.${name}.apiKey`] = true;
166
+ a.apiKey = '••••••••';
167
+ }
168
+ }
169
+ }
80
170
  return { ...settings, _secretStatus: status };
81
171
  }
82
172
 
@@ -84,13 +174,16 @@ export function loadSettingsMasked(): Settings & { _secretStatus: Record<string,
84
174
  export function saveSettings(settings: Settings) {
85
175
  const dir = dirname(SETTINGS_FILE);
86
176
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
87
- // Encrypt secret fields before saving
88
- const toSave = { ...settings };
177
+ // Deep copy to avoid mutating original
178
+ const toSave = JSON.parse(JSON.stringify(settings));
179
+ // Encrypt top-level secret fields
89
180
  for (const field of SECRET_FIELDS) {
90
181
  if (toSave[field] && !isEncrypted(toSave[field])) {
91
182
  toSave[field] = encryptSecret(toSave[field]);
92
183
  }
93
184
  }
185
+ // Encrypt nested apiKeys
186
+ encryptNestedSecrets(toSave);
94
187
  writeFileSync(SETTINGS_FILE, YAML.stringify(toSave), 'utf-8');
95
188
  }
96
189