@bodhi-ventures/aiocs 0.5.3 → 0.6.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/README.md +21 -3
- package/dist/{chunk-M767TPUX.js → chunk-2UWV3O7E.js} +676 -1
- package/dist/cli.js +206 -2
- package/dist/mcp-server.js +281 -1
- package/docs/json-contract.md +196 -0
- package/package.json +1 -1
- package/skills/aiocs/SKILL.md +37 -6
- package/skills/aiocs-curation/SKILL.md +24 -3
|
@@ -7,8 +7,10 @@ var AIOCS_ERROR_CODES = {
|
|
|
7
7
|
noPagesFetched: "NO_PAGES_FETCHED",
|
|
8
8
|
noProjectScope: "NO_PROJECT_SCOPE",
|
|
9
9
|
chunkNotFound: "CHUNK_NOT_FOUND",
|
|
10
|
+
pageNotFound: "PAGE_NOT_FOUND",
|
|
10
11
|
referenceFileNotFound: "REFERENCE_FILE_NOT_FOUND",
|
|
11
12
|
invalidReferenceFile: "INVALID_REFERENCE_FILE",
|
|
13
|
+
sourceContextInvalid: "SOURCE_CONTEXT_INVALID",
|
|
12
14
|
authEnvMissing: "AUTH_ENV_MISSING",
|
|
13
15
|
canaryFailed: "CANARY_FAILED",
|
|
14
16
|
backupConflict: "BACKUP_CONFLICT",
|
|
@@ -818,6 +820,35 @@ function initSchema(db) {
|
|
|
818
820
|
PRIMARY KEY(project_path, source_id)
|
|
819
821
|
);
|
|
820
822
|
|
|
823
|
+
CREATE TABLE IF NOT EXISTS source_context (
|
|
824
|
+
source_id TEXT PRIMARY KEY REFERENCES sources(id) ON DELETE CASCADE,
|
|
825
|
+
context_json TEXT NOT NULL,
|
|
826
|
+
created_at TEXT NOT NULL,
|
|
827
|
+
updated_at TEXT NOT NULL
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
CREATE TABLE IF NOT EXISTS routing_learnings (
|
|
831
|
+
id TEXT PRIMARY KEY,
|
|
832
|
+
route_key TEXT NOT NULL UNIQUE,
|
|
833
|
+
source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
834
|
+
snapshot_id TEXT REFERENCES snapshots(id) ON DELETE SET NULL,
|
|
835
|
+
learning_type TEXT NOT NULL CHECK(learning_type IN ('discovery', 'negative')),
|
|
836
|
+
intent TEXT NOT NULL,
|
|
837
|
+
page_url TEXT,
|
|
838
|
+
file_path TEXT,
|
|
839
|
+
title TEXT,
|
|
840
|
+
note TEXT,
|
|
841
|
+
search_terms_json TEXT NOT NULL,
|
|
842
|
+
created_at TEXT NOT NULL,
|
|
843
|
+
updated_at TEXT NOT NULL
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
CREATE INDEX IF NOT EXISTS idx_routing_learnings_source_type
|
|
847
|
+
ON routing_learnings(source_id, learning_type, updated_at);
|
|
848
|
+
|
|
849
|
+
CREATE INDEX IF NOT EXISTS idx_routing_learnings_intent
|
|
850
|
+
ON routing_learnings(intent, updated_at);
|
|
851
|
+
|
|
821
852
|
CREATE TABLE IF NOT EXISTS daemon_state (
|
|
822
853
|
singleton_id INTEGER PRIMARY KEY CHECK(singleton_id = 1),
|
|
823
854
|
last_started_at TEXT,
|
|
@@ -926,6 +957,15 @@ function normalizeQuery(query) {
|
|
|
926
957
|
const words = query.replace(/[^\p{L}\p{N}]+/gu, " ").split(/\s+/).map((part) => part.trim()).filter(Boolean);
|
|
927
958
|
return words.join(" ");
|
|
928
959
|
}
|
|
960
|
+
function buildRouteKey(input) {
|
|
961
|
+
return sha256(stableStringify({
|
|
962
|
+
sourceId: input.sourceId,
|
|
963
|
+
learningType: input.learningType,
|
|
964
|
+
intent: normalizeQuery(input.intent).toLowerCase(),
|
|
965
|
+
pageUrl: input.pageUrl ?? null,
|
|
966
|
+
filePath: input.filePath ?? null
|
|
967
|
+
}));
|
|
968
|
+
}
|
|
929
969
|
function normalizePatternFilters(patterns) {
|
|
930
970
|
if (!patterns || patterns.length === 0) {
|
|
931
971
|
return null;
|
|
@@ -1297,6 +1337,62 @@ function openCatalog(options) {
|
|
|
1297
1337
|
}
|
|
1298
1338
|
return JSON.parse(row.spec_json);
|
|
1299
1339
|
},
|
|
1340
|
+
getSourceById(sourceId) {
|
|
1341
|
+
return this.listSources().find((source) => source.id === sourceId) ?? null;
|
|
1342
|
+
},
|
|
1343
|
+
upsertSourceContext(sourceId, context) {
|
|
1344
|
+
const source = this.getSourceById(sourceId);
|
|
1345
|
+
if (!source) {
|
|
1346
|
+
throw new AiocsError(
|
|
1347
|
+
AIOCS_ERROR_CODES.sourceNotFound,
|
|
1348
|
+
`Unknown source '${sourceId}'`
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
const timestamp = nowIso();
|
|
1352
|
+
const existing = db.prepare(`
|
|
1353
|
+
SELECT created_at
|
|
1354
|
+
FROM source_context
|
|
1355
|
+
WHERE source_id = ?
|
|
1356
|
+
`).get(sourceId);
|
|
1357
|
+
db.prepare(`
|
|
1358
|
+
INSERT INTO source_context (source_id, context_json, created_at, updated_at)
|
|
1359
|
+
VALUES (?, ?, ?, ?)
|
|
1360
|
+
ON CONFLICT(source_id) DO UPDATE SET
|
|
1361
|
+
context_json = excluded.context_json,
|
|
1362
|
+
updated_at = excluded.updated_at
|
|
1363
|
+
`).run(
|
|
1364
|
+
sourceId,
|
|
1365
|
+
JSON.stringify(context),
|
|
1366
|
+
existing?.created_at ?? timestamp,
|
|
1367
|
+
timestamp
|
|
1368
|
+
);
|
|
1369
|
+
return {
|
|
1370
|
+
sourceId,
|
|
1371
|
+
context,
|
|
1372
|
+
createdAt: existing?.created_at ?? timestamp,
|
|
1373
|
+
updatedAt: timestamp
|
|
1374
|
+
};
|
|
1375
|
+
},
|
|
1376
|
+
getSourceContext(sourceId) {
|
|
1377
|
+
const source = this.getSourceById(sourceId);
|
|
1378
|
+
if (!source) {
|
|
1379
|
+
throw new AiocsError(
|
|
1380
|
+
AIOCS_ERROR_CODES.sourceNotFound,
|
|
1381
|
+
`Unknown source '${sourceId}'`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
const row = db.prepare(`
|
|
1385
|
+
SELECT context_json, created_at, updated_at
|
|
1386
|
+
FROM source_context
|
|
1387
|
+
WHERE source_id = ?
|
|
1388
|
+
`).get(sourceId);
|
|
1389
|
+
return {
|
|
1390
|
+
sourceId,
|
|
1391
|
+
context: row ? JSON.parse(row.context_json) : null,
|
|
1392
|
+
createdAt: row?.created_at ?? null,
|
|
1393
|
+
updatedAt: row?.updated_at ?? null
|
|
1394
|
+
};
|
|
1395
|
+
},
|
|
1300
1396
|
listSources() {
|
|
1301
1397
|
const rows = db.prepare(`
|
|
1302
1398
|
SELECT
|
|
@@ -1335,6 +1431,280 @@ function openCatalog(options) {
|
|
|
1335
1431
|
};
|
|
1336
1432
|
});
|
|
1337
1433
|
},
|
|
1434
|
+
listPages(input) {
|
|
1435
|
+
const source = this.getSourceById(input.sourceId);
|
|
1436
|
+
if (!source) {
|
|
1437
|
+
throw new AiocsError(
|
|
1438
|
+
AIOCS_ERROR_CODES.sourceNotFound,
|
|
1439
|
+
`Unknown source '${input.sourceId}'`
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
const snapshotId = input.snapshotId ?? source.lastSuccessfulSnapshotId;
|
|
1443
|
+
if (!snapshotId) {
|
|
1444
|
+
throw new AiocsError(
|
|
1445
|
+
AIOCS_ERROR_CODES.snapshotNotFound,
|
|
1446
|
+
`No successful snapshot found for source '${input.sourceId}'`
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
const snapshotRow = db.prepare(`
|
|
1450
|
+
SELECT id
|
|
1451
|
+
FROM snapshots
|
|
1452
|
+
WHERE id = ?
|
|
1453
|
+
AND source_id = ?
|
|
1454
|
+
`).get(snapshotId, input.sourceId);
|
|
1455
|
+
if (!snapshotRow) {
|
|
1456
|
+
throw new AiocsError(
|
|
1457
|
+
AIOCS_ERROR_CODES.snapshotNotFound,
|
|
1458
|
+
`Snapshot '${snapshotId}' not found for source '${input.sourceId}'`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
const normalizedQuery = input.query ? normalizeQuery(input.query).toLowerCase() : "";
|
|
1462
|
+
const pathPatterns = normalizePatternFilters(input.pathPatterns);
|
|
1463
|
+
const limit = assertPaginationValue(input.limit, "limit", 50);
|
|
1464
|
+
const offset = assertPaginationValue(input.offset, "offset", 0);
|
|
1465
|
+
const whereSql = [
|
|
1466
|
+
"snapshot_id = ?"
|
|
1467
|
+
];
|
|
1468
|
+
const args = [snapshotId];
|
|
1469
|
+
if (normalizedQuery) {
|
|
1470
|
+
whereSql.push("(LOWER(title) LIKE ? OR LOWER(url) LIKE ? OR LOWER(COALESCE(file_path, '')) LIKE ?)");
|
|
1471
|
+
const queryLike = `%${normalizedQuery}%`;
|
|
1472
|
+
args.push(queryLike, queryLike, queryLike);
|
|
1473
|
+
}
|
|
1474
|
+
if (pathPatterns && pathPatterns.length > 0) {
|
|
1475
|
+
whereSql.push(`file_path IS NOT NULL AND (${pathPatterns.map(() => "file_path GLOB ?").join(" OR ")})`);
|
|
1476
|
+
args.push(...pathPatterns.map((pattern) => toSqliteGlob(pattern)));
|
|
1477
|
+
}
|
|
1478
|
+
const whereClause = whereSql.join(" AND ");
|
|
1479
|
+
const totalRow = db.prepare(`
|
|
1480
|
+
SELECT COUNT(*) AS total
|
|
1481
|
+
FROM pages
|
|
1482
|
+
WHERE ${whereClause}
|
|
1483
|
+
`).get(...args);
|
|
1484
|
+
const rows = db.prepare(`
|
|
1485
|
+
SELECT url, title, markdown, page_kind, file_path, language
|
|
1486
|
+
FROM pages
|
|
1487
|
+
WHERE ${whereClause}
|
|
1488
|
+
ORDER BY title, url
|
|
1489
|
+
LIMIT ?
|
|
1490
|
+
OFFSET ?
|
|
1491
|
+
`).all(...args, limit, offset);
|
|
1492
|
+
return {
|
|
1493
|
+
sourceId: input.sourceId,
|
|
1494
|
+
snapshotId,
|
|
1495
|
+
total: totalRow.total,
|
|
1496
|
+
limit,
|
|
1497
|
+
offset,
|
|
1498
|
+
hasMore: offset + rows.length < totalRow.total,
|
|
1499
|
+
pages: rows.map((row) => ({
|
|
1500
|
+
url: row.url,
|
|
1501
|
+
title: row.title,
|
|
1502
|
+
pageKind: row.page_kind,
|
|
1503
|
+
filePath: row.file_path,
|
|
1504
|
+
language: row.language,
|
|
1505
|
+
markdownLength: row.markdown.length
|
|
1506
|
+
}))
|
|
1507
|
+
};
|
|
1508
|
+
},
|
|
1509
|
+
getPage(input) {
|
|
1510
|
+
const source = this.getSourceById(input.sourceId);
|
|
1511
|
+
if (!source) {
|
|
1512
|
+
throw new AiocsError(
|
|
1513
|
+
AIOCS_ERROR_CODES.sourceNotFound,
|
|
1514
|
+
`Unknown source '${input.sourceId}'`
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
if (!input.url && !input.filePath || input.url && input.filePath) {
|
|
1518
|
+
throw new AiocsError(
|
|
1519
|
+
AIOCS_ERROR_CODES.invalidArgument,
|
|
1520
|
+
"Provide exactly one of url or filePath"
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
const snapshotId = input.snapshotId ?? source.lastSuccessfulSnapshotId;
|
|
1524
|
+
if (!snapshotId) {
|
|
1525
|
+
throw new AiocsError(
|
|
1526
|
+
AIOCS_ERROR_CODES.snapshotNotFound,
|
|
1527
|
+
`No successful snapshot found for source '${input.sourceId}'`
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
const row = db.prepare(`
|
|
1531
|
+
SELECT p.snapshot_id, p.url, p.title, p.markdown, p.page_kind, p.file_path, p.language
|
|
1532
|
+
FROM pages p
|
|
1533
|
+
INNER JOIN snapshots s
|
|
1534
|
+
ON s.id = p.snapshot_id
|
|
1535
|
+
WHERE p.snapshot_id = ?
|
|
1536
|
+
AND s.source_id = ?
|
|
1537
|
+
AND ${input.url ? "p.url = ?" : "p.file_path = ?"}
|
|
1538
|
+
LIMIT 1
|
|
1539
|
+
`).get(snapshotId, input.sourceId, input.url ?? input.filePath);
|
|
1540
|
+
if (!row) {
|
|
1541
|
+
throw new AiocsError(
|
|
1542
|
+
AIOCS_ERROR_CODES.pageNotFound,
|
|
1543
|
+
input.url ? `Page '${input.url}' not found for source '${input.sourceId}'` : `Page path '${input.filePath}' not found for source '${input.sourceId}'`
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
return {
|
|
1547
|
+
sourceId: input.sourceId,
|
|
1548
|
+
snapshotId,
|
|
1549
|
+
page: {
|
|
1550
|
+
url: row.url,
|
|
1551
|
+
title: row.title,
|
|
1552
|
+
markdown: row.markdown,
|
|
1553
|
+
pageKind: row.page_kind,
|
|
1554
|
+
filePath: row.file_path,
|
|
1555
|
+
language: row.language
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
},
|
|
1559
|
+
upsertRoutingLearning(input) {
|
|
1560
|
+
const source = this.getSourceById(input.sourceId);
|
|
1561
|
+
if (!source) {
|
|
1562
|
+
throw new AiocsError(
|
|
1563
|
+
AIOCS_ERROR_CODES.sourceNotFound,
|
|
1564
|
+
`Unknown source '${input.sourceId}'`
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
const timestamp = nowIso();
|
|
1568
|
+
const routeKey = buildRouteKey(input);
|
|
1569
|
+
const searchTerms = [...new Set((input.searchTerms ?? []).map((term) => term.trim()).filter(Boolean))];
|
|
1570
|
+
const validationSnapshotId = input.snapshotId ?? source.lastSuccessfulSnapshotId ?? null;
|
|
1571
|
+
const storedSnapshotId = input.snapshotId ?? null;
|
|
1572
|
+
const hasTargetPage = Boolean(input.pageUrl || input.filePath);
|
|
1573
|
+
if (input.snapshotId) {
|
|
1574
|
+
const snapshotRow = db.prepare(`
|
|
1575
|
+
SELECT id
|
|
1576
|
+
FROM snapshots
|
|
1577
|
+
WHERE id = ?
|
|
1578
|
+
AND source_id = ?
|
|
1579
|
+
`).get(input.snapshotId, input.sourceId);
|
|
1580
|
+
if (!snapshotRow) {
|
|
1581
|
+
throw new AiocsError(
|
|
1582
|
+
AIOCS_ERROR_CODES.snapshotNotFound,
|
|
1583
|
+
`Snapshot '${input.snapshotId}' not found for source '${input.sourceId}'`
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
if (hasTargetPage) {
|
|
1588
|
+
if (!validationSnapshotId) {
|
|
1589
|
+
throw new AiocsError(
|
|
1590
|
+
AIOCS_ERROR_CODES.snapshotNotFound,
|
|
1591
|
+
`No successful snapshot found for source '${input.sourceId}'`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
this.getPage({
|
|
1595
|
+
sourceId: input.sourceId,
|
|
1596
|
+
snapshotId: validationSnapshotId,
|
|
1597
|
+
...input.pageUrl ? { url: input.pageUrl } : input.filePath ? { filePath: input.filePath } : {}
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
const existing = db.prepare(`
|
|
1601
|
+
SELECT id, created_at
|
|
1602
|
+
FROM routing_learnings
|
|
1603
|
+
WHERE route_key = ?
|
|
1604
|
+
`).get(routeKey);
|
|
1605
|
+
const learningId = existing?.id ?? randomUUID();
|
|
1606
|
+
db.prepare(`
|
|
1607
|
+
INSERT INTO routing_learnings (
|
|
1608
|
+
id,
|
|
1609
|
+
route_key,
|
|
1610
|
+
source_id,
|
|
1611
|
+
snapshot_id,
|
|
1612
|
+
learning_type,
|
|
1613
|
+
intent,
|
|
1614
|
+
page_url,
|
|
1615
|
+
file_path,
|
|
1616
|
+
title,
|
|
1617
|
+
note,
|
|
1618
|
+
search_terms_json,
|
|
1619
|
+
created_at,
|
|
1620
|
+
updated_at
|
|
1621
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1622
|
+
ON CONFLICT(route_key) DO UPDATE SET
|
|
1623
|
+
snapshot_id = excluded.snapshot_id,
|
|
1624
|
+
title = excluded.title,
|
|
1625
|
+
note = excluded.note,
|
|
1626
|
+
search_terms_json = excluded.search_terms_json,
|
|
1627
|
+
updated_at = excluded.updated_at
|
|
1628
|
+
`).run(
|
|
1629
|
+
learningId,
|
|
1630
|
+
routeKey,
|
|
1631
|
+
input.sourceId,
|
|
1632
|
+
storedSnapshotId,
|
|
1633
|
+
input.learningType,
|
|
1634
|
+
input.intent.trim(),
|
|
1635
|
+
input.pageUrl ?? null,
|
|
1636
|
+
input.filePath ?? null,
|
|
1637
|
+
input.title ?? null,
|
|
1638
|
+
input.note ?? null,
|
|
1639
|
+
JSON.stringify(searchTerms),
|
|
1640
|
+
existing?.created_at ?? timestamp,
|
|
1641
|
+
timestamp
|
|
1642
|
+
);
|
|
1643
|
+
return {
|
|
1644
|
+
learningId,
|
|
1645
|
+
sourceId: input.sourceId,
|
|
1646
|
+
snapshotId: storedSnapshotId,
|
|
1647
|
+
learningType: input.learningType,
|
|
1648
|
+
intent: input.intent.trim(),
|
|
1649
|
+
pageUrl: input.pageUrl ?? null,
|
|
1650
|
+
filePath: input.filePath ?? null,
|
|
1651
|
+
title: input.title ?? null,
|
|
1652
|
+
note: input.note ?? null,
|
|
1653
|
+
searchTerms,
|
|
1654
|
+
createdAt: existing?.created_at ?? timestamp,
|
|
1655
|
+
updatedAt: timestamp
|
|
1656
|
+
};
|
|
1657
|
+
},
|
|
1658
|
+
listRoutingLearnings(input) {
|
|
1659
|
+
const whereSql = [];
|
|
1660
|
+
const args = [];
|
|
1661
|
+
if (input?.sourceId) {
|
|
1662
|
+
whereSql.push("source_id = ?");
|
|
1663
|
+
args.push(input.sourceId);
|
|
1664
|
+
}
|
|
1665
|
+
if (input?.learningType) {
|
|
1666
|
+
whereSql.push("learning_type = ?");
|
|
1667
|
+
args.push(input.learningType);
|
|
1668
|
+
}
|
|
1669
|
+
if (input?.intentQuery) {
|
|
1670
|
+
whereSql.push("LOWER(intent) LIKE ?");
|
|
1671
|
+
args.push(`%${normalizeQuery(input.intentQuery).toLowerCase()}%`);
|
|
1672
|
+
}
|
|
1673
|
+
const limit = assertPaginationValue(input?.limit, "limit", 50);
|
|
1674
|
+
const rows = db.prepare(`
|
|
1675
|
+
SELECT
|
|
1676
|
+
id,
|
|
1677
|
+
source_id,
|
|
1678
|
+
snapshot_id,
|
|
1679
|
+
learning_type,
|
|
1680
|
+
intent,
|
|
1681
|
+
page_url,
|
|
1682
|
+
file_path,
|
|
1683
|
+
title,
|
|
1684
|
+
note,
|
|
1685
|
+
search_terms_json,
|
|
1686
|
+
created_at,
|
|
1687
|
+
updated_at
|
|
1688
|
+
FROM routing_learnings
|
|
1689
|
+
${whereSql.length > 0 ? `WHERE ${whereSql.join(" AND ")}` : ""}
|
|
1690
|
+
ORDER BY updated_at DESC, source_id, intent
|
|
1691
|
+
LIMIT ?
|
|
1692
|
+
`).all(...args, limit);
|
|
1693
|
+
return rows.map((row) => ({
|
|
1694
|
+
learningId: row.id,
|
|
1695
|
+
sourceId: row.source_id,
|
|
1696
|
+
snapshotId: row.snapshot_id,
|
|
1697
|
+
learningType: row.learning_type,
|
|
1698
|
+
intent: row.intent,
|
|
1699
|
+
pageUrl: row.page_url,
|
|
1700
|
+
filePath: row.file_path,
|
|
1701
|
+
title: row.title,
|
|
1702
|
+
note: row.note,
|
|
1703
|
+
searchTerms: JSON.parse(row.search_terms_json),
|
|
1704
|
+
createdAt: row.created_at,
|
|
1705
|
+
updatedAt: row.updated_at
|
|
1706
|
+
}));
|
|
1707
|
+
},
|
|
1338
1708
|
listDueSourceIds(referenceTime = nowIso()) {
|
|
1339
1709
|
const rows = db.prepare(`
|
|
1340
1710
|
SELECT id
|
|
@@ -3997,7 +4367,7 @@ async function startDaemon(input) {
|
|
|
3997
4367
|
// package.json
|
|
3998
4368
|
var package_default = {
|
|
3999
4369
|
name: "@bodhi-ventures/aiocs",
|
|
4000
|
-
version: "0.
|
|
4370
|
+
version: "0.6.0",
|
|
4001
4371
|
license: "MIT",
|
|
4002
4372
|
type: "module",
|
|
4003
4373
|
description: "Local-only documentation store, fetcher, and search CLI for AI agents.",
|
|
@@ -5052,7 +5422,121 @@ async function searchHybridCatalog(input) {
|
|
|
5052
5422
|
};
|
|
5053
5423
|
}
|
|
5054
5424
|
|
|
5425
|
+
// src/retrieval.ts
|
|
5426
|
+
function tokenize(value) {
|
|
5427
|
+
return value.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").split(/\s+/).map((part) => part.trim()).filter(Boolean);
|
|
5428
|
+
}
|
|
5429
|
+
function overlapScore(queryTokens, candidate) {
|
|
5430
|
+
const candidateTokens = new Set(tokenize(candidate));
|
|
5431
|
+
if (candidateTokens.size === 0) {
|
|
5432
|
+
return 0;
|
|
5433
|
+
}
|
|
5434
|
+
let matches = 0;
|
|
5435
|
+
for (const token of queryTokens) {
|
|
5436
|
+
if (candidateTokens.has(token)) {
|
|
5437
|
+
matches += 1;
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
5440
|
+
return matches;
|
|
5441
|
+
}
|
|
5442
|
+
function scoreLearning(query, learning) {
|
|
5443
|
+
const queryTokens = tokenize(query);
|
|
5444
|
+
const candidates = [learning.intent, ...learning.searchTerms];
|
|
5445
|
+
const bestOverlap = Math.max(0, ...candidates.map((candidate) => overlapScore(queryTokens, candidate)));
|
|
5446
|
+
const exactIntentBoost = learning.intent.trim().toLowerCase() === query.trim().toLowerCase() ? 10 : 0;
|
|
5447
|
+
const recencyBoost = learning.learningType === "discovery" ? 1 : 0;
|
|
5448
|
+
return bestOverlap + exactIntentBoost + recencyBoost;
|
|
5449
|
+
}
|
|
5450
|
+
function scoreSourceContext(query, context) {
|
|
5451
|
+
if (!context) {
|
|
5452
|
+
return 0;
|
|
5453
|
+
}
|
|
5454
|
+
const queryTokens = tokenize(query);
|
|
5455
|
+
const candidates = [
|
|
5456
|
+
context.purpose ?? "",
|
|
5457
|
+
context.summary ?? "",
|
|
5458
|
+
...context.topicHints,
|
|
5459
|
+
...context.gotchas,
|
|
5460
|
+
...context.authNotes,
|
|
5461
|
+
...context.commonLocations.flatMap((location) => [location.label, location.note ?? "", location.url ?? "", location.filePath ?? ""])
|
|
5462
|
+
];
|
|
5463
|
+
return Math.max(0, ...candidates.map((candidate) => overlapScore(queryTokens, candidate)));
|
|
5464
|
+
}
|
|
5465
|
+
|
|
5466
|
+
// src/source-context.ts
|
|
5467
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
5468
|
+
import { extname as extname3 } from "path";
|
|
5469
|
+
import YAML2 from "yaml";
|
|
5470
|
+
import { z as z2 } from "zod";
|
|
5471
|
+
var commonLocationSchema = z2.object({
|
|
5472
|
+
label: z2.string().min(1),
|
|
5473
|
+
url: z2.string().url().optional(),
|
|
5474
|
+
filePath: z2.string().min(1).optional(),
|
|
5475
|
+
note: z2.string().min(1).optional()
|
|
5476
|
+
}).superRefine((value, context) => {
|
|
5477
|
+
if (!value.url && !value.filePath) {
|
|
5478
|
+
context.addIssue({
|
|
5479
|
+
code: z2.ZodIssueCode.custom,
|
|
5480
|
+
message: "common location entries must include url or filePath",
|
|
5481
|
+
path: ["url"]
|
|
5482
|
+
});
|
|
5483
|
+
}
|
|
5484
|
+
});
|
|
5485
|
+
var sourceContextSchema = z2.object({
|
|
5486
|
+
purpose: z2.string().min(1).optional(),
|
|
5487
|
+
summary: z2.string().min(1).optional(),
|
|
5488
|
+
topicHints: z2.array(z2.string().min(1)).default([]),
|
|
5489
|
+
commonLocations: z2.array(commonLocationSchema).default([]),
|
|
5490
|
+
gotchas: z2.array(z2.string().min(1)).default([]),
|
|
5491
|
+
authNotes: z2.array(z2.string().min(1)).default([])
|
|
5492
|
+
});
|
|
5493
|
+
function parseSourceContext(raw, extension) {
|
|
5494
|
+
if (extension === ".json") {
|
|
5495
|
+
return JSON.parse(raw);
|
|
5496
|
+
}
|
|
5497
|
+
return YAML2.parse(raw);
|
|
5498
|
+
}
|
|
5499
|
+
async function loadSourceContextFile(path) {
|
|
5500
|
+
try {
|
|
5501
|
+
const raw = await readFile4(path, "utf8");
|
|
5502
|
+
return sourceContextSchema.parse(parseSourceContext(raw, extname3(path).toLowerCase()));
|
|
5503
|
+
} catch (error) {
|
|
5504
|
+
if (error instanceof z2.ZodError) {
|
|
5505
|
+
throw new AiocsError(
|
|
5506
|
+
AIOCS_ERROR_CODES.sourceContextInvalid,
|
|
5507
|
+
`Invalid source context file '${path}'`,
|
|
5508
|
+
{
|
|
5509
|
+
issues: error.issues.map((issue) => ({
|
|
5510
|
+
path: issue.path.join("."),
|
|
5511
|
+
message: issue.message
|
|
5512
|
+
}))
|
|
5513
|
+
}
|
|
5514
|
+
);
|
|
5515
|
+
}
|
|
5516
|
+
if (error instanceof Error) {
|
|
5517
|
+
throw new AiocsError(
|
|
5518
|
+
AIOCS_ERROR_CODES.sourceContextInvalid,
|
|
5519
|
+
`Failed to load source context file '${path}': ${error.message}`
|
|
5520
|
+
);
|
|
5521
|
+
}
|
|
5522
|
+
throw error;
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5055
5526
|
// src/services.ts
|
|
5527
|
+
function dedupePagesByIdentity(rows) {
|
|
5528
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5529
|
+
const deduped = [];
|
|
5530
|
+
for (const row of rows) {
|
|
5531
|
+
const key = `${row.sourceId}::${row.snapshotId}::${row.filePath ?? row.pageUrl}`;
|
|
5532
|
+
if (seen.has(key)) {
|
|
5533
|
+
continue;
|
|
5534
|
+
}
|
|
5535
|
+
seen.add(key);
|
|
5536
|
+
deduped.push(row);
|
|
5537
|
+
}
|
|
5538
|
+
return deduped;
|
|
5539
|
+
}
|
|
5056
5540
|
function createCatalog() {
|
|
5057
5541
|
const dataDir = getAiocsDataDir();
|
|
5058
5542
|
getAiocsConfigDir();
|
|
@@ -5079,6 +5563,70 @@ async function listSources() {
|
|
|
5079
5563
|
const sources = await withCatalog(({ catalog }) => catalog.listSources());
|
|
5080
5564
|
return { sources };
|
|
5081
5565
|
}
|
|
5566
|
+
async function describeSource(sourceId) {
|
|
5567
|
+
return withCatalog(({ catalog }) => {
|
|
5568
|
+
const source = catalog.getSourceById(sourceId);
|
|
5569
|
+
if (!source) {
|
|
5570
|
+
throw new AiocsError(
|
|
5571
|
+
AIOCS_ERROR_CODES.sourceNotFound,
|
|
5572
|
+
`Unknown source '${sourceId}'`
|
|
5573
|
+
);
|
|
5574
|
+
}
|
|
5575
|
+
const latestSnapshot = catalog.listSnapshots(sourceId)[0] ?? null;
|
|
5576
|
+
return {
|
|
5577
|
+
source,
|
|
5578
|
+
context: catalog.getSourceContext(sourceId),
|
|
5579
|
+
latestSnapshot,
|
|
5580
|
+
recentLearnings: catalog.listRoutingLearnings({
|
|
5581
|
+
sourceId,
|
|
5582
|
+
limit: 10
|
|
5583
|
+
})
|
|
5584
|
+
};
|
|
5585
|
+
});
|
|
5586
|
+
}
|
|
5587
|
+
async function upsertSourceContextFromFile(sourceId, contextFile) {
|
|
5588
|
+
const resolvedContextFile = resolve8(contextFile);
|
|
5589
|
+
const context = await loadSourceContextFile(resolvedContextFile);
|
|
5590
|
+
const result = await withCatalog(({ catalog }) => catalog.upsertSourceContext(sourceId, context));
|
|
5591
|
+
return {
|
|
5592
|
+
...result,
|
|
5593
|
+
contextFile: resolvedContextFile
|
|
5594
|
+
};
|
|
5595
|
+
}
|
|
5596
|
+
async function getSourceContextForSource(sourceId) {
|
|
5597
|
+
return withCatalog(({ catalog }) => catalog.getSourceContext(sourceId));
|
|
5598
|
+
}
|
|
5599
|
+
async function listSourcePages(sourceId, options) {
|
|
5600
|
+
return withCatalog(({ catalog }) => catalog.listPages({
|
|
5601
|
+
sourceId,
|
|
5602
|
+
...options.snapshot ? { snapshotId: options.snapshot } : {},
|
|
5603
|
+
...options.query ? { query: options.query } : {},
|
|
5604
|
+
...options.path && options.path.length > 0 ? { pathPatterns: options.path } : {},
|
|
5605
|
+
...typeof options.limit === "number" ? { limit: options.limit } : {},
|
|
5606
|
+
...typeof options.offset === "number" ? { offset: options.offset } : {}
|
|
5607
|
+
}));
|
|
5608
|
+
}
|
|
5609
|
+
async function showPage(input) {
|
|
5610
|
+
return withCatalog(({ catalog }) => catalog.getPage(input));
|
|
5611
|
+
}
|
|
5612
|
+
async function saveRoutingLearning(input) {
|
|
5613
|
+
const learning = await withCatalog(({ catalog }) => catalog.upsertRoutingLearning({
|
|
5614
|
+
sourceId: input.sourceId,
|
|
5615
|
+
...input.snapshotId ? { snapshotId: input.snapshotId } : {},
|
|
5616
|
+
learningType: input.learningType,
|
|
5617
|
+
intent: input.intent,
|
|
5618
|
+
...input.pageUrl ? { pageUrl: input.pageUrl } : {},
|
|
5619
|
+
...input.filePath ? { filePath: input.filePath } : {},
|
|
5620
|
+
...input.title ? { title: input.title } : {},
|
|
5621
|
+
...input.note ? { note: input.note } : {},
|
|
5622
|
+
...input.searchTerms ? { searchTerms: input.searchTerms } : {}
|
|
5623
|
+
}));
|
|
5624
|
+
return { learning };
|
|
5625
|
+
}
|
|
5626
|
+
async function listRoutingLearningsForQuery(input) {
|
|
5627
|
+
const learnings = await withCatalog(({ catalog }) => catalog.listRoutingLearnings(input));
|
|
5628
|
+
return { learnings };
|
|
5629
|
+
}
|
|
5082
5630
|
async function fetchSources(sourceIdOrAll) {
|
|
5083
5631
|
const results = await withCatalog(async ({ catalog, dataDir }) => {
|
|
5084
5632
|
const sourceIds = sourceIdOrAll === "all" ? catalog.listSources().map((item) => item.id) : [sourceIdOrAll];
|
|
@@ -5222,6 +5770,125 @@ async function searchCatalog(query, options) {
|
|
|
5222
5770
|
results: results.results
|
|
5223
5771
|
};
|
|
5224
5772
|
}
|
|
5773
|
+
async function retrieveContext(query, options) {
|
|
5774
|
+
const cwd = options.project ? resolve8(options.project) : process.cwd();
|
|
5775
|
+
const explicitSources = options.source.length > 0;
|
|
5776
|
+
const pageLimit = typeof options.pageLimit === "number" ? options.pageLimit : 3;
|
|
5777
|
+
return withCatalog(async ({ catalog }) => {
|
|
5778
|
+
const hybridConfig = getHybridRuntimeConfig();
|
|
5779
|
+
const scope = resolveProjectScope(cwd, catalog.listProjectLinks());
|
|
5780
|
+
if (!explicitSources && !options.all && !scope) {
|
|
5781
|
+
throw new AiocsError(
|
|
5782
|
+
AIOCS_ERROR_CODES.noProjectScope,
|
|
5783
|
+
"No linked project scope found. Use --source or --all."
|
|
5784
|
+
);
|
|
5785
|
+
}
|
|
5786
|
+
const sourceScope = explicitSources ? options.source : options.all ? catalog.listSources().map((source) => source.id) : scope?.sourceIds ?? [];
|
|
5787
|
+
const learnings = catalog.listRoutingLearnings({
|
|
5788
|
+
limit: 100
|
|
5789
|
+
}).filter((learning) => sourceScope.length === 0 || sourceScope.includes(learning.sourceId));
|
|
5790
|
+
const scoredLearnings = learnings.map((learning) => ({
|
|
5791
|
+
...learning,
|
|
5792
|
+
score: scoreLearning(query, learning)
|
|
5793
|
+
})).filter((learning) => learning.score > 0).sort((left, right) => right.score - left.score || right.updatedAt.localeCompare(left.updatedAt));
|
|
5794
|
+
const matchedLearnings = scoredLearnings.filter((learning) => learning.learningType === "discovery");
|
|
5795
|
+
const avoidedLearnings = scoredLearnings.filter((learning) => learning.learningType === "negative");
|
|
5796
|
+
const avoidedPageKeys = new Set(
|
|
5797
|
+
avoidedLearnings.map((learning) => `${learning.sourceId}::${learning.filePath ?? learning.pageUrl ?? ""}`)
|
|
5798
|
+
);
|
|
5799
|
+
const sourceHints = sourceScope.map((sourceId) => {
|
|
5800
|
+
const contextRecord = catalog.getSourceContext(sourceId);
|
|
5801
|
+
const score = scoreSourceContext(query, contextRecord.context);
|
|
5802
|
+
if (!contextRecord.context || score <= 0) {
|
|
5803
|
+
return null;
|
|
5804
|
+
}
|
|
5805
|
+
const matchedCommonLocations = contextRecord.context.commonLocations.filter(
|
|
5806
|
+
(location) => scoreSourceContext(query, {
|
|
5807
|
+
purpose: "",
|
|
5808
|
+
summary: "",
|
|
5809
|
+
topicHints: [],
|
|
5810
|
+
commonLocations: [location],
|
|
5811
|
+
gotchas: [],
|
|
5812
|
+
authNotes: []
|
|
5813
|
+
}) > 0
|
|
5814
|
+
);
|
|
5815
|
+
return {
|
|
5816
|
+
sourceId,
|
|
5817
|
+
score,
|
|
5818
|
+
context: contextRecord.context,
|
|
5819
|
+
matchedCommonLocations
|
|
5820
|
+
};
|
|
5821
|
+
}).filter((entry) => Boolean(entry)).sort((left, right) => right.score - left.score || left.sourceId.localeCompare(right.sourceId));
|
|
5822
|
+
const search = await searchHybridCatalog({
|
|
5823
|
+
catalog,
|
|
5824
|
+
config: hybridConfig,
|
|
5825
|
+
query,
|
|
5826
|
+
mode: options.mode ?? hybridConfig.defaultSearchMode,
|
|
5827
|
+
searchInput: {
|
|
5828
|
+
cwd,
|
|
5829
|
+
...explicitSources ? { sourceIds: options.source } : {},
|
|
5830
|
+
...options.snapshot ? { snapshotId: options.snapshot } : {},
|
|
5831
|
+
...options.all ? { all: true } : {},
|
|
5832
|
+
...options.path && options.path.length > 0 ? { pathPatterns: options.path } : {},
|
|
5833
|
+
...options.language && options.language.length > 0 ? { languages: options.language } : {},
|
|
5834
|
+
...typeof options.limit === "number" ? { limit: options.limit } : {},
|
|
5835
|
+
...typeof options.offset === "number" ? { offset: options.offset } : {}
|
|
5836
|
+
}
|
|
5837
|
+
});
|
|
5838
|
+
const learnedPages = matchedLearnings.filter((learning) => learning.pageUrl || learning.filePath).map((learning) => ({
|
|
5839
|
+
sourceId: learning.sourceId,
|
|
5840
|
+
snapshotId: learning.snapshotId ?? catalog.getSourceById(learning.sourceId)?.lastSuccessfulSnapshotId ?? "",
|
|
5841
|
+
pageUrl: learning.pageUrl ?? "",
|
|
5842
|
+
filePath: learning.filePath ?? null
|
|
5843
|
+
})).filter((entry) => entry.snapshotId);
|
|
5844
|
+
const searchedPages = search.results.filter((result) => !avoidedPageKeys.has(`${result.sourceId}::${result.filePath ?? result.pageUrl}`)).map((result) => ({
|
|
5845
|
+
sourceId: result.sourceId,
|
|
5846
|
+
snapshotId: result.snapshotId,
|
|
5847
|
+
pageUrl: result.pageUrl,
|
|
5848
|
+
filePath: result.filePath
|
|
5849
|
+
}));
|
|
5850
|
+
const selectedPages = dedupePagesByIdentity([
|
|
5851
|
+
...learnedPages,
|
|
5852
|
+
...searchedPages
|
|
5853
|
+
]).slice(0, Math.max(1, pageLimit));
|
|
5854
|
+
const pages = selectedPages.flatMap((entry) => {
|
|
5855
|
+
try {
|
|
5856
|
+
const page = catalog.getPage({
|
|
5857
|
+
sourceId: entry.sourceId,
|
|
5858
|
+
snapshotId: entry.snapshotId,
|
|
5859
|
+
...entry.filePath ? { filePath: entry.filePath } : { url: entry.pageUrl }
|
|
5860
|
+
});
|
|
5861
|
+
return [{
|
|
5862
|
+
sourceId: page.sourceId,
|
|
5863
|
+
snapshotId: page.snapshotId,
|
|
5864
|
+
...page.page
|
|
5865
|
+
}];
|
|
5866
|
+
} catch (error) {
|
|
5867
|
+
if (error instanceof AiocsError && (error.code === AIOCS_ERROR_CODES.pageNotFound || error.code === AIOCS_ERROR_CODES.snapshotNotFound)) {
|
|
5868
|
+
return [];
|
|
5869
|
+
}
|
|
5870
|
+
throw error;
|
|
5871
|
+
}
|
|
5872
|
+
});
|
|
5873
|
+
return {
|
|
5874
|
+
query,
|
|
5875
|
+
modeRequested: search.modeRequested,
|
|
5876
|
+
modeUsed: search.modeUsed,
|
|
5877
|
+
sourceScope,
|
|
5878
|
+
sourceHints,
|
|
5879
|
+
matchedLearnings,
|
|
5880
|
+
avoidedLearnings,
|
|
5881
|
+
search: {
|
|
5882
|
+
total: search.total,
|
|
5883
|
+
limit: search.limit,
|
|
5884
|
+
offset: search.offset,
|
|
5885
|
+
hasMore: search.hasMore,
|
|
5886
|
+
results: search.results
|
|
5887
|
+
},
|
|
5888
|
+
pages
|
|
5889
|
+
};
|
|
5890
|
+
});
|
|
5891
|
+
}
|
|
5225
5892
|
async function showChunk(chunkId) {
|
|
5226
5893
|
const chunk = await withCatalog(({ catalog }) => catalog.getChunkById(chunkId));
|
|
5227
5894
|
if (!chunk) {
|
|
@@ -5361,6 +6028,13 @@ export {
|
|
|
5361
6028
|
packageDescription,
|
|
5362
6029
|
upsertSourceFromSpecFile,
|
|
5363
6030
|
listSources,
|
|
6031
|
+
describeSource,
|
|
6032
|
+
upsertSourceContextFromFile,
|
|
6033
|
+
getSourceContextForSource,
|
|
6034
|
+
listSourcePages,
|
|
6035
|
+
showPage,
|
|
6036
|
+
saveRoutingLearning,
|
|
6037
|
+
listRoutingLearningsForQuery,
|
|
5364
6038
|
fetchSources,
|
|
5365
6039
|
refreshDueSources,
|
|
5366
6040
|
runSourceCanaries,
|
|
@@ -5369,6 +6043,7 @@ export {
|
|
|
5369
6043
|
linkProjectSources,
|
|
5370
6044
|
unlinkProjectSources,
|
|
5371
6045
|
searchCatalog,
|
|
6046
|
+
retrieveContext,
|
|
5372
6047
|
showChunk,
|
|
5373
6048
|
verifyCoverage,
|
|
5374
6049
|
initManagedSources,
|