@synergenius/flow-weaver-pack-weaver 0.9.3 → 0.9.5

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 (89) hide show
  1. package/dist/bot/ansi.d.ts +13 -0
  2. package/dist/bot/ansi.d.ts.map +1 -0
  3. package/dist/bot/ansi.js +13 -0
  4. package/dist/bot/ansi.js.map +1 -0
  5. package/dist/bot/assistant-core.d.ts.map +1 -1
  6. package/dist/bot/assistant-core.js +68 -32
  7. package/dist/bot/assistant-core.js.map +1 -1
  8. package/dist/bot/assistant-tools.d.ts +3 -2
  9. package/dist/bot/assistant-tools.d.ts.map +1 -1
  10. package/dist/bot/assistant-tools.js +6 -284
  11. package/dist/bot/assistant-tools.js.map +1 -1
  12. package/dist/bot/conversation-store.d.ts.map +1 -1
  13. package/dist/bot/conversation-store.js +1 -1
  14. package/dist/bot/conversation-store.js.map +1 -1
  15. package/dist/bot/error-classifier.d.ts +27 -0
  16. package/dist/bot/error-classifier.d.ts.map +1 -0
  17. package/dist/bot/error-classifier.js +71 -0
  18. package/dist/bot/error-classifier.js.map +1 -0
  19. package/dist/bot/error-guide.d.ts +2 -7
  20. package/dist/bot/error-guide.d.ts.map +1 -1
  21. package/dist/bot/error-guide.js +2 -31
  22. package/dist/bot/error-guide.js.map +1 -1
  23. package/dist/bot/paths.d.ts +11 -0
  24. package/dist/bot/paths.d.ts.map +1 -0
  25. package/dist/bot/paths.js +26 -0
  26. package/dist/bot/paths.js.map +1 -0
  27. package/dist/bot/response-formatter.d.ts +15 -0
  28. package/dist/bot/response-formatter.d.ts.map +1 -0
  29. package/dist/bot/response-formatter.js +40 -0
  30. package/dist/bot/response-formatter.js.map +1 -0
  31. package/dist/bot/retry-utils.d.ts +2 -16
  32. package/dist/bot/retry-utils.d.ts.map +1 -1
  33. package/dist/bot/retry-utils.js +2 -61
  34. package/dist/bot/retry-utils.js.map +1 -1
  35. package/dist/bot/rich-input.d.ts +39 -0
  36. package/dist/bot/rich-input.d.ts.map +1 -0
  37. package/dist/bot/rich-input.js +308 -0
  38. package/dist/bot/rich-input.js.map +1 -0
  39. package/dist/bot/safety.d.ts +10 -0
  40. package/dist/bot/safety.d.ts.map +1 -0
  41. package/dist/bot/safety.js +14 -0
  42. package/dist/bot/safety.js.map +1 -0
  43. package/dist/bot/slash-commands.d.ts +20 -0
  44. package/dist/bot/slash-commands.d.ts.map +1 -0
  45. package/dist/bot/slash-commands.js +93 -0
  46. package/dist/bot/slash-commands.js.map +1 -0
  47. package/dist/bot/steering.js +2 -2
  48. package/dist/bot/steering.js.map +1 -1
  49. package/dist/bot/task-queue.d.ts.map +1 -1
  50. package/dist/bot/task-queue.js +2 -15
  51. package/dist/bot/task-queue.js.map +1 -1
  52. package/dist/bot/terminal-renderer.d.ts.map +1 -1
  53. package/dist/bot/terminal-renderer.js +12 -13
  54. package/dist/bot/terminal-renderer.js.map +1 -1
  55. package/dist/bot/tool-registry.d.ts +24 -0
  56. package/dist/bot/tool-registry.d.ts.map +1 -0
  57. package/dist/bot/tool-registry.js +458 -0
  58. package/dist/bot/tool-registry.js.map +1 -0
  59. package/dist/bot/weaver-tools.d.ts +2 -2
  60. package/dist/bot/weaver-tools.d.ts.map +1 -1
  61. package/dist/bot/weaver-tools.js +4 -95
  62. package/dist/bot/weaver-tools.js.map +1 -1
  63. package/dist/cli-handlers.d.ts.map +1 -1
  64. package/dist/cli-handlers.js +1 -2
  65. package/dist/cli-handlers.js.map +1 -1
  66. package/dist/node-types/agent-execute.d.ts.map +1 -1
  67. package/dist/node-types/agent-execute.js +4 -8
  68. package/dist/node-types/agent-execute.js.map +1 -1
  69. package/flowweaver.manifest.json +1 -1
  70. package/package.json +1 -1
  71. package/src/bot/ansi.ts +12 -0
  72. package/src/bot/assistant-core.ts +70 -33
  73. package/src/bot/assistant-tools.ts +7 -294
  74. package/src/bot/conversation-store.ts +1 -1
  75. package/src/bot/error-classifier.ts +90 -0
  76. package/src/bot/error-guide.ts +2 -32
  77. package/src/bot/paths.ts +27 -0
  78. package/src/bot/response-formatter.ts +42 -0
  79. package/src/bot/retry-utils.ts +2 -74
  80. package/src/bot/rich-input.ts +307 -0
  81. package/src/bot/safety.ts +16 -0
  82. package/src/bot/slash-commands.ts +114 -0
  83. package/src/bot/steering.ts +2 -2
  84. package/src/bot/task-queue.ts +2 -16
  85. package/src/bot/terminal-renderer.ts +11 -14
  86. package/src/bot/tool-registry.ts +477 -0
  87. package/src/bot/weaver-tools.ts +4 -95
  88. package/src/cli-handlers.ts +1 -2
  89. package/src/node-types/agent-execute.ts +3 -6
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Unified error classification — merges error-guide.ts and retry-utils.ts
3
+ * into a single source of truth for error handling.
4
+ */
5
+
6
+ export interface ErrorClassification {
7
+ isTransient: boolean;
8
+ guidance: string | null;
9
+ category: 'auth' | 'network' | 'rate-limit' | 'timeout' | 'parse' | 'system' | 'unknown';
10
+ }
11
+
12
+ const PATTERNS: Array<{ pattern: RegExp; isTransient: boolean; guidance: string; category: ErrorClassification['category'] }> = [
13
+ { pattern: /ETIMEDOUT/i, isTransient: true, guidance: 'Network timeout. Check internet or increase timeout.', category: 'timeout' },
14
+ { pattern: /ECONNRESET/i, isTransient: true, guidance: 'Connection reset. Retry in a few seconds.', category: 'network' },
15
+ { pattern: /ECONNREFUSED/i, isTransient: true, guidance: 'Connection refused. Is the service running?', category: 'network' },
16
+ { pattern: /EPIPE|ENOTFOUND/i, isTransient: true, guidance: 'Network error.', category: 'network' },
17
+ { pattern: /401|authentication|invalid.*key/i, isTransient: false, guidance: 'Authentication failed. Check API key or run "weaver init".', category: 'auth' },
18
+ { pattern: /403|forbidden/i, isTransient: false, guidance: 'Access denied. API key may lack permissions.', category: 'auth' },
19
+ { pattern: /429|rate.?limit|too many requests/i, isTransient: true, guidance: 'Rate limited. Wait a few minutes or reduce --parallel.', category: 'rate-limit' },
20
+ { pattern: /502|bad gateway/i, isTransient: true, guidance: 'Server error (502). Will auto-retry.', category: 'network' },
21
+ { pattern: /503|service unavailable|overloaded/i, isTransient: true, guidance: 'Service overloaded. Will auto-retry.', category: 'network' },
22
+ { pattern: /504|gateway timeout/i, isTransient: true, guidance: 'Gateway timeout (504). Will auto-retry.', category: 'timeout' },
23
+ { pattern: /exit(?:ed with)? code 143/i, isTransient: true, guidance: 'Process killed (SIGTERM). Likely timeout or Ctrl+C.', category: 'timeout' },
24
+ { pattern: /exit code 137/i, isTransient: false, guidance: 'Process killed (OOM). System may be low on memory.', category: 'system' },
25
+ { pattern: /ENOMEM/i, isTransient: false, guidance: 'Out of memory.', category: 'system' },
26
+ { pattern: /ENOSPC/i, isTransient: false, guidance: 'Disk full.', category: 'system' },
27
+ { pattern: /lock.*retries|failed to acquire.*lock/i, isTransient: true, guidance: 'File lock contention. Another weaver process may be running.', category: 'system' },
28
+ { pattern: /not a workflow|No @flowWeaver/i, isTransient: false, guidance: 'Not a Flow Weaver workflow. Ensure @flowWeaver annotations exist.', category: 'parse' },
29
+ { pattern: /parse.*json|unexpected token/i, isTransient: false, guidance: 'JSON parse error. AI may have returned malformed output.', category: 'parse' },
30
+ { pattern: /Queue full/i, isTransient: false, guidance: 'Too many pending tasks (200 max). Process or clear first.', category: 'system' },
31
+ ];
32
+
33
+ /**
34
+ * Classify an error into transient/permanent with guidance and category.
35
+ */
36
+ export function classifyError(err: unknown): ErrorClassification {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ // Also check Node.js error codes
39
+ const code = err instanceof Error && 'code' in err ? (err as NodeJS.ErrnoException).code : undefined;
40
+ const fullMsg = code ? `${msg} ${code}` : msg;
41
+
42
+ for (const p of PATTERNS) {
43
+ if (p.pattern.test(fullMsg)) {
44
+ return { isTransient: p.isTransient, guidance: p.guidance, category: p.category };
45
+ }
46
+ }
47
+ return { isTransient: false, guidance: null, category: 'unknown' };
48
+ }
49
+
50
+ /** Convenience: check if an error is transient (retriable). */
51
+ export function isTransientError(err: unknown): boolean {
52
+ return classifyError(err).isTransient;
53
+ }
54
+
55
+ /** Convenience: get actionable guidance for an error message. */
56
+ export function getErrorGuidance(msg: string): string | null {
57
+ return classifyError(new Error(msg)).guidance;
58
+ }
59
+
60
+ /**
61
+ * Run a function with exponential backoff retry on transient errors.
62
+ */
63
+ export async function withRetry<T>(
64
+ fn: () => Promise<T>,
65
+ options?: {
66
+ maxRetries?: number;
67
+ baseDelayMs?: number;
68
+ multiplier?: number;
69
+ onRetry?: (attempt: number, delay: number, err: Error) => void;
70
+ },
71
+ ): Promise<T> {
72
+ const maxRetries = options?.maxRetries ?? 3;
73
+ const baseDelay = options?.baseDelayMs ?? 5_000;
74
+ const multiplier = options?.multiplier ?? 3;
75
+
76
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
77
+ try {
78
+ return await fn();
79
+ } catch (err: unknown) {
80
+ const isLast = attempt >= maxRetries;
81
+ if (isLast || !isTransientError(err)) throw err;
82
+ const delay = baseDelay * Math.pow(multiplier, attempt);
83
+ options?.onRetry?.(attempt + 1, delay, err instanceof Error ? err : new Error(String(err)));
84
+ await new Promise((r) => setTimeout(r, delay));
85
+ }
86
+ }
87
+
88
+ // Unreachable, but TypeScript needs it
89
+ throw new Error('withRetry: exhausted retries');
90
+ }
@@ -1,34 +1,4 @@
1
1
  /**
2
- * Actionable error guidance maps cryptic error messages
3
- * to human-readable explanations with fix suggestions.
2
+ * @deprecated Use error-classifier.ts instead. This file re-exports for backward compatibility.
4
3
  */
5
-
6
- const GUIDES: Array<{ pattern: RegExp; guidance: string }> = [
7
- { pattern: /ETIMEDOUT/i, guidance: 'Network timeout. Check internet connection or increase timeout with --timeout.' },
8
- { pattern: /ECONNRESET/i, guidance: 'Connection reset. The server closed the connection. Retry in a few seconds.' },
9
- { pattern: /ECONNREFUSED/i, guidance: 'Connection refused. Is the service running? Check the provider URL.' },
10
- { pattern: /401|authentication|invalid.*key/i, guidance: 'Authentication failed. Check ANTHROPIC_API_KEY or run "weaver init" to reconfigure.' },
11
- { pattern: /403|forbidden/i, guidance: 'Access denied. Your API key may not have permission for this model.' },
12
- { pattern: /429|rate.?limit|too many requests/i, guidance: 'Rate limited. Wait a few minutes or reduce --parallel.' },
13
- { pattern: /502|bad gateway/i, guidance: 'Server error (502). The API is temporarily unavailable. Will auto-retry.' },
14
- { pattern: /503|service unavailable|overloaded/i, guidance: 'Service overloaded. Will auto-retry with backoff.' },
15
- { pattern: /exit code 143/i, guidance: 'Process was killed (SIGTERM). Likely our timeout or Ctrl+C.' },
16
- { pattern: /exit code 137/i, guidance: 'Process was killed (OOM or SIGKILL). System may be low on memory.' },
17
- { pattern: /ENOMEM/i, guidance: 'Out of memory. Close other applications or increase available RAM.' },
18
- { pattern: /ENOSPC/i, guidance: 'Disk full. Free up disk space.' },
19
- { pattern: /lock.*retries|failed to acquire.*lock/i, guidance: 'File lock contention. Another weaver process may be running. Check with "ps aux | grep weaver".' },
20
- { pattern: /not a workflow|No @flowWeaver/i, guidance: 'File is not a Flow Weaver workflow. Ensure it has @flowWeaver annotations.' },
21
- { pattern: /parse.*json|unexpected token/i, guidance: 'JSON parse error. The AI may have returned malformed output. Retry the task.' },
22
- { pattern: /Queue full/i, guidance: 'Too many pending tasks (200 max). Process or clear existing tasks first.' },
23
- ];
24
-
25
- /**
26
- * Get actionable guidance for an error message.
27
- * Returns null if no guidance is available.
28
- */
29
- export function getErrorGuidance(msg: string): string | null {
30
- for (const { pattern, guidance } of GUIDES) {
31
- if (pattern.test(msg)) return guidance;
32
- }
33
- return null;
34
- }
4
+ export { getErrorGuidance } from './error-classifier.js';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared path resolution — single source of truth for .weaver directory layout.
3
+ */
4
+ import * as path from 'node:path';
5
+ import * as os from 'node:os';
6
+ import * as crypto from 'node:crypto';
7
+
8
+ /**
9
+ * Hash a directory path into a short filesystem-safe string.
10
+ * Used for per-project isolation under ~/.weaver/projects/.
11
+ */
12
+ export function hashDir(dir: string): string {
13
+ return crypto.createHash('sha256').update(dir).digest('hex').slice(0, 8);
14
+ }
15
+
16
+ /**
17
+ * Resolve the weaver working directory.
18
+ * Priority: explicit > WEAVER_QUEUE_DIR > WEAVER_STEERING_DIR > project-scoped > global fallback.
19
+ */
20
+ export function resolveWeaverDir(explicit?: string): string {
21
+ return explicit
22
+ ?? process.env.WEAVER_QUEUE_DIR
23
+ ?? process.env.WEAVER_STEERING_DIR
24
+ ?? (process.env.WEAVER_PROJECT_DIR
25
+ ? path.join(os.homedir(), '.weaver', 'projects', hashDir(process.env.WEAVER_PROJECT_DIR))
26
+ : path.join(os.homedir(), '.weaver'));
27
+ }
@@ -0,0 +1,42 @@
1
+ import * as path from 'node:path';
2
+ import { c } from './ansi.js';
3
+
4
+ /**
5
+ * Highlight code blocks in streamed text.
6
+ * Detects ```lang ... ``` patterns and applies dim styling.
7
+ */
8
+ export function highlightCodeBlocks(text: string): string {
9
+ return text.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => {
10
+ const header = lang ? ` ${c.cyan(`[${lang}]`)}\n` : '';
11
+ return `${header}${c.dim(code)}`;
12
+ });
13
+ }
14
+
15
+ /**
16
+ * Make file paths clickable using OSC 8 terminal hyperlinks.
17
+ * Only active when terminal supports it.
18
+ */
19
+ export function linkifyPaths(text: string, cwd: string): string {
20
+ if (!supportsHyperlinks()) return text;
21
+ return text.replace(/\b((?:src|tests|lib|dist)\/[\w/.-]+\.(?:ts|js|json|md))\b/g, (match) => {
22
+ const abs = path.resolve(cwd, match);
23
+ return `\x1b]8;;file://${abs}\x07${match}\x1b]8;;\x07`;
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Format a full response — apply all formatting passes.
29
+ */
30
+ export function formatResponse(text: string, cwd: string): string {
31
+ let result = text;
32
+ result = highlightCodeBlocks(result);
33
+ result = linkifyPaths(result, cwd);
34
+ return result;
35
+ }
36
+
37
+ function supportsHyperlinks(): boolean {
38
+ const term = process.env.TERM_PROGRAM ?? '';
39
+ // Known terminals that support OSC 8
40
+ return ['iTerm.app', 'WezTerm', 'vscode', 'Hyper'].includes(term)
41
+ || !!process.env.TERM_PROGRAM_VERSION; // Most modern terminals
42
+ }
@@ -1,76 +1,4 @@
1
1
  /**
2
- * Retry utilities for transient error handling with exponential backoff.
2
+ * @deprecated Use error-classifier.ts instead. This file re-exports for backward compatibility.
3
3
  */
4
-
5
- const TRANSIENT_STATUS_CODES = [429, 502, 503, 504];
6
- const TRANSIENT_ERROR_CODES = ['ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ENOTFOUND'];
7
- const TRANSIENT_MESSAGES = ['rate limit', 'too many requests', 'overloaded', 'bad gateway', 'service unavailable'];
8
-
9
- /**
10
- * Check if an error is transient (retriable) vs permanent.
11
- * Transient: network issues, rate limits, server errors.
12
- * Permanent: auth failures, parse errors, validation errors.
13
- */
14
- export function isTransientError(err: unknown): boolean {
15
- const msg = err instanceof Error ? err.message : String(err);
16
- const lower = msg.toLowerCase();
17
-
18
- // Check for HTTP status codes in message
19
- for (const code of TRANSIENT_STATUS_CODES) {
20
- if (msg.includes(String(code))) return true;
21
- }
22
-
23
- // Check for Node.js error codes
24
- if (err instanceof Error && 'code' in err) {
25
- const code = (err as NodeJS.ErrnoException).code;
26
- if (code && TRANSIENT_ERROR_CODES.includes(code)) return true;
27
- }
28
-
29
- // Also check message for error code strings (e.g. "ETIMEDOUT" in message)
30
- for (const code of TRANSIENT_ERROR_CODES) {
31
- if (msg.includes(code)) return true;
32
- }
33
-
34
- // Check for rate limit / overload messages
35
- for (const phrase of TRANSIENT_MESSAGES) {
36
- if (lower.includes(phrase)) return true;
37
- }
38
-
39
- // Check for exit code 143 (SIGTERM — likely our timeout killed the process)
40
- if (msg.includes('exit code 143') || msg.includes('exited with code 143')) return true;
41
-
42
- return false;
43
- }
44
-
45
- /**
46
- * Run a function with exponential backoff retry on transient errors.
47
- */
48
- export async function withRetry<T>(
49
- fn: () => Promise<T>,
50
- options?: {
51
- maxRetries?: number;
52
- baseDelayMs?: number;
53
- multiplier?: number;
54
- onRetry?: (attempt: number, delay: number, err: Error) => void;
55
- },
56
- ): Promise<T> {
57
- const maxRetries = options?.maxRetries ?? 3;
58
- const baseDelay = options?.baseDelayMs ?? 5_000;
59
- const multiplier = options?.multiplier ?? 3;
60
-
61
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
62
- try {
63
- return await fn();
64
- } catch (err: unknown) {
65
- const isLast = attempt >= maxRetries;
66
- if (isLast || !isTransientError(err)) throw err;
67
-
68
- const delay = baseDelay * Math.pow(multiplier, attempt);
69
- options?.onRetry?.(attempt + 1, delay, err instanceof Error ? err : new Error(String(err)));
70
- await new Promise((r) => setTimeout(r, delay));
71
- }
72
- }
73
-
74
- // Unreachable, but TypeScript needs it
75
- throw new Error('withRetry: exhausted retries');
76
- }
4
+ export { isTransientError, withRetry } from './error-classifier.js';
@@ -0,0 +1,307 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import * as readline from 'node:readline';
5
+
6
+ export interface RichInputOptions {
7
+ historyFile?: string;
8
+ prompt?: string;
9
+ completionProvider?: (partial: string) => string[];
10
+ maxHistorySize?: number;
11
+ }
12
+
13
+ export class RichInput {
14
+ private history: string[] = [];
15
+ private historyIndex = -1;
16
+ private currentLine = '';
17
+ private cursorPos = 0;
18
+ private multiLineBuffer: string[] = [];
19
+ private searchMode = false;
20
+ private searchQuery = '';
21
+ private ctrlCCount = 0;
22
+ private prompt: string;
23
+ private historyFile: string;
24
+ private completionProvider?: (partial: string) => string[];
25
+ private maxHistory: number;
26
+
27
+ constructor(opts: RichInputOptions = {}) {
28
+ this.prompt = opts.prompt ?? '❯ ';
29
+ this.historyFile = opts.historyFile ?? path.join(os.homedir(), '.weaver', 'input-history.txt');
30
+ this.completionProvider = opts.completionProvider;
31
+ this.maxHistory = opts.maxHistorySize ?? 500;
32
+ this.loadHistory();
33
+ }
34
+
35
+ async getInput(): Promise<string | null> {
36
+ // Non-TTY fallback
37
+ if (!process.stdin.isTTY) {
38
+ return this.getInputReadline();
39
+ }
40
+
41
+ return new Promise((resolve) => {
42
+ this.ctrlCCount = 0;
43
+ this.historyIndex = -1;
44
+ this.currentLine = '';
45
+ this.cursorPos = 0;
46
+ this.searchMode = false;
47
+
48
+ process.stdin.setRawMode(true);
49
+ process.stdin.resume();
50
+
51
+ const handler = (key: Buffer) => {
52
+ this.handleKey(key, (result) => {
53
+ process.stdin.setRawMode(false);
54
+ process.stdin.pause();
55
+ process.stdin.removeListener('data', handler);
56
+ resolve(result);
57
+ });
58
+ };
59
+
60
+ process.stdin.on('data', handler);
61
+ this.renderPrompt();
62
+ });
63
+ }
64
+
65
+ resetCtrlC(): void {
66
+ this.ctrlCCount = 0;
67
+ }
68
+
69
+ private handleKey(key: Buffer, resolve: (value: string | null) => void): void {
70
+ const s = key.toString();
71
+
72
+ // Handle search mode separately
73
+ if (this.searchMode) {
74
+ this.handleSearchKey(s, resolve);
75
+ return;
76
+ }
77
+
78
+ if (s === '\r' || s === '\n') {
79
+ this.handleEnter(resolve);
80
+ } else if (s === '\x03') { // Ctrl+C
81
+ this.ctrlCCount++;
82
+ if (this.ctrlCCount >= 2 || this.currentLine === '') {
83
+ process.stderr.write('\n');
84
+ resolve(null);
85
+ } else {
86
+ this.currentLine = '';
87
+ this.cursorPos = 0;
88
+ process.stderr.write('\n');
89
+ this.renderPrompt();
90
+ }
91
+ } else if (s === '\x0c') { // Ctrl+L
92
+ process.stderr.write('\x1b[2J\x1b[H'); // clear screen + move to top
93
+ this.renderPrompt();
94
+ } else if (s === '\x12') { // Ctrl+R
95
+ this.searchMode = true;
96
+ this.searchQuery = '';
97
+ this.renderSearchPrompt();
98
+ } else if (s === '\x09') { // Tab
99
+ this.handleTab();
100
+ } else if (s === '\x1b[A') { // Arrow Up
101
+ this.historyUp();
102
+ } else if (s === '\x1b[B') { // Arrow Down
103
+ this.historyDown();
104
+ } else if (s === '\x1b[C') { // Arrow Right
105
+ if (this.cursorPos < this.currentLine.length) {
106
+ this.cursorPos++;
107
+ process.stderr.write('\x1b[C');
108
+ }
109
+ } else if (s === '\x1b[D') { // Arrow Left
110
+ if (this.cursorPos > 0) {
111
+ this.cursorPos--;
112
+ process.stderr.write('\x1b[D');
113
+ }
114
+ } else if (s === '\x7f' || s === '\b') { // Backspace
115
+ if (this.cursorPos > 0) {
116
+ this.currentLine = this.currentLine.slice(0, this.cursorPos - 1) + this.currentLine.slice(this.cursorPos);
117
+ this.cursorPos--;
118
+ this.renderPrompt();
119
+ }
120
+ } else if (s === '\x1b[3~') { // Delete
121
+ if (this.cursorPos < this.currentLine.length) {
122
+ this.currentLine = this.currentLine.slice(0, this.cursorPos) + this.currentLine.slice(this.cursorPos + 1);
123
+ this.renderPrompt();
124
+ }
125
+ } else if (s === '\x01') { // Ctrl+A (home)
126
+ this.cursorPos = 0;
127
+ this.renderPrompt();
128
+ } else if (s === '\x05') { // Ctrl+E (end)
129
+ this.cursorPos = this.currentLine.length;
130
+ this.renderPrompt();
131
+ } else if (s === '\x15') { // Ctrl+U (clear line)
132
+ this.currentLine = '';
133
+ this.cursorPos = 0;
134
+ this.renderPrompt();
135
+ } else if (s >= ' ' && s.length === 1) { // Printable
136
+ this.ctrlCCount = 0;
137
+ this.currentLine = this.currentLine.slice(0, this.cursorPos) + s + this.currentLine.slice(this.cursorPos);
138
+ this.cursorPos++;
139
+ this.renderPrompt();
140
+ } else if (s.length > 1 && !s.startsWith('\x1b')) {
141
+ // Pasted text (multiple chars at once)
142
+ this.ctrlCCount = 0;
143
+ this.currentLine = this.currentLine.slice(0, this.cursorPos) + s + this.currentLine.slice(this.cursorPos);
144
+ this.cursorPos += s.length;
145
+ this.renderPrompt();
146
+ }
147
+ }
148
+
149
+ private handleEnter(resolve: (value: string | null) => void): void {
150
+ const fullLine = this.multiLineBuffer.length > 0
151
+ ? [...this.multiLineBuffer, this.currentLine].join('\n')
152
+ : this.currentLine;
153
+
154
+ // Check for multi-line continuation
155
+ if (this.isIncomplete(fullLine)) {
156
+ this.multiLineBuffer.push(this.currentLine);
157
+ this.currentLine = '';
158
+ this.cursorPos = 0;
159
+ process.stderr.write('\n');
160
+ process.stderr.write(' ... ');
161
+ return;
162
+ }
163
+
164
+ process.stderr.write('\n');
165
+
166
+ const trimmed = fullLine.trim();
167
+ if (trimmed) {
168
+ this.addToHistory(trimmed);
169
+ }
170
+
171
+ this.multiLineBuffer = [];
172
+ this.currentLine = '';
173
+ this.cursorPos = 0;
174
+
175
+ resolve(trimmed || null);
176
+ }
177
+
178
+ private isIncomplete(text: string): boolean {
179
+ const backticks = (text.match(/```/g) || []).length;
180
+ if (backticks % 2 !== 0) return true;
181
+ if (text.endsWith('\\')) return true;
182
+ return false;
183
+ }
184
+
185
+ private historyUp(): void {
186
+ if (this.history.length === 0) return;
187
+ if (this.historyIndex < this.history.length - 1) {
188
+ this.historyIndex++;
189
+ this.currentLine = this.history[this.history.length - 1 - this.historyIndex];
190
+ this.cursorPos = this.currentLine.length;
191
+ this.renderPrompt();
192
+ }
193
+ }
194
+
195
+ private historyDown(): void {
196
+ if (this.historyIndex > 0) {
197
+ this.historyIndex--;
198
+ this.currentLine = this.history[this.history.length - 1 - this.historyIndex];
199
+ this.cursorPos = this.currentLine.length;
200
+ this.renderPrompt();
201
+ } else if (this.historyIndex === 0) {
202
+ this.historyIndex = -1;
203
+ this.currentLine = '';
204
+ this.cursorPos = 0;
205
+ this.renderPrompt();
206
+ }
207
+ }
208
+
209
+ private handleTab(): void {
210
+ if (!this.completionProvider) return;
211
+ const candidates = this.completionProvider(this.currentLine);
212
+ if (candidates.length === 0) return;
213
+ if (candidates.length === 1) {
214
+ this.currentLine = candidates[0];
215
+ this.cursorPos = this.currentLine.length;
216
+ this.renderPrompt();
217
+ } else {
218
+ // Show candidates below, then redraw prompt
219
+ process.stderr.write('\n ' + candidates.join(' ') + '\n');
220
+ this.renderPrompt();
221
+ }
222
+ }
223
+
224
+ private handleSearchKey(s: string, resolve: (value: string | null) => void): void {
225
+ if (s === '\x1b' || s === '\x03') { // Esc or Ctrl+C — cancel search
226
+ this.searchMode = false;
227
+ this.renderPrompt();
228
+ } else if (s === '\r' || s === '\n') { // Enter — accept match
229
+ this.searchMode = false;
230
+ const match = this.searchHistory(this.searchQuery);
231
+ if (match) {
232
+ this.currentLine = match;
233
+ this.cursorPos = this.currentLine.length;
234
+ }
235
+ this.renderPrompt();
236
+ } else if (s === '\x7f' || s === '\b') { // Backspace
237
+ this.searchQuery = this.searchQuery.slice(0, -1);
238
+ this.renderSearchPrompt();
239
+ } else if (s >= ' ' && s.length === 1) {
240
+ this.searchQuery += s;
241
+ this.renderSearchPrompt();
242
+ }
243
+ }
244
+
245
+ private searchHistory(query: string): string | null {
246
+ if (!query) return null;
247
+ const lower = query.toLowerCase();
248
+ for (let i = this.history.length - 1; i >= 0; i--) {
249
+ if (this.history[i].toLowerCase().includes(lower)) return this.history[i];
250
+ }
251
+ return null;
252
+ }
253
+
254
+ private renderPrompt(): void {
255
+ process.stderr.write(`\r\x1b[K${this.prompt}${this.currentLine}`);
256
+ // Move cursor to correct position
257
+ const offset = this.currentLine.length - this.cursorPos;
258
+ if (offset > 0) process.stderr.write(`\x1b[${offset}D`);
259
+ }
260
+
261
+ private renderSearchPrompt(): void {
262
+ const match = this.searchHistory(this.searchQuery) ?? '';
263
+ process.stderr.write(`\r\x1b[K\x1b[2m(search): ${this.searchQuery}\x1b[0m ${match}`);
264
+ }
265
+
266
+ private addToHistory(line: string): void {
267
+ // Don't add duplicates of the last entry
268
+ if (this.history.length > 0 && this.history[this.history.length - 1] === line) return;
269
+ this.history.push(line);
270
+ if (this.history.length > this.maxHistory) this.history.shift();
271
+ this.saveHistory();
272
+ }
273
+
274
+ private loadHistory(): void {
275
+ try {
276
+ if (fs.existsSync(this.historyFile)) {
277
+ this.history = fs.readFileSync(this.historyFile, 'utf-8')
278
+ .split('\n')
279
+ .filter(Boolean)
280
+ .slice(-this.maxHistory);
281
+ }
282
+ } catch { /* history not available */ }
283
+ }
284
+
285
+ private saveHistory(): void {
286
+ try {
287
+ const dir = path.dirname(this.historyFile);
288
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
289
+ fs.writeFileSync(this.historyFile, this.history.join('\n') + '\n');
290
+ } catch { /* non-fatal */ }
291
+ }
292
+
293
+ private async getInputReadline(): Promise<string | null> {
294
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr, prompt: this.prompt });
295
+ return new Promise<string | null>((resolve) => {
296
+ rl.prompt();
297
+ rl.once('line', (line) => { rl.close(); resolve(line.trim() || null); });
298
+ rl.once('close', () => resolve(null));
299
+ });
300
+ }
301
+
302
+ destroy(): void {
303
+ if (process.stdin.isTTY) {
304
+ try { process.stdin.setRawMode(false); } catch {}
305
+ }
306
+ }
307
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared safety constants and checks — single source of truth.
3
+ */
4
+
5
+ export const BLOCKED_COMMANDS = ['rm -rf', 'git push', 'npm publish', 'sudo', 'curl|sh', 'wget|sh'];
6
+ export const BLOCKED_URL_PATTERN = /localhost|127\.0\.0\.1|0\.0\.0\.0|10\.\d|172\.(1[6-9]|2\d|3[01])\.|192\.168\./i;
7
+ export const MAX_READ_SIZE = 1_048_576;
8
+ export const CHARS_PER_TOKEN = 4;
9
+
10
+ export function isBlockedCommand(cmd: string): boolean {
11
+ return BLOCKED_COMMANDS.some(b => cmd.includes(b));
12
+ }
13
+
14
+ export function isBlockedUrl(url: string): boolean {
15
+ return BLOCKED_URL_PATTERN.test(url);
16
+ }