@gagik.co/snippet-agent 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 (61) hide show
  1. package/.eslintrc.js +13 -0
  2. package/.prettierrc.json +1 -0
  3. package/README.md +23 -0
  4. package/dist/agent-class.d.ts +47 -0
  5. package/dist/agent-class.js +314 -0
  6. package/dist/agent.d.ts +1 -0
  7. package/dist/agent.js +392 -0
  8. package/dist/banner.d.ts +1 -0
  9. package/dist/banner.js +23 -0
  10. package/dist/confirmation-extension.d.ts +10 -0
  11. package/dist/confirmation-extension.js +213 -0
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.js +141 -0
  14. package/dist/mongosh-interactive-mode.d.ts +33 -0
  15. package/dist/mongosh-interactive-mode.js +244 -0
  16. package/dist/project-agent.d.ts +1 -0
  17. package/dist/project-agent.js +36 -0
  18. package/dist/shell-context.d.ts +17 -0
  19. package/dist/shell-context.js +75 -0
  20. package/dist/skills-loader.d.ts +2 -0
  21. package/dist/skills-loader.js +69 -0
  22. package/dist/src/index.d.ts +1 -0
  23. package/dist/src/index.js +8 -0
  24. package/dist/src/project-agent.d.ts +1 -0
  25. package/dist/src/project-agent.js +36 -0
  26. package/dist/stdout-patcher.d.ts +5 -0
  27. package/dist/stdout-patcher.js +41 -0
  28. package/dist/tools/index.d.ts +4 -0
  29. package/dist/tools/index.js +7 -0
  30. package/dist/tools/mongosh-eval.d.ts +7 -0
  31. package/dist/tools/mongosh-eval.js +84 -0
  32. package/dist/tools/search-docs.d.ts +2 -0
  33. package/dist/tools/search-docs.js +106 -0
  34. package/dist/tools/types.d.ts +12 -0
  35. package/dist/tools/types.js +2 -0
  36. package/dist/tools.d.ts +7 -0
  37. package/dist/tools.js +189 -0
  38. package/dist/types.d.ts +21 -0
  39. package/dist/types.js +2 -0
  40. package/package.json +38 -0
  41. package/skills/mongodb-connection.md +208 -0
  42. package/skills/mongodb-natural-language-querying.md +202 -0
  43. package/skills/mongodb-query-optimizer.md +265 -0
  44. package/skills/mongodb-schema-design.md +455 -0
  45. package/skills/mongodb-search-and-ai.md +357 -0
  46. package/skills/mongosh-shell.md +227 -0
  47. package/src/agent-class.ts +393 -0
  48. package/src/banner.ts +36 -0
  49. package/src/confirmation-extension.ts +297 -0
  50. package/src/index.ts +137 -0
  51. package/src/mongosh-interactive-mode.ts +420 -0
  52. package/src/shell-context.ts +97 -0
  53. package/src/skills-loader.ts +37 -0
  54. package/src/stdout-patcher.ts +48 -0
  55. package/src/tools/index.ts +4 -0
  56. package/src/tools/mongosh-eval.ts +115 -0
  57. package/src/tools/search-docs.ts +115 -0
  58. package/src/tools/types.ts +15 -0
  59. package/src/types.ts +23 -0
  60. package/tsconfig-lint.json +4 -0
  61. package/tsconfig.json +20 -0
@@ -0,0 +1,297 @@
1
+ import { inspect } from 'util';
2
+ import chalk from 'chalk';
3
+ import { highlight } from 'cli-highlight';
4
+
5
+ type ConfirmationExtensionOptions = {
6
+ skipConfirmation?: boolean;
7
+ allowedTools?: string[];
8
+ blockedTools?: string[];
9
+ skipConfirmTools?: string[];
10
+ };
11
+
12
+ let globalOptions: ConfirmationExtensionOptions = {};
13
+
14
+ export function setConfirmationOptions(
15
+ options: ConfirmationExtensionOptions,
16
+ ): void {
17
+ globalOptions = options;
18
+ }
19
+
20
+ export function getConfirmationOptions(): ConfirmationExtensionOptions {
21
+ return globalOptions;
22
+ }
23
+
24
+ function formatToolName(toolName: string): string {
25
+ const displayName = toolName.replace(/_/g, ' ');
26
+ return chalk.white.bold(displayName);
27
+ }
28
+
29
+ function formatToolParams(
30
+ toolName: string,
31
+ input: Record<string, unknown>,
32
+ ): string {
33
+ const lines: string[] = [];
34
+ for (const [key, value] of Object.entries(input)) {
35
+ const formatted =
36
+ typeof value === 'string'
37
+ ? value
38
+ : inspect(value, { depth: 3, breakLength: 80 });
39
+ lines.push(` ${chalk.gray(key)}: ${formatted}`);
40
+ }
41
+
42
+ return lines.length > 0 ? '\n\n' + lines.join('\n') : '';
43
+ }
44
+
45
+ function stripAnsiCodes(str: string): string {
46
+ // eslint-disable-next-line no-control-regex
47
+ return str.replace(/\u001b\[[0-9;]*m/g, '');
48
+ }
49
+
50
+ function formatWithBackground(content: string): string {
51
+ // ANSI color codes - darker gray background (48;5;236) and reset
52
+ const BG_DARK_GRAY = '\u001b[48;5;236m';
53
+ const RESET = '\u001b[0m';
54
+ const PADDING = ' ';
55
+
56
+ // Always use maximum available terminal width (minus small margin)
57
+ const terminalWidth = process.stdout.columns || 80;
58
+ const boxWidth = Math.max(40, terminalWidth - 2);
59
+ const contentWidth = boxWidth - 4; // minus padding on both sides
60
+
61
+ // Split content and wrap long lines to fit the full width
62
+ const rawLines = content.split('\n');
63
+
64
+ const processedLines: string[] = [];
65
+ for (const line of rawLines) {
66
+ const visibleLen = stripAnsiCodes(line).length;
67
+ if (visibleLen > contentWidth) {
68
+ // Hard wrap: keep ANSI codes, split at visible character boundary
69
+ let currentLine = line;
70
+ let currentVisibleLen = visibleLen;
71
+
72
+ while (currentVisibleLen > contentWidth) {
73
+ // Map visible character position to actual string position (preserving ANSI codes)
74
+ let visibleCount = 0;
75
+ let actualIndex = 0;
76
+ for (
77
+ let i = 0;
78
+ i < currentLine.length && visibleCount < contentWidth;
79
+ i++
80
+ ) {
81
+ if (currentLine[i] === '\u001b') {
82
+ // Skip ANSI sequence
83
+ while (i < currentLine.length && currentLine[i] !== 'm') {
84
+ i++;
85
+ }
86
+ } else {
87
+ visibleCount++;
88
+ }
89
+ actualIndex = i + 1;
90
+ }
91
+
92
+ processedLines.push(currentLine.slice(0, actualIndex));
93
+ currentLine = currentLine.slice(actualIndex);
94
+ currentVisibleLen = stripAnsiCodes(currentLine).length;
95
+ }
96
+ if (currentLine.length > 0) {
97
+ processedLines.push(currentLine);
98
+ }
99
+ } else {
100
+ processedLines.push(line);
101
+ }
102
+ }
103
+
104
+ // Build lines with dark gray background extending to full box width
105
+ const formattedLines = processedLines.map((line) => {
106
+ const visibleLen = stripAnsiCodes(line).length;
107
+ const pad = ' '.repeat(Math.max(0, contentWidth - visibleLen));
108
+ return `${BG_DARK_GRAY}${PADDING}${line}${pad}${PADDING}${RESET}`;
109
+ });
110
+
111
+ // Add empty padding lines with same full-width background
112
+ const emptyLine = `${BG_DARK_GRAY}${' '.repeat(boxWidth)}${RESET}`;
113
+
114
+ return [emptyLine, ...formattedLines, emptyLine].join('\n');
115
+ }
116
+
117
+ function formatConfirmationMessage(
118
+ toolName: string,
119
+ input: Record<string, unknown>,
120
+ ): { title: string; message: string } {
121
+ if (toolName === 'mongosh_eval') {
122
+ const expression = input.expression as string | undefined;
123
+ if (expression) {
124
+ const highlighted = highlight(expression, {
125
+ language: 'javascript',
126
+ theme: {
127
+ keyword: chalk.magenta,
128
+ function: chalk.cyan,
129
+ string: chalk.green,
130
+ number: chalk.yellow,
131
+ comment: chalk.gray,
132
+ operator: chalk.white,
133
+ punctuation: chalk.white,
134
+ literal: chalk.yellow,
135
+ params: chalk.white,
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ } as any,
138
+ });
139
+ return {
140
+ title: `Run this mongosh script? ${chalk.gray('(please review the code above)')}`,
141
+ message: '\n' + formatWithBackground(highlighted),
142
+ };
143
+ }
144
+ }
145
+
146
+ const nameLine = formatToolName(toolName);
147
+ const paramsSection = formatToolParams(toolName, input);
148
+ return {
149
+ title: 'Tool Call Confirmation',
150
+ message: nameLine + paramsSection,
151
+ };
152
+ }
153
+
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ export default function createConfirmationExtension(pi: any): void {
156
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
157
+ pi.on(
158
+ 'tool_call',
159
+ async (
160
+ event: {
161
+ type: 'tool_call';
162
+ toolCallId: string;
163
+ toolName: string;
164
+ input: Record<string, unknown>;
165
+ },
166
+ ctx: {
167
+ ui: {
168
+ confirm: (title: string, message: string) => Promise<boolean>;
169
+ notify: (message: string, type: 'info' | 'warning' | 'error') => void;
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ custom: <T>(
172
+ factory: (
173
+ tui: any,
174
+ theme: any,
175
+ keybindings: any,
176
+ done: (result: T) => void,
177
+ ) => {
178
+ render: () => string[];
179
+ invalidate: () => void;
180
+ handleInput?: (keyData: string) => void;
181
+ },
182
+ options?: {
183
+ overlay?: boolean;
184
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
+ overlayOptions?: any;
186
+ },
187
+ ) => Promise<T>;
188
+ setWidget: (
189
+ key: string,
190
+ content:
191
+ | string[]
192
+ | ((
193
+ tui: unknown,
194
+ theme: unknown,
195
+ ) => {
196
+ render: (width: number) => string[];
197
+ invalidate: () => void;
198
+ })
199
+ | undefined,
200
+ options?: { position?: 'above' | 'below' },
201
+ ) => void;
202
+ };
203
+ },
204
+ ) => {
205
+ const { toolName, input } = event;
206
+ const options = getConfirmationOptions();
207
+
208
+ if (options.skipConfirmation) {
209
+ return;
210
+ }
211
+
212
+ if (options.allowedTools && !options.allowedTools.includes(toolName)) {
213
+ return {
214
+ block: true,
215
+ reason: `Tool "${toolName}" is not in the allowed tools list.`,
216
+ };
217
+ }
218
+
219
+ if (options.blockedTools?.includes(toolName)) {
220
+ return {
221
+ block: true,
222
+ reason: `Tool "${toolName}" is blocked by policy.`,
223
+ };
224
+ }
225
+
226
+ // Skip confirmation for whitelisted tools
227
+ if (options.skipConfirmTools?.includes(toolName)) {
228
+ return;
229
+ }
230
+
231
+ const { title, message } = formatConfirmationMessage(toolName, input);
232
+
233
+ // Show code in chat history (static, scrollable)
234
+ ctx.ui.notify(message, 'info');
235
+
236
+ // Custom minimal confirm dialog using Pi SDK's custom() API with done callback
237
+ const confirmed = await ctx.ui.custom(
238
+ (_tui, _theme, _keybindings, done) => {
239
+ let selectedIndex = 0;
240
+ const opts = ['Yes', 'No'];
241
+
242
+ return {
243
+ render: () => {
244
+ const lines: string[] = [];
245
+ // Title at the top of the dialog
246
+ lines.push(chalk.white.bold(title));
247
+ // Yes/No options
248
+ for (let i = 0; i < opts.length; i++) {
249
+ const isSelected = i === selectedIndex;
250
+ const line = isSelected
251
+ ? `${chalk.cyan('→')} ${chalk.cyan.bold(opts[i])}`
252
+ : ` ${chalk.gray(opts[i])}`;
253
+ lines.push(line);
254
+ }
255
+ return lines;
256
+ },
257
+ invalidate: () => {},
258
+ handleInput: (keyData: string) => {
259
+ // Up/Down to navigate
260
+ if (keyData === '\u001b[A') {
261
+ // Up arrow
262
+ selectedIndex = Math.max(0, selectedIndex - 1);
263
+ } else if (keyData === '\u001b[B') {
264
+ // Down arrow
265
+ selectedIndex = Math.min(opts.length - 1, selectedIndex + 1);
266
+ } else if (keyData === '\r' || keyData === '\n') {
267
+ // Enter to confirm
268
+ done(selectedIndex === 0); // Yes = index 0
269
+ } else if (keyData === '\x03' || keyData === '\u001b') {
270
+ // Ctrl+C or Escape to cancel
271
+ done(false);
272
+ } else if (keyData.toLowerCase() === 'y') {
273
+ done(true);
274
+ } else if (keyData.toLowerCase() === 'n') {
275
+ done(false);
276
+ }
277
+ },
278
+ };
279
+ },
280
+ {
281
+ overlay: true,
282
+ overlayOptions: {
283
+ anchor: 'bottom-center',
284
+ offsetY: -4, // Move up above the input field
285
+ },
286
+ },
287
+ );
288
+
289
+ if (!confirmed) {
290
+ return {
291
+ block: true,
292
+ reason: `Tool "${toolName}" was cancelled by user.`,
293
+ };
294
+ }
295
+ },
296
+ );
297
+ }
package/src/index.ts ADDED
@@ -0,0 +1,137 @@
1
+ import * as path from 'path';
2
+ import type { CliContext } from './types';
3
+ import { loadSkillsFromDir } from './skills-loader';
4
+ import { createShellContext } from './shell-context';
5
+ import { createMongoshEvalTool, createSearchDocsTool } from './tools';
6
+ import { createStdoutPatcher } from './stdout-patcher';
7
+ import { setConfirmationOptions } from './confirmation-extension';
8
+ import { Agent } from './agent-class';
9
+
10
+ function setupDebugLogging(): boolean {
11
+ // Ensure telemetry is disabled from pi-coding-agent
12
+ // This is a safeguard in addition to the settings manager configuration
13
+ process.env.PI_TELEMETRY = 'false';
14
+
15
+ const debugLogging = process.env.DEBUG_AGENT === '1';
16
+ const logRequests = process.env.DEBUG_AGENT_REQUESTS === '1';
17
+
18
+ if (debugLogging) {
19
+ process.stderr.write(`[agent] DEBUG_AGENT_REQUESTS=${process.env.DEBUG_AGENT_REQUESTS ?? 'undefined'}\n`);
20
+ }
21
+
22
+ if (logRequests) {
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ const originalFetch = (globalThis as any).fetch;
25
+ if (originalFetch) {
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ (globalThis as any).fetch = async (input: any, init?: any) => {
28
+ const url = typeof input === 'string' ? input : input.toString();
29
+ const method = init?.method || 'GET';
30
+ process.stderr.write(`[agent:fetch] ${String(method)} ${String(url)}\n`);
31
+ const start = Date.now();
32
+ try {
33
+ const response = await originalFetch(input, init);
34
+ process.stderr.write(`[agent:fetch] Response: ${String(response.status)} (${String(Date.now() - start)}ms)\n`);
35
+ return response;
36
+ } catch (err) {
37
+ process.stderr.write(`[agent:fetch] Error: ${String(err)}\n`);
38
+ throw err;
39
+ }
40
+ };
41
+ }
42
+ }
43
+
44
+ return debugLogging;
45
+ }
46
+
47
+ async function loadServices() {
48
+ const {
49
+ createAgentSessionRuntime,
50
+ createAgentSessionServices,
51
+ createAgentSessionFromServices,
52
+ SessionManager,
53
+ InteractiveMode,
54
+ SettingsManager,
55
+ getAgentDir,
56
+ initTheme,
57
+ AuthStorage,
58
+ ModelRegistry,
59
+ } = await import('@earendil-works/pi-coding-agent');
60
+
61
+ return {
62
+ createAgentSessionRuntime,
63
+ createAgentSessionServices,
64
+ createAgentSessionFromServices,
65
+ SessionManager,
66
+ InteractiveMode,
67
+ SettingsManager,
68
+ getAgentDir,
69
+ initTheme,
70
+ AuthStorage,
71
+ ModelRegistry,
72
+ };
73
+ }
74
+
75
+ export = async (mongoshContext: CliContext) => {
76
+ const debugLogging = setupDebugLogging();
77
+
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ const shellContext = mongoshContext as any;
80
+
81
+ const skillsDir = path.join(__dirname, '..', 'skills');
82
+ const loadedSkills = loadSkillsFromDir(skillsDir);
83
+
84
+ const shellCtx = createShellContext({ shellContext });
85
+ const mongoshEvalTool = await createMongoshEvalTool({ shellContext: shellCtx, debugLogging });
86
+ const searchDocsTool = await createSearchDocsTool();
87
+ const stdoutPatcher = createStdoutPatcher();
88
+
89
+ const services = await loadServices();
90
+
91
+ // Initialize theme after loading services
92
+ services.initTheme('dark', false);
93
+
94
+ // Whitelist search_docs tool to run without confirmation
95
+ setConfirmationOptions({
96
+ skipConfirmTools: ['search_docs'],
97
+ });
98
+
99
+ const agent = new Agent({
100
+ services,
101
+ mongoshEvalTool,
102
+ searchDocsTool,
103
+ loadedSkills,
104
+ skillsDir,
105
+ debugLogging,
106
+ stdoutPatcher,
107
+ shellContext: shellCtx,
108
+ });
109
+
110
+ const agentFn = async () => {
111
+ await agent.run();
112
+ };
113
+
114
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
+ (agentFn as any).isDirectShellCommand = true;
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ (agentFn as any).returnsPromise = true;
118
+
119
+ // Add resume method - callable as agent.resume <sessionId>
120
+ const resumeFn = async (sessionId: string) => {
121
+ if (!sessionId) {
122
+ process.stderr.write('Usage: agent.resume <session-id>\n');
123
+ return;
124
+ }
125
+ await agent.resume(sessionId);
126
+ };
127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
128
+ (resumeFn as any).isDirectShellCommand = true;
129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
+ (resumeFn as any).returnsPromise = true;
131
+
132
+ // Register agent and agent.resume
133
+ shellCtx.instanceState.shellApi['agent'] = agentFn;
134
+ shellCtx.instanceState.context['agent'] = agentFn;
135
+ shellCtx.instanceState.shellApi['agent.resume'] = resumeFn;
136
+ shellCtx.instanceState.context['agent.resume'] = resumeFn;
137
+ };