Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ kib — The Headless Knowledge Compiler. Bun + TypeScript monorepo.

- `packages/core` → `@kibhq/core` on npm
- `packages/cli` → `@kibhq/cli` on npm
- `packages/dashboard` → `@kibhq/dashboard` on npm (web UI, launched via `kib ui`)
- npm org: `kibhq`
- GitHub: `keeganthomp/kib`

Expand Down Expand Up @@ -50,6 +51,24 @@ bun run packages/cli/bin/kib.ts # run CLI locally
- LLM providers: Anthropic, OpenAI, Ollama (auto-detected from env vars)
- Credentials stored at `~/.config/kib/credentials`, loaded on CLI startup

## Shared Workspaces

Git-based team collaboration. No custom server, no accounts.

```bash
kib share <url> # connect vault to git remote
kib clone <url> # join a shared vault
kib pull # get latest from team
kib push # commit + push local changes
kib share --status # check sync state
```

- Manifest conflicts auto-resolve via 3-way merge (union by key, prefer newer)
- Machine-local state (.kb/cache, .kb/vault.lock, pipeline.db) is gitignored
- Dashboard Team page: sync status, pull/push buttons, contributor list
- MCP tools: `kib_share_status`, `kib_pull`, `kib_push`
- Config: `[sharing]` section in config.toml

## Skill Ecosystem (v0.8.0)

### Built-in Skills (10)
Expand Down
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ INTEGRATION
serve Start MCP server for AI tool integration
mcp Configure MCP in AI clients (auto-runs on init)
watch Passive learning daemon (inbox, folders, clipboard, screenshots)
ui Launch local web dashboard

COLLABORATION
share <url> Share vault with a team via git remote
clone <url> [dir] Clone a shared vault from a git remote
pull Pull latest changes from shared vault
push Push local changes to shared vault

MANAGEMENT
config [key] [val] Get or set configuration
Expand Down Expand Up @@ -277,6 +284,29 @@ glob = "*.{png,jpg,jpeg,webp,gif,bmp,tiff}"

Failed ingestions retry up to 3 times before moving to the failed queue. Logs are written to `.kb/logs/watch.log` with automatic rotation at 10 MB.

### Shared Workspaces

Share a vault with your team using git. No custom server, no accounts — just git.

```bash
# Share your vault (one-time setup)
kib share https://github.com/team/research.git

# Team members join
kib clone https://github.com/team/research.git

# Day-to-day sync
kib pull # get latest from team
kib push # share your changes

# Check sync status
kib share --status
```

Each team member ingests and compiles locally, then pushes to the shared remote. Manifest conflicts are auto-resolved via 3-way merge (union sources/articles by key, prefer newer). Machine-local state (cache, search index, lockfile) is gitignored.

The web dashboard (`kib ui`) includes a Team page with sync status, pull/push buttons, and a contributor list. MCP tools (`kib_pull`, `kib_push`, `kib_share_status`) let AI assistants sync too.

### Export

```bash
Expand Down Expand Up @@ -351,7 +381,7 @@ That's it. Restart your AI client and it can search, query, ingest, and compile

Already have a vault? Run `kib mcp` to configure MCP clients without re-initializing.

**11 tools:** `kib_status`, `kib_list`, `kib_read`, `kib_search`, `kib_query`, `kib_ingest`, `kib_compile`, `kib_lint`, `kib_config`, `kib_skill`, `kib_export`
**14 tools:** `kib_status`, `kib_list`, `kib_read`, `kib_search`, `kib_query`, `kib_ingest`, `kib_compile`, `kib_lint`, `kib_config`, `kib_skill`, `kib_export`, `kib_share_status`, `kib_pull`, `kib_push`

**3 resources:** `wiki://index`, `wiki://graph`, `wiki://log`

Expand Down
20 changes: 12 additions & 8 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,16 +204,19 @@ Most of kib's value is locked behind `kib compile`. That's wrong — value shoul
CLI-only means developer-only. The knowledge is valuable to everyone.
- [ ] VS Code extension: sidebar with search, query, ingest from editor
- [ ] Obsidian plugin: sync kib vault ↔ Obsidian vault, use kib's compile + search
- [ ] Web UI: local dashboard with graph visualization, search, query (not just export)
- [x] Web UI: local dashboard with graph visualization, search, query, ingest, compile (`kib ui`)
- [ ] Raycast/Alfred integration: global hotkey → search your knowledge base
- [ ] Mobile: read-only PWA for querying on the go

### Shared Knowledge Bases
Personal wikis are useful. Team wikis are essential.
- [ ] `kib share` — push vault to a git remote, team members clone + contribute
- [ ] Multi-user ingest: team members ingest from their own browsers, shared compile
- [x] `kib share <url>` — connect vault to git remote, push, team members clone + contribute
- [x] `kib clone <url>` — join a shared vault from a git remote
- [x] `kib pull` / `kib push` — sync changes with team (auto-commit, manifest auto-merge)
- [x] Multi-user ingest: team members ingest from their own machines, push to shared remote
- [x] Team dashboard: contributors, sync status, pull/push from web UI
- [x] MCP tools: `kib_share_status`, `kib_pull`, `kib_push`
- [ ] Access control: public wiki articles vs private notes
- [ ] Team dashboard: who ingested what, what's new this week, knowledge gaps
- [ ] Org-wide knowledge graph: connect team vaults into a federated search

---
Expand All @@ -238,10 +241,11 @@ Personal wikis are useful. Team wikis are essential.
- [ ] Shared vault hosting (GitHub repo as vault backend)

### Web UI
- [ ] `kib serve` — local web server with read-only wiki viewer
- [ ] Search interface in the browser
- [ ] Article graph visualization (force-directed graph)
- [ ] Reading mode with backlink sidebar
- [x] `kib ui` — local web dashboard (Bun server on port 4848, React + D3)
- [x] Search interface in the browser
- [x] Article graph visualization (D3 force-directed graph)
- [x] Reading mode for wiki and raw articles
- [ ] Backlink sidebar in reading mode

### Additional Export Formats
- [ ] PDF export (via Puppeteer or wkhtmltopdf)
Expand Down
67 changes: 67 additions & 0 deletions packages/cli/src/commands/clone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { basename, resolve } from "node:path";
import * as log from "../ui/logger.js";
import { createSpinner } from "../ui/spinner.js";

interface CloneOpts {
json?: boolean;
}

export async function clone(remoteUrl: string, dir: string | undefined, opts: CloneOpts) {
if (!remoteUrl) {
log.error("Usage: kib clone <remote-url> [directory]");
process.exit(1);
}

// Default dir name from URL: git@github.com:user/my-vault.git → my-vault
const defaultDir = basename(remoteUrl, ".git").replace(/[^a-zA-Z0-9_-]/g, "-");
const targetDir = resolve(dir ?? defaultDir);

const spinner = opts.json ? null : createSpinner("Cloning shared vault...");
spinner?.start();

try {
const { cloneVault, loadManifest, loadConfig } = await import("@kibhq/core");
const root = await cloneVault(remoteUrl, targetDir);
const manifest = await loadManifest(root);
const config = await loadConfig(root);

spinner?.stop();

if (opts.json) {
console.log(
JSON.stringify(
{
path: root,
vault: manifest.vault.name,
sources: Object.keys(manifest.sources).length,
articles: Object.keys(manifest.articles).length,
},
null,
2,
),
);
return;
}

const sourceCount = Object.keys(manifest.sources).length;
const articleCount = Object.keys(manifest.articles).length;

log.header("vault cloned");
log.success(`Path: ${root}`);
log.success(`Vault: ${manifest.vault.name}`);
log.success(`Provider: ${config.provider.default} (${config.provider.model})`);
log.keyValue("sources", `${sourceCount}`);
log.keyValue("articles", `${articleCount}`);
log.blank();
log.dim("start working:");
log.dim(` cd ${targetDir}`);
log.dim(" kib ingest <source> — add content");
log.dim(" kib pull — get latest");
log.dim(" kib push — share changes");
log.blank();
} catch (err) {
spinner?.stop();
log.error((err as Error).message);
process.exit(1);
}
}
69 changes: 69 additions & 0 deletions packages/cli/src/commands/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { resolveVaultRoot, VaultNotFoundError } from "@kibhq/core";
import { debug } from "../ui/debug.js";
import * as log from "../ui/logger.js";
import { createSpinner } from "../ui/spinner.js";

interface PullOpts {
json?: boolean;
}

export async function pull(opts: PullOpts) {
let root: string;
try {
root = resolveVaultRoot();
} catch (err) {
if (err instanceof VaultNotFoundError) {
log.error(err.message);
process.exit(1);
}
throw err;
}

debug(`vault root: ${root}`);

const spinner = opts.json ? null : createSpinner("Pulling from remote...");
spinner?.start();

try {
const { pullVault } = await import("@kibhq/core");
const result = await pullVault(root);

spinner?.stop();

if (opts.json) {
console.log(JSON.stringify(result, null, 2));
return;
}

if (!result.updated) {
log.header("pull");
log.dim("Already up to date.");
log.blank();
return;
}

log.header("pull");
log.success(result.summary);

if (result.newSources > 0) {
log.info(`${result.newSources} new source${result.newSources === 1 ? "" : "s"}`);
}
if (result.newArticles > 0) {
log.info(`${result.newArticles} new article${result.newArticles === 1 ? "" : "s"}`);
}
if (result.conflicts.length > 0) {
log.blank();
log.warn("Unresolved conflicts:");
for (const file of result.conflicts) {
log.warn(` ${file}`);
}
log.dim("Resolve these manually, then run kib push.");
}

log.blank();
} catch (err) {
spinner?.stop();
log.error((err as Error).message);
process.exit(1);
}
}
56 changes: 56 additions & 0 deletions packages/cli/src/commands/push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { resolveVaultRoot, VaultNotFoundError } from "@kibhq/core";
import { debug } from "../ui/debug.js";
import * as log from "../ui/logger.js";
import { createSpinner } from "../ui/spinner.js";

interface PushOpts {
message?: string;
json?: boolean;
}

export async function push(opts: PushOpts) {
let root: string;
try {
root = resolveVaultRoot();
} catch (err) {
if (err instanceof VaultNotFoundError) {
log.error(err.message);
process.exit(1);
}
throw err;
}

debug(`vault root: ${root}`);

const spinner = opts.json ? null : createSpinner("Pushing to remote...");
spinner?.start();

try {
const { pushVault } = await import("@kibhq/core");
const result = await pushVault(root, opts.message);

spinner?.stop();

if (opts.json) {
console.log(JSON.stringify(result, null, 2));
return;
}

log.header("push");

if (!result.pushed) {
log.dim("Nothing to push — vault is clean.");
log.blank();
return;
}

log.success(
`Pushed ${result.filesChanged} file${result.filesChanged === 1 ? "" : "s"} (${result.commit})`,
);
log.blank();
} catch (err) {
spinner?.stop();
log.error((err as Error).message);
process.exit(1);
}
}
Loading
Loading