@schuttdev/kon 0.2.6

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 (2) hide show
  1. package/dist/index.js +908 -0
  2. package/package.json +29 -0
package/dist/index.js ADDED
@@ -0,0 +1,908 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { defineCommand, runMain } from "citty";
5
+
6
+ // ../cli/src/config.ts
7
+ import { readFile, writeFile, mkdir } from "fs/promises";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+ function getConfigDir() {
11
+ return process.env.GIGAI_CONFIG_DIR ?? join(homedir(), ".gigai");
12
+ }
13
+ function getConfigPath() {
14
+ return join(getConfigDir(), "config.json");
15
+ }
16
+ async function readConfig() {
17
+ try {
18
+ const raw = await readFile(getConfigPath(), "utf8");
19
+ const parsed = JSON.parse(raw);
20
+ if (parsed.server && parsed.token && !parsed.servers) {
21
+ const name = deriveServerName(parsed.server);
22
+ const migrated = {
23
+ activeServer: name,
24
+ servers: {
25
+ [name]: {
26
+ server: parsed.server,
27
+ token: parsed.token,
28
+ sessionToken: parsed.sessionToken,
29
+ sessionExpiresAt: parsed.sessionExpiresAt
30
+ }
31
+ }
32
+ };
33
+ await writeConfig(migrated);
34
+ return migrated;
35
+ }
36
+ return { activeServer: parsed.activeServer, servers: parsed.servers ?? {} };
37
+ } catch {
38
+ return { servers: {} };
39
+ }
40
+ }
41
+ async function writeConfig(config) {
42
+ const dir = getConfigDir();
43
+ await mkdir(dir, { recursive: true });
44
+ await writeFile(getConfigPath(), JSON.stringify(config, null, 2) + "\n", { mode: 384 });
45
+ }
46
+ function getActiveEntry(config) {
47
+ if (!config.activeServer || !config.servers[config.activeServer]) return void 0;
48
+ return { name: config.activeServer, entry: config.servers[config.activeServer] };
49
+ }
50
+ async function addServer(name, server, token) {
51
+ const config = await readConfig();
52
+ for (const [existingName, entry] of Object.entries(config.servers)) {
53
+ if (normalizeUrl(entry.server) === normalizeUrl(server)) {
54
+ config.servers[existingName] = { server, token };
55
+ config.activeServer = existingName;
56
+ await writeConfig(config);
57
+ return;
58
+ }
59
+ }
60
+ config.servers[name] = { server, token };
61
+ config.activeServer = name;
62
+ await writeConfig(config);
63
+ }
64
+ async function updateServerSession(name, sessionToken, sessionExpiresAt) {
65
+ const config = await readConfig();
66
+ const entry = config.servers[name];
67
+ if (!entry) return;
68
+ entry.sessionToken = sessionToken;
69
+ entry.sessionExpiresAt = sessionExpiresAt;
70
+ await writeConfig(config);
71
+ }
72
+ function deriveServerName(url) {
73
+ try {
74
+ const hostname = new URL(url).hostname;
75
+ return hostname.split(".")[0];
76
+ } catch {
77
+ return "default";
78
+ }
79
+ }
80
+ function normalizeUrl(url) {
81
+ try {
82
+ const u = new URL(url);
83
+ return u.hostname.toLowerCase();
84
+ } catch {
85
+ return url.toLowerCase();
86
+ }
87
+ }
88
+
89
+ // ../shared/dist/index.mjs
90
+ import { z } from "zod";
91
+ var TailscaleHttpsConfigSchema = z.object({
92
+ provider: z.literal("tailscale"),
93
+ funnelPort: z.number().optional()
94
+ });
95
+ var CloudflareHttpsConfigSchema = z.object({
96
+ provider: z.literal("cloudflare"),
97
+ tunnelName: z.string(),
98
+ domain: z.string().optional()
99
+ });
100
+ var ManualHttpsConfigSchema = z.object({
101
+ provider: z.literal("manual"),
102
+ certPath: z.string(),
103
+ keyPath: z.string()
104
+ });
105
+ var HttpsConfigSchema = z.discriminatedUnion("provider", [
106
+ TailscaleHttpsConfigSchema,
107
+ CloudflareHttpsConfigSchema,
108
+ ManualHttpsConfigSchema
109
+ ]);
110
+ var CliToolConfigSchema = z.object({
111
+ type: z.literal("cli"),
112
+ name: z.string(),
113
+ command: z.string(),
114
+ args: z.array(z.string()).optional(),
115
+ description: z.string(),
116
+ timeout: z.number().optional(),
117
+ cwd: z.string().optional(),
118
+ env: z.record(z.string()).optional()
119
+ });
120
+ var McpToolConfigSchema = z.object({
121
+ type: z.literal("mcp"),
122
+ name: z.string(),
123
+ command: z.string(),
124
+ args: z.array(z.string()).optional(),
125
+ description: z.string(),
126
+ env: z.record(z.string()).optional()
127
+ });
128
+ var ScriptToolConfigSchema = z.object({
129
+ type: z.literal("script"),
130
+ name: z.string(),
131
+ path: z.string(),
132
+ description: z.string(),
133
+ timeout: z.number().optional(),
134
+ interpreter: z.string().optional()
135
+ });
136
+ var BuiltinToolConfigSchema = z.object({
137
+ type: z.literal("builtin"),
138
+ name: z.string(),
139
+ builtin: z.enum(["filesystem", "shell"]),
140
+ description: z.string(),
141
+ config: z.record(z.unknown()).optional()
142
+ });
143
+ var ToolConfigSchema = z.discriminatedUnion("type", [
144
+ CliToolConfigSchema,
145
+ McpToolConfigSchema,
146
+ ScriptToolConfigSchema,
147
+ BuiltinToolConfigSchema
148
+ ]);
149
+ var AuthConfigSchema = z.object({
150
+ encryptionKey: z.string().length(64),
151
+ pairingTtlSeconds: z.number().default(300),
152
+ sessionTtlSeconds: z.number().default(14400)
153
+ });
154
+ var ServerConfigSchema = z.object({
155
+ port: z.number().default(7443),
156
+ host: z.string().default("0.0.0.0"),
157
+ https: HttpsConfigSchema.optional()
158
+ });
159
+ var GigaiConfigSchema = z.object({
160
+ serverName: z.string().optional(),
161
+ server: ServerConfigSchema,
162
+ auth: AuthConfigSchema,
163
+ tools: z.array(ToolConfigSchema).default([])
164
+ });
165
+ function decodeJWTPayload(token) {
166
+ const parts = token.split(".");
167
+ if (parts.length !== 3) {
168
+ throw new Error("Invalid JWT format: expected 3 segments");
169
+ }
170
+ const payload = parts[1];
171
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
172
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
173
+ const decoded = Buffer.from(padded, "base64").toString("utf8");
174
+ return JSON.parse(decoded);
175
+ }
176
+
177
+ // ../cli/src/identity.ts
178
+ function getOrgUUID() {
179
+ if (process.env.GIGAI_ORG_UUID) {
180
+ return process.env.GIGAI_ORG_UUID;
181
+ }
182
+ const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || "";
183
+ const jwtMatch = proxyUrl.match(/jwt_([^@]+)/);
184
+ if (jwtMatch) {
185
+ try {
186
+ const payload = decodeJWTPayload(jwtMatch[1]);
187
+ if (payload.organization_uuid) {
188
+ return payload.organization_uuid;
189
+ }
190
+ } catch {
191
+ }
192
+ }
193
+ const anthropicProxy = process.env.ANTHROPIC_PROXY_URL ?? "";
194
+ const anthropicJwtMatch = anthropicProxy.match(/([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/);
195
+ if (anthropicJwtMatch) {
196
+ try {
197
+ const payload = decodeJWTPayload(anthropicJwtMatch[1]);
198
+ if (payload.organization_uuid) {
199
+ return payload.organization_uuid;
200
+ }
201
+ } catch {
202
+ }
203
+ }
204
+ const apiKey = process.env.ANTHROPIC_API_KEY ?? "";
205
+ if (apiKey.includes(".")) {
206
+ try {
207
+ const payload = decodeJWTPayload(apiKey);
208
+ if (payload.organization_uuid) {
209
+ return payload.organization_uuid;
210
+ }
211
+ } catch {
212
+ }
213
+ }
214
+ throw new Error(
215
+ "Cannot determine organization UUID. Set GIGAI_ORG_UUID environment variable."
216
+ );
217
+ }
218
+
219
+ // ../cli/src/http.ts
220
+ async function getProxyDispatcher() {
221
+ const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
222
+ if (!proxyUrl) return void 0;
223
+ try {
224
+ const undici = await import("undici");
225
+ return new undici.ProxyAgent(proxyUrl);
226
+ } catch {
227
+ return void 0;
228
+ }
229
+ }
230
+ var _dispatcher = null;
231
+ async function ensureDispatcher() {
232
+ if (_dispatcher === null) {
233
+ _dispatcher = await getProxyDispatcher();
234
+ }
235
+ return _dispatcher;
236
+ }
237
+ function createHttpClient(serverUrl, sessionToken) {
238
+ const baseUrl = serverUrl.replace(/\/$/, "");
239
+ async function request(path, init = {}) {
240
+ const headers = {
241
+ ...init.headers ?? {}
242
+ };
243
+ if (sessionToken) {
244
+ headers["Authorization"] = `Bearer ${sessionToken}`;
245
+ }
246
+ if (!headers["Content-Type"] && init.body && typeof init.body === "string") {
247
+ headers["Content-Type"] = "application/json";
248
+ }
249
+ const dispatcher = await ensureDispatcher();
250
+ const fetchOpts = {
251
+ ...init,
252
+ headers
253
+ };
254
+ if (dispatcher) {
255
+ fetchOpts.dispatcher = dispatcher;
256
+ }
257
+ const res = await fetch(`${baseUrl}${path}`, fetchOpts);
258
+ if (!res.ok) {
259
+ let errorBody;
260
+ try {
261
+ errorBody = await res.json();
262
+ } catch {
263
+ }
264
+ const message = errorBody?.error?.message ?? `HTTP ${res.status}: ${res.statusText}`;
265
+ throw new Error(message);
266
+ }
267
+ return res.json();
268
+ }
269
+ return {
270
+ get(path) {
271
+ return request(path);
272
+ },
273
+ post(path, body) {
274
+ return request(path, {
275
+ method: "POST",
276
+ body: body ? JSON.stringify(body) : void 0
277
+ });
278
+ },
279
+ async postMultipart(path, formData) {
280
+ const headers = {};
281
+ if (sessionToken) {
282
+ headers["Authorization"] = `Bearer ${sessionToken}`;
283
+ }
284
+ const dispatcher = await ensureDispatcher();
285
+ const fetchOpts = {
286
+ method: "POST",
287
+ headers,
288
+ body: formData
289
+ };
290
+ if (dispatcher) {
291
+ fetchOpts.dispatcher = dispatcher;
292
+ }
293
+ const res = await fetch(`${baseUrl}${path}`, fetchOpts);
294
+ if (!res.ok) {
295
+ let errorBody;
296
+ try {
297
+ errorBody = await res.json();
298
+ } catch {
299
+ }
300
+ throw new Error(errorBody?.error?.message ?? `HTTP ${res.status}`);
301
+ }
302
+ return res.json();
303
+ },
304
+ async getRaw(path) {
305
+ const headers = {};
306
+ if (sessionToken) {
307
+ headers["Authorization"] = `Bearer ${sessionToken}`;
308
+ }
309
+ const dispatcher = await ensureDispatcher();
310
+ const fetchOpts = { headers };
311
+ if (dispatcher) {
312
+ fetchOpts.dispatcher = dispatcher;
313
+ }
314
+ const res = await fetch(`${baseUrl}${path}`, fetchOpts);
315
+ if (!res.ok) {
316
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
317
+ }
318
+ return res;
319
+ }
320
+ };
321
+ }
322
+
323
+ // ../cli/src/version.ts
324
+ var VERSION = "0.2.6";
325
+
326
+ // ../cli/src/connect.ts
327
+ async function connect(serverName) {
328
+ const config = await readConfig();
329
+ if (serverName) {
330
+ if (!config.servers[serverName]) {
331
+ const available = Object.keys(config.servers);
332
+ throw new Error(
333
+ available.length > 0 ? `Unknown server "${serverName}". Available: ${available.join(", ")}` : `No servers configured. Run 'gigai pair' first.`
334
+ );
335
+ }
336
+ config.activeServer = serverName;
337
+ await writeConfig(config);
338
+ }
339
+ const active = getActiveEntry(config);
340
+ if (!active) {
341
+ throw new Error("No server configured. Run 'gigai pair' first.");
342
+ }
343
+ const { name, entry } = active;
344
+ if (entry.sessionToken && entry.sessionExpiresAt) {
345
+ if (Date.now() < entry.sessionExpiresAt - 5 * 60 * 1e3) {
346
+ await checkAndUpdateServer(entry.server, entry.sessionToken);
347
+ return { serverUrl: entry.server, sessionToken: entry.sessionToken };
348
+ }
349
+ }
350
+ const orgUuid = getOrgUUID();
351
+ const http = createHttpClient(entry.server);
352
+ const res = await http.post("/auth/connect", {
353
+ encryptedToken: entry.token,
354
+ orgUuid
355
+ });
356
+ await updateServerSession(name, res.sessionToken, res.expiresAt);
357
+ await checkAndUpdateServer(entry.server, res.sessionToken);
358
+ return { serverUrl: entry.server, sessionToken: res.sessionToken };
359
+ }
360
+ async function checkAndUpdateServer(serverUrl, sessionToken) {
361
+ try {
362
+ const http = createHttpClient(serverUrl);
363
+ const health = await http.get("/health");
364
+ if (isNewer(VERSION, health.version)) {
365
+ console.log(`Server is outdated (${health.version} \u2192 ${VERSION}). Updating...`);
366
+ const authedHttp = createHttpClient(serverUrl, sessionToken);
367
+ const res = await authedHttp.post("/admin/update");
368
+ if (res.updated) {
369
+ console.log("Server updated and restarting.");
370
+ await waitForServer(serverUrl, 15e3);
371
+ console.log("Server is back online.");
372
+ } else {
373
+ console.log(`Server update failed: ${res.error ?? "unknown error"}`);
374
+ }
375
+ }
376
+ } catch {
377
+ }
378
+ }
379
+ async function waitForServer(serverUrl, timeoutMs) {
380
+ const start = Date.now();
381
+ const http = createHttpClient(serverUrl);
382
+ await new Promise((r) => setTimeout(r, 2e3));
383
+ while (Date.now() - start < timeoutMs) {
384
+ try {
385
+ await http.get("/health");
386
+ return;
387
+ } catch {
388
+ await new Promise((r) => setTimeout(r, 1e3));
389
+ }
390
+ }
391
+ }
392
+ function isNewer(client, server) {
393
+ const parse = (v) => {
394
+ const [core, pre] = v.replace(/^v/, "").split("-");
395
+ const parts = core.split(".").map(Number);
396
+ const preNum = pre ? parseInt(pre.replace(/\D+/g, "")) || 0 : Infinity;
397
+ return [...parts, preNum];
398
+ };
399
+ const c = parse(client);
400
+ const s = parse(server);
401
+ for (let i = 0; i < Math.max(c.length, s.length); i++) {
402
+ const cv = c[i] ?? 0;
403
+ const sv = s[i] ?? 0;
404
+ if (cv > sv) return true;
405
+ if (cv < sv) return false;
406
+ }
407
+ return false;
408
+ }
409
+
410
+ // ../cli/src/skill.ts
411
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
412
+ import "path";
413
+ var SKILL_MD = `---
414
+ name: gigai
415
+ description: Access tools on the user's machine via the gigai CLI
416
+ ---
417
+
418
+ # gigai
419
+
420
+ You have access to tools running on the user's local machine through the gigai CLI. This CLI proxies commands over HTTPS to a server on the user's machine.
421
+
422
+ ## Setup (run once per conversation)
423
+
424
+ \`\`\`bash
425
+ npm install -g @schuttdev/kon 2>/dev/null
426
+ mkdir -p ~/.gigai
427
+ cp /mnt/skills/user/gigai/config.json ~/.gigai/config.json
428
+ gigai connect
429
+ \`\`\`
430
+
431
+ If \`gigai connect\` succeeds, you are ready to use tools. If it fails, tell the user.
432
+
433
+ ## Usage
434
+
435
+ ### List available tools
436
+ \`\`\`bash
437
+ gigai list
438
+ \`\`\`
439
+
440
+ ### Run a tool
441
+ \`\`\`bash
442
+ gigai <tool-name> [args...]
443
+ \`\`\`
444
+
445
+ Examples:
446
+ \`\`\`bash
447
+ gigai shell date
448
+ gigai fs list Documents
449
+ gigai shell whoami
450
+ \`\`\`
451
+
452
+ ### Get help for a tool
453
+ \`\`\`bash
454
+ gigai help <tool-name>
455
+ \`\`\`
456
+
457
+ ### Switch server (if multiple are configured)
458
+ \`\`\`bash
459
+ gigai connect <server-name>
460
+ \`\`\`
461
+
462
+ ### File transfer
463
+ \`\`\`bash
464
+ gigai upload <file>
465
+ gigai download <id> <dest>
466
+ \`\`\`
467
+
468
+ ## Important
469
+
470
+ - Always run the setup block before first use in a new conversation
471
+ - All commands execute on the **user's machine**, not in this sandbox
472
+ - If you get auth errors, run \`gigai connect\` to refresh the session
473
+ - Tools are scoped to what the user has configured \u2014 if a tool is missing, tell the user
474
+ `;
475
+ async function hasExistingSkill() {
476
+ try {
477
+ await readFile2("/mnt/skills/user/gigai/config.json", "utf8");
478
+ return true;
479
+ } catch {
480
+ return false;
481
+ }
482
+ }
483
+ async function generateSkillZip(serverName, serverUrl, token) {
484
+ let skillConfig = { servers: {} };
485
+ try {
486
+ const raw = await readFile2("/mnt/skills/user/gigai/config.json", "utf8");
487
+ const existing = JSON.parse(raw);
488
+ if (existing.servers) {
489
+ skillConfig = existing;
490
+ }
491
+ } catch {
492
+ }
493
+ let merged = false;
494
+ for (const [name, entry] of Object.entries(skillConfig.servers)) {
495
+ if (normalizeHost(entry.server) === normalizeHost(serverUrl)) {
496
+ skillConfig.servers[name] = { server: serverUrl, token };
497
+ skillConfig.activeServer = name;
498
+ merged = true;
499
+ break;
500
+ }
501
+ }
502
+ if (!merged) {
503
+ skillConfig.servers[serverName] = { server: serverUrl, token };
504
+ skillConfig.activeServer = serverName;
505
+ }
506
+ const configJson = JSON.stringify(skillConfig, null, 2) + "\n";
507
+ return createZip([
508
+ { path: "gigai/SKILL.md", data: Buffer.from(SKILL_MD, "utf8") },
509
+ { path: "gigai/config.json", data: Buffer.from(configJson, "utf8") }
510
+ ]);
511
+ }
512
+ async function writeSkillZip(zip) {
513
+ const outputsDir = "/mnt/user-data/outputs";
514
+ try {
515
+ await mkdir2(outputsDir, { recursive: true });
516
+ const outPath = `${outputsDir}/gigai.zip`;
517
+ await writeFile2(outPath, zip);
518
+ return outPath;
519
+ } catch {
520
+ const outPath = "gigai.zip";
521
+ await writeFile2(outPath, zip);
522
+ return outPath;
523
+ }
524
+ }
525
+ function normalizeHost(url) {
526
+ try {
527
+ return new URL(url).hostname.toLowerCase();
528
+ } catch {
529
+ return url.toLowerCase();
530
+ }
531
+ }
532
+ var crc32Table = new Uint32Array(256);
533
+ for (let i = 0; i < 256; i++) {
534
+ let c = i;
535
+ for (let j = 0; j < 8; j++) {
536
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
537
+ }
538
+ crc32Table[i] = c >>> 0;
539
+ }
540
+ function crc32(data) {
541
+ let crc = 4294967295;
542
+ for (const byte of data) {
543
+ crc = crc >>> 8 ^ crc32Table[(crc ^ byte) & 255];
544
+ }
545
+ return (crc ^ 4294967295) >>> 0;
546
+ }
547
+ function createZip(entries) {
548
+ const parts = [];
549
+ const centralParts = [];
550
+ let offset = 0;
551
+ for (const entry of entries) {
552
+ const name = Buffer.from(entry.path, "utf8");
553
+ const checksum = crc32(entry.data);
554
+ const local = Buffer.alloc(30);
555
+ local.writeUInt32LE(67324752, 0);
556
+ local.writeUInt16LE(20, 4);
557
+ local.writeUInt16LE(0, 6);
558
+ local.writeUInt16LE(0, 8);
559
+ local.writeUInt16LE(0, 10);
560
+ local.writeUInt16LE(0, 12);
561
+ local.writeUInt32LE(checksum, 14);
562
+ local.writeUInt32LE(entry.data.length, 18);
563
+ local.writeUInt32LE(entry.data.length, 22);
564
+ local.writeUInt16LE(name.length, 26);
565
+ local.writeUInt16LE(0, 28);
566
+ parts.push(local, name, entry.data);
567
+ const central = Buffer.alloc(46);
568
+ central.writeUInt32LE(33639248, 0);
569
+ central.writeUInt16LE(20, 4);
570
+ central.writeUInt16LE(20, 6);
571
+ central.writeUInt16LE(0, 8);
572
+ central.writeUInt16LE(0, 10);
573
+ central.writeUInt16LE(0, 12);
574
+ central.writeUInt16LE(0, 14);
575
+ central.writeUInt32LE(checksum, 16);
576
+ central.writeUInt32LE(entry.data.length, 20);
577
+ central.writeUInt32LE(entry.data.length, 24);
578
+ central.writeUInt16LE(name.length, 28);
579
+ central.writeUInt16LE(0, 30);
580
+ central.writeUInt16LE(0, 32);
581
+ central.writeUInt16LE(0, 34);
582
+ central.writeUInt16LE(0, 36);
583
+ central.writeUInt32LE(0, 38);
584
+ central.writeUInt32LE(offset, 42);
585
+ centralParts.push(central, name);
586
+ offset += 30 + name.length + entry.data.length;
587
+ }
588
+ const centralDir = Buffer.concat(centralParts);
589
+ const eocd = Buffer.alloc(22);
590
+ eocd.writeUInt32LE(101010256, 0);
591
+ eocd.writeUInt16LE(0, 4);
592
+ eocd.writeUInt16LE(0, 6);
593
+ eocd.writeUInt16LE(entries.length, 8);
594
+ eocd.writeUInt16LE(entries.length, 10);
595
+ eocd.writeUInt32LE(centralDir.length, 12);
596
+ eocd.writeUInt32LE(offset, 16);
597
+ eocd.writeUInt16LE(0, 20);
598
+ return Buffer.concat([...parts, centralDir, eocd]);
599
+ }
600
+
601
+ // ../cli/src/pair.ts
602
+ async function pair(code, serverUrl) {
603
+ const orgUuid = getOrgUUID();
604
+ const http = createHttpClient(serverUrl);
605
+ const res = await http.post("/auth/pair", {
606
+ pairingCode: code,
607
+ orgUuid
608
+ });
609
+ await addServer(res.serverName, serverUrl, res.encryptedToken);
610
+ console.log(`Paired with "${res.serverName}" successfully!`);
611
+ const existing = await hasExistingSkill();
612
+ const zip = await generateSkillZip(res.serverName, serverUrl, res.encryptedToken);
613
+ const outPath = await writeSkillZip(zip);
614
+ console.log(`
615
+ Skill zip written to: ${outPath}`);
616
+ if (existing) {
617
+ console.log("Skill file updated. Download and re-upload to Claude.");
618
+ } else {
619
+ console.log("Upload this file as a skill in Claude (Settings \u2192 Customize \u2192 Upload Skill).");
620
+ }
621
+ }
622
+
623
+ // ../cli/src/discover.ts
624
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
625
+ import { join as join2 } from "path";
626
+ import { homedir as homedir2 } from "os";
627
+ var MANIFEST_TTL = 5 * 60 * 1e3;
628
+ function getManifestPath() {
629
+ const dir = process.env.GIGAI_CONFIG_DIR ?? join2(homedir2(), ".gigai");
630
+ return join2(dir, "tool-manifest.json");
631
+ }
632
+ async function fetchTools(http) {
633
+ try {
634
+ const raw = await readFile3(getManifestPath(), "utf8");
635
+ const cache = JSON.parse(raw);
636
+ if (Date.now() - cache.fetchedAt < MANIFEST_TTL) {
637
+ return cache.tools;
638
+ }
639
+ } catch {
640
+ }
641
+ const res = await http.get("/tools");
642
+ try {
643
+ const dir = process.env.GIGAI_CONFIG_DIR ?? join2(homedir2(), ".gigai");
644
+ await mkdir3(dir, { recursive: true });
645
+ const cache = { tools: res.tools, fetchedAt: Date.now() };
646
+ await writeFile3(getManifestPath(), JSON.stringify(cache));
647
+ } catch {
648
+ }
649
+ return res.tools;
650
+ }
651
+ async function fetchToolDetail(http, name) {
652
+ return http.get(`/tools/${encodeURIComponent(name)}`);
653
+ }
654
+
655
+ // ../cli/src/exec.ts
656
+ async function execTool(http, name, args, timeout) {
657
+ const res = await http.post("/exec", {
658
+ tool: name,
659
+ args,
660
+ timeout
661
+ });
662
+ if (res.stdout) process.stdout.write(res.stdout);
663
+ if (res.stderr) process.stderr.write(res.stderr);
664
+ process.exitCode = res.exitCode;
665
+ }
666
+ async function execMcpTool(http, tool, mcpTool, args) {
667
+ const res = await http.post("/exec/mcp", {
668
+ tool,
669
+ mcpTool,
670
+ args
671
+ });
672
+ for (const content of res.content) {
673
+ if (content.type === "text" && content.text) {
674
+ process.stdout.write(content.text + "\n");
675
+ } else if (content.type === "image") {
676
+ console.log(`[Image: ${content.mimeType}]`);
677
+ } else if (content.type === "resource") {
678
+ console.log(`[Resource: ${content.mimeType}]`);
679
+ }
680
+ }
681
+ if (res.isError) {
682
+ process.exitCode = 1;
683
+ }
684
+ }
685
+
686
+ // ../cli/src/transfer.ts
687
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
688
+ import { basename } from "path";
689
+ import { Blob } from "buffer";
690
+ async function upload(http, filePath) {
691
+ const content = await readFile4(filePath);
692
+ const filename = basename(filePath);
693
+ const formData = new FormData();
694
+ const blob = new Blob([content]);
695
+ formData.append("file", blob, filename);
696
+ const res = await http.postMultipart(
697
+ "/transfer/upload",
698
+ formData
699
+ );
700
+ console.log(`Uploaded: ${res.id}`);
701
+ return res.id;
702
+ }
703
+ async function download(http, id, destPath) {
704
+ const res = await http.getRaw(`/transfer/${encodeURIComponent(id)}`);
705
+ const buffer = Buffer.from(await res.arrayBuffer());
706
+ await writeFile4(destPath, buffer);
707
+ console.log(`Downloaded to: ${destPath}`);
708
+ }
709
+
710
+ // ../cli/src/output.ts
711
+ function formatToolList(tools) {
712
+ if (tools.length === 0) return "No tools registered.";
713
+ const maxName = Math.max(...tools.map((t) => t.name.length));
714
+ const maxType = Math.max(...tools.map((t) => t.type.length));
715
+ const lines = tools.map((t) => {
716
+ const name = t.name.padEnd(maxName);
717
+ const type = t.type.padEnd(maxType);
718
+ return ` ${name} ${type} ${t.description}`;
719
+ });
720
+ return `Available tools:
721
+ ${lines.join("\n")}`;
722
+ }
723
+ function formatToolDetail(detail) {
724
+ const lines = [];
725
+ lines.push(`${detail.name} (${detail.type})`);
726
+ lines.push(` ${detail.description}`);
727
+ if (detail.usage) {
728
+ lines.push(`
729
+ Usage: ${detail.usage}`);
730
+ }
731
+ if (detail.args?.length) {
732
+ lines.push("\nArguments:");
733
+ for (const arg of detail.args) {
734
+ const req = arg.required ? " (required)" : "";
735
+ const def = arg.default ? ` [default: ${arg.default}]` : "";
736
+ lines.push(` ${arg.name}${req}${def} \u2014 ${arg.description}`);
737
+ }
738
+ }
739
+ if (detail.mcpTools?.length) {
740
+ lines.push("\nMCP Tools:");
741
+ for (const t of detail.mcpTools) {
742
+ lines.push(` ${t.name} \u2014 ${t.description}`);
743
+ }
744
+ }
745
+ return lines.join("\n");
746
+ }
747
+ function formatStatus(config) {
748
+ const serverNames = Object.keys(config.servers);
749
+ if (serverNames.length === 0) {
750
+ return "Not connected. Run 'gigai pair <code> <server-url>' to set up.";
751
+ }
752
+ const lines = [];
753
+ for (const name of serverNames) {
754
+ const entry = config.servers[name];
755
+ const active = name === config.activeServer ? " (active)" : "";
756
+ lines.push(` ${name}${active} ${entry.server}`);
757
+ if (entry.sessionExpiresAt) {
758
+ const remaining = entry.sessionExpiresAt - Date.now();
759
+ if (remaining > 0) {
760
+ lines.push(` Session expires in ${Math.floor(remaining / 6e4)} minutes`);
761
+ } else {
762
+ lines.push(" Session expired \u2014 will auto-renew on next command");
763
+ }
764
+ }
765
+ }
766
+ return `Servers:
767
+ ${lines.join("\n")}`;
768
+ }
769
+
770
+ // src/index.ts
771
+ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
772
+ "pair",
773
+ "connect",
774
+ "list",
775
+ "help",
776
+ "status",
777
+ "upload",
778
+ "download",
779
+ "version",
780
+ "--help",
781
+ "-h"
782
+ ]);
783
+ var firstArg = process.argv[2];
784
+ if (firstArg && !firstArg.startsWith("-") && !KNOWN_COMMANDS.has(firstArg)) {
785
+ const toolName = firstArg;
786
+ const toolArgs = process.argv.slice(3);
787
+ try {
788
+ const { serverUrl, sessionToken } = await connect();
789
+ const http = createHttpClient(serverUrl, sessionToken);
790
+ const { tool: detail } = await fetchToolDetail(http, toolName);
791
+ if (detail.type === "mcp") {
792
+ const mcpToolName = toolArgs[0];
793
+ if (!mcpToolName) {
794
+ const toolNames = (detail.mcpTools ?? []).map((t) => ` ${t.name} \u2014 ${t.description}`);
795
+ console.log(`MCP tools for ${toolName}:
796
+ ${toolNames.join("\n")}`);
797
+ } else {
798
+ const jsonArg = toolArgs.slice(1).join(" ");
799
+ const args = jsonArg ? JSON.parse(jsonArg) : {};
800
+ await execMcpTool(http, toolName, mcpToolName, args);
801
+ }
802
+ } else {
803
+ await execTool(http, toolName, toolArgs);
804
+ }
805
+ } catch (e) {
806
+ console.error(`Error: ${e.message}`);
807
+ process.exitCode = 1;
808
+ }
809
+ } else {
810
+ runCitty();
811
+ }
812
+ function runCitty() {
813
+ const pairCommand = defineCommand({
814
+ meta: { name: "pair", description: "Pair with a gigai server" },
815
+ args: {
816
+ code: { type: "positional", description: "Pairing code", required: true },
817
+ server: { type: "positional", description: "Server URL", required: true }
818
+ },
819
+ async run({ args }) {
820
+ await pair(args.code, args.server);
821
+ }
822
+ });
823
+ const connectCommand = defineCommand({
824
+ meta: { name: "connect", description: "Establish a session with the server" },
825
+ args: {
826
+ name: { type: "positional", description: "Server name (optional)", required: false }
827
+ },
828
+ async run({ args }) {
829
+ const { serverUrl } = await connect(args.name);
830
+ console.log(`Connected to ${serverUrl}`);
831
+ }
832
+ });
833
+ const listCommand = defineCommand({
834
+ meta: { name: "list", description: "List available tools" },
835
+ async run() {
836
+ const { serverUrl, sessionToken } = await connect();
837
+ const http = createHttpClient(serverUrl, sessionToken);
838
+ const tools = await fetchTools(http);
839
+ console.log(formatToolList(tools));
840
+ }
841
+ });
842
+ const helpCommand = defineCommand({
843
+ meta: { name: "help", description: "Show help for a tool" },
844
+ args: {
845
+ tool: { type: "positional", description: "Tool name", required: true }
846
+ },
847
+ async run({ args }) {
848
+ const { serverUrl, sessionToken } = await connect();
849
+ const http = createHttpClient(serverUrl, sessionToken);
850
+ const { tool } = await fetchToolDetail(http, args.tool);
851
+ console.log(formatToolDetail(tool));
852
+ }
853
+ });
854
+ const statusCommand = defineCommand({
855
+ meta: { name: "status", description: "Show connection status" },
856
+ async run() {
857
+ const config = await readConfig();
858
+ console.log(formatStatus(config));
859
+ }
860
+ });
861
+ const uploadCommand = defineCommand({
862
+ meta: { name: "upload", description: "Upload a file to the server" },
863
+ args: {
864
+ file: { type: "positional", description: "File path", required: true }
865
+ },
866
+ async run({ args }) {
867
+ const { serverUrl, sessionToken } = await connect();
868
+ const http = createHttpClient(serverUrl, sessionToken);
869
+ await upload(http, args.file);
870
+ }
871
+ });
872
+ const downloadCommand = defineCommand({
873
+ meta: { name: "download", description: "Download a file from the server" },
874
+ args: {
875
+ id: { type: "positional", description: "Transfer ID", required: true },
876
+ dest: { type: "positional", description: "Destination path", required: true }
877
+ },
878
+ async run({ args }) {
879
+ const { serverUrl, sessionToken } = await connect();
880
+ const http = createHttpClient(serverUrl, sessionToken);
881
+ await download(http, args.id, args.dest);
882
+ }
883
+ });
884
+ const versionCommand = defineCommand({
885
+ meta: { name: "version", description: "Show version" },
886
+ run() {
887
+ console.log(`gigai v${VERSION} (kon)`);
888
+ }
889
+ });
890
+ const main = defineCommand({
891
+ meta: {
892
+ name: "gigai",
893
+ version: VERSION,
894
+ description: "gigai client \u2014 bridge CLI tools to Claude"
895
+ },
896
+ subCommands: {
897
+ pair: pairCommand,
898
+ connect: connectCommand,
899
+ list: listCommand,
900
+ help: helpCommand,
901
+ status: statusCommand,
902
+ upload: uploadCommand,
903
+ download: downloadCommand,
904
+ version: versionCommand
905
+ }
906
+ });
907
+ runMain(main);
908
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@schuttdev/kon",
3
+ "version": "0.2.6",
4
+ "type": "module",
5
+ "description": "Lightweight gigai client for Claude code execution",
6
+ "bin": {
7
+ "gigai": "dist/index.js"
8
+ },
9
+ "exports": {
10
+ ".": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "clean": "rm -rf dist"
18
+ },
19
+ "dependencies": {
20
+ "citty": "^0.1.6",
21
+ "zod": "^3.22.0",
22
+ "undici": "^6.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@gigai/shared": "*",
26
+ "tsup": "^8.0.1",
27
+ "typescript": "^5.3.3"
28
+ }
29
+ }