@clubnet/seedclub 0.2.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Seed Club
4
+ Copyright (c) 2025 Mario Zechner (original pi project)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # seedclub
2
+
3
+ The Human+ Venture Network — AI agent for deal sourcing, research, and signal tracking.
4
+
5
+ Requirements: Node.js 22+
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @clubnet/seedclub
11
+ ```
12
+
13
+ Then:
14
+
15
+ ```bash
16
+ seedclub
17
+ ```
18
+
19
+ Use `/login` to set up your API keys.
20
+
21
+ ### Alternative: curl | bash
22
+
23
+ ```bash
24
+ curl -fsSL https://raw.githubusercontent.com/seedclub/seedclub/main/install.sh | bash
25
+ ```
26
+
27
+ ### Team onboarding (private package)
28
+
29
+ seedclub is published to npmjs as a private package in the `@clubnet` org.
30
+
31
+ Fast path:
32
+
33
+ ```bash
34
+ SEEDCLUB_NPM_TOKEN=YOUR_NPM_TOKEN curl -fsSL https://raw.githubusercontent.com/seedclub/seedclub/main/install.sh | bash
35
+ seedclub setup-auth
36
+ ```
37
+
38
+ Manual one-time `.npmrc` setup:
39
+
40
+ ```bash
41
+ echo "@clubnet:registry=https://registry.npmjs.org/" >> ~/.npmrc
42
+ echo "//registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN" >> ~/.npmrc
43
+ ```
44
+
45
+ Then `npm install -g @clubnet/seedclub` works.
46
+
47
+ ## Commands
48
+
49
+ | Command | What it does |
50
+ |---|---|
51
+ | `/seedclub` | Main menu — connect, add signals, sort signals |
52
+ | `/add <url>` | Add a signal (any URL) |
53
+ | `/sort` | Sort signals into angel.md values |
54
+ | `seedclub setup-auth` | Configure npm auth for npmjs private package access in `~/.npmrc` |
55
+
56
+ ## How it works
57
+
58
+ seedclub is an npm package (`@clubnet/seedclub`) that wraps [pi](https://github.com/badlogic/pi-mono) as a dependency. Installing the package globally gives you the `seedclub` command.
59
+
60
+ The `seedclub` CLI wrapper does three things:
61
+ 1. Sets `PI_CODING_AGENT_DIR` to point pi at `~/.seedclub/agent/`
62
+ 2. Sets `PI_SKIP_VERSION_CHECK=1` so pi's update banner never shows
63
+ 3. Spawns the pi binary from the package's own `node_modules/`
64
+
65
+ A `postinstall` script runs after every install/update and sets up:
66
+
67
+ ```
68
+ ~/.seedclub/
69
+ └── agent/
70
+ ├── extensions/
71
+ │ ├── seedclub/ ← core: auth, tools, commands
72
+ │ └── seedclub-ui/ ← UI: welcome screen, update check
73
+ ├── themes/
74
+ │ └── seedclub.json
75
+ ├── settings.json
76
+ └── .seedclub-version
77
+ ```
78
+
79
+ This means:
80
+ - Running `seedclub` → uses `~/.seedclub/agent/` config, package-local pi binary
81
+ - Running `pi` (if installed separately) → uses `~/.pi/agent/` config, totally independent
82
+ - **seedclub never modifies pi's installation**
83
+
84
+ ## Version pinning
85
+
86
+ seedclub pins versions in `package.json`:
87
+
88
+ ```json
89
+ {
90
+ "version": "0.2.0",
91
+ "dependencies": {
92
+ "@mariozechner/pi-coding-agent": "0.52.9"
93
+ }
94
+ }
95
+ ```
96
+
97
+ Users never interact with pi's npm package directly. When they run `seedclub update`, it runs `npm install -g @clubnet/seedclub@latest`.
98
+
99
+ ## Theme
100
+
101
+ The seedclub theme lives at `assets/theme/seedclub.json` and gets installed to `~/.seedclub/agent/themes/seedclub.json`.
102
+
103
+ It's a standard pi theme with 51 color tokens. Edit it to change any visual aspect of the app:
104
+
105
+ | Section | What it controls |
106
+ |---|---|
107
+ | **Core UI** | `accent`, `border`, `success`, `error`, `warning`, `muted`, `text` |
108
+ | **Backgrounds** | User messages, tool boxes (pending/success/error), selection highlight |
109
+ | **Markdown** | Headings, links, code blocks, quotes, list bullets |
110
+ | **Syntax** | Comments, keywords, functions, strings, numbers, types |
111
+ | **Thinking borders** | Editor border color per thinking level (off → xhigh) |
112
+ | **Diffs** | Added/removed/context lines in tool output |
113
+
114
+ The `vars` block at the top defines reusable colors (e.g. `brand: "#00C853"`) that are referenced throughout `colors`. To change the brand color, just update `vars.brand`.
115
+
116
+ **Hot reload:** Edit the theme file while seedclub is running and it reloads instantly.
117
+
118
+ Colors can be hex (`"#00C853"`), 256-color palette index (`242`), a reference to a `vars` entry (`"brand"`), or empty string (`""`) for the terminal default.
119
+
120
+ ## Development
121
+
122
+ ### Setup
123
+
124
+ ```bash
125
+ git clone https://github.com/seedclub/seedclub.git
126
+ cd seedclub
127
+
128
+ # Install locally from the repo
129
+ npm install -g ./
130
+ ```
131
+
132
+ ### Repo structure
133
+
134
+ ```
135
+ seedclub/
136
+ ├── package.json ← npm package definition (@clubnet/seedclub)
137
+ ├── bin/cli.js ← Node.js CLI wrapper (the `seedclub` command)
138
+ ├── postinstall.js ← runs after npm install (sets up ~/.seedclub/agent/)
139
+ ├── install.sh ← curl | bash installer (just runs npm install -g)
140
+ ├── README.md
141
+ └── assets/
142
+ ├── theme/
143
+ │ └── seedclub.json
144
+ └── extensions/
145
+ ├── seedclub/ ← core extension source
146
+ └── seedclub-ui/ ← UI extension source
147
+ ```
148
+
149
+ The `assets/` directory contains the canonical source for extensions and themes. The postinstall script copies these into `~/.seedclub/agent/`.
150
+
151
+ ### Updating pi
152
+
153
+ When a new pi version comes out:
154
+
155
+ 1. **Test it locally:**
156
+ ```bash
157
+ # Update the dependency
158
+ npm install @mariozechner/pi-coding-agent@NEW_VERSION
159
+ npm install -g ./
160
+ seedclub # test everything works
161
+ ```
162
+
163
+ 2. **Bump versions:**
164
+ Update the `package.json` dependency and package version:
165
+ ```json
166
+ {
167
+ "version": "0.3.0",
168
+ "dependencies": {
169
+ "@mariozechner/pi-coding-agent": "0.52.12"
170
+ }
171
+ }
172
+ ```
173
+
174
+ 3. **Publish:**
175
+ ```bash
176
+ git add -A
177
+ git commit -m "bump pi to 0.52.12"
178
+ git push
179
+ npm publish
180
+ ```
181
+
182
+ Users get the update when they run `seedclub update`.
183
+
184
+ ### Updating extensions
185
+
186
+ Extensions live in `assets/extensions/`. Edit them there, then:
187
+
188
+ ```bash
189
+ # Test locally
190
+ npm install -g ./
191
+ seedclub # test
192
+
193
+ # When ready, bump package version and publish
194
+ git add -A
195
+ git commit -m "update extensions"
196
+ git push
197
+ npm publish
198
+ ```
199
+
200
+ For reproducible extension dependency installs, commit `assets/extensions/seedclub/package-lock.json` and keep it in sync when changing extension deps.
201
+
202
+ ### Release checklist
203
+
204
+ 1. Test new pi version locally
205
+ 2. Test extensions work
206
+ 3. Bump version in `package.json`
207
+ 4. Push to main
208
+ 5. `npm publish`
209
+
210
+ ### Publishing
211
+
212
+ ```bash
213
+ # One-time setup
214
+ echo "@clubnet:registry=https://registry.npmjs.org/" >> ~/.npmrc
215
+ echo "//registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN" >> ~/.npmrc
216
+
217
+ # Publish
218
+ npm publish --access restricted
219
+ ```
220
+
221
+ ## Update
222
+
223
+ ```bash
224
+ seedclub update
225
+ ```
226
+
227
+ ## Uninstall
228
+
229
+ ```bash
230
+ npm uninstall -g @clubnet/seedclub
231
+ rm -rf ~/.seedclub
232
+ ```
233
+
234
+ ### Coming from the old version (curl | bash)
235
+
236
+ The previous version installed into `~/.seedclub/bin/` and modified your PATH. The npm package cleans this up automatically, but you can also remove the old PATH line from your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) manually.
237
+
238
+ ```bash
239
+ npm install -g @clubnet/seedclub
240
+ ```
241
+
242
+ If you have pi installed globally (`npm install -g @mariozechner/pi-coding-agent`), that's fine — seedclub and pi are completely independent.
243
+
244
+ ## License
245
+
246
+ MIT
@@ -0,0 +1,102 @@
1
+ /**
2
+ * API Client for Seed Club.
3
+ * Simple HTTP client with Bearer token auth.
4
+ */
5
+
6
+ import { clearStoredToken, getApiBase, getToken } from "./auth.js";
7
+
8
+ let cachedToken: string | null = null;
9
+ let cachedApiBase: string | null = null;
10
+
11
+ export class ApiError extends Error {
12
+ constructor(
13
+ public status: number,
14
+ message: string,
15
+ public details?: unknown,
16
+ ) {
17
+ super(message);
18
+ this.name = "ApiError";
19
+ }
20
+ }
21
+
22
+ export class NotConnectedError extends Error {
23
+ constructor() {
24
+ super("Not connected. Run /seedclub to authenticate.");
25
+ this.name = "NotConnectedError";
26
+ }
27
+ }
28
+
29
+ export function setCachedToken(token: string, apiBase: string): void {
30
+ cachedToken = token;
31
+ cachedApiBase = apiBase;
32
+ }
33
+
34
+ export async function clearCredentials(): Promise<void> {
35
+ cachedToken = null;
36
+ cachedApiBase = null;
37
+ await clearStoredToken();
38
+ }
39
+
40
+ async function getAuthToken(): Promise<string> {
41
+ if (cachedToken) return cachedToken;
42
+ const token = await getToken();
43
+ if (!token) throw new NotConnectedError();
44
+ cachedToken = token;
45
+ cachedApiBase = getApiBase();
46
+ return token;
47
+ }
48
+
49
+ interface RequestOptions {
50
+ method?: "GET" | "POST" | "PATCH" | "DELETE";
51
+ body?: unknown;
52
+ params?: Record<string, string | number | undefined>;
53
+ }
54
+
55
+ async function apiRequest<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
56
+ const { method = "GET", body, params } = options;
57
+ const token = await getAuthToken();
58
+ const apiBase = cachedApiBase || getApiBase();
59
+
60
+ const url = new URL(`/api/mcp${endpoint}`, apiBase);
61
+ if (params) {
62
+ for (const [key, value] of Object.entries(params)) {
63
+ if (value !== undefined) url.searchParams.set(key, String(value));
64
+ }
65
+ }
66
+
67
+ const response = await fetch(url.toString(), {
68
+ method,
69
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
70
+ body: body ? JSON.stringify(body) : undefined,
71
+ });
72
+
73
+ if (response.status === 401) {
74
+ await clearCredentials();
75
+ throw new ApiError(401, "Token expired or revoked. Run /seedclub to reconnect.");
76
+ }
77
+
78
+ const text = await response.text();
79
+ let data: any;
80
+ try {
81
+ data = text ? JSON.parse(text) : {};
82
+ } catch {
83
+ if (!response.ok) {
84
+ throw new ApiError(response.status, `Request failed (${response.status}): ${text.slice(0, 200)}`);
85
+ }
86
+ throw new ApiError(response.status, `Invalid JSON response from server: ${text.slice(0, 200)}`);
87
+ }
88
+
89
+ if (!response.ok) {
90
+ throw new ApiError(response.status, data.error || `Request failed (${response.status})`, data.details);
91
+ }
92
+ return data as T;
93
+ }
94
+
95
+ export const api = {
96
+ get: <T>(endpoint: string, params?: Record<string, string | number | undefined>) =>
97
+ apiRequest<T>(endpoint, { method: "GET", params }),
98
+ post: <T>(endpoint: string, body: unknown) => apiRequest<T>(endpoint, { method: "POST", body }),
99
+ patch: <T>(endpoint: string, body: unknown) => apiRequest<T>(endpoint, { method: "PATCH", body }),
100
+ delete: <T>(endpoint: string, params?: Record<string, string | number | undefined>) =>
101
+ apiRequest<T>(endpoint, { method: "DELETE", params }),
102
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Token storage for Seed Club.
3
+ *
4
+ * Priority: SEEDCLUB_TOKEN env var > stored token file.
5
+ * Use /seedclub to connect.
6
+ */
7
+
8
+ import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+
12
+ const CONFIG_DIR = join(homedir(), ".config", "seedclub");
13
+ const TOKEN_FILE = join(CONFIG_DIR, "token");
14
+ const LEGACY_CONFIG_DIR = join(homedir(), ".config", "looseleaf");
15
+ const LEGACY_TOKEN_FILE = join(LEGACY_CONFIG_DIR, "token");
16
+ const DEFAULT_API_BASE = "https://looseleaf-rouge.vercel.app";
17
+
18
+ export interface StoredToken {
19
+ token: string;
20
+ email: string;
21
+ createdAt: string;
22
+ apiBase: string;
23
+ }
24
+
25
+ let _cachedApiBase: string | null = null;
26
+
27
+ export function getApiBase(): string {
28
+ if (process.env.SEEDCLUB_API || process.env.SEED_NETWORK_API)
29
+ return process.env.SEEDCLUB_API || process.env.SEED_NETWORK_API!;
30
+ if (_cachedApiBase) return _cachedApiBase;
31
+ return DEFAULT_API_BASE;
32
+ }
33
+
34
+ export function setCachedApiBase(apiBase: string): void {
35
+ _cachedApiBase = apiBase;
36
+ }
37
+
38
+ export function clearCachedApiBase(): void {
39
+ _cachedApiBase = null;
40
+ }
41
+
42
+ async function tryReadTokenFile(path: string): Promise<StoredToken | null> {
43
+ try {
44
+ const content = await readFile(path, "utf-8");
45
+ const stored = JSON.parse(content) as StoredToken;
46
+ if (!stored.token || !stored.token.startsWith("sn_")) return null;
47
+ if (stored.apiBase && !process.env.SEEDCLUB_API) {
48
+ _cachedApiBase = stored.apiBase;
49
+ }
50
+ return stored;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ export async function getStoredToken(): Promise<StoredToken | null> {
57
+ // Try new location first, fall back to legacy
58
+ const stored = await tryReadTokenFile(TOKEN_FILE);
59
+ if (stored) return stored;
60
+ return await tryReadTokenFile(LEGACY_TOKEN_FILE);
61
+ }
62
+
63
+ export async function getToken(): Promise<string | null> {
64
+ if (process.env.SEEDCLUB_TOKEN || process.env.SEED_NETWORK_TOKEN)
65
+ return (process.env.SEEDCLUB_TOKEN || process.env.SEED_NETWORK_TOKEN)!;
66
+ const stored = await getStoredToken();
67
+ return stored?.token ?? null;
68
+ }
69
+
70
+ export async function storeToken(token: string, email: string, apiBase: string): Promise<void> {
71
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
72
+ await writeFile(
73
+ TOKEN_FILE,
74
+ JSON.stringify({ token, email, createdAt: new Date().toISOString(), apiBase }, null, 2),
75
+ { mode: 0o600 },
76
+ );
77
+ try {
78
+ await chmod(TOKEN_FILE, 0o600);
79
+ } catch {}
80
+ }
81
+
82
+ export async function clearStoredToken(): Promise<boolean> {
83
+ try {
84
+ await unlink(TOKEN_FILE);
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ }
89
+ }