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
22 changes: 22 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,25 @@ kib skill installed # list installed skills
- Dependency resolution with circular dependency detection
- Hooks system: skills auto-run after compile/ingest/lint via `hooks` field or `[skills.hooks]` in config.toml
- Config: `[skills]` section in `config.toml` for hooks and per-skill settings

## Passive Learning Daemon

### Watch Sources
- **Inbox folder** — auto-ingest files dropped into `inbox/`
- **HTTP endpoint** — `POST localhost:4747/ingest` (content or URL-only)
- **Folder watchers** — configurable multi-folder with glob patterns
- **Clipboard watcher** — polls system clipboard, dedup via hash, configurable min length
- **Screenshot watcher** — watches OS screenshot folder via vision pipeline, auto-detects per platform

### Chrome Extension
- **Manual save** — "Save to kib" button with content extraction via Readability
- **Auto-capture** — dwell time tracking, configurable threshold (10–120s), toggle in popup
- **History sync** — periodic browser history scan, configurable interval and lookback, URL dedup

### CLI Flags
```bash
kib watch --clipboard # enable clipboard watching
kib watch --no-clipboard # disable clipboard watching
kib watch --screenshots # enable screenshot watching
kib watch --no-screenshots # disable screenshot watching
```
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ CORE COMMANDS
INTEGRATION
serve Start MCP server for AI tool integration
mcp Configure MCP in AI clients (auto-runs on init)
watch Watch inbox/ and auto-ingest new files
watch Passive learning daemon (inbox, folders, clipboard, screenshots)

MANAGEMENT
config [key] [val] Get or set configuration
Expand Down Expand Up @@ -203,7 +203,7 @@ kib skill run connections

### Watch Daemon (Passive Learning)

Run a background daemon that monitors your inbox, watched folders, and an HTTP endpoint — automatically ingesting new content and compiling it into your wiki.
Run a background daemon that silently absorbs knowledge from multiple sources — inbox, folders, clipboard, screenshots, and the browser. Content is automatically ingested and compiled into your wiki.

```bash
# Start in foreground (logs to terminal)
Expand All @@ -221,16 +221,25 @@ kib watch --stop
# Install as system service (auto-start on login)
kib watch --install # macOS: launchd, Linux: systemd
kib watch --uninstall

# Enable clipboard/screenshot watchers via CLI flags
kib watch --clipboard --screenshots
```

**Three ingestion channels run simultaneously:**
**Five ingestion channels run simultaneously:**

1. **Inbox folder** — drop any file into `inbox/` and it's auto-ingested. Files already in the inbox when the daemon starts are picked up too.
2. **HTTP endpoint** — `POST http://localhost:4747/ingest` accepts JSON `{ content, title?, url? }`. Built for browser extensions.
2. **HTTP endpoint** — `POST http://localhost:4747/ingest` accepts JSON `{ content, title?, url? }`. Supports URL-only requests for web extraction. Built for browser extensions.
3. **Folder watchers** — monitor external directories with glob filtering (e.g., watch `~/Downloads` for `*.pdf`).
4. **Clipboard watcher** — polls the system clipboard and auto-ingests meaningful text (macOS, Linux, Windows). Deduplicates via hash.
5. **Screenshot watcher** — monitors your OS screenshots folder, auto-ingests new images through the vision pipeline. Auto-detects the default screenshot directory per platform.

**Auto-compile** triggers automatically after N new sources (default: 5) or after idle timeout (default: 30 min).

**Chrome extension** adds two more passive learning modes:
- **Auto-capture** — automatically saves pages you spend 30+ seconds reading (configurable dwell time)
- **History sync** — periodically scans browser history and sends recently visited pages to kib

Configure in `.kb/config.toml`:

```toml
Expand All @@ -252,6 +261,18 @@ recursive = false
path = "~/Documents/notes"
glob = "*.{md,txt}"
recursive = true

# Clipboard watching (off by default)
[watch.clipboard]
enabled = true
min_length = 100 # ignore clips shorter than 100 chars
poll_interval_ms = 2_000

# Screenshot watching (off by default)
[watch.screenshots]
enabled = true
path = "~/Pictures/Screenshots" # optional — auto-detected per OS
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.
Expand Down
8 changes: 4 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ The features that take kib from "cool tool" to "can't live without it."

### Passive Learning Daemon
kib should silently learn from everything you read without you thinking about it.
- [ ] Chrome extension: "Send to KB" button + optional auto-capture of pages you spend >30s on
- [ ] `kib watch` as a background daemon (launchd/systemd) — not just inbox, but browser history, clipboard, screenshots
- [ ] OS-level integration: watch a folder of PDFs, auto-ingest Kindle highlights, Readwise sync
- [ ] Zero-friction ingest: no commands, no thinking, it just absorbs
- [x] Chrome extension: "Send to KB" button + optional auto-capture of pages you spend >30s on
- [x] `kib watch` as a background daemon (launchd/systemd) — not just inbox, but browser history, clipboard, screenshots
- [ ] OS-level integration: auto-ingest Kindle highlights, Readwise sync
- [x] Zero-friction ingest: no commands, no thinking, it just absorbs

### Instant Value Without Compile
Most of kib's value is locked behind `kib compile`. That's wrong — value should be immediate on ingest.
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,12 @@ kib watch --uninstall # remove system service

**Ingestion channels:**
- **Inbox** — drop files into `inbox/` (picks up files added while daemon was off)
- **HTTP** — `POST localhost:4747/ingest` with `{ content, title?, url? }`
- **HTTP** — `POST localhost:4747/ingest` with `{ content, title?, url? }` (supports URL-only for web extraction)
- **Folder watchers** — monitor external directories with glob patterns
- **Clipboard** — polls system clipboard, auto-ingests meaningful text (`--clipboard`)
- **Screenshots** — watches your OS screenshots folder, ingests via vision model (`--screenshots`)

The **Chrome extension** adds auto-capture (saves pages after configurable dwell time) and browser history sync.

**Auto-compile** triggers after a configurable number of new sources or idle timeout. Configure via `[watch]` section in `.kb/config.toml`.

Expand Down
106 changes: 97 additions & 9 deletions packages/cli/src/commands/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ interface WatchOptions {
status?: boolean;
install?: boolean;
uninstall?: boolean;
clipboard?: boolean;
screenshots?: boolean;
}

export async function watch(opts: WatchOptions = {}) {
Expand Down Expand Up @@ -112,7 +114,10 @@ export async function watch(opts: WatchOptions = {}) {
const config = await loadConfig(root);
await writePid(root);

const cleanup = await startWatch(root, config);
const cleanup = await startWatch(root, config, {
clipboard: opts.clipboard,
screenshots: opts.screenshots,
});

const shutdown = async () => {
cleanup();
Expand All @@ -128,17 +133,28 @@ export async function watch(opts: WatchOptions = {}) {
process.on("SIGTERM", shutdown);
}

interface WatchOverrides {
clipboard?: boolean;
screenshots?: boolean;
}

/**
* Core watch loop. Sets up:
* 1. Inbox file watcher
* 2. HTTP server for browser extension
* 3. Multi-folder watchers (from config)
* 4. Ingest queue consumer
* 5. Auto-compile scheduler
* 4. Clipboard watcher (if enabled)
* 5. Screenshot watcher (if enabled)
* 6. Ingest queue consumer
* 7. Auto-compile scheduler
*
* Returns a cleanup function.
*/
async function startWatch(root: string, config: VaultConfig): Promise<() => void> {
async function startWatch(
root: string,
config: VaultConfig,
overrides: WatchOverrides = {},
): Promise<() => void> {
const {
ingestSource,
enqueue,
Expand All @@ -150,6 +166,8 @@ async function startWatch(root: string, config: VaultConfig): Promise<() => void
appendWatchLog,
CompileScheduler,
startFolderWatchers,
startClipboardWatcher,
startScreenshotWatcher,
compileVault,
createProvider,
isLocked,
Expand Down Expand Up @@ -295,6 +313,48 @@ async function startWatch(root: string, config: VaultConfig): Promise<() => void
emit("info", `Watching ${config.watch.folders.length} additional folder(s).`);
}

// ── Clipboard watcher ───────────────────────────────────────

const clipboardEnabled = overrides.clipboard ?? config.watch.clipboard.enabled;
let clipboardCleanup: { stop: () => void } | null = null;
if (clipboardEnabled) {
const slug = () => `clipboard-${Date.now()}`;
clipboardCleanup = startClipboardWatcher({
minLength: config.watch.clipboard.min_length,
pollIntervalMs: config.watch.clipboard.poll_interval_ms,
onText: async (text) => {
const tmpPath = join(root, "inbox", `${slug()}.md`);
await Bun.write(tmpPath, text);
emit("info", `Clipboard captured (${text.length} chars)`);
await enqueue(root, tmpPath, "clipboard");
await consumeQueue();
},
});
emit("info", `Clipboard watcher active (min ${config.watch.clipboard.min_length} chars).`);
}

// ── Screenshot watcher ──────────────────────────────────────

const screenshotsEnabled = overrides.screenshots ?? config.watch.screenshots.enabled;
let screenshotCleanup: { stop: () => void } | null = null;
if (screenshotsEnabled) {
const result = await startScreenshotWatcher({
path: config.watch.screenshots.path,
glob: config.watch.screenshots.glob,
onFile: async (filePath) => {
emit("info", `Screenshot detected: ${filePath}`);
await enqueue(root, filePath, "screenshot");
await consumeQueue();
},
});
if (result) {
screenshotCleanup = result;
emit("info", `Screenshot watcher active: ${result.dir}`);
} else {
emit("warn", "Screenshot watcher: could not detect screenshot directory.");
}
}

// ── Process any items already in the queue ───────────────────

const initialDepth = await queueDepth(root);
Expand All @@ -313,6 +373,14 @@ async function startWatch(root: string, config: VaultConfig): Promise<() => void
log.dim(` + ${f.path} (${f.glob}${f.recursive ? ", recursive" : ""})`);
}
}
if (clipboardEnabled) {
log.dim(
` + clipboard (min ${config.watch.clipboard.min_length} chars, poll ${config.watch.clipboard.poll_interval_ms}ms)`,
);
}
if (screenshotCleanup) {
log.dim(` + screenshots (${config.watch.screenshots.path ?? "auto-detected"})`);
}
log.dim(
`Auto-compile: after ${config.watch.auto_compile_threshold} sources or ${Math.round(config.watch.auto_compile_delay_ms / 60000)} min idle.`,
);
Expand All @@ -328,6 +396,8 @@ async function startWatch(root: string, config: VaultConfig): Promise<() => void
inboxWatcher.close();
server?.stop();
folderCleanup?.stop();
clipboardCleanup?.stop();
screenshotCleanup?.stop();
scheduler.stop();
clearInterval(queuePollInterval);
emit("info", "Daemon stopped.");
Expand All @@ -347,15 +417,33 @@ function startHttpServer(

if (req.method === "POST" && url.pathname === "/ingest") {
try {
const body = (await req.json()) as { content?: string; url?: string; title?: string };

if (!body.content || typeof body.content !== "string") {
return new Response(JSON.stringify({ error: "Missing required field: content" }), {
status: 400,
const body = (await req.json()) as {
content?: string;
url?: string;
title?: string;
source?: string;
};

// URL-only mode: enqueue the URL directly for web extraction
if (!body.content && body.url && typeof body.url === "string") {
const source = (body.source as "history" | "http") || "http";
await enqueue(root, body.url, source, { title: body.title });
await consumeQueue();
return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
}

if (!body.content || typeof body.content !== "string") {
return new Response(
JSON.stringify({ error: "Missing required field: content or url" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}

const slug = (body.title ?? "untitled")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,16 @@ program

program
.command("watch")
.description("Watch inbox/ and auto-ingest (passive learning daemon)")
.description("Passive learning daemon — inbox, folders, clipboard, screenshots, auto-compile")
.option("--daemon", "run in background as a daemon")
.option("--stop", "stop the running daemon")
.option("--status", "check if the daemon is running")
.option("--install", "install as a system service (launchd/systemd)")
.option("--uninstall", "remove the system service")
.option("--clipboard", "enable clipboard watching")
.option("--no-clipboard", "disable clipboard watching")
.option("--screenshots", "enable screenshot watching")
.option("--no-screenshots", "disable screenshot watching")
.action(async (opts) => {
const { watch } = await import("./commands/watch.js");
await watch(opts);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ npm i @kibhq/core
| **Query** | RAG engine — retrieves relevant articles and generates cited answers |
| **Lint** | 5 health-check rules (orphan articles, broken links, stale sources, etc.) |
| **Skills** | Skill loader and runner for extensible vault operations |
| **Daemon** | Watch daemon primitives — FIFO queue, folder watchers, auto-compile scheduler, PID management, log rotation, system service installer (launchd/systemd) |
| **Daemon** | Watch daemon primitives — FIFO queue, folder watchers, clipboard watcher, screenshot watcher, auto-compile scheduler, PID management, log rotation, system service installer (launchd/systemd) |
| **Providers** | LLM adapters for Anthropic Claude, OpenAI, and Ollama |

## Usage
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export const DEFAULTS = {
maxSourceTokens: 32_000, // auto-summarize sources larger than this
maxParallel: 3, // max concurrent source compilations
tokensPerChar: 0.25, // rough estimate: ~4 chars per token
clipboardPollIntervalMs: 2000,
clipboardMinLength: 100, // ignore clipboard text shorter than this
screenshotGlob: "*.{png,jpg,jpeg,webp,gif,bmp,tiff}",
} as const;

/** Manifest version */
Expand Down
Loading
Loading