termux-dev 1.6.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.
Files changed (96) 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 +92 -10
  5. package/dist/cli/banner.js +1 -1
  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/headless.js +23 -11
  15. package/dist/cli/index.js +1377 -480
  16. package/dist/cli/keys.js +69 -0
  17. package/dist/cli/liveblock.js +45 -0
  18. package/dist/cli/log.js +19 -0
  19. package/dist/cli/markdown.js +158 -36
  20. package/dist/cli/mcp-panel.js +432 -0
  21. package/dist/cli/permission.js +84 -0
  22. package/dist/cli/picker.js +135 -0
  23. package/dist/cli/prompt.js +256 -56
  24. package/dist/cli/resume.js +15 -0
  25. package/dist/cli/rewind.js +51 -0
  26. package/dist/cli/runtime_errors.js +6 -0
  27. package/dist/cli/server.js +85 -24
  28. package/dist/cli/settings.js +307 -0
  29. package/dist/cli/statusline.js +1 -1
  30. package/dist/cli/theme.js +126 -108
  31. package/dist/cli/transcript.js +114 -0
  32. package/dist/cli/typeahead.js +371 -0
  33. package/dist/cli/ui.js +1021 -0
  34. package/dist/cli/updater.js +23 -21
  35. package/dist/cli/usage-panel.js +242 -0
  36. package/dist/cli/width.js +178 -0
  37. package/dist/core/capabilities.js +433 -0
  38. package/dist/core/catalog-util.js +27 -0
  39. package/dist/core/checkpoint.js +90 -0
  40. package/dist/core/context-stats.js +29 -0
  41. package/dist/core/cost.js +191 -0
  42. package/dist/core/formats.js +199 -0
  43. package/dist/core/history.js +51 -1
  44. package/dist/core/hooks.js +79 -0
  45. package/dist/core/i18n.js +42 -0
  46. package/dist/core/local-config.js +69 -0
  47. package/dist/core/locales/de.js +210 -0
  48. package/dist/core/locales/en.js +221 -0
  49. package/dist/core/locales/es.js +210 -0
  50. package/dist/core/locales/fr.js +210 -0
  51. package/dist/core/locales/ja.js +210 -0
  52. package/dist/core/locales/pt.js +210 -0
  53. package/dist/core/locales/ru.js +210 -0
  54. package/dist/core/locales/uk.js +210 -0
  55. package/dist/core/locales/zh.js +210 -0
  56. package/dist/core/loop.js +125 -49
  57. package/dist/core/memory.js +39 -12
  58. package/dist/core/models.js +33 -20
  59. package/dist/core/network.js +12 -11
  60. package/dist/core/notify.js +54 -0
  61. package/dist/core/planfile.js +43 -0
  62. package/dist/core/price-catalog.json +1 -0
  63. package/dist/core/prices.js +249 -0
  64. package/dist/core/reasoning-catalog.json +1 -0
  65. package/dist/core/reasoning.js +393 -0
  66. package/dist/core/redact.js +41 -0
  67. package/dist/core/secure-file.js +22 -0
  68. package/dist/core/session.js +16 -5
  69. package/dist/core/snapshot.js +14 -2
  70. package/dist/core/title.js +61 -0
  71. package/dist/core/types.js +1 -1
  72. package/dist/core/usage.js +43 -7
  73. package/dist/core/vision-catalog.json +1 -0
  74. package/dist/core/vision.js +123 -0
  75. package/dist/mcp/client.js +36 -5
  76. package/dist/mcp/http-client.js +174 -0
  77. package/dist/mcp/manager.js +330 -93
  78. package/dist/permissions/guard.js +385 -117
  79. package/dist/prompts/builder.js +46 -38
  80. package/dist/providers/anthropic.js +517 -0
  81. package/dist/providers/index.js +18 -1
  82. package/dist/providers/openai.js +138 -125
  83. package/dist/providers/shared.js +116 -0
  84. package/dist/providers/toolguard.js +101 -0
  85. package/dist/tools/bash.js +141 -51
  86. package/dist/tools/diagnostics.js +76 -48
  87. package/dist/tools/fs.js +77 -5
  88. package/dist/tools/index.js +2 -1
  89. package/dist/tools/mcp.js +72 -0
  90. package/dist/tools/packages.js +16 -1
  91. package/dist/tools/plan.js +8 -8
  92. package/dist/tools/questions.js +50 -54
  93. package/dist/tools/server.js +6 -0
  94. package/dist/tools/todo.js +20 -18
  95. package/dist/tools/web.js +143 -8
  96. package/package.json +1 -1
@@ -0,0 +1,249 @@
1
+ import fsp from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { NATIVE_PROVIDERS, devxDir, normalizeModelKey, readJsonFile } from './catalog-util.js';
5
+ import { ensureSecureDir } from './secure-file.js';
6
+ import { computeCost, isFreeModel, reportedCost, } from './cost.js';
7
+ /**
8
+ * Prices of models: where they come from and how a request is priced.
9
+ *
10
+ * The models.dev catalogue lists the price (USD per 1M tokens: input, output, cache read and
11
+ * write, long-context tiers) of thousands of models. A snapshot ships with devx and
12
+ * `/effort refresh` downloads a fresh one together with the reasoning data. The user's own
13
+ * provider is asked too (see capabilities.ts: model lists of OpenRouter and LiteLLM carry prices).
14
+ */
15
+ export const PRICE_URL = 'https://models.dev/api.json';
16
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null);
17
+ function rowFromCost(cost) {
18
+ if (!cost || typeof cost !== 'object')
19
+ return null;
20
+ const input = num(cost.input);
21
+ const output = num(cost.output);
22
+ if (input === null || output === null)
23
+ return null;
24
+ const row = [input, output, num(cost.cache_read), num(cost.cache_write), num(cost.reasoning)];
25
+ const tier = Array.isArray(cost.tiers)
26
+ ? cost.tiers.find((t) => t?.tier?.type === 'context' && num(t.tier.size) !== null)
27
+ : null;
28
+ if (tier) {
29
+ const tierInput = num(tier.input);
30
+ const tierOutput = num(tier.output);
31
+ if (tierInput !== null && tierOutput !== null) {
32
+ row.push(tier.tier.size, tierInput, tierOutput, num(tier.cache_read), num(tier.cache_write));
33
+ }
34
+ }
35
+ while (row.length > 2 && row[row.length - 1] === null)
36
+ row.pop();
37
+ return row;
38
+ }
39
+ /** Reduces the models.dev API response to the prices of each model. */
40
+ export function slimPrices(api, now = new Date()) {
41
+ const rows = [];
42
+ const rowIndex = new Map();
43
+ const models = {};
44
+ for (const [providerId, provider] of Object.entries(api || {})) {
45
+ for (const [modelId, model] of Object.entries(provider?.models || {})) {
46
+ const row = rowFromCost(model?.cost);
47
+ if (!row)
48
+ continue;
49
+ const key = JSON.stringify(row);
50
+ let index = rowIndex.get(key);
51
+ if (index === undefined) {
52
+ index = rows.length;
53
+ rows.push(row);
54
+ rowIndex.set(key, index);
55
+ }
56
+ (models[providerId] ||= {})[modelId.toLowerCase()] = index;
57
+ }
58
+ }
59
+ return { version: 1, generated: now.toISOString().slice(0, 10), source: PRICE_URL, rows, models };
60
+ }
61
+ function ratesFromRow(row) {
62
+ const rates = { input: row[0] ?? 0, output: row[1] ?? 0 };
63
+ if (row[2] != null)
64
+ rates.cacheRead = row[2];
65
+ if (row[3] != null)
66
+ rates.cacheWrite = row[3];
67
+ if (row[4] != null)
68
+ rates.reasoning = row[4];
69
+ if (row[5] != null && row[6] != null && row[7] != null) {
70
+ rates.tier = { over: row[5], input: row[6], output: row[7] };
71
+ if (row[8] != null)
72
+ rates.tier.cacheRead = row[8];
73
+ if (row[9] != null)
74
+ rates.tier.cacheWrite = row[9];
75
+ }
76
+ return rates;
77
+ }
78
+ export class PriceCatalog {
79
+ byProvider = new Map();
80
+ normalized = new Map();
81
+ byName = new Map();
82
+ generated;
83
+ size;
84
+ constructor(file) {
85
+ this.generated = file?.generated ?? '';
86
+ let size = 0;
87
+ if (file && file.version === 1) {
88
+ for (const [provider, models] of Object.entries(file.models)) {
89
+ const raw = new Map();
90
+ const norm = new Map();
91
+ for (const [id, rowIndex] of Object.entries(models)) {
92
+ const row = file.rows[rowIndex];
93
+ if (!row)
94
+ continue;
95
+ const rates = ratesFromRow(row);
96
+ const key = normalizeModelKey(id);
97
+ raw.set(id, rates);
98
+ norm.set(key, rates);
99
+ // A zero price in some provider's list is a free tier of that provider, not the price
100
+ // of the model: it must not be mistaken for the price somewhere else.
101
+ if (rates.input + rates.output > 0) {
102
+ const list = this.byName.get(key) ?? [];
103
+ list.push({ provider, rates });
104
+ this.byName.set(key, list);
105
+ }
106
+ size++;
107
+ }
108
+ this.byProvider.set(provider, raw);
109
+ this.normalized.set(provider, norm);
110
+ }
111
+ }
112
+ this.size = size;
113
+ }
114
+ /** The price of a model: on the user's own provider first, then at its maker, then the middle of the rest. */
115
+ lookup(provider, model) {
116
+ const id = model.toLowerCase().trim();
117
+ const key = normalizeModelKey(model);
118
+ if (!key)
119
+ return undefined;
120
+ if (provider) {
121
+ const exact = this.byProvider.get(provider)?.get(id) ?? this.normalized.get(provider)?.get(key);
122
+ if (exact)
123
+ return { rates: exact, provider, own: true };
124
+ }
125
+ const candidates = this.byName.get(key);
126
+ if (!candidates || candidates.length === 0)
127
+ return undefined;
128
+ const rank = (p) => {
129
+ const i = NATIVE_PROVIDERS.indexOf(p);
130
+ return i === -1 ? NATIVE_PROVIDERS.length : i;
131
+ };
132
+ const sorted = [...candidates].sort((a, b) => rank(a.provider) - rank(b.provider));
133
+ if (rank(sorted[0].provider) < NATIVE_PROVIDERS.length) {
134
+ return { rates: sorted[0].rates, provider: sorted[0].provider, own: false };
135
+ }
136
+ // Only resellers know the model: take the middle price, not the lowest or the highest.
137
+ const byPrice = [...candidates].sort((a, b) => a.rates.input + a.rates.output - (b.rates.input + b.rates.output));
138
+ const mid = byPrice[Math.floor((byPrice.length - 1) / 2)];
139
+ return { rates: mid.rates, provider: mid.provider, own: false };
140
+ }
141
+ }
142
+ let bundled = null;
143
+ let fresher = null;
144
+ /** The snapshot shipped with devx (scripts/build-reasoning-catalog.mjs creates it). */
145
+ export function bundledPrices() {
146
+ if (bundled)
147
+ return bundled;
148
+ let file = null;
149
+ try {
150
+ const here = path.dirname(fileURLToPath(import.meta.url));
151
+ file = readJsonFile(path.join(here, 'price-catalog.json'));
152
+ }
153
+ catch { }
154
+ file ??= readJsonFile(path.join(process.cwd(), 'src/core/price-catalog.json'));
155
+ bundled = new PriceCatalog(file);
156
+ return bundled;
157
+ }
158
+ /** A catalogue downloaded earlier with /effort refresh; it wins over the shipped snapshot. */
159
+ export function loadUserPrices(dir = devxDir()) {
160
+ const file = readJsonFile(path.join(dir, 'price-catalog.json'));
161
+ fresher = file ? new PriceCatalog(file) : null;
162
+ return fresher;
163
+ }
164
+ /** For tests: replace the catalogues. */
165
+ export function setPriceCatalogs(shipped, downloaded = null) {
166
+ bundled = shipped;
167
+ fresher = downloaded;
168
+ }
169
+ export function priceCatalogInfo() {
170
+ const active = fresher && fresher.size > 0 ? fresher : bundledPrices();
171
+ return { generated: active.generated, models: active.size, downloaded: active === fresher };
172
+ }
173
+ /** Saves a downloaded catalogue and uses it from now on. */
174
+ export async function storePrices(file, dir = devxDir()) {
175
+ await ensureSecureDir(dir);
176
+ const target = path.join(dir, 'price-catalog.json');
177
+ await fsp.writeFile(target + '.tmp', JSON.stringify(file), 'utf8');
178
+ await fsp.rename(target + '.tmp', target);
179
+ fresher = new PriceCatalog(file);
180
+ }
181
+ let endpointPrice = null;
182
+ export function setEndpointPriceLookup(lookup) {
183
+ endpointPrice = lookup;
184
+ }
185
+ // A vendor's own endpoint: the catalogue is its price list, so the number is not a guess.
186
+ const FIRST_PARTY_HOSTS = {
187
+ 'api.openai.com': 'openai',
188
+ 'api.anthropic.com': 'anthropic',
189
+ 'api.deepseek.com': 'deepseek',
190
+ 'generativelanguage.googleapis.com': 'google',
191
+ 'openrouter.ai': 'openrouter',
192
+ };
193
+ function catalogProviderOf(ctx) {
194
+ let host = '';
195
+ try {
196
+ host = new URL(ctx.baseUrl || '').hostname.toLowerCase();
197
+ }
198
+ catch { }
199
+ return FIRST_PARTY_HOSTS[host] ?? (host.endsWith('openrouter.ai') ? 'openrouter' : ctx.provider);
200
+ }
201
+ function configuredRates(ctx) {
202
+ const table = ctx.prices;
203
+ if (!table || typeof table !== 'object')
204
+ return undefined;
205
+ const id = ctx.model.trim();
206
+ const entry = table[id] ?? table[id.toLowerCase()];
207
+ if (!entry || typeof entry.input !== 'number' || typeof entry.output !== 'number')
208
+ return undefined;
209
+ const rates = { input: entry.input, output: entry.output };
210
+ if (typeof entry.cacheRead === 'number')
211
+ rates.cacheRead = entry.cacheRead;
212
+ if (typeof entry.cacheWrite === 'number')
213
+ rates.cacheWrite = entry.cacheWrite;
214
+ return rates;
215
+ }
216
+ /** What is known about the price of the model: the user's config, the endpoint, the catalogue. */
217
+ export function resolvePrice(ctx) {
218
+ const configured = configuredRates(ctx);
219
+ if (configured)
220
+ return { rates: configured, source: 'configured' };
221
+ if (isFreeModel(ctx.model, ctx.baseUrl))
222
+ return { rates: { input: 0, output: 0 }, source: 'free' };
223
+ const listed = endpointPrice?.(ctx.baseUrl || '', ctx.model);
224
+ if (listed)
225
+ return { rates: listed, source: 'listed' };
226
+ const provider = catalogProviderOf(ctx);
227
+ const hit = (fresher?.lookup(provider, ctx.model)) ?? bundledPrices().lookup(provider, ctx.model);
228
+ if (!hit)
229
+ return undefined;
230
+ const firstParty = !!provider && Object.values(FIRST_PARTY_HOSTS).includes(provider);
231
+ return { rates: hit.rates, source: hit.own && firstParty ? 'listed' : 'estimated' };
232
+ }
233
+ /**
234
+ * The cost of one request. The provider's own number wins; otherwise the tokens are priced with
235
+ * the best known rates. A reported 0 for a model that is known to cost money is not believed.
236
+ */
237
+ export function priceUsage(ctx, counts, rawUsage) {
238
+ const info = resolvePrice(ctx);
239
+ const reported = reportedCost(rawUsage);
240
+ const tokens = (counts.promptTokens || 0) + (counts.completionTokens || 0);
241
+ if (reported !== undefined) {
242
+ const suspicious = reported === 0 && tokens > 0 && !!info && info.source !== 'free' && info.rates.input + info.rates.output > 0;
243
+ if (!suspicious)
244
+ return { cost: reported, costSource: reported === 0 && info?.source === 'free' ? 'free' : 'reported' };
245
+ }
246
+ if (!info)
247
+ return {};
248
+ return { cost: computeCost(info.rates, counts), costSource: info.source };
249
+ }