gambi 0.2.3
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 +1 -0
- package/package.json +60 -0
- package/src/cli.ts +32 -0
- package/src/commands/create.ts +160 -0
- package/src/commands/join.ts +937 -0
- package/src/commands/list.ts +104 -0
- package/src/commands/monitor.ts +27 -0
- package/src/commands/serve.ts +116 -0
- package/src/utils/network-endpoint.test.ts +40 -0
- package/src/utils/network-endpoint.ts +136 -0
- package/src/utils/option.ts +10 -0
- package/src/utils/prompt.ts +24 -0
- package/src/utils/runtime-config.ts +211 -0
- package/src/utils/specs.ts +149 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# `gambi`
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gambi",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "CLI for Gambi - Share local LLMs across your network",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"llm",
|
|
7
|
+
"openresponses",
|
|
8
|
+
"open-responses",
|
|
9
|
+
"ollama",
|
|
10
|
+
"ai",
|
|
11
|
+
"cli",
|
|
12
|
+
"local-ai",
|
|
13
|
+
"local-llm",
|
|
14
|
+
"lm-studio"
|
|
15
|
+
],
|
|
16
|
+
"author": "Arthur BM",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/arthurbm/gambi.git",
|
|
21
|
+
"directory": "packages/cli"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/arthurbm/gambi",
|
|
24
|
+
"bugs": "https://github.com/arthurbm/gambi/issues",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"gambi": "./src/cli.ts"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
"./serve": "./src/commands/serve.ts",
|
|
31
|
+
"./create": "./src/commands/create.ts",
|
|
32
|
+
"./join": "./src/commands/join.ts",
|
|
33
|
+
"./list": "./src/commands/list.ts",
|
|
34
|
+
"./monitor": "./src/commands/monitor.ts"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"dev": "bun run src/cli.ts",
|
|
42
|
+
"build": "bun run build.ts",
|
|
43
|
+
"check-types": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@clack/prompts": "^1.1.0",
|
|
47
|
+
"@gambi/core": "workspace:*",
|
|
48
|
+
"clipanion": "^4.0.0-rc.4",
|
|
49
|
+
"nanoid": "^5.1.5"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/bun": "latest"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"typescript": "^5"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Builtins, Cli } from "./utils/option.ts";
|
|
3
|
+
import { CreateCommand } from "./commands/create.ts";
|
|
4
|
+
import { JoinCommand } from "./commands/join.ts";
|
|
5
|
+
import { ListCommand } from "./commands/list.ts";
|
|
6
|
+
import { MonitorCommand } from "./commands/monitor.ts";
|
|
7
|
+
import { ServeCommand } from "./commands/serve.ts";
|
|
8
|
+
|
|
9
|
+
const cli = new Cli({
|
|
10
|
+
binaryLabel: "gambi",
|
|
11
|
+
binaryName: "gambi",
|
|
12
|
+
binaryVersion: "0.0.1",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
cli.register(ServeCommand);
|
|
16
|
+
cli.register(CreateCommand);
|
|
17
|
+
cli.register(JoinCommand);
|
|
18
|
+
cli.register(ListCommand);
|
|
19
|
+
cli.register(MonitorCommand);
|
|
20
|
+
cli.register(Builtins.HelpCommand);
|
|
21
|
+
cli.register(Builtins.VersionCommand);
|
|
22
|
+
|
|
23
|
+
// TODO: Re-add TUI support as a separate optional package.
|
|
24
|
+
// Previously, running `gambi` with no args launched the TUI,
|
|
25
|
+
// but bundling OpenTUI + React inflated the binary from ~50MB to ~110MB.
|
|
26
|
+
// See: https://github.com/arthurbm/gambi/issues — create issue for TUI separation
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
if (args.length === 0) {
|
|
29
|
+
cli.runExit(["--help"]);
|
|
30
|
+
} else {
|
|
31
|
+
cli.runExit(args);
|
|
32
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { intro, outro, password as passwordPrompt, text } from "@clack/prompts";
|
|
2
|
+
import { Command, Option } from "../utils/option.ts";
|
|
3
|
+
import { handleCancel, isInteractive } from "../utils/prompt.ts";
|
|
4
|
+
import {
|
|
5
|
+
hasRuntimeConfig,
|
|
6
|
+
loadRuntimeConfigFile,
|
|
7
|
+
promptRuntimeConfig,
|
|
8
|
+
} from "../utils/runtime-config.ts";
|
|
9
|
+
|
|
10
|
+
interface CreateRoomResponse {
|
|
11
|
+
room: {
|
|
12
|
+
id: string;
|
|
13
|
+
code: string;
|
|
14
|
+
name: string;
|
|
15
|
+
hostId: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
};
|
|
18
|
+
hostId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ErrorResponse {
|
|
22
|
+
error: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class CreateCommand extends Command {
|
|
26
|
+
static override paths = [["create"]];
|
|
27
|
+
|
|
28
|
+
static override usage = Command.Usage({
|
|
29
|
+
description: "Create a new room on a hub",
|
|
30
|
+
examples: [
|
|
31
|
+
["Create a room (interactive)", "gambi create"],
|
|
32
|
+
["Create a room", "gambi create --name 'My Room'"],
|
|
33
|
+
[
|
|
34
|
+
"Create on custom hub",
|
|
35
|
+
"gambi create --name 'My Room' --hub http://192.168.1.10:3000",
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
"Create a password-protected room",
|
|
39
|
+
"gambi create --name 'My Room' --password secret123",
|
|
40
|
+
],
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
name = Option.String("--name,-n", {
|
|
45
|
+
description: "Room name",
|
|
46
|
+
required: false,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
password = Option.String("--password,-p", {
|
|
50
|
+
description: "Optional password to protect the room",
|
|
51
|
+
required: false,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
configPath = Option.String("--config", {
|
|
55
|
+
description: "Path to a JSON file with room defaults",
|
|
56
|
+
required: false,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
hub = Option.String("--hub,-H", "http://localhost:3000", {
|
|
60
|
+
description: "Hub URL",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
async execute(): Promise<number> {
|
|
64
|
+
let name = this.name;
|
|
65
|
+
let password = this.password;
|
|
66
|
+
let defaults = this.configPath
|
|
67
|
+
? await loadRuntimeConfigFile(this.configPath).catch((error) => {
|
|
68
|
+
this.context.stderr.write(`${error}\n`);
|
|
69
|
+
return null;
|
|
70
|
+
})
|
|
71
|
+
: {};
|
|
72
|
+
|
|
73
|
+
if (defaults === null) {
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!name && isInteractive()) {
|
|
78
|
+
intro("gambi create");
|
|
79
|
+
|
|
80
|
+
const nameResult = await text({
|
|
81
|
+
message: "Room name:",
|
|
82
|
+
validate: (v) => (v ? undefined : "Room name is required"),
|
|
83
|
+
});
|
|
84
|
+
handleCancel(nameResult);
|
|
85
|
+
name = nameResult as string;
|
|
86
|
+
|
|
87
|
+
if (password === undefined) {
|
|
88
|
+
const passwordResult = await passwordPrompt({
|
|
89
|
+
message: "Room password (leave empty for no password):",
|
|
90
|
+
});
|
|
91
|
+
handleCancel(passwordResult);
|
|
92
|
+
const pwd = passwordResult as string;
|
|
93
|
+
if (pwd) {
|
|
94
|
+
password = pwd;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
defaults = await promptRuntimeConfig("room", defaults);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.context.stderr.write(`${error}\n`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
} else if (!name) {
|
|
105
|
+
this.context.stderr.write(
|
|
106
|
+
"Error: --name is required (or run in a terminal for interactive mode)\n"
|
|
107
|
+
);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const body: {
|
|
113
|
+
defaults?: typeof defaults;
|
|
114
|
+
name: string;
|
|
115
|
+
password?: string;
|
|
116
|
+
} = { name };
|
|
117
|
+
if (password) {
|
|
118
|
+
body.password = password;
|
|
119
|
+
}
|
|
120
|
+
if (hasRuntimeConfig(defaults)) {
|
|
121
|
+
body.defaults = defaults;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const response = await fetch(`${this.hub}/rooms`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify(body),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const data = (await response.json()) as ErrorResponse;
|
|
132
|
+
this.context.stderr.write(`Error: ${data.error}\n`);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const data = (await response.json()) as CreateRoomResponse;
|
|
137
|
+
|
|
138
|
+
const successMsg = [
|
|
139
|
+
"Room created!",
|
|
140
|
+
` Code: ${data.room.code}`,
|
|
141
|
+
` ID: ${data.room.id}`,
|
|
142
|
+
...(password ? [" Protection: Password-protected"] : []),
|
|
143
|
+
"",
|
|
144
|
+
"Share the code with participants to join.",
|
|
145
|
+
].join("\n");
|
|
146
|
+
|
|
147
|
+
if (isInteractive() && !this.name) {
|
|
148
|
+
outro(successMsg);
|
|
149
|
+
} else {
|
|
150
|
+
this.context.stdout.write(`${successMsg}\n`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return 0;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
this.context.stderr.write(`Failed to connect to hub at ${this.hub}\n`);
|
|
156
|
+
this.context.stderr.write(`${err}\n`);
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|