@ainyc/canonry 2.14.2 → 2.16.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.
@@ -1,5 +1,4 @@
1
1
  import {
2
- AGENT_MEMORY_KEY_MAX_LENGTH,
3
2
  AGENT_MEMORY_VALUE_MAX_BYTES,
4
3
  AGENT_PROVIDER_IDS,
5
4
  AgentProviderIds,
@@ -21,6 +20,7 @@ import {
21
20
  authRequired,
22
21
  brandKeyFromText,
23
22
  buildRunErrorFromMessages,
23
+ canonryMcpTools,
24
24
  categorizeSource,
25
25
  categoryLabel,
26
26
  competitorBatchRequestSchema,
@@ -60,7 +60,7 @@ import {
60
60
  visibilityStateFromAnswerMentioned,
61
61
  windowCutoff,
62
62
  wordpressEnvSchema
63
- } from "./chunk-7VWSR5F6.js";
63
+ } from "./chunk-HNVRN5QL.js";
64
64
  import {
65
65
  IntelligenceService,
66
66
  agentMemory,
@@ -5547,6 +5547,73 @@ var canonryLocalRouteCatalog = [
5547
5547
  404: { description: "Project not found." }
5548
5548
  }
5549
5549
  },
5550
+ {
5551
+ method: "get",
5552
+ path: "/api/v1/projects/{name}/agent/memory",
5553
+ summary: "List durable Aero memory entries for a project",
5554
+ description: "Returns the project-scoped agent_memory rows newest-first. Includes both operator-authored notes (source `user`/`aero`) and LLM-authored compaction summaries (source `compaction`, key prefix `compaction:`). The N most-recent rows are also injected into the system prompt at every new session start.",
5555
+ tags: ["agent"],
5556
+ parameters: [nameParameter],
5557
+ responses: {
5558
+ 200: { description: "Memory entries returned." },
5559
+ 404: { description: "Project not found." }
5560
+ }
5561
+ },
5562
+ {
5563
+ method: "put",
5564
+ path: "/api/v1/projects/{name}/agent/memory",
5565
+ summary: "Upsert a durable Aero memory entry",
5566
+ description: "Creates or replaces a project-scoped note (max 2 KB, max 128-char key). Same key replaces the prior value. Keys with the reserved `compaction:` prefix are rejected \u2014 that namespace is owned by transcript compaction.",
5567
+ tags: ["agent"],
5568
+ parameters: [nameParameter],
5569
+ requestBody: {
5570
+ required: true,
5571
+ content: {
5572
+ "application/json": {
5573
+ schema: {
5574
+ type: "object",
5575
+ required: ["key", "value"],
5576
+ properties: {
5577
+ key: { type: "string", description: "Stable identifier for this note (max 128 chars)." },
5578
+ value: { type: "string", description: "Plain-text note body (max 2 KB)." }
5579
+ }
5580
+ }
5581
+ }
5582
+ }
5583
+ },
5584
+ responses: {
5585
+ 200: { description: "Entry upserted." },
5586
+ 400: { description: "Validation failed (key length, value size, reserved prefix)." },
5587
+ 404: { description: "Project not found." }
5588
+ }
5589
+ },
5590
+ {
5591
+ method: "delete",
5592
+ path: "/api/v1/projects/{name}/agent/memory",
5593
+ summary: "Delete a durable Aero memory entry",
5594
+ description: "Removes a single project-scoped note by key. Returns `status: missing` (non-error) when the key never existed. Keys with the reserved `compaction:` prefix are rejected \u2014 those notes are pruned automatically.",
5595
+ tags: ["agent"],
5596
+ parameters: [nameParameter],
5597
+ requestBody: {
5598
+ required: true,
5599
+ content: {
5600
+ "application/json": {
5601
+ schema: {
5602
+ type: "object",
5603
+ required: ["key"],
5604
+ properties: {
5605
+ key: { type: "string", description: "Exact key of the note to remove." }
5606
+ }
5607
+ }
5608
+ }
5609
+ }
5610
+ },
5611
+ responses: {
5612
+ 200: { description: "Entry removed or already absent." },
5613
+ 400: { description: "Validation failed (reserved prefix)." },
5614
+ 404: { description: "Project not found." }
5615
+ }
5616
+ },
5550
5617
  {
5551
5618
  method: "get",
5552
5619
  path: "/api/v1/projects/{name}/agent/providers",
@@ -8848,6 +8915,10 @@ async function ga4Routes(app, opts) {
8848
8915
  GROUP BY date, source, medium
8849
8916
  )`
8850
8917
  ).get();
8918
+ const aiBySession = app.db.select({
8919
+ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
8920
+ users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
8921
+ }).from(gaAiReferrals).where(and8(...aiConditions, eq19(gaAiReferrals.sourceDimension, "session"))).get();
8851
8922
  const socialReferrals = app.db.select({
8852
8923
  source: gaSocialReferrals.source,
8853
8924
  medium: gaSocialReferrals.medium,
@@ -8883,6 +8954,8 @@ async function ga4Routes(app, opts) {
8883
8954
  })),
8884
8955
  aiSessionsDeduped: aiDeduped?.sessions ?? 0,
8885
8956
  aiUsersDeduped: aiDeduped?.users ?? 0,
8957
+ aiSessionsBySession: aiBySession?.sessions ?? 0,
8958
+ aiUsersBySession: aiBySession?.users ?? 0,
8886
8959
  socialReferrals: socialReferrals.map((r) => ({
8887
8960
  source: r.source,
8888
8961
  medium: r.medium,
@@ -8894,6 +8967,7 @@ async function ga4Routes(app, opts) {
8894
8967
  socialUsers: socialTotals?.users ?? 0,
8895
8968
  organicSharePct: total > 0 ? Math.round((summaryRow?.totalOrganicSessions ?? 0) / total * 100) : 0,
8896
8969
  aiSharePct: total > 0 ? Math.round((aiDeduped?.sessions ?? 0) / total * 100) : 0,
8970
+ aiSharePctBySession: total > 0 ? Math.round((aiBySession?.sessions ?? 0) / total * 100) : 0,
8897
8971
  directSharePct: total > 0 ? Math.round(totalDirectSessions / total * 100) : 0,
8898
8972
  socialSharePct: total > 0 ? Math.round((socialTotals?.sessions ?? 0) / total * 100) : 0,
8899
8973
  lastSyncedAt: latestSync?.syncedAt ?? null,
@@ -9013,12 +9087,13 @@ async function ga4Routes(app, opts) {
9013
9087
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
9014
9088
  const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9015
9089
  const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9016
- const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(max_sessions), 0)` }).from(sql5`(
9017
- SELECT date, source, medium, MAX(sessions) AS max_sessions
9018
- FROM ga_ai_referrals
9019
- WHERE project_id = ${project.id} AND date >= ${from} AND date < ${to}
9020
- GROUP BY date, source, medium
9021
- )`).get();
9090
+ const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9091
+ const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9092
+ eq19(gaAiReferrals.projectId, project.id),
9093
+ sql5`${gaAiReferrals.date} >= ${from}`,
9094
+ sql5`${gaAiReferrals.date} < ${to}`,
9095
+ eq19(gaAiReferrals.sourceDimension, "session")
9096
+ )).get();
9022
9097
  const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(eq19(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
9023
9098
  const todayStr = fmt(today);
9024
9099
  const buildTrend = (sum) => {
@@ -9028,18 +9103,18 @@ async function ga4Routes(app, opts) {
9028
9103
  const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
9029
9104
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
9030
9105
  };
9031
- const aiSourceCurrent = app.db.select({ source: sql5`source`, sessions: sql5`COALESCE(SUM(max_sessions), 0)` }).from(sql5`(
9032
- SELECT date, source, medium, MAX(sessions) AS max_sessions
9033
- FROM ga_ai_referrals
9034
- WHERE project_id = ${project.id} AND date >= ${daysAgo2(7)} AND date < ${todayStr}
9035
- GROUP BY date, source, medium
9036
- )`).groupBy(sql5`source`).all();
9037
- const aiSourcePrev = app.db.select({ source: sql5`source`, sessions: sql5`COALESCE(SUM(max_sessions), 0)` }).from(sql5`(
9038
- SELECT date, source, medium, MAX(sessions) AS max_sessions
9039
- FROM ga_ai_referrals
9040
- WHERE project_id = ${project.id} AND date >= ${daysAgo2(14)} AND date < ${daysAgo2(7)}
9041
- GROUP BY date, source, medium
9042
- )`).groupBy(sql5`source`).all();
9106
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9107
+ eq19(gaAiReferrals.projectId, project.id),
9108
+ sql5`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
9109
+ sql5`${gaAiReferrals.date} < ${todayStr}`,
9110
+ eq19(gaAiReferrals.sourceDimension, "session")
9111
+ )).groupBy(gaAiReferrals.source).all();
9112
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9113
+ eq19(gaAiReferrals.projectId, project.id),
9114
+ sql5`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
9115
+ sql5`${gaAiReferrals.date} < ${daysAgo2(7)}`,
9116
+ eq19(gaAiReferrals.sourceDimension, "session")
9117
+ )).groupBy(gaAiReferrals.source).all();
9043
9118
  const findBiggestMover = (current, prev) => {
9044
9119
  const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
9045
9120
  let mover = null;
@@ -9061,6 +9136,7 @@ async function ga4Routes(app, opts) {
9061
9136
  organic: buildTrend(sumOrganic),
9062
9137
  ai: buildTrend(sumAi),
9063
9138
  social: buildTrend(sumSocial),
9139
+ direct: buildTrend(sumDirect),
9064
9140
  aiBiggestMover: findBiggestMover(aiSourceCurrent, aiSourcePrev),
9065
9141
  socialBiggestMover: findBiggestMover(socialSourceCurrent, socialSourcePrev)
9066
9142
  };
@@ -16670,8 +16746,138 @@ function buildSkillDocTools() {
16670
16746
  ];
16671
16747
  }
16672
16748
 
16673
- // src/agent/tools.ts
16749
+ // src/agent/mcp-to-agent-tool.ts
16674
16750
  import { Type as Type2 } from "@sinclair/typebox";
16751
+ var MAX_TOOL_RESULT_CHARS = 2e4;
16752
+ function truncate(json) {
16753
+ if (json.length <= MAX_TOOL_RESULT_CHARS) return json;
16754
+ return json.slice(0, MAX_TOOL_RESULT_CHARS) + "\n... (truncated \u2014 result too large)";
16755
+ }
16756
+ function textResult2(details) {
16757
+ return {
16758
+ content: [{ type: "text", text: truncate(JSON.stringify(details, null, 2)) }],
16759
+ details
16760
+ };
16761
+ }
16762
+ function stripProjectFromJsonSchema(jsonSchema) {
16763
+ if (!jsonSchema || typeof jsonSchema !== "object") {
16764
+ return { schema: jsonSchema, hadProject: false };
16765
+ }
16766
+ const obj = jsonSchema;
16767
+ const properties = obj.properties;
16768
+ if (!properties || typeof properties !== "object" || !("project" in properties)) {
16769
+ return { schema: jsonSchema, hadProject: false };
16770
+ }
16771
+ const { project: _project, ...remainingProps } = properties;
16772
+ const required = Array.isArray(obj.required) ? obj.required.filter((name) => name !== "project") : obj.required;
16773
+ const stripped = { ...obj, properties: remainingProps };
16774
+ if (required === void 0) {
16775
+ delete stripped.required;
16776
+ } else {
16777
+ stripped.required = required;
16778
+ }
16779
+ return { schema: stripped, hadProject: true };
16780
+ }
16781
+ function mcpToAgentTool(tool, ctx) {
16782
+ const { schema: visibleSchema, hadProject } = stripProjectFromJsonSchema(tool.inputJsonSchema);
16783
+ const parameters = Type2.Unsafe(visibleSchema);
16784
+ const execute = async (_toolCallId, params) => {
16785
+ const handlerInput = hadProject ? { ...params, project: ctx.projectName } : params;
16786
+ const result = await tool.handler(ctx.client, handlerInput);
16787
+ return textResult2(result);
16788
+ };
16789
+ return {
16790
+ name: tool.name,
16791
+ label: tool.title,
16792
+ description: tool.description,
16793
+ parameters,
16794
+ execute
16795
+ };
16796
+ }
16797
+ var AERO_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
16798
+ "canonry_agent_clear"
16799
+ ]);
16800
+ function buildMcpAgentTools(registry, ctx, opts = {}) {
16801
+ return registry.filter((tool) => !AERO_EXCLUDED_MCP_TOOLS.has(tool.name)).filter((tool) => opts.readOnly ? tool.access === "read" : true).map((tool) => mcpToAgentTool(tool, ctx));
16802
+ }
16803
+
16804
+ // src/agent/tools.ts
16805
+ function buildReadTools(ctx) {
16806
+ return buildMcpAgentTools(canonryMcpTools, ctx, { readOnly: true });
16807
+ }
16808
+ function buildAllTools(ctx) {
16809
+ return buildMcpAgentTools(canonryMcpTools, ctx);
16810
+ }
16811
+
16812
+ // src/agent/session.ts
16813
+ var builtinsRegistered = false;
16814
+ function ensureBuiltinsRegistered() {
16815
+ if (!builtinsRegistered) {
16816
+ registerBuiltInApiProviders();
16817
+ validateAgentProviderRegistry();
16818
+ builtinsRegistered = true;
16819
+ }
16820
+ }
16821
+ function loadAeroSystemPrompt(pkgDir) {
16822
+ const skillDir = resolveAeroSkillDir(pkgDir);
16823
+ const skillBody = fs11.readFileSync(path13.join(skillDir, "SKILL.md"), "utf-8");
16824
+ const soulPath = path13.join(skillDir, "soul.md");
16825
+ if (!fs11.existsSync(soulPath)) return skillBody;
16826
+ const soulBody = fs11.readFileSync(soulPath, "utf-8");
16827
+ return `${soulBody.trimEnd()}
16828
+
16829
+ ---
16830
+
16831
+ ${skillBody}`;
16832
+ }
16833
+ function missingProviderMessage() {
16834
+ const configHints = agentProvidersByPriority().join(", ");
16835
+ const envHints = agentProvidersByPriority().map((p) => `${AGENT_PROVIDERS[p].piAiProvider.toUpperCase()}_API_KEY`).join(" / ");
16836
+ return `No agent LLM provider configured. Add an API key for one of: ${configHints} in ~/.canonry/config.yaml, or export ${envHints}.`;
16837
+ }
16838
+ function detectAgentProvider(config) {
16839
+ for (const provider of agentProvidersByPriority()) {
16840
+ if (resolveApiKeyFor(provider, config)) return provider;
16841
+ }
16842
+ return void 0;
16843
+ }
16844
+ function resolveAeroModel(provider, modelId) {
16845
+ ensureBuiltinsRegistered();
16846
+ return resolveModelForProvider(provider, modelId);
16847
+ }
16848
+ function buildApiKeyResolver(config) {
16849
+ return (piAiProvider) => resolveApiKeyFor(piAiProvider, config);
16850
+ }
16851
+ function createAeroSession(opts) {
16852
+ const systemPrompt = opts.systemPromptOverride ?? loadAeroSystemPrompt();
16853
+ const provider = opts.provider ?? detectAgentProvider(opts.config);
16854
+ if (!provider) throw new Error(missingProviderMessage());
16855
+ const model = resolveAeroModel(provider, opts.modelId);
16856
+ const toolScope = opts.toolScope ?? "all";
16857
+ const toolCtx = {
16858
+ client: opts.client,
16859
+ projectName: opts.projectName
16860
+ };
16861
+ const stateTools = toolScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
16862
+ const defaultTools = [...stateTools, ...buildSkillDocTools()];
16863
+ const tools = opts.tools ?? defaultTools;
16864
+ return new Agent({
16865
+ initialState: {
16866
+ systemPrompt,
16867
+ model,
16868
+ tools,
16869
+ ...opts.initialMessages ? { messages: opts.initialMessages } : {}
16870
+ },
16871
+ streamFn: opts.streamFn,
16872
+ getApiKey: buildApiKeyResolver(opts.config)
16873
+ });
16874
+ }
16875
+ function resolveSessionProviderAndModel(config, opts) {
16876
+ const provider = opts?.provider ?? detectAgentProvider(config);
16877
+ if (!provider) throw new Error(missingProviderMessage());
16878
+ const modelId = opts?.modelId ?? getAgentProvider(provider).defaultModel;
16879
+ return { provider, modelId };
16880
+ }
16675
16881
 
16676
16882
  // src/agent/memory-store.ts
16677
16883
  import crypto26 from "crypto";
@@ -16770,556 +16976,6 @@ function writeCompactionNote(db, args) {
16770
16976
  return inserted;
16771
16977
  }
16772
16978
 
16773
- // src/agent/tools.ts
16774
- var MAX_TOOL_RESULT_CHARS = 2e4;
16775
- function truncate(json) {
16776
- if (json.length <= MAX_TOOL_RESULT_CHARS) return json;
16777
- return json.slice(0, MAX_TOOL_RESULT_CHARS) + "\n... (truncated \u2014 result too large)";
16778
- }
16779
- function textResult2(details) {
16780
- return {
16781
- content: [{ type: "text", text: truncate(JSON.stringify(details, null, 2)) }],
16782
- details
16783
- };
16784
- }
16785
- var StatusSchema = Type2.Object({
16786
- runLimit: Type2.Optional(
16787
- Type2.Number({
16788
- description: "Max recent runs to include. Default 5.",
16789
- minimum: 1,
16790
- maximum: 50
16791
- })
16792
- )
16793
- });
16794
- function buildGetStatusTool(ctx) {
16795
- return {
16796
- name: "get_status",
16797
- label: "Get status",
16798
- description: "Current project overview with its most recent runs.",
16799
- parameters: StatusSchema,
16800
- execute: async (_toolCallId, params) => {
16801
- const runLimit = params.runLimit ?? 5;
16802
- const [project, runs2] = await Promise.all([
16803
- ctx.client.getProject(ctx.projectName),
16804
- ctx.client.listRuns(ctx.projectName, runLimit)
16805
- ]);
16806
- return textResult2({ project, runs: runs2 });
16807
- }
16808
- };
16809
- }
16810
- var HealthSchema = Type2.Object({});
16811
- function buildGetHealthTool(ctx) {
16812
- return {
16813
- name: "get_health",
16814
- label: "Get health",
16815
- description: 'Latest visibility health snapshot including overall cited rate, pair counts, and per-provider breakdown. Returns `status: "no-data"` with `reason: "no-runs-yet"` and zeroed metrics for projects with no successful runs yet.',
16816
- parameters: HealthSchema,
16817
- execute: async () => {
16818
- const health = await ctx.client.getHealth(ctx.projectName);
16819
- return textResult2(health);
16820
- }
16821
- };
16822
- }
16823
- var TimelineSchema = Type2.Object({
16824
- keyword: Type2.Optional(
16825
- Type2.String({
16826
- description: "Restrict the timeline to a single keyword. Omit to return all keywords."
16827
- })
16828
- )
16829
- });
16830
- function buildGetTimelineTool(ctx) {
16831
- return {
16832
- name: "get_timeline",
16833
- label: "Get timeline",
16834
- description: "Per-keyword citation timeline showing how visibility evolved across runs. Use to identify regressions, emerging citations, or competitor movement.",
16835
- parameters: TimelineSchema,
16836
- execute: async (_toolCallId, params) => {
16837
- const timeline = await ctx.client.getTimeline(ctx.projectName);
16838
- const filtered = params.keyword ? timeline.filter((row) => row.keyword === params.keyword) : timeline;
16839
- return textResult2(filtered);
16840
- }
16841
- };
16842
- }
16843
- var InsightsSchema = Type2.Object({
16844
- includeDismissed: Type2.Optional(
16845
- Type2.Boolean({
16846
- description: "Include dismissed insights. Default false (only active insights)."
16847
- })
16848
- ),
16849
- runId: Type2.Optional(
16850
- Type2.String({
16851
- description: "Restrict insights to a specific run id. Omit for all runs."
16852
- })
16853
- )
16854
- });
16855
- function buildGetInsightsTool(ctx) {
16856
- return {
16857
- name: "get_insights",
16858
- label: "Get insights",
16859
- description: "Insights produced by the canonry intelligence engine \u2014 regressions, gains, and opportunities with cause/recommendation metadata. Query this before re-deriving conclusions from raw timeline data.",
16860
- parameters: InsightsSchema,
16861
- execute: async (_toolCallId, params) => {
16862
- const insights2 = await ctx.client.getInsights(ctx.projectName, {
16863
- dismissed: params.includeDismissed,
16864
- runId: params.runId
16865
- });
16866
- return textResult2(insights2);
16867
- }
16868
- };
16869
- }
16870
- var KeywordsSchema = Type2.Object({});
16871
- function buildListKeywordsTool(ctx) {
16872
- return {
16873
- name: "list_keywords",
16874
- label: "List keywords",
16875
- description: "All keywords currently tracked for this project.",
16876
- parameters: KeywordsSchema,
16877
- execute: async () => {
16878
- const keywords2 = await ctx.client.listKeywords(ctx.projectName);
16879
- return textResult2(keywords2);
16880
- }
16881
- };
16882
- }
16883
- var CompetitorsSchema = Type2.Object({});
16884
- function buildListCompetitorsTool(ctx) {
16885
- return {
16886
- name: "list_competitors",
16887
- label: "List competitors",
16888
- description: "Competitor domains tracked alongside this project for side-by-side comparison.",
16889
- parameters: CompetitorsSchema,
16890
- execute: async () => {
16891
- const competitors2 = await ctx.client.listCompetitors(ctx.projectName);
16892
- return textResult2(competitors2);
16893
- }
16894
- };
16895
- }
16896
- var ContentTargetsSchema = Type2.Object({
16897
- limit: Type2.Optional(
16898
- Type2.Number({
16899
- description: "Max rows. Defaults to all. Use a small number (3\u201310) when summarizing for the user."
16900
- })
16901
- ),
16902
- includeInProgress: Type2.Optional(
16903
- Type2.Boolean({
16904
- description: "Include rows that already have an in-flight tracked action. Default false."
16905
- })
16906
- )
16907
- });
16908
- function buildGetContentTargetsTool(ctx) {
16909
- return {
16910
- name: "get_content_targets",
16911
- label: "Get content targets",
16912
- description: "Ranked, action-typed content opportunities. Each row is `{query, action \u2208 create|expand|refresh|add-schema, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence}`. Use this to recommend which post the user should write/refresh next.",
16913
- parameters: ContentTargetsSchema,
16914
- execute: async (_toolCallId, params) => {
16915
- const response = await ctx.client.getContentTargets(ctx.projectName, {
16916
- limit: params.limit,
16917
- includeInProgress: params.includeInProgress === true
16918
- });
16919
- return textResult2(response);
16920
- }
16921
- };
16922
- }
16923
- var ContentSourcesSchema = Type2.Object({});
16924
- function buildGetContentSourcesTool(ctx) {
16925
- return {
16926
- name: "get_grounding_sources",
16927
- label: "Get grounding sources",
16928
- description: "URL-level competitive grounding-source map. Per query, lists every URL the LLM cited (our domain vs competitors), with citation count and providers. Read this to understand what specific competitor URL is winning a query.",
16929
- parameters: ContentSourcesSchema,
16930
- execute: async () => {
16931
- const response = await ctx.client.getContentSources(ctx.projectName);
16932
- return textResult2(response);
16933
- }
16934
- };
16935
- }
16936
- var ContentGapsSchema = Type2.Object({});
16937
- function buildGetContentGapsTool(ctx) {
16938
- return {
16939
- name: "get_content_gaps",
16940
- label: "Get content gaps",
16941
- description: 'Queries where competitors are cited but our domain is not. Ranked by miss rate. The blunt-instrument view of "what competitors are winning that we are not." Use `get_content_targets` for action-typed recommendations on the same data.',
16942
- parameters: ContentGapsSchema,
16943
- execute: async () => {
16944
- const response = await ctx.client.getContentGaps(ctx.projectName);
16945
- return textResult2(response);
16946
- }
16947
- };
16948
- }
16949
- var RunDetailSchema = Type2.Object({
16950
- runId: Type2.String({
16951
- description: "Run id (UUID) to fetch. Typically obtained from get_status runs[].id."
16952
- })
16953
- });
16954
- function buildGetRunTool(ctx) {
16955
- return {
16956
- name: "get_run",
16957
- label: "Get run detail",
16958
- description: "Full detail for a specific run including per-keyword snapshots, error messages, and provider breakdown. Use to investigate failed runs or drill into a particular sweep.",
16959
- parameters: RunDetailSchema,
16960
- execute: async (_toolCallId, params) => {
16961
- const run = await ctx.client.getRun(params.runId);
16962
- return textResult2(run);
16963
- }
16964
- };
16965
- }
16966
- var BacklinksSchema = Type2.Object({
16967
- limit: Type2.Optional(
16968
- Type2.Number({
16969
- description: "Max linking-domain rows to include. Default 50, max 200.",
16970
- minimum: 1,
16971
- maximum: 200
16972
- })
16973
- ),
16974
- release: Type2.Optional(
16975
- Type2.String({
16976
- description: "Common Crawl release id (e.g., cc-main-2026-jan-feb-mar). Omit for the most recent release with data."
16977
- })
16978
- )
16979
- });
16980
- function buildListBacklinksTool(ctx) {
16981
- return {
16982
- name: "list_backlinks",
16983
- label: "List backlinks",
16984
- description: "Backlink summary and top linking domains from the most recent ready Common Crawl release. Off-site authority signal that correlates with citation likelihood. Returns null summary when no release sync has completed for this workspace.",
16985
- parameters: BacklinksSchema,
16986
- execute: async (_toolCallId, params) => {
16987
- const response = await ctx.client.backlinksDomains(ctx.projectName, {
16988
- limit: params.limit ?? 50,
16989
- release: params.release
16990
- });
16991
- return textResult2(response);
16992
- }
16993
- };
16994
- }
16995
- var RecallSchema = Type2.Object({
16996
- limit: Type2.Optional(
16997
- Type2.Number({
16998
- description: "Max notes to return, ordered newest-first. Default 50. Max 100.",
16999
- minimum: 1,
17000
- maximum: 100
17001
- })
17002
- )
17003
- });
17004
- function buildRecallTool(ctx) {
17005
- return {
17006
- name: "recall",
17007
- label: "Recall memory",
17008
- description: "Read project-scoped durable notes Aero has stored via `remember` (plus compaction summaries). Returns entries newest-first. The N most-recent entries are also injected into the system prompt at session start, so you usually do not need to call this \u2014 reach for it when you need older context or the full note value.",
17009
- parameters: RecallSchema,
17010
- execute: async (_toolCallId, params) => {
17011
- const entries = listMemoryEntries(ctx.db, ctx.projectId, { limit: params.limit ?? 50 });
17012
- return textResult2({ entries });
17013
- }
17014
- };
17015
- }
17016
- function buildReadTools(ctx) {
17017
- return [
17018
- buildGetStatusTool(ctx),
17019
- buildGetHealthTool(ctx),
17020
- buildGetTimelineTool(ctx),
17021
- buildGetInsightsTool(ctx),
17022
- buildListKeywordsTool(ctx),
17023
- buildListCompetitorsTool(ctx),
17024
- buildGetRunTool(ctx),
17025
- buildRecallTool(ctx),
17026
- buildListBacklinksTool(ctx),
17027
- buildGetContentTargetsTool(ctx),
17028
- buildGetContentSourcesTool(ctx),
17029
- buildGetContentGapsTool(ctx)
17030
- ];
17031
- }
17032
- var RunSweepSchema = Type2.Object({
17033
- providers: Type2.Optional(
17034
- Type2.Array(Type2.String(), {
17035
- description: "Subset of providers to run. Omit to use every configured provider on the project."
17036
- })
17037
- ),
17038
- noLocation: Type2.Optional(
17039
- Type2.Boolean({
17040
- description: "Run without a location context. Default: use the project default location."
17041
- })
17042
- )
17043
- });
17044
- function buildRunSweepTool(ctx) {
17045
- return {
17046
- name: "run_sweep",
17047
- label: "Trigger sweep",
17048
- description: "Trigger a new answer-visibility sweep for this project across configured AI providers. Returns the run id(s). Use when fresh citation data is needed.",
17049
- parameters: RunSweepSchema,
17050
- execute: async (_toolCallId, params) => {
17051
- const body = {};
17052
- if (params.providers?.length) body.providers = params.providers;
17053
- if (params.noLocation) body.noLocation = true;
17054
- const result = await ctx.client.triggerRun(ctx.projectName, body);
17055
- return textResult2(result);
17056
- }
17057
- };
17058
- }
17059
- var DismissInsightSchema = Type2.Object({
17060
- insightId: Type2.String({
17061
- description: "Insight id to dismiss. Obtain from get_insights details[].id."
17062
- })
17063
- });
17064
- function buildDismissInsightTool(ctx) {
17065
- return {
17066
- name: "dismiss_insight",
17067
- label: "Dismiss insight",
17068
- description: "Mark an insight as dismissed so it no longer surfaces in active insight lists. Reversible via the dashboard.",
17069
- parameters: DismissInsightSchema,
17070
- execute: async (_toolCallId, params) => {
17071
- const result = await ctx.client.dismissInsight(ctx.projectName, params.insightId);
17072
- return textResult2(result);
17073
- }
17074
- };
17075
- }
17076
- var AddKeywordsSchema = Type2.Object({
17077
- keywords: Type2.Array(Type2.String(), {
17078
- minItems: 1,
17079
- description: "Keywords to add to the tracking list. Duplicates against existing keywords are ignored server-side."
17080
- })
17081
- });
17082
- function buildAddKeywordsTool(ctx) {
17083
- return {
17084
- name: "add_keywords",
17085
- label: "Add keywords",
17086
- description: "Append keywords to the project tracking list. Additive only \u2014 existing keywords are preserved. Use exact phrasing you want tracked.",
17087
- parameters: AddKeywordsSchema,
17088
- execute: async (_toolCallId, params) => {
17089
- await ctx.client.appendKeywords(ctx.projectName, params.keywords);
17090
- return textResult2({ added: params.keywords });
17091
- }
17092
- };
17093
- }
17094
- var AddCompetitorsSchema = Type2.Object({
17095
- domains: Type2.Array(Type2.String(), {
17096
- minItems: 1,
17097
- description: 'Competitor domains to track. Provide bare domains (e.g. "example.com"), not URLs.'
17098
- })
17099
- });
17100
- function buildAddCompetitorsTool(ctx) {
17101
- return {
17102
- name: "add_competitors",
17103
- label: "Add competitors",
17104
- description: "Append competitor domains to the project. Existing competitors are skipped by the API.",
17105
- parameters: AddCompetitorsSchema,
17106
- execute: async (_toolCallId, params) => {
17107
- const existing = await ctx.client.listCompetitors(ctx.projectName);
17108
- const existingDomains = new Set(existing.map((c) => c.domain));
17109
- const newDomains = params.domains.filter((d) => !existingDomains.has(d));
17110
- if (newDomains.length === 0) {
17111
- return textResult2({ added: [], alreadyTracked: params.domains });
17112
- }
17113
- await ctx.client.appendCompetitors(ctx.projectName, newDomains);
17114
- return textResult2({ added: newDomains, alreadyTracked: params.domains.filter((d) => existingDomains.has(d)) });
17115
- }
17116
- };
17117
- }
17118
- var UpdateScheduleSchema = Type2.Object({
17119
- cron: Type2.Optional(
17120
- Type2.String({ description: 'Cron expression (e.g. "0 */6 * * *"). Provide cron OR preset, not both.' })
17121
- ),
17122
- preset: Type2.Optional(
17123
- Type2.String({ description: 'Preset keyword (e.g. "daily", "hourly"). Provide cron OR preset, not both.' })
17124
- ),
17125
- timezone: Type2.Optional(Type2.String({ description: 'IANA timezone. Default: "UTC".' })),
17126
- enabled: Type2.Optional(
17127
- Type2.Boolean({ description: "Whether the schedule is active. Default: true." })
17128
- ),
17129
- providers: Type2.Optional(
17130
- Type2.Array(Type2.String(), {
17131
- description: "Providers to run on each scheduled sweep. Omit to use all configured providers."
17132
- })
17133
- )
17134
- });
17135
- function buildUpdateScheduleTool(ctx) {
17136
- return {
17137
- name: "update_schedule",
17138
- label: "Update schedule",
17139
- description: "Create or update the recurring sweep schedule for this project. Provide exactly one of `cron` (expression) or `preset` (keyword). Fully replaces any existing schedule.",
17140
- parameters: UpdateScheduleSchema,
17141
- execute: async (_toolCallId, params) => {
17142
- if (params.cron && params.preset || !params.cron && !params.preset) {
17143
- throw new Error("update_schedule: provide exactly one of `cron` or `preset`");
17144
- }
17145
- const body = {};
17146
- if (params.cron) body.cron = params.cron;
17147
- if (params.preset) body.preset = params.preset;
17148
- if (params.timezone) body.timezone = params.timezone;
17149
- if (params.enabled !== void 0) body.enabled = params.enabled;
17150
- if (params.providers?.length) body.providers = params.providers;
17151
- const result = await ctx.client.putSchedule(ctx.projectName, body);
17152
- return textResult2(result);
17153
- }
17154
- };
17155
- }
17156
- var AttachAgentWebhookSchema = Type2.Object({
17157
- url: Type2.String({
17158
- description: "External agent webhook URL. Canonry will POST run.completed, insight.critical, insight.high, and citation.gained events to it."
17159
- })
17160
- });
17161
- function buildAttachAgentWebhookTool(ctx) {
17162
- return {
17163
- name: "attach_agent_webhook",
17164
- label: "Attach agent webhook",
17165
- description: "Register an external agent webhook for this project. Use when wiring a Claude Code / Codex / custom agent to receive canonry run and insight events. Idempotent \u2014 skips if one already exists.",
17166
- parameters: AttachAgentWebhookSchema,
17167
- execute: async (_toolCallId, params) => {
17168
- const existing = await ctx.client.listNotifications(ctx.projectName);
17169
- const hasAgent = existing.some((n) => n.source === "agent");
17170
- if (hasAgent) {
17171
- return textResult2({ status: "already-attached" });
17172
- }
17173
- const result = await ctx.client.createNotification(ctx.projectName, {
17174
- channel: "webhook",
17175
- url: params.url,
17176
- events: ["run.completed", "insight.critical", "insight.high", "citation.gained"],
17177
- source: "agent"
17178
- });
17179
- return textResult2({ status: "attached", notificationId: result.id, url: params.url });
17180
- }
17181
- };
17182
- }
17183
- var RememberSchema = Type2.Object({
17184
- key: Type2.String({
17185
- description: `Stable identifier for this note (max ${AGENT_MEMORY_KEY_MAX_LENGTH} chars). Writing the same key overwrites the prior value. Do NOT use the "${COMPACTION_KEY_PREFIX}" prefix \u2014 that namespace is reserved for transcript compaction summaries.`,
17186
- minLength: 1,
17187
- maxLength: AGENT_MEMORY_KEY_MAX_LENGTH
17188
- }),
17189
- value: Type2.String({
17190
- description: `Plain-text note to persist (max ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes). Use for durable operator preferences, migration context, or non-obvious reasoning you'll want on a future turn. Do NOT duplicate data canonry already tracks (runs, insights, timelines) \u2014 query those instead.`,
17191
- minLength: 1
17192
- })
17193
- });
17194
- function buildRememberTool(ctx) {
17195
- return {
17196
- name: "remember",
17197
- label: "Remember",
17198
- description: "Persist a project-scoped durable note visible to every future Aero session for this project. Upsert \u2014 writing the same key replaces the prior value. Capped at 2 KB per note.",
17199
- parameters: RememberSchema,
17200
- execute: async (_toolCallId, params) => {
17201
- const entry = upsertMemoryEntry(ctx.db, {
17202
- projectId: ctx.projectId,
17203
- key: params.key,
17204
- value: params.value,
17205
- source: MemorySources.aero
17206
- });
17207
- return textResult2({ status: "remembered", entry });
17208
- }
17209
- };
17210
- }
17211
- var ForgetSchema = Type2.Object({
17212
- key: Type2.String({
17213
- description: "Exact key of the note to remove. No-op (status=missing) when no note exists for that key.",
17214
- minLength: 1,
17215
- maxLength: AGENT_MEMORY_KEY_MAX_LENGTH
17216
- })
17217
- });
17218
- function buildForgetTool(ctx) {
17219
- return {
17220
- name: "forget",
17221
- label: "Forget",
17222
- description: "Delete a durable note by key. Use when a previously-remembered fact is wrong or no longer relevant.",
17223
- parameters: ForgetSchema,
17224
- execute: async (_toolCallId, params) => {
17225
- if (params.key.startsWith(COMPACTION_KEY_PREFIX)) {
17226
- throw new Error(
17227
- `cannot forget compaction notes directly \u2014 they are pruned automatically (key prefix "${COMPACTION_KEY_PREFIX}" is reserved)`
17228
- );
17229
- }
17230
- const removed = deleteMemoryEntry(ctx.db, ctx.projectId, params.key);
17231
- return textResult2({ status: removed ? "forgotten" : "missing", key: params.key });
17232
- }
17233
- };
17234
- }
17235
- function buildWriteTools(ctx) {
17236
- return [
17237
- buildRunSweepTool(ctx),
17238
- buildDismissInsightTool(ctx),
17239
- buildAddKeywordsTool(ctx),
17240
- buildAddCompetitorsTool(ctx),
17241
- buildUpdateScheduleTool(ctx),
17242
- buildAttachAgentWebhookTool(ctx),
17243
- buildRememberTool(ctx),
17244
- buildForgetTool(ctx)
17245
- ];
17246
- }
17247
- function buildAllTools(ctx) {
17248
- return [...buildReadTools(ctx), ...buildWriteTools(ctx)];
17249
- }
17250
-
17251
- // src/agent/session.ts
17252
- var builtinsRegistered = false;
17253
- function ensureBuiltinsRegistered() {
17254
- if (!builtinsRegistered) {
17255
- registerBuiltInApiProviders();
17256
- validateAgentProviderRegistry();
17257
- builtinsRegistered = true;
17258
- }
17259
- }
17260
- function loadAeroSystemPrompt(pkgDir) {
17261
- const skillDir = resolveAeroSkillDir(pkgDir);
17262
- const skillBody = fs11.readFileSync(path13.join(skillDir, "SKILL.md"), "utf-8");
17263
- const soulPath = path13.join(skillDir, "soul.md");
17264
- if (!fs11.existsSync(soulPath)) return skillBody;
17265
- const soulBody = fs11.readFileSync(soulPath, "utf-8");
17266
- return `${soulBody.trimEnd()}
17267
-
17268
- ---
17269
-
17270
- ${skillBody}`;
17271
- }
17272
- function missingProviderMessage() {
17273
- const configHints = agentProvidersByPriority().join(", ");
17274
- const envHints = agentProvidersByPriority().map((p) => `${AGENT_PROVIDERS[p].piAiProvider.toUpperCase()}_API_KEY`).join(" / ");
17275
- return `No agent LLM provider configured. Add an API key for one of: ${configHints} in ~/.canonry/config.yaml, or export ${envHints}.`;
17276
- }
17277
- function detectAgentProvider(config) {
17278
- for (const provider of agentProvidersByPriority()) {
17279
- if (resolveApiKeyFor(provider, config)) return provider;
17280
- }
17281
- return void 0;
17282
- }
17283
- function resolveAeroModel(provider, modelId) {
17284
- ensureBuiltinsRegistered();
17285
- return resolveModelForProvider(provider, modelId);
17286
- }
17287
- function buildApiKeyResolver(config) {
17288
- return (piAiProvider) => resolveApiKeyFor(piAiProvider, config);
17289
- }
17290
- function createAeroSession(opts) {
17291
- const systemPrompt = opts.systemPromptOverride ?? loadAeroSystemPrompt();
17292
- const provider = opts.provider ?? detectAgentProvider(opts.config);
17293
- if (!provider) throw new Error(missingProviderMessage());
17294
- const model = resolveAeroModel(provider, opts.modelId);
17295
- const toolScope = opts.toolScope ?? "all";
17296
- const toolCtx = {
17297
- client: opts.client,
17298
- projectName: opts.projectName,
17299
- db: opts.db,
17300
- projectId: opts.projectId
17301
- };
17302
- const stateTools = toolScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
17303
- const defaultTools = [...stateTools, ...buildSkillDocTools()];
17304
- const tools = opts.tools ?? defaultTools;
17305
- return new Agent({
17306
- initialState: {
17307
- systemPrompt,
17308
- model,
17309
- tools,
17310
- ...opts.initialMessages ? { messages: opts.initialMessages } : {}
17311
- },
17312
- streamFn: opts.streamFn,
17313
- getApiKey: buildApiKeyResolver(opts.config)
17314
- });
17315
- }
17316
- function resolveSessionProviderAndModel(config, opts) {
17317
- const provider = opts?.provider ?? detectAgentProvider(config);
17318
- if (!provider) throw new Error(missingProviderMessage());
17319
- const modelId = opts?.modelId ?? getAgentProvider(provider).defaultModel;
17320
- return { provider, modelId };
17321
- }
17322
-
17323
16979
  // src/agent/compaction.ts
17324
16980
  import { complete } from "@mariozechner/pi-ai";
17325
16981
 
@@ -17501,8 +17157,6 @@ var SessionRegistry = class {
17501
17157
  projectName,
17502
17158
  client: this.opts.client,
17503
17159
  config: this.opts.config,
17504
- db: this.opts.db,
17505
- projectId,
17506
17160
  provider: effectiveProvider,
17507
17161
  modelId: effectiveModelId,
17508
17162
  systemPromptOverride: this.buildHydratedSystemPrompt(projectId, row.systemPrompt),
@@ -17525,8 +17179,6 @@ var SessionRegistry = class {
17525
17179
  projectName,
17526
17180
  client: this.opts.client,
17527
17181
  config: this.opts.config,
17528
- db: this.opts.db,
17529
- projectId,
17530
17182
  provider,
17531
17183
  modelId,
17532
17184
  // Hydrate on the fresh path too — a brand-new session may still see
@@ -17586,7 +17238,7 @@ var SessionRegistry = class {
17586
17238
  ---
17587
17239
 
17588
17240
  <memory>
17589
- Project-scoped durable notes (newest first). Use remember/forget/recall to manage. Entries tagged [compaction] are LLM-summarized transcript slices.
17241
+ Project-scoped durable notes (newest first). Use canonry_memory_set/canonry_memory_forget/canonry_memory_list to manage. Entries tagged [compaction] are LLM-summarized transcript slices.
17590
17242
 
17591
17243
  ${lines.join("\n")}
17592
17244
  </memory>`;
@@ -17693,7 +17345,7 @@ ${lines.join("\n")}
17693
17345
  if (this.scopes.get(projectName) === wantScope) return;
17694
17346
  const projectId = this.projectIds.get(projectName) ?? this.resolveProjectId(projectName);
17695
17347
  this.projectIds.set(projectName, projectId);
17696
- const toolCtx = { client: this.opts.client, projectName, db: this.opts.db, projectId };
17348
+ const toolCtx = { client: this.opts.client, projectName };
17697
17349
  const stateTools = wantScope === "read-only" ? buildReadTools(toolCtx) : buildAllTools(toolCtx);
17698
17350
  agent.state.tools = [...stateTools, ...buildSkillDocTools()];
17699
17351
  this.scopes.set(projectName, wantScope);
@@ -18964,7 +18616,7 @@ async function createServer(opts) {
18964
18616
  if (!project) return;
18965
18617
  sessionRegistry.queueFollowUp(project.name, {
18966
18618
  role: "user",
18967
- content: `[system] Run ${runId} completed for project ${project.name}. ${insightCount} insights generated (${criticalOrHigh} critical/high). Use get_run to inspect the run and get_insights to review new findings. Surface anything notable briefly \u2014 skip chit-chat.`,
18619
+ content: `[system] Run ${runId} completed for project ${project.name}. ${insightCount} insights generated (${criticalOrHigh} critical/high). Use canonry_run_get to inspect the run and canonry_insights_list to review new findings. Surface anything notable briefly \u2014 skip chit-chat.`,
18968
18620
  timestamp: Date.now()
18969
18621
  });
18970
18622
  void sessionRegistry.drainNow(project.name);