thatgfsj-code 0.2.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 (179) hide show
  1. package/.patches/0001-fix-repl-replace-readline-with-inquirer-input-for-ke.patch +279 -0
  2. package/.patches/0002-fix-repl-stream-AI-output-directly-to-stdout-so-term.patch +564 -0
  3. package/.patches/0003-fix-session-break-the-hallucination-loop-in-assistan.patch +194 -0
  4. package/.patches/0004-chore-release-bump-version-to-0.2.1.patch +24 -0
  5. package/LICENSE +21 -0
  6. package/README.md +100 -0
  7. package/ROADMAP.md +107 -0
  8. package/dist/agent/core.d.ts +56 -0
  9. package/dist/agent/core.d.ts.map +1 -0
  10. package/dist/agent/core.js +142 -0
  11. package/dist/agent/core.js.map +1 -0
  12. package/dist/agent/index.d.ts +8 -0
  13. package/dist/agent/index.d.ts.map +1 -0
  14. package/dist/agent/index.js +8 -0
  15. package/dist/agent/index.js.map +1 -0
  16. package/dist/agent/intent.d.ts +36 -0
  17. package/dist/agent/intent.d.ts.map +1 -0
  18. package/dist/agent/intent.js +146 -0
  19. package/dist/agent/intent.js.map +1 -0
  20. package/dist/agent/streaming.d.ts +50 -0
  21. package/dist/agent/streaming.d.ts.map +1 -0
  22. package/dist/agent/streaming.js +110 -0
  23. package/dist/agent/streaming.js.map +1 -0
  24. package/dist/core/ai-engine.d.ts +59 -0
  25. package/dist/core/ai-engine.d.ts.map +1 -0
  26. package/dist/core/ai-engine.js +378 -0
  27. package/dist/core/ai-engine.js.map +1 -0
  28. package/dist/core/cli.d.ts +21 -0
  29. package/dist/core/cli.d.ts.map +1 -0
  30. package/dist/core/cli.js +143 -0
  31. package/dist/core/cli.js.map +1 -0
  32. package/dist/core/config.d.ts +38 -0
  33. package/dist/core/config.d.ts.map +1 -0
  34. package/dist/core/config.js +129 -0
  35. package/dist/core/config.js.map +1 -0
  36. package/dist/core/context-compactor.d.ts +54 -0
  37. package/dist/core/context-compactor.d.ts.map +1 -0
  38. package/dist/core/context-compactor.js +197 -0
  39. package/dist/core/context-compactor.js.map +1 -0
  40. package/dist/core/hooks.d.ts +75 -0
  41. package/dist/core/hooks.d.ts.map +1 -0
  42. package/dist/core/hooks.js +146 -0
  43. package/dist/core/hooks.js.map +1 -0
  44. package/dist/core/permissions.d.ts +52 -0
  45. package/dist/core/permissions.d.ts.map +1 -0
  46. package/dist/core/permissions.js +237 -0
  47. package/dist/core/permissions.js.map +1 -0
  48. package/dist/core/session.d.ts +54 -0
  49. package/dist/core/session.d.ts.map +1 -0
  50. package/dist/core/session.js +147 -0
  51. package/dist/core/session.js.map +1 -0
  52. package/dist/core/skills.d.ts +60 -0
  53. package/dist/core/skills.d.ts.map +1 -0
  54. package/dist/core/skills.js +175 -0
  55. package/dist/core/skills.js.map +1 -0
  56. package/dist/core/state.d.ts +60 -0
  57. package/dist/core/state.d.ts.map +1 -0
  58. package/dist/core/state.js +106 -0
  59. package/dist/core/state.js.map +1 -0
  60. package/dist/core/subagent.d.ts +68 -0
  61. package/dist/core/subagent.d.ts.map +1 -0
  62. package/dist/core/subagent.js +142 -0
  63. package/dist/core/subagent.js.map +1 -0
  64. package/dist/core/system-prompt.d.ts +40 -0
  65. package/dist/core/system-prompt.d.ts.map +1 -0
  66. package/dist/core/system-prompt.js +138 -0
  67. package/dist/core/system-prompt.js.map +1 -0
  68. package/dist/core/tool-registry.d.ts +53 -0
  69. package/dist/core/tool-registry.d.ts.map +1 -0
  70. package/dist/core/tool-registry.js +138 -0
  71. package/dist/core/tool-registry.js.map +1 -0
  72. package/dist/core/types.d.ts +206 -0
  73. package/dist/core/types.d.ts.map +1 -0
  74. package/dist/core/types.js +127 -0
  75. package/dist/core/types.js.map +1 -0
  76. package/dist/index.d.ts +7 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +481 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/mcp/client.d.ts +83 -0
  81. package/dist/mcp/client.d.ts.map +1 -0
  82. package/dist/mcp/client.js +267 -0
  83. package/dist/mcp/client.js.map +1 -0
  84. package/dist/repl/index.d.ts +8 -0
  85. package/dist/repl/index.d.ts.map +1 -0
  86. package/dist/repl/index.js +8 -0
  87. package/dist/repl/index.js.map +1 -0
  88. package/dist/repl/input.d.ts +61 -0
  89. package/dist/repl/input.d.ts.map +1 -0
  90. package/dist/repl/input.js +127 -0
  91. package/dist/repl/input.js.map +1 -0
  92. package/dist/repl/loop.d.ts +53 -0
  93. package/dist/repl/loop.d.ts.map +1 -0
  94. package/dist/repl/loop.js +241 -0
  95. package/dist/repl/loop.js.map +1 -0
  96. package/dist/repl/output.d.ts +105 -0
  97. package/dist/repl/output.d.ts.map +1 -0
  98. package/dist/repl/output.js +197 -0
  99. package/dist/repl/output.js.map +1 -0
  100. package/dist/repl/welcome.d.ts +68 -0
  101. package/dist/repl/welcome.d.ts.map +1 -0
  102. package/dist/repl/welcome.js +249 -0
  103. package/dist/repl/welcome.js.map +1 -0
  104. package/dist/tools/file.d.ts +46 -0
  105. package/dist/tools/file.d.ts.map +1 -0
  106. package/dist/tools/file.js +100 -0
  107. package/dist/tools/file.js.map +1 -0
  108. package/dist/tools/git.d.ts +52 -0
  109. package/dist/tools/git.d.ts.map +1 -0
  110. package/dist/tools/git.js +112 -0
  111. package/dist/tools/git.js.map +1 -0
  112. package/dist/tools/index.d.ts +22 -0
  113. package/dist/tools/index.d.ts.map +1 -0
  114. package/dist/tools/index.js +43 -0
  115. package/dist/tools/index.js.map +1 -0
  116. package/dist/tools/search.d.ts +36 -0
  117. package/dist/tools/search.d.ts.map +1 -0
  118. package/dist/tools/search.js +223 -0
  119. package/dist/tools/search.js.map +1 -0
  120. package/dist/tools/shell.d.ts +57 -0
  121. package/dist/tools/shell.d.ts.map +1 -0
  122. package/dist/tools/shell.js +153 -0
  123. package/dist/tools/shell.js.map +1 -0
  124. package/dist/utils/diff-preview.d.ts +42 -0
  125. package/dist/utils/diff-preview.d.ts.map +1 -0
  126. package/dist/utils/diff-preview.js +174 -0
  127. package/dist/utils/diff-preview.js.map +1 -0
  128. package/dist/utils/index.d.ts +8 -0
  129. package/dist/utils/index.d.ts.map +1 -0
  130. package/dist/utils/index.js +8 -0
  131. package/dist/utils/index.js.map +1 -0
  132. package/dist/utils/memory.d.ts +81 -0
  133. package/dist/utils/memory.d.ts.map +1 -0
  134. package/dist/utils/memory.js +186 -0
  135. package/dist/utils/memory.js.map +1 -0
  136. package/dist/utils/project-context.d.ts +62 -0
  137. package/dist/utils/project-context.d.ts.map +1 -0
  138. package/dist/utils/project-context.js +197 -0
  139. package/dist/utils/project-context.js.map +1 -0
  140. package/docs/API_KEY_GUIDE.md +236 -0
  141. package/docs/FAQ.md +182 -0
  142. package/install.bat +63 -0
  143. package/install.ps1 +238 -0
  144. package/install.sh +113 -0
  145. package/package.json +36 -0
  146. package/src/agent/core.ts +179 -0
  147. package/src/agent/index.ts +8 -0
  148. package/src/agent/intent.ts +181 -0
  149. package/src/agent/streaming.ts +132 -0
  150. package/src/core/ai-engine.ts +437 -0
  151. package/src/core/cli.ts +171 -0
  152. package/src/core/config.ts +147 -0
  153. package/src/core/context-compactor.ts +245 -0
  154. package/src/core/hooks.ts +196 -0
  155. package/src/core/permissions.ts +308 -0
  156. package/src/core/session.ts +165 -0
  157. package/src/core/skills.ts +208 -0
  158. package/src/core/state.ts +120 -0
  159. package/src/core/subagent.ts +195 -0
  160. package/src/core/system-prompt.ts +163 -0
  161. package/src/core/tool-registry.ts +157 -0
  162. package/src/core/types.ts +280 -0
  163. package/src/index.ts +544 -0
  164. package/src/mcp/client.ts +330 -0
  165. package/src/repl/index.ts +8 -0
  166. package/src/repl/input.ts +139 -0
  167. package/src/repl/loop.ts +280 -0
  168. package/src/repl/output.ts +222 -0
  169. package/src/repl/welcome.ts +296 -0
  170. package/src/tools/file.ts +117 -0
  171. package/src/tools/git.ts +132 -0
  172. package/src/tools/index.ts +48 -0
  173. package/src/tools/search.ts +263 -0
  174. package/src/tools/shell.ts +181 -0
  175. package/src/utils/diff-preview.ts +202 -0
  176. package/src/utils/index.ts +8 -0
  177. package/src/utils/memory.ts +223 -0
  178. package/src/utils/project-context.ts +207 -0
  179. package/tsconfig.json +19 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * S11: State & Session Management
3
+ *
4
+ * Three layers:
5
+ * 1. EphemeralState - in-memory, current session only
6
+ * 2. PersistentState - survives restarts (file-based)
7
+ * 3. SessionState - per-conversation state
8
+ */
9
+
10
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
11
+ import { join } from 'path';
12
+
13
+ /**
14
+ * S11: Ephemeral State — in-memory key-value store
15
+ */
16
+ export class EphemeralState<T = any> {
17
+ private data: Map<string, T> = new Map();
18
+
19
+ get(key: string): T | undefined { return this.data.get(key); }
20
+ set(key: string, value: T): void { this.data.set(key, value); }
21
+ has(key: string): boolean { return this.data.has(key); }
22
+ delete(key: string): void { this.data.delete(key); }
23
+ clear(): void { this.data.clear(); }
24
+ keys(): string[] { return [...this.data.keys()]; }
25
+ entries(): [string, T][] { return [...this.data.entries()]; }
26
+ }
27
+
28
+ /**
29
+ * S11: Persistent State — file-backed key-value store
30
+ */
31
+ export class PersistentState<T = any> {
32
+ private filePath: string;
33
+ private data: Record<string, T> = {};
34
+
35
+ constructor(filePath: string) {
36
+ this.filePath = filePath;
37
+ this.load();
38
+ }
39
+
40
+ private load(): void {
41
+ try {
42
+ if (existsSync(this.filePath)) {
43
+ this.data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
44
+ }
45
+ } catch {
46
+ this.data = {};
47
+ }
48
+ }
49
+
50
+ private save(): void {
51
+ try {
52
+ writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), 'utf-8');
53
+ } catch (error: any) {
54
+ console.error('[State] Failed to save:', error.message);
55
+ }
56
+ }
57
+
58
+ get(key: string): T | undefined { return this.data[key]; }
59
+ set(key: string, value: T): void { this.data[key] = value; this.save(); }
60
+ has(key: string): boolean { return key in this.data; }
61
+ delete(key: string): void { delete this.data[key]; this.save(); }
62
+ clear(): void { this.data = {}; this.save(); }
63
+ keys(): string[] { return Object.keys(this.data); }
64
+ }
65
+
66
+ /**
67
+ * S11: Session State — per-conversation state
68
+ */
69
+ export class SessionState<T = any> {
70
+ private sessions: Map<string, EphemeralState<T>> = new Map();
71
+ private currentSessionId: string | null = null;
72
+
73
+ createSession(sessionId: string): void {
74
+ this.sessions.set(sessionId, new EphemeralState<T>());
75
+ }
76
+
77
+ deleteSession(sessionId: string): void {
78
+ this.sessions.delete(sessionId);
79
+ }
80
+
81
+ setCurrentSession(sessionId: string): void {
82
+ if (!this.sessions.has(sessionId)) {
83
+ this.createSession(sessionId);
84
+ }
85
+ this.currentSessionId = sessionId;
86
+ }
87
+
88
+ getCurrentSession(): EphemeralState<T> | null {
89
+ if (!this.currentSessionId) return null;
90
+ return this.sessions.get(this.currentSessionId) || null;
91
+ }
92
+
93
+ getSession(sessionId: string): EphemeralState<T> | null {
94
+ return this.sessions.get(sessionId) || null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * S11: Global State Manager
100
+ */
101
+ export class StateManager {
102
+ ephemeral: EphemeralState;
103
+ persistent: PersistentState;
104
+ session: SessionState;
105
+
106
+ constructor(dataDir: string = join(process.cwd(), '.thatgfsj', 'state')) {
107
+ this.ephemeral = new EphemeralState();
108
+ this.persistent = new PersistentState(join(dataDir, 'persistent.json'));
109
+ this.session = new SessionState();
110
+ }
111
+ }
112
+
113
+ let globalStateManager: StateManager | null = null;
114
+
115
+ export function getStateManager(): StateManager {
116
+ if (!globalStateManager) {
117
+ globalStateManager = new StateManager();
118
+ }
119
+ return globalStateManager;
120
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * S06: Subagent Manager
3
+ * Spawn and manage subagents for parallel task execution
4
+ *
5
+ * Subagents enable "infinite parallelism" — complex tasks are broken into
6
+ * independent subtasks, each run by a separate agent instance.
7
+ */
8
+
9
+ import { AIEngine, ChatMessage } from './ai-engine.js';
10
+ import { AIConfig } from './types.js';
11
+
12
+ export interface SubagentOptions {
13
+ /** Task prompt for the subagent */
14
+ prompt: string;
15
+ /** Custom system prompt (optional) */
16
+ systemPrompt?: string;
17
+ /** Tools available to this subagent */
18
+ tools?: any[];
19
+ /** Max iterations for this subagent */
20
+ maxIterations?: number;
21
+ /** Subagent name for logging */
22
+ name?: string;
23
+ /** Working directory */
24
+ cwd?: string;
25
+ }
26
+
27
+ export interface SubagentResult {
28
+ success: boolean;
29
+ output: string;
30
+ error?: string;
31
+ iterations: number;
32
+ durationMs: number;
33
+ name: string;
34
+ }
35
+
36
+ /**
37
+ * S06: Subagent Manager
38
+ */
39
+ export class SubagentManager {
40
+ private config: AIConfig;
41
+ private activeSubagents: Map<string, {
42
+ task: Promise<SubagentResult>;
43
+ startedAt: Date;
44
+ name: string;
45
+ }> = new Map();
46
+ private maxConcurrent = 3;
47
+
48
+ constructor(config: AIConfig) {
49
+ this.config = config;
50
+ }
51
+
52
+ /**
53
+ * S06: Spawn a subagent to run a task asynchronously
54
+ */
55
+ async spawn(options: SubagentOptions): Promise<SubagentResult> {
56
+ const name = options.name || `subagent_${Date.now()}`;
57
+ const startedAt = new Date();
58
+
59
+ const task = this.runSubagent(name, options, startedAt);
60
+
61
+ // Track active subagent
62
+ this.activeSubagents.set(name, { task, startedAt, name });
63
+
64
+ // Auto-cleanup after completion
65
+ task.then(() => {
66
+ setTimeout(() => this.activeSubagents.delete(name), 5000);
67
+ }).catch(() => {
68
+ this.activeSubagents.delete(name);
69
+ });
70
+
71
+ // Return handle — caller can await or ignore
72
+ return task;
73
+ }
74
+
75
+ /**
76
+ * S06: Spawn multiple subagents in parallel (bounded concurrency)
77
+ */
78
+ async spawnAll(tasks: SubagentOptions[]): Promise<SubagentResult[]> {
79
+ const results: SubagentResult[] = [];
80
+ const queue = [...tasks];
81
+ const running: Promise<void>[] = [];
82
+
83
+ while (queue.length > 0 || running.length > 0) {
84
+ while (running.length < this.maxConcurrent && queue.length > 0) {
85
+ const task = queue.shift()!;
86
+ const p: Promise<void> = this.spawn(task).then(r => { results.push(r); }).then(() => {});
87
+ running.push(p);
88
+ }
89
+
90
+ if (running.length > 0) {
91
+ await Promise.race(running).catch(() => {});
92
+ await Promise.allSettled(running);
93
+ running.length = 0;
94
+ }
95
+ }
96
+
97
+ return results;
98
+ }
99
+
100
+ /**
101
+ * S06: Run a single subagent task
102
+ */
103
+ private async runSubagent(
104
+ name: string,
105
+ options: SubagentOptions,
106
+ startedAt: Date
107
+ ): Promise<SubagentResult> {
108
+ const startMs = Date.now();
109
+ let iterations = 0;
110
+ const maxIterations = options.maxIterations || 5;
111
+
112
+ try {
113
+ // Create isolated AI engine for subagent
114
+ const subAI = new AIEngine({
115
+ ...this.config,
116
+ model: options.systemPrompt ? (this.config.model || 'unknown') : this.config.model
117
+ });
118
+
119
+ // Register tools if provided
120
+ if (options.tools) {
121
+ for (const tool of options.tools) {
122
+ subAI.registerTool(tool);
123
+ }
124
+ }
125
+
126
+ // Build messages
127
+ const messages: ChatMessage[] = [];
128
+ if (options.systemPrompt) {
129
+ messages.push({ role: 'system', content: options.systemPrompt });
130
+ } else {
131
+ messages.push({
132
+ role: 'system',
133
+ content: 'You are ' + (options.name || 'a subagent') + '. Complete the task assigned by the parent agent. Be concise and focused.'
134
+ });
135
+ }
136
+ messages.push({ role: 'user', content: options.prompt });
137
+
138
+ // Run agent loop
139
+ let output = '';
140
+ for await (const chunk of subAI.chatStream(messages, maxIterations)) {
141
+ output += chunk;
142
+ iterations++;
143
+ }
144
+
145
+ return {
146
+ success: true,
147
+ output,
148
+ iterations,
149
+ durationMs: Date.now() - startMs,
150
+ name
151
+ };
152
+
153
+ } catch (error: any) {
154
+ return {
155
+ success: false,
156
+ output: '',
157
+ error: error.message,
158
+ iterations,
159
+ durationMs: Date.now() - startMs,
160
+ name
161
+ };
162
+ }
163
+ }
164
+
165
+ /**
166
+ * S06: Get number of active subagents
167
+ */
168
+ getActiveCount(): number {
169
+ return this.activeSubagents.size;
170
+ }
171
+
172
+ /**
173
+ * S06: Get active subagent names
174
+ */
175
+ getActiveNames(): string[] {
176
+ return [...this.activeSubagents.values()].map(s => s.name);
177
+ }
178
+
179
+ /**
180
+ * S06: Abort all active subagents
181
+ */
182
+ abortAll(): void {
183
+ for (const [name] of this.activeSubagents) {
184
+ // Subagents don't have abort support yet, just remove from tracking
185
+ this.activeSubagents.delete(name);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * S06: Set max concurrent subagents
191
+ */
192
+ setMaxConcurrent(max: number) {
193
+ this.maxConcurrent = max;
194
+ }
195
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * S04: System Prompt Builder
3
+ * Dynamic system prompt construction - not a static string, but a program
4
+ * that assembles context from multiple sources each turn
5
+ */
6
+
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { Tool } from './types.js';
10
+
11
+ export interface SystemPromptConfig {
12
+ cwd?: string;
13
+ tools?: Tool[];
14
+ includeClaudeMd?: boolean;
15
+ projectInstructions?: string;
16
+ permissionMode?: 'accept' | 'deny' | 'ask';
17
+ date?: Date;
18
+ model?: string;
19
+ }
20
+
21
+ /**
22
+ * S04: System Prompt Builder - assembles prompt from multiple fragments
23
+ */
24
+ export class SystemPromptBuilder {
25
+ private config: SystemPromptConfig;
26
+
27
+ constructor(config: SystemPromptConfig = {}) {
28
+ this.config = {
29
+ cwd: process.cwd(),
30
+ tools: [],
31
+ includeClaudeMd: true,
32
+ permissionMode: 'ask',
33
+ date: new Date(),
34
+ model: 'unknown',
35
+ ...config
36
+ };
37
+ }
38
+
39
+ build(): string {
40
+ const fragments: string[] = [];
41
+ fragments.push(this.buildIdentity());
42
+ fragments.push(this.buildToolInstructions());
43
+ fragments.push(this.buildEnvironment());
44
+ fragments.push(this.buildPermissionMode());
45
+ fragments.push(this.buildProjectInstructions());
46
+ fragments.push(this.buildDateInfo());
47
+ fragments.push(this.buildOutputFormat());
48
+ return fragments.filter(Boolean).join('\n\n');
49
+ }
50
+
51
+ private buildIdentity(): string {
52
+ return [
53
+ 'You are Thatgfsj Code, an AI coding assistant.',
54
+ '',
55
+ 'You have access to tools (file operations, shell commands, git, search) that let you',
56
+ 'read, write, and modify files and run commands.',
57
+ '',
58
+ 'Be concise, helpful, and show your reasoning when working through problems.',
59
+ '',
60
+ 'When using tools:',
61
+ '- Prefer small, targeted operations over large batch changes',
62
+ '- Confirm destructive operations (rm, git push --force, etc.)',
63
+ '- Explain what you are going to do before doing it',
64
+ '- If something is not clear, ask clarifying questions'
65
+ ].join('\n');
66
+ }
67
+
68
+ private buildToolInstructions(): string {
69
+ const tools = this.config.tools || [];
70
+ if (tools.length === 0) {
71
+ return '## Tools\n\nNo tools are currently registered.';
72
+ }
73
+
74
+ const toolDescs = tools.map(t => {
75
+ const params = t.parameters
76
+ .map(p => ' - ' + p.name + ' (' + p.type + (p.required ? ', required' : '') + '): ' + p.description)
77
+ .join('\n');
78
+ return '### ' + t.name + '\n' + t.description + '\n\nParameters:\n' + params;
79
+ }).join('\n\n');
80
+
81
+ return '## Tools\n\nYou have access to the following tools:\n\n' + toolDescs + '\n\nTo use a tool, respond with a tool call. Each tool call must include:\n- The tool name\n- Parameters as a JSON object';
82
+ }
83
+
84
+ private buildEnvironment(): string {
85
+ const cwd = this.config.cwd || process.cwd();
86
+ return '## Environment\n\nWorking directory: ' + cwd;
87
+ }
88
+
89
+ private buildPermissionMode(): string {
90
+ const mode = this.config.permissionMode || 'ask';
91
+ const explanations: Record<string, string> = {
92
+ accept: 'All tool calls are automatically allowed without confirmation.',
93
+ deny: 'All tool calls are blocked. You may only read and discuss.',
94
+ ask: 'Dangerous or destructive commands require your confirmation before execution.'
95
+ };
96
+ return '## Permission Mode\n\nCurrent mode: ' + mode + '\n\n' + (explanations[mode] || '');
97
+ }
98
+
99
+ private buildProjectInstructions(): string {
100
+ const cwd = this.config.cwd || process.cwd();
101
+ if (this.config.includeClaudeMd !== false) {
102
+ const paths = [join(cwd, 'CLAUDE.md'), join(cwd, '.claude.md')];
103
+ for (const path of paths) {
104
+ if (existsSync(path)) {
105
+ try {
106
+ const content = readFileSync(path, 'utf-8');
107
+ return '## Project Instructions (from ' + path.split(/[/\\]/).pop() + ')\n\n' + content;
108
+ } catch {
109
+ // Ignore
110
+ }
111
+ }
112
+ }
113
+ }
114
+ if (this.config.projectInstructions) {
115
+ return '## Project Instructions\n\n' + this.config.projectInstructions;
116
+ }
117
+ return '';
118
+ }
119
+
120
+ private buildDateInfo(): string {
121
+ const now = this.config.date || new Date();
122
+ const iso = now.toISOString().replace('T', ' ').split('.')[0];
123
+ return '## Current Time\n\n' + iso;
124
+ }
125
+
126
+ private buildOutputFormat(): string {
127
+ return [
128
+ '## Output Format',
129
+ '',
130
+ '- Use clear section headers for multi-part responses',
131
+ '- Use bullet points for lists',
132
+ '- Use code blocks for code',
133
+ '- Be concise, avoid unnecessary repetition',
134
+ '- If you need multiple steps, explain the plan first'
135
+ ].join('\n');
136
+ }
137
+
138
+ setTools(tools: Tool[]) { this.config.tools = tools; return this; }
139
+ setCwd(cwd: string) { this.config.cwd = cwd; return this; }
140
+ setPermissionMode(mode: 'accept' | 'deny' | 'ask') { this.config.permissionMode = mode; return this; }
141
+ setProjectInstructions(instructions: string) { this.config.projectInstructions = instructions; return this; }
142
+ setModel(model: string) { this.config.model = model; return this; }
143
+
144
+ buildToolsFragment(): string { return this.buildToolInstructions(); }
145
+
146
+ injectReminder(reminder: string): string {
147
+ return '\n## Reminder\n\n' + reminder + '\n';
148
+ }
149
+ }
150
+
151
+ let globalBuilder: SystemPromptBuilder | null = null;
152
+
153
+ export function getSystemPromptBuilder(config?: SystemPromptConfig): SystemPromptBuilder {
154
+ if (!globalBuilder) globalBuilder = new SystemPromptBuilder(config);
155
+ return globalBuilder;
156
+ }
157
+
158
+ export function updateSystemPrompt(tools?: Tool[], permissionMode?: 'accept' | 'deny' | 'ask') {
159
+ if (globalBuilder) {
160
+ if (tools) globalBuilder.setTools(tools);
161
+ if (permissionMode) globalBuilder.setPermissionMode(permissionMode);
162
+ }
163
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Tool Registry - Manages available tools with context support
3
+ * S02: Enhanced with inputSchema generation and permission metadata
4
+ */
5
+
6
+ import { Tool, ToolResult, ToolContext, ToolInputSchema } from './types.js';
7
+
8
+ export class ToolRegistry {
9
+ private tools: Map<string, Tool> = new Map();
10
+ private context: ToolContext = {};
11
+
12
+ constructor() {
13
+ this.registerDefaultTools();
14
+ }
15
+
16
+ setContext(ctx: Partial<ToolContext>) {
17
+ this.context = { ...this.context, ...ctx };
18
+ }
19
+
20
+ getContext(): ToolContext {
21
+ return this.context;
22
+ }
23
+
24
+ register(tool: Tool): void {
25
+ if (this.tools.has(tool.name)) {
26
+ console.warn(`Tool ${tool.name} already exists, overwriting...`);
27
+ }
28
+ this.tools.set(tool.name, tool);
29
+ }
30
+
31
+ unregister(name: string): boolean {
32
+ return this.tools.delete(name);
33
+ }
34
+
35
+ get(name: string): Tool | undefined {
36
+ return this.tools.get(name);
37
+ }
38
+
39
+ list(): string[] {
40
+ return Array.from(this.tools.keys());
41
+ }
42
+
43
+ /**
44
+ * S02: Get tools formatted for AI API
45
+ * Uses inputSchema if available, otherwise builds from parameters
46
+ */
47
+ getToolsForAPI() {
48
+ return Array.from(this.tools.values()).map(tool => ({
49
+ type: 'function' as const,
50
+ function: {
51
+ name: tool.name,
52
+ description: tool.description,
53
+ parameters: tool.inputSchema || this.buildInputSchema(tool)
54
+ }
55
+ }));
56
+ }
57
+
58
+ /**
59
+ * S02: Build JSON Schema from legacy parameters array
60
+ */
61
+ private buildInputSchema(tool: Tool): ToolInputSchema {
62
+ return {
63
+ type: 'object',
64
+ properties: tool.parameters.reduce((acc, p) => {
65
+ acc[p.name] = {
66
+ type: p.type === 'string' ? 'string'
67
+ : p.type === 'number' ? 'number'
68
+ : p.type === 'boolean' ? 'boolean'
69
+ : 'string',
70
+ description: p.description
71
+ };
72
+ return acc;
73
+ }, {} as Record<string, { type: string; description: string }>),
74
+ required: tool.parameters.filter(p => p.required).map(p => p.name)
75
+ };
76
+ }
77
+
78
+ /**
79
+ * S02: List all tools with their metadata
80
+ */
81
+ listAll() {
82
+ return Array.from(this.tools.values()).map(t => ({
83
+ name: t.name,
84
+ description: t.description,
85
+ permissions: t.metadata?.permissions,
86
+ tags: t.metadata?.tags,
87
+ deprecated: t.metadata?.deprecated
88
+ }));
89
+ }
90
+
91
+ async execute(name: string, params: Record<string, any>): Promise<ToolResult> {
92
+ const tool = this.tools.get(name);
93
+
94
+ if (!tool) {
95
+ return { success: false, error: `Tool "${name}" not found` };
96
+ }
97
+
98
+ // S02: Validate params against inputSchema
99
+ const validationError = this.validateParams(tool, params);
100
+ if (validationError) {
101
+ return { success: false, error: validationError };
102
+ }
103
+
104
+ try {
105
+ return await tool.execute(params, this.context);
106
+ } catch (error: any) {
107
+ return { success: false, error: error.message };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * S02: Validate params against tool's inputSchema
113
+ */
114
+ private validateParams(tool: Tool, params: Record<string, any>): string | null {
115
+ const schema = tool.inputSchema || this.buildInputSchema(tool);
116
+
117
+ // Check required
118
+ if (schema.required) {
119
+ for (const req of schema.required) {
120
+ if (params[req] === undefined || params[req] === null) {
121
+ return `Missing required parameter: ${req}`;
122
+ }
123
+ }
124
+ }
125
+
126
+ // Check types
127
+ for (const [key, value] of Object.entries(params)) {
128
+ const prop = schema.properties[key];
129
+ if (prop && value !== undefined) {
130
+ const expectedType = prop.type;
131
+ const actualType = typeof value;
132
+ // Type coercion for string/number
133
+ if (expectedType === 'number' && actualType === 'string' && !isNaN(Number(value))) {
134
+ // Auto-coerce strings to numbers — ok
135
+ } else if (actualType !== expectedType) {
136
+ return `Invalid type for ${key}: expected ${expectedType}, got ${actualType}`;
137
+ }
138
+ }
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ async executeAll(tools: Array<{ name: string; params: Record<string, any> }>): Promise<ToolResult[]> {
145
+ const results: ToolResult[] = [];
146
+ for (const { name, params } of tools) {
147
+ const result = await this.execute(name, params);
148
+ results.push(result);
149
+ if (!result.success) break;
150
+ }
151
+ return results;
152
+ }
153
+
154
+ private registerDefaultTools(): void {
155
+ // Tools registered externally via AIEngine
156
+ }
157
+ }