opencode-plugin-tsaperture 0.1.1

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 ADDED
@@ -0,0 +1,59 @@
1
+ # OpenCode Tailscale Aperture Plugin
2
+
3
+ Automatically populate models from Tailscale Aperture.
4
+
5
+ ## Configuration
6
+
7
+ Configure the Aperture base URL using one of these methods (in order of precedence):
8
+
9
+ ### 1. Plugin Options (opencode.json)
10
+
11
+ ```json
12
+ {
13
+ "plugins": [
14
+ ["opencode-plugin-tsaperture"]
15
+ ]
16
+ }
17
+ ```
18
+
19
+ ### 2. Environment Variable
20
+
21
+ ```bash
22
+ export APERTURE_BASE_URL="http://ai.my-tailnet.ts.net"
23
+ ```
24
+
25
+ ### 3. Config File (aperture.json)
26
+
27
+ Create `aperture.json` in the opencode config directory:
28
+
29
+ **macOS:**
30
+ ```bash
31
+ ~/Library/Application\ Support/opencode/aperture.json
32
+ ```
33
+
34
+ **Linux:**
35
+ ```bash
36
+ ~/.config/opencode/aperture.json
37
+ ```
38
+
39
+ **Windows:**
40
+ ```
41
+ %APPDATA%\opencode\aperture.json
42
+ ```
43
+
44
+ Contents:
45
+ ```json
46
+ {
47
+ "baseUrl": "http://ai.my-tailnet.ts.net",
48
+ "apiKey": ""
49
+ }
50
+ ```
51
+
52
+ `apiKey` is optional. Set it when your Aperture endpoint requires bearer auth. If omitted, the plugin passes an empty key to the OpenAI-compatible provider.
53
+
54
+ ## Usage
55
+
56
+ Once configured, models will be available via the plugin tools:
57
+
58
+ - `/list_aperture_models` - List available models from Aperture
59
+ - `/get_aperture_model modelId=<id>` - Get model details
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const TailscaleAperturePlugin: Plugin;
3
+ export default TailscaleAperturePlugin;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AA+MlD,eAAO,MAAM,uBAAuB,EAAE,MAkJrC,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,287 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import { readFile } from "fs/promises";
5
+ import { platform } from "process";
6
+ function normalizeBaseUrl(baseUrl) {
7
+ return baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
8
+ }
9
+ function getModelDefaults(model) {
10
+ const id = model.id.toLowerCase();
11
+ const providerID = model.metadata?.provider?.id?.toLowerCase();
12
+ const providerName = model.metadata?.provider?.name?.toLowerCase();
13
+ const isZai = id.includes("glm")
14
+ || providerID === "zai"
15
+ || providerID === "z.ai"
16
+ || providerID === "zai-coding-plan"
17
+ || providerName === "z.ai"
18
+ || providerName === "zai-coding-plan";
19
+ const isKimi = id.includes("kimi")
20
+ || providerID === "kimi"
21
+ || providerID === "kimi-for-coding"
22
+ || providerName === "kimi"
23
+ || providerName === "kimi-for-coding";
24
+ if (isZai) {
25
+ return {
26
+ limit: {
27
+ context: 200_000,
28
+ output: 8_192,
29
+ },
30
+ reasoning: true,
31
+ temperature: true,
32
+ tool_call: true,
33
+ modalities: {
34
+ input: ["text"],
35
+ output: ["text"],
36
+ },
37
+ interleaved: {
38
+ field: "reasoning_content",
39
+ },
40
+ options: {
41
+ thinking: {
42
+ type: "enabled",
43
+ clear_thinking: false,
44
+ },
45
+ },
46
+ };
47
+ }
48
+ if (isKimi) {
49
+ return {
50
+ limit: {
51
+ context: 200_000,
52
+ output: 128_000,
53
+ },
54
+ reasoning: true,
55
+ temperature: true,
56
+ tool_call: true,
57
+ modalities: {
58
+ input: ["text"],
59
+ output: ["text"],
60
+ },
61
+ interleaved: {
62
+ field: "reasoning_content",
63
+ },
64
+ options: {
65
+ thinking: {
66
+ type: "enabled",
67
+ clear_thinking: false,
68
+ },
69
+ },
70
+ headers: {
71
+ "User-Agent": "KimiCLI/1.3",
72
+ },
73
+ };
74
+ }
75
+ return {
76
+ limit: {
77
+ context: 128_000,
78
+ output: 8_192,
79
+ },
80
+ reasoning: false,
81
+ temperature: true,
82
+ tool_call: true,
83
+ modalities: {
84
+ input: ["text"],
85
+ output: ["text"],
86
+ },
87
+ interleaved: {
88
+ field: "reasoning_content",
89
+ },
90
+ options: {
91
+ thinking: {
92
+ type: "enabled",
93
+ clear_thinking: false,
94
+ },
95
+ },
96
+ };
97
+ }
98
+ async function fetchApertureModels(baseUrl) {
99
+ const response = await fetch(`${baseUrl}/v1/models`);
100
+ if (!response.ok) {
101
+ throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
102
+ }
103
+ const data = await response.json();
104
+ return data.data || [];
105
+ }
106
+ function getOpenCodeConfigDirs() {
107
+ const home = homedir();
108
+ const dirs = [];
109
+ if (platform === "win32") {
110
+ dirs.push(join(process.env.APPDATA || process.env.LOCALAPPDATA || home, "opencode"));
111
+ }
112
+ else if (platform === "darwin") {
113
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
114
+ if (xdgConfig) {
115
+ dirs.push(join(xdgConfig, "opencode"));
116
+ }
117
+ dirs.push(join(home, ".config", "opencode"));
118
+ dirs.push(join(home, "Library", "Application Support", "opencode"));
119
+ }
120
+ else {
121
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
122
+ if (xdgConfig) {
123
+ dirs.push(join(xdgConfig, "opencode"));
124
+ }
125
+ dirs.push(join(home, ".config", "opencode"));
126
+ }
127
+ return dirs;
128
+ }
129
+ const openCodeConfigDirs = getOpenCodeConfigDirs();
130
+ async function loadApertureConfig() {
131
+ for (const configDir of openCodeConfigDirs) {
132
+ const configPath = join(configDir, "aperture.json");
133
+ try {
134
+ const content = await readFile(configPath, "utf-8");
135
+ console.log(`[TailscaleAperture] Loaded config from ${configPath}`);
136
+ return JSON.parse(content);
137
+ }
138
+ catch (error) {
139
+ if (error.code !== "ENOENT") {
140
+ console.warn(`[TailscaleAperture] Failed to read ${configPath}:`, error);
141
+ }
142
+ }
143
+ }
144
+ return {};
145
+ }
146
+ export const TailscaleAperturePlugin = async (_ctx, options) => {
147
+ const fileConfig = await loadApertureConfig();
148
+ const rawBaseUrl = options?.baseUrl || process.env.APERTURE_BASE_URL || fileConfig.baseUrl;
149
+ const apiKey = options?.apiKey || process.env.APERTURE_API_KEY || fileConfig.apiKey || "";
150
+ if (!rawBaseUrl) {
151
+ console.warn("[TailscaleAperture] No baseUrl configured. Set APERTURE_BASE_URL, add baseUrl to plugin options, or create aperture.json in opencode config directory.");
152
+ return {};
153
+ }
154
+ const baseUrl = normalizeBaseUrl(rawBaseUrl);
155
+ let discoveredModels = [];
156
+ let modelsLoaded = false;
157
+ async function loadModels(refresh = false) {
158
+ if (!refresh && modelsLoaded) {
159
+ return discoveredModels;
160
+ }
161
+ discoveredModels = await fetchApertureModels(baseUrl);
162
+ modelsLoaded = true;
163
+ return discoveredModels;
164
+ }
165
+ try {
166
+ discoveredModels = await loadModels(true);
167
+ if (discoveredModels.length === 0) {
168
+ console.warn("[TailscaleAperture] No models found");
169
+ }
170
+ else {
171
+ console.log(`[TailscaleAperture] Discovered ${discoveredModels.length} models from ${baseUrl}`);
172
+ }
173
+ }
174
+ catch (error) {
175
+ console.warn("[TailscaleAperture] Failed to preload models:", error);
176
+ }
177
+ return {
178
+ config: async (config) => {
179
+ try {
180
+ config.provider ??= {};
181
+ if (discoveredModels.length === 0) {
182
+ return;
183
+ }
184
+ const existingProvider = config.provider.aperture ?? {};
185
+ config.provider.aperture = {
186
+ ...existingProvider,
187
+ npm: existingProvider.npm ?? "@ai-sdk/openai-compatible",
188
+ name: existingProvider.name ?? "Tailscale Aperture",
189
+ options: {
190
+ ...existingProvider.options,
191
+ baseURL: `${baseUrl}/v1`,
192
+ apiKey: existingProvider.options?.apiKey ?? apiKey,
193
+ },
194
+ models: {
195
+ ...existingProvider.models,
196
+ },
197
+ };
198
+ // Add discovered models while preserving explicit user overrides.
199
+ for (const model of discoveredModels) {
200
+ const existingModel = config.provider.aperture.models[model.id] ?? {};
201
+ const defaults = getModelDefaults(model);
202
+ config.provider.aperture.models[model.id] = {
203
+ ...defaults,
204
+ ...existingModel,
205
+ limit: {
206
+ ...defaults.limit,
207
+ ...existingModel.limit,
208
+ },
209
+ modalities: {
210
+ ...defaults.modalities,
211
+ ...existingModel.modalities,
212
+ },
213
+ ...(defaults.interleaved || existingModel.interleaved ? {
214
+ interleaved: existingModel.interleaved ?? defaults.interleaved,
215
+ } : {}),
216
+ ...(defaults.options || existingModel.options ? {
217
+ options: {
218
+ ...defaults.options,
219
+ ...existingModel.options,
220
+ ...(defaults.options?.thinking || existingModel.options?.thinking ? {
221
+ thinking: {
222
+ ...defaults.options?.thinking,
223
+ ...existingModel.options?.thinking,
224
+ },
225
+ } : {}),
226
+ },
227
+ } : {}),
228
+ ...(defaults.headers || existingModel.headers ? {
229
+ headers: {
230
+ ...defaults.headers,
231
+ ...existingModel.headers,
232
+ },
233
+ } : {}),
234
+ id: model.id,
235
+ name: existingModel.name ?? model.id,
236
+ };
237
+ }
238
+ console.log(`[TailscaleAperture] Registered ${discoveredModels.length} models`);
239
+ }
240
+ catch (error) {
241
+ console.error("[TailscaleAperture] Failed to register models:", error);
242
+ }
243
+ },
244
+ tool: {
245
+ list_aperture_models: tool({
246
+ description: "List available models from Tailscale Aperture",
247
+ args: {
248
+ refresh: tool.schema.boolean().optional().describe("Refresh the cached Aperture model list before returning it"),
249
+ },
250
+ async execute(args) {
251
+ try {
252
+ const models = await loadModels(args.refresh ?? false);
253
+ return JSON.stringify({
254
+ models,
255
+ count: models.length,
256
+ }, null, 2);
257
+ }
258
+ catch (error) {
259
+ return JSON.stringify({ error: String(error) });
260
+ }
261
+ },
262
+ }),
263
+ get_aperture_model: tool({
264
+ description: "Get details for a specific Aperture model",
265
+ args: {
266
+ modelId: tool.schema.string().describe("Model ID"),
267
+ refresh: tool.schema.boolean().optional().describe("Refresh the cached Aperture model list before looking up the model"),
268
+ },
269
+ async execute(args) {
270
+ try {
271
+ const models = await loadModels(args.refresh ?? false);
272
+ const model = models.find(m => m.id === args.modelId);
273
+ if (!model) {
274
+ return JSON.stringify({ error: `Model ${args.modelId} not found` });
275
+ }
276
+ return JSON.stringify({ model }, null, 2);
277
+ }
278
+ catch (error) {
279
+ return JSON.stringify({ error: String(error) });
280
+ }
281
+ },
282
+ }),
283
+ },
284
+ };
285
+ };
286
+ export default TailscaleAperturePlugin;
287
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "opencode-plugin-tsaperture",
3
+ "version": "0.1.1",
4
+ "description": "OpenCode plugin to automatically populate models from Tailscale Aperture API",
5
+ "homepage": "https://github.com/SharkyRawr/opencode-plugin-tsaperture",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "keywords": [
10
+ "opencode",
11
+ "plugin",
12
+ "tailscale",
13
+ "aperture"
14
+ ],
15
+ "author": "",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "@opencode-ai/plugin": "^1.3.10"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "^5.9.3",
22
+ "@types/node": "^20.19.37"
23
+ },
24
+ "peerDependencies": {
25
+ "opencode-ai": "^1.3.7"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "watch": "tsc --watch"
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,356 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import { tool } from "@opencode-ai/plugin";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import { readFile } from "fs/promises";
6
+ import { platform } from "process";
7
+
8
+ interface ApertureModel {
9
+ id: string;
10
+ object: string;
11
+ created: number;
12
+ owned_by: string;
13
+ metadata?: {
14
+ provider?: {
15
+ id: string;
16
+ name: string;
17
+ description?: string;
18
+ };
19
+ };
20
+ }
21
+
22
+ interface ApertureResponse {
23
+ object: string;
24
+ data: ApertureModel[];
25
+ }
26
+
27
+ interface ApertureConfig {
28
+ baseUrl?: string;
29
+ apiKey?: string;
30
+ }
31
+
32
+ type ModelConfig = {
33
+ id: string;
34
+ name: string;
35
+ limit: {
36
+ context: number;
37
+ output: number;
38
+ };
39
+ reasoning: boolean;
40
+ temperature: boolean;
41
+ tool_call: boolean;
42
+ modalities: {
43
+ input: Array<"text">;
44
+ output: Array<"text">;
45
+ };
46
+ interleaved?: true | {
47
+ field: "reasoning_content" | "reasoning_details";
48
+ };
49
+ options?: {
50
+ thinking?: {
51
+ type?: string;
52
+ clear_thinking?: boolean;
53
+ };
54
+ };
55
+ headers?: Record<string, string>;
56
+ };
57
+
58
+ function normalizeBaseUrl(baseUrl: string): string {
59
+ return baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
60
+ }
61
+
62
+ function getModelDefaults(model: ApertureModel): Omit<ModelConfig, "id" | "name"> {
63
+ const id = model.id.toLowerCase();
64
+ const providerID = model.metadata?.provider?.id?.toLowerCase();
65
+ const providerName = model.metadata?.provider?.name?.toLowerCase();
66
+ const isZai = id.includes("glm")
67
+ || providerID === "zai"
68
+ || providerID === "z.ai"
69
+ || providerID === "zai-coding-plan"
70
+ || providerName === "z.ai"
71
+ || providerName === "zai-coding-plan";
72
+ const isKimi = id.includes("kimi")
73
+ || providerID === "kimi"
74
+ || providerID === "kimi-for-coding"
75
+ || providerName === "kimi"
76
+ || providerName === "kimi-for-coding";
77
+
78
+ if (isZai) {
79
+ return {
80
+ limit: {
81
+ context: 200_000,
82
+ output: 8_192,
83
+ },
84
+ reasoning: true,
85
+ temperature: true,
86
+ tool_call: true,
87
+ modalities: {
88
+ input: ["text"],
89
+ output: ["text"],
90
+ },
91
+ interleaved: {
92
+ field: "reasoning_content",
93
+ },
94
+ options: {
95
+ thinking: {
96
+ type: "enabled",
97
+ clear_thinking: false,
98
+ },
99
+ },
100
+ };
101
+ }
102
+
103
+ if (isKimi) {
104
+ return {
105
+ limit: {
106
+ context: 200_000,
107
+ output: 128_000,
108
+ },
109
+ reasoning: true,
110
+ temperature: true,
111
+ tool_call: true,
112
+ modalities: {
113
+ input: ["text"],
114
+ output: ["text"],
115
+ },
116
+ interleaved: {
117
+ field: "reasoning_content",
118
+ },
119
+ options: {
120
+ thinking: {
121
+ type: "enabled",
122
+ clear_thinking: false,
123
+ },
124
+ },
125
+ headers: {
126
+ "User-Agent": "KimiCLI/1.3",
127
+ },
128
+ };
129
+ }
130
+
131
+ return {
132
+ limit: {
133
+ context: 128_000,
134
+ output: 8_192,
135
+ },
136
+ reasoning: false,
137
+ temperature: true,
138
+ tool_call: true,
139
+ modalities: {
140
+ input: ["text"],
141
+ output: ["text"],
142
+ },
143
+ interleaved: {
144
+ field: "reasoning_content",
145
+ },
146
+ options: {
147
+ thinking: {
148
+ type: "enabled",
149
+ clear_thinking: false,
150
+ },
151
+ },
152
+ };
153
+ }
154
+
155
+ async function fetchApertureModels(baseUrl: string): Promise<ApertureModel[]> {
156
+ const response = await fetch(`${baseUrl}/v1/models`);
157
+ if (!response.ok) {
158
+ throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
159
+ }
160
+
161
+ const data = await response.json() as ApertureResponse;
162
+ return data.data || [];
163
+ }
164
+
165
+ function getOpenCodeConfigDirs(): string[] {
166
+ const home = homedir();
167
+ const dirs: string[] = [];
168
+
169
+ if (platform === "win32") {
170
+ dirs.push(join(process.env.APPDATA || process.env.LOCALAPPDATA || home, "opencode"));
171
+ } else if (platform === "darwin") {
172
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
173
+ if (xdgConfig) {
174
+ dirs.push(join(xdgConfig, "opencode"));
175
+ }
176
+ dirs.push(join(home, ".config", "opencode"));
177
+ dirs.push(join(home, "Library", "Application Support", "opencode"));
178
+ } else {
179
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
180
+ if (xdgConfig) {
181
+ dirs.push(join(xdgConfig, "opencode"));
182
+ }
183
+ dirs.push(join(home, ".config", "opencode"));
184
+ }
185
+
186
+ return dirs;
187
+ }
188
+
189
+ const openCodeConfigDirs = getOpenCodeConfigDirs();
190
+
191
+ async function loadApertureConfig(): Promise<ApertureConfig> {
192
+ for (const configDir of openCodeConfigDirs) {
193
+ const configPath = join(configDir, "aperture.json");
194
+ try {
195
+ const content = await readFile(configPath, "utf-8");
196
+ console.log(`[TailscaleAperture] Loaded config from ${configPath}`);
197
+ return JSON.parse(content) as ApertureConfig;
198
+ } catch (error) {
199
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
200
+ console.warn(`[TailscaleAperture] Failed to read ${configPath}:`, error);
201
+ }
202
+ }
203
+ }
204
+
205
+ return {};
206
+ }
207
+
208
+ export const TailscaleAperturePlugin: Plugin = async (_ctx, options) => {
209
+ const fileConfig = await loadApertureConfig();
210
+ const rawBaseUrl = (options?.baseUrl as string) || process.env.APERTURE_BASE_URL || fileConfig.baseUrl;
211
+ const apiKey = (options?.apiKey as string) || process.env.APERTURE_API_KEY || fileConfig.apiKey || "";
212
+
213
+ if (!rawBaseUrl) {
214
+ console.warn("[TailscaleAperture] No baseUrl configured. Set APERTURE_BASE_URL, add baseUrl to plugin options, or create aperture.json in opencode config directory.");
215
+ return {};
216
+ }
217
+
218
+ const baseUrl = normalizeBaseUrl(rawBaseUrl);
219
+ let discoveredModels: ApertureModel[] = [];
220
+ let modelsLoaded = false;
221
+
222
+ async function loadModels(refresh = false): Promise<ApertureModel[]> {
223
+ if (!refresh && modelsLoaded) {
224
+ return discoveredModels;
225
+ }
226
+
227
+ discoveredModels = await fetchApertureModels(baseUrl);
228
+ modelsLoaded = true;
229
+ return discoveredModels;
230
+ }
231
+
232
+ try {
233
+ discoveredModels = await loadModels(true);
234
+ if (discoveredModels.length === 0) {
235
+ console.warn("[TailscaleAperture] No models found");
236
+ } else {
237
+ console.log(`[TailscaleAperture] Discovered ${discoveredModels.length} models from ${baseUrl}`);
238
+ }
239
+ } catch (error) {
240
+ console.warn("[TailscaleAperture] Failed to preload models:", error);
241
+ }
242
+
243
+ return {
244
+ config: async (config: any) => {
245
+ try {
246
+ config.provider ??= {};
247
+
248
+ if (discoveredModels.length === 0) {
249
+ return;
250
+ }
251
+
252
+ const existingProvider = config.provider.aperture ?? {};
253
+ config.provider.aperture = {
254
+ ...existingProvider,
255
+ npm: existingProvider.npm ?? "@ai-sdk/openai-compatible",
256
+ name: existingProvider.name ?? "Tailscale Aperture",
257
+ options: {
258
+ ...existingProvider.options,
259
+ baseURL: `${baseUrl}/v1`,
260
+ apiKey: existingProvider.options?.apiKey ?? apiKey,
261
+ },
262
+ models: {
263
+ ...existingProvider.models,
264
+ },
265
+ };
266
+
267
+ // Add discovered models while preserving explicit user overrides.
268
+ for (const model of discoveredModels) {
269
+ const existingModel = config.provider.aperture.models[model.id] ?? {};
270
+ const defaults = getModelDefaults(model);
271
+ config.provider.aperture.models[model.id] = {
272
+ ...defaults,
273
+ ...existingModel,
274
+ limit: {
275
+ ...defaults.limit,
276
+ ...existingModel.limit,
277
+ },
278
+ modalities: {
279
+ ...defaults.modalities,
280
+ ...existingModel.modalities,
281
+ },
282
+ ...(defaults.interleaved || existingModel.interleaved ? {
283
+ interleaved: existingModel.interleaved ?? defaults.interleaved,
284
+ } : {}),
285
+ ...(defaults.options || existingModel.options ? {
286
+ options: {
287
+ ...defaults.options,
288
+ ...existingModel.options,
289
+ ...(defaults.options?.thinking || existingModel.options?.thinking ? {
290
+ thinking: {
291
+ ...defaults.options?.thinking,
292
+ ...existingModel.options?.thinking,
293
+ },
294
+ } : {}),
295
+ },
296
+ } : {}),
297
+ ...(defaults.headers || existingModel.headers ? {
298
+ headers: {
299
+ ...defaults.headers,
300
+ ...existingModel.headers,
301
+ },
302
+ } : {}),
303
+ id: model.id,
304
+ name: existingModel.name ?? model.id,
305
+ };
306
+ }
307
+
308
+ console.log(`[TailscaleAperture] Registered ${discoveredModels.length} models`);
309
+ } catch (error) {
310
+ console.error("[TailscaleAperture] Failed to register models:", error);
311
+ }
312
+ },
313
+
314
+ tool: {
315
+ list_aperture_models: tool({
316
+ description: "List available models from Tailscale Aperture",
317
+ args: {
318
+ refresh: tool.schema.boolean().optional().describe("Refresh the cached Aperture model list before returning it"),
319
+ },
320
+ async execute(args) {
321
+ try {
322
+ const models = await loadModels(args.refresh ?? false);
323
+ return JSON.stringify({
324
+ models,
325
+ count: models.length,
326
+ }, null, 2);
327
+ } catch (error) {
328
+ return JSON.stringify({ error: String(error) });
329
+ }
330
+ },
331
+ }),
332
+
333
+ get_aperture_model: tool({
334
+ description: "Get details for a specific Aperture model",
335
+ args: {
336
+ modelId: tool.schema.string().describe("Model ID"),
337
+ refresh: tool.schema.boolean().optional().describe("Refresh the cached Aperture model list before looking up the model"),
338
+ },
339
+ async execute(args) {
340
+ try {
341
+ const models = await loadModels(args.refresh ?? false);
342
+ const model = models.find(m => m.id === args.modelId);
343
+ if (!model) {
344
+ return JSON.stringify({ error: `Model ${args.modelId} not found` });
345
+ }
346
+ return JSON.stringify({ model }, null, 2);
347
+ } catch (error) {
348
+ return JSON.stringify({ error: String(error) });
349
+ }
350
+ },
351
+ }),
352
+ },
353
+ };
354
+ };
355
+
356
+ export default TailscaleAperturePlugin;
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true,
17
+ "noImplicitAny": true,
18
+ "strictNullChecks": true,
19
+ "strictFunctionTypes": true,
20
+ "strictBindCallApply": true,
21
+ "strictPropertyInitialization": true,
22
+ "noImplicitThis": true,
23
+ "alwaysStrict": true
24
+ },
25
+ "include": ["src/**/*"],
26
+ "exclude": ["node_modules", "dist"]
27
+ }