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.
- package/.env.example +25 -0
- package/CHANGELOG.md +402 -0
- package/LICENSE +21 -0
- package/LOOM.md +235 -0
- package/README.md +433 -0
- package/bin/loom-tui.js +43 -0
- package/bin/loom.js +44 -0
- package/docs/acp.md +151 -0
- package/docs/web.md +205 -0
- package/package.json +97 -0
- package/scripts/acp-smoke.js +146 -0
- package/src/acp/acp-server.js +287 -0
- package/src/config/provider-cmd.js +37 -0
- package/src/config/settings.js +164 -0
- package/src/core/agents.js +361 -0
- package/src/core/background-tasks.js +103 -0
- package/src/core/cli.js +579 -0
- package/src/core/custom-commands.js +70 -0
- package/src/core/errors.js +29 -0
- package/src/core/events.js +24 -0
- package/src/core/file-diffs.js +282 -0
- package/src/core/format.js +206 -0
- package/src/core/graph.js +257 -0
- package/src/core/hooks.js +82 -0
- package/src/core/lsp.js +385 -0
- package/src/core/memory.js +87 -0
- package/src/core/model-router.js +87 -0
- package/src/core/permissions.js +327 -0
- package/src/core/platform.js +33 -0
- package/src/core/plugin-cmd.js +380 -0
- package/src/core/restore.js +207 -0
- package/src/core/session-store.js +167 -0
- package/src/core/session.js +910 -0
- package/src/core/subagent-log.js +134 -0
- package/src/core/tokens.js +31 -0
- package/src/core/update.js +6 -0
- package/src/core/usage.js +166 -0
- package/src/index.js +41 -0
- package/src/mcp/mcp-client.js +201 -0
- package/src/mcp/mcp-manager.js +193 -0
- package/src/providers/anthropic.js +243 -0
- package/src/providers/google.js +29 -0
- package/src/providers/index.js +175 -0
- package/src/providers/local.js +27 -0
- package/src/providers/nvidia.js +85 -0
- package/src/providers/openai-compat.js +269 -0
- package/src/providers/openai.js +35 -0
- package/src/providers/openrouter.js +43 -0
- package/src/providers/registry.js +196 -0
- package/src/providers/tokenrouter.js +19 -0
- package/src/skills/skill-matcher.js +133 -0
- package/src/skills/skills-manager.js +213 -0
- package/src/tools/index.js +543 -0
- package/src/tui/App.tsx +1578 -0
- package/src/tui/components/BreadcrumbBar.tsx +34 -0
- package/src/tui/components/ChatArea.tsx +518 -0
- package/src/tui/components/InputBar.tsx +354 -0
- package/src/tui/components/MdText.tsx +105 -0
- package/src/tui/components/Modals.tsx +851 -0
- package/src/tui/components/PermissionPopup.tsx +264 -0
- package/src/tui/components/Sidebar.tsx +182 -0
- package/src/tui/components/SplashScreen.tsx +51 -0
- package/src/tui/components/SubagentPanel.tsx +217 -0
- package/src/tui/components/ToastOverlay.tsx +34 -0
- package/src/tui/keybinds.ts +318 -0
- package/src/tui/mcp-presets.ts +189 -0
- package/src/tui/md-render.ts +228 -0
- package/src/tui/store.ts +714 -0
- package/src/tui/suite-home.ts +20 -0
- package/src/tui/theme.ts +313 -0
- package/src/tui/themes.generated.ts +968 -0
- package/src/tui/tool-display.ts +176 -0
- package/src/tui/toolname.ts +60 -0
- package/src/tui/tui-config.ts +28 -0
- package/src/tui-open.tsx +51 -0
- package/src/web/attach.js +242 -0
- package/src/web/graph-view.html +262 -0
- package/src/web/index.html +824 -0
- package/src/web/web-server.js +470 -0
package/bin/loom-tui.js
ADDED
|
@@ -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
|
+
});
|