supipowers 1.5.3 → 2.0.1
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 +14 -8
- package/bin/install.mjs +20 -5
- package/bin/install.ts +95 -0
- package/package.json +8 -4
- package/skills/context-mode/SKILL.md +17 -10
- package/skills/harness/SKILL.md +94 -0
- package/skills/ui-design/SKILL.md +63 -0
- package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
- package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
- package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
- package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
- package/skills/ultraplan-discover/SKILL.md +96 -0
- package/skills/ultraplan-intake/SKILL.md +89 -0
- package/skills/ultraplan-research/SKILL.md +129 -0
- package/skills/ultraplan-review/SKILL.md +86 -0
- package/skills/ultraplan-review-scope/SKILL.md +111 -0
- package/skills/ultraplan-review-structure/SKILL.md +120 -0
- package/skills/ultraplan-review-tdd/SKILL.md +142 -0
- package/skills/ultraplan-scout/SKILL.md +110 -0
- package/skills/ultraplan-synthesize/SKILL.md +124 -0
- package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
- package/src/ai/schema-text.ts +129 -0
- package/src/ai/structured-output.ts +274 -0
- package/src/ai/template.ts +27 -0
- package/src/bootstrap.ts +63 -28
- package/src/commands/agents.ts +131 -42
- package/src/commands/ai-review.ts +251 -30
- package/src/commands/clear.ts +434 -0
- package/src/commands/commit.ts +1 -0
- package/src/commands/config.ts +242 -44
- package/src/commands/context.ts +55 -28
- package/src/commands/doctor.ts +234 -6
- package/src/commands/fix-pr.ts +306 -131
- package/src/commands/generate.ts +111 -21
- package/src/commands/memory.ts +192 -0
- package/src/commands/model-picker.ts +28 -21
- package/src/commands/model.ts +18 -8
- package/src/commands/optimize-context.ts +408 -29
- package/src/commands/plan.ts +2 -0
- package/src/commands/qa.ts +312 -137
- package/src/commands/release.ts +259 -76
- package/src/commands/review.ts +293 -59
- package/src/commands/status.ts +200 -13
- package/src/commands/supi.ts +3 -35
- package/src/commands/ui-design.ts +394 -0
- package/src/commands/ultraplan.ts +1518 -0
- package/src/commands/update.ts +86 -0
- package/src/config/defaults.ts +62 -0
- package/src/config/loader.ts +448 -60
- package/src/config/schema.ts +108 -2
- package/src/context/optimizer.ts +25 -33
- package/src/context/rule-renderer.ts +223 -0
- package/src/context/savings.ts +258 -0
- package/src/context/startup-check.ts +380 -0
- package/src/context/startup-optimizer.ts +355 -0
- package/src/context/tokenignore.ts +146 -0
- package/src/context-mode/cache-handle.ts +49 -0
- package/src/context-mode/cache-preview.ts +71 -0
- package/src/context-mode/cache-store.ts +738 -0
- package/src/context-mode/compressor.ts +131 -26
- package/src/context-mode/dedup.ts +108 -0
- package/src/context-mode/detector.ts +35 -4
- package/src/context-mode/event-extractor.ts +14 -12
- package/src/context-mode/event-store.ts +91 -36
- package/src/context-mode/hooks.ts +798 -56
- package/src/context-mode/knowledge/store.ts +255 -11
- package/src/context-mode/memory-store.ts +325 -0
- package/src/context-mode/metrics-recorder.ts +158 -0
- package/src/context-mode/metrics-store.ts +765 -0
- package/src/context-mode/model.ts +24 -0
- package/src/context-mode/processor-keys.ts +29 -0
- package/src/context-mode/processors/build.ts +66 -0
- package/src/context-mode/processors/docker.ts +57 -0
- package/src/context-mode/processors/git.ts +111 -0
- package/src/context-mode/processors/json.ts +112 -0
- package/src/context-mode/processors/k8s.ts +67 -0
- package/src/context-mode/processors/lint.ts +67 -0
- package/src/context-mode/processors/log.ts +86 -0
- package/src/context-mode/processors/registry.ts +116 -0
- package/src/context-mode/processors/test-runner.ts +102 -0
- package/src/context-mode/processors/types.ts +20 -0
- package/src/context-mode/repomap.ts +400 -0
- package/src/context-mode/routing.ts +97 -24
- package/src/context-mode/sandbox/runners.ts +5 -1
- package/src/context-mode/snapshot-builder.ts +106 -11
- package/src/context-mode/source-hash.ts +173 -0
- package/src/context-mode/tool-name.ts +11 -0
- package/src/context-mode/tools.ts +654 -22
- package/src/context-mode/web/fetcher.ts +31 -12
- package/src/debug/logger.ts +2 -1
- package/src/deps/registry.ts +1 -1
- package/src/discipline/failure-summarizer.ts +170 -0
- package/src/discipline/failure-taxonomy.ts +131 -0
- package/src/discipline/workflow-invariants.ts +125 -0
- package/src/discovery/index.ts +31 -0
- package/src/discovery/lsp.ts +87 -0
- package/src/discovery/rank.ts +144 -0
- package/src/discovery/sources.ts +89 -0
- package/src/discovery/workflow.ts +87 -0
- package/src/docs/contracts.ts +39 -0
- package/src/docs/drift.ts +117 -87
- package/src/fix-pr/assessment.ts +200 -0
- package/src/fix-pr/contracts.ts +47 -0
- package/src/fix-pr/fetch-comments.ts +80 -0
- package/src/fix-pr/prompt-builder.ts +58 -40
- package/src/fix-pr/scripts/exec.ts +34 -0
- package/src/fix-pr/scripts/trigger-review.ts +106 -0
- package/src/fix-pr/scripts/wait-and-check.ts +108 -0
- package/src/fix-pr/types.ts +4 -0
- package/src/git/branch-finish.ts +5 -0
- package/src/git/commit-contract.ts +83 -0
- package/src/git/commit.ts +121 -184
- package/src/git/status.ts +62 -8
- package/src/harness/anti_slop/architecture-parser.ts +210 -0
- package/src/harness/anti_slop/backend-factory.ts +30 -0
- package/src/harness/anti_slop/backend.ts +140 -0
- package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
- package/src/harness/anti_slop/fallow-adapter.ts +305 -0
- package/src/harness/anti_slop/installer.ts +227 -0
- package/src/harness/anti_slop/queue.ts +216 -0
- package/src/harness/anti_slop/recommend.ts +84 -0
- package/src/harness/anti_slop/score.ts +180 -0
- package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
- package/src/harness/artifacts/agents-md.ts +88 -0
- package/src/harness/artifacts/checks-wiring.ts +57 -0
- package/src/harness/artifacts/docs-tree.ts +79 -0
- package/src/harness/artifacts/lint-configs.ts +136 -0
- package/src/harness/artifacts/review-agents.ts +67 -0
- package/src/harness/bare-entry.ts +108 -0
- package/src/harness/command.ts +1010 -0
- package/src/harness/default-agents/design.md +23 -0
- package/src/harness/default-agents/discover.md +18 -0
- package/src/harness/default-agents/implement.md +24 -0
- package/src/harness/default-agents/plan.md +19 -0
- package/src/harness/default-agents/research.md +21 -0
- package/src/harness/default-agents/validate.md +22 -0
- package/src/harness/gc/reporter.ts +28 -0
- package/src/harness/gc/runner.ts +136 -0
- package/src/harness/hooks/layer-context-inject.ts +155 -0
- package/src/harness/hooks/post-session-sweep.ts +130 -0
- package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
- package/src/harness/hooks/register.ts +118 -0
- package/src/harness/model.ts +117 -0
- package/src/harness/pipeline.ts +348 -0
- package/src/harness/project-paths.ts +235 -0
- package/src/harness/stage-runner.ts +107 -0
- package/src/harness/stages/design.ts +386 -0
- package/src/harness/stages/discover.ts +454 -0
- package/src/harness/stages/implement.ts +162 -0
- package/src/harness/stages/plan.ts +335 -0
- package/src/harness/stages/research.ts +263 -0
- package/src/harness/stages/validate.ts +684 -0
- package/src/harness/storage.ts +467 -0
- package/src/harness/tools.ts +426 -0
- package/src/lsp/bridge.ts +56 -95
- package/src/lsp/capabilities.ts +108 -0
- package/src/lsp/contracts.ts +35 -0
- package/src/lsp/detector.ts +8 -12
- package/src/markdown-frontmatter.ts +68 -0
- package/src/mempalace/bridge.ts +135 -0
- package/src/mempalace/config.ts +75 -0
- package/src/mempalace/format.ts +163 -0
- package/src/mempalace/hooks.ts +370 -0
- package/src/mempalace/installer-helper.ts +194 -0
- package/src/mempalace/python/mempalace_bridge.py +440 -0
- package/src/mempalace/runtime.ts +565 -0
- package/src/mempalace/schema.ts +268 -0
- package/src/mempalace/session-summary.ts +198 -0
- package/src/mempalace/tool.ts +186 -0
- package/src/mempalace/uv.ts +256 -0
- package/src/migrate/runner.ts +354 -0
- package/src/planning/approval-flow.ts +206 -9
- package/src/planning/plan-writer-prompt.ts +4 -3
- package/src/planning/planning-ask-tool.ts +39 -0
- package/src/planning/render-markdown.ts +74 -0
- package/src/planning/spec.ts +42 -0
- package/src/planning/system-prompt.ts +11 -8
- package/src/planning/validate.ts +84 -0
- package/src/platform/omp.ts +15 -2
- package/src/platform/system-prompt.ts +37 -0
- package/src/platform/test-utils.ts +3 -0
- package/src/platform/types.ts +6 -1
- package/src/qa/config.ts +12 -6
- package/src/qa/detect-app-type.ts +13 -6
- package/src/qa/matrix.ts +12 -6
- package/src/qa/prompt-builder.ts +28 -30
- package/src/qa/scripts/dev-server-utils.ts +72 -0
- package/src/qa/scripts/run-e2e-tests.ts +226 -0
- package/src/qa/scripts/start-dev-server.ts +138 -0
- package/src/qa/scripts/stop-dev-server.ts +77 -0
- package/src/qa/session.ts +13 -7
- package/src/quality/ai-setup.ts +27 -25
- package/src/quality/contracts.ts +34 -0
- package/src/quality/gates/ai-review.ts +20 -58
- package/src/quality/gates/command.ts +249 -46
- package/src/quality/review-gates.ts +18 -2
- package/src/quality/runner.ts +63 -22
- package/src/quality/schemas.ts +37 -2
- package/src/quality/setup.ts +96 -16
- package/src/release/changelog.ts +1 -1
- package/src/release/channels/custom.ts +13 -3
- package/src/release/channels/types.ts +5 -0
- package/src/release/contracts.ts +90 -0
- package/src/release/executor.ts +122 -45
- package/src/release/prompt.ts +18 -2
- package/src/release/targets.ts +86 -0
- package/src/release/version.ts +96 -71
- package/src/review/agent-loader.ts +221 -109
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +114 -13
- package/src/review/output.ts +12 -139
- package/src/review/runner.ts +12 -6
- package/src/review/scope.ts +144 -24
- package/src/review/types.ts +1 -20
- package/src/review/validator.ts +12 -6
- package/src/storage/fix-pr-sessions.ts +21 -14
- package/src/storage/plans.ts +14 -5
- package/src/storage/qa-sessions.ts +25 -19
- package/src/storage/reliability-metrics.ts +180 -0
- package/src/storage/reports.ts +8 -7
- package/src/storage/review-sessions.ts +55 -20
- package/src/tool-catalog/active-tool-controller.ts +164 -0
- package/src/tool-catalog/active-tool-planner.ts +212 -0
- package/src/tool-catalog/tool-groups.ts +102 -0
- package/src/types.ts +1399 -5
- package/src/ui-design/backend-adapter.ts +78 -0
- package/src/ui-design/backends/local-html.ts +82 -0
- package/src/ui-design/backends/pencil-mcp.ts +111 -0
- package/src/ui-design/components-scanner.ts +124 -0
- package/src/ui-design/config.ts +55 -0
- package/src/ui-design/pen-scanner.ts +95 -0
- package/src/ui-design/pen-selector.ts +72 -0
- package/src/ui-design/prompt-builder.ts +73 -0
- package/src/ui-design/scanner.ts +136 -0
- package/src/ui-design/session.ts +974 -0
- package/src/ui-design/system-prompt.ts +312 -0
- package/src/ui-design/tokens-scanner.ts +181 -0
- package/src/ui-design/types.ts +96 -0
- package/src/ultraplan/agent-catalog.ts +522 -0
- package/src/ultraplan/authoring/agent-catalog.ts +310 -0
- package/src/ultraplan/authoring/authoring-tools.ts +552 -0
- package/src/ultraplan/authoring/command-handlers.ts +339 -0
- package/src/ultraplan/authoring/markdown.ts +510 -0
- package/src/ultraplan/authoring/model.ts +162 -0
- package/src/ultraplan/authoring/pipeline.ts +319 -0
- package/src/ultraplan/authoring/stage-runner.ts +141 -0
- package/src/ultraplan/authoring/stages/approve.ts +249 -0
- package/src/ultraplan/authoring/stages/discover.ts +289 -0
- package/src/ultraplan/authoring/stages/intake.ts +203 -0
- package/src/ultraplan/authoring/stages/research.ts +399 -0
- package/src/ultraplan/authoring/stages/review.ts +333 -0
- package/src/ultraplan/authoring/stages/scout.ts +188 -0
- package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
- package/src/ultraplan/authoring/storage.ts +594 -0
- package/src/ultraplan/authoring/synth-gate.ts +165 -0
- package/src/ultraplan/authoring-draft.ts +653 -0
- package/src/ultraplan/authoring-persist.ts +180 -0
- package/src/ultraplan/authoring-tool.ts +608 -0
- package/src/ultraplan/authoring-wizard.ts +587 -0
- package/src/ultraplan/batch/merge.ts +98 -0
- package/src/ultraplan/batch/planner.ts +150 -0
- package/src/ultraplan/batch/presenter.ts +97 -0
- package/src/ultraplan/batch/storage.ts +420 -0
- package/src/ultraplan/batch/supervisor.ts +317 -0
- package/src/ultraplan/batch/worker.ts +26 -0
- package/src/ultraplan/batch/worktree.ts +110 -0
- package/src/ultraplan/contracts.ts +1593 -0
- package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
- package/src/ultraplan/default-agents/authoring/intake.md +12 -0
- package/src/ultraplan/default-agents/authoring/planner.md +12 -0
- package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
- package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/scout.md +12 -0
- package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
- package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-executor.md +10 -0
- package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-tester.md +10 -0
- package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-executor.md +10 -0
- package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-tester.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
- package/src/ultraplan/execution/contract.ts +71 -0
- package/src/ultraplan/execution/policy.ts +217 -0
- package/src/ultraplan/execution/runtime-tools.ts +107 -0
- package/src/ultraplan/execution/session-runner.ts +281 -0
- package/src/ultraplan/next-router.ts +85 -0
- package/src/ultraplan/presenter.ts +359 -0
- package/src/ultraplan/project-paths.ts +342 -0
- package/src/ultraplan/runtime/active-execution.ts +72 -0
- package/src/ultraplan/runtime/apply-mutation.ts +416 -0
- package/src/ultraplan/runtime/blockers.ts +243 -0
- package/src/ultraplan/runtime/hook-bridge.ts +486 -0
- package/src/ultraplan/runtime/launch-context.ts +207 -0
- package/src/ultraplan/runtime/migration.ts +524 -0
- package/src/ultraplan/runtime/normalize.ts +281 -0
- package/src/ultraplan/runtime/proof.ts +260 -0
- package/src/ultraplan/runtime/reducer.ts +416 -0
- package/src/ultraplan/runtime/repair.ts +251 -0
- package/src/ultraplan/runtime/tracker-storage.ts +368 -0
- package/src/ultraplan/session-selection.ts +291 -0
- package/src/ultraplan/storage.ts +374 -0
- package/src/utils/editor.ts +38 -0
- package/src/utils/executable.ts +80 -0
- package/src/utils/paths.ts +1 -20
- package/src/utils/shell.ts +31 -0
- package/src/visual/companion.ts +2 -1
- package/src/visual/scripts/frame-template.html +60 -0
- package/src/visual/scripts/index.js +59 -13
- package/src/visual/scripts/package.json +3 -0
- package/src/visual/start-server.ts +2 -1
- package/src/workspace/git-scope.ts +64 -0
- package/src/workspace/locks.ts +23 -0
- package/src/workspace/package-manager.ts +117 -0
- package/src/workspace/path-mapping.ts +75 -0
- package/src/workspace/project-slug.ts +92 -0
- package/src/workspace/repo-root.ts +137 -0
- package/src/workspace/selector.ts +115 -0
- package/src/workspace/state-paths.ts +118 -0
- package/src/workspace/targets.ts +313 -0
- package/src/fix-pr/scripts/diff-comments.sh +0 -33
- package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
- package/src/fix-pr/scripts/trigger-review.sh +0 -36
- package/src/fix-pr/scripts/wait-and-check.sh +0 -37
- package/src/qa/scripts/detect-app-type.sh +0 -68
- package/src/qa/scripts/discover-routes.sh +0 -143
- package/src/qa/scripts/run-e2e-tests.sh +0 -131
- package/src/qa/scripts/start-dev-server.sh +0 -46
- package/src/qa/scripts/stop-dev-server.sh +0 -36
- package/src/review/prompts/fix-output-schema.md +0 -18
- package/src/review/prompts/review-output-schema.md +0 -38
- package/src/review/template.ts +0 -15
- /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
// src/context-mode/tools.ts
|
|
2
2
|
//
|
|
3
|
-
// Registers
|
|
3
|
+
// Registers native context-mode tools via platform.registerTool().
|
|
4
4
|
// Orchestration layer: delegates execution to sandbox, owns intent-driven
|
|
5
5
|
// filtering (auto-indexing large output into knowledge store).
|
|
6
6
|
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
7
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { extname, isAbsolute, join, resolve } from "node:path";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
8
10
|
import type { Platform } from "../platform/types.js";
|
|
9
11
|
import { executeCode } from "./sandbox/executor.js";
|
|
10
12
|
import { getSupportedLanguages } from "./sandbox/runners.js";
|
|
11
13
|
import { chunkMarkdown } from "./knowledge/chunker.js";
|
|
12
14
|
import { KnowledgeStore } from "./knowledge/store.js";
|
|
15
|
+
import { getCacheStore, getMetricsStore, getSessionId } from "./hooks.js";
|
|
13
16
|
import { fetchAndIndex } from "./web/fetcher.js";
|
|
17
|
+
import { parseCacheHandle } from "./cache-handle.js";
|
|
18
|
+
import { sliceCachedText } from "./cache-preview.js";
|
|
19
|
+
import { buildRepoMap } from "./repomap.js";
|
|
20
|
+
import { canonicalizeSourcePath } from "./source-hash.js";
|
|
14
21
|
|
|
15
22
|
/** Threshold (bytes) above which intent-driven filtering kicks in. */
|
|
16
23
|
const INTENT_THRESHOLD = 5 * 1024;
|
|
@@ -47,6 +54,123 @@ function trackCall(toolName: string, outputBytes: number): void {
|
|
|
47
54
|
stats.bytesReturned += outputBytes;
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
function byteLength(text: string): number {
|
|
58
|
+
return new TextEncoder().encode(text).byteLength;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stripCurrentDirPrefixBeforeWindowsAbsolute(p: string): string {
|
|
62
|
+
return p.replace(/^\.[\\/]+(?=[A-Za-z]:[\\/])/, "");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveNativeFilePath(filePath: string, cwd = process.cwd()): string {
|
|
66
|
+
const normalized = stripCurrentDirPrefixBeforeWindowsAbsolute(filePath);
|
|
67
|
+
return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
function currentKnowledgeOwner() {
|
|
72
|
+
return { ownerScope: "session" as const, ownerId: getSessionId() };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sha256Text(value: string): string {
|
|
76
|
+
return createHash("sha256").update(value).digest("hex");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stableStringify(value: unknown): string {
|
|
80
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
81
|
+
if (value && typeof value === "object") {
|
|
82
|
+
return `{${Object.entries(value as Record<string, unknown>)
|
|
83
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
84
|
+
.map(([key, val]) => `${JSON.stringify(key)}:${stableStringify(val)}`)
|
|
85
|
+
.join(",")}}`;
|
|
86
|
+
}
|
|
87
|
+
return JSON.stringify(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function recordCacheOpenMetric(opts: { beforeBytes: number; afterBytes: number; cacheHit: 0 | 1 }): void {
|
|
91
|
+
|
|
92
|
+
const metrics = getMetricsStore();
|
|
93
|
+
if (!metrics) return;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
metrics.record({
|
|
97
|
+
session_id: getSessionId(),
|
|
98
|
+
ts: Date.now(),
|
|
99
|
+
layer: "L3",
|
|
100
|
+
tool: "ctx_open_cached",
|
|
101
|
+
processor: "cache-open",
|
|
102
|
+
before_bytes: Math.max(0, Math.floor(opts.beforeBytes)),
|
|
103
|
+
after_bytes: Math.max(0, Math.floor(opts.afterBytes)),
|
|
104
|
+
cache_hit: opts.cacheHit,
|
|
105
|
+
unique_source_hash: null,
|
|
106
|
+
context_tokens: null,
|
|
107
|
+
context_window: null,
|
|
108
|
+
context_percent: null,
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
// Cache reads must not depend on metrics health.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
function recordRequestCacheMetric(opts: { tool: string; beforeBytes: number; afterBytes: number; cacheHit: 0 | 1 }): void {
|
|
117
|
+
const metrics = getMetricsStore();
|
|
118
|
+
if (!metrics) return;
|
|
119
|
+
try {
|
|
120
|
+
metrics.record({
|
|
121
|
+
session_id: getSessionId(),
|
|
122
|
+
ts: Date.now(),
|
|
123
|
+
layer: "L3",
|
|
124
|
+
tool: opts.tool,
|
|
125
|
+
processor: "cache-open",
|
|
126
|
+
before_bytes: Math.max(0, Math.floor(opts.beforeBytes)),
|
|
127
|
+
after_bytes: Math.max(0, Math.floor(opts.afterBytes)),
|
|
128
|
+
cache_hit: opts.cacheHit,
|
|
129
|
+
unique_source_hash: null,
|
|
130
|
+
context_tokens: null,
|
|
131
|
+
context_window: null,
|
|
132
|
+
context_percent: null,
|
|
133
|
+
});
|
|
134
|
+
} catch {
|
|
135
|
+
// Request-cache metrics are best-effort.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function recordL4RetrievalMetric(tool: "ctx_repomap" | "ctx_symbol", beforeBytes: number, afterBytes: number): void {
|
|
140
|
+
const metrics = getMetricsStore();
|
|
141
|
+
if (!metrics) return;
|
|
142
|
+
try {
|
|
143
|
+
metrics.record({
|
|
144
|
+
session_id: getSessionId(),
|
|
145
|
+
ts: Date.now(),
|
|
146
|
+
layer: "L4",
|
|
147
|
+
tool,
|
|
148
|
+
processor: "passthrough",
|
|
149
|
+
before_bytes: Math.max(0, Math.floor(beforeBytes)),
|
|
150
|
+
after_bytes: Math.max(0, Math.floor(afterBytes)),
|
|
151
|
+
cache_hit: 0,
|
|
152
|
+
unique_source_hash: null,
|
|
153
|
+
context_tokens: null,
|
|
154
|
+
context_window: null,
|
|
155
|
+
context_percent: null,
|
|
156
|
+
});
|
|
157
|
+
} catch {
|
|
158
|
+
// Retrieval metrics are diagnostic-only and must never break tool responses.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type KnowledgeStoreProvider = KnowledgeStore | (() => KnowledgeStore | null);
|
|
163
|
+
|
|
164
|
+
function resolveKnowledgeStore(provider: KnowledgeStoreProvider): KnowledgeStore | null {
|
|
165
|
+
return typeof provider === "function" ? provider() : provider;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function requireKnowledgeStore(provider: KnowledgeStoreProvider): KnowledgeStore {
|
|
169
|
+
const store = resolveKnowledgeStore(provider);
|
|
170
|
+
if (!store) throw new Error("Knowledge store unavailable for the active session.");
|
|
171
|
+
return store;
|
|
172
|
+
}
|
|
173
|
+
|
|
50
174
|
/**
|
|
51
175
|
* If output exceeds INTENT_THRESHOLD and an intent is provided, auto-index
|
|
52
176
|
* the output and return search results instead of raw text.
|
|
@@ -55,15 +179,16 @@ function maybeFilterByIntent(
|
|
|
55
179
|
output: string,
|
|
56
180
|
intent: string | undefined,
|
|
57
181
|
source: string,
|
|
58
|
-
store: KnowledgeStore,
|
|
182
|
+
store: KnowledgeStore | null,
|
|
59
183
|
): string {
|
|
184
|
+
if (!store) return output;
|
|
60
185
|
if (!intent || output.length < INTENT_THRESHOLD) return output;
|
|
61
186
|
|
|
62
187
|
const chunks = chunkMarkdown(output, source);
|
|
63
188
|
if (chunks.length === 0) return output;
|
|
64
189
|
|
|
65
|
-
store.index(chunks, source);
|
|
66
|
-
const results = store.search([intent], { source, limit: 5 });
|
|
190
|
+
store.index(chunks, source, currentKnowledgeOwner());
|
|
191
|
+
const results = store.search([intent], { source, limit: 5, owner: currentKnowledgeOwner() });
|
|
67
192
|
|
|
68
193
|
const sections = chunks.map((c) => `- ${c.title || "(untitled)"} (${c.body.length}B)`).join("\n");
|
|
69
194
|
|
|
@@ -150,10 +275,169 @@ function formatSearchResults(grouped: ReturnType<KnowledgeStore["search"]>): str
|
|
|
150
275
|
return text;
|
|
151
276
|
}
|
|
152
277
|
|
|
153
|
-
|
|
278
|
+
// ── Auto-index bootstrap for ctx_search ────────────────────────────
|
|
279
|
+
|
|
280
|
+
/** Extensions worth scanning when bootstrapping the knowledge store. */
|
|
281
|
+
const AUTO_INDEX_EXTENSIONS = new Set([
|
|
282
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
283
|
+
".py", ".rb", ".go", ".rs", ".java", ".kt", ".swift",
|
|
284
|
+
".md", ".mdx", ".txt", ".rst",
|
|
285
|
+
".yaml", ".yml", ".toml", ".json",
|
|
286
|
+
".sh", ".bash", ".zsh",
|
|
287
|
+
".html", ".css", ".scss",
|
|
288
|
+
".sql",
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
/** Directory names to skip during bootstrap scan. */
|
|
292
|
+
const AUTO_INDEX_SKIP_DIRS = new Set([
|
|
293
|
+
"node_modules", ".git", ".svn", ".hg",
|
|
294
|
+
"dist", "build", "out", ".next", ".nuxt", ".cache", ".turbo", ".bun",
|
|
295
|
+
"coverage", "vendor", "target", "__pycache__", ".venv", "venv",
|
|
296
|
+
".pytest_cache", ".mypy_cache", ".ruff_cache", ".tox",
|
|
297
|
+
"tmp", ".tmp", ".idea", ".vscode",
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
const AUTO_INDEX_MAX_FILES = 80;
|
|
301
|
+
const AUTO_INDEX_MAX_SCAN = 4000;
|
|
302
|
+
const AUTO_INDEX_MAX_FILE_BYTES = 256 * 1024;
|
|
303
|
+
const AUTO_INDEX_MAX_DEPTH = 8;
|
|
304
|
+
const AUTO_INDEX_MIN_TERM_LEN = 3;
|
|
305
|
+
|
|
306
|
+
/** Sessions for which we have already attempted bootstrap. Prevents repeat scans. */
|
|
307
|
+
const _autoIndexAttempted = new Set<string>();
|
|
308
|
+
|
|
309
|
+
/** Reset auto-index attempt tracking. Test-only. */
|
|
310
|
+
export function _resetAutoIndexAttempts(): void {
|
|
311
|
+
_autoIndexAttempted.clear();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Drop bootstrap-attempted entries that belong to a closed session. */
|
|
315
|
+
export function _forgetAutoIndexSession(ownerId: string): void {
|
|
316
|
+
if (!ownerId) return;
|
|
317
|
+
const prefix = `${ownerId}|`;
|
|
318
|
+
for (const key of _autoIndexAttempted) {
|
|
319
|
+
if (key.startsWith(prefix)) _autoIndexAttempted.delete(key);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function extractIndexTerms(queries: string[]): string[] {
|
|
324
|
+
const terms = new Set<string>();
|
|
325
|
+
for (const query of queries) {
|
|
326
|
+
if (typeof query !== "string") continue;
|
|
327
|
+
const tokens = query.toLowerCase().split(/[^\p{L}\p{N}_]+/u);
|
|
328
|
+
for (const token of tokens) {
|
|
329
|
+
if (token.length >= AUTO_INDEX_MIN_TERM_LEN) terms.add(token);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return [...terms];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Bootstrap the knowledge store by scanning `cwd` for files containing any of
|
|
337
|
+
* the query terms, then indexing those files. Used when ctx_search is called
|
|
338
|
+
* against an empty store — without this, search can never return useful
|
|
339
|
+
* results until the agent manually runs ctx_index/ctx_batch_execute.
|
|
340
|
+
*
|
|
341
|
+
* Returns the number of chunks indexed. Caller should re-search after.
|
|
342
|
+
*/
|
|
343
|
+
export function autoIndexFromCwd(
|
|
344
|
+
store: KnowledgeStore,
|
|
345
|
+
queries: string[],
|
|
346
|
+
cwd: string,
|
|
347
|
+
owner: { ownerScope: "session"; ownerId: string },
|
|
348
|
+
): { chunksIndexed: number; filesIndexed: number; filesScanned: number } {
|
|
349
|
+
const terms = extractIndexTerms(queries);
|
|
350
|
+
if (terms.length === 0) return { chunksIndexed: 0, filesIndexed: 0, filesScanned: 0 };
|
|
351
|
+
|
|
352
|
+
const matched: string[] = [];
|
|
353
|
+
let filesScanned = 0;
|
|
354
|
+
|
|
355
|
+
const walk = (dir: string, depth: number): void => {
|
|
356
|
+
if (depth > AUTO_INDEX_MAX_DEPTH) return;
|
|
357
|
+
if (matched.length >= AUTO_INDEX_MAX_FILES) return;
|
|
358
|
+
if (filesScanned >= AUTO_INDEX_MAX_SCAN) return;
|
|
359
|
+
|
|
360
|
+
let entries;
|
|
361
|
+
try {
|
|
362
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
363
|
+
} catch {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const entry of entries) {
|
|
368
|
+
if (matched.length >= AUTO_INDEX_MAX_FILES) return;
|
|
369
|
+
if (filesScanned >= AUTO_INDEX_MAX_SCAN) return;
|
|
370
|
+
const name = entry.name;
|
|
371
|
+
if (name.startsWith(".") && name !== "." && name !== "..") {
|
|
372
|
+
// Allow a few well-known dotted source dirs but skip caches/VCS.
|
|
373
|
+
if (AUTO_INDEX_SKIP_DIRS.has(name)) continue;
|
|
374
|
+
if (entry.isDirectory()) continue;
|
|
375
|
+
}
|
|
376
|
+
if (entry.isDirectory()) {
|
|
377
|
+
if (AUTO_INDEX_SKIP_DIRS.has(name)) continue;
|
|
378
|
+
walk(join(dir, name), depth + 1);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (!entry.isFile()) continue;
|
|
382
|
+
const ext = extname(name).toLowerCase();
|
|
383
|
+
if (!AUTO_INDEX_EXTENSIONS.has(ext)) continue;
|
|
384
|
+
const full = join(dir, name);
|
|
385
|
+
filesScanned++;
|
|
386
|
+
let body: string;
|
|
387
|
+
try {
|
|
388
|
+
const stat = statSync(full);
|
|
389
|
+
if (stat.size > AUTO_INDEX_MAX_FILE_BYTES) continue;
|
|
390
|
+
body = readFileSync(full, "utf8");
|
|
391
|
+
} catch {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const lower = body.toLowerCase();
|
|
395
|
+
if (terms.some((t) => lower.includes(t))) {
|
|
396
|
+
matched.push(full);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
walk(cwd, 0);
|
|
402
|
+
|
|
403
|
+
if (matched.length === 0) {
|
|
404
|
+
return { chunksIndexed: 0, filesIndexed: 0, filesScanned };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let chunksIndexed = 0;
|
|
408
|
+
for (const file of matched) {
|
|
409
|
+
let body: string;
|
|
410
|
+
try {
|
|
411
|
+
body = readFileSync(file, "utf8");
|
|
412
|
+
} catch {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const rel = file.startsWith(cwd) ? file.slice(cwd.length).replace(/^[\\/]+/, "") : file;
|
|
416
|
+
const source = `auto-index:${rel}`;
|
|
417
|
+
// Wrap with a markdown title so the chunker assigns a useful heading.
|
|
418
|
+
const chunks = chunkMarkdown(`# ${rel}\n\n${body}`, source);
|
|
419
|
+
if (chunks.length === 0) continue;
|
|
420
|
+
store.index(chunks, source, owner);
|
|
421
|
+
chunksIndexed += chunks.length;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { chunksIndexed, filesIndexed: matched.length, filesScanned };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export interface RegisterContextModeToolsOptions {
|
|
428
|
+
repomap?: { enabled?: boolean; tokenBudget: number; maxFiles: number };
|
|
429
|
+
knowledgeToolsEnabled?: boolean;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function registerContextModeTools(
|
|
433
|
+
platform: Platform,
|
|
434
|
+
storeProvider: KnowledgeStoreProvider,
|
|
435
|
+
options: RegisterContextModeToolsOptions = {},
|
|
436
|
+
): void {
|
|
154
437
|
if (!platform.registerTool) return;
|
|
155
438
|
|
|
156
439
|
const languages = getSupportedLanguages();
|
|
440
|
+
const knowledgeToolsEnabled = options.knowledgeToolsEnabled !== false;
|
|
157
441
|
|
|
158
442
|
// ── ctx_execute ────────────────────────────────────────────
|
|
159
443
|
platform.registerTool({
|
|
@@ -187,7 +471,7 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
187
471
|
if (result.exitCode !== 0) output += `\n[exit code: ${result.exitCode}]`;
|
|
188
472
|
|
|
189
473
|
const source = `ctx_execute:${language}:${Date.now()}`;
|
|
190
|
-
output = maybeFilterByIntent(output, intent, source,
|
|
474
|
+
output = maybeFilterByIntent(output, intent, source, resolveKnowledgeStore(storeProvider));
|
|
191
475
|
|
|
192
476
|
trackCall("ctx_execute", output.length);
|
|
193
477
|
return { content: [{ type: "text", text: capResponseSize(output) }] };
|
|
@@ -213,12 +497,51 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
213
497
|
code: { type: "string", description: "Code to process FILE_CONTENT. Print summary via console.log/print/echo." },
|
|
214
498
|
intent: { type: "string", description: "What you're looking for in the output" },
|
|
215
499
|
timeout: { type: "number", description: "Max execution time in ms (default: 30000)" },
|
|
500
|
+
cache: { type: "boolean", description: "Opt into request caching for deterministic file-processing calls" },
|
|
501
|
+
cacheTtlMs: { type: "number", description: "Request cache TTL in milliseconds (default: 300000)" },
|
|
216
502
|
},
|
|
217
503
|
required: ["path", "language", "code"],
|
|
218
504
|
},
|
|
219
505
|
async execute(_toolCallId: string, params: any) {
|
|
220
|
-
const { path: filePath, language, code, intent, timeout } = params;
|
|
221
|
-
const
|
|
506
|
+
const { path: filePath, language, code, intent, timeout, cache, cacheTtlMs } = params;
|
|
507
|
+
const nativeFilePath = resolveNativeFilePath(String(filePath));
|
|
508
|
+
const canonicalFilePath = canonicalizeSourcePath(nativeFilePath, process.cwd());
|
|
509
|
+
const fileContent = readFileSync(nativeFilePath, "utf-8");
|
|
510
|
+
const fileHash = sha256Text(fileContent);
|
|
511
|
+
const cacheStore = cache === true ? getCacheStore() : null;
|
|
512
|
+
const requestCacheArgs = {
|
|
513
|
+
path: canonicalFilePath,
|
|
514
|
+
language,
|
|
515
|
+
codeHash: sha256Text(String(code)),
|
|
516
|
+
timeout: timeout ?? null,
|
|
517
|
+
intent: typeof intent === "string" ? intent : null,
|
|
518
|
+
fileHash,
|
|
519
|
+
};
|
|
520
|
+
const argsHash = sha256Text(stableStringify(requestCacheArgs));
|
|
521
|
+
const fingerprint = fileHash;
|
|
522
|
+
if (cacheStore) {
|
|
523
|
+
const cached = cacheStore.getRequestCache({
|
|
524
|
+
tool: "ctx_execute_file",
|
|
525
|
+
argsHash,
|
|
526
|
+
cwd: process.cwd(),
|
|
527
|
+
fingerprint,
|
|
528
|
+
});
|
|
529
|
+
if (cached.hit) {
|
|
530
|
+
const opened = cacheStore.openText(cached.handle);
|
|
531
|
+
if (opened.ok) {
|
|
532
|
+
const output = `[cache hit: ${cached.handle}]\n${opened.text}`;
|
|
533
|
+
recordRequestCacheMetric({
|
|
534
|
+
tool: "ctx_execute_file",
|
|
535
|
+
beforeBytes: opened.meta.sizeBytes,
|
|
536
|
+
afterBytes: byteLength(output),
|
|
537
|
+
cacheHit: 1,
|
|
538
|
+
});
|
|
539
|
+
trackCall("ctx_execute_file", output.length);
|
|
540
|
+
return { content: [{ type: "text", text: capResponseSize(output) }] };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
222
545
|
const augmentedCode = injectFileContent(language, fileContent, code);
|
|
223
546
|
|
|
224
547
|
const result = await executeCode(language, augmentedCode, { timeout });
|
|
@@ -227,14 +550,38 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
227
550
|
if (result.stderr) output += `\n[stderr]\n${result.stderr}`;
|
|
228
551
|
if (result.exitCode !== 0) output += `\n[exit code: ${result.exitCode}]`;
|
|
229
552
|
|
|
230
|
-
const source = `ctx_execute_file:${
|
|
231
|
-
output = maybeFilterByIntent(output, intent, source,
|
|
553
|
+
const source = `ctx_execute_file:${canonicalFilePath}:${Date.now()}`;
|
|
554
|
+
output = maybeFilterByIntent(output, intent, source, resolveKnowledgeStore(storeProvider));
|
|
555
|
+
if (cacheStore && result.exitCode === 0) {
|
|
556
|
+
const meta = cacheStore.putText({
|
|
557
|
+
sessionId: getSessionId(),
|
|
558
|
+
text: output,
|
|
559
|
+
sourceTool: "ctx_execute_file",
|
|
560
|
+
sourceHash: argsHash,
|
|
561
|
+
recordMetric: false,
|
|
562
|
+
});
|
|
563
|
+
cacheStore.putRequestCache({
|
|
564
|
+
tool: "ctx_execute_file",
|
|
565
|
+
argsHash,
|
|
566
|
+
cwd: process.cwd(),
|
|
567
|
+
fingerprint,
|
|
568
|
+
handle: meta.handle,
|
|
569
|
+
ttlMs: typeof cacheTtlMs === "number" ? cacheTtlMs : 5 * 60 * 1000,
|
|
570
|
+
});
|
|
571
|
+
recordRequestCacheMetric({
|
|
572
|
+
tool: "ctx_execute_file",
|
|
573
|
+
beforeBytes: byteLength(output),
|
|
574
|
+
afterBytes: byteLength(output),
|
|
575
|
+
cacheHit: 0,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
232
578
|
|
|
233
579
|
trackCall("ctx_execute_file", output.length);
|
|
234
580
|
return { content: [{ type: "text", text: capResponseSize(output) }] };
|
|
235
581
|
},
|
|
236
582
|
});
|
|
237
583
|
|
|
584
|
+
if (knowledgeToolsEnabled) {
|
|
238
585
|
// ── ctx_batch_execute ──────────────────────────────────────
|
|
239
586
|
platform.registerTool({
|
|
240
587
|
name: "ctx_batch_execute",
|
|
@@ -272,6 +619,7 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
272
619
|
required: ["commands", "queries"],
|
|
273
620
|
},
|
|
274
621
|
async execute(_toolCallId: string, params: any) {
|
|
622
|
+
const store = requireKnowledgeStore(storeProvider);
|
|
275
623
|
const { commands, queries, timeout = 60000 } = params;
|
|
276
624
|
const sections: string[] = [];
|
|
277
625
|
|
|
@@ -284,13 +632,13 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
284
632
|
const source = `batch:${cmd.label}`;
|
|
285
633
|
const chunks = chunkMarkdown(output, source);
|
|
286
634
|
if (chunks.length > 0) {
|
|
287
|
-
store.index(chunks, source);
|
|
635
|
+
store.index(chunks, source, currentKnowledgeOwner());
|
|
288
636
|
}
|
|
289
637
|
sections.push(`- ${cmd.label} (${(output.length / 1024).toFixed(1)}KB)`);
|
|
290
638
|
}
|
|
291
639
|
|
|
292
640
|
// Search across all indexed content
|
|
293
|
-
const results = store.search(queries, { limit: 5 });
|
|
641
|
+
const results = store.search(queries, { limit: 5, owner: currentKnowledgeOwner() });
|
|
294
642
|
|
|
295
643
|
let text = `Executed ${commands.length} commands. Indexed ${sections.length} sections.\n\n`;
|
|
296
644
|
text += `## Indexed Sections\n\n${sections.join("\n")}\n\n`;
|
|
@@ -307,7 +655,9 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
307
655
|
return { content: [{ type: "text", text: capResponseSize(text) }] };
|
|
308
656
|
},
|
|
309
657
|
});
|
|
658
|
+
}
|
|
310
659
|
|
|
660
|
+
if (knowledgeToolsEnabled) {
|
|
311
661
|
// ── ctx_index ──────────────────────────────────────────────
|
|
312
662
|
platform.registerTool({
|
|
313
663
|
name: "ctx_index",
|
|
@@ -315,6 +665,11 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
315
665
|
description:
|
|
316
666
|
"Index documentation or knowledge content into a searchable BM25 knowledge base. After indexing, use ctx_search to retrieve specific sections on-demand.",
|
|
317
667
|
promptSnippet: "ctx_index — store content in knowledge base for later search",
|
|
668
|
+
promptGuidelines: [
|
|
669
|
+
"Use when you have raw text or markdown to make searchable later via ctx_search",
|
|
670
|
+
"Use ctx_fetch_and_index instead when the source is a URL",
|
|
671
|
+
"Provide a descriptive `source` label so others can scope ctx_search by source",
|
|
672
|
+
],
|
|
318
673
|
parameters: {
|
|
319
674
|
type: "object",
|
|
320
675
|
properties: {
|
|
@@ -325,6 +680,7 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
325
680
|
required: ["source"],
|
|
326
681
|
},
|
|
327
682
|
async execute(_toolCallId: string, params: any) {
|
|
683
|
+
const store = requireKnowledgeStore(storeProvider);
|
|
328
684
|
const { content, path: filePath, source } = params;
|
|
329
685
|
|
|
330
686
|
if ((content && filePath) || (!content && !filePath)) {
|
|
@@ -333,21 +689,29 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
333
689
|
|
|
334
690
|
const text = filePath ? readFileSync(filePath, "utf-8") : content;
|
|
335
691
|
const chunks = chunkMarkdown(text, source);
|
|
336
|
-
store.index(chunks, source);
|
|
692
|
+
store.index(chunks, source, currentKnowledgeOwner());
|
|
337
693
|
|
|
338
694
|
const output = `Indexed ${chunks.length} chunks under source "${source}".`;
|
|
339
695
|
trackCall("ctx_index", output.length);
|
|
340
696
|
return { content: [{ type: "text", text: output }] };
|
|
341
697
|
},
|
|
342
698
|
});
|
|
699
|
+
}
|
|
343
700
|
|
|
701
|
+
if (knowledgeToolsEnabled) {
|
|
344
702
|
// ── ctx_search ─────────────────────────────────────────────
|
|
345
703
|
platform.registerTool({
|
|
346
704
|
name: "ctx_search",
|
|
347
705
|
label: "Search Knowledge Base",
|
|
348
706
|
description:
|
|
349
|
-
"Search indexed content.
|
|
707
|
+
"Search indexed content. Auto-bootstraps the knowledge store from the project on the first call when nothing is indexed yet — subsequent calls skip auto-indexing.",
|
|
350
708
|
promptSnippet: "ctx_search — query indexed content with BM25 search",
|
|
709
|
+
promptGuidelines: [
|
|
710
|
+
"Use to retrieve specific sections from previously indexed content",
|
|
711
|
+
"Pass ALL questions in the queries array in one call — do not chain ctx_search calls",
|
|
712
|
+
"Filter by `source` when you know which indexed bundle to search",
|
|
713
|
+
"Safe to call on a fresh session: the first call bootstraps an index from the project when none exists",
|
|
714
|
+
],
|
|
351
715
|
parameters: {
|
|
352
716
|
type: "object",
|
|
353
717
|
properties: {
|
|
@@ -362,16 +726,193 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
362
726
|
},
|
|
363
727
|
required: ["queries"],
|
|
364
728
|
},
|
|
365
|
-
async execute(_toolCallId: string, params: any) {
|
|
729
|
+
async execute(_toolCallId: string, params: any, _abortSignal?: any, _onUpdate?: any, ctx?: any) {
|
|
730
|
+
const store = requireKnowledgeStore(storeProvider);
|
|
366
731
|
const { queries, source, contentType, limit } = params;
|
|
367
|
-
const
|
|
368
|
-
|
|
732
|
+
const owner = currentKnowledgeOwner();
|
|
733
|
+
|
|
734
|
+
let results = store.search(queries, { source, contentType, limit, owner });
|
|
735
|
+
let bootstrapNote = "";
|
|
369
736
|
|
|
737
|
+
const allEmpty = results.every((g) => g.results.length === 0);
|
|
738
|
+
const sessionKey = `${owner.ownerId}|${source ?? ""}`;
|
|
739
|
+
const canBootstrap =
|
|
740
|
+
allEmpty &&
|
|
741
|
+
Array.isArray(queries) &&
|
|
742
|
+
queries.length > 0 &&
|
|
743
|
+
!source &&
|
|
744
|
+
!_autoIndexAttempted.has(sessionKey);
|
|
745
|
+
|
|
746
|
+
if (canBootstrap) {
|
|
747
|
+
_autoIndexAttempted.add(sessionKey);
|
|
748
|
+
const stats = store.getStats();
|
|
749
|
+
if (stats.totalChunks === 0) {
|
|
750
|
+
const cwd = typeof ctx?.cwd === "string" && ctx.cwd.length > 0 ? ctx.cwd : process.cwd();
|
|
751
|
+
let bootstrap;
|
|
752
|
+
try {
|
|
753
|
+
bootstrap = autoIndexFromCwd(store, queries, cwd, owner);
|
|
754
|
+
} catch {
|
|
755
|
+
bootstrap = { chunksIndexed: 0, filesIndexed: 0, filesScanned: 0 };
|
|
756
|
+
}
|
|
757
|
+
if (bootstrap.chunksIndexed > 0) {
|
|
758
|
+
results = store.search(queries, { source, contentType, limit, owner });
|
|
759
|
+
bootstrapNote =
|
|
760
|
+
`[auto-indexed ${bootstrap.filesIndexed} files (${bootstrap.chunksIndexed} chunks) ` +
|
|
761
|
+
`from ${bootstrap.filesScanned} scanned to bootstrap the empty knowledge store]\n\n`;
|
|
762
|
+
} else if (bootstrap.filesScanned > 0) {
|
|
763
|
+
bootstrapNote =
|
|
764
|
+
`[scanned ${bootstrap.filesScanned} files but none matched the query terms; ` +
|
|
765
|
+
`use ctx_batch_execute or ctx_index to index relevant content explicitly]\n\n`;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const output = bootstrapNote + formatSearchResults(results);
|
|
370
771
|
trackCall("ctx_search", output.length);
|
|
371
772
|
return { content: [{ type: "text", text: capResponseSize(output) }] };
|
|
372
773
|
},
|
|
373
774
|
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ── ctx_repomap ─────────────────────────────────────────────
|
|
778
|
+
if (options.repomap?.enabled !== false) {
|
|
779
|
+
platform.registerTool({
|
|
780
|
+
name: "ctx_repomap",
|
|
781
|
+
label: "Repository Map",
|
|
782
|
+
description: "Build a deterministic structural repository map capped by an estimated token budget.",
|
|
783
|
+
promptSnippet: "ctx_repomap — structural repository map with symbols/imports",
|
|
784
|
+
promptGuidelines: [
|
|
785
|
+
"Use before broad code exploration when you need a bounded overview",
|
|
786
|
+
"Pass focus files to personalize ranking toward the current task",
|
|
787
|
+
],
|
|
788
|
+
parameters: {
|
|
789
|
+
type: "object",
|
|
790
|
+
properties: {
|
|
791
|
+
focus: { type: "array", items: { type: "string" }, description: "Optional focus files for personalized ranking" },
|
|
792
|
+
tokenBudget: { type: "number", description: "Estimated token budget for the emitted map (default: 4000)" },
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
async execute(_toolCallId: string, params: any, _abortSignal?: any, _onUpdate?: any, ctx?: any) {
|
|
796
|
+
const cwd = typeof ctx?.cwd === "string" ? ctx.cwd : process.cwd();
|
|
797
|
+
const result = await buildRepoMap(platform, {
|
|
798
|
+
cwd,
|
|
799
|
+
focus: Array.isArray(params?.focus) ? params.focus : [],
|
|
800
|
+
tokenBudget: typeof params?.tokenBudget === "number" ? params.tokenBudget : options.repomap?.tokenBudget,
|
|
801
|
+
maxFiles: options.repomap?.maxFiles,
|
|
802
|
+
});
|
|
803
|
+
const output = capResponseSize(result.text);
|
|
804
|
+
const outputBytes = byteLength(output);
|
|
805
|
+
recordL4RetrievalMetric("ctx_repomap", result.emittedSourceBytes, outputBytes);
|
|
806
|
+
trackCall("ctx_repomap", output.length);
|
|
807
|
+
return { content: [{ type: "text", text: output }] };
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ── ctx_symbol ──────────────────────────────────────────────
|
|
813
|
+
platform.registerTool({
|
|
814
|
+
name: "ctx_symbol",
|
|
815
|
+
label: "Symbol Summary",
|
|
816
|
+
description: "Capability-gated facade for native LSP symbol summaries. Returns an explicit diagnostic when the platform has no callable LSP API.",
|
|
817
|
+
promptSnippet: "ctx_symbol — bounded symbol summary when platform LSP facade is available",
|
|
818
|
+
promptGuidelines: [
|
|
819
|
+
"Use native lsp directly when this reports platform_lsp_facade_unavailable",
|
|
820
|
+
],
|
|
821
|
+
parameters: {
|
|
822
|
+
type: "object",
|
|
823
|
+
properties: {
|
|
824
|
+
query: { type: "string", description: "Symbol name or query" },
|
|
825
|
+
action: { type: "string", enum: ["definition", "references", "hover", "symbols"], description: "LSP action" },
|
|
826
|
+
},
|
|
827
|
+
required: ["query"],
|
|
828
|
+
},
|
|
829
|
+
async execute() {
|
|
830
|
+
const text = [
|
|
831
|
+
"platform_lsp_facade_unavailable",
|
|
832
|
+
"The current OMP Platform adapter does not expose a callable native LSP/tool API to extensions.",
|
|
833
|
+
"Use the native lsp tool directly for definitions, references, hover, and symbols.",
|
|
834
|
+
].join("\n");
|
|
835
|
+
const diagnosticBytes = byteLength(text);
|
|
836
|
+
recordL4RetrievalMetric("ctx_symbol", diagnosticBytes, diagnosticBytes);
|
|
837
|
+
trackCall("ctx_symbol", text.length);
|
|
838
|
+
return { content: [{ type: "text", text }] };
|
|
839
|
+
},
|
|
840
|
+
});
|
|
374
841
|
|
|
842
|
+
// ── ctx_open_cached ────────────────────────────────────────
|
|
843
|
+
platform.registerTool({
|
|
844
|
+
name: "ctx_open_cached",
|
|
845
|
+
label: "Open Cached Context Handle",
|
|
846
|
+
description:
|
|
847
|
+
"Open a cache://<sha256> handle and return a bounded slice of cached text with offset metadata.",
|
|
848
|
+
promptSnippet: "ctx_open_cached — open a cache:// handle with bounded offset/limit reads",
|
|
849
|
+
promptGuidelines: [
|
|
850
|
+
"Use when the user provides a cache:// handle or asks to open cached content",
|
|
851
|
+
"Always use offset/limit for follow-up reads instead of requesting the whole payload",
|
|
852
|
+
"Invalid, missing, or corrupt handles return explicit text instead of throwing",
|
|
853
|
+
],
|
|
854
|
+
parameters: {
|
|
855
|
+
type: "object",
|
|
856
|
+
properties: {
|
|
857
|
+
handle: { type: "string", description: "cache://<sha256> handle to open" },
|
|
858
|
+
offset: { type: "number", description: "Character offset in decoded cached text (default: 0)" },
|
|
859
|
+
limit: { type: "number", description: "Maximum characters to return, capped at 100KB characters" },
|
|
860
|
+
},
|
|
861
|
+
required: ["handle"],
|
|
862
|
+
},
|
|
863
|
+
async execute(_toolCallId: string, params: any) {
|
|
864
|
+
const parsed = parseCacheHandle(String(params?.handle ?? ""));
|
|
865
|
+
if (!parsed.ok) {
|
|
866
|
+
const output = `Cannot open cached content: ${parsed.message}.`;
|
|
867
|
+
recordCacheOpenMetric({ beforeBytes: 0, afterBytes: byteLength(output), cacheHit: 0 });
|
|
868
|
+
trackCall("ctx_open_cached", output.length);
|
|
869
|
+
return { content: [{ type: "text", text: capResponseSize(output) }] };
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const cacheStore = getCacheStore();
|
|
873
|
+
if (!cacheStore) {
|
|
874
|
+
const output = "Cannot open cached content: cache store is unavailable for this session.";
|
|
875
|
+
recordCacheOpenMetric({ beforeBytes: 0, afterBytes: byteLength(output), cacheHit: 0 });
|
|
876
|
+
trackCall("ctx_open_cached", output.length);
|
|
877
|
+
return { content: [{ type: "text", text: output }] };
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const opened = cacheStore.openText(parsed.handle);
|
|
881
|
+
if (!opened.ok) {
|
|
882
|
+
recordCacheOpenMetric({ beforeBytes: 0, afterBytes: byteLength(opened.message), cacheHit: 0 });
|
|
883
|
+
trackCall("ctx_open_cached", opened.message.length);
|
|
884
|
+
return { content: [{ type: "text", text: capResponseSize(opened.message) }] };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Reserve headroom for the metadata block so capResponseSize never silently drops
|
|
888
|
+
// characters that we already advertised in `Returned`/`Next offset`.
|
|
889
|
+
const HEADER_HEADROOM_CHARS = 512;
|
|
890
|
+
const userLimit = typeof params?.limit === "number" ? params.limit : undefined;
|
|
891
|
+
const responseBudget = Math.max(0, MAX_RESPONSE_SIZE - HEADER_HEADROOM_CHARS);
|
|
892
|
+
const cappedLimit = userLimit === undefined
|
|
893
|
+
? responseBudget
|
|
894
|
+
: Math.min(userLimit, responseBudget);
|
|
895
|
+
const slice = sliceCachedText(opened.text, params?.offset, cappedLimit);
|
|
896
|
+
const end = slice.offset + slice.returnedChars;
|
|
897
|
+
let output = `## Cached content ${opened.handle}\n\n`;
|
|
898
|
+
output += `- Total: ${opened.meta.sizeBytes} bytes, ${slice.totalChars} chars\n`;
|
|
899
|
+
output += `- Returned: chars ${slice.offset}..${end} of ${slice.totalChars}\n`;
|
|
900
|
+
if (slice.nextOffset !== null) {
|
|
901
|
+
output += `- Next offset: ${slice.nextOffset}\n`;
|
|
902
|
+
}
|
|
903
|
+
output += `\n---\n${slice.text}`;
|
|
904
|
+
|
|
905
|
+
recordCacheOpenMetric({
|
|
906
|
+
beforeBytes: opened.meta.sizeBytes,
|
|
907
|
+
afterBytes: byteLength(slice.text),
|
|
908
|
+
cacheHit: 1,
|
|
909
|
+
});
|
|
910
|
+
trackCall("ctx_open_cached", output.length);
|
|
911
|
+
return { content: [{ type: "text", text: capResponseSize(output) }] };
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
if (knowledgeToolsEnabled) {
|
|
375
916
|
// ── ctx_fetch_and_index ────────────────────────────────────
|
|
376
917
|
platform.registerTool({
|
|
377
918
|
name: "ctx_fetch_and_index",
|
|
@@ -379,6 +920,10 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
379
920
|
description:
|
|
380
921
|
"Fetch URL content, convert HTML to markdown, index into searchable knowledge base, and return a ~3KB preview. Use ctx_search for deeper lookups.",
|
|
381
922
|
promptSnippet: "ctx_fetch_and_index — fetch URL, index content, return preview",
|
|
923
|
+
promptGuidelines: [
|
|
924
|
+
"Use this instead of curl/wget/WebFetch — raw HTML never enters context",
|
|
925
|
+
"After indexing, use ctx_search for deeper lookups beyond the preview",
|
|
926
|
+
],
|
|
382
927
|
parameters: {
|
|
383
928
|
type: "object",
|
|
384
929
|
properties: {
|
|
@@ -389,8 +934,9 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
389
934
|
required: ["url"],
|
|
390
935
|
},
|
|
391
936
|
async execute(_toolCallId: string, params: any) {
|
|
937
|
+
const store = requireKnowledgeStore(storeProvider);
|
|
392
938
|
const { url, source, force } = params;
|
|
393
|
-
const result = await fetchAndIndex(url, store, { source, force });
|
|
939
|
+
const result = await fetchAndIndex(url, store, { source, force, owner: currentKnowledgeOwner() });
|
|
394
940
|
|
|
395
941
|
let output = result.preview;
|
|
396
942
|
if (!result.cached) {
|
|
@@ -403,6 +949,7 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
403
949
|
return { content: [{ type: "text", text: capResponseSize(output) }] };
|
|
404
950
|
},
|
|
405
951
|
});
|
|
952
|
+
}
|
|
406
953
|
|
|
407
954
|
// ── ctx_stats ──────────────────────────────────────────────
|
|
408
955
|
platform.registerTool({
|
|
@@ -411,9 +958,72 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
411
958
|
description:
|
|
412
959
|
"Returns context consumption statistics for the current session. Shows total bytes returned to context, breakdown by tool, call counts, and knowledge base stats.",
|
|
413
960
|
promptSnippet: "ctx_stats — show session context stats",
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
961
|
+
promptGuidelines: [
|
|
962
|
+
"Use only when the user asks about context consumption or to debug context bloat",
|
|
963
|
+
"Do not call this proactively — it consumes context itself",
|
|
964
|
+
],
|
|
965
|
+
parameters: {
|
|
966
|
+
type: "object",
|
|
967
|
+
properties: {
|
|
968
|
+
format: {
|
|
969
|
+
type: "string",
|
|
970
|
+
enum: ["markdown", "json"],
|
|
971
|
+
description: "Output format. Defaults to markdown for human reading; json is for tokscale-class consumers.",
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
},
|
|
975
|
+
async execute(_id: string, params: any = {}) {
|
|
976
|
+
const store = resolveKnowledgeStore(storeProvider);
|
|
977
|
+
const format = params?.format === "json" ? "json" : "markdown";
|
|
978
|
+
const metricsStore = getMetricsStore();
|
|
979
|
+
const sessionId = getSessionId();
|
|
980
|
+
|
|
981
|
+
if (format === "json") {
|
|
982
|
+
const meta = (() => {
|
|
983
|
+
try {
|
|
984
|
+
return metricsStore?.getSessionMeta(sessionId) ?? null;
|
|
985
|
+
} catch {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
})();
|
|
989
|
+
const totals = metricsStore
|
|
990
|
+
? metricsStore.getSessionTotals(sessionId)
|
|
991
|
+
: { beforeBytes: 0, afterBytes: 0, saved: 0, rowCount: 0 };
|
|
992
|
+
const perProcessor = metricsStore ? metricsStore.getTopProcessors(sessionId, 50) : [];
|
|
993
|
+
const perLayer = metricsStore ? metricsStore.getPerLayer(sessionId) : [];
|
|
994
|
+
const uniqueSourceShare = metricsStore
|
|
995
|
+
? metricsStore.getUniqueSourceShare(sessionId)
|
|
996
|
+
: 0;
|
|
997
|
+
const writeFailures = metricsStore
|
|
998
|
+
? metricsStore.getSessionWriteFailures(sessionId)
|
|
999
|
+
: 0;
|
|
1000
|
+
const tokensEstimated = Math.ceil(totals.saved / 4);
|
|
1001
|
+
const payload = {
|
|
1002
|
+
session: {
|
|
1003
|
+
id: sessionId,
|
|
1004
|
+
startedAt: meta?.started_at ?? 0,
|
|
1005
|
+
rowCount: totals.rowCount,
|
|
1006
|
+
},
|
|
1007
|
+
totals: {
|
|
1008
|
+
beforeBytes: totals.beforeBytes,
|
|
1009
|
+
afterBytes: totals.afterBytes,
|
|
1010
|
+
saved: totals.saved,
|
|
1011
|
+
tokensEstimated,
|
|
1012
|
+
},
|
|
1013
|
+
perProcessor,
|
|
1014
|
+
perLayer,
|
|
1015
|
+
uniqueSourceShare,
|
|
1016
|
+
writeFailures,
|
|
1017
|
+
};
|
|
1018
|
+
const text = JSON.stringify(payload, null, 2);
|
|
1019
|
+
trackCall("ctx_stats", text.length);
|
|
1020
|
+
return { content: [{ type: "text", text }] };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Markdown (default; existing behavior preserved).
|
|
1024
|
+
const storeStats = store
|
|
1025
|
+
? store.getStats()
|
|
1026
|
+
: { totalChunks: 0, sources: [], dbSizeBytes: 0 };
|
|
417
1027
|
const totalCalls = Object.values(stats.calls).reduce((sum, n) => sum + n, 0);
|
|
418
1028
|
const estimatedTokens = Math.ceil(stats.bytesReturned / 4); // rough estimate
|
|
419
1029
|
|
|
@@ -433,11 +1043,23 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
433
1043
|
output += `- **Sources**: ${storeStats.sources.length > 0 ? storeStats.sources.join(", ") : "(none)"}\n`;
|
|
434
1044
|
output += `- **DB size**: ${(storeStats.dbSizeBytes / 1024).toFixed(1)}KB\n`;
|
|
435
1045
|
|
|
1046
|
+
// Append a Savings panel for the active metrics session, when available.
|
|
1047
|
+
if (metricsStore) {
|
|
1048
|
+
const totals = metricsStore.getSessionTotals(sessionId);
|
|
1049
|
+
const uniqueSourceShare = metricsStore.getUniqueSourceShare(sessionId);
|
|
1050
|
+
output += `\n### Session savings (L1 metrics)\n\n`;
|
|
1051
|
+
output += `- **Before**: ${(totals.beforeBytes / 1024).toFixed(1)}KB\n`;
|
|
1052
|
+
output += `- **After**: ${(totals.afterBytes / 1024).toFixed(1)}KB\n`;
|
|
1053
|
+
output += `- **Saved**: ${(totals.saved / 1024).toFixed(1)}KB\n`;
|
|
1054
|
+
output += `- **Unique-source share**: ${Math.round(uniqueSourceShare * 100)}%\n`;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
436
1057
|
trackCall("ctx_stats", output.length);
|
|
437
1058
|
return { content: [{ type: "text", text: output }] };
|
|
438
1059
|
},
|
|
439
1060
|
});
|
|
440
1061
|
|
|
1062
|
+
if (knowledgeToolsEnabled) {
|
|
441
1063
|
// ── ctx_purge ──────────────────────────────────────────────
|
|
442
1064
|
platform.registerTool({
|
|
443
1065
|
name: "ctx_purge",
|
|
@@ -445,14 +1067,24 @@ export function registerContextModeTools(platform: Platform, store: KnowledgeSto
|
|
|
445
1067
|
description:
|
|
446
1068
|
"Delete all indexed content from the knowledge base. Does NOT touch the event store (events.db). Use when you want a fresh start.",
|
|
447
1069
|
promptSnippet: "ctx_purge — clear all indexed content",
|
|
1070
|
+
promptGuidelines: [
|
|
1071
|
+
"Use only when the user explicitly asks to reset the knowledge base",
|
|
1072
|
+
"Does NOT delete the event store (events.db) — only the knowledge index",
|
|
1073
|
+
],
|
|
448
1074
|
parameters: { type: "object", properties: {} },
|
|
449
1075
|
async execute() {
|
|
1076
|
+
const store = requireKnowledgeStore(storeProvider);
|
|
450
1077
|
const count = store.purge();
|
|
1078
|
+
// Mark the current session as bootstrap-attempted so a follow-up
|
|
1079
|
+
// ctx_search does not undo the explicit purge by re-bootstrapping.
|
|
1080
|
+
const owner = currentKnowledgeOwner();
|
|
1081
|
+
_autoIndexAttempted.add(`${owner.ownerId}|`);
|
|
451
1082
|
const output = `Purged ${count} chunks from knowledge base.`;
|
|
452
1083
|
trackCall("ctx_purge", output.length);
|
|
453
1084
|
return { content: [{ type: "text", text: output }] };
|
|
454
1085
|
},
|
|
455
1086
|
});
|
|
1087
|
+
}
|
|
456
1088
|
}
|
|
457
1089
|
|
|
458
1090
|
// Exported for testing
|