gencode-ai 0.1.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 (274) hide show
  1. package/.env.example +11 -0
  2. package/CLAUDE.md +70 -0
  3. package/LICENSE +21 -0
  4. package/README.md +117 -0
  5. package/dist/agent/agent.d.ts +84 -0
  6. package/dist/agent/agent.d.ts.map +1 -0
  7. package/dist/agent/agent.js +233 -0
  8. package/dist/agent/agent.js.map +1 -0
  9. package/dist/agent/index.d.ts +6 -0
  10. package/dist/agent/index.d.ts.map +1 -0
  11. package/dist/agent/index.js +6 -0
  12. package/dist/agent/index.js.map +1 -0
  13. package/dist/agent/types.d.ts +47 -0
  14. package/dist/agent/types.d.ts.map +1 -0
  15. package/dist/agent/types.js +5 -0
  16. package/dist/agent/types.js.map +1 -0
  17. package/dist/cli/components/App.d.ts +14 -0
  18. package/dist/cli/components/App.d.ts.map +1 -0
  19. package/dist/cli/components/App.js +395 -0
  20. package/dist/cli/components/App.js.map +1 -0
  21. package/dist/cli/components/CommandSuggestions.d.ts +13 -0
  22. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -0
  23. package/dist/cli/components/CommandSuggestions.js +32 -0
  24. package/dist/cli/components/CommandSuggestions.js.map +1 -0
  25. package/dist/cli/components/Header.d.ts +9 -0
  26. package/dist/cli/components/Header.d.ts.map +1 -0
  27. package/dist/cli/components/Header.js +13 -0
  28. package/dist/cli/components/Header.js.map +1 -0
  29. package/dist/cli/components/Input.d.ts +13 -0
  30. package/dist/cli/components/Input.d.ts.map +1 -0
  31. package/dist/cli/components/Input.js +27 -0
  32. package/dist/cli/components/Input.js.map +1 -0
  33. package/dist/cli/components/Logo.d.ts +2 -0
  34. package/dist/cli/components/Logo.d.ts.map +1 -0
  35. package/dist/cli/components/Logo.js +8 -0
  36. package/dist/cli/components/Logo.js.map +1 -0
  37. package/dist/cli/components/Messages.d.ts +37 -0
  38. package/dist/cli/components/Messages.d.ts.map +1 -0
  39. package/dist/cli/components/Messages.js +106 -0
  40. package/dist/cli/components/Messages.js.map +1 -0
  41. package/dist/cli/components/ModelSelector.d.ts +13 -0
  42. package/dist/cli/components/ModelSelector.d.ts.map +1 -0
  43. package/dist/cli/components/ModelSelector.js +72 -0
  44. package/dist/cli/components/ModelSelector.js.map +1 -0
  45. package/dist/cli/components/Spinner.d.ts +12 -0
  46. package/dist/cli/components/Spinner.d.ts.map +1 -0
  47. package/dist/cli/components/Spinner.js +45 -0
  48. package/dist/cli/components/Spinner.js.map +1 -0
  49. package/dist/cli/components/index.d.ts +12 -0
  50. package/dist/cli/components/index.d.ts.map +1 -0
  51. package/dist/cli/components/index.js +12 -0
  52. package/dist/cli/components/index.js.map +1 -0
  53. package/dist/cli/components/theme.d.ts +31 -0
  54. package/dist/cli/components/theme.d.ts.map +1 -0
  55. package/dist/cli/components/theme.js +36 -0
  56. package/dist/cli/components/theme.js.map +1 -0
  57. package/dist/cli/index-legacy.d.ts +7 -0
  58. package/dist/cli/index-legacy.d.ts.map +1 -0
  59. package/dist/cli/index-legacy.js +431 -0
  60. package/dist/cli/index-legacy.js.map +1 -0
  61. package/dist/cli/index.d.ts +7 -0
  62. package/dist/cli/index.d.ts.map +1 -0
  63. package/dist/cli/index.js +116 -0
  64. package/dist/cli/index.js.map +1 -0
  65. package/dist/cli/ink-cli.d.ts +7 -0
  66. package/dist/cli/ink-cli.d.ts.map +1 -0
  67. package/dist/cli/ink-cli.js +105 -0
  68. package/dist/cli/ink-cli.js.map +1 -0
  69. package/dist/cli/session-picker.d.ts +16 -0
  70. package/dist/cli/session-picker.d.ts.map +1 -0
  71. package/dist/cli/session-picker.js +280 -0
  72. package/dist/cli/session-picker.js.map +1 -0
  73. package/dist/cli/ui.d.ts +61 -0
  74. package/dist/cli/ui.d.ts.map +1 -0
  75. package/dist/cli/ui.js +364 -0
  76. package/dist/cli/ui.js.map +1 -0
  77. package/dist/config/index.d.ts +7 -0
  78. package/dist/config/index.d.ts.map +1 -0
  79. package/dist/config/index.js +6 -0
  80. package/dist/config/index.js.map +1 -0
  81. package/dist/config/manager.d.ts +31 -0
  82. package/dist/config/manager.d.ts.map +1 -0
  83. package/dist/config/manager.js +65 -0
  84. package/dist/config/manager.js.map +1 -0
  85. package/dist/config/types.d.ts +22 -0
  86. package/dist/config/types.d.ts.map +1 -0
  87. package/dist/config/types.js +6 -0
  88. package/dist/config/types.js.map +1 -0
  89. package/dist/index.d.ts +12 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +21 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/memory/index.d.ts +10 -0
  94. package/dist/memory/index.d.ts.map +1 -0
  95. package/dist/memory/index.js +9 -0
  96. package/dist/memory/index.js.map +1 -0
  97. package/dist/memory/init.d.ts +20 -0
  98. package/dist/memory/init.d.ts.map +1 -0
  99. package/dist/memory/init.js +332 -0
  100. package/dist/memory/init.js.map +1 -0
  101. package/dist/memory/manager.d.ts +85 -0
  102. package/dist/memory/manager.d.ts.map +1 -0
  103. package/dist/memory/manager.js +234 -0
  104. package/dist/memory/manager.js.map +1 -0
  105. package/dist/memory/types.d.ts +74 -0
  106. package/dist/memory/types.d.ts.map +1 -0
  107. package/dist/memory/types.js +6 -0
  108. package/dist/memory/types.js.map +1 -0
  109. package/dist/permissions/index.d.ts +7 -0
  110. package/dist/permissions/index.d.ts.map +1 -0
  111. package/dist/permissions/index.js +6 -0
  112. package/dist/permissions/index.js.map +1 -0
  113. package/dist/permissions/manager.d.ts +32 -0
  114. package/dist/permissions/manager.d.ts.map +1 -0
  115. package/dist/permissions/manager.js +79 -0
  116. package/dist/permissions/manager.js.map +1 -0
  117. package/dist/permissions/types.d.ts +14 -0
  118. package/dist/permissions/types.d.ts.map +1 -0
  119. package/dist/permissions/types.js +17 -0
  120. package/dist/permissions/types.js.map +1 -0
  121. package/dist/providers/anthropic.d.ts +20 -0
  122. package/dist/providers/anthropic.d.ts.map +1 -0
  123. package/dist/providers/anthropic.js +185 -0
  124. package/dist/providers/anthropic.js.map +1 -0
  125. package/dist/providers/gemini.d.ts +21 -0
  126. package/dist/providers/gemini.d.ts.map +1 -0
  127. package/dist/providers/gemini.js +241 -0
  128. package/dist/providers/gemini.js.map +1 -0
  129. package/dist/providers/index.d.ts +34 -0
  130. package/dist/providers/index.d.ts.map +1 -0
  131. package/dist/providers/index.js +72 -0
  132. package/dist/providers/index.js.map +1 -0
  133. package/dist/providers/openai.d.ts +19 -0
  134. package/dist/providers/openai.d.ts.map +1 -0
  135. package/dist/providers/openai.js +221 -0
  136. package/dist/providers/openai.js.map +1 -0
  137. package/dist/providers/types.d.ts +125 -0
  138. package/dist/providers/types.d.ts.map +1 -0
  139. package/dist/providers/types.js +6 -0
  140. package/dist/providers/types.js.map +1 -0
  141. package/dist/session/index.d.ts +6 -0
  142. package/dist/session/index.d.ts.map +1 -0
  143. package/dist/session/index.js +6 -0
  144. package/dist/session/index.js.map +1 -0
  145. package/dist/session/manager.d.ts +101 -0
  146. package/dist/session/manager.d.ts.map +1 -0
  147. package/dist/session/manager.js +295 -0
  148. package/dist/session/manager.js.map +1 -0
  149. package/dist/session/types.d.ts +39 -0
  150. package/dist/session/types.d.ts.map +1 -0
  151. package/dist/session/types.js +10 -0
  152. package/dist/session/types.js.map +1 -0
  153. package/dist/tools/builtin/bash.d.ts +7 -0
  154. package/dist/tools/builtin/bash.d.ts.map +1 -0
  155. package/dist/tools/builtin/bash.js +80 -0
  156. package/dist/tools/builtin/bash.js.map +1 -0
  157. package/dist/tools/builtin/edit.d.ts +7 -0
  158. package/dist/tools/builtin/edit.d.ts.map +1 -0
  159. package/dist/tools/builtin/edit.js +32 -0
  160. package/dist/tools/builtin/edit.js.map +1 -0
  161. package/dist/tools/builtin/glob.d.ts +7 -0
  162. package/dist/tools/builtin/glob.d.ts.map +1 -0
  163. package/dist/tools/builtin/glob.js +36 -0
  164. package/dist/tools/builtin/glob.js.map +1 -0
  165. package/dist/tools/builtin/grep.d.ts +7 -0
  166. package/dist/tools/builtin/grep.d.ts.map +1 -0
  167. package/dist/tools/builtin/grep.js +59 -0
  168. package/dist/tools/builtin/grep.js.map +1 -0
  169. package/dist/tools/builtin/read.d.ts +7 -0
  170. package/dist/tools/builtin/read.d.ts.map +1 -0
  171. package/dist/tools/builtin/read.js +29 -0
  172. package/dist/tools/builtin/read.js.map +1 -0
  173. package/dist/tools/builtin/write.d.ts +7 -0
  174. package/dist/tools/builtin/write.d.ts.map +1 -0
  175. package/dist/tools/builtin/write.js +24 -0
  176. package/dist/tools/builtin/write.js.map +1 -0
  177. package/dist/tools/index.d.ts +38 -0
  178. package/dist/tools/index.d.ts.map +1 -0
  179. package/dist/tools/index.js +32 -0
  180. package/dist/tools/index.js.map +1 -0
  181. package/dist/tools/registry.d.ts +22 -0
  182. package/dist/tools/registry.d.ts.map +1 -0
  183. package/dist/tools/registry.js +71 -0
  184. package/dist/tools/registry.js.map +1 -0
  185. package/dist/tools/types.d.ts +62 -0
  186. package/dist/tools/types.d.ts.map +1 -0
  187. package/dist/tools/types.js +126 -0
  188. package/dist/tools/types.js.map +1 -0
  189. package/docs/README.md +16 -0
  190. package/docs/proposals/0001-web-fetch-tool.md +293 -0
  191. package/docs/proposals/0002-web-search-tool.md +306 -0
  192. package/docs/proposals/0003-task-subagents.md +333 -0
  193. package/docs/proposals/0004-plan-mode.md +338 -0
  194. package/docs/proposals/0005-todo-system.md +299 -0
  195. package/docs/proposals/0006-memory-system.md +539 -0
  196. package/docs/proposals/0007-context-management.md +429 -0
  197. package/docs/proposals/0008-checkpointing.md +327 -0
  198. package/docs/proposals/0009-hooks-system.md +343 -0
  199. package/docs/proposals/0010-mcp-integration.md +382 -0
  200. package/docs/proposals/0011-custom-commands.md +374 -0
  201. package/docs/proposals/0012-ask-user-question.md +317 -0
  202. package/docs/proposals/0013-multi-edit-tool.md +345 -0
  203. package/docs/proposals/0014-lsp-tool.md +478 -0
  204. package/docs/proposals/0015-ls-tool.md +407 -0
  205. package/docs/proposals/0016-kill-shell-tool.md +455 -0
  206. package/docs/proposals/0017-background-tasks.md +489 -0
  207. package/docs/proposals/0018-parallel-tool-execution.md +415 -0
  208. package/docs/proposals/0019-session-enhancements.md +462 -0
  209. package/docs/proposals/0020-session-summarization.md +447 -0
  210. package/docs/proposals/0021-skills-system.md +409 -0
  211. package/docs/proposals/0022-plugin-system.md +467 -0
  212. package/docs/proposals/0023-permission-enhancements.md +470 -0
  213. package/docs/proposals/0024-keyboard-shortcuts.md +443 -0
  214. package/docs/proposals/0025-cost-tracking.md +447 -0
  215. package/docs/proposals/0026-git-integration.md +475 -0
  216. package/docs/proposals/0027-enhanced-read-tool.md +514 -0
  217. package/docs/proposals/0028-enhanced-bash-tool.md +511 -0
  218. package/docs/proposals/0029-notebook-edit-tool.md +413 -0
  219. package/docs/proposals/0030-plugin-marketplace.md +360 -0
  220. package/docs/proposals/0031-command-suggestions.md +295 -0
  221. package/docs/proposals/0032-ide-integrations.md +328 -0
  222. package/docs/proposals/0033-enterprise-deployment.md +221 -0
  223. package/docs/proposals/0034-sandboxing.md +273 -0
  224. package/docs/proposals/0035-auto-updater.md +311 -0
  225. package/docs/proposals/0036-enhanced-glob-tool.md +267 -0
  226. package/docs/proposals/0037-enhanced-grep-tool.md +360 -0
  227. package/docs/proposals/0038-interactive-cli-ui.md +373 -0
  228. package/docs/proposals/0039-streaming-enhancements.md +359 -0
  229. package/docs/proposals/0040-multi-provider-enhancements.md +369 -0
  230. package/docs/proposals/README.md +84 -0
  231. package/docs/proposals/TEMPLATE.md +57 -0
  232. package/docs/proposals/research/claude-code-research.md +307 -0
  233. package/examples/agent-demo.ts +115 -0
  234. package/examples/basic.ts +166 -0
  235. package/package.json +50 -0
  236. package/src/agent/agent.ts +276 -0
  237. package/src/agent/index.ts +6 -0
  238. package/src/agent/types.ts +62 -0
  239. package/src/cli/components/App.tsx +565 -0
  240. package/src/cli/components/CommandSuggestions.tsx +58 -0
  241. package/src/cli/components/Header.tsx +36 -0
  242. package/src/cli/components/Input.tsx +60 -0
  243. package/src/cli/components/Logo.tsx +16 -0
  244. package/src/cli/components/Messages.tsx +210 -0
  245. package/src/cli/components/ModelSelector.tsx +135 -0
  246. package/src/cli/components/Spinner.tsx +72 -0
  247. package/src/cli/components/index.ts +21 -0
  248. package/src/cli/components/theme.ts +36 -0
  249. package/src/cli/index.tsx +136 -0
  250. package/src/config/index.ts +7 -0
  251. package/src/config/manager.ts +77 -0
  252. package/src/config/types.ts +25 -0
  253. package/src/index.ts +86 -0
  254. package/src/permissions/index.ts +7 -0
  255. package/src/permissions/manager.ts +97 -0
  256. package/src/permissions/types.ts +29 -0
  257. package/src/providers/anthropic.ts +224 -0
  258. package/src/providers/gemini.ts +295 -0
  259. package/src/providers/index.ts +97 -0
  260. package/src/providers/openai.ts +261 -0
  261. package/src/providers/types.ts +181 -0
  262. package/src/session/index.ts +6 -0
  263. package/src/session/manager.ts +354 -0
  264. package/src/session/types.ts +49 -0
  265. package/src/tools/builtin/bash.ts +92 -0
  266. package/src/tools/builtin/edit.ts +37 -0
  267. package/src/tools/builtin/glob.ts +42 -0
  268. package/src/tools/builtin/grep.ts +67 -0
  269. package/src/tools/builtin/read.ts +34 -0
  270. package/src/tools/builtin/write.ts +27 -0
  271. package/src/tools/index.ts +36 -0
  272. package/src/tools/registry.ts +83 -0
  273. package/src/tools/types.ts +172 -0
  274. package/tsconfig.json +21 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Unified types for LLM providers
3
+ * Abstracts differences between OpenAI, Anthropic, and Gemini APIs
4
+ */
5
+
6
+ // ============================================================================
7
+ // Message Types
8
+ // ============================================================================
9
+
10
+ export type MessageRole = 'system' | 'user' | 'assistant';
11
+
12
+ export interface TextContent {
13
+ type: 'text';
14
+ text: string;
15
+ }
16
+
17
+ export interface ToolUseContent {
18
+ type: 'tool_use';
19
+ id: string;
20
+ name: string;
21
+ input: Record<string, unknown>;
22
+ }
23
+
24
+ export interface ToolResultContent {
25
+ type: 'tool_result';
26
+ toolUseId: string;
27
+ content: string;
28
+ isError?: boolean;
29
+ }
30
+
31
+ export type MessageContent = TextContent | ToolUseContent | ToolResultContent;
32
+
33
+ export interface Message {
34
+ role: MessageRole;
35
+ content: string | MessageContent[];
36
+ }
37
+
38
+ // ============================================================================
39
+ // Tool Types
40
+ // ============================================================================
41
+
42
+ export interface JSONSchema {
43
+ type: string;
44
+ properties?: Record<string, JSONSchema>;
45
+ required?: string[];
46
+ description?: string;
47
+ items?: JSONSchema;
48
+ enum?: unknown[];
49
+ [key: string]: unknown;
50
+ }
51
+
52
+ export interface ToolDefinition {
53
+ name: string;
54
+ description: string;
55
+ parameters: JSONSchema;
56
+ }
57
+
58
+ export interface ToolCall {
59
+ id: string;
60
+ name: string;
61
+ input: Record<string, unknown>;
62
+ }
63
+
64
+ export interface ToolResult {
65
+ toolUseId: string;
66
+ content: string;
67
+ isError?: boolean;
68
+ }
69
+
70
+ // ============================================================================
71
+ // Completion Types
72
+ // ============================================================================
73
+
74
+ export interface CompletionOptions {
75
+ model: string;
76
+ messages: Message[];
77
+ tools?: ToolDefinition[];
78
+ systemPrompt?: string;
79
+ maxTokens?: number;
80
+ temperature?: number;
81
+ stream?: boolean;
82
+ }
83
+
84
+ export type StopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence';
85
+
86
+ export interface CompletionResponse {
87
+ content: MessageContent[];
88
+ stopReason: StopReason;
89
+ usage?: {
90
+ inputTokens: number;
91
+ outputTokens: number;
92
+ };
93
+ }
94
+
95
+ // ============================================================================
96
+ // Streaming Types
97
+ // ============================================================================
98
+
99
+ export interface StreamChunkText {
100
+ type: 'text';
101
+ text: string;
102
+ }
103
+
104
+ export interface StreamChunkToolStart {
105
+ type: 'tool_start';
106
+ id: string;
107
+ name: string;
108
+ }
109
+
110
+ export interface StreamChunkToolInput {
111
+ type: 'tool_input';
112
+ id: string;
113
+ input: string; // Partial JSON string
114
+ }
115
+
116
+ export interface StreamChunkDone {
117
+ type: 'done';
118
+ response: CompletionResponse;
119
+ }
120
+
121
+ export interface StreamChunkError {
122
+ type: 'error';
123
+ error: Error;
124
+ }
125
+
126
+ export type StreamChunk =
127
+ | StreamChunkText
128
+ | StreamChunkToolStart
129
+ | StreamChunkToolInput
130
+ | StreamChunkDone
131
+ | StreamChunkError;
132
+
133
+ // ============================================================================
134
+ // Provider Interface
135
+ // ============================================================================
136
+
137
+ export interface ModelInfo {
138
+ id: string;
139
+ name: string;
140
+ description?: string;
141
+ }
142
+
143
+ export interface LLMProvider {
144
+ readonly name: string;
145
+
146
+ /**
147
+ * Generate a completion (non-streaming)
148
+ */
149
+ complete(options: CompletionOptions): Promise<CompletionResponse>;
150
+
151
+ /**
152
+ * Generate a streaming completion
153
+ */
154
+ stream(options: CompletionOptions): AsyncGenerator<StreamChunk, void, unknown>;
155
+
156
+ /**
157
+ * List available models from the provider
158
+ */
159
+ listModels(): Promise<ModelInfo[]>;
160
+ }
161
+
162
+ // ============================================================================
163
+ // Provider Configuration
164
+ // ============================================================================
165
+
166
+ export interface OpenAIConfig {
167
+ apiKey?: string;
168
+ baseURL?: string;
169
+ organization?: string;
170
+ }
171
+
172
+ export interface AnthropicConfig {
173
+ apiKey?: string;
174
+ baseURL?: string;
175
+ }
176
+
177
+ export interface GeminiConfig {
178
+ apiKey?: string;
179
+ }
180
+
181
+ export type ProviderConfig = OpenAIConfig | AnthropicConfig | GeminiConfig;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Session Management
3
+ */
4
+
5
+ export * from './types.js';
6
+ export { SessionManager } from './manager.js';
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Session Manager - Handles session persistence and retrieval
3
+ */
4
+
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import * as crypto from 'crypto';
9
+ import type { Message } from '../providers/types.js';
10
+ import type {
11
+ Session,
12
+ SessionMetadata,
13
+ SessionListItem,
14
+ SessionConfig,
15
+ } from './types.js';
16
+ import { DEFAULT_SESSION_CONFIG } from './types.js';
17
+
18
+ export class SessionManager {
19
+ private config: SessionConfig;
20
+ private storageDir: string;
21
+ private currentSession: Session | null = null;
22
+
23
+ constructor(config?: Partial<SessionConfig>) {
24
+ this.config = { ...DEFAULT_SESSION_CONFIG, ...config };
25
+ this.storageDir = this.config.storageDir.replace('~', os.homedir());
26
+ }
27
+
28
+ /**
29
+ * Initialize storage directory
30
+ */
31
+ async init(): Promise<void> {
32
+ await fs.mkdir(this.storageDir, { recursive: true });
33
+ }
34
+
35
+ /**
36
+ * Generate a new session ID
37
+ */
38
+ private generateId(): string {
39
+ const timestamp = Date.now().toString(36);
40
+ const random = crypto.randomBytes(4).toString('hex');
41
+ return `${timestamp}-${random}`;
42
+ }
43
+
44
+ /**
45
+ * Get session file path
46
+ */
47
+ private getSessionPath(id: string): string {
48
+ return path.join(this.storageDir, `${id}.json`);
49
+ }
50
+
51
+ /**
52
+ * Create a new session
53
+ */
54
+ async create(options: {
55
+ provider: string;
56
+ model: string;
57
+ cwd?: string;
58
+ title?: string;
59
+ parentId?: string;
60
+ }): Promise<Session> {
61
+ const id = this.generateId();
62
+ const now = new Date().toISOString();
63
+
64
+ const session: Session = {
65
+ metadata: {
66
+ id,
67
+ title: options.title ?? `Session ${new Date().toLocaleString()}`,
68
+ createdAt: now,
69
+ updatedAt: now,
70
+ provider: options.provider,
71
+ model: options.model,
72
+ cwd: options.cwd ?? process.cwd(),
73
+ messageCount: 0,
74
+ parentId: options.parentId,
75
+ },
76
+ messages: [],
77
+ };
78
+
79
+ this.currentSession = session;
80
+
81
+ if (this.config.autoSave) {
82
+ await this.save(session);
83
+ }
84
+
85
+ return session;
86
+ }
87
+
88
+ /**
89
+ * Fork an existing session
90
+ */
91
+ async fork(sessionId: string, title?: string): Promise<Session> {
92
+ const parent = await this.load(sessionId);
93
+ if (!parent) {
94
+ throw new Error(`Session not found: ${sessionId}`);
95
+ }
96
+
97
+ const id = this.generateId();
98
+ const now = new Date().toISOString();
99
+
100
+ const forked: Session = {
101
+ metadata: {
102
+ ...parent.metadata,
103
+ id,
104
+ title: title ?? `Fork of ${parent.metadata.title}`,
105
+ createdAt: now,
106
+ updatedAt: now,
107
+ parentId: sessionId,
108
+ },
109
+ messages: [...parent.messages],
110
+ systemPrompt: parent.systemPrompt,
111
+ };
112
+
113
+ this.currentSession = forked;
114
+
115
+ if (this.config.autoSave) {
116
+ await this.save(forked);
117
+ }
118
+
119
+ return forked;
120
+ }
121
+
122
+ /**
123
+ * Save a session to disk
124
+ */
125
+ async save(session: Session): Promise<void> {
126
+ await this.init();
127
+ session.metadata.updatedAt = new Date().toISOString();
128
+ session.metadata.messageCount = session.messages.length;
129
+
130
+ const filePath = this.getSessionPath(session.metadata.id);
131
+ await fs.writeFile(filePath, JSON.stringify(session, null, 2), 'utf-8');
132
+ }
133
+
134
+ /**
135
+ * Load a session from disk
136
+ */
137
+ async load(id: string): Promise<Session | null> {
138
+ try {
139
+ const filePath = this.getSessionPath(id);
140
+ const content = await fs.readFile(filePath, 'utf-8');
141
+ const session = JSON.parse(content) as Session;
142
+ this.currentSession = session;
143
+ return session;
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Resume the most recent session
151
+ */
152
+ async resumeLatest(): Promise<Session | null> {
153
+ const sessions = await this.list();
154
+ if (sessions.length === 0) {
155
+ return null;
156
+ }
157
+ return this.load(sessions[0].id);
158
+ }
159
+
160
+ /**
161
+ * Resume by index (1-based, most recent first)
162
+ */
163
+ async resumeByIndex(index: number): Promise<Session | null> {
164
+ const sessions = await this.list();
165
+ if (index < 1 || index > sessions.length) {
166
+ return null;
167
+ }
168
+ return this.load(sessions[index - 1].id);
169
+ }
170
+
171
+ /**
172
+ * List sessions, optionally filtered by project directory
173
+ */
174
+ async list(options?: { cwd?: string; all?: boolean }): Promise<SessionListItem[]> {
175
+ await this.init();
176
+ const filterCwd = options?.all ? undefined : (options?.cwd ?? process.cwd());
177
+
178
+ try {
179
+ const files = await fs.readdir(this.storageDir);
180
+ const sessions: SessionListItem[] = [];
181
+
182
+ for (const file of files) {
183
+ if (!file.endsWith('.json')) continue;
184
+
185
+ try {
186
+ const filePath = path.join(this.storageDir, file);
187
+ const content = await fs.readFile(filePath, 'utf-8');
188
+ const session = JSON.parse(content) as Session;
189
+
190
+ // Filter by cwd if specified
191
+ if (filterCwd && session.metadata.cwd !== filterCwd) {
192
+ continue;
193
+ }
194
+
195
+ // Get first user message as preview
196
+ const firstUserMsg = session.messages.find((m) => m.role === 'user');
197
+ let preview = '';
198
+ if (firstUserMsg) {
199
+ const text =
200
+ typeof firstUserMsg.content === 'string'
201
+ ? firstUserMsg.content
202
+ : firstUserMsg.content
203
+ .filter((c) => c.type === 'text')
204
+ .map((c) => (c as { text: string }).text)
205
+ .join(' ');
206
+ preview = text.slice(0, 80) + (text.length > 80 ? '...' : '');
207
+ }
208
+
209
+ sessions.push({
210
+ id: session.metadata.id,
211
+ title: session.metadata.title,
212
+ updatedAt: session.metadata.updatedAt,
213
+ messageCount: session.metadata.messageCount,
214
+ preview,
215
+ });
216
+ } catch {
217
+ // Skip invalid files
218
+ }
219
+ }
220
+
221
+ // Sort by updated time, newest first
222
+ sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
223
+
224
+ return sessions;
225
+ } catch {
226
+ return [];
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Delete a session
232
+ */
233
+ async delete(id: string): Promise<boolean> {
234
+ try {
235
+ const filePath = this.getSessionPath(id);
236
+ await fs.unlink(filePath);
237
+
238
+ if (this.currentSession?.metadata.id === id) {
239
+ this.currentSession = null;
240
+ }
241
+
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Clear old sessions based on config
250
+ */
251
+ async cleanup(): Promise<number> {
252
+ const sessions = await this.list();
253
+ let deleted = 0;
254
+
255
+ const now = Date.now();
256
+ const maxAge = this.config.maxAge * 24 * 60 * 60 * 1000;
257
+
258
+ for (let i = 0; i < sessions.length; i++) {
259
+ const session = sessions[i];
260
+ const age = now - new Date(session.updatedAt).getTime();
261
+
262
+ // Delete if too old or exceeds max count
263
+ if (age > maxAge || i >= this.config.maxSessions) {
264
+ if (await this.delete(session.id)) {
265
+ deleted++;
266
+ }
267
+ }
268
+ }
269
+
270
+ return deleted;
271
+ }
272
+
273
+ /**
274
+ * Get current session
275
+ */
276
+ getCurrent(): Session | null {
277
+ return this.currentSession;
278
+ }
279
+
280
+ /**
281
+ * Set current session
282
+ */
283
+ setCurrent(session: Session): void {
284
+ this.currentSession = session;
285
+ }
286
+
287
+ /**
288
+ * Add message to current session
289
+ */
290
+ async addMessage(message: Message): Promise<void> {
291
+ if (!this.currentSession) {
292
+ throw new Error('No active session');
293
+ }
294
+
295
+ this.currentSession.messages.push(message);
296
+
297
+ if (this.config.autoSave) {
298
+ await this.save(this.currentSession);
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Update session title
304
+ */
305
+ async updateTitle(id: string, title: string): Promise<void> {
306
+ const session = await this.load(id);
307
+ if (session) {
308
+ session.metadata.title = title;
309
+ await this.save(session);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Get messages from current session
315
+ */
316
+ getMessages(): Message[] {
317
+ return this.currentSession?.messages ?? [];
318
+ }
319
+
320
+ /**
321
+ * Clear current session messages
322
+ */
323
+ clearMessages(): void {
324
+ if (this.currentSession) {
325
+ this.currentSession.messages = [];
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Export session to JSON
331
+ */
332
+ async export(id: string): Promise<string> {
333
+ const session = await this.load(id);
334
+ if (!session) {
335
+ throw new Error(`Session not found: ${id}`);
336
+ }
337
+ return JSON.stringify(session, null, 2);
338
+ }
339
+
340
+ /**
341
+ * Import session from JSON
342
+ */
343
+ async import(json: string): Promise<Session> {
344
+ const session = JSON.parse(json) as Session;
345
+
346
+ // Generate new ID to avoid conflicts
347
+ session.metadata.id = this.generateId();
348
+ session.metadata.createdAt = new Date().toISOString();
349
+ session.metadata.updatedAt = new Date().toISOString();
350
+
351
+ await this.save(session);
352
+ return session;
353
+ }
354
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Session Management Types
3
+ */
4
+
5
+ import type { Message } from '../providers/types.js';
6
+
7
+ export interface SessionMetadata {
8
+ id: string;
9
+ title: string;
10
+ createdAt: string;
11
+ updatedAt: string;
12
+ provider: string;
13
+ model: string;
14
+ cwd: string;
15
+ messageCount: number;
16
+ tokenUsage?: {
17
+ input: number;
18
+ output: number;
19
+ };
20
+ parentId?: string; // For forked sessions
21
+ }
22
+
23
+ export interface Session {
24
+ metadata: SessionMetadata;
25
+ messages: Message[];
26
+ systemPrompt?: string;
27
+ }
28
+
29
+ export interface SessionListItem {
30
+ id: string;
31
+ title: string;
32
+ updatedAt: string;
33
+ messageCount: number;
34
+ preview: string; // First user message preview
35
+ }
36
+
37
+ export interface SessionConfig {
38
+ storageDir: string;
39
+ maxSessions: number;
40
+ maxAge: number; // Days
41
+ autoSave: boolean;
42
+ }
43
+
44
+ export const DEFAULT_SESSION_CONFIG: SessionConfig = {
45
+ storageDir: '~/.gencode/sessions',
46
+ maxSessions: 50,
47
+ maxAge: 30,
48
+ autoSave: true,
49
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Bash Tool - Execute shell commands
3
+ */
4
+
5
+ import { spawn } from 'child_process';
6
+ import type { Tool, ToolContext, ToolResult } from '../types.js';
7
+ import { BashInputSchema, type BashInput } from '../types.js';
8
+
9
+ export const bashTool: Tool<BashInput> = {
10
+ name: 'Bash',
11
+ description: 'Execute a bash command and return its output. Use for running scripts, git commands, npm, etc.',
12
+ parameters: BashInputSchema,
13
+
14
+ async execute(input: BashInput, context: ToolContext): Promise<ToolResult> {
15
+ const timeout = input.timeout ?? 30000;
16
+
17
+ return new Promise((resolve) => {
18
+ const proc = spawn('bash', ['-c', input.command], {
19
+ cwd: context.cwd,
20
+ env: process.env,
21
+ stdio: ['pipe', 'pipe', 'pipe'],
22
+ });
23
+
24
+ let stdout = '';
25
+ let stderr = '';
26
+
27
+ proc.stdout.on('data', (data) => {
28
+ stdout += data.toString();
29
+ });
30
+
31
+ proc.stderr.on('data', (data) => {
32
+ stderr += data.toString();
33
+ });
34
+
35
+ // Handle timeout
36
+ const timer = setTimeout(() => {
37
+ proc.kill('SIGTERM');
38
+ resolve({
39
+ success: false,
40
+ output: stdout,
41
+ error: `Command timed out after ${timeout}ms`,
42
+ });
43
+ }, timeout);
44
+
45
+ // Handle abort signal
46
+ if (context.abortSignal) {
47
+ context.abortSignal.addEventListener('abort', () => {
48
+ proc.kill('SIGTERM');
49
+ clearTimeout(timer);
50
+ resolve({
51
+ success: false,
52
+ output: stdout,
53
+ error: 'Command aborted',
54
+ });
55
+ });
56
+ }
57
+
58
+ proc.on('close', (code) => {
59
+ clearTimeout(timer);
60
+
61
+ // Truncate output if too long
62
+ const maxLength = 30000;
63
+ let output = stdout;
64
+ if (output.length > maxLength) {
65
+ output = output.slice(0, maxLength) + '\n... (output truncated)';
66
+ }
67
+
68
+ if (code === 0) {
69
+ resolve({
70
+ success: true,
71
+ output: output || '(no output)',
72
+ });
73
+ } else {
74
+ resolve({
75
+ success: false,
76
+ output,
77
+ error: stderr || `Command exited with code ${code}`,
78
+ });
79
+ }
80
+ });
81
+
82
+ proc.on('error', (error) => {
83
+ clearTimeout(timer);
84
+ resolve({
85
+ success: false,
86
+ output: '',
87
+ error: `Failed to execute command: ${error.message}`,
88
+ });
89
+ });
90
+ });
91
+ },
92
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Edit Tool - Edit file contents with string replacement
3
+ */
4
+
5
+ import * as fs from 'fs/promises';
6
+ import type { Tool, ToolResult } from '../types.js';
7
+ import { EditInputSchema, type EditInput, resolvePath, getErrorMessage } from '../types.js';
8
+
9
+ export const editTool: Tool<EditInput> = {
10
+ name: 'Edit',
11
+ description: 'Edit a file by replacing a specific string with another. The old_string must be unique in the file.',
12
+ parameters: EditInputSchema,
13
+
14
+ async execute(input, context): Promise<ToolResult> {
15
+ try {
16
+ const filePath = resolvePath(input.file_path, context.cwd);
17
+ const content = await fs.readFile(filePath, 'utf-8');
18
+ const occurrences = content.split(input.old_string).length - 1;
19
+
20
+ if (occurrences === 0) {
21
+ return { success: false, output: '', error: 'The string to replace was not found in the file.' };
22
+ }
23
+ if (occurrences > 1) {
24
+ return { success: false, output: '', error: `The string to replace occurs ${occurrences} times. Please provide a more unique string.` };
25
+ }
26
+
27
+ const newContent = content.replace(input.old_string, input.new_string);
28
+ await fs.writeFile(filePath, newContent, 'utf-8');
29
+
30
+ const oldLines = input.old_string.split('\n').length;
31
+ const newLines = input.new_string.split('\n').length;
32
+ return { success: true, output: `Successfully edited ${filePath}: replaced ${oldLines} line(s) with ${newLines} line(s)` };
33
+ } catch (error) {
34
+ return { success: false, output: '', error: `Failed to edit file: ${getErrorMessage(error)}` };
35
+ }
36
+ },
37
+ };