@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 +22 -0
- package/README.md +246 -0
- package/assets/extensions/seedclub/api-client.ts +102 -0
- package/assets/extensions/seedclub/auth.ts +89 -0
- package/assets/extensions/seedclub/commands/add.ts +601 -0
- package/assets/extensions/seedclub/commands/seedclub.ts +67 -0
- package/assets/extensions/seedclub/commands/signals.ts +86 -0
- package/assets/extensions/seedclub/commands/sort.ts +91 -0
- package/assets/extensions/seedclub/dia-cookies.ts +126 -0
- package/assets/extensions/seedclub/index.ts +166 -0
- package/assets/extensions/seedclub/package-lock.json +65 -0
- package/assets/extensions/seedclub/package.json +11 -0
- package/assets/extensions/seedclub/tool-utils.ts +32 -0
- package/assets/extensions/seedclub/tools/signals.ts +275 -0
- package/assets/extensions/seedclub/tools/utility.ts +31 -0
- package/assets/extensions/seedclub/twitter-client.ts +277 -0
- package/assets/extensions/seedclub-ui/editor.ts +93 -0
- package/assets/extensions/seedclub-ui/index.ts +15 -0
- package/assets/extensions/seedclub-ui/update.ts +73 -0
- package/assets/extensions/seedclub-ui/welcome.ts +250 -0
- package/assets/theme/seedclub.json +86 -0
- package/bin/cli.js +195 -0
- package/package.json +30 -0
- package/postinstall.js +175 -0
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
|
+
}
|