@tokentop/ttop 0.4.0 → 0.6.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/README.md +7 -6
- package/package.json +12 -5
- package/src/agents/aggregator.test.ts +306 -0
- package/src/agents/aggregator.ts +44 -2
- package/src/agents/types.ts +3 -0
- package/src/cli.ts +16 -2
- package/src/demo/simulator.ts +2 -2
- package/src/plugins/agents/index.ts +3 -0
- package/src/plugins/auth-sources.ts +2 -3
- package/src/plugins/lifecycle.ts +2 -2
- package/src/plugins/loader.test.ts +1 -1
- package/src/plugins/loader.ts +15 -7
- package/src/plugins/notification-bus.ts +5 -2
- package/src/plugins/notifications/terminal-bell.ts +1 -1
- package/src/plugins/notifications/visual-flash.ts +1 -1
- package/src/plugins/plugin-context-factory.ts +4 -19
- package/src/plugins/plugin-host.test.ts +197 -0
- package/src/plugins/plugin-host.ts +45 -1
- package/src/plugins/providers/anthropic.test.ts +202 -0
- package/src/plugins/providers/anthropic.ts +21 -2
- package/src/plugins/providers/antigravity.ts +17 -1
- package/src/plugins/providers/chutes.ts +205 -0
- package/src/plugins/providers/codex.ts +17 -1
- package/src/plugins/providers/gemini.ts +17 -1
- package/src/plugins/providers/github-copilot.ts +17 -1
- package/src/plugins/providers/index.ts +1 -0
- package/src/plugins/providers/minimax.ts +17 -1
- package/src/plugins/providers/openai-api.ts +53 -1
- package/src/plugins/providers/opencode-zen.ts +1 -1
- package/src/plugins/providers/perplexity.ts +11 -2
- package/src/plugins/providers/zai.ts +17 -1
- package/src/plugins/registry.ts +9 -9
- package/src/plugins/sandbox-guard.test.ts +1 -1
- package/src/plugins/sandbox-guard.ts +1 -1
- package/src/plugins/sandbox.ts +1 -1
- package/src/plugins/themes/catppuccin-latte.ts +1 -1
- package/src/plugins/themes/catppuccin-mocha.ts +1 -1
- package/src/plugins/themes/claude-code.ts +1 -1
- package/src/plugins/themes/dracula.ts +1 -1
- package/src/plugins/themes/github-light.ts +1 -1
- package/src/plugins/themes/gruvbox-dark.ts +1 -1
- package/src/plugins/themes/gruvbox-light.ts +1 -1
- package/src/plugins/themes/kanagawa.ts +1 -1
- package/src/plugins/themes/nord.ts +1 -1
- package/src/plugins/themes/one-dark.ts +1 -1
- package/src/plugins/themes/opencode.ts +1 -1
- package/src/plugins/themes/rose-pine-dawn.ts +1 -1
- package/src/plugins/themes/rose-pine.ts +1 -1
- package/src/plugins/themes/solarized-light.ts +1 -1
- package/src/plugins/themes/tokyo-night.ts +1 -1
- package/src/plugins/update-checker.ts +2 -2
- package/src/pricing/estimator.test.ts +1 -1
- package/src/pricing/estimator.ts +1 -1
- package/src/pricing/fallback.ts +5 -1
- package/src/pricing/index.ts +1 -1
- package/src/pricing/models-dev.ts +1 -1
- package/src/storage/index.ts +6 -0
- package/src/storage/migrations/index.ts +17 -0
- package/src/storage/repos/pluginStorage.ts +35 -0
- package/src/tui/App.tsx +17 -9
- package/src/tui/components/HelpOverlay.tsx +1 -1
- package/src/tui/components/LimitGauge.tsx +12 -4
- package/src/tui/components/ProviderCard.tsx +39 -23
- package/src/tui/components/ProviderDetailPanel.tsx +1 -1
- package/src/tui/components/ProviderLimitsPanel.tsx +5 -0
- package/src/tui/components/SessionDetailsDrawer.tsx +10 -0
- package/src/tui/components/SessionsTable.tsx +62 -37
- package/src/tui/components/SettingsModal.tsx +1 -1
- package/src/tui/components/Skeleton.tsx +9 -6
- package/src/tui/components/SortOverlay.tsx +167 -0
- package/src/tui/components/Spinner.tsx +16 -11
- package/src/tui/components/ThemePicker.tsx +1 -1
- package/src/tui/contexts/AgentSessionContext.tsx +70 -17
- package/src/tui/contexts/PluginContext.tsx +72 -14
- package/src/tui/contexts/RealTimeActivityContext.tsx +33 -20
- package/src/tui/contexts/ThemeContext.tsx +7 -4
- package/src/tui/createApp.tsx +2 -0
- package/src/tui/hooks/index.ts +1 -0
- package/src/tui/hooks/useAnimatedValue.ts +29 -26
- package/src/tui/hooks/useAnimationTick.ts +93 -0
- package/src/tui/hooks/useDashboardKeyboard.ts +11 -4
- package/src/tui/hooks/useEntranceAnimation.ts +16 -10
- package/src/tui/hooks/usePulse.ts +13 -7
- package/src/tui/hooks/useSessionEnrichedProviders.ts +1 -1
- package/src/tui/hooks/useValueFlash.ts +25 -33
- package/src/tui/index.tsx +4 -0
- package/src/tui/types/projectSort.ts +35 -0
- package/src/tui/types/sort.ts +61 -0
- package/src/tui/views/ProjectsView.tsx +104 -38
- package/src/tui/views/RealTimeDashboard.tsx +91 -8
- package/src/tui/views/SettingsView.tsx +1 -1
- package/src/utils/__tests__/error-category.test.ts +147 -0
- package/src/utils/currency.ts +4 -2
- package/src/utils/error-category.ts +91 -0
- package/src/version.ts +1 -1
- package/src/plugins/types/agent.ts +0 -108
- package/src/plugins/types/base.ts +0 -278
- package/src/plugins/types/index.ts +0 -19
- package/src/plugins/types/notification.ts +0 -51
- package/src/plugins/types/provider.ts +0 -237
- package/src/plugins/types/theme.ts +0 -78
- package/src/storage/database.ts +0 -623
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { classifyProviderError } from "../error-category.ts";
|
|
3
|
+
|
|
4
|
+
describe("classifyProviderError", () => {
|
|
5
|
+
describe("auth errors", () => {
|
|
6
|
+
test("classifies expired token errors", () => {
|
|
7
|
+
expect(classifyProviderError("Token expired. Run any command in OpenCode to refresh.")).toBe(
|
|
8
|
+
"auth",
|
|
9
|
+
);
|
|
10
|
+
expect(
|
|
11
|
+
classifyProviderError(
|
|
12
|
+
"Token expired or invalid. Re-authenticate in OpenCode or Claude Code.",
|
|
13
|
+
),
|
|
14
|
+
).toBe("auth");
|
|
15
|
+
expect(classifyProviderError("OAuth token expired. Re-authenticate in OpenCode.")).toBe(
|
|
16
|
+
"auth",
|
|
17
|
+
);
|
|
18
|
+
expect(
|
|
19
|
+
classifyProviderError("OAuth token expired or invalid. Re-authenticate in OpenCode."),
|
|
20
|
+
).toBe("auth");
|
|
21
|
+
expect(
|
|
22
|
+
classifyProviderError("OAuth token expired or invalid. Re-authenticate via Gemini CLI."),
|
|
23
|
+
).toBe("auth");
|
|
24
|
+
expect(classifyProviderError("Claude Code token expired")).toBe("auth");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("classifies scope errors", () => {
|
|
28
|
+
expect(
|
|
29
|
+
classifyProviderError(
|
|
30
|
+
"Token lacks required scope. Re-authenticate in OpenCode or Claude Code.",
|
|
31
|
+
),
|
|
32
|
+
).toBe("auth");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("classifies missing credential errors", () => {
|
|
36
|
+
expect(
|
|
37
|
+
classifyProviderError(
|
|
38
|
+
"OAuth token or API key required. Authenticate via OpenCode or Claude Code.",
|
|
39
|
+
),
|
|
40
|
+
).toBe("auth");
|
|
41
|
+
expect(
|
|
42
|
+
classifyProviderError(
|
|
43
|
+
"OAuth token required. Sign in via OpenCode with ChatGPT Pro account.",
|
|
44
|
+
),
|
|
45
|
+
).toBe("auth");
|
|
46
|
+
expect(
|
|
47
|
+
classifyProviderError("OAuth token required. Sign in via OpenCode with Google account."),
|
|
48
|
+
).toBe("auth");
|
|
49
|
+
expect(
|
|
50
|
+
classifyProviderError("OAuth token required. Login via Gemini CLI with Google account."),
|
|
51
|
+
).toBe("auth");
|
|
52
|
+
expect(
|
|
53
|
+
classifyProviderError("API key required. Set OPENAI_API_KEY environment variable."),
|
|
54
|
+
).toBe("auth");
|
|
55
|
+
expect(
|
|
56
|
+
classifyProviderError("API key required. Set PERPLEXITY_API_KEY environment variable."),
|
|
57
|
+
).toBe("auth");
|
|
58
|
+
expect(classifyProviderError("API key required. Configure MiniMax in OpenCode.")).toBe(
|
|
59
|
+
"auth",
|
|
60
|
+
);
|
|
61
|
+
expect(classifyProviderError("API key required. Configure Chutes in OpenCode.")).toBe("auth");
|
|
62
|
+
expect(classifyProviderError("API key required. Configure Z.ai in OpenCode.")).toBe("auth");
|
|
63
|
+
expect(
|
|
64
|
+
classifyProviderError("API key required. Run /connect in OpenCode to set up Zen."),
|
|
65
|
+
).toBe("auth");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("classifies authorization failure errors", () => {
|
|
69
|
+
expect(classifyProviderError("Authorization failed. Check your MiniMax API key.")).toBe(
|
|
70
|
+
"auth",
|
|
71
|
+
);
|
|
72
|
+
expect(classifyProviderError("Authorization failed. Check your Chutes API key.")).toBe(
|
|
73
|
+
"auth",
|
|
74
|
+
);
|
|
75
|
+
expect(classifyProviderError("Authorization failed. Check your Z.ai API key.")).toBe("auth");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("classifies invalid key errors", () => {
|
|
79
|
+
expect(classifyProviderError("Invalid API key")).toBe("auth");
|
|
80
|
+
expect(classifyProviderError("Unauthorized – check API key")).toBe("auth");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("classifies HTTP status code auth errors", () => {
|
|
84
|
+
expect(classifyProviderError("API error: 401 Unauthorized")).toBe("auth");
|
|
85
|
+
expect(classifyProviderError("API error: 403 Forbidden")).toBe("auth");
|
|
86
|
+
expect(classifyProviderError("Authentication failed (401). Check API key.")).toBe("auth");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("classifies not-configured errors", () => {
|
|
90
|
+
expect(classifyProviderError("Not configured: missing GitHub token")).toBe("auth");
|
|
91
|
+
expect(classifyProviderError("Copilot not enabled for this account")).toBe("auth");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("classifies refresh failure errors", () => {
|
|
95
|
+
expect(classifyProviderError("No valid access token and refresh failed.")).toBe("auth");
|
|
96
|
+
expect(
|
|
97
|
+
classifyProviderError("ChatGPT account ID required. Re-authenticate in OpenCode."),
|
|
98
|
+
).toBe("auth");
|
|
99
|
+
expect(
|
|
100
|
+
classifyProviderError(
|
|
101
|
+
"MiniMax groupId required. Set MINIMAX_GROUP_ID or configure in OpenCode.",
|
|
102
|
+
),
|
|
103
|
+
).toBe("auth");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("rate limit errors", () => {
|
|
108
|
+
test("classifies rate limit errors", () => {
|
|
109
|
+
expect(classifyProviderError("Rate limited")).toBe("rate_limit");
|
|
110
|
+
expect(classifyProviderError("API error: 429 Too Many Requests")).toBe("rate_limit");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("insufficient credits errors", () => {
|
|
115
|
+
test("classifies insufficient credits errors", () => {
|
|
116
|
+
expect(classifyProviderError("Insufficient credits")).toBe("insufficient_credits");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("timeout errors", () => {
|
|
121
|
+
test("classifies timeout errors", () => {
|
|
122
|
+
expect(classifyProviderError("The operation timed out")).toBe("timeout");
|
|
123
|
+
expect(classifyProviderError("Request timeout")).toBe("timeout");
|
|
124
|
+
expect(classifyProviderError("ETIMEDOUT")).toBe("timeout");
|
|
125
|
+
expect(classifyProviderError("Request aborted")).toBe("timeout");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("network errors", () => {
|
|
130
|
+
test("classifies network errors", () => {
|
|
131
|
+
expect(classifyProviderError("fetch failed")).toBe("network");
|
|
132
|
+
expect(classifyProviderError("ECONNREFUSED")).toBe("network");
|
|
133
|
+
expect(classifyProviderError("ENOTFOUND")).toBe("network");
|
|
134
|
+
expect(classifyProviderError("ECONNRESET")).toBe("network");
|
|
135
|
+
expect(classifyProviderError("DNS resolution failed")).toBe("network");
|
|
136
|
+
expect(classifyProviderError("Network error")).toBe("network");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("unknown errors", () => {
|
|
141
|
+
test("classifies unrecognized errors as unknown", () => {
|
|
142
|
+
expect(classifyProviderError("Unknown error")).toBe("unknown");
|
|
143
|
+
expect(classifyProviderError("API error: 500 Internal Server Error")).toBe("unknown");
|
|
144
|
+
expect(classifyProviderError("Something went wrong")).toBe("unknown");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
package/src/utils/currency.ts
CHANGED
|
@@ -26,7 +26,9 @@ export function parseCurrencyInput(input: string): number | null {
|
|
|
26
26
|
} else if (hasComma) {
|
|
27
27
|
const parts = normalized.split(",");
|
|
28
28
|
// 3 digits after comma = thousands separator (1,234), otherwise decimal (10,50)
|
|
29
|
-
|
|
29
|
+
const afterComma = parts[1]?.length ?? 0;
|
|
30
|
+
const beforeComma = parts[0]?.length ?? 0;
|
|
31
|
+
if (parts.length === 2 && afterComma === 3 && beforeComma > 0) {
|
|
30
32
|
normalized = normalized.replace(",", "");
|
|
31
33
|
} else {
|
|
32
34
|
normalized = normalized.replace(",", ".");
|
|
@@ -35,7 +37,7 @@ export function parseCurrencyInput(input: string): number | null {
|
|
|
35
37
|
|
|
36
38
|
const parsed = parseFloat(normalized);
|
|
37
39
|
|
|
38
|
-
if (isNaN(parsed) || !isFinite(parsed)) {
|
|
40
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
39
41
|
return null;
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type ProviderErrorCategory =
|
|
2
|
+
| "auth"
|
|
3
|
+
| "rate_limit"
|
|
4
|
+
| "timeout"
|
|
5
|
+
| "network"
|
|
6
|
+
| "insufficient_credits"
|
|
7
|
+
| "unknown";
|
|
8
|
+
|
|
9
|
+
export function classifyProviderError(error: string): ProviderErrorCategory {
|
|
10
|
+
const lower = error.toLowerCase();
|
|
11
|
+
|
|
12
|
+
// Auth errors: expired tokens, invalid credentials, scope issues
|
|
13
|
+
if (
|
|
14
|
+
lower.includes("expired") ||
|
|
15
|
+
lower.includes("re-authenticate") ||
|
|
16
|
+
lower.includes("invalid") ||
|
|
17
|
+
lower.includes("unauthorized") ||
|
|
18
|
+
lower.includes("scope") ||
|
|
19
|
+
lower.includes("authentication failed") ||
|
|
20
|
+
lower.includes("authorization failed") ||
|
|
21
|
+
lower.includes("check api key") ||
|
|
22
|
+
lower.includes("api key required") ||
|
|
23
|
+
lower.includes("token required") ||
|
|
24
|
+
lower.includes("not configured") ||
|
|
25
|
+
lower.includes("sign in") ||
|
|
26
|
+
lower.includes("refresh failed") ||
|
|
27
|
+
lower.includes("not enabled") ||
|
|
28
|
+
lower.includes("configure") ||
|
|
29
|
+
/\b401\b/.test(lower) ||
|
|
30
|
+
/\b403\b/.test(lower)
|
|
31
|
+
) {
|
|
32
|
+
return "auth";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Rate limits
|
|
36
|
+
if (
|
|
37
|
+
lower.includes("rate limit") ||
|
|
38
|
+
lower.includes("too many requests") ||
|
|
39
|
+
/\b429\b/.test(lower)
|
|
40
|
+
) {
|
|
41
|
+
return "rate_limit";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Insufficient credits
|
|
45
|
+
if (lower.includes("insufficient") || lower.includes("credits") || /\b402\b/.test(lower)) {
|
|
46
|
+
return "insufficient_credits";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Timeouts
|
|
50
|
+
if (
|
|
51
|
+
lower.includes("timeout") ||
|
|
52
|
+
lower.includes("timed out") ||
|
|
53
|
+
lower.includes("etimedout") ||
|
|
54
|
+
lower.includes("aborted")
|
|
55
|
+
) {
|
|
56
|
+
return "timeout";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Network errors
|
|
60
|
+
if (
|
|
61
|
+
lower.includes("econnrefused") ||
|
|
62
|
+
lower.includes("enotfound") ||
|
|
63
|
+
lower.includes("network") ||
|
|
64
|
+
lower.includes("fetch failed") ||
|
|
65
|
+
lower.includes("dns") ||
|
|
66
|
+
lower.includes("econnreset")
|
|
67
|
+
) {
|
|
68
|
+
return "network";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return "unknown";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ErrorCategoryDisplay {
|
|
75
|
+
label: string;
|
|
76
|
+
icon: string;
|
|
77
|
+
isWarning: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const CATEGORY_DISPLAY: Record<ProviderErrorCategory, ErrorCategoryDisplay> = {
|
|
81
|
+
auth: { label: "AUTH", icon: "\u26B7", isWarning: true },
|
|
82
|
+
rate_limit: { label: "RATE", icon: "\u29D7", isWarning: true },
|
|
83
|
+
timeout: { label: "TIME", icon: "\u29D7", isWarning: true },
|
|
84
|
+
insufficient_credits: { label: "CRED", icon: "\u2717", isWarning: true },
|
|
85
|
+
network: { label: "NET", icon: "\u2717", isWarning: false },
|
|
86
|
+
unknown: { label: "ERR", icon: "\u2717", isWarning: false },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export function getErrorCategoryDisplay(category: ProviderErrorCategory): ErrorCategoryDisplay {
|
|
90
|
+
return CATEGORY_DISPLAY[category];
|
|
91
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.6.0"; // x-release-please-version
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { BasePlugin, PluginHttpClient, PluginLogger } from "./base.ts";
|
|
3
|
-
import type { Credentials, OAuthCredentials, PluginContext } from "./provider.ts";
|
|
4
|
-
|
|
5
|
-
export const AgentCapabilitiesSchema = z.object({
|
|
6
|
-
sessionParsing: z.boolean(),
|
|
7
|
-
authReading: z.boolean(),
|
|
8
|
-
realTimeTracking: z.boolean(),
|
|
9
|
-
multiProvider: z.boolean(),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export type AgentCapabilities = z.infer<typeof AgentCapabilitiesSchema>;
|
|
13
|
-
|
|
14
|
-
export interface AgentConfig {
|
|
15
|
-
/** Display name for this coding agent (e.g. "OpenCode", "Cursor"). */
|
|
16
|
-
name: string;
|
|
17
|
-
/** CLI command that launches the agent (for display/detection). */
|
|
18
|
-
command?: string;
|
|
19
|
-
/** Path to the agent's config directory (for display/debugging). */
|
|
20
|
-
configPath?: string;
|
|
21
|
-
/** Path to the agent's session storage. */
|
|
22
|
-
sessionPath?: string;
|
|
23
|
-
/** Path to the agent's auth file (for credential reading). */
|
|
24
|
-
authPath?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface AgentCredentials {
|
|
28
|
-
providers: Record<string, Credentials | undefined>;
|
|
29
|
-
oauth?: Record<string, OAuthCredentials>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface AgentProviderConfig {
|
|
33
|
-
id: string;
|
|
34
|
-
name: string;
|
|
35
|
-
configured: boolean;
|
|
36
|
-
enabled?: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface SessionParseOptions {
|
|
40
|
-
sessionId?: string;
|
|
41
|
-
timePeriod?: "session" | "daily" | "weekly" | "monthly";
|
|
42
|
-
limit?: number;
|
|
43
|
-
/** Epoch ms — only return sessions updated after this timestamp. */
|
|
44
|
-
since?: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface SessionUsageData {
|
|
48
|
-
sessionId: string;
|
|
49
|
-
sessionName?: string;
|
|
50
|
-
providerId: string;
|
|
51
|
-
modelId: string;
|
|
52
|
-
tokens: {
|
|
53
|
-
input: number;
|
|
54
|
-
output: number;
|
|
55
|
-
cacheRead?: number;
|
|
56
|
-
cacheWrite?: number;
|
|
57
|
-
};
|
|
58
|
-
timestamp: number;
|
|
59
|
-
sessionUpdatedAt?: number;
|
|
60
|
-
projectPath?: string;
|
|
61
|
-
cost?: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface AgentFetchContext {
|
|
65
|
-
readonly http: PluginHttpClient;
|
|
66
|
-
readonly logger: PluginLogger;
|
|
67
|
-
readonly config: Record<string, unknown>;
|
|
68
|
-
readonly signal: AbortSignal;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface ActivityUpdate {
|
|
72
|
-
sessionId: string;
|
|
73
|
-
messageId: string;
|
|
74
|
-
tokens: {
|
|
75
|
-
input: number;
|
|
76
|
-
output: number;
|
|
77
|
-
reasoning?: number;
|
|
78
|
-
cacheRead?: number;
|
|
79
|
-
cacheWrite?: number;
|
|
80
|
-
};
|
|
81
|
-
timestamp: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export type ActivityCallback = (update: ActivityUpdate) => void;
|
|
85
|
-
|
|
86
|
-
export interface AgentPlugin extends BasePlugin {
|
|
87
|
-
readonly type: "agent";
|
|
88
|
-
readonly agent: AgentConfig;
|
|
89
|
-
readonly capabilities: AgentCapabilities;
|
|
90
|
-
|
|
91
|
-
/** Check if this coding agent is installed on the user's machine. */
|
|
92
|
-
isInstalled(ctx: PluginContext): Promise<boolean>;
|
|
93
|
-
|
|
94
|
-
/** Read credentials that the coding agent has stored. */
|
|
95
|
-
readCredentials?(ctx: AgentFetchContext): Promise<AgentCredentials>;
|
|
96
|
-
|
|
97
|
-
/** Parse session usage data from the agent's local storage. */
|
|
98
|
-
parseSessions(options: SessionParseOptions, ctx: AgentFetchContext): Promise<SessionUsageData[]>;
|
|
99
|
-
|
|
100
|
-
/** List which model providers this agent is configured to use. */
|
|
101
|
-
getProviders?(ctx: AgentFetchContext): Promise<AgentProviderConfig[]>;
|
|
102
|
-
|
|
103
|
-
/** Start watching for real-time activity updates. */
|
|
104
|
-
startActivityWatch?(ctx: PluginContext, callback: ActivityCallback): void;
|
|
105
|
-
|
|
106
|
-
/** Stop watching for real-time activity updates. */
|
|
107
|
-
stopActivityWatch?(ctx: PluginContext): void;
|
|
108
|
-
}
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Base plugin types and interfaces for the tokentop plugin system.
|
|
3
|
-
*
|
|
4
|
-
* These types mirror the Plugin SDK (@tokentop/plugin-sdk) — the SDK is the
|
|
5
|
-
* authoritative contract for community developers; core adapts to match.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { z } from "zod";
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Identity
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Plugin type discriminator
|
|
16
|
-
*/
|
|
17
|
-
export type PluginType = "provider" | "agent" | "theme" | "notification";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Current API contract version.
|
|
21
|
-
* Core checks this at load time to ensure compatibility.
|
|
22
|
-
*/
|
|
23
|
-
export const CURRENT_API_VERSION = 2;
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Permissions
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Plugin permissions schema - defines what a plugin can access
|
|
31
|
-
*/
|
|
32
|
-
export const PluginPermissionsSchema = z.object({
|
|
33
|
-
network: z
|
|
34
|
-
.object({
|
|
35
|
-
enabled: z.boolean(),
|
|
36
|
-
allowedDomains: z.array(z.string()).optional(),
|
|
37
|
-
})
|
|
38
|
-
.optional(),
|
|
39
|
-
filesystem: z
|
|
40
|
-
.object({
|
|
41
|
-
read: z.boolean().optional(),
|
|
42
|
-
write: z.boolean().optional(),
|
|
43
|
-
paths: z.array(z.string()).optional(),
|
|
44
|
-
})
|
|
45
|
-
.optional(),
|
|
46
|
-
env: z
|
|
47
|
-
.object({
|
|
48
|
-
read: z.boolean().optional(),
|
|
49
|
-
vars: z.array(z.string()).optional(),
|
|
50
|
-
})
|
|
51
|
-
.optional(),
|
|
52
|
-
system: z
|
|
53
|
-
.object({
|
|
54
|
-
notifications: z.boolean().optional(),
|
|
55
|
-
clipboard: z.boolean().optional(),
|
|
56
|
-
})
|
|
57
|
-
.optional(),
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
export type PluginPermissions = z.infer<typeof PluginPermissionsSchema>;
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Metadata
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Plugin metadata
|
|
68
|
-
*/
|
|
69
|
-
export const PluginMetaSchema = z.object({
|
|
70
|
-
author: z.string().optional(),
|
|
71
|
-
description: z.string().optional(),
|
|
72
|
-
homepage: z.string().url().optional(),
|
|
73
|
-
repository: z.string().url().optional(),
|
|
74
|
-
license: z.string().optional(),
|
|
75
|
-
/**
|
|
76
|
-
* Brand color as a hex string (e.g. `"#d97757"`).
|
|
77
|
-
* Used by the TUI for provider cards, charts, and status indicators.
|
|
78
|
-
*/
|
|
79
|
-
brandColor: z.string().optional(),
|
|
80
|
-
/**
|
|
81
|
-
* Single-character icon or short glyph for compact displays.
|
|
82
|
-
* Example: `"◆"`, `"▲"`, `"⚡"`
|
|
83
|
-
*/
|
|
84
|
-
icon: z.string().optional(),
|
|
85
|
-
/**
|
|
86
|
-
* Additional provider IDs that should resolve to this plugin.
|
|
87
|
-
* Coding agents may tag sessions with provider IDs that differ from
|
|
88
|
-
* the plugin's `id` (e.g. OpenCode uses `"openai"` but the plugin
|
|
89
|
-
* registers as `"openai-api"`). List those alternate IDs here so the
|
|
90
|
-
* TUI can resolve brand colors and other metadata correctly.
|
|
91
|
-
*/
|
|
92
|
-
providerAliases: z.array(z.string()).optional(),
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
export type PluginMeta = z.infer<typeof PluginMetaSchema>;
|
|
96
|
-
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// Configuration
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Plugin configuration field definition (for plugin settings UI)
|
|
103
|
-
*/
|
|
104
|
-
export interface ConfigField {
|
|
105
|
-
/** Data type of the setting value. */
|
|
106
|
-
type: "string" | "number" | "boolean" | "select";
|
|
107
|
-
/** Label shown in the settings UI. */
|
|
108
|
-
label?: string;
|
|
109
|
-
/** Help text shown below the field. */
|
|
110
|
-
description?: string;
|
|
111
|
-
/** Whether the field must have a value. */
|
|
112
|
-
required?: boolean;
|
|
113
|
-
/** Default value when no user config exists. */
|
|
114
|
-
default?: unknown;
|
|
115
|
-
/** Available options (for `select` type only). */
|
|
116
|
-
options?: Array<{ value: string; label: string }>;
|
|
117
|
-
/** Minimum value (for `number` type only). */
|
|
118
|
-
min?: number;
|
|
119
|
-
/** Maximum value (for `number` type only). */
|
|
120
|
-
max?: number;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// Lifecycle Context
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Minimal context provided to lifecycle hooks.
|
|
129
|
-
*/
|
|
130
|
-
export interface PluginLifecycleContext {
|
|
131
|
-
/** Plugin's validated configuration values. */
|
|
132
|
-
readonly config: Record<string, unknown>;
|
|
133
|
-
/** Scoped logger that prefixes all output with the plugin ID. */
|
|
134
|
-
readonly logger: PluginLogger;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
// Base Plugin
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Base plugin interface - all plugins must implement this.
|
|
143
|
-
*
|
|
144
|
-
* Provides identity, metadata, permissions, optional configuration schema,
|
|
145
|
-
* and lifecycle hooks.
|
|
146
|
-
*/
|
|
147
|
-
export interface BasePlugin {
|
|
148
|
-
/** API version this plugin targets. Must equal {@link CURRENT_API_VERSION}. */
|
|
149
|
-
readonly apiVersion: 2;
|
|
150
|
-
|
|
151
|
-
/** Unique plugin identifier (kebab-case) */
|
|
152
|
-
readonly id: string;
|
|
153
|
-
|
|
154
|
-
/** Plugin type discriminator */
|
|
155
|
-
readonly type: PluginType;
|
|
156
|
-
|
|
157
|
-
/** Human-readable display name */
|
|
158
|
-
readonly name: string;
|
|
159
|
-
|
|
160
|
-
/** Semantic version string */
|
|
161
|
-
readonly version: string;
|
|
162
|
-
|
|
163
|
-
/** Plugin metadata */
|
|
164
|
-
readonly meta?: PluginMeta;
|
|
165
|
-
|
|
166
|
-
/** Required permissions */
|
|
167
|
-
readonly permissions: PluginPermissions;
|
|
168
|
-
|
|
169
|
-
/** Plugin-declared configuration fields, rendered in Settings UI */
|
|
170
|
-
readonly configSchema?: Record<string, ConfigField>;
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Default config values. Used when no user configuration exists.
|
|
174
|
-
* Keys must match those in {@link configSchema}.
|
|
175
|
-
*/
|
|
176
|
-
readonly defaultConfig?: Record<string, unknown>;
|
|
177
|
-
|
|
178
|
-
// -- Lifecycle Hooks (all optional) -------------------------------------
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Called once after the plugin is loaded and validated.
|
|
182
|
-
* Use for one-time setup (open connections, allocate resources).
|
|
183
|
-
*/
|
|
184
|
-
initialize?(ctx: PluginLifecycleContext): Promise<void>;
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Called when the plugin should begin active work (e.g. polling).
|
|
188
|
-
* Called after `initialize()` during app startup, and after re-enable.
|
|
189
|
-
*/
|
|
190
|
-
start?(ctx: PluginLifecycleContext): Promise<void>;
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Called when the plugin should pause active work.
|
|
194
|
-
* Called before `destroy()` during app shutdown, and on disable.
|
|
195
|
-
*/
|
|
196
|
-
stop?(ctx: PluginLifecycleContext): Promise<void>;
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Called once before the plugin is unloaded.
|
|
200
|
-
* Use for cleanup (close connections, flush buffers).
|
|
201
|
-
*/
|
|
202
|
-
destroy?(ctx: PluginLifecycleContext): Promise<void>;
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Called when the user changes this plugin's configuration.
|
|
206
|
-
* Receive the new validated config values.
|
|
207
|
-
*/
|
|
208
|
-
onConfigChange?(
|
|
209
|
-
config: Record<string, unknown>,
|
|
210
|
-
ctx: PluginLifecycleContext,
|
|
211
|
-
): Promise<void> | void;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
// Logger
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Logger interface provided to plugins
|
|
220
|
-
*/
|
|
221
|
-
export interface PluginLogger {
|
|
222
|
-
debug(message: string, data?: Record<string, unknown>): void;
|
|
223
|
-
info(message: string, data?: Record<string, unknown>): void;
|
|
224
|
-
warn(message: string, data?: Record<string, unknown>): void;
|
|
225
|
-
error(message: string, data?: Record<string, unknown>): void;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ---------------------------------------------------------------------------
|
|
229
|
-
// HTTP Client
|
|
230
|
-
// ---------------------------------------------------------------------------
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* HTTP client interface provided to plugins (sandboxed)
|
|
234
|
-
*/
|
|
235
|
-
export interface PluginHttpClient {
|
|
236
|
-
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ---------------------------------------------------------------------------
|
|
240
|
-
// Validation
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Plugin validation result
|
|
245
|
-
*/
|
|
246
|
-
export interface PluginValidationResult {
|
|
247
|
-
valid: boolean;
|
|
248
|
-
errors: string[];
|
|
249
|
-
warnings: string[];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Plugin load result
|
|
254
|
-
*/
|
|
255
|
-
export interface PluginLoadResult<T extends BasePlugin = BasePlugin> {
|
|
256
|
-
success: boolean;
|
|
257
|
-
plugin?: T;
|
|
258
|
-
error?: string;
|
|
259
|
-
source: "builtin" | "local" | "npm";
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
// Errors
|
|
264
|
-
// ---------------------------------------------------------------------------
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Plugin permission error
|
|
268
|
-
*/
|
|
269
|
-
export class PluginPermissionError extends Error {
|
|
270
|
-
constructor(
|
|
271
|
-
public readonly pluginId: string,
|
|
272
|
-
public readonly permission: keyof PluginPermissions,
|
|
273
|
-
message: string,
|
|
274
|
-
) {
|
|
275
|
-
super(`Plugin "${pluginId}" permission denied: ${message}`);
|
|
276
|
-
this.name = "PluginPermissionError";
|
|
277
|
-
}
|
|
278
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export * from "./agent.ts";
|
|
2
|
-
export * from "./base.ts";
|
|
3
|
-
export * from "./notification.ts";
|
|
4
|
-
export * from "./provider.ts";
|
|
5
|
-
export * from "./theme.ts";
|
|
6
|
-
|
|
7
|
-
import type { AgentPlugin } from "./agent.ts";
|
|
8
|
-
import type { NotificationPlugin } from "./notification.ts";
|
|
9
|
-
import type { ProviderPlugin } from "./provider.ts";
|
|
10
|
-
import type { ThemePlugin } from "./theme.ts";
|
|
11
|
-
|
|
12
|
-
export type AnyPlugin = ProviderPlugin | AgentPlugin | ThemePlugin | NotificationPlugin;
|
|
13
|
-
|
|
14
|
-
export type PluginByType = {
|
|
15
|
-
provider: ProviderPlugin;
|
|
16
|
-
agent: AgentPlugin;
|
|
17
|
-
theme: ThemePlugin;
|
|
18
|
-
notification: NotificationPlugin;
|
|
19
|
-
};
|