@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/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  AiocsError,
5
5
  backfillEmbeddings,
6
6
  clearEmbeddings,
7
+ describeSource,
7
8
  diffSnapshotsForSource,
8
9
  exportCatalogBackup,
9
10
  fetchSources,
@@ -12,26 +13,33 @@ import {
12
13
  getDoctorReport,
13
14
  getEmbeddingStatus,
14
15
  getManagedSourceSpecDirectories,
16
+ getSourceContextForSource,
15
17
  importCatalogBackup,
16
18
  initManagedSources,
17
19
  linkProjectSources,
20
+ listRoutingLearningsForQuery,
18
21
  listSnapshotsForSource,
22
+ listSourcePages,
19
23
  listSources,
20
24
  openCatalog,
21
25
  packageName,
22
26
  packageVersion,
23
27
  parseDaemonConfig,
24
28
  refreshDueSources,
29
+ retrieveContext,
25
30
  runEmbeddingWorker,
26
31
  runSourceCanaries,
32
+ saveRoutingLearning,
27
33
  searchCatalog,
28
34
  showChunk,
35
+ showPage,
29
36
  startDaemon,
30
37
  toAiocsError,
31
38
  unlinkProjectSources,
39
+ upsertSourceContextFromFile,
32
40
  upsertSourceFromSpecFile,
33
41
  verifyCoverage
34
- } from "./chunk-M767TPUX.js";
42
+ } from "./chunk-2UWV3O7E.js";
35
43
 
36
44
  // src/cli.ts
37
45
  import { Command, CommanderError as CommanderError2 } from "commander";
@@ -99,7 +107,7 @@ function inferRequestedCommand(argv) {
99
107
  if (!first) {
100
108
  return "cli";
101
109
  }
102
- if (["source", "snapshot", "project", "refresh", "verify", "backup"].includes(first) && second && !second.startsWith("-")) {
110
+ if (["source", "snapshot", "page", "project", "refresh", "verify", "backup", "learning"].includes(first) && second && !second.startsWith("-")) {
103
111
  return `${first}.${second}`;
104
112
  }
105
113
  return first;
@@ -124,6 +132,20 @@ function renderSearchResult(result) {
124
132
  ""
125
133
  ].join("\n");
126
134
  }
135
+ function renderPageResult(result) {
136
+ return [
137
+ `Source: ${result.sourceId}`,
138
+ `Snapshot: ${result.snapshotId}`,
139
+ ...result.pageKind ? [`Kind: ${result.pageKind}`] : [],
140
+ ...result.filePath ? [`Path: ${result.filePath}`] : [],
141
+ ...result.language ? [`Language: ${result.language}`] : [],
142
+ `Page: ${result.title}`,
143
+ `URL: ${result.url}`,
144
+ "",
145
+ result.markdown,
146
+ ""
147
+ ].join("\n");
148
+ }
127
149
  function parsePositiveIntegerOption(value, field) {
128
150
  if (typeof value === "undefined") {
129
151
  return void 0;
@@ -351,6 +373,58 @@ source.command("list").action(async (_options, command) => {
351
373
  };
352
374
  });
353
375
  });
376
+ source.command("describe").argument("<source-id>").action(async (sourceId, _options, command) => {
377
+ await executeCommand(command, "source.describe", async () => {
378
+ const result = await describeSource(sourceId);
379
+ return {
380
+ data: result,
381
+ human: [
382
+ `${result.source.id} | ${result.source.kind} | ${result.source.label}`,
383
+ `Spec ${result.source.specPath ?? "(inline/unknown)"}`,
384
+ result.latestSnapshot ? `Latest ${result.latestSnapshot.snapshotId} | pages=${result.latestSnapshot.pageCount} | created=${result.latestSnapshot.createdAt}` : "No successful snapshots",
385
+ ...result.context.context ? [
386
+ ...result.context.context.purpose ? [`Purpose: ${result.context.context.purpose}`] : [],
387
+ ...result.context.context.summary ? [`Summary: ${result.context.context.summary}`] : [],
388
+ ...result.context.context.topicHints.length > 0 ? [`Topics: ${result.context.context.topicHints.join(", ")}`] : [],
389
+ ...result.context.context.commonLocations.length > 0 ? [
390
+ "Common locations:",
391
+ ...result.context.context.commonLocations.map((location) => `- ${location.label} | ${location.filePath ?? location.url ?? "(unknown)"}${location.note ? ` | ${location.note}` : ""}`)
392
+ ] : []
393
+ ] : ["No curated source context"],
394
+ ...result.recentLearnings.length > 0 ? [
395
+ "Recent learnings:",
396
+ ...result.recentLearnings.map((learning2) => `- ${learning2.learningType} | ${learning2.intent} | ${learning2.filePath ?? learning2.pageUrl ?? "(no target)"}`)
397
+ ] : ["No routing learnings"]
398
+ ]
399
+ };
400
+ });
401
+ });
402
+ var sourceContext = source.command("context");
403
+ sourceContext.command("show").argument("<source-id>").action(async (sourceId, _options, command) => {
404
+ await executeCommand(command, "source.context.show", async () => {
405
+ const result = await getSourceContextForSource(sourceId);
406
+ return {
407
+ data: result,
408
+ human: result.context ? [
409
+ `Context for ${sourceId}`,
410
+ ...result.context.purpose ? [`Purpose: ${result.context.purpose}`] : [],
411
+ ...result.context.summary ? [`Summary: ${result.context.summary}`] : [],
412
+ ...result.context.topicHints.length > 0 ? [`Topics: ${result.context.topicHints.join(", ")}`] : [],
413
+ ...result.context.authNotes.length > 0 ? ["Auth notes:", ...result.context.authNotes.map((item) => `- ${item}`)] : [],
414
+ ...result.context.gotchas.length > 0 ? ["Gotchas:", ...result.context.gotchas.map((item) => `- ${item}`)] : []
415
+ ] : `No curated source context for ${sourceId}`
416
+ };
417
+ });
418
+ });
419
+ sourceContext.command("upsert").argument("<source-id>").argument("<context-file>").action(async (sourceId, contextFile, _options, command) => {
420
+ await executeCommand(command, "source.context.upsert", async () => {
421
+ const result = await upsertSourceContextFromFile(sourceId, contextFile);
422
+ return {
423
+ data: result,
424
+ human: `Updated source context for ${result.sourceId}`
425
+ };
426
+ });
427
+ });
354
428
  program.command("fetch").argument("<source-id-or-all>").action(async (sourceIdOrAll, _options, command) => {
355
429
  await executeCommand(command, "fetch", async () => {
356
430
  const result = await fetchSources(sourceIdOrAll);
@@ -403,6 +477,48 @@ snapshot.command("list").argument("<source-id>").action(async (sourceId, _option
403
477
  };
404
478
  });
405
479
  });
480
+ var page = program.command("page");
481
+ page.command("list").argument("<source-id>").option("--snapshot <snapshot-id>", "list pages from a specific snapshot").option("--query <text>", "filter pages by title, url, or file path").option("--path <glob>", "restrict pages to file paths matching a glob", (value, current) => {
482
+ current.push(value);
483
+ return current;
484
+ }, []).option("--limit <count>", "maximum number of pages to return").option("--offset <count>", "number of pages to skip before returning results").action(async (sourceId, options, command) => {
485
+ await executeCommand(command, "page.list", async () => {
486
+ const limit = parsePositiveIntegerOption(options.limit, "limit");
487
+ const offset = parsePositiveIntegerOption(options.offset, "offset");
488
+ const result = await listSourcePages(sourceId, {
489
+ ...options.snapshot ? { snapshot: options.snapshot } : {},
490
+ ...options.query ? { query: options.query } : {},
491
+ ...options.path && options.path.length > 0 ? { path: options.path } : {},
492
+ ...typeof limit === "number" ? { limit } : {},
493
+ ...typeof offset === "number" ? { offset } : {}
494
+ });
495
+ return {
496
+ data: result,
497
+ human: result.pages.length === 0 ? `No pages for ${sourceId}` : [
498
+ `Showing ${result.offset + 1}-${result.offset + result.pages.length} of ${result.total} page(s) for ${sourceId}`,
499
+ ...result.pages.map((entry) => `${entry.title} | ${entry.filePath ?? entry.url} | kind=${entry.pageKind} | chars=${entry.markdownLength}`)
500
+ ]
501
+ };
502
+ });
503
+ });
504
+ page.command("show").argument("<source-id>").option("--snapshot <snapshot-id>", "read from a specific snapshot").option("--url <page-url>", "read a page by URL").option("--path <file-path>", "read a file-backed page by file path").action(async (sourceId, options, command) => {
505
+ await executeCommand(command, "page.show", async () => {
506
+ const result = await showPage({
507
+ sourceId,
508
+ ...options.snapshot ? { snapshotId: options.snapshot } : {},
509
+ ...options.url ? { url: options.url } : {},
510
+ ...options.path ? { filePath: options.path } : {}
511
+ });
512
+ return {
513
+ data: result,
514
+ human: renderPageResult({
515
+ sourceId: result.sourceId,
516
+ snapshotId: result.snapshotId,
517
+ ...result.page
518
+ })
519
+ };
520
+ });
521
+ });
406
522
  program.command("diff").alias("changes").argument("<source-id>").option("--from <snapshot-id>", "base snapshot id").option("--to <snapshot-id>", "target snapshot id").description("Compare two snapshots for a source.").action(
407
523
  async (sourceId, options, command) => {
408
524
  await executeCommand(command, "diff", async () => {
@@ -521,6 +637,94 @@ embeddings.command("run").description("Process queued embedding jobs immediately
521
637
  };
522
638
  });
523
639
  });
640
+ var learning = program.command("learning");
641
+ learning.command("save").requiredOption("--source <source-id>", "source to attach the learning to").requiredOption("--kind <discovery|negative>", "learning type").requiredOption("--intent <text>", "intent or question pattern this learning applies to").option("--snapshot <snapshot-id>", "snapshot associated with the learning").option("--page-url <page-url>", "page URL associated with the learning").option("--file-path <file-path>", "file-backed page path associated with the learning").option("--title <title>", "page title or short label").option("--note <text>", "additional note").option("--search-term <term>", "search term that worked (or failed)", (value, current) => {
642
+ current.push(value);
643
+ return current;
644
+ }, []).action(async (options, command) => {
645
+ await executeCommand(command, "learning.save", async () => {
646
+ if (options.kind !== "discovery" && options.kind !== "negative") {
647
+ throw new AiocsError(
648
+ AIOCS_ERROR_CODES.invalidArgument,
649
+ "kind must be discovery or negative"
650
+ );
651
+ }
652
+ const result = await saveRoutingLearning({
653
+ sourceId: options.source,
654
+ learningType: options.kind,
655
+ intent: options.intent,
656
+ ...options.snapshot ? { snapshotId: options.snapshot } : {},
657
+ ...options.pageUrl ? { pageUrl: options.pageUrl } : {},
658
+ ...options.filePath ? { filePath: options.filePath } : {},
659
+ ...options.title ? { title: options.title } : {},
660
+ ...options.note ? { note: options.note } : {},
661
+ ...options.searchTerm && options.searchTerm.length > 0 ? { searchTerms: options.searchTerm } : {}
662
+ });
663
+ return {
664
+ data: result,
665
+ human: `Saved ${result.learning.learningType} learning for ${result.learning.sourceId}`
666
+ };
667
+ });
668
+ });
669
+ learning.command("list").option("--source <source-id>", "filter by source id").option("--kind <discovery|negative>", "filter by learning type").option("--intent <text>", "filter by intent substring").option("--limit <count>", "maximum number of learnings to return").action(async (options, command) => {
670
+ await executeCommand(command, "learning.list", async () => {
671
+ const limit = parsePositiveIntegerOption(options.limit, "limit");
672
+ if (options.kind && options.kind !== "discovery" && options.kind !== "negative") {
673
+ throw new AiocsError(
674
+ AIOCS_ERROR_CODES.invalidArgument,
675
+ "kind must be discovery or negative"
676
+ );
677
+ }
678
+ const result = await listRoutingLearningsForQuery({
679
+ ...options.source ? { sourceId: options.source } : {},
680
+ ...options.kind ? { learningType: options.kind } : {},
681
+ ...options.intent ? { intentQuery: options.intent } : {},
682
+ ...typeof limit === "number" ? { limit } : {}
683
+ });
684
+ return {
685
+ data: result,
686
+ human: result.learnings.length === 0 ? "No learnings recorded." : result.learnings.map((entry) => `${entry.learningType} | ${entry.sourceId} | ${entry.intent} | ${entry.filePath ?? entry.pageUrl ?? "(no target)"}`)
687
+ };
688
+ });
689
+ });
690
+ program.command("retrieve").argument("<query>").option("--source <source-id>", "restrict retrieval to a source", (value, current) => {
691
+ current.push(value);
692
+ return current;
693
+ }, []).option("--snapshot <snapshot-id>", "retrieve from a specific snapshot").option("--all", "retrieve across all latest snapshots").option("--project <path>", "resolve retrieval scope as if running from this path").option("--path <glob>", "restrict retrieval to file paths matching a glob", (value, current) => {
694
+ current.push(value);
695
+ return current;
696
+ }, []).option("--language <name>", "restrict retrieval to a language", (value, current) => {
697
+ current.push(value);
698
+ return current;
699
+ }, []).option("--mode <mode>", "search mode: auto, lexical, hybrid, semantic").option("--limit <count>", "maximum number of search results to shortlist").option("--offset <count>", "number of search results to skip before shortlisting").option("--page-limit <count>", "maximum number of full pages to read into context").action(async (query, options, command) => {
700
+ await executeCommand(command, "retrieve", async () => {
701
+ const limit = parsePositiveIntegerOption(options.limit, "limit");
702
+ const offset = parsePositiveIntegerOption(options.offset, "offset");
703
+ const pageLimit = parsePositiveIntegerOption(options.pageLimit, "limit");
704
+ const mode = parseSearchModeOption(options.mode);
705
+ const result = await retrieveContext(query, {
706
+ source: options.source,
707
+ ...options.snapshot ? { snapshot: options.snapshot } : {},
708
+ ...typeof options.all !== "undefined" ? { all: options.all } : {},
709
+ ...options.project ? { project: options.project } : {},
710
+ ...options.path && options.path.length > 0 ? { path: options.path } : {},
711
+ ...options.language && options.language.length > 0 ? { language: options.language } : {},
712
+ ...mode ? { mode } : {},
713
+ ...typeof limit === "number" ? { limit } : {},
714
+ ...typeof offset === "number" ? { offset } : {},
715
+ ...typeof pageLimit === "number" ? { pageLimit } : {}
716
+ });
717
+ return {
718
+ data: result,
719
+ human: [
720
+ `Retrieval for "${query}" | mode=${result.modeUsed} | sources=${result.sourceScope.join(", ") || "(none)"}`,
721
+ ...result.sourceHints.length > 0 ? ["Source hints:", ...result.sourceHints.map((hint) => `- ${hint.sourceId} | score=${hint.score} | ${hint.context.summary ?? hint.context.purpose ?? "context"}`)] : [],
722
+ ...result.matchedLearnings.length > 0 ? ["Matched learnings:", ...result.matchedLearnings.map((learning2) => `- ${learning2.sourceId} | ${learning2.intent} | score=${learning2.score}`)] : [],
723
+ ...result.pages.length > 0 ? ["Full pages read:", ...result.pages.map((page2) => `${page2.sourceId} | ${page2.filePath ?? page2.url} | ${page2.title}`)] : ["No full pages selected"]
724
+ ]
725
+ };
726
+ });
727
+ });
524
728
  program.command("search").argument("<query>").option("--source <source-id>", "restrict search to a source", (value, current) => {
525
729
  current.push(value);
526
730
  return current;
@@ -4,29 +4,37 @@ import {
4
4
  AiocsError,
5
5
  backfillEmbeddings,
6
6
  clearEmbeddings,
7
+ describeSource,
7
8
  diffSnapshotsForSource,
8
9
  exportCatalogBackup,
9
10
  fetchSources,
10
11
  getDoctorReport,
11
12
  getEmbeddingStatus,
13
+ getSourceContextForSource,
12
14
  importCatalogBackup,
13
15
  initManagedSources,
14
16
  linkProjectSources,
17
+ listRoutingLearningsForQuery,
15
18
  listSnapshotsForSource,
19
+ listSourcePages,
16
20
  listSources,
17
21
  packageDescription,
18
22
  packageName,
19
23
  packageVersion,
20
24
  refreshDueSources,
25
+ retrieveContext,
21
26
  runEmbeddingWorker,
22
27
  runSourceCanaries,
28
+ saveRoutingLearning,
23
29
  searchCatalog,
24
30
  showChunk,
31
+ showPage,
25
32
  toAiocsError,
26
33
  unlinkProjectSources,
34
+ upsertSourceContextFromFile,
27
35
  upsertSourceFromSpecFile,
28
36
  verifyCoverage
29
- } from "./chunk-M767TPUX.js";
37
+ } from "./chunk-2UWV3O7E.js";
30
38
 
31
39
  // src/mcp-server.ts
32
40
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -64,6 +72,52 @@ var sourceSchema = z.object({
64
72
  lastSuccessfulCanaryAt: z.string().nullable(),
65
73
  lastCanaryStatus: z.enum(["pass", "fail"]).nullable()
66
74
  });
75
+ var sourceContextSchema = z.object({
76
+ purpose: z.string().optional(),
77
+ summary: z.string().optional(),
78
+ topicHints: z.array(z.string()),
79
+ commonLocations: z.array(z.object({
80
+ label: z.string(),
81
+ url: z.string().optional(),
82
+ filePath: z.string().optional(),
83
+ note: z.string().optional()
84
+ })),
85
+ gotchas: z.array(z.string()),
86
+ authNotes: z.array(z.string())
87
+ });
88
+ var routingLearningSchema = z.object({
89
+ learningId: z.string(),
90
+ sourceId: z.string(),
91
+ snapshotId: z.string().nullable(),
92
+ learningType: z.enum(["discovery", "negative"]),
93
+ intent: z.string(),
94
+ pageUrl: z.string().nullable(),
95
+ filePath: z.string().nullable(),
96
+ title: z.string().nullable(),
97
+ note: z.string().nullable(),
98
+ searchTerms: z.array(z.string()),
99
+ createdAt: z.string(),
100
+ updatedAt: z.string()
101
+ });
102
+ var pageSchema = z.object({
103
+ url: z.string(),
104
+ title: z.string(),
105
+ markdown: z.string(),
106
+ pageKind: z.enum(["document", "file"]),
107
+ filePath: z.string().nullable(),
108
+ language: z.string().nullable()
109
+ });
110
+ var pageListingSchema = z.object({
111
+ sourceId: z.string(),
112
+ snapshotId: z.string(),
113
+ total: z.number().int().nonnegative(),
114
+ limit: z.number().int().nonnegative(),
115
+ offset: z.number().int().nonnegative(),
116
+ hasMore: z.boolean(),
117
+ pages: z.array(pageSchema.omit({ markdown: true }).extend({
118
+ markdownLength: z.number().int().nonnegative()
119
+ }))
120
+ });
67
121
  var fetchResultSchema = z.object({
68
122
  sourceId: z.string(),
69
123
  snapshotId: z.string(),
@@ -254,10 +308,26 @@ var toolHandlers = {
254
308
  }),
255
309
  source_upsert: async (args = {}) => upsertSourceFromSpecFile(args.specFile),
256
310
  source_list: async () => listSources(),
311
+ source_describe: async (args = {}) => describeSource(args.sourceId),
312
+ source_context_show: async (args = {}) => getSourceContextForSource(args.sourceId),
313
+ source_context_upsert: async (args = {}) => upsertSourceContextFromFile(args.sourceId, args.contextFile),
257
314
  fetch: async (args = {}) => fetchSources(args.sourceIdOrAll),
258
315
  canary: async (args = {}) => runSourceCanaries(args.sourceIdOrAll),
259
316
  refresh_due: async (args = {}) => refreshDueSources(args.sourceIdOrAll ?? "all"),
260
317
  snapshot_list: async (args = {}) => listSnapshotsForSource(args.sourceId),
318
+ page_list: async (args = {}) => listSourcePages(args.sourceId, {
319
+ ...typeof args.snapshotId === "string" ? { snapshot: args.snapshotId } : {},
320
+ ...typeof args.query === "string" ? { query: args.query } : {},
321
+ ...Array.isArray(args.pathPatterns) ? { path: args.pathPatterns } : {},
322
+ ...typeof args.limit === "number" ? { limit: args.limit } : {},
323
+ ...typeof args.offset === "number" ? { offset: args.offset } : {}
324
+ }),
325
+ page_show: async (args = {}) => showPage({
326
+ sourceId: args.sourceId,
327
+ ...typeof args.snapshotId === "string" ? { snapshotId: args.snapshotId } : {},
328
+ ...typeof args.url === "string" ? { url: args.url } : {},
329
+ ...typeof args.filePath === "string" ? { filePath: args.filePath } : {}
330
+ }),
261
331
  diff_snapshots: async (args = {}) => diffSnapshotsForSource({
262
332
  sourceId: args.sourceId,
263
333
  ...typeof args.fromSnapshotId === "string" ? { fromSnapshotId: args.fromSnapshotId } : {},
@@ -276,6 +346,35 @@ var toolHandlers = {
276
346
  ...typeof args.limit === "number" ? { limit: args.limit } : {},
277
347
  ...typeof args.offset === "number" ? { offset: args.offset } : {}
278
348
  }),
349
+ retrieve_context: async (args = {}) => retrieveContext(args.query, {
350
+ source: args.sourceIds ?? [],
351
+ ...typeof args.snapshotId === "string" ? { snapshot: args.snapshotId } : {},
352
+ ...typeof args.all === "boolean" ? { all: args.all } : {},
353
+ ...typeof args.project === "string" ? { project: args.project } : {},
354
+ ...Array.isArray(args.pathPatterns) ? { path: args.pathPatterns } : {},
355
+ ...Array.isArray(args.languages) ? { language: args.languages } : {},
356
+ ...typeof args.mode === "string" ? { mode: args.mode } : {},
357
+ ...typeof args.limit === "number" ? { limit: args.limit } : {},
358
+ ...typeof args.offset === "number" ? { offset: args.offset } : {},
359
+ ...typeof args.pageLimit === "number" ? { pageLimit: args.pageLimit } : {}
360
+ }),
361
+ learning_save: async (args = {}) => saveRoutingLearning({
362
+ sourceId: args.sourceId,
363
+ learningType: args.learningType,
364
+ intent: args.intent,
365
+ ...typeof args.snapshotId === "string" ? { snapshotId: args.snapshotId } : {},
366
+ ...typeof args.pageUrl === "string" ? { pageUrl: args.pageUrl } : {},
367
+ ...typeof args.filePath === "string" ? { filePath: args.filePath } : {},
368
+ ...typeof args.title === "string" ? { title: args.title } : {},
369
+ ...typeof args.note === "string" ? { note: args.note } : {},
370
+ ...Array.isArray(args.searchTerms) ? { searchTerms: args.searchTerms } : {}
371
+ }),
372
+ learning_list: async (args = {}) => listRoutingLearningsForQuery({
373
+ ...typeof args.sourceId === "string" ? { sourceId: args.sourceId } : {},
374
+ ...typeof args.learningType === "string" ? { learningType: args.learningType } : {},
375
+ ...typeof args.intentQuery === "string" ? { intentQuery: args.intentQuery } : {},
376
+ ...typeof args.limit === "number" ? { limit: args.limit } : {}
377
+ }),
279
378
  show: async (args = {}) => showChunk(args.chunkId),
280
379
  embeddings_status: async () => getEmbeddingStatus(),
281
380
  embeddings_backfill: async (args = {}) => backfillEmbeddings(args.sourceIdOrAll),
@@ -410,6 +509,66 @@ registerAiocsTool(
410
509
  })
411
510
  }
412
511
  );
512
+ registerAiocsTool(
513
+ "source_describe",
514
+ {
515
+ title: "Source describe",
516
+ description: "Describe one source with latest snapshot info, curated source context, and recent routing learnings.",
517
+ inputSchema: z.object({
518
+ sourceId: z.string()
519
+ }),
520
+ outputSchema: z.object({
521
+ source: sourceSchema,
522
+ context: z.object({
523
+ sourceId: z.string(),
524
+ context: sourceContextSchema.nullable(),
525
+ createdAt: z.string().nullable(),
526
+ updatedAt: z.string().nullable()
527
+ }),
528
+ latestSnapshot: z.object({
529
+ snapshotId: z.string(),
530
+ detectedVersion: z.string().nullable(),
531
+ createdAt: z.string(),
532
+ pageCount: z.number().int().nonnegative()
533
+ }).nullable(),
534
+ recentLearnings: z.array(routingLearningSchema)
535
+ })
536
+ }
537
+ );
538
+ registerAiocsTool(
539
+ "source_context_show",
540
+ {
541
+ title: "Source context show",
542
+ description: "Show curated local source context for a source.",
543
+ inputSchema: z.object({
544
+ sourceId: z.string()
545
+ }),
546
+ outputSchema: z.object({
547
+ sourceId: z.string(),
548
+ context: sourceContextSchema.nullable(),
549
+ createdAt: z.string().nullable(),
550
+ updatedAt: z.string().nullable()
551
+ })
552
+ }
553
+ );
554
+ registerAiocsTool(
555
+ "source_context_upsert",
556
+ {
557
+ title: "Source context upsert",
558
+ description: "Load or update curated local source context from a JSON or YAML file.",
559
+ inputSchema: z.object({
560
+ sourceId: z.string(),
561
+ contextFile: z.string()
562
+ }),
563
+ outputSchema: z.object({
564
+ sourceId: z.string(),
565
+ context: sourceContextSchema,
566
+ contextFile: z.string(),
567
+ createdAt: z.string(),
568
+ updatedAt: z.string()
569
+ })
570
+ }
571
+ );
413
572
  registerAiocsTool(
414
573
  "fetch",
415
574
  {
@@ -469,6 +628,40 @@ registerAiocsTool(
469
628
  })
470
629
  }
471
630
  );
631
+ registerAiocsTool(
632
+ "page_list",
633
+ {
634
+ title: "Page list",
635
+ description: "List stored pages for a source snapshot with optional query and path filtering.",
636
+ inputSchema: z.object({
637
+ sourceId: z.string(),
638
+ snapshotId: z.string().optional(),
639
+ query: z.string().optional(),
640
+ pathPatterns: z.array(z.string()).optional(),
641
+ limit: z.number().int().positive().optional(),
642
+ offset: z.number().int().nonnegative().optional()
643
+ }),
644
+ outputSchema: pageListingSchema
645
+ }
646
+ );
647
+ registerAiocsTool(
648
+ "page_show",
649
+ {
650
+ title: "Page show",
651
+ description: "Read a full stored page by URL or file path.",
652
+ inputSchema: z.object({
653
+ sourceId: z.string(),
654
+ snapshotId: z.string().optional(),
655
+ url: z.string().optional(),
656
+ filePath: z.string().optional()
657
+ }),
658
+ outputSchema: z.object({
659
+ sourceId: z.string(),
660
+ snapshotId: z.string(),
661
+ page: pageSchema
662
+ })
663
+ }
664
+ );
472
665
  registerAiocsTool(
473
666
  "diff_snapshots",
474
667
  {
@@ -541,6 +734,56 @@ registerAiocsTool(
541
734
  })
542
735
  }
543
736
  );
737
+ registerAiocsTool(
738
+ "retrieve_context",
739
+ {
740
+ title: "Retrieve context",
741
+ description: "Run aiocs awareness, learned routing hints, search, and full-page reads to assemble grounded answer context.",
742
+ inputSchema: z.object({
743
+ query: z.string(),
744
+ sourceIds: z.array(z.string()).optional(),
745
+ snapshotId: z.string().optional(),
746
+ all: z.boolean().optional(),
747
+ project: z.string().optional(),
748
+ pathPatterns: z.array(z.string()).optional(),
749
+ languages: z.array(z.string()).optional(),
750
+ mode: z.enum(["auto", "lexical", "hybrid", "semantic"]).optional(),
751
+ limit: z.number().int().positive().optional(),
752
+ offset: z.number().int().nonnegative().optional(),
753
+ pageLimit: z.number().int().positive().optional()
754
+ }),
755
+ outputSchema: z.object({
756
+ query: z.string(),
757
+ modeRequested: z.enum(["auto", "lexical", "hybrid", "semantic"]),
758
+ modeUsed: z.enum(["lexical", "hybrid", "semantic"]),
759
+ sourceScope: z.array(z.string()),
760
+ sourceHints: z.array(z.object({
761
+ sourceId: z.string(),
762
+ score: z.number(),
763
+ context: sourceContextSchema,
764
+ matchedCommonLocations: z.array(z.object({
765
+ label: z.string(),
766
+ url: z.string().optional(),
767
+ filePath: z.string().optional(),
768
+ note: z.string().optional()
769
+ }))
770
+ })),
771
+ matchedLearnings: z.array(routingLearningSchema.extend({ score: z.number() })),
772
+ avoidedLearnings: z.array(routingLearningSchema.extend({ score: z.number() })),
773
+ search: z.object({
774
+ total: z.number().int().nonnegative(),
775
+ limit: z.number().int().positive(),
776
+ offset: z.number().int().nonnegative(),
777
+ hasMore: z.boolean(),
778
+ results: z.array(searchResultSchema)
779
+ }),
780
+ pages: z.array(pageSchema.extend({
781
+ sourceId: z.string(),
782
+ snapshotId: z.string()
783
+ }))
784
+ })
785
+ }
786
+ );
544
787
  registerAiocsTool(
545
788
  "show",
546
789
  {
@@ -554,6 +797,43 @@ registerAiocsTool(
554
797
  })
555
798
  }
556
799
  );
800
+ registerAiocsTool(
801
+ "learning_save",
802
+ {
803
+ title: "Learning save",
804
+ description: "Save a discovery or negative routing learning for future retrieval hints.",
805
+ inputSchema: z.object({
806
+ sourceId: z.string(),
807
+ learningType: z.enum(["discovery", "negative"]),
808
+ intent: z.string(),
809
+ snapshotId: z.string().optional(),
810
+ pageUrl: z.string().optional(),
811
+ filePath: z.string().optional(),
812
+ title: z.string().optional(),
813
+ note: z.string().optional(),
814
+ searchTerms: z.array(z.string()).optional()
815
+ }),
816
+ outputSchema: z.object({
817
+ learning: routingLearningSchema
818
+ })
819
+ }
820
+ );
821
+ registerAiocsTool(
822
+ "learning_list",
823
+ {
824
+ title: "Learning list",
825
+ description: "List saved routing learnings, optionally filtered by source, type, or intent substring.",
826
+ inputSchema: z.object({
827
+ sourceId: z.string().optional(),
828
+ learningType: z.enum(["discovery", "negative"]).optional(),
829
+ intentQuery: z.string().optional(),
830
+ limit: z.number().int().positive().optional()
831
+ }),
832
+ outputSchema: z.object({
833
+ learnings: z.array(routingLearningSchema)
834
+ })
835
+ }
836
+ );
557
837
  registerAiocsTool(
558
838
  "embeddings_status",
559
839
  {