c0de-agent 1.7.0 → 1.9.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/dist/core/config.js +4 -1
- package/dist/core/loop/compaction.d.ts +31 -0
- package/dist/core/loop/compaction.js +137 -0
- package/dist/core/loop/persist.d.ts +11 -0
- package/dist/core/loop/persist.js +107 -0
- package/dist/core/loop/segment.d.ts +8 -0
- package/dist/core/loop/segment.js +72 -0
- package/dist/core/loop/stream-collect.d.ts +47 -0
- package/dist/core/loop/stream-collect.js +183 -0
- package/dist/core/loop/subagent.d.ts +12 -0
- package/dist/core/loop/subagent.js +171 -0
- package/dist/core/loop/todo.d.ts +5 -0
- package/dist/core/loop/todo.js +26 -0
- package/dist/core/loop.d.ts +2 -20
- package/dist/core/loop.js +12 -658
- package/dist/core/prompt-registry.d.ts +1 -1
- package/dist/core/prompt-registry.js +10 -0
- package/dist/core/slash.js +1 -1
- package/dist/core/todo-tags.d.ts +46 -0
- package/dist/core/todo-tags.js +192 -0
- package/dist/core/workflows/builtins.js +4 -4
- package/dist/core/workflows/discovery.js +4 -1
- package/dist/llm/registry.d.ts +30 -3
- package/dist/llm/registry.js +25 -7
- package/dist/llm/retry.d.ts +4 -2
- package/dist/llm/retry.js +7 -6
- package/dist/llm/schema/errors.d.ts +13 -3
- package/dist/llm/schema/errors.js +35 -3
- package/dist/llm/transport.js +5 -1
- package/dist/project/resolve.js +2 -2
- package/dist/server/routes/chat.js +8 -2
- package/dist/server/routes/todo.js +5 -2
- package/dist/server/server.d.ts +4 -2
- package/dist/server/server.js +20 -12
- package/dist/session/squash.js +31 -24
- package/dist/shared/types/agent.d.ts +11 -0
- package/dist/tools/builtin/todo.d.ts +7 -0
- package/dist/tools/builtin/todo.js +23 -16
- package/package.json +2 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/server/routes/todo.ts
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
-
import {
|
|
3
|
+
import { appendMessage, getMessages } from '../../session/message.js';
|
|
4
4
|
import { getSession } from '../../session/session.js';
|
|
5
5
|
import { generateId } from '../../shared/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { formatSummary, getLatestTodoPhasesFromMessages, todoTool, } from '../../tools/builtin/todo.js';
|
|
7
7
|
import { apiError } from '../middleware/error.js';
|
|
8
8
|
/** 构造仅供 todo tool execute 使用的最小 ToolContext。 */
|
|
9
9
|
function makeTodoCtx(phases, abort) {
|
|
@@ -89,6 +89,9 @@ function createTodoRoute(ctx) {
|
|
|
89
89
|
// 更新活跃 agent 的内存状态
|
|
90
90
|
if (run) {
|
|
91
91
|
run.state.todoPhases = updatedPhases;
|
|
92
|
+
// 前端手动修改了 todo 状态 → 注入 steering 通知 LLM(通道 A)
|
|
93
|
+
const summary = formatSummary(updatedPhases, [], true);
|
|
94
|
+
run.state.steeringQueue.push(`<todo-state-external>\nA todo change was made externally (via UI).\n${summary}\n</todo-state-external>`);
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
return c.json({
|
package/dist/server/server.d.ts
CHANGED
|
@@ -32,8 +32,10 @@ type BootstrappedServer = {
|
|
|
32
32
|
/** 把 config.providers 注册到新建的 LLM registry(修复此前空 registry 的遗漏)。 */
|
|
33
33
|
declare function buildRegistryFromConfig(config: Config): Registry;
|
|
34
34
|
/**
|
|
35
|
-
* config
|
|
36
|
-
*
|
|
35
|
+
* config 变更后原子地同步 registry:在隔离的 next registry 上重建全部路由,
|
|
36
|
+
* 完成后一次性替换 registry 内部 table 引用。运行中的 resolveRoute 任何时刻
|
|
37
|
+
* 看到的都是完整的旧表或完整的新表,不会读到「已清空但未注册完」的半状态,
|
|
38
|
+
* 因此不会把本可用的 provider 误判为 NoRoute。ServerContext 立即生效,无需重启。
|
|
37
39
|
*/
|
|
38
40
|
declare function syncRegistryFromConfig(registry: Registry, config: Config): void;
|
|
39
41
|
/** 解析 PGLite 持久化数据目录:优先 C0DE_DB_DIR,否则全局数据根下 pglite 子目录。
|
package/dist/server/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/server/server.ts
|
|
2
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import { connect as tcpConnect } from 'node:net';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
@@ -10,7 +10,7 @@ import { loadConfig } from '../core/config.js';
|
|
|
10
10
|
import { decryptSecret } from '../core/secret.js';
|
|
11
11
|
import { createAndPopulateRegistry } from '../core/workflows/index.js';
|
|
12
12
|
import { createDB, migrateDB } from '../db/index.js';
|
|
13
|
-
import { createRegistry, overrideToCapabilities, registerProvider, } from '../llm/registry.js';
|
|
13
|
+
import { createRegistry, overrideToCapabilities, rebuildRegistry, registerProvider, } from '../llm/registry.js';
|
|
14
14
|
import { initPlugins } from '../plugins/index.js';
|
|
15
15
|
import { createDefaultRegistry, createDefaultURLRegistry } from '../tools/index.js';
|
|
16
16
|
import { checkForUpdate, createHandoffServer, createUpdateScheduler, requestHandoff, restoreSessions, } from '../update/index.js';
|
|
@@ -44,15 +44,17 @@ function registerProviderFromConfig(registry, p) {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* config
|
|
48
|
-
*
|
|
47
|
+
* config 变更后原子地同步 registry:在隔离的 next registry 上重建全部路由,
|
|
48
|
+
* 完成后一次性替换 registry 内部 table 引用。运行中的 resolveRoute 任何时刻
|
|
49
|
+
* 看到的都是完整的旧表或完整的新表,不会读到「已清空但未注册完」的半状态,
|
|
50
|
+
* 因此不会把本可用的 provider 误判为 NoRoute。ServerContext 立即生效,无需重启。
|
|
49
51
|
*/
|
|
50
52
|
function syncRegistryFromConfig(registry, config) {
|
|
51
|
-
registry
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
53
|
+
rebuildRegistry(registry, (next) => {
|
|
54
|
+
for (const p of config.providers) {
|
|
55
|
+
registerProviderFromConfig(next, p);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
56
58
|
}
|
|
57
59
|
/** 全局数据根目录:XDG_DATA_HOME 优先,否则 ~/.local/share/c0de。
|
|
58
60
|
* 与 opencode (~/.local/share/opencode/)、oh-my-pi (~/.omp/agent/) 同约定——
|
|
@@ -221,11 +223,15 @@ function acquireDevDbLock(dataDir) {
|
|
|
221
223
|
try {
|
|
222
224
|
unlinkSync(lockPath);
|
|
223
225
|
}
|
|
224
|
-
catch {
|
|
226
|
+
catch {
|
|
227
|
+
/* best-effort */
|
|
228
|
+
}
|
|
225
229
|
try {
|
|
226
230
|
unlinkSync(join(dataDir, 'postmaster.pid'));
|
|
227
231
|
}
|
|
228
|
-
catch {
|
|
232
|
+
catch {
|
|
233
|
+
/* best-effort */
|
|
234
|
+
}
|
|
229
235
|
}
|
|
230
236
|
writeFileSync(lockPath, String(process.pid));
|
|
231
237
|
}
|
|
@@ -234,7 +240,9 @@ function releaseDevDbLock(dataDir) {
|
|
|
234
240
|
try {
|
|
235
241
|
unlinkSync(join(dataDir, DEV_LOCK_FILE));
|
|
236
242
|
}
|
|
237
|
-
catch {
|
|
243
|
+
catch {
|
|
244
|
+
/* best-effort */
|
|
245
|
+
}
|
|
238
246
|
}
|
|
239
247
|
/** dev 专用:创建 + migrate PGLite,跨热重载复用(单写者,只建一次)。 */
|
|
240
248
|
async function createDevDb(cwd) {
|
package/dist/session/squash.js
CHANGED
|
@@ -50,31 +50,38 @@ async function squashRecent(handle, sessionId, count, summarizer, config) {
|
|
|
50
50
|
${history}`;
|
|
51
51
|
const summary = await summarizer(prompt);
|
|
52
52
|
const squashEntryId = generateId();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const fileSnapshotIds =
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
// Atomic rewrite: archive originals → upsert snapshots → delete originals →
|
|
54
|
+
// insert summary. If any step throws, the whole transaction rolls back and
|
|
55
|
+
// the original message history is left intact. (Mirrors compactSession.)
|
|
56
|
+
const { archiveId, fileSnapshotIds } = await handle.db.transaction(async (tx) => {
|
|
57
|
+
const txHandle = { db: tx, close: handle.close };
|
|
58
|
+
const archiveId = cfg.archiveOriginal
|
|
59
|
+
? await archiveOriginalEntries(txHandle, sessionId, toSquash, 'squash', summary, squashEntryId)
|
|
60
|
+
: generateId();
|
|
61
|
+
const fileSnapshotIds = [];
|
|
62
|
+
if (cfg.preserveFileSnapshots) {
|
|
63
|
+
const hotFiles = extractHotFiles(toSquash);
|
|
64
|
+
for (const file of hotFiles) {
|
|
65
|
+
const id = await upsertFileSnapshot(txHandle, sessionId, file.path, file.content);
|
|
66
|
+
fileSnapshotIds.push(id);
|
|
67
|
+
}
|
|
62
68
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
await deleteEntriesByIds(txHandle, toSquash.map((m) => m.id));
|
|
70
|
+
await insertEntry(txHandle, {
|
|
71
|
+
id: squashEntryId,
|
|
72
|
+
sessionId,
|
|
73
|
+
tag: 'squash',
|
|
74
|
+
content: {
|
|
75
|
+
summary,
|
|
76
|
+
squashedEntryIds: toSquash.map((m) => m.id),
|
|
77
|
+
archiveId,
|
|
78
|
+
},
|
|
79
|
+
tokenCount: estimateTokens(summary),
|
|
80
|
+
// Position at the first squashed message's timestamp so it sorts between
|
|
81
|
+
// the prefix and the kept tail under createdAt-ascending order.
|
|
82
|
+
createdAt: toSquash[0] ? new Date(toSquash[0].createdAt) : new Date(),
|
|
83
|
+
});
|
|
84
|
+
return { archiveId, fileSnapshotIds };
|
|
78
85
|
});
|
|
79
86
|
return {
|
|
80
87
|
compacted: true,
|
|
@@ -184,6 +184,17 @@ type AgentEvent = {
|
|
|
184
184
|
archiveId?: string;
|
|
185
185
|
compactedCount: number;
|
|
186
186
|
keptCount: number;
|
|
187
|
+
}
|
|
188
|
+
/** Tag-based todo 操作成功后发射,前端据此刷新 TodoPanel。 */
|
|
189
|
+
| {
|
|
190
|
+
_tag: 'todo_update';
|
|
191
|
+
phases: {
|
|
192
|
+
name: string;
|
|
193
|
+
tasks: {
|
|
194
|
+
content: string;
|
|
195
|
+
status: string;
|
|
196
|
+
}[];
|
|
197
|
+
}[];
|
|
187
198
|
} | {
|
|
188
199
|
_tag: 'done';
|
|
189
200
|
};
|
|
@@ -47,6 +47,11 @@ export declare function nextActionableTask(phases: readonly TodoPhase[]): TodoIt
|
|
|
47
47
|
* `descriptions`. Normalize-then-equal first, with a substring fallback
|
|
48
48
|
* in either direction (≥6 char overlap on the contained side). */
|
|
49
49
|
export declare function todoMatchesAnyDescription(content: string, descriptions: readonly string[]): boolean;
|
|
50
|
+
/** Apply a single todo op to existing phases. Returns new phases + errors. */
|
|
51
|
+
declare function applyParams(phases: TodoPhase[], params: TodoInput): {
|
|
52
|
+
phases: TodoPhase[];
|
|
53
|
+
errors: string[];
|
|
54
|
+
};
|
|
50
55
|
/** Render todo phases as a Markdown checklist suitable for editing/copying. */
|
|
51
56
|
export declare function phasesToMarkdown(phases: TodoPhase[]): string;
|
|
52
57
|
/** Parse a Markdown checklist back into todo phases. */
|
|
@@ -54,6 +59,7 @@ export declare function markdownToPhases(md: string): {
|
|
|
54
59
|
phases: TodoPhase[];
|
|
55
60
|
errors: string[];
|
|
56
61
|
};
|
|
62
|
+
export declare function formatSummary(phases: TodoPhase[], errors: string[], readOnly?: boolean): string;
|
|
57
63
|
/** Extract the latest todo phases from stored messages (tool results).
|
|
58
64
|
* Scans backwards for the most recent `todo` tool result with phases metadata. */
|
|
59
65
|
export declare function getLatestTodoPhasesFromMessages(messages: {
|
|
@@ -65,3 +71,4 @@ export declare function getLatestTodoPhasesFromMessages(messages: {
|
|
|
65
71
|
* State is held in-memory via ctx.todoState hook (dependency-reversal). */
|
|
66
72
|
export declare const todoTool: ToolDef;
|
|
67
73
|
export type { TodoInput, TodoItem, TodoPhase, TodoStatus };
|
|
74
|
+
export { applyParams };
|
|
@@ -293,10 +293,9 @@ export function phasesToMarkdown(phases) {
|
|
|
293
293
|
if (phases.length === 0)
|
|
294
294
|
return '# Todos\n';
|
|
295
295
|
const out = [];
|
|
296
|
-
for (
|
|
296
|
+
for (const [i, phase] of phases.entries()) {
|
|
297
297
|
if (i > 0)
|
|
298
298
|
out.push('');
|
|
299
|
-
const phase = phases[i];
|
|
300
299
|
out.push(`# ${phase.name}`);
|
|
301
300
|
for (const task of phase.tasks) {
|
|
302
301
|
out.push(`- [${STATUS_TO_MARKER[task.status]}] ${task.content}`);
|
|
@@ -320,14 +319,13 @@ export function markdownToPhases(md) {
|
|
|
320
319
|
const phases = [];
|
|
321
320
|
let currentPhase;
|
|
322
321
|
const lines = md.split(/\r?\n/);
|
|
323
|
-
for (
|
|
324
|
-
const raw = lines[lineNum];
|
|
322
|
+
for (const [lineNum, raw] of lines.entries()) {
|
|
325
323
|
const trimmed = raw.trim();
|
|
326
324
|
if (!trimmed)
|
|
327
325
|
continue;
|
|
328
326
|
const headingMatch = /^#{1,6}\s+(.+?)\s*$/.exec(trimmed);
|
|
329
327
|
if (headingMatch) {
|
|
330
|
-
currentPhase = { name: headingMatch[1].trim(), tasks: [] };
|
|
328
|
+
currentPhase = { name: (headingMatch[1] ?? '').trim(), tasks: [] };
|
|
331
329
|
phases.push(currentPhase);
|
|
332
330
|
continue;
|
|
333
331
|
}
|
|
@@ -343,7 +341,7 @@ export function markdownToPhases(md) {
|
|
|
343
341
|
errors.push(`Line ${lineNum + 1}: unknown status marker "[${marker}]" (use [ ], [x], [/], [-])`);
|
|
344
342
|
continue;
|
|
345
343
|
}
|
|
346
|
-
currentPhase.tasks.push({ content: taskMatch[2].trim(), status });
|
|
344
|
+
currentPhase.tasks.push({ content: (taskMatch[2] ?? '').trim(), status });
|
|
347
345
|
continue;
|
|
348
346
|
}
|
|
349
347
|
errors.push(`Line ${lineNum + 1}: unrecognized syntax "${trimmed}"`);
|
|
@@ -354,7 +352,7 @@ export function markdownToPhases(md) {
|
|
|
354
352
|
// =============================================================================
|
|
355
353
|
// Summary formatter
|
|
356
354
|
// =============================================================================
|
|
357
|
-
function formatSummary(phases, errors, readOnly = false) {
|
|
355
|
+
export function formatSummary(phases, errors, readOnly = false) {
|
|
358
356
|
const tasks = phases.flatMap((phase) => phase.tasks);
|
|
359
357
|
if (tasks.length === 0) {
|
|
360
358
|
if (errors.length > 0)
|
|
@@ -362,16 +360,20 @@ function formatSummary(phases, errors, readOnly = false) {
|
|
|
362
360
|
return readOnly ? 'Todo list is empty.' : 'Todo list cleared.';
|
|
363
361
|
}
|
|
364
362
|
const remainingByPhase = phases
|
|
365
|
-
.map((phase) => ({
|
|
363
|
+
.map((phase, pi) => ({
|
|
366
364
|
name: phase.name,
|
|
367
|
-
tasks: phase.tasks
|
|
365
|
+
tasks: phase.tasks
|
|
366
|
+
.map((task, ti) => ({ task, seq: `${pi + 1}-${ti + 1}` }))
|
|
367
|
+
.filter(({ task }) => task.status === 'pending' || task.status === 'in_progress'),
|
|
368
368
|
}))
|
|
369
369
|
.filter((phase) => phase.tasks.length > 0);
|
|
370
|
-
const remainingTasks = remainingByPhase.flatMap((phase) => phase.tasks.map((task) => ({ ...task, phase: phase.name })));
|
|
370
|
+
const remainingTasks = remainingByPhase.flatMap((phase) => phase.tasks.map(({ task, seq }) => ({ ...task, seq, phase: phase.name })));
|
|
371
371
|
let currentIdx = phases.findIndex((phase) => phase.tasks.some((task) => task.status === 'pending' || task.status === 'in_progress'));
|
|
372
372
|
if (currentIdx === -1)
|
|
373
373
|
currentIdx = phases.length - 1;
|
|
374
374
|
const current = phases[currentIdx];
|
|
375
|
+
if (!current)
|
|
376
|
+
return errors.length > 0 ? `Errors: ${errors.join('; ')}` : 'Todo list cleared.';
|
|
375
377
|
const done = current.tasks.filter((task) => task.status === 'completed' || task.status === 'abandoned').length;
|
|
376
378
|
const lines = [];
|
|
377
379
|
if (errors.length > 0)
|
|
@@ -382,7 +384,7 @@ function formatSummary(phases, errors, readOnly = false) {
|
|
|
382
384
|
else {
|
|
383
385
|
lines.push(`Remaining items (${remainingTasks.length}):`);
|
|
384
386
|
for (const task of remainingTasks) {
|
|
385
|
-
lines.push(` - ${task.content} [${task.status}] (${task.phase})`);
|
|
387
|
+
lines.push(` - ${task.seq}: ${task.content} [${task.status}] (${task.phase})`);
|
|
386
388
|
}
|
|
387
389
|
}
|
|
388
390
|
const closedAll = tasks.filter((task) => task.status === 'completed' || task.status === 'abandoned').length;
|
|
@@ -392,16 +394,17 @@ function formatSummary(phases, errors, readOnly = false) {
|
|
|
392
394
|
lines.push(`Active phase ${currentIdx + 1}/${phases.length} "${current.name}" (${done}/${current.tasks.length})${workedAhead
|
|
393
395
|
? ' — earliest phase with open tasks; the in-progress pointer auto-advances to the earliest open task on each completion, so it can sit behind out-of-order work (nothing was un-completed).'
|
|
394
396
|
: '.'}`);
|
|
395
|
-
for (const phase of phases) {
|
|
397
|
+
for (const [pi, phase] of phases.entries()) {
|
|
396
398
|
lines.push(` ${phase.name}:`);
|
|
397
|
-
for (const task of phase.tasks) {
|
|
399
|
+
for (const [ti, task] of phase.tasks.entries()) {
|
|
400
|
+
const seq = `${pi + 1}-${ti + 1}`;
|
|
398
401
|
const checkbox = task.status === 'completed' ? '[X]' : '[ ]';
|
|
399
402
|
const tag = task.status === 'in_progress'
|
|
400
403
|
? ' (in progress)'
|
|
401
404
|
: task.status === 'abandoned'
|
|
402
405
|
? ' (dropped)'
|
|
403
406
|
: '';
|
|
404
|
-
lines.push(` - ${checkbox} ${task.content}${tag}`);
|
|
407
|
+
lines.push(` - ${checkbox} ${seq}: ${task.content}${tag}`);
|
|
405
408
|
}
|
|
406
409
|
}
|
|
407
410
|
return lines.join('\n');
|
|
@@ -414,7 +417,7 @@ function formatSummary(phases, errors, readOnly = false) {
|
|
|
414
417
|
export function getLatestTodoPhasesFromMessages(messages) {
|
|
415
418
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
416
419
|
const msg = messages[i];
|
|
417
|
-
if (msg
|
|
420
|
+
if (msg?.role !== 'tool')
|
|
418
421
|
continue;
|
|
419
422
|
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
420
423
|
const part = msg.content[j];
|
|
@@ -463,7 +466,10 @@ const todoParameters = {
|
|
|
463
466
|
},
|
|
464
467
|
},
|
|
465
468
|
task: { type: 'string', description: 'Task content (for start/done/drop/rm)' },
|
|
466
|
-
phase: {
|
|
469
|
+
phase: {
|
|
470
|
+
type: 'string',
|
|
471
|
+
description: 'Phase name (for done/drop/rm/append, or init flat mode)',
|
|
472
|
+
},
|
|
467
473
|
items: {
|
|
468
474
|
type: 'array',
|
|
469
475
|
items: { type: 'string' },
|
|
@@ -515,3 +521,4 @@ export const todoTool = {
|
|
|
515
521
|
return { _tag: 'success', output, metadata };
|
|
516
522
|
},
|
|
517
523
|
};
|
|
524
|
+
export { applyParams };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c0de-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Open-source AI coding assistant with Browser-Server architecture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
"@native-router/core": "^1.1.0",
|
|
95
95
|
"@native-router/react": "^1.1.2",
|
|
96
96
|
"@tanstack/react-query": "^5.101.2",
|
|
97
|
+
"@tanstack/react-virtual": "^3.14.6",
|
|
97
98
|
"@xterm/addon-fit": "^0.11.0",
|
|
98
99
|
"@xterm/addon-web-links": "^0.12.0",
|
|
99
100
|
"@xterm/xterm": "^6.0.0",
|