@standardagents/code 0.0.2-dev.b3cdaaf → 0.1.1

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/src/markdown.ts DELETED
@@ -1,226 +0,0 @@
1
- /**
2
- * A light-touch Markdown → ANSI formatter for the transcript. Deliberately not a
3
- * full Markdown engine: it adds color/emphasis hints (bold, inline code,
4
- * headings, links), tidy list bullets, basic aligned tables, and fenced code
5
- * gutters — enough to make agent replies readable without fighting the terminal.
6
- * Zero dependencies; returns an array of ready-to-print lines.
7
- */
8
-
9
- const ESC = "\x1b[";
10
- const R = ESC + "0m";
11
- const BOLD = ESC + "1m";
12
- const DIM = ESC + "2m";
13
- const ITAL = ESC + "3m";
14
- const UNDER = ESC + "4m";
15
- const TEAL = ESC + "38;5;37m";
16
- const CYAN = ESC + "36m";
17
- const GRAY = ESC + "90m";
18
-
19
- /** Visible width of a string, ignoring ANSI SGR sequences. */
20
- // eslint-disable-next-line no-control-regex
21
- const ANSI = /\x1b\[[0-9;]*m/g;
22
- function visibleWidth(s: string): number {
23
- return s.replace(ANSI, "").length;
24
- }
25
- function padEndVisible(s: string, width: number): string {
26
- const pad = width - visibleWidth(s);
27
- return pad > 0 ? s + " ".repeat(pad) : s;
28
- }
29
-
30
- /** Apply inline emphasis: code spans, links, bold, italic, strikethrough. */
31
- function inline(s: string): string {
32
- // Protect inline code first so its contents aren't re-formatted. NUL sentinels
33
- // never appear in real text, so they can't collide with digits in the prose.
34
- const codes: string[] = [];
35
- s = s.replace(/`([^`]+)`/g, (_, code) => {
36
- codes.push(code);
37
- return "\x00" + (codes.length - 1) + "\x00";
38
- });
39
- // Links: [text](url) → underlined text + dim url.
40
- s = s.replace(
41
- /\[([^\]]+)\]\(([^)\s]+)\)/g,
42
- (_, text, url) => `${CYAN}${UNDER}${text}${R} ${DIM}${url}${R}`
43
- );
44
- // Bold then italic (asterisk forms only — underscores are common in code/ids).
45
- s = s.replace(/\*\*([^*]+)\*\*/g, (_, t) => `${BOLD}${t}${R}`);
46
- s = s.replace(/\*([^*\n]+)\*/g, (_, t) => `${ITAL}${t}${R}`);
47
- s = s.replace(/~~([^~]+)~~/g, (_, t) => `${DIM}${t}${R}`);
48
- // Restore code spans in teal. Spaces inside a code span become non-breaking
49
- // (NBSP) so word-wrapping never splits a command/path like `curl | bash`.
50
- // eslint-disable-next-line no-control-regex
51
- s = s.replace(/\x00(\d+)\x00/g, (_, i) => `${TEAL}${codes[+i].replace(/ /g, String.fromCharCode(160))}${R}`);
52
- return s;
53
- }
54
-
55
- /**
56
- * Word-wrap styled text to a visible width. ANSI-aware (escape sequences don't
57
- * count toward width), splits only on regular spaces (so NBSP-protected code
58
- * spans stay intact). A single word longer than the width is left to overflow
59
- * rather than split mid-token.
60
- */
61
- function wrapStyled(text: string, width: number): string[] {
62
- if (width < 4 || visibleWidth(text) <= width) return [text];
63
- const words = text.split(" ");
64
- const lines: string[] = [];
65
- let cur = "";
66
- let curLen = 0;
67
- for (const w of words) {
68
- const wLen = visibleWidth(w);
69
- if (cur === "") {
70
- cur = w;
71
- curLen = wLen;
72
- } else if (curLen + 1 + wLen <= width) {
73
- cur += " " + w;
74
- curLen += 1 + wLen;
75
- } else {
76
- lines.push(cur);
77
- cur = w;
78
- curLen = wLen;
79
- }
80
- }
81
- if (cur !== "" || lines.length === 0) lines.push(cur);
82
- return lines;
83
- }
84
-
85
- /**
86
- * Emit a block (paragraph/list item/quote) word-wrapped to `cols`, with a lead
87
- * on the first line and an aligned hanging indent of the same visible width on
88
- * the rest. `leadWidth` is the visible width of both leads.
89
- */
90
- function wrapBlock(
91
- out: string[],
92
- cols: number,
93
- leadFirst: string,
94
- leadRest: string,
95
- leadWidth: number,
96
- text: string
97
- ): void {
98
- const wrapped = wrapStyled(text, Math.max(8, cols - leadWidth));
99
- wrapped.forEach((ln, idx) => out.push((idx === 0 ? leadFirst : leadRest) + ln));
100
- }
101
-
102
- /** Split a table row "| a | b |" into trimmed cells. */
103
- function tableCells(row: string): string[] {
104
- let r = row.trim();
105
- if (r.startsWith("|")) r = r.slice(1);
106
- if (r.endsWith("|")) r = r.slice(0, -1);
107
- return r.split("|").map((c) => c.trim());
108
- }
109
-
110
- const SEPARATOR = /^[\s|:-]+$/;
111
- function isTableSeparator(line: string): boolean {
112
- return SEPARATOR.test(line) && line.includes("-") && line.includes("|");
113
- }
114
-
115
- /** Render a collected table block (header row + body rows) as aligned columns. */
116
- function renderTable(rows: string[][]): string[] {
117
- const cols = Math.max(...rows.map((r) => r.length));
118
- const widths: number[] = [];
119
- for (let c = 0; c < cols; c++) {
120
- widths[c] = Math.max(...rows.map((r) => visibleWidth(inline(r[c] ?? ""))));
121
- }
122
- const sep = `${GRAY} │ ${R}`;
123
- const out: string[] = [];
124
- rows.forEach((r, ri) => {
125
- const cells = [];
126
- for (let c = 0; c < cols; c++) {
127
- const raw = r[c] ?? "";
128
- const styled = ri === 0 ? `${BOLD}${inline(raw)}${R}` : inline(raw);
129
- cells.push(padEndVisible(styled, widths[c]));
130
- }
131
- out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
132
- if (ri === 0) {
133
- // A thin underline beneath the header row.
134
- const rule = widths.map((w) => `${GRAY}${"─".repeat(w)}${R}`).join(`${GRAY}─┼─${R}`);
135
- out.push(" " + rule);
136
- }
137
- });
138
- return out;
139
- }
140
-
141
- /** Format Markdown source into an array of ANSI-styled, word-wrapped terminal lines. */
142
- export function renderMarkdown(src: string, cols = 80): string[] {
143
- const lines = src.replace(/\r\n/g, "\n").split("\n");
144
- const out: string[] = [];
145
- let inFence = false;
146
- let i = 0;
147
-
148
- while (i < lines.length) {
149
- const line = lines[i];
150
-
151
- // Fenced code block: render with a dim gutter, no inline formatting.
152
- if (/^\s*```/.test(line)) {
153
- inFence = !inFence;
154
- i++;
155
- continue;
156
- }
157
- if (inFence) {
158
- out.push(`${GRAY}│${R} ${line}`);
159
- i++;
160
- continue;
161
- }
162
-
163
- // Table: a row containing "|" immediately followed by a separator row.
164
- if (line.includes("|") && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
165
- const block: string[][] = [tableCells(line)];
166
- i += 2; // skip header + separator
167
- while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
168
- block.push(tableCells(lines[i]));
169
- i++;
170
- }
171
- out.push(...renderTable(block));
172
- continue;
173
- }
174
-
175
- // Heading.
176
- const heading = line.match(/^(#{1,6})\s+(.*)$/);
177
- if (heading) {
178
- for (const ln of wrapStyled(heading[2].trim(), cols)) out.push(`${BOLD}${TEAL}${ln}${R}`);
179
- i++;
180
- continue;
181
- }
182
-
183
- // Horizontal rule → a short, subtle divider (never full width).
184
- if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
185
- out.push(`${GRAY}──────${R}`);
186
- i++;
187
- continue;
188
- }
189
-
190
- // Blockquote.
191
- const quote = line.match(/^\s*>\s?(.*)$/);
192
- if (quote) {
193
- for (const ln of wrapStyled(inline(quote[1]), Math.max(8, cols - 2))) {
194
- out.push(`${GRAY}│${R} ${DIM}${ln}${R}`);
195
- }
196
- i++;
197
- continue;
198
- }
199
-
200
- // Bullet list (-, *, +) with indentation preserved; continuation hangs under the text.
201
- const bullet = line.match(/^(\s*)[-*+]\s+(.*)$/);
202
- if (bullet) {
203
- const leadWidth = bullet[1].length + 2;
204
- wrapBlock(out, cols, `${bullet[1]}${TEAL}•${R} `, " ".repeat(leadWidth), leadWidth, inline(bullet[2]));
205
- i++;
206
- continue;
207
- }
208
-
209
- // Numbered list — keep the number, embolden the marker.
210
- const numbered = line.match(/^(\s*)(\d+)([.)])\s+(.*)$/);
211
- if (numbered) {
212
- const marker = `${numbered[2]}${numbered[3]}`;
213
- const leadWidth = numbered[1].length + marker.length + 1;
214
- wrapBlock(out, cols, `${numbered[1]}${BOLD}${marker}${R} `, " ".repeat(leadWidth), leadWidth, inline(numbered[4]));
215
- i++;
216
- continue;
217
- }
218
-
219
- // Plain paragraph line (blank lines pass through as spacing).
220
- if (line.trim()) wrapBlock(out, cols, "", "", 0, inline(line));
221
- else out.push("");
222
- i++;
223
- }
224
-
225
- return out;
226
- }
package/src/mcp-config.ts DELETED
@@ -1,137 +0,0 @@
1
- /**
2
- * MCP server configuration — the list of Model Context Protocol servers this
3
- * CLI can launch locally and negotiate with. Stored in `~/.standardagents/mcp.json`
4
- * so it persists across sessions and machines (per-user, not per-thread).
5
- *
6
- * Each server is a local process the CLI spawns and speaks JSON-RPC 2.0 to over
7
- * stdio (see `mcp.ts`). The CLI is the MCP *host*: it owns the connection, does
8
- * the capability handshake, and forwards the agent's tool calls to the server.
9
- */
10
- import fs from "node:fs";
11
- import path from "node:path";
12
- import os from "node:os";
13
-
14
- const DIR = path.join(os.homedir(), ".standardagents");
15
- const FILE = path.join(DIR, "mcp.json");
16
-
17
- /** A locally-launchable MCP server (stdio transport). */
18
- export interface McpServerConfig {
19
- /** Stable, human-readable id (used to namespace tools, e.g. `mcp:<name>/<tool>`). */
20
- name: string;
21
- /** Executable to spawn (e.g. `node`, `npx`, `uvx`, an absolute path). */
22
- command: string;
23
- /** Arguments passed to the command. */
24
- args: string[];
25
- /** Extra environment variables for the server process (merged over the CLI's env). */
26
- env?: Record<string, string>;
27
- /** Working directory for the server process (defaults to the project dir). */
28
- cwd?: string;
29
- /** When false, the server is remembered but not connected on startup. */
30
- enabled: boolean;
31
- }
32
-
33
- export interface McpConfigFile {
34
- servers: Record<string, McpServerConfig>;
35
- }
36
-
37
- export function loadMcpConfig(): McpConfigFile {
38
- try {
39
- const raw = fs.readFileSync(FILE, "utf8");
40
- const parsed = JSON.parse(raw) as McpConfigFile;
41
- if (!parsed.servers || typeof parsed.servers !== "object") parsed.servers = {};
42
- return parsed;
43
- } catch {
44
- return { servers: {} };
45
- }
46
- }
47
-
48
- export function listMcpServers(): McpServerConfig[] {
49
- const cfg = loadMcpConfig();
50
- return Object.values(cfg.servers).sort((a, b) => a.name.localeCompare(b.name));
51
- }
52
-
53
- export function saveMcpServer(server: McpServerConfig): void {
54
- const cfg = loadMcpConfig();
55
- cfg.servers[server.name] = server;
56
- write(cfg);
57
- }
58
-
59
- export function removeMcpServer(name: string): void {
60
- const cfg = loadMcpConfig();
61
- delete cfg.servers[name];
62
- write(cfg);
63
- }
64
-
65
- export function setMcpServerEnabled(name: string, enabled: boolean): void {
66
- const cfg = loadMcpConfig();
67
- const s = cfg.servers[name];
68
- if (!s) return;
69
- s.enabled = enabled;
70
- write(cfg);
71
- }
72
-
73
- function write(cfg: McpConfigFile): void {
74
- fs.mkdirSync(DIR, { recursive: true });
75
- fs.writeFileSync(FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
76
- }
77
-
78
- /**
79
- * Parse a server definition from a CLI-style command string, e.g.
80
- * `weather: npx -y @modelcontextprotocol/server-weather`
81
- * The part before the first `:` is the server name; the rest is the command +
82
- * args. Returns null if the shape is unusable.
83
- */
84
- export function parseServerSpec(spec: string): McpServerConfig | null {
85
- const trimmed = spec.trim();
86
- const colon = trimmed.indexOf(":");
87
- if (colon <= 0) return null;
88
- const name = trimmed.slice(0, colon).trim();
89
- const rest = trimmed.slice(colon + 1).trim();
90
- if (!name || !rest) return null;
91
- const parts = tokenize(rest);
92
- if (!parts.length) return null;
93
- const [command, ...args] = parts;
94
- return { name, command, args, enabled: true };
95
- }
96
-
97
- /**
98
- * Build a server config from a short name + a full command line, e.g.
99
- * serverFromCommand("playwright", "npx -y @playwright/mcp@latest")
100
- * Returns null if the name or command is unusable.
101
- */
102
- export function serverFromCommand(
103
- name: string,
104
- commandLine: string,
105
- env?: Record<string, string>
106
- ): McpServerConfig | null {
107
- const cleanName = name.trim();
108
- const parts = tokenize(commandLine.trim());
109
- if (!cleanName || !parts.length) return null;
110
- const [command, ...args] = parts;
111
- return { name: cleanName, command, args, env, enabled: true };
112
- }
113
-
114
- /** Minimal shell-ish tokenizer that honours single/double quotes (no expansion). */
115
- function tokenize(input: string): string[] {
116
- const out: string[] = [];
117
- let cur = "";
118
- let quote: '"' | "'" | null = null;
119
- for (let i = 0; i < input.length; i++) {
120
- const ch = input[i];
121
- if (quote) {
122
- if (ch === quote) quote = null;
123
- else cur += ch;
124
- } else if (ch === '"' || ch === "'") {
125
- quote = ch;
126
- } else if (/\s/.test(ch)) {
127
- if (cur) {
128
- out.push(cur);
129
- cur = "";
130
- }
131
- } else {
132
- cur += ch;
133
- }
134
- }
135
- if (cur) out.push(cur);
136
- return out;
137
- }