osai-agent 4.0.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 (86) hide show
  1. package/LICENSE +7 -0
  2. package/package.json +72 -0
  3. package/src/agent/context.js +141 -0
  4. package/src/agent/loop/context-summary.js +196 -0
  5. package/src/agent/loop/directory-utils.js +102 -0
  6. package/src/agent/loop/local.js +196 -0
  7. package/src/agent/loop/loop-detection.js +288 -0
  8. package/src/agent/loop/stream-parser.js +515 -0
  9. package/src/agent/loop/tool-executor.js +470 -0
  10. package/src/agent/loop/verification.js +263 -0
  11. package/src/agent/loop/websocket.js +80 -0
  12. package/src/agent/prompt.js +259 -0
  13. package/src/agent/react-loop.js +697 -0
  14. package/src/agent/subagent.js +263 -0
  15. package/src/commands/config.js +53 -0
  16. package/src/commands/connect.js +190 -0
  17. package/src/commands/devices.js +121 -0
  18. package/src/commands/login.js +77 -0
  19. package/src/commands/logout.js +31 -0
  20. package/src/commands/mcp.js +258 -0
  21. package/src/commands/provider.js +633 -0
  22. package/src/commands/register.js +74 -0
  23. package/src/commands/run.js +150 -0
  24. package/src/commands/search.js +64 -0
  25. package/src/commands/session.js +57 -0
  26. package/src/commands/skills.js +54 -0
  27. package/src/commands/stop-subagent.js +58 -0
  28. package/src/index.js +208 -0
  29. package/src/llm/direct.js +317 -0
  30. package/src/memory/store.js +215 -0
  31. package/src/mock-readline.js +27 -0
  32. package/src/parser/dependencies.js +71 -0
  33. package/src/parser/markdown.js +505 -0
  34. package/src/parser/stream.js +96 -0
  35. package/src/prompts/modes/CODING.js +160 -0
  36. package/src/prompts/modes/GENERAL.js +105 -0
  37. package/src/prompts/modes/NETWORK.js +69 -0
  38. package/src/prompts/modes/SSH.js +53 -0
  39. package/src/prompts/systemPrompt.js +85 -0
  40. package/src/safety/check.js +210 -0
  41. package/src/services/crypto.js +78 -0
  42. package/src/services/executor.js +68 -0
  43. package/src/services/history.js +58 -0
  44. package/src/services/server-url.js +11 -0
  45. package/src/services/session.js +194 -0
  46. package/src/services/ssh.js +176 -0
  47. package/src/services/websocket.js +112 -0
  48. package/src/skills/loader.js +231 -0
  49. package/src/tools/browser.js +434 -0
  50. package/src/tools/local.js +1254 -0
  51. package/src/tools/mcp-client.js +209 -0
  52. package/src/tools/registry.js +132 -0
  53. package/src/tools/search-providers.js +237 -0
  54. package/src/tools/ssh.js +74 -0
  55. package/src/ui/App.js +2031 -0
  56. package/src/ui/animation.js +47 -0
  57. package/src/ui/components/AskUserDialog.js +33 -0
  58. package/src/ui/components/ConfirmationDialog.js +45 -0
  59. package/src/ui/components/DiffView.js +201 -0
  60. package/src/ui/components/Header.js +157 -0
  61. package/src/ui/components/HistoryPicker.js +130 -0
  62. package/src/ui/components/InputShell.js +22 -0
  63. package/src/ui/components/MessageHistory.js +1200 -0
  64. package/src/ui/components/ModalPanel.js +40 -0
  65. package/src/ui/components/ModePicker.js +161 -0
  66. package/src/ui/components/PlanDialog.js +48 -0
  67. package/src/ui/components/ProviderMenu.js +1095 -0
  68. package/src/ui/components/SavePicker.js +106 -0
  69. package/src/ui/components/SelectMenu.js +194 -0
  70. package/src/ui/components/SlashMenu.js +168 -0
  71. package/src/ui/components/SubagentPanel.js +138 -0
  72. package/src/ui/components/TextInputSafe.js +117 -0
  73. package/src/ui/components/TodoPanel.js +54 -0
  74. package/src/ui/components/ToolExecution.js +261 -0
  75. package/src/ui/components/TranscriptViewport.js +99 -0
  76. package/src/ui/diff.js +249 -0
  77. package/src/ui/h.js +7 -0
  78. package/src/ui/mouse-scroll.js +63 -0
  79. package/src/ui/slash-picker.js +58 -0
  80. package/src/ui/terminal.js +41 -0
  81. package/src/ui/theme.js +5 -0
  82. package/src/ui/welcome.js +12 -0
  83. package/src/utils/constants.js +231 -0
  84. package/src/utils/helpers.js +154 -0
  85. package/src/utils/logger.js +81 -0
  86. package/src/utils/sound.js +33 -0
@@ -0,0 +1,176 @@
1
+ // =============================================================================
2
+ // OS AI Agent — SSH Connection Manager
3
+ // =============================================================================
4
+ import { Client } from 'ssh2';
5
+ import { DEFAULTS } from '../utils/constants.js';
6
+ import { logger } from '../utils/logger.js';
7
+
8
+ /**
9
+ * Validate SSH private key format before connection
10
+ */
11
+ const validateSshPrivateKey = (privateKey) => {
12
+ if (!privateKey || typeof privateKey !== 'string') {
13
+ throw new Error('Invalid SSH private key: missing or not a string');
14
+ }
15
+
16
+ const trimmedKey = privateKey.trim();
17
+
18
+ // Check for common SSH private key headers
19
+ const validKeyPatterns = [
20
+ /^-----BEGIN ([A-Z ]+) PRIVATE KEY-----/,
21
+ /^-----BEGIN OPENSSH PRIVATE KEY-----/,
22
+ /^-----BEGIN RSA PRIVATE KEY-----/,
23
+ /^-----BEGIN DSA PRIVATE KEY-----/,
24
+ /^-----BEGIN EC PRIVATE KEY-----/,
25
+ /^-----BEGIN ED25519 PRIVATE KEY-----/,
26
+ /^-----BEGIN ENCRYPTED PRIVATE KEY-----/,
27
+ ];
28
+
29
+ const isValidFormat = validKeyPatterns.some(pattern => pattern.test(trimmedKey));
30
+
31
+ if (!isValidFormat) {
32
+ throw new Error('Invalid SSH private key format: must be a valid PEM-formatted private key');
33
+ }
34
+
35
+ // Check for key footer
36
+ if (!trimmedKey.includes('-----END')) {
37
+ throw new Error('Invalid SSH private key: missing END marker');
38
+ }
39
+
40
+ // Basic length check
41
+ if (trimmedKey.length < 100) {
42
+ throw new Error('Invalid SSH private key: too short to be valid');
43
+ }
44
+
45
+ if (trimmedKey.length > 100000) {
46
+ throw new Error('Invalid SSH private key: exceeds maximum length');
47
+ }
48
+
49
+ return trimmedKey;
50
+ };
51
+
52
+ const connections = new Map();
53
+ const timeouts = new Map();
54
+
55
+ export const connectSSH = async ({ host, port, username, password, privateKey }, deviceType) => {
56
+ return new Promise((resolve, reject) => {
57
+ if (!host) return reject(new Error('SSH host is required'));
58
+
59
+ const conn = new Client();
60
+ const connectPort = port || 22;
61
+ const connectConfig = {
62
+ host,
63
+ port: connectPort,
64
+ username: username || 'root',
65
+ readyTimeout: 30000,
66
+ keepaliveInterval: 30000,
67
+ keepaliveCountMax: 3,
68
+ };
69
+
70
+ if (privateKey) {
71
+ try {
72
+ const validatedKey = validateSshPrivateKey(privateKey);
73
+ connectConfig.privateKey = validatedKey;
74
+ if (password) connectConfig.passphrase = password;
75
+ } catch (keyError) {
76
+ return reject(new Error(`SSH key validation failed: ${keyError.message}`));
77
+ }
78
+ } else if (password) {
79
+ connectConfig.password = password;
80
+ } else {
81
+ return reject(new Error('No authentication method provided (need password or privateKey)'));
82
+ }
83
+
84
+ conn.on('ready', () => {
85
+ logger.info(`SSH connected: ${host}:${connectPort}`);
86
+ // Store key on the connection object for timer reset in execSSH
87
+ conn._sshKey = `${host}:${connectPort}`;
88
+ // Device-specific initialization commands
89
+ if (deviceType?.toLowerCase().includes('cisco')) {
90
+ conn.exec('terminal length 0\n', () => {});
91
+ } else if (deviceType?.toLowerCase().includes('mikrotik')) {
92
+ conn.exec(':put "connected"\n', () => {});
93
+ }
94
+ // Auto-close on inactivity
95
+ const timeoutId = setTimeout(() => {
96
+ logger.info(`SSH closed (inactivity): ${host}`);
97
+ try { conn.end(); } catch {}
98
+ connections.delete(`${host}:${connectPort}`);
99
+ }, DEFAULTS.SSH_INACTIVITY_TIMEOUT);
100
+ timeouts.set(host, timeoutId);
101
+ connections.set(`${host}:${connectPort}`, conn);
102
+ resolve(conn);
103
+ });
104
+
105
+ conn.on('error', (err) => {
106
+ logger.error(`SSH connection error: ${host}`, { error: err.message });
107
+ reject(err);
108
+ });
109
+
110
+ conn.on('close', () => {
111
+ const key = `${host}:${connectPort}`;
112
+ connections.delete(key);
113
+ const tid = timeouts.get(host);
114
+ if (tid) { clearTimeout(tid); timeouts.delete(host); }
115
+ logger.debug(`SSH closed: ${host}`);
116
+ });
117
+
118
+ conn.connect(connectConfig);
119
+ });
120
+ };
121
+
122
+ export const execSSH = (conn, cmd, timeout = DEFAULTS.SSH_COMMAND_TIMEOUT) => {
123
+ return new Promise((resolve, reject) => {
124
+ if (!conn) return reject(new Error('No SSH connection provided'));
125
+ if (!cmd) return reject(new Error('No command provided'));
126
+
127
+ // Reset inactivity timer on command execution
128
+ const fullKey = conn._sshKey;
129
+ if (fullKey) {
130
+ const host = fullKey.split(':')[0];
131
+ const old = timeouts.get(host);
132
+ if (old) clearTimeout(old);
133
+ const t = setTimeout(() => {
134
+ logger.info(`SSH closed (inactivity): ${fullKey}`);
135
+ try { conn.end(); } catch {}
136
+ connections.delete(fullKey);
137
+ timeouts.delete(host);
138
+ }, DEFAULTS.SSH_INACTIVITY_TIMEOUT);
139
+ timeouts.set(host, t);
140
+ }
141
+
142
+ const timer = setTimeout(() => {
143
+ reject(new Error(`SSH command timed out after ${timeout / 1000}s`));
144
+ }, timeout);
145
+
146
+ conn.exec(cmd, (err, stream) => {
147
+ if (err) {
148
+ clearTimeout(timer);
149
+ return reject(err);
150
+ }
151
+ let output = '';
152
+ stream.on('data', (data) => { output += data.toString(); });
153
+ stream.stderr?.on('data', (data) => { output += data.toString(); });
154
+ stream.on('close', (code) => {
155
+ clearTimeout(timer);
156
+ resolve({ output, status: code === 0 || code === null ? 'success' : 'error', exitCode: code });
157
+ });
158
+ stream.on('error', (e) => {
159
+ clearTimeout(timer);
160
+ resolve({ output: e.message, status: 'error' });
161
+ });
162
+ });
163
+ });
164
+ };
165
+
166
+ export const closeSSH = (host) => {
167
+ const conn = connections.get(host);
168
+ if (conn) {
169
+ try { conn.end(); } catch {}
170
+ connections.delete(host);
171
+ const tid = timeouts.get(host);
172
+ if (tid) { clearTimeout(tid); timeouts.delete(host); }
173
+ }
174
+ };
175
+
176
+ export const getActiveConnectionCount = () => connections.size;
@@ -0,0 +1,112 @@
1
+ // =============================================================================
2
+ // OS AI Agent — WebSocket Client (Standalone)
3
+ // =============================================================================
4
+ // Standalone WebSocket client with auto-reconnect and exponential backoff.
5
+ // Used as a lower-level utility that can be imported by the agent loop or
6
+ // other modules needing persistent WebSocket connections.
7
+ // =============================================================================
8
+ import WebSocket from 'ws';
9
+ import { DEFAULTS } from '../utils/constants.js';
10
+ import { logger } from '../utils/logger.js';
11
+
12
+ let ws = null;
13
+ let reconnectAttempts = 0;
14
+ let reconnecting = false;
15
+ const MAX_RECONNECT_DELAY = DEFAULTS.WS_RECONNECT_MAX_DELAY;
16
+
17
+ export const connectWS = async (serverUrl, token, onMessage) => {
18
+ return new Promise((resolve, reject) => {
19
+ ws = new WebSocket(`${serverUrl}/ws`);
20
+
21
+ ws.on('open', () => {
22
+ ws.send(JSON.stringify({ type: 'REGISTER', token }));
23
+ reconnectAttempts = 0;
24
+ logger.info('WebSocket connected');
25
+ });
26
+
27
+ ws.on('message', async (data) => {
28
+ try {
29
+ const msg = JSON.parse(data.toString());
30
+ await onMessage(msg);
31
+ if (msg.type === 'REGISTERED') resolve(msg);
32
+ } catch (e) {
33
+ logger.error('WebSocket message parse error', { error: e.message });
34
+ }
35
+ });
36
+
37
+ ws.on('close', (code, reason) => {
38
+ logger.warn(`WebSocket closed: ${code} ${reason}`);
39
+ if (reconnecting) return;
40
+ if (reconnectAttempts >= DEFAULTS.WS_RECONNECT_MAX_ATTEMPTS) {
41
+ logger.warn('Max WebSocket reconnection attempts reached');
42
+ return;
43
+ }
44
+ reconnecting = true;
45
+ const delay = Math.min(
46
+ DEFAULTS.WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts),
47
+ MAX_RECONNECT_DELAY
48
+ );
49
+ reconnectAttempts++;
50
+ setTimeout(() => {
51
+ reconnecting = false;
52
+ connectWS(serverUrl, token, onMessage).catch(() => {});
53
+ }, delay);
54
+ });
55
+
56
+ ws.on('error', (err) => {
57
+ logger.error('WebSocket error', { error: err.message });
58
+ reject(err);
59
+ });
60
+
61
+ ws.on('ping', () => {
62
+ try { ws.pong(); } catch {}
63
+ });
64
+ });
65
+ };
66
+
67
+ export const send = (data) => {
68
+ if (ws && ws.readyState === WebSocket.OPEN) {
69
+ ws.send(JSON.stringify(data));
70
+ } else {
71
+ logger.warn('WebSocket not connected');
72
+ }
73
+ };
74
+
75
+ export const sendCancellation = (taskId) => {
76
+ if (ws && ws.readyState === WebSocket.OPEN) {
77
+ try {
78
+ ws.send(JSON.stringify({
79
+ type: 'CANCEL',
80
+ taskId: taskId,
81
+ timestamp: Date.now()
82
+ }));
83
+ logger.info('Cancellation signal sent to server', { taskId });
84
+ return true;
85
+ } catch (error) {
86
+ logger.error('Failed to send cancellation signal', { error: error.message });
87
+ return false;
88
+ }
89
+ }
90
+ logger.warn('WebSocket not connected for cancellation');
91
+ return false;
92
+ };
93
+
94
+ export const close = () => {
95
+ if (ws) {
96
+ ws.close();
97
+ ws = null;
98
+ }
99
+ };
100
+
101
+ export const isConnected = () => ws && ws.readyState === WebSocket.OPEN;
102
+
103
+ export const getConnectionState = () => {
104
+ if (!ws) return 'disconnected';
105
+ switch (ws.readyState) {
106
+ case WebSocket.CONNECTING: return 'connecting';
107
+ case WebSocket.OPEN: return 'connected';
108
+ case WebSocket.CLOSING: return 'closing';
109
+ case WebSocket.CLOSED: return 'closed';
110
+ default: return 'unknown';
111
+ }
112
+ };
@@ -0,0 +1,231 @@
1
+ // =============================================================================
2
+ // OS AI Agent — Skills Loader
3
+ // =============================================================================
4
+ // Discovers and manages SKILL.md files from personal and project directories.
5
+ // =============================================================================
6
+
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import fg from 'fast-glob';
11
+ import { logger } from '../utils/logger.js';
12
+
13
+ const SKILL_FILENAME = 'SKILL.md';
14
+
15
+ const SKILL_ROOTS = [
16
+ { base: path.join(os.homedir(), '.osai-agent', 'skills'), source: 'personal' },
17
+ { base: path.join(process.cwd(), '.osai-agent', 'skills'), source: 'project' },
18
+ ];
19
+
20
+ let _cache = null;
21
+ let _cacheCwd = null;
22
+
23
+ function parseFrontmatter(content) {
24
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
25
+ if (!match) {
26
+ return { meta: {}, body: content.trim() };
27
+ }
28
+
29
+ const meta = {};
30
+ for (const line of match[1].split('\n')) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed || trimmed.startsWith('#')) continue;
33
+ const idx = trimmed.indexOf(':');
34
+ if (idx === -1) continue;
35
+ const key = trimmed.slice(0, idx).trim();
36
+ let value = trimmed.slice(idx + 1).trim();
37
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
38
+ value = value.slice(1, -1);
39
+ }
40
+ meta[key] = value;
41
+ }
42
+
43
+ return { meta, body: content.slice(match[0].length).trim() };
44
+ }
45
+
46
+ function skillNameFromPath(skillPath) {
47
+ return path.basename(path.dirname(skillPath));
48
+ }
49
+
50
+ function sanitizeSkillName(name) {
51
+ return String(name || '')
52
+ .trim()
53
+ .toLowerCase()
54
+ .replace(/[^a-z0-9-]+/g, '-')
55
+ .replace(/^-+|-+$/g, '')
56
+ .slice(0, 64);
57
+ }
58
+
59
+ async function fileExists(p) {
60
+ try {
61
+ await fs.access(p);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ async function findSkillFiles() {
69
+ const files = new Set();
70
+
71
+ for (const { base } of SKILL_ROOTS) {
72
+ if (!(await fileExists(base))) continue;
73
+ const matches = await fg('**/SKILL.md', {
74
+ cwd: base,
75
+ absolute: true,
76
+ onlyFiles: true,
77
+ suppressErrors: true,
78
+ });
79
+ for (const match of matches) files.add(match);
80
+ }
81
+
82
+ return [...files];
83
+ }
84
+
85
+ function sourceLabel(skillPath) {
86
+ const normalized = skillPath.replace(/\\/g, '/');
87
+ const cwd = process.cwd().replace(/\\/g, '/');
88
+ const inProject = normalized.startsWith(`${cwd}/`);
89
+
90
+ if (inProject && normalized.includes('/.osai-agent/skills/')) return 'project';
91
+ if (normalized.includes('/.osai-agent/skills/')) return 'personal';
92
+ return 'unknown';
93
+ }
94
+
95
+ function skillDirForScope(scope = 'project') {
96
+ return scope === 'personal'
97
+ ? path.join(os.homedir(), '.osai-agent', 'skills')
98
+ : path.join(process.cwd(), '.osai-agent', 'skills');
99
+ }
100
+
101
+ /**
102
+ * Discover all available skills (metadata only).
103
+ * @param {{ refresh?: boolean }} opts
104
+ */
105
+ export async function discoverSkills(opts = {}) {
106
+ if (!opts.refresh && _cache && _cacheCwd === process.cwd()) {
107
+ return _cache;
108
+ }
109
+
110
+ const skillFiles = await findSkillFiles();
111
+ const byName = new Map();
112
+
113
+ for (const skillPath of skillFiles) {
114
+ try {
115
+ const content = await fs.readFile(skillPath, 'utf-8');
116
+ const { meta } = parseFrontmatter(content);
117
+ const folderName = skillNameFromPath(skillPath);
118
+ const name = (meta.name || folderName).trim();
119
+ if (!name) continue;
120
+
121
+ const entry = {
122
+ name,
123
+ description: (meta.description || 'No description').trim(),
124
+ path: skillPath,
125
+ source: sourceLabel(skillPath),
126
+ disableModelInvocation: meta['disable-model-invocation'] === 'true',
127
+ };
128
+
129
+ const existing = byName.get(name);
130
+ if (!existing || entry.source === 'project') {
131
+ byName.set(name, entry);
132
+ }
133
+ } catch (err) {
134
+ logger.debug('Failed to read skill file', { skillPath, error: err.message });
135
+ }
136
+ }
137
+
138
+ _cache = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
139
+ _cacheCwd = process.cwd();
140
+ return _cache;
141
+ }
142
+
143
+ /**
144
+ * Load full skill content by name.
145
+ */
146
+ export async function loadSkill(name) {
147
+ const skills = await discoverSkills();
148
+ const skill = skills.find((s) => s.name === name);
149
+ if (!skill) {
150
+ return {
151
+ success: false,
152
+ output: '',
153
+ error: `Skill not found: "${name}". Use SKILL_LIST to see available skills.`,
154
+ };
155
+ }
156
+
157
+ try {
158
+ const content = await fs.readFile(skill.path, 'utf-8');
159
+ const { meta, body } = parseFrontmatter(content);
160
+ const header = [
161
+ `# Skill: ${meta.name || skill.name}`,
162
+ meta.description ? `> ${meta.description}` : '',
163
+ `Source: ${skill.source} (${skill.path})`,
164
+ '',
165
+ ].filter(Boolean).join('\n');
166
+
167
+ return {
168
+ success: true,
169
+ output: `${header}\n${body}`,
170
+ skill: skill.name,
171
+ source: skill.source,
172
+ };
173
+ } catch (err) {
174
+ return { success: false, output: '', error: err.message };
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Create or update a skill file.
180
+ */
181
+ export async function createSkill({ name, description = '', content = '', scope = 'project' }) {
182
+ const safeName = sanitizeSkillName(name);
183
+ if (!safeName) {
184
+ return { success: false, output: '', error: 'Invalid skill name. Use letters, numbers, and hyphens only.' };
185
+ }
186
+
187
+ const desc = String(description || `Workflow for ${safeName}`).trim();
188
+ const body = String(content || `# ${safeName}\n\nAdd workflow instructions here.`).trim();
189
+ const dir = path.join(skillDirForScope(scope), safeName);
190
+ const skillPath = path.join(dir, SKILL_FILENAME);
191
+
192
+ try {
193
+ await fs.mkdir(dir, { recursive: true });
194
+ const fileContent = `---\nname: ${safeName}\ndescription: ${desc}\n---\n\n${body}\n`;
195
+ const exists = await fileExists(skillPath);
196
+ await fs.writeFile(skillPath, fileContent, 'utf-8');
197
+ invalidateSkillsCache();
198
+
199
+ return {
200
+ success: true,
201
+ output: `Skill "${safeName}" ${exists ? 'updated' : 'created'} at ${skillPath}`,
202
+ path: skillPath,
203
+ name: safeName,
204
+ scope: scope === 'personal' ? 'personal' : 'project',
205
+ updated: exists,
206
+ };
207
+ } catch (err) {
208
+ return { success: false, output: '', error: `Failed to create skill: ${err.message}` };
209
+ }
210
+ }
211
+
212
+ export function formatSkillsList(skills) {
213
+ if (!skills.length) {
214
+ return 'No skills found. Create ~/.osai-agent/skills/<name>/SKILL.md or .osai-agent/skills/<name>/SKILL.md';
215
+ }
216
+ return skills.map((s) => `- ${s.name}: ${s.description} [${s.source}]`).join('\n');
217
+ }
218
+
219
+ export function formatSkillsForPrompt(skills) {
220
+ if (!skills.length) return '';
221
+ const lines = skills
222
+ .filter((s) => !s.disableModelInvocation)
223
+ .map((s) => `- **${s.name}**: ${s.description}`);
224
+ if (!lines.length) return '';
225
+ return lines.join('\n');
226
+ }
227
+
228
+ export function invalidateSkillsCache() {
229
+ _cache = null;
230
+ _cacheCwd = null;
231
+ }