@soleri/forge 4.2.2 → 5.0.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.
Files changed (78) hide show
  1. package/dist/domain-manager.d.ts +2 -2
  2. package/dist/domain-manager.js +35 -16
  3. package/dist/domain-manager.js.map +1 -1
  4. package/dist/index.js +0 -0
  5. package/dist/knowledge-installer.js +18 -12
  6. package/dist/knowledge-installer.js.map +1 -1
  7. package/dist/patching.d.ts +15 -6
  8. package/dist/patching.js +37 -12
  9. package/dist/patching.js.map +1 -1
  10. package/dist/scaffolder.js +18 -28
  11. package/dist/scaffolder.js.map +1 -1
  12. package/dist/templates/brain.d.ts +6 -0
  13. package/dist/templates/brain.js +478 -0
  14. package/dist/templates/brain.js.map +1 -0
  15. package/dist/templates/core-facade.js +95 -47
  16. package/dist/templates/core-facade.js.map +1 -1
  17. package/dist/templates/entry-point.d.ts +4 -0
  18. package/dist/templates/entry-point.js +146 -89
  19. package/dist/templates/entry-point.js.map +1 -1
  20. package/dist/templates/facade-factory.d.ts +1 -0
  21. package/dist/templates/facade-factory.js +63 -0
  22. package/dist/templates/facade-factory.js.map +1 -0
  23. package/dist/templates/facade-types.d.ts +1 -0
  24. package/dist/templates/facade-types.js +46 -0
  25. package/dist/templates/facade-types.js.map +1 -0
  26. package/dist/templates/intelligence-loader.d.ts +1 -0
  27. package/dist/templates/intelligence-loader.js +43 -0
  28. package/dist/templates/intelligence-loader.js.map +1 -0
  29. package/dist/templates/intelligence-types.d.ts +1 -0
  30. package/dist/templates/intelligence-types.js +24 -0
  31. package/dist/templates/intelligence-types.js.map +1 -0
  32. package/dist/templates/llm-key-pool.d.ts +7 -0
  33. package/dist/templates/llm-key-pool.js +211 -0
  34. package/dist/templates/llm-key-pool.js.map +1 -0
  35. package/dist/templates/llm-types.d.ts +5 -0
  36. package/dist/templates/llm-types.js +161 -0
  37. package/dist/templates/llm-types.js.map +1 -0
  38. package/dist/templates/llm-utils.d.ts +5 -0
  39. package/dist/templates/llm-utils.js +260 -0
  40. package/dist/templates/llm-utils.js.map +1 -0
  41. package/dist/templates/package-json.js +3 -1
  42. package/dist/templates/package-json.js.map +1 -1
  43. package/dist/templates/planner.d.ts +5 -0
  44. package/dist/templates/planner.js +150 -0
  45. package/dist/templates/planner.js.map +1 -0
  46. package/dist/templates/test-brain.d.ts +6 -0
  47. package/dist/templates/test-brain.js +474 -0
  48. package/dist/templates/test-brain.js.map +1 -0
  49. package/dist/templates/test-facades.d.ts +1 -1
  50. package/dist/templates/test-facades.js +182 -456
  51. package/dist/templates/test-facades.js.map +1 -1
  52. package/dist/templates/test-llm.d.ts +7 -0
  53. package/dist/templates/test-llm.js +574 -0
  54. package/dist/templates/test-llm.js.map +1 -0
  55. package/dist/templates/test-loader.d.ts +5 -0
  56. package/dist/templates/test-loader.js +146 -0
  57. package/dist/templates/test-loader.js.map +1 -0
  58. package/dist/templates/test-planner.d.ts +5 -0
  59. package/dist/templates/test-planner.js +271 -0
  60. package/dist/templates/test-planner.js.map +1 -0
  61. package/dist/templates/test-vault.d.ts +5 -0
  62. package/dist/templates/test-vault.js +380 -0
  63. package/dist/templates/test-vault.js.map +1 -0
  64. package/dist/templates/vault.d.ts +5 -0
  65. package/dist/templates/vault.js +263 -0
  66. package/dist/templates/vault.js.map +1 -0
  67. package/dist/types.d.ts +2 -2
  68. package/package.json +1 -1
  69. package/src/__tests__/scaffolder.test.ts +52 -109
  70. package/src/domain-manager.ts +34 -15
  71. package/src/knowledge-installer.ts +23 -15
  72. package/src/patching.ts +44 -12
  73. package/src/scaffolder.ts +18 -29
  74. package/src/templates/entry-point.ts +146 -91
  75. package/src/templates/package-json.ts +3 -1
  76. package/src/templates/test-facades.ts +182 -458
  77. package/src/templates/core-facade.ts +0 -517
  78. package/src/templates/llm-client.ts +0 -301
@@ -0,0 +1,43 @@
1
+ export function generateIntelligenceLoader() {
2
+ return `import { readFileSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import type { IntelligenceBundle, IntelligenceEntry } from './types.js';
5
+
6
+ export function loadIntelligenceData(dataDir: string): IntelligenceEntry[] {
7
+ const entries: IntelligenceEntry[] = [];
8
+ let files: string[];
9
+ try {
10
+ files = readdirSync(dataDir).filter((f) => f.endsWith('.json'));
11
+ } catch {
12
+ console.warn('Intelligence data directory not found: ' + dataDir);
13
+ return entries;
14
+ }
15
+
16
+ for (const file of files) {
17
+ try {
18
+ const raw = readFileSync(join(dataDir, file), 'utf-8');
19
+ const bundle = JSON.parse(raw) as IntelligenceBundle;
20
+ if (!bundle.entries || !Array.isArray(bundle.entries)) continue;
21
+ for (const entry of bundle.entries) {
22
+ if (validateEntry(entry)) entries.push(entry);
23
+ }
24
+ } catch (err) {
25
+ console.warn('Failed to load ' + file + ': ' + (err instanceof Error ? err.message : err));
26
+ }
27
+ }
28
+ return entries;
29
+ }
30
+
31
+ function validateEntry(entry: IntelligenceEntry): boolean {
32
+ return (
33
+ typeof entry.id === 'string' && entry.id.length > 0 &&
34
+ ['pattern', 'anti-pattern', 'rule'].includes(entry.type) &&
35
+ typeof entry.title === 'string' && entry.title.length > 0 &&
36
+ typeof entry.description === 'string' && entry.description.length > 0 &&
37
+ ['critical', 'warning', 'suggestion'].includes(entry.severity) &&
38
+ Array.isArray(entry.tags)
39
+ );
40
+ }
41
+ `;
42
+ }
43
+ //# sourceMappingURL=intelligence-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intelligence-loader.js","sourceRoot":"","sources":["../../src/templates/intelligence-loader.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,0BAA0B;IACxC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCR,CAAC;AACF,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function generateIntelligenceTypes(): string;
@@ -0,0 +1,24 @@
1
+ export function generateIntelligenceTypes() {
2
+ return `export interface IntelligenceEntry {
3
+ id: string;
4
+ type: 'pattern' | 'anti-pattern' | 'rule';
5
+ domain: string;
6
+ title: string;
7
+ severity: 'critical' | 'warning' | 'suggestion';
8
+ description: string;
9
+ context?: string;
10
+ example?: string;
11
+ counterExample?: string;
12
+ why?: string;
13
+ tags: string[];
14
+ appliesTo?: string[];
15
+ }
16
+
17
+ export interface IntelligenceBundle {
18
+ domain: string;
19
+ version: string;
20
+ entries: IntelligenceEntry[];
21
+ }
22
+ `;
23
+ }
24
+ //# sourceMappingURL=intelligence-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intelligence-types.js","sourceRoot":"","sources":["../../src/templates/intelligence-types.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,yBAAyB;IACvC,OAAO;;;;;;;;;;;;;;;;;;;;CAoBR,CAAC;AACF,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { AgentConfig } from '../types.js';
2
+ /**
3
+ * Generate the LLM key pool file for a new agent.
4
+ * KeyPool manages multiple API keys with per-key circuit breakers.
5
+ * Uses config.id to resolve ~/.{agentId}/keys.json.
6
+ */
7
+ export declare function generateLLMKeyPool(config: AgentConfig): string;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Generate the LLM key pool file for a new agent.
3
+ * KeyPool manages multiple API keys with per-key circuit breakers.
4
+ * Uses config.id to resolve ~/.{agentId}/keys.json.
5
+ */
6
+ export function generateLLMKeyPool(config) {
7
+ return `/**
8
+ * Key Pool — Multi-key management with auto-rotation
9
+ * Generated by Soleri — do not edit manually.
10
+ *
11
+ * Key loading priority:
12
+ * 1. ~/.${config.id}/keys.json → { "openai": ["sk-..."], "anthropic": ["sk-ant-..."] }
13
+ * 2. Fallback: OPENAI_API_KEY / ANTHROPIC_API_KEY env vars
14
+ * 3. Empty pool → graceful degradation
15
+ */
16
+
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+ import { homedir } from 'node:os';
20
+ import { SecretString } from './types.js';
21
+ import type { KeyPoolConfig, KeyStatus } from './types.js';
22
+ import { CircuitBreaker } from './utils.js';
23
+
24
+ // =============================================================================
25
+ // CONSTANTS
26
+ // =============================================================================
27
+
28
+ const DEFAULT_PREEMPTIVE_THRESHOLD = 50;
29
+
30
+ // =============================================================================
31
+ // KEY POOL
32
+ // =============================================================================
33
+
34
+ class RateLimitRotationError extends Error {
35
+ retryable = true;
36
+ constructor() {
37
+ super('Rate limit rotation');
38
+ this.name = 'RateLimitRotationError';
39
+ }
40
+ }
41
+
42
+ export class KeyPool {
43
+ private keys: SecretString[];
44
+ private activeIndex: number = 0;
45
+ private keyBreakers: Map<number, CircuitBreaker> = new Map();
46
+ private remainingQuota: Map<number, number> = new Map();
47
+ private readonly preemptiveThreshold: number;
48
+
49
+ constructor(config: KeyPoolConfig) {
50
+ this.keys = config.keys
51
+ .filter((k) => k.length > 0)
52
+ .map((k) => new SecretString(k));
53
+ this.preemptiveThreshold =
54
+ config.preemptiveThreshold ?? DEFAULT_PREEMPTIVE_THRESHOLD;
55
+
56
+ for (let i = 0; i < this.keys.length; i++) {
57
+ this.keyBreakers.set(
58
+ i,
59
+ new CircuitBreaker({
60
+ name: \`key-pool-\${i}\`,
61
+ failureThreshold: 3,
62
+ resetTimeoutMs: 30_000,
63
+ }),
64
+ );
65
+ }
66
+
67
+ if (this.keys.length > 1) {
68
+ console.error(\`[LLM] KeyPool initialized with \${this.keys.length} keys\`);
69
+ }
70
+ }
71
+
72
+ get hasKeys(): boolean {
73
+ return this.keys.length > 0;
74
+ }
75
+
76
+ getActiveKey(): SecretString {
77
+ return this.keys[this.activeIndex];
78
+ }
79
+
80
+ get activeKeyIndex(): number {
81
+ return this.activeIndex;
82
+ }
83
+
84
+ get poolSize(): number {
85
+ return this.keys.length;
86
+ }
87
+
88
+ get exhausted(): boolean {
89
+ if (this.keys.length === 0) return true;
90
+ for (let i = 0; i < this.keys.length; i++) {
91
+ const breaker = this.keyBreakers.get(i)!;
92
+ if (!breaker.isOpen()) return false;
93
+ }
94
+ return true;
95
+ }
96
+
97
+ rotateOnError(): SecretString | null {
98
+ const breaker = this.keyBreakers.get(this.activeIndex);
99
+ if (breaker) {
100
+ breaker.recordFailure();
101
+ }
102
+ return this.findNextHealthyKey();
103
+ }
104
+
105
+ rotatePreemptive(): boolean {
106
+ const previousIndex = this.activeIndex;
107
+ const remaining = this.remainingQuota.get(previousIndex);
108
+ if (remaining !== undefined && remaining < this.preemptiveThreshold) {
109
+ const next = this.findNextHealthyKey();
110
+ if (next) {
111
+ console.error(
112
+ \`[LLM] KeyPool preemptive rotation: key \${previousIndex} remaining=\${remaining} < threshold=\${this.preemptiveThreshold}\`,
113
+ );
114
+ return true;
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+
120
+ updateQuota(keyIndex: number, remaining: number): void {
121
+ this.remainingQuota.set(keyIndex, remaining);
122
+ }
123
+
124
+ getStatus(): {
125
+ poolSize: number;
126
+ activeKeyIndex: number;
127
+ exhausted: boolean;
128
+ perKeyStatus: KeyStatus[];
129
+ } {
130
+ const perKeyStatus: KeyStatus[] = [];
131
+ for (let i = 0; i < this.keys.length; i++) {
132
+ perKeyStatus.push({
133
+ index: i,
134
+ circuitState: this.keyBreakers.get(i)!.getState(),
135
+ remainingQuota: this.remainingQuota.get(i) ?? null,
136
+ });
137
+ }
138
+ return {
139
+ poolSize: this.poolSize,
140
+ activeKeyIndex: this.activeIndex,
141
+ exhausted: this.exhausted,
142
+ perKeyStatus,
143
+ };
144
+ }
145
+
146
+ private findNextHealthyKey(): SecretString | null {
147
+ const startIndex = this.activeIndex;
148
+ for (let offset = 1; offset <= this.keys.length; offset++) {
149
+ const candidateIndex = (startIndex + offset) % this.keys.length;
150
+ const breaker = this.keyBreakers.get(candidateIndex)!;
151
+ if (!breaker.isOpen()) {
152
+ this.activeIndex = candidateIndex;
153
+ return this.keys[candidateIndex];
154
+ }
155
+ }
156
+ console.error('[LLM] KeyPool: all keys exhausted — no healthy key available');
157
+ return null;
158
+ }
159
+ }
160
+
161
+ // =============================================================================
162
+ // KEY LOADING
163
+ // =============================================================================
164
+
165
+ export interface KeyPoolFiles {
166
+ openai: KeyPoolConfig;
167
+ anthropic: KeyPoolConfig;
168
+ }
169
+
170
+ export function loadKeyPoolConfig(): KeyPoolFiles {
171
+ const keysFilePath = path.join(
172
+ homedir(),
173
+ '.${config.id}',
174
+ 'keys.json',
175
+ );
176
+
177
+ let openaiKeys: string[] = [];
178
+ let anthropicKeys: string[] = [];
179
+
180
+ try {
181
+ if (fs.existsSync(keysFilePath)) {
182
+ const data = JSON.parse(fs.readFileSync(keysFilePath, 'utf-8'));
183
+ if (data?.openai && Array.isArray(data.openai)) {
184
+ openaiKeys = data.openai;
185
+ }
186
+ if (data?.anthropic && Array.isArray(data.anthropic)) {
187
+ anthropicKeys = data.anthropic;
188
+ }
189
+ }
190
+ } catch {
191
+ console.error('[LLM] Could not read keys.json, falling back to env vars');
192
+ }
193
+
194
+ // Fallback: environment variables
195
+ if (openaiKeys.length === 0) {
196
+ const envKey = process.env.OPENAI_API_KEY || '';
197
+ if (envKey) openaiKeys = [envKey];
198
+ }
199
+ if (anthropicKeys.length === 0) {
200
+ const envKey = process.env.ANTHROPIC_API_KEY || '';
201
+ if (envKey) anthropicKeys = [envKey];
202
+ }
203
+
204
+ return {
205
+ openai: { keys: openaiKeys },
206
+ anthropic: { keys: anthropicKeys },
207
+ };
208
+ }
209
+ `;
210
+ }
211
+ //# sourceMappingURL=llm-key-pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-key-pool.js","sourceRoot":"","sources":["../../src/templates/llm-key-pool.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAmB;IACpD,OAAO;;;;;WAKE,MAAM,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAiKZ,MAAM,CAAC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoChB,CAAC;AACF,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generate the LLM types file for a new agent.
3
+ * Contains all types, LLMError class, and SecretString class.
4
+ */
5
+ export declare function generateLLMTypes(): string;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Generate the LLM types file for a new agent.
3
+ * Contains all types, LLMError class, and SecretString class.
4
+ */
5
+ export function generateLLMTypes() {
6
+ return `/**
7
+ * LLM Module Types
8
+ * Generated by Soleri — do not edit manually.
9
+ */
10
+
11
+ // =============================================================================
12
+ // SECRET STRING
13
+ // =============================================================================
14
+
15
+ const REDACTED = '[REDACTED]';
16
+
17
+ export class SecretString {
18
+ #value: string;
19
+
20
+ constructor(value: string) {
21
+ this.#value = value;
22
+ }
23
+
24
+ expose(): string {
25
+ return this.#value;
26
+ }
27
+
28
+ get isSet(): boolean {
29
+ return this.#value.length > 0;
30
+ }
31
+
32
+ toString(): string {
33
+ return REDACTED;
34
+ }
35
+ toJSON(): string {
36
+ return REDACTED;
37
+ }
38
+ [Symbol.toPrimitive](): string {
39
+ return REDACTED;
40
+ }
41
+ [Symbol.for('nodejs.util.inspect.custom')](): string {
42
+ return REDACTED;
43
+ }
44
+ }
45
+
46
+ // =============================================================================
47
+ // LLM ERROR
48
+ // =============================================================================
49
+
50
+ export class LLMError extends Error {
51
+ retryable: boolean;
52
+ statusCode?: number;
53
+
54
+ constructor(message: string, options?: { retryable?: boolean; statusCode?: number }) {
55
+ super(message);
56
+ this.name = 'LLMError';
57
+ this.retryable = options?.retryable ?? false;
58
+ this.statusCode = options?.statusCode;
59
+ Object.setPrototypeOf(this, LLMError.prototype);
60
+ }
61
+ }
62
+
63
+ // =============================================================================
64
+ // LLM CALL TYPES
65
+ // =============================================================================
66
+
67
+ export interface LLMCallOptions {
68
+ provider: 'openai' | 'anthropic';
69
+ model: string;
70
+ systemPrompt: string;
71
+ userPrompt: string;
72
+ temperature?: number;
73
+ maxTokens?: number;
74
+ caller: string;
75
+ task?: string;
76
+ }
77
+
78
+ export interface LLMCallResult {
79
+ text: string;
80
+ model: string;
81
+ provider: 'openai' | 'anthropic';
82
+ inputTokens?: number;
83
+ outputTokens?: number;
84
+ durationMs: number;
85
+ }
86
+
87
+ // =============================================================================
88
+ // CIRCUIT BREAKER TYPES
89
+ // =============================================================================
90
+
91
+ export type CircuitState = 'closed' | 'open' | 'half-open';
92
+
93
+ export interface CircuitBreakerConfig {
94
+ failureThreshold: number;
95
+ resetTimeoutMs: number;
96
+ name: string;
97
+ }
98
+
99
+ export interface CircuitBreakerSnapshot {
100
+ state: CircuitState;
101
+ failureCount: number;
102
+ lastFailureAt: number | null;
103
+ }
104
+
105
+ // =============================================================================
106
+ // KEY POOL TYPES
107
+ // =============================================================================
108
+
109
+ export interface KeyPoolConfig {
110
+ keys: string[];
111
+ preemptiveThreshold?: number;
112
+ }
113
+
114
+ export interface KeyStatus {
115
+ index: number;
116
+ circuitState: CircuitBreakerSnapshot;
117
+ remainingQuota: number | null;
118
+ }
119
+
120
+ // =============================================================================
121
+ // ROUTING TYPES
122
+ // =============================================================================
123
+
124
+ export interface RouteEntry {
125
+ caller: string;
126
+ task?: string;
127
+ model: string;
128
+ provider: 'openai' | 'anthropic';
129
+ }
130
+
131
+ export interface RoutingConfig {
132
+ routes: RouteEntry[];
133
+ defaultOpenAIModel: string;
134
+ defaultAnthropicModel: string;
135
+ }
136
+
137
+ // =============================================================================
138
+ // RATE LIMIT TYPES
139
+ // =============================================================================
140
+
141
+ export interface RateLimitInfo {
142
+ remaining: number | null;
143
+ resetMs: number | null;
144
+ retryAfterMs: number | null;
145
+ }
146
+
147
+ // =============================================================================
148
+ // RETRY TYPES
149
+ // =============================================================================
150
+
151
+ export interface RetryConfig {
152
+ maxAttempts: number;
153
+ baseDelayMs: number;
154
+ maxDelayMs: number;
155
+ jitter: number;
156
+ shouldRetry?: (error: unknown) => boolean;
157
+ onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
158
+ }
159
+ `;
160
+ }
161
+ //# sourceMappingURL=llm-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-types.js","sourceRoot":"","sources":["../../src/templates/llm-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyJR,CAAC;AACF,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generate the LLM utils file for a new agent.
3
+ * Contains CircuitBreaker, retry with backoff+jitter, and rate limit header parser.
4
+ */
5
+ export declare function generateLLMUtils(): string;