@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 +5 -3
- package/assets/extensions/seedclub/auth.ts +12 -2
- package/assets/extensions/seedclub/commands/seedclub.ts +7 -0
- package/assets/extensions/seedclub/index.ts +9 -4
- package/assets/extensions/seedclub-ui/editor.ts +87 -62
- package/assets/extensions/seedclub-ui/footer.ts +134 -0
- package/assets/extensions/seedclub-ui/index.ts +3 -3
- package/assets/extensions/seedclub-ui/state.ts +4 -0
- package/assets/extensions/seedclub-ui/update.ts +1 -21
- package/assets/extensions/seedclub-ui/welcome.ts +138 -6
- package/assets/theme/{seedclub.json → dark.json} +34 -34
- package/assets/theme/light.json +86 -0
- package/bin/cli.js +73 -13
- package/bin/pi-main-launcher.js +51 -0
- package/package.json +1 -1
- package/postinstall.js +19 -8
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
|
-
│
|
|
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
|
-
|
|
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
|
-
│
|
|
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(
|
|
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(
|
|
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",
|
|
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",
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
this.
|
|
24
|
+
this.palette = palette;
|
|
25
|
+
this.setPaddingX(2);
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
render(width: number): string[] {
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
13
|
+
footerExtension(pi);
|
|
14
|
+
welcomeExtension(pi, { enableFrame: false });
|
|
15
15
|
}
|
|
@@ -49,25 +49,5 @@ function getLatestVersion(): Promise<string | null> {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export default function (pi: ExtensionAPI) {
|
|
52
|
-
|
|
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
|
|
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;
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
3
|
+
"name": "dark",
|
|
4
4
|
"vars": {
|
|
5
|
-
"brand": "#
|
|
6
|
-
"brandDim": "#
|
|
7
|
-
"brandLight": "#
|
|
8
|
-
"warmWhite": "#
|
|
9
|
-
"coolGray": "#
|
|
10
|
-
"dimGray": "#
|
|
11
|
-
"darkGray": "#
|
|
12
|
-
"deepBg": "#
|
|
13
|
-
"cardBg": "#
|
|
14
|
-
"surfaceBg": "#
|
|
15
|
-
"red": "#
|
|
16
|
-
"yellow": "#
|
|
17
|
-
"blue": "#
|
|
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": "#
|
|
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": "
|
|
36
|
-
"userMessageBg": "
|
|
35
|
+
"selectedBg": "surfaceBg",
|
|
36
|
+
"userMessageBg": "#141414",
|
|
37
37
|
"userMessageText": "",
|
|
38
|
-
"customMessageBg": "
|
|
38
|
+
"customMessageBg": "surfaceBg",
|
|
39
39
|
"customMessageText": "",
|
|
40
40
|
"customMessageLabel": "brand",
|
|
41
41
|
"toolPendingBg": "deepBg",
|
|
42
|
-
"toolSuccessBg": "#
|
|
43
|
-
"toolErrorBg": "#
|
|
44
|
-
"toolTitle": "
|
|
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": "
|
|
51
|
-
"mdCodeBlock": "
|
|
50
|
+
"mdCode": "blue",
|
|
51
|
+
"mdCodeBlock": "coolGray",
|
|
52
52
|
"mdCodeBlockBorder": "coolGray",
|
|
53
53
|
"mdQuote": "coolGray",
|
|
54
54
|
"mdQuoteBorder": "coolGray",
|
|
55
55
|
"mdHr": "darkGray",
|
|
56
|
-
"mdListBullet": "
|
|
56
|
+
"mdListBullet": "coolGray",
|
|
57
57
|
|
|
58
58
|
"toolDiffAdded": "green",
|
|
59
59
|
"toolDiffRemoved": "red",
|
|
60
60
|
"toolDiffContext": "coolGray",
|
|
61
61
|
|
|
62
|
-
"syntaxComment": "#
|
|
63
|
-
"syntaxKeyword": "
|
|
62
|
+
"syntaxComment": "#888888",
|
|
63
|
+
"syntaxKeyword": "warmWhite",
|
|
64
64
|
"syntaxFunction": "blue",
|
|
65
|
-
"syntaxVariable": "
|
|
66
|
-
"syntaxString": "
|
|
67
|
-
"syntaxNumber": "
|
|
68
|
-
"syntaxType": "
|
|
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": "#
|
|
77
|
+
"thinkingXhigh": "#8CBFAD",
|
|
78
78
|
|
|
79
79
|
"bashMode": "yellow"
|
|
80
80
|
},
|
|
81
81
|
"export": {
|
|
82
|
-
"pageBg": "#
|
|
83
|
-
"cardBg": "#
|
|
84
|
-
"infoBg": "#
|
|
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 =
|
|
211
|
+
const piPkgPath = resolvePackageJsonPath("@mariozechner/pi-coding-agent", [
|
|
162
212
|
__dirname,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"
|
|
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(
|
|
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, [
|
|
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
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
|
|
36
|
+
// ── 2. Copy themes ──────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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: "
|
|
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
|
|