helixbox-cli 0.1.123 → 0.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/CHANGELOG.md +17 -0
- package/LICENSE +7 -0
- package/README.md +36 -26
- package/dist/ai/cli.d.ts +59 -0
- package/dist/ai/cli.js +193 -0
- package/dist/ai/codex.js +11 -0
- package/dist/ai/index.d.ts +3 -1
- package/dist/ai/index.js +14 -2
- package/dist/ai/model-mcp.d.ts +1 -0
- package/dist/ai/model-mcp.js +66 -0
- package/dist/ai/model-tools.d.ts +30 -0
- package/dist/ai/model-tools.js +106 -0
- package/dist/index.js +20 -11
- package/package.json +10 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 - 2026-11-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Live model discovery, mandate, quote, purchase and result tools for Codex.
|
|
7
|
+
- A stdio MCP server for the same tools in Claude Code.
|
|
8
|
+
- Authenticated image/audio delivery into project-local artifact files.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Workspace access and installed coding agents no longer require a timed HelixBox access purchase.
|
|
12
|
+
- Optional model calls use phone-approved, bounded Solana USDC mandates on the existing backend.
|
|
13
|
+
- No wallet signing key is passed to the coding agent or written into its MCP configuration.
|
|
14
|
+
|
|
15
|
+
### Notes
|
|
16
|
+
- Mainnet batch settlement remains experimental. Actual device and wallet verification is required before relying on this beta.
|
|
17
|
+
- Existing agent-provider subscriptions and API charges are separate from model-tool purchases.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Soham Bharambe
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,30 +1,40 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Helix Box CLI
|
|
2
2
|
|
|
3
|
-
Node.js CLI that connects a local machine to the
|
|
3
|
+
Node.js CLI that connects a local machine to the Helix Box mobile app through the Helix Box gateway. It runs from the project directory you want to expose and keeps filesystem, terminal, process, port, git, and AI actions scoped to that working tree.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- Node.js 18 or newer
|
|
8
8
|
- npm
|
|
9
|
-
-
|
|
9
|
+
- Helix Box mobile app for QR/session pairing
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
Run the published package:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx
|
|
16
|
+
npx helixbox-cli@latest
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The CLI prints a QR code and session details. Scan the QR code with the
|
|
19
|
+
The CLI prints a QR code and session details. Scan the QR code with the Helix Box app to connect to the current working directory.
|
|
20
|
+
|
|
21
|
+
## Model tools in 0.2.0
|
|
22
|
+
|
|
23
|
+
Pairing, workspace controls and access to your installed Codex/Claude Code are free. Their own provider subscriptions remain separate.
|
|
24
|
+
|
|
25
|
+
The CLI gives Codex and Claude Code five tools: live model discovery, approved mandates, bounded quotes, purchase requests and result retrieval. Choose allowed models and approve a USDC budget in the Android app first. Your foreground phone signs calls within those limits; the CLI and coding agent never receive a wallet signing key.
|
|
26
|
+
|
|
27
|
+
Optional chat, image and speech calls use real x402 payments on Solana Mainnet. A complete result is prepared before its charge settles and is released only after acknowledgement. Keep the original purchase ID if a response is interrupted; do not buy again to retrieve it. Completed image/audio files are saved in `.helixbox-model-results/` inside your project.
|
|
28
|
+
|
|
29
|
+
This is an experimental mainnet beta. Start with a small budget and verify pairing, model delivery and returning unused escrow on your phone. The phone must remain foregrounded to approve new calls. Older CLI versions do not expose these tools; restart the CLI and pair again after upgrading.
|
|
20
30
|
|
|
21
31
|
Common options:
|
|
22
32
|
|
|
23
33
|
```bash
|
|
24
|
-
npx
|
|
25
|
-
npx
|
|
26
|
-
npx
|
|
27
|
-
npx
|
|
34
|
+
npx helixbox-cli --help
|
|
35
|
+
npx helixbox-cli --new
|
|
36
|
+
npx helixbox-cli --debug
|
|
37
|
+
npx helixbox-cli --extra-ports 3000,8080
|
|
28
38
|
```
|
|
29
39
|
|
|
30
40
|
Options:
|
|
@@ -34,39 +44,39 @@ Options:
|
|
|
34
44
|
| `-h`, `--help` | Show CLI help |
|
|
35
45
|
| `-n`, `--new` | Create a fresh session code instead of reusing the saved one |
|
|
36
46
|
| `-d`, `--debug` | Enable verbose CLI and AI backend logs |
|
|
37
|
-
| `--extra-ports` | Comma-separated local ports to expose through
|
|
47
|
+
| `--extra-ports` | Comma-separated local ports to expose through Helix Box |
|
|
38
48
|
|
|
39
49
|
## Configuration
|
|
40
50
|
|
|
41
|
-
By default, the CLI uses the public
|
|
51
|
+
By default, the CLI uses the public Helix Box services:
|
|
42
52
|
|
|
43
|
-
- Gateway: `https://
|
|
44
|
-
- Manager: `https://manager.
|
|
53
|
+
- Gateway: `https://helixbox-proxy.onrender.com`
|
|
54
|
+
- Manager: `https://helixbox-manager.onrender.com`
|
|
45
55
|
|
|
46
56
|
Override them with environment variables when developing against local or custom infrastructure:
|
|
47
57
|
|
|
48
58
|
```bash
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
npx
|
|
59
|
+
HELIXBOX_PROXY_URL=http://localhost:3001 \
|
|
60
|
+
HELIXBOX_MANAGER_URL=http://localhost:3002 \
|
|
61
|
+
npx helixbox-cli
|
|
52
62
|
```
|
|
53
63
|
|
|
54
64
|
Other useful environment variables:
|
|
55
65
|
|
|
56
66
|
| Variable | Description |
|
|
57
67
|
| --- | --- |
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
68
|
+
| `HELIXBOX_PROXY_URL` | Gateway/proxy URL |
|
|
69
|
+
| `HELIXBOX_MANAGER_URL` | Manager URL |
|
|
70
|
+
| `HELIXBOX_DEBUG` | Set to `1` for debug logging |
|
|
71
|
+
| `HELIXBOX_DEBUG_AI` | Set to `1` for AI backend debug logging |
|
|
62
72
|
| `NO_COLOR` | Disable colored terminal output |
|
|
63
73
|
| `FORCE_COLOR` | Force colored terminal output |
|
|
64
74
|
|
|
65
|
-
Session config is saved per project root in the OS-specific
|
|
75
|
+
Session config is saved per project root in the OS-specific Helix Box config directory:
|
|
66
76
|
|
|
67
|
-
- macOS: `~/Library/Application Support/
|
|
68
|
-
- Windows: `%APPDATA%\
|
|
69
|
-
- Linux: `$XDG_CONFIG_HOME/
|
|
77
|
+
- macOS: `~/Library/Application Support/helixbox/config.json`
|
|
78
|
+
- Windows: `%APPDATA%\helixbox\config.json`
|
|
79
|
+
- Linux: `$XDG_CONFIG_HOME/helixbox/config.json` or `~/.config/helixbox/config.json`
|
|
70
80
|
|
|
71
81
|
## Development
|
|
72
82
|
|
|
@@ -95,14 +105,14 @@ The package entrypoint is `dist/index.js`, generated from `src/index.ts`. `npm r
|
|
|
95
105
|
```text
|
|
96
106
|
src/
|
|
97
107
|
index.ts CLI entrypoint and local machine bridge
|
|
98
|
-
ai/ Codex
|
|
108
|
+
ai/ Codex, OpenCode, Claude Code and Hermes integration
|
|
99
109
|
transport/ Session transport protocol
|
|
100
110
|
libsodium-wrappers.d.ts
|
|
101
111
|
```
|
|
102
112
|
|
|
103
113
|
## Publishing
|
|
104
114
|
|
|
105
|
-
The package is published as `
|
|
115
|
+
The package is published as `helixbox-cli`. `prepublishOnly` runs the production build before publishing.
|
|
106
116
|
|
|
107
117
|
```bash
|
|
108
118
|
npm publish
|
package/dist/ai/cli.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AIProvider, AiEventEmitter, CodexPromptOptions, FileAttachment, MessageInfo, ModelSelector, ProviderInfo, SessionInfo } from "./interface.js";
|
|
2
|
+
import type { AiBackend } from "./index.js";
|
|
3
|
+
type CliBackend = Extract<AiBackend, "claude" | "hermes">;
|
|
4
|
+
export declare class CliAgentProvider implements AIProvider {
|
|
5
|
+
private readonly backend;
|
|
6
|
+
private emitter;
|
|
7
|
+
private sessions;
|
|
8
|
+
private shuttingDown;
|
|
9
|
+
constructor(backend: CliBackend);
|
|
10
|
+
init(): Promise<void>;
|
|
11
|
+
destroy(): Promise<void>;
|
|
12
|
+
subscribe(emitter: AiEventEmitter): () => void;
|
|
13
|
+
createSession(title?: string): Promise<{
|
|
14
|
+
session: SessionInfo;
|
|
15
|
+
}>;
|
|
16
|
+
listSessions(): Promise<{
|
|
17
|
+
sessions: unknown;
|
|
18
|
+
}>;
|
|
19
|
+
getSession(id: string): Promise<{
|
|
20
|
+
session: SessionInfo;
|
|
21
|
+
}>;
|
|
22
|
+
deleteSession(id: string): Promise<{
|
|
23
|
+
deleted: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
renameSession(id: string, title: string): Promise<{
|
|
26
|
+
session: SessionInfo;
|
|
27
|
+
}>;
|
|
28
|
+
getMessages(sessionId: string): Promise<{
|
|
29
|
+
messages: MessageInfo[];
|
|
30
|
+
}>;
|
|
31
|
+
statuses(): Promise<{
|
|
32
|
+
statuses: Record<string, unknown>;
|
|
33
|
+
}>;
|
|
34
|
+
prompt(sessionId: string, text: string, model?: ModelSelector, _agent?: string, files?: FileAttachment[], _codexOptions?: CodexPromptOptions): Promise<{
|
|
35
|
+
ack: true;
|
|
36
|
+
}>;
|
|
37
|
+
abort(sessionId: string): Promise<Record<string, never>>;
|
|
38
|
+
agents(): Promise<{
|
|
39
|
+
agents: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
providers(): Promise<ProviderInfo>;
|
|
42
|
+
setAuth(): Promise<Record<string, never>>;
|
|
43
|
+
command(): Promise<{
|
|
44
|
+
result: unknown;
|
|
45
|
+
}>;
|
|
46
|
+
revert(): Promise<Record<string, never>>;
|
|
47
|
+
unrevert(): Promise<Record<string, never>>;
|
|
48
|
+
share(): Promise<{
|
|
49
|
+
share: Record<string, never>;
|
|
50
|
+
}>;
|
|
51
|
+
permissionReply(): Promise<Record<string, never>>;
|
|
52
|
+
private argsForPrompt;
|
|
53
|
+
private cleanOutput;
|
|
54
|
+
private finishError;
|
|
55
|
+
private emit;
|
|
56
|
+
private requireSession;
|
|
57
|
+
private toSession;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
package/dist/ai/cli.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { prepareClaudeModelTools } from './model-tools.js';
|
|
4
|
+
const COMMANDS = { claude: "claude", hermes: "hermes" };
|
|
5
|
+
const DEBUG = process.env.HELIXBOX_DEBUG_AI === "1";
|
|
6
|
+
function commandAvailable(command) {
|
|
7
|
+
const result = spawnSync(command, ["--version"], {
|
|
8
|
+
stdio: "ignore",
|
|
9
|
+
shell: process.platform === "win32",
|
|
10
|
+
});
|
|
11
|
+
return !result.error;
|
|
12
|
+
}
|
|
13
|
+
function now() { return Date.now(); }
|
|
14
|
+
export class CliAgentProvider {
|
|
15
|
+
backend;
|
|
16
|
+
emitter = null;
|
|
17
|
+
sessions = new Map();
|
|
18
|
+
shuttingDown = false;
|
|
19
|
+
constructor(backend) {
|
|
20
|
+
this.backend = backend;
|
|
21
|
+
}
|
|
22
|
+
async init() {
|
|
23
|
+
if (!commandAvailable(COMMANDS[this.backend])) {
|
|
24
|
+
throw new Error(`${COMMANDS[this.backend]} CLI is not installed`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async destroy() {
|
|
28
|
+
this.shuttingDown = true;
|
|
29
|
+
for (const session of this.sessions.values())
|
|
30
|
+
session.process?.kill();
|
|
31
|
+
this.sessions.clear();
|
|
32
|
+
this.emitter = null;
|
|
33
|
+
}
|
|
34
|
+
subscribe(emitter) {
|
|
35
|
+
this.emitter = emitter;
|
|
36
|
+
this.shuttingDown = false;
|
|
37
|
+
return () => { this.emitter = null; };
|
|
38
|
+
}
|
|
39
|
+
async createSession(title) {
|
|
40
|
+
const timestamp = now();
|
|
41
|
+
const session = {
|
|
42
|
+
id: `${this.backend}-${crypto.randomUUID()}`,
|
|
43
|
+
title: title?.trim() || `${this.backend === "claude" ? "Claude Code" : "Hermes"} session`,
|
|
44
|
+
created: timestamp,
|
|
45
|
+
updated: timestamp,
|
|
46
|
+
messages: [],
|
|
47
|
+
};
|
|
48
|
+
this.sessions.set(session.id, session);
|
|
49
|
+
return { session: this.toSession(session) };
|
|
50
|
+
}
|
|
51
|
+
async listSessions() {
|
|
52
|
+
return { sessions: [...this.sessions.values()].map((session) => this.toSession(session)) };
|
|
53
|
+
}
|
|
54
|
+
async getSession(id) {
|
|
55
|
+
return { session: this.toSession(this.requireSession(id)) };
|
|
56
|
+
}
|
|
57
|
+
async deleteSession(id) {
|
|
58
|
+
const session = this.requireSession(id);
|
|
59
|
+
session.process?.kill();
|
|
60
|
+
return { deleted: this.sessions.delete(id) };
|
|
61
|
+
}
|
|
62
|
+
async renameSession(id, title) {
|
|
63
|
+
const session = this.requireSession(id);
|
|
64
|
+
session.title = title.trim() || session.title;
|
|
65
|
+
session.updated = now();
|
|
66
|
+
return { session: this.toSession(session) };
|
|
67
|
+
}
|
|
68
|
+
async getMessages(sessionId) {
|
|
69
|
+
return { messages: this.requireSession(sessionId).messages };
|
|
70
|
+
}
|
|
71
|
+
async statuses() {
|
|
72
|
+
const statuses = {};
|
|
73
|
+
for (const session of this.sessions.values()) {
|
|
74
|
+
statuses[session.id] = { type: session.process ? "busy" : "idle" };
|
|
75
|
+
}
|
|
76
|
+
return { statuses };
|
|
77
|
+
}
|
|
78
|
+
async prompt(sessionId, text, model, _agent, files = [], _codexOptions) {
|
|
79
|
+
const session = this.requireSession(sessionId);
|
|
80
|
+
if (session.process)
|
|
81
|
+
throw new Error(`Session ${sessionId} is already running`);
|
|
82
|
+
const userId = crypto.randomUUID();
|
|
83
|
+
const assistantId = crypto.randomUUID();
|
|
84
|
+
const created = now();
|
|
85
|
+
const prompt = files.length > 0
|
|
86
|
+
? `${text}\n\nAttached files: ${files.map((file) => file.filename || file.url).join(", ")}`
|
|
87
|
+
: text;
|
|
88
|
+
session.messages.push({ id: userId, role: "user", parts: [{ type: "text", text: prompt }], time: { created, updated: created } });
|
|
89
|
+
this.emit("message.updated", { info: { sessionID: session.id, id: userId, role: "user", time: { created, updated: created } } });
|
|
90
|
+
this.emit("message.updated", { info: { sessionID: session.id, id: assistantId, role: "assistant", time: { created, updated: created } } });
|
|
91
|
+
this.emit("session.status", { sessionID: session.id, status: { type: "busy" } });
|
|
92
|
+
const args = this.argsForPrompt(prompt, model);
|
|
93
|
+
const modelTools = this.backend === 'claude' ? await prepareClaudeModelTools() : null;
|
|
94
|
+
if (modelTools)
|
|
95
|
+
args.push('--mcp-config', modelTools.filename, '--allowedTools', 'mcp__helixbox__*');
|
|
96
|
+
const child = spawn(COMMANDS[this.backend], args, {
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
env: { ...process.env, ...modelTools?.env, NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
99
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
100
|
+
shell: process.platform === "win32",
|
|
101
|
+
});
|
|
102
|
+
session.process = child;
|
|
103
|
+
session.updated = now();
|
|
104
|
+
let stdout = "";
|
|
105
|
+
let stderr = "";
|
|
106
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
107
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
108
|
+
child.on("error", (error) => { void modelTools?.cleanup(); this.finishError(session, assistantId, error.message); });
|
|
109
|
+
child.on("close", (code) => {
|
|
110
|
+
void modelTools?.cleanup();
|
|
111
|
+
session.process = undefined;
|
|
112
|
+
if (this.shuttingDown)
|
|
113
|
+
return;
|
|
114
|
+
const output = this.cleanOutput(stdout || stderr);
|
|
115
|
+
if (code !== 0 && !output) {
|
|
116
|
+
this.finishError(session, assistantId, `${this.backend} exited with code ${code ?? "unknown"}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const message = {
|
|
120
|
+
id: assistantId,
|
|
121
|
+
role: "assistant",
|
|
122
|
+
parts: [{ type: "text", text: output || "(No output)" }],
|
|
123
|
+
time: { created, updated: now() },
|
|
124
|
+
};
|
|
125
|
+
session.messages.push(message);
|
|
126
|
+
session.updated = now();
|
|
127
|
+
this.emit("message.part.updated", { sessionID: session.id, messageID: assistantId, part: { id: `${assistantId}-text`, sessionID: session.id, messageID: assistantId, type: "text", text: output || "(No output)" } });
|
|
128
|
+
this.emit("session.status", { sessionID: session.id, status: { type: "idle" } });
|
|
129
|
+
});
|
|
130
|
+
if (DEBUG)
|
|
131
|
+
console.log(`[ai] started ${this.backend} prompt for ${session.id}`);
|
|
132
|
+
return { ack: true };
|
|
133
|
+
}
|
|
134
|
+
async abort(sessionId) {
|
|
135
|
+
const session = this.requireSession(sessionId);
|
|
136
|
+
session.process?.kill();
|
|
137
|
+
session.process = undefined;
|
|
138
|
+
this.emit("session.status", { sessionID: session.id, status: { type: "idle" } });
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
async agents() {
|
|
142
|
+
return { agents: [{ name: this.backend === "claude" ? "Claude Code" : "Hermes", mode: "default", description: "Installed local CLI agent" }] };
|
|
143
|
+
}
|
|
144
|
+
async providers() {
|
|
145
|
+
return { providers: [{ id: this.backend, name: this.backend === "claude" ? "Claude Code" : "Hermes", models: {} }], default: {} };
|
|
146
|
+
}
|
|
147
|
+
async setAuth() {
|
|
148
|
+
throw new Error(`${COMMANDS[this.backend]} authentication is managed by its local CLI`);
|
|
149
|
+
}
|
|
150
|
+
async command() { throw new Error(`${COMMANDS[this.backend]} command execution is not supported by HelixBox`); }
|
|
151
|
+
async revert() { throw new Error(`${COMMANDS[this.backend]} revert is not supported by HelixBox`); }
|
|
152
|
+
async unrevert() { throw new Error(`${COMMANDS[this.backend]} unrevert is not supported by HelixBox`); }
|
|
153
|
+
async share() { throw new Error(`${COMMANDS[this.backend]} sharing is not supported by HelixBox`); }
|
|
154
|
+
async permissionReply() { throw new Error(`${COMMANDS[this.backend]} permissions are handled by the CLI`); }
|
|
155
|
+
argsForPrompt(prompt, model) {
|
|
156
|
+
if (this.backend === "claude") {
|
|
157
|
+
const args = ["-p", prompt, "--output-format", "json", "--permission-mode", "plan"];
|
|
158
|
+
if (model?.modelID)
|
|
159
|
+
args.push("--model", model.modelID);
|
|
160
|
+
return args;
|
|
161
|
+
}
|
|
162
|
+
const args = ["chat", "-q", prompt];
|
|
163
|
+
if (model?.modelID)
|
|
164
|
+
args.push("--model", model.modelID);
|
|
165
|
+
return args;
|
|
166
|
+
}
|
|
167
|
+
cleanOutput(output) {
|
|
168
|
+
const trimmed = output.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").trim();
|
|
169
|
+
if (this.backend !== "claude")
|
|
170
|
+
return trimmed;
|
|
171
|
+
try {
|
|
172
|
+
const parsed = JSON.parse(trimmed);
|
|
173
|
+
if (parsed.is_error)
|
|
174
|
+
throw new Error(String(parsed.result || parsed.subtype || "Claude Code failed"));
|
|
175
|
+
return typeof parsed.result === "string" ? parsed.result : trimmed;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return trimmed;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
finishError(session, assistantId, error) {
|
|
182
|
+
session.process = undefined;
|
|
183
|
+
this.emit("prompt_error", { sessionID: session.id, error });
|
|
184
|
+
this.emit("session.status", { sessionID: session.id, status: { type: "idle" } });
|
|
185
|
+
if (DEBUG)
|
|
186
|
+
console.warn(`[ai] ${this.backend} prompt failed: ${error}`);
|
|
187
|
+
void assistantId;
|
|
188
|
+
}
|
|
189
|
+
emit(type, properties) { this.emitter?.({ type, properties }); }
|
|
190
|
+
requireSession(id) { const session = this.sessions.get(id); if (!session)
|
|
191
|
+
throw new Error(`Session ${id} not found`); return session; }
|
|
192
|
+
toSession(session) { return { id: session.id, title: session.title, time: { created: session.created, updated: session.updated } }; }
|
|
193
|
+
}
|
package/dist/ai/codex.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as crypto from "crypto";
|
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { spawn } from "child_process";
|
|
7
7
|
import { createInterface } from "readline";
|
|
8
|
+
import { MODEL_TOOL_DEFINITIONS, modelTools } from './model-tools.js';
|
|
8
9
|
const THREAD_LIST_SOURCE_KINDS = ["cli", "vscode", "appServer", "exec", "unknown"];
|
|
9
10
|
const CODEX_AGENTS = [
|
|
10
11
|
{
|
|
@@ -114,6 +115,7 @@ export class CodexProvider {
|
|
|
114
115
|
const result = await this.call("thread/start", {
|
|
115
116
|
cwd: process.cwd(),
|
|
116
117
|
persistExtendedHistory: true,
|
|
118
|
+
dynamicTools: MODEL_TOOL_DEFINITIONS.map(tool => ({ type: 'function', ...tool })),
|
|
117
119
|
});
|
|
118
120
|
const threadObject = this.extractThreadObject(result);
|
|
119
121
|
const threadId = this.extractThreadId(threadObject);
|
|
@@ -498,6 +500,14 @@ export class CodexProvider {
|
|
|
498
500
|
}
|
|
499
501
|
handleServerRequest(method, requestId, params) {
|
|
500
502
|
const session = this.resolveSessionFromPayload(params);
|
|
503
|
+
if (method === 'item/tool/call') {
|
|
504
|
+
void modelTools.call(String(params.tool), params.arguments ?? {}).then(value => {
|
|
505
|
+
this.send({ jsonrpc: '2.0', id: requestId, result: { success: true, contentItems: [{ type: 'inputText', text: JSON.stringify(value) }] } });
|
|
506
|
+
}).catch(error => {
|
|
507
|
+
this.send({ jsonrpc: '2.0', id: requestId, result: { success: false, contentItems: [{ type: 'inputText', text: error instanceof Error ? error.message : 'Model tool failed' }] } });
|
|
508
|
+
});
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
501
511
|
if (method === "item/tool/requestUserInput") {
|
|
502
512
|
const questionRequestId = String(requestId);
|
|
503
513
|
const callId = this.extractItemId(params) ?? undefined;
|
|
@@ -1178,6 +1188,7 @@ export class CodexProvider {
|
|
|
1178
1188
|
const params = {
|
|
1179
1189
|
threadId,
|
|
1180
1190
|
persistExtendedHistory: true,
|
|
1191
|
+
dynamicTools: MODEL_TOOL_DEFINITIONS.map(tool => ({ type: 'function', ...tool })),
|
|
1181
1192
|
};
|
|
1182
1193
|
if (session?.cwd) {
|
|
1183
1194
|
params.cwd = session.cwd;
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AiEvent, ModelSelector, FileAttachment, CodexPromptOptions } from "./interface.js";
|
|
2
|
-
export type AiBackend = "opencode" | "codex";
|
|
2
|
+
export type AiBackend = "opencode" | "codex" | "claude" | "hermes";
|
|
3
|
+
export declare const AI_BACKENDS: readonly AiBackend[];
|
|
4
|
+
export declare function isAiBackend(value: unknown): value is AiBackend;
|
|
3
5
|
export declare class AiManager {
|
|
4
6
|
private _providers;
|
|
5
7
|
private _available;
|
package/dist/ai/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
// AI manager — runs
|
|
1
|
+
// AI manager — runs every installed provider simultaneously and routes calls
|
|
2
2
|
// by the `backend` field in each request. Backends that fail to init are
|
|
3
3
|
// skipped gracefully; the available list is exposed to the app.
|
|
4
|
+
export const AI_BACKENDS = ["opencode", "codex", "claude", "hermes"];
|
|
5
|
+
export function isAiBackend(value) {
|
|
6
|
+
return typeof value === "string" && AI_BACKENDS.includes(value);
|
|
7
|
+
}
|
|
4
8
|
const DEBUG_MODE = process.env.HELIXBOX_DEBUG === "1" || process.env.HELIXBOX_DEBUG_AI === "1";
|
|
5
9
|
export class AiManager {
|
|
6
10
|
_providers = {};
|
|
@@ -9,6 +13,8 @@ export class AiManager {
|
|
|
9
13
|
await Promise.allSettled([
|
|
10
14
|
this.tryInit("opencode"),
|
|
11
15
|
this.tryInit("codex"),
|
|
16
|
+
this.tryInit("claude"),
|
|
17
|
+
this.tryInit("hermes"),
|
|
12
18
|
]);
|
|
13
19
|
if (this._available.length === 0) {
|
|
14
20
|
console.warn("[ai] No AI backends available. CLI will continue without AI features.");
|
|
@@ -26,12 +32,18 @@ export class AiManager {
|
|
|
26
32
|
await p.init();
|
|
27
33
|
this._providers.opencode = p;
|
|
28
34
|
}
|
|
29
|
-
else {
|
|
35
|
+
else if (backend === "codex") {
|
|
30
36
|
const { CodexProvider } = await import("./codex.js");
|
|
31
37
|
const p = new CodexProvider();
|
|
32
38
|
await p.init();
|
|
33
39
|
this._providers.codex = p;
|
|
34
40
|
}
|
|
41
|
+
else {
|
|
42
|
+
const { CliAgentProvider } = await import("./cli.js");
|
|
43
|
+
const p = new CliAgentProvider(backend);
|
|
44
|
+
await p.init();
|
|
45
|
+
this._providers[backend] = p;
|
|
46
|
+
}
|
|
35
47
|
this._available.push(backend);
|
|
36
48
|
}
|
|
37
49
|
catch (err) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createModelToolClient, MODEL_TOOL_DEFINITIONS } from './model-tools.js';
|
|
2
|
+
const client = createModelToolClient(() => process.env.HELIXBOX_MODEL_PASSWORD ? { managerUrl: process.env.HELIXBOX_MODEL_MANAGER || 'https://helixbox-manager.onrender.com', password: process.env.HELIXBOX_MODEL_PASSWORD } : null);
|
|
3
|
+
let pending = '', active = 0;
|
|
4
|
+
process.stdin.setEncoding('utf8');
|
|
5
|
+
const send = (value) => process.stdout.write(JSON.stringify(value) + '\n');
|
|
6
|
+
async function handle(raw) {
|
|
7
|
+
let message;
|
|
8
|
+
try {
|
|
9
|
+
message = JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Invalid JSON' } });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!message || message.jsonrpc !== '2.0' || message.id === undefined)
|
|
16
|
+
return;
|
|
17
|
+
if (++active > 4) {
|
|
18
|
+
active--;
|
|
19
|
+
send({ jsonrpc: '2.0', id: message.id, error: { code: -32000, message: 'Model tool is busy' } });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
let result;
|
|
24
|
+
if (message.method === 'initialize')
|
|
25
|
+
result = { protocolVersion: ['2025-11-25', '2025-06-18', '2024-11-05'].includes(message.params?.protocolVersion) ? message.params.protocolVersion : '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'helixbox-model-tools', version: '1.0.0' } };
|
|
26
|
+
else if (message.method === 'ping')
|
|
27
|
+
result = {};
|
|
28
|
+
else if (message.method === 'tools/list')
|
|
29
|
+
result = { tools: MODEL_TOOL_DEFINITIONS };
|
|
30
|
+
else if (message.method === 'tools/call') {
|
|
31
|
+
try {
|
|
32
|
+
result = { content: [{ type: 'text', text: JSON.stringify(await client.call(message.params?.name, message.params?.arguments ?? {})) }] };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
result = { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Model tool failed' }] };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
send({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: 'Method not found' } });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
send({ jsonrpc: '2.0', id: message.id, result });
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
active--;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
process.stdin.on('data', (chunk) => {
|
|
49
|
+
pending += chunk;
|
|
50
|
+
let newline;
|
|
51
|
+
while ((newline = pending.indexOf('\n')) >= 0) {
|
|
52
|
+
const line = pending.slice(0, newline);
|
|
53
|
+
pending = pending.slice(newline + 1);
|
|
54
|
+
if (Buffer.byteLength(line) > 32768) {
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
process.stdin.destroy();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (line.trim())
|
|
60
|
+
void handle(line);
|
|
61
|
+
}
|
|
62
|
+
if (Buffer.byteLength(pending) > 32768) {
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
process.stdin.destroy();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface ModelToolConnection {
|
|
2
|
+
managerUrl: string;
|
|
3
|
+
password: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function configureModelTools(resolve: () => ModelToolConnection | null): void;
|
|
6
|
+
export declare function modelToolConnection(): ModelToolConnection | null;
|
|
7
|
+
export declare function prepareClaudeModelTools(): Promise<{
|
|
8
|
+
filename: string;
|
|
9
|
+
env: {
|
|
10
|
+
HELIXBOX_MODEL_MANAGER: string;
|
|
11
|
+
HELIXBOX_MODEL_PASSWORD: string;
|
|
12
|
+
};
|
|
13
|
+
cleanup: () => Promise<void>;
|
|
14
|
+
} | null>;
|
|
15
|
+
export declare const MODEL_TOOL_DEFINITIONS: {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: string;
|
|
20
|
+
properties: Record<string, unknown>;
|
|
21
|
+
required: string[];
|
|
22
|
+
additionalProperties: boolean;
|
|
23
|
+
};
|
|
24
|
+
}[];
|
|
25
|
+
export declare function createModelToolClient(resolve: () => ModelToolConnection | null, fetcher?: typeof fetch, workspace?: string): {
|
|
26
|
+
call(name: string, args: unknown): Promise<unknown>;
|
|
27
|
+
};
|
|
28
|
+
export declare const modelTools: {
|
|
29
|
+
call(name: string, args: unknown): Promise<unknown>;
|
|
30
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { mkdir, lstat, realpath, writeFile, readFile, unlink } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
let connection = () => null;
|
|
6
|
+
export function configureModelTools(resolve) { connection = resolve; }
|
|
7
|
+
export function modelToolConnection() { return connection(); }
|
|
8
|
+
export async function prepareClaudeModelTools() {
|
|
9
|
+
const current = connection();
|
|
10
|
+
if (!current)
|
|
11
|
+
return null;
|
|
12
|
+
const filename = `.helixbox-mcp-${randomUUID()}.json`, file = path.join(process.cwd(), filename);
|
|
13
|
+
// No credential is written to the project; Claude expands these inherited variables at launch.
|
|
14
|
+
await writeFile(file, JSON.stringify({ mcpServers: { helixbox: { type: 'stdio', command: process.execPath, args: [fileURLToPath(new URL('./model-mcp.js', import.meta.url))],
|
|
15
|
+
env: { HELIXBOX_MODEL_MANAGER: '${HELIXBOX_MODEL_MANAGER}', HELIXBOX_MODEL_PASSWORD: '${HELIXBOX_MODEL_PASSWORD}' } } } }), { flag: 'wx', mode: 0o600 });
|
|
16
|
+
return { filename, env: { HELIXBOX_MODEL_MANAGER: current.managerUrl, HELIXBOX_MODEL_PASSWORD: current.password }, cleanup: () => unlink(file).catch(() => { }) };
|
|
17
|
+
}
|
|
18
|
+
const id = (value) => { if (typeof value !== 'string' || !/^[a-f0-9-]{36}$/.test(value))
|
|
19
|
+
throw new Error('Invalid purchase or mandate ID'); return value; };
|
|
20
|
+
const string = (value, max) => { if (typeof value !== 'string' || !value || Buffer.byteLength(value) > max)
|
|
21
|
+
throw new Error('Invalid model tool input'); return value; };
|
|
22
|
+
const schema = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
23
|
+
const identifier = { type: 'string', pattern: '^[a-f0-9-]{36}$' };
|
|
24
|
+
export const MODEL_TOOL_DEFINITIONS = [
|
|
25
|
+
{ name: 'helixbox_model_catalog', description: 'Discover live chat, image and speech models. Listing is free; use a quote to get a fixed USDC price.', inputSchema: schema({ search: { type: 'string', maxLength: 100 }, kind: { type: 'string', enum: ['chat', 'image', 'voice'] } }) },
|
|
26
|
+
{ name: 'helixbox_model_mandates', description: 'Read this workspace user’s approved model allowlists, remaining budgets and expiry. Only active mandates can buy tools.', inputSchema: schema({}) },
|
|
27
|
+
{ name: 'helixbox_model_quote', description: 'Quote a bounded model call within an active user-approved mandate. Sends only the supplied prompt to the model service. Never include secrets.', inputSchema: schema({ mandateId: identifier, modelId: { type: 'string', maxLength: 256 }, kind: { type: 'string', enum: ['chat', 'image', 'voice'] }, prompt: { type: 'string', maxLength: 8000 }, maxOutputTokens: { type: 'integer', minimum: 1, maximum: 2048 }, voice: { type: 'string', maxLength: 128 } }, ['mandateId', 'modelId', 'kind', 'prompt']) },
|
|
28
|
+
{ name: 'helixbox_model_purchase', description: 'Queue an existing quote for the paired foreground phone to authorize under its mandate. No agent/server wallet key. Do not repurchase an uncertain operation; poll status.', inputSchema: schema({ purchaseId: identifier, mandateId: identifier }, ['purchaseId', 'mandateId']) },
|
|
29
|
+
{ name: 'helixbox_model_status', description: 'Read the original purchase result without another payment. Completed images/audio are saved as real project artifacts. Pending means wait for the phone, not buy again.', inputSchema: schema({ purchaseId: identifier }, ['purchaseId']) },
|
|
30
|
+
];
|
|
31
|
+
async function artifact(job, workspace) {
|
|
32
|
+
const result = job.result;
|
|
33
|
+
if (!result?.base64)
|
|
34
|
+
return job;
|
|
35
|
+
const suffix = result.kind === 'image' && result.mediaType === 'image/png' ? '.png' : result.kind === 'voice' && result.mediaType === 'audio/mpeg' ? '.mp3' : null;
|
|
36
|
+
if (!suffix || typeof result.base64 !== 'string' || result.base64.length > 16777216 || !/^[A-Za-z0-9+/]*={0,2}$/.test(result.base64))
|
|
37
|
+
throw new Error('Invalid model artifact');
|
|
38
|
+
const root = await realpath(workspace), directory = path.join(root, '.helixbox-model-results');
|
|
39
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
40
|
+
if ((await lstat(directory)).isSymbolicLink() || await realpath(directory) !== directory)
|
|
41
|
+
throw new Error('Model artifact directory cannot be a symlink');
|
|
42
|
+
const file = path.join(directory, id(job.id) + suffix), bytes = Buffer.from(result.base64, 'base64');
|
|
43
|
+
try {
|
|
44
|
+
await writeFile(file, bytes, { flag: 'wx', mode: 0o600 });
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code !== 'EEXIST')
|
|
48
|
+
throw error;
|
|
49
|
+
if ((await lstat(file)).isSymbolicLink() || createHash('sha256').update(await readFile(file)).digest('hex') !== createHash('sha256').update(bytes).digest('hex'))
|
|
50
|
+
throw new Error('Existing model artifact differs; refusing to overwrite');
|
|
51
|
+
}
|
|
52
|
+
const { base64, ...metadata } = result;
|
|
53
|
+
return { ...job, result: { ...metadata, artifactPath: file } };
|
|
54
|
+
}
|
|
55
|
+
export function createModelToolClient(resolve, fetcher = fetch, workspace = process.cwd()) {
|
|
56
|
+
async function request(route, body) {
|
|
57
|
+
const current = resolve();
|
|
58
|
+
if (!current)
|
|
59
|
+
throw new Error('Pair a HelixBox workspace first');
|
|
60
|
+
const url = new URL(current.managerUrl);
|
|
61
|
+
if (url.protocol !== 'https:')
|
|
62
|
+
throw new Error('Model tools require an HTTPS manager');
|
|
63
|
+
const response = await fetcher(url.origin + '/v2/models' + route, { method: body === undefined ? 'GET' : 'POST', headers: { Authorization: `Bearer ${current.password}`, 'Content-Type': 'application/json' },
|
|
64
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }), signal: AbortSignal.timeout(120000) });
|
|
65
|
+
if (resolve()?.password !== current.password)
|
|
66
|
+
throw new Error('Workspace pairing changed; discard the old result');
|
|
67
|
+
const value = await response.json().catch(() => null);
|
|
68
|
+
if (!response.ok)
|
|
69
|
+
throw new Error(value?.error || `Model operation failed (${response.status})`);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
return { async call(name, args) {
|
|
73
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
74
|
+
throw new Error('Invalid model tool arguments');
|
|
75
|
+
const input = args;
|
|
76
|
+
if (!MODEL_TOOL_DEFINITIONS.some(tool => tool.name === name))
|
|
77
|
+
throw new Error('Unknown model tool');
|
|
78
|
+
if (name === 'helixbox_model_catalog') {
|
|
79
|
+
if (input.kind !== undefined && !['chat', 'image', 'voice'].includes(String(input.kind)))
|
|
80
|
+
throw new Error('Invalid model kind');
|
|
81
|
+
const search = input.search === undefined ? '' : string(input.search, 100).toLowerCase(), catalog = await request('');
|
|
82
|
+
return { paymentsEnabled: catalog.paymentsEnabled, models: catalog.models.filter((m) => (!input.kind || m.kind === input.kind) && `${m.id} ${m.name}`.toLowerCase().includes(search)).slice(0, 20) };
|
|
83
|
+
}
|
|
84
|
+
if (name === 'helixbox_model_mandates')
|
|
85
|
+
return request('/mandates');
|
|
86
|
+
if (name === 'helixbox_model_quote') {
|
|
87
|
+
const mandateId = id(input.mandateId), modelId = string(input.modelId, 256), prompt = string(input.prompt, 8000);
|
|
88
|
+
const { mandates } = await request('/mandates');
|
|
89
|
+
const mandate = mandates.find((m) => m.id === mandateId && m.state === 'active' && m.policy.expiresAt > Date.now());
|
|
90
|
+
if (!mandate?.policy.models.some((m) => m.id === modelId && m.kind === input.kind))
|
|
91
|
+
throw new Error('User has not approved this model in an active mandate');
|
|
92
|
+
const quoted = await request('/quotes', { modelId, kind: input.kind, prompt, consent: true, ...input.maxOutputTokens !== undefined ? { maxOutputTokens: input.maxOutputTokens } : {}, ...input.voice !== undefined ? { voice: input.voice } : {} });
|
|
93
|
+
if (BigInt(quoted.terms.amount) > BigInt(mandate.policy.perCallAmount))
|
|
94
|
+
throw new Error('Quoted price exceeds the user’s per-call cap');
|
|
95
|
+
return quoted;
|
|
96
|
+
}
|
|
97
|
+
const purchaseId = id(input.purchaseId);
|
|
98
|
+
if (name === 'helixbox_model_purchase')
|
|
99
|
+
return request(`/purchases/${purchaseId}/request`, { mandateId: id(input.mandateId) });
|
|
100
|
+
let job = await request(`/purchases/${purchaseId}`);
|
|
101
|
+
if (job.state === 'paid')
|
|
102
|
+
job = await request(`/purchases/${purchaseId}/resume`, {});
|
|
103
|
+
return artifact(job, workspace);
|
|
104
|
+
} };
|
|
105
|
+
}
|
|
106
|
+
export const modelTools = createModelToolClient(() => connection());
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { WebSocket } from "ws";
|
|
3
3
|
import qrcode from "qrcode-terminal";
|
|
4
4
|
import shell from "shelljs";
|
|
5
|
-
import { createAiManager } from "./ai/index.js";
|
|
5
|
+
import { createAiManager, isAiBackend } from "./ai/index.js";
|
|
6
|
+
import { configureModelTools } from './ai/model-tools.js';
|
|
6
7
|
import { V2SessionTransport } from "./transport/v2.js";
|
|
7
8
|
import Ignore from "ignore";
|
|
8
9
|
const ignore = Ignore.default;
|
|
@@ -14,8 +15,8 @@ import { randomBytes } from "crypto";
|
|
|
14
15
|
import { spawn, spawnSync, execSync } from "child_process";
|
|
15
16
|
import { createServer, createConnection } from "net";
|
|
16
17
|
import { createInterface } from "readline";
|
|
17
|
-
const DEFAULT_PROXY_URL = normalizeGatewayUrl(process.env.HELIXBOX_PROXY_URL || "https://
|
|
18
|
-
const MANAGER_URL = normalizeGatewayUrl(process.env.HELIXBOX_MANAGER_URL || "https://manager.
|
|
18
|
+
const DEFAULT_PROXY_URL = normalizeGatewayUrl(process.env.HELIXBOX_PROXY_URL || "https://helixbox-proxy.onrender.com");
|
|
19
|
+
const MANAGER_URL = normalizeGatewayUrl(process.env.HELIXBOX_MANAGER_URL || "https://helixbox-manager.onrender.com");
|
|
19
20
|
const CLI_ARGS = process.argv.slice(2);
|
|
20
21
|
function hasAnyFlag(args, ...flags) {
|
|
21
22
|
return flags.some((flag) => args.includes(flag));
|
|
@@ -30,23 +31,25 @@ import { createRequire } from "module";
|
|
|
30
31
|
const __require = createRequire(import.meta.url);
|
|
31
32
|
const VERSION = __require("../package.json").version;
|
|
32
33
|
const VERBOSE_AI_LOGS = process.env.HELIXBOX_DEBUG_AI === "1";
|
|
33
|
-
const PTY_RELEASE_BASE_URL = "https://github.com/
|
|
34
|
+
const PTY_RELEASE_BASE_URL = "https://github.com/devndesigner6/helix-box/releases/download/v0";
|
|
34
35
|
const AI_RUNTIME_INSTALL_CANDIDATES = {
|
|
35
36
|
opencode: ["opencode-ai", "@opencode-ai/cli", "opencode"],
|
|
36
37
|
codex: ["@openai/codex", "codex"],
|
|
38
|
+
claude: ["@anthropic-ai/claude-code", "claude"],
|
|
39
|
+
hermes: ["hermes-agent", "hermes"],
|
|
37
40
|
};
|
|
38
41
|
const PTY_RELEASES = {
|
|
39
42
|
"linux:x64": {
|
|
40
43
|
fileName: "helixbox-pty-linux-x8664-0",
|
|
41
|
-
url: `${PTY_RELEASE_BASE_URL}/
|
|
44
|
+
url: `${PTY_RELEASE_BASE_URL}/helixbox-pty-linux-x8664-0`,
|
|
42
45
|
},
|
|
43
46
|
"darwin:arm64": {
|
|
44
47
|
fileName: "helixbox-pty-macos-arm64-0",
|
|
45
|
-
url: `${PTY_RELEASE_BASE_URL}/
|
|
48
|
+
url: `${PTY_RELEASE_BASE_URL}/helixbox-pty-macos-arm64-0`,
|
|
46
49
|
},
|
|
47
50
|
"win32:x64": {
|
|
48
51
|
fileName: "helixbox-pty-windows-x8664-1.exe",
|
|
49
|
-
url: `${PTY_RELEASE_BASE_URL}/
|
|
52
|
+
url: `${PTY_RELEASE_BASE_URL}/helixbox-pty-windows-x8664-1.exe`,
|
|
50
53
|
},
|
|
51
54
|
};
|
|
52
55
|
// Root directory - sandbox all file operations to this
|
|
@@ -2702,7 +2705,8 @@ async function processMessage(message) {
|
|
|
2702
2705
|
case "ai": {
|
|
2703
2706
|
if (!aiManager)
|
|
2704
2707
|
throw Object.assign(new Error("AI manager not initialized"), { code: "EUNAVAILABLE" });
|
|
2705
|
-
const
|
|
2708
|
+
const requestedBackend = payload.backend;
|
|
2709
|
+
const backend = isAiBackend(requestedBackend) ? requestedBackend : "opencode";
|
|
2706
2710
|
switch (action) {
|
|
2707
2711
|
case "backends":
|
|
2708
2712
|
result = { backends: aiManager.availableBackends() };
|
|
@@ -3053,6 +3057,10 @@ function isCommandAvailable(command) {
|
|
|
3053
3057
|
}
|
|
3054
3058
|
return !err;
|
|
3055
3059
|
}
|
|
3060
|
+
function isBackendAvailable(backend) {
|
|
3061
|
+
const command = backend === "opencode" ? "opencode" : backend === "codex" ? "codex" : backend;
|
|
3062
|
+
return isCommandAvailable(command);
|
|
3063
|
+
}
|
|
3056
3064
|
function askYesNo(question, defaultValue = false) {
|
|
3057
3065
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
3058
3066
|
return Promise.resolve(false);
|
|
@@ -3081,7 +3089,7 @@ function installLatestNpmPackage(pkg) {
|
|
|
3081
3089
|
}
|
|
3082
3090
|
async function ensureAiCliRuntimes() {
|
|
3083
3091
|
const missingBackends = Object.keys(AI_RUNTIME_INSTALL_CANDIDATES)
|
|
3084
|
-
.filter((backend) => !
|
|
3092
|
+
.filter((backend) => !isBackendAvailable(backend));
|
|
3085
3093
|
if (missingBackends.length === 0)
|
|
3086
3094
|
return;
|
|
3087
3095
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -3095,7 +3103,7 @@ async function ensureAiCliRuntimes() {
|
|
|
3095
3103
|
return;
|
|
3096
3104
|
}
|
|
3097
3105
|
for (const backend of missingBackends) {
|
|
3098
|
-
if (
|
|
3106
|
+
if (isBackendAvailable(backend))
|
|
3099
3107
|
continue;
|
|
3100
3108
|
const candidates = AI_RUNTIME_INSTALL_CANDIDATES[backend];
|
|
3101
3109
|
let installed = false;
|
|
@@ -3103,7 +3111,7 @@ async function ensureAiCliRuntimes() {
|
|
|
3103
3111
|
console.log(`[ai] Installing ${backend} via npm package ${pkg}@latest...`);
|
|
3104
3112
|
if (!installLatestNpmPackage(pkg))
|
|
3105
3113
|
continue;
|
|
3106
|
-
if (
|
|
3114
|
+
if (isBackendAvailable(backend)) {
|
|
3107
3115
|
installed = true;
|
|
3108
3116
|
console.log(`[ai] ${backend} installed successfully.`);
|
|
3109
3117
|
break;
|
|
@@ -3322,6 +3330,7 @@ async function main() {
|
|
|
3322
3330
|
}
|
|
3323
3331
|
currentSessionCode = sessionCodeToUse;
|
|
3324
3332
|
currentSessionPassword = sessionPasswordToUse;
|
|
3333
|
+
configureModelTools(() => currentSessionPassword ? { managerUrl: MANAGER_URL, password: currentSessionPassword } : null);
|
|
3325
3334
|
if (usedSavedSession) {
|
|
3326
3335
|
const reattach = await claimReattach(sessionPasswordToUse);
|
|
3327
3336
|
currentPrimaryGateway = reattach.proxyUrl;
|
package/package.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helixbox-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Pair a laptop workspace with HelixBox on Android, with bounded model tools for Codex and Claude Code.",
|
|
5
|
+
"homepage": "https://helixboxide.xyz",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/devndesigner6/helix-box-solana.git",
|
|
9
|
+
"directory": "cli"
|
|
10
|
+
},
|
|
4
11
|
"author": [
|
|
5
12
|
{
|
|
6
13
|
"name": "Soham Bharambe",
|
|
@@ -18,7 +25,8 @@
|
|
|
18
25
|
},
|
|
19
26
|
"files": [
|
|
20
27
|
"dist",
|
|
21
|
-
"bin"
|
|
28
|
+
"bin",
|
|
29
|
+
"CHANGELOG.md"
|
|
22
30
|
],
|
|
23
31
|
"scripts": {
|
|
24
32
|
"build": "tsc && npm run prepare-bin",
|