@pebblehouse/odin-cli 0.1.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/README.md +89 -0
- package/dist/index.js +326 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @pebblehouse/odin-cli
|
|
2
|
+
|
|
3
|
+
CLI for [Odin](https://www.odin.mu) — the knowledge backbone for Pebble House.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @pebblehouse/odin-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Authenticate via browser (Google sign-in)
|
|
15
|
+
odin login
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This opens your browser, completes Google OAuth, and stores credentials locally at `~/.odin/credentials.json`. Tokens auto-refresh — you won't need to re-login for ~6 months.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Pebbles
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
odin pebbles list
|
|
26
|
+
odin pebbles get <slug>
|
|
27
|
+
odin pebbles create <slug> --name "My Project"
|
|
28
|
+
odin pebbles update <slug> --status building
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Documents
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
odin docs list <slug>
|
|
35
|
+
odin docs get <slug> <type>
|
|
36
|
+
odin docs create <slug> architecture --title "Architecture" --file ./ARCHITECTURE.md
|
|
37
|
+
odin docs update <slug> architecture --file ./ARCHITECTURE.md
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Decisions
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
odin decisions list <slug>
|
|
44
|
+
odin decisions log <slug> "Use Hono for API" --rationale "Lightweight, consistent"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Context (Tier 1 Retrieval)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
odin context <slug> # Last 20 observations
|
|
51
|
+
odin context <slug> --limit 50 # Custom limit
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Sessions
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
odin sessions start <slug> # Returns session ID
|
|
58
|
+
odin sessions end <session-id> --summary "Completed auth"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Search
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
odin search "authentication" # Cross-pebble
|
|
65
|
+
odin search "RLS policies" --pebble odin # Scoped
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Auth
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
odin login # Browser OAuth
|
|
72
|
+
odin logout # Clear credentials
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Output
|
|
76
|
+
|
|
77
|
+
All commands output JSON to stdout. Errors go to stderr with non-zero exit codes.
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
| Variable | Default | Description |
|
|
82
|
+
|----------|---------|-------------|
|
|
83
|
+
| `ODIN_API_URL` | `https://www.odin.mu` | API base URL |
|
|
84
|
+
| `ODIN_SUPABASE_URL` | — | Supabase URL (for token refresh) |
|
|
85
|
+
| `ODIN_SUPABASE_ANON_KEY` | — | Supabase anon key (for token refresh) |
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command7 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/auth/login.ts
|
|
7
|
+
import { createServer } from "http";
|
|
8
|
+
import open from "open";
|
|
9
|
+
|
|
10
|
+
// src/auth/credentials.ts
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
var ODIN_DIR = join(homedir(), ".odin");
|
|
15
|
+
var CREDENTIALS_PATH = join(ODIN_DIR, "credentials.json");
|
|
16
|
+
function getCredentials() {
|
|
17
|
+
if (!existsSync(CREDENTIALS_PATH)) return null;
|
|
18
|
+
try {
|
|
19
|
+
const raw = readFileSync(CREDENTIALS_PATH, "utf-8");
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function saveCredentials(creds) {
|
|
26
|
+
if (!existsSync(ODIN_DIR)) {
|
|
27
|
+
mkdirSync(ODIN_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
30
|
+
}
|
|
31
|
+
function clearCredentials() {
|
|
32
|
+
if (existsSync(CREDENTIALS_PATH)) {
|
|
33
|
+
unlinkSync(CREDENTIALS_PATH);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/auth/login.ts
|
|
38
|
+
var API_URL = process.env.ODIN_API_URL ?? "https://www.odin.mu";
|
|
39
|
+
async function login() {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const server = createServer(async (req, res) => {
|
|
42
|
+
if (req.method === "POST" && req.url?.startsWith("/callback")) {
|
|
43
|
+
let body = "";
|
|
44
|
+
for await (const chunk of req) {
|
|
45
|
+
body += chunk;
|
|
46
|
+
}
|
|
47
|
+
const params = new URLSearchParams(body);
|
|
48
|
+
const access_token = params.get("access_token");
|
|
49
|
+
const refresh_token = params.get("refresh_token");
|
|
50
|
+
const expires_at = params.get("expires_at");
|
|
51
|
+
if (!access_token || !refresh_token || !expires_at) {
|
|
52
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
53
|
+
res.end("<html><body><p>Missing tokens. Login failed.</p></body></html>");
|
|
54
|
+
server.close();
|
|
55
|
+
reject(new Error("Missing tokens in callback"));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const creds = {
|
|
59
|
+
access_token,
|
|
60
|
+
refresh_token,
|
|
61
|
+
expires_at: parseInt(expires_at, 10)
|
|
62
|
+
};
|
|
63
|
+
saveCredentials(creds);
|
|
64
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
65
|
+
res.end(`<!DOCTYPE html>
|
|
66
|
+
<html><body style="margin:0;background:#111;color:#e5e5e5;font-family:monospace;display:flex;align-items:center;justify-content:center;min-height:100vh;text-align:center">
|
|
67
|
+
<div>
|
|
68
|
+
<p style="color:#f2c94c;font-size:1.5rem;margin-bottom:0.5rem">✓ Login successful</p>
|
|
69
|
+
<p style="color:#666;font-size:0.9rem">You can close this tab.</p>
|
|
70
|
+
</div>
|
|
71
|
+
</body></html>`);
|
|
72
|
+
server.close();
|
|
73
|
+
resolve();
|
|
74
|
+
} else {
|
|
75
|
+
res.writeHead(404);
|
|
76
|
+
res.end();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
server.listen(0, "127.0.0.1", () => {
|
|
80
|
+
const addr = server.address();
|
|
81
|
+
if (!addr || typeof addr === "string") {
|
|
82
|
+
reject(new Error("Failed to start callback server"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const port = addr.port;
|
|
86
|
+
const authUrl = `${API_URL}/auth/cli?port=${port}`;
|
|
87
|
+
console.error(`Opening browser for authentication...`);
|
|
88
|
+
console.error(`If the browser doesn't open, visit: ${authUrl}`);
|
|
89
|
+
open(authUrl).catch(() => {
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
const timeout = setTimeout(() => {
|
|
93
|
+
server.close();
|
|
94
|
+
reject(new Error("Login timed out. Try again."));
|
|
95
|
+
}, 12e4);
|
|
96
|
+
server.on("close", () => clearTimeout(timeout));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/commands/pebbles.ts
|
|
101
|
+
import { Command } from "commander";
|
|
102
|
+
|
|
103
|
+
// src/auth/refresh.ts
|
|
104
|
+
import { createClient } from "@supabase/supabase-js";
|
|
105
|
+
var SUPABASE_URL = process.env.ODIN_SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
|
|
106
|
+
var SUPABASE_ANON_KEY = process.env.ODIN_SUPABASE_ANON_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? "";
|
|
107
|
+
async function getValidToken() {
|
|
108
|
+
const creds = getCredentials();
|
|
109
|
+
if (!creds) {
|
|
110
|
+
throw new Error("Not logged in. Run 'odin login' first.");
|
|
111
|
+
}
|
|
112
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
113
|
+
if (creds.expires_at > now + 60) {
|
|
114
|
+
return creds.access_token;
|
|
115
|
+
}
|
|
116
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
117
|
+
const { data, error } = await supabase.auth.setSession({
|
|
118
|
+
access_token: creds.access_token,
|
|
119
|
+
refresh_token: creds.refresh_token
|
|
120
|
+
});
|
|
121
|
+
if (error || !data.session) {
|
|
122
|
+
throw new Error("Session expired. Run 'odin login' to re-authenticate.");
|
|
123
|
+
}
|
|
124
|
+
const newCreds = {
|
|
125
|
+
access_token: data.session.access_token,
|
|
126
|
+
refresh_token: data.session.refresh_token,
|
|
127
|
+
expires_at: data.session.expires_at ?? now + 3600
|
|
128
|
+
};
|
|
129
|
+
saveCredentials(newCreds);
|
|
130
|
+
return newCreds.access_token;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/api-client.ts
|
|
134
|
+
var BASE_URL = process.env.ODIN_API_URL ?? "https://www.odin.mu";
|
|
135
|
+
async function apiRequest(path, options = {}) {
|
|
136
|
+
const { method = "GET", body, params } = options;
|
|
137
|
+
const token = await getValidToken();
|
|
138
|
+
let url = `${BASE_URL}/api${path}`;
|
|
139
|
+
if (params) {
|
|
140
|
+
const searchParams = new URLSearchParams(params);
|
|
141
|
+
url += `?${searchParams.toString()}`;
|
|
142
|
+
}
|
|
143
|
+
const headers = {
|
|
144
|
+
Authorization: `Bearer ${token}`,
|
|
145
|
+
"Content-Type": "application/json"
|
|
146
|
+
};
|
|
147
|
+
const res = await fetch(url, {
|
|
148
|
+
method,
|
|
149
|
+
headers,
|
|
150
|
+
body: body ? JSON.stringify(body) : void 0
|
|
151
|
+
});
|
|
152
|
+
const json = await res.json();
|
|
153
|
+
return json;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/commands/pebbles.ts
|
|
157
|
+
var pebblesCmd = new Command("pebbles").description("Manage pebbles");
|
|
158
|
+
pebblesCmd.command("list").description("List all pebbles").action(async () => {
|
|
159
|
+
const res = await apiRequest("/pebbles");
|
|
160
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
161
|
+
if (res.error) process.exit(1);
|
|
162
|
+
});
|
|
163
|
+
pebblesCmd.command("get <slug>").description("Get pebble details").action(async (slug) => {
|
|
164
|
+
const res = await apiRequest(`/pebbles/${slug}`);
|
|
165
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
166
|
+
if (res.error) process.exit(1);
|
|
167
|
+
});
|
|
168
|
+
pebblesCmd.command("create <slug>").description("Create a pebble").requiredOption("--name <name>", "Pebble name").option("--status <status>", "Initial status", "idea").option("--summary <summary>", "Summary").action(async (slug, opts) => {
|
|
169
|
+
const res = await apiRequest("/pebbles", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
body: { slug, name: opts.name, status: opts.status, summary: opts.summary }
|
|
172
|
+
});
|
|
173
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
174
|
+
if (res.error) process.exit(1);
|
|
175
|
+
});
|
|
176
|
+
pebblesCmd.command("update <slug>").description("Update a pebble").option("--name <name>", "New name").option("--status <status>", "New status").option("--summary <summary>", "New summary").option("--repo-url <url>", "Repository URL").option("--domain <domain>", "Domain").action(async (slug, opts) => {
|
|
177
|
+
const body = {};
|
|
178
|
+
if (opts.name) body.name = opts.name;
|
|
179
|
+
if (opts.status) body.status = opts.status;
|
|
180
|
+
if (opts.summary) body.summary = opts.summary;
|
|
181
|
+
if (opts.repoUrl) body.repo_url = opts.repoUrl;
|
|
182
|
+
if (opts.domain) body.domain = opts.domain;
|
|
183
|
+
const res = await apiRequest(`/pebbles/${slug}`, { method: "PUT", body });
|
|
184
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
185
|
+
if (res.error) process.exit(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// src/commands/documents.ts
|
|
189
|
+
import { Command as Command2 } from "commander";
|
|
190
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
191
|
+
var docsCmd = new Command2("docs").description("Manage documents");
|
|
192
|
+
docsCmd.command("list <slug>").description("List documents for a pebble").action(async (slug) => {
|
|
193
|
+
const res = await apiRequest(`/pebbles/${slug}/documents`);
|
|
194
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
195
|
+
if (res.error) process.exit(1);
|
|
196
|
+
});
|
|
197
|
+
docsCmd.command("get <slug> <type>").description("Get document by type").action(async (slug, type) => {
|
|
198
|
+
const res = await apiRequest(`/pebbles/${slug}/documents/${type}`);
|
|
199
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
200
|
+
if (res.error) process.exit(1);
|
|
201
|
+
});
|
|
202
|
+
docsCmd.command("create <slug> <type>").description("Create a document").requiredOption("--title <title>", "Document title").option("--file <path>", "Read content from file").option("--source <source>", "Source", "manual").action(async (slug, type, opts) => {
|
|
203
|
+
const content = opts.file ? readFileSync2(opts.file, "utf-8") : "";
|
|
204
|
+
const res = await apiRequest(`/pebbles/${slug}/documents`, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
body: { doc_type: type, title: opts.title, content, source: opts.source }
|
|
207
|
+
});
|
|
208
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
209
|
+
if (res.error) process.exit(1);
|
|
210
|
+
});
|
|
211
|
+
docsCmd.command("update <slug> <type>").description("Update document content").requiredOption("--file <path>", "Read content from file").option("--source <source>", "Source", "manual").action(async (slug, type, opts) => {
|
|
212
|
+
const content = readFileSync2(opts.file, "utf-8");
|
|
213
|
+
const res = await apiRequest(`/pebbles/${slug}/documents/${type}`, {
|
|
214
|
+
method: "PUT",
|
|
215
|
+
body: { content, source: opts.source }
|
|
216
|
+
});
|
|
217
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
218
|
+
if (res.error) process.exit(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// src/commands/decisions.ts
|
|
222
|
+
import { Command as Command3 } from "commander";
|
|
223
|
+
var decisionsCmd = new Command3("decisions").description("Manage decisions");
|
|
224
|
+
decisionsCmd.command("list <slug>").description("List decisions for a pebble").action(async (slug) => {
|
|
225
|
+
const res = await apiRequest(`/pebbles/${slug}/decisions`);
|
|
226
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
227
|
+
if (res.error) process.exit(1);
|
|
228
|
+
});
|
|
229
|
+
decisionsCmd.command("log <slug> <title>").description("Log a decision").option("--rationale <rationale>", "Decision rationale").option("--alternatives <alternatives>", "Alternatives considered").option("--session-id <id>", "Link to active session").action(async (slug, title, opts) => {
|
|
230
|
+
const res = await apiRequest(`/pebbles/${slug}/decisions`, {
|
|
231
|
+
method: "POST",
|
|
232
|
+
body: {
|
|
233
|
+
title,
|
|
234
|
+
rationale: opts.rationale,
|
|
235
|
+
alternatives_considered: opts.alternatives,
|
|
236
|
+
session_id: opts.sessionId
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
240
|
+
if (res.error) process.exit(1);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// src/commands/context.ts
|
|
244
|
+
import { Command as Command4 } from "commander";
|
|
245
|
+
var contextCmd = new Command4("context").description("Get Tier 1 context for a pebble").argument("<slug>", "Pebble slug").option("--limit <n>", "Number of observations", "20").action(async (slug, opts) => {
|
|
246
|
+
const res = await apiRequest(`/pebbles/${slug}/context`, {
|
|
247
|
+
params: { limit: opts.limit }
|
|
248
|
+
});
|
|
249
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
250
|
+
if (res.error) process.exit(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// src/commands/sessions.ts
|
|
254
|
+
import { Command as Command5 } from "commander";
|
|
255
|
+
var sessionsCmd = new Command5("sessions").description("Manage sessions");
|
|
256
|
+
sessionsCmd.command("start <slug>").description("Start a session").option("--source <source>", "Session source", "claude-code").action(async (slug, opts) => {
|
|
257
|
+
const res = await apiRequest("/sessions", {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: { pebble_slug: slug, source: opts.source }
|
|
260
|
+
});
|
|
261
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
262
|
+
if (res.error) process.exit(1);
|
|
263
|
+
});
|
|
264
|
+
sessionsCmd.command("end <id>").description("End a session").option("--summary <summary>", "Session summary").option("--status <status>", "Final status", "completed").action(async (id, opts) => {
|
|
265
|
+
const res = await apiRequest(`/sessions/${id}`, {
|
|
266
|
+
method: "PUT",
|
|
267
|
+
body: { summary: opts.summary, status: opts.status }
|
|
268
|
+
});
|
|
269
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
270
|
+
if (res.error) process.exit(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// src/commands/search.ts
|
|
274
|
+
import { Command as Command6 } from "commander";
|
|
275
|
+
var searchCmd = new Command6("search").description("Full-text search across Odin").argument("<query>", "Search query").option("--pebble <slug>", "Scope to a pebble").action(async (query, opts) => {
|
|
276
|
+
const params = { q: query };
|
|
277
|
+
if (opts.pebble) params.pebble = opts.pebble;
|
|
278
|
+
const res = await apiRequest("/search", { params });
|
|
279
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
280
|
+
if (res.error) process.exit(1);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// src/index.ts
|
|
284
|
+
var GOLD = "\x1B[33m";
|
|
285
|
+
var DIM = "\x1B[2m";
|
|
286
|
+
var RESET = "\x1B[0m";
|
|
287
|
+
var VERSION = "0.1.0";
|
|
288
|
+
function printBanner() {
|
|
289
|
+
const cwd = process.cwd();
|
|
290
|
+
console.error(`${GOLD}
|
|
291
|
+
\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588 \u2588\u2588
|
|
292
|
+
\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588
|
|
293
|
+
\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588
|
|
294
|
+
\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588
|
|
295
|
+
\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588
|
|
296
|
+
${RESET}
|
|
297
|
+
${GOLD}ODIN${RESET} v${VERSION}
|
|
298
|
+
${DIM}${cwd}${RESET}
|
|
299
|
+
`);
|
|
300
|
+
}
|
|
301
|
+
var program = new Command7();
|
|
302
|
+
program.name("odin").description("CLI for Odin \u2014 the knowledge backbone for Pebble House").version(VERSION);
|
|
303
|
+
program.command("login").description("Authenticate with Odin via browser").action(async () => {
|
|
304
|
+
printBanner();
|
|
305
|
+
try {
|
|
306
|
+
await login();
|
|
307
|
+
console.error(`${GOLD}Logged in successfully.${RESET}`);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
console.error(`Login failed: ${err instanceof Error ? err.message : err}`);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
program.command("logout").description("Clear stored credentials").action(() => {
|
|
314
|
+
clearCredentials();
|
|
315
|
+
console.error("Logged out.");
|
|
316
|
+
});
|
|
317
|
+
program.addCommand(pebblesCmd);
|
|
318
|
+
program.addCommand(docsCmd);
|
|
319
|
+
program.addCommand(decisionsCmd);
|
|
320
|
+
program.addCommand(contextCmd);
|
|
321
|
+
program.addCommand(sessionsCmd);
|
|
322
|
+
program.addCommand(searchCmd);
|
|
323
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
324
|
+
console.error(err instanceof Error ? err.message : err);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pebblehouse/odin-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI for Odin — the knowledge backbone for Pebble House",
|
|
6
|
+
"bin": {
|
|
7
|
+
"odin": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsup --watch"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"odin",
|
|
18
|
+
"pebblehouse",
|
|
19
|
+
"cli",
|
|
20
|
+
"knowledge",
|
|
21
|
+
"context"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/pebblehouse/odin-web.git",
|
|
26
|
+
"directory": "packages/cli"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://www.odin.mu",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@supabase/supabase-js": "^2.47.10",
|
|
35
|
+
"commander": "^12.0.0",
|
|
36
|
+
"open": "^10.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"tsup": "^8.0.0",
|
|
40
|
+
"typescript": "^5.3.3"
|
|
41
|
+
}
|
|
42
|
+
}
|