@schuttdev/kon 0.2.8 → 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 +420 -58
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -136,7 +136,7 @@ var ScriptToolConfigSchema = z.object({
136
136
  var BuiltinToolConfigSchema = z.object({
137
137
  type: z.literal("builtin"),
138
138
  name: z.string(),
139
- builtin: z.enum(["filesystem", "shell"]),
139
+ builtin: z.enum(["filesystem", "shell", "read", "write", "edit", "glob", "grep", "bash"]),
140
140
  description: z.string(),
141
141
  config: z.record(z.unknown()).optional()
142
142
  });
@@ -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.8";
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
@@ -430,34 +500,61 @@ kon connect
430
500
 
431
501
  If \`kon connect\` succeeds, you are ready to use tools. If it fails, tell the user.
432
502
 
433
- ## Usage
503
+ ## Discovering tools
434
504
 
435
- ### List available tools
505
+ List all available tools:
436
506
  \`\`\`bash
437
507
  kon list
438
508
  \`\`\`
439
509
 
440
- ### Run a tool
510
+ Get detailed help for a specific tool:
441
511
  \`\`\`bash
442
- kon <tool-name> [args...]
512
+ kon help <tool-name>
443
513
  \`\`\`
444
514
 
445
- Examples:
515
+ ## Core tools
516
+
517
+ These tools may be available depending on the server's configuration. Run \`kon list\` to see what's enabled.
518
+
519
+ ### read \u2014 Read file contents
446
520
  \`\`\`bash
447
- kon shell date
448
- kon fs list Documents
449
- kon shell whoami
521
+ kon read <file> [offset] [limit]
450
522
  \`\`\`
523
+ - \`offset\`: start from this line number (0-based)
524
+ - \`limit\`: max number of lines to return
451
525
 
452
- ### Get help for a tool
526
+ ### write \u2014 Write content to a file
453
527
  \`\`\`bash
454
- kon help <tool-name>
528
+ kon write <file> <content>
455
529
  \`\`\`
456
530
 
457
- ### Switch server (if multiple are configured)
531
+ ### edit \u2014 Replace text in a file
458
532
  \`\`\`bash
459
- kon connect <server-name>
533
+ kon edit <file> <old_string> <new_string> [--all]
460
534
  \`\`\`
535
+ - Without \`--all\`: fails if old_string matches multiple locations (provide more context)
536
+ - With \`--all\`: replaces every occurrence
537
+
538
+ ### glob \u2014 Find files by pattern
539
+ \`\`\`bash
540
+ kon glob <pattern> [path]
541
+ \`\`\`
542
+ - Supports \`*\`, \`**\`, \`?\`, \`{a,b}\` syntax
543
+ - Example: \`kon glob "**/*.ts" ~/projects/myapp\`
544
+
545
+ ### grep \u2014 Search file contents
546
+ \`\`\`bash
547
+ kon grep <pattern> [path] [--glob <filter>] [-i] [-n] [-C <num>]
548
+ \`\`\`
549
+ - Uses ripgrep if available, falls back to built-in search
550
+ - Example: \`kon grep "TODO" ~/projects --glob "*.ts"\`
551
+
552
+ ### bash \u2014 Execute shell commands
553
+ \`\`\`bash
554
+ kon bash <command> [args...]
555
+ \`\`\`
556
+ - Commands are restricted to the server's allowlist
557
+ - Example: \`kon bash git status\`
461
558
 
462
559
  ### File transfer
463
560
  \`\`\`bash
@@ -465,25 +562,164 @@ kon upload <file>
465
562
  kon download <id> <dest>
466
563
  \`\`\`
467
564
 
565
+ ## Other tools
566
+
567
+ The server may have additional tools registered (CLI commands, MCP servers, scripts). Any unknown subcommand is treated as a tool name:
568
+
569
+ \`\`\`bash
570
+ kon <tool-name> [args...]
571
+ \`\`\`
572
+
573
+ For MCP tools, the first arg is the MCP tool name:
574
+ \`\`\`bash
575
+ kon <mcp-tool> <mcp-action> [json-args]
576
+ \`\`\`
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
+
589
+ ## Multiple servers
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
+
593
+ \`\`\`bash
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
597
+ \`\`\`
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
+
468
613
  ## Important
469
614
 
470
615
  - Always run the setup block before first use in a new conversation
471
616
  - All commands execute on the **user's machine**, not in this sandbox
472
617
  - If you get auth errors, run \`kon connect\` to refresh the session
473
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
474
620
  `;
475
621
  async function hasExistingSkill() {
476
622
  try {
477
- await readFile2("/mnt/skills/user/gigai/config.json", "utf8");
623
+ await readFile3("/mnt/skills/user/gigai/config.json", "utf8");
478
624
  return true;
479
625
  } catch {
480
626
  return false;
481
627
  }
482
628
  }
483
- 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) {
484
720
  let skillConfig = { servers: {} };
485
721
  try {
486
- const raw = await readFile2("/mnt/skills/user/gigai/config.json", "utf8");
722
+ const raw = await readFile3("/mnt/skills/user/gigai/config.json", "utf8");
487
723
  const existing = JSON.parse(raw);
488
724
  if (existing.servers) {
489
725
  skillConfig = existing;
@@ -504,21 +740,31 @@ async function generateSkillZip(serverName, serverUrl, token) {
504
740
  skillConfig.activeServer = serverName;
505
741
  }
506
742
  const configJson = JSON.stringify(skillConfig, null, 2) + "\n";
507
- return createZip([
743
+ const entries = [
508
744
  { path: "gigai/SKILL.md", data: Buffer.from(SKILL_MD, "utf8") },
509
745
  { path: "gigai/config.json", data: Buffer.from(configJson, "utf8") }
510
- ]);
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);
511
757
  }
512
758
  async function writeSkillZip(zip) {
513
759
  const outputsDir = "/mnt/user-data/outputs";
514
760
  try {
515
- await mkdir2(outputsDir, { recursive: true });
761
+ await mkdir3(outputsDir, { recursive: true });
516
762
  const outPath = `${outputsDir}/gigai.zip`;
517
- await writeFile2(outPath, zip);
763
+ await writeFile3(outPath, zip);
518
764
  return outPath;
519
765
  } catch {
520
766
  const outPath = "gigai.zip";
521
- await writeFile2(outPath, zip);
767
+ await writeFile3(outPath, zip);
522
768
  return outPath;
523
769
  }
524
770
  }
@@ -608,8 +854,21 @@ async function pair(code, serverUrl) {
608
854
  });
609
855
  await addServer(res.serverName, serverUrl, res.encryptedToken);
610
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
+ }
611
870
  const existing = await hasExistingSkill();
612
- const zip = await generateSkillZip(res.serverName, serverUrl, res.encryptedToken);
871
+ const zip = await generateSkillZip(res.serverName, serverUrl, res.encryptedToken, toolDetails);
613
872
  const outPath = await writeSkillZip(zip);
614
873
  console.log(`
615
874
  Skill zip written to: ${outPath}`);
@@ -620,38 +879,6 @@ Skill zip written to: ${outPath}`);
620
879
  }
621
880
  }
622
881
 
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
882
  // ../cli/src/exec.ts
656
883
  async function execTool(http, name, args, timeout) {
657
884
  const res = await http.post("/exec", {
@@ -753,7 +980,8 @@ function formatStatus(config) {
753
980
  for (const name of serverNames) {
754
981
  const entry = config.servers[name];
755
982
  const active = name === config.activeServer ? " (active)" : "";
756
- lines.push(` ${name}${active} ${entry.server}`);
983
+ const platformTag = entry.platform ? ` [${entry.platform}]` : "";
984
+ lines.push(` ${name}${active}${platformTag} ${entry.server}`);
757
985
  if (entry.sessionExpiresAt) {
758
986
  const remaining = entry.sessionExpiresAt - Date.now();
759
987
  if (remaining > 0) {
@@ -777,6 +1005,8 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
777
1005
  "upload",
778
1006
  "download",
779
1007
  "version",
1008
+ "skill",
1009
+ "cron",
780
1010
  "--help",
781
1011
  "-h"
782
1012
  ]);
@@ -887,6 +1117,136 @@ function runCitty() {
887
1117
  console.log(`kon v${VERSION}`);
888
1118
  }
889
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
+ });
890
1250
  const main = defineCommand({
891
1251
  meta: {
892
1252
  name: "kon",
@@ -901,7 +1261,9 @@ function runCitty() {
901
1261
  status: statusCommand,
902
1262
  upload: uploadCommand,
903
1263
  download: downloadCommand,
904
- version: versionCommand
1264
+ version: versionCommand,
1265
+ skill: skillCommand,
1266
+ cron: cronCommand
905
1267
  }
906
1268
  });
907
1269
  runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schuttdev/kon",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Lightweight gigai client for Claude code execution",
6
6
  "bin": {