@link-assistant/agent 0.0.8 → 0.0.11
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/EXAMPLES.md +80 -1
- package/MODELS.md +72 -24
- package/README.md +95 -2
- package/TOOLS.md +20 -0
- package/package.json +36 -2
- package/src/agent/agent.ts +68 -54
- package/src/auth/claude-oauth.ts +426 -0
- package/src/auth/index.ts +28 -26
- package/src/auth/plugins.ts +876 -0
- package/src/bun/index.ts +53 -43
- package/src/bus/global.ts +5 -5
- package/src/bus/index.ts +59 -53
- package/src/cli/bootstrap.js +12 -12
- package/src/cli/bootstrap.ts +6 -6
- package/src/cli/cmd/agent.ts +97 -92
- package/src/cli/cmd/auth.ts +468 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +210 -53
- package/src/cli/cmd/models.ts +30 -29
- package/src/cli/cmd/run.ts +269 -213
- package/src/cli/cmd/stats.ts +185 -146
- package/src/cli/error.ts +17 -13
- package/src/cli/ui.ts +78 -0
- package/src/command/index.ts +26 -26
- package/src/config/config.ts +528 -288
- package/src/config/markdown.ts +15 -15
- package/src/file/ripgrep.ts +201 -169
- package/src/file/time.ts +21 -18
- package/src/file/watcher.ts +51 -42
- package/src/file.ts +1 -1
- package/src/flag/flag.ts +26 -11
- package/src/format/formatter.ts +206 -162
- package/src/format/index.ts +61 -61
- package/src/global/index.ts +21 -21
- package/src/id/id.ts +47 -33
- package/src/index.js +554 -332
- package/src/json-standard/index.ts +173 -0
- package/src/mcp/index.ts +135 -128
- package/src/patch/index.ts +336 -267
- package/src/project/bootstrap.ts +15 -15
- package/src/project/instance.ts +43 -36
- package/src/project/project.ts +47 -47
- package/src/project/state.ts +37 -33
- package/src/provider/models-macro.ts +5 -5
- package/src/provider/models.ts +32 -32
- package/src/provider/opencode.js +19 -19
- package/src/provider/provider.ts +518 -277
- package/src/provider/transform.ts +143 -102
- package/src/server/project.ts +21 -21
- package/src/server/server.ts +111 -105
- package/src/session/agent.js +66 -60
- package/src/session/compaction.ts +136 -111
- package/src/session/index.ts +189 -156
- package/src/session/message-v2.ts +312 -268
- package/src/session/message.ts +73 -57
- package/src/session/processor.ts +180 -166
- package/src/session/prompt.ts +678 -533
- package/src/session/retry.ts +26 -23
- package/src/session/revert.ts +76 -62
- package/src/session/status.ts +26 -26
- package/src/session/summary.ts +97 -76
- package/src/session/system.ts +77 -63
- package/src/session/todo.ts +22 -16
- package/src/snapshot/index.ts +92 -76
- package/src/storage/storage.ts +157 -120
- package/src/tool/bash.ts +116 -106
- package/src/tool/batch.ts +73 -59
- package/src/tool/codesearch.ts +60 -53
- package/src/tool/edit.ts +319 -263
- package/src/tool/glob.ts +32 -28
- package/src/tool/grep.ts +72 -53
- package/src/tool/invalid.ts +7 -7
- package/src/tool/ls.ts +77 -64
- package/src/tool/multiedit.ts +30 -21
- package/src/tool/patch.ts +121 -94
- package/src/tool/read.ts +140 -122
- package/src/tool/registry.ts +38 -38
- package/src/tool/task.ts +93 -60
- package/src/tool/todo.ts +16 -16
- package/src/tool/tool.ts +45 -36
- package/src/tool/webfetch.ts +97 -74
- package/src/tool/websearch.ts +78 -64
- package/src/tool/write.ts +21 -15
- package/src/util/binary.ts +27 -19
- package/src/util/context.ts +8 -8
- package/src/util/defer.ts +7 -5
- package/src/util/error.ts +24 -19
- package/src/util/eventloop.ts +16 -10
- package/src/util/filesystem.ts +37 -33
- package/src/util/fn.ts +11 -8
- package/src/util/iife.ts +1 -1
- package/src/util/keybind.ts +44 -44
- package/src/util/lazy.ts +7 -7
- package/src/util/locale.ts +20 -16
- package/src/util/lock.ts +43 -38
- package/src/util/log.ts +95 -85
- package/src/util/queue.ts +8 -8
- package/src/util/rpc.ts +35 -23
- package/src/util/scrap.ts +4 -4
- package/src/util/signal.ts +5 -5
- package/src/util/timeout.ts +6 -6
- package/src/util/token.ts +2 -2
- package/src/util/wildcard.ts +38 -27
package/src/cli/cmd/stats.ts
CHANGED
|
@@ -1,98 +1,112 @@
|
|
|
1
|
-
import type { Argv } from
|
|
2
|
-
import { cmd } from
|
|
3
|
-
import { Session } from
|
|
4
|
-
import { bootstrap } from
|
|
5
|
-
import { Storage } from
|
|
6
|
-
import { Project } from
|
|
7
|
-
import { Instance } from
|
|
1
|
+
import type { Argv } from 'yargs';
|
|
2
|
+
import { cmd } from './cmd';
|
|
3
|
+
import { Session } from '../../session';
|
|
4
|
+
import { bootstrap } from '../bootstrap';
|
|
5
|
+
import { Storage } from '../../storage/storage';
|
|
6
|
+
import { Project } from '../../project/project';
|
|
7
|
+
import { Instance } from '../../project/instance';
|
|
8
8
|
|
|
9
9
|
interface SessionStats {
|
|
10
|
-
totalSessions: number
|
|
11
|
-
totalMessages: number
|
|
12
|
-
totalCost: number
|
|
10
|
+
totalSessions: number;
|
|
11
|
+
totalMessages: number;
|
|
12
|
+
totalCost: number;
|
|
13
13
|
totalTokens: {
|
|
14
|
-
input: number
|
|
15
|
-
output: number
|
|
16
|
-
reasoning: number
|
|
14
|
+
input: number;
|
|
15
|
+
output: number;
|
|
16
|
+
reasoning: number;
|
|
17
17
|
cache: {
|
|
18
|
-
read: number
|
|
19
|
-
write: number
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
toolUsage: Record<string, number
|
|
18
|
+
read: number;
|
|
19
|
+
write: number;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
toolUsage: Record<string, number>;
|
|
23
23
|
dateRange: {
|
|
24
|
-
earliest: number
|
|
25
|
-
latest: number
|
|
26
|
-
}
|
|
27
|
-
days: number
|
|
28
|
-
costPerDay: number
|
|
24
|
+
earliest: number;
|
|
25
|
+
latest: number;
|
|
26
|
+
};
|
|
27
|
+
days: number;
|
|
28
|
+
costPerDay: number;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export const StatsCommand = cmd({
|
|
32
|
-
command:
|
|
33
|
-
describe:
|
|
32
|
+
command: 'stats',
|
|
33
|
+
describe: 'show token usage and cost statistics',
|
|
34
34
|
builder: (yargs: Argv) => {
|
|
35
35
|
return yargs
|
|
36
|
-
.option(
|
|
37
|
-
describe:
|
|
38
|
-
type:
|
|
36
|
+
.option('days', {
|
|
37
|
+
describe: 'show stats for the last N days (default: all time)',
|
|
38
|
+
type: 'number',
|
|
39
39
|
})
|
|
40
|
-
.option(
|
|
41
|
-
describe:
|
|
42
|
-
type:
|
|
43
|
-
})
|
|
44
|
-
.option("project", {
|
|
45
|
-
describe: "filter by project (default: all projects, empty string: current project)",
|
|
46
|
-
type: "string",
|
|
40
|
+
.option('tools', {
|
|
41
|
+
describe: 'number of tools to show (default: all)',
|
|
42
|
+
type: 'number',
|
|
47
43
|
})
|
|
44
|
+
.option('project', {
|
|
45
|
+
describe:
|
|
46
|
+
'filter by project (default: all projects, empty string: current project)',
|
|
47
|
+
type: 'string',
|
|
48
|
+
});
|
|
48
49
|
},
|
|
49
50
|
handler: async (args) => {
|
|
50
51
|
await bootstrap(process.cwd(), async () => {
|
|
51
|
-
const stats = await aggregateSessionStats(args.days, args.project)
|
|
52
|
-
displayStats(stats, args.tools)
|
|
53
|
-
})
|
|
52
|
+
const stats = await aggregateSessionStats(args.days, args.project);
|
|
53
|
+
displayStats(stats, args.tools);
|
|
54
|
+
});
|
|
54
55
|
},
|
|
55
|
-
})
|
|
56
|
+
});
|
|
56
57
|
|
|
57
58
|
async function getCurrentProject(): Promise<Project.Info> {
|
|
58
|
-
return Instance.project
|
|
59
|
+
return Instance.project;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
async function getAllSessions(): Promise<Session.Info[]> {
|
|
62
|
-
const sessions: Session.Info[] = []
|
|
63
|
+
const sessions: Session.Info[] = [];
|
|
63
64
|
|
|
64
|
-
const projectKeys = await Storage.list([
|
|
65
|
-
const projects = await Promise.all(
|
|
65
|
+
const projectKeys = await Storage.list(['project']);
|
|
66
|
+
const projects = await Promise.all(
|
|
67
|
+
projectKeys.map((key) => Storage.read<Project.Info>(key))
|
|
68
|
+
);
|
|
66
69
|
|
|
67
70
|
for (const project of projects) {
|
|
68
|
-
if (!project) continue
|
|
71
|
+
if (!project) continue;
|
|
69
72
|
|
|
70
|
-
const sessionKeys = await Storage.list([
|
|
71
|
-
const projectSessions = await Promise.all(
|
|
73
|
+
const sessionKeys = await Storage.list(['session', project.id]);
|
|
74
|
+
const projectSessions = await Promise.all(
|
|
75
|
+
sessionKeys.map((key) => Storage.read<Session.Info>(key))
|
|
76
|
+
);
|
|
72
77
|
|
|
73
78
|
for (const session of projectSessions) {
|
|
74
79
|
if (session) {
|
|
75
|
-
sessions.push(session)
|
|
80
|
+
sessions.push(session);
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
return sessions
|
|
85
|
+
return sessions;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
async function aggregateSessionStats(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
async function aggregateSessionStats(
|
|
89
|
+
days?: number,
|
|
90
|
+
projectFilter?: string
|
|
91
|
+
): Promise<SessionStats> {
|
|
92
|
+
const sessions = await getAllSessions();
|
|
93
|
+
const DAYS_IN_SECOND = 24 * 60 * 60 * 1000;
|
|
94
|
+
const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0;
|
|
87
95
|
|
|
88
|
-
let filteredSessions = days
|
|
96
|
+
let filteredSessions = days
|
|
97
|
+
? sessions.filter((session) => session.time.updated >= cutoffTime)
|
|
98
|
+
: sessions;
|
|
89
99
|
|
|
90
100
|
if (projectFilter !== undefined) {
|
|
91
|
-
if (projectFilter ===
|
|
92
|
-
const currentProject = await getCurrentProject()
|
|
93
|
-
filteredSessions = filteredSessions.filter(
|
|
101
|
+
if (projectFilter === '') {
|
|
102
|
+
const currentProject = await getCurrentProject();
|
|
103
|
+
filteredSessions = filteredSessions.filter(
|
|
104
|
+
(session) => session.projectID === currentProject.id
|
|
105
|
+
);
|
|
94
106
|
} else {
|
|
95
|
-
filteredSessions = filteredSessions.filter(
|
|
107
|
+
filteredSessions = filteredSessions.filter(
|
|
108
|
+
(session) => session.projectID === projectFilter
|
|
109
|
+
);
|
|
96
110
|
}
|
|
97
111
|
}
|
|
98
112
|
|
|
@@ -116,46 +130,54 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
|
|
116
130
|
},
|
|
117
131
|
days: 0,
|
|
118
132
|
costPerDay: 0,
|
|
119
|
-
}
|
|
133
|
+
};
|
|
120
134
|
|
|
121
135
|
if (filteredSessions.length > 1000) {
|
|
122
|
-
console.log(
|
|
136
|
+
console.log(
|
|
137
|
+
`Large dataset detected (${filteredSessions.length} sessions). This may take a while...`
|
|
138
|
+
);
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
if (filteredSessions.length === 0) {
|
|
126
|
-
return stats
|
|
142
|
+
return stats;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
let earliestTime = Date.now()
|
|
130
|
-
let latestTime = 0
|
|
145
|
+
let earliestTime = Date.now();
|
|
146
|
+
let latestTime = 0;
|
|
131
147
|
|
|
132
|
-
const BATCH_SIZE = 20
|
|
148
|
+
const BATCH_SIZE = 20;
|
|
133
149
|
for (let i = 0; i < filteredSessions.length; i += BATCH_SIZE) {
|
|
134
|
-
const batch = filteredSessions.slice(i, i + BATCH_SIZE)
|
|
150
|
+
const batch = filteredSessions.slice(i, i + BATCH_SIZE);
|
|
135
151
|
|
|
136
152
|
const batchPromises = batch.map(async (session) => {
|
|
137
|
-
const messages = await Session.messages({ sessionID: session.id })
|
|
153
|
+
const messages = await Session.messages({ sessionID: session.id });
|
|
138
154
|
|
|
139
|
-
let sessionCost = 0
|
|
140
|
-
let sessionTokens = {
|
|
141
|
-
|
|
155
|
+
let sessionCost = 0;
|
|
156
|
+
let sessionTokens = {
|
|
157
|
+
input: 0,
|
|
158
|
+
output: 0,
|
|
159
|
+
reasoning: 0,
|
|
160
|
+
cache: { read: 0, write: 0 },
|
|
161
|
+
};
|
|
162
|
+
let sessionToolUsage: Record<string, number> = {};
|
|
142
163
|
|
|
143
164
|
for (const message of messages) {
|
|
144
|
-
if (message.info.role ===
|
|
145
|
-
sessionCost += message.info.cost || 0
|
|
165
|
+
if (message.info.role === 'assistant') {
|
|
166
|
+
sessionCost += message.info.cost || 0;
|
|
146
167
|
|
|
147
168
|
if (message.info.tokens) {
|
|
148
|
-
sessionTokens.input += message.info.tokens.input || 0
|
|
149
|
-
sessionTokens.output += message.info.tokens.output || 0
|
|
150
|
-
sessionTokens.reasoning += message.info.tokens.reasoning || 0
|
|
151
|
-
sessionTokens.cache.read += message.info.tokens.cache?.read || 0
|
|
152
|
-
sessionTokens.cache.write += message.info.tokens.cache?.write || 0
|
|
169
|
+
sessionTokens.input += message.info.tokens.input || 0;
|
|
170
|
+
sessionTokens.output += message.info.tokens.output || 0;
|
|
171
|
+
sessionTokens.reasoning += message.info.tokens.reasoning || 0;
|
|
172
|
+
sessionTokens.cache.read += message.info.tokens.cache?.read || 0;
|
|
173
|
+
sessionTokens.cache.write += message.info.tokens.cache?.write || 0;
|
|
153
174
|
}
|
|
154
175
|
}
|
|
155
176
|
|
|
156
177
|
for (const part of message.parts) {
|
|
157
|
-
if (part.type ===
|
|
158
|
-
sessionToolUsage[part.tool] =
|
|
178
|
+
if (part.type === 'tool' && part.tool) {
|
|
179
|
+
sessionToolUsage[part.tool] =
|
|
180
|
+
(sessionToolUsage[part.tool] || 0) + 1;
|
|
159
181
|
}
|
|
160
182
|
}
|
|
161
183
|
}
|
|
@@ -167,110 +189,127 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
|
|
167
189
|
sessionToolUsage,
|
|
168
190
|
earliestTime: session.time.created,
|
|
169
191
|
latestTime: session.time.updated,
|
|
170
|
-
}
|
|
171
|
-
})
|
|
192
|
+
};
|
|
193
|
+
});
|
|
172
194
|
|
|
173
|
-
const batchResults = await Promise.all(batchPromises)
|
|
195
|
+
const batchResults = await Promise.all(batchPromises);
|
|
174
196
|
|
|
175
197
|
for (const result of batchResults) {
|
|
176
|
-
earliestTime = Math.min(earliestTime, result.earliestTime)
|
|
177
|
-
latestTime = Math.max(latestTime, result.latestTime)
|
|
198
|
+
earliestTime = Math.min(earliestTime, result.earliestTime);
|
|
199
|
+
latestTime = Math.max(latestTime, result.latestTime);
|
|
178
200
|
|
|
179
|
-
stats.totalMessages += result.messageCount
|
|
180
|
-
stats.totalCost += result.sessionCost
|
|
181
|
-
stats.totalTokens.input += result.sessionTokens.input
|
|
182
|
-
stats.totalTokens.output += result.sessionTokens.output
|
|
183
|
-
stats.totalTokens.reasoning += result.sessionTokens.reasoning
|
|
184
|
-
stats.totalTokens.cache.read += result.sessionTokens.cache.read
|
|
185
|
-
stats.totalTokens.cache.write += result.sessionTokens.cache.write
|
|
201
|
+
stats.totalMessages += result.messageCount;
|
|
202
|
+
stats.totalCost += result.sessionCost;
|
|
203
|
+
stats.totalTokens.input += result.sessionTokens.input;
|
|
204
|
+
stats.totalTokens.output += result.sessionTokens.output;
|
|
205
|
+
stats.totalTokens.reasoning += result.sessionTokens.reasoning;
|
|
206
|
+
stats.totalTokens.cache.read += result.sessionTokens.cache.read;
|
|
207
|
+
stats.totalTokens.cache.write += result.sessionTokens.cache.write;
|
|
186
208
|
|
|
187
209
|
for (const [tool, count] of Object.entries(result.sessionToolUsage)) {
|
|
188
|
-
stats.toolUsage[tool] = (stats.toolUsage[tool] || 0) + count
|
|
210
|
+
stats.toolUsage[tool] = (stats.toolUsage[tool] || 0) + count;
|
|
189
211
|
}
|
|
190
212
|
}
|
|
191
213
|
}
|
|
192
214
|
|
|
193
|
-
const actualDays = Math.max(
|
|
215
|
+
const actualDays = Math.max(
|
|
216
|
+
1,
|
|
217
|
+
Math.ceil((latestTime - earliestTime) / DAYS_IN_SECOND)
|
|
218
|
+
);
|
|
194
219
|
stats.dateRange = {
|
|
195
220
|
earliest: earliestTime,
|
|
196
221
|
latest: latestTime,
|
|
197
|
-
}
|
|
198
|
-
stats.days = actualDays
|
|
199
|
-
stats.costPerDay = stats.totalCost / actualDays
|
|
222
|
+
};
|
|
223
|
+
stats.days = actualDays;
|
|
224
|
+
stats.costPerDay = stats.totalCost / actualDays;
|
|
200
225
|
|
|
201
|
-
return stats
|
|
226
|
+
return stats;
|
|
202
227
|
}
|
|
203
228
|
|
|
204
229
|
export function displayStats(stats: SessionStats, toolLimit?: number) {
|
|
205
|
-
const width = 56
|
|
230
|
+
const width = 56;
|
|
206
231
|
|
|
207
232
|
function renderRow(label: string, value: string): string {
|
|
208
|
-
const availableWidth = width - 1
|
|
209
|
-
const paddingNeeded = availableWidth - label.length - value.length
|
|
210
|
-
const padding = Math.max(0, paddingNeeded)
|
|
211
|
-
return `│${label}${
|
|
233
|
+
const availableWidth = width - 1;
|
|
234
|
+
const paddingNeeded = availableWidth - label.length - value.length;
|
|
235
|
+
const padding = Math.max(0, paddingNeeded);
|
|
236
|
+
return `│${label}${' '.repeat(padding)}${value} │`;
|
|
212
237
|
}
|
|
213
238
|
|
|
214
239
|
// Overview section
|
|
215
|
-
console.log(
|
|
216
|
-
console.log(
|
|
217
|
-
console.log(
|
|
218
|
-
console.log(renderRow(
|
|
219
|
-
console.log(renderRow(
|
|
220
|
-
console.log(renderRow(
|
|
221
|
-
console.log(
|
|
222
|
-
console.log()
|
|
240
|
+
console.log('┌────────────────────────────────────────────────────────┐');
|
|
241
|
+
console.log('│ OVERVIEW │');
|
|
242
|
+
console.log('├────────────────────────────────────────────────────────┤');
|
|
243
|
+
console.log(renderRow('Sessions', stats.totalSessions.toLocaleString()));
|
|
244
|
+
console.log(renderRow('Messages', stats.totalMessages.toLocaleString()));
|
|
245
|
+
console.log(renderRow('Days', stats.days.toString()));
|
|
246
|
+
console.log('└────────────────────────────────────────────────────────┘');
|
|
247
|
+
console.log();
|
|
223
248
|
|
|
224
249
|
// Cost & Tokens section
|
|
225
|
-
console.log(
|
|
226
|
-
console.log(
|
|
227
|
-
console.log(
|
|
228
|
-
const cost = isNaN(stats.totalCost) ? 0 : stats.totalCost
|
|
229
|
-
const costPerDay = isNaN(stats.costPerDay) ? 0 : stats.costPerDay
|
|
230
|
-
console.log(renderRow(
|
|
231
|
-
console.log(renderRow(
|
|
232
|
-
console.log(renderRow(
|
|
233
|
-
console.log(renderRow(
|
|
234
|
-
console.log(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
console.log(
|
|
250
|
+
console.log('┌────────────────────────────────────────────────────────┐');
|
|
251
|
+
console.log('│ COST & TOKENS │');
|
|
252
|
+
console.log('├────────────────────────────────────────────────────────┤');
|
|
253
|
+
const cost = isNaN(stats.totalCost) ? 0 : stats.totalCost;
|
|
254
|
+
const costPerDay = isNaN(stats.costPerDay) ? 0 : stats.costPerDay;
|
|
255
|
+
console.log(renderRow('Total Cost', `$${cost.toFixed(2)}`));
|
|
256
|
+
console.log(renderRow('Cost/Day', `$${costPerDay.toFixed(2)}`));
|
|
257
|
+
console.log(renderRow('Input', formatNumber(stats.totalTokens.input)));
|
|
258
|
+
console.log(renderRow('Output', formatNumber(stats.totalTokens.output)));
|
|
259
|
+
console.log(
|
|
260
|
+
renderRow('Cache Read', formatNumber(stats.totalTokens.cache.read))
|
|
261
|
+
);
|
|
262
|
+
console.log(
|
|
263
|
+
renderRow('Cache Write', formatNumber(stats.totalTokens.cache.write))
|
|
264
|
+
);
|
|
265
|
+
console.log('└────────────────────────────────────────────────────────┘');
|
|
266
|
+
console.log();
|
|
238
267
|
|
|
239
268
|
// Tool Usage section
|
|
240
269
|
if (Object.keys(stats.toolUsage).length > 0) {
|
|
241
|
-
const sortedTools = Object.entries(stats.toolUsage).sort(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
270
|
+
const sortedTools = Object.entries(stats.toolUsage).sort(
|
|
271
|
+
([, a], [, b]) => b - a
|
|
272
|
+
);
|
|
273
|
+
const toolsToDisplay = toolLimit
|
|
274
|
+
? sortedTools.slice(0, toolLimit)
|
|
275
|
+
: sortedTools;
|
|
276
|
+
|
|
277
|
+
console.log('┌────────────────────────────────────────────────────────┐');
|
|
278
|
+
console.log('│ TOOL USAGE │');
|
|
279
|
+
console.log('├────────────────────────────────────────────────────────┤');
|
|
280
|
+
|
|
281
|
+
const maxCount = Math.max(...toolsToDisplay.map(([, count]) => count));
|
|
282
|
+
const totalToolUsage = Object.values(stats.toolUsage).reduce(
|
|
283
|
+
(a, b) => a + b,
|
|
284
|
+
0
|
|
285
|
+
);
|
|
250
286
|
|
|
251
287
|
for (const [tool, count] of toolsToDisplay) {
|
|
252
|
-
const barLength = Math.max(1, Math.floor((count / maxCount) * 20))
|
|
253
|
-
const bar =
|
|
254
|
-
const percentage = ((count / totalToolUsage) * 100).toFixed(1)
|
|
255
|
-
|
|
256
|
-
const maxToolLength = 18
|
|
257
|
-
const truncatedTool =
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
288
|
+
const barLength = Math.max(1, Math.floor((count / maxCount) * 20));
|
|
289
|
+
const bar = '█'.repeat(barLength);
|
|
290
|
+
const percentage = ((count / totalToolUsage) * 100).toFixed(1);
|
|
291
|
+
|
|
292
|
+
const maxToolLength = 18;
|
|
293
|
+
const truncatedTool =
|
|
294
|
+
tool.length > maxToolLength
|
|
295
|
+
? tool.substring(0, maxToolLength - 2) + '..'
|
|
296
|
+
: tool;
|
|
297
|
+
const toolName = truncatedTool.padEnd(maxToolLength);
|
|
298
|
+
|
|
299
|
+
const content = ` ${toolName} ${bar.padEnd(20)} ${count.toString().padStart(3)} (${percentage.padStart(4)}%)`;
|
|
300
|
+
const padding = Math.max(0, width - content.length - 1);
|
|
301
|
+
console.log(`│${content}${' '.repeat(padding)} │`);
|
|
263
302
|
}
|
|
264
|
-
console.log(
|
|
303
|
+
console.log('└────────────────────────────────────────────────────────┘');
|
|
265
304
|
}
|
|
266
|
-
console.log()
|
|
305
|
+
console.log();
|
|
267
306
|
}
|
|
268
307
|
|
|
269
308
|
function formatNumber(num: number): string {
|
|
270
309
|
if (num >= 1000000) {
|
|
271
|
-
return (num / 1000000).toFixed(1) +
|
|
310
|
+
return (num / 1000000).toFixed(1) + 'M';
|
|
272
311
|
} else if (num >= 1000) {
|
|
273
|
-
return (num / 1000).toFixed(1) +
|
|
312
|
+
return (num / 1000).toFixed(1) + 'K';
|
|
274
313
|
}
|
|
275
|
-
return num.toString()
|
|
314
|
+
return num.toString();
|
|
276
315
|
}
|
package/src/cli/error.ts
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
import { ConfigMarkdown } from
|
|
2
|
-
import { Config } from
|
|
3
|
-
import { MCP } from
|
|
4
|
-
import { UI } from
|
|
1
|
+
import { ConfigMarkdown } from '../config/markdown';
|
|
2
|
+
import { Config } from '../config/config';
|
|
3
|
+
import { MCP } from '../mcp';
|
|
4
|
+
import { UI } from './ui';
|
|
5
5
|
|
|
6
6
|
export function FormatError(input: unknown) {
|
|
7
7
|
if (MCP.Failed.isInstance(input))
|
|
8
|
-
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet
|
|
8
|
+
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`;
|
|
9
9
|
if (Config.JsonError.isInstance(input)) {
|
|
10
10
|
return (
|
|
11
|
-
`Config file at ${input.data.path} is not valid JSON(C)` +
|
|
12
|
-
|
|
11
|
+
`Config file at ${input.data.path} is not valid JSON(C)` +
|
|
12
|
+
(input.data.message ? `: ${input.data.message}` : '')
|
|
13
|
+
);
|
|
13
14
|
}
|
|
14
15
|
if (Config.ConfigDirectoryTypoError.isInstance(input)) {
|
|
15
|
-
return `Directory "${input.data.dir}" in ${input.data.path} is not valid. Use "${input.data.suggestion}" instead. This is a common typo
|
|
16
|
+
return `Directory "${input.data.dir}" in ${input.data.path} is not valid. Use "${input.data.suggestion}" instead. This is a common typo.`;
|
|
16
17
|
}
|
|
17
18
|
if (ConfigMarkdown.FrontmatterError.isInstance(input)) {
|
|
18
|
-
return `Failed to parse frontmatter in ${input.data.path}:\n${input.data.message}
|
|
19
|
+
return `Failed to parse frontmatter in ${input.data.path}:\n${input.data.message}`;
|
|
19
20
|
}
|
|
20
21
|
if (Config.InvalidError.isInstance(input))
|
|
21
22
|
return [
|
|
22
|
-
`Config file at ${input.data.path} is invalid` +
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
`Config file at ${input.data.path} is invalid` +
|
|
24
|
+
(input.data.message ? `: ${input.data.message}` : ''),
|
|
25
|
+
...(input.data.issues?.map(
|
|
26
|
+
(issue) => '↳ ' + issue.message + ' ' + issue.path.join('.')
|
|
27
|
+
) ?? []),
|
|
28
|
+
].join('\n');
|
|
25
29
|
|
|
26
|
-
if (UI.CancelledError.isInstance(input)) return
|
|
30
|
+
if (UI.CancelledError.isInstance(input)) return '';
|
|
27
31
|
}
|
package/src/cli/ui.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { NamedError } from '../util/error';
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
|
|
4
|
+
export namespace UI {
|
|
5
|
+
// ANSI color codes for terminal output
|
|
6
|
+
export const Style = {
|
|
7
|
+
TEXT_NORMAL: '\x1b[0m',
|
|
8
|
+
TEXT_BOLD: '\x1b[1m',
|
|
9
|
+
TEXT_DIM: '\x1b[2m',
|
|
10
|
+
TEXT_DANGER_BOLD: '\x1b[1;31m',
|
|
11
|
+
TEXT_SUCCESS_BOLD: '\x1b[1;32m',
|
|
12
|
+
TEXT_WARNING_BOLD: '\x1b[1;33m',
|
|
13
|
+
TEXT_INFO_BOLD: '\x1b[1;34m',
|
|
14
|
+
TEXT_HIGHLIGHT_BOLD: '\x1b[1;35m',
|
|
15
|
+
TEXT_DIM_BOLD: '\x1b[1;90m',
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
// Error for cancelled operations (e.g., Ctrl+C in prompts)
|
|
19
|
+
export const CancelledError = NamedError.create(
|
|
20
|
+
'CancelledError',
|
|
21
|
+
z.object({})
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Print an empty line
|
|
25
|
+
export function empty() {
|
|
26
|
+
process.stderr.write('\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Print a line with optional formatting
|
|
30
|
+
export function println(...args: string[]) {
|
|
31
|
+
process.stderr.write(args.join('') + Style.TEXT_NORMAL + '\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Print an error message
|
|
35
|
+
export function error(message: string) {
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
Style.TEXT_DANGER_BOLD + 'Error: ' + Style.TEXT_NORMAL + message + '\n'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Print a success message
|
|
42
|
+
export function success(message: string) {
|
|
43
|
+
process.stderr.write(
|
|
44
|
+
Style.TEXT_SUCCESS_BOLD + 'Success: ' + Style.TEXT_NORMAL + message + '\n'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Print an info message
|
|
49
|
+
export function info(message: string) {
|
|
50
|
+
process.stderr.write(
|
|
51
|
+
Style.TEXT_INFO_BOLD + 'Info: ' + Style.TEXT_NORMAL + message + '\n'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Basic markdown rendering for terminal
|
|
56
|
+
export function markdown(text: string): string {
|
|
57
|
+
// Simple markdown to ANSI conversion
|
|
58
|
+
let result = text;
|
|
59
|
+
|
|
60
|
+
// Bold text: **text** or __text__
|
|
61
|
+
result = result.replace(
|
|
62
|
+
/\*\*(.+?)\*\*/g,
|
|
63
|
+
Style.TEXT_BOLD + '$1' + Style.TEXT_NORMAL
|
|
64
|
+
);
|
|
65
|
+
result = result.replace(
|
|
66
|
+
/__(.+?)__/g,
|
|
67
|
+
Style.TEXT_BOLD + '$1' + Style.TEXT_NORMAL
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Code blocks: `code`
|
|
71
|
+
result = result.replace(
|
|
72
|
+
/`([^`]+)`/g,
|
|
73
|
+
Style.TEXT_DIM + '$1' + Style.TEXT_NORMAL
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/command/index.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import z from
|
|
2
|
-
import { Config } from
|
|
3
|
-
import { Instance } from
|
|
4
|
-
import PROMPT_INITIALIZE from
|
|
5
|
-
import { Bus } from
|
|
6
|
-
import { Identifier } from
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { Config } from '../config/config';
|
|
3
|
+
import { Instance } from '../project/instance';
|
|
4
|
+
import PROMPT_INITIALIZE from './template/initialize.txt';
|
|
5
|
+
import { Bus } from '../bus';
|
|
6
|
+
import { Identifier } from '../id/id';
|
|
7
7
|
|
|
8
8
|
export namespace Command {
|
|
9
9
|
export const Default = {
|
|
10
|
-
INIT:
|
|
11
|
-
} as const
|
|
10
|
+
INIT: 'init',
|
|
11
|
+
} as const;
|
|
12
12
|
|
|
13
13
|
export const Event = {
|
|
14
14
|
Executed: Bus.event(
|
|
15
|
-
|
|
15
|
+
'command.executed',
|
|
16
16
|
z.object({
|
|
17
17
|
name: z.string(),
|
|
18
|
-
sessionID: Identifier.schema(
|
|
18
|
+
sessionID: Identifier.schema('session'),
|
|
19
19
|
arguments: z.string(),
|
|
20
|
-
messageID: Identifier.schema(
|
|
21
|
-
})
|
|
20
|
+
messageID: Identifier.schema('message'),
|
|
21
|
+
})
|
|
22
22
|
),
|
|
23
|
-
}
|
|
23
|
+
};
|
|
24
24
|
|
|
25
25
|
export const Info = z
|
|
26
26
|
.object({
|
|
@@ -32,14 +32,14 @@ export namespace Command {
|
|
|
32
32
|
subtask: z.boolean().optional(),
|
|
33
33
|
})
|
|
34
34
|
.meta({
|
|
35
|
-
ref:
|
|
36
|
-
})
|
|
37
|
-
export type Info = z.infer<typeof Info
|
|
35
|
+
ref: 'Command',
|
|
36
|
+
});
|
|
37
|
+
export type Info = z.infer<typeof Info>;
|
|
38
38
|
|
|
39
39
|
const state = Instance.state(async () => {
|
|
40
|
-
const cfg = await Config.get()
|
|
40
|
+
const cfg = await Config.get();
|
|
41
41
|
|
|
42
|
-
const result: Record<string, Info> = {}
|
|
42
|
+
const result: Record<string, Info> = {};
|
|
43
43
|
|
|
44
44
|
for (const [name, command] of Object.entries(cfg.command ?? {})) {
|
|
45
45
|
result[name] = {
|
|
@@ -49,25 +49,25 @@ export namespace Command {
|
|
|
49
49
|
description: command.description,
|
|
50
50
|
template: command.template,
|
|
51
51
|
subtask: command.subtask,
|
|
52
|
-
}
|
|
52
|
+
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
if (result[Default.INIT] === undefined) {
|
|
56
56
|
result[Default.INIT] = {
|
|
57
57
|
name: Default.INIT,
|
|
58
|
-
description:
|
|
59
|
-
template: PROMPT_INITIALIZE.replace(
|
|
60
|
-
}
|
|
58
|
+
description: 'create/update AGENTS.md',
|
|
59
|
+
template: PROMPT_INITIALIZE.replace('${path}', Instance.worktree),
|
|
60
|
+
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
return result
|
|
64
|
-
})
|
|
63
|
+
return result;
|
|
64
|
+
});
|
|
65
65
|
|
|
66
66
|
export async function get(name: string) {
|
|
67
|
-
return state().then((x) => x[name])
|
|
67
|
+
return state().then((x) => x[name]);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export async function list() {
|
|
71
|
-
return state().then((x) => Object.values(x))
|
|
71
|
+
return state().then((x) => Object.values(x));
|
|
72
72
|
}
|
|
73
73
|
}
|