@schuttdev/kon 0.2.9 → 0.3.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 (2) hide show
  1. package/dist/index.js +362 -47
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -276,6 +276,26 @@ function createHttpClient(serverUrl, sessionToken) {
276
276
  body: body ? JSON.stringify(body) : void 0
277
277
  });
278
278
  },
279
+ async delete(path) {
280
+ const headers = {};
281
+ if (sessionToken) {
282
+ headers["Authorization"] = `Bearer ${sessionToken}`;
283
+ }
284
+ const dispatcher = await ensureDispatcher();
285
+ const fetchOpts = { method: "DELETE", headers };
286
+ if (dispatcher) {
287
+ fetchOpts.dispatcher = dispatcher;
288
+ }
289
+ const res = await fetch(`${baseUrl}${path}`, fetchOpts);
290
+ if (!res.ok) {
291
+ let errorBody;
292
+ try {
293
+ errorBody = await res.json();
294
+ } catch {
295
+ }
296
+ throw new Error(errorBody?.error?.message ?? `HTTP ${res.status}`);
297
+ }
298
+ },
279
299
  async postMultipart(path, formData) {
280
300
  const headers = {};
281
301
  if (sessionToken) {
@@ -321,7 +341,7 @@ function createHttpClient(serverUrl, sessionToken) {
321
341
  }
322
342
 
323
343
  // ../cli/src/version.ts
324
- var VERSION = "0.2.9";
344
+ var VERSION = "0.3.0";
325
345
 
326
346
  // ../cli/src/connect.ts
327
347
  async function connect(serverName) {
@@ -361,6 +381,17 @@ async function checkAndUpdateServer(serverUrl, sessionToken) {
361
381
  try {
362
382
  const http = createHttpClient(serverUrl);
363
383
  const health = await http.get("/health");
384
+ if (health.platform || health.hostname) {
385
+ const config = await readConfig();
386
+ for (const entry of Object.values(config.servers)) {
387
+ if (normalizeUrl2(entry.server) === normalizeUrl2(serverUrl)) {
388
+ entry.platform = health.platform;
389
+ entry.hostname = health.hostname;
390
+ break;
391
+ }
392
+ }
393
+ await writeConfig(config);
394
+ }
364
395
  if (isNewer(VERSION, health.version)) {
365
396
  console.log(`Server is outdated (${health.version} \u2192 ${VERSION}). Updating...`);
366
397
  const authedHttp = createHttpClient(serverUrl, sessionToken);
@@ -389,6 +420,13 @@ async function waitForServer(serverUrl, timeoutMs) {
389
420
  }
390
421
  }
391
422
  }
423
+ function normalizeUrl2(url) {
424
+ try {
425
+ return new URL(url).hostname.toLowerCase();
426
+ } catch {
427
+ return url.toLowerCase();
428
+ }
429
+ }
392
430
  function isNewer(client, server) {
393
431
  const parse = (v) => {
394
432
  const [core, pre] = v.replace(/^v/, "").split("-");
@@ -407,8 +445,40 @@ function isNewer(client, server) {
407
445
  return false;
408
446
  }
409
447
 
410
- // ../cli/src/skill.ts
448
+ // ../cli/src/discover.ts
411
449
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
450
+ import { join as join2 } from "path";
451
+ import { homedir as homedir2 } from "os";
452
+ var MANIFEST_TTL = 5 * 60 * 1e3;
453
+ function getManifestPath() {
454
+ const dir = process.env.GIGAI_CONFIG_DIR ?? join2(homedir2(), ".gigai");
455
+ return join2(dir, "tool-manifest.json");
456
+ }
457
+ async function fetchTools(http) {
458
+ try {
459
+ const raw = await readFile2(getManifestPath(), "utf8");
460
+ const cache = JSON.parse(raw);
461
+ if (Date.now() - cache.fetchedAt < MANIFEST_TTL) {
462
+ return cache.tools;
463
+ }
464
+ } catch {
465
+ }
466
+ const res = await http.get("/tools");
467
+ try {
468
+ const dir = process.env.GIGAI_CONFIG_DIR ?? join2(homedir2(), ".gigai");
469
+ await mkdir2(dir, { recursive: true });
470
+ const cache = { tools: res.tools, fetchedAt: Date.now() };
471
+ await writeFile2(getManifestPath(), JSON.stringify(cache));
472
+ } catch {
473
+ }
474
+ return res.tools;
475
+ }
476
+ async function fetchToolDetail(http, name) {
477
+ return http.get(`/tools/${encodeURIComponent(name)}`);
478
+ }
479
+
480
+ // ../cli/src/skill.ts
481
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
412
482
  import "path";
413
483
  var SKILL_MD = `---
414
484
  name: gigai
@@ -505,32 +575,151 @@ For MCP tools, the first arg is the MCP tool name:
505
575
  kon <mcp-tool> <mcp-action> [json-args]
506
576
  \`\`\`
507
577
 
578
+ ## Scheduling tasks
579
+
580
+ Schedule any tool execution on the server:
581
+ \`\`\`bash
582
+ kon cron add "0 9 * * *" bash git pull # daily at 9am
583
+ kon cron add --at "9:00 AM tomorrow" bash git pull # one-shot
584
+ kon cron add --at "in 30 minutes" read ~/log.txt # relative time
585
+ kon cron list # list scheduled jobs
586
+ kon cron remove <id> # remove a job
587
+ \`\`\`
588
+
508
589
  ## Multiple servers
509
590
 
591
+ The user may have multiple servers configured (e.g. a Mac and a Linux machine). Use \`kon status\` to see all servers and which is active.
592
+
510
593
  \`\`\`bash
511
- kon connect <server-name>
512
- kon status
594
+ kon status # show all servers + active
595
+ kon connect <server-name> # switch to a different server
596
+ kon list # list tools on the current server
513
597
  \`\`\`
514
598
 
599
+ **Routing between servers:** When a tool is not available on the current server, or when a task requires a specific platform (e.g. iMessage requires macOS), switch to the appropriate server:
600
+
601
+ 1. Run \`kon status\` to see available servers
602
+ 2. Run \`kon connect <server-name>\` to switch
603
+ 3. Run \`kon list\` to verify the tool is available
604
+ 4. Execute the command
605
+
606
+ Platform-specific capabilities:
607
+ - **iMessage**: only available on macOS servers
608
+ - **macOS apps** (Shortcuts, AppleScript): only on macOS servers
609
+ - **systemd, apt, etc.**: only on Linux servers
610
+
611
+ The \`kon list\` response includes the server's platform. Use this to determine which server to route a request to.
612
+
515
613
  ## Important
516
614
 
517
615
  - Always run the setup block before first use in a new conversation
518
616
  - All commands execute on the **user's machine**, not in this sandbox
519
617
  - If you get auth errors, run \`kon connect\` to refresh the session
520
618
  - Tools are scoped to what the user has configured \u2014 if a tool is missing, tell the user
619
+ - If you have multiple servers, check which server has the tool you need before executing
521
620
  `;
522
621
  async function hasExistingSkill() {
523
622
  try {
524
- await readFile2("/mnt/skills/user/gigai/config.json", "utf8");
623
+ await readFile3("/mnt/skills/user/gigai/config.json", "utf8");
525
624
  return true;
526
625
  } catch {
527
626
  return false;
528
627
  }
529
628
  }
530
- async function generateSkillZip(serverName, serverUrl, token) {
629
+ function generateToolMarkdown(tool) {
630
+ const lines = [];
631
+ lines.push("---");
632
+ lines.push(`name: ${tool.name}`);
633
+ lines.push(`description: ${tool.description}`);
634
+ lines.push("---");
635
+ lines.push("");
636
+ lines.push(`# ${tool.name}`);
637
+ lines.push("");
638
+ lines.push(`**Type:** ${tool.type}`);
639
+ lines.push("");
640
+ lines.push(tool.description);
641
+ lines.push("");
642
+ lines.push("## Usage");
643
+ lines.push("");
644
+ if (tool.type === "builtin") {
645
+ switch (tool.name) {
646
+ case "read":
647
+ lines.push("```bash");
648
+ lines.push("kon read <file> [offset] [limit]");
649
+ lines.push("```");
650
+ break;
651
+ case "write":
652
+ lines.push("```bash");
653
+ lines.push("kon write <file> <content>");
654
+ lines.push("```");
655
+ break;
656
+ case "edit":
657
+ lines.push("```bash");
658
+ lines.push("kon edit <file> <old_string> <new_string> [--all]");
659
+ lines.push("```");
660
+ break;
661
+ case "glob":
662
+ lines.push("```bash");
663
+ lines.push("kon glob <pattern> [path]");
664
+ lines.push("```");
665
+ break;
666
+ case "grep":
667
+ lines.push("```bash");
668
+ lines.push("kon grep <pattern> [path] [--glob <filter>] [-i] [-n] [-C <num>]");
669
+ lines.push("```");
670
+ break;
671
+ case "bash":
672
+ lines.push("```bash");
673
+ lines.push("kon bash <command> [args...]");
674
+ lines.push("```");
675
+ break;
676
+ default:
677
+ lines.push("```bash");
678
+ lines.push(`kon ${tool.name} [args...]`);
679
+ lines.push("```");
680
+ }
681
+ } else if (tool.type === "mcp") {
682
+ lines.push("```bash");
683
+ lines.push(`kon ${tool.name} <mcp-tool-name> [json-args]`);
684
+ lines.push("```");
685
+ } else {
686
+ lines.push("```bash");
687
+ lines.push(`kon ${tool.name} [args...]`);
688
+ lines.push("```");
689
+ }
690
+ lines.push("");
691
+ if (tool.args && tool.args.length > 0) {
692
+ lines.push("## Arguments");
693
+ lines.push("");
694
+ for (const arg of tool.args) {
695
+ const req = arg.required ? " *(required)*" : "";
696
+ const def = arg.default ? ` (default: \`${arg.default}\`)` : "";
697
+ lines.push(`- \`${arg.name}\`${req}: ${arg.description}${def}`);
698
+ }
699
+ lines.push("");
700
+ }
701
+ if (tool.type === "mcp" && tool.mcpTools && tool.mcpTools.length > 0) {
702
+ lines.push("## Available MCP Tools");
703
+ lines.push("");
704
+ for (const mcpTool of tool.mcpTools) {
705
+ lines.push(`### ${mcpTool.name}`);
706
+ lines.push("");
707
+ lines.push(mcpTool.description);
708
+ lines.push("");
709
+ lines.push("**Input Schema:**");
710
+ lines.push("");
711
+ lines.push("```json");
712
+ lines.push(JSON.stringify(mcpTool.inputSchema, null, 2));
713
+ lines.push("```");
714
+ lines.push("");
715
+ }
716
+ }
717
+ return lines.join("\n");
718
+ }
719
+ async function generateSkillZip(serverName, serverUrl, token, tools) {
531
720
  let skillConfig = { servers: {} };
532
721
  try {
533
- const raw = await readFile2("/mnt/skills/user/gigai/config.json", "utf8");
722
+ const raw = await readFile3("/mnt/skills/user/gigai/config.json", "utf8");
534
723
  const existing = JSON.parse(raw);
535
724
  if (existing.servers) {
536
725
  skillConfig = existing;
@@ -551,21 +740,31 @@ async function generateSkillZip(serverName, serverUrl, token) {
551
740
  skillConfig.activeServer = serverName;
552
741
  }
553
742
  const configJson = JSON.stringify(skillConfig, null, 2) + "\n";
554
- return createZip([
743
+ const entries = [
555
744
  { path: "gigai/SKILL.md", data: Buffer.from(SKILL_MD, "utf8") },
556
745
  { path: "gigai/config.json", data: Buffer.from(configJson, "utf8") }
557
- ]);
746
+ ];
747
+ if (tools && tools.length > 0) {
748
+ for (const tool of tools) {
749
+ const md = generateToolMarkdown(tool);
750
+ entries.push({
751
+ path: `gigai/tools/${tool.name}.md`,
752
+ data: Buffer.from(md, "utf8")
753
+ });
754
+ }
755
+ }
756
+ return createZip(entries);
558
757
  }
559
758
  async function writeSkillZip(zip) {
560
759
  const outputsDir = "/mnt/user-data/outputs";
561
760
  try {
562
- await mkdir2(outputsDir, { recursive: true });
761
+ await mkdir3(outputsDir, { recursive: true });
563
762
  const outPath = `${outputsDir}/gigai.zip`;
564
- await writeFile2(outPath, zip);
763
+ await writeFile3(outPath, zip);
565
764
  return outPath;
566
765
  } catch {
567
766
  const outPath = "gigai.zip";
568
- await writeFile2(outPath, zip);
767
+ await writeFile3(outPath, zip);
569
768
  return outPath;
570
769
  }
571
770
  }
@@ -655,8 +854,21 @@ async function pair(code, serverUrl) {
655
854
  });
656
855
  await addServer(res.serverName, serverUrl, res.encryptedToken);
657
856
  console.log(`Paired with "${res.serverName}" successfully!`);
857
+ let toolDetails;
858
+ try {
859
+ const session = await connect();
860
+ const authedHttp = createHttpClient(session.serverUrl, session.sessionToken);
861
+ const tools = await fetchTools(authedHttp);
862
+ toolDetails = await Promise.all(
863
+ tools.map(async (t) => {
864
+ const { tool } = await fetchToolDetail(authedHttp, t.name);
865
+ return tool;
866
+ })
867
+ );
868
+ } catch {
869
+ }
658
870
  const existing = await hasExistingSkill();
659
- const zip = await generateSkillZip(res.serverName, serverUrl, res.encryptedToken);
871
+ const zip = await generateSkillZip(res.serverName, serverUrl, res.encryptedToken, toolDetails);
660
872
  const outPath = await writeSkillZip(zip);
661
873
  console.log(`
662
874
  Skill zip written to: ${outPath}`);
@@ -667,38 +879,6 @@ Skill zip written to: ${outPath}`);
667
879
  }
668
880
  }
669
881
 
670
- // ../cli/src/discover.ts
671
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
672
- import { join as join2 } from "path";
673
- import { homedir as homedir2 } from "os";
674
- var MANIFEST_TTL = 5 * 60 * 1e3;
675
- function getManifestPath() {
676
- const dir = process.env.GIGAI_CONFIG_DIR ?? join2(homedir2(), ".gigai");
677
- return join2(dir, "tool-manifest.json");
678
- }
679
- async function fetchTools(http) {
680
- try {
681
- const raw = await readFile3(getManifestPath(), "utf8");
682
- const cache = JSON.parse(raw);
683
- if (Date.now() - cache.fetchedAt < MANIFEST_TTL) {
684
- return cache.tools;
685
- }
686
- } catch {
687
- }
688
- const res = await http.get("/tools");
689
- try {
690
- const dir = process.env.GIGAI_CONFIG_DIR ?? join2(homedir2(), ".gigai");
691
- await mkdir3(dir, { recursive: true });
692
- const cache = { tools: res.tools, fetchedAt: Date.now() };
693
- await writeFile3(getManifestPath(), JSON.stringify(cache));
694
- } catch {
695
- }
696
- return res.tools;
697
- }
698
- async function fetchToolDetail(http, name) {
699
- return http.get(`/tools/${encodeURIComponent(name)}`);
700
- }
701
-
702
882
  // ../cli/src/exec.ts
703
883
  async function execTool(http, name, args, timeout) {
704
884
  const res = await http.post("/exec", {
@@ -800,7 +980,8 @@ function formatStatus(config) {
800
980
  for (const name of serverNames) {
801
981
  const entry = config.servers[name];
802
982
  const active = name === config.activeServer ? " (active)" : "";
803
- lines.push(` ${name}${active} ${entry.server}`);
983
+ const platformTag = entry.platform ? ` [${entry.platform}]` : "";
984
+ lines.push(` ${name}${active}${platformTag} ${entry.server}`);
804
985
  if (entry.sessionExpiresAt) {
805
986
  const remaining = entry.sessionExpiresAt - Date.now();
806
987
  if (remaining > 0) {
@@ -824,6 +1005,8 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
824
1005
  "upload",
825
1006
  "download",
826
1007
  "version",
1008
+ "skill",
1009
+ "cron",
827
1010
  "--help",
828
1011
  "-h"
829
1012
  ]);
@@ -934,6 +1117,136 @@ function runCitty() {
934
1117
  console.log(`kon v${VERSION}`);
935
1118
  }
936
1119
  });
1120
+ const skillCommand = defineCommand({
1121
+ meta: { name: "skill", description: "Regenerate the skill zip with current tool details" },
1122
+ async run() {
1123
+ const { serverUrl, sessionToken } = await connect();
1124
+ const http = createHttpClient(serverUrl, sessionToken);
1125
+ const tools = await fetchTools(http);
1126
+ console.log(`Fetching details for ${tools.length} tool(s)...`);
1127
+ const toolDetails = await Promise.all(
1128
+ tools.map(async (t) => {
1129
+ const { tool } = await fetchToolDetail(http, t.name);
1130
+ return tool;
1131
+ })
1132
+ );
1133
+ const config = await readConfig();
1134
+ const activeServer = config.activeServer;
1135
+ if (!activeServer || !config.servers[activeServer]) {
1136
+ throw new Error("No active server. Run 'kon connect' first.");
1137
+ }
1138
+ const entry = config.servers[activeServer];
1139
+ const zip = await generateSkillZip(activeServer, entry.server, entry.token, toolDetails);
1140
+ const outPath = await writeSkillZip(zip);
1141
+ console.log(`
1142
+ Skill zip written to: ${outPath}`);
1143
+ console.log(`Included ${toolDetails.length} tool documentation file(s).`);
1144
+ console.log("Upload this file as a skill in Claude (Settings \u2192 Customize \u2192 Upload Skill).");
1145
+ }
1146
+ });
1147
+ const cronAddCommand = defineCommand({
1148
+ meta: { name: "add", description: "Schedule a tool execution" },
1149
+ args: {
1150
+ at: { type: "string", description: "Human-readable time (e.g. '9:00 AM tomorrow')" }
1151
+ },
1152
+ async run({ args }) {
1153
+ const { serverUrl, sessionToken } = await connect();
1154
+ const http = createHttpClient(serverUrl, sessionToken);
1155
+ const rawArgs = process.argv.slice(4);
1156
+ const positional = [];
1157
+ let atValue = args.at;
1158
+ for (let i = 0; i < rawArgs.length; i++) {
1159
+ if (rawArgs[i] === "--at" && rawArgs[i + 1]) {
1160
+ atValue = rawArgs[i + 1];
1161
+ i++;
1162
+ } else if (!rawArgs[i].startsWith("--")) {
1163
+ positional.push(rawArgs[i]);
1164
+ }
1165
+ }
1166
+ let schedule;
1167
+ let tool;
1168
+ let toolArgs;
1169
+ let oneShot = false;
1170
+ if (atValue) {
1171
+ tool = positional[0];
1172
+ toolArgs = positional.slice(1);
1173
+ const res2 = await http.post("/cron", {
1174
+ schedule: `@at ${atValue}`,
1175
+ tool,
1176
+ args: toolArgs,
1177
+ oneShot: true
1178
+ });
1179
+ console.log(`Scheduled: ${res2.job.id}`);
1180
+ console.log(` ${tool} ${toolArgs.join(" ")}`);
1181
+ if (res2.job.nextRun) {
1182
+ console.log(` Next run: ${new Date(res2.job.nextRun).toLocaleString()}`);
1183
+ }
1184
+ return;
1185
+ }
1186
+ schedule = positional[0];
1187
+ tool = positional[1];
1188
+ toolArgs = positional.slice(2);
1189
+ if (!schedule || !tool) {
1190
+ console.error("Usage:");
1191
+ console.error(' kon cron add "0 9 * * *" <tool> [args...]');
1192
+ console.error(' kon cron add --at "9:00 AM tomorrow" <tool> [args...]');
1193
+ process.exitCode = 1;
1194
+ return;
1195
+ }
1196
+ const res = await http.post("/cron", {
1197
+ schedule,
1198
+ tool,
1199
+ args: toolArgs
1200
+ });
1201
+ console.log(`Scheduled: ${res.job.id}`);
1202
+ console.log(` ${schedule} \u2014 ${tool} ${toolArgs.join(" ")}`);
1203
+ if (res.job.nextRun) {
1204
+ console.log(` Next run: ${new Date(res.job.nextRun).toLocaleString()}`);
1205
+ }
1206
+ }
1207
+ });
1208
+ const cronListCommand = defineCommand({
1209
+ meta: { name: "list", description: "List scheduled jobs" },
1210
+ async run() {
1211
+ const { serverUrl, sessionToken } = await connect();
1212
+ const http = createHttpClient(serverUrl, sessionToken);
1213
+ const res = await http.get("/cron");
1214
+ if (res.jobs.length === 0) {
1215
+ console.log("No scheduled jobs.");
1216
+ return;
1217
+ }
1218
+ for (const job of res.jobs) {
1219
+ const status = job.enabled ? "active" : "disabled";
1220
+ const cmd = `${job.tool} ${job.args.join(" ")}`.trim();
1221
+ const next = job.nextRun ? new Date(job.nextRun).toLocaleString() : "\u2014";
1222
+ const last = job.lastRun ? new Date(job.lastRun).toLocaleString() : "never";
1223
+ console.log(`${job.id} [${status}] ${job.schedule}`);
1224
+ console.log(` ${cmd}`);
1225
+ console.log(` next: ${next} last: ${last}`);
1226
+ console.log();
1227
+ }
1228
+ }
1229
+ });
1230
+ const cronRemoveCommand = defineCommand({
1231
+ meta: { name: "remove", description: "Remove a scheduled job" },
1232
+ args: {
1233
+ id: { type: "positional", description: "Job ID", required: true }
1234
+ },
1235
+ async run({ args }) {
1236
+ const { serverUrl, sessionToken } = await connect();
1237
+ const http = createHttpClient(serverUrl, sessionToken);
1238
+ await http.delete(`/cron/${encodeURIComponent(args.id)}`);
1239
+ console.log(`Removed: ${args.id}`);
1240
+ }
1241
+ });
1242
+ const cronCommand = defineCommand({
1243
+ meta: { name: "cron", description: "Manage scheduled tasks" },
1244
+ subCommands: {
1245
+ add: cronAddCommand,
1246
+ list: cronListCommand,
1247
+ remove: cronRemoveCommand
1248
+ }
1249
+ });
937
1250
  const main = defineCommand({
938
1251
  meta: {
939
1252
  name: "kon",
@@ -948,7 +1261,9 @@ function runCitty() {
948
1261
  status: statusCommand,
949
1262
  upload: uploadCommand,
950
1263
  download: downloadCommand,
951
- version: versionCommand
1264
+ version: versionCommand,
1265
+ skill: skillCommand,
1266
+ cron: cronCommand
952
1267
  }
953
1268
  });
954
1269
  runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schuttdev/kon",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Lightweight gigai client for Claude code execution",
6
6
  "bin": {