termux-dev 1.6.0 → 1.8.1

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 (99) hide show
  1. package/README.md +214 -166
  2. package/assets/banner.svg +23 -28
  3. package/assets/preview.png +0 -0
  4. package/assets/preview.svg +101 -10
  5. package/dist/cli/banner.js +3 -4
  6. package/dist/cli/clipboard-write.js +55 -0
  7. package/dist/cli/clipboard.js +182 -106
  8. package/dist/cli/colors.js +67 -0
  9. package/dist/cli/confirm.js +50 -0
  10. package/dist/cli/doctor.js +4 -4
  11. package/dist/cli/effort.js +80 -0
  12. package/dist/cli/export.js +3 -3
  13. package/dist/cli/files.js +19 -5
  14. package/dist/cli/finder.js +241 -0
  15. package/dist/cli/headless.js +23 -11
  16. package/dist/cli/index.js +1557 -744
  17. package/dist/cli/keys.js +69 -0
  18. package/dist/cli/liveblock.js +45 -0
  19. package/dist/cli/log.js +19 -0
  20. package/dist/cli/logo.js +30 -0
  21. package/dist/cli/markdown.js +174 -37
  22. package/dist/cli/mcp-panel.js +432 -0
  23. package/dist/cli/permission.js +84 -0
  24. package/dist/cli/picker.js +143 -0
  25. package/dist/cli/prompt.js +256 -56
  26. package/dist/cli/resume.js +15 -0
  27. package/dist/cli/rewind.js +51 -0
  28. package/dist/cli/runtime_errors.js +6 -0
  29. package/dist/cli/server.js +85 -24
  30. package/dist/cli/settings.js +307 -0
  31. package/dist/cli/statusline.js +1 -1
  32. package/dist/cli/theme.js +126 -108
  33. package/dist/cli/transcript.js +134 -0
  34. package/dist/cli/typeahead.js +371 -0
  35. package/dist/cli/ui.js +1022 -0
  36. package/dist/cli/updater.js +23 -21
  37. package/dist/cli/usage-panel.js +242 -0
  38. package/dist/cli/welcome.js +56 -0
  39. package/dist/cli/width.js +178 -0
  40. package/dist/core/capabilities.js +433 -0
  41. package/dist/core/catalog-util.js +27 -0
  42. package/dist/core/checkpoint.js +90 -0
  43. package/dist/core/context-stats.js +29 -0
  44. package/dist/core/cost.js +191 -0
  45. package/dist/core/formats.js +199 -0
  46. package/dist/core/history.js +51 -1
  47. package/dist/core/hooks.js +79 -0
  48. package/dist/core/i18n.js +42 -0
  49. package/dist/core/local-config.js +69 -0
  50. package/dist/core/locales/de.js +265 -0
  51. package/dist/core/locales/en.js +276 -0
  52. package/dist/core/locales/es.js +265 -0
  53. package/dist/core/locales/fr.js +265 -0
  54. package/dist/core/locales/ja.js +265 -0
  55. package/dist/core/locales/pt.js +265 -0
  56. package/dist/core/locales/ru.js +265 -0
  57. package/dist/core/locales/uk.js +265 -0
  58. package/dist/core/locales/zh.js +265 -0
  59. package/dist/core/loop.js +125 -49
  60. package/dist/core/memory.js +39 -12
  61. package/dist/core/models.js +33 -20
  62. package/dist/core/network.js +12 -11
  63. package/dist/core/notify.js +54 -0
  64. package/dist/core/planfile.js +43 -0
  65. package/dist/core/price-catalog.json +1 -0
  66. package/dist/core/prices.js +249 -0
  67. package/dist/core/reasoning-catalog.json +1 -0
  68. package/dist/core/reasoning.js +393 -0
  69. package/dist/core/redact.js +41 -0
  70. package/dist/core/secure-file.js +22 -0
  71. package/dist/core/session.js +16 -5
  72. package/dist/core/snapshot.js +14 -2
  73. package/dist/core/title.js +61 -0
  74. package/dist/core/types.js +1 -1
  75. package/dist/core/usage.js +43 -7
  76. package/dist/core/vision-catalog.json +1 -0
  77. package/dist/core/vision.js +123 -0
  78. package/dist/mcp/client.js +36 -5
  79. package/dist/mcp/http-client.js +174 -0
  80. package/dist/mcp/manager.js +330 -93
  81. package/dist/permissions/guard.js +385 -117
  82. package/dist/prompts/builder.js +46 -38
  83. package/dist/providers/anthropic.js +517 -0
  84. package/dist/providers/index.js +18 -1
  85. package/dist/providers/openai.js +138 -125
  86. package/dist/providers/shared.js +116 -0
  87. package/dist/providers/toolguard.js +101 -0
  88. package/dist/tools/bash.js +141 -51
  89. package/dist/tools/diagnostics.js +76 -48
  90. package/dist/tools/fs.js +77 -5
  91. package/dist/tools/index.js +2 -1
  92. package/dist/tools/mcp.js +72 -0
  93. package/dist/tools/packages.js +16 -1
  94. package/dist/tools/plan.js +8 -8
  95. package/dist/tools/questions.js +50 -54
  96. package/dist/tools/server.js +6 -0
  97. package/dist/tools/todo.js +20 -18
  98. package/dist/tools/web.js +143 -8
  99. package/package.json +1 -1
@@ -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 && (this.config.command.endsWith('.cmd') ||
36
- this.config.command.endsWith('.bat') ||
37
- this.config.command === 'npx' ||
38
- this.config.command === 'npm');
39
- this.process = spawn(this.config.command, this.config.args || [], {
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
+ }