lunel-cli 0.1.120 → 0.1.122
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 +109 -0
- package/dist/ai/index.d.ts +3 -0
- package/dist/ai/index.js +6 -0
- package/dist/ai/interface.d.ts +3 -0
- package/dist/ai/opencode.d.ts +4 -0
- package/dist/ai/opencode.js +32 -16
- package/dist/index.js +3 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Lunel CLI
|
|
2
|
+
|
|
3
|
+
Node.js CLI that connects a local machine to the Lunel mobile app through the Lunel 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
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 18 or newer
|
|
8
|
+
- npm
|
|
9
|
+
- Lunel mobile app for QR/session pairing
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Run the published package:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx lunel-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The CLI prints a QR code and session details. Scan the QR code with the Lunel app to connect to the current working directory.
|
|
20
|
+
|
|
21
|
+
Common options:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx lunel-cli --help
|
|
25
|
+
npx lunel-cli --new
|
|
26
|
+
npx lunel-cli --debug
|
|
27
|
+
npx lunel-cli --extra-ports 3000,8080
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
|
|
32
|
+
| Option | Description |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `-h`, `--help` | Show CLI help |
|
|
35
|
+
| `-n`, `--new` | Create a fresh session code instead of reusing the saved one |
|
|
36
|
+
| `-d`, `--debug` | Enable verbose CLI and AI backend logs |
|
|
37
|
+
| `--extra-ports` | Comma-separated local ports to expose through Lunel |
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
By default, the CLI uses the public Lunel services:
|
|
42
|
+
|
|
43
|
+
- Gateway: `https://gateway.lunel.dev`
|
|
44
|
+
- Manager: `https://manager.lunel.dev`
|
|
45
|
+
|
|
46
|
+
Override them with environment variables when developing against local or custom infrastructure:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
LUNEL_PROXY_URL=http://localhost:3001 \
|
|
50
|
+
LUNEL_MANAGER_URL=http://localhost:3002 \
|
|
51
|
+
npx lunel-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Other useful environment variables:
|
|
55
|
+
|
|
56
|
+
| Variable | Description |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `LUNEL_PROXY_URL` | Gateway/proxy URL |
|
|
59
|
+
| `LUNEL_MANAGER_URL` | Manager URL |
|
|
60
|
+
| `LUNEL_DEBUG` | Set to `1` for debug logging |
|
|
61
|
+
| `LUNEL_DEBUG_AI` | Set to `1` for AI backend debug logging |
|
|
62
|
+
| `NO_COLOR` | Disable colored terminal output |
|
|
63
|
+
| `FORCE_COLOR` | Force colored terminal output |
|
|
64
|
+
|
|
65
|
+
Session config is saved per project root in the OS-specific Lunel config directory:
|
|
66
|
+
|
|
67
|
+
- macOS: `~/Library/Application Support/lunel/config.json`
|
|
68
|
+
- Windows: `%APPDATA%\lunel\config.json`
|
|
69
|
+
- Linux: `$XDG_CONFIG_HOME/lunel/config.json` or `~/.config/lunel/config.json`
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
Install dependencies:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Build the CLI:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm run build
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Run from source output:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm run dev
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The package entrypoint is `dist/index.js`, generated from `src/index.ts`. `npm run build` compiles TypeScript and marks the generated entrypoint executable.
|
|
92
|
+
|
|
93
|
+
## Project Layout
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
src/
|
|
97
|
+
index.ts CLI entrypoint and local machine bridge
|
|
98
|
+
ai/ Codex/OpenCode provider integration
|
|
99
|
+
transport/ Session transport protocol
|
|
100
|
+
libsodium-wrappers.d.ts
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Publishing
|
|
104
|
+
|
|
105
|
+
The package is published as `lunel-cli`. `prepublishOnly` runs the production build before publishing.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm publish
|
|
109
|
+
```
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -29,6 +29,9 @@ export declare class AiManager {
|
|
|
29
29
|
getMessages(backend: AiBackend, sessionId: string): Promise<{
|
|
30
30
|
messages: import("./interface.js").MessageInfo[];
|
|
31
31
|
}>;
|
|
32
|
+
statuses(backend: AiBackend): Promise<{
|
|
33
|
+
statuses: Record<string, unknown>;
|
|
34
|
+
}>;
|
|
32
35
|
prompt(backend: AiBackend, sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
|
|
33
36
|
ack: true;
|
|
34
37
|
}>;
|
package/dist/ai/index.js
CHANGED
|
@@ -74,6 +74,12 @@ export class AiManager {
|
|
|
74
74
|
deleteSession(backend, id) { return this.get(backend).deleteSession(id); }
|
|
75
75
|
renameSession(backend, id, title) { return this.get(backend).renameSession(id, title); }
|
|
76
76
|
getMessages(backend, sessionId) { return this.get(backend).getMessages(sessionId); }
|
|
77
|
+
async statuses(backend) {
|
|
78
|
+
const provider = this.get(backend);
|
|
79
|
+
if (!provider.statuses)
|
|
80
|
+
return { statuses: {} };
|
|
81
|
+
return provider.statuses();
|
|
82
|
+
}
|
|
77
83
|
prompt(backend, sessionId, text, model, agent, files, codexOptions) {
|
|
78
84
|
this.get(backend).setActiveSession?.(sessionId);
|
|
79
85
|
return this.get(backend).prompt(sessionId, text, model, agent, files, codexOptions);
|
package/dist/ai/interface.d.ts
CHANGED
|
@@ -67,6 +67,9 @@ export interface AIProvider {
|
|
|
67
67
|
getMessages(sessionId: string): Promise<{
|
|
68
68
|
messages: MessageInfo[];
|
|
69
69
|
}>;
|
|
70
|
+
statuses?(): Promise<{
|
|
71
|
+
statuses: Record<string, unknown>;
|
|
72
|
+
}>;
|
|
70
73
|
prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
|
|
71
74
|
ack: true;
|
|
72
75
|
}>;
|
package/dist/ai/opencode.d.ts
CHANGED
|
@@ -33,6 +33,9 @@ export declare class OpenCodeProvider implements AIProvider {
|
|
|
33
33
|
getMessages(sessionId: string): Promise<{
|
|
34
34
|
messages: MessageInfo[];
|
|
35
35
|
}>;
|
|
36
|
+
statuses(): Promise<{
|
|
37
|
+
statuses: Record<string, unknown>;
|
|
38
|
+
}>;
|
|
36
39
|
prompt(sessionId: string, text: string, model?: ModelSelector, agent?: string, files?: FileAttachment[], codexOptions?: CodexPromptOptions): Promise<{
|
|
37
40
|
ack: true;
|
|
38
41
|
}>;
|
|
@@ -62,6 +65,7 @@ export declare class OpenCodeProvider implements AIProvider {
|
|
|
62
65
|
private refreshPendingQuestions;
|
|
63
66
|
private fetchOpenCodeJson;
|
|
64
67
|
private refreshSessionStatuses;
|
|
68
|
+
private fetchSessionStatuses;
|
|
65
69
|
private trackPermissionEvent;
|
|
66
70
|
private readString;
|
|
67
71
|
private asRecord;
|
package/dist/ai/opencode.js
CHANGED
|
@@ -102,10 +102,19 @@ function normalizeOpenCodePart(part) {
|
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
else if (status === "error") {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
if (metadata.interrupted === true) {
|
|
106
|
+
normalized.state = "completed";
|
|
107
|
+
normalized.interrupted = true;
|
|
108
|
+
const interruptedOutput = readString(metadata.output);
|
|
109
|
+
if (interruptedOutput)
|
|
110
|
+
normalized.output = interruptedOutput;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
normalized.error = readString(state.error) ?? "Tool failed";
|
|
114
|
+
const errorMessage = readString(state.error);
|
|
115
|
+
if (errorMessage)
|
|
116
|
+
normalized.output = errorMessage;
|
|
117
|
+
}
|
|
109
118
|
}
|
|
110
119
|
const attachments = Array.isArray(state.attachments) ? state.attachments : [];
|
|
111
120
|
if (attachments.length > 0) {
|
|
@@ -393,6 +402,9 @@ export class OpenCodeProvider {
|
|
|
393
402
|
throw err;
|
|
394
403
|
}
|
|
395
404
|
}
|
|
405
|
+
async statuses() {
|
|
406
|
+
return { statuses: await this.fetchSessionStatuses() };
|
|
407
|
+
}
|
|
396
408
|
// -------------------------------------------------------------------------
|
|
397
409
|
// Interaction
|
|
398
410
|
// -------------------------------------------------------------------------
|
|
@@ -797,10 +809,22 @@ export class OpenCodeProvider {
|
|
|
797
809
|
return response.json().catch(() => null);
|
|
798
810
|
}
|
|
799
811
|
async refreshSessionStatuses() {
|
|
812
|
+
const payload = await this.fetchSessionStatuses();
|
|
813
|
+
for (const [sessionId, status] of Object.entries(payload)) {
|
|
814
|
+
this.emitter?.({
|
|
815
|
+
type: "session.status",
|
|
816
|
+
properties: {
|
|
817
|
+
sessionID: sessionId,
|
|
818
|
+
status: status,
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async fetchSessionStatuses() {
|
|
800
824
|
const server = this.server;
|
|
801
825
|
const authHeader = this.authHeader;
|
|
802
826
|
if (!server || !authHeader) {
|
|
803
|
-
return;
|
|
827
|
+
return {};
|
|
804
828
|
}
|
|
805
829
|
const url = new URL("/session/status", server.url);
|
|
806
830
|
const response = await fetch(url, {
|
|
@@ -810,21 +834,13 @@ export class OpenCodeProvider {
|
|
|
810
834
|
},
|
|
811
835
|
});
|
|
812
836
|
if (!response.ok) {
|
|
813
|
-
return;
|
|
837
|
+
return {};
|
|
814
838
|
}
|
|
815
839
|
const payload = await response.json().catch(() => null);
|
|
816
840
|
if (!payload || typeof payload !== "object") {
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
for (const [sessionId, status] of Object.entries(payload)) {
|
|
820
|
-
this.emitter?.({
|
|
821
|
-
type: "session.status",
|
|
822
|
-
properties: {
|
|
823
|
-
sessionID: sessionId,
|
|
824
|
-
status: status,
|
|
825
|
-
},
|
|
826
|
-
});
|
|
841
|
+
return {};
|
|
827
842
|
}
|
|
843
|
+
return payload;
|
|
828
844
|
}
|
|
829
845
|
trackPermissionEvent(type, properties) {
|
|
830
846
|
if (type === "permission.updated") {
|
package/dist/index.js
CHANGED
|
@@ -2728,6 +2728,9 @@ async function processMessage(message) {
|
|
|
2728
2728
|
case "getMessages":
|
|
2729
2729
|
result = await aiManager.getMessages(backend, payload.id);
|
|
2730
2730
|
break;
|
|
2731
|
+
case "statuses":
|
|
2732
|
+
result = await aiManager.statuses(backend);
|
|
2733
|
+
break;
|
|
2731
2734
|
case "abort":
|
|
2732
2735
|
result = await aiManager.abort(backend, payload.sessionId);
|
|
2733
2736
|
break;
|