jowork 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/dist/cli.js ADDED
@@ -0,0 +1,1407 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ configPath,
4
+ createJoWorkMcpServer,
5
+ credentialsDir,
6
+ dbPath,
7
+ joworkDir,
8
+ logError,
9
+ logInfo,
10
+ logsDir
11
+ } from "./chunk-S24PDC46.js";
12
+ import {
13
+ createId
14
+ } from "./chunk-3NMLDZBL.js";
15
+
16
+ // src/cli.ts
17
+ import { Command } from "commander";
18
+
19
+ // src/commands/init.ts
20
+ import { existsSync as existsSync2 } from "fs";
21
+
22
+ // src/db/manager.ts
23
+ import Database from "better-sqlite3";
24
+ import { drizzle } from "drizzle-orm/better-sqlite3";
25
+ var MIGRATIONS = [
26
+ // 001 — Base tables
27
+ `
28
+ CREATE TABLE IF NOT EXISTS settings (
29
+ key TEXT PRIMARY KEY,
30
+ value TEXT NOT NULL,
31
+ updated_at INTEGER NOT NULL
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS connector_configs (
35
+ id TEXT PRIMARY KEY,
36
+ type TEXT NOT NULL,
37
+ name TEXT NOT NULL,
38
+ status TEXT NOT NULL DEFAULT 'disconnected',
39
+ config TEXT NOT NULL DEFAULT '{}',
40
+ last_sync_at INTEGER,
41
+ created_at INTEGER NOT NULL,
42
+ updated_at INTEGER NOT NULL
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS objects (
46
+ id TEXT PRIMARY KEY,
47
+ source TEXT NOT NULL,
48
+ source_type TEXT NOT NULL,
49
+ uri TEXT NOT NULL UNIQUE,
50
+ title TEXT,
51
+ summary TEXT,
52
+ tags TEXT,
53
+ doc_map TEXT,
54
+ content_hash TEXT,
55
+ last_synced_at INTEGER,
56
+ created_at INTEGER
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS object_bodies (
60
+ object_id TEXT PRIMARY KEY REFERENCES objects(id),
61
+ content TEXT NOT NULL,
62
+ content_type TEXT,
63
+ fetched_at INTEGER
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS object_chunks (
67
+ id TEXT PRIMARY KEY,
68
+ object_id TEXT NOT NULL REFERENCES objects(id),
69
+ idx INTEGER NOT NULL,
70
+ heading TEXT,
71
+ content TEXT NOT NULL,
72
+ tokens INTEGER,
73
+ UNIQUE(object_id, idx)
74
+ );
75
+ CREATE INDEX IF NOT EXISTS idx_chunks_object ON object_chunks(object_id);
76
+
77
+ CREATE TABLE IF NOT EXISTS sync_cursors (
78
+ connector_id TEXT PRIMARY KEY,
79
+ cursor TEXT,
80
+ last_synced_at INTEGER
81
+ );
82
+
83
+ CREATE TABLE IF NOT EXISTS memories (
84
+ id TEXT PRIMARY KEY,
85
+ title TEXT NOT NULL,
86
+ content TEXT NOT NULL,
87
+ tags TEXT,
88
+ scope TEXT NOT NULL DEFAULT 'personal',
89
+ pinned INTEGER DEFAULT 0,
90
+ source TEXT,
91
+ access_count INTEGER NOT NULL DEFAULT 0,
92
+ last_used_at INTEGER,
93
+ created_at INTEGER NOT NULL,
94
+ updated_at INTEGER NOT NULL
95
+ );
96
+ CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
97
+ `,
98
+ // 002 — Object links
99
+ `
100
+ CREATE TABLE IF NOT EXISTS object_links (
101
+ id TEXT PRIMARY KEY,
102
+ source_object_id TEXT NOT NULL,
103
+ target_object_id TEXT,
104
+ link_type TEXT NOT NULL,
105
+ identifier TEXT NOT NULL,
106
+ metadata TEXT,
107
+ confidence TEXT DEFAULT 'medium',
108
+ created_at INTEGER NOT NULL
109
+ );
110
+ CREATE INDEX IF NOT EXISTS idx_object_links_source ON object_links(source_object_id);
111
+ CREATE INDEX IF NOT EXISTS idx_object_links_target ON object_links(target_object_id);
112
+ CREATE INDEX IF NOT EXISTS idx_object_links_type ON object_links(link_type);
113
+ `,
114
+ // 003 — Goal-Signal-Measure system
115
+ `
116
+ CREATE TABLE IF NOT EXISTS goals (
117
+ id TEXT PRIMARY KEY,
118
+ title TEXT NOT NULL,
119
+ description TEXT,
120
+ status TEXT NOT NULL DEFAULT 'active',
121
+ autonomy_level TEXT NOT NULL DEFAULT 'copilot',
122
+ parent_id TEXT,
123
+ evolved_from TEXT,
124
+ created_at INTEGER NOT NULL,
125
+ updated_at INTEGER NOT NULL
126
+ );
127
+
128
+ CREATE TABLE IF NOT EXISTS signals (
129
+ id TEXT PRIMARY KEY,
130
+ goal_id TEXT NOT NULL REFERENCES goals(id),
131
+ title TEXT NOT NULL,
132
+ source TEXT NOT NULL,
133
+ metric TEXT NOT NULL,
134
+ direction TEXT NOT NULL,
135
+ poll_interval INTEGER DEFAULT 3600,
136
+ config TEXT,
137
+ current_value REAL,
138
+ last_polled_at INTEGER,
139
+ created_at INTEGER NOT NULL,
140
+ updated_at INTEGER NOT NULL
141
+ );
142
+
143
+ CREATE TABLE IF NOT EXISTS measures (
144
+ id TEXT PRIMARY KEY,
145
+ signal_id TEXT NOT NULL REFERENCES signals(id),
146
+ threshold REAL NOT NULL,
147
+ comparison TEXT NOT NULL,
148
+ upper_bound REAL,
149
+ current REAL,
150
+ met INTEGER DEFAULT 0,
151
+ last_evaluated_at INTEGER,
152
+ created_at INTEGER NOT NULL,
153
+ updated_at INTEGER NOT NULL
154
+ );
155
+
156
+ CREATE INDEX IF NOT EXISTS idx_goals_status ON goals(status);
157
+ CREATE INDEX IF NOT EXISTS idx_signals_goal ON signals(goal_id);
158
+ CREATE INDEX IF NOT EXISTS idx_measures_signal ON measures(signal_id);
159
+ `
160
+ ];
161
+ var DbManager = class {
162
+ db;
163
+ sqlite;
164
+ constructor(dbPath2) {
165
+ this.sqlite = new Database(dbPath2);
166
+ this.sqlite.pragma("journal_mode = WAL");
167
+ this.sqlite.pragma("busy_timeout = 5000");
168
+ this.sqlite.pragma("foreign_keys = ON");
169
+ this.db = drizzle(this.sqlite);
170
+ logInfo("database", "Database opened", { dbPath: dbPath2 });
171
+ }
172
+ /** Run all pending migrations, then ensure FTS virtual tables. */
173
+ migrate() {
174
+ this.sqlite.exec(`
175
+ CREATE TABLE IF NOT EXISTS schema_version (
176
+ version INTEGER PRIMARY KEY
177
+ );
178
+ `);
179
+ const currentRow = this.sqlite.prepare(
180
+ "SELECT MAX(version) AS v FROM schema_version"
181
+ ).get();
182
+ const current = currentRow?.v ?? 0;
183
+ for (let i = current; i < MIGRATIONS.length; i++) {
184
+ const version = i + 1;
185
+ const sql = MIGRATIONS[i];
186
+ const txn = this.sqlite.transaction(() => {
187
+ this.sqlite.exec(sql);
188
+ this.sqlite.prepare("INSERT INTO schema_version (version) VALUES (?)").run(version);
189
+ });
190
+ try {
191
+ txn();
192
+ logInfo("database", `Migration ${String(version).padStart(3, "0")} applied`);
193
+ } catch (err) {
194
+ logError("database", `Migration ${String(version).padStart(3, "0")} failed`, {
195
+ error: err instanceof Error ? err.message : String(err)
196
+ });
197
+ throw err;
198
+ }
199
+ }
200
+ this.ensureFts();
201
+ }
202
+ ensureFts() {
203
+ const ftsCheck = this.sqlite.prepare(
204
+ `SELECT name FROM sqlite_master WHERE type='table' AND name IN ('objects_fts', 'memories_fts')`
205
+ ).all();
206
+ const existing = new Set(ftsCheck.map((r) => r.name));
207
+ if (!existing.has("objects_fts")) {
208
+ this.sqlite.exec(`
209
+ CREATE VIRTUAL TABLE objects_fts USING fts5(
210
+ title, summary, tags, source, source_type, body_excerpt,
211
+ content=''
212
+ );
213
+ `);
214
+ logInfo("database", "Created objects_fts virtual table");
215
+ }
216
+ if (!existing.has("memories_fts")) {
217
+ this.sqlite.exec(`
218
+ CREATE VIRTUAL TABLE memories_fts USING fts5(
219
+ title, content, tags,
220
+ content=''
221
+ );
222
+ `);
223
+ const rows = this.sqlite.prepare(
224
+ `SELECT rowid, title, content, COALESCE(tags, '') AS tags FROM memories`
225
+ ).all();
226
+ if (rows.length > 0) {
227
+ const insert = this.sqlite.prepare(
228
+ `INSERT INTO memories_fts(rowid, title, content, tags) VALUES (?, ?, ?, ?)`
229
+ );
230
+ for (const r of rows) {
231
+ insert.run(r.rowid, r.title, r.content, r.tags);
232
+ }
233
+ logInfo("database", `Backfilled ${rows.length} memories into FTS`);
234
+ }
235
+ logInfo("database", "Created memories_fts virtual table");
236
+ }
237
+ }
238
+ /** Convenience: migrate + return self for chaining. */
239
+ ensureTables() {
240
+ this.migrate();
241
+ return this;
242
+ }
243
+ getDb() {
244
+ return this.db;
245
+ }
246
+ getSqlite() {
247
+ return this.sqlite;
248
+ }
249
+ close() {
250
+ try {
251
+ this.sqlite.close();
252
+ logInfo("database", "Database closed");
253
+ } catch {
254
+ }
255
+ }
256
+ };
257
+
258
+ // src/utils/config.ts
259
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
260
+ var DEFAULT_CONFIG = {
261
+ version: "0.1.0",
262
+ initialized: false,
263
+ connectors: {}
264
+ };
265
+ function readConfig() {
266
+ const path = configPath();
267
+ if (!existsSync(path)) return { ...DEFAULT_CONFIG };
268
+ try {
269
+ return JSON.parse(readFileSync(path, "utf-8"));
270
+ } catch {
271
+ return { ...DEFAULT_CONFIG };
272
+ }
273
+ }
274
+ function writeConfig(config) {
275
+ mkdirSync(joworkDir(), { recursive: true });
276
+ writeFileSync(configPath(), JSON.stringify(config, null, 2));
277
+ }
278
+
279
+ // src/commands/init.ts
280
+ function initCommand(program2) {
281
+ program2.command("init").description("Initialize JoWork \u2014 create local database and config").action(async () => {
282
+ const config = readConfig();
283
+ if (config.initialized && existsSync2(dbPath())) {
284
+ console.log("\u2713 JoWork already initialized at", joworkDir());
285
+ return;
286
+ }
287
+ console.log("Initializing JoWork...");
288
+ const db = new DbManager(dbPath());
289
+ db.ensureTables();
290
+ db.close();
291
+ writeConfig({ ...config, initialized: true });
292
+ console.log("\u2713 Database created at", dbPath());
293
+ console.log("\u2713 Config saved at", joworkDir());
294
+ console.log("");
295
+ console.log("Next steps:");
296
+ console.log(" jowork register claude-code # Connect to Claude Code");
297
+ console.log(" jowork connect feishu # Connect Feishu data source");
298
+ });
299
+ }
300
+
301
+ // src/commands/serve.ts
302
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, appendFileSync, mkdirSync as mkdirSync3 } from "fs";
303
+ import { join as join2 } from "path";
304
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
305
+ import { Cron } from "croner";
306
+
307
+ // src/connectors/credential-store.ts
308
+ import {
309
+ readFileSync as readFileSync2,
310
+ writeFileSync as writeFileSync2,
311
+ existsSync as existsSync3,
312
+ mkdirSync as mkdirSync2,
313
+ chmodSync,
314
+ unlinkSync,
315
+ readdirSync
316
+ } from "fs";
317
+ import { join } from "path";
318
+ function saveCredential(name, credential) {
319
+ const dir = credentialsDir();
320
+ mkdirSync2(dir, { recursive: true });
321
+ const filePath = join(dir, `${name}.json`);
322
+ writeFileSync2(filePath, JSON.stringify(credential, null, 2));
323
+ try {
324
+ chmodSync(filePath, 384);
325
+ } catch {
326
+ }
327
+ }
328
+ function loadCredential(name) {
329
+ const filePath = join(credentialsDir(), `${name}.json`);
330
+ if (!existsSync3(filePath)) return null;
331
+ try {
332
+ return JSON.parse(readFileSync2(filePath, "utf-8"));
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
337
+ function listCredentials() {
338
+ const dir = credentialsDir();
339
+ if (!existsSync3(dir)) return [];
340
+ return readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
341
+ }
342
+
343
+ // src/sync/linker.ts
344
+ var PATTERNS = [
345
+ // GitHub/GitLab PR/Issue references
346
+ { type: "pr", regex: /(?:PR|pr|Pull Request|pull request)\s*#?(\d+)/g, confidence: "high" },
347
+ { type: "issue", regex: /(?:issue|Issue|ISSUE)\s*#?(\d+)/g, confidence: "high" },
348
+ { type: "issue", regex: /#(\d{2,6})\b/g, confidence: "medium" },
349
+ // bare #123
350
+ // Linear-style issue keys
351
+ { type: "issue", regex: /\b([A-Z]{2,10}-\d{1,6})\b/g, confidence: "high" },
352
+ // Git commit SHA
353
+ { type: "commit", regex: /\b([0-9a-f]{7,40})\b/g, confidence: "low" },
354
+ // URLs
355
+ { type: "url", regex: /https?:\/\/[^\s<>"{}|\\^`\[\]]+/g, confidence: "high" },
356
+ // @mentions (feishu user_id format)
357
+ { type: "mention", regex: /@([a-zA-Z0-9_]+)/g, confidence: "medium" }
358
+ ];
359
+ function extractLinks(content) {
360
+ const links = [];
361
+ const seen = /* @__PURE__ */ new Set();
362
+ for (const pattern of PATTERNS) {
363
+ if (pattern.type === "commit" && content.length < 100) continue;
364
+ const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
365
+ let match;
366
+ while ((match = regex.exec(content)) !== null) {
367
+ const identifier = match[1] ?? match[0];
368
+ const key = `${pattern.type}:${identifier}`;
369
+ if (seen.has(key)) continue;
370
+ seen.add(key);
371
+ if (identifier.length < 3) continue;
372
+ if (pattern.type === "commit" && identifier.length < 7) continue;
373
+ links.push({
374
+ linkType: pattern.type,
375
+ identifier,
376
+ confidence: pattern.confidence
377
+ });
378
+ }
379
+ }
380
+ return links;
381
+ }
382
+ function processObjectLinks(sqlite, objectId, content) {
383
+ const links = extractLinks(content);
384
+ if (links.length === 0) return 0;
385
+ const insert = sqlite.prepare(`
386
+ INSERT OR IGNORE INTO object_links (id, source_object_id, target_object_id, link_type, identifier, metadata, confidence, created_at)
387
+ VALUES (?, ?, NULL, ?, ?, ?, ?, ?)
388
+ `);
389
+ const now = Date.now();
390
+ let count = 0;
391
+ const batch = sqlite.transaction(() => {
392
+ for (const link of links) {
393
+ const id = `${objectId}:${link.linkType}:${link.identifier}`.slice(0, 64);
394
+ insert.run(
395
+ id,
396
+ objectId,
397
+ link.linkType,
398
+ link.identifier,
399
+ link.metadata ? JSON.stringify(link.metadata) : null,
400
+ link.confidence,
401
+ now
402
+ );
403
+ count++;
404
+ }
405
+ });
406
+ batch();
407
+ return count;
408
+ }
409
+ function linkAllUnprocessed(sqlite) {
410
+ const unprocessed = sqlite.prepare(`
411
+ SELECT o.id, ob.content FROM objects o
412
+ JOIN object_bodies ob ON ob.object_id = o.id
413
+ LEFT JOIN object_links ol ON ol.source_object_id = o.id
414
+ WHERE ol.id IS NULL
415
+ LIMIT 1000
416
+ `).all();
417
+ let linksCreated = 0;
418
+ for (const obj of unprocessed) {
419
+ linksCreated += processObjectLinks(sqlite, obj.id, obj.content);
420
+ }
421
+ logInfo("linker", `Processed ${unprocessed.length} objects, created ${linksCreated} links`);
422
+ return { processed: unprocessed.length, linksCreated };
423
+ }
424
+
425
+ // src/commands/serve.ts
426
+ function serveCommand(program2) {
427
+ program2.command("serve").description("Start MCP server (stdio mode for agents, or --daemon for background)").option("--daemon", "Run as background daemon with cron sync").action(async (opts) => {
428
+ const resolvedDbPath = process.env["JOWORK_DB_PATH"] ?? dbPath();
429
+ if (!existsSync4(resolvedDbPath)) {
430
+ console.error("Error: JoWork not initialized. Run `jowork init` first.");
431
+ process.exit(1);
432
+ }
433
+ if (opts.daemon) {
434
+ await startDaemon();
435
+ return;
436
+ }
437
+ const server = createJoWorkMcpServer({ dbPath: resolvedDbPath });
438
+ const transport = new StdioServerTransport();
439
+ await server.connect(transport);
440
+ });
441
+ }
442
+ var SYNC_INTERVAL = "*/15 * * * *";
443
+ function daemonLog(level, msg, ctx) {
444
+ const logFile = join2(logsDir(), "daemon.log");
445
+ const entry = JSON.stringify({
446
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
447
+ level,
448
+ msg,
449
+ ...ctx
450
+ });
451
+ appendFileSync(logFile, entry + "\n");
452
+ }
453
+ async function startDaemon() {
454
+ const pidFile = join2(joworkDir(), "daemon.pid");
455
+ if (existsSync4(pidFile)) {
456
+ const existingPid = readFileSync3(pidFile, "utf-8").trim();
457
+ try {
458
+ process.kill(parseInt(existingPid, 10), 0);
459
+ console.error(`Daemon already running (PID ${existingPid}). Kill it first or delete ${pidFile}`);
460
+ process.exit(1);
461
+ } catch {
462
+ }
463
+ }
464
+ mkdirSync3(joworkDir(), { recursive: true });
465
+ writeFileSync3(pidFile, process.pid.toString());
466
+ const cleanup = () => {
467
+ try {
468
+ unlinkSync2(pidFile);
469
+ } catch {
470
+ }
471
+ daemonLog("info", "Daemon stopped");
472
+ process.exit(0);
473
+ };
474
+ process.on("SIGINT", cleanup);
475
+ process.on("SIGTERM", cleanup);
476
+ const envFile = join2(process.cwd(), ".env");
477
+ if (existsSync4(envFile)) {
478
+ for (const line of readFileSync3(envFile, "utf-8").split("\n")) {
479
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
480
+ if (match) process.env[match[1]] = match[2];
481
+ }
482
+ }
483
+ daemonLog("info", "Daemon started", { pid: process.pid });
484
+ await runSync();
485
+ const _syncJob = new Cron(SYNC_INTERVAL, async () => {
486
+ await runSync();
487
+ });
488
+ console.log(`Daemon started (PID ${process.pid})`);
489
+ console.log(` Sync: every 15 minutes`);
490
+ console.log(` PID file: ${pidFile}`);
491
+ console.log(` Log file: ${join2(logsDir(), "daemon.log")}`);
492
+ console.log(" Press Ctrl+C to stop");
493
+ setInterval(() => {
494
+ }, 6e4);
495
+ }
496
+ async function runSync() {
497
+ const sources = listCredentials();
498
+ if (sources.length === 0) {
499
+ daemonLog("info", "No sources connected, skipping sync");
500
+ return;
501
+ }
502
+ daemonLog("info", "Sync cycle starting", { sources });
503
+ let db = null;
504
+ try {
505
+ db = new DbManager(dbPath());
506
+ db.ensureTables();
507
+ const sqlite = db.getSqlite();
508
+ for (const source of sources) {
509
+ const cred = loadCredential(source);
510
+ if (!cred) {
511
+ daemonLog("warn", `No credentials for ${source}, skipping`);
512
+ continue;
513
+ }
514
+ try {
515
+ switch (source) {
516
+ case "feishu":
517
+ await syncFeishuDaemon(db, cred.data);
518
+ break;
519
+ default:
520
+ daemonLog("info", `Source ${source} sync not implemented yet`);
521
+ }
522
+ } catch (err) {
523
+ daemonLog("error", `Failed to sync ${source}`, {
524
+ error: err instanceof Error ? err.message : String(err)
525
+ });
526
+ }
527
+ }
528
+ const { processed, linksCreated } = linkAllUnprocessed(sqlite);
529
+ if (processed > 0) {
530
+ daemonLog("info", "Entity extraction complete", { processed, linksCreated });
531
+ }
532
+ } catch (err) {
533
+ daemonLog("error", "Sync cycle failed", {
534
+ error: err instanceof Error ? err.message : String(err)
535
+ });
536
+ } finally {
537
+ db?.close();
538
+ }
539
+ daemonLog("info", "Sync cycle complete");
540
+ }
541
+ async function syncFeishuDaemon(db, data) {
542
+ const { appId, appSecret } = data;
543
+ if (!appId || !appSecret) throw new Error("Missing Feishu credentials");
544
+ const tokenRes = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
545
+ method: "POST",
546
+ headers: { "Content-Type": "application/json" },
547
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret })
548
+ });
549
+ const tokenData = await tokenRes.json();
550
+ if (tokenData.code !== 0) throw new Error(`Auth failed: code ${tokenData.code}`);
551
+ const token = tokenData.tenant_access_token;
552
+ const chatsRes = await fetch("https://open.feishu.cn/open-apis/im/v1/chats?page_size=50", {
553
+ headers: { Authorization: `Bearer ${token}` }
554
+ });
555
+ const chatsData = await chatsRes.json();
556
+ if (chatsData.code !== 0) throw new Error(`Failed to list chats: code ${chatsData.code}`);
557
+ const chats = chatsData.data?.items ?? [];
558
+ const sqlite = db.getSqlite();
559
+ let totalMessages = 0;
560
+ let newMessages = 0;
561
+ const { createId: createId2 } = await import("./src-WYAQWZZZ.js");
562
+ for (const chat of chats) {
563
+ const cursorRow = sqlite.prepare(`SELECT cursor FROM sync_cursors WHERE connector_id = ?`).get(`feishu:${chat.chat_id}`);
564
+ let pageToken = cursorRow?.cursor ?? void 0;
565
+ let hasMore = true;
566
+ while (hasMore) {
567
+ const url = new URL("https://open.feishu.cn/open-apis/im/v1/messages");
568
+ url.searchParams.set("container_id_type", "chat");
569
+ url.searchParams.set("container_id", chat.chat_id);
570
+ url.searchParams.set("page_size", "50");
571
+ url.searchParams.set("sort_type", "ByCreateTimeAsc");
572
+ if (pageToken) url.searchParams.set("page_token", pageToken);
573
+ const msgRes = await fetch(url.toString(), {
574
+ headers: { Authorization: `Bearer ${token}` }
575
+ });
576
+ const msgData = await msgRes.json();
577
+ if (msgData.code !== 0) {
578
+ if (msgData.code === 99991400) {
579
+ daemonLog("warn", `Rate limited on ${chat.name}, waiting 5s`);
580
+ await new Promise((r) => setTimeout(r, 5e3));
581
+ continue;
582
+ }
583
+ daemonLog("warn", `Failed to get messages from "${chat.name}": code ${msgData.code}`);
584
+ break;
585
+ }
586
+ const messages = msgData.data?.items ?? [];
587
+ const insertObj = sqlite.prepare(`
588
+ INSERT OR IGNORE INTO objects (id, source, source_type, uri, title, summary, tags, content_hash, last_synced_at, created_at)
589
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
590
+ `);
591
+ const insertBody = sqlite.prepare(`
592
+ INSERT OR IGNORE INTO object_bodies (object_id, content, content_type, fetched_at)
593
+ VALUES (?, ?, ?, ?)
594
+ `);
595
+ const batchInsert = sqlite.transaction((msgs) => {
596
+ for (const msg of msgs) {
597
+ if (msg.msg_type !== "text" && msg.msg_type !== "post") continue;
598
+ let content = "";
599
+ try {
600
+ const bodyContent = JSON.parse(msg.body?.content ?? "{}");
601
+ const raw = bodyContent.text ?? bodyContent.content ?? bodyContent;
602
+ content = typeof raw === "string" ? raw : JSON.stringify(raw);
603
+ } catch {
604
+ content = msg.body?.content ?? "";
605
+ }
606
+ if (!content || typeof content !== "string") continue;
607
+ const uri = `feishu://message/${msg.message_id}`;
608
+ const hash = simpleHash(content);
609
+ const now = Date.now();
610
+ const id = createId2("obj");
611
+ const createTime = msg.create_time ? parseInt(msg.create_time) : now;
612
+ const summary = content.length > 200 ? content.slice(0, 200) + "..." : content;
613
+ insertObj.run(id, "feishu", "message", uri, `${chat.name}`, summary, JSON.stringify(["feishu", "message"]), hash, now, createTime);
614
+ insertBody.run(id, content, "text/plain", now);
615
+ newMessages++;
616
+ }
617
+ });
618
+ for (let i = 0; i < messages.length; i += 100) {
619
+ batchInsert(messages.slice(i, i + 100));
620
+ }
621
+ totalMessages += messages.length;
622
+ hasMore = msgData.data.has_more;
623
+ pageToken = msgData.data.page_token;
624
+ if (pageToken) {
625
+ sqlite.prepare(`INSERT OR REPLACE INTO sync_cursors (connector_id, cursor, last_synced_at) VALUES (?, ?, ?)`).run(`feishu:${chat.chat_id}`, pageToken, Date.now());
626
+ }
627
+ }
628
+ }
629
+ try {
630
+ sqlite.exec(`INSERT INTO objects_fts(objects_fts) VALUES('rebuild')`);
631
+ } catch {
632
+ }
633
+ daemonLog("info", "Feishu sync complete", { totalMessages, newMessages, chats: chats.length });
634
+ }
635
+ function simpleHash(str) {
636
+ let hash = 0;
637
+ for (let i = 0; i < str.length; i++) {
638
+ const char = str.charCodeAt(i);
639
+ hash = (hash << 5) - hash + char;
640
+ hash = hash & hash;
641
+ }
642
+ return hash.toString(36);
643
+ }
644
+
645
+ // src/commands/register.ts
646
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, copyFileSync, existsSync as existsSync5 } from "fs";
647
+ import { join as join3 } from "path";
648
+ var HOME = process.env["HOME"] ?? "";
649
+ function registerCommand(program2) {
650
+ program2.command("register").description("Register JoWork MCP server with an AI agent engine").argument("<engine>", "Engine to register with: claude-code, codex").action(async (engine) => {
651
+ switch (engine) {
652
+ case "claude-code":
653
+ registerClaudeCode();
654
+ break;
655
+ case "codex":
656
+ console.log("Codex registration not yet implemented.");
657
+ break;
658
+ default:
659
+ console.error(`Unknown engine: ${engine}. Supported: claude-code, codex`);
660
+ process.exit(1);
661
+ }
662
+ });
663
+ }
664
+ function registerClaudeCode() {
665
+ const configPath2 = join3(HOME, ".claude.json");
666
+ if (existsSync5(configPath2)) {
667
+ const backupPath = configPath2 + ".bak";
668
+ copyFileSync(configPath2, backupPath);
669
+ console.log(`\u2713 Backed up existing config to ${backupPath}`);
670
+ }
671
+ let config = {};
672
+ if (existsSync5(configPath2)) {
673
+ try {
674
+ config = JSON.parse(readFileSync4(configPath2, "utf-8"));
675
+ } catch {
676
+ console.error(`Warning: ${configPath2} contains invalid JSON. Creating fresh config.`);
677
+ console.error(` Original backed up to ${configPath2}.bak`);
678
+ config = {};
679
+ }
680
+ }
681
+ if (!config.mcpServers) config.mcpServers = {};
682
+ config.mcpServers["jowork"] = {
683
+ command: "jowork",
684
+ args: ["serve"]
685
+ };
686
+ writeFileSync4(configPath2, JSON.stringify(config, null, 2));
687
+ console.log(`\u2713 Registered JoWork MCP server in ${configPath2}`);
688
+ console.log("");
689
+ console.log("Claude Code will now have access to JoWork tools:");
690
+ console.log(" search_data, read_memory, write_memory, search_memory, ...");
691
+ }
692
+
693
+ // src/commands/status.ts
694
+ import { existsSync as existsSync6, statSync } from "fs";
695
+ function statusCommand(program2) {
696
+ program2.command("status").description("Show JoWork system status").action(async () => {
697
+ if (!existsSync6(dbPath())) {
698
+ console.log("JoWork is not initialized. Run `jowork init` first.");
699
+ return;
700
+ }
701
+ const db = new DbManager(dbPath());
702
+ const sqlite = db.getSqlite();
703
+ console.log("JoWork Status");
704
+ console.log("\u2500".repeat(40));
705
+ console.log(` Data dir: ${joworkDir()}`);
706
+ const stats = statSync(dbPath());
707
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
708
+ console.log(` Database: ${sizeMB} MB`);
709
+ console.log("");
710
+ console.log("Data:");
711
+ const tables = ["objects", "memories", "connector_configs", "object_links"];
712
+ for (const table of tables) {
713
+ try {
714
+ const row = sqlite.prepare(`SELECT COUNT(*) as count FROM ${table}`).get();
715
+ console.log(` ${table.padEnd(20)} ${row.count} rows`);
716
+ } catch {
717
+ console.log(` ${table.padEnd(20)} (not created)`);
718
+ }
719
+ }
720
+ console.log("");
721
+ console.log("Connectors:");
722
+ const creds = listCredentials();
723
+ if (creds.length === 0) {
724
+ console.log(" (none connected)");
725
+ } else {
726
+ for (const name of creds) {
727
+ console.log(` \u2713 ${name}`);
728
+ }
729
+ }
730
+ try {
731
+ const cursor = sqlite.prepare(`SELECT connector_id, last_synced_at FROM sync_cursors ORDER BY last_synced_at DESC LIMIT 1`).get();
732
+ if (cursor) {
733
+ const lastSync = new Date(cursor.last_synced_at).toLocaleString();
734
+ console.log(`
735
+ Last sync: ${cursor.connector_id} at ${lastSync}`);
736
+ }
737
+ } catch {
738
+ }
739
+ db.close();
740
+ });
741
+ }
742
+
743
+ // src/commands/doctor.ts
744
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
745
+ import { join as join4 } from "path";
746
+ function doctorCommand(program2) {
747
+ program2.command("doctor").description("Run diagnostic checks").action(async () => {
748
+ console.log("JoWork Doctor");
749
+ console.log("\u2500".repeat(40));
750
+ let ok = true;
751
+ const nodeVersion = process.versions.node;
752
+ const major = parseInt(nodeVersion.split(".")[0]);
753
+ if (major >= 20) {
754
+ console.log(` \u2713 Node.js ${nodeVersion}`);
755
+ } else {
756
+ console.log(` \u2717 Node.js ${nodeVersion} (requires >= 20)`);
757
+ ok = false;
758
+ }
759
+ if (existsSync7(joworkDir())) {
760
+ console.log(` \u2713 Data directory exists: ${joworkDir()}`);
761
+ } else {
762
+ console.log(` \u2717 Data directory missing: ${joworkDir()}`);
763
+ ok = false;
764
+ }
765
+ if (existsSync7(dbPath())) {
766
+ try {
767
+ const Database3 = (await import("better-sqlite3")).default;
768
+ const db = new Database3(dbPath());
769
+ db.pragma("integrity_check");
770
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
771
+ console.log(` \u2713 Database OK (${tables.length} tables)`);
772
+ db.close();
773
+ } catch (err) {
774
+ console.log(` \u2717 Database error: ${err}`);
775
+ ok = false;
776
+ }
777
+ } else {
778
+ console.log(` \u2717 Database not found. Run \`jowork init\``);
779
+ ok = false;
780
+ }
781
+ if (existsSync7(configPath())) {
782
+ console.log(` \u2713 Config file exists`);
783
+ } else {
784
+ console.log(` \u2717 Config file missing`);
785
+ ok = false;
786
+ }
787
+ const claudeConfigPath = join4(process.env["HOME"] ?? "", ".claude.json");
788
+ if (existsSync7(claudeConfigPath)) {
789
+ try {
790
+ const config = JSON.parse(readFileSync5(claudeConfigPath, "utf-8"));
791
+ if (config.mcpServers?.jowork) {
792
+ console.log(` \u2713 Registered with Claude Code`);
793
+ } else {
794
+ console.log(` \u25CB Not registered with Claude Code (run \`jowork register claude-code\`)`);
795
+ }
796
+ } catch {
797
+ console.log(` \u25CB Cannot read Claude Code config`);
798
+ }
799
+ } else {
800
+ console.log(` \u25CB Claude Code config not found`);
801
+ }
802
+ console.log("");
803
+ console.log(ok ? "\u2713 All checks passed" : "\u2717 Some checks failed");
804
+ });
805
+ }
806
+
807
+ // src/commands/export.ts
808
+ import { existsSync as existsSync8, writeFileSync as writeFileSync5 } from "fs";
809
+ import Database2 from "better-sqlite3";
810
+ function exportCommand(program2) {
811
+ program2.command("export").description("Export database backup").option("--format <format>", "Export format: sqlite or json", "sqlite").option("--output <path>", "Output file path").action(async (opts) => {
812
+ if (!existsSync8(dbPath())) {
813
+ console.error("Error: JoWork not initialized. Run `jowork init` first.");
814
+ process.exit(1);
815
+ }
816
+ const outputPath = opts.output ?? `jowork-backup-${Date.now()}.${opts.format === "json" ? "json" : "db"}`;
817
+ if (opts.format === "sqlite") {
818
+ const db = new Database2(dbPath());
819
+ await db.backup(outputPath);
820
+ db.close();
821
+ console.log(`\u2713 Database backed up to ${outputPath}`);
822
+ } else if (opts.format === "json") {
823
+ const db = new Database2(dbPath());
824
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '%_fts%' AND name != 'schema_version'").all();
825
+ const data = {};
826
+ for (const { name } of tables) {
827
+ data[name] = db.prepare(`SELECT * FROM "${name}"`).all();
828
+ }
829
+ writeFileSync5(outputPath, JSON.stringify(data, null, 2));
830
+ db.close();
831
+ console.log(`\u2713 Data exported to ${outputPath} (${tables.length} tables)`);
832
+ } else {
833
+ console.error(`Unknown format: ${opts.format}. Use 'sqlite' or 'json'.`);
834
+ }
835
+ });
836
+ }
837
+
838
+ // src/commands/connect.ts
839
+ function connectCommand(program2) {
840
+ program2.command("connect").description("Connect a data source").argument("<source>", "Data source: feishu, github").option("--app-id <id>", "App ID (for Feishu)").option("--app-secret <secret>", "App Secret (for Feishu)").option("--token <token>", "Access token (for GitHub)").action(async (source, opts) => {
841
+ switch (source) {
842
+ case "feishu":
843
+ await connectFeishu(opts);
844
+ break;
845
+ case "github":
846
+ await connectGitHub(opts);
847
+ break;
848
+ default:
849
+ console.error(`Unknown source: ${source}. Supported: feishu, github`);
850
+ process.exit(1);
851
+ }
852
+ });
853
+ }
854
+ async function connectFeishu(opts) {
855
+ let appId = opts.appId;
856
+ let appSecret = opts.appSecret;
857
+ if (!appId) appId = process.env["FEISHU_APP_ID"];
858
+ if (!appSecret) appSecret = process.env["FEISHU_APP_SECRET"];
859
+ if (!appId || !appSecret) {
860
+ const { default: inquirer } = await import("inquirer");
861
+ const answers = await inquirer.prompt([
862
+ { type: "input", name: "appId", message: "Feishu App ID:", when: !appId },
863
+ { type: "password", name: "appSecret", message: "Feishu App Secret:", when: !appSecret }
864
+ ]);
865
+ appId = appId ?? answers.appId;
866
+ appSecret = appSecret ?? answers.appSecret;
867
+ }
868
+ if (!appId || !appSecret) {
869
+ console.error("Error: App ID and App Secret are required.");
870
+ process.exit(1);
871
+ }
872
+ console.log("Verifying Feishu credentials...");
873
+ try {
874
+ const res = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
875
+ method: "POST",
876
+ headers: { "Content-Type": "application/json" },
877
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret })
878
+ });
879
+ const data = await res.json();
880
+ if (data.code !== 0) {
881
+ console.error(`Feishu auth failed: ${data.msg}`);
882
+ process.exit(1);
883
+ }
884
+ console.log("\u2713 Feishu credentials verified");
885
+ } catch (err) {
886
+ console.error(`Network error: ${err}`);
887
+ process.exit(1);
888
+ }
889
+ saveCredential("feishu", {
890
+ type: "feishu",
891
+ data: { appId, appSecret },
892
+ createdAt: Date.now(),
893
+ updatedAt: Date.now()
894
+ });
895
+ console.log("\u2713 Feishu connected. Run `jowork sync` to start syncing data.");
896
+ }
897
+ async function connectGitHub(opts) {
898
+ let token = opts.token;
899
+ if (!token) token = process.env["GITHUB_PERSONAL_ACCESS_TOKEN"];
900
+ if (!token) {
901
+ const { default: inquirer } = await import("inquirer");
902
+ const answers = await inquirer.prompt([
903
+ { type: "password", name: "token", message: "GitHub Personal Access Token:" }
904
+ ]);
905
+ token = answers.token;
906
+ }
907
+ if (!token) {
908
+ console.error("Error: GitHub token is required.");
909
+ process.exit(1);
910
+ }
911
+ saveCredential("github", {
912
+ type: "github",
913
+ data: { token },
914
+ createdAt: Date.now(),
915
+ updatedAt: Date.now()
916
+ });
917
+ console.log("\u2713 GitHub connected.");
918
+ }
919
+
920
+ // src/commands/sync.ts
921
+ import { existsSync as existsSync9 } from "fs";
922
+ function syncCommand(program2) {
923
+ program2.command("sync").description("Sync data from connected sources").option("--source <source>", "Sync specific source only").action(async (opts) => {
924
+ if (!existsSync9(dbPath())) {
925
+ console.error("Error: JoWork not initialized. Run `jowork init` first.");
926
+ process.exit(1);
927
+ }
928
+ const db = new DbManager(dbPath());
929
+ db.ensureTables();
930
+ const sources = opts.source ? [opts.source] : listCredentials();
931
+ if (sources.length === 0) {
932
+ console.log("No data sources connected. Run `jowork connect <source>` first.");
933
+ db.close();
934
+ return;
935
+ }
936
+ for (const source of sources) {
937
+ const cred = loadCredential(source);
938
+ if (!cred) {
939
+ console.log(`\u2298 ${source}: no credentials found, skipping`);
940
+ continue;
941
+ }
942
+ console.log(`Syncing ${source}...`);
943
+ try {
944
+ switch (source) {
945
+ case "feishu":
946
+ await syncFeishu(db, cred.data);
947
+ break;
948
+ case "github":
949
+ console.log(` GitHub sync not yet implemented (Phase 4)`);
950
+ break;
951
+ default:
952
+ console.log(` Unknown source: ${source}`);
953
+ }
954
+ } catch (err) {
955
+ logError("sync", `Failed to sync ${source}`, { error: String(err) });
956
+ console.error(` \u2717 ${source} sync failed: ${err}`);
957
+ }
958
+ }
959
+ console.log("Running entity extraction...");
960
+ const { processed, linksCreated } = linkAllUnprocessed(db.getSqlite());
961
+ console.log(` \u2713 Extracted ${linksCreated} links from ${processed} objects`);
962
+ db.close();
963
+ });
964
+ }
965
+ async function syncFeishu(db, data) {
966
+ const { appId, appSecret } = data;
967
+ if (!appId || !appSecret) throw new Error("Missing Feishu credentials");
968
+ const tokenRes = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
969
+ method: "POST",
970
+ headers: { "Content-Type": "application/json" },
971
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret })
972
+ });
973
+ const tokenData = await tokenRes.json();
974
+ if (tokenData.code !== 0) throw new Error(`Auth failed: code ${tokenData.code}`);
975
+ const token = tokenData.tenant_access_token;
976
+ const chatsRes = await fetch("https://open.feishu.cn/open-apis/im/v1/chats?page_size=50", {
977
+ headers: { Authorization: `Bearer ${token}` }
978
+ });
979
+ const chatsData = await chatsRes.json();
980
+ if (chatsData.code !== 0) throw new Error(`Failed to list chats: code ${chatsData.code}`);
981
+ const chats = chatsData.data?.items ?? [];
982
+ console.log(` Found ${chats.length} chats`);
983
+ const sqlite = db.getSqlite();
984
+ let totalMessages = 0;
985
+ let newMessages = 0;
986
+ for (const chat of chats) {
987
+ const cursorRow = sqlite.prepare(`SELECT cursor FROM sync_cursors WHERE connector_id = ?`).get(`feishu:${chat.chat_id}`);
988
+ let pageToken = cursorRow?.cursor ?? void 0;
989
+ let hasMore = true;
990
+ while (hasMore) {
991
+ const url = new URL("https://open.feishu.cn/open-apis/im/v1/messages");
992
+ url.searchParams.set("container_id_type", "chat");
993
+ url.searchParams.set("container_id", chat.chat_id);
994
+ url.searchParams.set("page_size", "50");
995
+ url.searchParams.set("sort_type", "ByCreateTimeAsc");
996
+ if (pageToken) url.searchParams.set("page_token", pageToken);
997
+ const msgRes = await fetch(url.toString(), {
998
+ headers: { Authorization: `Bearer ${token}` }
999
+ });
1000
+ const msgData = await msgRes.json();
1001
+ if (msgData.code !== 0) {
1002
+ if (msgData.code === 99991400) {
1003
+ console.log(` Rate limited on ${chat.name}, waiting 5s...`);
1004
+ await new Promise((r) => setTimeout(r, 5e3));
1005
+ continue;
1006
+ }
1007
+ console.log(` \u26A0 Failed to get messages from "${chat.name}": code ${msgData.code}`);
1008
+ break;
1009
+ }
1010
+ const messages = msgData.data?.items ?? [];
1011
+ const insertObj = sqlite.prepare(`
1012
+ INSERT OR IGNORE INTO objects (id, source, source_type, uri, title, summary, tags, content_hash, last_synced_at, created_at)
1013
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1014
+ `);
1015
+ const insertBody = sqlite.prepare(`
1016
+ INSERT OR IGNORE INTO object_bodies (object_id, content, content_type, fetched_at)
1017
+ VALUES (?, ?, ?, ?)
1018
+ `);
1019
+ const batchInsert = sqlite.transaction((msgs) => {
1020
+ for (const msg of msgs) {
1021
+ if (msg.msg_type !== "text" && msg.msg_type !== "post") continue;
1022
+ let content = "";
1023
+ try {
1024
+ const bodyContent = JSON.parse(msg.body?.content ?? "{}");
1025
+ const raw = bodyContent.text ?? bodyContent.content ?? bodyContent;
1026
+ content = typeof raw === "string" ? raw : JSON.stringify(raw);
1027
+ } catch {
1028
+ content = msg.body?.content ?? "";
1029
+ }
1030
+ if (!content || typeof content !== "string") continue;
1031
+ const uri = `feishu://message/${msg.message_id}`;
1032
+ const hash = simpleHash2(content);
1033
+ const now = Date.now();
1034
+ const id = createId("obj");
1035
+ const createTime = msg.create_time ? parseInt(msg.create_time) : now;
1036
+ const summary = content.length > 200 ? content.slice(0, 200) + "..." : content;
1037
+ insertObj.run(id, "feishu", "message", uri, `${chat.name}`, summary, JSON.stringify(["feishu", "message"]), hash, now, createTime);
1038
+ insertBody.run(id, content, "text/plain", now);
1039
+ newMessages++;
1040
+ }
1041
+ });
1042
+ for (let i = 0; i < messages.length; i += 100) {
1043
+ batchInsert(messages.slice(i, i + 100));
1044
+ }
1045
+ totalMessages += messages.length;
1046
+ hasMore = msgData.data.has_more;
1047
+ pageToken = msgData.data.page_token;
1048
+ if (pageToken) {
1049
+ sqlite.prepare(`INSERT OR REPLACE INTO sync_cursors (connector_id, cursor, last_synced_at) VALUES (?, ?, ?)`).run(`feishu:${chat.chat_id}`, pageToken, Date.now());
1050
+ }
1051
+ }
1052
+ }
1053
+ try {
1054
+ sqlite.exec(`INSERT INTO objects_fts(objects_fts) VALUES('rebuild')`);
1055
+ } catch {
1056
+ }
1057
+ console.log(` \u2713 Synced ${totalMessages} messages (${newMessages} new)`);
1058
+ logInfo("sync", "Feishu sync complete", { totalMessages, newMessages, chats: chats.length });
1059
+ }
1060
+ function simpleHash2(str) {
1061
+ let hash = 0;
1062
+ for (let i = 0; i < str.length; i++) {
1063
+ const char = str.charCodeAt(i);
1064
+ hash = (hash << 5) - hash + char;
1065
+ hash = hash & hash;
1066
+ }
1067
+ return hash.toString(36);
1068
+ }
1069
+
1070
+ // src/commands/search.ts
1071
+ import { existsSync as existsSync10 } from "fs";
1072
+ function searchCommand(program2) {
1073
+ program2.command("search").description("Search across all synced data").argument("<query>", "Search keywords").option("--source <source>", "Filter by source").option("--limit <n>", "Max results", "20").action(async (query, opts) => {
1074
+ if (!existsSync10(dbPath())) {
1075
+ console.error("Error: JoWork not initialized. Run `jowork init` first.");
1076
+ process.exit(1);
1077
+ }
1078
+ const db = new DbManager(dbPath());
1079
+ const sqlite = db.getSqlite();
1080
+ const limit = parseInt(opts.limit) || 20;
1081
+ const pattern = `%${query.replace(/[%_\\]/g, "\\$&")}%`;
1082
+ let rows;
1083
+ if (opts.source) {
1084
+ rows = sqlite.prepare(`
1085
+ SELECT id, title, summary, source, source_type, uri FROM objects
1086
+ WHERE (title LIKE ? OR summary LIKE ?) AND source = ?
1087
+ ORDER BY last_synced_at DESC LIMIT ?
1088
+ `).all(pattern, pattern, opts.source, limit);
1089
+ } else {
1090
+ rows = sqlite.prepare(`
1091
+ SELECT id, title, summary, source, source_type, uri FROM objects
1092
+ WHERE title LIKE ? OR summary LIKE ?
1093
+ ORDER BY last_synced_at DESC LIMIT ?
1094
+ `).all(pattern, pattern, limit);
1095
+ }
1096
+ if (rows.length === 0) {
1097
+ console.log(`No results for "${query}"`);
1098
+ } else {
1099
+ for (const row of rows) {
1100
+ console.log(`[${row.source}/${row.source_type}] ${row.title}`);
1101
+ if (row.summary) console.log(` ${row.summary.slice(0, 100)}`);
1102
+ console.log("");
1103
+ }
1104
+ console.log(`${rows.length} results`);
1105
+ }
1106
+ db.close();
1107
+ });
1108
+ }
1109
+
1110
+ // src/goals/manager.ts
1111
+ var GoalManager = class {
1112
+ constructor(sqlite) {
1113
+ this.sqlite = sqlite;
1114
+ }
1115
+ // ── Goals ──
1116
+ createGoal(opts) {
1117
+ const now = Date.now();
1118
+ const id = createId("goal");
1119
+ this.sqlite.prepare(`
1120
+ INSERT INTO goals (id, title, description, status, autonomy_level, parent_id, created_at, updated_at)
1121
+ VALUES (?, ?, ?, 'active', ?, ?, ?, ?)
1122
+ `).run(id, opts.title, opts.description ?? null, opts.autonomyLevel ?? "copilot", opts.parentId ?? null, now, now);
1123
+ logInfo("goals", `Goal created: "${opts.title}"`, { id });
1124
+ return this.getGoal(id);
1125
+ }
1126
+ getGoal(id) {
1127
+ const row = this.sqlite.prepare("SELECT * FROM goals WHERE id = ?").get(id);
1128
+ if (!row) return null;
1129
+ const goal = this.rowToGoal(row);
1130
+ goal.signals = this.getSignalsForGoal(id);
1131
+ return goal;
1132
+ }
1133
+ listGoals(opts = {}) {
1134
+ let query = "SELECT * FROM goals";
1135
+ const args = [];
1136
+ if (opts.status) {
1137
+ query += " WHERE status = ?";
1138
+ args.push(opts.status);
1139
+ }
1140
+ query += " ORDER BY created_at DESC";
1141
+ const rows = this.sqlite.prepare(query).all(...args);
1142
+ return rows.map((r) => {
1143
+ const goal = this.rowToGoal(r);
1144
+ goal.signals = this.getSignalsForGoal(goal.id);
1145
+ return goal;
1146
+ });
1147
+ }
1148
+ updateGoal(id, patch) {
1149
+ const now = Date.now();
1150
+ const sets = ["updated_at = ?"];
1151
+ const args = [now];
1152
+ if (patch.title !== void 0) {
1153
+ sets.push("title = ?");
1154
+ args.push(patch.title);
1155
+ }
1156
+ if (patch.description !== void 0) {
1157
+ sets.push("description = ?");
1158
+ args.push(patch.description);
1159
+ }
1160
+ if (patch.status !== void 0) {
1161
+ sets.push("status = ?");
1162
+ args.push(patch.status);
1163
+ }
1164
+ if (patch.autonomyLevel !== void 0) {
1165
+ sets.push("autonomy_level = ?");
1166
+ args.push(patch.autonomyLevel);
1167
+ }
1168
+ args.push(id);
1169
+ this.sqlite.prepare(`UPDATE goals SET ${sets.join(", ")} WHERE id = ?`).run(...args);
1170
+ return this.getGoal(id);
1171
+ }
1172
+ // ── Signals ──
1173
+ createSignal(opts) {
1174
+ const now = Date.now();
1175
+ const id = createId("sig");
1176
+ this.sqlite.prepare(`
1177
+ INSERT INTO signals (id, goal_id, title, source, metric, direction, poll_interval, config, created_at, updated_at)
1178
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1179
+ `).run(id, opts.goalId, opts.title, opts.source, opts.metric, opts.direction, opts.pollInterval ?? 3600, opts.config ? JSON.stringify(opts.config) : null, now, now);
1180
+ logInfo("goals", `Signal created: "${opts.title}" for goal ${opts.goalId}`, { id });
1181
+ return this.getSignal(id);
1182
+ }
1183
+ getSignal(id) {
1184
+ const row = this.sqlite.prepare("SELECT * FROM signals WHERE id = ?").get(id);
1185
+ if (!row) return null;
1186
+ const signal = this.rowToSignal(row);
1187
+ signal.measures = this.getMeasuresForSignal(id);
1188
+ return signal;
1189
+ }
1190
+ getSignalsForGoal(goalId) {
1191
+ const rows = this.sqlite.prepare("SELECT * FROM signals WHERE goal_id = ? ORDER BY created_at").all(goalId);
1192
+ return rows.map((r) => {
1193
+ const sig = this.rowToSignal(r);
1194
+ sig.measures = this.getMeasuresForSignal(sig.id);
1195
+ return sig;
1196
+ });
1197
+ }
1198
+ updateSignalValue(id, value) {
1199
+ const now = Date.now();
1200
+ this.sqlite.prepare("UPDATE signals SET current_value = ?, last_polled_at = ?, updated_at = ? WHERE id = ?").run(value, now, now, id);
1201
+ const measures = this.getMeasuresForSignal(id);
1202
+ for (const m of measures) {
1203
+ const met = this.evaluateMeasure(m, value);
1204
+ this.sqlite.prepare("UPDATE measures SET current = ?, met = ?, last_evaluated_at = ?, updated_at = ? WHERE id = ?").run(value, met ? 1 : 0, now, now, m.id);
1205
+ }
1206
+ }
1207
+ // ── Measures ──
1208
+ createMeasure(opts) {
1209
+ const now = Date.now();
1210
+ const id = createId("msr");
1211
+ this.sqlite.prepare(`
1212
+ INSERT INTO measures (id, signal_id, threshold, comparison, upper_bound, created_at, updated_at)
1213
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1214
+ `).run(id, opts.signalId, opts.threshold, opts.comparison, opts.upperBound ?? null, now, now);
1215
+ return this.getMeasure(id);
1216
+ }
1217
+ getMeasure(id) {
1218
+ const row = this.sqlite.prepare("SELECT * FROM measures WHERE id = ?").get(id);
1219
+ return row ? this.rowToMeasure(row) : null;
1220
+ }
1221
+ getMeasuresForSignal(signalId) {
1222
+ const rows = this.sqlite.prepare("SELECT * FROM measures WHERE signal_id = ? ORDER BY created_at").all(signalId);
1223
+ return rows.map((r) => this.rowToMeasure(r));
1224
+ }
1225
+ // ── Helpers ──
1226
+ evaluateMeasure(measure, value) {
1227
+ switch (measure.comparison) {
1228
+ case "gte":
1229
+ return value >= measure.threshold;
1230
+ case "lte":
1231
+ return value <= measure.threshold;
1232
+ case "gt":
1233
+ return value > measure.threshold;
1234
+ case "lt":
1235
+ return value < measure.threshold;
1236
+ case "eq":
1237
+ return value === measure.threshold;
1238
+ case "between":
1239
+ return value >= measure.threshold && value <= (measure.upperBound ?? Infinity);
1240
+ default:
1241
+ return false;
1242
+ }
1243
+ }
1244
+ rowToGoal(row) {
1245
+ return {
1246
+ id: row.id,
1247
+ title: row.title,
1248
+ description: row.description,
1249
+ status: row.status,
1250
+ autonomyLevel: row.autonomy_level,
1251
+ parentId: row.parent_id,
1252
+ evolvedFrom: row.evolved_from,
1253
+ createdAt: row.created_at,
1254
+ updatedAt: row.updated_at
1255
+ };
1256
+ }
1257
+ rowToSignal(row) {
1258
+ return {
1259
+ id: row.id,
1260
+ goalId: row.goal_id,
1261
+ title: row.title,
1262
+ source: row.source,
1263
+ metric: row.metric,
1264
+ direction: row.direction,
1265
+ pollInterval: row.poll_interval,
1266
+ config: row.config ? JSON.parse(row.config) : null,
1267
+ currentValue: row.current_value,
1268
+ lastPolledAt: row.last_polled_at,
1269
+ createdAt: row.created_at,
1270
+ updatedAt: row.updated_at
1271
+ };
1272
+ }
1273
+ rowToMeasure(row) {
1274
+ return {
1275
+ id: row.id,
1276
+ signalId: row.signal_id,
1277
+ threshold: row.threshold,
1278
+ comparison: row.comparison,
1279
+ upperBound: row.upper_bound,
1280
+ current: row.current,
1281
+ met: row.met === 1,
1282
+ lastEvaluatedAt: row.last_evaluated_at,
1283
+ createdAt: row.created_at,
1284
+ updatedAt: row.updated_at
1285
+ };
1286
+ }
1287
+ };
1288
+
1289
+ // src/commands/goal.ts
1290
+ function goalCommand(program2) {
1291
+ const goal = program2.command("goal").description("Manage goals (Goal-Signal-Measure system)");
1292
+ goal.command("add").description("Add a new goal").argument("<title>", "Goal title").option("--description <desc>", "Goal description").option("--parent <id>", "Parent goal ID").action(async (title, opts) => {
1293
+ const db = new DbManager(dbPath());
1294
+ db.ensureTables();
1295
+ const gm = new GoalManager(db.getSqlite());
1296
+ const g = gm.createGoal({ title, description: opts.description, parentId: opts.parent });
1297
+ console.log(`\u2713 Goal created: "${g.title}" (${g.id})`);
1298
+ db.close();
1299
+ });
1300
+ goal.command("list").description("List goals").option("--status <status>", "Filter by status: active, paused, completed").action(async (opts) => {
1301
+ const db = new DbManager(dbPath());
1302
+ db.ensureTables();
1303
+ const gm = new GoalManager(db.getSqlite());
1304
+ const goals = gm.listGoals({ status: opts.status });
1305
+ if (goals.length === 0) {
1306
+ console.log("No goals found.");
1307
+ db.close();
1308
+ return;
1309
+ }
1310
+ for (const g of goals) {
1311
+ const signalCount = g.signals?.length ?? 0;
1312
+ const metCount = g.signals?.reduce((acc, s) => acc + (s.measures?.filter((m) => m.met).length ?? 0), 0) ?? 0;
1313
+ const totalMeasures = g.signals?.reduce((acc, s) => acc + (s.measures?.length ?? 0), 0) ?? 0;
1314
+ const progress = totalMeasures > 0 ? `${metCount}/${totalMeasures} measures met` : "no measures";
1315
+ console.log(`[${g.status}] ${g.title} (${g.id})`);
1316
+ console.log(` ${signalCount} signals, ${progress}`);
1317
+ if (g.description) console.log(` ${g.description}`);
1318
+ console.log("");
1319
+ }
1320
+ db.close();
1321
+ });
1322
+ goal.command("status").description("Show detailed goal status").argument("[id]", "Goal ID (shows all if omitted)").action(async (id) => {
1323
+ const db = new DbManager(dbPath());
1324
+ db.ensureTables();
1325
+ const gm = new GoalManager(db.getSqlite());
1326
+ const goals = id ? [gm.getGoal(id)].filter(Boolean) : gm.listGoals({ status: "active" });
1327
+ for (const g of goals) {
1328
+ console.log(`Goal: ${g.title} [${g.status}]`);
1329
+ console.log(` ID: ${g.id}`);
1330
+ console.log(` Autonomy: ${g.autonomyLevel}`);
1331
+ if (g.signals && g.signals.length > 0) {
1332
+ for (const s of g.signals) {
1333
+ const arrow = s.direction === "maximize" ? "\u2191" : s.direction === "minimize" ? "\u2193" : "\u2192";
1334
+ console.log(` Signal: ${s.title} ${arrow} (${s.source}/${s.metric})`);
1335
+ console.log(` Current: ${s.currentValue ?? "no data"}, Poll: ${s.pollInterval}s`);
1336
+ if (s.measures) {
1337
+ for (const m of s.measures) {
1338
+ const icon = m.met ? "\u2713" : "\u2717";
1339
+ console.log(` ${icon} ${m.comparison} ${m.threshold}${m.upperBound ? `-${m.upperBound}` : ""} (current: ${m.current ?? "N/A"})`);
1340
+ }
1341
+ }
1342
+ }
1343
+ } else {
1344
+ console.log(" No signals configured");
1345
+ }
1346
+ console.log("");
1347
+ }
1348
+ db.close();
1349
+ });
1350
+ const signal = program2.command("signal").description("Manage signals for goals");
1351
+ signal.command("add").description("Add a signal to a goal").argument("<goal_id>", "Goal ID").requiredOption("--source <source>", "Data source (e.g. posthog, feishu)").requiredOption("--metric <metric>", "Metric name (e.g. dau, crash_rate)").requiredOption("--direction <dir>", "Direction: maximize, minimize, maintain").option("--title <title>", "Signal title (auto-generated if omitted)").option("--interval <seconds>", "Poll interval in seconds", "3600").action(async (goalId, opts) => {
1352
+ const db = new DbManager(dbPath());
1353
+ db.ensureTables();
1354
+ const gm = new GoalManager(db.getSqlite());
1355
+ const goal2 = gm.getGoal(goalId);
1356
+ if (!goal2) {
1357
+ console.error(`Goal not found: ${goalId}`);
1358
+ process.exit(1);
1359
+ }
1360
+ const title = opts.title ?? `${opts.metric} (${opts.source})`;
1361
+ const sig = gm.createSignal({
1362
+ goalId,
1363
+ title,
1364
+ source: opts.source,
1365
+ metric: opts.metric,
1366
+ direction: opts.direction,
1367
+ pollInterval: parseInt(opts.interval)
1368
+ });
1369
+ console.log(`\u2713 Signal added: "${sig.title}" \u2192 ${goal2.title} (${sig.id})`);
1370
+ db.close();
1371
+ });
1372
+ const measure = program2.command("measure").description("Manage measures for signals");
1373
+ measure.command("add").description("Add a measure to a signal").argument("<signal_id>", "Signal ID").requiredOption("--threshold <n>", "Threshold value").requiredOption("--type <type>", "Comparison: gte, lte, gt, lt, eq, between").option("--upper <n>", "Upper bound (for between)").action(async (signalId, opts) => {
1374
+ const db = new DbManager(dbPath());
1375
+ db.ensureTables();
1376
+ const gm = new GoalManager(db.getSqlite());
1377
+ const sig = gm.getSignal(signalId);
1378
+ if (!sig) {
1379
+ console.error(`Signal not found: ${signalId}`);
1380
+ process.exit(1);
1381
+ }
1382
+ const m = gm.createMeasure({
1383
+ signalId,
1384
+ threshold: parseFloat(opts.threshold),
1385
+ comparison: opts.type,
1386
+ upperBound: opts.upper ? parseFloat(opts.upper) : void 0
1387
+ });
1388
+ console.log(`\u2713 Measure added: ${opts.type} ${opts.threshold} (${m.id})`);
1389
+ db.close();
1390
+ });
1391
+ }
1392
+
1393
+ // src/cli.ts
1394
+ process.env["I18NEXT_DISABLE_BANNER"] = "1";
1395
+ var program = new Command();
1396
+ program.name("jowork").description("AI Agent Infrastructure \u2014 let AI agents truly understand your work").version("0.1.0");
1397
+ initCommand(program);
1398
+ serveCommand(program);
1399
+ registerCommand(program);
1400
+ connectCommand(program);
1401
+ syncCommand(program);
1402
+ statusCommand(program);
1403
+ doctorCommand(program);
1404
+ exportCommand(program);
1405
+ searchCommand(program);
1406
+ goalCommand(program);
1407
+ program.parse();