termux-dev 1.5.0 → 1.8.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.
- package/README.md +215 -163
- package/assets/banner.svg +23 -28
- package/assets/preview.png +0 -0
- package/assets/preview.svg +94 -0
- package/dist/cli/banner.js +7 -3
- package/dist/cli/clipboard-write.js +55 -0
- package/dist/cli/clipboard.js +182 -106
- package/dist/cli/colors.js +67 -0
- package/dist/cli/confirm.js +50 -0
- package/dist/cli/doctor.js +4 -4
- package/dist/cli/effort.js +80 -0
- package/dist/cli/export.js +3 -3
- package/dist/cli/files.js +19 -5
- package/dist/cli/headless.js +29 -14
- package/dist/cli/index.js +1395 -474
- package/dist/cli/keys.js +69 -0
- package/dist/cli/liveblock.js +45 -0
- package/dist/cli/log.js +19 -0
- package/dist/cli/markdown.js +158 -36
- package/dist/cli/mcp-panel.js +432 -0
- package/dist/cli/permission.js +84 -0
- package/dist/cli/picker.js +135 -0
- package/dist/cli/prompt.js +338 -121
- package/dist/cli/prompt_palette.js +6 -0
- package/dist/cli/resume.js +15 -0
- package/dist/cli/rewind.js +51 -0
- package/dist/cli/runtime_errors.js +6 -0
- package/dist/cli/server.js +85 -24
- package/dist/cli/settings.js +307 -0
- package/dist/cli/statusline.js +27 -1
- package/dist/cli/theme.js +126 -108
- package/dist/cli/transcript.js +114 -0
- package/dist/cli/typeahead.js +371 -0
- package/dist/cli/ui.js +1021 -0
- package/dist/cli/updater.js +23 -21
- package/dist/cli/usage-panel.js +242 -0
- package/dist/cli/width.js +178 -0
- package/dist/core/capabilities.js +433 -0
- package/dist/core/catalog-util.js +27 -0
- package/dist/core/checkpoint.js +90 -0
- package/dist/core/context-stats.js +29 -0
- package/dist/core/cost.js +191 -0
- package/dist/core/formats.js +199 -0
- package/dist/core/history.js +51 -1
- package/dist/core/hooks.js +79 -0
- package/dist/core/i18n.js +42 -0
- package/dist/core/local-config.js +69 -0
- package/dist/core/locales/de.js +210 -0
- package/dist/core/locales/en.js +221 -0
- package/dist/core/locales/es.js +210 -0
- package/dist/core/locales/fr.js +210 -0
- package/dist/core/locales/ja.js +210 -0
- package/dist/core/locales/pt.js +210 -0
- package/dist/core/locales/ru.js +210 -0
- package/dist/core/locales/uk.js +210 -0
- package/dist/core/locales/zh.js +210 -0
- package/dist/core/loop.js +125 -49
- package/dist/core/memory.js +39 -12
- package/dist/core/models.js +33 -20
- package/dist/core/network.js +12 -11
- package/dist/core/notify.js +54 -0
- package/dist/core/planfile.js +43 -0
- package/dist/core/price-catalog.json +1 -0
- package/dist/core/prices.js +249 -0
- package/dist/core/reasoning-catalog.json +1 -0
- package/dist/core/reasoning.js +393 -0
- package/dist/core/redact.js +41 -0
- package/dist/core/secure-file.js +22 -0
- package/dist/core/session.js +16 -5
- package/dist/core/snapshot.js +14 -2
- package/dist/core/title.js +61 -0
- package/dist/core/types.js +1 -1
- package/dist/core/usage.js +43 -7
- package/dist/core/vision-catalog.json +1 -0
- package/dist/core/vision.js +123 -0
- package/dist/mcp/client.js +36 -5
- package/dist/mcp/http-client.js +174 -0
- package/dist/mcp/manager.js +330 -93
- package/dist/permissions/guard.js +385 -117
- package/dist/prompts/builder.js +46 -38
- package/dist/providers/anthropic.js +517 -0
- package/dist/providers/index.js +18 -1
- package/dist/providers/openai.js +138 -125
- package/dist/providers/shared.js +116 -0
- package/dist/providers/toolguard.js +101 -0
- package/dist/tools/bash.js +141 -51
- package/dist/tools/diagnostics.js +76 -48
- package/dist/tools/fs.js +77 -5
- package/dist/tools/index.js +2 -1
- package/dist/tools/mcp.js +72 -0
- package/dist/tools/packages.js +16 -1
- package/dist/tools/plan.js +8 -8
- package/dist/tools/questions.js +50 -54
- package/dist/tools/server.js +6 -0
- package/dist/tools/todo.js +20 -18
- package/dist/tools/web.js +143 -8
- package/package.json +4 -2
- package/assets/preview_work1.png +0 -0
package/dist/mcp/client.js
CHANGED
|
@@ -9,6 +9,7 @@ export class MCPClient {
|
|
|
9
9
|
buffer = '';
|
|
10
10
|
tools = [];
|
|
11
11
|
isConnected = false;
|
|
12
|
+
capabilities = [];
|
|
12
13
|
constructor(name, config) {
|
|
13
14
|
this.name = name;
|
|
14
15
|
this.config = config;
|
|
@@ -17,6 +18,9 @@ export class MCPClient {
|
|
|
17
18
|
if (this.config.disabled) {
|
|
18
19
|
throw new Error(`MCP server "${this.name}" is disabled in configuration.`);
|
|
19
20
|
}
|
|
21
|
+
const program = this.config.command;
|
|
22
|
+
if (!program)
|
|
23
|
+
throw new Error(`MCP server "${this.name}" has no command.`);
|
|
20
24
|
return new Promise(async (resolve, reject) => {
|
|
21
25
|
let isSettled = false;
|
|
22
26
|
const initialTimer = setTimeout(() => {
|
|
@@ -32,11 +36,11 @@ export class MCPClient {
|
|
|
32
36
|
...(this.config.env || {})
|
|
33
37
|
};
|
|
34
38
|
const isWindows = process.platform === 'win32';
|
|
35
|
-
const useShell = isWindows && (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.process = spawn(
|
|
39
|
+
const useShell = isWindows && (program.endsWith('.cmd') ||
|
|
40
|
+
program.endsWith('.bat') ||
|
|
41
|
+
program === 'npx' ||
|
|
42
|
+
program === 'npm');
|
|
43
|
+
this.process = spawn(program, this.config.args || [], {
|
|
40
44
|
env,
|
|
41
45
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
42
46
|
shell: useShell
|
|
@@ -71,6 +75,7 @@ export class MCPClient {
|
|
|
71
75
|
throw new Error(`Invalid initialize response from MCP server "${this.name}".`);
|
|
72
76
|
}
|
|
73
77
|
// 2. Send initialized notification
|
|
78
|
+
this.capabilities = Object.keys(initResult.capabilities || {});
|
|
74
79
|
this.sendNotification('notifications/initialized', {});
|
|
75
80
|
// 3. Fetch Tools List
|
|
76
81
|
const toolsResult = await this.sendRequest('tools/list', {});
|
|
@@ -95,6 +100,10 @@ export class MCPClient {
|
|
|
95
100
|
getTools() {
|
|
96
101
|
return this.tools;
|
|
97
102
|
}
|
|
103
|
+
/** The capability groups the server announced in the handshake (tools, resources, prompts, ...). */
|
|
104
|
+
getCapabilities() {
|
|
105
|
+
return this.capabilities;
|
|
106
|
+
}
|
|
98
107
|
hasConnected() {
|
|
99
108
|
return this.isConnected;
|
|
100
109
|
}
|
|
@@ -137,6 +146,16 @@ export class MCPClient {
|
|
|
137
146
|
continue;
|
|
138
147
|
try {
|
|
139
148
|
const msg = JSON.parse(trimmed);
|
|
149
|
+
if (!msg || typeof msg !== 'object')
|
|
150
|
+
continue;
|
|
151
|
+
// Messages carrying a "method" come FROM the server: a request (has an id)
|
|
152
|
+
// or a notification. Their ids live in the server's own id space and must
|
|
153
|
+
// never be matched against our pending requests.
|
|
154
|
+
if (typeof msg.method === 'string') {
|
|
155
|
+
if (msg.id !== undefined && msg.id !== null)
|
|
156
|
+
this.answerServerRequest(msg);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
140
159
|
if ('id' in msg && msg.id !== undefined) {
|
|
141
160
|
const pending = this.pendingRequests.get(msg.id);
|
|
142
161
|
if (pending) {
|
|
@@ -156,6 +175,18 @@ export class MCPClient {
|
|
|
156
175
|
}
|
|
157
176
|
}
|
|
158
177
|
}
|
|
178
|
+
/** Replies to requests initiated by the server (we only support `ping`). */
|
|
179
|
+
answerServerRequest(msg) {
|
|
180
|
+
if (!this.process || !this.process.stdin)
|
|
181
|
+
return;
|
|
182
|
+
const reply = msg.method === 'ping'
|
|
183
|
+
? { jsonrpc: '2.0', id: msg.id, result: {} }
|
|
184
|
+
: { jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } };
|
|
185
|
+
try {
|
|
186
|
+
this.process.stdin.write(JSON.stringify(reply) + '\n');
|
|
187
|
+
}
|
|
188
|
+
catch { }
|
|
189
|
+
}
|
|
159
190
|
sendRequest(method, params, timeoutMs = 15000) {
|
|
160
191
|
return new Promise((resolve, reject) => {
|
|
161
192
|
if (!this.process || !this.process.stdin) {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { DEVX_VERSION } from '../core/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A remote MCP server over HTTP ("streamable HTTP": a POST per message, the answer is JSON or an
|
|
4
|
+
* event stream). Configured with `{ "url": "https://…", "headers": { "Authorization": "Bearer ${TOKEN}" } }`;
|
|
5
|
+
* `${NAME}` in a header is taken from the environment, so a token need not be written into the file.
|
|
6
|
+
* A server that needs OAuth cannot be used this way: reach it through `npx -y mcp-remote <url>`.
|
|
7
|
+
*/
|
|
8
|
+
export class HttpMCPClient {
|
|
9
|
+
fetchImpl;
|
|
10
|
+
name;
|
|
11
|
+
url;
|
|
12
|
+
headers;
|
|
13
|
+
sessionId = null;
|
|
14
|
+
nextId = 1;
|
|
15
|
+
tools = [];
|
|
16
|
+
capabilities = [];
|
|
17
|
+
connected = false;
|
|
18
|
+
protocolVersion = '2025-03-26';
|
|
19
|
+
constructor(name, config, fetchImpl = fetch) {
|
|
20
|
+
this.fetchImpl = fetchImpl;
|
|
21
|
+
this.name = name;
|
|
22
|
+
this.url = String(config.url);
|
|
23
|
+
this.headers = {};
|
|
24
|
+
for (const [key, value] of Object.entries(config.headers ?? {})) {
|
|
25
|
+
this.headers[key] = String(value).replace(/\$\{(\w+)\}/g, (_m, v) => process.env[v] ?? '');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async post(message, timeoutMs) {
|
|
29
|
+
const response = await this.fetchImpl(this.url, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'content-type': 'application/json',
|
|
33
|
+
accept: 'application/json, text/event-stream',
|
|
34
|
+
'mcp-protocol-version': this.protocolVersion,
|
|
35
|
+
...(this.sessionId ? { 'mcp-session-id': this.sessionId } : {}),
|
|
36
|
+
...this.headers,
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify(message),
|
|
39
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
40
|
+
});
|
|
41
|
+
const session = response.headers.get('mcp-session-id');
|
|
42
|
+
if (session)
|
|
43
|
+
this.sessionId = session;
|
|
44
|
+
if (response.status === 401 || response.status === 403) {
|
|
45
|
+
throw new Error(`MCP server "${this.name}" refused the request (${response.status}). It needs a token (set "headers") or OAuth: use "npx -y mcp-remote ${this.url}" as the command instead.`);
|
|
46
|
+
}
|
|
47
|
+
if (!response.ok && response.status !== 202) {
|
|
48
|
+
throw new Error(`MCP server "${this.name}" answered ${response.status} ${response.statusText}`.trim());
|
|
49
|
+
}
|
|
50
|
+
if (!('id' in message))
|
|
51
|
+
return null; // a notification: nothing to read
|
|
52
|
+
const type = response.headers.get('content-type') || '';
|
|
53
|
+
if (type.includes('text/event-stream'))
|
|
54
|
+
return this.readStream(response, message.id);
|
|
55
|
+
const body = await response.json();
|
|
56
|
+
return this.unwrap(body);
|
|
57
|
+
}
|
|
58
|
+
unwrap(body) {
|
|
59
|
+
if (body?.error)
|
|
60
|
+
throw new Error(`MCP error ${body.error.code}: ${body.error.message}`);
|
|
61
|
+
return body?.result;
|
|
62
|
+
}
|
|
63
|
+
/** Reads the events until the answer to our request arrives. */
|
|
64
|
+
async readStream(response, id) {
|
|
65
|
+
const reader = response.body?.getReader();
|
|
66
|
+
if (!reader)
|
|
67
|
+
throw new Error(`MCP server "${this.name}" sent an empty stream`);
|
|
68
|
+
const decoder = new TextDecoder();
|
|
69
|
+
let buffer = '';
|
|
70
|
+
try {
|
|
71
|
+
for (;;) {
|
|
72
|
+
const { value, done } = await reader.read();
|
|
73
|
+
if (done)
|
|
74
|
+
break;
|
|
75
|
+
buffer += decoder.decode(value, { stream: true });
|
|
76
|
+
let end;
|
|
77
|
+
while ((end = buffer.search(/\r?\n\r?\n/)) !== -1) {
|
|
78
|
+
const event = buffer.slice(0, end);
|
|
79
|
+
buffer = buffer.slice(end).replace(/^\r?\n\r?\n/, '');
|
|
80
|
+
const data = event.split(/\r?\n/).filter((l) => l.startsWith('data:')).map((l) => l.slice(5).replace(/^ /, '')).join('\n');
|
|
81
|
+
if (!data)
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
const msg = JSON.parse(data);
|
|
85
|
+
if (msg && msg.id === id && ('result' in msg || 'error' in msg))
|
|
86
|
+
return this.unwrap(msg);
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
try {
|
|
94
|
+
await reader.cancel();
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`MCP server "${this.name}" ended the stream without an answer`);
|
|
99
|
+
}
|
|
100
|
+
request(method, params, timeoutMs = 15000) {
|
|
101
|
+
return this.post({ jsonrpc: '2.0', id: this.nextId++, method, params }, timeoutMs);
|
|
102
|
+
}
|
|
103
|
+
async start() {
|
|
104
|
+
try {
|
|
105
|
+
const init = await this.request('initialize', {
|
|
106
|
+
protocolVersion: this.protocolVersion,
|
|
107
|
+
capabilities: {},
|
|
108
|
+
clientInfo: { name: 'devx', version: DEVX_VERSION },
|
|
109
|
+
});
|
|
110
|
+
if (!init)
|
|
111
|
+
throw new Error(`Invalid initialize response from MCP server "${this.name}".`);
|
|
112
|
+
if (typeof init.protocolVersion === 'string')
|
|
113
|
+
this.protocolVersion = init.protocolVersion;
|
|
114
|
+
this.capabilities = Object.keys(init.capabilities || {});
|
|
115
|
+
await this.post({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }, 15000);
|
|
116
|
+
const tools = [];
|
|
117
|
+
let cursor;
|
|
118
|
+
for (let page = 0; page < 20; page++) {
|
|
119
|
+
const res = await this.request('tools/list', cursor ? { cursor } : {});
|
|
120
|
+
tools.push(...(res?.tools || []));
|
|
121
|
+
cursor = res?.nextCursor;
|
|
122
|
+
if (!cursor)
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
this.tools = tools;
|
|
126
|
+
this.connected = true;
|
|
127
|
+
return tools;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
this.close();
|
|
131
|
+
if (err?.name === 'TimeoutError' || err?.name === 'AbortError')
|
|
132
|
+
throw new Error(`MCP server "${this.name}" did not answer in time.`);
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
getTools() {
|
|
137
|
+
return this.tools;
|
|
138
|
+
}
|
|
139
|
+
getCapabilities() {
|
|
140
|
+
return this.capabilities;
|
|
141
|
+
}
|
|
142
|
+
hasConnected() {
|
|
143
|
+
return this.connected;
|
|
144
|
+
}
|
|
145
|
+
async callTool(toolName, args) {
|
|
146
|
+
if (!this.connected)
|
|
147
|
+
throw new Error(`MCP server "${this.name}" is not connected.`);
|
|
148
|
+
const res = await this.request('tools/call', { name: toolName, arguments: args || {} }, 60000);
|
|
149
|
+
if (!res || !res.content)
|
|
150
|
+
return JSON.stringify(res || {});
|
|
151
|
+
const parts = [];
|
|
152
|
+
for (const c of res.content) {
|
|
153
|
+
if (c.type === 'text' && c.text)
|
|
154
|
+
parts.push(c.text);
|
|
155
|
+
else if (c.type === 'image' && c.data)
|
|
156
|
+
parts.push(`[Image content (${c.mimeType || 'image/png'})]`);
|
|
157
|
+
else if (c.type === 'resource')
|
|
158
|
+
parts.push(`[Resource: ${JSON.stringify(c)}]`);
|
|
159
|
+
}
|
|
160
|
+
const text = parts.join('\n\n') || JSON.stringify(res);
|
|
161
|
+
if (res.isError)
|
|
162
|
+
throw new Error(text);
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
close() {
|
|
166
|
+
this.connected = false;
|
|
167
|
+
if (this.sessionId) {
|
|
168
|
+
// The session is ended politely; whether the server cares is its own business.
|
|
169
|
+
const id = this.sessionId;
|
|
170
|
+
this.sessionId = null;
|
|
171
|
+
void this.fetchImpl(this.url, { method: 'DELETE', headers: { 'mcp-session-id': id, ...this.headers }, signal: AbortSignal.timeout(3000) }).catch(() => { });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|