@algosuite/vo-mcp 0.2.0-beta.2 → 0.2.0-beta.20
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/README.md +178 -153
- package/bin/vo-mcp +44 -38
- package/dist/autostart-cli.js +104 -14
- package/dist/autostart-cli.js.map +2 -2
- package/dist/ci/check-local-pr-overlap.js +102449 -0
- package/dist/cli.js +1725 -796
- package/dist/cli.js.map +4 -4
- package/dist/index.js +717 -129
- package/dist/index.js.map +4 -4
- package/dist/install-cli.js +365 -327
- package/dist/install-cli.js.map +4 -4
- package/dist/login-cli.js +4 -4
- package/dist/login-cli.js.map +2 -2
- package/dist/pair-cli.js +1 -1
- package/dist/pair-cli.js.map +2 -2
- package/dist/runner-cli.js +8180 -1079
- package/dist/runner-cli.js.map +4 -4
- package/dist/runner-supervisor.js +2330 -0
- package/dist/runner-supervisor.js.map +7 -0
- package/dist/set-key-cli.js +38 -4
- package/dist/set-key-cli.js.map +2 -2
- package/dist/supervisor-credential-helper.js +233 -0
- package/dist/supervisor-credential-helper.js.map +7 -0
- package/dist/update-cli.js +125 -0
- package/dist/update-cli.js.map +7 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,153 +1,178 @@
|
|
|
1
|
-
# @algosuite/vo-mcp
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
The
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
1
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
1
|
+
# @algosuite/vo-mcp
|
|
2
|
+
|
|
3
|
+
AlgoHQ MCP server — the open protocol surface that exposes HQ's consensus and ratchet tool family to any MCP-capable LLM client (Claude Code, Claude Desktop, Cursor, Continue, Codex, etc.).
|
|
4
|
+
|
|
5
|
+
The live AlgoHQ whiteboard is available to every MCP client through
|
|
6
|
+
`hq_whiteboard_post` and `hq_whiteboard_read`. Both use the scoped credential
|
|
7
|
+
created by `vo-mcp login`; tenant/operator ownership is derived by the control
|
|
8
|
+
plane and cannot be widened by tool input. The control plane admits scoped
|
|
9
|
+
credentials only for operator IDs on its internal `HQ_WHITEBOARD_OPERATOR_IDS`
|
|
10
|
+
allowlist; arbitrary self-serve tenants cannot read or write the shared fleet board.
|
|
11
|
+
|
|
12
|
+
**Status:** Phase 2. Stdio transport. **26 tools registered** (source of truth: `src/server.ts` `buildToolRegistry()`). The static-ratchet tools (`vo_check_assertion_strength`, `vo_check_ratchets`) run locally today; the consensus-routed tools fall back to `{ verdict: "unimplemented", ... }` until engine credentials are present; the heal-family, PR-admin, session-state, and AlgoHQ whiteboard tools forward to the control plane in cloud mode. `vo_review_merge` is a **read-only consensus pre-merge review** (the verify-before-act gate for the Command Center — never merges). The shape is stable; the institutional-knowledge content is loaded from closed packages.
|
|
13
|
+
|
|
14
|
+
This package is intentionally **shell-only**. Per `docs/handoffs/vo-mcp-server-2026-05-21.md` §C-1: tool *definitions* (schemas, names, descriptions) ship openly. Tool *implementations* that encode institutional knowledge (specific ratchet thresholds, consensus prompt content, architectural-defaults knowledge base) live behind the cloud or in closed companion packages.
|
|
15
|
+
|
|
16
|
+
## Install and refresh client configuration
|
|
17
|
+
|
|
18
|
+
`vo-mcp install` safely registers the same required `algohq` MCP server in
|
|
19
|
+
Claude Desktop, Claude Code, and Codex (`~/.codex/config.toml`), then offers
|
|
20
|
+
pairing and runner auto-start. Existing client settings and comments are
|
|
21
|
+
preserved, and every changed config is backed up first.
|
|
22
|
+
|
|
23
|
+
To refresh client configuration on an already-paired runner without touching
|
|
24
|
+
its keychain credential or auto-start service, use the noninteractive form:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
vo-mcp install --config-only
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Restart the MCP clients and runner after updating, then use
|
|
31
|
+
[AlgoHQ](https://algosuite.ai/algohq) to dispatch work.
|
|
32
|
+
|
|
33
|
+
## Tools
|
|
34
|
+
|
|
35
|
+
| Name | Phase 1 status | Description |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `vo_check_assertion_strength` | Implemented (stub ratchet) | Score a test file's assertions 0-100 with per-assertion findings. |
|
|
38
|
+
| `vo_check_hollow_test` | Stub | Detect tests that pass without verifying product truth. |
|
|
39
|
+
| `vo_verify_answer` | Stub | Semantic-equivalence comparison of expected vs observed. |
|
|
40
|
+
| `vo_consensus_judgment` | Stub + logging skeleton | Submit a prompt to multiple models, return synthesized verdict. **Logs every call** (V1 launch gate #7). |
|
|
41
|
+
| `vo_architecture_review` | Stub | Senior-architect review of a diff against project defaults. |
|
|
42
|
+
|
|
43
|
+
All tools follow the same response envelope:
|
|
44
|
+
|
|
45
|
+
```jsonc
|
|
46
|
+
{
|
|
47
|
+
"tool": "vo_check_assertion_strength",
|
|
48
|
+
"schema_version": 1,
|
|
49
|
+
"cache": { "hit": false, "key": "<sha256 hex>" },
|
|
50
|
+
"payload": { /* tool-specific */ }
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## V1 launch gates implemented in Phase 1
|
|
55
|
+
|
|
56
|
+
- **Gate #7 — consensus-call logging.** Every tool invocation appends a JSONL line to `~/.claude/vo-mcp-events.jsonl` (override with `$VO_MCP_EVENTS_PATH`). Schema is locked: see `src/types.ts` `ConsensusCallEvent`. `per_model_verdicts` and `synthesized_verdict` are empty/null in the scaffold; the schema shape is the deliverable.
|
|
57
|
+
- **Gate #8 — content-hash cache.** Every tool invocation computes `sha256(canonicalize(tool_input))`. Repeat calls with the same canonicalized input return the cached envelope with `cache.hit = true`. Backed by sqlite at `~/.claude/vo-mcp-cache.db` (override with `$VO_MCP_DB_PATH`).
|
|
58
|
+
|
|
59
|
+
## Building & testing
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
cd packages/vo-mcp
|
|
63
|
+
pnpm install
|
|
64
|
+
pnpm run build # tsc -p tsconfig.json
|
|
65
|
+
pnpm run typecheck # tsc --noEmit
|
|
66
|
+
pnpm test # vitest run
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The integration test (`test/integration/stdio-roundtrip.test.ts`) spawns the built CLI, exchanges JSON-RPC over stdio, and asserts:
|
|
70
|
+
|
|
71
|
+
- `initialize` + `tools/list` returns the 26 expected tools.
|
|
72
|
+
- `vo_check_assertion_strength` runs end-to-end and writes a JSONL event line.
|
|
73
|
+
- A repeat call with identical input hits the cache (`cache.hit = true`).
|
|
74
|
+
- `vo_consensus_judgment` returns `unimplemented` and still writes its event line.
|
|
75
|
+
|
|
76
|
+
## Running the server
|
|
77
|
+
|
|
78
|
+
The CLI is a stdio MCP server. Don't run it interactively; register it in your MCP client.
|
|
79
|
+
|
|
80
|
+
### Claude Desktop / Claude Code
|
|
81
|
+
|
|
82
|
+
Add to `~/.claude/mcp_settings.json` (Claude Desktop) or your Claude Code config:
|
|
83
|
+
|
|
84
|
+
```jsonc
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"vo": {
|
|
88
|
+
"command": "node",
|
|
89
|
+
"args": ["<absolute-path>/packages/vo-mcp/dist/cli.js"],
|
|
90
|
+
"env": {
|
|
91
|
+
// Optional overrides — defaults shown below
|
|
92
|
+
// "VO_MCP_EVENTS_PATH": "/path/to/events.jsonl",
|
|
93
|
+
// "VO_MCP_DB_PATH": "/path/to/cache.db"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Cursor
|
|
101
|
+
|
|
102
|
+
Cursor reads MCP servers from its global config (Settings → Features → MCP). Add an entry:
|
|
103
|
+
|
|
104
|
+
```jsonc
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"vo": {
|
|
108
|
+
"command": "node",
|
|
109
|
+
"args": ["<absolute-path>/packages/vo-mcp/dist/cli.js"]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
After saving, restart Cursor. The tools appear under the `@vo` namespace in the chat composer's tool picker.
|
|
116
|
+
|
|
117
|
+
### Continue
|
|
118
|
+
|
|
119
|
+
Continue reads MCP servers from `~/.continue/config.json` under the `experimental.modelContextProtocolServer` (or v2 `mcpServers`) section, depending on the Continue release on your machine. Verify the current shape against Continue's docs; the launch command itself is the same:
|
|
120
|
+
|
|
121
|
+
```jsonc
|
|
122
|
+
{
|
|
123
|
+
"command": "node",
|
|
124
|
+
"args": ["<absolute-path>/packages/vo-mcp/dist/cli.js"]
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
> **Cross-vendor smoke.** A captured cross-vendor smoke now exists — see
|
|
129
|
+
> [`CROSS_VENDOR_SMOKE.md`](./CROSS_VENDOR_SMOKE.md). Run `pnpm run smoke:cross-vendor`
|
|
130
|
+
> to drive the built server through the full MCP lifecycle under each vendor's
|
|
131
|
+
> real client parameters (protocol-version negotiation, capabilities, client
|
|
132
|
+
> identity, schema validity) and reproduce the recorded transcripts. That doc
|
|
133
|
+
> also carries the operator checklist for capturing real Cursor/Continue/Codex
|
|
134
|
+
> GUI sessions. The integration test in this package exercises the generic
|
|
135
|
+
> single-version stdio surface that any MCP-spec-compliant client uses.
|
|
136
|
+
|
|
137
|
+
## Environment variables
|
|
138
|
+
|
|
139
|
+
| Var | Default | Purpose |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `VO_MCP_EVENTS_PATH` | `~/.claude/vo-mcp-events.jsonl` | Where consensus-call event lines are appended. |
|
|
142
|
+
| `VO_MCP_DB_PATH` | `~/.claude/vo-mcp-cache.db` | sqlite cache file for content-hash cache. |
|
|
143
|
+
| `VO_CONTROL_PLANE_URL` | _(unset → cloud off)_ | vo-control-plane base URL. Set together with the admin token and tenant ID to activate **cloud mode** — the admin-proxy tools (concierge dispatch, heal/PR families) forward to vo-control-plane instead of returning `unimplemented` stubs. |
|
|
144
|
+
| `VO_CONTROL_PLANE_ADMIN_TOKEN` | _(unset → cloud off)_ | Bearer token vo-control-plane validates. Required with the URL and tenant ID above; setting only some is a configuration error. |
|
|
145
|
+
| `VO_TENANT_ID` | _(unset → cloud off)_ | Tenant UUID. Required with the control-plane URL and admin token above to enable cloud mode. Interactive agents (Claude Code, Cursor, Codex, Continue) auto-allocate their session on first report and appear on the live fleet whiteboard. |
|
|
146
|
+
| `VO_ADMIN_CALLABLES_READONLY` | `false` | When truthy (`1`/`true`), cloud mode runs **read-only**: read-only tools (concierge dispatch + the list/get diagnostics) forward, but the heal/PR **write** tools (merge, reject, trigger-heal, …) stay gated to their stubs. Lets you expose concierge without exposing destructive admin actions. |
|
|
147
|
+
|
|
148
|
+
## Adding a new tool (Phase 2+)
|
|
149
|
+
|
|
150
|
+
1. Create `src/tools/<tool>.ts` exporting:
|
|
151
|
+
- `TOOL_NAME` constant
|
|
152
|
+
- `description` (writes the discovery copy that cross-vendor clients show)
|
|
153
|
+
- `inputSchema` (JSON Schema for MCP `tools/list`)
|
|
154
|
+
- `handle<Tool>(deps, rawInput)` (validate input, hit cache, call backend, log event, return `jsonContent(envelope)`)
|
|
155
|
+
2. Add it to `buildToolRegistry()` in `src/server.ts`.
|
|
156
|
+
3. Add tests under `test/tools/<tool>.test.ts`.
|
|
157
|
+
4. Update this README's tool table and `EXTRACTION_AUDIT.md`.
|
|
158
|
+
|
|
159
|
+
The tool handler MUST:
|
|
160
|
+
|
|
161
|
+
- Hash the canonicalized input via `deps.cache.keyFor(TOOL_NAME, rawInput)`.
|
|
162
|
+
- Read the cache before any expensive work; return cached envelope with `cache.hit = true` if present.
|
|
163
|
+
- Append a `ConsensusCallEvent` via `deps.events.append(buildBaseEvent(...))` on every invocation.
|
|
164
|
+
- Sanitize inputs before logging (`sanitizeExcerpt` is applied automatically in `buildBaseEvent`).
|
|
165
|
+
- Return a `ToolResultEnvelope<TPayload>` JSON-encoded in a single text content block.
|
|
166
|
+
|
|
167
|
+
## Hard rules (handoff §C)
|
|
168
|
+
|
|
169
|
+
This package complies with:
|
|
170
|
+
|
|
171
|
+
1. **Open shell, closed content.** Stub ratchet client lives here; real thresholds load from `@algosuite/ratchets-generic` in Phase 2 (see `EXTRACTION_AUDIT.md`).
|
|
172
|
+
2. **Cross-vendor by design.** No Claude-specific assumptions; tool schemas are MCP-spec compliant.
|
|
173
|
+
3. **Content-hash cache every consensus call.** Implemented at `src/cache/sqlite-cache.ts`.
|
|
174
|
+
4. **Log every consensus call.** Implemented at `src/logging/events-writer.ts`.
|
|
175
|
+
5. **TypeScript strict, zero `any`, zero `@ts-ignore`.**
|
|
176
|
+
6. **No PII in logs.** `sanitizeExcerpt` strips JWTs, sk- keys, bearer tokens, and email addresses before logging.
|
|
177
|
+
|
|
178
|
+
See `EXTRACTION_AUDIT.md` for what's stub, what's real, and where the moat lives.
|
package/bin/vo-mcp
CHANGED
|
@@ -1,38 +1,44 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* vo-mcp CLI dispatcher.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* vo-mcp # MCP stdio server (default)
|
|
7
|
-
* vo-mcp install # one-command installer
|
|
8
|
-
* vo-mcp
|
|
9
|
-
* vo-mcp
|
|
10
|
-
* vo-mcp
|
|
11
|
-
* vo-mcp
|
|
12
|
-
* vo-mcp runner
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import('../dist/
|
|
22
|
-
} else if (command === '
|
|
23
|
-
import('../dist/
|
|
24
|
-
} else if (command === '
|
|
25
|
-
import('../dist/
|
|
26
|
-
} else if (command === '
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} else {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* vo-mcp CLI dispatcher.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* vo-mcp # MCP stdio server (default)
|
|
7
|
+
* vo-mcp install # one-command installer
|
|
8
|
+
* vo-mcp install --config-only # refresh Claude + Codex MCP config only
|
|
9
|
+
* vo-mcp update # update MCP + runner package
|
|
10
|
+
* vo-mcp login # credential login (browser loopback)
|
|
11
|
+
* vo-mcp pair # device-code pairing (enter a code in the web)
|
|
12
|
+
* vo-mcp runner # supervised agent runner
|
|
13
|
+
* vo-mcp runner --install-autostart # register runner to start at login
|
|
14
|
+
* vo-mcp runner --uninstall-autostart # remove auto-start registration
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const command = process.argv[2];
|
|
18
|
+
const subcommand = process.argv[3];
|
|
19
|
+
|
|
20
|
+
if (command === 'install') {
|
|
21
|
+
import('../dist/install-cli.js');
|
|
22
|
+
} else if (command === 'update') {
|
|
23
|
+
import('../dist/update-cli.js');
|
|
24
|
+
} else if (command === 'login') {
|
|
25
|
+
import('../dist/login-cli.js');
|
|
26
|
+
} else if (command === 'pair') {
|
|
27
|
+
import('../dist/pair-cli.js');
|
|
28
|
+
} else if (command === 'set-key') {
|
|
29
|
+
import('../dist/set-key-cli.js');
|
|
30
|
+
} else if (command === 'runner') {
|
|
31
|
+
if (subcommand === '--install-autostart') {
|
|
32
|
+
import('../dist/autostart-cli.js').then((m) => m.installAutostartCli());
|
|
33
|
+
} else if (subcommand === '--uninstall-autostart') {
|
|
34
|
+
import('../dist/autostart-cli.js').then((m) => m.uninstallAutostartCli());
|
|
35
|
+
} else if (process.argv.includes('--once') || process.argv.includes('--status') || process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
36
|
+
// Direct diagnostic/one-shot mode; remote maintenance uses the supervisor.
|
|
37
|
+
import('../dist/runner-cli.js');
|
|
38
|
+
} else {
|
|
39
|
+
import('../dist/runner-supervisor.js');
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
// Default: MCP stdio server
|
|
43
|
+
import('../dist/cli.js');
|
|
44
|
+
}
|
package/dist/autostart-cli.js
CHANGED
|
@@ -12,7 +12,16 @@ function installWindowsAutostart(runnerCommand, log, env) {
|
|
|
12
12
|
const appData = env["APPDATA"] ?? join(homedir(), "AppData", "Roaming");
|
|
13
13
|
const startupDir = join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
14
14
|
mkdirSync(startupDir, { recursive: true });
|
|
15
|
-
const launcherPath = join(startupDir, "vo-runner.
|
|
15
|
+
const launcherPath = join(startupDir, "vo-runner.vbs");
|
|
16
|
+
const legacyCmdPath = join(startupDir, "vo-runner.cmd");
|
|
17
|
+
if (existsSync(legacyCmdPath)) {
|
|
18
|
+
try {
|
|
19
|
+
unlinkSync(legacyCmdPath);
|
|
20
|
+
log(` Removed legacy minimized .cmd launcher: ${legacyCmdPath}`);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
log(`\u26A0 Could not remove legacy launcher ${legacyCmdPath} (${error instanceof Error ? error.message : String(error)}); remove it manually to avoid a double start`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
16
25
|
if (existsSync(launcherPath)) {
|
|
17
26
|
const existing = readFileSync(launcherPath, "utf8");
|
|
18
27
|
if (existing.includes("vo-mcp runner")) {
|
|
@@ -24,27 +33,31 @@ function installWindowsAutostart(runnerCommand, log, env) {
|
|
|
24
33
|
copyFileSync(launcherPath, backupPath);
|
|
25
34
|
log(` Backed up existing launcher to: ${backupPath}`);
|
|
26
35
|
}
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
const hiddenCommand = `cmd /c ${runnerCommand} >> "%USERPROFILE%\\.claude\\vo-runner.log" 2>&1`.replace(/"/g, '""');
|
|
37
|
+
const launcherContent = `' Auto-start launcher for vo-mcp runner
|
|
38
|
+
' Created by vo-mcp autostart installer
|
|
39
|
+
CreateObject("WScript.Shell").Run "${hiddenCommand}", 0, False
|
|
31
40
|
`;
|
|
32
41
|
writeFileSync(launcherPath, launcherContent, "utf8");
|
|
33
42
|
log(`\u2713 Installed Windows auto-start launcher`);
|
|
34
43
|
log(` Path: ${launcherPath}`);
|
|
35
|
-
log(` The runner will start
|
|
44
|
+
log(` The runner will start hidden at next login.`);
|
|
36
45
|
}
|
|
37
46
|
function uninstallWindowsAutostart(log, env) {
|
|
38
47
|
const appData = env["APPDATA"] ?? join(homedir(), "AppData", "Roaming");
|
|
39
48
|
const startupDir = join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
40
|
-
const
|
|
41
|
-
|
|
49
|
+
const launcherPaths = [join(startupDir, "vo-runner.vbs"), join(startupDir, "vo-runner.cmd")];
|
|
50
|
+
let removedAny = false;
|
|
51
|
+
for (const launcherPath of launcherPaths) {
|
|
52
|
+
if (!existsSync(launcherPath)) continue;
|
|
53
|
+
unlinkSync(launcherPath);
|
|
54
|
+
removedAny = true;
|
|
55
|
+
log(`\u2713 Removed Windows auto-start launcher`);
|
|
56
|
+
log(` Path: ${launcherPath}`);
|
|
57
|
+
}
|
|
58
|
+
if (!removedAny) {
|
|
42
59
|
log(`\u2713 Auto-start launcher not found (already removed)`);
|
|
43
|
-
return;
|
|
44
60
|
}
|
|
45
|
-
unlinkSync(launcherPath);
|
|
46
|
-
log(`\u2713 Removed Windows auto-start launcher`);
|
|
47
|
-
log(` Path: ${launcherPath}`);
|
|
48
61
|
}
|
|
49
62
|
async function installMacAutostart(runnerCommand, log) {
|
|
50
63
|
const launchAgentsDir = join(homedir(), "Library", "LaunchAgents");
|
|
@@ -117,6 +130,79 @@ async function uninstallMacAutostart(log) {
|
|
|
117
130
|
log(`\u2713 Removed launchd plist`);
|
|
118
131
|
log(` Path: ${plistPath}`);
|
|
119
132
|
}
|
|
133
|
+
async function installLinuxAutostart(runnerCommand, log, env) {
|
|
134
|
+
const home = env["HOME"]?.trim() || homedir();
|
|
135
|
+
const unitDir = join(home, ".config", "systemd", "user");
|
|
136
|
+
mkdirSync(unitDir, { recursive: true });
|
|
137
|
+
const unitPath = join(unitDir, "vo-runner.service");
|
|
138
|
+
if (existsSync(unitPath)) {
|
|
139
|
+
const existing = readFileSync(unitPath, "utf8");
|
|
140
|
+
if (existing.includes(runnerCommand) || existing.includes("vo-mcp runner")) {
|
|
141
|
+
log(`\u2713 Auto-start is already configured (systemd user unit)`);
|
|
142
|
+
log(` Path: ${unitPath}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const backupPath = `${unitPath}.backup-${Date.now()}`;
|
|
146
|
+
copyFileSync(unitPath, backupPath);
|
|
147
|
+
log(` Backed up existing unit to: ${backupPath}`);
|
|
148
|
+
}
|
|
149
|
+
const logFile = join(home, ".claude", "vo-runner.log");
|
|
150
|
+
const errFile = join(home, ".claude", "vo-runner-error.log");
|
|
151
|
+
mkdirSync(join(home, ".claude"), { recursive: true });
|
|
152
|
+
const unit = `[Unit]
|
|
153
|
+
Description=AlgoHQ Code Runner (vo-mcp)
|
|
154
|
+
After=network-online.target
|
|
155
|
+
Wants=network-online.target
|
|
156
|
+
|
|
157
|
+
[Service]
|
|
158
|
+
Type=simple
|
|
159
|
+
ExecStart=/bin/sh -lc '${runnerCommand}'
|
|
160
|
+
Restart=on-failure
|
|
161
|
+
RestartSec=10
|
|
162
|
+
StandardOutput=append:${logFile}
|
|
163
|
+
StandardError=append:${errFile}
|
|
164
|
+
|
|
165
|
+
[Install]
|
|
166
|
+
WantedBy=default.target
|
|
167
|
+
`;
|
|
168
|
+
writeFileSync(unitPath, unit, "utf8");
|
|
169
|
+
log(`\u2713 Installed systemd user unit`);
|
|
170
|
+
log(` Path: ${unitPath}`);
|
|
171
|
+
if (process.env["VITEST"]) {
|
|
172
|
+
log(` (test mode: skipping systemctl enable)`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const { execSync } = await import("node:child_process");
|
|
177
|
+
execSync("systemctl --user daemon-reload", { stdio: "ignore" });
|
|
178
|
+
execSync("systemctl --user enable --now vo-runner.service", { stdio: "ignore" });
|
|
179
|
+
log(`\u2713 Enabled + started vo-runner.service (starts at login)`);
|
|
180
|
+
log(` Logs: ${logFile}`);
|
|
181
|
+
} catch {
|
|
182
|
+
log(`\u26A0 Could not enable via systemctl (enable it manually):`);
|
|
183
|
+
log(` systemctl --user daemon-reload && systemctl --user enable --now vo-runner.service`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function uninstallLinuxAutostart(log, env) {
|
|
187
|
+
const home = env["HOME"]?.trim() || homedir();
|
|
188
|
+
const unitPath = join(home, ".config", "systemd", "user", "vo-runner.service");
|
|
189
|
+
if (!existsSync(unitPath)) {
|
|
190
|
+
log(`\u2713 Auto-start unit not found (already removed)`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!process.env["VITEST"]) {
|
|
194
|
+
try {
|
|
195
|
+
const { execSync } = await import("node:child_process");
|
|
196
|
+
execSync("systemctl --user disable --now vo-runner.service", { stdio: "ignore" });
|
|
197
|
+
log(`\u2713 Disabled + stopped vo-runner.service`);
|
|
198
|
+
} catch {
|
|
199
|
+
log(`\u26A0 Could not disable via systemctl (continuing anyway)`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
unlinkSync(unitPath);
|
|
203
|
+
log(`\u2713 Removed systemd user unit`);
|
|
204
|
+
log(` Path: ${unitPath}`);
|
|
205
|
+
}
|
|
120
206
|
async function installAutostart(opts = {}) {
|
|
121
207
|
const log = opts.log ?? ((m) => console.error(m));
|
|
122
208
|
const env = opts.env ?? process.env;
|
|
@@ -126,9 +212,11 @@ async function installAutostart(opts = {}) {
|
|
|
126
212
|
installWindowsAutostart(runnerCommand, log, env);
|
|
127
213
|
} else if (plat === "darwin") {
|
|
128
214
|
await installMacAutostart(runnerCommand, log);
|
|
215
|
+
} else if (plat === "linux") {
|
|
216
|
+
await installLinuxAutostart(runnerCommand, log, env);
|
|
129
217
|
} else {
|
|
130
218
|
log(`\u2717 Auto-start is not supported on platform: ${plat}`);
|
|
131
|
-
log(` Supported platforms: win32 (Windows), darwin (macOS)`);
|
|
219
|
+
log(` Supported platforms: win32 (Windows), darwin (macOS), linux (Linux)`);
|
|
132
220
|
}
|
|
133
221
|
}
|
|
134
222
|
async function uninstallAutostart(opts = {}) {
|
|
@@ -139,9 +227,11 @@ async function uninstallAutostart(opts = {}) {
|
|
|
139
227
|
uninstallWindowsAutostart(log, env);
|
|
140
228
|
} else if (plat === "darwin") {
|
|
141
229
|
await uninstallMacAutostart(log);
|
|
230
|
+
} else if (plat === "linux") {
|
|
231
|
+
await uninstallLinuxAutostart(log, env);
|
|
142
232
|
} else {
|
|
143
233
|
log(`\u2717 Auto-start is not supported on platform: ${plat}`);
|
|
144
|
-
log(` Supported platforms: win32 (Windows), darwin (macOS)`);
|
|
234
|
+
log(` Supported platforms: win32 (Windows), darwin (macOS), linux (Linux)`);
|
|
145
235
|
}
|
|
146
236
|
}
|
|
147
237
|
|