hanzi-browse 2.2.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.
Files changed (78) hide show
  1. package/README.md +182 -0
  2. package/dist/agent/loop.d.ts +63 -0
  3. package/dist/agent/loop.js +186 -0
  4. package/dist/agent/system-prompt.d.ts +7 -0
  5. package/dist/agent/system-prompt.js +41 -0
  6. package/dist/agent/tools.d.ts +9 -0
  7. package/dist/agent/tools.js +154 -0
  8. package/dist/cli/detect-credentials.d.ts +31 -0
  9. package/dist/cli/detect-credentials.js +44 -0
  10. package/dist/cli/import-credentials-handler.d.ts +14 -0
  11. package/dist/cli/import-credentials-handler.js +22 -0
  12. package/dist/cli/session-files.d.ts +28 -0
  13. package/dist/cli/session-files.js +118 -0
  14. package/dist/cli/setup.d.ts +10 -0
  15. package/dist/cli/setup.js +915 -0
  16. package/dist/cli.d.ts +16 -0
  17. package/dist/cli.js +506 -0
  18. package/dist/dashboard/assets/index-CEFyesbT.js +46 -0
  19. package/dist/dashboard/assets/index-Dnht2kLU.css +1 -0
  20. package/dist/dashboard/index.html +13 -0
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +1116 -0
  23. package/dist/ipc/index.d.ts +8 -0
  24. package/dist/ipc/index.js +8 -0
  25. package/dist/ipc/native-host.d.ts +96 -0
  26. package/dist/ipc/native-host.js +223 -0
  27. package/dist/ipc/websocket-client.d.ts +73 -0
  28. package/dist/ipc/websocket-client.js +199 -0
  29. package/dist/license/manager.d.ts +20 -0
  30. package/dist/license/manager.js +15 -0
  31. package/dist/llm/client.d.ts +72 -0
  32. package/dist/llm/client.js +227 -0
  33. package/dist/llm/credentials.d.ts +61 -0
  34. package/dist/llm/credentials.js +200 -0
  35. package/dist/llm/vertex.d.ts +22 -0
  36. package/dist/llm/vertex.js +335 -0
  37. package/dist/managed/api-http.test.d.ts +7 -0
  38. package/dist/managed/api-http.test.js +623 -0
  39. package/dist/managed/api.d.ts +51 -0
  40. package/dist/managed/api.js +1448 -0
  41. package/dist/managed/api.test.d.ts +10 -0
  42. package/dist/managed/api.test.js +146 -0
  43. package/dist/managed/auth.d.ts +38 -0
  44. package/dist/managed/auth.js +192 -0
  45. package/dist/managed/billing.d.ts +70 -0
  46. package/dist/managed/billing.js +227 -0
  47. package/dist/managed/deploy.d.ts +17 -0
  48. package/dist/managed/deploy.js +385 -0
  49. package/dist/managed/e2e.test.d.ts +15 -0
  50. package/dist/managed/e2e.test.js +151 -0
  51. package/dist/managed/hardening.test.d.ts +14 -0
  52. package/dist/managed/hardening.test.js +346 -0
  53. package/dist/managed/integration.test.d.ts +8 -0
  54. package/dist/managed/integration.test.js +274 -0
  55. package/dist/managed/log.d.ts +18 -0
  56. package/dist/managed/log.js +31 -0
  57. package/dist/managed/server.d.ts +12 -0
  58. package/dist/managed/server.js +69 -0
  59. package/dist/managed/store-pg.d.ts +191 -0
  60. package/dist/managed/store-pg.js +479 -0
  61. package/dist/managed/store.d.ts +188 -0
  62. package/dist/managed/store.js +379 -0
  63. package/dist/relay/auto-start.d.ts +19 -0
  64. package/dist/relay/auto-start.js +71 -0
  65. package/dist/relay/server.d.ts +17 -0
  66. package/dist/relay/server.js +403 -0
  67. package/dist/types/index.d.ts +5 -0
  68. package/dist/types/index.js +4 -0
  69. package/dist/types/session.d.ts +134 -0
  70. package/dist/types/session.js +16 -0
  71. package/package.json +61 -0
  72. package/skills/README.md +48 -0
  73. package/skills/a11y-auditor/SKILL.md +42 -0
  74. package/skills/e2e-tester/SKILL.md +154 -0
  75. package/skills/hanzi-browse/SKILL.md +182 -0
  76. package/skills/linkedin-prospector/SKILL.md +149 -0
  77. package/skills/social-poster/SKILL.md +146 -0
  78. package/skills/x-marketer/SKILL.md +479 -0
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Vertex AI Gemini Provider for Server-Side LLM Client
3
+ *
4
+ * Handles:
5
+ * - Service account JWT → OAuth token exchange
6
+ * - Gemini API request/response format
7
+ * - Streaming SSE parsing
8
+ * - Anthropic ↔ Gemini format conversion (canonical internal format is Anthropic)
9
+ *
10
+ * The server uses Anthropic's content block format internally.
11
+ * This module converts to/from Gemini's format at the API boundary.
12
+ */
13
+ import { createSign } from "crypto";
14
+ import { readFileSync } from "fs";
15
+ let vertexConfig = null;
16
+ let cachedToken = null;
17
+ let cachedTokenExpiry = 0;
18
+ /**
19
+ * Initialize Vertex AI with a service account JSON file path or object.
20
+ */
21
+ export function initVertex(serviceAccountPathOrJson, region = "us-central1") {
22
+ const sa = typeof serviceAccountPathOrJson === "string"
23
+ ? JSON.parse(readFileSync(serviceAccountPathOrJson, "utf8"))
24
+ : serviceAccountPathOrJson;
25
+ if (!sa.project_id || !sa.private_key || !sa.client_email) {
26
+ throw new Error("Invalid Vertex AI service account: missing project_id, private_key, or client_email");
27
+ }
28
+ vertexConfig = {
29
+ projectId: sa.project_id,
30
+ region,
31
+ serviceAccountJson: sa,
32
+ };
33
+ cachedToken = null;
34
+ cachedTokenExpiry = 0;
35
+ console.error(`[Vertex] Initialized: project=${sa.project_id} region=${region}`);
36
+ }
37
+ /**
38
+ * Check if Vertex AI is configured.
39
+ */
40
+ export function isVertexConfigured() {
41
+ return vertexConfig !== null;
42
+ }
43
+ // --- Auth ---
44
+ async function getAccessToken() {
45
+ if (cachedToken && Date.now() < cachedTokenExpiry - 5 * 60 * 1000) {
46
+ return cachedToken;
47
+ }
48
+ const sa = vertexConfig.serviceAccountJson;
49
+ const now = Math.floor(Date.now() / 1000);
50
+ const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
51
+ const payload = Buffer.from(JSON.stringify({
52
+ iss: sa.client_email,
53
+ scope: "https://www.googleapis.com/auth/cloud-platform",
54
+ aud: "https://oauth2.googleapis.com/token",
55
+ iat: now,
56
+ exp: now + 3600,
57
+ })).toString("base64url");
58
+ const sign = createSign("RSA-SHA256");
59
+ sign.update(`${header}.${payload}`);
60
+ const signature = sign.sign(sa.private_key, "base64url");
61
+ const res = await fetch("https://oauth2.googleapis.com/token", {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
64
+ body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${header}.${payload}.${signature}`,
65
+ });
66
+ if (!res.ok) {
67
+ const err = await res.text();
68
+ throw new Error(`Vertex AI token exchange failed: ${res.status} ${err}`);
69
+ }
70
+ const data = await res.json();
71
+ cachedToken = data.access_token;
72
+ cachedTokenExpiry = Date.now() + data.expires_in * 1000;
73
+ return cachedToken;
74
+ }
75
+ // --- Format Conversion ---
76
+ /**
77
+ * Convert Anthropic-format messages to Gemini format.
78
+ */
79
+ function convertMessages(messages) {
80
+ const geminiMessages = [];
81
+ const toolUseIdToName = {};
82
+ for (const msg of messages) {
83
+ const role = msg.role === "assistant" ? "model" : "user";
84
+ const parts = [];
85
+ if (typeof msg.content === "string") {
86
+ parts.push({ text: msg.content });
87
+ }
88
+ else if (Array.isArray(msg.content)) {
89
+ for (const block of msg.content) {
90
+ if (block.type === "text") {
91
+ parts.push({ text: block.text });
92
+ }
93
+ else if (block.type === "image") {
94
+ const img = block;
95
+ parts.push({
96
+ inlineData: {
97
+ mimeType: img.source.media_type || "image/jpeg",
98
+ data: img.source.data,
99
+ },
100
+ });
101
+ }
102
+ else if (block.type === "tool_use") {
103
+ const tu = block;
104
+ toolUseIdToName[tu.id] = tu.name;
105
+ parts.push({
106
+ functionCall: { name: tu.name, args: tu.input },
107
+ });
108
+ }
109
+ else if (block.type === "tool_result") {
110
+ const tr = block;
111
+ let responseText = tr.content;
112
+ if (Array.isArray(tr.content)) {
113
+ const textParts = [];
114
+ for (const c of tr.content) {
115
+ if (c.type === "text") {
116
+ textParts.push(c.text);
117
+ }
118
+ else if (c.type === "image" && c.source?.data) {
119
+ parts.push({
120
+ inlineData: {
121
+ mimeType: c.source.media_type || "image/jpeg",
122
+ data: c.source.data,
123
+ },
124
+ });
125
+ }
126
+ }
127
+ responseText = textParts.join("\n");
128
+ }
129
+ const functionName = toolUseIdToName[tr.tool_use_id] || "unknown";
130
+ parts.push({
131
+ functionResponse: {
132
+ name: functionName,
133
+ response: { result: responseText },
134
+ },
135
+ });
136
+ }
137
+ }
138
+ }
139
+ if (parts.length > 0) {
140
+ geminiMessages.push({ role, parts });
141
+ }
142
+ }
143
+ return geminiMessages;
144
+ }
145
+ /**
146
+ * Convert Anthropic-format tools to Gemini format.
147
+ */
148
+ function convertTools(tools) {
149
+ if (!tools || tools.length === 0)
150
+ return [];
151
+ return [
152
+ {
153
+ functionDeclarations: tools.map((tool) => ({
154
+ name: tool.name,
155
+ description: tool.description,
156
+ parameters: sanitizeSchema(tool.input_schema),
157
+ })),
158
+ },
159
+ ];
160
+ }
161
+ /**
162
+ * Sanitize JSON Schema for Gemini (stricter than OpenAPI).
163
+ */
164
+ function sanitizeSchema(schema) {
165
+ if (!schema || typeof schema !== "object")
166
+ return schema;
167
+ const cleaned = {};
168
+ if (schema.type) {
169
+ cleaned.type = Array.isArray(schema.type) ? schema.type[0] || "string" : schema.type;
170
+ }
171
+ if (schema.description)
172
+ cleaned.description = schema.description;
173
+ if (schema.enum)
174
+ cleaned.enum = schema.enum;
175
+ if (schema.required)
176
+ cleaned.required = schema.required;
177
+ if (schema.properties) {
178
+ cleaned.properties = {};
179
+ for (const [key, value] of Object.entries(schema.properties)) {
180
+ cleaned.properties[key] = sanitizeSchema(value);
181
+ }
182
+ }
183
+ if (schema.items)
184
+ cleaned.items = sanitizeSchema(schema.items);
185
+ if (schema.oneOf || schema.anyOf) {
186
+ const options = schema.oneOf || schema.anyOf;
187
+ if (Array.isArray(options) && options.length > 0) {
188
+ return sanitizeSchema(options[0]);
189
+ }
190
+ }
191
+ return cleaned;
192
+ }
193
+ // --- Streaming ---
194
+ /**
195
+ * Parse Gemini SSE stream into Anthropic-format LLMResponse.
196
+ */
197
+ async function parseGeminiStream(response, onText, signal) {
198
+ const reader = response.body.getReader();
199
+ const decoder = new TextDecoder();
200
+ let buffer = "";
201
+ const content = [];
202
+ let currentText = "";
203
+ const toolCalls = [];
204
+ let stopReason = "end_turn";
205
+ let usage = { input_tokens: 0, output_tokens: 0 };
206
+ try {
207
+ while (true) {
208
+ if (signal?.aborted) {
209
+ reader.cancel();
210
+ throw new DOMException("Aborted", "AbortError");
211
+ }
212
+ const { done, value } = await reader.read();
213
+ if (done)
214
+ break;
215
+ buffer += decoder.decode(value, { stream: true });
216
+ const lines = buffer.split("\n");
217
+ buffer = lines.pop();
218
+ for (const line of lines) {
219
+ if (!line.startsWith("data: "))
220
+ continue;
221
+ const data = line.slice(6);
222
+ let chunk;
223
+ try {
224
+ chunk = JSON.parse(data);
225
+ }
226
+ catch {
227
+ continue;
228
+ }
229
+ const candidate = chunk.candidates?.[0];
230
+ if (!candidate)
231
+ continue;
232
+ const parts = candidate.content?.parts || [];
233
+ for (const part of parts) {
234
+ if (part.text) {
235
+ currentText += part.text;
236
+ onText?.(part.text);
237
+ }
238
+ if (part.functionCall) {
239
+ toolCalls.push({
240
+ id: part.functionCall.id || `call_${Date.now()}_${toolCalls.length}`,
241
+ name: part.functionCall.name,
242
+ input: part.functionCall.args || {},
243
+ });
244
+ }
245
+ }
246
+ if (candidate.finishReason === "MAX_TOKENS") {
247
+ stopReason = "max_tokens";
248
+ }
249
+ // Usage metadata
250
+ if (chunk.usageMetadata) {
251
+ usage.input_tokens = chunk.usageMetadata.promptTokenCount || 0;
252
+ usage.output_tokens = chunk.usageMetadata.candidatesTokenCount || 0;
253
+ }
254
+ }
255
+ }
256
+ }
257
+ finally {
258
+ reader.releaseLock();
259
+ }
260
+ // Build content in Anthropic format
261
+ if (currentText) {
262
+ content.push({ type: "text", text: currentText });
263
+ }
264
+ for (const tc of toolCalls) {
265
+ content.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.input });
266
+ }
267
+ if (content.length === 0) {
268
+ content.push({ type: "text", text: "" });
269
+ }
270
+ // If there are tool calls, stop reason is tool_use
271
+ if (toolCalls.length > 0) {
272
+ stopReason = "tool_use";
273
+ }
274
+ return { content, stop_reason: stopReason, usage };
275
+ }
276
+ // --- Main Call ---
277
+ /**
278
+ * Call Vertex AI Gemini. Returns Anthropic-format LLMResponse.
279
+ *
280
+ * Drop-in replacement for the Anthropic callLLM — same params, same response format.
281
+ */
282
+ const MAX_RETRIES = 5;
283
+ export async function callVertexLLM(params) {
284
+ if (!vertexConfig) {
285
+ throw new Error("Vertex AI not initialized. Call initVertex() first.");
286
+ }
287
+ const { messages, system, tools, model = "gemini-2.5-flash", maxTokens = 16384, signal, onText, } = params;
288
+ const { projectId } = vertexConfig;
289
+ // Use global endpoint — Google routes to whichever region has capacity,
290
+ // reducing 429s compared to pinning to a single region.
291
+ const url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/global/publishers/google/models/${model}:streamGenerateContent?alt=sse`;
292
+ const systemText = system.map((s) => s.text).join("\n\n");
293
+ const body = JSON.stringify({
294
+ contents: convertMessages(messages),
295
+ tools: convertTools(tools),
296
+ tool_config: {
297
+ function_calling_config: { mode: "AUTO" },
298
+ },
299
+ generationConfig: {
300
+ maxOutputTokens: maxTokens,
301
+ },
302
+ systemInstruction: { parts: [{ text: systemText }] },
303
+ });
304
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
305
+ if (signal?.aborted)
306
+ throw new DOMException("Aborted", "AbortError");
307
+ const token = await getAccessToken();
308
+ const response = await fetch(url, {
309
+ method: "POST",
310
+ headers: {
311
+ "Content-Type": "application/json",
312
+ Authorization: `Bearer ${token}`,
313
+ },
314
+ body,
315
+ signal,
316
+ });
317
+ if (response.status === 429 && attempt < MAX_RETRIES) {
318
+ const retryAfter = response.headers.get("retry-after");
319
+ const delay = retryAfter
320
+ ? parseInt(retryAfter, 10) * 1000
321
+ : Math.min(1000 * Math.pow(2, attempt), 30000) + Math.random() * 1000;
322
+ console.error(`[Vertex] 429 rate limited, retry ${attempt + 1}/${MAX_RETRIES} in ${Math.round(delay)}ms`);
323
+ await new Promise((r) => setTimeout(r, delay));
324
+ continue;
325
+ }
326
+ if (!response.ok) {
327
+ const errorText = await response.text().catch(() => "");
328
+ throw new Error(`Vertex AI error ${response.status}: ${errorText.slice(0, 300)}`);
329
+ }
330
+ const result = await parseGeminiStream(response, onText, signal);
331
+ result.model = model;
332
+ return result;
333
+ }
334
+ throw new Error("Vertex AI: max retries exceeded (429 rate limit)");
335
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * HTTP-level API tests
3
+ *
4
+ * Starts the actual server and hits endpoints.
5
+ * Tests auth enforcement, workspace isolation, session validation.
6
+ */
7
+ export {};