@wrongstack/tools 0.286.0 → 0.289.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 +2 -2
- package/dist/_edit-match.d.ts.map +1 -1
- package/dist/_fetch-guard.d.ts.map +1 -1
- package/dist/_redact-command.d.ts.map +1 -1
- package/dist/_spawn-stream.d.ts.map +1 -1
- package/dist/audit.js +61 -14
- package/dist/audit.js.map +3 -3
- package/dist/bash.d.ts +1 -1
- package/dist/bash.d.ts.map +1 -1
- package/dist/bash.js +97 -19
- package/dist/bash.js.map +3 -3
- package/dist/batch-tool-use.d.ts +1 -1
- package/dist/batch-tool-use.d.ts.map +1 -1
- package/dist/batch-tool-use.js +26 -8
- package/dist/batch-tool-use.js.map +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/types.d.ts +1 -1
- package/dist/browser/types.d.ts.map +1 -1
- package/dist/builtin.d.ts +12 -10
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.js +2107 -864
- package/dist/builtin.js.map +4 -4
- package/dist/codebase-index/background-indexer.d.ts.map +1 -1
- package/dist/codebase-index/bm25.d.ts +1 -2
- package/dist/codebase-index/bm25.d.ts.map +1 -1
- package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
- package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
- package/dist/codebase-index/go-parser.d.ts.map +1 -1
- package/dist/codebase-index/index-service.d.ts +11 -1
- package/dist/codebase-index/index-service.d.ts.map +1 -1
- package/dist/codebase-index/index.d.ts +2 -1
- package/dist/codebase-index/index.d.ts.map +1 -1
- package/dist/codebase-index/index.js +934 -283
- package/dist/codebase-index/index.js.map +4 -4
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/py-parser.d.ts.map +1 -1
- package/dist/codebase-index/rs-parser.d.ts.map +1 -1
- package/dist/codebase-index/schema.d.ts +43 -1
- package/dist/codebase-index/schema.d.ts.map +1 -1
- package/dist/codebase-index/ts-parser.d.ts.map +1 -1
- package/dist/codebase-index/worker-protocol.d.ts +1 -0
- package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
- package/dist/codebase-index/worker.js +793 -241
- package/dist/codebase-index/worker.js.map +4 -4
- package/dist/codebase-index/writer.d.ts +27 -1
- package/dist/codebase-index/writer.d.ts.map +1 -1
- package/dist/e2e.js +477 -0
- package/dist/e2e.js.map +7 -0
- package/dist/edit.d.ts.map +1 -1
- package/dist/edit.js +36 -12
- package/dist/edit.js.map +3 -3
- package/dist/exec.d.ts +1 -1
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +60 -5
- package/dist/exec.js.map +3 -3
- package/dist/fetch.js +2 -2
- package/dist/fetch.js.map +2 -2
- package/dist/format.js +61 -14
- package/dist/format.js.map +3 -3
- package/dist/glob.js +2 -2
- package/dist/glob.js.map +1 -1
- package/dist/grep.d.ts.map +1 -1
- package/dist/grep.js +2 -2
- package/dist/grep.js.map +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2535 -865
- package/dist/index.js.map +4 -4
- package/dist/install.js +61 -14
- package/dist/install.js.map +3 -3
- package/dist/kanban.d.ts +16 -2
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +795 -532
- package/dist/kanban.js.map +4 -4
- package/dist/languages/detect.d.ts.map +1 -1
- package/dist/languages/index.js +61 -14
- package/dist/languages/index.js.map +3 -3
- package/dist/languages/types.d.ts +7 -0
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/lint.js +61 -14
- package/dist/lint.js.map +3 -3
- package/dist/next-steps.d.ts +6 -2
- package/dist/next-steps.d.ts.map +1 -1
- package/dist/next-steps.js +3 -3
- package/dist/next-steps.js.map +2 -2
- package/dist/outdated.js +61 -14
- package/dist/outdated.js.map +3 -3
- package/dist/pack.js +2104 -861
- package/dist/pack.js.map +4 -4
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +158 -4
- package/dist/plan.js.map +4 -4
- package/dist/process-registry.js +12 -5
- package/dist/process-registry.js.map +2 -2
- package/dist/search.js +2 -2
- package/dist/search.js.map +2 -2
- package/dist/session-kanban.d.ts +47 -0
- package/dist/session-kanban.d.ts.map +1 -0
- package/dist/session-kanban.js +626 -0
- package/dist/session-kanban.js.map +7 -0
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +167 -4
- package/dist/task.js.map +4 -4
- package/dist/test.js +61 -14
- package/dist/test.js.map +3 -3
- package/dist/todo.d.ts.map +1 -1
- package/dist/todo.js +159 -7
- package/dist/todo.js.map +4 -4
- package/dist/tool-icon-map.d.ts.map +1 -1
- package/dist/tool-icons.d.ts.map +1 -1
- package/dist/tool-icons.js +7 -1
- package/dist/tool-icons.js.map +2 -2
- package/dist/tree.js +2 -2
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js +61 -14
- package/dist/typecheck.js.map +3 -3
- package/package.json +11 -3
package/dist/todo.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"todo.d.ts","sourceRoot":"","sources":["../src/todo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAIvD,UAAU,SAAS;IACjB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"todo.d.ts","sourceRoot":"","sources":["../src/todo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAIvD,UAAU,SAAS;IACjB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CA2IhD,CAAC"}
|
package/dist/todo.js
CHANGED
|
@@ -1,6 +1,153 @@
|
|
|
1
1
|
// src/todo.ts
|
|
2
|
-
import { loadPlan, savePlan, setPlanItemStatus } from "@wrongstack/core";
|
|
3
|
-
|
|
2
|
+
import { loadPlan as loadPlan2, loadTasks as loadTasks2, savePlan, saveTasks, setPlanItemStatus } from "@wrongstack/core";
|
|
3
|
+
|
|
4
|
+
// src/session-kanban.ts
|
|
5
|
+
import {
|
|
6
|
+
deserializeTaskGraph,
|
|
7
|
+
GlobalMailbox,
|
|
8
|
+
loadPlan,
|
|
9
|
+
loadTasks,
|
|
10
|
+
mutatePlan,
|
|
11
|
+
mutateTasks
|
|
12
|
+
} from "@wrongstack/core";
|
|
13
|
+
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
14
|
+
import {
|
|
15
|
+
createBoard,
|
|
16
|
+
getBoard,
|
|
17
|
+
getKanbanDir,
|
|
18
|
+
listBoards,
|
|
19
|
+
removeBoard,
|
|
20
|
+
syncBoardFromTaskGraph,
|
|
21
|
+
touchKanbanPresence,
|
|
22
|
+
updateBoard
|
|
23
|
+
} from "@wrongstack/kanban";
|
|
24
|
+
var SESSION_BOARD_TAG = "session-work";
|
|
25
|
+
var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
|
|
26
|
+
var SESSION_KANBAN_COLUMNS = [
|
|
27
|
+
{ id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
|
|
28
|
+
{ id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
|
|
29
|
+
{ id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
|
|
30
|
+
{ id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
|
|
31
|
+
];
|
|
32
|
+
var boardQueue = /* @__PURE__ */ new Map();
|
|
33
|
+
var boardEnsures = /* @__PURE__ */ new Map();
|
|
34
|
+
function boardKey(projectRoot, sessionId) {
|
|
35
|
+
return `${projectRoot}\0${sessionId}`;
|
|
36
|
+
}
|
|
37
|
+
function sessionTag(sessionId) {
|
|
38
|
+
return `session:${sessionId}`;
|
|
39
|
+
}
|
|
40
|
+
function sessionBoardTitle(sessionId) {
|
|
41
|
+
const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
|
|
42
|
+
return `Session ${leaf.slice(0, 12)}`;
|
|
43
|
+
}
|
|
44
|
+
function sessionBoardTags(sessionId) {
|
|
45
|
+
return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
|
|
46
|
+
}
|
|
47
|
+
function sameColumns(columns) {
|
|
48
|
+
return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
|
|
49
|
+
}
|
|
50
|
+
async function ensureSessionKanbanBoard(projectRoot, sessionId) {
|
|
51
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
|
|
52
|
+
const key = boardKey(projectRoot, sessionId);
|
|
53
|
+
const inFlight = boardEnsures.get(key);
|
|
54
|
+
if (inFlight) return inFlight;
|
|
55
|
+
const promise = (async () => {
|
|
56
|
+
const summary = (await listBoards(projectRoot)).find(
|
|
57
|
+
(board2) => board2.tags?.includes(sessionTag(sessionId))
|
|
58
|
+
);
|
|
59
|
+
let board = summary ? await getBoard(projectRoot, summary.id) : null;
|
|
60
|
+
if (!board) {
|
|
61
|
+
return createBoard(projectRoot, {
|
|
62
|
+
title: sessionBoardTitle(sessionId),
|
|
63
|
+
description: "Live session work: todos, tasks, and plan items.",
|
|
64
|
+
tags: sessionBoardTags(sessionId),
|
|
65
|
+
columns: SESSION_KANBAN_COLUMNS,
|
|
66
|
+
generatedBy: `session-kanban:${sessionId}`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
|
|
70
|
+
board = await updateBoard(projectRoot, board.id, {
|
|
71
|
+
title: sessionBoardTitle(sessionId),
|
|
72
|
+
description: "Live session work: todos, tasks, and plan items.",
|
|
73
|
+
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
74
|
+
columns: SESSION_KANBAN_COLUMNS
|
|
75
|
+
}) ?? board;
|
|
76
|
+
}
|
|
77
|
+
return board;
|
|
78
|
+
})();
|
|
79
|
+
boardEnsures.set(key, promise);
|
|
80
|
+
try {
|
|
81
|
+
return await promise;
|
|
82
|
+
} finally {
|
|
83
|
+
boardEnsures.delete(key);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function enqueueBoardWork(projectRoot, sessionId, work) {
|
|
87
|
+
const key = boardKey(projectRoot, sessionId);
|
|
88
|
+
const previous = boardQueue.get(key) ?? Promise.resolve();
|
|
89
|
+
const result = previous.catch(() => void 0).then(work);
|
|
90
|
+
const tail = result.then(
|
|
91
|
+
() => void 0,
|
|
92
|
+
() => void 0
|
|
93
|
+
);
|
|
94
|
+
boardQueue.set(key, tail);
|
|
95
|
+
void tail.then(() => {
|
|
96
|
+
if (boardQueue.get(key) === tail) boardQueue.delete(key);
|
|
97
|
+
});
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
|
|
101
|
+
if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
|
|
102
|
+
return enqueueBoardWork(projectRoot, sessionId, async () => {
|
|
103
|
+
const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
|
|
104
|
+
if (!board) return null;
|
|
105
|
+
const result = await syncBoardFromTaskGraph(
|
|
106
|
+
projectRoot,
|
|
107
|
+
board.id,
|
|
108
|
+
deserializeTaskGraph(graph),
|
|
109
|
+
{
|
|
110
|
+
sourceSystem,
|
|
111
|
+
tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
|
|
112
|
+
archiveMissingTasks: true,
|
|
113
|
+
includeCompletedTasks: true
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
return result?.board ?? null;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function todoListToSerializedGraph(todos, sessionId) {
|
|
120
|
+
const nodes = todos.map((todo, index) => ({
|
|
121
|
+
id: todo.id,
|
|
122
|
+
title: todo.content,
|
|
123
|
+
description: todo.activeForm ?? "",
|
|
124
|
+
type: "chore",
|
|
125
|
+
priority: "medium",
|
|
126
|
+
status: todo.status,
|
|
127
|
+
createdAt: index,
|
|
128
|
+
updatedAt: index
|
|
129
|
+
}));
|
|
130
|
+
return {
|
|
131
|
+
id: `todo:${sessionId}`,
|
|
132
|
+
specId: `todo:${sessionId}`,
|
|
133
|
+
title: "Session todos",
|
|
134
|
+
nodes,
|
|
135
|
+
edges: [],
|
|
136
|
+
rootNodes: nodes.map((node) => node.id),
|
|
137
|
+
createdAt: 0,
|
|
138
|
+
updatedAt: 0
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
142
|
+
return projectGraph(
|
|
143
|
+
projectRoot,
|
|
144
|
+
sessionId,
|
|
145
|
+
todoListToSerializedGraph(todos, sessionId),
|
|
146
|
+
"session-todo"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/todo.ts
|
|
4
151
|
var todoTool = {
|
|
5
152
|
name: "todo",
|
|
6
153
|
category: "Session",
|
|
@@ -9,7 +156,7 @@ var todoTool = {
|
|
|
9
156
|
permission: "auto",
|
|
10
157
|
mutating: false,
|
|
11
158
|
// mutates only conversation state (ctx.todos), not external state — no confirmation needed
|
|
12
|
-
timeoutMs:
|
|
159
|
+
timeoutMs: 5e3,
|
|
13
160
|
capabilities: ["session.todo"],
|
|
14
161
|
icon: "todo",
|
|
15
162
|
inputSchema: {
|
|
@@ -61,16 +208,21 @@ var todoTool = {
|
|
|
61
208
|
}
|
|
62
209
|
}
|
|
63
210
|
ctx.state.replaceTodos(items);
|
|
211
|
+
await projectSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
|
|
64
212
|
const completedPlanIds = /* @__PURE__ */ new Set();
|
|
65
213
|
const completedTaskIds = /* @__PURE__ */ new Set();
|
|
66
214
|
const pendingPlanIds = /* @__PURE__ */ new Set();
|
|
67
215
|
const pendingTaskIds = /* @__PURE__ */ new Set();
|
|
68
216
|
for (const item of items) {
|
|
69
217
|
if (item.promotedFromPlan) {
|
|
70
|
-
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
|
|
218
|
+
(item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
|
|
219
|
+
item.promotedFromPlan
|
|
220
|
+
);
|
|
71
221
|
}
|
|
72
222
|
if (item.promotedFromTask) {
|
|
73
|
-
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
|
|
223
|
+
(item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
|
|
224
|
+
item.promotedFromTask
|
|
225
|
+
);
|
|
74
226
|
}
|
|
75
227
|
}
|
|
76
228
|
for (const planId of completedPlanIds) {
|
|
@@ -78,7 +230,7 @@ var todoTool = {
|
|
|
78
230
|
const planPath = ctx.meta["plan.path"];
|
|
79
231
|
if (typeof planPath !== "string" || !planPath) continue;
|
|
80
232
|
try {
|
|
81
|
-
const plan = await
|
|
233
|
+
const plan = await loadPlan2(planPath);
|
|
82
234
|
if (plan) {
|
|
83
235
|
const updated = setPlanItemStatus(plan, planId, "done");
|
|
84
236
|
await savePlan(planPath, updated);
|
|
@@ -91,7 +243,7 @@ var todoTool = {
|
|
|
91
243
|
const taskPath = ctx.meta["task.path"];
|
|
92
244
|
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
93
245
|
try {
|
|
94
|
-
const file = await
|
|
246
|
+
const file = await loadTasks2(taskPath);
|
|
95
247
|
if (file) {
|
|
96
248
|
const task = file.tasks.find((t) => t.id === taskId);
|
|
97
249
|
if (task && task.status !== "completed") {
|
package/dist/todo.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/todo.ts"],
|
|
4
|
-
"sourcesContent": ["import type { TodoItem, Tool } from '@wrongstack/core';\nimport { loadPlan, savePlan, setPlanItemStatus } from '@wrongstack/core';\nimport { loadTasks, saveTasks } from '@wrongstack/core';\n\ninterface TodoInput {\n todos: TodoItem[];\n}\n\ninterface TodoOutput {\n count: number;\n in_progress: number;\n}\n\nexport const todoTool: Tool<TodoInput, TodoOutput> = {\n name: 'todo',\n category: 'Session',\n description:\n 'Manage the session-level todo list. This is the primary mechanism for tracking multi-step work. ' +\n 'The list is fully replaced on every call (not appended).',\n usageHint:\n 'BEST PRACTICE for complex tasks:\\n' +\n '- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\\n' +\n '- Only **one** item should be `in_progress` at any time.\\n' +\n '- Update the list frequently as work progresses (mark items done, add new ones, change status).\\n' +\n '- **Re-order items** to reflect current priorities \u2014 the full list is replaced each call, so item order is entirely under your control.\\n' +\n '- When all items are completed the board auto-clears \u2014 you do NOT need to send an empty list.\\n' +\n '- The system and user can see this list, so keep it honest and up-to-date.\\n' +\n 'This tool is extremely valuable for maintaining focus and giving the user visibility into your plan.',\n permission: 'auto',\n mutating: false, // mutates only conversation state (ctx.todos), not external state \u2014 no confirmation needed\n timeoutMs: 1_000,\n capabilities: ['session.todo'],\n icon: 'todo',\n inputSchema: {\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique identifier for the todo item (e.g. \"1\", \"auth-flow\").',\n },\n content: {\n type: 'string',\n description: 'Clear, actionable description of the task.',\n },\n status: {\n type: 'string',\n enum: ['pending', 'in_progress', 'completed'],\n description: 'Current status. Only one item should be \"in_progress\" at a time.',\n },\n activeForm: {\n type: 'string',\n description: 'Optional present-tense form shown while the task is active (e.g. \"Fixing auth bug\").',\n },\n },\n required: ['id', 'content', 'status'],\n },\n description: 'The complete new list of todos. This replaces the previous list entirely.',\n },\n },\n required: ['todos'],\n },\n async execute(input, ctx) {\n if (!Array.isArray(input?.todos)) {\n throw new Error('todo: todos must be an array');\n }\n const items = input.todos.filter((t): t is TodoItem => Boolean(t?.id && t.content));\n const inProgress = items.filter((t) => t.status === 'in_progress');\n if (inProgress.length > 1) {\n // Keep only the first as in_progress, mark rest pending\n let seenInProgress = false;\n for (const item of items) {\n if (item.status === 'in_progress') {\n if (seenInProgress) item.status = 'pending';\n seenInProgress = true;\n }\n }\n }\n ctx.state.replaceTodos(items);\n\n // Auto-complete parent plan items / tasks when all their promoted\n // todos are done. Runs after state mutation so the UI sees the new\n // todo list before we touch the plan/task files.\n const completedPlanIds = new Set<string>();\n const completedTaskIds = new Set<string>();\n const pendingPlanIds = new Set<string>();\n const pendingTaskIds = new Set<string>();\n\n for (const item of items) {\n if (item.promotedFromPlan) {\n (item.status === 'completed' ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);\n }\n if (item.promotedFromTask) {\n (item.status === 'completed' ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);\n }\n }\n\n // Mark fully-completed plan items as done\n for (const planId of completedPlanIds) {\n if (pendingPlanIds.has(planId)) continue; // not all done yet\n const planPath = (ctx.meta as Record<string, unknown>)['plan.path'];\n if (typeof planPath !== 'string' || !planPath) continue;\n try {\n const plan = await loadPlan(planPath);\n if (plan) {\n const updated = setPlanItemStatus(plan, planId, 'done');\n await savePlan(planPath, updated);\n }\n } catch { /* best-effort */ }\n }\n\n // Mark fully-completed tasks as completed\n for (const taskId of completedTaskIds) {\n if (pendingTaskIds.has(taskId)) continue; // not all done yet\n const taskPath = (ctx.meta as Record<string, unknown>)['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) continue;\n try {\n const file = await loadTasks(taskPath);\n if (file) {\n const task = file.tasks.find((t) => t.id === taskId);\n if (task && task.status !== 'completed') {\n task.status = 'completed';\n task.updatedAt = new Date().toISOString();\n await saveTasks(taskPath, file);\n }\n }\n } catch { /* best-effort */ }\n }\n\n return {\n count: items.length,\n in_progress: items.filter((t) => t.status === 'in_progress').length,\n };\n },\n};\n"],
|
|
5
|
-
"mappings": ";AACA,SAAS,
|
|
6
|
-
"names": []
|
|
3
|
+
"sources": ["../src/todo.ts", "../src/session-kanban.ts"],
|
|
4
|
+
"sourcesContent": ["import type { TodoItem, Tool } from '@wrongstack/core';\nimport { loadPlan, loadTasks, savePlan, saveTasks, setPlanItemStatus } from '@wrongstack/core';\nimport { projectSessionTodosToKanban } from './session-kanban.js';\n\ninterface TodoInput {\n todos: TodoItem[];\n}\n\ninterface TodoOutput {\n count: number;\n in_progress: number;\n}\n\nexport const todoTool: Tool<TodoInput, TodoOutput> = {\n name: 'todo',\n category: 'Session',\n description:\n 'Manage the session-level todo list. This is the primary mechanism for tracking multi-step work. ' +\n 'The list is fully replaced on every call (not appended).',\n usageHint:\n 'BEST PRACTICE for complex tasks:\\n' +\n '- At the beginning of a non-trivial task, create a clear todo list with specific, actionable items.\\n' +\n '- Only **one** item should be `in_progress` at any time.\\n' +\n '- Update the list frequently as work progresses (mark items done, add new ones, change status).\\n' +\n '- **Re-order items** to reflect current priorities \u2014 the full list is replaced each call, so item order is entirely under your control.\\n' +\n '- When all items are completed the board auto-clears \u2014 you do NOT need to send an empty list.\\n' +\n '- The system and user can see this list, so keep it honest and up-to-date.\\n' +\n 'This tool is extremely valuable for maintaining focus and giving the user visibility into your plan.',\n permission: 'auto',\n mutating: false, // mutates only conversation state (ctx.todos), not external state \u2014 no confirmation needed\n timeoutMs: 5_000,\n capabilities: ['session.todo'],\n icon: 'todo',\n inputSchema: {\n type: 'object',\n properties: {\n todos: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique identifier for the todo item (e.g. \"1\", \"auth-flow\").',\n },\n content: {\n type: 'string',\n description: 'Clear, actionable description of the task.',\n },\n status: {\n type: 'string',\n enum: ['pending', 'in_progress', 'completed'],\n description: 'Current status. Only one item should be \"in_progress\" at a time.',\n },\n activeForm: {\n type: 'string',\n description:\n 'Optional present-tense form shown while the task is active (e.g. \"Fixing auth bug\").',\n },\n },\n required: ['id', 'content', 'status'],\n },\n description: 'The complete new list of todos. This replaces the previous list entirely.',\n },\n },\n required: ['todos'],\n },\n async execute(input, ctx) {\n if (!Array.isArray(input?.todos)) {\n throw new Error('todo: todos must be an array');\n }\n const items = input.todos.filter((t): t is TodoItem => Boolean(t?.id && t.content));\n const inProgress = items.filter((t) => t.status === 'in_progress');\n if (inProgress.length > 1) {\n // Keep only the first as in_progress, mark rest pending\n let seenInProgress = false;\n for (const item of items) {\n if (item.status === 'in_progress') {\n if (seenInProgress) item.status = 'pending';\n seenInProgress = true;\n }\n }\n }\n ctx.state.replaceTodos(items);\n\n // Kanban is the session work surface. Mirror the requested list (rather\n // than ctx.todos) so the final all-completed snapshot reaches Done even\n // though ConversationState auto-clears an all-done tactical todo list.\n await projectSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? 'session');\n\n // Auto-complete parent plan items / tasks when all their promoted\n // todos are done. Runs after state mutation so the UI sees the new\n // todo list before we touch the plan/task files.\n const completedPlanIds = new Set<string>();\n const completedTaskIds = new Set<string>();\n const pendingPlanIds = new Set<string>();\n const pendingTaskIds = new Set<string>();\n\n for (const item of items) {\n if (item.promotedFromPlan) {\n (item.status === 'completed' ? completedPlanIds : pendingPlanIds).add(\n item.promotedFromPlan,\n );\n }\n if (item.promotedFromTask) {\n (item.status === 'completed' ? completedTaskIds : pendingTaskIds).add(\n item.promotedFromTask,\n );\n }\n }\n\n // Mark fully-completed plan items as done\n for (const planId of completedPlanIds) {\n if (pendingPlanIds.has(planId)) continue; // not all done yet\n const planPath = (ctx.meta as Record<string, unknown>)['plan.path'];\n if (typeof planPath !== 'string' || !planPath) continue;\n try {\n const plan = await loadPlan(planPath);\n if (plan) {\n const updated = setPlanItemStatus(plan, planId, 'done');\n await savePlan(planPath, updated);\n }\n } catch {\n /* best-effort */\n }\n }\n\n // Mark fully-completed tasks as completed\n for (const taskId of completedTaskIds) {\n if (pendingTaskIds.has(taskId)) continue; // not all done yet\n const taskPath = (ctx.meta as Record<string, unknown>)['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) continue;\n try {\n const file = await loadTasks(taskPath);\n if (file) {\n const task = file.tasks.find((t) => t.id === taskId);\n if (task && task.status !== 'completed') {\n task.status = 'completed';\n task.updatedAt = new Date().toISOString();\n await saveTasks(taskPath, file);\n }\n }\n } catch {\n /* best-effort */\n }\n }\n\n return {\n count: items.length,\n in_progress: items.filter((t) => t.status === 'in_progress').length,\n };\n },\n};\n", "import { type FSWatcher, watch } from 'node:fs';\nimport { basename, dirname } from 'node:path';\nimport {\n type Context,\n deserializeTaskGraph,\n GlobalMailbox,\n loadPlan,\n loadTasks,\n mutatePlan,\n mutateTasks,\n type PlanFile,\n type PlanItem,\n type SerializedTaskGraph,\n type TaskFile,\n type TaskItem,\n type TaskStatus,\n type TodoItem,\n} from '@wrongstack/core';\nimport { resolveWstackPaths } from '@wrongstack/core/utils';\nimport {\n createBoard,\n getBoard,\n getKanbanDir,\n type KanbanBoard,\n type KanbanColumn,\n type KanbanTask,\n listBoards,\n removeBoard,\n syncBoardFromTaskGraph,\n touchKanbanPresence,\n updateBoard,\n} from '@wrongstack/kanban';\n\nconst SESSION_BOARD_TAG = 'session-work';\nconst MIRROR_DISABLED_ENV = 'WRONGSTACK_KANBAN_TASK_MIRROR';\n\n/** The canonical workflow shared by WebUI and TUI session boards. */\nexport const SESSION_KANBAN_COLUMNS: KanbanColumn[] = [\n { id: 'todo', title: 'Todo', order: 0, wipLimit: 0, color: '#2563eb' },\n { id: 'in-progress', title: 'Running', order: 1, wipLimit: 1, color: '#d97706' },\n { id: 'review', title: 'Preview', order: 2, wipLimit: 0, color: '#7c3aed' },\n { id: 'done', title: 'Done', order: 3, wipLimit: 0, color: '#16a34a' },\n];\n\nconst boardQueue = new Map<string, Promise<void>>();\nconst boardEnsures = new Map<string, Promise<KanbanBoard>>();\nconst bindings = new WeakMap<Context, () => void>();\nconst suppressedTodoMirrors = new WeakSet<Context>();\nconst activeSessionBoards = new Map<string, number>();\n\nfunction boardKey(projectRoot: string, sessionId: string): string {\n return `${projectRoot}\\0${sessionId}`;\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction sessionBoardTitle(sessionId: string): string {\n const leaf = sessionId.split(/[\\\\/]/).filter(Boolean).pop() ?? sessionId;\n return `Session ${leaf.slice(0, 12)}`;\n}\n\nfunction sessionBoardTags(sessionId: string): string[] {\n return ['session', SESSION_BOARD_TAG, sessionTag(sessionId)];\n}\n\nfunction sessionIdFromTags(tags: readonly string[] | undefined): string | null {\n const tag = tags?.find((candidate) => candidate.startsWith('session:'));\n return tag?.slice('session:'.length) || null;\n}\n\nfunction isOwnedSessionBoard(tags: readonly string[] | undefined): boolean {\n return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));\n}\n\nfunction retainActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);\n}\n\nfunction releaseActiveSessionBoard(projectRoot: string, sessionId: string): void {\n const key = boardKey(projectRoot, sessionId);\n const remaining = (activeSessionBoards.get(key) ?? 0) - 1;\n if (remaining > 0) activeSessionBoards.set(key, remaining);\n else activeSessionBoards.delete(key);\n}\n\nfunction isSessionBoardActive(projectRoot: string, sessionId: string): boolean {\n return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;\n}\n\nfunction sameColumns(columns: readonly KanbanColumn[]): boolean {\n return (\n columns.length === SESSION_KANBAN_COLUMNS.length &&\n columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id)\n );\n}\n\n/** Create (or migrate) the single Kanban board owned by a session. */\nexport async function ensureSessionKanbanBoard(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n const key = boardKey(projectRoot, sessionId);\n const inFlight = boardEnsures.get(key);\n if (inFlight) return inFlight;\n\n const promise = (async () => {\n const summary = (await listBoards(projectRoot)).find((board) =>\n board.tags?.includes(sessionTag(sessionId)),\n );\n let board = summary ? await getBoard(projectRoot, summary.id) : null;\n if (!board) {\n return createBoard(projectRoot, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: sessionBoardTags(sessionId),\n columns: SESSION_KANBAN_COLUMNS,\n generatedBy: `session-kanban:${sessionId}`,\n });\n }\n\n // Boards produced by the older task/plan mirrors used the generic five\n // columns. Migrate only session-owned boards; updateBoard reconciles cards\n // from Backlog into Todo while preserving Running/Preview/Done cards.\n if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {\n board =\n (await updateBoard(projectRoot, board.id, {\n title: sessionBoardTitle(sessionId),\n description: 'Live session work: todos, tasks, and plan items.',\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n columns: SESSION_KANBAN_COLUMNS,\n })) ?? board;\n }\n return board;\n })();\n\n boardEnsures.set(key, promise);\n try {\n return await promise;\n } finally {\n boardEnsures.delete(key);\n }\n}\n\nfunction enqueueBoardWork<T>(\n projectRoot: string,\n sessionId: string,\n work: () => Promise<T>,\n): Promise<T> {\n const key = boardKey(projectRoot, sessionId);\n const previous = boardQueue.get(key) ?? Promise.resolve();\n const result = previous.catch(() => undefined).then(work);\n const tail = result.then(\n () => undefined,\n () => undefined,\n );\n boardQueue.set(key, tail);\n void tail.then(() => {\n if (boardQueue.get(key) === tail) boardQueue.delete(key);\n });\n return result;\n}\n\nasync function removeEmptySessionBoard(\n projectRoot: string,\n boardId: string,\n sessionId: string,\n): Promise<string | null> {\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n if (isSessionBoardActive(projectRoot, sessionId)) return null;\n const board = await getBoard(projectRoot, boardId);\n if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;\n if (sessionIdFromTags(board.tags) !== sessionId) return null;\n return (await removeBoard(projectRoot, board.id)) ? board.id : null;\n });\n}\n\n/** Remove a particular inactive session's system-owned board when it has no cards. */\nexport async function cleanupSessionKanbanBoardIfEmpty(\n projectRoot: string | undefined,\n sessionId: string,\n): Promise<string[]> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n if (isSessionBoardActive(projectRoot, sessionId)) return [];\n const candidates = (await listBoards(projectRoot)).filter(\n (board) =>\n board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n sessionIdFromTags(board.tags) === sessionId,\n );\n const removed = await Promise.all(\n candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId)),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\n/** Prune stale empty session boards while preserving manual and live boards. */\nexport async function cleanupEmptySessionKanbanBoards(\n projectRoot: string | undefined,\n activeSessionId = '',\n): Promise<string[]> {\n if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === '0') return [];\n const candidates = (await listBoards(projectRoot)).flatMap((board) => {\n const ownerSessionId = sessionIdFromTags(board.tags);\n return board.taskCount === 0 &&\n isOwnedSessionBoard(board.tags) &&\n ownerSessionId &&\n ownerSessionId !== activeSessionId &&\n !isSessionBoardActive(projectRoot, ownerSessionId)\n ? [{ boardId: board.id, sessionId: ownerSessionId }]\n : [];\n });\n const removed = await Promise.all(\n candidates.map(({ boardId, sessionId }) =>\n removeEmptySessionBoard(projectRoot, boardId, sessionId),\n ),\n );\n return removed.filter((boardId): boardId is string => Boolean(boardId));\n}\n\nasync function projectGraph(\n projectRoot: string | undefined,\n sessionId: string,\n graph: SerializedTaskGraph,\n sourceSystem: 'session-todo' | 'session-task' | 'session-plan',\n): Promise<KanbanBoard | null> {\n if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === '0') return null;\n return enqueueBoardWork(projectRoot, sessionId, async () => {\n const board = await ensureSessionKanbanBoard(projectRoot, sessionId);\n if (!board) return null;\n const result = await syncBoardFromTaskGraph(\n projectRoot,\n board.id,\n deserializeTaskGraph(graph),\n {\n sourceSystem,\n tags: [...new Set([...(board.tags ?? []), ...sessionBoardTags(sessionId)])],\n archiveMissingTasks: true,\n includeCompletedTasks: true,\n },\n );\n return result?.board ?? null;\n });\n}\n\nexport function todoListToSerializedGraph(\n todos: readonly TodoItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = todos.map((todo, index) => ({\n id: todo.id,\n title: todo.content,\n description: todo.activeForm ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: todo.status,\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `todo:${sessionId}`,\n specId: `todo:${sessionId}`,\n title: 'Session todos',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function taskFileToSerializedGraph(\n tasks: readonly TaskItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const ids = new Set(tasks.map((task) => task.id));\n const nodes = tasks.map((task, index) => ({\n id: task.id,\n title: task.title,\n description: task.description ?? '',\n type: task.type,\n priority: task.priority,\n status: task.status,\n ...(task.assignee ? { assignee: task.assignee } : {}),\n ...(task.estimateHours !== undefined ? { estimateHours: task.estimateHours } : {}),\n createdAt: index,\n updatedAt: index,\n }));\n const edges = tasks.flatMap((task) =>\n (task.dependsOn ?? [])\n .filter((dependency) => ids.has(dependency))\n .map((dependency) => ({\n id: `${dependency}->${task.id}`,\n from: dependency,\n to: task.id,\n type: 'depends_on' as const,\n })),\n );\n const hasIncoming = new Set(edges.map((edge) => edge.to));\n const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);\n return {\n // Keep the historical graph id so existing mirrored task cards are reused.\n id: `session:${sessionId}`,\n specId: `session:${sessionId}`,\n title: 'Session tasks',\n nodes,\n edges,\n rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nconst PLAN_STATUS_TO_TASK: Record<PlanItem['status'], TaskStatus> = {\n open: 'pending',\n in_progress: 'in_progress',\n done: 'completed',\n};\n\nexport function planFileToSerializedGraph(\n items: readonly PlanItem[],\n sessionId: string,\n): SerializedTaskGraph {\n const nodes = items.map((item, index) => ({\n id: item.id,\n title: item.title,\n description: item.details ?? '',\n type: 'chore' as const,\n priority: 'medium' as const,\n status: PLAN_STATUS_TO_TASK[item.status],\n createdAt: index,\n updatedAt: index,\n }));\n return {\n id: `plan:${sessionId}`,\n specId: `plan:${sessionId}`,\n title: 'Session plan',\n nodes,\n edges: [],\n rootNodes: nodes.map((node) => node.id),\n createdAt: 0,\n updatedAt: 0,\n };\n}\n\nexport function projectSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n todoListToSerializedGraph(todos, sessionId),\n 'session-todo',\n );\n}\n\nexport function projectSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n taskFileToSerializedGraph(tasks, sessionId),\n 'session-task',\n );\n}\n\nexport function projectSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): Promise<KanbanBoard | null> {\n return projectGraph(\n projectRoot,\n sessionId,\n planFileToSerializedGraph(items, sessionId),\n 'session-plan',\n );\n}\n\nfunction fireAndForget(work: Promise<unknown>): void {\n void work.catch(() => {\n // The session files/state remain recoverable if the observational board\n // cannot be written; the next mutation or file watcher retries the mirror.\n });\n}\n\nfunction broadcastTodoUpdate(context: Context, todos: readonly TodoItem[]): void {\n const sessionId = context.session?.id ?? '';\n if (!context.agentId || !sessionId) return;\n const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;\n const mailbox = new GlobalMailbox(projectDir);\n void mailbox\n .send({\n from: context.agentId,\n to: '*',\n type: 'status',\n subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? '' : 's'})`,\n body: JSON.stringify({\n kind: 'kanban.todos.updated',\n sessionId,\n revision: context.state.revision,\n todos,\n }),\n priority: 'normal',\n senderSessionId: sessionId,\n ttlMs: 6 * 60 * 60 * 1000,\n })\n .catch(() => {\n // Mailbox awareness is best-effort; canonical state is already updated.\n });\n}\n\nfunction notifyTodoUpdate(context: Context, todos: readonly TodoItem[]): void {\n const summary = todos.length\n ? todos\n .map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`)\n .join('\\n')\n : '- No active todos remain.';\n const text = `[KANBAN TODO UPDATE]\\nAnother Kanban agent reassessed the shared board. The canonical todo list is now:\\n${summary}\\nReassess your current plan before continuing; do not rely on the initial todo snapshot.`;\n const state = context.state as Partial<Context['state']>;\n if (typeof state.appendBlockToLastUserMessage === 'function') {\n if (state.appendBlockToLastUserMessage({ type: 'text', text })) return;\n }\n if (typeof state.appendMessage === 'function') {\n state.appendMessage({ role: 'user', content: [{ type: 'text', text }] });\n }\n}\n\nexport function mirrorSessionTodosToKanban(\n projectRoot: string | undefined,\n todos: readonly TodoItem[],\n sessionId: string,\n): void {\n fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));\n}\n\nexport function mirrorSessionTasksToKanban(\n projectRoot: string | undefined,\n tasks: readonly TaskItem[],\n sessionId: string,\n): void {\n fireAndForget(projectSessionTasksToKanban(projectRoot, tasks, sessionId));\n}\n\nexport function mirrorSessionPlanToKanban(\n projectRoot: string | undefined,\n items: readonly PlanItem[],\n sessionId: string,\n): void {\n fireAndForget(projectSessionPlanToKanban(projectRoot, items, sessionId));\n}\n\n/**\n * Bind all live session work paths to Kanban. Todo changes are observed from\n * ConversationState; plan/task sidecars are watched so slash commands, WebUI,\n * plugins, and tools all pass through the same board.\n */\nexport function attachSessionKanbanMirror(context: Context): () => void {\n const existing = bindings.get(context);\n if (existing) return existing;\n\n const attachedProjectRoot = context.projectRoot ?? '';\n let registeredSessionId = '';\n const syncActiveSessionRegistration = () => {\n if (!attachedProjectRoot) return;\n const currentSessionId = context.session?.id ?? '';\n if (currentSessionId === registeredSessionId) return;\n if (registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));\n }\n registeredSessionId = currentSessionId;\n if (registeredSessionId) {\n retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n }\n };\n syncActiveSessionRegistration();\n\n let watcher: FSWatcher | null = null;\n let watchedDir = '';\n let timer: NodeJS.Timeout | null = null;\n let boardWatcher: FSWatcher | null = null;\n let watchedBoardId = '';\n let boardTimer: NodeJS.Timeout | null = null;\n let presenceTimer: NodeJS.Timeout | null = null;\n\n const sessionId = () => context.session?.id ?? '';\n const refreshFiles = async () => {\n const id = sessionId();\n if (!id) return;\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n };\n\n const refreshBoard = async () => {\n if (!watchedBoardId) return;\n const board = await getBoard(context.projectRoot, watchedBoardId);\n if (board) applySessionKanbanBoardToTodos(context, board);\n };\n\n const configureBoardWatcher = async () => {\n const id = sessionId();\n const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;\n if (!board || board.id === watchedBoardId) return;\n boardWatcher?.close();\n boardWatcher = null;\n watchedBoardId = board.id;\n try {\n const boardFileName = `${board.id}.json`;\n boardWatcher = watch(\n getKanbanDir(context.projectRoot),\n { persistent: false },\n (_event, filename) => {\n if (filename?.toString() !== boardFileName) return;\n if (boardTimer) clearTimeout(boardTimer);\n boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);\n },\n );\n const touchPresence = () =>\n touchKanbanPresence(context.projectRoot, board.id, {\n sessionId: id,\n agentId: context.agentId,\n agentName: context.agentName,\n });\n fireAndForget(touchPresence());\n if (presenceTimer) clearInterval(presenceTimer);\n presenceTimer = setInterval(() => fireAndForget(touchPresence()), 60_000);\n presenceTimer.unref?.();\n boardWatcher.on('error', () => {\n boardWatcher?.close();\n boardWatcher = null;\n watchedBoardId = '';\n });\n } catch {\n boardWatcher = null;\n watchedBoardId = '';\n }\n };\n\n const configureWatcher = () => {\n const planPath = context.meta['plan.path'];\n const taskPath = context.meta['task.path'];\n const candidate =\n typeof planPath === 'string' && planPath\n ? dirname(planPath)\n : typeof taskPath === 'string' && taskPath\n ? dirname(taskPath)\n : '';\n if (!candidate || candidate === watchedDir) return;\n watcher?.close();\n watcher = null;\n watchedDir = candidate;\n try {\n watcher = watch(candidate, { persistent: false }, (_event, filename) => {\n const name = filename?.toString();\n const currentPlanPath = context.meta['plan.path'];\n const currentTaskPath = context.meta['task.path'];\n const planName = typeof currentPlanPath === 'string' ? basename(currentPlanPath) : '';\n const taskName = typeof currentTaskPath === 'string' ? basename(currentTaskPath) : '';\n if (name && name !== planName && name !== taskName) return;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => fireAndForget(refreshFiles()), 60);\n });\n watcher.on('error', () => watcher?.close());\n } catch {\n // The session directory can briefly disappear during project/session\n // switches; the next meta update re-attempts the binding.\n watcher = null;\n watchedDir = '';\n }\n };\n\n const unsubscribe = context.state.onChange((change) => {\n if (change.kind === 'todos_replaced' && !suppressedTodoMirrors.has(context)) {\n // ConversationState auto-clears an all-done tactical list. Project the\n // pre-clear completion snapshot so every card reaches Done atomically.\n mirrorSessionTodosToKanban(\n context.projectRoot,\n change.completedSnapshot ?? change.todos,\n sessionId(),\n );\n return;\n }\n if (change.kind === 'meta_set' && (change.key === 'plan.path' || change.key === 'task.path')) {\n syncActiveSessionRegistration();\n configureWatcher();\n fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));\n fireAndForget(configureBoardWatcher());\n fireAndForget(refreshFiles());\n }\n });\n\n configureWatcher();\n fireAndForget(configureBoardWatcher());\n\n const detach = () => {\n unsubscribe();\n if (timer) clearTimeout(timer);\n if (boardTimer) clearTimeout(boardTimer);\n if (presenceTimer) clearInterval(presenceTimer);\n watcher?.close();\n boardWatcher?.close();\n bindings.delete(context);\n if (attachedProjectRoot && registeredSessionId) {\n releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);\n fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));\n registeredSessionId = '';\n }\n };\n bindings.set(context, detach);\n return detach;\n}\n\n/** Fully hydrate a session board before a host announces the session as ready. */\nexport async function hydrateSessionKanban(context: Context): Promise<KanbanBoard | null> {\n const id = context.session?.id ?? '';\n if (!id) return null;\n await cleanupEmptySessionKanbanBoards(context.projectRoot, id);\n let board = await ensureSessionKanbanBoard(context.projectRoot, id);\n if (context.todos.length) {\n board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);\n }\n const planPath = context.meta['plan.path'];\n if (typeof planPath === 'string' && planPath) {\n const plan = await loadPlan(planPath);\n if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);\n }\n const taskPath = context.meta['task.path'];\n if (typeof taskPath === 'string' && taskPath) {\n const tasks = await loadTasks(taskPath);\n if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);\n }\n return board;\n}\n\nexport interface SessionKanbanSourceUpdate {\n source: 'todo' | 'task' | 'plan' | null;\n todos?: TodoItem[] | undefined;\n tasks?: TaskFile | undefined;\n plan?: PlanFile | undefined;\n}\n\nfunction sourceStatus(task: KanbanTask): TaskStatus {\n if (task.status === 'completed') return 'completed';\n if (task.status === 'in_progress') return 'in_progress';\n if (task.status === 'review') return 'review';\n if (task.status === 'blocked') return 'blocked';\n if (task.status === 'failed') return 'failed';\n return 'pending';\n}\n\nfunction todoStatus(task: KanbanTask): TodoItem['status'] {\n const status = sourceStatus(task);\n if (status === 'completed') return 'completed';\n if (status === 'in_progress' || status === 'review') return 'in_progress';\n return 'pending';\n}\n\nfunction sessionTodoFromTask(task: KanbanTask): TodoItem {\n return {\n id: task.origin?.taskId ?? task.id,\n content: task.title,\n status: todoStatus(task),\n ...(task.description ? { activeForm: task.description } : {}),\n };\n}\n\nfunction sameTodos(left: readonly TodoItem[], right: readonly TodoItem[]): boolean {\n return (\n left.length === right.length &&\n left.every((todo, index) => {\n const candidate = right[index];\n return (\n candidate?.id === todo.id &&\n candidate.content === todo.content &&\n candidate.status === todo.status &&\n candidate.activeForm === todo.activeForm &&\n candidate.promotedFromPlan === todo.promotedFromPlan &&\n candidate.promotedFromTask === todo.promotedFromTask\n );\n })\n );\n}\n\n/**\n * Replace the tactical todo list from the current cards on a session-owned board.\n *\n * Existing mirrored todo cards retain their stable source ids. New cards created\n * by a Kanban worker become todos under their card ids, so reassessment can add,\n * split, merge, reprioritize, or remove work without being overwritten by the\n * todo list that happened to exist when the run started.\n */\nexport function applySessionKanbanBoardToTodos(context: Context, board: KanbanBoard): TodoItem[] {\n const sessionId = context.session?.id ?? '';\n if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {\n return [...context.todos];\n }\n\n const projectedTodos = board.tasks\n .filter(\n (task) =>\n task.status !== 'archived' &&\n (!task.origin ||\n task.origin.system === 'session-todo' ||\n (task.origin.graphId ?? '').startsWith('todo:')),\n )\n .sort((left, right) => {\n const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;\n const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;\n return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);\n })\n .map(sessionTodoFromTask);\n\n const allCompleted =\n projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === 'completed');\n const effectiveTodos = allCompleted ? [] : projectedTodos;\n if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];\n suppressedTodoMirrors.add(context);\n try {\n context.state.replaceTodos(projectedTodos);\n } finally {\n suppressedTodoMirrors.delete(context);\n }\n notifyTodoUpdate(context, context.todos);\n broadcastTodoUpdate(context, context.todos);\n return [...context.todos];\n}\n\n/** Reflect a TUI/WebUI Kanban card edit back to its originating work list. */\nexport async function applySessionKanbanTaskToSource(\n context: Context,\n task: KanbanTask,\n options: { remove?: boolean | undefined } = {},\n): Promise<SessionKanbanSourceUpdate> {\n const originId = task.origin?.taskId;\n const graphId = task.origin?.graphId ?? '';\n if (!originId) return { source: null };\n\n if (task.origin?.system === 'session-todo' || graphId.startsWith('todo:')) {\n const next = options.remove\n ? context.todos.filter((todo) => todo.id !== originId)\n : context.todos.map((todo) =>\n todo.id === originId\n ? {\n ...todo,\n content: task.title,\n status:\n sourceStatus(task) === 'completed'\n ? ('completed' as const)\n : sourceStatus(task) === 'in_progress' || sourceStatus(task) === 'review'\n ? ('in_progress' as const)\n : ('pending' as const),\n }\n : todo,\n );\n suppressedTodoMirrors.add(context);\n try {\n context.state.replaceTodos(next);\n } finally {\n suppressedTodoMirrors.delete(context);\n }\n return { source: 'todo', todos: [...context.todos] };\n }\n\n const id = context.session?.id ?? '';\n if (task.origin?.system === 'session-plan' || graphId.startsWith('plan:')) {\n const planPath = context.meta['plan.path'];\n if (typeof planPath !== 'string' || !planPath) return { source: 'plan' };\n const plan = await mutatePlan(planPath, id, (file) => ({\n ...file,\n updatedAt: new Date().toISOString(),\n items: options.remove\n ? file.items.filter((item) => item.id !== originId)\n : file.items.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n details: task.description,\n status:\n task.status === 'completed'\n ? ('done' as const)\n : task.status === 'in_progress' || task.status === 'review'\n ? ('in_progress' as const)\n : ('open' as const),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'plan', plan };\n }\n\n if (\n task.origin?.system === 'session-task' ||\n task.origin?.system === 'session' ||\n graphId.startsWith('session:')\n ) {\n const taskPath = context.meta['task.path'];\n if (typeof taskPath !== 'string' || !taskPath) return { source: 'task' };\n const tasks = await mutateTasks(taskPath, id, (file) => ({\n ...file,\n tasks: options.remove\n ? file.tasks.filter((item) => item.id !== originId)\n : file.tasks.map((item) =>\n item.id === originId\n ? {\n ...item,\n title: task.title,\n description: task.description,\n status: sourceStatus(task),\n updatedAt: new Date().toISOString(),\n }\n : item,\n ),\n }));\n return { source: 'task', tasks };\n }\n\n return { source: null };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["loadPlan", "loadTasks", "board", "loadPlan", "loadTasks"]
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-icon-map.d.ts","sourceRoot":"","sources":["../src/tool-icon-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,KAAK,GACL,KAAK,GACL,MAAM,GACN,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,MAAM,GACN,MAAM,GACN,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,MAAM,GACN,MAAM,GACN,UAAU,GACV,OAAO,GACP,UAAU,CAAC;AAEf;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"tool-icon-map.d.ts","sourceRoot":"","sources":["../src/tool-icon-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,KAAK,GACL,KAAK,GACL,MAAM,GACN,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,MAAM,GACN,MAAM,GACN,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,MAAM,GACN,MAAM,GACN,UAAU,GACV,OAAO,GACP,UAAU,CAAC;AAEf;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAsIpD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAExD;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE,cAAc,CAyB/D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UAAuB,CAAC"}
|
package/dist/tool-icons.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-icons.d.ts","sourceRoot":"","sources":["../src/tool-icons.ts"],"names":[],"mappings":"AAYA,6DAA6D;AAC7D,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,KAAK,GACL,KAAK,GACL,MAAM,GACN,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,MAAM,GACN,MAAM,GACN,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,MAAM,GACN,MAAM,GACN,UAAU,GACV,OAAO,GACP,UAAU,CAAC;AAEf,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAyBlE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"tool-icons.d.ts","sourceRoot":"","sources":["../src/tool-icons.ts"],"names":[],"mappings":"AAYA,6DAA6D;AAC7D,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,KAAK,GACL,KAAK,GACL,MAAM,GACN,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,MAAM,GACN,MAAM,GACN,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,MAAM,GACN,MAAM,GACN,UAAU,GACV,OAAO,GACP,UAAU,CAAC;AAEf,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,UAAU,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAyBlE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAsFpD,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAGpD"}
|
package/dist/tool-icons.js
CHANGED
|
@@ -128,7 +128,13 @@ var TOOL_ICON_MAP = {
|
|
|
128
128
|
memory_graph: "brain",
|
|
129
129
|
memory_verify: "brain",
|
|
130
130
|
memory_hygiene: "brain",
|
|
131
|
-
memory_candidates: "brain"
|
|
131
|
+
memory_candidates: "brain",
|
|
132
|
+
// ── language intelligence ──
|
|
133
|
+
lsp_diagnostics: "code",
|
|
134
|
+
lsp_definition: "search",
|
|
135
|
+
lsp_completion: "code",
|
|
136
|
+
lsp_rename: "edit",
|
|
137
|
+
"codebase-lsp-search": "index"
|
|
132
138
|
};
|
|
133
139
|
function getToolIcon(name) {
|
|
134
140
|
if (!name) return "fallback";
|
package/dist/tool-icons.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/tool-icons.ts"],
|
|
4
|
-
"sourcesContent": ["// Canonical, surface-agnostic per-tool icon identity + color.\n//\n// This is PURE DATA (no node imports) so it is safe to import from the browser\n// (WebUI) as well as the TUI. Each surface maps a `ToolIconId` to its own\n// rendering primitive \u2014 the WebUI to a lucide-react component (`tool-icon.ts`),\n// the TUI to a unicode glyph (`tool-glyph.ts`) \u2014 and pulls the shared color\n// from `TOOL_ICON_CONFIG` so both stay in lockstep.\n//\n// Add a new tool by giving it an entry in `TOOL_ICON_MAP` (name \u2192 id). The id\n// must be one of the `ToolIconId` union members below; every id already has a\n// color, a lucide component, and a glyph.\n\n/** The closed set of visual identities a tool can map to. */\nexport type ToolIconId =\n | 'file'\n | 'edit'\n | 'search'\n | 'folder'\n | 'terminal'\n | 'web'\n | 'git'\n | 'tree'\n | 'code'\n | 'test'\n | 'package'\n | 'document'\n | 'scaffold'\n | 'todo'\n | 'plan'\n | 'task'\n | 'meta'\n | 'index'\n | 'json'\n | 'diff'\n | 'logs'\n | 'settings'\n | 'brain'\n | 'fallback';\n\n/** Canonical hex color per icon id \u2014 the single source both surfaces read. */\nexport const TOOL_ICON_CONFIG: Record<ToolIconId, { color: string }> = {\n file: { color: '#60a5fa' }, // blue\n edit: { color: '#fbbf24' }, // amber\n search: { color: '#a78bfa' }, // violet\n folder: { color: '#38bdf8' }, // sky\n terminal: { color: '#67e8f9' }, // cyan-light\n web: { color: '#34d399' }, // emerald\n git: { color: '#a3e635' }, // lime\n tree: { color: '#22d3ee' }, // cyan\n code: { color: '#818cf8' }, // indigo\n test: { color: '#4ade80' }, // green\n package: { color: '#f472b6' }, // pink\n document: { color: '#94a3b8' }, // slate\n scaffold: { color: '#c084fc' }, // purple\n todo: { color: '#facc15' }, // yellow\n plan: { color: '#2dd4bf' }, // teal\n task: { color: '#5eead4' }, // teal-light\n meta: { color: '#cbd5e1' }, // slate-light\n index: { color: '#06b6d4' }, // cyan-dark\n json: { color: '#eab308' }, // yellow-dark\n diff: { color: '#93c5fd' }, // blue-light\n logs: { color: '#a3a3a3' }, // neutral\n settings: { color: '#9ca3af' }, // gray\n brain: { color: '#e879f9' }, // fuchsia\n fallback: { color: '#9ca3af' }, // gray\n};\n\n/**\n * Tool name (and common alias) \u2192 icon id. Keys are lowercase; `getToolIcon`\n * lowercases the query, so lookups are case-insensitive. Covers every builtin\n * tool plus the aliases models commonly emit.\n */\nexport const TOOL_ICON_MAP: Record<string, ToolIconId> = {\n // \u2500\u2500 file IO \u2500\u2500\n read: 'file',\n cat: 'file',\n view: 'file',\n write: 'file',\n create: 'file',\n edit: 'edit',\n replace: 'edit',\n str_replace: 'edit',\n multi_edit: 'edit',\n patch: 'diff',\n // \u2500\u2500 search \u2500\u2500\n grep: 'search',\n search: 'search',\n rg: 'search',\n ripgrep: 'search',\n glob: 'search',\n find: 'search',\n // \u2500\u2500 navigation \u2500\u2500\n folder: 'folder',\n ls: 'folder',\n list: 'folder',\n set_working_dir: 'folder',\n tree: 'tree',\n // \u2500\u2500 shell \u2500\u2500\n bash: 'terminal',\n shell: 'terminal',\n sh: 'terminal',\n exec: 'terminal',\n run: 'terminal',\n command: 'terminal',\n // \u2500\u2500 web \u2500\u2500\n fetch: 'web',\n web_fetch: 'web',\n web_search: 'web',\n // \u2500\u2500 vcs \u2500\u2500\n git: 'git',\n diff: 'diff',\n // \u2500\u2500 code quality \u2500\u2500\n lint: 'code',\n format: 'settings',\n typecheck: 'code',\n language_info: 'code',\n language: 'code',\n language_package: 'package',\n test: 'test',\n // \u2500\u2500 packages \u2500\u2500\n install: 'package',\n outdated: 'package',\n audit: 'package',\n // \u2500\u2500 docs / scaffold \u2500\u2500\n document: 'document',\n scaffold: 'scaffold',\n // \u2500\u2500 planning / work tracking \u2500\u2500\n todo: 'todo',\n plan: 'plan',\n task: 'task',\n // \u2500\u2500 data \u2500\u2500\n json: 'json',\n index: 'index',\n logs: 'logs',\n // \u2500\u2500 meta / tooling \u2500\u2500\n mode: 'meta',\n tool_search: 'meta',\n tool_use: 'meta',\n batch_tool_use: 'meta',\n tool_help: 'meta',\n // \u2500\u2500 memory \u2500\u2500\n remember: 'brain',\n forget: 'brain',\n search_memory: 'brain',\n find_related_memories: 'brain',\n memory_for_file: 'brain',\n memory_for_path: 'brain',\n memory_search: 'brain',\n memory_graph: 'brain',\n memory_verify: 'brain',\n memory_hygiene: 'brain',\n memory_candidates: 'brain',\n};\n\n/**\n * Resolve a tool name to its canonical `ToolIconId`. Case-insensitive.\n * Unknown names \u2014 including `mcp__*` server tools and plugin tools \u2014 fall back\n * to `'fallback'`.\n */\nexport function getToolIcon(name: string): ToolIconId {\n if (!name) return 'fallback';\n return TOOL_ICON_MAP[name.toLowerCase()] ?? 'fallback';\n}\n"],
|
|
5
|
-
"mappings": ";AAwCO,IAAM,mBAA0D;AAAA,EACrE,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,QAAQ,EAAE,OAAO,UAAU;AAAA;AAAA,EAC3B,QAAQ,EAAE,OAAO,UAAU;AAAA;AAAA,EAC3B,UAAU,EAAE,OAAO,UAAU;AAAA;AAAA,EAC7B,KAAK,EAAE,OAAO,UAAU;AAAA;AAAA,EACxB,KAAK,EAAE,OAAO,UAAU;AAAA;AAAA,EACxB,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,SAAS,EAAE,OAAO,UAAU;AAAA;AAAA,EAC5B,UAAU,EAAE,OAAO,UAAU;AAAA;AAAA,EAC7B,UAAU,EAAE,OAAO,UAAU;AAAA;AAAA,EAC7B,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,OAAO,EAAE,OAAO,UAAU;AAAA;AAAA,EAC1B,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,MAAM,EAAE,OAAO,UAAU;AAAA;AAAA,EACzB,UAAU,EAAE,OAAO,UAAU;AAAA;AAAA,EAC7B,OAAO,EAAE,OAAO,UAAU;AAAA;AAAA,EAC1B,UAAU,EAAE,OAAO,UAAU;AAAA;AAC/B;AAOO,IAAM,gBAA4C;AAAA;AAAA,EAEvD,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,SAAS;AAAA,EACT,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,OAAO;AAAA;AAAA,EAEP,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,MAAM;AAAA,EACN,MAAM;AAAA;AAAA,EAEN,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,iBAAiB;AAAA,EACjB,MAAM;AAAA;AAAA,EAEN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,KAAK;AAAA,EACL,SAAS;AAAA;AAAA,EAET,OAAO;AAAA,EACP,WAAW;AAAA,EACX,YAAY;AAAA;AAAA,EAEZ,KAAK;AAAA,EACL,MAAM;AAAA;AAAA,EAEN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,eAAe;AAAA,EACf,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB,MAAM;AAAA;AAAA,EAEN,SAAS;AAAA,EACT,UAAU;AAAA,EACV,OAAO;AAAA;AAAA,EAEP,UAAU;AAAA,EACV,UAAU;AAAA;AAAA,EAEV,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA;AAAA,EAEN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AAAA;AAAA,EAEN,MAAM;AAAA,EACN,aAAa;AAAA,EACb,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,WAAW;AAAA;AAAA,EAEX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,eAAe;AAAA,EACf,uBAAuB;AAAA,EACvB,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,eAAe;AAAA,EACf,gBAAgB;AAAA,EAChB,mBAAmB;
|
|
4
|
+
"sourcesContent": ["// Canonical, surface-agnostic per-tool icon identity + color.\n//\n// This is PURE DATA (no node imports) so it is safe to import from the browser\n// (WebUI) as well as the TUI. Each surface maps a `ToolIconId` to its own\n// rendering primitive \u2014 the WebUI to a lucide-react component (`tool-icon.ts`),\n// the TUI to a unicode glyph (`tool-glyph.ts`) \u2014 and pulls the shared color\n// from `TOOL_ICON_CONFIG` so both stay in lockstep.\n//\n// Add a new tool by giving it an entry in `TOOL_ICON_MAP` (name \u2192 id). The id\n// must be one of the `ToolIconId` union members below; every id already has a\n// color, a lucide component, and a glyph.\n\n/** The closed set of visual identities a tool can map to. */\nexport type ToolIconId =\n | 'file'\n | 'edit'\n | 'search'\n | 'folder'\n | 'terminal'\n | 'web'\n | 'git'\n | 'tree'\n | 'code'\n | 'test'\n | 'package'\n | 'document'\n | 'scaffold'\n | 'todo'\n | 'plan'\n | 'task'\n | 'meta'\n | 'index'\n | 'json'\n | 'diff'\n | 'logs'\n | 'settings'\n | 'brain'\n | 'fallback';\n\n/** Canonical hex color per icon id \u2014 the single source both surfaces read. */\nexport const TOOL_ICON_CONFIG: Record<ToolIconId, { color: string }> = {\n file: { color: '#60a5fa' }, // blue\n edit: { color: '#fbbf24' }, // amber\n search: { color: '#a78bfa' }, // violet\n folder: { color: '#38bdf8' }, // sky\n terminal: { color: '#67e8f9' }, // cyan-light\n web: { color: '#34d399' }, // emerald\n git: { color: '#a3e635' }, // lime\n tree: { color: '#22d3ee' }, // cyan\n code: { color: '#818cf8' }, // indigo\n test: { color: '#4ade80' }, // green\n package: { color: '#f472b6' }, // pink\n document: { color: '#94a3b8' }, // slate\n scaffold: { color: '#c084fc' }, // purple\n todo: { color: '#facc15' }, // yellow\n plan: { color: '#2dd4bf' }, // teal\n task: { color: '#5eead4' }, // teal-light\n meta: { color: '#cbd5e1' }, // slate-light\n index: { color: '#06b6d4' }, // cyan-dark\n json: { color: '#eab308' }, // yellow-dark\n diff: { color: '#93c5fd' }, // blue-light\n logs: { color: '#a3a3a3' }, // neutral\n settings: { color: '#9ca3af' }, // gray\n brain: { color: '#e879f9' }, // fuchsia\n fallback: { color: '#9ca3af' }, // gray\n};\n\n/**\n * Tool name (and common alias) \u2192 icon id. Keys are lowercase; `getToolIcon`\n * lowercases the query, so lookups are case-insensitive. Covers every builtin\n * tool plus the aliases models commonly emit.\n */\nexport const TOOL_ICON_MAP: Record<string, ToolIconId> = {\n // \u2500\u2500 file IO \u2500\u2500\n read: 'file',\n cat: 'file',\n view: 'file',\n write: 'file',\n create: 'file',\n edit: 'edit',\n replace: 'edit',\n str_replace: 'edit',\n multi_edit: 'edit',\n patch: 'diff',\n // \u2500\u2500 search \u2500\u2500\n grep: 'search',\n search: 'search',\n rg: 'search',\n ripgrep: 'search',\n glob: 'search',\n find: 'search',\n // \u2500\u2500 navigation \u2500\u2500\n folder: 'folder',\n ls: 'folder',\n list: 'folder',\n set_working_dir: 'folder',\n tree: 'tree',\n // \u2500\u2500 shell \u2500\u2500\n bash: 'terminal',\n shell: 'terminal',\n sh: 'terminal',\n exec: 'terminal',\n run: 'terminal',\n command: 'terminal',\n // \u2500\u2500 web \u2500\u2500\n fetch: 'web',\n web_fetch: 'web',\n web_search: 'web',\n // \u2500\u2500 vcs \u2500\u2500\n git: 'git',\n diff: 'diff',\n // \u2500\u2500 code quality \u2500\u2500\n lint: 'code',\n format: 'settings',\n typecheck: 'code',\n language_info: 'code',\n language: 'code',\n language_package: 'package',\n test: 'test',\n // \u2500\u2500 packages \u2500\u2500\n install: 'package',\n outdated: 'package',\n audit: 'package',\n // \u2500\u2500 docs / scaffold \u2500\u2500\n document: 'document',\n scaffold: 'scaffold',\n // \u2500\u2500 planning / work tracking \u2500\u2500\n todo: 'todo',\n plan: 'plan',\n task: 'task',\n // \u2500\u2500 data \u2500\u2500\n json: 'json',\n index: 'index',\n logs: 'logs',\n // \u2500\u2500 meta / tooling \u2500\u2500\n mode: 'meta',\n tool_search: 'meta',\n tool_use: 'meta',\n batch_tool_use: 'meta',\n tool_help: 'meta',\n // \u2500\u2500 memory \u2500\u2500\n remember: 'brain',\n forget: 'brain',\n search_memory: 'brain',\n find_related_memories: 'brain',\n memory_for_file: 'brain',\n memory_for_path: 'brain',\n memory_search: 'brain',\n memory_graph: 'brain',\n memory_verify: 'brain',\n memory_hygiene: 'brain',\n memory_candidates: 'brain',\n // \u2500\u2500 language intelligence \u2500\u2500\n lsp_diagnostics: 'code',\n lsp_definition: 'search',\n lsp_completion: 'code',\n lsp_rename: 'edit',\n 'codebase-lsp-search': 'index',\n};\n\n/**\n * Resolve a tool name to its canonical `ToolIconId`. Case-insensitive.\n * Unknown names \u2014 including `mcp__*` server tools and plugin tools \u2014 fall back\n * to `'fallback'`.\n */\nexport function getToolIcon(name: string): ToolIconId {\n if (!name) return 'fallback';\n return TOOL_ICON_MAP[name.toLowerCase()] ?? 'fallback';\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/tree.js
CHANGED
|
@@ -45,8 +45,8 @@ var DEFAULT_IGNORE = [
|
|
|
45
45
|
var treeTool = {
|
|
46
46
|
name: "tree",
|
|
47
47
|
category: "Filesystem",
|
|
48
|
-
description: "Display a
|
|
49
|
-
usageHint: "
|
|
48
|
+
description: "Display a project or subpath directory tree. Use it for structural layout; use index-backed `codebase-search` first for code understanding when it is live.",
|
|
49
|
+
usageHint: "DIRECTORY-LAYOUT EXPLORATION:\n\n- When `codebase-search` is live, use it first for symbols and concepts; use `tree` when the directory hierarchy itself matters.\n- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\n- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\nOutput is truncated for very large trees.",
|
|
50
50
|
permission: "auto",
|
|
51
51
|
mutating: false,
|
|
52
52
|
capabilities: ["fs.read"],
|
package/dist/tree.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/tree.ts", "../src/_util.ts"],
|
|
4
|
-
"sourcesContent": ["import { expectDefined } from '@wrongstack/core';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { Tool, ToolProgressEvent, ToolStreamEvent } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\nconst DEFAULT_IGNORE = [\n 'node_modules',\n '.git',\n 'dist',\n 'build',\n '.next',\n 'coverage',\n '__pycache__',\n '.wrongstack',\n '.ssh',\n '.gnupg',\n '.aws',\n];\n\ninterface TreeInput {\n path?: string | undefined;\n depth?: number | undefined;\n glob?: string | undefined;\n exclude?: string[] | undefined;\n show_files?: boolean | undefined;\n show_dirs?: boolean | undefined;\n show_hidden?: boolean | undefined;\n}\n\ninterface TreeOutput {\n tree: string;\n total_files: number;\n total_dirs: number;\n truncated: boolean;\n path: string;\n}\n\nexport const treeTool: Tool<TreeInput, TreeOutput> = {\n name: 'tree',\n category: 'Filesystem',\n description:\n 'Display a directory tree of the project (or a subpath). This is the recommended way to explore the high-level structure of a codebase before reading specific files.',\n usageHint:\n 'BEST PRACTICE FOR INITIAL EXPLORATION:\\n\\n' +\n '- Call early when working with an unfamiliar project or module.\\n' +\n '- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\\n' +\n '- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\\n' +\n 'Output is truncated for very large trees.',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'tree',\n timeoutMs: 15_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Root directory to display the tree from (defaults to project root).',\n },\n depth: {\n type: 'integer',\n description: 'Maximum directory depth to traverse (default 3, use 0 for unlimited).',\n minimum: 0,\n maximum: 20,\n },\n glob: {\n type: 'string',\n description: 'Only include files matching this glob pattern.',\n },\n exclude: {\n type: 'array',\n items: { type: 'string' },\n description: 'List of directory names to completely ignore.',\n },\n show_files: {\n type: 'boolean',\n description: 'Whether to show individual files (default true).',\n },\n show_dirs: {\n type: 'boolean',\n description: 'Whether to show directories (default true).',\n },\n show_hidden: {\n type: 'boolean',\n description: 'Show hidden files starting with . (default: false)',\n },\n },\n },\n async execute(input, ctx, opts) {\n let final: TreeOutput | undefined;\n const executeStream = treeTool.executeStream;\n if (!executeStream) throw new Error('treeTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('tree: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx): AsyncGenerator<ToolStreamEvent<TreeOutput>> {\n const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;\n const maxDepth = input.depth ?? 3;\n const showFiles = input.show_files ?? true;\n const showDirs = input.show_dirs ?? true;\n const showHidden = input.show_hidden ?? false;\n const exclude = new Set([...DEFAULT_IGNORE, ...(input.exclude ?? [])]);\n const filterGlob = input.glob;\n\n const lines: string[] = [basePath];\n const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };\n\n // Walker pushes progress into an async queue; the generator drains it.\n const queue: ToolProgressEvent[] = [];\n const FLUSH_EVERY = 200; // emit metric every 200 entries seen\n let lastEmittedTotal = 0;\n\n const tickProgress = () => {\n const seen = totals.totalFiles.value + totals.totalDirs.value;\n if (seen - lastEmittedTotal >= FLUSH_EVERY) {\n queue.push({\n type: 'metric',\n text: `${seen} entries`,\n data: { files: totals.totalFiles.value, dirs: totals.totalDirs.value },\n });\n lastEmittedTotal = seen;\n }\n };\n\n const walkPromise = walkDir(basePath, 0, {\n maxDepth,\n exclude,\n showFiles,\n showDirs,\n showHidden,\n filterGlob,\n lines,\n prefix: '',\n isLast: true,\n totalFiles: totals.totalFiles,\n totalDirs: totals.totalDirs,\n onProgress: tickProgress,\n });\n\n // Race the walk against periodic flushes \u2014 yield metrics while it runs.\n let walkDone = false;\n walkPromise.finally(() => {\n walkDone = true;\n });\n\n while (!walkDone || queue.length > 0) {\n if (queue.length > 0) {\n yield expectDefined(queue.shift());\n } else {\n // Race the walk completion against a short tick so we don't busy-\n // spin while the producer fills the queue. Previously the\n // setTimeout was never cleared when walkPromise won \u2014 one stray\n // timer per drain iteration accumulated on the event loop.\n let pollTimer: ReturnType<typeof setTimeout> | undefined;\n const poll = new Promise<void>((r) => {\n pollTimer = setTimeout(r, 50);\n });\n try {\n await Promise.race([walkPromise, poll]).catch(() => undefined);\n } finally {\n if (pollTimer) clearTimeout(pollTimer);\n }\n }\n }\n await walkPromise; // surface any error\n\n yield {\n type: 'final',\n output: {\n tree: lines.join('\\n'),\n total_files: totals.totalFiles.value,\n total_dirs: totals.totalDirs.value,\n truncated: false,\n path: basePath,\n },\n };\n },\n};\n\ninterface WalkOptions {\n maxDepth: number;\n exclude: Set<string>;\n showFiles: boolean;\n showDirs: boolean;\n showHidden: boolean;\n filterGlob?: string | undefined;\n lines: string[];\n prefix: string;\n isLast: boolean;\n totalFiles: { value: number };\n totalDirs: { value: number };\n onProgress?: (() => void) | undefined;\n}\n\nasync function walkDir(dir: string, depth: number, opts: WalkOptions): Promise<void> {\n const entries = await fs\n .readdir(dir, { withFileTypes: true })\n .catch(() => [] as import('node:fs').Dirent[]);\n\n const filtered = entries.filter((e) => {\n if (!opts.showHidden && e.name.startsWith('.')) return false;\n if (opts.exclude.has(e.name)) return false;\n return true;\n });\n\n if (depth > 0) {\n const dirCount = filtered.filter((e) => e.isDirectory()).length;\n const fileCount = filtered.filter((e) => e.isFile()).length;\n opts.totalDirs.value += dirCount;\n opts.totalFiles.value += fileCount;\n opts.onProgress?.();\n }\n\n const items = filtered.sort((a, b) => {\n if (a.isDirectory() && !b.isDirectory()) return -1;\n if (!a.isDirectory() && b.isDirectory()) return 1;\n return a.name.localeCompare(b.name);\n });\n\n for (let i = 0; i < items.length; i++) {\n const entry = items[i];\n /* v8 ignore next -- i is bounded by items.length, so entry is always defined; defensive. */\n if (!entry) continue;\n const isLast = i === items.length - 1;\n const connector = opts.isLast ? ' ' : '\u2502 ';\n const branch = isLast ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ';\n const displayName = entry.name + (entry.isDirectory() ? '/' : '');\n\n if (!opts.showDirs && entry.isDirectory()) continue;\n if (!opts.showFiles && entry.isFile()) continue;\n\n opts.lines.push(opts.prefix + branch + displayName);\n\n if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {\n const childPrefix = opts.prefix + connector;\n await walkDir(path.join(dir, entry.name), depth + 1, {\n ...opts,\n prefix: childPrefix,\n isLast,\n });\n }\n }\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { expectDefined } from '@wrongstack/core';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { Tool, ToolProgressEvent, ToolStreamEvent } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\nconst DEFAULT_IGNORE = [\n 'node_modules',\n '.git',\n 'dist',\n 'build',\n '.next',\n 'coverage',\n '__pycache__',\n '.wrongstack',\n '.ssh',\n '.gnupg',\n '.aws',\n];\n\ninterface TreeInput {\n path?: string | undefined;\n depth?: number | undefined;\n glob?: string | undefined;\n exclude?: string[] | undefined;\n show_files?: boolean | undefined;\n show_dirs?: boolean | undefined;\n show_hidden?: boolean | undefined;\n}\n\ninterface TreeOutput {\n tree: string;\n total_files: number;\n total_dirs: number;\n truncated: boolean;\n path: string;\n}\n\nexport const treeTool: Tool<TreeInput, TreeOutput> = {\n name: 'tree',\n category: 'Filesystem',\n description:\n 'Display a project or subpath directory tree. Use it for structural layout; use index-backed `codebase-search` first for code understanding when it is live.',\n usageHint:\n 'DIRECTORY-LAYOUT EXPLORATION:\\n\\n' +\n '- When `codebase-search` is live, use it first for symbols and concepts; use `tree` when the directory hierarchy itself matters.\\n' +\n '- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\\n' +\n '- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\\n' +\n 'Output is truncated for very large trees.',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'tree',\n timeoutMs: 15_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Root directory to display the tree from (defaults to project root).',\n },\n depth: {\n type: 'integer',\n description: 'Maximum directory depth to traverse (default 3, use 0 for unlimited).',\n minimum: 0,\n maximum: 20,\n },\n glob: {\n type: 'string',\n description: 'Only include files matching this glob pattern.',\n },\n exclude: {\n type: 'array',\n items: { type: 'string' },\n description: 'List of directory names to completely ignore.',\n },\n show_files: {\n type: 'boolean',\n description: 'Whether to show individual files (default true).',\n },\n show_dirs: {\n type: 'boolean',\n description: 'Whether to show directories (default true).',\n },\n show_hidden: {\n type: 'boolean',\n description: 'Show hidden files starting with . (default: false)',\n },\n },\n },\n async execute(input, ctx, opts) {\n let final: TreeOutput | undefined;\n const executeStream = treeTool.executeStream;\n if (!executeStream) throw new Error('treeTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('tree: stream ended without final event');\n return final;\n },\n async *executeStream(input, ctx): AsyncGenerator<ToolStreamEvent<TreeOutput>> {\n const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;\n const maxDepth = input.depth ?? 3;\n const showFiles = input.show_files ?? true;\n const showDirs = input.show_dirs ?? true;\n const showHidden = input.show_hidden ?? false;\n const exclude = new Set([...DEFAULT_IGNORE, ...(input.exclude ?? [])]);\n const filterGlob = input.glob;\n\n const lines: string[] = [basePath];\n const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };\n\n // Walker pushes progress into an async queue; the generator drains it.\n const queue: ToolProgressEvent[] = [];\n const FLUSH_EVERY = 200; // emit metric every 200 entries seen\n let lastEmittedTotal = 0;\n\n const tickProgress = () => {\n const seen = totals.totalFiles.value + totals.totalDirs.value;\n if (seen - lastEmittedTotal >= FLUSH_EVERY) {\n queue.push({\n type: 'metric',\n text: `${seen} entries`,\n data: { files: totals.totalFiles.value, dirs: totals.totalDirs.value },\n });\n lastEmittedTotal = seen;\n }\n };\n\n const walkPromise = walkDir(basePath, 0, {\n maxDepth,\n exclude,\n showFiles,\n showDirs,\n showHidden,\n filterGlob,\n lines,\n prefix: '',\n isLast: true,\n totalFiles: totals.totalFiles,\n totalDirs: totals.totalDirs,\n onProgress: tickProgress,\n });\n\n // Race the walk against periodic flushes \u2014 yield metrics while it runs.\n let walkDone = false;\n walkPromise.finally(() => {\n walkDone = true;\n });\n\n while (!walkDone || queue.length > 0) {\n if (queue.length > 0) {\n yield expectDefined(queue.shift());\n } else {\n // Race the walk completion against a short tick so we don't busy-\n // spin while the producer fills the queue. Previously the\n // setTimeout was never cleared when walkPromise won \u2014 one stray\n // timer per drain iteration accumulated on the event loop.\n let pollTimer: ReturnType<typeof setTimeout> | undefined;\n const poll = new Promise<void>((r) => {\n pollTimer = setTimeout(r, 50);\n });\n try {\n await Promise.race([walkPromise, poll]).catch(() => undefined);\n } finally {\n if (pollTimer) clearTimeout(pollTimer);\n }\n }\n }\n await walkPromise; // surface any error\n\n yield {\n type: 'final',\n output: {\n tree: lines.join('\\n'),\n total_files: totals.totalFiles.value,\n total_dirs: totals.totalDirs.value,\n truncated: false,\n path: basePath,\n },\n };\n },\n};\n\ninterface WalkOptions {\n maxDepth: number;\n exclude: Set<string>;\n showFiles: boolean;\n showDirs: boolean;\n showHidden: boolean;\n filterGlob?: string | undefined;\n lines: string[];\n prefix: string;\n isLast: boolean;\n totalFiles: { value: number };\n totalDirs: { value: number };\n onProgress?: (() => void) | undefined;\n}\n\nasync function walkDir(dir: string, depth: number, opts: WalkOptions): Promise<void> {\n const entries = await fs\n .readdir(dir, { withFileTypes: true })\n .catch(() => [] as import('node:fs').Dirent[]);\n\n const filtered = entries.filter((e) => {\n if (!opts.showHidden && e.name.startsWith('.')) return false;\n if (opts.exclude.has(e.name)) return false;\n return true;\n });\n\n if (depth > 0) {\n const dirCount = filtered.filter((e) => e.isDirectory()).length;\n const fileCount = filtered.filter((e) => e.isFile()).length;\n opts.totalDirs.value += dirCount;\n opts.totalFiles.value += fileCount;\n opts.onProgress?.();\n }\n\n const items = filtered.sort((a, b) => {\n if (a.isDirectory() && !b.isDirectory()) return -1;\n if (!a.isDirectory() && b.isDirectory()) return 1;\n return a.name.localeCompare(b.name);\n });\n\n for (let i = 0; i < items.length; i++) {\n const entry = items[i];\n /* v8 ignore next -- i is bounded by items.length, so entry is always defined; defensive. */\n if (!entry) continue;\n const isLast = i === items.length - 1;\n const connector = opts.isLast ? ' ' : '\u2502 ';\n const branch = isLast ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ';\n const displayName = entry.name + (entry.isDirectory() ? '/' : '');\n\n if (!opts.showDirs && entry.isDirectory()) continue;\n if (!opts.showFiles && entry.isFile()) continue;\n\n opts.lines.push(opts.prefix + branch + displayName);\n\n if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {\n const childPrefix = opts.prefix + connector;\n await walkDir(path.join(dir, entry.name), depth + 1, {\n ...opts,\n prefix: childPrefix,\n isLast,\n });\n }\n }\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["path"]
|
|
7
7
|
}
|