zubo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +35 -0
- package/README.md +149 -0
- package/bun.lock +216 -0
- package/desktop/README.md +57 -0
- package/desktop/package.json +12 -0
- package/desktop/src-tauri/Cargo.toml +25 -0
- package/desktop/src-tauri/build.rs +3 -0
- package/desktop/src-tauri/icons/README.md +17 -0
- package/desktop/src-tauri/icons/icon.png +0 -0
- package/desktop/src-tauri/src/main.rs +189 -0
- package/desktop/src-tauri/tauri.conf.json +68 -0
- package/docs/ROADMAP.md +490 -0
- package/migrations/001_init.sql +9 -0
- package/migrations/002_memory.sql +33 -0
- package/migrations/003_cron.sql +24 -0
- package/migrations/004_usage.sql +12 -0
- package/migrations/005_secrets.sql +8 -0
- package/migrations/006_agents.sql +1 -0
- package/migrations/007_workflows.sql +22 -0
- package/migrations/008_proactive.sql +24 -0
- package/migrations/009_uploads.sql +9 -0
- package/migrations/010_observability.sql +22 -0
- package/migrations/011_api_keys.sql +7 -0
- package/migrations/012_indexes.sql +5 -0
- package/migrations/013_budget.sql +11 -0
- package/migrations/014_usage_session_idx.sql +2 -0
- package/package.json +39 -0
- package/site/404.html +156 -0
- package/site/CNAME +1 -0
- package/site/docs/agents.html +294 -0
- package/site/docs/api.html +446 -0
- package/site/docs/channels.html +345 -0
- package/site/docs/cli.html +238 -0
- package/site/docs/config.html +1034 -0
- package/site/docs/index.html +433 -0
- package/site/docs/integrations.html +381 -0
- package/site/docs/memory.html +254 -0
- package/site/docs/security.html +375 -0
- package/site/docs/skills.html +322 -0
- package/site/docs.css +412 -0
- package/site/index.html +638 -0
- package/site/install.sh +98 -0
- package/site/logo.svg +1 -0
- package/site/og-image.png +0 -0
- package/site/robots.txt +4 -0
- package/site/script.js +361 -0
- package/site/sitemap.xml +63 -0
- package/site/skills.html +532 -0
- package/site/style.css +1686 -0
- package/src/agent/agents.ts +159 -0
- package/src/agent/compaction.ts +53 -0
- package/src/agent/context.ts +18 -0
- package/src/agent/delegate.ts +118 -0
- package/src/agent/loop.ts +318 -0
- package/src/agent/prompts.ts +111 -0
- package/src/agent/session.ts +87 -0
- package/src/agent/teams.ts +116 -0
- package/src/agent/workflow-executor.ts +192 -0
- package/src/agent/workflow.ts +175 -0
- package/src/channels/adapter.ts +21 -0
- package/src/channels/dashboard.html.ts +2969 -0
- package/src/channels/discord.ts +137 -0
- package/src/channels/optional-deps.d.ts +17 -0
- package/src/channels/router.ts +199 -0
- package/src/channels/signal.ts +133 -0
- package/src/channels/slack.ts +101 -0
- package/src/channels/telegram.ts +102 -0
- package/src/channels/utils.ts +18 -0
- package/src/channels/webchat.ts +1797 -0
- package/src/channels/whatsapp.ts +119 -0
- package/src/config/loader.ts +22 -0
- package/src/config/paths.ts +43 -0
- package/src/config/schema.ts +121 -0
- package/src/db/connection.ts +20 -0
- package/src/db/export.ts +148 -0
- package/src/db/migrations.ts +42 -0
- package/src/index.ts +261 -0
- package/src/llm/claude.ts +193 -0
- package/src/llm/factory.ts +115 -0
- package/src/llm/failover.ts +101 -0
- package/src/llm/openai-compat.ts +409 -0
- package/src/llm/provider.ts +83 -0
- package/src/llm/smart-router.ts +241 -0
- package/src/logs.ts +53 -0
- package/src/memory/chunker.ts +58 -0
- package/src/memory/document-parser.ts +115 -0
- package/src/memory/embedder.ts +235 -0
- package/src/memory/engine.ts +170 -0
- package/src/memory/fts-index.ts +55 -0
- package/src/memory/hybrid-search.ts +72 -0
- package/src/memory/store.ts +56 -0
- package/src/memory/vector-index.ts +72 -0
- package/src/model.ts +118 -0
- package/src/registry/cli.ts +43 -0
- package/src/registry/client.ts +54 -0
- package/src/registry/installer.ts +67 -0
- package/src/scheduler/briefing.ts +71 -0
- package/src/scheduler/cron.ts +258 -0
- package/src/scheduler/heartbeat.ts +58 -0
- package/src/scheduler/memory-triggers.ts +100 -0
- package/src/scheduler/natural-cron.ts +163 -0
- package/src/scheduler/proactive.ts +25 -0
- package/src/scheduler/recipes.ts +110 -0
- package/src/secrets/store.ts +64 -0
- package/src/setup.ts +413 -0
- package/src/skills.ts +293 -0
- package/src/start.ts +373 -0
- package/src/status.ts +165 -0
- package/src/tools/builtin/connect-service.ts +205 -0
- package/src/tools/builtin/cron.ts +126 -0
- package/src/tools/builtin/datetime.ts +36 -0
- package/src/tools/builtin/delegate-task.ts +81 -0
- package/src/tools/builtin/delegate.ts +42 -0
- package/src/tools/builtin/diagnose.ts +41 -0
- package/src/tools/builtin/google-oauth.ts +379 -0
- package/src/tools/builtin/manage-agents.ts +149 -0
- package/src/tools/builtin/manage-skills.ts +294 -0
- package/src/tools/builtin/manage-teams.ts +89 -0
- package/src/tools/builtin/manage-triggers.ts +94 -0
- package/src/tools/builtin/manage-workflows.ts +119 -0
- package/src/tools/builtin/memory-search.ts +38 -0
- package/src/tools/builtin/memory-write.ts +30 -0
- package/src/tools/builtin/run-workflow.ts +36 -0
- package/src/tools/builtin/secrets.ts +122 -0
- package/src/tools/builtin/skill-registry.ts +75 -0
- package/src/tools/builtin-integrations/api-helpers.ts +26 -0
- package/src/tools/builtin-integrations/github/github_issues/SKILL.md +56 -0
- package/src/tools/builtin-integrations/github/github_issues/handler.ts +108 -0
- package/src/tools/builtin-integrations/github/github_prs/SKILL.md +57 -0
- package/src/tools/builtin-integrations/github/github_prs/handler.ts +113 -0
- package/src/tools/builtin-integrations/github/github_repos/SKILL.md +37 -0
- package/src/tools/builtin-integrations/github/github_repos/handler.ts +88 -0
- package/src/tools/builtin-integrations/google/gmail/SKILL.md +51 -0
- package/src/tools/builtin-integrations/google/gmail/handler.ts +125 -0
- package/src/tools/builtin-integrations/google/google_calendar/SKILL.md +35 -0
- package/src/tools/builtin-integrations/google/google_calendar/handler.ts +105 -0
- package/src/tools/builtin-integrations/google/google_docs/SKILL.md +35 -0
- package/src/tools/builtin-integrations/google/google_docs/handler.ts +108 -0
- package/src/tools/builtin-integrations/google/google_drive/SKILL.md +39 -0
- package/src/tools/builtin-integrations/google/google_drive/handler.ts +106 -0
- package/src/tools/builtin-integrations/google/google_sheets/SKILL.md +36 -0
- package/src/tools/builtin-integrations/google/google_sheets/handler.ts +116 -0
- package/src/tools/builtin-integrations/jira/jira_boards/SKILL.md +21 -0
- package/src/tools/builtin-integrations/jira/jira_boards/handler.ts +74 -0
- package/src/tools/builtin-integrations/jira/jira_issues/SKILL.md +28 -0
- package/src/tools/builtin-integrations/jira/jira_issues/handler.ts +140 -0
- package/src/tools/builtin-integrations/linear/linear_issues/SKILL.md +30 -0
- package/src/tools/builtin-integrations/linear/linear_issues/handler.ts +75 -0
- package/src/tools/builtin-integrations/linear/linear_projects/SKILL.md +21 -0
- package/src/tools/builtin-integrations/linear/linear_projects/handler.ts +43 -0
- package/src/tools/builtin-integrations/notion/notion_databases/SKILL.md +39 -0
- package/src/tools/builtin-integrations/notion/notion_databases/handler.ts +83 -0
- package/src/tools/builtin-integrations/notion/notion_pages/SKILL.md +43 -0
- package/src/tools/builtin-integrations/notion/notion_pages/handler.ts +130 -0
- package/src/tools/builtin-integrations/notion/notion_search/SKILL.md +27 -0
- package/src/tools/builtin-integrations/notion/notion_search/handler.ts +69 -0
- package/src/tools/builtin-integrations/slack/slack_messages/SKILL.md +42 -0
- package/src/tools/builtin-integrations/slack/slack_messages/handler.ts +72 -0
- package/src/tools/builtin-integrations/twitter/twitter_posts/SKILL.md +24 -0
- package/src/tools/builtin-integrations/twitter/twitter_posts/handler.ts +133 -0
- package/src/tools/builtin-skills/file-read/SKILL.md +26 -0
- package/src/tools/builtin-skills/file-read/handler.ts +66 -0
- package/src/tools/builtin-skills/file-write/SKILL.md +30 -0
- package/src/tools/builtin-skills/file-write/handler.ts +64 -0
- package/src/tools/builtin-skills/http-request/SKILL.md +34 -0
- package/src/tools/builtin-skills/http-request/handler.ts +87 -0
- package/src/tools/builtin-skills/shell/SKILL.md +26 -0
- package/src/tools/builtin-skills/shell/handler.ts +96 -0
- package/src/tools/builtin-skills/url-fetch/SKILL.md +26 -0
- package/src/tools/builtin-skills/url-fetch/handler.ts +37 -0
- package/src/tools/builtin-skills/web-search/SKILL.md +26 -0
- package/src/tools/builtin-skills/web-search/handler.ts +50 -0
- package/src/tools/executor.ts +205 -0
- package/src/tools/integration-installer.ts +106 -0
- package/src/tools/permissions.ts +45 -0
- package/src/tools/registry.ts +39 -0
- package/src/tools/sandbox-runner.ts +56 -0
- package/src/tools/sandbox.ts +82 -0
- package/src/tools/skill-installer.ts +52 -0
- package/src/tools/skill-loader.ts +259 -0
- package/src/types/optional-deps.d.ts +23 -0
- package/src/util/auth.ts +121 -0
- package/src/util/costs.ts +59 -0
- package/src/util/error-buffer.ts +32 -0
- package/src/util/google-tokens.ts +180 -0
- package/src/util/logger.ts +73 -0
- package/src/util/perf-collector.ts +35 -0
- package/src/util/rate-limiter.ts +70 -0
- package/src/util/tokens.ts +17 -0
- package/src/voice/stt.ts +57 -0
- package/src/voice/tts.ts +103 -0
- package/tests/agent/session.test.ts +109 -0
- package/tests/agent-loop.test.ts +54 -0
- package/tests/auth.test.ts +89 -0
- package/tests/channels.test.ts +67 -0
- package/tests/compaction.test.ts +44 -0
- package/tests/config.test.ts +51 -0
- package/tests/costs.test.ts +19 -0
- package/tests/cron.test.ts +55 -0
- package/tests/db/export.test.ts +219 -0
- package/tests/executor.test.ts +144 -0
- package/tests/export.test.ts +137 -0
- package/tests/helpers/mock-llm.ts +34 -0
- package/tests/helpers/test-db.ts +74 -0
- package/tests/integration/chat-flow.test.ts +48 -0
- package/tests/integrations.test.ts +97 -0
- package/tests/memory/engine.test.ts +114 -0
- package/tests/memory-engine.test.ts +57 -0
- package/tests/permissions.test.ts +21 -0
- package/tests/rate-limiter.test.ts +70 -0
- package/tests/registry.test.ts +67 -0
- package/tests/router.test.ts +36 -0
- package/tests/session.test.ts +58 -0
- package/tests/skill-loader.test.ts +44 -0
- package/tests/tokens.test.ts +30 -0
- package/tests/tools/executor.test.ts +130 -0
- package/tests/util/auth.test.ts +75 -0
- package/tests/util/rate-limiter.test.ts +73 -0
- package/tests/voice.test.ts +60 -0
- package/tests/webchat.test.ts +88 -0
- package/tests/workflow.test.ts +38 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { readAllMemoryFiles, writeMemory } from "./store";
|
|
3
|
+
import { chunkText, type Chunk } from "./chunker";
|
|
4
|
+
import { initEmbedder, embed, isEmbedderReady } from "./embedder";
|
|
5
|
+
import { storeEmbedding } from "./vector-index";
|
|
6
|
+
import { hybridSearch, type SearchResult } from "./hybrid-search";
|
|
7
|
+
import { ftsSearch } from "./fts-index";
|
|
8
|
+
import { logger } from "../util/logger";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MAX_CHUNKS = 10_000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initialize the memory engine: load embedder, index existing files.
|
|
14
|
+
*/
|
|
15
|
+
export async function initMemory(db: Database): Promise<void> {
|
|
16
|
+
// Try to initialize the embedder (non-blocking if it fails)
|
|
17
|
+
const embedderOk = await initEmbedder();
|
|
18
|
+
if (!embedderOk) {
|
|
19
|
+
logger.warn("Running memory with FTS-only (no vector embeddings)");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Index existing memory files
|
|
23
|
+
await indexAllFiles(db);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Index all memory files: chunk → store in DB → embed.
|
|
28
|
+
*/
|
|
29
|
+
export async function indexAllFiles(db: Database): Promise<number> {
|
|
30
|
+
const files = readAllMemoryFiles();
|
|
31
|
+
let totalChunks = 0;
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const chunks = chunkText(file.content, file.filePath);
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
const existing = db
|
|
37
|
+
.query(
|
|
38
|
+
"SELECT id FROM memory_chunks WHERE source_file = ? AND chunk_index = ?"
|
|
39
|
+
)
|
|
40
|
+
.get(chunk.sourceFile, chunk.index) as { id: number } | null;
|
|
41
|
+
|
|
42
|
+
let chunkId: number;
|
|
43
|
+
if (existing) {
|
|
44
|
+
// Update existing chunk
|
|
45
|
+
db.prepare(
|
|
46
|
+
"UPDATE memory_chunks SET content = ? WHERE id = ?"
|
|
47
|
+
).run(chunk.content, existing.id);
|
|
48
|
+
chunkId = existing.id;
|
|
49
|
+
} else {
|
|
50
|
+
// Insert new chunk
|
|
51
|
+
const result = db
|
|
52
|
+
.prepare(
|
|
53
|
+
"INSERT INTO memory_chunks (source_file, chunk_index, content) VALUES (?, ?, ?)"
|
|
54
|
+
)
|
|
55
|
+
.run(chunk.sourceFile, chunk.index, chunk.content);
|
|
56
|
+
chunkId = Number(result.lastInsertRowid);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Generate and store embedding
|
|
60
|
+
if (isEmbedderReady()) {
|
|
61
|
+
const embedding = await embed(chunk.content);
|
|
62
|
+
if (embedding) {
|
|
63
|
+
await storeEmbedding(db, chunkId, embedding);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
totalChunks++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
logger.info("Memory indexed", { files: files.length, chunks: totalChunks });
|
|
72
|
+
return totalChunks;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Write a memory and index it immediately.
|
|
77
|
+
*/
|
|
78
|
+
export async function writeAndIndexMemory(
|
|
79
|
+
db: Database,
|
|
80
|
+
content: string
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const filePath = writeMemory(content);
|
|
83
|
+
|
|
84
|
+
// Atomically get next index and insert in a transaction
|
|
85
|
+
const chunkId = db.transaction(() => {
|
|
86
|
+
const existing = db
|
|
87
|
+
.query("SELECT MAX(chunk_index) as maxIdx FROM memory_chunks WHERE source_file = ?")
|
|
88
|
+
.get(filePath) as { maxIdx: number | null } | null;
|
|
89
|
+
|
|
90
|
+
const nextIdx = (existing?.maxIdx ?? -1) + 1;
|
|
91
|
+
|
|
92
|
+
const result = db
|
|
93
|
+
.prepare(
|
|
94
|
+
"INSERT INTO memory_chunks (source_file, chunk_index, content) VALUES (?, ?, ?)"
|
|
95
|
+
)
|
|
96
|
+
.run(filePath, nextIdx, content);
|
|
97
|
+
|
|
98
|
+
return Number(result.lastInsertRowid);
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
// Embed outside transaction (async not allowed inside)
|
|
102
|
+
if (isEmbedderReady()) {
|
|
103
|
+
const embedding = await embed(content);
|
|
104
|
+
if (embedding) {
|
|
105
|
+
await storeEmbedding(db, chunkId, embedding);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
logger.info("Memory written and indexed", { filePath, content: content.slice(0, 50) });
|
|
110
|
+
|
|
111
|
+
// Prune if over limit
|
|
112
|
+
pruneOldChunks(db);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Remove oldest memory chunks when the total exceeds the max limit.
|
|
117
|
+
*/
|
|
118
|
+
export function pruneOldChunks(db: Database, maxChunks: number = DEFAULT_MAX_CHUNKS): number {
|
|
119
|
+
try {
|
|
120
|
+
const row = db.query("SELECT COUNT(*) as c FROM memory_chunks").get() as { c: number } | null;
|
|
121
|
+
const count = row?.c ?? 0;
|
|
122
|
+
if (count <= maxChunks) return 0;
|
|
123
|
+
|
|
124
|
+
const excess = count - maxChunks;
|
|
125
|
+
db.prepare(
|
|
126
|
+
"DELETE FROM memory_chunks WHERE id IN (SELECT id FROM memory_chunks ORDER BY id ASC LIMIT ?)"
|
|
127
|
+
).run(excess);
|
|
128
|
+
logger.info(`Pruned ${excess} old memory chunks (was ${count}, max ${maxChunks})`);
|
|
129
|
+
return excess;
|
|
130
|
+
} catch (err: any) {
|
|
131
|
+
logger.warn("Memory pruning failed", { error: err.message });
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get memory statistics.
|
|
138
|
+
*/
|
|
139
|
+
export function getMemoryStats(db: Database): { totalChunks: number; oldestDate: string | null } {
|
|
140
|
+
try {
|
|
141
|
+
const row = db.query(
|
|
142
|
+
"SELECT COUNT(*) as c, MIN(rowid) as oldest FROM memory_chunks"
|
|
143
|
+
).get() as { c: number; oldest: number | null } | null;
|
|
144
|
+
return { totalChunks: row?.c ?? 0, oldestDate: null };
|
|
145
|
+
} catch {
|
|
146
|
+
return { totalChunks: 0, oldestDate: null };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Search memory using FTS (synchronous, for use in router).
|
|
152
|
+
*/
|
|
153
|
+
export function searchMemory(
|
|
154
|
+
db: Database,
|
|
155
|
+
query: string,
|
|
156
|
+
limit: number = 5
|
|
157
|
+
): SearchResult[] {
|
|
158
|
+
return ftsSearch(db, query, limit);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Async hybrid search (vector + FTS).
|
|
163
|
+
*/
|
|
164
|
+
export async function searchMemoryAsync(
|
|
165
|
+
db: Database,
|
|
166
|
+
query: string,
|
|
167
|
+
limit: number = 5
|
|
168
|
+
): Promise<SearchResult[]> {
|
|
169
|
+
return hybridSearch(db, query, limit);
|
|
170
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
export interface FtsResult {
|
|
4
|
+
id: number;
|
|
5
|
+
content: string;
|
|
6
|
+
sourceFile: string;
|
|
7
|
+
score: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Full-text search using SQLite FTS5 with BM25 ranking.
|
|
12
|
+
*/
|
|
13
|
+
export function ftsSearch(
|
|
14
|
+
db: Database,
|
|
15
|
+
query: string,
|
|
16
|
+
limit: number = 5
|
|
17
|
+
): FtsResult[] {
|
|
18
|
+
// Escape FTS5 special characters
|
|
19
|
+
const escaped = query.replace(/['"*()]/g, "");
|
|
20
|
+
if (!escaped.trim()) return [];
|
|
21
|
+
|
|
22
|
+
// Split into terms and join with OR for broader matching
|
|
23
|
+
const terms = escaped.split(/\s+/).filter(Boolean);
|
|
24
|
+
const ftsQuery = terms.join(" OR ");
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const rows = db
|
|
28
|
+
.query(
|
|
29
|
+
`SELECT mc.id, mc.content, mc.source_file,
|
|
30
|
+
rank as score
|
|
31
|
+
FROM memory_fts
|
|
32
|
+
JOIN memory_chunks mc ON mc.id = memory_fts.rowid
|
|
33
|
+
WHERE memory_fts MATCH ?
|
|
34
|
+
ORDER BY rank
|
|
35
|
+
LIMIT ?`
|
|
36
|
+
)
|
|
37
|
+
.all(ftsQuery, limit) as Array<{
|
|
38
|
+
id: number;
|
|
39
|
+
content: string;
|
|
40
|
+
source_file: string;
|
|
41
|
+
score: number;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
// Normalize FTS scores (BM25 returns negative values, lower = better)
|
|
45
|
+
const maxScore = rows.length > 0 ? Math.abs(rows[0].score) : 1;
|
|
46
|
+
return rows.map((row) => ({
|
|
47
|
+
id: row.id,
|
|
48
|
+
content: row.content,
|
|
49
|
+
sourceFile: row.source_file,
|
|
50
|
+
score: maxScore > 0 ? Math.abs(row.score) / maxScore : 0,
|
|
51
|
+
}));
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { vectorSearch, type VectorResult } from "./vector-index";
|
|
3
|
+
import { ftsSearch, type FtsResult } from "./fts-index";
|
|
4
|
+
import { isEmbedderReady } from "./embedder";
|
|
5
|
+
|
|
6
|
+
const VECTOR_WEIGHT = 0.6;
|
|
7
|
+
const FTS_WEIGHT = 0.4;
|
|
8
|
+
|
|
9
|
+
export interface SearchResult {
|
|
10
|
+
id: number;
|
|
11
|
+
content: string;
|
|
12
|
+
sourceFile: string;
|
|
13
|
+
score: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hybrid search: weighted union of vector similarity and BM25 keyword search.
|
|
18
|
+
* Falls back to FTS-only when embedder is not available.
|
|
19
|
+
*/
|
|
20
|
+
export async function hybridSearch(
|
|
21
|
+
db: Database,
|
|
22
|
+
query: string,
|
|
23
|
+
limit: number = 5
|
|
24
|
+
): Promise<SearchResult[]> {
|
|
25
|
+
const scoreMap = new Map<
|
|
26
|
+
number,
|
|
27
|
+
{ content: string; sourceFile: string; score: number }
|
|
28
|
+
>();
|
|
29
|
+
|
|
30
|
+
// FTS search (always available)
|
|
31
|
+
const ftsResults = ftsSearch(db, query, limit * 2);
|
|
32
|
+
for (const r of ftsResults) {
|
|
33
|
+
const existing = scoreMap.get(r.id);
|
|
34
|
+
const ftsWeight = isEmbedderReady() ? FTS_WEIGHT : 1.0;
|
|
35
|
+
const score = r.score * ftsWeight;
|
|
36
|
+
if (existing) {
|
|
37
|
+
existing.score += score;
|
|
38
|
+
} else {
|
|
39
|
+
scoreMap.set(r.id, {
|
|
40
|
+
content: r.content,
|
|
41
|
+
sourceFile: r.sourceFile,
|
|
42
|
+
score,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Vector search (when embedder is ready)
|
|
48
|
+
if (isEmbedderReady()) {
|
|
49
|
+
const vecResults = await vectorSearch(db, query, limit * 2);
|
|
50
|
+
for (const r of vecResults) {
|
|
51
|
+
const existing = scoreMap.get(r.id);
|
|
52
|
+
const score = r.score * VECTOR_WEIGHT;
|
|
53
|
+
if (existing) {
|
|
54
|
+
existing.score += score;
|
|
55
|
+
} else {
|
|
56
|
+
scoreMap.set(r.id, {
|
|
57
|
+
content: r.content,
|
|
58
|
+
sourceFile: r.sourceFile,
|
|
59
|
+
score,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Sort by combined score
|
|
66
|
+
const results = Array.from(scoreMap.entries())
|
|
67
|
+
.map(([id, data]) => ({ id, ...data }))
|
|
68
|
+
.sort((a, b) => b.score - a.score)
|
|
69
|
+
.slice(0, limit);
|
|
70
|
+
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { paths } from "../config/paths";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, appendFileSync } from "fs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read all markdown memory files from the workspace.
|
|
7
|
+
*/
|
|
8
|
+
export function readAllMemoryFiles(): Array<{
|
|
9
|
+
filePath: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}> {
|
|
12
|
+
const files: Array<{ filePath: string; content: string }> = [];
|
|
13
|
+
|
|
14
|
+
// Read MEMORY.md
|
|
15
|
+
if (existsSync(paths.memoryFile)) {
|
|
16
|
+
files.push({
|
|
17
|
+
filePath: paths.memoryFile,
|
|
18
|
+
content: readFileSync(paths.memoryFile, "utf-8"),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Read dated memory files
|
|
23
|
+
if (existsSync(paths.memory)) {
|
|
24
|
+
const memFiles = readdirSync(paths.memory).filter((f) =>
|
|
25
|
+
f.endsWith(".md")
|
|
26
|
+
);
|
|
27
|
+
for (const f of memFiles) {
|
|
28
|
+
const fp = join(paths.memory, f);
|
|
29
|
+
files.push({
|
|
30
|
+
filePath: fp,
|
|
31
|
+
content: readFileSync(fp, "utf-8"),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Append a memory entry to today's dated memory file.
|
|
41
|
+
*/
|
|
42
|
+
export function writeMemory(content: string): string {
|
|
43
|
+
const today = new Date().toISOString().split("T")[0];
|
|
44
|
+
const filePath = join(paths.memory, `${today}.md`);
|
|
45
|
+
|
|
46
|
+
if (!existsSync(filePath)) {
|
|
47
|
+
Bun.spawnSync(["mkdir", "-p", paths.memory]);
|
|
48
|
+
const header = `# Memories — ${today}\n\n`;
|
|
49
|
+
appendFileSync(filePath, header);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entry = `- ${content}\n`;
|
|
53
|
+
appendFileSync(filePath, entry);
|
|
54
|
+
|
|
55
|
+
return filePath;
|
|
56
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { embed, cosineSimilarity, isEmbedderReady } from "./embedder";
|
|
3
|
+
import { logger } from "../util/logger";
|
|
4
|
+
|
|
5
|
+
export interface VectorResult {
|
|
6
|
+
id: number;
|
|
7
|
+
content: string;
|
|
8
|
+
sourceFile: string;
|
|
9
|
+
score: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Store embedding as blob in memory_chunks table.
|
|
14
|
+
*/
|
|
15
|
+
export async function storeEmbedding(
|
|
16
|
+
db: Database,
|
|
17
|
+
chunkId: number,
|
|
18
|
+
embedding: Float32Array
|
|
19
|
+
) {
|
|
20
|
+
const blob = Buffer.from(embedding.buffer);
|
|
21
|
+
db.prepare("UPDATE memory_chunks SET embedding = ? WHERE id = ?").run(
|
|
22
|
+
blob,
|
|
23
|
+
chunkId
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Vector search using cosine similarity computed in JS.
|
|
29
|
+
* Since sqlite-vec can't load, we pull all embeddings and compute in memory.
|
|
30
|
+
* This is fine for personal memory (<10k chunks).
|
|
31
|
+
*/
|
|
32
|
+
export async function vectorSearch(
|
|
33
|
+
db: Database,
|
|
34
|
+
query: string,
|
|
35
|
+
limit: number = 5
|
|
36
|
+
): Promise<VectorResult[]> {
|
|
37
|
+
if (!isEmbedderReady()) return [];
|
|
38
|
+
|
|
39
|
+
const queryEmbedding = await embed(query);
|
|
40
|
+
if (!queryEmbedding) return [];
|
|
41
|
+
|
|
42
|
+
const rows = db
|
|
43
|
+
.query(
|
|
44
|
+
"SELECT id, content, source_file, embedding FROM memory_chunks WHERE embedding IS NOT NULL ORDER BY id DESC LIMIT 5000"
|
|
45
|
+
)
|
|
46
|
+
.all() as Array<{
|
|
47
|
+
id: number;
|
|
48
|
+
content: string;
|
|
49
|
+
source_file: string;
|
|
50
|
+
embedding: Buffer;
|
|
51
|
+
}>;
|
|
52
|
+
|
|
53
|
+
const scored: VectorResult[] = [];
|
|
54
|
+
for (const row of rows) {
|
|
55
|
+
if (!row.embedding) continue;
|
|
56
|
+
const emb = new Float32Array(
|
|
57
|
+
row.embedding.buffer,
|
|
58
|
+
row.embedding.byteOffset,
|
|
59
|
+
row.embedding.byteLength / 4
|
|
60
|
+
);
|
|
61
|
+
const score = cosineSimilarity(queryEmbedding, emb);
|
|
62
|
+
scored.push({
|
|
63
|
+
id: row.id,
|
|
64
|
+
content: row.content,
|
|
65
|
+
sourceFile: row.source_file,
|
|
66
|
+
score,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
scored.sort((a, b) => b.score - a.score);
|
|
71
|
+
return scored.slice(0, limit);
|
|
72
|
+
}
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { paths } from "./config/paths";
|
|
3
|
+
import { configExists } from "./config/loader";
|
|
4
|
+
|
|
5
|
+
function readConfig(): any {
|
|
6
|
+
if (!configExists()) {
|
|
7
|
+
console.log("Config not found. Run 'zubo setup' first.");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
return JSON.parse(readFileSync(paths.config, "utf-8"));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function writeConfig(config: any) {
|
|
14
|
+
writeFileSync(paths.config, JSON.stringify(config, null, 2) + "\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function showCurrent() {
|
|
18
|
+
const config = readConfig();
|
|
19
|
+
|
|
20
|
+
if (config.providers && config.activeProvider) {
|
|
21
|
+
const active = config.providers[config.activeProvider];
|
|
22
|
+
console.log(`\n Active: ${config.activeProvider}/${active?.model ?? "?"}`);
|
|
23
|
+
|
|
24
|
+
if (config.failover?.length) {
|
|
25
|
+
const fallbacks = config.failover
|
|
26
|
+
.map((name: string) => {
|
|
27
|
+
const p = config.providers?.[name];
|
|
28
|
+
return p ? `${name}/${p.model}` : `${name} (not configured)`;
|
|
29
|
+
})
|
|
30
|
+
.join(", ");
|
|
31
|
+
console.log(` Failover: ${fallbacks}`);
|
|
32
|
+
}
|
|
33
|
+
} else if (config.anthropicApiKey) {
|
|
34
|
+
console.log(`\n Active: anthropic/${config.model ?? "claude-sonnet-4-5-20250929"} (legacy config)`);
|
|
35
|
+
} else {
|
|
36
|
+
console.log("\n No provider configured.");
|
|
37
|
+
}
|
|
38
|
+
console.log("");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listProviders() {
|
|
42
|
+
const config = readConfig();
|
|
43
|
+
|
|
44
|
+
console.log("\n Configured Providers\n");
|
|
45
|
+
|
|
46
|
+
if (config.providers) {
|
|
47
|
+
for (const [name, p] of Object.entries(config.providers) as [string, any][]) {
|
|
48
|
+
const active = name === config.activeProvider ? " ← active" : "";
|
|
49
|
+
const failover = config.failover?.includes(name) ? " (failover)" : "";
|
|
50
|
+
const baseUrl = p.baseUrl ? ` @ ${p.baseUrl}` : "";
|
|
51
|
+
console.log(` ${name}/${p.model}${baseUrl}${active}${failover}`);
|
|
52
|
+
}
|
|
53
|
+
} else if (config.anthropicApiKey) {
|
|
54
|
+
console.log(` anthropic/${config.model ?? "claude-sonnet-4-5-20250929"} ← active (legacy)`);
|
|
55
|
+
} else {
|
|
56
|
+
console.log(" None configured.");
|
|
57
|
+
}
|
|
58
|
+
console.log("");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function switchModel(target: string) {
|
|
62
|
+
const config = readConfig();
|
|
63
|
+
|
|
64
|
+
// Parse "provider/model" or just "provider"
|
|
65
|
+
const slashIdx = target.indexOf("/");
|
|
66
|
+
let providerName: string;
|
|
67
|
+
let modelName: string | null;
|
|
68
|
+
|
|
69
|
+
if (slashIdx > 0) {
|
|
70
|
+
providerName = target.substring(0, slashIdx);
|
|
71
|
+
modelName = target.substring(slashIdx + 1);
|
|
72
|
+
} else {
|
|
73
|
+
providerName = target;
|
|
74
|
+
modelName = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Ensure providers object exists
|
|
78
|
+
if (!config.providers) {
|
|
79
|
+
config.providers = {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if this provider is already configured
|
|
83
|
+
if (config.providers[providerName]) {
|
|
84
|
+
config.activeProvider = providerName;
|
|
85
|
+
if (modelName) {
|
|
86
|
+
config.providers[providerName].model = modelName;
|
|
87
|
+
}
|
|
88
|
+
// Keep legacy field in sync
|
|
89
|
+
config.model = config.providers[providerName].model;
|
|
90
|
+
|
|
91
|
+
writeConfig(config);
|
|
92
|
+
console.log(`Switched to ${providerName}/${config.providers[providerName].model}`);
|
|
93
|
+
console.log("Restart Zubo for changes to take effect.");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Provider not configured — offer quick setup for known providers
|
|
98
|
+
console.log(`Provider "${providerName}" is not configured.`);
|
|
99
|
+
console.log(`Run 'zubo setup' to add it, or edit ~/.zubo/config.json directly.`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function runModelCommand(args: string[]) {
|
|
104
|
+
if (args.includes("--list") || args.includes("-l")) {
|
|
105
|
+
listProviders();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// If a target is given, switch to it
|
|
110
|
+
const target = args.find((a) => !a.startsWith("-"));
|
|
111
|
+
if (target) {
|
|
112
|
+
switchModel(target);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Default: show current
|
|
117
|
+
showCurrent();
|
|
118
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { searchRegistry, fetchRegistry } from "./client";
|
|
2
|
+
import { installFromRegistry } from "./installer";
|
|
3
|
+
|
|
4
|
+
export async function handleRegistrySearch(query: string): Promise<void> {
|
|
5
|
+
console.log(`Searching registry for "${query}"...`);
|
|
6
|
+
const results = await searchRegistry(query);
|
|
7
|
+
if (results.length === 0) {
|
|
8
|
+
console.log("No skills found matching your query.");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
console.log(`\nFound ${results.length} skill(s):\n`);
|
|
12
|
+
for (const entry of results) {
|
|
13
|
+
console.log(` ${entry.name} — ${entry.description}`);
|
|
14
|
+
console.log(` repo: ${entry.repo} tags: ${entry.tags.join(", ")}`);
|
|
15
|
+
if (entry.secrets?.length) {
|
|
16
|
+
console.log(` requires: ${entry.secrets.join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
console.log();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function handleRegistryInstall(name: string): Promise<void> {
|
|
23
|
+
console.log(`Installing skill "${name}" from registry...`);
|
|
24
|
+
const result = await installFromRegistry(name);
|
|
25
|
+
if (result.success) {
|
|
26
|
+
console.log(`Installed "${name}" to ${result.path}`);
|
|
27
|
+
if (result.requiredSecrets?.length) {
|
|
28
|
+
console.log(`\nThis skill requires secrets: ${result.requiredSecrets.join(", ")}`);
|
|
29
|
+
console.log("Use 'zubo start' and tell Zubo to set them, or use the dashboard.");
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
console.error(`Failed: ${result.error}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function handleRegistryPublish(name: string): Promise<void> {
|
|
37
|
+
console.log(`Publishing skills is done via Pull Request to the zubo-skills/registry repo.`);
|
|
38
|
+
console.log(`\nSteps:`);
|
|
39
|
+
console.log(`1. Create a GitHub repo with your skill (SKILL.md + handler.ts)`);
|
|
40
|
+
console.log(`2. Fork https://github.com/zubo-skills/registry`);
|
|
41
|
+
console.log(`3. Add your skill entry to registry.json`);
|
|
42
|
+
console.log(`4. Submit a Pull Request`);
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { logger } from "../util/logger";
|
|
2
|
+
|
|
3
|
+
const REGISTRY_URL = "https://raw.githubusercontent.com/zubo-skills/registry/main/registry.json";
|
|
4
|
+
const CACHE_TTL_MS = 5 * 60_000; // 5 minutes
|
|
5
|
+
|
|
6
|
+
export interface RegistryEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
repo: string;
|
|
10
|
+
author: string;
|
|
11
|
+
version: string;
|
|
12
|
+
tags: string[];
|
|
13
|
+
secrets?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let cachedRegistry: RegistryEntry[] | null = null;
|
|
17
|
+
let cacheTime = 0;
|
|
18
|
+
|
|
19
|
+
export async function fetchRegistry(): Promise<RegistryEntry[]> {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
if (cachedRegistry && now - cacheTime < CACHE_TTL_MS) {
|
|
22
|
+
return cachedRegistry;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(REGISTRY_URL);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
throw new Error(`Registry fetch failed: ${res.status}`);
|
|
29
|
+
}
|
|
30
|
+
const data = (await res.json()) as { skills: RegistryEntry[] };
|
|
31
|
+
cachedRegistry = data.skills ?? [];
|
|
32
|
+
cacheTime = now;
|
|
33
|
+
return cachedRegistry;
|
|
34
|
+
} catch (err: any) {
|
|
35
|
+
logger.warn("Failed to fetch skill registry", { error: err.message });
|
|
36
|
+
return cachedRegistry ?? [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function searchRegistry(query: string): Promise<RegistryEntry[]> {
|
|
41
|
+
const registry = await fetchRegistry();
|
|
42
|
+
const q = query.toLowerCase();
|
|
43
|
+
return registry.filter(
|
|
44
|
+
(e) =>
|
|
45
|
+
e.name.toLowerCase().includes(q) ||
|
|
46
|
+
e.description.toLowerCase().includes(q) ||
|
|
47
|
+
e.tags.some((t) => t.toLowerCase().includes(q))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function getRegistryEntry(name: string): Promise<RegistryEntry | null> {
|
|
52
|
+
const registry = await fetchRegistry();
|
|
53
|
+
return registry.find((e) => e.name === name) ?? null;
|
|
54
|
+
}
|