This is the full developer documentation for Kunkun # Kunkun > An open source, extensible, cross-platform app launcher. Alternative to Alfred and Raycast. import { Card, CardGrid } from "@astrojs/starlight/components"; Supports macOS, Windows, and Linux. Extensions are designed to be write-once, run-anywhere. Create custom extensions using JavaScript. An extension store is available for sharing and discovering extensions. You can also install extensions freely from any source. Extensions are designed with controlled access to your system, requesting only the permissions necessary for their operation. You have the ability to examine both application and extension code.
# Creation of Kunkun > Why did I create Kunkun? > Kunkun is designed to be a cross-platform and open source alternative for apps like Alfred, Raycast, uTools. I will discuss them in this blog. These are very successful and popular products that I have used and loved. Kunkun was created to solve some of the problems I had with these products. ## Discussion ### Cross-platform - Alfred and Raycast are macOS only - uTools is cross-platform but doesn't seem to have good extension support for Linux Mac native apps usually look better than that of the native apps on Windows and Linux. However I really wish all good apps were cross-platform. I use all three operating systems and I want to use the same app on all of them. Electron (used by uTools) is a popular choice for cross-platform apps, but it is controversial because of its resource usage. Bundling a whole chromium browser and NodeJS runtime for each Electron app doesn't sound like a good idea to me. I don't like running multiple browsers simultaneously. ### Security Concerns - **Closed Source Applications**: All three products are closed source. Given that these applications typically require access to the entire disk, an open source alternative would be preferable for transparency and security. - **Trust in Extensions**: When we install an application, we trust it, but do we really trust the extensions we install? Installing an extension is significantly easier than installing a regular application. Installing extensions from the extension store of an app we trust may let our guard down. You may trust the app developers, but do you trust the extension developers? - **NodeJS Runtime Risks**: Raycast and uTools extensions run in a NodeJS runtime. Unlike Deno, NodeJS lacks a built-in sandbox to restrict access to the file system, network, etc. - **Potential Threats**: Extensions theoretically have the same capabilities as the user. They can read/write files, steal SSH private keys from `~/.ssh`, or even grant remote access to an attacker as the extension can execute any script in NodeJS runtime. - **NodeJS as a Double-Edged Sword**: Using NodeJS makes it easy to write powerful extensions, but it also increases the risk of exposing users to security threats. - **Raycast’s Approach**: Raycast mitigates this issue by open-sourcing all community extensions for public review. While open-sourcing is a good practice, it is not a perfect or scalable long-term solution. Open source does not inherently guarantee security; many well-known open source projects have significant vulnerabilities. - **Scalability and Review Challenges**: As of now, Raycast has over 1600 extensions, and the number is growing. It is challenging to review all these extensions thoroughly. Regular users lack the time and expertise to scrutinize the code of each extension they install, relying instead on the community's vigilance. - **Proposed Solution**: There needs to be a technical solution to this problem, akin to the permission systems in mobile operating systems. Extensions should only access the resources that the user has explicitly granted permission to, utilizing a sandboxing mechanism. ### Resource Usage #### Bundle Size | App | Installer Size | Installed App Size | | ------- | -------------- | ------------------ | | Alfred | 5.5MB | 17MB | | Raycast | 99.5MB | 124MB | | uTools | 93MB | 220MB | The size is acceptable given they are extendable. The size of Alfred is actually surprisingly small given its features. uTools is based on Electron which enables it to be cross-platform, no wonder it is so big. I don't understand why raycast is so big as a native app. Its NodeJS runtime is not even bundled into the app itself. I remember the older version of raycast was much smaller. ### UI - Alfred and Raycast are both native Mac apps, and look "Apple". Which is good. - Native apps are usually more performant and look better (Mac Apps). - However, I believe web is the future. Browser is the most important/successful cross-platform app. There are so many apps that doesn't need to be native. - Web/JS Ecosystem is probably the largest and most active ecosystem. One can create pretty much any complex UI with web technologies. Animation, 3D, etc. - Web apps are easy to develop and deploy. If I need to build native apps for all platforms, I have to be familiar with at least Swift, C++, C#, Java. Web, only JS. - Most importantly, for such apps, extension ecosystem is very important, and JS is the most popular language for extensions. VSCode is a good example. - Although Raycast extensioins are written with React/JS, the app itself is not web-based. React is used as a templating engine. The benefits of web technologies are not fully utilized. ### HCI I love the design of Raycast, which offers a hands-on keyboard experience similar to Vim. I also appreciate the flexibility of uTools, which leverages web technologies to provide a customizable UI. Using web technologies gives developers the freedom to tailor the UI, but this can lead to inconsistent experiences across extensions since different developers may have varying design skills. Raycast, on the other hand, has a more "Apple-style" extension system where developers use a provided React template components. The app then renders native components through reconciliation, ensuring a consistent UI and interaction, but at the cost of flexibility. Alfred's GUI for creating workflows is fantastic, especially for non-developers. It allows anyone to create their own workflows without needing programming knowledge. Overall, balancing the flexibility of web technologies with the consistency of native rendering can greatly enhance the user experience, making the app both versatile and user-friendly. ## My Vision I used to use Ubuntu and Windows as my main machine, and one of the main reasons I use Mac as my main machine was Alfred. I didn't find another app as good as Alfred on Linux and Windows. Then I found Raycast, and it's now my favorite app on Mac. It's pretty much perfect for me, but I still need to use Windows and Linux. What I want is, - Cross-platform (Linux, Windows, Mac) like uTools - Open source - Beautiful UI like Raycast - Secure (extensions restricted by a permission system) - Small bundle size (don't bundle a whole browser) - Extensible (Allow users to write extensions in JS) - Support Raycast-style extensions (developers create extensions following a provided template) for consistent UI and interaction - Flexible UI: Support web-based extensions (any SPA/SSG/CSR web apps can be converted into an extension) with maximum UI flexibility - This means any existing open source web apps can be converted into an extension with a few lines of code. When I explain my app to non-tech friends, I usually use the example of WeChat Mini Programs. Although I'm not a fan of WeChat, I have to admit that WeChat Mini Programs are a brilliant idea. They represent the largest-scale extension ecosystem I know of, used in everyday life by non-tech people. WeChat Mini Programs are like apps, but they are not installed. They are web apps that run within the WeChat app, offering good integration with WeChat to enable a seamless user experience. I want to create an app that functions like WeChat Mini Programs, but for the desktop. There are many desktop apps that don't need to be native. For example, image/video converters, todo apps, file finders, video downloaders, and encoder/decoder tools for developers can all be implemented as lightweight, integrated extensions within a single desktop app, providing a unified and efficient user experience. - If you build a native app, you have to know the native language of the platform. If you build a cross-platform app with Electron, do you really want to consume serveral hundred MBs of your precious disk space for each app (Mac disk space is like gold)? - To implement a very simple feature native apps with GUI usually cost at least a few MBs. The smallest Electron app costs at least ~100MB. The same features can be implemented in Kunkun with as low as 40KB. - Tauri or Wails are good alternatives, they are small (as low as a few MB), based on web, and cross-platform, but developers have to build the entire app from scratch, which is a little more difficult than building a web app. Code signing, promotion, advertisement, etc. takes a lot of effort, and could discourage developers to write a handy tool (Remember? Developers need to pay Apple tax to code sign Mac apps, even if the app is free). - Desktop apps lives and runs directly on the OS. Web apps run in browsers. Browsers restrict the access to the OS. What if you can install a web app as a desktop app, with access to system APIs (under a permission system of course)? That's what I want to achieve with Kunkun (Kind of like PWA, but PWA also have many limitations as of now and has a different HCI design from app launchers). I know this sounds dangerous, browsers restrict access to the OS for security reasons. There is always a trade-off between security and functionality. But I believe it is possible to create a secure system that allows web apps to access the OS in a secure way. No need to be scared. Browser extensions are a good example. They can access the OS, but they are sandboxed. ## Tech Stack - Desktop - Framework: Tauri - Frontend: Sveltekit - Backend: Rust - Frontend - Tailwind CSS - shadcn-svelte - Backend - Server: Axum + Tonic (grpc) - Web Server - Supabase: open source Firebase alternative - Web Apps - Astro + Starlight: Documentation Site - Cloudflare pages: web app hosting - Dev - pnpm workspace - GitHub Actions (CI/CD) - turbo repo (monorepo management) - `bun` - `rollup` ### Explanation - **Tauri** - **Lightweight & Cross-Platform:** Tauri provides a lightweight, cross-platform framework using web technologies. - **Rust Backend:** Rust is fast and memory-safe, though challenging to write. It is the hardest high-level language I've encountered. - **Consideration for Wails:** Wails is another good alternative. Golang is much easier than rust. For this kind of app, Golang's GC is not a problem. I picked Tauri since Wails is not as mature as Tauri. Multi-window support is not stable yet. I may switch to Wails in the future if one day it becomes as good as Tauri. - Tauri v2 has a window-based permission system. Although this is not the only way to implement a permission system, this feature is very important for Kunkun. I may use this feature to restrict access of extensions to the OS. - **Sveltekit** is my favorite frontend stack. I am familiar with React, Vue and Svelte. - In terms of ecosystem, my ranking is React > Vue > Svelte. - In terms of the framework/language itself, my ranking is Svelte > Vue > React. - There is no perfect framework, and there is always a trade-off. I picked Vue + Nuxt initially because it has a mature ecosystem and is easy to use. However, I then faced many problems with Nuxt when building a Tauri app, and eventually switched to Sveltekit, which I found to be the best choice for me. - When I was using Nuxt, its auto import feature sounds very nice, but could be very inconvenient for debugging. When there is import error with a nuxt module, I don't know where is happens because all imports happen in background. I had to remove all nuxt modules and add them back one by one to see which one is causing the problem. I never had to do this again after switching to sveltekit. - I always choose a **meta-framework** (Next, Nuxt, Sveltekit) rather than the vanilla framework when building a Tauri app. Metaframeworks provide lots of useful features out of the box. SSR isn't feasible for a desktop app, but other features like file-based routing, layouts, SSG, etc. are still useful. When an SPA grows, I have to do code splitting, lazy loading, etc. manually. A metaframework does this for me. - React is probably the most future proof. If you are unsure what library you will need in the future, React is a good choice. Most UI libraries support React or have a React version. - But I didn't have a good experience with React and especially Next.js when writing Tauri apps. The double mounting from React 18 is so annoying. `useEffect` doesn't support async. Tauri APIs are mostly async. A listener is usually obtained with `unlisten = await listen("event_name")`. I had to write a lot more boilerplate code in React to avoid listening twice than with Vue or Svelte. Next.js throws so many errors when I was building a Tauri app (I don't remember what they were but I wasted so much time on them rather than the app logic). I also have to keep thinking about using `use client;`. In Nuxt and Sveltekit, I never need to think about whether I am in the client or server (when building a pure frontend app). - **Rust** is nice. Fast and memory safe. Extremely hard to write, slow to build. Huge cache. (60GB accumulated for Kunkun). `node_modules` is not `heavy` at all compring to rust `target`. `target` is the heaviest object in the universe. I don't get it. I use rust because of Tauri. If Wails is as good as Tauri, I will happily switch to use Golang. - **TailwindCSS**: I don't think I need to explain this. - **Shadcn-svelte**: a svelte port from the original react shadcn. I like the styling of Shadcn and philosophy of letting user customize the components. And, I plan to support any SSG/CSR web app as an extension, which means any JS frameworks are supported. Shadcn is based on React, and there is ports for Vue and Svelte, with the same styling. It's simpler to keep extension UI styling consistent with the app UI styling if shadcn is used. - Axum+Tonic is probably the best choice in rust to support regular HTTP server and gRPC server together. I want to use gRPC because of its proto can guarantee the compatibility between the client and server. Reflection server is also easy for development, kind of like graphql. # Convert neohtop to Extension [neohtop](https://github.com/Abdenasser/neohtop) is a beautiful desktop version of the htop command. > A modern, cross-platform system monitor built on top of Svelte, Rust, and Tauri. In this blog, I will talk about how I converted neohtop to an extension. It maybe simpler than you think. ## TC;SC - A demo PR: https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files - The extension: https://kunkun.sh/store/neohtop - The repo: https://github.com/kunkunsh/kunkun-ext-neohtop Here is a screenshot of the extension, exactly the same as the desktop version. ![](https://imgur.com/D8VHDEz.png) ## Intro [neohtop](https://github.com/Abdenasser/neohtop) is a Tauri app. It's frontend is built with Svelte, and backend is built with Rust. > One of the reasons I created kunkun is to make it easy to write cross-platform apps and distribute them on different platforms. > > And neohtop is a perfect example for this. It's a nice GUI wrapper over the [`sysinfo`](https://crates.io/crates/sysinfo) crate. See its [Cargo.toml](https://github.com/Abdenasser/neohtop/blob/8269d6f95a41df5fd31b175f26113b1692b6163c/src-tauri/Cargo.toml#L15). Luckily, Kunkun's extension API also provides a `sysInfo` API, see [docs](/developer/api/sysinfo/). It is based on [tauri-plugin-system-info](https://github.com/HuakunShen/tauri-plugin-system-info), which is also a wrapper over the [`sysinfo`](https://crates.io/crates/sysinfo) crate. See [Cargo.toml](https://github.com/HuakunShen/tauri-plugin-system-info/blob/cb32fe842b8ba9a893a2aa4d6850b9a81516e7aa/Cargo.toml#L21). Since the data source are the same, then it should be straightforward to convert it to an extension. I set a 30 minutes goal for myself, but it took me 90 minutes to complete the conversion. This is because `neohtop` did its data aggregation and preprocessing in rust, and I need to convert the logic to TypeScript. Kind of like how TypeScript is converted to Golang, I just needed to convert the rust code to TypeScript, not hard at all, just time-consuming. ## Code In this [PR](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files), I demonstrated all the changes I made for the conversion. First of all, neohtop is written in frontend framework svelte, so obviously this is a custom UI extension. ### Manifest [`package.json`](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519) ```json title="package.json" showLineNumbers {10-11} {20-22} "kunkun": { "name": "neohtop", "shortDescription": "A modern, cross-platform system monitor", "longDescription": "A modern, cross-platform system monitor", "identifier": "neohtop", "demoImages": [ "https://imgur.com/D8VHDEz.png" ], "permissions": [ "system-info:all", "shell:kill-any" ], "icon": { "type": "remote-url", "value": "https://github.com/Abdenasser/neohtop/raw/main/app-icon.png" }, "customUiCmds": [ { "name": "neohtop", "main": "/", "dist": "build", "devMain": "http://localhost:1420", "cmds": [], "window": { "title": "Neohtop", "hiddenTitle": true, "titleBarStyle": "overlay", "width": 1400, "height": 1000 } } ] }, ``` - `build` folder is the default output folder of svelte projects, thus `dist` is set to `build`. - `devMain` is the local server that serves the svelte project, it is used when developing the extension with HMR. - `system-info:all` permission is required to access the system info data. - `shell:kill-any` permission is required to kill any process by a `pid`. ### Frontend neohtop has no default window title bar, but users can drag the window by clicking on the top of the app. This is achieved by simply including `data-tauri-drag-region` in a `div` element. ```html
``` In a KK extension, we use `data-kunkun-drag-region` instead, just remember to call `ui.registerDragRegion` in app initialization. import { Code } from "@astrojs/starlight/components"; export const exampleCode = `console.log('This could come from a file or CMS!');`; export const fileName = "example.js"; export const highlights = ["file", "CMS"]; {/* */} ```diff lang="svelte" title="TitleBar.svelte" -
+
``` ```diff lang="ts" title="+page.svelte" + import { ui } from "@kksh/api/ui/custom"; + onMount(() => { + ui.registerDragRegion("kunkun"); + }); ``` - [TitleBar.svelte](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files#diff-5b930dbf5fac4f54096c3e2bc4e0a320b89a6010da94963ce000193b929e29a5) - [+page.svelte](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files#diff-1e1270da740e81dd13f8a65057582b0fce8a5fb3817ef913f3e90d7f82c4e9dfR76) ### Data Preprocessing/Aggregation > This is the most time-consuming part. The source system info data is preprocessed before rendering. The preprocessing logic is written in Rust, thus I need to convert them to TypeScript. | neohtop (Rust) | kunkun-ext-neohtop (TypeScript) | | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | [process_monitor.rs](https://github.com/Abdenasser/neohtop/blob/8269d6f95a41df5fd31b175f26113b1692b6163c/src-tauri/src/monitoring/process_monitor.rs#L19) | [processMonitor.ts](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files#diff-875f3104224e0084f31fd16597f39627b5bb41359aaa82ae34d408e90cd708d7) | | [system_monitor.rs](https://github.com/Abdenasser/neohtop/blob/8269d6f95a41df5fd31b175f26113b1692b6163c/src-tauri/src/monitoring/system_monitor.rs#L19) | [systemMonitor.ts](https://github.com/kunkunsh/kunkun-ext-neohtop/pull/1/files#diff-e49245b092e4c4cac8ce0c2f7754bdc520c6bd16a1f8a1c1633554c86192ca95) | ## Acknowledgements - [Abdenasser](https://github.com/Abdenasser) for the awesome [neohtop](https://github.com/Abdenasser/neohtop) # Tauri + Drizzle Proxy GitHub: https://github.com/HuakunShen/tauri-demo/tree/master/examples/drizzle-sqlite-proxy > [!info] > This demo let you use drizzle to control your sqlite DB in a Tauri app, without any sidecar. > This is a Tauri v2 reproduction for the archived repo [https://github.com/tdwesten/tauri-drizzle-sqlite-proxy-demo](https://github.com/tdwesten/tauri-drizzle-sqlite-proxy-demo) Tauri's backend is in Rust, so I always thought the only way to use sqlite ORM in a Tauri app is with projects like [diesel](https://diesel.rs/) or [prisma-client-rust](https://github.com/Brendonovich/prisma-client-rust), which could be hard because they are in rust. What is even harder is sqlite db encryption. Both of the 2 ORMs don't support cipher encryption, thus I had to write raw sql queries in #kunkun [Example](https://github.com/kunkunsh/kunkun/blob/c39e98258c3bf287e17ce3d3d143459b590d0976/packages/db/src/lib.rs#L119) Maintaining raw sql queries is a nightmare to me, especially when it comes to schema migration. I have to rely on thorough testing to make sure everything is correct. There is no type checking. I never knew it's possible to use TypeScript ORM like drizzle in Tauri without a sidecar. In my project [[Projects/kkrpc|kkrpc]] I implemented a Tauri adapter and made it possible to use compiled TypeScript backend as a sidecar in a Tauri app. With kkrpc, it's easy to call TypeScript backend from frontend. I can use any TypeScript libraries in a compiled deno/bun/node binary, including drizzle. However that introduces at least 60MB to the bundle size. It's a good deal if you can take advantage of many node packages, but not really worth it for just DB. Then I found this project https://github.com/tdwesten/tauri-drizzle-sqlite-proxy-demo using drizzle proxy to send queries to Tauri's sql plugin. Drizzle Proxy Docs: https://orm.drizzle.team/docs/connect-drizzle-proxy Basically, it's a translator between the [tauri-plugin-sql](https://crates.io/crates/tauri-plugin-sql) and drizzle ORM. drizzle computes the sql query in frontend and send the query + params to backend to execute. The backend can be a http server, or Tauri core. ```ts // Example of driver implementation import { drizzle } from 'drizzle-orm/pg-proxy'; const db = drizzle(async (sql, params, method) => { try { const rows = await axios.post('http://localhost:3000/query', { sql, params, method }); return { rows: rows.data }; } catch (e: any) { console.error('Error from pg proxy server: ', e.response.data) return { rows: [] }; } }); ``` Here is real code [Excalidraw Diagrams](https://excalidraw.com/#json=qGv0x2Lrxuj1hjT8WFaw-,W3_F55HzeA6QtgbmsPqJ6g) ![Excalidraw Diagram](https://i.imgur.com/T1GFsy6.png) ```ts import { drizzle } from "drizzle-orm/sqlite-proxy"; import Database from "@tauri-apps/plugin-sql"; import * as schema from "./schema"; export async function getDb() { return await Database.load("sqlite:test.db"); } export const db = drizzle( async (sql, params, method) => { const sqlite = await getDb(); let rows: any = []; let results = []; // If the query is a SELECT, use the select method if (isSelectQuery(sql)) { rows = await sqlite.select(sql, params).catch((e) => { console.error("SQL Error:", e); return []; }); } else { // Otherwise, use the execute method rows = await sqlite.execute(sql, params).catch((e) => { console.error("SQL Error:", e); return []; }); return { rows: [] }; } rows = rows.map((row: any) => { return Object.values(row); }); // If the method is "all", return all rows results = method === "all" ? rows : rows[0]; await sqlite.close(); return { rows: results }; }, // Pass the schema to the drizzle instance { schema: schema, logger: true } ); ``` And here is how it can be used (just like regular drizzle code) ```ts const loadUsers = async () => { db.query.users .findMany() .execute() .then((results) => { console.log("🚀 ~ FindMany response from Drizzle:", results); users = results; }); }; async function addUser() { await db.insert(schema.users).values({ name: nameInput }); nameInput = ""; loadUsers(); } ``` ```mermaid graph TD A[Frontend JavaScript/TypeScript] --> B[Drizzle ORM] B --> C[Drizzle Proxy] C --> D[Tauri SQL Plugin] D --> E[SQLite Database] subgraph "Frontend JavaScript/TypeScript" A B C end subgraph "Backend Rust" D E end F[SQL Query + Params] -.Generated by Drizzle.-> C C -.Translates and forwards.-> D D -.Executes query.-> E E -.Returns results.-> D D -.Returns data.-> C C -.Formats for ORM.-> B B -.Type-safe results.-> A ``` # Kunkun API [![NPM Version](https://img.shields.io/npm/v/%40kksh%2Fapi)](https://www.npmjs.com/package/@kksh/api) :::note ### Acknowledgement A lot of the APIs provided in `@kksh/api` are alias of Tauri's official APIs, or inspired by Tauri's APIs. ::: [Kunkun API](https://www.npmjs.com/package/@kksh/api) is an NPM package designed for developers to create extensions for KK. It provides a set of APIs to interact with KK and access system resources. As of now, there are 2 sandboxed environments to run the extension: web worker and iframe. APIs for the two environments are almost identical. I made separate subpackages for them for clarity and make tree-shaking easier. The APIs are exposed in the following subpackages: - `@kksh/api/ui/custom` - `@kksh/api/ui/template` The `/ui` subpackage means they are designed for UI extensions (i.e. extension has GUI). This is to distinguish from "headless" extensions for non-UI extensions (which I plan to develop in the future). See [API Reference](/api-docs) for more details. https://docs.api.kunkun.sh/ is the generated API documentation with a full list of APIs and their type definitions. ## Playground Here is a code editor playground with `@kksh/api` installed. You can try out the API, but don't try to run it, as the API is not designed to run in the browser. The sample code here is only for template extensions running in web worker environment. APIs are imported from `@kksh/api/ui/template`. The custom UI extension running in iframe have similar APIs, imported from `@kksh/api/ui/custom`. The playground is only to give you a taste of how the API are called and what kind of APIs are available. For more details and examples, please keep read the following pages.
https://docs.api.kunkun.sh/ # App > This API provides information about the application **Docs:**: https://docs.api.kunkun.sh/interfaces/index.IApp ## API - `language()`: Get the language preference import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts app.language().then((lang) => { console.log("Language:", lang); }); ``` # Clipboard **Docs:** https://docs.api.kunkun.sh/interfaces/index.IClipboard import ClipboardAPI from '@/components/api/clipboard.astro' ## API and Permissions ## Sample Usage import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts // Check if the clipboard has data const hasFiles: boolean = await clipboard.hasFiles() const hasHTML: boolean = await clipboard.hasHTML() const hasImage: boolean = await clipboard.hasImage() const hasRTF: boolean = await clipboard.hasRTF() const hasText: boolean = await clipboard.hasText() // Read data from the clipboard const text: string = await clipboard.readText() const imageBinary: Blob = await clipboard.readImageBinary('Blob') as Blob const imageBase64: string = await clipboard.readImageBase64() const html: string = await clipboard.readHtml() const files: string[] = await clipboard.readFiles() const rtf: string = await clipboard.readRtf() // Write data to the clipboard await clipboard.writeText('Hello, World!') await clipboard.writeImageBinary(Array.from(new Uint8Array(await imageBinary.arrayBuffer()))); await clipboard.writeHtmlAndText('

Hello, World!

', 'Hello, World!') await clipboard.writeFiles(['file1.txt', 'file2.txt']) await clipboard.writeRtf(rtf) await clipboard.writeImageBase64(imageBase64) ``` :::note Clipboard API taken from https://github.com/CrossCopy/tauri-plugin-clipboard ::: # Database **Docs:** https://docs.api.kunkun.sh/interfaces/index.IDb Database API is for extensions that need to store data in a database. Please avoid using browser storage like `localStorage` to avoid data collisions with other extensions and data leaks. **Database API doesn't require any permissions, but one extension can only access its own data.** This database is based on sqlite, for a key-value style storage, see [KV API](/developer/api/kv). The following APIs are now available: - `add` - `delete` - `search` - `retrieveAll` - `retrieveAllByType` - `deleteAll` - `update` CRUD are the basic operations provided by the database API. Let's talk about them one by one. ## Add There are 3 parameters for each data entry: - `data` - Any data you want to store. - `dataType` - An arbitrary string to categorize the data. - `searchText` - A string to search the data. You can use this to store preview data as well. - This field is searchable via sqlite full-text search. Only `data` is required. `dataType` and `searchText` are optional, and everything is stored as a string. You need to use `JSON.stringify` to store objects, and `JSON.parse` to deserialize them. > Later I may provide a way to store objects directly, but for now, you need to serialize them. import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts await db.add({ data: JSON.stringify({ name: "Kunkun" }), dataType: "preference", searchText: "preference", }); ``` :::caution Since the database API is not a key-value store. `dataType` isn't a unique key. Running `.add()` multiple times with the same data will create multiple entries. If you need to make sure there is a single entry, please use the [KV API](/developer/api/kv) instead. ::: ## Read/Search There are multiple APIs to read data from the database. - `db.retrieveAll` returns all data entries for the extension. Sometimes the extension may store large amount of data in the `data` field, while you may only need the `searchText`. So a `fields` parameter is provided to specify which fields you want to retrieve. - `db.retrieveAllByType` returns all data entries by `dataType`. - `db.search` run search query within the database instead of retrieving all data and filtering them with JS. ```ts const allData = await db.retrieveAll({ fields: ["data", "search_text"], }); const result = await db.search({ dataType: "preference" }); const result = await db.search({ dataType: "preference" }); ``` ### Search `db.search` API supports searching on multiple fields. Use your IDE to go to the definition of `search()` to see the full type definition. ```ts enum SQLSortOrder { ASC = "ASC", DESC = "DESC", } type SearchParams = { dataId?: number; fullTextSearch?: boolean; dataType?: string; searchText?: string; afterCreatedAt?: Date; beforeCreatedAt?: Date; limit?: number; orderByCreatedAt?: SQLSortOrder; orderByUpdatedAt?: SQLSortOrder; fields?: ExtDataField[]; }; ``` - `dataId` is the unique ID of the data entry. - `fullTextSearch` is a boolean to enable full-text search on the `searchText` field. - `limit` is the maximum number of results to return. - `fields` is an array of fields to return. ## Update You need to provide the `dataId` to update the data. ```ts await db.update({ dataId: 803, data: "new data", searchText: "new data" }); ``` ## Delete You need to provide the `dataId` to delete the data. ```ts await db.delete(803); ``` # Deno > Deno provides an API to run JavaScript/TypeScript in non-browser environment with more access to system resources, > but still within a sandbox. :::tip Deno API is part of the shell API and works similarly but provides a easier abstraction, you may want to read [shell API](/developer/api/shell) first. ::: ## API and Permissions All [Deno permissions](https://docs.deno.com/runtime/fundamentals/security/) must be explicitly declared in `package.json`. `DenoCommand` works similarly to a regular [shell Command](/developer/api/shell#command). It can be created with `shell.createDenoCommand`. The returned `DenoCommand` object has `execute` and `spawn` methods (just like regular `Command`). To run a Deno command, you need one of the the following scoped permissions: - `shell:deno:execute` - `shell:deno:spawn` Read [sample code](#sample-code) for more details. ### RPC Library Style API > If you're looking to run Deno as a library for resource-intensive tasks or those requiring system access, the Deno API offers a solution. You can execute Deno scripts using `shell.createDenoCommand`, passing input through `argv` and `stdin`, and retrieving results via `stdout` and `stderr`. This approach essentially treats Deno like a CLI, as illustrated in an example (Transform Image with Sharp) provided below. > However, using a CLI is not always ideal for those who prefer calling scripts directly like a typical library. The `@kksh/api` solves this by offering an abstraction in RPC style, allowing Deno scripts to be invoked as libraries rather than CLIs. The shell API provides a function `createDenoRpcChannel` that creates a Deno RPC API. ```ts function createDenoRpcChannel( scriptPath: string, args: string[], config: Partial & SpawnOptions, localAPIImplementation: LocalAPI ): Promise> // ... } ``` Then you can call the remote API functions directly. - Since this is a bidirectional IPC channel, you can pass `LocalAPI` and `RemoteAPI` generics to define the API. - `LocalAPI` is what you want to expose to the other side of the channel, and `RemoteAPI` is what you want to call from the other side. - `args` are `argv` like regular commands - `config` is the same as config in `shell.createDenoCommand`, used for configuring deno permissions and other options. - `localAPIImplementation` is the implementation of the API you want to expose, pass `{}` if you don't want to expose any API. #### Debug Log in Deno :::caution This RPC style API is implemented with `@kunkun/kkrpc` package, see https://jsr.io/@kunkun/kkrpc It's using `stdin` and `stdout` to communicate under the hood. If your Deno script needs to print log, use `console.error` to log to `stderr` instead of `console.log` to `stdout`. All `stdout` will be parsed by `kkrpc` protocol, if you print something to `stdout`, it could potentially break the protocol and cause undefined behavior. If an imported package logs to `stdout` and `kkrpc` cannot parse it, it will log an error to console, but the extension will not crash. ::: To show `stderr` in the console, you have to manually listen to the `stderr` event. ```ts const { rpcChannel, process, command } = await shell.createDenoRpcChannel( '$EXTENSION/deno-src/index.ts', [], {}, {} ); command.stderr.on('data', (data: string) => { console.warn(data); }); ``` Here I used `console.warn` to display the deno log in a different color. ## Sample Code ### Math Library (RPC Style) Suppose we want to build a math library in Deno and call it from extension running in web worker or iframe. Here is how the library will be written in Deno. Just provide an API object. :::caution All API methods must be `async` functions (return `Promise`), otherwise the return type will be wrong. ::: ```ts title="types.ts" export interface MathLibAPI { add(a: number, b: number): Promise subtract(a: number, b: number): Promise } ``` #### Deno Script I recommend use a separate subdirectory for Deno scripts to avoid confusion with regular extension code. I will use `deno-src` in this example. ```bash deno init deno-src deno add jsr:@kunkun/api ``` This will create a import map in `deno.json` ```json title="deno-src/deno.json" { "imports": { "@kunkun/api": "jsr:@kunkun/api@^x.x.x" // Use the latest version } } ``` Kunkun's API package for Deno is on JSR, https://jsr.io/@kunkun/api Note: the scope is `@kunkun` rather than `@kksh`. This is because `@kunkun` scope was taken on npm registry. `expose` function is provided by `@kunkun/api/runtime/deno` to expose the API object to its parent process. ```ts title="deno-src/math-lib.ts" import { expose } from '@kunkun/api/runtime/deno' import { type MathLibAPI } from "../types.ts" // Define your API methods export const mathLib: MathLibAPI = { add: async (a: number, b: number) => a + b, subtract: async (a: number, b: number) => a - b } expose(apiMethods) ``` `expose()` is a wrapper function for building a bidirectional RPC channel. It returns the `RPCChannel` object, which can be used to call API exposed from the other side of the channel.
Manual Channel Configuration You shouldn't need to worry about how the channel is created, but here is how it's done manually. It's based on `@kunkun/kkrpc` package, see https://jsr.io/@kunkun/kkrpc ```ts title="deno-src/math-lib.ts" import { DenoStdio, RPCChannel } from "@kunkun/kkrpc" import { type MathLibAPI } from "../types.ts" // Define your API methods (that will be exposed to the other side) export const mathLib: MathLibAPI = { add: async (a: number, b: number) => a + b, subtract: async (a: number, b: number) => a - b } const stdio = new DenoStdio(Deno.stdin.readable, Deno.stdout.writable) const channel = new RPCChannel(stdio, mathLib) const api = channel.getApi() ```
#### Extension Code In main extension code, use `shell.createDenoRpcChannel` to start running a Deno script and get the API object. import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts title="extension.ts" import { type MathLibAPI } from "./types.ts" // ... // First generic is Local API interface to expose to the other side of the channel. We set it to {} to not expose any API. Last function parameter is the implementation of the API, which we also set to {} const { rpcChannel, process } = await shell.createDenoRpcChannel< {}, MathLibAPI >("$EXTENSION/deno-src/math-lib.ts", [], { allowRead: ["$DESKTOP/**"] // this math lib doesn't need this permission, it's just an example to show how to pass permissions }, {}); const api = rpcChannel.getApi(); await api.add(1, 2).then(console.log); // 3 await api.subtract(1, 2).then(console.log); // -1 await process.kill() // please always remember to kill the Deno process, or it may run forever ``` :::danger Please remember to kill the process manually after you are done using the API. Otherwise, the process could run in the background forever. An auto process cleanup mechanism is implemented in Kunkun to auto kill all leftover processes when an extension quits, but it's still better to control it manually. If you are using template worker extension command, you could store the process as a class instance variable, and kill it in `onBeforeGoBack()` hook. I recommend spawning a new process when you need it and kill it immediately after to avoid problems. ::: :::tip This RPC style Deno API is recommended over CLI style for complicated tasks. ::: #### Permissions Using Deno as a CLI requires `shell:deno:execute` permission. Using Deno as a library requires `shell:deno:spawn` permission because it will be a long running process. `"shell:stdin-write"` is required for `shell.createDenoRpcChannel` to work, because our `kkrpc` protocol uses `stdin` and `stdout` to communicate. `"shell:kill"` is required to kill the Deno process when you are done. (Although when extension quits, it will auto kill all leftover processes. But it's still a good practice to kill it manually.) ```ts title="package.json" { ... "permissions": [ { "permission": "shell:deno:spawn", "allow": [ { "path": "$EXTENSION/deno-src/index.ts", "read": ["$DESKTOP"] } ] }, "shell:stdin-write", "shell:kill" ] ... } ``` ### Transform Image with Sharp (CLI Style) In this sample, we will transform an image with [sharp](https://sharp.pixelplumbing.com/), which is not runnable in browser. Suppose we have a template worker extension project with the following structure: import { FileTree } from '@astrojs/starlight/components'; - src - deno-script.ts - index.ts - ... `index.ts` is the entrypoint to the extension command, `deno-script.ts` is a script that will be executed by Deno. ```ts title="deno-script.ts" import { parseArgs } from "jsr:@std/cli/parse-args" import sharp from "npm:sharp" const args = parseArgs(Deno.args) console.log(args) const input = args.i const output = args.o sharp(input).blur(10).resize(300, 300).toFile(output) ``` The deno script above is very simple, it takes in an input image path and output image path, then apply blur and resize and save to output path. To run this sample directly with `deno`: ```bash deno run --allow-ffi \ --allow-env=npm_package_config_libvips,CWD \ --allow-read=/Users/user/Desktop/avatar.png src/deno-script.ts \ -i ~/Desktop/avatar.png -o ~/Desktop/avatar.jpeg ``` In the command above, we need to grant fine-grained permissions to Deno. It's not a good idea to run Deno with all permissions. #### Use Deno API In the following sample code, we will use `shell.createDenoCommand` to create a Deno command with permission settings. See https://docs.api.kunkun.sh/interfaces/index.DenoRunConfig for options you can pass to `shell.createDenoCommand`. ```ts title="index.ts" import { shell, path, TemplateUiCommand } from "@kksh/api/ui/template"; class ExtensionTemplate extends TemplateUiCommand { async load() { const denoCmd = shell.createDenoCommand( "$EXTENSION/src/deno-script.ts", ["-i=./avatar.png", "-o=./avatar-blur.jpeg"], { allowEnv: ["npm_package_config_libvips", "CWD"], allowRead: ["$DESKTOP"], allowAllFfi: true, cwd: await path.desktopDir(), } ); const denoRes = await denoCmd.execute(); console.log("stdout", denoRes.stdout); } } ``` The above TypeScript code sets CWD to the desktop directory, but you don't have to. You can always just compute the absolute path for the input and output file. `$EXTENSION` was used to refer to the directory of the extension. It will be translated to the real path of your extension root at runtime. `$DESKTOP` is also a path alias that will be translated to the real path of your desktop at runtime. You can find other path aliases in [file system API](/developer/api/fs#path-alias). #### Permissions in package.json To ensure that an extension operates within a restricted scope, it must declare the necessary permissions in the `package.json` file. This step prevents the extension from executing any code beyond the predefined limits. ```ts title="package.json" ... "permissions": [ { "permission": "shell:deno:execute", "allow": [ { "path": "$EXTENSION/src/deno-script.ts", "env": ["npm_package_config_libvips", "CWD"], "ffi": "*", "read": ["$DESKTOP"] } ] }, ... ] ... ``` `path` is the path to the deno script to execute. The rest of the permissions are from [Deno permissions](https://docs.deno.com/runtime/fundamentals/security/). - `net` - `env` - `read` - `write` - `run` - `ffi` - `sys` They are `string[]` or `"*"`, when set to `"*"`, it allows all. For example, setting `"read": "*"` allows setting `allowAllRead` to true in `shell.createDenoCommand`, and will be translated to `--allow-read` in the code execution. Setting `"read": ["$DESKTOP"]` allows you to set `allowRead: ["$DESKTOP"]` in `shell.createDenoCommand`, and will be translated to `--allow-read=/Users/user/Desktop` in the code execution. # Dialog **Docs:** https://docs.api.kunkun.sh/interfaces/index.IDialog Dialog API is used to interact with the user by showing dialog boxes. This API is used to show alert, confirm, prompt, and file dialog boxes. - `ask`: Shows a question dialog with Yes and No buttons. - `confirm`: Shows a question dialog with Ok and Cancel buttons. - `message`: Shows a message dialog with an Ok button. - `open`: Open a file/directory selection dialog. - `save`: Open a file/directory save dialog. import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts const answer = await dialog.ask('This action cannot be reverted. Are you sure?', { title: 'KK', kind: 'warning', }); const confirmation = await dialog.confirm( 'This action cannot be reverted. Are you sure?', { title: 'KK', kind: 'warning' } ); await dialog.message('File not found', { title: 'KK', kind: 'error' }); const file = await open({ multiple: false, directory: false, }); console.log(file); // Prints file path and name to the console const path = await save({ filters: [ { name: 'My Filter', extensions: ['png', 'jpeg'], }, ], }); // Prints the chosen path ``` :::note Alias of Tauri's dialog plugin https://v2.tauri.app/plugin/dialog/ ::: # Event **Docs:** https://docs.api.kunkun.sh/interfaces/index.IEvent Event API contains commands to listen to some window/app events. For more details, refer to [**Docs**](https://docs.api.kunkun.sh/interfaces/index.IEvent). ## Sample Code import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts event.onDragDrop((payload) => { const filePaths = payload.paths const { x, y } = payload.position }) event.onWindowFocus(() => { document.getElementById("search-bar")?.focus() }) event.onWindowBlur(() => { // e.g. disconnect from server }) ``` ## Permission import EventAPI from '@/components/api/event.astro' # Fetch **Docs:** https://docs.api.kunkun.sh/interfaces/index.IFetch When you use the built-in `fetch` or http client like Axios directly in iframe or web worker, you may encounter CORS issue. `fetch()` API provided in `@kksh/api` sends the request through the host app, thus bypassing CORS. `'fetch:all'` permission is required to use this API. import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts await fetch('https://schema.kunkun.sh') .then((res) => res.json()) .then((data) => {}); ``` Sample `package.json` permission configuration: ```json title="package.json" ... "permissions": ["fetch:all"], ... ``` # File System **Docs:** https://docs.api.kunkun.sh/interfaces/index.IFs The File System API can be used to read/write files and directories, and search for files. File System access requires scoped permissions. Only the directories specified in the permissions can be accessed. ## APIs and Required Permissions import FsAPI from '@/components/api/fs.astro' The APIs are very intuitive by their names, so I won't explain every API here. See the auto-generated API documentation for more details and their type definitions. I will explain how the scoped permission works and the file search API in the following sections. ## Scoped Permission To access a file, or directory, or directory recursively, you need to specify the path pattern in the permission. Since KK is cross-platform, the path is different on different platforms. You need to use path alias to specify the path. ### Path Alias - `$DESKTOP` - `$DOCUMENT` - `$DOWNLOAD` - `$HOME` - `$APPDATA` - `$EXTENSION` - `$EXTENSION_SUPPORT` (Where extensions can store files to support the extension, such as data, config, cache, binary) Currently, only these aliases are supported. Files outside these directories are inaccessible. For example, this is an example permission to access the `projects.json` file of VSCode Project Manager extension config file on all 3 platforms. > You may need `"os:all"` permission to get current platform to know which path to read in the extension code. ```json title="package.json" ... "permissions": [ "os:all", { "permission": "fs:read", "allow": [ { "path": "$HOME/Library/Application Support/Code/User/globalStorage/alefragnani.project-manager/projects.json" }, { "path": "$HOME/.config/Code/User/globalStorage/alefragnani.project-manager/projects.json" }, { "path": "$APPDATA/Code/User/globalStorage/alefragnani.project-manager/projects.json" } ] } ], ... ``` Here is how to use the API in JS. A `baseDir` parameter can be provided as well. import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts const fileContent: string = await fs.readTextFile( "Library/Application Support/Code/User/globalStorage/alefragnani.project-manager/projects.json", { baseDir: path.BaseDirectory.Home } ) ``` ### Directory Access To list items in a directory ```json title="package.json" ... "permissions": [ "os:all", { "permission": "fs:read-dir", "allow": [ { "path": "$DOWNLOAD" } ] }, ], ... ``` ```ts const downloads: string[] = await fs.readDir("Downloads", { baseDir: path.BaseDirectory.Home }) // or const downloads: string[] = await fs.readDir(await path.downloadDir()) ``` import { FileTree } from '@astrojs/starlight/components'; Suppose we have this file tree structure, and we want to list items in `kunkun` folder. - Home - Downloads - kunkun - file.txt - file2.txt This is how to access the `kunkun` folder from js, but will got permission denied with the previous permission (i.e. `"$DOWNLOAD"`). ```ts const downloads = await fs.readDir("Downloads/kunkun", { baseDir: path.BaseDirectory.Home } ) ``` The path scope permission should be changed to `"$DOWNLOAD/*"` to access everything under the `Downloads` directory. ```json title="package.json" ... "permissions": [ "os:all", { "permission": "fs:read-dir", "allow": [ { "path": "$DOWNLOAD/*" } ] }, ], ... ``` ### Recursive Directory Access To access `file.txt` in the `kunkun` folder, the path scope should be changed to `"$DOWNLOAD/kunkun/file.txt"` if you only want to access this file., Or `"$DOWNLOAD/**"` if you want to access everything under the `Downloads` directory. ## File Search API The file search API is used to search for files in a directory. Here is an example of searching for a file named `file.txt` in the `Downloads` directory. ```ts const searchRes: string[] = await fs.fileSearch({ locations: [await path.downloadDir()], query: "file.txt", }) ``` Each search location must be specified in the permission under `fs:search` permission. ```json title="package.json" ... "permissions": [ { "permission": "fs:search", "allow": [ { "path": "$DOWNLOAD" } ] }, ], ... ``` More search options are provided, check the API documentation for more details. - `locations` - `query` - `ext` - `depth` - `limit` - `hidden` - `ignore_case` - `file_size_greater` - `file_size_smaller` - `file_size_equal` - `created_after` - `created_before` - `modified_after` - `modified_before` # Helper This API contains some helper functions to help you guide users to install the dependencies. Your extension may depend on some common dependencies like Homebrew, Deno, FFmpeg, etc. You can implement your own guide functions to guide users to install the dependencies. Or you can use the following APIs to guide users to KK's builtin guides. ## APIs - `guideInstallDeno` - `guideInstallFfmpeg` - `guideInstallHomebrew` # KV **Docs:** https://docs.api.kunkun.sh/interfaces/index.IKV KV API is a persistent key-value store for extensions similar to `localStorage`. Please avoid using browser storage like `localStorage` to avoid data collisions with other extensions and data leaks. **KV API doesn't require any permissions, but one extension can only access its own data.** :::tip FYI: The KV API is just a wrapper over the [Database API](/developer/api/db), which is built on top of sqlite. The `dataType` field is `kunkun_kv`, so if you want to access KV store data using the Database API, you can set `dataType` to `kunkun_kv` in search query. ::: The following APIs are now available: - `add` - `get` - `exists` - `delete` ## Sample Code import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts kv.exists("test").then((exists) => { console.log("KV exists:", exists); }); kv.set("test", Math.random().toString()).then(() => kv.get("test").then((value) => { console.log("KV value:", value); }); ); ``` :::caution The data is stored as JSON string in database, so data passed to `set` must be serializable with `JSON.stringify`. It doesn't have to be a string, but it will be converted to string when stored, and `JSON.parse` will be called when retrieved. ::: # Log **Docs:** https://docs.api.kunkun.sh/interfaces/index.ILogger This log API not only logs to browser console, but also logs to a log file stored on disk. Could be useful for debugging. If there is any serious error, use this log API and display a toast/notification message to the user. import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts log.debug("debug message") log.error("error message") log.info("info message") log.trace("trace message") log.warn("warn message") ``` # Network :::danger This API is implemented but not ready for use yet. ::: # Notification It is used to send native system notification. import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts await notification.sendNotification("Extension Loaded.") ``` # Open **Docs:** https://docs.api.kunkun.sh/interfaces/index.IOpen import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts open.url("https://kunkun.sh") // open url in default app open.folder("/Users/kksh/Downloads/") // open folder open.file("/Users/kksh/Downloads/schema.mp4") // open file in default app ``` ## Permissions Open API may potentially be used to open malicious URLs or files. To prevent this, detailed permissions must be declared in the manifest file. Only URLs and file paths matching the declared patterns will be allowed. - `url`: `'open:url'` permission is required to use this API. - `folder`: `open:folder` permission is required to use this API. - `file`: `open:file` permission is required to use this API. ### `url` ```json title="package.json" { ... "permissions": [ { "permission": "open:url", "allow": [ { "url": "https://kunkun.sh" }, { "url": "https://kunkun.sh/**" }, { "url": "http://kunkun.sh" }, { "url": "mailto://**@gmail.com" } ] } ], ... } ``` To allow any https URL, use `https://**`. ### `folder` The following permissions means - Allow to open any folder under the `Desktop` folder (recursively) - Allow to open the `Downloads` folder - Allow to open any file in the `dev` folder under the `Documents` folder > See FS API for more details on the path pattern and folder alias. ```json title="package.json" { ... "permissions": [ { "permission": "open:folder", "allow": [ { "path": "$DESKTOP/**" }, { "path": "$DOWNLOAD" }, { "path": "$DOCUMENT/dev/*" } ] } ], ... } ``` ### `file` Similar to `openFolder`, but only allow to open files. Can be used set to open specific files only. > See FS API for more details on the path pattern and folder alias. ```json title="package.json" { ... "permissions": [ { "permission": "open:file", "allow": [ { "path": "$DESKTOP/**" }, { "path": "$DOWNLOAD/schema.json" } ], "deny": [ { "path": "$DESKTOP/malicious.sh" } ] } ], ... } ``` # OS **Docs:** https://docs.api.kunkun.sh/interfaces/index.IOs This API provides some basic info about the OS. import OsApiList from "@/components/api/os-api-list.astro" import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts const platform = await os.platform() console.log(platform) // "windows" | "mnacos" | "linux" ``` # Path **Docs:** https://docs.api.kunkun.sh/interfaces/index.IPath No permission is required to use this API. This API is very trivial and simple. It is used to get the path of various directories. Read the docs for more. import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts const videoPath = await path.join(await path.desktopDir(), 'kunkun.mp4') // sample output: /Users/kksh/Desktop/kunkun.mp4 ``` ## Full List of APIs import PathApiList from '@/components/api/path-api-list.astro' The documentation https://docs.api.kunkun.sh/interfaces/index.IPath contains detailed description and examples for each of the following APIs. :::note Paths API is taken from Tauri's path API `@tauri-apps/api/path`. https://v2.tauri.app/reference/javascript/api/namespacepath/#_top ::: # Security **Docs:** https://docs.api.kunkun.sh/interfaces/index.ISecurity This API provides some security and privacy utility functions. Extensions inherit system permissions from the host app (Kunkun) Some features may require access to sensitive permissions, while Kunkun may not have access to them. With security API, you can check if certain permissions are granted to Kunkun and reveal the security pane to let user grant them. For example, on MacOS, if your extension needs to take screenshots or record screen, user has to grant screen capture permission to Kunkun. Otherwise, the content captured will be empty. A `verifyFingerprint()` API is also provided to verify the fingerprint of the current user. If your extension stores any sensitive data, you may want to verify the fingerprint to ensure the user is the one who is using the extension. ## Platforms :::caution MacOS has more security limitations that could affect the usage of extensions, currently this security API is only for MacOS. ::: - [x] MacOS - [ ] Windows - [ ] Linux ## API and Permissions import SecurityApiList from "@/components/api/security-api-list.astro" ## Usage Here is a simple demo to ask user for screen capture permission and reveal the security pane. import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts const verified = await security.mac.verifyFingerprint() const hasPermission = await security.mac.checkScreenCapturePermission() if (hasPermission) { const granted = await security.mac.requestScreenCapturePermission() // or await security.mac.revealSecurityPane("ScreenCapture") } // Sometimes, if user denies a permission, the prompt won't show up the next time. // You can reveal the security pane. // If you still want the prompt to show up, you may want to reset the permission. await security.mac.resetPermission("ScreenCapture") ``` # Shell **Docs:** https://docs.api.kunkun.sh/types/ui.IShell :::danger This API is dangerous and should be used with caution. Unless you have a good reason to use it, you should not use it. KK has provided many common and cross-platform APIs to interact with the system, and you should use them instead. Extension developers are expected to explain why they need to use this API in their PR. KK has implemented some measures to reduce the risk of using this API, but it should still be used with caution. ::: Shell API provides a way to execute shell commands/scripts and spawn processes. Commands executed runs at once and are not interactive. `stdout` and `stderr` are captured and returned as a result. If you are running a long running process and need to capture the `stdout`/`stderr` stream, or interact with `stdin`, you should use the `spawn` API. More examples will be provided below. All shell permissions are scoped, read the documentation carefully before using them. Without proper permissions, KK's API will deny the shell command. ## API and Permissions Here is a list of APIs and permissions required to use them. import ShellAPI from '@/components/api/shell.astro' :::tip Deno API is also part of the shell API, it's moved to a separate page: [Deno API](/developer/api/deno/) ::: ## Command ### Execute Command import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts const cmd = shell.createCommand("echo", ["Hello World"]) const output = await cmd.execute() console.log(output.stdout) // Hello World ``` To execute a command, you need to add scoped permission for the command you are executing. This is used to prevent extensions from being hacked and running injected malicious code. So always make the permission as specific as possible. Here is an example permission for the sample code above. ```json title="package.json" ... "permissions": [ { "permission": "shell:execute", "allow": [ { "cmd": { "program": "echo", "args": [ "Hello World" ] } } ] }, ... ], ... ``` Each item in the `args` array is a regex to match the argument. You can add a permission like this to allow dynamic arguments. ```json title="package.json" ... "permissions": [ { "permission": "shell:execute", "allow": [ { "cmd": { "program": "echo", "args": [ "[a-zA-Z0-9\s]+" ] } } ] }, ... ], ... ``` ### Spawn Command Executed command runs at once and are not interactive. `stdout` and `stderr` are captured and returned as a result. If you are running a long running process and need to capture the `stdout`/`stderr` stream or event interact with `stdin`, you should use the `spawn` API. For example, this could be useful if you are converting a video file with `ffmpeg`, and need to read stdout to get the progress throughout the conversion process. Here is a simplified example of how to use the `spawn` API. ```ts const cmd = shell.createCommand("echo", ["Hello World"]) let stdout = "" let stderr = "" cmd.on("close", (data) => { console.log(`command finished with code ${data.code} and signal ${data.signal}`) // sample output: "command finished with code 0 and signal null" }) cmd.on("error", (error) => { console.error(error) }) cmd.stdout.on("data", (line) => { stdout += line }) cmd.stderr.on("data", (line) => { stderr += line }) await cmd.spawn() ``` Spawn also requires its own scoped permission (`shell:spawn`). ```json title="package.json" ... "permissions": [ { "permission": "shell:spawn", "allow": [ { "cmd": { "program": "echo", "args": [ "Hello World" ] } } ] }, ... ], ... ``` :::tip The `await cmd.spawn()` doesn't block. You need to rely on the `close` event to know when the command has finished. Read the last section for a promise example on `spawn` which makes it easier to work with. ::: ## Execute Scripts Directly Sometimes adding args one by one is inconvenient, especially for extensions that need to run a lot of shell scripts (like an emulated terminal). KK provides some convenient methods for executing scripts directly. This API allows extension to run any shell script, which could be potentially **unsafe**. :::danger It's not recommended to use this API. Extensions without proper reason will be rejected. ::: - `executeBashScript` (`bash -c`) - `shellExecutePowershellScript` (`powershell -Command`) - `executeAppleScript` (`osascript -e`) - `executePythonScript` (`python -c`) - `executeZshScript` (`zsh -c`) - `executeNodeScript` (`node -e`) ```ts import { shell } from "@kksh/api/ui/template" await shell.executeAppleScript("display dialog \"Hello World\"") // macOS only, display a dialog const ret = await shell.executePythonScript("print('Hello World')") console.log(ret.stdout) ``` ### Permissions Direct shell script execution use the entire script as one argument. Thus the scoped permission will look like this. `program` is the command to execute, first arg can be `-c`, `-e` etc. depending on which shell you are using. Check the list above. The second argument is a regex to match the script. In this example `.+` is used to match any script, you should replace it with a more specific regex. ```json title="package.json" ... "permissions": [ { "permission": "shell:execute", "allow": [ { "cmd": { "program": "osascript", "args": [ "-e", ".+" ] } }, { "cmd": { "program": "python", "args": [ "-c", ".+" ] } } ] }, ... ], ... ``` ## Make Command Script The previous section provides APIs to execute scripts directly. You can't interact with the script. In this section, we will create a command script object and execute/spawn it. ```ts const cmd = shell.makeBashScript("echo \"Hello World\"") const output = await cmd.execute() console.log(output.stdout); // or spawn the command let stdout = "" let stderr = "" cmd.on("close", (data) => { console.log(`command finished with code ${data.code} and signal ${data.signal}`) // sample output: "command finished with code 0 and signal null" }) cmd.on("error", (error) => { console.error(error) }) cmd.stdout.on("data", (line) => { stdout += line }) cmd.stderr.on("data", (line) => { stderr += line }) await cmd.spawn() ``` And of course, you need to add permission for running bash scripts. ```json title="package.json" ... "permissions": [ { "permission": "shell:execute", "allow": [ { "cmd": { "program": "bash", "args": [ "-c", ".+" ] } } ] }, ... ], ... ``` ## More Example Code ### Promisify `spawn` You can turn the spawned command into a promise, so you can easily work with it. Here we define a custom `execute` function that returns a promise. You can write custom logic to handle the `stdout` and `stderr` stream. The final output is returned as a promise. ```ts function execute( command: shell.Command, ): Promise<{ stderr: string; stdout: string }> { return new Promise((resolve, reject) => { let stdout = ""; let stderr = ""; command.on("close", (data) => { return resolve({ stdout, stderr }); }); command.on("error", (error) => reject(error)); command.stdout.on("data", (line) => { stdout += line; }); command.stderr.on("data", (line) => { stderr += line; }); command.spawn(); }); } const command = shell.createCommand("ffprobe", [ "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", videoPath, ]); return execute(command).then(({ stdout, stderr }) => { console.log(stdout); console.log(stderr); }); ``` # System Info **Docs:** https://docs.api.kunkun.sh/interfaces/index.ISystemInfo This API provides information about the system, such as CPU, memory, and disk usage. ## Sample Code import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts await sysInfo.refreshCpu() const cpuInfo = await sysInfo.cpuInfo() cpuInfo.cpus.forEach((cpu) => { console.log("name: ", cpu.name) console.log("frequency: ", cpu.frequency) console.log("cpu_usage: ", cpu.cpu_usage) console.log("vendor_id: ", cpu.vendor_id) console.log("brand: ", cpu.brand) }) ``` ## API and Permissions import SysInfoAPI from '@/components/api/sysinfo.astro' # System **Docs:** https://docs.api.kunkun.sh/interfaces/index.ISystem System API contains commands to control the system. This is a work in progress, some APIs are not yet implemented. ## Required Permissions import SystemAPI from "@/components/api/system.astro" # Toast **Docs:** https://docs.api.kunkun.sh/interfaces/index.IToast If you are using custom UI extension running in iframe, you can build your own toast component and use the API to show toast messages. - `toast.message` - `toast.info` - `toast.success` - `toast.warning` - `toast.error` import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts // Show an error message await toast.error("Item not found") // More options await toast.error("Item not found", { description: 'The item you selected does not have a URL', duration: 3000, closeButton: true, position: 'top-left' }) await toast.warning( "Item Deleted", { actionLabel: "Undo" }, () => { toast.success("Item Restored") } ) ``` # Custom UI :::caution The `ui` API differs between custom (iframe) extensions and template (web worker) extensions. ::: ## Custom (Iframe) Extensions **Docs:** https://docs.api.kunkun.sh/interfaces/ui.IUiIframe ### Built-in Buttons A custom extension rendered with iframe will take up the entire view port of the window. The extension provides a few optional built-in buttons to help users navigate and control the extension if the extension does not provide its own navigation and control buttons. - `showBackButton` - Show a back button to navigate back to the main page - Will close window if your extension runs in a separate window - `showMoveButton` - Show a move button user can grab to move the app window around - Designed for scenarios where extension doesn't have a title bar - You can choose to make any element in your extension draggable as well - `showRefreshButton` - Show a refresh button to reload the extension, could be useful during development You can hide these buttons if you want to provide your own - `hideBackButton` - `hideMoveButton` - `hideRefreshButton` Here we use the back button as an example. ```ts import { ui } from "@kksh/api/ui/custom" type CustomPosition = { top: number, right: number, bottom: number, left: number } type Position = "top-left" | "top-right" | "bottom-left" | "bottom-right" | CustomPosition; await ui.showBackButton("bottom-right") // or set custom position await ui.showBackButton({ top: 5, right: 5 }) // hide the back button if you want to provide your own await ui.hideBackButton() ``` ### Navigation ```ts import { ui } from "@kksh/api/ui/custom" /** * Will return to main page if the extension runs in the main window * Will close the window if the extension runs in a separate window */ ui.goBack() /** * Will reload the window */ ui.reloadPage() ``` ### Window Control ```ts import { ui } from "@kksh/api/ui/custom" // maximize the window ui.toggleMaximize() // set window background to transparent // if the extension also has a transparent background, this will make the extension appear to be floating ui.setTransparentWindowBackground(true) ``` ### Theme Get KK's color theme, if your extension is using KK component libraries, you can set your extension theme to match KK's theme. import UpdateUiTheme from "./snippets/update-ui-theme.mdx" :::note Dependending on which KK component library you are using, you may need to wrap your app with a `ThemeProvider` for the theme to take effect. Read the component library documentation for more information. ::: # Template UI :::caution The `ui` API differs between custom (iframe) extensions and template (web worker) extensions. ::: ## Template (Web Worker) Extensions **Docs:** https://docs.api.kunkun.sh/interfaces/ui.IUiWorker A template extension scaffold can be created with the following command: ```bash npm init kunkun@latest ``` Read the [extensions section](/extensions/worker-template) first to learn more about this kind of extension. `ui` API provides the following methods: - `showLoadingBar` - Display a loading bar at the top of the page ```ts ui.showLoadingBar(true); // show loading bar ui.showLoadingBar(false); // hide loading bar ``` - `setScrollLoading` - Display a loading spinner when the user scrolls to the bottom of list view - `setSearchTerm` - Set the search term in the search bar - `setSearchBarPlaceholder` - Set the placeholder text in the search bar - `render` - Render a extension based on template, (e.g. list view, form view, etc.) I will explain how to use the `ui` API to build a template extension. Sample code can be found in the [extensions section](/extensions/worker-template). # Updownload > Download and upload files **Docs:** https://docs.api.kunkun.sh/interfaces/index.IUpdownload ## API and Permissions import ImportCodeBlock from '@/components/api/import-code-block.astro' ```ts await updownload.download( 'https://schema.kunkun.sh', await path.join(await path.desktopDir(), 'kk-ext-schema.json') ); ``` :::note Updownload API is taken from Tauri's Upload Plugin https://v2.tauri.app/plugin/upload/ ::: # Component Library KK supports turning any static website into an extension. However, if you are building a custom UI extension from scratch it is recommended to use KK's Component Library to keep the UI styling consistent with KK's design. To keep the style consistent across different frameworks, KK uses TailwindCSS + Shadcn. Shadcn has ports for multiple popular frontend frameworks. For now, KK supports React, Vue, and Svelte. Shadcn adds component source code to your project, so you can customize the component as you like, but I want to make it easy for developers to use the components without worrying about dealing with the component files. So I created component libraries wrapping Shadcn components for React, Vue, and Svelte so you can import components directly. :::note Since the component libraries are just wrappers around Shadcn components, the usage is pretty much exactly the same. I won't build a full documentation for each library. A few examples are provided for each library in the following pages. To see the full list of components, please refer to the Shadcn documentation. - [Shadcn React](https://ui.shadcn.com/) - [Shadcn Vue](https://www.shadcn-vue.com/) - [Shadcn Svelte](https://www.shadcn-svelte.com/) ::: :::tip If you use the KK's provided scaffolding CLI [**create-kunkun**](https://www.npmjs.com/package/create-kunkun) to create a new project, the component libraries are already installed and ready to use. ::: # React import ReactCmdDemo from "@/components/demo/react/command.astro"; import { Code } from '@astrojs/starlight/components'; import { Tabs, TabItem } from '@astrojs/starlight/components'; import ButtonDemo from "@/components/demo/react/button.astro"; **NPM Package:** `@kksh/react` ![NPM Version](https://img.shields.io/npm/v/%40kksh%2Freact) **Original Shadcn:** https://ui.shadcn.com/ Normally, Shadcn components are imported from local `@/components/ui` directory. Now you can import the components from `@kksh/react` package. Most of Shadcn components are exported from `@kksh/react` package. ## Component Examples ### Button Example
```tsx import { Button } from "@kksh/react" export function ButtonDemo() { return } ```
### Command Palette Example
```tsx import { CalendarIcon, EnvelopeClosedIcon, FaceIcon, GearIcon, PersonIcon, RocketIcon, } from "@radix-ui/react-icons"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, } from "@kksh/react"; export default function CommandDemo() { return ( No results found. Calendar Search Emoji Launch Profile ⌘P Mail ⌘B Settings ⌘S ); } ```
## Theme You can get KK's color theme and set your extension theme to match KK's theme. `@kksh/react` requires `` and `` components to be wrapped around the root component. - `` is responsible for dark/light mode switching - `` is responsible for setting the color theme ```tsx import { ui } from "@kksh/api/ui/custom" import { updateTheme } from "@kksh/react" useEffect(() => { ui.getTheme().then((theme) => { updateTheme(theme) }) }, []) return (
...
) ``` For color theme to take effect, you need to import the theme styles from `@kksh/vue/themes`. ```css title="styles.css" @import url("@kksh/react/css"); @import url("@kksh/react/themes"); ``` import ThemeCustomizer from '@/components/demo/react/theme-customizer.astro';
Sample Theme Customizer
# Svelte import { Code } from "@astrojs/starlight/components"; import SvelteCmdDemo from "@/components/demo/svelte/command.svelte"; import { Button } from "@kksh/svelte5"; import { Tabs, TabItem } from "@astrojs/starlight/components";
**NPM Package (svelte 4):** `@kksh/svelte5` ![NPM Version](https://img.shields.io/npm/v/%40kksh%2Fsvelte) As svelte 5 is released in 2024, we have released a new package `@kksh/svelte5` for svelte 5. **NPM Package (svelte 5):** `@kksh/svelte5` ![NPM Version](https://img.shields.io/npm/v/%40kksh%2Fsvelte5) **Original Shadcn Svelte:** https://www.shadcn-svelte.com/ Normally, Shadcn components are imported from local `@/components/ui` directory. Now you can import the components from `@kksh/svelte5` package. Most of Shadcn components are exported from `@kksh/svelte5` package. ## Component Examples ### Button Example
```svelte ```
### Command Palette Example
```svelte No results found. Calendar Search Emoji Launch Profile ⌘P Mail ⌘B Settings ⌘S ```
## Theme You can get KK's color theme and set your extension theme to match KK's theme. `@kksh/svelte` requires `` component to be wrapped around the root component. ```svelte
...
``` For color theme to take effect, you need to import the theme styles from `@kksh/vue/themes`. ```css title="styles.css" @import url("@kksh/svelte5/themes"); ``` # Vue import { Code } from '@astrojs/starlight/components'; import VueCmdDemo from "@/components/demo/vue/command.vue"; import { Button } from '@kksh/vue/button' import { Tabs, TabItem } from '@astrojs/starlight/components'; **NPM Package:** `@kksh/vue` ![NPM Version](https://img.shields.io/npm/v/%40kksh%2Fvue) **Original Shadcn Vue:** https://www.shadcn-vue.com/ Normally, Shadcn components are imported from local `@/components/ui` directory. Now you can import the components from `@kksh/vue` package. Most of Shadcn components are exported from `@kksh/vue` package. For now, `@kksh/vue` has some treeshaking issues, importing from `@kksh/vue` may potentially add all components to your bundle. I didn't figure out how to fix it yet. If you are an expert in building Vue library, please help me. The source code is at https://github.com/kunkunsh/kunkun-components/tree/main/packages/vue. The best way to import components from this library is to import from the subpackages, like `@kksh/vue/button` or `@kksh/vue/command`. ## Component Examples ### Button Example
```vue ```
### Command Palette Example
```vue ```
## Theme You can get KK's color theme and set your extension theme to match KK's theme. `@kksh/vue`'s `updateTheme()` function set the theme globally, there is no need for a `ThemeProvider`. ```ts import { ui } from "@kksh/api/ui/custom" import { updateTheme } from "@kksh/vue" ui.getTheme().then((theme) => { updateTheme(theme) }) ``` For color theme to take effect, you need to import the theme styles from `@kksh/vue/themes`. ```css title="styles.css" @import url("@kksh/vue/css"); @import url("@kksh/vue/themes"); ``` # Developer Experience As a developer myself, I always want to make the development experience as smooth as possible. **KK** is designed to be developer-friendly, with a focus on the following aspects: 1. Debugability - When I have a problem with some apps, I don't know what's going on behind the scenes, and wish there is log to help me debug. - **KK** will provide a log system to help you debug your extension as well as the app itself. - **[WIP]** Troubleshooters for common issues will be provided. They will analyze automatically and give you suggestions. # Extension Manifest Validator > Kunkun Extension Manifest Validator :::caution Kunkun is still in early development, the extension schema is not finalized yet, and could change any time. I will try to make the schema as stable as possible, and backward compatible. But there is no guarantee in the current stage. To get the schema for latest app release: `https://schema.kunkun.sh/`. To get the nightly release for latest app in development: `https://schema.kunkun.sh/nightly/`. ::: Kunkun extensions metadata are defined in `package.json`. Writing manifest file in json without intellisense is error-prone and comfusing. For better DX, I provide some tools for manifest file validation and intellisense. ## Json Schema In the `package.json` of your extension, you can add a `$schema` field. In VSCode and most modern code editors, this will enable intellisense for the fields in the manifest file. Missing fields and wrong data types will be highlighted. The schema will run through a validator during loading and publishing. Without a valid manifest file, you will not be able to develop or publish the extension. ```json title="package.json" { "$schema": "https://schema.kunkun.sh", ... } ``` If you don't want to include this in your `package.json`, you can also configure VSCode workspace setting ```json title=".vscode/settings.json" { "json.schemas": [ { "fileMatch": ["/package.json"], "url": "https://schema.kunkun.sh" } ] } ``` ## Key Fields - `name` (`string`): The name of the extension. - `version` (`string`): The version of the extension. - `files` (`string[]`): The files to be included in the publish package. Only include necessary files. e.g. most of the time `["dist"]` is enough. - `kunkun`: Add extension details and commands info here. ## Playground Here are two playgrounds with schema enabled. You can validate your extension manifest or experiment with the intellisense. :::caution You should see a yellow wiggle line under the line 1 of this `package.json`. Hover over it to see the error message. :::
import { Image } from "astro:assets"; import KunkunExtArch from "../../../assets/design/kunkun-ext-arch.png"; Kunkun Extension Architecture The communication between KK and the extension is done through the `kkrpc` protocol. This is how `kkrpc` works in browser between iframe, main thread, web worker and server. ![](../../../assets/design/kkrpc-web.png) Here is how it's used in Kunkun's extension system. ![](../../../assets/design/kunkun-ext-arch-2.png) # Custom UI Command
Custom UI command is just a web app. You can use any front-end framework (React, Vue, Angular, Svelte, etc) to build arbitrarily complex UI, as long as it can be built into static files (e.g. SSG, CSR, but not SSR). For example, you can create a transparent window with a 3D model floating on your screen. Here is an example of KK's git-skyline extension,
The headless command is simply a [Template UI Command](/extensions/worker-template) without UI. Use APIs exported from `"@kksh/api/headless"`. Some APIs for rendering UI are removed in `"@kksh/api/headless"`. It's useful for some simple tasks without needing to open a new page. Run the project scaffolding command to create a new project with Worker Template Extension. :::note Currently, the headless command is just a simplified version of the [Template UI Command](/extensions/worker-template) by removing UI. In the future, more features will be added to the headless command. Such as cron jobs, etc. ::: ```bash npm init kunkun@latest # choose the template ``` ## `package.json` ```json { ..., "kunkun": { ... "identifier": "template-ext-headless", "permissions": [ "clipboard:write-text" ], "icon": { "type": "iconify", "value": "material-symbols:extensiontabler:code" }, "headlessCmds": [ { "name": "Template: Headless Command UUID", "main": "dist/index.js", "cmds": [] } ] }, ... } ``` ## Sample UUID Extension This simple extension simply generates a UUID v4 and copies it to the clipboard. Thus why `"clipboard:write-text"` permission is required in `package.json`. `HeadlessCommand` is the base class for headless commands. ```ts import { clipboard, expose, HeadlessCommand, toast, } from "@kksh/api/headless"; import { v4 as uuidv4 } from "uuid"; class UuidExt extends HeadlessCommand { async load() { const uuid = uuidv4(); return clipboard .writeText(uuid) .then(() => { toast.success(`Copied UUID: ${uuid}`); }) .catch((err) => { toast.error(`Failed to copy UUID: ${err}`); }); } } expose(new UuidExt()); ``` ## Publish See [Guide: Extension Publish](/guides/extensions/publish/design) for more details. # Extension Permissions > Extension Permissions :::caution Work In Progress The project is still under early development, more permissions will be added in the future. If you need any permissions or have any suggestions, plean open an issue on GitHub or reach out to me on discord. ::: If a permission requires access to system APIs such as clipboard, file system, etc., the extension must declare the permission in the `package.json` file, under `kunkun.permissions` field. Go to [Manifest Validator](/developer/manifest) to experiment with the permissions. The requested permissions will be shown to the user when the extension is installed. Here is a list of all permissions and their descriptions. Some permissions are scoped (e.g. file system API has limited access to certain directories declared in the manifest). To learn how to use them, read the API documentation. import PermissionExplanation from "../../../../components/dev-tools/permission-explanation.astro"; # Extension Security > Extension Security Design Extension is a powerful feature that allows users to extend the functionality however they want. However, great functionality may require access to file system and system APIs, which can be dangerous if not properly secured. Since community maintained extensions can be installed to Kunkun, there is no guarantee that the extension is safe, just like other extension systems like Chrome extensions, VSCode Extensions, Raycast Extensions, etc. We can only try to maintain a safety design to minimize the risk. All community extensions are open sourced on GitHub, and published to NPM or JSR with provenance statements. See [Design](/guides/extensions/publish/design/) for more details. Provenance statements are not a guarantee of security, but it's a good start. With provenance statements, we can at least know the origin of the extension and the build process. ## Permission Management System Similar apps like Raycast gives extensions full access to the file system and system APIs. This gives extensions a lot of power to do anything they want, but also makes it dangerous. Although their extensions are all open sourced for review, there is still a risk that the extension can do something malicious. KK also takes advantages of system APIs to provide powerful features, but it also has a multi-level permission management system to control what an extension can do. KK is designed to take a similar approach to Chrome extensions and Tauri, where the required permission for an app must be declared in the manifest file. The extension store will show the required permissions for each extension, and the user can decide whether to install it or not. If an extension accessed APIs they didn't declare in the manifest file, the behaviour will be logged, and the user will be notified. For example, if a timer extension is expected to request notification permission, but not clipboard permission. KK splits exposed APIs into different permission groups. e.g. clipboard APIs have `clipboard:read-text`, `clipboard:read-all`, `clipboard:write-image` and more. Extensions should only request the minimum required permissions. File system APIs will also have different access scopes. e.g. 1. Access to Desktop, Documents, Downloads 2. Access to all files in the user's home directory 3. Access to all files in the system Fore more details about permission declaration, see [Extension Permissions](/extensions/security/permissions) and the documentation of each API. ## Application Lifecycle Threats ![](https://hacker-storage.s3.us-east-2.amazonaws.com/2024/5/14/application-flow-simple.KMHPzXtV_5V2Cf.svg) ### Buildtime Threats Buildtime threats are the threats that happen when the extension is being built. e.g. All extensions are open sourced with provenance statements. One can easily find the exact repo, commit and GitHub Action workflow that built the extension. However, it is possible that some extensions includes malicious code that can be triggered during build time. # Template UI Command Worker Template command is a kind of lightweight extension command running in web worker (30KB+), following pre-defined template, making it easy to develop extension with consistent GUI. ## Installation Prerequisites: - Node.js 20+ - [bun](https://bun.sh/) - bun is used to build extension from TypeScript into JavaScript. Run the project scaffolding command to create a new project with Worker Template Extension. ```bash npm init kunkun@latest # choose the template ``` After creating a template project, you need to edit `package.json`. ## Development ### Add Dev Extension to Kunkun import AddDevExt from "./snippets/add-dev-ext.mdx"; ### Edit Manifest KK uses `package.json` to define an extension. See [Manifest](/developer/manifest/) for more details. You need to edit the name, description, identifier and icon of your extension. #### Permissions KK provides many APIs within `@kksh/api` package. See [API Docs](/developer/api/api/) for more details. Most of them requires adding corresponding permissions in `package.json`. For example, if you want to read clipboard text, you need to add `clipboard:read-text` permission in `package.json`, under `kunkun.permissions` field. See [Extension Permissions](/extensions/security/permissions/) for a full list of permissions, and API docs for each API to see how to add permissions. Some permissions are just a single string, some are scoped permissions. ### Start Coding ```bash bun install bun run build # build the extension, generate dist/index.js file, this is the entry file of the extension, make sure it's listed in the package.json bun dev # start the development server, basically running `build` with watch mode ``` In dev mode (`bun dev`), every time you save the file, the extension will be recompiled and Kunkun desktop app will be notified to reload the extension. Otherwise you need to manually reload the extension. A template worker extension should `extends` abstract class `TemplateUiCommand` (https://docs.api.kunkun.sh/classes/ui_worker.TemplateUiCommand) Override the methods of this class to implement your extension. - `load` - `onSearchTermChange` - `onActionSelected` - `onEnterPressedOnSearchBar` **List View** - `onListItemSelected` - `onListScrolledToBottom` - `onHighlightedListItemChanged` **Form View** - `onFormSubmit` ```ts import { expose, TemplateUiCommand } from "@kksh/api/ui/template"; class DemoExtension extends TemplateUiCommand { async load() { // load method is run when the extension is loaded, you can use it as an initializer } } expose(new DemoExtension()); // expose this extension at the bottom of the file ``` There is no concept like routing in worker template extension. Different views can be rendered with `ui.render()` method. You can add any variables to the Extension class to keep track of the state and render different views based on the state. Once the command is built, a js file will be generated at `dist/index.js`. Refresh kunkun app, you should see the command. ### Advanced Usage The scaffold template uses `bun` to build the extension from TypeScript into JavaScript. You can install other dependencies (they need to run in web worker), add more `.ts` files. Eventually they will be bundled into a single js file and run in web worker. `build.ts` in the template project contains the build script, you can modify it as you want, or replace bun with other build tools like esbuild, rollup, vite, etc. `@rollup/plugin-terser` is known to have some problems with the current `@kksh/api` package. `bun` is the simpliest and fastest tool for this job.
Sample Build Script ```ts import { watch } from "fs"; import { join } from "path"; import { refreshTemplateWorkerCommand } from "@kksh/api/dev"; import { $ } from "bun"; async function build() { await $`bun build --minify --target=browser --outdir=./dist ./src/index.ts`; await refreshTemplateWorkerCommand(); } const srcDir = join(import.meta.dir, "src"); await build(); if (Bun.argv.includes("dev")) { console.log(`Watching ${srcDir} for changes...`); watch(srcDir, { recursive: true }, async (event, filename) => { await build(); }); } ```
## Views Template Command follows templates. Each template is a type of view. ### Markdown View Basically renders markdown on page. ```ts import { expose, Form, ui, TemplateUiCommand } from "@kksh/api/ui/template" class DemoExtension extends TemplateUiCommand { load() { return ui.render(new Markdown("# Hello, World!")) } ... } expose(new DemoExtension()) ``` ![](./markdown-view-demo.png) ### Form View A form can be easily rendered with the `ui` API. Every form field is an object. Available form fields: - `Form.InputField` - `Form.NumberField` - `Form.SelectField` - `Form.BooleanField` - `Form.DateField` - `Form.ArrayField` - `Form.Form` ```ts import { expose, Form, ui, TemplateUiCommand } from "@kksh/api/ui/template"; class DemoExtension extends TemplateUiCommand { async onFormSubmit(value: Record): Promise { console.log("Form submitted", value); } async load() { return ui.render( new Form.Form({ key: "form1", fields: [ new Form.NumberField({ key: "age", label: "Age", placeholder: "Enter your age", }), new Form.InputField({ key: "name", }), ], }) ); } } expose(new DemoExtension()); ``` ![](./form-view-demo.png) ### List View List view can contain sections and items. Everything is an object. An list item can have title, value, icon and actions. Each item can contain multiple actions. When selected, `onActionSelected()` will be called. ![](./list-view-demo.png) ```ts import { Action, expose, Icon, IconEnum, List, ui, TemplateUiCommand, } from "@kksh/api/ui/template"; class DemoExtension extends TemplateUiCommand { async onFormSubmit(value: Record): Promise { console.log("Form submitted", value); } async load() { return ui.setSearchBarPlaceholder("Enter search term").then(() => { return ui.render( new List.List({ sections: [ new List.Section({ title: "Section 1", items: [ new List.Item({ title: "Hello, World!", value: "Section 1 Hello, World!", icon: new Icon({ type: IconEnum.Iconify, value: "gg:hello" }), }), new List.Item({ title: "Hello, World 2!", value: "Section 1 Hello, World 2!", }), ], }), new List.Section({ title: "Section 2", items: [ new List.Item({ title: "Hello, World!", value: "Section 2 Hello, World!", icon: new Icon({ type: IconEnum.Iconify, value: "gg:hello" }), }), ], }), ], items: [ new List.Item({ title: "Hello, World!", value: "Hello, World!", icon: new Icon({ type: IconEnum.Iconify, value: "ri:star-s-fill", }), }), new List.Item({ title: "Hello, World 2!", value: "Hello, World 2!", icon: new Icon({ type: IconEnum.Iconify, value: "gg:hello" }), actions: new Action.ActionPanel({ items: [ new Action.Action({ title: "Open", value: "open", icon: new Icon({ type: IconEnum.Iconify, value: "ion:open-outline", }), }), ], }), }), ], }) ); }); } async onSearchTermChange(term: string): Promise { console.log("Search term changed to:", term); } async onItemSelected(value: string): Promise { console.log("Item selected:", value); } async onActionSelected(value: string): Promise { console.log("Action selected:", value); } } expose(new DemoExtension()); ``` import VerifyInstructions from "./snippets/verify-instructions.mdx"; ## Publish See [Guide: Extension Publish](/guides/extensions/publish/design) for more details. # Contributing https://github.com/kunkunsh/kunkun/blob/develop/CONTRIBUTING.md If you are interested in contributing to the project, please read the following guidelines. ## Development ### Prerequisites - [Node.js](https://nodejs.org/en) - [pnpm](https://pnpm.io/) - [Bun](https://bun.sh/) - [Deno](https://deno.com/) - [Rust](https://www.rust-lang.org/) - [protobuf](https://grpc.io/docs/protoc-installation/) - MacOS: `brew install protobuf` - Linux: `sudo apt install -y protobuf-compiler` - Windows: ```powershell choco install protoc choco install openssl ``` Then configure the environment variables (yours may differ): - `OPENSSL_DIR`: `C:\Program Files\OpenSSL-Win64` - `OPENSSL_INCLUDE_DIR`: `C:\Program Files\OpenSSL-Win64\include` - `OPENSSL_LIB_DIR`: `C:\Program Files\OpenSSL-Win64\lib` - [cmake](https://cmake.org/) - MacOS: `brew install cmake` - Linux: `sudo apt install -y cmake` ### Setup ```bash git clone https://github.com/kunkunsh/kunkun.git --recursive pnpm install pnpm prepare ``` ### Run Desktop App ```bash pnpm --filter @kksh/desktop tauri dev # or run it within the desktop app directory cd apps/desktop pnpm tauri dev ``` ## i18n If you are willing to help with the translation, please use translations in json files in `apps/desktop/messages`. Use `en.json` as a reference. # Demo > Demonstration of using Kunkun In the demo video below, I discussed the following: - What is Kunkun? - Why was is created? - Open Source - Cross-platform - Security and Privacy (achieved with a strict permission model) - More flexible (with web technologies, like Svelte) - How to use it?
:::tip See [YouTube Playlist](https://www.youtube.com/playlist?list=PLUxw2JoWliirhQROHwpa0yMEYiUHoGw2_) for more videos. ::: ## Installation Download Kunkun's installer from https://kunkun.sh/download. :::caution Although installer for Windows and Linux are available, they are not yet fully tested and have compatibility issues. It's recommended to use Mac for the best experience. :::
On Mac, if it asks for password to access keychain, you can select "Always Allow". I will explain. It uses system keychain to store a randomly generated password to encrypt sqlite database, instead of using the same encryption key on everyone's machine. Storing the password in a regular file would be a security risk. Even with obfuscation or encryption, it's still possible to reverse engineer the binary to get the password. System keychain is the most secure way to store the password, but requires user to enter password when the app updates.
## Features 1. Key Displayer: Display the keys pressed on the keyboard. 2. Clipboard history 3. File Transfer (send files to other devices in local network with Kunkun installed) 1. Support files and folders 2. If you don't have another computer in local network, you can try to send files to your own computer 4. Store (install extensions from the store) ## Screenshots ### Extension Store ![](../../../assets/demo/extension-store.png) ### Clipboard History ![](../../../assets/demo/clipboard-history.png) ### Disk Speed ![](../../../assets/demo/disk-speed.png) ### File Transfer ![](../../../assets/demo/file-transfer-1.png) ![](../../../assets/demo/file-transfer-2.png) ### Generate QR Code ![](../../../assets/demo/generate-qrcode.png) ### Hacker News ![](../../../assets/demo/hacker-news.png) ### Image Info ![](../../../assets/demo/image-info.png) ### IP Info ![](../../../assets/demo/ip-info.png) ### JWT Viewer ![](../../../assets/demo/jwt-viewer.png) ### Key Displayer ![](../../../assets/demo/key-displayer.png) ### Letterboxd Movie Search ![](../../../assets/demo/letterboxd-movie-search.png) ### Git Skyline with Transparent Window ![](../../../assets/demo/transparent-git-skyline.png) ### Video Conversion ![](../../../assets/demo/video-conversion.png) ### Video Info ![](../../../assets/demo/video-info.png) ### Extension Permission Disclaimer ![](../../../assets/demo/vscode-store-permissions.png) # Extension Installation > Installation of Extensions > Multiple methods are available for installing extensions in Kunkun. ## Extension Store In the app, search for store, open the store to see a list of extensions available for installation. In the settings, under extensions tab, you can delete extensions. ## For Developers > This section is for developers and advanced users. Regular users are recommanded to use the Extension Store to install extensions. The following methods are designed for developers and more advanced users who may want to install extensions from other sources or develop an extension. The installation interface is under settings page, developer tab. ### Remote Tarball URL Provide an URL to the tarball `.tgz` file of the extension. It can be hosted on any server, such as asset in a GitHub release, or NPM package. - e.g. https://registry.npmjs.org/@huakunshen/jarvis-ext-qrcode/-/jarvis-ext-qrcode-0.0.0.tgz ### NPM NPM is one of the most successful package manager in the world. It is used by millions of developers to share their code. It makes sense to use it to share KK extensions. However, I don't want to use `npm` command to install extensions, as that will requires end users to have `npm` installed on their system, or KK to have an `npm` binary embedded. Moreover, this will download unnecessary development dependencies of the extension. NPM's API `https://registry.npmjs.org/{package-name}/{version}` provides a link to the tarball `.tgz` file of the package. This is what we will use to download the extension. We can set `version` to latest to get the latest version of the package. ### Manual Manual mode is used to develop an extension locally. There are two directories for extensions, one of them is for developing extensions, the other is for extensions installed from store. Extensions installed from Tarball files/urls are also installed in the development extension directory. Under settings page, developer tab, you can set the dev extension path. Extensions placed in this directory will be loaded as development extensions. import { FileTree } from "@astrojs/starlight/components"; - dev-extensions - ext-qrcode - package.json - dist - ext-jwt - package.json - dist Under settings/developer page, you can also drag and drop a tarball file to install an extension. The tarball has to be produced by `npm pack` command. ### Remote Extension Under settings/developer page, you can also enter a URL of a website that hosts the extension. For example, you can use it for local development. Add a remote extension with URL: http://localhost:5173 to load your local extension through dev server with hot reload. Theoretically you could even add any website as a remote extension (e.g. https://google.com), but it is not recommended for security and privary concerns. The website could potentially run malicious code in Kunkun. Limited permissions are granted to remote extensions. They cannot access file system or clipboard, but it's still possible to leak information if you are using a non-localhost remote extension. # Extension Publish Workflow ## TLDR To publish an extension to KK's extension store, follow one of these guides: 1. [Publishing with NPM](/guides/extensions/publish/npm) 2. [Publishing with JSR](/guides/extensions/publish/jsr) ## Overview
This article explains KK's extension publishing system and its core design principles. The system is built around two fundamental goals: 1. Open Source Transparency 2. Security Assurance All community extensions must be open source for transparency and security. This requirement means that extensions are hosted on GitHub where they're available for public review and contribution. While KK implements a permission control system to restrict extension capabilities, some extensions may need elevated permissions to function properly. Even with these permission controls in place, we cannot completely guarantee that an extension won't behave maliciously. This is one key reason why we require extensions to be open source. However, open source alone doesn't ensure complete transparency and security. For example, while npm packages typically link to their GitHub source code, there's no inherent guarantee that the published package is indeed built from the source code. A malicious actor could modify code locally before publishing to npm. To address this security gap, both npm and jsr support provenance statements. See - https://docs.npmjs.com/generating-provenance-statements - https://jsr.io/docs/trust #### NPM Provenance ![](../../../../../assets/demo/instructions/npm-provenance.png) #### JSR Provenance ![](../../../../../assets/demo/instructions/jsr-provenance.png) A provenance statement verifies the origin and authenticity of software packages. It creates a verifiable record of where the code came from, how it was built, and who contributed to it. This helps ensure transparency and detect potential security risks or supply chain vulnerabilities. ## Current Implementation While we plan to implement a dedicated provenance system for KK's extension store in the future, we currently leverage the existing provenance systems from npm and jsr. The current publishing process works as follows: 1. Extensions must be published to either npm or jsr via GitHub Actions 2. Developers then register their extension through our website For detailed publishing instructions, see the following guides: 1. [Publishing with NPM](/guides/extensions/publish/npm) 2. [Publishing with JSR](/guides/extensions/publish/jsr) # With JSR
JSR is a new package registry for the JavaScript ecosystem. **TLDR** This is a very simple sample extension https://github.com/kunkunsh/kunkun-ext-ip-info. You can use it as a template. After developing your extension, you can publish it to JSR by following these steps: ## Create a package on `jsr.io/new` 1. Go to https://jsr.io/new, enter a scope (e.g. `kunkun`) and a package name (e.g. `kunkun-ext-ip-info`), and click "Create Package". 2. Link your GitHub repository to the package. ![](../../../../../assets/demo/instructions/jsr-link-github.png) ## Add a `jsr.json` file ```json title="jsr.json" { "name": "@kunkun/kunkun-ext-ip-info", "version": "0.0.5", "license": "MIT", "exports": "./mod.ts", "publish": { "include": ["dist", "README.md", "package.json", "mod.ts"] } } ``` Although you are not publishing a library, but JSR requires an `exports` field in `jsr.json`, so create an empty `mod.ts` file in the root directory of your project. Include the files you want to publish in the `publish.include` field. `package.json`, `mod.ts` are necessary, and don't forget to include the built artifact of your extension, e.g. `dist` or `build` folder. ## Add a GitHub Action Workflow In order to get provenance statements for your package, you must publish it through GitHub Action. > Of source you can experiment with publishing your package locally with `npx jsr publish` to make sure everything works. > After that, bump your package version and publish it through GitHub Action. > Any package without provenance statement will be rejected by KK's extension store. In the following sample GitHub Action Workflow file, we use `bun` for the entire workflow. You can use whatever you like, `npm`, `node`, `pnpm`, `yarn`. You can even build wasm with rust and publish it to JSR. `bunx kksh verify --publish` is an optional but recommented step. `kksh` is Kunkun's CLI tool. If the `verify --publish` command fails, the package will not be able to be published to KK's extension store. :::caution When publishing a package to JSR, make sure the version in `jsr.json` and `package.json` are the same. This is because Kunkun only uses `package.json` as it's manifest file and use its version field, but the version in `jsr.json` is what is used on JSR, and during the verification process we find your package with package name and version in `jsr.json`. `bunx kksh verify --publish` will check if the version in `jsr.json` and `package.json` are the same, so it's helpful to include it. ::: ```yaml title=".github/workflows/jsr-publish.yml" name: JSR Publish on: push: branches: - main jobs: publish: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - name: Install dependencies run: bun install - name: Build run: bun run build - name: Verify Package run: bunx kksh verify --publish - name: Publish package run: bunx jsr publish ``` :::tip If you are using monorepo, your root of extension package may not be the root of your repository. Then in GitHub Action you can use `working-directory: ./path-to-extension` under step to tell `jsr` which directory to publish. ::: ## Register your Extension Go to https://kunkun.sh/dashboard/publish-extension/jsr, enter your jsr scope. All your packages will be listed, select the one you want to publish to KK's extension store. Make sure you login with GitHub, it needs to verify the ownership of the package. ![](../../../../../assets/demo/instructions/jsr-publish.png) # With NPM
NPM is the most popular package manager for JavaScript. After developing your extension, you can publish it to JSR by following these steps: ## Obtain NPM Access Token Read https://docs.npmjs.com/creating-and-viewing-access-tokens Obtain an NPM access token and store it in your GitHub repository secrets. [Docs](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions) ## Create a package on NPM Suppose your extension will generate a `dist` folder after building. 1. Make sure your `package.json`'s `files` field includes the `dist` folder as well as everything else you want to publish. 2. Make sure your `package.json` has a `license` field with a valid license like `MIT`. 3. Make sure your `package.json` has a `repository` field with a valid GitHub repository URL. (e.g. `"repository": "https://github.com/kunkunsh/kunkun-ext-video-processing",`) - This repo url is necessary for the provenance statement. 4. `name` in `package.json` is recommended to start with `kunkun-ext` (no strict requirement). 5. `npm login` and `npm publish` Then your package should be available on NPM. `https://www.npmjs.com/package/` You may not be able to find it on NPM by searching for it, but you can access it directly by URL. ## Add a GitHub Action Workflow Make sure `npm publish --provenance` command has the `--provenance` flag. Otherwise provenance statements won't be generated. Make sure `NPM_TOKEN` is assigned to `NODE_AUTH_TOKEN` environment variable during the publish step. ```yaml title=".github/workflows/npm-publish.yml" name: NPM Package Publish on: workflow_dispatch: jobs: publish-npm: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - run: bun install - run: bun run build - name: Publish to NPM run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} ``` `workflow_dispatch:` means you have to go to GitHub Action tab in your repo and manually trigger the workflow. You can configure it to run on **release created** event or **push event** to main branch. If you let it trigger on push, publishing duplicate version to npm will fail. To prevent this, you can check the version of the package in the npm registry before publishing. Here is an example of how to do this: ```yaml title=".github/workflows/npm-publish.yml" name: NPM Package Publish on: push: branches: [main] release: types: [created] workflow_dispatch: jobs: publish-npm: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - uses: actions/setup-node@v4 with: node-version: "22.x" registry-url: "https://registry.npmjs.org" - run: bun install - run: bun run build - name: Check if version is already published run: | PACKAGE_VERSION=$(node -p "require('./package.json').version") PACKAGE_NAME=$(jq -r '.name' package.json) npm view $PACKAGE_NAME@$PACKAGE_VERSION continue-on-error: true id: check_version - name: Publish if: steps.check_version.outcome != 'success' run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} ``` ## Register your Extension Go to https://kunkun.sh/dashboard/publish-extension/npm, enter your npm package name. Make sure you login with GitHub, it needs to verify the ownership of the package. ![](../../../../../assets/demo/instructions/npm-publish.png) # Write Extension > Write your own extension in TypeScript. Install extensons from a store. > This guide only talks about how to write an extension. To learn more about the design and security model , see [Extensions Doc](/extensions). https://kunkun.sh/store is the store for KK, all published extensions are available here. :::tip I will make a video tutorial to demonstrate how to write an extension once the development process is stable. Subscribe to the [YouTube Channel](https://www.youtube.com/@huakun) to get notified when the video is released. ::: - [Write Template Extension Command](/extensions/worker-template) - [Write Headless Extension Command](/extensions/headless-command) - [Write Custom UI Extension Command](/extensions/custom-ui-ext) # Quick Link You can create a quick link with the built-in command "Create Quick Link" in the command palette. ![](../../../../assets/demo/quick-link-form.png) # Get Started > Get started with Kunkun import { ShowcaseYouTube } from "starlight-showcases"; **KK** is an open source cross-platform extensible app launcher that aims to be an alternative to Alfred and Raycast. This guide will help you get started with KK both as a user and a developer. ## Installation Download the app from the official website https://kunkun.sh/download, or GitHub release https://github.com/kunkunsh/kunkun/releases ## Supported Platforms KK is designed to be cross-platform. It not only supports Windows and MacOS, but also Linux. Linux is usually ignored by software companies and app developers as it has a small market share. However, as a developer myself, I admire the open source spirit of Linux and I want to make KK available to Linux users. ## Extensions ### Unleash the Power of KK: Build Custom Tools with Ease KK takes a page out of popular platforms like VSCode, Raycast, and Alfred, offering an extensible framework for you to craft your own tools. Imagine a toolbox that adapts to your needs – that's what KK empowers you with. ### JavaScript for Speedy Development Building extensions in KK is a breeze. Each extension is a lightweight JavaScript applet, making development fast and familiar. Need a simple UI? A pre-built template lets you create it with minimal code. But don't be limited! KK supports complex interfaces built with web technologies like React, Vue, or Svelte, as long as the final output is a static website (SSG/CSR). ### Lightweight KK extensions are designed to be featherweights, typically clocking in at a mere 30-200KB (even with UI libraries). Compare that to traditional desktop apps (often in the megabyte range) or Electron apps (usually exceeding 100MB). For simple tasks like document searching, file/video conversions, a full-blown app is overkill. KK extensions provide a streamlined solution within your existing workflow. ### Write Once, Run Everywhere The magic of web-based extensions? Write your code once and deploy it across platforms. No more compatibility headaches! KK acts as the launchpad for your creations, letting you share them with the community without worrying about native code complexities, distribution hassles, or code signing nightmares. ### Open Source and Community-Driven All community extensions are open source and available on GitHub. You can find the link to them (including the exactly commit and GitHub action workflow used to build the extension) in the extension store. This transparency ensures you know exactly what you're installing. ### Explore the Extension Store KK comes with a dedicated extension store like Apple's app store where you can discover and download tools created by the community. While I personally review submissions before adding them to the repository, the focus remains on openness and public scrutiny. This way, you have complete control over what you install. ### Beyond the Store KK offers ultimate flexibility. You're not restricted to the store. Install extensions directly from sources like Github, npm, or even a simple tarball file. But always ensure the source is trustworthy before installing an extension. ## Permissions KK prioritizes security, but understands the need for powerful tools. Here's how we navigate this balance: ### Permissions for Functionality Some extensions require access to specific resources to function effectively. For instance, a file search extension needs to scan your file system, while a clipboard manager relies on clipboard access. Without these permissions, extensions would have limited capabilities. ### Transparency is Key Similar to Chrome extensions, KK implements a permission system. Developers must explicitly declare required permissions, which you'll see clearly before installing. This allows informed decisions about the data your extensions access. ### Security Measures in Place Access to unauthorized APIs is strictly blocked and logged by KK. This ensures your data stays protected, even if an extension malfunctions. ### Ready to Build? Head over to the [Write Extension](/guides/extensions/write-extension/) or reach out to me on Discord. :::tip See [YouTube Playlist](https://www.youtube.com/playlist?list=PLUxw2JoWliioxX3klgGmmaPGISfQlURuv) for more videos. ::: # Metrics
Total Downloads: GitHub Downloads (all assets, all releases)
@kksh/api Downloads:{" "} NPM Downloads
create-kunkun Downloads:{" "} NPM Downloads
kksh Downloads:{" "} NPM Downloads
[GitHub Repo stars](https://github.com/kunkunsh/kunkun) Star History Chart {/*
# Headless Command ## Kunkun: Headless Command Tutorial
# Template UI Command # Introduction ## Kunkun: Introduction # Publish Extension with JSR
# Publish Extension with NPM