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.
- package/.patches/0001-fix-repl-replace-readline-with-inquirer-input-for-ke.patch +279 -0
- package/.patches/0002-fix-repl-stream-AI-output-directly-to-stdout-so-term.patch +564 -0
- package/.patches/0003-fix-session-break-the-hallucination-loop-in-assistan.patch +194 -0
- package/.patches/0004-chore-release-bump-version-to-0.2.1.patch +24 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/ROADMAP.md +107 -0
- package/dist/agent/core.d.ts +56 -0
- package/dist/agent/core.d.ts.map +1 -0
- package/dist/agent/core.js +142 -0
- package/dist/agent/core.js.map +1 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +8 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/intent.d.ts +36 -0
- package/dist/agent/intent.d.ts.map +1 -0
- package/dist/agent/intent.js +146 -0
- package/dist/agent/intent.js.map +1 -0
- package/dist/agent/streaming.d.ts +50 -0
- package/dist/agent/streaming.d.ts.map +1 -0
- package/dist/agent/streaming.js +110 -0
- package/dist/agent/streaming.js.map +1 -0
- package/dist/core/ai-engine.d.ts +59 -0
- package/dist/core/ai-engine.d.ts.map +1 -0
- package/dist/core/ai-engine.js +378 -0
- package/dist/core/ai-engine.js.map +1 -0
- package/dist/core/cli.d.ts +21 -0
- package/dist/core/cli.d.ts.map +1 -0
- package/dist/core/cli.js +143 -0
- package/dist/core/cli.js.map +1 -0
- package/dist/core/config.d.ts +38 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +129 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/context-compactor.d.ts +54 -0
- package/dist/core/context-compactor.d.ts.map +1 -0
- package/dist/core/context-compactor.js +197 -0
- package/dist/core/context-compactor.js.map +1 -0
- package/dist/core/hooks.d.ts +75 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +146 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/permissions.d.ts +52 -0
- package/dist/core/permissions.d.ts.map +1 -0
- package/dist/core/permissions.js +237 -0
- package/dist/core/permissions.js.map +1 -0
- package/dist/core/session.d.ts +54 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +147 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/skills.d.ts +60 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/core/skills.js +175 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/state.d.ts +60 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/state.js +106 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/subagent.d.ts +68 -0
- package/dist/core/subagent.d.ts.map +1 -0
- package/dist/core/subagent.js +142 -0
- package/dist/core/subagent.js.map +1 -0
- package/dist/core/system-prompt.d.ts +40 -0
- package/dist/core/system-prompt.d.ts.map +1 -0
- package/dist/core/system-prompt.js +138 -0
- package/dist/core/system-prompt.js.map +1 -0
- package/dist/core/tool-registry.d.ts +53 -0
- package/dist/core/tool-registry.d.ts.map +1 -0
- package/dist/core/tool-registry.js +138 -0
- package/dist/core/tool-registry.js.map +1 -0
- package/dist/core/types.d.ts +206 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +127 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +481 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +83 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +267 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/repl/index.d.ts +8 -0
- package/dist/repl/index.d.ts.map +1 -0
- package/dist/repl/index.js +8 -0
- package/dist/repl/index.js.map +1 -0
- package/dist/repl/input.d.ts +61 -0
- package/dist/repl/input.d.ts.map +1 -0
- package/dist/repl/input.js +127 -0
- package/dist/repl/input.js.map +1 -0
- package/dist/repl/loop.d.ts +53 -0
- package/dist/repl/loop.d.ts.map +1 -0
- package/dist/repl/loop.js +241 -0
- package/dist/repl/loop.js.map +1 -0
- package/dist/repl/output.d.ts +105 -0
- package/dist/repl/output.d.ts.map +1 -0
- package/dist/repl/output.js +197 -0
- package/dist/repl/output.js.map +1 -0
- package/dist/repl/welcome.d.ts +68 -0
- package/dist/repl/welcome.d.ts.map +1 -0
- package/dist/repl/welcome.js +249 -0
- package/dist/repl/welcome.js.map +1 -0
- package/dist/tools/file.d.ts +46 -0
- package/dist/tools/file.d.ts.map +1 -0
- package/dist/tools/file.js +100 -0
- package/dist/tools/file.js.map +1 -0
- package/dist/tools/git.d.ts +52 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/git.js +112 -0
- package/dist/tools/git.js.map +1 -0
- package/dist/tools/index.d.ts +22 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +43 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/search.d.ts +36 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +223 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/shell.d.ts +57 -0
- package/dist/tools/shell.d.ts.map +1 -0
- package/dist/tools/shell.js +153 -0
- package/dist/tools/shell.js.map +1 -0
- package/dist/utils/diff-preview.d.ts +42 -0
- package/dist/utils/diff-preview.d.ts.map +1 -0
- package/dist/utils/diff-preview.js +174 -0
- package/dist/utils/diff-preview.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/memory.d.ts +81 -0
- package/dist/utils/memory.d.ts.map +1 -0
- package/dist/utils/memory.js +186 -0
- package/dist/utils/memory.js.map +1 -0
- package/dist/utils/project-context.d.ts +62 -0
- package/dist/utils/project-context.d.ts.map +1 -0
- package/dist/utils/project-context.js +197 -0
- package/dist/utils/project-context.js.map +1 -0
- package/docs/API_KEY_GUIDE.md +236 -0
- package/docs/FAQ.md +182 -0
- package/install.bat +63 -0
- package/install.ps1 +238 -0
- package/install.sh +113 -0
- package/package.json +36 -0
- package/src/agent/core.ts +179 -0
- package/src/agent/index.ts +8 -0
- package/src/agent/intent.ts +181 -0
- package/src/agent/streaming.ts +132 -0
- package/src/core/ai-engine.ts +437 -0
- package/src/core/cli.ts +171 -0
- package/src/core/config.ts +147 -0
- package/src/core/context-compactor.ts +245 -0
- package/src/core/hooks.ts +196 -0
- package/src/core/permissions.ts +308 -0
- package/src/core/session.ts +165 -0
- package/src/core/skills.ts +208 -0
- package/src/core/state.ts +120 -0
- package/src/core/subagent.ts +195 -0
- package/src/core/system-prompt.ts +163 -0
- package/src/core/tool-registry.ts +157 -0
- package/src/core/types.ts +280 -0
- package/src/index.ts +544 -0
- package/src/mcp/client.ts +330 -0
- package/src/repl/index.ts +8 -0
- package/src/repl/input.ts +139 -0
- package/src/repl/loop.ts +280 -0
- package/src/repl/output.ts +222 -0
- package/src/repl/welcome.ts +296 -0
- package/src/tools/file.ts +117 -0
- package/src/tools/git.ts +132 -0
- package/src/tools/index.ts +48 -0
- package/src/tools/search.ts +263 -0
- package/src/tools/shell.ts +181 -0
- package/src/utils/diff-preview.ts +202 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/memory.ts +223 -0
- package/src/utils/project-context.ts +207 -0
- 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
|
+
}
|