bots-garden/mini-mepublic Fork
⑂ v1.0.4
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. a8d612a · on v1.0.4 · k33g · 13d ago
README.en.md · 86 lines · 5.0 KBmarkdown
Blame HistoryOpen raw

mini-me from Node.js: minime.js

Version française

A Node.js client that uses mini-me as a library by driving mm -acp over ACP (ticket 167). One file, minime.js, no dependency (Node ≥ 20: child_process and readline). See ../README.en.md for the ACP messages behind it.

import { Agent } from "./minime.js";

const agent = await Agent.start({ config: "agent.yaml" });

const { text } = await agent.complete("Explain this repository");      // not streamed

for await (const ev of agent.stream("And the tests?")) {               // streamed
  if (ev.kind === "text") process.stdout.write(ev.text);
}

const ac = new AbortController();
setTimeout(() => ac.abort(), 3000);
await agent.complete("A long task", { signal: ac.signal });           // cancelled

await agent.close();

Run the examples

llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
llmman serve

cd demo.acp.sdk/nodejs
node examples/complete.js      # not streamed
node examples/stream.js        # streamed, with the tool calls
node examples/cancel.js        # cancelled after 3 seconds
node examples/permission.js    # a command refused, then allowed

mm must be on the PATH, or named by MM_BIN. The examples use agent.yaml (Qwen3.5 4B on llmman, bash only, yolo: false); any agent file works.

The API

await Agent.start({ config, cwd?, mm?, onPermission?, onStderr? }) starts mm -acp -config <config> (made absolute), then initialize and session/new {cwd}. mm defaults to $MM_BIN, then mm.
await agent.complete(prompt, { signal?, onEvent? }) one turn; resolves to { text, stopReason, usage } once it is over. onEvent sees every event as it comes.
agent.stream(prompt, { signal? }) one turn as an async iterator of events; the last one is done. Leaving the loop early cancels the turn.
signal (an AbortSignal) aborting it sends session/cancel; the turn ends with stopReason: "cancelled" and the text so far.
onPermission(call) asked when the agent wants to run a tool that needs a confirmation (yolo: false). Return true (allow once), false (reject once) or an ACP optionId ("allow_always", "never"). Without it, every such call is refused.
onStderr(line) mm's stderr (banner, warnings, [acp] lines). Otherwise it is read and dropped; its last line is added to the error when mm exits.
await agent.reset() a new session on the same process: the conversation starts over.
await agent.close() closes mm's stdin and waits for it to exit.
agent.sessionId the ACP session id.

The events: {kind: "text", text}, {kind: "thinking", text}, {kind: "tool_call", id, title, toolKind, input}, {kind: "tool_result", id, status, output}, {kind: "done", stopReason, usage, text}.

Rules worth knowing:

  • One Agent = one mm process = one conversation. Calls continue it, as in an editor. One turn at a time per Agent (a second one throws already running); start several Agents to work in parallel.
  • A turn mm ends with an error rejects with mm's own words, e.g. minime: Internal error (-32603): {"error":"the model repeated the same bash call 6 times…"}.
  • A cancelled question is dropped from the conversation by mm: the next question does not know about it.
  • The client advertises no file-system, terminal or form capability: mm needs none, and its ask_user tool is not offered.

Tests

npm test        # node --test test/minime.test.js — 11 tests, no model, no mm

test/fake-mm.js plays mm's side of ACP with the message shapes captured from mm v1.0.1: answers, a tool call with its permission, a slow turn to cancel, a crash. Not a bare node --test: it would also run fake-mm.js as a test and wait forever.

Measured (2026-10-26, llmman on the author's host, mm v1.0.1 built from the working tree)

Example Result
complete.js one-sentence answer, end_turn, 663 tokens, 2.9 s
stream.js find examples -name "*.js" | wc -l allowed by the callback (read-only), output 4, answer streamed chunk by chunk, end_turn
cancel.js story cut after 3.0 s (cancelled, 383 characters kept); the next question on the same Agent answered at once — without knowing the story, which mm dropped
permission.js pwd refused: the model invented an answer (/root); allowed: the real directory. A refusal does not stop a small model from guessing.

Seen in three runs before the callback allowed find: the model retried the same refused command until mm's loop detector stopped the turn (6 identical calls); stream() rejected with that message — hence the try/catch in stream.js.

Not done

  • The official ACP TypeScript library was not tried (no network in the sandbox where this was written): raw JSON-RPC keeps the client dependency-free.
  • Not a published npm package: the mm binary would have to ship with it, per platform.
  • session/load / session/resume (stored sessions), images and file attachments in prompts, ask_user forms: not exposed.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# mini-me from Node.js: `minime.js`

*[Version française](README.fr.md)*

A Node.js client that uses mini-me as a library by driving `mm -acp` over ACP (ticket 167). One file, `minime.js`, no dependency (Node ≥ 20: `child_process` and `readline`). See [`../README.en.md`](../README.en.md) for the ACP messages behind it.

```js
import { Agent } from "./minime.js";

const agent = await Agent.start({ config: "agent.yaml" });

const { text } = await agent.complete("Explain this repository");      // not streamed

for await (const ev of agent.stream("And the tests?")) {               // streamed
  if (ev.kind === "text") process.stdout.write(ev.text);
}

const ac = new AbortController();
setTimeout(() => ac.abort(), 3000);
await agent.complete("A long task", { signal: ac.signal });           // cancelled

await agent.close();
```

## Run the examples

```bash
llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M
llmman serve

cd demo.acp.sdk/nodejs
node examples/complete.js      # not streamed
node examples/stream.js        # streamed, with the tool calls
node examples/cancel.js        # cancelled after 3 seconds
node examples/permission.js    # a command refused, then allowed
```

`mm` must be on the PATH, or named by `MM_BIN`. The examples use `agent.yaml` (Qwen3.5 4B on llmman, bash only, `yolo: false`); any agent file works.

## The API

| | |
|---|---|
| `await Agent.start({ config, cwd?, mm?, onPermission?, onStderr? })` | starts `mm -acp -config <config>` (made absolute), then `initialize` and `session/new {cwd}`. `mm` defaults to `$MM_BIN`, then `mm`. |
| `await agent.complete(prompt, { signal?, onEvent? })` | one turn; resolves to `{ text, stopReason, usage }` once it is over. `onEvent` sees every event as it comes. |
| `agent.stream(prompt, { signal? })` | one turn as an async iterator of events; the last one is `done`. Leaving the loop early cancels the turn. |
| `signal` (an `AbortSignal`) | aborting it sends `session/cancel`; the turn ends with `stopReason: "cancelled"` and the text so far. |
| `onPermission(call)` | asked when the agent wants to run a tool that needs a confirmation (`yolo: false`). Return `true` (allow once), `false` (reject once) or an ACP `optionId` (`"allow_always"`, `"never"`). **Without it, every such call is refused.** |
| `onStderr(line)` | mm's stderr (banner, warnings, `[acp]` lines). Otherwise it is read and dropped; its last line is added to the error when mm exits. |
| `await agent.reset()` | a new session on the same process: the conversation starts over. |
| `await agent.close()` | closes mm's stdin and waits for it to exit. |
| `agent.sessionId` | the ACP session id. |

The events: `{kind: "text", text}`, `{kind: "thinking", text}`, `{kind: "tool_call", id, title, toolKind, input}`, `{kind: "tool_result", id, status, output}`, `{kind: "done", stopReason, usage, text}`.

Rules worth knowing:

- **One Agent = one mm process = one conversation.** Calls continue it, as in an editor. One turn at a time per Agent (a second one throws `already running`); start several Agents to work in parallel.
- **A turn mm ends with an error rejects** with mm's own words, e.g. `minime: Internal error (-32603): {"error":"the model repeated the same bash call 6 times…"}`.
- **A cancelled question is dropped from the conversation by mm**: the next question does not know about it.
- The client advertises no file-system, terminal or form capability: mm needs none, and its `ask_user` tool is not offered.

## Tests

```bash
npm test        # node --test test/minime.test.js — 11 tests, no model, no mm
```

`test/fake-mm.js` plays mm's side of ACP with the message shapes captured from mm v1.0.1: answers, a tool call with its permission, a slow turn to cancel, a crash. Not a bare `node --test`: it would also run `fake-mm.js` as a test and wait forever.

## Measured (2026-10-26, llmman on the author's host, mm v1.0.1 built from the working tree)

| Example | Result |
|---|---|
| `complete.js` | one-sentence answer, `end_turn`, 663 tokens, **2.9 s** |
| `stream.js` | `find examples -name "*.js" \| wc -l` allowed by the callback (read-only), output `4`, answer streamed chunk by chunk, `end_turn` |
| `cancel.js` | story cut after **3.0 s** (`cancelled`, 383 characters kept); the next question on the same Agent answered at once — without knowing the story, which mm dropped |
| `permission.js` | `pwd` refused: the model **invented** an answer (`/root`); allowed: the real directory. A refusal does not stop a small model from guessing. |

Seen in three runs before the callback allowed `find`: the model retried the same refused command until mm's loop detector stopped the turn (6 identical calls); `stream()` rejected with that message — hence the `try/catch` in `stream.js`.

## Not done

- The official ACP TypeScript library was not tried (no network in the sandbox where this was written): raw JSON-RPC keeps the client dependency-free.
- Not a published npm package: the `mm` binary would have to ship with it, per platform.
- `session/load` / `session/resume` (stored sessions), images and file attachments in prompts, `ask_user` forms: not exposed.