@aliou/pi-synthetic 0.4.6 → 0.5.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/package.json +24 -7
- package/src/commands/quotas.ts +19 -85
- package/src/components/quotas-display.ts +211 -110
- package/src/components/tabbed-panel.ts +161 -0
- package/src/hooks/search-tool-availability.ts +2 -27
- package/src/hooks/sub-integration.ts +145 -0
- package/src/index.ts +2 -0
- package/src/providers/models.ts +15 -0
- package/src/types/quotas.ts +12 -0
- package/src/utils/quotas.ts +33 -0
- package/.changeset/config.json +0 -11
- package/.github/workflows/ci.yml +0 -30
- package/.github/workflows/publish.yml +0 -151
- package/.husky/pre-commit +0 -3
- package/AGENTS.md +0 -69
- package/CHANGELOG.md +0 -91
- package/biome.json +0 -30
- package/shell.nix +0 -10
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
4
|
+
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
5
|
+
|
|
6
|
+
export interface PanelTab {
|
|
7
|
+
label: string;
|
|
8
|
+
buildContent: () => string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TabbedScrollablePanelOptions {
|
|
12
|
+
title: string;
|
|
13
|
+
tabs: PanelTab[];
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
maxVisible?: number;
|
|
16
|
+
keymap?: "vim" | "default";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class TabbedScrollablePanel implements Component {
|
|
20
|
+
private activeTab = 0;
|
|
21
|
+
private scrollOffset = 0;
|
|
22
|
+
private cachedLines: string[] | null = null;
|
|
23
|
+
private cachedWidth = 0;
|
|
24
|
+
private border: DynamicBorder;
|
|
25
|
+
private options: TabbedScrollablePanelOptions;
|
|
26
|
+
private theme: Theme;
|
|
27
|
+
|
|
28
|
+
constructor(options: TabbedScrollablePanelOptions, _tui: TUI, theme: Theme) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.theme = theme;
|
|
31
|
+
this.border = new DynamicBorder((segment) => theme.fg("border", segment));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
handleInput(data: string): boolean {
|
|
35
|
+
if (matchesKey(data, "escape") || data === "q") {
|
|
36
|
+
this.options.onClose();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (matchesKey(data, "tab")) {
|
|
41
|
+
this.activeTab = (this.activeTab + 1) % this.options.tabs.length;
|
|
42
|
+
this.scrollOffset = 0;
|
|
43
|
+
this.invalidate();
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (matchesKey(data, "shift+tab")) {
|
|
48
|
+
this.activeTab =
|
|
49
|
+
(this.activeTab - 1 + this.options.tabs.length) %
|
|
50
|
+
this.options.tabs.length;
|
|
51
|
+
this.scrollOffset = 0;
|
|
52
|
+
this.invalidate();
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const maxVisible = this.options.maxVisible ?? 16;
|
|
57
|
+
const totalLines = this.cachedLines?.length ?? 0;
|
|
58
|
+
const maxScroll = Math.max(0, totalLines - maxVisible);
|
|
59
|
+
|
|
60
|
+
if (data === "j" || matchesKey(data, "down")) {
|
|
61
|
+
if (this.scrollOffset < maxScroll) {
|
|
62
|
+
this.scrollOffset++;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (data === "k" || matchesKey(data, "up")) {
|
|
68
|
+
if (this.scrollOffset > 0) {
|
|
69
|
+
this.scrollOffset--;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (data === " " || matchesKey(data, "pageDown")) {
|
|
75
|
+
this.scrollOffset = Math.min(this.scrollOffset + maxVisible, maxScroll);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (matchesKey(data, "pageUp")) {
|
|
80
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - maxVisible);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (matchesKey(data, "home")) {
|
|
85
|
+
this.scrollOffset = 0;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (matchesKey(data, "end")) {
|
|
90
|
+
this.scrollOffset = maxScroll;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
invalidate(): void {
|
|
98
|
+
this.cachedLines = null;
|
|
99
|
+
this.cachedWidth = 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
render(width: number): string[] {
|
|
103
|
+
const tab = this.options.tabs[this.activeTab];
|
|
104
|
+
|
|
105
|
+
if (!this.cachedLines || this.cachedWidth !== width) {
|
|
106
|
+
this.cachedLines = tab ? tab.buildContent() : [];
|
|
107
|
+
this.cachedWidth = width;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const lines: string[] = [];
|
|
111
|
+
|
|
112
|
+
lines.push(...this.border.render(width));
|
|
113
|
+
lines.push(
|
|
114
|
+
truncateToWidth(
|
|
115
|
+
` ${this.theme.fg("accent", this.theme.bold(this.options.title))}`,
|
|
116
|
+
width,
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
lines.push(this.renderTabBar(width));
|
|
120
|
+
lines.push("");
|
|
121
|
+
|
|
122
|
+
// Content - no forced padding, just render what we have
|
|
123
|
+
for (const line of this.cachedLines) {
|
|
124
|
+
lines.push(truncateToWidth(` ${line}`, width));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Footer directly after content
|
|
128
|
+
lines.push("");
|
|
129
|
+
lines.push(
|
|
130
|
+
truncateToWidth(
|
|
131
|
+
this.theme.fg("dim", " Tab/S-Tab switch tabs q/Esc close"),
|
|
132
|
+
width,
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
lines.push(...this.border.render(width));
|
|
136
|
+
|
|
137
|
+
return lines;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private renderTabBar(width: number): string {
|
|
141
|
+
const parts: string[] = [];
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < this.options.tabs.length; i++) {
|
|
144
|
+
const tab = this.options.tabs[i];
|
|
145
|
+
if (!tab) continue;
|
|
146
|
+
const active = i === this.activeTab;
|
|
147
|
+
|
|
148
|
+
if (active) {
|
|
149
|
+
parts.push(this.theme.fg("accent", this.theme.bold(` ${tab.label} `)));
|
|
150
|
+
} else {
|
|
151
|
+
parts.push(this.theme.fg("dim", ` ${tab.label} `));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (i < this.options.tabs.length - 1) {
|
|
155
|
+
parts.push(this.theme.fg("borderMuted", "│"));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return truncateToWidth(` ${parts.join("")}`, width);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
} from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
2
|
import { SYNTHETIC_WEB_SEARCH_TOOL } from "../tools/search";
|
|
6
3
|
|
|
7
|
-
function notifyDebug(ctx: ExtensionContext, message: string): void {
|
|
8
|
-
ctx.ui.notify(`[pi-synthetic:web-search] ${message}`, "info");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
4
|
async function checkSubscriptionAccess(
|
|
12
5
|
apiKey: string,
|
|
13
6
|
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
@@ -51,48 +44,36 @@ export function registerSyntheticWebSearchHooks(pi: ExtensionAPI): void {
|
|
|
51
44
|
let didNotifyDenied = false;
|
|
52
45
|
|
|
53
46
|
// Keep tool inactive at session start. Availability is decided before each agent run.
|
|
54
|
-
pi.on("session_start", (
|
|
55
|
-
notifyDebug(ctx, "session_start: preparing web search tool");
|
|
56
|
-
|
|
47
|
+
pi.on("session_start", () => {
|
|
57
48
|
const current = pi.getActiveTools();
|
|
58
49
|
if (current.includes(SYNTHETIC_WEB_SEARCH_TOOL)) {
|
|
59
50
|
pi.setActiveTools(
|
|
60
51
|
current.filter((toolName) => toolName !== SYNTHETIC_WEB_SEARCH_TOOL),
|
|
61
52
|
);
|
|
62
|
-
notifyDebug(ctx, "session_start: tool disabled until subscription check");
|
|
63
53
|
}
|
|
64
54
|
});
|
|
65
55
|
|
|
66
56
|
// Verify subscription only when user starts agent execution.
|
|
67
57
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
68
|
-
notifyDebug(ctx, "before_agent_start: ensuring tool availability");
|
|
69
|
-
|
|
70
58
|
const apiKey = process.env.SYNTHETIC_API_KEY;
|
|
71
59
|
if (!apiKey) {
|
|
72
60
|
hasAccess = false;
|
|
73
61
|
deniedReason = "SYNTHETIC_API_KEY is not configured";
|
|
74
62
|
accessCheckPromise = undefined;
|
|
75
|
-
notifyDebug(ctx, "before_agent_start: access denied (missing API key)");
|
|
76
63
|
} else {
|
|
77
64
|
if (deniedReason === "SYNTHETIC_API_KEY is not configured") {
|
|
78
65
|
deniedReason = undefined;
|
|
79
66
|
}
|
|
80
67
|
|
|
81
68
|
if (!hasAccess && !deniedReason) {
|
|
82
|
-
notifyDebug(ctx, "before_agent_start: checking subscription access");
|
|
83
69
|
accessCheckPromise ??= checkSubscriptionAccess(apiKey);
|
|
84
70
|
const access = await accessCheckPromise;
|
|
85
71
|
|
|
86
72
|
if (!access.ok) {
|
|
87
73
|
deniedReason = access.reason;
|
|
88
|
-
notifyDebug(
|
|
89
|
-
ctx,
|
|
90
|
-
`before_agent_start: access denied (${access.reason})`,
|
|
91
|
-
);
|
|
92
74
|
} else {
|
|
93
75
|
hasAccess = true;
|
|
94
76
|
didNotifyDenied = false;
|
|
95
|
-
notifyDebug(ctx, "before_agent_start: access granted");
|
|
96
77
|
}
|
|
97
78
|
}
|
|
98
79
|
}
|
|
@@ -103,7 +84,6 @@ export function registerSyntheticWebSearchHooks(pi: ExtensionAPI): void {
|
|
|
103
84
|
pi.setActiveTools(
|
|
104
85
|
current.filter((toolName) => toolName !== SYNTHETIC_WEB_SEARCH_TOOL),
|
|
105
86
|
);
|
|
106
|
-
notifyDebug(ctx, "before_agent_start: tool kept disabled");
|
|
107
87
|
}
|
|
108
88
|
|
|
109
89
|
if (ctx.hasUI && !didNotifyDenied) {
|
|
@@ -112,10 +92,6 @@ export function registerSyntheticWebSearchHooks(pi: ExtensionAPI): void {
|
|
|
112
92
|
"warning",
|
|
113
93
|
);
|
|
114
94
|
didNotifyDenied = true;
|
|
115
|
-
notifyDebug(
|
|
116
|
-
ctx,
|
|
117
|
-
"before_agent_start: user notified about disabled tool",
|
|
118
|
-
);
|
|
119
95
|
}
|
|
120
96
|
return;
|
|
121
97
|
}
|
|
@@ -123,7 +99,6 @@ export function registerSyntheticWebSearchHooks(pi: ExtensionAPI): void {
|
|
|
123
99
|
const current = pi.getActiveTools();
|
|
124
100
|
if (!current.includes(SYNTHETIC_WEB_SEARCH_TOOL)) {
|
|
125
101
|
pi.setActiveTools([...current, SYNTHETIC_WEB_SEARCH_TOOL]);
|
|
126
|
-
notifyDebug(ctx, "before_agent_start: tool enabled");
|
|
127
102
|
}
|
|
128
103
|
});
|
|
129
104
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { QuotasResponse } from "../types/quotas";
|
|
3
|
+
import { fetchQuotas, formatResetTime } from "../utils/quotas";
|
|
4
|
+
|
|
5
|
+
interface RateWindow {
|
|
6
|
+
label: string;
|
|
7
|
+
usedPercent: number;
|
|
8
|
+
resetDescription?: string;
|
|
9
|
+
resetAt?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface UsageSnapshot {
|
|
13
|
+
provider: string;
|
|
14
|
+
displayName: string;
|
|
15
|
+
windows: RateWindow[];
|
|
16
|
+
lastSuccessAt?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SubCoreSettingsPayload {
|
|
20
|
+
settings?: {
|
|
21
|
+
behavior?: {
|
|
22
|
+
refreshInterval: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toUsageSnapshot(quotas: QuotasResponse): UsageSnapshot {
|
|
28
|
+
const windows: RateWindow[] = [];
|
|
29
|
+
|
|
30
|
+
if (quotas.subscription) {
|
|
31
|
+
const pct =
|
|
32
|
+
(quotas.subscription.requests / quotas.subscription.limit) * 100;
|
|
33
|
+
windows.push({
|
|
34
|
+
label: "5h",
|
|
35
|
+
usedPercent: Math.round(pct),
|
|
36
|
+
resetDescription: formatResetTime(quotas.subscription.renewsAt),
|
|
37
|
+
resetAt: quotas.subscription.renewsAt,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (quotas.search?.hourly) {
|
|
42
|
+
const pct =
|
|
43
|
+
(quotas.search.hourly.requests / quotas.search.hourly.limit) * 100;
|
|
44
|
+
windows.push({
|
|
45
|
+
label: "Search",
|
|
46
|
+
usedPercent: Math.round(pct),
|
|
47
|
+
resetDescription: formatResetTime(quotas.search.hourly.renewsAt),
|
|
48
|
+
resetAt: quotas.search.hourly.renewsAt,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (quotas.freeToolCalls) {
|
|
53
|
+
const pct =
|
|
54
|
+
(quotas.freeToolCalls.requests / quotas.freeToolCalls.limit) * 100;
|
|
55
|
+
windows.push({
|
|
56
|
+
label: "Free",
|
|
57
|
+
usedPercent: Math.round(pct),
|
|
58
|
+
resetDescription: formatResetTime(quotas.freeToolCalls.renewsAt),
|
|
59
|
+
resetAt: quotas.freeToolCalls.renewsAt,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
provider: "synthetic",
|
|
65
|
+
displayName: "Synthetic",
|
|
66
|
+
windows,
|
|
67
|
+
lastSuccessAt: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function emitCurrentUsage(pi: ExtensionAPI): Promise<void> {
|
|
72
|
+
const quotas = await fetchQuotas();
|
|
73
|
+
if (!quotas) return;
|
|
74
|
+
pi.events.emit("sub-core:update-current", {
|
|
75
|
+
state: { provider: "synthetic", usage: toUsageSnapshot(quotas) },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function registerSubIntegration(pi: ExtensionAPI): void {
|
|
80
|
+
if (!process.env.SYNTHETIC_API_KEY) return;
|
|
81
|
+
|
|
82
|
+
let interval: NodeJS.Timeout | undefined;
|
|
83
|
+
let refreshMs = 60000;
|
|
84
|
+
let subCoreReady = false;
|
|
85
|
+
let currentProvider: string | undefined;
|
|
86
|
+
|
|
87
|
+
function isSynthetic(): boolean {
|
|
88
|
+
return currentProvider === "synthetic";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function stop(): void {
|
|
92
|
+
if (interval) {
|
|
93
|
+
clearInterval(interval);
|
|
94
|
+
interval = undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function start(): void {
|
|
99
|
+
stop();
|
|
100
|
+
if (!subCoreReady || !isSynthetic()) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
emitCurrentUsage(pi);
|
|
104
|
+
const ms = Math.max(10000, refreshMs);
|
|
105
|
+
interval = setInterval(() => {
|
|
106
|
+
if (isSynthetic()) emitCurrentUsage(pi);
|
|
107
|
+
}, ms);
|
|
108
|
+
interval.unref?.();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Custom events (inter-extension bus)
|
|
112
|
+
pi.events.on("sub-core:ready", () => {
|
|
113
|
+
subCoreReady = true;
|
|
114
|
+
start();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
pi.events.on("sub-core:settings:updated", (data: unknown) => {
|
|
118
|
+
const payload = data as SubCoreSettingsPayload;
|
|
119
|
+
if (payload.settings?.behavior?.refreshInterval) {
|
|
120
|
+
refreshMs = payload.settings.behavior.refreshInterval * 1000;
|
|
121
|
+
if (interval) start();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Lifecycle events (pi.on, not pi.events.on)
|
|
126
|
+
pi.on("session_start", (_event, ctx) => {
|
|
127
|
+
currentProvider = ctx.model?.provider;
|
|
128
|
+
start();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
pi.on("model_select", (event, _ctx) => {
|
|
132
|
+
currentProvider = event.model?.provider;
|
|
133
|
+
if (isSynthetic()) {
|
|
134
|
+
emitCurrentUsage(pi);
|
|
135
|
+
start();
|
|
136
|
+
} else {
|
|
137
|
+
stop();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
pi.on("session_shutdown", () => {
|
|
142
|
+
currentProvider = undefined;
|
|
143
|
+
stop();
|
|
144
|
+
});
|
|
145
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { registerQuotasCommand } from "./commands/quotas";
|
|
3
3
|
import { registerSyntheticWebSearchHooks } from "./hooks/search-tool-availability";
|
|
4
|
+
import { registerSubIntegration } from "./hooks/sub-integration";
|
|
4
5
|
import { registerSyntheticProvider } from "./providers/index";
|
|
5
6
|
import { registerSyntheticWebSearchTool } from "./tools/search";
|
|
6
7
|
|
|
@@ -11,5 +12,6 @@ export default async function (pi: ExtensionAPI) {
|
|
|
11
12
|
|
|
12
13
|
if (process.env.SYNTHETIC_API_KEY) {
|
|
13
14
|
registerQuotasCommand(pi);
|
|
15
|
+
registerSubIntegration(pi);
|
|
14
16
|
}
|
|
15
17
|
}
|
package/src/providers/models.ts
CHANGED
|
@@ -235,4 +235,19 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
235
235
|
contextWindow: 262144,
|
|
236
236
|
maxTokens: 32000,
|
|
237
237
|
},
|
|
238
|
+
// API: hf:Qwen/Qwen3.5-397B-A17B → ctx=262144, out=32000
|
|
239
|
+
{
|
|
240
|
+
id: "hf:Qwen/Qwen3.5-397B-A17B",
|
|
241
|
+
name: "Qwen/Qwen3.5-397B-A17B",
|
|
242
|
+
reasoning: false,
|
|
243
|
+
input: ["text"],
|
|
244
|
+
cost: {
|
|
245
|
+
input: 0.6,
|
|
246
|
+
output: 3,
|
|
247
|
+
cacheRead: 0.6,
|
|
248
|
+
cacheWrite: 0,
|
|
249
|
+
},
|
|
250
|
+
contextWindow: 262144,
|
|
251
|
+
maxTokens: 32000,
|
|
252
|
+
},
|
|
238
253
|
];
|
package/src/types/quotas.ts
CHANGED
|
@@ -4,4 +4,16 @@ export interface QuotasResponse {
|
|
|
4
4
|
requests: number;
|
|
5
5
|
renewsAt: string;
|
|
6
6
|
};
|
|
7
|
+
search: {
|
|
8
|
+
hourly: {
|
|
9
|
+
limit: number;
|
|
10
|
+
requests: number;
|
|
11
|
+
renewsAt: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
freeToolCalls: {
|
|
15
|
+
limit: number;
|
|
16
|
+
requests: number;
|
|
17
|
+
renewsAt: string;
|
|
18
|
+
};
|
|
7
19
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { QuotasResponse } from "../types/quotas";
|
|
2
|
+
|
|
3
|
+
const API_KEY = process.env.SYNTHETIC_API_KEY;
|
|
4
|
+
|
|
5
|
+
export async function fetchQuotas(): Promise<QuotasResponse | null> {
|
|
6
|
+
if (!API_KEY) return null;
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetch("https://api.synthetic.new/v2/quotas", {
|
|
10
|
+
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!response.ok) return null;
|
|
14
|
+
return (await response.json()) as QuotasResponse;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatResetTime(renewsAt: string): string {
|
|
21
|
+
const date = new Date(renewsAt);
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const diffMs = date.getTime() - now.getTime();
|
|
24
|
+
|
|
25
|
+
if (diffMs <= 0) return "soon";
|
|
26
|
+
|
|
27
|
+
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
|
|
28
|
+
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
|
29
|
+
|
|
30
|
+
if (diffHours < 24) return `in ${diffHours}h`;
|
|
31
|
+
if (diffDays < 7) return `in ${diffDays}d`;
|
|
32
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
33
|
+
}
|
package/.changeset/config.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
|
|
3
|
-
"changelog": "@changesets/cli/changelog",
|
|
4
|
-
"commit": false,
|
|
5
|
-
"fixed": [],
|
|
6
|
-
"linked": [],
|
|
7
|
-
"access": "public",
|
|
8
|
-
"baseBranch": "main",
|
|
9
|
-
"updateInternalDependencies": "patch",
|
|
10
|
-
"ignore": []
|
|
11
|
-
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
|
|
6
|
-
concurrency:
|
|
7
|
-
group: ${{ github.workflow }}-${{ github.ref }}
|
|
8
|
-
cancel-in-progress: true
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
check:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v4
|
|
15
|
-
|
|
16
|
-
- uses: pnpm/action-setup@v4
|
|
17
|
-
|
|
18
|
-
- uses: actions/setup-node@v4
|
|
19
|
-
with:
|
|
20
|
-
node-version: "22"
|
|
21
|
-
cache: "pnpm"
|
|
22
|
-
|
|
23
|
-
- name: Install dependencies
|
|
24
|
-
run: pnpm install --frozen-lockfile
|
|
25
|
-
|
|
26
|
-
- name: Lint
|
|
27
|
-
run: pnpm lint
|
|
28
|
-
|
|
29
|
-
- name: Typecheck
|
|
30
|
-
run: pnpm typecheck
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
name: Publish
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
inputs:
|
|
9
|
-
skip-checks:
|
|
10
|
-
description: "Skip lint and typecheck"
|
|
11
|
-
type: boolean
|
|
12
|
-
default: false
|
|
13
|
-
|
|
14
|
-
concurrency:
|
|
15
|
-
group: ${{ github.workflow }}-${{ github.ref }}
|
|
16
|
-
cancel-in-progress: true
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
check:
|
|
20
|
-
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.skip-checks) }}
|
|
21
|
-
runs-on: ubuntu-latest
|
|
22
|
-
steps:
|
|
23
|
-
- uses: actions/checkout@v4
|
|
24
|
-
|
|
25
|
-
- uses: pnpm/action-setup@v4
|
|
26
|
-
|
|
27
|
-
- uses: actions/setup-node@v4
|
|
28
|
-
with:
|
|
29
|
-
node-version: "22"
|
|
30
|
-
cache: "pnpm"
|
|
31
|
-
|
|
32
|
-
- name: Install dependencies
|
|
33
|
-
run: pnpm install --frozen-lockfile
|
|
34
|
-
|
|
35
|
-
- name: Lint
|
|
36
|
-
run: pnpm lint
|
|
37
|
-
|
|
38
|
-
- name: Typecheck
|
|
39
|
-
run: pnpm typecheck
|
|
40
|
-
|
|
41
|
-
publish:
|
|
42
|
-
name: Publish
|
|
43
|
-
needs: check
|
|
44
|
-
if: ${{ always() && (needs.check.result == 'success' || needs.check.result == 'skipped') }}
|
|
45
|
-
runs-on: ubuntu-latest
|
|
46
|
-
permissions:
|
|
47
|
-
contents: write
|
|
48
|
-
packages: write
|
|
49
|
-
pull-requests: write
|
|
50
|
-
id-token: write
|
|
51
|
-
|
|
52
|
-
steps:
|
|
53
|
-
- name: Checkout
|
|
54
|
-
uses: actions/checkout@v4
|
|
55
|
-
with:
|
|
56
|
-
fetch-depth: 0
|
|
57
|
-
|
|
58
|
-
- name: Setup pnpm
|
|
59
|
-
uses: pnpm/action-setup@v4
|
|
60
|
-
|
|
61
|
-
- name: Setup Node.js
|
|
62
|
-
uses: actions/setup-node@v4
|
|
63
|
-
with:
|
|
64
|
-
node-version: "22"
|
|
65
|
-
registry-url: "https://registry.npmjs.org"
|
|
66
|
-
scope: "@aliou"
|
|
67
|
-
cache: "pnpm"
|
|
68
|
-
|
|
69
|
-
- name: Upgrade npm for OIDC support
|
|
70
|
-
run: npm install -g npm@latest
|
|
71
|
-
|
|
72
|
-
- name: Install dependencies
|
|
73
|
-
run: pnpm install --frozen-lockfile
|
|
74
|
-
|
|
75
|
-
- name: Get release info
|
|
76
|
-
id: release-info
|
|
77
|
-
run: |
|
|
78
|
-
pnpm changeset status --output=release.json 2>/dev/null || echo '{"releases":[]}' > release.json
|
|
79
|
-
node <<NODE
|
|
80
|
-
const fs = require('fs');
|
|
81
|
-
const release = JSON.parse(fs.readFileSync('release.json', 'utf8'));
|
|
82
|
-
const releases = release.releases?.filter(r => r.type !== 'none') || [];
|
|
83
|
-
|
|
84
|
-
let title = 'Version Packages';
|
|
85
|
-
let commit = 'Version Packages';
|
|
86
|
-
if (releases.length === 1) {
|
|
87
|
-
const { name, newVersion } = releases[0];
|
|
88
|
-
title = 'Updating ' + name + ' to version ' + newVersion;
|
|
89
|
-
commit = name + '@' + newVersion;
|
|
90
|
-
} else if (releases.length > 1) {
|
|
91
|
-
const summary = releases.map(r => r.name + '@' + r.newVersion).join(', ');
|
|
92
|
-
title = 'Updating ' + summary;
|
|
93
|
-
commit = summary;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'title=' + title + '\n');
|
|
97
|
-
fs.appendFileSync(process.env.GITHUB_OUTPUT, 'commit=' + commit + '\n');
|
|
98
|
-
NODE
|
|
99
|
-
rm -f release.json
|
|
100
|
-
continue-on-error: true
|
|
101
|
-
|
|
102
|
-
- name: Create Release PR or Publish
|
|
103
|
-
id: changesets
|
|
104
|
-
uses: changesets/action@v1
|
|
105
|
-
with:
|
|
106
|
-
version: pnpm changeset version
|
|
107
|
-
publish: pnpm changeset publish
|
|
108
|
-
title: ${{ steps.release-info.outputs.title || 'Version Packages' }}
|
|
109
|
-
commit: ${{ steps.release-info.outputs.commit || 'Version Packages' }}
|
|
110
|
-
env:
|
|
111
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
112
|
-
NPM_CONFIG_PROVENANCE: true
|
|
113
|
-
|
|
114
|
-
- name: Create GitHub releases
|
|
115
|
-
if: steps.changesets.outputs.published == 'true'
|
|
116
|
-
env:
|
|
117
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
118
|
-
PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }}
|
|
119
|
-
run: |
|
|
120
|
-
node <<'NODE'
|
|
121
|
-
const { execSync } = require("node:child_process");
|
|
122
|
-
|
|
123
|
-
const published = JSON.parse(process.env.PUBLISHED_PACKAGES || "[]");
|
|
124
|
-
|
|
125
|
-
for (const pkg of published) {
|
|
126
|
-
const shortName = pkg.name.replace(/^@[^/]+\//, "");
|
|
127
|
-
const tag = `${shortName}@${pkg.version}`;
|
|
128
|
-
|
|
129
|
-
const existing = execSync(`git tag --list ${tag}`, { encoding: "utf8" }).trim();
|
|
130
|
-
if (!existing) {
|
|
131
|
-
execSync(`git tag ${tag}`);
|
|
132
|
-
execSync(`git push origin ${tag}`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
let hasRelease = false;
|
|
136
|
-
try {
|
|
137
|
-
const output = execSync(`gh release view ${tag} --json tagName --jq .tagName`, {
|
|
138
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
139
|
-
}).toString().trim();
|
|
140
|
-
hasRelease = output.length > 0;
|
|
141
|
-
} catch {
|
|
142
|
-
hasRelease = false;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (!hasRelease) {
|
|
146
|
-
execSync(`gh release create ${tag} --title ${tag} --notes "Release ${tag}"`, {
|
|
147
|
-
stdio: "inherit",
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
NODE
|
package/.husky/pre-commit
DELETED