@knosi/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -2
- package/package.json +8 -4
- package/src/commands/auth-login.mjs +107 -0
- package/src/commands/auth-login.test.mjs +17 -0
- package/src/commands/install-skill.mjs +16 -0
- package/src/commands/save-ai-note.mjs +34 -0
- package/src/commands/save-ai-note.test.mjs +7 -0
- package/src/config.mjs +36 -0
- package/src/daemon.mjs +108 -0
- package/src/http.mjs +58 -0
- package/src/index.mjs +29 -105
- package/templates/save-to-knosi/SKILL.md +21 -0
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# @knosi/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Knosi CLI for [Second Brain](https://github.com/zhousiyao03-cyber/second-brain).
|
|
4
4
|
|
|
5
|
-
Runs on your machine
|
|
5
|
+
Runs on your machine and supports three workflows:
|
|
6
|
+
|
|
7
|
+
1. the existing Claude Code daemon
|
|
8
|
+
2. OAuth login against your Knosi deployment
|
|
9
|
+
3. explicit raw AI capture saves for Claude Code skills
|
|
6
10
|
|
|
7
11
|
## Prerequisites
|
|
8
12
|
|
|
@@ -22,6 +26,49 @@ The daemon will:
|
|
|
22
26
|
|
|
23
27
|
Press Ctrl+C to stop.
|
|
24
28
|
|
|
29
|
+
### OAuth Login
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @knosi/cli auth login https://www.knosi.xyz
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This starts a local callback server on `127.0.0.1:6274`, opens the browser, completes OAuth against Knosi, and stores CLI credentials in `~/.knosi/cli.json`.
|
|
36
|
+
|
|
37
|
+
### Save a Raw AI Capture
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cat payload.json | npx @knosi/cli save-ai-note --json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Payload shape:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"sourceApp": "claude-code",
|
|
48
|
+
"title": "Optional custom title",
|
|
49
|
+
"capturedAtLabel": "2026-04-12 15:20 SGT",
|
|
50
|
+
"messages": [
|
|
51
|
+
{ "role": "user", "content": "Question" },
|
|
52
|
+
{ "role": "assistant", "content": "Answer" }
|
|
53
|
+
],
|
|
54
|
+
"sourceMeta": {
|
|
55
|
+
"projectPath": "/Users/bytedance/second-brain"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Install the Claude Code Skill
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx @knosi/cli install-skill
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This copies the bundled template to:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
~/.claude/skills/save-to-knosi/SKILL.md
|
|
70
|
+
```
|
|
71
|
+
|
|
25
72
|
## Options
|
|
26
73
|
|
|
27
74
|
| Flag | Description | Default |
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knosi/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Knosi CLI for Claude Code daemon, OAuth login, raw AI capture saves, and skill install",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"knosi": "./src/index.mjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"src/"
|
|
11
|
+
"src/",
|
|
12
|
+
"templates/"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=20"
|
|
@@ -17,6 +18,9 @@
|
|
|
17
18
|
"second-brain",
|
|
18
19
|
"claude",
|
|
19
20
|
"ai",
|
|
20
|
-
"daemon"
|
|
21
|
+
"daemon",
|
|
22
|
+
"oauth",
|
|
23
|
+
"mcp",
|
|
24
|
+
"knowledge-base"
|
|
21
25
|
]
|
|
22
26
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { getDefaultBaseUrl, saveConfig } from "../config.mjs";
|
|
5
|
+
|
|
6
|
+
export function createPkceVerifier() {
|
|
7
|
+
return crypto.randomBytes(32).toString("base64url");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createPkceChallenge(verifier) {
|
|
11
|
+
return crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildAuthorizationUrl({
|
|
15
|
+
baseUrl,
|
|
16
|
+
codeChallenge,
|
|
17
|
+
redirectUri,
|
|
18
|
+
}) {
|
|
19
|
+
const url = new URL("/oauth/authorize", baseUrl);
|
|
20
|
+
url.searchParams.set("response_type", "code");
|
|
21
|
+
url.searchParams.set("client_id", "knosi-cli");
|
|
22
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
23
|
+
url.searchParams.set("scope", "knowledge:read knowledge:write_inbox");
|
|
24
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
25
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
26
|
+
return url.toString();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function openUrl(url) {
|
|
30
|
+
const command = process.platform === "darwin" ? "open" : "xdg-open";
|
|
31
|
+
try {
|
|
32
|
+
spawn(command, [url], { stdio: "ignore", detached: true }).unref();
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function waitForCallback(port) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const server = http.createServer((req, res) => {
|
|
42
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
43
|
+
const code = url.searchParams.get("code");
|
|
44
|
+
const error = url.searchParams.get("error");
|
|
45
|
+
|
|
46
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
47
|
+
res.end(error ? `Login failed: ${error}\n` : "Knosi CLI login complete. You can return to Claude Code.\n");
|
|
48
|
+
server.close();
|
|
49
|
+
|
|
50
|
+
if (error) {
|
|
51
|
+
reject(new Error(`OAuth login failed: ${error}`));
|
|
52
|
+
} else if (!code) {
|
|
53
|
+
reject(new Error("OAuth callback did not include a code."));
|
|
54
|
+
} else {
|
|
55
|
+
resolve(code);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
server.listen(port, "127.0.0.1");
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function runAuthLogin(args) {
|
|
64
|
+
const baseUrl = args[0] && args[0].startsWith("http") ? args[0] : getDefaultBaseUrl();
|
|
65
|
+
const port = 6274;
|
|
66
|
+
const redirectUri = `http://127.0.0.1:${port}/oauth/callback`;
|
|
67
|
+
const verifier = createPkceVerifier();
|
|
68
|
+
const challenge = createPkceChallenge(verifier);
|
|
69
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
70
|
+
baseUrl,
|
|
71
|
+
codeChallenge: challenge,
|
|
72
|
+
redirectUri,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
console.log(`Opening browser for Knosi login at ${baseUrl} ...`);
|
|
76
|
+
if (!openUrl(authorizationUrl)) {
|
|
77
|
+
console.log("Open this URL manually:");
|
|
78
|
+
console.log(authorizationUrl);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const code = await waitForCallback(port);
|
|
82
|
+
const response = await fetch(`${baseUrl}/api/oauth/token`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
85
|
+
body: new URLSearchParams({
|
|
86
|
+
grant_type: "authorization_code",
|
|
87
|
+
client_id: "knosi-cli",
|
|
88
|
+
code,
|
|
89
|
+
redirect_uri: redirectUri,
|
|
90
|
+
code_verifier: verifier,
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`OAuth token exchange failed: ${await response.text()}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const body = await response.json();
|
|
99
|
+
await saveConfig({
|
|
100
|
+
baseUrl,
|
|
101
|
+
accessToken: body.access_token,
|
|
102
|
+
refreshToken: body.refresh_token,
|
|
103
|
+
clientId: "knosi-cli",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
console.log("✓ Saved Knosi CLI credentials.");
|
|
107
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { buildAuthorizationUrl, createPkceChallenge } from "./auth-login.mjs";
|
|
4
|
+
|
|
5
|
+
test("buildAuthorizationUrl encodes the Knosi CLI OAuth request", () => {
|
|
6
|
+
const url = new URL(
|
|
7
|
+
buildAuthorizationUrl({
|
|
8
|
+
baseUrl: "https://www.knosi.xyz",
|
|
9
|
+
codeChallenge: createPkceChallenge("a".repeat(43)),
|
|
10
|
+
redirectUri: "http://127.0.0.1:6274/oauth/callback",
|
|
11
|
+
})
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
assert.equal(url.pathname, "/oauth/authorize");
|
|
15
|
+
assert.equal(url.searchParams.get("client_id"), "knosi-cli");
|
|
16
|
+
assert.equal(url.searchParams.get("code_challenge_method"), "S256");
|
|
17
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const SKILL_TEMPLATE_PATH = new URL("../../templates/save-to-knosi/SKILL.md", import.meta.url);
|
|
6
|
+
|
|
7
|
+
export async function runInstallSkill() {
|
|
8
|
+
const targetDir = path.join(os.homedir(), ".claude", "skills", "save-to-knosi");
|
|
9
|
+
const targetPath = path.join(targetDir, "SKILL.md");
|
|
10
|
+
const template = await readFile(SKILL_TEMPLATE_PATH, "utf8");
|
|
11
|
+
|
|
12
|
+
await mkdir(targetDir, { recursive: true });
|
|
13
|
+
await writeFile(targetPath, template, "utf8");
|
|
14
|
+
|
|
15
|
+
console.log(`✓ Installed Claude Code skill at ${targetPath}`);
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { authorizedFetch } from "../http.mjs";
|
|
2
|
+
|
|
3
|
+
export async function readStdinJson() {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
for await (const chunk of process.stdin) {
|
|
6
|
+
chunks.push(chunk);
|
|
7
|
+
}
|
|
8
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
9
|
+
if (!raw) {
|
|
10
|
+
throw new Error("Expected JSON on stdin.");
|
|
11
|
+
}
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runSaveAiNote(args) {
|
|
16
|
+
const useJson = args.includes("--json");
|
|
17
|
+
if (!useJson) {
|
|
18
|
+
throw new Error("Use `knosi save-ai-note --json` and pipe the payload on stdin.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const payload = await readStdinJson();
|
|
22
|
+
const response = await authorizedFetch("/api/integrations/ai-captures", {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify(payload),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`save-ai-note failed: ${await response.text()}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const body = await response.json();
|
|
33
|
+
console.log(JSON.stringify(body, null, 2));
|
|
34
|
+
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), ".knosi");
|
|
6
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "cli.json");
|
|
7
|
+
const DEFAULT_BASE_URL = "https://www.knosi.xyz";
|
|
8
|
+
|
|
9
|
+
export function getConfigPath() {
|
|
10
|
+
return CONFIG_PATH;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getDefaultBaseUrl() {
|
|
14
|
+
return DEFAULT_BASE_URL;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadConfig() {
|
|
18
|
+
try {
|
|
19
|
+
const content = await readFile(CONFIG_PATH, "utf8");
|
|
20
|
+
return JSON.parse(content);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function saveConfig(config) {
|
|
27
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
28
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function updateConfig(patch) {
|
|
32
|
+
const current = (await loadConfig()) ?? {};
|
|
33
|
+
const next = { ...current, ...patch };
|
|
34
|
+
await saveConfig(next);
|
|
35
|
+
return next;
|
|
36
|
+
}
|
package/src/daemon.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { configure, claimTask, sendHeartbeat } from "./api.mjs";
|
|
3
|
+
import { setClaudeBin } from "./spawn-claude.mjs";
|
|
4
|
+
import { handleChatTask } from "./handler-chat.mjs";
|
|
5
|
+
import { handleStructuredTask } from "./handler-structured.mjs";
|
|
6
|
+
|
|
7
|
+
function getArg(args, flag) {
|
|
8
|
+
const idx = args.indexOf(flag);
|
|
9
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function checkClaude(claudeBinArg) {
|
|
13
|
+
try {
|
|
14
|
+
const version = execSync(`${claudeBinArg} --version`, { encoding: "utf8" }).trim();
|
|
15
|
+
console.log(`✓ Claude CLI: ${version}`);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
console.error(`✗ Claude CLI not found at "${claudeBinArg}"`);
|
|
19
|
+
console.error(" Install: npm install -g @anthropic-ai/claude-code");
|
|
20
|
+
console.error(" Or specify: --claude-bin /path/to/claude");
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ts() {
|
|
26
|
+
return new Date().toLocaleTimeString("en-GB", { hour12: false });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runDaemon(args) {
|
|
30
|
+
const serverUrl = getArg(args, "--url") || "https://second-brain-self-alpha.vercel.app";
|
|
31
|
+
const isOnce = args.includes("--once");
|
|
32
|
+
const claudeBinArg = getArg(args, "--claude-bin") || "claude";
|
|
33
|
+
|
|
34
|
+
const CHAT_POLL_MS = 2_000;
|
|
35
|
+
const STRUCTURED_POLL_MS = 1_000;
|
|
36
|
+
const HEARTBEAT_MS = 120_000;
|
|
37
|
+
const MAX_CONCURRENT_CHAT = 3;
|
|
38
|
+
const MAX_CONCURRENT_STRUCTURED = 5;
|
|
39
|
+
|
|
40
|
+
configure(serverUrl);
|
|
41
|
+
setClaudeBin(claudeBinArg);
|
|
42
|
+
|
|
43
|
+
if (!checkClaude(claudeBinArg)) process.exit(1);
|
|
44
|
+
|
|
45
|
+
let chatRunning = 0;
|
|
46
|
+
let structuredRunning = 0;
|
|
47
|
+
|
|
48
|
+
async function pollChat() {
|
|
49
|
+
if (chatRunning >= MAX_CONCURRENT_CHAT) return;
|
|
50
|
+
try {
|
|
51
|
+
const task = await claimTask("chat");
|
|
52
|
+
if (!task) return;
|
|
53
|
+
chatRunning++;
|
|
54
|
+
handleChatTask(task)
|
|
55
|
+
.catch(() => {})
|
|
56
|
+
.finally(() => {
|
|
57
|
+
chatRunning--;
|
|
58
|
+
});
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function pollStructured() {
|
|
63
|
+
if (structuredRunning >= MAX_CONCURRENT_STRUCTURED) return;
|
|
64
|
+
try {
|
|
65
|
+
const task = await claimTask("structured");
|
|
66
|
+
if (!task) return;
|
|
67
|
+
structuredRunning++;
|
|
68
|
+
handleStructuredTask(task)
|
|
69
|
+
.catch(() => {})
|
|
70
|
+
.finally(() => {
|
|
71
|
+
structuredRunning--;
|
|
72
|
+
});
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isOnce) {
|
|
77
|
+
console.log("🔍 Single-run mode...");
|
|
78
|
+
await pollChat();
|
|
79
|
+
await pollStructured();
|
|
80
|
+
console.log("Done.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log("");
|
|
85
|
+
console.log("🚀 Knosi AI Daemon");
|
|
86
|
+
console.log(` Server: ${serverUrl}`);
|
|
87
|
+
console.log(
|
|
88
|
+
` Chat poll: ${CHAT_POLL_MS / 1000}s | Structured poll: ${STRUCTURED_POLL_MS / 1000}s`
|
|
89
|
+
);
|
|
90
|
+
console.log(
|
|
91
|
+
` Max concurrent: chat=${MAX_CONCURRENT_CHAT} structured=${MAX_CONCURRENT_STRUCTURED}`
|
|
92
|
+
);
|
|
93
|
+
console.log("");
|
|
94
|
+
console.log(" Waiting for tasks... (Ctrl+C to stop)");
|
|
95
|
+
console.log("");
|
|
96
|
+
|
|
97
|
+
await sendHeartbeat("daemon");
|
|
98
|
+
setInterval(() => sendHeartbeat("daemon"), HEARTBEAT_MS);
|
|
99
|
+
setInterval(pollChat, CHAT_POLL_MS);
|
|
100
|
+
setInterval(pollStructured, STRUCTURED_POLL_MS);
|
|
101
|
+
|
|
102
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
103
|
+
process.on(sig, () => {
|
|
104
|
+
console.log(`\n[${ts()}] daemon stopped`);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/http.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { loadConfig, saveConfig } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
async function refreshAccessToken(config) {
|
|
4
|
+
if (!config?.refreshToken || !config?.baseUrl) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const response = await fetch(`${config.baseUrl}/api/oauth/token`, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
11
|
+
body: new URLSearchParams({
|
|
12
|
+
grant_type: "refresh_token",
|
|
13
|
+
client_id: "knosi-cli",
|
|
14
|
+
refresh_token: config.refreshToken,
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const body = await response.json();
|
|
23
|
+
const nextConfig = {
|
|
24
|
+
...config,
|
|
25
|
+
accessToken: body.access_token,
|
|
26
|
+
};
|
|
27
|
+
await saveConfig(nextConfig);
|
|
28
|
+
return nextConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function authorizedFetch(pathname, init = {}) {
|
|
32
|
+
let config = await loadConfig();
|
|
33
|
+
if (!config?.accessToken || !config?.baseUrl) {
|
|
34
|
+
throw new Error("Not logged in. Run `knosi auth login` first.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const makeRequest = async (token) =>
|
|
38
|
+
fetch(`${config.baseUrl}${pathname}`, {
|
|
39
|
+
...init,
|
|
40
|
+
headers: {
|
|
41
|
+
...(init.headers ?? {}),
|
|
42
|
+
Authorization: `Bearer ${token}`,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let response = await makeRequest(config.accessToken);
|
|
47
|
+
if (response.status !== 401) {
|
|
48
|
+
return response;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
config = await refreshAccessToken(config);
|
|
52
|
+
if (!config?.accessToken) {
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
response = await makeRequest(config.accessToken);
|
|
57
|
+
return response;
|
|
58
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -1,118 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* npx @knosi/cli --url https://your-instance.vercel.app
|
|
7
|
-
* npx @knosi/cli --once
|
|
8
|
-
*/
|
|
9
|
-
import { execSync } from "node:child_process";
|
|
10
|
-
import { configure, claimTask, sendHeartbeat } from "./api.mjs";
|
|
11
|
-
import { setClaudeBin } from "./spawn-claude.mjs";
|
|
12
|
-
import { handleChatTask } from "./handler-chat.mjs";
|
|
13
|
-
import { handleStructuredTask } from "./handler-structured.mjs";
|
|
2
|
+
import { runAuthLogin } from "./commands/auth-login.mjs";
|
|
3
|
+
import { runInstallSkill } from "./commands/install-skill.mjs";
|
|
4
|
+
import { runSaveAiNote } from "./commands/save-ai-note.mjs";
|
|
5
|
+
import { runDaemon } from "./daemon.mjs";
|
|
14
6
|
|
|
15
|
-
// ── Parse args ──────────────────────────────────────────────────────────
|
|
16
7
|
const args = process.argv.slice(2);
|
|
8
|
+
const [command, subcommand, ...rest] = args;
|
|
17
9
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const serverUrl = getArg("--url") || "https://second-brain-self-alpha.vercel.app";
|
|
24
|
-
const isOnce = args.includes("--once");
|
|
25
|
-
const claudeBinArg = getArg("--claude-bin") || "claude";
|
|
26
|
-
|
|
27
|
-
const CHAT_POLL_MS = 2_000;
|
|
28
|
-
const STRUCTURED_POLL_MS = 1_000;
|
|
29
|
-
const HEARTBEAT_MS = 120_000;
|
|
30
|
-
const MAX_CONCURRENT_CHAT = 3;
|
|
31
|
-
const MAX_CONCURRENT_STRUCTURED = 5;
|
|
32
|
-
|
|
33
|
-
// ── Preflight ───────────────────────────────────────────────────────────
|
|
34
|
-
function checkClaude() {
|
|
35
|
-
try {
|
|
36
|
-
const version = execSync(`${claudeBinArg} --version`, { encoding: "utf8" }).trim();
|
|
37
|
-
console.log(`✓ Claude CLI: ${version}`);
|
|
38
|
-
return true;
|
|
39
|
-
} catch {
|
|
40
|
-
console.error(`✗ Claude CLI not found at "${claudeBinArg}"`);
|
|
41
|
-
console.error(" Install: npm install -g @anthropic-ai/claude-code");
|
|
42
|
-
console.error(" Or specify: --claude-bin /path/to/claude");
|
|
43
|
-
return false;
|
|
10
|
+
async function main() {
|
|
11
|
+
if (!command || command.startsWith("--")) {
|
|
12
|
+
await runDaemon(args);
|
|
13
|
+
return;
|
|
44
14
|
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ── Main ────────────────────────────────────────────────────────────────
|
|
48
|
-
configure(serverUrl);
|
|
49
|
-
setClaudeBin(claudeBinArg);
|
|
50
|
-
|
|
51
|
-
if (!checkClaude()) process.exit(1);
|
|
52
|
-
|
|
53
|
-
function ts() {
|
|
54
|
-
return new Date().toLocaleTimeString("en-GB", { hour12: false });
|
|
55
|
-
}
|
|
56
15
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
async function pollChat() {
|
|
61
|
-
if (chatRunning >= MAX_CONCURRENT_CHAT) return;
|
|
62
|
-
try {
|
|
63
|
-
const task = await claimTask("chat");
|
|
64
|
-
if (!task) return;
|
|
65
|
-
chatRunning++;
|
|
66
|
-
handleChatTask(task)
|
|
67
|
-
.catch(() => {})
|
|
68
|
-
.finally(() => { chatRunning--; });
|
|
69
|
-
} catch {
|
|
70
|
-
// server unreachable
|
|
16
|
+
if (command === "daemon") {
|
|
17
|
+
await runDaemon([subcommand, ...rest].filter(Boolean));
|
|
18
|
+
return;
|
|
71
19
|
}
|
|
72
|
-
}
|
|
73
20
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const task = await claimTask("structured");
|
|
78
|
-
if (!task) return;
|
|
79
|
-
structuredRunning++;
|
|
80
|
-
handleStructuredTask(task)
|
|
81
|
-
.catch(() => {})
|
|
82
|
-
.finally(() => { structuredRunning--; });
|
|
83
|
-
} catch {
|
|
84
|
-
// server unreachable
|
|
21
|
+
if (command === "auth" && subcommand === "login") {
|
|
22
|
+
await runAuthLogin(rest);
|
|
23
|
+
return;
|
|
85
24
|
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (isOnce) {
|
|
89
|
-
console.log("🔍 Single-run mode...");
|
|
90
|
-
await pollChat();
|
|
91
|
-
await pollStructured();
|
|
92
|
-
console.log("Done.");
|
|
93
|
-
} else {
|
|
94
|
-
console.log("");
|
|
95
|
-
console.log("🚀 Knosi AI Daemon");
|
|
96
|
-
console.log(` Server: ${serverUrl}`);
|
|
97
|
-
console.log(` Chat poll: ${CHAT_POLL_MS / 1000}s | Structured poll: ${STRUCTURED_POLL_MS / 1000}s`);
|
|
98
|
-
console.log(` Max concurrent: chat=${MAX_CONCURRENT_CHAT} structured=${MAX_CONCURRENT_STRUCTURED}`);
|
|
99
|
-
console.log("");
|
|
100
|
-
console.log(" Waiting for tasks... (Ctrl+C to stop)");
|
|
101
|
-
console.log("");
|
|
102
25
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Poll loops
|
|
108
|
-
setInterval(pollChat, CHAT_POLL_MS);
|
|
109
|
-
setInterval(pollStructured, STRUCTURED_POLL_MS);
|
|
26
|
+
if (command === "save-ai-note") {
|
|
27
|
+
await runSaveAiNote([subcommand, ...rest].filter(Boolean));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
110
30
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
console.log(`\n[${ts()}] daemon stopped`);
|
|
115
|
-
process.exit(0);
|
|
116
|
-
});
|
|
31
|
+
if (command === "install-skill") {
|
|
32
|
+
await runInstallSkill();
|
|
33
|
+
return;
|
|
117
34
|
}
|
|
35
|
+
|
|
36
|
+
throw new Error(`Unknown command: ${args.join(" ")}`);
|
|
118
37
|
}
|
|
38
|
+
|
|
39
|
+
main().catch((error) => {
|
|
40
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: save-to-knosi
|
|
3
|
+
description: Save the current Claude Code exchange into Knosi as one raw AI Inbox note. Use only when the user explicitly asks to save or archive something to Knosi.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
When the user explicitly asks to save the current exchange to Knosi:
|
|
7
|
+
|
|
8
|
+
1. Collect the message excerpt that should be saved.
|
|
9
|
+
2. Build JSON with:
|
|
10
|
+
- `sourceApp`: `claude-code`
|
|
11
|
+
- `title` if the user provided one
|
|
12
|
+
- `messages`: the raw user/assistant turns to preserve
|
|
13
|
+
- `sourceMeta.projectPath` when it is useful
|
|
14
|
+
3. Run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
knosi save-ai-note --json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
4. Pipe the JSON payload to stdin.
|
|
21
|
+
5. After the command succeeds, reply with the created note id or title.
|