@clubnet/seedclub 0.2.6 → 0.2.7

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
@@ -71,7 +71,8 @@ A `postinstall` script runs after every install/update and sets up:
71
71
  │ ├── seedclub/ ← core: auth, tools, commands
72
72
  │ └── seedclub-ui/ ← UI: welcome screen, update check
73
73
  ├── themes/
74
- └── seedclub.json
74
+ ├── dark.json
75
+ │ └── light.json
75
76
  ├── settings.json
76
77
  └── .seedclub-version
77
78
  ```
@@ -98,7 +99,7 @@ Users never interact with pi's npm package directly. When they run `seedclub upd
98
99
 
99
100
  ## Theme
100
101
 
101
- The seedclub theme lives at `assets/theme/seedclub.json` and gets installed to `~/.seedclub/agent/themes/seedclub.json`.
102
+ Themes live under `assets/theme/` and are installed to `~/.seedclub/agent/themes/` (currently `dark.json` and `light.json`).
102
103
 
103
104
  It's a standard pi theme with 51 color tokens. Edit it to change any visual aspect of the app:
104
105
 
@@ -140,7 +141,8 @@ seedclub/
140
141
  ├── README.md
141
142
  └── assets/
142
143
  ├── theme/
143
- └── seedclub.json
144
+ ├── dark.json
145
+ │ └── light.json
144
146
  └── extensions/
145
147
  ├── seedclub/ ← core extension source
146
148
  └── seedclub-ui/ ← UI extension source
@@ -18,6 +18,7 @@ const DEFAULT_API_BASE = "https://looseleaf-rouge.vercel.app";
18
18
  export interface StoredToken {
19
19
  token: string;
20
20
  email: string;
21
+ name?: string;
21
22
  createdAt: string;
22
23
  apiBase: string;
23
24
  }
@@ -67,11 +68,20 @@ export async function getToken(): Promise<string | null> {
67
68
  return stored?.token ?? null;
68
69
  }
69
70
 
70
- export async function storeToken(token: string, email: string, apiBase: string): Promise<void> {
71
+ export async function storeToken(
72
+ token: string,
73
+ email: string,
74
+ apiBase: string,
75
+ options?: { name?: string },
76
+ ): Promise<void> {
71
77
  await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
72
78
  await writeFile(
73
79
  TOKEN_FILE,
74
- JSON.stringify({ token, email, createdAt: new Date().toISOString(), apiBase }, null, 2),
80
+ JSON.stringify(
81
+ { token, email, name: options?.name, createdAt: new Date().toISOString(), apiBase },
82
+ null,
83
+ 2,
84
+ ),
75
85
  { mode: 0o600 },
76
86
  );
77
87
  try {
@@ -14,6 +14,13 @@ interface SeedclubDeps {
14
14
  }
15
15
 
16
16
  export function registerSeedclubCommand(pi: ExtensionAPI, deps: SeedclubDeps) {
17
+ pi.registerCommand("connect", {
18
+ description: "Connect your Seed Club account",
19
+ handler: async (args, ctx) => {
20
+ await deps.connect(args, ctx);
21
+ },
22
+ });
23
+
17
24
  pi.registerCommand("seedclub", {
18
25
  description: "Seed Club",
19
26
  handler: async (args, ctx) => {
@@ -17,6 +17,11 @@ import { registerSignalTools } from "./tools/signals.js";
17
17
  import { getCurrentUser, registerUtilityTools } from "./tools/utility.js";
18
18
 
19
19
  export default function (pi: ExtensionAPI) {
20
+ const formatSeedLabel = (name?: string, email?: string) => {
21
+ const label = (name?.trim() || email?.trim() || "connected").replace(/\s+/g, " ");
22
+ return label;
23
+ };
24
+
20
25
  // Tools (read-only signals + user info)
21
26
  registerSignalTools(pi);
22
27
  registerUtilityTools(pi);
@@ -31,7 +36,7 @@ export default function (pi: ExtensionAPI) {
31
36
  const stored = await getStoredToken();
32
37
  if (stored) {
33
38
  const isDev = stored.apiBase?.includes("localhost") || stored.apiBase?.includes("127.0.0.1");
34
- ctx.ui.setStatus("seed", `seed: ${stored.email}`);
39
+ ctx.ui.setStatus("seed", formatSeedLabel(stored.name, stored.email));
35
40
  if (isDev) ctx.ui.setStatus("seed-api", `dev: ${stored.apiBase}`);
36
41
  } else if (process.env.SEEDCLUB_TOKEN || process.env.SEED_NETWORK_TOKEN) {
37
42
  ctx.ui.setStatus("seed", "seed: connected (env)");
@@ -89,9 +94,9 @@ export default function (pi: ExtensionAPI) {
89
94
  return;
90
95
  }
91
96
 
92
- await storeToken(token, result.email, apiBase);
93
- ctx.ui.notify(`Connected as ${result.email}`, "success");
94
- ctx.ui.setStatus("seed", `seed: ${result.email}`);
97
+ await storeToken(token, result.email, apiBase, { name: result.name });
98
+ ctx.ui.notify(`Connected as ${result.name || result.email}`, "success");
99
+ ctx.ui.setStatus("seed", formatSeedLabel(result.name, result.email));
95
100
  }
96
101
  }
97
102
 
@@ -1,93 +1,118 @@
1
- /**
2
- * Green Bar Editor — solid emerald background input, full width.
3
- * The text input is a bright green bar. Content above is black bg.
4
- */
5
-
6
- import { type EditorTheme, type TUI, visibleWidth } from "@mariozechner/pi-tui";
1
+ import { type EditorTheme, type TUI, truncateToWidth } from "@mariozechner/pi-tui";
7
2
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
3
  import type { KeybindingsManager } from "@mariozechner/pi-coding-agent";
9
4
  import { CustomEditor } from "@mariozechner/pi-coding-agent";
5
+ import { visibleWidth } from "@mariozechner/pi-tui";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { uiState } from "./state.js";
10
+
11
+ interface Palette {
12
+ surfaceBg: string;
13
+ }
10
14
 
11
- const EMERALD_BG = "\x1b[48;2;80;200;120m";
12
- const BLACK_FG = "\x1b[38;2;14;14;15m";
13
- const EMERALD_FG = "\x1b[38;2;80;200;120m";
14
- const PANEL_BG = "\x1b[48;2;14;14;15m";
15
- const PANEL_FG = EMERALD_FG;
16
- const RESET = "\x1b[0m";
15
+ class SurfaceEditor extends CustomEditor {
16
+ private readonly palette: Palette;
17
+ private static readonly RESET = "\x1b[0m";
18
+ private static readonly INSET = " ";
19
+ private static readonly PLACEHOLDER = "/ for menu";
20
+ private static readonly PLACEHOLDER_DIM = "\x1b[38;2;85;85;85m";
17
21
 
18
- class GreenBarEditor extends CustomEditor {
19
- constructor(tui: TUI, theme: EditorTheme, kb: KeybindingsManager) {
22
+ constructor(tui: TUI, theme: EditorTheme, kb: KeybindingsManager, palette: Palette) {
20
23
  super(tui, theme, kb);
21
- // Disable default border rendering
22
- this.borderColor = (_text: string) => "";
24
+ this.palette = palette;
25
+ this.setPaddingX(2);
23
26
  }
24
27
 
25
28
  render(width: number): string[] {
26
- const rawLines = super.render(width);
27
- const filtered: string[] = [];
28
-
29
- for (const line of rawLines) {
30
- const stripped = this.stripAnsi(line);
31
- if (this.isBorderLine(stripped)) continue;
32
- filtered.push(line);
29
+ if (!uiState.ready) {
30
+ return [];
33
31
  }
32
+ const rawLines = super.render(width);
33
+ const showPlaceholder = this.getText().length === 0;
34
+ let placeholderPlaced = false;
35
+ return rawLines.map((line) => {
36
+ const stripped = this.stripAnsi(line).trim();
37
+ const isBorderLine = stripped.startsWith("─");
38
+ if (isBorderLine) {
39
+ return this.paintLine(line, width, false);
40
+ }
41
+ if (showPlaceholder && !placeholderPlaced) {
42
+ placeholderPlaced = true;
43
+ return this.paintLine(line, width, true);
44
+ }
45
+ return this.paintLine(line, width, false);
46
+ });
47
+ }
34
48
 
35
- const paintEmerald = (line: string): string => {
36
- const plain = this.stripAnsi(line);
37
- const vis = visibleWidth(plain);
38
- const pad = " ".repeat(Math.max(0, width - vis));
39
- return `${EMERALD_BG}${BLACK_FG}${plain}${pad}${RESET}`;
40
- };
49
+ override invalidate(): void {
50
+ super.invalidate();
51
+ }
41
52
 
42
- if (filtered.length === 0) {
43
- return [paintEmerald("")];
53
+ override handleInput(data: string): void {
54
+ if (!uiState.ready) {
55
+ // Keep app-level interrupts available while startup UI is loading.
56
+ if (data === "\u0003" || data === "\u0004" || data === "\u001b") {
57
+ super.handleInput(data);
58
+ }
59
+ return;
44
60
  }
61
+ super.handleInput(data);
62
+ }
45
63
 
46
- // Keep input bar emerald, but render slash/autocomplete menu on dark panel
47
- // so it isn't washed out by emerald background.
48
- if (this.isShowingAutocomplete()) {
49
- const [inputLine, ...menuLines] = filtered;
50
- const out = [paintEmerald(inputLine ?? "")];
51
-
52
- for (const menuLine of menuLines) {
53
- const plain = this.stripAnsi(menuLine);
54
- const trimmed = plain.trim();
55
- // Drop visual separator rules for cleaner menu block
56
- if (/^[─-]+$/.test(trimmed)) continue;
57
- const vis = visibleWidth(plain);
58
- const pad = " ".repeat(Math.max(0, width - vis));
59
- const color = trimmed.startsWith("→") || trimmed.startsWith(">") ? EMERALD_FG : PANEL_FG;
60
- out.push(`${PANEL_BG}${color}${plain}${pad}${RESET}`);
64
+ private paintLine(line: string, width: number, addPlaceholder: boolean): string {
65
+ const stripped = this.stripAnsi(line).trim();
66
+ const isBorderLine = stripped.startsWith("─");
67
+ if (isBorderLine) {
68
+ return `${this.palette.surfaceBg}${" ".repeat(Math.max(0, width))}${SurfaceEditor.RESET}`;
69
+ }
70
+ // Editor lines include ANSI resets (e.g. cursor rendering). Re-apply our bg
71
+ // after each reset so the input row stays a solid surface.
72
+ let lineWithStableBg = line.replace(/\x1b\[0m/g, `${SurfaceEditor.RESET}${this.palette.surfaceBg}`);
73
+ if (addPlaceholder) {
74
+ const cursorCell = "\x1b[7m \x1b[0m";
75
+ if (lineWithStableBg.includes(cursorCell)) {
76
+ const slashCursor = "\x1b[7m/\x1b[0m";
77
+ lineWithStableBg = lineWithStableBg.replace(
78
+ cursorCell,
79
+ `${slashCursor}${this.palette.surfaceBg}${SurfaceEditor.PLACEHOLDER_DIM}${SurfaceEditor.PLACEHOLDER.slice(1)}${SurfaceEditor.RESET}${this.palette.surfaceBg}`,
80
+ );
81
+ } else {
82
+ lineWithStableBg += `${this.palette.surfaceBg}${SurfaceEditor.PLACEHOLDER_DIM}${SurfaceEditor.PLACEHOLDER}${SurfaceEditor.RESET}${this.palette.surfaceBg}`;
61
83
  }
62
- return out;
63
84
  }
64
-
65
- return filtered.map((line) => paintEmerald(line));
85
+ const usableWidth = Math.max(1, width - visibleWidth(SurfaceEditor.INSET));
86
+ const shifted = truncateToWidth(lineWithStableBg, usableWidth, "");
87
+ const pad = " ".repeat(Math.max(0, usableWidth - visibleWidth(shifted)));
88
+ return `${this.palette.surfaceBg}${SurfaceEditor.INSET}${shifted}${pad}${SurfaceEditor.RESET}`;
66
89
  }
67
90
 
68
91
  private stripAnsi(s: string): string {
69
92
  return s
70
- // CSI sequences (colors, clears, cursor moves, etc.)
71
93
  .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
72
- // OSC/APC style sequences
73
94
  .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
74
95
  .replace(/\x1b_[^\x1b]*\x1b\\/g, "");
75
96
  }
97
+ }
76
98
 
77
- private isBorderLine(stripped: string): boolean {
78
- const trimmed = stripped.trim();
79
- if (trimmed.length === 0) return false;
80
- // Match lines that are purely border characters
81
- return /^[─╭╮╰╯├┤┬┴│]+$/.test(trimmed);
82
- }
83
-
84
- override invalidate(): void {
85
- super.invalidate();
86
- }
99
+ function pickPalette(): Palette {
100
+ const settingsPath = join(homedir(), ".seedclub", "agent", "settings.json");
101
+ let theme = "dark";
102
+ try {
103
+ if (existsSync(settingsPath)) {
104
+ const raw = JSON.parse(readFileSync(settingsPath, "utf-8"));
105
+ if (typeof raw?.theme === "string") theme = raw.theme.toLowerCase();
106
+ }
107
+ } catch {}
108
+ return theme === "light"
109
+ ? { surfaceBg: "\x1b[48;2;228;234;242m" }
110
+ : { surfaceBg: "\x1b[48;2;38;38;38m" };
87
111
  }
88
112
 
89
113
  export default function (pi: ExtensionAPI) {
90
114
  pi.on("session_start", (_event, ctx) => {
91
- ctx.ui.setEditorComponent((tui, theme, kb) => new GreenBarEditor(tui, theme, kb));
115
+ const palette = pickPalette();
116
+ ctx.ui.setEditorComponent((tui, theme, kb) => new SurfaceEditor(tui, theme, kb, palette));
92
117
  });
93
118
  }
@@ -0,0 +1,134 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
3
+ import { uiState } from "./state.js";
4
+
5
+ const WHITE = "\x1b[38;2;242;244;247m";
6
+ const RESET = "\x1b[0m";
7
+ const INSET = " ";
8
+
9
+ type FooterState = {
10
+ modelLine: string;
11
+ contextDisplay: string;
12
+ userLine: string;
13
+ costLine: string;
14
+ };
15
+
16
+ function formatTokens(count: number): string {
17
+ if (!Number.isFinite(count) || count <= 0) return "0";
18
+ if (count < 1000) return String(Math.round(count));
19
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
20
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
21
+ if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
22
+ return `${Math.round(count / 1000000)}M`;
23
+ }
24
+
25
+ function joinLeftRight(left: string, right: string, width: number): string {
26
+ const leftWidth = visibleWidth(left);
27
+ const rightWidth = visibleWidth(right);
28
+ if (leftWidth + rightWidth + 1 <= width) {
29
+ const pad = " ".repeat(Math.max(1, width - leftWidth - rightWidth));
30
+ return `${left}${pad}${right}`;
31
+ }
32
+ const targetLeft = Math.max(1, width - rightWidth - 1);
33
+ const truncLeft = truncateToWidth(left, targetLeft, "...");
34
+ const pad = " ".repeat(Math.max(1, width - visibleWidth(truncLeft) - rightWidth));
35
+ return `${truncLeft}${pad}${right}`;
36
+ }
37
+
38
+ function computeFooterState(pi: ExtensionAPI, ctx: ExtensionContext): FooterState {
39
+ const modelId = ctx.model?.id ?? "no-model";
40
+ const thinking = ctx.model?.reasoning ? pi.getThinkingLevel() : "off";
41
+ const modelLine = `${modelId} • ${thinking}`;
42
+
43
+ const context = ctx.getContextUsage();
44
+ const contextWindow = context?.contextWindow ?? ctx.model?.contextWindow ?? 0;
45
+ const pct = context?.percent;
46
+ const contextPct = pct == null ? "?" : `${pct.toFixed(1)}%`;
47
+ const contextDisplay = `${contextPct}/${formatTokens(contextWindow)} (auto)`;
48
+
49
+ let totalCost = 0;
50
+ for (const entry of ctx.sessionManager.getEntries()) {
51
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
52
+ totalCost += entry.message.usage?.cost?.total ?? 0;
53
+ }
54
+ const costLine = `$${totalCost.toFixed(3)}`;
55
+ return {
56
+ modelLine,
57
+ contextDisplay,
58
+ userLine: "",
59
+ costLine,
60
+ };
61
+ }
62
+
63
+ export default function footerExtension(pi: ExtensionAPI) {
64
+ const state: FooterState = {
65
+ modelLine: "no-model • off",
66
+ contextDisplay: "?/0 (auto)",
67
+ userLine: "",
68
+ costLine: "$0.000",
69
+ };
70
+ let requestRender: (() => void) | undefined;
71
+ let lastCtx: ExtensionContext | undefined;
72
+
73
+ const refresh = (ctx: ExtensionContext) => {
74
+ const next = computeFooterState(pi, ctx);
75
+ state.modelLine = next.modelLine;
76
+ state.contextDisplay = next.contextDisplay;
77
+ state.costLine = next.costLine;
78
+ requestRender?.();
79
+ };
80
+
81
+ pi.on("session_start", (_event, ctx) => {
82
+ if (!ctx.hasUI) return;
83
+ lastCtx = ctx;
84
+ refresh(ctx);
85
+ ctx.ui.setFooter((tui, theme, footerData) => {
86
+ requestRender = () => tui.requestRender();
87
+ return {
88
+ render(width: number): string[] {
89
+ if (!uiState.ready) return [];
90
+ const usableWidth = Math.max(1, width - visibleWidth(INSET));
91
+ if (lastCtx) {
92
+ const live = computeFooterState(pi, lastCtx);
93
+ state.modelLine = live.modelLine;
94
+ state.contextDisplay = live.contextDisplay;
95
+ state.costLine = live.costLine;
96
+ }
97
+ state.userLine = footerData.getExtensionStatuses().get("seed") ?? "";
98
+ const topRight = theme.fg("dim", `${state.costLine} • ${state.contextDisplay}`);
99
+ const top = theme.fg("dim", truncateToWidth(state.modelLine, usableWidth));
100
+ const topAligned = joinLeftRight(top, topRight, usableWidth);
101
+ if (!state.userLine) return [`${INSET}${topAligned}`];
102
+ const bottomLeft = `${WHITE}${state.userLine}${RESET}`;
103
+ const bottom = joinLeftRight(
104
+ bottomLeft,
105
+ "",
106
+ usableWidth,
107
+ );
108
+ return [`${INSET}${topAligned}`, `${INSET}${bottom}`];
109
+ },
110
+ invalidate() {},
111
+ dispose() {
112
+ requestRender = undefined;
113
+ },
114
+ };
115
+ });
116
+ });
117
+
118
+ pi.on("turn_end", (_event, ctx) => {
119
+ if (!ctx.hasUI) return;
120
+ refresh(ctx);
121
+ });
122
+
123
+ pi.on("model_select", (_event, ctx) => {
124
+ if (!ctx.hasUI) return;
125
+ lastCtx = ctx;
126
+ refresh(ctx);
127
+ });
128
+
129
+ pi.on("session_switch", (_event, ctx) => {
130
+ if (!ctx.hasUI) return;
131
+ lastCtx = ctx;
132
+ refresh(ctx);
133
+ });
134
+ }
@@ -5,11 +5,11 @@
5
5
 
6
6
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
7
  import editorExtension from "./editor.js";
8
- import updateExtension from "./update.js";
8
+ import footerExtension from "./footer.js";
9
9
  import welcomeExtension from "./welcome.js";
10
10
 
11
11
  export default function (pi: ExtensionAPI) {
12
12
  editorExtension(pi);
13
- welcomeExtension(pi, { enableFrame: true });
14
- updateExtension(pi);
13
+ footerExtension(pi);
14
+ welcomeExtension(pi, { enableFrame: false });
15
15
  }
@@ -0,0 +1,4 @@
1
+ export const uiState = {
2
+ ready: false,
3
+ };
4
+
@@ -49,25 +49,5 @@ function getLatestVersion(): Promise<string | null> {
49
49
  }
50
50
 
51
51
  export default function (pi: ExtensionAPI) {
52
- // Check for updates on session start (background, non-blocking)
53
- pi.on("session_start", async (_event, ctx) => {
54
- if (!ctx.hasUI) return;
55
-
56
- const installed = getInstalledVersion();
57
- if (!installed) return;
58
-
59
- try {
60
- const latest = await getLatestVersion();
61
- if (!latest) return;
62
-
63
- if (compareSemver(latest, installed.seedclubVersion) > 0) {
64
- ctx.ui.setWidget("seedclub-update", [
65
- `⚡ seedclub v${latest} available (current: v${installed.seedclubVersion})`,
66
- ` Run: seedclub update`,
67
- ]);
68
- }
69
- } catch {
70
- // Silent — no network is fine
71
- }
72
- });
52
+ void pi;
73
53
  }
@@ -1,16 +1,23 @@
1
1
  /**
2
2
  * Welcome header with live weather + market data.
3
- * /commands and /extensions still available as commands.
3
+ * /commands and /extensions are available as commands.
4
4
  */
5
5
 
6
6
  import { execFileSync } from "node:child_process";
7
+ import { existsSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
7
10
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import { uiState } from "./state.js";
8
12
 
9
- const EMERALD = "\x1b[38;2;80;200;120m";
13
+ const EMERALD = "\x1b[38;2;39;163;91m";
10
14
  const DODGER = "\x1b[38;2;30;144;255m";
11
15
  const RED = "\x1b[38;2;255;0;0m";
12
16
  const DIM = "\x1b[38;2;85;85;85m";
13
17
  const WHITE = "\x1b[38;2;200;200;200m";
18
+ const GRAY_MID = "\x1b[38;2;160;160;160m";
19
+ const GRAY_DIM = "\x1b[38;2;115;115;115m";
20
+ const WHITE_BRIGHT = "\x1b[38;2;236;236;236m";
14
21
  const BOLD = "\x1b[1m";
15
22
  const RESET = "\x1b[0m";
16
23
 
@@ -130,29 +137,105 @@ async function getData() {
130
137
  return { weather, market };
131
138
  }
132
139
 
140
+ function hasSeedConnection(): boolean {
141
+ if (process.env.SEEDCLUB_TOKEN || process.env.SEED_NETWORK_TOKEN) return true;
142
+ const tokenPath = join(homedir(), ".config", "seedclub", "token");
143
+ return existsSync(tokenPath);
144
+ }
145
+
133
146
  function formatQuote(q: MarketQuote): string {
134
147
  const color = q.change.startsWith("+") ? EMERALD : q.change.startsWith("-") ? RED : DIM;
135
148
  return `${WHITE}${q.symbol}${RESET} ${DIM}${q.price}${RESET} ${color}${q.pct}${RESET}`;
136
149
  }
137
150
 
138
- const EMERALD_ANSI = "\x1b[38;2;80;200;120m";
151
+ const EMERALD_ANSI = "\x1b[38;2;39;163;91m";
152
+
153
+ function renderCoinLoaderLines(frame: number): string[] {
154
+ const rawShape = [
155
+ " XXXXXX XXXXXX",
156
+ " XXXXXX XXXXXX",
157
+ " XXXXXX XXXXXX",
158
+ " XXXXXX XXXXXX",
159
+ " XXXXXX XXXXXX",
160
+ " XXXXXX XXXXXX",
161
+ " XXXXXX",
162
+ " XXXXXX",
163
+ " XXXXXX",
164
+ " XXXXXX",
165
+ " XXXXXX",
166
+ " XXXXXX",
167
+ " XXXXXX",
168
+ " XXXXXX",
169
+ " XXXXXX",
170
+ " XXXXXX",
171
+ ];
172
+ const cols = rawShape.reduce((max, row) => Math.max(max, row.length), 0);
173
+ const shape = rawShape.map((row) => row.padEnd(cols, " "));
174
+ const rows = shape.length;
175
+ const texture = [" ", ".", ":", "-", "=", "+", "*", "#"] as const;
176
+
177
+ const lines: string[] = [];
178
+ for (let r = 0; r < rows; r++) {
179
+ let line = " ";
180
+ for (let c = 0; c < cols; c++) {
181
+ if (shape[r]![c] !== "X") {
182
+ line += " ";
183
+ continue;
184
+ }
185
+
186
+ // Diagonal wipe: bright band moves across cube faces.
187
+ const wipePos = (frame * 1.1) % (cols + 12);
188
+ const dist = Math.abs(c + r * 0.6 - wipePos);
189
+ const glow = Math.max(0, 1 - dist / 8);
190
+ const base = ((Math.sin(frame * 0.18 + c * 0.21 - r * 0.33) + 1) / 2) * 0.35;
191
+ const intensity = Math.max(0, Math.min(1, base + glow * 0.85));
192
+
193
+ const idx = Math.min(texture.length - 1, Math.max(1, Math.floor(intensity * texture.length)));
194
+ const glyph = texture[idx]!;
195
+
196
+ if (intensity > 0.72) {
197
+ line += `${WHITE_BRIGHT}${glyph}${RESET}`;
198
+ } else if (intensity > 0.38) {
199
+ line += `${GRAY_MID}${glyph}${RESET}`;
200
+ } else {
201
+ line += `${GRAY_DIM}${glyph}${RESET}`;
202
+ }
203
+ }
204
+ lines.push(line);
205
+ }
206
+
207
+ const loadingLabel = "loading...";
208
+ const loadingIndent = " ".repeat(2 + Math.max(0, Math.floor((cols - loadingLabel.length) / 2)));
209
+ return [...lines, "", `${loadingIndent}${DIM}${loadingLabel}${RESET}`];
210
+ }
139
211
 
140
212
  export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean }) {
141
213
  let headerLines: string[] = [
142
214
  "",
143
215
  ` ${EMERALD}${BOLD}The Human+ Venture Network${RESET}`,
144
216
  "",
145
- ` ${DIM}loading...${RESET}`,
217
+ ...renderCoinLoaderLines(0),
146
218
  "",
147
219
  ];
148
220
 
149
221
  pi.on("session_start", async (_event, ctx) => {
150
222
  if (!ctx.hasUI) return;
223
+ uiState.ready = false;
224
+ const hasAnyAuth = ctx.modelRegistry.getAvailable().length > 0;
225
+ const hasSelectedModel = !!ctx.model;
226
+ const connectedToSeed = hasSeedConnection();
227
+ const setupHints: string[] = [];
228
+ if (!hasAnyAuth) setupHints.push("/login");
229
+ if (!hasSelectedModel) setupHints.push("/model");
230
+ if (!connectedToSeed) setupHints.push("/connect");
151
231
 
152
232
  let tuiRef: any = null;
233
+ let loaderFrame = 0;
234
+ let loaderTimer: ReturnType<typeof setInterval> | undefined;
153
235
 
154
236
  ctx.ui.setHeader((tui, _theme) => {
155
237
  tuiRef = tui;
238
+ tui.setClearOnShrink(true);
156
239
 
157
240
  // Enable window frame around entire TUI (footer excluded)
158
241
  if (options?.enableFrame && tui.setFrame) {
@@ -172,10 +255,34 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
172
255
  };
173
256
  });
174
257
 
258
+ loaderTimer = setInterval(() => {
259
+ if (uiState.ready) return;
260
+ headerLines = [
261
+ "",
262
+ ` ${EMERALD}${BOLD}The Human+ Venture Network${RESET}`,
263
+ "",
264
+ ...renderCoinLoaderLines(loaderFrame++),
265
+ "",
266
+ ];
267
+ tuiRef?.requestRender();
268
+ }, 120);
269
+
175
270
  try {
176
271
  const { weather, market } = await getData();
177
272
  const weatherLine = ` ${weather.icon} ${WHITE}${weather.temp}${RESET} ${DIM}${weather.condition}${RESET} ${DIM}·${RESET} ${DIM}${weather.location}${RESET}`;
178
273
  const marketLine = ` ${market.map(formatQuote).join(` ${DIM}·${RESET} `)}`;
274
+ const setupLines = setupHints.flatMap((hint) => {
275
+ if (hint === "/login") {
276
+ return [` ${WHITE}/login${RESET} ${DIM}sign in with Anthropic, OpenAI, Gemini, or others${RESET}`];
277
+ }
278
+ if (hint === "/model") {
279
+ return [` ${WHITE}/model${RESET} ${DIM}choose your model${RESET}`];
280
+ }
281
+ if (hint === "/connect") {
282
+ return [` ${WHITE}/connect${RESET} ${DIM}to seeclub.com${RESET}`];
283
+ }
284
+ return [` ${WHITE}${hint}${RESET}`];
285
+ });
179
286
 
180
287
  headerLines = [
181
288
  "",
@@ -184,20 +291,45 @@ export default function (pi: ExtensionAPI, options?: { enableFrame?: boolean })
184
291
  weatherLine,
185
292
  marketLine,
186
293
  "",
187
- ` ${DIM}type ${WHITE}/${RESET}${DIM} to see commands${RESET}`,
294
+ ...setupLines,
188
295
  "",
189
296
  ];
190
297
  tuiRef?.requestRender();
191
298
  } catch {
299
+ const setupLines = setupHints.flatMap((hint) => {
300
+ if (hint === "/login") {
301
+ return [` ${WHITE}/login${RESET} ${DIM}sign in with Anthropic, OpenAI, Gemini, or others${RESET}`];
302
+ }
303
+ if (hint === "/model") {
304
+ return [` ${WHITE}/model${RESET} ${DIM}choose your model${RESET}`];
305
+ }
306
+ if (hint === "/connect") {
307
+ return [` ${WHITE}/connect${RESET} ${DIM}to seeclub.com${RESET}`];
308
+ }
309
+ return [` ${WHITE}${hint}${RESET}`];
310
+ });
192
311
  headerLines = [
193
312
  "",
194
313
  ` ${EMERALD}${BOLD}The Human+ Venture Network${RESET}`,
195
314
  "",
196
- ` ${DIM}type ${WHITE}/${RESET}${DIM} to see commands${RESET}`,
315
+ ...setupLines,
197
316
  "",
198
317
  ];
199
318
  tuiRef?.requestRender();
319
+ } finally {
320
+ if (loaderTimer) clearInterval(loaderTimer);
200
321
  }
322
+ uiState.ready = true;
323
+ ctx.ui.setEditorText("");
324
+ });
325
+
326
+ pi.on("input", async (event, ctx) => {
327
+ if (event.source !== "interactive") return;
328
+ if (!uiState.ready) return { action: "handled" };
329
+ if (event.text.trim().startsWith("/")) return;
330
+ if (ctx.model) return;
331
+ ctx.ui.notify("Set up first: /login, then /model, then /connect.", "info");
332
+ return { action: "handled" };
201
333
  });
202
334
 
203
335
  pi.registerCommand("commands", {
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
- "name": "seedclub",
3
+ "name": "dark",
4
4
  "vars": {
5
- "brand": "#00C853",
6
- "brandDim": "#00963F",
7
- "brandLight": "#69F0AE",
8
- "warmWhite": "#E8E6E3",
9
- "coolGray": "#8A8F98",
10
- "dimGray": "#5C6370",
11
- "darkGray": "#3E4451",
12
- "deepBg": "#1A1D23",
13
- "cardBg": "#21252B",
14
- "surfaceBg": "#282C34",
15
- "red": "#E06C75",
16
- "yellow": "#E5C07B",
17
- "blue": "#61AFEF",
5
+ "brand": "#3EBE4D",
6
+ "brandDim": "#3EBE4D",
7
+ "brandLight": "#3EBE4D",
8
+ "warmWhite": "#F2F4F7",
9
+ "coolGray": "#B8B8B8",
10
+ "dimGray": "#909090",
11
+ "darkGray": "#5F5F5F",
12
+ "deepBg": "#111111",
13
+ "cardBg": "#1D1D1D",
14
+ "surfaceBg": "#262626",
15
+ "red": "#E1251B",
16
+ "yellow": "#F0B323",
17
+ "blue": "#1E90FF",
18
18
  "purple": "#C678DD",
19
19
  "cyan": "#56B6C2",
20
- "green": "#98C379"
20
+ "green": "#3EBE4D"
21
21
  },
22
22
  "colors": {
23
23
  "accent": "brand",
@@ -32,40 +32,40 @@
32
32
  "text": "",
33
33
  "thinkingText": "coolGray",
34
34
 
35
- "selectedBg": "#2C3340",
36
- "userMessageBg": "surfaceBg",
35
+ "selectedBg": "surfaceBg",
36
+ "userMessageBg": "#141414",
37
37
  "userMessageText": "",
38
- "customMessageBg": "cardBg",
38
+ "customMessageBg": "surfaceBg",
39
39
  "customMessageText": "",
40
40
  "customMessageLabel": "brand",
41
41
  "toolPendingBg": "deepBg",
42
- "toolSuccessBg": "#1A2E1A",
43
- "toolErrorBg": "#2E1A1A",
44
- "toolTitle": "brandLight",
42
+ "toolSuccessBg": "#0F0F0F",
43
+ "toolErrorBg": "#180D0D",
44
+ "toolTitle": "warmWhite",
45
45
  "toolOutput": "coolGray",
46
46
 
47
47
  "mdHeading": "yellow",
48
48
  "mdLink": "blue",
49
49
  "mdLinkUrl": "dimGray",
50
- "mdCode": "cyan",
51
- "mdCodeBlock": "green",
50
+ "mdCode": "blue",
51
+ "mdCodeBlock": "coolGray",
52
52
  "mdCodeBlockBorder": "coolGray",
53
53
  "mdQuote": "coolGray",
54
54
  "mdQuoteBorder": "coolGray",
55
55
  "mdHr": "darkGray",
56
- "mdListBullet": "brand",
56
+ "mdListBullet": "coolGray",
57
57
 
58
58
  "toolDiffAdded": "green",
59
59
  "toolDiffRemoved": "red",
60
60
  "toolDiffContext": "coolGray",
61
61
 
62
- "syntaxComment": "#7F848E",
63
- "syntaxKeyword": "purple",
62
+ "syntaxComment": "#888888",
63
+ "syntaxKeyword": "warmWhite",
64
64
  "syntaxFunction": "blue",
65
- "syntaxVariable": "red",
66
- "syntaxString": "green",
67
- "syntaxNumber": "#D19A66",
68
- "syntaxType": "cyan",
65
+ "syntaxVariable": "coolGray",
66
+ "syntaxString": "coolGray",
67
+ "syntaxNumber": "coolGray",
68
+ "syntaxType": "blue",
69
69
  "syntaxOperator": "warmWhite",
70
70
  "syntaxPunctuation": "coolGray",
71
71
 
@@ -74,13 +74,13 @@
74
74
  "thinkingLow": "brandDim",
75
75
  "thinkingMedium": "brand",
76
76
  "thinkingHigh": "brandLight",
77
- "thinkingXhigh": "#B9F6CA",
77
+ "thinkingXhigh": "#8CBFAD",
78
78
 
79
79
  "bashMode": "yellow"
80
80
  },
81
81
  "export": {
82
- "pageBg": "#1A1D23",
83
- "cardBg": "#21252B",
84
- "infoBg": "#2A2D25"
82
+ "pageBg": "#111111",
83
+ "cardBg": "#1D1D1D",
84
+ "infoBg": "#262626"
85
85
  }
86
86
  }
@@ -0,0 +1,86 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "light",
4
+ "vars": {
5
+ "brand": "#3EBE4D",
6
+ "brandDim": "#3EBE4D",
7
+ "brandLight": "#3EBE4D",
8
+ "warmWhite": "#1F2730",
9
+ "coolGray": "#666666",
10
+ "dimGray": "#7A7A7A",
11
+ "darkGray": "#CCCCCC",
12
+ "deepBg": "#F4F4F4",
13
+ "cardBg": "#ECECEC",
14
+ "surfaceBg": "#E4E4E4",
15
+ "red": "#D91F16",
16
+ "yellow": "#B57900",
17
+ "blue": "#1E90FF",
18
+ "purple": "#7D5AC8",
19
+ "cyan": "#227E90",
20
+ "green": "#3EBE4D"
21
+ },
22
+ "colors": {
23
+ "accent": "brand",
24
+ "border": "brandDim",
25
+ "borderAccent": "brandLight",
26
+ "borderMuted": "darkGray",
27
+ "success": "green",
28
+ "error": "red",
29
+ "warning": "yellow",
30
+ "muted": "coolGray",
31
+ "dim": "dimGray",
32
+ "text": "warmWhite",
33
+ "thinkingText": "coolGray",
34
+
35
+ "selectedBg": "surfaceBg",
36
+ "userMessageBg": "#CECECE",
37
+ "userMessageText": "warmWhite",
38
+ "customMessageBg": "surfaceBg",
39
+ "customMessageText": "warmWhite",
40
+ "customMessageLabel": "brand",
41
+ "toolPendingBg": "cardBg",
42
+ "toolSuccessBg": "#C6C6C6",
43
+ "toolErrorBg": "#E6D6D6",
44
+ "toolTitle": "warmWhite",
45
+ "toolOutput": "coolGray",
46
+
47
+ "mdHeading": "yellow",
48
+ "mdLink": "blue",
49
+ "mdLinkUrl": "dimGray",
50
+ "mdCode": "blue",
51
+ "mdCodeBlock": "coolGray",
52
+ "mdCodeBlockBorder": "coolGray",
53
+ "mdQuote": "coolGray",
54
+ "mdQuoteBorder": "coolGray",
55
+ "mdHr": "darkGray",
56
+ "mdListBullet": "coolGray",
57
+
58
+ "toolDiffAdded": "green",
59
+ "toolDiffRemoved": "red",
60
+ "toolDiffContext": "coolGray",
61
+
62
+ "syntaxComment": "#7A7A7A",
63
+ "syntaxKeyword": "warmWhite",
64
+ "syntaxFunction": "blue",
65
+ "syntaxVariable": "coolGray",
66
+ "syntaxString": "coolGray",
67
+ "syntaxNumber": "coolGray",
68
+ "syntaxType": "blue",
69
+ "syntaxOperator": "warmWhite",
70
+ "syntaxPunctuation": "coolGray",
71
+
72
+ "thinkingOff": "darkGray",
73
+ "thinkingMinimal": "dimGray",
74
+ "thinkingLow": "brandDim",
75
+ "thinkingMedium": "brand",
76
+ "thinkingHigh": "brandLight",
77
+ "thinkingXhigh": "#2F5C4A",
78
+
79
+ "bashMode": "yellow"
80
+ },
81
+ "export": {
82
+ "pageBg": "#F4F4F4",
83
+ "cardBg": "#ECECEC",
84
+ "infoBg": "#E4E4E4"
85
+ }
86
+ }
package/bin/cli.js CHANGED
@@ -3,12 +3,15 @@
3
3
 
4
4
  const { execFileSync, spawn } = require("child_process");
5
5
  const { readFileSync, existsSync, writeFileSync, unlinkSync } = require("fs");
6
- const { join, dirname } = require("path");
6
+ const { join, dirname, basename } = require("path");
7
7
  const { homedir } = require("os");
8
8
  const readline = require("readline");
9
9
 
10
10
  const SC_DIR = join(homedir(), ".seedclub", "agent");
11
11
  const VERSION_FILE = join(SC_DIR, ".seedclub-version");
12
+ const PI_MAIN_LAUNCHER = join(__dirname, "pi-main-launcher.js");
13
+ process.title = "seedclub";
14
+ const SEEDCLUB_ENV_EXCLUDE = new Set(["SEEDCLUB_NPM_TOKEN", "SEEDCLUB_PI_MAIN"]);
12
15
 
13
16
  function printPrivateRegistryHint() {
14
17
  console.error("seedclub: install/update failed.");
@@ -114,6 +117,51 @@ function withOptionalEphemeralNpmrc(run) {
114
117
  }
115
118
  }
116
119
 
120
+ function findPackageRoot(fromFile, expectedName) {
121
+ let dir = dirname(fromFile);
122
+ while (true) {
123
+ const pkgPath = join(dir, "package.json");
124
+ if (existsSync(pkgPath)) {
125
+ try {
126
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
127
+ if (!expectedName || pkg.name === expectedName) {
128
+ return { dir, pkgPath, pkg };
129
+ }
130
+ } catch {}
131
+ }
132
+ const parent = dirname(dir);
133
+ if (parent === dir) break;
134
+ dir = parent;
135
+ }
136
+ throw new Error(`Could not find package root for ${expectedName || "module"}`);
137
+ }
138
+
139
+ function resolvePackageJsonPath(packageName, extraBaseDirs = []) {
140
+ const searchDirs = [];
141
+ for (const baseDir of extraBaseDirs) {
142
+ searchDirs.push(join(baseDir, "node_modules"));
143
+ }
144
+ const resolverDirs = require.resolve.paths(packageName) || [];
145
+ searchDirs.push(...resolverDirs);
146
+ for (const dir of searchDirs) {
147
+ const pkgPath = join(dir, packageName, "package.json");
148
+ if (existsSync(pkgPath)) return pkgPath;
149
+ }
150
+ throw new Error(`Package not found: ${packageName}`);
151
+ }
152
+
153
+ function mapSeedclubEnvToPiEnv(env) {
154
+ for (const [key, value] of Object.entries(env)) {
155
+ if (!key.startsWith("SEEDCLUB_") || SEEDCLUB_ENV_EXCLUDE.has(key)) continue;
156
+ const suffix = key.slice("SEEDCLUB_".length);
157
+ if (!suffix) continue;
158
+ const piKey = `PI_${suffix}`;
159
+ if (env[piKey] === undefined && value !== undefined) {
160
+ env[piKey] = value;
161
+ }
162
+ }
163
+ }
164
+
117
165
  // ── Subcommands ──────────────────────────────────────────────────────────
118
166
 
119
167
  const cmd = process.argv[2];
@@ -157,23 +205,30 @@ if (cmd !== "setup-auth") {
157
205
  // ── Resolve pi binary ───────────────────────────────────────────────────
158
206
 
159
207
  let piBin;
208
+ let piEntry;
209
+ let piMainPath;
160
210
  try {
161
- const piPkgPath = join(
211
+ const piPkgPath = resolvePackageJsonPath("@mariozechner/pi-coding-agent", [
162
212
  __dirname,
163
- "..",
164
- "node_modules",
165
- "@mariozechner",
166
- "pi-coding-agent",
167
- "package.json",
213
+ process.cwd(),
214
+ ]);
215
+ const { dir: piPkgDir, pkg: piPkg } = findPackageRoot(
216
+ piPkgPath,
217
+ "@mariozechner/pi-coding-agent",
168
218
  );
169
- if (!existsSync(piPkgPath)) {
170
- throw new Error("pi package.json not found in package-local node_modules");
171
- }
172
- const piPkg = JSON.parse(readFileSync(piPkgPath, "utf-8"));
173
219
  const binEntry = piPkg.bin && (piPkg.bin.pi || Object.values(piPkg.bin)[0]);
174
220
  if (!binEntry) throw new Error("No bin entry in pi package.json");
175
- piBin = join(dirname(piPkgPath), binEntry);
221
+ piBin = join(piPkgDir, binEntry);
176
222
  if (!existsSync(piBin)) throw new Error(`pi bin not found at ${piBin}`);
223
+ piEntry = piBin;
224
+ // pi's CLI shim hardcodes process.title = "pi"; call main.js directly to keep Seedclub naming.
225
+ if (basename(piBin) === "cli.js") {
226
+ const mainCandidate = join(dirname(piBin), "main.js");
227
+ if (existsSync(mainCandidate)) {
228
+ piMainPath = mainCandidate;
229
+ piEntry = PI_MAIN_LAUNCHER;
230
+ }
231
+ }
177
232
  } catch (err) {
178
233
  console.error("seedclub: could not locate pi runtime. Reinstall with:");
179
234
  console.error(" npm install -g @clubnet/seedclub");
@@ -182,16 +237,21 @@ if (cmd !== "setup-auth") {
182
237
 
183
238
  // ── Environment ─────────────────────────────────────────────────────────
184
239
 
240
+ mapSeedclubEnvToPiEnv(process.env);
185
241
  process.env.PI_CODING_AGENT_DIR = SC_DIR;
186
242
  process.env.PI_SKIP_VERSION_CHECK = "1";
243
+ if (!process.env.PI_CLEAR_ON_SHRINK) process.env.PI_CLEAR_ON_SHRINK = "1";
244
+ process.env.PI_HARDWARE_CURSOR = "0";
245
+ if (piMainPath) process.env.SEEDCLUB_PI_MAIN = piMainPath;
187
246
  process.env.NODE_OPTIONS = `--no-warnings ${process.env.NODE_OPTIONS || ""}`.trim();
188
247
 
189
248
  // ── Spawn pi ────────────────────────────────────────────────────────────
190
249
 
191
250
  const args = process.argv.slice(2);
192
- const child = spawn(process.execPath, [piBin, ...args], {
251
+ const child = spawn(process.execPath, [piEntry, ...args], {
193
252
  stdio: "inherit",
194
253
  env: process.env,
254
+ argv0: "seedclub",
195
255
  });
196
256
 
197
257
  child.on("exit", (code, signal) => {
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { pathToFileURL } = require("url");
5
+ const { dirname, join } = require("path");
6
+ const { existsSync } = require("fs");
7
+
8
+ process.title = "seedclub";
9
+
10
+ async function patchInteractiveWarnings(piMainPath) {
11
+ try {
12
+ const interactiveModePath = join(dirname(piMainPath), "modes", "interactive", "interactive-mode.js");
13
+ if (!existsSync(interactiveModePath)) return;
14
+ const interactiveModeUrl = pathToFileURL(interactiveModePath).href;
15
+ const mod = await import(interactiveModeUrl);
16
+ const InteractiveMode = mod?.InteractiveMode;
17
+ if (!InteractiveMode?.prototype?.showWarning) return;
18
+
19
+ const originalShowWarning = InteractiveMode.prototype.showWarning;
20
+ InteractiveMode.prototype.showWarning = function (message, ...args) {
21
+ if (
22
+ typeof message === "string" &&
23
+ message.includes("No models available. Use /login or set an API key environment variable")
24
+ ) {
25
+ return;
26
+ }
27
+ return originalShowWarning.call(this, message, ...args);
28
+ };
29
+ } catch {
30
+ // Best-effort patch; ignore if pi internals change.
31
+ }
32
+ }
33
+
34
+ async function run() {
35
+ const piMainPath = process.env.SEEDCLUB_PI_MAIN;
36
+ if (!piMainPath) {
37
+ throw new Error("Missing SEEDCLUB_PI_MAIN");
38
+ }
39
+ await patchInteractiveWarnings(piMainPath);
40
+ const mainUrl = pathToFileURL(piMainPath).href;
41
+ const mod = await import(mainUrl);
42
+ if (!mod || typeof mod.main !== "function") {
43
+ throw new Error(`Invalid pi main entry: ${piMainPath}`);
44
+ }
45
+ await mod.main(process.argv.slice(2));
46
+ }
47
+
48
+ run().catch((err) => {
49
+ console.error(err instanceof Error ? err.stack || err.message : String(err));
50
+ process.exit(1);
51
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubnet/seedclub",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "The Human+ Venture Network — AI agent for deal sourcing, research, and signal tracking",
5
5
  "license": "MIT",
6
6
  "repository": {
package/postinstall.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, rmSync } = require("fs");
3
+ const { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, rmSync, readdirSync } = require("fs");
4
4
  const { join } = require("path");
5
5
  const { homedir } = require("os");
6
6
  const { execFileSync } = require("child_process");
@@ -33,13 +33,15 @@ for (const dir of ["extensions", "themes", "prompts"]) {
33
33
  mkdirSync(join(SC_DIR, dir), { recursive: true });
34
34
  }
35
35
 
36
- // ── 2. Copy theme ───────────────────────────────────────────────────────
36
+ // ── 2. Copy themes ──────────────────────────────────────────────────────
37
37
 
38
- const themeSrc = join(PKG_ROOT, "assets", "theme", "seedclub.json");
39
- const themeDest = join(SC_DIR, "themes", "seedclub.json");
40
- if (existsSync(themeSrc)) {
41
- cpSync(themeSrc, themeDest);
42
- ok("Installed theme");
38
+ const themeDir = join(PKG_ROOT, "assets", "theme");
39
+ if (existsSync(themeDir)) {
40
+ for (const file of readdirSync(themeDir)) {
41
+ if (!file.endsWith(".json")) continue;
42
+ cpSync(join(themeDir, file), join(SC_DIR, "themes", file));
43
+ }
44
+ ok("Installed themes");
43
45
  }
44
46
 
45
47
  // ── 3. Copy extensions ──────────────────────────────────────────────────
@@ -89,13 +91,22 @@ if (!existsSync(settingsFile)) {
89
91
  writeFileSync(
90
92
  settingsFile,
91
93
  JSON.stringify(
92
- { theme: "seedclub", quietStartup: true, enableSkillCommands: true },
94
+ { theme: "dark", quietStartup: true, enableSkillCommands: true },
93
95
  null,
94
96
  2,
95
97
  ) + "\n",
96
98
  );
97
99
  ok("Created default settings");
98
100
  } else {
101
+ // Migrate legacy theme name to the current dark/light naming.
102
+ try {
103
+ const raw = JSON.parse(readFileSync(settingsFile, "utf-8"));
104
+ if (raw && typeof raw === "object" && raw.theme === "seedclub") {
105
+ raw.theme = "dark";
106
+ writeFileSync(settingsFile, JSON.stringify(raw, null, 2) + "\n");
107
+ ok("Migrated theme: seedclub -> dark");
108
+ }
109
+ } catch {}
99
110
  ok("Settings already exist (preserved)");
100
111
  }
101
112