northbase 0.1.1 → 0.1.4

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 CHANGED
@@ -1,4 +1,4 @@
1
- # mybot-notes-cli
1
+ # northbase
2
2
 
3
3
  A local-first CLI for reading and writing text files stored in a Supabase `public.files` table. Uses email/password auth with RLS so each user only sees their own files. Session is stored locally — no env file required.
4
4
 
@@ -40,7 +40,7 @@ create trigger files_updated_at
40
40
 
41
41
  ```bash
42
42
  npm install
43
- npm link # makes `mybot` available globally on your PATH
43
+ npm link # makes `northbase` available globally on your PATH
44
44
  ```
45
45
 
46
46
  No environment file needed. The Supabase project URL and anon key are public constants baked into the CLI.
@@ -52,27 +52,27 @@ No environment file needed. The Supabase project URL and anon key are public con
52
52
  Prompts for your email and password interactively (password is not echoed):
53
53
 
54
54
  ```bash
55
- mybot login
55
+ northbase login
56
56
  # Email: you@example.com
57
57
  # Password:
58
58
  # Logged in.
59
59
  ```
60
60
 
61
- Your session (access + refresh tokens) is stored at `~/.mybot/session.json` with mode `600`. Tokens are refreshed automatically when they expire — you should only need to log in once.
61
+ Your session (access + refresh tokens) is stored at `~/.northbase/session.json` with mode `600`. Tokens are refreshed automatically when they expire — you should only need to log in once.
62
62
 
63
63
  ### Log out
64
64
 
65
65
  ```bash
66
- mybot logout
66
+ northbase logout
67
67
  # Logged out.
68
68
  ```
69
69
 
70
- Deletes `~/.mybot/session.json` and signs out server-side.
70
+ Deletes `~/.northbase/session.json` and signs out server-side.
71
71
 
72
72
  ### Check who you are
73
73
 
74
74
  ```bash
75
- mybot whoami
75
+ northbase whoami
76
76
  # Logged in as you@example.com (uuid...)
77
77
  ```
78
78
 
@@ -80,11 +80,11 @@ mybot whoami
80
80
 
81
81
  ### Get a file
82
82
 
83
- Fetches file content and prints it to stdout. Uses a local cache at `~/.mybot/files/` and only downloads from Supabase when the remote `updated_at` timestamp differs from the cached value.
83
+ Fetches file content and prints it to stdout. Uses a local cache at `~/.northbase/files/` and only downloads from Supabase when the remote `updated_at` timestamp differs from the cached value.
84
84
 
85
85
  ```bash
86
- mybot get ideas.md
87
- mybot get notes/todo.txt
86
+ northbase get ideas.md
87
+ northbase get notes/todo.txt
88
88
  ```
89
89
 
90
90
  ### Put a file
@@ -92,9 +92,9 @@ mybot get notes/todo.txt
92
92
  Reads content from stdin, upserts it to Supabase, then updates the local cache.
93
93
 
94
94
  ```bash
95
- printf "hello world\n" | mybot put test/cli.md
96
- cat my-local-file.md | mybot put ideas.md
97
- echo "updated content" | mybot put notes/todo.txt
95
+ printf "hello world\n" | northbase put test/cli.md
96
+ cat my-local-file.md | northbase put ideas.md
97
+ echo "updated content" | northbase put notes/todo.txt
98
98
  ```
99
99
 
100
100
  On success, prints a single confirmation line to stdout:
@@ -103,24 +103,50 @@ On success, prints a single confirmation line to stdout:
103
103
  PUT ok test/cli.md bytes=12 updated_at=2024-01-15T10:30:00.000Z
104
104
  ```
105
105
 
106
+ ### List remote files
107
+
108
+ Prints every file path stored remotely, one per line. Useful for discovery or scripting.
109
+
110
+ ```bash
111
+ northbase list # all files
112
+ northbase list memory/ # only paths starting with "memory/"
113
+ ```
114
+
115
+ ### Pull (bulk sync)
116
+
117
+ Downloads all remote files whose `updated_at` differs from the local cache. Useful for "discover and mirror all your notes so an agent can read them locally."
118
+
119
+ ```bash
120
+ northbase pull # sync everything
121
+ northbase pull memory/ # sync only files under memory/ prefix
122
+ ```
123
+
124
+ Prints a summary line to stdout:
125
+
126
+ ```
127
+ PULL ok files=12 downloaded=3 skipped=9
128
+ ```
129
+
130
+ Per-file download progress goes to stderr. Pull never deletes local files.
131
+
106
132
  ## Local mirror
107
133
 
108
134
  All files are mirrored at:
109
135
 
110
136
  ```
111
- ~/.mybot/files/<path>
137
+ ~/.northbase/files/<path>
112
138
  ```
113
139
 
114
140
  Metadata (timestamps and byte counts) is stored at:
115
141
 
116
142
  ```
117
- ~/.mybot/index.json
143
+ ~/.northbase/index.json
118
144
  ```
119
145
 
120
146
  Session tokens are stored at:
121
147
 
122
148
  ```
123
- ~/.mybot/session.json (mode 600 — readable only by you)
149
+ ~/.northbase/session.json (mode 600 — readable only by you)
124
150
  ```
125
151
 
126
152
  The CLI compares `updated_at` timestamps before downloading — if your local copy is current, no content fetch is made.
@@ -135,8 +161,8 @@ The CLI compares `updated_at` timestamps before downloading — if your local co
135
161
  Debug logs go to stderr so they never pollute stdout pipelines:
136
162
 
137
163
  ```
138
- MYBOT GET local-hit ideas.md # served from local cache
139
- MYBOT GET remote-refresh ideas.md # downloaded from Supabase
140
- MYBOT PUT test/cli.md bytes=12 updated_at=2024-01-15T10:30:00.000Z
141
- MYBOT session refreshing # printed when access token is silently renewed
164
+ NORTHBASE GET local-hit ideas.md # served from local cache
165
+ NORTHBASE GET remote-refresh ideas.md # downloaded from Supabase
166
+ NORTHBASE PUT test/cli.md bytes=12 updated_at=2024-01-15T10:30:00.000Z
167
+ NORTHBASE session refreshing # printed when access token is silently renewed
142
168
  ```
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "northbase",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "Local-first CLI for reading and writing text files stored in Supabase",
5
5
  "type": "module",
6
6
  "bin": {
7
- "northbase": "./src/mybot.mjs"
7
+ "northbase": "src/northbase.mjs"
8
8
  },
9
9
  "engines": {
10
10
  "node": ">=18"
@@ -10,11 +10,11 @@ import { createClient } from "@supabase/supabase-js";
10
10
  const SUPABASE_URL = "https://ivxgpjracfctkkdhlwgm.supabase.co";
11
11
  const SUPABASE_KEY = "sb_publishable_LZkkAwsx9q5KgIAeoZAO_A_U88rfFHL";
12
12
 
13
- const MYBOT_DIR = path.join(os.homedir(), ".mybot");
14
- const ROOT = path.join(MYBOT_DIR, "files");
15
- const INDEX_PATH = path.join(MYBOT_DIR, "index.json");
16
- const SESSION_PATH = path.join(MYBOT_DIR, "session.json");
17
- const MAX_BYTES = 500_000;
13
+ const NORTHBASE_DIR = path.join(os.homedir(), ".northbase");
14
+ const ROOT = path.join(NORTHBASE_DIR, "files");
15
+ const INDEX_PATH = path.join(NORTHBASE_DIR, "index.json");
16
+ const SESSION_PATH = path.join(NORTHBASE_DIR, "session.json");
17
+ const MAX_BYTES = 500_000;
18
18
 
19
19
  // ── directory / index helpers ─────────────────────────────────────────────────
20
20
 
@@ -43,12 +43,12 @@ function loadSession() {
43
43
  if (!s?.access_token || !s?.refresh_token) throw new Error("incomplete");
44
44
  return s;
45
45
  } catch {
46
- throw new Error("Not logged in. Run `mybot login`.");
46
+ throw new Error("Not logged in. Run `northbase login`.");
47
47
  }
48
48
  }
49
49
 
50
50
  function saveSession(session) {
51
- ensureDir(MYBOT_DIR);
51
+ ensureDir(NORTHBASE_DIR);
52
52
  fs.writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2), { mode: 0o600 });
53
53
  }
54
54
 
@@ -65,15 +65,15 @@ async function getAuthenticatedClient() {
65
65
  auth: { persistSession: false, autoRefreshToken: false },
66
66
  });
67
67
 
68
- const nowSec = Math.floor(Date.now() / 1000);
68
+ const nowSec = Math.floor(Date.now() / 1000);
69
69
  const isExpired = stored.expires_at && nowSec >= stored.expires_at - 60;
70
70
 
71
71
  if (isExpired) {
72
- console.error("MYBOT session refreshing");
72
+ console.error("NORTHBASE session refreshing");
73
73
  const { data, error } = await supabase.auth.refreshSession({
74
74
  refresh_token: stored.refresh_token,
75
75
  });
76
- if (error) throw new Error("Session expired. Run `mybot login` again.");
76
+ if (error) throw new Error("Session expired. Run `northbase login` again.");
77
77
  saveSession(data.session);
78
78
  const { error: setErr } = await supabase.auth.setSession({
79
79
  access_token: data.session.access_token,
@@ -85,7 +85,7 @@ async function getAuthenticatedClient() {
85
85
  access_token: stored.access_token,
86
86
  refresh_token: stored.refresh_token,
87
87
  });
88
- if (error) throw new Error("Session invalid. Run `mybot login` again.");
88
+ if (error) throw new Error("Session invalid. Run `northbase login` again.");
89
89
  }
90
90
 
91
91
  return supabase;
@@ -153,7 +153,7 @@ async function getFile(rel) {
153
153
  const cached = idx.files?.[relSafe];
154
154
 
155
155
  if (local === null) {
156
- console.error("MYBOT GET remote-refresh", relSafe);
156
+ console.error("NORTHBASE GET remote-refresh", relSafe);
157
157
  const { content, updated_at } = await fetchRemoteContent(supabase, relSafe);
158
158
  const bytes = Buffer.byteLength(content, "utf8");
159
159
  if (bytes > MAX_BYTES) throw new Error(`File too large (${bytes} bytes)`);
@@ -167,11 +167,11 @@ async function getFile(rel) {
167
167
  if (!remoteUpdatedAt) return local;
168
168
 
169
169
  if (cached?.updated_at === remoteUpdatedAt) {
170
- console.error("MYBOT GET local-hit", relSafe);
170
+ console.error("NORTHBASE GET local-hit", relSafe);
171
171
  return local;
172
172
  }
173
173
 
174
- console.error("MYBOT GET remote-refresh", relSafe);
174
+ console.error("NORTHBASE GET remote-refresh", relSafe);
175
175
  const { content, updated_at } = await fetchRemoteContent(supabase, relSafe);
176
176
  const bytes = Buffer.byteLength(content, "utf8");
177
177
  if (bytes > MAX_BYTES) throw new Error(`File too large (${bytes} bytes)`);
@@ -201,10 +201,72 @@ async function putFile(rel, content) {
201
201
  idx.files[relSafe] = { updated_at, bytes };
202
202
  saveIndex(idx);
203
203
 
204
- console.error("MYBOT PUT", relSafe, `bytes=${bytes}`, `updated_at=${updated_at}`);
204
+ console.error("NORTHBASE PUT", relSafe, `bytes=${bytes}`, `updated_at=${updated_at}`);
205
205
  return { bytes, updated_at };
206
206
  }
207
207
 
208
+ // ── concurrency helper ────────────────────────────────────────────────────────
209
+
210
+ async function concurrentMap(items, limit, fn) {
211
+ const results = [];
212
+ let i = 0;
213
+ async function worker() {
214
+ while (i < items.length) {
215
+ const idx = i++;
216
+ results[idx] = await fn(items[idx]);
217
+ }
218
+ }
219
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
220
+ return results;
221
+ }
222
+
223
+ // ── list / pull commands ──────────────────────────────────────────────────────
224
+
225
+ async function cmdList(prefix) {
226
+ const supabase = await getAuthenticatedClient();
227
+ let query = supabase.from("files").select("path").order("path", { ascending: true });
228
+ if (prefix) query = query.like("path", `${prefix}%`);
229
+ const { data, error } = await query;
230
+ if (error) throw error;
231
+ for (const row of data) process.stdout.write(row.path + "\n");
232
+ }
233
+
234
+ async function cmdPull(prefix) {
235
+ ensureDir(ROOT);
236
+ const idx = loadIndex();
237
+ const supabase = await getAuthenticatedClient();
238
+
239
+ let query = supabase.from("files").select("path, updated_at").order("path", { ascending: true });
240
+ if (prefix) query = query.like("path", `${prefix}%`);
241
+ const { data: remote, error } = await query;
242
+ if (error) throw error;
243
+
244
+ let downloaded = 0, skipped = 0;
245
+
246
+ await concurrentMap(remote, 5, async (row) => {
247
+ const rel = row.path;
248
+ const cached = idx.files?.[rel];
249
+ if (cached?.updated_at === row.updated_at) {
250
+ skipped++;
251
+ return;
252
+ }
253
+ console.error("NORTHBASE PULL download", rel);
254
+ const { data: rows, error: err } = await supabase
255
+ .from("files").select("content, updated_at").eq("path", rel).limit(1);
256
+ if (err) throw err;
257
+ const content = rows?.[0]?.content ?? "";
258
+ const updated_at = rows?.[0]?.updated_at ?? row.updated_at;
259
+ const bytes = Buffer.byteLength(content, "utf8");
260
+ if (bytes > MAX_BYTES) throw new Error(`File too large (${bytes} bytes): ${rel}`);
261
+ writeLocal(rel, content);
262
+ idx.files[rel] = { updated_at, bytes };
263
+ downloaded++;
264
+ });
265
+
266
+ saveIndex(idx);
267
+ console.log(`PULL ok files=${remote.length} downloaded=${downloaded} skipped=${skipped}`);
268
+ }
269
+
208
270
  // ── interactive prompts ───────────────────────────────────────────────────────
209
271
 
210
272
  async function promptLine(question) {
@@ -218,7 +280,7 @@ async function promptLine(question) {
218
280
 
219
281
  async function promptPassword(question) {
220
282
  if (!process.stdin.isTTY) {
221
- throw new Error("`mybot login` requires an interactive terminal (stdin is not a TTY).");
283
+ throw new Error("`northbase login` requires an interactive terminal (stdin is not a TTY).");
222
284
  }
223
285
  return new Promise((resolve) => {
224
286
  process.stdout.write(question);
@@ -299,13 +361,15 @@ async function cmdWhoami() {
299
361
  async function main() {
300
362
  const [cmd, ...args] = process.argv.slice(2);
301
363
 
302
- if (cmd === "login") { await cmdLogin(); return; }
303
- if (cmd === "logout") { await cmdLogout(); return; }
304
- if (cmd === "whoami") { await cmdWhoami(); return; }
364
+ if (cmd === "login") { await cmdLogin(); return; }
365
+ if (cmd === "logout") { await cmdLogout(); return; }
366
+ if (cmd === "whoami") { await cmdWhoami(); return; }
367
+ if (cmd === "list") { await cmdList(args[0]); return; }
368
+ if (cmd === "pull") { await cmdPull(args[0]); return; }
305
369
 
306
370
  if (cmd === "get") {
307
371
  const rel = args[0];
308
- if (!rel) { console.log("Usage: mybot get <path>"); process.exit(1); }
372
+ if (!rel) { console.log("Usage: northbase get <path>"); process.exit(1); }
309
373
  const content = await getFile(rel);
310
374
  process.stdout.write(content);
311
375
  return;
@@ -313,7 +377,7 @@ async function main() {
313
377
 
314
378
  if (cmd === "put") {
315
379
  const rel = args[0];
316
- if (!rel) { console.log("Usage: mybot put <path>"); process.exit(1); }
380
+ if (!rel) { console.log("Usage: northbase put <path>"); process.exit(1); }
317
381
  const chunks = [];
318
382
  for await (const chunk of process.stdin) chunks.push(chunk);
319
383
  const content = Buffer.concat(chunks).toString("utf8");
@@ -323,15 +387,17 @@ async function main() {
323
387
  }
324
388
 
325
389
  console.log("Usage:");
326
- console.log(" mybot login");
327
- console.log(" mybot logout");
328
- console.log(" mybot whoami");
329
- console.log(" mybot get <path>");
330
- console.log(" mybot put <path>");
390
+ console.log(" northbase login");
391
+ console.log(" northbase logout");
392
+ console.log(" northbase whoami");
393
+ console.log(" northbase list [prefix]");
394
+ console.log(" northbase pull [prefix]");
395
+ console.log(" northbase get <path>");
396
+ console.log(" northbase put <path>");
331
397
  process.exit(1);
332
398
  }
333
399
 
334
400
  main().catch((e) => {
335
- console.error("MYBOT:", e?.message ?? e);
401
+ console.error("NORTHBASE:", e?.message ?? e);
336
402
  process.exit(1);
337
403
  });