thebird 1.2.79 → 1.2.81

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.
Files changed (79) hide show
  1. package/.github/workflows/publish.yml +9 -1
  2. package/CHANGELOG.md +217 -0
  3. package/CLAUDE.md +16 -0
  4. package/docs/agent-chat.js +7 -4
  5. package/docs/app.js +14 -11
  6. package/docs/defaults.json +1 -1
  7. package/docs/index.html +23 -6
  8. package/docs/kilo-fs-mirror.js +15 -0
  9. package/docs/kilo-http-stream.js +47 -0
  10. package/docs/node-builtins.js +24 -0
  11. package/docs/preview/index.html +32 -0
  12. package/docs/preview-sw-client.js +37 -6
  13. package/docs/preview-sw.js +55 -51
  14. package/docs/shell-awk.js +113 -0
  15. package/docs/shell-builtins-extra.js +121 -0
  16. package/docs/shell-builtins-text.js +109 -0
  17. package/docs/shell-builtins-util.js +112 -0
  18. package/docs/shell-builtins.js +183 -0
  19. package/docs/shell-bun.js +45 -0
  20. package/docs/shell-control.js +132 -0
  21. package/docs/shell-deno.js +54 -0
  22. package/docs/shell-exec.js +85 -0
  23. package/docs/shell-expand.js +164 -0
  24. package/docs/shell-fd.js +86 -0
  25. package/docs/shell-jobs.js +86 -0
  26. package/docs/shell-node-advanced.js +86 -0
  27. package/docs/shell-node-brotli.js +22 -0
  28. package/docs/shell-node-busnet.js +90 -0
  29. package/docs/shell-node-cipher.js +61 -0
  30. package/docs/shell-node-cluster.js +33 -0
  31. package/docs/shell-node-coreutils.js +36 -0
  32. package/docs/shell-node-crypto.js +137 -0
  33. package/docs/shell-node-dns.js +41 -0
  34. package/docs/shell-node-extras.js +148 -0
  35. package/docs/shell-node-firefox.js +95 -0
  36. package/docs/shell-node-git.js +60 -0
  37. package/docs/shell-node-inspector.js +39 -0
  38. package/docs/shell-node-io.js +131 -0
  39. package/docs/shell-node-ipc.js +15 -0
  40. package/docs/shell-node-keyobject.js +60 -0
  41. package/docs/shell-node-modules.js +157 -0
  42. package/docs/shell-node-native.js +31 -0
  43. package/docs/shell-node-net.js +71 -0
  44. package/docs/shell-node-observe.js +80 -0
  45. package/docs/shell-node-opfs.js +54 -0
  46. package/docs/shell-node-procfs.js +42 -0
  47. package/docs/shell-node-profiler.js +50 -0
  48. package/docs/shell-node-registry.js +24 -0
  49. package/docs/shell-node-resolve.js +147 -0
  50. package/docs/shell-node-runtime.js +83 -0
  51. package/docs/shell-node-srcmap.js +52 -0
  52. package/docs/shell-node-stdlib.js +103 -0
  53. package/docs/shell-node-streams.js +66 -0
  54. package/docs/shell-node-tar.js +47 -0
  55. package/docs/shell-node-testrunner.js +35 -0
  56. package/docs/shell-node-util-extras.js +66 -0
  57. package/docs/shell-node.js +175 -169
  58. package/docs/shell-npm.js +173 -0
  59. package/docs/shell-parser.js +122 -0
  60. package/docs/shell-pm-layout.js +62 -0
  61. package/docs/shell-pm.js +39 -0
  62. package/docs/shell-posix.js +70 -0
  63. package/docs/shell-procsub.js +65 -0
  64. package/docs/shell-readline.js +59 -4
  65. package/docs/shell-runtime.js +37 -0
  66. package/docs/shell-sed.js +83 -0
  67. package/docs/shell-signals.js +54 -0
  68. package/docs/shell-sw-jobs.js +76 -0
  69. package/docs/shell-ts.js +30 -0
  70. package/docs/shell.js +161 -152
  71. package/docs/terminal.js +9 -11
  72. package/docs/todo.html +211 -0
  73. package/package.json +1 -1
  74. package/server.js +43 -4
  75. package/start-kilo.js +45 -0
  76. package/test.js +199 -0
  77. package/.codeinsight +0 -73
  78. package/docs/acp-stream.js +0 -102
  79. package/docs/coi-serviceworker.js +0 -2
@@ -28,7 +28,15 @@ jobs:
28
28
 
29
29
  - name: Bump patch version
30
30
  run: |
31
- NEW_VERSION=$(npm version patch --no-git-tag-version)
31
+ git fetch --tags --force
32
+ for i in 1 2 3 4 5; do
33
+ NEW_VERSION=$(npm version patch --no-git-tag-version)
34
+ if git rev-parse "${NEW_VERSION}" >/dev/null 2>&1; then
35
+ echo "Tag ${NEW_VERSION} already exists, bumping again (attempt $i)"
36
+ continue
37
+ fi
38
+ break
39
+ done
32
40
  git add package.json
33
41
  git commit -m "chore: bump to ${NEW_VERSION}"
34
42
  git tag "${NEW_VERSION}"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,213 @@
1
+
2
+ ## [unreleased] 2026-04-21 node parity pass 12 — internal listen infrastructure
3
+ - feat: busnet — in-browser TCP-like listen/connect via BroadcastChannel cross-tab fabric
4
+ - feat: net.createServer now uses busnet — apps listen on ports other in-browser apps can connect to
5
+ - feat: busHttp — HTTP request/response framing over busnet
6
+ - feat: service discovery — busnet.discover() returns [{port,service,origin}] from peer tabs
7
+ - feat: netstat builtin — lists local listeners + peer services
8
+ - feat: window.__debug.node.busnet exposes full state
9
+ - feat: same-tab listen+connect works instantly without BroadcastChannel round-trip
10
+
11
+ ## [unreleased] 2026-04-21 node parity pass 11 — virtualization + polyfills wave 3
12
+ - feat: virtual /proc filesystem — /proc/self/{cmdline,environ,stat,status,maps,limits}, /proc/{cpuinfo,meminfo,uptime,loadavg,version,stat,mounts,filesystems}
13
+ - feat: virtual /etc — hosts, resolv.conf, passwd, group, os-release, hostname, machine-id, shells
14
+ - feat: isomorphic-git wrapper — git.clone/commit/push/pull/status/log/checkout against real remotes via HTTP
15
+ - feat: tar/tar.gz extract + list — hand-rolled POSIX tar reader + fflate gunzip, works with real npm tarballs
16
+ - feat: DoH DNS polyfill — dns.resolve/resolve4/resolve6/resolveMx/resolveTxt/resolveNs/lookup/reverse via Cloudflare DoH with Google fallback
17
+ - feat: native addon dispatch — .node files route to WASM/JS equivalents (bufferutil, utf-8-validate, bcrypt, argon2, farmhash; sharp/better_sqlite3 placeholders)
18
+ - feat: process.dlopen for native modules
19
+ - feat: coreutils builtins — uname/whoami/hostname/id/df/free/uptime/ps/nproc/arch/yes/seq/tac/rev/nl/fold/od/xxd/dirname/basename/pwd/groups/logname/tty/stty/locale
20
+ - feat: npm registry shim — view/search/deps/tarballUrl/fetchTarball via esm.sh + registry.npmjs.org
21
+ - feat: os.cpus() returns real navigator.hardwareConcurrency, os.networkInterfaces returns lo
22
+ - feat: process.resourceUsage real numbers from performance.memory
23
+ - feat: crypto.secureHeapUsed
24
+ - feat: os.constants.signals/errno populated
25
+
26
+ ## [unreleased] 2026-04-21 node parity pass 10 — test runner + util extras + IPC
27
+ - feat: node:test real runner — test/describe/it execute, report pass/fail/skip with colors and timing
28
+ - feat: node:test mock.fn / mock.method with calls recording
29
+ - feat: node:test/reporters TAP reporter (ok/not ok/plan)
30
+ - feat: util.styleText (named styles: red/green/bold/italic/etc)
31
+ - feat: util.stripVTControlCharacters removes ANSI escapes
32
+ - feat: util.getCallSites frame extractor
33
+ - feat: util.MIMEType + util.MIMEParams (RFC-compliant parse/serialize)
34
+ - feat: console.table/group/groupEnd/time/timeEnd/timeLog/count/countReset/dir/trace/assert/clear — full console surface
35
+ - feat: readline.createInterface — real interactive question/answer via xterm, asyncIterator, cursorTo/clearLine
36
+ - feat: fork IPC via BroadcastChannel — process.send/process.on('message')
37
+ - feat: node:sqlite module alias (DatabaseSync) — Node 22+ API
38
+
39
+ ## [unreleased] 2026-04-21 node parity pass 9 — pnpm/yarn wired, workspaces, dlx
40
+ - feat: shell dispatches pnpm/yarn/bun/deno/corepack/dlx commands (previously shell-pm.js existed but wasn't wired)
41
+ - feat: workspaces resolution — package.json 'workspaces' field + pnpm-workspace.yaml packages:- syntax
42
+ - feat: Yarn Classic v1 lockfile writer + parser (real format, not JSON placeholder)
43
+ - feat: pnpm dlx / yarn dlx / bun x / npx — unified dlx runner via esm.sh
44
+ - feat: runtime observability — window.__debug.node.runtime.history tracks node→deno→bun switches
45
+ - feat: window.__debug.node.pm — pm command history (200 entries, cwd, ts, args)
46
+ - feat: Deno.stdin/stdout/stderr with ReadableStream/WritableStream surfaces
47
+ - feat: Bun.stdin/stdout/stderr .stream()/.text()/.writer() API
48
+ - feat: tab completion includes pnpm/yarn/bun/deno/npx/corepack
49
+ - feat: pnpm layout scaffold (.pnpm/<name>@<ver>/node_modules/<name> + symlinks) via shell-pm-layout.js
50
+
51
+ ## [unreleased] 2026-04-20 node parity pass 8 — Deno/Bun/pnpm/yarn + POSIX
52
+ - feat: runtime detection (Deno, Bun, Node, browser) with capability flags
53
+ - feat: Deno global namespace — readTextFile/writeTextFile/mkdir/remove/stat/serve/Command/permissions/env
54
+ - feat: Bun global namespace — file/write/serve/spawn/shell(\`\`)/hash/password/TOML/nanoseconds/deepEquals
55
+ - feat: package manager dispatcher — auto-detects bun.lock/pnpm-lock.yaml/yarn.lock/package-lock.json + packageManager field
56
+ - feat: pm install/add/remove/run/ls/init/task unified across npm/pnpm/yarn/bun
57
+ - feat: deno task, deno.json/jsonc parsing, bunfig.toml parser, workspaces enumeration hooks
58
+ - feat: jsr: and npm: specifier rewriting to esm.sh
59
+ - feat: TypeScript direct execution — .ts/.tsx strip via regex (sucrase lazy-loaded)
60
+ - feat: shebang dispatch — #!/usr/bin/env deno|bun|node sets matching globals
61
+ - feat: corepack stub (no-op)
62
+ - feat: POSIX symlinks via sentinel entries — symlinkSync/readlinkSync/lstatSync/realpathSync with ELOOP at 40 hops
63
+ - feat: hard links + inode refcounting — linkSync, stat.nlink, stat.ino
64
+ - feat: file mode bits — chmodSync, S_IFREG/S_IFDIR/S_IFLNK/S_IFIFO constants
65
+ - feat: file descriptors — openSync/closeSync/readSync/writeSync/fstatSync/ftruncate
66
+ - feat: process.umask/cwd/chdir
67
+ - feat: mkdtempSync, cpSync(recursive), fs.mkfifoSync stub
68
+ - feat: Stats with isFile/isDirectory/isSymbolicLink/isFIFO, atime/mtime/ctime/birthtime Dates
69
+
70
+ ## [unreleased] 2026-04-20 node parity pass 7 — Firefox maximization + polyfills
71
+ - feat: browser detection (vendor, version, 10+ capabilities) + window.__debug.node.polyfills registry
72
+ - feat: OPFS-backed fs.promises when available — real persistence (readFile/writeFile/mkdir/rm/stat/list) via SyncAccessHandle in worker, IDB fallback
73
+ - feat: brotli polyfill via brotli-wasm (compressSync/decompressSync + Transform streams)
74
+ - feat: Error.prepareStackTrace source-map integration via source-map-js — original filenames/lines when process.sourceMapsEnabled
75
+ - feat: net.Socket/tls.connect real polyfill via WebSocket-backed TCP tunnel (window.__plugkit_tcp_relay)
76
+ - feat: dgram.Socket polyfill via WebSocket-wrapped datagrams (window.__plugkit_udp_relay)
77
+ - feat: inspector.open() real CDP endpoint via postMessage channel (Runtime.evaluate/Debugger.enable/Profiler.*)
78
+ - feat: v8 CPU profiler backed by PerformanceObserver (CPUProfile.startProfiling/stopProfiling)
79
+ - feat: v8.writeHeapSnapshot — minimal valid V8 heap snapshot JSON format, Chrome DevTools importable
80
+ - feat: X509Certificate sync access via crypto.preloadX509()
81
+ - feat: vm cross-realm structuredClone boundary — Array/Object instanceof works across iframe
82
+ - feat: cluster module real impl via BroadcastChannel (fork/isMaster/worker.send/'message')
83
+ - feat: CompressionStream native gzip/deflate preferred over fflate when available
84
+ - feat: WebCodecs exposed as 'codecs' module (VideoEncoder/AudioEncoder/etc)
85
+ - feat: web-push module (pushManager.subscribe/getSubscription)
86
+ - feat: process.storage.estimate/persist/persisted + process.storageBuckets
87
+ - feat: FileSystemObserver integration for real fs.watch events on OPFS
88
+ - feat: Firefox Worker module-type compat shim
89
+ - feat: observability panel window.__debug.node.polyfills shows backing (native|wasm|fallback) per feature
90
+
91
+ ## [unreleased] 2026-04-20 node parity pass 6 — 23 PRD items shipped
92
+ - feat: crypto ECDSA PEM sign/verify (P-256/P-384/P-521)
93
+ - feat: crypto.hkdf/hkdfAsync via webcrypto deriveBits
94
+ - feat: crypto.createECDH (prime256v1/secp384r1/secp521r1) shared-secret derivation
95
+ - feat: KeyObject (createPrivateKey/createPublicKey) + X509Certificate via @peculiar/x509
96
+ - feat: streaming zlib (createGzip/Gunzip/Deflate/Inflate) via fflate class API
97
+ - feat: vm module (runInThisContext/runInNewContext/runInContext via iframe)
98
+ - feat: module.register() ESM loader hooks (resolve/load pipeline)
99
+ - feat: http2.connect fetch-backed ClientHttp2Session
100
+ - feat: WASI real impl via @bjorn3/browser_wasi_shim
101
+ - feat: diagnostics_channel real pub/sub + tracingChannel
102
+ - feat: trace_events real recorder with event buffer
103
+ - feat: worker_threads.Worker backed by real Web Worker + Blob URL
104
+ - feat: child_process exec/spawn via WebContainer when available
105
+ - feat: fs.watch real events via IDB snapshot diff polling
106
+ - feat: REPL command handling (.clear/.exit/.help/.load/.save/.editor) + multi-line balance detection
107
+ - feat: Buffer pool (Buffer.poolSize=8192) for small allocUnsafe
108
+ - feat: process.binding('util') selective exposure, execArgv, features
109
+ - feat: process.memoryUsage from performance.memory
110
+ - feat: http.Agent / https.Agent real fetch pool with maxSockets
111
+ - feat: Error.prepareStackTrace V8-hook via Object.defineProperty on Error.prototype.stack
112
+ - feat: window.__debug.node registry for runtime observability
113
+ - feat: net/tls stubs with clearer error messages
114
+
115
+ ## [unreleased] 2026-04-20 node parity pass 5
116
+ - feat: zlib sync (fflate via esm.sh /es2022 bundle) — gzipSync/gunzipSync/deflateSync/inflateSync
117
+ - feat: crypto.sign/verify + createSign/createVerify with PEM key import (RSA-SHA256 via webcrypto pkcs8/spki)
118
+ - feat: module resolution — full conditional exports (node/import/require/default/browser), subpath patterns (*), #internal imports map, type:module ESM detection
119
+ - feat: util.inspect — BigInt 'n' suffix, Symbol keys, colors:true ANSI, showHidden for non-enumerable
120
+ - feat: Error.captureStackTrace polyfill, process.execArgv from NODE_OPTIONS, --enable-source-maps flag, expanded allowedNodeEnvironmentFlags, process.features
121
+
122
+ ## [unreleased] 2026-04-20 node parity pass 3
123
+ - feat: crypto sha1/sha512/md5 pure-JS + hmac (RFC 2104) + pbkdf2Sync + randomBytes via Web Crypto — all byte-for-byte match with real node
124
+ - feat: util.inspect circular refs use node format '<ref *N> { ... [Circular *N] }' exactly
125
+ - feat: package.json exports field resolution + node_modules walk-up parent dirs
126
+ - feat: require('module') with builtinModules, createRequire, _resolveFilename, Module, wrap, wrapper
127
+ - feat: require throws Error with .code='MODULE_NOT_FOUND' and requireStack
128
+ - feat: fs.promises mirrors sync API; fs.watch FSWatcher stub
129
+ - feat: net/dgram throw descriptive errors on use (not silent stubs)
130
+ - feat: worker_threads throws descriptive; execSync throws with explanation
131
+ - feat: process.stdin.setRawMode no-op (inquirer compatibility)
132
+ - feat: globalThis.process/Buffer set during eval (real node globals)
133
+ - feat: __filename/__dirname injected into ESM preamble
134
+ - feat: REPL eval loop — input → try-expr-then-stmt → util.inspect result; .exit/.help/.clear commands; prompt '> ' during REPL
135
+ - new: shell-node-crypto.js (sha1/256/512/md5/hmac/pbkdf2), shell-node-resolve.js (exports/walk-up/module/fs.promises/net/dgram/worker_threads stubs)
136
+
137
+ ## [unreleased] 2026-04-20 node parity pass 2
138
+ - feat: util.inspect matches node format (braces-with-spaces, Map(N){k=>v}, arrays, circular, <Buffer ...>)
139
+ - feat: console.log/info/warn/error use util.format (printf-style %s/%d/%o)
140
+ - feat: crypto.createHash pure-JS sha256 — matches node hex output byte-for-byte
141
+ - feat: Buffer.write/compare/equals/indexOf/includes/subarray/readUIntXX/Buffer.compare/allocUnsafe
142
+ - feat: fs.rmSync/rmdirSync/accessSync/realpathSync
143
+ - feat: child_process.spawn/exec route through shell runPipeline, EventEmitter-style stdout/stderr/exit
144
+ - feat: http.request/http.get via fetch, return IncomingMessage-style with statusCode/headers/on(data,end)
145
+ - feat: process.execPath/argv0/title/memoryUsage/uptime/getuid/umask/release; env defaults PATH/HOME/USER/SHELL/TERM/LANG
146
+ - feat: ESM detection — code with import/export wrapped in Blob URL + dynamic import
147
+ - feat: unhandledrejection → lastExitCode=1 + node-style stack
148
+ - feat: stack trace trailer "Node.js v23.10.0" on error
149
+ - feat: .env loading at script start
150
+ - feat: node: prefixed specifiers (node:fs, node:path, etc.)
151
+ - feat: zlib.gzip/gunzip via pako (async, auto-loaded from esm.sh)
152
+ - new: shell-node-stdlib.js (util/crypto/zlib), shell-node-io.js (cp/http/proc env/ESM/stack/dotenv)
153
+
154
+ ## [unreleased] 2026-04-20 node/npm CLI parity
155
+ - feat: node reports v23.10.0 + full process.versions map (27 fields, matches real CLI)
156
+ - feat: npm reports 10.9.2, npm_lifecycle_event/npm_package_name/npm_package_version env injection, pre/post<script> lifecycle hooks
157
+ - feat: process.exit(n) throws NodeExit, propagates to ctx.lastExitCode
158
+ - feat: script errors set lastExitCode=1 with stack trace display
159
+ - feat: node reads stdin via pipe (echo x | node script.js) through proc.stdin._feed
160
+ - feat: require.resolve + require.cache for module introspection
161
+ - feat: npx builtin (npx cowsay hi)
162
+ - feat: node -h/--help, node -p fixed (stdout.write not console.log)
163
+ - refactor: extracted runNode + runNpmResult into shell-exec.js (shell.js stays <200L)
164
+
165
+ ## [unreleased] 2026-04-18 browser validation
166
+ - fix: require('express') returned instance not factory (MODULES wrapper called createExpress twice)
167
+ - fix: SW registration non-blocking — shell boots immediately, SW registers in background
168
+ - fix: splitTopLevel sep semantics — sep is preceding operator not following, fixes && || chain evaluation
169
+ - fix: lastExitCode-based lastOk — false builtin sets exit code 1 without throwing
170
+
171
+ ## [unreleased] 2026-04-18
172
+ - feat: full CLI overhaul — shell-parser.js (tokenize/expand/parsePipes/splitTopLevel/parseRedirects), shell-builtins.js (POSIX builtins: ls -la, rm -r, cp -r, grep -inH, cd -, history), shell-npm.js (install/uninstall/ls/run/init with package.json read/write), shell.js refactored to use modules; IDB_KEY bumped to thebird_fs_v4; defaults.json updated with all new files
1
173
  ## [Unreleased]
2
174
 
175
+ ### Added
176
+ - `docs/defaults.json`: bundled real project files (package.json, index.js, server.js, lib/*, lib/providers/*) so browser jsh has a working thebird source tree on boot — user can immediately run `npm install && node server.js` without needing to write files first
177
+ - `docs/shell.js`: `npm install` with no args reads cwd `package.json` dependencies + peerDependencies, installs all of them (multi-pkg via single command)
178
+ - `docs/shell-node.js`: `preloadAsyncPkgs` now walks the full require graph from entry file (BFS through relative requires) so transitive external package deps get loaded before sync require runs. Previously only scanned top-level code — server.js → ./index.js → @google/genai chain failed
179
+ - `docs/index.html`: callExpressRoute response object supports both Node http-style (writeHead/write/end/setHeader) and express-style (send/json/status). Request object now has `url`, `method`, async iterator for empty body — matches what `http.createServer` handlers expect
180
+
181
+ ### Changed
182
+ - `docs/terminal.js`: IDB_KEY bumped `thebird_fs_v2` → `thebird_fs_v3` to force refresh of browser fs cache (users with stale idb will re-fetch defaults.json with the real project files)
183
+
184
+ ### Added (prev)
185
+ - `docs/shell-node.js`: `http` and `https` core builtins — `http.createServer(handler)` registers wildcard route in `window.__debug.shell.httpHandlers[port]` (same mechanism as express.listen), so `node server.js` now works for servers that use raw `require('http')`
186
+ - `docs/shell-node.js`: `buffer`, `child_process`, `net`, `zlib`, `assert` builtin stubs so common Node scripts don't die on trivial requires
187
+ - `docs/shell-node.js`: `preloadAsyncPkgs(code)` scans source for `require('pkg')` calls, resolves each via dynamic `import(esm.sh/pkg)`, populates `pkgCache`. Synchronous `require()` then reads from cache — bridges Node CJS semantics to browser ESM loading
188
+ - `docs/shell-node-modules.js`: new file holding `createExpress`, `createHttp`, `createSqlite`, `createConsole`, `createProcess` factories (split out to keep shell-node.js under 200 lines)
189
+
190
+ ### Changed
191
+ - `docs/shell.js`: `npm install` supports multiple packages per invocation; writes an `await import(...)` stub to `node_modules/<pkg>/index.js` as a marker, real resolution happens via `preloadAsyncPkgs` in nodeEval
192
+ - `docs/shell-node.js`: external (non-relative, non-builtin) require throws clear `Cannot find module: X (run: npm install X)` instead of generic error
193
+ - `docs/shell-node-modules.js` createExpress: routes now store `{ path, fn }` where `fn` runs full middleware chain via `runFns`, matching `index.html` callExpressRoute's `match.fn(req, res)` expectation (was previously `{ path, fns }` which broke route invocation)
194
+
195
+ ### Fixed
196
+ - `docs/shell.js`: httpHandlers now returned on shell object instead of assigned to window.__debug.shell separately — terminal.js overwrote the debug object (which had httpHandlers) with the createShell() return value (which had none), making express routes invisible to index.html callExpressRoute(). Fix: remove internal window.__debug.shell assignment, include httpHandlers and all debug getters on the returned shell object so terminal.js assignment preserves the reference
197
+ - `test.js`: consolidate e2e-test.js coverage into test.js (express routing e2e + httpHandlers fix regression); delete e2e-test.js to enforce single-test-file policy
198
+
199
+ ### Fixed
200
+ - `docs/app.js`: Normalize message content format to Anthropic array structure `[{ type: 'text', text: '...' }]` to prevent double-conversion in streamGemini/streamOpenAI (was sending string content)
201
+ - `docs/agent-chat.js`: Add lastError tracking in window.__debug.agent for error visibility and debugging
202
+ - `docs/shell.js`: Expose onPreviewWrite callback on returned shell object for preview refresh integration
203
+ - `docs/terminal.js`: Add shell reference to window.__debug for tool access; reduce preview refresh debounce 5s → 1s for quicker feedback
204
+ - `docs/preview-sw.js`: Add missing service worker for preview iframe routing (handle EXPRESS_REQUEST messages from main thread)
205
+ - `test.js`: Create integration test suite validating bootstrap, defaults.json, tools, errors, observability structures
206
+
207
+ ### Changed
208
+ - `docs/defaults.json`: Split and optimized for Git constraints — reduced from 154.83 MB to 1.23 MB by including only 16 critical bootstrap files (app.js, agent-chat.js, terminal.js, shell.js, vendor/xterm-bundle.js, vendor/xstate.js, vendor/ui-libs.js, vendor/thebird-browser.js, etc.). Excludes large unused vendors (winterjs.wasm 46 MB, wasmer_js_bg.wasm 6.3 MB, rippleui.css 4.5 MB, sql-wasm.wasm 0.6 MB, acp-sdk.js 0.6 MB) not required for WebContainer bootstrap path.
209
+ - `docs/terminal.js`: Updated xterm.js theme with green foreground (#33ff33) to match Claude Code TUI aesthetic. Maintains AAA contrast ratio (14.61:1 on black background).
210
+
3
211
  ### Added
4
212
  - `docs/node-builtins.js`: Full Node.js module polyfills — path, fs (IDB-backed), events (EventEmitter), url, querystring, Buffer class with encoding support
5
213
  - `docs/shell-node.js`: Enhanced Node env — relative require, JSON require, per-file __dirname, process.stdout/stderr/nextTick/argv/hrtime, console.dir/table/time/assert/count, express with route params/middleware/static/json, os/util/crypto/stream modules
@@ -95,3 +303,12 @@
95
303
  ### Added
96
304
  - `docs/index.html`: Preview tab with iframe (`#preview-frame`), `switchTab` extended to dispatch over `['chat','term','preview']`.
97
305
  - `docs/terminal.js`: DEFAULT_FILES now includes `server.js` (HTTP server on port 3000, JSON status endpoint) and updated `index.js` (loads @anthropic-ai/sdk, hits server). Server auto-starts after `npm install`. `container.on('server-ready')` wires iframe src + `window.__debug.previewUrl`. Shell upgraded from `sh` loop to `jsh` with PTY resize. `window.__debug.srv` and `window.__debug.shell` live.
306
+
307
+
308
+ ## shell predictability fixes (2026-04-18)
309
+ - Pipe stdin passthrough: grep/sed detect piped content as first arg (stdinFirst pattern)
310
+ - Tokenizer: preserves \\n inside double-quotes so echo -e works correctly
311
+ - $? expansion, $() command substitution, inline var assignment (X=val cmd)
312
+ - echo -e with \\n \\t escape sequences
313
+ - New builtins: sed, sort, uniq, tr
314
+ - shell-builtins.js split into shell-builtins-text.js (both under 200L)
package/CLAUDE.md CHANGED
@@ -134,6 +134,20 @@ Run examples against real Gemini API to validate message translation.
134
134
  - Tool parameter types must be lowercase for Gemini API — `object`, `string`, `number` not `OBJECT`, `STRING`, `NUMBER`. Uppercase types fail schema validation.
135
135
  - agentGenerate passes raw Anthropic-format messages to streamGemini internally, which calls convertMessages. Do NOT pre-convert in app.js — double-conversion breaks tool schemas.
136
136
  - Anthropic format IS the canonical message format — do NOT add an intermediate transformation layer. Direct translation to provider-native formats is cleaner than another abstraction level.
137
+ - **shell.js httpHandlers visibility chain**: shell.js creates `httpHandlers = {}` and returns it on the shell object. terminal.js assigns `window.__debug.shell = shell`. shell-node.js mutates `window.__debug.shell.httpHandlers[port]` to register routes. index.html reads from `window.__debug.shell.httpHandlers`. All four modules reference the same object; httpHandlers MUST be returned on the shell object or the chain breaks.
138
+
139
+ - **Shell builtin pipe stdin detection**: When a builtin receives piped input (e.g. `echo hello | grep hello`), the pipe buffer arrives as `args[0]`. Builtins with mandatory leading args (grep: pattern, sed: expression) must detect `positional[0].includes('\n')` BEFORE extracting those args, then shift stdin off. `cat`/`wc`/`sort`/`uniq` use the same check. Single-line piped content always has `\n` because `echo` appends `\r\n`.
140
+ - **Tokenizer double-quote escape sequences**: Inside double-quotes, only `"`, `\`, `` ` ``, `$` are valid escape sequences. `\n` in double-quotes must be preserved as literal backslash-n for `echo -e` to process. Fix: when escape=true and quote==='"', re-emit backslash before char if char not in `"\\\`$`.- **Test policy**: Single test.js file (max 139L per constraint). e2e-test.js deleted. Validation via live API calls with exec:nodejs (browser automation unavailable in this environment).
141
+ - **ACP protocolVersion is a number**: Kilo Code's `kilo acp` server rejects string `'0.1'` with `-32602 Invalid params: expected number`. Use integer `1`. acp-stream.js:73 fixed.
142
+ - **Kilo ACP over WebSocket bridge (Windows)**: To consume Kilo Code as the page's ACP provider, bridge its stdio to WS. The npm shim `kilo.cmd` uses `stdio: "inherit"` which detaches from the bridge's pipes, so invoke the platform binary directly: `C:\Users\user\AppData\Roaming\npm\node_modules\@kilocode\cli\node_modules\@kilocode\cli-windows-x64\bin\kilo.exe acp`. Bridge command: `bunx -y stdio-to-ws "<abs-path>\kilo.exe acp" --port 3015`. Bunx cold start is 15–25s; wait for `[stdio-to-ws] WebSocket server listening`. Validated: initialize → result with `protocolVersion: 1, agentInfo: { name: 'Kilo', version: '7.2.0' }, authMethods: [{ id: 'kilo-login' }]`. Actual prompts require `kilo auth login` first.
143
+ - **Kilo ACP `session/new` requires `mcpServers` array**: Kilo's zod schema for `session/new` params requires `mcpServers` (array). Omitting it returns `-32602 Invalid params: expected array, received undefined`. Fix: pass `{ cwd, mcpServers: [] }` — see docs/acp-stream.js:74.
144
+ - **Kilo ACP `session/prompt` shape**: Kilo's schema is `{ sessionId, prompt: [...content blocks] }` — NOT `{ sessionId, message: { role, content: [...] } }`. Wrong shape returns `-32602 Invalid params: prompt expected array, received undefined`. Fix: send `prompt: [{ type:'text', text }]` directly — see docs/acp-stream.js:80.
145
+ - **Kilo ACP `session/update` notification shape**: Kilo emits `{ params: { sessionId, update: { sessionUpdate:'agent_message_chunk', content:{ type:'text', text } } } }` — singular `update`, and the discriminator is `sessionUpdate` not `type`. Iterating `updates[]` (plural) with `item.type==='message_chunk'` never matches real Kilo output. Fix: normalize to `update ? [update] : (updates||[])` and match `sessionUpdate==='agent_message_chunk'` with `content.type==='text'`. See docs/acp-stream.js:93.
146
+ - **Kilo ACP `kilo/*` free models require `kilo auth login`**: Built-in free routes (`kilo/x-ai/grok-code-fast-1:optimized:free`, `kilo/openrouter/free`, etc.) only work after interactive `kilo auth login` stores a credential in `~/.local/share/kilo/auth.json`. `kilo auth list` exposes `OPENAI_API_KEY`/`GEMINI_API_KEY` env vars as alternate auth sources, but those only apply to direct-provider models (`openai/*`, `google/*`) — not to `kilo/*` routes. With an invalid/missing GEMINI_API_KEY, `session/prompt` returns `stopReason:'end_turn'` with zero tokens and no text chunks — silent no-op, not an error. Any real end-to-end prompt validation requires a valid provider key or an authenticated Kilo account.
147
+ - **Kilo ACP `session/set_model` is supported**: Call `session/set_model` with `{ sessionId, modelId }` between `session/new` and `session/prompt` to override the default (`kilo/x-ai/grok-code-fast-1:optimized:free`). Validated with `modelId:'google/gemini-2.5-flash'` — response includes `_meta.opencode.modelId` confirming the switch.
148
+ - **Kilo free models DO work non-interactively without `kilo auth login`**: Earlier assumption was wrong. The bundled `@kilocode/kilo-gateway` provider (`providerID:'kilo'`, source:'custom', env:[KILO_API_KEY]) serves free routes (`x-ai/grok-code-fast-1:optimized:free`, `openrouter/free`, `kilo-auto/free`, etc.) anonymously over the built-in HTTP server. Witnessed via `kilo serve --port N` + `POST /session` + `POST /session/:id/message` with `{ parts:[{type:'text',text}], modelID:'x-ai/grok-code-fast-1:optimized:free', providerID:'kilo' }` → `status 200`, `finish:stop`, real text output in ~5s. Cost: 0. Reference: C:\dev\agentgui\lib\claude-runner-acp.js shows the identical flow working over ACP stdio — key detail is a 5-min (300000ms) timeout because the gm agent plugin (globally installed at `C:\Users\user\.config\kilo\plugins\gm-kilo.mjs`) does git ops on first prompt. gm system prompt is always injected (~18-19k input tokens regardless of agent param).
149
+ - **Kilo agent plugin overrides requested model**: Specifying `modelID:'x-ai/grok-code-fast-1:optimized:free'` on a prompt often gets silently routed to `openrouter/elephant-alpha` because the `gm` agent (auto-selected when `default_agent:'gm'` is in `opencode.json`) has its own model picker. Check `info.modelID` in the response to see what actually ran. To bypass gm routing, delete or rename `C:\Users\user\.config\kilo\opencode.json` and `kilocode.json` which set `default_agent:'gm'`.
150
+ - **Kilo `run` subcommand hangs, `serve` + HTTP works**: The `kilo run` CLI path currently blocks indefinitely after config load (even at DEBUG log level, no provider activity logged). Workaround: use `kilo serve --port N` and POST to `/session/:id/message` — same bundled provider, same free models, responds in seconds. Also applies to ACP stdio: `kilo acp` works but has long cold-start through the gm plugin, so any bridge must allow at least 5 minutes per prompt.
137
151
 
138
152
  ## Error Architecture
139
153
 
@@ -172,6 +186,8 @@ Run examples against real Gemini API to validate message translation.
172
186
  - `examples/`: Working examples using Anthropic SDK format
173
187
  - `wasi/cli.ts`: Deno streaming CLI — `deno run --allow-net --allow-env wasi/cli.ts [--model M] [--system S] <prompt>`
174
188
  - `deno.json`: tasks `cli` (run) and `cli:compile` (→ `dist/thebird` binary)
189
+ - `docs/shell-builtins.js`: FS/IO builtins (ls/cat/echo/cd/mkdir/rm/cp/mv/touch/head/tail/wc) — imports makeTextBuiltins
190
+ - `docs/shell-builtins-text.js`: Text-processing builtins (grep/sed/sort/uniq/tr) + env/export/clear/history/which/exit/true/false/printenv
175
191
 
176
192
  ## WebContainer Terminal in docs/
177
193
 
@@ -1,5 +1,5 @@
1
1
  import { streamGemini, streamOpenAI } from './vendor/thebird-browser.js';
2
- import { streamACP } from './acp-stream.js';
2
+ import { streamKiloHTTP } from './kilo-http-stream.js';
3
3
 
4
4
  function idbRead(path) {
5
5
  const snap = window.__debug.idbSnapshot;
@@ -91,8 +91,8 @@ function buildStream(provider) {
91
91
  if (provider.type === 'gemini') {
92
92
  return streamGemini({ model: provider.model, messages: provider.messages, tools: TOOLS, apiKey: provider.apiKey, maxOutputTokens: 8192 }).fullStream;
93
93
  }
94
- if (provider.type === 'acp') {
95
- return streamACP({ url: provider.baseUrl, model: provider.model, messages: provider.messages, tools: TOOLS, maxOutputTokens: 8192 });
94
+ if (provider.type === 'kilo') {
95
+ return streamKiloHTTP({ url: provider.baseUrl, model: provider.model, messages: provider.messages });
96
96
  }
97
97
  const url = (provider.baseUrl || '').replace(/\/$/, '') + '/chat/completions';
98
98
  return streamOpenAI({ url, apiKey: provider.apiKey, messages: provider.messages, model: provider.model, tools: TOOLS, maxOutputTokens: 8192 });
@@ -100,7 +100,7 @@ function buildStream(provider) {
100
100
 
101
101
  export async function agentGenerate(provider, messages, onChunk, onTool) {
102
102
  Object.assign(window.__debug = window.__debug || {}, {
103
- agent: { provider: provider.type, model: provider.model, active: true, lastTool: null },
103
+ agent: { provider: provider.type, model: provider.model, active: true, lastTool: null, lastError: null },
104
104
  });
105
105
  try {
106
106
  for await (const ev of buildStream({ ...provider, messages })) {
@@ -110,6 +110,9 @@ export async function agentGenerate(provider, messages, onChunk, onTool) {
110
110
  onTool(ev.toolName, ev.args);
111
111
  } else if (ev.type === 'error') throw ev.error;
112
112
  }
113
+ } catch (e) {
114
+ window.__debug.agent.lastError = { message: e.message, stack: e.stack, timestamp: Date.now() };
115
+ throw e;
113
116
  } finally {
114
117
  window.__debug.agent.active = false;
115
118
  }
package/docs/app.js CHANGED
@@ -11,7 +11,7 @@ const PROVIDERS = {
11
11
  mistral: { label: 'Mistral', baseUrl: 'https://api.mistral.ai/v1', keyPlaceholder: 'MISTRAL_API_KEY', models: ['mistral-large-latest', 'mistral-small-latest', 'codestral-latest'] },
12
12
  deepseek: { label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', keyPlaceholder: 'DEEPSEEK_API_KEY', models: ['deepseek-chat', 'deepseek-reasoner'] },
13
13
  cerebras: { label: 'Cerebras', baseUrl: 'https://api.cerebras.ai/v1', keyPlaceholder: 'CEREBRAS_API_KEY', models: ['gpt-oss-120b', 'llama3.1-8b'] },
14
- acp: { label: 'ACP Agent', baseUrl: 'ws://localhost:3000', keyPlaceholder: '(no key needed)', models: ['default'] },
14
+ kilo: { label: 'Kilo Code', baseUrl: 'http://localhost:4780', keyPlaceholder: '(no key needed)', models: ['x-ai/grok-code-fast-1:optimized:free', 'openrouter/free', 'kilo-auto/free'] },
15
15
  custom: { label: 'Custom (OpenAI-compat)', baseUrl: '', keyPlaceholder: 'API_KEY', models: [] },
16
16
  };
17
17
 
@@ -45,7 +45,8 @@ async function fetchModels(providerType, baseUrl, apiKey) {
45
45
  class BirdChat extends HTMLElement {
46
46
  constructor() {
47
47
  super();
48
- const savedProvider = localStorage.getItem('provider_type') || 'gemini';
48
+ const rawSaved = localStorage.getItem('provider_type') || 'gemini';
49
+ const savedProvider = PROVIDERS[rawSaved] ? rawSaved : (rawSaved === 'acp' || rawSaved === 'kilohttp' ? 'kilo' : 'gemini');
49
50
  const savedBaseUrl = localStorage.getItem('provider_base_url') || PROVIDERS[savedProvider]?.baseUrl || '';
50
51
  this.state = {
51
52
  messages: [], streaming: false,
@@ -107,11 +108,11 @@ class BirdChat extends HTMLElement {
107
108
  <div class="tui-toolbar">
108
109
  <label>provider:</label>
109
110
  <select class="tui-select" onchange=${e => this.setProvider(e.target.value)}>${provOpts}</select>
110
- ${(providerType === 'custom' || providerType === 'acp') ? html`
111
+ ${(providerType === 'custom' || providerType === 'kilo') ? html`
111
112
  <input type="text" class="tui-input" style="flex:1;min-width:140px"
112
- placeholder=${providerType === 'acp' ? 'ws://localhost:3000' : 'https://your-endpoint/v1'} value=${baseUrl}
113
+ placeholder=${providerType === 'kilo' ? 'http://localhost:4780' : 'https://your-endpoint/v1'} value=${baseUrl}
113
114
  onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />` : ''}
114
- ${providerType !== 'acp' ? html`<input id="api-key-input" type="password" class="tui-input" style="flex:1;min-width:120px"
115
+ ${providerType !== 'kilo' ? html`<input id="api-key-input" type="password" class="tui-input" style="flex:1;min-width:120px"
115
116
  placeholder=${provDef.keyPlaceholder} value=${apiKey}
116
117
  onchange=${e => {
117
118
  const v = e.target.value.trim();
@@ -151,12 +152,14 @@ class BirdChat extends HTMLElement {
151
152
  const text = input?.value.trim();
152
153
  if (!text || this.state.streaming) return;
153
154
  const { apiKey, model, providerType, baseUrl } = this.state;
154
- if (!apiKey && providerType !== 'acp') { this.setState({ status: 'Enter an API key above.' }); return; }
155
+ if (!apiKey && providerType !== 'kilo') { this.setState({ status: 'Enter an API key above.' }); return; }
155
156
  input.value = '';
156
157
  input.style.height = 'auto';
157
- const messages = [...this.state.messages, { role: 'user', content: text }];
158
- this.setState({ messages, streaming: true, status: '', streamingText: '' });
159
- const provider = { type: providerType, apiKey, model, baseUrl: providerType === 'gemini' ? '' : baseUrl };
158
+ const normalizedMessages = [...this.state.messages, { role: 'user', content: text }].map(m => ({
159
+ ...m, content: typeof m.content === 'string' ? [{ type: 'text', text: m.content }] : m.content
160
+ }));
161
+ this.setState({ messages: normalizedMessages, streaming: true, status: '', streamingText: '' });
162
+ const provider = { type: providerType, apiKey, model, baseUrl: providerType === 'gemini' ? '' : baseUrl, url: baseUrl };
160
163
  try {
161
164
  let full = '';
162
165
  const streamEl = document.createElement('div');
@@ -169,12 +172,12 @@ class BirdChat extends HTMLElement {
169
172
  wrap.appendChild(cursor);
170
173
  const list = this.querySelector('#msg-list');
171
174
  if (list) list.appendChild(wrap);
172
- await agentGenerate(provider, messages,
175
+ await agentGenerate(provider, normalizedMessages,
173
176
  chunk => { full += chunk; streamEl.textContent = full; const l = this.querySelector('#msg-list'); if (l) l.scrollTop = l.scrollHeight; },
174
177
  (name, args) => { full += `\n[tool: ${name}(${JSON.stringify(args)})]\n`; streamEl.textContent = full; }
175
178
  );
176
179
  wrap.remove();
177
- this.setState({ messages: [...messages, { role: 'assistant', content: full || '(empty)' }], streaming: false, streamingText: '' });
180
+ this.setState({ messages: [...normalizedMessages, { role: 'assistant', content: [{ type: 'text', text: full || '(empty)' }] }], streaming: false, streamingText: '' });
178
181
  const l2 = this.querySelector('#msg-list');
179
182
  if (l2) l2.scrollTop = l2.scrollHeight;
180
183
  } catch (err) {