ada-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +256 -0
- package/bench/README.md +88 -0
- package/bench/swebench.mjs +242 -0
- package/bin/ada-server.mjs +6 -0
- package/bin/ada.mjs +7 -0
- package/docs/agent-loop.svg +66 -0
- package/docs/architecture.md +139 -0
- package/docs/architecture.svg +73 -0
- package/docs/connectors.md +48 -0
- package/docs/integrations.md +59 -0
- package/docs/login-flow.svg +56 -0
- package/docs/orchestration.md +45 -0
- package/package.json +64 -0
- package/skills/accessibility/SKILL.md +23 -0
- package/skills/add-logging/SKILL.md +23 -0
- package/skills/add-metrics/SKILL.md +23 -0
- package/skills/adr/SKILL.md +24 -0
- package/skills/aesthetic-direction/SKILL.md +24 -0
- package/skills/agent-loop/SKILL.md +23 -0
- package/skills/alerting/SKILL.md +23 -0
- package/skills/alpha-compositing/SKILL.md +23 -0
- package/skills/android-compose/SKILL.md +23 -0
- package/skills/angular-module/SKILL.md +23 -0
- package/skills/ansible-playbook/SKILL.md +24 -0
- package/skills/api-docs/SKILL.md +24 -0
- package/skills/app-store-prep/SKILL.md +23 -0
- package/skills/architecture-diagram/SKILL.md +21 -0
- package/skills/architecture-doc/SKILL.md +24 -0
- package/skills/audit-log/SKILL.md +23 -0
- package/skills/authz-review/SKILL.md +23 -0
- package/skills/aws-lambda/SKILL.md +24 -0
- package/skills/bash-script/SKILL.md +23 -0
- package/skills/batch/SKILL.md +23 -0
- package/skills/bisect/SKILL.md +23 -0
- package/skills/bounding-box/SKILL.md +24 -0
- package/skills/branch-cleanup/SKILL.md +23 -0
- package/skills/bundle-analyze/SKILL.md +23 -0
- package/skills/cache/SKILL.md +23 -0
- package/skills/call-graph/SKILL.md +23 -0
- package/skills/canvas-debug/SKILL.md +23 -0
- package/skills/cdn-setup/SKILL.md +23 -0
- package/skills/changelog/SKILL.md +24 -0
- package/skills/cherry-pick/SKILL.md +23 -0
- package/skills/ci-setup/SKILL.md +23 -0
- package/skills/cleanup/SKILL.md +23 -0
- package/skills/cli-tool/SKILL.md +23 -0
- package/skills/cloudformation/SKILL.md +23 -0
- package/skills/code-examples/SKILL.md +24 -0
- package/skills/code-review/SKILL.md +23 -0
- package/skills/color-palette/SKILL.md +24 -0
- package/skills/color-space/SKILL.md +24 -0
- package/skills/comment-why/SKILL.md +23 -0
- package/skills/commit/SKILL.md +26 -0
- package/skills/complexity-audit/SKILL.md +23 -0
- package/skills/component/SKILL.md +23 -0
- package/skills/component-library/SKILL.md +23 -0
- package/skills/connect-github/SKILL.md +20 -0
- package/skills/connect-mcp/SKILL.md +21 -0
- package/skills/connect-postgres/SKILL.md +20 -0
- package/skills/connect-remote/SKILL.md +23 -0
- package/skills/connect-slack/SKILL.md +20 -0
- package/skills/contract-audit/SKILL.md +25 -0
- package/skills/contributing/SKILL.md +23 -0
- package/skills/cpp-raii/SKILL.md +23 -0
- package/skills/cron-job/SKILL.md +23 -0
- package/skills/cv-preprocess/SKILL.md +24 -0
- package/skills/dark-mode/SKILL.md +24 -0
- package/skills/dashboard/SKILL.md +23 -0
- package/skills/dashboard-ui/SKILL.md +23 -0
- package/skills/data-export/SKILL.md +23 -0
- package/skills/data-validation/SKILL.md +23 -0
- package/skills/dataframe/SKILL.md +23 -0
- package/skills/db-index/SKILL.md +24 -0
- package/skills/dead-code/SKILL.md +23 -0
- package/skills/debug/SKILL.md +24 -0
- package/skills/deck-review/SKILL.md +24 -0
- package/skills/dedupe/SKILL.md +23 -0
- package/skills/dedupe-deps/SKILL.md +23 -0
- package/skills/dependency-audit/SKILL.md +23 -0
- package/skills/dependency-update/SKILL.md +23 -0
- package/skills/deploy/SKILL.md +23 -0
- package/skills/design-system/SKILL.md +24 -0
- package/skills/design-tokens/SKILL.md +24 -0
- package/skills/diagram-as-code/SKILL.md +24 -0
- package/skills/diff-explain/SKILL.md +23 -0
- package/skills/django-view/SKILL.md +23 -0
- package/skills/doc-lint/SKILL.md +24 -0
- package/skills/docker-compose/SKILL.md +23 -0
- package/skills/dockerize/SKILL.md +23 -0
- package/skills/docstrings/SKILL.md +23 -0
- package/skills/dotfiles/SKILL.md +23 -0
- package/skills/dpi-scaling/SKILL.md +23 -0
- package/skills/e2e-test/SKILL.md +23 -0
- package/skills/embeddings/SKILL.md +23 -0
- package/skills/empty-states/SKILL.md +23 -0
- package/skills/env-setup/SKILL.md +23 -0
- package/skills/erc20/SKILL.md +24 -0
- package/skills/error-tracking/SKILL.md +23 -0
- package/skills/estimate/SKILL.md +23 -0
- package/skills/etl-pipeline/SKILL.md +24 -0
- package/skills/eval-harness/SKILL.md +23 -0
- package/skills/exif-orientation/SKILL.md +23 -0
- package/skills/explain-code/SKILL.md +23 -0
- package/skills/express-middleware/SKILL.md +23 -0
- package/skills/extract-function/SKILL.md +23 -0
- package/skills/faq/SKILL.md +24 -0
- package/skills/fastapi-endpoint/SKILL.md +23 -0
- package/skills/favicon/SKILL.md +23 -0
- package/skills/feature-engineering/SKILL.md +23 -0
- package/skills/few-shot/SKILL.md +23 -0
- package/skills/find-owner/SKILL.md +23 -0
- package/skills/firmware-driver/SKILL.md +23 -0
- package/skills/fix-flaky-tests/SKILL.md +23 -0
- package/skills/flutter-widget/SKILL.md +23 -0
- package/skills/font-rendering/SKILL.md +23 -0
- package/skills/form-validation/SKILL.md +23 -0
- package/skills/format/SKILL.md +23 -0
- package/skills/game-loop/SKILL.md +23 -0
- package/skills/gas-optimize/SKILL.md +25 -0
- package/skills/gdpr-review/SKILL.md +24 -0
- package/skills/github-actions/SKILL.md +23 -0
- package/skills/glossary/SKILL.md +24 -0
- package/skills/go-idioms/SKILL.md +23 -0
- package/skills/gpu-profile/SKILL.md +23 -0
- package/skills/graphify/SKILL.md +21 -0
- package/skills/graphql-resolver/SKILL.md +23 -0
- package/skills/grpc-service/SKILL.md +23 -0
- package/skills/guardrails/SKILL.md +23 -0
- package/skills/healthcheck/SKILL.md +23 -0
- package/skills/heisenbug/SKILL.md +23 -0
- package/skills/helm-chart/SKILL.md +24 -0
- package/skills/hero-section/SKILL.md +23 -0
- package/skills/html-email/SKILL.md +24 -0
- package/skills/html-form/SKILL.md +23 -0
- package/skills/html-sanitize/SKILL.md +23 -0
- package/skills/html-table/SKILL.md +23 -0
- package/skills/html-to-pdf/SKILL.md +23 -0
- package/skills/http-client/SKILL.md +23 -0
- package/skills/i18n/SKILL.md +23 -0
- package/skills/i2c-spi/SKILL.md +23 -0
- package/skills/image-decode/SKILL.md +24 -0
- package/skills/image-memory/SKILL.md +24 -0
- package/skills/image-perf/SKILL.md +24 -0
- package/skills/image-pipeline/SKILL.md +24 -0
- package/skills/image-upload/SKILL.md +24 -0
- package/skills/infra-cost/SKILL.md +24 -0
- package/skills/input-validation/SKILL.md +23 -0
- package/skills/issue-template/SKILL.md +23 -0
- package/skills/java-streams/SKILL.md +23 -0
- package/skills/k8s-manifest/SKILL.md +23 -0
- package/skills/kotlin-coroutines/SKILL.md +23 -0
- package/skills/landing-page/SKILL.md +24 -0
- package/skills/laravel-controller/SKILL.md +23 -0
- package/skills/lazy-load/SKILL.md +23 -0
- package/skills/license-check/SKILL.md +23 -0
- package/skills/license-header/SKILL.md +23 -0
- package/skills/lint-fix/SKILL.md +23 -0
- package/skills/llm-cost/SKILL.md +23 -0
- package/skills/lockfile-fix/SKILL.md +23 -0
- package/skills/low-power/SKILL.md +23 -0
- package/skills/makefile/SKILL.md +23 -0
- package/skills/man-page/SKILL.md +24 -0
- package/skills/mcp-server/SKILL.md +23 -0
- package/skills/memory-leak/SKILL.md +23 -0
- package/skills/mermaid-diagram/SKILL.md +23 -0
- package/skills/meta-tags/SKILL.md +23 -0
- package/skills/micro-interactions/SKILL.md +23 -0
- package/skills/migration/SKILL.md +23 -0
- package/skills/migration-guide/SKILL.md +24 -0
- package/skills/mkdocs-setup/SKILL.md +24 -0
- package/skills/mobile-permissions/SKILL.md +23 -0
- package/skills/mock-api/SKILL.md +23 -0
- package/skills/modernize/SKILL.md +23 -0
- package/skills/monorepo-setup/SKILL.md +23 -0
- package/skills/motion-design/SKILL.md +23 -0
- package/skills/n-plus-one/SKILL.md +23 -0
- package/skills/naming-review/SKILL.md +23 -0
- package/skills/nextjs-route/SKILL.md +23 -0
- package/skills/nginx-config/SKILL.md +23 -0
- package/skills/ocr-debug/SKILL.md +24 -0
- package/skills/onboard/SKILL.md +23 -0
- package/skills/onboarding-map/SKILL.md +23 -0
- package/skills/open-pr/SKILL.md +24 -0
- package/skills/openapi/SKILL.md +23 -0
- package/skills/opencv-debug/SKILL.md +24 -0
- package/skills/orm-model/SKILL.md +23 -0
- package/skills/owasp-check/SKILL.md +24 -0
- package/skills/page-transitions/SKILL.md +23 -0
- package/skills/pagination/SKILL.md +23 -0
- package/skills/perf-optimize/SKILL.md +23 -0
- package/skills/perf-profile/SKILL.md +23 -0
- package/skills/physics/SKILL.md +23 -0
- package/skills/pitch-deck/SKILL.md +24 -0
- package/skills/pixel-diff/SKILL.md +23 -0
- package/skills/ponytail/SKILL.md +46 -0
- package/skills/postmortem/SKILL.md +24 -0
- package/skills/pptx-deck/SKILL.md +23 -0
- package/skills/pptx-export/SKILL.md +23 -0
- package/skills/pptx-from-markdown/SKILL.md +23 -0
- package/skills/pptx-template/SKILL.md +24 -0
- package/skills/pr-review/SKILL.md +24 -0
- package/skills/precommit/SKILL.md +23 -0
- package/skills/pricing-page/SKILL.md +23 -0
- package/skills/project-overview/SKILL.md +22 -0
- package/skills/prompt-template/SKILL.md +23 -0
- package/skills/property-test/SKILL.md +23 -0
- package/skills/protobuf/SKILL.md +23 -0
- package/skills/py-async/SKILL.md +23 -0
- package/skills/py-typing/SKILL.md +23 -0
- package/skills/query-optimize/SKILL.md +23 -0
- package/skills/rag-pipeline/SKILL.md +23 -0
- package/skills/rails-resource/SKILL.md +23 -0
- package/skills/rate-limit/SKILL.md +23 -0
- package/skills/react-hooks/SKILL.md +23 -0
- package/skills/react-native-screen/SKILL.md +23 -0
- package/skills/react-perf/SKILL.md +23 -0
- package/skills/readme/SKILL.md +24 -0
- package/skills/rebase/SKILL.md +24 -0
- package/skills/refactor/SKILL.md +23 -0
- package/skills/regression-test/SKILL.md +23 -0
- package/skills/release-notes/SKILL.md +24 -0
- package/skills/rename-symbol/SKILL.md +23 -0
- package/skills/repro/SKILL.md +23 -0
- package/skills/resolve-conflicts/SKILL.md +23 -0
- package/skills/responsive/SKILL.md +23 -0
- package/skills/rest-endpoint/SKILL.md +23 -0
- package/skills/retro/SKILL.md +23 -0
- package/skills/rtos-task/SKILL.md +23 -0
- package/skills/runbook/SKILL.md +25 -0
- package/skills/rust-borrow/SKILL.md +23 -0
- package/skills/rust-unsafe-audit/SKILL.md +23 -0
- package/skills/sanitize/SKILL.md +23 -0
- package/skills/schema-design/SKILL.md +23 -0
- package/skills/screenshot-debug/SKILL.md +22 -0
- package/skills/scroll-animation/SKILL.md +23 -0
- package/skills/secret-scan/SKILL.md +23 -0
- package/skills/security-audit/SKILL.md +23 -0
- package/skills/security-review/SKILL.md +23 -0
- package/skills/seed-data/SKILL.md +23 -0
- package/skills/self-review/SKILL.md +23 -0
- package/skills/semantic-html/SKILL.md +23 -0
- package/skills/semver-bump/SKILL.md +24 -0
- package/skills/shader/SKILL.md +23 -0
- package/skills/shader-debug/SKILL.md +23 -0
- package/skills/simplify-conditionals/SKILL.md +23 -0
- package/skills/sitemap/SKILL.md +23 -0
- package/skills/skeleton-loader/SKILL.md +23 -0
- package/skills/slide-charts/SKILL.md +24 -0
- package/skills/slide-outline/SKILL.md +23 -0
- package/skills/snapshot-update/SKILL.md +23 -0
- package/skills/solidity-contract/SKILL.md +25 -0
- package/skills/speaker-notes/SKILL.md +23 -0
- package/skills/spike/SKILL.md +23 -0
- package/skills/split-file/SKILL.md +23 -0
- package/skills/spring-controller/SKILL.md +23 -0
- package/skills/sprite-anim/SKILL.md +23 -0
- package/skills/sql-report/SKILL.md +23 -0
- package/skills/squash/SKILL.md +24 -0
- package/skills/ssl-setup/SKILL.md +23 -0
- package/skills/stacktrace/SKILL.md +23 -0
- package/skills/static-site/SKILL.md +24 -0
- package/skills/structured-logging/SKILL.md +23 -0
- package/skills/svelte-store/SKILL.md +23 -0
- package/skills/swiftui-view/SKILL.md +23 -0
- package/skills/tailwind-theme/SKILL.md +24 -0
- package/skills/tcp-server/SKILL.md +23 -0
- package/skills/tdd/SKILL.md +23 -0
- package/skills/terraform-module/SKILL.md +24 -0
- package/skills/test-coverage/SKILL.md +23 -0
- package/skills/texture-debug/SKILL.md +23 -0
- package/skills/threat-model/SKILL.md +23 -0
- package/skills/thumbnail/SKILL.md +24 -0
- package/skills/todo-scan/SKILL.md +23 -0
- package/skills/tool-definition/SKILL.md +23 -0
- package/skills/trace-flow/SKILL.md +23 -0
- package/skills/tracing/SKILL.md +23 -0
- package/skills/train-model/SKILL.md +24 -0
- package/skills/tree-shake/SKILL.md +23 -0
- package/skills/ts-generics/SKILL.md +23 -0
- package/skills/ts-strict/SKILL.md +23 -0
- package/skills/tui-app/SKILL.md +23 -0
- package/skills/tutorial/SKILL.md +24 -0
- package/skills/type-tighten/SKILL.md +23 -0
- package/skills/typography/SKILL.md +24 -0
- package/skills/ui-bug-repro/SKILL.md +23 -0
- package/skills/ui-polish/SKILL.md +24 -0
- package/skills/ui-review/SKILL.md +24 -0
- package/skills/vendor/SKILL.md +23 -0
- package/skills/visual-diff-ci/SKILL.md +24 -0
- package/skills/visual-regression/SKILL.md +23 -0
- package/skills/vue-composition/SKILL.md +23 -0
- package/skills/web-component/SKILL.md +23 -0
- package/skills/web-fonts/SKILL.md +24 -0
- package/skills/web3-frontend/SKILL.md +25 -0
- package/skills/webgl-debug/SKILL.md +23 -0
- package/skills/webhook/SKILL.md +23 -0
- package/skills/websocket/SKILL.md +23 -0
- package/skills/write-tests/SKILL.md +19 -0
- package/src/client/agent.ts +803 -0
- package/src/client/background.ts +39 -0
- package/src/client/checkpoint.ts +48 -0
- package/src/client/cli.ts +1253 -0
- package/src/client/compaction.ts +86 -0
- package/src/client/extensions.ts +83 -0
- package/src/client/hooks.ts +40 -0
- package/src/client/image.ts +26 -0
- package/src/client/lsp.ts +0 -0
- package/src/client/mcp.ts +276 -0
- package/src/client/models-dev.ts +52 -0
- package/src/client/pkg.ts +41 -0
- package/src/client/platform.ts +94 -0
- package/src/client/prompts.ts +47 -0
- package/src/client/render.ts +138 -0
- package/src/client/session.ts +107 -0
- package/src/client/settings.ts +86 -0
- package/src/client/skill-router.ts +79 -0
- package/src/client/skills.ts +199 -0
- package/src/client/snapshot.ts +56 -0
- package/src/client/telemetry.ts +24 -0
- package/src/client/todos.ts +23 -0
- package/src/client/tools.ts +756 -0
- package/src/client/tui-mode.ts +41 -0
- package/src/client/tui.ts +224 -0
- package/src/sdk/index.ts +36 -0
- package/src/selfcheck.ts +364 -0
- package/src/server/config.ts +58 -0
- package/src/server/credentials.ts +89 -0
- package/src/server/identity.ts +58 -0
- package/src/server/index.ts +113 -0
- package/src/server/oauth.ts +93 -0
- package/src/server/providers/adapter.ts +25 -0
- package/src/server/providers/anthropic.ts +189 -0
- package/src/server/providers/openai-compat.ts +76 -0
- package/src/server/providers/registry.ts +31 -0
- package/src/server/router.ts +29 -0
- package/src/server/sse.ts +20 -0
- package/src/shared/types.ts +20 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Context management (pi-style compaction). When the transcript grows large, summarize the
|
|
2
|
+
// older turns into one compact summary and keep the recent ones, so a session can run forever.
|
|
3
|
+
// The summary is produced by the same model via the backend. Token sizing is a chars/4 estimate
|
|
4
|
+
// (no tokenizer dependency) — accurate enough to decide *when* to compact.
|
|
5
|
+
|
|
6
|
+
import type OpenAI from "openai";
|
|
7
|
+
|
|
8
|
+
type Msg = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
|
9
|
+
|
|
10
|
+
// Cap on the text fed to the summarizer, so the summary call itself can't overflow the context.
|
|
11
|
+
const SUMMARY_INPUT_MAX = 30_000;
|
|
12
|
+
|
|
13
|
+
export function estimateTokens(messages: Msg[]): number {
|
|
14
|
+
let chars = 0;
|
|
15
|
+
for (const m of messages) chars += JSON.stringify(m).length;
|
|
16
|
+
return Math.ceil(chars / 4); // ponytail: chars/4 heuristic, not a real tokenizer — fine for a threshold
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isContextOverflowError(e: unknown): boolean {
|
|
20
|
+
const s = (e instanceof Error ? e.message : String(e)).toLowerCase();
|
|
21
|
+
return /context|max(imum)?[ _-]*tokens?|too long|exceeds|context[_ ]length|reduce the length|prompt is too/.test(s);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Pure: split messages into { system, toSummarize, tail } at a clean turn boundary, or null when
|
|
25
|
+
* there isn't enough to compact. `tail` always starts at a user message, so a tool_call/tool_result
|
|
26
|
+
* pair is never split across the cut. */
|
|
27
|
+
export function planCut(
|
|
28
|
+
messages: Msg[],
|
|
29
|
+
keepLast = 6,
|
|
30
|
+
): { system: Msg | null; toSummarize: Msg[]; tail: Msg[] } | null {
|
|
31
|
+
const system = messages[0]?.role === "system" ? messages[0]! : null;
|
|
32
|
+
const body = system ? messages.slice(1) : messages;
|
|
33
|
+
if (body.length <= keepLast + 1) return null;
|
|
34
|
+
|
|
35
|
+
let cut = body.length - keepLast;
|
|
36
|
+
while (cut > 0 && body[cut]?.role !== "user") cut--; // snap the tail start back to a user message
|
|
37
|
+
if (cut <= 0) return null;
|
|
38
|
+
|
|
39
|
+
return { system, toSummarize: body.slice(0, cut), tail: body.slice(cut) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function serialize(messages: Msg[]): string {
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
for (const m of messages) {
|
|
45
|
+
let text = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? "(non-text content)" : "";
|
|
46
|
+
const calls = (m as { tool_calls?: Array<{ function?: { name?: string; arguments?: string } }> }).tool_calls;
|
|
47
|
+
if (calls) text += calls.map((c) => `\n[calls ${c.function?.name}(${c.function?.arguments ?? ""})]`).join("");
|
|
48
|
+
parts.push(`${m.role}: ${text}`.trim());
|
|
49
|
+
}
|
|
50
|
+
let out = parts.join("\n\n");
|
|
51
|
+
if (out.length > SUMMARY_INPUT_MAX) out = `[…older messages omitted…]\n\n${out.slice(-SUMMARY_INPUT_MAX)}`;
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Summarize the older messages and return a compacted transcript: [system, summary, ...tail]. */
|
|
56
|
+
export async function compact(
|
|
57
|
+
client: OpenAI,
|
|
58
|
+
model: string,
|
|
59
|
+
messages: Msg[],
|
|
60
|
+
keepLast = 6,
|
|
61
|
+
): Promise<{ messages: Msg[]; summary: string } | null> {
|
|
62
|
+
const plan = planCut(messages, keepLast);
|
|
63
|
+
if (!plan) return null;
|
|
64
|
+
|
|
65
|
+
// Stream the summary (works for every adapter — anthropic always streams, openai-compat too).
|
|
66
|
+
const stream = await client.chat.completions.create({
|
|
67
|
+
model,
|
|
68
|
+
stream: true,
|
|
69
|
+
messages: [
|
|
70
|
+
{
|
|
71
|
+
role: "system",
|
|
72
|
+
content:
|
|
73
|
+
"You compress a coding agent's conversation into a concise summary so the task can continue. " +
|
|
74
|
+
"Preserve: the user's goal, key decisions, files created/edited (with paths), important command " +
|
|
75
|
+
"results, and remaining TODOs. Be terse and factual. Output only the summary.",
|
|
76
|
+
},
|
|
77
|
+
{ role: "user", content: `${serialize(plan.toSummarize)}\n\n---\nSummarize the conversation above.` },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
let summary = "";
|
|
81
|
+
for await (const chunk of stream) summary += chunk.choices[0]?.delta?.content ?? "";
|
|
82
|
+
summary = summary.trim() || "(summary unavailable)";
|
|
83
|
+
|
|
84
|
+
const summaryMsg: Msg = { role: "user", content: `[Summary of earlier conversation in this session]\n${summary}` };
|
|
85
|
+
return { messages: [...(plan.system ? [plan.system] : []), summaryMsg, ...plan.tail], summary };
|
|
86
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Extensions: JS/TS in .ada/extensions/ (project) or ~/.ada/extensions/ (global). An entry can be
|
|
2
|
+
// a file (foo.ts) or a directory (package.json "main", else index.{ts,js,mjs}) — so `ada add`
|
|
3
|
+
// can clone/install whole packages. Each default-exports { name?, tools?, onStart? }.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { registerTool, type Tool } from "./tools.ts";
|
|
10
|
+
import { addHook, type ToolHooks } from "./hooks.ts";
|
|
11
|
+
|
|
12
|
+
export interface Command {
|
|
13
|
+
name: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
run(args: string): string | void | Promise<string | void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Extension {
|
|
19
|
+
name?: string;
|
|
20
|
+
tools?: Tool[];
|
|
21
|
+
hooks?: ToolHooks;
|
|
22
|
+
commands?: Command[];
|
|
23
|
+
onStart?: () => void | Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const commands = new Map<string, Command>();
|
|
27
|
+
export function getCommands(): Map<string, Command> {
|
|
28
|
+
return commands;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extDirs(includeProject: boolean): string[] {
|
|
32
|
+
const global = resolve(homedir(), ".ada", "extensions");
|
|
33
|
+
return includeProject ? [resolve(process.cwd(), ".ada", "extensions"), global] : [global];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Resolve an entry (file or directory) to an importable module URL, or null. */
|
|
37
|
+
function entryUrl(full: string): string | null {
|
|
38
|
+
try {
|
|
39
|
+
if (statSync(full).isDirectory()) {
|
|
40
|
+
const pkg = join(full, "package.json");
|
|
41
|
+
if (existsSync(pkg)) {
|
|
42
|
+
const main = (JSON.parse(readFileSync(pkg, "utf8")) as { main?: string }).main;
|
|
43
|
+
if (main && existsSync(join(full, main))) return pathToFileURL(join(full, main)).href;
|
|
44
|
+
}
|
|
45
|
+
for (const idx of ["index.ts", "index.js", "index.mjs"]) {
|
|
46
|
+
if (existsSync(join(full, idx))) return pathToFileURL(join(full, idx)).href;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return /\.(ts|js|mjs)$/.test(full) ? pathToFileURL(full).href : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function loadExtensions(includeProject: boolean): Promise<string[]> {
|
|
57
|
+
const loaded: string[] = [];
|
|
58
|
+
for (const dir of extDirs(includeProject)) {
|
|
59
|
+
if (!existsSync(dir)) continue;
|
|
60
|
+
let entries: string[];
|
|
61
|
+
try {
|
|
62
|
+
entries = readdirSync(dir);
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const e of entries) {
|
|
67
|
+
const url = entryUrl(join(dir, e));
|
|
68
|
+
if (!url) continue;
|
|
69
|
+
try {
|
|
70
|
+
const ext = ((await import(url)) as { default?: Extension }).default;
|
|
71
|
+
if (!ext) continue;
|
|
72
|
+
for (const t of ext.tools ?? []) registerTool(t);
|
|
73
|
+
if (ext.hooks) addHook(ext.hooks);
|
|
74
|
+
for (const c of ext.commands ?? []) commands.set(c.name, c);
|
|
75
|
+
await ext.onStart?.();
|
|
76
|
+
loaded.push(ext.name ?? e);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(`extension ${e} failed: ${err instanceof Error ? err.message : err}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return loaded;
|
|
83
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Extension hook registry: transform user input, and intercept tool calls (deny / rewrite args /
|
|
2
|
+
// post-process results). Extensions register a ToolHooks object; the agent runs them in order.
|
|
3
|
+
|
|
4
|
+
import type { ToolResult } from "./tools.ts";
|
|
5
|
+
|
|
6
|
+
export interface ToolHooks {
|
|
7
|
+
onUserMessage?(text: string): string | undefined | Promise<string | undefined>;
|
|
8
|
+
beforeTool?(
|
|
9
|
+
name: string,
|
|
10
|
+
args: Record<string, unknown>,
|
|
11
|
+
): { deny?: string; args?: Record<string, unknown> } | void | Promise<{ deny?: string; args?: Record<string, unknown> } | void>;
|
|
12
|
+
afterTool?(name: string, args: Record<string, unknown>, result: ToolResult): ToolResult | undefined | Promise<ToolResult | undefined>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const hooks: ToolHooks[] = [];
|
|
16
|
+
|
|
17
|
+
export function addHook(h: ToolHooks): void {
|
|
18
|
+
hooks.push(h);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function transformInput(text: string): Promise<string> {
|
|
22
|
+
for (const h of hooks) if (h.onUserMessage) text = (await h.onUserMessage(text)) ?? text;
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function beforeTool(name: string, args: Record<string, unknown>): Promise<{ deny?: string; args: Record<string, unknown> }> {
|
|
27
|
+
let cur = args;
|
|
28
|
+
for (const h of hooks) {
|
|
29
|
+
if (!h.beforeTool) continue;
|
|
30
|
+
const r = await h.beforeTool(name, cur);
|
|
31
|
+
if (r?.deny) return { deny: r.deny, args: cur };
|
|
32
|
+
if (r?.args) cur = r.args;
|
|
33
|
+
}
|
|
34
|
+
return { args: cur };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function afterTool(name: string, args: Record<string, unknown>, result: ToolResult): Promise<ToolResult> {
|
|
38
|
+
for (const h of hooks) if (h.afterTool) result = (await h.afterTool(name, args, result)) ?? result;
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Load an image file into a data URL for multimodal messages (OpenAI image_url format).
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { extname, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
const MIME: Record<string, string> = {
|
|
7
|
+
".png": "image/png",
|
|
8
|
+
".jpg": "image/jpeg",
|
|
9
|
+
".jpeg": "image/jpeg",
|
|
10
|
+
".gif": "image/gif",
|
|
11
|
+
".webp": "image/webp",
|
|
12
|
+
".bmp": "image/bmp",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function loadImage(path: string): { dataUrl: string; bytes: number; name: string } | null {
|
|
16
|
+
const abs = resolve(process.cwd(), path);
|
|
17
|
+
if (!existsSync(abs)) return null;
|
|
18
|
+
const mime = MIME[extname(abs).toLowerCase()];
|
|
19
|
+
if (!mime) return null;
|
|
20
|
+
try {
|
|
21
|
+
const buf = readFileSync(abs);
|
|
22
|
+
return { dataUrl: `data:${mime};base64,${buf.toString("base64")}`, bytes: buf.length, name: path };
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// Minimal MCP client (stdio, JSON-RPC 2.0). Reads .ada/mcp.json, spawns each server, lists its
|
|
2
|
+
// tools, and registers them as ada tools (prefixed `<server>__<tool>`, gated behind approval).
|
|
3
|
+
// Config: { "servers": { "fs": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."] } } }
|
|
4
|
+
|
|
5
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
import { registerTool } from "./tools.ts";
|
|
9
|
+
|
|
10
|
+
interface RpcClient {
|
|
11
|
+
call(method: string, params?: unknown): Promise<Record<string, unknown>>;
|
|
12
|
+
notify(method: string, params?: unknown): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function makeClient(proc: ChildProcess): RpcClient {
|
|
16
|
+
let nextId = 1;
|
|
17
|
+
const pending = new Map<number, { resolve: (v: Record<string, unknown>) => void; reject: (e: Error) => void }>();
|
|
18
|
+
let buf = "";
|
|
19
|
+
proc.stdout?.on("data", (d: Buffer) => {
|
|
20
|
+
buf += d.toString("utf8");
|
|
21
|
+
let nl: number;
|
|
22
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
23
|
+
const line = buf.slice(0, nl).trim();
|
|
24
|
+
buf = buf.slice(nl + 1);
|
|
25
|
+
if (!line) continue;
|
|
26
|
+
try {
|
|
27
|
+
const msg = JSON.parse(line) as { id?: number; result?: Record<string, unknown>; error?: { message?: string } };
|
|
28
|
+
if (msg.id != null && pending.has(msg.id)) {
|
|
29
|
+
const p = pending.get(msg.id)!;
|
|
30
|
+
pending.delete(msg.id);
|
|
31
|
+
if (msg.error) p.reject(new Error(msg.error.message ?? "rpc error"));
|
|
32
|
+
else p.resolve(msg.result ?? {});
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
/* servers sometimes log non-JSON to stdout — ignore */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const send = (obj: unknown): void => void proc.stdin?.write(`${JSON.stringify(obj)}\n`);
|
|
40
|
+
return {
|
|
41
|
+
call(method, params) {
|
|
42
|
+
const id = nextId++;
|
|
43
|
+
return new Promise((res, rej) => {
|
|
44
|
+
pending.set(id, { resolve: res, reject: rej });
|
|
45
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
notify(method, params) {
|
|
49
|
+
send({ jsonrpc: "2.0", method, params });
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Streamable-HTTP MCP client: POST JSON-RPC, read a JSON or SSE response.
|
|
55
|
+
// ponytail: request/response only — no server-initiated notifications, no stream resumability.
|
|
56
|
+
function makeHttpClient(url: string, headers: Record<string, string>): RpcClient {
|
|
57
|
+
let nextId = 1;
|
|
58
|
+
let sessionId: string | undefined;
|
|
59
|
+
const post = (body: unknown): Promise<Response> =>
|
|
60
|
+
fetch(url, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "content-type": "application/json", accept: "application/json, text/event-stream", ...(sessionId ? { "mcp-session-id": sessionId } : {}), ...headers },
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
});
|
|
65
|
+
const take = (msg: { result?: Record<string, unknown>; error?: { message?: string } }): Record<string, unknown> => {
|
|
66
|
+
if (msg.error) throw new Error(msg.error.message ?? "rpc error");
|
|
67
|
+
return msg.result ?? {};
|
|
68
|
+
};
|
|
69
|
+
const readResult = async (res: Response, id: number): Promise<Record<string, unknown>> => {
|
|
70
|
+
const sid = res.headers.get("mcp-session-id");
|
|
71
|
+
if (sid) sessionId = sid;
|
|
72
|
+
if (!(res.headers.get("content-type") ?? "").includes("text/event-stream")) return take((await res.json()) as Parameters<typeof take>[0]);
|
|
73
|
+
const reader = res.body!.getReader();
|
|
74
|
+
const dec = new TextDecoder();
|
|
75
|
+
let buf = "";
|
|
76
|
+
let data = "";
|
|
77
|
+
for (;;) {
|
|
78
|
+
const { done, value } = await reader.read();
|
|
79
|
+
if (done) break;
|
|
80
|
+
buf += dec.decode(value, { stream: true });
|
|
81
|
+
let nl: number;
|
|
82
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
83
|
+
let line = buf.slice(0, nl);
|
|
84
|
+
buf = buf.slice(nl + 1);
|
|
85
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
86
|
+
if (line.startsWith("data:")) {
|
|
87
|
+
data += line.slice(5).replace(/^ /, "");
|
|
88
|
+
} else if (line === "" && data) {
|
|
89
|
+
let msg: { id?: number; result?: Record<string, unknown>; error?: { message?: string } } | undefined;
|
|
90
|
+
try {
|
|
91
|
+
msg = JSON.parse(data) as typeof msg;
|
|
92
|
+
} catch {
|
|
93
|
+
msg = undefined;
|
|
94
|
+
}
|
|
95
|
+
data = "";
|
|
96
|
+
if (msg && msg.id === id) {
|
|
97
|
+
await reader.cancel();
|
|
98
|
+
return take(msg);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw new Error("stream ended without a matching response");
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
async call(method, params) {
|
|
107
|
+
const id = nextId++;
|
|
108
|
+
const res = await post({ jsonrpc: "2.0", id, method, params });
|
|
109
|
+
if (!res.ok) throw new Error(`http ${res.status}`);
|
|
110
|
+
return readResult(res, id);
|
|
111
|
+
},
|
|
112
|
+
notify(method, params) {
|
|
113
|
+
void post({ jsonrpc: "2.0", method, params }).catch(() => {});
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface McpServerDef {
|
|
119
|
+
command?: string;
|
|
120
|
+
args?: string[];
|
|
121
|
+
env?: Record<string, string>;
|
|
122
|
+
url?: string; // remote MCP server (Streamable HTTP) instead of a local stdio command
|
|
123
|
+
headers?: Record<string, string>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function loadMcpServers(includeProject: boolean): Promise<string[]> {
|
|
127
|
+
if (!includeProject) return []; // MCP servers run code — trusted projects only
|
|
128
|
+
const cfgPath = resolve(process.cwd(), ".ada", "mcp.json");
|
|
129
|
+
if (!existsSync(cfgPath)) return [];
|
|
130
|
+
let cfg: { servers?: Record<string, McpServerDef> };
|
|
131
|
+
try {
|
|
132
|
+
cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
133
|
+
} catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
const loaded: string[] = [];
|
|
137
|
+
for (const [name, def] of Object.entries(cfg.servers ?? {})) {
|
|
138
|
+
try {
|
|
139
|
+
let rpc: RpcClient;
|
|
140
|
+
if (def.url) {
|
|
141
|
+
rpc = makeHttpClient(def.url, def.headers ?? {});
|
|
142
|
+
} else if (def.command) {
|
|
143
|
+
rpc = makeClient(spawn(def.command, def.args ?? [], { env: { ...process.env, ...def.env }, stdio: ["pipe", "pipe", "ignore"] }));
|
|
144
|
+
} else {
|
|
145
|
+
console.error(`mcp ${name}: needs a "command" (stdio) or "url" (http)`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
await rpc.call("initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "ada", version: "0.0.1" } });
|
|
149
|
+
rpc.notify("notifications/initialized");
|
|
150
|
+
const list = await rpc.call("tools/list", {});
|
|
151
|
+
const mcpTools = (list.tools as Array<Record<string, unknown>>) ?? [];
|
|
152
|
+
for (const t of mcpTools) {
|
|
153
|
+
const toolName = String(t.name);
|
|
154
|
+
registerTool({
|
|
155
|
+
name: `${name}__${toolName}`,
|
|
156
|
+
description: String(t.description ?? `${name} tool ${toolName}`),
|
|
157
|
+
parameters: (t.inputSchema as Record<string, unknown>) ?? { type: "object", properties: {} },
|
|
158
|
+
needsApproval: true,
|
|
159
|
+
async run(args) {
|
|
160
|
+
try {
|
|
161
|
+
const res = await rpc.call("tools/call", { name: toolName, arguments: args });
|
|
162
|
+
const content = (res.content as Array<Record<string, unknown>>) ?? [];
|
|
163
|
+
const text = content.map((c) => (c.text != null ? String(c.text) : JSON.stringify(c))).join("\n");
|
|
164
|
+
return { output: text || "(no content)", isError: !!res.isError };
|
|
165
|
+
} catch (e) {
|
|
166
|
+
return { output: String(e), isError: true };
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// Resources (optional): expose a read_resource tool listing the server's resource URIs.
|
|
172
|
+
try {
|
|
173
|
+
const rl = await rpc.call("resources/list", {});
|
|
174
|
+
const resources = (rl.resources as Array<{ uri: string; name?: string }>) ?? [];
|
|
175
|
+
if (resources.length) {
|
|
176
|
+
registerTool({
|
|
177
|
+
name: `${name}__read_resource`,
|
|
178
|
+
description: `Read a resource from ${name}. Available URIs: ${resources.slice(0, 30).map((r) => (r.name ? `${r.uri} (${r.name})` : r.uri)).join("; ")}`,
|
|
179
|
+
parameters: { type: "object", properties: { uri: { type: "string" } }, required: ["uri"], additionalProperties: false },
|
|
180
|
+
needsApproval: true,
|
|
181
|
+
async run(args) {
|
|
182
|
+
try {
|
|
183
|
+
const res = await rpc.call("resources/read", { uri: String(args.uri) });
|
|
184
|
+
const contents = (res.contents as Array<{ text?: string; blob?: string }>) ?? [];
|
|
185
|
+
return { output: contents.map((c) => c.text ?? (c.blob ? "[binary content]" : "")).join("\n") || "(empty)" };
|
|
186
|
+
} catch (e) {
|
|
187
|
+
return { output: String(e), isError: true };
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
loaded.push(`${name} (+${resources.length} resources)`);
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
/* server doesn't support resources */
|
|
195
|
+
}
|
|
196
|
+
loaded.push(`${name} (${mcpTools.length} tools)`);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error(`mcp ${name} failed: ${e instanceof Error ? e.message : e}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return loaded;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---- connector catalog + .ada/mcp.json management (`ada mcp …`) ----
|
|
205
|
+
|
|
206
|
+
// A curated set of popular MCP connectors. `ada mcp add <name>` drops the entry into .ada/mcp.json.
|
|
207
|
+
// ponytail: package names track the public MCP servers — adjust an entry if an upstream renames.
|
|
208
|
+
export const CATALOG: Record<string, { description: string; server: McpServerDef }> = {
|
|
209
|
+
filesystem: { description: "Local filesystem read/write", server: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."] } },
|
|
210
|
+
github: { description: "GitHub repos, issues, PRs", server: { command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], env: { GITHUB_PERSONAL_ACCESS_TOKEN: "" } } },
|
|
211
|
+
git: { description: "Local git repository operations", server: { command: "npx", args: ["-y", "@modelcontextprotocol/server-git", "--repository", "."] } },
|
|
212
|
+
postgres: { description: "Postgres (read-only SQL)", server: { command: "npx", args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/postgres"] } },
|
|
213
|
+
sqlite: { description: "SQLite database", server: { command: "npx", args: ["-y", "mcp-server-sqlite", "--db-path", "./db.sqlite"] } },
|
|
214
|
+
fetch: { description: "Fetch and convert web pages", server: { command: "npx", args: ["-y", "@modelcontextprotocol/server-fetch"] } },
|
|
215
|
+
"brave-search": { description: "Web search via Brave", server: { command: "npx", args: ["-y", "@modelcontextprotocol/server-brave-search"], env: { BRAVE_API_KEY: "" } } },
|
|
216
|
+
puppeteer: { description: "Browser automation (Puppeteer)", server: { command: "npx", args: ["-y", "@modelcontextprotocol/server-puppeteer"] } },
|
|
217
|
+
slack: { description: "Slack channels and messages", server: { command: "npx", args: ["-y", "@modelcontextprotocol/server-slack"], env: { SLACK_BOT_TOKEN: "", SLACK_TEAM_ID: "" } } },
|
|
218
|
+
memory: { description: "Persistent knowledge-graph memory", server: { command: "npx", args: ["-y", "@modelcontextprotocol/server-memory"] } },
|
|
219
|
+
sentry: { description: "Sentry issues and events", server: { command: "npx", args: ["-y", "@modelcontextprotocol/server-sentry"], env: { SENTRY_AUTH_TOKEN: "" } } },
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function configPath(): string {
|
|
223
|
+
return resolve(process.cwd(), ".ada", "mcp.json");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readConfig(): { servers: Record<string, McpServerDef> } {
|
|
227
|
+
const p = configPath();
|
|
228
|
+
if (!existsSync(p)) return { servers: {} };
|
|
229
|
+
try {
|
|
230
|
+
const c = JSON.parse(readFileSync(p, "utf8")) as { servers?: Record<string, McpServerDef> };
|
|
231
|
+
return { servers: c.servers ?? {} };
|
|
232
|
+
} catch {
|
|
233
|
+
return { servers: {} };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function writeConfig(cfg: { servers: Record<string, McpServerDef> }): void {
|
|
238
|
+
const p = configPath();
|
|
239
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
240
|
+
writeFileSync(p, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Add a catalog connector to .ada/mcp.json. Returns the env vars the user still needs to set. */
|
|
244
|
+
export function addConnector(name: string): { ok: boolean; envVars: string[]; error?: string } {
|
|
245
|
+
const entry = CATALOG[name];
|
|
246
|
+
if (!entry) return { ok: false, envVars: [], error: `unknown connector "${name}" — run \`ada mcp\` to list the catalog` };
|
|
247
|
+
const cfg = readConfig();
|
|
248
|
+
cfg.servers[name] = entry.server;
|
|
249
|
+
writeConfig(cfg);
|
|
250
|
+
return { ok: true, envVars: Object.keys(entry.server.env ?? {}) };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Remove a connector from .ada/mcp.json. */
|
|
254
|
+
export function removeConnector(name: string): boolean {
|
|
255
|
+
const cfg = readConfig();
|
|
256
|
+
if (!cfg.servers[name]) return false;
|
|
257
|
+
delete cfg.servers[name];
|
|
258
|
+
writeConfig(cfg);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Names of the servers currently configured in .ada/mcp.json. */
|
|
263
|
+
export function configuredServers(): string[] {
|
|
264
|
+
return Object.keys(readConfig().servers);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The catalog annotated with whether each connector is already in .ada/mcp.json. */
|
|
268
|
+
export function listConnectors(): { name: string; description: string; configured: boolean; needsEnv: string[] }[] {
|
|
269
|
+
const cfg = readConfig();
|
|
270
|
+
return Object.entries(CATALOG).map(([name, e]) => ({
|
|
271
|
+
name,
|
|
272
|
+
description: e.description,
|
|
273
|
+
configured: !!cfg.servers[name],
|
|
274
|
+
needsEnv: Object.keys(e.server.env ?? {}),
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// models.dev catalog — model metadata (context limits, pricing, capabilities). Prefetched once at
|
|
2
|
+
// startup and cached for an hour; reads are synchronous from the in-memory cache. Offline-safe:
|
|
3
|
+
// if the fetch fails, the cache stays empty and callers fall back to their own tables.
|
|
4
|
+
|
|
5
|
+
interface Info {
|
|
6
|
+
context?: number;
|
|
7
|
+
output?: number;
|
|
8
|
+
inputCost?: number; // $ per 1M input tokens
|
|
9
|
+
outputCost?: number; // $ per 1M output tokens
|
|
10
|
+
reasoning?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const cache = new Map<string, Info>();
|
|
14
|
+
let fetchedAt = 0;
|
|
15
|
+
|
|
16
|
+
/** Fetch and cache the models.dev catalog (no-op if fetched within the last hour). */
|
|
17
|
+
export async function prefetch(): Promise<void> {
|
|
18
|
+
if (cache.size && Date.now() - fetchedAt < 3_600_000) return;
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch("https://models.dev/api.json", { signal: AbortSignal.timeout(10_000) });
|
|
21
|
+
if (!res.ok) return;
|
|
22
|
+
const data = (await res.json()) as Record<string, { models?: Record<string, { limit?: { context?: number; output?: number }; cost?: { input?: number; output?: number }; reasoning?: boolean }> }>;
|
|
23
|
+
cache.clear();
|
|
24
|
+
for (const prov of Object.values(data)) {
|
|
25
|
+
for (const [id, m] of Object.entries(prov.models ?? {})) {
|
|
26
|
+
cache.set(id, { context: m.limit?.context, output: m.limit?.output, inputCost: m.cost?.input, outputCost: m.cost?.output, reasoning: m.reasoning });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
fetchedAt = Date.now();
|
|
30
|
+
} catch {
|
|
31
|
+
/* offline — keep whatever's cached */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function lookup(modelId: string): Info | null {
|
|
36
|
+
return cache.get(modelId) ?? cache.get(modelId.split("/").pop() ?? "") ?? cache.get(modelId.split(":")[0] ?? "") ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** [inputCostPer1M, outputCostPer1M] from models.dev, or null. */
|
|
40
|
+
export function priceOf(modelId: string): [number, number] | null {
|
|
41
|
+
const i = lookup(modelId);
|
|
42
|
+
return i && i.inputCost != null && i.outputCost != null ? [i.inputCost, i.outputCost] : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Context-window limit (tokens) from models.dev, or null. */
|
|
46
|
+
export function contextOf(modelId: string): number | null {
|
|
47
|
+
return lookup(modelId)?.context ?? null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function catalogSize(): number {
|
|
51
|
+
return cache.size;
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Extension package manager: `ada add <git-url | npm-package>` installs into .ada/extensions/
|
|
2
|
+
// as a directory the extension loader can import.
|
|
3
|
+
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const EXT = resolve(process.cwd(), ".ada", "extensions");
|
|
10
|
+
|
|
11
|
+
export function addExtension(spec: string): void {
|
|
12
|
+
mkdirSync(EXT, { recursive: true });
|
|
13
|
+
|
|
14
|
+
if (spec.includes("://") || spec.startsWith("git@")) {
|
|
15
|
+
const name = (spec.split("/").pop() ?? "ext").replace(/\.git$/, "");
|
|
16
|
+
const r = spawnSync("git", ["clone", "--depth", "1", spec, join(EXT, name)], { stdio: "inherit" });
|
|
17
|
+
if (r.status !== 0) throw new Error("git clone failed");
|
|
18
|
+
console.log(`installed extension ${name} → .ada/extensions/${name}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// npm package: install into a self-contained dir + a re-export entry the loader imports.
|
|
23
|
+
const pkgName = spec.replace(/@[^@/]+$/, ""); // drop a trailing @version
|
|
24
|
+
const name = pkgName.replace(/^@/, "").replace(/\//g, "-");
|
|
25
|
+
const dir = join(EXT, name);
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
const r = spawnSync("npm", ["install", spec, "--prefix", dir], { stdio: "inherit", shell: process.platform === "win32" });
|
|
28
|
+
if (r.status !== 0) throw new Error("npm install failed");
|
|
29
|
+
writeFileSync(join(dir, "index.mjs"), `export { default } from ${JSON.stringify(pkgName)};\n`);
|
|
30
|
+
console.log(`installed extension ${name} (${pkgName}) → .ada/extensions/${name}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Self-update: ada runs from source, so update = git pull in the repo root. */
|
|
34
|
+
export function selfUpdate(): void {
|
|
35
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); // src/client/pkg.ts → repo root
|
|
36
|
+
const r = spawnSync("git", ["-C", root, "pull", "--ff-only"], { stdio: "inherit" });
|
|
37
|
+
if (r.status !== 0) {
|
|
38
|
+
console.error("self-update failed (is this a git checkout?)");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|