@aliou/pi-synthetic 0.4.5 → 0.4.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/package.json +29 -7
- package/src/commands/quotas.ts +0 -10
- package/src/hooks/search-tool-availability.ts +104 -0
- package/src/index.ts +3 -2
- package/src/tools/search.ts +14 -88
- 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 -84
- package/biome.json +0 -30
- package/shell.nix +0 -10
- package/tsconfig.json +0 -15
package/package.json
CHANGED
|
@@ -1,36 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-synthetic",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"pi"
|
|
11
|
+
],
|
|
4
12
|
"repository": {
|
|
5
13
|
"type": "git",
|
|
6
14
|
"url": "https://github.com/aliou/pi-synthetic"
|
|
7
15
|
},
|
|
8
|
-
"keywords": [
|
|
9
|
-
"pi-package"
|
|
10
|
-
],
|
|
11
16
|
"pi": {
|
|
12
17
|
"extensions": [
|
|
13
18
|
"./src/index.ts"
|
|
14
19
|
],
|
|
15
20
|
"video": "https://assets.aliou.me/pi-extensions/demos/pi-synthetic.mp4"
|
|
16
21
|
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
17
29
|
"peerDependencies": {
|
|
18
|
-
"@mariozechner/pi-coding-agent": ">=0.52.7"
|
|
30
|
+
"@mariozechner/pi-coding-agent": ">=0.52.7",
|
|
31
|
+
"@mariozechner/pi-tui": ">=0.51.0"
|
|
19
32
|
},
|
|
20
33
|
"devDependencies": {
|
|
21
|
-
"@
|
|
34
|
+
"@aliou/biome-plugins": "^0.3.2",
|
|
35
|
+
"@biomejs/biome": "^2.4.2",
|
|
22
36
|
"@changesets/cli": "^2.27.11",
|
|
23
37
|
"@mariozechner/pi-coding-agent": "0.52.7",
|
|
24
|
-
"@mariozechner/pi-tui": "0.52.7",
|
|
25
38
|
"@sinclair/typebox": "^0.34.48",
|
|
26
39
|
"@types/node": "^25.0.10",
|
|
27
40
|
"husky": "^9.1.7",
|
|
28
41
|
"typescript": "^5.9.3"
|
|
29
42
|
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@mariozechner/pi-coding-agent": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@mariozechner/pi-tui": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
30
51
|
"scripts": {
|
|
31
52
|
"typecheck": "tsc --noEmit",
|
|
32
53
|
"lint": "biome check",
|
|
33
54
|
"format": "biome check --write",
|
|
55
|
+
"check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
|
|
34
56
|
"changeset": "changeset",
|
|
35
57
|
"version": "changeset version",
|
|
36
58
|
"release": "pnpm changeset publish"
|
package/src/commands/quotas.ts
CHANGED
|
@@ -9,16 +9,6 @@ export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
|
9
9
|
pi.registerCommand("synthetic:quotas", {
|
|
10
10
|
description: "Display Synthetic API usage quotas",
|
|
11
11
|
handler: async (_args, ctx) => {
|
|
12
|
-
if (!ctx.hasUI) {
|
|
13
|
-
const quotas = await fetchQuotas();
|
|
14
|
-
if (!quotas) {
|
|
15
|
-
console.error("Failed to fetch quotas");
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
console.log(formatQuotasPlain(quotas));
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
12
|
const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
|
|
23
13
|
let currentComponent: Component = new QuotasLoadingComponent(theme);
|
|
24
14
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { SYNTHETIC_WEB_SEARCH_TOOL } from "../tools/search";
|
|
3
|
+
|
|
4
|
+
async function checkSubscriptionAccess(
|
|
5
|
+
apiKey: string,
|
|
6
|
+
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch("https://api.synthetic.new/v2/quotas", {
|
|
9
|
+
method: "GET",
|
|
10
|
+
headers: {
|
|
11
|
+
Authorization: `Bearer ${apiKey}`,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
reason: `Quotas check failed (HTTP ${response.status})`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
if (data?.subscription?.limit > 0) {
|
|
24
|
+
return { ok: true };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
reason: "No active subscription (search requires a subscription plan)",
|
|
30
|
+
};
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const message =
|
|
33
|
+
error instanceof Error ? error.message : "Unknown error occurred";
|
|
34
|
+
return { ok: false, reason: `Quotas check failed: ${message}` };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function registerSyntheticWebSearchHooks(pi: ExtensionAPI): void {
|
|
39
|
+
let accessCheckPromise:
|
|
40
|
+
| Promise<{ ok: true } | { ok: false; reason: string }>
|
|
41
|
+
| undefined;
|
|
42
|
+
let hasAccess = false;
|
|
43
|
+
let deniedReason: string | undefined;
|
|
44
|
+
let didNotifyDenied = false;
|
|
45
|
+
|
|
46
|
+
// Keep tool inactive at session start. Availability is decided before each agent run.
|
|
47
|
+
pi.on("session_start", () => {
|
|
48
|
+
const current = pi.getActiveTools();
|
|
49
|
+
if (current.includes(SYNTHETIC_WEB_SEARCH_TOOL)) {
|
|
50
|
+
pi.setActiveTools(
|
|
51
|
+
current.filter((toolName) => toolName !== SYNTHETIC_WEB_SEARCH_TOOL),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Verify subscription only when user starts agent execution.
|
|
57
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
58
|
+
const apiKey = process.env.SYNTHETIC_API_KEY;
|
|
59
|
+
if (!apiKey) {
|
|
60
|
+
hasAccess = false;
|
|
61
|
+
deniedReason = "SYNTHETIC_API_KEY is not configured";
|
|
62
|
+
accessCheckPromise = undefined;
|
|
63
|
+
} else {
|
|
64
|
+
if (deniedReason === "SYNTHETIC_API_KEY is not configured") {
|
|
65
|
+
deniedReason = undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!hasAccess && !deniedReason) {
|
|
69
|
+
accessCheckPromise ??= checkSubscriptionAccess(apiKey);
|
|
70
|
+
const access = await accessCheckPromise;
|
|
71
|
+
|
|
72
|
+
if (!access.ok) {
|
|
73
|
+
deniedReason = access.reason;
|
|
74
|
+
} else {
|
|
75
|
+
hasAccess = true;
|
|
76
|
+
didNotifyDenied = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (deniedReason) {
|
|
82
|
+
const current = pi.getActiveTools();
|
|
83
|
+
if (current.includes(SYNTHETIC_WEB_SEARCH_TOOL)) {
|
|
84
|
+
pi.setActiveTools(
|
|
85
|
+
current.filter((toolName) => toolName !== SYNTHETIC_WEB_SEARCH_TOOL),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (ctx.hasUI && !didNotifyDenied) {
|
|
90
|
+
ctx.ui.notify(
|
|
91
|
+
`Synthetic web search disabled: ${deniedReason}`,
|
|
92
|
+
"warning",
|
|
93
|
+
);
|
|
94
|
+
didNotifyDenied = true;
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const current = pi.getActiveTools();
|
|
100
|
+
if (!current.includes(SYNTHETIC_WEB_SEARCH_TOOL)) {
|
|
101
|
+
pi.setActiveTools([...current, SYNTHETIC_WEB_SEARCH_TOOL]);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { registerQuotasCommand } from "./commands/quotas";
|
|
3
|
+
import { registerSyntheticWebSearchHooks } from "./hooks/search-tool-availability";
|
|
3
4
|
import { registerSyntheticProvider } from "./providers/index";
|
|
4
5
|
import { registerSyntheticWebSearchTool } from "./tools/search";
|
|
5
6
|
|
|
6
7
|
export default async function (pi: ExtensionAPI) {
|
|
7
8
|
registerSyntheticProvider(pi);
|
|
9
|
+
registerSyntheticWebSearchTool(pi);
|
|
10
|
+
registerSyntheticWebSearchHooks(pi);
|
|
8
11
|
|
|
9
|
-
// Only register quotas command and web search tool if API key is available
|
|
10
12
|
if (process.env.SYNTHETIC_API_KEY) {
|
|
11
13
|
registerQuotasCommand(pi);
|
|
12
|
-
registerSyntheticWebSearchTool(pi);
|
|
13
14
|
}
|
|
14
15
|
}
|
package/src/tools/search.ts
CHANGED
|
@@ -8,7 +8,8 @@ import type {
|
|
|
8
8
|
import { Text } from "@mariozechner/pi-tui";
|
|
9
9
|
import { type Static, Type } from "@sinclair/typebox";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
export const SYNTHETIC_WEB_SEARCH_TOOL = "synthetic_web_search" as const;
|
|
12
|
+
|
|
12
13
|
interface SyntheticSearchResult {
|
|
13
14
|
url: string;
|
|
14
15
|
title: string;
|
|
@@ -27,7 +28,6 @@ interface WebSearchDetails {
|
|
|
27
28
|
isError?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
// Schema
|
|
31
31
|
const SearchParams = Type.Object({
|
|
32
32
|
query: Type.String({
|
|
33
33
|
description: "The search query. Be specific for best results.",
|
|
@@ -36,80 +36,9 @@ const SearchParams = Type.Object({
|
|
|
36
36
|
|
|
37
37
|
type SearchParamsType = Static<typeof SearchParams>;
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
// Returns "ok" if the user has access, or an error message if not.
|
|
41
|
-
async function checkSubscriptionAccess(
|
|
42
|
-
apiKey: string,
|
|
43
|
-
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
44
|
-
try {
|
|
45
|
-
const response = await fetch("https://api.synthetic.new/v2/quotas", {
|
|
46
|
-
method: "GET",
|
|
47
|
-
headers: {
|
|
48
|
-
Authorization: `Bearer ${apiKey}`,
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
return {
|
|
54
|
-
ok: false,
|
|
55
|
-
reason: `Quotas check failed (HTTP ${response.status})`,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const data = await response.json();
|
|
60
|
-
if (data?.subscription?.limit > 0) {
|
|
61
|
-
return { ok: true };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
ok: false,
|
|
66
|
-
reason: "No active subscription (search requires a subscription plan)",
|
|
67
|
-
};
|
|
68
|
-
} catch (error) {
|
|
69
|
-
const message =
|
|
70
|
-
error instanceof Error ? error.message : "Unknown error occurred";
|
|
71
|
-
return { ok: false, reason: `Quotas check failed: ${message}` };
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Tool Registration
|
|
76
|
-
export function registerSyntheticWebSearchTool(pi: ExtensionAPI) {
|
|
77
|
-
const apiKey = process.env.SYNTHETIC_API_KEY;
|
|
78
|
-
if (!apiKey) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Register tool immediately so it's available when tools are collected
|
|
83
|
-
registerTool(pi, apiKey);
|
|
84
|
-
|
|
85
|
-
// On session start, remove tool from active set, check subscription, re-add if valid
|
|
86
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
87
|
-
// Disable tool until subscription is verified
|
|
88
|
-
const activeTools = pi.getActiveTools();
|
|
89
|
-
pi.setActiveTools(activeTools.filter((t) => t !== "synthetic_web_search"));
|
|
90
|
-
|
|
91
|
-
const access = await checkSubscriptionAccess(apiKey);
|
|
92
|
-
if (!access.ok) {
|
|
93
|
-
if (ctx.hasUI) {
|
|
94
|
-
ctx.ui.notify(
|
|
95
|
-
`Synthetic web search disabled: ${access.reason}`,
|
|
96
|
-
"warning",
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Add tool back to active tools
|
|
103
|
-
const current = pi.getActiveTools();
|
|
104
|
-
if (!current.includes("synthetic_web_search")) {
|
|
105
|
-
pi.setActiveTools([...current, "synthetic_web_search"]);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function registerTool(pi: ExtensionAPI, apiKey: string) {
|
|
39
|
+
export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
111
40
|
pi.registerTool<typeof SearchParams, WebSearchDetails>({
|
|
112
|
-
name:
|
|
41
|
+
name: SYNTHETIC_WEB_SEARCH_TOOL,
|
|
113
42
|
label: "Synthetic: Web Search",
|
|
114
43
|
description:
|
|
115
44
|
"Search the web using Synthetic's zero-data-retention API. Returns search results with titles, URLs, content snippets, and publication dates. Use for finding documentation, articles, recent information, or any web content. Results are fresh and not cached by Synthetic.",
|
|
@@ -124,14 +53,21 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
|
|
|
124
53
|
| undefined,
|
|
125
54
|
_ctx: ExtensionContext,
|
|
126
55
|
): Promise<AgentToolResult<WebSearchDetails>> {
|
|
127
|
-
// Send progress update
|
|
128
56
|
onUpdate?.({
|
|
129
57
|
content: [{ type: "text", text: "Searching..." }],
|
|
130
58
|
details: { query: params.query },
|
|
131
59
|
});
|
|
132
60
|
|
|
133
61
|
try {
|
|
134
|
-
|
|
62
|
+
const apiKey = process.env.SYNTHETIC_API_KEY;
|
|
63
|
+
if (!apiKey) {
|
|
64
|
+
const error = "SYNTHETIC_API_KEY is not configured";
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: `Error: ${error}` }],
|
|
67
|
+
details: { error, isError: true },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
135
71
|
const response = await fetch("https://api.synthetic.new/v2/search", {
|
|
136
72
|
method: "POST",
|
|
137
73
|
headers: {
|
|
@@ -151,7 +87,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
|
|
|
151
87
|
};
|
|
152
88
|
}
|
|
153
89
|
|
|
154
|
-
// Parse response
|
|
155
90
|
let data: SyntheticSearchResponse;
|
|
156
91
|
try {
|
|
157
92
|
data = await response.json();
|
|
@@ -166,7 +101,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
|
|
|
166
101
|
};
|
|
167
102
|
}
|
|
168
103
|
|
|
169
|
-
// Format results for LLM
|
|
170
104
|
let content = `Found ${data.results.length} result(s):\n\n`;
|
|
171
105
|
for (const result of data.results) {
|
|
172
106
|
content += `## ${result.title}\n`;
|
|
@@ -184,7 +118,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
|
|
|
184
118
|
},
|
|
185
119
|
};
|
|
186
120
|
} catch (error) {
|
|
187
|
-
// Handle abort signal
|
|
188
121
|
if (error instanceof Error && error.name === "AbortError") {
|
|
189
122
|
return {
|
|
190
123
|
content: [{ type: "text", text: "Search cancelled" }],
|
|
@@ -192,7 +125,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
|
|
|
192
125
|
};
|
|
193
126
|
}
|
|
194
127
|
|
|
195
|
-
// Handle other errors
|
|
196
128
|
const message =
|
|
197
129
|
error instanceof Error ? error.message : "Unknown error occurred";
|
|
198
130
|
return {
|
|
@@ -215,7 +147,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
|
|
|
215
147
|
): Text {
|
|
216
148
|
const { expanded, isPartial } = options;
|
|
217
149
|
|
|
218
|
-
// Handle partial/loading state
|
|
219
150
|
if (isPartial) {
|
|
220
151
|
const text =
|
|
221
152
|
result.content?.[0]?.type === "text"
|
|
@@ -225,8 +156,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
|
|
|
225
156
|
}
|
|
226
157
|
|
|
227
158
|
const details = result.details;
|
|
228
|
-
|
|
229
|
-
// Handle error state
|
|
230
159
|
if (details?.isError) {
|
|
231
160
|
const errorMsg =
|
|
232
161
|
result.content?.[0]?.type === "text"
|
|
@@ -235,21 +164,18 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
|
|
|
235
164
|
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
236
165
|
}
|
|
237
166
|
|
|
238
|
-
// Handle success state
|
|
239
167
|
const results = details?.results || [];
|
|
240
168
|
let text = theme.fg("success", `✓ Found ${results.length} result(s)`);
|
|
241
169
|
|
|
242
|
-
// Collapsed view
|
|
243
170
|
if (!expanded && results.length > 0) {
|
|
244
171
|
const first = results[0];
|
|
245
172
|
text += `\n ${theme.fg("dim", `${first.title}`)}`;
|
|
246
173
|
if (results.length > 1) {
|
|
247
174
|
text += theme.fg("dim", ` (${results.length - 1} more)`);
|
|
248
175
|
}
|
|
249
|
-
text += theme.fg("muted",
|
|
176
|
+
text += theme.fg("muted", " [Ctrl+O to expand]");
|
|
250
177
|
}
|
|
251
178
|
|
|
252
|
-
// Expanded view
|
|
253
179
|
if (expanded) {
|
|
254
180
|
for (const r of results) {
|
|
255
181
|
text += `\n\n${theme.fg("accent", theme.bold(r.title))}`;
|
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
package/AGENTS.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# pi-synthetic
|
|
2
|
-
|
|
3
|
-
Public Pi extension providing open-source language models via Synthetic's API. People could be using this, so consider backwards compatibility when making changes.
|
|
4
|
-
|
|
5
|
-
Pi is pre-1.0.0, so breaking changes can happen between Pi versions. This extension must stay up to date with Pi or things will break.
|
|
6
|
-
|
|
7
|
-
## Stack
|
|
8
|
-
|
|
9
|
-
- TypeScript (strict mode)
|
|
10
|
-
- pnpm 10.26.1
|
|
11
|
-
- Biome for linting/formatting
|
|
12
|
-
- Changesets for versioning
|
|
13
|
-
|
|
14
|
-
## Scripts
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
pnpm typecheck # Type check
|
|
18
|
-
pnpm lint # Lint (runs on pre-commit)
|
|
19
|
-
pnpm format # Format
|
|
20
|
-
pnpm changeset # Create changeset for versioning
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Structure
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
src/
|
|
27
|
-
index.ts # Extension entry, registers provider
|
|
28
|
-
providers/
|
|
29
|
-
index.ts # Provider registration
|
|
30
|
-
models.ts # Hardcoded model definitions
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Conventions
|
|
34
|
-
|
|
35
|
-
- API key comes from environment (`SYNTHETIC_API_KEY`)
|
|
36
|
-
- Uses OpenAI-compatible API at `https://api.synthetic.new/openai/v1`
|
|
37
|
-
- Models are hardcoded in `src/providers/models.ts`
|
|
38
|
-
- Update model list when Synthetic adds new models
|
|
39
|
-
|
|
40
|
-
## Adding Models
|
|
41
|
-
|
|
42
|
-
Edit `src/providers/models.ts`:
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
{
|
|
46
|
-
id: "hf:vendor/model-name",
|
|
47
|
-
name: "vendor/model-name",
|
|
48
|
-
reasoning: true/false,
|
|
49
|
-
input: ["text"] or ["text", "image"],
|
|
50
|
-
cost: {
|
|
51
|
-
input: 0.55, // $ per million tokens
|
|
52
|
-
output: 2.19,
|
|
53
|
-
cacheRead: 0.55,
|
|
54
|
-
cacheWrite: 0
|
|
55
|
-
},
|
|
56
|
-
contextWindow: 202752,
|
|
57
|
-
maxTokens: 65536
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Get pricing from `https://api.synthetic.new/openai/v1/models`.
|
|
62
|
-
|
|
63
|
-
## Versioning
|
|
64
|
-
|
|
65
|
-
Uses changesets. Run `pnpm changeset` before committing user-facing changes.
|
|
66
|
-
|
|
67
|
-
- `patch`: bug fixes, model updates
|
|
68
|
-
- `minor`: new models, features
|
|
69
|
-
- `major`: breaking changes
|
package/CHANGELOG.md
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# @aliou/pi-synthetic
|
|
2
|
-
|
|
3
|
-
## 0.4.5
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- 7489bc0: update model list: add nvidia/Kimi-K2.5-NVFP4, remove 6 discontinued models
|
|
8
|
-
|
|
9
|
-
## 0.4.4
|
|
10
|
-
|
|
11
|
-
### Patch Changes
|
|
12
|
-
|
|
13
|
-
- 86a3145: Fix quotas command showing duplicate notification in TUI mode
|
|
14
|
-
- f94cc6b: fix: register search tool at init time so it's available when pi collects tools
|
|
15
|
-
|
|
16
|
-
## 0.4.3
|
|
17
|
-
|
|
18
|
-
### Patch Changes
|
|
19
|
-
|
|
20
|
-
- 7dc1d80: Defer subscription check to session_start for non-blocking extension init.
|
|
21
|
-
|
|
22
|
-
## 0.4.2
|
|
23
|
-
|
|
24
|
-
### Patch Changes
|
|
25
|
-
|
|
26
|
-
- d9af905: Add demo video URL for the Pi package browser.
|
|
27
|
-
|
|
28
|
-
## 0.4.1
|
|
29
|
-
|
|
30
|
-
### Patch Changes
|
|
31
|
-
|
|
32
|
-
- aba3bb8: fix: use correct /v2/quotas endpoint for subscription access check
|
|
33
|
-
|
|
34
|
-
## 0.4.0
|
|
35
|
-
|
|
36
|
-
### Minor Changes
|
|
37
|
-
|
|
38
|
-
- 5cca252: Add `/synthetic:quotas` command to display API usage quotas
|
|
39
|
-
|
|
40
|
-
A new slash command that shows your Synthetic API subscription quotas in a rich terminal UI:
|
|
41
|
-
|
|
42
|
-
- Visual usage bar with color-coded severity (green/yellow/red based on usage)
|
|
43
|
-
- Aligned columns showing limit, used, and remaining requests
|
|
44
|
-
- ISO8601 renewal timestamp with relative time formatting (e.g., "in 5 hours")
|
|
45
|
-
- Closes on any key press
|
|
46
|
-
|
|
47
|
-
The command is only registered when `SYNTHETIC_API_KEY` environment variable is set.
|
|
48
|
-
|
|
49
|
-
- a8cacfb: Add Synthetic web search tool
|
|
50
|
-
|
|
51
|
-
New tool `synthetic_web_search` allows agents to search the web using Synthetic's zero-data-retention API. Returns search results with titles, URLs, content snippets, and publication dates.
|
|
52
|
-
|
|
53
|
-
**Note:** Search is a subscription-only feature. The tool will only be registered if the `SYNTHETIC_API_KEY` belongs to an active subscription (verified via the usage endpoint).
|
|
54
|
-
|
|
55
|
-
## 0.3.0
|
|
56
|
-
|
|
57
|
-
### Minor Changes
|
|
58
|
-
|
|
59
|
-
- 5f67daf: Switch from Anthropic to OpenAI API endpoints
|
|
60
|
-
|
|
61
|
-
- Change API endpoint from `/anthropic` to `/openai/v1`
|
|
62
|
-
- Update from `anthropic-messages` to `openai-completions` API
|
|
63
|
-
- Add compatibility flags for proper role handling (`supportsDeveloperRole: false`)
|
|
64
|
-
- Use standard `max_tokens` field instead of `max_completion_tokens`
|
|
65
|
-
|
|
66
|
-
## 0.2.0
|
|
67
|
-
|
|
68
|
-
### Minor Changes
|
|
69
|
-
|
|
70
|
-
- 58d21ca: Fix model configurations from Synthetic API
|
|
71
|
-
|
|
72
|
-
- Update maxTokens for all Synthetic models using values from models.dev (synthetic provider)
|
|
73
|
-
- Fix Kimi-K2-Instruct-0905 reasoning flag to false
|
|
74
|
-
|
|
75
|
-
## 0.1.0
|
|
76
|
-
|
|
77
|
-
### Minor Changes
|
|
78
|
-
|
|
79
|
-
- 4a32d18: Initial release with 19 open-source models
|
|
80
|
-
|
|
81
|
-
- Add Synthetic provider with Anthropic-compatible API
|
|
82
|
-
- Support for DeepSeek, Qwen, MiniMax, Kimi, Llama, GLM models
|
|
83
|
-
- Vision and reasoning capabilities where available
|
|
84
|
-
- Hardcoded model definitions with per-token pricing
|
package/biome.json
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
|
|
3
|
-
"vcs": {
|
|
4
|
-
"enabled": true,
|
|
5
|
-
"clientKind": "git",
|
|
6
|
-
"useIgnoreFile": true
|
|
7
|
-
},
|
|
8
|
-
"files": {
|
|
9
|
-
"includes": ["**/*.ts", "**/*.json"],
|
|
10
|
-
"ignoreUnknown": true
|
|
11
|
-
},
|
|
12
|
-
"assist": {
|
|
13
|
-
"actions": {
|
|
14
|
-
"source": {
|
|
15
|
-
"organizeImports": "on"
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"linter": {
|
|
20
|
-
"enabled": true,
|
|
21
|
-
"rules": {
|
|
22
|
-
"recommended": true
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
"formatter": {
|
|
26
|
-
"enabled": true,
|
|
27
|
-
"indentStyle": "space",
|
|
28
|
-
"indentWidth": 2
|
|
29
|
-
}
|
|
30
|
-
}
|
package/shell.nix
DELETED
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"forceConsistentCasingInFileNames": true,
|
|
10
|
-
"resolveJsonModule": true,
|
|
11
|
-
"noEmit": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*"],
|
|
14
|
-
"exclude": ["node_modules"]
|
|
15
|
-
}
|