@oh-my-pi/pi-coding-agent 2.0.1337 → 2.2.1337

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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.2.1337] - 2026-01-03
6
+
7
+ ## [2.1.1337] - 2026-01-03
8
+
9
+ ### Added
10
+
11
+ - Added `pi update` command to check for and install updates from GitHub releases or via bun
12
+
13
+ ### Changed
14
+
15
+ - Changed HTML export to use compile-time bundled templates via Bun macros for improved performance
16
+ - Changed `exportToHtml` and `exportFromFile` functions to be async
17
+ - Simplified build process by embedding assets (themes, templates, agents, commands) directly into the binary at compile time
18
+ - Removed separate asset copying steps from build scripts
19
+
5
20
  ## [2.0.1337] - 2026-01-03
6
21
  ### Added
7
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "2.0.1337",
3
+ "version": "2.2.1337",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -31,17 +31,15 @@
31
31
  "scripts": {
32
32
  "check": "tsgo --noEmit",
33
33
  "clean": "rm -rf dist",
34
- "build": "tsgo -p tsconfig.build.json && chmod +x dist/cli.js && npm run copy-assets",
35
- "build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
36
- "copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && mkdir -p dist/core/export-html && cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && mkdir -p dist/core/tools/task/bundled-agents && cp src/core/tools/task/bundled-agents/*.md dist/core/tools/task/bundled-agents/",
37
- "copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && mkdir -p dist/export-html && cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/export-html/ && cp -r docs dist/ && cp -r examples dist/",
38
- "test": "vitest --run",
34
+ "build": "tsgo -p tsconfig.build.json && chmod +x dist/cli.js",
35
+ "build:binary": "bun build --compile ./src/cli.ts --outfile dist/pi",
36
+ "test": "bun test",
39
37
  "prepublishOnly": "npm run clean && npm run build"
40
38
  },
41
39
  "dependencies": {
42
- "@oh-my-pi/pi-agent-core": "workspace:*",
43
- "@oh-my-pi/pi-ai": "workspace:*",
44
- "@oh-my-pi/pi-tui": "workspace:*",
40
+ "@oh-my-pi/pi-agent-core": "2.2.1337",
41
+ "@oh-my-pi/pi-ai": "2.2.1337",
42
+ "@oh-my-pi/pi-tui": "2.2.1337",
45
43
  "@sinclair/typebox": "^0.34.46",
46
44
  "ajv": "^8.17.1",
47
45
  "chalk": "^5.5.0",
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Type declarations for Bun's import attributes.
3
+ * These allow importing non-JS files as text at build time.
4
+ */
5
+
6
+ // Markdown files imported as text
7
+ declare module "*.md" {
8
+ const content: string;
9
+ export default content;
10
+ }
11
+
12
+ // Text files imported as text
13
+ declare module "*.txt" {
14
+ const content: string;
15
+ export default content;
16
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Update CLI command handler.
3
+ *
4
+ * Handles `pi update` to check for and install updates.
5
+ * Uses bun if available, otherwise downloads binary from GitHub releases.
6
+ */
7
+
8
+ import { execSync, spawnSync } from "node:child_process";
9
+ import { createWriteStream, existsSync, renameSync, unlinkSync } from "node:fs";
10
+ import { dirname } from "node:path";
11
+ import { Readable } from "node:stream";
12
+ import { pipeline } from "node:stream/promises";
13
+ import chalk from "chalk";
14
+ import { VERSION } from "../config";
15
+
16
+ /**
17
+ * Detect if we're running as a Bun compiled binary.
18
+ */
19
+ const isBunBinary =
20
+ import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN");
21
+
22
+ const REPO = "can1357/oh-my-pi";
23
+ const PACKAGE = "@oh-my-pi/pi-coding-agent";
24
+
25
+ interface ReleaseInfo {
26
+ tag: string;
27
+ version: string;
28
+ assets: Array<{ name: string; url: string }>;
29
+ }
30
+
31
+ /**
32
+ * Parse update subcommand arguments.
33
+ * Returns undefined if not an update command.
34
+ */
35
+ export function parseUpdateArgs(args: string[]): { force: boolean; check: boolean } | undefined {
36
+ if (args.length === 0 || args[0] !== "update") {
37
+ return undefined;
38
+ }
39
+
40
+ return {
41
+ force: args.includes("--force") || args.includes("-f"),
42
+ check: args.includes("--check") || args.includes("-c"),
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Check if bun is available in PATH.
48
+ */
49
+ function hasBun(): boolean {
50
+ try {
51
+ const result = spawnSync("bun", ["--version"], { encoding: "utf-8", stdio: "pipe" });
52
+ return result.status === 0;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get the latest release info from GitHub.
60
+ */
61
+ async function getLatestRelease(): Promise<ReleaseInfo> {
62
+ const response = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`);
63
+ if (!response.ok) {
64
+ throw new Error(`Failed to fetch release info: ${response.statusText}`);
65
+ }
66
+
67
+ const data = (await response.json()) as {
68
+ tag_name: string;
69
+ assets: Array<{ name: string; browser_download_url: string }>;
70
+ };
71
+
72
+ return {
73
+ tag: data.tag_name,
74
+ version: data.tag_name.replace(/^v/, ""),
75
+ assets: data.assets.map((a) => ({ name: a.name, url: a.browser_download_url })),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Compare semver versions. Returns:
81
+ * - negative if a < b
82
+ * - 0 if a == b
83
+ * - positive if a > b
84
+ */
85
+ function compareVersions(a: string, b: string): number {
86
+ const pa = a.split(".").map(Number);
87
+ const pb = b.split(".").map(Number);
88
+
89
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
90
+ const na = pa[i] || 0;
91
+ const nb = pb[i] || 0;
92
+ if (na !== nb) return na - nb;
93
+ }
94
+ return 0;
95
+ }
96
+
97
+ /**
98
+ * Get the appropriate binary name for this platform.
99
+ */
100
+ function getBinaryName(): string {
101
+ const platform = process.platform;
102
+ const arch = process.arch;
103
+
104
+ let os: string;
105
+ switch (platform) {
106
+ case "linux":
107
+ os = "linux";
108
+ break;
109
+ case "darwin":
110
+ os = "darwin";
111
+ break;
112
+ case "win32":
113
+ os = "windows";
114
+ break;
115
+ default:
116
+ throw new Error(`Unsupported platform: ${platform}`);
117
+ }
118
+
119
+ let archName: string;
120
+ switch (arch) {
121
+ case "x64":
122
+ archName = "x64";
123
+ break;
124
+ case "arm64":
125
+ archName = "arm64";
126
+ break;
127
+ default:
128
+ throw new Error(`Unsupported architecture: ${arch}`);
129
+ }
130
+
131
+ if (os === "windows") {
132
+ return `pi-${os}-${archName}.exe`;
133
+ }
134
+ return `pi-${os}-${archName}`;
135
+ }
136
+
137
+ /**
138
+ * Update via bun package manager.
139
+ */
140
+ async function updateViaBun(): Promise<void> {
141
+ console.log(chalk.dim("Updating via bun..."));
142
+
143
+ try {
144
+ execSync(`bun update -g ${PACKAGE}`, { stdio: "inherit" });
145
+ console.log(chalk.green("\n✓ Update complete"));
146
+ } catch {
147
+ throw new Error("bun update failed");
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Update by downloading binary from GitHub releases.
153
+ */
154
+ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
155
+ const binaryName = getBinaryName();
156
+ const asset = release.assets.find((a) => a.name === binaryName);
157
+
158
+ if (!asset) {
159
+ throw new Error(`No binary found for ${binaryName}`);
160
+ }
161
+
162
+ const execPath = process.execPath;
163
+ const _execDir = dirname(execPath);
164
+ const tempPath = `${execPath}.new`;
165
+ const backupPath = `${execPath}.bak`;
166
+
167
+ console.log(chalk.dim(`Downloading ${binaryName}...`));
168
+
169
+ // Download to temp file
170
+ const response = await fetch(asset.url, { redirect: "follow" });
171
+ if (!response.ok || !response.body) {
172
+ throw new Error(`Download failed: ${response.statusText}`);
173
+ }
174
+
175
+ const fileStream = createWriteStream(tempPath, { mode: 0o755 });
176
+ const nodeStream = Readable.fromWeb(response.body as import("stream/web").ReadableStream);
177
+ await pipeline(nodeStream, fileStream);
178
+
179
+ // Replace current binary
180
+ console.log(chalk.dim("Installing update..."));
181
+
182
+ try {
183
+ // Backup current binary
184
+ if (existsSync(backupPath)) {
185
+ unlinkSync(backupPath);
186
+ }
187
+ renameSync(execPath, backupPath);
188
+
189
+ // Move new binary into place
190
+ renameSync(tempPath, execPath);
191
+
192
+ // Clean up backup
193
+ unlinkSync(backupPath);
194
+
195
+ console.log(chalk.green(`\n✓ Updated to ${release.version}`));
196
+ console.log(chalk.dim("Restart pi to use the new version"));
197
+ } catch (err) {
198
+ // Restore from backup if possible
199
+ if (existsSync(backupPath) && !existsSync(execPath)) {
200
+ renameSync(backupPath, execPath);
201
+ }
202
+ if (existsSync(tempPath)) {
203
+ unlinkSync(tempPath);
204
+ }
205
+ throw err;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Run the update command.
211
+ */
212
+ export async function runUpdateCommand(opts: { force: boolean; check: boolean }): Promise<void> {
213
+ console.log(chalk.dim(`Current version: ${VERSION}`));
214
+
215
+ // Check for updates
216
+ let release: ReleaseInfo;
217
+ try {
218
+ release = await getLatestRelease();
219
+ } catch (err) {
220
+ console.error(chalk.red(`Failed to check for updates: ${err}`));
221
+ process.exit(1);
222
+ }
223
+
224
+ const comparison = compareVersions(release.version, VERSION);
225
+
226
+ if (comparison <= 0 && !opts.force) {
227
+ console.log(chalk.green("✓ Already up to date"));
228
+ return;
229
+ }
230
+
231
+ if (comparison > 0) {
232
+ console.log(chalk.cyan(`New version available: ${release.version}`));
233
+ } else {
234
+ console.log(chalk.yellow(`Forcing reinstall of ${release.version}`));
235
+ }
236
+
237
+ if (opts.check) {
238
+ // Just check, don't install
239
+ return;
240
+ }
241
+
242
+ // Choose update method
243
+ try {
244
+ if (!isBunBinary && hasBun()) {
245
+ await updateViaBun();
246
+ } else {
247
+ await updateViaBinary(release);
248
+ }
249
+ } catch (err) {
250
+ console.error(chalk.red(`Update failed: ${err}`));
251
+ process.exit(1);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Print update command help.
257
+ */
258
+ export function printUpdateHelp(): void {
259
+ console.log(`${chalk.bold("pi update")} - Check for and install updates
260
+
261
+ ${chalk.bold("Usage:")}
262
+ pi update [options]
263
+
264
+ ${chalk.bold("Options:")}
265
+ -c, --check Check for updates without installing
266
+ -f, --force Force reinstall even if up to date
267
+
268
+ ${chalk.bold("Examples:")}
269
+ pi update Update to latest version
270
+ pi update --check Check if updates are available
271
+ pi update --force Force reinstall
272
+ `);
273
+ }
package/src/config.ts CHANGED
@@ -1,34 +1,31 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
 
5
+ // Embed package.json at build time for config
6
+ import packageJson from "../package.json" with { type: "json" };
7
+
5
8
  // =============================================================================
6
- // Package Detection
9
+ // App Config (from embedded package.json)
7
10
  // =============================================================================
8
11
 
9
- /**
10
- * Detect if we're running as a Bun compiled binary.
11
- * Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path)
12
- */
13
- export const isBunBinary =
14
- import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN");
12
+ export const APP_NAME: string = (packageJson as { piConfig?: { name?: string } }).piConfig?.name || "pi";
13
+ export const CONFIG_DIR_NAME: string =
14
+ (packageJson as { piConfig?: { configDir?: string } }).piConfig?.configDir || ".pi";
15
+ export const VERSION: string = (packageJson as { version: string }).version;
16
+
17
+ // e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
18
+ export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
15
19
 
16
20
  // =============================================================================
17
- // Package Asset Paths (shipped with executable)
21
+ // Package Directory (for optional external docs/examples)
18
22
  // =============================================================================
19
23
 
20
24
  /**
21
- * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md).
22
- * - For Bun binary: returns the directory containing the executable
23
- * - For Node.js (dist/): returns __dirname (the dist/ directory)
24
- * - For tsx (src/): returns parent directory (the package root)
25
+ * Get the base directory for resolving optional package assets (docs, examples).
26
+ * Walk up from import.meta.dir until we find package.json, or fall back to cwd.
25
27
  */
26
28
  export function getPackageDir(): string {
27
- if (isBunBinary) {
28
- // Bun binary: process.execPath points to the compiled executable
29
- return dirname(process.execPath);
30
- }
31
- // Node.js: walk up from import.meta.dir until we find package.json
32
29
  let dir = import.meta.dir;
33
30
  while (dir !== dirname(dir)) {
34
31
  if (existsSync(join(dir, "package.json"))) {
@@ -36,79 +33,30 @@ export function getPackageDir(): string {
36
33
  }
37
34
  dir = dirname(dir);
38
35
  }
39
- // Fallback (shouldn't happen)
40
- return import.meta.dir;
36
+ // Fallback to cwd (docs/examples won't be found, but that's fine)
37
+ return process.cwd();
41
38
  }
42
39
 
43
- /**
44
- * Get path to built-in themes directory (shipped with package)
45
- * - For Bun binary: theme/ next to executable
46
- * - For Node.js (dist/): dist/modes/interactive/theme/
47
- * - For tsx (src/): src/modes/interactive/theme/
48
- */
49
- export function getThemesDir(): string {
50
- if (isBunBinary) {
51
- return join(dirname(process.execPath), "theme");
52
- }
53
- // Theme is in modes/interactive/theme/ relative to src/ or dist/
54
- const packageDir = getPackageDir();
55
- const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
56
- return join(packageDir, srcOrDist, "modes", "interactive", "theme");
57
- }
58
-
59
- /**
60
- * Get path to HTML export template directory (shipped with package)
61
- * - For Bun binary: export-html/ next to executable
62
- * - For Node.js (dist/): dist/core/export-html/
63
- * - For tsx (src/): src/core/export-html/
64
- */
65
- export function getExportTemplateDir(): string {
66
- if (isBunBinary) {
67
- return join(dirname(process.execPath), "export-html");
68
- }
69
- const packageDir = getPackageDir();
70
- const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
71
- return join(packageDir, srcOrDist, "core", "export-html");
72
- }
73
-
74
- /** Get path to package.json */
75
- export function getPackageJsonPath(): string {
76
- return join(getPackageDir(), "package.json");
77
- }
78
-
79
- /** Get path to README.md */
40
+ /** Get path to README.md (optional, may not exist in binary) */
80
41
  export function getReadmePath(): string {
81
42
  return resolve(join(getPackageDir(), "README.md"));
82
43
  }
83
44
 
84
- /** Get path to docs directory */
45
+ /** Get path to docs directory (optional, may not exist in binary) */
85
46
  export function getDocsPath(): string {
86
47
  return resolve(join(getPackageDir(), "docs"));
87
48
  }
88
49
 
89
- /** Get path to examples directory */
50
+ /** Get path to examples directory (optional, may not exist in binary) */
90
51
  export function getExamplesPath(): string {
91
52
  return resolve(join(getPackageDir(), "examples"));
92
53
  }
93
54
 
94
- /** Get path to CHANGELOG.md */
55
+ /** Get path to CHANGELOG.md (optional, may not exist in binary) */
95
56
  export function getChangelogPath(): string {
96
57
  return resolve(join(getPackageDir(), "CHANGELOG.md"));
97
58
  }
98
59
 
99
- // =============================================================================
100
- // App Config (from package.json piConfig)
101
- // =============================================================================
102
-
103
- const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
104
-
105
- export const APP_NAME: string = pkg.piConfig?.name || "pi";
106
- export const CONFIG_DIR_NAME: string = pkg.piConfig?.configDir || ".pi";
107
- export const VERSION: string = pkg.version;
108
-
109
- // e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
110
- export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
111
-
112
60
  // =============================================================================
113
61
  // User Config Paths (~/.pi/agent/*)
114
62
  // =============================================================================
@@ -1900,7 +1900,7 @@ export class AgentSession {
1900
1900
  * @param outputPath Optional output path (defaults to session directory)
1901
1901
  * @returns Path to exported file
1902
1902
  */
1903
- exportToHtml(outputPath?: string): string {
1903
+ async exportToHtml(outputPath?: string): Promise<string> {
1904
1904
  const themeName = this.settingsManager.getTheme();
1905
1905
  return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
1906
1906
  }
@@ -3,7 +3,7 @@
3
3
  * Handles loading, saving, and refreshing credentials from auth.json.
4
4
  */
5
5
 
6
- import { chmodSync, existsSync, mkdirSync } from "node:fs";
6
+ import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
7
7
  import { dirname } from "node:path";
8
8
  import {
9
9
  getEnvApiKey,
@@ -73,8 +73,7 @@ export class AuthStorage {
73
73
  return;
74
74
  }
75
75
  try {
76
- const file = Bun.file(this.authPath);
77
- this.data = JSON.parse(file.text() as unknown as string);
76
+ this.data = JSON.parse(readFileSync(this.authPath, "utf-8"));
78
77
  } catch {
79
78
  this.data = {};
80
79
  }
@@ -1,44 +1,19 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { basename, join } from "node:path";
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
3
  import type { AgentState } from "@oh-my-pi/pi-agent-core";
4
- import { APP_NAME, getExportTemplateDir } from "../../config";
4
+ import { APP_NAME } from "../../config";
5
5
  import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme";
6
6
  import { SessionManager } from "../session-manager";
7
7
 
8
- // Cached minified assets (populated on first use)
9
- let cachedTemplate: string | null = null;
10
- let cachedJs: string | null = null;
11
-
12
- /** Minify CSS by removing comments, unnecessary whitespace, and newlines. */
13
- function minifyCss(css: string): string {
14
- return css
15
- .replace(/\/\*[\s\S]*?\*\//g, "") // Remove comments
16
- .replace(/\s+/g, " ") // Collapse whitespace
17
- .replace(/\s*([{}:;,>+~])\s*/g, "$1") // Remove space around punctuation
18
- .replace(/;}/g, "}") // Remove trailing semicolons
19
- .trim();
20
- }
21
-
22
- /** Minify JS using Bun's transpiler. */
23
- function minifyJs(js: string): string {
24
- const transpiler = new Bun.Transpiler({ loader: "js", minifyWhitespace: true });
25
- return transpiler.transformSync(js);
26
- }
27
-
28
- /** Minify HTML by collapsing whitespace outside of tags. */
29
- function minifyHtml(html: string): string {
30
- return html
31
- .replace(/>\s+</g, "><") // Remove whitespace between tags
32
- .replace(/\s{2,}/g, " ") // Collapse multiple spaces
33
- .trim();
34
- }
8
+ // Bun macro: bundles HTML+CSS+JS at compile time, evaluated at bundle time
9
+ import { getTemplate } from "./template.macro" with { type: "macro" };
35
10
 
36
11
  export interface ExportOptions {
37
12
  outputPath?: string;
38
13
  themeName?: string;
39
14
  }
40
15
 
41
- /** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */
16
+ /** Parse a color string to RGB values. */
42
17
  function parseColor(color: string): { r: number; g: number; b: number } | undefined {
43
18
  const hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
44
19
  if (hexMatch) {
@@ -68,7 +43,7 @@ function getLuminance(r: number, g: number, b: number): number {
68
43
  return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
69
44
  }
70
45
 
71
- /** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */
46
+ /** Adjust color brightness. */
72
47
  function adjustBrightness(color: string, factor: number): string {
73
48
  const parsed = parseColor(color);
74
49
  if (!parsed) return color;
@@ -76,21 +51,15 @@ function adjustBrightness(color: string, factor: number): string {
76
51
  return `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`;
77
52
  }
78
53
 
79
- /** Derive export background colors from a base color (e.g., userMessageBg). */
54
+ /** Derive export background colors from a base color. */
80
55
  function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string; infoBg: string } {
81
56
  const parsed = parseColor(baseColor);
82
57
  if (!parsed) {
83
- return {
84
- pageBg: "rgb(24, 24, 30)",
85
- cardBg: "rgb(30, 30, 36)",
86
- infoBg: "rgb(60, 55, 40)",
87
- };
58
+ return { pageBg: "rgb(24, 24, 30)", cardBg: "rgb(30, 30, 36)", infoBg: "rgb(60, 55, 40)" };
88
59
  }
89
60
 
90
61
  const luminance = getLuminance(parsed.r, parsed.g, parsed.b);
91
- const isLight = luminance > 0.5;
92
-
93
- if (isLight) {
62
+ if (luminance > 0.5) {
94
63
  return {
95
64
  pageBg: adjustBrightness(baseColor, 0.96),
96
65
  cardBg: baseColor,
@@ -104,9 +73,7 @@ function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string
104
73
  };
105
74
  }
106
75
 
107
- /**
108
- * Generate CSS custom property declarations from theme colors.
109
- */
76
+ /** Generate CSS custom properties for theme. */
110
77
  function generateThemeVars(themeName?: string): string {
111
78
  const colors = getResolvedThemeColors(themeName);
112
79
  const lines: string[] = [];
@@ -114,16 +81,15 @@ function generateThemeVars(themeName?: string): string {
114
81
  lines.push(`--${key}: ${value};`);
115
82
  }
116
83
 
117
- // Use explicit theme export colors if available, otherwise derive from userMessageBg
118
84
  const themeExport = getThemeExportColors(themeName);
119
85
  const userMessageBg = colors.userMessageBg || "#343541";
120
- const derivedColors = deriveExportColors(userMessageBg);
86
+ const derived = deriveExportColors(userMessageBg);
121
87
 
122
- lines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`);
123
- lines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`);
124
- lines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`);
88
+ lines.push(`--body-bg: ${themeExport.pageBg ?? derived.pageBg};`);
89
+ lines.push(`--container-bg: ${themeExport.cardBg ?? derived.cardBg};`);
90
+ lines.push(`--info-bg: ${themeExport.infoBg ?? derived.infoBg};`);
125
91
 
126
- return lines.join("\n ");
92
+ return lines.join(" ");
127
93
  }
128
94
 
129
95
  interface SessionData {
@@ -134,61 +100,28 @@ interface SessionData {
134
100
  tools?: { name: string; description: string }[];
135
101
  }
136
102
 
137
- /**
138
- * Core HTML generation logic shared by both export functions.
139
- */
140
- function generateHtml(sessionData: SessionData, themeName?: string): string {
141
- const templateDir = getExportTemplateDir();
142
-
143
- // Load and minify assets on first use
144
- if (!cachedTemplate) {
145
- cachedTemplate = minifyHtml(readFileSync(join(templateDir, "template.html"), "utf-8"));
146
- }
147
- if (!cachedJs) {
148
- cachedJs = minifyJs(readFileSync(join(templateDir, "template.js"), "utf-8"));
149
- }
150
-
151
- const templateCss = readFileSync(join(templateDir, "template.css"), "utf-8");
152
-
103
+ /** Generate HTML from bundled template with runtime substitutions. */
104
+ async function generateHtml(sessionData: SessionData, themeName?: string): Promise<string> {
153
105
  const themeVars = generateThemeVars(themeName);
154
- const colors = getResolvedThemeColors(themeName);
155
- const exportColors = deriveExportColors(colors.userMessageBg || "#343541");
156
- const bodyBg = exportColors.pageBg;
157
- const containerBg = exportColors.cardBg;
158
- const infoBg = exportColors.infoBg;
159
-
160
- // Base64 encode session data to avoid escaping issues
161
106
  const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64");
107
+ const template = await getTemplate();
162
108
 
163
- // Build and minify the CSS with theme variables injected
164
- const css = minifyCss(
165
- templateCss
166
- .replace("{{THEME_VARS}}", themeVars)
167
- .replace("{{BODY_BG}}", bodyBg)
168
- .replace("{{CONTAINER_BG}}", containerBg)
169
- .replace("{{INFO_BG}}", infoBg),
170
- );
171
-
172
- return cachedTemplate
173
- .replace("{{CSS}}", css)
174
- .replace("{{JS}}", cachedJs)
109
+ return template
110
+ .replace("<theme-vars/>", `<style>:root { ${themeVars} }</style>`)
175
111
  .replace("{{SESSION_DATA}}", sessionDataBase64);
176
112
  }
177
113
 
178
- /**
179
- * Export session to HTML using SessionManager and AgentState.
180
- * Used by TUI's /export command.
181
- */
182
- export function exportSessionToHtml(sm: SessionManager, state?: AgentState, options?: ExportOptions | string): string {
114
+ /** Export session to HTML using SessionManager and AgentState. */
115
+ export async function exportSessionToHtml(
116
+ sm: SessionManager,
117
+ state?: AgentState,
118
+ options?: ExportOptions | string,
119
+ ): Promise<string> {
183
120
  const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {};
184
121
 
185
122
  const sessionFile = sm.getSessionFile();
186
- if (!sessionFile) {
187
- throw new Error("Cannot export in-memory session to HTML");
188
- }
189
- if (!existsSync(sessionFile)) {
190
- throw new Error("Nothing to export yet - start a conversation first");
191
- }
123
+ if (!sessionFile) throw new Error("Cannot export in-memory session to HTML");
124
+ if (!existsSync(sessionFile)) throw new Error("Nothing to export yet - start a conversation first");
192
125
 
193
126
  const sessionData: SessionData = {
194
127
  header: sm.getHeader(),
@@ -198,46 +131,28 @@ export function exportSessionToHtml(sm: SessionManager, state?: AgentState, opti
198
131
  tools: state?.tools?.map((t) => ({ name: t.name, description: t.description })),
199
132
  };
200
133
 
201
- const html = generateHtml(sessionData, opts.themeName);
202
-
203
- let outputPath = opts.outputPath;
204
- if (!outputPath) {
205
- const sessionBasename = basename(sessionFile, ".jsonl");
206
- outputPath = `${APP_NAME}-session-${sessionBasename}.html`;
207
- }
134
+ const html = await generateHtml(sessionData, opts.themeName);
135
+ const outputPath = opts.outputPath || `${APP_NAME}-session-${basename(sessionFile, ".jsonl")}.html`;
208
136
 
209
137
  writeFileSync(outputPath, html, "utf8");
210
138
  return outputPath;
211
139
  }
212
140
 
213
- /**
214
- * Export session file to HTML (standalone, without AgentState).
215
- * Used by CLI for exporting arbitrary session files.
216
- */
217
- export function exportFromFile(inputPath: string, options?: ExportOptions | string): string {
141
+ /** Export session file to HTML (standalone). */
142
+ export async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise<string> {
218
143
  const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {};
219
144
 
220
- if (!existsSync(inputPath)) {
221
- throw new Error(`File not found: ${inputPath}`);
222
- }
145
+ if (!existsSync(inputPath)) throw new Error(`File not found: ${inputPath}`);
223
146
 
224
147
  const sm = SessionManager.open(inputPath);
225
-
226
148
  const sessionData: SessionData = {
227
149
  header: sm.getHeader(),
228
150
  entries: sm.getEntries(),
229
151
  leafId: sm.getLeafId(),
230
- systemPrompt: undefined,
231
- tools: undefined,
232
152
  };
233
153
 
234
- const html = generateHtml(sessionData, opts.themeName);
235
-
236
- let outputPath = opts.outputPath;
237
- if (!outputPath) {
238
- const inputBasename = basename(inputPath, ".jsonl");
239
- outputPath = `${APP_NAME}-session-${inputBasename}.html`;
240
- }
154
+ const html = await generateHtml(sessionData, opts.themeName);
155
+ const outputPath = opts.outputPath || `${APP_NAME}-session-${basename(inputPath, ".jsonl")}.html`;
241
156
 
242
157
  writeFileSync(outputPath, html, "utf8");
243
158
  return outputPath;
@@ -1,10 +1,3 @@
1
- :root {
2
- {{THEME_VARS}}
3
- --body-bg: {{BODY_BG}};
4
- --container-bg: {{CONTAINER_BG}};
5
- --info-bg: {{INFO_BG}};
6
- }
7
-
8
1
  * { margin: 0; padding: 0; box-sizing: border-box; }
9
2
 
10
3
  :root {
@@ -4,9 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Session Export</title>
7
- <style>
8
- {{CSS}}
9
- </style>
7
+ <template-css/>
8
+ <theme-vars/>
10
9
  </head>
11
10
  <body>
12
11
  <button id="hamburger" title="Open sidebar"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="12" r="2.5"/><rect x="5" y="6" width="2" height="12"/><path d="M6 12h10c1 0 2 0 2-2V8"/></svg></button>
@@ -41,6 +40,6 @@
41
40
  <script id="session-data" type="application/json">{{SESSION_DATA}}</script>
42
41
  <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.4/marked.min.js" integrity="sha512-VmLxPVdDGeR+F0DzUHVqzHwaR4ZSSh1g/7aYXwKT1PAGVxunOEcysta+4H5Utvmpr2xExEPybZ8q+iM9F1tGdw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
43
42
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
44
- <script>{{JS}}</script>
43
+ <template-js/>
45
44
  </body>
46
45
  </html>
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Bun macro that inlines HTML template with CSS/JS at compile time.
3
+ * This runs during `bun build` and embeds the result as a string.
4
+ */
5
+ export async function getTemplate(): Promise<string> {
6
+ const dir = new URL(".", import.meta.url).pathname;
7
+
8
+ // Read all files
9
+ const html = await Bun.file(`${dir}template.html`).text();
10
+ const css = await Bun.file(`${dir}template.css`).text();
11
+ const js = await Bun.file(`${dir}template.js`).text();
12
+
13
+ // Minify CSS
14
+ const minifiedCss = css
15
+ .replace(/\/\*[\s\S]*?\*\//g, "")
16
+ .replace(/\s+/g, " ")
17
+ .replace(/\s*([{}:;,])\s*/g, "$1")
18
+ .trim();
19
+
20
+ // Inline everything
21
+ return html
22
+ .replace("<template-css/>", `<style>${minifiedCss}</style>`)
23
+ .replace("<template-js/>", `<script>${js}</script>`);
24
+ }
@@ -1,17 +1,24 @@
1
1
  /**
2
2
  * Bundled agent definitions.
3
3
  *
4
- * Agents are loaded from .md files in the bundled-agents directory.
5
- * These serve as defaults when no user/project agents are discovered.
4
+ * Agents are embedded at build time via Bun's import with { type: "text" }.
6
5
  */
7
6
 
8
- import * as fs from "node:fs";
9
- import * as path from "node:path";
10
- import { fileURLToPath } from "node:url";
7
+ // Embed agent markdown files at build time
8
+ import browserMd from "./bundled-agents/browser.md" with { type: "text" };
9
+ import exploreMd from "./bundled-agents/explore.md" with { type: "text" };
10
+ import planMd from "./bundled-agents/plan.md" with { type: "text" };
11
+ import reviewerMd from "./bundled-agents/reviewer.md" with { type: "text" };
12
+ import taskMd from "./bundled-agents/task.md" with { type: "text" };
11
13
  import type { AgentDefinition, AgentSource } from "./types";
12
14
 
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
- const BUNDLED_AGENTS_DIR = path.join(__dirname, "bundled-agents");
15
+ const EMBEDDED_AGENTS: { name: string; content: string }[] = [
16
+ { name: "browser.md", content: browserMd },
17
+ { name: "explore.md", content: exploreMd },
18
+ { name: "plan.md", content: planMd },
19
+ { name: "reviewer.md", content: reviewerMd },
20
+ { name: "task.md", content: taskMd },
21
+ ];
15
22
 
16
23
  /**
17
24
  * Parse YAML frontmatter from markdown content.
@@ -47,16 +54,9 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, string
47
54
  }
48
55
 
49
56
  /**
50
- * Load a single agent from a markdown file.
57
+ * Parse an agent from embedded content.
51
58
  */
52
- function loadAgentFromFile(filePath: string, source: AgentSource): AgentDefinition | null {
53
- let content: string;
54
- try {
55
- content = fs.readFileSync(filePath, "utf-8");
56
- } catch {
57
- return null;
58
- }
59
-
59
+ function parseAgent(fileName: string, content: string, source: AgentSource): AgentDefinition | null {
60
60
  const { frontmatter, body } = parseFrontmatter(content);
61
61
 
62
62
  if (!frontmatter.name || !frontmatter.description) {
@@ -79,7 +79,7 @@ function loadAgentFromFile(filePath: string, source: AgentSource): AgentDefiniti
79
79
  recursive,
80
80
  systemPrompt: body,
81
81
  source,
82
- filePath,
82
+ filePath: `embedded:${fileName}`,
83
83
  };
84
84
  }
85
85
 
@@ -87,7 +87,7 @@ function loadAgentFromFile(filePath: string, source: AgentSource): AgentDefiniti
87
87
  let bundledAgentsCache: AgentDefinition[] | null = null;
88
88
 
89
89
  /**
90
- * Load all bundled agents from the bundled-agents directory.
90
+ * Load all bundled agents from embedded content.
91
91
  * Results are cached after first load.
92
92
  */
93
93
  export function loadBundledAgents(): AgentDefinition[] {
@@ -97,24 +97,8 @@ export function loadBundledAgents(): AgentDefinition[] {
97
97
 
98
98
  const agents: AgentDefinition[] = [];
99
99
 
100
- if (!fs.existsSync(BUNDLED_AGENTS_DIR)) {
101
- bundledAgentsCache = agents;
102
- return agents;
103
- }
104
-
105
- let entries: fs.Dirent[];
106
- try {
107
- entries = fs.readdirSync(BUNDLED_AGENTS_DIR, { withFileTypes: true });
108
- } catch {
109
- bundledAgentsCache = agents;
110
- return agents;
111
- }
112
-
113
- for (const entry of entries) {
114
- if (!entry.name.endsWith(".md")) continue;
115
-
116
- const filePath = path.join(BUNDLED_AGENTS_DIR, entry.name);
117
- const agent = loadAgentFromFile(filePath, "bundled");
100
+ for (const { name, content } of EMBEDDED_AGENTS) {
101
+ const agent = parseAgent(name, content, "bundled");
118
102
  if (agent) {
119
103
  agents.push(agent);
120
104
  }
@@ -1,17 +1,23 @@
1
1
  /**
2
2
  * Workflow commands for orchestrating multi-agent workflows.
3
3
  *
4
- * Commands are loaded from .md files with YAML frontmatter.
5
- * They define multi-step workflows that chain agent outputs.
4
+ * Commands are embedded at build time via Bun's import with { type: "text" }.
6
5
  */
7
6
 
8
7
  import * as fs from "node:fs";
9
8
  import * as os from "node:os";
10
9
  import * as path from "node:path";
11
- import { fileURLToPath } from "node:url";
12
10
 
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
- const BUNDLED_COMMANDS_DIR = path.join(__dirname, "bundled-commands");
11
+ // Embed command markdown files at build time
12
+ import architectPlanMd from "./bundled-commands/architect-plan.md" with { type: "text" };
13
+ import implementMd from "./bundled-commands/implement.md" with { type: "text" };
14
+ import implementWithCriticMd from "./bundled-commands/implement-with-critic.md" with { type: "text" };
15
+
16
+ const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
17
+ { name: "architect-plan.md", content: architectPlanMd },
18
+ { name: "implement-with-critic.md", content: implementWithCriticMd },
19
+ { name: "implement.md", content: implementMd },
20
+ ];
15
21
 
16
22
  /** Workflow command definition */
17
23
  export interface WorkflowCommand {
@@ -56,9 +62,9 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, string
56
62
  }
57
63
 
58
64
  /**
59
- * Load commands from a directory.
65
+ * Load commands from a directory (for user/project commands).
60
66
  */
61
- function loadCommandsFromDir(dir: string, source: "bundled" | "user" | "project"): WorkflowCommand[] {
67
+ function loadCommandsFromDir(dir: string, source: "user" | "project"): WorkflowCommand[] {
62
68
  const commands: WorkflowCommand[] = [];
63
69
 
64
70
  if (!fs.existsSync(dir)) {
@@ -137,15 +143,30 @@ function findNearestDir(cwd: string, relPath: string): string | null {
137
143
  let bundledCommandsCache: WorkflowCommand[] | null = null;
138
144
 
139
145
  /**
140
- * Load all bundled commands.
146
+ * Load all bundled commands from embedded content.
141
147
  */
142
148
  export function loadBundledCommands(): WorkflowCommand[] {
143
149
  if (bundledCommandsCache !== null) {
144
150
  return bundledCommandsCache;
145
151
  }
146
152
 
147
- bundledCommandsCache = loadCommandsFromDir(BUNDLED_COMMANDS_DIR, "bundled");
148
- return bundledCommandsCache;
153
+ const commands: WorkflowCommand[] = [];
154
+
155
+ for (const { name, content } of EMBEDDED_COMMANDS) {
156
+ const { frontmatter, body } = parseFrontmatter(content);
157
+ const cmdName = name.replace(/\.md$/, "");
158
+
159
+ commands.push({
160
+ name: cmdName,
161
+ description: frontmatter.description || "",
162
+ instructions: body,
163
+ source: "bundled",
164
+ filePath: `embedded:${name}`,
165
+ });
166
+ }
167
+
168
+ bundledCommandsCache = commands;
169
+ return commands;
149
170
  }
150
171
 
151
172
  /**
package/src/main.ts CHANGED
@@ -14,6 +14,7 @@ import { processFileArguments } from "./cli/file-processor";
14
14
  import { listModels } from "./cli/list-models";
15
15
  import { parsePluginArgs, printPluginHelp, runPluginCommand } from "./cli/plugin-cli";
16
16
  import { selectSession } from "./cli/session-picker";
17
+ import { parseUpdateArgs, printUpdateHelp, runUpdateCommand } from "./cli/update-cli";
17
18
  import { CONFIG_DIR_NAME, getAgentDir, getModelsPath, VERSION } from "./config";
18
19
  import type { AgentSession } from "./core/agent-session";
19
20
  import type { LoadedCustomTool } from "./core/custom-tools/index";
@@ -308,6 +309,17 @@ export async function main(args: string[]) {
308
309
  return;
309
310
  }
310
311
 
312
+ // Handle update subcommand
313
+ const updateCmd = parseUpdateArgs(args);
314
+ if (updateCmd) {
315
+ if (args.includes("--help") || args.includes("-h")) {
316
+ printUpdateHelp();
317
+ return;
318
+ }
319
+ await runUpdateCommand(updateCmd);
320
+ return;
321
+ }
322
+
311
323
  // Run migrations
312
324
  const { migratedAuthProviders: migratedProviders } = runMigrations();
313
325
 
@@ -338,7 +350,7 @@ export async function main(args: string[]) {
338
350
  if (parsed.export) {
339
351
  try {
340
352
  const outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;
341
- const result = exportFromFile(parsed.export, outputPath);
353
+ const result = await exportFromFile(parsed.export, outputPath);
342
354
  console.log(`Exported to: ${result}`);
343
355
  return;
344
356
  } catch (error: unknown) {
@@ -2130,7 +2130,7 @@ export class InteractiveMode {
2130
2130
 
2131
2131
  // HTML file export
2132
2132
  try {
2133
- const filePath = this.session.exportToHtml(arg);
2133
+ const filePath = await this.session.exportToHtml(arg);
2134
2134
  this.showStatus(`Session exported to: ${filePath}`);
2135
2135
  } catch (error: unknown) {
2136
2136
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -2153,7 +2153,7 @@ export class InteractiveMode {
2153
2153
  // Export to a temp file
2154
2154
  const tmpFile = path.join(os.tmpdir(), "session.html");
2155
2155
  try {
2156
- this.session.exportToHtml(tmpFile);
2156
+ await this.session.exportToHtml(tmpFile);
2157
2157
  } catch (error: unknown) {
2158
2158
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
2159
2159
  return;
@@ -5,8 +5,11 @@ import { type Static, Type } from "@sinclair/typebox";
5
5
  import { TypeCompiler } from "@sinclair/typebox/compiler";
6
6
  import chalk from "chalk";
7
7
  import { highlight, supportsLanguage } from "cli-highlight";
8
- import { getCustomThemesDir, getThemesDir } from "../../../config";
8
+ import { getCustomThemesDir } from "../../../config";
9
9
  import { logger } from "../../../core/logger";
10
+ // Embed theme JSON files at build time
11
+ import darkThemeJson from "./dark.json" with { type: "json" };
12
+ import lightThemeJson from "./light.json" with { type: "json" };
10
13
 
11
14
  // ============================================================================
12
15
  // Types & Schema
@@ -450,18 +453,12 @@ export class Theme {
450
453
  // Theme Loading
451
454
  // ============================================================================
452
455
 
453
- let BUILTIN_THEMES: Record<string, ThemeJson> | undefined;
456
+ const BUILTIN_THEMES: Record<string, ThemeJson> = {
457
+ dark: darkThemeJson as ThemeJson,
458
+ light: lightThemeJson as ThemeJson,
459
+ };
454
460
 
455
461
  function getBuiltinThemes(): Record<string, ThemeJson> {
456
- if (!BUILTIN_THEMES) {
457
- const themesDir = getThemesDir();
458
- const darkPath = path.join(themesDir, "dark.json");
459
- const lightPath = path.join(themesDir, "light.json");
460
- BUILTIN_THEMES = {
461
- dark: JSON.parse(fs.readFileSync(darkPath, "utf-8")) as ThemeJson,
462
- light: JSON.parse(fs.readFileSync(lightPath, "utf-8")) as ThemeJson,
463
- };
464
- }
465
462
  return BUILTIN_THEMES;
466
463
  }
467
464
 
@@ -401,7 +401,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
401
401
  }
402
402
 
403
403
  case "export_html": {
404
- const path = session.exportToHtml(command.outputPath);
404
+ const path = await session.exportToHtml(command.outputPath);
405
405
  return success(id, "export_html", { path });
406
406
  }
407
407