loom-agent 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.env.example +25 -0
  2. package/CHANGELOG.md +402 -0
  3. package/LICENSE +21 -0
  4. package/LOOM.md +235 -0
  5. package/README.md +433 -0
  6. package/bin/loom-tui.js +43 -0
  7. package/bin/loom.js +44 -0
  8. package/docs/acp.md +151 -0
  9. package/docs/web.md +205 -0
  10. package/package.json +97 -0
  11. package/scripts/acp-smoke.js +146 -0
  12. package/src/acp/acp-server.js +287 -0
  13. package/src/config/provider-cmd.js +37 -0
  14. package/src/config/settings.js +164 -0
  15. package/src/core/agents.js +361 -0
  16. package/src/core/background-tasks.js +103 -0
  17. package/src/core/cli.js +579 -0
  18. package/src/core/custom-commands.js +70 -0
  19. package/src/core/errors.js +29 -0
  20. package/src/core/events.js +24 -0
  21. package/src/core/file-diffs.js +282 -0
  22. package/src/core/format.js +206 -0
  23. package/src/core/graph.js +257 -0
  24. package/src/core/hooks.js +82 -0
  25. package/src/core/lsp.js +385 -0
  26. package/src/core/memory.js +87 -0
  27. package/src/core/model-router.js +87 -0
  28. package/src/core/permissions.js +327 -0
  29. package/src/core/platform.js +33 -0
  30. package/src/core/plugin-cmd.js +380 -0
  31. package/src/core/restore.js +207 -0
  32. package/src/core/session-store.js +167 -0
  33. package/src/core/session.js +910 -0
  34. package/src/core/subagent-log.js +134 -0
  35. package/src/core/tokens.js +31 -0
  36. package/src/core/update.js +6 -0
  37. package/src/core/usage.js +166 -0
  38. package/src/index.js +41 -0
  39. package/src/mcp/mcp-client.js +201 -0
  40. package/src/mcp/mcp-manager.js +193 -0
  41. package/src/providers/anthropic.js +243 -0
  42. package/src/providers/google.js +29 -0
  43. package/src/providers/index.js +175 -0
  44. package/src/providers/local.js +27 -0
  45. package/src/providers/nvidia.js +85 -0
  46. package/src/providers/openai-compat.js +269 -0
  47. package/src/providers/openai.js +35 -0
  48. package/src/providers/openrouter.js +43 -0
  49. package/src/providers/registry.js +196 -0
  50. package/src/providers/tokenrouter.js +19 -0
  51. package/src/skills/skill-matcher.js +133 -0
  52. package/src/skills/skills-manager.js +213 -0
  53. package/src/tools/index.js +543 -0
  54. package/src/tui/App.tsx +1578 -0
  55. package/src/tui/components/BreadcrumbBar.tsx +34 -0
  56. package/src/tui/components/ChatArea.tsx +518 -0
  57. package/src/tui/components/InputBar.tsx +354 -0
  58. package/src/tui/components/MdText.tsx +105 -0
  59. package/src/tui/components/Modals.tsx +851 -0
  60. package/src/tui/components/PermissionPopup.tsx +264 -0
  61. package/src/tui/components/Sidebar.tsx +182 -0
  62. package/src/tui/components/SplashScreen.tsx +51 -0
  63. package/src/tui/components/SubagentPanel.tsx +217 -0
  64. package/src/tui/components/ToastOverlay.tsx +34 -0
  65. package/src/tui/keybinds.ts +318 -0
  66. package/src/tui/mcp-presets.ts +189 -0
  67. package/src/tui/md-render.ts +228 -0
  68. package/src/tui/store.ts +714 -0
  69. package/src/tui/suite-home.ts +20 -0
  70. package/src/tui/theme.ts +313 -0
  71. package/src/tui/themes.generated.ts +968 -0
  72. package/src/tui/tool-display.ts +176 -0
  73. package/src/tui/toolname.ts +60 -0
  74. package/src/tui/tui-config.ts +28 -0
  75. package/src/tui-open.tsx +51 -0
  76. package/src/web/attach.js +242 -0
  77. package/src/web/graph-view.html +262 -0
  78. package/src/web/index.html +824 -0
  79. package/src/web/web-server.js +470 -0
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ // Loom Code — TUI launcher. Prefers bun for the TSX/JSX pipeline; falls back to
3
+ // plain node for environments without bun (the TUI will tell the user to install
4
+ // bun first).
5
+ const { spawnSync } = require('child_process');
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+
10
+ function findBun() {
11
+ const candidates = [
12
+ // Common install locations
13
+ path.join(os.homedir(), '.bun', 'bin', 'bun.exe'),
14
+ path.join(os.homedir(), '.bun', 'bin', 'bun'),
15
+ '/usr/local/bin/bun',
16
+ '/opt/homebrew/bin/bun',
17
+ path.join(os.homedir(), 'bin', 'bun'),
18
+ path.join(os.homedir(), '.local', 'bin', 'bun'),
19
+ ];
20
+ for (const p of candidates) { try { if (fs.existsSync(p)) return p; } catch {} }
21
+ // PATH lookup (fast)
22
+ try {
23
+ const out = require('child_process').execSync('bun --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim();
24
+ if (out) return 'bun';
25
+ } catch {}
26
+ return null;
27
+ }
28
+
29
+ const entry = path.join(__dirname, '..', 'src', 'tui-open.tsx');
30
+ const bun = findBun();
31
+
32
+ if (bun) {
33
+ const result = spawnSync(bun, ['run', entry, ...process.argv.slice(2)], {
34
+ stdio: 'inherit',
35
+ cwd: process.cwd(),
36
+ env: process.env,
37
+ });
38
+ process.exit(result.status ?? 0);
39
+ }
40
+
41
+ console.log('Loom TUI requires bun (https://bun.sh) to run the TSX/JSX pipeline.');
42
+ console.log('Install bun first, then use: bun run ' + entry);
43
+ process.exit(1);
package/bin/loom.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // Loom Code — CLI entry point. Works with Node directly (runtime is plain JS).
3
+ require('dotenv').config();
4
+ const { main } = require('../src/core/cli');
5
+ const { updateCheck } = require('../src/core/update');
6
+ const { LoomError } = require('../src/core/errors');
7
+
8
+ process.title = 'loom-code';
9
+ process.on('uncaughtException', (err) => {
10
+ if (err instanceof LoomError) { console.error(`\n[Loom Error] ${err.message}`); process.exit(1); }
11
+ console.error(`\n[Unexpected Error] ${err.message}`);
12
+ if (process.env.LOOM_DEBUG) console.error(err.stack);
13
+ process.exit(1);
14
+ });
15
+ process.on('unhandledRejection', (reason) => {
16
+ console.error(`\n[Unhandled Promise]`, reason);
17
+ if (process.env.LOOM_DEBUG) console.error(reason?.stack);
18
+ process.exit(1);
19
+ });
20
+ (async () => {
21
+ const sub = process.argv.slice(2)[0];
22
+ if (sub === 'acp') {
23
+ // ACP subprocess mode: JSON-RPC over stdio for editor integration.
24
+ require('../src/acp/acp-server').main();
25
+ return;
26
+ }
27
+ if (sub === 'web') {
28
+ // Browser interface: HTTP server on 127.0.0.1, opens the browser.
29
+ require('../src/web/web-server').main();
30
+ return;
31
+ }
32
+ if (sub === 'attach') {
33
+ // Terminal client for a running `loom web` server.
34
+ require('../src/web/attach').main();
35
+ return;
36
+ }
37
+ try {
38
+ await updateCheck();
39
+ await main();
40
+ } catch (err) {
41
+ console.error(err?.message || err);
42
+ process.exit(1);
43
+ }
44
+ })();
package/docs/acp.md ADDED
@@ -0,0 +1,151 @@
1
+ # Editor integration via the Agent Client Protocol (ACP)
2
+
3
+ Loom can be driven as a **background coding agent by any editor that speaks the
4
+ [Agent Client Protocol (ACP)](https://agentclientprotocol.com)** — the same open
5
+ standard that powers opencode, Claude Code, and Gemini CLI integrations. You
6
+ don't need a Loom plugin or a Loom-owned editor: the editor spawns `loom acp`
7
+ as a subprocess, and the two sides exchange JSON-RPC messages over stdio.
8
+
9
+ > New in v1.3.0 — implemented in `src/acp/acp-server.js` (protocol tests in
10
+ > `src/acp/acp-server.test.js`).
11
+
12
+ ## How it works
13
+
14
+ - **Transport:** newline-delimited JSON-RPC 2.0 over stdio. The editor writes
15
+ requests to stdin; Loom answers on stdout and queues agent events that the
16
+ client pulls with `fetchAgentEvent`. Loom never writes protocol data to
17
+ stdout outside responses, so the stream stays parseable.
18
+ - **Launch:** `loom acp` starts the server and prints a readiness line to
19
+ **stderr** (never stdout): `[loom acp] started — provider: …, config: …`.
20
+ - **Lifecycle:** the editor sends `initialize` → `connect` (creates a session,
21
+ chooses `plan`/`chat`/`build` mode, optional instruction block) →
22
+ `sendChatRequest` → polls `fetchAgentEvent` → `cancelCurrentTask` when asked.
23
+
24
+ ## Prerequisites
25
+
26
+ - Loom installed (provides the `loom` command) — or run `bun src/acp/acp-server.js`
27
+ from a checkout.
28
+ - An API key for at least one provider (the same config used by the TUI;
29
+ provider is chosen automatically from `~/.loom/config.json` or `LOOM_CONFIG_DIR`).
30
+
31
+ ## Configure an editor
32
+
33
+ ### Zed
34
+
35
+ Zed hosts ACP agents in its Agent Panel as **External Agents**. Add Loom as a
36
+ custom agent in `~/.config/zed/settings.json`:
37
+
38
+ ```json
39
+ {
40
+ "agent_servers": {
41
+ "Loom Code": {
42
+ "type": "custom",
43
+ "command": "loom",
44
+ "args": ["acp"],
45
+ "env": {}
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ Then open the Command Palette and run **`agent: new thread`**, pick *Loom Code*,
52
+ and chat. To bind a key (open the Agent Panel / start a thread):
53
+
54
+ ```json
55
+ {
56
+ "bindings": {
57
+ "cmd-alt-o": ["agent::NewExternalAgentThread", {
58
+ "agent": { "custom": { "name": "Loom Code", "command": { "command": "loom", "args": ["acp"] } } }
59
+ }]
60
+ }
61
+ }
62
+ ```
63
+
64
+ Prefer pointing `command` at the full path to `loom` (`which loom`) so Zed can
65
+ find it.
66
+
67
+ ### JetBrains IDEs (IntelliJ / PyCharm / WebStorm …)
68
+
69
+ JetBrains co-developed ACP with Zed. In the JetBrains **Agent** integration,
70
+ set the ACP provider command to `loom acp` (same shape as the Zed entry above:
71
+ command `loom`, arguments `acp`).
72
+
73
+ ### Neovim
74
+
75
+ Point an ACP-capable completion plugin’s custom adapter at `loom acp`:
76
+
77
+ - **Avante.nvim** — add a provider with `custom_api = "acp"` and make the
78
+ command run `loom acp`.
79
+ - **CodeCompanion.nvim** — add an ACP adapter whose command is `loom acp`.
80
+
81
+ If your plugin needs the agent to reachable on `PATH`, install Loom globally
82
+ (`npm i -g .` / `npm link` from the repo, or publish) or use the absolute path.
83
+
84
+ ## Implemented ACP methods
85
+
86
+ | Method | Notes |
87
+ |-----------------------|-----------------------------------------------------------------------|
88
+ | `initialize` | Returns `protocolVersion: 1`, capabilities (`openai`, `customInstructions`), and the full OpenAI-style `toolSchemas` + `builtInTools`. |
89
+ | `connect` | Creates a session/task. Accepts `agentConfig.mode` (`plan`/`chat`/`build`) and `agentConfig.instructions` (injected into the agent prompt). Emits `session.updated: created`; returns `{ taskId }`. |
90
+ | `storeMessage` | Persists a user/assistant message into the session before/after requests. |
91
+ | `sendChatRequest` | Starts an async turn; returns `{ requestId }` immediately. Streams `agent.message` (text + reasoning), `tool.use`, `tool.result`, then `request.completed` / `request.error`. |
92
+ | `fetchAgentEvent` | Poll with `{ taskId, cursor }`; returns `{ events, cursor }`. Increment the cursor and keep polling until `request.completed`/`request.error`. |
93
+ | `cancelCurrentTask` | Interrupts the active request (maps to session interrupt / abort). |
94
+ | `updateAgentConfig` | Replaces the session instruction block. |
95
+ | `changeDefaultMode` | Switches mode between `plan`/`chat`/`build` mid-session. |
96
+ | `enableToolUse` / `disposeTool` | Accepted; all built-in + MCP tools are enabled by default (no gating). |
97
+
98
+ ## Events (retrieved via `fetchAgentEvent`)
99
+
100
+ `session.updated` (`created`/`expanded`), `agent.message` (`type: text` /
101
+ `type: reasoning`), `agent.message.completed`, `tool.use`, `tool.result`,
102
+ `request.completed`, `request.error`.
103
+
104
+ ## Try it without an editor
105
+
106
+ Pipe newline-delimited JSON into `loom acp` — initialize and create a task:
107
+
108
+ ```bash
109
+ printf '%s\n' \
110
+ '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \
111
+ '{"jsonrpc":"2.0","id":2,"method":"connect","params":{}}' \
112
+ | loom acp
113
+ ```
114
+
115
+ Or run the bundled self-test client against a real model turn:
116
+
117
+ ```bash
118
+ node scripts/acp-smoke.js # spawns `node bin/loom.js acp`, chats
119
+ node scripts/acp-smoke.js "explain this"' # custom prompt
120
+ node scripts/acp-smoke.js --cancel # demonstrate cancelCurrentTask
121
+ ```
122
+
123
+ `scripts/acp-smoke.js` requires a configured provider key and exits non-zero on
124
+ protocol error, so it doubles as a CI/diagnostic ping after any ACP change.
125
+
126
+ ## Publishing to the ACP Registry
127
+
128
+ To make Loom installable with **one click** in ACP-aware editors (like Zed’s
129
+ `zed: acp registry`, or the JetBrains ACP marketplace), publish a listing on the
130
+ [Agent Client Protocol registry](https://agentclientprotocol.com):
131
+
132
+ - **Command:** `loom acp` (runs the stdio server; no extra flags).
133
+ - **Runtime note:** the agent needs a provider API key — same setup as the TUI.
134
+ - **Repository/release:** link the Loom repo, README, and npm package.
135
+
136
+ A registry listing is pure metadata — the agent code is already ACP-compatible,
137
+ so no code changes are required to publish.
138
+
139
+ ## Troubleshooting
140
+
141
+ - **The editor gets no response / broken JSON** — make sure nothing writes to
142
+ stdout before/around the server. The readiness line goes to stderr by design;
143
+ external logging must go to stderr too, or set `LOOM_DEBUG` and check
144
+ `~/.loom/debug`.
145
+ - **`sendChatRequest` errors with “Task already has an active request”** — the
146
+ client must wait for `request.completed`/`request.error` (or cancel) before
147
+ the next request.
148
+ - **`fetchAgentEvent` returns no new events** — keep polling with the returned
149
+ `cursor`; events only flush on fetch.
150
+ - **“API key is invalid” / quota errors surface as `request.error`** — configure
151
+ the provider key exactly as you would for the TUI.
package/docs/web.md ADDED
@@ -0,0 +1,205 @@
1
+ # Web — running Loom in your browser
2
+
3
+ Loom can run as a **web application** in your browser, giving you the same
4
+ multi-provider coding agent without a terminal. It uses a small Node `http`
5
+ server (no framework, no build step) that serves a single-page UI and drives
6
+ the **same core Session loop** as the TUI and ACP — so any provider/model,
7
+ tool, MCP server, or saved session works identically.
8
+
9
+ ## Getting started
10
+
11
+ Start the web interface:
12
+
13
+ ```bash
14
+ loom web
15
+ ```
16
+
17
+ This starts a local server on `127.0.0.1`, picks a random available port,
18
+ prints the URL(s), and **opens your default browser** automatically.
19
+
20
+ > **Caution — security.** If `LOOM_SERVER_PASSWORD` is not set, the server is
21
+ > **unsecured**. That's fine for local single-user use but **must** be set
22
+ > before binding to a network interface (`--hostname 0.0.0.0` / `--mdns`).
23
+ > HTTP credentials and the `loom_token` cookie travel **unencrypted** — plain
24
+ > HTTP is only safe on a trusted network; for access from anything else, use a
25
+ > VPN or a TLS-terminating reverse proxy.
26
+
27
+ > **Windows.** `loom web` works from PowerShell, but if you want full terminal
28
+ > parity run it from WSL — same advice opencode gives for filesystem access.
29
+
30
+ ## Configuration
31
+
32
+ You can configure the server with **command-line flags** or the `server` block
33
+ in your config (`~/.loom/config.json`). CLI flags take precedence.
34
+
35
+ ### Port
36
+
37
+ By default Loom picks an available port. Pin one with:
38
+
39
+ ```bash
40
+ loom web --port 4096
41
+ ```
42
+
43
+ ### Hostname
44
+
45
+ By default the server binds to `127.0.0.1` (localhost only). Make it reachable
46
+ on your network:
47
+
48
+ ```bash
49
+ loom web --hostname 0.0.0.0
50
+ ```
51
+
52
+ With `0.0.0.0`, Loom prints both local and network addresses, e.g.:
53
+
54
+ ```
55
+ Local access: http://localhost:4096
56
+ Network access: http://192.168.1.100:4096
57
+ ```
58
+
59
+ ### mDNS
60
+
61
+ Advertise the server on the local network (sets hostname to `0.0.0.0` and
62
+ publishes it as `loom.local` via [bonjour-service](https://www.npmjs.com/package/bonjour-service)):
63
+
64
+ ```bash
65
+ loom web --mdns
66
+ ```
67
+
68
+ Run multiple instances on the same network with a custom domain name:
69
+
70
+ ```bash
71
+ loom web --mdns --mdns-domain myproject.local
72
+ ```
73
+
74
+ ### CORS
75
+
76
+ Allow additional origins for custom frontends:
77
+
78
+ ```bash
79
+ loom web --cors https://example.com # one origin
80
+ loom web --cors https://a.com,https://b.com # several
81
+ loom web --cors '*' # any
82
+ ```
83
+
84
+ ### Authentication
85
+
86
+ Protect access with a password environment variable:
87
+
88
+ ```bash
89
+ LOOM_SERVER_PASSWORD=secret loom web
90
+ ```
91
+
92
+ > Setting the password via the command line (or passing `--password` to
93
+ > `loom attach`) can leave the literal value in shell history and process
94
+ > metadata — prefer the `LOOM_SERVER_PASSWORD` environment variable (or a
95
+ > `.env` file) where possible.
96
+
97
+ The username defaults to `loom` and can be changed with
98
+ `LOOM_SERVER_USERNAME`. When a password is set, the browser shows a login
99
+ screen; API endpoints return `401` until you log in.
100
+
101
+ ### Skip the auto-open
102
+
103
+ ```bash
104
+ loom web --no-open
105
+ ```
106
+
107
+ ## Using the web interface
108
+
109
+ Once started, the homepage lists your **saved sessions** (from
110
+ `~/.loom/sessions/`), showing provider, model, and message count. Click a
111
+ session to view its transcript; **Send a message** to continue it (or pick
112
+ **New chat** to start fresh). Responses stream live (Server-Sent Events) with
113
+ inline tool-use and reasoning.
114
+
115
+ ### See Servers
116
+
117
+ Click **See Servers** in the header to view your configured MCP servers and
118
+ their enabled/disabled status (the same `/mcp` view the TUI shows).
119
+
120
+ ## Attaching a terminal
121
+
122
+ You can attach a terminal client to a running web server — the two share the
123
+ same sessions and state:
124
+
125
+ ```bash
126
+ # Terminal A — start the web server
127
+ loom web --port 4096
128
+
129
+ # Terminal B — attach to it
130
+ loom attach http://localhost:4096
131
+ ```
132
+
133
+ `loom attach` is a **line-mode** terminal client (the SolidJS OpenTUI runs
134
+ in-process today and isn't rewired to a remote server). It lets you:
135
+
136
+ - **pick** or **create** a session (shares the server's session list),
137
+ - print a session's transcript,
138
+ - chat with live streaming, and
139
+ - send **Ctrl+C** to cancel the current request.
140
+
141
+ For password-protected servers:
142
+
143
+ ```bash
144
+ LOOM_SERVER_PASSWORD=secret loom attach http://localhost:4096
145
+ # or inline:
146
+ loom attach http://localhost:4096 --username loom --password secret
147
+ ```
148
+
149
+ Resume a specific session without prompting:
150
+
151
+ ```bash
152
+ loom attach http://localhost:4096 --session web-abc123
153
+ ```
154
+
155
+ ## Config file
156
+
157
+ You can put the same server settings in your `~/.loom/config.json` under a
158
+ `server` key:
159
+
160
+ ```json
161
+ {
162
+ "server": {
163
+ "port": 4096,
164
+ "hostname": "0.0.0.0",
165
+ "mdns": true,
166
+ "cors": ["https://example.com"]
167
+ }
168
+ }
169
+ ```
170
+
171
+ **Command-line flags take precedence** over config-file settings.
172
+
173
+ ## API (for custom clients)
174
+
175
+ The UI talks to a small JSON API over the same HTTP server — feel free to use
176
+ it from your own client or script. (Set `LOOM_SERVER_PASSWORD` and send
177
+ `Cookie: loom_token=…` to authenticate; get the token from `POST /api/auth`.)
178
+
179
+ | Endpoint | Method | Notes |
180
+ |---|---|---|
181
+ | `GET /api/health` | GET | `{ ok: true }` |
182
+ | `GET /api/auth` | GET | `{ required, username }` |
183
+ | `POST /api/auth` | POST | `{ username, password }` → sets `loom_token` cookie |
184
+ | `GET /api/sessions` | GET | List saved sessions |
185
+ | `GET /api/sessions/:id` | GET | Load a session transcript |
186
+ | `POST /api/sessions` | POST | `{ mode }` → `{ id, mode }` (new web session) |
187
+ | `POST /api/chat` | POST | `{ id?, message, mode? }` → `text/event-stream` of `{type, …}` |
188
+ | `POST /api/cancel` | POST | `{ id }` interrupts the active request |
189
+ | `GET /api/servers` | GET | MCP server list + status |
190
+ | `GET /api/config` | GET | Provider/model/version banner data |
191
+
192
+ SSE event `type`s: `delta`, `reasoning`, `tool.use`, `tool.result`,
193
+ `message.completed`, `request.completed`, `request.error`, `session.updated`,
194
+ `done`.
195
+
196
+ ## Troubleshooting
197
+
198
+ - **"Authentication required"** — set `LOOM_SERVER_PASSWORD` (and optionally
199
+ `LOOM_SERVER_USERNAME`) before starting the server.
200
+ - **Tail of `--cors`** — separate multiple origins with a comma: `--cors a,b`.
201
+ - **mDNS not advertised on Windows** — `bonjour-service` is pure JS but mDNS
202
+ is best-effort on the OS; if it fails, an error is printed and the HTTP
203
+ server still works at the printed URLs.
204
+ - **"session is busy" from a client** — you must wait for `request.completed`
205
+ / `request.error` (or call `/api/cancel`) before sending the next message.
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "loom-agent",
3
+ "version": "1.2.0",
4
+ "repository": { "type": "git", "url": "git+https://github.com/toshalkumbhar8979-design/loomcode.git" },
5
+ "homepage": "https://github.com/toshalkumbhar8979-design/loomcode#readme",
6
+ "bugs": { "url": "https://github.com/toshalkumbhar8979-design/loomcode/issues" },
7
+ "description": "Loom Code — AI-powered coding agent for the terminal. Multi-provider support including NVIDIA. OpenTUI interface.",
8
+ "main": "src/index.js",
9
+ "bin": {
10
+ "loom": "bin/loom.js",
11
+ "loom-tui": "bin/loom-tui.js"
12
+ },
13
+ "scripts": {
14
+ "start": "node src/index.js",
15
+ "dev": "node --watch src/index.js",
16
+ "setup": "node src/setup.js",
17
+ "test": "bun run src/tui/test-interactive.tsx && bun run test:unit && bun run lint:core",
18
+ "test:unit": "bun test src/core/agents.test.js src/core/hooks.test.js src/core/custom-commands.test.js src/core/background-tasks.test.js src/core/memory.test.js src/core/subagent-log.test.js src/core/session.test.js src/tools/index.test.js src/providers/providers.test.js src/providers/caching.test.js src/providers/caching-thinking.test.js src/providers/registry.test.js src/mcp/mcp-client.test.js src/mcp/mcp-manager.test.js src/core/session-store.test.js src/skills/skills-manager.test.js src/core/usage.test.js src/core/permissions.test.js src/core/format.test.js src/acp/acp-server.test.js src/web/web-server.test.js src/tui/keybinds.test.ts",
19
+ "lint:core": "node node_modules/typescript/bin/tsc -p tsconfig.core.json",
20
+ "smoke:acp": "node scripts/acp-smoke.js",
21
+ "tui": "node bin/loom-tui.js",
22
+ "tui:win": "\"%USERPROFILE%\\..\\bun\\bin\\bun.exe\" run src/tui-open.tsx",
23
+ "prepublishOnly": "npm test",
24
+ "lint": "tsc --noEmit"
25
+ },
26
+ "dependencies": {
27
+ "@anthropic-ai/sdk": "^0.37.0",
28
+ "@opentui/core": "^0.5.1",
29
+ "@opentui/solid": "^0.5.1",
30
+ "@shikijs/themes": "4.4.3",
31
+ "bonjour-service": "^1.4.4",
32
+ "boxen": "^8.0.0",
33
+ "chalk": "^5.3.0",
34
+ "cli-spinners": "^3.2.0",
35
+ "commander": "^12.1.0",
36
+ "dayjs": "^1.11.0",
37
+ "diff": "^7.0.0",
38
+ "dotenv": "^16.4.0",
39
+ "glob": "^11.0.0",
40
+ "inquirer": "^12.1.0",
41
+ "openai": "^4.73.0",
42
+ "ora": "^8.0.0",
43
+ "shell-quote": "^1.8.1",
44
+ "solid-js": "^1.9.14",
45
+ "strip-ansi": "^7.1.0",
46
+ "tiktoken": "^1.0.22",
47
+ "uuid": "^11.0.0",
48
+ "wrap-ansi": "^9.0.0"
49
+ },
50
+ "optionalDependencies": {
51
+ "@opentui/core-win32-x64": "^0.5.1",
52
+ "@opentui/core-darwin-arm64": "^0.5.1",
53
+ "@opentui/core-darwin-x64": "^0.5.1",
54
+ "@opentui/core-linux-x64": "^0.5.1",
55
+ "@opentui/core-linux-arm64": "^0.5.1"
56
+ },
57
+ "devDependencies": {
58
+ "@types/bun": "^1.2.0",
59
+ "typescript": "^5.7.3"
60
+ },
61
+ "files": [
62
+ "bin/loom.js",
63
+ "bin/loom-tui.js",
64
+ "src/**/*.js",
65
+ "src/**/*.tsx",
66
+ "src/**/*.ts",
67
+ "!src/**/test-*",
68
+ "!src/**/*.test.*",
69
+ "!src/test-loop.js",
70
+ "!src/test-tools.js",
71
+ "README.md",
72
+ "CHANGELOG.md",
73
+ "LICENSE",
74
+ ".env.example",
75
+ "LOOM.md",
76
+ "docs/acp.md",
77
+ "scripts/acp-smoke.js",
78
+ "docs/web.md",
79
+ "src/web/index.html",
80
+ "src/web/graph-view.html"
81
+ ],
82
+ "keywords": [
83
+ "ai",
84
+ "cli",
85
+ "coding-agent",
86
+ "loom-code",
87
+ "anthropic",
88
+ "openai",
89
+ "nvidia",
90
+ "terminal",
91
+ "opentui"
92
+ ],
93
+ "engines": {
94
+ "node": ">=18.0.0",
95
+ "bun": ">=1.0.0"
96
+ }
97
+ }
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ // ACP smoke-test client — drives `loom acp` the way an editor would and prints
3
+ // the resulting event stream. Verifies the protocol end-to-end (initialize ->
4
+ // connect -> sendChatRequest -> fetchAgentEvent -> cancel/complete).
5
+ //
6
+ // node scripts/acp-smoke.js [prompt] [--cancel] [--mode plan|chat|build]
7
+ //
8
+ // Requires a configured provider key (same as the TUI). Exits 0 on a clean
9
+ // request.completed, non-zero on protocol error / request.error / timeout.
10
+ 'use strict';
11
+
12
+ const { spawn } = require('child_process');
13
+ const readline = require('readline');
14
+ const path = require('path');
15
+
16
+ const args = process.argv.slice(2);
17
+ const cancel = args.includes('--cancel');
18
+ const modeIdx = args.indexOf('--mode');
19
+ const mode = modeIdx !== -1 && args[modeIdx + 1] ? args[modeIdx + 1] : 'build';
20
+ const prompt = args.filter((a) => a !== '--cancel' && a !== '--mode' && (modeIdx === -1 || a !== args[modeIdx + 1]))[0]
21
+ || 'Say hello, then list up to five files in this repository.';
22
+
23
+ const root = path.resolve(__dirname, '..');
24
+ const child = spawn(process.execPath, [path.join('bin', 'loom.js'), 'acp'], {
25
+ cwd: root,
26
+ stdio: ['pipe', 'pipe', 'pipe'],
27
+ env: { ...process.env, LOOM_MCP_NO_WARM: '1', LOOM_MEM_AUTO: '0' },
28
+ });
29
+
30
+ const pending = new Map();
31
+ let seq = 0;
32
+ let stderrBuf = '';
33
+ let sawEvent = false;
34
+
35
+ child.stderr.on('data', (d) => { stderrBuf += d.toString(); });
36
+ readline.createInterface({ input: child.stdout }).on('line', (line) => {
37
+ const trimmed = String(line).trim();
38
+ if (!trimmed) return;
39
+ let msg;
40
+ try { msg = JSON.parse(trimmed); } catch { return; }
41
+ if (msg.id != null && pending.has(msg.id)) {
42
+ const h = pending.get(msg.id);
43
+ pending.delete(msg.id);
44
+ clearTimeout(h.t);
45
+ h.resolve(msg);
46
+ }
47
+ });
48
+ child.on('error', (e) => { console.error('[spawn error]', e.message); process.exit(1); });
49
+ child.on('exit', (code) => { if (pending.size && !finishing) { console.error('[server exited early] code=' + code + ' stderr=' + stderrBuf.slice(-400)); process.exit(1); } });
50
+
51
+ function send(method, params) {
52
+ return new Promise((resolve, reject) => {
53
+ const rid = ++seq;
54
+ const t = setTimeout(() => { pending.delete(rid); reject(new Error('timeout waiting for ' + method)); }, 30000);
55
+ pending.set(rid, { t, resolve });
56
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: rid, method, params: params || {} }) + '\n');
57
+ });
58
+ }
59
+
60
+ let finishing = false;
61
+ function finish(code) {
62
+ finishing = true;
63
+ try { child.stdin.end(); } catch {}
64
+ setTimeout(() => { try { child.kill(); } catch {} process.exit(code); }, 100).unref();
65
+ }
66
+
67
+ function show(e) {
68
+ sawEvent = true;
69
+ switch (e.event) {
70
+ case 'agent.message':
71
+ console.log(' ' + (e.content && e.content.type === 'reasoning' ? '(reasoning) ' : '') + (e.content && e.content.content != null ? String(e.content.content) : ''));
72
+ break;
73
+ case 'tool.use':
74
+ console.log('[tool.use] ' + e.toolName + ' ' + safeJSON(e.input));
75
+ break;
76
+ case 'tool.result': {
77
+ const r = e.result;
78
+ const s = typeof r === 'string' ? r : safeJSON(r);
79
+ console.log('[tool.result] ' + e.toolName + ' ' + s.slice(0, 160) + (s.length > 160 ? '…' : ''));
80
+ break;
81
+ }
82
+ case 'request.completed':
83
+ console.log('[request.completed] ' + (e.response && e.response.text ? String(e.response.text) : ''));
84
+ break;
85
+ case 'request.error':
86
+ console.log('[request.error] ' + (e.message || ''));
87
+ break;
88
+ case 'session.updated':
89
+ console.log('[session.updated:' + e.type + ']');
90
+ break;
91
+ default:
92
+ console.log('[' + e.event + ']');
93
+ }
94
+ }
95
+
96
+ function safeJSON(v) {
97
+ if (v == null) return String(v);
98
+ if (typeof v !== 'object') return String(v);
99
+ try { const s = JSON.stringify(v); return s && s.length > 400 ? s.slice(0, 400) + '…' : s; } catch { return String(v); }
100
+ }
101
+
102
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
103
+
104
+ (async () => {
105
+ const init = await send('initialize');
106
+ if (init.error) throw new Error('initialize failed: ' + JSON.stringify(init.error) + '\nstderr: ' + stderrBuf.slice(-300));
107
+ console.log('provider ok — protocolVersion ' + init.result.protocolVersion + ', tools ' + init.result.toolSchemas.length + ', builtin ' + init.result.agentConfig.builtInTools.length);
108
+
109
+ const conn = await send('connect', { agentConfig: { mode, instructions: 'You are connected to a smoke-test client. Keep replies brief.' } });
110
+ if (conn.error) throw new Error('connect failed: ' + JSON.stringify(conn.error));
111
+ const taskId = conn.result.taskId;
112
+
113
+ const req = await send('sendChatRequest', { taskId, message: { content: prompt } });
114
+ if (req.error) throw new Error('sendChatRequest failed: ' + JSON.stringify(req.error));
115
+ console.log('prompt: ' + prompt);
116
+ console.log('mode: ' + mode + (cancel ? ' (cancel demo)' : ''));
117
+
118
+ let cursor = 0;
119
+ let state = 'running';
120
+ const cancelTimer = cancel ? setTimeout(() => {
121
+ if (state === 'running') { state = 'cancelling'; console.log('[cancelling]'); send('cancelCurrentTask', { taskId }).catch(() => {}); }
122
+ }, 3000) : null;
123
+
124
+ const started = Date.now();
125
+ while (state === 'running' || state === 'cancelling') {
126
+ if (Date.now() - started > 120000) { console.error('[timeout] no terminal event within 120s'); finish(1); return; }
127
+ const resp = await send('fetchAgentEvent', { taskId, cursor });
128
+ if (resp.error) throw new Error('fetchAgentEvent failed: ' + JSON.stringify(resp.error));
129
+ for (const e of resp.result.events) show(e);
130
+ cursor = resp.result.cursor;
131
+ const terminal = resp.result.events.find((e) => e.event === 'request.completed' || e.event === 'request.error');
132
+ if (terminal) {
133
+ const ok = terminal.event === 'request.completed' || (cancel && /interrupt/i.test(terminal.message || ''));
134
+ console.log(ok ? '[PASS]' : '[FAIL] request ended with ' + terminal.event + (terminal.message ? ': ' + terminal.message : ''));
135
+ if (cancelTimer) clearTimeout(cancelTimer);
136
+ finish(ok ? 0 : 1);
137
+ return;
138
+ }
139
+ await sleep(120);
140
+ }
141
+ if (cancelTimer) clearTimeout(cancelTimer);
142
+ finish(sawEvent ? 0 : 1);
143
+ })().catch((e) => {
144
+ console.error('[smoke error]', e.message);
145
+ finish(1);
146
+ });