tokenmaw 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/markdown.js CHANGED
@@ -1,21 +1,49 @@
1
1
  /**
2
2
  * markdown.ts — Lightweight terminal markdown renderer.
3
3
  *
4
- * Exported so it can be unit-tested independently of the TUI.
5
- * No external dependencies pure string transformation.
4
+ * Emits Blessed style tags (not raw ANSI) so the TUI renders every highlight
5
+ * with theme-driven colors, and exports the theme shape so unit tests and the
6
+ * TUI share one definition. No external dependencies — pure string transform.
6
7
  */
7
- import chalk from 'chalk';
8
- const MARKDOWN_THEME = {
8
+ /** Blessed tags quantize hex colors to the terminal palette; chalk's
9
+ * truecolor SGR sequences are dropped by Blessed's attribute parser, which is
10
+ * why highlights previously rendered as plain white. */
11
+ export const DEFAULT_MARKDOWN_THEME = {
9
12
  text: '#d7e0ea',
10
13
  muted: '#7f92a6',
11
14
  accent: '#6fb1d6',
12
- accentStrong: '#8ac3e6',
15
+ heading: '#8ac3e6',
16
+ headingStrong: '#d7e0ea',
13
17
  codeBg: '#16212d',
14
18
  codeText: '#c7d7e6',
15
19
  codeFence: '#5f7388',
16
- codeGreen: '#8ebd93',
17
- codeRed: '#c97c7c',
20
+ diffAddBg: '#10281a',
21
+ diffAddText: '#9fd0a6',
22
+ diffDelBg: '#2b1215',
23
+ diffDelText: '#d99f9f',
18
24
  };
25
+ let currentTheme = DEFAULT_MARKDOWN_THEME;
26
+ export function setMarkdownTheme(theme) {
27
+ currentTheme = theme;
28
+ }
29
+ export function getMarkdownTheme() {
30
+ return currentTheme;
31
+ }
32
+ /** Blessed reserves braces for style tags; escape literal ones. */
33
+ export function escapeTags(text) {
34
+ return text.replace(/{/g, '{open}').replace(/}/g, '{close}');
35
+ }
36
+ function fg(color, text) {
37
+ return `{${color}-fg}${text}{/${color}-fg}`;
38
+ }
39
+ /** Italic and strikethrough are not Blessed flags; raw SGR is honored by the
40
+ * content parser and excluded from width measurement. */
41
+ function italic(text) {
42
+ return `\x1b[3m${text}\x1b[23m`;
43
+ }
44
+ function strike(text) {
45
+ return `\x1b[9m${text}\x1b[29m`;
46
+ }
19
47
  function isDiffLanguage(lang) {
20
48
  return lang.toLowerCase() === 'diff' || lang.toLowerCase() === 'patch';
21
49
  }
@@ -31,27 +59,30 @@ export function diffKind(line) {
31
59
  return 'context';
32
60
  }
33
61
  export function renderDiffLine(line) {
62
+ const theme = getMarkdownTheme();
34
63
  switch (diffKind(line)) {
35
64
  case 'add':
36
- return chalk.hex(MARKDOWN_THEME.codeGreen)(line);
65
+ return fg(theme.diffAddText, escapeTags(line));
37
66
  case 'del':
38
- return chalk.hex(MARKDOWN_THEME.codeRed)(line);
67
+ return fg(theme.diffDelText, escapeTags(line));
39
68
  case 'hunk':
40
- return chalk.hex(MARKDOWN_THEME.accent)(line);
69
+ return fg(theme.accent, escapeTags(line));
41
70
  case 'file':
42
- return chalk.bold.hex(MARKDOWN_THEME.text)(line);
71
+ return fg(theme.text, escapeTags(line));
43
72
  case 'context':
44
73
  default:
45
- return chalk.hex(MARKDOWN_THEME.muted)(line);
74
+ return fg(theme.muted, escapeTags(line));
46
75
  }
47
76
  }
48
77
  function renderCodeLine(lang, line) {
49
- return isDiffLanguage(lang) ? renderDiffLine(line) : chalk.hex(MARKDOWN_THEME.codeText)(line);
78
+ return isDiffLanguage(lang) ? renderDiffLine(line) : fg(getMarkdownTheme().codeText, escapeTags(line));
50
79
  }
51
80
  const ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
52
- /** Display width ignoring ANSI escapes, counting East-Asian wide chars as 2 columns. */
81
+ const TAG_PATTERN = /\{[^{}]*\}/g;
82
+ /** Display width ignoring ANSI escapes and style tags, counting East-Asian
83
+ * wide chars as 2 columns. */
53
84
  export function displayWidth(text) {
54
- const clean = text.replace(ANSI_PATTERN, '');
85
+ const clean = text.replace(ANSI_PATTERN, '').replace(TAG_PATTERN, '');
55
86
  let width = 0;
56
87
  for (const ch of clean) {
57
88
  const code = ch.codePointAt(0);
@@ -135,6 +166,7 @@ function truncateToWidth(text, maxWidth) {
135
166
  }
136
167
  /** Render a parsed table as aligned monospace lines that fit within maxWidth columns. */
137
168
  export function renderGfmTable(table, maxWidth) {
169
+ const theme = getMarkdownTheme();
138
170
  const columns = table.header.length;
139
171
  const gap = ' │ ';
140
172
  const gapWidth = displayWidth(gap);
@@ -153,40 +185,43 @@ export function renderGfmTable(table, maxWidth) {
153
185
  overflow -= reduce;
154
186
  }
155
187
  }
156
- const padCell = (rendered, width, align) => {
157
- const padding = Math.max(0, width - displayWidth(rendered));
188
+ const padCell = (plain, width, align) => {
189
+ const padding = Math.max(0, width - displayWidth(plain));
158
190
  if (align === 'right')
159
- return ' '.repeat(padding) + rendered;
191
+ return ' '.repeat(padding) + plain;
160
192
  if (align === 'center') {
161
193
  const left = Math.floor(padding / 2);
162
- return ' '.repeat(left) + rendered + ' '.repeat(padding - left);
194
+ return ' '.repeat(left) + plain + ' '.repeat(padding - left);
163
195
  }
164
- return rendered + ' '.repeat(padding);
196
+ return plain + ' '.repeat(padding);
165
197
  };
198
+ // Pad the plain cell first, then style — so padding never measures tags.
166
199
  const renderRow = (cells, style) => cells
167
200
  .map((cell, index) => {
168
201
  const width = widths[index];
169
202
  const plain = displayWidth(cell) > width ? truncateToWidth(cell, width) : cell;
170
- return padCell(style(plain), width, table.aligns[index]);
203
+ return style(padCell(plain, width, table.aligns[index]));
171
204
  })
172
- .join(chalk.hex(MARKDOWN_THEME.muted)(gap));
173
- const header = renderRow(table.header, (cell) => chalk.bold.hex(MARKDOWN_THEME.accent)(inlineMarkdown(cell)));
205
+ .join(fg(theme.muted, gap));
206
+ const header = renderRow(table.header, (plain) => fg(theme.accent, `{bold}${inlineMarkdown(plain)}{/bold}`));
174
207
  // One uniform border color for every structural character (│, ─, ┼).
175
- const separator = chalk.hex(MARKDOWN_THEME.muted)(widths.map((width) => '─'.repeat(width)).join('─┼─'));
176
- const body = table.rows.map((row) => renderRow(row, (cell) => inlineMarkdown(cell)));
177
- return [header, chalk.hex(MARKDOWN_THEME.muted)(separator), ...body];
208
+ const separator = fg(theme.muted, widths.map((width) => '─'.repeat(width)).join('─┼─'));
209
+ const body = table.rows.map((row) => renderRow(row, (plain) => inlineMarkdown(plain)));
210
+ return [header, separator, ...body];
178
211
  }
179
212
  export function inlineMarkdown(text) {
180
- return text
181
- .replace(/\*\*\*(.+?)\*\*\*/g, (_m, t) => chalk.bold.italic(t))
182
- .replace(/\*\*(.+?)\*\*/g, (_m, t) => chalk.bold(t))
183
- .replace(/__(.+?)__/g, (_m, t) => chalk.bold(t))
184
- .replace(/\*(.+?)\*/g, (_m, t) => chalk.italic(t))
185
- .replace(/_(.+?)_/g, (_m, t) => chalk.italic(t))
186
- .replace(/`([^`]+)`/g, (_m, t) => chalk.bgHex(MARKDOWN_THEME.codeBg).hex(MARKDOWN_THEME.codeText)(` ${t} `))
187
- .replace(/~~(.+?)~~/g, (_m, t) => chalk.strikethrough(t));
213
+ const theme = getMarkdownTheme();
214
+ return escapeTags(text)
215
+ .replace(/\*\*\*(.+?)\*\*\*/g, (_m, t) => `{bold}${italic(t)}{/bold}`)
216
+ .replace(/\*\*(.+?)\*\*/g, (_m, t) => `{bold}${t}{/bold}`)
217
+ .replace(/__(.+?)__/g, (_m, t) => `{bold}${t}{/bold}`)
218
+ .replace(/\*(.+?)\*/g, (_m, t) => italic(t))
219
+ .replace(/_(.+?)_/g, (_m, t) => italic(t))
220
+ .replace(/`([^`]+)`/g, (_m, t) => `{${theme.codeBg}-bg}{${theme.codeText}-fg} ${t} {/${theme.codeText}-fg}{/${theme.codeBg}-bg}`)
221
+ .replace(/~~(.+?)~~/g, (_m, t) => strike(t));
188
222
  }
189
223
  export function renderMarkdown(text, cols = 80) {
224
+ const theme = getMarkdownTheme();
190
225
  const lines = text.split('\n');
191
226
  const out = [];
192
227
  let inCodeBlock = false;
@@ -203,12 +238,12 @@ export function renderMarkdown(text, cols = 80) {
203
238
  }
204
239
  else {
205
240
  inCodeBlock = false;
206
- const langLabel = codeLang ? chalk.hex(MARKDOWN_THEME.muted).italic(` ${codeLang}`) : '';
207
- out.push(chalk.hex(MARKDOWN_THEME.codeFence)('┌' + '─'.repeat(Math.max(2, cols - 2))) + langLabel);
241
+ const langLabel = codeLang ? fg(theme.muted, italic(` ${escapeTags(codeLang)}`)) : '';
242
+ out.push(fg(theme.codeFence, '┌' + '─'.repeat(Math.max(2, cols - 2))) + langLabel);
208
243
  for (const cl of codeLines) {
209
- out.push(chalk.hex(MARKDOWN_THEME.codeFence)('│ ') + renderCodeLine(codeLang, cl));
244
+ out.push(fg(theme.codeFence, '│ ') + renderCodeLine(codeLang, cl));
210
245
  }
211
- out.push(chalk.hex(MARKDOWN_THEME.codeFence)('└' + '─'.repeat(Math.max(2, cols - 2))));
246
+ out.push(fg(theme.codeFence, '└' + '─'.repeat(Math.max(2, cols - 2))));
212
247
  codeLang = '';
213
248
  codeLines = [];
214
249
  }
@@ -222,19 +257,19 @@ export function renderMarkdown(text, cols = 80) {
222
257
  const h2 = raw.match(/^## (.+)/);
223
258
  const h3 = raw.match(/^### (.+)/);
224
259
  if (h1) {
225
- out.push('\n' + chalk.bold.hex(MARKDOWN_THEME.accentStrong)(h1[1]));
260
+ out.push('\n' + fg(theme.heading, `{bold}${escapeTags(h1[1])}{/bold}`));
226
261
  continue;
227
262
  }
228
263
  if (h2) {
229
- out.push('\n' + chalk.bold.hex(MARKDOWN_THEME.text)(h2[1]));
264
+ out.push('\n' + fg(theme.headingStrong, `{bold}${escapeTags(h2[1])}{/bold}`));
230
265
  continue;
231
266
  }
232
267
  if (h3) {
233
- out.push(chalk.bold(h3[1]));
268
+ out.push(`{bold}${escapeTags(h3[1])}{/bold}`);
234
269
  continue;
235
270
  }
236
271
  if (/^---+$/.test(raw) || /^\*\*\*+$/.test(raw)) {
237
- out.push(chalk.hex(MARKDOWN_THEME.muted)('─'.repeat(cols)));
272
+ out.push(fg(theme.muted, '─'.repeat(cols)));
238
273
  continue;
239
274
  }
240
275
  const tableMatch = matchGfmTable(lines, index);
@@ -245,30 +280,30 @@ export function renderMarkdown(text, cols = 80) {
245
280
  }
246
281
  const bullet = raw.match(/^(\s*)[*\-+] (.+)/);
247
282
  if (bullet) {
248
- out.push((bullet[1] ?? '') + chalk.hex(MARKDOWN_THEME.accent)('•') + ' ' + inlineMarkdown(bullet[2] ?? ''));
283
+ out.push((bullet[1] ?? '') + fg(theme.accent, '•') + ' ' + inlineMarkdown(bullet[2] ?? ''));
249
284
  continue;
250
285
  }
251
286
  const numbered = raw.match(/^(\s*)(\d+)\. (.+)/);
252
287
  if (numbered) {
253
288
  out.push((numbered[1] ?? '') +
254
- chalk.hex(MARKDOWN_THEME.accent)(numbered[2] + '.') +
289
+ fg(theme.accent, escapeTags(numbered[2] + '.')) +
255
290
  ' ' +
256
291
  inlineMarkdown(numbered[3] ?? ''));
257
292
  continue;
258
293
  }
259
294
  const bq = raw.match(/^> (.+)/);
260
295
  if (bq) {
261
- out.push(chalk.hex(MARKDOWN_THEME.muted)('│ ') + chalk.italic.hex(MARKDOWN_THEME.muted)(bq[1]));
296
+ out.push(fg(theme.muted, '│ ') + fg(theme.muted, italic(escapeTags(bq[1]))));
262
297
  continue;
263
298
  }
264
299
  out.push(inlineMarkdown(raw));
265
300
  }
266
301
  if (inCodeBlock && codeLines.length > 0) {
267
- out.push(chalk.hex(MARKDOWN_THEME.codeFence)('┌─'));
302
+ out.push(fg(theme.codeFence, '┌─'));
268
303
  for (const cl of codeLines) {
269
- out.push(chalk.hex(MARKDOWN_THEME.codeFence)('│ ') + renderCodeLine(codeLang, cl));
304
+ out.push(fg(theme.codeFence, '│ ') + renderCodeLine(codeLang, cl));
270
305
  }
271
- out.push(chalk.hex(MARKDOWN_THEME.codeFence)('└─'));
306
+ out.push(fg(theme.codeFence, '└─'));
272
307
  }
273
308
  return out.join('\n');
274
309
  }
package/dist/responses.js CHANGED
@@ -66,7 +66,13 @@ export async function* responsesStream(config, instructions, messages, tools, si
66
66
  throw new Error(event.response?.error?.message ?? event.message ?? event.response?.incomplete_details?.reason ?? `Responses: ${event.type}`);
67
67
  }
68
68
  if (event.type === 'response.completed') {
69
- yield { content: null, done: true };
69
+ const usage = event.response?.usage;
70
+ yield { content: null, done: true, usage: usage ? {
71
+ inputTokens: usage.input_tokens,
72
+ outputTokens: usage.output_tokens,
73
+ cachedInputTokens: usage.input_tokens_details?.cached_tokens,
74
+ reasoningTokens: usage.output_tokens_details?.reasoning_tokens,
75
+ } : undefined };
70
76
  return;
71
77
  }
72
78
  }
@@ -1,6 +1,6 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
- import { relative, resolve, sep } from 'node:path';
3
+ import { join, relative, resolve, sep } from 'node:path';
4
4
  function unquote(value) {
5
5
  const trimmed = value.trim();
6
6
  if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
@@ -86,6 +86,25 @@ async function markdownFiles(root) {
86
86
  function specId(root, file) {
87
87
  return relative(root, file).split(sep).join('/').replace(/\.md$/i, '');
88
88
  }
89
+ /** Names checked in order when loading project context. The lowercase form
90
+ * covers case-insensitive filesystems; the capitalized forms follow the
91
+ * agents.md convention used by other coding agents. */
92
+ const WORKSPACE_CONTEXT_FILENAMES = ['AGENTS.md', 'AGENT.md', 'agents.md'];
93
+ /** Loads an optional AGENTS.md-style project context document from the
94
+ * workspace root. Plain Markdown with no frontmatter; a missing, empty, or
95
+ * unreadable file is not an error — the convention is opt-in per workspace. */
96
+ export async function loadWorkspaceContext(root) {
97
+ for (const name of WORKSPACE_CONTEXT_FILENAMES) {
98
+ try {
99
+ const content = (await readFile(join(root, name), 'utf8')).replace(/^\uFEFF/, '').trim();
100
+ if (content)
101
+ return content;
102
+ // An empty file counts as absent; keep looking at the remaining names.
103
+ }
104
+ catch { /* try the next candidate name */ }
105
+ }
106
+ return undefined;
107
+ }
89
108
  function matchesSelector(id, selector) {
90
109
  if (selector === '*')
91
110
  return true;
@@ -96,15 +115,26 @@ function matchesSelector(id, selector) {
96
115
  export class AgentRegistry {
97
116
  specs = new Map();
98
117
  roots;
118
+ builtinDir;
119
+ userDir;
120
+ projectDir;
99
121
  constructor(options = {}) {
100
122
  const workspaceRoot = resolve(options.workspaceRoot ?? process.cwd());
101
- const builtinDir = resolve(options.builtinDir ?? resolve(import.meta.dirname, '..', '..', 'agents'));
123
+ this.builtinDir = resolve(options.builtinDir ?? resolve(import.meta.dirname, '..', '..', 'agents'));
124
+ this.userDir = resolve(options.userDir ?? resolve(homedir(), '.coder', 'agents'));
125
+ this.projectDir = options.projectDir ? resolve(options.projectDir) : undefined;
102
126
  this.roots = [
103
- { path: builtinDir, scope: 'builtin' },
104
- { path: resolve(options.userDir ?? resolve(homedir(), '.coder', 'agents')), scope: 'user' },
105
- { path: resolve(options.projectDir ?? resolve(workspaceRoot, '.coder', 'agents')), scope: 'project' },
127
+ { path: this.builtinDir, scope: 'builtin' },
128
+ { path: this.userDir, scope: 'user' },
129
+ { path: this.projectDir ?? resolve(workspaceRoot, '.coder', 'agents'), scope: 'project' },
106
130
  ];
107
131
  }
132
+ /** Point the project-scope spec root at another workspace (<root>/.coder/agents).
133
+ * Takes effect on the next load(); used by /cd when switching workspaces. */
134
+ setProjectDir(workspaceRoot) {
135
+ this.projectDir = resolve(workspaceRoot, '.coder', 'agents');
136
+ this.roots[this.roots.length - 1] = { path: this.projectDir, scope: 'project' };
137
+ }
108
138
  async load() {
109
139
  this.specs.clear();
110
140
  for (const root of this.roots) {