@getjack/jack 0.1.23 → 0.1.25

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.
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Vectorize index creation logic for jack MCP tools
3
+ *
4
+ * Uses wrangler CLI to create Vectorize indexes.
5
+ */
6
+
7
+ import { join } from "node:path";
8
+ import { $ } from "bun";
9
+ import { getProjectNameFromDir } from "../storage/index.ts";
10
+ import { addVectorizeBinding, getExistingVectorizeBindings } from "./vectorize-config.ts";
11
+
12
+ export type VectorizeMetric = "cosine" | "euclidean" | "dot-product";
13
+
14
+ export interface CreateVectorizeOptions {
15
+ name?: string;
16
+ dimensions?: number;
17
+ metric?: VectorizeMetric;
18
+ }
19
+
20
+ export interface CreateVectorizeResult {
21
+ indexName: string;
22
+ bindingName: string;
23
+ dimensions: number;
24
+ metric: VectorizeMetric;
25
+ created: boolean; // false if reused existing
26
+ }
27
+
28
+ /**
29
+ * Convert an index name to SCREAMING_SNAKE_CASE for the binding name.
30
+ * Special case: first index in a project gets "VECTORS" as the binding.
31
+ */
32
+ function toBindingName(indexName: string, isFirst: boolean): string {
33
+ if (isFirst) {
34
+ return "VECTORS";
35
+ }
36
+ // Convert kebab-case/snake_case to SCREAMING_SNAKE_CASE
37
+ return indexName
38
+ .replace(/-/g, "_")
39
+ .replace(/[^a-zA-Z0-9_]/g, "")
40
+ .toUpperCase();
41
+ }
42
+
43
+ /**
44
+ * Generate a unique index name for a project.
45
+ * First index: {project}-vectors
46
+ * Subsequent: {project}-vectors-{n}
47
+ */
48
+ function generateIndexName(projectName: string, existingCount: number): string {
49
+ if (existingCount === 0) {
50
+ return `${projectName}-vectors`;
51
+ }
52
+ return `${projectName}-vectors-${existingCount + 1}`;
53
+ }
54
+
55
+ interface ExistingIndex {
56
+ name: string;
57
+ dimensions?: number;
58
+ metric?: string;
59
+ }
60
+
61
+ /**
62
+ * List all Vectorize indexes in the Cloudflare account via wrangler
63
+ */
64
+ async function listIndexesViaWrangler(): Promise<ExistingIndex[]> {
65
+ const result = await $`wrangler vectorize list --json`.nothrow().quiet();
66
+
67
+ if (result.exitCode !== 0) {
68
+ // If wrangler fails, return empty list (might not be logged in)
69
+ return [];
70
+ }
71
+
72
+ try {
73
+ const output = result.stdout.toString().trim();
74
+ const data = JSON.parse(output);
75
+ // wrangler vectorize list --json returns array: [{ "name": "...", ... }]
76
+ return Array.isArray(data) ? data : [];
77
+ } catch {
78
+ return [];
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Find an existing Vectorize index by name
84
+ */
85
+ async function findExistingIndex(indexName: string): Promise<ExistingIndex | null> {
86
+ const indexes = await listIndexesViaWrangler();
87
+ return indexes.find((idx) => idx.name === indexName) ?? null;
88
+ }
89
+
90
+ /**
91
+ * Create a Vectorize index via wrangler
92
+ */
93
+ async function createIndexViaWrangler(
94
+ indexName: string,
95
+ dimensions: number,
96
+ metric: VectorizeMetric,
97
+ ): Promise<{ created: boolean }> {
98
+ // Check if index already exists
99
+ const existing = await findExistingIndex(indexName);
100
+ if (existing) {
101
+ return { created: false };
102
+ }
103
+
104
+ const result =
105
+ await $`wrangler vectorize create ${indexName} --dimensions=${dimensions} --metric=${metric}`.nothrow().quiet();
106
+
107
+ if (result.exitCode !== 0) {
108
+ const stderr = result.stderr.toString().trim();
109
+ throw new Error(stderr || `Failed to create Vectorize index ${indexName}`);
110
+ }
111
+
112
+ return { created: true };
113
+ }
114
+
115
+ /**
116
+ * Create a Vectorize index for the current project.
117
+ *
118
+ * Uses wrangler vectorize create to create the index, then updates
119
+ * wrangler.jsonc with the new binding.
120
+ */
121
+ export async function createVectorizeIndex(
122
+ projectDir: string,
123
+ options: CreateVectorizeOptions = {},
124
+ ): Promise<CreateVectorizeResult> {
125
+ // Get project name from wrangler config
126
+ const projectName = await getProjectNameFromDir(projectDir);
127
+
128
+ // Get existing Vectorize bindings to determine naming
129
+ const wranglerPath = join(projectDir, "wrangler.jsonc");
130
+ const existingBindings = await getExistingVectorizeBindings(wranglerPath);
131
+ const existingCount = existingBindings.length;
132
+
133
+ // Determine index name
134
+ const indexName = options.name ?? generateIndexName(projectName, existingCount);
135
+
136
+ // Determine binding name
137
+ const isFirst = existingCount === 0;
138
+ const bindingName = toBindingName(indexName, isFirst);
139
+
140
+ // Check if binding name already exists
141
+ const bindingExists = existingBindings.some((b) => b.binding === bindingName);
142
+ if (bindingExists) {
143
+ throw new Error(`Binding "${bindingName}" already exists. Choose a different index name.`);
144
+ }
145
+
146
+ // Use omakase defaults: 768 dimensions (for bge-base-en-v1.5), cosine metric
147
+ const dimensions = options.dimensions ?? 768;
148
+ const metric = options.metric ?? "cosine";
149
+
150
+ // Create via wrangler
151
+ const result = await createIndexViaWrangler(indexName, dimensions, metric);
152
+
153
+ // Update wrangler.jsonc with the new binding
154
+ await addVectorizeBinding(wranglerPath, {
155
+ binding: bindingName,
156
+ index_name: indexName,
157
+ });
158
+
159
+ return {
160
+ indexName,
161
+ bindingName,
162
+ dimensions,
163
+ metric,
164
+ created: result.created,
165
+ };
166
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Vectorize index deletion logic for jack MCP tools
3
+ *
4
+ * Uses wrangler CLI to delete Vectorize indexes.
5
+ */
6
+
7
+ import { join } from "node:path";
8
+ import { $ } from "bun";
9
+ import { removeVectorizeBinding } from "./vectorize-config.ts";
10
+
11
+ export interface DeleteVectorizeResult {
12
+ indexName: string;
13
+ deleted: boolean;
14
+ bindingRemoved: boolean;
15
+ }
16
+
17
+ /**
18
+ * Delete a Vectorize index via wrangler
19
+ */
20
+ async function deleteIndexViaWrangler(indexName: string): Promise<void> {
21
+ const result = await $`wrangler vectorize delete ${indexName} --force`.nothrow().quiet();
22
+
23
+ if (result.exitCode !== 0) {
24
+ const stderr = result.stderr.toString().trim();
25
+ // Ignore "not found" errors - the index might already be deleted
26
+ if (!stderr.includes("not found") && !stderr.includes("does not exist")) {
27
+ throw new Error(stderr || `Failed to delete Vectorize index ${indexName}`);
28
+ }
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Delete a Vectorize index for the current project.
34
+ *
35
+ * Uses wrangler vectorize delete to delete the index, then removes
36
+ * the binding from wrangler.jsonc.
37
+ */
38
+ export async function deleteVectorizeIndex(
39
+ projectDir: string,
40
+ indexName: string,
41
+ ): Promise<DeleteVectorizeResult> {
42
+ // Delete via wrangler
43
+ await deleteIndexViaWrangler(indexName);
44
+
45
+ // Remove binding from wrangler.jsonc
46
+ const wranglerPath = join(projectDir, "wrangler.jsonc");
47
+ const bindingRemoved = await removeVectorizeBinding(wranglerPath, indexName);
48
+
49
+ return {
50
+ indexName,
51
+ deleted: true,
52
+ bindingRemoved,
53
+ };
54
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Vectorize index info logic for jack MCP tools
3
+ *
4
+ * Uses wrangler CLI to get information about Vectorize indexes.
5
+ */
6
+
7
+ import { $ } from "bun";
8
+
9
+ export interface VectorizeInfo {
10
+ name: string;
11
+ dimensions: number;
12
+ metric: string;
13
+ vectorCount: number;
14
+ createdOn?: string;
15
+ modifiedOn?: string;
16
+ }
17
+
18
+ /**
19
+ * Get Vectorize index info via wrangler vectorize info
20
+ */
21
+ export async function getVectorizeInfo(indexName: string): Promise<VectorizeInfo | null> {
22
+ const result = await $`wrangler vectorize info ${indexName} --json`.nothrow().quiet();
23
+
24
+ if (result.exitCode !== 0) {
25
+ return null;
26
+ }
27
+
28
+ try {
29
+ const output = result.stdout.toString().trim();
30
+ const data = JSON.parse(output);
31
+
32
+ // wrangler vectorize info --json returns:
33
+ // {
34
+ // "name": "...",
35
+ // "config": { "dimensions": N, "metric": "..." },
36
+ // "vectorsCount": N,
37
+ // "created_on": "...",
38
+ // "modified_on": "..."
39
+ // }
40
+ return {
41
+ name: data.name || indexName,
42
+ dimensions: data.config?.dimensions || 0,
43
+ metric: data.config?.metric || "unknown",
44
+ vectorCount: data.vectorsCount || 0,
45
+ createdOn: data.created_on,
46
+ modifiedOn: data.modified_on,
47
+ };
48
+ } catch {
49
+ // Failed to parse JSON output
50
+ return null;
51
+ }
52
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Vectorize index listing logic for jack MCP tools
3
+ *
4
+ * Lists Vectorize indexes configured in wrangler.jsonc with their metadata.
5
+ */
6
+
7
+ import { join } from "node:path";
8
+ import { getExistingVectorizeBindings } from "./vectorize-config.ts";
9
+ import { getVectorizeInfo } from "./vectorize-info.ts";
10
+
11
+ export interface VectorizeListEntry {
12
+ name: string;
13
+ binding: string;
14
+ dimensions?: number;
15
+ metric?: string;
16
+ vectorCount?: number;
17
+ }
18
+
19
+ /**
20
+ * List all Vectorize indexes configured for a project.
21
+ *
22
+ * Reads bindings from wrangler.jsonc and fetches additional metadata
23
+ * (dimensions, metric, vector count) via wrangler vectorize info for each index.
24
+ */
25
+ export async function listVectorizeIndexes(projectDir: string): Promise<VectorizeListEntry[]> {
26
+ const wranglerPath = join(projectDir, "wrangler.jsonc");
27
+
28
+ // Get existing Vectorize bindings from wrangler.jsonc
29
+ const bindings = await getExistingVectorizeBindings(wranglerPath);
30
+
31
+ if (bindings.length === 0) {
32
+ return [];
33
+ }
34
+
35
+ // Fetch detailed info for each index
36
+ const entries: VectorizeListEntry[] = [];
37
+
38
+ for (const binding of bindings) {
39
+ const entry: VectorizeListEntry = {
40
+ name: binding.index_name,
41
+ binding: binding.binding,
42
+ };
43
+
44
+ // Try to get additional metadata via wrangler
45
+ const info = await getVectorizeInfo(binding.index_name);
46
+ if (info) {
47
+ entry.dimensions = info.dimensions;
48
+ entry.metric = info.metric;
49
+ entry.vectorCount = info.vectorCount;
50
+ }
51
+
52
+ entries.push(entry);
53
+ }
54
+
55
+ return entries;
56
+ }
@@ -91,11 +91,11 @@ export async function checkForUpdate(
91
91
  skipCache = false,
92
92
  ): Promise<string | null> {
93
93
  const currentVersion = getCurrentVersion();
94
+ const now = Date.now();
94
95
 
95
96
  // Check cache first (unless skipCache is true)
96
97
  if (!skipCache) {
97
98
  const cache = await readVersionCache();
98
- const now = Date.now();
99
99
 
100
100
  if (cache && now - cache.checkedAt < CACHE_TTL_MS) {
101
101
  // Use cached value
@@ -106,7 +106,7 @@ export async function checkForUpdate(
106
106
  }
107
107
  }
108
108
 
109
- // Fetch fresh version (don't await in caller for non-blocking)
109
+ // Fetch fresh version
110
110
  const latestVersion = await fetchLatestVersion();
111
111
  if (!latestVersion) return null;
112
112
 
@@ -1,5 +1,9 @@
1
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
4
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
5
+ import { createMcpServer } from "./server.ts";
6
+ import type { McpServerOptions } from "./types.ts";
3
7
 
4
8
  export interface McpClientOptions {
5
9
  command: string;
@@ -12,12 +16,50 @@ export interface McpClientOptions {
12
16
 
13
17
  export interface McpTestClient {
14
18
  client: Client;
15
- transport: StdioClientTransport;
19
+ transport: Transport;
16
20
  getStderr(): string;
17
21
  close(): Promise<void>;
18
22
  }
19
23
 
20
- export async function openMcpTestClient(options: McpClientOptions): Promise<McpTestClient> {
24
+ /**
25
+ * Open an MCP test client using in-memory transport.
26
+ * This is the recommended approach for testing - no process spawning,
27
+ * no race conditions, deterministic behavior.
28
+ */
29
+ export async function openMcpTestClientInMemory(
30
+ serverOptions: McpServerOptions = {},
31
+ ): Promise<McpTestClient> {
32
+ // Create linked transport pair
33
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
34
+
35
+ // Create and connect server
36
+ const { server } = await createMcpServer(serverOptions);
37
+ await server.connect(serverTransport);
38
+
39
+ // Create and connect client
40
+ const client = new Client({
41
+ name: "jack-mcp-test",
42
+ version: "0.1.0",
43
+ });
44
+ await client.connect(clientTransport);
45
+
46
+ return {
47
+ client,
48
+ transport: clientTransport,
49
+ getStderr: () => "(in-memory transport - no stderr)",
50
+ close: async () => {
51
+ await clientTransport.close();
52
+ await serverTransport.close();
53
+ },
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Open an MCP test client using stdio transport (spawns a child process).
59
+ * This tests the full stdio path but is prone to race conditions.
60
+ * Use openMcpTestClientInMemory() for reliable testing.
61
+ */
62
+ export async function openMcpTestClientStdio(options: McpClientOptions): Promise<McpTestClient> {
21
63
  const transport = new StdioClientTransport({
22
64
  command: options.command,
23
65
  args: options.args ?? [],
@@ -51,6 +93,9 @@ export async function openMcpTestClient(options: McpClientOptions): Promise<McpT
51
93
  };
52
94
  }
53
95
 
96
+ // Legacy alias for backwards compatibility
97
+ export const openMcpTestClient = openMcpTestClientStdio;
98
+
54
99
  export function parseMcpToolResult(toolResult: {
55
100
  content?: Array<{ type: string; text?: string }>;
56
101
  [key: string]: unknown;