@gobi-ai/cli 0.1.1
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/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/auth/credentials.js +28 -0
- package/dist/auth/manager.js +55 -0
- package/dist/client.js +43 -0
- package/dist/commands/astra.js +543 -0
- package/dist/commands/auth.js +96 -0
- package/dist/commands/init.js +212 -0
- package/dist/constants.js +6 -0
- package/dist/errors.js +45 -0
- package/dist/index.js +3 -0
- package/dist/main.js +82 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Gobi AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# gobi-cli
|
|
2
|
+
|
|
3
|
+
[](https://github.com/gobi-ai/gobi-cli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@gobi-ai/cli)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Command-line interface for the [Gobi](https://joingobi.com) collaborative knowledge platform.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### Homebrew
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
brew tap gobi-ai/tap
|
|
15
|
+
brew install gobi
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### npm
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install -g @gobi-ai/cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### From source
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
git clone https://github.com/gobi-ai/gobi-cli.git
|
|
28
|
+
cd gobi-cli
|
|
29
|
+
npm install
|
|
30
|
+
npm run build
|
|
31
|
+
npm link
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
# Authenticate with your Gobi account
|
|
38
|
+
gobi auth login
|
|
39
|
+
|
|
40
|
+
# Set up your space and vault
|
|
41
|
+
gobi init
|
|
42
|
+
|
|
43
|
+
# Search brains in your space
|
|
44
|
+
gobi astra search-brain --query "machine learning"
|
|
45
|
+
|
|
46
|
+
# Ask a brain a question
|
|
47
|
+
gobi astra ask-brain --vault-slug my-vault --question "What is RAG?"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
### Authentication
|
|
53
|
+
|
|
54
|
+
| Command | Description |
|
|
55
|
+
|---------|-------------|
|
|
56
|
+
| `gobi auth login` | Sign in via device code flow |
|
|
57
|
+
| `gobi auth status` | Show current auth status |
|
|
58
|
+
| `gobi auth logout` | Sign out and clear credentials |
|
|
59
|
+
|
|
60
|
+
### Setup
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
|---------|-------------|
|
|
64
|
+
| `gobi init` | Interactive setup — select your vault and space |
|
|
65
|
+
|
|
66
|
+
### Brains
|
|
67
|
+
|
|
68
|
+
| Command | Description |
|
|
69
|
+
|---------|-------------|
|
|
70
|
+
| `gobi astra search-brain --query <q>` | Search brains in a space |
|
|
71
|
+
| `gobi astra ask-brain --vault-slug <slug> --question <q>` | Ask a brain a question (creates a 1:1 session) |
|
|
72
|
+
| `gobi astra publish-brain` | Upload `BRAIN.md` to your vault |
|
|
73
|
+
| `gobi astra unpublish-brain` | Remove `BRAIN.md` from your vault |
|
|
74
|
+
|
|
75
|
+
### Posts
|
|
76
|
+
|
|
77
|
+
| Command | Description |
|
|
78
|
+
|---------|-------------|
|
|
79
|
+
| `gobi astra list-posts` | List posts in the current space |
|
|
80
|
+
| `gobi astra get-post <id>` | Get a post and its replies |
|
|
81
|
+
| `gobi astra create-post --title <t> --content <c>` | Create a post |
|
|
82
|
+
| `gobi astra edit-post <id> --title <t>` | Edit a post |
|
|
83
|
+
| `gobi astra delete-post <id>` | Delete a post |
|
|
84
|
+
|
|
85
|
+
### Replies
|
|
86
|
+
|
|
87
|
+
| Command | Description |
|
|
88
|
+
|---------|-------------|
|
|
89
|
+
| `gobi astra list-replies <postId>` | List replies to a post |
|
|
90
|
+
| `gobi astra create-reply <postId> --content <c>` | Reply to a post |
|
|
91
|
+
| `gobi astra edit-reply <replyId> --content <c>` | Edit a reply |
|
|
92
|
+
| `gobi astra delete-reply <replyId>` | Delete a reply |
|
|
93
|
+
|
|
94
|
+
### Sessions
|
|
95
|
+
|
|
96
|
+
| Command | Description |
|
|
97
|
+
|---------|-------------|
|
|
98
|
+
| `gobi astra list-sessions` | List your sessions |
|
|
99
|
+
| `gobi astra get-session <id>` | Get a session and its messages |
|
|
100
|
+
| `gobi astra reply-session <id> --content <c>` | Send a message in a session |
|
|
101
|
+
|
|
102
|
+
### Brain updates
|
|
103
|
+
|
|
104
|
+
| Command | Description |
|
|
105
|
+
|---------|-------------|
|
|
106
|
+
| `gobi astra list-brain-updates` | List brain updates in the space |
|
|
107
|
+
| `gobi astra create-brain-update --title <t> --content <c>` | Create a brain update |
|
|
108
|
+
| `gobi astra edit-brain-update <id> --title <t>` | Edit a brain update |
|
|
109
|
+
| `gobi astra delete-brain-update <id>` | Delete a brain update |
|
|
110
|
+
|
|
111
|
+
### Global options
|
|
112
|
+
|
|
113
|
+
| Option | Description |
|
|
114
|
+
|--------|-------------|
|
|
115
|
+
| `--json` | Output results as JSON |
|
|
116
|
+
| `--space-slug <slug>` | Override the default space (astra commands) |
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
### Environment variables
|
|
121
|
+
|
|
122
|
+
| Variable | Default | Description |
|
|
123
|
+
|----------|---------|-------------|
|
|
124
|
+
| `GOBI_BASE_URL` | `https://backend.joingobi.com` | API server URL |
|
|
125
|
+
| `GOBI_WEBDRIVE_BASE_URL` | `https://webdrive.joingobi.com` | File storage URL |
|
|
126
|
+
|
|
127
|
+
### Files
|
|
128
|
+
|
|
129
|
+
| Path | Description |
|
|
130
|
+
|------|-------------|
|
|
131
|
+
| `~/.gobi/credentials.json` | Stored authentication tokens |
|
|
132
|
+
| `.gobi/settings.yaml` | Per-project vault and space configuration |
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
git clone https://github.com/gobi-ai/gobi-cli.git
|
|
138
|
+
cd gobi-cli
|
|
139
|
+
npm install
|
|
140
|
+
npm run build
|
|
141
|
+
npm test
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Run from source without compiling:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
npm run dev -- auth status
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const CREDENTIALS_DIR = join(homedir(), ".gobi");
|
|
5
|
+
const CREDENTIALS_PATH = join(CREDENTIALS_DIR, "credentials.json");
|
|
6
|
+
export async function loadCredentials() {
|
|
7
|
+
try {
|
|
8
|
+
const raw = readFileSync(CREDENTIALS_PATH, "utf-8");
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function saveCredentials(creds) {
|
|
16
|
+
mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
|
17
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), {
|
|
18
|
+
mode: 0o600,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export async function clearCredentials() {
|
|
22
|
+
try {
|
|
23
|
+
unlinkSync(CREDENTIALS_PATH);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// ignore if file doesn't exist
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { BASE_URL, TOKEN_REFRESH_BUFFER_MS } from "../constants.js";
|
|
2
|
+
import { NotAuthenticatedError, TokenRefreshError } from "../errors.js";
|
|
3
|
+
import { loadCredentials, saveCredentials, clearCredentials, } from "./credentials.js";
|
|
4
|
+
let cachedCredentials = null;
|
|
5
|
+
export async function initCredentials() {
|
|
6
|
+
cachedCredentials = await loadCredentials();
|
|
7
|
+
}
|
|
8
|
+
export function isAuthenticated() {
|
|
9
|
+
return cachedCredentials !== null;
|
|
10
|
+
}
|
|
11
|
+
export function getCurrentUser() {
|
|
12
|
+
if (cachedCredentials === null)
|
|
13
|
+
return null;
|
|
14
|
+
return cachedCredentials.user;
|
|
15
|
+
}
|
|
16
|
+
function isExpiringSoon(creds) {
|
|
17
|
+
return Date.now() >= creds.expiresAt - TOKEN_REFRESH_BUFFER_MS;
|
|
18
|
+
}
|
|
19
|
+
async function performRefresh(creds) {
|
|
20
|
+
const res = await fetch(`${BASE_URL}/auth/refresh`, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
body: JSON.stringify({ refreshToken: creds.refreshToken }),
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const body = (await res.text()) || "(no body)";
|
|
27
|
+
throw new TokenRefreshError(`HTTP ${res.status}: ${body}`);
|
|
28
|
+
}
|
|
29
|
+
const data = (await res.json());
|
|
30
|
+
const updated = {
|
|
31
|
+
...creds,
|
|
32
|
+
accessToken: data.accessToken,
|
|
33
|
+
refreshToken: data.refreshToken,
|
|
34
|
+
expiresAt: Date.now() + data.expiresIn * 1000,
|
|
35
|
+
};
|
|
36
|
+
await saveCredentials(updated);
|
|
37
|
+
return updated;
|
|
38
|
+
}
|
|
39
|
+
export async function getValidToken() {
|
|
40
|
+
if (cachedCredentials === null) {
|
|
41
|
+
throw new NotAuthenticatedError();
|
|
42
|
+
}
|
|
43
|
+
if (isExpiringSoon(cachedCredentials)) {
|
|
44
|
+
cachedCredentials = await performRefresh(cachedCredentials);
|
|
45
|
+
}
|
|
46
|
+
return cachedCredentials.accessToken;
|
|
47
|
+
}
|
|
48
|
+
export async function storeTokens(creds) {
|
|
49
|
+
await saveCredentials(creds);
|
|
50
|
+
cachedCredentials = creds;
|
|
51
|
+
}
|
|
52
|
+
export async function logout() {
|
|
53
|
+
await clearCredentials();
|
|
54
|
+
cachedCredentials = null;
|
|
55
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BASE_URL } from "./constants.js";
|
|
2
|
+
import { ApiError } from "./errors.js";
|
|
3
|
+
import { getValidToken } from "./auth/manager.js";
|
|
4
|
+
async function request(method, path, options) {
|
|
5
|
+
const token = await getValidToken();
|
|
6
|
+
let url = `${BASE_URL}${path}`;
|
|
7
|
+
// Filter out null/undefined values from params
|
|
8
|
+
if (options?.params) {
|
|
9
|
+
const filtered = Object.entries(options.params)
|
|
10
|
+
.filter(([, v]) => v != null)
|
|
11
|
+
.map(([k, v]) => [k, String(v)]);
|
|
12
|
+
if (filtered.length > 0) {
|
|
13
|
+
url += "?" + new URLSearchParams(filtered).toString();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const headers = {
|
|
17
|
+
Authorization: `Bearer ${token}`,
|
|
18
|
+
};
|
|
19
|
+
const body = options?.body != null ? JSON.stringify(options.body) : undefined;
|
|
20
|
+
if (body !== undefined) {
|
|
21
|
+
headers["Content-Type"] = "application/json";
|
|
22
|
+
}
|
|
23
|
+
const res = await fetch(url, { method, headers, body });
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
const text = (await res.text()) || "(no body)";
|
|
26
|
+
throw new ApiError(res.status, path, text);
|
|
27
|
+
}
|
|
28
|
+
if (res.status === 204)
|
|
29
|
+
return null;
|
|
30
|
+
return res.json();
|
|
31
|
+
}
|
|
32
|
+
export function apiGet(path, params) {
|
|
33
|
+
return request("GET", path, { params });
|
|
34
|
+
}
|
|
35
|
+
export function apiPost(path, body) {
|
|
36
|
+
return request("POST", path, { body });
|
|
37
|
+
}
|
|
38
|
+
export function apiPatch(path, body) {
|
|
39
|
+
return request("PATCH", path, { body });
|
|
40
|
+
}
|
|
41
|
+
export function apiDelete(path) {
|
|
42
|
+
return request("DELETE", path);
|
|
43
|
+
}
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
|
|
4
|
+
import { WEBDRIVE_BASE_URL } from "../constants.js";
|
|
5
|
+
import { getValidToken } from "../auth/manager.js";
|
|
6
|
+
import { getSpaceSlug, getVaultSlug } from "./init.js";
|
|
7
|
+
function isJsonMode(cmd) {
|
|
8
|
+
return !!cmd.parent?.opts().json;
|
|
9
|
+
}
|
|
10
|
+
function jsonOut(data) {
|
|
11
|
+
console.log(JSON.stringify({ success: true, data }));
|
|
12
|
+
}
|
|
13
|
+
function resolveSpaceSlug(cmd) {
|
|
14
|
+
return cmd.opts().spaceSlug || getSpaceSlug();
|
|
15
|
+
}
|
|
16
|
+
function resolveVaultSlug(opts) {
|
|
17
|
+
return opts.vaultSlug || getVaultSlug();
|
|
18
|
+
}
|
|
19
|
+
function unwrapResp(resp) {
|
|
20
|
+
if (typeof resp === "object" && resp !== null && "data" in resp) {
|
|
21
|
+
return resp.data;
|
|
22
|
+
}
|
|
23
|
+
return resp;
|
|
24
|
+
}
|
|
25
|
+
export function registerAstraCommand(program) {
|
|
26
|
+
const astra = program
|
|
27
|
+
.command("astra")
|
|
28
|
+
.description("Astra commands (posts, sessions, brains, brain updates).")
|
|
29
|
+
.option("--space-slug <slug>", "Space slug (overrides .gobi/settings.yaml)");
|
|
30
|
+
// ── Brains ──
|
|
31
|
+
astra
|
|
32
|
+
.command("search-brain")
|
|
33
|
+
.description("Search brains (second brains/vaults) in a space using text and semantic search.")
|
|
34
|
+
.requiredOption("--query <query>", "Search query")
|
|
35
|
+
.action(async (opts) => {
|
|
36
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
37
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/brains`, {
|
|
38
|
+
query: opts.query,
|
|
39
|
+
}));
|
|
40
|
+
const results = (Array.isArray(resp) ? resp : resp.data || resp);
|
|
41
|
+
if (isJsonMode(astra)) {
|
|
42
|
+
jsonOut(results || []);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!results || results.length === 0) {
|
|
46
|
+
console.log(`No brains found matching "${opts.query}".`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const lines = [];
|
|
50
|
+
for (const entry of results) {
|
|
51
|
+
const vault = (entry.vault || entry);
|
|
52
|
+
const owner = (entry.owner || {});
|
|
53
|
+
const ownerName = owner.name ? ` by ${owner.name}` : "";
|
|
54
|
+
const sim = entry.similarity != null
|
|
55
|
+
? ` [similarity: ${entry.similarity.toFixed(3)}]`
|
|
56
|
+
: "";
|
|
57
|
+
lines.push(`- ${vault.name || vault.title || "N/A"} (ID: ${vault.vaultId || vault.id || "N/A"})${ownerName}${sim}`);
|
|
58
|
+
}
|
|
59
|
+
console.log(`Brains matching "${opts.query}":\n` + lines.join("\n"));
|
|
60
|
+
});
|
|
61
|
+
astra
|
|
62
|
+
.command("ask-brain")
|
|
63
|
+
.description("Ask a brain a question. Creates a targeted session (1:1 conversation).")
|
|
64
|
+
.requiredOption("--vault-slug <vaultSlug>", "Slug of the brain/vault to ask")
|
|
65
|
+
.requiredOption("--question <question>", "The question to ask (markdown supported)")
|
|
66
|
+
.option("--mode <mode>", 'Session mode: "auto" or "manual"', "auto")
|
|
67
|
+
.action(async (opts) => {
|
|
68
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
69
|
+
const resp = (await apiPost(`/session/targeted`, {
|
|
70
|
+
vaultSlug: opts.vaultSlug,
|
|
71
|
+
spaceSlug,
|
|
72
|
+
question: opts.question,
|
|
73
|
+
mode: opts.mode,
|
|
74
|
+
}));
|
|
75
|
+
const data = unwrapResp(resp);
|
|
76
|
+
if (isJsonMode(astra)) {
|
|
77
|
+
jsonOut(data);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const session = (data.session || {});
|
|
81
|
+
const members = (data.members || []);
|
|
82
|
+
console.log(`Session created!\n` +
|
|
83
|
+
` Session ID: ${session.id}\n` +
|
|
84
|
+
` Mode: ${session.mode}\n` +
|
|
85
|
+
` Members: ${members.length}\n` +
|
|
86
|
+
` Question sent.`);
|
|
87
|
+
});
|
|
88
|
+
astra
|
|
89
|
+
.command("publish-brain")
|
|
90
|
+
.description("Upload BRAIN.md to the vault root on webdrive. Triggers post-processing (brain sync, metadata update, Discord notification).")
|
|
91
|
+
.action(async () => {
|
|
92
|
+
const vaultId = getVaultSlug();
|
|
93
|
+
const filePath = join(process.cwd(), "BRAIN.md");
|
|
94
|
+
if (!existsSync(filePath)) {
|
|
95
|
+
throw new Error(`BRAIN.md not found in ${process.cwd()}`);
|
|
96
|
+
}
|
|
97
|
+
const content = readFileSync(filePath, "utf-8");
|
|
98
|
+
const token = await getValidToken();
|
|
99
|
+
const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/files/BRAIN.md`;
|
|
100
|
+
const res = await fetch(url, {
|
|
101
|
+
method: "PUT",
|
|
102
|
+
headers: {
|
|
103
|
+
Authorization: `Bearer ${token}`,
|
|
104
|
+
"Content-Type": "text/markdown",
|
|
105
|
+
},
|
|
106
|
+
body: content,
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
throw new Error(`Upload failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
|
|
110
|
+
}
|
|
111
|
+
if (isJsonMode(astra)) {
|
|
112
|
+
jsonOut({ vaultId });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.log(`Published BRAIN.md to vault "${vaultId}"`);
|
|
116
|
+
});
|
|
117
|
+
astra
|
|
118
|
+
.command("unpublish-brain")
|
|
119
|
+
.description("Delete BRAIN.md from the vault on webdrive.")
|
|
120
|
+
.action(async () => {
|
|
121
|
+
const vaultId = getVaultSlug();
|
|
122
|
+
const token = await getValidToken();
|
|
123
|
+
const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/files/BRAIN.md`;
|
|
124
|
+
const res = await fetch(url, {
|
|
125
|
+
method: "DELETE",
|
|
126
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
throw new Error(`Delete failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
|
|
130
|
+
}
|
|
131
|
+
if (isJsonMode(astra)) {
|
|
132
|
+
jsonOut({ vaultId });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log(`Deleted BRAIN.md from vault "${vaultId}"`);
|
|
136
|
+
});
|
|
137
|
+
// ── Posts (get, list, create, edit, delete) ──
|
|
138
|
+
astra
|
|
139
|
+
.command("get-post <postId>")
|
|
140
|
+
.description("Get a post and its replies (paginated).")
|
|
141
|
+
.option("--limit <number>", "Replies per page", "20")
|
|
142
|
+
.option("--offset <number>", "Offset for reply pagination", "0")
|
|
143
|
+
.action(async (postId, opts) => {
|
|
144
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
145
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/posts/${postId}`, {
|
|
146
|
+
limit: parseInt(opts.limit, 10),
|
|
147
|
+
offset: parseInt(opts.offset, 10),
|
|
148
|
+
}));
|
|
149
|
+
const data = unwrapResp(resp);
|
|
150
|
+
const pagination = (resp.pagination || {});
|
|
151
|
+
if (isJsonMode(astra)) {
|
|
152
|
+
jsonOut({ ...data, pagination });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const msg = (data.post || data);
|
|
156
|
+
const replies = (data.replies || []);
|
|
157
|
+
const totalReplies = pagination.total ||
|
|
158
|
+
msg.replyCount ||
|
|
159
|
+
0;
|
|
160
|
+
const author = msg.author?.name ||
|
|
161
|
+
`User ${msg.authorId}`;
|
|
162
|
+
const replyLines = [];
|
|
163
|
+
for (const r of replies) {
|
|
164
|
+
const rAuthor = r.author?.name ||
|
|
165
|
+
`User ${r.authorId}`;
|
|
166
|
+
const text = r.content;
|
|
167
|
+
const truncated = text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
|
|
168
|
+
replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
|
|
169
|
+
}
|
|
170
|
+
const output = [
|
|
171
|
+
`Post: ${msg.title}`,
|
|
172
|
+
`By: ${author} on ${msg.createdAt}`,
|
|
173
|
+
"",
|
|
174
|
+
msg.content,
|
|
175
|
+
"",
|
|
176
|
+
`Replies (${replies.length} of ${totalReplies}):`,
|
|
177
|
+
...replyLines,
|
|
178
|
+
].join("\n");
|
|
179
|
+
console.log(output);
|
|
180
|
+
});
|
|
181
|
+
astra
|
|
182
|
+
.command("list-posts")
|
|
183
|
+
.description("List posts in a space (paginated).")
|
|
184
|
+
.option("--limit <number>", "Items per page", "20")
|
|
185
|
+
.option("--offset <number>", "Offset for pagination", "0")
|
|
186
|
+
.action(async (opts) => {
|
|
187
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
188
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/posts`, {
|
|
189
|
+
limit: parseInt(opts.limit, 10),
|
|
190
|
+
offset: parseInt(opts.offset, 10),
|
|
191
|
+
}));
|
|
192
|
+
if (isJsonMode(astra)) {
|
|
193
|
+
jsonOut({
|
|
194
|
+
items: resp.data || [],
|
|
195
|
+
pagination: resp.pagination || {},
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const items = (resp.data || []);
|
|
200
|
+
const pagination = (resp.pagination || {});
|
|
201
|
+
if (!items.length) {
|
|
202
|
+
console.log("No posts found.");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const lines = [];
|
|
206
|
+
for (const msg of items) {
|
|
207
|
+
const author = msg.author?.name ||
|
|
208
|
+
`User ${msg.authorId}`;
|
|
209
|
+
lines.push(`- [${msg.id}] "${msg.title}" by ${author} (${msg.replyCount} replies, ${msg.createdAt})`);
|
|
210
|
+
}
|
|
211
|
+
const total = pagination.total || items.length;
|
|
212
|
+
console.log(`Posts (${items.length} of ${total}):\n` + lines.join("\n"));
|
|
213
|
+
});
|
|
214
|
+
astra
|
|
215
|
+
.command("create-post")
|
|
216
|
+
.description("Create a post in a space.")
|
|
217
|
+
.requiredOption("--title <title>", "Title of the post")
|
|
218
|
+
.requiredOption("--content <content>", "Post content (markdown supported)")
|
|
219
|
+
.action(async (opts) => {
|
|
220
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
221
|
+
const resp = (await apiPost(`/spaces/${spaceSlug}/posts`, {
|
|
222
|
+
title: opts.title,
|
|
223
|
+
content: opts.content,
|
|
224
|
+
}));
|
|
225
|
+
const msg = unwrapResp(resp);
|
|
226
|
+
if (isJsonMode(astra)) {
|
|
227
|
+
jsonOut(msg);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
console.log(`Post created!\n` +
|
|
231
|
+
` ID: ${msg.id}\n` +
|
|
232
|
+
` Title: ${msg.title}\n` +
|
|
233
|
+
` Created: ${msg.createdAt}`);
|
|
234
|
+
});
|
|
235
|
+
astra
|
|
236
|
+
.command("edit-post <postId>")
|
|
237
|
+
.description("Edit a post. You must be the author.")
|
|
238
|
+
.option("--title <title>", "New title for the post")
|
|
239
|
+
.option("--content <content>", "New content for the post (markdown supported)")
|
|
240
|
+
.action(async (postId, opts) => {
|
|
241
|
+
if (!opts.title && !opts.content) {
|
|
242
|
+
throw new Error("Provide at least --title or --content to update.");
|
|
243
|
+
}
|
|
244
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
245
|
+
const body = {};
|
|
246
|
+
if (opts.title != null)
|
|
247
|
+
body.title = opts.title;
|
|
248
|
+
if (opts.content != null)
|
|
249
|
+
body.content = opts.content;
|
|
250
|
+
const resp = (await apiPatch(`/spaces/${spaceSlug}/posts/${postId}`, body));
|
|
251
|
+
const msg = unwrapResp(resp);
|
|
252
|
+
if (isJsonMode(astra)) {
|
|
253
|
+
jsonOut(msg);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
console.log(`Post edited!\n` +
|
|
257
|
+
` ID: ${msg.id}\n` +
|
|
258
|
+
` Title: ${msg.title}\n` +
|
|
259
|
+
` Edited: ${msg.editedAt}`);
|
|
260
|
+
});
|
|
261
|
+
astra
|
|
262
|
+
.command("delete-post <postId>")
|
|
263
|
+
.description("Delete a post. You must be the author.")
|
|
264
|
+
.action(async (postId) => {
|
|
265
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
266
|
+
await apiDelete(`/spaces/${spaceSlug}/posts/${postId}`);
|
|
267
|
+
if (isJsonMode(astra)) {
|
|
268
|
+
jsonOut({ id: postId });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
console.log(`Post ${postId} deleted.`);
|
|
272
|
+
});
|
|
273
|
+
// ── Replies (list, create, edit, delete) ──
|
|
274
|
+
astra
|
|
275
|
+
.command("list-replies <postId>")
|
|
276
|
+
.description("List replies to a post (paginated).")
|
|
277
|
+
.option("--limit <number>", "Replies per page", "20")
|
|
278
|
+
.option("--offset <number>", "Offset for reply pagination", "0")
|
|
279
|
+
.action(async (postId, opts) => {
|
|
280
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
281
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/posts/${postId}`, {
|
|
282
|
+
limit: parseInt(opts.limit, 10),
|
|
283
|
+
offset: parseInt(opts.offset, 10),
|
|
284
|
+
}));
|
|
285
|
+
const data = unwrapResp(resp);
|
|
286
|
+
const pagination = (resp.pagination || {});
|
|
287
|
+
if (isJsonMode(astra)) {
|
|
288
|
+
jsonOut({
|
|
289
|
+
replies: data.replies || [],
|
|
290
|
+
pagination,
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const replies = (data.replies || []);
|
|
295
|
+
const totalReplies = pagination.total || replies.length;
|
|
296
|
+
if (!replies.length) {
|
|
297
|
+
console.log("No replies found.");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const lines = [];
|
|
301
|
+
for (const r of replies) {
|
|
302
|
+
const author = r.author?.name ||
|
|
303
|
+
`User ${r.authorId}`;
|
|
304
|
+
const text = r.content;
|
|
305
|
+
const truncated = text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
|
|
306
|
+
lines.push(`- [${r.id}] ${author}: ${truncated} (${r.createdAt})`);
|
|
307
|
+
}
|
|
308
|
+
console.log(`Replies (${replies.length} of ${totalReplies}):\n` +
|
|
309
|
+
lines.join("\n"));
|
|
310
|
+
});
|
|
311
|
+
astra
|
|
312
|
+
.command("create-reply <postId>")
|
|
313
|
+
.description("Create a reply to a post in a space.")
|
|
314
|
+
.requiredOption("--content <content>", "Reply content (markdown supported)")
|
|
315
|
+
.action(async (postId, opts) => {
|
|
316
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
317
|
+
const resp = (await apiPost(`/spaces/${spaceSlug}/posts/${postId}/replies`, { content: opts.content }));
|
|
318
|
+
const msg = unwrapResp(resp);
|
|
319
|
+
if (isJsonMode(astra)) {
|
|
320
|
+
jsonOut(msg);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
console.log(`Reply created!\n ID: ${msg.id}\n Created: ${msg.createdAt}`);
|
|
324
|
+
});
|
|
325
|
+
astra
|
|
326
|
+
.command("edit-reply <replyId>")
|
|
327
|
+
.description("Edit a reply. You must be the author.")
|
|
328
|
+
.requiredOption("--content <content>", "New content for the reply (markdown supported)")
|
|
329
|
+
.action(async (replyId, opts) => {
|
|
330
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
331
|
+
const resp = (await apiPatch(`/spaces/${spaceSlug}/replies/${replyId}`, { content: opts.content }));
|
|
332
|
+
const msg = unwrapResp(resp);
|
|
333
|
+
if (isJsonMode(astra)) {
|
|
334
|
+
jsonOut(msg);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
console.log(`Reply edited!\n ID: ${msg.id}\n Edited: ${msg.editedAt}`);
|
|
338
|
+
});
|
|
339
|
+
astra
|
|
340
|
+
.command("delete-reply <replyId>")
|
|
341
|
+
.description("Delete a reply. You must be the author.")
|
|
342
|
+
.action(async (replyId) => {
|
|
343
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
344
|
+
await apiDelete(`/spaces/${spaceSlug}/replies/${replyId}`);
|
|
345
|
+
if (isJsonMode(astra)) {
|
|
346
|
+
jsonOut({ replyId });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
console.log(`Reply ${replyId} deleted.`);
|
|
350
|
+
});
|
|
351
|
+
// ── Sessions (get, list, reply) ──
|
|
352
|
+
astra
|
|
353
|
+
.command("get-session <sessionId>")
|
|
354
|
+
.description("Get a session and its messages (paginated).")
|
|
355
|
+
.option("--limit <number>", "Messages per page", "20")
|
|
356
|
+
.option("--offset <number>", "Offset for message pagination", "0")
|
|
357
|
+
.action(async (sessionId, opts) => {
|
|
358
|
+
const resp = (await apiGet(`/session/${sessionId}`, {
|
|
359
|
+
limit: parseInt(opts.limit, 10),
|
|
360
|
+
offset: parseInt(opts.offset, 10),
|
|
361
|
+
}));
|
|
362
|
+
const data = unwrapResp(resp);
|
|
363
|
+
if (isJsonMode(astra)) {
|
|
364
|
+
jsonOut(data);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const session = (data.session || data);
|
|
368
|
+
const messages = (data.messages || []);
|
|
369
|
+
const pagination = (data.pagination || {});
|
|
370
|
+
const totalMessages = pagination.total || messages.length;
|
|
371
|
+
const msgLines = [];
|
|
372
|
+
for (const m of messages) {
|
|
373
|
+
const author = m.author?.name ||
|
|
374
|
+
m.source ||
|
|
375
|
+
`User ${m.authorId}`;
|
|
376
|
+
const text = m.content;
|
|
377
|
+
const truncated = text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
|
|
378
|
+
msgLines.push(` - ${author}: ${truncated} (${m.createdAt})`);
|
|
379
|
+
}
|
|
380
|
+
const output = [
|
|
381
|
+
`Session: ${session.title}`,
|
|
382
|
+
` ID: ${session.id}`,
|
|
383
|
+
` Mode: ${session.mode}`,
|
|
384
|
+
` Last activity: ${session.lastMessageAt}`,
|
|
385
|
+
"",
|
|
386
|
+
`Messages (${messages.length} of ${totalMessages}):`,
|
|
387
|
+
...msgLines,
|
|
388
|
+
].join("\n");
|
|
389
|
+
console.log(output);
|
|
390
|
+
});
|
|
391
|
+
astra
|
|
392
|
+
.command("list-sessions")
|
|
393
|
+
.description("List all sessions you are part of, sorted by most recent activity.")
|
|
394
|
+
.option("--limit <number>", "Items per page", "20")
|
|
395
|
+
.option("--offset <number>", "Offset for pagination", "0")
|
|
396
|
+
.action(async (opts) => {
|
|
397
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
398
|
+
const limit = parseInt(opts.limit, 10);
|
|
399
|
+
const offset = parseInt(opts.offset, 10);
|
|
400
|
+
const resp = (await apiGet(`/session/my-sessions`, {
|
|
401
|
+
spaceSlug,
|
|
402
|
+
limit,
|
|
403
|
+
offset,
|
|
404
|
+
}));
|
|
405
|
+
const data = unwrapResp(resp);
|
|
406
|
+
const items = (data.sessions || []);
|
|
407
|
+
const total = data.total ?? items.length;
|
|
408
|
+
if (isJsonMode(astra)) {
|
|
409
|
+
jsonOut({
|
|
410
|
+
sessions: items,
|
|
411
|
+
pagination: { total, limit, offset },
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!items.length) {
|
|
416
|
+
console.log("No sessions found.");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const lines = [];
|
|
420
|
+
for (const s of items) {
|
|
421
|
+
const title = s.title || "(no title)";
|
|
422
|
+
lines.push(`- [${s.id}] "${title}" (mode: ${s.mode}, last activity: ${s.lastMessageAt})`);
|
|
423
|
+
}
|
|
424
|
+
console.log(`Sessions (${items.length} of ${total}):\n` + lines.join("\n"));
|
|
425
|
+
});
|
|
426
|
+
astra
|
|
427
|
+
.command("reply-session <sessionId>")
|
|
428
|
+
.description("Send a human reply to a session you are a member of.")
|
|
429
|
+
.requiredOption("--content <content>", "Reply content (markdown supported)")
|
|
430
|
+
.action(async (sessionId, opts) => {
|
|
431
|
+
const resp = (await apiPost(`/session/${sessionId}/reply`, {
|
|
432
|
+
content: opts.content,
|
|
433
|
+
}));
|
|
434
|
+
const msg = unwrapResp(resp);
|
|
435
|
+
if (isJsonMode(astra)) {
|
|
436
|
+
jsonOut(msg);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
console.log(`Reply sent!\n` +
|
|
440
|
+
` Message ID: ${msg.id}\n` +
|
|
441
|
+
` Source: ${msg.source}\n` +
|
|
442
|
+
` Created: ${msg.createdAt}`);
|
|
443
|
+
});
|
|
444
|
+
// ── Brain Updates (list, create, edit, delete) ──
|
|
445
|
+
astra
|
|
446
|
+
.command("list-brain-updates")
|
|
447
|
+
.description("List recent brain updates in a space (paginated).")
|
|
448
|
+
.option("--limit <number>", "Items per page", "20")
|
|
449
|
+
.option("--offset <number>", "Offset for pagination", "0")
|
|
450
|
+
.action(async (opts) => {
|
|
451
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
452
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/brain-updates`, {
|
|
453
|
+
limit: parseInt(opts.limit, 10),
|
|
454
|
+
offset: parseInt(opts.offset, 10),
|
|
455
|
+
}));
|
|
456
|
+
if (isJsonMode(astra)) {
|
|
457
|
+
jsonOut({
|
|
458
|
+
items: resp.data || [],
|
|
459
|
+
pagination: resp.pagination || {},
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const items = (resp.data || []);
|
|
464
|
+
const pagination = (resp.pagination || {});
|
|
465
|
+
if (!items.length) {
|
|
466
|
+
console.log("No brain updates found.");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const lines = [];
|
|
470
|
+
for (const u of items) {
|
|
471
|
+
const author = u.author?.name ||
|
|
472
|
+
`User ${u.authorId}`;
|
|
473
|
+
const vaultSlug = u.vault?.vaultSlug ||
|
|
474
|
+
"?";
|
|
475
|
+
lines.push(`- [${u.id}] "${u.title}" by ${author} (vault: ${vaultSlug}, ${u.createdAt})`);
|
|
476
|
+
}
|
|
477
|
+
const total = pagination.total || items.length;
|
|
478
|
+
console.log(`Brain updates (${items.length} of ${total}):\n` + lines.join("\n"));
|
|
479
|
+
});
|
|
480
|
+
astra
|
|
481
|
+
.command("create-brain-update")
|
|
482
|
+
.description("Create a brain update in a space. Uses the vault from settings.")
|
|
483
|
+
.option("--vault-slug <vaultSlug>", "Vault slug (overrides .gobi/settings.yaml)")
|
|
484
|
+
.requiredOption("--title <title>", "Title of the update")
|
|
485
|
+
.requiredOption("--content <content>", "Update content (markdown supported)")
|
|
486
|
+
.action(async (opts) => {
|
|
487
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
488
|
+
const vaultSlug = resolveVaultSlug(opts);
|
|
489
|
+
const resp = (await apiPost(`/spaces/${spaceSlug}/brain-updates`, {
|
|
490
|
+
vaultSlug,
|
|
491
|
+
title: opts.title,
|
|
492
|
+
content: opts.content,
|
|
493
|
+
}));
|
|
494
|
+
const u = unwrapResp(resp);
|
|
495
|
+
if (isJsonMode(astra)) {
|
|
496
|
+
jsonOut(u);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
console.log(`Brain update created!\n` +
|
|
500
|
+
` ID: ${u.id}\n` +
|
|
501
|
+
` Title: ${u.title}\n` +
|
|
502
|
+
` Vault: ${u.vaultSlug || vaultSlug}\n` +
|
|
503
|
+
` Created: ${u.createdAt}`);
|
|
504
|
+
});
|
|
505
|
+
astra
|
|
506
|
+
.command("edit-brain-update <updateId>")
|
|
507
|
+
.description("Edit a published brain update. You must be the author.")
|
|
508
|
+
.option("--title <title>", "New title for the update")
|
|
509
|
+
.option("--content <content>", "New content for the update (markdown supported)")
|
|
510
|
+
.action(async (updateId, opts) => {
|
|
511
|
+
if (!opts.title && !opts.content) {
|
|
512
|
+
throw new Error("Provide at least --title or --content to update.");
|
|
513
|
+
}
|
|
514
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
515
|
+
const body = {};
|
|
516
|
+
if (opts.title != null)
|
|
517
|
+
body.title = opts.title;
|
|
518
|
+
if (opts.content != null)
|
|
519
|
+
body.content = opts.content;
|
|
520
|
+
const resp = (await apiPatch(`/spaces/${spaceSlug}/brain-updates/${updateId}`, body));
|
|
521
|
+
const u = unwrapResp(resp);
|
|
522
|
+
if (isJsonMode(astra)) {
|
|
523
|
+
jsonOut(u);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
console.log(`Brain update edited!\n` +
|
|
527
|
+
` ID: ${u.id}\n` +
|
|
528
|
+
` Title: ${u.title}\n` +
|
|
529
|
+
` Updated: ${u.updatedAt}`);
|
|
530
|
+
});
|
|
531
|
+
astra
|
|
532
|
+
.command("delete-brain-update <updateId>")
|
|
533
|
+
.description("Delete a published brain update. You must be the author.")
|
|
534
|
+
.action(async (updateId) => {
|
|
535
|
+
const spaceSlug = resolveSpaceSlug(astra);
|
|
536
|
+
await apiDelete(`/spaces/${spaceSlug}/brain-updates/${updateId}`);
|
|
537
|
+
if (isJsonMode(astra)) {
|
|
538
|
+
jsonOut({ id: updateId });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
console.log(`Brain update ${updateId} deleted.`);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { BASE_URL, POLL_MAX_DURATION_MS } from "../constants.js";
|
|
2
|
+
import { DeviceCodeError } from "../errors.js";
|
|
3
|
+
import { storeTokens, logout, isAuthenticated, getCurrentUser, } from "../auth/manager.js";
|
|
4
|
+
import { printContext, readSettings, runInitFlow } from "./init.js";
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
export function registerAuthCommand(program) {
|
|
9
|
+
const auth = program
|
|
10
|
+
.command("auth")
|
|
11
|
+
.description("Authentication commands.");
|
|
12
|
+
auth
|
|
13
|
+
.command("login")
|
|
14
|
+
.description("Log in to Gobi. Opens a browser URL for Google OAuth, then polls until authentication is complete.")
|
|
15
|
+
.action(async () => {
|
|
16
|
+
const res = await fetch(`${BASE_URL}/auth/device`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
});
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const body = (await res.text()) || "(no body)";
|
|
22
|
+
throw new DeviceCodeError(`Failed to initiate login: HTTP ${res.status}: ${body}`);
|
|
23
|
+
}
|
|
24
|
+
const deviceData = (await res.json());
|
|
25
|
+
const intervalS = deviceData.interval || 5;
|
|
26
|
+
const startMs = Date.now();
|
|
27
|
+
console.log(`Open this URL in your browser to log in:\n ${deviceData.verificationUri}`);
|
|
28
|
+
console.log(`Your user code: ${deviceData.userCode}`);
|
|
29
|
+
console.log("Waiting for authentication...");
|
|
30
|
+
while (Date.now() - startMs < POLL_MAX_DURATION_MS) {
|
|
31
|
+
await sleep(intervalS * 1000);
|
|
32
|
+
const tokenRes = await fetch(`${BASE_URL}/auth/device/token`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
body: JSON.stringify({ deviceCode: deviceData.deviceCode }),
|
|
36
|
+
});
|
|
37
|
+
if (!tokenRes.ok) {
|
|
38
|
+
const body = (await tokenRes.text()) || "(no body)";
|
|
39
|
+
throw new DeviceCodeError(`Token poll failed: HTTP ${tokenRes.status}: ${body}`);
|
|
40
|
+
}
|
|
41
|
+
const tokenData = (await tokenRes.json());
|
|
42
|
+
if ("accessToken" in tokenData) {
|
|
43
|
+
const user = tokenData.user;
|
|
44
|
+
const creds = {
|
|
45
|
+
accessToken: tokenData.accessToken,
|
|
46
|
+
refreshToken: tokenData.refreshToken,
|
|
47
|
+
expiresAt: Date.now() + tokenData.expiresIn * 1000,
|
|
48
|
+
user: {
|
|
49
|
+
id: user.id,
|
|
50
|
+
email: user.email,
|
|
51
|
+
name: user.name,
|
|
52
|
+
pictureUrl: user.pictureUrl || null,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
await storeTokens(creds);
|
|
56
|
+
console.log(`Successfully logged in as ${user.name} (${user.email}).`);
|
|
57
|
+
const settings = readSettings();
|
|
58
|
+
if (settings && settings.selectedSpaceSlug) {
|
|
59
|
+
printContext();
|
|
60
|
+
console.log("Run 'gobi init' to change.");
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
console.log("");
|
|
64
|
+
await runInitFlow();
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (tokenData.status === "expired") {
|
|
69
|
+
throw new DeviceCodeError("Login session expired. Please try 'gobi auth login' again.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new DeviceCodeError("Login timed out. Please try 'gobi auth login' again.");
|
|
73
|
+
});
|
|
74
|
+
auth
|
|
75
|
+
.command("status")
|
|
76
|
+
.description("Check whether you are currently authenticated with Gobi.")
|
|
77
|
+
.action(() => {
|
|
78
|
+
if (isAuthenticated()) {
|
|
79
|
+
const user = getCurrentUser();
|
|
80
|
+
const name = user?.name || "Unknown";
|
|
81
|
+
const email = user?.email || "Unknown";
|
|
82
|
+
console.log(`Authenticated as ${name} (${email})`);
|
|
83
|
+
printContext();
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.log("You are not authenticated. Use 'gobi auth login' to log in.");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
auth
|
|
90
|
+
.command("logout")
|
|
91
|
+
.description("Log out of Gobi and remove stored credentials.")
|
|
92
|
+
.action(async () => {
|
|
93
|
+
await logout();
|
|
94
|
+
console.log("Logged out. Credentials removed.");
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { apiGet, apiPost } from "../client.js";
|
|
6
|
+
import { isAuthenticated } from "../auth/manager.js";
|
|
7
|
+
const SETTINGS_DIR = ".gobi";
|
|
8
|
+
const SETTINGS_FILE = "settings.yaml";
|
|
9
|
+
function settingsPath() {
|
|
10
|
+
return join(process.cwd(), SETTINGS_DIR, SETTINGS_FILE);
|
|
11
|
+
}
|
|
12
|
+
export function readSettings() {
|
|
13
|
+
const path = settingsPath();
|
|
14
|
+
if (!existsSync(path))
|
|
15
|
+
return null;
|
|
16
|
+
const content = readFileSync(path, "utf-8");
|
|
17
|
+
return yaml.load(content);
|
|
18
|
+
}
|
|
19
|
+
export function getSpaceSlug() {
|
|
20
|
+
const settings = readSettings();
|
|
21
|
+
const slug = settings?.selectedSpaceSlug;
|
|
22
|
+
if (!slug) {
|
|
23
|
+
throw new Error("Not initialized. Run 'gobi init' first.");
|
|
24
|
+
}
|
|
25
|
+
return slug;
|
|
26
|
+
}
|
|
27
|
+
export function getVaultSlug() {
|
|
28
|
+
const settings = readSettings();
|
|
29
|
+
const vault = settings?.vaultSlug;
|
|
30
|
+
if (!vault) {
|
|
31
|
+
throw new Error("Not initialized. Run 'gobi init' first.");
|
|
32
|
+
}
|
|
33
|
+
return vault;
|
|
34
|
+
}
|
|
35
|
+
export function printContext() {
|
|
36
|
+
const settings = readSettings();
|
|
37
|
+
const slug = settings?.selectedSpaceSlug;
|
|
38
|
+
if (!slug) {
|
|
39
|
+
console.log("Not initialized. Run 'gobi init' to set up.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const vaultId = settings?.vaultSlug || "?";
|
|
43
|
+
console.log(`Space: ${slug} | Vault: ${vaultId}`);
|
|
44
|
+
}
|
|
45
|
+
function writeSettings(vaultId, spaceSlug) {
|
|
46
|
+
const path = settingsPath();
|
|
47
|
+
const dir = join(process.cwd(), SETTINGS_DIR);
|
|
48
|
+
if (!existsSync(dir)) {
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
const content = yaml.dump({ vaultSlug: vaultId, selectedSpaceSlug: spaceSlug }, { flowLevel: -1 });
|
|
52
|
+
writeFileSync(path, content, "utf-8");
|
|
53
|
+
}
|
|
54
|
+
async function selectSpace() {
|
|
55
|
+
const resp = (await apiGet("/spaces"));
|
|
56
|
+
const spaces = (Array.isArray(resp) ? resp : resp.data || resp);
|
|
57
|
+
if (!spaces || spaces.length === 0) {
|
|
58
|
+
throw new Error("You are not a member of any spaces. Join or create a space first.");
|
|
59
|
+
}
|
|
60
|
+
const choices = spaces.map((s) => ({
|
|
61
|
+
name: `${s.name} (${s.slug})`,
|
|
62
|
+
value: s,
|
|
63
|
+
}));
|
|
64
|
+
if (choices.length > 1) {
|
|
65
|
+
choices.push({ name: "Go back", value: null });
|
|
66
|
+
}
|
|
67
|
+
const { selected } = await inquirer.prompt([
|
|
68
|
+
{
|
|
69
|
+
type: "list",
|
|
70
|
+
name: "selected",
|
|
71
|
+
message: "Select a space:",
|
|
72
|
+
choices,
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
if (selected === null)
|
|
76
|
+
return null;
|
|
77
|
+
return { slug: selected.slug, name: selected.name };
|
|
78
|
+
}
|
|
79
|
+
async function selectExistingVault() {
|
|
80
|
+
const resp = (await apiGet("/vault"));
|
|
81
|
+
const vaults = (Array.isArray(resp) ? resp : resp.data || resp);
|
|
82
|
+
if (!vaults || vaults.length === 0) {
|
|
83
|
+
console.log("You don't have any vaults yet. Let's create one.");
|
|
84
|
+
return createNewVault();
|
|
85
|
+
}
|
|
86
|
+
const choices = vaults.map((v) => ({
|
|
87
|
+
name: `${v.name} (${v.vaultId})`,
|
|
88
|
+
value: v,
|
|
89
|
+
}));
|
|
90
|
+
choices.push({ name: "Go back", value: null });
|
|
91
|
+
const { selected } = await inquirer.prompt([
|
|
92
|
+
{
|
|
93
|
+
type: "list",
|
|
94
|
+
name: "selected",
|
|
95
|
+
message: "Select a vault:",
|
|
96
|
+
choices,
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
if (selected === null)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
vaultId: selected.vaultId,
|
|
103
|
+
name: selected.name,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function createNewVault() {
|
|
107
|
+
let vaultId;
|
|
108
|
+
while (true) {
|
|
109
|
+
const { id } = await inquirer.prompt([
|
|
110
|
+
{
|
|
111
|
+
type: "input",
|
|
112
|
+
name: "id",
|
|
113
|
+
message: "Enter a unique vault ID:",
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
if (!id.trim()) {
|
|
117
|
+
console.log("Vault ID cannot be empty.");
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
vaultId = id.trim();
|
|
121
|
+
const resp = (await apiGet(`/vault/check/${vaultId}`));
|
|
122
|
+
const available = typeof resp === "object" && resp !== null
|
|
123
|
+
? resp.available
|
|
124
|
+
: false;
|
|
125
|
+
if (available) {
|
|
126
|
+
console.log(`ID "${vaultId}" is available!`);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
console.log(`ID "${vaultId}" is already taken. Try another.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const { vaultName } = await inquirer.prompt([
|
|
134
|
+
{
|
|
135
|
+
type: "input",
|
|
136
|
+
name: "vaultName",
|
|
137
|
+
message: "Enter vault name:",
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
const name = vaultName.trim() || vaultId;
|
|
141
|
+
const resp = (await apiPost("/vault", {
|
|
142
|
+
vaultId,
|
|
143
|
+
name,
|
|
144
|
+
}));
|
|
145
|
+
const vault = (typeof resp === "object" && resp !== null && "data" in resp
|
|
146
|
+
? resp.data
|
|
147
|
+
: resp);
|
|
148
|
+
console.log(`Created vault "${vault.name}" (${vault.vaultId})`);
|
|
149
|
+
return { vaultId: vault.vaultId, name: vault.name };
|
|
150
|
+
}
|
|
151
|
+
export async function runInitFlow() {
|
|
152
|
+
if (!isAuthenticated()) {
|
|
153
|
+
throw new Error("Not authenticated. Run 'gobi auth login' first.");
|
|
154
|
+
}
|
|
155
|
+
// Step 1: Select or create vault
|
|
156
|
+
let vaultId;
|
|
157
|
+
let vaultName;
|
|
158
|
+
while (true) {
|
|
159
|
+
const { action } = await inquirer.prompt([
|
|
160
|
+
{
|
|
161
|
+
type: "list",
|
|
162
|
+
name: "action",
|
|
163
|
+
message: "How would you like to set up your vault?",
|
|
164
|
+
choices: [
|
|
165
|
+
{ name: "Select an existing vault", value: "existing" },
|
|
166
|
+
{ name: "Create a new vault", value: "new" },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
]);
|
|
170
|
+
if (action === "existing") {
|
|
171
|
+
const result = await selectExistingVault();
|
|
172
|
+
if (result === null)
|
|
173
|
+
continue;
|
|
174
|
+
vaultId = result.vaultId;
|
|
175
|
+
vaultName = result.name;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const result = await createNewVault();
|
|
179
|
+
vaultId = result.vaultId;
|
|
180
|
+
vaultName = result.name;
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
// Step 2: Select space
|
|
185
|
+
let spaceSlug;
|
|
186
|
+
let spaceName;
|
|
187
|
+
while (true) {
|
|
188
|
+
const result = await selectSpace();
|
|
189
|
+
if (result === null)
|
|
190
|
+
continue;
|
|
191
|
+
spaceSlug = result.slug;
|
|
192
|
+
spaceName = result.name;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
writeSettings(vaultId, spaceSlug);
|
|
196
|
+
console.log(`Linked to space "${spaceName}" (${spaceSlug}) with vault "${vaultName}" (${vaultId})`);
|
|
197
|
+
console.log(`Created ${SETTINGS_DIR}/${SETTINGS_FILE}`);
|
|
198
|
+
// Create default BRAIN.md if it doesn't exist
|
|
199
|
+
const brainPath = join(process.cwd(), "BRAIN.md");
|
|
200
|
+
if (!existsSync(brainPath)) {
|
|
201
|
+
writeFileSync(brainPath, `---\ntitle: ${vaultName}\ntags: []\ndescription:\nthumbnail:\nprompt:\n---\n`, "utf-8");
|
|
202
|
+
console.log("Created BRAIN.md");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export function registerInitCommand(program) {
|
|
206
|
+
program
|
|
207
|
+
.command("init")
|
|
208
|
+
.description("Set up or change the space and vault linked to the current directory.")
|
|
209
|
+
.action(async () => {
|
|
210
|
+
await runInitFlow();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const BASE_URL = process.env.GOBI_BASE_URL || "https://backend.joingobi.com";
|
|
2
|
+
export const WEBDRIVE_BASE_URL = process.env.GOBI_WEBDRIVE_BASE_URL || "https://webdrive.joingobi.com";
|
|
3
|
+
// Refresh access token when less than 5 minutes remain
|
|
4
|
+
export const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
5
|
+
// Max polling duration before giving up (ms) - 10 minutes
|
|
6
|
+
export const POLL_MAX_DURATION_MS = 10 * 60 * 1000;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class AstraError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "AstraError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class NotAuthenticatedError extends AstraError {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("Not authenticated. Use 'gobi auth login' to begin the login flow.", "NOT_AUTHENTICATED");
|
|
12
|
+
this.name = "NotAuthenticatedError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class TokenRefreshError extends AstraError {
|
|
16
|
+
constructor(detail) {
|
|
17
|
+
super(`Failed to refresh access token: ${detail}. Please run 'gobi auth login' to re-authenticate.`, "TOKEN_REFRESH_FAILED");
|
|
18
|
+
this.name = "TokenRefreshError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class ApiError extends AstraError {
|
|
22
|
+
status;
|
|
23
|
+
endpoint;
|
|
24
|
+
constructor(status, endpoint, body) {
|
|
25
|
+
let message = body;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(body);
|
|
28
|
+
if (parsed.message)
|
|
29
|
+
message = parsed.message;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// use raw body as-is
|
|
33
|
+
}
|
|
34
|
+
super(message, "API_ERROR");
|
|
35
|
+
this.status = status;
|
|
36
|
+
this.endpoint = endpoint;
|
|
37
|
+
this.name = "ApiError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class DeviceCodeError extends AstraError {
|
|
41
|
+
constructor(detail) {
|
|
42
|
+
super(`Device code flow error: ${detail}`, "DEVICE_CODE_ERROR");
|
|
43
|
+
this.name = "DeviceCodeError";
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/index.js
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { initCredentials } from "./auth/manager.js";
|
|
4
|
+
import { ApiError, AstraError } from "./errors.js";
|
|
5
|
+
import { registerAuthCommand } from "./commands/auth.js";
|
|
6
|
+
import { registerInitCommand, printContext } from "./commands/init.js";
|
|
7
|
+
import { registerAstraCommand } from "./commands/astra.js";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { version } = require("../package.json");
|
|
10
|
+
const SKIP_BANNER_COMMANDS = new Set(["auth", "init"]);
|
|
11
|
+
function shouldShowBanner() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
if (args.length === 0)
|
|
14
|
+
return true;
|
|
15
|
+
return !SKIP_BANNER_COMMANDS.has(args[0]);
|
|
16
|
+
}
|
|
17
|
+
export async function cli() {
|
|
18
|
+
const program = new Command();
|
|
19
|
+
program
|
|
20
|
+
.name("gobi")
|
|
21
|
+
.version(version)
|
|
22
|
+
.description("CLI client for the Gobi collaborative knowledge platform")
|
|
23
|
+
.option("--json", "Output results as JSON instead of human-readable text")
|
|
24
|
+
.configureHelp({ helpWidth: process.stdout.columns || 200 });
|
|
25
|
+
// Register all command groups
|
|
26
|
+
registerAuthCommand(program);
|
|
27
|
+
registerInitCommand(program);
|
|
28
|
+
registerAstraCommand(program);
|
|
29
|
+
// Propagate helpWidth to all subcommands
|
|
30
|
+
const helpWidth = process.stdout.columns || 200;
|
|
31
|
+
for (const cmd of program.commands) {
|
|
32
|
+
cmd.configureHelp({ helpWidth });
|
|
33
|
+
for (const sub of cmd.commands) {
|
|
34
|
+
sub.configureHelp({ helpWidth });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Hook into the pre-action to init credentials and show banner
|
|
38
|
+
program.hook("preAction", async () => {
|
|
39
|
+
await initCredentials();
|
|
40
|
+
if (!program.opts().json && shouldShowBanner()) {
|
|
41
|
+
printContext();
|
|
42
|
+
console.log("");
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
await program.parseAsync(process.argv);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const isJson = program.opts().json;
|
|
50
|
+
if (err instanceof ApiError) {
|
|
51
|
+
if (isJson) {
|
|
52
|
+
console.log(JSON.stringify({ success: false, error: `API error (HTTP ${err.status}): ${err.message}` }));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
let hint = "";
|
|
56
|
+
if (err.status === 403) {
|
|
57
|
+
hint = " You may not have permission to perform this action.";
|
|
58
|
+
}
|
|
59
|
+
else if (err.status === 404) {
|
|
60
|
+
hint = " The requested resource was not found.";
|
|
61
|
+
}
|
|
62
|
+
console.error(`Error: API error (HTTP ${err.status}): ${err.message}${hint}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
else if (err instanceof AstraError) {
|
|
66
|
+
if (isJson) {
|
|
67
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
console.error(`Error: ${err.message}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
74
|
+
if (isJson) {
|
|
75
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.error(`Error: ${msg}`);
|
|
79
|
+
}
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gobi-ai/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "CLI client for the Gobi collaborative knowledge platform",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/gobi-ai/gobi-cli.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/gobi-ai/gobi-cli",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/gobi-ai/gobi-cli/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"gobi",
|
|
17
|
+
"cli",
|
|
18
|
+
"second-brain",
|
|
19
|
+
"knowledge"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"!dist/**/*.test.js"
|
|
30
|
+
],
|
|
31
|
+
"bin": {
|
|
32
|
+
"gobi": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"clean": "rm -rf dist",
|
|
36
|
+
"build": "npm run clean && tsc && chmod +x dist/index.js",
|
|
37
|
+
"dev": "tsx src/index.ts",
|
|
38
|
+
"start": "node dist/index.js",
|
|
39
|
+
"test": "node --test dist/*.test.js",
|
|
40
|
+
"prepublishOnly": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"commander": "^12.1.0",
|
|
44
|
+
"inquirer": "^12.3.0",
|
|
45
|
+
"js-yaml": "^4.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/inquirer": "^9.0.7",
|
|
49
|
+
"@types/js-yaml": "^4.0.9",
|
|
50
|
+
"@types/node": "^22.0.0",
|
|
51
|
+
"tsx": "^4.19.0",
|
|
52
|
+
"typescript": "^5.6.0"
|
|
53
|
+
}
|
|
54
|
+
}
|