overarch-linux-arm64 0.8.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/dist/bin/overarch.d.ts +2 -0
  4. package/dist/bin/overarch.js +178 -0
  5. package/dist/src/agent/context.d.ts +14 -0
  6. package/dist/src/agent/context.js +200 -0
  7. package/dist/src/agent/context_compressor.d.ts +21 -0
  8. package/dist/src/agent/context_compressor.js +90 -0
  9. package/dist/src/agent/manifest.d.ts +22 -0
  10. package/dist/src/agent/manifest.js +131 -0
  11. package/dist/src/agent/planner.d.ts +38 -0
  12. package/dist/src/agent/planner.js +118 -0
  13. package/dist/src/agent/runtime.d.ts +59 -0
  14. package/dist/src/agent/runtime.js +400 -0
  15. package/dist/src/cli/approval.d.ts +13 -0
  16. package/dist/src/cli/approval.js +71 -0
  17. package/dist/src/cli/args.d.ts +15 -0
  18. package/dist/src/cli/args.js +47 -0
  19. package/dist/src/cli/commands/audit.d.ts +10 -0
  20. package/dist/src/cli/commands/audit.js +79 -0
  21. package/dist/src/cli/commands/companion.d.ts +1 -0
  22. package/dist/src/cli/commands/companion.js +143 -0
  23. package/dist/src/cli/commands/config.d.ts +1 -0
  24. package/dist/src/cli/commands/config.js +135 -0
  25. package/dist/src/cli/commands/doctor.d.ts +1 -0
  26. package/dist/src/cli/commands/doctor.js +217 -0
  27. package/dist/src/cli/commands/init.d.ts +1 -0
  28. package/dist/src/cli/commands/init.js +134 -0
  29. package/dist/src/cli/commands/memory.d.ts +2 -0
  30. package/dist/src/cli/commands/memory.js +98 -0
  31. package/dist/src/cli/commands/onboarding.d.ts +3 -0
  32. package/dist/src/cli/commands/onboarding.js +223 -0
  33. package/dist/src/cli/commands/sessions.d.ts +1 -0
  34. package/dist/src/cli/commands/sessions.js +22 -0
  35. package/dist/src/cli/commands/status.d.ts +1 -0
  36. package/dist/src/cli/commands/status.js +37 -0
  37. package/dist/src/cli/commands/update.d.ts +9 -0
  38. package/dist/src/cli/commands/update.js +105 -0
  39. package/dist/src/cli/repl.d.ts +3 -0
  40. package/dist/src/cli/repl.js +821 -0
  41. package/dist/src/cli/ui.d.ts +53 -0
  42. package/dist/src/cli/ui.js +215 -0
  43. package/dist/src/companion/petdex_adapter.d.ts +31 -0
  44. package/dist/src/companion/petdex_adapter.js +171 -0
  45. package/dist/src/companion/petdex_bridge.d.ts +65 -0
  46. package/dist/src/companion/petdex_bridge.js +356 -0
  47. package/dist/src/companion/types.d.ts +57 -0
  48. package/dist/src/companion/types.js +11 -0
  49. package/dist/src/config/index.d.ts +24 -0
  50. package/dist/src/config/index.js +237 -0
  51. package/dist/src/config/types.d.ts +58 -0
  52. package/dist/src/config/types.js +7 -0
  53. package/dist/src/core/diagnostics.d.ts +48 -0
  54. package/dist/src/core/diagnostics.js +115 -0
  55. package/dist/src/core/index.d.ts +23 -0
  56. package/dist/src/core/index.js +23 -0
  57. package/dist/src/core/mission.d.ts +55 -0
  58. package/dist/src/core/mission.js +173 -0
  59. package/dist/src/index.d.ts +1 -0
  60. package/dist/src/index.js +1 -0
  61. package/dist/src/learning/memory_manager.d.ts +26 -0
  62. package/dist/src/learning/memory_manager.js +211 -0
  63. package/dist/src/learning/types.d.ts +54 -0
  64. package/dist/src/learning/types.js +1 -0
  65. package/dist/src/mcp/registry.d.ts +17 -0
  66. package/dist/src/mcp/registry.js +97 -0
  67. package/dist/src/mcp/types.d.ts +13 -0
  68. package/dist/src/mcp/types.js +1 -0
  69. package/dist/src/providers/anthropic.d.ts +10 -0
  70. package/dist/src/providers/anthropic.js +95 -0
  71. package/dist/src/providers/base.d.ts +15 -0
  72. package/dist/src/providers/base.js +108 -0
  73. package/dist/src/providers/factory.d.ts +28 -0
  74. package/dist/src/providers/factory.js +142 -0
  75. package/dist/src/providers/gemini.d.ts +10 -0
  76. package/dist/src/providers/gemini.js +97 -0
  77. package/dist/src/providers/openai.d.ts +9 -0
  78. package/dist/src/providers/openai.js +85 -0
  79. package/dist/src/providers/openrouter.d.ts +5 -0
  80. package/dist/src/providers/openrouter.js +14 -0
  81. package/dist/src/providers/registry.d.ts +19 -0
  82. package/dist/src/providers/registry.js +209 -0
  83. package/dist/src/providers/types.d.ts +59 -0
  84. package/dist/src/providers/types.js +1 -0
  85. package/dist/src/providers/validator.d.ts +24 -0
  86. package/dist/src/providers/validator.js +161 -0
  87. package/dist/src/security/audit.d.ts +56 -0
  88. package/dist/src/security/audit.js +154 -0
  89. package/dist/src/security/guardrails.d.ts +9 -0
  90. package/dist/src/security/guardrails.js +32 -0
  91. package/dist/src/security/permissions.d.ts +12 -0
  92. package/dist/src/security/permissions.js +68 -0
  93. package/dist/src/security/policy.d.ts +220 -0
  94. package/dist/src/security/policy.js +720 -0
  95. package/dist/src/session/manager.d.ts +22 -0
  96. package/dist/src/session/manager.js +172 -0
  97. package/dist/src/session/types.d.ts +36 -0
  98. package/dist/src/session/types.js +1 -0
  99. package/dist/src/tools/checkpoint.d.ts +26 -0
  100. package/dist/src/tools/checkpoint.js +160 -0
  101. package/dist/src/tools/diagnostics.d.ts +2 -0
  102. package/dist/src/tools/diagnostics.js +125 -0
  103. package/dist/src/tools/diff.d.ts +14 -0
  104. package/dist/src/tools/diff.js +124 -0
  105. package/dist/src/tools/filesystem.d.ts +2 -0
  106. package/dist/src/tools/filesystem.js +224 -0
  107. package/dist/src/tools/git.d.ts +2 -0
  108. package/dist/src/tools/git.js +138 -0
  109. package/dist/src/tools/markdown.d.ts +33 -0
  110. package/dist/src/tools/markdown.js +190 -0
  111. package/dist/src/tools/project.d.ts +2 -0
  112. package/dist/src/tools/project.js +63 -0
  113. package/dist/src/tools/registry.d.ts +14 -0
  114. package/dist/src/tools/registry.js +230 -0
  115. package/dist/src/tools/search.d.ts +2 -0
  116. package/dist/src/tools/search.js +136 -0
  117. package/dist/src/tools/shell.d.ts +2 -0
  118. package/dist/src/tools/shell.js +74 -0
  119. package/dist/src/tools/symbols.d.ts +30 -0
  120. package/dist/src/tools/symbols.js +155 -0
  121. package/dist/src/tools/types.d.ts +28 -0
  122. package/dist/src/tools/types.js +1 -0
  123. package/dist/src/tools/web_search.d.ts +14 -0
  124. package/dist/src/tools/web_search.js +154 -0
  125. package/install.sh +29 -0
  126. package/overarch +15 -0
  127. package/package.json +28 -0
@@ -0,0 +1,356 @@
1
+ import * as http from 'node:http';
2
+ import * as path from 'node:path';
3
+ import * as fs from 'node:fs/promises';
4
+ import * as os from 'node:os';
5
+ import { spawn } from 'node:child_process';
6
+ import { petdexAdapter } from './petdex_adapter.js';
7
+ export const STATE_TO_PETDEX_TRIGGER = {
8
+ idle: 'idle',
9
+ thinking: 'focus',
10
+ reading: 'reading',
11
+ searching: 'searching',
12
+ editing: 'typing',
13
+ executing: 'running',
14
+ testing: 'testing',
15
+ waiting: 'waiting',
16
+ awaiting_approval: 'waiting',
17
+ success: 'celebrate',
18
+ warning: 'alert',
19
+ error: 'alert',
20
+ paused: 'sleep',
21
+ stopped: 'sleep',
22
+ offline: 'sleep',
23
+ };
24
+ export class PetdexBridge {
25
+ server = null;
26
+ sseClients = new Set();
27
+ currentState = 'idle';
28
+ currentMessage = '';
29
+ activePet = 'savage-codex-hacker';
30
+ port = 45731;
31
+ externalHookPort = 7777;
32
+ isRunning = false;
33
+ companionProcess = null;
34
+ constructor(options) {
35
+ if (options?.port)
36
+ this.port = options.port;
37
+ if (options?.externalHookPort)
38
+ this.externalHookPort = options.externalHookPort;
39
+ }
40
+ /**
41
+ * Starts local SSE / HTTP bridge server (unref'd so it does not block Node process exit)
42
+ */
43
+ start() {
44
+ if (this.isRunning && this.server) {
45
+ return Promise.resolve();
46
+ }
47
+ return new Promise((resolve) => {
48
+ try {
49
+ this.server = http.createServer((req, res) => {
50
+ this.handleRequest(req, res);
51
+ });
52
+ this.server.on('error', (_err) => {
53
+ this.isRunning = false;
54
+ resolve();
55
+ });
56
+ this.server.listen(this.port, '127.0.0.1', () => {
57
+ this.isRunning = true;
58
+ this.server?.unref();
59
+ resolve();
60
+ });
61
+ }
62
+ catch {
63
+ resolve();
64
+ }
65
+ });
66
+ }
67
+ handleRequest(req, res) {
68
+ const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
69
+ res.setHeader('Access-Control-Allow-Origin', '*');
70
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
71
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
72
+ if (req.method === 'OPTIONS') {
73
+ res.writeHead(204);
74
+ res.end();
75
+ return;
76
+ }
77
+ // SSE endpoint: /events
78
+ if (url.pathname === '/events' && req.method === 'GET') {
79
+ res.writeHead(200, {
80
+ 'Content-Type': 'text/event-stream',
81
+ 'Cache-Control': 'no-cache, no-transform',
82
+ Connection: 'keep-alive',
83
+ });
84
+ res.write(`: connected to Overarch Petdex Companion Bridge\n\n`);
85
+ this.sseClients.add(res);
86
+ const rowMapping = petdexAdapter.mapStateToAnimationRow(this.currentState);
87
+ const initialPayload = {
88
+ event: 'state_change',
89
+ data: {
90
+ state: this.currentState,
91
+ petdexTrigger: STATE_TO_PETDEX_TRIGGER[this.currentState] || 'idle',
92
+ animationRow: rowMapping.row,
93
+ message: this.currentMessage,
94
+ pet: this.activePet,
95
+ timestamp: new Date().toISOString(),
96
+ },
97
+ };
98
+ res.write(`data: ${JSON.stringify(initialPayload)}\n\n`);
99
+ req.on('close', () => {
100
+ this.sseClients.delete(res);
101
+ });
102
+ return;
103
+ }
104
+ // JSON status endpoint: /state or /companion/status
105
+ if ((url.pathname === '/state' || url.pathname === '/companion/status') && req.method === 'GET') {
106
+ res.writeHead(200, { 'Content-Type': 'application/json' });
107
+ res.end(JSON.stringify({
108
+ active: true,
109
+ state: this.currentState,
110
+ petdexTrigger: STATE_TO_PETDEX_TRIGGER[this.currentState] || 'idle',
111
+ animationRow: petdexAdapter.mapStateToAnimationRow(this.currentState).row,
112
+ message: this.currentMessage,
113
+ pet: this.activePet,
114
+ activePet: this.activePet,
115
+ clientsConnected: this.sseClients.size,
116
+ port: this.port,
117
+ }));
118
+ return;
119
+ }
120
+ // Discovered pets endpoint: /pets or /companion/pets
121
+ if ((url.pathname === '/pets' || url.pathname === '/companion/pets') && req.method === 'GET') {
122
+ petdexAdapter.listPets().then((pets) => {
123
+ res.writeHead(200, { 'Content-Type': 'application/json' });
124
+ res.end(JSON.stringify({ activePet: this.activePet, pets }));
125
+ });
126
+ return;
127
+ }
128
+ // Select active companion pet: POST /select or /companion/select
129
+ if ((url.pathname === '/select' || url.pathname === '/companion/select') && req.method === 'POST') {
130
+ let body = '';
131
+ req.on('data', (chunk) => (body += chunk));
132
+ req.on('end', () => {
133
+ try {
134
+ const parsed = JSON.parse(body || '{}');
135
+ if (parsed.pet) {
136
+ this.activePet = String(parsed.pet);
137
+ const rowMapping = petdexAdapter.mapStateToAnimationRow(this.currentState);
138
+ this.broadcast('pet_selected', {
139
+ state: this.currentState,
140
+ petdexTrigger: STATE_TO_PETDEX_TRIGGER[this.currentState] || 'idle',
141
+ animationRow: rowMapping.row,
142
+ pet: this.activePet,
143
+ timestamp: new Date().toISOString(),
144
+ });
145
+ res.writeHead(200, { 'Content-Type': 'application/json' });
146
+ res.end(JSON.stringify({ success: true, activePet: this.activePet }));
147
+ return;
148
+ }
149
+ }
150
+ catch { }
151
+ res.writeHead(400, { 'Content-Type': 'application/json' });
152
+ res.end(JSON.stringify({ error: 'Missing or invalid pet name in payload' }));
153
+ });
154
+ return;
155
+ }
156
+ // Install Petdex pet endpoint: POST /companion/install
157
+ if (url.pathname === '/companion/install' && req.method === 'POST') {
158
+ let body = '';
159
+ req.on('data', (chunk) => (body += chunk));
160
+ req.on('end', async () => {
161
+ try {
162
+ const parsed = JSON.parse(body || '{}');
163
+ if (parsed.slug) {
164
+ const installRes = await petdexAdapter.installPet(parsed.slug);
165
+ res.writeHead(installRes.success ? 200 : 500, { 'Content-Type': 'application/json' });
166
+ res.end(JSON.stringify(installRes));
167
+ return;
168
+ }
169
+ }
170
+ catch { }
171
+ res.writeHead(400, { 'Content-Type': 'application/json' });
172
+ res.end(JSON.stringify({ error: 'Missing slug' }));
173
+ });
174
+ return;
175
+ }
176
+ res.writeHead(404, { 'Content-Type': 'application/json' });
177
+ res.end(JSON.stringify({ error: 'Not found' }));
178
+ }
179
+ /**
180
+ * Broadcast state change to SSE subscribers and notify external hook port if present
181
+ */
182
+ broadcastState(state, message) {
183
+ this.currentState = state;
184
+ if (message !== undefined) {
185
+ this.currentMessage = message;
186
+ }
187
+ const trigger = STATE_TO_PETDEX_TRIGGER[state] || 'idle';
188
+ const rowMapping = petdexAdapter.mapStateToAnimationRow(state);
189
+ const payload = {
190
+ event: 'state_change',
191
+ data: {
192
+ state,
193
+ petdexTrigger: trigger,
194
+ animationRow: rowMapping.row,
195
+ message: this.currentMessage,
196
+ pet: this.activePet,
197
+ timestamp: new Date().toISOString(),
198
+ },
199
+ };
200
+ const dataStr = `data: ${JSON.stringify(payload)}\n\n`;
201
+ for (const client of this.sseClients) {
202
+ try {
203
+ client.write(dataStr);
204
+ }
205
+ catch {
206
+ this.sseClients.delete(client);
207
+ }
208
+ }
209
+ this.notifyExternalPetdexHook(payload);
210
+ }
211
+ broadcast(event, data) {
212
+ const payload = { event, data };
213
+ const dataStr = `data: ${JSON.stringify(payload)}\n\n`;
214
+ for (const client of this.sseClients) {
215
+ try {
216
+ client.write(dataStr);
217
+ }
218
+ catch {
219
+ this.sseClients.delete(client);
220
+ }
221
+ }
222
+ this.notifyExternalPetdexHook(payload);
223
+ }
224
+ /**
225
+ * Non-blocking, error-tolerant notification to standard Petdex hook port 7777
226
+ */
227
+ notifyExternalPetdexHook(payload) {
228
+ const postData = JSON.stringify(payload);
229
+ const req = http.request({
230
+ hostname: '127.0.0.1',
231
+ port: this.externalHookPort,
232
+ path: '/events',
233
+ method: 'POST',
234
+ headers: {
235
+ 'Content-Type': 'application/json',
236
+ 'Content-Length': Buffer.byteLength(postData),
237
+ },
238
+ timeout: 60,
239
+ }, (res) => {
240
+ res.resume();
241
+ });
242
+ req.on('error', () => {
243
+ // Gracefully silent when external hook server is not running
244
+ });
245
+ req.on('timeout', () => {
246
+ req.destroy();
247
+ });
248
+ req.write(postData);
249
+ req.end();
250
+ }
251
+ /**
252
+ * Discovers Petdex-compatible pets
253
+ */
254
+ async discoverPets() {
255
+ return petdexAdapter.listPets();
256
+ }
257
+ /**
258
+ * Launches native Overarch desktop companion (or external Petdex app if present)
259
+ */
260
+ async launchCompanion(cwd = process.cwd()) {
261
+ // Check candidate paths for OverarchCompanion.exe
262
+ const companionCandidates = [
263
+ path.join(cwd, 'dist-desktop', 'OverarchCompanion.exe'),
264
+ path.join(path.dirname(process.execPath), 'OverarchCompanion.exe'),
265
+ path.join(os.homedir(), '.overarch', 'bin', 'OverarchCompanion.exe'),
266
+ ];
267
+ let binaryPath = null;
268
+ for (const candidate of companionCandidates) {
269
+ try {
270
+ await fs.access(candidate);
271
+ binaryPath = candidate;
272
+ break;
273
+ }
274
+ catch { }
275
+ }
276
+ if (binaryPath) {
277
+ try {
278
+ const proc = spawn(binaryPath, [`--bridge=http://127.0.0.1:${this.port}`, `--pet=${this.activePet}`], {
279
+ detached: true,
280
+ stdio: 'ignore',
281
+ });
282
+ proc.unref();
283
+ this.companionProcess = proc;
284
+ return {
285
+ success: true,
286
+ message: `Launched Overarch Desktop Companion (${this.activePet}) connected to bridge port ${this.port}.`,
287
+ };
288
+ }
289
+ catch (err) {
290
+ return { success: false, message: `Failed to launch ${binaryPath}: ${err.message}` };
291
+ }
292
+ }
293
+ // Fallback: try petdex CLI or desktop
294
+ return new Promise((resolve) => {
295
+ const proc = spawn(`petdex start --bridge http://127.0.0.1:${this.port}`, {
296
+ detached: true,
297
+ stdio: 'ignore',
298
+ shell: true,
299
+ });
300
+ proc.on('error', () => {
301
+ resolve({
302
+ success: false,
303
+ message: 'OverarchCompanion.exe not found in dist-desktop/ and external petdex not in PATH.',
304
+ });
305
+ });
306
+ proc.unref();
307
+ setTimeout(() => {
308
+ resolve({
309
+ success: true,
310
+ message: `Dispatched launch to companion bridge (listening on http://127.0.0.1:${this.port}/events).`,
311
+ });
312
+ }, 120);
313
+ });
314
+ }
315
+ getActivePet() {
316
+ return this.activePet;
317
+ }
318
+ setActivePet(pet) {
319
+ this.activePet = pet;
320
+ const rowMapping = petdexAdapter.mapStateToAnimationRow(this.currentState);
321
+ this.broadcast('pet_selected', {
322
+ state: this.currentState,
323
+ petdexTrigger: STATE_TO_PETDEX_TRIGGER[this.currentState] || 'idle',
324
+ animationRow: rowMapping.row,
325
+ pet: this.activePet,
326
+ timestamp: new Date().toISOString(),
327
+ });
328
+ }
329
+ getStatus() {
330
+ return {
331
+ running: this.isRunning,
332
+ port: this.port,
333
+ connectedClients: this.sseClients.size,
334
+ activePet: this.activePet,
335
+ state: this.currentState,
336
+ };
337
+ }
338
+ stop() {
339
+ for (const client of this.sseClients) {
340
+ try {
341
+ client.end();
342
+ }
343
+ catch { }
344
+ }
345
+ this.sseClients.clear();
346
+ if (this.server) {
347
+ try {
348
+ this.server.close();
349
+ }
350
+ catch { }
351
+ this.server = null;
352
+ }
353
+ this.isRunning = false;
354
+ }
355
+ }
356
+ export const petdexBridge = new PetdexBridge();
@@ -0,0 +1,57 @@
1
+ export type AgentState = 'idle' | 'thinking' | 'reading' | 'searching' | 'editing' | 'executing' | 'testing' | 'waiting' | 'awaiting_approval' | 'success' | 'warning' | 'error' | 'paused' | 'stopped' | 'offline';
2
+ export type MissionState = AgentState;
3
+ export interface PetdexManifest {
4
+ id: string;
5
+ displayName: string;
6
+ description?: string;
7
+ spritesheetPath?: string;
8
+ spriteVersionNumber?: 1 | 2;
9
+ author?: string;
10
+ version?: string;
11
+ }
12
+ export interface CompanionPetInfo {
13
+ id: string;
14
+ displayName: string;
15
+ description: string;
16
+ source: 'petdex-installed' | 'petdex-gallery' | 'local';
17
+ spritesheetPath?: string;
18
+ spritesheetFormat: 'webp' | 'png';
19
+ spriteVersionNumber: 1 | 2;
20
+ columns: number;
21
+ rows: number;
22
+ frameWidth?: number;
23
+ frameHeight?: number;
24
+ }
25
+ export interface PetdexAnimationRowMapping {
26
+ idle: number;
27
+ runningRight: number;
28
+ runningLeft: number;
29
+ waving: number;
30
+ jumping: number;
31
+ failed: number;
32
+ waiting: number;
33
+ running: number;
34
+ review: number;
35
+ }
36
+ export declare const PETDEX_DEFAULT_ROW_MAPPING: PetdexAnimationRowMapping;
37
+ export interface CompanionConfig {
38
+ enabled: boolean;
39
+ activePet: string;
40
+ position?: {
41
+ x: number;
42
+ y: number;
43
+ };
44
+ alwaysOnTop: boolean;
45
+ opacity: number;
46
+ scale: number;
47
+ }
48
+ export interface PetProvider {
49
+ id: string;
50
+ name: string;
51
+ listPets(): Promise<CompanionPetInfo[]>;
52
+ getPet(id: string): Promise<CompanionPetInfo | null>;
53
+ installPet?(slug: string): Promise<{
54
+ success: boolean;
55
+ message: string;
56
+ }>;
57
+ }
@@ -0,0 +1,11 @@
1
+ export const PETDEX_DEFAULT_ROW_MAPPING = {
2
+ idle: 0,
3
+ runningRight: 1,
4
+ runningLeft: 2,
5
+ waving: 3,
6
+ jumping: 4,
7
+ failed: 5,
8
+ waiting: 6,
9
+ running: 7,
10
+ review: 8,
11
+ };
@@ -0,0 +1,24 @@
1
+ import { OverarchConfig, ResolvedConfig } from './types.js';
2
+ export declare const DEFAULT_CONFIG: OverarchConfig;
3
+ export declare function getConfigDir(): string;
4
+ /**
5
+ * Load environment variables from .env file without external dependencies
6
+ */
7
+ export declare function loadDotenv(cwd?: string): Promise<Record<string, string>>;
8
+ /**
9
+ * Safely writes or updates an API key in the project's .env file.
10
+ * Automatically verifies and adds .env to .gitignore to prevent secret leaks.
11
+ */
12
+ export declare function writeDotenvKey(cwd: string, keyName: string, keyValue: string): Promise<void>;
13
+ /**
14
+ * Ensures an entry is present in the workspace .gitignore
15
+ */
16
+ export declare function ensureGitignoreEntry(cwd: string, entry: string): Promise<void>;
17
+ /**
18
+ * Auto-generates or verifies the .overarch project subsystem structure
19
+ */
20
+ export declare function autoEnsureOverarch(cwd?: string): Promise<string>;
21
+ export declare function loadConfig(): Promise<ResolvedConfig>;
22
+ export declare function saveConfig(config: OverarchConfig, targetPath: string): Promise<void>;
23
+ export declare function saveGlobalConfig(updates: Partial<OverarchConfig>): Promise<void>;
24
+ export declare function getApiKey(provider: string, cwd?: string): string | undefined;
@@ -0,0 +1,237 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { PermissionTier } from './types.js';
5
+ export const DEFAULT_CONFIG = {
6
+ providers: {},
7
+ defaultModel: '',
8
+ mode: 'online',
9
+ agentMode: 'approval',
10
+ ponytail: 'full',
11
+ ccss: 'auto',
12
+ permissions: {
13
+ autoApprove: [PermissionTier.ReadOnly, PermissionTier.SafeMutation],
14
+ requireApproval: [PermissionTier.StateMutating, PermissionTier.Destructive],
15
+ deny: []
16
+ },
17
+ context: {
18
+ maxTokens: 100000,
19
+ maxFileSize: 1024 * 1024
20
+ },
21
+ project: {
22
+ include: [],
23
+ exclude: [],
24
+ instructions: ''
25
+ },
26
+ ui: {
27
+ accentColor: 'cyan',
28
+ avatar: '◆',
29
+ verbosity: 'normal',
30
+ },
31
+ onboarding: {
32
+ completed: false,
33
+ version: '0.3.0',
34
+ },
35
+ };
36
+ export function getConfigDir() {
37
+ return path.join(os.homedir(), '.overarch');
38
+ }
39
+ /**
40
+ * Load environment variables from .env file without external dependencies
41
+ */
42
+ export async function loadDotenv(cwd = process.cwd()) {
43
+ const envVars = {};
44
+ const pathsToCheck = [
45
+ path.join(cwd, '.env'),
46
+ path.join(getConfigDir(), '.env')
47
+ ];
48
+ for (const p of pathsToCheck) {
49
+ try {
50
+ const content = await fs.readFile(p, 'utf8');
51
+ const lines = content.replace(/\r\n/g, '\n').split('\n');
52
+ for (const line of lines) {
53
+ const trimmed = line.trim();
54
+ if (!trimmed || trimmed.startsWith('#'))
55
+ continue;
56
+ const eqIdx = trimmed.indexOf('=');
57
+ if (eqIdx > 0) {
58
+ const key = trimmed.slice(0, eqIdx).trim();
59
+ let val = trimmed.slice(eqIdx + 1).trim();
60
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
61
+ val = val.slice(1, -1);
62
+ }
63
+ envVars[key] = val;
64
+ if (!process.env[key]) {
65
+ process.env[key] = val;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ catch { }
71
+ }
72
+ return envVars;
73
+ }
74
+ /**
75
+ * Safely writes or updates an API key in the project's .env file.
76
+ * Automatically verifies and adds .env to .gitignore to prevent secret leaks.
77
+ */
78
+ export async function writeDotenvKey(cwd, keyName, keyValue) {
79
+ const envPath = path.join(cwd, '.env');
80
+ let lines = [];
81
+ try {
82
+ const existing = await fs.readFile(envPath, 'utf8');
83
+ lines = existing.replace(/\r\n/g, '\n').split('\n');
84
+ }
85
+ catch { }
86
+ let found = false;
87
+ const updatedLines = lines.map(line => {
88
+ const trimmed = line.trim();
89
+ if (trimmed.startsWith(`${keyName}=`)) {
90
+ found = true;
91
+ return `${keyName}="${keyValue}"`;
92
+ }
93
+ return line;
94
+ });
95
+ if (!found) {
96
+ if (updatedLines.length > 0 && updatedLines[updatedLines.length - 1] !== '') {
97
+ updatedLines.push('');
98
+ }
99
+ updatedLines.push(`${keyName}="${keyValue}"`);
100
+ }
101
+ await fs.writeFile(envPath, updatedLines.join('\n') + '\n', 'utf8');
102
+ process.env[keyName] = keyValue;
103
+ // Ensure .env is in .gitignore
104
+ await ensureGitignoreEntry(cwd, '.env');
105
+ }
106
+ /**
107
+ * Ensures an entry is present in the workspace .gitignore
108
+ */
109
+ export async function ensureGitignoreEntry(cwd, entry) {
110
+ const gitignorePath = path.join(cwd, '.gitignore');
111
+ try {
112
+ let content = '';
113
+ try {
114
+ content = await fs.readFile(gitignorePath, 'utf8');
115
+ }
116
+ catch { }
117
+ const lines = content.replace(/\r\n/g, '\n').split('\n').map(l => l.trim());
118
+ if (!lines.includes(entry.trim())) {
119
+ const addition = (content && !content.endsWith('\n') ? '\n' : '') + `${entry}\n`;
120
+ await fs.appendFile(gitignorePath, addition, 'utf8');
121
+ }
122
+ }
123
+ catch { }
124
+ }
125
+ /**
126
+ * Auto-generates or verifies the .overarch project subsystem structure
127
+ */
128
+ export async function autoEnsureOverarch(cwd = process.cwd()) {
129
+ const overarchDir = path.join(cwd, '.overarch');
130
+ await fs.mkdir(overarchDir, { recursive: true });
131
+ await fs.mkdir(path.join(overarchDir, 'sessions'), { recursive: true });
132
+ await fs.mkdir(path.join(overarchDir, 'cache'), { recursive: true });
133
+ const filesToEnsure = {
134
+ 'config.json': JSON.stringify({ project: DEFAULT_CONFIG.project }, null, 2),
135
+ 'instructions.md': '# Overarch Project Instructions\n\nAdd repository-specific guidelines, architecture rules, and patterns here.\n',
136
+ 'memory.md': '# Project Memory\n\nPersistent decisions, patterns, and notes discovered during sessions.\n',
137
+ '.gitignore': 'sessions/\ncache/\n*.log\n*.env\nstate.json\n'
138
+ };
139
+ for (const [filename, defaultContent] of Object.entries(filesToEnsure)) {
140
+ const filePath = path.join(overarchDir, filename);
141
+ try {
142
+ await fs.access(filePath);
143
+ }
144
+ catch {
145
+ await fs.writeFile(filePath, defaultContent, 'utf8');
146
+ }
147
+ }
148
+ return overarchDir;
149
+ }
150
+ export async function loadConfig() {
151
+ await loadDotenv(process.cwd());
152
+ let globalConfig = {};
153
+ let projectConfig = {};
154
+ const globalConfigPath = path.join(getConfigDir(), 'config.json');
155
+ try {
156
+ const globalConfigData = await fs.readFile(globalConfigPath, 'utf8');
157
+ globalConfig = JSON.parse(globalConfigData);
158
+ }
159
+ catch (e) {
160
+ // Ignore error
161
+ }
162
+ const projectConfigPath = path.join(process.cwd(), '.overarch', 'config.json');
163
+ try {
164
+ const projectConfigData = await fs.readFile(projectConfigPath, 'utf8');
165
+ projectConfig = JSON.parse(projectConfigData);
166
+ }
167
+ catch (e) {
168
+ // Ignore error
169
+ }
170
+ const mergedConfig = {
171
+ ...DEFAULT_CONFIG,
172
+ ...globalConfig,
173
+ ...projectConfig,
174
+ providers: { ...DEFAULT_CONFIG.providers, ...globalConfig.providers, ...projectConfig.providers },
175
+ permissions: { ...DEFAULT_CONFIG.permissions, ...globalConfig.permissions, ...projectConfig.permissions },
176
+ context: { ...DEFAULT_CONFIG.context, ...globalConfig.context, ...projectConfig.context },
177
+ project: { ...DEFAULT_CONFIG.project, ...globalConfig.project, ...projectConfig.project },
178
+ ui: { ...DEFAULT_CONFIG.ui, ...globalConfig.ui, ...projectConfig.ui },
179
+ onboarding: { ...DEFAULT_CONFIG.onboarding, ...globalConfig.onboarding, ...projectConfig.onboarding },
180
+ };
181
+ return {
182
+ ...mergedConfig,
183
+ configPath: Object.keys(projectConfig).length > 0 ? projectConfigPath : (Object.keys(globalConfig).length > 0 ? globalConfigPath : ''),
184
+ isProjectConfig: Object.keys(projectConfig).length > 0
185
+ };
186
+ }
187
+ export async function saveConfig(config, targetPath) {
188
+ const dir = path.dirname(targetPath);
189
+ await fs.mkdir(dir, { recursive: true });
190
+ await fs.writeFile(targetPath, JSON.stringify(config, null, 2), 'utf8');
191
+ }
192
+ export async function saveGlobalConfig(updates) {
193
+ const globalConfigPath = path.join(getConfigDir(), 'config.json');
194
+ let currentConfig = {};
195
+ try {
196
+ const data = await fs.readFile(globalConfigPath, 'utf8');
197
+ currentConfig = JSON.parse(data);
198
+ }
199
+ catch (e) {
200
+ // ignore
201
+ }
202
+ const newConfig = {
203
+ ...currentConfig,
204
+ ...updates,
205
+ providers: { ...currentConfig.providers, ...updates.providers },
206
+ permissions: { ...currentConfig.permissions, ...updates.permissions },
207
+ context: { ...currentConfig.context, ...updates.context },
208
+ project: { ...currentConfig.project, ...updates.project },
209
+ ui: { ...currentConfig.ui, ...updates.ui },
210
+ onboarding: { ...currentConfig.onboarding, ...updates.onboarding },
211
+ };
212
+ // Strip raw API keys before persisting to disk
213
+ const configToSave = JSON.parse(JSON.stringify(newConfig));
214
+ if (configToSave.providers) {
215
+ for (const p of Object.values(configToSave.providers)) {
216
+ if (p && 'apiKey' in p) {
217
+ delete p.apiKey;
218
+ }
219
+ }
220
+ }
221
+ await fs.mkdir(getConfigDir(), { recursive: true });
222
+ await fs.writeFile(globalConfigPath, JSON.stringify(configToSave, null, 2), 'utf8');
223
+ }
224
+ export function getApiKey(provider, cwd = process.cwd()) {
225
+ const envMap = {
226
+ openai: 'OPENAI_API_KEY',
227
+ anthropic: 'ANTHROPIC_API_KEY',
228
+ gemini: 'GEMINI_API_KEY',
229
+ google: 'GEMINI_API_KEY',
230
+ openrouter: 'OPENROUTER_API_KEY'
231
+ };
232
+ const envVar = envMap[provider.toLowerCase()];
233
+ if (envVar && process.env[envVar]) {
234
+ return process.env[envVar];
235
+ }
236
+ return undefined;
237
+ }