Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Changelog

## 0.7.0 (2026-05-02)

### Added
- **MCP server** (`bin/iak-mcp.mjs`) exposing `wake_ide`, `list_sessions`, `wake_all`, `read_session`, `tmux_run`, plus a fail-closed allowlist gate for `tmux_run` so it is omitted from the tool list entirely when no `tmux.allow` is configured (PR #10, commits `1a3378e`, `ab1784e`).
- **`request_confirmation` MCP tool** + `iak-mcp-daemon` long-running flavor with HTTP listener + chat-reply poller. Agents can ask for explicit user approval over the room and over CodeWatch (`6dc0d57`, `62fd0bd`).
- **`POST /intent` endpoint** on the daemon — any caller (not just MCP tools) can create a confirmation intent. Used by the new PreToolUse Bash gate on the mini side (`1f63d80`).
- **`POST /wake` endpoint + `wake_remote` MCP tool** for cross-machine direct nudge between Claude Code instances. Spawns the configured wake script detached, returns 202 immediately (`c985b52`).
- **GroupMind announcements include `metadata.actions` + `metadata.intent_id`** so antfarm `messages/route.ts` can render inline Approve/Deny buttons (paired with `antfarm` PR #13) (`0f1c6d0`).
- **`scripts/claudecode-stop-resume.sh`** — Stop-hook auto-resume mechanism for the Claude Code desktop app, alternative to AppleScript wake when Accessibility permission is unavailable (`01b7180`).
- **`scripts/install.sh`** — one-shot macOS bootstrap. Brew prereqs, repo clone, npm install, starter config, hook wiring, tmux daemon start, prints LAN URL for CodeWatch (`1dfd456`, `95101b2`).
- **`docs/auto-wake.md`** — full setup guide for both AppleScript and Stop-hook auto-wake paths (`927c3d8`).
- **Configurable `mcp.sessions`** array replaces the fragile "scan every top-level config key for objects with a .session string" heuristic. Falls back to `tmux.ide_session` + `tmux.default_session`.
- **`read_session` MCP tool** for capturing tmux pane output after `wake_ide`.

### Changed
- Server version now read from `package.json` at module load time instead of hard-coded.
- `tmux_run` allowlist matcher kept the prefix-match semantics from prior releases; shell-chain bypass via `&&` and `;` remains a known limitation flagged by @CodexMB during PR #10 review. Argv-based allowlist semantics deferred to a follow-up release.

### Compatibility
- New `mcp.confirmations` config block (room, host, port). Daemon exits 2 if `mcp.confirmations.room` and `mcp.confirmations.codewatch_gate_url` are both unset.
- Existing configs without `mcp` block keep working — MCP server + daemon are opt-in.

## 0.6.1 (2026-04-07)

### Docs
Expand Down
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,95 @@ Latest local verification from the Petrus machine, dogfooded against `v0.6.1`:

## Integrations

### MCP server (`src/mcp-server.mjs`)

Exposes IAK's tmux-backed wake / list / run primitives as MCP tools so any
MCP-aware client (Claude Desktop / Code, Cursor, custom agents) can drive the
agent fleet directly without re-implementing tmux send-keys.

Tools exposed (stdio transport):

| Tool | Args | Notes |
|-----------------|-----------------------------------|-------|
| `wake_ide` | `session`, `text?` (default `"check rooms"`) | Sends nudge text and presses Enter in the named tmux session. |
| `list_sessions` | (none) | Returns every live tmux session on the host with attach state + window count. |
| `wake_all` | `text?` (default `"check rooms"`) | Sends the same nudge to every session IAK knows about (per-session pass/fail). Configure via `mcp.sessions: ["...", ...]`. Falls back to `tmux.ide_session` + `tmux.default_session`. |
| `read_session` | `session`, `lines?` (default 50) | `tmux capture-pane` of the named session — see what the agent printed in response to a `wake_ide`. |
| `tmux_run` | `cmd`, `session?`, `cwd?`, `timeoutSec?` | Runs an allowlisted command in a tmux session. **Only registered when `tmux.allow` is non-empty or `mcp.allow_unrestricted: true` is set.** Otherwise omitted entirely from the tool list (fail-closed). Same allowlist as the CLI's `tmux run` subcommand. |

### MCP-specific config keys

Added to `ide-agent-kit.json` (or your own config path passed via `--config`):

```jsonc
{
"mcp": {
// Explicit list of sessions wake_all should target.
// If omitted, falls back to [tmux.ide_session, tmux.default_session].
"sessions": ["claudemb", "antigravity", "codex"],

// Set true to expose tmux_run with NO allowlist filter — any command runs.
// Default: false. Use only on a trusted host with a trusted MCP client.
"allow_unrestricted": false,

// User-confirmation flow (request_confirmation, list_intents,
// approve_intent, deny_intent tools). Tools are only registered if at
// least one channel below is configured.
"confirmations": {
"port": 8788, // HTTP port for /intent/:id/decision
"host": "127.0.0.1", // bind host (keep local unless tunneled)
"auth_token": "", // optional bearer for the HTTP endpoint
"callback_base": "http://...", // URL the watch / chat reach back on; defaults to http://host:port
"room": "thinkoff-development", // GroupMind room to post the prompt in (uses poller.api_key)
"codewatch_gate_url": "http://family@localhost:18791/intent",
"codewatch_gate_token": "" // bearer for CLAWWATCH_GATE
}
}
}
```

### Confirmation flow (request_confirmation tool)

When `mcp.confirmations` is configured, four extra tools appear:

| Tool | Args | Notes |
|-----------------------|---------------------------------------------------|-------|
| `request_confirmation`| `prompt`, `session?`, `channels?`, `timeoutSec?` | Posts an Approve / Deny prompt to GroupMind and/or Codewatch and BLOCKS until user decides or timeout. Returns `{decision: "approve"\|"deny"}` or `{status: "timeout", id}`. |
| `list_intents` | (none) | All intents — pending and recently decided. |
| `approve_intent` | `id` | Manually settle a pending intent (e.g. MCP override). |
| `deny_intent` | `id` | |

End-to-end:
1. MCP-aware agent calls `request_confirmation({prompt: "Drop production DB?"})`.
2. The IAK MCP server posts to GroupMind room (`/approve <id>` / `/deny <id>` quick replies) and to the CLAWWATCH_GATE (Android interactive notification with Approve / Deny buttons that vibrate the watch).
3. User taps Approve / Deny on the watch — Codewatch's notification action POSTs to `http://<callback_base>/intent/<id>/decision` with `{decision: "approve"}`.
4. The MCP tool's blocking `request_confirmation` call resolves with the decision.
5. The agent proceeds (or doesn't) based on the decision.

Run standalone:

```bash
node bin/iak-mcp.mjs # default config
node bin/iak-mcp.mjs --config /path/to/config.json
npm run mcp # via package.json script
```

Wire into Claude Desktop / Code:

```json
{
"mcpServers": {
"ide-agent-kit": {
"command": "node",
"args": ["/absolute/path/to/ide-agent-kit/bin/iak-mcp.mjs"]
}
}
}
```

After install: restart the MCP client. The four tools above appear in the tool
picker and can be called directly.

### GitHub Webhooks (`src/webhook-server.mjs`)

Receives GitHub webhook events, verifies HMAC signatures, normalizes them to a stable JSON schema, and appends to a local JSONL queue. Optionally nudges a tmux session when events arrive.
Expand Down
152 changes: 152 additions & 0 deletions bin/iak-mcp-daemon.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env node
// SPDX-License-Identifier: AGPL-3.0-only
//
// Long-running daemon flavor of the MCP confirmation flow. Keeps the HTTP
// listener up + watches the configured GroupMind room for `/approve <id>` and
// `/deny <id>` quick-reply messages and routes them to the local
// /intent/:id/decision endpoint.
//
// Used together with the MCP server: an MCP client triggers
// request_confirmation which posts to the room with an intent id; the user
// replies "/approve abc12345" from the watch / chat; this daemon catches the
// reply and POSTs it to the same HTTP listener that the MCP tool is waiting
// on; the MCP tool resolves with {decision: "approve"}.
//
// Run: node bin/iak-mcp-daemon.mjs [--config path/to/config.json]

import { loadConfig } from '../src/config.mjs';
import {
startConfirmationsServer,
decideIntent,
getIntent,
createIntent,
makeGroupmindAnnouncer,
makeCodewatchAnnouncer,
composeAnnouncers,
} from '../src/confirmations.mjs';

const argv = process.argv.slice(2);
let configPath;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--config' && argv[i + 1]) { configPath = argv[i + 1]; i++; }
}
const config = await loadConfig(configPath);
const cc = config?.mcp?.confirmations || {};
if (!cc.room && !cc.codewatch_gate_url) {
process.stderr.write('[iak-mcp-daemon] no mcp.confirmations channels configured — exiting\n');
process.exit(2);
}

// Build the announcer map up-front so the HTTP server can use it for
// externally-created intents (POST /intent).
const apiKey = config?.poller?.api_key;
const room = cc.room;

const serverAnnouncerMap = {};
if (cc.room && apiKey) {
serverAnnouncerMap.groupmind = makeGroupmindAnnouncer({
apiKey, room: cc.room, callbackBase: cc.callback_base || `http://127.0.0.1:${cc.port || 8788}`,
});
}
if (cc.codewatch_gate_url) {
serverAnnouncerMap.codewatch = makeCodewatchAnnouncer({
gateUrl: cc.codewatch_gate_url, gateToken: cc.codewatch_gate_token,
});
}
const serverAnnounce = composeAnnouncers(serverAnnouncerMap);

// Start the HTTP listener first so any decisions can settle.
// Wake script: defaults to scripts/claudemb-wake.sh in this repo.
// Override via mcp.confirmations.wake_script in config.
const wakeScript = cc.wake_script ||
new URL('../scripts/claudemb-wake.sh', import.meta.url).pathname;

startConfirmationsServer({
port: cc.port || 8788,
host: cc.host || '127.0.0.1',
authToken: cc.auth_token || '',
receiptsPath: config?.receipts?.path,
announce: serverAnnounce,
wakeScript,
});
console.log(`[iak-mcp-daemon] HTTP listener on http://${cc.host || '127.0.0.1'}:${cc.port || 8788} (POST /intent enabled: ${Object.keys(serverAnnouncerMap).join(',') || 'no announcers'})`);

// Chat-reply poller: watch the configured GroupMind room for "/approve <id>"
// and "/deny <id>" messages and route them to the local intent endpoint.
if (!apiKey) {
console.warn('[iak-mcp-daemon] poller.api_key missing — chat-reply poller disabled');
} else if (!room) {
console.warn('[iak-mcp-daemon] mcp.confirmations.room missing — chat-reply poller disabled');
} else {
startChatReplyPoller({ apiKey, room, intervalMs: 5000 });
console.log(`[iak-mcp-daemon] chat-reply poller watching room "${room}" every 5s`);
}

// Codewatch path: just announce when a message arrives at /push (not implemented
// here, would route through CodexMB's watch-gate.py — see PR #8).

// Manual ping: prove the round-trip by creating one demo intent on first run if
// --demo is passed.
if (argv.includes('--demo')) {
const announcerMap = {};
if (cc.room && apiKey) {
announcerMap.groupmind = makeGroupmindAnnouncer({
apiKey, room: cc.room, callbackBase: cc.callback_base || `http://127.0.0.1:${cc.port || 8788}`,
});
}
if (cc.codewatch_gate_url) {
announcerMap.codewatch = makeCodewatchAnnouncer({
gateUrl: cc.codewatch_gate_url, gateToken: cc.codewatch_gate_token,
});
}
const announce = composeAnnouncers(announcerMap);
const id = await createIntent({
prompt: 'iak-mcp-daemon demo: approve to confirm the round-trip works.',
session: 'demo',
channels: Object.keys(announcerMap),
announce,
receiptsPath: config?.receipts?.path,
});
console.log(`[iak-mcp-daemon] demo intent created: id=${id}`);
}

// Keep the process alive.
process.stdin.resume();

// --- helpers --------------------------------------------------------------

function startChatReplyPoller({ apiKey, room, intervalMs }) {
const seen = new Set();
let primed = false;
const poll = async () => {
try {
const url = `https://groupmind.one/api/v1/rooms/${encodeURIComponent(room)}/messages?limit=30`;
const res = await fetch(url, { headers: { 'X-API-Key': apiKey } });
if (!res.ok) return;
const body = await res.json();
const messages = body?.messages || [];
for (const m of messages) {
if (seen.has(m.id)) continue;
seen.add(m.id);
if (!primed) continue; // ignore historical messages on first pass
const text = (m.body || '').trim();
const match = text.match(/^\/(approve|deny)\s+([a-f0-9]+)$/i);
if (!match) continue;
const decision = match[1].toLowerCase();
const id = match[2];
const intent = getIntent(id);
if (!intent) {
console.log(`[iak-mcp-daemon] /${decision} ${id} from ${m.from}: unknown intent, ignoring`);
continue;
}
const r = decideIntent(id, decision);
console.log(`[iak-mcp-daemon] /${decision} ${id} from ${m.from}: ${r.ok ? 'settled' : r.error}`);
}
primed = true;
} catch (e) {
console.warn(`[iak-mcp-daemon] poll error: ${e.message}`);
}
};
poll();
setInterval(poll, intervalMs);
}
32 changes: 32 additions & 0 deletions bin/iak-mcp.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env node
// SPDX-License-Identifier: AGPL-3.0-only
//
// Entry point for the IAK MCP server. Designed to be invoked by an MCP
// client over stdio. Example Claude Desktop / Code config:
//
// {
// "mcpServers": {
// "ide-agent-kit": {
// "command": "node",
// "args": ["/path/to/ide-agent-kit/bin/iak-mcp.mjs"]
// }
// }
// }
//
// Or via npx if installed as a package: `npx ide-agent-kit-mcp`.

import { runMcpServer } from '../src/mcp-server.mjs';

const argv = process.argv.slice(2);
let configPath;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--config' && argv[i + 1]) {
configPath = argv[i + 1];
i++;
}
}

runMcpServer({ configPath }).catch((e) => {
process.stderr.write(`[iak-mcp] fatal: ${e.message}\n`);
process.exit(1);
});
Loading